@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
@@ -0,0 +1,226 @@
1
+ // Patch-negative verification (X-204).
2
+ //
3
+ // A re-scan that no longer fires proves the DETECTOR went quiet; a cosmetic edit can do that. A repair is `verified-fix` here
4
+ // only when three things are shown, each by an executed oracle run in an equivalent disposable environment:
5
+ //
6
+ // 1. original-positive the original revision reproduces the exploit (oracle outcome: confirmed)
7
+ // 2. patched-negative the patched revision no longer does, same oracle (oracle outcome: refuted)
8
+ // 3. functional-regression declared legitimate behaviour is intact on the patched revision, with the same expectations
9
+ // holding on the original first (a baseline), so a wrong expectation is reported rather than blamed on the patch.
10
+ //
11
+ // All four runs (the baseline is one) go through `replayManifest`, so each is pinned by commit, fixture hash, patch hash,
12
+ // toolchain, oracle version and inputs, executes inside the trust boundary, and issues a verifier receipt. The result is then
13
+ // handed to `promotePatch`, which re-derives the diff and revision from the proposal and refuses any receipt that does not
14
+ // match them. `verified-fix` needs the promotion; nothing else grants it.
15
+ //
16
+ // A failure never degrades to a vague "not verified": it names the SPECIFIC step that is incomplete and a typed failure code:
17
+ // original-not-reproduced the original did not reproduce under the oracle (nothing to fix, or the scenario is wrong)
18
+ // patched-still-exploitable the exploit still works on the patched revision
19
+ // patched-build-broken the patched revision did not load or its benign control failed
20
+ // patched-negative-inconclusive the patched run decided nothing
21
+ // functional-omitted no functional check was supplied
22
+ // functional-baseline-invalid the declared expectations do not hold on the ORIGINAL revision
23
+ // functional-regression-detected the patched revision behaves differently from the declared expectations
24
+ // oracle-changed the oracle, its version or its inputs differ between the original and patched runs
25
+ // revision-unbound no exact commit: a verdict cannot be tied to a revision
26
+ // prerequisite-unmet a toolchain, feature or isolation prerequisite is missing; resumable where stated
27
+ // environment-mismatch the runs were not in equivalent environments
28
+ // promotion-refused the receipts do not match the exact proposed diff and revision
29
+ //
30
+ // The behaviour check is the functional-regression oracle by default. A requester may name `business-state` as `functional.oracleId`
31
+ // (X-406, `invariants/repair.js`): the same two runs and the same expectation, with an authorized-workflow scenario as the declared
32
+ // case, and `promotePatch` then requires the functional receipts to come from that oracle class instead.
33
+ //
34
+ // Scope, stated so no caller overclaims: the exploit scenario and the functional cases are the REQUESTER's; this verifies the
35
+ // declared scenario and cases, not that the patch is correct elsewhere. Linux is not claimed (the oracle platform statements
36
+ // stay `unverified`). Receipts are valid inside this process only (see patch-promotion.js).
37
+ import { digestOf } from '../assurance/identity.js';
38
+ import { isCommit, isPlainObject } from '../assurance/schema-kit.js';
39
+ import { resolveAssuranceConfig, featureStatus } from '../assurance/config.js';
40
+ import { createReplayManifest, replayManifest } from '../replay/replay.js';
41
+ import * as registry from '../oracles/registry.js';
42
+ import { promotePatch } from './patch-promotion.js';
43
+ import { appendRepairRecord, recordPromotion, verificationRepairFor, withRepairStatus } from './repair-records.js';
44
+
45
+ const PATCH_NEGATIVE_FEATURE = 'patch-negative-verification';
46
+ const FUNCTIONAL_ORACLE = 'functional-regression';
47
+ // The check that behaviour is preserved is normally the functional-regression oracle. A requester may name `business-state`
48
+ // instead (X-406): the same two runs, the same expectation (refuted), with an authorized-workflow scenario as the declared case.
49
+ const FUNCTIONAL_ORACLES = Object.freeze([FUNCTIONAL_ORACLE, 'business-state']);
50
+
51
+ const STEPS = Object.freeze(['original-positive', 'patched-negative', 'functional-baseline', 'functional-regression']);
52
+
53
+ const ROLE_OF_STEP = Object.freeze({ 'original-positive': 'original', 'patched-negative': 'patched', 'functional-baseline': 'functionalBaseline', 'functional-regression': 'functional' });
54
+ const EXPECTED = Object.freeze({ 'original-positive': 'confirmed', 'patched-negative': 'refuted', 'functional-baseline': 'refuted', 'functional-regression': 'refuted' });
55
+
56
+ const stepResult = (step, status, extra = {}) => ({ step, status, expected: EXPECTED[step], outcome: null, code: null, reason: null, recordId: null, receiptDigest: null, ...extra });
57
+
58
+ function replayCode(run) {
59
+ const first = (run.errors || [])[0];
60
+ if (!first) return 'manifest-rejected';
61
+ if (first.code === 'ORACLE_MISMATCH') return 'oracle-changed';
62
+ if (first.code === 'HASH_MISMATCH') return 'content-hash-mismatch';
63
+ return 'manifest-rejected';
64
+ }
65
+
66
+ function classify(step, run) {
67
+ if (run.status === 'rejected') return { status: 'incomplete', code: replayCode(run), reason: (run.errors || []).map((e) => e.message).join('; ').slice(0, 300) || 'the replay manifest was rejected' };
68
+ if (run.status === 'awaiting-prerequisites') {
69
+ const resumable = (run.prerequisites || []).every((p) => p.resumable);
70
+ return { status: 'incomplete', code: 'prerequisite-unmet', outcome: run.outcome ?? null, resumable, reason: `${(run.prerequisites || []).map((p) => `${p.kind} ${p.id} (${p.state})`).join('; ') || 'a prerequisite is unmet'}; nothing was executed${resumable ? ' (resumable)' : ''}` };
71
+ }
72
+ const outcome = run.outcome;
73
+ const observed = run.receipt?.observed || {};
74
+ // a business-state run reports its control flow under `preconditions`: a control flow that no longer works is a broken revision
75
+ const loadBroken = observed.targetLoaded === false || observed.benignCompleted === false || observed.preconditions?.controlRan === false || observed.preconditions?.controlDurableEffect === false;
76
+ if (outcome === 'error') return { status: 'incomplete', code: 'harness-error', reason: String(run.record?.reason || 'the oracle run failed').slice(0, 300), outcome };
77
+ if (outcome === EXPECTED[step]) return { status: 'passed', code: null, reason: run.record?.reason ?? null, outcome };
78
+ const reason = String(run.record?.reason || '').slice(0, 300);
79
+ switch (step) {
80
+ case 'original-positive':
81
+ return { status: 'failed', code: 'original-not-reproduced', reason: `the original revision did not reproduce the exploit (outcome '${outcome}'): ${reason}`, outcome };
82
+ case 'patched-negative':
83
+ if (outcome === 'confirmed') return { status: 'failed', code: 'patched-still-exploitable', reason: `the exploit still works on the patched revision: ${reason}`, outcome };
84
+ return loadBroken
85
+ ? { status: 'failed', code: 'patched-build-broken', reason: `the patched revision did not load or its benign control failed: ${reason}`, outcome }
86
+ : { status: 'failed', code: 'patched-negative-inconclusive', reason: `the patched run decided nothing (outcome '${outcome}'): ${reason}`, outcome };
87
+ case 'functional-baseline':
88
+ return { status: 'failed', code: 'functional-baseline-invalid', reason: `the declared functional expectations do not hold on the original revision (outcome '${outcome}'): ${reason}`, outcome };
89
+ default:
90
+ if (outcome === 'confirmed') return { status: 'failed', code: 'functional-regression-detected', reason: `the patched revision behaves differently from the declared expectations: ${reason}`, outcome };
91
+ return loadBroken
92
+ ? { status: 'failed', code: 'patched-build-broken', reason: `the patched revision did not load for the functional check: ${reason}`, outcome }
93
+ : { status: 'failed', code: 'functional-inconclusive', reason: `the functional check decided nothing (outcome '${outcome}'): ${reason}`, outcome };
94
+ }
95
+ }
96
+
97
+ function summaryOf(steps, verified, incomplete, code) {
98
+ const label = (s) => ({ passed: 'PASS', failed: 'FAIL', incomplete: 'INCOMPLETE', 'not-run': 'not run' })[s.status];
99
+ const lines = steps.map((s) => `${s.step}: ${label(s)}${s.code ? ` (${s.code})` : ''}${s.reason && s.status !== 'passed' ? ` - ${s.reason}` : ''}`);
100
+ lines.push(verified
101
+ ? 'verified-fix: the exploit reproduced on the original revision, did not reproduce on the patched revision, and the declared functional cases still hold. Only the declared scenario and cases were exercised.'
102
+ : `NOT verified-fix: incomplete step '${incomplete}'${code ? ` (${code})` : ''}.`);
103
+ return lines.join('\n');
104
+ }
105
+
106
+ /**
107
+ * Run the three-part patch-negative verification for one proposed patch. Never throws.
108
+ *
109
+ * @param {object} req
110
+ * @param {string} req.hypothesisId the finding's stable id
111
+ * @param {string} req.commit exact revision the patch is proposed against (40 or 64 hex)
112
+ * @param {object} req.original { files, entry, oracleId, inputs, budgets? } the exploit scenario on the original files
113
+ * @param {object} req.patch { files } the proposed diff: the new content of each changed file
114
+ * @param {object} [req.patched] { oracleId?, inputs?, entry? }: must equal the original's; anything else is `oracle-changed`
115
+ * @param {object} [req.functional] { inputs, budgets? } for the functional-regression oracle; omitting it prevents verified-fix
116
+ * @param {object} [req.pinnedOracle] { id, version, logicDigest } the oracle identity the requester recorded earlier
117
+ * @param {object[]} [req.ledger] earlier repair records to extend
118
+ * @param {object} [o] { config, runOptions } config is the assurance config; runOptions go to the oracle runner (test seams)
119
+ */
120
+ export async function verifyPatchNegative(req, o = {}) {
121
+ const config = o.config || resolveAssuranceConfig({ env: process.env });
122
+ const gate = featureStatus(config, PATCH_NEGATIVE_FEATURE);
123
+ const steps = STEPS.map((s) => stepResult(s, 'not-run', { reason: 'not reached' }));
124
+ const base = {
125
+ status: 'not-verified-fix', verifiedFix: false, incompleteStep: null, failureCode: null, reason: null, steps,
126
+ patchDigest: null, promotion: null, receipts: null, records: {}, verificationRecord: null, repairRecords: Array.isArray(req?.ledger) ? req.ledger : [], summary: '',
127
+ };
128
+ if (gate.status !== 'ok') {
129
+ return { ...base, status: 'disabled', incompleteStep: 'feature-gate', failureCode: gate.code || 'disabled', reason: `${PATCH_NEGATIVE_FEATURE} is not available: ${gate.reason}`, summary: `patch-negative verification did not run: ${gate.reason}` };
130
+ }
131
+ const refuse = (step, code, reason, extra = {}) => {
132
+ const rejected = req && isPlainObject(req.patch) && isPlainObject(req.patch.files) && Object.keys(req.patch.files).length && typeof req.hypothesisId === 'string'
133
+ ? ledgerFor(base.repairRecords, req, base.patchDigest ?? digestOf(req.patch.files), { rejected: { step, code, reason } }) : { records: base.repairRecords };
134
+ return { ...base, ...extra, patchDigest: req?.patch?.files ? digestOf(req.patch.files) : null, incompleteStep: step, failureCode: code, reason, repairRecords: rejected.records, summary: summaryOf(steps, false, step, code) };
135
+ };
136
+
137
+ // ---- shape and preconditions that need no execution
138
+ const patchFiles = req?.patch?.files;
139
+ if (!isPlainObject(req) || typeof req.hypothesisId !== 'string' || !req.hypothesisId || !isPlainObject(req.original) || !isPlainObject(req.original.files)
140
+ || typeof req.original.entry !== 'string' || typeof req.original.oracleId !== 'string' || !isPlainObject(req.original.inputs)
141
+ || !isPlainObject(patchFiles) || Object.keys(patchFiles).length === 0) {
142
+ return refuse('input-validation', 'bad-request', 'a request needs a hypothesis id, the original files, entry, oracle and inputs, and a non-empty patch');
143
+ }
144
+ if (!isCommit(req.commit)) return refuse('input-validation', 'revision-unbound', 'no exact commit was given: a verdict cannot be tied to a revision');
145
+ const original = req.original;
146
+ const oracle = registry.getOracle(original.oracleId);
147
+ if (!oracle) return refuse('input-validation', 'unknown-oracle', `the exploit oracle '${String(original.oracleId).slice(0, 60)}' is not registered`);
148
+ if (oracle.id === FUNCTIONAL_ORACLE) return refuse('input-validation', 'bad-request', 'the exploit oracle cannot be the functional-regression oracle');
149
+ const patched = isPlainObject(req.patched) ? req.patched : {};
150
+ if ((patched.oracleId !== undefined && patched.oracleId !== original.oracleId) || (patched.inputs !== undefined && digestOf(patched.inputs) !== digestOf(original.inputs)) || (patched.entry !== undefined && patched.entry !== original.entry)) {
151
+ return refuse('oracle-consistency', 'oracle-changed', 'the patched run names a different oracle, entry or inputs than the original run: a changed oracle cannot show the exploit is closed');
152
+ }
153
+ const pin = req.pinnedOracle;
154
+ if (pin !== undefined && (!isPlainObject(pin) || pin.id !== oracle.id || pin.version !== oracle.version || pin.logicDigest !== oracle.logicDigest)) {
155
+ return refuse('oracle-consistency', 'oracle-changed', 'the oracle differs from the identity the requester pinned (id, version or logic digest)');
156
+ }
157
+ const fn = req.functional;
158
+ if (!isPlainObject(fn) || !isPlainObject(fn.inputs)) {
159
+ return refuse('functional-regression', 'functional-omitted', 'no functional regression check was supplied: a patch that was never checked for behaviour cannot be a verified fix');
160
+ }
161
+ const functionalOracle = fn.oracleId === undefined ? FUNCTIONAL_ORACLE : fn.oracleId;
162
+ if (!FUNCTIONAL_ORACLES.includes(functionalOracle)) {
163
+ return refuse('input-validation', 'bad-request', `the functional check oracle must be one of ${FUNCTIONAL_ORACLES.join(', ')}`);
164
+ }
165
+
166
+ const patchDigest = digestOf(patchFiles);
167
+ base.patchDigest = patchDigest;
168
+ let ledger = ledgerFor(base.repairRecords, req, patchDigest, {}).records; // proposed
169
+ const common = { hypothesisId: req.hypothesisId, commit: req.commit };
170
+ const plan = [
171
+ ['original-positive', { oracleId: original.oracleId, entry: original.entry, inputs: original.inputs, patchFiles: null, budgets: original.budgets }],
172
+ ['patched-negative', { oracleId: original.oracleId, entry: original.entry, inputs: original.inputs, patchFiles, budgets: original.budgets }],
173
+ ['functional-baseline', { oracleId: functionalOracle, entry: original.entry, inputs: fn.inputs, patchFiles: null, budgets: fn.budgets }],
174
+ ['functional-regression', { oracleId: functionalOracle, entry: original.entry, inputs: fn.inputs, patchFiles, budgets: fn.budgets }],
175
+ ];
176
+ const runs = {};
177
+ let failed = null;
178
+ for (const [i, [step, spec]] of plan.entries()) {
179
+ if (failed) continue;
180
+ let run;
181
+ try {
182
+ const manifest = createReplayManifest({ ...common, fixtureFiles: original.files, patchFiles: spec.patchFiles, oracleId: spec.oracleId, entry: spec.entry, inputs: spec.inputs, budgets: spec.budgets || {}, expected: { outcome: EXPECTED[step] } });
183
+ run = await replayManifest({ manifest, fixtureFiles: original.files, patchFiles: spec.patchFiles }, { config: o.config || config, runOptions: o.runOptions });
184
+ } catch (e) {
185
+ run = { status: 'rejected', errors: [{ code: 'RUN_FAILED', message: String(e?.message || e).slice(0, 200) }] };
186
+ }
187
+ const c = classify(step, run);
188
+ steps[i] = stepResult(step, c.status, { ...c, recordId: run.record?.id ?? null, receiptDigest: run.receipt?.receiptDigest ?? null });
189
+ if (run.executed && run.receipt) runs[ROLE_OF_STEP[step]] = run;
190
+ if (c.status !== 'passed') failed = { step, ...c };
191
+ }
192
+ base.records = Object.fromEntries(Object.entries(runs).map(([k, r]) => [k, r.record]));
193
+ if (failed) {
194
+ ledger = ledgerFor(ledger, req, patchDigest, { rejected: { step: failed.step, code: failed.code, reason: failed.reason } }).records;
195
+ const original0 = base.records.original ? withRepairStatus(base.records.original, verificationRepairFor(ledger, req.hypothesisId, patchDigest)) : null;
196
+ return { ...base, incompleteStep: failed.step, failureCode: failed.code, reason: failed.reason, resumable: failed.resumable ?? false, repairRecords: ledger, verificationRecord: original0, summary: summaryOf(steps, false, failed.step, failed.code) };
197
+ }
198
+
199
+ // ---- the receipts must match the exact proposed diff and revision before anything is promoted
200
+ const receipts = Object.fromEntries(Object.entries(runs).map(([k, r]) => [k, r.receipt]));
201
+ const promotion = promotePatch({ proposal: { hypothesisId: req.hypothesisId, revision: req.commit, originalFiles: original.files, patchFiles, functionalOracle }, receipts });
202
+ if (!promotion.ok) {
203
+ const envBroken = promotion.reasons.some((r) => r.code === 'environment-mismatch');
204
+ const code = envBroken ? 'environment-mismatch' : promotion.reasons.some((r) => r.code === 'oracle-changed') ? 'oracle-changed' : 'promotion-refused';
205
+ const step = envBroken ? 'environment-equivalence' : code === 'oracle-changed' ? 'oracle-consistency' : 'promotion';
206
+ const reason = promotion.reasons.map((r) => r.message).join('; ').slice(0, 400);
207
+ ledger = ledgerFor(ledger, req, patchDigest, { rejected: { step, code, reason } }).records;
208
+ return { ...base, promotion, incompleteStep: step, failureCode: code, reason, repairRecords: ledger, verificationRecord: base.records.original ? withRepairStatus(base.records.original, verificationRepairFor(ledger, req.hypothesisId, patchDigest)) : null, summary: summaryOf(steps, false, step, code) };
209
+ }
210
+ const promoted = recordPromotion(ledger, promotion, { verificationRecordId: base.records.patched?.id ?? null });
211
+ ledger = promoted.records;
212
+ const verificationRecord = withRepairStatus(base.records.original, verificationRepairFor(ledger, req.hypothesisId, patchDigest, base.records.patched?.id));
213
+ return {
214
+ ...base, status: 'verified-fix', verifiedFix: true, promotion, receipts, repairRecords: ledger, verificationRecord,
215
+ reason: 'verified', summary: summaryOf(steps, true, null, null),
216
+ };
217
+ }
218
+
219
+ // The ledger for one verification: always starts with a `proposed` record, and records a rejection when verification stopped.
220
+ function ledgerFor(records, req, patchDigest, { rejected } = {}) {
221
+ let list = Array.isArray(records) ? records : [];
222
+ const fields = { hypothesisId: req.hypothesisId, revision: isCommit(req.commit) ? req.commit : null, diffDigest: patchDigest };
223
+ if (!list.some((r) => r.hypothesisId === fields.hypothesisId && r.diffDigest === patchDigest)) list = appendRepairRecord(list, { ...fields, kind: 'proposed', reason: 'a patch was proposed for verification' }).records;
224
+ if (rejected) list = appendRepairRecord(list, { ...fields, kind: 'rejected', step: rejected.step, reason: `${rejected.code}: ${String(rejected.reason || '').slice(0, 300)}` }).records;
225
+ return { records: list };
226
+ }
@@ -0,0 +1,113 @@
1
+ // Promoting a patch on independent verifier receipts (X-204.AC03).
2
+ //
3
+ // A patch may be PROMOTED (made eligible to be applied) only after the verifier domain has issued receipts for the exact
4
+ // diff it is being asked to promote, at the exact revision it was proposed against:
5
+ //
6
+ // original the original files reproduce the exploit -> confirmed
7
+ // patched original files with the proposed diff applied -> refuted
8
+ // functionalBaseline the functional expectations on the original files -> refuted (no regression)
9
+ // functional the same expectations on the patched files -> refuted (no regression)
10
+ //
11
+ // Every receipt is checked against the PROPOSAL, not against itself: the digest of the files a receipt covers must equal the
12
+ // digest this module recomputes from the proposed diff, and the commit it was issued for must be the proposal's revision. A
13
+ // receipt for a different diff, a different revision or a different hypothesis does not promote, and neither does a receipt
14
+ // the verifier did not issue (a copy, a hand-built object, a receipt whose content was edited: `isIssuedReceipt` is true only
15
+ // for the frozen object `runOracle` itself created).
16
+ //
17
+ // What this proves and what it does not: the receipts are unsigned objects valid inside the verifier's own process. A receipt
18
+ // that crossed a process boundary (JSON) is no longer an issued receipt and cannot promote; carrying promotion across
19
+ // processes needs a signing domain, which belongs to the signer, not to this module. Receipts show that the declared scenario
20
+ // and the declared functional cases behaved as stated; they do not show the patch is correct beyond them.
21
+ import { digestOf } from '../assurance/identity.js';
22
+ import { isCommit, isPlainObject } from '../assurance/schema-kit.js';
23
+ import { isIssuedReceipt } from '../oracles/oracle.js';
24
+
25
+ const PROMOTION_ROLES = Object.freeze(['original', 'patched', 'functionalBaseline', 'functional']);
26
+ // The oracle class the behaviour-preservation runs must come from. Default `functional-regression`; a proposal may name
27
+ // `business-state` (X-406: an authorized-workflow scenario as the declared case). Nothing else is accepted.
28
+ const FUNCTIONAL_CLASSES = Object.freeze({ 'functional-regression': 'functional-regression', 'business-state': 'business-state' });
29
+
30
+ const ISSUED_PROMOTIONS = new WeakSet();
31
+
32
+ /** True only for a result `promotePatch` itself returned with `ok: true`. A copy or a hand-built object is not. */
33
+ export function isIssuedPromotion(p) { return isPlainObject(p) && ISSUED_PROMOTIONS.has(p); }
34
+
35
+ // What each receipt role must have settled to.
36
+ const EXPECTED_OUTCOME = Object.freeze({ original: 'confirmed', patched: 'refuted', functionalBaseline: 'refuted', functional: 'refuted' });
37
+
38
+ const same = (a, b) => digestOf(a ?? null) === digestOf(b ?? null);
39
+
40
+ /**
41
+ * @param {object} args
42
+ * @param {object} args.proposal { hypothesisId, revision, originalFiles, patchFiles, functionalOracle? }
43
+ * @param {object} args.receipts { original, patched, functionalBaseline, functional }, each issued by runOracle
44
+ * @returns {{ ok: boolean, diffDigest: string|null, revision: string|null, reasons: {code,role,message}[], receiptDigests: object }}
45
+ */
46
+ export function promotePatch({ proposal, receipts } = {}) {
47
+ const reasons = [];
48
+ const why = (code, role, message) => reasons.push({ code, role, message });
49
+ const out = (extra = {}) => {
50
+ const result = { ok: reasons.length === 0, diffDigest: null, revision: null, hypothesisId: null, reasons, receiptDigests: {}, ...extra };
51
+ result.ok = reasons.length === 0;
52
+ if (result.ok) ISSUED_PROMOTIONS.add(result);
53
+ return result;
54
+ };
55
+
56
+ if (!isPlainObject(proposal) || typeof proposal.hypothesisId !== 'string' || !proposal.hypothesisId
57
+ || !isPlainObject(proposal.originalFiles) || !isPlainObject(proposal.patchFiles) || Object.keys(proposal.patchFiles).length === 0) {
58
+ why('bad-proposal', null, 'a proposal needs a hypothesis id, the original files and a non-empty diff');
59
+ return out();
60
+ }
61
+ if (!isCommit(proposal.revision)) {
62
+ why('revision-unbound', null, 'a promotion is bound to one exact 40 or 64 character commit; none was given');
63
+ return out();
64
+ }
65
+ const functionalClass = proposal.functionalOracle === undefined ? 'functional-regression' : FUNCTIONAL_CLASSES[proposal.functionalOracle];
66
+ if (!functionalClass) {
67
+ why('bad-proposal', null, 'the functional check oracle must be functional-regression or business-state');
68
+ return out();
69
+ }
70
+ const diffDigest = digestOf(proposal.patchFiles);
71
+ const originalDigest = digestOf(proposal.originalFiles);
72
+ const patchedDigest = digestOf({ ...proposal.originalFiles, ...proposal.patchFiles });
73
+ const wantFiles = { original: originalDigest, functionalBaseline: originalDigest, patched: patchedDigest, functional: patchedDigest };
74
+ const base = { diffDigest, revision: proposal.revision, hypothesisId: proposal.hypothesisId };
75
+
76
+ const given = isPlainObject(receipts) ? receipts : {};
77
+ const digests = {};
78
+ for (const role of PROMOTION_ROLES) {
79
+ const r = given[role];
80
+ if (!r) { why('receipt-missing', role, `no ${role} receipt was supplied`); continue; }
81
+ if (!isIssuedReceipt(r) || r.issuedBy !== 'verifier' || r.schema !== 'agentic-security/oracle-receipt') {
82
+ why('receipt-not-issued', role, `the ${role} receipt was not issued by the verifier domain (a copy or constructed receipt is not valid)`);
83
+ continue;
84
+ }
85
+ digests[role] = r.receiptDigest;
86
+ if (r.request.hypothesisId !== proposal.hypothesisId) why('receipt-hypothesis', role, `the ${role} receipt is for a different hypothesis`);
87
+ if (r.request.commit !== proposal.revision) why('receipt-revision', role, `the ${role} receipt was issued for a different revision than the proposal`);
88
+ if (r.request.filesDigest !== wantFiles[role]) why('receipt-diff', role, `the ${role} receipt covers different content than the exact proposed diff on the original revision`);
89
+ if (r.settled.outcome !== EXPECTED_OUTCOME[role]) why('receipt-outcome', role, `the ${role} receipt settled '${r.settled.outcome}', not '${EXPECTED_OUTCOME[role]}'`);
90
+ if (role !== 'original' && r.settled.outcome === 'refuted' && r.settled.preconditionsHeld !== true) why('receipt-preconditions', role, `the ${role} receipt did not prove its preconditions`);
91
+ }
92
+ if (reasons.length) return out({ ...base, receiptDigests: digests });
93
+
94
+ const { original, patched, functionalBaseline, functional } = given;
95
+ // the exploit oracle is the SAME oracle on the SAME inputs for both revisions
96
+ if (!same(original.oracle, patched.oracle) || original.request.inputsDigest !== patched.request.inputsDigest || original.request.entry !== patched.request.entry) {
97
+ why('oracle-changed', 'patched', 'the patched-negative run did not use the same oracle, entry and inputs as the original-positive run');
98
+ }
99
+ // the functional check is one oracle on one set of inputs for both revisions
100
+ if (!same(functionalBaseline.oracle, functional.oracle) || functionalBaseline.request.inputsDigest !== functional.request.inputsDigest) {
101
+ why('oracle-changed', 'functional', 'the functional check on the patched revision did not use the same oracle and cases as its baseline');
102
+ }
103
+ if (functional.oracle.class !== functionalClass) why('oracle-changed', 'functional', `the functional check is not a ${functionalClass} oracle`);
104
+ if (original.oracle.class === 'functional-regression') why('oracle-changed', 'original', 'the exploit oracle must not be the functional-regression oracle');
105
+ // equivalent disposable environments: one sanitized environment identity across every run
106
+ const envDigest = digestOf(original.environment);
107
+ for (const role of PROMOTION_ROLES) {
108
+ if (digestOf(given[role].environment) !== envDigest) why('environment-mismatch', role, `the ${role} run executed in a different environment than the original run`);
109
+ }
110
+ // four runs are four distinct receipts: one receipt cannot be presented in two roles
111
+ if (new Set(Object.values(digests)).size !== PROMOTION_ROLES.length) why('receipt-reused', null, 'the same receipt was presented in more than one role');
112
+ return out({ ...base, receiptDigests: digests });
113
+ }
@@ -0,0 +1,183 @@
1
+ // One projection of the verification record for every interface (X-206).
2
+ //
3
+ // The CLI JSON report, the MCP tools, the human-readable report and the autopilot response all show the SAME verification of
4
+ // the SAME finding. They do that by calling this one module: nothing else renders, summarises or re-derives a state, a scope,
5
+ // an evidence id or a replay prerequisite. A surface that formats its own text would eventually disagree with the others,
6
+ // which is the defect this file exists to prevent.
7
+ //
8
+ // Rules the projection keeps:
9
+ // - It is a VIEW. It reads a validated version-1 record (`assurance/verification-record.js`) and invents nothing: an
10
+ // invalid record yields `{ ok: false, errors }`, never a guessed state.
11
+ // - Nothing the record holds is dropped. Every record field has a counterpart in the view (the test enumerates them), so a
12
+ // richer consumer loses nothing and a legacy consumer keeps its own boolean view under `legacy`.
13
+ // - The text never labels a partial result "safe" or "fixed". It states what the check established and what it did not,
14
+ // in words chosen per state, and a repair is described by its own separate status.
15
+ // - Output is deterministic: no clock, no randomness, a fixed key order and sorted prerequisites.
16
+ import { validateVerificationRecord } from '../assurance/verification-record.js';
17
+ import { toLegacyVerificationView } from '../assurance/migrations.js';
18
+ import { isPlainObject } from '../assurance/schema-kit.js';
19
+ import { trustedNegativeDenominator } from '../oracles/scenario-classes.js';
20
+
21
+ export const VIEW_SCHEMA = 'agentic-security/verification-view';
22
+ const VIEW_VERSION = '1.0.0';
23
+
24
+ const RUNTIME_ORACLE_KINDS = ['runtime-replay', 'differential'];
25
+
26
+ // What a reader is told for each state. Chosen so no partial state reads as a pass: only `confirmed` and `refuted` say anything
27
+ // was decided, and both are limited to the declared scenario.
28
+ const STATE_LABEL = Object.freeze({
29
+ confirmed: 'CONFIRMED (the declared scenario reproduced the effect)',
30
+ refuted: 'NOT REPRODUCED (the declared scenario did not reproduce the effect)',
31
+ inconclusive: 'INCONCLUSIVE (nothing was decided)',
32
+ 'not-run': 'NOT RUN (no check was executed)',
33
+ unsupported: 'UNSUPPORTED (no applicable check exists here)',
34
+ error: 'ERROR (the check failed to run)',
35
+ });
36
+
37
+ const REPAIR_LINE = Object.freeze({
38
+ none: 'no patch is recorded for this finding',
39
+ proposed: 'a patch was proposed; it has not been replay-verified',
40
+ applied: 'a patch was applied; its effect has not been replay-verified',
41
+ 'replay-verified': 'a patch was replay-verified against the declared scenario and functional cases only',
42
+ 'rolled-back': 'a patch was rolled back',
43
+ });
44
+
45
+ function verifiedStatement(rec) {
46
+ const how = rec.oracle ? `oracle '${rec.oracle.id}'${rec.oracle.version ? ` v${rec.oracle.version}` : ''}` : 'no oracle';
47
+ const at = rec.commit ? ` at commit ${rec.commit.slice(0, 12)}` : ' without an exact commit';
48
+ switch (rec.outcome) {
49
+ case 'confirmed': return `${how} reproduced the effect${at}, within: ${rec.scope.description}`;
50
+ case 'refuted': return `${how} ran with valid preconditions and did not reproduce the effect${at}, within: ${rec.scope.description}`;
51
+ case 'inconclusive': return `nothing was established${rec.attempt > 0 ? ` (${how} ran but did not decide)` : ''}`;
52
+ case 'not-run': return 'nothing was established; no check was executed';
53
+ case 'unsupported': return 'nothing was established; no applicable check exists in this environment';
54
+ default: return 'nothing was established; the check did not complete';
55
+ }
56
+ }
57
+
58
+ function untestedStatements(rec) {
59
+ const out = [];
60
+ if (rec.outcome === 'confirmed' || rec.outcome === 'refuted') {
61
+ out.push('behaviour outside the declared scenario and inputs');
62
+ if (rec.outcome === 'refuted') out.push('other inputs and code paths that could still reach the same effect');
63
+ } else if (rec.outcome === 'inconclusive') {
64
+ out.push('whether the effect is reachable: the check did not decide');
65
+ } else {
66
+ out.push('the hypothesis itself: it has not been exercised');
67
+ }
68
+ if (rec.repair.status !== 'replay-verified') out.push('the effect of any proposed patch under a trusted exploit-negative replay');
69
+ if (rec.confirmationLevel === 'none' && rec.outcome !== 'confirmed') out.push('independent adjudication: none is recorded');
70
+ return out;
71
+ }
72
+
73
+ /**
74
+ * The replay prerequisites for this record. Derived ones come from the record itself, so every surface computes the same
75
+ * list. Typed prerequisites a replay attempt reported (`replay/replay.js`) are merged in by id.
76
+ */
77
+ function replayPrerequisites(rec, supplied) {
78
+ const items = new Map();
79
+ items.set('exact-commit', {
80
+ id: 'exact-commit', state: rec.commit ? 'met' : 'unmet',
81
+ detail: rec.commit ? 'the record is bound to one exact commit' : 'no exact commit is recorded; a replay cannot be pinned',
82
+ });
83
+ const runtime = !!rec.oracle && RUNTIME_ORACLE_KINDS.includes(rec.oracle.kind);
84
+ items.set('runtime-oracle', {
85
+ id: 'runtime-oracle', state: runtime ? 'met' : 'unmet',
86
+ detail: runtime ? `oracle '${rec.oracle.id}' is a runtime oracle` : 'no runtime oracle produced this record, so there is nothing to replay',
87
+ });
88
+ items.set('execution-boundary', {
89
+ id: 'execution-boundary', state: 'declared',
90
+ detail: `recorded on ${rec.scope.platform}${rec.scope.backend ? ` (${rec.scope.backend} backend)` : ''}; a replay needs an equivalent host`,
91
+ });
92
+ for (const p of Array.isArray(supplied) ? supplied : []) {
93
+ if (!isPlainObject(p) || typeof p.id !== 'string' || !p.id) continue;
94
+ const state = ['met', 'unmet', 'declared'].includes(p.state) ? p.state : 'unmet';
95
+ const item = { id: p.kind ? `${p.kind}:${p.id}` : p.id, state, detail: typeof p.reason === 'string' && p.reason ? p.reason.slice(0, 200) : 'reported by the replay attempt' };
96
+ if (typeof p.resumable === 'boolean') item.resumable = p.resumable;
97
+ items.set(item.id, item);
98
+ }
99
+ const list = [...items.values()].sort((a, b) => a.id.localeCompare(b.id));
100
+ const replayable = runtime && !!rec.commit && !['not-run', 'unsupported'].includes(rec.outcome) && !list.some((p) => p.state === 'unmet');
101
+ return { replayable, prerequisites: list };
102
+ }
103
+
104
+ function textLines(view) {
105
+ const lines = [];
106
+ lines.push(`Verification: ${STATE_LABEL[view.state]}`);
107
+ lines.push(` Record: ${view.recordId}${view.oracle ? ` oracle: ${view.oracle.id}${view.oracle.version ? `@${view.oracle.version}` : ''}` : ' oracle: none'}`);
108
+ lines.push(` Scope: ${view.scope.description} [${view.scope.platform}${view.scope.backend ? `/${view.scope.backend}` : ''}]`);
109
+ lines.push(` Verified: ${view.summary.verified}`);
110
+ lines.push(` Not verified: ${view.summary.untested.join('; ')}`);
111
+ lines.push(` Repair: ${REPAIR_LINE[view.repair.status]}`);
112
+ lines.push(` Reason: ${view.reason}`);
113
+ lines.push(` Evidence: ${view.evidenceIds.length ? view.evidenceIds.join(', ') : 'none'}`);
114
+ lines.push(` Replay prerequisites: ${view.replay.prerequisites.map((p) => `${p.id} (${p.state})`).join(', ')}${view.replay.replayable ? '' : ' [not replayable as recorded]'}`);
115
+ return lines;
116
+ }
117
+
118
+ /**
119
+ * Project a version-1 verification record.
120
+ *
121
+ * @param {object} record a record from `emitVerification` / `runOracle`
122
+ * @param {object} [o]
123
+ * @param {object[]} [o.replay] typed prerequisites a replay attempt reported (`attempt.prerequisites`)
124
+ * @returns {{ ok: boolean, view: object|null, errors: object[] }} never throws
125
+ */
126
+ export function projectVerification(record, o = {}) {
127
+ try {
128
+ const v = validateVerificationRecord(record);
129
+ if (!v.ok) return { ok: false, view: null, errors: v.errors };
130
+ const replay = replayPrerequisites(record, o.replay);
131
+ const view = {
132
+ schema: VIEW_SCHEMA, schemaVersion: VIEW_VERSION,
133
+ recordId: record.id, recordSchemaVersion: record.schemaVersion,
134
+ hypothesisId: record.hypothesisId, commit: record.commit, detectorOrigin: record.detectorOrigin,
135
+ oracle: record.oracle, attempt: record.attempt,
136
+ state: record.outcome, reason: record.reason,
137
+ confirmationLevel: record.confirmationLevel, preconditions: record.preconditions,
138
+ scope: record.scope, repair: record.repair,
139
+ evidence: record.evidence.map((e) => ({ id: e.id, kind: e.kind, producer: e.producer, digest: e.digest, source: e.source ?? null })),
140
+ evidenceIds: record.evidence.map((e) => e.id),
141
+ referencedEvidenceIds: [...record.evidenceRefs],
142
+ replay,
143
+ summary: { verified: verifiedStatement(record), untested: untestedStatements(record) },
144
+ legacy: toLegacyVerificationView(record),
145
+ migration: record.migration ?? null, createdAt: record.createdAt ?? null,
146
+ };
147
+ view.text = textLines(view);
148
+ return { ok: true, view, errors: [] };
149
+ } catch (e) {
150
+ return { ok: false, view: null, errors: [{ code: 'RULE_VIOLATION', path: '', message: `verification projection failed: ${String(e?.message || e).slice(0, 160)}` }] };
151
+ }
152
+ }
153
+
154
+ /**
155
+ * The fields every surface adds to an output that carries a verification record. The key is `verificationView`, NOT
156
+ * `verification`: a finding's `verification` field is the producer/verifier separation record
157
+ * (`posture/verification-separation.js`) and keeps its meaning. ADDITIVE: an output without a record gets
158
+ * `{}` and is byte-identical to what it was. A record that does not validate is reported, not dropped and not guessed.
159
+ */
160
+ export function verificationFields(record, o = {}) {
161
+ if (!isPlainObject(record)) return {};
162
+ const p = projectVerification(record, o);
163
+ return p.ok ? { verificationView: p.view } : { verificationView: null, verificationViewErrors: p.errors };
164
+ }
165
+
166
+ /**
167
+ * The trusted-negative accounting for a set of verification records, for every interface that summarises a run
168
+ * (`oracles/scenario-classes.js` owns the rule): only a decided result of an executed oracle of an advertised non-taint class,
169
+ * with proven preconditions and trusted runtime proof, is counted. Every other record is listed with why it was left out, so an
170
+ * unsupported or unrun case can never read as "checked and clean". Records that are not valid are reported as excluded.
171
+ */
172
+ export function verificationCoverage(records) {
173
+ const list = (Array.isArray(records) ? records : []).filter(isPlainObject);
174
+ const valid = list.filter((r) => validateVerificationRecord(r).ok);
175
+ const d = trustedNegativeDenominator(valid);
176
+ const invalid = list.length - valid.length;
177
+ const excluded = [...d.excluded, ...Array.from({ length: invalid }, () => ({ id: null, outcome: null, reason: 'malformed' }))];
178
+ const reasons = {};
179
+ for (const e of excluded) reasons[e.reason] = (reasons[e.reason] || 0) + 1;
180
+ const why = Object.entries(reasons).sort(([a], [b]) => a.localeCompare(b)).map(([k, n]) => `${k}: ${n}`).join(', ');
181
+ const lines = [`Trusted non-taint verification: ${d.denominator} decided (${d.trustedNegatives} not reproduced, ${d.trustedPositives} confirmed) of ${list.length} record(s); ${excluded.length} not counted${excluded.length ? ` (${why})` : ''}`];
182
+ return { schema: 'agentic-security/verification-coverage', schemaVersion: '1.0.0', total: list.length, denominator: d.denominator, trustedNegatives: d.trustedNegatives, trustedPositives: d.trustedPositives, excluded, lines };
183
+ }