@geonosis/ratchet 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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)}
@@ -153,11 +162,11 @@ var acquireExclusive = async ({
153
162
 
154
163
  // src/core/config.ts
155
164
  import { existsSync, readFileSync as readFileSync3 } from "fs";
156
- import { resolve } from "path";
165
+ import { resolve as resolve2 } from "path";
157
166
  var CONFIG_FILE = "geonosis.ratchet.json";
158
167
  var keyOf = (entry) => entry.key ?? entry.counter;
159
168
  var loadConfig = (cwd) => {
160
- const path = resolve(cwd, CONFIG_FILE);
169
+ const path = resolve2(cwd, CONFIG_FILE);
161
170
  if (!existsSync(path)) {
162
171
  throw new Error(`no ${CONFIG_FILE} in ${cwd} \u2014 the ratchet has nothing to count`);
163
172
  }
@@ -189,7 +198,7 @@ var loadConfig = (cwd) => {
189
198
  };
190
199
  var resolveBaseline = (cwd) => {
191
200
  try {
192
- const parsed = JSON.parse(readFileSync3(resolve(cwd, CONFIG_FILE), "utf8"));
201
+ const parsed = JSON.parse(readFileSync3(resolve2(cwd, CONFIG_FILE), "utf8"));
193
202
  return typeof parsed.baseline === "string" ? parsed.baseline : "gate-baseline.json";
194
203
  } catch {
195
204
  return "gate-baseline.json";
@@ -237,11 +246,11 @@ ${output.trim()}`
237
246
  // src/core/prove.ts
238
247
  import { existsSync as existsSync2, mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
239
248
  import { tmpdir } from "os";
240
- import { delimiter, dirname as dirname4, join as join3, resolve as resolve3 } from "path";
249
+ import { delimiter, dirname as dirname4, join as join3, resolve as resolve4 } from "path";
241
250
 
242
251
  // src/core/exclusive.ts
243
252
  import { spawn } from "child_process";
244
- import { dirname as dirname3, resolve as resolve2 } from "path";
253
+ import { dirname as dirname3, resolve as resolve3 } from "path";
245
254
  var MARK = /^exclusive-hold (start|end) (\d+)$/gm;
246
255
  var KEY = "exclusive";
247
256
  var spanOf = (output) => {
@@ -256,7 +265,7 @@ var hold = (cli, holdMs, lockPath) => new Promise((done) => {
256
265
  process.execPath,
257
266
  [cli, "--exclusive", "--exclusive-timeout", "60", "--hold", String(holdMs)],
258
267
  {
259
- cwd: dirname3(resolve2(cli)),
268
+ cwd: dirname3(resolve3(cli)),
260
269
  env: { ...process.env, GEONOSIS_HEAVY_LOCK: lockPath },
261
270
  stdio: ["ignore", "pipe", "pipe"]
262
271
  }
@@ -306,7 +315,7 @@ ${runs.map((one) => `exit ${String(one.code)}: ${one.output.trim().slice(-300)}`
306
315
  // src/core/prove.ts
307
316
  var toolPath = (cwd) => {
308
317
  const dirs = [];
309
- let dir = resolve3(cwd);
318
+ let dir = resolve4(cwd);
310
319
  for (; ; ) {
311
320
  const bin = join3(dir, "node_modules", ".bin");
312
321
  if (existsSync2(bin)) dirs.push(bin);
@@ -433,7 +442,7 @@ var runProve = async ({
433
442
 
434
443
  // src/core/ratchet.ts
435
444
  import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
436
- import { resolve as resolve4 } from "path";
445
+ import { resolve as resolve5 } from "path";
437
446
  var EVIDENCE_LINES = 10;
438
447
  var recorded = (counter, params, run) => {
439
448
  let output = "";
@@ -480,7 +489,7 @@ var runRatchet = async ({
480
489
  tier
481
490
  }) => {
482
491
  const config = loadConfig(cwd);
483
- const baselinePath = resolve4(cwd, config.baseline);
492
+ const baselinePath = resolve5(cwd, config.baseline);
484
493
  if (!existsSync3(baselinePath)) {
485
494
  throw new Error(`no ${config.baseline} in ${cwd} \u2014 nothing to ratchet against`);
486
495
  }
@@ -649,7 +658,7 @@ var bundleBytes = {
649
658
 
650
659
  // src/counters/ci.ts
651
660
  import { readdirSync, readFileSync as readFileSync5 } from "fs";
652
- import { join as join5, resolve as resolve5 } from "path";
661
+ import { join as join5, resolve as resolve6 } from "path";
653
662
  var DISABLED = /\bif:[ \t]*false\b/;
654
663
  var WORKFLOW = /\.ya?ml$/;
655
664
  var disabledCiJobs = {
@@ -664,7 +673,7 @@ var disabledCiJobs = {
664
673
  },
665
674
  run: async ({ cwd, params }) => {
666
675
  const relative = stringParam("disabledCiJobs", params, "dir", ".github/workflows");
667
- const dir = resolve5(cwd, relative);
676
+ const dir = resolve6(cwd, relative);
668
677
  let files;
669
678
  try {
670
679
  files = readdirSync(dir, { withFileTypes: true }).filter((entry) => WORKFLOW.test(entry.name)).map((entry) => join5(dir, entry.name));
@@ -722,12 +731,40 @@ var excusesIn = (params) => {
722
731
  typeof declared === "object" && declared !== null && !Array.isArray(declared) ? Object.keys(declared) : []
723
732
  );
724
733
  };
734
+ var HEADING_LINE = /^([A-Z][A-Za-z ]+?) \((\d+)\)\s*$/;
735
+ var UNRESOLVED = "Unresolved imports";
736
+ var KNIP_DEFAULT = "npx knip --reporter compact";
737
+ var knipEvidence = ({
738
+ output,
739
+ params
740
+ }) => {
741
+ const command = typeof params["command"] === "string" ? params["command"] : KNIP_DEFAULT;
742
+ const said = [];
743
+ let heading;
744
+ for (const line of output.split("\n")) {
745
+ const found = HEADING_LINE.exec(line);
746
+ if (found !== null) {
747
+ heading = found[1];
748
+ said.push(line.trim());
749
+ continue;
750
+ }
751
+ if (heading === void 0 || line.trim() === "") continue;
752
+ said.push(` ${line.trim()}`);
753
+ if (heading === UNRESOLVED) {
754
+ said.push(
755
+ ` 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`
756
+ );
757
+ }
758
+ }
759
+ return said;
760
+ };
725
761
  var knipIssues = {
762
+ evidence: knipEvidence,
726
763
  id: "knipIssues",
727
764
  probe: { ...captured("Unused files (1)\nsrc/gone.ts\n"), expect: 1 },
728
765
  readingParams: ["headings"],
729
766
  run: async ({ params, run }) => {
730
- const command = stringParam("knipIssues", params, "command", "npx knip --reporter compact");
767
+ const command = stringParam("knipIssues", params, "command", KNIP_DEFAULT);
731
768
  const printed = headingsIn(run(command).output);
732
769
  const counted = stringsParam(params, "headings", [...printed.keys()]);
733
770
  const excused = excusesIn(params);
@@ -755,7 +792,7 @@ var boundaryIssues = {
755
792
 
756
793
  // src/counters/format.ts
757
794
  import { existsSync as existsSync4 } from "fs";
758
- import { resolve as resolve6 } from "path";
795
+ import { resolve as resolve7 } from "path";
759
796
  var unformattedFiles = {
760
797
  id: "unformattedFiles",
761
798
  probe: {
@@ -770,13 +807,13 @@ var unformattedFiles = {
770
807
  "command",
771
808
  "npx oxfmt --config .oxfmtrc.json --list-different ."
772
809
  );
773
- return run(command).output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && existsSync4(resolve6(cwd, line))).length;
810
+ return run(command).output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && existsSync4(resolve7(cwd, line))).length;
774
811
  }
775
812
  };
776
813
 
777
814
  // src/counters/gate-report.ts
778
815
  import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
779
- import { resolve as resolve7 } from "path";
816
+ import { resolve as resolve8 } from "path";
780
817
  var DEFAULT_REPORT = ".geonosis/gate-report.json";
781
818
  var fastTierMs = {
782
819
  id: "fastTierMs",
@@ -799,8 +836,8 @@ var fastTierMs = {
799
836
  const wanted = stringParam("fastTierMs", params, "tier", "fast");
800
837
  const own = `.geonosis/gate-report.${wanted}.json`;
801
838
  const asked = stringParam("fastTierMs", params, "report", "");
802
- const relative = asked !== "" ? asked : existsSync5(resolve7(cwd, own)) ? own : DEFAULT_REPORT;
803
- const path = resolve7(cwd, relative);
839
+ const relative = asked !== "" ? asked : existsSync5(resolve8(cwd, own)) ? own : DEFAULT_REPORT;
840
+ const path = resolve8(cwd, relative);
804
841
  if (!existsSync5(path)) {
805
842
  throw new CounterError(
806
843
  "fastTierMs",
@@ -832,7 +869,7 @@ var fastTierMs = {
832
869
 
833
870
  // src/counters/law.ts
834
871
  import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
835
- import { resolve as resolve8 } from "path";
872
+ import { resolve as resolve9 } from "path";
836
873
  var lawLineCount = {
837
874
  id: "lawLineCount",
838
875
  probe: {
@@ -842,7 +879,7 @@ var lawLineCount = {
842
879
  },
843
880
  run: async ({ cwd, params }) => {
844
881
  const relative = stringParam("lawLineCount", params, "path", "CLAUDE.md");
845
- const path = resolve8(cwd, relative);
882
+ const path = resolve9(cwd, relative);
846
883
  if (!existsSync6(path)) throw new CounterError("lawLineCount", `no law file at ${relative}`);
847
884
  return readFileSync7(path, "utf8").replace(/\n$/, "").split("\n").length;
848
885
  }
@@ -850,7 +887,7 @@ var lawLineCount = {
850
887
 
851
888
  // src/counters/oxlint.ts
852
889
  import { existsSync as existsSync7, readFileSync as readFileSync8, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
853
- import { resolve as resolve9 } from "path";
890
+ import { resolve as resolve10 } from "path";
854
891
  var DEFAULT_COMMAND = "npx oxlint --format=unix --config .oxlintrc.json .";
855
892
  var PROBE_COMMAND = "oxlint --format=unix --config .oxlintrc.json .";
856
893
  var oxlintProbe = (severity, rule, source) => ({
@@ -1013,18 +1050,18 @@ var oxlintRule = {
1013
1050
  const config = typeof params.config === "string" ? params.config : "";
1014
1051
  const expect = expectedFormat("oxlintRule", params);
1015
1052
  if (config === "") return countRule(run(command), rule, expect);
1016
- const source = resolve9(cwd, config);
1053
+ const source = resolve10(cwd, config);
1017
1054
  if (!existsSync7(source)) throw new CounterError("oxlintRule", `no config at ${config}`);
1018
1055
  const strictName = `.oxlintrc.ratchet-${key}.json`;
1019
1056
  const strict = readFileSync8(source, "utf8").replace(
1020
1057
  new RegExp(`("[^"]*${escapeForRegex(rule)}"\\s*:\\s*\\[?\\s*)"(warn|off)"`),
1021
1058
  '$1"error"'
1022
1059
  );
1023
- writeFileSync6(resolve9(cwd, strictName), strict);
1060
+ writeFileSync6(resolve10(cwd, strictName), strict);
1024
1061
  try {
1025
1062
  return countRule(run(command.replace("{config}", strictName)), rule, expect);
1026
1063
  } finally {
1027
- rmSync3(resolve9(cwd, strictName), { force: true });
1064
+ rmSync3(resolve10(cwd, strictName), { force: true });
1028
1065
  }
1029
1066
  }
1030
1067
  };
@@ -1032,7 +1069,7 @@ var oxlintRule = {
1032
1069
  // src/counters/probes.ts
1033
1070
  import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
1034
1071
  import { createRequire } from "module";
1035
- import { join as join6, resolve as resolve10 } from "path";
1072
+ import { join as join6, resolve as resolve11 } from "path";
1036
1073
  import { pathToFileURL } from "url";
1037
1074
  var ID = "probelessRules";
1038
1075
  var OFF = /* @__PURE__ */ new Set([0, "0", "allow", "off", false]);
@@ -1110,7 +1147,7 @@ var probelessRules = {
1110
1147
  },
1111
1148
  run: async ({ cwd, params }) => {
1112
1149
  const relative = stringParam(ID, params, "config", ".oxlintrc.json");
1113
- const path = resolve10(cwd, relative);
1150
+ const path = resolve11(cwd, relative);
1114
1151
  if (!existsSync8(path)) throw new CounterError(ID, `no oxlint config at ${relative}`);
1115
1152
  let config;
1116
1153
  try {
@@ -1152,7 +1189,7 @@ import { join as join8 } from "path";
1152
1189
 
1153
1190
  // src/counters/workspace.ts
1154
1191
  import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync10 } from "fs";
1155
- import { join as join7, resolve as resolve11 } from "path";
1192
+ import { join as join7, resolve as resolve12 } from "path";
1156
1193
  var SKIP = /^(node_modules|\.)/;
1157
1194
  var childDirs = (dir) => {
1158
1195
  try {
@@ -1207,7 +1244,7 @@ var nameOf = (dir) => {
1207
1244
  }
1208
1245
  };
1209
1246
  var workspaceDirs = (cwd) => {
1210
- const root = resolve11(cwd);
1247
+ const root = resolve12(cwd);
1211
1248
  const pnpm = join7(root, "pnpm-workspace.yaml");
1212
1249
  const manifest = join7(root, "package.json");
1213
1250
  const patterns = existsSync9(pnpm) ? pnpmPatterns(pnpm) : existsSync9(manifest) ? npmPatterns(manifest) : [];
@@ -1283,6 +1320,56 @@ var packagesWithoutTypecheck = {
1283
1320
  }
1284
1321
  };
1285
1322
 
1323
+ // 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";
1326
+ var CONFIG = "geonosis.json";
1327
+ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".turbo", "coverage", "dist", "node_modules"]);
1328
+ var asPattern = (glob) => new RegExp(
1329
+ `^${glob.split("/").map(
1330
+ (part) => part === "**" ? ".*" : part.replaceAll(/[.+^${}()|[\]\\]/g, String.raw`\$&`).replaceAll("*", "[^/]*")
1331
+ ).join("/").replaceAll(".*/", "(?:.*/)?")}$`
1332
+ );
1333
+ var filesUnder = (root, at = root) => readdirSync4(at, { withFileTypes: true }).flatMap((entry) => {
1334
+ if (SKIP_DIRS.has(entry.name)) return [];
1335
+ const full = join9(at, entry.name);
1336
+ if (entry.isDirectory()) return filesUnder(root, full);
1337
+ return entry.isFile() ? [full.slice(root.length + 1)] : [];
1338
+ });
1339
+ var seamGlobsIn = (root) => {
1340
+ const at = join9(root, CONFIG);
1341
+ if (!existsSync10(at)) return [];
1342
+ try {
1343
+ const seams = JSON.parse(readFileSync11(at, "utf8")).adoption?.seams;
1344
+ return Array.isArray(seams) ? seams.filter((one) => typeof one === "string") : [];
1345
+ } catch {
1346
+ return [];
1347
+ }
1348
+ };
1349
+ var linesIn = (path) => {
1350
+ const body = readFileSync11(path, "utf8");
1351
+ return body === "" ? 0 : body.replace(/\n$/, "").split("\n").length;
1352
+ };
1353
+ var adoptionSeamLines = {
1354
+ id: "adoptionSeamLines",
1355
+ probe: {
1356
+ expect: 3,
1357
+ input: (dir) => {
1358
+ plant(dir, CONFIG, `${JSON.stringify({ adoption: { seams: ["src/**/*.ts"] } })}
1359
+ `);
1360
+ plant(dir, "src/adapter.ts", "one\ntwo\nthree\n");
1361
+ plant(dir, "src/not-a-seam.md", "four\nfive\n");
1362
+ },
1363
+ params: {}
1364
+ },
1365
+ run: async ({ cwd }) => {
1366
+ const root = resolve13(cwd);
1367
+ const globs = seamGlobsIn(root).map(asPattern);
1368
+ if (globs.length === 0) return 0;
1369
+ return filesUnder(root).filter((one) => globs.some((pattern) => pattern.test(one))).reduce((total2, one) => total2 + linesIn(join9(root, one)), 0);
1370
+ }
1371
+ };
1372
+
1286
1373
  // src/counters/sum-of-counts.ts
1287
1374
  var sumOfCounts = {
1288
1375
  id: "sumOfCounts",
@@ -1305,15 +1392,15 @@ var sumOfCounts = {
1305
1392
  };
1306
1393
 
1307
1394
  // src/counters/suppressions.ts
1308
- import { readdirSync as readdirSync4, readFileSync as readFileSync11 } from "fs";
1309
- import { join as join9, resolve as resolve12 } from "path";
1395
+ import { readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
1396
+ import { join as join10, resolve as resolve14 } from "path";
1310
1397
  var CODE_FILE = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts|vue|svelte|astro)$/;
1311
1398
  var SKIP3 = /^(?:node_modules|dist|coverage|\.[^.]|__fixtures__)/;
1312
1399
  var SUPPRESSION = /(?:eslint|oxlint|biome)-(?:disable|ignore)(?:-next-line|-line)?|@ts-expect-error|@ts-ignore/g;
1313
1400
  var countIn = (dir, depth = 12) => {
1314
1401
  let entries;
1315
1402
  try {
1316
- entries = readdirSync4(dir, { withFileTypes: true });
1403
+ entries = readdirSync5(dir, { withFileTypes: true });
1317
1404
  } catch {
1318
1405
  return 0;
1319
1406
  }
@@ -1321,12 +1408,12 @@ var countIn = (dir, depth = 12) => {
1321
1408
  for (const entry of entries) {
1322
1409
  if (SKIP3.test(entry.name)) continue;
1323
1410
  if (entry.isDirectory()) {
1324
- if (depth > 0) found += countIn(join9(dir, entry.name), depth - 1);
1411
+ if (depth > 0) found += countIn(join10(dir, entry.name), depth - 1);
1325
1412
  continue;
1326
1413
  }
1327
1414
  if (!CODE_FILE.test(entry.name)) continue;
1328
1415
  try {
1329
- found += readFileSync11(join9(dir, entry.name), "utf8").match(SUPPRESSION)?.length ?? 0;
1416
+ found += readFileSync12(join10(dir, entry.name), "utf8").match(SUPPRESSION)?.length ?? 0;
1330
1417
  } catch {
1331
1418
  }
1332
1419
  }
@@ -1340,14 +1427,107 @@ var suppressionCount = {
1340
1427
  },
1341
1428
  run: async ({ cwd, params }) => {
1342
1429
  const roots = stringsParam(params, "roots", ["."]);
1343
- return roots.reduce((sum, root) => sum + countIn(resolve12(cwd, root)), 0);
1430
+ return roots.reduce((sum, root) => sum + countIn(resolve14(cwd, root)), 0);
1431
+ }
1432
+ };
1433
+
1434
+ // src/counters/surface.ts
1435
+ import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
1436
+ import { join as join11, resolve as resolve15 } from "path";
1437
+ var DECLARED = /^export\s+(?:declare\s+)?(?:abstract\s+)?(?:const|function|class|type|interface|enum|let|var)\s+([$\w]+)/;
1438
+ var BRACED = /^export\s+(?:type\s+)?\{([^}]*)\}(?:\s+from\s+(['"])(.+?)\2)?/;
1439
+ var nameOf2 = (spec) => {
1440
+ const named = spec.trim().replace(/^type\s+/, "");
1441
+ if (named === "") return void 0;
1442
+ const parts = named.split(/\s+as\s+/);
1443
+ return (parts.at(-1) ?? "").trim() || void 0;
1444
+ };
1445
+ var exportedSurfaceIn = (source) => {
1446
+ const found = /* @__PURE__ */ new Map();
1447
+ for (const line of source.split("\n")) {
1448
+ const declared = DECLARED.exec(line);
1449
+ if (declared !== null) {
1450
+ found.set(declared[1] ?? "", line.trim());
1451
+ continue;
1452
+ }
1453
+ const braced = BRACED.exec(line);
1454
+ if (braced === null) continue;
1455
+ const from = braced[3];
1456
+ for (const spec of (braced[1] ?? "").split(",")) {
1457
+ const name = nameOf2(spec);
1458
+ if (name === void 0) continue;
1459
+ found.set(
1460
+ name,
1461
+ from === void 0 ? `export { ${name} }` : `export { ${name} } from '${from}'`
1462
+ );
1463
+ }
1464
+ }
1465
+ return found;
1466
+ };
1467
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1468
+ var declarationEntriesOf = (dir) => {
1469
+ const at = join11(dir, "package.json");
1470
+ if (!existsSync11(at)) {
1471
+ throw new CounterError("floorSurface", `no package.json in ${dir} \u2014 that is not a package`);
1472
+ }
1473
+ const manifest = JSON.parse(readFileSync13(at, "utf8"));
1474
+ const found = [];
1475
+ const walk = (value) => {
1476
+ if (typeof value === "string" && value.endsWith(".d.ts")) found.push(value);
1477
+ if (isRecord(value)) for (const one of Object.values(value)) walk(one);
1478
+ };
1479
+ walk({ exports: manifest["exports"], types: manifest["types"] });
1480
+ return [...new Set(found)];
1481
+ };
1482
+ var floorSurface = {
1483
+ evidence: ({ params }) => [`the doors declared by ${String(params["dir"] ?? "this package")}`],
1484
+ id: "floorSurface",
1485
+ probe: {
1486
+ expect: 3,
1487
+ input: (dir) => {
1488
+ plant(
1489
+ dir,
1490
+ "floor/package.json",
1491
+ `${JSON.stringify({ exports: { ".": { types: "./dist/index.d.ts" } }, name: "floor" })}
1492
+ `
1493
+ );
1494
+ plant(
1495
+ dir,
1496
+ "floor/dist/index.d.ts",
1497
+ "export declare const open: () => void;\nexport type Handle = { id: string };\nexport { c as close } from './chunk.js';\ndeclare const hidden: number;\n"
1498
+ );
1499
+ },
1500
+ params: { dir: "floor" }
1501
+ },
1502
+ run: async ({ cwd, params }) => {
1503
+ const relative = stringParam("floorSurface", params, "dir");
1504
+ const dir = resolve15(cwd, relative);
1505
+ const entries = declarationEntriesOf(dir);
1506
+ if (entries.length === 0) {
1507
+ throw new CounterError(
1508
+ "floorSurface",
1509
+ `${relative} promises no .d.ts in its exports map \u2014 a package whose surface cannot be read is not a package this can hold`
1510
+ );
1511
+ }
1512
+ const names = /* @__PURE__ */ new Set();
1513
+ for (const entry of entries) {
1514
+ const at = resolve15(dir, entry);
1515
+ if (!existsSync11(at)) {
1516
+ throw new CounterError(
1517
+ "floorSurface",
1518
+ `${relative} promises ${entry} and it is not there \u2014 build before measuring, or the surface reads as empty`
1519
+ );
1520
+ }
1521
+ for (const name of exportedSurfaceIn(readFileSync13(at, "utf8")).keys()) names.add(name);
1522
+ }
1523
+ return names.size;
1344
1524
  }
1345
1525
  };
1346
1526
 
1347
1527
  // src/counters/tests.ts
1348
- import { existsSync as existsSync10, mkdtempSync as mkdtempSync2, readFileSync as readFileSync12, rmSync as rmSync4 } from "fs";
1528
+ import { existsSync as existsSync12, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, rmSync as rmSync4 } from "fs";
1349
1529
  import { tmpdir as tmpdir2 } from "os";
1350
- import { join as join10, resolve as resolve13 } from "path";
1530
+ import { join as join12, resolve as resolve16 } from "path";
1351
1531
  var COUNTER = "testFailures";
1352
1532
  var VITEST_LINE = /^\s*Tests {2,}(.+?)\s*$/;
1353
1533
  var VITEST_TOTAL = /\(\d+\)$/;
@@ -1379,7 +1559,7 @@ ${output.trim().slice(-500)}`
1379
1559
  );
1380
1560
  };
1381
1561
  var fromReport = (path) => {
1382
- if (!existsSync10(path)) {
1562
+ if (!existsSync12(path)) {
1383
1563
  throw new CounterError(
1384
1564
  COUNTER,
1385
1565
  `the runner wrote no report at ${path} \u2014 a crash before the reporter is not a pass`
@@ -1387,7 +1567,7 @@ var fromReport = (path) => {
1387
1567
  }
1388
1568
  let report;
1389
1569
  try {
1390
- report = JSON.parse(readFileSync12(path, "utf8"));
1570
+ report = JSON.parse(readFileSync14(path, "utf8"));
1391
1571
  } catch (error) {
1392
1572
  throw new CounterError(
1393
1573
  COUNTER,
@@ -1411,14 +1591,14 @@ var fromReport = (path) => {
1411
1591
  };
1412
1592
  var reportPathFor = (cwd, params, command) => {
1413
1593
  const named = params.reportPath;
1414
- if (typeof named === "string" && named !== "") return { own: false, path: resolve13(cwd, named) };
1594
+ if (typeof named === "string" && named !== "") return { own: false, path: resolve16(cwd, named) };
1415
1595
  if (!command.includes(PLACEHOLDER)) {
1416
1596
  throw new CounterError(
1417
1597
  COUNTER,
1418
1598
  `report: "${VITEST_JSON}" needs somewhere to put the report \u2014 write ${PLACEHOLDER} into the command (--outputFile=${PLACEHOLDER}) or give the entry a "reportPath"`
1419
1599
  );
1420
1600
  }
1421
- return { own: true, path: join10(mkdtempSync2(join10(tmpdir2(), "geonosis-report-")), "report.json") };
1601
+ return { own: true, path: join12(mkdtempSync2(join12(tmpdir2(), "geonosis-report-")), "report.json") };
1422
1602
  };
1423
1603
  var testFailures = {
1424
1604
  id: COUNTER,
@@ -1467,21 +1647,21 @@ var testFailures = {
1467
1647
  run(command.replaceAll(PLACEHOLDER, path));
1468
1648
  return fromReport(path);
1469
1649
  } finally {
1470
- if (own) rmSync4(join10(path, ".."), { force: true, recursive: true });
1650
+ if (own) rmSync4(join12(path, ".."), { force: true, recursive: true });
1471
1651
  }
1472
1652
  }
1473
1653
  };
1474
1654
 
1475
1655
  // 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";
1656
+ import { existsSync as existsSync13, readdirSync as readdirSync6, readFileSync as readFileSync15 } from "fs";
1657
+ import { join as join13, resolve as resolve17 } from "path";
1478
1658
  var CODE_FILE2 = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts|vue|svelte|astro)$/;
1479
1659
  var SKIP4 = /^(?:node_modules|dist|coverage|\.[^.]|__fixtures__)/;
1480
1660
  var PLAN_FILE = /^(\d{3,})-[a-z\d][a-z\d.-]*\.md$/;
1481
1661
  var plansIn = (dir) => {
1482
- if (!existsSync11(dir)) return /* @__PURE__ */ new Set();
1662
+ if (!existsSync13(dir)) return /* @__PURE__ */ new Set();
1483
1663
  const found = /* @__PURE__ */ new Set();
1484
- for (const name of readdirSync5(dir)) {
1664
+ for (const name of readdirSync6(dir)) {
1485
1665
  const number = PLAN_FILE.exec(name)?.[1];
1486
1666
  if (number !== void 0) found.add(String(Number(number)));
1487
1667
  }
@@ -1490,7 +1670,7 @@ var plansIn = (dir) => {
1490
1670
  var countIn2 = (dir, marker, plans, depth = 12) => {
1491
1671
  let entries;
1492
1672
  try {
1493
- entries = readdirSync5(dir, { withFileTypes: true });
1673
+ entries = readdirSync6(dir, { withFileTypes: true });
1494
1674
  } catch {
1495
1675
  return 0;
1496
1676
  }
@@ -1498,13 +1678,13 @@ var countIn2 = (dir, marker, plans, depth = 12) => {
1498
1678
  for (const entry of entries) {
1499
1679
  if (SKIP4.test(entry.name)) continue;
1500
1680
  if (entry.isDirectory()) {
1501
- if (depth > 0) found += countIn2(join11(dir, entry.name), marker, plans, depth - 1);
1681
+ if (depth > 0) found += countIn2(join13(dir, entry.name), marker, plans, depth - 1);
1502
1682
  continue;
1503
1683
  }
1504
1684
  if (!CODE_FILE2.test(entry.name)) continue;
1505
1685
  let text = "";
1506
1686
  try {
1507
- text = readFileSync13(join11(dir, entry.name), "utf8");
1687
+ text = readFileSync15(join13(dir, entry.name), "utf8");
1508
1688
  } catch {
1509
1689
  continue;
1510
1690
  }
@@ -1528,22 +1708,22 @@ var orphanTodos = {
1528
1708
  readingParams: ["markers"],
1529
1709
  run: async ({ cwd, params }) => {
1530
1710
  const markers = stringsParam(params, "markers", ["TODO", "FIXME"]);
1531
- const plans = plansIn(resolve14(cwd, stringParam("orphanTodos", params, "plans", "plans")));
1711
+ const plans = plansIn(resolve17(cwd, stringParam("orphanTodos", params, "plans", "plans")));
1532
1712
  const marker = new RegExp(
1533
1713
  `\\b(?:${markers.map(escapeForRegex).join("|")})\\b(?:\\((\\d+)\\))?`,
1534
1714
  "g"
1535
1715
  );
1536
1716
  const roots = stringsParam(params, "roots", ["."]);
1537
- return roots.reduce((sum, root) => sum + countIn2(resolve14(cwd, root), marker, plans), 0);
1717
+ return roots.reduce((sum, root) => sum + countIn2(resolve17(cwd, root), marker, plans), 0);
1538
1718
  }
1539
1719
  };
1540
1720
 
1541
1721
  // src/counters/typecheck.ts
1542
- var UNRESOLVED = /error TS(?:2305|2307): ([^\n]*)/g;
1722
+ var UNRESOLVED2 = /error TS(?:2305|2307): ([^\n]*)/g;
1543
1723
  var SPECIFIER = /'([^']+)'/g;
1544
1724
  var specifiersIn = (message) => [...message.matchAll(SPECIFIER)].map(([, quoted]) => (quoted ?? "").replaceAll('"', ""));
1545
1725
  var unbuiltIn = (cwd, output) => {
1546
- const messages = [...output.matchAll(UNRESOLVED)].map(([, message]) => message ?? "");
1726
+ const messages = [...output.matchAll(UNRESOLVED2)].map(([, message]) => message ?? "");
1547
1727
  if (messages.length === 0) return [];
1548
1728
  const packages = workspacePackageNames(cwd);
1549
1729
  const named = messages.flatMap((message) => specifiersIn(message)).flatMap(
@@ -1573,8 +1753,8 @@ var typecheckErrors = {
1573
1753
  };
1574
1754
 
1575
1755
  // src/counters/walk.ts
1576
- import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
1577
- import { resolve as resolve15 } from "path";
1756
+ import { existsSync as existsSync14, readFileSync as readFileSync16 } from "fs";
1757
+ import { resolve as resolve18 } from "path";
1578
1758
  var DEFAULT_REPORT2 = ".geonosis/walk-report.json";
1579
1759
  var CLASSES = /* @__PURE__ */ new Set([
1580
1760
  "buy-box-above-fold",
@@ -1618,8 +1798,8 @@ var walkFindings = {
1618
1798
  },
1619
1799
  run: async ({ cwd, params }) => {
1620
1800
  const relative = stringParam("walkFindings", params, "report", DEFAULT_REPORT2);
1621
- const path = resolve15(cwd, relative);
1622
- if (!existsSync12(path)) {
1801
+ const path = resolve18(cwd, relative);
1802
+ if (!existsSync14(path)) {
1623
1803
  throw new CounterError(
1624
1804
  "walkFindings",
1625
1805
  `no walk report at ${relative} \u2014 run \`geonosis-walk\` before measuring it`
@@ -1627,7 +1807,7 @@ var walkFindings = {
1627
1807
  }
1628
1808
  let report;
1629
1809
  try {
1630
- report = JSON.parse(readFileSync14(path, "utf8"));
1810
+ report = JSON.parse(readFileSync16(path, "utf8"));
1631
1811
  } catch (error) {
1632
1812
  throw new CounterError(
1633
1813
  "walkFindings",
@@ -1649,12 +1829,14 @@ var walkFindings = {
1649
1829
 
1650
1830
  // src/counters/index.ts
1651
1831
  var COUNTERS = [
1832
+ adoptionSeamLines,
1652
1833
  archViolations,
1653
1834
  boundaryIssues,
1654
1835
  bundleBytes,
1655
1836
  cloneCount,
1656
1837
  disabledCiJobs,
1657
1838
  fastTierMs,
1839
+ floorSurface,
1658
1840
  knipIssues,
1659
1841
  lawLineCount,
1660
1842
  suppressionCount,
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  runRatchet,
8
8
  versionOf,
9
9
  writeEnvelope
10
- } from "./chunk-H4PLCM5J.js";
10
+ } from "./chunk-IQ6TXLWC.js";
11
11
 
12
12
  // src/cli.ts
13
13
  import process from "process";
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-IQ6TXLWC.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.2.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": [