@geonosis/ratchet 1.3.0 → 2.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
@@ -363,11 +363,12 @@ Every counter takes its `command` from the config, so the toolchain stays the re
363
363
  | `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` |
364
364
  | `unformattedFiles` | paths `--list-different` names that exist on disk | `command` |
365
365
  | `cloneCount` | jscpd's `Found N clones` | `command` |
366
- | `knipIssues` | the totals under knip's unused-* headings | `command`, `headings` |
366
+ | `knipIssues` | the total under EVERY heading knip's compact reporter emits — the shape `<Title> (N)`, not a list of titles, so a heading the kit has not heard of is debt rather than a silent zero. `headings` narrows it, and every printed heading the narrowing leaves out must be named under `excusedHeadings` with a reason or the run refuses | `command`, `headings`, `excusedHeadings` |
367
367
  | `boundaryIssues` | `N issues found` from a boundary scan | `command` |
368
368
  | `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` |
369
369
  | `sumOfCounts` | the total of one capture group across a per-file census (`grep -rc`) | `command`, `match` |
370
370
  | `lawLineCount` | the lines of the law file — a ceiling that can only come down | `path` |
371
+ | `probelessRules` | the rules an oxlint config ENABLES that the plugin it loads declares no `probe()` for. Those are the ones `geonosis-doctor`'s `exercised` can only answer UNJUDGED about, which #137 makes a WARN naming the kit as owner — and a WARN the kit carries across releases is a downgraded rule by another name. The plugin is imported from the tree it is pointed at, and one that cannot be loaded is a refusal, never a zero | `config`, `plugin` |
371
372
  | `runtimeCodeShipped` | 0 when a change shipped runtime code, 1 when it shipped none | `command`, `patterns` |
372
373
  | `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` |
373
374
  | `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` |
@@ -375,6 +376,7 @@ Every counter takes its `command` from the config, so the toolchain stays the re
375
376
  | `testsWithoutRunner` | workspaces holding `*.test.*`, `*.spec.*` or `__tests__/` with no `test` script — the suites nobody runs, which read exactly like suites that pass | `script` |
376
377
  | `packagesWithoutTypecheck` | workspaces with no `typecheck` script | `script` |
377
378
  | `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` |
379
+ | `orphanTodos` | debt markers outside the plan graph: one naming no plan, and one naming a plan that is not in the plans directory. Leading zeroes are a spelling, so `(021)` finds `21-…md`. Fixtures, dependencies and build output are not read | `markers`, `plans`, `roots` |
378
380
 
379
381
  ### `tolerance`, and the two counters that accept one
380
382
 
@@ -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 ?? join(homedir(), ".cache", "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(readFileSync(path, "utf8"));
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
- mkdirSync(dirname(path), { recursive: true });
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
- writeFileSync(staging, JSON.stringify(mine));
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 readFileSync2 } from "fs";
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(readFileSync2(path, "utf8"));
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
  }
@@ -126,7 +189,7 @@ var loadConfig = (cwd) => {
126
189
  };
