coderifts 5.0.0 → 6.0.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,6 +1,34 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 6.0.0
4
+
5
+ _Nothing yet._
6
+
7
+ ## 5.0.1
8
+
9
+ ### Fixed
10
+ - **A fresh install shipped the guard with the conditional-write hole.**
11
+ `coderifts@5.0.0` declared `@coderifts/agent-guard: ^11.0.0`, which resolved to
12
+ 11.0.1 — precisely the tarball the guard's own 12.0.0 round reproduced the
13
+ defect against, where `requireConditionalWrite` did not fire on an ordinary
14
+ mutation and the side effect **ran** with `enforced: true`. The guard is a
15
+ **direct** dependency with no `peerDependencies` and no bundled copy, so this
16
+ range *is* the adopter's resolution, not advice. Now `^12.0.0`, with
17
+ `MIN_GUARD_MAJOR` raised to 12. Cost measured as zero: the CLI imports only
18
+ `deployGate`, `asVerifiedDeployReceiptView`, `DEPLOY_RECEIPT_VIEW_SPEC` and
19
+ `matchGlob`, whose modules are byte-identical 11.0.1 → 12.0.0.
20
+ - **Two defects in the publish gate itself**, both proven by replaying the real
21
+ script. The resolved check only ever compared *downward* — `resolved >= floor`,
22
+ never whether the resolved version sits **inside** the declared range — so
23
+ declaring `^11.0.0` with 12.0.0 staged for a cost measurement printed OK and
24
+ exited 0, which is exactly the shape that makes "we measured 12 and shipped
25
+ ^11" indistinguishable from a correct release. And an unresolvable install was
26
+ a **pass**. Three failure modes added (resolved below the declared range at
27
+ full precision, resolved above it, and not resolvable at all), with no
28
+ opt-out — an `allowUnresolved` flag was written and deliberately removed, and
29
+ a test asserts it stays absent.
30
+
31
+ ## 5.0.0
4
32
 
5
33
  ### Fixed
6
34
  - **P0 — `setup-required-check --apply` no longer reports `APPLIED` over a
@@ -59,6 +87,63 @@
59
87
  template is unchanged and stays non-blocking; it carries the same three lines
60
88
  commented, with a note saying why the two differ.
61
89
 
90
+ ## 4.13.0 — never published (folded into 5.0.0)
91
+
92
+ Bumped in-repo (`917611d`) but never tagged or released to npm; 5.0.0 superseded
93
+ it, so adopters received this content **as part of 5.0.0**. Recorded separately
94
+ because the version number appears in the source history and in
95
+ `assert-guard-major.js`'s floor-raise log.
96
+
97
+ ### Changed
98
+ - **Guard floor raised to 11.** Cost measured as zero the way the 10-raise was —
99
+ the CLI imports only `deployGate`, `asVerifiedDeployReceiptView`,
100
+ `DEPLOY_RECEIPT_VIEW_SPEC` and `matchGlob`, none of which 11.0.0 touched, and
101
+ the suite staged against 11 returned an identical 532/530/2 with a
102
+ byte-identical failure set. It rises anyway, because a semantics floor is about
103
+ what the CLI may **run** against, not what it happens to import today: 10.x's
104
+ `computeBundleFingerprint` disagreed with the server, which is exactly the
105
+ class this gate exists to refuse.
106
+ - The two tests pinning the old floor were updated, and the invariant made
107
+ explicit: **the floor only ever rises**.
108
+
109
+ ### Added
110
+ - `/api/v1/agent/preflight` returns `scorer_version` — the one field that makes
111
+ the single-spec fingerprint reproducible by a consumer holding their own
112
+ before/after/policy. Purely additive; no surface moves and `tools_sha256` is
113
+ unchanged.
114
+
115
+ ## 4.12.0
116
+
117
+ ### Changed
118
+ - **Guard floor raised to 10.** Cost measured as zero: the CLI imports only
119
+ `deployGate`, `asVerifiedDeployReceiptView`, `DEPLOY_RECEIPT_VIEW_SPEC` and
120
+ `matchGlob` — never `withCodeRifts` — so 10.0.0's strict-composition break
121
+ cannot reach it. Verified by staging guard 10 into the install and running the
122
+ suite (guard-conformance 6/6, the same two pre-existing failures as the guard-9
123
+ baseline). The gate was satisfied, not weakened: before the install it
124
+ correctly reported FAIL on the stale resolve.
125
+
126
+ ## 4.11.0
127
+
128
+ ### Fixed
129
+ - **P0 — the issuer binding pointed at a check that cannot block.** GitHub passes
130
+ a required status check on `success`, `skipped` **or `neutral`**, so our App's
131
+ advisory check genuinely cannot block; a false comment in
132
+ `webhook-integration.js` claiming otherwise is what produced the regression.
133
+ The enforcing issuer is `app_id 15368` (github-actions), which the contract-gate
134
+ Action posts under, so that binding is now VERIFIED — with the shared-identity
135
+ residual **named** rather than implied: anyone who can edit
136
+ `.github/workflows` can post under it. A binding to our own App id grades
137
+ UNVERIFIABLE rather than NOT_VERIFIED, because `MERGEGATE_ENFORCE` is
138
+ server-side and invisible from here.
139
+ - **P0 — the installer wrote what our own verifier rejects.** It now writes
140
+ `checks[]` with an explicit `app_id`, because an omitted `app_id` silently
141
+ resolves to whoever posted last, which is unauditable. It never re-binds
142
+ another team's existing check, and the success path prints what it does **not**
143
+ achieve. A coupling test asserts the installer's and the verifier's issuer
144
+ constants are equal. The verifier was not weakened — the name-only and
145
+ legacy-contexts paths are byte-identical, asserted by two unchanged tests.
146
+
62
147
  ## 4.10.0
63
148
 
