@clear-capabilities/agentic-security-scanner 0.161.0 → 0.163.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/bin/agentic-security.js +98 -2
  3. package/dist/1340.index.js +1871 -0
  4. package/dist/3477.index.js +727 -0
  5. package/dist/3499.index.js +23 -3
  6. package/dist/4113.index.js +59 -8
  7. package/dist/5014.index.js +833 -0
  8. package/dist/5756.index.js +2 -2
  9. package/dist/6257.index.js +1 -1
  10. package/dist/6907.index.js +248 -0
  11. package/dist/6994.index.js +1 -1
  12. package/dist/7033.index.js +33 -5
  13. package/dist/7439.index.js +1822 -0
  14. package/dist/7838.index.js +38 -2
  15. package/dist/8798.index.js +1 -1
  16. package/dist/9207.index.js +5 -0
  17. package/dist/9560.index.js +2336 -27
  18. package/dist/957.index.js +1 -1
  19. package/dist/{1905.index.js → 9999.index.js} +306 -13
  20. package/dist/agentic-security.mjs +12 -12
  21. package/dist/agentic-security.mjs.sha256 +1 -1
  22. package/package.json +39 -7
  23. package/src/capabilities/CLAUDE.md +114 -0
  24. package/src/capabilities/address.js +73 -0
  25. package/src/capabilities/attack-coverage.js +93 -0
  26. package/src/capabilities/decide.js +256 -0
  27. package/src/capabilities/delegate.js +69 -0
  28. package/src/capabilities/egress.js +20 -0
  29. package/src/capabilities/hook-advice.js +50 -0
  30. package/src/capabilities/index.js +32 -0
  31. package/src/capabilities/manifest.js +344 -0
  32. package/src/capabilities/outbound.js +189 -0
  33. package/src/capabilities/paths.js +57 -0
  34. package/src/capabilities/probes.js +211 -0
  35. package/src/capabilities/proxy.js +182 -0
  36. package/src/capabilities/reasons.js +67 -0
  37. package/src/capabilities/receipts.js +356 -0
  38. package/src/capabilities/records.js +58 -0
  39. package/src/capabilities/recovery.js +367 -0
  40. package/src/capabilities/report.js +81 -0
  41. package/src/capabilities/runner.js +236 -0
  42. package/src/capabilities/secrets.js +26 -0
  43. package/src/capabilities/tool-gate.js +119 -0
  44. package/src/capabilities/tool-registry.js +60 -0
  45. package/src/dataflow/CLAUDE.md +29 -0
  46. package/src/dataflow/catalog.js +21 -0
  47. package/src/dataflow/engine.js +20 -0
  48. package/src/dataflow/guard-dominance.js +352 -0
  49. package/src/discovery/CLAUDE.md +18 -0
  50. package/src/discovery/index.js +43 -1
  51. package/src/discovery/lenses.js +18 -0
  52. package/src/discovery/memory.js +7 -5
  53. package/src/engine.js +80 -15
  54. package/src/fix/apply-fix-service.js +33 -3
  55. package/src/integrations/index.js +5 -2
  56. package/src/ir/CLAUDE.md +14 -0
  57. package/src/ir/implicit-handler-params.js +74 -0
  58. package/src/ir/parser-cs.js +20 -0
  59. package/src/ir/parser-go.js +30 -13
  60. package/src/ir/parser-java.js +11 -0
  61. package/src/language/nix-eval-isolation.js +33 -5
  62. package/src/lineage/CLAUDE.md +31 -0
  63. package/src/lineage/deployment/adapter-cloud.js +202 -0
  64. package/src/lineage/deployment/adapter-compose.js +90 -0
  65. package/src/lineage/deployment/adapter-kit.js +142 -0
  66. package/src/lineage/deployment/adapter-kubernetes.js +247 -0
  67. package/src/lineage/deployment/attack-paths.js +443 -0
  68. package/src/lineage/deployment/boundaries-run.js +223 -0
  69. package/src/lineage/deployment/boundary-graph.js +567 -0
  70. package/src/lineage/deployment/cross-repo.js +253 -0
  71. package/src/lineage/deployment/drift.js +383 -0
  72. package/src/lineage/deployment/ingest.js +221 -0
  73. package/src/lineage/deployment/projection.js +244 -0
  74. package/src/lineage/deployment/trace-correlation.js +541 -0
  75. package/src/lineage/source-registry.js +19 -0
  76. package/src/mcp/CLAUDE.md +14 -3
  77. package/src/mcp/invariant-tools.js +64 -0
  78. package/src/mcp/portfolio-tools.js +79 -0
  79. package/src/mcp/redact.js +15 -0
  80. package/src/mcp/server.js +24 -3
  81. package/src/mcp/tools.js +30 -5
  82. package/src/posture/CLAUDE.md +505 -0
  83. package/src/posture/accuracy-scorecard.js +95 -4
  84. package/src/posture/assurance/baseline-inventory.js +218 -0
  85. package/src/posture/assurance/baseline.js +322 -0
  86. package/src/posture/assurance/bounded-io.js +145 -0
  87. package/src/posture/assurance/config.js +299 -0
  88. package/src/posture/assurance/contracts.js +315 -0
  89. package/src/posture/assurance/identity.js +62 -0
  90. package/src/posture/assurance/migrations.js +259 -0
  91. package/src/posture/assurance/rollback.js +214 -0
  92. package/src/posture/assurance/rollout.js +200 -0
  93. package/src/posture/assurance/schema-kit.js +111 -0
  94. package/src/posture/assurance/verification-record.js +258 -0
  95. package/src/posture/attestation.js +5 -0
  96. package/src/posture/autopilot.js +28 -1
  97. package/src/posture/business-logic.js +26 -4
  98. package/src/posture/evaluation/bakeoff.js +148 -0
  99. package/src/posture/evaluation/custody.js +200 -0
  100. package/src/posture/evaluation/deployment-ablation.js +416 -0
  101. package/src/posture/evaluation/dev-cases.js +65 -0
  102. package/src/posture/evaluation/economics.js +85 -0
  103. package/src/posture/evaluation/gates.js +161 -0
  104. package/src/posture/evaluation/grouping.js +101 -0
  105. package/src/posture/evaluation/interval.js +132 -0
  106. package/src/posture/evaluation/invariant-ablation.js +325 -0
  107. package/src/posture/evaluation/labels.js +200 -0
  108. package/src/posture/evaluation/protocol.js +308 -0
  109. package/src/posture/evaluation/release-gate.js +209 -0
  110. package/src/posture/evaluation/report.js +158 -0
  111. package/src/posture/evaluation/runner.js +273 -0
  112. package/src/posture/evaluation/scan-child.js +80 -0
  113. package/src/posture/evaluation/score.js +174 -0
  114. package/src/posture/evaluation/synthetic.js +90 -0
  115. package/src/posture/evaluation/why-missed.js +219 -0
  116. package/src/posture/evidence-bundle.js +93 -7
  117. package/src/posture/execution-proof.js +11 -1
  118. package/src/posture/fix-verify-loop.js +20 -6
  119. package/src/posture/fix-verify.js +37 -2
  120. package/src/posture/fleet.js +11 -2
  121. package/src/posture/invariants/cli.js +95 -0
  122. package/src/posture/invariants/coverage.js +164 -0
  123. package/src/posture/invariants/export.js +178 -0
  124. package/src/posture/invariants/expressions.js +46 -0
  125. package/src/posture/invariants/lifecycle.js +289 -0
  126. package/src/posture/invariants/project-input.js +55 -0
  127. package/src/posture/invariants/repair.js +340 -0
  128. package/src/posture/invariants/run.js +45 -0
  129. package/src/posture/invariants/scenarios.js +311 -0
  130. package/src/posture/invariants/schema.js +167 -0
  131. package/src/posture/invariants/shrink.js +407 -0
  132. package/src/posture/invariants/state-assertions.js +165 -0
  133. package/src/posture/logic-claims.js +12 -0
  134. package/src/posture/model-routing.js +40 -0
  135. package/src/posture/oracles/adapters.js +531 -0
  136. package/src/posture/oracles/conformance.js +276 -0
  137. package/src/posture/oracles/oracle.js +453 -0
  138. package/src/posture/oracles/registry.js +76 -0
  139. package/src/posture/oracles/scenario-classes.js +223 -0
  140. package/src/posture/portfolio/backend.js +279 -0
  141. package/src/posture/portfolio/bundle.js +314 -0
  142. package/src/posture/portfolio/cli.js +142 -0
  143. package/src/posture/portfolio/manifest.js +263 -0
  144. package/src/posture/portfolio/progress.js +267 -0
  145. package/src/posture/portfolio/resume.js +130 -0
  146. package/src/posture/portfolio/retention.js +207 -0
  147. package/src/posture/portfolio/scheduler.js +488 -0
  148. package/src/posture/portfolio/signing.js +155 -0
  149. package/src/posture/portfolio/wording.js +59 -0
  150. package/src/posture/portfolio/work-units.js +405 -0
  151. package/src/posture/replay/replay.js +250 -0
  152. package/src/posture/routing/adapter.js +140 -0
  153. package/src/posture/routing/calibration.js +278 -0
  154. package/src/posture/routing/canary.js +141 -0
  155. package/src/posture/routing/control.js +35 -0
  156. package/src/posture/routing/decide.js +211 -0
  157. package/src/posture/routing/drift.js +137 -0
  158. package/src/posture/routing/economics.js +185 -0
  159. package/src/posture/routing/feedback.js +207 -0
  160. package/src/posture/routing/index.js +31 -0
  161. package/src/posture/routing/outcomes.js +281 -0
  162. package/src/posture/routing/promotion.js +291 -0
  163. package/src/posture/routing/receipts.js +59 -0
  164. package/src/posture/routing/report.js +142 -0
  165. package/src/posture/routing/shadow.js +53 -0
  166. package/src/posture/specification-mining.js +26 -0
  167. package/src/posture/verification/advisory-state.js +71 -0
  168. package/src/posture/verification/emit.js +257 -0
  169. package/src/posture/verification/hypothesis-promotion.js +211 -0
  170. package/src/posture/verification/patch-negative.js +226 -0
  171. package/src/posture/verification/patch-promotion.js +113 -0
  172. package/src/posture/verification/projection.js +183 -0
  173. package/src/posture/verification/repair-records.js +124 -0
  174. package/src/posture/verifier.js +16 -0
  175. package/src/report/index.js +100 -1
  176. package/src/sandbox/CLAUDE.md +51 -0
  177. package/src/sandbox/backend-namespace.js +7 -0
  178. package/src/sandbox/backend-userspace.js +151 -34
  179. package/src/sandbox/control-probes.js +215 -0
  180. package/src/sandbox/supervise.js +201 -0
  181. package/src/sandbox/trust-boundary.js +113 -0
  182. package/src/sandbox/trust-domains.js +171 -0
