@clear-capabilities/agentic-security-scanner 0.160.0 → 0.162.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 (76) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/bin/agentic-security.js +11 -2
  3. package/dist/1919.index.js +1 -1
  4. package/dist/2432.index.js +10 -1
  5. package/dist/4113.index.js +60 -9
  6. package/dist/5613.index.js +500 -0
  7. package/dist/6239.index.js +469 -0
  8. package/dist/7033.index.js +4 -1
  9. package/dist/7838.index.js +39 -3
  10. package/dist/8714.index.js +1 -1
  11. package/dist/8798.index.js +2 -2
  12. package/dist/9207.index.js +5 -0
  13. package/dist/9560.index.js +205 -62
  14. package/dist/{1905.index.js → 9999.index.js} +265 -8
  15. package/dist/agentic-security.mjs +12 -12
  16. package/dist/agentic-security.mjs.sha256 +1 -1
  17. package/package.json +14 -9
  18. package/src/dataflow/engine.js +20 -4
  19. package/src/discovery/CLAUDE.md +9 -0
  20. package/src/discovery/index.js +28 -0
  21. package/src/discovery/memory.js +7 -5
  22. package/src/engine.js +29 -8
  23. package/src/fix/apply-fix-service.js +33 -3
  24. package/src/language/CLAUDE.md +6 -1
  25. package/src/language/aibom.js +4 -1
  26. package/src/language/fix-lifecycle.js +9 -0
  27. package/src/language/haskell-ir.js +47 -7
  28. package/src/language/haskell-sca.js +87 -7
  29. package/src/language/haskell-supply.js +175 -6
  30. package/src/language/haskell-web.js +200 -18
  31. package/src/language/nix-fix.js +1 -1
  32. package/src/language/resolved-pass.js +4 -1
  33. package/src/language/witness.js +1 -1
  34. package/src/mcp/CLAUDE.md +43 -4
  35. package/src/mcp/tools.js +185 -43
  36. package/src/posture/CLAUDE.md +174 -0
  37. package/src/posture/assurance/baseline-inventory.js +218 -0
  38. package/src/posture/assurance/baseline.js +322 -0
  39. package/src/posture/assurance/bounded-io.js +145 -0
  40. package/src/posture/assurance/config.js +299 -0
  41. package/src/posture/assurance/contracts.js +315 -0
  42. package/src/posture/assurance/identity.js +62 -0
  43. package/src/posture/assurance/migrations.js +259 -0
  44. package/src/posture/assurance/schema-kit.js +111 -0
  45. package/src/posture/assurance/verification-record.js +258 -0
  46. package/src/posture/attestation.js +5 -0
  47. package/src/posture/autopilot.js +28 -1
  48. package/src/posture/blast-radius.js +15 -1
  49. package/src/posture/evidence-bundle.js +93 -7
  50. package/src/posture/execution-proof.js +11 -1
  51. package/src/posture/fix-verify-loop.js +20 -6
  52. package/src/posture/fix-verify.js +37 -2
  53. package/src/posture/oracles/adapters.js +360 -0
  54. package/src/posture/oracles/conformance.js +275 -0
  55. package/src/posture/oracles/oracle.js +433 -0
  56. package/src/posture/oracles/registry.js +76 -0
  57. package/src/posture/oracles/scenario-classes.js +223 -0
  58. package/src/posture/remediation-ledger.js +10 -1
  59. package/src/posture/replay/replay.js +249 -0
  60. package/src/posture/verification/advisory-state.js +71 -0
  61. package/src/posture/verification/emit.js +257 -0
  62. package/src/posture/verification/hypothesis-promotion.js +211 -0
  63. package/src/posture/verification/patch-negative.js +214 -0
  64. package/src/posture/verification/patch-promotion.js +105 -0
  65. package/src/posture/verification/projection.js +183 -0
  66. package/src/posture/verification/repair-records.js +124 -0
  67. package/src/posture/verifier.js +16 -0
  68. package/src/report/index.js +49 -0
  69. package/src/sandbox/CLAUDE.md +41 -0
  70. package/src/sandbox/backend-namespace.js +7 -0
  71. package/src/sandbox/backend-userspace.js +68 -29
  72. package/src/sandbox/control-probes.js +215 -0
  73. package/src/sandbox/supervise.js +201 -0
  74. package/src/sandbox/trust-boundary.js +113 -0
  75. package/src/sandbox/trust-domains.js +171 -0
  76. package/src/sast/pipeline.js +51 -4
