# CI enforcement of the audit contract (/docs/ci-enforcement)



The auditability and replayability contract isn't a promise in
prose. Each clause is backed by a named `npm run audit:*` gate that
fails CI when the invariant breaks. This page reads the
[full gate catalog](/docs/audit-gates) through the lens of those two
contracts: it pairs every clause with the gate that enforces it and
the condition that turns the gate red.

For the exhaustive, subsystem-organized list — package shape, plugin
contracts, tool coverage, graph lattice — see
[Audit gates](/docs/audit-gates). This page is the contract-first
index into that catalog.

<SourceMeta subpath="@pleach/core" source="{ label: &#x22;scripts/audit/&#x22;, href: &#x22;https://github.com/pleachhq/core/tree/main/scripts/audit&#x22; }" />

## How a gate runs [#how-a-gate-runs]

Every gate is an `.mjs` or `.ts` script invoked through npm:

```bash
npm run audit:<name>
```

Most are pure source-text scans that finish in under a second. They
fall into two shapes:

* **Source-text regression.** The gate asserts a set of canonical
  anchors (a flag name, an exported symbol, a column literal) still
  exist in the production files. Drift in any anchor signals the
  invariant is being silently rolled back. The script names the
  anchor in its failure output, so the fix is the file and symbol to
  restore.
* **Soak ledger.** The gate reads a rolling window of runtime probe
  emissions and passes only when the last N batches are clean. Soak
  gates verify a behavior reached production, not just that the code
  still compiles.

Suffix conventions are stable across the suite. `:strict` is the CI
mode that fails on novel drift; `:json` emits machine-readable
output; `:list` dumps the inventory; `:update-baseline` regenerates
the allowlist after intentional drift. Gates exit `0` clean, `1` on
a contract failure, `2` on a config or IO error.

## The auditability contract [#the-auditability-contract]

The promise: every model call writes one typed, tenant-scoped,
reconstructable row to the event log. Six gates hold the clauses up.

| Contract clause                            | Gate                                                                  | What turns it red                                                                                                                                                                                                                                                                                                                                                        |
| ------------------------------------------ | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| The row shape is locked and versioned      | `audit:auditable-call`                                                | A fingerprint diff in `AuditableCall.ts` without a matching `AuditRecordVersion` bump — the silent-breaking-change detector. Composite of `audit:auditable-call-shape` (TS wire-format fingerprint) + `audit:auditable-call-version`.                                                                                                                                    |
| Persisted rows match the typed wire format | `audit:auditable-call-soak`                                           | A DB-side row missing a required cluster field, a pre-projected flag disagreeing with its input, an `audit_record_version` below the per-cluster floor (`familyLock ≥ 2`, `cacheBreakpoint ≥ 3`, `fallbackStep ≥ 4`, `providerCascade ≥ 5`), or a gap in `seq_within_turn` (the dropped-write tamper signal). Runs `--print-sql` by default — no DB connection, CI-safe. |
| Every row can reconstruct its substrate    | `audit:event-log-manifest-hash-completeness`                          | A `harness_event_log` write site in `SessionRuntime.ts` or `EventLogWriter.ts` that neither stamps `manifest_hash`, routes through the `eventToRow` wire-layer stamper, nor carries a `// @no-manifest-hash-allowlist` annotation. `manifest_hash` is the foreign reference replay uses to recover the runtime config active when the event fired.                       |
| The referenced manifest actually exists    | `audit:config-manifest-referential-integrity`                         | An event row pointing at a manifest that isn't present.                                                                                                                                                                                                                                                                                                                  |
| A referenced manifest survives GC          | `audit:config-manifest-retention-completeness`                        | A surviving event referencing a manifest that retention dropped.                                                                                                                                                                                                                                                                                                         |
| Every event type is scrubber-covered       | `audit:c8-event-type-allowlist-coverage`                              | An `EventLogInput` union member with no `SCRUBBABLE_FIELDS` allowlist entry — an event type that could slip past redaction.                                                                                                                                                                                                                                              |
| Every event type is actually produced      | `audit:c8-union-member-has-producer`                                  | An `EventLogInput` union member with zero producer call sites.                                                                                                                                                                                                                                                                                                           |
| Multi-tenant rows are tenant-scoped        | `audit:tenant-scoping` + `audit:harness-event-log-tenant-id-required` | A `tenant_id`-bearing table without an RLS policy, or a `harness_event_log` write that doesn't thread `tenant_id`.                                                                                                                                                                                                                                                       |

The row shape itself is locked at `AuditRecordVersion` (currently
**18**), and `AUDIT_RECORD_VERSION_HISTORY` records every additive
bump. See [the AuditableCall row](/docs/auditable-call-row) for the
field-by-field shape.

## The replayability contract [#the-replayability-contract]

