@geonosis/ratchet 1.4.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -363,11 +363,12 @@ Every counter takes its `command` from the config, so the toolchain stays the re
363
363
  | `testFailures` | the runner's own failure summary — or, with `report: "vitest-json"`, `numFailedTests` out of the JSON report the command wrote; throws when nothing is readable, when the report is absent, and when the report failed with nothing failing | `command`, `report`, `reportPath` |
364
364
  | `unformattedFiles` | paths `--list-different` names that exist on disk | `command` |
365
365
  | `cloneCount` | jscpd's `Found N clones` | `command` |
366
- | `knipIssues` | the totals under knip's unused-* headings | `command`, `headings` |
366
+ | `knipIssues` | the total under EVERY heading knip's compact reporter emits — the shape `<Title> (N)`, not a list of titles, so a heading the kit has not heard of is debt rather than a silent zero. `headings` narrows it, and every printed heading the narrowing leaves out must be named under `excusedHeadings` with a reason or the run refuses | `command`, `headings`, `excusedHeadings` |
367
367
  | `boundaryIssues` | `N issues found` from a boundary scan | `command` |
368
368
  | `archViolations` | lines matching a marker your own architecture scan prints, refusing a non-zero exit that printed none of them — a scan that could not run is not a clean scan | `command`, `match` |
369
369
  | `sumOfCounts` | the total of one capture group across a per-file census (`grep -rc`) | `command`, `match` |
370
370
  | `lawLineCount` | the lines of the law file — a ceiling that can only come down | `path` |
371
+ | `probelessRules` | the rules an oxlint config ENABLES that the plugin it loads declares no `probe()` for. Those are the ones `geonosis-doctor`'s `exercised` can only answer UNJUDGED about, which #137 makes a WARN naming the kit as owner — and a WARN the kit carries across releases is a downgraded rule by another name. The plugin is imported from the tree it is pointed at, and one that cannot be loaded is a refusal, never a zero | `config`, `plugin` |
371
372
  | `runtimeCodeShipped` | 0 when a change shipped runtime code, 1 when it shipped none | `command`, `patterns` |
372
373
  | `disabledCiJobs` | lines of `if: false` across the workflow files — a job switched off to get a release through, still off. A condition that merely mentions `false` is not one. No workflows directory at all reads 0 | `dir` |
373
374
  | `bundleBytes` | one integer out of whatever your sizing command printed, separators and all; with `match`, the group that pattern names rather than the last integer, refusing when it matches nothing. **Tolerates.** | `command`, `match`, `tolerance` |
@@ -375,6 +376,7 @@ Every counter takes its `command` from the config, so the toolchain stays the re
375
376
  | `testsWithoutRunner` | workspaces holding `*.test.*`, `*.spec.*` or `__tests__/` with no `test` script — the suites nobody runs, which read exactly like suites that pass | `script` |
376
377
  | `packagesWithoutTypecheck` | workspaces with no `typecheck` script | `script` |
377
378
  | `walkFindings` | the defects in the report `geonosis-walk` wrote, over every page; with `classes`, only those classes, refusing a class the walk does not have. A missing or unparsable report is a refusal — the walk writes none when it could not run | `report`, `classes` |
379
+ | `orphanTodos` | debt markers outside the plan graph: one naming no plan, and one naming a plan that is not in the plans directory. Leading zeroes are a spelling, so `(021)` finds `21-…md`. Fixtures, dependencies and build output are not read | `markers`, `plans`, `roots` |
378
380
 
379
381
  ### `tolerance`, and the two counters that accept one
380
382
 
