coderifts 8.2.0 → 8.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 8.3.0 - 2026-09-05
4
+
5
+ ### Changed
6
+ - `@coderifts/agent-guard` range raised to `^17.1.0`. Guard 17.1.0 fixes the audience binding (the Guard now sends top-level `audience`, which the server actually reads) and makes its public suite hermetic. No CLI behaviour change beyond the installed guard minor.
7
+
8
+
3
9
  ## 8.2.0 - 2026-09-05
4
10
 
5
11
  ### Changed
package/bin/coderifts.js CHANGED
@@ -52,6 +52,19 @@ program
52
52
  // Exit status is set only at this command boundary (library never process.exit).
53
53
  // program.parse() does not await async actions; process.exit here makes the gate's
54
54
  // fail-closed status authoritative for npm prepublishOnly / child_process callers.
55
+ // ── doctor (1222) — the adoption entry point ──
56
+ // Tier 0 needs no key and calls no model; Tier 1 is BYOK on the developer's own machine.
57
+ program
58
+ .command('doctor')
59
+ .description('Would an agent select CodeRifts for a contract change? Structural check (no key), or the 15-fixture benchmark on your own model.')
60
+ .option('--tools <file>', 'Your host\'s tools/list response (array, { tools }, or { result: { tools } })')
61
+ .option('--model <id>', 'TIER 1: run the fixtures through this model using YOUR key from the environment')
62
+ .option('--mock', 'TIER 1 dry run: a deterministic scripted model. Measures the script, not a model.')
63
+ .action(async (options) => {
64
+ const { runDoctor } = require('../src/commands/doctor');
65
+ await runDoctor(options);
66
+ });
67
+
55
68
  program
56
69
  .command('publish-gate')
57
70
  .description('Gate npm publish on contract-artifact preflight (before=git baseline, after=working tree)')
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "$comment": "GENERATED by scripts/build.js. sha256 over src/**.js + bin/**.js (path + bytes). test/build-freshness.test.js recomputes it; a mismatch means dist/cli.js is stale.",
3
- "sha256": "d9d48f2b01fa76855978dbbaa67d81be419b7632e1cc80eb5b999d5efcf9c090",
4
- "file_count": 42
3
+ "sha256": "9596f98c3c463ea53896621305484fd1ca3c5685b15c410fa4205c07a285c709",
4
+ "file_count": 44
5
5
  }
