coderifts 4.2.1 → 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.1",
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,1137 +71780,1850 @@ 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;
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" : "");
71856
71874
  }
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
- };
71875
+ return fp.length > 16 ? `${fp.slice(0, 16)}\u2026` : fp;
71869
71876
  }
71870
- function clampExit(deployCheckStatus, enforce) {
71871
- if (deployCheckStatus === "success") return 0;
71872
- if (deployCheckStatus === "failure" && enforce === true) return 1;
71873
- return 0;
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");
71874
71887
  }
71875
- function readReceiptFile(filePath) {
71876
- if (!filePath) return null;
71877
- const resolved = path.resolve(filePath);
71878
- if (!fs.existsSync(resolved)) return null;
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;
71893
+ }
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" };
72013
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" };
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;
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 : {};
72136
72049
  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
- };
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)");
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
+ }));
72386
72208
  }
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));
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" });
72216
+ }
72217
+ errLog(`CodeRifts claude-hook: ${PARSE_GAP_STDERR}`);
72218
+ return emitTerminal({ exitCode: 2, reason: "stdin_unparseable" });
72392
72219
  }
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);
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" });
72420
72227
  }
72421
- return yaml.load(trimmed);
72422
- } catch (_) {
72423
- return null;
72228
+ errLog(`CodeRifts claude-hook: ${PARSE_GAP_STDERR}`);
72229
+ return emitTerminal({ exitCode: 2, reason: "missing_file_path" });
72424
72230
  }
72425
- }
72426
- function validateOpenApiSpec(parsed) {
72427
- if (!parsed || typeof parsed !== "object") {
72428
- return { valid: false, error: "Not a valid YAML or JSON object" };
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" });
72429
72236
  }
72430
- if (parsed.openapi) {
72431
- const ver = String(parsed.openapi);
72432
- if (ver.startsWith("3.")) {
72433
- return { valid: true, version: ver };
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
+ }));
72434
72252
  }