64
149
  ### Added
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: "5.0.0",
3031
+ version: "6.0.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",
@@ -3067,13 +3067,13 @@ var require_package = __commonJS({
3067
3067
  },
3068
3068
  scripts: {
3069
3069
  build: "node scripts/copy-corpus.js && node scripts/build.js",
3070
- prepublishOnly: "node scripts/assert-guard-major.js && bash ../../scripts/freeze-gate.sh && npm run build",
3070
+ prepublishOnly: "node scripts/assert-guard-major.js && node scripts/assert-changelog-version.js && bash ../../scripts/freeze-gate.sh && npm run build",
3071
3071
  "assert-guard-major": "node scripts/assert-guard-major.js",
3072
3072
  postinstall: "node scripts/postinstall.js",
3073
3073
  test: "node --test test/*.test.js"
3074
3074
  },
3075
3075
  dependencies: {
3076
- "@coderifts/agent-guard": "^11.0.0",
3076
+ "@coderifts/agent-guard": "^12.0.0",
3077
3077
  chalk: "^4.1.2",
3078
3078
  "cli-table3": "^0.6.4",
3079
3079
  commander: "^12.0.0",
@@ -66357,7 +66357,7 @@ var require_execution_time_fingerprint = __commonJS({
66357
66357
  exports2.authorizedFingerprintFromEnvelope = authorizedFingerprintFromEnvelope;
66358
66358
  exports2.checkExecutionTimeFingerprint = checkExecutionTimeFingerprint;
66359
66359
  var node_crypto_1 = require("node:crypto");
66360
- var NUL = "";
66360
+ var US = "";
66361
66361
  var BUNDLE_FP_DOMAIN = "crbundle.v1";
66362
66362
  function sha256hex(s) {
66363
66363
  return (0, node_crypto_1.createHash)("sha256").update(s, "utf8").digest("hex");
@@ -66372,13 +66372,13 @@ var require_execution_time_fingerprint = __commonJS({
66372
66372
  }
66373
66373
  function computeCanonicalBundleFingerprint(artifacts, context) {
66374
66374
  const sorted = artifacts.slice().sort((x, y) => {
66375
- const kx = `${x.type}${NUL}${x.id}`;
66376
- const ky = `${y.type}${NUL}${y.id}`;
66375
+ const kx = `${x.type}${US}${x.id}`;
66376
+ const ky = `${y.type}${US}${y.id}`;
66377
66377
  return kx < ky ? -1 : kx > ky ? 1 : 0;
66378
66378
  });
66379
66379
  const parts = [BUNDLE_FP_DOMAIN, String(artifacts.length)];
66380
66380
  for (const a of sorted) {
66381
- parts.push([a.type, a.id, sha256hex(specStr(a.before)), sha256hex(specStr(a.after))].join(NUL));
66381
+ parts.push([a.type, a.id, sha256hex(specStr(a.before)), sha256hex(specStr(a.after))].join(US));
66382
66382
  }
66383
66383
  const ctx = context || {};
66384
66384
  parts.push([
@@ -66388,8 +66388,8 @@ var require_execution_time_fingerprint = __commonJS({
66388
66388
  scalar(ctx.branch),
66389
66389
  scalar(ctx.pull_request),
66390
66390
  scalar(ctx.policy_profile)
66391
- ].join(NUL));
66392
- return `sha256:${sha256hex(parts.join(NUL))}`;
66391
+ ].join(US));
66392
+ return `sha256:${sha256hex(parts.join(US))}`;
66393
66393
  }
66394
66394
  exports2.EXECUTION_TIME_FP_REASONS = Object.freeze({
66395
66395
  MATCH: "match",
@@ -66485,7 +66485,7 @@ var require_enforcement_gate = __commonJS({
66485
66485
  function isAction(v) {
66486
66486
  return v === "CONTINUE" || v === "CONTINUE_WITH_MONITORING" || v === "REQUEST_APPROVAL" || v === "STOP";
66487
66487
  }
66488
- var NUL = "";
66488
+ var US = "";
66489
66489
  function sha256hex(s) {
66490
66490
  return (0, node_crypto_1.createHash)("sha256").update(s).digest("hex");
66491
66491
  }
@@ -66493,7 +66493,7 @@ var require_enforcement_gate = __commonJS({
66493
66493
  return v == null ? "" : typeof v === "string" ? v : JSON.stringify(v);
66494
66494
  }
66495
66495
  function computeArtifactDigest(artifacts) {
66496
- const preimage = artifacts.slice().sort((a, b) => `${a.type}${NUL}${a.id}` < `${b.type}${NUL}${b.id}` ? -1 : 1).map((a) => `${sha256hex(specStr(a.before))}${sha256hex(specStr(a.after))}`).join(NUL);
66496
+ const preimage = artifacts.slice().sort((a, b) => `${a.type}${US}${a.id}` < `${b.type}${US}${b.id}` ? -1 : 1).map((a) => `${sha256hex(specStr(a.before))}${sha256hex(specStr(a.after))}`).join(US);
66497
66497
  return `sha256:${sha256hex(preimage)}`;
66498
66498
  }
66499
66499
  function computeBundleFingerprint(artifacts, context) {
@@ -66699,7 +66699,8 @@ var require_execution_proof = __commonJS({
66699
66699
  if (input.preflighted !== true || input.receiptVerified !== true) {
66700
66700
  throw new Error("@coderifts/agent-guard: enforced:true without receiptVerified+preflighted (runtime invariant)");
66701
66701
  }
66702
- if (input.requireConditionalWrite === true && input.writeStyle === true) {
66702
+ const mutating = input.mutating === void 0 ? input.writeStyle === true : input.mutating === true;
66703
+ if (input.requireConditionalWrite === true && mutating) {
66703
66704
  if (input.conditionalWrite !== true) {
66704
66705
  throw new Error("@coderifts/agent-guard: enforced:true without conditional_write:true under requireConditionalWrite (runtime invariant)");
66705
66706
  }
@@ -66763,6 +66764,7 @@ var require_execution_proof = __commonJS({
66763
66764
  receiptVerified,
66764
66765
  requireConditionalWrite: cwb?.require_conditional_write,
66765
66766
  writeStyle: cwb?.write_style,
66767
+ mutating: cwb?.mutating,
66766
66768
  conditionalWrite: cwb?.conditional_write
66767
66769
  });
66768
66770
  const env = envelopeOf(input.verdict);
@@ -66854,6 +66856,7 @@ var require_freshness = __commonJS({
66854
66856
  exports2.assessWriteStylePrior = assessWriteStylePrior;
66855
66857
  exports2.freshnessAllowsEnforce = freshnessAllowsEnforce;
66856
66858
  exports2.isWriteStyleCall = isWriteStyleCall;
66859
+ exports2.isMutatingCall = isMutatingCall;
66857
66860
  exports2.artifactIdsForResolve = artifactIdsForResolve;
66858
66861
  exports2.collectFreshnessCallContext = collectFreshnessCallContext;
66859
66862
  exports2.buildFreshnessBasis = buildFreshnessBasis;
@@ -66987,6 +66990,24 @@ var require_freshness = __commonJS({
66987
66990
  }
66988
66991
  return false;
66989
66992
  }
66993
+ function isMutatingCall(call) {
66994
+ const arts = call.artifacts;
66995
+ if (Array.isArray(arts)) {
66996
+ for (const a of arts) {
66997
+ if (a && typeof a.after === "string" && a.after.length > 0)
66998
+ return true;
66999
+ }
67000
+ }
67001
+ const args = call.arguments && typeof call.arguments === "object" ? call.arguments : {};
67002
+ for (const k of ["contents", "content", "new_string", "old_string", "patch"]) {
67003
+ const v = args[k];
67004
+ if (typeof v === "string" && v.length > 0)
67005
+ return true;
67006
+ }
67007
+ if (Array.isArray(args.edits) && args.edits.length > 0)
67008
+ return true;
67009
+ return false;
67010
+ }
66990
67011
  function artifactIdsForResolve(call) {
66991
67012
  const ids = [];
66992
67013
  if (Array.isArray(call.artifacts)) {
@@ -67159,12 +67180,14 @@ var require_conditional_write = __commonJS({
67159
67180
  function buildConditionalWriteBasis(input) {
67160
67181
  const require_conditional_write2 = input.requireConditionalWrite === true;
67161
67182
  const write_style = input.writeStyle === true;
67183
+ const mutating = input.mutating === void 0 ? write_style : input.mutating === true;
67162
67184
  const ctx = input.ctx && typeof input.ctx === "object" ? input.ctx : {};
67163
67185
  const conditional_write = normalizeReport(ctx.conditional_write);
67164
67186
  const basis = {
67165
67187
  conditional_write,
67166
67188
  require_conditional_write: require_conditional_write2,
67167
- write_style
67189
+ write_style,
67190
+ mutating
67168
67191
  };
67169
67192
  if (conditional_write === true) {
67170
67193
  if (typeof ctx.conditioned_on_token === "string") {
@@ -67175,7 +67198,7 @@ var require_conditional_write = __commonJS({
67175
67198
  basis.conditioned_on_token = null;
67176
67199
  }
67177
67200
  }
67178
- if (write_style && require_conditional_write2 && conditional_write !== true) {
67201
+ if (mutating && require_conditional_write2 && conditional_write !== true) {
67179
67202
  return { basis, blockCause: "CONDITIONAL_WRITE_REQUIRED" };
67180
67203
  }
67181
67204
  return { basis };
@@ -67184,7 +67207,8 @@ var require_conditional_write = __commonJS({
67184
67207
  function conditionalWriteResidual(input) {
67185
67208
  if (input.requireConditionalWrite !== true)
67186
67209
  return null;
67187
- if (input.writeStyle !== true)
67210
+ const mutating = input.mutating === void 0 ? input.writeStyle === true : input.mutating === true;
67211
+ if (!mutating)
67188
67212
  return null;
67189
67213
  if (input.conditionalWrite === true)
67190
67214
  return null;
@@ -70303,9 +70327,11 @@ var require_guard = __commonJS({
70303
70327
  allowStaleContext: config.allowStaleContext
70304
70328
  });
70305
70329
  }
70306
- function conditionalWriteFor(config, redacted, ctx) {
70330
+ function conditionalWriteFor(config, redacted, ctx, detectedArts) {
70331
+ const mutating = (0, freshness_js_1.isMutatingCall)(redacted) || Array.isArray(detectedArts) && (0, freshness_js_1.isMutatingCall)({ artifacts: detectedArts });
70307
70332
  return (0, conditional_write_js_1.buildConditionalWriteBasis)({
70308
70333
  writeStyle: (0, freshness_js_1.isWriteStyleCall)(redacted),
70334
+ mutating,
70309
70335
  requireConditionalWrite: config.requireConditionalWrite === true,
70310
70336
  ctx
70311
70337
  });
@@ -70377,7 +70403,7 @@ var require_guard = __commonJS({
70377
70403
  }
70378
70404
  }
70379
70405
  {
70380
- const { blockCause } = conditionalWriteFor(config, redacted, cwctx);
70406
+ const { blockCause } = conditionalWriteFor(config, redacted, cwctx, detection.artifacts);
70381
70407
  if (blockCause === "CONDITIONAL_WRITE_REQUIRED") {
70382
70408
  breakerRecord(config);
70383
70409
  return closedIntegrity(config, blockCause, failPolicy, fctx, cwctx, redacted, detection.artifacts);
@@ -70388,7 +70414,7 @@ var require_guard = __commonJS({
70388
70414
  const count = breakers.get(config)?.fails.length ?? 0;
70389
70415
  const v = unavailableVerdict({ cause: "MISSING_ARTIFACT_CONTENT", failPolicy, resolution: "CLOSED", action: "STOP" }, count);
70390
70416
  const { basis } = freshnessFor(config, redacted, fctx, detection.artifacts);
70391
- const { basis: cw } = conditionalWriteFor(config, redacted, cwctx);
70417
+ const { basis: cw } = conditionalWriteFor(config, redacted, cwctx, detection.artifacts);
70392
70418
  return blocked(config, v, false, basis, cw);
70393
70419
  }
70394
70420
  if (failPolicy === "lkg" && !config.lkg) {
@@ -70425,7 +70451,7 @@ var require_guard = __commonJS({
70425
70451
  breakerRecord(config);
70426
70452
  const count = breakers.get(config)?.fails.length ?? 1;
70427
70453
  const { basis } = freshnessFor(config, redacted, fctx, detection.artifacts);
70428
- const { basis: cw } = conditionalWriteFor(config, redacted, cwctx);
70454
+ const { basis: cw } = conditionalWriteFor(config, redacted, cwctx, detection.artifacts);
70429
70455
  if (pf.integrity) {
70430
70456
  emit(config, { type: "breaker_tripped", at: iso(), cause: pf.cause });
70431
70457
  const v2 = unavailableVerdict({ cause: pf.cause, failPolicy, resolution: "CLOSED", action: "STOP" }, count);
@@ -70493,7 +70519,7 @@ var require_guard = __commonJS({
70493
70519
  }
70494
70520
  if (gate.verdict === "block-strict") {
70495
70521
  const { basis } = freshnessFor(config, redacted, fctx, detection.artifacts);
70496
- const { basis: cw } = conditionalWriteFor(config, redacted, cwctx);
70522
+ const { basis: cw } = conditionalWriteFor(config, redacted, cwctx, detection.artifacts);
70497
70523
  return gate.decision === "BLOCK" ? blocked(config, { kind: "BLOCK", action: "STOP", envelope, receiptVerified }, true, basis, cw, void 0, void 0, grantObs) : blocked(config, { kind: "APPROVAL", action: "REQUEST_APPROVAL", envelope, receiptVerified }, true, basis, cw, void 0, void 0, grantObs);
70498
70524
  }
70499
70525
  const kind = gate.kind;
@@ -70564,7 +70590,7 @@ var require_guard = __commonJS({
70564
70590
  breakerRecord(config);
70565
70591
  return closedIntegrity(config, freshBlock, failPolicy, fctx, cwctx, redacted, detection.artifacts, void 0, void 0, grantObs);
70566
70592
  }
70567
- const { basis: cwBasis, blockCause: cwBlock } = conditionalWriteFor(config, redacted, cwctx);
70593
+ const { basis: cwBasis, blockCause: cwBlock } = conditionalWriteFor(config, redacted, cwctx, detection.artifacts);
70568
70594
  if (cwBlock === "CONDITIONAL_WRITE_REQUIRED") {
70569
70595
  breakerRecord(config);
70570
70596
  return closedIntegrity(config, cwBlock, failPolicy, fctx, cwctx, redacted, detection.artifacts, void 0, void 0, grantObs);
@@ -70639,7 +70665,7 @@ var require_guard = __commonJS({
70639
70665
  emit(config, { type: "breaker_tripped", at: iso(), cause });
70640
70666
  const v = unavailableVerdict({ cause, failPolicy, resolution: "CLOSED", action: "STOP" }, count);
70641
70667
  const { basis } = freshnessFor(config, redacted, fctx, arts);
70642
- const { basis: cw } = conditionalWriteFor(config, redacted, cwctx);
70668
+ const { basis: cw } = conditionalWriteFor(config, redacted, cwctx, arts);
70643
70669
  return blocked(config, v, false, basis, cw, monitoring_delivery, monitoring_attestation, grantObs);
70644
70670
  }
70645
70671
  function isExpired(envelope) {
@@ -74818,7 +74844,8 @@ var require_execute_tool_call = __commonJS({
74818
74844
  var CW_NOT_REPORTED = Object.freeze({
74819
74845
  conditional_write: "not_reported",
74820
74846
  require_conditional_write: false,
74821
- write_style: false
74847
+ write_style: false,
74848
+ mutating: false
74822
74849
  });
74823
74850
  function notObserved() {
74824
74851
  return {
@@ -75271,9 +75298,9 @@ var require_cjs4 = __commonJS({
75271
75298
  "use strict";
75272
75299
  Object.defineProperty(exports2, "__esModule", { value: true });
75273
75300
  exports2.monitorAttestSigningInput = exports2.kidFromMonitoringAttestation = exports2.tryIssueMonitoringAttestation = exports2.DEFAULT_MONITORING_SINK_TIMEOUT_MS = exports2.ackBytes = exports2.verifyAckHmac = exports2.monitoringDeliveryFailClosed = exports2.formatMonitoringDeliveryLine = exports2.deliverMonitoring = exports2.hashObservedContent = exports2.observeCommit = exports2.deriveKeySignal = exports2.pathClass = exports2.classifyCommand = exports2.projectState = exports2.emptySessionState = exports2.computeTainted = exports2.evaluate = exports2.updateSession = exports2.SESSION_TAINT_VERSION = exports2.SessionTaintTracker = exports2.readDecision = exports2.EXECUTION_TIME_FP_REASONS = exports2.EXECUTION_STATE_UNMEASURABLE_NOTE = exports2.isUnmeasurableExecutionStateReason = exports2.computeCanonicalBundleFingerprint = exports2.authorizedFingerprintFromEnvelope = exports2.checkExecutionTimeFingerprint = exports2.computeBundleFingerprint = exports2.computeArtifactDigest = exports2.evaluateEnvelope = exports2.RECEIPT_PREV_NULL = exports2.decodeReceiptBodyPrev = exports2.previousReceiptCommitment = exports2.verifyReceiptChainLinkage = exports2.canonicalJson = exports2.computeBodyHash = exports2.bindReceiptToEnvelope = exports2.DETECTOR_VERSION = exports2.builtinDetector = exports2.resetPolicyWarnForTests = exports2.warnPolicyAbsentOnce = exports2.observePolicyPresence = exports2.detectPolicyPresence = exports2.policyPresenceOf = exports2.withPolicy = exports2.POLICY_ABSENT_WARN = exports2.POLICY_MARKER = exports2.CODERIFTS_POLICY = exports2.guardToolCall = void 0;
75274
- exports2.createRegistryVersionToken = exports2.DB_ABSENT_TOKEN = exports2.DB_VERSION_TOKEN_PREFIX = exports2.dbTokenRaw = exports2.writeDbIfUnchanged = exports2.createDbVersionToken = exports2.API_ABSENT_TOKEN = exports2.API_VERSION_TOKEN_PREFIX = exports2.apiTokenRaw = exports2.writeApiIfUnchanged = exports2.createApiVersionToken = exports2.FS_ABSENT_TOKEN = exports2.FS_VERSION_TOKEN_PREFIX = exports2.fsTokenContentHash = exports2.createFsPriorContentResolver = exports2.writeFileIfUnchanged = exports2.readVersionedFile = exports2.createFsVersionToken = exports2.StaleVersionTokenAbort = exports2.executeIfUnchanged = exports2.RESIDUAL_UNCONDITIONAL_WRITE = exports2.conditionalWriteResidual = exports2.tokensEqual = exports2.buildConditionalWriteBasis = exports2.buildFreshnessBasis = exports2.collectFreshnessCallContext = exports2.artifactIdsForResolve = exports2.isWriteStyleCall = exports2.freshnessAllowsEnforce = exports2.computePathSetTreeHash = exports2.contentByteIdentical = exports2.assessWriteStylePrior = exports2.assessFreshness = exports2.deriveProofBanner = exports2.attachProofToAgentResponse = exports2.renderFinalAnswerProof = exports2.EXECUTION_PROOF_SPEC = exports2.assertEnforcedReceiptInvariant = exports2.hashExecutionResult = exports2.buildExecutionProof = exports2.COVERAGE_ATTEST_ENVELOPE_TAG = exports2.COVERAGE_ATTEST_SIGNING_PREFIX = exports2.COVERAGE_ATTEST_VERSION = exports2.coverageAttestSigningInput = exports2.kidFromCoverageAttestation = exports2.tryIssueCoverageAttestation = exports2.MONITOR_ATTEST_ENVELOPE_TAG = exports2.MONITOR_ATTEST_SIGNING_PREFIX = exports2.MONITOR_ATTEST_VERSION = exports2.receiptDigestOfToken = void 0;
75275
- exports2.formatCoverageObservedLine = exports2.freezeCoverageObserved = exports2.createCoverageObserver = exports2.readExecutionGrantToken = exports2.isExecutionGrantEnabled = exports2.guardedFractionAmongRoutes = exports2.foldTableSettledCalls = exports2.withCodeRifts = exports2.AUTO_DERIVE_READ_TIMEOUT_MS = exports2.AUTO_DERIVE_SOURCE = exports2.defaultFsReader = exports2.normalizeAutoDerive = exports2.runAutoDerive = exports2.AUTO_RECHECK_MAX_CAP = exports2.clampMaxAttempts = exports2.normalizeAutoRecheck = exports2.runAutoRecheckLoop = exports2.coverageReport = exports2.DEPLOY_REPAIRABLE_REASONS = exports2.bindDeploy = exports2.verifyDeployReceiptToken = exports2.DEPLOY_RECEIPT_VIEW_SPEC = exports2.isVerifiedDeployReceiptView = exports2.asVerifiedDeployReceiptView = exports2.deployGate = exports2.gateDecision = exports2.RegistryConstructionError = exports2.guardToolRegistry = exports2.globToRegExp = exports2.matchGlob = exports2.blobMapKey = exports2.classifyByName = exports2.resolveArtifacts = exports2.REMEDIATION_LOOP_ATTESTATION_SPEC = exports2.isCasAttestation = exports2.readPriorBlockRemediation = exports2.readRemediationTransaction = exports2.buildRemediationLoopAttestation = exports2.COMMIT_EVIDENCE_MISSING = exports2.CAS_ATTESTATION_SPEC = exports2.strictCommitObservation = exports2.extractExecutorAttestationToken = exports2.evaluateCasEvidence = exports2.isExecuteIfUnchangedOutcome = exports2.isGuardExecutionProof = exports2.buildCasAttestation = exports2.REGISTRY_ABSENT_TOKEN = exports2.REGISTRY_VERSION_TOKEN_PREFIX = exports2.registryTokenRaw = exports2.writeRegistryIfUnchanged = void 0;
75276
- exports2.kidFromToolsetAttestation = exports2.tryIssueToolsetAttestation = exports2.toolsetAttestSigningInput = exports2.declarationEntriesFromTools = exports2.computeToolsetDigest = exports2.TOOLSET_ATTEST_STATEMENT = exports2.TOOLSET_ATTEST_ENVELOPE_TAG = exports2.TOOLSET_ATTEST_SIGNING_PREFIX = exports2.TOOLSET_ATTEST_VERSION = exports2.surfaceEnvelopeFields = exports2.isGuardOutcome = exports2.executeLangGraphToolCall = exports2.executeGeminiToolCall = exports2.executeAnthropicToolCall = exports2.executeOpenAIToolCall = exports2.executeProtectedTool = exports2.defaultSerializeGeminiToolResult = exports2.bindGeminiGuardOutcome = exports2.protectedToolToFunctionDeclaration = exports2.toGeminiTools = exports2.geminiToolAdapter = exports2.withCodeRiftsGemini = exports2.wrapWriteWithFsCas = exports2.inferFullFileWriteContent = exports2.inferFsPathFromArgs = exports2.FRESHNESS_RESOLVER_FIX = exports2.freshnessRefusalTeaching = exports2.formatGateRefusalBody = exports2.defaultSerializeLangGraphToolResult = exports2.bindLangGraphGuardOutcome = exports2.LangGraphToolsNotStructuredError = exports2.isLangGraphReactAgentTool = exports2.bindLangGraphTools = exports2.protectedToolToLangGraph = exports2.toLangGraphTools = exports2.langGraphToolAdapter = exports2.withCodeRiftsLangGraph = exports2.defaultSerializeAnthropicToolResult = exports2.bindAnthropicGuardOutcome = exports2.protectedToolToAnthropic = exports2.toAnthropicTools = exports2.anthropicToolAdapter = exports2.withCodeRiftsAnthropic = exports2.defaultSerializeOpenAIToolResult = exports2.bindOpenAIGuardOutcome = exports2.protectedToolToOpenAI = exports2.toOpenAITools = exports2.openAIToolAdapter = exports2.withCodeRiftsOpenAI = void 0;
75301
+ exports2.DB_ABSENT_TOKEN = exports2.DB_VERSION_TOKEN_PREFIX = exports2.dbTokenRaw = exports2.writeDbIfUnchanged = exports2.createDbVersionToken = exports2.API_ABSENT_TOKEN = exports2.API_VERSION_TOKEN_PREFIX = exports2.apiTokenRaw = exports2.writeApiIfUnchanged = exports2.createApiVersionToken = exports2.FS_ABSENT_TOKEN = exports2.FS_VERSION_TOKEN_PREFIX = exports2.fsTokenContentHash = exports2.createFsPriorContentResolver = exports2.writeFileIfUnchanged = exports2.readVersionedFile = exports2.createFsVersionToken = exports2.StaleVersionTokenAbort = exports2.executeIfUnchanged = exports2.RESIDUAL_UNCONDITIONAL_WRITE = exports2.conditionalWriteResidual = exports2.tokensEqual = exports2.buildConditionalWriteBasis = exports2.buildFreshnessBasis = exports2.collectFreshnessCallContext = exports2.artifactIdsForResolve = exports2.isMutatingCall = exports2.isWriteStyleCall = exports2.freshnessAllowsEnforce = exports2.computePathSetTreeHash = exports2.contentByteIdentical = exports2.assessWriteStylePrior = exports2.assessFreshness = exports2.deriveProofBanner = exports2.attachProofToAgentResponse = exports2.renderFinalAnswerProof = exports2.EXECUTION_PROOF_SPEC = exports2.assertEnforcedReceiptInvariant = exports2.hashExecutionResult = exports2.buildExecutionProof = exports2.COVERAGE_ATTEST_ENVELOPE_TAG = exports2.COVERAGE_ATTEST_SIGNING_PREFIX = exports2.COVERAGE_ATTEST_VERSION = exports2.coverageAttestSigningInput = exports2.kidFromCoverageAttestation = exports2.tryIssueCoverageAttestation = exports2.MONITOR_ATTEST_ENVELOPE_TAG = exports2.MONITOR_ATTEST_SIGNING_PREFIX = exports2.MONITOR_ATTEST_VERSION = exports2.receiptDigestOfToken = void 0;
75302
+ exports2.freezeCoverageObserved = exports2.createCoverageObserver = exports2.readExecutionGrantToken = exports2.isExecutionGrantEnabled = exports2.guardedFractionAmongRoutes = exports2.foldTableSettledCalls = exports2.withCodeRifts = exports2.AUTO_DERIVE_READ_TIMEOUT_MS = exports2.AUTO_DERIVE_SOURCE = exports2.defaultFsReader = exports2.normalizeAutoDerive = exports2.runAutoDerive = exports2.AUTO_RECHECK_MAX_CAP = exports2.clampMaxAttempts = exports2.normalizeAutoRecheck = exports2.runAutoRecheckLoop = exports2.coverageReport = exports2.DEPLOY_REPAIRABLE_REASONS = exports2.bindDeploy = exports2.verifyDeployReceiptToken = exports2.DEPLOY_RECEIPT_VIEW_SPEC = exports2.isVerifiedDeployReceiptView = exports2.asVerifiedDeployReceiptView = exports2.deployGate = exports2.gateDecision = exports2.RegistryConstructionError = exports2.guardToolRegistry = exports2.globToRegExp = exports2.matchGlob = exports2.blobMapKey = exports2.classifyByName = exports2.resolveArtifacts = exports2.REMEDIATION_LOOP_ATTESTATION_SPEC = exports2.isCasAttestation = exports2.readPriorBlockRemediation = exports2.readRemediationTransaction = exports2.buildRemediationLoopAttestation = exports2.COMMIT_EVIDENCE_MISSING = exports2.CAS_ATTESTATION_SPEC = exports2.strictCommitObservation = exports2.extractExecutorAttestationToken = exports2.evaluateCasEvidence = exports2.isExecuteIfUnchangedOutcome = exports2.isGuardExecutionProof = exports2.buildCasAttestation = exports2.REGISTRY_ABSENT_TOKEN = exports2.REGISTRY_VERSION_TOKEN_PREFIX = exports2.registryTokenRaw = exports2.writeRegistryIfUnchanged = exports2.createRegistryVersionToken = void 0;
75303
+ exports2.kidFromToolsetAttestation = exports2.tryIssueToolsetAttestation = exports2.toolsetAttestSigningInput = exports2.declarationEntriesFromTools = exports2.computeToolsetDigest = exports2.TOOLSET_ATTEST_STATEMENT = exports2.TOOLSET_ATTEST_ENVELOPE_TAG = exports2.TOOLSET_ATTEST_SIGNING_PREFIX = exports2.TOOLSET_ATTEST_VERSION = exports2.surfaceEnvelopeFields = exports2.isGuardOutcome = exports2.executeLangGraphToolCall = exports2.executeGeminiToolCall = exports2.executeAnthropicToolCall = exports2.executeOpenAIToolCall = exports2.executeProtectedTool = exports2.defaultSerializeGeminiToolResult = exports2.bindGeminiGuardOutcome = exports2.protectedToolToFunctionDeclaration = exports2.toGeminiTools = exports2.geminiToolAdapter = exports2.withCodeRiftsGemini = exports2.wrapWriteWithFsCas = exports2.inferFullFileWriteContent = exports2.inferFsPathFromArgs = exports2.FRESHNESS_RESOLVER_FIX = exports2.freshnessRefusalTeaching = exports2.formatGateRefusalBody = exports2.defaultSerializeLangGraphToolResult = exports2.bindLangGraphGuardOutcome = exports2.LangGraphToolsNotStructuredError = exports2.isLangGraphReactAgentTool = exports2.bindLangGraphTools = exports2.protectedToolToLangGraph = exports2.toLangGraphTools = exports2.langGraphToolAdapter = exports2.withCodeRiftsLangGraph = exports2.defaultSerializeAnthropicToolResult = exports2.bindAnthropicGuardOutcome = exports2.protectedToolToAnthropic = exports2.toAnthropicTools = exports2.anthropicToolAdapter = exports2.withCodeRiftsAnthropic = exports2.defaultSerializeOpenAIToolResult = exports2.bindOpenAIGuardOutcome = exports2.protectedToolToOpenAI = exports2.toOpenAITools = exports2.openAIToolAdapter = exports2.withCodeRiftsOpenAI = exports2.formatCoverageObservedLine = void 0;
75277
75304
  var guard_js_1 = require_guard();
75278
75305
  Object.defineProperty(exports2, "guardToolCall", { enumerable: true, get: function() {
75279
75306
  return guard_js_1.guardToolCall;
@@ -75509,6 +75536,9 @@ var require_cjs4 = __commonJS({
75509
75536
  Object.defineProperty(exports2, "isWriteStyleCall", { enumerable: true, get: function() {
75510
75537
  return freshness_js_1.isWriteStyleCall;
75511
75538
  } });
75539
+ Object.defineProperty(exports2, "isMutatingCall", { enumerable: true, get: function() {
75540
+ return freshness_js_1.isMutatingCall;
75541
+ } });
75512
75542
  Object.defineProperty(exports2, "artifactIdsForResolve", { enumerable: true, get: function() {
75513
75543
  return freshness_js_1.artifactIdsForResolve;
75514
75544
  } });
@@ -253945,6 +253975,16 @@ var require_github_enforcement = __commonJS({
253945
253975
  RULESET_ABSENT: "ruleset_absent",
253946
253976
  /** Rulesets could not be read at all. */
253947
253977
  RULESET_UNREADABLE: "ruleset_unreadable",
253978
+ /** A ruleset requires our context but does not TARGET the branch that matters (1104a). */
253979
+ RULESET_REF_NOT_TARGETED: "ruleset_ref_not_targeted",
253980
+ /** Active, bound, targeted — but strict latest-head is off (1104b). Recorded, not fatal. */
253981
+ RULESET_STRICT_POLICY_OFF: "ruleset_strict_policy_off",
253982
+ /** 1102 — no active targeting ruleset requires a workflow at all. */
253983
+ REQUIRED_WORKFLOW_ABSENT: "required_workflow_absent",
253984
+ /** 1102 — a workflows rule exists but carries no sha, so it names a path and not bytes. */
253985
+ REQUIRED_WORKFLOW_UNPINNED: "required_workflow_unpinned",
253986
+ /** 1102 — a required workflow is pinned to a full commit sha. */
253987
+ REQUIRED_WORKFLOW_SHA_PINNED: "required_workflow_sha_pinned",
253948
253988
  REQUIRED_CHECK_ABSENT: "required_check_absent",
253949
253989
  ACTIVE_JOB: "active_job",
253950
253990
  NO_ACTIVE_JOB: "no_active_job",
@@ -253975,6 +254015,41 @@ var require_github_enforcement = __commonJS({
253975
254015
  const n = Number(value);
253976
254016
  return Number.isInteger(n) && n > 0 ? n : null;
253977
254017
  }
254018
+ function refPatterns(rs, key) {
254019
+ const rn = rs && rs.conditions && rs.conditions.ref_name;
254020
+ if (!rn || !Array.isArray(rn[key])) return null;
254021
+ return rn[key].map((x) => String(x));
254022
+ }
254023
+ function targetsDefaultBranch(entry, defaultBranch) {
254024
+ if (entry.target === "tag") return false;
254025
+ const inc = entry.ref_include;
254026
+ if (!Array.isArray(inc)) return null;
254027
+ const ref = defaultBranch ? `refs/heads/${defaultBranch}` : null;
254028
+ const matches = (pat) => {
254029
+ if (pat === "~ALL" || pat === "~DEFAULT_BRANCH") return true;
254030
+ if (!ref) return null;
254031
+ if (pat === ref) return true;
254032
+ if (!pat.includes("*") && !pat.includes("[") && !pat.includes("?")) return false;
254033
+ const rx = new RegExp(`^${pat.replace(/[.+^${}()|\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, ".")}$`);
254034
+ return rx.test(ref);
254035
+ };
254036
+ let included = false;
254037
+ for (const pat of inc) {
254038
+ const m = matches(pat);
254039
+ if (m === null) return null;
254040
+ if (m) {
254041
+ included = true;
254042
+ break;
254043
+ }
254044
+ }
254045
+ if (!included) return false;
254046
+ for (const pat of entry.ref_exclude || []) {
254047
+ const m = matches(pat);
254048
+ if (m === null) return null;
254049
+ if (m) return false;
254050
+ }
254051
+ return true;
254052
+ }
253978
254053
  function rulesetRequiredCheckEntries(rulesets) {
253979
254054
  const out = [];
253980
254055
  const list = Array.isArray(rulesets) ? rulesets : [];
@@ -253993,7 +254068,15 @@ var require_github_enforcement = __commonJS({
253993
254068
  enforcement: typeof rs.enforcement === "string" ? rs.enforcement : null,
253994
254069
  integration_id: numericAppId(c && c.integration_id),
253995
254070
  bypass_visible: Object.prototype.hasOwnProperty.call(rs, "bypass_actors"),
253996
- bypass_count: Array.isArray(rs.bypass_actors) ? rs.bypass_actors.length : null
254071
+ bypass_count: Array.isArray(rs.bypass_actors) ? rs.bypass_actors.length : null,
254072
+ // 1104a — a ruleset can be ACTIVE and target nothing that matters. MEASURED against the
254073
+ // live API: `target` and `conditions.ref_name` are both in the detail document we already
254074
+ // fetch, so this is a grading change and needs no new call.
254075
+ target: typeof rs.target === "string" ? rs.target : null,
254076
+ ref_include: refPatterns(rs, "include"),
254077
+ ref_exclude: refPatterns(rs, "exclude"),
254078
+ // 1104b — same document, one boolean.
254079
+ strict_policy: typeof params.strict_required_status_checks_policy === "boolean" ? params.strict_required_status_checks_policy : null
253997
254080
  });
253998
254081
  }
253999
254082
  }
@@ -254001,7 +254084,7 @@ var require_github_enforcement = __commonJS({
254001
254084
  return out;
254002
254085
  }
254003
254086
  var RULESET_BYPASS_CAVEAT = " Bypass actors were not readable on this surface, so this does not establish that nobody can bypass the ruleset.";
254004
- function evaluateRuleset(rulesets) {
254087
+ function evaluateRuleset(rulesets, defaultBranch = null) {
254005
254088
  if (rulesets == null) {
254006
254089
  return {
254007
254090
  status: STATUS.UNVERIFIABLE,
@@ -254032,12 +254115,41 @@ var require_github_enforcement = __commonJS({
254032
254115
  }
254033
254116
  const bound = active.find((e) => e.integration_id === ENFORCING_ISSUER_APP_ID);
254034
254117
  if (bound) {
254118
+ const targeted = targetsDefaultBranch(bound, defaultBranch);
254119
+ if (targeted === false) {
254120
+ return {
254121
+ status: STATUS.NOT_VERIFIED,
254122
+ reason_code: REASON.RULESET_REF_NOT_TARGETED,
254123
+ reason: `active ruleset "${bound.ruleset}" requires "${CHECK_NAME}" bound to the enforcing issuer, but its conditions do not target ${defaultBranch ? `refs/heads/${defaultBranch}` : "the default branch"}${bound.target === "tag" ? " (the ruleset targets tags, which never gate a merge to a branch)" : ""} \u2014 include: ${JSON.stringify(bound.ref_include)}, exclude: ${JSON.stringify(bound.ref_exclude || [])}`,
254124
+ ruleset_names: names,
254125
+ app_id: ENFORCING_ISSUER_APP_ID,
254126
+ ref_targeted: false
254127
+ };
254128
+ }
254129
+ if (targeted === null) {
254130
+ return {
254131
+ status: STATUS.UNVERIFIABLE,
254132
+ reason_code: REASON.RULESET_REF_NOT_TARGETED,
254133
+ reason: `active ruleset "${bound.ruleset}" binds "${CHECK_NAME}" to the enforcing issuer, but its ref conditions could not be read, so whether it targets the branch that matters CANNOT BE DETERMINED FROM HERE.${RULESET_BYPASS_CAVEAT}`,
254134
+ ruleset_names: names,
254135
+ app_id: ENFORCING_ISSUER_APP_ID,
254136
+ ref_targeted: null
254137
+ };
254138
+ }
254035
254139
  return {
254036
254140
  status: STATUS.VERIFIED,
254037
254141
  reason_code: REASON.RULESET_ISSUER_BOUND,
254038
- reason: `active ruleset "${bound.ruleset}" requires "${CHECK_NAME}" bound to integration_id ${ENFORCING_ISSUER_APP_ID} (${GITHUB_ACTIONS_APP_SLUG}) \u2014 the contract-gate Action, which concludes failure and blocks the merge.${RULESET_BYPASS_CAVEAT}`,
254142
+ reason: `active ruleset "${bound.ruleset}" requires "${CHECK_NAME}" bound to integration_id ${ENFORCING_ISSUER_APP_ID} (${GITHUB_ACTIONS_APP_SLUG}) \u2014 the contract-gate Action, which concludes failure and blocks the merge. It targets ${defaultBranch ? `refs/heads/${defaultBranch}` : "the default branch"}.` + (bound.strict_policy === true ? " strict_required_status_checks_policy is ON, so a stale head cannot merge." : bound.strict_policy === false ? " RESIDUAL: strict_required_status_checks_policy is OFF, so a branch whose head is behind the base can merge on a check that passed against older bytes." : " strict_required_status_checks_policy was not readable here.") + RULESET_BYPASS_CAVEAT,
254039
254143
  ruleset_names: names,
254040
- app_id: ENFORCING_ISSUER_APP_ID
254144
+ app_id: ENFORCING_ISSUER_APP_ID,
254145
+ ref_targeted: true,
254146
+ strict_policy: bound.strict_policy,
254147
+ strict_policy_residual: bound.strict_policy === false ? REASON.RULESET_STRICT_POLICY_OFF : null,
254148
+ // 1104c — NEVER an absence we cannot see. bypass_actors is in the ruleset schema but was
254149
+ // absent from all six live rulesets read without a credential (measured 2026-08-27), so
254150
+ // "not present in the response" is reported as NOT VISIBLE and never as "none configured".
254151
+ bypass_visible: bound.bypass_visible === true,
254152
+ bypass_count: bound.bypass_visible === true ? bound.bypass_count : null
254041
254153
  };
254042
254154
  }
254043
254155
  const other = active.find((e) => e.integration_id != null);
@@ -254058,9 +254170,66 @@ var require_github_enforcement = __commonJS({
254058
254170
  ruleset_names: names
254059
254171
  };
254060
254172
  }
254061
- function evaluateRequiredCheckAcrossSurfaces(protection, rulesets) {
254173
+ function evaluateRequiredWorkflowPin(rulesets, defaultBranch = null) {
254174
+ if (rulesets == null) {
254175
+ return { status: STATUS.UNVERIFIABLE, reason_code: REASON.RULESET_UNREADABLE, pinned: null, workflows: [] };
254176
+ }
254177
+ const found = [];
254178
+ for (const rs of Array.isArray(rulesets) ? rulesets : []) {
254179
+ if (!rs || typeof rs !== "object") continue;
254180
+ if (rs.enforcement !== "active") continue;
254181
+ const entryShape = {
254182
+ target: typeof rs.target === "string" ? rs.target : null,
254183
+ ref_include: refPatterns(rs, "include"),
254184
+ ref_exclude: refPatterns(rs, "exclude")
254185
+ };
254186
+ if (targetsDefaultBranch(entryShape, defaultBranch) !== true) continue;
254187
+ for (const rule of Array.isArray(rs.rules) ? rs.rules : []) {
254188
+ if (!rule || rule.type !== "workflows") continue;
254189
+ const list = rule.parameters && Array.isArray(rule.parameters.workflows) ? rule.parameters.workflows : [];
254190
+ for (const w of list) {
254191
+ if (!w || typeof w !== "object") continue;
254192
+ found.push({
254193
+ ruleset: String(rs.name || rs.id || "ruleset"),
254194
+ path: typeof w.path === "string" ? w.path : null,
254195
+ repository_id: Number.isInteger(w.repository_id) ? w.repository_id : null,
254196
+ ref: typeof w.ref === "string" ? w.ref : null,
254197
+ // The whole question. sha is optional in the schema, so its ABSENCE is the finding.
254198
+ sha: typeof w.sha === "string" && /^[0-9a-f]{40}$/i.test(w.sha) ? w.sha : null
254199
+ });
254200
+ }
254201
+ }
254202
+ }
254203
+ if (found.length === 0) {
254204
+ return {
254205
+ status: STATUS.NOT_VERIFIED,
254206
+ reason_code: REASON.REQUIRED_WORKFLOW_ABSENT,
254207
+ pinned: false,
254208
+ workflows: [],
254209
+ reason: `no active ruleset targeting this branch requires a workflow, so the check that gates the merge is bound only to the SHARED issuer ${ENFORCING_ISSUER_APP_ID} (${GITHUB_ACTIONS_APP_SLUG}) \u2014 anyone who can edit .github/workflows can post under it`
254210
+ };
254211
+ }
254212
+ const pinned = found.filter((w) => w.sha != null);
254213
+ if (pinned.length === 0) {
254214
+ return {
254215
+ status: STATUS.NOT_VERIFIED,
254216
+ reason_code: REASON.REQUIRED_WORKFLOW_UNPINNED,
254217
+ pinned: false,
254218
+ workflows: found,
254219
+ reason: `a required workflow is configured (${found.map((w) => w.path || "?").join(", ")}) but NONE carries a sha. \`sha\` is optional in the rule schema, and without it the ruleset names a path rather than bytes, so rewriting that file still changes what runs`
254220
+ };
254221
+ }
254222
+ return {
254223
+ status: STATUS.VERIFIED,
254224
+ reason_code: REASON.REQUIRED_WORKFLOW_SHA_PINNED,
254225
+ pinned: true,
254226
+ workflows: found,
254227
+ reason: `active ruleset "${pinned[0].ruleset}" requires workflow ${pinned[0].path} pinned to sha ${pinned[0].sha} in repository_id ${pinned[0].repository_id}. Moving that pin requires editing the ruleset (repository administration), which is a different permission from workflow write. This does NOT establish that any pull request ran it, nor that bypass actors cannot skip the ruleset.`
254228
+ };
254229
+ }
254230
+ function evaluateRequiredCheckAcrossSurfaces(protection, rulesets, defaultBranch = null) {
254062
254231
  const classic = evaluateRequiredCheck(protection);
254063
- const ruleset = evaluateRuleset(rulesets);
254232
+ const ruleset = evaluateRuleset(rulesets, defaultBranch);
254064
254233
  const merged = (base) => ({
254065
254234
  ...base,
254066
254235
  surfaces_inspected: ["classic_branch_protection", rulesets == null ? "rulesets:unread" : "rulesets"],
@@ -254180,8 +254349,22 @@ var require_github_enforcement = __commonJS({
254180
254349
  var STATUS = Object.freeze({
254181
254350
  VERIFIED: "VERIFIED",
254182
254351
  NOT_VERIFIED: "NOT_VERIFIED",
254183
- UNVERIFIABLE: "UNVERIFIABLE"
254184
- });
254352
+ UNVERIFIABLE: "UNVERIFIABLE",
254353
+ UNSUPPORTED_PLAN: "UNSUPPORTED_PLAN",
254354
+ PROVIDER_ERROR: "PROVIDER_ERROR"
254355
+ });
254356
+ var FRESHNESS = Object.freeze({ FRESH: "FRESH", STALE_EVIDENCE: "STALE_EVIDENCE" });
254357
+ function supportsStrongClaim(status) {
254358
+ return status === STATUS.VERIFIED;
254359
+ }
254360
+ var EVIDENCE_TTL_SECONDS = 3600;
254361
+ function evidenceFreshness(report, nowIso) {
254362
+ if (!report || !report.expires_at) return null;
254363
+ const now = Date.parse(nowIso || (/* @__PURE__ */ new Date()).toISOString());
254364
+ const exp = Date.parse(report.expires_at);
254365
+ if (!Number.isFinite(now) || !Number.isFinite(exp)) return null;
254366
+ return now > exp ? FRESHNESS.STALE_EVIDENCE : FRESHNESS.FRESH;
254367
+ }
254185
254368
  function resolveGitHubAuth(env = process.env) {
254186
254369
  const user = String(env && (env.GITHUB_TOKEN || env.GH_TOKEN) || "").trim();
254187
254370
  if (user) {
@@ -254512,6 +254695,10 @@ var require_github_enforcement = __commonJS({
254512
254695
  NO_TOKEN_INSTRUCTION,
254513
254696
  LAYER_IDS,
254514
254697
  STATUS,
254698
+ FRESHNESS,
254699
+ EVIDENCE_TTL_SECONDS,
254700
+ supportsStrongClaim,
254701
+ evidenceFreshness,
254515
254702
  REASON,
254516
254703
  CHECK_NAME,
254517
254704
  GATE_ACTION,
@@ -254525,6 +254712,8 @@ var require_github_enforcement = __commonJS({
254525
254712
  evaluateLayers,
254526
254713
  evaluateRequiredCheck,
254527
254714
  evaluateRuleset,
254715
+ evaluateRequiredWorkflowPin,
254716
+ targetsDefaultBranch,
254528
254717
  evaluateRequiredCheckAcrossSurfaces,
254529
254718
  verifiedRequiresBinding,
254530
254719
  scanLocalWorkflows,
@@ -254911,7 +255100,7 @@ var require_setup_required_check = __commonJS({
254911
255100
  }
254912
255101
  } catch {
254913
255102
  }
254914
- const rulesetVerdict = evaluateRuleset(rulesetDocs);
255103
+ const rulesetVerdict = evaluateRuleset(rulesetDocs, branch);
254915
255104
  const rulesetNamed = detectRulesetRequiredCheck(rulesetDocs || [], CHECK_NAME);
254916
255105
  const ruleset = {
254917
255106
  enforced: rulesetVerdict.status === STATUS.VERIFIED,
@@ -255284,6 +255473,8 @@ var require_enforce_check = __commonJS({
255284
255473
  var chalk = require_source();
255285
255474
  var {
255286
255475
  EVIDENCE_SPEC,
255476
+ EVIDENCE_TTL_SECONDS,
255477
+ supportsStrongClaim,
255287
255478
  APP_INSTALLATION,
255288
255479
  NO_TOKEN_INSTRUCTION,
255289
255480
  resolveGitHubAuth,
@@ -255295,6 +255486,35 @@ var require_enforce_check = __commonJS({
255295
255486
  var { isValidRepo } = require_status();
255296
255487
  var { resolveOwnerRepo } = require_setup_required_check();
255297
255488
  if (process.env.NO_COLOR) chalk.level = 0;
255489
+ var crypto = require("node:crypto");
255490
+ var EVIDENCE_SCOPE = Object.freeze({
255491
+ grades: "repository configuration \u2014 what the provider would require of any pull request",
255492
+ covers: Object.freeze([
255493
+ "required status check present and named exactly",
255494
+ "the check bound to an issuer that can block (integration_id / app_id)",
255495
+ "ruleset enforcement is active rather than evaluate",
255496
+ "the ruleset targets the branch that matters",
255497
+ "strict latest-head policy recorded",
255498
+ "bypass actors reported as VISIBLE or NOT VISIBLE, never as absent"
255499
+ ]),
255500
+ does_not_cover: Object.freeze([
255501
+ "which GitHub App actually posted a check run on any pull request",
255502
+ "the head_sha a check run executed against",
255503
+ "whether any check run concluded success",
255504
+ "a recomputable external proof hash over a specific run"
255505
+ ]),
255506
+ why_not: "Those are facts about one pull request at one moment, not about configuration. Reading them needs a per-PR check-runs query this command does not make; a passing config is not a passing run."
255507
+ });
255508
+ function responseDigest(read) {
255509
+ if (!read) return null;
255510
+ return {
255511
+ endpoint: read.endpoint ? `GET ${read.endpoint}` : null,
255512
+ http_status: read.status == null ? null : read.status,
255513
+ // Hash of the BODY as we received it. Not the verdict — the INPUT to the verdict, so a
255514
+ // disagreement can be localised to the answer or to our grading of it.
255515
+ body_sha256: read.body === void 0 || read.body === null ? null : `sha256:${crypto.createHash("sha256").update(JSON.stringify(read.body)).digest("hex")}`
255516
+ };
255517
+ }
255298
255518
  var USAGE = [
255299
255519
  "Usage: coderifts enforce --check --repo owner/repo [--env <github-environment>] [--branch <name>] [--json]",
255300
255520
  "",
@@ -255407,7 +255627,12 @@ var require_enforce_check = __commonJS({
255407
255627
  const report2 = {
255408
255628
  command: "enforce --check",
255409
255629
  spec: EVIDENCE_SPEC,
255630
+ // OBSERVED_AT WAS NOT ADDED. `queried_at` already carries exactly that fact, and a
255631
+ // second name for one fact is the defect this round was told to look for.
255410
255632
  queried_at: queriedAt,
255633
+ expires_at: new Date(Date.parse(queriedAt) + EVIDENCE_TTL_SECONDS * 1e3).toISOString(),
255634
+ evidence_ttl_seconds: EVIDENCE_TTL_SECONDS,
255635
+ evidence_scope: EVIDENCE_SCOPE,
255411
255636
  repo: `${ownerRepo.owner}/${ownerRepo.repo}`,
255412
255637
  branch: options.branch || null,
255413
255638
  environment: environmentName,
@@ -255451,14 +255676,33 @@ var require_enforce_check = __commonJS({
255451
255676
  const report = {
255452
255677
  command: "enforce --check",
255453
255678
  spec: EVIDENCE_SPEC,
255679
+ // OBSERVED_AT WAS NOT ADDED. `queried_at` already carries exactly that fact, and a
255680
+ // second name for one fact is the defect this round was told to look for.
255454
255681
  queried_at: queriedAt,
255682
+ expires_at: new Date(Date.parse(queriedAt) + EVIDENCE_TTL_SECONDS * 1e3).toISOString(),
255683
+ evidence_ttl_seconds: EVIDENCE_TTL_SECONDS,
255684
+ evidence_scope: EVIDENCE_SCOPE,
255455
255685
  repo: `${ownerRepo.owner}/${ownerRepo.repo}`,
255456
255686
  branch,
255457
255687
  environment: environmentName,
255458
255688
  provider: "github",
255459
255689
  auth: authPublic,
255690
+ // The raw answers behind the verdict, so a disagreement can be localised to the provider's
255691
+ // response or to our grading of it.
255692
+ responses: {
255693
+ protection: responseDigest(protectionRead),
255694
+ environment: responseDigest(environmentRead)
255695
+ },
255460
255696
  ...evaluated
255461
255697
  };
255698
+ if (report.claim && report.claim.inescapable_deploy === true) {
255699
+ const unsupported = (report.layers || []).filter((l) => !supportsStrongClaim(l.status));
255700
+ if (unsupported.length > 0) {
255701
+ throw new Error(
255702
+ `enforce --check: inescapable_deploy is true with ${unsupported.length} layer(s) not VERIFIED (${unsupported.map((l) => `${l.id}=${l.status}`).join(", ")}) \u2014 refusing to emit`
255703
+ );
255704
+ }
255705
+ }
255462
255706
  if (options.json) log(JSON.stringify(report, null, 2));
255463
255707
  else log(renderHuman(report));
255464
255708
  return { exitCode: 0, report };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderifts",
3
- "version": "5.0.0",
3
+ "version": "6.0.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",
@@ -39,13 +39,13 @@
39
39
  },
40
40
  "scripts": {
41
41
  "build": "node scripts/copy-corpus.js && node scripts/build.js",
42
- "prepublishOnly": "node scripts/assert-guard-major.js && bash ../../scripts/freeze-gate.sh && npm run build",
42
+ "prepublishOnly": "node scripts/assert-guard-major.js && node scripts/assert-changelog-version.js && bash ../../scripts/freeze-gate.sh && npm run build",
43
43
  "assert-guard-major": "node scripts/assert-guard-major.js",
44
44
  "postinstall": "node scripts/postinstall.js",
45
45
  "test": "node --test test/*.test.js"
46
46
  },
47
47
  "dependencies": {
48
- "@coderifts/agent-guard": "^11.0.0",
48
+ "@coderifts/agent-guard": "^12.0.0",
49
49
  "chalk": "^4.1.2",
50
50
  "cli-table3": "^0.6.4",
51
51
  "commander": "^12.0.0",
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * CLI release gate (prepublishOnly): refuse to publish a version the CHANGELOG does not name.
6
+ *
7
+ * WHY THIS EXISTS — measured, not hypothetical. On 2026-08-27 packages/cli/CHANGELOG.md listed
8
+ * 4.10.0 and then "Unreleased", while npm carried 4.11.0, 4.12.0, 5.0.0 and 5.0.1. Four published
9
+ * versions were undocumented and a fifth (5.0.0) was published while still labelled Unreleased.
10
+ * Two distinct failure modes produced that, and BOTH are caught by the one check below:
11
+ *
12
+ * 1. The bump commit never touched the changelog at all. Measured: of the last eight CLI version
13
+ * bumps, four omitted it (4.11.0, 4.12.0, 4.13.0, 5.0.1) — and those four are exactly the
14
+ * versions that went missing. The correlation is not partial.
15
+ * 2. The bump commit DID write the entry, but under "## Unreleased", and nobody retitled it at
16
+ * publish. Atomicity alone does not save you: @coderifts/agent-guard 12.0.0 was written
17
+ * atomically and still shipped as "Unreleased", and 11.0.1 before it had to retitle a stale
18
+ * heading by hand.
19
+ *
20
+ * So the invariant is NOT "the bump touched the changelog" — it is that the version being published
21
+ * appears as its own heading. A human step that must be remembered at publish time is the step that
22
+ * gets skipped; this makes it fail the publish instead.
23
+ *
24
+ * Dependency-free on purpose: a publish gate that needs an install to run is a gate that stops
25
+ * running (same reasoning as assert-guard-major.js).
26
+ */
27
+
28
+ const fs = require('fs');
29
+ const path = require('path');
30
+
31
+ const PKG_PATH = path.join(__dirname, '..', 'package.json');
32
+ const CHANGELOG_PATH = path.join(__dirname, '..', 'CHANGELOG.md');
33
+
34
+ function fail(msg) {
35
+ console.error(`assert-changelog-version: FAIL — ${msg}`);
36
+ process.exit(1);
37
+ }
38
+
39
+ /**
40
+ * Headings this file recognises as "this version is documented".
41
+ * A heading may carry a trailing note (e.g. "## 4.13.0 — never published"), so match the version
42
+ * token at a word boundary rather than requiring the line to be exactly the version.
43
+ */
44
+ function headingNamesVersion(line, version) {
45
+ if (!/^##\s+/.test(line)) return false;
46
+ const rest = line.replace(/^##\s+/, '').trim();
47
+ const esc = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
48
+ return new RegExp(`^v?${esc}(\\b|$)`).test(rest);
49
+ }
50
+
51
+ function main() {
52
+ if (!fs.existsSync(PKG_PATH)) fail(`package.json not found at ${PKG_PATH}`);
53
+ if (!fs.existsSync(CHANGELOG_PATH)) fail(`CHANGELOG.md not found at ${CHANGELOG_PATH}`);
54
+
55
+ let version;
56
+ try {
57
+ version = JSON.parse(fs.readFileSync(PKG_PATH, 'utf8')).version;
58
+ } catch (err) {
59
+ fail(`package.json is not readable JSON: ${err.message}`);
60
+ }
61
+ if (typeof version !== 'string' || !version.trim()) fail('package.json has no version string');
62
+
63
+ const lines = fs.readFileSync(CHANGELOG_PATH, 'utf8').split('\n');
64
+ const headings = lines.filter((l) => /^##\s+/.test(l));
65
+ if (headings.length === 0) fail('CHANGELOG.md has no "## " headings at all');
66
+
67
+ const match = headings.find((l) => headingNamesVersion(l, version));
68
+ if (!match) {
69
+ const listed = headings.slice(0, 4).map((h) => h.replace(/^##\s+/, '').trim());
70
+ fail(
71
+ `package.json is ${version} but CHANGELOG.md has no "## ${version}" heading.\n`
72
+ + ` Top headings are: ${listed.join(' · ')}\n`
73
+ + ' Publishing now is how a version goes undocumented. Retitle the Unreleased section to\n'
74
+ + ` ${version}, or add its section, then publish.`,
75
+ );
76
+ }
77
+
78
+ // "Unreleased" must never be the section that documents the version being published.
79
+ if (/^##\s+unreleased\b/i.test(match)) {
80
+ fail(`the section naming ${version} is the Unreleased heading — retitle it before publishing`);
81
+ }
82
+
83
+ console.log(`assert-changelog-version: OK — ${version} is documented ("${match.trim()}")`);
84
+ }
85
+
86
+ main();
87
+
88
+ module.exports = { headingNamesVersion, PKG_PATH, CHANGELOG_PATH };
@@ -25,13 +25,33 @@
25
25
  * rises anyway, because a semantics floor is about what the CLI may RUN against, not about what
26
26
  * it happens to import today — a fingerprint that disagrees with the server is exactly the class
27
27
  * of difference this gate exists to refuse.)
28
+ * 5.0.0: floor raised to 12 (requireConditionalWrite now FIRES on an ordinary mutation. Through
29
+ * 11.0.1 the policy gated on isWriteStyleCall, which answers "did the caller supply a both-sides
30
+ * snapshot" — the right question for FRESHNESS, which it was written for, and the wrong one for
31
+ * atomicity. So a mutation carrying artifacts[] with non-empty before AND after classified as
32
+ * not-write-style and the demand never fired: reproduced against the published 11.0.1 tarball,
33
+ * where the side effect RAN with enforced:true and conditional_write:"not_reported". It bites on
34
+ * the NORMAL path, not an exotic config, because the tool-registry defaultBinder lifts
35
+ * old_string/new_string and edits[] into exactly that shape — so an agent editing a contract
36
+ * artifact under ENFORCING_STRICT wrote unconditionally. 12.0.0 adds isMutatingCall and gates on
37
+ * it; ConditionalWriteBasis gains a required `mutating` field. COST MEASURED AS ZERO, the same
38
+ * way the 11-raise was: the CLI imports only deployGate, asVerifiedDeployReceiptView,
39
+ * DEPLOY_RECEIPT_VIEW_SPEC and matchGlob, whose implementing modules (deploy-gate.js,
40
+ * resolver-glob.js, deploy-receipt-token.js) are BYTE-IDENTICAL 11.0.1 -> 12.0.0; the CLI
41
+ * constructs no ConditionalWriteBasis and never sets requireConditionalWrite, so the one
42
+ * type-level break cannot reach it — it is JS besides. Suite on 12.0.0: 556/554/2, identical to
43
+ * 11.0.0 with a byte-identical failure set; the only edits the raise needed were this gate's own
44
+ * two value-pins in assert-guard-major-coverage.test.js, which exist to move with each recorded
45
+ * raise, and no product code. The floor rises anyway, because a semantics floor is about
46
+ * what the CLI may RUN against: a guard that silently skips the atomicity demand on an ordinary
47
+ * edit is exactly the class of difference this gate exists to refuse.)
28
48
  */
29
49
 
30
50
  const fs = require('fs');
31
51
  const path = require('path');
32
52
 
33
53
  /** Minimum acceptable major for @coderifts/agent-guard in THIS package (the CLI). */
34
- const MIN_GUARD_MAJOR = 11;
54
+ const MIN_GUARD_MAJOR = 12;
35
55
  const DEP = '@coderifts/agent-guard';
36
56
 
37
57
  /**
@@ -91,6 +111,18 @@ const CHECKOUTS = [
91
111
  * 11 makes the bump neither easier nor harder — the blocker is unchanged and the deferral still
92
112
  * holds for exactly the reason above, not for a residual shape that moved.
93
113
  *
114
+ *
115
+ * WHAT THE RESOLVED-RANGE CHECK SAYS ABOUT THIS ENTRY: NOTHING, and that is the correct
116
+ * answer rather than a lucky one. The check added for the 5.0.0 finding grades whether the
117
+ * DECLARED range and the RESOLVED install AGREE. The root declares ^3.0.0 and resolves 3.0.0,
118
+ * so they agree exactly and it reports "in range". The root's gap is not drift between its
119
+ * manifest and its tree — it is a deliberately LOW FLOOR, and a floor is a human decision this
120
+ * gate records rather than derives. That decision lives in `floor: 3` and the reason above.
121
+ *
122
+ * A check that DID flag it — "resolved major must equal the newest published major" — was
123
+ * considered and rejected: it would force exactly the bump the paragraphs above say must not
124
+ * be forced before the residual-precedence reconciliation. The gate must not legislate a
125
+ * decision it is only supposed to remember.
94
126
  * WHAT THE BUMP ROUND MUST DECIDE: whether the guard should emit all holding residuals (then
95
127
  * the app consumes gateOut.residuals and collectBindingResiduals is deleted), or whether the
96
128
  * single-slot shape is deliberate (then the app keeps its collector and the duplication is
@@ -148,6 +180,94 @@ function resolvedVersionFor(startDir) {
148
180
  return null;
149
181
  }
150
182
 
183
+ /**
184
+ * RESOLVED-VERSION GRADING — the half the floor check cannot do.
185
+ *
186
+ * WHY THIS EXISTS. coderifts@5.0.0 published declaring `@coderifts/agent-guard: ^11.0.0` while
187
+ * guard 12.0.0 was already out carrying a semantics break (requireConditionalWrite began firing on
188
+ * ordinary mutations). This gate PASSED, and it was not wrong to: it graded the declared floor
189
+ * (11 >= 11) and the resolved version (11.0.0 >= 11) against a constant a human maintains by hand.
190
+ * Both facts were true. Both were stale. A gate that can only compare against the last floor
191
+ * someone remembered to raise cannot notice a major it has never been told about.
192
+ *
193
+ * So the constant is not the whole gate. What CAN be checked without a registry call is whether
194
+ * the tree and the manifest AGREE — and they must agree in both directions:
195
+ *
196
+ * · resolved BELOW the declared range → a stale install. The suite measured an older guard than
197
+ * the one we ship against, so every "cost measured as zero" conclusion is about the wrong
198
+ * package.
199
+ * · resolved ABOVE the declared range → the more dangerous direction, and the one that produced
200
+ * this finding's shape. Staging a NEWER guard into node_modules to measure a raise, then
201
+ * publishing with the old range, means the suite proved something adopters never receive: they
202
+ * resolve the range, not our node_modules. A green suite over a version we do not declare is a
203
+ * false confidence, not a measurement.
204
+ *
205
+ * NOT RESOLVED IS A FAILURE, not a pass. The previous code returned a success line reading
206
+ * "not resolved in tree" — "we could not look" recorded as "we looked and it is fine", which is
207
+ * the exact fail-open class this repository refuses elsewhere (a 404 on the source_ref anchor is
208
+ * deliberately on the FAIL side for the same reason). A publish gate that verifies nothing must
209
+ * say so by failing.
210
+ *
211
+ * WHAT THIS STILL CANNOT DO, named rather than implied: it makes no network call, so it cannot
212
+ * know that a newer major exists. Raising the floor stays a human act with a recorded reason. This
213
+ * check only guarantees that what we measured is what we declare.
214
+ */
215
+
216
+ /** Parse "1.2.3" (ignoring any prerelease/build suffix) to [1,2,3], or null. */
217
+ function parseVersion(v) {
218
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(String(v || '').trim());
219
+ return m ? [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)] : null;
220
+ }
221
+
222
+ /** -1 / 0 / 1 over [maj,min,pat] triples. */
223
+ function compareVersions(a, b) {
224
+ for (let i = 0; i < 3; i += 1) {
225
+ if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
226
+ }
227
+ return 0;
228
+ }
229
+
230
+ /**
231
+ * Inclusive lower bound and EXCLUSIVE upper bound for the range forms this gate already accepts.
232
+ * Deliberately dependency-free — the rest of this file is, and a publish gate that needs an
233
+ * install to run is a gate that stops running.
234
+ *
235
+ * ^1.2.3 -> [1.2.3, 2.0.0) ~1.2.3 -> [1.2.3, 1.3.0) >=1.2.3 -> [1.2.3, inf)
236
+ * 1.2.3 -> [1.2.3, 1.2.4) 1.x -> [1.0.0, 2.0.0)
237
+ * Anything else -> null (caller treats an unparseable range as unverifiable, never as satisfied).
238
+ */
239
+ function rangeBounds(range) {
240
+ const s = String(range || '').trim();
241
+ const m = /^(\^|~|>=|=)?\s*(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?/.exec(s);
242
+ if (!m) return null;
243
+ const op = m[1] || '';
244
+ const maj = parseInt(m[2], 10);
245
+ const minRaw = m[3];
246
+ const patRaw = m[4];
247
+ const wild = (t) => t === undefined || t === 'x' || t === '*';
248
+ const min = wild(minRaw) ? 0 : parseInt(minRaw, 10);
249
+ const pat = wild(patRaw) ? 0 : parseInt(patRaw, 10);
250
+ const lower = [maj, min, pat];
251
+ if (wild(minRaw)) return { lower, upper: [maj + 1, 0, 0] }; // 1.x
252
+ if (op === '^') return { lower, upper: [maj + 1, 0, 0] };
253
+ if (op === '~') return { lower, upper: [maj, min + 1, 0] };
254
+ if (op === '>=') return { lower, upper: null };
255
+ if (wild(patRaw)) return { lower, upper: [maj, min + 1, 0] }; // 1.2.x
256
+ return { lower, upper: [maj, min, pat + 1] }; // exact
257
+ }
258
+
259
+ /**
260
+ * @returns {'in'|'below'|'above'|'unknown'} where `version` sits relative to `range`.
261
+ */
262
+ function versionVsRange(version, range) {
263
+ const v = parseVersion(version);
264
+ const b = rangeBounds(range);
265
+ if (!v || !b) return 'unknown';
266
+ if (compareVersions(v, b.lower) < 0) return 'below';
267
+ if (b.upper && compareVersions(v, b.upper) >= 0) return 'above';
268
+ return 'in';
269
+ }
270
+
151
271
  /** Check ONE checkout. Returns a line for the summary; calls fail() and exits on a violation. */
152
272
  function checkCheckout(c) {
153
273
  if (!fs.existsSync(c.pkgPath)) {
@@ -173,7 +293,15 @@ function checkCheckout(c) {
173
293
 
174
294
  const resolved = resolvedVersionFor(path.dirname(c.pkgPath));
175
295
  if (!resolved) {
176
- return `${c.label}: declared ${range} (floor ${floor} >= ${c.floor}); not resolved in tree`;
296
+ // FAIL-CLOSED, with NO opt-out. An `allowUnresolved` flag was written and then removed on
297
+ // purpose: the first time this fires in CI the cheapest response is to set the flag, and the
298
+ // hole is back with a config line to justify it. If a checkout ever legitimately ships
299
+ // uninstalled, add the exemption THEN, with the measurement that earned it.
300
+ fail(
301
+ `${c.label}: ${DEP} is declared ${JSON.stringify(range)} but is NOT RESOLVABLE from `
302
+ + `${path.dirname(c.pkgPath)}. This gate cannot verify what will ship, and an unverified `
303
+ + 'publish is not a verified one. Install dependencies for this checkout and re-run.',
304
+ );
177
305
  }
178
306
  const rm = /^(\d+)/.exec(resolved);
179
307
  const rMajor = rm ? parseInt(rm[1], 10) : null;
@@ -183,7 +311,32 @@ function checkCheckout(c) {
183
311
  if (rMajor < floor) {
184
312
  fail(`${c.label}: resolved ${DEP}@${resolved} major ${rMajor} is below declared range floor ${floor}`);
185
313
  }
186
- return `${c.label}: declared ${range} (floor ${floor} >= ${c.floor}); resolved ${resolved}`;
314
+
315
+ // THE DECLARED RANGE AND THE INSTALLED TREE MUST AGREE — both directions, full precision.
316
+ const where = versionVsRange(resolved, range);
317
+ if (where === 'below') {
318
+ fail(
319
+ `${c.label}: resolved ${DEP}@${resolved} is BELOW the declared range ${JSON.stringify(range)} `
320
+ + '— a stale install. Every cost measured against this tree describes an older guard than the '
321
+ + 'one this package declares. Run npm install for this checkout and re-measure.',
322
+ );
323
+ }
324
+ if (where === 'above') {
325
+ fail(
326
+ `${c.label}: resolved ${DEP}@${resolved} is ABOVE the declared range ${JSON.stringify(range)}. `
327
+ + 'The tree runs a guard this package does not declare, so the suite measured a version '
328
+ + 'adopters will never receive — they resolve the range, not our node_modules. Either raise '
329
+ + `the declared range to admit ${resolved} (and record WHY in the history block), or remove `
330
+ + 'the staged install.',
331
+ );
332
+ }
333
+ if (where === 'unknown') {
334
+ fail(
335
+ `${c.label}: cannot decide whether resolved ${DEP}@${resolved} satisfies declared range `
336
+ + `${JSON.stringify(range)} — unparseable. A range this gate cannot grade must not ship.`,
337
+ );
338
+ }
339
+ return `${c.label}: declared ${range} (floor ${floor} >= ${c.floor}); resolved ${resolved} (in range)`;
187
340
  }
188
341
 
189
342
  function main() {
@@ -206,4 +359,7 @@ function main() {
206
359
  // floorMajorFromRange without triggering a process.exit.
207
360
  if (require.main === module) main();
208
361
 
209
- module.exports = { CHECKOUTS, floorMajorFromRange, MIN_GUARD_MAJOR, DEP };
362
+ module.exports = {
363
+ CHECKOUTS, floorMajorFromRange, MIN_GUARD_MAJOR, DEP,
364
+ parseVersion, compareVersions, rangeBounds, versionVsRange, resolvedVersionFor,
365
+ };