docs: architecture hub, eight ADRs, fractal READMEs, and a generated API reference that cannot drift - #56
Merged
Conversation
The hand-written route table in CLAUDE.md listed 14 paths against the 59 the server actually serves, which is the failure mode any hand-maintained table reaches eventually. docs/api-routes.md is now a projection of the route registry, regenerated by pnpm docs:routes. The generator boots buildServer, never startServer, so it cannot run migration, crash recovery or job resume against a real library, and it points TUTOR_DATA_DIR at a temp directory it asserts is still empty afterwards. Route attribution is checked back against app.hasRoute and fails loudly rather than publishing a plausible wrong path. Claude-Session: https://claude.ai/code/session_01FSkESkJTRpt3Ye2cM7c5n5
…rift gate ARCHITECTURE.md is the entry point for a reader who has never seen the repo. Five diagrams carry it, one per question a cold reader asks, and the prose between them only says what a diagram cannot. Every folder README links back to it. CONTEXT.md records the ubiquitous language and, more usefully, the words this domain overloads. Status is three unrelated enums, review is two unrelated features, and job and task stopped being synonyms when the job journal landed. CI and lefthook now regenerate docs/api-routes.md and fail on a diff, so the generated doc cannot drift from the routes it describes. The lefthook copy is scoped to server changes and runs in under a second. Claude-Session: https://claude.ai/code/session_01FSkESkJTRpt3Ye2cM7c5n5
Each ADR was written from code that exists, with a citable symbol or file, and any draft claim that could not be found in the tree was dropped rather than smoothed over. Two drafts were wrong and are corrected here. Chapter generation is no longer lost on restart, so 0002 describes the journal and resume path instead of the in-memory one, and prompt text lives in typed template literals inside each service rather than in runtime markdown files, so 0005 says that instead. Also adds the two Agent Skills the repo needs to be worked on by an agent, verify for the green-before-commit check and add-feature for the test-first port to adapter to service to route to client recipe, plus .mcp.json for the built-in MCP server. Claude-Session: https://claude.ai/code/session_01FSkESkJTRpt3Ye2cM7c5n5
Ten folder READMEs, each opening with an Up breadcrumb to the hub and closing with Related links, so a reader who lands anywhere in the tree can walk up or sideways without grepping. Three of them state a rule the outline had wrong. Services are not free of node:fs, five of them read a file behind a path-returning ArtifactStore and the real rule is that fs must not appear in a route. Features do import each other, so the README describes the pattern that exists rather than one that does not. The port list is fifteen, not thirteen. CLAUDE.md drops the per-file tree and the fourteen-row API table it had grown wrong, and points at ARCHITECTURE.md, CONTEXT.md, docs/adr/ and the generated docs/api-routes.md instead. Every Electron constraint is byte-identical; the only two edits inside that section are stale file references, epub-gen-memory moved to server/adapters/epub-gen-export.ts and the renderer entry is client/app/main.tsx, which is what index.html already says. Claude-Session: https://claude.ai/code/session_01FSkESkJTRpt3Ye2cM7c5n5
Twelve of the seventeen do. The five that do not are the ones whose real subject would spend money against a provider, download a model, or reach a remote service on every run. Claiming universal coverage in a doc about why the contract exists would have been the exact failure this phase is meant to prevent. Claude-Session: https://claude.ai/code/session_01FSkESkJTRpt3Ye2cM7c5n5
Eighty new JSDoc blocks across ports, adapters, services, and domain. Every port interface file now names its adapters, its in-memory fake, and its contract test in prose, so a reader landing in server/ports can find all four files without grepping. Every adapter names the port it implements and the one external constraint that comes with its technology. Two existing blocks were lying and are corrected. The TextGeneration header described the pre-port world in present tense and put the call-site counts at 9 and 6 against a real 8 and 6, and its streamText doc said 5 where explain-passage makes two of the six. The ImageGeneration header pointed at generateImageWithFallback in a services module Phase 2 deleted, and called the adapter that has existed since then future work. Every changed line in every file is blank or a comment, verified by stripping the diff. Claude-Session: https://claude.ai/code/session_01FSkESkJTRpt3Ye2cM7c5n5
Forty-four blocks, concentrated on the streaming surface. Every function in client/api that opens a stream now answers the three questions a caller cannot answer from the signature. Who aborts it, what happens on unmount or when the caller returns, and whether partial content survives a mid-stream error. Several of them turn out not to take an AbortSignal at all, which is worth saying out loud. The eleven blocks in shared/contracts.ts and shared/domain.ts are chosen, not swept. A block on BookMetaSchema saying it is the schema for book metadata would be worse than none, so only the schemas carrying a real constraint got one, including two asymmetries a reader would otherwise trip over. ReviseTocBody and StartBookBody take raw model and provider strings where every sibling validates them, and RatingBody caps the final quiz score at 100 where the persisted field does not. Claude-Session: https://claude.ai/code/session_01FSkESkJTRpt3Ye2cM7c5n5
The question the owner asked, answered where a reader of the routes doc hits it. The note lives in the generator template rather than in the markdown, so it regenerates with the file it explains and cannot be edited out of a generated document, and its route count interpolates so the number it cites cannot drift either. Claude-Session: https://claude.ai/code/session_01FSkESkJTRpt3Ye2cM7c5n5
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.
Objective
Make the repo readable, and make the parts of it that describe themselves impossible to get wrong. Docs and tooling only. The one production change is the routes-doc generator plus its
package.jsonline, and everything else touching.tsfiles is a comment.Adds the architecture hub, the domain glossary, eight ADRs, ten folder READMEs, a generated API reference with a CI gate against drift, two Agent Skills,
.mcp.json, and 126 JSDoc blocks.What landed
ARCHITECTURE.mdCONTEXT.mddocs/adr/0001-0008docs/api-routes.mdpnpm docs:routes. 70 routes over 59 paths across 17 modules.server/,server/ports/,server/adapters/,server/services/,client/,client/features/,electron/are new.shared/,e2e/,server/migrations/already existed and took breadcrumbs only. Each opens withUp:and closes withRelated:.CLAUDE.md.claude/skills/{verify,add-feature}The drift gate
The old API table in
CLAUDE.mdlisted 14 paths against the 59 the server actually serves. That is where a hand-maintained table ends up.docs/api-routes.mdis now a projection of the route registry, and CI fails if the committed file differs from a fresh run.Demonstrated rather than asserted. A throwaway route was added to
server/routes/library.ts, then:Reverting the route and regenerating passes again. The same check runs in
lefthook.ymlpre-commit, scoped toserver/**, because it takes 0.71s.docs/api-routes.mdalso carries a short "Why a generated table and not OpenAPI" note, answering the question before a reader has to ask it. The note lives in the generator template rather than in the markdown, so it regenerates with the file it explains and cannot be edited out of a generated document, and its route count interpolates so the number it cites cannot drift either. The short version is that a spec would need converted JSON Schema on all 70 routes, validation is deliberately Zod inserver/http/parse.tsrather than Fastify's ajvschema.bodybecause ajv changes the 400 body shape the client already handles and drops the Zod defaults the domain relies on, and the only client ships in this repo and imports the real schemas out ofshared/. Revisit when a second client or an outside consumer appears.The generator boots
buildServer()and neverstartServer(), so it cannot run migration, crash recovery, or job resume against a real library. As a second, independent guard it pointsTUTOR_DATA_DIRat a temp directory and asserts that directory is still empty afterwards, so an adapter that starts writing eagerly fails the generator instead of quietly touching a reader's books.Gate evidence
pnpm testpnpm typecheckpnpm lint--max-warnings 0pnpm e2epnpm docs:routespnpm dev:serverthencurl 127.0.0.1:3147/api/healthreturns{"status":"ok"}pnpm mcp:devlists 18 tools,list_booksreturns the real library.tsfile is blank or a comment, verified by stripping the diff. Zero executable lines changedFacts the plan had wrong, corrected against the tree
The whole point of this phase is docs that cannot lie, so every count and path was re-checked rather than copied from the plan.
JobJournalandLibraryMigrator.string | undefined. Phase 3 was expected to tighten them toBookStatusand did not. Follow-up ledger item 3 stays open.jobin code" is dead. 187 occurrences, all Phase 7 vocabulary.CONTEXT.mddefines the word instead of banning it.server/prompts/holds one file. Prompt text is inline template literals in each service.node:fsdirectly.ArtifactStorereturns paths not bytes for large binaries. The real rule is thatnode:fsmust not appear in a route.readerreaches intoaudiobook,chat,markdown. The README documents the pattern that exists.server/ports/were themselves lying.TextGeneration's header described the pre-port world in present tense with call-site counts of 9 and 6 against a real 8 and 6.ImageGeneration's header pointed at a function in a services module Phase 2 deleted, and called the adapter that has existed since then future work. Both fixed.Note on the CLAUDE.md Electron section
Every Electron constraint sentence is byte-identical, proved by diffing the section against the previous commit. Exactly two lines changed, both stale file references sitting inside constraint sentences.
books.tsbecameserver/adapters/epub-gen-export.ts, which is where the double-default import actually lives now, and/src/main.tsxbecame/client/app/main.tsx, which is whatindex.html:27already says. A constraint that points at a deleted file cannot be followed.Found and not fixed
Reported rather than touched, since this phase is docs only.
ReviseTocBodySchemaandStartBookBodySchematake raw model and provider strings where every sibling AI body schema validates throughModelSchemaandProviderSchema.RatingBodySchemacapsfinalQuizScoreandfinalQuizTotalat 100 where the persistedBookMetaSchemafields do not, so a value read off disk cannot be assumed to satisfy that cap.list-provider-models.tshas aswitchwhosedefaultis dead today but has noneverexhaustiveness guard, so a fourth provider would compile and silently 400.audiobook-installer.ts'sinstallAllthreads itsAbortSignalinto the ffmpeg download but not intoKokoroTTS.from_pretrained, so aborting only silences model-download progress.fs-book-repository.ts'ssaveReferencespreads a matched entry and reassigns the samenameit matched on.unhandledRejectionhazard, is documented onkokoro-speech-synthesis.tsand inserver/adapters/README.md.A scripted doc-path checker was written and used for the gate above but not committed, since this phase's production budget was the routes generator alone. It is a good Phase 5 candidate.
https://claude.ai/code/session_01FSkESkJTRpt3Ye2cM7c5n5