@geonosis/ratchet 1.2.0 → 1.4.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/README.md +28 -1
- package/dist/{chunk-LSYVFUP4.js → chunk-ESN6ULZZ.js} +300 -90
- package/dist/cli.js +97 -2
- package/dist/index.d.ts +131 -2
- package/dist/index.js +15 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -172,10 +172,17 @@ lines) is refused **even on a clean exit**. `npm ERR!` is not stripped — that
|
|
|
172
172
|
command never ran, and it must never read as zero. Unset is the default and changes nothing, so a repo that never opts in keeps exactly
|
|
173
173
|
the behaviour it had. Valid values: `unix`, `agent`, `default`.
|
|
174
174
|
|
|
175
|
-
When a number **grew**,
|
|
175
|
+
When a number **grew**, up to ten lines of that counter's command output print under the
|
|
176
176
|
`<-- REGRESSED` line, indented — so the report says what grew, not only that something did. A
|
|
177
177
|
counter that reads a file rather than running a command (`lawLineCount`) prints nothing extra.
|
|
178
178
|
|
|
179
|
+
The lines are the ones the counter says its number came from, not the tail of the run. `oxlintRule`
|
|
180
|
+
cites the findings attributed to ITS rule; `oxlintErrors` cites errors and `oxlintWarnings`
|
|
181
|
+
warnings. The tail was wrong for exactly the run that needs it most: a consumer's +2 on one rule
|
|
182
|
+
printed two unrelated **warnings** as its evidence, because those were the last lines oxlint wrote
|
|
183
|
+
and the two real errors sat higher up. A counter written outside this package that names nothing —
|
|
184
|
+
or that recognises nothing in a run — falls back to the tail, as before.
|
|
185
|
+
|
|
179
186
|
### `testFailures` reads the runner's summary, never the exit code
|
|
180
187
|
|
|
181
188
|
The counter looks for the runner's own count and **refuses when it cannot parse one**. It never
|
|
@@ -281,6 +288,19 @@ been shown to work — and this one guards the running time of every other gate.
|
|
|
281
288
|
nothing more. If a child command needs `NODE_OPTIONS` — a TypeScript shim, a loader — put it on
|
|
282
289
|
the script that invokes `geonosis-ratchet`, not on the counter's own line, and not only in your
|
|
283
290
|
interactive shell.
|
|
291
|
+
- **`NODE_OPTIONS` in the parent script + a counter that shells out to `pnpm` = the counter dies.**
|
|
292
|
+
The nested `pnpm` INHERITS the option. dielime preloads a TypeScript-5 shim through
|
|
293
|
+
`NODE_OPTIONS` in its `lint` script; run the ratchet from inside that script and the nested pnpm
|
|
294
|
+
goes looking for a `.pnpmfile.mjs` that is not there and exits non-zero. The ratchet refuses —
|
|
295
|
+
correctly, a command that cannot run is never a silent zero — but nothing in the message is near
|
|
296
|
+
the cause. Run the ratchet outside that script, or unset `NODE_OPTIONS` for the nested call:
|
|
297
|
+
|
|
298
|
+
```json
|
|
299
|
+
{ "counter": "archViolations", "command": "env -u NODE_OPTIONS pnpm --silent verify:arch" }
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
`geonosis-doctor --only drift` asks for this shape by name: a manifest script carrying
|
|
303
|
+
`NODE_OPTIONS` beside a counter whose command shells to `pnpm`.
|
|
284
304
|
- **A path in a counter's command must be absolute, or `--prove` cannot run it.** A probe runs in a
|
|
285
305
|
scratch directory with none of your repo in it, so a loader or shim named relatively — `node
|
|
286
306
|
--import ./scripts/ts5-shim.mjs …` — resolves to nothing there and the counter comes back
|
|
@@ -423,3 +443,10 @@ npx geonosis-doctor --baseline-against origin/main --strict
|
|
|
423
443
|
```
|
|
424
444
|
|
|
425
445
|
Apache-2.0.
|
|
446
|
+
|
|
447
|
+
## The pairing rule (#136)
|
|
448
|
+
|
|
449
|
+
A scoped fast tier (changed files only) is safe exactly when the full tier is TOTAL: every hard cap
|
|
450
|
+
— max-lines, bundle bytes, suppression counts — needs a full-tier counter watching the whole tree,
|
|
451
|
+
or it is a cap in prose that a file can sit over indefinitely. `oxlintRule`, `suppressionCount`,
|
|
452
|
+
`bundleBytes` and friends exist to be that counter.
|
|
@@ -1,13 +1,76 @@
|
|
|
1
|
+
// src/core/envelope.ts
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
var ENVELOPES_DIR = ".geonosis/envelopes";
|
|
6
|
+
var envelopePath = (root, tool) => join(root, ENVELOPES_DIR, `${tool}.json`);
|
|
7
|
+
var UnbalancedEnvelope = class extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "UnbalancedEnvelope";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var isCount = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
14
|
+
var unbalancedMessage = (envelope, next) => `${envelope.tool}: considered ${envelope.considered} but accounts for ${envelope.read + envelope.refused.length + envelope.excused.length} \u2014 ${envelope.read} read + ${envelope.refused.length} refused + ${envelope.excused.length} excused. A run that has lost count of its own inputs cannot say what it measured, so no verdict was rendered and no envelope was written. Next: ${next}`;
|
|
15
|
+
var writeEnvelope = ({
|
|
16
|
+
envelope,
|
|
17
|
+
next,
|
|
18
|
+
root
|
|
19
|
+
}) => {
|
|
20
|
+
if (envelope.tool.trim() === "") {
|
|
21
|
+
throw new UnbalancedEnvelope(
|
|
22
|
+
`an envelope with no tool name cannot be filed or reported against. Next: ${next}`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
if (envelope.version.trim() === "") {
|
|
26
|
+
throw new UnbalancedEnvelope(
|
|
27
|
+
`${envelope.tool}: an envelope that cannot name the build that wrote it dates nothing, and a stale one reads exactly like a fresh one. Next: ${next}`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
if (!isCount(envelope.considered) || !isCount(envelope.read)) {
|
|
31
|
+
throw new UnbalancedEnvelope(
|
|
32
|
+
`${envelope.tool}: considered ${envelope.considered} and read ${envelope.read} \u2014 a census is a whole number of things, and arithmetic over anything else balances by accident. Next: ${next}`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (envelope.considered !== envelope.read + envelope.refused.length + envelope.excused.length) {
|
|
36
|
+
throw new UnbalancedEnvelope(unbalancedMessage(envelope, next));
|
|
37
|
+
}
|
|
38
|
+
const at = envelopePath(root, envelope.tool);
|
|
39
|
+
mkdirSync(dirname(at), { recursive: true });
|
|
40
|
+
writeFileSync(at, `${JSON.stringify(envelope, void 0, 2)}
|
|
41
|
+
`);
|
|
42
|
+
return at;
|
|
43
|
+
};
|
|
44
|
+
var UNKNOWN = "unknown";
|
|
45
|
+
var versionIn = (dir) => {
|
|
46
|
+
try {
|
|
47
|
+
const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
48
|
+
return typeof manifest.version === "string" ? manifest.version : void 0;
|
|
49
|
+
} catch {
|
|
50
|
+
return void 0;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var versionOf = (moduleUrl) => {
|
|
54
|
+
let dir = dirname(fileURLToPath(moduleUrl));
|
|
55
|
+
for (; ; ) {
|
|
56
|
+
const found = versionIn(dir);
|
|
57
|
+
if (found !== void 0) return found;
|
|
58
|
+
const up = dirname(dir);
|
|
59
|
+
if (up === dir) return UNKNOWN;
|
|
60
|
+
dir = up;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
1
64
|
// src/core/lock.ts
|
|
2
65
|
import { randomUUID } from "crypto";
|
|
3
|
-
import { linkSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
66
|
+
import { linkSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
4
67
|
import { homedir } from "os";
|
|
5
|
-
import { dirname, join } from "path";
|
|
6
|
-
var heavyLockPath = () => process.env.GEONOSIS_HEAVY_LOCK ??
|
|
68
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
69
|
+
var heavyLockPath = () => process.env.GEONOSIS_HEAVY_LOCK ?? join2(homedir(), ".cache", "geonosis", "heavy.lock");
|
|
7
70
|
var sleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
8
71
|
var holderOf = (path) => {
|
|
9
72
|
try {
|
|
10
|
-
const parsed = JSON.parse(
|
|
73
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
11
74
|
return typeof parsed.pid === "number" ? {
|
|
12
75
|
cwd: parsed.cwd ?? "somewhere",
|
|
13
76
|
pid: parsed.pid,
|
|
@@ -31,7 +94,7 @@ var heldFor = (holder) => {
|
|
|
31
94
|
return `${Math.round((Date.now() - since) / 1e3)}s`;
|
|
32
95
|
};
|
|
33
96
|
var write = (path) => {
|
|
34
|
-
|
|
97
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
35
98
|
const mine = {
|
|
36
99
|
cwd: process.cwd(),
|
|
37
100
|
pid: process.pid,
|
|
@@ -39,7 +102,7 @@ var write = (path) => {
|
|
|
39
102
|
};
|
|
40
103
|
const staging = `${path}.${process.pid}.${randomUUID()}`;
|
|
41
104
|
try {
|
|
42
|
-
|
|
105
|
+
writeFileSync2(staging, JSON.stringify(mine));
|
|
43
106
|
linkSync(staging, path);
|
|
44
107
|
return true;
|
|
45
108
|
} catch (error) {
|
|
@@ -89,7 +152,7 @@ var acquireExclusive = async ({
|
|
|
89
152
|
};
|
|
90
153
|
|
|
91
154
|
// src/core/config.ts
|
|
92
|
-
import { existsSync, readFileSync as
|
|
155
|
+
import { existsSync, readFileSync as readFileSync3 } from "fs";
|
|
93
156
|
import { resolve } from "path";
|
|
94
157
|
var CONFIG_FILE = "geonosis.ratchet.json";
|
|
95
158
|
var keyOf = (entry) => entry.key ?? entry.counter;
|
|
@@ -98,7 +161,7 @@ var loadConfig = (cwd) => {
|
|
|
98
161
|
if (!existsSync(path)) {
|
|
99
162
|
throw new Error(`no ${CONFIG_FILE} in ${cwd} \u2014 the ratchet has nothing to count`);
|
|
100
163
|
}
|
|
101
|
-
const parsed = JSON.parse(
|
|
164
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
102
165
|
if (!Array.isArray(parsed.counters)) {
|
|
103
166
|
throw new Error(`${CONFIG_FILE} has no "counters" array`);
|
|
104
167
|
}
|
|
@@ -124,6 +187,14 @@ var loadConfig = (cwd) => {
|
|
|
124
187
|
}
|
|
125
188
|
return { baseline: parsed.baseline ?? "gate-baseline.json", counters: parsed.counters };
|
|
126
189
|
};
|
|
190
|
+
var resolveBaseline = (cwd) => {
|
|
191
|
+
try {
|
|
192
|
+
const parsed = JSON.parse(readFileSync3(resolve(cwd, CONFIG_FILE), "utf8"));
|
|
193
|
+
return typeof parsed.baseline === "string" ? parsed.baseline : "gate-baseline.json";
|
|
194
|
+
} catch {
|
|
195
|
+
return "gate-baseline.json";
|
|
196
|
+
}
|
|
197
|
+
};
|
|
127
198
|
|
|
128
199
|
// src/core/types.ts
|
|
129
200
|
var CounterError = class extends Error {
|
|
@@ -136,23 +207,21 @@ var CounterError = class extends Error {
|
|
|
136
207
|
};
|
|
137
208
|
|
|
138
209
|
// src/core/shell.ts
|
|
139
|
-
import {
|
|
210
|
+
import { spawnSync } from "child_process";
|
|
140
211
|
var ANSI = /\[[0-9;]*m/g;
|
|
141
212
|
var runCommand = (cwd, counterId, env) => (command) => {
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
${command}
|
|
145
|
-
) 2>&1`, {
|
|
213
|
+
{
|
|
214
|
+
const run = spawnSync(command, {
|
|
146
215
|
cwd,
|
|
147
216
|
encoding: "utf8",
|
|
148
217
|
env,
|
|
149
218
|
maxBuffer: 64 * 1024 * 1024,
|
|
219
|
+
shell: true,
|
|
150
220
|
stdio: ["ignore", "pipe", "pipe"]
|
|
151
221
|
});
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const failed =
|
|
155
|
-
const output = `${failed.stdout ?? ""}${failed.stderr ?? ""}`.replaceAll(ANSI, "");
|
|
222
|
+
const output = `${run.stdout ?? ""}${run.stderr ?? ""}`.replaceAll(ANSI, "");
|
|
223
|
+
if (run.error === void 0 && (run.status ?? 0) === 0) return { code: 0, output };
|
|
224
|
+
const failed = { status: run.status ?? 1, stderr: "", stdout: "" };
|
|
156
225
|
const code = failed.status ?? -1;
|
|
157
226
|
if (code === 126 || code === 127 || code === -1) {
|
|
158
227
|
throw new CounterError(
|
|
@@ -166,13 +235,13 @@ ${output.trim()}`
|
|
|
166
235
|
};
|
|
167
236
|
|
|
168
237
|
// src/core/prove.ts
|
|
169
|
-
import { existsSync as existsSync2, mkdtempSync, rmSync as rmSync2 } from "fs";
|
|
238
|
+
import { existsSync as existsSync2, mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
170
239
|
import { tmpdir } from "os";
|
|
171
|
-
import { delimiter, dirname as
|
|
240
|
+
import { delimiter, dirname as dirname4, join as join3, resolve as resolve3 } from "path";
|
|
172
241
|
|
|
173
242
|
// src/core/exclusive.ts
|
|
174
243
|
import { spawn } from "child_process";
|
|
175
|
-
import { dirname as
|
|
244
|
+
import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
176
245
|
var MARK = /^exclusive-hold (start|end) (\d+)$/gm;
|
|
177
246
|
var KEY = "exclusive";
|
|
178
247
|
var spanOf = (output) => {
|
|
@@ -187,7 +256,7 @@ var hold = (cli, holdMs, lockPath) => new Promise((done) => {
|
|
|
187
256
|
process.execPath,
|
|
188
257
|
[cli, "--exclusive", "--exclusive-timeout", "60", "--hold", String(holdMs)],
|
|
189
258
|
{
|
|
190
|
-
cwd:
|
|
259
|
+
cwd: dirname3(resolve2(cli)),
|
|
191
260
|
env: { ...process.env, GEONOSIS_HEAVY_LOCK: lockPath },
|
|
192
261
|
stdio: ["ignore", "pipe", "pipe"]
|
|
193
262
|
}
|
|
@@ -239,9 +308,9 @@ var toolPath = (cwd) => {
|
|
|
239
308
|
const dirs = [];
|
|
240
309
|
let dir = resolve3(cwd);
|
|
241
310
|
for (; ; ) {
|
|
242
|
-
const bin =
|
|
311
|
+
const bin = join3(dir, "node_modules", ".bin");
|
|
243
312
|
if (existsSync2(bin)) dirs.push(bin);
|
|
244
|
-
const parent =
|
|
313
|
+
const parent = dirname4(dir);
|
|
245
314
|
if (parent === dir) break;
|
|
246
315
|
dir = parent;
|
|
247
316
|
}
|
|
@@ -249,7 +318,7 @@ var toolPath = (cwd) => {
|
|
|
249
318
|
};
|
|
250
319
|
var NO_PROBE = "no probe \u2014 a counter nobody has seen read a planted finding has not been shown to measure";
|
|
251
320
|
var oneProofOf = async (counter, key, path, probe) => {
|
|
252
|
-
const dir = mkdtempSync(
|
|
321
|
+
const dir = mkdtempSync(join3(tmpdir(), "geonosis-prove-"));
|
|
253
322
|
try {
|
|
254
323
|
probe.input(dir);
|
|
255
324
|
const command = probe.command?.(dir);
|
|
@@ -316,11 +385,43 @@ var runProve = async ({
|
|
|
316
385
|
if (taken.some((one) => one.verdict !== "proven" && one.verdict !== "skipped")) {
|
|
317
386
|
return { proofs, proven: false };
|
|
318
387
|
}
|
|
388
|
+
const customized = (counter.readingParams ?? []).filter((one) => entry[one] !== void 0);
|
|
389
|
+
if (customized.length > 0) {
|
|
390
|
+
const declared = entry.probe;
|
|
391
|
+
const named = customized.map((one) => `${one} ${JSON.stringify(entry[one])}`).join(", ");
|
|
392
|
+
if (declared === void 0 || typeof declared.sample !== "string" || typeof declared.expect !== "number") {
|
|
393
|
+
proofs.push({
|
|
394
|
+
counter: counter.id,
|
|
395
|
+
key,
|
|
396
|
+
reason: `the configured ${named} is not exercised by this counter's shipped probe \u2014 declare probe: { sample, expect } beside it, a sample this reading must count`,
|
|
397
|
+
verdict: "unproven"
|
|
398
|
+
});
|
|
399
|
+
return { proofs, proven: false };
|
|
400
|
+
}
|
|
401
|
+
const sample = declared.sample;
|
|
402
|
+
const entryProbe = {
|
|
403
|
+
command: () => "cat sample.txt",
|
|
404
|
+
expect: declared.expect,
|
|
405
|
+
input: (dir) => writeFileSync3(join3(dir, "sample.txt"), sample),
|
|
406
|
+
name: "configured reading",
|
|
407
|
+
params: Object.fromEntries(customized.map((one) => [one, entry[one]]))
|
|
408
|
+
};
|
|
409
|
+
const proof = await oneProofOf(counter, key, path, entryProbe);
|
|
410
|
+
const said = proof.verdict === "cannot-fail" ? {
|
|
411
|
+
counter: counter.id,
|
|
412
|
+
expected: declared.expect,
|
|
413
|
+
key,
|
|
414
|
+
reading: 0,
|
|
415
|
+
verdict: "misread"
|
|
416
|
+
} : proof;
|
|
417
|
+
proofs.push(said);
|
|
418
|
+
if (said.verdict !== "proven") return { proofs, proven: false };
|
|
419
|
+
}
|
|
319
420
|
}
|
|
320
421
|
if (exclusiveVia !== void 0) {
|
|
321
|
-
const dir = mkdtempSync(
|
|
422
|
+
const dir = mkdtempSync(join3(tmpdir(), "geonosis-lock-"));
|
|
322
423
|
try {
|
|
323
|
-
const proof = await proveExclusive({ cli: exclusiveVia, lockPath:
|
|
424
|
+
const proof = await proveExclusive({ cli: exclusiveVia, lockPath: join3(dir, "heavy.lock") });
|
|
324
425
|
proofs.push(proof);
|
|
325
426
|
if (proof.verdict !== "serialised") return { proofs, proven: false };
|
|
326
427
|
} finally {
|
|
@@ -331,16 +432,23 @@ var runProve = async ({
|
|
|
331
432
|
};
|
|
332
433
|
|
|
333
434
|
// src/core/ratchet.ts
|
|
334
|
-
import { existsSync as existsSync3, readFileSync as
|
|
435
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
335
436
|
import { resolve as resolve4 } from "path";
|
|
336
437
|
var EVIDENCE_LINES = 10;
|
|
337
|
-
var recorded = (run) => {
|
|
438
|
+
var recorded = (counter, params, run) => {
|
|
338
439
|
let output = "";
|
|
440
|
+
let lastCommand;
|
|
441
|
+
const trimmed = (lines) => lines.map((line) => line.trimEnd()).filter((line) => line !== "").slice(-EVIDENCE_LINES);
|
|
339
442
|
return {
|
|
340
|
-
|
|
443
|
+
cited: () => {
|
|
444
|
+
const counted = counter.evidence === void 0 ? [] : counter.evidence({ output, params });
|
|
445
|
+
return trimmed(counted.length > 0 ? counted : output.split("\n"));
|
|
446
|
+
},
|
|
447
|
+
lastCommand: () => lastCommand,
|
|
341
448
|
run: (command) => {
|
|
342
449
|
const result = run(command);
|
|
343
450
|
output = result.output;
|
|
451
|
+
lastCommand = command;
|
|
344
452
|
return result;
|
|
345
453
|
}
|
|
346
454
|
};
|
|
@@ -376,9 +484,10 @@ var runRatchet = async ({
|
|
|
376
484
|
if (!existsSync3(baselinePath)) {
|
|
377
485
|
throw new Error(`no ${config.baseline} in ${cwd} \u2014 nothing to ratchet against`);
|
|
378
486
|
}
|
|
379
|
-
const baseline = JSON.parse(
|
|
487
|
+
const baseline = JSON.parse(readFileSync4(baselinePath, "utf8"));
|
|
380
488
|
const byId = new Map(counters.map((one) => [one.id, one]));
|
|
381
489
|
const measurements = [];
|
|
490
|
+
const refusals = [];
|
|
382
491
|
for (const entry of config.counters) {
|
|
383
492
|
const counter = byId.get(entry.counter);
|
|
384
493
|
if (counter === void 0) {
|
|
@@ -398,12 +507,22 @@ var runRatchet = async ({
|
|
|
398
507
|
);
|
|
399
508
|
}
|
|
400
509
|
const tolerance = toleranceOf(entry, key, counter);
|
|
401
|
-
const recorder = recorded(runCommand(cwd, entry.counter));
|
|
402
|
-
|
|
510
|
+
const recorder = recorded(counter, entry, runCommand(cwd, entry.counter));
|
|
511
|
+
let now;
|
|
512
|
+
try {
|
|
513
|
+
now = await counter.run({ cwd, key, params: entry, run: recorder.run });
|
|
514
|
+
} catch (error) {
|
|
515
|
+
const ran = recorder.lastCommand();
|
|
516
|
+
refusals.push({
|
|
517
|
+
key,
|
|
518
|
+
reason: `${error.message}${ran === void 0 ? "" : `. Next: run \`${ran}\` in ${cwd} and read what it prints`}`
|
|
519
|
+
});
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
403
522
|
const verdict = verdictOf(now, limit, tolerance);
|
|
404
523
|
measurements.push({
|
|
405
524
|
baseline: limit,
|
|
406
|
-
evidence: verdict === "grew" ? recorder.
|
|
525
|
+
evidence: verdict === "grew" ? recorder.cited() : [],
|
|
407
526
|
key,
|
|
408
527
|
now,
|
|
409
528
|
verdict
|
|
@@ -411,16 +530,16 @@ var runRatchet = async ({
|
|
|
411
530
|
}
|
|
412
531
|
const grew = measurements.some((one) => one.verdict === "grew");
|
|
413
532
|
const shrank = measurements.some((one) => one.verdict === "shrank");
|
|
414
|
-
if (shrank && !grew) {
|
|
533
|
+
if (shrank && !grew && refusals.length === 0) {
|
|
415
534
|
const next = { ...baseline };
|
|
416
535
|
for (const one of measurements) {
|
|
417
536
|
if (one.verdict === "shrank") next[one.key] = one.now;
|
|
418
537
|
}
|
|
419
|
-
|
|
538
|
+
writeFileSync4(baselinePath, `${JSON.stringify(next, null, 2)}
|
|
420
539
|
`);
|
|
421
|
-
return { measurements, rewritten: true };
|
|
540
|
+
return { considered: config.counters.length, measurements, refusals, rewritten: true };
|
|
422
541
|
}
|
|
423
|
-
return { measurements, rewritten: false };
|
|
542
|
+
return { considered: config.counters.length, measurements, refusals, rewritten: false };
|
|
424
543
|
};
|
|
425
544
|
|
|
426
545
|
// src/counters/params.ts
|
|
@@ -439,12 +558,12 @@ var countMatches = (text, pattern) => text.match(new RegExp(pattern.source, `${p
|
|
|
439
558
|
var escapeForRegex = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
440
559
|
|
|
441
560
|
// src/counters/plant.ts
|
|
442
|
-
import { mkdirSync as
|
|
443
|
-
import { dirname as
|
|
561
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
562
|
+
import { dirname as dirname5, join as join4 } from "path";
|
|
444
563
|
var plant = (dir, relative, contents) => {
|
|
445
|
-
const path =
|
|
446
|
-
|
|
447
|
-
|
|
564
|
+
const path = join4(dir, relative);
|
|
565
|
+
mkdirSync3(dirname5(path), { recursive: true });
|
|
566
|
+
writeFileSync5(path, contents);
|
|
448
567
|
};
|
|
449
568
|
var captured = (sample) => ({
|
|
450
569
|
command: () => "cat sample.txt",
|
|
@@ -529,8 +648,8 @@ var bundleBytes = {
|
|
|
529
648
|
};
|
|
530
649
|
|
|
531
650
|
// src/counters/ci.ts
|
|
532
|
-
import { readdirSync, readFileSync as
|
|
533
|
-
import { join as
|
|
651
|
+
import { readdirSync, readFileSync as readFileSync5 } from "fs";
|
|
652
|
+
import { join as join5, resolve as resolve5 } from "path";
|
|
534
653
|
var DISABLED = /\bif:[ \t]*false\b/;
|
|
535
654
|
var WORKFLOW = /\.ya?ml$/;
|
|
536
655
|
var disabledCiJobs = {
|
|
@@ -548,7 +667,7 @@ var disabledCiJobs = {
|
|
|
548
667
|
const dir = resolve5(cwd, relative);
|
|
549
668
|
let files;
|
|
550
669
|
try {
|
|
551
|
-
files = readdirSync(dir, { withFileTypes: true }).filter((entry) => WORKFLOW.test(entry.name)).map((entry) =>
|
|
670
|
+
files = readdirSync(dir, { withFileTypes: true }).filter((entry) => WORKFLOW.test(entry.name)).map((entry) => join5(dir, entry.name));
|
|
552
671
|
} catch {
|
|
553
672
|
return 0;
|
|
554
673
|
}
|
|
@@ -556,7 +675,7 @@ var disabledCiJobs = {
|
|
|
556
675
|
for (const file of files) {
|
|
557
676
|
let contents;
|
|
558
677
|
try {
|
|
559
|
-
contents =
|
|
678
|
+
contents = readFileSync5(file, "utf8");
|
|
560
679
|
} catch (error) {
|
|
561
680
|
throw new CounterError(
|
|
562
681
|
"disabledCiJobs",
|
|
@@ -579,10 +698,11 @@ var cloneCount = {
|
|
|
579
698
|
const output = run(command).output;
|
|
580
699
|
const found = output.match(CLONES)?.[1];
|
|
581
700
|
if (found === void 0) {
|
|
701
|
+
const said = output.trim();
|
|
582
702
|
throw new CounterError(
|
|
583
703
|
"cloneCount",
|
|
584
|
-
`no "Found N clones" line:
|
|
585
|
-
${
|
|
704
|
+
said === "" ? 'the command printed nothing at all, so there is no "Found N clones" line to read and no output to show \u2014 it is not installed, or it never ran' : `no "Found N clones" line in what it printed:
|
|
705
|
+
${said.slice(-500)}`
|
|
586
706
|
);
|
|
587
707
|
}
|
|
588
708
|
return Number(found);
|
|
@@ -636,7 +756,7 @@ var unformattedFiles = {
|
|
|
636
756
|
};
|
|
637
757
|
|
|
638
758
|
// src/counters/gate-report.ts
|
|
639
|
-
import { existsSync as existsSync5, readFileSync as
|
|
759
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
|
|
640
760
|
import { resolve as resolve7 } from "path";
|
|
641
761
|
var DEFAULT_REPORT = ".geonosis/gate-report.json";
|
|
642
762
|
var fastTierMs = {
|
|
@@ -646,7 +766,7 @@ var fastTierMs = {
|
|
|
646
766
|
expect: 4500,
|
|
647
767
|
input: (dir) => plant(
|
|
648
768
|
dir,
|
|
649
|
-
|
|
769
|
+
".geonosis/gate-report.fast.json",
|
|
650
770
|
JSON.stringify({
|
|
651
771
|
finishedAt: "2026-08-30T10:00:04.500Z",
|
|
652
772
|
ok: true,
|
|
@@ -657,8 +777,10 @@ var fastTierMs = {
|
|
|
657
777
|
)
|
|
658
778
|
},
|
|
659
779
|
run: async ({ cwd, params }) => {
|
|
660
|
-
const relative = stringParam("fastTierMs", params, "report", DEFAULT_REPORT);
|
|
661
780
|
const wanted = stringParam("fastTierMs", params, "tier", "fast");
|
|
781
|
+
const own = `.geonosis/gate-report.${wanted}.json`;
|
|
782
|
+
const asked = stringParam("fastTierMs", params, "report", "");
|
|
783
|
+
const relative = asked !== "" ? asked : existsSync5(resolve7(cwd, own)) ? own : DEFAULT_REPORT;
|
|
662
784
|
const path = resolve7(cwd, relative);
|
|
663
785
|
if (!existsSync5(path)) {
|
|
664
786
|
throw new CounterError(
|
|
@@ -668,7 +790,7 @@ var fastTierMs = {
|
|
|
668
790
|
}
|
|
669
791
|
let report;
|
|
670
792
|
try {
|
|
671
|
-
report = JSON.parse(
|
|
793
|
+
report = JSON.parse(readFileSync6(path, "utf8"));
|
|
672
794
|
} catch (error) {
|
|
673
795
|
throw new CounterError(
|
|
674
796
|
"fastTierMs",
|
|
@@ -678,7 +800,7 @@ var fastTierMs = {
|
|
|
678
800
|
if (report.tier !== wanted) {
|
|
679
801
|
throw new CounterError(
|
|
680
802
|
"fastTierMs",
|
|
681
|
-
`${relative} is a report of tier "${String(report.tier)}", not "${wanted}"`
|
|
803
|
+
`${relative} is a report of tier "${String(report.tier)}", not "${wanted}" \u2014 run \`geonosis-verify ${wanted}\`, which records ${own}`
|
|
682
804
|
);
|
|
683
805
|
}
|
|
684
806
|
const spent = Date.parse(String(report.finishedAt)) - Date.parse(String(report.startedAt));
|
|
@@ -690,7 +812,7 @@ var fastTierMs = {
|
|
|
690
812
|
};
|
|
691
813
|
|
|
692
814
|
// src/counters/law.ts
|
|
693
|
-
import { existsSync as existsSync6, readFileSync as
|
|
815
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
|
|
694
816
|
import { resolve as resolve8 } from "path";
|
|
695
817
|
var lawLineCount = {
|
|
696
818
|
id: "lawLineCount",
|
|
@@ -703,12 +825,12 @@ var lawLineCount = {
|
|
|
703
825
|
const relative = stringParam("lawLineCount", params, "path", "CLAUDE.md");
|
|
704
826
|
const path = resolve8(cwd, relative);
|
|
705
827
|
if (!existsSync6(path)) throw new CounterError("lawLineCount", `no law file at ${relative}`);
|
|
706
|
-
return
|
|
828
|
+
return readFileSync7(path, "utf8").replace(/\n$/, "").split("\n").length;
|
|
707
829
|
}
|
|
708
830
|
};
|
|
709
831
|
|
|
710
832
|
// src/counters/oxlint.ts
|
|
711
|
-
import { existsSync as existsSync7, readFileSync as
|
|
833
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
712
834
|
import { resolve as resolve9 } from "path";
|
|
713
835
|
var DEFAULT_COMMAND = "npx oxlint --format=unix --config .oxlintrc.json .";
|
|
714
836
|
var PROBE_COMMAND = "oxlint --format=unix --config .oxlintrc.json .";
|
|
@@ -729,12 +851,32 @@ var UNIX_FINDING = /^\S[^\n]*:\d+:\d+: .*\[(Error|Warning)\/[^\]\n]+\]$/gm;
|
|
|
729
851
|
var UNIX_SUMMARY = /^(\d+) problems?$/m;
|
|
730
852
|
var AGENT_FINDING = /^\S[^\n]*:\d+:\d+: (error|warning) /gm;
|
|
731
853
|
var DEFAULT_SUMMARY = /^Found (\d+) warnings? and (\d+) errors?\.$/m;
|
|
732
|
-
var DEFAULT_FINDING = /^\s*[x!] [^\s(]+\([^)\n]+\): /m;
|
|
854
|
+
var DEFAULT_FINDING = /^\s*([x!]) [^\s(]+\([^)\n]+\): /m;
|
|
733
855
|
var FINDING_SHAPES = [UNIX_FINDING, AGENT_FINDING, DEFAULT_FINDING].map(
|
|
734
856
|
(shape) => new RegExp(shape.source)
|
|
735
857
|
);
|
|
736
858
|
var RULE_TOKEN = /\([^()\n]+\)/;
|
|
737
|
-
var
|
|
859
|
+
var SEVERITY_BY_TOKEN = {
|
|
860
|
+
"!": "warning",
|
|
861
|
+
Error: "error",
|
|
862
|
+
error: "error",
|
|
863
|
+
Warning: "warning",
|
|
864
|
+
warning: "warning",
|
|
865
|
+
x: "error"
|
|
866
|
+
};
|
|
867
|
+
var severityOf = (line) => {
|
|
868
|
+
for (const shape of FINDING_SHAPES) {
|
|
869
|
+
const token = shape.exec(line)?.[1];
|
|
870
|
+
if (token !== void 0) return SEVERITY_BY_TOKEN[token];
|
|
871
|
+
}
|
|
872
|
+
return void 0;
|
|
873
|
+
};
|
|
874
|
+
var findingLinesOf = (output) => output.split("\n").filter((line) => severityOf(line) !== void 0);
|
|
875
|
+
var linesOfSeverity = (output, want) => output.split("\n").filter((line) => severityOf(line) === want);
|
|
876
|
+
var linesOfRule = (output, rule) => {
|
|
877
|
+
const named = new RegExp(`\\(${escapeForRegex(rule)}\\)`);
|
|
878
|
+
return findingLinesOf(output).filter((line) => named.test(line));
|
|
879
|
+
};
|
|
738
880
|
var FORMATS = ["agent", "default", "unix"];
|
|
739
881
|
var isFormat = (value) => typeof value === "string" && FORMATS.some((one) => one === value);
|
|
740
882
|
var expectedFormat = (counter, params) => {
|
|
@@ -793,6 +935,7 @@ ${output.trim()}`);
|
|
|
793
935
|
return refuse2(`the tool exited ${code} and printed no findings and no summary`);
|
|
794
936
|
};
|
|
795
937
|
var oxlintErrors = {
|
|
938
|
+
evidence: ({ output }) => linesOfSeverity(output, "error"),
|
|
796
939
|
id: "oxlintErrors",
|
|
797
940
|
probe: {
|
|
798
941
|
...oxlintProbe("error", "typescript/no-explicit-any", ONE_ANY),
|
|
@@ -806,6 +949,7 @@ var oxlintErrors = {
|
|
|
806
949
|
}
|
|
807
950
|
};
|
|
808
951
|
var oxlintWarnings = {
|
|
952
|
+
evidence: ({ output }) => linesOfSeverity(output, "warning"),
|
|
809
953
|
id: "oxlintWarnings",
|
|
810
954
|
// A warning, so the run exits 0 — the window `expectFormat` exists to close is also the window a
|
|
811
955
|
// probe has to survive.
|
|
@@ -830,10 +974,12 @@ var countRule = (result, rule, expect) => {
|
|
|
830
974
|
${result.output.trim()}`
|
|
831
975
|
);
|
|
832
976
|
}
|
|
833
|
-
|
|
834
|
-
return lines.filter((line) => named.test(line)).length;
|
|
977
|
+
return linesOfRule(result.output, rule).length;
|
|
835
978
|
};
|
|
836
979
|
var oxlintRule = {
|
|
980
|
+
// The rule is a required param, so a run that got here always has one; a citation is not the
|
|
981
|
+
// place to raise that, and the report falls back to the tail rather than losing the regression.
|
|
982
|
+
evidence: ({ output, params }) => typeof params.rule === "string" ? linesOfRule(output, params.rule) : [],
|
|
837
983
|
id: "oxlintRule",
|
|
838
984
|
// The probe names its OWN rule: which rule a repo tracks is its business, and a probe that had to
|
|
839
985
|
// make the repo's rule fire would need the repo's plugin loadable from a scratch directory.
|
|
@@ -851,11 +997,11 @@ var oxlintRule = {
|
|
|
851
997
|
const source = resolve9(cwd, config);
|
|
852
998
|
if (!existsSync7(source)) throw new CounterError("oxlintRule", `no config at ${config}`);
|
|
853
999
|
const strictName = `.oxlintrc.ratchet-${key}.json`;
|
|
854
|
-
const strict =
|
|
1000
|
+
const strict = readFileSync8(source, "utf8").replace(
|
|
855
1001
|
new RegExp(`("[^"]*${escapeForRegex(rule)}"\\s*:\\s*\\[?\\s*)"(warn|off)"`),
|
|
856
1002
|
'$1"error"'
|
|
857
1003
|
);
|
|
858
|
-
|
|
1004
|
+
writeFileSync6(resolve9(cwd, strictName), strict);
|
|
859
1005
|
try {
|
|
860
1006
|
return countRule(run(command.replace("{config}", strictName)), rule, expect);
|
|
861
1007
|
} finally {
|
|
@@ -889,15 +1035,15 @@ var runtimeCodeShipped = {
|
|
|
889
1035
|
|
|
890
1036
|
// src/counters/scripts.ts
|
|
891
1037
|
import { readdirSync as readdirSync3 } from "fs";
|
|
892
|
-
import { join as
|
|
1038
|
+
import { join as join7 } from "path";
|
|
893
1039
|
|
|
894
1040
|
// src/counters/workspace.ts
|
|
895
|
-
import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as
|
|
896
|
-
import { join as
|
|
1041
|
+
import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync9 } from "fs";
|
|
1042
|
+
import { join as join6, resolve as resolve10 } from "path";
|
|
897
1043
|
var SKIP = /^(node_modules|\.)/;
|
|
898
1044
|
var childDirs = (dir) => {
|
|
899
1045
|
try {
|
|
900
|
-
return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !SKIP.test(entry.name)).map((entry) =>
|
|
1046
|
+
return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !SKIP.test(entry.name)).map((entry) => join6(dir, entry.name));
|
|
901
1047
|
} catch {
|
|
902
1048
|
return [];
|
|
903
1049
|
}
|
|
@@ -907,14 +1053,14 @@ var expand = (root, pattern) => {
|
|
|
907
1053
|
const segments = pattern.split("/").filter((one) => one !== "" && one !== ".");
|
|
908
1054
|
let dirs = [root];
|
|
909
1055
|
for (const segment of segments) {
|
|
910
|
-
dirs = segment === "*" ? dirs.flatMap((dir) => childDirs(dir)) : segment === "**" ? dirs.flatMap((dir) => descendants(dir, 3)) : dirs.map((dir) =>
|
|
1056
|
+
dirs = segment === "*" ? dirs.flatMap((dir) => childDirs(dir)) : segment === "**" ? dirs.flatMap((dir) => descendants(dir, 3)) : dirs.map((dir) => join6(dir, segment)).filter((dir) => existsSync8(dir));
|
|
911
1057
|
}
|
|
912
1058
|
return dirs;
|
|
913
1059
|
};
|
|
914
1060
|
var QUOTED = /^['"]|['"]$/g;
|
|
915
1061
|
var cleaned = (value) => value.replace(/#.*$/, "").trim().replaceAll(QUOTED, "");
|
|
916
1062
|
var pnpmPatterns = (path) => {
|
|
917
|
-
const lines =
|
|
1063
|
+
const lines = readFileSync9(path, "utf8").split("\n");
|
|
918
1064
|
const at = lines.findIndex((line) => line.startsWith("packages:"));
|
|
919
1065
|
if (at === -1) return [];
|
|
920
1066
|
const inline = lines[at]?.slice("packages:".length).trim() ?? "";
|
|
@@ -933,15 +1079,15 @@ var pnpmPatterns = (path) => {
|
|
|
933
1079
|
return patterns;
|
|
934
1080
|
};
|
|
935
1081
|
var npmPatterns = (path) => {
|
|
936
|
-
const parsed = JSON.parse(
|
|
1082
|
+
const parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
937
1083
|
const declared = Array.isArray(parsed.workspaces) ? parsed.workspaces : parsed.workspaces?.packages ?? [];
|
|
938
1084
|
return declared.filter((one) => typeof one === "string");
|
|
939
1085
|
};
|
|
940
1086
|
var nameOf = (dir) => {
|
|
941
|
-
const manifest =
|
|
1087
|
+
const manifest = join6(dir, "package.json");
|
|
942
1088
|
if (!existsSync8(manifest)) return void 0;
|
|
943
1089
|
try {
|
|
944
|
-
const { name } = JSON.parse(
|
|
1090
|
+
const { name } = JSON.parse(readFileSync9(manifest, "utf8"));
|
|
945
1091
|
return typeof name === "string" && name !== "" ? name : void 0;
|
|
946
1092
|
} catch {
|
|
947
1093
|
return void 0;
|
|
@@ -949,15 +1095,15 @@ var nameOf = (dir) => {
|
|
|
949
1095
|
};
|
|
950
1096
|
var workspaceDirs = (cwd) => {
|
|
951
1097
|
const root = resolve10(cwd);
|
|
952
|
-
const pnpm =
|
|
953
|
-
const manifest =
|
|
1098
|
+
const pnpm = join6(root, "pnpm-workspace.yaml");
|
|
1099
|
+
const manifest = join6(root, "package.json");
|
|
954
1100
|
const patterns = existsSync8(pnpm) ? pnpmPatterns(pnpm) : existsSync8(manifest) ? npmPatterns(manifest) : [];
|
|
955
|
-
const dirs = patterns.filter((pattern) => !pattern.startsWith("!")).flatMap((pattern) => expand(root, pattern)).filter((dir) => existsSync8(
|
|
1101
|
+
const dirs = patterns.filter((pattern) => !pattern.startsWith("!")).flatMap((pattern) => expand(root, pattern)).filter((dir) => existsSync8(join6(dir, "package.json")));
|
|
956
1102
|
return [...new Set(dirs)];
|
|
957
1103
|
};
|
|
958
1104
|
var manifestOf = (dir) => {
|
|
959
1105
|
try {
|
|
960
|
-
const parsed = JSON.parse(
|
|
1106
|
+
const parsed = JSON.parse(readFileSync9(join6(dir, "package.json"), "utf8"));
|
|
961
1107
|
return typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
962
1108
|
} catch {
|
|
963
1109
|
return void 0;
|
|
@@ -987,7 +1133,7 @@ var holdsTests = (dir, depth = 6) => {
|
|
|
987
1133
|
if (entry.isDirectory()) {
|
|
988
1134
|
if (entry.name === TEST_DIR) return true;
|
|
989
1135
|
if (SKIP2.test(entry.name) || depth === 0) continue;
|
|
990
|
-
if (holdsTests(
|
|
1136
|
+
if (holdsTests(join7(dir, entry.name), depth - 1)) return true;
|
|
991
1137
|
continue;
|
|
992
1138
|
}
|
|
993
1139
|
if (TEST_FILE.test(entry.name)) return true;
|
|
@@ -1028,17 +1174,67 @@ var packagesWithoutTypecheck = {
|
|
|
1028
1174
|
var sumOfCounts = {
|
|
1029
1175
|
id: "sumOfCounts",
|
|
1030
1176
|
probe: { ...captured("src/a.ts:1\nsrc/b.ts:0\n"), expect: 1 },
|
|
1177
|
+
readingParams: ["match"],
|
|
1031
1178
|
run: async ({ params, run }) => {
|
|
1032
1179
|
const command = stringParam("sumOfCounts", params, "command");
|
|
1033
|
-
const
|
|
1034
|
-
|
|
1180
|
+
const pattern = stringParam("sumOfCounts", params, "match", ":(\\d+)$");
|
|
1181
|
+
const match = new RegExp(pattern, "gm");
|
|
1182
|
+
const output = run(command).output;
|
|
1183
|
+
const found = [...output.matchAll(match)];
|
|
1184
|
+
if (found.length === 0 && output.trim() !== "") {
|
|
1185
|
+
throw new CounterError(
|
|
1186
|
+
"sumOfCounts",
|
|
1187
|
+
`its match ${pattern} read nothing from ${output.trim().split("\n").length} line(s) of output (first: ${JSON.stringify(output.trim().split("\n")[0])}) \u2014 a match that cannot read the tool is not a zero`
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
return found.map(([, digits]) => Number(digits ?? 0)).filter((count) => Number.isFinite(count)).reduce((sum, count) => sum + count, 0);
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
// src/counters/suppressions.ts
|
|
1195
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync10 } from "fs";
|
|
1196
|
+
import { join as join8, resolve as resolve11 } from "path";
|
|
1197
|
+
var CODE_FILE = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts|vue|svelte|astro)$/;
|
|
1198
|
+
var SKIP3 = /^(?:node_modules|dist|coverage|\.[^.]|__fixtures__)/;
|
|
1199
|
+
var SUPPRESSION = /(?:eslint|oxlint|biome)-(?:disable|ignore)(?:-next-line|-line)?|@ts-expect-error|@ts-ignore/g;
|
|
1200
|
+
var countIn = (dir, depth = 12) => {
|
|
1201
|
+
let entries;
|
|
1202
|
+
try {
|
|
1203
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
1204
|
+
} catch {
|
|
1205
|
+
return 0;
|
|
1206
|
+
}
|
|
1207
|
+
let found = 0;
|
|
1208
|
+
for (const entry of entries) {
|
|
1209
|
+
if (SKIP3.test(entry.name)) continue;
|
|
1210
|
+
if (entry.isDirectory()) {
|
|
1211
|
+
if (depth > 0) found += countIn(join8(dir, entry.name), depth - 1);
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
if (!CODE_FILE.test(entry.name)) continue;
|
|
1215
|
+
try {
|
|
1216
|
+
found += readFileSync10(join8(dir, entry.name), "utf8").match(SUPPRESSION)?.length ?? 0;
|
|
1217
|
+
} catch {
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return found;
|
|
1221
|
+
};
|
|
1222
|
+
var suppressionCount = {
|
|
1223
|
+
id: "suppressionCount",
|
|
1224
|
+
probe: {
|
|
1225
|
+
expect: 2,
|
|
1226
|
+
input: (dir) => plant(dir, "src/planted.ts", "// eslint-disable-next-line x\n// @ts-expect-error y\n")
|
|
1227
|
+
},
|
|
1228
|
+
run: async ({ cwd, params }) => {
|
|
1229
|
+
const roots = stringsParam(params, "roots", ["."]);
|
|
1230
|
+
return roots.reduce((sum, root) => sum + countIn(resolve11(cwd, root)), 0);
|
|
1035
1231
|
}
|
|
1036
1232
|
};
|
|
1037
1233
|
|
|
1038
1234
|
// src/counters/tests.ts
|
|
1039
|
-
import { existsSync as existsSync9, mkdtempSync as mkdtempSync2, readFileSync as
|
|
1235
|
+
import { existsSync as existsSync9, mkdtempSync as mkdtempSync2, readFileSync as readFileSync11, rmSync as rmSync4 } from "fs";
|
|
1040
1236
|
import { tmpdir as tmpdir2 } from "os";
|
|
1041
|
-
import { join as
|
|
1237
|
+
import { join as join9, resolve as resolve12 } from "path";
|
|
1042
1238
|
var COUNTER = "testFailures";
|
|
1043
1239
|
var VITEST_LINE = /^\s*Tests {2,}(.+?)\s*$/;
|
|
1044
1240
|
var VITEST_TOTAL = /\(\d+\)$/;
|
|
@@ -1078,7 +1274,7 @@ var fromReport = (path) => {
|
|
|
1078
1274
|
}
|
|
1079
1275
|
let report;
|
|
1080
1276
|
try {
|
|
1081
|
-
report = JSON.parse(
|
|
1277
|
+
report = JSON.parse(readFileSync11(path, "utf8"));
|
|
1082
1278
|
} catch (error) {
|
|
1083
1279
|
throw new CounterError(
|
|
1084
1280
|
COUNTER,
|
|
@@ -1102,14 +1298,14 @@ var fromReport = (path) => {
|
|
|
1102
1298
|
};
|
|
1103
1299
|
var reportPathFor = (cwd, params, command) => {
|
|
1104
1300
|
const named = params.reportPath;
|
|
1105
|
-
if (typeof named === "string" && named !== "") return { own: false, path:
|
|
1301
|
+
if (typeof named === "string" && named !== "") return { own: false, path: resolve12(cwd, named) };
|
|
1106
1302
|
if (!command.includes(PLACEHOLDER)) {
|
|
1107
1303
|
throw new CounterError(
|
|
1108
1304
|
COUNTER,
|
|
1109
1305
|
`report: "${VITEST_JSON}" needs somewhere to put the report \u2014 write ${PLACEHOLDER} into the command (--outputFile=${PLACEHOLDER}) or give the entry a "reportPath"`
|
|
1110
1306
|
);
|
|
1111
1307
|
}
|
|
1112
|
-
return { own: true, path:
|
|
1308
|
+
return { own: true, path: join9(mkdtempSync2(join9(tmpdir2(), "geonosis-report-")), "report.json") };
|
|
1113
1309
|
};
|
|
1114
1310
|
var testFailures = {
|
|
1115
1311
|
id: COUNTER,
|
|
@@ -1158,7 +1354,7 @@ var testFailures = {
|
|
|
1158
1354
|
run(command.replaceAll(PLACEHOLDER, path));
|
|
1159
1355
|
return fromReport(path);
|
|
1160
1356
|
} finally {
|
|
1161
|
-
if (own) rmSync4(
|
|
1357
|
+
if (own) rmSync4(join9(path, ".."), { force: true, recursive: true });
|
|
1162
1358
|
}
|
|
1163
1359
|
}
|
|
1164
1360
|
};
|
|
@@ -1198,8 +1394,8 @@ var typecheckErrors = {
|
|
|
1198
1394
|
};
|
|
1199
1395
|
|
|
1200
1396
|
// src/counters/walk.ts
|
|
1201
|
-
import { existsSync as existsSync10, readFileSync as
|
|
1202
|
-
import { resolve as
|
|
1397
|
+
import { existsSync as existsSync10, readFileSync as readFileSync12 } from "fs";
|
|
1398
|
+
import { resolve as resolve13 } from "path";
|
|
1203
1399
|
var DEFAULT_REPORT2 = ".geonosis/walk-report.json";
|
|
1204
1400
|
var CLASSES = /* @__PURE__ */ new Set([
|
|
1205
1401
|
"buy-box-above-fold",
|
|
@@ -1243,7 +1439,7 @@ var walkFindings = {
|
|
|
1243
1439
|
},
|
|
1244
1440
|
run: async ({ cwd, params }) => {
|
|
1245
1441
|
const relative = stringParam("walkFindings", params, "report", DEFAULT_REPORT2);
|
|
1246
|
-
const path =
|
|
1442
|
+
const path = resolve13(cwd, relative);
|
|
1247
1443
|
if (!existsSync10(path)) {
|
|
1248
1444
|
throw new CounterError(
|
|
1249
1445
|
"walkFindings",
|
|
@@ -1252,7 +1448,7 @@ var walkFindings = {
|
|
|
1252
1448
|
}
|
|
1253
1449
|
let report;
|
|
1254
1450
|
try {
|
|
1255
|
-
report = JSON.parse(
|
|
1451
|
+
report = JSON.parse(readFileSync12(path, "utf8"));
|
|
1256
1452
|
} catch (error) {
|
|
1257
1453
|
throw new CounterError(
|
|
1258
1454
|
"walkFindings",
|
|
@@ -1282,6 +1478,7 @@ var COUNTERS = [
|
|
|
1282
1478
|
fastTierMs,
|
|
1283
1479
|
knipIssues,
|
|
1284
1480
|
lawLineCount,
|
|
1481
|
+
suppressionCount,
|
|
1285
1482
|
oxlintErrors,
|
|
1286
1483
|
oxlintRule,
|
|
1287
1484
|
oxlintWarnings,
|
|
@@ -1306,7 +1503,7 @@ var counterById = (id) => {
|
|
|
1306
1503
|
|
|
1307
1504
|
// src/report.ts
|
|
1308
1505
|
var WIDTH = 28;
|
|
1309
|
-
var formatReport = ({ measurements, rewritten }) => {
|
|
1506
|
+
var formatReport = ({ measurements, refusals, rewritten }) => {
|
|
1310
1507
|
const lines = measurements.map((one) => {
|
|
1311
1508
|
if (one.verdict === "skipped") {
|
|
1312
1509
|
return ` SKIP ${one.key}: not measured by --tier ${one.tier}`;
|
|
@@ -1319,8 +1516,14 @@ var formatReport = ({ measurements, rewritten }) => {
|
|
|
1319
1516
|
if (one.verdict === "shrank") return `${head} <-- improved -${one.baseline - one.now}`;
|
|
1320
1517
|
return head;
|
|
1321
1518
|
});
|
|
1519
|
+
lines.push(...refusals.map((one) => ` CANNOT MEASURE ${one.key}: ${one.reason}`));
|
|
1322
1520
|
const grew = measurements.filter((one) => one.verdict === "grew");
|
|
1323
|
-
if (
|
|
1521
|
+
if (refusals.length > 0) {
|
|
1522
|
+
lines.push(
|
|
1523
|
+
"",
|
|
1524
|
+
`ratchet FAIL \u2014 ${refusals.length} counter(s) could not be measured, and a gate that cannot measure has not passed. The baseline was left exactly as it was. Next: run each counter's own command by hand \u2014 ${refusals.map((one) => one.key).join(", ")} \u2014 and fix what it prints.`
|
|
1525
|
+
);
|
|
1526
|
+
} else if (grew.length > 0) {
|
|
1324
1527
|
lines.push(
|
|
1325
1528
|
"",
|
|
1326
1529
|
"ratchet FAIL \u2014 debt grew. Fix it, or say in the commit message why the baseline goes up."
|
|
@@ -1342,6 +1545,7 @@ var formatProve = ({ proofs, proven }) => {
|
|
|
1342
1545
|
}
|
|
1343
1546
|
if (one.verdict === "cannot-measure") return ` CANNOT MEASURE ${one.key}: ${one.reason}`;
|
|
1344
1547
|
if (one.verdict === "cannot-fail") return ` CANNOT FAIL ${one.key}: read 0`;
|
|
1548
|
+
if (one.verdict === "unproven") return ` UNPROVEN ${one.key}: ${one.reason}`;
|
|
1345
1549
|
if (one.verdict === "misread") {
|
|
1346
1550
|
return ` MISREAD ${one.key}: read ${one.reading} where its probe planted ${one.expected}`;
|
|
1347
1551
|
}
|
|
@@ -1356,11 +1560,17 @@ var formatProve = ({ proofs, proven }) => {
|
|
|
1356
1560
|
};
|
|
1357
1561
|
|
|
1358
1562
|
export {
|
|
1563
|
+
ENVELOPES_DIR,
|
|
1564
|
+
envelopePath,
|
|
1565
|
+
UnbalancedEnvelope,
|
|
1566
|
+
writeEnvelope,
|
|
1567
|
+
versionOf,
|
|
1359
1568
|
heavyLockPath,
|
|
1360
1569
|
acquireExclusive,
|
|
1361
1570
|
CONFIG_FILE,
|
|
1362
1571
|
keyOf,
|
|
1363
1572
|
loadConfig,
|
|
1573
|
+
resolveBaseline,
|
|
1364
1574
|
CounterError,
|
|
1365
1575
|
runCommand,
|
|
1366
1576
|
runProve,
|
package/dist/cli.js
CHANGED
|
@@ -4,10 +4,83 @@ import {
|
|
|
4
4
|
formatProve,
|
|
5
5
|
formatReport,
|
|
6
6
|
runProve,
|
|
7
|
-
runRatchet
|
|
8
|
-
|
|
7
|
+
runRatchet,
|
|
8
|
+
versionOf,
|
|
9
|
+
writeEnvelope
|
|
10
|
+
} from "./chunk-ESN6ULZZ.js";
|
|
9
11
|
|
|
10
12
|
// src/cli.ts
|
|
13
|
+
import process from "process";
|
|
14
|
+
var USAGE = `geonosis-ratchet [--cwd <dir>] [--tier <name>] [--prove] [--exclusive]
|
|
15
|
+
|
|
16
|
+
Debt as a number that may only shrink. Runs the counters named in geonosis.ratchet.json, compares
|
|
17
|
+
each against gate-baseline.json, and REWRITES the baseline down when a number shrank \u2014 so the win is
|
|
18
|
+
locked in the same commit that earned it.
|
|
19
|
+
|
|
20
|
+
--cwd <dir> the repo to measure (default: the working directory)
|
|
21
|
+
--tier <name> only the counters in that tier (default: every counter)
|
|
22
|
+
--prove plant a finding for each counter and require it to be read
|
|
23
|
+
--exclusive hold the machine-wide heavy lock for the run
|
|
24
|
+
--exclusive-timeout <secs> how long to wait for that lock
|
|
25
|
+
--hold <ms> take the lock, wait, give it back \u2014 the --prove self-test's slow thing
|
|
26
|
+
--help, -h this text
|
|
27
|
+
|
|
28
|
+
Exit codes: 0 no counter grew \xB7 1 a counter grew \xB7 2 the run could not measure.
|
|
29
|
+
|
|
30
|
+
Every flag it does not know is REFUSED. This bin used to let anything unrecognised fall through to a
|
|
31
|
+
full measurement, so \`--help\` ran the whole ratchet \u2014 a run that can rewrite the baseline, started
|
|
32
|
+
by a typo.`;
|
|
33
|
+
var KNOWN = /* @__PURE__ */ new Set([
|
|
34
|
+
"--cwd",
|
|
35
|
+
"--exclusive",
|
|
36
|
+
"--exclusive-timeout",
|
|
37
|
+
"--hold",
|
|
38
|
+
"--prove",
|
|
39
|
+
"--tier"
|
|
40
|
+
]);
|
|
41
|
+
var TAKES_A_VALUE = /* @__PURE__ */ new Set(["--cwd", "--exclusive-timeout", "--hold", "--tier"]);
|
|
42
|
+
var CONFIG_SHAPE = `geonosis-ratchet reads geonosis.ratchet.json and gate-baseline.json
|
|
43
|
+
|
|
44
|
+
geonosis.ratchet.json
|
|
45
|
+
baseline string? the file holding the numbers (default "gate-baseline.json")
|
|
46
|
+
counters CounterEntry[] required, and every entry is:
|
|
47
|
+
counter string required \u2014 the id of a counter this build ships
|
|
48
|
+
key string? the baseline key it writes (default: the counter id).
|
|
49
|
+
Two entries writing one key are refused: the loser's debt
|
|
50
|
+
would vanish into the baseline
|
|
51
|
+
tiers string[]? the tiers it runs in (default: all of them). A malformed
|
|
52
|
+
list is refused, never quietly matched to no tier
|
|
53
|
+
\u2026 each counter reads its own further keys
|
|
54
|
+
|
|
55
|
+
gate-baseline.json
|
|
56
|
+
<key> number one number per counter key. Written DOWN in place when a
|
|
57
|
+
number shrank, so the win lands in the commit that earned it`;
|
|
58
|
+
var argv = process.argv.slice(2);
|
|
59
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
60
|
+
process.stdout.write(`${USAGE}
|
|
61
|
+
`);
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
if (argv.includes("--print-config-shape")) {
|
|
65
|
+
process.stdout.write(`${CONFIG_SHAPE}
|
|
66
|
+
`);
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
var refuse = (message) => {
|
|
70
|
+
process.stderr.write(`geonosis-ratchet: ${message}
|
|
71
|
+
`);
|
|
72
|
+
process.exit(2);
|
|
73
|
+
};
|
|
74
|
+
for (let at = 0; at < argv.length; at += 1) {
|
|
75
|
+
const arg = argv[at];
|
|
76
|
+
if (KNOWN.has(arg)) {
|
|
77
|
+
if (TAKES_A_VALUE.has(arg)) at += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
refuse(
|
|
81
|
+
`${arg} is not an option it takes. Run geonosis-ratchet --help for the ones it does \u2014 an unrecognised argument is refused rather than measured, because a run here can rewrite the baseline.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
11
84
|
var numberAfter = (flag) => {
|
|
12
85
|
const at = process.argv.indexOf(flag);
|
|
13
86
|
return at === -1 ? void 0 : Number(process.argv[at + 1] ?? Number.NaN);
|
|
@@ -29,6 +102,19 @@ var holdTheLock = async (ms) => {
|
|
|
29
102
|
`);
|
|
30
103
|
return 0;
|
|
31
104
|
};
|
|
105
|
+
var ENVELOPE_NEXT = "geonosis-ratchet --print-config-shape, then count the entries in geonosis.ratchet.json \u2014 a run that measured fewer counters than the config named is a bug in this tool, not in the tree it measured";
|
|
106
|
+
var envelopeOf = (result, durationMs) => ({
|
|
107
|
+
considered: result.considered,
|
|
108
|
+
durationMs,
|
|
109
|
+
excused: result.measurements.flatMap(
|
|
110
|
+
(one) => one.verdict === "skipped" ? [{ path: one.key, reason: `not measured by --tier ${one.tier}` }] : []
|
|
111
|
+
),
|
|
112
|
+
findings: result.measurements.filter((one) => one.verdict === "grew"),
|
|
113
|
+
read: result.measurements.filter((one) => one.verdict !== "skipped").length,
|
|
114
|
+
refused: result.refusals.map((one) => ({ path: one.key, reason: one.reason })),
|
|
115
|
+
tool: "ratchet",
|
|
116
|
+
version: versionOf(import.meta.url)
|
|
117
|
+
});
|
|
32
118
|
var measure = async () => {
|
|
33
119
|
if (holdMs !== void 0) return holdTheLock(holdMs);
|
|
34
120
|
if (proving) {
|
|
@@ -41,8 +127,17 @@ var measure = async () => {
|
|
|
41
127
|
process.stdout.write(formatProve(proof));
|
|
42
128
|
return proof.proven ? 0 : 2;
|
|
43
129
|
}
|
|
130
|
+
const startedAt = Date.now();
|
|
44
131
|
const result = await runRatchet({ counters: COUNTERS, cwd, tier });
|
|
45
132
|
process.stdout.write(formatReport(result));
|
|
133
|
+
const at = writeEnvelope({
|
|
134
|
+
envelope: envelopeOf(result, Date.now() - startedAt),
|
|
135
|
+
next: ENVELOPE_NEXT,
|
|
136
|
+
root: cwd
|
|
137
|
+
});
|
|
138
|
+
process.stdout.write(`envelope: ${at}
|
|
139
|
+
`);
|
|
140
|
+
if (result.refusals.length > 0) return 2;
|
|
46
141
|
return result.measurements.some((one) => one.verdict === "grew") ? 1 : 0;
|
|
47
142
|
};
|
|
48
143
|
try {
|
package/dist/index.d.ts
CHANGED
|
@@ -41,6 +41,22 @@ type CounterProbe = {
|
|
|
41
41
|
params?: Record<string, unknown>;
|
|
42
42
|
};
|
|
43
43
|
type Counter = {
|
|
44
|
+
/**
|
|
45
|
+
* Which lines of a run this counter actually COUNTED, for the report to cite when the number
|
|
46
|
+
* grew. Absent means the tail of the run is shown, which is right for a counter whose tool prints
|
|
47
|
+
* nothing but findings and wrong for every counter that reads one rule or one severity out of a
|
|
48
|
+
* mixed run: the tail is whatever printed LAST, and dielime got two unrelated warnings under a
|
|
49
|
+
* +2 on one rule while the two real errors sat further up.
|
|
50
|
+
*
|
|
51
|
+
* It takes the output rather than being handed the lines during the run so that it stays a pure
|
|
52
|
+
* function of what the tool said — the same reading the count was taken from, testable on its
|
|
53
|
+
* own. Returning nothing falls back to the tail: a citation nobody could make is no reason to
|
|
54
|
+
* show the reader nothing at all.
|
|
55
|
+
*/
|
|
56
|
+
evidence?: (context: {
|
|
57
|
+
output: string;
|
|
58
|
+
params: Record<string, unknown>;
|
|
59
|
+
}) => string[];
|
|
44
60
|
id: string;
|
|
45
61
|
/**
|
|
46
62
|
* Whether this counter's number is a measured QUANTITY — bytes, milliseconds — rather than a
|
|
@@ -60,6 +76,14 @@ type Counter = {
|
|
|
60
76
|
* nobody configured says nothing about the mode they did.
|
|
61
77
|
*/
|
|
62
78
|
probe?: CounterProbe | CounterProbe[];
|
|
79
|
+
/**
|
|
80
|
+
* The params that change WHAT this counter reads (a `match`), as opposed to where it looks.
|
|
81
|
+
* #140: a configured reading the probe never exercises can be wrong in a way the probe cannot
|
|
82
|
+
* see — during.day's mis-escaped match read 0, was certified PROVEN, and the false win was
|
|
83
|
+
* banked into the baseline. A config customizing one of these must declare `probe: { sample,
|
|
84
|
+
* expect }` beside it, or the prove line says UNPROVEN.
|
|
85
|
+
*/
|
|
86
|
+
readingParams?: string[];
|
|
63
87
|
run: (context: CounterContext) => Promise<number>;
|
|
64
88
|
};
|
|
65
89
|
/** One line of `geonosis.ratchet.json`'s `counters` array. */
|
|
@@ -118,6 +142,11 @@ type Proof = {
|
|
|
118
142
|
key: string;
|
|
119
143
|
reason: string;
|
|
120
144
|
verdict: 'cannot-measure';
|
|
145
|
+
} | {
|
|
146
|
+
counter: string;
|
|
147
|
+
key: string;
|
|
148
|
+
reason: string;
|
|
149
|
+
verdict: 'unproven';
|
|
121
150
|
}
|
|
122
151
|
/**
|
|
123
152
|
* Not a counter: `--exclusive` itself, measured by running two of it. A lock is a claim about the
|
|
@@ -141,8 +170,24 @@ type ProveResult = {
|
|
|
141
170
|
/** False as soon as one counter could not be shown to read its own planted finding. */
|
|
142
171
|
proven: boolean;
|
|
143
172
|
};
|
|
173
|
+
/** A counter whose tool could not be read. Never a number, and never a silent zero. */
|
|
174
|
+
type Refusal = {
|
|
175
|
+
key: string;
|
|
176
|
+
reason: string;
|
|
177
|
+
};
|
|
144
178
|
type RatchetResult = {
|
|
179
|
+
/**
|
|
180
|
+
* How many counters the config named. The denominator: `considered === measurements.length +
|
|
181
|
+
* refusals.length`, and a run whose two do not add up has lost one somewhere between the config
|
|
182
|
+
* and the report.
|
|
183
|
+
*/
|
|
184
|
+
considered: number;
|
|
145
185
|
measurements: Measurement[];
|
|
186
|
+
/**
|
|
187
|
+
* Every counter that blew up, not just the first. The throw that used to leave the run at the
|
|
188
|
+
* first failure also left every counter behind it unrun and unreported.
|
|
189
|
+
*/
|
|
190
|
+
refusals: Refusal[];
|
|
146
191
|
/** True when the baseline file was rewritten because something shrank. */
|
|
147
192
|
rewritten: boolean;
|
|
148
193
|
};
|
|
@@ -151,6 +196,90 @@ declare const CONFIG_FILE = "geonosis.ratchet.json";
|
|
|
151
196
|
/** The baseline key an entry writes. Defaults to the counter's own id. */
|
|
152
197
|
declare const keyOf: (entry: CounterConfig) => string;
|
|
153
198
|
declare const loadConfig: (cwd: string) => RatchetConfig;
|
|
199
|
+
/**
|
|
200
|
+
* #120: the ONE answer to "where does the baseline live". Three tools guessed instead of asking —
|
|
201
|
+
* rails' deny rail rendered `<root>/gate-baseline.json` over a repo whose baseline is
|
|
202
|
+
* `scripts/gate-baseline.json` (measured on sandbox-exec: the tampering write LANDED), and mcp's
|
|
203
|
+
* preamble said "no gate-baseline.json here" over twenty ratcheted counters. Anything that SPEAKS
|
|
204
|
+
* about the baseline resolves it here; `loadConfig` stays the strict door for anything that RUNS
|
|
205
|
+
* counters. Never throws: a missing or unreadable config means the default, because naming the
|
|
206
|
+
* baseline must work in repos that have not adopted the ratchet yet.
|
|
207
|
+
*/
|
|
208
|
+
declare const resolveBaseline: (cwd: string) => string;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The report envelope: what a tool CONSIDERED, against what it actually read.
|
|
212
|
+
*
|
|
213
|
+
* Four denominator bugs landed in one day — a migrations run that reported on three of four files,
|
|
214
|
+
* a plan check that printed `PASS — 0 plan(s)` over a directory of twenty-one, a parity run over a
|
|
215
|
+
* tree the second config ignored, a chains validator that walked a list it had already filtered.
|
|
216
|
+
* Every one of them was GREEN, and every one of them was green because the tool published the
|
|
217
|
+
* numerator and nobody published the denominator.
|
|
218
|
+
*
|
|
219
|
+
* So every gate writes this, and the law is arithmetic:
|
|
220
|
+
*
|
|
221
|
+
* considered === read + refused.length + excused.length
|
|
222
|
+
*
|
|
223
|
+
* checked HERE, at write time. A tool whose own accounting does not add up has not measured what it
|
|
224
|
+
* says it measured, so it renders no verdict at all: the write throws, the file is not created, and
|
|
225
|
+
* the bin exits 2. That is deliberately harsher than a failing gate — a wrong verdict is worse than
|
|
226
|
+
* no verdict, and a tool that has lost count of its own inputs cannot tell which it is.
|
|
227
|
+
*
|
|
228
|
+
* `refused` and `excused` are both "considered and NOT read". They differ in whose fault it is:
|
|
229
|
+
* refused is the tool unable to read the thing (an unparseable migration, a counter whose command
|
|
230
|
+
* blew up), excused is the thing legitimately exempt (a contract-migration marker the gate believed,
|
|
231
|
+
* a step the run never reached, a plan written before the contract). Both carry a reason, because a
|
|
232
|
+
* bucket without reasons is just a smaller silence.
|
|
233
|
+
*/
|
|
234
|
+
type EnvelopeEntry = {
|
|
235
|
+
/** What was not read — a file, a counter key, a step id. Whatever the tool names its inputs by. */
|
|
236
|
+
path: string;
|
|
237
|
+
/** Why it was not read, in a sentence a reader can act on. */
|
|
238
|
+
reason: string;
|
|
239
|
+
};
|
|
240
|
+
type ReportEnvelope = {
|
|
241
|
+
/** Everything this run took as its input. The denominator. */
|
|
242
|
+
considered: number;
|
|
243
|
+
durationMs: number;
|
|
244
|
+
excused: EnvelopeEntry[];
|
|
245
|
+
/** Whatever the tool found. Shape is the tool's own; nothing here reads into it. */
|
|
246
|
+
findings: unknown[];
|
|
247
|
+
/** How many of `considered` the run actually read. The numerator. */
|
|
248
|
+
read: number;
|
|
249
|
+
refused: EnvelopeEntry[];
|
|
250
|
+
/** The name the envelope is filed under, and the name a refusal is reported against. */
|
|
251
|
+
tool: string;
|
|
252
|
+
/** The build that wrote it. An envelope that cannot name its build dates nothing. */
|
|
253
|
+
version: string;
|
|
254
|
+
};
|
|
255
|
+
/** Where every tool writes, and the one directory the doctor's envelope check reads. */
|
|
256
|
+
declare const ENVELOPES_DIR = ".geonosis/envelopes";
|
|
257
|
+
declare const envelopePath: (root: string, tool: string) => string;
|
|
258
|
+
/** A run whose accounting does not add up. Never a verdict — the refusal to render one. */
|
|
259
|
+
declare class UnbalancedEnvelope extends Error {
|
|
260
|
+
constructor(message: string);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Writes the envelope, or refuses. Returns the path written so a caller can print it.
|
|
264
|
+
*
|
|
265
|
+
* The check is before the write and not after it, because a file on disk is a verdict: the doctor
|
|
266
|
+
* would read a half-true envelope and report on numbers nobody stands behind.
|
|
267
|
+
*/
|
|
268
|
+
declare const writeEnvelope: ({ envelope, next, root, }: {
|
|
269
|
+
envelope: ReportEnvelope;
|
|
270
|
+
/** The command a reader should run to see the accounting for themselves. */
|
|
271
|
+
next: string;
|
|
272
|
+
root: string;
|
|
273
|
+
}) => string;
|
|
274
|
+
/**
|
|
275
|
+
* The version of the package a module belongs to, found by walking up to the nearest manifest.
|
|
276
|
+
*
|
|
277
|
+
* Called with `import.meta.url`, it answers the same in `src/` under vitest and in the bundled
|
|
278
|
+
* `dist/` a bin loads — which is the whole point, because the envelope a consumer's CI writes is
|
|
279
|
+
* written by the build, and a version baked in at compile time would be the version of whoever
|
|
280
|
+
* bundled it.
|
|
281
|
+
*/
|
|
282
|
+
declare const versionOf: (moduleUrl: string) => string;
|
|
154
283
|
|
|
155
284
|
declare const COUNTERS: Counter[];
|
|
156
285
|
declare const counterById: (id: string) => Counter;
|
|
@@ -225,7 +354,7 @@ declare const runRatchet: ({ counters, cwd, tier, }: {
|
|
|
225
354
|
* the tier left out says so by name — printing a number for it would be the stale OK this exists
|
|
226
355
|
* to prevent.
|
|
227
356
|
*/
|
|
228
|
-
declare const formatReport: ({ measurements, rewritten }: RatchetResult) => string;
|
|
357
|
+
declare const formatReport: ({ measurements, refusals, rewritten }: RatchetResult) => string;
|
|
229
358
|
declare const formatProve: ({ proofs, proven }: ProveResult) => string;
|
|
230
359
|
|
|
231
360
|
/**
|
|
@@ -244,4 +373,4 @@ declare const formatProve: ({ proofs, proven }: ProveResult) => string;
|
|
|
244
373
|
*/
|
|
245
374
|
declare const runCommand: (cwd: string, counterId: string, env?: NodeJS.ProcessEnv) => (command: string) => CommandResult;
|
|
246
375
|
|
|
247
|
-
export { CONFIG_FILE, COUNTERS, type CommandResult, type Counter, type CounterConfig, type CounterContext, CounterError, type CounterProbe, type Holder, type Measurement, type Proof, type ProveResult, type RatchetConfig, type RatchetResult, type Verdict, acquireExclusive, counterById, formatProve, formatReport, heavyLockPath, keyOf, loadConfig, runCommand, runProve, runRatchet };
|
|
376
|
+
export { CONFIG_FILE, COUNTERS, type CommandResult, type Counter, type CounterConfig, type CounterContext, CounterError, type CounterProbe, ENVELOPES_DIR, type EnvelopeEntry, type Holder, type Measurement, type Proof, type ProveResult, type RatchetConfig, type RatchetResult, type Refusal, type ReportEnvelope, UnbalancedEnvelope, type Verdict, acquireExclusive, counterById, envelopePath, formatProve, formatReport, heavyLockPath, keyOf, loadConfig, resolveBaseline, runCommand, runProve, runRatchet, versionOf, writeEnvelope };
|
package/dist/index.js
CHANGED
|
@@ -2,29 +2,41 @@ import {
|
|
|
2
2
|
CONFIG_FILE,
|
|
3
3
|
COUNTERS,
|
|
4
4
|
CounterError,
|
|
5
|
+
ENVELOPES_DIR,
|
|
6
|
+
UnbalancedEnvelope,
|
|
5
7
|
acquireExclusive,
|
|
6
8
|
counterById,
|
|
9
|
+
envelopePath,
|
|
7
10
|
formatProve,
|
|
8
11
|
formatReport,
|
|
9
12
|
heavyLockPath,
|
|
10
13
|
keyOf,
|
|
11
14
|
loadConfig,
|
|
15
|
+
resolveBaseline,
|
|
12
16
|
runCommand,
|
|
13
17
|
runProve,
|
|
14
|
-
runRatchet
|
|
15
|
-
|
|
18
|
+
runRatchet,
|
|
19
|
+
versionOf,
|
|
20
|
+
writeEnvelope
|
|
21
|
+
} from "./chunk-ESN6ULZZ.js";
|
|
16
22
|
export {
|
|
17
23
|
CONFIG_FILE,
|
|
18
24
|
COUNTERS,
|
|
19
25
|
CounterError,
|
|
26
|
+
ENVELOPES_DIR,
|
|
27
|
+
UnbalancedEnvelope,
|
|
20
28
|
acquireExclusive,
|
|
21
29
|
counterById,
|
|
30
|
+
envelopePath,
|
|
22
31
|
formatProve,
|
|
23
32
|
formatReport,
|
|
24
33
|
heavyLockPath,
|
|
25
34
|
keyOf,
|
|
26
35
|
loadConfig,
|
|
36
|
+
resolveBaseline,
|
|
27
37
|
runCommand,
|
|
28
38
|
runProve,
|
|
29
|
-
runRatchet
|
|
39
|
+
runRatchet,
|
|
40
|
+
versionOf,
|
|
41
|
+
writeEnvelope
|
|
30
42
|
};
|