@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.
@@ -1,4 +1,131 @@
1
- // src/types.ts
1
+ // src/core/lock.ts
2
+ import { randomUUID } from "crypto";
3
+ import { linkSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
4
+ import { homedir } from "os";
5
+ import { dirname, join } from "path";
6
+ var heavyLockPath = () => process.env.GEONOSIS_HEAVY_LOCK ?? join(homedir(), ".cache", "geonosis", "heavy.lock");
7
+ var sleep = (ms) => new Promise((done) => setTimeout(done, ms));
8
+ var holderOf = (path) => {
9
+ try {
10
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
11
+ return typeof parsed.pid === "number" ? {
12
+ cwd: parsed.cwd ?? "somewhere",
13
+ pid: parsed.pid,
14
+ startedAt: parsed.startedAt ?? "unknown"
15
+ } : void 0;
16
+ } catch {
17
+ return void 0;
18
+ }
19
+ };
20
+ var alive = (pid) => {
21
+ try {
22
+ process.kill(pid, 0);
23
+ return true;
24
+ } catch (error) {
25
+ return error.code === "EPERM";
26
+ }
27
+ };
28
+ var heldFor = (holder) => {
29
+ const since = Date.parse(holder.startedAt);
30
+ if (Number.isNaN(since)) return "an unknown time";
31
+ return `${Math.round((Date.now() - since) / 1e3)}s`;
32
+ };
33
+ var write = (path) => {
34
+ mkdirSync(dirname(path), { recursive: true });
35
+ const mine = {
36
+ cwd: process.cwd(),
37
+ pid: process.pid,
38
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
39
+ };
40
+ const staging = `${path}.${process.pid}.${randomUUID()}`;
41
+ try {
42
+ writeFileSync(staging, JSON.stringify(mine));
43
+ linkSync(staging, path);
44
+ return true;
45
+ } catch (error) {
46
+ if (error.code === "EEXIST") return false;
47
+ throw error;
48
+ } finally {
49
+ rmSync(staging, { force: true });
50
+ }
51
+ };
52
+ var acquireExclusive = async ({
53
+ noticeMs = 15e3,
54
+ path = heavyLockPath(),
55
+ pollMs = 250,
56
+ say = (line) => process.stderr.write(`${line}
57
+ `),
58
+ timeoutSeconds = 1800
59
+ } = {}) => {
60
+ const until = Date.now() + timeoutSeconds * 1e3;
61
+ let told = 0;
62
+ const release = () => {
63
+ if (holderOf(path)?.pid === process.pid) rmSync(path, { force: true });
64
+ };
65
+ for (; ; ) {
66
+ if (write(path)) return release;
67
+ const holder = holderOf(path);
68
+ if (holder === void 0 || !alive(holder.pid)) {
69
+ say(
70
+ `geonosis-ratchet: taking over a stale heavy lock (pid ${holder?.pid ?? "unreadable"} is gone)`
71
+ );
72
+ rmSync(path, { force: true });
73
+ continue;
74
+ }
75
+ if (Date.now() >= until) {
76
+ throw new Error(
77
+ `waited ${timeoutSeconds}s for the heavy lock held by pid ${holder.pid} in ${holder.cwd} (${heldFor(holder)}) \u2014 give it longer with --exclusive-timeout, or stop that run`
78
+ );
79
+ }
80
+ const now = Date.now();
81
+ if (now - told >= noticeMs) {
82
+ told = now;
83
+ say(
84
+ `geonosis-ratchet: waiting for the heavy lock \u2014 pid ${holder.pid} in ${holder.cwd}, held for ${heldFor(holder)}`
85
+ );
86
+ }
87
+ await sleep(pollMs);
88
+ }
89
+ };
90
+
91
+ // src/core/config.ts
92
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
93
+ import { resolve } from "path";
94
+ var CONFIG_FILE = "geonosis.ratchet.json";
95
+ var keyOf = (entry) => entry.key ?? entry.counter;
96
+ var loadConfig = (cwd) => {
97
+ const path = resolve(cwd, CONFIG_FILE);
98
+ if (!existsSync(path)) {
99
+ throw new Error(`no ${CONFIG_FILE} in ${cwd} \u2014 the ratchet has nothing to count`);
100
+ }
101
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
102
+ if (!Array.isArray(parsed.counters)) {
103
+ throw new Error(`${CONFIG_FILE} has no "counters" array`);
104
+ }
105
+ for (const entry of parsed.counters) {
106
+ if (typeof entry.counter !== "string") {
107
+ throw new Error(`${CONFIG_FILE}: every counter entry needs a "counter" id`);
108
+ }
109
+ if (entry.tiers !== void 0) {
110
+ const named = Array.isArray(entry.tiers) && entry.tiers.length > 0 && entry.tiers.every((tier) => typeof tier === "string" && tier.length > 0);
111
+ if (!named) {
112
+ throw new Error(
113
+ `${CONFIG_FILE}: "${keyOf(entry)}" has "tiers" that is not a non-empty list of tier names`
114
+ );
115
+ }
116
+ }
117
+ }
118
+ const keys = parsed.counters.map(keyOf);
119
+ const duplicate = keys.find((key, at) => keys.indexOf(key) !== at);
120
+ if (duplicate !== void 0) {
121
+ throw new Error(
122
+ `${CONFIG_FILE}: two counters both write "${duplicate}" \u2014 give one of them a distinct "key"`
123
+ );
124
+ }
125
+ return { baseline: parsed.baseline ?? "gate-baseline.json", counters: parsed.counters };
126
+ };
127
+
128
+ // src/core/types.ts
2
129
  var CounterError = class extends Error {
3
130
  constructor(counter, message) {
4
131
  super(`${counter}: ${message}`);
@@ -8,6 +135,294 @@ var CounterError = class extends Error {
8
135
  counter;
9
136
  };
10
137
 
138
+ // src/core/shell.ts
139
+ import { execSync } from "child_process";
140
+ var ANSI = /\[[0-9;]*m/g;
141
+ var runCommand = (cwd, counterId, env) => (command) => {
142
+ try {
143
+ const output = execSync(`(
144
+ ${command}
145
+ ) 2>&1`, {
146
+ cwd,
147
+ encoding: "utf8",
148
+ env,
149
+ maxBuffer: 64 * 1024 * 1024,
150
+ stdio: ["ignore", "pipe", "pipe"]
151
+ });
152
+ return { code: 0, output: output.replaceAll(ANSI, "") };
153
+ } catch (error) {
154
+ const failed = error;
155
+ const output = `${failed.stdout ?? ""}${failed.stderr ?? ""}`.replaceAll(ANSI, "");
156
+ const code = failed.status ?? -1;
157
+ if (code === 126 || code === 127 || code === -1) {
158
+ throw new CounterError(
159
+ counterId,
160
+ `command did not run (exit ${code}): ${command}
161
+ ${output.trim()}`
162
+ );
163
+ }
164
+ return { code, output };
165
+ }
166
+ };
167
+
168
+ // src/core/prove.ts
169
+ import { existsSync as existsSync2, mkdtempSync, rmSync as rmSync2 } from "fs";
170
+ import { tmpdir } from "os";
171
+ import { delimiter, dirname as dirname3, join as join2, resolve as resolve3 } from "path";
172
+
173
+ // src/core/exclusive.ts
174
+ import { spawn } from "child_process";
175
+ import { dirname as dirname2, resolve as resolve2 } from "path";
176
+ var MARK = /^exclusive-hold (start|end) (\d+)$/gm;
177
+ var KEY = "exclusive";
178
+ var spanOf = (output) => {
179
+ const marks = [...output.matchAll(MARK)];
180
+ const start = marks.find((one) => one[1] === "start")?.[2];
181
+ const end = marks.find((one) => one[1] === "end")?.[2];
182
+ if (start === void 0 || end === void 0) return void 0;
183
+ return { end: Number(end), start: Number(start) };
184
+ };
185
+ var hold = (cli, holdMs, lockPath) => new Promise((done) => {
186
+ const child = spawn(
187
+ process.execPath,
188
+ [cli, "--exclusive", "--exclusive-timeout", "60", "--hold", String(holdMs)],
189
+ {
190
+ cwd: dirname2(resolve2(cli)),
191
+ env: { ...process.env, GEONOSIS_HEAVY_LOCK: lockPath },
192
+ stdio: ["ignore", "pipe", "pipe"]
193
+ }
194
+ );
195
+ let output = "";
196
+ child.stdout.on("data", (chunk) => {
197
+ output += chunk.toString();
198
+ });
199
+ child.stderr.on("data", (chunk) => {
200
+ output += chunk.toString();
201
+ });
202
+ child.on("close", (code) => done({ code, output }));
203
+ });
204
+ var proveExclusive = async ({
205
+ cli,
206
+ holdMs = 400,
207
+ lockPath
208
+ }) => {
209
+ const runs = await Promise.all([hold(cli, holdMs, lockPath), hold(cli, holdMs, lockPath)]);
210
+ const spans = runs.map((one) => spanOf(one.output));
211
+ const [first, second] = spans;
212
+ if (first === void 0 || second === void 0) {
213
+ return {
214
+ counter: KEY,
215
+ key: KEY,
216
+ reason: `a run under --exclusive printed no start/end to compare:
217
+ ${runs.map((one) => `exit ${String(one.code)}: ${one.output.trim().slice(-300)}`).join("\n")}`,
218
+ verdict: "cannot-measure"
219
+ };
220
+ }
221
+ const [early, late] = first.start <= second.start ? [first, second] : [second, first];
222
+ const gap = late.start - early.end;
223
+ if (gap < 0) {
224
+ return {
225
+ key: KEY,
226
+ reason: `the second run started ${-gap}ms BEFORE the first finished \u2014 two runs, one lock, both holding it`,
227
+ verdict: "interleaved"
228
+ };
229
+ }
230
+ return {
231
+ key: KEY,
232
+ reason: `two runs of ${holdMs}ms serialised, the second starting ${gap}ms after the first finished`,
233
+ verdict: "serialised"
234
+ };
235
+ };
236
+
237
+ // src/core/prove.ts
238
+ var toolPath = (cwd) => {
239
+ const dirs = [];
240
+ let dir = resolve3(cwd);
241
+ for (; ; ) {
242
+ const bin = join2(dir, "node_modules", ".bin");
243
+ if (existsSync2(bin)) dirs.push(bin);
244
+ const parent = dirname3(dir);
245
+ if (parent === dir) break;
246
+ dir = parent;
247
+ }
248
+ return [...dirs, process.env.PATH ?? ""].join(delimiter);
249
+ };
250
+ var NO_PROBE = "no probe \u2014 a counter nobody has seen read a planted finding has not been shown to measure";
251
+ var oneProofOf = async (counter, key, path, probe) => {
252
+ const dir = mkdtempSync(join2(tmpdir(), "geonosis-prove-"));
253
+ try {
254
+ probe.input(dir);
255
+ const command = probe.command?.(dir);
256
+ const reading = await counter.run({
257
+ cwd: dir,
258
+ key,
259
+ params: { ...probe.params, ...command === void 0 ? {} : { command } },
260
+ run: runCommand(dir, counter.id, { ...process.env, PATH: path })
261
+ });
262
+ if (reading === 0) return { counter: counter.id, key, reading, verdict: "cannot-fail" };
263
+ if (reading < probe.expect) {
264
+ return { counter: counter.id, expected: probe.expect, key, reading, verdict: "misread" };
265
+ }
266
+ return { counter: counter.id, key, reading, verdict: "proven" };
267
+ } catch (error) {
268
+ return { counter: counter.id, key, reason: error.message, verdict: "cannot-measure" };
269
+ } finally {
270
+ rmSync2(dir, { force: true, recursive: true });
271
+ }
272
+ };
273
+ var probesOf = (counter) => {
274
+ if (counter.probe === void 0) return [];
275
+ return Array.isArray(counter.probe) ? counter.probe : [counter.probe];
276
+ };
277
+ var proofOf = async (counter, key, path) => {
278
+ const probes = probesOf(counter);
279
+ if (probes.length === 0) {
280
+ return [{ counter: counter.id, key, reason: NO_PROBE, verdict: "cannot-measure" }];
281
+ }
282
+ const proofs = [];
283
+ for (const probe of probes) {
284
+ const named = probe.name === void 0 ? key : `${key} (${probe.name})`;
285
+ const proof = await oneProofOf(counter, named, path, probe);
286
+ proofs.push(proof);
287
+ if (proof.verdict !== "proven") break;
288
+ }
289
+ return proofs;
290
+ };
291
+ var outsideTier = (entry, tier) => entry.tiers !== void 0 && !entry.tiers.includes(tier);
292
+ var runProve = async ({
293
+ counters,
294
+ cwd,
295
+ exclusiveVia,
296
+ tier
297
+ }) => {
298
+ const config = loadConfig(cwd);
299
+ const byId = new Map(counters.map((one) => [one.id, one]));
300
+ const path = toolPath(cwd);
301
+ const proofs = [];
302
+ for (const entry of config.counters) {
303
+ const counter = byId.get(entry.counter);
304
+ if (counter === void 0) {
305
+ throw new Error(
306
+ `no counter implements "${entry.counter}" \u2014 known ids: ${[...byId.keys()].toSorted().join(", ")}`
307
+ );
308
+ }
309
+ const key = keyOf(entry);
310
+ if (tier !== void 0 && outsideTier(entry, tier)) {
311
+ proofs.push({ key, tier, verdict: "skipped" });
312
+ continue;
313
+ }
314
+ const taken = await proofOf(counter, key, path);
315
+ proofs.push(...taken);
316
+ if (taken.some((one) => one.verdict !== "proven" && one.verdict !== "skipped")) {
317
+ return { proofs, proven: false };
318
+ }
319
+ }
320
+ if (exclusiveVia !== void 0) {
321
+ const dir = mkdtempSync(join2(tmpdir(), "geonosis-lock-"));
322
+ try {
323
+ const proof = await proveExclusive({ cli: exclusiveVia, lockPath: join2(dir, "heavy.lock") });
324
+ proofs.push(proof);
325
+ if (proof.verdict !== "serialised") return { proofs, proven: false };
326
+ } finally {
327
+ rmSync2(dir, { force: true, recursive: true });
328
+ }
329
+ }
330
+ return { proofs, proven: true };
331
+ };
332
+
333
+ // src/core/ratchet.ts
334
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
335
+ import { resolve as resolve4 } from "path";
336
+ var EVIDENCE_LINES = 10;
337
+ var recorded = (run) => {
338
+ let output = "";
339
+ return {
340
+ last: () => output.split("\n").map((line) => line.trimEnd()).filter((line) => line !== "").slice(-EVIDENCE_LINES),
341
+ run: (command) => {
342
+ const result = run(command);
343
+ output = result.output;
344
+ return result;
345
+ }
346
+ };
347
+ };
348
+ var verdictOf = (now, baseline, tolerance) => {
349
+ if (now > baseline * (1 + tolerance)) return "grew";
350
+ if (now < baseline) return "shrank";
351
+ return "held";
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
+ };
368
+ var outsideTier2 = (entry, tier) => entry.tiers !== void 0 && !entry.tiers.includes(tier);
369
+ var runRatchet = async ({
370
+ counters,
371
+ cwd,
372
+ tier
373
+ }) => {
374
+ const config = loadConfig(cwd);
375
+ const baselinePath = resolve4(cwd, config.baseline);
376
+ if (!existsSync3(baselinePath)) {
377
+ throw new Error(`no ${config.baseline} in ${cwd} \u2014 nothing to ratchet against`);
378
+ }
379
+ const baseline = JSON.parse(readFileSync3(baselinePath, "utf8"));
380
+ const byId = new Map(counters.map((one) => [one.id, one]));
381
+ const measurements = [];
382
+ for (const entry of config.counters) {
383
+ const counter = byId.get(entry.counter);
384
+ if (counter === void 0) {
385
+ throw new Error(
386
+ `no counter implements "${entry.counter}" \u2014 known ids: ${[...byId.keys()].toSorted().join(", ")}`
387
+ );
388
+ }
389
+ const key = keyOf(entry);
390
+ if (tier !== void 0 && outsideTier2(entry, tier)) {
391
+ measurements.push({ key, tier, verdict: "skipped" });
392
+ continue;
393
+ }
394
+ const limit = baseline[key];
395
+ if (typeof limit !== "number") {
396
+ throw new Error(
397
+ `${config.baseline} has no number for "${key}" \u2014 add it before enabling the counter`
398
+ );
399
+ }
400
+ const tolerance = toleranceOf(entry, key, counter);
401
+ const recorder = recorded(runCommand(cwd, entry.counter));
402
+ const now = await counter.run({ cwd, key, params: entry, run: recorder.run });
403
+ const verdict = verdictOf(now, limit, tolerance);
404
+ measurements.push({
405
+ baseline: limit,
406
+ evidence: verdict === "grew" ? recorder.last() : [],
407
+ key,
408
+ now,
409
+ verdict
410
+ });
411
+ }
412
+ const grew = measurements.some((one) => one.verdict === "grew");
413
+ const shrank = measurements.some((one) => one.verdict === "shrank");
414
+ if (shrank && !grew) {
415
+ const next = { ...baseline };
416
+ for (const one of measurements) {
417
+ if (one.verdict === "shrank") next[one.key] = one.now;
418
+ }
419
+ writeFileSync2(baselinePath, `${JSON.stringify(next, null, 2)}
420
+ `);
421
+ return { measurements, rewritten: true };
422
+ }
423
+ return { measurements, rewritten: false };
424
+ };
425
+
11
426
  // src/counters/params.ts
12
427
  var stringParam = (counter, params, name, fallback) => {
13
428
  const value = params[name];
@@ -24,12 +439,12 @@ var countMatches = (text, pattern) => text.match(new RegExp(pattern.source, `${p
24
439
  var escapeForRegex = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
25
440
 
26
441
  // src/counters/plant.ts
27
- import { mkdirSync, writeFileSync } from "fs";
28
- import { dirname, join } from "path";
442
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
443
+ import { dirname as dirname4, join as join3 } from "path";
29
444
  var plant = (dir, relative, contents) => {
30
- const path = join(dir, relative);
31
- mkdirSync(dirname(path), { recursive: true });
32
- writeFileSync(path, contents);
445
+ const path = join3(dir, relative);
446
+ mkdirSync2(dirname4(path), { recursive: true });
447
+ writeFileSync3(path, contents);
33
448
  };
34
449
  var captured = (sample) => ({
35
450
  command: () => "cat sample.txt",
@@ -37,13 +452,120 @@ var captured = (sample) => ({
37
452
  });
38
453
 
39
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
+ };
40
478
  var archViolations = {
41
479
  id: "archViolations",
42
- probe: { ...captured("\u2717 cell imports cell\nok\n"), expect: 1 },
480
+ probe: { ...scannerProbe, expect: 2 },
43
481
  run: async ({ params, run }) => {
44
482
  const command = stringParam("archViolations", params, "command");
45
483
  const match = new RegExp(stringParam("archViolations", params, "match", "^\u2717"));
46
- return run(command).output.split("\n").filter((line) => match.test(line)).length;
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;
47
569
  }
48
570
  };
49
571
 
@@ -93,8 +615,8 @@ var boundaryIssues = {
93
615
  };
94
616
 
95
617
  // src/counters/format.ts
96
- import { existsSync } from "fs";
97
- import { resolve } from "path";
618
+ import { existsSync as existsSync4 } from "fs";
619
+ import { resolve as resolve6 } from "path";
98
620
  var unformattedFiles = {
99
621
  id: "unformattedFiles",
100
622
  probe: {
@@ -109,13 +631,67 @@ var unformattedFiles = {
109
631
  "command",
110
632
  "npx oxfmt --config .oxfmtrc.json --list-different ."
111
633
  );
112
- return run(command).output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && existsSync(resolve(cwd, line))).length;
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;
113
689
  }
114
690
  };
115
691
 
116
692
  // src/counters/law.ts
117
- import { existsSync as existsSync2, readFileSync } from "fs";
118
- import { resolve as resolve2 } from "path";
693
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
694
+ import { resolve as resolve8 } from "path";
119
695
  var lawLineCount = {
120
696
  id: "lawLineCount",
121
697
  probe: {
@@ -125,15 +701,15 @@ var lawLineCount = {
125
701
  },
126
702
  run: async ({ cwd, params }) => {
127
703
  const relative = stringParam("lawLineCount", params, "path", "CLAUDE.md");
128
- const path = resolve2(cwd, relative);
129
- if (!existsSync2(path)) throw new CounterError("lawLineCount", `no law file at ${relative}`);
130
- return readFileSync(path, "utf8").replace(/\n$/, "").split("\n").length;
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;
131
707
  }
132
708
  };
133
709
 
134
710
  // src/counters/oxlint.ts
135
- import { existsSync as existsSync3, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
136
- import { resolve as resolve3 } from "path";
711
+ import { existsSync as existsSync7, readFileSync as readFileSync7, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
712
+ import { resolve as resolve9 } from "path";
137
713
  var DEFAULT_COMMAND = "npx oxlint --format=unix --config .oxlintrc.json .";
138
714
  var PROBE_COMMAND = "oxlint --format=unix --config .oxlintrc.json .";
139
715
  var oxlintProbe = (severity, rule, source) => ({
@@ -187,21 +763,21 @@ var readFindings = (counter, { code, output }, expect) => {
187
763
  const agent = countFindings(output, AGENT_FINDING, "error");
188
764
  const problems = UNIX_SUMMARY.exec(output);
189
765
  const found = DEFAULT_SUMMARY.exec(output);
190
- const refuse = (why) => {
766
+ const refuse2 = (why) => {
191
767
  throw new CounterError(counter, `${why} \u2014 oxlint's output changed:
192
768
  ${output.trim()}`);
193
769
  };
194
770
  if (total(unix) > 0 || problems !== null) {
195
- if (problems?.[1] === void 0) return refuse('unix findings with no "N problems" summary');
771
+ if (problems?.[1] === void 0) return refuse2('unix findings with no "N problems" summary');
196
772
  if (Number(problems[1]) !== total(unix)) {
197
- return refuse(`the summary says ${problems[1]} problems, the lines say ${total(unix)}`);
773
+ return refuse2(`the summary says ${problems[1]} problems, the lines say ${total(unix)}`);
198
774
  }
199
775
  return unix;
200
776
  }
201
777
  if (found?.[1] !== void 0 && found[2] !== void 0) {
202
778
  const summary = { errors: Number(found[2]), warnings: Number(found[1]) };
203
779
  if (total(agent) > 0 && (agent.errors !== summary.errors || agent.warnings !== summary.warnings)) {
204
- return refuse(
780
+ return refuse2(
205
781
  `the summary says ${summary.errors} errors and ${summary.warnings} warnings, the lines say ${agent.errors} and ${agent.warnings}`
206
782
  );
207
783
  }
@@ -210,11 +786,11 @@ ${output.trim()}`);
210
786
  if (total(agent) > 0) return agent;
211
787
  if (code === 0) {
212
788
  if (expect !== void 0 && spokenByTheTool(output) !== "") {
213
- return refuse(`asked for --format=${expect} and got output in no shape this counter reads`);
789
+ return refuse2(`asked for --format=${expect} and got output in no shape this counter reads`);
214
790
  }
215
791
  return { errors: 0, warnings: 0 };
216
792
  }
217
- return refuse(`the tool exited ${code} and printed no findings and no summary`);
793
+ return refuse2(`the tool exited ${code} and printed no findings and no summary`);
218
794
  };
219
795
  var oxlintErrors = {
220
796
  id: "oxlintErrors",
@@ -272,18 +848,18 @@ var oxlintRule = {
272
848
  const config = typeof params.config === "string" ? params.config : "";
273
849
  const expect = expectedFormat("oxlintRule", params);
274
850
  if (config === "") return countRule(run(command), rule, expect);
275
- const source = resolve3(cwd, config);
276
- if (!existsSync3(source)) throw new CounterError("oxlintRule", `no config at ${config}`);
851
+ const source = resolve9(cwd, config);
852
+ if (!existsSync7(source)) throw new CounterError("oxlintRule", `no config at ${config}`);
277
853
  const strictName = `.oxlintrc.ratchet-${key}.json`;
278
- const strict = readFileSync2(source, "utf8").replace(
854
+ const strict = readFileSync7(source, "utf8").replace(
279
855
  new RegExp(`("[^"]*${escapeForRegex(rule)}"\\s*:\\s*\\[?\\s*)"(warn|off)"`),
280
856
  '$1"error"'
281
857
  );
282
- writeFileSync2(resolve3(cwd, strictName), strict);
858
+ writeFileSync4(resolve9(cwd, strictName), strict);
283
859
  try {
284
860
  return countRule(run(command.replace("{config}", strictName)), rule, expect);
285
861
  } finally {
286
- rmSync(resolve3(cwd, strictName), { force: true });
862
+ rmSync3(resolve9(cwd, strictName), { force: true });
287
863
  }
288
864
  }
289
865
  };
@@ -311,44 +887,17 @@ var runtimeCodeShipped = {
311
887
  }
312
888
  };
313
889
 
314
- // src/counters/sum-of-counts.ts
315
- var sumOfCounts = {
316
- id: "sumOfCounts",
317
- probe: { ...captured("src/a.ts:1\nsrc/b.ts:0\n"), expect: 1 },
318
- run: async ({ params, run }) => {
319
- const command = stringParam("sumOfCounts", params, "command");
320
- const match = new RegExp(stringParam("sumOfCounts", params, "match", ":(\\d+)$"), "gm");
321
- return [...run(command).output.matchAll(match)].map(([, digits]) => Number(digits ?? 0)).filter((count) => Number.isFinite(count)).reduce((sum, count) => sum + count, 0);
322
- }
323
- };
324
-
325
- // src/counters/tests.ts
326
- var FAILED = /(\d+)\s+fail(?:ed|ing|s)?\b/;
327
- var PASSED = /(\d+)\s+pass(?:ed|ing|es)?\b/;
328
- var testFailures = {
329
- id: "testFailures",
330
- probe: { ...captured(" Tests 1 failed | 0 passed (1)\n"), expect: 1 },
331
- run: async ({ params, run }) => {
332
- const command = stringParam("testFailures", params, "command", "npx vitest run");
333
- const output = run(command).output;
334
- const failed = output.match(FAILED)?.[1];
335
- if (failed !== void 0) return Number(failed);
336
- if (PASSED.test(output)) return 0;
337
- throw new CounterError(
338
- "testFailures",
339
- `no pass/fail summary in the runner output:
340
- ${output.trim().slice(-500)}`
341
- );
342
- }
343
- };
890
+ // src/counters/scripts.ts
891
+ import { readdirSync as readdirSync3 } from "fs";
892
+ import { join as join6 } from "path";
344
893
 
345
894
  // src/counters/workspace.ts
346
- import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync3 } from "fs";
347
- import { join as join2, resolve as resolve4 } from "path";
895
+ import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync8 } from "fs";
896
+ import { join as join5, resolve as resolve10 } from "path";
348
897
  var SKIP = /^(node_modules|\.)/;
349
898
  var childDirs = (dir) => {
350
899
  try {
351
- return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !SKIP.test(entry.name)).map((entry) => join2(dir, entry.name));
900
+ return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !SKIP.test(entry.name)).map((entry) => join5(dir, entry.name));
352
901
  } catch {
353
902
  return [];
354
903
  }
@@ -358,14 +907,14 @@ var expand = (root, pattern) => {
358
907
  const segments = pattern.split("/").filter((one) => one !== "" && one !== ".");
359
908
  let dirs = [root];
360
909
  for (const segment of segments) {
361
- dirs = segment === "*" ? dirs.flatMap((dir) => childDirs(dir)) : segment === "**" ? dirs.flatMap((dir) => descendants(dir, 3)) : dirs.map((dir) => join2(dir, segment)).filter((dir) => existsSync4(dir));
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));
362
911
  }
363
912
  return dirs;
364
913
  };
