diff --git a/app.js b/app.js index 3c58519..e44244e 100644 --- a/app.js +++ b/app.js @@ -211,8 +211,8 @@ class TheseusVisualizer { .attr("x1", "0%").attr("y1", "0%") .attr("x2", "0%").attr("y2", "100%"); - grad.append("stop").attr("offset", "0%").attr("stop-color", color).attr("stop-opacity", 0.6); - grad.append("stop").attr("offset", "100%").attr("stop-color", color).attr("stop-opacity", 0.05); + grad.append("stop").attr("offset", "0%").attr("stop-color", color).attr("stop-opacity", 0.9); + grad.append("stop").attr("offset", "100%").attr("stop-color", color).attr("stop-opacity", 0.4); }); // Specialized gradients for Identity mode if needed @@ -223,8 +223,8 @@ class TheseusVisualizer { .attr("id", `grad-id-${id}`) .attr("x1", "0%").attr("y1", "0%") .attr("x2", "0%").attr("y2", "100%"); - grad.append("stop").attr("offset", "0%").attr("stop-color", color).attr("stop-opacity", 0.6); - grad.append("stop").attr("offset", "100%").attr("stop-color", color).attr("stop-opacity", 0.05); + grad.append("stop").attr("offset", "0%").attr("stop-color", color).attr("stop-opacity", 0.9); + grad.append("stop").attr("offset", "100%").attr("stop-color", color).attr("stop-opacity", 0.4); }); } @@ -272,9 +272,58 @@ class TheseusVisualizer { // Interaction Components (Legend, Axes, Scrubber) this.renderLegend(); this.renderAxes(g, chartWidth, chartHeight, xScale, yScale); + this.renderMilestoneMarkers(g, chartWidth, chartHeight, xScale); this.setupInteractivity(g, chartWidth, chartHeight, xScale, yScale); } + renderMilestoneMarkers(g, chartWidth, chartHeight, xScale) { + const repoInfo = this.manifest.find(r => r.name === this.currentRepo); + if (!repoInfo || !repoInfo.milestones) return; + + const tooltip = this; + + repoInfo.milestones.forEach(m => { + const milestoneDate = new Date(m.date + '-01'); + const xPos = xScale(milestoneDate); + + if (xPos >= 0 && xPos <= chartWidth) { + const marker = g.append('g') + .attr('class', 'milestone-marker') + .attr('transform', `translate(${xPos}, 0)`) + .style('cursor', 'pointer'); + + marker.append('text') + .attr('x', 0) + .attr('y', 18) + .attr('text-anchor', 'middle') + .attr('font-size', '14px') + .attr('fill', '#3bc7c7') + .text('★') + .style('opacity', 0.8) + .style('filter', 'drop-shadow(0 0 4px rgba(59, 199, 199, 0.6))'); + + marker.append('title') + .text(m.title + ': ' + m.description); + + marker.on('mouseenter', function () { + d3.select(this).select('text') + .transition() + .duration(200) + .attr('font-size', '18px') + .style('opacity', 1); + }); + + marker.on('mouseleave', function () { + d3.select(this).select('text') + .transition() + .duration(200) + .attr('font-size', '14px') + .style('opacity', 0.8); + }); + } + }); + } + renderLegend() { this.legend.innerHTML = ''; const items = this.vizMode === 'identity' @@ -434,54 +483,55 @@ class TheseusVisualizer { ? point.date.toISOString().split('T')[0] : point.date; - const oldestYear = this.years[0]; - const originalVal = point[oldestYear] || 0; - - // Find previous point to detect refactor - const idx = this.points.indexOf(point); - const prev = idx > 0 ? this.points[idx - 1] : null; - const prevOldVal = prev ? (prev[oldestYear] || 0) : null; - const isRefactor = prevOldVal && originalVal < prevOldVal * 0.85; - - const evolutionVal = point.total - originalVal; - - let refactorHTML = ''; - if (originalVal === 0) { - refactorHTML = ` -
- Ship of Theseus: The Great Rebirth - The original source code is now entirely gone.
Is this still the same codebase? -
- `; - } else if (isRefactor) { - refactorHTML = ` -
- Ship of Theseus: Major Refactor - A significant part of the original source was refactored here.
How much can you change before the identity shifts? -
- `; + const foundationYear = this.years[0]; + const foundationVal = point[foundationYear] || 0; + + const existingYears = Object.keys(point).filter(k => k !== 'date' && k !== 'total' && point[k] > 0).sort(); + const oldestSurvivingYear = existingYears[0]; + const oldestSurvivingVal = point[oldestSurvivingYear] || 0; + + const isFoundationAlive = foundationVal > 0; + const refactoredVal = point.total - foundationVal; + + // Find milestone if close to current date + let milestoneHTML = ''; + const repoInfo = this.manifest.find(r => r.name === this.currentRepo); + if (repoInfo && repoInfo.milestones) { + const pointDate = new Date(point.date); + for (const m of repoInfo.milestones) { + const milestoneDate = new Date(m.date + '-01'); + const monthsDiff = Math.abs((pointDate - milestoneDate) / (1000 * 60 * 60 * 24 * 30)); + if (monthsDiff <= 3) { // Within 3 months + milestoneHTML = ` +
+
🏛️
+
+
${m.title}
+
${m.description}
+
+
+ `; + break; + } + } } this.tooltip.innerHTML = ` - ${refactorHTML} + ${milestoneHTML}
Snapshot: ${dateStr}
Total Project Size - ${point.total.toLocaleString()} lines + ${point.total.toLocaleString()} lines
- Original (${oldestYear}) + Foundation (${foundationYear})
- ${originalVal.toLocaleString()} - ${point.total > 0 ? ((originalVal / point.total) * 100).toFixed(1) : '0.0'}% + ${foundationVal.toLocaleString()} + ${point.total > 0 ? ((foundationVal / point.total) * 100).toFixed(1) : '0.0'}%
@@ -490,10 +540,22 @@ class TheseusVisualizer { Refactored
- ${evolutionVal.toLocaleString()} - ${point.total > 0 ? ((evolutionVal / point.total) * 100).toFixed(1) : '0.0'}% + ${refactoredVal.toLocaleString()} + ${point.total > 0 ? ((refactoredVal / point.total) * 100).toFixed(1) : '0.0'}% +
+ + ${!isFoundationAlive && oldestSurvivingYear && oldestSurvivingYear !== foundationYear ? ` +
+
+ + Oldest surviving (${oldestSurvivingYear}) +
+
+ ${oldestSurvivingVal.toLocaleString()} + ${point.total > 0 ? ((oldestSurvivingVal / point.total) * 100).toFixed(1) : '0.0'}%
+ ` : ''} `; // Positioning AFTER content injection @@ -539,68 +601,20 @@ class TheseusVisualizer { } document.getElementById('oldest-line').textContent = oldestSurviving; - if (birthYear && first.total > 0) { - const originalLinesInFirst = first[birthYear] || 0; - if (originalLinesInFirst > 0) { - const originalLinesInLast = last[birthYear] || 0; - const replaced = ((originalLinesInFirst - originalLinesInLast) / originalLinesInFirst) * 100; - document.getElementById('percent-replaced').textContent = `${Math.min(100, Math.max(0, replaced)).toFixed(1)}%`; - } else { - document.getElementById('percent-replaced').textContent = '0.0%'; - } + const foundationLines = last[birthYear] || 0; + const totalLines = last.total || 0; + if (birthYear && totalLines > 0) { + const foundationPercent = (foundationLines / totalLines) * 100; + document.getElementById('percent-replaced').textContent = `${foundationPercent.toFixed(1)}%`; + } else if (totalLines > 0) { + document.getElementById('percent-replaced').textContent = '0.0%'; } else { document.getElementById('percent-replaced').textContent = '--'; } - // Death counter: count times when original code dropped to 0 - let deathCount = 0; - let wasDead = false; - for (const point of this.points) { - const origLines = point[birthYear] || 0; - if (origLines === 0 && !wasDead) { - deathCount++; - wasDead = true; - } else if (origLines > 0) { - wasDead = false; - } - } - document.getElementById('death-count').textContent = deathCount; - - // 4. Modernization Velocity (Δ Old Code / Δ Time) + // Mean Code Age (Weighted average) const lastDate = new Date(last.date); const currentYear = lastDate.getFullYear(); - const oldThreshold = currentYear - 3; - - // Find snapshot approx 6 months ago (180 days) - const targetMs = lastDate.getTime() - (180 * 24 * 60 * 60 * 1000); - let prevSnapshot = this.points[0]; - for (let i = this.points.length - 1; i >= 0; i--) { - if (new Date(this.points[i].date).getTime() <= targetMs) { - prevSnapshot = this.points[i]; - break; - } - } - - const getOldLines = (snap) => { - return this.years - .filter(y => y <= oldThreshold) - .reduce((sum, y) => sum + (snap[y] || 0), 0); - }; - - const oldNow = getOldLines(last); - const oldThen = getOldLines(prevSnapshot); - const months = Math.max(1, (lastDate - new Date(prevSnapshot.date)) / (30 * 24 * 60 * 60 * 1000)); - const velocity = (oldThen - oldNow) / months; - - const velEl = document.getElementById('modernization-velocity'); - if (this.points.length < 2 || oldThen === 0) { - velEl.textContent = 'Stable'; - } else { - velEl.textContent = `${Math.max(0, Math.round(velocity)).toLocaleString()}`; - } - - // 5. Mean Code Age (Weighted average) - const totalLines = last.total; if (totalLines > 0) { let totalAge = 0; this.years.forEach(y => { @@ -614,7 +628,7 @@ class TheseusVisualizer { document.getElementById('mean-code-age').textContent = '0.0 yrs'; } - // 6. Peak Preservation (Largest legacy year) + // Peak Preservation (Largest legacy year) let peakYear = '--'; let peakVal = 0; this.years.forEach(y => { @@ -674,12 +688,7 @@ class TheseusVisualizer { a.target = '_blank'; a.rel = 'noopener noreferrer'; a.textContent = display; - a.style.color = 'inherit'; - a.style.textDecoration = 'underline'; - a.style.textDecorationColor = 'rgba(255,255,255,0.2)'; - a.style.transition = 'color 0.3s ease'; - a.addEventListener('mouseover', () => { a.style.color = 'var(--accent-cyan)'; }); - a.addEventListener('mouseout', () => { a.style.color = 'inherit'; }); + a.className = 'fossil-link'; return a; }; diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 0a506fa..f3949ed 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -13,7 +13,10 @@ The Ship of Theseus engine operates centrally off a single file: `theseus.config "name": "react", "repo": "facebook/react", "displayName": "React", - "description": "A JavaScript library for building user interfaces" + "description": "A JavaScript library for building user interfaces", + "milestones": [ + { "date": "2013-05", "title": "Open Source", "description": "React is released." } + ] } ] } @@ -33,6 +36,20 @@ The `repositories` array takes objects consisting of the following key attribute | `repo` | *String* | The GitHub repository namespace (the URL ending). The engine automatically strips trailing slashes and resolves this to `https://github.com/namespace/repo.git`. | `"django/django"` | | `displayName` | *String* | The aesthetic name rendered on UI Cards. | `"Django"` | | `description` | *String* | A short UI subheading clarifying what the project is. | `"The web framework for perfectionists with deadlines."` | +| `milestones` | *Array* | An optional list of significant events to display on the timeline. | `[{"date": "2024-01", "title": "Launch"}]` | + +--- + +## Milestone Structure + +The `milestones` array contains objects with the following properties: + +| Key | Type | Description | Example | +| :--- | :---: | :--- | :--- | +| `date` | *String* | The date of the milestone in `YYYY-MM` format. | `"2024-06"` | +| `title` | *String* | A short, catchy name for the event shown in tooltips. | `"Monorepo Migration"` | +| `description` | *String* | A concise explanation of the event. | `"Unified all integrations into a single repository."` | + --- diff --git a/index.html b/index.html index c3e8c99..1324b90 100644 --- a/index.html +++ b/index.html @@ -108,15 +108,16 @@

