@geonosis/ratchet 2.1.0 → 2.3.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
@@ -1,5 +1,8 @@
1
1
  # @geonosis/ratchet
2
2
 
3
+ Through the front door: `geonosis ratchet` — the metapackage pins this and every other kit tool at
4
+ ONE version, and passes the exit code through unchanged.
5
+
3
6
  Debt as a number that may only shrink. It measures a repo's counters, compares each against a
4
7
  committed baseline, and fails **only** on regression — so a repo can adopt a gate the day it is
5
8
  written, with the existing debt named rather than forgiven, and nothing new can land behind it.
@@ -66,6 +69,34 @@ A counter you wrote yourself takes a `probe` beside its `run`: `{ input: (dir) =
66
69
  (dir) => string, params?, expect: number }`. `expect` is what the planted input is worth, and it is
67
70
  at least 1 — a probe that plants nothing proves nothing.
68
71
 
72
+ ### A probe proves the command, not the regex
73
+
74
+ A shipped probe plants an input in the shape the counter's OWN default reads. `archViolations`
75
+ plants a scanner that prints `✗ …` lines and exits non-zero; `sumOfCounts` plants a line its default
76
+ `match` counts. That proves the command is spawned, its output is captured and its exit code is told
77
+ apart from its findings — and it proves nothing whatever about a `match` YOUR config supplies,
78
+ because the planted input was written for the default one.
79
+
80
+ This was measured, not imagined: a consumer's mis-escaped `match` read 0 from a real scan, `--prove`
81
+ said PROVEN off the shipped plant, and the ratchet lowered the baseline — ten pieces of debt erased
82
+ with a congratulatory message (#140).
83
+
84
+ So the counters that take a reading param — `archViolations` (`match`), `sumOfCounts` (`match`),
85
+ `duplication` (`headings`), `todos` (`markers`) — require a sample from the config that supplied it:
86
+
87
+ ```jsonc
88
+ {
89
+ "counter": "archViolations",
90
+ "command": "node scripts/scan-architecture.mjs",
91
+ "match": "^VIOLATION ",
92
+ "probe": { "sample": "VIOLATION a\nVIOLATION b\nfine\n", "expect": 2 }
93
+ }
94
+ ```
95
+
96
+ Without the `probe` block the line reads `UNPROVEN`, naming the param and its value. With it, the
97
+ sample is run through YOUR match and must count `expect` — a sample your match cannot read is a
98
+ refusal that names the match, not a zero.
99
+
69
100
  ### A baseline is not proof the gate still gates
70
101
 
71
102
  The ratchet compares a number against a number. Swapping the tool that produces it — a vendored lint
@@ -196,9 +227,9 @@ captured from the binary rather than guessed at:
196
227
 
197
228
  | Runner | The line | Read as |
198
229
  | --- | --- | --- |
199
- | vitest 3.2.7 | ` Tests 1 failed \| 1 passed (2)` | 1 |
200
- | vitest 3.2.7 | ` Tests 3 passed (3)` · ` Tests 2 skipped (2)` | 0 |
201
- | vitest 3.2.7 | ` Tests no tests` (a suite that threw before collecting) | **refused** |
230
+ | vitest 3.2.7 · 4.1.11 | ` Tests 1 failed \| 1 passed (2)` | 1 |
231
+ | vitest 3.2.7 · 4.1.11 | ` Tests 3 passed (3)` · ` Tests 2 skipped (2)` | 0 |
232
+ | vitest 3.2.7 · 4.1.11 | ` Tests no tests` (a suite that threw before collecting) | **refused** |
202
233
  | bun 1.4.0 | ` 1 fail` on its own line | 1 |
203
234
  | bun 1.4.0 | ` 0 fail` | 0 |
204
235
 
@@ -1,6 +1,6 @@
1
1
  // src/core/envelope.ts
2
2
  import { mkdirSync, readFileSync, writeFileSync } from "fs";
3
- import { dirname, join } from "path";
3
+ import { dirname, join, resolve } from "path";
4
4
  import { fileURLToPath } from "url";
5
5
  var ENVELOPES_DIR = ".geonosis/envelopes";
6
6
  var envelopePath = (root, tool) => join(root, ENVELOPES_DIR, `${tool}.json`);
@@ -12,6 +12,14 @@ var UnbalancedEnvelope = class extends Error {
12
12
  };
13
13
  var isCount = (value) => Number.isSafeInteger(value) && value >= 0;
14
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 FORBIDDEN_ROOT = "GEONOSIS_ENVELOPES_FORBIDDEN_ROOT";
16
+ var refuseForbiddenRoot = (root) => {
17
+ const forbidden = process.env[FORBIDDEN_ROOT];
18
+ if (forbidden === void 0 || resolve(forbidden) !== resolve(root)) return;
19
+ throw new UnbalancedEnvelope(
20
+ `${root} is off limits to envelope writers in this process (${FORBIDDEN_ROOT}) \u2014 a run that writes one into a shared root races every other run reading it, and leaves a file the next one takes for real. Point this at a scratch root of its own: tooling/scratch-dir.ts.`
21
+ );
22
+ };
15
23
  var writeEnvelope = ({
16
24
  envelope,
17
25
  next,
@@ -35,6 +43,7 @@ var writeEnvelope = ({
35
43
  if (envelope.considered !== envelope.read + envelope.refused.length + envelope.excused.length) {
36
44
  throw new UnbalancedEnvelope(unbalancedMessage(envelope, next));
37
45
  }
46
+ refuseForbiddenRoot(root);
38
47
  const at = envelopePath(root, envelope.tool);
39
48
  mkdirSync(dirname(at), { recursive: true });
40
49
  writeFileSync(at, `${JSON.stringify(envelope, void 0, 2)}
@@ -65,8 +74,14 @@ var versionOf = (moduleUrl) => {
65
74
  import { randomUUID } from "crypto";
66
75
  import { linkSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
67
76
  import { homedir } from "os";
68
- import { dirname as dirname2, join as join2 } from "path";
69
- var heavyLockPath = () => process.env.GEONOSIS_HEAVY_LOCK ?? join2(homedir(), ".cache", "geonosis", "heavy.lock");
77
+ import { dirname as dirname2, isAbsolute, join as join2 } from "path";
78
+ var heavyLockPath = () => {
79
+ const named = process.env.GEONOSIS_HEAVY_LOCK;
80
+ if (named !== void 0 && named !== "") return named;
81
+ const declared = process.env.XDG_CACHE_HOME;
82
+ const cache = declared !== void 0 && isAbsolute(declared) ? declared : join2(homedir(), ".cache");
83
+ return join2(cache, "geonosis", "heavy.lock");
84
+ };
70
85
  var sleep = (ms) => new Promise((done) => setTimeout(done, ms));
71
86
  var holderOf = (path) => {
72
87
  try {
@@ -153,11 +168,11 @@ var acquireExclusive = async ({
153
168
 
154
169
  // src/core/config.ts
155
170
  import { existsSync, readFileSync as readFileSync3 } from "fs";
156
- import { resolve } from "path";
171
+ import { resolve as resolve2 } from "path";
157
172
  var CONFIG_FILE = "geonosis.ratchet.json";
158
173
  var keyOf = (entry) => entry.key ?? entry.counter;
159
174
  var loadConfig = (cwd) => {
160
- const path = resolve(cwd, CONFIG_FILE);
175
+ const path = resolve2(cwd, CONFIG_FILE);
161
176
  if (!existsSync(path)) {
162
177
  throw new Error(`no ${CONFIG_FILE} in ${cwd} \u2014 the ratchet has nothing to count`);
163
178
  }
@@ -189,7 +204,7 @@ var loadConfig = (cwd) => {
189
204
  };
190
205
  var resolveBaseline = (cwd) => {
191
206
  try {
192
- const parsed = JSON.parse(readFileSync3(resolve(cwd, CONFIG_FILE), "utf8"));
207
+ const parsed = JSON.parse(readFileSync3(resolve2(cwd, CONFIG_FILE), "utf8"));
193
208
  return typeof parsed.baseline === "string" ? parsed.baseline : "gate-baseline.json";
194
209
  } catch {
195
210
  return "gate-baseline.json";
@@ -237,11 +252,11 @@ ${output.trim()}`
237
252
  // src/core/prove.ts
238
253
  import { existsSync as existsSync2, mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
239
254
  import { tmpdir } from "os";
240
- import { delimiter, dirname as dirname4, join as join3, resolve as resolve3 } from "path";
255
+ import { delimiter, dirname as dirname4, join as join3, resolve as resolve4 } from "path";
241
256
 
242
257
  // src/core/exclusive.ts
243
258
  import { spawn } from "child_process";
244
- import { dirname as dirname3, resolve as resolve2 } from "path";
259
+ import { dirname as dirname3, resolve as resolve3 } from "path";
245
260
  var MARK = /^exclusive-hold (start|end) (\d+)$/gm;
246
261
  var KEY = "exclusive";
247
262
  var spanOf = (output) => {
@@ -256,7 +271,7 @@ var hold = (cli, holdMs, lockPath) => new Promise((done) => {
256
271
  process.execPath,
257
272
  [cli, "--exclusive", "--exclusive-timeout", "60", "--hold", String(holdMs)],
258
273
  {
259
- cwd: dirname3(resolve2(cli)),
274
+ cwd: dirname3(resolve3(cli)),
260
275
  env: { ...process.env, GEONOSIS_HEAVY_LOCK: lockPath },
261
276
  stdio: ["ignore", "pipe", "pipe"]
262
277
  }
@@ -306,7 +321,7 @@ ${runs.map((one) => `exit ${String(one.code)}: ${one.output.trim().slice(-300)}`
306
321
  // src/core/prove.ts
307
322
  var toolPath = (cwd) => {
308
323
  const dirs = [];
309
- let dir = resolve3(cwd);
324
+ let dir = resolve4(cwd);
310
325
  for (; ; ) {
311
326
  const bin = join3(dir, "node_modules", ".bin");
312
327
  if (existsSync2(bin)) dirs.push(bin);
@@ -433,7 +448,7 @@ var runProve = async ({
433
448
 
434
449
  // src/core/ratchet.ts
435
450
  import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
436
- import { resolve as resolve4 } from "path";
451
+ import { resolve as resolve5 } from "path";
437
452
  var EVIDENCE_LINES = 10;
438
453
  var recorded = (counter, params, run) => {
439
454
  let output = "";
@@ -480,7 +495,7 @@ var runRatchet = async ({
480
495
  tier
481
496
  }) => {
482
497
  const config = loadConfig(cwd);
483
- const baselinePath = resolve4(cwd, config.baseline);
498
+ const baselinePath = resolve5(cwd, config.baseline);
484
499
  if (!existsSync3(baselinePath)) {
485
500
  throw new Error(`no ${config.baseline} in ${cwd} \u2014 nothing to ratchet against`);
486
501
  }
@@ -597,6 +612,10 @@ var scannerProbe = {
597
612
  var archViolations = {
598
613
  id: "archViolations",
599
614
  probe: { ...scannerProbe, expect: 2 },
615
+ // #30: the shipped probe runs a scanner that prints `✗`, so it proves the COMMAND and never a
616
+ // `match` a repo configured for its own scanner's shape. Declared here, a customised match is
617
+ // UNPROVEN until the config declares a sample of its own.
618
+ readingParams: ["match"],
600
619
  run: async ({ params, run }) => {
601
620
  const command = stringParam("archViolations", params, "command");
602
621
  const match = new RegExp(stringParam("archViolations", params, "match", "^\u2717"));
@@ -649,7 +668,7 @@ var bundleBytes = {
649
668
 
650
669
  // src/counters/ci.ts
651
670
  import { readdirSync, readFileSync as readFileSync5 } from "fs";
652
- import { join as join5, resolve as resolve5 } from "path";
671
+ import { join as join5, resolve as resolve6 } from "path";
653
672
  var DISABLED = /\bif:[ \t]*false\b/;
654
673
  var WORKFLOW = /\.ya?ml$/;
655
674
  var disabledCiJobs = {
@@ -664,7 +683,7 @@ var disabledCiJobs = {
664
683
  },
665
684
  run: async ({ cwd, params }) => {
666
685
  const relative = stringParam("disabledCiJobs", params, "dir", ".github/workflows");
667
- const dir = resolve5(cwd, relative);
686
+ const dir = resolve6(cwd, relative);
668
687
  let files;
669
688
  try {
670
689
  files = readdirSync(dir, { withFileTypes: true }).filter((entry) => WORKFLOW.test(entry.name)).map((entry) => join5(dir, entry.name));
@@ -722,12 +741,40 @@ var excusesIn = (params) => {
722
741
  typeof declared === "object" && declared !== null && !Array.isArray(declared) ? Object.keys(declared) : []
723
742
  );
724
743
  };
744
+ var HEADING_LINE = /^([A-Z][A-Za-z ]+?) \((\d+)\)\s*$/;
745
+ var UNRESOLVED = "Unresolved imports";
746
+ var KNIP_DEFAULT = "npx knip --reporter compact";
747
+ var knipEvidence = ({
748
+ output,
749
+ params
750
+ }) => {
751
+ const command = typeof params["command"] === "string" ? params["command"] : KNIP_DEFAULT;
752
+ const said = [];
753
+ let heading;
754
+ for (const line of output.split("\n")) {
755
+ const found = HEADING_LINE.exec(line);
756
+ if (found !== null) {
757
+ heading = found[1];
758
+ said.push(line.trim());
759
+ continue;
760
+ }
761
+ if (heading === void 0 || line.trim() === "") continue;
762
+ said.push(` ${line.trim()}`);
763
+ if (heading === UNRESOLVED) {
764
+ said.push(
765
+ ` resolved by \`${command}\` on ${process.version} \u2014 an unresolved import is a fact about the resolver, and this class read one number on CI and another on a laptop over one tree`
766
+ );
767
+ }
768
+ }
769
+ return said;
770
+ };
725
771
  var knipIssues = {
772
+ evidence: knipEvidence,
726
773
  id: "knipIssues",
727
774
  probe: { ...captured("Unused files (1)\nsrc/gone.ts\n"), expect: 1 },
728
775
  readingParams: ["headings"],
729
776
  run: async ({ params, run }) => {
730
- const command = stringParam("knipIssues", params, "command", "npx knip --reporter compact");
777
+ const command = stringParam("knipIssues", params, "command", KNIP_DEFAULT);
731
778
  const printed = headingsIn(run(command).output);
732
779
  const counted = stringsParam(params, "headings", [...printed.keys()]);
733
780
  const excused = excusesIn(params);
@@ -753,9 +800,119 @@ var boundaryIssues = {
753
800
  }
754
801
  };
755
802
 
803
+ // src/counters/dx.ts
804
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
805
+ import { resolve as resolve7 } from "path";
806
+ var DEFAULT_BUMP_REPORT = ".geonosis/bump-report.json";
807
+ var bumpFile = (id, cwd, params) => {
808
+ const relative = stringParam(id, params, "report", DEFAULT_BUMP_REPORT);
809
+ const path = resolve7(cwd, relative);
810
+ if (!existsSync4(path)) {
811
+ throw new CounterError(
812
+ id,
813
+ `no bump report at ${relative} \u2014 this repo has never run \`geonosis update\`, and a zero here would read as a bump that took no time`
814
+ );
815
+ }
816
+ try {
817
+ return JSON.parse(readFileSync6(path, "utf8"));
818
+ } catch (error) {
819
+ throw new CounterError(id, `${relative} is not readable JSON: ${error.message}`);
820
+ }
821
+ };
822
+ var REPORT_WITH_A_GREEN_DOCTOR = JSON.stringify({
823
+ at: "2026-09-03T09:00:00.000Z",
824
+ durationMs: 12e3,
825
+ ok: true,
826
+ steps: [
827
+ { detail: "", durationMs: 5e3, ok: true, ran: true, step: "install" },
828
+ { detail: "", durationMs: 7e3, ok: true, ran: true, step: "doctor" }
829
+ ],
830
+ to: "2.2.0"
831
+ });
832
+ var bumpDurationMs = {
833
+ id: "bumpDurationMs",
834
+ probe: {
835
+ expect: 12e3,
836
+ input: (dir) => plant(dir, DEFAULT_BUMP_REPORT, REPORT_WITH_A_GREEN_DOCTOR)
837
+ },
838
+ run: async ({ cwd, params }) => {
839
+ const report = bumpFile("bumpDurationMs", cwd, params);
840
+ if (typeof report.durationMs !== "number") {
841
+ throw new CounterError(
842
+ "bumpDurationMs",
843
+ "the bump report carries no durationMs \u2014 it was written by a build that did not time itself"
844
+ );
845
+ }
846
+ return report.durationMs;
847
+ },
848
+ tolerates: true
849
+ };
850
+ var timeToGreenDoctorMs = {
851
+ id: "timeToGreenDoctorMs",
852
+ probe: {
853
+ expect: 12e3,
854
+ input: (dir) => plant(dir, DEFAULT_BUMP_REPORT, REPORT_WITH_A_GREEN_DOCTOR)
855
+ },
856
+ run: async ({ cwd, params }) => {
857
+ const steps = bumpFile("timeToGreenDoctorMs", cwd, params).steps ?? [];
858
+ const at = steps.findIndex((one) => one.step === "doctor");
859
+ if (at === -1) {
860
+ throw new CounterError(
861
+ "timeToGreenDoctorMs",
862
+ "the bump report names no doctor step, so nothing in it says when this repo first read green"
863
+ );
864
+ }
865
+ if (steps[at]?.ok !== true) {
866
+ throw new CounterError(
867
+ "timeToGreenDoctorMs",
868
+ "the doctor step of the last bump was not green \u2014 there is no time to a green that never came"
869
+ );
870
+ }
871
+ return steps.slice(0, at + 1).reduce((total2, one) => total2 + (typeof one.durationMs === "number" ? one.durationMs : 0), 0);
872
+ },
873
+ tolerates: true
874
+ };
875
+ var SEPARATOR = /^\|[\s:|-]*\|\s*$/;
876
+ var ROW = /^\|.*\|\s*$/;
877
+ var cellsOf = (line) => line.replace(/^\|/, "").replace(/\|\s*$/, "").split("|").map((one) => one.trim());
878
+ var openBacklogRows = {
879
+ id: "openBacklogRows",
880
+ probe: {
881
+ expect: 2,
882
+ input: (dir) => plant(
883
+ dir,
884
+ "backlog.md",
885
+ [
886
+ "| # | Finding | Status |",
887
+ "|---|---|---|",
888
+ "| 1 | closed | **DONE** |",
889
+ "| 2 | still here | open |",
890
+ "| 3 | also here | open \u2014 next |",
891
+ ""
892
+ ].join("\n")
893
+ ),
894
+ params: { path: "backlog.md" }
895
+ },
896
+ run: async ({ cwd, params }) => {
897
+ const relative = stringParam("openBacklogRows", params, "path");
898
+ const done = stringParam("openBacklogRows", params, "done", "DONE");
899
+ const at = resolve7(cwd, relative);
900
+ if (!existsSync4(at)) {
901
+ throw new CounterError("openBacklogRows", `no register at ${relative}`);
902
+ }
903
+ const lines = readFileSync6(at, "utf8").split("\n");
904
+ return lines.filter((line, index) => {
905
+ if (!ROW.test(line) || SEPARATOR.test(line)) return false;
906
+ if (SEPARATOR.test(lines[index + 1] ?? "")) return false;
907
+ const status = cellsOf(line).at(-1) ?? "";
908
+ return status !== "" && !status.includes(done);
909
+ }).length;
910
+ }
911
+ };
912
+
756
913
  // src/counters/format.ts
757
- import { existsSync as existsSync4 } from "fs";
758
- import { resolve as resolve6 } from "path";
914
+ import { existsSync as existsSync5 } from "fs";
915
+ import { resolve as resolve8 } from "path";
759
916
  var unformattedFiles = {
760
917
  id: "unformattedFiles",
761
918
  probe: {
@@ -770,13 +927,13 @@ var unformattedFiles = {
770
927
  "command",
771
928
  "npx oxfmt --config .oxfmtrc.json --list-different ."
772
929
  );
773
- return run(command).output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && existsSync4(resolve6(cwd, line))).length;
930
+ return run(command).output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && existsSync5(resolve8(cwd, line))).length;
774
931
  }
775
932
  };
776
933
 
777
934
  // src/counters/gate-report.ts
778
- import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
779
- import { resolve as resolve7 } from "path";
935
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
936
+ import { resolve as resolve9 } from "path";
780
937
  var DEFAULT_REPORT = ".geonosis/gate-report.json";
781
938
  var fastTierMs = {
782
939
  id: "fastTierMs",
@@ -799,9 +956,9 @@ var fastTierMs = {
799
956
  const wanted = stringParam("fastTierMs", params, "tier", "fast");
800
957
  const own = `.geonosis/gate-report.${wanted}.json`;
801
958
  const asked = stringParam("fastTierMs", params, "report", "");
802
- const relative = asked !== "" ? asked : existsSync5(resolve7(cwd, own)) ? own : DEFAULT_REPORT;
803
- const path = resolve7(cwd, relative);
804
- if (!existsSync5(path)) {
959
+ const relative = asked !== "" ? asked : existsSync6(resolve9(cwd, own)) ? own : DEFAULT_REPORT;
960
+ const path = resolve9(cwd, relative);
961
+ if (!existsSync6(path)) {
805
962
  throw new CounterError(
806
963
  "fastTierMs",
807
964
  `no gate report at ${relative} \u2014 run \`geonosis-verify ${wanted}\` before measuring it`
@@ -809,7 +966,7 @@ var fastTierMs = {
809
966
  }
810
967
  let report;
811
968
  try {
812
- report = JSON.parse(readFileSync6(path, "utf8"));
969
+ report = JSON.parse(readFileSync7(path, "utf8"));
813
970
  } catch (error) {
814
971
  throw new CounterError(
815
972
  "fastTierMs",
@@ -831,8 +988,8 @@ var fastTierMs = {
831
988
  };
832
989
 
833
990
  // src/counters/law.ts
834
- import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
835
- import { resolve as resolve8 } from "path";
991
+ import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
992
+ import { resolve as resolve10 } from "path";
836
993
  var lawLineCount = {
837
994
  id: "lawLineCount",
838
995
  probe: {
@@ -842,15 +999,15 @@ var lawLineCount = {
842
999
  },
843
1000
  run: async ({ cwd, params }) => {
844
1001
  const relative = stringParam("lawLineCount", params, "path", "CLAUDE.md");
845
- const path = resolve8(cwd, relative);
846
- if (!existsSync6(path)) throw new CounterError("lawLineCount", `no law file at ${relative}`);
847
- return readFileSync7(path, "utf8").replace(/\n$/, "").split("\n").length;
1002
+ const path = resolve10(cwd, relative);
1003
+ if (!existsSync7(path)) throw new CounterError("lawLineCount", `no law file at ${relative}`);
1004
+ return readFileSync8(path, "utf8").replace(/\n$/, "").split("\n").length;
848
1005
  }
849
1006
  };
850
1007
 
851
1008
  // src/counters/oxlint.ts
852
- import { existsSync as existsSync7, readFileSync as readFileSync8, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
853
- import { resolve as resolve9 } from "path";
1009
+ import { existsSync as existsSync8, readFileSync as readFileSync9, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
1010
+ import { resolve as resolve11 } from "path";
854
1011
  var DEFAULT_COMMAND = "npx oxlint --format=unix --config .oxlintrc.json .";
855
1012
  var PROBE_COMMAND = "oxlint --format=unix --config .oxlintrc.json .";
856
1013
  var oxlintProbe = (severity, rule, source) => ({
@@ -1013,26 +1170,26 @@ var oxlintRule = {
1013
1170
  const config = typeof params.config === "string" ? params.config : "";
1014
1171
  const expect = expectedFormat("oxlintRule", params);
1015
1172
  if (config === "") return countRule(run(command), rule, expect);
1016
- const source = resolve9(cwd, config);
1017
- if (!existsSync7(source)) throw new CounterError("oxlintRule", `no config at ${config}`);
1173
+ const source = resolve11(cwd, config);
1174
+ if (!existsSync8(source)) throw new CounterError("oxlintRule", `no config at ${config}`);
1018
1175
  const strictName = `.oxlintrc.ratchet-${key}.json`;
1019
- const strict = readFileSync8(source, "utf8").replace(
1176
+ const strict = readFileSync9(source, "utf8").replace(
1020
1177
  new RegExp(`("[^"]*${escapeForRegex(rule)}"\\s*:\\s*\\[?\\s*)"(warn|off)"`),
1021
1178
  '$1"error"'
1022
1179
  );
1023
- writeFileSync6(resolve9(cwd, strictName), strict);
1180
+ writeFileSync6(resolve11(cwd, strictName), strict);
1024
1181
  try {
1025
1182
  return countRule(run(command.replace("{config}", strictName)), rule, expect);
1026
1183
  } finally {
1027
- rmSync3(resolve9(cwd, strictName), { force: true });
1184
+ rmSync3(resolve11(cwd, strictName), { force: true });
1028
1185
  }
1029
1186
  }
1030
1187
  };
1031
1188
 
1032
1189
  // src/counters/probes.ts
1033
- import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
1190
+ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
1034
1191
  import { createRequire } from "module";
1035
- import { join as join6, resolve as resolve10 } from "path";
1192
+ import { join as join6, resolve as resolve12 } from "path";
1036
1193
  import { pathToFileURL } from "url";
1037
1194
  var ID = "probelessRules";
1038
1195
  var OFF = /* @__PURE__ */ new Set([0, "0", "allow", "off", false]);
@@ -1110,11 +1267,11 @@ var probelessRules = {
1110
1267
  },
1111
1268
  run: async ({ cwd, params }) => {
1112
1269
  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}`);
1270
+ const path = resolve12(cwd, relative);
1271
+ if (!existsSync9(path)) throw new CounterError(ID, `no oxlint config at ${relative}`);
1115
1272
  let config;
1116
1273
  try {
1117
- config = JSON.parse(readFileSync9(path, "utf8"));
1274
+ config = JSON.parse(readFileSync10(path, "utf8"));
1118
1275
  } catch (error) {
1119
1276
  throw new CounterError(ID, `${relative} does not parse: ${error.message}`);
1120
1277
  }
@@ -1151,8 +1308,8 @@ import { readdirSync as readdirSync3 } from "fs";
1151
1308
  import { join as join8 } from "path";
1152
1309
 
1153
1310
  // src/counters/workspace.ts
1154
- import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync10 } from "fs";
1155
- import { join as join7, resolve as resolve11 } from "path";
1311
+ import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync11 } from "fs";
1312
+ import { join as join7, resolve as resolve13 } from "path";
1156
1313
  var SKIP = /^(node_modules|\.)/;
1157
1314
  var childDirs = (dir) => {
1158
1315
  try {
@@ -1166,14 +1323,14 @@ var expand = (root, pattern) => {
1166
1323
  const segments = pattern.split("/").filter((one) => one !== "" && one !== ".");
1167
1324
  let dirs = [root];
1168
1325
  for (const segment of segments) {
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));
1326
+ dirs = segment === "*" ? dirs.flatMap((dir) => childDirs(dir)) : segment === "**" ? dirs.flatMap((dir) => descendants(dir, 3)) : dirs.map((dir) => join7(dir, segment)).filter((dir) => existsSync10(dir));
1170
1327
  }
1171
1328
  return dirs;
1172
1329
  };
1173
1330
  var QUOTED = /^['"]|['"]$/g;
1174
1331
  var cleaned = (value) => value.replace(/#.*$/, "").trim().replaceAll(QUOTED, "");
1175
1332
  var pnpmPatterns = (path) => {
1176
- const lines = readFileSync10(path, "utf8").split("\n");
1333
+ const lines = readFileSync11(path, "utf8").split("\n");
1177
1334
  const at = lines.findIndex((line) => line.startsWith("packages:"));
1178
1335
  if (at === -1) return [];
1179
1336
  const inline = lines[at]?.slice("packages:".length).trim() ?? "";
@@ -1192,31 +1349,31 @@ var pnpmPatterns = (path) => {
1192
1349
  return patterns;
1193
1350
  };
1194
1351
  var npmPatterns = (path) => {
1195
- const parsed = JSON.parse(readFileSync10(path, "utf8"));
1352
+ const parsed = JSON.parse(readFileSync11(path, "utf8"));
1196
1353
  const declared = Array.isArray(parsed.workspaces) ? parsed.workspaces : parsed.workspaces?.packages ?? [];
1197
1354
  return declared.filter((one) => typeof one === "string");
1198
1355
  };
1199
1356
  var nameOf = (dir) => {
1200
1357
  const manifest = join7(dir, "package.json");
1201
- if (!existsSync9(manifest)) return void 0;
1358
+ if (!existsSync10(manifest)) return void 0;
1202
1359
  try {
1203
- const { name } = JSON.parse(readFileSync10(manifest, "utf8"));
1360
+ const { name } = JSON.parse(readFileSync11(manifest, "utf8"));
1204
1361
  return typeof name === "string" && name !== "" ? name : void 0;
1205
1362
  } catch {
1206
1363
  return void 0;
1207
1364
  }
1208
1365
  };
1209
1366
  var workspaceDirs = (cwd) => {
1210
- const root = resolve11(cwd);
1367
+ const root = resolve13(cwd);
1211
1368
  const pnpm = join7(root, "pnpm-workspace.yaml");
1212
1369
  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")));
1370
+ const patterns = existsSync10(pnpm) ? pnpmPatterns(pnpm) : existsSync10(manifest) ? npmPatterns(manifest) : [];
1371
+ const dirs = patterns.filter((pattern) => !pattern.startsWith("!")).flatMap((pattern) => expand(root, pattern)).filter((dir) => existsSync10(join7(dir, "package.json")));
1215
1372
  return [...new Set(dirs)];
1216
1373
  };
1217
1374
  var manifestOf = (dir) => {
1218
1375
  try {
1219
- const parsed = JSON.parse(readFileSync10(join7(dir, "package.json"), "utf8"));
1376
+ const parsed = JSON.parse(readFileSync11(join7(dir, "package.json"), "utf8"));
1220
1377
  return typeof parsed === "object" && parsed !== null ? parsed : void 0;
1221
1378
  } catch {
1222
1379
  return void 0;
@@ -1283,6 +1440,56 @@ var packagesWithoutTypecheck = {
1283
1440
  }
1284
1441
  };
1285
1442
 
1443
+ // src/counters/seams.ts
1444
+ import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync12 } from "fs";
1445
+ import { join as join9, resolve as resolve14 } from "path";
1446
+ var CONFIG = "geonosis.json";
1447
+ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".turbo", "coverage", "dist", "node_modules"]);
1448
+ var asPattern = (glob) => new RegExp(
1449
+ `^${glob.split("/").map(
1450
+ (part) => part === "**" ? ".*" : part.replaceAll(/[.+^${}()|[\]\\]/g, String.raw`\$&`).replaceAll("*", "[^/]*")
1451
+ ).join("/").replaceAll(".*/", "(?:.*/)?")}$`
1452
+ );
1453
+ var filesUnder = (root, at = root) => readdirSync4(at, { withFileTypes: true }).flatMap((entry) => {
1454
+ if (SKIP_DIRS.has(entry.name)) return [];
1455
+ const full = join9(at, entry.name);
1456
+ if (entry.isDirectory()) return filesUnder(root, full);
1457
+ return entry.isFile() ? [full.slice(root.length + 1)] : [];
1458
+ });
1459
+ var seamGlobsIn = (root) => {
1460
+ const at = join9(root, CONFIG);
1461
+ if (!existsSync11(at)) return [];
1462
+ try {
1463
+ const seams = JSON.parse(readFileSync12(at, "utf8")).adoption?.seams;
1464
+ return Array.isArray(seams) ? seams.filter((one) => typeof one === "string") : [];
1465
+ } catch {
1466
+ return [];
1467
+ }
1468
+ };
1469
+ var linesIn = (path) => {
1470
+ const body = readFileSync12(path, "utf8");
1471
+ return body === "" ? 0 : body.replace(/\n$/, "").split("\n").length;
1472
+ };
1473
+ var adoptionSeamLines = {
1474
+ id: "adoptionSeamLines",
1475
+ probe: {
1476
+ expect: 3,
1477
+ input: (dir) => {
1478
+ plant(dir, CONFIG, `${JSON.stringify({ adoption: { seams: ["src/**/*.ts"] } })}
1479
+ `);
1480
+ plant(dir, "src/adapter.ts", "one\ntwo\nthree\n");
1481
+ plant(dir, "src/not-a-seam.md", "four\nfive\n");
1482
+ },
1483
+ params: {}
1484
+ },
1485
+ run: async ({ cwd }) => {
1486
+ const root = resolve14(cwd);
1487
+ const globs = seamGlobsIn(root).map(asPattern);
1488
+ if (globs.length === 0) return 0;
1489
+ return filesUnder(root).filter((one) => globs.some((pattern) => pattern.test(one))).reduce((total2, one) => total2 + linesIn(join9(root, one)), 0);
1490
+ }
1491
+ };
1492
+
1286
1493
  // src/counters/sum-of-counts.ts
1287
1494
  var sumOfCounts = {
1288
1495
  id: "sumOfCounts",
@@ -1305,15 +1512,15 @@ var sumOfCounts = {
1305
1512
  };
1306
1513
 
1307
1514
  // src/counters/suppressions.ts
1308
- import { readdirSync as readdirSync4, readFileSync as readFileSync11 } from "fs";
1309
- import { join as join9, resolve as resolve12 } from "path";
1515
+ import { readdirSync as readdirSync5, readFileSync as readFileSync13 } from "fs";
1516
+ import { join as join10, resolve as resolve15 } from "path";
1310
1517
  var CODE_FILE = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts|vue|svelte|astro)$/;
1311
1518
  var SKIP3 = /^(?:node_modules|dist|coverage|\.[^.]|__fixtures__)/;
1312
1519
  var SUPPRESSION = /(?:eslint|oxlint|biome)-(?:disable|ignore)(?:-next-line|-line)?|@ts-expect-error|@ts-ignore/g;
1313
1520
  var countIn = (dir, depth = 12) => {
1314
1521
  let entries;
1315
1522
  try {
1316
- entries = readdirSync4(dir, { withFileTypes: true });
1523
+ entries = readdirSync5(dir, { withFileTypes: true });
1317
1524
  } catch {
1318
1525
  return 0;
1319
1526
  }
@@ -1321,12 +1528,12 @@ var countIn = (dir, depth = 12) => {
1321
1528
  for (const entry of entries) {
1322
1529
  if (SKIP3.test(entry.name)) continue;
1323
1530
  if (entry.isDirectory()) {
1324
- if (depth > 0) found += countIn(join9(dir, entry.name), depth - 1);
1531
+ if (depth > 0) found += countIn(join10(dir, entry.name), depth - 1);
1325
1532
  continue;
1326
1533
  }
1327
1534
  if (!CODE_FILE.test(entry.name)) continue;
1328
1535
  try {
1329
- found += readFileSync11(join9(dir, entry.name), "utf8").match(SUPPRESSION)?.length ?? 0;
1536
+ found += readFileSync13(join10(dir, entry.name), "utf8").match(SUPPRESSION)?.length ?? 0;
1330
1537
  } catch {
1331
1538
  }
1332
1539
  }
@@ -1340,14 +1547,107 @@ var suppressionCount = {
1340
1547
  },
1341
1548
  run: async ({ cwd, params }) => {
1342
1549
  const roots = stringsParam(params, "roots", ["."]);
1343
- return roots.reduce((sum, root) => sum + countIn(resolve12(cwd, root)), 0);
1550
+ return roots.reduce((sum, root) => sum + countIn(resolve15(cwd, root)), 0);
1551
+ }
1552
+ };
1553
+
1554
+ // src/counters/surface.ts
1555
+ import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
1556
+ import { join as join11, resolve as resolve16 } from "path";
1557
+ var DECLARED = /^export\s+(?:declare\s+)?(?:abstract\s+)?(?:const|function|class|type|interface|enum|let|var)\s+([$\w]+)/;
1558
+ var BRACED = /^export\s+(?:type\s+)?\{([^}]*)\}(?:\s+from\s+(['"])(.+?)\2)?/;
1559
+ var nameOf2 = (spec) => {
1560
+ const named = spec.trim().replace(/^type\s+/, "");
1561
+ if (named === "") return void 0;
1562
+ const parts = named.split(/\s+as\s+/);
1563
+ return (parts.at(-1) ?? "").trim() || void 0;
1564
+ };
1565
+ var exportedSurfaceIn = (source) => {
1566
+ const found = /* @__PURE__ */ new Map();
1567
+ for (const line of source.split("\n")) {
1568
+ const declared = DECLARED.exec(line);
1569
+ if (declared !== null) {
1570
+ found.set(declared[1] ?? "", line.trim());
1571
+ continue;
1572
+ }
1573
+ const braced = BRACED.exec(line);
1574
+ if (braced === null) continue;
1575
+ const from = braced[3];
1576
+ for (const spec of (braced[1] ?? "").split(",")) {
1577
+ const name = nameOf2(spec);
1578
+ if (name === void 0) continue;
1579
+ found.set(
1580
+ name,
1581
+ from === void 0 ? `export { ${name} }` : `export { ${name} } from '${from}'`
1582
+ );
1583
+ }
1584
+ }
1585
+ return found;
1586
+ };
1587
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1588
+ var declarationEntriesOf = (dir) => {
1589
+ const at = join11(dir, "package.json");
1590
+ if (!existsSync12(at)) {
1591
+ throw new CounterError("floorSurface", `no package.json in ${dir} \u2014 that is not a package`);
1592
+ }
1593
+ const manifest = JSON.parse(readFileSync14(at, "utf8"));
1594
+ const found = [];
1595
+ const walk = (value) => {
1596
+ if (typeof value === "string" && value.endsWith(".d.ts")) found.push(value);
1597
+ if (isRecord(value)) for (const one of Object.values(value)) walk(one);
1598
+ };
1599
+ walk({ exports: manifest["exports"], types: manifest["types"] });
1600
+ return [...new Set(found)];
1601
+ };
1602
+ var floorSurface = {
1603
+ evidence: ({ params }) => [`the doors declared by ${String(params["dir"] ?? "this package")}`],
1604
+ id: "floorSurface",
1605
+ probe: {
1606
+ expect: 3,
1607
+ input: (dir) => {
1608
+ plant(
1609
+ dir,
1610
+ "floor/package.json",
1611
+ `${JSON.stringify({ exports: { ".": { types: "./dist/index.d.ts" } }, name: "floor" })}
1612
+ `
1613
+ );
1614
+ plant(
1615
+ dir,
1616
+ "floor/dist/index.d.ts",
1617
+ "export declare const open: () => void;\nexport type Handle = { id: string };\nexport { c as close } from './chunk.js';\ndeclare const hidden: number;\n"
1618
+ );
1619
+ },
1620
+ params: { dir: "floor" }
1621
+ },
1622
+ run: async ({ cwd, params }) => {
1623
+ const relative = stringParam("floorSurface", params, "dir");
1624
+ const dir = resolve16(cwd, relative);
1625
+ const entries = declarationEntriesOf(dir);
1626
+ if (entries.length === 0) {
1627
+ throw new CounterError(
1628
+ "floorSurface",
1629
+ `${relative} promises no .d.ts in its exports map \u2014 a package whose surface cannot be read is not a package this can hold`
1630
+ );
1631
+ }
1632
+ const names = /* @__PURE__ */ new Set();
1633
+ for (const entry of entries) {
1634
+ const at = resolve16(dir, entry);
1635
+ if (!existsSync12(at)) {
1636
+ throw new CounterError(
1637
+ "floorSurface",
1638
+ `${relative} promises ${entry} and it is not there \u2014 build before measuring, or the surface reads as empty`
1639
+ );
1640
+ }
1641
+ for (const name of exportedSurfaceIn(readFileSync14(at, "utf8")).keys()) names.add(name);
1642
+ }
1643
+ return names.size;
1344
1644
  }
1345
1645
  };
1346
1646
 
1347
1647
  // src/counters/tests.ts
1348
- import { existsSync as existsSync10, mkdtempSync as mkdtempSync2, readFileSync as readFileSync12, rmSync as rmSync4 } from "fs";
1648
+ import { existsSync as existsSync13, mkdtempSync as mkdtempSync2, readFileSync as readFileSync15, rmSync as rmSync4 } from "fs";
1349
1649
  import { tmpdir as tmpdir2 } from "os";
1350
- import { join as join10, resolve as resolve13 } from "path";
1650
+ import { join as join12, resolve as resolve17 } from "path";
1351
1651
  var COUNTER = "testFailures";
1352
1652
  var VITEST_LINE = /^\s*Tests {2,}(.+?)\s*$/;
1353
1653
  var VITEST_TOTAL = /\(\d+\)$/;
@@ -1379,7 +1679,7 @@ ${output.trim().slice(-500)}`
1379
1679
  );
1380
1680
  };
1381
1681
  var fromReport = (path) => {
1382
- if (!existsSync10(path)) {
1682
+ if (!existsSync13(path)) {
1383
1683
  throw new CounterError(
1384
1684
  COUNTER,
1385
1685
  `the runner wrote no report at ${path} \u2014 a crash before the reporter is not a pass`
@@ -1387,7 +1687,7 @@ var fromReport = (path) => {
1387
1687
  }
1388
1688
  let report;
1389
1689
  try {
1390
- report = JSON.parse(readFileSync12(path, "utf8"));
1690
+ report = JSON.parse(readFileSync15(path, "utf8"));
1391
1691
  } catch (error) {
1392
1692
  throw new CounterError(
1393
1693
  COUNTER,
@@ -1411,14 +1711,14 @@ var fromReport = (path) => {
1411
1711
  };
1412
1712
  var reportPathFor = (cwd, params, command) => {
1413
1713
  const named = params.reportPath;
1414
- if (typeof named === "string" && named !== "") return { own: false, path: resolve13(cwd, named) };
1714
+ if (typeof named === "string" && named !== "") return { own: false, path: resolve17(cwd, named) };
1415
1715
  if (!command.includes(PLACEHOLDER)) {
1416
1716
  throw new CounterError(
1417
1717
  COUNTER,
1418
1718
  `report: "${VITEST_JSON}" needs somewhere to put the report \u2014 write ${PLACEHOLDER} into the command (--outputFile=${PLACEHOLDER}) or give the entry a "reportPath"`
1419
1719
  );
1420
1720
  }
1421
- return { own: true, path: join10(mkdtempSync2(join10(tmpdir2(), "geonosis-report-")), "report.json") };
1721
+ return { own: true, path: join12(mkdtempSync2(join12(tmpdir2(), "geonosis-report-")), "report.json") };
1422
1722
  };
1423
1723
  var testFailures = {
1424
1724
  id: COUNTER,
@@ -1467,21 +1767,21 @@ var testFailures = {
1467
1767
  run(command.replaceAll(PLACEHOLDER, path));
1468
1768
  return fromReport(path);
1469
1769
  } finally {
1470
- if (own) rmSync4(join10(path, ".."), { force: true, recursive: true });
1770
+ if (own) rmSync4(join12(path, ".."), { force: true, recursive: true });
1471
1771
  }
1472
1772
  }
1473
1773
  };
1474
1774
 
1475
1775
  // 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";
1776
+ import { existsSync as existsSync14, readdirSync as readdirSync6, readFileSync as readFileSync16 } from "fs";
1777
+ import { join as join13, resolve as resolve18 } from "path";
1478
1778
  var CODE_FILE2 = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts|vue|svelte|astro)$/;
1479
1779
  var SKIP4 = /^(?:node_modules|dist|coverage|\.[^.]|__fixtures__)/;
1480
1780
  var PLAN_FILE = /^(\d{3,})-[a-z\d][a-z\d.-]*\.md$/;
1481
1781
  var plansIn = (dir) => {
1482
- if (!existsSync11(dir)) return /* @__PURE__ */ new Set();
1782
+ if (!existsSync14(dir)) return /* @__PURE__ */ new Set();
1483
1783
  const found = /* @__PURE__ */ new Set();
1484
- for (const name of readdirSync5(dir)) {
1784
+ for (const name of readdirSync6(dir)) {
1485
1785
  const number = PLAN_FILE.exec(name)?.[1];
1486
1786
  if (number !== void 0) found.add(String(Number(number)));
1487
1787
  }
@@ -1490,7 +1790,7 @@ var plansIn = (dir) => {
1490
1790
  var countIn2 = (dir, marker, plans, depth = 12) => {
1491
1791
  let entries;
1492
1792
  try {
1493
- entries = readdirSync5(dir, { withFileTypes: true });
1793
+ entries = readdirSync6(dir, { withFileTypes: true });
1494
1794
  } catch {
1495
1795
  return 0;
1496
1796
  }
@@ -1498,13 +1798,13 @@ var countIn2 = (dir, marker, plans, depth = 12) => {
1498
1798
  for (const entry of entries) {
1499
1799
  if (SKIP4.test(entry.name)) continue;
1500
1800
  if (entry.isDirectory()) {
1501
- if (depth > 0) found += countIn2(join11(dir, entry.name), marker, plans, depth - 1);
1801
+ if (depth > 0) found += countIn2(join13(dir, entry.name), marker, plans, depth - 1);
1502
1802
  continue;
1503
1803
  }
1504
1804
  if (!CODE_FILE2.test(entry.name)) continue;
1505
1805
  let text = "";
1506
1806
  try {
1507
- text = readFileSync13(join11(dir, entry.name), "utf8");
1807
+ text = readFileSync16(join13(dir, entry.name), "utf8");
1508
1808
  } catch {
1509
1809
  continue;
1510
1810
  }
@@ -1528,22 +1828,22 @@ var orphanTodos = {
1528
1828
  readingParams: ["markers"],
1529
1829
  run: async ({ cwd, params }) => {
1530
1830
  const markers = stringsParam(params, "markers", ["TODO", "FIXME"]);
1531
- const plans = plansIn(resolve14(cwd, stringParam("orphanTodos", params, "plans", "plans")));
1831
+ const plans = plansIn(resolve18(cwd, stringParam("orphanTodos", params, "plans", "plans")));
1532
1832
  const marker = new RegExp(
1533
1833
  `\\b(?:${markers.map(escapeForRegex).join("|")})\\b(?:\\((\\d+)\\))?`,
1534
1834
  "g"
1535
1835
  );
1536
1836
  const roots = stringsParam(params, "roots", ["."]);
1537
- return roots.reduce((sum, root) => sum + countIn2(resolve14(cwd, root), marker, plans), 0);
1837
+ return roots.reduce((sum, root) => sum + countIn2(resolve18(cwd, root), marker, plans), 0);
1538
1838
  }
1539
1839
  };
1540
1840
 
1541
1841
  // src/counters/typecheck.ts
1542
- var UNRESOLVED = /error TS(?:2305|2307): ([^\n]*)/g;
1842
+ var UNRESOLVED2 = /error TS(?:2305|2307): ([^\n]*)/g;
1543
1843
  var SPECIFIER = /'([^']+)'/g;
1544
1844
  var specifiersIn = (message) => [...message.matchAll(SPECIFIER)].map(([, quoted]) => (quoted ?? "").replaceAll('"', ""));
1545
1845
  var unbuiltIn = (cwd, output) => {
1546
- const messages = [...output.matchAll(UNRESOLVED)].map(([, message]) => message ?? "");
1846
+ const messages = [...output.matchAll(UNRESOLVED2)].map(([, message]) => message ?? "");
1547
1847
  if (messages.length === 0) return [];
1548
1848
  const packages = workspacePackageNames(cwd);
1549
1849
  const named = messages.flatMap((message) => specifiersIn(message)).flatMap(
@@ -1573,8 +1873,8 @@ var typecheckErrors = {
1573
1873
  };
1574
1874
 
1575
1875
  // src/counters/walk.ts
1576
- import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
1577
- import { resolve as resolve15 } from "path";
1876
+ import { existsSync as existsSync15, readFileSync as readFileSync17 } from "fs";
1877
+ import { resolve as resolve19 } from "path";
1578
1878
  var DEFAULT_REPORT2 = ".geonosis/walk-report.json";
1579
1879
  var CLASSES = /* @__PURE__ */ new Set([
1580
1880
  "buy-box-above-fold",
@@ -1618,8 +1918,8 @@ var walkFindings = {
1618
1918
  },
1619
1919
  run: async ({ cwd, params }) => {
1620
1920
  const relative = stringParam("walkFindings", params, "report", DEFAULT_REPORT2);
1621
- const path = resolve15(cwd, relative);
1622
- if (!existsSync12(path)) {
1921
+ const path = resolve19(cwd, relative);
1922
+ if (!existsSync15(path)) {
1623
1923
  throw new CounterError(
1624
1924
  "walkFindings",
1625
1925
  `no walk report at ${relative} \u2014 run \`geonosis-walk\` before measuring it`
@@ -1627,7 +1927,7 @@ var walkFindings = {
1627
1927
  }
1628
1928
  let report;
1629
1929
  try {
1630
- report = JSON.parse(readFileSync14(path, "utf8"));
1930
+ report = JSON.parse(readFileSync17(path, "utf8"));
1631
1931
  } catch (error) {
1632
1932
  throw new CounterError(
1633
1933
  "walkFindings",
@@ -1649,14 +1949,18 @@ var walkFindings = {
1649
1949
 
1650
1950
  // src/counters/index.ts
1651
1951
  var COUNTERS = [
1952
+ adoptionSeamLines,
1652
1953
  archViolations,
1653
1954
  boundaryIssues,
1955
+ bumpDurationMs,
1654
1956
  bundleBytes,
1655
1957
  cloneCount,
1656
1958
  disabledCiJobs,
1657
1959
  fastTierMs,
1960
+ floorSurface,
1658
1961
  knipIssues,
1659
1962
  lawLineCount,
1963
+ openBacklogRows,
1660
1964
  suppressionCount,
1661
1965
  oxlintErrors,
1662
1966
  oxlintRule,
@@ -1668,6 +1972,7 @@ var COUNTERS = [
1668
1972
  sumOfCounts,
1669
1973
  testFailures,
1670
1974
  testsWithoutRunner,
1975
+ timeToGreenDoctorMs,
1671
1976
  typecheckErrors,
1672
1977
  unformattedFiles,
1673
1978
  walkFindings
package/dist/cli.js CHANGED
@@ -7,11 +7,11 @@ import {
7
7
  runRatchet,
8
8
  versionOf,
9
9
  writeEnvelope
10
- } from "./chunk-H4PLCM5J.js";
10
+ } from "./chunk-E55APSHM.js";
11
11
 
12
12
  // src/cli.ts
13
13
  import process from "process";
14
- var USAGE = `geonosis-ratchet [--cwd <dir>] [--tier <name>] [--prove] [--exclusive]
14
+ var USAGE = `geonosis-ratchet [--cwd <dir>] [--tier <name>] [--prove] [--exclusive] [--json]
15
15
 
16
16
  Debt as a number that may only shrink. Runs the counters named in geonosis.ratchet.json, compares
17
17
  each against gate-baseline.json, and REWRITES the baseline down when a number shrank \u2014 so the win is
@@ -23,6 +23,7 @@ locked in the same commit that earned it.
23
23
  --exclusive hold the machine-wide heavy lock for the run
24
24
  --exclusive-timeout <secs> how long to wait for that lock
25
25
  --hold <ms> take the lock, wait, give it back \u2014 the --prove self-test's slow thing
26
+ --json the run's own report on stdout, for a caller that parses it
26
27
  --help, -h this text
27
28
 
28
29
  Exit codes: 0 no counter grew \xB7 1 a counter grew \xB7 2 the run could not measure.
@@ -35,6 +36,7 @@ var KNOWN = /* @__PURE__ */ new Set([
35
36
  "--exclusive",
36
37
  "--exclusive-timeout",
37
38
  "--hold",
39
+ "--json",
38
40
  "--prove",
39
41
  "--tier"
40
42
  ]);
@@ -91,6 +93,7 @@ var tierFlag = process.argv.indexOf("--tier");
91
93
  var tier = tierFlag === -1 ? void 0 : process.argv[tierFlag + 1];
92
94
  var proving = process.argv.includes("--prove");
93
95
  var exclusive = process.argv.includes("--exclusive");
96
+ var json = process.argv.includes("--json");
94
97
  var timeoutFlag = process.argv.indexOf("--exclusive-timeout");
95
98
  var timeoutSeconds = timeoutFlag === -1 ? void 0 : Number(process.argv[timeoutFlag + 1] ?? Number.NaN);
96
99
  var holdMs = numberAfter("--hold");
@@ -124,18 +127,20 @@ var measure = async () => {
124
127
  exclusiveVia: process.argv[1],
125
128
  tier
126
129
  });
127
- process.stdout.write(formatProve(proof));
130
+ process.stdout.write(json ? `${JSON.stringify(proof, void 0, 2)}
131
+ ` : formatProve(proof));
128
132
  return proof.proven ? 0 : 2;
129
133
  }
130
134
  const startedAt = Date.now();
131
135
  const result = await runRatchet({ counters: COUNTERS, cwd, tier });
132
- process.stdout.write(formatReport(result));
136
+ if (!json) process.stdout.write(formatReport(result));
133
137
  const at = writeEnvelope({
134
138
  envelope: envelopeOf(result, Date.now() - startedAt),
135
139
  next: ENVELOPE_NEXT,
136
140
  root: cwd
137
141
  });
138
- process.stdout.write(`envelope: ${at}
142
+ process.stdout.write(json ? `${JSON.stringify(result, void 0, 2)}
143
+ ` : `envelope: ${at}
139
144
  `);
140
145
  if (result.refusals.length > 0) return 2;
141
146
  return result.measurements.some((one) => one.verdict === "grew") ? 1 : 0;
package/dist/index.d.ts CHANGED
@@ -259,12 +259,6 @@ declare const envelopePath: (root: string, tool: string) => string;
259
259
  declare class UnbalancedEnvelope extends Error {
260
260
  constructor(message: string);
261
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
262
  declare const writeEnvelope: ({ envelope, next, root, }: {
269
263
  envelope: ReportEnvelope;
270
264
  /** The command a reader should run to see the accounting for themselves. */
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  runRatchet,
19
19
  versionOf,
20
20
  writeEnvelope
21
- } from "./chunk-H4PLCM5J.js";
21
+ } from "./chunk-E55APSHM.js";
22
22
  export {
23
23
  CONFIG_FILE,
24
24
  COUNTERS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/ratchet",
3
- "version": "2.1.0",
3
+ "version": "2.3.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": [