Why do all seven fusion subsystems look the same? Because each obeys one discipline: depend on injected ports, return Result at every boundary, validate untrusted input with Zod, and never throw. The discipline isn't decoration — it's what makes the engine testable, deterministic, and store-agnostic (ADR-0009).
Everything rests on a tiny type from @alembic/contracts. A Result is a value that is either a success or a failure — never an exception:
// packages/contracts/src/result.ts:10-26 export interface Ok<T> { readonly ok: true; readonly value: T; } export interface Err<E> { readonly ok: false; readonly error: E; } export type Result<T, E = Error> = Ok<T> | Err<E>; export const ok = <T>(value: T): Ok<T> => ({ ok: true, value }); export const err = <E>(error: E): Err<E> => ({ ok: false, error });
The ok boolean is a discriminated-union tag: after if (!r.ok) return r; the compiler knows the rest is r.value. There is no third state and no hidden control flow — a failure is a value you must handle, not an exception that unwinds the stack.
The only place a try/catch belongs is at the very edge, wrapping a throwing call back into the contract:
// packages/contracts/src/result.ts:57-69 — "Never rejects." /** Wrap an async throwing function as a Result. Never rejects. */ export const tryCatchAsync = async <T>( fn: () => Promise<T>, onError: (cause: unknown) => Error = toError, ): Promise<Result<T, Error>> => { try { return ok(await fn()); } catch (cause) { return err(onError(cause)); } };
Result<T, Error> … avoid throwing in library code." If library code can't throw, a caller can never be surprised by an exception — every failure path is in the type. The fusion subsystems honor this without exception.A port is an injected interface — the subsystem declares what it needs and the caller supplies it. No subsystem constructs its own filesystem, clock, model, or network client. Four seams recur across the fusion:
| Port | What it abstracts | Prod vs test |
|---|---|---|
FsPort | filesystem IO (read/write/atomic-write) | real node:fs impl · in-memory fake |
Clock | the current time | system clock · a fixed/advanceable clock |
| backend / adapter | a model call or network provider | fetch/ModelAdapter · a canned fake |
idFactory | minting identifiers | monotonic counter (deterministic everywhere) |
The constructor takes its filesystem, its file path, and its clock. It builds none of them:
// packages/hermes/src/curator/usage-store.ts:58-63 export class UsageStore { constructor( private readonly fs: FsPort, // injected IO — no node:fs private readonly sidecarPath: string, // path is an argument, no global home private readonly clock: Clock, // injected time — no Date.now() ) {}
Atomic writes go through FsPort.writeFileAtomic so a crash never leaves a half-written sidecar; reads are best-effort (a corrupt file is treated as empty, returning ok, so a hot-path telemetry call can't break the host). The asymmetry is deliberate: corrupt read → ok(empty), failed write → err.
The web kernel imports no SDK and no concrete backend. It declares two injected seams — the provider and an optional compressor — both returning Result:
// packages/hermes/src/web/types.ts:120-140 export interface WebBackend { search(query: WebSearchQuery): Promise<Result<readonly WebSearchResult[], Error>>; extract(url: string): Promise<Result<WebExtractResult, Error>>; } // Optional LLM-compression seam — wraps ONE ModelAdapter call in prod; // absent ⇒ raw content is returned unchanged. export type Compressor = ( text: string, instruction: string, ) => Promise<Result<string, Error>>;
A network failure is err; an empty search is ok([]) — the difference is preserved in the type. This is exactly the seam the learning loop uses for its ReviewProposer: "a model call in prod, a fake in tests."
The blocking human-in-the-loop primitive needs ids. It takes an injected id factory, defaulting to a monotonic counter — never Math.random() or Date.now(), which the engine's plan VM rejects and which would break replay:
// packages/hermes/src/clarify/gateway.ts:72-74 + 176-182 constructor(options: ClarifyGatewayOptions = {}) { this.mintId = options.idFactory ?? monotonicIdFactory(); // injected, deterministic default } export const monotonicIdFactory = (prefix = 'clarify'): (() => ClarifyId) => { let n = 0; return () => { n += 1; return `${prefix}-${n}`; }; };
Node has no blocking thread, so the gateway is a promise + resolver registry + timeout: ask() registers a pending entry under a minted id, arms a setTimeout, and returns the awaited promise. On timeout the entry is dropped and the promise resolves to err — it never hangs and never throws.
Clock and idFactory are the same pattern. Both replace a forbidden non-deterministic global — Date.now() and Math.random() — with an injected seam, for the same two reasons: testability and replay (the plan VM rejects both globals). The source makes the link explicit: the Clock port's own doc-comment (curator/types.ts:147-154) cross-references monotonicIdFactory. So the determinism half of this discipline is one idea with two instances — inject the thing that would otherwise make a run unrepeatable.Anything from outside the program — a model's proposal, a platform's clarify response, a backend's JSON — is untrusted, so it's validated with Zod at the boundary and a failure becomes err, not a thrown exception:
// packages/hermes/src/clarify/gateway.ts:86-89 const parsed = clarifyQuestionSchema.safeParse(question); if (!parsed.success) { return err(new Error(`Invalid clarify question: ${parsed.error.message}`)); } // …and in learning/review.ts:83-85, proposer output (untrusted model output) is // reviewProposalSchema.safeParse'd before any write. Same shape, every boundary.
FsPort + a fixed Clock + a canned backend and the kernel runs with zero IO, zero network, zero flakiness — and zero Date.now() nondeterminism.Date.now()/Math.random()), so runs are replayable.Result and never throws. The only try/catch is inside tryCatchAsync at the very edge, which converts a throw into err. The caller handles the failure as a value the type forces them to consider.UsageStore take a Clock in its constructor instead of calling Date.now()?reviewProposalSchema.safeParse) before any write. A malformed proposal becomes err — never a thrown exception, never an unchecked cast.Result is just exceptions with extra steps." The difference is the type system. An exception is invisible in a function's signature; a Result<T,Error> is right there, and the compiler won't let you read .value until you've handled !r.ok. Failure becomes impossible to forget.