Versioned policies, fingerprints, append-only evidence
Decisions you can account for later
People ask this as: make an agent-built system reproducible and auditable
Determinism here means a decision can be reproduced and accounted for later. It does not mean the decision was correct, and nothing in the framework checks that a policy encodes the rule you meant. A policy version is a fingerprint of its own source and declared config, and editing a registered version stops the application at boot rather than quietly changing what an old score meant. Evidence is append-only: immutable quote versions, append-only migrations, one contribution row per scoring rule. The event buffer is in-process and not durable.
Where this stops
Read this before the rest of the page. Every line below is a thing this does not do.
- Determinism is about reproducibility, not correctness. Nothing verifies that a policy encodes the rule you meant, and a wrong rule is reproduced exactly.
- The event buffer is in-process. A crash between the database commit and the flush loses the events, and no durable outbox exists — so these events cannot back a remote integration.
- A fingerprint covers a definition's own source and its declared config. Closure state and out-of-file helpers are not captured, and closure analysis is deliberately not attempted.
- Reproducing a decision is not reproducing its inputs. A score run fingerprints the mutable record fields it read rather than copying them, which is why reproducing a historical routing decision exactly stays partially supported.
- A code-version fingerprint is not remote-provider reproducibility. Every provider underneath is an offline fixture, and no fixture can promise what a real external service would return.
- Module evolution does no data transformation, backfill, field split or merge, and no table rename. Rolling generated source back after a migration has applied is not supported.
- There is no scheduler, so no policy is ever evaluated because time passed.
No authentication, tenancy or RBAC. The server is local-development-only. An actor header is an assertion, not an identity. Do not expose it to a network. Every claim and every limitation is on one page.
Why an agent-written system needs this more than a hand-written one
The core rule is one sentence: CRM state is deterministic; AI agents interpret the user's intent and compose the system, but state changes pass through services and workflows (ARCHITECTURE.md).
A hand-written system keeps part of its rules in the memory of the person who wrote them. Six months later somebody asks why a deal was approved, and there is a human to ask. A system composed by a coding agent has no such author, and the agent that wrote it does not remember. So the rules have to be in artifacts a reviewer can diff and a machine can verify: a versioned policy, a fingerprint, a named migration, a step-level trace, an audit event. Every mutation goes through a module service or a named workflow and leaves an audit event and a step-level trace behind it (C-16), and preserving validation, actor identity, audit and trace is one of the repository's non-negotiable rules rather than a convention.
This is also why the same architecture insists that generated infrastructure is not generated behaviour. Manifests generate migrations and adapters; business logic stays explicit, reviewable source (ADR-006, ADR-011). There is no interpreter over a config format to read when you want to know what the system decided.
A version is a fingerprint, not a commit
ADR-015 puts it directly: git history alone is not runtime policy versioning. Every scoring-model, routing-policy, enrichment-provider and discount-policy version carries a deterministic SHA-256 fingerprint of its canonicalized source. A core migration adds a definition_versions table, and startup inserts-or-verifies each name, version and fingerprint in one transaction: re-registering identical source is a no-op, and a registered version whose source has changed stops the application. Definitions are immutable once registered, and rollback means publishing a new version derived from an earlier definition — v3 carrying v1's rules — never editing history.
What the fingerprint does not capture is stated as plainly as what it does. It covers a definition's own source and its declared config; a handler closing over a mutable outer variable or an out-of-file helper is not captured, because toString() serializes identifiers and not values, and closure analysis is deliberately not attempted. Thresholds and tunables must therefore live in config, which is fingerprinted and passed frozen into every evaluation. Values that cannot be canonicalized — Date, Map, Set, class instances, BigInt, symbols, non-finite numbers, cycles — fail fingerprinting loudly rather than producing a fingerprint that does not describe the definition.
The runs carry their versions with them. Every score run stores the model name, version and fingerprint it executed under plus stable references to its inputs, and persists one contribution row per rule, so a number from last quarter can still be accounted for (C-07). Every quote version stores the discount policy's decision, name, version and fingerprint (C-08).
Atomicity, and the outbox named precisely
ADR-012 was written after two real gaps were found rather than imagined: the workflow engine ran steps with manual compensation and no outer transaction, and generated services emitted domain events as each savepoint released, so a subscriber could observe a half-applied multi-step action. An action's business writes now run inside one BEGIN IMMEDIATE transaction with generated-service savepoints nested inside it — all writes commit or none do — and the event bus installs a transaction-scoped outbox on Node's AsyncLocalStorage. Events are queued inside that scope, dispatched only after the commit, and dropped on rollback or on a throw. The workflow trace is written after the transaction resolves, so a failed action still leaves a record (ADR-011).
The ADR then names the thing precisely, and this page repeats the name rather than the benefit: this is a transaction-scoped in-process event buffer, not a durable transactional outbox. Events live only in memory until the flush, and a process crash after the database commit but before the flush loses delivery. In-process subscribers are the intended consumers. A persistent outbox is required before these events can back remote integrations, and none is claimed.
Schema history is append-only too
A generated module that already holds rows gains a field, a status or an index by an explicit revision bump. A changed schema at the same revision is refused and names the field; an identical schema with a bumped revision is refused; skipping a revision is refused, so each step has its own reviewable migration. The factory then appends exactly one migration for the difference, and the next boot runs it (ADR-019, docs/MODULE_EVOLUTION.md, C-19).
History is not rewritten. The create migration keeps its original identity forever and is never regenerated from a newer manifest, because its checksum is recorded in every database that ran it; an edited applied migration stops the next boot. The module.state.json file that records the last generated definition is checked in deliberately — it travels with the source, is reviewable in a diff, exists before any database does, and is identical in every environment — and a hand-edited one is refused, because its fingerprint and every migration checksum must match what they describe.
What v1 refuses is the list of things that could lose or silently reinterpret stored data, and every refusal happens before any file or database write: removing or renaming a field, changing a type, narrowing an enum, adding a required or unique field, changing unique or a reference target or delete rule, renaming a table, and rebuilding a table another table holds a foreign key into. There is no data transformation, no default backfill, no arbitrary SQL hook and no field split or merge. If you need one, you write and review it yourself.
Evidence you can break on purpose
Determinism claims are easy to write and hard to check, so five of them can be removed on purpose in one command. Each mutation deletes exactly one rule this project makes in public, runs the suite that is supposed to defend it, and reports which named test caught it — in about two seconds, with every edit restored in a finally block and the restoration verified byte-for-byte (C-23, docs/FALSIFY.md). One of the five is definition-version-immutability: with it removed, a scoring model or discount policy could change behaviour while keeping its version, so every historical decision citing that version would now cite something else.
The same discipline shows up in what the domains persist. Delivery records what it consumed as append-only time and expense evidence, costed server-side by a versioned fingerprinted policy, with a reproducible contribution estimate (C-12). A submitted quote version snapshots everything needed to reproduce the commercial decision, and a later catalog sync leaves every existing quote version byte-identical (C-08).
The evidence this page rests on
Claims and limitations are printed from site/claims.json word for word. Job statuses come from docs/benchmarks/jobs.json; a job with no page of its own is listed with its status rather than linked.
Claims
- C-16 Every mutation goes through a module service or a named workflow, and leaves an audit event and a step-level trace behind it.
LimitAudit records what the process did under an asserted actor. It is not a tamper-evident or externally attestable log, and it is not a compliance control.
- C-07 Scoring is explainable and versioned: every score carries the fingerprint of the model version that produced it, so a number from last quarter can still be accounted for.
LimitDeterministic weighted rules, not a machine-learning model. Nothing trains, fits or backtests.
- C-08 Quotes price on the server from a catalog — one-time and recurring, flat, per-unit, volume and graduated tiers — and freeze into an immutable version when a discount goes for approval.
LimitCatalog sync runs against a fixture provider; no real external catalog (Stripe, Zuora, ERP) is connected. Money is integer cents with no FX — currencies are never summed.
- C-12 Delivery records what it consumed: append-only time and expense evidence, costed server-side by a versioned fingerprinted policy, with a reproducible contribution estimate grouped by currency.
LimitDeliberately not a margin: no revenue recognition, no cost of goods sold, no ARR/MRR/TCV, no annualization, no FX. A project carrying a recurring obligation returns no estimate at all and says why.
- C-19 Generated modules evolve without rewriting history: explicit revisions, a checked-in state file and append-only named migrations.
LimitThe view is source-only: what the checked-in revisions and migrations say is knowable; what a particular database has actually applied is not.
- C-23 Five rules can be removed on purpose in one command, and the suite catches every one in about two seconds — naming the test that caught it. Anything that survives is reported as a gap, not omitted.
LimitIt falsifies six named rules, not the claims in this ledger, and it proves only that a test holds each one — a rule that is wrong but faithfully defended passes every mutation. It is not mutation testing: nothing is generated or sampled, and no score is derived.
Limitations
- L-04 No scheduler, no task engine, no reminders. One follow-up Task is created inside lead qualification. There is no recurring work, no delayed workflow, no queue — so nothing fires on a renewal notice period either.
- L-02 SQLite only. Persistence is Node's built-in SQLite adapter. PostgreSQL is on the Production Spine track and is not implemented.
- L-01 No authentication, tenancy or RBAC. The server is local-development-only. An actor header is an assertion, not an identity. Do not expose it to a network.
Jobs it covers
- JTBD-LI-07 Version and publish a scoring/routing policy — validated end to end
- JTBD-LI-08 Roll back a policy to an earlier version — partially supported
- JTBD-LI-09 Reproduce a historical routing decision exactly — partially supported
- JTBD-CS-10 Read a complete amendment history — not supported
- JTBD-DG-05 Apply a retention policy and evidence that it ran — not supported
- JTBD-DG-08 Prove a deletion request completed — not supported