@@ -708,20 +708,39 @@ ${said.slice(-500)}`
708
708
  return Number(found);
709
709
  }
710
710
  };
711
- var DEFAULT_HEADINGS = [
712
- "Unused files",
713
- "Unused dependencies",
714
- "Unused devDependencies",
715
- "Unused exports",
716
- "Unused exported types"
717
- ];
711
+ var HEADING = /^([A-Z][A-Za-z ]+?) \((\d+)\)\s*$/gm;
712
+ var headingsIn = (output) => {
713
+ const found = /* @__PURE__ */ new Map();
714
+ for (const [, heading = "", count = "0"] of output.matchAll(HEADING)) {
715
+ found.set(heading, Number(count));
716
+ }
717
+ return found;
718
+ };
719
+ var excusesIn = (params) => {
720
+ const declared = params["excusedHeadings"];
721
+ return new Set(
722
+ typeof declared === "object" && declared !== null && !Array.isArray(declared) ? Object.keys(declared) : []
723
+ );
724
+ };
718
725
  var knipIssues = {
719
726
  id: "knipIssues",
720
727
  probe: { ...captured("Unused files (1)\nsrc/gone.ts\n"), expect: 1 },
728
+ readingParams: ["headings"],
721
729
  run: async ({ params, run }) => {
722
730
  const command = stringParam("knipIssues", params, "command", "npx knip --reporter compact");
723
- const output = run(command).output;
724
- return stringsParam(params, "headings", DEFAULT_HEADINGS).map((heading) => Number(output.match(new RegExp(`${heading} \\((\\d+)\\)`))?.[1] ?? 0)).reduce((sum, count) => sum + count, 0);
731
+ const printed = headingsIn(run(command).output);
732
+ const counted = stringsParam(params, "headings", [...printed.keys()]);
733
+ const excused = excusesIn(params);
734
+ const dropped = [...printed.entries()].filter(
735
+ ([heading, count]) => count > 0 && !counted.includes(heading) && !excused.has(heading)
736
+ );
737
+ if (dropped.length > 0) {
738
+ throw new CounterError(
739
+ "knipIssues",
740
+ `knip printed ${dropped.map(([heading, count]) => `${heading} (${count})`).join(", ")} and the configured "headings" counts ${counted.join(", ")} \u2014 three ways out: add each to "headings", name it under "excusedHeadings" with the reason this repo does not hold it, or remove "headings" altogether and count every heading knip prints`
741
+ );
742
+ }
743
+ return counted.reduce((sum, heading) => sum + (printed.get(heading) ?? 0), 0);
725
744
  }
726
745
  };
727
746
  var ISSUES = /(\d+) issues? found/;
@@ -1010,6 +1029,100 @@ var oxlintRule = {
1010
1029
  }
1011
1030
  };
1012
1031
 
1032
+ // src/counters/probes.ts
1033
+ import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
1034
+ import { createRequire } from "module";
1035
+ import { join as join6, resolve as resolve10 } from "path";
1036
+ import { pathToFileURL } from "url";
1037
+ var ID = "probelessRules";
1038
+ var OFF = /* @__PURE__ */ new Set([0, "0", "allow", "off", false]);
1039
+ var severityOf2 = (level) => Array.isArray(level) ? level[0] : level;
1040
+ var enabledIn = (config, namespace) => {
1041
+ const on = /* @__PURE__ */ new Set();
1042
+ for (const block of [
1043
+ config.rules ?? {},
1044
+ ...(config.overrides ?? []).map((one) => one.rules ?? {})
1045
+ ]) {
1046
+ for (const [id, level] of Object.entries(block)) {
1047
+ if (id.startsWith(`${namespace}/`) && !OFF.has(severityOf2(level))) {
1048
+ on.add(id.slice(namespace.length + 1));
1049
+ }
1050
+ }
1051
+ }
1052
+ return [...on].toSorted();
1053
+ };
1054
+ var probedBy = async (cwd, plugin) => {
1055
+ let entry;
1056
+ try {
1057
+ entry = createRequire(join6(cwd, "noop.js")).resolve(plugin);
1058
+ } catch (error) {
1059
+ throw new CounterError(
1060
+ ID,
1061
+ `could not resolve ${plugin} from ${cwd} \u2014 ${String(error.message).split("\n")[0]}. A plugin nobody can load says nothing about which rules ship a probe, and it is not zero of them`
1062
+ );
1063
+ }
1064
+ let loaded;
1065
+ try {
1066
+ loaded = await import(pathToFileURL(entry).href);
1067
+ } catch (error) {
1068
+ throw new CounterError(ID, `could not import ${entry} \u2014 ${error.message}`);
1069
+ }
1070
+ const namespace = loaded.default?.meta?.name;
1071
+ if (typeof namespace !== "string" || namespace === "") {
1072
+ throw new CounterError(
1073
+ ID,
1074
+ `${plugin} declares no meta.name, so nothing says which rule ids in the config are its own`
1075
+ );
1076
+ }
1077
+ return {
1078
+ namespace,
1079
+ probed: new Set(
1080
+ Object.entries(loaded.default?.rules ?? {}).filter(([, rule]) => typeof rule?.probe === "function").map(([name]) => name)
1081
+ )
1082
+ };
1083
+ };
1084
+ var PLUGIN = "@geonosis/oxlint-plugin-biological-architecture";
1085
+ var probelessRules = {
1086
+ id: ID,
1087
+ probe: {
1088
+ expect: 1,
1089
+ input: (dir) => {
1090
+ plant(
1091
+ dir,
1092
+ ".oxlintrc.json",
1093
+ JSON.stringify({
1094
+ jsPlugins: ["probe-plugin"],
1095
+ rules: { "x/probed": "error", "x/bare": "error" }
1096
+ })
1097
+ );
1098
+ plant(
1099
+ dir,
1100
+ "node_modules/probe-plugin/package.json",
1101
+ '{"name":"probe-plugin","main":"index.js"}'
1102
+ );
1103
+ plant(
1104
+ dir,
1105
+ "node_modules/probe-plugin/index.js",
1106
+ "export default { meta: { name: 'x' }, rules: { probed: { probe: () => [], create: () => ({}) }, bare: { create: () => ({}) } } }\n"
1107
+ );
1108
+ },
1109
+ params: { plugin: "probe-plugin" }
1110
+ },
1111
+ run: async ({ cwd, params }) => {
1112
+ const relative = stringParam(ID, params, "config", ".oxlintrc.json");
1113
+ const path = resolve10(cwd, relative);
1114
+ if (!existsSync8(path)) throw new CounterError(ID, `no oxlint config at ${relative}`);
1115
+ let config;
1116
+ try {
1117
+ config = JSON.parse(readFileSync9(path, "utf8"));
1118
+ } catch (error) {
1119
+ throw new CounterError(ID, `${relative} does not parse: ${error.message}`);
1120
+ }
1121
+ const { namespace, probed } = await probedBy(cwd, stringParam(ID, params, "plugin", PLUGIN));
1122
+ return enabledIn(config, namespace).filter((rule) => !probed.has(rule)).length;
1123
+ }
1124
+ };
1125
+
1013
1126
  // src/counters/runtime-code.ts
1014
1127
  var DEFAULT_PATTERNS = ["^(apps|packages)/[^/]+/src/"];
1015
1128
  var NOT_RUNTIME = /(\.test\.|\.spec\.|__tests__\/|__fixtures__\/|\.d\.ts$)/;
@@ -1035,15 +1148,15 @@ var runtimeCodeShipped = {
1035
1148
 
1036
1149
  // src/counters/scripts.ts
1037
1150
  import { readdirSync as readdirSync3 } from "fs";
1038
- import { join as join7 } from "path";
1151
+ import { join as join8 } from "path";
1039
1152
 
1040
1153
  // src/counters/workspace.ts
1041
- import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync9 } from "fs";
1042
- import { join as join6, resolve as resolve10 } from "path";
1154
+ import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync10 } from "fs";
1155
+ import { join as join7, resolve as resolve11 } from "path";
1043
1156
  var SKIP = /^(node_modules|\.)/;
1044
1157
  var childDirs = (dir) => {
1045
1158
  try {
1046
- return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !SKIP.test(entry.name)).map((entry) => join6(dir, entry.name));
1159
+ return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !SKIP.test(entry.name)).map((entry) => join7(dir, entry.name));
1047
1160
  } catch {
1048
1161
  return [];
1049
1162
  }
@@ -1053,14 +1166,14 @@ var expand = (root, pattern) => {
1053
1166
  const segments = pattern.split("/").filter((one) => one !== "" && one !== ".");
1054
1167
  let dirs = [root];
1055
1168
  for (const segment of segments) {
1056
- dirs = segment === "*" ? dirs.flatMap((dir) => childDirs(dir)) : segment === "**" ? dirs.flatMap((dir) => descendants(dir, 3)) : dirs.map((dir) => join6(dir, segment)).filter((dir) => existsSync8(dir));
1169
+ dirs = segment === "*" ? dirs.flatMap((dir) => childDirs(dir)) : segment === "**" ? dirs.flatMap((dir) => descendants(dir, 3)) : dirs.map((dir) => join7(dir, segment)).filter((dir) => existsSync9(dir));
1057
1170
  }
1058
1171
  return dirs;
1059
1172
  };
1060
1173
  var QUOTED = /^['"]|['"]$/g;
1061
1174
  var cleaned = (value) => value.replace(/#.*$/, "").trim().replaceAll(QUOTED, "");
1062
1175
  var pnpmPatterns = (path) => {
1063
- const lines = readFileSync9(path, "utf8").split("\n");
1176
+ const lines = readFileSync10(path, "utf8").split("\n");
1064
1177
  const at = lines.findIndex((line) => line.startsWith("packages:"));
1065
1178
  if (at === -1) return [];
1066
1179
  const inline = lines[at]?.slice("packages:".length).trim() ?? "";
@@ -1079,31 +1192,31 @@ var pnpmPatterns = (path) => {
1079
1192
  return patterns;
1080
1193
  };
1081
1194
  var npmPatterns = (path) => {
1082
- const parsed = JSON.parse(readFileSync9(path, "utf8"));
1195
+ const parsed = JSON.parse(readFileSync10(path, "utf8"));
1083
1196
  const declared = Array.isArray(parsed.workspaces) ? parsed.workspaces : parsed.workspaces?.packages ?? [];
1084
1197
  return declared.filter((one) => typeof one === "string");
1085
1198
  };
1086
1199
  var nameOf = (dir) => {
1087
- const manifest = join6(dir, "package.json");
1088
- if (!existsSync8(manifest)) return void 0;
1200
+ const manifest = join7(dir, "package.json");
1201
+ if (!existsSync9(manifest)) return void 0;
1089
1202
  try {
1090
- const { name } = JSON.parse(readFileSync9(manifest, "utf8"));
1203
+ const { name } = JSON.parse(readFileSync10(manifest, "utf8"));
1091
1204
  return typeof name === "string" && name !== "" ? name : void 0;
1092
1205
  } catch {
1093
1206
  return void 0;
1094
1207
  }
1095
1208
  };
1096
1209
  var workspaceDirs = (cwd) => {
1097
- const root = resolve10(cwd);
1098
- const pnpm = join6(root, "pnpm-workspace.yaml");
1099
- const manifest = join6(root, "package.json");
1100
- const patterns = existsSync8(pnpm) ? pnpmPatterns(pnpm) : existsSync8(manifest) ? npmPatterns(manifest) : [];
1101
- const dirs = patterns.filter((pattern) => !pattern.startsWith("!")).flatMap((pattern) => expand(root, pattern)).filter((dir) => existsSync8(join6(dir, "package.json")));
1210
+ const root = resolve11(cwd);
1211
+ const pnpm = join7(root, "pnpm-workspace.yaml");
1212
+ const manifest = join7(root, "package.json");
1213
+ const patterns = existsSync9(pnpm) ? pnpmPatterns(pnpm) : existsSync9(manifest) ? npmPatterns(manifest) : [];
1214
+ const dirs = patterns.filter((pattern) => !pattern.startsWith("!")).flatMap((pattern) => expand(root, pattern)).filter((dir) => existsSync9(join7(dir, "package.json")));
1102
1215
  return [...new Set(dirs)];
1103
1216
  };
1104
1217
  var manifestOf = (dir) => {
1105
1218
  try {
1106
- const parsed = JSON.parse(readFileSync9(join6(dir, "package.json"), "utf8"));
1219
+ const parsed = JSON.parse(readFileSync10(join7(dir, "package.json"), "utf8"));
1107
1220
  return typeof parsed === "object" && parsed !== null ? parsed : void 0;
1108
1221
  } catch {
1109
1222
  return void 0;
@@ -1133,7 +1246,7 @@ var holdsTests = (dir, depth = 6) => {
1133
1246
  if (entry.isDirectory()) {
1134
1247
  if (entry.name === TEST_DIR) return true;
1135
1248
  if (SKIP2.test(entry.name) || depth === 0) continue;
1136
- if (holdsTests(join7(dir, entry.name), depth - 1)) return true;
1249
+ if (holdsTests(join8(dir, entry.name), depth - 1)) return true;
1137
1250
  continue;
1138
1251
  }
1139
1252
  if (TEST_FILE.test(entry.name)) return true;
@@ -1192,8 +1305,8 @@ var sumOfCounts = {
1192
1305
  };
1193
1306
 
1194
1307
  // src/counters/suppressions.ts
1195
- import { readdirSync as readdirSync4, readFileSync as readFileSync10 } from "fs";
1196
- import { join as join8, resolve as resolve11 } from "path";
1308
+ import { readdirSync as readdirSync4, readFileSync as readFileSync11 } from "fs";
1309
+ import { join as join9, resolve as resolve12 } from "path";
1197
1310
  var CODE_FILE = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts|vue|svelte|astro)$/;
1198
1311
  var SKIP3 = /^(?:node_modules|dist|coverage|\.[^.]|__fixtures__)/;
1199
1312
  var SUPPRESSION = /(?:eslint|oxlint|biome)-(?:disable|ignore)(?:-next-line|-line)?|@ts-expect-error|@ts-ignore/g;
@@ -1208,12 +1321,12 @@ var countIn = (dir, depth = 12) => {
1208
1321
  for (const entry of entries) {
1209
1322
  if (SKIP3.test(entry.name)) continue;
1210
1323
  if (entry.isDirectory()) {
1211
- if (depth > 0) found += countIn(join8(dir, entry.name), depth - 1);
1324
+ if (depth > 0) found += countIn(join9(dir, entry.name), depth - 1);
1212
1325
  continue;
1213
1326
  }
1214
1327
  if (!CODE_FILE.test(entry.name)) continue;
1215
1328
  try {
1216
- found += readFileSync10(join8(dir, entry.name), "utf8").match(SUPPRESSION)?.length ?? 0;
1329
+ found += readFileSync11(join9(dir, entry.name), "utf8").match(SUPPRESSION)?.length ?? 0;
1217
1330
  } catch {
1218
1331
  }
1219
1332
  }
@@ -1227,14 +1340,14 @@ var suppressionCount = {
1227
1340
  },
1228
1341
  run: async ({ cwd, params }) => {
1229
1342
  const roots = stringsParam(params, "roots", ["."]);
1230
- return roots.reduce((sum, root) => sum + countIn(resolve11(cwd, root)), 0);
1343
+ return roots.reduce((sum, root) => sum + countIn(resolve12(cwd, root)), 0);
1231
1344
  }
1232
1345
  };
1233
1346
 
1234
1347
  // src/counters/tests.ts
1235
- import { existsSync as existsSync9, mkdtempSync as mkdtempSync2, readFileSync as readFileSync11, rmSync as rmSync4 } from "fs";
1348
+ import { existsSync as existsSync10, mkdtempSync as mkdtempSync2, readFileSync as readFileSync12, rmSync as rmSync4 } from "fs";
1236
1349
  import { tmpdir as tmpdir2 } from "os";
1237
- import { join as join9, resolve as resolve12 } from "path";
1350
+ import { join as join10, resolve as resolve13 } from "path";
1238
1351
  var COUNTER = "testFailures";
1239
1352
  var VITEST_LINE = /^\s*Tests {2,}(.+?)\s*$/;
1240
1353
  var VITEST_TOTAL = /\(\d+\)$/;
@@ -1266,7 +1379,7 @@ ${output.trim().slice(-500)}`
1266
1379
  );
