coderifts 4.12.0 → 5.0.1
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 +59 -0
- package/LICENSE +21 -0
- package/dist/cli.js +112268 -96525
- package/package.json +2 -2
- package/scripts/assert-guard-major.js +204 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coderifts",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.1",
|
|
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",
|
|
@@ -45,7 +45,7 @@
|
|
|
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",
|
|
@@ -15,13 +15,43 @@
|
|
|
15
15
|
* 4.12.0: floor raised to 10 (ENFORCING_STRICT requires the execution chain — a strict
|
|
16
16
|
* composition that built under 9.x refuses to construct under 10.x, so a CLI that documents or
|
|
17
17
|
* scaffolds strict must not run against a guard with the older meaning).
|
|
18
|
+
* 4.13.0: floor raised to 11 (computeBundleFingerprint returned a WRONG digest in 10.x — it
|
|
19
|
+
* omitted the artifact count and the whole context block, so on identical inputs it produced
|
|
20
|
+
* sha256:1a0e7470… where the server produced sha256:049650f2…; 11.0.0 delegates to the single
|
|
21
|
+
* canonical crbundle.v1 preimage and matches byte for byte. COST MEASURED AS ZERO, the same way
|
|
22
|
+
* the 10-raise was: the CLI imports only deployGate, asVerifiedDeployReceiptView,
|
|
23
|
+
* DEPLOY_RECEIPT_VIEW_SPEC and matchGlob, none of which 11.0.0 touched, and the suite staged
|
|
24
|
+
* against 11.0.0 returned an identical 532/530/2 with a byte-identical failure set. The floor
|
|
25
|
+
* rises anyway, because a semantics floor is about what the CLI may RUN against, not about what
|
|
26
|
+
* it happens to import today — a fingerprint that disagrees with the server is exactly the class
|
|
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.)
|
|
18
48
|
*/
|
|
19
49
|
|
|
20
50
|
const fs = require('fs');
|
|
21
51
|
const path = require('path');
|
|
22
52
|
|
|
23
53
|
/** Minimum acceptable major for @coderifts/agent-guard in THIS package (the CLI). */
|
|
24
|
-
const MIN_GUARD_MAJOR =
|
|
54
|
+
const MIN_GUARD_MAJOR = 12;
|
|
25
55
|
const DEP = '@coderifts/agent-guard';
|
|
26
56
|
|
|
27
57
|
/**
|
|
@@ -55,17 +85,54 @@ const CHECKOUTS = [
|
|
|
55
85
|
/**
|
|
56
86
|
* DELIBERATE, MEASURED, AND LOWER THAN THE CLI'S — recorded so it is a decision, not a default.
|
|
57
87
|
*
|
|
58
|
-
* 2026-08-26: bumping the root to ^10 breaks three merge-gate tests
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
88
|
+
* 2026-08-26: bumping the root to ^10 breaks three merge-gate tests
|
|
89
|
+
* (test/mergegate-webhook.test.js: 46/46 on 3.0.0, 43/46 on 10.x). The app's own comment at
|
|
90
|
+
* webhook-integration.js:336-339 records why 3.x has been SAFE rather than merely unnoticed:
|
|
91
|
+
* the app ANDs three conditions (protection_inescapable && change_set_rebound && app_bound) so
|
|
92
|
+
* a stale package can never fail-open. That AND is load-bearing — measured against 3.0.0,
|
|
93
|
+
* gateDecision claims inescapable_merge:true even when required_check_app_bound is false or
|
|
94
|
+
* absent; the published claim is correct only because the app re-derives it.
|
|
63
95
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
96
|
+
* RE-MEASURED 2026-08-26 against 11.0.0, and TWO THINGS IN THE OLD NOTE WERE WRONG:
|
|
97
|
+
*
|
|
98
|
+
* 1. It said 10.0.0 "returns residuals: []". It does not. Given the correct input shape it
|
|
99
|
+
* returns ['required_check_app_not_bound'] / ['required_check_app_binding_unknown'] — the
|
|
100
|
+
* app's own vocabulary. The earlier reading passed protection.enforcement_state; the field
|
|
101
|
+
* the gate reads is protection.enforcement, so the gate saw no protection at all.
|
|
102
|
+
* 2. The real difference is PRECEDENCE, not absence. The guard emits exactly ONE residual and
|
|
103
|
+
* short-circuits on the enforcement gap, so where two gaps hold at once it reports
|
|
104
|
+
* 'admin_bypass_open' and the app expects 'required_check_app_binding_unknown'. The app's
|
|
105
|
+
* collectBindingResiduals emits EVERY residual that holds — its docstring says so: "the
|
|
106
|
+
* pure gate's published shape is a single honesty slot ... the second was suppressed".
|
|
107
|
+
* That is why the app duplicates rather than consumes, and it is the whole reconciliation.
|
|
108
|
+
*
|
|
109
|
+
* 11.0.0 vs 10.0.0: NO CHANGE. Residual output is byte-identical across all three app-binding
|
|
110
|
+
* cases, and 11.0.0 touched only computeBundleFingerprint, which this path does not import. So
|
|
111
|
+
* 11 makes the bump neither easier nor harder — the blocker is unchanged and the deferral still
|
|
112
|
+
* holds for exactly the reason above, not for a residual shape that moved.
|
|
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.
|
|
126
|
+
* WHAT THE BUMP ROUND MUST DECIDE: whether the guard should emit all holding residuals (then
|
|
127
|
+
* the app consumes gateOut.residuals and collectBindingResiduals is deleted), or whether the
|
|
128
|
+
* single-slot shape is deliberate (then the app keeps its collector and the duplication is
|
|
129
|
+
* documented as intended rather than left looking accidental). Do not bump before that choice.
|
|
130
|
+
*
|
|
131
|
+
* So the floor is pinned at 3 to stop it drifting LOWER unnoticed. Raise this number in that
|
|
132
|
+
* round and record the reason on the line below, in the style of the CLI history above.
|
|
67
133
|
*/
|
|
68
|
-
why: 'root pinned at 3 pending the gateDecision residual-
|
|
134
|
+
why: 'root pinned at 3 pending the gateDecision residual-PRECEDENCE reconciliation (see comment; '
|
|
135
|
+
+ 're-measured against 11.0.0 — unchanged from 10.0.0).',
|
|
69
136
|
},
|
|
70
137
|
];
|
|
71
138
|
|
|
@@ -113,6 +180,94 @@ function resolvedVersionFor(startDir) {
|
|
|
113
180
|
return null;
|
|
114
181
|
}
|
|
115
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
|
+
|
|
116
271
|
/** Check ONE checkout. Returns a line for the summary; calls fail() and exits on a violation. */
|
|
117
272
|
function checkCheckout(c) {
|
|
118
273
|
if (!fs.existsSync(c.pkgPath)) {
|
|
@@ -138,7 +293,15 @@ function checkCheckout(c) {
|
|
|
138
293
|
|
|
139
294
|
const resolved = resolvedVersionFor(path.dirname(c.pkgPath));
|
|
140
295
|
if (!resolved) {
|
|
141
|
-
|
|
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
|
+
);
|
|
142
305
|
}
|
|
143
306
|
const rm = /^(\d+)/.exec(resolved);
|
|
144
307
|
const rMajor = rm ? parseInt(rm[1], 10) : null;
|
|
@@ -148,7 +311,32 @@ function checkCheckout(c) {
|
|
|
148
311
|
if (rMajor < floor) {
|
|
149
312
|
fail(`${c.label}: resolved ${DEP}@${resolved} major ${rMajor} is below declared range floor ${floor}`);
|
|
150
313
|
}
|
|
151
|
-
|
|
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)`;
|
|
152
340
|
}
|
|
153
341
|
|
|
154
342
|
function main() {
|
|
@@ -171,4 +359,7 @@ function main() {
|
|
|
171
359
|
// floorMajorFromRange without triggering a process.exit.
|
|
172
360
|
if (require.main === module) main();
|
|
173
361
|
|
|
174
|
-
module.exports = {
|
|
362
|
+
module.exports = {
|
|
363
|
+
CHECKOUTS, floorMajorFromRange, MIN_GUARD_MAJOR, DEP,
|
|
364
|
+
parseVersion, compareVersions, rangeBounds, versionVsRange, resolvedVersionFor,
|
|
365
|
+
};
|