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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderifts",
3
- "version": "4.11.0",
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": "^9.0.0",
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 (published line). */
21
- const MIN_GUARD_MAJOR = 9;
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
- function main() {
51
- const pkgPath = path.join(__dirname, '..', 'package.json');
52
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
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. ^${MIN_GUARD_MAJOR}.0.0)`,
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 < MIN_GUARD_MAJOR) {
167
+ if (floor < c.floor) {
64
168
  fail(
65
- `${DEP} range ${JSON.stringify(range)} floors at major ${floor}; `
66
- + `minimum acceptable major is ${MIN_GUARD_MAJOR}`,
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
- // Optional: resolved install must not lag the declared floor (stale node_modules).
71
- try {
72
- const resolved = require(path.join(
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
- let resolvedVersion = null;
85
- try {
86
- // Walk up from the main entry to package.json (exports block ./package.json).
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
- if (resolvedVersion) {
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
- ok(`declared ${DEP}@${range} (floor major ${floor} ≥ ${MIN_GUARD_MAJOR})`);
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
- main();
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 };