1267
1380
  };
1268
1381
  var fromReport = (path) => {
1269
- if (!existsSync9(path)) {
1382
+ if (!existsSync10(path)) {
1270
1383
  throw new CounterError(
1271
1384
  COUNTER,
1272
1385
  `the runner wrote no report at ${path} \u2014 a crash before the reporter is not a pass`
@@ -1274,7 +1387,7 @@ var fromReport = (path) => {
1274
1387
  }
1275
1388
  let report;
1276
1389
  try {
1277
- report = JSON.parse(readFileSync11(path, "utf8"));
1390
+ report = JSON.parse(readFileSync12(path, "utf8"));
1278
1391
  } catch (error) {
1279
1392
  throw new CounterError(
1280
1393
  COUNTER,
@@ -1298,14 +1411,14 @@ var fromReport = (path) => {
1298
1411
  };
1299
1412
  var reportPathFor = (cwd, params, command) => {
1300
1413
  const named = params.reportPath;
1301
- if (typeof named === "string" && named !== "") return { own: false, path: resolve12(cwd, named) };
1414
+ if (typeof named === "string" && named !== "") return { own: false, path: resolve13(cwd, named) };
1302
1415
  if (!command.includes(PLACEHOLDER)) {
1303
1416
  throw new CounterError(
1304
1417
  COUNTER,
1305
1418
  `report: "${VITEST_JSON}" needs somewhere to put the report \u2014 write ${PLACEHOLDER} into the command (--outputFile=${PLACEHOLDER}) or give the entry a "reportPath"`
1306
1419
  );
1307
1420
  }
1308
- return { own: true, path: join9(mkdtempSync2(join9(tmpdir2(), "geonosis-report-")), "report.json") };
1421
+ return { own: true, path: join10(mkdtempSync2(join10(tmpdir2(), "geonosis-report-")), "report.json") };
1309
1422
  };
1310
1423
  var testFailures = {
1311
1424
  id: COUNTER,
@@ -1354,8 +1467,74 @@ var testFailures = {
1354
1467
  run(command.replaceAll(PLACEHOLDER, path));
1355
1468
  return fromReport(path);
1356
1469
  } finally {
1357
- if (own) rmSync4(join9(path, ".."), { force: true, recursive: true });
1470
+ if (own) rmSync4(join10(path, ".."), { force: true, recursive: true });
1471
+ }
1472
+ }
1473
+ };
1474
+
1475
+ // src/counters/todos.ts
1476
+ import { existsSync as existsSync11, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "fs";
1477
+ import { join as join11, resolve as resolve14 } from "path";
1478
+ var CODE_FILE2 = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts|vue|svelte|astro)$/;
1479
+ var SKIP4 = /^(?:node_modules|dist|coverage|\.[^.]|__fixtures__)/;
1480
+ var PLAN_FILE = /^(\d{3,})-[a-z\d][a-z\d.-]*\.md$/;
1481
+ var plansIn = (dir) => {
1482
+ if (!existsSync11(dir)) return /* @__PURE__ */ new Set();
1483
+ const found = /* @__PURE__ */ new Set();
1484
+ for (const name of readdirSync5(dir)) {
1485
+ const number = PLAN_FILE.exec(name)?.[1];
1486
+ if (number !== void 0) found.add(String(Number(number)));
1487
+ }
1488
+ return found;
1489
+ };
1490
+ var countIn2 = (dir, marker, plans, depth = 12) => {
1491
+ let entries;
1492
+ try {
1493
+ entries = readdirSync5(dir, { withFileTypes: true });
1494
+ } catch {
1495
+ return 0;
1496
+ }
1497
+ let found = 0;
1498
+ for (const entry of entries) {
1499
+ if (SKIP4.test(entry.name)) continue;
1500
+ if (entry.isDirectory()) {
1501
+ if (depth > 0) found += countIn2(join11(dir, entry.name), marker, plans, depth - 1);
1502
+ continue;
1358
1503
  }
1504
+ if (!CODE_FILE2.test(entry.name)) continue;
1505
+ let text = "";
1506
+ try {
1507
+ text = readFileSync13(join11(dir, entry.name), "utf8");
1508
+ } catch {
1509
+ continue;
1510
+ }
1511
+ for (const match of text.matchAll(marker)) {
1512
+ const cited = match[1];
1513
+ if (cited === void 0 || !plans.has(String(Number(cited)))) found += 1;
1514
+ }
1515
+ }
1516
+ return found;
1517
+ };
1518
+ var orphanTodos = {
1519
+ id: "orphanTodos",
1520
+ probe: {
1521
+ expect: 2,
1522
+ input: (dir) => plant(
1523
+ dir,
1524
+ "src/planted.ts",
1525
+ "// TODO: no plan named at all\n// TODO(099): a plan nobody wrote\nexport const a = 1\n"
1526
+ )
1527
+ },
1528
+ readingParams: ["markers"],
1529
+ run: async ({ cwd, params }) => {
1530
+ const markers = stringsParam(params, "markers", ["TODO", "FIXME"]);
1531
+ const plans = plansIn(resolve14(cwd, stringParam("orphanTodos", params, "plans", "plans")));
1532
+ const marker = new RegExp(
1533
+ `\\b(?:${markers.map(escapeForRegex).join("|")})\\b(?:\\((\\d+)\\))?`,
1534
+ "g"
1535
+ );
1536
+ const roots = stringsParam(params, "roots", ["."]);
1537
+ return roots.reduce((sum, root) => sum + countIn2(resolve14(cwd, root), marker, plans), 0);
1359
1538
  }
1360
1539
  };
1361
1540
 
@@ -1394,8 +1573,8 @@ var typecheckErrors = {
1394
1573
  };
1395
1574
 
1396
1575
  // src/counters/walk.ts
1397
- import { existsSync as existsSync10, readFileSync as readFileSync12 } from "fs";
1398
- import { resolve as resolve13 } from "path";
1576
+ import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
1577
+ import { resolve as resolve15 } from "path";
1399
1578
  var DEFAULT_REPORT2 = ".geonosis/walk-report.json";
1400
1579
  var CLASSES = /* @__PURE__ */ new Set([
1401
1580
  "buy-box-above-fold",
@@ -1439,8 +1618,8 @@ var walkFindings = {
1439
1618
  },
1440
1619
  run: async ({ cwd, params }) => {
1441
1620
  const relative = stringParam("walkFindings", params, "report", DEFAULT_REPORT2);
1442
- const path = resolve13(cwd, relative);
1443
- if (!existsSync10(path)) {
1621
+ const path = resolve15(cwd, relative);
1622
+ if (!existsSync12(path)) {
1444
1623
  throw new CounterError(
1445
1624
  "walkFindings",
1446
1625
  `no walk report at ${relative} \u2014 run \`geonosis-walk\` before measuring it`
@@ -1448,7 +1627,7 @@ var walkFindings = {
1448
1627
  }
1449
1628
  let report;
1450
1629
  try {
1451
- report = JSON.parse(readFileSync12(path, "utf8"));
1630
+ report = JSON.parse(readFileSync14(path, "utf8"));
1452
1631
  } catch (error) {
1453
1632
  throw new CounterError(
1454
1633
  "walkFindings",
@@ -1482,7 +1661,9 @@ var COUNTERS = [
1482
1661
  oxlintErrors,
1483
1662
  oxlintRule,
1484
1663
  oxlintWarnings,
1664
+ orphanTodos,
1485
1665
  packagesWithoutTypecheck,
1666
+ probelessRules,
1486
1667
  runtimeCodeShipped,
1487
1668
  sumOfCounts,
1488
1669
  testFailures,
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  runRatchet,
8
8
  versionOf,
9
9
  writeEnvelope
10
- } from "./chunk-ESN6ULZZ.js";
10
+ } from "./chunk-H4PLCM5J.js";
11
11
 
12
12
  // src/cli.ts
13
13
  import process from "process";
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  runRatchet,
19
19
  versionOf,
20
20
  writeEnvelope
21
- } from "./chunk-ESN6ULZZ.js";
21
+ } from "./chunk-H4PLCM5J.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": "1.4.0",
3
+ "version": "2.1.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": [