@geonosis/ratchet 0.4.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 — `N failed` — and **refuses when it cannot parse
182
- one**. It never reads the process exit code, because a test runner exiting 0 over a red suite is
183
- commoner than anyone expects: `@cloudflare/vitest-pool-workers` 0.22 on vitest 4.1 exited 0 with
184
- failing tests on every workerd suite of a consumer, and every gate that trusted the exit code
185
- reported green over red for weeks. A refusal is loud and stops the run; a trusted 0 is silent and
186
- banks the red as a win.
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
@@ -196,6 +214,38 @@ failures, they are unmeasured — and unmeasured reads exactly like green.
196
214
  `examples/during-day.ratchet.json` is four test entries for that reason: `apps/web` plus the three
197
215
  workerd packages that were previously outside every gate.
198
216
 
217
+ ### …or the runner's JSON report, which is stronger
218
+
219
+ `report: "vitest-json"` reads the runner's own machine-readable answer instead of its prose:
220
+
221
+ ```jsonc
222
+ {
223
+ "counter": "testFailures",
224
+ "key": "testFailuresApi",
225
+ "command": "cd apps/api && bunx vitest run --reporter=json --outputFile={report}",
226
+ "report": "vitest-json"
227
+ }
228
+ ```
229
+
230
+ `{report}` is replaced with a path in a temp directory the counter makes and removes; give the entry
231
+ a `reportPath` instead when the report belongs somewhere your CI already collects. A command in this
232
+ mode with neither is refused, naming what is missing — a run whose report goes nowhere is a run
233
+ nobody can read.
234
+
235
+ The counter then reads `numFailedTests`, and **refuses** rather than returning a number when:
236
+
237
+ - the file is not there — a crash before the reporter wrote is not a pass, and it prints no summary
238
+ line either, so the summary parser had nothing to refuse on;
239
+ - the file is not JSON, or has no `numFailedTests` in it;
240
+ - the report says `success: false` and names **0** failing tests — the shape a pool that dies
241
+ mid-run writes. The run did not finish, so there is no number to bank.
242
+
243
+ The summary mode stays the default; nothing changes for an entry that does not ask for a report.
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.
248
+
199
249
  ### `oxlintRule` counts a warned rule twice, on purpose
200
250
 
201
251
  A rule parked at `"warn"` as ratcheted debt appears in two numbers: once inside `oxlintWarnings`,
@@ -206,12 +256,36 @@ That is not double-counting the total; the second key exists so the debt is visi
206
256
  of hidden inside a lump sum that a different rule's warning could mask. dielime today: `oxlintWarnings`
207
257
  12 → 135 when `no-raw-html-atoms` was armed, with its own key at 123.
208
258
 
259
+ ### `--prove` also proves the lock
260
+
261
+ `--exclusive` is a claim about the machine, so it is measured on the machine, every `--prove`:
262
+
263
+ ```
264
+ PROVEN exclusive: two runs of 400ms serialised, the second starting 161ms after the first finished
265
+ ```
266
+
267
+ The self-test runs two children of this CLI over `--hold <ms>` — an instrument that takes the lock,
268
+ says when it started, waits, says when it finished, and gives the lock back — and requires the second
269
+ to have started after the first finished. Anything else prints `CANNOT FAIL exclusive: interleaved`
270
+ and exits 2. It uses a lock file of its own in a temp directory, so proving the lock never takes the
271
+ real one out from under the runs it exists to serialise.
272
+
273
+ It is here because the lock did not work for two releases and every gate was green throughout:
274
+ `openSync(path, 'wx')` is atomic about the NAME and not about the holder, and a run polling in that
275
+ window read an unparsable lock, called it stale, and took it. A lock nobody has watched fail has not
276
+ been shown to work — and this one guards the running time of every other gate.
277
+
209
278
  ## Running it where it will actually run
210
279
 
211
280
  - **Counters run in the caller's environment.** They inherit the shell the ratchet was started in,
212
281
  nothing more. If a child command needs `NODE_OPTIONS` — a TypeScript shim, a loader — put it on
213
282
  the script that invokes `geonosis-ratchet`, not on the counter's own line, and not only in your
214
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.
215
289
  - **A `typecheckErrors` counter needs the same precondition CI gives it: build the workspace
216
290
  packages first.** In a fresh worktree the `dist/*.d.ts` files do not exist yet, and `tsc` reports
217
291
  a false +N of missing-module errors that has nothing to do with the change under test. `pnpm build
@@ -266,15 +340,60 @@ Every counter takes its `command` from the config, so the toolchain stays the re
266
340
  | `oxlintErrors` / `oxlintWarnings` | findings under any shape oxlint prints — `--format=unix`, the compact `agent` format, the graphical `default` — cross-checked against the tool's own summary | `command`, `expectFormat` |
267
341
  | `oxlintRule` | one named rule's findings, counted only on lines the run reported as findings and refused when none of them attributes itself readably; with `config`, after forcing the rule to `error` in a temp copy — `"warn"`, `"off"` and the `["off", { … }]` array form alike — so debt cannot grow behind a downgrade | `rule`, `config`, `command`, `expectFormat` |
268
342
  | `typecheckErrors` | `error TS` occurrences, refusing when TS2305/TS2307 name a workspace package of this repo — an unbuilt sibling is a missing build, not debt | `command` |
269
- | `testFailures` | the runner's own failure summary; throws when neither a pass nor a fail count is readable | `command` |
343
+ | `testFailures` | the runner's own failure summary — or, with `report: "vitest-json"`, `numFailedTests` out of the JSON report the command wrote; throws when nothing is readable, when the report is absent, and when the report failed with nothing failing | `command`, `report`, `reportPath` |
270
344
  | `unformattedFiles` | paths `--list-different` names that exist on disk | `command` |
271
345
  | `cloneCount` | jscpd's `Found N clones` | `command` |
272
346
  | `knipIssues` | the totals under knip's unused-* headings | `command`, `headings` |
273
347
  | `boundaryIssues` | `N issues found` from a boundary scan | `command` |
274
- | `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` |
275
349
  | `sumOfCounts` | the total of one capture group across a per-file census (`grep -rc`) | `command`, `match` |
276
350
  | `lawLineCount` | the lines of the law file — a ceiling that can only come down | `path` |
277
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.
278
397
 
279
398
  Two example configs from real repos live in `examples/` in the repository.
280
399
 
@@ -289,4 +408,18 @@ process.stdout.write(formatReport(result))
289
408
 
290
409
  `counters` is a plain array, so a repo can add its own `{ id, run }` beside the built-ins.
291
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
+
292
425
  Apache-2.0.