@@ -1,8 +1,8 @@
1
- export const id = 1905;
2
- export const ids = [1905,3499];
1
+ export const id = 9999;
2
+ export const ids = [9999,3499];
3
3
  export const modules = {
4
4
 
5
- /***/ 11905:
5
+ /***/ 49999:
6
6
  /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
7
 
8
8
 
@@ -788,6 +788,8 @@ var external_node_fs_ = __webpack_require__(73024);
788
788
  var external_node_path_ = __webpack_require__(76760);
789
789
  // EXTERNAL MODULE: ./src/posture/state-dir.js
790
790
  var state_dir = __webpack_require__(31174);
791
+ // EXTERNAL MODULE: ./src/posture/verification/advisory-state.js
792
+ var advisory_state = __webpack_require__(73826);
791
793
  ;// CONCATENATED MODULE: ./src/discovery/memory.js
792
794
  // Cross-run discovery memory — PRD Phase 3 / C4.
793
795
  //
@@ -831,6 +833,7 @@ var state_dir = __webpack_require__(31174);
831
833
 
832
834
 
833
835
 
836
+
834
837
  const MEMORY_SCHEMA = 'agentic-security/discovery-memory@1';
835
838
  const MEMORY_FILE = external_node_path_.join('.agentic-security', 'discovery-memory.json');
836
839
 
@@ -864,14 +867,15 @@ function loadMemory(scanRoot) {
864
867
  }
865
868
  }
866
869
 
