A5 · Part 3 · How it holds up
Current health: what is actually wrong
The checks pass. The debt is in a stuck framework version, a few very large files, repeated code, and documents that have drifted from the code.
07On this page
What passes
| Check | Result, latest local run |
|---|---|
| TypeScript, ESLint, Prettier, cspell | clean |
| Vitest | 1630 of 1630 |
| Build | 1,803 prerendered routes |
| Playwright, one smoke spec | 27 of 27 |
| Lighthouse budgets, 3 URLs, 3 runs each | pass, last run before CI was removed |
TODO, FIXME, HACK in source | 0 |
npm audit, production dependencies | 1 critical, 1 high |
The zero on the TODO row needs a footnote. This codebase has no comments at all, and a pre-push hook enforces that, so there is nowhere in the code for a TODO to live. A zero here does not mean there is no debt. It means the debt has to be written down somewhere else, and this chapter is that place.
1. The framework version is stuck, and it matters
npm audit --omit=dev reports two things:
- Next 16.3.0, critical. Advisories cover Next 16.0.0 to 16.3.2: remote code execution on Windows-hosted servers, and remote code execution in the Image Optimization API when AVIF files are used. The newest release is 16.3.6.
sharp, high. Vulnerabilities inlibheif. The full audit, dev tools included, adds a high advisory againstjs-yaml.
The exposure here is probably narrow. The site imports next/image nowhere and does
not run on Windows. But "probably not exploitable here" is a reason to be calm, not a reason to
stay on the version.
The reason it has not moved is on the record. Dependabot opened a grouped Next update. Its CI run failed Lighthouse on one slow sample: 0.91, with 245 ms of blocking time, on a page that measures 100 and 0 ms locally. It was closed with the others. All 13 Dependabot pull requests so far have been closed, and none merged. Lighthouse now takes three samples, so the same update deserves another run.
Two other updates cannot land without a hand:
- Pyodide fails a unit test until the version typed into
lib/wasmAssets.tsis changed to match. - The dev-tooling group fails at install, because
esbuildis pinned to exactly 0.25.12 and a newer Vite wants more.
2. A few files carry most of the weight
Line counts from wc -l as of 11 October 2026, for source outside
content/ and for the two largest content files:
app/globals.css, about 7,400 lines, is the largest single piece of debt.
The newer parts of the site (the mock interview, the whiteboard and the problems page) use CSS
modules, and the older reader and editor styles still live in the global sheet. Every page
downloads all of it, and nothing tells you which rules are dead.
The three big components each hold a whole feature in one file:
| Component | Lines | useState | useEffect | useRef |
|---|---|---|---|---|
app/whiteboard/Board.tsx | 1,993 | 25 | 17 | 24 |
components/practice/PracticeWorkspace.tsx | 1,825 | 26 | 8 | 15 |
components/practice/CodeEditor.tsx | 1,118 | 22 | 8 | 13 |
None of them has a unit test, and the browser smoke spec only touches the playground. A refactor of them is verified by
hand. The pure parts have already been pulled out and are
tested: lib/whiteboard/model.ts is 695 lines under 17 tests. The next useful split is
the same move for the playground's file and run state.
3. Duplication
- Three runners, one pattern.
lib/runner.ts,lib/pythonRunner.tsandlib/scriptRunner.tseach keep their own module-level worker, their owngetWorkeranddiscardWorker, and their own timeout-and-terminate logic. The default timeouts disagree: 5 seconds for JavaScript, 20 for everything else. A fix to one does not reach the others. - Two share formats, two validators. The playground and the whiteboard share
lib/compress.ts, but each validates its own payload. That one is reasonable, because the shapes differ. It is listed so it stays a choice rather than an accident.
4. Drift between the documents and the code
The code moved faster than what was written about it. Each row below was checked against the repository today.
| Where | Says | Is |
|---|---|---|
docs/ROADMAP.md | 602 prerendered pages | 1,803 prerendered routes in a local build |
docs/ROADMAP.md, section 6 | A weekly cleanup keeping 5 and 5, with 24 hours of grace | Daily, keeping 3 and 2, with 6 hours |
docs/ROADMAP.md, section 6 | public/wasm/ is 18 MB | 1.4 MB; the runtimes come from jsDelivr |
docs/ROADMAP.md, section 7 | No error service; /git shows crawlers 42 characters | Sentry is wired behind a DSN; /git renders its body on the server |
scripts/vercel-cleanup.mjs | Defaults of 5, 5 and 24 hours | The workflow overrides them with 3, 2 and 6 |
lib/wasmAssets.ts | Six runtime versions typed in by hand | Only Pyodide and sql.js are checked against package.json. wasmoon, Ruby, php-wasm and Clang are not dependencies, so Dependabot never sees them |
lib/fonts.ts | Kalam at weights 300, 400 and 700 | The roadmap flagged the 300 weight as barely used; it still ships |
The chapters of this topic had drifted the same way, and that is what the rewrite fixes. Only
the figures that tests/claims.test.ts checks are protected from drifting again.
5. Known and accepted
| Issue | Severity | State |
|---|---|---|
| SQL runs on the main thread with no timeout | Real | A runaway query freezes the reader's own tab. Moving sql.js into a worker would fix it. |
| The heaviest pages have no Lighthouse budget | Real | /practice and the problem pages ship 422 KB of gzipped script and are not on the URL list. |
| The narrator depends on an unofficial service | Accepted | msedge-tts was a known risk when it went in. It degrades to no narration, not to a broken page. |
The policy allows inline scripts and eval | Accepted | Needed by the theme script and the playground. Nothing a reader controls is rendered as HTML. |
| Progress lives in one browser | Accepted, for now | No accounts means no sync. See the roadmap chapter. |
The one thing that looks broken and is not
Run the site with next start and every page logs two 404s, for
/_vercel/insights/script.js and /_vercel/speed-insights/script.js. Those
files exist only on Vercel's edge. Locally there is nothing to serve, so the analytics do not run.
The smoke tests filter exactly these URLs. It is worth knowing, because it is the kind of thing
that gets "fixed" by deleting the analytics.
Two diagrams once lost every arrow. The arrows sat in their own group, and that group had the hand-drawn filter applied. When every child of a group is a straight line along one axis, the group's bounding box has no height or no width. The filter region is derived from that box, so the whole group was clipped away. Arrows now share a group with the boxes, which always has an area. It was invisible in the source and obvious in a screenshot.
Finished reading?
0 of 26 chapters read in this series