The Ship of Theseus

+

Insights

- How much has changed? + How much foundation remains? ? + data-tooltip="Percentage of the current codebase that originated in the foundation year">?

--

-

of original code replaced

+

of current codebase is foundation code

@@ -162,30 +163,6 @@

-
-

- How fast is code being replaced? - ? -

-
-

- -- -

-

Lines replaced per month

-
-
-
-

- How many times did the original codebase die? - ? -

-
-

--

-

Complete rebuilds

-
-

What's the average code age? @@ -220,6 +197,7 @@

How the data is collected

Ancient Code Fragments

+

Click the file path below to view on GitHub

@@ -233,8 +211,7 @@

Ancient Code Fragments

Loading...
-

The - oldest line ever written in this repo's history, even if deleted long ago.

+

The oldest line ever written in this repo's history, even if deleted long ago.

@@ -248,8 +225,7 @@

Ancient Code Fragments

Loading...
-

The - oldest line that is still alive in the codebase today.

+

The oldest line that is still alive in the codebase today.

diff --git a/style.css b/style.css index a2999ea..75dff70 100644 --- a/style.css +++ b/style.css @@ -10,6 +10,20 @@ --font-mono: 'JetBrains Mono', monospace; } +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + -webkit-clip-path: inset(50%); + clip-path: inset(50%); + white-space: nowrap; + border: 0; +} + * { margin: 0; padding: 0; @@ -69,7 +83,6 @@ body { text-transform: uppercase; letter-spacing: 0.15em; color: var(--text-secondary); - opacity: 0.6; transition: all 0.3s ease; cursor: default; } @@ -160,7 +173,6 @@ body { font-size: 0.9rem; cursor: pointer; transition: all 0.3s ease; - opacity: 0.6; } .mode-btn, @@ -192,7 +204,6 @@ body { font-style: italic; font-size: 0.9rem; color: var(--text-secondary); - opacity: 0.7; margin-top: 0.5rem; } @@ -401,6 +412,49 @@ svg#main-chart { transition: opacity 0.2s ease; } +.milestone-banner { + display: flex; + gap: 0.75rem; + align-items: flex-start; + background: linear-gradient(135deg, rgba(139, 92, 246, 0.15), rgba(59, 199, 199, 0.1)); + border: 1px solid rgba(139, 92, 246, 0.3); + border-radius: 0.75rem; + padding: 1rem; + margin-bottom: 1.25rem; +} + +.milestone-icon { + font-size: 1.5rem; + flex-shrink: 0; +} + +.milestone-content { + flex: 1; +} + +.milestone-title { + font-weight: 600; + font-size: 0.9rem; + color: var(--text-primary); + margin-bottom: 0.35rem; +} + +.milestone-desc { + font-size: 0.75rem; + color: var(--text-secondary); + line-height: 1.4; + opacity: 0.9; +} + +.milestone-marker text { + transition: all 0.2s ease; +} + +.milestone-marker:hover text { + filter: drop-shadow(0 0 8px rgba(59, 199, 199, 0.8)); +} + + .tooltip-header { font-size: 0.75rem; text-transform: uppercase; @@ -417,6 +471,7 @@ svg#main-chart { align-items: center; font-size: 0.95rem; padding: 0.35rem 0; + gap: 1rem; } .label-group { @@ -434,7 +489,7 @@ svg#main-chart { .value-group { display: flex; align-items: center; - gap: 0.75rem; + gap: 1rem; } .percent-tag { @@ -671,7 +726,6 @@ svg#main-chart { .footer-text { font-size: 0.8rem; color: var(--text-secondary); - opacity: 0.5; } /* Personal Narrative Section */ @@ -732,7 +786,6 @@ svg#main-chart { font-style: normal; font-size: 0.85rem; color: var(--text-secondary); - opacity: 0.7; display: block; margin-top: 0.5rem; letter-spacing: 0.05em; @@ -764,15 +817,24 @@ svg#main-chart { .section-title { font-family: var(--font-serif); font-size: 2rem; - margin-bottom: 2rem; + margin-bottom: 0.5rem; color: var(--text-primary); text-align: center; } +.fossil-hint { + text-align: center; + font-size: 0.8rem; + color: var(--text-secondary); + opacity: 0.6; + margin-bottom: 2.5rem; + font-style: italic; +} + .fossil-grid { display: grid; grid-template-columns: 1fr; - gap: 2rem; + gap: 2.5rem; } @media (min-width: 768px) { @@ -785,10 +847,12 @@ svg#main-chart { background: rgba(255, 255, 255, 0.02); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 1.5rem; - padding: 1.25rem 1.5rem; + padding: 1.5rem 2rem; position: relative; overflow: hidden; text-align: left; + cursor: pointer; + transition: all 0.3s ease; } .fossil-card::before { @@ -807,6 +871,7 @@ svg#main-chart { .fossil-card:hover { border-color: rgba(59, 199, 199, 0.3); box-shadow: 0 0 40px rgba(59, 199, 199, 0.1); + transform: translateY(-4px); } .fossil-header { @@ -821,6 +886,20 @@ svg#main-chart { text-transform: uppercase; letter-spacing: 0.1em; color: var(--accent-cyan); + display: flex; + align-items: center; + gap: 0.5rem; +} + +.fossil-label::after { + content: '↗'; + font-size: 0.7rem; + opacity: 0; + transition: opacity 0.3s ease; +} + +.fossil-card:hover .fossil-label::after { + opacity: 0.6; } .fossil-year { @@ -834,14 +913,32 @@ svg#main-chart { .fossil-meta { font-size: 0.8rem; color: var(--text-secondary); - margin-bottom: 1rem; + margin-bottom: 1.25rem; opacity: 0.6; display: flex; - gap: 0.5rem; + gap: 0.75rem; align-items: center; flex-wrap: wrap; } +.fossil-link { + color: var(--accent-cyan); + text-decoration: underline; + text-underline-offset: 3px; + text-decoration-color: rgba(59, 199, 199, 0.4); + cursor: pointer; + transition: all 0.2s ease; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + background: rgba(59, 199, 199, 0.05); + display: inline-block; +} + +.fossil-link:hover { + background: rgba(59, 199, 199, 0.15); + text-decoration-color: rgba(59, 199, 199, 0.8); +} + .fossil-commit { font-family: var(--font-mono); font-size: 0.7rem; @@ -876,3 +973,12 @@ svg#main-chart { flex-shrink: 0; display: inline; } + +.fossil-description { + font-size: 0.75rem; + font-style: italic; + color: var(--text-secondary); + opacity: 0.5; + margin-top: 1.25rem; + line-height: 1.5; +} diff --git a/theseus.config.json b/theseus.config.json index ec4968d..d9345ec 100644 --- a/theseus.config.json +++ b/theseus.config.json @@ -5,31 +5,146 @@ "name": "langchain", "file": "langchain_data.json", "description": "Framework for developing LLM-driven applications and agents.", - "repo": "langchain-ai/langchain" + "repo": "langchain-ai/langchain", + "milestones": [ + { + "date": "2023-12", + "title": "LangChain v0.1", + "description": "First stable architecture release with standardized core components." + }, + { + "date": "2024-06", + "title": "Monorepo & LangGraph Cloud", + "description": "Massive expansion with LangGraph Cloud and integration packages." + }, + { + "date": "2025-10", + "title": "LangChain v1.0 (The Pruning)", + "description": "Legacy code moved to langchain-classic for a leaner, faster core." + } + ] }, { "name": "react", "file": "react_data.json", "description": "Component-based JavaScript library for building user interfaces.", - "repo": "facebook/react" + "repo": "facebook/react", + "milestones": [ + { + "date": "2013-05", + "title": "React open sourced", + "description": "Public release of React at JSConf 2013." + }, + { + "date": "2017-09", + "title": "React rebuilt from scratch", + "description": "React 16 rewrite for better performance." + }, + { + "date": "2019-02", + "title": "Hooks changed everything", + "description": "Introduced Hooks for state management without classes." + }, + { + "date": "2024-04", + "title": "React 19 major update", + "description": "Added Server Components and new form handling." + }, + { + "date": "2019-06", + "title": "Data quirk: code looks different", + "description": "Data quirk: Internal code reorganization." + }, + { + "date": "2023-06", + "title": "Data quirk: lots missing", + "description": "Data quirk: Missing historical data." + } + ] }, { "name": "numpy", "file": "numpy_data.json", "description": "The fundamental package for scientific computing in Python.", - "repo": "numpy/numpy" + "repo": "numpy/numpy", + "milestones": [ + { + "date": "2005-09", + "title": "NumPy is born", + "description": "Created by merging Numeric and Numarray." + }, + { + "date": "2006-03", + "title": "NumPy 1.0 released", + "description": "Official 1.0 release as a standalone library." + }, + { + "date": "2013-04", + "title": "Python 2 & 3 unified", + "description": "Unified support for Python 2 and 3." + }, + { + "date": "2017-06", + "title": "Major features added", + "description": "Major update with 50+ new functions." + }, + { + "date": "2024-06", + "title": "NumPy 2.0 released", + "description": "NumPy 2.0: Major update with new dtype system." + } + ] }, { "name": "zed", "file": "zed_data.json", "description": "High-performance, GPU-accelerated code editor for teamwork.", - "repo": "zed-industries/zed" + "repo": "zed-industries/zed", + "milestones": [ + { + "date": "2023-03", + "title": "Zed Beta Launch", + "description": "High-performance code editor enters public beta on macOS." + }, + { + "date": "2023-12", + "title": "GPUI2 Transition and Open Sourced", + "description": "Major rewrite of the UI framework for 120 FPS performance and The Zed source code is made available to the public." + }, + { + "date": "2024-06", + "title": "Linux Release", + "description": "Official launch of Zed for Linux after months of development." + }, + { + "date": "2024-09", + "title": "Zed AI Launch", + "description": "Introduction of native AI features and the Zed AI ecosystem." + } + ] }, { "name": "claude-code", "file": "claude-code_data.json", "description": "Claude's agentic CLI tool for local coding tasks.", - "repo": "anthropics/claude-code" + "repo": "anthropics/claude-code", + "milestones": [ + { + "date": "2025-05", + "title": "Official v1.0 Launch", + "description": "General availability alongside the Claude 4 model family." + }, + { + "date": "2025-09", + "title": "v2.0 Major Overhaul", + "description": "Foundational rewrite with terminal-first architecture." + }, + { + "date": "2026-03", + "title": "Source Code Leak (v2.1.88)", + "description": "Accidental exposure of source code via npm map files." + } + ] } ] -} +} \ No newline at end of file