coderifts 8.6.2 → 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.2",
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",
@@ -3056,13 +3056,12 @@ var require_package = __commonJS({
3056
3056
  "swagger",
3057
3057
  "devtools"
3058
3058
  ],
3059
- repository: {
3060
- type: "git",
3061
- url: "git+https://github.com/coderifts/app.git",
3062
- directory: "packages/cli"
3063
- },
3059
+ repository: "https://coderifts.com",
3064
3060
  homepage: "https://coderifts.com",
3065
- bugs: "https://github.com/coderifts/app/issues",
3061
+ bugs: {
3062
+ url: "https://coderifts.com",
3063
+ email: "support@coderifts.com"
3064
+ },
3066
3065
  engines: {
3067
3066
  node: ">=20.0.0"
3068
3067
  },
@@ -3080,7 +3079,7 @@ var require_package = __commonJS({
3080
3079
  "cli-table3": "^0.6.4",
3081
3080
  commander: "^12.0.0",
3082
3081
  inquirer: "^8.2.6",
3083
- "js-yaml": "^4.3.1",
3082
+ "js-yaml": "^4.3.2",
3084
3083
  "openapi-diff": "^0.24.1",
3085
3084
  ora: "^5.4.1"
3086
3085
  },
@@ -11701,16 +11700,21 @@ var require_loader = __commonJS({
11701
11700
  state.result += _result;
11702
11701
  }
11703
11702
  }
11703
+ function chargeMergeWork(state) {
11704
+ state.totalMergeKeys++;
11705
+ if (state.maxTotalMergeKeys !== -1 && state.totalMergeKeys > state.maxTotalMergeKeys) {
11706
+ throwError(state, "merge keys exceeded maxTotalMergeKeys (" + state.maxTotalMergeKeys + ")");
11707
+ }
11708
+ }
11704
11709
  function mergeMappings(state, destination, source, overridableKeys) {
11705
11710
  if (!common.isObject(source)) {
11706
11711
  throwError(state, "cannot merge mappings; the provided source object is unacceptable");
11707
11712
  }
11713
+ chargeMergeWork(state);
11708
11714
  const sourceKeys = Object.keys(source);
11709
11715
  for (let index = 0, quantity = sourceKeys.length; index < quantity; index += 1) {
11710
11716
  const key = sourceKeys[index];
11711
- if (state.maxTotalMergeKeys !== -1 && ++state.totalMergeKeys > state.maxTotalMergeKeys) {
11712
- throwError(state, "merge keys exceeded maxTotalMergeKeys (" + state.maxTotalMergeKeys + ")");
11713
- }
11717
+ chargeMergeWork(state);
11714
11718
  if (!_hasOwnProperty.call(destination, key)) {
11715
11719
  setProperty(destination, key, source[key]);
11716
11720
  overridableKeys[key] = true;
@@ -11738,6 +11742,9 @@ var require_loader = __commonJS({
11738
11742
  }
11739
11743
  if (keyTag === "tag:yaml.org,2002:merge") {
11740
11744
  if (Array.isArray(valueNode)) {
11745
+ if (valueNode.length > 100) {
11746
+ throwError(state, "abnormal merge sequence size");
11747
+ }
11741
11748
  for (let index = 0, quantity = valueNode.length; index < quantity; index += 1) {
11742
11749
  mergeMappings(state, _result, valueNode[index], overridableKeys);
11743
11750
  }
@@ -163516,16 +163523,21 @@ var require_loader2 = __commonJS({
163516
163523
  state.result += _result;
163517
163524
  }
163518
163525
  }
163526
+ function chargeMergeWork(state) {
163527
+ state.totalMergeKeys++;
163528
+ if (state.maxTotalMergeKeys !== -1 && state.totalMergeKeys > state.maxTotalMergeKeys) {
163529
+ throwError(state, "merge keys exceeded maxTotalMergeKeys (" + state.maxTotalMergeKeys + ")");
163530
+ }
163531
+ }
163519
163532
  function mergeMappings(state, destination, source, overridableKeys) {
163520
163533
  if (!common.isObject(source)) {
163521
163534
  throwError(state, "cannot merge mappings; the provided source object is unacceptable");
163522
163535
  }
163536
+ chargeMergeWork(state);
163523
163537
  const sourceKeys = Object.keys(source);
163524
163538
  for (let index = 0, quantity = sourceKeys.length; index < quantity; index += 1) {
163525
163539
  const key = sourceKeys[index];
163526
- if (state.maxTotalMergeKeys !== -1 && ++state.totalMergeKeys > state.maxTotalMergeKeys) {
163527
- throwError(state, "merge keys exceeded maxTotalMergeKeys (" + state.maxTotalMergeKeys + ")");
163528
- }
163540
+ chargeMergeWork(state);
163529
163541
  if (!_hasOwnProperty.call(destination, key)) {
163530
163542
  setProperty(destination, key, source[key]);
163531
163543
  overridableKeys[key] = true;
@@ -163553,6 +163565,9 @@ var require_loader2 = __commonJS({
163553
163565
  }
163554
163566
  if (keyTag === "tag:yaml.org,2002:merge") {
163555
163567
  if (Array.isArray(valueNode)) {
163568
+ if (valueNode.length > 100) {
163569
+ throwError(state, "abnormal merge sequence size");
163570
+ }
163556
163571
  for (let index = 0, quantity = valueNode.length; index < quantity; index += 1) {
163557
163572
  mergeMappings(state, _result, valueNode[index], overridableKeys);
163558
163573
  }
@@ -168595,7 +168610,7 @@ var require_picomatch2 = __commonJS({
168595
168610
  var require_mcp_poison_gate = __commonJS({
168596
168611
  "../../src/mcp-poison-gate.js"(exports2, module2) {
168597
168612
  "use strict";
168598
- var MCP_POISON_GATE_VERSION = "mpg-1";
168613
+ var MCP_POISON_GATE_VERSION = "mpg-2";
168599
168614
  var BLOCK_TYPES = /* @__PURE__ */ new Set([
168600
168615
  "suspicious_instruction",
168601
168616
  "exfiltration_url_cosignal",
@@ -168604,7 +168619,8 @@ var require_mcp_poison_gate = __commonJS({
168604
168619
  ]);
168605
168620
  var RA_TYPES = /* @__PURE__ */ new Set([
168606
168621
  "structural_anomaly",
168607
- "hidden_encoding"
168622
+ "hidden_encoding",
168623
+ "instruction_heuristic"
168608
168624
  ]);
168609
168625
  var DECISION_ORDER = { ALLOW: 0, WARN: 1, REQUIRE_APPROVAL: 2, BLOCK: 3 };
168610
168626
  function isLowerThan(decision, floor) {
@@ -205500,6 +205516,16 @@ var require_mcp_poisoning_detector = __commonJS({
205500
205516
  "use strict";
205501
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)";
205502
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
+ }
205503
205529
  var SUSPICIOUS_PATTERNS = [
205504
205530
  // Injection / "ignore your instructions" family. The trailing object
205505
205531
  // (instructions|rules|prompts|context) is OPTIONAL so "ignore previous and ..."
@@ -205509,9 +205535,18 @@ var require_mcp_poisoning_detector = __commonJS({
205509
205535
  // ~40 chars so benign "always include the source URL" stays quiet.
205510
205536
  new RegExp("always\\s+(include|append|send|attach|return|add)\\b[\\s\\S]{0,40}?\\b" + SENSITIVE_OBJECT + "\\b", "i"),
205511
205537
  /you\s+(must|should|have to)\s+(now|always|instead)/i,
205512
- /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,
205513
205545
  // Exfiltration-intent family: an action verb targeting a sensitive payload.
205514
- /\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,
205515
205550
  // Return/echo scoped sensitive material without a path ("return any stored credentials").
205516
205551
  // Scope word required — bare "return the new API token" must stay quiet.
205517
205552
  new RegExp(
@@ -205531,23 +205566,39 @@ var require_mcp_poisoning_detector = __commonJS({
205531
205566
  /bypass\s+(auth|security|validation|filter)/i,
205532
205567
  /override\s+(safety|policy|rules?)/i,
205533
205568
  /do\s+not\s+(follow|respect|obey)\s+(original|previous)/i,
205534
- /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,
205535
205582
  /\[system\]|<\s*system\s*>/i,
205536
- /always\s+return\s+(success|ok|true)/i,
205583
+ ALWAYS_RETURN_OK,
205537
205584
  /hide\s+(errors|failures|issues)/i,
205538
205585
  /return\s+secret|exfiltrat|leak\s+data/i
205539
205586
  ];
205540
205587
  var URL_PATTERN = /https?:\/\/[^\s)'"]+/i;
205541
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;
205542
- 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;
205543
205590
  var BASE64_BLOB_PATTERN = /[A-Za-z0-9+/]{40,}={0,2}/;
205544
205591
  function isLikelyBase64(s) {
205545
205592
  if (/=$/.test(s)) return true;
205546
205593
  return /[A-Z]/.test(s) && /[a-z]/.test(s) && /[0-9]/.test(s);
205547
205594
  }
205595
+ function patternHits(regex, str) {
205596
+ if (regex === MODE_PHRASE) return modePhraseHits(str);
205597
+ return regex.test(str);
205598
+ }
205548
205599
  function hasInjectionIntent(str) {
205549
205600
  if (typeof str !== "string") return false;
205550
- if (SUSPICIOUS_PATTERNS.some((re) => re.test(str))) return true;
205601
+ if (SUSPICIOUS_PATTERNS.some((re) => patternHits(re, str))) return true;
205551
205602
  if (SENSITIVE_PATH_PATTERN.test(str) && CONTENT_MOVE_VERB_PATTERN.test(str)) return true;
205552
205603
  return false;
205553
205604
  }
@@ -205555,9 +205606,9 @@ var require_mcp_poisoning_detector = __commonJS({
205555
205606
  if (typeof value !== "string" || value.length < 5) return [];
205556
205607
  const findings = [];
205557
205608
  for (const regex of SUSPICIOUS_PATTERNS) {
205558
- if (regex.test(value)) {
205609
+ if (patternHits(regex, value)) {
205559
205610
  findings.push({
205560
- type: "suspicious_instruction",
205611
+ type: findingTypeFor(regex),
205561
205612
  path,
205562
205613
  snippet: value.substring(0, 120),
205563
205614
  severity: "HIGH",
@@ -205728,6 +205779,130 @@ var require_mcp_poisoning_detector = __commonJS({
205728
205779
  }
205729
205780
  });
205730
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
+
205731
205906
  // ../../src/remediation-taxonomy.js
205732
205907
  var require_remediation_taxonomy = __commonJS({
205733
205908
  "../../src/remediation-taxonomy.js"(exports2, module2) {
@@ -208140,7 +208315,7 @@ var require_decision_result_v1_producer = __commonJS({
208140
208315
  blast_radius: {
208141
208316
  type: "object",
208142
208317
  additionalProperties: false,
208143
- 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.",
208144
208319
  required: [
208145
208320
  "endpoints",
208146
208321
  "fields",
@@ -208782,7 +208957,7 @@ var require_execution_grant_request_v2_producer = __commonJS({
208782
208957
  "v2"
208783
208958
  ],
208784
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.",
208785
- "x-coderifts-effective-default": "v1",
208960
+ "x-coderifts-effective-default": "v2",
208786
208961
  "x-coderifts-default-changes-at": "2026-09-18"
208787
208962
  },
208788
208963
  grantVersion: {
@@ -208792,7 +208967,7 @@ var require_execution_grant_request_v2_producer = __commonJS({
208792
208967
  "v2"
208793
208968
  ],
208794
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).",
208795
- "x-coderifts-effective-default": "v1",
208970
+ "x-coderifts-effective-default": "v2",
208796
208971
  "x-coderifts-default-changes-at": "2026-09-18"
208797
208972
  },
208798
208973
  tenant_id: {
@@ -210370,6 +210545,11 @@ var require_change_set = __commonJS({
210370
210545
  var { scoreMcpRisk } = require_mcp_risk_scorer();
210371
210546
  var { detectPoisoning } = require_mcp_poisoning_detector();
210372
210547
  var { classifyPoison, isLowerThan } = require_mcp_poison_gate();
210548
+ var {
210549
+ classifyDescriptionChange,
210550
+ addedText,
210551
+ PATTERN: TOOL_INSTRUCTION_DRIFT
210552
+ } = require_tool_description_classifier();
210373
210553
  var { parse: parseGraphql } = require_graphql2();
210374
210554
  var {
210375
210555
  ensureDecisionSpec,
@@ -210599,25 +210779,83 @@ var require_change_set = __commonJS({
210599
210779
  ensureDecisionSpec(art);
210600
210780
  return finding(art);
210601
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
+ }
210602
210833
  function analyzeMcpManifest(before, after) {
210603
210834
  const oldMcp = parseManifest(before);
210604
210835
  const newMcp = parseManifest(after);
210605
210836
  const risk = scoreMcpRisk({ oldManifest: oldMcp, newManifest: newMcp });
210606
210837
  const breaking = risk.signals.filter((s) => (s.impact || 0) > 0).length;
210607
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
+ }
210608
210842
  const art = {
210609
210843
  risk_score: risk.score,
210610
210844
  breaking_changes: breaking,
210611
210845
  patterns
210612
210846
  };
210613
210847
  if (risk.score >= 50) art.should_block = true;
210614
- const poisonGate = classifyPoison(detectPoisoning(oldMcp, newMcp));
210848
+ const poisonGate = classifyPoison(detectPoisoning(oldMcp, manifestWithAddedSentences(oldMcp, newMcp)));
210615
210849
  if (poisonGate.tier === "block") art.omega_decision = "BLOCK";
210616
210850
  ensureDecisionSpec(art);
210617
210851
  if (poisonGate.tier === "ra" && isLowerThan(art.decision, "REQUIRE_APPROVAL")) {
210618
210852
  art.decision = "REQUIRE_APPROVAL";
210619
210853
  ensureDecisionSpec(art);
210620
210854
  }
210855
+ if (patterns.includes(TOOL_INSTRUCTION_DRIFT) && isLowerThan(art.decision, "REQUIRE_APPROVAL")) {
210856
+ art.decision = "REQUIRE_APPROVAL";
210857
+ ensureDecisionSpec(art);
210858
+ }
210621
210859
  return finding(art);
210622
210860
  }
210623
210861
  function parseAgentToolsPayload(v) {
@@ -210715,7 +210953,23 @@ var require_change_set = __commonJS({
210715
210953
  reflex_triggers: json.reflex_triggers,
210716
210954
  omega_components: json.omega_components
210717
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
+ }
210718
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
+ }
210719
210973
  return finding(art);
210720
210974
  }
210721
210975
  var ANALYZE_PERMISSION_KEYS = Object.freeze(["decision", "safe_for_agent", "execution_action"]);
@@ -210870,6 +211124,7 @@ var require_change_set = __commonJS({
210870
211124
  var DECISION_SPEC_VERSION = "2.0";
210871
211125
  var DECISION_SPEC_LEGACY_VERSION = "1.0";
210872
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);
210873
211128
  var ANALYSIS_OUTCOMES = Object.freeze({
210874
211129
  NO_BREAK_DETECTED: "NO_BREAK_DETECTED",
210875
211130
  BREAKS_DETECTED: "BREAKS_DETECTED",
@@ -210881,6 +211136,18 @@ var require_change_set = __commonJS({
210881
211136
  const s = String(raw).trim();
210882
211137
  return s === "1.0" || s === "1" || s === "decision-result.v1" || s === "decision-result.v1.0";
210883
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
+ }
210884
211151
  function deriveAnalysisOutcome({ hasDegraded, decision, breaking_changes }) {
210885
211152
  if (hasDegraded) return ANALYSIS_OUTCOMES.ANALYSIS_FAILED;
210886
211153
  if (decision !== "ALLOW" || (breaking_changes || 0) > 0) return ANALYSIS_OUTCOMES.BREAKS_DETECTED;
@@ -210892,7 +211159,7 @@ var require_change_set = __commonJS({
210892
211159
  if (legacy) return { mode: "analyze", defaulted: true };
210893
211160
  throw typedError(
210894
211161
  "INVALID_INPUT",
210895
- "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."
210896
211163
  );
210897
211164
  }
210898
211165
  if (raw === "analyze" || raw === "authorize") {
@@ -210931,6 +211198,7 @@ var require_change_set = __commonJS({
210931
211198
  if (seen.has(key)) throw typedError("INVALID_INPUT", `duplicate artifact (type, id): (${a.type}, ${a.id})`);
210932
211199
  seen.add(key);
210933
211200
  }
211201
+ rejectRetiredLegacyPin(input, deps);
210934
211202
  const legacyPin = isLegacyDecisionSpecPin(input);
210935
211203
  const { mode: preflightMode, defaulted: preflightModeDefaulted } = resolvePreflightMode(input, {
210936
211204
  legacy: legacyPin
@@ -211501,6 +211769,7 @@ var require_change_set = __commonJS({
211501
211769
  resolvePreflightMode,
211502
211770
  deriveAnalysisOutcome,
211503
211771
  isLegacyDecisionSpecPin,
211772
+ isLegacyDecisionSpecPinRetired,
211504
211773
  artifactSidesIdentical,
211505
211774
  isNoChangeBundle,
211506
211775
  isContractChangeTrigger,
@@ -211510,6 +211779,7 @@ var require_change_set = __commonJS({
211510
211779
  DECISION_SPEC_VERSION,
211511
211780
  DECISION_SPEC_LEGACY_VERSION,
211512
211781
  DECISION_SPEC_LEGACY_SUNSET,
211782
+ DECISION_SPEC_LEGACY_SUNSET_AT_MS,
211513
211783
  ANALYSIS_OUTCOMES,
211514
211784
  MAX_ARTIFACTS,
211515
211785
  SUPPORTED_TYPES,
@@ -224867,17 +225137,17 @@ var require_mcp_decision_result_shape = __commonJS({
224867
225137
  audience: { type: ["string", "null"] },
224868
225138
  authority: {
224869
225139
  type: ["object", "null"],
224870
- 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."
224871
225141
  },
224872
225142
  derivation: {
224873
225143
  type: ["object", "null"],
224874
- 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.'
224875
225145
  },
224876
225146
  receipt: { type: "object" },
224877
225147
  expires_at: { type: "string" },
224878
225148
  blast_radius: {
224879
225149
  type: "object",
224880
- description: "ID27 additive COUNTS (not a score). Not permission.",
225150
+ description: "Additive COUNTS (not a score). Not permission.",
224881
225151
  properties: {
224882
225152
  endpoints: { type: "integer", minimum: 0 },
224883
225153
  fields: { type: "integer", minimum: 0 },
@@ -225016,7 +225286,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225016
225286
  },
225017
225287
  "detected_patterns": {
225018
225288
  "type": "array",
225019
- "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.",
225020
225290
  "items": {
225021
225291
  "type": "object",
225022
225292
  "additionalProperties": false,
@@ -225066,7 +225336,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225066
225336
  },
225067
225337
  "breaking_changes_details": {
225068
225338
  "type": "array",
225069
- "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).",
225070
225340
  "items": {
225071
225341
  "type": "object",
225072
225342
  "additionalProperties": true,
@@ -225095,7 +225365,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225095
225365
  },
225096
225366
  "severity_summary": {
225097
225367
  "type": "object",
225098
- "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.",
225099
225369
  "additionalProperties": false,
225100
225370
  "properties": {
225101
225371
  "diff_severity": {
@@ -225118,7 +225388,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225118
225388
  "decision_basis": {},
225119
225389
  "analysis": {
225120
225390
  "type": "object",
225121
- "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.",
225122
225392
  "properties": {
225123
225393
  "breaking_changes": {
225124
225394
  "type": "integer"
@@ -225245,7 +225515,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225245
225515
  },
225246
225516
  "human_report": {
225247
225517
  "type": "object",
225248
- "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.",
225249
225519
  "additionalProperties": false,
225250
225520
  "properties": {
225251
225521
  "summary": {
@@ -225305,7 +225575,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225305
225575
  "blast_radius": {
225306
225576
  "type": "object",
225307
225577
  "additionalProperties": false,
225308
- "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.",
225309
225579
  "required": [
225310
225580
  "endpoints",
225311
225581
  "fields",
@@ -225470,7 +225740,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225470
225740
  },
225471
225741
  "detected_patterns": {
225472
225742
  "type": "array",
225473
- "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.",
225474
225744
  "items": {
225475
225745
  "type": "object",
225476
225746
  "additionalProperties": false,
@@ -225520,7 +225790,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225520
225790
  },
225521
225791
  "breaking_changes_details": {
225522
225792
  "type": "array",
225523
- "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).",
225524
225794
  "items": {
225525
225795
  "type": "object",
225526
225796
  "additionalProperties": true,
@@ -225549,7 +225819,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225549
225819
  },
225550
225820
  "severity_summary": {
225551
225821
  "type": "object",
225552
- "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.",
225553
225823
  "additionalProperties": false,
225554
225824
  "properties": {
225555
225825
  "diff_severity": {
@@ -225572,7 +225842,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225572
225842
  "decision_basis": {},
225573
225843
  "analysis": {
225574
225844
  "type": "object",
225575
- "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.",
225576
225846
  "properties": {
225577
225847
  "breaking_changes": {
225578
225848
  "type": "integer"
@@ -225699,7 +225969,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225699
225969
  },
225700
225970
  "human_report": {
225701
225971
  "type": "object",
225702
- "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.",
225703
225973
  "additionalProperties": false,
225704
225974
  "properties": {
225705
225975
  "summary": {
@@ -225721,7 +225991,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225721
225991
  },
225722
225992
  "execution_grant": {
225723
225993
  "type": "string",
225724
- "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."
225725
225995
  },
225726
225996
  "chain_status": {
225727
225997
  "type": "string"
@@ -225811,14 +226081,14 @@ var require_mcp_preflight_output_schema = __commonJS({
225811
226081
  "object",
225812
226082
  "null"
225813
226083
  ],
225814
- "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."
225815
226085
  },
225816
226086
  "derivation": {
225817
226087
  "type": [
225818
226088
  "object",
225819
226089
  "null"
225820
226090
  ],
225821
- "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.'
225822
226092
  },
225823
226093
  "receipt": {
225824
226094
  "type": "object"
@@ -225828,7 +226098,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225828
226098
  },
225829
226099
  "blast_radius": {
225830
226100
  "type": "object",
225831
- "description": "ID27 additive COUNTS (not a score). Not permission.",
226101
+ "description": "Additive COUNTS (not a score). Not permission.",
225832
226102
  "properties": {
225833
226103
  "endpoints": {
225834
226104
  "type": "integer",
@@ -225906,7 +226176,7 @@ var require_mcp_preflight_output_schema = __commonJS({
225906
226176
  "blast_radius": {
225907
226177
  "type": "object",
225908
226178
  "additionalProperties": false,
225909
- "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.",
225910
226180
  "required": [
225911
226181
  "endpoints",
225912
226182
  "fields",
@@ -226065,7 +226335,7 @@ var require_mcp_preflight_output_schema = __commonJS({
226065
226335
  },
226066
226336
  "detected_patterns": {
226067
226337
  "type": "array",
226068
- "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.",
226069
226339
  "items": {
226070
226340
  "type": "object",
226071
226341
  "additionalProperties": false,
@@ -226115,7 +226385,7 @@ var require_mcp_preflight_output_schema = __commonJS({
226115
226385
  },
226116
226386
  "breaking_changes_details": {
226117
226387
  "type": "array",
226118
- "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).",
226119
226389
  "items": {
226120
226390
  "type": "object",
226121
226391
  "additionalProperties": true,
@@ -226144,7 +226414,7 @@ var require_mcp_preflight_output_schema = __commonJS({
226144
226414
  },
226145
226415
  "severity_summary": {
226146
226416
  "type": "object",
226147
- "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.",
226148
226418
  "additionalProperties": false,
226149
226419
  "properties": {
226150
226420
  "diff_severity": {
@@ -226170,7 +226440,7 @@ var require_mcp_preflight_output_schema = __commonJS({
226170
226440
  "decision_basis": {},
226171
226441
  "analysis": {
226172
226442
  "type": "object",
226173
- "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.",
226174
226444
  "properties": {
226175
226445
  "breaking_changes": {
226176
226446
  "type": "integer"
@@ -226297,7 +226567,7 @@ var require_mcp_preflight_output_schema = __commonJS({
226297
226567
  },
226298
226568
  "human_report": {
226299
226569
  "type": "object",
226300
- "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.",
226301
226571
  "additionalProperties": false,
226302
226572
  "properties": {
226303
226573
  "summary": {
@@ -226408,14 +226678,14 @@ var require_mcp_preflight_output_schema = __commonJS({
226408
226678
  "object",
226409
226679
  "null"
226410
226680
  ],
226411
- "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."
226412
226682
  },
226413
226683
  "derivation": {
226414
226684
  "type": [
226415
226685
  "object",
226416
226686
  "null"
226417
226687
  ],
226418
- "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.'
226419
226689
  },
226420
226690
  "receipt": {
226421
226691
  "type": "object"
@@ -226425,7 +226695,7 @@ var require_mcp_preflight_output_schema = __commonJS({
226425
226695
  },
226426
226696
  "blast_radius": {
226427
226697
  "type": "object",
226428
- "description": "ID27 additive COUNTS (not a score). Not permission.",
226698
+ "description": "Additive COUNTS (not a score). Not permission.",
226429
226699
  "properties": {
226430
226700
  "endpoints": {
226431
226701
  "type": "integer",
@@ -227321,7 +227591,7 @@ var require_mcp_streamable = __commonJS({
227321
227591
  });
227322
227592
  next();
227323
227593
  });
227324
- 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.';
227325
227595
  var DESC_VERIFY_RECEIPT = `Verify a CodeRifts signed chain-receipt you ALREADY HOLD: cryptographic
227326
227596
  authenticity (signature + key id), body binding, and \u2014 when lifecycle indices
227327
227597
  are available \u2014 whether it is currently valid authorization (not expired,
@@ -227561,23 +227831,33 @@ next_agent_step is a suggestion, not permission.`;
227561
227831
  properties: {
227562
227832
  artifacts: {
227563
227833
  type: "array",
227564
- 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".',
227565
227835
  minItems: 1,
227566
227836
  maxItems: 20,
227567
227837
  items: {
227568
227838
  type: "object",
227569
227839
  properties: {
227570
227840
  id: { type: "string", description: "Caller-chosen id, unique within the bundle" },
227571
- type: { type: "string", enum: ["openapi", "graphql", "grpc", "asyncapi", "mcp_manifest", "agent_tools"] },
227572
- before: { type: "string", description: "Prior artifact content (spec/schema/manifest) as a string" },
227573
- 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
+ }
227574
227854
  },
227575
227855
  required: ["id", "type", "before", "after"]
227576
227856
  }
227577
227857
  },
227578
227858
  context: {
227579
227859
  type: "object",
227580
- 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.",
227581
227861
  properties: {
227582
227862
  // Server: context.operation is String(context.operation) with no closed enum
227583
227863
  // (src/change-set.js computeBundleFingerprint / envelopeFields). Document conventional set.
@@ -227607,14 +227887,17 @@ next_agent_step is a suggestion, not permission.`;
227607
227887
  // OPEN: unknown context keys are accepted (not 400) and ignored by buildChangeSet
227608
227888
  // except the keys it reads. Do not set additionalProperties:false.
227609
227889
  },
227610
- 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
+ },
227611
227894
  include_execution_grant: {
227612
227895
  type: "boolean",
227613
- 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".'
227614
227897
  },
227615
227898
  state_nonce: {
227616
227899
  type: "string",
227617
- 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."
227618
227901
  },
227619
227902
  // ── cr.exec.v2 authorize fields (1130-F1) ────────────────────────────────────────
227620
227903
  // DECLARED, not added: the authorize handler already reads every one of these
@@ -227628,33 +227911,33 @@ next_agent_step is a suggestion, not permission.`;
227628
227911
  // 1363: JSON Schema `default` is a SINGLE static value. A date-gated default cannot
227629
227912
  // be that without lying on one side of the cutoff. The 1344 mechanism (resolver +
227630
227913
  // GRANT_DEFAULT_V2_DATE) is unchanged; this is the schema expression of it.
227631
- // 1384: the TEMPLATE here is the frozen surface constant (GRANT_VERSION_V1).
227632
- // visibleTools() stamps the LIVE omit-value from resolveGrantVersion so the
227633
- // served header is truthful on both sides of 2026-09-18. tools_sha256 hashes
227634
- // 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.
227635
227918
  "x-coderifts-effective-default": GRANT_VERSION_V1,
227636
227919
  "x-coderifts-default-changes-at": GRANT_DEFAULT_V2_DATE,
227637
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.'
227638
227921
  },
227639
227922
  tenant_id: {
227640
227923
  type: "string",
227641
- 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.'
227642
227925
  },
227643
227926
  executor_id: {
227644
227927
  type: "string",
227645
- 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".'
227646
227929
  },
227647
227930
  adapter_id: {
227648
227931
  type: "string",
227649
- 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."
227650
227933
  },
227651
227934
  target_uri: {
227652
227935
  type: "string",
227653
- 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."
227654
227937
  },
227655
227938
  expected_state_token: {
227656
227939
  type: "string",
227657
- 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.'
227658
227941
  },
227659
227942
  audience: {
227660
227943
  type: "string",
@@ -227662,25 +227945,25 @@ next_agent_step is a suggestion, not permission.`;
227662
227945
  },
227663
227946
  policy_hash: {
227664
227947
  type: "string",
227665
- 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."
227666
227949
  },
227667
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." },
227668
227951
  // TOP-LEVEL (not context). Decision Spec 2.0: REQUIRED on every call.
227669
- // 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.
227670
227953
  preflight_mode: {
227671
227954
  type: "string",
227672
227955
  enum: ["analyze", "authorize"],
227673
- 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.'
227674
227957
  },
227675
227958
  decision_spec_version: {
227676
227959
  type: "string",
227677
- enum: ["1.0", "2.0"],
227678
- 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."
227679
227962
  },
227680
227963
  derivation: {
227681
227964
  type: "string",
227682
227965
  enum: ["server"],
227683
- 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.'
227684
227967
  }
227685
227968
  },
227686
227969
  // artifacts is NOT in required: derivation:"server" omits it (server lists the set).
@@ -227780,7 +228063,7 @@ next_agent_step is a suggestion, not permission.`;
227780
228063
  enum: RECEIPT_STATUSES.slice()
227781
228064
  },
227782
228065
  reason: { type: ["string", "null"] },
227783
- 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" },
227784
228067
  payload: { type: "object" },
227785
228068
  // Authorization layer (§106): signature-valid ≠ currently-authorized.
227786
228069
  // null means authorization could NOT be evaluated (missing intended context and/or
@@ -228301,9 +228584,12 @@ var require_generate_surface_anchor = __commonJS({
228301
228584
  var OUT = path.join(__dirname, "..", "src", "generated", "surface-anchor.json");
228302
228585
  var ENDPOINT = "https://app.coderifts.com/mcp";
228303
228586
  var VOLATILE_KEYS = Object.freeze([]);
228304
- var SOURCE_REF = "https://raw.githubusercontent.com/coderifts/api-governance/surface-dc86678e/tools.wire.v1.json";
228305
- var SOURCE_REF_COMMIT = "e0a9ae6fabef489c7f0f13714396add51519c195";
228306
- 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.";
228307
228593
  var HONESTY = Object.freeze({
228308
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.",
228309
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.",
@@ -228322,10 +228608,25 @@ var require_generate_surface_anchor = __commonJS({
228322
228608
  }
228323
228609
  return v;
228324
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
+ }
228325
228626
  function buildAnchor(opts = {}) {
228326
228627
  const includeAdvanced = opts.includeAdvanced === true;
228327
228628
  const tools = mcp.visibleTools({ includeAdvanced, now: opts.now });
228328
- const digestTools = typeof mcp.toolsForSurfaceDigest === "function" ? mcp.toolsForSurfaceDigest(tools) : tools;
228629
+ const digestTools = tools;
228329
228630
  const profile = buildRequestProfile({
228330
228631
  endpoint: ENDPOINT,
228331
228632
  method: "tools/list",
@@ -228333,11 +228634,12 @@ var require_generate_surface_anchor = __commonJS({
228333
228634
  params: includeAdvanced ? { include_advanced_tools: true } : {}
228334
228635
  });
228335
228636
  const digest = crypto.createHash("sha256").update(canonicalJson(digestTools), "utf8").digest("hex");
228637
+ lastDigestTools = digestTools;
228336
228638
  return {
228337
228639
  profile,
228338
228640
  tools_sha256: `sha256:${digest}`,
228339
228641
  tool_count: tools.length,
228340
- 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.",
228341
228643
  source_ref: SOURCE_REF,
228342
228644
  // Machine-readable, so a consumer can pin the immutable form without parsing the prose above.
228343
228645
  source_ref_commit: SOURCE_REF_COMMIT,
@@ -228423,6 +228725,25 @@ var require_generate_surface_anchor = __commonJS({
228423
228725
  const check = argv.includes("--check");
228424
228726
  const verifyRef = argv.includes("--verify-source-ref");
228425
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
+ }
228426
228747
  if (verifyRef) {
228427
228748
  const r = await verifySourceRef(anchor);
228428
228749
  if (r.ok) {
@@ -228488,7 +228809,7 @@ var require_generate_surface_anchor = __commonJS({
228488
228809
  process.exit(1);
228489
228810
  });
228490
228811
  }
228491
- 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 };
228492
228813
  }
228493
228814
  });
228494
228815
 
@@ -231520,10 +231841,10 @@ var require_gate_pin = __commonJS({
231520
231841
  "src/generated/gate-pin.json"(exports2, module2) {
231521
231842
  module2.exports = {
231522
231843
  repo: "coderifts/contract-gate",
231523
- sha: "f288c4bea752db6b405367c8e3b81e7885c54b20",
231524
- tag: "v0.9.3",
231844
+ sha: "15c19a6373ad69f5a8aaee992b69bc38baf98f14",
231845
+ tag: "v0.9.5",
231525
231846
  resolved_from: "https://github.com/coderifts/contract-gate refs/tags/v0",
231526
- resolved_at: "2026-09-10T06:43:53Z",
231847
+ resolved_at: "2026-09-12T19:02:41Z",
231527
231848
  note: "STRICT template pins this full commit SHA (GitHub: only a full SHA is immutable). v0 is a tag we move on every release, so it is NOT a pin. Re-run scripts/generate-gate-pin.js on each gate release; Dependabot bumps adopters."
231528
231849
  };
231529
231850
  }
@@ -233546,13 +233867,14 @@ ${HOOK_MARKER}
233546
233867
  #
233547
233868
  # Re-install after CLI upgrades: coderifts hook install
233548
233869
  #
233549
- # Exit map (fail-closed when configured):
233550
- # CODERIFTS_SKIP=1 \u2192 exit 0 (explicit loud override)
233551
- # no apiKey \u2192 exit 0 soft (not configured)
233552
- # 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
233553
233875
  # INDETERMINATE (no decision obtainable) \u2192 exit 1
233554
- # ALLOW / analysis clean \u2192 exit 0
233555
- # REQUIRE_APPROVAL / WARN \u2192 warn, exit 0
233876
+ # ALLOW / analysis clean \u2192 exit 0
233877
+ # REQUIRE_APPROVAL / WARN \u2192 warn, exit 0
233556
233878
 
233557
233879
  # Explicit escape hatch \u2014 never the default on error; human must set this.
233558
233880
  if [ "\${CODERIFTS_SKIP:-}" = "1" ]; then
@@ -233560,13 +233882,23 @@ if [ "\${CODERIFTS_SKIP:-}" = "1" ]; then
233560
233882
  exit 0
233561
233883
  fi
233562
233884
 
233563
- 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
233564
233890
  SPEC_PATH=$(git config coderifts.specPath || echo "api/openapi.yaml")
233565
233891
  ZERO="0000000000000000000000000000000000000000"
233566
233892
 
233567
- if [ -z "$CODERIFTS_API_KEY" ]; then
233568
- echo "CodeRifts: No API key configured. Run: git config coderifts.apiKey <your-key>"
233569
- 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
233570
233902
  fi
233571
233903
 
233572
233904
  # --- helpers: three-state git blob read (present | absent | error) ---
@@ -234019,8 +234351,10 @@ exit 0
234019
234351
  fs.writeFileSync(hookPath, PRE_PUSH_SCRIPT, { mode: 493 });
234020
234352
  console.log("CodeRifts pre-push hook installed.");
234021
234353
  console.log("");
234022
- console.log("Configure your API key:");
234023
- 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).");
234024
234358
  console.log("");
234025
234359
  console.log("Optionally set spec path (default: api/openapi.yaml):");
234026
234360
  console.log(" git config coderifts.specPath path/to/openapi.yaml");
@@ -263444,6 +263778,212 @@ ${USAGE}` };
263444
263778
  }
263445
263779
  });
263446
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
+
263447
263987
  // src/commands/lock.js
263448
263988
  var require_lock = __commonJS({
263449
263989
  "src/commands/lock.js"(exports2, module2) {
@@ -263508,6 +264048,7 @@ var require_lock = __commonJS({
263508
264048
  return `${JSON.stringify(doc, null, 2)}
263509
264049
  `;
263510
264050
  }
264051
+ var mcpLockDrift = require_mcp_lock_drift();
263511
264052
  function loadDriftCore(deps = {}) {
263512
264053
  if (deps.buildLockDrift) {
263513
264054
  return {
@@ -263516,21 +264057,7 @@ var require_lock = __commonJS({
263516
264057
  extractLockedManifestsFromLock: deps.extractLockedManifestsFromLock
263517
264058
  };
263518
264059
  }
263519
- const candidates = [
263520
- path.join(__dirname, "../../../../src/core/mcp-lock-drift"),
263521
- path.join(__dirname, "../../../src/core/mcp-lock-drift")
263522
- ];
263523
- for (const c of candidates) {
263524
- try {
263525
- return require(c);
263526
- } catch (_) {
263527
- }
263528
- }
263529
- const err = new Error(
263530
- "Cannot load mcp-lock-drift core. Run from the coderifts-app monorepo (packages/cli expects ../../../../src/core/mcp-lock-drift)."
263531
- );
263532
- err.code = "CORE_UNAVAILABLE";
263533
- throw err;
264060
+ return mcpLockDrift;
263534
264061
  }
263535
264062
  function readJsonFile(filePath) {
263536
264063
  const raw = fs.readFileSync(filePath, "utf8");
@@ -263804,6 +264331,176 @@ var require_lock = __commonJS({
263804
264331
  }
263805
264332
  });
263806
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
+
263807
264504
  // src/commands/adopt.js
263808
264505
  var require_adopt = __commonJS({
263809
264506
  "src/commands/adopt.js"(exports2, module2) {
@@ -263868,20 +264565,9 @@ var require_adopt = __commonJS({
263868
264565
  else process.env.LOG_LEVEL = prevEnv;
263869
264566
  }
263870
264567
  }
264568
+ var counterfactualAdopt = require_counterfactual_adopt();
263871
264569
  function loadCore() {
263872
- const candidates = [
263873
- path.join(__dirname, "../../../../src/core/counterfactual-adopt"),
263874
- path.join(__dirname, "../../../src/core/counterfactual-adopt")
263875
- ];
263876
- for (const c of candidates) {
263877
- try {
263878
- return require(c);
263879
- } catch (_) {
263880
- }
263881
- }
263882
- throw new Error(
263883
- "Cannot load counterfactual-adopt core. Run from the coderifts-app monorepo (packages/cli expects ../../../../src/core/counterfactual-adopt)."
263884
- );
264570
+ return counterfactualAdopt;
263885
264571
  }
263886
264572
  function listCommits({ commits, days, gitImpl, cwd }) {
263887
264573
  const args = ["log", "--format=%H %cI", "--reverse"];