365
914
  var QUOTED = /^['"]|['"]$/g;
366
915
  var cleaned = (value) => value.replace(/#.*$/, "").trim().replaceAll(QUOTED, "");
367
916
  var pnpmPatterns = (path) => {
368
- const lines = readFileSync3(path, "utf8").split("\n");
917
+ const lines = readFileSync8(path, "utf8").split("\n");
369
918
  const at = lines.findIndex((line) => line.startsWith("packages:"));
370
919
  if (at === -1) return [];
371
920
  const inline = lines[at]?.slice("packages:".length).trim() ?? "";
@@ -384,410 +933,378 @@ var pnpmPatterns = (path) => {
384
933
  return patterns;
385
934
  };
386
935
  var npmPatterns = (path) => {
387
- const parsed = JSON.parse(readFileSync3(path, "utf8"));
936
+ const parsed = JSON.parse(readFileSync8(path, "utf8"));
388
937
  const declared = Array.isArray(parsed.workspaces) ? parsed.workspaces : parsed.workspaces?.packages ?? [];
389
938
  return declared.filter((one) => typeof one === "string");
390
939
  };
391
940
  var nameOf = (dir) => {
392
- const manifest = join2(dir, "package.json");
393
- if (!existsSync4(manifest)) return void 0;
941
+ const manifest = join5(dir, "package.json");
942
+ if (!existsSync8(manifest)) return void 0;
394
943
  try {
395
- const { name } = JSON.parse(readFileSync3(manifest, "utf8"));
944
+ const { name } = JSON.parse(readFileSync8(manifest, "utf8"));
396
945
  return typeof name === "string" && name !== "" ? name : void 0;
397
946
  } catch {
398
947
  return void 0;
399
948
  }
400
949
  };
401
- var workspacePackageNames = (cwd) => {
402
- const root = resolve4(cwd);
403
- const pnpm = join2(root, "pnpm-workspace.yaml");
404
- const manifest = join2(root, "package.json");
405
- const patterns = existsSync4(pnpm) ? pnpmPatterns(pnpm) : existsSync4(manifest) ? npmPatterns(manifest) : [];
406
- const names = patterns.filter((pattern) => !pattern.startsWith("!")).flatMap((pattern) => expand(root, pattern)).map((dir) => nameOf(dir)).filter((name) => name !== void 0);
407
- return [...new Set(names)];
408
- };
409
-
410
- // src/counters/typecheck.ts
411
- var UNRESOLVED = /error TS(?:2305|2307): ([^\n]*)/g;
412
- var SPECIFIER = /'([^']+)'/g;
413
- var specifiersIn = (message) => [...message.matchAll(SPECIFIER)].map(([, quoted]) => (quoted ?? "").replaceAll('"', ""));
414
- var unbuiltIn = (cwd, output) => {
415
- const messages = [...output.matchAll(UNRESOLVED)].map(([, message]) => message ?? "");
416
- if (messages.length === 0) return [];
417
- const packages = workspacePackageNames(cwd);
418
- const named = messages.flatMap((message) => specifiersIn(message)).flatMap(
419
- (specifier) => packages.filter((name) => specifier === name || specifier.startsWith(`${name}/`))
420
- );
421
- return [...new Set(named)].toSorted();
422
- };
423
- var typecheckErrors = {
424
- id: "typecheckErrors",
425
- probe: {
426
- command: () => "tsc --noEmit a.ts",
427
- expect: 1,
428
- input: (dir) => plant(dir, "a.ts", "export const n: number = 'not a number'\n")
429
- },
430
- run: async ({ cwd, params, run }) => {
431
- const command = stringParam("typecheckErrors", params, "command", "npx tsc --noEmit");
432
- const output = run(command).output;
433
- const unbuilt = unbuiltIn(cwd, output);
434
- if (unbuilt.length > 0) {
435
- throw new CounterError(
436
- "typecheckErrors",
437
- `workspace packages not built: ${unbuilt.join(", ")} \u2014 build them before measuring typecheckErrors`
438
- );
439
- }
440
- return countMatches(output, /error TS/);
441
- }
442
- };
443
-
444
- // src/counters/index.ts
445
- var COUNTERS = [
446
- archViolations,
447
- boundaryIssues,
448
- cloneCount,
449
- knipIssues,
450
- lawLineCount,
451
- oxlintErrors,
452
- oxlintRule,
453
- oxlintWarnings,
454
- runtimeCodeShipped,
455
- sumOfCounts,
456
- testFailures,
457
- typecheckErrors,
458
- unformattedFiles
459
- ];
460
- var counterById = (id) => {
461
- const counter = COUNTERS.find((one) => one.id === id);
462
- if (counter === void 0) {
463
- throw new Error(
464
- `no counter implements "${id}" \u2014 known ids: ${COUNTERS.map((one) => one.id).join(", ")}`
465
- );
466
- }
467
- return counter;
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)];
468
957
  };
