Handbooks
Konzepte

Architektur

Elf Pakete in vier Schichten, eine strikt einbahnige Abhängigkeitsrichtung und die Grenzen, die die deterministische Hälfte eigenständig wiederverwendbar machen.

Die Schichtung

Paketschichtung: Einstiegspunkte, Fähigkeiten, Engines, Fundament
SchichtPaketeAufgabe
Einstiegspunktecli, studioWas ein Mensch oder ein Container ausführt
Fähigkeitenpipeline, renderer, skill, planner, patcher, resyncJe eine Aufgabe, unabhängig nutzbar
Enginesanalyzer, llmDie zwei Dinge, auf denen alles andere aufbaut
FundamentcoreDatenmodell, Konfigurations-Registry, Utilities

Abhängigkeiten zeigen immer nur nach unten:

cli → pipeline / renderer / skill / planner / patcher / resync → analyzer / llm → core

Drei Regeln, die es gesund halten

1. Einbahn-Abhängigkeiten, erzwungen

core importiert nichts Internes. Nichts importiert cli. Ein Zyklus oder ein Import nach oben lässt pnpm check:workspace fehlschlagen — das außerdem prüft, dass die TypeScript-Projektreferenzen jedes Pakets exakt seine package.json-Abhängigkeiten spiegeln. Eine fehlende Referenz lässt tsc -b in der falschen Reihenfolge bauen, und ein Root-Build versteckt das.

2. LLM-Isolation ist eine Paketgrenze, keine Konvention

Nur llm, pipeline, planner und resync dürfen mit einem Modell sprechen, und nur über die ChatClient-Schnittstelle:

interface ChatClient {
  readonly model: string;
  complete(prompt: string, options?: ChatOptions): Promise<ChatResult>;
}

analyzer, renderer, skill und patcher hängen überhaupt nicht von @handbooks/llm ab. Sie sind vollständig deterministisch und wiederverwendbar, ohne dass irgendwo ein LLM in Sicht wäre. Deshalb können render, skill, validate, apply und rollback bedenkenlos in CI laufen.

Und deshalb läuft die gesamte Testsuite offline: eine Naht, ein Mock.

3. Die Renderer-Grenze ist ein Typ

HandbookModel (definiert in core) ist das Einzige, was der Renderer kennt. Pipeline-Interna liest er nie.

interface HandbookModel {
  title: string;
  lang: NarrateLang;
  skeleton: Skeleton;
  cards: Record<string, FileCard>;
  assignment: Assignment;
  organization: Organization;
  narration: Narration;
  registers: RegisterEntry[];
  provenance?: { commit?: string; generatedAt: string };
}

Jeder Erzeuger, der ein HandbookModel füllen kann, bekommt Rendering, Skill-Paketierung und Planung geschenkt. Wer ein Handbuch auf anderem Weg generieren will: Das ist der gesamte Vertrag, der zu erfüllen ist.

Datenfluss

source tree
   │  analyzer — tree-sitter WASM, one adapter per language

phase1/graph.json · functions.csv · graph.dot · dropped-calls.json · scan-coverage.json
   │  pipeline 2a — cards (batched LLM, three-tier degradation, resumable)

phase2/cards/<rel>.json + _coverage.json
   │  pipeline 2b — skeleton synthesis (+ doctor loop) + file assignment

phase2/skeleton.yaml + assignment.json
   │  pipeline 2c — call-graph topological order + LLM grouping (flat fallback)

phase2/organization.yaml
   │  pipeline 3 — bottom-up narration + register extraction (content-hash cached)

phase3/narration.json + registers.json
   │  loadHandbookModel()

HandbookModel ──▶ renderer ──▶ handbook/  (md · html/ · handbook.html · agent/ · llms.txt)

                     └──▶ skill ──▶ SKILL.md + references/ (+ coverage.json)

Der Arbeitsverzeichnis-Vertrag: Jede Phase liest nur ihre vorgelagerten Artefakte und schreibt nur ihre eigenen — alle beim Lesen schema-validiert, mit einem version-Feld. Jede Phase kann allein erneut laufen. Abstürze sind fortsetzbar — Karten werden pro Batch geschrieben, der Erzähltext ist per Content-Hash gecacht.

Das Artefakt für Menschen erklärt; das Artefakt für Agenten verortet

Ein HandbookModel, zwei Ausgaben mit wirklich verschiedenen Aufgaben — und diese Teilung ist das Design, kein Detail der Verpackung.

Das Markdown- und das HTML-Handbuch sind zum Lesen geschrieben: Prosa, Reihenfolge, ein erzählerisches Rückgrat. agent/ ist zum Greppen geschrieben: symbols.tsv beantwortet „wo ist sendPayment definiert“ in einer Zeile, was keine Menge Prosa schafft.

Früher waren beide dieselbe Prosa in zwei Formen, und der Preis war konkret: Der Agenten-Index kam auf das 2,1-Fache der Größe des Index für Menschen und enthielt dabei keine einzige Symbolposition, weil 42 % davon Modellprosa waren, Byte für Byte von den Seiten für Menschen kopiert. Jetzt trägt die Agentenseite Fakten und eine gekürzte Zeile Prosa pro Datei; wo die Erklärung gebraucht wird, verlinkt jede Etappenseite die Seite für Menschen, statt sie zu duplizieren.

Im Inneren des Analyzers

Jede Sprache implementiert einen LanguageAdapter: discover, analyze und optional statementSpans. Jede Grammatik ist WebAssembly — die Installation kompiliert also nie nativen Code.