72435
- return { valid: false, error: `Unsupported OpenAPI version: ${ver}` };
72436
72253
  }
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}` };
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
+ }));
72443
72265
  }
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
- }
72460
- }
72266
+ if (diskBefore === derived.after) {
72267
+ logEv({ type: "detection_skip", signals: ["identical"], tool, path: filePath });
72268
+ return emitTerminal({ exitCode: 0, reason: "identical" });
72461
72269
  }
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 });
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
+ }));
72470
72288
  }
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
- }
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
+ });
72486
72314
  }
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
- }
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
+ });
72499
72331
  }
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
- }
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
+ });
72348
+ }
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
72361
+ });
72515
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
+ });
72516
72377
  }
72517
- return scopes;
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
+ });
72518
72392
  }
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;
72393
+ module2.exports = {
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
72417
+ };
72418
+ }
72419
+ });
72420
+
72421
+ // src/commands/deploy-gate.js
72422
+ var require_deploy_gate2 = __commonJS({
72423
+ "src/commands/deploy-gate.js"(exports2, module2) {
72424
+ "use strict";
72425
+ var fs = require("fs");
72426
+ var path = require("path");
72427
+ var chalk = require_source();
72428
+ var { deployGate } = require_cjs3();
72429
+ var { renderJson } = require_json2();
72430
+ var { renderDecisionWhy, isEnvFlag } = require_claude_hook();
72431
+ if (process.env.NO_COLOR) chalk.level = 0;
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";
72435
+ }
72436
+ function isDeployAdvisory(env) {
72437
+ const e = env || process.env;
72438
+ return isEnvFlag(e, "CODERIFTS_DEPLOY_ADVISORY") || isEnvFlag(e, "CODERIFTS_ADVISORY");
72439
+ }
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");
72524
72462
  }
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
- }
72463
+ if (change_set_rebound !== true) {
72464
+ out.push("change_set_not_rebound");
72531
72465
  }
72532
- return refs;
72466
+ return out;
72533
72467
  }
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];
72541
- }
72542
- return current !== void 0;
72468
+ function deployCoverageInput(enforcement_state, inescapable_deploy) {
72469
+ return { enforcement_state, inescapable_deploy: inescapable_deploy === true, applicability_attested: true };
72543
72470
  }
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 } };
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) {
72474
+ return {
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)
72482
+ };
72547
72483
  }
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;
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)
72560
72490
  }
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;
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;
72570
72497
  }
72571
- parsedSpecs.push({ name, parsed });
72498
+ if (expected_body_hash != null) requiredContext.expected_body_hash = expected_body_hash;
72572
72499
  }
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
- }
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;
72503
+ return {
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)
72511
+ };
72512
+ }
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);
72581
72527
  }
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
72590
- });
72591
- }
72528
+ if (bind.must_re_preflight) {
72529
+ lines.push("- action: re-preflight this { environment, artifact } \u2014 the receipt does not authorize it.");
72592
72530
  }
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 });
72531
+ lines.push(advisory ? ADVISORY_LABEL : SOFTEN_HINT);
72532
+ return lines.join("\n");
72533
+ }
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 };
72599
72551
  }
72552
+ return { kind: "ok", receipt: parsed };
72553
+ } catch (_) {
72554
+ return { kind: "malformed", receipt: null };
72600
72555
  }
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
- }
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();
72614
72564
  }
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
- }
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();
72638
72568
  }
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
- }
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" };
72651
72613
  }
72652
- let totalEndpoints = 0;
72653
- let totalSchemas = 0;
72654
- for (const { parsed } of parsedSpecs) {
72655
- totalEndpoints += extractEndpoints(parsed).length;
72656
- totalSchemas += extractSchemaNames(parsed).length;
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" };
72657
72625
  }
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
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("");
72666
72654
  }
72667
- };
72655
+ }
72656
+ exitFn(code);
72657
+ return { exitCode: code, failure_class: failureClass };
72668
72658
  }
72669
72659
  module2.exports = {
72670
- validateRegistry,
72671
- safeParse,
72672
- validateOpenApiSpec,
72673
- // Exported for testing
72674
- extractEndpoints,
72675
- extractSchemaNames,
72676
- extractDefinedScopes,
72677
- extractUsedScopes,
72678
- extractRefs,
72679
- refResolves
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
72680
72674
  };
72681
72675
  }
72682
72676
  });
72683
72677
 
72684
- // src/commands/registry-gate.js
72685
- var require_registry_gate = __commonJS({
72686
- "src/commands/registry-gate.js"(exports2, module2) {
72678
+ // src/commands/publish-gate.js
72679
+ var require_publish_gate = __commonJS({
72680
+ "src/commands/publish-gate.js"(exports2, module2) {
72687
72681
  "use strict";
72688
72682
  var fs = require("fs");
72689
72683
  var path = require("path");
72684
+ var { execFileSync } = require("child_process");
72690
72685
  var chalk = require_source();
72691
- var { matchGlob } = require_cjs3();
72692
- var {
72693
- validateRegistry,
72694
- safeParse,
72695
- validateOpenApiSpec
72696
- } = require_registry_validation_core();
72686
+ var { getApiKey } = require_config();
72687
+ var { cloudDiff } = require_cloud();
72697
72688
  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();
72744
- }
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;
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();
72749
72704
  }
72750
- function readFileThreeState(absPath, readFileSync = fs.readFileSync.bind(fs)) {
72705
+ function gitShow(ref, filePath, { gitImpl = defaultGit, cwd } = {}) {
72751
72706
  try {
72752
- const content = readFileSync(absPath, "utf8");
72753
- return { ok: true, content: content == null ? "" : String(content) };
72707
+ const out = gitImpl(["show", `${ref}:${filePath}`], cwd);
72708
+ return { ok: true, content: out == null ? "" : String(out) };
72754
72709
  } catch (err) {
72755
- const msg = err && err.message ? String(err.message) : String(err);
72710
+ const msg = err && err.stderr ? String(err.stderr) : err && err.message || String(err);
72756
72711
  return {
72757
72712
  ok: false,
72758
- code: "GATE_ERROR",
72759
- message: `unreadable file: ${absPath} (${msg.slice(0, 200)})`,
72760
- path: absPath
72713
+ code: "GIT_ERROR",
72714
+ message: `git error reading ${ref}:${filePath}: ${msg.slice(0, 300)}`
72761
72715
  };
72762
72716
  }
72763
72717
  }
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;
72718
+ function readPackageVersion(cwd, readFile = fs.readFileSync) {
72719
+ const pkgPath = path.join(cwd || process.cwd(), "package.json");
72772
72720
  try {
72773
- candidates = walkSpecCandidates(rootDir, { readdirSync, statSync });
72774
- } catch (err) {
72775
- return {
72776
- ok: false,
72777
- code: err.code || "GATE_ERROR",
72778
- message: err.message || String(err),
72779
- path: err.path
72780
- };
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;
72781
72726
  }
72782
- if (glob) {
72783
- candidates = candidates.filter((abs) => matchGlob(glob, relPosix(rootDir, abs)));
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)");
72784
72766
  }
72785
- const specs = [];
72786
- let skippedNonSpec = 0;
72787
- for (const abs of candidates) {
72788
- const read = readFileThreeState(abs, readFileSync);
72789
- if (!read.ok) {
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) {
72790
72779
  return {
72791
72780
  ok: false,
72792
- code: "GATE_ERROR",
72793
- message: read.message,
72794
- path: read.path || abs
72781
+ code: shown.code || "GIT_ERROR",
72782
+ message: shown.message,
72783
+ tried
72795
72784
  };
72796
72785
  }
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;
72802
- }
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;
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
+ };
72811
72793
  }
72812
- specs.push({ name, spec: read.content });
72794
+ return {
72795
+ ok: true,
72796
+ content: shown.content,
72797
+ source: `merge-base:${base}@${mb.slice(0, 12)}`,
72798
+ tried
72799
+ };
72813
72800
  }
72814
72801
  return {
72815
- ok: true,
72816
- specs,
72817
- skippedNonSpec,
72818
- candidates: candidates.length,
72819
- rootDir
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
72820
72806
  };
72821
72807
  }
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;
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
+ };
72827
72837
  }
72828
- return false;
72829
72838
  }
72830
- function countBySeverity(issues) {
72831
- const c = { error: 0, warning: 0, info: 0 };
72832
- for (const i of issues || []) {
72833
- if (i.severity === "error") c.error += 1;
72834
- else if (i.severity === "warning") c.warning += 1;
72835
- else if (i.severity === "info") c.info += 1;
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
+ };
72836
72847
  }
72837
- return c;
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
+ };
72838
72901
  }
72839
- function severityColor(sev) {
72840
- if (sev === "error") return chalk.red;
72841
- if (sev === "warning") return chalk.yellow;
72842
- return chalk.cyan;
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;
72843
72913
  }
72844
- function printFindings(issues, log) {
72845
- for (const i of issues || []) {
72846
- const color = severityColor(i.severity);
72847
- const files = Array.isArray(i.specs) ? i.specs.join(", ") : "";
72848
- log(color(`[${i.severity}] ${i.type}`) + (files ? chalk.dim(` ${files}`) : ""));
72849
- log(` ${i.message}`);
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;
72850
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
+ };
72851
72957
  }
72852
- function runRegistryGate(options = {}, deps = {}) {
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;
72853
72964
  const log = deps.log || console.log.bind(console);
72854
72965
  const logErr = deps.logErr || console.error.bind(console);
72855
- const cwd = deps.cwd || process.cwd();
72856
- const dirArg = options.dir != null && options.dir !== "" ? options.dir : ".";
72857
- const rootDir = path.isAbsolute(dirArg) ? dirArg : path.resolve(cwd, dirArg);
72858
- const readdirSync = deps.readdirSync || fs.readdirSync.bind(fs);
72859
- const readFileSync = deps.readFileSync || fs.readFileSync.bind(fs);
72860
- const statSync = deps.statSync || fs.statSync.bind(fs);
72861
- const existsSync = deps.existsSync || fs.existsSync.bind(fs);
72862
- if (!existsSync(rootDir)) {
72863
- const msg = `CodeRifts registry-gate: GATE_ERROR \u2014 directory not found: ${rootDir}`;
72864
- logErr(chalk.red(msg));
72865
- return { ok: false, exitCode: 1, code: "GATE_ERROR", message: msg };
72966
+ let specPath = options.spec;
72967
+ if (!specPath) {
72968
+ try {
72969
+ specPath = gitImpl(["config", "coderifts.specPath"], cwd);
72970
+ } catch {
72971
+ specPath = "";
72972
+ }
72866
72973
  }
72867
- let st;
72974
+ if (!specPath) specPath = "api/openapi.yaml";
72868
72975
  try {
72869
- st = statSync(rootDir);
72976
+ gitImpl(["rev-parse", "--is-inside-work-tree"], cwd);
72870
72977
  } catch (err) {
72871
- const msg = `CodeRifts registry-gate: GATE_ERROR \u2014 cannot access ${rootDir}: ${err && err.message}`;
72872
- logErr(chalk.red(msg));
72873
- return { ok: false, exitCode: 1, code: "GATE_ERROR", message: msg };
72874
- }
72875
- if (!st.isDirectory()) {
72876
- const msg = `CodeRifts registry-gate: GATE_ERROR \u2014 not a directory: ${rootDir}`;
72978
+ const msg = "CodeRifts publish-gate: GIT_ERROR \u2014 not a git repository (or git unavailable). Cannot resolve before-spec without git. Fail-closed.";
72877
72979
  logErr(chalk.red(msg));
72878
- return { ok: false, exitCode: 1, code: "GATE_ERROR", message: 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 });
72879
72987
  }
72880
- const discovered = discoverRegistrySpecs(rootDir, {
72881
- glob: options.glob || null,
72882
- readdirSync,
72883
- readFileSync,
72884
- statSync
72988
+ const beforeRes = resolveBeforeSpec(specPath, {
72989
+ gitImpl,
72990
+ cwd,
72991
+ readFile,
72992
+ packageVersion: deps.packageVersion
72885
72993
  });
72886
- if (!discovered.ok) {
72887
- const msg = `CodeRifts registry-gate: ${discovered.code} \u2014 ${discovered.message}`;
72888
- logErr(chalk.red(msg));
72889
- return {
72994
+ if (!beforeRes.ok) {
72995
+ logErr(chalk.red(`CodeRifts publish-gate: ${beforeRes.code} \u2014 ${beforeRes.message}`));
72996
+ return finish({
72890
72997
  ok: false,
72891
72998
  exitCode: 1,
72892
- code: discovered.code || "GATE_ERROR",
72893
- message: discovered.message,
72894
- path: discovered.path
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
72895
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 });
72896
73033
  }
72897
- if (discovered.specs.length === 0) {
72898
- const msg = `CodeRifts registry-gate: REGISTRY_EMPTY \u2014 no OpenAPI/Swagger specs discovered (scanned ${discovered.candidates} yaml/json file(s), skipped non-spec: ${discovered.skippedNonSpec}). An admission gate guarding nothing must fail, not pass.`;
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}`;
72899
73042
  logErr(chalk.red(msg));