The promise: a recorded session re-executes deterministically
against an untampered log, and the chain proves the log wasn't
mutated after the fact. Four gates hold these clauses up.

| Contract clause                                        | Gate                                                       | What turns it red                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| ------------------------------------------------------ | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The hash-chain wire-in stays intact                    | `audit:c9-hash-chain-integrity`                            | Drift in a C9 anchor (`c9PhaseBEnabled`, `chainStep`, `verifyChainForChat`, `generateProof`) across `EventLogWriter.ts` / `hashChain.ts` / `index.ts`. Also evaluates a 3-batch soak when probe batches are staged: `[UXParity:c9-hash-chain-row-stamp]` count `> 0`, every `[UXParity:c9-hash-chain-verify]` carrying `chainValid: true`, and zero legacy-prefix diagnostics. The soak ledger ships empty, so the gate is informational today; `:strict` is the verifier-CLI promotion gate. |
| Canonicalization is versioned                          | `PLEACH_C9_CANONICALIZATION_VERSION`                       | Pinned at `"pleach.c9.v1"`. `verifyChain` returns `{ ok: true }` or a row-precise `{ ok: false, failedIndex, expected, actual, reason }`. The verifier and writer share the canonicalization helper by construction.                                                                                                                                                                                                                                                                          |
| The replay surface can't regress before its next phase | `audit:eval-phase-a-coverage`                              | A drift in the 19 Phase A anchors across `@pleach/eval`'s four subpaths (`./`, `./scorers`, `./replay`, `./report`), or test coverage falling below the floor in `__tests__/eval/`. `:strict` gates the Phase B dispatch — Phase B can't land until this holds clean.                                                                                                                                                                                                                         |
| The graph stays a fixed lattice                        | `audit:graph-stages` + `audit:edge-inventory-completeness` | A compiled cross-stage edge that isn't in the allowed-edge inventory. Replay can only reproduce a graph whose edges are fixed; an out-of-lattice edge breaks deterministic re-execution.                                                                                                                                                                                                                                                                                                      |

See [Tamper-evident hash chain](/docs/hash-chain) for what the chain
proves and the verifier surface, and
[Eval and replay](/docs/eval-and-replay) for the replay path that
reads the chain.

## Where the gates run [#where-the-gates-run]

Three CI jobs carry the contract gates on every merge:

| CI job               | What it runs                                                                                                                                                                     |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `graphnoderef`       | `npm run ci:graphnoderef` — chains `audit:auditable-call`, `audit:event-log-manifest-hash-completeness`, the three `audit:config-manifest-*` gates, and the graph-lattice gates. |
| `local-clone`        | `npm run audit:harness-package:local-clone` — packs via `npm pack`, installs into an isolated scratch dir, and smoke-imports `SessionRuntime` + `createPleachRuntime`.           |
| `consumer-rehearsal` | A per-SKU matrix running `npm run audit:consumer-rehearsal:<sku>` — the six-phase end-to-end publish rehearsal.                                                                  |

The per-SKU consumer rehearsal also runs on a weekly cron (Sundays
08:00 UTC) and nightly, so a packaging break that a PR didn't touch
still surfaces within a day rather than at the next publish.

## Reproducing the gates as a consumer [#reproducing-the-gates-as-a-consumer]

A host embedding `@pleach/core` inherits the contract but not the
CI. Two gates are built to be run from outside the source repo:

* `audit:consumer-rehearsal:core` walks six phases against a packed
  tarball — build, `npm pack` + install of the target and its peers
  into a scratch dir, facet smoke imports, one subpath import per
  `package.json:exports` key, a TypeScript `.d.ts` shape check under
  `strict`, and example runs. It writes only outside the repo and is
  idempotent across runs.
* `audit:harness-package:local-clone` proves the published surface
  imports cleanly from an isolated install — the check that catches
  an unconditional-import blocker before a first publish.

For the wider mirroring pattern — one invariant per script,
structured failure output, documented next to the gate — see
[Audit gates → Mirroring gates in your own repo](/docs/audit-gates#mirroring-gates-in-your-own-repo).
The discipline is that adding a new invariant means adding a new
gate, not a line on a review checklist.

## Where to go next [#where-to-go-next]

<Cards>
  <Card title="Audit gates" href="/docs/audit-gates" description="The full subsystem-organized gate catalog this page indexes by contract." />

  <Card title="AuditableCall row" href="/docs/auditable-call-row" description="The typed per-call row shape locked at AuditRecordVersion." />

  <Card title="Tamper-evident hash chain" href="/docs/hash-chain" description="What the chain proves and the verifier surface the C9 gate protects." />

  <Card title="Eval and replay" href="/docs/eval-and-replay" description="The replay surface that reads the chain to assert deterministic re-execution." />

  <Card title="Config manifest" href="/docs/config-manifest" description="The content-addressable substrate snapshot manifest_hash points back to." />
</Cards>