Adapter machen zwei Durchgänge pro Modul:

  1. Scan — Deklarationen, Imports, Klassen und Methoden sowie Fakten pro Funktion: Signatur, Zeilenbereich, Async-heit, Dekoratoren, self/this-Attributlese- und -schreibzugriffe, typisierte Parameter und aus Konstruktorzuweisungen gelernte Attributtypen.
  2. Resolve — jede Aufrufstelle wird zu einer typisierten Kante: self_method, self_attr_method, param_method, internal_func, internal_constructor, boundary, boundary_constructor — oder unresolved, was der Graph-Builder mit einer Kategorie in dropped-calls.json in Quarantäne stellt.

Der behaltene Graph enthält ausschließlich aufgelöste, benannte Aufrufziele. Genau das macht eine Kante darin vertrauenswürdig.

Dieselbe Regel gilt eine Ebene höher, für ganze Dateien. Eine Datei, die der Adapter nicht lesen konnte, bei der die Grammatik geworfen hat oder die mit Syntaxfehlern geparst wurde, wird mit ihrem Grund in scan-coverage.json festgehalten — und die ersten beiden Fälle bleiben aus scannedFiles heraus, damit keine spätere Phase eine Datei beschreiben kann, die der Parser nie gesehen hat.

Das Nav-Pack ist eine deterministische Orientierungszusammenfassung aus dem Graphen — Verzeichnis-Rollups, Einstiegspunkt-Kandidaten, Fan-out, externe Subsysteme. Es ist die einzige Sicht auf die Codebasis, die der Skelett-Synthesizer bekommt, was diesen Prompt klein und geerdet hält.

Die Qualitätsmaschinerie der Pipeline

Dreistufige Kartendegradation (2a). Ganzer Batch → einzelne Datei → Chunks pro Funktion für übergroße Dateien. Dateien, die dann noch scheitern, bekommen eine ehrliche leere Karte und stehen in _coverage.json. Die Abdeckung ist per Konstruktion vollständig; Lücken sind sichtbar statt still.

Actor-Critic-Skelettdoktor (2b). Der Actor schlägt höchstens drei Strukturänderungen gegen Ground-Truth-Statistiken vor; drei rollenspielende Kritiker (Ingenieur, Architekt, Leser) begutachten parallel; jede überlebende Änderung wird vor der Anwendung mechanisch re-validiert; betroffene Dateien werden neu zugeordnet. Die Schleife stoppt bei Konvergenz oder nach zwei Runden ohne Fortschritt. Ein kaputter Kritiker zählt als REJECT — ein ausfallender Gutachter darf Änderungen niemals durchwinken.

Deterministische Fallbacks überall (2c, 3). Die Organisation fällt auf die Aufrufgraph-Reihenfolge zurück. Der Erzähltext fällt auf die Etappenbeschreibung zurück. Eine gescheiterte Registerextraktion ergibt eine leere Liste. Ein Generierungslauf degradiert; er blockiert nicht.

Content-Hash-Caches (3). Etappen- und Systemprosa wird unter phase3/cache/ gecacht, geschlüsselt nach Prompt-Version, Sprache und dem vollen Prompt-Hash — Wiederholungsläufe und Resyncs zahlen so nur für das, was sich wirklich geändert hat.

Nebenläufigkeit und Sicherheit

  • Ein Lauf pro Arbeitsverzeichnis. generateHandbook und resyncHandbook nehmen dieselbe reentrante Verzeichnissperre, sodass ein CLI-Lauf und ein Studio-Job ihre Schreibzugriffe auf dieselben Artefakte nicht verschränken können.
  • Atomare Schreibvorgänge. Jedes Artefakt wird in eine Temp-Datei geschrieben und umbenannt. Ein Absturz hinterlässt nie eine halb geschriebene Datei, an der der nächste Lauf erstickt.
  • Kooperativer Abbruch. Ein AbortSignal wird zwischen den Phasen und an jedem Batch-Checkpoint geprüft und in jeden LLM-Aufruf durchgereicht, sodass laufende Anfragen abbrechen. Ein abgebrochener Lauf behält, was er gespeichert hat, und schreibt kein Run-Manifest.

Entscheidungen, die man kennen sollte

#EntscheidungWarum
1Nur-WASM-tree-sitterNull native Builds; ein Ladepfad für jede Sprache; versionsfixierte Grammatiken
2Handgerollter fetch-LLM-ClientOpenAI-kompatible Endpunkte variieren; ein dünner Client mit explizitem Retry schlägt eine SDK-Abhängigkeit. Die Schnittstellennaht zählt mehr als der Transport
3Eine Pipeline, zwei StrategienGetrennte Groß-/Klein-Pipelines duplizieren Adapter, Kritiker, Clients und Renderer; ein Strategie-Flag entfernt rund 40 % dieser Fläche
4zod-validierte Artefakte mit versionKorrupte oder handeditierte Artefakte scheitern laut an der Grenze, statt spätere Phasen zu vergiften
5Fakten/Prosa-Trennung in den KartenDas Modell annotiert ein vollständiges, aus dem Graphen abgeleitetes Inventar. Prosa darf leer sein; Fakten dürfen nicht falsch sein
6Single-Turn-Planner-ProtokollFunktioniert auf jedem Endpunkt, trivial mockbar, und das Transkript ist inspizierbar. Der Preis — Tokens erneut senden — ist im Planner-Maßstab akzeptabel
7ESM + tsc -b, kein BundlerBibliotheken liefern typgeprüftes dist/ und .d.ts; Composite-Referenzen geben inkrementelle Builds ohne zusätzliches Tooling
8Eine Konfigurations-RegistryFlags, Env-Namen, YAML-Schlüssel und drei generierte Dokumente stammen alle aus einer Tabelle und können daher nicht auseinanderdriften

Weiter

Auf dieser Seite