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 +86 -1
- package/dist/cli.js +279 -35
- package/package.json +3 -3
- package/scripts/assert-changelog-version.js +88 -0
- package/scripts/assert-guard-major.js +160 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
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: "
|
|
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": "^
|
|
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
|
|
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}${
|
|
66376
|
-
const ky = `${y.type}${
|
|
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(
|
|
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(
|
|
66392
|
-
return `sha256:${sha256hex(parts.join(
|
|
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
|
|
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}${
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
75275
|
-
exports2.
|
|
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
|
|
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
|
|
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": "
|
|
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": "^
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
|
362
|
+
module.exports = {
|
|
363
|
+
CHECKOUTS, floorMajorFromRange, MIN_GUARD_MAJOR, DEP,
|
|
364
|
+
parseVersion, compareVersions, rangeBounds, versionVsRange, resolvedVersionFor,
|
|
365
|
+
};
|