pleach
Reference

CI enforcement of the audit contract

The named audit gates that make the auditability and replayability contract load-bearing in CI — what each clause promises, the gate that fails the build when it breaks, and where the gates run.

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 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. This page is the contract-first index into that catalog.

Subpath@pleach/coreSourcescripts/audit/

How a gate runs

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

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 promise: every model call writes one typed, tenant-scoped, reconstructable row to the event log. Six gates hold the clauses up.

Contract clauseGateWhat turns it red
The row shape is locked and versionedaudit:auditable-callA 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 formataudit:auditable-call-soakA 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 substrateaudit:event-log-manifest-hash-completenessA 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 existsaudit:config-manifest-referential-integrityAn event row pointing at a manifest that isn't present.
A referenced manifest survives GCaudit:config-manifest-retention-completenessA surviving event referencing a manifest that retention dropped.
Every event type is scrubber-coveredaudit:c8-event-type-allowlist-coverageAn EventLogInput union member with no SCRUBBABLE_FIELDS allowlist entry — an event type that could slip past redaction.
Every event type is actually producedaudit:c8-union-member-has-producerAn EventLogInput union member with zero producer call sites.
Multi-tenant rows are tenant-scopedaudit:tenant-scoping + audit:harness-event-log-tenant-id-requiredA 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 for the field-by-field shape.

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 clauseGateWhat turns it red
The hash-chain wire-in stays intactaudit:c9-hash-chain-integrityDrift 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 versionedPLEACH_C9_CANONICALIZATION_VERSIONPinned 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 phaseaudit:eval-phase-a-coverageA 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 latticeaudit:graph-stages + audit:edge-inventory-completenessA 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 for what the chain proves and the verifier surface, and Eval and replay for the replay path that reads the chain.

Where the gates run

Three CI jobs carry the contract gates on every merge:

CI jobWhat it runs
graphnoderefnpm 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-clonenpm run audit:harness-package:local-clone — packs via npm pack, installs into an isolated scratch dir, and smoke-imports SessionRuntime + createPleachRuntime.
consumer-rehearsalA 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

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. The discipline is that adding a new invariant means adding a new gate, not a line on a review checklist.

Where to go next

On this page