867
- /** Persist. Failure is non-fatal — the run still produced its report. */
870
+ /**
871
+ * Persist. Failure is non-fatal -- the run still produced its report. X-207: the write goes through the advisory-state writer
872
+ * (closed file-name allowlist, atomic rename, never through a link), so hunt output cannot reach `last-scan.json` or its
873
+ * signature even when a hostile tree planted a link at the memory file.
874
+ */
868
875
  function saveMemory(scanRoot, memory) {
869
876
  try {
870
- const p = external_node_path_.join(scanRoot, MEMORY_FILE);
871
877
  if (!(0,state_dir.stateWritesEnabled)()) return;
872
- external_node_fs_.mkdirSync(external_node_path_.dirname(p), { recursive: true });
873
- external_node_fs_.writeFileSync(p, JSON.stringify(memory, null, 2) + '\n');
874
- return true;
878
+ return (0,advisory_state/* writeAdvisoryState */.iS)(scanRoot, external_node_path_.basename(MEMORY_FILE), JSON.stringify(memory, null, 2) + '\n').ok;
875
879
  } catch {
876
880
  return false;
877
881
  }
@@ -953,6 +957,231 @@ function forgetRefuted(memory) {
953
957
  return { ...emptyMemory(), ...memory, candidates };
954
958
  }
955
959
 
960
+ // EXTERNAL MODULE: ./src/posture/verification/emit.js
961
+ var emit = __webpack_require__(85578);
962
+ // EXTERNAL MODULE: ./src/posture/oracles/oracle.js + 4 modules
963
+ var oracle = __webpack_require__(80219);
964
+ // EXTERNAL MODULE: ./src/posture/assurance/verification-record.js
965
+ var verification_record = __webpack_require__(29668);
966
+ // EXTERNAL MODULE: ./src/posture/assurance/identity.js
967
+ var identity = __webpack_require__(41877);
968
+ // EXTERNAL MODULE: ./src/posture/assurance/schema-kit.js
969
+ var schema_kit = __webpack_require__(53353);
970
+ // EXTERNAL MODULE: ./src/posture/verification-separation.js
971
+ var verification_separation = __webpack_require__(3107);
972
+ ;// CONCATENATED MODULE: ./src/posture/verification/hypothesis-promotion.js
973
+ // Promoting an advisory hypothesis into a finding (X-207.AC02, AC03).
974
+ //
975
+ // A hunt hypothesis is advisory. It becomes a FINDING only through `promoteHypothesis`, which needs all three of:
976
+ //
977
+ // 1. a trusted verifier receipt, VERIFIED here (not merely present): issued by `runOracle` in this process
978
+ // (`isIssuedReceipt`: a copy, a JSON round trip or a hand-built object is not), bound to this hypothesis and one exact
979
+ // commit, carrying a valid version-1 record whose id is the receipt's `recordId`, settled `confirmed` at the
980
+ // `runtime-confirmed` level with trusted-runtime-proof evidence from the trusted runner;
981
+ // 2. an explicit policy evaluation. The caller passes a policy; the result carries every rule that was evaluated with its
982
+ // outcome and a digest of the policy. The mandatory rules cannot be removed by a policy, and a policy key this module does
983
+ // not know is refused (fail closed);
984
+ // 3. an audit record, returned for EVERY decision (promoted or not), that links the source hypothesis to the promoted finding
985
+ // (or to none) and names the receipt, the record and the policy evaluation. It carries no clock, so identical inputs give
986
+ // an identical record.
987
+ //
988
+ // What can never satisfy the policy: a confidence score, a repeated or unanimous model agreement, a refutation panel's votes,
989
+ // a taint-probe tier, or a severity. None of them is an input to any rule except the optional `minConfidence` / `minAgreement`
990
+ // rules, which can only ADD a requirement: the receipt rules are always evaluated, so a hypothesis with perfect confidence and
991
+ // no receipt is rejected, and a policy made only of those rules still rejects it.
992
+ //
993
+ // The producer of a hunt hypothesis and the verifier that confirms it must be different parties (`verification-separation.js`
994
+ // is reused for that check). The audit record is returned, not persisted: persisting an audit trail needs an artifact-registry
995
+ // classification and a signing domain, which are not decided here.
996
+
997
+
998
+
999
+
1000
+
1001
+
1002
+
1003
+ const AUDIT_SCHEMA = 'agentic-security/promotion-audit';
1004
+ const AUDIT_VERSION = '1.0.0';
1005
+ const POLICY_SCHEMA = 'agentic-security/promotion-policy';
1006
+
1007
+ const DECISIONS = Object.freeze(['promoted', 'rejected', 'unsupported', 'inconclusive']);
1008
+ const SEVERITY_ORDER = ['info', 'low', 'medium', 'high', 'critical'];
1009
+
1010
+ /** The default policy: runtime-confirmed by a class oracle other than the functional-regression one, severity capped at high. */
1011
+ const DEFAULT_PROMOTION_POLICY = Object.freeze({
1012
+ schema: POLICY_SCHEMA, id: 'default', version: '1',
1013
+ allowedOracleClasses: Object.freeze(oracle/* ORACLE_CLASSES */.aI.filter((c) => c !== 'functional-regression')),
1014
+ maxSeverity: 'high',
1015
+ });
1016
+
1017
+ const POLICY_KEYS = ['schema', 'id', 'version', 'allowedOracleClasses', 'maxSeverity', 'expectedCommit', 'minConfidence', 'minAgreement'];
1018
+
1019
+ const PROMOTED = new WeakSet();
1020
+ /** True only for a finding `promoteHypothesis` itself returned. */
1021
+ function isPromotedFinding(f) { return isPlainObject(f) && PROMOTED.has(f); }
1022
+
1023
+ function validatePolicy(policy) {
1024
+ if (!(0,schema_kit/* isPlainObject */.Qd)(policy)) return ['no policy was supplied: promotion needs an explicit policy evaluation'];
1025
+ const errs = [];
1026
+ for (const k of Object.keys(policy)) if (!POLICY_KEYS.includes(k)) errs.push(`unknown policy key '${k}'`);
1027
+ if (policy.schema !== POLICY_SCHEMA) errs.push(`policy.schema must be '${POLICY_SCHEMA}'`);
1028
+ if (typeof policy.id !== 'string' || !policy.id) errs.push('policy.id is required');
1029
+ if (typeof policy.version !== 'string' || !policy.version) errs.push('policy.version is required');
1030
+ if (!Array.isArray(policy.allowedOracleClasses) || policy.allowedOracleClasses.length === 0 || !policy.allowedOracleClasses.every((c) => oracle/* ORACLE_CLASSES */.aI.includes(c))) errs.push('policy.allowedOracleClasses must list known oracle classes');
1031
+ if (!SEVERITY_ORDER.includes(policy.maxSeverity)) errs.push('policy.maxSeverity must be a severity');
1032
+ if (policy.expectedCommit !== undefined && !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(String(policy.expectedCommit))) errs.push('policy.expectedCommit must be an exact commit');
1033
+ for (const k of ['minConfidence', 'minAgreement']) if (policy[k] !== undefined && !(typeof policy[k] === 'number' && policy[k] >= 0 && policy[k] <= 1)) errs.push(`policy.${k} must be a number between 0 and 1`);
1034
+ return errs;
1035
+ }
1036
+
1037
+ function agreementOf(h) {
1038
+ const votes = h?.discovery?.refutation?.votes;
1039
+ if (!Array.isArray(votes) || votes.length === 0) return null;
1040
+ const upheld = votes.filter((v) => v && (v.verdict === 'upheld' || v.upheld === true)).length;
1041
+ return upheld / votes.length;
1042
+ }
1043
+
1044
+ /**
1045
+ * Evaluate the policy for one hypothesis and one oracle result. Every rule is evaluated and listed.
1046
+ * @returns {{ ok: boolean, rules: {id: string, pass: boolean, detail: string}[], status: string, policyDigest: string|null, record: object|null }}
1047
+ */
1048
+ function evaluatePromotionPolicy({ hypothesis, result, policy } = {}) {
1049
+ const rules = [];
1050
+ const rule = (id, pass, detail) => rules.push({ id, pass: !!pass, detail });
1051
+ const bad = validatePolicy(policy);
1052
+ if (bad.length) {
1053
+ rule('policy-valid', false, bad.join('; '));
1054
+ return { ok: false, rules, status: 'rejected', policyDigest: null, record: null };
1055
+ }
1056
+ rule('policy-valid', true, `policy '${policy.id}' v${policy.version}`);
1057
+ const policyDigest = (0,identity/* digestOf */.ol)(policy);
1058
+
1059
+ const receipt = result?.receipt ?? null;
1060
+ const record = result?.record ?? null;
1061
+ const hypothesisId = (0,schema_kit/* isPlainObject */.Qd)(hypothesis) ? (0,identity/* hypothesisIdFromFinding */.XC)(hypothesis) : null;
1062
+
1063
+ rule('hypothesis-advisory', (0,advisory_state/* isAdvisoryHypothesis */.Si)(hypothesis) && !!hypothesisId, 'the source must be an unpromoted hunt hypothesis with a stable id');
1064
+ const issued = (0,oracle/* isIssuedReceipt */.AH)(receipt) && receipt.issuedBy === 'verifier' && receipt.schema === 'agentic-security/oracle-receipt';
1065
+ rule('receipt-issued', issued, issued ? 'the receipt was issued by the verifier domain in this process' : 'no receipt issued by the verifier domain was supplied (a copy, a parsed file or a constructed object is not valid)');
1066
+
1067
+ const recordOk = (0,schema_kit/* isPlainObject */.Qd)(record) && (0,verification_record/* validateVerificationRecord */.LL)(record).ok;
1068
+ rule('record-valid', recordOk, recordOk ? 'the verification record validates' : 'the verification record is missing or invalid');
1069
+ const bound = issued && recordOk && receipt.recordId === record.id && receipt.request.hypothesisId === hypothesisId && record.hypothesisId === hypothesisId;
1070
+ rule('receipt-binds-hypothesis', bound, bound ? 'the receipt, the record and the hypothesis name one another' : 'the receipt or record is for a different hypothesis, or they do not reference each other');
1071
+ const commit = issued ? receipt.request.commit : null;
1072
+ rule('commit-bound', !!commit && (!policy.expectedCommit || policy.expectedCommit === commit) && recordOk && record.commit === commit,
1073
+ commit ? (policy.expectedCommit && policy.expectedCommit !== commit ? `the receipt is for a different commit than the policy expects` : 'the receipt is bound to one exact commit') : 'the receipt is not bound to an exact commit');
1074
+
1075
+ const outcome = bound ? receipt.settled.outcome : null;
1076
+ rule('outcome-confirmed', outcome === 'confirmed', `the verifier settled '${outcome ?? 'nothing'}'`);
1077
+ const runtime = bound && record.confirmationLevel === 'runtime-confirmed'
1078
+ && record.evidence.some((e) => e.kind === 'trusted-runtime-proof' && e.producer === 'trusted-runner' && record.evidenceRefs.includes(e.id));
1079
+ rule('runtime-confirmed', runtime, runtime ? 'trusted runtime proof from the trusted runner is referenced' : 'no referenced trusted runtime proof at the runtime-confirmed level');
1080
+ const classOk = bound && policy.allowedOracleClasses.includes(receipt.oracle.class);
1081
+ rule('class-allowed', classOk, classOk ? `oracle class '${receipt.oracle.class}' is allowed` : 'the oracle class is not allowed by the policy');
1082
+
1083
+ // producer and verifier must be different parties
1084
+ const probe = { ...hypothesis };
1085
+ let sep = { ok: false, reason: 'no receipt' };
1086
+ if (bound) {
1087
+ const stamped = (0,verification_separation/* recordProducer */.Ms)(probe, `hunter:${hypothesis?.discovery?.lens ?? 'unknown'}`);
1088
+ sep = stamped.ok ? (0,verification_separation/* assertSeparation */.jm)(probe, `verifier:oracle:${receipt.oracle.id}`) : stamped;
1089
+ }
1090
+ rule('producer-verifier-separation', sep.ok, sep.ok ? 'the hunter and the verifying oracle are different parties' : String(sep.reason));
1091
+
1092
+ // additive-only rules: they can add a requirement, never satisfy one
1093
+ if (policy.minConfidence !== undefined) {
1094
+ const c = typeof hypothesis?.confidence === 'number' ? hypothesis.confidence : null;
1095
+ rule('min-confidence', c !== null && c >= policy.minConfidence, `an additional requirement only: confidence ${c ?? 'absent'} against ${policy.minConfidence}`);
1096
+ }
1097
+ if (policy.minAgreement !== undefined) {
1098
+ const a = agreementOf(hypothesis);
1099
+ rule('min-agreement', a !== null && a >= policy.minAgreement, `an additional requirement only: panel agreement ${a ?? 'absent'} against ${policy.minAgreement}`);
1100
+ }
1101
+
1102
+ const ok = rules.every((r) => r.pass);
1103
+ let status = 'promoted';
1104
+ if (!ok) {
1105
+ const settled = bound ? outcome : ((0,schema_kit/* isPlainObject */.Qd)(record) && typeof record.outcome === 'string' ? record.outcome : null);
1106
+ if (settled === 'unsupported') status = 'unsupported';
1107
+ else if (['inconclusive', 'not-run', 'error'].includes(settled)) status = 'inconclusive';
1108
+ else status = 'rejected';
1109
+ }
1110
+ return { ok, rules, status, policyDigest, record: recordOk ? record : null };
1111
+ }
1112
+
1113
+ const capSeverity = (sev, max) => (SEVERITY_ORDER.indexOf(sev) > SEVERITY_ORDER.indexOf(max) ? max : (SEVERITY_ORDER.includes(sev) ? sev : max));
1114
+
1115
+ /**
1116
+ * Promote one hypothesis, or decline to. Never throws.
1117
+ *
1118
+ * @param {object} args
1119
+ * @param {object} args.hypothesis a hunt finding (`discovery/judge.js` shape)
1120
+ * @param {object} args.result the `runOracle` result for it (`{ record, receipt, outcome }`)
1121
+ * @param {object} args.policy an explicit policy (see DEFAULT_PROMOTION_POLICY)
1122
+ * @returns {{ ok: boolean, decision: string, finding: object|null, audit: object, evaluation: object }}
1123
+ */
1124
+ function promoteHypothesis({ hypothesis, result, policy } = {}) {
1125
+ let evaluation;
1126
+ try { evaluation = evaluatePromotionPolicy({ hypothesis, result, policy }); }
1127
+ catch (e) { evaluation = { ok: false, status: 'rejected', policyDigest: null, record: null, rules: [{ id: 'evaluation', pass: false, detail: `the policy evaluation failed: ${String(e?.message || e).slice(0, 120)}` }] }; }
1128
+ const hypothesisId = (0,schema_kit/* isPlainObject */.Qd)(hypothesis) ? (0,identity/* hypothesisIdFromFinding */.XC)(hypothesis) : null;
1129
+ const decision = evaluation.ok ? 'promoted' : evaluation.status;
1130
+
1131
+ let finding = null;
1132
+ if (evaluation.ok) {
1133
+ const h = hypothesis;
1134
+ const base = {
1135
+ id: `promoted-${hypothesisId}`, stableId: hypothesisId,
1136
+ severity: capSeverity(h.severity, policy.maxSeverity),
1137
+ file: h.file, line: h.line, vuln: h.vuln, cwe: h.cwe, description: h.description, remediation: h.remediation,
1138
+ // A promoted finding is no longer a hunt hypothesis: it carries neither the hunt parser nor the `discovery` stamp, so the
1139
+ // advisory exclusion does not apply to it, and its origin lives in `promotedFrom`.
1140
+ parser: 'PROMOTED-HYPOTHESIS', family: h.family ?? 'other', ruleId: h.ruleId ?? 'promoted-hypothesis', snippet: h.snippet ?? '',
1141
+ };
1142
+ finding = { ...base, verificationRecord: JSON.parse(JSON.stringify(evaluation.record)) };
1143
+ }
1144
+
1145
+ const audit = {
1146
+ schema: AUDIT_SCHEMA, schemaVersion: AUDIT_VERSION, decision,
1147
+ sourceHypothesis: {
1148
+ id: hypothesisId, digest: (0,identity/* digestOf */.ol)((0,schema_kit/* isPlainObject */.Qd)(hypothesis) ? hypothesis : null),
1149
+ origin: { lens: hypothesis?.discovery?.lens ?? null, parser: hypothesis?.parser ?? null, family: hypothesis?.family ?? null },
1150
+ },
1151
+ promotedFinding: finding ? { stableId: finding.stableId, id: finding.id } : null,
1152
+ verification: {
1153
+ recordId: evaluation.record?.id ?? null, receiptDigest: (0,oracle/* isIssuedReceipt */.AH)(result?.receipt) ? result.receipt.receiptDigest : null,
1154
+ oracle: evaluation.record?.oracle ?? null, outcome: evaluation.record?.outcome ?? null,
1155
+ confirmationLevel: evaluation.record?.confirmationLevel ?? null, commit: evaluation.record?.commit ?? null,
1156
+ },
1157
+ policy: { id: (0,schema_kit/* isPlainObject */.Qd)(policy) ? policy.id ?? null : null, version: (0,schema_kit/* isPlainObject */.Qd)(policy) ? policy.version ?? null : null, digest: evaluation.policyDigest, rules: evaluation.rules },
1158
+ };
1159
+ audit.id = (0,identity/* semanticId */.YN)('padt', audit, ['decision', 'sourceHypothesis', 'promotedFinding', 'verification', 'policy']);
1160
+ deepFreeze(audit);
1161
+ if (finding) {
1162
+ finding.promotedFrom = { hypothesisId, auditId: audit.id, lens: hypothesis?.discovery?.lens ?? null };
1163
+ deepFreeze(finding);
1164
+ PROMOTED.add(finding);
1165
+ }
1166
+ return { ok: !!finding, decision, finding, audit, evaluation };
1167
+ }
1168
+
1169
+ /** Do an audit record, the hypothesis it names and the promoted finding it names agree? (The link, checked from both ends.) */
1170
+ function auditLinksHold({ audit, hypothesis, finding }) {
1171
+ if (!isPlainObject(audit) || audit.schema !== AUDIT_SCHEMA) return false;
1172
+ const hid = isPlainObject(hypothesis) ? hypothesisIdFromFinding(hypothesis) : null;
1173
+ if (!hid || audit.sourceHypothesis?.id !== hid || audit.sourceHypothesis?.digest !== digestOf(hypothesis)) return false;
1174
+ if (audit.id !== semanticId('padt', audit, ['decision', 'sourceHypothesis', 'promotedFinding', 'verification', 'policy'])) return false;
1175
+ if (audit.decision !== 'promoted') return finding == null && audit.promotedFinding === null;
1176
+ return isPromotedFinding(finding) && finding.promotedFrom?.auditId === audit.id && finding.promotedFrom?.hypothesisId === hid
1177
+ && audit.promotedFinding?.stableId === finding.stableId && finding.verificationRecord?.id === audit.verification.recordId;
1178
+ }
1179
+
1180
+ function deepFreeze(o) {
1181
+ for (const v of Object.values(o)) if (v && typeof v === 'object' && !Object.isFrozen(v)) deepFreeze(v);
1182
+ return Object.freeze(o);
1183
+ }
1184
+
956
1185
  ;// CONCATENATED MODULE: ./src/discovery/index.js
957
1186
  //
958
1187
  // Compose the discovery pipeline:
@@ -971,6 +1200,8 @@ function forgetRefuted(memory) {
971
1200
 
972
1201
 
973
1202
 
1203
+
1204
+
974
1205
  // Bridge a candidate to the deterministic layer. A taint finding at or within
975
1206
  // two lines of the candidate corroborates it; a modelled sink on the line
976
1207
  // without a full path is weaker corroboration ("sink-adjacent").
@@ -1239,11 +1470,37 @@ async function runDiscovery(ctx = {}, opts = {}) {
1239
1470
  }));
