@geonosis/ratchet 2.2.0 → 2.3.1

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
 
@@ -74,8 +74,14 @@ var versionOf = (moduleUrl) => {
74
74
  import { randomUUID } from "crypto";
75
75
  import { linkSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
76
76
  import { homedir } from "os";
77
- import { dirname as dirname2, join as join2 } from "path";
78
- 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
+ };
79
85
  var sleep = (ms) => new Promise((done) => setTimeout(done, ms));
80
86
  var holderOf = (path) => {
81
87
  try {
@@ -606,6 +612,10 @@ var scannerProbe = {
606
612
  var archViolations = {
607
613
  id: "archViolations",
608
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"],
609
619
  run: async ({ params, run }) => {
610
620
  const command = stringParam("archViolations", params, "command");
611
621
  const match = new RegExp(stringParam("archViolations", params, "match", "^\u2717"));
@@ -790,9 +800,119 @@ var boundaryIssues = {
790
800
  }
791
801
  };
792
802
 
793
- // src/counters/format.ts
794
- import { existsSync as existsSync4 } from "fs";
803
+ // src/counters/dx.ts
804
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
795
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
+
913
+ // src/counters/format.ts
914
+ import { existsSync as existsSync5 } from "fs";
915
+ import { resolve as resolve8 } from "path";
796
916
  var unformattedFiles = {
797
917
  id: "unformattedFiles",
798
918
  probe: {
@@ -807,13 +927,13 @@ var unformattedFiles = {
807
927
  "command",
808
928
  "npx oxfmt --config .oxfmtrc.json --list-different ."
809
929
  );
810
- return run(command).output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && existsSync4(resolve7(cwd, line))).length;
930
+ return run(command).output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && existsSync5(resolve8(cwd, line))).length;
811
931
  }
812
932
  };
813
933
 
814
934
  // src/counters/gate-report.ts
815
- import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
816
- import { resolve as resolve8 } from "path";
935
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
936
+ import { resolve as resolve9 } from "path";
817
937
  var DEFAULT_REPORT = ".geonosis/gate-report.json";
818
938
  var fastTierMs = {
819
939
  id: "fastTierMs",
@@ -836,9 +956,9 @@ var fastTierMs = {
836
956
  const wanted = stringParam("fastTierMs", params, "tier", "fast");
837
957
  const own = `.geonosis/gate-report.${wanted}.json`;
838
958
  const asked = stringParam("fastTierMs", params, "report", "");
839
- const relative = asked !== "" ? asked : existsSync5(resolve8(cwd, own)) ? own : DEFAULT_REPORT;
840
- const path = resolve8(cwd, relative);
841
- 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)) {
842
962
  throw new CounterError(
843
963
  "fastTierMs",
844
964
  `no gate report at ${relative} \u2014 run \`geonosis-verify ${wanted}\` before measuring it`
@@ -846,7 +966,7 @@ var fastTierMs = {
846
966
  }
847
967
  let report;
848
968
  try {
849
- report = JSON.parse(readFileSync6(path, "utf8"));
969
+ report = JSON.parse(readFileSync7(path, "utf8"));
850
970
  } catch (error) {
851
971
  throw new CounterError(
852
972
  "fastTierMs",
@@ -868,8 +988,8 @@ var fastTierMs = {
868
988
  };
869
989
 
870
990
  // src/counters/law.ts
871
- import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
872
- import { resolve as resolve9 } from "path";
991
+ import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
992
+ import { resolve as resolve10 } from "path";
873
993
  var lawLineCount = {
874
994
  id: "lawLineCount",
875
995
  probe: {
@@ -879,15 +999,15 @@ var lawLineCount = {
879
999
  },
880
1000
  run: async ({ cwd, params }) => {
881
1001
  const relative = stringParam("lawLineCount", params, "path", "CLAUDE.md");
882
- const path = resolve9(cwd, relative);
883
- if (!existsSync6(path)) throw new CounterError("lawLineCount", `no law file at ${relative}`);
884
- 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;
885
1005
  }
886
1006
  };
887
1007
 
888
1008
  // src/counters/oxlint.ts
889
- import { existsSync as existsSync7, readFileSync as readFileSync8, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
890
- import { resolve as resolve10 } 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";
891
1011
  var DEFAULT_COMMAND = "npx oxlint --format=unix --config .oxlintrc.json .";
892
1012
  var PROBE_COMMAND = "oxlint --format=unix --config .oxlintrc.json .";
893
1013
  var oxlintProbe = (severity, rule, source) => ({
@@ -1050,26 +1170,26 @@ var oxlintRule = {
1050
1170
  const config = typeof params.config === "string" ? params.config : "";
1051
1171
  const expect = expectedFormat("oxlintRule", params);
1052
1172
  if (config === "") return countRule(run(command), rule, expect);
1053
- const source = resolve10(cwd, config);
1054
- 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}`);
1055
1175
  const strictName = `.oxlintrc.ratchet-${key}.json`;
1056
- const strict = readFileSync8(source, "utf8").replace(
1176
+ const strict = readFileSync9(source, "utf8").replace(
1057
1177
  new RegExp(`("[^"]*${escapeForRegex(rule)}"\\s*:\\s*\\[?\\s*)"(warn|off)"`),
1058
1178
  '$1"error"'
1059
1179
  );
1060
- writeFileSync6(resolve10(cwd, strictName), strict);
1180
+ writeFileSync6(resolve11(cwd, strictName), strict);
1061
1181
  try {
1062
1182
  return countRule(run(command.replace("{config}", strictName)), rule, expect);
1063
1183
  } finally {
1064
- rmSync3(resolve10(cwd, strictName), { force: true });
1184
+ rmSync3(resolve11(cwd, strictName), { force: true });
1065
1185
  }
1066
1186
  }
1067
1187
  };
1068
1188
 
1069
1189
  // src/counters/probes.ts
1070
- import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
1190
+ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
1071
1191
  import { createRequire } from "module";
1072
- import { join as join6, resolve as resolve11 } from "path";
1192
+ import { join as join6, resolve as resolve12 } from "path";
1073
1193
  import { pathToFileURL } from "url";
1074
1194
  var ID = "probelessRules";
1075
1195
  var OFF = /* @__PURE__ */ new Set([0, "0", "allow", "off", false]);
@@ -1147,11 +1267,11 @@ var probelessRules = {
1147
1267
  },
1148
1268
  run: async ({ cwd, params }) => {
1149
1269
  const relative = stringParam(ID, params, "config", ".oxlintrc.json");
1150
- const path = resolve11(cwd, relative);
1151
- 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}`);
1152
1272
  let config;
1153
1273
  try {
1154
- config = JSON.parse(readFileSync9(path, "utf8"));
1274
+ config = JSON.parse(readFileSync10(path, "utf8"));
1155
1275
  } catch (error) {
1156
1276
  throw new CounterError(ID, `${relative} does not parse: ${error.message}`);
1157
1277
  }
@@ -1188,8 +1308,8 @@ import { readdirSync as readdirSync3 } from "fs";
1188
1308
  import { join as join8 } from "path";
1189
1309
 
1190
1310
  // src/counters/workspace.ts
1191
- import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync10 } from "fs";
1192
- import { join as join7, resolve as resolve12 } 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";
1193
1313
  var SKIP = /^(node_modules|\.)/;
1194
1314
  var childDirs = (dir) => {
1195
1315
  try {
@@ -1203,14 +1323,14 @@ var expand = (root, pattern) => {
1203
1323
  const segments = pattern.split("/").filter((one) => one !== "" && one !== ".");
1204
1324
  let dirs = [root];
1205
1325
  for (const segment of segments) {
1206
- 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));
1207
1327
  }
1208
1328
  return dirs;
1209
1329
  };
1210
1330
  var QUOTED = /^['"]|['"]$/g;
1211
1331
  var cleaned = (value) => value.replace(/#.*$/, "").trim().replaceAll(QUOTED, "");
1212
1332
  var pnpmPatterns = (path) => {
1213
- const lines = readFileSync10(path, "utf8").split("\n");
1333
+ const lines = readFileSync11(path, "utf8").split("\n");
1214
1334
  const at = lines.findIndex((line) => line.startsWith("packages:"));
1215
1335
  if (at === -1) return [];
1216
1336
  const inline = lines[at]?.slice("packages:".length).trim() ?? "";
@@ -1229,31 +1349,31 @@ var pnpmPatterns = (path) => {
1229
1349
  return patterns;
1230
1350
  };
1231
1351
  var npmPatterns = (path) => {
1232
- const parsed = JSON.parse(readFileSync10(path, "utf8"));
1352
+ const parsed = JSON.parse(readFileSync11(path, "utf8"));
1233
1353
  const declared = Array.isArray(parsed.workspaces) ? parsed.workspaces : parsed.workspaces?.packages ?? [];
1234
1354
  return declared.filter((one) => typeof one === "string");
1235
1355
  };
1236
1356
  var nameOf = (dir) => {
1237
1357
  const manifest = join7(dir, "package.json");
1238
- if (!existsSync9(manifest)) return void 0;
1358
+ if (!existsSync10(manifest)) return void 0;
1239
1359
  try {
1240
- const { name } = JSON.parse(readFileSync10(manifest, "utf8"));
1360
+ const { name } = JSON.parse(readFileSync11(manifest, "utf8"));
1241
1361
  return typeof name === "string" && name !== "" ? name : void 0;
1242
1362
  } catch {
1243
1363
  return void 0;
1244
1364
  }
1245
1365
  };
1246
1366
  var workspaceDirs = (cwd) => {
1247
- const root = resolve12(cwd);
1367
+ const root = resolve13(cwd);
1248
1368
  const pnpm = join7(root, "pnpm-workspace.yaml");
1249
1369
  const manifest = join7(root, "package.json");
1250
- const patterns = existsSync9(pnpm) ? pnpmPatterns(pnpm) : existsSync9(manifest) ? npmPatterns(manifest) : [];
1251
- 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")));
1252
1372
  return [...new Set(dirs)];
1253
1373
  };
1254
1374
  var manifestOf = (dir) => {
1255
1375
  try {
1256
- const parsed = JSON.parse(readFileSync10(join7(dir, "package.json"), "utf8"));
1376
+ const parsed = JSON.parse(readFileSync11(join7(dir, "package.json"), "utf8"));
1257
1377
  return typeof parsed === "object" && parsed !== null ? parsed : void 0;
1258
1378
  } catch {
1259
1379
  return void 0;
@@ -1321,8 +1441,8 @@ var packagesWithoutTypecheck = {
1321
1441
  };
1322
1442
 
1323
1443
  // src/counters/seams.ts
1324
- import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync11 } from "fs";
1325
- import { join as join9, resolve as resolve13 } from "path";
1444
+ import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync12 } from "fs";
1445
+ import { join as join9, resolve as resolve14 } from "path";
1326
1446
  var CONFIG = "geonosis.json";
1327
1447
  var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".turbo", "coverage", "dist", "node_modules"]);
1328
1448
  var asPattern = (glob) => new RegExp(
@@ -1338,16 +1458,16 @@ var filesUnder = (root, at = root) => readdirSync4(at, { withFileTypes: true }).
1338
1458
  });
1339
1459
  var seamGlobsIn = (root) => {
1340
1460
  const at = join9(root, CONFIG);
1341
- if (!existsSync10(at)) return [];
1461
+ if (!existsSync11(at)) return [];
1342
1462
  try {
1343
- const seams = JSON.parse(readFileSync11(at, "utf8")).adoption?.seams;
1463
+ const seams = JSON.parse(readFileSync12(at, "utf8")).adoption?.seams;
1344
1464
  return Array.isArray(seams) ? seams.filter((one) => typeof one === "string") : [];
1345
1465
  } catch {
1346
1466
  return [];
1347
1467
  }
1348
1468
  };
1349
1469
  var linesIn = (path) => {
1350
- const body = readFileSync11(path, "utf8");
1470
+ const body = readFileSync12(path, "utf8");
1351
1471
  return body === "" ? 0 : body.replace(/\n$/, "").split("\n").length;
1352
1472
  };
1353
1473
  var adoptionSeamLines = {
@@ -1363,7 +1483,7 @@ var adoptionSeamLines = {
1363
1483
  params: {}
1364
1484
  },
1365
1485
  run: async ({ cwd }) => {
1366
- const root = resolve13(cwd);
1486
+ const root = resolve14(cwd);
1367
1487
  const globs = seamGlobsIn(root).map(asPattern);
1368
1488
  if (globs.length === 0) return 0;
1369
1489
  return filesUnder(root).filter((one) => globs.some((pattern) => pattern.test(one))).reduce((total2, one) => total2 + linesIn(join9(root, one)), 0);
@@ -1392,8 +1512,8 @@ var sumOfCounts = {
1392
1512
  };
1393
1513
 
1394
1514
  // src/counters/suppressions.ts
1395
- import { readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
1396
- import { join as join10, resolve as resolve14 } from "path";
1515
+ import { readdirSync as readdirSync5, readFileSync as readFileSync13 } from "fs";
1516
+ import { join as join10, resolve as resolve15 } from "path";
1397
1517
  var CODE_FILE = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts|vue|svelte|astro)$/;
1398
1518
  var SKIP3 = /^(?:node_modules|dist|coverage|\.[^.]|__fixtures__)/;
1399
1519
  var SUPPRESSION = /(?:eslint|oxlint|biome)-(?:disable|ignore)(?:-next-line|-line)?|@ts-expect-error|@ts-ignore/g;
@@ -1413,7 +1533,7 @@ var countIn = (dir, depth = 12) => {
1413
1533
  }
1414
1534
  if (!CODE_FILE.test(entry.name)) continue;
1415
1535
  try {
1416
- found += readFileSync12(join10(dir, entry.name), "utf8").match(SUPPRESSION)?.length ?? 0;
1536
+ found += readFileSync13(join10(dir, entry.name), "utf8").match(SUPPRESSION)?.length ?? 0;
1417
1537
  } catch {
1418
1538
  }
1419
1539
  }
@@ -1427,13 +1547,13 @@ var suppressionCount = {
1427
1547
  },
1428
1548
  run: async ({ cwd, params }) => {
1429
1549
  const roots = stringsParam(params, "roots", ["."]);
1430
- return roots.reduce((sum, root) => sum + countIn(resolve14(cwd, root)), 0);
1550
+ return roots.reduce((sum, root) => sum + countIn(resolve15(cwd, root)), 0);
1431
1551
  }
1432
1552
  };
1433
1553
 
1434
1554
  // src/counters/surface.ts
1435
- import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
1436
- import { join as join11, resolve as resolve15 } from "path";
1555
+ import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
1556
+ import { join as join11, resolve as resolve16 } from "path";
1437
1557
  var DECLARED = /^export\s+(?:declare\s+)?(?:abstract\s+)?(?:const|function|class|type|interface|enum|let|var)\s+([$\w]+)/;
1438
1558
  var BRACED = /^export\s+(?:type\s+)?\{([^}]*)\}(?:\s+from\s+(['"])(.+?)\2)?/;
1439
1559
  var nameOf2 = (spec) => {
@@ -1467,10 +1587,10 @@ var exportedSurfaceIn = (source) => {
1467
1587
  var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1468
1588
  var declarationEntriesOf = (dir) => {
1469
1589
  const at = join11(dir, "package.json");
1470
- if (!existsSync11(at)) {
1590
+ if (!existsSync12(at)) {
1471
1591
  throw new CounterError("floorSurface", `no package.json in ${dir} \u2014 that is not a package`);
1472
1592
  }
1473
- const manifest = JSON.parse(readFileSync13(at, "utf8"));
1593
+ const manifest = JSON.parse(readFileSync14(at, "utf8"));
1474
1594
  const found = [];
1475
1595
  const walk = (value) => {
1476
1596
  if (typeof value === "string" && value.endsWith(".d.ts")) found.push(value);
@@ -1501,7 +1621,7 @@ var floorSurface = {
1501
1621
  },
1502
1622
  run: async ({ cwd, params }) => {
1503
1623
  const relative = stringParam("floorSurface", params, "dir");
1504
- const dir = resolve15(cwd, relative);
1624
+ const dir = resolve16(cwd, relative);
1505
1625
  const entries = declarationEntriesOf(dir);
1506
1626
  if (entries.length === 0) {
1507
1627
  throw new CounterError(
@@ -1511,23 +1631,23 @@ var floorSurface = {
1511
1631
  }
1512
1632
  const names = /* @__PURE__ */ new Set();
1513
1633
  for (const entry of entries) {
1514
- const at = resolve15(dir, entry);
1515
- if (!existsSync11(at)) {
1634
+ const at = resolve16(dir, entry);
1635
+ if (!existsSync12(at)) {
1516
1636
  throw new CounterError(
1517
1637
  "floorSurface",
1518
1638
  `${relative} promises ${entry} and it is not there \u2014 build before measuring, or the surface reads as empty`
1519
1639
  );
1520
1640
  }
1521
- for (const name of exportedSurfaceIn(readFileSync13(at, "utf8")).keys()) names.add(name);
1641
+ for (const name of exportedSurfaceIn(readFileSync14(at, "utf8")).keys()) names.add(name);
1522
1642
  }
1523
1643
  return names.size;
1524
1644
  }
1525
1645
  };
1526
1646
 
1527
1647
  // src/counters/tests.ts
1528
- import { existsSync as existsSync12, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, rmSync as rmSync4 } from "fs";
1648
+ import { existsSync as existsSync13, mkdtempSync as mkdtempSync2, readFileSync as readFileSync15, rmSync as rmSync4 } from "fs";
1529
1649
  import { tmpdir as tmpdir2 } from "os";
1530
- import { join as join12, resolve as resolve16 } from "path";
1650
+ import { join as join12, resolve as resolve17 } from "path";
1531
1651
  var COUNTER = "testFailures";
1532
1652
  var VITEST_LINE = /^\s*Tests {2,}(.+?)\s*$/;
1533
1653
  var VITEST_TOTAL = /\(\d+\)$/;
@@ -1559,7 +1679,7 @@ ${output.trim().slice(-500)}`
1559
1679
  );
1560
1680
  };
1561
1681
  var fromReport = (path) => {
1562
- if (!existsSync12(path)) {
1682
+ if (!existsSync13(path)) {
1563
1683
  throw new CounterError(
1564
1684
  COUNTER,
1565
1685
  `the runner wrote no report at ${path} \u2014 a crash before the reporter is not a pass`
@@ -1567,7 +1687,7 @@ var fromReport = (path) => {
1567
1687
  }
1568
1688
  let report;
1569
1689
  try {
1570
- report = JSON.parse(readFileSync14(path, "utf8"));
1690
+ report = JSON.parse(readFileSync15(path, "utf8"));
1571
1691
  } catch (error) {
1572
1692
  throw new CounterError(
1573
1693
  COUNTER,
@@ -1591,7 +1711,7 @@ var fromReport = (path) => {
1591
1711
  };
1592
1712
  var reportPathFor = (cwd, params, command) => {
1593
1713
  const named = params.reportPath;
1594
- if (typeof named === "string" && named !== "") return { own: false, path: resolve16(cwd, named) };
1714
+ if (typeof named === "string" && named !== "") return { own: false, path: resolve17(cwd, named) };
1595
1715
  if (!command.includes(PLACEHOLDER)) {
1596
1716
  throw new CounterError(
1597
1717
  COUNTER,
@@ -1653,13 +1773,13 @@ var testFailures = {
1653
1773
  };
1654
1774
 
1655
1775
  // src/counters/todos.ts
1656
- import { existsSync as existsSync13, readdirSync as readdirSync6, readFileSync as readFileSync15 } from "fs";
1657
- import { join as join13, resolve as resolve17 } 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";
1658
1778
  var CODE_FILE2 = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts|vue|svelte|astro)$/;
1659
1779
  var SKIP4 = /^(?:node_modules|dist|coverage|\.[^.]|__fixtures__)/;
1660
1780
  var PLAN_FILE = /^(\d{3,})-[a-z\d][a-z\d.-]*\.md$/;
1661
1781
  var plansIn = (dir) => {
1662
- if (!existsSync13(dir)) return /* @__PURE__ */ new Set();
1782
+ if (!existsSync14(dir)) return /* @__PURE__ */ new Set();
1663
1783
  const found = /* @__PURE__ */ new Set();
1664
1784
  for (const name of readdirSync6(dir)) {
1665
1785
  const number = PLAN_FILE.exec(name)?.[1];
@@ -1684,7 +1804,7 @@ var countIn2 = (dir, marker, plans, depth = 12) => {
1684
1804
  if (!CODE_FILE2.test(entry.name)) continue;
1685
1805
  let text = "";
1686
1806
  try {
1687
- text = readFileSync15(join13(dir, entry.name), "utf8");
1807
+ text = readFileSync16(join13(dir, entry.name), "utf8");
1688
1808
  } catch {
1689
1809
  continue;
1690
1810
  }
@@ -1708,13 +1828,13 @@ var orphanTodos = {
1708
1828
  readingParams: ["markers"],
1709
1829
  run: async ({ cwd, params }) => {
1710
1830
  const markers = stringsParam(params, "markers", ["TODO", "FIXME"]);
1711
- const plans = plansIn(resolve17(cwd, stringParam("orphanTodos", params, "plans", "plans")));
1831
+ const plans = plansIn(resolve18(cwd, stringParam("orphanTodos", params, "plans", "plans")));
1712
1832
  const marker = new RegExp(
1713
1833
  `\\b(?:${markers.map(escapeForRegex).join("|")})\\b(?:\\((\\d+)\\))?`,
1714
1834
  "g"
1715
1835
  );
1716
1836
  const roots = stringsParam(params, "roots", ["."]);
1717
- return roots.reduce((sum, root) => sum + countIn2(resolve17(cwd, root), marker, plans), 0);
1837
+ return roots.reduce((sum, root) => sum + countIn2(resolve18(cwd, root), marker, plans), 0);
1718
1838
  }
1719
1839
  };
1720
1840
 
@@ -1753,8 +1873,8 @@ var typecheckErrors = {
1753
1873
  };
1754
1874
 
1755
1875
  // src/counters/walk.ts
1756
- import { existsSync as existsSync14, readFileSync as readFileSync16 } from "fs";
1757
- import { resolve as resolve18 } from "path";
1876
+ import { existsSync as existsSync15, readFileSync as readFileSync17 } from "fs";
1877
+ import { resolve as resolve19 } from "path";
1758
1878
  var DEFAULT_REPORT2 = ".geonosis/walk-report.json";
1759
1879
  var CLASSES = /* @__PURE__ */ new Set([
1760
1880
  "buy-box-above-fold",
@@ -1798,8 +1918,8 @@ var walkFindings = {
1798
1918
  },
1799
1919
  run: async ({ cwd, params }) => {
1800
1920
  const relative = stringParam("walkFindings", params, "report", DEFAULT_REPORT2);
1801
- const path = resolve18(cwd, relative);
1802
- if (!existsSync14(path)) {
1921
+ const path = resolve19(cwd, relative);
1922
+ if (!existsSync15(path)) {
1803
1923
  throw new CounterError(
1804
1924
  "walkFindings",
1805
1925
  `no walk report at ${relative} \u2014 run \`geonosis-walk\` before measuring it`
@@ -1807,7 +1927,7 @@ var walkFindings = {
1807
1927
  }
1808
1928
  let report;
1809
1929
  try {
1810
- report = JSON.parse(readFileSync16(path, "utf8"));
1930
+ report = JSON.parse(readFileSync17(path, "utf8"));
1811
1931
  } catch (error) {
1812
1932
  throw new CounterError(
1813
1933
  "walkFindings",
@@ -1832,6 +1952,7 @@ var COUNTERS = [
1832
1952
  adoptionSeamLines,
1833
1953
  archViolations,
1834
1954
  boundaryIssues,
1955
+ bumpDurationMs,
1835
1956
  bundleBytes,
1836
1957
  cloneCount,
1837
1958
  disabledCiJobs,
@@ -1839,6 +1960,7 @@ var COUNTERS = [
1839
1960
  floorSurface,
1840
1961
  knipIssues,
1841
1962
  lawLineCount,
1963
+ openBacklogRows,
1842
1964
  suppressionCount,
1843
1965
  oxlintErrors,
1844
1966
  oxlintRule,
@@ -1850,6 +1972,7 @@ var COUNTERS = [
1850
1972
  sumOfCounts,
1851
1973
  testFailures,
1852
1974
  testsWithoutRunner,
1975
+ timeToGreenDoctorMs,
1853
1976
  typecheckErrors,
1854
1977
  unformattedFiles,
1855
1978
  walkFindings
package/dist/cli.js CHANGED
@@ -7,11 +7,11 @@ import {
7
7
  runRatchet,
8
8
  versionOf,
9
9
  writeEnvelope
10
- } from "./chunk-IQ6TXLWC.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.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  runRatchet,
19
19
  versionOf,
20
20
  writeEnvelope
21
- } from "./chunk-IQ6TXLWC.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.2.0",
3
+ "version": "2.3.1",
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": [