coderifts 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -3007,7 +3007,7 @@ var require_package = __commonJS({
3007
3007
  "package.json"(exports2, module2) {
3008
3008
  module2.exports = {
3009
3009
  name: "coderifts",
3010
- version: "4.2.0",
3010
+ version: "4.3.0",
3011
3011
  description: "Detect breaking API changes from the command line. Works locally or with the CodeRifts cloud API.",
3012
3012
  author: "CodeRifts <hello@coderifts.com>",
3013
3013
  license: "MIT",
@@ -71780,1056 +71780,1769 @@ var require_cjs3 = __commonJS({
71780
71780
  }
71781
71781
  });
71782
71782
 
71783
- // src/commands/deploy-gate.js
71784
- var require_deploy_gate2 = __commonJS({
71785
- "src/commands/deploy-gate.js"(exports2, module2) {
71783
+ // src/commands/claude-hook.js
71784
+ var require_claude_hook = __commonJS({
71785
+ "src/commands/claude-hook.js"(exports2, module2) {
71786
71786
  "use strict";
71787
71787
  var fs = require("fs");
71788
71788
  var path = require("path");
71789
- var chalk = require_source();
71790
- var { deployGate } = require_cjs3();
71791
- var { renderJson } = require_json2();
71792
- if (process.env.NO_COLOR) chalk.level = 0;
71793
- var REPAIRABLE = /* @__PURE__ */ new Set(["env_mismatch", "stale_artifact", "operation_mismatch", "receipt_not_authorized", "fingerprint_mismatch", "body_hash_mismatch"]);
71794
- function enforceSignal(options) {
71795
- return options && options.enforce === true || String(process.env.CODERIFTS_DEPLOY_ENFORCE || "").toLowerCase() === "true";
71789
+ var { execSync } = require("child_process");
71790
+ var { getApiKey } = require_config();
71791
+ var { cloudAuthorizePreflight } = require_cloud();
71792
+ var DEFAULT_SPEC_PATH = "api/openapi.yaml";
71793
+ var CLOSED_ACTIONS = /* @__PURE__ */ new Set([
71794
+ "CONTINUE",
71795
+ "CONTINUE_WITH_MONITORING",
71796
+ "REQUEST_APPROVAL",
71797
+ "STOP"
71798
+ ]);
71799
+ var USAGE = [
71800
+ "Usage: coderifts claude-hook",
71801
+ "",
71802
+ "Claude Code PreToolUse hook: gates Write|Edit|MultiEdit when they touch the",
71803
+ "configured contract spec path. Reads JSON context from STDIN.",
71804
+ "",
71805
+ "Exit map (Claude Code semantics \u2014 get this exactly right):",
71806
+ " 2 BLOCK \u2014 tool call cancelled (BLOCK/STOP, REQUEST_APPROVAL, unrecognised action,",
71807
+ " governance could not run, CONTINUE_WITH_MONITORING without a wired sink)",
71808
+ " 0 allow \u2014 CONTINUE, or CWM with a host-asserted monitoring sink, or non-contract skip",
71809
+ " 1 NEVER used for security deny (Claude treats exit 1 as non-blocking; action proceeds)",
71810
+ "",
71811
+ "Fail-closed DEFAULT (exit 2, reason enforce_indeterminate) when governance could not run:",
71812
+ " no API key / API unreachable / disk unreadable / edit-apply failure on the spec path",
71813
+ " Absence of a key is not permission. Explicit opt-out: CODERIFTS_ADVISORY=1|true.",
71814
+ "",
71815
+ "Parse-gap DEFAULT (exit 2): unparseable stdin / missing file_path \u2014 refusing (fail-closed);",
71816
+ " set CODERIFTS_ADVISORY=1 to soften. JSONL hook_blocked cause stdin_unparseable|missing_file_path.",
71817
+ "",
71818
+ "Still exit 0 (nothing to govern): non-spec path, identical content.",
71819
+ "",
71820
+ "CONTINUE_WITH_MONITORING: allow only if the host asserts a sink",
71821
+ " (CODERIFTS_MONITORING_SINK_WIRED=1|true or git config coderifts.monitoringSinkWired).",
71822
+ " Host claim \u2014 not delivery proof (same class as GuardConfig.monitoringSinkWired).",
71823
+ "",
71824
+ "CODERIFTS_STRICT=1|true: same fail-closed as default; cannot be weakened by ADVISORY.",
71825
+ "",
71826
+ "Spec path (same source as git pre-push hook):",
71827
+ " git config coderifts.specPath (default: api/openapi.yaml)",
71828
+ "",
71829
+ "Baseline at PreToolUse: before = current file on disk (pre-edit state \u2014 the tool has",
71830
+ "not written yet). after = proposed content from tool_input (Write: content; Edit/MultiEdit:",
71831
+ "old\u2192new applied to disk content).",
71832
+ "",
71833
+ "Push-time equivalent: coderifts hook install (git exit 1 on BLOCK)."
71834
+ ].join("\n");
71835
+ function isEnvFlag(env, name) {
71836
+ const v = env && env[name];
71837
+ if (v == null || v === "") return false;
71838
+ const s = String(v).trim().toLowerCase();
71839
+ return s === "1" || s === "true";
71796
71840
  }
71797
- function observeCDEnforcement(options = {}) {
71798
- const envVal = String(process.env.CODERIFTS_DEPLOY_ENFORCE || "").toLowerCase();
71799
- let enforcement;
71800
- if (enforceSignal(options)) enforcement = "ENFORCING";
71801
- else if (envVal === "unknown") enforcement = "UNKNOWN";
71802
- else enforcement = "ADVISORY";
71803
- const bypass_possible = String(process.env.CODERIFTS_DEPLOY_NO_BYPASS || "").toLowerCase() !== "true";
71804
- return {
71805
- enforcement,
71806
- bypass_possible,
71807
- step_is_required: enforcement === "ENFORCING",
71808
- required_step_name: "CodeRifts / deploy-gate",
71809
- attestation_source: "cli_flag"
71810
- };
71841
+ function isStrictMode(env = process.env) {
71842
+ return isEnvFlag(env, "CODERIFTS_STRICT");
71811
71843
  }
71812
- function deployReportResiduals(state, enforcement_inescapable, enforcement, change_set_rebound) {
71813
- const out = [];
71814
- if (state !== "success") return out;
71815
- if (enforcement_inescapable !== true) {
71816
- if (enforcement === "ENFORCING") out.push("bypass_open");
71817
- else if (enforcement === "ADVISORY") out.push("deploy_gate_advisory");
71818
- else if (enforcement === "ABSENT") out.push("deploy_path_ungated");
71819
- }
71820
- if (change_set_rebound !== true) {
71821
- out.push("change_set_not_rebound");
71844
+ function isAdvisoryMode(env = process.env) {
71845
+ return isEnvFlag(env, "CODERIFTS_ADVISORY");
71846
+ }
71847
+ function isMonitoringSinkWired({ env = process.env, deps = {}, result = null } = {}) {
71848
+ if (deps && deps.monitoringSinkWired === true) return true;
71849
+ if (isEnvFlag(env, "CODERIFTS_MONITORING_SINK_WIRED")) return true;
71850
+ const git = readGitConfig("coderifts.monitoringSinkWired", deps);
71851
+ if (git) {
71852
+ const s = String(git).trim().toLowerCase();
71853
+ if (s === "1" || s === "true") return true;
71822
71854
  }
71823
- return out;
71855
+ if (result && result.monitoringSinkWired === true) return true;
71856
+ const dr = result && result.decision_result;
71857
+ if (dr && typeof dr === "object" && dr.monitoringSinkWired === true) return true;
71858
+ return false;
71824
71859
  }
71825
- function deployCoverageInput(enforcement_state, inescapable_deploy) {
71826
- return { enforcement_state, inescapable_deploy: inescapable_deploy === true, applicability_attested: true };
71860
+ var HOOK_TRIGGER_SOURCE = "claude_hook";
71861
+ function extractFingerprint(result) {
71862
+ const d = result && typeof result === "object" && !Array.isArray(result) ? result : {};
71863
+ const dr = d.decision_result && typeof d.decision_result === "object" ? d.decision_result : {};
71864
+ const fp = dr.fingerprint || d.fingerprint || d.verdict_fingerprint || d.input_fingerprint;
71865
+ return typeof fp === "string" && fp.length > 0 ? fp : void 0;
71827
71866
  }
71828
- function deployBind({ environment, artifact_id, receipt, observed_cd_enforcement, expected_fingerprint, expected_body_hash }) {
71829
- const attested_enforcement = observed_cd_enforcement && observed_cd_enforcement.enforcement || "UNKNOWN";
71830
- if (!receipt) {
71831
- return {
71832
- deploy_check_status: "pending",
71833
- reason: "no_receipt",
71834
- must_re_preflight: true,
71835
- attested_enforcement,
71836
- gate: null,
71837
- report_residuals: [],
71838
- coverage_deploy_input: deployCoverageInput(attested_enforcement, false)
71839
- };
71840
- }
71841
- const requiredContext = {
71842
- operation: "deploy",
71843
- enforcement: {
71844
- enforcement: attested_enforcement,
71845
- // fail-closed: bypass is possible unless observation proved it disabled.
71846
- bypass_possible: !(observed_cd_enforcement && observed_cd_enforcement.bypass_possible === false)
71847
- }
71848
- };
71849
- let change_set_rebound = false;
71850
- if (attested_enforcement === "ENFORCING") {
71851
- if (expected_fingerprint != null) {
71852
- requiredContext.expected_fingerprint = expected_fingerprint;
71853
- change_set_rebound = true;
71854
- }
71855
- if (expected_body_hash != null) requiredContext.expected_body_hash = expected_body_hash;
71856
- }
71857
- const gate = deployGate({ deployTarget: { environment, artifact_id }, receipt, requiredContext });
71858
- const enforcement_inescapable = attested_enforcement === "ENFORCING" && !!(observed_cd_enforcement && observed_cd_enforcement.bypass_possible === false);
71859
- const inescapable_deploy = gate.inescapable_deploy === true && change_set_rebound === true;
71860
- return {
71861
- deploy_check_status: gate.state,
71862
- reason: gate.reason,
71863
- must_re_preflight: REPAIRABLE.has(gate.reason),
71864
- attested_enforcement,
71865
- gate: { deploy_allowed: gate.deploy_allowed, reason: gate.reason, inescapable_deploy },
71866
- report_residuals: deployReportResiduals(gate.state, enforcement_inescapable, attested_enforcement, change_set_rebound),
71867
- coverage_deploy_input: deployCoverageInput(attested_enforcement, inescapable_deploy)
71868
- };
71867
+ function fingerprintPrefix(fp) {
71868
+ if (typeof fp !== "string" || fp.length === 0) return void 0;
71869
+ const colon = fp.indexOf(":");
71870
+ if (colon > 0 && colon < fp.length - 1) {
71871
+ const scheme = fp.slice(0, colon + 1);
71872
+ const rest = fp.slice(colon + 1);
71873
+ return scheme + rest.slice(0, 12) + (rest.length > 12 ? "\u2026" : "");
71874
+ }
71875
+ return fp.length > 16 ? `${fp.slice(0, 16)}\u2026` : fp;
71876
+ }
71877
+ function renderAllowProofBlock({ decision, executionAction, decisionId, fingerprint } = {}) {
71878
+ const decPart = decision ? ` decision=${decision}` : "";
71879
+ const eaPart = executionAction ? ` execution_action=${executionAction}` : "";
71880
+ const idPart = decisionId ? ` decision_id=${decisionId}` : "";
71881
+ const lines = [`CodeRifts claude-hook: AUTHORIZED${decPart}${eaPart}${idPart}`];
71882
+ const pref = fingerprintPrefix(fingerprint);
71883
+ if (pref) lines.push(`- receipt: ${pref}`);
71884
+ lines.push("- Interpretation: authorized at verify time for the bound scope only \u2014 see Limits.");
71885
+ lines.push("- authorized; execution/commit not proven by this hook");
71886
+ return lines.join("\n");
71869
71887
  }
71870
- function clampExit(deployCheckStatus, enforce) {
71871
- if (deployCheckStatus === "success") return 0;
71872
- if (deployCheckStatus === "failure" && enforce === true) return 1;
71873
- return 0;
71888
+ function hookSessionId(env) {
71889
+ const fromEnv = env && env.CODERIFTS_SESSION_ID && String(env.CODERIFTS_SESSION_ID).trim();
71890
+ if (fromEnv) return fromEnv;
71891
+ if (!hookSessionId._gen) hookSessionId._gen = `hook-${process.pid}-${Date.now()}`;
71892
+ return hookSessionId._gen;
71874
71893
  }
71875
- function readReceiptFile(filePath) {
71876
- if (!filePath) return null;
71877
- const resolved = path.resolve(filePath);
71878
- if (!fs.existsSync(resolved)) return null;
71894
+ function appendGuardEventLog(env, event, deps = {}) {
71895
+ const p = env && env.CODERIFTS_GUARD_EVENT_LOG;
71896
+ if (p == null || String(p).trim() === "") return;
71897
+ const errLog = deps.errLog || ((m) => console.error(String(m)));
71898
+ const append = deps.appendFileSync || ((file, data) => fs.appendFileSync(file, data));
71879
71899
  try {
71880
- return JSON.parse(fs.readFileSync(resolved, "utf-8"));
71881
- } catch (_) {
71882
- return null;
71900
+ append(String(p).trim(), JSON.stringify(event) + "\n");
71901
+ } catch (err) {
71902
+ try {
71903
+ errLog(`CodeRifts claude-hook: CODERIFTS_GUARD_EVENT_LOG write failed (${err && err.message})`);
71904
+ } catch {
71905
+ }
71883
71906
  }
71884
71907
  }
71885
- function extractDecisionIdFromReceipt(receipt) {
71886
- if (!receipt || typeof receipt !== "object") return null;
71887
- if (typeof receipt.decision_id === "string" && receipt.decision_id.trim()) {
71888
- return receipt.decision_id.trim();
71908
+ function failClosedOrAdvisory({ advisory, strict, site, softMsg, why, errLog }) {
71909
+ if (advisory && !strict) {
71910
+ errLog(`${softMsg} (CODERIFTS_ADVISORY)`);
71911
+ return { exitCode: 0, reason: site, site };
71889
71912
  }
71890
- const nested = receipt.decision_result;
71891
- if (nested && typeof nested === "object" && typeof nested.decision_id === "string" && nested.decision_id.trim()) {
71892
- return nested.decision_id.trim();
71913
+ if (strict) {
71914
+ errLog(
71915
+ `CodeRifts claude-hook: CODERIFTS_STRICT: governance could not run (${why}) \u2014 blocking. Set a key / restore network, or unset CODERIFTS_STRICT.`
71916
+ );
71917
+ return { exitCode: 2, reason: "enforce_indeterminate", site, strictBlocked: true };
71893
71918
  }
71894
- return null;
71895
- }
71896
- function formatOutcomeReportHint(decisionId) {
71897
- const id = String(decisionId);
71898
- return [
71899
- "# report the deploy outcome after your deploy step:",
71900
- `coderifts outcome <deploy_succeeded|deploy_failed|rolled_back> --decision ${id}`
71901
- ].join("\n");
71919
+ errLog(
71920
+ `CodeRifts claude-hook: governance could not run (${why}) \u2014 blocking. Set CODERIFTS_ADVISORY=1 to allow without governance (explicit opt-out).`
71921
+ );
71922
+ return { exitCode: 2, reason: "enforce_indeterminate", site };
71902
71923
  }
71903
- function renderDeployGateTerminal(bind, enforce) {
71904
- const g = bind.gate;
71905
- const color = bind.deploy_check_status === "success" ? chalk.green : bind.deploy_check_status === "failure" ? chalk.red : chalk.yellow;
71906
- const lines = [];
71907
- lines.push("");
71908
- lines.push(chalk.bold(` CodeRifts deploy-gate \u2014 ${color(bind.deploy_check_status.toUpperCase())}`));
71909
- lines.push(` Reason: ${bind.reason}`);
71910
- lines.push(` Enforcement: ${bind.attested_enforcement}`);
71911
- lines.push(` inescapable_deploy: ${g ? g.inescapable_deploy : false}`);
71912
- if (bind.report_residuals.length) lines.push(` Residuals: ${bind.report_residuals.join(", ")}`);
71913
- if (bind.must_re_preflight) lines.push(chalk.yellow(" Action: re-preflight this { environment, artifact } \u2014 the receipt does not authorize it."));
71914
- lines.push("");
71915
- lines.push(enforce ? chalk.dim(" Enforcing \u2014 a failing gate exits non-zero (blocks the deploy step).") : chalk.dim(" Advisory (phase 1) \u2014 does not block the deploy (exit 0)."));
71916
- lines.push("");
71917
- return lines.join("\n");
71924
+ function softOrStrictBlock({ strict, reason, softMsg, why, errLog }) {
71925
+ return failClosedOrAdvisory({
71926
+ advisory: false,
71927
+ strict: !!strict,
71928
+ site: reason,
71929
+ softMsg,
71930
+ why,
71931
+ errLog
71932
+ });
71918
71933
  }
71919
- async function runDeployGate(options = {}) {
71920
- const environment = options.env;
71921
- const artifactId = options.artifact;
71922
- if (!environment || !artifactId) {
71923
- console.error(chalk.red("Error: --env and --artifact are required."));
71924
- process.exit(1);
71925
- return;
71926
- }
71927
- const enforce = enforceSignal(options);
71928
- const receipt = readReceiptFile(options.receipt);
71929
- const observed = observeCDEnforcement({ enforce });
71930
- const bind = deployBind({ environment, artifact_id: artifactId, receipt, observed_cd_enforcement: observed });
71931
- const code = clampExit(bind.deploy_check_status, enforce);
71932
- const decisionId = extractDecisionIdFromReceipt(receipt);
71933
- if (options.json) {
71934
- console.log(renderJson({
71935
- command: "deploy-gate",
71936
- environment,
71937
- artifact_id: artifactId,
71938
- phase: enforce ? "enforcing" : "advisory",
71939
- exit_code: code,
71940
- decision_id: decisionId,
71941
- ...bind
71942
- }));
71943
- } else {
71944
- console.log(renderDeployGateTerminal(bind, enforce));
71945
- if (decisionId) {
71946
- console.log(formatOutcomeReportHint(decisionId));
71947
- console.log("");
71948
- }
71934
+ function readGitConfig(key, deps = {}) {
71935
+ const run = deps.execSync || execSync;
71936
+ const cwd = deps.cwd || process.cwd();
71937
+ try {
71938
+ const v = run(`git config ${key}`, { encoding: "utf8", cwd, stdio: ["ignore", "pipe", "pipe"] });
71939
+ const t = String(v || "").trim();
71940
+ return t || null;
71941
+ } catch {
71942
+ return null;
71949
71943
  }
71950
- process.exit(code);
71951
71944
  }
71952
- module2.exports = {
71953
- runDeployGate,
71954
- deployBind,
71955
- observeCDEnforcement,
71956
- clampExit,
71957
- readReceiptFile,
71958
- extractDecisionIdFromReceipt,
71959
- formatOutcomeReportHint,
71960
- renderDeployGateTerminal
71961
- };
71962
- }
71963
- });
71964
-
71965
- // src/commands/publish-gate.js
71966
- var require_publish_gate = __commonJS({
71967
- "src/commands/publish-gate.js"(exports2, module2) {
71968
- "use strict";
71969
- var fs = require("fs");
71970
- var path = require("path");
71971
- var { execFileSync } = require("child_process");
71972
- var chalk = require_source();
71973
- var { getApiKey } = require_config();
71974
- var { cloudDiff } = require_cloud();
71975
- if (process.env.NO_COLOR) chalk.level = 0;
71976
- var ZERO_SHA = "0000000000000000000000000000000000000000";
71977
- var CLOSED_ACTIONS = /* @__PURE__ */ new Set([
71978
- "CONTINUE",
71979
- "CONTINUE_WITH_MONITORING",
71980
- "REQUEST_APPROVAL",
71981
- "STOP"
71982
- ]);
71983
- var PERMIT_ACTIONS = /* @__PURE__ */ new Set(["CONTINUE", "CONTINUE_WITH_MONITORING"]);
71984
- function defaultGit(args, cwd) {
71985
- return execFileSync("git", args, {
71986
- cwd: cwd || process.cwd(),
71987
- encoding: "utf8",
71988
- maxBuffer: 16 * 1024 * 1024,
71989
- stdio: ["ignore", "pipe", "pipe"]
71990
- }).trim();
71945
+ function resolveApiKey(deps = {}) {
71946
+ const getKey = deps.getApiKey || getApiKey;
71947
+ const fromLogin = getKey();
71948
+ if (fromLogin) return fromLogin;
71949
+ const env = deps.env || process.env;
71950
+ const fromEnv = env.CODERIFTS_API_KEY && String(env.CODERIFTS_API_KEY).trim();
71951
+ if (fromEnv) return fromEnv;
71952
+ return readGitConfig("coderifts.apiKey", deps);
71991
71953
  }
71992
- function gitShow(ref, filePath, { gitImpl = defaultGit, cwd } = {}) {
71993
- try {
71994
- const out = gitImpl(["show", `${ref}:${filePath}`], cwd);
71995
- return { ok: true, content: out == null ? "" : String(out) };
71996
- } catch (err) {
71997
- const msg = err && err.stderr ? String(err.stderr) : err && err.message || String(err);
71998
- return {
71999
- ok: false,
72000
- code: "GIT_ERROR",
72001
- message: `git error reading ${ref}:${filePath}: ${msg.slice(0, 300)}`
72002
- };
72003
- }
71954
+ function resolveSpecPath(deps = {}) {
71955
+ const fromGit = readGitConfig("coderifts.specPath", deps);
71956
+ if (fromGit) return fromGit;
71957
+ return DEFAULT_SPEC_PATH;
72004
71958
  }
72005
- function readPackageVersion(cwd, readFile = fs.readFileSync) {
72006
- const pkgPath = path.join(cwd || process.cwd(), "package.json");
71959
+ function parseStdinJson(raw) {
71960
+ if (raw == null || String(raw).trim() === "") {
71961
+ return { ok: false, reason: "empty stdin" };
71962
+ }
71963
+ let obj;
72007
71964
  try {
72008
- const raw = readFile(pkgPath, "utf8");
72009
- const pkg2 = JSON.parse(raw);
72010
- return pkg2 && typeof pkg2.version === "string" ? pkg2.version : null;
71965
+ obj = JSON.parse(String(raw));
72011
71966
  } catch {
72012
- return null;
71967
+ return { ok: false, reason: "stdin is not JSON" };
71968
+ }
71969
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
71970
+ return { ok: false, reason: "stdin JSON is not an object" };
71971
+ }
71972
+ const toolName = obj.tool_name || obj.toolName || obj.name || null;
71973
+ let toolInput = obj.tool_input || obj.toolInput || obj.input || null;
71974
+ if (toolInput == null && obj.file_path) toolInput = obj;
71975
+ if (!toolName || typeof toolName !== "string") {
71976
+ return { ok: false, reason: "missing tool_name" };
72013
71977
  }
71978
+ if (!toolInput || typeof toolInput !== "object") {
71979
+ return { ok: false, reason: "missing tool_input" };
71980
+ }
71981
+ return { ok: true, toolName: String(toolName), toolInput };
72014
71982
  }
72015
- function resolveBeforeSpec(specPath, {
72016
- gitImpl = defaultGit,
72017
- cwd = process.cwd(),
72018
- readFile = fs.readFileSync,
72019
- packageVersion = null
72020
- } = {}) {
72021
- const version = packageVersion != null ? packageVersion : readPackageVersion(cwd, readFile);
72022
- const tried = [];
72023
- if (version) {
72024
- const tags = [`v${version}`, version];
72025
- for (const tag of tags) {
72026
- tried.push(`tag:${tag}`);
72027
- try {
72028
- gitImpl(["rev-parse", "--verify", `${tag}^{commit}`], cwd);
72029
- } catch {
72030
- continue;
72031
- }
72032
- const shown = gitShow(tag, specPath, { gitImpl, cwd });
72033
- if (!shown.ok) {
72034
- return {
72035
- ok: false,
72036
- code: shown.code || "GIT_ERROR",
72037
- message: shown.message,
72038
- tried
72039
- };
72040
- }
72041
- if (shown.content.trim() === "") {
72042
- return {
72043
- ok: false,
72044
- code: "EMPTY_BEFORE",
72045
- message: `Empty contract artifact at tag ${tag}:${specPath}. Refusing to treat empty before as NEW_ARTIFACT (would false-pass enforce).`,
72046
- tried
72047
- };
72048
- }
72049
- return { ok: true, content: shown.content, source: `tag:${tag}`, tried };
72050
- }
72051
- } else {
72052
- tried.push("package.json:version (missing)");
71983
+ function isSpecPath(filePath, specPath) {
71984
+ if (!filePath || !specPath) return false;
71985
+ const fp = path.normalize(String(filePath).replace(/\\/g, "/"));
71986
+ const sp = path.normalize(String(specPath).replace(/\\/g, "/"));
71987
+ if (fp === sp) return true;
71988
+ if (fp.endsWith("/" + sp) || fp.endsWith(sp)) return true;
71989
+ const baseFp = path.basename(fp);
71990
+ const baseSp = path.basename(sp);
71991
+ if (baseFp === baseSp && (fp.endsWith(sp) || sp.endsWith(baseSp))) {
71992
+ const tail = sp.split("/").filter(Boolean).join("/");
71993
+ return fp.replace(/\\/g, "/").endsWith(tail);
72053
71994
  }
72054
- const bases = ["origin/main", "origin/master", "main", "master"];
72055
- for (const base of bases) {
72056
- tried.push(`merge-base:${base}`);
72057
- let mb;
72058
- try {
72059
- mb = gitImpl(["merge-base", "HEAD", base], cwd);
72060
- } catch {
72061
- continue;
71995
+ return false;
71996
+ }
71997
+ function deriveAfterContent(toolName, toolInput, diskBefore) {
71998
+ const name = String(toolName || "");
71999
+ if (name === "Write" || name === "write") {
72000
+ if (typeof toolInput.content !== "string") {
72001
+ return { ok: false, reason: "Write tool_input.content missing or not a string" };
72062
72002
  }
72063
- if (!mb || mb === ZERO_SHA) continue;
72064
- const shown = gitShow(mb, specPath, { gitImpl, cwd });
72065
- if (!shown.ok) {
72066
- return {
72067
- ok: false,
72068
- code: shown.code || "GIT_ERROR",
72069
- message: shown.message,
72070
- tried
72071
- };
72003
+ return { ok: true, after: toolInput.content };
72004
+ }
72005
+ if (name === "Edit" || name === "edit") {
72006
+ const oldS = toolInput.old_string != null ? toolInput.old_string : toolInput.oldString;
72007
+ const newS = toolInput.new_string != null ? toolInput.new_string : toolInput.newString;
72008
+ if (typeof oldS !== "string" || typeof newS !== "string") {
72009
+ return { ok: false, reason: "Edit tool_input.old_string/new_string missing" };
72072
72010
  }
72073
- if (shown.content.trim() === "") {
72074
- return {
72075
- ok: false,
72076
- code: "EMPTY_BEFORE",
72077
- message: `Empty contract artifact at merge-base ${mb}:${specPath} (${base}). Refusing to treat empty before as NEW_ARTIFACT (would false-pass enforce).`,
72078
- tried
72079
- };
72011
+ if (!diskBefore.includes(oldS)) {
72012
+ return { ok: false, reason: "Edit old_string not found in disk content" };
72080
72013
  }
72081
- return {
72082
- ok: true,
72083
- content: shown.content,
72084
- source: `merge-base:${base}@${mb.slice(0, 12)}`,
72085
- tried
72086
- };
72014
+ return { ok: true, after: diskBefore.replace(oldS, newS) };
72087
72015
  }
72088
- return {
72089
- ok: false,
72090
- code: "BEFORE_UNRESOLVED",
72091
- message: `Could not resolve a non-empty before-spec for ${specPath}. Tried: (a) git tag of package.json version, (b) merge-base with origin/main. Attempts: ${tried.join(", ")}. Fail-closed \u2014 will not publish without a baseline.`,
72092
- tried
72093
- };
72094
- }
72095
- function resolveAfterSpec(specPath, {
72096
- cwd = process.cwd(),
72097
- readFile = fs.readFileSync,
72098
- exists = fs.existsSync
72099
- } = {}) {
72100
- const resolved = path.isAbsolute(specPath) ? specPath : path.join(cwd, specPath);
72101
- try {
72102
- if (!exists(resolved)) {
72103
- return {
72104
- ok: false,
72105
- code: "AFTER_MISSING",
72106
- message: `Working-tree contract artifact not found: ${specPath}`
72107
- };
72016
+ if (name === "MultiEdit" || name === "multi_edit" || name === "multiEdit") {
72017
+ const edits = toolInput.edits || toolInput.Edits;
72018
+ if (!Array.isArray(edits) || edits.length === 0) {
72019
+ return { ok: false, reason: "MultiEdit tool_input.edits missing or empty" };
72108
72020
  }
72109
- const content = readFile(resolved, "utf8");
72110
- if (content == null || String(content).trim() === "") {
72111
- return {
72112
- ok: false,
72113
- code: "AFTER_EMPTY",
72114
- message: `Working-tree contract artifact is empty: ${specPath}`
72115
- };
72021
+ let cur = diskBefore;
72022
+ for (let i = 0; i < edits.length; i++) {
72023
+ const e = edits[i] || {};
72024
+ const oldS = e.old_string != null ? e.old_string : e.oldString;
72025
+ const newS = e.new_string != null ? e.new_string : e.newString;
72026
+ if (typeof oldS !== "string" || typeof newS !== "string") {
72027
+ return { ok: false, reason: `MultiEdit edits[${i}] missing old_string/new_string` };
72028
+ }
72029
+ if (!cur.includes(oldS)) {
72030
+ return { ok: false, reason: `MultiEdit edits[${i}] old_string not found in content` };
72031
+ }
72032
+ cur = cur.replace(oldS, newS);
72116
72033
  }
72117
- return { ok: true, content: String(content), path: resolved };
72118
- } catch (err) {
72119
- return {
72120
- ok: false,
72121
- code: "AFTER_READ_ERROR",
72122
- message: `Failed to read working-tree ${specPath}: ${err && err.message}`
72123
- };
72034
+ return { ok: true, after: cur };
72124
72035
  }
72036
+ return { ok: false, reason: `unsupported tool_name for content derive: ${name}` };
72125
72037
  }
72126
- function evaluatePublishPermission(result) {
72127
- if (!result || typeof result !== "object") {
72128
- return {
72129
- allow: false,
72130
- execution_action: null,
72131
- decision: null,
72132
- policy: "fail_closed:unreadable_response"
72133
- };
72134
- }
72135
- const env = result.decision_result && typeof result.decision_result === "object" ? result.decision_result : null;
72136
- let ea = null;
72137
- if (env && typeof env.execution_action === "string") ea = env.execution_action;
72138
- else if (typeof result.execution_action === "string") ea = result.execution_action;
72139
- const decision = env && env.decision || result.decision || result.omega_decision || null;
72140
- if (ea && CLOSED_ACTIONS.has(ea)) {
72141
- const allow = PERMIT_ACTIONS.has(ea);
72142
- return {
72143
- allow,
72144
- execution_action: ea,
72145
- decision: decision || null,
72146
- policy: allow ? `permit:execution_action=${ea}` : `block:execution_action=${ea}`
72147
- };
72148
- }
72149
- if (ea != null && ea !== "" && !CLOSED_ACTIONS.has(ea)) {
72150
- return {
72151
- allow: false,
72152
- execution_action: ea,
72153
- decision: decision || null,
72154
- policy: `block:unrecognised_execution_action=${ea}`
72155
- };
72156
- }
72157
- if (decision === "BLOCK" || decision === "REQUIRE_APPROVAL") {
72158
- return {
72159
- allow: false,
72160
- execution_action: null,
72161
- decision,
72162
- policy: `block:decision=${decision}`
72163
- };
72038
+ function isV2DecisionBody(d, dr) {
72039
+ if (dr && typeof dr === "object") return true;
72040
+ if (d.preflight_mode != null && d.preflight_mode !== "") return true;
72041
+ const ver = d.decision_spec_version;
72042
+ return typeof ver === "string" && ver.startsWith("2.");
72043
+ }
72044
+ function allowLegacyDecisionMap(d, dr) {
72045
+ return d.decision_spec_version === "1.0" && !isV2DecisionBody(d, dr);
72046
+ }
72047
+ function mapDecisionSeverity(result) {
72048
+ const d = result && typeof result === "object" && !Array.isArray(result) ? result : {};
72049
+ let ea = null;
72050
+ const dr = d.decision_result;
72051
+ if (dr && typeof dr === "object" && typeof dr.execution_action === "string" && dr.execution_action !== "") {
72052
+ ea = dr.execution_action;
72053
+ } else if (typeof d.execution_action === "string" && d.execution_action !== "") {
72054
+ ea = d.execution_action;
72164
72055
  }
72165
- if (decision === "ALLOW" || decision === "WARN" || decision === "PASS") {
72056
+ const hasDecision = d.omega_decision != null && d.omega_decision !== "" || d.decision != null && d.decision !== "" || dr && typeof dr === "object" && dr.execution_action;
72057
+ if (d.error && !hasDecision && (ea == null || ea === "")) {
72166
72058
  return {
72167
- allow: true,
72168
- execution_action: null,
72169
- decision,
72170
- policy: `permit:decision=${decision}`
72059
+ severity: "INDETERMINATE",
72060
+ decision: null,
72061
+ decisionId: null,
72062
+ executionAction: null
72171
72063
  };
72172
72064
  }
72173
- const omega = result.omega_decision;
72174
- if (omega === "BLOCK" || omega === "REQUIRE_APPROVAL") {
72175
- return {
72176
- allow: false,
72177
- execution_action: null,
72178
- decision: omega,
72179
- policy: `block:omega_decision=${omega}`
72180
- };
72065
+ let severity;
72066
+ if (ea != null && ea !== "") {
72067
+ if (!CLOSED_ACTIONS.has(ea)) {
72068
+ severity = "UNKNOWN";
72069
+ } else if (ea === "CONTINUE") {
72070
+ severity = "ALLOW";
72071
+ } else if (ea === "CONTINUE_WITH_MONITORING") {
72072
+ severity = "MONITOR";
72073
+ } else if (ea === "STOP") {
72074
+ severity = "BLOCK";
72075
+ } else {
72076
+ severity = "REQUIRE_APPROVAL";
72077
+ }
72078
+ } else if (allowLegacyDecisionMap(d, dr)) {
72079
+ const od = d.omega_decision || d.decision;
72080
+ if (od == null || od === "") {
72081
+ severity = "INDETERMINATE";
72082
+ } else if (od === "BLOCK") severity = "BLOCK";
72083
+ else if (od === "REQUIRE_APPROVAL") severity = "REQUIRE_APPROVAL";
72084
+ else if (od === "WARN") severity = "WARN";
72085
+ else severity = "ALLOW";
72086
+ } else {
72087
+ severity = "INDETERMINATE";
72181
72088
  }
72089
+ const decision = dr && dr.decision || d.decision || d.omega_decision || null;
72090
+ const decisionId = dr && dr.decision_id || d.decision_id || null;
72182
72091
  return {
72183
- allow: false,
72184
- execution_action: ea,
72185
- decision: decision || omega || null,
72186
- policy: "fail_closed:no_permission_signal"
72092
+ severity,
72093
+ decision: decision != null && decision !== "" ? String(decision) : null,
72094
+ decisionId: decisionId != null ? String(decisionId) : null,
72095
+ executionAction: ea
72187
72096
  };
72188
72097
  }
72189
- function extractReceiptRef(result) {
72190
- if (!result || typeof result !== "object") return null;
72191
- const env = result.decision_result;
72192
- if (env && env.receipt && typeof env.receipt.token === "string") {
72193
- return env.receipt.token.slice(0, 24) + (env.receipt.token.length > 24 ? "\u2026" : "");
72194
- }
72195
- if (env && typeof env.decision_id === "string") return env.decision_id;
72196
- if (typeof result.decision_id === "string") return result.decision_id;
72197
- if (typeof result.fingerprint === "string") return result.fingerprint;
72198
- if (env && typeof env.fingerprint === "string") return env.fingerprint;
72199
- return null;
72200
- }
72201
- async function defaultPreflight(before, after, { apiKey } = {}) {
72202
- const key = process.env.CODERIFTS_FORCE_LOCAL_PREFLIGHT ? null : apiKey != null ? apiKey : getApiKey();
72203
- if (key) {
72204
- return cloudDiff(before, after, key);
72098
+ function renderDecisionWhy(result, opts = {}) {
72099
+ const maxFixes = Number.isInteger(opts.maxFixes) ? opts.maxFixes : 5;
72100
+ const maxLines = Number.isInteger(opts.maxLines) ? opts.maxLines : 13;
72101
+ const d = result && typeof result === "object" && !Array.isArray(result) ? result : {};
72102
+ const dr = d.decision_result && typeof d.decision_result === "object" ? d.decision_result : {};
72103
+ const pick = (k) => dr[k] !== void 0 ? dr[k] : d[k];
72104
+ const arr = (v) => Array.isArray(v) ? v : [];
72105
+ const str = (v) => typeof v === "string" ? v.trim() : "";
72106
+ const lines = [];
72107
+ const reasonCodes = [];
72108
+ for (const r of arr(pick("blocking_reasons"))) {
72109
+ if (!r || typeof r !== "object") continue;
72110
+ const code = str(r.code);
72111
+ const message = str(r.message);
72112
+ if (code) reasonCodes.push(code);
72113
+ if (!code && !message) continue;
72114
+ lines.push(`- ${code || "REASON"}${message ? `: ${message}` : ""}`);
72205
72115
  }
72206
- const yaml = require_js_yaml();
72207
- const { diffSpecs } = require_api2();
72208
- let oldSpec;
72209
- let newSpec;
72210
- try {
72211
- oldSpec = yaml.load(before);
72212
- newSpec = yaml.load(after);
72213
- } catch (e) {
72214
- const err = new Error(`Failed to parse specs: ${e.message}`);
72215
- err.code = "PREFLIGHT_UNREACHABLE";
72216
- throw err;
72116
+ for (const r of arr(pick("degraded_reasons"))) {
72117
+ if (!r || typeof r !== "object") continue;
72118
+ const code = str(r.code);
72119
+ const message = str(r.message);
72120
+ if (!code && !message) continue;
72121
+ lines.push(`- degraded: ${[code, message].filter(Boolean).join(": ")}`);
72217
72122
  }
72218
- let diffResult;
72219
- try {
72220
- diffResult = await diffSpecs({
72221
- sourceSpec: { content: JSON.stringify(oldSpec), location: "before.json", format: "openapi3" },
72222
- destinationSpec: { content: JSON.stringify(newSpec), location: "after.json", format: "openapi3" }
72223
- });
72224
- } catch (e) {
72225
- const err = new Error(`Local preflight engine error: ${e.message}`);
72226
- err.code = "PREFLIGHT_UNREACHABLE";
72227
- throw err;
72123
+ const action = str(pick("required_action"));
72124
+ if (action) lines.push(`- action: ${action}`);
72125
+ const rt = pick("remediation_transaction");
72126
+ const changes = arr(rt && typeof rt === "object" ? rt.required_changes : null).filter((c) => c && typeof c === "object");
72127
+ const shown = changes.slice(0, maxFixes);
72128
+ for (const c of shown) {
72129
+ const target = str(c.target);
72130
+ const instruction = str(c.instruction);
72131
+ if (!instruction && !target) continue;
72132
+ lines.push(`- fix${target ? ` (${target})` : ""}: ${instruction || str(c.precise_label)}`);
72228
72133
  }
72229
- const breaking = (diffResult.breakingDifferences || []).length;
72230
- const decision = breaking > 0 ? "BLOCK" : "ALLOW";
72231
- const execution_action = breaking > 0 ? "STOP" : "CONTINUE";
72232
- return {
72233
- decision,
72234
- omega_decision: decision,
72235
- execution_action,
72236
- decision_result: {
72237
- decision,
72238
- execution_action,
72239
- decision_id: `local-${Date.now()}`
72240
- },
72241
- breaking_changes: diffResult.breakingDifferences || [],
72242
- risk_score: Math.min(breaking * 15, 100)
72243
- };
72134
+ if (changes.length > shown.length) lines.push(`- fix: +${changes.length - shown.length} more`);
72135
+ return { lines: lines.slice(0, maxLines), reasonCodes };
72244
72136
  }
72245
- async function runPublishGate(options = {}, deps = {}) {
72246
- const cwd = deps.cwd || process.cwd();
72247
- const gitImpl = deps.gitImpl || defaultGit;
72248
- const readFile = deps.readFile || fs.readFileSync.bind(fs);
72249
- const exists = deps.exists || fs.existsSync.bind(fs);
72250
- const preflightFn = deps.preflightFn || defaultPreflight;
72251
- const log = deps.log || console.log.bind(console);
72252
- const logErr = deps.logErr || console.error.bind(console);
72253
- let specPath = options.spec;
72254
- if (!specPath) {
72137
+ async function runClaudeHook(options = {}, deps = {}) {
72138
+ const errLog = deps.errLog || ((m) => console.error(String(m)));
72139
+ const readStdin = deps.readStdin || (() => {
72255
72140
  try {
72256
- specPath = gitImpl(["config", "coderifts.specPath"], cwd);
72141
+ return fs.readFileSync(0, "utf8");
72257
72142
  } catch {
72258
- specPath = "";
72143
+ return "";
72259
72144
  }
72260
- }
72261
- if (!specPath) specPath = "api/openapi.yaml";
72262
- try {
72263
- gitImpl(["rev-parse", "--is-inside-work-tree"], cwd);
72264
- } catch (err) {
72265
- const msg = "CodeRifts publish-gate: GIT_ERROR \u2014 not a git repository (or git unavailable). Cannot resolve before-spec without git. Fail-closed.";
72266
- logErr(chalk.red(msg));
72267
- return finish({
72268
- ok: false,
72269
- exitCode: 1,
72270
- code: "GIT_ERROR",
72271
- message: msg,
72272
- policy: "fail_closed:git_error"
72273
- }, { log, json: options.json });
72274
- }
72275
- const beforeRes = resolveBeforeSpec(specPath, {
72276
- gitImpl,
72277
- cwd,
72278
- readFile,
72279
- packageVersion: deps.packageVersion
72280
72145
  });
72281
- if (!beforeRes.ok) {
72282
- logErr(chalk.red(`CodeRifts publish-gate: ${beforeRes.code} \u2014 ${beforeRes.message}`));
72283
- return finish({
72284
- ok: false,
72285
- exitCode: 1,
72286
- code: beforeRes.code,
72287
- message: beforeRes.message,
72288
- policy: `fail_closed:${beforeRes.code}`,
72289
- tried: beforeRes.tried
72290
- }, { log, json: options.json });
72291
- }
72292
- const afterRes = resolveAfterSpec(specPath, { cwd, readFile, exists });
72293
- if (!afterRes.ok) {
72294
- logErr(chalk.red(`CodeRifts publish-gate: ${afterRes.code} \u2014 ${afterRes.message}`));
72295
- return finish({
72296
- ok: false,
72297
- exitCode: 1,
72298
- code: afterRes.code,
72299
- message: afterRes.message,
72300
- policy: `fail_closed:${afterRes.code}`
72301
- }, { log, json: options.json });
72302
- }
72303
- if (beforeRes.content === afterRes.content) {
72304
- const payload2 = {
72305
- ok: true,
72306
- exitCode: 0,
72307
- code: "UNCHANGED",
72308
- message: "Contract artifact unchanged vs baseline; publish permitted.",
72309
- policy: "permit:unchanged",
72310
- before_source: beforeRes.source,
72311
- execution_action: "CONTINUE",
72312
- receipt: null
72313
- };
72314
- if (!options.json) {
72315
- log(chalk.green("CodeRifts publish-gate: ALLOW (unchanged)"));
72316
- log(` baseline: ${beforeRes.source}`);
72317
- log(` spec: ${specPath}`);
72318
- }
72319
- return finish(payload2, { log, json: options.json });
72320
- }
72321
- let result;
72322
- try {
72323
- result = await preflightFn(beforeRes.content, afterRes.content, {
72324
- apiKey: deps.apiKey,
72325
- cwd
72326
- });
72327
- } catch (err) {
72328
- const msg = `CodeRifts publish-gate: PREFLIGHT_UNREACHABLE \u2014 ${err && err.message}`;
72329
- logErr(chalk.red(msg));
72330
- return finish({
72331
- ok: false,
72332
- exitCode: 1,
72333
- code: "PREFLIGHT_UNREACHABLE",
72334
- message: msg,
72335
- policy: "fail_closed:preflight_unreachable"
72336
- }, { log, json: options.json });
72337
- }
72338
- const perm = evaluatePublishPermission(result);
72339
- const receipt = extractReceiptRef(result);
72340
- if (!perm.allow) {
72341
- const payload2 = {
72342
- ok: false,
72343
- exitCode: 1,
72344
- code: "BLOCK",
72345
- message: "Publish not permitted by execution_action / decision.",
72346
- policy: perm.policy,
72347
- execution_action: perm.execution_action,
72348
- decision: perm.decision,
72349
- before_source: beforeRes.source,
72350
- receipt,
72351
- fail_policy: "exit_1_on_block_or_resolver_error_or_unreachable"
72146
+ const readFile = deps.readFile || ((p) => fs.readFileSync(p, "utf8"));
72147
+ const exists = deps.exists || ((p) => fs.existsSync(p));
72148
+ const authorize = deps.cloudAuthorizePreflight || cloudAuthorizePreflight;
72149
+ const cwd = deps.cwd || process.cwd();
72150
+ const env = deps.env || process.env;
72151
+ const strict = isStrictMode(env);
72152
+ const advisory = isAdvisoryMode(env);
72153
+ const sessionId = hookSessionId(env);
72154
+ const logEv = (partial) => {
72155
+ appendGuardEventLog(env, {
72156
+ at: (/* @__PURE__ */ new Date()).toISOString(),
72157
+ sessionId,
72158
+ trigger_source: HOOK_TRIGGER_SOURCE,
72159
+ ...partial
72160
+ }, { errLog, appendFileSync: deps.appendFileSync });
72161
+ };
72162
+ let tool = null;
72163
+ let filePathKnown = null;
72164
+ const emitTerminal = (r) => {
72165
+ const extra = {
72166
+ tool: tool || void 0,
72167
+ path: filePathKnown || void 0,
72168
+ decision: r.decision != null ? r.decision : void 0,
72169
+ decisionId: r.decisionId != null ? r.decisionId : void 0
72352
72170
  };
72353
- if (!options.json) {
72354
- logErr("");
72355
- logErr(chalk.red("========================================"));
72356
- logErr(chalk.red(" CodeRifts: PUBLISH BLOCKED"));
72357
- logErr(chalk.red("========================================"));
72358
- logErr(` Policy: ${perm.policy}`);
72359
- logErr(` execution_action: ${perm.execution_action || "(none)"}`);
72360
- logErr(` decision: ${perm.decision || "(none)"}`);
72361
- logErr(` baseline: ${beforeRes.source}`);
72362
- logErr(` fail policy: exit 1 on BLOCK / resolver error / preflight unreachability`);
72363
- logErr(chalk.red("========================================"));
72364
- logErr("");
72171
+ if (r.exitCode === 2) {
72172
+ const reasons = Array.isArray(r.reasons) && r.reasons.length ? r.reasons : void 0;
72173
+ logEv({
72174
+ type: "hook_blocked",
72175
+ exit: 2,
72176
+ ...extra,
72177
+ reasons,
72178
+ cause: r.reason || r.site
72179
+ });
72180
+ } else if (r.exitCode === 0 && r.site && advisory && !strict) {
72181
+ logEv({ type: "hook_advisory_passthrough", exit: 0, decision: r.decision || null, ...extra, cause: r.site });
72182
+ } else {
72183
+ const proof = r.reason === "allow" ? {
72184
+ decision_id: r.decisionId != null ? r.decisionId : extra.decisionId || null,
72185
+ fp_prefix: fingerprintPrefix(r.fingerprint) || null
72186
+ } : void 0;
72187
+ logEv({
72188
+ type: "hook_continue",
72189
+ exit: 0,
72190
+ ...extra,
72191
+ cause: r.reason,
72192
+ ...proof ? { proof } : {}
72193
+ });
72365
72194
  }
72366
- return finish(payload2, { log, json: options.json });
72367
- }
72368
- const payload = {
72369
- ok: true,
72370
- exitCode: 0,
72371
- code: "ALLOW",
72372
- message: "Publish permitted.",
72373
- policy: perm.policy,
72374
- execution_action: perm.execution_action,
72375
- decision: perm.decision,
72376
- before_source: beforeRes.source,
72377
- receipt
72195
+ return r;
72378
72196
  };
72379
- if (!options.json) {
72380
- log(chalk.green("CodeRifts publish-gate: ALLOW"));
72381
- log(` Policy: ${perm.policy}`);
72382
- log(` execution_action: ${perm.execution_action || "(mapped from decision)"}`);
72383
- log(` baseline: ${beforeRes.source}`);
72384
- if (receipt) log(` Receipt reference: ${receipt}`);
72385
- else log(" Receipt reference: (none issued on this path)");
72386
- }
72387
- return finish(payload, { log, json: options.json });
72388
- }
72389
- function finish(payload, { log, json }) {
72390
- if (json) {
72391
- log(JSON.stringify(payload, null, 2));
72197
+ const apiKey = resolveApiKey({ ...deps, cwd, env });
72198
+ if (!apiKey) {
72199
+ logEv({ type: "preflight_unavailable", cause: "missing_api_key" });
72200
+ return emitTerminal(failClosedOrAdvisory({
72201
+ advisory,
72202
+ strict,
72203
+ site: "missing_api_key",
72204
+ why: "no API key",
72205
+ softMsg: "CodeRifts claude-hook: no API key (coderifts login / CODERIFTS_API_KEY / git config coderifts.apiKey) \u2014 allowing",
72206
+ errLog
72207
+ }));
72392
72208
  }
72393
- return payload;
72394
- }
72395
- module2.exports = {
72396
- runPublishGate,
72397
- resolveBeforeSpec,
72398
- resolveAfterSpec,
72399
- evaluatePublishPermission,
72400
- extractReceiptRef,
72401
- gitShow,
72402
- defaultGit,
72403
- PERMIT_ACTIONS,
72404
- CLOSED_ACTIONS
72405
- };
72406
- }
72407
- });
72408
-
72409
- // src/registry-validation-core.js
72410
- var require_registry_validation_core = __commonJS({
72411
- "src/registry-validation-core.js"(exports2, module2) {
72412
- "use strict";
72413
- var yaml = require_js_yaml();
72414
- function safeParse(content) {
72415
- if (!content) return null;
72416
- try {
72417
- const trimmed = content.trim();
72418
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
72419
- return JSON.parse(trimmed);
72209
+ const PARSE_GAP_STDERR = "unparseable input \u2014 refusing (fail-closed); set CODERIFTS_ADVISORY=1 to soften";
72210
+ const raw = typeof options.stdin === "string" ? options.stdin : readStdin();
72211
+ const parsed = parseStdinJson(raw);
72212
+ if (!parsed.ok) {
72213
+ if (advisory && !strict) {
72214
+ errLog(`CodeRifts claude-hook: ${parsed.reason} \u2014 allowing (CODERIFTS_ADVISORY)`);
72215
+ return emitTerminal({ exitCode: 0, reason: "stdin_unparseable", site: "stdin_unparseable" });
72420
72216
  }
72421
- return yaml.load(trimmed);
72422
- } catch (_) {
72423
- return null;
72424
- }
72425
- }
72426
- function validateOpenApiSpec(parsed) {
72427
- if (!parsed || typeof parsed !== "object") {
72428
- return { valid: false, error: "Not a valid YAML or JSON object" };
72217
+ errLog(`CodeRifts claude-hook: ${PARSE_GAP_STDERR}`);
72218
+ return emitTerminal({ exitCode: 2, reason: "stdin_unparseable" });
72429
72219
  }
72430
- if (parsed.openapi) {
72431
- const ver = String(parsed.openapi);
72432
- if (ver.startsWith("3.")) {
72433
- return { valid: true, version: ver };
72220
+ const { toolName, toolInput } = parsed;
72221
+ tool = toolName;
72222
+ const filePath = toolInput.file_path || toolInput.filePath || toolInput.path;
72223
+ if (!filePath || typeof filePath !== "string") {
72224
+ if (advisory && !strict) {
72225
+ errLog("CodeRifts claude-hook: tool_input.file_path missing \u2014 allowing (CODERIFTS_ADVISORY)");
72226
+ return emitTerminal({ exitCode: 0, reason: "missing_file_path", site: "missing_file_path" });
72434
72227
  }
72435
- return { valid: false, error: `Unsupported OpenAPI version: ${ver}` };
72228
+ errLog(`CodeRifts claude-hook: ${PARSE_GAP_STDERR}`);
72229
+ return emitTerminal({ exitCode: 2, reason: "missing_file_path" });
72436
72230
  }
72437
- if (parsed.swagger) {
72438
- const ver = String(parsed.swagger);
72439
- if (ver.startsWith("2.")) {
72440
- return { valid: true, version: ver };
72441
- }
72442
- return { valid: false, error: `Unsupported Swagger version: ${ver}` };
72231
+ filePathKnown = filePath;
72232
+ const specPath = resolveSpecPath({ ...deps, cwd });
72233
+ if (!isSpecPath(filePath, specPath)) {
72234
+ logEv({ type: "detection_skip", signals: ["not_spec_path"], tool, path: filePath });
72235
+ return emitTerminal({ exitCode: 0, reason: "not_spec_path" });
72443
72236
  }
72444
- return { valid: false, error: "Missing required field: 'openapi' or 'swagger'" };
72445
- }
72446
- function extractEndpoints(parsed) {
72447
- const endpoints = [];
72448
- const paths = parsed.paths || {};
72449
- const httpMethods = ["get", "post", "put", "patch", "delete", "head", "options"];
72450
- for (const [path, pathItem] of Object.entries(paths)) {
72451
- if (!pathItem || typeof pathItem !== "object") continue;
72452
- for (const method of httpMethods) {
72453
- if (pathItem[method]) {
72454
- endpoints.push({
72455
- path,
72456
- method: method.toUpperCase(),
72457
- operationId: pathItem[method].operationId || ""
72458
- });
72459
- }
72237
+ const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
72238
+ let diskBefore = "";
72239
+ if (exists(absPath)) {
72240
+ try {
72241
+ diskBefore = readFile(absPath, "utf8");
72242
+ } catch (e) {
72243
+ logEv({ type: "preflight_unavailable", cause: "disk_unreadable", tool, path: filePath });
72244
+ return emitTerminal(failClosedOrAdvisory({
72245
+ advisory,
72246
+ strict,
72247
+ site: "disk_unreadable",
72248
+ why: `cannot read contract file ${absPath}`,
72249
+ softMsg: `CodeRifts claude-hook: cannot read ${absPath} \u2014 allowing (soft)`,
72250
+ errLog
72251
+ }));
72460
72252
  }
72461
72253
  }
72462
- return endpoints;
72463
- }
72464
- function extractSchemaNames(parsed) {
72465
- const schemas = [];
72466
- const components = parsed.components?.schemas || {};
72467
- for (const [name, schema] of Object.entries(components)) {
72468
- const hash = JSON.stringify(schema);
72469
- schemas.push({ name, hash });
72254
+ const derived = deriveAfterContent(toolName, toolInput, diskBefore);
72255
+ if (!derived.ok) {
72256
+ logEv({ type: "preflight_unavailable", cause: "edit_apply_failed", tool, path: filePath });
72257
+ return emitTerminal(failClosedOrAdvisory({
72258
+ advisory,
72259
+ strict,
72260
+ site: "edit_apply_failed",
72261
+ why: derived.reason,
72262
+ softMsg: `CodeRifts claude-hook: ${derived.reason} \u2014 allowing (soft; never guess edit apply)`,
72263
+ errLog
72264
+ }));
72470
72265
  }
72471
- return schemas;
72472
- }
72473
- function extractDefinedScopes(parsed) {
72474
- const scopes = /* @__PURE__ */ new Set();
72475
- const schemes = parsed.components?.securitySchemes || {};
72476
- for (const scheme of Object.values(schemes)) {
72477
- if (scheme.type === "oauth2" && scheme.flows) {
72478
- for (const flow of Object.values(scheme.flows)) {
72479
- if (flow.scopes) {
72480
- for (const scope of Object.keys(flow.scopes)) {
72481
- scopes.add(scope);
72482
- }
72483
- }
72484
- }
72485
- }
72266
+ if (diskBefore === derived.after) {
72267
+ logEv({ type: "detection_skip", signals: ["identical"], tool, path: filePath });
72268
+ return emitTerminal({ exitCode: 0, reason: "identical" });
72486
72269
  }
72487
- return scopes;
72488
- }
72489
- function extractUsedScopes(parsed) {
72490
- const scopes = /* @__PURE__ */ new Set();
72491
- if (Array.isArray(parsed.security)) {
72492
- for (const req of parsed.security) {
72493
- for (const scopeList of Object.values(req)) {
72494
- if (Array.isArray(scopeList)) {
72495
- for (const s of scopeList) scopes.add(s);
72496
- }
72497
- }
72498
- }
72499
- }
72500
- const paths = parsed.paths || {};
72501
- const httpMethods = ["get", "post", "put", "patch", "delete", "head", "options"];
72502
- for (const pathItem of Object.values(paths)) {
72503
- if (!pathItem || typeof pathItem !== "object") continue;
72504
- for (const method of httpMethods) {
72505
- const op = pathItem[method];
72506
- if (op?.security && Array.isArray(op.security)) {
72507
- for (const req of op.security) {
72508
- for (const scopeList of Object.values(req)) {
72509
- if (Array.isArray(scopeList)) {
72510
- for (const s of scopeList) scopes.add(s);
72511
- }
72512
- }
72513
- }
72514
- }
72515
- }
72516
- }
72517
- return scopes;
72518
- }
72519
- function extractRefs(obj, refs = /* @__PURE__ */ new Set()) {
72520
- if (!obj || typeof obj !== "object") return refs;
72521
- if (Array.isArray(obj)) {
72522
- for (const item of obj) extractRefs(item, refs);
72523
- return refs;
72524
- }
72525
- for (const [key, value] of Object.entries(obj)) {
72526
- if (key === "$ref" && typeof value === "string") {
72527
- refs.add(value);
72528
- } else {
72529
- extractRefs(value, refs);
72530
- }
72531
- }
72532
- return refs;
72533
- }
72534
- function refResolves(parsed, ref) {
72535
- if (!ref.startsWith("#/")) return true;
72536
- const parts = ref.replace("#/", "").split("/");
72537
- let current = parsed;
72538
- for (const part of parts) {
72539
- if (!current || typeof current !== "object") return false;
72540
- current = current[part];
72270
+ logEv({ type: "preflight_start", tool, path: filePath });
72271
+ let result;
72272
+ try {
72273
+ result = await authorize(diskBefore, derived.after, apiKey, {
72274
+ operation: "merge",
72275
+ environment: "staging"
72276
+ });
72277
+ } catch (e) {
72278
+ const msg = e && e.message ? String(e.message) : "request failed";
72279
+ logEv({ type: "preflight_unavailable", cause: "api_unreachable", tool, path: filePath });
72280
+ return emitTerminal(failClosedOrAdvisory({
72281
+ advisory,
72282
+ strict,
72283
+ site: "api_unreachable",
72284
+ why: `API unreachable: ${msg}`,
72285
+ softMsg: `CodeRifts claude-hook: API unreachable (${msg}) \u2014 allowing (soft; availability must not brick the editor)`,
72286
+ errLog
72287
+ }));
72541
72288
  }
72542
- return current !== void 0;
72543
- }
72544
- function validateRegistry(specs) {
72545
- if (!Array.isArray(specs) || specs.length === 0) {
72546
- return { valid: true, issues: [], stats: { specs_count: 0, endpoints_count: 0, schemas_count: 0 } };
72289
+ const mapped = mapDecisionSeverity(result);
72290
+ const fingerprint = extractFingerprint(result);
72291
+ const idPart = mapped.decisionId ? ` decision_id=${mapped.decisionId}` : "";
72292
+ const decPart = mapped.decision ? ` decision=${mapped.decision}` : "";
72293
+ const eaPart = mapped.executionAction ? ` execution_action=${mapped.executionAction}` : "";
72294
+ const actionForLog = mapped.executionAction || (mapped.severity === "BLOCK" || mapped.severity === "UNKNOWN" || mapped.severity === "INDETERMINATE" ? "STOP" : mapped.severity === "REQUIRE_APPROVAL" ? "REQUEST_APPROVAL" : mapped.severity === "MONITOR" || mapped.severity === "WARN" ? "CONTINUE_WITH_MONITORING" : "CONTINUE");
72295
+ logEv({
72296
+ type: "preflight_result",
72297
+ action: actionForLog,
72298
+ decisionId: mapped.decisionId || void 0,
72299
+ fingerprint,
72300
+ tool,
72301
+ path: filePath
72302
+ });
72303
+ if (mapped.severity === "INDETERMINATE") {
72304
+ errLog(
72305
+ "CodeRifts claude-hook: BLOCKED (indeterminate response \u2014 no execution_action and no decision; not permission)"
72306
+ );
72307
+ return emitTerminal({
72308
+ exitCode: 2,
72309
+ reason: "indeterminate",
72310
+ severity: "INDETERMINATE",
72311
+ decision: mapped.decision,
72312
+ decisionId: mapped.decisionId
72313
+ });
72547
72314
  }
72548
- const issues = [];
72549
- const parsedSpecs = [];
72550
- for (const { name, spec } of specs) {
72551
- const parsed = safeParse(spec);
72552
- if (!parsed) {
72553
- issues.push({
72554
- severity: "error",
72555
- type: "parse_error",
72556
- message: `Could not parse spec '${name}' as valid YAML or JSON`,
72557
- specs: [name]
72558
- });
72559
- continue;
72560
- }
72561
- const validation = validateOpenApiSpec(parsed);
72562
- if (!validation.valid) {
72563
- issues.push({
72564
- severity: "error",
72565
- type: "invalid_spec",
72566
- message: `Spec '${name}' is not a valid OpenAPI document: ${validation.error}`,
72567
- specs: [name]
72568
- });
72569
- continue;
72570
- }
72571
- parsedSpecs.push({ name, parsed });
72315
+ if (mapped.severity === "BLOCK" || mapped.severity === "UNKNOWN") {
72316
+ const why = renderDecisionWhy(result);
72317
+ errLog(
72318
+ [
72319
+ `CodeRifts claude-hook: BLOCKED${decPart}${eaPart}${idPart}` + (mapped.severity === "UNKNOWN" ? " (unrecognised execution_action)" : ""),
72320
+ ...why.lines
72321
+ ].join("\n")
72322
+ );
72323
+ return emitTerminal({
72324
+ exitCode: 2,
72325
+ reason: mapped.severity === "UNKNOWN" ? "unknown_action" : "block",
72326
+ severity: mapped.severity,
72327
+ decision: mapped.decision,
72328
+ decisionId: mapped.decisionId,
72329
+ reasons: why.reasonCodes
72330
+ });
72572
72331
  }
72573
- const endpointMap = /* @__PURE__ */ new Map();
72574
- for (const { name, parsed } of parsedSpecs) {
72575
- const endpoints = extractEndpoints(parsed);
72576
- for (const ep of endpoints) {
72577
- const key = `${ep.method} ${ep.path}`;
72578
- if (!endpointMap.has(key)) endpointMap.set(key, []);
72579
- endpointMap.get(key).push({ spec: name, operationId: ep.operationId });
72580
- }
72332
+ if (mapped.severity === "REQUIRE_APPROVAL") {
72333
+ const why = renderDecisionWhy(result);
72334
+ errLog(
72335
+ [
72336
+ `CodeRifts claude-hook: BLOCKED approval_required${decPart}${eaPart}${idPart}`,
72337
+ ...why.lines
72338
+ ].join("\n")
72339
+ );
72340
+ return emitTerminal({
72341
+ exitCode: 2,
72342
+ reason: "approval_required",
72343
+ severity: mapped.severity,
72344
+ decision: mapped.decision,
72345
+ decisionId: mapped.decisionId,
72346
+ reasons: why.reasonCodes
72347
+ });
72581
72348
  }
72582
- for (const [endpoint, owners] of endpointMap) {
72583
- if (owners.length > 1) {
72584
- const specNames = owners.map((o) => o.spec);
72585
- issues.push({
72586
- severity: "warning",
72587
- type: "endpoint_collision",
72588
- message: `Endpoint '${endpoint}' is defined in multiple specs: ${specNames.join(", ")}`,
72589
- specs: specNames
72349
+ if (mapped.severity === "MONITOR" || mapped.severity === "WARN") {
72350
+ const sinkWired = isMonitoringSinkWired({ env, deps, result });
72351
+ if (!sinkWired) {
72352
+ errLog(
72353
+ `CodeRifts claude-hook: BLOCKED monitoring_unwired${decPart}${eaPart}${idPart} \u2014 CONTINUE_WITH_MONITORING requires CODERIFTS_MONITORING_SINK_WIRED=1 (host assertion; not delivery proof)`
72354
+ );
72355
+ return emitTerminal({
72356
+ exitCode: 2,
72357
+ reason: "monitoring_unwired",
72358
+ severity: mapped.severity === "WARN" ? "MONITOR" : mapped.severity,
72359
+ decision: mapped.decision,
72360
+ decisionId: mapped.decisionId
72590
72361
  });
72591
72362
  }
72363
+ errLog(renderAllowProofBlock({
72364
+ decision: mapped.decision,
72365
+ executionAction: mapped.executionAction,
72366
+ decisionId: mapped.decisionId,
72367
+ fingerprint
72368
+ }));
72369
+ return emitTerminal({
72370
+ exitCode: 0,
72371
+ reason: "allow",
72372
+ severity: mapped.severity === "WARN" ? "MONITOR" : mapped.severity,
72373
+ decision: mapped.decision,
72374
+ decisionId: mapped.decisionId,
72375
+ fingerprint
72376
+ });
72592
72377
  }
72593
- const schemaMap = /* @__PURE__ */ new Map();
72594
- for (const { name, parsed } of parsedSpecs) {
72595
- const schemas = extractSchemaNames(parsed);
72596
- for (const s of schemas) {
72597
- if (!schemaMap.has(s.name)) schemaMap.set(s.name, []);
72598
- schemaMap.get(s.name).push({ spec: name, hash: s.hash });
72599
- }
72600
- }
72601
- for (const [schemaName, definitions] of schemaMap) {
72602
- if (definitions.length > 1) {
72603
- const uniqueHashes = new Set(definitions.map((d) => d.hash));
72604
- if (uniqueHashes.size > 1) {
72605
- const specNames = definitions.map((d) => d.spec);
72606
- issues.push({
72607
- severity: "warning",
72608
- type: "schema_conflict",
72609
- message: `Schema '${schemaName}' has conflicting definitions across specs: ${specNames.join(", ")}`,
72610
- specs: specNames
72611
- });
72612
- }
72613
- }
72614
- }
72615
- for (const { name, parsed } of parsedSpecs) {
72616
- const defined = extractDefinedScopes(parsed);
72617
- const used = extractUsedScopes(parsed);
72618
- for (const scope of used) {
72619
- if (!defined.has(scope)) {
72620
- issues.push({
72621
- severity: "warning",
72622
- type: "undefined_scope",
72623
- message: `Scope '${scope}' is used in '${name}' but not defined in securitySchemes`,
72624
- specs: [name]
72625
- });
72626
- }
72627
- }
72628
- for (const scope of defined) {
72629
- if (!used.has(scope)) {
72630
- issues.push({
72631
- severity: "info",
72632
- type: "unused_scope",
72633
- message: `Scope '${scope}' is defined in '${name}' but never used in any operation`,
72634
- specs: [name]
72635
- });
72636
- }
72637
- }
72638
- }
72639
- for (const { name, parsed } of parsedSpecs) {
72640
- const refs = extractRefs(parsed);
72641
- for (const ref of refs) {
72642
- if (!refResolves(parsed, ref)) {
72643
- issues.push({
72644
- severity: "error",
72645
- type: "unresolved_ref",
72646
- message: `$ref '${ref}' in '${name}' does not resolve`,
72647
- specs: [name]
72648
- });
72649
- }
72650
- }
72651
- }
72652
- let totalEndpoints = 0;
72653
- let totalSchemas = 0;
72654
- for (const { parsed } of parsedSpecs) {
72655
- totalEndpoints += extractEndpoints(parsed).length;
72656
- totalSchemas += extractSchemaNames(parsed).length;
72657
- }
72658
- const hasErrors = issues.some((i) => i.severity === "error");
72659
- return {
72660
- valid: !hasErrors,
72661
- issues,
72662
- stats: {
72663
- specs_count: parsedSpecs.length,
72664
- endpoints_count: totalEndpoints,
72665
- schemas_count: totalSchemas
72666
- }
72667
- };
72378
+ errLog(renderAllowProofBlock({
72379
+ decision: mapped.decision,
72380
+ executionAction: mapped.executionAction,
72381
+ decisionId: mapped.decisionId,
72382
+ fingerprint
72383
+ }));
72384
+ return emitTerminal({
72385
+ exitCode: 0,
72386
+ reason: "allow",
72387
+ severity: "ALLOW",
72388
+ decision: mapped.decision,
72389
+ decisionId: mapped.decisionId,
72390
+ fingerprint
72391
+ });
72668
72392
  }
72669
72393
  module2.exports = {
72670
- validateRegistry,
72671
- safeParse,
72672
- validateOpenApiSpec,
72673
- // Exported for testing
72674
- extractEndpoints,
72675
- extractSchemaNames,
72676
- extractDefinedScopes,
72677
- extractUsedScopes,
72678
- extractRefs,
72679
- refResolves
72394
+ runClaudeHook,
72395
+ parseStdinJson,
72396
+ isSpecPath,
72397
+ deriveAfterContent,
72398
+ mapDecisionSeverity,
72399
+ renderDecisionWhy,
72400
+ resolveApiKey,
72401
+ resolveSpecPath,
72402
+ readGitConfig,
72403
+ isEnvFlag,
72404
+ isStrictMode,
72405
+ isAdvisoryMode,
72406
+ isMonitoringSinkWired,
72407
+ failClosedOrAdvisory,
72408
+ softOrStrictBlock,
72409
+ DEFAULT_SPEC_PATH,
72410
+ CLOSED_ACTIONS,
72411
+ USAGE,
72412
+ appendGuardEventLog,
72413
+ extractFingerprint,
72414
+ fingerprintPrefix,
72415
+ renderAllowProofBlock,
72416
+ HOOK_TRIGGER_SOURCE
72680
72417
  };
72681
72418
  }
72682
72419
  });
72683
72420
 
72684
- // src/commands/registry-gate.js
72685
- var require_registry_gate = __commonJS({
72686
- "src/commands/registry-gate.js"(exports2, module2) {
72421
+ // src/commands/deploy-gate.js
72422
+ var require_deploy_gate2 = __commonJS({
72423
+ "src/commands/deploy-gate.js"(exports2, module2) {
72687
72424
  "use strict";
72688
72425
  var fs = require("fs");
72689
72426
  var path = require("path");
72690
72427
  var chalk = require_source();
72691
- var { matchGlob } = require_cjs3();
72692
- var {
72693
- validateRegistry,
72694
- safeParse,
72695
- validateOpenApiSpec
72696
- } = require_registry_validation_core();
72428
+ var { deployGate } = require_cjs3();
72429
+ var { renderJson } = require_json2();
72430
+ var { renderDecisionWhy, isEnvFlag } = require_claude_hook();
72697
72431
  if (process.env.NO_COLOR) chalk.level = 0;
72698
- var SPEC_EXT = /* @__PURE__ */ new Set([".yaml", ".yml", ".json"]);
72699
- function walkSpecCandidates(rootDir, {
72700
- readdirSync = fs.readdirSync.bind(fs),
72701
- statSync = fs.statSync.bind(fs)
72702
- } = {}) {
72703
- const out = [];
72704
- function walk(absDir) {
72705
- let entries;
72706
- try {
72707
- entries = readdirSync(absDir, { withFileTypes: true });
72708
- } catch (err) {
72709
- const e = new Error(`Cannot read directory ${absDir}: ${err && err.message}`);
72710
- e.code = "GATE_ERROR";
72711
- e.path = absDir;
72712
- throw e;
72713
- }
72714
- for (const ent of entries) {
72715
- const name = ent.name;
72716
- if (name === "node_modules" || name.startsWith(".")) continue;
72717
- const abs = path.join(absDir, name);
72718
- let isDir = ent.isDirectory && ent.isDirectory();
72719
- let isFile = ent.isFile && ent.isFile();
72720
- if (!isDir && !isFile) {
72721
- try {
72722
- const st = statSync(abs);
72723
- isDir = st.isDirectory();
72724
- isFile = st.isFile();
72725
- } catch (err) {
72726
- const e = new Error(`Cannot stat ${abs}: ${err && err.message}`);
72727
- e.code = "GATE_ERROR";
72728
- e.path = abs;
72729
- throw e;
72730
- }
72731
- }
72732
- if (isDir) {
72733
- walk(abs);
72734
- continue;
72735
- }
72736
- if (isFile) {
72737
- const ext = path.extname(name).toLowerCase();
72738
- if (SPEC_EXT.has(ext)) out.push(abs);
72739
- }
72740
- }
72741
- }
72742
- walk(rootDir);
72743
- return out.sort();
72432
+ var REPAIRABLE = /* @__PURE__ */ new Set(["env_mismatch", "stale_artifact", "operation_mismatch", "receipt_not_authorized", "fingerprint_mismatch", "body_hash_mismatch"]);
72433
+ function enforceSignal(options) {
72434
+ return options && options.enforce === true || String(process.env.CODERIFTS_DEPLOY_ENFORCE || "").toLowerCase() === "true";
72744
72435
  }
72745
- function relPosix(rootDir, absPath) {
72746
- let rel = path.relative(rootDir, absPath);
72747
- if (path.sep !== "/") rel = rel.split(path.sep).join("/");
72748
- return rel;
72436
+ function isDeployAdvisory(env) {
72437
+ const e = env || process.env;
72438
+ return isEnvFlag(e, "CODERIFTS_DEPLOY_ADVISORY") || isEnvFlag(e, "CODERIFTS_ADVISORY");
72749
72439
  }
72750
- function readFileThreeState(absPath, readFileSync = fs.readFileSync.bind(fs)) {
72751
- try {
72752
- const content = readFileSync(absPath, "utf8");
72753
- return { ok: true, content: content == null ? "" : String(content) };
72754
- } catch (err) {
72755
- const msg = err && err.message ? String(err.message) : String(err);
72756
- return {
72757
- ok: false,
72758
- code: "GATE_ERROR",
72759
- message: `unreadable file: ${absPath} (${msg.slice(0, 200)})`,
72760
- path: absPath
72761
- };
72440
+ function observeCDEnforcement(options = {}) {
72441
+ const envVal = String(process.env.CODERIFTS_DEPLOY_ENFORCE || "").toLowerCase();
72442
+ let enforcement;
72443
+ if (enforceSignal(options)) enforcement = "ENFORCING";
72444
+ else if (envVal === "unknown") enforcement = "UNKNOWN";
72445
+ else enforcement = "ADVISORY";
72446
+ const bypass_possible = String(process.env.CODERIFTS_DEPLOY_NO_BYPASS || "").toLowerCase() !== "true";
72447
+ return {
72448
+ enforcement,
72449
+ bypass_possible,
72450
+ step_is_required: enforcement === "ENFORCING",
72451
+ required_step_name: "CodeRifts / deploy-gate",
72452
+ attestation_source: "cli_flag"
72453
+ };
72454
+ }
72455
+ function deployReportResiduals(state, enforcement_inescapable, enforcement, change_set_rebound) {
72456
+ const out = [];
72457
+ if (state !== "success") return out;
72458
+ if (enforcement_inescapable !== true) {
72459
+ if (enforcement === "ENFORCING") out.push("bypass_open");
72460
+ else if (enforcement === "ADVISORY") out.push("deploy_gate_advisory");
72461
+ else if (enforcement === "ABSENT") out.push("deploy_path_ungated");
72462
+ }
72463
+ if (change_set_rebound !== true) {
72464
+ out.push("change_set_not_rebound");
72762
72465
  }
72466
+ return out;
72763
72467
  }
72764
- function discoverRegistrySpecs(dir, {
72765
- glob = null,
72766
- readdirSync = fs.readdirSync.bind(fs),
72767
- readFileSync = fs.readFileSync.bind(fs),
72768
- statSync = fs.statSync.bind(fs)
72769
- } = {}) {
72770
- const rootDir = path.resolve(dir);
72771
- let candidates;
72772
- try {
72773
- candidates = walkSpecCandidates(rootDir, { readdirSync, statSync });
72774
- } catch (err) {
72468
+ function deployCoverageInput(enforcement_state, inescapable_deploy) {
72469
+ return { enforcement_state, inescapable_deploy: inescapable_deploy === true, applicability_attested: true };
72470
+ }
72471
+ function deployBind({ environment, artifact_id, receipt, observed_cd_enforcement, expected_fingerprint, expected_body_hash }) {
72472
+ const attested_enforcement = observed_cd_enforcement && observed_cd_enforcement.enforcement || "UNKNOWN";
72473
+ if (!receipt) {
72775
72474
  return {
72776
- ok: false,
72777
- code: err.code || "GATE_ERROR",
72778
- message: err.message || String(err),
72779
- path: err.path
72475
+ deploy_check_status: "pending",
72476
+ reason: "no_receipt",
72477
+ must_re_preflight: true,
72478
+ attested_enforcement,
72479
+ gate: null,
72480
+ report_residuals: [],
72481
+ coverage_deploy_input: deployCoverageInput(attested_enforcement, false)
72780
72482
  };
72781
72483
  }
72782
- if (glob) {
72783
- candidates = candidates.filter((abs) => matchGlob(glob, relPosix(rootDir, abs)));
72784
- }
72785
- const specs = [];
72786
- let skippedNonSpec = 0;
72787
- for (const abs of candidates) {
72788
- const read = readFileThreeState(abs, readFileSync);
72789
- if (!read.ok) {
72790
- return {
72791
- ok: false,
72792
- code: "GATE_ERROR",
72793
- message: read.message,
72794
- path: read.path || abs
72795
- };
72796
- }
72797
- const name = relPosix(rootDir, abs);
72798
- const parsed = safeParse(read.content);
72799
- if (!parsed || typeof parsed !== "object") {
72800
- specs.push({ name, spec: read.content });
72801
- continue;
72484
+ const requiredContext = {
72485
+ operation: "deploy",
72486
+ enforcement: {
72487
+ enforcement: attested_enforcement,
72488
+ // fail-closed: bypass is possible unless observation proved it disabled.
72489
+ bypass_possible: !(observed_cd_enforcement && observed_cd_enforcement.bypass_possible === false)
72802
72490
  }
72803
- const shape = validateOpenApiSpec(parsed);
72804
- if (!shape.valid) {
72805
- if (shape.error && shape.error.includes("Missing required field: 'openapi' or 'swagger'")) {
72806
- skippedNonSpec += 1;
72807
- continue;
72808
- }
72809
- specs.push({ name, spec: read.content });
72810
- continue;
72491
+ };
72492
+ let change_set_rebound = false;
72493
+ if (attested_enforcement === "ENFORCING") {
72494
+ if (expected_fingerprint != null) {
72495
+ requiredContext.expected_fingerprint = expected_fingerprint;
72496
+ change_set_rebound = true;
72811
72497
  }
72812
- specs.push({ name, spec: read.content });
72498
+ if (expected_body_hash != null) requiredContext.expected_body_hash = expected_body_hash;
72813
72499
  }
72500
+ const gate = deployGate({ deployTarget: { environment, artifact_id }, receipt, requiredContext });
72501
+ const enforcement_inescapable = attested_enforcement === "ENFORCING" && !!(observed_cd_enforcement && observed_cd_enforcement.bypass_possible === false);
72502
+ const inescapable_deploy = gate.inescapable_deploy === true && change_set_rebound === true;
72814
72503
  return {
72815
- ok: true,
72816
- specs,
72817
- skippedNonSpec,
72818
- candidates: candidates.length,
72819
- rootDir
72504
+ deploy_check_status: gate.state,
72505
+ reason: gate.reason,
72506
+ must_re_preflight: REPAIRABLE.has(gate.reason),
72507
+ attested_enforcement,
72508
+ gate: { deploy_allowed: gate.deploy_allowed, reason: gate.reason, inescapable_deploy },
72509
+ report_residuals: deployReportResiduals(gate.state, enforcement_inescapable, attested_enforcement, change_set_rebound),
72510
+ coverage_deploy_input: deployCoverageInput(attested_enforcement, inescapable_deploy)
72820
72511
  };
72821
72512
  }
72822
- function findingsFailGate(issues, { warnOnly = false, errorsOnly = false } = {}) {
72823
- if (warnOnly) return false;
72824
- for (const i of issues || []) {
72825
- if (i.severity === "error") return true;
72826
- if (i.severity === "warning" && !errorsOnly) return true;
72513
+ function clampExit(deployCheckStatus, advisory) {
72514
+ if (deployCheckStatus === "success") return 0;
72515
+ if (advisory === true) return 0;
72516
+ return 1;
72517
+ }
72518
+ var ADVISORY_LABEL = "ADVISORY MODE \u2014 this gate did not block";
72519
+ var SOFTEN_HINT = "set CODERIFTS_DEPLOY_ADVISORY=1 to soften";
72520
+ function renderPolicyStderr(bind, receipt, advisory) {
72521
+ const lines = [
72522
+ `CodeRifts deploy-gate: BLOCKED reason=${bind.reason} status=${bind.deploy_check_status}`
72523
+ ];
72524
+ if (receipt && typeof receipt === "object") {
72525
+ const why = renderDecisionWhy(receipt);
72526
+ for (const l of why.lines) lines.push(l);
72827
72527
  }
72828
- return false;
72528
+ if (bind.must_re_preflight) {
72529
+ lines.push("- action: re-preflight this { environment, artifact } \u2014 the receipt does not authorize it.");
72530
+ }
72531
+ lines.push(advisory ? ADVISORY_LABEL : SOFTEN_HINT);
72532
+ return lines.join("\n");
72829
72533
  }
72830
- function countBySeverity(issues) {
72831
- const c = { error: 0, warning: 0, info: 0 };
72832
- for (const i of issues || []) {
72534
+ function renderInfraStderr({ site, why, retryable = true, advisory = false }) {
72535
+ const lines = [
72536
+ `CodeRifts deploy-gate: INFRA ${site} \u2014 ${why}`,
72537
+ `- retryable: ${retryable ? "true" : "false"}`,
72538
+ "- this is not a policy BLOCK"
72539
+ ];
72540
+ lines.push(advisory ? ADVISORY_LABEL : SOFTEN_HINT);
72541
+ return lines.join("\n");
72542
+ }
72543
+ function inspectReceiptFile(filePath) {
72544
+ if (!filePath) return { kind: "absent", receipt: null };
72545
+ const resolved = path.resolve(filePath);
72546
+ if (!fs.existsSync(resolved)) return { kind: "missing_file", receipt: null };
72547
+ try {
72548
+ const parsed = JSON.parse(fs.readFileSync(resolved, "utf-8"));
72549
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
72550
+ return { kind: "malformed", receipt: null };
72551
+ }
72552
+ return { kind: "ok", receipt: parsed };
72553
+ } catch (_) {
72554
+ return { kind: "malformed", receipt: null };
72555
+ }
72556
+ }
72557
+ function readReceiptFile(filePath) {
72558
+ return inspectReceiptFile(filePath).receipt;
72559
+ }
72560
+ function extractDecisionIdFromReceipt(receipt) {
72561
+ if (!receipt || typeof receipt !== "object") return null;
72562
+ if (typeof receipt.decision_id === "string" && receipt.decision_id.trim()) {
72563
+ return receipt.decision_id.trim();
72564
+ }
72565
+ const nested = receipt.decision_result;
72566
+ if (nested && typeof nested === "object" && typeof nested.decision_id === "string" && nested.decision_id.trim()) {
72567
+ return nested.decision_id.trim();
72568
+ }
72569
+ return null;
72570
+ }
72571
+ function formatOutcomeReportHint(decisionId) {
72572
+ const id = String(decisionId);
72573
+ return [
72574
+ "# report the deploy outcome after your deploy step:",
72575
+ `coderifts outcome <deploy_succeeded|deploy_failed|rolled_back> --decision ${id}`
72576
+ ].join("\n");
72577
+ }
72578
+ function renderDeployGateTerminal(bind, enforce) {
72579
+ const g = bind.gate;
72580
+ const color = bind.deploy_check_status === "success" ? chalk.green : bind.deploy_check_status === "failure" ? chalk.red : chalk.yellow;
72581
+ const lines = [];
72582
+ lines.push("");
72583
+ lines.push(chalk.bold(` CodeRifts deploy-gate \u2014 ${color(bind.deploy_check_status.toUpperCase())}`));
72584
+ lines.push(` Reason: ${bind.reason}`);
72585
+ lines.push(` Enforcement: ${bind.attested_enforcement}`);
72586
+ lines.push(` inescapable_deploy: ${g ? g.inescapable_deploy : false}`);
72587
+ if (bind.report_residuals.length) lines.push(` Residuals: ${bind.report_residuals.join(", ")}`);
72588
+ if (bind.must_re_preflight) lines.push(chalk.yellow(" Action: re-preflight this { environment, artifact } \u2014 the receipt does not authorize it."));
72589
+ lines.push("");
72590
+ lines.push(enforce ? chalk.dim(" Attested ENFORCING (inescapable_deploy claim). Exit is fail-closed by default.") : chalk.dim(" Fail-closed default \u2014 a failing gate exits non-zero. " + SOFTEN_HINT + "."));
72591
+ lines.push("");
72592
+ return lines.join("\n");
72593
+ }
72594
+ async function runDeployGate(options = {}) {
72595
+ const env = options.envObj || process.env;
72596
+ const advisory = isDeployAdvisory(env);
72597
+ const errLog = options.errLog || ((m) => console.error(String(m)));
72598
+ const outLog = options.outLog || ((m) => console.log(String(m)));
72599
+ const exitFn = options.exitFn || ((code2) => {
72600
+ process.exit(code2);
72601
+ });
72602
+ const environment = options.env;
72603
+ const artifactId = options.artifact;
72604
+ if (!environment || !artifactId) {
72605
+ errLog(renderInfraStderr({
72606
+ site: "missing_inputs",
72607
+ why: "--env and --artifact are required",
72608
+ retryable: true,
72609
+ advisory
72610
+ }));
72611
+ exitFn(advisory ? 0 : 1);
72612
+ return { exitCode: advisory ? 0 : 1, failure_class: "infra" };
72613
+ }
72614
+ const enforce = enforceSignal(options);
72615
+ const inspected = inspectReceiptFile(options.receipt);
72616
+ if (inspected.kind === "malformed") {
72617
+ errLog(renderInfraStderr({
72618
+ site: "malformed_receipt",
72619
+ why: `receipt file is not valid JSON: ${options.receipt}`,
72620
+ retryable: true,
72621
+ advisory
72622
+ }));
72623
+ exitFn(advisory ? 0 : 1);
72624
+ return { exitCode: advisory ? 0 : 1, failure_class: "infra" };
72625
+ }
72626
+ const receipt = inspected.receipt;
72627
+ const observed = observeCDEnforcement({ enforce });
72628
+ const bind = deployBind({ environment, artifact_id: artifactId, receipt, observed_cd_enforcement: observed });
72629
+ const code = clampExit(bind.deploy_check_status, advisory);
72630
+ const decisionId = extractDecisionIdFromReceipt(receipt);
72631
+ const failureClass = bind.deploy_check_status === "success" ? null : "policy";
72632
+ if (bind.deploy_check_status !== "success") {
72633
+ errLog(renderPolicyStderr(bind, receipt, advisory));
72634
+ } else if (advisory) {
72635
+ errLog(ADVISORY_LABEL);
72636
+ }
72637
+ if (options.json) {
72638
+ outLog(renderJson({
72639
+ command: "deploy-gate",
72640
+ environment,
72641
+ artifact_id: artifactId,
72642
+ phase: advisory ? "advisory" : "fail_closed",
72643
+ exit_code: code,
72644
+ decision_id: decisionId,
72645
+ failure_class: failureClass,
72646
+ retryable: false,
72647
+ ...bind
72648
+ }));
72649
+ } else {
72650
+ outLog(renderDeployGateTerminal(bind, enforce));
72651
+ if (decisionId) {
72652
+ outLog(formatOutcomeReportHint(decisionId));
72653
+ outLog("");
72654
+ }
72655
+ }
72656
+ exitFn(code);
72657
+ return { exitCode: code, failure_class: failureClass };
72658
+ }
72659
+ module2.exports = {
72660
+ runDeployGate,
72661
+ deployBind,
72662
+ observeCDEnforcement,
72663
+ clampExit,
72664
+ readReceiptFile,
72665
+ inspectReceiptFile,
72666
+ extractDecisionIdFromReceipt,
72667
+ formatOutcomeReportHint,
72668
+ renderDeployGateTerminal,
72669
+ renderPolicyStderr,
72670
+ renderInfraStderr,
72671
+ isDeployAdvisory,
72672
+ ADVISORY_LABEL,
72673
+ SOFTEN_HINT
72674
+ };
72675
+ }
72676
+ });
72677
+
72678
+ // src/commands/publish-gate.js
72679
+ var require_publish_gate = __commonJS({
72680
+ "src/commands/publish-gate.js"(exports2, module2) {
72681
+ "use strict";
72682
+ var fs = require("fs");
72683
+ var path = require("path");
72684
+ var { execFileSync } = require("child_process");
72685
+ var chalk = require_source();
72686
+ var { getApiKey } = require_config();
72687
+ var { cloudDiff } = require_cloud();
72688
+ if (process.env.NO_COLOR) chalk.level = 0;
72689
+ var ZERO_SHA = "0000000000000000000000000000000000000000";
72690
+ var CLOSED_ACTIONS = /* @__PURE__ */ new Set([
72691
+ "CONTINUE",
72692
+ "CONTINUE_WITH_MONITORING",
72693
+ "REQUEST_APPROVAL",
72694
+ "STOP"
72695
+ ]);
72696
+ var PERMIT_ACTIONS = /* @__PURE__ */ new Set(["CONTINUE", "CONTINUE_WITH_MONITORING"]);
72697
+ function defaultGit(args, cwd) {
72698
+ return execFileSync("git", args, {
72699
+ cwd: cwd || process.cwd(),
72700
+ encoding: "utf8",
72701
+ maxBuffer: 16 * 1024 * 1024,
72702
+ stdio: ["ignore", "pipe", "pipe"]
72703
+ }).trim();
72704
+ }
72705
+ function gitShow(ref, filePath, { gitImpl = defaultGit, cwd } = {}) {
72706
+ try {
72707
+ const out = gitImpl(["show", `${ref}:${filePath}`], cwd);
72708
+ return { ok: true, content: out == null ? "" : String(out) };
72709
+ } catch (err) {
72710
+ const msg = err && err.stderr ? String(err.stderr) : err && err.message || String(err);
72711
+ return {
72712
+ ok: false,
72713
+ code: "GIT_ERROR",
72714
+ message: `git error reading ${ref}:${filePath}: ${msg.slice(0, 300)}`
72715
+ };
72716
+ }
72717
+ }
72718
+ function readPackageVersion(cwd, readFile = fs.readFileSync) {
72719
+ const pkgPath = path.join(cwd || process.cwd(), "package.json");
72720
+ try {
72721
+ const raw = readFile(pkgPath, "utf8");
72722
+ const pkg2 = JSON.parse(raw);
72723
+ return pkg2 && typeof pkg2.version === "string" ? pkg2.version : null;
72724
+ } catch {
72725
+ return null;
72726
+ }
72727
+ }
72728
+ function resolveBeforeSpec(specPath, {
72729
+ gitImpl = defaultGit,
72730
+ cwd = process.cwd(),
72731
+ readFile = fs.readFileSync,
72732
+ packageVersion = null
72733
+ } = {}) {
72734
+ const version = packageVersion != null ? packageVersion : readPackageVersion(cwd, readFile);
72735
+ const tried = [];
72736
+ if (version) {
72737
+ const tags = [`v${version}`, version];
72738
+ for (const tag of tags) {
72739
+ tried.push(`tag:${tag}`);
72740
+ try {
72741
+ gitImpl(["rev-parse", "--verify", `${tag}^{commit}`], cwd);
72742
+ } catch {
72743
+ continue;
72744
+ }
72745
+ const shown = gitShow(tag, specPath, { gitImpl, cwd });
72746
+ if (!shown.ok) {
72747
+ return {
72748
+ ok: false,
72749
+ code: shown.code || "GIT_ERROR",
72750
+ message: shown.message,
72751
+ tried
72752
+ };
72753
+ }
72754
+ if (shown.content.trim() === "") {
72755
+ return {
72756
+ ok: false,
72757
+ code: "EMPTY_BEFORE",
72758
+ message: `Empty contract artifact at tag ${tag}:${specPath}. Refusing to treat empty before as NEW_ARTIFACT (would false-pass enforce).`,
72759
+ tried
72760
+ };
72761
+ }
72762
+ return { ok: true, content: shown.content, source: `tag:${tag}`, tried };
72763
+ }
72764
+ } else {
72765
+ tried.push("package.json:version (missing)");
72766
+ }
72767
+ const bases = ["origin/main", "origin/master", "main", "master"];
72768
+ for (const base of bases) {
72769
+ tried.push(`merge-base:${base}`);
72770
+ let mb;
72771
+ try {
72772
+ mb = gitImpl(["merge-base", "HEAD", base], cwd);
72773
+ } catch {
72774
+ continue;
72775
+ }
72776
+ if (!mb || mb === ZERO_SHA) continue;
72777
+ const shown = gitShow(mb, specPath, { gitImpl, cwd });
72778
+ if (!shown.ok) {
72779
+ return {
72780
+ ok: false,
72781
+ code: shown.code || "GIT_ERROR",
72782
+ message: shown.message,
72783
+ tried
72784
+ };
72785
+ }
72786
+ if (shown.content.trim() === "") {
72787
+ return {
72788
+ ok: false,
72789
+ code: "EMPTY_BEFORE",
72790
+ message: `Empty contract artifact at merge-base ${mb}:${specPath} (${base}). Refusing to treat empty before as NEW_ARTIFACT (would false-pass enforce).`,
72791
+ tried
72792
+ };
72793
+ }
72794
+ return {
72795
+ ok: true,
72796
+ content: shown.content,
72797
+ source: `merge-base:${base}@${mb.slice(0, 12)}`,
72798
+ tried
72799
+ };
72800
+ }
72801
+ return {
72802
+ ok: false,
72803
+ code: "BEFORE_UNRESOLVED",
72804
+ message: `Could not resolve a non-empty before-spec for ${specPath}. Tried: (a) git tag of package.json version, (b) merge-base with origin/main. Attempts: ${tried.join(", ")}. Fail-closed \u2014 will not publish without a baseline.`,
72805
+ tried
72806
+ };
72807
+ }
72808
+ function resolveAfterSpec(specPath, {
72809
+ cwd = process.cwd(),
72810
+ readFile = fs.readFileSync,
72811
+ exists = fs.existsSync
72812
+ } = {}) {
72813
+ const resolved = path.isAbsolute(specPath) ? specPath : path.join(cwd, specPath);
72814
+ try {
72815
+ if (!exists(resolved)) {
72816
+ return {
72817
+ ok: false,
72818
+ code: "AFTER_MISSING",
72819
+ message: `Working-tree contract artifact not found: ${specPath}`
72820
+ };
72821
+ }
72822
+ const content = readFile(resolved, "utf8");
72823
+ if (content == null || String(content).trim() === "") {
72824
+ return {
72825
+ ok: false,
72826
+ code: "AFTER_EMPTY",
72827
+ message: `Working-tree contract artifact is empty: ${specPath}`
72828
+ };
72829
+ }
72830
+ return { ok: true, content: String(content), path: resolved };
72831
+ } catch (err) {
72832
+ return {
72833
+ ok: false,
72834
+ code: "AFTER_READ_ERROR",
72835
+ message: `Failed to read working-tree ${specPath}: ${err && err.message}`
72836
+ };
72837
+ }
72838
+ }
72839
+ function evaluatePublishPermission(result) {
72840
+ if (!result || typeof result !== "object") {
72841
+ return {
72842
+ allow: false,
72843
+ execution_action: null,
72844
+ decision: null,
72845
+ policy: "fail_closed:unreadable_response"
72846
+ };
72847
+ }
72848
+ const env = result.decision_result && typeof result.decision_result === "object" ? result.decision_result : null;
72849
+ let ea = null;
72850
+ if (env && typeof env.execution_action === "string") ea = env.execution_action;
72851
+ else if (typeof result.execution_action === "string") ea = result.execution_action;
72852
+ const decision = env && env.decision || result.decision || result.omega_decision || null;
72853
+ if (ea && CLOSED_ACTIONS.has(ea)) {
72854
+ const allow = PERMIT_ACTIONS.has(ea);
72855
+ return {
72856
+ allow,
72857
+ execution_action: ea,
72858
+ decision: decision || null,
72859
+ policy: allow ? `permit:execution_action=${ea}` : `block:execution_action=${ea}`
72860
+ };
72861
+ }
72862
+ if (ea != null && ea !== "" && !CLOSED_ACTIONS.has(ea)) {
72863
+ return {
72864
+ allow: false,
72865
+ execution_action: ea,
72866
+ decision: decision || null,
72867
+ policy: `block:unrecognised_execution_action=${ea}`
72868
+ };
72869
+ }
72870
+ if (decision === "BLOCK" || decision === "REQUIRE_APPROVAL") {
72871
+ return {
72872
+ allow: false,
72873
+ execution_action: null,
72874
+ decision,
72875
+ policy: `block:decision=${decision}`
72876
+ };
72877
+ }
72878
+ if (decision === "ALLOW" || decision === "WARN" || decision === "PASS") {
72879
+ return {
72880
+ allow: true,
72881
+ execution_action: null,
72882
+ decision,
72883
+ policy: `permit:decision=${decision}`
72884
+ };
72885
+ }
72886
+ const omega = result.omega_decision;
72887
+ if (omega === "BLOCK" || omega === "REQUIRE_APPROVAL") {
72888
+ return {
72889
+ allow: false,
72890
+ execution_action: null,
72891
+ decision: omega,
72892
+ policy: `block:omega_decision=${omega}`
72893
+ };
72894
+ }
72895
+ return {
72896
+ allow: false,
72897
+ execution_action: ea,
72898
+ decision: decision || omega || null,
72899
+ policy: "fail_closed:no_permission_signal"
72900
+ };
72901
+ }
72902
+ function extractReceiptRef(result) {
72903
+ if (!result || typeof result !== "object") return null;
72904
+ const env = result.decision_result;
72905
+ if (env && env.receipt && typeof env.receipt.token === "string") {
72906
+ return env.receipt.token.slice(0, 24) + (env.receipt.token.length > 24 ? "\u2026" : "");
72907
+ }
72908
+ if (env && typeof env.decision_id === "string") return env.decision_id;
72909
+ if (typeof result.decision_id === "string") return result.decision_id;
72910
+ if (typeof result.fingerprint === "string") return result.fingerprint;
72911
+ if (env && typeof env.fingerprint === "string") return env.fingerprint;
72912
+ return null;
72913
+ }
72914
+ async function defaultPreflight(before, after, { apiKey } = {}) {
72915
+ const key = process.env.CODERIFTS_FORCE_LOCAL_PREFLIGHT ? null : apiKey != null ? apiKey : getApiKey();
72916
+ if (key) {
72917
+ return cloudDiff(before, after, key);
72918
+ }
72919
+ const yaml = require_js_yaml();
72920
+ const { diffSpecs } = require_api2();
72921
+ let oldSpec;
72922
+ let newSpec;
72923
+ try {
72924
+ oldSpec = yaml.load(before);
72925
+ newSpec = yaml.load(after);
72926
+ } catch (e) {
72927
+ const err = new Error(`Failed to parse specs: ${e.message}`);
72928
+ err.code = "PREFLIGHT_UNREACHABLE";
72929
+ throw err;
72930
+ }
72931
+ let diffResult;
72932
+ try {
72933
+ diffResult = await diffSpecs({
72934
+ sourceSpec: { content: JSON.stringify(oldSpec), location: "before.json", format: "openapi3" },
72935
+ destinationSpec: { content: JSON.stringify(newSpec), location: "after.json", format: "openapi3" }
72936
+ });
72937
+ } catch (e) {
72938
+ const err = new Error(`Local preflight engine error: ${e.message}`);
72939
+ err.code = "PREFLIGHT_UNREACHABLE";
72940
+ throw err;
72941
+ }
72942
+ const breaking = (diffResult.breakingDifferences || []).length;
72943
+ const decision = breaking > 0 ? "BLOCK" : "ALLOW";
72944
+ const execution_action = breaking > 0 ? "STOP" : "CONTINUE";
72945
+ return {
72946
+ decision,
72947
+ omega_decision: decision,
72948
+ execution_action,
72949
+ decision_result: {
72950
+ decision,
72951
+ execution_action,
72952
+ decision_id: `local-${Date.now()}`
72953
+ },
72954
+ breaking_changes: diffResult.breakingDifferences || [],
72955
+ risk_score: Math.min(breaking * 15, 100)
72956
+ };
72957
+ }
72958
+ async function runPublishGate(options = {}, deps = {}) {
72959
+ const cwd = deps.cwd || process.cwd();
72960
+ const gitImpl = deps.gitImpl || defaultGit;
72961
+ const readFile = deps.readFile || fs.readFileSync.bind(fs);
72962
+ const exists = deps.exists || fs.existsSync.bind(fs);
72963
+ const preflightFn = deps.preflightFn || defaultPreflight;
72964
+ const log = deps.log || console.log.bind(console);
72965
+ const logErr = deps.logErr || console.error.bind(console);
72966
+ let specPath = options.spec;
72967
+ if (!specPath) {
72968
+ try {
72969
+ specPath = gitImpl(["config", "coderifts.specPath"], cwd);
72970
+ } catch {
72971
+ specPath = "";
72972
+ }
72973
+ }
72974
+ if (!specPath) specPath = "api/openapi.yaml";
72975
+ try {
72976
+ gitImpl(["rev-parse", "--is-inside-work-tree"], cwd);
72977
+ } catch (err) {
72978
+ const msg = "CodeRifts publish-gate: GIT_ERROR \u2014 not a git repository (or git unavailable). Cannot resolve before-spec without git. Fail-closed.";
72979
+ logErr(chalk.red(msg));
72980
+ return finish({
72981
+ ok: false,
72982
+ exitCode: 1,
72983
+ code: "GIT_ERROR",
72984
+ message: msg,
72985
+ policy: "fail_closed:git_error"
72986
+ }, { log, json: options.json });
72987
+ }
72988
+ const beforeRes = resolveBeforeSpec(specPath, {
72989
+ gitImpl,
72990
+ cwd,
72991
+ readFile,
72992
+ packageVersion: deps.packageVersion
72993
+ });
72994
+ if (!beforeRes.ok) {
72995
+ logErr(chalk.red(`CodeRifts publish-gate: ${beforeRes.code} \u2014 ${beforeRes.message}`));
72996
+ return finish({
72997
+ ok: false,
72998
+ exitCode: 1,
72999
+ code: beforeRes.code,
73000
+ message: beforeRes.message,
73001
+ policy: `fail_closed:${beforeRes.code}`,
73002
+ tried: beforeRes.tried
73003
+ }, { log, json: options.json });
73004
+ }
73005
+ const afterRes = resolveAfterSpec(specPath, { cwd, readFile, exists });
73006
+ if (!afterRes.ok) {
73007
+ logErr(chalk.red(`CodeRifts publish-gate: ${afterRes.code} \u2014 ${afterRes.message}`));
73008
+ return finish({
73009
+ ok: false,
73010
+ exitCode: 1,
73011
+ code: afterRes.code,
73012
+ message: afterRes.message,
73013
+ policy: `fail_closed:${afterRes.code}`
73014
+ }, { log, json: options.json });
73015
+ }
73016
+ if (beforeRes.content === afterRes.content) {
73017
+ const payload2 = {
73018
+ ok: true,
73019
+ exitCode: 0,
73020
+ code: "UNCHANGED",
73021
+ message: "Contract artifact unchanged vs baseline; publish permitted.",
73022
+ policy: "permit:unchanged",
73023
+ before_source: beforeRes.source,
73024
+ execution_action: "CONTINUE",
73025
+ receipt: null
73026
+ };
73027
+ if (!options.json) {
73028
+ log(chalk.green("CodeRifts publish-gate: ALLOW (unchanged)"));
73029
+ log(` baseline: ${beforeRes.source}`);
73030
+ log(` spec: ${specPath}`);
73031
+ }
73032
+ return finish(payload2, { log, json: options.json });
73033
+ }
73034
+ let result;
73035
+ try {
73036
+ result = await preflightFn(beforeRes.content, afterRes.content, {
73037
+ apiKey: deps.apiKey,
73038
+ cwd
73039
+ });
73040
+ } catch (err) {
73041
+ const msg = `CodeRifts publish-gate: PREFLIGHT_UNREACHABLE \u2014 ${err && err.message}`;
73042
+ logErr(chalk.red(msg));
73043
+ return finish({
73044
+ ok: false,
73045
+ exitCode: 1,
73046
+ code: "PREFLIGHT_UNREACHABLE",
73047
+ message: msg,
73048
+ policy: "fail_closed:preflight_unreachable"
73049
+ }, { log, json: options.json });
73050
+ }
73051
+ const perm = evaluatePublishPermission(result);
73052
+ const receipt = extractReceiptRef(result);
73053
+ if (!perm.allow) {
73054
+ const payload2 = {
73055
+ ok: false,
73056
+ exitCode: 1,
73057
+ code: "BLOCK",
73058
+ message: "Publish not permitted by execution_action / decision.",
73059
+ policy: perm.policy,
73060
+ execution_action: perm.execution_action,
73061
+ decision: perm.decision,
73062
+ before_source: beforeRes.source,
73063
+ receipt,
73064
+ fail_policy: "exit_1_on_block_or_resolver_error_or_unreachable"
73065
+ };
73066
+ if (!options.json) {
73067
+ logErr("");
73068
+ logErr(chalk.red("========================================"));
73069
+ logErr(chalk.red(" CodeRifts: PUBLISH BLOCKED"));
73070
+ logErr(chalk.red("========================================"));
73071
+ logErr(` Policy: ${perm.policy}`);
73072
+ logErr(` execution_action: ${perm.execution_action || "(none)"}`);
73073
+ logErr(` decision: ${perm.decision || "(none)"}`);
73074
+ logErr(` baseline: ${beforeRes.source}`);
73075
+ logErr(` fail policy: exit 1 on BLOCK / resolver error / preflight unreachability`);
73076
+ logErr(chalk.red("========================================"));
73077
+ logErr("");
73078
+ }
73079
+ return finish(payload2, { log, json: options.json });
73080
+ }
73081
+ const payload = {
73082
+ ok: true,
73083
+ exitCode: 0,
73084
+ code: "ALLOW",
73085
+ message: "Publish permitted.",
73086
+ policy: perm.policy,
73087
+ execution_action: perm.execution_action,
73088
+ decision: perm.decision,
73089
+ before_source: beforeRes.source,
73090
+ receipt
73091
+ };
73092
+ if (!options.json) {
73093
+ log(chalk.green("CodeRifts publish-gate: ALLOW"));
73094
+ log(` Policy: ${perm.policy}`);
73095
+ log(` execution_action: ${perm.execution_action || "(mapped from decision)"}`);
73096
+ log(` baseline: ${beforeRes.source}`);
73097
+ if (receipt) log(` Receipt reference: ${receipt}`);
73098
+ else log(" Receipt reference: (none issued on this path)");
73099
+ }
73100
+ return finish(payload, { log, json: options.json });
73101
+ }
73102
+ function finish(payload, { log, json }) {
73103
+ if (json) {
73104
+ log(JSON.stringify(payload, null, 2));
73105
+ }
73106
+ return payload;
73107
+ }
73108
+ module2.exports = {
73109
+ runPublishGate,
73110
+ resolveBeforeSpec,
73111
+ resolveAfterSpec,
73112
+ evaluatePublishPermission,
73113
+ extractReceiptRef,
73114
+ gitShow,
73115
+ defaultGit,
73116
+ PERMIT_ACTIONS,
73117
+ CLOSED_ACTIONS
73118
+ };
73119
+ }
73120
+ });
73121
+
73122
+ // src/registry-validation-core.js
73123
+ var require_registry_validation_core = __commonJS({
73124
+ "src/registry-validation-core.js"(exports2, module2) {
73125
+ "use strict";
73126
+ var yaml = require_js_yaml();
73127
+ function safeParse(content) {
73128
+ if (!content) return null;
73129
+ try {
73130
+ const trimmed = content.trim();
73131
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
73132
+ return JSON.parse(trimmed);
73133
+ }
73134
+ return yaml.load(trimmed);
73135
+ } catch (_) {
73136
+ return null;
73137
+ }
73138
+ }
73139
+ function validateOpenApiSpec(parsed) {
73140
+ if (!parsed || typeof parsed !== "object") {
73141
+ return { valid: false, error: "Not a valid YAML or JSON object" };
73142
+ }
73143
+ if (parsed.openapi) {
73144
+ const ver = String(parsed.openapi);
73145
+ if (ver.startsWith("3.")) {
73146
+ return { valid: true, version: ver };
73147
+ }
73148
+ return { valid: false, error: `Unsupported OpenAPI version: ${ver}` };
73149
+ }
73150
+ if (parsed.swagger) {
73151
+ const ver = String(parsed.swagger);
73152
+ if (ver.startsWith("2.")) {
73153
+ return { valid: true, version: ver };
73154
+ }
73155
+ return { valid: false, error: `Unsupported Swagger version: ${ver}` };
73156
+ }
73157
+ return { valid: false, error: "Missing required field: 'openapi' or 'swagger'" };
73158
+ }
73159
+ function extractEndpoints(parsed) {
73160
+ const endpoints = [];
73161
+ const paths = parsed.paths || {};
73162
+ const httpMethods = ["get", "post", "put", "patch", "delete", "head", "options"];
73163
+ for (const [path, pathItem] of Object.entries(paths)) {
73164
+ if (!pathItem || typeof pathItem !== "object") continue;
73165
+ for (const method of httpMethods) {
73166
+ if (pathItem[method]) {
73167
+ endpoints.push({
73168
+ path,
73169
+ method: method.toUpperCase(),
73170
+ operationId: pathItem[method].operationId || ""
73171
+ });
73172
+ }
73173
+ }
73174
+ }
73175
+ return endpoints;
73176
+ }
73177
+ function extractSchemaNames(parsed) {
73178
+ const schemas = [];
73179
+ const components = parsed.components?.schemas || {};
73180
+ for (const [name, schema] of Object.entries(components)) {
73181
+ const hash = JSON.stringify(schema);
73182
+ schemas.push({ name, hash });
73183
+ }
73184
+ return schemas;
73185
+ }
73186
+ function extractDefinedScopes(parsed) {
73187
+ const scopes = /* @__PURE__ */ new Set();
73188
+ const schemes = parsed.components?.securitySchemes || {};
73189
+ for (const scheme of Object.values(schemes)) {
73190
+ if (scheme.type === "oauth2" && scheme.flows) {
73191
+ for (const flow of Object.values(scheme.flows)) {
73192
+ if (flow.scopes) {
73193
+ for (const scope of Object.keys(flow.scopes)) {
73194
+ scopes.add(scope);
73195
+ }
73196
+ }
73197
+ }
73198
+ }
73199
+ }
73200
+ return scopes;
73201
+ }
73202
+ function extractUsedScopes(parsed) {
73203
+ const scopes = /* @__PURE__ */ new Set();
73204
+ if (Array.isArray(parsed.security)) {
73205
+ for (const req of parsed.security) {
73206
+ for (const scopeList of Object.values(req)) {
73207
+ if (Array.isArray(scopeList)) {
73208
+ for (const s of scopeList) scopes.add(s);
73209
+ }
73210
+ }
73211
+ }
73212
+ }
73213
+ const paths = parsed.paths || {};
73214
+ const httpMethods = ["get", "post", "put", "patch", "delete", "head", "options"];
73215
+ for (const pathItem of Object.values(paths)) {
73216
+ if (!pathItem || typeof pathItem !== "object") continue;
73217
+ for (const method of httpMethods) {
73218
+ const op = pathItem[method];
73219
+ if (op?.security && Array.isArray(op.security)) {
73220
+ for (const req of op.security) {
73221
+ for (const scopeList of Object.values(req)) {
73222
+ if (Array.isArray(scopeList)) {
73223
+ for (const s of scopeList) scopes.add(s);
73224
+ }
73225
+ }
73226
+ }
73227
+ }
73228
+ }
73229
+ }
73230
+ return scopes;
73231
+ }
73232
+ function extractRefs(obj, refs = /* @__PURE__ */ new Set()) {
73233
+ if (!obj || typeof obj !== "object") return refs;
73234
+ if (Array.isArray(obj)) {
73235
+ for (const item of obj) extractRefs(item, refs);
73236
+ return refs;
73237
+ }
73238
+ for (const [key, value] of Object.entries(obj)) {
73239
+ if (key === "$ref" && typeof value === "string") {
73240
+ refs.add(value);
73241
+ } else {
73242
+ extractRefs(value, refs);
73243
+ }
73244
+ }
73245
+ return refs;
73246
+ }
73247
+ function refResolves(parsed, ref) {
73248
+ if (!ref.startsWith("#/")) return true;
73249
+ const parts = ref.replace("#/", "").split("/");
73250
+ let current = parsed;
73251
+ for (const part of parts) {
73252
+ if (!current || typeof current !== "object") return false;
73253
+ current = current[part];
73254
+ }
73255
+ return current !== void 0;
73256
+ }
73257
+ function validateRegistry(specs) {
73258
+ if (!Array.isArray(specs) || specs.length === 0) {
73259
+ return { valid: true, issues: [], stats: { specs_count: 0, endpoints_count: 0, schemas_count: 0 } };
73260
+ }
73261
+ const issues = [];
73262
+ const parsedSpecs = [];
73263
+ for (const { name, spec } of specs) {
73264
+ const parsed = safeParse(spec);
73265
+ if (!parsed) {
73266
+ issues.push({
73267
+ severity: "error",
73268
+ type: "parse_error",
73269
+ message: `Could not parse spec '${name}' as valid YAML or JSON`,
73270
+ specs: [name]
73271
+ });
73272
+ continue;
73273
+ }
73274
+ const validation = validateOpenApiSpec(parsed);
73275
+ if (!validation.valid) {
73276
+ issues.push({
73277
+ severity: "error",
73278
+ type: "invalid_spec",
73279
+ message: `Spec '${name}' is not a valid OpenAPI document: ${validation.error}`,
73280
+ specs: [name]
73281
+ });
73282
+ continue;
73283
+ }
73284
+ parsedSpecs.push({ name, parsed });
73285
+ }
73286
+ const endpointMap = /* @__PURE__ */ new Map();
73287
+ for (const { name, parsed } of parsedSpecs) {
73288
+ const endpoints = extractEndpoints(parsed);
73289
+ for (const ep of endpoints) {
73290
+ const key = `${ep.method} ${ep.path}`;
73291
+ if (!endpointMap.has(key)) endpointMap.set(key, []);
73292
+ endpointMap.get(key).push({ spec: name, operationId: ep.operationId });
73293
+ }
73294
+ }
73295
+ for (const [endpoint, owners] of endpointMap) {
73296
+ if (owners.length > 1) {
73297
+ const specNames = owners.map((o) => o.spec);
73298
+ issues.push({
73299
+ severity: "warning",
73300
+ type: "endpoint_collision",
73301
+ message: `Endpoint '${endpoint}' is defined in multiple specs: ${specNames.join(", ")}`,
73302
+ specs: specNames
73303
+ });
73304
+ }
73305
+ }
73306
+ const schemaMap = /* @__PURE__ */ new Map();
73307
+ for (const { name, parsed } of parsedSpecs) {
73308
+ const schemas = extractSchemaNames(parsed);
73309
+ for (const s of schemas) {
73310
+ if (!schemaMap.has(s.name)) schemaMap.set(s.name, []);
73311
+ schemaMap.get(s.name).push({ spec: name, hash: s.hash });
73312
+ }
73313
+ }
73314
+ for (const [schemaName, definitions] of schemaMap) {
73315
+ if (definitions.length > 1) {
73316
+ const uniqueHashes = new Set(definitions.map((d) => d.hash));
73317
+ if (uniqueHashes.size > 1) {
73318
+ const specNames = definitions.map((d) => d.spec);
73319
+ issues.push({
73320
+ severity: "warning",
73321
+ type: "schema_conflict",
73322
+ message: `Schema '${schemaName}' has conflicting definitions across specs: ${specNames.join(", ")}`,
73323
+ specs: specNames
73324
+ });
73325
+ }
73326
+ }
73327
+ }
73328
+ for (const { name, parsed } of parsedSpecs) {
73329
+ const defined = extractDefinedScopes(parsed);
73330
+ const used = extractUsedScopes(parsed);
73331
+ for (const scope of used) {
73332
+ if (!defined.has(scope)) {
73333
+ issues.push({
73334
+ severity: "warning",
73335
+ type: "undefined_scope",
73336
+ message: `Scope '${scope}' is used in '${name}' but not defined in securitySchemes`,
73337
+ specs: [name]
73338
+ });
73339
+ }
73340
+ }
73341
+ for (const scope of defined) {
73342
+ if (!used.has(scope)) {
73343
+ issues.push({
73344
+ severity: "info",
73345
+ type: "unused_scope",
73346
+ message: `Scope '${scope}' is defined in '${name}' but never used in any operation`,
73347
+ specs: [name]
73348
+ });
73349
+ }
73350
+ }
73351
+ }
73352
+ for (const { name, parsed } of parsedSpecs) {
73353
+ const refs = extractRefs(parsed);
73354
+ for (const ref of refs) {
73355
+ if (!refResolves(parsed, ref)) {
73356
+ issues.push({
73357
+ severity: "error",
73358
+ type: "unresolved_ref",
73359
+ message: `$ref '${ref}' in '${name}' does not resolve`,
73360
+ specs: [name]
73361
+ });
73362
+ }
73363
+ }
73364
+ }
73365
+ let totalEndpoints = 0;
73366
+ let totalSchemas = 0;
73367
+ for (const { parsed } of parsedSpecs) {
73368
+ totalEndpoints += extractEndpoints(parsed).length;
73369
+ totalSchemas += extractSchemaNames(parsed).length;
73370
+ }
73371
+ const hasErrors = issues.some((i) => i.severity === "error");
73372
+ return {
73373
+ valid: !hasErrors,
73374
+ issues,
73375
+ stats: {
73376
+ specs_count: parsedSpecs.length,
73377
+ endpoints_count: totalEndpoints,
73378
+ schemas_count: totalSchemas
73379
+ }
73380
+ };
73381
+ }
73382
+ module2.exports = {
73383
+ validateRegistry,
73384
+ safeParse,
73385
+ validateOpenApiSpec,
73386
+ // Exported for testing
73387
+ extractEndpoints,
73388
+ extractSchemaNames,
73389
+ extractDefinedScopes,
73390
+ extractUsedScopes,
73391
+ extractRefs,
73392
+ refResolves
73393
+ };
73394
+ }
73395
+ });
73396
+
73397
+ // src/commands/registry-gate.js
73398
+ var require_registry_gate = __commonJS({
73399
+ "src/commands/registry-gate.js"(exports2, module2) {
73400
+ "use strict";
73401
+ var fs = require("fs");
73402
+ var path = require("path");
73403
+ var chalk = require_source();
73404
+ var { matchGlob } = require_cjs3();
73405
+ var {
73406
+ validateRegistry,
73407
+ safeParse,
73408
+ validateOpenApiSpec
73409
+ } = require_registry_validation_core();
73410
+ if (process.env.NO_COLOR) chalk.level = 0;
73411
+ var SPEC_EXT = /* @__PURE__ */ new Set([".yaml", ".yml", ".json"]);
73412
+ function walkSpecCandidates(rootDir, {
73413
+ readdirSync = fs.readdirSync.bind(fs),
73414
+ statSync = fs.statSync.bind(fs)
73415
+ } = {}) {
73416
+ const out = [];
73417
+ function walk(absDir) {
73418
+ let entries;
73419
+ try {
73420
+ entries = readdirSync(absDir, { withFileTypes: true });
73421
+ } catch (err) {
73422
+ const e = new Error(`Cannot read directory ${absDir}: ${err && err.message}`);
73423
+ e.code = "GATE_ERROR";
73424
+ e.path = absDir;
73425
+ throw e;
73426
+ }
73427
+ for (const ent of entries) {
73428
+ const name = ent.name;
73429
+ if (name === "node_modules" || name.startsWith(".")) continue;
73430
+ const abs = path.join(absDir, name);
73431
+ let isDir = ent.isDirectory && ent.isDirectory();
73432
+ let isFile = ent.isFile && ent.isFile();
73433
+ if (!isDir && !isFile) {
73434
+ try {
73435
+ const st = statSync(abs);
73436
+ isDir = st.isDirectory();
73437
+ isFile = st.isFile();
73438
+ } catch (err) {
73439
+ const e = new Error(`Cannot stat ${abs}: ${err && err.message}`);
73440
+ e.code = "GATE_ERROR";
73441
+ e.path = abs;
73442
+ throw e;
73443
+ }
73444
+ }
73445
+ if (isDir) {
73446
+ walk(abs);
73447
+ continue;
73448
+ }
73449
+ if (isFile) {
73450
+ const ext = path.extname(name).toLowerCase();
73451
+ if (SPEC_EXT.has(ext)) out.push(abs);
73452
+ }
73453
+ }
73454
+ }
73455
+ walk(rootDir);
73456
+ return out.sort();
73457
+ }
73458
+ function relPosix(rootDir, absPath) {
73459
+ let rel = path.relative(rootDir, absPath);
73460
+ if (path.sep !== "/") rel = rel.split(path.sep).join("/");
73461
+ return rel;
73462
+ }
73463
+ function readFileThreeState(absPath, readFileSync = fs.readFileSync.bind(fs)) {
73464
+ try {
73465
+ const content = readFileSync(absPath, "utf8");
73466
+ return { ok: true, content: content == null ? "" : String(content) };
73467
+ } catch (err) {
73468
+ const msg = err && err.message ? String(err.message) : String(err);
73469
+ return {
73470
+ ok: false,
73471
+ code: "GATE_ERROR",
73472
+ message: `unreadable file: ${absPath} (${msg.slice(0, 200)})`,
73473
+ path: absPath
73474
+ };
73475
+ }
73476
+ }
73477
+ function discoverRegistrySpecs(dir, {
73478
+ glob = null,
73479
+ readdirSync = fs.readdirSync.bind(fs),
73480
+ readFileSync = fs.readFileSync.bind(fs),
73481
+ statSync = fs.statSync.bind(fs)
73482
+ } = {}) {
73483
+ const rootDir = path.resolve(dir);
73484
+ let candidates;
73485
+ try {
73486
+ candidates = walkSpecCandidates(rootDir, { readdirSync, statSync });
73487
+ } catch (err) {
73488
+ return {
73489
+ ok: false,
73490
+ code: err.code || "GATE_ERROR",
73491
+ message: err.message || String(err),
73492
+ path: err.path
73493
+ };
73494
+ }
73495
+ if (glob) {
73496
+ candidates = candidates.filter((abs) => matchGlob(glob, relPosix(rootDir, abs)));
73497
+ }
73498
+ const specs = [];
73499
+ let skippedNonSpec = 0;
73500
+ for (const abs of candidates) {
73501
+ const read = readFileThreeState(abs, readFileSync);
73502
+ if (!read.ok) {
73503
+ return {
73504
+ ok: false,
73505
+ code: "GATE_ERROR",
73506
+ message: read.message,
73507
+ path: read.path || abs
73508
+ };
73509
+ }
73510
+ const name = relPosix(rootDir, abs);
73511
+ const parsed = safeParse(read.content);
73512
+ if (!parsed || typeof parsed !== "object") {
73513
+ specs.push({ name, spec: read.content });
73514
+ continue;
73515
+ }
73516
+ const shape = validateOpenApiSpec(parsed);
73517
+ if (!shape.valid) {
73518
+ if (shape.error && shape.error.includes("Missing required field: 'openapi' or 'swagger'")) {
73519
+ skippedNonSpec += 1;
73520
+ continue;
73521
+ }
73522
+ specs.push({ name, spec: read.content });
73523
+ continue;
73524
+ }
73525
+ specs.push({ name, spec: read.content });
73526
+ }
73527
+ return {
73528
+ ok: true,
73529
+ specs,
73530
+ skippedNonSpec,
73531
+ candidates: candidates.length,
73532
+ rootDir
73533
+ };
73534
+ }
73535
+ function findingsFailGate(issues, { warnOnly = false, errorsOnly = false } = {}) {
73536
+ if (warnOnly) return false;
73537
+ for (const i of issues || []) {
73538
+ if (i.severity === "error") return true;
73539
+ if (i.severity === "warning" && !errorsOnly) return true;
73540
+ }
73541
+ return false;
73542
+ }
73543
+ function countBySeverity(issues) {
73544
+ const c = { error: 0, warning: 0, info: 0 };
73545
+ for (const i of issues || []) {
72833
73546
  if (i.severity === "error") c.error += 1;
72834
73547
  else if (i.severity === "warning") c.warning += 1;
72835
73548
  else if (i.severity === "info") c.info += 1;
@@ -99945,7 +100658,7 @@ var require_enforce = __commonJS({
99945
100658
  var DEPLOY_GUIDANCE = [
99946
100659
  "Deploy is declared-only on the server (never server-observed ENFORCING).",
99947
100660
  "Add a CD step that runs: coderifts deploy-gate --env <env> --artifact <id> --receipt <file> --enforce",
99948
- " (phase-1 default is advisory; --enforce attests ENFORCING for the pipeline).",
100661
+ " (fail-closed by default; --enforce attests ENFORCING for inescapable_deploy. CODERIFTS_DEPLOY_ADVISORY=1 to soften.)",
99949
100662
  "Also set policy.require_source_binding: true in .coderifts.yml for the deploy declaration leg."
99950
100663
  ].join("\n");
99951
100664
  var CONTENT_GUIDANCE = [
@@ -101101,809 +101814,218 @@ var require_adopt = __commonJS({
101101
101814
  lines.push(chalk.bold("Per change set (counterfactual)"));
101102
101815
  lines.push(
101103
101816
  pad("REF", 12) + pad("PATH", 28) + pad("ACTION", 18) + pad("RISK", 6) + "DETECTORS"
101104
- );
101105
- lines.push("-".repeat(90));
101106
- for (const row of report.change_sets) {
101107
- const ref = (row.ref || "").slice(0, 10);
101108
- const pth = (row.path || "").slice(0, 26);
101109
- const act = row.execution_action || (row.error ? "ERROR" : "\u2014");
101110
- const risk = row.risk_score != null ? String(row.risk_score) : "\u2014";
101111
- const dets = (row.detectors_fired || []).map((d) => d.id).join(",") || "\u2014";
101112
- lines.push(pad(ref, 12) + pad(pth, 28) + pad(act, 18) + pad(risk, 6) + dets.slice(0, 40));
101113
- }
101114
- lines.push("");
101115
- lines.push(chalk.bold("Per-detector aggregate (default-flip evidence \u2014 not a recommendation)"));
101116
- if (!report.per_detector || report.per_detector.length === 0) {
101117
- lines.push(chalk.dim("(no detectors fired on any change set)"));
101118
- } else {
101119
- lines.push(pad("DETECTOR", 36) + pad("SEVERITY", 12) + "FIRE_COUNT");
101120
- lines.push("-".repeat(60));
101121
- for (const d of report.per_detector) {
101122
- lines.push(pad(d.id, 36) + pad(d.severity, 12) + String(d.fire_count));
101123
- }
101124
- }
101125
- lines.push("");
101126
- lines.push(chalk.dim("High fire_count \u2192 candidate false-positive noise (not safe to default-block without review)."));
101127
- lines.push(chalk.dim("Rare + CRITICAL/HIGH \u2192 safer default-block candidates. Human decides; no auto-flip."));
101128
- lines.push("");
101129
- return lines.join("\n");
101130
- }
101131
- function pad(s, n) {
101132
- const t = String(s || "");
101133
- if (t.length >= n) return `${t.slice(0, n - 1)} `;
101134
- return t + " ".repeat(n - t.length);
101135
- }
101136
- async function runAdopt(options = {}, deps = {}) {
101137
- const log = deps.log || console.log.bind(console);
101138
- const logErr = deps.logErr || console.error.bind(console);
101139
- const load = deps.loadCore || loadCore;
101140
- const collected = collectChangeSetsFromGit(options, deps);
101141
- if (!collected.ok) {
101142
- logErr(chalk.red(`CodeRifts adopt: ${collected.code} \u2014 ${collected.message}`));
101143
- return { ok: false, exitCode: 1, code: collected.code, message: collected.message };
101144
- }
101145
- let core;
101146
- try {
101147
- core = load();
101148
- } catch (err) {
101149
- logErr(chalk.red(`CodeRifts adopt: ${err.message}`));
101150
- return { ok: false, exitCode: 1, code: "CORE_LOAD_ERROR", message: err.message };
101151
- }
101152
- const runCore = () => core.buildCounterfactualReport(collected.changeSets, deps.config || {});
101153
- const report = options.json ? await withJsonQuietAnalyzerLogs(runCore) : await runCore();
101154
- if (options.json) {
101155
- log(JSON.stringify({
101156
- ...report,
101157
- meta: {
101158
- commit_count: collected.commit_count,
101159
- change_set_count: collected.changeSets.length
101160
- }
101161
- }, null, 2));
101162
- } else {
101163
- log(renderHuman(report, { commit_count: collected.commit_count }));
101164
- }
101165
- return {
101166
- ok: true,
101167
- exitCode: 0,
101168
- report,
101169
- meta: { commit_count: collected.commit_count }
101170
- };
101171
- }
101172
- module2.exports = {
101173
- runAdopt,
101174
- collectChangeSetsFromGit,
101175
- isContractPath,
101176
- withJsonQuietAnalyzerLogs,
101177
- renderHuman,
101178
- defaultGit,
101179
- SOURCE_CODE_EXT_RE
101180
- };
101181
- }
101182
- });
101183
-
101184
- // src/commands/outcome.js
101185
- var require_outcome = __commonJS({
101186
- "src/commands/outcome.js"(exports2, module2) {
101187
- "use strict";
101188
- var chalk = require_source();
101189
- var { getApiKey } = require_config();
101190
- var { cloudPostOutcome } = require_cloud();
101191
- if (process.env.NO_COLOR) chalk.level = 0;
101192
- var OUTCOME_KINDS = Object.freeze([
101193
- "deploy_succeeded",
101194
- "deploy_failed",
101195
- "rolled_back",
101196
- "consumer_break_reported",
101197
- "remediation_verified_working",
101198
- "false_positive_reported",
101199
- "other_reported"
101200
- ]);
101201
- var OUTCOME_KIND_SET = new Set(OUTCOME_KINDS);
101202
- var USAGE = [
101203
- "Usage: coderifts outcome <kind> --decision <decision_id> [--observed-at <ISO>] [--details <json>] [--json]",
101204
- "",
101205
- "Report a post-hoc observed outcome for a past decision_id to the CodeRifts cloud API.",
101206
- "This records the CALLER'S assertion (e.g. your deploy job knows success/failure) \u2014",
101207
- "the command does NOT itself verify that a deploy succeeded or failed.",
101208
- "",
101209
- "kind (required, closed set):",
101210
- ` ${OUTCOME_KINDS.join(", ")}`,
101211
- "",
101212
- "Requires a cloud API key (coderifts login or CODERIFTS_API_KEY).",
101213
- "POST /api/v1/outcomes \u2014 source is always reported (server-side); reporter is derived from the key."
101214
- ].join("\n");
101215
- function isValidOutcomeKind(kind) {
101216
- return typeof kind === "string" && OUTCOME_KIND_SET.has(kind);
101217
- }
101218
- function isValidObservedAt(iso) {
101219
- if (typeof iso !== "string" || !iso.trim()) return false;
101220
- const t = Date.parse(iso);
101221
- return Number.isFinite(t);
101222
- }
101223
- function parseDetails(raw) {
101224
- if (raw == null || raw === "") return { ok: true, details: null };
101225
- if (typeof raw !== "string") return { ok: false, error: "--details must be a JSON string" };
101226
- try {
101227
- const v = JSON.parse(raw);
101228
- if (v !== null && typeof v !== "object") {
101229
- return { ok: false, error: "--details must be a JSON object or array" };
101230
- }
101231
- return { ok: true, details: v };
101232
- } catch (e) {
101233
- return { ok: false, error: `--details is not valid JSON: ${e && e.message || "parse error"}` };
101234
- }
101235
- }
101236
- async function runOutcome(kind, options = {}, deps = {}) {
101237
- const getKey = deps.getApiKey || getApiKey;
101238
- const postOutcome = deps.cloudPostOutcome || cloudPostOutcome;
101239
- const log = deps.log || console.log;
101240
- const errLog = deps.errLog || console.error;
101241
- const nowIso = deps.nowIso || (() => (/* @__PURE__ */ new Date()).toISOString());
101242
- if (kind == null || String(kind).trim() === "") {
101243
- errLog(chalk.red("Error: missing outcome kind"));
101244
- errLog(USAGE);
101245
- return { exitCode: 1, error: "missing_kind" };
101246
- }
101247
- const outcomeKind = String(kind).trim();
101248
- if (!isValidOutcomeKind(outcomeKind)) {
101249
- errLog(chalk.red(`Error: invalid outcome kind '${outcomeKind}'`));
101250
- errLog(chalk.dim(` Valid kinds: ${OUTCOME_KINDS.join(", ")}`));
101251
- return { exitCode: 1, error: "invalid_kind" };
101252
- }
101253
- const decisionId = options.decision != null ? String(options.decision).trim() : "";
101254
- if (!decisionId) {
101255
- errLog(chalk.red("Error: --decision <decision_id> is required"));
101256
- errLog(USAGE);
101257
- return { exitCode: 1, error: "missing_decision" };
101258
- }
101259
- let observedAt;
101260
- if (options.observedAt != null && String(options.observedAt).trim() !== "") {
101261
- observedAt = String(options.observedAt).trim();
101262
- if (!isValidObservedAt(observedAt)) {
101263
- errLog(chalk.red("Error: --observed-at must be a valid ISO-8601 timestamp"));
101264
- return { exitCode: 1, error: "invalid_observed_at" };
101265
- }
101266
- } else {
101267
- observedAt = nowIso();
101268
- }
101269
- const det = parseDetails(options.details);
101270
- if (!det.ok) {
101271
- errLog(chalk.red(`Error: ${det.error}`));
101272
- return { exitCode: 1, error: "invalid_details" };
101273
- }
101274
- const apiKey = getKey();
101275
- if (!apiKey) {
101276
- errLog(chalk.red("Error: no API key. Run `coderifts login` or set CODERIFTS_API_KEY."));
101277
- return { exitCode: 1, error: "missing_api_key" };
101278
- }
101279
- const body = {
101280
- decision_id: decisionId,
101281
- outcome_kind: outcomeKind,
101282
- observed_at: observedAt
101283
- };
101284
- if (det.details != null) body.details = det.details;
101285
- let response;
101286
- try {
101287
- response = await postOutcome(apiKey, body);
101288
- } catch (e) {
101289
- const msg = e && e.message ? String(e.message) : "request failed";
101290
- errLog(chalk.red(`Error: ${msg}`));
101291
- if (e && e.statusCode) errLog(chalk.dim(` (HTTP ${e.statusCode})`));
101292
- if (e && e.code) errLog(chalk.dim(` (${e.code})`));
101293
- return { exitCode: 1, error: msg, body };
101294
- }
101295
- if (options.json) {
101296
- log(JSON.stringify(response, null, 2));
101297
- } else {
101298
- const o = response && response.outcome ? response.outcome : response;
101299
- log(chalk.bold("CodeRifts outcome recorded"));
101300
- log(` kind: ${outcomeKind}`);
101301
- log(` decision_id: ${decisionId}`);
101302
- log(` observed_at: ${observedAt}`);
101303
- if (o && o.id) log(` id: ${o.id}`);
101304
- if (response && response.source) log(` source: ${response.source}`);
101305
- log(chalk.dim(" Caller assertion only \u2014 this command did not verify the deploy."));
101306
- }
101307
- return { exitCode: 0, response, body };
101308
- }
101309
- module2.exports = {
101310
- runOutcome,
101311
- isValidOutcomeKind,
101312
- isValidObservedAt,
101313
- parseDetails,
101314
- OUTCOME_KINDS,
101315
- USAGE
101316
- };
101317
- }
101318
- });
101319
-
101320
- // src/commands/claude-hook.js
101321
- var require_claude_hook = __commonJS({
101322
- "src/commands/claude-hook.js"(exports2, module2) {
101323
- "use strict";
101324
- var fs = require("fs");
101325
- var path = require("path");
101326
- var { execSync } = require("child_process");
101327
- var { getApiKey } = require_config();
101328
- var { cloudAuthorizePreflight } = require_cloud();
101329
- var DEFAULT_SPEC_PATH = "api/openapi.yaml";
101330
- var CLOSED_ACTIONS = /* @__PURE__ */ new Set([
101331
- "CONTINUE",
101332
- "CONTINUE_WITH_MONITORING",
101333
- "REQUEST_APPROVAL",
101334
- "STOP"
101335
- ]);
101336
- var USAGE = [
101337
- "Usage: coderifts claude-hook",
101338
- "",
101339
- "Claude Code PreToolUse hook: gates Write|Edit|MultiEdit when they touch the",
101340
- "configured contract spec path. Reads JSON context from STDIN.",
101341
- "",
101342
- "Exit map (Claude Code semantics \u2014 get this exactly right):",
101343
- " 2 BLOCK \u2014 tool call cancelled (BLOCK/STOP, REQUEST_APPROVAL, unrecognised action,",
101344
- " governance could not run, CONTINUE_WITH_MONITORING without a wired sink)",
101345
- " 0 allow \u2014 CONTINUE, or CWM with a host-asserted monitoring sink, or non-contract skip",
101346
- " 1 NEVER used for security deny (Claude treats exit 1 as non-blocking; action proceeds)",
101347
- "",
101348
- "Fail-closed DEFAULT (exit 2, reason enforce_indeterminate) when governance could not run:",
101349
- " no API key / API unreachable / disk unreadable / edit-apply failure on the spec path",
101350
- " Absence of a key is not permission. Explicit opt-out: CODERIFTS_ADVISORY=1|true.",
101351
- "",
101352
- "Parse-gap DEFAULT (exit 2): unparseable stdin / missing file_path \u2014 refusing (fail-closed);",
101353
- " set CODERIFTS_ADVISORY=1 to soften. JSONL hook_blocked cause stdin_unparseable|missing_file_path.",
101354
- "",
101355
- "Still exit 0 (nothing to govern): non-spec path, identical content.",
101356
- "",
101357
- "CONTINUE_WITH_MONITORING: allow only if the host asserts a sink",
101358
- " (CODERIFTS_MONITORING_SINK_WIRED=1|true or git config coderifts.monitoringSinkWired).",
101359
- " Host claim \u2014 not delivery proof (same class as GuardConfig.monitoringSinkWired).",
101360
- "",
101361
- "CODERIFTS_STRICT=1|true: same fail-closed as default; cannot be weakened by ADVISORY.",
101362
- "",
101363
- "Spec path (same source as git pre-push hook):",
101364
- " git config coderifts.specPath (default: api/openapi.yaml)",
101365
- "",
101366
- "Baseline at PreToolUse: before = current file on disk (pre-edit state \u2014 the tool has",
101367
- "not written yet). after = proposed content from tool_input (Write: content; Edit/MultiEdit:",
101368
- "old\u2192new applied to disk content).",
101369
- "",
101370
- "Push-time equivalent: coderifts hook install (git exit 1 on BLOCK)."
101371
- ].join("\n");
101372
- function isEnvFlag(env, name) {
101373
- const v = env && env[name];
101374
- if (v == null || v === "") return false;
101375
- const s = String(v).trim().toLowerCase();
101376
- return s === "1" || s === "true";
101377
- }
101378
- function isStrictMode(env = process.env) {
101379
- return isEnvFlag(env, "CODERIFTS_STRICT");
101380
- }
101381
- function isAdvisoryMode(env = process.env) {
101382
- return isEnvFlag(env, "CODERIFTS_ADVISORY");
101383
- }
101384
- function isMonitoringSinkWired({ env = process.env, deps = {}, result = null } = {}) {
101385
- if (deps && deps.monitoringSinkWired === true) return true;
101386
- if (isEnvFlag(env, "CODERIFTS_MONITORING_SINK_WIRED")) return true;
101387
- const git = readGitConfig("coderifts.monitoringSinkWired", deps);
101388
- if (git) {
101389
- const s = String(git).trim().toLowerCase();
101390
- if (s === "1" || s === "true") return true;
101391
- }
101392
- if (result && result.monitoringSinkWired === true) return true;
101393
- const dr = result && result.decision_result;
101394
- if (dr && typeof dr === "object" && dr.monitoringSinkWired === true) return true;
101395
- return false;
101396
- }
101397
- var HOOK_TRIGGER_SOURCE = "claude_hook";
101398
- function extractFingerprint(result) {
101399
- const d = result && typeof result === "object" && !Array.isArray(result) ? result : {};
101400
- const dr = d.decision_result && typeof d.decision_result === "object" ? d.decision_result : {};
101401
- const fp = dr.fingerprint || d.fingerprint || d.verdict_fingerprint || d.input_fingerprint;
101402
- return typeof fp === "string" && fp.length > 0 ? fp : void 0;
101403
- }
101404
- function hookSessionId(env) {
101405
- const fromEnv = env && env.CODERIFTS_SESSION_ID && String(env.CODERIFTS_SESSION_ID).trim();
101406
- if (fromEnv) return fromEnv;
101407
- if (!hookSessionId._gen) hookSessionId._gen = `hook-${process.pid}-${Date.now()}`;
101408
- return hookSessionId._gen;
101409
- }
101410
- function appendGuardEventLog(env, event, deps = {}) {
101411
- const p = env && env.CODERIFTS_GUARD_EVENT_LOG;
101412
- if (p == null || String(p).trim() === "") return;
101413
- const errLog = deps.errLog || ((m) => console.error(String(m)));
101414
- const append = deps.appendFileSync || ((file, data) => fs.appendFileSync(file, data));
101415
- try {
101416
- append(String(p).trim(), JSON.stringify(event) + "\n");
101417
- } catch (err) {
101418
- try {
101419
- errLog(`CodeRifts claude-hook: CODERIFTS_GUARD_EVENT_LOG write failed (${err && err.message})`);
101420
- } catch {
101421
- }
101422
- }
101423
- }
101424
- function failClosedOrAdvisory({ advisory, strict, site, softMsg, why, errLog }) {
101425
- if (advisory && !strict) {
101426
- errLog(`${softMsg} (CODERIFTS_ADVISORY)`);
101427
- return { exitCode: 0, reason: site, site };
101428
- }
101429
- if (strict) {
101430
- errLog(
101431
- `CodeRifts claude-hook: CODERIFTS_STRICT: governance could not run (${why}) \u2014 blocking. Set a key / restore network, or unset CODERIFTS_STRICT.`
101432
- );
101433
- return { exitCode: 2, reason: "enforce_indeterminate", site, strictBlocked: true };
101434
- }
101435
- errLog(
101436
- `CodeRifts claude-hook: governance could not run (${why}) \u2014 blocking. Set CODERIFTS_ADVISORY=1 to allow without governance (explicit opt-out).`
101437
- );
101438
- return { exitCode: 2, reason: "enforce_indeterminate", site };
101439
- }
101440
- function softOrStrictBlock({ strict, reason, softMsg, why, errLog }) {
101441
- return failClosedOrAdvisory({
101442
- advisory: false,
101443
- strict: !!strict,
101444
- site: reason,
101445
- softMsg,
101446
- why,
101447
- errLog
101448
- });
101449
- }
101450
- function readGitConfig(key, deps = {}) {
101451
- const run = deps.execSync || execSync;
101452
- const cwd = deps.cwd || process.cwd();
101453
- try {
101454
- const v = run(`git config ${key}`, { encoding: "utf8", cwd, stdio: ["ignore", "pipe", "pipe"] });
101455
- const t = String(v || "").trim();
101456
- return t || null;
101457
- } catch {
101458
- return null;
101459
- }
101460
- }
101461
- function resolveApiKey(deps = {}) {
101462
- const getKey = deps.getApiKey || getApiKey;
101463
- const fromLogin = getKey();
101464
- if (fromLogin) return fromLogin;
101465
- const env = deps.env || process.env;
101466
- const fromEnv = env.CODERIFTS_API_KEY && String(env.CODERIFTS_API_KEY).trim();
101467
- if (fromEnv) return fromEnv;
101468
- return readGitConfig("coderifts.apiKey", deps);
101469
- }
101470
- function resolveSpecPath(deps = {}) {
101471
- const fromGit = readGitConfig("coderifts.specPath", deps);
101472
- if (fromGit) return fromGit;
101473
- return DEFAULT_SPEC_PATH;
101474
- }
101475
- function parseStdinJson(raw) {
101476
- if (raw == null || String(raw).trim() === "") {
101477
- return { ok: false, reason: "empty stdin" };
101478
- }
101479
- let obj;
101480
- try {
101481
- obj = JSON.parse(String(raw));
101482
- } catch {
101483
- return { ok: false, reason: "stdin is not JSON" };
101484
- }
101485
- if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
101486
- return { ok: false, reason: "stdin JSON is not an object" };
101487
- }
101488
- const toolName = obj.tool_name || obj.toolName || obj.name || null;
101489
- let toolInput = obj.tool_input || obj.toolInput || obj.input || null;
101490
- if (toolInput == null && obj.file_path) toolInput = obj;
101491
- if (!toolName || typeof toolName !== "string") {
101492
- return { ok: false, reason: "missing tool_name" };
101493
- }
101494
- if (!toolInput || typeof toolInput !== "object") {
101495
- return { ok: false, reason: "missing tool_input" };
101496
- }
101497
- return { ok: true, toolName: String(toolName), toolInput };
101498
- }
101499
- function isSpecPath(filePath, specPath) {
101500
- if (!filePath || !specPath) return false;
101501
- const fp = path.normalize(String(filePath).replace(/\\/g, "/"));
101502
- const sp = path.normalize(String(specPath).replace(/\\/g, "/"));
101503
- if (fp === sp) return true;
101504
- if (fp.endsWith("/" + sp) || fp.endsWith(sp)) return true;
101505
- const baseFp = path.basename(fp);
101506
- const baseSp = path.basename(sp);
101507
- if (baseFp === baseSp && (fp.endsWith(sp) || sp.endsWith(baseSp))) {
101508
- const tail = sp.split("/").filter(Boolean).join("/");
101509
- return fp.replace(/\\/g, "/").endsWith(tail);
101510
- }
101511
- return false;
101512
- }
101513
- function deriveAfterContent(toolName, toolInput, diskBefore) {
101514
- const name = String(toolName || "");
101515
- if (name === "Write" || name === "write") {
101516
- if (typeof toolInput.content !== "string") {
101517
- return { ok: false, reason: "Write tool_input.content missing or not a string" };
101518
- }
101519
- return { ok: true, after: toolInput.content };
101520
- }
101521
- if (name === "Edit" || name === "edit") {
101522
- const oldS = toolInput.old_string != null ? toolInput.old_string : toolInput.oldString;
101523
- const newS = toolInput.new_string != null ? toolInput.new_string : toolInput.newString;
101524
- if (typeof oldS !== "string" || typeof newS !== "string") {
101525
- return { ok: false, reason: "Edit tool_input.old_string/new_string missing" };
101526
- }
101527
- if (!diskBefore.includes(oldS)) {
101528
- return { ok: false, reason: "Edit old_string not found in disk content" };
101529
- }
101530
- return { ok: true, after: diskBefore.replace(oldS, newS) };
101531
- }
101532
- if (name === "MultiEdit" || name === "multi_edit" || name === "multiEdit") {
101533
- const edits = toolInput.edits || toolInput.Edits;
101534
- if (!Array.isArray(edits) || edits.length === 0) {
101535
- return { ok: false, reason: "MultiEdit tool_input.edits missing or empty" };
101536
- }
101537
- let cur = diskBefore;
101538
- for (let i = 0; i < edits.length; i++) {
101539
- const e = edits[i] || {};
101540
- const oldS = e.old_string != null ? e.old_string : e.oldString;
101541
- const newS = e.new_string != null ? e.new_string : e.newString;
101542
- if (typeof oldS !== "string" || typeof newS !== "string") {
101543
- return { ok: false, reason: `MultiEdit edits[${i}] missing old_string/new_string` };
101544
- }
101545
- if (!cur.includes(oldS)) {
101546
- return { ok: false, reason: `MultiEdit edits[${i}] old_string not found in content` };
101547
- }
101548
- cur = cur.replace(oldS, newS);
101817
+ );
101818
+ lines.push("-".repeat(90));
101819
+ for (const row of report.change_sets) {
101820
+ const ref = (row.ref || "").slice(0, 10);
101821
+ const pth = (row.path || "").slice(0, 26);
101822
+ const act = row.execution_action || (row.error ? "ERROR" : "\u2014");
101823
+ const risk = row.risk_score != null ? String(row.risk_score) : "\u2014";
101824
+ const dets = (row.detectors_fired || []).map((d) => d.id).join(",") || "\u2014";
101825
+ lines.push(pad(ref, 12) + pad(pth, 28) + pad(act, 18) + pad(risk, 6) + dets.slice(0, 40));
101826
+ }
101827
+ lines.push("");
101828
+ lines.push(chalk.bold("Per-detector aggregate (default-flip evidence \u2014 not a recommendation)"));
101829
+ if (!report.per_detector || report.per_detector.length === 0) {
101830
+ lines.push(chalk.dim("(no detectors fired on any change set)"));
101831
+ } else {
101832
+ lines.push(pad("DETECTOR", 36) + pad("SEVERITY", 12) + "FIRE_COUNT");
101833
+ lines.push("-".repeat(60));
101834
+ for (const d of report.per_detector) {
101835
+ lines.push(pad(d.id, 36) + pad(d.severity, 12) + String(d.fire_count));
101549
101836
  }
101550
- return { ok: true, after: cur };
101551
101837
  }
101552
- return { ok: false, reason: `unsupported tool_name for content derive: ${name}` };
101553
- }
101554
- function isV2DecisionBody(d, dr) {
101555
- if (dr && typeof dr === "object") return true;
101556
- if (d.preflight_mode != null && d.preflight_mode !== "") return true;
101557
- const ver = d.decision_spec_version;
101558
- return typeof ver === "string" && ver.startsWith("2.");
101838
+ lines.push("");
101839
+ lines.push(chalk.dim("High fire_count \u2192 candidate false-positive noise (not safe to default-block without review)."));
101840
+ lines.push(chalk.dim("Rare + CRITICAL/HIGH \u2192 safer default-block candidates. Human decides; no auto-flip."));
101841
+ lines.push("");
101842
+ return lines.join("\n");
101559
101843
  }
101560
- function allowLegacyDecisionMap(d, dr) {
101561
- return d.decision_spec_version === "1.0" && !isV2DecisionBody(d, dr);
101844
+ function pad(s, n) {
101845
+ const t = String(s || "");
101846
+ if (t.length >= n) return `${t.slice(0, n - 1)} `;
101847
+ return t + " ".repeat(n - t.length);
101562
101848
  }
101563
- function mapDecisionSeverity(result) {
101564
- const d = result && typeof result === "object" && !Array.isArray(result) ? result : {};
101565
- let ea = null;
101566
- const dr = d.decision_result;
101567
- if (dr && typeof dr === "object" && typeof dr.execution_action === "string" && dr.execution_action !== "") {
101568
- ea = dr.execution_action;
101569
- } else if (typeof d.execution_action === "string" && d.execution_action !== "") {
101570
- ea = d.execution_action;
101849
+ async function runAdopt(options = {}, deps = {}) {
101850
+ const log = deps.log || console.log.bind(console);
101851
+ const logErr = deps.logErr || console.error.bind(console);
101852
+ const load = deps.loadCore || loadCore;
101853
+ const collected = collectChangeSetsFromGit(options, deps);
101854
+ if (!collected.ok) {
101855
+ logErr(chalk.red(`CodeRifts adopt: ${collected.code} \u2014 ${collected.message}`));
101856
+ return { ok: false, exitCode: 1, code: collected.code, message: collected.message };
101571
101857
  }
101572
- const hasDecision = d.omega_decision != null && d.omega_decision !== "" || d.decision != null && d.decision !== "" || dr && typeof dr === "object" && dr.execution_action;
101573
- if (d.error && !hasDecision && (ea == null || ea === "")) {
101574
- return {
101575
- severity: "INDETERMINATE",
101576
- decision: null,
101577
- decisionId: null,
101578
- executionAction: null
101579
- };
101858
+ let core;
101859
+ try {
101860
+ core = load();
101861
+ } catch (err) {
101862
+ logErr(chalk.red(`CodeRifts adopt: ${err.message}`));
101863
+ return { ok: false, exitCode: 1, code: "CORE_LOAD_ERROR", message: err.message };
101580
101864
  }
101581
- let severity;
101582
- if (ea != null && ea !== "") {
101583
- if (!CLOSED_ACTIONS.has(ea)) {
101584
- severity = "UNKNOWN";
101585
- } else if (ea === "CONTINUE") {
101586
- severity = "ALLOW";
101587
- } else if (ea === "CONTINUE_WITH_MONITORING") {
101588
- severity = "MONITOR";
101589
- } else if (ea === "STOP") {
101590
- severity = "BLOCK";
101591
- } else {
101592
- severity = "REQUIRE_APPROVAL";
101593
- }
101594
- } else if (allowLegacyDecisionMap(d, dr)) {
101595
- const od = d.omega_decision || d.decision;
101596
- if (od == null || od === "") {
101597
- severity = "INDETERMINATE";
101598
- } else if (od === "BLOCK") severity = "BLOCK";
101599
- else if (od === "REQUIRE_APPROVAL") severity = "REQUIRE_APPROVAL";
101600
- else if (od === "WARN") severity = "WARN";
101601
- else severity = "ALLOW";
101865
+ const runCore = () => core.buildCounterfactualReport(collected.changeSets, deps.config || {});
101866
+ const report = options.json ? await withJsonQuietAnalyzerLogs(runCore) : await runCore();
101867
+ if (options.json) {
101868
+ log(JSON.stringify({
101869
+ ...report,
101870
+ meta: {
101871
+ commit_count: collected.commit_count,
101872
+ change_set_count: collected.changeSets.length
101873
+ }
101874
+ }, null, 2));
101602
101875
  } else {
101603
- severity = "INDETERMINATE";
101876
+ log(renderHuman(report, { commit_count: collected.commit_count }));
101604
101877
  }
101605
- const decision = dr && dr.decision || d.decision || d.omega_decision || null;
101606
- const decisionId = dr && dr.decision_id || d.decision_id || null;
101607
101878
  return {
101608
- severity,
101609
- decision: decision != null && decision !== "" ? String(decision) : null,
101610
- decisionId: decisionId != null ? String(decisionId) : null,
101611
- executionAction: ea
101879
+ ok: true,
101880
+ exitCode: 0,
101881
+ report,
101882
+ meta: { commit_count: collected.commit_count }
101612
101883
  };
101613
101884
  }
101614
- function renderDecisionWhy(result, opts = {}) {
101615
- const maxFixes = Number.isInteger(opts.maxFixes) ? opts.maxFixes : 5;
101616
- const maxLines = Number.isInteger(opts.maxLines) ? opts.maxLines : 13;
101617
- const d = result && typeof result === "object" && !Array.isArray(result) ? result : {};
101618
- const dr = d.decision_result && typeof d.decision_result === "object" ? d.decision_result : {};
101619
- const pick = (k) => dr[k] !== void 0 ? dr[k] : d[k];
101620
- const arr = (v) => Array.isArray(v) ? v : [];
101621
- const str = (v) => typeof v === "string" ? v.trim() : "";
101622
- const lines = [];
101623
- const reasonCodes = [];
101624
- for (const r of arr(pick("blocking_reasons"))) {
101625
- if (!r || typeof r !== "object") continue;
101626
- const code = str(r.code);
101627
- const message = str(r.message);
101628
- if (code) reasonCodes.push(code);
101629
- if (!code && !message) continue;
101630
- lines.push(`- ${code || "REASON"}${message ? `: ${message}` : ""}`);
101631
- }
101632
- for (const r of arr(pick("degraded_reasons"))) {
101633
- if (!r || typeof r !== "object") continue;
101634
- const code = str(r.code);
101635
- const message = str(r.message);
101636
- if (!code && !message) continue;
101637
- lines.push(`- degraded: ${[code, message].filter(Boolean).join(": ")}`);
101638
- }
101639
- const action = str(pick("required_action"));
101640
- if (action) lines.push(`- action: ${action}`);
101641
- const rt = pick("remediation_transaction");
101642
- const changes = arr(rt && typeof rt === "object" ? rt.required_changes : null).filter((c) => c && typeof c === "object");
101643
- const shown = changes.slice(0, maxFixes);
101644
- for (const c of shown) {
101645
- const target = str(c.target);
101646
- const instruction = str(c.instruction);
101647
- if (!instruction && !target) continue;
101648
- lines.push(`- fix${target ? ` (${target})` : ""}: ${instruction || str(c.precise_label)}`);
101649
- }
101650
- if (changes.length > shown.length) lines.push(`- fix: +${changes.length - shown.length} more`);
101651
- return { lines: lines.slice(0, maxLines), reasonCodes };
101885
+ module2.exports = {
101886
+ runAdopt,
101887
+ collectChangeSetsFromGit,
101888
+ isContractPath,
101889
+ withJsonQuietAnalyzerLogs,
101890
+ renderHuman,
101891
+ defaultGit,
101892
+ SOURCE_CODE_EXT_RE
101893
+ };
101894
+ }
101895
+ });
101896
+
101897
+ // src/commands/outcome.js
101898
+ var require_outcome = __commonJS({
101899
+ "src/commands/outcome.js"(exports2, module2) {
101900
+ "use strict";
101901
+ var chalk = require_source();
101902
+ var { getApiKey } = require_config();
101903
+ var { cloudPostOutcome } = require_cloud();
101904
+ if (process.env.NO_COLOR) chalk.level = 0;
101905
+ var OUTCOME_KINDS = Object.freeze([
101906
+ "deploy_succeeded",
101907
+ "deploy_failed",
101908
+ "rolled_back",
101909
+ "consumer_break_reported",
101910
+ "remediation_verified_working",
101911
+ "false_positive_reported",
101912
+ "other_reported"
101913
+ ]);
101914
+ var OUTCOME_KIND_SET = new Set(OUTCOME_KINDS);
101915
+ var USAGE = [
101916
+ "Usage: coderifts outcome <kind> --decision <decision_id> [--observed-at <ISO>] [--details <json>] [--json]",
101917
+ "",
101918
+ "Report a post-hoc observed outcome for a past decision_id to the CodeRifts cloud API.",
101919
+ "This records the CALLER'S assertion (e.g. your deploy job knows success/failure) \u2014",
101920
+ "the command does NOT itself verify that a deploy succeeded or failed.",
101921
+ "",
101922
+ "kind (required, closed set):",
101923
+ ` ${OUTCOME_KINDS.join(", ")}`,
101924
+ "",
101925
+ "Requires a cloud API key (coderifts login or CODERIFTS_API_KEY).",
101926
+ "POST /api/v1/outcomes \u2014 source is always reported (server-side); reporter is derived from the key."
101927
+ ].join("\n");
101928
+ function isValidOutcomeKind(kind) {
101929
+ return typeof kind === "string" && OUTCOME_KIND_SET.has(kind);
101652
101930
  }
101653
- async function runClaudeHook(options = {}, deps = {}) {
101654
- const errLog = deps.errLog || ((m) => console.error(String(m)));
101655
- const readStdin = deps.readStdin || (() => {
101656
- try {
101657
- return fs.readFileSync(0, "utf8");
101658
- } catch {
101659
- return "";
101660
- }
101661
- });
101662
- const readFile = deps.readFile || ((p) => fs.readFileSync(p, "utf8"));
101663
- const exists = deps.exists || ((p) => fs.existsSync(p));
101664
- const authorize = deps.cloudAuthorizePreflight || cloudAuthorizePreflight;
101665
- const cwd = deps.cwd || process.cwd();
101666
- const env = deps.env || process.env;
101667
- const strict = isStrictMode(env);
101668
- const advisory = isAdvisoryMode(env);
101669
- const sessionId = hookSessionId(env);
101670
- const logEv = (partial) => {
101671
- appendGuardEventLog(env, {
101672
- at: (/* @__PURE__ */ new Date()).toISOString(),
101673
- sessionId,
101674
- trigger_source: HOOK_TRIGGER_SOURCE,
101675
- ...partial
101676
- }, { errLog, appendFileSync: deps.appendFileSync });
101677
- };
101678
- let tool = null;
101679
- let filePathKnown = null;
101680
- const emitTerminal = (r) => {
101681
- const extra = {
101682
- tool: tool || void 0,
101683
- path: filePathKnown || void 0,
101684
- decision: r.decision != null ? r.decision : void 0,
101685
- decisionId: r.decisionId != null ? r.decisionId : void 0
101686
- };
101687
- if (r.exitCode === 2) {
101688
- const reasons = Array.isArray(r.reasons) && r.reasons.length ? r.reasons : void 0;
101689
- logEv({
101690
- type: "hook_blocked",
101691
- exit: 2,
101692
- ...extra,
101693
- reasons,
101694
- cause: r.reason || r.site
101695
- });
101696
- } else if (r.exitCode === 0 && r.site && advisory && !strict) {
101697
- logEv({ type: "hook_advisory_passthrough", exit: 0, decision: r.decision || null, ...extra, cause: r.site });
101698
- } else {
101699
- logEv({ type: "hook_continue", exit: 0, ...extra, cause: r.reason });
101931
+ function isValidObservedAt(iso) {
101932
+ if (typeof iso !== "string" || !iso.trim()) return false;
101933
+ const t = Date.parse(iso);
101934
+ return Number.isFinite(t);
101935
+ }
101936
+ function parseDetails(raw) {
101937
+ if (raw == null || raw === "") return { ok: true, details: null };
101938
+ if (typeof raw !== "string") return { ok: false, error: "--details must be a JSON string" };
101939
+ try {
101940
+ const v = JSON.parse(raw);
101941
+ if (v !== null && typeof v !== "object") {
101942
+ return { ok: false, error: "--details must be a JSON object or array" };
101700
101943
  }
101701
- return r;
101702
- };
101703
- const apiKey = resolveApiKey({ ...deps, cwd, env });
101704
- if (!apiKey) {
101705
- logEv({ type: "preflight_unavailable", cause: "missing_api_key" });
101706
- return emitTerminal(failClosedOrAdvisory({
101707
- advisory,
101708
- strict,
101709
- site: "missing_api_key",
101710
- why: "no API key",
101711
- softMsg: "CodeRifts claude-hook: no API key (coderifts login / CODERIFTS_API_KEY / git config coderifts.apiKey) \u2014 allowing",
101712
- errLog
101713
- }));
101944
+ return { ok: true, details: v };
101945
+ } catch (e) {
101946
+ return { ok: false, error: `--details is not valid JSON: ${e && e.message || "parse error"}` };
101714
101947
  }
101715
- const PARSE_GAP_STDERR = "unparseable input \u2014 refusing (fail-closed); set CODERIFTS_ADVISORY=1 to soften";
101716
- const raw = typeof options.stdin === "string" ? options.stdin : readStdin();
101717
- const parsed = parseStdinJson(raw);
101718
- if (!parsed.ok) {
101719
- if (advisory && !strict) {
101720
- errLog(`CodeRifts claude-hook: ${parsed.reason} \u2014 allowing (CODERIFTS_ADVISORY)`);
101721
- return emitTerminal({ exitCode: 0, reason: "stdin_unparseable", site: "stdin_unparseable" });
101722
- }
101723
- errLog(`CodeRifts claude-hook: ${PARSE_GAP_STDERR}`);
101724
- return emitTerminal({ exitCode: 2, reason: "stdin_unparseable" });
101948
+ }
101949
+ async function runOutcome(kind, options = {}, deps = {}) {
101950
+ const getKey = deps.getApiKey || getApiKey;
101951
+ const postOutcome = deps.cloudPostOutcome || cloudPostOutcome;
101952
+ const log = deps.log || console.log;
101953
+ const errLog = deps.errLog || console.error;
101954
+ const nowIso = deps.nowIso || (() => (/* @__PURE__ */ new Date()).toISOString());
101955
+ if (kind == null || String(kind).trim() === "") {
101956
+ errLog(chalk.red("Error: missing outcome kind"));
101957
+ errLog(USAGE);
101958
+ return { exitCode: 1, error: "missing_kind" };
101725
101959
  }
101726
- const { toolName, toolInput } = parsed;
101727
- tool = toolName;
101728
- const filePath = toolInput.file_path || toolInput.filePath || toolInput.path;
101729
- if (!filePath || typeof filePath !== "string") {
101730
- if (advisory && !strict) {
101731
- errLog("CodeRifts claude-hook: tool_input.file_path missing \u2014 allowing (CODERIFTS_ADVISORY)");
101732
- return emitTerminal({ exitCode: 0, reason: "missing_file_path", site: "missing_file_path" });
101733
- }
101734
- errLog(`CodeRifts claude-hook: ${PARSE_GAP_STDERR}`);
101735
- return emitTerminal({ exitCode: 2, reason: "missing_file_path" });
101960
+ const outcomeKind = String(kind).trim();
101961
+ if (!isValidOutcomeKind(outcomeKind)) {
101962
+ errLog(chalk.red(`Error: invalid outcome kind '${outcomeKind}'`));
101963
+ errLog(chalk.dim(` Valid kinds: ${OUTCOME_KINDS.join(", ")}`));
101964
+ return { exitCode: 1, error: "invalid_kind" };
101736
101965
  }
101737
- filePathKnown = filePath;
101738
- const specPath = resolveSpecPath({ ...deps, cwd });
101739
- if (!isSpecPath(filePath, specPath)) {
101740
- logEv({ type: "detection_skip", signals: ["not_spec_path"], tool, path: filePath });
101741
- return emitTerminal({ exitCode: 0, reason: "not_spec_path" });
101966
+ const decisionId = options.decision != null ? String(options.decision).trim() : "";
101967
+ if (!decisionId) {
101968
+ errLog(chalk.red("Error: --decision <decision_id> is required"));
101969
+ errLog(USAGE);
101970
+ return { exitCode: 1, error: "missing_decision" };
101742
101971
  }
101743
- const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
101744
- let diskBefore = "";
101745
- if (exists(absPath)) {
101746
- try {
101747
- diskBefore = readFile(absPath, "utf8");
101748
- } catch (e) {
101749
- logEv({ type: "preflight_unavailable", cause: "disk_unreadable", tool, path: filePath });
101750
- return emitTerminal(failClosedOrAdvisory({
101751
- advisory,
101752
- strict,
101753
- site: "disk_unreadable",
101754
- why: `cannot read contract file ${absPath}`,
101755
- softMsg: `CodeRifts claude-hook: cannot read ${absPath} \u2014 allowing (soft)`,
101756
- errLog
101757
- }));
101972
+ let observedAt;
101973
+ if (options.observedAt != null && String(options.observedAt).trim() !== "") {
101974
+ observedAt = String(options.observedAt).trim();
101975
+ if (!isValidObservedAt(observedAt)) {
101976
+ errLog(chalk.red("Error: --observed-at must be a valid ISO-8601 timestamp"));
101977
+ return { exitCode: 1, error: "invalid_observed_at" };
101758
101978
  }
101979
+ } else {
101980
+ observedAt = nowIso();
101759
101981
  }
101760
- const derived = deriveAfterContent(toolName, toolInput, diskBefore);
101761
- if (!derived.ok) {
101762
- logEv({ type: "preflight_unavailable", cause: "edit_apply_failed", tool, path: filePath });
101763
- return emitTerminal(failClosedOrAdvisory({
101764
- advisory,
101765
- strict,
101766
- site: "edit_apply_failed",
101767
- why: derived.reason,
101768
- softMsg: `CodeRifts claude-hook: ${derived.reason} \u2014 allowing (soft; never guess edit apply)`,
101769
- errLog
101770
- }));
101982
+ const det = parseDetails(options.details);
101983
+ if (!det.ok) {
101984
+ errLog(chalk.red(`Error: ${det.error}`));
101985
+ return { exitCode: 1, error: "invalid_details" };
101771
101986
  }
101772
- if (diskBefore === derived.after) {
101773
- logEv({ type: "detection_skip", signals: ["identical"], tool, path: filePath });
101774
- return emitTerminal({ exitCode: 0, reason: "identical" });
101987
+ const apiKey = getKey();
101988
+ if (!apiKey) {
101989
+ errLog(chalk.red("Error: no API key. Run `coderifts login` or set CODERIFTS_API_KEY."));
101990
+ return { exitCode: 1, error: "missing_api_key" };
101775
101991
  }
101776
- logEv({ type: "preflight_start", tool, path: filePath });
101777
- let result;
101992
+ const body = {
101993
+ decision_id: decisionId,
101994
+ outcome_kind: outcomeKind,
101995
+ observed_at: observedAt
101996
+ };
101997
+ if (det.details != null) body.details = det.details;
101998
+ let response;
101778
101999
  try {
101779
- result = await authorize(diskBefore, derived.after, apiKey, {
101780
- operation: "merge",
101781
- environment: "staging"
101782
- });
102000
+ response = await postOutcome(apiKey, body);
101783
102001
  } catch (e) {
101784
102002
  const msg = e && e.message ? String(e.message) : "request failed";
101785
- logEv({ type: "preflight_unavailable", cause: "api_unreachable", tool, path: filePath });
101786
- return emitTerminal(failClosedOrAdvisory({
101787
- advisory,
101788
- strict,
101789
- site: "api_unreachable",
101790
- why: `API unreachable: ${msg}`,
101791
- softMsg: `CodeRifts claude-hook: API unreachable (${msg}) \u2014 allowing (soft; availability must not brick the editor)`,
101792
- errLog
101793
- }));
101794
- }
101795
- const mapped = mapDecisionSeverity(result);
101796
- const fingerprint = extractFingerprint(result);
101797
- const idPart = mapped.decisionId ? ` decision_id=${mapped.decisionId}` : "";
101798
- const decPart = mapped.decision ? ` decision=${mapped.decision}` : "";
101799
- const eaPart = mapped.executionAction ? ` execution_action=${mapped.executionAction}` : "";
101800
- const actionForLog = mapped.executionAction || (mapped.severity === "BLOCK" || mapped.severity === "UNKNOWN" || mapped.severity === "INDETERMINATE" ? "STOP" : mapped.severity === "REQUIRE_APPROVAL" ? "REQUEST_APPROVAL" : mapped.severity === "MONITOR" || mapped.severity === "WARN" ? "CONTINUE_WITH_MONITORING" : "CONTINUE");
101801
- logEv({
101802
- type: "preflight_result",
101803
- action: actionForLog,
101804
- decisionId: mapped.decisionId || void 0,
101805
- fingerprint,
101806
- tool,
101807
- path: filePath
101808
- });
101809
- if (mapped.severity === "INDETERMINATE") {
101810
- errLog(
101811
- "CodeRifts claude-hook: BLOCKED (indeterminate response \u2014 no execution_action and no decision; not permission)"
101812
- );
101813
- return emitTerminal({
101814
- exitCode: 2,
101815
- reason: "indeterminate",
101816
- severity: "INDETERMINATE",
101817
- decision: mapped.decision,
101818
- decisionId: mapped.decisionId
101819
- });
101820
- }
101821
- if (mapped.severity === "BLOCK" || mapped.severity === "UNKNOWN") {
101822
- const why = renderDecisionWhy(result);
101823
- errLog(
101824
- [
101825
- `CodeRifts claude-hook: BLOCKED${decPart}${eaPart}${idPart}` + (mapped.severity === "UNKNOWN" ? " (unrecognised execution_action)" : ""),
101826
- ...why.lines
101827
- ].join("\n")
101828
- );
101829
- return emitTerminal({
101830
- exitCode: 2,
101831
- reason: mapped.severity === "UNKNOWN" ? "unknown_action" : "block",
101832
- severity: mapped.severity,
101833
- decision: mapped.decision,
101834
- decisionId: mapped.decisionId,
101835
- reasons: why.reasonCodes
101836
- });
101837
- }
101838
- if (mapped.severity === "REQUIRE_APPROVAL") {
101839
- const why = renderDecisionWhy(result);
101840
- errLog(
101841
- [
101842
- `CodeRifts claude-hook: BLOCKED approval_required${decPart}${eaPart}${idPart}`,
101843
- ...why.lines
101844
- ].join("\n")
101845
- );
101846
- return emitTerminal({
101847
- exitCode: 2,
101848
- reason: "approval_required",
101849
- severity: mapped.severity,
101850
- decision: mapped.decision,
101851
- decisionId: mapped.decisionId,
101852
- reasons: why.reasonCodes
101853
- });
102003
+ errLog(chalk.red(`Error: ${msg}`));
102004
+ if (e && e.statusCode) errLog(chalk.dim(` (HTTP ${e.statusCode})`));
102005
+ if (e && e.code) errLog(chalk.dim(` (${e.code})`));
102006
+ return { exitCode: 1, error: msg, body };
101854
102007
  }
101855
- if (mapped.severity === "MONITOR" || mapped.severity === "WARN") {
101856
- const sinkWired = isMonitoringSinkWired({ env, deps, result });
101857
- if (!sinkWired) {
101858
- errLog(
101859
- `CodeRifts claude-hook: BLOCKED monitoring_unwired${decPart}${eaPart}${idPart} \u2014 CONTINUE_WITH_MONITORING requires CODERIFTS_MONITORING_SINK_WIRED=1 (host assertion; not delivery proof)`
101860
- );
101861
- return emitTerminal({
101862
- exitCode: 2,
101863
- reason: "monitoring_unwired",
101864
- severity: mapped.severity === "WARN" ? "MONITOR" : mapped.severity,
101865
- decision: mapped.decision,
101866
- decisionId: mapped.decisionId
101867
- });
101868
- }
101869
- return emitTerminal({
101870
- exitCode: 0,
101871
- reason: "allow",
101872
- severity: mapped.severity === "WARN" ? "MONITOR" : mapped.severity,
101873
- decision: mapped.decision,
101874
- decisionId: mapped.decisionId
101875
- });
102008
+ if (options.json) {
102009
+ log(JSON.stringify(response, null, 2));
102010
+ } else {
102011
+ const o = response && response.outcome ? response.outcome : response;
102012
+ log(chalk.bold("CodeRifts outcome recorded"));
102013
+ log(` kind: ${outcomeKind}`);
102014
+ log(` decision_id: ${decisionId}`);
102015
+ log(` observed_at: ${observedAt}`);
102016
+ if (o && o.id) log(` id: ${o.id}`);
102017
+ if (response && response.source) log(` source: ${response.source}`);
102018
+ log(chalk.dim(" Caller assertion only \u2014 this command did not verify the deploy."));
101876
102019
  }
101877
- return emitTerminal({
101878
- exitCode: 0,
101879
- reason: "allow",
101880
- severity: "ALLOW",
101881
- decision: mapped.decision,
101882
- decisionId: mapped.decisionId
101883
- });
102020
+ return { exitCode: 0, response, body };
101884
102021
  }
101885
102022
  module2.exports = {
101886
- runClaudeHook,
101887
- parseStdinJson,
101888
- isSpecPath,
101889
- deriveAfterContent,
101890
- mapDecisionSeverity,
101891
- renderDecisionWhy,
101892
- resolveApiKey,
101893
- resolveSpecPath,
101894
- readGitConfig,
101895
- isEnvFlag,
101896
- isStrictMode,
101897
- isAdvisoryMode,
101898
- isMonitoringSinkWired,
101899
- failClosedOrAdvisory,
101900
- softOrStrictBlock,
101901
- DEFAULT_SPEC_PATH,
101902
- CLOSED_ACTIONS,
101903
- USAGE,
101904
- appendGuardEventLog,
101905
- extractFingerprint,
101906
- HOOK_TRIGGER_SOURCE
102023
+ runOutcome,
102024
+ isValidOutcomeKind,
102025
+ isValidObservedAt,
102026
+ parseDetails,
102027
+ OUTCOME_KINDS,
102028
+ USAGE
101907
102029
  };
101908
102030
  }
101909
102031
  });
@@ -103796,7 +103918,7 @@ program.command("diff <old-spec> <new-spec>").description("Compare two OpenAPI s
103796
103918
  const { diff } = require_diff();
103797
103919
  await diff(oldSpec, newSpec, options);
103798
103920
  });
103799
- program.command("deploy-gate").description("Gate a deploy on the current { environment, artifact } using a preflight receipt (phase-1 advisory)").option("--env <environment>", "Target environment (e.g. production, staging)").option("--artifact <artifact_id>", "Immutable artifact identity being deployed (content digest or commit SHA)").option("--receipt <file>", "Path to the deploy-scoped receipt JSON produced by preflight").option("--json", "Output the binding result as JSON").option("--enforce", "Treat the step as enforcing: attest ENFORCING and exit non-zero on a gate failure").action(async (options) => {
103921
+ program.command("deploy-gate").description("Gate a deploy on the current { environment, artifact } using a preflight receipt (fail-closed by default; CODERIFTS_DEPLOY_ADVISORY=1 to soften)").option("--env <environment>", "Target environment (e.g. production, staging)").option("--artifact <artifact_id>", "Immutable artifact identity being deployed (content digest or commit SHA)").option("--receipt <file>", "Path to the deploy-scoped receipt JSON produced by preflight").option("--json", "Output the binding result as JSON").option("--enforce", "Accepted silently (exit is fail-closed by default). Still attests ENFORCING for inescapable_deploy.").action(async (options) => {
103800
103922
  const { runDeployGate } = require_deploy_gate2();
103801
103923
  await runDeployGate(options);
103802
103924
  });