28 KiB
Engineering Review — 2026-07-02
Point-in-time record. Full-project engineering review requested by the maintainer, with a specific question attached: "as we work session to session, we may forget to look back and see the bigger picture — the end-of-session skill checks aim to mitigate it, but I'm not sure to what extent it is successful."
Method: five independent clean-context reviewers (code quality, test suite, documentation, build/tooling/delivery, cross-session coherence), each required to cite file/line/commit evidence, followed by an adversarial verification wave that reproduced every load-bearing claim (rebuilt the bundle, re-ran greps and git archaeology, hand-walked the flagged logic, re-checked the live site). Findings below are only those that survived verification; where a reviewer's number was wrong, the corrected number is used. Reviewed state: working tree at
c5e4c4cplus the uncommitted spec-params/editor WIP.
Verdict
The codebase itself is in excellent shape — the architecture contract holds under grep, not just in prose, and the per-session review machinery demonstrably works at the session scale. The problems are concentrated at two horizons the per-session view cannot see: delivery (one measurable production defect: the marketing landing executes 1.3 MB gzipped of Monaco + Vega it never uses) and look-back. The maintainer's fear is confirmed, mechanically: no instrument — on-demand or scheduled — owns the whole-project view, so coherence work happens only when a session happens to collide with it. (Maintainer clarification after the first draft: the eng-council sweep and the ux-second-pass batch were designed as on-demand session guardrails, not periodic instruments — which sharpens the finding rather than softening it: the on-demand instruments have each fired once, and nothing at all runs on a cadence.)
Scorecard
| Dimension | Grade | One-line summary |
|---|---|---|
| Code quality | A | Zero any/suppressions, layering verifies by grep, error discipline is real |
| Test suite | A− | 1,366 tests, uniform harness, deterministic; one committed coverage hole that matters |
| Documentation | A− | Symbol-level accuracy at scale; drift is localized fossils plus one governance gap |
| Build / delivery | B | Strong local gates; landing bundle defect shipped because no gate measures output |
| Cross-session coherence | B− | A− for per-session machinery, D for look-back machinery |
Critical findings (all independently verified)
C1. The landing and /learn/ eagerly load and execute Monaco (940 kB gz) and Vega (285 kB gz)
Verified on the live site and reproduced from a clean build. dist/index.html modulepreloads
both chunks and the landing entry's static import graph executes them
(main-*.js ends with bare import"./vega-*.js";import"./monaco-*.js"). /learn/ is worse:
both arrive as direct <script type="module"> tags. Landing eager JS today: ~4.7 MB raw /
~1.30 MB gzip; the intended payload is ~255 kB raw / ~81 kB gzip — a 16× reduction available.
The source-level lazy-loading discipline is correct (LandingChart.tsx dynamic-imports
chart-renderer; nothing in src/landing/src/learn mentions Monaco). Two bundling-level
causes defeat it:
- Preload-helper placement. The object-form
manualChunksinvite.config.tsleads Rollup to emit Vite's__vitePreloadhelper inside themonacochunk;LandingChart-*.jsbeginsimport{_ as h}from"./monaco-*.js", so every chunk that usesimport()statically depends on all of Monaco. (Attribution to the object form specifically is plausible-but-untested; the observed mechanism is fully reproduced.) vega-scaleedge.Landing.tsx→@core/vega-themes→theme-controls.ts:27import { scheme } from 'vega-scale'merges into thevegamanual chunk, dragging all of Vega into the landing's static graph. The comment attheme-controls.ts:21-22("keeps the umbrella vega out of core") is true at source level and defeated by chunking. Theexampleschunk has a second, independent edge into the vega chunk.
Fix: break the vega-scale edge (lazy-import or move scheme resolution off the
landing-reachable path); switch to function-form manualChunks and confirm the helper lands
in a shared micro-chunk; and — the durable part — add a ~10-line post-build assertion that
dist/index.html and dist/learn/index.html reference neither monaco-* nor vega-*.
That assertion is the missing gate for this entire regression class.
C2. The project's operating regime is recorded nowhere the machinery reads
The 2026-06-10 phase shift — spec follows code; docs/spec is no longer the authoritative
contract — appears in exactly one repo location: docs/exploration/chart-builder-enhancement-scope.md:673,
inside the directory whose charter says nothing there is kept current. Meanwhile CLAUDE.md
("authoritative behavioral specification… This is the contract"), AGENTS.md ("Spec is the
contract"), SOUL.md, arch 00 ("the spec wins"), and .claude/skills/alignment/SKILL.md
rules #3/#17 all still assert the old regime. Two reviewers independently converged on this,
and the verification wave reproduced the greps.
This is the worst coherence defect because it corrupts the corrective machinery itself: the
wrap-up protocol's whole design is clean-context subagents judging against the recorded
contract — and the recorded contract is wrong. Fix: one paragraph in CLAUDE.md/AGENTS.md
stating the regime (spec is descriptive, kept current with code; on conflict fix the spec),
softened phrasing in docs/spec/README.md and arch 00, and an update to the alignment skill.
C3. The spec stopped absorbing new behavior at a verifiable cutover (2026-06-21 → 06-25)
Adjudicated timeline, commit-dated: through d76a7a5 (06-21) every feature landed with its
spec section in the same commit (Theme Builder, FontAsset, data inspector, onboarding, URL
datasets — all specified at full fidelity). From c19857b (06-25) onward the discipline
inverted: 12 of 13 feature commits through 06-30 updated docs/architecture/ in-commit and
zero touched docs/spec/. Unspecified user-facing surfaces at HEAD:
- the entire /learn/ section (
grep -ril learn docs/spec/→ nothing); - the composition wireframe (drag reorder / pull-out / stack / Simplify — zero matches);
- editor transform actions and CodeLens scaffolds (wrap/simplify/add-view, add-transform);
- the multi-view Data Inspector view picker (
docs/spec/04-live-preview.md:84references "the chosen view's table" — the chooser it refers to is specified nowhere); - the current uncommitted params-scaffolding work continues the pattern (arch 08 only).
Even under the spec-follows-code regime this is debt: the "then amend the spec to match"
half of the bargain has not happened for ten days of features. Root cause is structural, not
negligence: /doc-update flushes what a session remembers and /alignment reviews diffs —
no instrument owns "the spec describes the product." Alignment rule #17 covers removed/moved
surfaces only, not never-specified additions.
Significant findings
S1. No instrument owns the big picture on any cadence
- Eng-council sweep: once, 2026-06-12 (
0e225d7). Since then non-test source grew ~+76% (16.8k → 29.7k ts/tsx LOC; ~21.3k → ~37k all-source).docs/codebase-metrics.md, whose stated purpose is the trend, holds two same-day rows. Specific unswept accretion: the six-modulespec-*service family pluseditor-snippet/editor-cursor-lens(~1,900 LOC, mostly post-sweep) — which alignment rule #16 already notes "has grown by copy-paste twice." - Batched council pass over
docs/ux-second-pass.md: once, 2026-06-13 (92bfe88, 76→25 lines — the design worked). The file has regrown 25→48; oldest open item is from 06-16. - TODO gardening: never. Lifecycle is bimodal — 56 added / 43 removed over history
(~71–77% resolution, usually within 0–3 days, at least 10 by dedicated commits), but every
TODO that survived its first week is still alive. Oldest two are from 06-12:
src/app/modals/ModalCoordinator.ts:36(a latent init-ordering bug, not polish) andChartBuilderModal.tsx:524. The open set is increasingly the hard/ambiguous residue no session claims.
Fix direction (maintainer to ratify): give each instrument a trigger — re-sweep when
source LOC grows ~25% past the last metrics row; batch council pass when ux-second-pass open
items exceed a count or age past two weeks; a spec-reconciliation clause in /doc-update
("did this session add user-facing behavior? name the spec section or write it"); TODO
gardening as a standing sweep agenda item.
S2. The scaffold insertion math is untested, duplicated, and validity-critical
Verified: runAddTransform (spec-transform-scaffold.ts:130-146, committed c5e4c4c) and
runAddParam (spec-param-scaffold.ts:109-142, WIP) compute insert offsets, indentation,
and the trailing-comma decision; a bug here writes invalid JSON into the user's editor.
No test exercises the composition — no SpecEditor.test.tsx exists, so there is no indirect
path either. The create-property block is a verbatim 7-line duplicate between the two files;
the completion providers share ~35 structurally identical lines. Hand-walking the current
logic found no live bug — this is a coverage hole, not a defect — but a future inversion
of the ternary or offset drift ships silently. The repo's own TODOs
(spec-param-scaffold.ts:33, spec-params.test.ts:6) already point at the fix: extract a
neutral core module (working name spec-snippet) computing
(text, host offset, existing-array?) → {insertOffset, snippetText} and table-test it over
empty-host / host-with-following-key / compact-one-line / host-as-last-property.
Related: spec-transform-actions.ts (663 lines, largest service, no test file) carries pure
unexported helpers with real branching — reindent, defaultFacet, defaultRepeat (:106,
:123, :136) — one layer short of tested core, mitigated by buildNext being a thin
dispatcher over tested core wrappers.
S3. Documentation drift cluster (6/6 claims verified)
All in otherwise highly accurate docs; each is a fossil a maintainer would act on:
- 5 MB budget fossil.
docs/spec/09-data-model.md§E and08-import-export.mddescribe a budget-with-fill-warnings storage monitor that spec 02/10, arch 02 §6, and the code all contradict (the monitor is a composition breakdown; the only 5 MB logic is the import pre-check intransfer.ts:42). Arch 02:445's "80% threshold" Do-line contradicts the same doc's line 425 and matches nothing in code. - "Not installable" is false. Arch 10:625 claims the manifest ships no icons; four icons
ship (
vite.config.ts:85-89, files inpublic/), and manual-verification actively tests install. - The ajv layer was never built. Arch 08 presents it as planned-M2 and claims "we
deliberately do better: map ajv errors to editor positions" — no such module exists; ajv
isn't even in
package.json. The current WIP on arch 08 does not touch this. - Wrong store name in three docs. Arch 01/03/04 name
useSettingsPopoverStore/openSettingsPopover; the code isusePopoverStore/openPopover(PopoverStore.ts:22,30, called fromEventRouter.ts:96). Grep by the documented name finds nothing. - Arch 03's "closed union" is stale. Five modals listed;
modals/types.tshas six (themeBuilder). Same doc's add-a-modal checklist instructshasError/getErroronModalConfig— fields the real type doesn't have; following it fails typecheck. - Spec 09's UserSettings omits
ui.dataInspectorOpen/ui.dataInspectorHeight, which spec 04 mandates andsettings-store.ts:158-183persists (they're also absent fromcore/settings.ts's type — the code half of the same gap).
S4. Delivery gates never observe build output, and caching is misconfigured
- All quality gates live in the (excellent) pre-commit hook — lint-staged + full typecheck +
the whole test suite — but nothing anywhere runs or inspects
vite build. Cloudflare's build fails safe on compile errors (previous deploy stays live) but silently; the escape class is green-but-wrong output, which is exactly how C1 shipped. Cheapest durable fix: the C1 post-build assertion in thebuildscript; optionally a minimal CI workflow (typecheck + lint + test + build + assertion) to cover--no-verifyand web edits. - Hashed
/assets/*are servedcache-control: max-age=14400, must-revalidate(Cloudflare Pages default; nopublic/_headersexists). Content-hashed files are the textbookmax-age=31536000, immutablecase; today every returning visitor revalidates a 3.6 MB chunk every 4 hours. HTML cache headers are correct.
S5. Module-size outliers in the chart builder
src/core/chart-builder.ts (1,851 lines: catalog + rules + smart defaults + assembly/parse)
and ChartBuilderModal.tsx (1,653 lines, ~25 private subcomponents; 2.1× the next-largest
component). Internally cohesive, well-documented — maintainability risk, not defect. Split on
next substantive touch (chart-builder/{catalog,rules,defaults,assemble}.ts; hoist the
modal's sections into sibling files).
S6. Smaller verified items
noUncheckedIndexedAccessis off in a parser-heavy codebase (jsonc-parser node walking inspec-params.ts,spec-cursor.ts,spec-insert.ts) that is exactly its target class. Enable and sweep; if parser noise proves disproportionate, record the rejection.ajvis imported byspec-params.test.ts:1but undeclared — a phantom transitive dependency; add it to devDependencies (found during verification).npm audit: monaco 0.54.0 bundles a vulnerable DOMPurify (moderate; fix is 0.55.1, flagged breaking — plan deliberately); esbuild advisory is dev-only/Windows-only.- Landing/learn light-dark toggle duplicated since 06-25, TODO'd in both copies and reworded 06-28 instead of extracted — a breadcrumb treated as the deliverable.
Minor findings
- Per-editor CodeLens providers register on the global
'json'language (editor-cursor-lens.ts:83) while closing over one editor's cursor — safe with today's single editor (verified: onemonaco.editor.create), latent cross-wiring if a second JSON editor appears. One guard line:if (model !== editor.getModel()) return. - One avoidable non-null assertion in the WIP:
spec-transform-scaffold.ts:176. navigator.platform(deprecated) inEventRouter.ts:38.- Hooks are untested as a class;
useFocusTrap(a11y-load-bearing, shared by all overlays) is the one worth a cheap happy-dom test.ModalCoordinator's open/close/replace sequencing likewise (~5 tests). - No coverage reporting exists; for a philosophy that is explicitly proportional ("core
hardest"), nothing measures the proportion.
@vitest/coverage-v8scoped tosrc/core+src/app/{stores,services}, no thresholds, visibility only. - localStorage write failures are console-only (
settings-store.ts:80,ux-prefs.ts:80) — judged an acceptable, explicit low-stakes swallow; noted so the asymmetry with the IndexedDB path stays a decision. /learn/is reachable only from the landing; nothing in the app links it. Deliberate or forgotten is unrecorded — which is itself the gap.- Doc/housekeeping nits: pre-commit hook is stronger than AGENTS.md documents (says eslint;
runs typecheck + tests too — drift in the good direction); PWA precache carries ~150–200 kB
of landing/learn assets the
/app/-scoped SW can never serve; spec 00 still calls settings a modal; spec 02 overstates the "imported" tag (only foreign/older shapes are tagged — spec 08 has it right);docs/embedding-vega-lite.md(447 lines) is orphaned — index it with a charter line or move it;core/settings.ts:64andHeaderControls.tsx:2cite the old pre-move path of the theming scope doc; IMPLEMENTATION-PLAN files a completed item under "Next"; emptyops/directory;vega-themespinned exact with no recorded rationale; manual-verification.md lacks checks for the shipped theming/export surface (Theme Builder gallery, font upload incl. variable fonts, PNG/SVG export, clipboard).
Strengths (verified, and worth keeping deliberate)
- The architecture contract verifies by grep, both directions. No browser APIs, React,
or Monaco in
src/core/; storage APIs confined toinfrastructure/; landing/learn import no stores/orchestration/components. - TypeScript rigor at the top of the distribution. Zero
any, zero@ts-ignore/@ts-expect-error, exactly one non-null assertion across ~30k lines; type-aware ESLint (incl.no-floating-promises) passes clean; every intentionally unawaited promise is an explicitvoidwith a comment. - Error-handling discipline is real. Every swallowed catch inspected carries a rationale naming the sanctioned fallback; real failures route to notifications with actionable copy; quota errors are normalized and surfaced.
- Concurrency/resource engineering above typical app code.
LivePreview's generation token + render mutex (with reasoning written down), full Monaco disposable cleanup, the finalize-before-unsubscribe edge handled in chart-renderer. - The test suite is engineered, not accumulated. 1,366 tests / 8.5s, zero snowflake
harnesses across all 17 component test files, injected clocks and fake timers keyed to
exported constants, fresh
IDBFactoryper test (including interrupted-upgrade self-healing), tests that assert contracts with the why inline (spec-refs.test.ts,chart-renderer.test.ts). - Session-scale coherence is solved. All 12 stores share one canonical shape and cite
their canonical sibling in doc comments; modals go through one registry/coordinator/shell;
subtraction happens (
PreviewStoredeleted when obsoleted). The "written by strangers" failure mode is absent. - The breadcrumb→fix pipeline works. At least 10 TODOs resolved by dedicated commits (quota rollback, hash routing, splitter ARIA, field-name escaping…); the eng-council→law loop is real (sweep findings became alignment rules #15–#17; skills keep evolving).
- Docs are accurate at symbol level at scale — of ~35 spot-checked claims (constants, record shapes, hash grammar, keyboard maps, API surfaces), nearly all exact — and the "shipped divergence" fencing discipline keeps pedagogical sketches from lying.
- PWA configuration is best-practice:
registerType: 'prompt'properly consumed with a durable update toast, SW scope narrowed to/app/, sophisticated font precache strategy with reasoning in the config.
Recommended sequence
- Record the regime (C2): CLAUDE.md/AGENTS.md paragraph + alignment-skill update + soften spec README/arch 00. Smallest fix, unblocks every future clean-context review. Done same day: CLAUDE.md, AGENTS.md, SOUL.md, spec README, and alignment rules #3/#17 now state the spec-follows-code regime; #17 extended to cover never-specified additions.
- Fix the landing bundle and add the post-build assertion (C1): the only
production-visible defect, and the assertion closes the gate gap (S4) for free. Add
public/_headersfor immutable assets while in there. Done same day:schemeColorssplit intocore/scheme-colors.ts; function-formmanualChunkswith explicit homes for the preload helper and the shared light packages (vega-themes, vega-expression, vega-util, json-stringify-pretty-compact — Rollup was absorbing each into the nearest vendor chunk);scripts/check-light-entries.mjsgates every build;public/_headersships immutable caching. Landing eager JS measured ~98 kB gzipped, down from ~1,300 kB; all three entries smoke-tested on the production build (charts + Monaco render, zero console errors). - Spec back-fill sprint (C3): specify /learn/, the wireframe, editor actions/scaffolds,
and the view picker; add the spec-reconciliation clause to
/doc-update. - Give the periodic instruments triggers (S1) and run the overdue ones: an eng-council
re-sweep (which naturally absorbs S5's splits and the
spec-*family consolidation), a ux-second-pass batch pass, and TODO gardening — starting withModalCoordinator.ts:36. - Extract and test the scaffold insertion core (S2) before the params WIP is committed — the TODOs already name the module.
- Doc drift batch (S3 + minors): one session, mostly deletions of fossils.
Addendum: subtraction audit (same day)
Follow-up requested by the maintainer: how much of the codebase is duplication, boilerplate, or unnecessary wrapping — with the explicit instruction that deliberate architecture layers are not exempt; "less LoC = less surface for bugs" outranks ceremony. Two auditors (a jscpd-quantified duplication pass and a wrapper/indirection pass with the contract layers on trial); the largest verbatim-copy claims were independently re-diffed before inclusion.
The numbers
- Production duplication is low: ~0.9% by jscpd at min-tokens 50 (268 duplicated lines over ~29.5k production TS/TSX; roughly double counting renamed semantic twins). Test-file duplication is ~5.4% but is the documented harness convention — benign. CSS Modules are the highest-duplication format at 5.4% and deserve one deliberate pass.
- Pure ceremony is ~1.5% of non-test LoC. The plumbing layers (infrastructure +
orchestration + modals + hooks, ~2.8k lines, ~9% of src) are overwhelmingly load-bearing:
db.ts's promise wrapping/self-healing/quota normalization,url-hash.ts's total-parse grammar,remote-data.ts's size-cap and error classification, the modal coordinator's snapshot/confirm/URL lifecycle all earn their lines on inspection. No generic machinery with a single instantiation was found —editor-cursor-lenshas three real consumers,wireEntityWriteThroughfour, and every modal-registry field varies across entries. - Confidently deletable today, behavior identical: ~450–500 LoC, plus ~100–150 more behind design decisions.
The diagnosis, verbatim from the audit because it generalizes: the abstractions are
right; the residue is photocopied instantiation files and hand-unrolled adapter pairs that
the abstractions should have absorbed. Nothing calls for architectural change — only
tidying inside the architecture. Notably, one suspicion from the main review was stale: the
orchestration wirers were already consolidated behind wireEntityWriteThrough; the leftover
per-entity files are the residue that consolidation should have deleted.
The cut list (verified; ranked by value)
Do right away — all mechanical, most protected by existing tests:
- Per-preference persistence chain (~90–110 LoC; the compounding win). Five
near-identical load/save pairs in
settings-store.ts:116-184, four identical 10-line init/wire pairs inorchestration/preferences.ts, same shape again inorchestration/settings.ts/snippet-sort.ts, ten sequential calls inmain.tsx. Collapse: auiSlicePref(key, validate, fallback)adapter factory + awireStorePref(store, selector, save)orchestration helper. Every future preference then costs ~8 lines instead of ~30 across four files.theme.ts(FOUC ordering) andpanes.ts(debounce) stay bespoke — they carry real variation. - Entity-adapter photocopies (~55–60 LoC).
snippet-store.ts/dataset-store.ts/theme-store.ts/font-store.tsare the identical 28-line load/save/delete triple differing only in store name, migrate fn, and version constant (re-diffed: confirmed). Collapse:makeEntityAdapter<T>(storeName, version, migrate)indb.ts+ four 3-line instantiations. Migration files stay — real per-entity logic. - Wirer residue files (~55 LoC).
dataset-persistence.ts/theme-persistence.ts/font-persistence.tsare 25-line modules whose body is one 6-linewireEntityWriteThroughcall. Move the calls intoentity-persistence.tsorstartup.ts(their only caller).snippet-persistence.tsstays — the debounced autosave is real logic. editor.addActionceremony (~55 LoC). Thirteen copies of the same 6-line registration object acrossspec-transform-actions.ts:524-585andspec-config-actions.ts:210-235→ one table +.map(addAction).- Scaffold-twin merge (~40–50 LoC; overlaps main-review S2 and shares its fix). The
completion prologue, suggestion mapping, and create-property block are verbatim between
spec-transform-scaffold.tsandspec-param-scaffold.ts. Collapse intoeditor-snippet.tshelpers; the pure indent math moves to the TODO'd core module, where it also becomes testable. - Core jsonc-helper copies (~25–30 LoC).
objectKeysbyte-identical betweenspec-params.ts:85-93andspec-data-transforms.ts:103-111(re-diffed: confirmed);paramsArrayNode/transformArrayNodeidentical modulo key string; two more shared shapes. Extract/share — both sides have strong tests. - localStorage adapter shell (~28 LoC).
readRaw/writeRaw/availableidentical betweensettings-store.tsandux-prefs.tsmodulo type name and log tag (re-diffed: confirmed) →jsonLocalRecord<T>(key, tag)in the same layer. - Batch of micro-cuts (~90 LoC):
UrlStateSync's argument-ignoringsyncModalToUrl/clearModalFromUrlwrappers (delete when fixing theModalCoordinator.ts:36ordering TODO they paper over);DatasetStore's thrice-repeated save epilogue;readTextFile/readBinaryFile1:1 renames ofFilemethods;initPersistentStoragepass-through;modals/types.tsfolded into the registry;SpecEditor's seven subscribe/dispose pairs → array; the landing's four-times repeatedshotblock + TODO'dUiThemeredeclaration;Icon.tsx's four-times copied panel frame; one dead CSS rule (ChartBuilderModal.module.css:213).
Needs design thought (~100–150 LoC more): the splitter triple (ResizeHandle /
PaneSplitHandle / InspectorSplitHandle share a near-verbatim role="separator" block —
worthwhile but untested a11y surface, do with manual keyboard verification); the theme-panel
Color+Size+Weight row groups (60–100 LoC vs greppability of accessible names); the modal
master-detail CSS block; the landing/learn theme-toggle hook (blocked on where a shared
React hook may live under the entry-isolation rule — needs a maintainer call).
Adjudicated and acquitted (challenged per the widened mandate, earn their lines): the modal registry/coordinator/shell trio (every registry field varies; flattest honest version saves only ~35 lines); the Zustand store shapes (a generic collection-slice helper would cost more in typing than the ~60 lines it saves and obscure real per-store invariants); core purity shims (minimal — the altitude problem runs the other way, app services holding pure logic); the non-photocopy infrastructure adapters (real normalization, migration, error mapping).
Wrap-up follow-ups (recorded from the params-feature review passes, same day)
The params WIP was closed through the full wrap-up protocol (alignment + eng-council, both
clean-context). Fixed in that pass: an isUnitNode bug (nested layer containers offered
schema-invalid selection params), the S2 extraction (core/spec-snippet.ts, insertion math
now table-tested applied-and-reparsed), spec §03 scaffolding coverage, the transforms
catalog upgraded to real-schema validation, ajv declared. Deferred with recorded homes:
src/core/lesson-parse.tsexports five unused types (knip) — verify and unexport. Same check forLensFactoryineditor-cursor-lens.ts:24(exported, consumed only in-module).spec-transform-actions.tshas an internal8-line clone (:269-276 vs ~:346-353) — fold into the existing 663-LOC consolidation item.- The Ajv compile boilerplate now lives in two test files; a third schema-validating
catalog test should extract a shared helper — and at that point promote the rule
"seeded editor catalogs validate against the bundled VL schema, not just JSON.parse"
to an
/alignmentcheck. - A small knip config (ignore
@fontsource/*,markedfalse positives) would make future dead-export sweeps one command.