@@ -96,6 +96,8 @@ Wired in `bin/agentic-security.js` after every filter and after `makeDeterminist
96
96
 
97
97
  **`verifyRunAttestation` now has two real callers.** `agentic-security verify-attestation <file>` auto-detects whether the given JSON is an evidence bundle (`.finding`+`.signature`, verified via `evidence-bundle.js`'s Ed25519 path, unchanged) or a run attestation (`.digest`+`.canonicalisation`, either bare or embedded under a full `last-scan.json`'s `.attestation` field) and dispatches accordingly. A run attestation isn't self-contained the way a bundle is — verifying it means re-scanning the project (`--against <path>`, default `.`) and confirming the fresh scan reproduces the attested digest, which is the actual, meaningful claim this artifact makes ("does this codebase, scanned now, match what was attested earlier"). Separately, `scripts/release-check.mjs`'s `attestation-self-check` gate round-trips a synthetic finding set through compute→verify (and a mutated copy through verify, which must fail) on every release, catching a broken canonicalisation or signing path before it ships — independent of whether any project ever calls `verify-attestation` on a real artifact.
98
98
 
99
+ **Issuer and trust basis (CORE-003.AC03).** `evidence-bundle.js` signs an `issuance` block inside the canonical bytes (only when present, so pre-existing bundles keep their signatures): issuer id and key fingerprint, `trustBasis: 'self-issued-local-key'`, `independentlyCertified: false`, and a plain statement. `verifyEvidenceBundle` returns `trustBasis`/`issuer`/`trustNote`; a bundle with no `issuance` verifies and is reported `none-declared` (never as trusted); an unknown basis, an `independentlyCertified: true` claim, or an issuer fingerprint that is not the verifying key is rejected even when validly signed. The only basis this build can verify is the local one: a third-party basis needs a trust-root policy that does not exist here. A signed run attestation carries the same self-issued label. `verify-attestation` exit codes are unchanged. Signing keys are protected from workers by `sandbox/trust-boundary.js` (see `sandbox/CLAUDE.md`). Suite: `test/evidence-issuer.test.js`.
100
+
99
101
  **Integrity + signing** — `integrity.js` (per-install HMAC for `last-scan.json`), `rule-pack-signing.js`. The HMAC key lives at `$XDG_CONFIG_HOME/agentic-security/scan-key`; override via `$AGENTIC_SECURITY_HMAC_KEY`. Premortem-derived; do not regress to hostname-derived.
100
102
 
101
103
  **Rule lifecycle** — `custom-rules.js` (YAML pattern DSL), `rule-overrides.js` (`disable:` gated on signature), `rule-packs.js`, `rule-synthesis.js` (proposes suppressions from triage feedback), `ruleset-version.js`.
@@ -139,6 +141,509 @@ one was caught by the module's own test, not in review.
139
141
  - **No throwing.** Every annotation in `engine.js` is wrapped `try { … } catch (_) {}`. Your annotator must degrade gracefully — set `null` on the field and continue.
140
142
  - **Dead-module test.** `npm run test:lifecycle` fails the build if you export a public symbol from a posture module that no other source file imports. Wire it in `engine.js` (or allowlist it with a written reason in `test/no-dead-modules.test.js`).
141
143
 
144
+ ## Assurance contracts, baseline and rollout config — `assurance/` (11 modules)
145
+
146
+ Foundation for the differentiation work (CORE-001, CORE-002, CORE-004). All of it is pure or read-only, and **nothing in it is wired into `engine.js` or the scan/report path**: a default scan is byte-for-byte unaffected, which `test/posture/assurance-config.test.js` pins. Consumers (X-201 and later) import it directly.
147
+
148
+ - **`identity.js`**: ids for evidence records. Reuses `canonicalJson` (`evidence-bundle.js`) and `computeStableId` (`stable-id.js`, a hypothesis id is a finding's stable id). An id is a hash over an **allowlist** of semantic fields, so clocks (`createdAt`) and migration bookkeeping can never reach it. Prefixes `vrec/obind/capd/rlab/rev` are distinct from lineage's, and nothing here imports `lineage/` (a test checks that).
149
+ - **`schema-kit.js`**: validator primitives. Closed-world (unknown fields rejected), typed error codes, supported major version 1 only, `sha256:<64 hex>` digests.
150
+ - **`verification-record.js`**: the one verification record. Six distinct outcomes, a SEPARATE repair status, four evidence kinds. Structural rules: a model can only produce `inference`; `confirmed` needs referenced trusted-runtime-proof or independent-adjudication evidence; `confirmationLevel` must equal the level derived from the evidence (a forged level fails); `refuted` needs an applicable non-model oracle, valid preconditions and proving evidence.
151
+ - **`contracts.js`**: observation binding (references a lineage observation by id only; absence in a sampled observation cannot establish `blocked`), capability decision (only runner/proxy mediation with a backend and an active-probe digest may claim `enforced`; a hook or in-process check never can), routing label (a decided label must name an adjudication or trusted-execution source), release evidence (`complete` is derived, never trusted), and `validateRecordSet` for duplicate ids, dangling references and conflicting claims.
152
+ - **`migrations.js`**: explicit v1 adapters from legacy shapes (proof tier, boolean verified, egress decision, attestation, lineage observation) and a legacy view back out. Evidence can be lost, never invented: a legacy `true` is `inconclusive`, `proof-failed` is `inconclusive` not `refuted`, and the only route to `confirmed` is an explicit `trustedRunner` assertion bound to a commit. The legacy view's `verified` is `true` only for confirmed and `false` only for refuted, otherwise `null`.
153
+ - **`config.js`**: the unified feature configuration. Reuses the env/`.agentic-security/*.yml` mechanism. Precedence: kill switch (`AGENTIC_SECURITY_NO_ASSURANCE` or `AGENTIC_SECURITY_NO_<FEATURE>`) > explicit override > `AGENTIC_SECURITY_ASSURANCE_<FEATURE>` > `.agentic-security/assurance.yml` > default (everything off). **A project file can never enable a `high-risk-execution` feature** (the file lives in the scanned repo); invalid configuration fails closed; unsupported platforms return a typed `unsupported` with the disclosure. `runFeature` returns typed `blocked`/`unsupported`/`degraded`/`disabled` results and never prompts.
154
+ - **`bounded-io.js`**: `guardedModelCall` is the only route for new model/network calls. It owns no HTTP client (the transport is injected): feature gate, then `evaluateEgress` BEFORE the payload exists, then `redactPayload`, then the request-size limit, then deadline plus bounded retries, then output cap, with every decision appended to the existing egress audit chain. `readFileBounded`, `capOutput`, `withDeadline`, `retryBounded` enforce the limits `config.js` claims; `maxMemoryMiB` is carried for a runner and disclosed as not enforced here.
155
+ - **`baseline.js`, `baseline-inventory.js`**: baseline capture (`scripts/baseline-capture.mjs`, `npm run baseline:capture`). Read-only; records HEAD, dirty paths, exact source and bundle digests, tool versions, entry points, the seven mappings and an 18-entry capability inventory (implemented / partial / unsupported / unmeasured, every path and exported symbol verified at capture time, a missing one is a reported problem). Unknown facts are `{status:'unknown', reason}` with no value. `evaluateBaseline` invalidates exactly the capabilities whose cited files changed and lists unrelated user changes as retained/introduced. The manifest is machine-specific, so it is generated on demand and not committed. When you change a capability's source or tests, update its inventory entry in the same change.
156
+
157
+ - **`rollout.js`, `rollback.js` (REL-002).** Staged rollout and rollback. `rollout.js`: the three stages (`offline-fixtures`, `shadow-canary`,
158
+ `opt-in-supported`), the documented gates per feature and stage (`gatesFor`, `describeRollout`), an append-only ledger (`newLedger`), `promote`
159
+ (one stage at a time, every gate needs evidence that names its own suite and carries a sha256; the kill switch and an unsupported platform
160
+ block it; policy versions are never reused) and `effectiveMode` (an enabled feature below opt-in runs as `fixtures` or `shadow` and can never
161
+ change an outcome). `rollback.js`: `READER_POLICY` (per record kind, whether a previous reader is retained; four kinds honestly say no),
162
+ `planRollback`/`applyRollback` (restore the last known-good stage, append a `rollback` event, FLAG evidence and claims `stale` or
163
+ `incompatible`, never delete or rewrite), `EvidenceStore`/`guardedFeatureRun` (append-only, a thrown feature records a failure and costs no
164
+ evidence), and the known break: `verifyWithPreCore003Reader` is a faithful copy of the verifier before the `issuance` block existed, which
165
+ rejects every newly signed bundle; `exportLegacyBundle` is the migration path (verifies the original first, re-signs without issuance,
166
+ returns a sidecar carrying the dropped trust basis). Pure, consumed by tests and `docs/guides/assurance-rollout-and-rollback.md`; nothing in
167
+ the scan path imports them. Suite: `test/release-closure/` (`npm run test:release-closure`). The release closure itself (the plan, the
168
+ commit-bound record, the judge) is `scripts/release-closure.mjs`, not a posture module.
169
+
170
+ Tests: `test/posture/assurance-{baseline,contracts,config}.test.js`, all in `test:posture`.
171
+
172
+ ## One verification record, trusted oracles and replay (X-201, X-202, X-203)
173
+
174
+ Built on `assurance/` (the record schema, migrations, config) and `sandbox/trust-boundary.js` (the boundary). Three
175
+ directories, all consumed rather than wired into the default scan: a default scan is unchanged, and every addition to an
176
+ existing output is an additive field.
177
+
178
+ **`verification/emit.js`: the one emit function (X-201).** `emitVerification(surface, source, ctx)` is what every
179
+ verification surface calls to get the version-1 record (`{ ok, record, legacy, errors }`, never throws, validated before it
180
+ returns). Surfaces and where they call it: `fix-verify.js` (`verificationRecord` on the result, which the MCP `verify_fix` and
181
+ `apply_fix` tools forward), `fix-verify-loop.js`, `execution-proof.js` (`proveFinding` result), `verifier.js`
182
+ (`verifierVerificationRecords`, called by the CLI `verify` command into its `verifier-runs/` run record, never onto the findings that
183
+ are written back to `last-scan.json`), `autopilot.js` (`rec.verificationRecord`, `commit` option), `discovery/index.js` (hunt:
184
+ `verificationRecords`, advisory) and `oracles/oracle.js` (the `oracle` surface). The rule for every mapping: evidence is lost,
185
+ never invented, so no legacy surface can reach `confirmed` or `refuted`. A PoC run under the older sandbox path is `inconclusive`
186
+ (not a trusted runner; `proof-failed` is a triage signal, not a refutation), a static re-scan with no exploit oracle is `not-run`,
187
+ a family with no oracle is `unsupported`, a harness failure is `error`, a model verdict is `inference` evidence only. Only
188
+ `oracle` emissions carry trusted-runner evidence, because only `oracles/oracle.js` builds them.
189
+
190
+ Legacy consumers: nothing existing was renamed or removed. For a consumer that wants a status or a boolean, `toLegacyVerificationView`
191
+ (`assurance/migrations.js`, also returned as `legacy`) maps `verified` to `true` only for `confirmed` and `false` only for `refuted`,
192
+ `null` for the other four, and carries `status` with the real outcome name, so a skipped check can never read as a pass and an unrun
193
+ check never as a failure. Native vocabularies keep their own fields (`proofTier`, `verifier_verdict`, autopilot `outcome`, fix-verify
194
+ `ok`); the record sits beside them as `verificationRecord`. A boolean `true` migrated from a legacy record is `inconclusive`.
195
+
196
+ **`oracles/` (X-202).** `oracle.js` is the adapter contract (`validateOracleSpec`, `defineOracle`) and the runner (`runOracle`).
197
+ `adapters.js` holds the five class adapters (injection-execution, authorization-decision, state-transition,
198
+ side-effect-reachability, parser-resource), `registry.js` the frozen registry and `oracleManifest()`, the machine-readable
199
+ description X-208 gates on (class, prerequisites, platforms with an honest per-platform status, budgets, negative controls,
200
+ fixture locations, limitations, logic digest). Fixtures: `test/fixtures/oracles/<class>/{positive,negative,inconclusive}`.
201
+ - A run is: feature gate (`verification-oracles`, off by default, operator-only), request validation (closed-world: labels,
202
+ verdicts and receipts are never accepted; reserved `__oracle_*` names and path escapes are refused), prerequisite check
203
+ (an unmet one is `unsupported` with a typed reason and the target is never executed), a fresh workspace plus a verifier-owned
204
+ evidence directory outside it, the target through `runInBoundary` ONLY (a test pins that `oracle.js` imports no other way to
205
+ run code), a digest check of the adapter's own harness (a target that rewrote it makes the run `error`), then verifier-side
206
+ interpretation of what the run left behind, read after the whole process tree is dead. The settled status comes from
207
+ `settleVerification` with `observedBy: 'verifier'` evidence; a decided result (`confirmed`/`refuted`) additionally needs an
208
+ exact commit, else it is held at `inconclusive`.
209
+ - `refuted` needs the adapter's own precondition (a benign control passed); a target that failed to load, exited early or never
210
+ reported is `inconclusive`. A run cut by its deadline decides nothing except for the parser/resource class, where the supervisor's
211
+ own deadline observation IS the measurement.
212
+ - Receipts are issued only by `runOracle` (a WeakSet records them; they are deep-frozen; a copy or hand-built one is not
213
+ `isIssuedReceipt`). They carry the observed output (capped), the logic digest, sanitized environment metadata (an allowlist: no
214
+ hostname, paths or environment values) and the record id. Unsigned: signing belongs to the signer domain.
215
+ - Platforms: macOS (userspace backend) is the only host these have been executed on. Linux is declared `unverified` in the
216
+ manifest and cannot be `supported`; today the boundary refuses to run on the namespace backend, which the runner reports as
217
+ `unsupported`. Changing what an adapter asserts means bumping its `version` (the harness text and version feed `logicDigest`).
218
+ - KNOWN LIMIT: the target is imported into the harness process. A target written to recognise the oracle's payload and forge the
219
+ effect artifact is not distinguishable from a real injection; the oracle bounds the effect it tests, it is not a defence
220
+ against a target built to fool it. Tamper controls cover what a worker or target can do to adapter logic, labels, receipts and
221
+ status text.
222
+
223
+ **`replay/replay.js` (X-203).** A replay manifest pins the repository commit, the patch hash, the fixture hash, the toolchain
224
+ (runtime version, platform, architecture, and a container digest when there is one), the oracle id, version and logic digest, the
225
+ inputs and the budgets. It is a description of an oracle run, not a second runner: `replayManifest` ends in `runOracle`, so the
226
+ supervised process tree, output cap and cleanup are the existing ones. It rejects, and nothing executes, on a content hash that does
227
+ not match, an oracle that changed since the manifest was made, a scope wider than "exactly the named oracle, no network,
228
+ workspace-only writes", a budget over the oracle's ceiling, an unsafe file path or an unknown field. Missing toolchain, container
229
+ runtime or image, a denied network (acquisition is never attempted: this build fetches nothing), a disabled feature or a boundary
230
+ that cannot run are typed prerequisites with `resumable` set honestly; the attempt is parked (`awaiting-prerequisites`) and
231
+ `resume` re-evaluates the prerequisites against the real host (a claim that they are met does nothing). Outcome while parked is
232
+ `not-run` (or `unsupported` when this build can never meet it, such as container execution, which is not implemented), with no
233
+ evidence; nothing is substituted for the execution that did not happen. Toolchain identity is exact. Persistence of attempts is
234
+ the caller's (the attempt object is plain data). Suites: `test/verification/{verification-record,oracle-adapters,replay-manifest}.test.js`
235
+ (`npm run test:verification`).
236
+
237
+ ## Patch-negative verification and non-taint classes (X-204, X-205)
238
+
239
+ Same posture as the three directories above: consumed, not wired into the default scan, every output addition additive, and
240
+ nothing changes with the feature flags off. Suites: `test/verification/{patch-negative,non-taint-classes}.test.js`
241
+ (`npm run test:verification`).
242
+
243
+ **`verification/patch-negative.js` (X-204).** `verifyPatchNegative(req, { config, runOptions })` is `verified-fix` only when four
244
+ `replayManifest` runs (so each is pinned, boundary-confined and receipted) show: the exploit oracle CONFIRMS on the original
245
+ revision, REFUTES on original-plus-diff, and the `functional-regression` oracle (the requester's declared cases) is clean on the
246
+ original (a baseline: wrong expectations are reported, not blamed on the patch) and on the patched revision. It stops at the first
247
+ non-passing step and names it: the result carries `incompleteStep` and a typed `failureCode` (`original-not-reproduced`,
248
+ `patched-still-exploitable`, `patched-build-broken`, `functional-omitted`, `functional-baseline-invalid`,
249
+ `functional-regression-detected`, `oracle-changed`, `revision-unbound`, `prerequisite-unmet` (resumable where stated),
250
+ `environment-mismatch`, `promotion-refused`) and a `summary` that says what was and was not exercised. Gate: the
251
+ `patch-negative-verification` feature (high-risk execution, operator-only, off by default) plus `verification-oracles`.
252
+ Wiring: `fix-verify.js` (`verifyFix({ patchNegative, assuranceConfig })` adds `patchNegative` and `fixStatus`, and `ok` requires
253
+ `verified-fix`), `fix-verify-loop.js` (a `patchNegative` leg and the `verified-fix` verdict) and `fix/apply-fix-service.js`
254
+ (`applyVerifiedFix({ patchNegative })` writes only a promoted patch and returns `repairRecords`). With the flag off or no request,
255
+ those return exactly what they did before.
256
+
257
+ **`verification/patch-promotion.js`.** `promotePatch({ proposal, receipts })` re-derives the diff digest and revision from the
258
+ PROPOSAL and refuses any receipt that was not issued by `runOracle` (`isIssuedReceipt`: copies and hand-built receipts fail), that
259
+ covers different content than the exact diff on the original revision, that was issued for another commit or hypothesis, whose
260
+ oracle or inputs differ between the original and patched runs, whose environment identity differs, or that is reused in two roles.
261
+ Receipts are valid inside the verifier's process only; carrying a promotion across processes needs a signing domain (not here).
262
+
263
+ **`verification/repair-records.js`.** The CORE-002 record has one repair status and no value for a rejection, so the history is an
264
+ append-only ledger of separate, frozen, chained records: `proposed`, `rejected`, `promoted` (only through `recordPromotion`, which
265
+ accepts nothing but a result `promotePatch` returned `ok`), `applied` (only after a promotion for that exact diff) and
266
+ `rolled-back`. `verificationRepairFor`/`withRepairStatus` map the ledger onto the CORE-002 `repair` field (`replay-verified` carries the
267
+ patched-negative record id); a rejection leaves it at `proposed` because the foundation enum is not widened here. The ledger is
268
+ returned to the caller and not persisted: persisting it would need an artifact-registry classification.
269
+
270
+ **New adapters.** `replay-idempotency` (a factory receiving a recording stand-in for the effect: a request and a distinct request
271
+ are delivered as a control, then the same request is redelivered, and the verifier counts effects) and `functional-regression`
272
+ (declared cases against expected JSON values; the hypothesis is that behaviour REGRESSED, so a clean patch is `refuted`). Both have
273
+ the usual positive, negative and inconclusive fixtures under `test/fixtures/oracles/`.
274
+
275
+ **`oracles/scenario-classes.js` (X-205).** The supported-class manifest for non-taint defects, surfaced as
276
+ `oracleManifest().nonTaint`: tenant authorization and privileged action (REUSE `authorization-decision`), workflow order (REUSES
277
+ `state-transition`), bounded resource exhaustion (REUSES `parser-resource`) and replay/idempotency (the new adapter). Each class
278
+ lists what the requester must supply, its fixtures (`test/fixtures/non-taint/<class>/{positive,negative,scenario.json}`, application-style
279
+ code), its limitations and the report lines a reader sees; every `runOracle` result additionally carries a `disclosure`
280
+ (prerequisites, platform statement, limitations, class report). Unsupported non-taint classes (concurrent races, CSRF, authentication
281
+ throttling) are named with a reason. `judgeNonTaintHypothesis` is the rule that taint absence is never an input to a refutation:
282
+ without an executed oracle of the class's own adapter with proven preconditions the outcome is `not-run` (or `unsupported`); the hunt
283
+ mapper in `verification/emit.js` uses it. `trustedNegativeDenominator` counts only decided results of an executed oracle of an
284
+ advertised class with proven preconditions and trusted runtime proof, and lists every exclusion with its reason (its runtime consumer is `verificationCoverage` in
285
+ `verification/projection.js`, which the JSON report, the CLI report and the MCP/autopilot surfaces use for the trusted-negative line).
286
+
287
+ Not verified: Linux (the oracle platform statements stay `unverified`); only the declared exploit scenario and functional cases are
288
+ exercised, never the patch's correctness elsewhere.
289
+
290
+ ## One projection for every interface, advisory/gating separation, conformance (X-206, X-207, X-208)
291
+
292
+ Same posture as the sections above: consumed, not wired into the default scan, every output addition additive and present only
293
+ when a verification record is. Suites: `test/verification/{interface-equivalence,advisory-gating,oracle-conformance}.test.js`
294
+ (`npm run test:verification`). Docs: `docs/guides/verification-schema-migration.md`, `docs/guides/verification-oracle-conformance.md`.
295
+
296
+ **`verification/projection.js` (X-206).** `projectVerification(record, { replay })` is the ONE place a record becomes something a
297
+ person or another tool reads: state, scope, oracle, evidence ids, replay prerequisites (derived from the record, plus the typed
298
+ ones a replay attempt reported), a what-was-verified / what-was-not summary, the legacy boolean view, and `text` lines. The JSON
299
+ report (`report/index.js` `normalizeFindings`), the CLI and Markdown reports (`verificationBlock`), MCP `explain_finding`,
300
+ `verify_fix` and `apply_fix`, and the autopilot response (`serializeAutopilotResult`, used by `scripts/autopilot.mjs --json`) all
301
+ call it; none formats its own verification text. The key is **`verificationView`**, never `verification`: a finding's existing
302
+ `verification` field is the producer/verifier separation record (`verification-separation.js`) and a first draft of this work
303
+ collided with it. An invalid record yields `verificationView: null` plus `verificationViewErrors`, never a guessed state. The text
304
+ never says "safe" or "fixed" for a partial result (a lint-style test covers every state, every repair status and the wording the real
305
+ surfaces emit; the fix-verify leg's native status word `fixed` is deliberately not echoed into a record reason). `verificationCoverage`
306
+ wraps `trustedNegativeDenominator`. Previous output schemas are pinned in `test/fixtures/verification-compat/`.
307
+
308
+ **`verification/advisory-state.js`, `verification/hypothesis-promotion.js` (X-207).** Hunt (`discovery/`) is advisory.
309
+ `writeAdvisoryState` is the only way advisory code writes: a closed file-name allowlist (the one hunt memory file), authoritative names
310
+ refused, write to a temp file and rename (a symlink or hard link planted at the target is replaced, never written through), the state
311
+ directory must resolve inside the project root. `discovery/memory.js` `saveMemory` uses it. `normalizeFindings` excludes any hunt
312
+ hypothesis (`parser: DISCOVERY` or a `discovery` object) from every finding list and exit code and records `advisoryExcluded`.
313
+ `promoteHypothesis({ hypothesis, result, policy })` is the only crossing: it verifies (not just checks presence of) a receipt issued by
314
+ `runOracle` (`isIssuedReceipt`), bound to the hypothesis and an exact commit and to a valid record settled `confirmed` at the
315
+ `runtime-confirmed` level, applies an EXPLICIT policy (fail-closed on an unknown key; the receipt rules are mandatory and cannot be
316
+ waived; `minConfidence`/`minAgreement` can only add a requirement), reuses `verification-separation.js` for hunter != verifier, and
317
+ returns a frozen audit record for every decision (`promoted`, `rejected`, `unsupported`, `inconclusive`) with no clock in it.
318
+ `runDiscovery` calls it only when `opts.promote = { policy, verify }` is given. The audit record is returned, not persisted:
319
+ persisting needs an artifact-registry classification and a signing domain, which are not decided here.
320
+
321
+ **`oracles/conformance.js` and `scripts/verification-conformance-check.mjs` (X-208).** One contract over every registered adapter:
322
+ static (class scope, budgets under `BUDGET_CEILINGS`, negative controls with an on-disk fixture, verifier-side evidence logic bound by
323
+ a digest, fixtures and logic pinned in `test/fixtures/oracles/conformance-pins.json`) and execution (state mappings, issued and frozen
324
+ receipts, three tamper attempts, an unavailable prerequisite, cancellation, replay). Wired into `scripts/release-check.mjs`
325
+ (`verification-conformance-gate`, in `RELEASE_GROUPS['benches-b']`) and, static half only, the pre-push gate
326
+ (`verification-conformance-static`). Where the boundary cannot run, execution is reported `not-run`, never passed; `--require-execution`
327
+ makes that a failure. A new adapter must be pinned deliberately (`--update-pins`).
328
+
329
+ ## Frozen evaluation protocol, label custody and layer ablations: `evaluation/` (QA-001, QA-002, QA-003)
330
+
331
+ Offline measurement tooling, consumed by `scripts/evaluation.mjs` (`npm run evaluation -- <cmd>`, `npm run evaluation:synthetic`) and
332
+ `test:evaluation`. Nothing here is wired into `engine.js` or the scan path, and a test pins that no module outside `evaluation/`
333
+ imports it. **It builds machinery, not evidence.** No real adjudicated population, no real sealed set and no independent human
334
+ review exist in this repository, so every real-code gate reads `insufficient-population` or `unmeasured`; the exercised suite
335
+ (`synthetic.js`, fixtures in `test/fixtures/evaluation-synthetic/`) is authored by the tooling's developers and flagged
336
+ `synthetic: true` everywhere, and `gates.js` refuses to count it toward any minimum. Do not quote a score from the synthetic suite
337
+ as engine accuracy.
338
+
339
+ - **`protocol.js` (QA-001).** `freezeProtocol` validates a draft and returns a deep-frozen protocol with `protocolHash` (a digest over
340
+ every semantic field). It pins engine/bundle digest, a clean measurement tree, tool and model versions, dataset licences, per-target
341
+ pre/post commits and tree digests, scope, matching policy, limits, thresholds and split membership. Thresholds are the PRD section 5
342
+ floor (`PREREGISTERED_THRESHOLDS`) and may be stricter, never looser or missing. `amendProtocol` yields a new versioned protocol with a
343
+ field diff and empty `comparableWith`; with ANY result bound to the protocol's hash it refuses (`POST_RESULT_CHANGE`), so a threshold,
344
+ matching rule or denominator cannot move after results are observed. `deriveSuccessorProtocol` is the only way forward and rejects a
345
+ sealed target a result already consumed (`SEALED_REUSE`). A target removed from the population must appear in `retired` with a reason
346
+ and stays in `originalTargetIds`. `assertBoundToProtocol` rejects a run scored under any other hash.
347
+ - **`grouping.js`.** Union-find over pair id, normalised upstream (a fork resolves to its parent), advisory ids, commits and template
348
+ fingerprints; `assignSplits` places whole groups by a salted hash, `splitStraddles` is the check the protocol validator runs.
349
+ - **`labels.js` (QA-002).** Defect labels need a root-cause id, affected commit, language, family, a reviewed location RANGE (a CWE
350
+ alone cannot be a label), evidence, and adjudication with at least two distinct reviewers who are not the proposer, recorded as
351
+ independent, plus non-model evidence. A candidate (advisory-only) label is valid data and never scoreable. Negatives are `safe-real`,
352
+ `patched` (post variant, paired to a defect) and `near-miss`. `matchFinding` matches by location plus family or CWE; CWE-only,
353
+ wrong-file, no-line and out-of-range each fail with a distinct reason. A finding with no line cannot be localised, so it never
354
+ matches (this is why the pattern-only layer scores zero on the synthetic suite: it reports no line, not a defect of the harness).
355
+ This module can validate that a label SAYS it was independently adjudicated, not that it was.
356
+ - **`custody.js` (QA-002.AC03).** The custodian (verifier domain, role `custodian`) is the only writer of the label store; read needs the
357
+ trust-domain `sealed-labels` read grant and the sealed hash to match. `protectedTermsFrom` / `guardPrompt` / `auditWorkspace` /
358
+ `stageWorkspace` keep advisory prose, evidence refs, benchmark names, fixed-source hints, answer-key file names, advisory ids and
359
+ symlinks out of anything the engine or a prompt sees; a leaking tree is quarantined (removed, recorded as an outcome, never scanned).
360
+ These are PATTERN checks over known terms, not proof against a paraphrase. Process-level read denial of the label directory is the
361
+ sandbox boundary's job (`runInBoundary({ labelDirs })`, which reports `blocked` where the host cannot prove the control).
362
+ - **`runner.js`, `scan-child.js` (QA-003).** `runEvaluation` records, per target and variant, status (`completed|timeout|error|unavailable|
363
+ quarantined`), findings, duration, cost (null when unmeasured, never zero), failure reason and input/output hashes. The engine runs in a
364
+ child process (killed by process group on timeout) over a staged copy, with a scrubbed environment. Run identity covers protocol,
365
+ engine, layer, provider, model, settings, cache, budget, seed, replicate, split and every target digest; labels are not part of it.
366
+ A tree that differs from the digest pinned in the protocol is refused. `runReplicates` checks deterministic reproduction and requires
367
+ at least three replicates for the stochastic (model-assisted) layer; `runAblations` runs `deterministic-only`, `deep-taint` and
368
+ `model-assisted` over identical targets and refuses incomparable configurations. `model-assisted` is `unavailable` without
369
+ `AGENTIC_SECURITY_LLM_ENDPOINT`, recorded as such.
370
+ - **`score.js`.** End-to-end scoring: a timeout, error, unavailable or quarantined known positive is a MISS. Conditional-on-completion
371
+ metrics are reported beside it, labelled, never instead of it. False positives are counted only on adjudicated negatives. Findings no
372
+ adjudicated label accounts for go to a review queue and `unscored` (with counts and reasons), never auto-FP. Targets with no
373
+ adjudicated label are `unlabeled-target`.
374
+ - **`gates.js`.** Judges a sealed-split score against the protocol's own thresholds. `pass` needs the population minimums (>=100 real,
375
+ adjudicated, non-synthetic sealed positives and negatives per core language, >=30 positives per family) AND a measured value over the
376
+ threshold; otherwise `insufficient-population` or `unmeasured`. A protocol failing its hash check, or a score from another protocol, is
377
+ `rejected`. The interval-lower-bound gate is `unmeasured` until the interval method is implemented (QA-004), so `overall` cannot be
378
+ `pass` in this build. `accuracy-scorecard.js` relays a gate report in an `evaluationGates` section (`summarizeEvaluationGates`),
379
+ forcing `unmeasured` for a synthetic or unmarked report; `scripts/scorecard.mjs` reads the gate report from the independent bench directory
380
+ when one has been written (none exists today).
381
+
382
+ Tests: `test/evaluation/{protocol,grouping,labels,custody,runner,gates-scorecard}.test.js` (`npm run test:evaluation`).
383
+ ## Executable invariants, approval, scenarios and the business-state oracle: `invariants/` (X-401 to X-404)
384
+
385
+ Same posture as the verification directories above: consumed, not wired into the default scan, every output addition additive, and
386
+ everything off unless the **`invariant-scenarios`** feature (high-risk execution, operator-only, off by default) is enabled; running a
387
+ scenario also needs `verification-oracles`. Suite: `npm run test:invariants` (`test/invariants/`, executed tests skip loudly where the
388
+ trust boundary cannot run). Linux stays `unverified`: nothing here claims a Linux outcome. X-405 to X-408 (shrinking, repair
389
+ verification, coverage and export, the held-out ablation) are described in the section after this one.
390
+
391
+ - **`schema.js` + `expressions.js` (X-401).** The versioned invariant document: scope (an explicit application, entry, factory and
392
+ environment), tenants, actors, resources, allowed transitions, FORBIDDEN outcomes and an oracle binding. Five classes
393
+ (tenant-isolation, privilege-constraint, value-conservation, workflow-order, idempotency). Declarative only: forbidden outcomes are
394
+ expressions from a closed grammar (`EXPRESSIONS`), every string in them matches a conservative charset, so no field can carry code;
395
+ an op outside the grammar is rejected. Rejects ambiguous identities (duplicate actor/resource/transition/store key), dangling
396
+ references, a missing, unregistered or wrong-class oracle binding, and preserves author, revision and review state. The id is a
397
+ hash of the CONTENT (not the review), so an approval binds to exactly what was reviewed and an edit is a different contract.
398
+ `expressions.js` exists so the oracle adapter can validate expressions without importing the schema (the schema imports the
399
+ oracle registry; the registry imports the adapters).
400
+ - **`lifecycle.js` (X-402).** Inferred versus approved. `inferredInvariant` builds a PROPOSED skeleton (inferred, with source
401
+ evidence and an uncertainty, authored by a model or by code); miners reuse it: `specification-mining.js` (`mineInvariantProposals`),
402
+ `business-logic.js` (`mineWorkflowInvariants`), `logic-claims.js` (`invariantFromLogicClaim`) and the discovery lenses
403
+ (`discovery/lenses.js` `invariantProposalFor`, surfaced by `runDiscovery` as `invariantProposals` only when asked AND the feature is
404
+ on). The approval ledger is append-only, hash-chained and HMAC-signed under the per-install key (`integrity.js`): a transition
405
+ records who, why, from, to, the authority it rests on, and the previous record. Only a HUMAN named by an explicit local reviewer
406
+ policy (or verified by `fix/approver-registry.js`) can approve, reject or supersede; a model, code or the proposer cannot, and with
407
+ no policy nothing can be approved. `classifyViolation` calls a settled `confirmed` result an APPROVED violation (may gate) only
408
+ when the ledger verifies and says approved, else a CANDIDATE violation (advisory); a document that merely claims approval is a
409
+ candidate, and a result without a receipt `runOracle` issued is `unverified`. `violationReport` keeps the two groups apart.
410
+ Limits, stated: symmetric HMAC is tamper-evidence for the operator, not third-party non-repudiation; the reviewer identity is a
411
+ claim checked against the operator's list, not authenticated; dropping the LAST record is not detectable; persistence is the
412
+ caller's (the ledger is plain data).
413
+ - **`scenarios.js` (X-403).** `generateScenarios(invariant, { fixture, seed, bounds, config })`: five families (cross-tenant access,
414
+ privilege change, reordered workflow, duplicate request, concurrent operations), each with synthetic tenants and actors, a seeded
415
+ setup, a control sequence, an attack sequence, the forbidden outcomes and a cleanup. Bounded by actors, depth, requests,
416
+ scheduling (a parallel group is at most 4 steps with a SEEDED start order) and time (the budget becomes the run deadline); every
417
+ bound has a hard ceiling and an over-ceiling request is rejected, not clamped. Deterministic given the seed (no clock; ids are
418
+ hashes). Confined to contracts scoped to a `disposable-fixture` and to fixture files the caller supplies; a shared/production
419
+ scope, missing setup or a scenario that cannot be built is `unsupported` with its reason. `runScenario` builds a replay manifest
420
+ (`replay/replay.js`) so the environment, toolchain and oracle logic digest are pinned, then runs it. `run.js` (`verifyInvariant`)
421
+ is the one orchestration: generate, replay each scenario, classify against the ledger, report.
422
+ - **`business-state` oracle (X-404)** in `oracles/adapters.js` (class `business-state`, `requiresFeature: 'invariant-scenarios'`,
423
+ fixtures `test/fixtures/oracles/business-state/`, pinned like every adapter). The harness gives the application an in-memory
424
+ durable store and an effect recorder (`ctx.store`, `ctx.emit`), plays a control sequence and the attack sequence TWICE (a
425
+ disagreement is `inconclusive`, never a verdict), and logs every durable write and emitted effect. `state-assertions.js`
426
+ evaluates the forbidden outcomes VERIFIER-SIDE over that log after the tree is dead: durable writes, leaked content, totals,
427
+ state moves and repeated effects decide; the response status is recorded in the event sequence and never decides, so a handler
428
+ answering 403 after writing another tenant's record is a violation. A forbidden outcome reached by the control flow counts too.
429
+ `refuted` needs the control flow to have visibly worked (durable effect, no error, clean, cleaned up). The result carries a
430
+ bounded `report` (preconditions, sanitized snapshots with secret-looking fields redacted, the event sequence, per-assertion
431
+ results with stable `ievd:` evidence ids); the ids are record evidence and the report digest is bound into the receipt.
432
+ Two small additive changes to shared code: `oracle.js` supports an optional per-adapter `requiresFeature` and optional
433
+ `judged.report`/`judged.evidenceItems`; `conformance.js` enables an adapter's `requiresFeature` for its execution checks.
434
+ - **Limits, not hidden.** The application is a factory returning `(ctx, key, payload)` actions over an in-memory store: durable state
435
+ held in a real database, queue or file system is not observed. Concurrency is cooperative scheduling in one process (interleavings
436
+ at await points), not parallel execution. A clean result covers only the declared bounded scenario. A target that recognises the
437
+ oracle and forges its own log entries is not distinguishable from a real violation (the limit every adapter states).
438
+ - **`deployment-ablation.js` (X-308).** A frozen, SYNTHETIC deployment-context ablation: the same source deployed under an exploitable and a
439
+ non-exploitable boundary configuration (`test/fixtures/deployment-ablation/`, seven cases, fourteen instances, kubernetes and compose
440
+ deployments). `freezeAblationSet` hash-pins each source tree, each deployment tree, the labels and the graph arm's decision rule and binds
441
+ the cases into a QA-001 protocol (`synthetic: true`, never counted toward a real-population gate); `verifyFrozenSet` refuses any drift
442
+ before a measurement. `runPairedAblation` scores a source-only arm (the QA-003 child-process scan) and a graph-enabled arm (the shipped
443
+ `runBoundaries` path plus a frozen demotion rule: demote only on `blocked`, or `none-found` in a graph with no gap) on the same
444
+ instances and reports paired counts (false positives reduced, confirmed defects added, baseline confirmed defects lost, false positives
445
+ added), precision, recall, runtime and coverage (unresolved, stale and unbound reported apart). Uncertainty is a SIMPLE paired case
446
+ bootstrap, labelled as such: the grouped bootstrap of QA-004 is not in this base. `adapterValidation` marks an adapter validated only with
447
+ a positive and a negative fixture and no lost defect; `docs/guides/deployment-aware-support.md` may claim only those (a test compares).
448
+ CLI: `npm run evaluation -- deployment-ablation [--json] [--write-frozen]`. The cases, labels and rule are authored by the tooling's
449
+ developers and one case exists because an earlier rule lost a defect on it, so the set is a mechanism check, not an estimate.
450
+
451
+ Tests: `test/evaluation/{protocol,grouping,labels,custody,runner,gates-scorecard}.test.js` (`npm run test:evaluation`); the X-308 ablation is tested in `test/deployment/differentiation-ablation.test.js` (`npm run test:deployment`).
452
+
453
+ ## Shrinking, repair verification, coverage and the held-out ablation (X-405 to X-408)
454
+
455
+ Same posture as above: consumed, not wired into the default scan, behind `invariant-scenarios`, every output addition additive. Suites:
456
+ `test/invariants/{scenario-shrinking,invariant-repair,business-coverage,invariant-ablation}.test.js`. Guide: `docs/guides/business-logic-invariants.md`.
457
+
458
+ - **`invariants/shrink.js` (X-405).** `shrinkScenario(scenario, { fixture, commit, config, budget })`: a greedy removal search (an attack step,
459
+ one member of a parallel group, a control step, a seed record), each candidate replayed through `runScenario` (so manifest, boundary,
460
+ receipt), kept only if it settles `confirmed` with every precondition held AND the same authoritative failing assertion (the first
461
+ violated outcome in the contract's order; identified by forbidden-outcome id and phase). Units a surviving step addresses, or that
462
+ carry an asserted marker, are guarded (recorded with a reason, never silently skipped). Budget: `maxRuns` and `timeBudgetMs`, each with
463
+ a hard ceiling and rejected, not clamped, above it; `minimal: true` only when a full pass kept nothing more. One confirming replay
464
+ sits outside the budget. `reproductionSignals` is a PATTERN check over the fixture text for clocks, randomness, timers, network and
465
+ package imports; `reproducibility.blockedBy` names concurrency (cooperative scheduling, two fresh runs disagreed), external
466
+ dependencies and prerequisites. `checkMinimization` re-derives from the two scenarios that the minimized one is a removal-subset
467
+ (invariant, fixture, actors, forbidden outcomes untouched; no step added; no sequence emptied; no referenced record dropped);
468
+ `verifyShrinkRecord` recomputes the digest, id and receipt links. `scenarios.js` gained `deriveScenario` (same pinned scenario, new
469
+ inputs, recomputed limits and id) and `exerciseOf`; `run.js` gained an optional `shrink` input and an `exercise` field per result.
470
+ - **`invariants/repair.js` (X-406).** `verifyInvariantRepair` composes `verifyPatchNegative` (business-state as the declared behaviour
471
+ check: `req.functional.oracleId` and `promotePatch`'s `functionalOracle` are the two small additive changes in `verification/`) with
472
+ preservation legs for every other APPROVED contract on the fixture (baseline and patched, a pre-existing violation is not blamed on
473
+ the patch). Needs the violated contract approved in a verifying ledger or nothing runs. Blockers carry codes from `REASON_CODES`;
474
+ `reasonCodeFor` maps every patch-negative failure code and sends an unknown one to `setup-failed`. A preservation failure after a
475
+ promotion appends a `rejected` repair record. `patch-negative.js` now also counts a failed business-state control flow
476
+ (`observed.preconditions`) as a broken revision. A verified repair builds a regression artifact (exact patch, invariant document and
477
+ approval evidence, scenario, one pinned manifest per leg with its expected outcome, secret-checked); `runRegressionArtifact`
478
+ re-executes it from the artifact alone, `validateRegressionArtifact` detects an edit without running. Receipts do not cross a
479
+ process; the artifact crosses by re-execution. The approval evidence in it is a digest and a transition id: checking the HMAC needs
480
+ the operator's key.
481
+ - **`invariants/coverage.js`, `export.js`, `project-input.js`, `cli.js` (X-407).** `businessCoverage` reports inventory by the VERIFIED
482
+ ledger, exercised actors/states/transitions from executed scenarios only, and gaps (`GAP_CODES`) that running more of the same cannot
483
+ remove; it always states it is a bounded sample. `invariantCoverageFields` is the one place a surface asks for the additive
484
+ `invariantCoverage` field (`report/index.js` JSON and CLI); flag off or nothing attached returns `{}`, pinned by
485
+ `test/fixtures/invariant-compat/`. `exportScenarios` builds the reproducible package (no fixture source; a scenario holding
486
+ secret-looking content is withheld by path; claims `exhaustive: false`); `verifyScenarioExport` detects edits. CLI:
487
+ `agentic-security invariants export|coverage|regress`; MCP: `invariant_scenario_export` (`mcp/invariant-tools.js`, read-only).
488
+ - **`evaluation/invariant-ablation.js` (X-408).** Three arms (source-only engine scan, inferred contract, approved contract) over a frozen
489
+ synthetic benchmark (`test/fixtures/invariant-benchmark/`, `manifest.json` + `pin.json`, loader fails closed), one denominator and one
490
+ budget, flags produced before any label is read, Wilson intervals, paired counts, unique findings, scenario cost, and per-class
491
+ claims that are `supported` only from executed evidence meeting the registered policy (nothing executed is `unmeasured`). Driver:
492
+ `npm run bench:invariant-ablation -- run|verify|pin` (`scripts/invariant-ablation.mjs`). SYNTHETIC: authored and labelled by the
493
+ developers, no independent adjudication; a figure from it is not engine accuracy and not a real-world benefit claim.
494
+ - **Limits, not hidden.** Same as the section above (in-memory store, cooperative concurrency, only declared outcomes). A shrunk
495
+ scenario is a smaller reproduction, not a root cause. A verified repair covers the declared scenario, workflows and the other
496
+ contracts' bounded scenarios only. Linux is not claimed anywhere here.
497
+ - **`interval.js`, `report.js` (QA-004).** `groupedBootstrap` is the registered `grouped-bootstrap-95` method: percentile bootstrap over GROUPS
498
+ (grouping.js), 2000 resamples, seed = first 32 bits of sha256(protocolHash + statistic name), `unmeasured` with the reason below 10 independent
499
+ groups. `wilsonInterval` covers the completion rate. `buildAccuracyReport` publishes raw counts first, then P/R/F1 by language and family,
500
+ micro and macro, completion with an interval, and every disclosed gap (unscored labels and findings, negatives whose scan failed). A stratum below
501
+ the protocol's floor is `unmeasured` with raw counts and a separate `descriptive` figure. `score.js` now carries `perTarget` tallies (what the
502
+ bootstrap resamples) and scores repeated alerts of one flaw as ONE false positive (`clusterAlerts`, `alertsCollapsed`); `alertBurden` reports the
503
+ raw alerts a reviewer reads beside the root causes, so deduplication never hides burden. `checkPopulationsDisjoint` refuses a target in two of
504
+ regression, development, sealed, operational.
505
+ - **`economics.js` (QA-004.AC03).** `accountCosts` keeps model, tool, cache (USD) and human-review (MINUTES) apart, treats an entry with no amount as
506
+ unmeasured (never zero; the ratios become lower bounds), and divides ALL machine spend, failed and duplicate attempts included, by DISTINCT confirmed
507
+ root causes and validated fixes. A confirmed outcome must name its root cause so a duplicate cannot be counted twice.
508
+ - **`why-missed.js` (QA-005).** `classifyMiss` names the earliest failed stage for a labelled DEVELOPMENT miss (scan-execution, parser-ir,
509
+ source-modelling, propagation, sink-modelling, filters, suppression, deduplication, attribution) or `unknown` with a reason. A candidate observed
510
+ downstream (a reported finding, a dedupe loser, a ledger drop) is checked first because it proves the earlier stages worked. Evidence comes from
511
+ `scan-child.js` in `diagnose` mode (the suppression ledger incl. guard-recognized drops with finding id, CWE, family and the dominating guard line;
512
+ dedupe and rejected-guard stage evidence behind `AGENTIC_SECURITY_STAGE_EVIDENCE=1`, diagnostic only; sources, sinks, IR lowering).
513
+ `collectDiagnostics` refuses any sealed target. `compareMisses` gives before and after stage counts, the recovered defects with the stage that lost
514
+ each, and names new false positives instead of netting them. `dev-cases.js` builds the development case suite (`test/fixtures/engine-mechanisms/`,
515
+ every label flagged synthetic) that `scripts/dev-recovery.mjs` runs against ANY engine checkout, so a before and after is a measurement.
516
+ - **`release-gate.js` (QA-007).** `evaluateReleaseGates` wraps `gates.js` (which now computes the per-language F1 lower bound from the grouped bootstrap
517
+ and adds the integrity gates `answer-key-leaks` and `denominator-intact`, and `no-regression-vs-baseline`) and decides whether a NEW accuracy claim is
518
+ allowed. The baseline comes only from a verified ledger: `appendSealedEvaluation` / `verifyLedger` (hash chain) / `signLedger` (the custodian signs the
519
+ head, so a rebuilt chain does not verify). `assessReleaseClaim` blocks a different detector digest (`detectorDigestOf`, evaluation tooling excluded) on
520
+ already-consumed sealed targets (`fresh-sealed-population-required`); old results are retained as evaluation-only. `signGateReport` signs an AGGREGATE
521
+ verdict (no per-case outcome) with a local Ed25519 key, trust basis `self-issued-local-key`, never independent certification.
522
+ - **`bakeoff.js` (QA-008.AC02).** The bake-off manifest, generic comparator slots (no tool is named anywhere), adapters that mark an unavailable slot
523
+ `not-evaluated` with a typed reason, a comparability check (identical workload digest and verification standard) and a claim guard: accuracy and
524
+ cost claims only between evaluated, comparable participants; `superiority` is always `none claimed`. Scoring is `comparison.js`, unchanged.
525
+ Docs: `docs/guides/bakeoff-recipe.md`; the offline reproduction is `scripts/public-reproduction.mjs` (`npm run reproduce:mini`).
526
+
527
+ Tests: `test/evaluation/{protocol,grouping,labels,custody,runner,gates-scorecard,reporting,why-missed,guard-dominance,engine-mechanisms,release-gate,publication}.test.js` (`npm run test:evaluation`; the two engine files are also in `test:dataflow`). Guides: `docs/guides/{evaluation-reporting,miniature-reproduction,bakeoff-recipe,engine-mechanism-evidence}.md`.
528
+
529
+ ## Release assurance, portable evidence and resumable portfolios: `portfolio/` (X-701 to X-708)
530
+
531
+ Same posture as the sections above: consumed, not wired into the default scan. The one place it touches shipped output is the verdict
532
+ wording, and that is additive behind the `portfolio-assurance` feature (`assurance/config.js`, default off, so flag-off output is
533
+ byte-identical and no pinned fixture changed). **It builds machinery, not evidence**: no real release or portfolio exists, every
534
+ fixture is flagged `synthetic: true`, and nothing here claims real-world assurance. Linux enforcement stays `unverified`; no Linux
535
+ outcome is asserted. Suite: `npm run test:portfolio` (`test/portfolio/`).
536
+
537
+ - **`manifest.js` (X-701).** `buildManifest` / `validateManifest`: the release assurance manifest on the assurance schema kit and
538
+ `identity.js` (id prefix `ram`, allowlisted fields, no clock). Binds subject repository and commit, dependency revisions, build
539
+ artifact digests, scope, graph snapshot digest, invariant versions and verification receipts. Every mandatory check is in exactly one
540
+ of completed / incomplete / unsupported / waived (incomplete and unsupported name their gaps, a waiver names its approver and reason),
541
+ and the blocking policy (id, digest, severity) is part of the record. Validation rejects evidence not bound to a declared receipt,
542
+ a receipt whose commit is not the bound commit of its repository, a mandatory check listed nowhere, and coverage or `complete` that
543
+ disagrees with the lists. Residual risks and coverage are machine-readable fields.
544
+ - **`bundle.js` (X-702).** The portable bundle: an index file (bundle.json) plus `blobs/<sha256 hex>`. `exportBundle` writes the sanitized
545
+ findings (closed field set, snippets stay behind, secret shapes redacted, then refused if any survive), provenance, replay manifests,
546
+ toolchain identities and the cited receipts, index last. `verifyBundle` is the OFFLINE verifier (fs and crypto only, executes nothing
547
+ from the bundle): re-hashes every blob, rejects symlinks, oversize, bad names, missing roles, a modified index, a manifest that fails
548
+ validation and a cited receipt that is absent, and DISCLOSES replay prerequisites without attempting a replay. `importBundle`
549
+ verifies first, confines every destination, never overwrites. Limits are `BUNDLE_LIMITS`.
550
+ - **`signing.js` (X-703).** `signAssuranceClaim` is a signer-domain act (`sandbox/trust-domains.js`, default deny: worker, target and
551
+ verifier are refused before a key is touched). The key lives in its own `assurance-signing/` directory (reusing `ensureKeyPair`) and may
552
+ be required to differ from the scan evidence key. The signed claim names manifest id and digest, bundle digest, commit, blocking policy,
553
+ coverage, the bounded headline, signer id, key id and the assurance policy. `verifyAssuranceClaim` is offline against an explicit
554
+ trust-root policy (`buildTrustPolicy`): unknown root, revoked root, a basis this build cannot verify, a tampered claim, a tampered or
555
+ missing bundle and a claim that does not match the bundle's manifest are each typed failures. The only verifiable basis is
556
+ `self-issued-local-key`, `independentlyCertified` is always false and inside the signed bytes. Private key material is never in an
557
+ envelope, policy, error or output.
558
+ - **`wording.js` (X-703.AC03).** `assuranceStatement` renders "No blocking findings in completed supported checks", then scope, then
559
+ gaps, then the not-a-guarantee line. `report/index.js` `toShipVerdict` and `integrations/index.js` digests use it only when
560
+ `portfolioAssuranceEnabled` (option, else the assurance configuration); otherwise the old "Safe to deploy" strings are untouched.
561
+ - **`work-units.js` (X-704).** `planPortfolio` (authorized repositories only, exact commits, stable `wu:` ids), a seven-state machine with
562
+ lease expiry, retry count and a hash-chained attempt record per unit (rewriting an attempt fails `verifyStore`), idempotent lease /
563
+ start / complete / fail by attempt id (a duplicate completion verifies once; a stale attempt, such as a zombie after lease expiry,
564
+ is refused), `progressOf` counting only verified units, locked atomic persistence (`openStore`, `mutateStore`; a corrupt store is an
565
+ error, never a reset) and `runPortfolio` (bounded concurrency, an executor that throws fails its unit). `fleet.js` `runFleet` accepts
566
+ `portfolioVerified` (from `fullyVerifiedRepositories`) and skips those repositories like a resumed completed one.
567
+ - **`resume.js` (X-705).** Every verified result records one digest per dimension (code, policy, graph, invariant, oracle, toolchain;
568
+ `scan-checkpoint.js` `computeRunKey` / `computeGlobalKey` supply the code and toolchain digests). `planResume` reuses a unit only on a
569
+ full match; any other change `applyResume` invalidates: the old result moves to `stale` (inspectable with `inspectStale`), the unit
570
+ returns to pending in a new generation, `assessClaims` marks dependent claims stale. A graph-only change is narrowed by the drift
571
+ module (`lineage/deployment/drift.js` `planRescan`): a unit bound to hypotheses is reused only if the plan is complete and all its
572
+ hypotheses are in `skipped`; anything else fails closed.
573
+
574
+ Tests: `test/portfolio/{manifest,bundle,signing,work-units,resume}.test.js` (`npm run test:portfolio`).
575
+ ## Calibrated model routing: `routing/` (X-601 to X-608)
576
+ - **`scheduler.js` (X-706).** Bounded scheduling over the store. `scheduleNext` admits and leases ONE unit in a single locked transaction
577
+ (`mutateStore`), after checking five limits at two levels (portfolio and per repository: wall time, provider spend, requests, storage,
578
+ plus concurrency) against what is used PLUS what in-flight attempts have reserved PLUS the unit's own estimate. A unit with no
579
+ estimate is not leased (unknown is never zero). A refusal is `at-capacity` (a wait) or `exhausted` (will not recover), reported per
580
+ unit and per scope, and a refusal changes nothing. An attempt that ends with no usage report is charged its whole reservation; a
581
+ reported overrun is recorded (`overruns`) and shrinks what remains. Wall time is also enforced while a unit runs (the executor's
582
+ `signal` aborts at the unit's wall estimate). Fairness: priority is a WEIGHT (integer 1..8, default 1), never an order; the next lease
583
+ goes to the admissible repository with the lowest `leasesGranted / weight` (ties: repository name, then unit id), so a 120-unit
584
+ repository cannot starve a 1-unit one, weight 8 against 1 is served about eight times as often with the light one still served early,
585
+ and a repository that was blocked is owed service when it returns. A blocked or over-budget repository is skipped with a reason, never
586
+ waited on; an unaffordable unit does not block a cheaper sibling. `routingBudgetFor` hands the routing policy (`routing/decide.js`) the
587
+ smaller of the portfolio and repository dollar remainders, less reservations; `chargeModelSpend` charges a routing decision at its
588
+ upper cost bound and refuses a blocked or unbounded decision. No provider is called here. `createCancelScope().cancel` takes a scope
589
+ (`{unitId}`, `{repository}` or `{all}`; an empty scope is refused, never read as cancel-all), records it in the ledger so a process
590
+ that did not receive the call still honours it, aborts in-process attempts (work started with `ctx.spawn` runs under
591
+ `sandbox/supervise.js`, which ends the whole process tree and reports survivors), releases a lease ONLY when the attempt settled with no
592
+ survivor, otherwise leaves it to expire (so no other worker can start the unit while a descendant may live) and never re-leases it.
593
+ Verified units and receipts are untouched; the report is `incomplete` with the reason. `heartbeat` renews the lease and records a
594
+ heartbeat in one transaction; `runScheduled` heartbeats every `HEARTBEAT.intervalMs` (5 s) while an attempt runs and takes an optional
595
+ `backend`, probed before every lease (an unavailable one stops the run as `blocked`). Limits: the driver's heartbeat proves the
596
+ controlling process is alive, not that an in-process executor is making progress (the wall bound handles that); tree termination is
597
+ verified on the macOS userspace backend only and Linux enforcement stays `unverified`.
598
+ - **`progress.js` (X-707).** ONE projection (`buildProgressView`) for the CLI, the MCP tool and the fleet summary, in the pattern of
599
+ `lineage/deployment/projection.js`. It keeps apart verified units (the only progress), blocked / failed / canceled / stale units with
600
+ reasons, per-repository coverage (fully-verified, partial, incomplete, not-started), per-limit budget (limit, used, reserved,
601
+ remaining; an unenforced limit says so), pending human review (blocked units plus `reviewItemsFromInvariantCoverage` and
602
+ `reviewItemsFromBoundaryContexts`), and every worker with its age, `live` or `stale` (stale after `HEARTBEAT.staleAfterMs`, 15 s;
603
+ silent workers are measured from their lease). `aggregateFindings` deduplicates by `stableId` ONLY, keeps every occurrence
604
+ (repository, environment, release, commit, file, line, severity) and lists affected releases separately; a finding with no stable id
605
+ is listed apart, never merged. `completion` states that a finished controller is not every unit verified and not any repository
606
+ passing (`passAssessment: 'not-implied'`). Additive behind `portfolio-assurance`: `attachPortfolioProgress` returns the SAME rollup
607
+ object when the feature is off, `renderFleetSummary` adds a PORTFOLIO clause only when the field is present, and
608
+ `test/fixtures/portfolio/pre-change-pins.json` (generated by `test/portfolio/make-pins.mjs` from code before the change) pins the
609
+ fleet rollup, summary and HTML. CLI `agentic-security portfolio progress`; MCP `portfolio_progress` (read-only, `mcp/portfolio-tools.js`).
610
+ - **`backend.js` (X-708).** The storage backend interface (`probe`, `storeFile`, `withExclusive`, fenced `acquireLease` / `renewLease` /
611
+ `releaseLease`) and a file-lock reference implementation. `createLocalBackend` makes its directory; `createSharedBackend` requires a
612
+ marker written by an explicit `initSharedBackend`, so an unmounted path, an empty mount point, a symlink or a damaged marker is a typed
613
+ `blocked` (`openBackend` returns `{ ok: false, state: 'blocked', code }` and no backend; there is deliberately no fallback argument,
614
+ and a blocked open creates nothing). A backend that disappears mid-run stops `runScheduled` as `blocked` without recreating the
615
+ directory or writing anywhere else. Lease semantics, each tested with two REAL processes: mutual exclusion, monotonic fence, expiry
616
+ takeover, a replaced holder refused on renew and release. NOT claimed: network-filesystem safety (`describe()` reports it
617
+ `unverified`; the operator must verify exclusive-create atomicity on that mount), cross-host clock agreement, or race-free stale-lock
618
+ recovery (narrow window, documented). `exportState` / `verifyStateExport` / `importState`: one self-checking, secret-checked file for an
619
+ air-gapped machine; import verifies first and never overwrites.
620
+ - **`retention.js` (X-708.AC01).** Retention by class (`replay-evidence`, `metadata`, `model-trace`, `secret`; defaults 365 / 730 / 30 / 0
621
+ days, ceilings 1095 / 1825 / 90 / 7, an over-ceiling setting is clamped and disclosed). Order: a required current receipt (named in
622
+ `currentReceiptIds`, or `requiredBy` a unit that is `verified` in the store NOW) is never deleted and an expired one is reported as
623
+ `expiredButRequired`; then a legal hold (validated and checked with `legal-hold.js`'s `isUnderHold`; by id, class or repository; a
624
+ malformed hold refuses the whole plan); then expiry. An unclassified or malformed record is kept and reported. `applyRetention` re-plans
625
+ against the live store, confines paths to the root (traversal, absolute paths, symlinks and directories refused), and logs each deletion
626
+ to a hash-chained log BEFORE removing the file (no log, no deletion; a broken log blocks every deletion). The log holds digests and sizes,
627
+ never content. CLI `agentic-security portfolio retention plan|apply|verify-log`.
628
+
629
+ Tests: `test/portfolio/{manifest,bundle,signing,work-units,resume,scheduler,progress,retention,backend}.test.js` (`npm run test:portfolio`).
630
+ `test/portfolio/proc-worker.mjs` is the child process the multi-process consistency tests spawn; it is not a test file.
631
+ ## Calibrated model routing: `routing/` (X-601 to X-604)
632
+
633
+ Extends `model-routing.js`, `model-trust.js`, the provider catalog and cache economics; consumed, not wired into the scan or the interactive advisor. Everything is behind the **`model-routing`** feature (`assurance/config.js`, model-network risk, off by default, kill switch `AGENTIC_SECURITY_NO_MODEL_ROUTING`). With it off, `routeModelWithPolicy` returns exactly what `routeModelWithTrust` returns, and `test/routing/unchanged-when-off.test.js` recomputes the routing, trust, cache-economics, catalog and advisor outputs against `test/fixtures/routing/pre-change-pins.json` (generated from the code before any routing edit). Suite: `npm run test:routing`. **Machinery, not evidence:** no real routing outcome or adjudicated task exists in this repository, every population in the tests is SYNTHETIC (`test/helpers/routing-fixtures.js`), and the section 5 routing gate reads `unmeasured` (synthetic) or `insufficient-population`, never `pass`. No cost or quality improvement is claimed anywhere.
634
+
635
+ - **`outcomes.js` (X-601).** Routing task record (kind: discovery, triage, verification-planning, repair; language; vulnerability class; context tokens and bucket; required capabilities; data class), the stratum key `kind|language|class|bucket` and its coarser parents, and the outcome record. `deriveRoutingLabel` is the only way to a decided label: independent adjudication (a human or verifier service that is not the model or its provider, with the adjudication version) or trusted execution (a `vrec:` verification record). Provider agreement, self-reported confidence, accepted suggestions, model judgement and majority vote are refused (`NOT_A_TRUTH_LABEL`) and the outcome is recorded `unknown`. Outcome records keep every status (failed, timeout, cancelled, provider-error, partial, blocked) and unknown or delayed correctness; an unreported count is `null`, never 0. `createOutcomeLedger` has no remove or overwrite.
636
+ - **`economics.js` (X-602).** `priceBookFromCatalog` builds the versioned price book from `provider-catalog.js` (the catalog's dated snapshot stays the single price table). `costOf` splits input, cached input, output, retries and tool execution, and names price version, currency and billing basis. Unknown is `null` or a bounded `[lower, upper]` estimate, never 0 (an unpriced model, a subscription basis, an unreported token count, an unknown cache split, an unmeasured tool cost). A prediction goes in `predicted` and never fills `measured`. `cache-economics.js` (which prices Claude Code's own transcript) and `hooks/model-cost-advisor.js` are deliberately unchanged and pinned.
637
+ - **`adapter.js` (X-602.AC03).** `createProviderAdapter(...).invoke` has no HTTP client: it goes through `guardedModelCall` (feature gate, egress policy before the prompt exists, redaction, request cap, deadline, bounded retries, audit chain) with an INJECTED transport. Telemetry is built from an allowlist (provider, model, endpoint host only, status, a fixed failure code, token counts, cache state, latency, byte counts); it never carries the prompt, the response, an error message or a credential. Provider failures, partial responses, cancellation, timeouts and cache hits each yield an outcome record.
638
+ - **`calibration.js` (X-603).** `buildCalibration` fits per (model, version, stratum) quality on HELD-OUT outcomes with a time split, a model-version split (versions are never pooled) and group disjointness (a group on both sides is removed from the held-out side and counted). Uncertainty is `groupedBootstrap` from `evaluation/interval.js` (`unmeasured` below 10 groups). The label `reliable` needs at least 200 paired adjudicated tasks overall, 30 per stratum (`ROUTING_MINIMUMS`, a config cannot lower them), a measured interval and a stable development-versus-held-out rate; a synthetic population only ever gets `reliable-synthetic`. Otherwise the stratum carries `fallback-parent` (a reliable coarser stratum, named), `shifted` or `insufficient-evidence`, and an explicit fallback (`parent-stratum` or `baseline-model`). The artifact records the dataset hash, config hash, adjudication versions, counts and every exclusion by reason, and `verifyCalibration` rebuilds it byte for byte. `routingPopulationGate` is the population half of the PRD section 5 routing row only; the paired quality, cost and latency comparison is `promotion.js` (X-605).
639
+ - **`decide.js` (X-604).** `routeConstrained` applies availability, capability, privacy (the egress policy, or a capability-manifest egress check), context window, reliable and fresh quality evidence whose LOWER bound meets the minimum, interval width and budget BEFORE minimising measured cost or latency. Every candidate lists all of its failing constraints. When nothing is acceptable the result is `fallback` (the existing capability route, only if it passes capability, privacy, context and budget, with quality marked UNVERIFIED) or `blocked` with a code; a constraint is never relaxed to find a route. `routeModelWithPolicy` in `model-routing.js` is the entry point; a blocked decision returns `model: null, blocked: true`.
640
+ - **Limits, not hidden.** The provider catalog carries prices and cache models but not capabilities or context windows, so `candidateFromCatalog` takes them from the operator and an unknown window is rejected. Catalog prices are a dated snapshot. Cost bounds cover model tokens only. Telemetry redaction depends on the egress redaction patterns, which are pattern checks, not proof against every secret shape.
641
+ - **`shadow.js` (X-605.AC01).** `createShadowRecorder().shadow()` records what `routeConstrained` WOULD pick and returns the production route it was given, the same object. It holds no transport or adapter (a test pins that the source cannot call a provider), so no paid call is possible by construction. A disabled routing control records nothing.
642
+ - **`promotion.js` (X-605.AC02, AC03).** `freezeTaskSet` fixes the evaluation tasks by hash; `replayPaired` pairs each frozen task's baseline arm with the arm the shadow decision chose, from recorded outcomes. It issues a provider call only with BOTH an injected `invoke` and a bounded authorization (`authorizedBy`, finite `maxCalls`, `maxUsd`, `perCallUsdCeiling`, all under `REPLAY_LIMITS`); an `invoke` without them is refused (`UNBOUNDED_REPLAY`), an unknown cost is charged at the ceiling, and a replay that hits its bound is `truncated`. `evaluatePromotion` judges the PRD section 5 routing row (200 paired adjudicated overall, 30 per promoted stratum, quality difference lower bound above -0.02, median cost 20% lower or quality +0.05 at no higher median cost, p95 latency 1.2x) with the grouped bootstrap. Verdict precedence: `invalid` > `unmeasured` (synthetic) > `insufficient-population` > `fail` > `pass`. Invalidations: `cherry-picked` (pairs differ from the frozen set), `dropped-tasks` (no pair: no shadow record, missing or ambiguous outcome, bound reached), `unbounded-replay`, `truncated-replay`, `replay-edited`, `frozen-set-edited`. A failure status, a timeout, a blocked proposal or an unserved task is not-correct in the comparison and is reported, never dropped; an unadjudicated pair is counted apart and never imputed; an unknown cost or latency leaves the criterion unmet. The quality-gain path is stricter than the PRD wording (the lower bound must clear zero too).
643
+ - **`drift.js`, `canary.js` (X-606).** `snapshotRoutingBasis` records what a policy was promoted on; `assessDrift` compares it with the present (model version, price entry, schema, recent decided quality) and returns a documented `fallback` or `shadow` state; `applyInvalidations` derives a calibration whose affected estimates are `invalidated` (quality, version, schema) or lose their measured cost (pricing), so `routeConstrained` refuses them. A cost-invalidated model ranks behind one with a valid measured cost, by the existing ranking rule. `createCanary` is finite by construction (no limits, no canary): task count, spend budget, error rate, consecutive-error outage, incorrect rate; a trip restores the known-good policy and only appends receipts. `activate` needs a `pass` promotion verdict. `routeUnderDrift` is the state-aware entry point.
644
+ - **`feedback.js` (X-607).** `createFeedbackStore`: actors (human or verifier-service) registered with a key sign each record (HMAC); the first accepted label is the immutable original, a change is a correction citing the entry it corrects; an allowlist of fields is kept and the names of dropped fields only; unauthorized, unverifiable, tampered, duplicate and poisoned feedback is quarantined with a count and reason, and `applyToOutcomes` is what a calibration should be built from. `authorizePayload` / `sendToProvider` need the task policy AND the egress policy (or a capability manifest check) to allow source or evidence, and send only through the guarded adapter. Limit: a trusted-execution label cannot be recomputed from the outcome alone, so a flip of one is not detected here.
645
+ - **`receipts.js`, `control.js`, `report.js` (X-608).** `createReceiptLog` is a hash-linked log of every shadow decision, replay, promotion, drift, canary, rollback and feedback decision (`exportReceipts`, `verifyReceiptChain`; tamper evidence, not a signature). `resolveRoutingControl` (option, then `AGENTIC_SECURITY_ROUTING`, then default; a bad value fails closed to `disabled`) is honoured by `routeModelWithPolicy` (disabled keeps the capability route, `pin:<model>` selects that model). `buildRoutingReplayReport` / `renderRoutingReport` / `buildPolicyCard` restate the verdict with exact denominators, intervals, measured cost, cache states and latency percentiles, restrict claims to the strata and model versions the gate passed, and keep unsupported strata and unmet gates listed. Reproduce offline with `npm run reproduce:routing` (`scripts/routing-replay.mjs`, controls, `--fault`); guide `docs/guides/routing-replay.md`. Everything in the tests and the script is generated and `synthetic`; the gate reads `unmeasured`, never `pass`, on it, and no advantage is claimed.
646
+
142
647
  ## Execution-proof tiers (R2)
143
648
 
144
649
  `proof-tier.js` + `execution-proof.js` add a fourth axis to a finding's