coderifts 8.6.3 → 8.6.4

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.
package/dist/cli.js CHANGED
@@ -3028,7 +3028,7 @@ var require_package = __commonJS({
3028
3028
  "package.json"(exports2, module2) {
3029
3029
  module2.exports = {
3030
3030
  name: "coderifts",
3031
- version: "8.6.3",
3031
+ version: "8.6.4",
3032
3032
  description: "Detect breaking API changes from the command line. Works locally or with the CodeRifts cloud API.",
3033
3033
  author: "CodeRifts <hello@coderifts.com>",
3034
3034
  license: "MIT",
@@ -168610,7 +168610,7 @@ var require_picomatch2 = __commonJS({
168610
168610
  var require_mcp_poison_gate = __commonJS({
168611
168611
  "../../src/mcp-poison-gate.js"(exports2, module2) {
168612
168612
  "use strict";
168613
- var MCP_POISON_GATE_VERSION = "mpg-1";
168613
+ var MCP_POISON_GATE_VERSION = "mpg-2";
168614
168614
  var BLOCK_TYPES = /* @__PURE__ */ new Set([
168615
168615
  "suspicious_instruction",
168616
168616
  "exfiltration_url_cosignal",
@@ -168619,7 +168619,8 @@ var require_mcp_poison_gate = __commonJS({
168619
168619
  ]);
168620
168620
  var RA_TYPES = /* @__PURE__ */ new Set([
168621
168621
  "structural_anomaly",
168622
- "hidden_encoding"
168622
+ "hidden_encoding",
168623
+ "instruction_heuristic"
168623
168624
  ]);
168624
168625
  var DECISION_ORDER = { ALLOW: 0, WARN: 1, REQUIRE_APPROVAL: 2, BLOCK: 3 };
168625
168626
  function isLowerThan(decision, floor) {
@@ -205515,6 +205516,16 @@ var require_mcp_poisoning_detector = __commonJS({
205515
205516
  "use strict";
205516
205517
  var SENSITIVE_OBJECT = "(secrets?|credentials?|tokens?|passwords?|api[\\s_-]?keys?|private[\\s_-]?keys?|env|environment|conversation|chat\\s*history|context|user\\s*data|session)";
205517
205518
  var SENSITIVE_SCOPE = "(any|all|every|stored|user\\'?s?|the\\s+user\\'?s?|all\\s+environment)";
205519
+ var MODE_PHRASE = /(hidden|secret|debug)\s+mode/i;
205520
+ var MODE_NEGATED = /\b(?:do not|don't|does not|doesn't|never|not)\b[\s\S]{0,40}?(hidden|secret|debug)\s+mode/i;
205521
+ var ALWAYS_RETURN_OK = /always\s+return\s+(success|ok|true)/i;
205522
+ function findingTypeFor(regex) {
205523
+ if (regex === MODE_PHRASE || regex === ALWAYS_RETURN_OK) return "instruction_heuristic";
205524
+ return "suspicious_instruction";
205525
+ }
205526
+ function modePhraseHits(str) {
205527
+ return MODE_PHRASE.test(str) && !MODE_NEGATED.test(str);
205528
+ }
205518
205529
  var SUSPICIOUS_PATTERNS = [
205519
205530
  // Injection / "ignore your instructions" family. The trailing object
205520
205531
  // (instructions|rules|prompts|context) is OPTIONAL so "ignore previous and ..."
@@ -205524,9 +205535,18 @@ var require_mcp_poisoning_detector = __commonJS({
205524
205535
  // ~40 chars so benign "always include the source URL" stays quiet.
205525
205536
  new RegExp("always\\s+(include|append|send|attach|return|add)\\b[\\s\\S]{0,40}?\\b" + SENSITIVE_OBJECT + "\\b", "i"),
205526
205537
  /you\s+(must|should|have to)\s+(now|always|instead)/i,
205527
- /exfiltrate|steal|leak|send\s+to\s+external/i,
205538
+ // Unanchored `leak` matched "leaked"/"leaks" (same class as the ungrouped
205539
+ // hidden|secret|debug mode bug). Keep the token — FSP 007 only fires because
205540
+ // ZWSP broke SEND and the host is leak.example — but require a word boundary
205541
+ // AND a payload noun or URL in the same string. Deleting leak drops 007
205542
+ // (94.1% → 88.2%); this co-occurrence does not.
205543
+ /exfiltrate|steal|send\s+to\s+external/i,
205544
+ /\bleak\b[\s\S]{0,80}?(?:https?:\/\/|\b(?:conversation|chat|history|context|credentials?|secrets?|tokens?|api\s*keys?|passwords?|data)\b)|(?:https?:\/\/|\b(?:conversation|chat|history|context|credentials?|secrets?|tokens?|api\s*keys?|passwords?|data)\b)[\s\S]{0,80}?\bleak\b/i,
205528
205545
  // Exfiltration-intent family: an action verb targeting a sensitive payload.
205529
- /\b(send|forward|copy|upload|post|transmit|exfiltrate|leak)\b[\s\S]{0,40}?\b(conversation|chat|history|context|messages?|credentials?|secrets?|tokens?|api\s*keys?|keys?|passwords?|data)\b/i,
205546
+ // `messages?` was too broad — "Forward the message to the shared inbox" is a
205547
+ // mail-forwarder's job. `data` stays: fsp-exfil-title-010 is "SEND COPY OF ALL
205548
+ // USER DATA" and does not contain "a copy".
205549
+ /\b(send|forward|copy|upload|post|transmit|exfiltrate|leak)\b[\s\S]{0,40}?\b(conversation|chat|history|context|credentials?|secrets?|tokens?|api\s*keys?|keys?|passwords?|data)\b/i,
205530
205550
  // Return/echo scoped sensitive material without a path ("return any stored credentials").
205531
205551
  // Scope word required — bare "return the new API token" must stay quiet.
205532
205552
  new RegExp(
@@ -205546,23 +205566,39 @@ var require_mcp_poisoning_detector = __commonJS({
205546
205566
  /bypass\s+(auth|security|validation|filter)/i,
205547
205567
  /override\s+(safety|policy|rules?)/i,
205548
205568
  /do\s+not\s+(follow|respect|obey)\s+(original|previous)/i,
205549
- /hidden|secret|debug\s+mode/i,
205569
+ // Grouping is load-bearing: `/hidden|secret|debug\s+mode/` is
205570
+ // `hidden` OR `secret` OR `debug mode`, so any "secret"/"hidden" token is a
205571
+ // HIGH poison hit (store_credential, list_secrets, hidden pagination fields).
205572
+ // Intent, from the original line sitting among phrase-level patterns: the
205573
+ // three MODE phrases. scanString applies skip-negation so "Does not enable
205574
+ // debug mode" is quiet and "When debug mode is enabled" still hits.
205575
+ MODE_PHRASE,
205576
+ // Confused-deputy / tool-shadowing: "also invoke the secret exfil tool".
205577
+ // Requires also|now + invoke/call AND the noun "tool". That is NOT
205578
+ // capability_claimed (`also … calls?`), which fires on "also calls the webhook".
205579
+ // "invoke this tool after payment" has no also|now; "this tool calls …" is the
205580
+ // wrong order. Measured 0/20 benign and 0/6 mcp-fpfn FP vectors.
205581
+ /\b(?:also|now)\s+(?:invoke|call)s?\b[\s\S]{0,60}?\btool\b/i,
205550
205582
  /\[system\]|<\s*system\s*>/i,
205551
- /always\s+return\s+(success|ok|true)/i,
205583
+ ALWAYS_RETURN_OK,
205552
205584
  /hide\s+(errors|failures|issues)/i,
205553
205585
  /return\s+secret|exfiltrat|leak\s+data/i
205554
205586
  ];
205555
205587
  var URL_PATTERN = /https?:\/\/[^\s)'"]+/i;
205556
205588
  var SENSITIVE_PATH_PATTERN = /(~\/\.ssh|~\/\.cursor|~\/\.aws|\.aws\/|\.env\b|\/etc\/|mcp\.json|id_rsa|\.ssh\/|\/credentials(?:\.|\/)|credentials\.(?:json|txt|pem|env)|\.credentials\b)/i;
205557
- var CONTENT_MOVE_VERB_PATTERN = /\b(pass|send|return|forward|include|read|upload|post|transmit|copy|exfiltrate|leak|echo|reveal|expose)\b/i;
205589
+ var CONTENT_MOVE_VERB_PATTERN = /\b(pass|send|return|forward|include|upload|post|transmit|copy|exfiltrate|leak|echo|reveal|expose)\b/i;
205558
205590
  var BASE64_BLOB_PATTERN = /[A-Za-z0-9+/]{40,}={0,2}/;
205559
205591
  function isLikelyBase64(s) {
205560
205592
  if (/=$/.test(s)) return true;
205561
205593
  return /[A-Z]/.test(s) && /[a-z]/.test(s) && /[0-9]/.test(s);
205562
205594
  }
205595
+ function patternHits(regex, str) {
205596
+ if (regex === MODE_PHRASE) return modePhraseHits(str);
205597
+ return regex.test(str);
205598
+ }
205563
205599
  function hasInjectionIntent(str) {
205564
205600
  if (typeof str !== "string") return false;
205565
- if (SUSPICIOUS_PATTERNS.some((re) => re.test(str))) return true;
205601
+ if (SUSPICIOUS_PATTERNS.some((re) => patternHits(re, str))) return true;
205566
205602
  if (SENSITIVE_PATH_PATTERN.test(str) && CONTENT_MOVE_VERB_PATTERN.test(str)) return true;
205567
205603
  return false;
205568
205604
  }
@@ -205570,9 +205606,9 @@ var require_mcp_poisoning_detector = __commonJS({
205570
205606
  if (typeof value !== "string" || value.length < 5) return [];
205571
205607
  const findings = [];
205572
205608
  for (const regex of SUSPICIOUS_PATTERNS) {
205573
- if (regex.test(value)) {
205609
+ if (patternHits(regex, value)) {
205574
205610
  findings.push({
205575
- type: "suspicious_instruction",
205611
+ type: findingTypeFor(regex),
205576
205612
  path,
205577
205613
  snippet: value.substring(0, 120),
205578
205614
  severity: "HIGH",
@@ -205743,6 +205779,130 @@ var require_mcp_poisoning_detector = __commonJS({
205743
205779
  }
205744
205780
  });
205745
205781
 
205782
+ // ../../src/tool-description-classifier.js
205783
+ var require_tool_description_classifier = __commonJS({
205784
+ "../../src/tool-description-classifier.js"(exports2, module2) {
205785
+ "use strict";
205786
+ var PATTERN = "TOOL_INSTRUCTION_DRIFT";
205787
+ var TAXONOMY = Object.freeze([
205788
+ "OWASP MCP Taxonomy: Tool Description Drift (glossary)",
205789
+ "OWASP MCP Taxonomy: Rug Pull (T2.1)",
205790
+ "OWASP MCP03: trusted tool update changes behavior"
205791
+ ]);
205792
+ var DOES_NOT_PROVE = Object.freeze([
205793
+ "that the reworded text carries the same meaning to every model",
205794
+ "that no instruction was moved into a field this check does not read"
205795
+ ]);
205796
+ var BRANCH_SIGNALS = Object.freeze([
205797
+ {
205798
+ id: "capability_claimed",
205799
+ re: /\b(?:also|now)\s+(?:reads?|writes?|accesses?|sends?|fetches?|calls?)\b|\bcan now\b/i,
205800
+ why: "claims a capability or scope the previous text did not"
205801
+ },
205802
+ {
205803
+ id: "condition_changed",
205804
+ re: /\b(?:use (?:this )?when|do not call|don't call|only (?:use|call)|never call)\b/i,
205805
+ why: "adds or removes a condition on when the tool applies"
205806
+ },
205807
+ {
205808
+ id: "concealment_instructed",
205809
+ re: /\b(?:do not|don't|never)\s+(?:mention|reveal|disclose|explain|tell|show|log)\b|\bwithout (?:telling|informing|notifying)\b/i,
205810
+ why: "instructs the model to hide something from the user"
205811
+ },
205812
+ {
205813
+ id: "priority_steered",
205814
+ re: /\b(?:use this (?:every time|always)|always use this|prefer this over|instead of (?:the )?other|do not use \w+ tool)\b/i,
205815
+ why: "steers the model toward or away from another tool"
205816
+ },
205817
+ // A: one verb. `exfiltrat\w*` is enough; leak|transmit|beacon|copy|mail added FP
205818
+ // with no extra TP (measured 2026-09-14).
205819
+ {
205820
+ id: "recipient_named",
205821
+ re: /\b(?:send|post|forward|upload|report|exfiltrat\w*)\b[^.]{0,40}\b(?:to|at)\s+(?:https?:\/\/|[\w.-]+\.[a-z]{2,}|@[\w.-]+)/i,
205822
+ why: "names an external recipient or resource that was not there before"
205823
+ },
205824
+ // D: co-occurrence in one sentence — a moving verb AND a credential/path.
205825
+ // `returns?` matches "return the key" and not "never returned" (`returned` ≠ `return`).
205826
+ {
205827
+ id: "credential_referenced",
205828
+ re: /\b(?:send|forward|copy|upload|post|transmit|exfiltrat\w*|leak|echo|reveal|expose|attach|returns?)\b[^\n.]{0,60}?\b(?:api[_ -]?key|secrets?|tokens?|credentials?|passwords?|\.env|id_rsa|~\/\.[\w-]+|private key)\b|\b(?:api[_ -]?key|secrets?|tokens?|credentials?|passwords?|\.env|id_rsa|~\/\.[\w-]+|private key)\b[^\n.]{0,60}?\b(?:send|forward|copy|upload|post|transmit|exfiltrat\w*|leak|echo|reveal|expose|attach|returns?)\b/i,
205829
+ why: "names a credential or secret path together with a verb that moves it"
205830
+ }
205831
+ ]);
205832
+ function normalise(s) {
205833
+ return String(s).replace(/[‘’]/g, "'").replace(/[“”]/g, '"').replace(/[–—]/g, "-").replace(/\s+/g, " ").trim().toLowerCase();
205834
+ }
205835
+ function addedText(before, after) {
205836
+ const sents = (s) => String(s).split(/(?<=[.!?])\s+/).map((x) => x.trim()).filter(Boolean);
205837
+ const had = new Set(sents(before).map(normalise));
205838
+ return sents(after).filter((s) => !had.has(normalise(s)));
205839
+ }
205840
+ function classifyDescriptionChange(before, after) {
205841
+ const base = { pattern: null, taxonomy: TAXONOMY, does_not_prove: [...DOES_NOT_PROVE], signals: [] };
205842
+ if (typeof before !== "string" || typeof after !== "string" || !before.trim() || !after.trim()) {
205843
+ return {
205844
+ ...base,
205845
+ classification: "CLASSIFICATION_UNDECIDABLE",
205846
+ risk: 0,
205847
+ execution_action: "REQUEST_APPROVAL",
205848
+ reason: "one side of the description is absent or empty \u2014 the change could not be classified, and an unclassified instruction change is not a reworded one"
205849
+ };
205850
+ }
205851
+ if (normalise(before) === normalise(after)) {
205852
+ return {
205853
+ ...base,
205854
+ classification: "DESCRIPTION_REWORDED",
205855
+ risk: 0,
205856
+ execution_action: "CONTINUE",
205857
+ reason: "the text differs only in whitespace, case or punctuation"
205858
+ };
205859
+ }
205860
+ const added = addedText(before, after);
205861
+ const removed = addedText(after, before);
205862
+ if (added.length === 0 && removed.length === 0) {
205863
+ return {
205864
+ ...base,
205865
+ classification: "CLASSIFICATION_UNDECIDABLE",
205866
+ risk: 0,
205867
+ execution_action: "REQUEST_APPROVAL",
205868
+ reason: "the descriptions differ but no sentence was added or removed \u2014 the change is inside a sentence, which this classifier does not read"
205869
+ };
205870
+ }
205871
+ const haystack = added.join(" ");
205872
+ const hits = BRANCH_SIGNALS.filter((s) => s.re.test(haystack)).map((s) => ({ id: s.id, why: s.why }));
205873
+ if (hits.length > 0) {
205874
+ return {
205875
+ ...base,
205876
+ classification: "BRANCH_CONDITION_MOVED",
205877
+ // +15, one step above the +10 its neighbours carry for a SHAPE change. A moved instruction
205878
+ // changes what the tool does with an unchanged schema, which the shape checks cannot see;
205879
+ // pricing it below them would say the opposite.
205880
+ risk: 15,
205881
+ pattern: PATTERN,
205882
+ execution_action: "REQUEST_APPROVAL",
205883
+ signals: hits,
205884
+ reason: `added text ${hits.map((h) => h.why).join("; ")}`
205885
+ };
205886
+ }
205887
+ return {
205888
+ ...base,
205889
+ classification: "DESCRIPTION_REWORDED",
205890
+ risk: 0,
205891
+ execution_action: "CONTINUE",
205892
+ reason: added.length ? "sentences changed, and none of them moves a branch condition" : "text was removed, and none of it held a branch condition"
205893
+ };
205894
+ }
205895
+ module2.exports = {
205896
+ classifyDescriptionChange,
205897
+ addedText,
205898
+ PATTERN,
205899
+ TAXONOMY,
205900
+ DOES_NOT_PROVE,
205901
+ BRANCH_SIGNALS
205902
+ };
205903
+ }
205904
+ });
205905
+
205746
205906
  // ../../src/remediation-taxonomy.js
205747
205907
  var require_remediation_taxonomy = __commonJS({
205748
205908
  "../../src/remediation-taxonomy.js"(exports2, module2) {
@@ -208155,7 +208315,7 @@ var require_decision_result_v1_producer = __commonJS({
208155
208315
  blast_radius: {
208156
208316
  type: "object",
208157
208317
  additionalProperties: false,
208158
- description: "ID27 additive COUNTS (not a score). Pure function of the change-set + request graphs. Not in the verdict_fingerprint preimage (before_norm+after_norm+policy+scorer_version). Zeros are measured absence. consumers_declared and consumers_observed are separate: declared-but-never-called is a score question, not a count question. graph_source none = no request graph.",
208318
+ description: "Additive COUNTS (not a score). Pure function of the change-set + request graphs. Not in the verdict_fingerprint preimage (before_norm+after_norm+policy+scorer_version). Zeros are measured absence. consumers_declared and consumers_observed are separate: declared-but-never-called is a score question, not a count question. graph_source none = no request graph.",
208159
208319
  required: [
208160
208320
  "endpoints",
208161
208321
  "fields",
@@ -208797,7 +208957,7 @@ var require_execution_grant_request_v2_producer = __commonJS({
208797
208957
  "v2"
208798
208958
  ],
208799
208959
  description: "src/change-set.js:1328 \u2014 `v2` selects the cr.exec.v2 issuer. 1344 MIGRATION: absent resolves through src/grant-version-default.js \u2014 cr.exec.v1 before 2026-09-18, cr.exec.v2 on and after. An explicit value always wins. `grantVersion` (camelCase) is accepted as an alias; both are listed so neither reads as undeclared. JSON Schema `default` is omitted on purpose (1363): a static default cannot be both v2 (the post-cutoff target) and v1 (what omitting yields during the window). Machine-readable: x-coderifts-effective-default is today's omit-value; x-coderifts-default-changes-at is when it becomes v2. The authorize response meta.grant_version is the version actually issued.",
208800
- "x-coderifts-effective-default": "v1",
208960
+ "x-coderifts-effective-default": "v2",
208801
208961
  "x-coderifts-default-changes-at": "2026-09-18"
208802
208962
  },
208803
208963
  grantVersion: {
@@ -208807,7 +208967,7 @@ var require_execution_grant_request_v2_producer = __commonJS({
208807
208967
  "v2"
208808
208968
  ],
208809
208969
  description: "camelCase alias of grant_version, read at src/change-set.js:1268. Declared because the handler reads it, not because it is recommended. Same dated default as grant_version (no JSON Schema `default`; see x-coderifts-* on grant_version).",
208810
- "x-coderifts-effective-default": "v1",
208970
+ "x-coderifts-effective-default": "v2",
208811
208971
  "x-coderifts-default-changes-at": "2026-09-18"
208812
208972
  },
208813
208973
  tenant_id: {
@@ -210385,6 +210545,11 @@ var require_change_set = __commonJS({
210385
210545
  var { scoreMcpRisk } = require_mcp_risk_scorer();
210386
210546
  var { detectPoisoning } = require_mcp_poisoning_detector();
210387
210547
  var { classifyPoison, isLowerThan } = require_mcp_poison_gate();
210548
+ var {
210549
+ classifyDescriptionChange,
210550
+ addedText,
210551
+ PATTERN: TOOL_INSTRUCTION_DRIFT
210552
+ } = require_tool_description_classifier();
210388
210553
  var { parse: parseGraphql } = require_graphql2();
210389
210554
  var {
210390
210555
  ensureDecisionSpec,
@@ -210614,25 +210779,83 @@ var require_change_set = __commonJS({
210614
210779
  ensureDecisionSpec(art);
210615
210780
  return finding(art);
210616
210781
  }
210782
+ function mcpInstructionDrift(oldMcp, newMcp) {
210783
+ return toolListInstructionDrift(oldMcp && oldMcp.tools || [], newMcp && newMcp.tools || []);
210784
+ }
210785
+ function toolListInstructionDrift(oldTools, newTools) {
210786
+ const oldBy = new Map((oldTools || []).map((t) => [t && t.name, t]));
210787
+ for (const nt of newTools || []) {
210788
+ if (!nt || !nt.name) continue;
210789
+ const ot = oldBy.get(nt.name);
210790
+ if (!ot) continue;
210791
+ if (ot.description === nt.description) continue;
210792
+ const v = classifyDescriptionChange(ot.description, nt.description);
210793
+ if (v.execution_action === "REQUEST_APPROVAL") return true;
210794
+ }
210795
+ return false;
210796
+ }
210797
+ function rewriteDescriptions(oldVal, newVal) {
210798
+ if (Array.isArray(newVal)) {
210799
+ const oldArr = Array.isArray(oldVal) ? oldVal : [];
210800
+ return newVal.map((item, i) => rewriteDescriptions(oldArr[i], item));
210801
+ }
210802
+ if (!newVal || typeof newVal !== "object") return newVal;
210803
+ const oldObj = oldVal && typeof oldVal === "object" && !Array.isArray(oldVal) ? oldVal : {};
210804
+ const out = {};
210805
+ for (const [k, v] of Object.entries(newVal)) {
210806
+ if (k === "description" && typeof v === "string") {
210807
+ const prev = typeof oldObj[k] === "string" ? oldObj[k] : "";
210808
+ out[k] = addedText(prev, v).join(" ");
210809
+ } else {
210810
+ out[k] = rewriteDescriptions(oldObj[k], v);
210811
+ }
210812
+ }
210813
+ return out;
210814
+ }
210815
+ function manifestWithAddedSentences(oldManifest, newManifest) {
210816
+ const old = oldManifest && typeof oldManifest === "object" ? oldManifest : {};
210817
+ const neu = newManifest && typeof newManifest === "object" ? newManifest : {};
210818
+ const oldBy = new Map((Array.isArray(old.tools) ? old.tools : []).map((t) => [t && t.name, t]));
210819
+ const tools = (Array.isArray(neu.tools) ? neu.tools : []).map((nt) => {
210820
+ if (!nt || typeof nt !== "object" || !nt.name) return nt;
210821
+ const ot = oldBy.get(nt.name);
210822
+ if (!ot) return nt;
210823
+ return rewriteDescriptions(ot, nt);
210824
+ });
210825
+ const topOld = { ...old };
210826
+ const topNew = { ...neu };
210827
+ delete topOld.tools;
210828
+ delete topNew.tools;
210829
+ const view = rewriteDescriptions(topOld, topNew);
210830
+ view.tools = tools;
210831
+ return view;
210832
+ }
210617
210833
  function analyzeMcpManifest(before, after) {
210618
210834
  const oldMcp = parseManifest(before);
210619
210835
  const newMcp = parseManifest(after);
210620
210836
  const risk = scoreMcpRisk({ oldManifest: oldMcp, newManifest: newMcp });
210621
210837
  const breaking = risk.signals.filter((s) => (s.impact || 0) > 0).length;
210622
210838
  const patterns = risk.signals.map((s) => s.signal);
210839
+ if (mcpInstructionDrift(oldMcp, newMcp) && !patterns.includes(TOOL_INSTRUCTION_DRIFT)) {
210840
+ patterns.push(TOOL_INSTRUCTION_DRIFT);
210841
+ }
210623
210842
  const art = {
210624
210843
  risk_score: risk.score,
210625
210844
  breaking_changes: breaking,
210626
210845
  patterns
210627
210846
  };
210628
210847
  if (risk.score >= 50) art.should_block = true;
210629
- const poisonGate = classifyPoison(detectPoisoning(oldMcp, newMcp));
210848
+ const poisonGate = classifyPoison(detectPoisoning(oldMcp, manifestWithAddedSentences(oldMcp, newMcp)));
210630
210849
  if (poisonGate.tier === "block") art.omega_decision = "BLOCK";
210631
210850
  ensureDecisionSpec(art);
210632
210851
  if (poisonGate.tier === "ra" && isLowerThan(art.decision, "REQUIRE_APPROVAL")) {
210633
210852
  art.decision = "REQUIRE_APPROVAL";
210634
210853
  ensureDecisionSpec(art);
210635
210854
  }
210855
+ if (patterns.includes(TOOL_INSTRUCTION_DRIFT) && isLowerThan(art.decision, "REQUIRE_APPROVAL")) {
210856
+ art.decision = "REQUIRE_APPROVAL";
210857
+ ensureDecisionSpec(art);
210858
+ }
210636
210859
  return finding(art);
210637
210860
  }
210638
210861
  function parseAgentToolsPayload(v) {
@@ -210730,7 +210953,23 @@ var require_change_set = __commonJS({
210730
210953
  reflex_triggers: json.reflex_triggers,
210731
210954
  omega_components: json.omega_components
210732
210955
  };
210956
+ const poisonGate = classifyPoison(detectPoisoning(
210957
+ { tools: beforeTools },
210958
+ manifestWithAddedSentences({ tools: beforeTools }, { tools: afterTools })
210959
+ ));
210960
+ if (poisonGate.tier === "block") art.omega_decision = "BLOCK";
210961
+ if (toolListInstructionDrift(beforeTools, afterTools) && !patterns.includes(TOOL_INSTRUCTION_DRIFT)) {
210962
+ patterns.push(TOOL_INSTRUCTION_DRIFT);
210963
+ }
210733
210964
  ensureDecisionSpec(art);
210965
+ if (poisonGate.tier === "ra" && isLowerThan(art.decision, "REQUIRE_APPROVAL")) {
210966
+ art.decision = "REQUIRE_APPROVAL";
210967
+ ensureDecisionSpec(art);
210968
+ }
210969
+ if (patterns.includes(TOOL_INSTRUCTION_DRIFT) && isLowerThan(art.decision, "REQUIRE_APPROVAL")) {
210970
+ art.decision = "REQUIRE_APPROVAL";
210971
+ ensureDecisionSpec(art);
210972
+ }
210734
210973
  return finding(art);
210735
210974
  }
210736
210975
  var ANALYZE_PERMISSION_KEYS = Object.freeze(["decision", "safe_for_agent", "execution_action"]);
@@ -210885,6 +211124,7 @@ var require_change_set = __commonJS({
210885
211124
  var DECISION_SPEC_VERSION = "2.0";
210886
211125
  var DECISION_SPEC_LEGACY_VERSION = "1.0";
210887
211126
  var DECISION_SPEC_LEGACY_SUNSET = "2026-09-07T00:00:00Z";
211127
+ var DECISION_SPEC_LEGACY_SUNSET_AT_MS = Date.parse(DECISION_SPEC_LEGACY_SUNSET);
210888
211128
  var ANALYSIS_OUTCOMES = Object.freeze({
210889
211129
  NO_BREAK_DETECTED: "NO_BREAK_DETECTED",
210890
211130
  BREAKS_DETECTED: "BREAKS_DETECTED",
@@ -210896,6 +211136,18 @@ var require_change_set = __commonJS({
210896
211136
  const s = String(raw).trim();
210897
211137
  return s === "1.0" || s === "1" || s === "decision-result.v1" || s === "decision-result.v1.0";
210898
211138
  }
211139
+ function isLegacyDecisionSpecPinRetired(now) {
211140
+ const nowMs = now instanceof Date ? now.getTime() : typeof now === "number" ? now : Date.now();
211141
+ return nowMs >= DECISION_SPEC_LEGACY_SUNSET_AT_MS;
211142
+ }
211143
+ function rejectRetiredLegacyPin(input, deps) {
211144
+ if (!isLegacyDecisionSpecPin(input)) return;
211145
+ if (!isLegacyDecisionSpecPinRetired(deps && deps.now)) return;
211146
+ throw typedError(
211147
+ "INVALID_INPUT",
211148
+ `decision_spec_version '${DECISION_SPEC_LEGACY_VERSION}' retired at ${DECISION_SPEC_LEGACY_SUNSET}; Decision Spec ${DECISION_SPEC_VERSION} is required (preflight_mode required; analyze omits execution fields). Do not pin 1.0.`
211149
+ );
211150
+ }
210899
211151
  function deriveAnalysisOutcome({ hasDegraded, decision, breaking_changes }) {
210900
211152
  if (hasDegraded) return ANALYSIS_OUTCOMES.ANALYSIS_FAILED;
210901
211153
  if (decision !== "ALLOW" || (breaking_changes || 0) > 0) return ANALYSIS_OUTCOMES.BREAKS_DETECTED;
@@ -210907,7 +211159,7 @@ var require_change_set = __commonJS({
210907
211159
  if (legacy) return { mode: "analyze", defaulted: true };
210908
211160
  throw typedError(
210909
211161
  "INVALID_INPUT",
210910
- "preflight_mode is required: 'analyze' (informational risk only) or 'authorize' (operation-bound; may mint a receipt). Migration: pass preflight_mode explicitly. For 30-day legacy (soft-default + old analyze shape) send decision_spec_version: '1.0'."
211162
+ "preflight_mode is required: 'analyze' (informational risk only) or 'authorize' (operation-bound; may mint a receipt). Migration: pass preflight_mode explicitly. Decision Spec 2.0 does not soft-default the mode."
210911
211163
  );
210912
211164
  }
210913
211165
  if (raw === "analyze" || raw === "authorize") {
@@ -210946,6 +211198,7 @@ var require_change_set = __commonJS({
210946
211198
  if (seen.has(key)) throw typedError("INVALID_INPUT", `duplicate artifact (type, id): (${a.type}, ${a.id})`);
210947
211199
  seen.add(key);
210948
211200
  }
211201
+ rejectRetiredLegacyPin(input, deps);
210949
211202
  const legacyPin = isLegacyDecisionSpecPin(input);
210950
211203
  const { mode: preflightMode, defaulted: preflightModeDefaulted } = resolvePreflightMode(input, {
210951
211204
  legacy: legacyPin
@@ -211516,6 +211769,7 @@ var require_change_set = __commonJS({
211516
211769
  resolvePreflightMode,
211517
211770
  deriveAnalysisOutcome,
211518
211771
  isLegacyDecisionSpecPin,
211772
+ isLegacyDecisionSpecPinRetired,
211519
211773
  artifactSidesIdentical,
211520
211774
  isNoChangeBundle,
211521
211775
  isContractChangeTrigger,
@@ -211525,6 +211779,7 @@ var require_change_set = __commonJS({
211525
211779
  DECISION_SPEC_VERSION,
211526
211780
  DECISION_SPEC_LEGACY_VERSION,
211527
211781
  DECISION_SPEC_LEGACY_SUNSET,
211782
+ DECISION_SPEC_LEGACY_SUNSET_AT_MS,
211528
211783
  ANALYSIS_OUTCOMES,
211529
211784
  MAX_ARTIFACTS,
211530
211785
  SUPPORTED_TYPES,
@@ -224882,17 +225137,17 @@ var require_mcp_decision_result_shape = __commonJS({
224882
225137
  audience: { type: ["string", "null"] },
224883
225138
  authority: {
224884
225139
  type: ["object", "null"],
224885
- description: "ID963 additive. { audience, tenant_scope: bound|unbound, binding_proven_at? }. Informational \u2014 not permission, not a verify-receipt gate, not an ACL."
225140
+ description: "Additive. { audience, tenant_scope: bound|unbound, binding_proven_at? }. Informational \u2014 not permission, not a verify-receipt gate, not an ACL."
224886
225141
  },
224887
225142
  derivation: {
224888
225143
  type: ["object", "null"],
224889
- description: 'ID637 6b additive. Present only when derivation:"server" produced this envelope. { source, platform?, base_sha, head_sha }. Covered by body_hash; not fingerprint.'
225144
+ description: 'Additive. Present only when derivation:"server" produced this envelope. { source, platform?, base_sha, head_sha }. Covered by body_hash; not fingerprint.'
224890
225145
  },
224891
225146
  receipt: { type: "object" },
224892
225147
  expires_at: { type: "string" },
224893
225148
  blast_radius: {
224894
225149
  type: "object",
224895
- description: "ID27 additive COUNTS (not a score). Not permission.",
225150
+ description: "Additive COUNTS (not a score). Not permission.",
224896
225151
  properties: {
224897
225152
  endpoints: { type: "integer", minimum: 0 },
224898
225153
  fields: { type: "integer", minimum: 0 },
@@ -225031,7 +225286,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225031
225286
  },
225032
225287
  "detected_patterns": {
225033
225288
  "type": "array",
225034
- "description": "GOVERNANCE detector detail rows (src/change-patterns.js; validated by decision-spec-fields.js). Row shape measured live: name, severity, description, consequence, affected_path, affected_field; optional side (request|response) on ENUM_NARROWING. Names \u2286 patterns (not equality). Agent-detector names may appear only in patterns. Free-text fields are untrusted.",
225289
+ "description": "GOVERNANCE detector detail rows, emitted by the pattern detectors and validated against the decision-spec field contract before they leave the server. Row shape measured live: name, severity, description, consequence, affected_path, affected_field; optional side (request|response) on ENUM_NARROWING. Names \u2286 patterns (not equality). Agent-detector names may appear only in patterns. Free-text fields are untrusted.",
225035
225290
  "items": {
225036
225291
  "type": "object",
225037
225292
  "additionalProperties": false,
@@ -225081,7 +225336,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225081
225336
  },
225082
225337
  "breaking_changes_details": {
225083
225338
  "type": "array",
225084
- "description": "Per-change IR/detail rows from the engine (src/blast/diff-to-change.js maps these). Measured row keys: type, path, method, field, severity, description. Distinct from breaking_changes (integer count).",
225339
+ "description": "Per-change IR/detail rows, mapped from the engine's change IR. Measured row keys: type, path, method, field, severity, description. Distinct from breaking_changes (integer count).",
225085
225340
  "items": {
225086
225341
  "type": "object",
225087
225342
  "additionalProperties": true,
@@ -225110,7 +225365,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225110
225365
  },
225111
225366
  "severity_summary": {
225112
225367
  "type": "object",
225113
- "description": "Bundle severity axes (src/change-set.js severity_summary). Distinct axes, not contradictory. Measured keys: diff_severity, governance_severity, policy_effect, note.",
225368
+ "description": "Bundle severity axes, computed once per change set. Distinct axes, not contradictory. Measured keys: diff_severity, governance_severity, policy_effect, note.",
225114
225369
  "additionalProperties": false,
225115
225370
  "properties": {
225116
225371
  "diff_severity": {
@@ -225133,7 +225388,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225133
225388
  "decision_basis": {},
225134
225389
  "analysis": {
225135
225390
  "type": "object",
225136
- "description": "Tier-2 analysis mirror (src/response-envelope.js buildAnalysisTier / attachControlSurface). Dual-write of the flat analysis fields present on the verdict, plus remediations[]. PROPERTIES ARE GENERATED from ANALYSIS_TIER_FIELDS + remediations by scripts/generate-analysis-tier-schema.js \u2014 do not hand-edit them. OPEN BY DESIGN: additionalProperties stays TRUE and this is not an oversight. The fields above are copied conditionally, so which of them appear depends on the input \u2014 a verdict with no PII findings simply omits pii_findings. Closing this object would turn every future analysis field into a breaking change that fails inside the consumer, and would reject exactly the verdict paths that no one sampled when the union was built. Declared, not closed: you can now see what you may get, and you must still tolerate more.",
225391
+ "description": "Tier-2 analysis mirror, assembled by the response builder alongside the control surface. Dual-write of the flat analysis fields present on the verdict, plus remediations[]. PROPERTIES ARE GENERATED \u2014 do not hand-edit them. OPEN BY DESIGN: additionalProperties stays TRUE and this is not an oversight. The fields above are copied conditionally, so which of them appear depends on the input \u2014 a verdict with no PII findings simply omits pii_findings. Closing this object would turn every future analysis field into a breaking change that fails inside the consumer, and would reject exactly the verdict paths that no one sampled when the union was built. Declared, not closed: you can now see what you may get, and you must still tolerate more.",
225137
225392
  "properties": {
225138
225393
  "breaking_changes": {
225139
225394
  "type": "integer"
@@ -225260,7 +225515,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225260
225515
  },
225261
225516
  "human_report": {
225262
225517
  "type": "object",
225263
- "description": "Human-readable report tier (src/response-envelope.js buildHumanReport / analyze v2 stub). Measured keys: summary, breaking_highlights, suggestions, next_steps_prose.",
225518
+ "description": "Human-readable report tier, assembled by the response builder; analyze returns a reduced form. Measured keys: summary, breaking_highlights, suggestions, next_steps_prose.",
225264
225519
  "additionalProperties": false,
225265
225520
  "properties": {
225266
225521
  "summary": {
@@ -225320,7 +225575,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225320
225575
  "blast_radius": {
225321
225576
  "type": "object",
225322
225577
  "additionalProperties": false,
225323
- "description": "ID27 additive COUNTS (not a score). Pure function of the change-set + request graphs. Not in the verdict_fingerprint preimage.",
225578
+ "description": "Additive COUNTS (not a score). Pure function of the change-set + request graphs. Not in the verdict_fingerprint preimage.",
225324
225579
  "required": [
225325
225580
  "endpoints",
225326
225581
  "fields",
@@ -225485,7 +225740,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225485
225740
  },
225486
225741
  "detected_patterns": {
225487
225742
  "type": "array",
225488
- "description": "GOVERNANCE detector detail rows (src/change-patterns.js; validated by decision-spec-fields.js). Row shape measured live: name, severity, description, consequence, affected_path, affected_field; optional side (request|response) on ENUM_NARROWING. Names \u2286 patterns (not equality). Agent-detector names may appear only in patterns. Free-text fields are untrusted.",
225743
+ "description": "GOVERNANCE detector detail rows, emitted by the pattern detectors and validated against the decision-spec field contract before they leave the server. Row shape measured live: name, severity, description, consequence, affected_path, affected_field; optional side (request|response) on ENUM_NARROWING. Names \u2286 patterns (not equality). Agent-detector names may appear only in patterns. Free-text fields are untrusted.",
225489
225744
  "items": {
225490
225745
  "type": "object",
225491
225746
  "additionalProperties": false,
@@ -225535,7 +225790,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225535
225790
  },
225536
225791
  "breaking_changes_details": {
225537
225792
  "type": "array",
225538
- "description": "Per-change IR/detail rows from the engine (src/blast/diff-to-change.js maps these). Measured row keys: type, path, method, field, severity, description. Distinct from breaking_changes (integer count).",
225793
+ "description": "Per-change IR/detail rows, mapped from the engine's change IR. Measured row keys: type, path, method, field, severity, description. Distinct from breaking_changes (integer count).",
225539
225794
  "items": {
225540
225795
  "type": "object",
225541
225796
  "additionalProperties": true,
@@ -225564,7 +225819,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225564
225819
  },
225565
225820
  "severity_summary": {
225566
225821
  "type": "object",
225567
- "description": "Bundle severity axes (src/change-set.js severity_summary). Distinct axes, not contradictory. Measured keys: diff_severity, governance_severity, policy_effect, note.",
225822
+ "description": "Bundle severity axes, computed once per change set. Distinct axes, not contradictory. Measured keys: diff_severity, governance_severity, policy_effect, note.",
225568
225823
  "additionalProperties": false,
225569
225824
  "properties": {
225570
225825
  "diff_severity": {
@@ -225587,7 +225842,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225587
225842
  "decision_basis": {},
225588
225843
  "analysis": {
225589
225844
  "type": "object",
225590
- "description": "Tier-2 analysis mirror (src/response-envelope.js buildAnalysisTier / attachControlSurface). Dual-write of the flat analysis fields present on the verdict, plus remediations[]. PROPERTIES ARE GENERATED from ANALYSIS_TIER_FIELDS + remediations by scripts/generate-analysis-tier-schema.js \u2014 do not hand-edit them. OPEN BY DESIGN: additionalProperties stays TRUE and this is not an oversight. The fields above are copied conditionally, so which of them appear depends on the input \u2014 a verdict with no PII findings simply omits pii_findings. Closing this object would turn every future analysis field into a breaking change that fails inside the consumer, and would reject exactly the verdict paths that no one sampled when the union was built. Declared, not closed: you can now see what you may get, and you must still tolerate more.",
225845
+ "description": "Tier-2 analysis mirror, assembled by the response builder alongside the control surface. Dual-write of the flat analysis fields present on the verdict, plus remediations[]. PROPERTIES ARE GENERATED \u2014 do not hand-edit them. OPEN BY DESIGN: additionalProperties stays TRUE and this is not an oversight. The fields above are copied conditionally, so which of them appear depends on the input \u2014 a verdict with no PII findings simply omits pii_findings. Closing this object would turn every future analysis field into a breaking change that fails inside the consumer, and would reject exactly the verdict paths that no one sampled when the union was built. Declared, not closed: you can now see what you may get, and you must still tolerate more.",
225591
225846
  "properties": {
225592
225847
  "breaking_changes": {
225593
225848
  "type": "integer"
@@ -225714,7 +225969,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225714
225969
  },
225715
225970
  "human_report": {
225716
225971
  "type": "object",
225717
- "description": "Human-readable report tier (src/response-envelope.js buildHumanReport / analyze v2 stub). Measured keys: summary, breaking_highlights, suggestions, next_steps_prose.",
225972
+ "description": "Human-readable report tier, assembled by the response builder; analyze returns a reduced form. Measured keys: summary, breaking_highlights, suggestions, next_steps_prose.",
225718
225973
  "additionalProperties": false,
225719
225974
  "properties": {
225720
225975
  "summary": {
@@ -225736,7 +225991,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225736
225991
  },
225737
225992
  "execution_grant": {
225738
225993
  "type": "string",
225739
- "description": "Opt-in cr.exec.v1 execution grant (PHASE-0). Issued only when include_execution_grant is true on authorize. Short-lived mutation-bound sibling of chain_receipt; never unsigned. Optional inner state_nonce (ATOMIC profile) is additive and is NOT in scope_hash. See docs/cr-exec-v1.md / docs/cr-exec-attest-v1.md."
225994
+ "description": "Opt-in cr.exec.v1 execution grant (PHASE-0). Issued only when include_execution_grant is true on authorize. Short-lived mutation-bound sibling of chain_receipt; never unsigned. Optional inner state_nonce (ATOMIC profile) is additive and is NOT in scope_hash."
225740
225995
  },
225741
225996
  "chain_status": {
225742
225997
  "type": "string"
@@ -225826,14 +226081,14 @@ var require_mcp_preflight_output_schema = __commonJS({
225826
226081
  "object",
225827
226082
  "null"
225828
226083
  ],
225829
- "description": "ID963 additive. { audience, tenant_scope: bound|unbound, binding_proven_at? }. Informational \u2014 not permission, not a verify-receipt gate, not an ACL."
226084
+ "description": "Additive. { audience, tenant_scope: bound|unbound, binding_proven_at? }. Informational \u2014 not permission, not a verify-receipt gate, not an ACL."
225830
226085
  },
225831
226086
  "derivation": {
225832
226087
  "type": [
225833
226088
  "object",
225834
226089
  "null"
225835
226090
  ],
225836
- "description": 'ID637 6b additive. Present only when derivation:"server" produced this envelope. { source, platform?, base_sha, head_sha }. Covered by body_hash; not fingerprint.'
226091
+ "description": 'Additive. Present only when derivation:"server" produced this envelope. { source, platform?, base_sha, head_sha }. Covered by body_hash; not fingerprint.'
225837
226092
  },
225838
226093
  "receipt": {
225839
226094
  "type": "object"
@@ -225843,7 +226098,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225843
226098
  },
225844
226099
  "blast_radius": {
225845
226100
  "type": "object",
225846
- "description": "ID27 additive COUNTS (not a score). Not permission.",
226101
+ "description": "Additive COUNTS (not a score). Not permission.",
225847
226102
  "properties": {
225848
226103
  "endpoints": {
225849
226104
  "type": "integer",
@@ -225921,7 +226176,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225921
226176
  "blast_radius": {
225922
226177
  "type": "object",
225923
226178
  "additionalProperties": false,
225924
- "description": "ID27 additive COUNTS (not a score). Pure function of the change-set + request graphs. Not in the verdict_fingerprint preimage.",
226179
+ "description": "Additive COUNTS (not a score). Pure function of the change-set + request graphs. Not in the verdict_fingerprint preimage.",
225925
226180
  "required": [
225926
226181
  "endpoints",
225927
226182
  "fields",
@@ -226080,7 +226335,7 @@ var require_mcp_preflight_output_schema = __commonJS({
226080
226335
  },
226081
226336
  "detected_patterns": {
226082
226337
  "type": "array",
226083
- "description": "GOVERNANCE detector detail rows (src/change-patterns.js; validated by decision-spec-fields.js). Row shape measured live: name, severity, description, consequence, affected_path, affected_field; optional side (request|response) on ENUM_NARROWING. Names \u2286 patterns (not equality). Agent-detector names may appear only in patterns. Free-text fields are untrusted.",
226338
+ "description": "GOVERNANCE detector detail rows, emitted by the pattern detectors and validated against the decision-spec field contract before they leave the server. Row shape measured live: name, severity, description, consequence, affected_path, affected_field; optional side (request|response) on ENUM_NARROWING. Names \u2286 patterns (not equality). Agent-detector names may appear only in patterns. Free-text fields are untrusted.",
226084
226339
  "items": {
226085
226340
  "type": "object",
226086
226341
  "additionalProperties": false,
@@ -226130,7 +226385,7 @@ var require_mcp_preflight_output_schema = __commonJS({
226130
226385
  },
226131
226386
  "breaking_changes_details": {
226132
226387
  "type": "array",
226133
- "description": "Per-change IR/detail rows from the engine (src/blast/diff-to-change.js maps these). Measured row keys: type, path, method, field, severity, description. Distinct from breaking_changes (integer count).",
226388
+ "description": "Per-change IR/detail rows, mapped from the engine's change IR. Measured row keys: type, path, method, field, severity, description. Distinct from breaking_changes (integer count).",
226134
226389
  "items": {
226135
226390
  "type": "object",
226136
226391
  "additionalProperties": true,
@@ -226159,7 +226414,7 @@ var require_mcp_preflight_output_schema = __commonJS({
226159
226414
  },
226160
226415
  "severity_summary": {
226161
226416
  "type": "object",
226162
- "description": "Bundle severity axes (src/change-set.js severity_summary). Distinct axes, not contradictory. Measured keys: diff_severity, governance_severity, policy_effect, note.",
226417
+ "description": "Bundle severity axes, computed once per change set. Distinct axes, not contradictory. Measured keys: diff_severity, governance_severity, policy_effect, note.",
226163
226418
  "additionalProperties": false,
226164
226419
  "properties": {
226165
226420
  "diff_severity": {
@@ -226185,7 +226440,7 @@ var require_mcp_preflight_output_schema = __commonJS({
226185
226440
  "decision_basis": {},
226186
226441
  "analysis": {
226187
226442
  "type": "object",
226188
- "description": "Tier-2 analysis mirror (src/response-envelope.js buildAnalysisTier / attachControlSurface). Dual-write of the flat analysis fields present on the verdict, plus remediations[]. PROPERTIES ARE GENERATED from ANALYSIS_TIER_FIELDS + remediations by scripts/generate-analysis-tier-schema.js \u2014 do not hand-edit them. OPEN BY DESIGN: additionalProperties stays TRUE and this is not an oversight. The fields above are copied conditionally, so which of them appear depends on the input \u2014 a verdict with no PII findings simply omits pii_findings. Closing this object would turn every future analysis field into a breaking change that fails inside the consumer, and would reject exactly the verdict paths that no one sampled when the union was built. Declared, not closed: you can now see what you may get, and you must still tolerate more.",
226443
+ "description": "Tier-2 analysis mirror, assembled by the response builder alongside the control surface. Dual-write of the flat analysis fields present on the verdict, plus remediations[]. PROPERTIES ARE GENERATED \u2014 do not hand-edit them. OPEN BY DESIGN: additionalProperties stays TRUE and this is not an oversight. The fields above are copied conditionally, so which of them appear depends on the input \u2014 a verdict with no PII findings simply omits pii_findings. Closing this object would turn every future analysis field into a breaking change that fails inside the consumer, and would reject exactly the verdict paths that no one sampled when the union was built. Declared, not closed: you can now see what you may get, and you must still tolerate more.",
226189
226444
  "properties": {
226190
226445
  "breaking_changes": {
226191
226446
  "type": "integer"
@@ -226312,7 +226567,7 @@ var require_mcp_preflight_output_schema = __commonJS({
226312
226567
  },
226313
226568
  "human_report": {
226314
226569
  "type": "object",
226315
- "description": "Human-readable report tier (src/response-envelope.js buildHumanReport / analyze v2 stub). Measured keys: summary, breaking_highlights, suggestions, next_steps_prose.",
226570
+ "description": "Human-readable report tier, assembled by the response builder; analyze returns a reduced form. Measured keys: summary, breaking_highlights, suggestions, next_steps_prose.",
226316
226571
  "additionalProperties": false,
226317
226572
  "properties": {
226318
226573
  "summary": {
@@ -226423,14 +226678,14 @@ var require_mcp_preflight_output_schema = __commonJS({
226423
226678
  "object",
226424
226679
  "null"
226425
226680
  ],
226426
- "description": "ID963 additive. { audience, tenant_scope: bound|unbound, binding_proven_at? }. Informational \u2014 not permission, not a verify-receipt gate, not an ACL."
226681
+ "description": "Additive. { audience, tenant_scope: bound|unbound, binding_proven_at? }. Informational \u2014 not permission, not a verify-receipt gate, not an ACL."
226427
226682
  },
226428
226683
  "derivation": {
226429
226684
  "type": [
226430
226685
  "object",
226431
226686
  "null"
226432
226687
  ],
226433
- "description": 'ID637 6b additive. Present only when derivation:"server" produced this envelope. { source, platform?, base_sha, head_sha }. Covered by body_hash; not fingerprint.'
226688
+ "description": 'Additive. Present only when derivation:"server" produced this envelope. { source, platform?, base_sha, head_sha }. Covered by body_hash; not fingerprint.'
226434
226689
  },
226435
226690
  "receipt": {
226436
226691
  "type": "object"
@@ -226440,7 +226695,7 @@ var require_mcp_preflight_output_schema = __commonJS({
226440
226695
  },
226441
226696
  "blast_radius": {
226442
226697
  "type": "object",
226443
- "description": "ID27 additive COUNTS (not a score). Not permission.",
226698
+ "description": "Additive COUNTS (not a score). Not permission.",
226444
226699
  "properties": {
226445
226700
  "endpoints": {
226446
226701
  "type": "integer",
@@ -227336,7 +227591,7 @@ var require_mcp_streamable = __commonJS({
227336
227591
  });
227337
227592
  next();
227338
227593
  });
227339
- var DESC_PREFLIGHT_CHANGE_SET = "Use this when: a change set of contract artifacts modifies OpenAPI, GraphQL, protobuf, AsyncAPI, MCP manifests, or agent tool schemas before merge, deploy, publish, or tool registration. Do not call for documentation-only changes, static readiness scoring, or receipt verification. Use analyze for risk only; authorize requires context.operation for permission. For receipt verification use coderifts.verify_receipt instead; for details of a past decision use coderifts.get_decision_details instead.";
227594
+ var DESC_PREFLIGHT_CHANGE_SET = 'Use this when: a change set of contract artifacts modifies OpenAPI, GraphQL, protobuf, AsyncAPI, MCP manifests, or agent tool schemas before merge, deploy, publish, or tool registration. Do not call for documentation-only changes, static readiness scoring, or receipt verification. Use analyze for risk only; authorize requires context.operation for permission. For receipt verification use coderifts.verify_receipt instead; for details of a past decision use coderifts.get_decision_details instead. Inputs: preflight_mode is required: "analyze" (risk only; no receipt, no execution_action) or "authorize" (may mint a receipt; requires context.operation \u2014 merge is not deploy is not publish). Supply exactly one artifact source: artifacts[] (1\u201320 items, each {id, type, before, after} as the FULL spec/schema text, not a path or URL; type is openapi|graphql|grpc|asyncapi|mcp_manifest|agent_tools) XOR derivation="server" (server reads GitHub Compare; needs context.repository + context.base + context.head; sending artifacts[] together is 400). Grant fields (include_execution_grant, grant_version, tenant_id, executor_id, adapter_id, target_uri, expected_state_token, state_nonce, policy_hash) apply only when preflight_mode="authorize" AND include_execution_grant=true; analyze ignores them. previous_receipt is a chain token base64url(body).base64url(signature) to LINK a prior decision \u2014 it does not re-verify; call verify_receipt for that. idempotency_key replays authorize only (24h), never analyze.';
227340
227595
  var DESC_VERIFY_RECEIPT = `Verify a CodeRifts signed chain-receipt you ALREADY HOLD: cryptographic
227341
227596
  authenticity (signature + key id), body binding, and \u2014 when lifecycle indices
227342
227597
  are available \u2014 whether it is currently valid authorization (not expired,
@@ -227576,23 +227831,33 @@ next_agent_step is a suggestion, not permission.`;
227576
227831
  properties: {
227577
227832
  artifacts: {
227578
227833
  type: "array",
227579
- description: "Contract artifacts to analyze together (max 20). Each is { id, type, before, after }.",
227834
+ description: '1\u201320 contract documents analyzed together. Each item is {id, type, before, after} where before/after are the FULL document strings (YAML/JSON/proto text), not URLs or file paths. Omit this array entirely when derivation="server".',
227580
227835
  minItems: 1,
227581
227836
  maxItems: 20,
227582
227837
  items: {
227583
227838
  type: "object",
227584
227839
  properties: {
227585
227840
  id: { type: "string", description: "Caller-chosen id, unique within the bundle" },
227586
- type: { type: "string", enum: ["openapi", "graphql", "grpc", "asyncapi", "mcp_manifest", "agent_tools"] },
227587
- before: { type: "string", description: "Prior artifact content (spec/schema/manifest) as a string" },
227588
- after: { type: "string", description: "Updated artifact content as a string" }
227841
+ type: {
227842
+ type: "string",
227843
+ enum: ["openapi", "graphql", "grpc", "asyncapi", "mcp_manifest", "agent_tools"],
227844
+ description: "Artifact kind. One of openapi, graphql, grpc, asyncapi, mcp_manifest, agent_tools. Determines how before/after text is parsed. Not inferred from the filename."
227845
+ },
227846
+ before: {
227847
+ type: "string",
227848
+ description: 'The baseline document as raw text (the spec/schema/manifest body). Empty string means "no prior version" (create), not "load from disk".'
227849
+ },
227850
+ after: {
227851
+ type: "string",
227852
+ description: "The proposed document as raw text, same kind as before. Must be the bytes you intend to merge/deploy/publish, not a diff."
227853
+ }
227589
227854
  },
227590
227855
  required: ["id", "type", "before", "after"]
227591
227856
  }
227592
227857
  },
227593
227858
  context: {
227594
227859
  type: "object",
227595
- description: "Optional apply-site context folded into the bundle fingerprint. operation distinguishes merge vs deploy vs publish (and other labels); the server accepts any string for operation (change-set.js) \u2014 conventional values below.",
227860
+ description: "Optional apply-site context folded into the bundle fingerprint. operation distinguishes merge vs deploy vs publish (and other labels); the server accepts any non-empty string; conventional values: merge, deploy, tool_call, publish. The receipt/gate must match this label.",
227596
227861
  properties: {
227597
227862
  // Server: context.operation is String(context.operation) with no closed enum
227598
227863
  // (src/change-set.js computeBundleFingerprint / envelopeFields). Document conventional set.
@@ -227622,14 +227887,17 @@ next_agent_step is a suggestion, not permission.`;
227622
227887
  // OPEN: unknown context keys are accepted (not 400) and ignored by buildChangeSet
227623
227888
  // except the keys it reads. Do not set additionalProperties:false.
227624
227889
  },
227625
- previous_receipt: { type: "string", description: "Optional prior chain receipt token to link" },
227890
+ previous_receipt: {
227891
+ type: "string",
227892
+ description: "Optional prior chain token to LINK this call into a receipt chain: base64url(body).base64url(signature). Linking is not verification \u2014 a linked token is not re-checked here; use verify_receipt."
227893
+ },
227626
227894
  include_execution_grant: {
227627
227895
  type: "boolean",
227628
- description: "Opt-in (authorize only). When true on allow-class authorize, the response includes a signed execution_grant (cr.exec.v1) alongside chain_receipt, or HTTP 503 SIGNER_UNAVAILABLE \u2014 never unsigned. Default false. Analyze ignores this flag. See docs/cr-exec-v1.md."
227896
+ description: 'Authorize only. When true on an allow-class authorize, the response includes a signed execution_grant, or HTTP 503 SIGNER_UNAVAILABLE \u2014 never an unsigned grant. Default false. Analyze ignores this flag. Ignored unless preflight_mode="authorize".'
227629
227897
  },
227630
227898
  state_nonce: {
227631
227899
  type: "string",
227632
- description: "Optional ATOMIC-profile nonce. When include_execution_grant is true, copied into the signed grant as state_nonce (a separate signed field \u2014 NOT folded into scope_hash). Absent \u2192 BEARER grant (today's default). See docs/cr-exec-v1.md."
227900
+ description: "Authorize+grant only. Opaque nonce copied into the signed grant as its own field (not folded into scope_hash). Absent \u2192 BEARER grant. Ignored unless include_execution_grant is true."
227633
227901
  },
227634
227902
  // ── cr.exec.v2 authorize fields (1130-F1) ────────────────────────────────────────
227635
227903
  // DECLARED, not added: the authorize handler already reads every one of these
@@ -227643,33 +227911,33 @@ next_agent_step is a suggestion, not permission.`;
227643
227911
  // 1363: JSON Schema `default` is a SINGLE static value. A date-gated default cannot
227644
227912
  // be that without lying on one side of the cutoff. The 1344 mechanism (resolver +
227645
227913
  // GRANT_DEFAULT_V2_DATE) is unchanged; this is the schema expression of it.
227646
- // 1384: the TEMPLATE here is the frozen surface constant (GRANT_VERSION_V1).
227647
- // visibleTools() stamps the LIVE omit-value from resolveGrantVersion so the
227648
- // served header is truthful on both sides of 2026-09-18. tools_sha256 hashes
227649
- // the frozen constant, not the live value (volatile; see toolsForSurfaceDigest).
227914
+ // 1384: the TEMPLATE starts at GRANT_VERSION_V1. visibleTools() stamps the LIVE
227915
+ // omit-value from resolveGrantVersion so the served header is truthful on both
227916
+ // sides of 2026-09-18. Decision B (2026-09-18): tools_sha256 hashes the served
227917
+ // tools[] including that live stamp do not freeze it to v1.
227650
227918
  "x-coderifts-effective-default": GRANT_VERSION_V1,
227651
227919
  "x-coderifts-default-changes-at": GRANT_DEFAULT_V2_DATE,
227652
227920
  description: "Grant envelope to mint when include_execution_grant is true. Omitting this yields cr.exec.v1 until " + GRANT_DEFAULT_V2_DATE + ' and cr.exec.v2 on and after it (see x-coderifts-effective-default / x-coderifts-default-changes-at). The response meta.grant_version is the version actually issued. An explicit value always wins \u2014 pin "v1" to keep current behaviour with no code change on the date.'
227653
227921
  },
227654
227922
  tenant_id: {
227655
227923
  type: "string",
227656
- description: 'Tenant the grant is issued under (cr.exec.v2). Defaults to "default" when absent.'
227924
+ description: 'Authorize+grant v2 only. Tenant the grant is issued under. ASCII slug. When omitted the server uses "default" \u2014 pin it if you are not on the default tenant.'
227657
227925
  },
227658
227926
  executor_id: {
227659
227927
  type: "string",
227660
- description: "Identity of the executor the grant is bound to (cr.exec.v2)."
227928
+ description: 'Authorize+grant v2 only. Executor identity the grant is bound to (example: agent:ci-bot, host:github-actions). Empty/absent is not "any executor".'
227661
227929
  },
227662
227930
  adapter_id: {
227663
227931
  type: "string",
227664
- description: "Adapter the executor will apply the change with, e.g. fs, postgres, git (cr.exec.v2)."
227932
+ description: "Authorize+grant v2 only. Adapter that will apply the change. Conventional values: fs, postgres, git. Must match the adapter the executor actually uses; a git grant does not authorize an fs write."
227665
227933
  },
227666
227934
  target_uri: {
227667
227935
  type: "string",
227668
- description: "Target the grant is bound to (cr.exec.v2). Falls back to context.target_uri, then to a repository/head-derived value."
227936
+ description: "Authorize+grant v2 only. URI the grant binds (example: git://owner/repo.git/refs/heads/main). Fallback if omitted: context.target_uri, then repository/head-derived. Distinct from context.target_id."
227669
227937
  },
227670
227938
  expected_state_token: {
227671
227939
  type: "string",
227672
- description: "State token the executor expects to observe at apply time (cr.exec.v2). Signed as a separate field; empty string when absent."
227940
+ description: 'Authorize+grant v2 only. Compare-and-swap token the executor must observe at apply time (the "before" state). Signed as its own field. Omit only if the adapter has no prior state; do not send a placeholder.'
227673
227941
  },
227674
227942
  audience: {
227675
227943
  type: "string",
@@ -227677,25 +227945,25 @@ next_agent_step is a suggestion, not permission.`;
227677
227945
  },
227678
227946
  policy_hash: {
227679
227947
  type: "string",
227680
- description: "Policy identity the grant is issued under (cr.exec.v2). Bound into the signed grant when supplied."
227948
+ description: "Authorize+grant v2 only. Policy identity bound into the grant, sha256: + 64 hex. When supplied, apply must use that same policy; a different policy is a different grant."
227681
227949
  },
227682
227950
  idempotency_key: { type: "string", description: "Optional client key; in authorize mode, a repeat with the same key + body replays the original decision (24h). Analyze responses are not replayed." },
227683
227951
  // TOP-LEVEL (not context). Decision Spec 2.0: REQUIRED on every call.
227684
- // Omitted without decision_spec_version:'1.0' legacy pin INVALID_INPUT 400.
227952
+ // Omission INVALID_INPUT 400. The 1.0 legacy pin retired at DECISION_SPEC_LEGACY_SUNSET.
227685
227953
  preflight_mode: {
227686
227954
  type: "string",
227687
227955
  enum: ["analyze", "authorize"],
227688
- description: `REQUIRED. "analyze" = informational risk only (no decision/execution_action/safe_for_agent; analysis_outcome + may_execute:false). "authorize" = operation-bound path; may mint a receipt (requires context.operation). Decision Spec 2.0: omission is an error unless decision_spec_version is '1.0' (30-day legacy pin with soft-default analyze).`
227956
+ description: 'REQUIRED. "analyze" = informational risk only (no decision/execution_action/safe_for_agent; analysis_outcome + may_execute:false). "authorize" = operation-bound path; may mint a receipt (requires context.operation). Decision Spec 2.0: omission is INVALID_INPUT. Do not pin 1.0 \u2014 that pin retired.'
227689
227957
  },
227690
227958
  decision_spec_version: {
227691
227959
  type: "string",
227692
- enum: ["1.0", "2.0"],
227693
- description: "Optional pin. '1.0' = legacy contract (soft-default mode + analyze still carries decision/execution_action) until the sunset date. Omit or '2.0' = current contract."
227960
+ enum: ["2.0"],
227961
+ description: "Optional. Omit or '2.0' = current contract. '1.0' retired (INVALID_INPUT); do not pin 1.0."
227694
227962
  },
227695
227963
  derivation: {
227696
227964
  type: "string",
227697
227965
  enum: ["server"],
227698
- description: `Opt-in (ID637 6b). "server" = the server derives artifacts[] from GitHub Compare via the App installation. Allowed only when the tenant has a proven binding for context.repository and context.base + context.head are present. Omit artifacts[] (caller-supplied artifacts[] with this flag is 400 \u2014 one source of truth). Default absent = today's caller-artifacts path (byte-identical).`
227966
+ description: '"server" = the server derives artifacts[] from GitHub Compare via the App installation. Allowed only when context.repository, context.base and context.head are all present and the tenant has a proven binding for that repository. Do not send artifacts[] in the same call (400 \u2014 one source of truth). Omit this field for the caller-supplied artifacts[] path.'
227699
227967
  }
227700
227968
  },
227701
227969
  // artifacts is NOT in required: derivation:"server" omits it (server lists the set).
@@ -227795,7 +228063,7 @@ next_agent_step is a suggestion, not permission.`;
227795
228063
  enum: RECEIPT_STATUSES.slice()
227796
228064
  },
227797
228065
  reason: { type: ["string", "null"] },
227798
- correlation_id: { type: "string", description: "Route-owned trace id (ID828); always a non-empty string on 200" },
228066
+ correlation_id: { type: "string", description: "Route-owned trace id, set by the route itself rather than by correlation middleware; always a non-empty string on 200" },
227799
228067
  payload: { type: "object" },
227800
228068
  // Authorization layer (§106): signature-valid ≠ currently-authorized.
227801
228069
  // null means authorization could NOT be evaluated (missing intended context and/or
@@ -228316,9 +228584,12 @@ var require_generate_surface_anchor = __commonJS({
228316
228584
  var OUT = path.join(__dirname, "..", "src", "generated", "surface-anchor.json");
228317
228585
  var ENDPOINT = "https://app.coderifts.com/mcp";
228318
228586
  var VOLATILE_KEYS = Object.freeze([]);
228319
- var SOURCE_REF = "https://raw.githubusercontent.com/coderifts/api-governance/surface-dc86678e/tools.wire.v1.json";
228320
- var SOURCE_REF_COMMIT = "e0a9ae6fabef489c7f0f13714396add51519c195";
228321
- var SOURCE_REF_STATUS = `PINNED TO ONE REVISION. source_ref is the wire-format tools[] as served, published in the public coderifts/api-governance repo at tag surface-dc86678e (commit ${SOURCE_REF_COMMIT}). Verified at fill time: the file's own tools_sha256, its tools[] recomputed under the canonical rule in digest_input, and this anchor's tools_sha256 are the same value. HOW TO USE IT: fetch it, recompute the digest yourself, and compare it to tools_sha256 here. IF THEY DIFFER, TRUST NEITHER -- it means the surface moved and this ref was not re-tagged. A tag can be force-moved; the commit SHA above cannot, so pin the SHA URL if you want the stronger form. OUR OBLIGATION: every surface change requires a NEW tag and a NEW source_ref in this file. Skipping that leaves this field pointing at an old surface while still looking authoritative, which is worse than the null it replaced. HOW THE OBLIGATION IS ENFORCED, on both sides: this repo CI-runs generate-surface-anchor.js --check --verify-source-ref, which fetches the ref above, recomputes its digest under the rule in digest_input, and FAILS if it is stale, self-inconsistent, or its tag is gone. The target repo runs scripts/validate-tools-wire.mjs from its own npm test and a CI workflow -- on push, on PR, and daily, because the wire target can go stale without anything in that repo changing. Both SKIP LOUDLY rather than fail on a transport error, and both treat a 404 as a real verdict rather than a network problem. Neither this ref nor those gates is a root of trust: pin the digest at YOUR approval time -- see honesty.pin_at_approval_time.`;
228587
+ var SOURCE_REF = "https://raw.githubusercontent.com/coderifts/api-governance/surface-497eb029/tools.wire.v1.json";
228588
+ var SOURCE_REF_COMMIT = "0ef58c28475fe1b85dc363da9a95bf566fca4587";
228589
+ var SOURCE_REF_TAG = (SOURCE_REF.match(/\/api-governance\/([^/]+)\//) || [])[1] || "(unknown tag)";
228590
+ var SOURCE_REF_IS_PINNED = /^[0-9a-f]{40}$/.test(SOURCE_REF_COMMIT);
228591
+ var SOURCE_REF_STATUS_OPENING = SOURCE_REF_IS_PINNED ? `PINNED TO ONE REVISION. source_ref is the wire-format tools[] as served, published in the public coderifts/api-governance repo at tag ${SOURCE_REF_TAG} (commit ${SOURCE_REF_COMMIT}). Verified at fill time: the file's own tools_sha256, its tools[] recomputed under the canonical rule in digest_input, and this anchor's tools_sha256 are the same value. ` : `NOT YET PINNED -- THE TAG DOES NOT EXIST YET. source_ref is the wire-format tools[] as served, to be published in the public coderifts/api-governance repo at tag ${SOURCE_REF_TAG} (commit ${SOURCE_REF_COMMIT}). Until that tag is pushed this URL 404s, and --verify-source-ref FAILS on it deliberately: a 404 here is the obligation below being unmet, not a network problem. Once it is pushed, verify at fill time that the file's own tools_sha256, its tools[] recomputed under the canonical rule in digest_input, and this anchor's tools_sha256 are the same value. `;
228592
+ var SOURCE_REF_STATUS = SOURCE_REF_STATUS_OPENING + "HOW TO USE IT: fetch it, recompute the digest yourself, and compare it to tools_sha256 here. IF THEY DIFFER, TRUST NEITHER -- it means the surface moved and this ref was not re-tagged. A tag can be force-moved; the commit SHA above cannot, so pin the SHA URL if you want the stronger form. OUR OBLIGATION: every surface change requires a NEW tag and a NEW source_ref in this file. Skipping that leaves this field pointing at an old surface while still looking authoritative, which is worse than the null it replaced. HOW THE OBLIGATION IS ENFORCED, on both sides: this repo CI-runs generate-surface-anchor.js --check --verify-source-ref, which fetches the ref above, recomputes its digest under the rule in digest_input, and FAILS if it is stale, self-inconsistent, or its tag is gone. The target repo runs scripts/validate-tools-wire.mjs from its own npm test and a CI workflow -- on push, on PR, and daily, because the wire target can go stale without anything in that repo changing. Both SKIP LOUDLY rather than fail on a transport error, and both treat a 404 as a real verdict rather than a network problem. Neither this ref nor those gates is a root of trust: pin the digest at YOUR approval time -- see honesty.pin_at_approval_time.";
228322
228593
  var HONESTY = Object.freeze({
228323
228594
  pins_bytes_not_behaviour: "This anchor pins BYTES, not behaviour. It says the tool text you received is the text that was published. It says nothing about whether the model read it, understood it, or obeyed it.",
228324
228595
  scoped_to_its_profile: "This digest is scoped to the profile named above and to nothing else. The default-profile digest says NOTHING about the surface served with include_advanced_tools, which is a different tool set with a different digest. A hash without its profile does not identify a surface.",
@@ -228337,10 +228608,25 @@ var require_generate_surface_anchor = __commonJS({
228337
228608
  }
228338
228609
  return v;
228339
228610
  }
228611
+ var lastDigestTools = null;
228612
+ function buildWire(opts = {}) {
228613
+ const anchor = buildAnchor(opts);
228614
+ const doc = {
228615
+ schema: "coderifts.tools-wire.v1",
228616
+ tool_count: anchor.tool_count,
228617
+ tools_sha256: anchor.tools_sha256,
228618
+ tools: lastDigestTools
228619
+ };
228620
+ const recomputed = `sha256:${crypto.createHash("sha256").update(canonicalJson(doc.tools), "utf8").digest("hex")}`;
228621
+ if (recomputed !== doc.tools_sha256) {
228622
+ throw new Error(`buildWire: emitted tools[] hashes to ${recomputed} but the anchor says ${doc.tools_sha256} \u2014 the wire file would be self-inconsistent`);
228623
+ }
228624
+ return doc;
228625
+ }
228340
228626
  function buildAnchor(opts = {}) {
228341
228627
  const includeAdvanced = opts.includeAdvanced === true;
228342
228628
  const tools = mcp.visibleTools({ includeAdvanced, now: opts.now });
228343
- const digestTools = typeof mcp.toolsForSurfaceDigest === "function" ? mcp.toolsForSurfaceDigest(tools) : tools;
228629
+ const digestTools = tools;
228344
228630
  const profile = buildRequestProfile({
228345
228631
  endpoint: ENDPOINT,
228346
228632
  method: "tools/list",
@@ -228348,11 +228634,12 @@ var require_generate_surface_anchor = __commonJS({
228348
228634
  params: includeAdvanced ? { include_advanced_tools: true } : {}
228349
228635
  });
228350
228636
  const digest = crypto.createHash("sha256").update(canonicalJson(digestTools), "utf8").digest("hex");
228637
+ lastDigestTools = digestTools;
228351
228638
  return {
228352
228639
  profile,
228353
228640
  tools_sha256: `sha256:${digest}`,
228354
228641
  tool_count: tools.length,
228355
- digest_input: "sha256 over the canonical JSON of the served tools[]: object keys sorted lexicographically at every depth (arrays keep order), no whitespace, RAW UTF-8 with no \\uXXXX escaping. Python consumers must pass ensure_ascii=False -- the default escapes the 58 non-ASCII characters in this surface and produces a different digest over identical data.",
228642
+ digest_input: "sha256 over the canonical JSON of the served tools[] AS RETURNED by tools/list (including x-coderifts-effective-default at hash time \u2014 do not freeze it to v1): object keys sorted lexicographically at every depth (arrays keep order), no whitespace, RAW UTF-8 with no \\uXXXX escaping. Python consumers must pass ensure_ascii=False -- the default escapes the 58 non-ASCII characters in this surface and produces a different digest over identical data.",
228356
228643
  source_ref: SOURCE_REF,
228357
228644
  // Machine-readable, so a consumer can pin the immutable form without parsing the prose above.
228358
228645
  source_ref_commit: SOURCE_REF_COMMIT,
@@ -228438,6 +228725,25 @@ var require_generate_surface_anchor = __commonJS({
228438
228725
  const check = argv.includes("--check");
228439
228726
  const verifyRef = argv.includes("--verify-source-ref");
228440
228727
  const anchor = buildAnchor();
228728
+ if (argv.includes("--emit-wire")) {
228729
+ const bytes = `${JSON.stringify(buildWire(), null, 2)}
228730
+ `;
228731
+ const i = argv.indexOf("--out");
228732
+ const out = i >= 0 ? argv[i + 1] : null;
228733
+ if (i >= 0 && !out) {
228734
+ process.stderr.write("--out needs a path\n");
228735
+ process.exitCode = 1;
228736
+ return;
228737
+ }
228738
+ if (out) {
228739
+ fs.writeFileSync(out, bytes);
228740
+ process.stderr.write(`wire written: ${out} (${buildWire().tool_count} tools, ${buildWire().tools_sha256})
228741
+ `);
228742
+ return;
228743
+ }
228744
+ process.stdout.write(bytes);
228745
+ return;
228746
+ }
228441
228747
  if (verifyRef) {
228442
228748
  const r = await verifySourceRef(anchor);
228443
228749
  if (r.ok) {
@@ -228503,7 +228809,7 @@ var require_generate_surface_anchor = __commonJS({
228503
228809
  process.exit(1);
228504
228810
  });
228505
228811
  }
228506
- module2.exports = { buildAnchor, runCheck, verifySourceRef, canonicalJson, derived, derivedJson, VOLATILE_KEYS, HONESTY, OUT };
228812
+ module2.exports = { buildAnchor, buildWire, runCheck, verifySourceRef, canonicalJson, derived, derivedJson, VOLATILE_KEYS, HONESTY, OUT };
228507
228813
  }
228508
228814
  });
228509
228815
 
@@ -233561,13 +233867,14 @@ ${HOOK_MARKER}
233561
233867
  #
233562
233868
  # Re-install after CLI upgrades: coderifts hook install
233563
233869
  #
233564
- # Exit map (fail-closed when configured):
233565
- # CODERIFTS_SKIP=1 \u2192 exit 0 (explicit loud override)
233566
- # no apiKey \u2192 exit 0 soft (not configured)
233567
- # BLOCK / STOP / UNKNOWN \u2192 exit 1
233870
+ # Exit map (fail-closed when the hook is installed):
233871
+ # CODERIFTS_SKIP=1 \u2192 exit 0 (explicit loud override)
233872
+ # no key + CODERIFTS_ADVISORY=1|true \u2192 exit 0 (explicit opt-out; same flag as claude-hook)
233873
+ # no key \u2192 exit 1 named (absence is not permission)
233874
+ # BLOCK / STOP / UNKNOWN \u2192 exit 1
233568
233875
  # INDETERMINATE (no decision obtainable) \u2192 exit 1
233569
- # ALLOW / analysis clean \u2192 exit 0
233570
- # REQUIRE_APPROVAL / WARN \u2192 warn, exit 0
233876
+ # ALLOW / analysis clean \u2192 exit 0
233877
+ # REQUIRE_APPROVAL / WARN \u2192 warn, exit 0
233571
233878
 
233572
233879
  # Explicit escape hatch \u2014 never the default on error; human must set this.
233573
233880
  if [ "\${CODERIFTS_SKIP:-}" = "1" ]; then
@@ -233575,13 +233882,23 @@ if [ "\${CODERIFTS_SKIP:-}" = "1" ]; then
233575
233882
  exit 0
233576
233883
  fi
233577
233884
 
233578
- CODERIFTS_API_KEY=$(git config coderifts.apiKey)
233885
+ # git config wins when set. Do not clobber a process-env CODERIFTS_API_KEY with empty config.
233886
+ _CR_GIT_KEY=$(git config --get coderifts.apiKey 2>/dev/null || true)
233887
+ if [ -n "$_CR_GIT_KEY" ]; then
233888
+ CODERIFTS_API_KEY="$_CR_GIT_KEY"
233889
+ fi
233579
233890
  SPEC_PATH=$(git config coderifts.specPath || echo "api/openapi.yaml")
233580
233891
  ZERO="0000000000000000000000000000000000000000"
233581
233892
 
233582
- if [ -z "$CODERIFTS_API_KEY" ]; then
233583
- echo "CodeRifts: No API key configured. Run: git config coderifts.apiKey <your-key>"
233584
- exit 0 # Don't block if not configured
233893
+ if [ -z "\${CODERIFTS_API_KEY:-}" ]; then
233894
+ case "\${CODERIFTS_ADVISORY:-}" in
233895
+ 1|true|TRUE)
233896
+ echo "CodeRifts: no API key \u2014 allowing push (CODERIFTS_ADVISORY). This is explicit opt-out, not a missing-key default. Set git config coderifts.apiKey or CODERIFTS_API_KEY to enforce." >&2
233897
+ exit 0
233898
+ ;;
233899
+ esac
233900
+ echo "CodeRifts: this hook guards the push and cannot ask for a decision without an API key. Absence of a key is not permission. Set git config coderifts.apiKey <key> or export CODERIFTS_API_KEY. To allow without governance, set CODERIFTS_ADVISORY=1 (explicit opt-out) or CODERIFTS_SKIP=1 / git push --no-verify." >&2
233901
+ exit 1
233585
233902
  fi
233586
233903
 
233587
233904
  # --- helpers: three-state git blob read (present | absent | error) ---
@@ -234034,8 +234351,10 @@ exit 0
234034
234351
  fs.writeFileSync(hookPath, PRE_PUSH_SCRIPT, { mode: 493 });
234035
234352
  console.log("CodeRifts pre-push hook installed.");
234036
234353
  console.log("");
234037
- console.log("Configure your API key:");
234038
- console.log(" git config coderifts.apiKey <your-key>");
234354
+ console.log("This hook guards the push. Absence of a key is not permission.");
234355
+ console.log("Set a key (git config coderifts.apiKey <your-key> or CODERIFTS_API_KEY)");
234356
+ console.log("or the next push exits 1. Explicit opt-out: CODERIFTS_ADVISORY=1.");
234357
+ console.log("Loud skip: CODERIFTS_SKIP=1 (or git push --no-verify).");
234039
234358
  console.log("");
234040
234359
  console.log("Optionally set spec path (default: api/openapi.yaml):");
234041
234360
  console.log(" git config coderifts.specPath path/to/openapi.yaml");
@@ -263459,6 +263778,212 @@ ${USAGE}` };
263459
263778
  }
263460
263779
  });
263461
263780
 
263781
+ // ../../src/core/mcp-lock-drift.js
263782
+ var require_mcp_lock_drift = __commonJS({
263783
+ "../../src/core/mcp-lock-drift.js"(exports2, module2) {
263784
+ "use strict";
263785
+ var crypto = require("crypto");
263786
+ var { scoreMcpRisk, SIGNAL_CATALOG } = require_mcp_risk_scorer();
263787
+ function normalizeMcpManifest(input) {
263788
+ if (input == null) return null;
263789
+ if (Array.isArray(input)) {
263790
+ return { tools: input.filter((t) => t && typeof t === "object") };
263791
+ }
263792
+ if (typeof input !== "object") return null;
263793
+ if (Array.isArray(input.tools)) {
263794
+ return { tools: input.tools.filter((t) => t && typeof t === "object") };
263795
+ }
263796
+ if (input.manifest && typeof input.manifest === "object") {
263797
+ return normalizeMcpManifest(input.manifest);
263798
+ }
263799
+ if (input.server && typeof input.server === "object" && Array.isArray(input.server.tools)) {
263800
+ return { tools: input.server.tools.filter((t) => t && typeof t === "object") };
263801
+ }
263802
+ if (typeof input.name === "string" && (input.inputSchema || input.description != null)) {
263803
+ return { tools: [input] };
263804
+ }
263805
+ return null;
263806
+ }
263807
+ function manifestContentHash(manifest) {
263808
+ if (!manifest || !Array.isArray(manifest.tools)) return null;
263809
+ const tools = manifest.tools.map((t) => ({
263810
+ name: t && t.name != null ? String(t.name) : "",
263811
+ description: t && t.description != null ? String(t.description) : "",
263812
+ inputSchema: t && t.inputSchema != null ? t.inputSchema : null,
263813
+ // scopes / annotations if present
263814
+ annotations: t && t.annotations != null ? t.annotations : null
263815
+ })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
263816
+ const material = JSON.stringify(tools);
263817
+ return `sha256:${crypto.createHash("sha256").update(material).digest("hex")}`;
263818
+ }
263819
+ function extractLockedManifestsFromLock(lockDoc) {
263820
+ if (!lockDoc || typeof lockDoc !== "object") return [];
263821
+ const out = [];
263822
+ if (Array.isArray(lockDoc.mcp_manifests)) {
263823
+ lockDoc.mcp_manifests.forEach((entry, i) => {
263824
+ if (!entry) return;
263825
+ const raw = entry.manifest != null ? entry.manifest : entry;
263826
+ const m = normalizeMcpManifest(raw);
263827
+ if (m) {
263828
+ out.push({
263829
+ id: entry.id != null ? String(entry.id) : `mcp_manifests[${i}]`,
263830
+ manifest: m
263831
+ });
263832
+ }
263833
+ });
263834
+ }
263835
+ if (Array.isArray(lockDoc.mcp_servers)) {
263836
+ lockDoc.mcp_servers.forEach((entry, i) => {
263837
+ if (!entry) return;
263838
+ const m = normalizeMcpManifest(entry.manifest != null ? entry.manifest : entry);
263839
+ if (m) {
263840
+ out.push({
263841
+ id: (entry.id || entry.name) != null ? String(entry.id || entry.name) : `mcp_servers[${i}]`,
263842
+ manifest: m
263843
+ });
263844
+ }
263845
+ });
263846
+ }
263847
+ if (lockDoc.locked_mcp_manifest) {
263848
+ const m = normalizeMcpManifest(lockDoc.locked_mcp_manifest);
263849
+ if (m) out.push({ id: "locked_mcp_manifest", manifest: m });
263850
+ }
263851
+ return out;
263852
+ }
263853
+ function buildLockDrift(args = {}) {
263854
+ const serverId = args.serverId != null ? String(args.serverId) : "default";
263855
+ if (args.liveUnreachable === true) {
263856
+ return {
263857
+ status: "unreachable",
263858
+ drift: false,
263859
+ // honest: unknown, not drifted
263860
+ server_id: serverId,
263861
+ locked_hash: manifestContentHash(normalizeMcpManifest(args.lockedManifest)),
263862
+ live_hash: null,
263863
+ risk_score: null,
263864
+ level: null,
263865
+ signals: [],
263866
+ findings: [],
263867
+ summary: "Live MCP manifest unreachable \u2014 not scored as drift and not claimed unchanged (absence_is_unknown).",
263868
+ live_error: args.liveError != null ? String(args.liveError).slice(0, 300) : null,
263869
+ interpretation: "absence_is_unknown",
263870
+ note: "Drift is a scored finding of measurable change, not an accusation of intent. Unreachable \u2260 drifted. Monitor only \u2014 does not block.",
263871
+ scorer: "scoreMcpRisk"
263872
+ };
263873
+ }
263874
+ const locked = normalizeMcpManifest(args.lockedManifest);
263875
+ const live = normalizeMcpManifest(args.liveManifest);
263876
+ if (!locked) {
263877
+ return {
263878
+ status: "missing_locked",
263879
+ drift: false,
263880
+ server_id: serverId,
263881
+ locked_hash: null,
263882
+ live_hash: manifestContentHash(live),
263883
+ risk_score: null,
263884
+ level: null,
263885
+ signals: [],
263886
+ findings: [],
263887
+ summary: "No locked MCP manifest supplied \u2014 cannot compute drift (not a clean bill of health).",
263888
+ interpretation: "absence_is_unknown",
263889
+ note: "coderifts.lock v1 records observed agents/ops, not MCP tool bodies; pass --locked-manifest or additive lock mcp_manifests.",
263890
+ scorer: "scoreMcpRisk"
263891
+ };
263892
+ }
263893
+ if (!live) {
263894
+ return {
263895
+ status: "missing_live",
263896
+ drift: false,
263897
+ server_id: serverId,
263898
+ locked_hash: manifestContentHash(locked),
263899
+ live_hash: null,
263900
+ risk_score: null,
263901
+ level: null,
263902
+ signals: [],
263903
+ findings: [],
263904
+ summary: "No live MCP manifest supplied \u2014 cannot compute drift (absence_is_unknown).",
263905
+ interpretation: "absence_is_unknown",
263906
+ note: "Provide --live-manifest or --live-url. Unreachable fetches use status unreachable.",
263907
+ scorer: "scoreMcpRisk"
263908
+ };
263909
+ }
263910
+ const scored = scoreMcpRisk({
263911
+ oldManifest: locked,
263912
+ newManifest: live
263913
+ });
263914
+ const locked_hash = manifestContentHash(locked);
263915
+ const live_hash = manifestContentHash(live);
263916
+ const hashDiffers = locked_hash != null && live_hash != null && locked_hash !== live_hash;
263917
+ const scoredDrift = (scored.score || 0) > 0;
263918
+ const isDrift = hashDiffers || scoredDrift;
263919
+ const unclassified = hashDiffers && !scoredDrift;
263920
+ const findings = (scored.signals || []).filter((s) => s && (s.impact == null || Number(s.impact) > 0)).map((s) => ({
263921
+ signal: s.signal,
263922
+ impact: s.impact,
263923
+ detail: s.detail,
263924
+ data: s.data || null,
263925
+ // Honest framing: finding, not accusation
263926
+ kind: "measurable_change"
263927
+ }));
263928
+ findings.sort((a, b) => {
263929
+ const sa = String(a.signal || "");
263930
+ const sb = String(b.signal || "");
263931
+ if (sa !== sb) return sa < sb ? -1 : 1;
263932
+ return String(a.detail || "").localeCompare(String(b.detail || ""));
263933
+ });
263934
+ return {
263935
+ status: isDrift ? "drift" : "unchanged",
263936
+ drift: isDrift,
263937
+ server_id: serverId,
263938
+ locked_hash,
263939
+ live_hash,
263940
+ hash_differs: hashDiffers,
263941
+ risk_score: scored.score,
263942
+ level: scored.level,
263943
+ signals: scored.signals || [],
263944
+ findings,
263945
+ unclassified_change: unclassified,
263946
+ summary: unclassified ? "MCP manifest DIFFERS from the locked baseline (content hashes disagree), but no catalogued risk signal explains the difference. Treated as drift: an unrecognised change is still a change. Compare the manifests directly \u2014 the signal catalog is not exhaustive." : isDrift ? scored.summary || "MCP manifest differs from locked baseline (scored finding)." : "Live MCP manifest matches locked baseline under mcp-risk-scorer (no risk signals).",
263947
+ interpretation: "number_is_what_is",
263948
+ note: "Drift is a scored FINDING of measurable change (tool_removed, schema change, \u2026), not an accusation of poison vs legitimate update. Monitor only \u2014 does not block. Scoring: scoreMcpRisk (existing SIGNAL_CATALOG; no reimplemented weights).",
263949
+ scorer: "scoreMcpRisk",
263950
+ signal_catalog_version: "mcp-risk-scorer"
263951
+ };
263952
+ }
263953
+ function buildLockDriftReport(pairs) {
263954
+ const list = Array.isArray(pairs) ? pairs : [];
263955
+ const results = list.map((p, i) => buildLockDrift({
263956
+ ...p,
263957
+ serverId: p.serverId != null ? p.serverId : `server_${i}`
263958
+ })).sort((a, b) => String(a.server_id).localeCompare(String(b.server_id)));
263959
+ const anyDrift = results.some((r) => r.drift === true);
263960
+ const anyUnreachable = results.some((r) => r.status === "unreachable");
263961
+ const maxScore = results.reduce((m, r) => {
263962
+ if (r.risk_score == null || !Number.isFinite(r.risk_score)) return m;
263963
+ return Math.max(m, r.risk_score);
263964
+ }, 0);
263965
+ return {
263966
+ status: anyUnreachable && !anyDrift ? results.every((r) => r.status === "unreachable") ? "unreachable" : "partial" : anyDrift ? "drift" : "unchanged",
263967
+ drift: anyDrift,
263968
+ max_risk_score: maxScore,
263969
+ servers: results,
263970
+ summary: anyDrift ? `MCP lock drift on ${results.filter((r) => r.drift).length}/${results.length} server(s); max risk ${maxScore}.` : anyUnreachable ? "No scored drift; one or more live manifests unreachable (absence_is_unknown)." : "No MCP lock drift detected under scoreMcpRisk.",
263971
+ interpretation: "number_is_what_is",
263972
+ note: "Monitor report only. Existing poison-gate / policy engine remain the blockers. Unreachable is never treated as clean or as drift."
263973
+ };
263974
+ }
263975
+ module2.exports = {
263976
+ buildLockDrift,
263977
+ buildLockDriftReport,
263978
+ normalizeMcpManifest,
263979
+ manifestContentHash,
263980
+ extractLockedManifestsFromLock,
263981
+ SIGNAL_CATALOG
263982
+ // re-export for tests asserting reuse
263983
+ };
263984
+ }
263985
+ });
263986
+
263462
263987
  // src/commands/lock.js
263463
263988
  var require_lock = __commonJS({
263464
263989
  "src/commands/lock.js"(exports2, module2) {
@@ -263523,6 +264048,7 @@ var require_lock = __commonJS({
263523
264048
  return `${JSON.stringify(doc, null, 2)}
263524
264049
  `;
263525
264050
  }
264051
+ var mcpLockDrift = require_mcp_lock_drift();
263526
264052
  function loadDriftCore(deps = {}) {
263527
264053
  if (deps.buildLockDrift) {
263528
264054
  return {
@@ -263531,21 +264057,7 @@ var require_lock = __commonJS({
263531
264057
  extractLockedManifestsFromLock: deps.extractLockedManifestsFromLock
263532
264058
  };
263533
264059
  }
263534
- const candidates = [
263535
- path.join(__dirname, "../../../../src/core/mcp-lock-drift"),
263536
- path.join(__dirname, "../../../src/core/mcp-lock-drift")
263537
- ];
263538
- for (const c of candidates) {
263539
- try {
263540
- return require(c);
263541
- } catch (_) {
263542
- }
263543
- }
263544
- const err = new Error(
263545
- "Cannot load mcp-lock-drift core. Run from the coderifts-app monorepo (packages/cli expects ../../../../src/core/mcp-lock-drift)."
263546
- );
263547
- err.code = "CORE_UNAVAILABLE";
263548
- throw err;
264060
+ return mcpLockDrift;
263549
264061
  }
263550
264062
  function readJsonFile(filePath) {
263551
264063
  const raw = fs.readFileSync(filePath, "utf8");
@@ -263819,6 +264331,176 @@ var require_lock = __commonJS({
263819
264331
  }
263820
264332
  });
263821
264333
 
264334
+ // ../../src/core/counterfactual-adopt.js
264335
+ var require_counterfactual_adopt = __commonJS({
264336
+ "../../src/core/counterfactual-adopt.js"(exports2, module2) {
264337
+ "use strict";
264338
+ var { analyzeSpecs } = require_analyzer();
264339
+ var SCOPE_NOTE = "COUNTERFACTUAL only: each row is what analyzeSpecs would have returned on the supplied before\u2192after content. It does not assert the change was actually harmful, and Step A does not join real decision_outcomes (outcome correlation is Step B).";
264340
+ function mapDecision(result) {
264341
+ if (!result || typeof result !== "object") return "REQUIRE_APPROVAL";
264342
+ const od = result.omega_decision;
264343
+ if (od === "ALLOW" || od === "WARN" || od === "REQUIRE_APPROVAL" || od === "BLOCK") {
264344
+ return od;
264345
+ }
264346
+ if (result.should_block === true) return "BLOCK";
264347
+ const pats = Array.isArray(result.detected_patterns) ? result.detected_patterns : [];
264348
+ if (pats.some((p) => p && (p.severity === "CRITICAL" || p.severity === "HIGH"))) {
264349
+ return "BLOCK";
264350
+ }
264351
+ const breaking = result.breaking_changes;
264352
+ const n = Array.isArray(breaking) ? breaking.length : typeof breaking === "number" ? breaking : 0;
264353
+ if (n > 0) return "REQUIRE_APPROVAL";
264354
+ return "ALLOW";
264355
+ }
264356
+ function mapExecutionAction(decision) {
264357
+ if (decision === "BLOCK") return "STOP";
264358
+ if (decision === "REQUIRE_APPROVAL") return "REQUEST_APPROVAL";
264359
+ if (decision === "WARN") return "CONTINUE_WITH_MONITORING";
264360
+ return "CONTINUE";
264361
+ }
264362
+ function extractDetectors(result) {
264363
+ const pats = Array.isArray(result && result.detected_patterns) ? result.detected_patterns : [];
264364
+ const out = [];
264365
+ for (const p of pats) {
264366
+ if (!p || typeof p !== "object") continue;
264367
+ const id = p.name || p.id || p.type;
264368
+ if (!id) continue;
264369
+ out.push({
264370
+ id: String(id),
264371
+ severity: p.severity != null ? String(p.severity) : "UNKNOWN"
264372
+ });
264373
+ }
264374
+ return out;
264375
+ }
264376
+ async function buildCounterfactualReport(changeSets, config = {}) {
264377
+ if (!Array.isArray(changeSets) || changeSets.length === 0) {
264378
+ return {
264379
+ change_sets: [],
264380
+ per_detector: [],
264381
+ scope_note: SCOPE_NOTE,
264382
+ interpretation: "absence_is_unknown",
264383
+ summary: {
264384
+ change_set_count: 0,
264385
+ would_stop: 0,
264386
+ would_request_approval: 0,
264387
+ would_continue: 0,
264388
+ note: 'No contract-artifact change sets supplied \u2014 empty is UNKNOWN, not "clean".'
264389
+ }
264390
+ };
264391
+ }
264392
+ const rows = [];
264393
+ const detectorMap = /* @__PURE__ */ new Map();
264394
+ for (const cs of changeSets) {
264395
+ const ref = cs && (cs.ref || cs.sha) || null;
264396
+ const date = cs && cs.date || null;
264397
+ const path = cs && cs.path || null;
264398
+ const before = cs && cs.before_spec != null ? String(cs.before_spec) : "";
264399
+ const after = cs && cs.after_spec != null ? String(cs.after_spec) : "";
264400
+ let result;
264401
+ let error = null;
264402
+ try {
264403
+ result = await analyzeSpecs(before, after, config || {});
264404
+ } catch (err) {
264405
+ error = {
264406
+ code: err && err.code ? String(err.code) : "analyze_error",
264407
+ message: err && err.message ? String(err.message).slice(0, 300) : "analyzeSpecs failed"
264408
+ };
264409
+ result = null;
264410
+ }
264411
+ if (!result) {
264412
+ rows.push({
264413
+ ref,
264414
+ date,
264415
+ path,
264416
+ decision: null,
264417
+ execution_action: null,
264418
+ risk_score: null,
264419
+ detectors_fired: [],
264420
+ would_have: null,
264421
+ error,
264422
+ counterfactual: true
264423
+ });
264424
+ continue;
264425
+ }
264426
+ const decision = mapDecision(result);
264427
+ const execution_action = mapExecutionAction(decision);
264428
+ const risk_score = result.risk_score != null && Number.isFinite(Number(result.risk_score)) ? Number(result.risk_score) : null;
264429
+ const detectors_fired = extractDetectors(result);
264430
+ for (const d of detectors_fired) {
264431
+ const prev = detectorMap.get(d.id);
264432
+ if (!prev) {
264433
+ detectorMap.set(d.id, { id: d.id, severity: d.severity, fire_count: 1 });
264434
+ } else {
264435
+ prev.fire_count += 1;
264436
+ prev.severity = worseSeverity(prev.severity, d.severity);
264437
+ }
264438
+ }
264439
+ rows.push({
264440
+ ref,
264441
+ date,
264442
+ path,
264443
+ decision,
264444
+ execution_action,
264445
+ risk_score,
264446
+ detectors_fired,
264447
+ would_have: {
264448
+ decision,
264449
+ execution_action,
264450
+ risk_score
264451
+ },
264452
+ error: null,
264453
+ counterfactual: true
264454
+ });
264455
+ }
264456
+ const per_detector = [...detectorMap.values()].sort((a, b) => b.fire_count - a.fire_count || a.id.localeCompare(b.id));
264457
+ let would_stop = 0;
264458
+ let would_request_approval = 0;
264459
+ let would_continue = 0;
264460
+ for (const r of rows) {
264461
+ if (r.execution_action === "STOP") would_stop += 1;
264462
+ else if (r.execution_action === "REQUEST_APPROVAL") would_request_approval += 1;
264463
+ else if (r.execution_action === "CONTINUE" || r.execution_action === "CONTINUE_WITH_MONITORING") {
264464
+ would_continue += 1;
264465
+ }
264466
+ }
264467
+ return {
264468
+ change_sets: rows,
264469
+ per_detector,
264470
+ scope_note: SCOPE_NOTE,
264471
+ interpretation: "counterfactual_observed",
264472
+ summary: {
264473
+ change_set_count: rows.length,
264474
+ would_stop,
264475
+ would_request_approval,
264476
+ would_continue,
264477
+ note: "Per-detector fire_count is the default-flip evidence input: high-volume detectors are false-positive noise candidates; rare+severe detectors are safer default-block candidates. This report does NOT auto-recommend a flip set."
264478
+ }
264479
+ };
264480
+ }
264481
+ var SEV_RANK = {
264482
+ CRITICAL: 4,
264483
+ HIGH: 3,
264484
+ MEDIUM: 2,
264485
+ LOW: 1,
264486
+ INFO: 0,
264487
+ UNKNOWN: 0
264488
+ };
264489
+ function worseSeverity(a, b) {
264490
+ const ra = SEV_RANK[String(a).toUpperCase()] != null ? SEV_RANK[String(a).toUpperCase()] : 0;
264491
+ const rb = SEV_RANK[String(b).toUpperCase()] != null ? SEV_RANK[String(b).toUpperCase()] : 0;
264492
+ return rb > ra ? b : a;
264493
+ }
264494
+ module2.exports = {
264495
+ buildCounterfactualReport,
264496
+ mapDecision,
264497
+ mapExecutionAction,
264498
+ extractDetectors,
264499
+ SCOPE_NOTE
264500
+ };
264501
+ }
264502
+ });
264503
+
263822
264504
  // src/commands/adopt.js
263823
264505
  var require_adopt = __commonJS({
263824
264506
  "src/commands/adopt.js"(exports2, module2) {
@@ -263883,20 +264565,9 @@ var require_adopt = __commonJS({
263883
264565
  else process.env.LOG_LEVEL = prevEnv;
263884
264566
  }
263885
264567
  }
264568
+ var counterfactualAdopt = require_counterfactual_adopt();
263886
264569
  function loadCore() {
263887
- const candidates = [
263888
- path.join(__dirname, "../../../../src/core/counterfactual-adopt"),
263889
- path.join(__dirname, "../../../src/core/counterfactual-adopt")
263890
- ];
263891
- for (const c of candidates) {
263892
- try {
263893
- return require(c);
263894
- } catch (_) {
263895
- }
263896
- }
263897
- throw new Error(
263898
- "Cannot load counterfactual-adopt core. Run from the coderifts-app monorepo (packages/cli expects ../../../../src/core/counterfactual-adopt)."
263899
- );
264570
+ return counterfactualAdopt;
263900
264571
  }
263901
264572
  function listCommits({ commits, days, gitImpl, cwd }) {
263902
264573
  const args = ["log", "--format=%H %cI", "--reverse"];