Specification
Loom Architectural Fitness Functions
Status: Normative. These are the kernel’s design invariants, made executable — every PR proves them, so the architecture is enforced continuously rather than by reviewer memory. They complement the Kernel Specification (the what) by pinning that it stays true. Gated in CI and by
pnpm fitness.
How they run
pnpm fitness(root) =build → typecheck → lint → test. The ABI/schema pins, SPI conformance kits, replay/golden checks, and the G2 ratio gate all run insidepnpm testas ordinary tests, so there is one command and one CI gate.- CI (
.github/workflows/ci.yml) runsbuild,typecheck,lint,teston every PR. - Absolute benchmark numbers (
pnpm bench, benchmarks.md) stay advisory — they are machine-relative, so they are not a hard gate; the machine-stable checks below are.
The invariants
| # | Fitness function | What it proves | Where it is enforced |
|---|---|---|---|
| 1 | Replay unchanged | Folding a stored log reconstructs identical state; a code change can’t silently re-fold historical logs differently. | @loom/eval (replayAsFixture/diffRuns), @loom/deploy-sim (simulateDeploy), golden fixtures (captureGolden), wire fold-vectors (packages/wire-abi/src/wire-abi.test.ts). |
| 2 | ABI unchanged | The K1 syscall surface can’t drift silently (add = MINOR, remove = MAJOR). | packages/workflow-runtime/src/abi.test.ts (pins KERNEL_SYSCALLS + version). |
| 3 | Event schema compatible | The K5 event-type set can’t change without bumping the version + pin; stored logs stay foldable. | packages/replay-vm/src/eventSchema.test.ts (pins knownEventTypes() + EVENT_SCHEMA_VERSION); @loom/verify replay-safety rule for cross-version resume. |
| 4 | Zero-overhead when disabled (G2) | An unwired optional capability adds no work. | Behavioural: disabled-path tests (e.g. verify mode=off never builds input, verify.test.ts; scheduler Infinity fast-path). Relative-ratio canary: packages/verify/src/g2ratio.perf.test.ts (off ≫ warn) — in the PERF suite (pnpm test:perf, run locally and in the manual flake hunt), because a timing ratio never gates a merge (see the timing row below); the behavioural tests are the blocking guard. |
| 5 | Benchmarks passed | Perf is measured, not assumed; key hot paths don’t regress. | Timing ratios and budgets live in the PERF suite (*.perf.test.ts, pnpm test:perf): run locally and by the manual flake hunt (.github/workflows/flake-hunt.yml), never gating a merge. The blocking suite keeps only deterministic evidence or far-from-noise ceilings (e.g. foldScale.test.ts’s quadratic guard). Absolute baselines in benchmarks.md are advisory (pnpm bench). |
| 6 | Conformance passed | A storage/dispatch SPI implementation honours its contract. | run<Spi>Conformance kits (@loom/conformance). Each implementation runs the kit against itself — in-memory (packages/conformance/src/index.test.ts, which also pins the per-SPI version constants), SQLite (plugins/sqlite/src/conformance.test.ts), and the durable-state/read-model/content/crypto/worker-runtime-local packages in kind. loom plugin check enforces that a 3P plugin wires a kit too. |
| 7 | Compiler validation passed | Flows are structurally valid, non-determinism is caught pre-dispatch, and what a flow reaches is checked past its declared steps. | @loom/compiler (analyzeNonDeterminism, analyzeLoopIdentity, analyzeRunnerReach — all three emitted into loom-manifest.json and auto-injected), @loom/verify (loom verify / loom doctor), validateFlowGraph (@loom/app), loom check. |
| + | Maturity is self-checking | A capability’s badge can’t claim evidence it lacks. | @loom/spec spec.test.ts (asserts each capability’s version constant / conformance test / bench actually exists). |
| + | Maturity is complete | Shipped work can’t skip declaring a badge — the converse of the row above. Every accepted ADR is a declared capability or is explicitly listed as grading nothing. | @loom/spec spec.test.ts (capability coverage). Added after ADRs 0074–0077 landed in one day with no entry, making four shipped capabilities invisible in the generated docs. |
| + | A seam cannot ship undocumented | Adding an SPI/plugin seam can’t leave the docs that enumerate seams behind. The mapping is derived from the seam registry itself, and the assertion matches a TABLE ROW rather than a mention anywhere in the file. | tools/scaffold/src/seamDocs.test.ts (co-located with SEAM_KEYS). Added after the policy seam shipped with 5 code sites updated and 5 doc sites not — and immediately found an older instance, llm, that a manual audit of the same file had walked past. |
| + | A rule cannot ship undocumented | Adding a @loom/verify rule can’t leave the two documents that enumerate rules behind. Derived from RULE_ORDER; matches a table row / list bullet, and cross-checks the ADR’s stated rule count. | packages/verify/src/ruleDocs.test.ts (co-located with RULE_ORDER). Added on the fourth instance of the class: the README’s table was missing two rules while ADR 0028’s amendment still called the eighth rule “a seventh”. |
| + | A ctx.* call cannot ship without its durability | Adding to ExecutionContext can’t leave the canonical usage table behind, and every row must say whether the call puts a fact in the log. Both directions: every member has a row, every row names a real member. | packages/workflow-runtime/src/contextDocs.test.ts (co-located with ExecutionContext). Building it found the larger drift it was written to prevent — ctx.execute, ctx.decide and ctx.negotiateBudget appeared nowhere in usage.md, so the canonical table documented the Loom 1.x surface. |
| + | The root project graph is complete | tsconfig.json’s references ARE the graph tsc --build walks, so a package missing from it is not slower — it is silently unchecked. | packages/spec/src/projectGraph.test.ts. Added after packages/isolate and plugins/crypto were found producing no output from a root build at all, with three more (two of them kernel packages) built only transitively. |
| + | Reference docs don’t cite line numbers | docs/loom-2.0/* describes the current system; source line citations rot on the next edit, silently and authoritatively. Cite the file, let the prose name the symbol. | packages/spec/src/docCitations.test.ts. Added after an audit found 40 citations landing on a blank line, a closing brace or a comment terminator — five of them made wrong by a single afternoon’s refactor. ADRs and requirements docs are excluded: they are dated records, and “fixing” their line numbers would falsify them. |
| + | The minimal kernel stays minimal | No kernel or SPI package may reach a plugins/* adapter, transitively or otherwise — the invariant that keeps adapters swappable. | packages/spec/src/layering.test.ts (ADR 0043), which walks the real workspace dependency graph rather than a declared list. |
| + | Source carries no raw control bytes | A literal control character in source is invisible in review and changes behaviour — a NUL written raw into a composite key separator is the case that prompted it. | packages/spec/src/sourceHygiene.test.ts, sweeping packages/plugins/tools/docs. |
| + | Every workspace package has a section in the API reference | docs/api-reference.org opens by promising each package a section, which is a claim about the whole workspace and wrong the moment a package ships without one. Enumerates the workspace and asserts a HEADING, not a mention. | packages/spec/src/apiReferenceDocs.test.ts. Added after an audit found 16 of 61 packages with no section at all — including @loom/knowledge, @loom/verify and @loom/plugin-read-model, so a reader could not tell “undocumented” from “does not exist”. |
| + | Documented config examples use options that exist | Doc snippets are never compiled, so a defineConfig example can name a flat key that throws or a storage kind that silently loses data. Parses every such call in a current doc’s code block and checks its option names against the source. | packages/spec/src/docConfigExamples.test.ts. Added after an audit found a sqlite storage kind, which does not exist, taught in four docs. |
| + | A doc that claims the whole ADR corpus has it | “The full decision history (0001–NNNN)” is a completeness claim, so it is wrong the moment a record lands after it — and wrong in the way hardest to notice, since the sentence still reads as authoritative. Both halves are asserted: the range ends at the newest ADR, and the record-by-record walk reaches it. | packages/spec/src/adrIndexDocs.test.ts. Added on the third surface of one class — the CHANGELOG and the capability registry each already had a guard, and this list, maintained by remembering, had drifted four records behind. |
| + | A blocking test never gates on wall-clock noise | CI runs every suite concurrently on a shared runner, so a timing RATIO or a tight ceiling on a millisecond-scale operation measures the runner’s load as much as the code. Outside *.perf.test.ts: no performance.now(), no ratio of timings, and any elapsed Date.now() upper bound carries a // timing-bound: justification and is ≥ 5 s. Prefer deterministic evidence (call counts, rows scanned, round trips) or no bound at all when the slow path is a hang. | packages/spec/src/testTiming.test.ts, sweeping packages/plugins/tools. Added after main went red four times in two days on foldScale O(n) ratios (10.7×–26.9× against 10, nothing regressed); its first run found two more tight bounds an audit had missed. Paired with .github/workflows/flake-hunt.yml (manual only — it costs N× CI minutes), which runs the full suite N times to surface a suspected flake. |
| + | A fitness function cannot ship undocumented | This table is normative: a list of enforced invariants that omits enforced invariants reads as complete while being wrong. Registry → file exists → named here. | packages/spec/src/fitnessDocs.test.ts. Added after two guards landed in one session with neither reaching this table — and on its first run it found two MORE that had been missing for longer (layering, source hygiene). |
Adding a capability
An additive capability (a K/T ring, an SPI, a runtime) MUST, before merge:
- Preserve replay (#1) and, if it touches the schema/ABI, bump the version and the pin (#2/#3).
- Be opt-in with a disabled-path test and — for a hot path — a ratio canary (#4/#5).
- If it’s an SPI, ship a conformance kit and a version constant (#6), and register a capability in
@loom/specat the level its evidence supports (the maturity check,+). - Surface through an existing idiom (one
defineConfigkey /ctx.*method /flow$/tool$option) — the DX bar.
Source: docs/spec/fitness-functions.md in the loom repository.