72900
- return {
73043
+ return finish({
72901
73044
  ok: false,
72902
73045
  exitCode: 1,
72903
- code: "REGISTRY_EMPTY",
73046
+ code: "PREFLIGHT_UNREACHABLE",
72904
73047
  message: msg,
72905
- skippedNonSpec: discovered.skippedNonSpec,
72906
- candidates: discovered.candidates
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"
72907
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 });
72908
73080
  }
72909
- const result = validateRegistry(discovered.specs);
72910
- const counts = countBySeverity(result.issues);
72911
- const mode = {
72912
- warnOnly: !!options.warnOnly,
72913
- errorsOnly: !!options.errorsOnly
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 || []) {
73546
+ if (i.severity === "error") c.error += 1;
73547
+ else if (i.severity === "warning") c.warning += 1;
73548
+ else if (i.severity === "info") c.info += 1;
73549
+ }
73550
+ return c;
73551
+ }
73552
+ function severityColor(sev) {
73553
+ if (sev === "error") return chalk.red;
73554
+ if (sev === "warning") return chalk.yellow;
73555
+ return chalk.cyan;
73556
+ }
73557
+ function printFindings(issues, log) {
73558
+ for (const i of issues || []) {
73559
+ const color = severityColor(i.severity);
73560
+ const files = Array.isArray(i.specs) ? i.specs.join(", ") : "";
73561
+ log(color(`[${i.severity}] ${i.type}`) + (files ? chalk.dim(` ${files}`) : ""));
73562
+ log(` ${i.message}`);
73563
+ }
73564
+ }
73565
+ function runRegistryGate(options = {}, deps = {}) {
73566
+ const log = deps.log || console.log.bind(console);
73567
+ const logErr = deps.logErr || console.error.bind(console);
73568
+ const cwd = deps.cwd || process.cwd();
73569
+ const dirArg = options.dir != null && options.dir !== "" ? options.dir : ".";
73570
+ const rootDir = path.isAbsolute(dirArg) ? dirArg : path.resolve(cwd, dirArg);
73571
+ const readdirSync = deps.readdirSync || fs.readdirSync.bind(fs);
73572
+ const readFileSync = deps.readFileSync || fs.readFileSync.bind(fs);
73573
+ const statSync = deps.statSync || fs.statSync.bind(fs);
73574
+ const existsSync = deps.existsSync || fs.existsSync.bind(fs);
73575
+ if (!existsSync(rootDir)) {
73576
+ const msg = `CodeRifts registry-gate: GATE_ERROR \u2014 directory not found: ${rootDir}`;
73577
+ logErr(chalk.red(msg));
73578
+ return { ok: false, exitCode: 1, code: "GATE_ERROR", message: msg };
73579
+ }
73580
+ let st;
73581
+ try {
73582
+ st = statSync(rootDir);
73583
+ } catch (err) {
73584
+ const msg = `CodeRifts registry-gate: GATE_ERROR \u2014 cannot access ${rootDir}: ${err && err.message}`;
73585
+ logErr(chalk.red(msg));
73586
+ return { ok: false, exitCode: 1, code: "GATE_ERROR", message: msg };
73587
+ }
73588
+ if (!st.isDirectory()) {
73589
+ const msg = `CodeRifts registry-gate: GATE_ERROR \u2014 not a directory: ${rootDir}`;
73590
+ logErr(chalk.red(msg));
73591
+ return { ok: false, exitCode: 1, code: "GATE_ERROR", message: msg };
73592
+ }
73593
+ const discovered = discoverRegistrySpecs(rootDir, {
73594
+ glob: options.glob || null,
73595
+ readdirSync,
73596
+ readFileSync,
73597
+ statSync
73598
+ });
73599
+ if (!discovered.ok) {
73600
+ const msg = `CodeRifts registry-gate: ${discovered.code} \u2014 ${discovered.message}`;
73601
+ logErr(chalk.red(msg));
73602
+ return {
73603
+ ok: false,
73604
+ exitCode: 1,
73605
+ code: discovered.code || "GATE_ERROR",
73606
+ message: discovered.message,
73607
+ path: discovered.path
73608
+ };
73609
+ }
73610
+ if (discovered.specs.length === 0) {
73611
+ const msg = `CodeRifts registry-gate: REGISTRY_EMPTY \u2014 no OpenAPI/Swagger specs discovered (scanned ${discovered.candidates} yaml/json file(s), skipped non-spec: ${discovered.skippedNonSpec}). An admission gate guarding nothing must fail, not pass.`;
73612
+ logErr(chalk.red(msg));
73613
+ return {
73614
+ ok: false,
73615
+ exitCode: 1,
73616
+ code: "REGISTRY_EMPTY",
73617
+ message: msg,
73618
+ skippedNonSpec: discovered.skippedNonSpec,
73619
+ candidates: discovered.candidates
73620
+ };
73621
+ }
73622
+ const result = validateRegistry(discovered.specs);
73623
+ const counts = countBySeverity(result.issues);
73624
+ const mode = {
73625
+ warnOnly: !!options.warnOnly,
73626
+ errorsOnly: !!options.errorsOnly
72914
73627
  };
72915
73628
  const fail = findingsFailGate(result.issues, mode);
72916
73629
  if (result.issues.length > 0) {
@@ -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 = [
@@ -101123,834 +101836,196 @@ var require_adopt = __commonJS({
101123
101836
  }
101124
101837
  }
101125
101838
  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 fingerprintPrefix(fp) {
101405
- if (typeof fp !== "string" || fp.length === 0) return void 0;
101406
- const colon = fp.indexOf(":");
101407
- if (colon > 0 && colon < fp.length - 1) {
101408
- const scheme = fp.slice(0, colon + 1);
101409
- const rest = fp.slice(colon + 1);
101410
- return scheme + rest.slice(0, 12) + (rest.length > 12 ? "\u2026" : "");
101411
- }
101412
- return fp.length > 16 ? `${fp.slice(0, 16)}\u2026` : fp;
101413
- }
101414
- function renderAllowProofBlock({ decision, executionAction, decisionId, fingerprint } = {}) {
101415
- const decPart = decision ? ` decision=${decision}` : "";
101416
- const eaPart = executionAction ? ` execution_action=${executionAction}` : "";
101417
- const idPart = decisionId ? ` decision_id=${decisionId}` : "";
101418
- const lines = [`CodeRifts claude-hook: AUTHORIZED${decPart}${eaPart}${idPart}`];
101419
- const pref = fingerprintPrefix(fingerprint);
101420
- if (pref) lines.push(`- receipt: ${pref}`);
101421
- lines.push("- Interpretation: authorized at verify time for the bound scope only \u2014 see Limits.");
101422
- lines.push("- authorized; execution/commit not proven by this hook");
101423
- return lines.join("\n");
101424
- }
101425
- function hookSessionId(env) {
101426
- const fromEnv = env && env.CODERIFTS_SESSION_ID && String(env.CODERIFTS_SESSION_ID).trim();
101427
- if (fromEnv) return fromEnv;
101428
- if (!hookSessionId._gen) hookSessionId._gen = `hook-${process.pid}-${Date.now()}`;
101429
- return hookSessionId._gen;
101430
- }
101431
- function appendGuardEventLog(env, event, deps = {}) {
101432
- const p = env && env.CODERIFTS_GUARD_EVENT_LOG;
101433
- if (p == null || String(p).trim() === "") return;
101434
- const errLog = deps.errLog || ((m) => console.error(String(m)));
101435
- const append = deps.appendFileSync || ((file, data) => fs.appendFileSync(file, data));
101436
- try {
101437
- append(String(p).trim(), JSON.stringify(event) + "\n");
101438
- } catch (err) {
101439
- try {
101440
- errLog(`CodeRifts claude-hook: CODERIFTS_GUARD_EVENT_LOG write failed (${err && err.message})`);
101441
- } catch {
101442
- }
101443
- }
101444
- }
101445
- function failClosedOrAdvisory({ advisory, strict, site, softMsg, why, errLog }) {
101446
- if (advisory && !strict) {
101447
- errLog(`${softMsg} (CODERIFTS_ADVISORY)`);
101448
- return { exitCode: 0, reason: site, site };
101449
- }
101450
- if (strict) {
101451
- errLog(
101452
- `CodeRifts claude-hook: CODERIFTS_STRICT: governance could not run (${why}) \u2014 blocking. Set a key / restore network, or unset CODERIFTS_STRICT.`
101453
- );
101454
- return { exitCode: 2, reason: "enforce_indeterminate", site, strictBlocked: true };
101455
- }
101456
- errLog(
101457
- `CodeRifts claude-hook: governance could not run (${why}) \u2014 blocking. Set CODERIFTS_ADVISORY=1 to allow without governance (explicit opt-out).`
101458
- );
101459
- return { exitCode: 2, reason: "enforce_indeterminate", site };
101460
- }
101461
- function softOrStrictBlock({ strict, reason, softMsg, why, errLog }) {
101462
- return failClosedOrAdvisory({
101463
- advisory: false,
101464
- strict: !!strict,
101465
- site: reason,
101466
- softMsg,
101467
- why,
101468
- errLog
101469
- });
101470
- }
101471
- function readGitConfig(key, deps = {}) {
101472
- const run = deps.execSync || execSync;
101473
- const cwd = deps.cwd || process.cwd();
101474
- try {
101475
- const v = run(`git config ${key}`, { encoding: "utf8", cwd, stdio: ["ignore", "pipe", "pipe"] });
101476
- const t = String(v || "").trim();
101477
- return t || null;
101478
- } catch {
101479
- return null;
101480
- }
101481
- }
101482
- function resolveApiKey(deps = {}) {
101483
- const getKey = deps.getApiKey || getApiKey;
101484
- const fromLogin = getKey();
101485
- if (fromLogin) return fromLogin;
101486
- const env = deps.env || process.env;
101487
- const fromEnv = env.CODERIFTS_API_KEY && String(env.CODERIFTS_API_KEY).trim();
101488
- if (fromEnv) return fromEnv;
101489
- return readGitConfig("coderifts.apiKey", deps);
101490
- }
101491
- function resolveSpecPath(deps = {}) {
101492
- const fromGit = readGitConfig("coderifts.specPath", deps);
101493
- if (fromGit) return fromGit;
101494
- return DEFAULT_SPEC_PATH;
101495
- }
101496
- function parseStdinJson(raw) {
101497
- if (raw == null || String(raw).trim() === "") {
101498
- return { ok: false, reason: "empty stdin" };
101499
- }
101500
- let obj;
101501
- try {
101502
- obj = JSON.parse(String(raw));
101503
- } catch {
101504
- return { ok: false, reason: "stdin is not JSON" };
101505
- }
101506
- if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
101507
- return { ok: false, reason: "stdin JSON is not an object" };
101508
- }
101509
- const toolName = obj.tool_name || obj.toolName || obj.name || null;
101510
- let toolInput = obj.tool_input || obj.toolInput || obj.input || null;
101511
- if (toolInput == null && obj.file_path) toolInput = obj;
101512
- if (!toolName || typeof toolName !== "string") {
101513
- return { ok: false, reason: "missing tool_name" };
101514
- }
101515
- if (!toolInput || typeof toolInput !== "object") {
101516
- return { ok: false, reason: "missing tool_input" };
101517
- }
101518
- return { ok: true, toolName: String(toolName), toolInput };
101519
- }
101520
- function isSpecPath(filePath, specPath) {
101521
- if (!filePath || !specPath) return false;
101522
- const fp = path.normalize(String(filePath).replace(/\\/g, "/"));
101523
- const sp = path.normalize(String(specPath).replace(/\\/g, "/"));
101524
- if (fp === sp) return true;
101525
- if (fp.endsWith("/" + sp) || fp.endsWith(sp)) return true;
101526
- const baseFp = path.basename(fp);
101527
- const baseSp = path.basename(sp);
101528
- if (baseFp === baseSp && (fp.endsWith(sp) || sp.endsWith(baseSp))) {
101529
- const tail = sp.split("/").filter(Boolean).join("/");
101530
- return fp.replace(/\\/g, "/").endsWith(tail);
101531
- }
101532
- return false;
101533
- }
101534
- function deriveAfterContent(toolName, toolInput, diskBefore) {
101535
- const name = String(toolName || "");
101536
- if (name === "Write" || name === "write") {
101537
- if (typeof toolInput.content !== "string") {
101538
- return { ok: false, reason: "Write tool_input.content missing or not a string" };
101539
- }
101540
- return { ok: true, after: toolInput.content };
101541
- }
101542
- if (name === "Edit" || name === "edit") {
101543
- const oldS = toolInput.old_string != null ? toolInput.old_string : toolInput.oldString;
101544
- const newS = toolInput.new_string != null ? toolInput.new_string : toolInput.newString;
101545
- if (typeof oldS !== "string" || typeof newS !== "string") {
101546
- return { ok: false, reason: "Edit tool_input.old_string/new_string missing" };
101547
- }
101548
- if (!diskBefore.includes(oldS)) {
101549
- return { ok: false, reason: "Edit old_string not found in disk content" };
101550
- }
101551
- return { ok: true, after: diskBefore.replace(oldS, newS) };
101552
- }
101553
- if (name === "MultiEdit" || name === "multi_edit" || name === "multiEdit") {
101554
- const edits = toolInput.edits || toolInput.Edits;
101555
- if (!Array.isArray(edits) || edits.length === 0) {
101556
- return { ok: false, reason: "MultiEdit tool_input.edits missing or empty" };
101557
- }
101558
- let cur = diskBefore;
101559
- for (let i = 0; i < edits.length; i++) {
101560
- const e = edits[i] || {};
101561
- const oldS = e.old_string != null ? e.old_string : e.oldString;
101562
- const newS = e.new_string != null ? e.new_string : e.newString;
101563
- if (typeof oldS !== "string" || typeof newS !== "string") {
101564
- return { ok: false, reason: `MultiEdit edits[${i}] missing old_string/new_string` };
101565
- }
101566
- if (!cur.includes(oldS)) {
101567
- return { ok: false, reason: `MultiEdit edits[${i}] old_string not found in content` };
101568
- }
101569
- cur = cur.replace(oldS, newS);
101570
- }
101571
- return { ok: true, after: cur };
101572
- }
101573
- return { ok: false, reason: `unsupported tool_name for content derive: ${name}` };
101574
- }
101575
- function isV2DecisionBody(d, dr) {
101576
- if (dr && typeof dr === "object") return true;
101577
- if (d.preflight_mode != null && d.preflight_mode !== "") return true;
101578
- const ver = d.decision_spec_version;
101579
- return typeof ver === "string" && ver.startsWith("2.");
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");
101580
101843
  }
101581
- function allowLegacyDecisionMap(d, dr) {
101582
- 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);
101583
101848
  }
101584
- function mapDecisionSeverity(result) {
101585
- const d = result && typeof result === "object" && !Array.isArray(result) ? result : {};
101586
- let ea = null;
101587
- const dr = d.decision_result;
101588
- if (dr && typeof dr === "object" && typeof dr.execution_action === "string" && dr.execution_action !== "") {
101589
- ea = dr.execution_action;
101590
- } else if (typeof d.execution_action === "string" && d.execution_action !== "") {
101591
- 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 };
101592
101857
  }
101593
- const hasDecision = d.omega_decision != null && d.omega_decision !== "" || d.decision != null && d.decision !== "" || dr && typeof dr === "object" && dr.execution_action;
101594
- if (d.error && !hasDecision && (ea == null || ea === "")) {
101595
- return {
101596
- severity: "INDETERMINATE",
101597
- decision: null,
101598
- decisionId: null,
101599
- executionAction: null
101600
- };
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 };
101601
101864
  }