127
190
  var resolveBaseline = (cwd) => {
128
191
  try {
129
- const parsed = JSON.parse(readFileSync2(resolve(cwd, CONFIG_FILE), "utf8"));
192
+ const parsed = JSON.parse(readFileSync3(resolve(cwd, CONFIG_FILE), "utf8"));
130
193
  return typeof parsed.baseline === "string" ? parsed.baseline : "gate-baseline.json";
131
194
  } catch {
132
195
  return "gate-baseline.json";
@@ -172,13 +235,13 @@ ${output.trim()}`
172
235
  };
173
236
 
174
237
  // src/core/prove.ts
175
- import { existsSync as existsSync2, mkdtempSync, rmSync as rmSync2 } from "fs";
238
+ import { existsSync as existsSync2, mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
176
239
  import { tmpdir } from "os";
177
- import { delimiter, dirname as dirname3, join as join2, resolve as resolve3 } from "path";
240
+ import { delimiter, dirname as dirname4, join as join3, resolve as resolve3 } from "path";
178
241
 
179
242
  // src/core/exclusive.ts
180
243
  import { spawn } from "child_process";
181
- import { dirname as dirname2, resolve as resolve2 } from "path";
244
+ import { dirname as dirname3, resolve as resolve2 } from "path";
182
245
  var MARK = /^exclusive-hold (start|end) (\d+)$/gm;
183
246
  var KEY = "exclusive";
184
247
  var spanOf = (output) => {
@@ -193,7 +256,7 @@ var hold = (cli, holdMs, lockPath) => new Promise((done) => {
193
256
  process.execPath,
194
257
  [cli, "--exclusive", "--exclusive-timeout", "60", "--hold", String(holdMs)],
195
258
  {
196
- cwd: dirname2(resolve2(cli)),
259
+ cwd: dirname3(resolve2(cli)),
197
260
  env: { ...process.env, GEONOSIS_HEAVY_LOCK: lockPath },
198
261
  stdio: ["ignore", "pipe", "pipe"]
199
262
  }
@@ -245,9 +308,9 @@ var toolPath = (cwd) => {
245
308
  const dirs = [];
246
309
  let dir = resolve3(cwd);
247
310
  for (; ; ) {
248
- const bin = join2(dir, "node_modules", ".bin");
311
+ const bin = join3(dir, "node_modules", ".bin");
249
312
  if (existsSync2(bin)) dirs.push(bin);
250
- const parent = dirname3(dir);
313
+ const parent = dirname4(dir);
251
314
  if (parent === dir) break;
252
315
  dir = parent;
253
316
  }
@@ -255,7 +318,7 @@ var toolPath = (cwd) => {
255
318
  };
256
319
  var NO_PROBE = "no probe \u2014 a counter nobody has seen read a planted finding has not been shown to measure";
257
320
  var oneProofOf = async (counter, key, path, probe) => {
258
- const dir = mkdtempSync(join2(tmpdir(), "geonosis-prove-"));
321
+ const dir = mkdtempSync(join3(tmpdir(), "geonosis-prove-"));
259
322
  try {
260
323
  probe.input(dir);
261
324
  const command = probe.command?.(dir);
@@ -322,11 +385,43 @@ var runProve = async ({
322
385
  if (taken.some((one) => one.verdict !== "proven" && one.verdict !== "skipped")) {
323
386
  return { proofs, proven: false };
324
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
+ }
325
420
  }
326
421
  if (exclusiveVia !== void 0) {
327
- const dir = mkdtempSync(join2(tmpdir(), "geonosis-lock-"));
422
+ const dir = mkdtempSync(join3(tmpdir(), "geonosis-lock-"));
328
423
  try {
329
- const proof = await proveExclusive({ cli: exclusiveVia, lockPath: join2(dir, "heavy.lock") });
424
+ const proof = await proveExclusive({ cli: exclusiveVia, lockPath: join3(dir, "heavy.lock") });
330
425
  proofs.push(proof);
331
426
  if (proof.verdict !== "serialised") return { proofs, proven: false };
332
427
  } finally {
@@ -337,20 +432,23 @@ var runProve = async ({
337
432
  };
338
433
 
339
434
  // src/core/ratchet.ts
340
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
435
+ import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
341
436
  import { resolve as resolve4 } from "path";
342
437
  var EVIDENCE_LINES = 10;
343
438
  var recorded = (counter, params, run) => {
344
439
  let output = "";
440
+ let lastCommand;
345
441
  const trimmed = (lines) => lines.map((line) => line.trimEnd()).filter((line) => line !== "").slice(-EVIDENCE_LINES);
346
442
  return {
347
443
  cited: () => {
348
444
  const counted = counter.evidence === void 0 ? [] : counter.evidence({ output, params });
349
445
  return trimmed(counted.length > 0 ? counted : output.split("\n"));
350
446
  },
447
+ lastCommand: () => lastCommand,
351
448
  run: (command) => {
352
449
  const result = run(command);
353
450
  output = result.output;
451
+ lastCommand = command;
354
452
  return result;
355
453
  }
356
454
  };
@@ -386,9 +484,10 @@ var runRatchet = async ({
386
484
  if (!existsSync3(baselinePath)) {
387
485
  throw new Error(`no ${config.baseline} in ${cwd} \u2014 nothing to ratchet against`);
388
486
  }
389
- const baseline = JSON.parse(readFileSync3(baselinePath, "utf8"));
487
+ const baseline = JSON.parse(readFileSync4(baselinePath, "utf8"));
390
488
  const byId = new Map(counters.map((one) => [one.id, one]));
391
489
  const measurements = [];
490
+ const refusals = [];
392
491
  for (const entry of config.counters) {
393
492
  const counter = byId.get(entry.counter);
394
493
  if (counter === void 0) {
@@ -409,7 +508,17 @@ var runRatchet = async ({
409
508
  }
410
509
  const tolerance = toleranceOf(entry, key, counter);
411
510
  const recorder = recorded(counter, entry, runCommand(cwd, entry.counter));
412
- const now = await counter.run({ cwd, key, params: entry, run: recorder.run });
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
+ }
413
522
  const verdict = verdictOf(now, limit, tolerance);
414
523
  measurements.push({
415
524
  baseline: limit,
@@ -421,16 +530,16 @@ var runRatchet = async ({
421
530
  }
422
531
  const grew = measurements.some((one) => one.verdict === "grew");
423
532
  const shrank = measurements.some((one) => one.verdict === "shrank");
424
- if (shrank && !grew) {
533
+ if (shrank && !grew && refusals.length === 0) {
425
534
  const next = { ...baseline };
426
535
  for (const one of measurements) {
427
536
  if (one.verdict === "shrank") next[one.key] = one.now;
428
537
  }
429
- writeFileSync2(baselinePath, `${JSON.stringify(next, null, 2)}
538
+ writeFileSync4(baselinePath, `${JSON.stringify(next, null, 2)}
430
539
  `);
431
- return { measurements, rewritten: true };
540
+ return { considered: config.counters.length, measurements, refusals, rewritten: true };
432
541
  }
433
- return { measurements, rewritten: false };
542
+ return { considered: config.counters.length, measurements, refusals, rewritten: false };
434
543
  };
435
544
 
436
545
  // src/counters/params.ts
@@ -449,12 +558,12 @@ var countMatches = (text, pattern) => text.match(new RegExp(pattern.source, `${p
449
558
  var escapeForRegex = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
450
559
 
451
560
  // src/counters/plant.ts
452
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
453
- import { dirname as dirname4, join as join3 } from "path";
561
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
562
+ import { dirname as dirname5, join as join4 } from "path";
454
563
  var plant = (dir, relative, contents) => {
455
- const path = join3(dir, relative);
456
- mkdirSync2(dirname4(path), { recursive: true });
457
- writeFileSync3(path, contents);
564
+ const path = join4(dir, relative);
565
+ mkdirSync3(dirname5(path), { recursive: true });
566
+ writeFileSync5(path, contents);
458
567
  };
459
568
  var captured = (sample) => ({
460
569
  command: () => "cat sample.txt",
@@ -539,8 +648,8 @@ var bundleBytes = {
539
648
  };
540
649
 
541
650
  // src/counters/ci.ts
542
- import { readdirSync, readFileSync as readFileSync4 } from "fs";
543
- import { join as join4, resolve as resolve5 } from "path";
651
+ import { readdirSync, readFileSync as readFileSync5 } from "fs";
652
+ import { join as join5, resolve as resolve5 } from "path";
544
653
  var DISABLED = /\bif:[ \t]*false\b/;
545
654
  var WORKFLOW = /\.ya?ml$/;
546
655
  var disabledCiJobs = {
@@ -558,7 +667,7 @@ var disabledCiJobs = {
558
667
  const dir = resolve5(cwd, relative);
559
668
  let files;
560
669
  try {
561
- files = readdirSync(dir, { withFileTypes: true }).filter((entry) => WORKFLOW.test(entry.name)).map((entry) => join4(dir, entry.name));
670
+ files = readdirSync(dir, { withFileTypes: true }).filter((entry) => WORKFLOW.test(entry.name)).map((entry) => join5(dir, entry.name));
562
671
  } catch {
563
672
  return 0;
564
673
  }
@@ -566,7 +675,7 @@ var disabledCiJobs = {
566
675
  for (const file of files) {
567
676
  let contents;
568
677
  try {
569
- contents = readFileSync4(file, "utf8");
678
+ contents = readFileSync5(file, "utf8");
570
679
  } catch (error) {
571
680
  throw new CounterError(
572
681
  "disabledCiJobs",
@@ -589,29 +698,49 @@ var cloneCount = {
589
698
  const output = run(command).output;
590
699
  const found = output.match(CLONES)?.[1];
591
700
  if (found === void 0) {
701
+ const said = output.trim();
592
702
  throw new CounterError(
593
703
  "cloneCount",
594
- `no "Found N clones" line:
595
- ${output.trim().slice(-500)}`
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)}`
596
706
  );
597
707
  }
