+ [ Scanning repository history... ]
+
@@ -63,17 +78,91 @@
The Ship of Theseus
-
Evolution Metrics
+
+ How much has changed?
+ ?
+
--
of original code replaced
-
Ancestry Analysis
+
+ How old is the code?
+ ?
+
+
+
+
--
+
Repository birth year
+
+
+
+ --
+
+
Earliest code still here
+
+
+
+
+
+
+ Which year's code survives most?
+ ?
+
+
+
--
+
+ Year with most surviving code
+
+
+
+
+
+ How fast is code being replaced?
+ ?
+
+
+
+ --
+
+
Lines replaced per month
+
+
+
+
+ How many times did the original codebase die?
+ ?
+
-
--
-
Oldest surviving logic (Year)
+
--
+
Complete rebuilds
+
+
+
+
+ What's the average code age?
+ ?
+
+
@@ -81,20 +170,111 @@
Ancestry Analysis
How to read this chart
-
The X-axis represents time moving forward. The Y-axis shows total lines of code.
- Each colored band represents the surviving code originally written in a specific year.
+
+ The X-axis shows time. The Y-axis shows total lines of
+ code. Each colored band represents code that was
+ originally written in a specific year.
+
-
The Architecture
-
This data is generated monthly via a static analysis pipeline. It dynamically scans
- the repository history and outputs a structured JSON file to minimize visual overhead in the
- browser.
+
How the data is collected
+
+ Every month, we analyze the repository and use git blame
+ to determine when each line was last modified. This
+ gives us a snapshot of how much original code survives
+ over time.
+
+
+
+
+
+ Ancient Code Fragments
+
+
+
+
+ --
+
+
+
+ Loading...
+
+
+
+
+
+ --
+
+
+
+ Loading...
+
+
+
+
+
+
+ Where did this all come from?
+
+
+ Honestly, I'm just a guy who spent a bit too much time
+ reading Plato and not enough time touching grass. This
+ project is basically what happens when you combine a bit
+ of a philosophy obsession with a healthy dose of data
+ engineering. I've always felt that data isn't just
+ numbers in a JSON file, it's a living record of
+ evolution, like a digital ancestry.
+
+
+
+ I wanted to see if I could apply the
+ Ship of Theseus
+ paradox to software. If you haven't heard of it, it's an
+ ancient Greek thought experiment that asks: if you
+ replace every single part of a ship, plank by plank, is
+ it still the same ship? Or is it just a new ship wearing
+ its ancestor's name tag?
+
+
+
+ We do this to codebases all the time. We refactor,
+ delete, and rewrite until the original 2013 'timber' is
+ long gone. This tool is my way of staring at that
+ Identity Problem without having to
+ write a 50-page thesis. It gives us a window into how
+ our projects are constantly being reborn. Is it still
+ the same repo? I have no idea, but the data is
+ fascinating, and looking at entropy is better than
+ staring at a blank terminal.
+
+
+
+ If you find this digital paradox as fascinating as I do,
+ consider dropping a ⭐ on
+ GitHub.
+ It helps keep the ship afloat!
+
+
+
+ — Asif Sayyed
+ Data Scientist who also happens to read far too
+ much philosophy
+
diff --git a/scripts/add_fossils.py b/scripts/add_fossils.py
new file mode 100644
index 0000000..a664b53
--- /dev/null
+++ b/scripts/add_fossils.py
@@ -0,0 +1,185 @@
+import json
+import os
+import subprocess
+import logging
+from pathlib import Path
+from calendar import monthrange
+from datetime import datetime, timezone
+from collections import defaultdict
+
+# Setup logging
+logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
+logger = logging.getLogger(__name__)
+
+
+def _run_command(cmd, cwd=None):
+ try:
+ result = subprocess.run(
+ cmd,
+ cwd=cwd,
+ capture_output=True,
+ text=True,
+ check=True,
+ encoding="utf-8",
+ errors="replace",
+ )
+ return result.stdout.strip()
+ except subprocess.CalledProcessError as e:
+ logger.error(f"Command failed: {' '.join(cmd)} - {e.stderr}")
+ raise RuntimeError(f"Command failed: {e.stderr}") from e
+
+
+def get_snapshot_commit(repo_path, date_str):
+ """Find the commit closest to the given date (YYYY-MM)."""
+ try:
+ year, month = map(int, date_str.split("-"))
+ _, last_day = monthrange(year, month)
+ search_date = f"{year}-{month:02d}-{last_day}"
+ commit = _run_command(
+ ["git", "rev-list", "-n", "1", f"--before={search_date}", "HEAD"],
+ cwd=repo_path,
+ )
+ if commit:
+ return commit
+ except Exception as e:
+ logger.error(f"Error finding commit for {date_str}: {e}")
+ return None
+
+
+def get_fossil_metadata(repo_path, commit_hash):
+ """Find the oldest line in the repository at a specific commit."""
+ if not commit_hash:
+ return {}
+
+ logger.info(f"Analyzing fossils for commit {commit_hash[:7]}...")
+ _run_command(["git", "checkout", "--force", commit_hash], cwd=repo_path)
+ files_output = _run_command(["git", "ls-files"], cwd=repo_path)
+ files = [
+ f
+ for f in files_output.splitlines()
+ if os.path.isfile(os.path.join(repo_path, f))
+ ]
+
+ oldest_fossil = {
+ "timestamp": 2147483647,
+ "file": "",
+ "content": "",
+ "year": "",
+ "commit": "",
+ "line": 0,
+ }
+
+ for file in files:
+ try:
+ blame_output = _run_command(
+ ["git", "blame", "--line-porcelain", file], cwd=repo_path
+ )
+ current_commit_data = {}
+ line_num = 0
+
+ for line in blame_output.splitlines():
+ if line.startswith("\t"):
+ line_num += 1
+ timestamp = current_commit_data.get("author-time")
+ if timestamp and timestamp < oldest_fossil["timestamp"]:
+ oldest_fossil["timestamp"] = timestamp
+ oldest_fossil["file"] = file
+ oldest_fossil["content"] = line.lstrip("\t").strip()
+ oldest_fossil["year"] = datetime.fromtimestamp(
+ timestamp, timezone.utc
+ ).strftime("%Y")
+ oldest_fossil["commit"] = current_commit_data.get("commit", "")[
+ :7
+ ]
+ oldest_fossil["line"] = line_num
+ else:
+ if line and line[0] != "\t":
+ commit_hash = line.split(" ")[0]
+ if len(commit_hash) == 40:
+ current_commit_data["commit"] = commit_hash
+ elif line.startswith("author-time "):
+ parts = line.split(" ")
+ if len(parts) >= 2:
+ current_commit_data["author-time"] = int(parts[1])
+ except Exception:
+ continue
+
+ return oldest_fossil
+
+
+def backfill_fossils(data_dir, repo_urls):
+ """
+ Iterates through data files and adds fossil metadata.
+ """
+ data_path = Path(data_dir)
+ json_files = list(data_path.glob("*.json"))
+ temp_dir = Path("./temp_fossil_repos")
+ temp_dir.mkdir(exist_ok=True)
+
+ for json_file in json_files:
+ if json_file.name == "manifest.json":
+ continue
+
+ repo_name = json_file.stem.replace("_data", "")
+ repo_url = repo_urls.get(repo_name)
+
+ if not repo_url:
+ logger.warning(f"No URL found for {repo_name}, skipping.")
+ continue
+
+ logger.info(f"Processing {repo_name}...")
+
+ # 1. Load data
+ with open(json_file, "r", encoding="utf-8") as f:
+ raw_data = json.load(f)
+ if isinstance(raw_data, list):
+ snapshots = raw_data
+ fossils = {}
+ else:
+ snapshots = raw_data.get("snapshots", [])
+ fossils = raw_data.get("fossils", {})
+
+ if not snapshots:
+ continue
+
+ # 2. Clone repo if needed
+ local_repo = temp_dir / repo_name
+ if not local_repo.exists():
+ logger.info(f"Cloning {repo_url}...")
+ _run_command(["git", "clone", repo_url, str(local_repo)])
+
+ # 3. Resolve and Get Fossils
+ try:
+ if not fossils.get("genesis"):
+ first_date = snapshots[0]["snapshot_date"]
+ first_commit = get_snapshot_commit(local_repo, first_date)
+ fossils["genesis"] = get_fossil_metadata(local_repo, first_commit)
+
+ if not fossils.get("survivor"):
+ last_date = snapshots[-1]["snapshot_date"]
+ last_commit = get_snapshot_commit(local_repo, last_date)
+ fossils["survivor"] = get_fossil_metadata(local_repo, last_commit)
+
+ # 4. Write back
+ with open(json_file, "w", encoding="utf-8") as f:
+ json.dump(
+ {"snapshots": snapshots, "fossils": fossils},
+ f,
+ separators=(",", ":"),
+ )
+ logger.info(f"Successfully backfilled fossils for {repo_name}")
+
+ except Exception as e:
+ logger.error(f"Error backfilling {repo_name}: {e}")
+
+
+if __name__ == "__main__":
+ # Registry of repo URLs
+ REPO_URLS = {
+ "zed": "https://github.com/zed-industries/zed.git",
+ "langchain": "https://github.com/langchain-ai/langchain.git",
+ "numpy": "https://github.com/numpy/numpy.git",
+ "react": "https://github.com/facebook/react.git",
+ "claude-code": "https://github.com/anthropics/claude-code.git",
+ }
+ backfill_fossils("./data", REPO_URLS)
diff --git a/scripts/analyse_repository.py b/scripts/analyse_repository.py
index 428e599..d6a832e 100644
--- a/scripts/analyse_repository.py
+++ b/scripts/analyse_repository.py
@@ -15,6 +15,7 @@
from collections import defaultdict
from datetime import datetime, timezone
from itertools import groupby
+from typing import Optional
logger = logging.getLogger(__name__)
@@ -184,28 +185,93 @@ def analyze_snapshots(repo_path: str, commit_hash: str) -> dict[str, int]:
return dict(age_distribution)
-def load_existing_state(json_fname: str) -> list[dict]:
+def load_existing_state(json_fname: str) -> dict:
"""
- Load the existing historical data to prevent redundant re-calculations.
+ Load the existing historical data supporting both old list and new object schemas.
:param json_fname: Path to the existing JSON file containing the historical data.
- :return: A list of dictionaries with the historical data.
+ :return: A dictionary with 'snapshots' and 'fossils'.
"""
if os.path.exists(json_fname):
try:
with open(json_fname, "r", encoding="utf-8") as f:
- return json.load(f)
+ data = json.load(f)
+ if isinstance(data, list):
+ return {"snapshots": data, "fossils": {}}
+ return data
except json.JSONDecodeError:
logger.warning("%s is corrupted, starting fresh.", json_fname)
- return []
- return []
+ return {"snapshots": [], "fossils": {}}
+ return {"snapshots": [], "fossils": {}}
-def _atomic_write_json(json_path: str, data: list[dict]) -> None:
- """Write JSON data atomically to prevent corruption on crash."""
+def _get_fossil_metadata(repo_path: str, commit_hash: str) -> dict:
+ """
+ Find the oldest line in the repository at a specific commit.
+ """
+ _run_command(["git", "checkout", commit_hash], cwd=repo_path)
+ files_output = _run_command(["git", "ls-files"], cwd=repo_path)
+ files = [
+ f
+ for f in files_output.splitlines()
+ if os.path.isfile(os.path.join(repo_path, f))
+ ]
+
+ oldest_fossil = {
+ "timestamp": 2147483647,
+ "file": "",
+ "content": "",
+ "year": "",
+ "commit": "",
+ "line": 0,
+ }
+
+ for file in files:
+ try:
+ blame_output = _run_command(
+ ["git", "blame", "--line-porcelain", file], cwd=repo_path
+ )
+ current_commit_data = {}
+ line_num = 0
+
+ for line in blame_output.splitlines():
+ if line.startswith("\t"):
+ line_num += 1
+ timestamp = current_commit_data.get("author-time")
+ if timestamp and timestamp < oldest_fossil["timestamp"]:
+ oldest_fossil["timestamp"] = timestamp
+ oldest_fossil["file"] = file
+ oldest_fossil["content"] = line.lstrip("\t").strip()
+ oldest_fossil["year"] = datetime.fromtimestamp(
+ timestamp, timezone.utc
+ ).strftime("%Y")
+ oldest_fossil["commit"] = current_commit_data.get("commit", "")[
+ :7
+ ]
+ oldest_fossil["line"] = line_num
+ else:
+ if line and line[0] != "\t":
+ parsed_commit_hash = line.split(" ")[0]
+ if len(parsed_commit_hash) == 40:
+ current_commit_data["commit"] = parsed_commit_hash
+ elif line.startswith("author-time "):
+ parts = line.split(" ")
+ if len(parts) >= 2:
+ current_commit_data["author-time"] = int(parts[1])
+ except Exception:
+ continue
+
+ return oldest_fossil
+
+
+def _atomic_write_json(
+ json_path: str, snapshots: list[dict], fossils: Optional[dict] = None
+) -> None:
+ """Write JSON data atomically and minified to prevent corruption and save space."""
tmp_path = json_path + ".tmp"
+ data = {"snapshots": snapshots, "fossils": fossils or {}}
with open(tmp_path, "w", encoding="utf-8") as f:
- json.dump(data, f, indent=4)
+ json.dump(data, f, separators=(",", ":"))
os.replace(tmp_path, json_path)
@@ -240,8 +306,10 @@ def process_repository(repo_slug: str, data_dir: str) -> None:
continue
_run_command(["git", "pull"], cwd=temp_repo_path)
- historical_data = load_existing_state(output_json_path)
- processed_periods = set(item["snapshot_date"] for item in historical_data)
+ state = load_existing_state(output_json_path)
+ historical_snapshots = state["snapshots"]
+ fossils = state["fossils"]
+ processed_periods = set(item["snapshot_date"] for item in historical_snapshots)
all_snapshots = get_snapshots(temp_repo_path)
new_snapshots = [
@@ -304,7 +372,6 @@ def process_repository(repo_slug: str, data_dir: str) -> None:
year_data.append(
{
"snapshot_date": period,
- "total_lines": sum(distribution.values()),
"composition": distribution,
}
)
@@ -312,16 +379,39 @@ def process_repository(repo_slug: str, data_dir: str) -> None:
total_new_data.extend(year_data)
year_elapsed = time.perf_counter() - year_start
- final_dataset = historical_data + total_new_data
- final_dataset.sort(key=lambda x: x["snapshot_date"])
- _atomic_write_json(output_json_path, final_dataset)
+ final_snapshots = historical_snapshots + total_new_data
+ final_snapshots.sort(key=lambda x: x["snapshot_date"])
+
+ # Updated Fossil Stage: Logic for Genesis vs Survivor
+ # Recalculate if fossils are missing or missing key fields (commit, file, line)
+ needs_genesis = not fossils.get("genesis", {}).get("commit")
+ needs_survivor = not fossils.get("survivor", {}).get("commit")
+
+ if final_snapshots and (needs_genesis or needs_survivor):
+ logger.info(
+ "[%s] Computing targeted fossils (Genesis and Survivor)", repo_name
+ )
+
+ if needs_genesis:
+ genesis_commit = all_snapshots[0][1]
+ fossils["genesis"] = _get_fossil_metadata(
+ temp_repo_path, genesis_commit
+ )
+
+ if needs_survivor:
+ survivor_commit = all_snapshots[-1][1]
+ fossils["survivor"] = _get_fossil_metadata(
+ temp_repo_path, survivor_commit
+ )
+
+ _atomic_write_json(output_json_path, final_snapshots, fossils)
logger.info(
- "[%s] Completed year %s in %.2f seconds. Wrote %d total snapshots to disk.",
+ "[%s] Completed year %s in %.2f seconds. Wrote %d snapshots to disk.",
repo_name,
year,
year_elapsed,
- len(final_dataset),
+ len(final_snapshots),
)
finally:
@@ -367,7 +457,13 @@ def handle_remove_readonly(func, path, exc_info):
DATA_OUTPUT_DIR = "./data"
os.makedirs(DATA_OUTPUT_DIR, exist_ok=True)
- TARGETS = ["anthropics/claude-code", "facebook/react", "langchain-ai/langchain"]
+ TARGETS = [
+ "anthropics/claude-code",
+ "facebook/react",
+ "langchain-ai/langchain",
+ "zed-industries/zed",
+ "numpy/numpy",
+ ]
# Bound top-level workers by CPU count
max_top_level_workers = min(
diff --git a/scripts/cleanup_data.py b/scripts/cleanup_data.py
new file mode 100644
index 0000000..70a0a7e
--- /dev/null
+++ b/scripts/cleanup_data.py
@@ -0,0 +1,64 @@
+import json
+import os
+from pathlib import Path
+
+
+def cleanup_data(data_dir: str):
+ """
+ Cleans up all JSON data files in the specified directory.
+ - Removes 'total_lines' (redundant)
+ - Removes future-year keys in 'composition'
+ - Minifies output
+ """
+ data_path = Path(data_dir)
+ json_files = list(data_path.glob("*.json"))
+
+ if not json_files:
+ print(f"No JSON files found in {data_dir}")
+ return
+
+ for json_file in json_files:
+ if json_file.name == "manifest.json":
+ continue
+
+ print(f"Processing {json_file.name}...")
+ try:
+ with open(json_file, "r", encoding="utf-8") as f:
+ data = json.load(f)
+
+ # Handle both list and object schemas
+ snapshots = data.get("snapshots", data) if isinstance(data, dict) else data
+
+ for snapshot in snapshots:
+ # 1. Remove redundant total_lines
+ if "total_lines" in snapshot:
+ del snapshot["total_lines"]
+
+ # 2. Filter future years
+ snapshot_date = snapshot.get("snapshot_date")
+ if snapshot_date:
+ max_year = int(snapshot_date[:4])
+ composition = snapshot.get("composition", {})
+ keys_to_remove = [
+ year for year in composition.keys() if int(year) > max_year
+ ]
+ for key in keys_to_remove:
+ del composition[key]
+
+ # Write back with original schema
+ if isinstance(data, dict):
+ data["snapshots"] = snapshots
+ with open(json_file, "w", encoding="utf-8") as f:
+ json.dump(data, f, separators=(",", ":"))
+ else:
+ with open(json_file, "w", encoding="utf-8") as f:
+ json.dump(snapshots, f, separators=(",", ":"))
+ print(f" Successfully optimized and minified {json_file.name}")
+
+ except Exception as e:
+ print(f" Error processing {json_file.name}: {e}")
+
+
+if __name__ == "__main__":
+ DATA_DIR = "./data"
+ cleanup_data(DATA_DIR)
diff --git a/style.css b/style.css
index c5481a8..9e9c634 100644
--- a/style.css
+++ b/style.css
@@ -1,14 +1,13 @@
:root {
- --bg-void: #0a0a0c;
- --text-primary: #e0e0e0;
- --text-secondary: #9ca3af;
- --accent-cyan: hsl(180, 70%, 55%);
- --accent-purple: hsl(270, 70%, 55%);
- --accent-orange: hsl(30, 70%, 55%);
- --glass-background: rgba(17, 19, 25, 0.7);
- --glass-border: rgba(255, 255, 255, 0.1);
- --font-serif: "Playfair Display", serif;
- --font-mono: "JetBrains Mono", monospace;
+ --bg-dark: #0a0a0c;
+ --accent-cyan: #3bc7c7;
+ --accent-purple: #8b5cf6;
+ --accent-orange: #f0a33b;
+ --text-primary: #f8fafc;
+ --text-secondary: #94a3b8;
+ --glass-border: rgba(255, 255, 255, 0.08);
+ --font-serif: 'Playfair Display', serif;
+ --font-mono: 'JetBrains Mono', monospace;
}
* {
@@ -18,10 +17,11 @@
}
body {
- background-color: var(--bg-void);
+ background-color: var(--bg-dark);
color: var(--text-primary);
font-family: var(--font-mono);
line-height: 1.6;
+ margin: 0;
overflow-x: hidden;
min-height: 100vh;
}
@@ -54,19 +54,44 @@ body {
display: flex;
flex-direction: column;
align-items: center;
- gap: 2rem;
+ gap: 1.5rem; /* Reduced gap to accommodate badge */
+}
+
+.author-badge {
+ display: inline-block;
+ padding: 0.4rem 0.9rem;
+ background: rgba(255, 255, 255, 0.03);
+ border: 1px solid var(--glass-border);
+ border-radius: 9999px;
+ font-family: var(--font-mono);
+ font-size: 0.6rem;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.15em;
+ color: var(--text-secondary);
+ opacity: 0.6;
+ transition: all 0.3s ease;
+ cursor: default;
+}
+
+.author-badge:hover {
+ opacity: 1;
+ background: rgba(255, 255, 255, 0.06);
+ border-color: rgba(255, 255, 255, 0.2);
+ color: var(--text-primary);
+ transform: translateY(-1px);
}
.title {
font-family: var(--font-serif);
font-size: 4rem;
- font-weight: 900;
+ font-weight: 700;
letter-spacing: -0.02em;
- background: linear-gradient(180deg, #fff, #999);
+ margin: 0;
+ background: linear-gradient(to bottom, #fff, #94a3b8);
-webkit-background-clip: text;
background-clip: text;
- -webkit-text-fill-color: transparent;
- text-shadow: 0 0 30px rgba(255, 255, 255, 0.1);
+ color: transparent;
}
.subtitle {
@@ -157,11 +182,10 @@ body {
.repo-btn.active,
.mode-btn.active,
.scale-btn.active {
- background: rgba(255, 255, 255, 0.1);
- color: var(--text-primary);
- opacity: 1;
- box-shadow: 0 0 20px rgba(255, 255, 255, 0.05);
- border: 1px solid rgba(255, 255, 255, 0.15);
+ background: var(--accent-cyan);
+ color: var(--bg-dark);
+ font-weight: 600;
+ box-shadow: 0 0 15px rgba(59, 199, 199, 0.3);
}
.repo-description {
@@ -174,7 +198,7 @@ body {
/* Visualization Canvas */
.glass-panel {
- background: var(--glass-background);
+ background: rgba(17, 19, 25, 0.7);
border: 1px solid var(--glass-border);
border-radius: 2rem;
backdrop-filter: blur(20px);
@@ -183,11 +207,17 @@ body {
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
+.insight-card {
+ padding: 2.5rem;
+ transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ overflow: visible;
+}
+
.visualization-canvas {
- min-height: 650px;
+ min-height: 700px;
display: flex;
flex-direction: column;
- padding: 1.5rem 1rem 0.5rem 1rem;
+ padding-bottom: 3rem;
}
.loading-state {
@@ -245,14 +275,15 @@ svg#main-chart {
.custom-tooltip {
position: absolute;
pointer-events: none;
- background: rgba(10, 10, 12, 0.95);
+ background: rgba(10, 10, 12, 0.98);
border: 1px solid var(--glass-border);
- padding: 1.25rem;
- border-radius: 1rem;
- backdrop-filter: blur(12px);
+ padding: 1.75rem;
+ border-radius: 1.25rem;
+ backdrop-filter: blur(16px);
z-index: 100;
- min-width: 280px;
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.7);
+ min-width: 340px;
+ box-shadow: 0 30px 60px rgba(0, 0, 0, 0.8);
+ transition: opacity 0.2s ease;
}
.tooltip-header {
@@ -269,8 +300,8 @@ svg#main-chart {
display: flex;
justify-content: space-between;
align-items: center;
- font-size: 0.9rem;
- margin-top: 0.4rem;
+ font-size: 0.95rem;
+ padding: 0.35rem 0;
}
.label-group {
@@ -301,7 +332,7 @@ svg#main-chart {
.tooltip-divider {
height: 1px;
background: var(--glass-border);
- margin: 0.75rem 0;
+ margin: 1.25rem 0;
}
/* Legend Styling */
@@ -309,11 +340,10 @@ svg#main-chart {
display: flex;
flex-wrap: wrap;
justify-content: center;
- gap: 1.5rem;
- padding: 1rem;
- background: rgba(255, 255, 255, 0.02);
- border-bottom: 1px solid var(--glass-border);
- margin-bottom: 1rem;
+ gap: 2.5rem;
+ padding: 1.5rem 0;
+ margin-bottom: 2rem;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.legend-item {
@@ -331,9 +361,17 @@ svg#main-chart {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
+ position: relative;
}
-@media (min-width: 768px) {
+@media (min-width: 992px) {
+ .insights-grid {
+ grid-template-columns: repeat(3, 1fr);
+ z-index: 1;
+ }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
.insights-grid {
grid-template-columns: repeat(2, 1fr);
}
@@ -356,6 +394,103 @@ svg#main-chart {
letter-spacing: 0.1em;
color: var(--text-secondary);
margin-bottom: 1.5rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.help-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 1.1rem;
+ height: 1.1rem;
+ font-size: 0.7rem;
+ font-weight: 700;
+ color: var(--text-secondary);
+ background: rgba(255, 255, 255, 0.08);
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ border-radius: 50%;
+ cursor: help;
+ position: relative;
+ flex-shrink: 0;
+ transition: all 0.2s ease;
+ z-index: 10;
+}
+
+.help-icon:hover {
+ background: var(--accent-cyan);
+ color: var(--bg-dark);
+ border-color: var(--accent-cyan);
+}
+
+.help-icon::after {
+ content: attr(data-tooltip);
+ position: fixed;
+ transform: translateX(-50%);
+ background: rgba(0, 0, 0, 0.98);
+ border: 1px solid var(--glass-border);
+ color: var(--text-primary);
+ padding: 0.5rem 0.75rem;
+ border-radius: 0.5rem;
+ font-size: 0.7rem;
+ font-weight: 400;
+ text-transform: none;
+ letter-spacing: normal;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.2s ease;
+ z-index: 9999;
+ width: max-content;
+ max-width: 300px;
+ min-width: 180px;
+ line-height: 1.4;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
+ text-align: center;
+ left: 50%;
+ top: 50%;
+ margin-top: -100px;
+}
+
+.help-icon:hover::after {
+ opacity: 1;
+}
+
+.help-icon:hover {
+ background: var(--accent-cyan);
+ color: var(--bg-dark);
+ border-color: var(--accent-cyan);
+}
+
+.help-icon::after {
+ content: attr(data-tooltip);
+ position: fixed;
+ bottom: auto;
+ top: auto;
+ transform: translateX(-50%);
+ background: rgba(0, 0, 0, 0.95);
+ border: 1px solid var(--glass-border);
+ color: var(--text-primary);
+ padding: 0.5rem 0.75rem;
+ border-radius: 0.5rem;
+ font-size: 0.7rem;
+ font-weight: 400;
+ text-transform: none;
+ letter-spacing: normal;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.2s ease;
+ z-index: 10000;
+ width: max-content;
+ max-width: 300px;
+ min-width: 180px;
+ line-height: 1.4;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
+ text-align: center;
+}
+
+.help-icon:hover::after {
+ opacity: 1;
}
.metric-value {
@@ -364,10 +499,18 @@ svg#main-chart {
font-weight: 700;
line-height: 1;
margin-bottom: 0.5rem;
- background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple));
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
+ color: var(--accent-cyan);
+}
+
+.metric-value.small {
+ font-size: 2.2rem;
+}
+
+.card-content-split {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1.5rem;
+ align-items: end;
}
.metric-label {
@@ -416,13 +559,77 @@ svg#main-chart {
opacity: 0.5;
}
+/* Personal Narrative Section */
+.personal-narrative {
+ width: 100%;
+ margin: 4rem 0 6rem;
+ padding: 4rem;
+ background: rgba(255, 255, 255, 0.02);
+ border-radius: 2rem;
+ border: 1px solid var(--glass-border);
+ text-align: center;
+}
+
+.narrative-title {
+ font-family: var(--font-serif);
+ font-size: 2.2rem;
+ margin-bottom: 2.5rem;
+ color: var(--text-primary);
+}
+
+.narrative-content {
+ font-family: var(--font-serif);
+ font-size: 1.15rem;
+ line-height: 1.8;
+ color: var(--text-secondary);
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.narrative-content p {
+ margin: 0;
+}
+
+.philosophy-link {
+ color: var(--accent-cyan);
+ text-decoration: none;
+ border-bottom: 1px solid rgba(59, 199, 199, 0.3);
+ transition: all 0.3s ease;
+}
+
+.philosophy-link:hover {
+ border-bottom-color: var(--accent-cyan);
+ background: rgba(59, 199, 199, 0.05);
+}
+
+.signature {
+ margin-top: 3.5rem;
+ font-family: var(--font-serif);
+ font-style: italic;
+ font-size: 1.4rem;
+ color: var(--text-primary);
+ opacity: 0.9;
+ line-height: 1.4;
+}
+
+.signature-subtitle {
+ font-style: normal;
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ opacity: 0.7;
+ display: block;
+ margin-top: 0.5rem;
+ letter-spacing: 0.05em;
+}
+
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
- background: var(--bg-void);
+ background: var(--bg-dark);
}
::-webkit-scrollbar-thumb {
@@ -432,4 +639,119 @@ svg#main-chart {
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
+}
+
+/* Hall of Fossils Section */
+.fossil-finder {
+ padding: 3rem;
+}
+
+.section-title {
+ font-family: var(--font-serif);
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ color: var(--text-primary);
+ text-align: center;
+}
+
+.fossil-grid {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 2rem;
+}
+
+@media (min-width: 768px) {
+ .fossil-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+.fossil-card {
+ background: rgba(255, 255, 255, 0.02);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 1.5rem;
+ padding: 2rem;
+ position: relative;
+ overflow: hidden;
+}
+
+.fossil-card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: radial-gradient(ellipse at top left, rgba(59, 199, 199, 0.08), transparent 50%),
+ radial-gradient(ellipse at bottom right, rgba(240, 163, 59, 0.08), transparent 50%);
+ pointer-events: none;
+ opacity: 0.6;
+}
+
+.fossil-card:hover {
+ border-color: rgba(59, 199, 199, 0.3);
+ box-shadow: 0 0 40px rgba(59, 199, 199, 0.1);
+}
+
+.fossil-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+}
+
+.fossil-label {
+ font-size: 0.8rem;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ color: var(--accent-cyan);
+}
+
+.fossil-year {
+ font-family: var(--font-mono);
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--text-primary);
+ opacity: 0.8;
+}
+
+.fossil-meta {
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+ margin-bottom: 1rem;
+ opacity: 0.6;
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.fossil-commit {
+ font-family: var(--font-mono);
+ font-size: 0.7rem;
+ color: var(--accent-orange);
+ background: rgba(240, 163, 59, 0.1);
+ padding: 0.15rem 0.4rem;
+ border-radius: 0.25rem;
+}
+
+.fossil-code {
+ background: rgba(0, 0, 0, 0.4);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 0.75rem;
+ padding: 1.25rem;
+ font-family: var(--font-mono);
+ font-size: 0.85rem;
+ color: var(--text-primary);
+ opacity: 0.5;
+ line-height: 1.5;
+ word-break: break-all;
+ white-space: pre-wrap;
+ position: relative;
+}
+
+.fossil-code::before {
+ content: '> ';
+ color: var(--accent-cyan);
+ opacity: 0.5;
}
\ No newline at end of file
diff --git a/tests/test_data_integrity.py b/tests/test_data_integrity.py
new file mode 100644
index 0000000..b71b424
--- /dev/null
+++ b/tests/test_data_integrity.py
@@ -0,0 +1,80 @@
+import json
+from pathlib import Path
+import pytest
+
+
+def test_data_integrity_optimized_schema():
+ """
+ Test that the data follows the optimized schema:
+ 1. No 'total_lines' field (it's redundant)
+ 2. No future-year keys in 'composition'
+ 3. Supports both list and object schemas (backwards compatibility)
+ """
+ data_dir = Path("./data")
+ json_files = list(data_dir.glob("*.json"))
+
+ json_files = [f for f in json_files if f.name != "manifest.json"]
+
+ assert len(json_files) > 0, "No data files found in ./data"
+
+ for json_file in json_files:
+ with open(json_file, "r", encoding="utf-8") as f:
+ data = json.load(f)
+
+ # Support both list schema (legacy) and object schema (new)
+ if isinstance(data, dict):
+ snapshots = data.get("snapshots", [])
+ fossils = data.get("fossils", {})
+ else:
+ snapshots = data
+ fossils = {}
+
+ for snapshot in snapshots:
+ # 1. total_lines MUST be absent
+ assert "total_lines" not in snapshot, (
+ f"Error in {json_file.name}: 'total_lines' field should be "
+ f"stripped for optimization but was found in {snapshot.get('snapshot_date')}"
+ )
+
+ # 2. Composition year check
+ snapshot_date = snapshot.get("snapshot_date")
+ if not snapshot_date:
+ continue
+
+ snapshot_year = int(snapshot_date[:4])
+ composition = snapshot.get("composition", {})
+
+ for year_key in composition.keys():
+ year = int(year_key)
+ assert year <= snapshot_year, (
+ f"Error in {json_file.name}: Snapshot {snapshot_date} "
+ f"contains impossible future year {year} in composition."
+ )
+
+ # 3. Validate fossil structure if present (only if not empty)
+ for fossil_type, fossil_data in fossils.items():
+ if not isinstance(fossil_data, dict):
+ continue
+ if not fossil_data: # Empty fossil object is OK
+ continue
+ assert "year" in fossil_data, (
+ f"Error in {json_file.name}: Fossil '{fossil_type}' missing 'year' field"
+ )
+ assert "content" in fossil_data, (
+ f"Error in {json_file.name}: Fossil '{fossil_type}' missing 'content' field"
+ )
+ # New required fields: commit, line, file
+ assert "file" in fossil_data, (
+ f"Error in {json_file.name}: Fossil '{fossil_type}' missing 'file' field"
+ )
+ assert "commit" in fossil_data, (
+ f"Error in {json_file.name}: Fossil '{fossil_type}' missing 'commit' field"
+ )
+ assert "line" in fossil_data, (
+ f"Error in {json_file.name}: Fossil '{fossil_type}' missing 'line' field"
+ )
+
+
+if __name__ == "__main__":
+ test_data_integrity_optimized_schema()
+ print("All optimized data integrity checks passed!")