1240
1471
  }
1241
1472
 
1473
+ // X-201: one version-1 verification record per fresh hypothesis, ADDITIVE and advisory (hunt output never enters
1474
+ // last-scan.json). A taint-corroborated candidate is `inconclusive` (a static probe is corroboration, not
1475
+ // execution) and an uncorroborated one is `not-run`; a model refutation panel is recorded as inference only.
1476
+ const hypothesisCommit = (0,emit.headCommit)(opts.scanRoot);
1477
+ const verificationRecords = [];
1478
+ for (const f of fresh) {
1479
+ const e = (0,emit/* emitVerification */.T)('hunt', f, { finding: f, commit: hypothesisCommit });
1480
+ if (e.ok) verificationRecords.push(e.record);
1481
+ }
1482
+
1483
+ // X-207: opt-in promotion. A hypothesis crosses into a finding ONLY through `promoteHypothesis` (trusted verifier receipt,
1484
+ // explicit policy, audit record). `fresh` is unchanged; promoted findings and every decision's audit record are reported
1485
+ // separately, and nothing here writes `last-scan.json`. `opts.promote = { policy, verify(hypothesis) -> runOracle result }`.
1486
+ let promotions = null;
1487
+ if (opts.promote && typeof opts.promote.verify === 'function') {
1488
+ promotions = [];
1489
+ for (const f of fresh) {
1490
+ let result = null;
1491
+ try { result = await opts.promote.verify(f); } catch { result = null; }
1492
+ const p = promoteHypothesis({ hypothesis: f, result, policy: opts.promote.policy });
1493
+ promotions.push({ hypothesisId: p.audit.sourceHypothesis.id, decision: p.decision, audit: p.audit, finding: p.finding });
1494
+ }
1495
+ }
1496
+
1242
1497
  return {
1243
1498
  schema: 'agentic-security/discovery@1',
1244
1499
  focusAreas: areas.map(a => ({ id: a.id, label: a.label, files: a.files.length, size: a.size })),
1245
1500
  runs,
1246
1501
  fresh,
1502
+ verificationRecords,
1503
+ ...(promotions ? { promotions } : {}),
1247
1504
  duplicates,
1248
1505
  suppressed,
1249
1506
  // `refutedCandidates` holds RAW candidates straight from `disprovePanel`,