101602
- let severity;
101603
- if (ea != null && ea !== "") {
101604
- if (!CLOSED_ACTIONS.has(ea)) {
101605
- severity = "UNKNOWN";
101606
- } else if (ea === "CONTINUE") {
101607
- severity = "ALLOW";
101608
- } else if (ea === "CONTINUE_WITH_MONITORING") {
101609
- severity = "MONITOR";
101610
- } else if (ea === "STOP") {
101611
- severity = "BLOCK";
101612
- } else {
101613
- severity = "REQUIRE_APPROVAL";
101614
- }
101615
- } else if (allowLegacyDecisionMap(d, dr)) {
101616
- const od = d.omega_decision || d.decision;
101617
- if (od == null || od === "") {
101618
- severity = "INDETERMINATE";
101619
- } else if (od === "BLOCK") severity = "BLOCK";
101620
- else if (od === "REQUIRE_APPROVAL") severity = "REQUIRE_APPROVAL";
101621
- else if (od === "WARN") severity = "WARN";
101622
- 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));
101623
101875
  } else {
101624
- severity = "INDETERMINATE";
101876
+ log(renderHuman(report, { commit_count: collected.commit_count }));
101625
101877
  }
101626
- const decision = dr && dr.decision || d.decision || d.omega_decision || null;
101627
- const decisionId = dr && dr.decision_id || d.decision_id || null;
101628
101878
  return {
101629
- severity,
101630
- decision: decision != null && decision !== "" ? String(decision) : null,
101631
- decisionId: decisionId != null ? String(decisionId) : null,
101632
- executionAction: ea
101879
+ ok: true,
101880
+ exitCode: 0,
101881
+ report,
101882
+ meta: { commit_count: collected.commit_count }
101633
101883
  };