598
708
  return Number(found);
599
709
  }
600
710
  };
601
- var DEFAULT_HEADINGS = [
602
- "Unused files",
603
- "Unused dependencies",
604
- "Unused devDependencies",
605
- "Unused exports",
606
- "Unused exported types"
607
- ];
711
+ var HEADING = /^([A-Z][A-Za-z ]+?) \((\d+)\)\s*$/gm;
712
+ var headingsIn = (output) => {
713
+ const found = /* @__PURE__ */ new Map();
714
+ for (const [, heading = "", count = "0"] of output.matchAll(HEADING)) {
715
+ found.set(heading, Number(count));
716
+ }
717
+ return found;
718
+ };
719
+ var excusesIn = (params) => {
720
+ const declared = params["excusedHeadings"];
721
+ return new Set(
722
+ typeof declared === "object" && declared !== null && !Array.isArray(declared) ? Object.keys(declared) : []
723
+ );
724
+ };
608
725
  var knipIssues = {
609
726
  id: "knipIssues",
610
727
  probe: { ...captured("Unused files (1)\nsrc/gone.ts\n"), expect: 1 },
728
+ readingParams: ["headings"],
611
729
  run: async ({ params, run }) => {
612
730
  const command = stringParam("knipIssues", params, "command", "npx knip --reporter compact");
613
- const output = run(command).output;
614
- return stringsParam(params, "headings", DEFAULT_HEADINGS).map((heading) => Number(output.match(new RegExp(`${heading} \\((\\d+)\\)`))?.[1] ?? 0)).reduce((sum, count) => sum + count, 0);
731
+ const printed = headingsIn(run(command).output);
732
+ const counted = stringsParam(params, "headings", [...printed.keys()]);
733
+ const excused = excusesIn(params);
734
+ const dropped = [...printed.entries()].filter(
735
+ ([heading, count]) => count > 0 && !counted.includes(heading) && !excused.has(heading)
736
+ );
737
+ if (dropped.length > 0) {
738
+ throw new CounterError(
739
+ "knipIssues",
740
+ `knip printed ${dropped.map(([heading, count]) => `${heading} (${count})`).join(", ")} and the configured "headings" counts ${counted.join(", ")} \u2014 three ways out: add each to "headings", name it under "excusedHeadings" with the reason this repo does not hold it, or remove "headings" altogether and count every heading knip prints`
741
+ );
742
+ }
743
+ return counted.reduce((sum, heading) => sum + (printed.get(heading) ?? 0), 0);
615
744
  }
616
745
  };
617
746
  var ISSUES = /(\d+) issues? found/;
@@ -646,7 +775,7 @@ var unformattedFiles = {
646
775
  };
647
776
 
648
777
  // src/counters/gate-report.ts
649
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
778
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
650
779
  import { resolve as resolve7 } from "path";
651
780
  var DEFAULT_REPORT = ".geonosis/gate-report.json";
