diff --git a/public/sitemap.xml b/public/sitemap.xml index 1d27c71..96ff2a1 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -1 +1 @@ -https://refactron.dev/weekly1.0https://refactron.dev/blogweekly0.9https://refactron.dev/aboutmonthly0.6https://refactron.dev/changelogweekly0.7https://refactron.dev/securitymonthly0.5https://refactron.dev/researchmonthly0.5https://refactron.dev/privacy-policyyearly0.3https://refactron.dev/terms-of-serviceyearly0.3https://refactron.dev/blog/i-ran-refactron-on-djangos-codebasemonthly0.8https://refactron.dev/blog/refactron-vs-cursor-vs-codeantmonthly0.8https://refactron.dev/blog/why-we-built-verification-engine-firstmonthly0.8https://refactron.dev/blog/legacy-code-ai-refactoringmonthly0.8https://refactron.dev/blog/refactron-on-requests-librarymonthly0.8https://refactron.dev/blog/real-cost-of-not-refactoringmonthly0.8https://refactron.dev/blog/refactron-on-fastapimonthly0.8https://refactron.dev/blog/how-to-safely-refactor-python-code-you-didnt-writemonthly0.8https://refactron.dev/blog/why-refactron-runs-locallymonthly0.8https://refactron.dev/blog/refactron-is-now-a-nodejs-packagemonthly0.8 \ No newline at end of file +https://refactron.dev/weekly1.0https://refactron.dev/blogweekly0.9https://refactron.dev/aboutmonthly0.6https://refactron.dev/changelogweekly0.7https://refactron.dev/securitymonthly0.5https://refactron.dev/researchmonthly0.6https://refactron.dev/research/perf-01yearly0.5https://refactron.dev/research/comparison-01yearly0.5https://refactron.dev/privacy-policyyearly0.3https://refactron.dev/terms-of-serviceyearly0.3https://refactron.dev/blog/i-ran-refactron-on-djangos-codebasemonthly0.8https://refactron.dev/blog/refactron-vs-cursor-vs-codeantmonthly0.8https://refactron.dev/blog/why-we-built-verification-engine-firstmonthly0.8https://refactron.dev/blog/legacy-code-ai-refactoringmonthly0.8https://refactron.dev/blog/refactron-on-requests-librarymonthly0.8https://refactron.dev/blog/real-cost-of-not-refactoringmonthly0.8https://refactron.dev/blog/refactron-on-fastapimonthly0.8https://refactron.dev/blog/how-to-safely-refactor-python-code-you-didnt-writemonthly0.8https://refactron.dev/blog/why-refactron-runs-locallymonthly0.8https://refactron.dev/blog/refactron-is-now-a-nodejs-packagemonthly0.8 \ No newline at end of file diff --git a/scripts/generate-sitemap.js b/scripts/generate-sitemap.js index 8e11baf..356c693 100644 --- a/scripts/generate-sitemap.js +++ b/scripts/generate-sitemap.js @@ -19,7 +19,9 @@ const staticRoutes = [ { url: '/about', changefreq: 'monthly', priority: 0.6 }, { url: '/changelog', changefreq: 'weekly', priority: 0.7 }, { url: '/security', changefreq: 'monthly', priority: 0.5 }, - { url: '/research', changefreq: 'monthly', priority: 0.45 }, + { url: '/research', changefreq: 'monthly', priority: 0.6 }, + { url: '/research/perf-01', changefreq: 'yearly', priority: 0.5 }, + { url: '/research/comparison-01', changefreq: 'yearly', priority: 0.5 }, { url: '/privacy-policy', changefreq: 'yearly', priority: 0.3 }, { url: '/terms-of-service',changefreq: 'yearly', priority: 0.3 }, // blog posts added dynamically below diff --git a/scripts/prerender.js b/scripts/prerender.js index 4834909..43c9531 100644 --- a/scripts/prerender.js +++ b/scripts/prerender.js @@ -32,6 +32,8 @@ const PAGES = [ '/changelog', '/security', '/research', + '/research/perf-01', + '/research/comparison-01', '/privacy-policy', '/terms-of-service', ...blogSlugs.map(slug => `/blog/${slug}`), diff --git a/src/App.tsx b/src/App.tsx index c69690b..2472bc6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,12 +26,15 @@ import AuthApp from './components/AuthApp'; import NotFoundPage from './components/NotFoundPage'; import ErrorBoundary from './components/ErrorBoundary'; import SkipToMain from './components/SkipToMain'; +import ScrollToTop from './components/ScrollToTop'; import usePerformanceMonitoring from './hooks/usePerformanceMonitoring'; import useAccessibility from './hooks/useAccessibility'; import PageLayout from './components/PageLayout'; import Changelog from './components/Changelog'; import SecurityPage from './components/SecurityPage'; import ResearchPage from './components/ResearchPage'; +import ResearchPerf01Page from './components/ResearchPerf01Page'; +import ResearchComparison01Page from './components/ResearchComparison01Page'; import StatusPage from './components/StatusPage'; import { ThemeProvider } from './contexts/ThemeContext'; @@ -91,6 +94,7 @@ function App() { + {isDocsHost ? ( } /> @@ -161,6 +165,22 @@ function App() { } /> + + + + } + /> + + + + } + /> } /> ( .rfn-die-core { animation: rfn-die-pulse 2.6s ease-in-out infinite; } `} - {/* Surrounding mono labels */} -
+ {/* Surrounding mono labels — sit OUTSIDE the chip body in the safe + gutter. Bumped from neutral-600/700 to neutral-400/500 so they + actually read against the page bg instead of disappearing. */} +
FIG · 01
-
+
DETERMINISTIC · V0.5
ANALYZE · REFACTOR · VERIFY · DOCUMENT @@ -793,10 +795,12 @@ const HeroChipModule: React.FC = () => (
- {/* Bottom-left product label */} -
-
REFACTRON
-
ENGINE
+ {/* Bottom-left product label. The chip die above is bright on hover + of the eye, so labels need real contrast or they get read as part + of the dot grid. */} +
+
REFACTRON
+
ENGINE
{/* Bottom-right status LEDs — first one pulses (active) */} @@ -843,13 +847,21 @@ const AboutPage: React.FC = () => { }); return ( -
+
{/* ─── Hero ────────────────────────────────────────────────────── */} -
+ {/* Top padding pushes content clear of the fixed navbar (~5rem tall); + bottom padding gives the hero room to breathe before section #2. */} +
{sectionFades} -
+
{
{/* ─── Contact ─────────────────────────────────────────────── */} -
+

diff --git a/src/components/ResearchComparison01Page.tsx b/src/components/ResearchComparison01Page.tsx new file mode 100644 index 0000000..b41c59a --- /dev/null +++ b/src/components/ResearchComparison01Page.tsx @@ -0,0 +1,1361 @@ +import React, { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { motion } from 'framer-motion'; +import useSEO from '../hooks/useSEO'; + +/* ─── Shared tokens (match ResearchPerf01Page) ──────────────────── */ + +const eyebrow = + 'text-[10px] font-mono uppercase tracking-[0.28em] text-neutral-500'; + +const MONO = 'ui-monospace, SFMono-Regular, monospace'; +const EMERALD = 'rgba(74, 222, 128, 0.85)'; +const EMERALD_DIM = 'rgba(74, 222, 128, 0.12)'; +const ROSE = 'rgba(244, 63, 94, 0.8)'; +const ROSE_DIM = 'rgba(244, 63, 94, 0.1)'; + +/* ─── Table of contents ─────────────────────────────────────────── */ + +const TOC: { id: string; label: string }[] = [ + { id: 'abstract', label: 'Abstract' }, + { id: 'why', label: '01 · Why this study' }, + { id: 'setup', label: '02 · Setup' }, + { id: 'results', label: '03 · Results' }, + { id: 'split', label: '04 · The split' }, + { id: 'discussion', label: '05 · Discussion' }, + { id: 'limitations', label: '06 · Limitations' }, + { id: 'how-built', label: '07 · How it was built' }, + { id: 'references', label: 'References' }, +]; + +function useActiveSection(ids: string[]): string { + const [active, setActive] = useState(ids[0] ?? ''); + useEffect(() => { + const observer = new IntersectionObserver( + entries => { + const visible = entries + .filter(e => e.isIntersecting) + .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top); + if (visible[0]) setActive(visible[0].target.id); + }, + { rootMargin: '-15% 0px -75% 0px' } + ); + ids.forEach(id => { + const el = document.getElementById(id); + if (el) observer.observe(el); + }); + return () => observer.disconnect(); + }, [ids]); + return active; +} + +/* ─────────────────────────────────────────────────────────────── */ + +const ResearchComparison01Page: React.FC = () => { + useSEO({ + title: + 'Refactron vs the codemod baseline · A head-to-head study | Research', + description: + 'Refactron benchmarked against jscodeshift, Comby, ESLint --fix, and LibCST on var → const/let and format → f-string. Speed, coverage, and safety on identical inputs. Reproducible.', + canonical: 'https://refactron.dev/research/comparison-01', + robots: 'index, follow', + }); + + const active = useActiveSection(TOC.map(t => t.id)); + + return ( +

+ + + {/* ═══════════════ HERO (full-width) ═══════════════ */} +
+
+ +
+

Research · Comparison report 02

+ +

+ 2026-05-15 +

+
+

+ Refactron vs the{' '} + codemod baseline. A + head-to-head. +

+

+ Two transforms, four other tools, identical inputs. We measured + speed, coverage, and safety against the existing + deterministic-codemod technology. The result is mixed in exactly + the way an honest benchmark should be. +

+
+
+
+ + {/* ═══════════════ BENCHMARK BAND (full-width feature strip) ═══════════════ */} +
+
+

+ Coverage benchmark ·{' '} + var → const/let &{' '} + format → f-string · 5 + tools · identical inputs +

+ +
+
+ + {/* ═══════════════ TWO-COLUMN: TOC + PAPER BODY ═══════════════ */} +
+
+ {/* ── Sticky TOC ── */} + + + {/* ── Paper body ── */} +
+ {/* Authors box */} + + + {/* 00 · Abstract */} + + 00 · Abstract +

+ Refactron is the slowest tool we measured. It is also the only + one that is top-coverage on both transforms while never + producing a single unsafe rewrite. The two pure codemod tools + that ship no verification step run sub-second and write code + that does not compile. +

+

+ That tension is the paper. Speed without verification bought + broken code in every cell where it was measured. The benchmark + builds on the deterministic-refactoring tradition + — behaviour preservation checked, not assumed. +

+
+ + {/* 01 · Why */} + + 01 · Why this study +

The engineering baseline, not the competitors.

+

+ jscodeshift and LibCST are codemod frameworks — you + author codemods with them. Comby is a structural search/replace + DSL. ESLint --fix is a linter's autofix. None of + them is the product a team weighs Refactron against. +

+

+ But they are the existing technology that performs deterministic + source-to-source transformation — exactly what Refactron's + engine does. A new approach earns credibility by being measured + against the established one on identical inputs. This is + "transform + verify versus transform only," not "our product + versus theirs." +

+
+ + {/* 02 · Setup */} + + 02 · Setup +

Identical inputs, three axes.

+
+ + + +
+

+ Each tool runs the equivalent codemod, authored the way a + competent engineer would and committed to the repo for audit. + LibCST uses Instagram's reference{' '} + ConvertFormatStringCommand + ; ESLint runs its stock prefer-const{' '} + + no-var rules. +

+
+ {[ + { + t: 'Speed', + d: 'Wall-clock for the whole invocation, process startup included. What a user actually waits for.', + }, + { + t: 'Coverage', + d: 'Per-site exact classification — correct, missed, wrong, broken — by stable anchor, not line proximity.', + }, + { + t: 'Safety', + d: "tsc --noEmit / py_compile plus the fixture's own test suite, run against the tool's output.", + }, + ].map(x => ( +
+

+ {x.t} +

+

{x.d}

+
+ ))} +
+
+ + {/* 03 · Results */} + + 03 · Results +

Coverage and safety move together.

+
+ Figure 1. Correct-rewrite coverage per tool. Bar + colour encodes safety — green compiled and passed tests, red + did not. + + } + > + +
+ +

+ var → const/let +

+

+ TypeScript · 126 planted sites +

+ + +

+ format → f-string +

+

+ Python · 108 planted sites +

+ + + Table 1. Median of five runs. Refactron and ESLint + convert every site correctly and safely; the pure codemod tools + emit dozens of wrong rewrites that fail to compile. + +
+ + {/* 04 · The split */} + + 04 · The split +

+ Careful tools are safe. Unguarded tools are fast and broken. +

+
+ Figure 2. Every measured cell, speed against + coverage. Safe results (green) sit in a high-coverage band; + unsafe results (red) sit below 50%. + + } + > + +
+
+
+

+ Careful · safe output +

+

+ Refactron (verification + gate), ESLint (narrow + ruleset), LibCST{' '} + (conservative codemod). Every output compiles and passes + tests. +

+
+
+

+ Unguarded · broken output +

+

+ jscodeshift and{' '} + Comby transform with no + verification step. Sub-second, and every cell failed + compilation. +

+
+
+

+ No cell in this study was fast, high-coverage, and safe at once. + Speed without verification bought broken code every time. +

+
+ + {/* 05 · Discussion */} + + 05 · Discussion +

Where Refactron wins, and where it loses.

+
+ + Refactron is top-coverage on both transforms — a tie with + ESLint at 100% on var → const/let, an outright + win at 99.1% vs LibCST's 57.4% on{' '} + format → f-string — and never emits an unsafe + rewrite. No other tool here is top-coverage on both. + + + Refactron is the slowest tool measured — ~8× slower than + ESLint on var → const/let. This is not an + optimization gap to apologize for; it is the pipeline. + Refactron applies a transform, re-parses every changed file, + resolves every import, runs the full test suite on a shadow + tree, then writes atomically. The 5.22s figure is{' '} + that pipeline. + +
+

+ The honest claim is not "Refactron is fastest." It is: Refactron + is the only tool measured here that never wrote broken code — + and that guarantee has a price denominated in seconds. +

+
+ + {/* 06 · Limitations */} + + 06 · Limitations +

The honest fine print.

+
+ + These are frameworks and linters, not Refactron's commercial + alternatives. Cursor, SonarQube, and the LLM tools belong in a + separate, categorical study. + + + Refactron ships ten. These two were chosen for tool overlap, + not because they are the hardest cases. + + + Ten files per transform with planted patterns. Real codebases + are messier. The fixtures are published so the methodology can + be challenged. + + + We authored them and committed them for audit. If an expert + shows us a better visitor, we rerun and republish. + +
+
+ + {/* 07 · How it was built */} + + 07 · How this benchmark was built +

It embarrassed us first.

+

+ The first run reported Refactron at 27% coverage on{' '} + var → const/let. Investigation found two real bugs + in Refactron's own transform — a scope-unaware reference scan + and a missed AST node kind. They were fixed (27% → 100%) before + publication + . The benchmark also caught a precision flaw in + its own checker that was miscounting every tool; that was + corrected too. A benchmark you publish should be one that has + already embarrassed you in private. +

+
+ +
+
+ + {/* References */} + + References +
    + + Comparison bench — fixtures, per-tool codemods, harness, raw + results.{' '} + + github.com/Refactron-ai · bench/comparison + + + + Instagram / LibCST. ConvertFormatStringCommand — the + reference Python format codemod benchmarked here. + + + Opdyke, W. F. (1992).{' '} + Refactoring Object-Oriented Frameworks. PhD thesis, + University of Illinois Urbana-Champaign — the + precondition-checking foundation behaviour-preserving + refactoring rests on. + + + The var_to_const_let scope-correctness fix and the + printf-grammar percent converter.{' '} + + Refactron_Lib_TS · PR #27 + + + + Refactron 0.2.0 performance report —{' '} + + research paper #01 + + . + +
+ +
+ + ← All research + + + Documentation + + + Source ↗ + +
+
+
+
+
+
+ ); +}; + +/* ═════════════════ Layout primitives ═════════════════════════════ */ + +const DotGrid: React.FC = () => ( +
+); + +const Block: React.FC<{ + id: string; + last?: boolean; + children: React.ReactNode; +}> = ({ id, last, children }) => ( + + {children} + +); + +const Kicker: React.FC<{ children: React.ReactNode }> = ({ children }) => ( +

{children}

+); + +const H2: React.FC<{ children: React.ReactNode }> = ({ children }) => ( +

+ {children} +

+); + +const P: React.FC<{ children: React.ReactNode; className?: string }> = ({ + children, + className = '', +}) => ( +

+ {children} +

+); + +const Mono: React.FC<{ children: React.ReactNode }> = ({ children }) => ( + + {children} + +); + +const Cite: React.FC<{ n: string }> = ({ n }) => ( + + [{n}] + +); + +const Caption: React.FC<{ children: React.ReactNode }> = ({ children }) => ( +

+ {children} +

+); + +const Figure: React.FC<{ + caption: React.ReactNode; + children: React.ReactNode; +}> = ({ caption, children }) => ( +
+ {children} +
+ {caption} +
+
+); + +const AuthorsBox: React.FC = () => ( +
+

+ Authors +

+
+
+ + Om Sherikar ↗ + +

Founder, Refactron

+
+
+
+

+ Published +

+

2026-05-15

+
+
+

+ Hardware +

+

Apple M2

+
+
+
+
+); + +const FactTile: React.FC<{ label: string; value: string; sub: string }> = ({ + label, + value, + sub, +}) => ( +
+

+ {label} +

+

+ {value} +

+

{sub}

+
+); + +const Panel: React.FC<{ title: string; children: React.ReactNode }> = ({ + title, + children, +}) => ( +
+

+ {title} +

+

{children}

+
+); + +const Limitation: React.FC<{ title: string; children: React.ReactNode }> = ({ + title, + children, +}) => ( +
+

+ {title} +

+

{children}

+
+); + +const RefItem: React.FC<{ n: string; children: React.ReactNode }> = ({ + n, + children, +}) => ( +
  • + + [{n}] + + {children} +
  • +); + +const ExtLink: React.FC<{ href: string; children: React.ReactNode }> = ({ + href, + children, +}) => ( + + {children} + +); + +const CodeBlock: React.FC<{ caption: string; code: string }> = ({ + caption, + code, +}) => ( +
    +
    +

    + {caption} +

    +
    + + + +
    +
    +
    +      {code}
    +    
    +
    +); + +/* ═════════════════ Benchmark band — compact scorecard ════════════ */ + +const BAND_ROWS: { + transform: string; + tool: string; + coverage: number; + safe: boolean; + lead?: boolean; +}[] = [ + { + transform: 'var → const/let', + tool: 'Refactron', + coverage: 100, + safe: true, + lead: true, + }, + { + transform: 'var → const/let', + tool: 'ESLint --fix', + coverage: 100, + safe: true, + }, + { + transform: 'var → const/let', + tool: 'jscodeshift', + coverage: 46.0, + safe: false, + }, + { transform: 'var → const/let', tool: 'Comby', coverage: 47.6, safe: false }, + { + transform: 'format → f-string', + tool: 'Refactron', + coverage: 99.1, + safe: true, + lead: true, + }, + { + transform: 'format → f-string', + tool: 'LibCST', + coverage: 57.4, + safe: true, + }, + { + transform: 'format → f-string', + tool: 'Comby', + coverage: 15.7, + safe: false, + }, +]; + +const ScoreBand: React.FC = () => ( +
    +
    + Transform + Tool + Coverage + Safe +
    + {BAND_ROWS.map((r, i) => { + const firstOfGroup = + i === 0 || BAND_ROWS[i - 1].transform !== r.transform; + return ( +
    + + {firstOfGroup ? r.transform : ''} + + + {r.tool} + +
    +
    +
    +
    + + {r.coverage % 1 === 0 ? r.coverage : r.coverage.toFixed(1)}% + +
    + + + {r.safe ? '✓' : '✗'} + + +
    + ); + })} +
    +); + +/* ═════════════════ Result table ══════════════════════════════════ */ + +interface ResultRow { + tool: string; + speed: number; + coverage: number; + detail: string; + wrong: number; + safe: boolean; + brokenNote?: string; + highlight?: boolean; +} + +const ResultTable: React.FC<{ rows: ResultRow[]; speedCap: number }> = ({ + rows, + speedCap, +}) => ( +
    +
    + Tool + Speed + Coverage + Wrong + Safety +
    + {rows.map((r, i) => ( +
    + + {r.tool} + +
    +

    + {r.speed.toFixed(2)}s +

    +
    +
    +
    +
    +
    +

    + = 99 ? 'text-emerald-400/90' : 'text-neutral-300' + } + > + {r.coverage.toFixed(1)}% + {' '} + {r.detail} +

    +
    +
    = 99 ? 'bg-emerald-400/55' : 'bg-white/25' + }`} + style={{ width: `${r.coverage}%` }} + /> +
    +
    + 0 ? 'text-rose-400/90' : 'text-neutral-500' + }`} + > + {r.wrong > 0 ? r.wrong : '0'} + {r.brokenNote && ( + + {r.brokenNote} + + )} + + + + {r.safe ? 'safe' : 'fail'} + + +
    + ))} +
    +); + +/* ═════════════════ Visual: Coverage bar chart ════════════════════ */ + +interface CovBar { + tool: string; + pct: number; + safe: boolean; +} + +const COV_GROUPS: { label: string; bars: CovBar[] }[] = [ + { + label: 'var → const/let', + bars: [ + { tool: 'Refactron', pct: 100, safe: true }, + { tool: 'ESLint', pct: 100, safe: true }, + { tool: 'jscodeshift', pct: 46.0, safe: false }, + { tool: 'Comby', pct: 47.6, safe: false }, + ], + }, + { + label: 'format → f-string', + bars: [ + { tool: 'Refactron', pct: 99.1, safe: true }, + { tool: 'LibCST', pct: 57.4, safe: true }, + { tool: 'Comby', pct: 15.7, safe: false }, + ], + }, +]; + +const CoverageChart: React.FC = () => { + const W = 760; + const H = 400; + const padL = 46; + const padR = 24; + const padT = 28; + const plotBottom = 308; + const plotTop = padT; + const plotH = plotBottom - plotTop; + const plotW = W - padL - padR; + const groupW = plotW / COV_GROUPS.length; + const yFor = (p: number) => plotBottom - (p / 100) * plotH; + + return ( +
    + + + + + + + + + + + + {[0, 25, 50, 75, 100].map(t => ( + + + + {t}% + + + ))} + + {COV_GROUPS.map((group, gi) => { + const groupStart = padL + gi * groupW; + const sidePad = 30; + const gap = 14; + const n = group.bars.length; + const innerW = groupW - 2 * sidePad; + const barW = (innerW - gap * (n - 1)) / n; + return ( + + {group.bars.map((b, bi) => { + const x = groupStart + sidePad + bi * (barW + gap); + const top = yFor(b.pct); + return ( + + + + {b.pct % 1 === 0 ? b.pct : b.pct.toFixed(1)}% + + + {b.tool} + + + ); + })} + + {group.label.toUpperCase()} + + + ); + })} + +
    + ); +}; + +/* ═════════════════ Visual: Speed-vs-coverage scatter ══════════════ */ + +interface ScatterPt { + label: string; + speed: number; + coverage: number; + safe: boolean; + lx: number; + ly: number; + anchor: 'start' | 'middle' | 'end'; +} + +const SCATTER_PTS: ScatterPt[] = [ + // Both Refactron points label BELOW — above-the-point would clip the plot + // top and collide with the band caption. + { + label: 'Refactron · var', + speed: 5.22, + coverage: 100, + safe: true, + lx: 0, + ly: 24, + anchor: 'middle', + }, + { + label: 'Refactron · fmt', + speed: 3.76, + coverage: 99.1, + safe: true, + lx: 0, + ly: 24, + anchor: 'middle', + }, + { + label: 'ESLint · var', + speed: 0.65, + coverage: 100, + safe: true, + lx: 14, + ly: 4, + anchor: 'start', + }, + // The two bottom-left points sit close together — separate their labels + // vertically: the higher point (Comby) labels up, the lower (jscodeshift) down. + { + label: 'Comby · var', + speed: 0.29, + coverage: 47.6, + safe: false, + lx: 13, + ly: -12, + anchor: 'start', + }, + { + label: 'jscodeshift · var', + speed: 0.67, + coverage: 46.0, + safe: false, + lx: 13, + ly: 20, + anchor: 'start', + }, + { + label: 'LibCST · fmt', + speed: 2.68, + coverage: 57.4, + safe: true, + lx: 14, + ly: 4, + anchor: 'start', + }, + { + label: 'Comby · fmt', + speed: 4.79, + coverage: 15.7, + safe: false, + lx: -14, + ly: 4, + anchor: 'end', + }, +]; + +const ScatterChart: React.FC = () => { + const W = 760; + const H = 440; + const padL = 52; + const padR = 40; + const padT = 30; + const padB = 56; + const plotW = W - padL - padR; + const plotH = H - padT - padB; + const xMax = 5.6; + const xFor = (s: number) => padL + (s / xMax) * plotW; + const yFor = (c: number) => padT + (1 - c / 100) * plotH; + + return ( +
    + + + {/* Band caption sits in the band's empty middle so it never collides + with a data point or its label. */} + + ALL SAFE RESULTS LAND IN THIS BAND + + {[0, 25, 50, 75, 100].map(t => ( + + + + {t}% + + + ))} + {[0, 1, 2, 3, 4, 5].map(s => ( + + + + {s}s + + + ))} + + SPEED — WALL-CLOCK SECONDS + + + COVERAGE + + {SCATTER_PTS.map(p => { + const cx = xFor(p.speed); + const cy = yFor(p.coverage); + const color = p.safe ? EMERALD : ROSE; + return ( + + + + + {p.label} + + + ); + })} + +
    + + + safe — compiles + tests pass + + + + unsafe — failed compilation + +
    +
    + ); +}; + +export default ResearchComparison01Page; diff --git a/src/components/ResearchPage.tsx b/src/components/ResearchPage.tsx index 3f5fd08..bf10758 100644 --- a/src/components/ResearchPage.tsx +++ b/src/components/ResearchPage.tsx @@ -1,152 +1,376 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { motion } from 'framer-motion'; -import { Lock, Sparkles } from 'lucide-react'; import useSEO from '../hooks/useSEO'; +/* ─── Tokens shared with ResearchPerf01Page ──────────────────────── */ + +const eyebrow = + 'text-[10px] font-mono uppercase tracking-[0.28em] text-neutral-500'; + +const fadeUp = { + initial: { opacity: 0, y: 18 }, + whileInView: { opacity: 1, y: 0 }, + viewport: { once: true, margin: '-80px' }, + transition: { duration: 0.55 }, +}; + +/* ─── Paper index ────────────────────────────────────────────────── */ + +type PaperStatus = 'live' | 'planned'; + +interface Paper { + no: string; + status: PaperStatus; + date: string; + title: string; + abstract: string; + href?: string; + external?: boolean; +} + +const PAPERS: Paper[] = [ + { + no: '01', + status: 'live', + date: '2026-05-15', + title: + 'Refactron 0.2.0. A measured look at deterministic refactoring at scale.', + abstract: + 'Wall-clock benchmarks for analyze, plan, and the 3-gate verifier on synthetic and real Python fixtures. 45% faster on 100k LOC vs the 0.1 baseline. All scripts and raw runs in the public repo.', + href: '/research/perf-01', + }, + { + no: '02', + status: 'live', + date: '2026-05-15', + title: + 'Refactron vs the codemod baseline. A head-to-head on var → const/let and format → f-string.', + abstract: + 'Two transforms, measured against jscodeshift, Comby, ESLint --fix, and LibCST on identical inputs across speed, coverage, and safety. The unverified codemod tools run sub-second and write code that does not compile; Refactron is the slowest tool measured and the only one that is top-coverage on both transforms while never unsafe.', + href: '/research/comparison-01', + }, + { + no: '03', + status: 'planned', + date: 'Target 2026-06', + title: + 'Legacy patterns in the wild. An empirical survey of the top 100 PyPI packages.', + abstract: + 'How prevalent are the patterns Refactron transforms target? Which packages would benefit most from a deterministic refactoring pass? Distribution by transform, by package age, and by test coverage.', + }, + { + no: '04', + status: 'planned', + date: 'Target 2026-06', + title: + 'Cross-file preconditions for callback_to_async_await. A method paper.', + abstract: + 'Why this transform is the hardest of the ten and how the precondition set is constructed. Walks through the call-graph, the safety constraints, and the cases the transform deliberately refuses.', + }, +]; + +/* ─────────────────────────────────────────────────────────────── */ + const ResearchPage: React.FC = () => { useSEO({ title: 'Research | Refactron', description: - 'Refactron’s internal research on deterministic refactoring and verification is private today. Public notes, benchmarks, and write-ups are coming soon.', + "Refactron's research stream. Performance reports, comparison studies, and method papers on deterministic refactoring with verification-first guarantees.", canonical: 'https://refactron.dev/research', robots: 'index, follow', }); return ( -
    -
    - -

    - Research -

    -

    - We are doing serious work here. - - Not everything is ready to share yet. - -

    -

    - Our work on safe, deterministic refactoring, including verification - design, transform pipelines, and how we benchmark against real - codebases, is handled as{' '} - internal research for now. - Not everything belongs in a landing page; we'll publish what we - can, when it's ready. -

    -
    - -
    - -
    -
    - +
    + + +
    + {/* ── Hero ───────────────────────────────────────────────── */} +
    +
    + +

    Research

    + +

    + What we measure,{' '} + + why we measure it, what we found. + +

    + +

    + Refactron's thesis is that refactoring belongs to deterministic + tools with formal safety guarantees, not to text generators. + These papers are how we hold that claim to scrutiny: published + benchmarks, published methodology, published source. +

    + + {/* Counts strip */} +
    + p.status === 'live' + ).length.toString()} + /> + p.status === 'planned' + ).length.toString()} + /> +
    -

    - Why it stays private -

    -
    -
      -
    • - - Maps, datasets, and methodology drafts evolve quickly; we avoid - publishing half-finished claims. -
    • -
    • - - Some comparisons touch third-party products; we respect - accurate, sourced framing before putting anything on the record. -
    • -
    • - - Customer and partner contexts may fall under confidentiality. We - ship the product first, then share what we can responsibly. -
    • -
    - - - -
    -
    - + +
    +
    + + {/* ── Papers list ────────────────────────────────────────── */} +
    +
    +
    +
    +

    Papers

    +
    +
    +

    Papers

    +
      + {PAPERS.map((p, i) => ( + + ))} +
    -

    - What we'll reveal -

    -
      -
    • - - Deeper notes on verification, structural refactor scope, and how - we think about legacy detection versus lint-only tooling. -
    • -
    • - - Benchmarks and reproducible summaries where we can stand behind - the numbers. -
    • -
    • - - Updates will land here and on{' '} - - Changelog - {' '} - when they go public, with no treasure hunt required. -
    • -
    - -
    +
    +
    - - - ← Back to home - - · - +
    + +

    About this stream

    +
    +

    + Every Refactron research piece commits to three rules. +

    +
      + + Every published number ships with the script that produced + it. If you can't reproduce a claim from{' '} + + bench/ + + , it shouldn't be in the paper. + + + Methodology is described in detail upfront. No cherry-picked + runs, no proprietary inputs, no mystery hardware. + + + Each paper has a Discussion section listing what it does not + measure. We'd rather ship a narrow paper with sharp edges + than a broad one with caveats hidden in the small print. + +
    +
    +
    +
    +
    + + {/* ── Footer / nav ───────────────────────────────────────── */} +
    + +
    +
    + + ); +}; + +/* ═════════════════ Sub-components ═════════════════════════════════ */ + +const DotGridBackdrop: React.FC = () => ( +
    +); + +const Stat: React.FC<{ label: string; value: string }> = ({ label, value }) => ( +
    +

    + {label} +

    +

    + {value} +

    +
    +); + +const Rule: React.FC<{ title: string; children: React.ReactNode }> = ({ + title, + children, +}) => ( +
  • + + {title} + + {children} +
  • +); + +const ExtMono: React.FC<{ href: string; children: React.ReactNode }> = ({ + href, + children, +}) => ( + + {children} + +); + +const PaperRow: React.FC<{ paper: Paper; delay: number }> = ({ + paper, + delay, +}) => { + const live = paper.status === 'live'; + + const inner = ( + + {/* Number column */} +
    +

    + {paper.no} +

    +
    + + {/* Body */} +
    +
    +

    + {paper.date} +

    + - Comparison - - · - +
    +

    + {paper.title} +

    +

    + {paper.abstract} +

    +
    + + {/* Arrow column */} +
    + {live ? ( + - Documentation - - + + + + + ) : ( + + soon + + )}
    -
    + ); + + if (live && paper.href) { + return ( + + {inner} + + ); + } + return inner; }; export default ResearchPage; diff --git a/src/components/ResearchPerf01Page.tsx b/src/components/ResearchPerf01Page.tsx new file mode 100644 index 0000000..5de4ff3 --- /dev/null +++ b/src/components/ResearchPerf01Page.tsx @@ -0,0 +1,1255 @@ +import React, { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { motion } from 'framer-motion'; +import useSEO from '../hooks/useSEO'; + +/* ─── Shared tokens (match ResearchComparison01Page) ────────────── */ + +const eyebrow = + 'text-[10px] font-mono uppercase tracking-[0.28em] text-neutral-500'; + +const MONO = 'ui-monospace, SFMono-Regular, monospace'; + +/* ─── Table of contents ─────────────────────────────────────────── */ + +const TOC: { id: string; label: string }[] = [ + { id: 'abstract', label: 'Abstract' }, + { id: 'headline', label: '01 · Headline result' }, + { id: 'methodology', label: '02 · Methodology' }, + { id: 'results', label: '03 · Results — synthetic' }, + { id: 'pipeline', label: '04 · The full pipeline' }, + { id: 'apply', label: '05 · The apply step' }, + { id: 'discussion', label: '06 · Discussion' }, + { id: 'reproduce', label: '07 · Reproducibility' }, + { id: 'references', label: 'References' }, +]; + +function useActiveSection(ids: string[]): string { + const [active, setActive] = useState(ids[0] ?? ''); + useEffect(() => { + const observer = new IntersectionObserver( + entries => { + const visible = entries + .filter(e => e.isIntersecting) + .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top); + if (visible[0]) setActive(visible[0].target.id); + }, + { rootMargin: '-15% 0px -75% 0px' } + ); + ids.forEach(id => { + const el = document.getElementById(id); + if (el) observer.observe(el); + }); + return () => observer.disconnect(); + }, [ids]); + return active; +} + +/* ─────────────────────────────────────────────────────────────── */ + +const ResearchPerf01Page: React.FC = () => { + useSEO({ + title: 'Refactron 0.2.0 · A Performance Report | Research', + description: + 'Wall-clock benchmarks for Refactron 0.2.0: analyze, plan, and the 3-gate verifier on synthetic and real Python fixtures. Reproducible scripts in the repo.', + canonical: 'https://refactron.dev/research/perf-01', + robots: 'index, follow', + }); + + const active = useActiveSection(TOC.map(t => t.id)); + + return ( +
    + + + {/* ═══════════════ HERO (full-width) ═══════════════ */} +
    +
    + +
    +

    Research · Performance report 01

    + +

    + v0.2.0 · 2026-05-15 +

    +
    +

    + Refactron 0.2.0. A measured look at{' '} + deterministic{' '} + refactoring at scale. +

    +

    + We measured Refactron 0.2.0 on the work users actually do — + analyze a tree, plan a refactor, then verify and apply it. Every + number on this page is a wall-clock measurement. Every script that + produced it lives in the repo. +

    +
    +
    +
    + + {/* ═══════════════ BENCHMARK BAND ═══════════════ */} +
    +
    +

    + Analyze benchmark · v0.1.0-beta.2 →{' '} + v0.2.0 · synthetic + fixtures +

    + +
    +
    + + {/* ═══════════════ TWO-COLUMN: TOC + PAPER BODY ═══════════════ */} +
    +
    + {/* ── Sticky TOC ── */} + + + {/* ── Paper body ── */} +
    + + + {/* 00 · Abstract */} + + 00 · Abstract +

    + Refactron 0.2.0 analyzes a 100k-LOC tree in a median 11.13 + seconds — 45% faster than 0.1.0-beta.2, with run-to-run variance + compressed by 65%. On a real Python project the full analyze → + plan → apply loop, including the 3-gate verifier running pytest + on a shadow tree, completes in roughly five seconds. +

    +

    + This report measures the cost of safety-first deterministic + refactoring + . Every figure is wall-clock; every harness is + public. +

    +
    + + {/* 01 · Headline */} + + 01 · Headline result +
    + + 45% + + + faster analyze on 100k LOC + +
    +

    + vs 0.1.0-beta.2. Median dropped from 20.58s to 11.13s, and the + long-tail variance compressed by 65% — predictable enough to + drop into a pre-commit hook. +

    +
    + Figure 1. Every measured run on 100k LOC; the + horizontal bar is the median. Both spread and centre + collapse from v0.1 to v0.2. + + } + > + + + +
    +
    + + {/* 02 · Methodology */} + + 02 · Methodology +

    Reproducible by design.

    +

    + Each measurement is the wall-clock real time reported by{' '} + /usr/bin/time -p, captured over five runs after a + single warm-up. We report median, min, and max — never a single + best case. For the apply step, the fixture is freshly copied per + iteration because the command mutates the tree. +

    +
    + + + + +
    +
    + + {/* 03 · Results */} + + 03 · Results — synthetic +

    How fast can we walk a cold tree?

    +

    + Synthetic fixtures generated fresh per run from{' '} + bench/gen-fixture.ts + — mixed Python and TypeScript with every legacy + pattern Refactron's ten transforms target. This isolates the + analyze step. +

    +
    + +
    + + Table 1. Median over five runs. Bars show the spread of + all five measured runs; min and max in the same row. Δ% is the + v0.1 → v0.2 median improvement. + +
    + + {/* 04 · Pipeline */} + + 04 · The full pipeline +

    Real fixture, real test suite.

    +

    + Synthetic numbers isolate the analyze step. Real users run the + whole loop. We measure against{' '} + + python-legacy-mini + + — 9 files, 189 LOC, with a pytest suite that + exercises every function the transforms touch. +

    +
    + Figure 2. The three pipeline stages and their + measured median wall-clock. + + } + > + +
    +
    + + + +
    + + Table 2. Median wall-clock per step, five runs each, + fresh fixture copy per apply. End-to-end:{' '} + + ~5.2s + {' '} + from scan to atomically-written refactor. + +
    + + {/* 05 · Apply step */} + + 05 · Inside the apply step +

    Three gates. All or nothing.

    +

    + Of the 3.38-second apply budget on this fixture, roughly 3s is + the test gate — pytest cold-start dominates at 9 files. On + larger projects the ratio inverts: the test gate becomes bound + by your suite, while plan and verification overhead stay roughly + constant. +

    +
    + Figure 3. Every refactor passes three gates before + any byte is written. Any failure drops to the rejected state + — your tree never changes, so there is nothing to roll back. + + } + > + +
    +
    + + {/* 06 · Discussion */} + + 06 · Discussion +

    What this report does and doesn't claim.

    +
    + + A fair head-to-head against jscodeshift, Comby, and{' '} + eslint --fix is its own study — now published as{' '} + + research paper #02 + + . + + + Wall-clock only at v0.2. Peak RSS during the 100k LOC analyze + is in the next pass. + + + Apple M2 only. Linux x86 and Windows numbers when the bench + moves into CI. + + + Fixture generation alone takes ~30s and pushes 8 GB. The bench + script supports{' '} + SIZES=500000 bash bench/run-bench.sh; we don't + publish until we can run it with headroom. + +
    +
    + + {/* 07 · Reproducibility */} + + 07 · Reproducibility +

    Run it yourself.

    +

    + Both bench scripts ship in the public repo. No special hardware, + no proprietary fixtures, no telemetry. If your numbers come out + meaningfully different on Apple Silicon, please open an issue. +

    +
    + + +
    +
    + + {/* References */} + + References +
      + + Synthetic fixture generator and timing harness.{' '} + + bench/gen-fixture.ts + + + + Real Python fixture with pytest suite.{' '} + + fixtures/python-legacy-mini + + + + Opdyke, W. F. (1992).{' '} + Refactoring Object-Oriented Frameworks. PhD thesis, + University of Illinois Urbana-Champaign — the foundation + behaviour-preserving refactoring rests on. + + + Per-file parallelization PR — the source of the 45% win.{' '} + + Refactron_Lib_TS · PR #23 + + + + Refactron vs the codemod baseline —{' '} + + research paper #02 + + . + +
    + + +
    +
    +
    +
    +
    + ); +}; + +/* ═════════════════ Layout primitives ═════════════════════════════ */ + +const DotGrid: React.FC = () => ( +
    +); + +const Block: React.FC<{ + id: string; + last?: boolean; + children: React.ReactNode; +}> = ({ id, last, children }) => ( + + {children} + +); + +const Kicker: React.FC<{ children: React.ReactNode }> = ({ children }) => ( +

    {children}

    +); + +const H2: React.FC<{ children: React.ReactNode }> = ({ children }) => ( +

    + {children} +

    +); + +const P: React.FC<{ children: React.ReactNode; className?: string }> = ({ + children, + className = '', +}) => ( +

    + {children} +

    +); + +const Mono: React.FC<{ children: React.ReactNode }> = ({ children }) => ( + + {children} + +); + +const Cite: React.FC<{ n: string }> = ({ n }) => ( + + [{n}] + +); + +const Caption: React.FC<{ children: React.ReactNode }> = ({ children }) => ( +

    + {children} +

    +); + +const Figure: React.FC<{ + caption: React.ReactNode; + children: React.ReactNode; +}> = ({ caption, children }) => ( +
    + {children} +
    + {caption} +
    +
    +); + +/* Wraps a bare SVG chart in the same panel chrome the diagrams use. */ +const ChartFrame: React.FC<{ children: React.ReactNode }> = ({ children }) => ( +
    + {children} +
    +); + +const AuthorsBox: React.FC = () => ( +
    +

    + Authors +

    +
    +
    + + Om Sherikar ↗ + +

    Founder, Refactron

    +
    +
    +
    +

    + Published +

    +

    2026-05-15

    +
    +
    +

    + Version +

    +

    refactron 0.2.0

    +
    +
    +
    +
    +); + +const FactTile: React.FC<{ label: string; value: string; sub: string }> = ({ + label, + value, + sub, +}) => ( +
    +

    + {label} +

    +

    {value}

    +

    {sub}

    +
    +); + +const Limitation: React.FC<{ title: string; children: React.ReactNode }> = ({ + title, + children, +}) => ( +
    +

    + {title} +

    +

    {children}

    +
    +); + +const RefItem: React.FC<{ n: string; children: React.ReactNode }> = ({ + n, + children, +}) => ( +
  • + + [{n}] + + {children} +
  • +); + +const ExtLink: React.FC<{ href: string; children: React.ReactNode }> = ({ + href, + children, +}) => ( + + {children} + +); + +const CodeBlock: React.FC<{ caption: string; code: string }> = ({ + caption, + code, +}) => ( +
    +
    +

    + {caption} +

    +
    + + + +
    +
    +
    +      {code}
    +    
    +
    +); + +/* ═════════════════ Benchmark band — compact scorecard ════════════ */ + +const PERF_BAND: { + fixture: string; + files: string; + v01: number; + v02: number; + lead?: boolean; +}[] = [ + { fixture: '10k LOC', files: '448 files', v01: 1.31, v02: 1.21 }, + { + fixture: '100k LOC', + files: '4 465 files', + v01: 20.58, + v02: 11.13, + lead: true, + }, +]; + +const PerfBand: React.FC = () => ( +
    +
    + Fixture + v0.1 + v0.2 + Δ median +
    + {PERF_BAND.map((r, i) => { + const delta = ((r.v01 - r.v02) / r.v01) * 100; + return ( +
    +
    +

    + {r.fixture} +

    +

    {r.files}

    +
    +

    + {r.v01.toFixed(2)}s +

    +

    + {r.v02.toFixed(2)}s +

    +

    5 ? 'text-emerald-400/90' : 'text-neutral-500' + }`} + > + −{delta.toFixed(0)}% +

    +
    + ); + })} +
    +); + +/* ═════════════════ Visual: Comparison Chart (monochrome) ══════════ */ + +const ComparisonChart: React.FC = () => { + const v01Runs = [38.65, 24.66, 20.58, 17.58, 14.99]; + const v02Runs = [13.14, 11.2, 11.13, 10.61, 10.56]; + const v01Median = 20.58; + const v02Median = 11.13; + const maxY = 42; + + const w = 560; + const h = 320; + const padL = 48; + const padR = 16; + const padT = 30; + const padB = 36; + const chartW = w - padL - padR; + const chartH = h - padT - padB; + + const groups = ['v0.1.0-beta.2', 'v0.2.0']; + const groupW = chartW / groups.length; + + const xForRun = (gIdx: number, rIdx: number) => { + const groupStart = padL + gIdx * groupW; + const innerPad = 26; + const inner = groupW - innerPad * 2; + const step = inner / (5 - 1); + return groupStart + innerPad + rIdx * step; + }; + const yFor = (val: number) => padT + chartH - (val / maxY) * chartH; + + const yTicks = [0, 10, 20, 30, 40]; + + return ( + + + + + + + + + + + + + {yTicks.map(t => ( + + + + {t}s + + + ))} + + + + {v01Runs.map((r, i) => ( + + ))} + + + {v01Median.toFixed(2)}s + + + {v02Runs.map((r, i) => ( + + ))} + + + {v02Median.toFixed(2)}s + + + {groups.map((g, i) => ( + + {g.toUpperCase()} + + ))} + + ); +}; + +/* ═════════════════ Visual: Results Table (monochrome) ═════════════ */ + +interface Row { + label: string; + files: string; + v01: number; + v02: number; + runs: number[]; + cap: number; + highlight?: boolean; +} + +const ResultsTable: React.FC<{ rows: Row[] }> = ({ rows }) => ( +
    +
    + Fixture + v0.1 + v0.2 + Spread (5 runs) + Δ +
    + {rows.map((r, i) => { + const speedup = ((r.v01 - r.v02) / r.v01) * 100; + return ( +
    +
    +

    + {r.label} +

    +

    {r.files}

    +
    +

    + {r.v01.toFixed(2)}s +

    +

    + {r.v02.toFixed(2)}s +

    +
    +
    + {r.runs.map((run, j) => ( +
    + ))} +
    +

    + {Math.min(...r.runs).toFixed(2)}–{Math.max(...r.runs).toFixed(2)}s +

    +
    +

    5 ? 'text-emerald-400/95' : 'text-neutral-500' + }`} + > + {speedup > 0 ? '−' : '+'} + {Math.abs(speedup).toFixed(0)}% +

    +
    + ); + })} +
    +); + +/* ═════════════════ Visual: Pipeline Hero (monochrome) ═════════════ */ + +const PipelineHero: React.FC = () => ( +
    + + + + + + + + + + + + + + + + {[150, 500, 850].map(x => ( + + ))} + + {[ + { cx: 150, label: 'ANALYZE', median: '0.16s', accent: false }, + { cx: 500, label: 'PLAN', median: '1.70s', accent: false }, + { cx: 850, label: 'APPLY', median: '3.38s', accent: true }, + ].map(n => ( + + {n.accent && ( + + )} + + + + {n.label} + + + {n.median} + + + ))} + +
    +); + +/* ═════════════════ Visual: 3-Gate Diagram (monochrome) ════════════ */ + +const ThreeGateDiagram: React.FC = () => ( +
    + + + + + + + + + + + + {[330, 540, 750].map(x => ( + + + + + ))} + + {[ + { cx: 100, label: 'PLAN', sub: 'RefactorPlan', tone: 'muted' }, + { cx: 330, label: 'GATE 01', sub: 'syntax', tone: 'normal' }, + { cx: 540, label: 'GATE 02', sub: 'imports', tone: 'normal' }, + { cx: 750, label: 'GATE 03', sub: 'tests', tone: 'normal' }, + { cx: 1000, label: 'WRITE', sub: 'atomic', tone: 'accent' }, + ].map(n => { + const stroke = + n.tone === 'accent' + ? 'rgba(74, 222, 128, 0.85)' + : n.tone === 'muted' + ? 'rgba(255,255,255,0.14)' + : 'rgba(255,255,255,0.28)'; + const labelFill = + n.tone === 'accent' + ? 'rgba(74, 222, 128, 0.95)' + : n.tone === 'muted' + ? 'rgba(255,255,255,0.45)' + : 'rgba(255,255,255,0.7)'; + return ( + + + + {n.label} + + + {n.sub} + + + ); + })} + + + FAIL · tree untouched + + + INPUT + + + PASS · all three + + +
    +); + +const PipelineCard: React.FC<{ + step: string; + label: string; + cmd: string; + median: string; + detail: string; + accent?: boolean; +}> = ({ step, label, cmd, median, detail, accent }) => ( +
    +
    +

    + {step} +

    +

    + {label} +

    +
    +

    + {cmd} +

    +

    + {median} +

    +

    {detail}

    +
    +); + +export default ResearchPerf01Page; diff --git a/src/components/ScrollToTop.tsx b/src/components/ScrollToTop.tsx new file mode 100644 index 0000000..b114d5d --- /dev/null +++ b/src/components/ScrollToTop.tsx @@ -0,0 +1,28 @@ +import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; + +/** + * Scroll the window to the top whenever the route pathname changes. + * + * React Router does not reset scroll position on navigation by default, + * so links from a deep page (e.g. /research) into another page (e.g. + * /research/perf-01) inherit the scroll offset of the page that was just + * left. Mount this component once inside to fix that. + * + * If the URL contains a `#hash` we leave scroll alone so in-page anchors + * keep working. + */ +export const ScrollToTop: React.FC = () => { + const { pathname, hash } = useLocation(); + + useEffect(() => { + if (hash) return; + // Use 'auto' (instant) — 'smooth' looks broken on long pages because the + // browser scrolls past content before the new page has finished mounting. + window.scrollTo({ top: 0, left: 0, behavior: 'auto' }); + }, [pathname, hash]); + + return null; +}; + +export default ScrollToTop;