101634
101884
  }
101635
- function renderDecisionWhy(result, opts = {}) {
101636
- const maxFixes = Number.isInteger(opts.maxFixes) ? opts.maxFixes : 5;
101637
- const maxLines = Number.isInteger(opts.maxLines) ? opts.maxLines : 13;
101638
- const d = result && typeof result === "object" && !Array.isArray(result) ? result : {};
101639
- const dr = d.decision_result && typeof d.decision_result === "object" ? d.decision_result : {};
101640
- const pick = (k) => dr[k] !== void 0 ? dr[k] : d[k];
101641
- const arr = (v) => Array.isArray(v) ? v : [];
101642
- const str = (v) => typeof v === "string" ? v.trim() : "";
101643
- const lines = [];
101644
- const reasonCodes = [];
101645
- for (const r of arr(pick("blocking_reasons"))) {
101646
- if (!r || typeof r !== "object") continue;
101647
- const code = str(r.code);
101648
- const message = str(r.message);
101649
- if (code) reasonCodes.push(code);
101650
- if (!code && !message) continue;
101651
- lines.push(`- ${code || "REASON"}${message ? `: ${message}` : ""}`);
101652
- }
101653
- for (const r of arr(pick("degraded_reasons"))) {
101654
- if (!r || typeof r !== "object") continue;
101655
- const code = str(r.code);
101656
- const message = str(r.message);
101657
- if (!code && !message) continue;
101658
- lines.push(`- degraded: ${[code, message].filter(Boolean).join(": ")}`);
101659
- }
101660
- const action = str(pick("required_action"));
101661
- if (action) lines.push(`- action: ${action}`);
101662
- const rt = pick("remediation_transaction");
101663
- const changes = arr(rt && typeof rt === "object" ? rt.required_changes : null).filter((c) => c && typeof c === "object");
101664
- const shown = changes.slice(0, maxFixes);
101665
- for (const c of shown) {
101666
- const target = str(c.target);
101667
- const instruction = str(c.instruction);
101668
- if (!instruction && !target) continue;
101669
- lines.push(`- fix${target ? ` (${target})` : ""}: ${instruction || str(c.precise_label)}`);
101670
- }
101671
- if (changes.length > shown.length) lines.push(`- fix: +${changes.length - shown.length} more`);
101672
- 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);
101673
101930
  }
