refactor: fix the structure audit's findings — 7.9k lines deleted, a real type gate, and four live defects - #53
Merged
Merged
Conversation
…-hot type gate, and four live defects
A parallel audit of the repo asked whether the code is "clean, compact, and scalable and flexible for the long
term", and the answer had three parts: the live code is clean, the repo was not compact, and CLAUDE.md had
stopped describing the codebase. This fixes all of it. 46 files, +2,261/-7,981.
── FOUR DEFECTS THAT WERE LIVE ON THE SITE, all confirmed in built output before and after:
· The homepage told visitors Writing was "coming" while /writing/favourite-quotes was published and in the
nav, and the column carried no link to it. Now DERIVED from getCollection('writing') rather than from a
hand-written promise, so it cannot re-lie: `grep -c wk-tag dist/index.html` → 0, two hrefs to /writing.
· /writing/<slug> linked to /writing#misc. The real id is w-misc — data/writing.ts already exported
kindSectionId() whose docstring is "One speller, so the rail and the page cannot disagree", and the one
route serving every future piece bypassed it. It uses it now.
· The descent graph's readout lost its colour. `.dp-readout b` was a SCOPED selector against nodes the script
creates with innerHTML, which carry no data-astro-cid — so the ochre counter and the seal-red discovery line
never painted, and the <i> rendered as a synthesized oblique because JetBrains Mono ships style:'normal'
only. The two rules are :global() now. Same trap that ate the markdown prose styles last week; the third
instance this month, which is why CLAUDE.md now documents it as reach, not just specificity.
· Arrow keys did two things at once. Deck.astro bound keydown on window and exempted only INPUT/TEXTAREA/
SELECT, so a keyboard reader on a beat chip advanced the beat AND scrolled a whole slide. It honours
e.defaultPrevented now.
── TWO PROTOTYPE ROUTES WERE CRAWLABLE AND SELF-CANONICAL. /proto-showpiece and /proto-sketches never passed
noindex, and BaseLayout emits rel=canonical when it is absent — so internal work-in-progress, including a
scoring rubric, was inviting indexation under its own canonical URL. Both fixed, and tests/protoNoindex.test.ts
now fails if a fifth prototype forgets.
── A TYPE CHECKER, because none ran anywhere. CLAUDE.md claimed "npm run build (tsc-checked)"; it was not —
Astro and vitest both strip types via esbuild, leaving ~4,400 lines of .astro script bodies unchecked. Proof:
tsc found a real TS2345 in tests/skyShader.test.ts while build and test were green. Now @astrojs/check +
`npm run typecheck` (astro check && tsc --noEmit), blocking in CI between build and test.
The first run ever on the .astro files found 84 errors. All were real strict-TS null-safety gaps, and the gate
was NOT weakened to hide them: 79 were fixed by the agents who own those files, and the last 5 — CornerNav and
Toc — by the same alias pattern, which is worth recording because it will recur. The guards were correct; the
problem is that TypeScript does not carry a const's narrowing into a HOISTED `function` declaration, only into
arrow functions, since a hoisted function could in principle run before the guard. `const nav = maybeNav` after
the guard makes the type true at the declaration, so no narrowing has to survive anything.
Also `passWithNoTests: false` — it was true, so breaking the test glob would exit 0, take CI's required check
green, and pass the deploy gate with zero tests.
── ~7,900 LINES DELETED, about a third of src+tests: the QuantPod room (podRoomRetro, podRoomPaint, podScene,
podRoom, podCamera, podScreens, QuantPod.astro, feasible, decisionTree, pathspace, sensitivity, data/moment,
data/method), their 9 specs, tests/_vref, and the .terminal-chrome CSS for a component deleted long ago. All
verified unreachable by import-graph closure from every entry point, re-grepped file by file rather than trusted.
capability.ts SURVIVES because notes/showpiece.md deliberately preserves it — and it now carries a header
comment saying so, since nothing in src/ did, which is exactly how it got flagged as dead.
The most valuable deletion is invisible: tests/podScreens.test.ts asserted that every `period` in profile.ts
parses, so a résumé edit could red CI and block the deploy for a component no page rendered.
── THE SMOKE TEST OVER RENDERED OUTPUT, which is the structural answer to all four live defects. The suite had
819 pure-function tests and ZERO coverage of built HTML (`grep -rn dist tests/` → nothing). tests/distSmoke.test.ts
now parses dist/**/*.html and asserts every in-page fragment resolves, every nav page reaches the footer, every
prototype carries noindex, no page has two <h1>, and every <img> has alt. Both new tests were PROVEN to fail
when their subject breaks — a throwaway proto page without the prop, and #w-misc rewritten back to #misc — not
merely observed to pass.
It immediately caught two more real bugs, both fixed here rather than filed: profile.ts carried a SECOND page
list that had already diverged, so the footer offered CV/Research/Experience/Projects/Art and silently dropped
/writing; and proto-paper rendered the homepage <Toc />, whose stops are the homepage's, shipping five links that
scrolled nowhere. Both skips are now un-skipped: 638 tests, 0 skipped.
── THE WRITING SHELF CAN NOW HOLD WHAT IT PROMISES. Two of the four kinds promise "derivations" and "the
mathematics left in", and there was no math pipeline at all. remark-math + rehype-katex with output:'mathml' —
the load-bearing option, because KaTeX's HTML tree needs its CSS and ~half a megabyte of KaTeX_* webfonts, and a
third downloaded family is against the font rules. Verified end to end with a throwaway .md: <math> in dist,
zero katex CSS/JS/fonts, probe deleted. .prose also gained the rules it never had for table (scrolling the table,
not the document), pre/code, img, figure, h4 and math — in the GLOBAL block, since markdown output cannot be
reached by a scoped one.
── AND CLAUDE.md IS TRUE AGAIN, which the audit called the highest-leverage fix and the cause of three of the
defects above: the guide documented a React island, a terminal, script.ts and Ground.astro — none of which
exist — while saying nothing about the deck, viewport.ts's phone gate, pageStops, data/nav.ts or the writing
collection. It named 2 routes; there are 12. Every fix in this commit landed in files the guide did not mention.
It now also states the content-model rule the audit concluded with: records rendered in more than one shape stay
TypeScript data; unbounded prose with one renderer becomes a collection. Do not converge them.
Five dead React packages uninstalled (~8MB per npm ci, nothing reached the client). tsconfig no longer points
jsx at an uninstalled react. Node pinning audited and consistent; the one hardcoded version in deploy.yml is
documented as edit-together.
Gates: build clean (12 pages), typecheck 0 errors, 638 tests across 39 files, 0 skipped. No browsers were used
anywhere in this batch — every claim above is a command's output or a grep over dist/.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…re the pattern Mine, and caught by the owner reading the diff rather than by anything I did: "it seems like you've added lots of .tmp files." The previous commit used `git add -A`, and 164 ad-hoc verification files had been sitting UNTRACKED at the repo root for a long time — eight `.tmp-*/` directories of measurement probes, driver scripts and screenshots, plus 20 loose `tmp_*.mjs`/`.ts` calculators. `git add -A` does not care that they predate the change; it stages everything not ignored, so all 164 went in. The audit PR's own headline was that this repo is not compact, which makes shipping half a megabyte of abandoned screenshots in it a particularly poor look. Untracked, not deleted: `git rm --cached` keeps every file on disk. They are somebody's working notes — several are the probes behind measurements quoted in commit messages — and throwing them away is not the same problem as tracking them. THE PATTERN GOES IN .gitignore so the mistake cannot recur, and it is deliberately BROAD (`.tmp-*/` and `tmp_*`) rather than a list of the 28 names now present. Each of these was named on the spot by whoever was measuring; a specific list would be stale within a day. Anything worth keeping earns a real name and a real home — anything named `tmp_` is disposable by declaration. Verified: `git ls-files | grep -cE '^(\.tmp|tmp_)'` → 0 tracked, `git check-ignore -v` confirms both patterns match, and the files are still present on disk. Gates unchanged: build clean, typecheck 0 errors, 638 tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ed every deploy Found by a second cleanup sweep BEFORE merge, and it is the most important thing that sweep produced — because CI could not have caught it. deploy.yml's `test` job was checkout → setup-node → `npm ci` → `npm test`, with no build. tests/distSmoke.test.ts (added earlier on this branch) reads dist/**/*.html, and it deliberately THROWS when dist/ is missing rather than skipping, on the principle that a test which quietly passes when its subject is absent is worse than no test. dist/ is gitignored, so a fresh checkout has none. The consequence is the whole pipeline: that job would fail 8 assertions on every push to main, `build` needs `test`, `deploy` needs `build` — so the live site would have frozen at the last good commit while ci.yml stayed green, because ci.yml happens to build before it tests. A gate that is red for a reason unrelated to the change is also the kind people learn to force past. Reproduced before fixing (git archive of HEAD into a clean dir, node_modules symlinked, no build): Test Files 1 failed, Tests 8 failed. The fix is one step, and the file it breaks had already prescribed it — distSmoke.test.ts:29-31 says "deploy.yml's gate job did NOT [build] … so that job needs a build step before its Test step or this file fails the deploy." I wrote that sentence and then did not act on it. ALSO: CLAUDE.md's typecheck bullet said the opposite of the truth. It described the 86-error backlog `astro check` arrived with and told the reader "a red typecheck does not mean you broke it". All 86 were fixed in the same batch that added the gate — `npm run typecheck` reports 0 errors — so that sentence would have taught the next reader to dismiss a real failure as inherited mess, which is how a gate stops being one. It now states the gate is at zero and a red run is yours, and keeps the useful half: the error class was almost entirely ts(18047) "possibly null" where the guard was ALREADY correct, because TypeScript does not carry a const's narrowing into a hoisted `function` declaration — fix with a non-null alias after the guard, not a cast. Gates: build clean (12 pages), typecheck 0 errors, 638 tests across 39 files. deploy.yml parses and its gate job now reads: checkout → Set up Node → npm ci → Build → Test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tes, cut two superseded generations
The owner, on four questions the second cleanup sweep put to him: "i agree with your recommendations."
34 files, +417/-3,589.
── THE HEADLINE IS THE JAVASCRIPT. Shipped JS went 554,898 -> 67,648 bytes, a 87.8% cut, because
/proto-showpiece was the only path to three.js: lib/fanScene.ts held the tree's sole `import * as THREE`, and it
code-split into a 483,207-byte chunk that was 87% of ALL the JavaScript in dist — for one retired internal
prototype on a noindex route. `three` + `@types/three` are uninstalled (36.5MB off every npm ci).
── THREE PROTOTYPE ROUTES RETIRED, because each was an answered question and git keeps them:
· /proto-showpiece was "a kill gate, not a design proposal" — three candidate bottom-sections shown side by
side so one look could kill each. It did its job; the concept it presented is recorded in
notes/showpiece.md as having "died twice". With it go FactorFan, fanScene, fanFrame and protoFrames (1,657
lines), after confirming /proto-sketches imports none of them.
· /proto-ladder was a calibration ladder for the fluid sky, built because the first tune measured a mean
difference of 2.5/255 — literally invisible. A level was picked and shipped, and the page's own printed
numbers are now WRONG (it claims amps .30/.40/.78/1.15 against the shipped [0, 0.7, 1.0, 1.35, 1.8]). A
diagnostic that lies about the thing it calibrated is worse than no diagnostic. Its retirement also kills
FluidSky's `mode="contained"` branch, which it was the only consumer of.
· /proto-paper tested paper-editorial craft and records all three of its distinguishing layers as CUT in its
own header, with the measurements that killed them.
/proto-sketches SURVIVES: the still-frame gate is the one prototype workflow that still pays, and
notes/showpiece.md calls it "the cheapest thing that ever worked".
Mountains.astro (417 lines) loses its last renderer and is DELIBERATELY KEPT — CLAUDE.md documents it as parked
résumé markup — but it now carries a header comment saying it is intentionally unreferenced and where that
decision lives. An unreferenced file with no explanation is what the next sweep deletes; the comment is the
protection, the same one capability.ts got.
── THE SITE NO LONGER SHIPS ITS OWN DESIGN REVIEW. Astro preserves HTML comments, so 79 of them — 30,677 bytes,
9.6% of the homepage — reached every visitor's View Source, including verbatim owner quotes from review
("you may delete everything from below", "it's already useless"). astro.config.mjs was already hiding the proto
routes from crawlers BECAUSE "their rendered bodies quote internal review notes", while the homepage shipped the
same material; that asymmetry was never a decision, just an accident of the toolchain.
A small astro:build:done integration strips them from dist and leaves every one in source, where they are the
best documentation this repo has. It refuses to touch anything inside <script> or <style> — JS and CSS
legitimately contain those delimiters and a careless regex would corrupt the site — and it PROVES it is
text-preserving: the visible text of all 10 pages is byte-identical before and after, checked by two independent
implementations. tests/htmlComments.test.ts asserts zero comments in dist and throws (never skips) when dist is
missing.
── TWO SUPERSEDED GENERATIONS CUT, keeping what live code imports. lib/scenario.ts's 48-trader Monte Carlo over
six tickers is replaced on the shipped slide by data/define.ts's 5 policies x 4 holdings; it is truncated to the
seeded-RNG helpers (mulberry32, gauss) and the types that complexity.ts, policyPnl.ts and data/desk.ts actually
take. lib/problemSize.ts keeps humanCount — the only thing Rules.astro imports — and loses the rest, since the
numbers the slide renders come from complexity.ts. Plus seven dead value exports, each verified to appear only
at its own declaration, including a back-compat alias whose docstring claimed readers existed for a symbol the
tests do not import.
── AND THE TAIL: ~90 lines of CSS whose markup earlier redesigns removed; tests/_mainref + _darkexact.test.ts
(378 lines), the exact twin of the tests/_vref pair deleted last round — its snapshot was 125 lines against a
real file of 224, and its second it() contained ZERO expect() while printing "1918 differ (100%)" on every CI
run; and README.md, which round one never opened and which still advertised the React-island terminal, Inter,
avif, Node 22 and two routes. It is now short and points at CLAUDE.md rather than duplicating it.
TWO THINGS THE VERIFIER CAUGHT that the building agents got wrong about each other, worth recording because both
were invisible in the reports:
· The comment-stripping integration was WRITTEN AND NEVER REGISTERED — 529 lines of correct, tested code that
was a complete no-op in dist, because the agent that wrote it did not own astro.config.mjs and reverted its
wiring. Two lines fixed it. A deliverable that builds green and does nothing is the worst failure mode of
parallel work.
· A hardcoded floor of `expect(protoPages.length).toBeGreaterThanOrEqual(4)` in two proto tests made
"retire a prototype" a red suite. The floor existed to stop a rename silently emptying an it.each, which is
a real risk — so it is now anchored on the surviving route by name rather than on a headcount that turns any
future cleanup into a failure.
Gates: build 9 pages clean, typecheck 0 errors, 625 tests across 39 files, 0 skipped. Zero HTML comments in
dist. No browsers were used at any point in this batch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…browser's smooth scroll The owner, watching his own screen recording: "when i scroll up and down the slides, you notice the sudden change of slides instead of a smooth transition?" He was right, and I have to correct the part he was unsure about: this is NOT a regression. `behavior: 'smooth'` entered in 3bd29bc, the commit that created the deck, and had never been touched since. What he remembers being fixed was one of the neighbours — the mid-panel resting positions, or the nav probe that made the same moment sluggish two days ago. The transition itself has always been the browser's, and the browser's curve is the problem. MEASURED ON HIS CLIP, not guessed: 578 frames at 55.8fps, frame-to-frame luma difference. A slide took ~500ms — so it WAS animating, not teleporting — but the delta went 1.68 → 4.88 in one frame step, i.e. standstill to near-peak velocity in ~35ms, then decayed over ~250ms. A hard launch and a soft landing. "Sudden" is the right word for that shape: the slide leaves before the eye can follow it and then coasts. The clip showed a second defect too: transitions that DOUBLE-HUMPED (a small hump, then a larger one at 8.72s and 5.83s), which is a new scroll starting while the old one is still running. Both come from the same cause, and it is not the easing alone: NATIVE SMOOTH SCROLL CANNOT BE CANCELLED. There is no API to stop one in flight, so an overlapping transition can only be layered on top of the first, and the position lurches. So the deck owns the animation now — a rAF tween over easeInOutCubic, 650ms: · EASES IN as well as out. Symmetric, so departure and arrival read as one gesture. Under 5% of the distance is covered in the first 10% of the time, against the native curve's ramp being essentially over by then. · CANCELLABLE in one cancelAnimationFrame, which is what removes the double-hump. · easeInOutCubic, deliberately, not a spring or an overshooting bezier: this is a page moving to a resting position the reader chose, and overshoot on a full-viewport scroll reads as sloppiness rather than bounce. · 650ms rather than 500: an ease-in spends its opening frames at low velocity, so matching the native duration would force the middle to move faster than the scroll it replaced. `behavior: 'instant'` on every step is load-bearing and not a stylistic choice: global.css sets `html { scroll-behavior: smooth }`, so a plain scrollTo would hand each of our ~40 frames to the browser's own smoothing and the two curves would fight. This is the one place that global override must be bypassed. TEARDOWN CANCELS THE TWEEN. Without it a View Transition leaves a rAF loop still scrolling — and on the next route, one with no deck at all, it would drag the reader down a document that never asked to move. The easing and the duration live in lib/deck.ts because they are pure and therefore testable, which is this repo's rule for anything non-trivial. Six new tests assert the PROPERTIES rather than restating the formula: exact endpoints (a slide must land on its stop), clamping (a late frame must not scroll past and snap back), symmetry, that it genuinely eases in, monotonicity, and that the duration stays in a defensible band. Two comments corrected in the same pass, because the change made them false: the coalescing note described "the ~500ms smooth scroll", and ANIM_MAX_MS described guarding an interrupted smooth scroll — it is now pure belt-and-braces, since the tween releases the lock itself and only a backgrounded tab can leave it held. Gates: build clean, typecheck 0 errors, 631 tests across 39 files (was 625). Verified in the built bundle rather than by reading the diff: the easing is inlined, both `behavior:"instant"` calls and the rAF loop are present. No browser was used — the diagnosis came from the owner's recording via ffmpeg. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…AX already does it
The owner asked the question that dissolved 529 lines: "why do you have that strip HTML comments ts? why not
just delete the source comments directly"
Two answers, and the second one is the interesting one.
WHY NOT DELETE THEM FROM SOURCE: they are this repo's documentation, and this week kept proving it. The audit's
highest-leverage finding was that a STALE GUIDE caused three live bugs; the comment recording "scroll-snap froze
at y=84 through eight gestures" is what stops the next person retrying scroll-snap. A visitor never sees them
either way, so deleting them buys nothing and costs the reasoning that prevents regressions. CLAUDE.md mandates
the style.
WHY THE INTEGRATION WAS STILL WRONG: in a .astro template `<!-- … -->` is HTML and IS emitted, while
`{/* … */}` is a JS expression comment the compiler discards. Astro has always had the answer. So a build-time
hook walking the output with a regex — needing to avoid <script> and <style>, needing an attribute-value parser,
needing a text-identity proof before it dared write — was 529 lines solving a problem the language solves at
compile time, for free, with no build step and no class of risk.
83 comments across 19 files converted. The integration and its own unit tests are deleted.
PROVEN EQUIVALENT, not assumed: with the integration removed and the syntax swapped, every route's rendered word
count is IDENTICAL to the build the integration produced — / 2950, /writing 610, quotes 385, /research 963,
/projects 824, /experience 751, /art 350, /404 388 — and dist carries 0 HTML comments. Same output, minus the
machinery.
ONE THING THE CONVERSION TAUGHT ME, recorded in the test because it will bite again: `{/* … */}` is only valid
in TEMPLATE position. Inside an expression that is already open — `{size === 'large' && ( … )}` in PlushCow —
the braces are read as an object literal and the build fails with "Expected ) but found $$render". There the
comment must be a bare `/* … */`. One site, found by the build, fixed by dropping the braces.
tests/htmlComments.test.ts survives its subject: it stopped unit-testing a deleted stripper and now asserts only
the PROPERTY — zero comments in the built HTML — plus that dist is non-empty, so a build that emitted nothing
cannot pass as clean. Its failure message names the correct syntax, including the expression-position trap.
── AND FOUR ORPHANS, 793 lines, found by rebuilding the import closure after the prototype retirement:
· src/sections/proto/PaperHeights.astro (160), proto/PaperInterlude.astro (106),
components/proto/PaperTooth.astro (75) — /proto-paper's exclusive parts. I retired the route last commit and
missed its components, which is exactly the second-order effect that sweep had warned about. src/sections/proto/
is now gone entirely.
· src/sections/Mountains.astro (452) — the parked résumé section, on the owner's call. /proto-paper was its
last renderer, so nothing has rendered it since. Everything it held lives on /research, /experience and
/projects, and git keeps the markup. A parked section nobody can see is a file that gets rediscovered and
re-flagged by every sweep; deleting it ends that loop rather than adding another protective comment.
Gates: build clean (9 pages), typecheck 0 errors, 604 tests across 39 files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…reer names in
Two polish items the owner found, and both were a mismatch rather than a missing feature.
── 1. "where to start doesn't have the smooth inter tab animation as other slides."
It had one. It was 260ms, while `.ch-panel` (Choice) and `.ru-panel` (Rules) are both `.5s ease`. On a
crossfade between two stacked panels that difference is the whole effect: at 260ms the outgoing text is still
legible while the incoming text is already solid, so the eye reads two states rather than one dissolving into
the other. At .5s they overlap in a way that reads as one movement.
Worth noting what it was NOT, because that is where I would have looked first: the stacking was already right —
`.so-textbox` is a one-cell grid with every panel in `grid-area: 1 / 1`, exactly like the other two — and the
reduced-motion gate was already correct. Three slides that share an interaction have to share its timing, or the
odd one out feels broken for a reason nobody can name.
── 2. "the descent curve just have the name pops up. can we let the career names fades in with animations?"
They popped: each waypoint was drawn at full opacity on the first frame the reveal passed its arrival, so eight
names snapped into place while the trail beneath them moved smoothly. The line eased and the type did not, and
that mismatch is what reads as abrupt.
Each waypoint now fades in over 0.055 of the walk — measured in REVEAL units, not milliseconds, so it stays in
step however the walk is paced and survives `replay the walk` restarting it. Arrivals are 1/7 apart for eight
waypoints, so the fade is ~38% of the gap: long enough to read, finished before the next name arrives, so two
never fade at once. The dot and its name share one alpha, because they are one object arriving — fading only
the type would look like a label catching up with a dot that was already there.
A BUG I WROTE AND CAUGHT BEFORE BUILDING, worth recording because it would have been embarrassing and quiet: the
last waypoint's arrival is exactly 1, so `(reveal - arrival) / WP_FADE` is 0 at the end of the walk — the newest
and most important name on the graph would have been invisible, permanently, and the reduced-motion path (which
sets reveal = 1 and draws one finished frame) would have shown seven names out of eight. A `reveal >= 1`
short-circuit fixes it, and the comment says why it is load-bearing rather than defensive.
Alpha is saved and restored around the loop rather than reset to 1, so the climb callout below — which sets its
own 0.5 — cannot be broken by this, and neither can any future caller that draws the loop inside an alpha.
Verified in the built output, not the diff: `.so-panel{transition:opacity .5s ease}` in the CSS bundle, and the
fade math inlined in DescentPath's JS.
Gates: build clean (9 pages), typecheck 0 errors, 604 tests across 39 files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The owner: "i want you to delete that notes folder at the root." Two files, 2 design notes, gone. THE REFERENCES ARE THE ACTUAL WORK. Eleven comments across eight files named `notes/showpiece.md`, and a comment pointing at a deleted file is the precise failure this whole session has been fixing — it sends the next reader after something that is not there, which is how a stale CLAUDE.md caused three live bugs. Every one is rewritten to say where the reasoning lives now: inline, or in the commit history. ONE OF THEM MATTERED MUCH MORE THAN THE OTHERS. src/lib/capability.ts is deliberately unreferenced — kept because a design note said the statistics were honest and might serve a later slide — and its header explained that by CITING the note. Delete the note and that file becomes an orphan with a comment pointing into the void, which is exactly how it would get deleted by the next sweep as dead code. It survives because the header already QUOTED the decision verbatim rather than only linking to it, so the justification outlives its source. The comment now says so explicitly: a pointer outlives its target, a quotation does not. That is the general lesson from the notes/ deletion, and it is written where it will be read. Also fixed: proto-sketches rendered "are recorded in notes/showpiece.md" as VISIBLE PAGE TEXT, so deleting the folder would have made a shipped sentence false rather than just a comment. Gates: build clean (9 pages), typecheck 0 errors, 604 tests. The only surviving mention of `notes/` is the one line that says it is deleted, which is true. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A parallel audit asked whether this repo is "clean, compact, and scalable and flexible for the long term". The answer had three parts: the live code is clean, the repo was not compact, and
CLAUDE.mdhad stopped describing the codebase. This fixes all of it.46 files, +2,261 / −7,981. Gates: build clean (12 pages), typecheck 0 errors, 638 tests across 39 files, 0 skipped. No browsers used anywhere — every claim below is a command's output or a grep over
dist/.Four defects that were live on the site
/writing/favourite-quoteswas published and in the nav — and the column had no link to itgetCollection('writing')instead of a hand-written promise, so it cannot re-lie.grep -c wk-tag dist/index.html→ 0/writing/<slug>linked to/writing#misc; the real id isw-misckindSectionId()— which already existed, with the docstring "One speller, so the rail and the page cannot disagree", and the one route serving every future piece bypassed it.dp-readout bwas a scoped selector against nodes the script creates viainnerHTML, which carry nodata-astro-cid— so the ochre counter and seal-red discovery line never painted, and the<i>rendered as a synthesized oblique (JetBrains Mono shipsstyle:'normal'only). Now:global()Deck.astroboundkeydownon window and exempted only INPUT/TEXTAREA/SELECT, so a keyboard reader on a beat chip advanced the beat and scrolled a whole slide. Honourse.defaultPreventednowThe readout bug is the third instance this month of the Astro scoping trap, so
CLAUDE.mdnow documents it as a reach problem, not just a specificity one.Two prototype routes were crawlable and self-canonical
/proto-showpieceand/proto-sketchesnever passednoindex, andBaseLayoutemitsrel=canonicalwhen it's absent — so internal work-in-progress, including a scoring rubric, was inviting indexation under its own canonical URL. Fixed, plustests/protoNoindex.test.tsfails if a fifth prototype forgets.A type checker, because none ran anywhere
CLAUDE.mdclaimed "npm run build (tsc-checked)". It wasn't — Astro and vitest both strip types via esbuild, leaving ~4,400 lines of.astroscript bodies unchecked. Proof:tscfound a real TS2345 intests/skyShader.test.tswhile build and test were both green.Now
@astrojs/check+npm run typecheck(astro check && tsc --noEmit), blocking in CI between build and test. The first run ever on the.astrofiles found 84 errors, all real strict-TS null-safety gaps. The gate was not weakened to hide them — 79 fixed by the agents owning those files, the last 5 by an alias pattern worth recording because it will recur:Also
passWithNoTests: false. It wastrue, so breaking the test glob would exit 0, take CI's required check green, and pass the deploy gate with zero tests.~7,900 lines deleted — about a third of
src+testsThe QuantPod room (
podRoomRetro,podRoomPaint,podScene,podRoom,podCamera,podScreens,QuantPod.astro,feasible,decisionTree,pathspace,sensitivity,data/moment,data/method), their 9 specs,tests/_vref, and the.terminal-chromeCSS for a component deleted long ago. All verified unreachable by import-graph closure from every entry point, then re-grepped file by file rather than trusted.capability.tssurvives becausenotes/showpiece.mddeliberately preserves it — and now carries a header comment saying so, since nothing insrc/did, which is exactly how it got flagged as dead.The most valuable deletion is invisible:
tests/podScreens.test.tsasserted everyperiodinprofile.tsparses, so a résumé edit could red CI and block the deploy for a component no page rendered.The smoke test over rendered output
This is the structural answer to all four live defects. The suite had 819 pure-function tests and zero coverage of built HTML (
grep -rn dist tests/→ nothing).tests/distSmoke.test.tsnow parsesdist/**/*.htmland asserts: every in-page fragment resolves, every nav page reaches the footer, every prototype carriesnoindex, no page has two<h1>, every<img>hasalt. Both new tests were proven to fail when their subject breaks — a throwaway proto page without the prop, and#w-miscrewritten back to#misc— not merely observed to pass.It immediately caught two more real bugs, both fixed here rather than filed:
profile.tscarried a second page list that had already diverged — the footer offered CV/Research/Experience/Projects/Art and silently dropped/writing.proto-paperrendered the homepage<Toc />, whose stops are the homepage's, shipping five links that scrolled nowhere.Both parked skips are now un-skipped: 638 tests, 0 skipped.
The writing shelf can now hold what it promises
Two of the four kinds promise "derivations" and "the mathematics left in", and there was no math pipeline at all. Added
remark-math+rehype-katexwithoutput: 'mathml'— the load-bearing option, because KaTeX's HTML tree needs its CSS and ~half a megabyte ofKaTeX_*webfonts, and a third downloaded family is against the font rules. Verified end to end with a throwaway.md:<math>indist, zero katex CSS/JS/fonts, probe deleted..prosealso gained the rules it never had fortable(scrolling the table, not the document),pre/code,img,figure,h4and math — in the global block, since markdown output cannot be reached by a scoped one.And
CLAUDE.mdis true againThe audit called this the highest-leverage fix, and it's the cause of three of the defects above. The guide documented a React island, a terminal,
script.tsandGround.astro— none of which exist — while saying nothing about the deck,viewport.ts's phone gate,pageStops,data/nav.tsor the writing collection. It named 2 routes; there are 12. Every fix in this PR landed in files the guide did not mention.It now also states the content-model rule the audit concluded with: records rendered in more than one shape stay TypeScript data; unbounded prose with one renderer becomes a collection. Do not converge them.
Housekeeping
Five dead React packages uninstalled (~8MB per
npm ci; nothing reached the client).tsconfigno longer pointsjsxat an uninstalled react. Node pinning audited and consistent — the one hardcoded version indeploy.ymlis documented as edit-together.🤖 Generated with Claude Code