Course / Lesson 16  ·  PT-BR
Lesson 16 · Engine & method · 3 of 8

The four invariants that hold the engine together

Alembic's architecture rests on exactly four properties — not six, not "best practices," but four named invariants each asserted in source and most governed by an ADR. They are the rules every package keeps, the reason the system is testable, replayable, and trustworthy. Knowing them is knowing the engine's spine. Source: docs/alembic-complete-map.md §2, verified against the cited files.

A note on "six." An older version of this course said "six invariants." That was stale. The as-built map lists exactly four — the count below is authoritative. Conflating extra "principles" with the load-bearing invariants is precisely the kind of drift the reverse-engineering method (lesson 20) exists to catch.

The four, at a glance

① run never throws uniform discriminated union · ADR-0009 ② adapter- & store-agnostic pure kernel, injected side- effects ③ content- addressed IDs deterministic run-dir ⇒ replay ④ dissent preserved by the Verifier, not a prompt ADR-0003 four pillars — each asserted in source, most governed by an ADR

① run() never throws; the result is a uniform discriminated union

You met this in lesson 14. It is enforced structurally by runWithGuards (adapter-core.ts:118) and verified live — the handoff records a real 429 surfacing as a typed failure. The orchestration core re-establishes the same boundary one layer up with runDebateSafe/runSwarmSafe (harness/src/core.ts:313-334), so a throw inside a council or swarm step also becomes a value. Governed by ADR-0009.

② Engines are adapter-agnostic AND store-agnostic

The pure kernels take only readonly views and injected side-effects. The DebateEngine, scoring, and verifier take an injected AdapterRegistry (council/src/debate.ts:71-83). The ETL layer routes all IO through an injectable FsPort — "every function here is testable in-memory." And the funnel takes an injected adapter registry, so swapping in an offline registry makes the whole run $0 and hermetic (funnel.ts:79-81).

// the pattern, everywhere: dependencies arrive as arguments, never imports of concretes
runDebate({ board, pack, adapters, requestId });   // adapters injected → test with fakes
runT0Pipeline(corpusDir, { fs });                   // FsPort injected → run in-memory
runFunnel(corpusDir, { adapters: offlineRegistry }); // offline registry → $0, hermetic

This is lesson 5 (ports & injection) elevated to an architectural law: no engine reaches out to a concrete adapter, filesystem, or store. That is exactly what makes the 400-plus-test suite run fast, in-memory, with no network — and what let the @alembic/hermes subsystems (lessons 7–13) be tested without opening a single socket.

③ Content-addressed IDs + deterministic run-dir layout

A run's id is the SHA-256-style content hash of its spec: runIdFor(spec) (swarm/src/orchestrator.ts:168). Change any spec field and you get a new run directory — runs are replayable because identical inputs converge on the same id and the same place on disk:

// run dirs: <baseDir>/runs/<runId> · append-only events.jsonl + checkpoint.json
// stores are content-addressed by SHA-256 over canonical (key-sorted) JSON →
// re-appending identical content is a no-op, so re-runs converge instead of duplicating

This is why plan modules (alembic.plan.ts) must be deterministic — Date.now(), new Date(), and Math.random() are rejected by the VM. Non-determinism would change the spec hash on every run and break replay, so the engine refuses to load a plan that contains it (lesson 17 covers the gate that enforces this on plans).

④ Dissent is preserved/forced by the Verifier, not merely by a prompt

This is the subtlest, and the one most teams get wrong. Many "councils" add a prompt that says "play devil's advocate" and call it adversarial. Alembic does not trust a prompt to produce dissent — it makes dissent structural:

A bug that was fixed, recorded honestly. The handoff once listed "contrarian-last is fiction (prompt says, code runs parallel)" among bugs not to carry forward. In the current source it is no longer fiction: it is enforced at board-load and realized by serial phase execution. The map notes the divergence and says "the bug was fixed" — provenance over polish. Governed by ADR-0003: there is no privileged "contrarian" Role; adversarial pressure is the Verifier's job.
1. How many architectural invariants does the as-built map name?
Correct: c. The map's §2 lists four. An older course said "six" — that was stale. The four are the load-bearing properties; everything else is a consequence or a convention.
2. Why must a plan module avoid Date.now() and Math.random()?
Correct: b. The run id is a hash of the spec; non-determinism would yield a different hash (and different run dir) every time, defeating replay and cache. Determinism is a precondition of invariant ③, so the engine refuses non-deterministic plans.
3. What makes Alembic's "dissent" invariant stronger than a "be a contrarian" prompt?
Correct: d. A prompt can be ignored or rationalized away. Alembic enforces dissent in the architecture: an independent, mutation-free Verifier with deterministic oracles, plus contrarian ordering checked at load. ADR-0003 makes adversarial pressure a system property, not a role.

Common confusions

"Adapter-agnostic just means an interface." It means more: engines never import a concrete adapter, filesystem, or store — those arrive as injected arguments. The test of the invariant is that you can run any engine fully in-memory with fakes, which the suite does throughout.
"Content-addressing is for dedupe." Dedupe is a side benefit. The deeper purpose is replay and convergence: identical inputs produce the same run id and the same on-disk location, so a re-run resumes or re-derives the same result instead of forking a new one.