101674
- async function runClaudeHook(options = {}, deps = {}) {
101675
- const errLog = deps.errLog || ((m) => console.error(String(m)));
101676
- const readStdin = deps.readStdin || (() => {
101677
- try {
101678
- return fs.readFileSync(0, "utf8");
101679
- } catch {
101680
- return "";
101681
- }
101682
- });
101683
- const readFile = deps.readFile || ((p) => fs.readFileSync(p, "utf8"));
101684
- const exists = deps.exists || ((p) => fs.existsSync(p));
101685
- const authorize = deps.cloudAuthorizePreflight || cloudAuthorizePreflight;
101686
- const cwd = deps.cwd || process.cwd();
101687
- const env = deps.env || process.env;
101688
- const strict = isStrictMode(env);
101689
- const advisory = isAdvisoryMode(env);
101690
- const sessionId = hookSessionId(env);
101691
- const logEv = (partial) => {
101692
- appendGuardEventLog(env, {
101693
- at: (/* @__PURE__ */ new Date()).toISOString(),
101694
- sessionId,
101695
- trigger_source: HOOK_TRIGGER_SOURCE,
101696
- ...partial
101697
- }, { errLog, appendFileSync: deps.appendFileSync });
101698
- };
101699
- let tool = null;
101700
- let filePathKnown = null;
101701
- const emitTerminal = (r) => {
101702
- const extra = {
101703
- tool: tool || void 0,
101704
- path: filePathKnown || void 0,
101705
- decision: r.decision != null ? r.decision : void 0,
101706
- decisionId: r.decisionId != null ? r.decisionId : void 0
101707
- };
101708
- if (r.exitCode === 2) {
101709
- const reasons = Array.isArray(r.reasons) && r.reasons.length ? r.reasons : void 0;
101710
- logEv({
101711
- type: "hook_blocked",
101712
- exit: 2,
101713
- ...extra,
101714
- reasons,
101715
- cause: r.reason || r.site
101716
- });
101717
- } else if (r.exitCode === 0 && r.site && advisory && !strict) {
101718
- logEv({ type: "hook_advisory_passthrough", exit: 0, decision: r.decision || null, ...extra, cause: r.site });
101719
- } else {
101720
- const proof = r.reason === "allow" ? {
101721
- decision_id: r.decisionId != null ? r.decisionId : extra.decisionId || null,
101722
- fp_prefix: fingerprintPrefix(r.fingerprint) || null
101723
- } : void 0;
101724
- logEv({
101725
- type: "hook_continue",
101726
- exit: 0,
101727
- ...extra,
101728
- cause: r.reason,
101729
- ...proof ? { proof } : {}
101730
- });
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" };
101731
101943
  }
101732
- return r;
101733
- };
101734
- const apiKey = resolveApiKey({ ...deps, cwd, env });
101735
- if (!apiKey) {
101736
- logEv({ type: "preflight_unavailable", cause: "missing_api_key" });
101737
- return emitTerminal(failClosedOrAdvisory({
101738
- advisory,
101739
- strict,
101740
- site: "missing_api_key",
101741
- why: "no API key",
101742
- softMsg: "CodeRifts claude-hook: no API key (coderifts login / CODERIFTS_API_KEY / git config coderifts.apiKey) \u2014 allowing",
101743
- errLog
101744
- }));
101944
+ return { ok: true, details: v };
101945
+ } catch (e) {
101946
+ return { ok: false, error: `--details is not valid JSON: ${e && e.message || "parse error"}` };
101745
101947
  }
101746
- const PARSE_GAP_STDERR = "unparseable input \u2014 refusing (fail-closed); set CODERIFTS_ADVISORY=1 to soften";
101747
- const raw = typeof options.stdin === "string" ? options.stdin : readStdin();
101748
- const parsed = parseStdinJson(raw);
101749
- if (!parsed.ok) {
101750
- if (advisory && !strict) {
101751
- errLog(`CodeRifts claude-hook: ${parsed.reason} \u2014 allowing (CODERIFTS_ADVISORY)`);
101752
- return emitTerminal({ exitCode: 0, reason: "stdin_unparseable", site: "stdin_unparseable" });
101753
- }
101754
- errLog(`CodeRifts claude-hook: ${PARSE_GAP_STDERR}`);
101755
- 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" };
101756
101959
  }
101757
- const { toolName, toolInput } = parsed;
101758
- tool = toolName;
101759
- const filePath = toolInput.file_path || toolInput.filePath || toolInput.path;
101760
- if (!filePath || typeof filePath !== "string") {
101761
- if (advisory && !strict) {
101762
- errLog("CodeRifts claude-hook: tool_input.file_path missing \u2014 allowing (CODERIFTS_ADVISORY)");
101763
- return emitTerminal({ exitCode: 0, reason: "missing_file_path", site: "missing_file_path" });
101764
- }
101765
- errLog(`CodeRifts claude-hook: ${PARSE_GAP_STDERR}`);
101766
- 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" };
101767
101965
  }
101768
- filePathKnown = filePath;
101769
- const specPath = resolveSpecPath({ ...deps, cwd });
101770
- if (!isSpecPath(filePath, specPath)) {
101771
- logEv({ type: "detection_skip", signals: ["not_spec_path"], tool, path: filePath });
101772
- 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" };
101773
101971
  }
101774
- const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
101775
- let diskBefore = "";
101776
- if (exists(absPath)) {
101777
- try {
101778
- diskBefore = readFile(absPath, "utf8");
101779
- } catch (e) {
101780
- logEv({ type: "preflight_unavailable", cause: "disk_unreadable", tool, path: filePath });
101781
- return emitTerminal(failClosedOrAdvisory({
101782
- advisory,
101783
- strict,
101784
- site: "disk_unreadable",
101785
- why: `cannot read contract file ${absPath}`,
101786
- softMsg: `CodeRifts claude-hook: cannot read ${absPath} \u2014 allowing (soft)`,
101787
- errLog
101788
- }));
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" };
101789
101978
  }
101979
+ } else {
101980
+ observedAt = nowIso();
101790
101981
  }
101791
- const derived = deriveAfterContent(toolName, toolInput, diskBefore);
101792
- if (!derived.ok) {
101793
- logEv({ type: "preflight_unavailable", cause: "edit_apply_failed", tool, path: filePath });
101794
- return emitTerminal(failClosedOrAdvisory({
101795
- advisory,
101796
- strict,
101797
- site: "edit_apply_failed",
101798
- why: derived.reason,
101799
- softMsg: `CodeRifts claude-hook: ${derived.reason} \u2014 allowing (soft; never guess edit apply)`,
101800
- errLog
101801
- }));
101982
+ const det = parseDetails(options.details);
101983
+ if (!det.ok) {
101984
+ errLog(chalk.red(`Error: ${det.error}`));
101985
+ return { exitCode: 1, error: "invalid_details" };
101802
101986
  }
101803
- if (diskBefore === derived.after) {
101804
- logEv({ type: "detection_skip", signals: ["identical"], tool, path: filePath });
101805
- 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" };
101806
101991
  }
101807
- logEv({ type: "preflight_start", tool, path: filePath });
101808
- 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;
101809
101999
  try {
101810
- result = await authorize(diskBefore, derived.after, apiKey, {
101811
- operation: "merge",
101812
- environment: "staging"
101813
- });
102000
+ response = await postOutcome(apiKey, body);
101814
102001
  } catch (e) {
101815
102002
  const msg = e && e.message ? String(e.message) : "request failed";
101816
- logEv({ type: "preflight_unavailable", cause: "api_unreachable", tool, path: filePath });
101817
- return emitTerminal(failClosedOrAdvisory({
101818
- advisory,
101819
- strict,
101820
- site: "api_unreachable",
101821
- why: `API unreachable: ${msg}`,
101822
- softMsg: `CodeRifts claude-hook: API unreachable (${msg}) \u2014 allowing (soft; availability must not brick the editor)`,
101823
- errLog
101824
- }));
101825
- }
101826
- const mapped = mapDecisionSeverity(result);
101827
- const fingerprint = extractFingerprint(result);
101828
- const idPart = mapped.decisionId ? ` decision_id=${mapped.decisionId}` : "";
101829
- const decPart = mapped.decision ? ` decision=${mapped.decision}` : "";
101830
- const eaPart = mapped.executionAction ? ` execution_action=${mapped.executionAction}` : "";
101831
- 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");
101832
- logEv({
101833
- type: "preflight_result",
101834
- action: actionForLog,
101835
- decisionId: mapped.decisionId || void 0,
101836
- fingerprint,
101837
- tool,
101838
- path: filePath
101839
- });
101840
- if (mapped.severity === "INDETERMINATE") {
101841
- errLog(
101842
- "CodeRifts claude-hook: BLOCKED (indeterminate response \u2014 no execution_action and no decision; not permission)"
101843
- );
101844
- return emitTerminal({
101845
- exitCode: 2,
101846
- reason: "indeterminate",
101847
- severity: "INDETERMINATE",
101848
- decision: mapped.decision,
101849
- decisionId: mapped.decisionId
101850
- });
101851
- }
101852
- if (mapped.severity === "BLOCK" || mapped.severity === "UNKNOWN") {
101853
- const why = renderDecisionWhy(result);
101854
- errLog(
101855
- [
101856
- `CodeRifts claude-hook: BLOCKED${decPart}${eaPart}${idPart}` + (mapped.severity === "UNKNOWN" ? " (unrecognised execution_action)" : ""),
101857
- ...why.lines
101858
- ].join("\n")
101859
- );
101860
- return emitTerminal({
101861
- exitCode: 2,
101862
- reason: mapped.severity === "UNKNOWN" ? "unknown_action" : "block",
101863
- severity: mapped.severity,
101864
- decision: mapped.decision,
101865
- decisionId: mapped.decisionId,
101866
- reasons: why.reasonCodes
101867
- });
101868
- }
101869
- if (mapped.severity === "REQUIRE_APPROVAL") {
101870
- const why = renderDecisionWhy(result);
101871
- errLog(
101872
- [
101873
- `CodeRifts claude-hook: BLOCKED approval_required${decPart}${eaPart}${idPart}`,
101874
- ...why.lines
101875
- ].join("\n")
101876
- );
101877
- return emitTerminal({
101878
- exitCode: 2,
101879
- reason: "approval_required",
101880
- severity: mapped.severity,
101881
- decision: mapped.decision,
101882
- decisionId: mapped.decisionId,
101883
- reasons: why.reasonCodes
101884
- });
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 };
101885
102007
  }
101886
- if (mapped.severity === "MONITOR" || mapped.severity === "WARN") {
101887
- const sinkWired = isMonitoringSinkWired({ env, deps, result });
101888
- if (!sinkWired) {
101889
- errLog(
101890
- `CodeRifts claude-hook: BLOCKED monitoring_unwired${decPart}${eaPart}${idPart} \u2014 CONTINUE_WITH_MONITORING requires CODERIFTS_MONITORING_SINK_WIRED=1 (host assertion; not delivery proof)`
101891
- );
101892
- return emitTerminal({
101893
- exitCode: 2,
101894
- reason: "monitoring_unwired",
101895
- severity: mapped.severity === "WARN" ? "MONITOR" : mapped.severity,
101896
- decision: mapped.decision,
101897
- decisionId: mapped.decisionId
101898
- });
101899
- }
101900
- errLog(renderAllowProofBlock({
101901
- decision: mapped.decision,
101902
- executionAction: mapped.executionAction,
101903
- decisionId: mapped.decisionId,
101904
- fingerprint
101905
- }));
101906
- return emitTerminal({
101907
- exitCode: 0,
101908
- reason: "allow",
101909
- severity: mapped.severity === "WARN" ? "MONITOR" : mapped.severity,
101910
- decision: mapped.decision,
101911
- decisionId: mapped.decisionId,
101912
- fingerprint
101913
- });
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."));
101914
102019
  }
101915
- errLog(renderAllowProofBlock({
101916
- decision: mapped.decision,
101917
- executionAction: mapped.executionAction,
101918
- decisionId: mapped.decisionId,
101919
- fingerprint
101920
- }));
101921
- return emitTerminal({
101922
- exitCode: 0,
101923
- reason: "allow",
101924
- severity: "ALLOW",
101925
- decision: mapped.decision,
101926
- decisionId: mapped.decisionId,
101927
- fingerprint
101928
- });
102020
+ return { exitCode: 0, response, body };
101929
102021
  }
