Compilation is reconciliation
The documents are desired state, the graph is observed state
A build compares the two and schedules work until they agree. Everything that must never be wrong is deterministic code; the model is invoked only inside bounded turns, behind validation gates. The first build is not a special case: it reconciles against an empty graph. A rebuild with nothing changed makes zero LLM calls.
parse all docs → diff section trees → dirty set
→ ingest wave (reconcile-doc turns, root first, then levels in parallel)
→ review wave (review-entity turns)
→ checks (deterministic lint, coverage, reachability)
→ fixed point reached, or budget exhausted with work parked
The steps
-
Parse deterministic
Each document becomes a section tree with verbatim text and per-section content hashes.
-
Diff deterministic
The section diff is the sole source of staleness. Changed and removed sections are dirty; a section that merely moved has its anchors rewritten mechanically and stays clean. The model never decides what is stale.
-
Ingest turns LLM
One turn per dirty document. The model reads the dirty sections, searches the graph before creating any entity, records requirements with verbatim quotes, and marks each section covered or non-normative. Mutations are staged, validated one by one, and committed atomically when the turn finishes.
-
Review turns LLM
One turn per entity whose facts changed. The model judges whether the requirements form a coherent whole, merges lookalike duplicates, refreshes the definition, and records contradictions and ambiguities as diagnostics.
-
Checks deterministic
Whole-graph lint: sections left unprocessed, entities no requirement references, unreachable entities, quotes that no longer locate, and coverage claims that dodge normative text.
-
Fixed point deterministic
The build converges when a full wave proposes zero mutations and the checks pass. Work that exhausts its budget parks with a diagnostic; the next build resumes it first. Nothing fails silently.
Gates, not hope
Every mutation the model stages is validated by the graph store. A rejection names the rule and how to repair the call, because the caller is a model that will read it and try again:
✗ unknown-id: unknown entity id `ent:Product`; nearest existing:
ent:product; use one of those, or create it with upsert_entity first
✗ junk-name: `--api-key` looks like a CLI flag; entities are domain
concepts. If it truly is one, repeat the call with a note explaining why
✗ quote-not-found: quote not found in docs/admin.md#/admin-cli/rules;
copy the sentence verbatim from the section
Identity never depends on the model. Ids are minted by the store at creation and never change; creates key on the entity's name, so a retried or parallel create lands on the existing node instead of duplicating it.