@geonosis/ratchet 0.5.0 → 1.1.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 +93 -10
- package/bin/geonosis-ratchet.mjs +24 -1
- package/dist/{chunk-FM5I6PRT.js → chunk-LSYVFUP4.js} +467 -107
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +247 -0
- package/dist/index.js +1 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -178,12 +178,30 @@ counter that reads a file rather than running a command (`lawLineCount`) prints
|
|
|
178
178
|
|
|
179
179
|
### `testFailures` reads the runner's summary, never the exit code
|
|
180
180
|
|
|
181
|
-
The counter looks for the runner's own count
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
181
|
+
The counter looks for the runner's own count and **refuses when it cannot parse one**. It never
|
|
182
|
+
reads the process exit code, because a test runner exiting 0 over a red suite is commoner than
|
|
183
|
+
anyone expects: `@cloudflare/vitest-pool-workers` 0.22 on vitest 4.1 exited 0 with failing tests on
|
|
184
|
+
every workerd suite of a consumer, and every gate that trusted the exit code reported green over red
|
|
185
|
+
for weeks. A refusal is loud and stops the run; a trusted 0 is silent and banks the red as a win.
|
|
186
|
+
|
|
187
|
+
**The lines it reads are the runner's summary lines, whole, and nothing else** — two dialects, both
|
|
188
|
+
captured from the binary rather than guessed at:
|
|
189
|
+
|
|
190
|
+
| Runner | The line | Read as |
|
|
191
|
+
| --- | --- | --- |
|
|
192
|
+
| vitest 3.2.7 | ` Tests 1 failed \| 1 passed (2)` | 1 |
|
|
193
|
+
| vitest 3.2.7 | ` Tests 3 passed (3)` · ` Tests 2 skipped (2)` | 0 |
|
|
194
|
+
| vitest 3.2.7 | ` Tests no tests` (a suite that threw before collecting) | **refused** |
|
|
195
|
+
| bun 1.4.0 | ` 1 fail` on its own line | 1 |
|
|
196
|
+
| bun 1.4.0 | ` 0 fail` | 0 |
|
|
197
|
+
|
|
198
|
+
Nothing else in the output counts. `Test Files 1 failed (1)` is a count of files, not of tests;
|
|
199
|
+
`(fail) one [11.71ms]` is bun naming one; `naming 4 failing test(s)` is some wrapper counting its
|
|
200
|
+
own findings. All three used to be read as a failure count — a **green run regressing a baseline**,
|
|
201
|
+
the false RED mirroring the false green above (backlog #44). A run that prints no summary line at
|
|
202
|
+
all is unmeasured, and the refusal names the two dialects so a third runner's output is a message
|
|
203
|
+
rather than a wrong number. Summaries **add up**: a command that invokes the runner twice prints
|
|
204
|
+
two, and reading the first and stopping banks the second suite's failures as a win.
|
|
187
205
|
|
|
188
206
|
For the same reason, **give every test package its own `testFailures` entry**, each with its own
|
|
189
207
|
`key`. One entry over one workspace measures one workspace; the suites it does not run are not zero
|
|
@@ -223,9 +241,10 @@ The counter then reads `numFailedTests`, and **refuses** rather than returning a
|
|
|
223
241
|
mid-run writes. The run did not finish, so there is no number to bank.
|
|
224
242
|
|
|
225
243
|
The summary mode stays the default; nothing changes for an entry that does not ask for a report.
|
|
226
|
-
`--prove` proves
|
|
227
|
-
|
|
228
|
-
about the mode they did
|
|
244
|
+
`--prove` proves all three: `testFailures` ships one probe per reading mode — `summary (vitest)`,
|
|
245
|
+
`summary (bun)`, `vitest-json` — and prints them by name (`PROVEN testFailuresApi (vitest-json)`),
|
|
246
|
+
because proving the mode nobody configured says nothing about the mode they did, and a summary
|
|
247
|
+
dialect nobody proved is a dialect nobody has been shown to read.
|
|
229
248
|
|
|
230
249
|
### `oxlintRule` counts a warned rule twice, on purpose
|
|
231
250
|
|
|
@@ -262,6 +281,11 @@ been shown to work — and this one guards the running time of every other gate.
|
|
|
262
281
|
nothing more. If a child command needs `NODE_OPTIONS` — a TypeScript shim, a loader — put it on
|
|
263
282
|
the script that invokes `geonosis-ratchet`, not on the counter's own line, and not only in your
|
|
264
283
|
interactive shell.
|
|
284
|
+
- **A path in a counter's command must be absolute, or `--prove` cannot run it.** A probe runs in a
|
|
285
|
+
scratch directory with none of your repo in it, so a loader or shim named relatively — `node
|
|
286
|
+
--import ./scripts/ts5-shim.mjs …` — resolves to nothing there and the counter comes back
|
|
287
|
+
"command did not run". The command is the same string in both places; write the path as
|
|
288
|
+
`$PWD/scripts/ts5-shim.mjs` and it works in the repo and in the probe alike.
|
|
265
289
|
- **A `typecheckErrors` counter needs the same precondition CI gives it: build the workspace
|
|
266
290
|
packages first.** In a fresh worktree the `dist/*.d.ts` files do not exist yet, and `tsc` reports
|
|
267
291
|
a false +N of missing-module errors that has nothing to do with the change under test. `pnpm build
|
|
@@ -321,10 +345,55 @@ Every counter takes its `command` from the config, so the toolchain stays the re
|
|
|
321
345
|
| `cloneCount` | jscpd's `Found N clones` | `command` |
|
|
322
346
|
| `knipIssues` | the totals under knip's unused-* headings | `command`, `headings` |
|
|
323
347
|
| `boundaryIssues` | `N issues found` from a boundary scan | `command` |
|
|
324
|
-
| `archViolations` | lines matching a marker your own architecture scan prints | `command`, `match` |
|
|
348
|
+
| `archViolations` | lines matching a marker your own architecture scan prints, refusing a non-zero exit that printed none of them — a scan that could not run is not a clean scan | `command`, `match` |
|
|
325
349
|
| `sumOfCounts` | the total of one capture group across a per-file census (`grep -rc`) | `command`, `match` |
|
|
326
350
|
| `lawLineCount` | the lines of the law file — a ceiling that can only come down | `path` |
|
|
327
351
|
| `runtimeCodeShipped` | 0 when a change shipped runtime code, 1 when it shipped none | `command`, `patterns` |
|
|
352
|
+
| `disabledCiJobs` | lines of `if: false` across the workflow files — a job switched off to get a release through, still off. A condition that merely mentions `false` is not one. No workflows directory at all reads 0 | `dir` |
|
|
353
|
+
| `bundleBytes` | one integer out of whatever your sizing command printed, separators and all; with `match`, the group that pattern names rather than the last integer, refusing when it matches nothing. **Tolerates.** | `command`, `match`, `tolerance` |
|
|
354
|
+
| `fastTierMs` | `finishedAt − startedAt` from the gate report `geonosis-verify` wrote, refusing a report of another tier rather than timing the wrong gate. **Tolerates.** | `report`, `tier`, `tolerance` |
|
|
355
|
+
| `testsWithoutRunner` | workspaces holding `*.test.*`, `*.spec.*` or `__tests__/` with no `test` script — the suites nobody runs, which read exactly like suites that pass | `script` |
|
|
356
|
+
| `packagesWithoutTypecheck` | workspaces with no `typecheck` script | `script` |
|
|
357
|
+
| `walkFindings` | the defects in the report `geonosis-walk` wrote, over every page; with `classes`, only those classes, refusing a class the walk does not have. A missing or unparsable report is a refusal — the walk writes none when it could not run | `report`, `classes` |
|
|
358
|
+
|
|
359
|
+
### `tolerance`, and the two counters that accept one
|
|
360
|
+
|
|
361
|
+
Most counters count findings, where one more is one too many. `bundleBytes` and `fastTierMs` measure
|
|
362
|
+
something that moves on a dependency patch nobody chose, and a gate that fails on +40 bytes is a
|
|
363
|
+
gate that gets switched off within a week. `tolerance` is the fraction of the baseline such a number
|
|
364
|
+
may drift up before the ratchet calls it growth:
|
|
365
|
+
|
|
366
|
+
```jsonc
|
|
367
|
+
{ "counter": "bundleBytes", "command": "…", "tolerance": 0.1 } // +10 % is noise, +11 % is debt
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**Only a counter that declares `tolerates` accepts one, and only `bundleBytes` and `fastTierMs` do.**
|
|
371
|
+
Anything else stops the run:
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
geonosis-ratchet: "oxlintErrors" does not accept a tolerance — only a counter that measures a
|
|
375
|
+
quantity declares one
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
That refusal is the point. A band around a findings count is not a tolerance, it is the gate turned
|
|
379
|
+
off from the config file: `"tolerance": 100` on `oxlintErrors` and the run still prints PASS. It is
|
|
380
|
+
law 2 — never downgrade a rule — wearing a friendlier name, and it would be the easiest edit in the
|
|
381
|
+
repo to get past a reviewer. Whether a number is a quantity or a count is the counter's to know, not
|
|
382
|
+
the config's, so the counter declares it.
|
|
383
|
+
|
|
384
|
+
It forgives noise upward, never downward: a shrink of any size is still a shrink and still lowers
|
|
385
|
+
the baseline, or the number would stop following the artefact down. A `tolerance` that is not a
|
|
386
|
+
non-negative number stops the run and says which entry it was — one that silently became `NaN`
|
|
387
|
+
would make every comparison false, which reads exactly like a counter that can never grow.
|
|
388
|
+
|
|
389
|
+
The `geonosis` Claude Code plugin puts any write that introduces or raises one in front of a human
|
|
390
|
+
(`permissionDecision: "ask"`): a band somebody chose and a band somebody's agent chose are not the
|
|
391
|
+
same thing.
|
|
392
|
+
|
|
393
|
+
Tolerance is also why a rewrite lowers only the numbers that **shrank**. While `held` implied
|
|
394
|
+
`now === baseline`, writing every measured key back was a harmless no-op; with a tolerance it is
|
|
395
|
+
not, and a counter that grew inside its tolerance would have had that growth laundered into its new
|
|
396
|
+
floor by the first unrelated win.
|
|
328
397
|
|
|
329
398
|
Two example configs from real repos live in `examples/` in the repository.
|
|
330
399
|
|
|
@@ -339,4 +408,18 @@ process.stdout.write(formatReport(result))
|
|
|
339
408
|
|
|
340
409
|
`counters` is a plain array, so a repo can add its own `{ id, run }` beside the built-ins.
|
|
341
410
|
|
|
411
|
+
## Two things this cannot see about itself
|
|
412
|
+
|
|
413
|
+
A baseline is lowered **in place** when a number shrinks; that is what locks a win in, and it is
|
|
414
|
+
also what lets a branch write any number it likes and stay green on every gate it runs. And a
|
|
415
|
+
workspace whose suite no `testFailures` entry covers is unmeasured, which reads exactly like green.
|
|
416
|
+
|
|
417
|
+
[`@geonosis/doctor`](https://www.npmjs.com/package/@geonosis/doctor) asks both from outside — the
|
|
418
|
+
baseline at HEAD against another ref, and which workspaces have a test script nothing reads a report
|
|
419
|
+
from:
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
npx geonosis-doctor --baseline-against origin/main --strict
|
|
423
|
+
```
|
|
424
|
+
|
|
342
425
|
Apache-2.0.
|
package/bin/geonosis-ratchet.mjs
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Committed, so `pnpm install` can link the bin on a fresh clone — before `pnpm build` has
|
|
3
3
|
// produced dist/. A bin that only exists after a build is a bin that is missing when you need it.
|
|
4
|
-
|
|
4
|
+
//
|
|
5
|
+
// In the repo, `src/` sits beside `dist/`, and a dist older than src answered for a fix it did not
|
|
6
|
+
// carry once (#62). The published package ships no src, so there the check is skipped.
|
|
7
|
+
import { existsSync, readdirSync, statSync } from 'node:fs'
|
|
8
|
+
import { dirname, join } from 'node:path'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
|
|
11
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const newest = (dir) =>
|
|
13
|
+
existsSync(dir)
|
|
14
|
+
? readdirSync(dir, { withFileTypes: true }).reduce((most, entry) => {
|
|
15
|
+
const at = join(dir, entry.name)
|
|
16
|
+
return Math.max(most, entry.isDirectory() ? newest(at) : statSync(at).mtimeMs)
|
|
17
|
+
}, 0)
|
|
18
|
+
: 0
|
|
19
|
+
const src = join(here, '..', 'src')
|
|
20
|
+
if (existsSync(src) && newest(src) > newest(join(here, '..', 'dist'))) {
|
|
21
|
+
process.stderr.write(
|
|
22
|
+
'geonosis-ratchet: dist is older than src — run pnpm build before trusting this bin.\n',
|
|
23
|
+
)
|
|
24
|
+
process.exit(2)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
await import('../dist/cli.js')
|
|
@@ -140,7 +140,9 @@ import { execSync } from "child_process";
|
|
|
140
140
|
var ANSI = /\[[0-9;]*m/g;
|
|
141
141
|
var runCommand = (cwd, counterId, env) => (command) => {
|
|
142
142
|
try {
|
|
143
|
-
const output = execSync(
|
|
143
|
+
const output = execSync(`(
|
|
144
|
+
${command}
|
|
145
|
+
) 2>&1`, {
|
|
144
146
|
cwd,
|
|
145
147
|
encoding: "utf8",
|
|
146
148
|
env,
|
|
@@ -343,11 +345,26 @@ var recorded = (run) => {
|
|
|
343
345
|
}
|
|
344
346
|
};
|
|
345
347
|
};
|
|
346
|
-
var verdictOf = (now, baseline) => {
|
|
347
|
-
if (now > baseline) return "grew";
|
|
348
|
+
var verdictOf = (now, baseline, tolerance) => {
|
|
349
|
+
if (now > baseline * (1 + tolerance)) return "grew";
|
|
348
350
|
if (now < baseline) return "shrank";
|
|
349
351
|
return "held";
|
|
350
352
|
};
|
|
353
|
+
var toleranceOf = (entry, key, counter) => {
|
|
354
|
+
const declared = entry.tolerance;
|
|
355
|
+
if (declared === void 0) return 0;
|
|
356
|
+
if (counter.tolerates !== true) {
|
|
357
|
+
throw new Error(
|
|
358
|
+
`"${key}" does not accept a tolerance \u2014 only a counter that measures a quantity declares one`
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
if (typeof declared !== "number" || !Number.isFinite(declared) || declared < 0) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`"${key}" has a "tolerance" that is not a non-negative number: ${JSON.stringify(declared)}`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
return declared;
|
|
367
|
+
};
|
|
351
368
|
var outsideTier2 = (entry, tier) => entry.tiers !== void 0 && !entry.tiers.includes(tier);
|
|
352
369
|
var runRatchet = async ({
|
|
353
370
|
counters,
|
|
@@ -380,9 +397,10 @@ var runRatchet = async ({
|
|
|
380
397
|
`${config.baseline} has no number for "${key}" \u2014 add it before enabling the counter`
|
|
381
398
|
);
|
|
382
399
|
}
|
|
400
|
+
const tolerance = toleranceOf(entry, key, counter);
|
|
383
401
|
const recorder = recorded(runCommand(cwd, entry.counter));
|
|
384
402
|
const now = await counter.run({ cwd, key, params: entry, run: recorder.run });
|
|
385
|
-
const verdict = verdictOf(now, limit);
|
|
403
|
+
const verdict = verdictOf(now, limit, tolerance);
|
|
386
404
|
measurements.push({
|
|
387
405
|
baseline: limit,
|
|
388
406
|
evidence: verdict === "grew" ? recorder.last() : [],
|
|
@@ -396,7 +414,7 @@ var runRatchet = async ({
|
|
|
396
414
|
if (shrank && !grew) {
|
|
397
415
|
const next = { ...baseline };
|
|
398
416
|
for (const one of measurements) {
|
|
399
|
-
if (one.verdict
|
|
417
|
+
if (one.verdict === "shrank") next[one.key] = one.now;
|
|
400
418
|
}
|
|
401
419
|
writeFileSync2(baselinePath, `${JSON.stringify(next, null, 2)}
|
|
402
420
|
`);
|
|
@@ -434,13 +452,120 @@ var captured = (sample) => ({
|
|
|
434
452
|
});
|
|
435
453
|
|
|
436
454
|
// src/counters/arch.ts
|
|
455
|
+
var refuse = (command, { code, output }) => {
|
|
456
|
+
const tail = output.trim().split("\n").slice(-10).join("\n");
|
|
457
|
+
throw new CounterError(
|
|
458
|
+
"archViolations",
|
|
459
|
+
`\`${command}\` exited ${code} and printed no findings \u2014 a scan that could not run is not a clean scan:
|
|
460
|
+
${tail === "" ? "(it printed nothing at all)" : tail}`
|
|
461
|
+
);
|
|
462
|
+
};
|
|
463
|
+
var scannerProbe = {
|
|
464
|
+
command: () => "node scanner.mjs",
|
|
465
|
+
input: (dir) => {
|
|
466
|
+
plant(
|
|
467
|
+
dir,
|
|
468
|
+
"scanner.mjs",
|
|
469
|
+
[
|
|
470
|
+
"process.stdout.write('\\u2717 packages/a/src/cells/one.tsx cell imports cell\\n')",
|
|
471
|
+
"process.stdout.write('\\u2717 packages/a/src/tissues/two.tsx tissue holds state\\n')",
|
|
472
|
+
"process.exit(1)",
|
|
473
|
+
""
|
|
474
|
+
].join("\n")
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
};
|
|
437
478
|
var archViolations = {
|
|
438
479
|
id: "archViolations",
|
|
439
|
-
probe: { ...
|
|
480
|
+
probe: { ...scannerProbe, expect: 2 },
|
|
440
481
|
run: async ({ params, run }) => {
|
|
441
482
|
const command = stringParam("archViolations", params, "command");
|
|
442
483
|
const match = new RegExp(stringParam("archViolations", params, "match", "^\u2717"));
|
|
443
|
-
|
|
484
|
+
const result = run(command);
|
|
485
|
+
const found = result.output.split("\n").filter((line) => match.test(line)).length;
|
|
486
|
+
if (found === 0 && result.code !== 0) refuse(command, result);
|
|
487
|
+
return found;
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// src/counters/bundle.ts
|
|
492
|
+
var INTEGER = /\d[\d,_]*/g;
|
|
493
|
+
var asBytes = (digits) => Number(digits.replaceAll(/[,_]/g, ""));
|
|
494
|
+
var bundleBytes = {
|
|
495
|
+
id: "bundleBytes",
|
|
496
|
+
tolerates: true,
|
|
497
|
+
probe: { ...captured("dist/index.js\n minified 4096\n"), expect: 4096 },
|
|
498
|
+
run: async ({ params, run }) => {
|
|
499
|
+
const command = stringParam("bundleBytes", params, "command");
|
|
500
|
+
const output = run(command).output;
|
|
501
|
+
const pattern = params.match;
|
|
502
|
+
if (pattern !== void 0) {
|
|
503
|
+
if (typeof pattern !== "string") {
|
|
504
|
+
throw new CounterError("bundleBytes", `"match" must be a regular expression source`);
|
|
505
|
+
}
|
|
506
|
+
const found = new RegExp(pattern).exec(output);
|
|
507
|
+
if (found === null) {
|
|
508
|
+
throw new CounterError(
|
|
509
|
+
"bundleBytes",
|
|
510
|
+
`nothing matched /${pattern}/ in what \`${command}\` printed`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
const digits = found[1] ?? found[0];
|
|
514
|
+
const value = asBytes(digits);
|
|
515
|
+
if (!Number.isFinite(value)) {
|
|
516
|
+
throw new CounterError(
|
|
517
|
+
"bundleBytes",
|
|
518
|
+
`/${pattern}/ matched "${digits}", which is no number`
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
return value;
|
|
522
|
+
}
|
|
523
|
+
const integers = output.match(INTEGER);
|
|
524
|
+
if (integers === null || integers.length === 0) {
|
|
525
|
+
throw new CounterError("bundleBytes", `\`${command}\` printed no number to read as bytes`);
|
|
526
|
+
}
|
|
527
|
+
return asBytes(integers[integers.length - 1] ?? "");
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
// src/counters/ci.ts
|
|
532
|
+
import { readdirSync, readFileSync as readFileSync4 } from "fs";
|
|
533
|
+
import { join as join4, resolve as resolve5 } from "path";
|
|
534
|
+
var DISABLED = /\bif:[ \t]*false\b/;
|
|
535
|
+
var WORKFLOW = /\.ya?ml$/;
|
|
536
|
+
var disabledCiJobs = {
|
|
537
|
+
id: "disabledCiJobs",
|
|
538
|
+
probe: {
|
|
539
|
+
expect: 1,
|
|
540
|
+
input: (dir) => plant(
|
|
541
|
+
dir,
|
|
542
|
+
".github/workflows/ci.yml",
|
|
543
|
+
["jobs:", " build:", " if: false", " steps: []", ""].join("\n")
|
|
544
|
+
)
|
|
545
|
+
},
|
|
546
|
+
run: async ({ cwd, params }) => {
|
|
547
|
+
const relative = stringParam("disabledCiJobs", params, "dir", ".github/workflows");
|
|
548
|
+
const dir = resolve5(cwd, relative);
|
|
549
|
+
let files;
|
|
550
|
+
try {
|
|
551
|
+
files = readdirSync(dir, { withFileTypes: true }).filter((entry) => WORKFLOW.test(entry.name)).map((entry) => join4(dir, entry.name));
|
|
552
|
+
} catch {
|
|
553
|
+
return 0;
|
|
554
|
+
}
|
|
555
|
+
let disabled = 0;
|
|
556
|
+
for (const file of files) {
|
|
557
|
+
let contents;
|
|
558
|
+
try {
|
|
559
|
+
contents = readFileSync4(file, "utf8");
|
|
560
|
+
} catch (error) {
|
|
561
|
+
throw new CounterError(
|
|
562
|
+
"disabledCiJobs",
|
|
563
|
+
`could not read ${relative}/${file.split("/").pop() ?? file}: ${error.message}`
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
disabled += contents.split("\n").filter((line) => DISABLED.test(line)).length;
|
|
567
|
+
}
|
|
568
|
+
return disabled;
|
|
444
569
|
}
|
|
445
570
|
};
|
|
446
571
|
|
|
@@ -491,7 +616,7 @@ var boundaryIssues = {
|
|
|
491
616
|
|
|
492
617
|
// src/counters/format.ts
|
|
493
618
|
import { existsSync as existsSync4 } from "fs";
|
|
494
|
-
import { resolve as
|
|
619
|
+
import { resolve as resolve6 } from "path";
|
|
495
620
|
var unformattedFiles = {
|
|
496
621
|
id: "unformattedFiles",
|
|
497
622
|
probe: {
|
|
@@ -506,13 +631,67 @@ var unformattedFiles = {
|
|
|
506
631
|
"command",
|
|
507
632
|
"npx oxfmt --config .oxfmtrc.json --list-different ."
|
|
508
633
|
);
|
|
509
|
-
return run(command).output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && existsSync4(
|
|
634
|
+
return run(command).output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && existsSync4(resolve6(cwd, line))).length;
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// src/counters/gate-report.ts
|
|
639
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
640
|
+
import { resolve as resolve7 } from "path";
|
|
641
|
+
var DEFAULT_REPORT = ".geonosis/gate-report.json";
|
|
642
|
+
var fastTierMs = {
|
|
643
|
+
id: "fastTierMs",
|
|
644
|
+
tolerates: true,
|
|
645
|
+
probe: {
|
|
646
|
+
expect: 4500,
|
|
647
|
+
input: (dir) => plant(
|
|
648
|
+
dir,
|
|
649
|
+
DEFAULT_REPORT,
|
|
650
|
+
JSON.stringify({
|
|
651
|
+
finishedAt: "2026-08-30T10:00:04.500Z",
|
|
652
|
+
ok: true,
|
|
653
|
+
startedAt: "2026-08-30T10:00:00.000Z",
|
|
654
|
+
steps: [],
|
|
655
|
+
tier: "fast"
|
|
656
|
+
})
|
|
657
|
+
)
|
|
658
|
+
},
|
|
659
|
+
run: async ({ cwd, params }) => {
|
|
660
|
+
const relative = stringParam("fastTierMs", params, "report", DEFAULT_REPORT);
|
|
661
|
+
const wanted = stringParam("fastTierMs", params, "tier", "fast");
|
|
662
|
+
const path = resolve7(cwd, relative);
|
|
663
|
+
if (!existsSync5(path)) {
|
|
664
|
+
throw new CounterError(
|
|
665
|
+
"fastTierMs",
|
|
666
|
+
`no gate report at ${relative} \u2014 run \`geonosis-verify ${wanted}\` before measuring it`
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
let report;
|
|
670
|
+
try {
|
|
671
|
+
report = JSON.parse(readFileSync5(path, "utf8"));
|
|
672
|
+
} catch (error) {
|
|
673
|
+
throw new CounterError(
|
|
674
|
+
"fastTierMs",
|
|
675
|
+
`${relative} does not parse: ${error.message}`
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
if (report.tier !== wanted) {
|
|
679
|
+
throw new CounterError(
|
|
680
|
+
"fastTierMs",
|
|
681
|
+
`${relative} is a report of tier "${String(report.tier)}", not "${wanted}"`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
const spent = Date.parse(String(report.finishedAt)) - Date.parse(String(report.startedAt));
|
|
685
|
+
if (!Number.isFinite(spent)) {
|
|
686
|
+
throw new CounterError("fastTierMs", `${relative} has no timestamps a reader can subtract`);
|
|
687
|
+
}
|
|
688
|
+
return spent;
|
|
510
689
|
}
|
|
511
690
|
};
|
|
512
691
|
|
|
513
692
|
// src/counters/law.ts
|
|
514
|
-
import { existsSync as
|
|
515
|
-
import { resolve as
|
|
693
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
694
|
+
import { resolve as resolve8 } from "path";
|
|
516
695
|
var lawLineCount = {
|
|
517
696
|
id: "lawLineCount",
|
|
518
697
|
probe: {
|
|
@@ -522,15 +701,15 @@ var lawLineCount = {
|
|
|
522
701
|
},
|
|
523
702
|
run: async ({ cwd, params }) => {
|
|
524
703
|
const relative = stringParam("lawLineCount", params, "path", "CLAUDE.md");
|
|
525
|
-
const path =
|
|
526
|
-
if (!
|
|
527
|
-
return
|
|
704
|
+
const path = resolve8(cwd, relative);
|
|
705
|
+
if (!existsSync6(path)) throw new CounterError("lawLineCount", `no law file at ${relative}`);
|
|
706
|
+
return readFileSync6(path, "utf8").replace(/\n$/, "").split("\n").length;
|
|
528
707
|
}
|
|
529
708
|
};
|
|
530
709
|
|
|
531
710
|
// src/counters/oxlint.ts
|
|
532
|
-
import { existsSync as
|
|
533
|
-
import { resolve as
|
|
711
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
712
|
+
import { resolve as resolve9 } from "path";
|
|
534
713
|
var DEFAULT_COMMAND = "npx oxlint --format=unix --config .oxlintrc.json .";
|
|
535
714
|
var PROBE_COMMAND = "oxlint --format=unix --config .oxlintrc.json .";
|
|
536
715
|
var oxlintProbe = (severity, rule, source) => ({
|
|
@@ -584,21 +763,21 @@ var readFindings = (counter, { code, output }, expect) => {
|
|
|
584
763
|
const agent = countFindings(output, AGENT_FINDING, "error");
|
|
585
764
|
const problems = UNIX_SUMMARY.exec(output);
|
|
586
765
|
const found = DEFAULT_SUMMARY.exec(output);
|
|
587
|
-
const
|
|
766
|
+
const refuse2 = (why) => {
|
|
588
767
|
throw new CounterError(counter, `${why} \u2014 oxlint's output changed:
|
|
589
768
|
${output.trim()}`);
|
|
590
769
|
};
|
|
591
770
|
if (total(unix) > 0 || problems !== null) {
|
|
592
|
-
if (problems?.[1] === void 0) return
|
|
771
|
+
if (problems?.[1] === void 0) return refuse2('unix findings with no "N problems" summary');
|
|
593
772
|
if (Number(problems[1]) !== total(unix)) {
|
|
594
|
-
return
|
|
773
|
+
return refuse2(`the summary says ${problems[1]} problems, the lines say ${total(unix)}`);
|
|
595
774
|
}
|
|
596
775
|
return unix;
|
|
597
776
|
}
|
|
598
777
|
if (found?.[1] !== void 0 && found[2] !== void 0) {
|
|
599
778
|
const summary = { errors: Number(found[2]), warnings: Number(found[1]) };
|
|
600
779
|
if (total(agent) > 0 && (agent.errors !== summary.errors || agent.warnings !== summary.warnings)) {
|
|
601
|
-
return
|
|
780
|
+
return refuse2(
|
|
602
781
|
`the summary says ${summary.errors} errors and ${summary.warnings} warnings, the lines say ${agent.errors} and ${agent.warnings}`
|
|
603
782
|
);
|
|
604
783
|
}
|
|
@@ -607,11 +786,11 @@ ${output.trim()}`);
|
|
|
607
786
|
if (total(agent) > 0) return agent;
|
|
608
787
|
if (code === 0) {
|
|
609
788
|
if (expect !== void 0 && spokenByTheTool(output) !== "") {
|
|
610
|
-
return
|
|
789
|
+
return refuse2(`asked for --format=${expect} and got output in no shape this counter reads`);
|
|
611
790
|
}
|
|
612
791
|
return { errors: 0, warnings: 0 };
|
|
613
792
|
}
|
|
614
|
-
return
|
|
793
|
+
return refuse2(`the tool exited ${code} and printed no findings and no summary`);
|
|
615
794
|
};
|
|
616
795
|
var oxlintErrors = {
|
|
617
796
|
id: "oxlintErrors",
|
|
@@ -669,18 +848,18 @@ var oxlintRule = {
|
|
|
669
848
|
const config = typeof params.config === "string" ? params.config : "";
|
|
670
849
|
const expect = expectedFormat("oxlintRule", params);
|
|
671
850
|
if (config === "") return countRule(run(command), rule, expect);
|
|
672
|
-
const source =
|
|
673
|
-
if (!
|
|
851
|
+
const source = resolve9(cwd, config);
|
|
852
|
+
if (!existsSync7(source)) throw new CounterError("oxlintRule", `no config at ${config}`);
|
|
674
853
|
const strictName = `.oxlintrc.ratchet-${key}.json`;
|
|
675
|
-
const strict =
|
|
854
|
+
const strict = readFileSync7(source, "utf8").replace(
|
|
676
855
|
new RegExp(`("[^"]*${escapeForRegex(rule)}"\\s*:\\s*\\[?\\s*)"(warn|off)"`),
|
|
677
856
|
'$1"error"'
|
|
678
857
|
);
|
|
679
|
-
writeFileSync4(
|
|
858
|
+
writeFileSync4(resolve9(cwd, strictName), strict);
|
|
680
859
|
try {
|
|
681
860
|
return countRule(run(command.replace("{config}", strictName)), rule, expect);
|
|
682
861
|
} finally {
|
|
683
|
-
rmSync3(
|
|
862
|
+
rmSync3(resolve9(cwd, strictName), { force: true });
|
|
684
863
|
}
|
|
685
864
|
}
|
|
686
865
|
};
|
|
@@ -708,6 +887,143 @@ var runtimeCodeShipped = {
|
|
|
708
887
|
}
|
|
709
888
|
};
|
|
710
889
|
|
|
890
|
+
// src/counters/scripts.ts
|
|
891
|
+
import { readdirSync as readdirSync3 } from "fs";
|
|
892
|
+
import { join as join6 } from "path";
|
|
893
|
+
|
|
894
|
+
// src/counters/workspace.ts
|
|
895
|
+
import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync8 } from "fs";
|
|
896
|
+
import { join as join5, resolve as resolve10 } from "path";
|
|
897
|
+
var SKIP = /^(node_modules|\.)/;
|
|
898
|
+
var childDirs = (dir) => {
|
|
899
|
+
try {
|
|
900
|
+
return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !SKIP.test(entry.name)).map((entry) => join5(dir, entry.name));
|
|
901
|
+
} catch {
|
|
902
|
+
return [];
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
var descendants = (dir, depth) => depth === 0 ? [dir] : [dir, ...childDirs(dir).flatMap((child) => descendants(child, depth - 1))];
|
|
906
|
+
var expand = (root, pattern) => {
|
|
907
|
+
const segments = pattern.split("/").filter((one) => one !== "" && one !== ".");
|
|
908
|
+
let dirs = [root];
|
|
909
|
+
for (const segment of segments) {
|
|
910
|
+
dirs = segment === "*" ? dirs.flatMap((dir) => childDirs(dir)) : segment === "**" ? dirs.flatMap((dir) => descendants(dir, 3)) : dirs.map((dir) => join5(dir, segment)).filter((dir) => existsSync8(dir));
|
|
911
|
+
}
|
|
912
|
+
return dirs;
|
|
913
|
+
};
|
|
914
|
+
var QUOTED = /^['"]|['"]$/g;
|
|
915
|
+
var cleaned = (value) => value.replace(/#.*$/, "").trim().replaceAll(QUOTED, "");
|
|
916
|
+
var pnpmPatterns = (path) => {
|
|
917
|
+
const lines = readFileSync8(path, "utf8").split("\n");
|
|
918
|
+
const at = lines.findIndex((line) => line.startsWith("packages:"));
|
|
919
|
+
if (at === -1) return [];
|
|
920
|
+
const inline = lines[at]?.slice("packages:".length).trim() ?? "";
|
|
921
|
+
if (inline.startsWith("[")) {
|
|
922
|
+
return inline.replace(/^\[|\]$/g, "").split(",").map(cleaned).filter((one) => one !== "");
|
|
923
|
+
}
|
|
924
|
+
const patterns = [];
|
|
925
|
+
for (const line of lines.slice(at + 1)) {
|
|
926
|
+
const item = /^\s*-\s*(.+)$/.exec(line);
|
|
927
|
+
if (item?.[1] !== void 0) {
|
|
928
|
+
patterns.push(cleaned(item[1]));
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
if (line.trim() !== "") break;
|
|
932
|
+
}
|
|
933
|
+
return patterns;
|
|
934
|
+
};
|
|
935
|
+
var npmPatterns = (path) => {
|
|
936
|
+
const parsed = JSON.parse(readFileSync8(path, "utf8"));
|
|
937
|
+
const declared = Array.isArray(parsed.workspaces) ? parsed.workspaces : parsed.workspaces?.packages ?? [];
|
|
938
|
+
return declared.filter((one) => typeof one === "string");
|
|
939
|
+
};
|
|
940
|
+
var nameOf = (dir) => {
|
|
941
|
+
const manifest = join5(dir, "package.json");
|
|
942
|
+
if (!existsSync8(manifest)) return void 0;
|
|
943
|
+
try {
|
|
944
|
+
const { name } = JSON.parse(readFileSync8(manifest, "utf8"));
|
|
945
|
+
return typeof name === "string" && name !== "" ? name : void 0;
|
|
946
|
+
} catch {
|
|
947
|
+
return void 0;
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
var workspaceDirs = (cwd) => {
|
|
951
|
+
const root = resolve10(cwd);
|
|
952
|
+
const pnpm = join5(root, "pnpm-workspace.yaml");
|
|
953
|
+
const manifest = join5(root, "package.json");
|
|
954
|
+
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(join5(dir, "package.json")));
|
|
956
|
+
return [...new Set(dirs)];
|
|
957
|
+
};
|
|
958
|
+
var manifestOf = (dir) => {
|
|
959
|
+
try {
|
|
960
|
+
const parsed = JSON.parse(readFileSync8(join5(dir, "package.json"), "utf8"));
|
|
961
|
+
return typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
962
|
+
} catch {
|
|
963
|
+
return void 0;
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
var scriptsOf = (dir) => {
|
|
967
|
+
const scripts = manifestOf(dir)?.scripts;
|
|
968
|
+
return typeof scripts === "object" && scripts !== null ? Object.keys(scripts) : [];
|
|
969
|
+
};
|
|
970
|
+
var workspacePackageNames = (cwd) => {
|
|
971
|
+
const names = workspaceDirs(cwd).map((dir) => nameOf(dir)).filter((name) => name !== void 0);
|
|
972
|
+
return [...new Set(names)];
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
// src/counters/scripts.ts
|
|
976
|
+
var TEST_FILE = /(\.test\.|\.spec\.)/;
|
|
977
|
+
var TEST_DIR = "__tests__";
|
|
978
|
+
var SKIP2 = /^(node_modules|dist|\.)/;
|
|
979
|
+
var holdsTests = (dir, depth = 6) => {
|
|
980
|
+
let entries;
|
|
981
|
+
try {
|
|
982
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
983
|
+
} catch {
|
|
984
|
+
return false;
|
|
985
|
+
}
|
|
986
|
+
for (const entry of entries) {
|
|
987
|
+
if (entry.isDirectory()) {
|
|
988
|
+
if (entry.name === TEST_DIR) return true;
|
|
989
|
+
if (SKIP2.test(entry.name) || depth === 0) continue;
|
|
990
|
+
if (holdsTests(join6(dir, entry.name), depth - 1)) return true;
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
if (TEST_FILE.test(entry.name)) return true;
|
|
994
|
+
}
|
|
995
|
+
return false;
|
|
996
|
+
};
|
|
997
|
+
var testsWithoutRunner = {
|
|
998
|
+
id: "testsWithoutRunner",
|
|
999
|
+
probe: {
|
|
1000
|
+
expect: 1,
|
|
1001
|
+
input: (dir) => {
|
|
1002
|
+
plant(dir, "pnpm-workspace.yaml", "packages:\n - packages/*\n");
|
|
1003
|
+
plant(dir, "packages/lonely/package.json", '{"name":"lonely","scripts":{"build":"tsup"}}');
|
|
1004
|
+
plant(dir, "packages/lonely/src/thing.test.ts", 'test("x", () => {})\n');
|
|
1005
|
+
}
|
|
1006
|
+
},
|
|
1007
|
+
run: async ({ cwd, params }) => {
|
|
1008
|
+
const script = stringParam("testsWithoutRunner", params, "script", "test");
|
|
1009
|
+
return workspaceDirs(cwd).filter((dir) => holdsTests(dir) && !scriptsOf(dir).includes(script)).length;
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
var packagesWithoutTypecheck = {
|
|
1013
|
+
id: "packagesWithoutTypecheck",
|
|
1014
|
+
probe: {
|
|
1015
|
+
expect: 1,
|
|
1016
|
+
input: (dir) => {
|
|
1017
|
+
plant(dir, "pnpm-workspace.yaml", "packages:\n - packages/*\n");
|
|
1018
|
+
plant(dir, "packages/untyped/package.json", '{"name":"untyped","scripts":{"build":"tsup"}}');
|
|
1019
|
+
}
|
|
1020
|
+
},
|
|
1021
|
+
run: async ({ cwd, params }) => {
|
|
1022
|
+
const script = stringParam("packagesWithoutTypecheck", params, "script", "typecheck");
|
|
1023
|
+
return workspaceDirs(cwd).filter((dir) => !scriptsOf(dir).includes(script)).length;
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
|
|
711
1027
|
// src/counters/sum-of-counts.ts
|
|
712
1028
|
var sumOfCounts = {
|
|
713
1029
|
id: "sumOfCounts",
|
|
@@ -720,26 +1036,41 @@ var sumOfCounts = {
|
|
|
720
1036
|
};
|
|
721
1037
|
|
|
722
1038
|
// src/counters/tests.ts
|
|
723
|
-
import { existsSync as
|
|
1039
|
+
import { existsSync as existsSync9, mkdtempSync as mkdtempSync2, readFileSync as readFileSync9, rmSync as rmSync4 } from "fs";
|
|
724
1040
|
import { tmpdir as tmpdir2 } from "os";
|
|
725
|
-
import { join as
|
|
726
|
-
var FAILED = /(\d+)\s+fail(?:ed|ing|s)?\b/;
|
|
727
|
-
var PASSED = /(\d+)\s+pass(?:ed|ing|es)?\b/;
|
|
1041
|
+
import { join as join7, resolve as resolve11 } from "path";
|
|
728
1042
|
var COUNTER = "testFailures";
|
|
1043
|
+
var VITEST_LINE = /^\s*Tests {2,}(.+?)\s*$/;
|
|
1044
|
+
var VITEST_TOTAL = /\(\d+\)$/;
|
|
1045
|
+
var BUN_LINE = /^\s*(\d+) fail\s*$/;
|
|
1046
|
+
var FAILED_IN = /(\d+) failed\b/;
|
|
1047
|
+
var DIALECTS = 'vitest ("Tests N failed | M passed (T)") and bun ("N fail")';
|
|
729
1048
|
var VITEST_JSON = "vitest-json";
|
|
730
1049
|
var PLACEHOLDER = "{report}";
|
|
1050
|
+
var summaries = (output) => {
|
|
1051
|
+
const found = [];
|
|
1052
|
+
for (const line of output.split("\n")) {
|
|
1053
|
+
const vitest = VITEST_LINE.exec(line)?.[1];
|
|
1054
|
+
if (vitest !== void 0 && VITEST_TOTAL.test(vitest)) {
|
|
1055
|
+
found.push(Number(FAILED_IN.exec(vitest)?.[1] ?? 0));
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
const bun = BUN_LINE.exec(line)?.[1];
|
|
1059
|
+
if (bun !== void 0) found.push(Number(bun));
|
|
1060
|
+
}
|
|
1061
|
+
return found;
|
|
1062
|
+
};
|
|
731
1063
|
var fromSummary = (output) => {
|
|
732
|
-
const
|
|
733
|
-
if (
|
|
734
|
-
if (PASSED.test(output)) return 0;
|
|
1064
|
+
const found = summaries(output);
|
|
1065
|
+
if (found.length > 0) return found.reduce((total2, one) => total2 + one, 0);
|
|
735
1066
|
throw new CounterError(
|
|
736
1067
|
COUNTER,
|
|
737
|
-
`no
|
|
1068
|
+
`no runner summary line in the output \u2014 this reads ${DIALECTS}:
|
|
738
1069
|
${output.trim().slice(-500)}`
|
|
739
1070
|
);
|
|
740
1071
|
};
|
|
741
1072
|
var fromReport = (path) => {
|
|
742
|
-
if (!
|
|
1073
|
+
if (!existsSync9(path)) {
|
|
743
1074
|
throw new CounterError(
|
|
744
1075
|
COUNTER,
|
|
745
1076
|
`the runner wrote no report at ${path} \u2014 a crash before the reporter is not a pass`
|
|
@@ -747,7 +1078,7 @@ var fromReport = (path) => {
|
|
|
747
1078
|
}
|
|
748
1079
|
let report;
|
|
749
1080
|
try {
|
|
750
|
-
report = JSON.parse(
|
|
1081
|
+
report = JSON.parse(readFileSync9(path, "utf8"));
|
|
751
1082
|
} catch (error) {
|
|
752
1083
|
throw new CounterError(
|
|
753
1084
|
COUNTER,
|
|
@@ -771,19 +1102,32 @@ var fromReport = (path) => {
|
|
|
771
1102
|
};
|
|
772
1103
|
var reportPathFor = (cwd, params, command) => {
|
|
773
1104
|
const named = params.reportPath;
|
|
774
|
-
if (typeof named === "string" && named !== "") return { own: false, path:
|
|
1105
|
+
if (typeof named === "string" && named !== "") return { own: false, path: resolve11(cwd, named) };
|
|
775
1106
|
if (!command.includes(PLACEHOLDER)) {
|
|
776
1107
|
throw new CounterError(
|
|
777
1108
|
COUNTER,
|
|
778
1109
|
`report: "${VITEST_JSON}" needs somewhere to put the report \u2014 write ${PLACEHOLDER} into the command (--outputFile=${PLACEHOLDER}) or give the entry a "reportPath"`
|
|
779
1110
|
);
|
|
780
1111
|
}
|
|
781
|
-
return { own: true, path:
|
|
1112
|
+
return { own: true, path: join7(mkdtempSync2(join7(tmpdir2(), "geonosis-report-")), "report.json") };
|
|
782
1113
|
};
|
|
783
1114
|
var testFailures = {
|
|
784
1115
|
id: COUNTER,
|
|
785
1116
|
probe: [
|
|
786
|
-
{
|
|
1117
|
+
{
|
|
1118
|
+
...captured(" Tests 1 failed | 0 passed (1)\n"),
|
|
1119
|
+
expect: 1,
|
|
1120
|
+
name: "summary (vitest)"
|
|
1121
|
+
},
|
|
1122
|
+
{
|
|
1123
|
+
// Captured from bun 1.4.0 over one failing test of two, its per-test line included: the
|
|
1124
|
+
// summary is two lines below one that also says "fail", and only one of them is the count.
|
|
1125
|
+
...captured(
|
|
1126
|
+
"(fail) one [11.71ms]\n\n 1 pass\n 1 fail\n 2 expect() calls\nRan 2 tests across 1 file. [16.00ms]\n"
|
|
1127
|
+
),
|
|
1128
|
+
expect: 1,
|
|
1129
|
+
name: "summary (bun)"
|
|
1130
|
+
},
|
|
787
1131
|
{
|
|
788
1132
|
// `cat` stands in for the runner: the placeholder is substituted into it, so a counter that
|
|
789
1133
|
// stopped substituting would hand `cat` the literal token and read nothing.
|
|
@@ -814,76 +1158,11 @@ var testFailures = {
|
|
|
814
1158
|
run(command.replaceAll(PLACEHOLDER, path));
|
|
815
1159
|
return fromReport(path);
|
|
816
1160
|
} finally {
|
|
817
|
-
if (own) rmSync4(
|
|
1161
|
+
if (own) rmSync4(join7(path, ".."), { force: true, recursive: true });
|
|
818
1162
|
}
|
|
819
1163
|
}
|
|
820
1164
|
};
|
|
821
1165
|
|
|
822
|
-
// src/counters/workspace.ts
|
|
823
|
-
import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync7 } from "fs";
|
|
824
|
-
import { join as join5, resolve as resolve9 } from "path";
|
|
825
|
-
var SKIP = /^(node_modules|\.)/;
|
|
826
|
-
var childDirs = (dir) => {
|
|
827
|
-
try {
|
|
828
|
-
return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !SKIP.test(entry.name)).map((entry) => join5(dir, entry.name));
|
|
829
|
-
} catch {
|
|
830
|
-
return [];
|
|
831
|
-
}
|
|
832
|
-
};
|
|
833
|
-
var descendants = (dir, depth) => depth === 0 ? [dir] : [dir, ...childDirs(dir).flatMap((child) => descendants(child, depth - 1))];
|
|
834
|
-
var expand = (root, pattern) => {
|
|
835
|
-
const segments = pattern.split("/").filter((one) => one !== "" && one !== ".");
|
|
836
|
-
let dirs = [root];
|
|
837
|
-
for (const segment of segments) {
|
|
838
|
-
dirs = segment === "*" ? dirs.flatMap((dir) => childDirs(dir)) : segment === "**" ? dirs.flatMap((dir) => descendants(dir, 3)) : dirs.map((dir) => join5(dir, segment)).filter((dir) => existsSync8(dir));
|
|
839
|
-
}
|
|
840
|
-
return dirs;
|
|
841
|
-
};
|
|
842
|
-
var QUOTED = /^['"]|['"]$/g;
|
|
843
|
-
var cleaned = (value) => value.replace(/#.*$/, "").trim().replaceAll(QUOTED, "");
|
|
844
|
-
var pnpmPatterns = (path) => {
|
|
845
|
-
const lines = readFileSync7(path, "utf8").split("\n");
|
|
846
|
-
const at = lines.findIndex((line) => line.startsWith("packages:"));
|
|
847
|
-
if (at === -1) return [];
|
|
848
|
-
const inline = lines[at]?.slice("packages:".length).trim() ?? "";
|
|
849
|
-
if (inline.startsWith("[")) {
|
|
850
|
-
return inline.replace(/^\[|\]$/g, "").split(",").map(cleaned).filter((one) => one !== "");
|
|
851
|
-
}
|
|
852
|
-
const patterns = [];
|
|
853
|
-
for (const line of lines.slice(at + 1)) {
|
|
854
|
-
const item = /^\s*-\s*(.+)$/.exec(line);
|
|
855
|
-
if (item?.[1] !== void 0) {
|
|
856
|
-
patterns.push(cleaned(item[1]));
|
|
857
|
-
continue;
|
|
858
|
-
}
|
|
859
|
-
if (line.trim() !== "") break;
|
|
860
|
-
}
|
|
861
|
-
return patterns;
|
|
862
|
-
};
|
|
863
|
-
var npmPatterns = (path) => {
|
|
864
|
-
const parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
865
|
-
const declared = Array.isArray(parsed.workspaces) ? parsed.workspaces : parsed.workspaces?.packages ?? [];
|
|
866
|
-
return declared.filter((one) => typeof one === "string");
|
|
867
|
-
};
|
|
868
|
-
var nameOf = (dir) => {
|
|
869
|
-
const manifest = join5(dir, "package.json");
|
|
870
|
-
if (!existsSync8(manifest)) return void 0;
|
|
871
|
-
try {
|
|
872
|
-
const { name } = JSON.parse(readFileSync7(manifest, "utf8"));
|
|
873
|
-
return typeof name === "string" && name !== "" ? name : void 0;
|
|
874
|
-
} catch {
|
|
875
|
-
return void 0;
|
|
876
|
-
}
|
|
877
|
-
};
|
|
878
|
-
var workspacePackageNames = (cwd) => {
|
|
879
|
-
const root = resolve9(cwd);
|
|
880
|
-
const pnpm = join5(root, "pnpm-workspace.yaml");
|
|
881
|
-
const manifest = join5(root, "package.json");
|
|
882
|
-
const patterns = existsSync8(pnpm) ? pnpmPatterns(pnpm) : existsSync8(manifest) ? npmPatterns(manifest) : [];
|
|
883
|
-
const names = patterns.filter((pattern) => !pattern.startsWith("!")).flatMap((pattern) => expand(root, pattern)).map((dir) => nameOf(dir)).filter((name) => name !== void 0);
|
|
884
|
-
return [...new Set(names)];
|
|
885
|
-
};
|
|
886
|
-
|
|
887
1166
|
// src/counters/typecheck.ts
|
|
888
1167
|
var UNRESOLVED = /error TS(?:2305|2307): ([^\n]*)/g;
|
|
889
1168
|
var SPECIFIER = /'([^']+)'/g;
|
|
@@ -918,21 +1197,102 @@ var typecheckErrors = {
|
|
|
918
1197
|
}
|
|
919
1198
|
};
|
|
920
1199
|
|
|
1200
|
+
// src/counters/walk.ts
|
|
1201
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
1202
|
+
import { resolve as resolve12 } from "path";
|
|
1203
|
+
var DEFAULT_REPORT2 = ".geonosis/walk-report.json";
|
|
1204
|
+
var CLASSES = /* @__PURE__ */ new Set([
|
|
1205
|
+
"buy-box-above-fold",
|
|
1206
|
+
"error-page-status",
|
|
1207
|
+
"fabricated-claim",
|
|
1208
|
+
"fake-session",
|
|
1209
|
+
"light-on-light",
|
|
1210
|
+
"link-integrity",
|
|
1211
|
+
"ops-leakage",
|
|
1212
|
+
"placeholder-asset",
|
|
1213
|
+
"stub-only-entity"
|
|
1214
|
+
]);
|
|
1215
|
+
var walkFindings = {
|
|
1216
|
+
id: "walkFindings",
|
|
1217
|
+
probe: {
|
|
1218
|
+
expect: 1,
|
|
1219
|
+
input: (dir) => plant(
|
|
1220
|
+
dir,
|
|
1221
|
+
DEFAULT_REPORT2,
|
|
1222
|
+
JSON.stringify({
|
|
1223
|
+
counts: { "fabricated-claim": 1 },
|
|
1224
|
+
finishedAt: "2026-08-30T10:00:01.000Z",
|
|
1225
|
+
pages: [
|
|
1226
|
+
{
|
|
1227
|
+
findings: [
|
|
1228
|
+
{
|
|
1229
|
+
class: "fabricated-claim",
|
|
1230
|
+
evidence: '"4.9/5" is on the page',
|
|
1231
|
+
severity: "blocking",
|
|
1232
|
+
url: "http://localhost:3000/"
|
|
1233
|
+
}
|
|
1234
|
+
],
|
|
1235
|
+
url: "http://localhost:3000/"
|
|
1236
|
+
}
|
|
1237
|
+
],
|
|
1238
|
+
probes: ["fabricated-claim"],
|
|
1239
|
+
startedAt: "2026-08-30T10:00:00.000Z",
|
|
1240
|
+
viewport: { height: 900, width: 1440 }
|
|
1241
|
+
})
|
|
1242
|
+
)
|
|
1243
|
+
},
|
|
1244
|
+
run: async ({ cwd, params }) => {
|
|
1245
|
+
const relative = stringParam("walkFindings", params, "report", DEFAULT_REPORT2);
|
|
1246
|
+
const path = resolve12(cwd, relative);
|
|
1247
|
+
if (!existsSync10(path)) {
|
|
1248
|
+
throw new CounterError(
|
|
1249
|
+
"walkFindings",
|
|
1250
|
+
`no walk report at ${relative} \u2014 run \`geonosis-walk\` before measuring it`
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
let report;
|
|
1254
|
+
try {
|
|
1255
|
+
report = JSON.parse(readFileSync10(path, "utf8"));
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
throw new CounterError(
|
|
1258
|
+
"walkFindings",
|
|
1259
|
+
`${relative} does not parse: ${error.message}`
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
if (!Array.isArray(report.pages)) {
|
|
1263
|
+
throw new CounterError("walkFindings", `${relative} has no pages \u2014 it is not a walk report`);
|
|
1264
|
+
}
|
|
1265
|
+
const wanted = stringsParam(params, "classes", []);
|
|
1266
|
+
for (const one of wanted) {
|
|
1267
|
+
if (!CLASSES.has(one)) {
|
|
1268
|
+
throw new CounterError("walkFindings", `"${one}" is not one of the walk's defect classes`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
return report.pages.flatMap((page) => page.findings ?? []).filter((finding) => wanted.length === 0 || wanted.includes(String(finding.class))).length;
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
|
|
921
1275
|
// src/counters/index.ts
|
|
922
1276
|
var COUNTERS = [
|
|
923
1277
|
archViolations,
|
|
924
1278
|
boundaryIssues,
|
|
1279
|
+
bundleBytes,
|
|
925
1280
|
cloneCount,
|
|
1281
|
+
disabledCiJobs,
|
|
1282
|
+
fastTierMs,
|
|
926
1283
|
knipIssues,
|
|
927
1284
|
lawLineCount,
|
|
928
1285
|
oxlintErrors,
|
|
929
1286
|
oxlintRule,
|
|
930
1287
|
oxlintWarnings,
|
|
1288
|
+
packagesWithoutTypecheck,
|
|
931
1289
|
runtimeCodeShipped,
|
|
932
1290
|
sumOfCounts,
|
|
933
1291
|
testFailures,
|
|
1292
|
+
testsWithoutRunner,
|
|
934
1293
|
typecheckErrors,
|
|
935
|
-
unformattedFiles
|
|
1294
|
+
unformattedFiles,
|
|
1295
|
+
walkFindings
|
|
936
1296
|
];
|
|
937
1297
|
var counterById = (id) => {
|
|
938
1298
|
const counter = COUNTERS.find((one) => one.id === id);
|
package/dist/cli.js
CHANGED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/** What a command did. `code` is the exit status; `output` is stdout and stderr, merged. */
|
|
2
|
+
type CommandResult = {
|
|
3
|
+
code: number;
|
|
4
|
+
output: string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* A tool that reports findings exits non-zero — that is the tool working, not failing. A tool that
|
|
8
|
+
* could not be spawned at all, or whose output no longer parses, is a gate that cannot measure, and
|
|
9
|
+
* a gate that cannot measure has not passed. Counters throw this rather than returning zero.
|
|
10
|
+
*/
|
|
11
|
+
declare class CounterError extends Error {
|
|
12
|
+
readonly counter: string;
|
|
13
|
+
constructor(counter: string, message: string);
|
|
14
|
+
}
|
|
15
|
+
type CounterContext = {
|
|
16
|
+
/** The repo root every command runs in and every relative path resolves against. */
|
|
17
|
+
cwd: string;
|
|
18
|
+
/** The baseline key this instance writes — several instances of one counter can coexist. */
|
|
19
|
+
key: string;
|
|
20
|
+
/** Whatever the config gave this instance. Each counter validates its own. */
|
|
21
|
+
params: Record<string, unknown>;
|
|
22
|
+
run: (command: string) => CommandResult;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* A counter's falsification probe: the known-bad input it must be able to read.
|
|
26
|
+
*
|
|
27
|
+
* `input` plants it in a scratch directory, `command` is the command shape to run THERE (absent for
|
|
28
|
+
* a counter that reads a file instead), `params` is whatever else that counter needs, and `expect`
|
|
29
|
+
* is the reading the planted input is worth — at least 1, because a probe that plants nothing
|
|
30
|
+
* proves nothing.
|
|
31
|
+
*/
|
|
32
|
+
type CounterProbe = {
|
|
33
|
+
command?: (dir: string) => string;
|
|
34
|
+
expect: number;
|
|
35
|
+
input: (dir: string) => void;
|
|
36
|
+
/**
|
|
37
|
+
* What this probe proves, when the counter has more than one way of reading. It is printed beside
|
|
38
|
+
* the key, so a report says WHICH mode was seen red rather than that the counter was.
|
|
39
|
+
*/
|
|
40
|
+
name?: string;
|
|
41
|
+
params?: Record<string, unknown>;
|
|
42
|
+
};
|
|
43
|
+
type Counter = {
|
|
44
|
+
id: string;
|
|
45
|
+
/**
|
|
46
|
+
* Whether this counter's number is a measured QUANTITY — bytes, milliseconds — rather than a
|
|
47
|
+
* count of findings, and may therefore carry a `tolerance`.
|
|
48
|
+
*
|
|
49
|
+
* Opt-in, and declared by the counter rather than by the config, because the config is the wrong
|
|
50
|
+
* place to decide it: `"tolerance": 100` on a findings counter switches that gate off and the run
|
|
51
|
+
* still prints PASS. One more lint error is always one too many; forty more bytes is a dependency
|
|
52
|
+
* patch nobody chose. Only the counter knows which of those its number is.
|
|
53
|
+
*/
|
|
54
|
+
tolerates?: true;
|
|
55
|
+
/**
|
|
56
|
+
* Absent only for a counter written outside this package. A gate that has never been seen red has
|
|
57
|
+
* not been shown to measure, so `--prove` refuses a configured counter that ships no probe.
|
|
58
|
+
*
|
|
59
|
+
* A counter that reads its tool in more than one way ships one probe per way: proving the mode
|
|
60
|
+
* nobody configured says nothing about the mode they did.
|
|
61
|
+
*/
|
|
62
|
+
probe?: CounterProbe | CounterProbe[];
|
|
63
|
+
run: (context: CounterContext) => Promise<number>;
|
|
64
|
+
};
|
|
65
|
+
/** One line of `geonosis.ratchet.json`'s `counters` array. */
|
|
66
|
+
type CounterConfig = {
|
|
67
|
+
counter: string;
|
|
68
|
+
/** The baseline key. Defaults to `counter`; required when a counter is used more than once. */
|
|
69
|
+
key?: string;
|
|
70
|
+
/**
|
|
71
|
+
* The tiers this counter belongs to — free-form names a repo chooses, `fast` and `full` by
|
|
72
|
+
* convention. Absent means every tier, so a repo that never asks for one is unaffected.
|
|
73
|
+
*/
|
|
74
|
+
tiers?: string[];
|
|
75
|
+
} & Record<string, unknown>;
|
|
76
|
+
type RatchetConfig = {
|
|
77
|
+
baseline: string;
|
|
78
|
+
counters: CounterConfig[];
|
|
79
|
+
};
|
|
80
|
+
type Verdict = 'grew' | 'held' | 'shrank' | 'skipped';
|
|
81
|
+
/**
|
|
82
|
+
* A counter that ran carries its number; a counter the tier left out carries none — deliberately,
|
|
83
|
+
* so nothing downstream can mistake a census nobody took for one that came back equal.
|
|
84
|
+
*/
|
|
85
|
+
type Measurement = {
|
|
86
|
+
baseline: number;
|
|
87
|
+
/**
|
|
88
|
+
* The tail of what this counter's command printed, kept only when the number GREW. A report
|
|
89
|
+
* that says a number went up and nothing else sends the reader back to re-run the tool; the
|
|
90
|
+
* lines that made it go up are the answer they were going to look for.
|
|
91
|
+
*/
|
|
92
|
+
evidence: string[];
|
|
93
|
+
key: string;
|
|
94
|
+
now: number;
|
|
95
|
+
verdict: 'grew' | 'held' | 'shrank';
|
|
96
|
+
} | {
|
|
97
|
+
key: string;
|
|
98
|
+
tier: string;
|
|
99
|
+
verdict: 'skipped';
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* What one counter's probe did. A reading of 0 is the finding this exists to catch: the counter ran,
|
|
103
|
+
* the input was planted, and the number came back the same as it does on a clean tree.
|
|
104
|
+
*/
|
|
105
|
+
type Proof = {
|
|
106
|
+
counter: string;
|
|
107
|
+
expected: number;
|
|
108
|
+
key: string;
|
|
109
|
+
reading: number;
|
|
110
|
+
verdict: 'misread';
|
|
111
|
+
} | {
|
|
112
|
+
counter: string;
|
|
113
|
+
key: string;
|
|
114
|
+
reading: number;
|
|
115
|
+
verdict: 'cannot-fail' | 'proven';
|
|
116
|
+
} | {
|
|
117
|
+
counter: string;
|
|
118
|
+
key: string;
|
|
119
|
+
reason: string;
|
|
120
|
+
verdict: 'cannot-measure';
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Not a counter: `--exclusive` itself, measured by running two of it. A lock is a claim about the
|
|
124
|
+
* machine, and the only place that claim can be checked is the machine.
|
|
125
|
+
*/
|
|
126
|
+
| {
|
|
127
|
+
key: string;
|
|
128
|
+
reason: string;
|
|
129
|
+
verdict: 'interleaved';
|
|
130
|
+
} | {
|
|
131
|
+
key: string;
|
|
132
|
+
reason: string;
|
|
133
|
+
verdict: 'serialised';
|
|
134
|
+
} | {
|
|
135
|
+
key: string;
|
|
136
|
+
tier: string;
|
|
137
|
+
verdict: 'skipped';
|
|
138
|
+
};
|
|
139
|
+
type ProveResult = {
|
|
140
|
+
proofs: Proof[];
|
|
141
|
+
/** False as soon as one counter could not be shown to read its own planted finding. */
|
|
142
|
+
proven: boolean;
|
|
143
|
+
};
|
|
144
|
+
type RatchetResult = {
|
|
145
|
+
measurements: Measurement[];
|
|
146
|
+
/** True when the baseline file was rewritten because something shrank. */
|
|
147
|
+
rewritten: boolean;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
declare const CONFIG_FILE = "geonosis.ratchet.json";
|
|
151
|
+
/** The baseline key an entry writes. Defaults to the counter's own id. */
|
|
152
|
+
declare const keyOf: (entry: CounterConfig) => string;
|
|
153
|
+
declare const loadConfig: (cwd: string) => RatchetConfig;
|
|
154
|
+
|
|
155
|
+
declare const COUNTERS: Counter[];
|
|
156
|
+
declare const counterById: (id: string) => Counter;
|
|
157
|
+
|
|
158
|
+
/** Who is running the heavy thing, since when, and where from. */
|
|
159
|
+
type Holder = {
|
|
160
|
+
cwd: string;
|
|
161
|
+
pid: number;
|
|
162
|
+
startedAt: string;
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* One path for the whole machine, because the contention is for the machine: three sessions on one
|
|
166
|
+
* laptop, each starting a verify, each slowing the other two down until a 25-minute run took two
|
|
167
|
+
* hours and produced failures that were about the load and not the code.
|
|
168
|
+
*/
|
|
169
|
+
declare const heavyLockPath: () => string;
|
|
170
|
+
/**
|
|
171
|
+
* Takes the machine-wide lock, waiting for whoever has it, and gives back the release.
|
|
172
|
+
*
|
|
173
|
+
* A holder whose process is gone — killed, crashed, a laptop closed mid-run — is stale and is taken
|
|
174
|
+
* over with a printed note; so is a lock file nobody can parse, because a lock that can never be
|
|
175
|
+
* cleared is worse than no lock. The wait is bounded and says who it is waiting for: a run that
|
|
176
|
+
* hangs silently for half an hour is indistinguishable from a run that is broken.
|
|
177
|
+
*/
|
|
178
|
+
declare const acquireExclusive: ({ noticeMs, path, pollMs, say, timeoutSeconds, }?: {
|
|
179
|
+
noticeMs?: number;
|
|
180
|
+
path?: string;
|
|
181
|
+
pollMs?: number;
|
|
182
|
+
say?: (line: string) => void;
|
|
183
|
+
timeoutSeconds?: number;
|
|
184
|
+
}) => Promise<() => void>;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* D-029: every gate ships its own falsification probe. The ratchet compares a number against a
|
|
188
|
+
* number and cannot tell a counter that found nothing from a counter that CAN find nothing — the
|
|
189
|
+
* kit's own `oxlintErrors` read 0 under the format its config asked for, from inception, and every
|
|
190
|
+
* baseline it wrote was a census nobody took.
|
|
191
|
+
*
|
|
192
|
+
* So each counter plants a known-bad input and must read it. The run stops at the first counter that
|
|
193
|
+
* reads 0 or throws: the rest of the report would be about a gate whose first gate is not a gate.
|
|
194
|
+
*/
|
|
195
|
+
declare const runProve: ({ counters, cwd, exclusiveVia, tier, }: {
|
|
196
|
+
counters: Counter[];
|
|
197
|
+
cwd: string;
|
|
198
|
+
/**
|
|
199
|
+
* The CLI to run two of, to see whether `--exclusive` serialises on this machine. Absent means
|
|
200
|
+
* the caller is a library and there is no process to run twice, so the lock is not proved.
|
|
201
|
+
*/
|
|
202
|
+
exclusiveVia?: string;
|
|
203
|
+
tier?: string;
|
|
204
|
+
}) => Promise<ProveResult>;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* A binary gate has two bad modes: ignore the debt, or block every commit until it is zero. Neither
|
|
208
|
+
* works mid-migration. This measures the debt, compares it against the baseline, and fails ONLY on
|
|
209
|
+
* regression — so a repo that predates a rule can adopt it the same day, and no NEW debt can land
|
|
210
|
+
* behind it.
|
|
211
|
+
*
|
|
212
|
+
* A `tier` runs only the counters that declare it; the rest are reported as skipped and take no
|
|
213
|
+
* part in the verdict, the exit code or a baseline rewrite. An iteration gate that took eleven
|
|
214
|
+
* seconds gets run less often than one that takes four, and a gate nobody runs enforces nothing —
|
|
215
|
+
* but a skipped counter that printed OK would be worse than not running at all.
|
|
216
|
+
*/
|
|
217
|
+
declare const runRatchet: ({ counters, cwd, tier, }: {
|
|
218
|
+
counters: Counter[];
|
|
219
|
+
cwd: string;
|
|
220
|
+
tier?: string;
|
|
221
|
+
}) => Promise<RatchetResult>;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* One line per counter, then the verdict. Reads the same in a terminal and in a CI log. A counter
|
|
225
|
+
* the tier left out says so by name — printing a number for it would be the stale OK this exists
|
|
226
|
+
* to prevent.
|
|
227
|
+
*/
|
|
228
|
+
declare const formatReport: ({ measurements, rewritten }: RatchetResult) => string;
|
|
229
|
+
declare const formatProve: ({ proofs, proven }: ProveResult) => string;
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Runs a command and merges stderr into stdout — most of these tools write their summary line to
|
|
233
|
+
* stderr, and on a zero exit `execSync` would otherwise hand back only stdout and the parse would
|
|
234
|
+
* fail. A non-zero exit is NOT an error here: a linter that found something exits non-zero, and
|
|
235
|
+
* that is the whole reason we are asking. Counters decide what an unparseable answer means.
|
|
236
|
+
*
|
|
237
|
+
* The command runs inside a SUBSHELL, because `<command> 2>&1` binds the redirect to the last
|
|
238
|
+
* command of the string only. `echo boom >&2; echo fine` merged nothing, and on a zero exit
|
|
239
|
+
* execSync hands back stdout alone — so a tool that prints its findings to stderr and exits 0 read
|
|
240
|
+
* as a tool that found nothing. That is the exact shape of a gate that has quietly stopped gating.
|
|
241
|
+
* On a non-zero exit the two buffers were concatenated instead, which put every stderr line after
|
|
242
|
+
* every stdout line and moved each diagnostic away from the line it was about. The shell keeps
|
|
243
|
+
* them in the order they were written. The newlines let a command end in a comment.
|
|
244
|
+
*/
|
|
245
|
+
declare const runCommand: (cwd: string, counterId: string, env?: NodeJS.ProcessEnv) => (command: string) => CommandResult;
|
|
246
|
+
|
|
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 };
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geonosis/ratchet",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"types": "./dist/index.d.ts",
|
|
4
5
|
"description": "Debt as a number that may only shrink — one ratchet, pluggable counters.",
|
|
5
6
|
"keywords": [
|
|
6
7
|
"ratchet",
|
|
@@ -24,7 +25,10 @@
|
|
|
24
25
|
"geonosis-ratchet": "bin/geonosis-ratchet.mjs"
|
|
25
26
|
},
|
|
26
27
|
"exports": {
|
|
27
|
-
".":
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
}
|
|
28
32
|
},
|
|
29
33
|
"files": [
|
|
30
34
|
"bin",
|