469
-
470
- // src/lock.ts
471
- import { randomUUID } from "crypto";
472
- import { linkSync, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
473
- import { homedir } from "os";
474
- import { dirname as dirname2, join as join3 } from "path";
475
- var heavyLockPath = () => process.env.GEONOSIS_HEAVY_LOCK ?? join3(homedir(), ".cache", "geonosis", "heavy.lock");
476
- var sleep = (ms) => new Promise((done) => setTimeout(done, ms));
477
- var holderOf = (path) => {
958
+ var manifestOf = (dir) => {
478
959
  try {
479
- const parsed = JSON.parse(readFileSync4(path, "utf8"));
480
- return typeof parsed.pid === "number" ? {
481
- cwd: parsed.cwd ?? "somewhere",
482
- pid: parsed.pid,
483
- startedAt: parsed.startedAt ?? "unknown"
484
- } : void 0;
960
+ const parsed = JSON.parse(readFileSync8(join5(dir, "package.json"), "utf8"));
961
+ return typeof parsed === "object" && parsed !== null ? parsed : void 0;
485
962
  } catch {
486
963
  return void 0;
487
964
  }
488
965
  };
489
- var alive = (pid) => {
490
- try {
491
- process.kill(pid, 0);
492
- return true;
493
- } catch (error) {
494
- return error.code === "EPERM";
495
- }
966
+ var scriptsOf = (dir) => {
967
+ const scripts = manifestOf(dir)?.scripts;
968
+ return typeof scripts === "object" && scripts !== null ? Object.keys(scripts) : [];
496
969
  };
497
- var heldFor = (holder) => {
498
- const since = Date.parse(holder.startedAt);
499
- if (Number.isNaN(since)) return "an unknown time";
500
- return `${Math.round((Date.now() - since) / 1e3)}s`;
970
+ var workspacePackageNames = (cwd) => {
971
+ const names = workspaceDirs(cwd).map((dir) => nameOf(dir)).filter((name) => name !== void 0);
972
+ return [...new Set(names)];
501
973
  };
502
- var write = (path) => {
503
- mkdirSync2(dirname2(path), { recursive: true });
504
- const mine = {
505
- cwd: process.cwd(),
506
- pid: process.pid,
507
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
508
- };
509
- const staging = `${path}.${process.pid}.${randomUUID()}`;
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;
510
981
  try {
511
- writeFileSync3(staging, JSON.stringify(mine));
512
- linkSync(staging, path);
513
- return true;
514
- } catch (error) {
515
- if (error.code === "EEXIST") return false;
516
- throw error;
517
- } finally {
518
- rmSync2(staging, { force: true });
982
+ entries = readdirSync3(dir, { withFileTypes: true });
983
+ } catch {
984
+ return false;
519
985
  }
520
- };
521
- var acquireExclusive = async ({
522
- noticeMs = 15e3,
523
- path = heavyLockPath(),
524
- pollMs = 250,
525
- say = (line) => process.stderr.write(`${line}
526
- `),
527
- timeoutSeconds = 1800
528
- } = {}) => {
529
- const until = Date.now() + timeoutSeconds * 1e3;
530
- let told = 0;
531
- const release = () => {
532
- if (holderOf(path)?.pid === process.pid) rmSync2(path, { force: true });
533
- };
534
- for (; ; ) {
535
- if (write(path)) return release;
536
- const holder = holderOf(path);
537
- if (holder === void 0 || !alive(holder.pid)) {
538
- say(
539
- `geonosis-ratchet: taking over a stale heavy lock (pid ${holder?.pid ?? "unreadable"} is gone)`
540
- );
541
- rmSync2(path, { force: true });
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;
542
991
  continue;
543
992
  }
544
- if (Date.now() >= until) {
545
- throw new Error(
546
- `waited ${timeoutSeconds}s for the heavy lock held by pid ${holder.pid} in ${holder.cwd} (${heldFor(holder)}) \u2014 give it longer with --exclusive-timeout, or stop that run`
547
- );
548
- }
549
- const now = Date.now();
550
- if (now - told >= noticeMs) {
551
- told = now;
552
- say(
553
- `geonosis-ratchet: waiting for the heavy lock \u2014 pid ${holder.pid} in ${holder.cwd}, held for ${heldFor(holder)}`
554
- );
555
- }
556
- await sleep(pollMs);
993
+ if (TEST_FILE.test(entry.name)) return true;
557
994
  }
995
+ return false;
558
996
  };
559
-
560
- // src/config.ts
561
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
562
- import { resolve as resolve5 } from "path";
563
- var CONFIG_FILE = "geonosis.ratchet.json";
564
- var keyOf = (entry) => entry.key ?? entry.counter;
565
- var loadConfig = (cwd) => {
566
- const path = resolve5(cwd, CONFIG_FILE);
567
- if (!existsSync5(path)) {
568
- throw new Error(`no ${CONFIG_FILE} in ${cwd} \u2014 the ratchet has nothing to count`);
569
- }
570
- const parsed = JSON.parse(readFileSync5(path, "utf8"));
571
- if (!Array.isArray(parsed.counters)) {
572
- throw new Error(`${CONFIG_FILE} has no "counters" array`);
573
- }
574
- for (const entry of parsed.counters) {
575
- if (typeof entry.counter !== "string") {
576
- throw new Error(`${CONFIG_FILE}: every counter entry needs a "counter" id`);
577
- }
578
- if (entry.tiers !== void 0) {
579
- const named = Array.isArray(entry.tiers) && entry.tiers.length > 0 && entry.tiers.every((tier) => typeof tier === "string" && tier.length > 0);
580
- if (!named) {
581
- throw new Error(
582
- `${CONFIG_FILE}: "${keyOf(entry)}" has "tiers" that is not a non-empty list of tier names`
583
- );
584
- }
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');
585
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;
586
1010
  }
587
- const keys = parsed.counters.map(keyOf);
588
- const duplicate = keys.find((key, at) => keys.indexOf(key) !== at);
589
- if (duplicate !== void 0) {
590
- throw new Error(
591
- `${CONFIG_FILE}: two counters both write "${duplicate}" \u2014 give one of them a distinct "key"`
592
- );
593
- }
594
- return { baseline: parsed.baseline ?? "gate-baseline.json", counters: parsed.counters };
595
1011
  };
596
-
597
- // src/shell.ts
598
- import { execSync } from "child_process";
599
- var ANSI = /\[[0-9;]*m/g;
600
- var runCommand = (cwd, counterId, env) => (command) => {
601
- try {
602
- const output = execSync(`${command} 2>&1`, {
603
- cwd,
604
- encoding: "utf8",
605
- env,
606
- maxBuffer: 64 * 1024 * 1024,
607
- stdio: ["ignore", "pipe", "pipe"]
608
- });
609
- return { code: 0, output: output.replaceAll(ANSI, "") };
610
- } catch (error) {
611
- const failed = error;
612
- const output = `${failed.stdout ?? ""}${failed.stderr ?? ""}`.replaceAll(ANSI, "");
613
- const code = failed.status ?? -1;
614
- if (code === 126 || code === 127 || code === -1) {
615
- throw new CounterError(
616
- counterId,
617
- `command did not run (exit ${code}): ${command}
618
- ${output.trim()}`
619
- );
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"}}');
620
1019
  }
621
- return { code, output };
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;
622
1024
  }
623
1025
  };
624
1026
 
625
- // src/prove.ts
626
- import { existsSync as existsSync6, mkdtempSync, rmSync as rmSync3 } from "fs";
627
- import { tmpdir } from "os";
628
- import { delimiter, dirname as dirname3, join as join4, resolve as resolve6 } from "path";
629
- var toolPath = (cwd) => {
630
- const dirs = [];
631
- let dir = resolve6(cwd);
632
- for (; ; ) {
633
- const bin = join4(dir, "node_modules", ".bin");
634
- if (existsSync6(bin)) dirs.push(bin);
635
- const parent = dirname3(dir);
636
- if (parent === dir) break;
637
- dir = parent;
1027
+ // src/counters/sum-of-counts.ts
1028
+ var sumOfCounts = {
1029
+ id: "sumOfCounts",
1030
+ probe: { ...captured("src/a.ts:1\nsrc/b.ts:0\n"), expect: 1 },
1031
+ run: async ({ params, run }) => {
1032
+ const command = stringParam("sumOfCounts", params, "command");
1033
+ const match = new RegExp(stringParam("sumOfCounts", params, "match", ":(\\d+)$"), "gm");
1034
+ return [...run(command).output.matchAll(match)].map(([, digits]) => Number(digits ?? 0)).filter((count) => Number.isFinite(count)).reduce((sum, count) => sum + count, 0);
638
1035
  }
639
- return [...dirs, process.env.PATH ?? ""].join(delimiter);
640
1036
  };
641
- var NO_PROBE = "no probe \u2014 a counter nobody has seen read a planted finding has not been shown to measure";
642
- var proofOf = async (counter, key, path) => {
643
- const probe = counter.probe;
644
- if (probe === void 0)
645
- return { counter: counter.id, key, reason: NO_PROBE, verdict: "cannot-measure" };
646
- const dir = mkdtempSync(join4(tmpdir(), "geonosis-prove-"));
647
- try {
648
- probe.input(dir);
649
- const command = probe.command?.(dir);
650
- const reading = await counter.run({
651
- cwd: dir,
652
- key,
653
- params: { ...probe.params, ...command === void 0 ? {} : { command } },
654
- run: runCommand(dir, counter.id, { ...process.env, PATH: path })
655
- });
656
- if (reading === 0) return { counter: counter.id, key, reading, verdict: "cannot-fail" };
657
- if (reading < probe.expect) {
658
- return { counter: counter.id, expected: probe.expect, key, reading, verdict: "misread" };
1037
+
1038
+ // src/counters/tests.ts
1039
+ import { existsSync as existsSync9, mkdtempSync as mkdtempSync2, readFileSync as readFileSync9, rmSync as rmSync4 } from "fs";
1040
+ import { tmpdir as tmpdir2 } from "os";
1041
+ import { join as join7, resolve as resolve11 } from "path";
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")';
1048
+ var VITEST_JSON = "vitest-json";
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;
659
1057
  }
660
- return { counter: counter.id, key, reading, verdict: "proven" };
1058
+ const bun = BUN_LINE.exec(line)?.[1];
1059
+ if (bun !== void 0) found.push(Number(bun));
1060
+ }
1061
+ return found;
1062
+ };
1063
+ var fromSummary = (output) => {
1064
+ const found = summaries(output);
1065
+ if (found.length > 0) return found.reduce((total2, one) => total2 + one, 0);
1066
+ throw new CounterError(
1067
+ COUNTER,
1068
+ `no runner summary line in the output \u2014 this reads ${DIALECTS}:
1069
+ ${output.trim().slice(-500)}`
1070
+ );
1071
+ };
1072
+ var fromReport = (path) => {
1073
+ if (!existsSync9(path)) {
1074
+ throw new CounterError(
1075
+ COUNTER,
1076
+ `the runner wrote no report at ${path} \u2014 a crash before the reporter is not a pass`
1077
+ );
1078
+ }
1079
+ let report;
1080
+ try {
1081
+ report = JSON.parse(readFileSync9(path, "utf8"));
661
1082
  } catch (error) {
662
- return { counter: counter.id, key, reason: error.message, verdict: "cannot-measure" };
663
- } finally {
664
- rmSync3(dir, { force: true, recursive: true });
1083
+ throw new CounterError(
1084
+ COUNTER,
1085
+ `the report at ${path} is not JSON: ${error.message}`
1086
+ );
1087
+ }
1088
+ const failed = report.numFailedTests;
1089
+ if (typeof failed !== "number") {
1090
+ throw new CounterError(
1091
+ COUNTER,
1092
+ `the report at ${path} has no "numFailedTests" \u2014 it is not a vitest JSON report`
1093
+ );
665
1094
  }
1095
+ if (report.success === false && failed === 0) {
1096
+ throw new CounterError(
1097
+ COUNTER,
1098
+ `the report at ${path} says success: false and names 0 failing tests \u2014 the run did not finish, so there is no number to bank`
1099
+ );
1100
+ }
1101
+ return failed;
666
1102
  };
667
- var outsideTier = (entry, tier) => entry.tiers !== void 0 && !entry.tiers.includes(tier);
668
- var runProve = async ({
669
- counters,
670
- cwd,
671
- tier
672
- }) => {
673
- const config = loadConfig(cwd);
674
- const byId = new Map(counters.map((one) => [one.id, one]));
675
- const path = toolPath(cwd);
676
- const proofs = [];
677
- for (const entry of config.counters) {
678
- const counter = byId.get(entry.counter);
679
- if (counter === void 0) {
680
- throw new Error(
681
- `no counter implements "${entry.counter}" \u2014 known ids: ${[...byId.keys()].toSorted().join(", ")}`
682
- );
1103
+ var reportPathFor = (cwd, params, command) => {
1104
+ const named = params.reportPath;
1105
+ if (typeof named === "string" && named !== "") return { own: false, path: resolve11(cwd, named) };
1106
+ if (!command.includes(PLACEHOLDER)) {
1107
+ throw new CounterError(
1108
+ COUNTER,
1109
+ `report: "${VITEST_JSON}" needs somewhere to put the report \u2014 write ${PLACEHOLDER} into the command (--outputFile=${PLACEHOLDER}) or give the entry a "reportPath"`
1110
+ );
1111
+ }
1112
+ return { own: true, path: join7(mkdtempSync2(join7(tmpdir2(), "geonosis-report-")), "report.json") };
1113
+ };
1114
+ var testFailures = {
1115
+ id: COUNTER,
1116
+ probe: [
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
+ },
1131
+ {
1132
+ // `cat` stands in for the runner: the placeholder is substituted into it, so a counter that
1133
+ // stopped substituting would hand `cat` the literal token and read nothing.
1134
+ command: () => `cat ${PLACEHOLDER}`,
1135
+ expect: 1,
1136
+ input: (dir) => plant(
1137
+ dir,
1138
+ "planted-report.json",
1139
+ `${JSON.stringify({ numFailedTests: 1, numTotalTests: 1, success: false })}
1140
+ `
1141
+ ),
1142
+ name: VITEST_JSON,
1143
+ params: { report: VITEST_JSON, reportPath: "planted-report.json" }
683
1144
  }
684
- const key = keyOf(entry);
685
- if (tier !== void 0 && outsideTier(entry, tier)) {
686
- proofs.push({ key, tier, verdict: "skipped" });
687
- continue;
1145
+ ],
1146
+ run: async ({ cwd, params, run }) => {
1147
+ const command = stringParam(COUNTER, params, "command", "npx vitest run");
1148
+ const mode = params.report;
1149
+ if (mode === void 0) return fromSummary(run(command).output);
1150
+ if (mode !== VITEST_JSON) {
1151
+ throw new CounterError(
1152
+ COUNTER,
1153
+ `does not know the report format "${String(mode)}" \u2014 the one it reads is "${VITEST_JSON}"`
1154
+ );
688
1155
  }
689
- const proof = await proofOf(counter, key, path);
690
- proofs.push(proof);
691
- if (proof.verdict !== "proven" && proof.verdict !== "skipped") {
692
- return { proofs, proven: false };
1156
+ const { own, path } = reportPathFor(cwd, params, command);
1157
+ try {
1158
+ run(command.replaceAll(PLACEHOLDER, path));
1159
+ return fromReport(path);
1160
+ } finally {
1161
+ if (own) rmSync4(join7(path, ".."), { force: true, recursive: true });
693
1162
  }
694
1163
  }
695
- return { proofs, proven: true };
696
1164
  };
697
- var formatProve = ({ proofs, proven }) => {
698
- const lines = proofs.map((one) => {
699
- if (one.verdict === "skipped") return ` SKIP ${one.key}: not measured by --tier ${one.tier}`;
700
- if (one.verdict === "cannot-measure") return ` CANNOT MEASURE ${one.key}: ${one.reason}`;
701
- if (one.verdict === "cannot-fail") return ` CANNOT FAIL ${one.key}: read 0`;
702
- if (one.verdict === "misread") {
703
- return ` MISREAD ${one.key}: read ${one.reading} where its probe planted ${one.expected}`;
704
- }
705
- return ` PROVEN ${one.key}: reads ${one.reading} on a planted finding`;
706
- });
707
- lines.push(
708
- "",
709
- proven ? "prove PASS \u2014 every counter read the finding its probe planted." : "prove FAIL \u2014 a gate that has never been seen red has not been shown to measure."
1165
+
1166
+ // src/counters/typecheck.ts
1167
+ var UNRESOLVED = /error TS(?:2305|2307): ([^\n]*)/g;
1168
+ var SPECIFIER = /'([^']+)'/g;
1169
+ var specifiersIn = (message) => [...message.matchAll(SPECIFIER)].map(([, quoted]) => (quoted ?? "").replaceAll('"', ""));
1170
+ var unbuiltIn = (cwd, output) => {
1171
+ const messages = [...output.matchAll(UNRESOLVED)].map(([, message]) => message ?? "");
1172
+ if (messages.length === 0) return [];
1173
+ const packages = workspacePackageNames(cwd);
1174
+ const named = messages.flatMap((message) => specifiersIn(message)).flatMap(
1175
+ (specifier) => packages.filter((name) => specifier === name || specifier.startsWith(`${name}/`))
710
1176
  );
711
- return `${lines.join("\n")}
712
- `;
1177
+ return [...new Set(named)].toSorted();
713
1178
  };
714
-
715
- // src/ratchet.ts
716
- import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
717
- import { resolve as resolve7 } from "path";
718
- var EVIDENCE_LINES = 10;
719
- var recorded = (run) => {
720
- let output = "";
721
- return {
722
- last: () => output.split("\n").map((line) => line.trimEnd()).filter((line) => line !== "").slice(-EVIDENCE_LINES),
723
- run: (command) => {
724
- const result = run(command);
725
- output = result.output;
726
- return result;
1179
+ var typecheckErrors = {
1180
+ id: "typecheckErrors",
1181
+ probe: {
1182
+ command: () => "tsc --noEmit a.ts",
1183
+ expect: 1,
1184
+ input: (dir) => plant(dir, "a.ts", "export const n: number = 'not a number'\n")
1185
+ },
1186
+ run: async ({ cwd, params, run }) => {
1187
+ const command = stringParam("typecheckErrors", params, "command", "npx tsc --noEmit");
1188
+ const output = run(command).output;
1189
+ const unbuilt = unbuiltIn(cwd, output);
1190
+ if (unbuilt.length > 0) {
1191
+ throw new CounterError(
1192
+ "typecheckErrors",
1193
+ `workspace packages not built: ${unbuilt.join(", ")} \u2014 build them before measuring typecheckErrors`
1194
+ );
727
1195
  }
728
- };
729
- };
730
- var verdictOf = (now, baseline) => {
731
- if (now > baseline) return "grew";
732
- if (now < baseline) return "shrank";
733
- return "held";
734
- };
735
- var outsideTier2 = (entry, tier) => entry.tiers !== void 0 && !entry.tiers.includes(tier);
736
- var runRatchet = async ({
737
- counters,
738
- cwd,
739
- tier
740
- }) => {
741
- const config = loadConfig(cwd);
742
- const baselinePath = resolve7(cwd, config.baseline);
743
- if (!existsSync7(baselinePath)) {
744
- throw new Error(`no ${config.baseline} in ${cwd} \u2014 nothing to ratchet against`);
1196
+ return countMatches(output, /error TS/);
745
1197
  }
746
- const baseline = JSON.parse(readFileSync6(baselinePath, "utf8"));
747
- const byId = new Map(counters.map((one) => [one.id, one]));
748
- const measurements = [];
749
- for (const entry of config.counters) {
750
- const counter = byId.get(entry.counter);
751
- if (counter === void 0) {
752
- throw new Error(
753
- `no counter implements "${entry.counter}" \u2014 known ids: ${[...byId.keys()].toSorted().join(", ")}`
1198
+ };
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`
754
1251
  );
755
1252
  }
756
- const key = keyOf(entry);
757
- if (tier !== void 0 && outsideTier2(entry, tier)) {
758
- measurements.push({ key, tier, verdict: "skipped" });
759
- continue;
760
- }
761
- const limit = baseline[key];
762
- if (typeof limit !== "number") {
763
- throw new Error(
764
- `${config.baseline} has no number for "${key}" \u2014 add it before enabling the counter`
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}`
765
1260
  );
766
1261
  }
767
- const recorder = recorded(runCommand(cwd, entry.counter));
768
- const now = await counter.run({ cwd, key, params: entry, run: recorder.run });
769
- const verdict = verdictOf(now, limit);
770
- measurements.push({
771
- baseline: limit,
772
- evidence: verdict === "grew" ? recorder.last() : [],
773
- key,
774
- now,
775
- verdict
776
- });
777
- }
778
- const grew = measurements.some((one) => one.verdict === "grew");
779
- const shrank = measurements.some((one) => one.verdict === "shrank");
780
- if (shrank && !grew) {
781
- const next = { ...baseline };
782
- for (const one of measurements) {
783
- if (one.verdict !== "skipped") next[one.key] = one.now;
1262
+ if (!Array.isArray(report.pages)) {
1263
+ throw new CounterError("walkFindings", `${relative} has no pages \u2014 it is not a walk report`);
784
1264
  }
785
- writeFileSync4(baselinePath, `${JSON.stringify(next, null, 2)}
786
- `);
787
- return { measurements, rewritten: true };
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;
788
1272
  }
789
- return { measurements, rewritten: false };
790
1273
  };
1274
+
1275
+ // src/counters/index.ts
1276
+ var COUNTERS = [
1277
+ archViolations,
1278
+ boundaryIssues,
1279
+ bundleBytes,
1280
+ cloneCount,
1281
+ disabledCiJobs,
1282
+ fastTierMs,
1283
+ knipIssues,
1284
+ lawLineCount,
1285
+ oxlintErrors,
1286
+ oxlintRule,
1287
+ oxlintWarnings,
1288
+ packagesWithoutTypecheck,
1289
+ runtimeCodeShipped,
1290
+ sumOfCounts,
1291
+ testFailures,
1292
+ testsWithoutRunner,
1293
+ typecheckErrors,
1294
+ unformattedFiles,
1295
+ walkFindings
1296
+ ];
1297
+ var counterById = (id) => {
1298
+ const counter = COUNTERS.find((one) => one.id === id);
1299
+ if (counter === void 0) {
1300
+ throw new Error(
1301
+ `no counter implements "${id}" \u2014 known ids: ${COUNTERS.map((one) => one.id).join(", ")}`
1302
+ );
1303
+ }
1304
+ return counter;
1305
+ };
1306
+
1307
+ // src/report.ts
791
1308
  var WIDTH = 28;
792
1309
  var formatReport = ({ measurements, rewritten }) => {
793
1310
  const lines = measurements.map((one) => {
@@ -816,19 +1333,40 @@ var formatReport = ({ measurements, rewritten }) => {
816
1333
  return `${lines.join("\n")}
817
1334
  `;
818
1335
  };
1336
+ var formatProve = ({ proofs, proven }) => {
1337
+ const lines = proofs.map((one) => {
1338
+ if (one.verdict === "skipped") return ` SKIP ${one.key}: not measured by --tier ${one.tier}`;
1339
+ if (one.verdict === "serialised") return ` PROVEN ${one.key}: ${one.reason}`;
1340
+ if (one.verdict === "interleaved") {
1341
+ return ` CANNOT FAIL ${one.key}: interleaved \u2014 ${one.reason}`;
1342
+ }
1343
+ if (one.verdict === "cannot-measure") return ` CANNOT MEASURE ${one.key}: ${one.reason}`;
1344
+ if (one.verdict === "cannot-fail") return ` CANNOT FAIL ${one.key}: read 0`;
1345
+ if (one.verdict === "misread") {
1346
+ return ` MISREAD ${one.key}: read ${one.reading} where its probe planted ${one.expected}`;
1347
+ }
1348
+ return ` PROVEN ${one.key}: reads ${one.reading} on a planted finding`;
1349
+ });
1350
+ lines.push(
1351
+ "",
1352
+ proven ? "prove PASS \u2014 every counter read the finding its probe planted." : "prove FAIL \u2014 a gate that has never been seen red has not been shown to measure."
1353
+ );
1354
+ return `${lines.join("\n")}
1355
+ `;
1356
+ };
819
1357
 
820
1358
  export {
821
- CounterError,
822
- COUNTERS,
823
- counterById,
824
1359
  heavyLockPath,
825
1360
  acquireExclusive,
826
1361
  CONFIG_FILE,
827
1362
  keyOf,
828
1363
  loadConfig,
1364
+ CounterError,
829
1365
  runCommand,
830
1366
  runProve,
831
- formatProve,
832
1367
  runRatchet,
833
- formatReport
1368
+ COUNTERS,
1369
+ counterById,
1370
+ formatReport,
1371
+ formatProve
834
1372
  };