101930
102022
  module2.exports = {
101931
- runClaudeHook,
101932
- parseStdinJson,
101933
- isSpecPath,
101934
- deriveAfterContent,
101935
- mapDecisionSeverity,
101936
- renderDecisionWhy,
101937
- resolveApiKey,
101938
- resolveSpecPath,
101939
- readGitConfig,
101940
- isEnvFlag,
101941
- isStrictMode,
101942
- isAdvisoryMode,
101943
- isMonitoringSinkWired,
101944
- failClosedOrAdvisory,
101945
- softOrStrictBlock,
101946
- DEFAULT_SPEC_PATH,
101947
- CLOSED_ACTIONS,
101948
- USAGE,
101949
- appendGuardEventLog,
101950
- extractFingerprint,
101951
- fingerprintPrefix,
101952
- renderAllowProofBlock,
101953
- HOOK_TRIGGER_SOURCE
102023
+ runOutcome,
102024
+ isValidOutcomeKind,
102025
+ isValidObservedAt,
102026
+ parseDetails,
102027
+ OUTCOME_KINDS,
102028
+ USAGE
101954
102029
  };
101955
102030
  }
101956
102031
  });
@@ -103843,7 +103918,7 @@ program.command("diff <old-spec> <new-spec>").description("Compare two OpenAPI s
103843
103918
  const { diff } = require_diff();
103844
103919
  await diff(oldSpec, newSpec, options);
103845
103920
  });
103846
- 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) => {
103847
103922
  const { runDeployGate } = require_deploy_gate2();
103848
103923
  await runDeployGate(options);
103849
103924
  });