652
781
  var fastTierMs = {
@@ -680,7 +809,7 @@ var fastTierMs = {
680
809
  }
681
810
  let report;
682
811
  try {
683
- report = JSON.parse(readFileSync5(path, "utf8"));
812
+ report = JSON.parse(readFileSync6(path, "utf8"));
684
813
  } catch (error) {
685
814
  throw new CounterError(
686
815
  "fastTierMs",
@@ -702,7 +831,7 @@ var fastTierMs = {
702
831
  };
703
832
 
704
833
  // src/counters/law.ts
705
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
834
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
706
835
  import { resolve as resolve8 } from "path";
707
836
  var lawLineCount = {
708
837
  id: "lawLineCount",
@@ -715,12 +844,12 @@ var lawLineCount = {
715
844
  const relative = stringParam("lawLineCount", params, "path", "CLAUDE.md");
716
845
  const path = resolve8(cwd, relative);
717
846
  if (!existsSync6(path)) throw new CounterError("lawLineCount", `no law file at ${relative}`);
718
- return readFileSync6(path, "utf8").replace(/\n$/, "").split("\n").length;
847
+ return readFileSync7(path, "utf8").replace(/\n$/, "").split("\n").length;
719
848
  }
720
849
  };
721
850
 
722
851
  // src/counters/oxlint.ts
723
- import { existsSync as existsSync7, readFileSync as readFileSync7, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
852
+ import { existsSync as existsSync7, readFileSync as readFileSync8, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
724
853
  import { resolve as resolve9 } from "path";
725
854
  var DEFAULT_COMMAND = "npx oxlint --format=unix --config .oxlintrc.json .";
726
855
  var PROBE_COMMAND = "oxlint --format=unix --config .oxlintrc.json .";
@@ -887,11 +1016,11 @@ var oxlintRule = {
887
1016
  const source = resolve9(cwd, config);
888
1017
  if (!existsSync7(source)) throw new CounterError("oxlintRule", `no config at ${config}`);
889
1018
  const strictName = `.oxlintrc.ratchet-${key}.json`;
890
- const strict = readFileSync7(source, "utf8").replace(
1019
+ const strict = readFileSync8(source, "utf8").replace(
891
1020
  new RegExp(`("[^"]*${escapeForRegex(rule)}"\\s*:\\s*\\[?\\s*)"(warn|off)"`),
892
1021
  '$1"error"'
893
1022
  );
894
- writeFileSync4(resolve9(cwd, strictName), strict);
1023
+ writeFileSync6(resolve9(cwd, strictName), strict);
895
1024
  try {
896
1025
  return countRule(run(command.replace("{config}", strictName)), rule, expect);
897
1026
  } finally {
@@ -900,6 +1029,100 @@ var oxlintRule = {
900
1029
  }
901
1030
  };
902
1031
 
1032
+ // src/counters/probes.ts
1033
+ import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
1034
+ import { createRequire } from "module";
1035
+ import { join as join6, resolve as resolve10 } from "path";
1036
+ import { pathToFileURL } from "url";
1037
+ var ID = "probelessRules";
1038
+ var OFF = /* @__PURE__ */ new Set([0, "0", "allow", "off", false]);
1039
+ var severityOf2 = (level) => Array.isArray(level) ? level[0] : level;
1040
+ var enabledIn = (config, namespace) => {
1041
+ const on = /* @__PURE__ */ new Set();
1042
+ for (const block of [
1043
+ config.rules ?? {},
1044
+ ...(config.overrides ?? []).map((one) => one.rules ?? {})
1045
+ ]) {
1046
+ for (const [id, level] of Object.entries(block)) {
1047
+ if (id.startsWith(`${namespace}/`) && !OFF.has(severityOf2(level))) {
1048
+ on.add(id.slice(namespace.length + 1));
1049
+ }
1050
+ }
1051
+ }
1052
+ return [...on].toSorted();
1053
+ };
1054
+ var probedBy = async (cwd, plugin) => {
1055
+ let entry;
1056
+ try {
1057
+ entry = createRequire(join6(cwd, "noop.js")).resolve(plugin);
1058
+ } catch (error) {
1059
+ throw new CounterError(
1060
+ ID,
1061
+ `could not resolve ${plugin} from ${cwd} \u2014 ${String(error.message).split("\n")[0]}. A plugin nobody can load says nothing about which rules ship a probe, and it is not zero of them`
1062
+ );
1063
+ }
1064
+ let loaded;
1065
+ try {
1066
+ loaded = await import(pathToFileURL(entry).href);
1067
+ } catch (error) {
1068
+ throw new CounterError(ID, `could not import ${entry} \u2014 ${error.message}`);
1069
+ }
1070
+ const namespace = loaded.default?.meta?.name;
1071
+ if (typeof namespace !== "string" || namespace === "") {
1072
+ throw new CounterError(
1073
+ ID,
1074
+ `${plugin} declares no meta.name, so nothing says which rule ids in the config are its own`
1075
+ );
1076
+ }
1077
+ return {
1078
+ namespace,
1079
+ probed: new Set(
1080
+ Object.entries(loaded.default?.rules ?? {}).filter(([, rule]) => typeof rule?.probe === "function").map(([name]) => name)
1081
+ )
1082
+ };
1083
+ };
1084
+ var PLUGIN = "@geonosis/oxlint-plugin-biological-architecture";
1085
+ var probelessRules = {
1086
+ id: ID,
1087
+ probe: {
1088
+ expect: 1,
1089
+ input: (dir) => {
1090
+ plant(
1091
+ dir,
1092
+ ".oxlintrc.json",
1093
+ JSON.stringify({
1094
+ jsPlugins: ["probe-plugin"],
1095
+ rules: { "x/probed": "error", "x/bare": "error" }
1096
+ })
1097
+ );
1098
+ plant(
1099
+ dir,
1100
+ "node_modules/probe-plugin/package.json",
1101
+ '{"name":"probe-plugin","main":"index.js"}'
1102
+ );
1103
+ plant(
1104
+ dir,
1105
+ "node_modules/probe-plugin/index.js",
1106
+ "export default { meta: { name: 'x' }, rules: { probed: { probe: () => [], create: () => ({}) }, bare: { create: () => ({}) } } }\n"
1107
+ );
1108
+ },
1109
+ params: { plugin: "probe-plugin" }
1110
+ },
1111
+ run: async ({ cwd, params }) => {
1112
+ const relative = stringParam(ID, params, "config", ".oxlintrc.json");
1113
+ const path = resolve10(cwd, relative);
1114
+ if (!existsSync8(path)) throw new CounterError(ID, `no oxlint config at ${relative}`);
1115
+ let config;
1116
+ try {
1117
+ config = JSON.parse(readFileSync9(path, "utf8"));
1118
+ } catch (error) {
1119
+ throw new CounterError(ID, `${relative} does not parse: ${error.message}`);
1120
+ }
1121
+ const { namespace, probed } = await probedBy(cwd, stringParam(ID, params, "plugin", PLUGIN));
1122
+ return enabledIn(config, namespace).filter((rule) => !probed.has(rule)).length;
1123
+ }
1124
+ };
1125
+
903
1126
  // src/counters/runtime-code.ts
904
1127
  var DEFAULT_PATTERNS = ["^(apps|packages)/[^/]+/src/"];
905
1128
  var NOT_RUNTIME = /(\.test\.|\.spec\.|__tests__\/|__fixtures__\/|\.d\.ts$)/;
@@ -925,15 +1148,15 @@ var runtimeCodeShipped = {
925
1148
 
926
1149
  // src/counters/scripts.ts
927
1150
  import { readdirSync as readdirSync3 } from "fs";
928
- import { join as join6 } from "path";
1151
+ import { join as join8 } from "path";
929
1152
 
930
1153
  // src/counters/workspace.ts
931
- import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync8 } from "fs";
932
- import { join as join5, resolve as resolve10 } from "path";
1154
+ import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync10 } from "fs";
1155
+ import { join as join7, resolve as resolve11 } from "path";
933
1156
  var SKIP = /^(node_modules|\.)/;
934
1157
  var childDirs = (dir) => {
935
1158
  try {
936
- return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !SKIP.test(entry.name)).map((entry) => join5(dir, entry.name));
1159
+ return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !SKIP.test(entry.name)).map((entry) => join7(dir, entry.name));
937
1160
  } catch {
938
1161
  return [];
939
1162
  }
@@ -943,14 +1166,14 @@ var expand = (root, pattern) => {
943
1166
  const segments = pattern.split("/").filter((one) => one !== "" && one !== ".");
944
1167
  let dirs = [root];
945
1168
  for (const segment of segments) {
946
- dirs = segment === "*" ? dirs.flatMap((dir) => childDirs(dir)) : segment === "**" ? dirs.flatMap((dir) => descendants(dir, 3)) : dirs.map((dir) => join5(dir, segment)).filter((dir) => existsSync8(dir));
1169
+ dirs = segment === "*" ? dirs.flatMap((dir) => childDirs(dir)) : segment === "**" ? dirs.flatMap((dir) => descendants(dir, 3)) : dirs.map((dir) => join7(dir, segment)).filter((dir) => existsSync9(dir));
947
1170
  }
948
1171
  return dirs;
949
1172
  };
950
1173
  var QUOTED = /^['"]|['"]$/g;
951
1174
  var cleaned = (value) => value.replace(/#.*$/, "").trim().replaceAll(QUOTED, "");
952
1175
  var pnpmPatterns = (path) => {
953
- const lines = readFileSync8(path, "utf8").split("\n");
1176
+ const lines = readFileSync10(path, "utf8").split("\n");
954
1177
  const at = lines.findIndex((line) => line.startsWith("packages:"));
955
1178
  if (at === -1) return [];
956
1179
  const inline = lines[at]?.slice("packages:".length).trim() ?? "";
@@ -969,31 +1192,31 @@ var pnpmPatterns = (path) => {
969
1192
  return patterns;
970
1193
  };
971
1194
  var npmPatterns = (path) => {
972
- const parsed = JSON.parse(readFileSync8(path, "utf8"));
1195
+ const parsed = JSON.parse(readFileSync10(path, "utf8"));
973
1196
  const declared = Array.isArray(parsed.workspaces) ? parsed.workspaces : parsed.workspaces?.packages ?? [];
974
1197
  return declared.filter((one) => typeof one === "string");
975
1198
  };
976
1199
  var nameOf = (dir) => {
977
- const manifest = join5(dir, "package.json");
978
- if (!existsSync8(manifest)) return void 0;
1200
+ const manifest = join7(dir, "package.json");
1201
+ if (!existsSync9(manifest)) return void 0;
979
1202
  try {
980
- const { name } = JSON.parse(readFileSync8(manifest, "utf8"));
1203
+ const { name } = JSON.parse(readFileSync10(manifest, "utf8"));
981
1204
  return typeof name === "string" && name !== "" ? name : void 0;
982
1205
  } catch {
983
1206
  return void 0;
984
1207
  }
985
1208
  };
986
1209
  var workspaceDirs = (cwd) => {
987
- const root = resolve10(cwd);
988
- const pnpm = join5(root, "pnpm-workspace.yaml");
989
- const manifest = join5(root, "package.json");
990
- const patterns = existsSync8(pnpm) ? pnpmPatterns(pnpm) : existsSync8(manifest) ? npmPatterns(manifest) : [];
991
- const dirs = patterns.filter((pattern) => !pattern.startsWith("!")).flatMap((pattern) => expand(root, pattern)).filter((dir) => existsSync8(join5(dir, "package.json")));
1210
+ const root = resolve11(cwd);
1211
+ const pnpm = join7(root, "pnpm-workspace.yaml");
1212
+ const manifest = join7(root, "package.json");
1213
+ const patterns = existsSync9(pnpm) ? pnpmPatterns(pnpm) : existsSync9(manifest) ? npmPatterns(manifest) : [];
1214
+ const dirs = patterns.filter((pattern) => !pattern.startsWith("!")).flatMap((pattern) => expand(root, pattern)).filter((dir) => existsSync9(join7(dir, "package.json")));
992
1215
  return [...new Set(dirs)];
993
1216
  };
994
1217
  var manifestOf = (dir) => {
995
1218
  try {
996
- const parsed = JSON.parse(readFileSync8(join5(dir, "package.json"), "utf8"));
1219
+ const parsed = JSON.parse(readFileSync10(join7(dir, "package.json"), "utf8"));
997
1220
  return typeof parsed === "object" && parsed !== null ? parsed : void 0;
998
1221
  } catch {
999
1222
  return void 0;
@@ -1023,7 +1246,7 @@ var holdsTests = (dir, depth = 6) => {
1023
1246
  if (entry.isDirectory()) {
1024
1247
  if (entry.name === TEST_DIR) return true;
1025
1248
  if (SKIP2.test(entry.name) || depth === 0) continue;
1026
- if (holdsTests(join6(dir, entry.name), depth - 1)) return true;
1249
+ if (holdsTests(join8(dir, entry.name), depth - 1)) return true;
1027
1250
  continue;
1028
1251
  }
1029
1252
  if (TEST_FILE.test(entry.name)) return true;
@@ -1064,16 +1287,26 @@ var packagesWithoutTypecheck = {
1064
1287
  var sumOfCounts = {
1065
1288
  id: "sumOfCounts",
1066
1289
  probe: { ...captured("src/a.ts:1\nsrc/b.ts:0\n"), expect: 1 },
1290
+ readingParams: ["match"],
1067
1291
  run: async ({ params, run }) => {
1068
1292
  const command = stringParam("sumOfCounts", params, "command");
1069
- const match = new RegExp(stringParam("sumOfCounts", params, "match", ":(\\d+)$"), "gm");
1070
- return [...run(command).output.matchAll(match)].map(([, digits]) => Number(digits ?? 0)).filter((count) => Number.isFinite(count)).reduce((sum, count) => sum + count, 0);
1293
+ const pattern = stringParam("sumOfCounts", params, "match", ":(\\d+)$");
1294
+ const match = new RegExp(pattern, "gm");
1295
+ const output = run(command).output;
1296
+ const found = [...output.matchAll(match)];
1297
+ if (found.length === 0 && output.trim() !== "") {
1298
+ throw new CounterError(
1299
+ "sumOfCounts",
1300
+ `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`
1301
+ );
1302
+ }
1303
+ return found.map(([, digits]) => Number(digits ?? 0)).filter((count) => Number.isFinite(count)).reduce((sum, count) => sum + count, 0);
1071
1304
  }
1072
1305
  };
1073
1306
 
1074
1307
  // src/counters/suppressions.ts
1075
- import { readdirSync as readdirSync4, readFileSync as readFileSync9 } from "fs";
1076
- import { join as join7, resolve as resolve11 } from "path";
1308
+ import { readdirSync as readdirSync4, readFileSync as readFileSync11 } from "fs";
1309
+ import { join as join9, resolve as resolve12 } from "path";
1077
1310
  var CODE_FILE = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts|vue|svelte|astro)$/;
1078
1311
  var SKIP3 = /^(?:node_modules|dist|coverage|\.[^.]|__fixtures__)/;
1079
1312
  var SUPPRESSION = /(?:eslint|oxlint|biome)-(?:disable|ignore)(?:-next-line|-line)?|@ts-expect-error|@ts-ignore/g;
@@ -1088,12 +1321,12 @@ var countIn = (dir, depth = 12) => {
1088
1321
  for (const entry of entries) {
1089
1322
  if (SKIP3.test(entry.name)) continue;
1090
1323
  if (entry.isDirectory()) {
1091
- if (depth > 0) found += countIn(join7(dir, entry.name), depth - 1);
1324
+ if (depth > 0) found += countIn(join9(dir, entry.name), depth - 1);
1092
1325
  continue;
1093
1326
  }
1094
1327
  if (!CODE_FILE.test(entry.name)) continue;
1095
1328
  try {
1096
- found += readFileSync9(join7(dir, entry.name), "utf8").match(SUPPRESSION)?.length ?? 0;
1329
+ found += readFileSync11(join9(dir, entry.name), "utf8").match(SUPPRESSION)?.length ?? 0;
1097
1330
  } catch {
1098
1331
  }
1099
1332
  }
@@ -1107,14 +1340,14 @@ var suppressionCount = {
1107
1340
  },
1108
1341
  run: async ({ cwd, params }) => {
1109
1342
  const roots = stringsParam(params, "roots", ["."]);
1110
- return roots.reduce((sum, root) => sum + countIn(resolve11(cwd, root)), 0);
1343
+ return roots.reduce((sum, root) => sum + countIn(resolve12(cwd, root)), 0);
1111
1344
  }
1112
1345
  };
1113
1346
 
1114
1347
  // src/counters/tests.ts
1115
- import { existsSync as existsSync9, mkdtempSync as mkdtempSync2, readFileSync as readFileSync10, rmSync as rmSync4 } from "fs";
1348
+ import { existsSync as existsSync10, mkdtempSync as mkdtempSync2, readFileSync as readFileSync12, rmSync as rmSync4 } from "fs";
1116
1349
  import { tmpdir as tmpdir2 } from "os";
1117
- import { join as join8, resolve as resolve12 } from "path";
1350
+ import { join as join10, resolve as resolve13 } from "path";
1118
1351
  var COUNTER = "testFailures";
1119
1352
  var VITEST_LINE = /^\s*Tests {2,}(.+?)\s*$/;
1120
1353
  var VITEST_TOTAL = /\(\d+\)$/;
@@ -1146,7 +1379,7 @@ ${output.trim().slice(-500)}`
1146
1379
  );
1147
1380
  };
1148
1381
  var fromReport = (path) => {
1149
- if (!existsSync9(path)) {
1382
+ if (!existsSync10(path)) {
1150
1383
  throw new CounterError(
1151
1384
  COUNTER,
1152
1385
  `the runner wrote no report at ${path} \u2014 a crash before the reporter is not a pass`
@@ -1154,7 +1387,7 @@ var fromReport = (path) => {
1154
1387
  }
1155
1388
  let report;
1156
1389
  try {
1157
- report = JSON.parse(readFileSync10(path, "utf8"));
1390
+ report = JSON.parse(readFileSync12(path, "utf8"));
1158
1391
  } catch (error) {
1159
1392
  throw new CounterError(
1160
1393
  COUNTER,
@@ -1178,14 +1411,14 @@ var fromReport = (path) => {
1178
1411
  };
1179
1412
  var reportPathFor = (cwd, params, command) => {
1180
1413
  const named = params.reportPath;
1181
- if (typeof named === "string" && named !== "") return { own: false, path: resolve12(cwd, named) };
1414
+ if (typeof named === "string" && named !== "") return { own: false, path: resolve13(cwd, named) };
1182
1415
  if (!command.includes(PLACEHOLDER)) {
1183
1416
  throw new CounterError(
1184
1417
  COUNTER,
1185
1418
  `report: "${VITEST_JSON}" needs somewhere to put the report \u2014 write ${PLACEHOLDER} into the command (--outputFile=${PLACEHOLDER}) or give the entry a "reportPath"`
1186
1419
  );
1187
1420
  }
1188
- return { own: true, path: join8(mkdtempSync2(join8(tmpdir2(), "geonosis-report-")), "report.json") };
1421
+ return { own: true, path: join10(mkdtempSync2(join10(tmpdir2(), "geonosis-report-")), "report.json") };
1189
1422
  };
1190
1423
  var testFailures = {
1191
1424
  id: COUNTER,
@@ -1234,11 +1467,77 @@ var testFailures = {
1234
1467
  run(command.replaceAll(PLACEHOLDER, path));
1235
1468
  return fromReport(path);
1236
1469
  } finally {
1237
- if (own) rmSync4(join8(path, ".."), { force: true, recursive: true });
1470
+ if (own) rmSync4(join10(path, ".."), { force: true, recursive: true });
1238
1471
  }
1239
1472
  }
1240
1473
  };
1241
1474
 
1475
+ // src/counters/todos.ts
1476
+ import { existsSync as existsSync11, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "fs";
1477
+ import { join as join11, resolve as resolve14 } from "path";
1478
+ var CODE_FILE2 = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts|vue|svelte|astro)$/;
1479
+ var SKIP4 = /^(?:node_modules|dist|coverage|\.[^.]|__fixtures__)/;
1480
+ var PLAN_FILE = /^(\d{3,})-[a-z\d][a-z\d.-]*\.md$/;
1481
+ var plansIn = (dir) => {
1482
+ if (!existsSync11(dir)) return /* @__PURE__ */ new Set();
1483
+ const found = /* @__PURE__ */ new Set();
1484
+ for (const name of readdirSync5(dir)) {
1485
+ const number = PLAN_FILE.exec(name)?.[1];
1486
+ if (number !== void 0) found.add(String(Number(number)));
1487
+ }
1488
+ return found;
1489
+ };
1490
+ var countIn2 = (dir, marker, plans, depth = 12) => {
1491
+ let entries;
1492
+ try {
1493
+ entries = readdirSync5(dir, { withFileTypes: true });
1494
+ } catch {
1495
+ return 0;
1496
+ }
1497
+ let found = 0;
1498
+ for (const entry of entries) {
1499
+ if (SKIP4.test(entry.name)) continue;
1500
+ if (entry.isDirectory()) {
1501
+ if (depth > 0) found += countIn2(join11(dir, entry.name), marker, plans, depth - 1);
1502
+ continue;
1503
+ }
1504
+ if (!CODE_FILE2.test(entry.name)) continue;
1505
+ let text = "";
1506
+ try {
1507
+ text = readFileSync13(join11(dir, entry.name), "utf8");
1508
+ } catch {
1509
+ continue;
1510
+ }
1511
+ for (const match of text.matchAll(marker)) {
1512
+ const cited = match[1];
1513
+ if (cited === void 0 || !plans.has(String(Number(cited)))) found += 1;
1514
+ }
1515
+ }
1516
+ return found;
1517
+ };
1518
+ var orphanTodos = {
1519
+ id: "orphanTodos",
1520
+ probe: {
1521
+ expect: 2,
1522
+ input: (dir) => plant(
1523
+ dir,
1524
+ "src/planted.ts",
1525
+ "// TODO: no plan named at all\n// TODO(099): a plan nobody wrote\nexport const a = 1\n"
1526
+ )
1527
+ },
1528
+ readingParams: ["markers"],
1529
+ run: async ({ cwd, params }) => {
1530
+ const markers = stringsParam(params, "markers", ["TODO", "FIXME"]);
1531
+ const plans = plansIn(resolve14(cwd, stringParam("orphanTodos", params, "plans", "plans")));
1532
+ const marker = new RegExp(
1533
+ `\\b(?:${markers.map(escapeForRegex).join("|")})\\b(?:\\((\\d+)\\))?`,
1534
+ "g"
1535
+ );
1536
+ const roots = stringsParam(params, "roots", ["."]);
1537
+ return roots.reduce((sum, root) => sum + countIn2(resolve14(cwd, root), marker, plans), 0);
1538
+ }
1539
+ };
1540
+
1242
1541
  // src/counters/typecheck.ts
1243
1542
  var UNRESOLVED = /error TS(?:2305|2307): ([^\n]*)/g;
1244
1543
  var SPECIFIER = /'([^']+)'/g;
@@ -1274,8 +1573,8 @@ var typecheckErrors = {
1274
1573
  };
1275
1574
 
1276
1575
  // src/counters/walk.ts
1277
- import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
1278
- import { resolve as resolve13 } from "path";
1576
+ import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
1577
+ import { resolve as resolve15 } from "path";
1279
1578
  var DEFAULT_REPORT2 = ".geonosis/walk-report.json";
1280
1579
  var CLASSES = /* @__PURE__ */ new Set([
1281
1580
  "buy-box-above-fold",
@@ -1319,8 +1618,8 @@ var walkFindings = {
1319
1618
  },
1320
1619
  run: async ({ cwd, params }) => {
1321
1620
  const relative = stringParam("walkFindings", params, "report", DEFAULT_REPORT2);
1322
- const path = resolve13(cwd, relative);
1323
- if (!existsSync10(path)) {
1621
+ const path = resolve15(cwd, relative);
1622
+ if (!existsSync12(path)) {
1324
1623
  throw new CounterError(
1325
1624
  "walkFindings",
1326
1625
  `no walk report at ${relative} \u2014 run \`geonosis-walk\` before measuring it`
@@ -1328,7 +1627,7 @@ var walkFindings = {
1328
1627
  }
1329
1628
  let report;
1330
1629
  try {
1331
- report = JSON.parse(readFileSync11(path, "utf8"));
1630
+ report = JSON.parse(readFileSync14(path, "utf8"));
1332
1631
  } catch (error) {
1333
1632
  throw new CounterError(
1334
1633
  "walkFindings",
@@ -1362,7 +1661,9 @@ var COUNTERS = [
1362
1661
  oxlintErrors,
1363
1662
  oxlintRule,
1364
1663
  oxlintWarnings,
1664
+ orphanTodos,
1365
1665
  packagesWithoutTypecheck,
1666
+ probelessRules,
1366
1667
  runtimeCodeShipped,
1367
1668
  sumOfCounts,
1368
1669
  testFailures,
@@ -1383,7 +1684,7 @@ var counterById = (id) => {
1383
1684
 
1384
1685
  // src/report.ts
1385
1686
  var WIDTH = 28;
1386
- var formatReport = ({ measurements, rewritten }) => {
1687
+ var formatReport = ({ measurements, refusals, rewritten }) => {
1387
1688
  const lines = measurements.map((one) => {
1388
1689
  if (one.verdict === "skipped") {
1389
1690
  return ` SKIP ${one.key}: not measured by --tier ${one.tier}`;
@@ -1396,8 +1697,14 @@ var formatReport = ({ measurements, rewritten }) => {
1396
1697
  if (one.verdict === "shrank") return `${head} <-- improved -${one.baseline - one.now}`;
1397
1698
  return head;
1398
1699
  });
1700
+ lines.push(...refusals.map((one) => ` CANNOT MEASURE ${one.key}: ${one.reason}`));
1399
1701
  const grew = measurements.filter((one) => one.verdict === "grew");
1400
- if (grew.length > 0) {
1702
+ if (refusals.length > 0) {
1703
+ lines.push(
1704
+ "",
1705
+ `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.`
1706
+ );
1707
+ } else if (grew.length > 0) {
1401
1708
  lines.push(
1402
1709
  "",
1403
1710
  "ratchet FAIL \u2014 debt grew. Fix it, or say in the commit message why the baseline goes up."
@@ -1419,6 +1726,7 @@ var formatProve = ({ proofs, proven }) => {
1419
1726
  }
1420
1727
  if (one.verdict === "cannot-measure") return ` CANNOT MEASURE ${one.key}: ${one.reason}`;
1421
1728
  if (one.verdict === "cannot-fail") return ` CANNOT FAIL ${one.key}: read 0`;
1729
+ if (one.verdict === "unproven") return ` UNPROVEN ${one.key}: ${one.reason}`;
1422
1730
  if (one.verdict === "misread") {
1423
1731
  return ` MISREAD ${one.key}: read ${one.reading} where its probe planted ${one.expected}`;
1424
1732
  }
@@ -1433,6 +1741,11 @@ var formatProve = ({ proofs, proven }) => {
1433
1741
  };
1434
1742
 
1435
1743
  export {
1744
+ ENVELOPES_DIR,
1745
+ envelopePath,
1746
+ UnbalancedEnvelope,
1747
+ writeEnvelope,
1748
+ versionOf,
1436
1749
  heavyLockPath,
1437
1750
  acquireExclusive,
1438
1751
  CONFIG_FILE,
package/dist/cli.js CHANGED
@@ -4,8 +4,10 @@ import {
4
4
  formatProve,
5
5
  formatReport,
6
6
  runProve,
7
- runRatchet
8
- } from "./chunk-HBM6F3GO.js";
7
+ runRatchet,
8
+ versionOf,
9
+ writeEnvelope
10
+ } from "./chunk-H4PLCM5J.js";
9
11
 
10
12
  // src/cli.ts
11
13
  import process from "process";
@@ -100,6 +102,19 @@ var holdTheLock = async (ms) => {
100
102
  `);
101
103
  return 0;
102
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
+ });
103
118
  var measure = async () => {
104
119
  if (holdMs !== void 0) return holdTheLock(holdMs);
105
120
  if (proving) {
@@ -112,8 +127,17 @@ var measure = async () => {
112
127
  process.stdout.write(formatProve(proof));
113
128
  return proof.proven ? 0 : 2;
114
129
  }
130
+ const startedAt = Date.now();
115
131
  const result = await runRatchet({ counters: COUNTERS, cwd, tier });
116
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;
117
141
  return result.measurements.some((one) => one.verdict === "grew") ? 1 : 0;
118
142
  };
119
143
  try {
package/dist/index.d.ts CHANGED
@@ -76,6 +76,14 @@ type Counter = {
76
76
  * nobody configured says nothing about the mode they did.
77
77
  */
78
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[];
79
87
  run: (context: CounterContext) => Promise<number>;
80
88
  };
81
89
  /** One line of `geonosis.ratchet.json`'s `counters` array. */
@@ -134,6 +142,11 @@ type Proof = {
134
142
  key: string;
135
143
  reason: string;
136
144
  verdict: 'cannot-measure';
145
+ } | {
146
+ counter: string;
147
+ key: string;
148
+ reason: string;
149
+ verdict: 'unproven';
137
150
  }
138
151
  /**
139
152
  * Not a counter: `--exclusive` itself, measured by running two of it. A lock is a claim about the
@@ -157,8 +170,24 @@ type ProveResult = {
157
170
  /** False as soon as one counter could not be shown to read its own planted finding. */
158
171
  proven: boolean;
159
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
+ };
160
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;
161
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[];
162
191
  /** True when the baseline file was rewritten because something shrank. */
163
192
  rewritten: boolean;
164
193
  };
@@ -178,6 +207,80 @@ declare const loadConfig: (cwd: string) => RatchetConfig;
178
207
  */
179
208
  declare const resolveBaseline: (cwd: string) => string;
180
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;
283
+
181
284
  declare const COUNTERS: Counter[];
182
285
  declare const counterById: (id: string) => Counter;
183
286
 
@@ -251,7 +354,7 @@ declare const runRatchet: ({ counters, cwd, tier, }: {
251
354
  * the tier left out says so by name — printing a number for it would be the stale OK this exists
252
355
  * to prevent.
253
356
  */
254
- declare const formatReport: ({ measurements, rewritten }: RatchetResult) => string;
357
+ declare const formatReport: ({ measurements, refusals, rewritten }: RatchetResult) => string;
255
358
  declare const formatProve: ({ proofs, proven }: ProveResult) => string;
256
359
 
257
360
  /**
@@ -270,4 +373,4 @@ declare const formatProve: ({ proofs, proven }: ProveResult) => string;
270
373
  */
271
374
  declare const runCommand: (cwd: string, counterId: string, env?: NodeJS.ProcessEnv) => (command: string) => CommandResult;
272
375
 
273
- 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, resolveBaseline, 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,8 +2,11 @@ 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,
@@ -12,14 +15,19 @@ import {
12
15
  resolveBaseline,
13
16
  runCommand,
14
17
  runProve,
15
- runRatchet
16
- } from "./chunk-HBM6F3GO.js";
18
+ runRatchet,
19
+ versionOf,
20
+ writeEnvelope
21
+ } from "./chunk-H4PLCM5J.js";
17
22
  export {
18
23
  CONFIG_FILE,
19
24
  COUNTERS,
20
25
  CounterError,
26
+ ENVELOPES_DIR,
27
+ UnbalancedEnvelope,
21
28
  acquireExclusive,
22
29
  counterById,
30
+ envelopePath,
23
31
  formatProve,
24
32
  formatReport,
25
33
  heavyLockPath,
@@ -28,5 +36,7 @@ export {
28
36
  resolveBaseline,
29
37
  runCommand,
30
38
  runProve,
31
- runRatchet
39
+ runRatchet,
40
+ versionOf,
41
+ writeEnvelope
32
42
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/ratchet",
3
- "version": "1.3.0",
3
+ "version": "2.0.0",
4
4
  "types": "./dist/index.d.ts",
5
5
  "description": "Debt as a number that may only shrink — one ratchet, pluggable counters.",
6
6
  "keywords": [