coderifts 4.11.0 → 5.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 +59 -0
- package/LICENSE +21 -0
- package/dist/cli.js +114006 -96673
- package/package.json +2 -2
- package/scripts/assert-guard-major.js +145 -63
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coderifts",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.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",
|
|
@@ -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": "^11.0.0",
|
|
49
49
|
"chalk": "^4.1.2",
|
|
50
50
|
"cli-table3": "^0.6.4",
|
|
51
51
|
"commander": "^12.0.0",
|
|
@@ -12,15 +12,98 @@
|
|
|
12
12
|
* Why: evening audit 2.4 — CLI sat on ^1.6.0 while published guard is 6.x.
|
|
13
13
|
* 4.0.0: floor raised to 8 (deployGate inescapable_deploy includes fingerprint rebound).
|
|
14
14
|
* 4.4.1: floor raised to 9 (TOKEN / VERIFIED-VIEW; bare currently_authorized is unverified_receipt_view).
|
|
15
|
+
* 4.12.0: floor raised to 10 (ENFORCING_STRICT requires the execution chain — a strict
|
|
16
|
+
* composition that built under 9.x refuses to construct under 10.x, so a CLI that documents or
|
|
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.)
|
|
15
28
|
*/
|
|
16
29
|
|
|
17
30
|
const fs = require('fs');
|
|
18
31
|
const path = require('path');
|
|
19
32
|
|
|
20
|
-
/** Minimum acceptable major for @coderifts/agent-guard (
|
|
21
|
-
const MIN_GUARD_MAJOR =
|
|
33
|
+
/** Minimum acceptable major for @coderifts/agent-guard in THIS package (the CLI). */
|
|
34
|
+
const MIN_GUARD_MAJOR = 11;
|
|
22
35
|
const DEP = '@coderifts/agent-guard';
|
|
23
36
|
|
|
37
|
+
/**
|
|
38
|
+
* EVERY checkout that declares the guard — not just this one (roadmap 1058).
|
|
39
|
+
*
|
|
40
|
+
* WHY THIS TABLE EXISTS: this gate checked packages/cli only, while the app ROOT — where
|
|
41
|
+
* PRODUCTION uses the guard (src/mergegate/webhook-integration.js imports gateDecision) — sat on
|
|
42
|
+
* ^3.0.0 against a published 10.0.0. A seven-major gap survived precisely because the gate did not
|
|
43
|
+
* look there. A gate that inspects one of two call sites is not a gate.
|
|
44
|
+
*
|
|
45
|
+
* ONE FILE, PER-CHECKOUT FLOORS — chosen over a separate sibling check so there is ONE history
|
|
46
|
+
* section to record the next raise in. Two files would mean two floors with no structural reason
|
|
47
|
+
* they are ever raised together, which is the drift that produced this finding.
|
|
48
|
+
*
|
|
49
|
+
* Floors differ ON PURPOSE. A single shared floor would fail immediately and truthfully, but the
|
|
50
|
+
* fix for that is a dependency bump this gate must not force: see the app-root entry.
|
|
51
|
+
*/
|
|
52
|
+
const CHECKOUTS = [
|
|
53
|
+
{
|
|
54
|
+
id: 'cli',
|
|
55
|
+
label: 'packages/cli',
|
|
56
|
+
pkgPath: path.join(__dirname, '..', 'package.json'),
|
|
57
|
+
floor: MIN_GUARD_MAJOR,
|
|
58
|
+
why: 'CLI floor history is recorded in the module docblock above.',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'app-root',
|
|
62
|
+
label: 'coderifts-app (root)',
|
|
63
|
+
pkgPath: path.join(__dirname, '..', '..', '..', 'package.json'),
|
|
64
|
+
floor: 3,
|
|
65
|
+
/**
|
|
66
|
+
* DELIBERATE, MEASURED, AND LOWER THAN THE CLI'S — recorded so it is a decision, not a default.
|
|
67
|
+
*
|
|
68
|
+
* 2026-08-26: bumping the root to ^10 breaks three merge-gate tests
|
|
69
|
+
* (test/mergegate-webhook.test.js: 46/46 on 3.0.0, 43/46 on 10.x). The app's own comment at
|
|
70
|
+
* webhook-integration.js:336-339 records why 3.x has been SAFE rather than merely unnoticed:
|
|
71
|
+
* the app ANDs three conditions (protection_inescapable && change_set_rebound && app_bound) so
|
|
72
|
+
* a stale package can never fail-open. That AND is load-bearing — measured against 3.0.0,
|
|
73
|
+
* gateDecision claims inescapable_merge:true even when required_check_app_bound is false or
|
|
74
|
+
* absent; the published claim is correct only because the app re-derives it.
|
|
75
|
+
*
|
|
76
|
+
* RE-MEASURED 2026-08-26 against 11.0.0, and TWO THINGS IN THE OLD NOTE WERE WRONG:
|
|
77
|
+
*
|
|
78
|
+
* 1. It said 10.0.0 "returns residuals: []". It does not. Given the correct input shape it
|
|
79
|
+
* returns ['required_check_app_not_bound'] / ['required_check_app_binding_unknown'] — the
|
|
80
|
+
* app's own vocabulary. The earlier reading passed protection.enforcement_state; the field
|
|
81
|
+
* the gate reads is protection.enforcement, so the gate saw no protection at all.
|
|
82
|
+
* 2. The real difference is PRECEDENCE, not absence. The guard emits exactly ONE residual and
|
|
83
|
+
* short-circuits on the enforcement gap, so where two gaps hold at once it reports
|
|
84
|
+
* 'admin_bypass_open' and the app expects 'required_check_app_binding_unknown'. The app's
|
|
85
|
+
* collectBindingResiduals emits EVERY residual that holds — its docstring says so: "the
|
|
86
|
+
* pure gate's published shape is a single honesty slot ... the second was suppressed".
|
|
87
|
+
* That is why the app duplicates rather than consumes, and it is the whole reconciliation.
|
|
88
|
+
*
|
|
89
|
+
* 11.0.0 vs 10.0.0: NO CHANGE. Residual output is byte-identical across all three app-binding
|
|
90
|
+
* cases, and 11.0.0 touched only computeBundleFingerprint, which this path does not import. So
|
|
91
|
+
* 11 makes the bump neither easier nor harder — the blocker is unchanged and the deferral still
|
|
92
|
+
* holds for exactly the reason above, not for a residual shape that moved.
|
|
93
|
+
*
|
|
94
|
+
* WHAT THE BUMP ROUND MUST DECIDE: whether the guard should emit all holding residuals (then
|
|
95
|
+
* the app consumes gateOut.residuals and collectBindingResiduals is deleted), or whether the
|
|
96
|
+
* single-slot shape is deliberate (then the app keeps its collector and the duplication is
|
|
97
|
+
* documented as intended rather than left looking accidental). Do not bump before that choice.
|
|
98
|
+
*
|
|
99
|
+
* So the floor is pinned at 3 to stop it drifting LOWER unnoticed. Raise this number in that
|
|
100
|
+
* round and record the reason on the line below, in the style of the CLI history above.
|
|
101
|
+
*/
|
|
102
|
+
why: 'root pinned at 3 pending the gateDecision residual-PRECEDENCE reconciliation (see comment; '
|
|
103
|
+
+ 're-measured against 11.0.0 — unchanged from 10.0.0).',
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
|
|
24
107
|
function fail(msg) {
|
|
25
108
|
console.error(`assert-guard-major: FAIL — ${msg}`);
|
|
26
109
|
process.exit(1);
|
|
@@ -47,81 +130,80 @@ function floorMajorFromRange(range) {
|
|
|
47
130
|
return parseInt(m[1], 10);
|
|
48
131
|
}
|
|
49
132
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
133
|
+
/** Resolve the installed guard version for a checkout dir, or null. */
|
|
134
|
+
function resolvedVersionFor(startDir) {
|
|
135
|
+
try {
|
|
136
|
+
let dir = path.dirname(require.resolve(DEP, { paths: [startDir] }));
|
|
137
|
+
for (let i = 0; i < 6; i += 1) {
|
|
138
|
+
const cand = path.join(dir, 'package.json');
|
|
139
|
+
if (fs.existsSync(cand)) {
|
|
140
|
+
const rp = JSON.parse(fs.readFileSync(cand, 'utf8'));
|
|
141
|
+
if (rp.name === DEP) return rp.version;
|
|
142
|
+
}
|
|
143
|
+
const parent = path.dirname(dir);
|
|
144
|
+
if (parent === dir) break;
|
|
145
|
+
dir = parent;
|
|
146
|
+
}
|
|
147
|
+
} catch (_) { /* not installed here */ }
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Check ONE checkout. Returns a line for the summary; calls fail() and exits on a violation. */
|
|
152
|
+
function checkCheckout(c) {
|
|
153
|
+
if (!fs.existsSync(c.pkgPath)) {
|
|
154
|
+
fail(`${c.label}: package.json not found at ${c.pkgPath}`);
|
|
155
|
+
}
|
|
156
|
+
const pkg = JSON.parse(fs.readFileSync(c.pkgPath, 'utf8'));
|
|
53
157
|
const range = pkg.dependencies && pkg.dependencies[DEP];
|
|
54
|
-
if (!range) fail(`${DEP} missing from dependencies`);
|
|
158
|
+
if (!range) fail(`${c.label}: ${DEP} missing from dependencies`);
|
|
55
159
|
|
|
56
160
|
const floor = floorMajorFromRange(range);
|
|
57
161
|
if (floor == null) {
|
|
58
162
|
fail(
|
|
59
|
-
`${DEP} range ${JSON.stringify(range)} has no parseable major floor `
|
|
60
|
-
+ `(wildcards/non-semver not allowed; need e.g. ^${
|
|
163
|
+
`${c.label}: ${DEP} range ${JSON.stringify(range)} has no parseable major floor `
|
|
164
|
+
+ `(wildcards/non-semver not allowed; need e.g. ^${c.floor}.0.0)`,
|
|
61
165
|
);
|
|
62
166
|
}
|
|
63
|
-
if (floor <
|
|
167
|
+
if (floor < c.floor) {
|
|
64
168
|
fail(
|
|
65
|
-
`${DEP} range ${JSON.stringify(range)} floors at major ${floor}; `
|
|
66
|
-
+ `minimum acceptable major is ${
|
|
169
|
+
`${c.label}: ${DEP} range ${JSON.stringify(range)} floors at major ${floor}; `
|
|
170
|
+
+ `minimum acceptable major for this checkout is ${c.floor} — ${c.why}`,
|
|
67
171
|
);
|
|
68
172
|
}
|
|
69
173
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
path.dirname(require.resolve(`${DEP}`)),
|
|
74
|
-
// package root: require('@coderifts/agent-guard') → dist/cjs/index.js
|
|
75
|
-
'..',
|
|
76
|
-
'..',
|
|
77
|
-
'package.json',
|
|
78
|
-
));
|
|
79
|
-
// From dist/cjs → package root is ../..
|
|
80
|
-
} catch (_) {
|
|
81
|
-
// try alternate resolve
|
|
174
|
+
const resolved = resolvedVersionFor(path.dirname(c.pkgPath));
|
|
175
|
+
if (!resolved) {
|
|
176
|
+
return `${c.label}: declared ${range} (floor ${floor} >= ${c.floor}); not resolved in tree`;
|
|
82
177
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
let dir = path.dirname(require.resolve(DEP));
|
|
88
|
-
for (let i = 0; i < 6; i++) {
|
|
89
|
-
const cand = path.join(dir, 'package.json');
|
|
90
|
-
if (fs.existsSync(cand)) {
|
|
91
|
-
const rp = JSON.parse(fs.readFileSync(cand, 'utf8'));
|
|
92
|
-
if (rp.name === DEP) {
|
|
93
|
-
resolvedVersion = rp.version;
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
const parent = path.dirname(dir);
|
|
98
|
-
if (parent === dir) break;
|
|
99
|
-
dir = parent;
|
|
100
|
-
}
|
|
101
|
-
} catch (err) {
|
|
102
|
-
// Not installed — range check alone is enough for prepublish if install follows.
|
|
103
|
-
ok(`declared ${DEP}@${range} (floor major ${floor} ≥ ${MIN_GUARD_MAJOR}); package not resolved in tree (${err && err.message})`);
|
|
104
|
-
return;
|
|
178
|
+
const rm = /^(\d+)/.exec(resolved);
|
|
179
|
+
const rMajor = rm ? parseInt(rm[1], 10) : null;
|
|
180
|
+
if (rMajor == null || rMajor < c.floor) {
|
|
181
|
+
fail(`${c.label}: resolved ${DEP}@${resolved} major ${rMajor} is below minimum ${c.floor}`);
|
|
105
182
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const rm = /^(\d+)/.exec(resolvedVersion);
|
|
109
|
-
const rMajor = rm ? parseInt(rm[1], 10) : null;
|
|
110
|
-
if (rMajor == null || rMajor < MIN_GUARD_MAJOR) {
|
|
111
|
-
fail(
|
|
112
|
-
`resolved ${DEP}@${resolvedVersion} major ${rMajor} is below minimum ${MIN_GUARD_MAJOR}`,
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
if (rMajor < floor) {
|
|
116
|
-
fail(
|
|
117
|
-
`resolved ${DEP}@${resolvedVersion} major ${rMajor} is below declared range floor ${floor}`,
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
ok(`declared ${DEP}@${range} (floor ${floor}); resolved ${resolvedVersion} (major ≥ ${MIN_GUARD_MAJOR})`);
|
|
121
|
-
return;
|
|
183
|
+
if (rMajor < floor) {
|
|
184
|
+
fail(`${c.label}: resolved ${DEP}@${resolved} major ${rMajor} is below declared range floor ${floor}`);
|
|
122
185
|
}
|
|
186
|
+
return `${c.label}: declared ${range} (floor ${floor} >= ${c.floor}); resolved ${resolved}`;
|
|
187
|
+
}
|
|
123
188
|
|
|
124
|
-
|
|
189
|
+
function main() {
|
|
190
|
+
const lines = CHECKOUTS.map(checkCheckout);
|
|
191
|
+
for (const l of lines) ok(l);
|
|
192
|
+
|
|
193
|
+
// DIVERGENCE NOTICE — visible, never fatal. The floors differ deliberately (see CHECKOUTS), so
|
|
194
|
+
// failing here would block a publish over a state we chose. Printing it keeps the gap from
|
|
195
|
+
// going quiet again, which is how it survived seven majors.
|
|
196
|
+
const floors = CHECKOUTS.map((c) => c.floor);
|
|
197
|
+
if (new Set(floors).size > 1) {
|
|
198
|
+
console.log(
|
|
199
|
+
`assert-guard-major: NOTE — checkout floors differ (${CHECKOUTS.map((c) => `${c.id}=${c.floor}`).join(', ')}). `
|
|
200
|
+
+ 'Deliberate; each reason is recorded in CHECKOUTS. Not a failure.',
|
|
201
|
+
);
|
|
202
|
+
}
|
|
125
203
|
}
|
|
126
204
|
|
|
127
|
-
|
|
205
|
+
// Run only when executed directly, so the gate's own tests can import CHECKOUTS /
|
|
206
|
+
// floorMajorFromRange without triggering a process.exit.
|
|
207
|
+
if (require.main === module) main();
|
|
208
|
+
|
|
209
|
+
module.exports = { CHECKOUTS, floorMajorFromRange, MIN_GUARD_MAJOR, DEP };
|