package/dist/cli.js CHANGED
@@ -3028,7 +3028,7 @@ var require_package = __commonJS({
3028
3028
  "package.json"(exports2, module2) {
3029
3029
  module2.exports = {
3030
3030
  name: "coderifts",
3031
- version: "8.2.0",
3031
+ version: "8.3.0",
3032
3032
  description: "Detect breaking API changes from the command line. Works locally or with the CodeRifts cloud API.",
3033
3033
  author: "CodeRifts <hello@coderifts.com>",
3034
3034
  license: "MIT",
@@ -3074,7 +3074,7 @@ var require_package = __commonJS({
3074
3074
  test: "node --test test/*.test.js"
3075
3075
  },
3076
3076
  dependencies: {
3077
- "@coderifts/agent-guard": "^17.0.0",
3077
+ "@coderifts/agent-guard": "^17.1.0",
3078
3078
  chalk: "^4.1.2",
3079
3079
  "cli-table3": "^0.6.4",
3080
3080
  commander: "^12.0.0",
@@ -71328,7 +71328,21 @@ These feed DIFFERENT halves of the system \u2014 the top level binds the ATOMIC
71328
71328
  ["target_uri", "targetUri"],
71329
71329
  ["tenant_id", "tenantId"],
71330
71330
  ["policy_hash", "policyHash"],
71331
- ["audience_hash", "audienceHash"],
71331
+ // 1402 — `audience_hash` IS NOT ON THIS LIST, and its absence is the fix.
71332
+ //
71333
+ // MEASURED 2026-09-06 against the server, read-only:
71334
+ // src/change-set.js:1138 reads TOP-LEVEL `input.audience`, strict /^v:[0-9a-f]{12}$/
71335
+ // execution-grant-v2.js:166 derives `audience_hash: sha256pref(audience)` ITSELF
71336
+ // execution-grant-request.v2.producer.json states it outright: "`audience_hash` is NOT a
71337
+ // request field of its own … sending it separately has no
71338
+ // effect and it stays out of this schema"
71339
+ //
71340
+ // So the Guard was sending a field nobody reads. Worse, it was ALSO sending the audience under
71341
+ // `context.audience`, which the handler does not read either (it reads `input.audience`) — both
71342
+ // channels were severed, and the binding a host configured reached the server through neither.
71343
+ // A field that looks bound and is not is the exact failure the server's own 1206 note describes.
71344
+ //
71345
+ // The fix is to send what the server consumes: top-level `audience`. See guard.ts.
71332
71346
  ["expected_state_token", "expectedStateToken"]
71333
71347
  ]);
71334
71348
  function v2WireFields(config) {
@@ -71872,6 +71886,17 @@ var require_guard = __commonJS({
71872
71886
  previous_receipt: resolvePreviousReceipt(config),
71873
71887
  idempotency_key: void 0
71874
71888
  };
71889
+ if (typeof config.audience === "string" && config.audience.length > 0) {
71890
+ if (/^v:[0-9a-f]{12}$/.test(config.audience)) {
71891
+ request.audience = config.audience;
71892
+ } else {
71893
+ emit(config, {
71894
+ type: "audience_not_bindable",
71895
+ at: iso(),
71896
+ cause: `audience ${JSON.stringify(config.audience)} is not the measured v:<12 hex> form; the server discards any other string to null, so it would NOT be bound. Nothing was sent for it.`
71897
+ });
71898
+ }
71899
+ }
71875
71900
  let grantObs;
71876
71901
  let grantForCall = null;
71877
71902
  if ((0, execution_grant_js_1.isExecutionGrantEnabled)(config)) {
@@ -230166,6 +230191,272 @@ var require_deploy_gate2 = __commonJS({
230166
230191
  }
230167
230192
  });
230168
230193
 
230194
+ // src/doctor.js
230195
+ var require_doctor = __commonJS({
230196
+ "src/doctor.js"(exports2, module2) {
230197
+ "use strict";
230198
+ var fs = require("fs");
230199
+ var path = require("path");
230200
+ function harness() {
230201
+ return require(path.join(__dirname, "..", "..", "..", "benchmark", "tool-selection-benchmark.js"));
230202
+ }
230203
+ var PREFLIGHT = "preflight_change_set";
230204
+ function bare(name) {
230205
+ return String(name == null ? "" : name).replace(/^coderifts\./, "");
230206
+ }
230207
+ function parseToolsDocument(doc) {
230208
+ if (Array.isArray(doc)) return { ok: true, tools: doc };
230209
+ if (doc && Array.isArray(doc.tools)) return { ok: true, tools: doc.tools };
230210
+ if (doc && doc.result && Array.isArray(doc.result.tools)) return { ok: true, tools: doc.result.tools };
230211
+ return {
230212
+ ok: false,
230213
+ why: "expected a tools/list response \u2014 an array, { tools: [...] }, or { result: { tools: [...] } }"
230214
+ };
230215
+ }
230216
+ function descriptionOf(t) {
230217
+ return typeof t.description === "string" ? t.description : "";
230218
+ }
230219
+ function schemaOf(t) {
230220
+ return t.inputSchema || t.input_schema || t.parameters || void 0;
230221
+ }
230222
+ function signalPreflightPresent(tools) {
230223
+ const hit = tools.find((t) => bare(t.name) === PREFLIGHT);
230224
+ return hit ? { ok: true, detail: `\`${bare(hit.name)}\` is in the table` } : {
230225
+ ok: false,
230226
+ detail: `no \`${PREFLIGHT}\` tool in the table \u2014 nothing to select for a contract change`,
230227
+ fix: "coderifts agent-setup"
230228
+ };
230229
+ }
230230
+ function signalDescriptionMatchesShipped(tools, shipped) {
230231
+ const hit = tools.find((t) => bare(t.name) === PREFLIGHT);
230232
+ if (!hit) return { ok: false, detail: "not applicable \u2014 no preflight tool", fix: "coderifts agent-setup" };
230233
+ const ship = shipped.find((t) => bare(t.name) === PREFLIGHT);
230234
+ if (!ship) return { ok: false, detail: "the shipped manifest carries no preflight tool to compare against" };
230235
+ const mine = descriptionOf(hit).trim();
230236
+ const theirs = descriptionOf(ship).trim();
230237
+ if (mine === theirs) return { ok: true, detail: "description is byte-identical to the shipped text" };
230238
+ const pct = theirs.length ? Math.round(mine.length / theirs.length * 100) : 0;
230239
+ return {
230240
+ ok: false,
230241
+ detail: `description DRIFTED from the shipped text (${mine.length} vs ${theirs.length} chars, ${pct}%) \u2014 the model reads this to decide, so a shortened or stale copy changes selection`,
230242
+ fix: "coderifts agent-setup --force"
230243
+ };
230244
+ }
230245
+ function signalSchemaValid(tools, h) {
230246
+ const checks = [];
230247
+ const attempt = (label, fn, arg) => {
230248
+ try {
230249
+ fn(arg);
230250
+ checks.push({ label, ok: true });
230251
+ } catch (err) {
230252
+ checks.push({ label, ok: false, why: err && err.message || "rejected" });
230253
+ }
230254
+ };
230255
+ const anthropicShape = tools.map((t) => ({ name: bare(t.name), description: descriptionOf(t), input_schema: schemaOf(t) }));
230256
+ if (typeof h.validateAnthropicTools === "function") attempt("anthropic", h.validateAnthropicTools, anthropicShape.map(h.toAnthropicTool || ((x) => x)));
230257
+ if (typeof h.validateOpenAITools === "function") attempt("openai", h.validateOpenAITools, anthropicShape.map(h.toOpenAITool || ((x) => x)));
230258
+ if (typeof h.validateGoogleTools === "function") attempt("google", h.validateGoogleTools, anthropicShape.map(h.toGoogleFunctionDeclaration || ((x) => x)));
230259
+ const bad = checks.filter((c) => !c.ok);
230260
+ return bad.length === 0 ? { ok: true, detail: `accepted by ${checks.map((c) => c.label).join(", ") || "(no validator available)"}` } : {
230261
+ ok: false,
230262
+ detail: `rejected by ${bad.map((c) => `${c.label} (${c.why})`).join("; ")}`,
230263
+ fix: "coderifts agent-setup --force"
230264
+ };
230265
+ }
230266
+ var RIVAL_HINTS = ["breaking", "openapi", "contract", "api diff", "api change", "schema diff"];
230267
+ function signalNoAbsorbingRival(tools, shipped = []) {
230268
+ const own = new Set(shipped.map((t) => bare(t.name)));
230269
+ const rivals = tools.filter((t) => bare(t.name) !== PREFLIGHT && !own.has(bare(t.name))).filter((t) => {
230270
+ const text = `${bare(t.name)} ${descriptionOf(t)}`.toLowerCase();
230271
+ return RIVAL_HINTS.some((h) => text.includes(h));
230272
+ }).map((t) => bare(t.name));
230273
+ return rivals.length === 0 ? { ok: true, detail: "no other tool advertises contract-change checking" } : {
230274
+ ok: false,
230275
+ warning: true,
230276
+ detail: `${rivals.length} other tool(s) advertise contract-change work and may absorb the call: ` + rivals.join(", ") + " \u2014 Tier 1 measures whether they actually do",
230277
+ fix: null
230278
+ };
230279
+ }
230280
+ function tier0(tools) {
230281
+ const h = harness();
230282
+ const shipped = h.getCanonicalTools();
230283
+ const signals = [
230284
+ { id: "preflight_present", ...signalPreflightPresent(tools) },
230285
+ { id: "description_matches_shipped", ...signalDescriptionMatchesShipped(tools, shipped) },
230286
+ { id: "schema_valid", ...signalSchemaValid(tools, h) },
230287
+ { id: "no_absorbing_rival", ...signalNoAbsorbingRival(tools, shipped) }
230288
+ ];
230289
+ const blocking = signals.filter((s) => !s.ok && !s.warning);
230290
+ return {
230291
+ tier: 0,
230292
+ tool_count: tools.length,
230293
+ signals,
230294
+ passed: signals.filter((s) => s.ok).length,
230295
+ total: signals.length,
230296
+ blocking: blocking.length,
230297
+ fixes: [...new Set(signals.filter((s) => !s.ok && s.fix).map((s) => s.fix))]
230298
+ };
230299
+ }
230300
+ var DOES_NOT_PROVE = Object.freeze({
230301
+ 0: [
230302
+ "that your model WOULD select CodeRifts \u2014 Tier 0 is structural only, a necessary condition",
230303
+ "that a change reached a merge/deploy/publish sink, or that any gate fired",
230304
+ "anything about tools this file was not shown"
230305
+ ],
230306
+ 1: [
230307
+ "that a change reached a merge/deploy/publish sink \u2014 selection is not enforcement",
230308
+ "anything about a model other than the one you ran, at the version you ran it",
230309
+ "that your production prompt behaves like the harness prompt"
230310
+ ]
230311
+ });
230312
+ module2.exports = {
230313
+ parseToolsDocument,
230314
+ tier0,
230315
+ harness,
230316
+ bare,
230317
+ DOES_NOT_PROVE,
230318
+ PREFLIGHT,
230319
+ signalPreflightPresent,
230320
+ signalDescriptionMatchesShipped,
230321
+ signalSchemaValid,
230322
+ signalNoAbsorbingRival
230323
+ };
230324
+ }
230325
+ });
230326
+
230327
+ // src/commands/doctor.js
230328
+ var require_doctor2 = __commonJS({
230329
+ "src/commands/doctor.js"(exports2, module2) {
230330
+ "use strict";
230331
+ var fs = require("fs");
230332
+ var path = require("path");
230333
+ var chalk = require_source();
230334
+ var {
230335
+ parseToolsDocument,
230336
+ tier0,
230337
+ harness,
230338
+ DOES_NOT_PROVE
230339
+ } = require_doctor();
230340
+ function readToolsFile(file) {
230341
+ const resolved = path.resolve(file);
230342
+ if (!fs.existsSync(resolved)) return { ok: false, why: `no such file: ${file}` };
230343
+ let doc;
230344
+ try {
230345
+ doc = JSON.parse(fs.readFileSync(resolved, "utf8"));
230346
+ } catch (err) {
230347
+ return { ok: false, why: `not JSON: ${err && err.message || "parse error"}` };
230348
+ }
230349
+ return parseToolsDocument(doc);
230350
+ }
230351
+ function printBoundary(tier, log) {
230352
+ log("");
230353
+ log(chalk.bold(" WHAT THIS DOES NOT PROVE"));
230354
+ for (const line of DOES_NOT_PROVE[tier]) log(chalk.dim(` \xB7 ${line}`));
230355
+ }
230356
+ function renderTier0(r, log) {
230357
+ log("");
230358
+ log(chalk.bold(` coderifts doctor \u2014 structural fitness (${r.tool_count} tools, no model called)`));
230359
+ log("");
230360
+ for (const s of r.signals) {
230361
+ const mark = s.ok ? chalk.green(" ok ") : s.warning ? chalk.yellow(" !! ") : chalk.red(" X ");
230362
+ log(`${mark}${s.id}`);
230363
+ log(chalk.dim(` ${s.detail}`));
230364
+ }
230365
+ log("");
230366
+ const verdict = r.blocking === 0 ? chalk.green(` structurally fit to be selected: ${r.passed}/${r.total} signals`) : chalk.red(` NOT structurally fit: ${r.blocking} blocking signal(s), ${r.passed}/${r.total} passed`);
230367
+ log(verdict);
230368
+ if (r.fixes.length) {
230369
+ log("");
230370
+ log(chalk.bold(" NEXT COMMAND"));
230371
+ for (const f of r.fixes) log(` ${chalk.cyan(f)}`);
230372
+ }
230373
+ log("");
230374
+ log(chalk.dim(" Tier 0 is the NECESSARY condition, not selectability. To measure whether your"));
230375
+ log(chalk.dim(" model actually selects, run the 15 fixtures on your own key:"));
230376
+ log(chalk.dim(" coderifts doctor --tools <file> --model <model-id>"));
230377
+ }
230378
+ async function runTier1({ tools, model, log, mock }) {
230379
+ const h = harness();
230380
+ const fixtures = h.loadSelectionFixtures();
230381
+ log("");
230382
+ log(chalk.bold(` coderifts doctor \u2014 selection benchmark (${fixtures.length} fixtures, model ${model})`));
230383
+ let client;
230384
+ if (mock) {
230385
+ client = new h.MockClient(h.perfectScript());
230386
+ log(chalk.yellow(" MOCK RUN \u2014 a scripted model that answers perfectly by construction."));
230387
+ log(chalk.yellow(" This measures the script. It is not a measurement of any model."));
230388
+ } else {
230389
+ const provider = h.PROVIDERS && Object.keys(h.PROVIDERS).find((p) => String(model).toLowerCase().includes(p));
230390
+ const factory = provider === "openai" ? h.createOpenAIClient : provider === "google" ? h.createGoogleClient : provider === "xai" ? h.createXaiClient : h.createAnthropicClient;
230391
+ const envVar = provider === "openai" ? "OPENAI_API_KEY" : provider === "google" ? "GOOGLE_API_KEY" : provider === "xai" ? "XAI_API_KEY" : "ANTHROPIC_API_KEY";
230392
+ const key = process.env[envVar];
230393
+ if (!key) {
230394
+ log("");
230395
+ log(chalk.red(` ${envVar} is not set.`));
230396
+ log(chalk.dim(" Tier 1 runs on YOUR key, on YOUR machine. CodeRifts never sees it, and"));
230397
+ log(chalk.dim(" never pays for the run. Export the key and re-run, or use --mock for a"));
230398
+ log(chalk.dim(" no-cost dry run of the harness path."));
230399
+ return { ok: false, reason: "missing_key" };
230400
+ }
230401
+ client = factory({ apiKey: key, model });
230402
+ }
230403
+ const userNames = new Set(tools.map((t) => String(t.name || "").replace(/^coderifts\./, "")));
230404
+ const withUserTable = fixtures.map((f) => ({
230405
+ ...f,
230406
+ available_tools: f.available_tools.filter((n) => userNames.has(String(n).replace(/^coderifts\./, "")))
230407
+ }));
230408
+ const unreachable = withUserTable.filter((f) => f.available_tools.length === 0).length;
230409
+ if (unreachable) {
230410
+ log(chalk.yellow(` ${unreachable} fixture(s) have no matching tool in your table \u2014 they can only score as a miss.`));
230411
+ }
230412
+ const unanswerable = withUserTable.filter((f) => {
230413
+ const want = String(f.expected_tool || "").replace(/^coderifts\./, "");
230414
+ return want && want !== "none" && !userNames.has(want);
230415
+ }).map((f) => f.id);
230416
+ if (unanswerable.length) {
230417
+ log(chalk.yellow(` ${unanswerable.length} fixture(s) expect a tool your table does not offer (${unanswerable.join(", ")}) \u2014 they CANNOT score correct. That is your table, not your model.`));
230418
+ }
230419
+ const run = await h.runBenchmark({ modelId: model, client, fixtures: withUserTable });
230420
+ const m = run && run.metrics ? run.metrics : h.computeMetrics(run.results || []);
230421
+ log("");
230422
+ for (const k of ["selection_coverage", "bypass_rate", "omission_rate", "wrong_tool_rate", "overall_accuracy"]) {
230423
+ if (m[k] !== void 0) log(` ${String(k).padEnd(20)} ${m[k]}`);
230424
+ }
230425
+ return { ok: true, metrics: m };
230426
+ }
230427
+ async function runDoctor(options = {}) {
230428
+ const log = options.logFn || ((s) => process.stdout.write(`${s}
230429
+ `));
230430
+ const exitFn = options.exitFn || ((c) => process.exit(c));
230431
+ if (!options.tools) {
230432
+ log(chalk.red(" --tools <file> is required: the tools/list your host actually serves."));
230433
+ log(chalk.dim(` Save it with your MCP client, or: curl .../mcp -d '{"method":"tools/list"}' > tools.json`));
230434
+ exitFn(2);
230435
+ return { exitCode: 2 };
230436
+ }
230437
+ const parsed = readToolsFile(options.tools);
230438
+ if (!parsed.ok) {
230439
+ log(chalk.red(` could not read the tool table: ${parsed.why}`));
230440
+ exitFn(2);
230441
+ return { exitCode: 2 };
230442
+ }
230443
+ if (options.model) {
230444
+ const r = await runTier1({ tools: parsed.tools, model: options.model, log, mock: !!options.mock });
230445
+ printBoundary(1, log);
230446
+ const code = r.ok ? 0 : 1;
230447
+ exitFn(code);
230448
+ return { exitCode: code, tier: 1, metrics: r.metrics };
230449
+ }
230450
+ const report = tier0(parsed.tools);
230451
+ renderTier0(report, log);
230452
+ printBoundary(0, log);
230453
+ exitFn(0);
230454
+ return { exitCode: 0, tier: 0, report };
230455
+ }
230456
+ module2.exports = { runDoctor, readToolsFile, renderTier0, printBoundary };
230457
+ }
230458
+ });
230459
+
230169
230460
  // src/commands/publish-gate.js
230170
230461
  var require_publish_gate = __commonJS({
230171
230462
  "src/commands/publish-gate.js"(exports2, module2) {
@@ -264666,6 +264957,10 @@ program.command("deploy-gate").description("Gate a deploy on the current { envir
264666
264957
  const { runDeployGate } = require_deploy_gate2();
264667
264958
  await runDeployGate(options);
264668
264959
  });
264960
+ program.command("doctor").description("Would an agent select CodeRifts for a contract change? Structural check (no key), or the 15-fixture benchmark on your own model.").option("--tools <file>", "Your host's tools/list response (array, { tools }, or { result: { tools } })").option("--model <id>", "TIER 1: run the fixtures through this model using YOUR key from the environment").option("--mock", "TIER 1 dry run: a deterministic scripted model. Measures the script, not a model.").action(async (options) => {
264961
+ const { runDoctor } = require_doctor2();
264962
+ await runDoctor(options);
264963
+ });
264669
264964
  program.command("publish-gate").description("Gate npm publish on contract-artifact preflight (before=git baseline, after=working tree)").option("--spec <path>", "Contract artifact path (default: git config coderifts.specPath or api/openapi.yaml)").option("--json", "Machine-readable JSON result").action(async (options) => {
264670
264965
  const { runPublishGate } = require_publish_gate();
264671
264966
  const result = await runPublishGate(options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderifts",
3
- "version": "8.2.0",
3
+ "version": "8.3.0",
4
4
  "description": "Detect breaking API changes from the command line. Works locally or with the CodeRifts cloud API.",
5
5
  "author": "CodeRifts <hello@coderifts.com>",
6
6
  "license": "MIT",
@@ -46,7 +46,7 @@
46
46
  "test": "node --test test/*.test.js"
47
47
  },
48
48
  "dependencies": {
49
- "@coderifts/agent-guard": "^17.0.0",
49
+ "@coderifts/agent-guard": "^17.1.0",
50
50
  "chalk": "^4.1.2",
51
51
  "cli-table3": "^0.6.4",
52
52
  "commander": "^12.0.0",