@geonosis/ratchet 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -172,10 +172,17 @@ lines) is refused **even on a clean exit**. `npm ERR!` is not stripped — that
172
172
  command never ran, and it must never read as zero. Unset is the default and changes nothing, so a repo that never opts in keeps exactly
173
173
  the behaviour it had. Valid values: `unix`, `agent`, `default`.
174
174
 
175
- When a number **grew**, the last ten lines of that counter's command output print under the
175
+ When a number **grew**, up to ten lines of that counter's command output print under the
176
176
  `<-- REGRESSED` line, indented — so the report says what grew, not only that something did. A
177
177
  counter that reads a file rather than running a command (`lawLineCount`) prints nothing extra.
178
178
 
179
+ The lines are the ones the counter says its number came from, not the tail of the run. `oxlintRule`
180
+ cites the findings attributed to ITS rule; `oxlintErrors` cites errors and `oxlintWarnings`
181
+ warnings. The tail was wrong for exactly the run that needs it most: a consumer's +2 on one rule
182
+ printed two unrelated **warnings** as its evidence, because those were the last lines oxlint wrote
183
+ and the two real errors sat higher up. A counter written outside this package that names nothing —
184
+ or that recognises nothing in a run — falls back to the tail, as before.
185
+
179
186
  ### `testFailures` reads the runner's summary, never the exit code
180
187
 
181
188
  The counter looks for the runner's own count and **refuses when it cannot parse one**. It never
@@ -281,6 +288,19 @@ been shown to work — and this one guards the running time of every other gate.
281
288
  nothing more. If a child command needs `NODE_OPTIONS` — a TypeScript shim, a loader — put it on
282
289
  the script that invokes `geonosis-ratchet`, not on the counter's own line, and not only in your
283
290
  interactive shell.
291
+ - **`NODE_OPTIONS` in the parent script + a counter that shells out to `pnpm` = the counter dies.**
292
+ The nested `pnpm` INHERITS the option. dielime preloads a TypeScript-5 shim through
293
+ `NODE_OPTIONS` in its `lint` script; run the ratchet from inside that script and the nested pnpm
294
+ goes looking for a `.pnpmfile.mjs` that is not there and exits non-zero. The ratchet refuses —
295
+ correctly, a command that cannot run is never a silent zero — but nothing in the message is near
296
+ the cause. Run the ratchet outside that script, or unset `NODE_OPTIONS` for the nested call:
297
+
298
+ ```json
299
+ { "counter": "archViolations", "command": "env -u NODE_OPTIONS pnpm --silent verify:arch" }
300
+ ```
301
+
302
+ `geonosis-doctor --only drift` asks for this shape by name: a manifest script carrying
303
+ `NODE_OPTIONS` beside a counter whose command shells to `pnpm`.
284
304
  - **A path in a counter's command must be absolute, or `--prove` cannot run it.** A probe runs in a
285
305
  scratch directory with none of your repo in it, so a loader or shim named relatively — `node
286
306
  --import ./scripts/ts5-shim.mjs …` — resolves to nothing there and the counter comes back
@@ -423,3 +443,10 @@ npx geonosis-doctor --baseline-against origin/main --strict
423
443
  ```
424
444
 
425
445
  Apache-2.0.
446
+
447
+ ## The pairing rule (#136)
448
+
449
+ A scoped fast tier (changed files only) is safe exactly when the full tier is TOTAL: every hard cap
450
+ — max-lines, bundle bytes, suppression counts — needs a full-tier counter watching the whole tree,
451
+ or it is a cap in prose that a file can sit over indefinitely. `oxlintRule`, `suppressionCount`,
452
+ `bundleBytes` and friends exist to be that counter.
@@ -124,6 +124,14 @@ var loadConfig = (cwd) => {
124
124
  }
125
125
  return { baseline: parsed.baseline ?? "gate-baseline.json", counters: parsed.counters };
126
126
  };
127
+ var resolveBaseline = (cwd) => {
128
+ try {
129
+ const parsed = JSON.parse(readFileSync2(resolve(cwd, CONFIG_FILE), "utf8"));
130
+ return typeof parsed.baseline === "string" ? parsed.baseline : "gate-baseline.json";
131
+ } catch {
132
+ return "gate-baseline.json";
133
+ }
134
+ };
127
135
 
128
136
  // src/core/types.ts
129
137
  var CounterError = class extends Error {
@@ -136,23 +144,21 @@ var CounterError = class extends Error {
136
144
  };
137
145
 
138
146
  // src/core/shell.ts
139
- import { execSync } from "child_process";
147
+ import { spawnSync } from "child_process";
140
148
  var ANSI = /\[[0-9;]*m/g;
141
149
  var runCommand = (cwd, counterId, env) => (command) => {
142
- try {
143
- const output = execSync(`(
144
- ${command}
145
- ) 2>&1`, {
150
+ {
151
+ const run = spawnSync(command, {
146
152
  cwd,
147
153
  encoding: "utf8",
148
154
  env,
149
155
  maxBuffer: 64 * 1024 * 1024,
156
+ shell: true,
150
157
  stdio: ["ignore", "pipe", "pipe"]
151
158
  });
152
- return { code: 0, output: output.replaceAll(ANSI, "") };
153
- } catch (error) {
154
- const failed = error;
155
- const output = `${failed.stdout ?? ""}${failed.stderr ?? ""}`.replaceAll(ANSI, "");
159
+ const output = `${run.stdout ?? ""}${run.stderr ?? ""}`.replaceAll(ANSI, "");
160
+ if (run.error === void 0 && (run.status ?? 0) === 0) return { code: 0, output };
161
+ const failed = { status: run.status ?? 1, stderr: "", stdout: "" };
156
162
  const code = failed.status ?? -1;
157
163
  if (code === 126 || code === 127 || code === -1) {
158
164
  throw new CounterError(
@@ -334,10 +340,14 @@ var runProve = async ({
334
340
  import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
335
341
  import { resolve as resolve4 } from "path";
336
342
  var EVIDENCE_LINES = 10;
337
- var recorded = (run) => {
343
+ var recorded = (counter, params, run) => {
338
344
  let output = "";
345
+ const trimmed = (lines) => lines.map((line) => line.trimEnd()).filter((line) => line !== "").slice(-EVIDENCE_LINES);
339
346
  return {
340
- last: () => output.split("\n").map((line) => line.trimEnd()).filter((line) => line !== "").slice(-EVIDENCE_LINES),
347
+ cited: () => {
348
+ const counted = counter.evidence === void 0 ? [] : counter.evidence({ output, params });
349
+ return trimmed(counted.length > 0 ? counted : output.split("\n"));
350
+ },
341
351
  run: (command) => {
342
352
  const result = run(command);
343
353
  output = result.output;
@@ -398,12 +408,12 @@ var runRatchet = async ({
398
408
  );
399
409
  }
400
410
  const tolerance = toleranceOf(entry, key, counter);
401
- const recorder = recorded(runCommand(cwd, entry.counter));
411
+ const recorder = recorded(counter, entry, runCommand(cwd, entry.counter));
402
412
  const now = await counter.run({ cwd, key, params: entry, run: recorder.run });
403
413
  const verdict = verdictOf(now, limit, tolerance);
404
414
  measurements.push({
405
415
  baseline: limit,
406
- evidence: verdict === "grew" ? recorder.last() : [],
416
+ evidence: verdict === "grew" ? recorder.cited() : [],
407
417
  key,
408
418
  now,
409
419
  verdict
@@ -646,7 +656,7 @@ var fastTierMs = {
646
656
  expect: 4500,
647
657
  input: (dir) => plant(
648
658
  dir,
649
- DEFAULT_REPORT,
659
+ ".geonosis/gate-report.fast.json",
650
660
  JSON.stringify({
651
661
  finishedAt: "2026-08-30T10:00:04.500Z",
652
662
  ok: true,
@@ -657,8 +667,10 @@ var fastTierMs = {
657
667
  )
658
668
  },
659
669
  run: async ({ cwd, params }) => {
660
- const relative = stringParam("fastTierMs", params, "report", DEFAULT_REPORT);
661
670
  const wanted = stringParam("fastTierMs", params, "tier", "fast");
671
+ const own = `.geonosis/gate-report.${wanted}.json`;
672
+ const asked = stringParam("fastTierMs", params, "report", "");
673
+ const relative = asked !== "" ? asked : existsSync5(resolve7(cwd, own)) ? own : DEFAULT_REPORT;
662
674
  const path = resolve7(cwd, relative);
663
675
  if (!existsSync5(path)) {
664
676
  throw new CounterError(
@@ -678,7 +690,7 @@ var fastTierMs = {
678
690
  if (report.tier !== wanted) {
679
691
  throw new CounterError(
680
692
  "fastTierMs",
681
- `${relative} is a report of tier "${String(report.tier)}", not "${wanted}"`
693
+ `${relative} is a report of tier "${String(report.tier)}", not "${wanted}" \u2014 run \`geonosis-verify ${wanted}\`, which records ${own}`
682
694
  );
683
695
  }
684
696
  const spent = Date.parse(String(report.finishedAt)) - Date.parse(String(report.startedAt));
@@ -729,12 +741,32 @@ var UNIX_FINDING = /^\S[^\n]*:\d+:\d+: .*\[(Error|Warning)\/[^\]\n]+\]$/gm;
729
741
  var UNIX_SUMMARY = /^(\d+) problems?$/m;
730
742
  var AGENT_FINDING = /^\S[^\n]*:\d+:\d+: (error|warning) /gm;
731
743
  var DEFAULT_SUMMARY = /^Found (\d+) warnings? and (\d+) errors?\.$/m;
732
- var DEFAULT_FINDING = /^\s*[x!] [^\s(]+\([^)\n]+\): /m;
744
+ var DEFAULT_FINDING = /^\s*([x!]) [^\s(]+\([^)\n]+\): /m;
733
745
  var FINDING_SHAPES = [UNIX_FINDING, AGENT_FINDING, DEFAULT_FINDING].map(
734
746
  (shape) => new RegExp(shape.source)
735
747
  );
736
748
  var RULE_TOKEN = /\([^()\n]+\)/;
737
- var findingLinesOf = (output) => output.split("\n").filter((line) => FINDING_SHAPES.some((shape) => shape.test(line)));
749
+ var SEVERITY_BY_TOKEN = {
750
+ "!": "warning",
751
+ Error: "error",
752
+ error: "error",
753
+ Warning: "warning",
754
+ warning: "warning",
755
+ x: "error"
756
+ };
757
+ var severityOf = (line) => {
758
+ for (const shape of FINDING_SHAPES) {
759
+ const token = shape.exec(line)?.[1];
760
+ if (token !== void 0) return SEVERITY_BY_TOKEN[token];
761
+ }
762
+ return void 0;
763
+ };
764
+ var findingLinesOf = (output) => output.split("\n").filter((line) => severityOf(line) !== void 0);
765
+ var linesOfSeverity = (output, want) => output.split("\n").filter((line) => severityOf(line) === want);
766
+ var linesOfRule = (output, rule) => {
767
+ const named = new RegExp(`\\(${escapeForRegex(rule)}\\)`);
768
+ return findingLinesOf(output).filter((line) => named.test(line));
769
+ };
738
770
  var FORMATS = ["agent", "default", "unix"];
739
771
  var isFormat = (value) => typeof value === "string" && FORMATS.some((one) => one === value);
740
772
  var expectedFormat = (counter, params) => {
@@ -793,6 +825,7 @@ ${output.trim()}`);
793
825
  return refuse2(`the tool exited ${code} and printed no findings and no summary`);
794
826
  };
795
827
  var oxlintErrors = {
828
+ evidence: ({ output }) => linesOfSeverity(output, "error"),
796
829
  id: "oxlintErrors",
797
830
  probe: {
798
831
  ...oxlintProbe("error", "typescript/no-explicit-any", ONE_ANY),
@@ -806,6 +839,7 @@ var oxlintErrors = {
806
839
  }
807
840
  };
808
841
  var oxlintWarnings = {
842
+ evidence: ({ output }) => linesOfSeverity(output, "warning"),
809
843
  id: "oxlintWarnings",
810
844
  // A warning, so the run exits 0 — the window `expectFormat` exists to close is also the window a
811
845
  // probe has to survive.
@@ -830,10 +864,12 @@ var countRule = (result, rule, expect) => {
830
864
  ${result.output.trim()}`
831
865
  );
832
866
  }
833
- const named = new RegExp(`\\(${escapeForRegex(rule)}\\)`);
834
- return lines.filter((line) => named.test(line)).length;
867
+ return linesOfRule(result.output, rule).length;
835
868
  };
836
869
  var oxlintRule = {
870
+ // The rule is a required param, so a run that got here always has one; a citation is not the
871
+ // place to raise that, and the report falls back to the tail rather than losing the regression.
872
+ evidence: ({ output, params }) => typeof params.rule === "string" ? linesOfRule(output, params.rule) : [],
837
873
  id: "oxlintRule",
838
874
  // The probe names its OWN rule: which rule a repo tracks is its business, and a probe that had to
839
875
  // make the repo's rule fire would need the repo's plugin loadable from a scratch directory.
@@ -1035,10 +1071,50 @@ var sumOfCounts = {
1035
1071
  }
1036
1072
  };
1037
1073
 
1074
+ // src/counters/suppressions.ts
1075
+ import { readdirSync as readdirSync4, readFileSync as readFileSync9 } from "fs";
1076
+ import { join as join7, resolve as resolve11 } from "path";
1077
+ var CODE_FILE = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts|vue|svelte|astro)$/;
1078
+ var SKIP3 = /^(?:node_modules|dist|coverage|\.[^.]|__fixtures__)/;
1079
+ var SUPPRESSION = /(?:eslint|oxlint|biome)-(?:disable|ignore)(?:-next-line|-line)?|@ts-expect-error|@ts-ignore/g;
1080
+ var countIn = (dir, depth = 12) => {
1081
+ let entries;
1082
+ try {
1083
+ entries = readdirSync4(dir, { withFileTypes: true });
1084
+ } catch {
1085
+ return 0;
1086
+ }
1087
+ let found = 0;
1088
+ for (const entry of entries) {
1089
+ if (SKIP3.test(entry.name)) continue;
1090
+ if (entry.isDirectory()) {
1091
+ if (depth > 0) found += countIn(join7(dir, entry.name), depth - 1);
1092
+ continue;
1093
+ }
1094
+ if (!CODE_FILE.test(entry.name)) continue;
1095
+ try {
1096
+ found += readFileSync9(join7(dir, entry.name), "utf8").match(SUPPRESSION)?.length ?? 0;
1097
+ } catch {
1098
+ }
1099
+ }
1100
+ return found;
1101
+ };
1102
+ var suppressionCount = {
1103
+ id: "suppressionCount",
1104
+ probe: {
1105
+ expect: 2,
1106
+ input: (dir) => plant(dir, "src/planted.ts", "// eslint-disable-next-line x\n// @ts-expect-error y\n")
1107
+ },
1108
+ run: async ({ cwd, params }) => {
1109
+ const roots = stringsParam(params, "roots", ["."]);
1110
+ return roots.reduce((sum, root) => sum + countIn(resolve11(cwd, root)), 0);
1111
+ }
1112
+ };
1113
+
1038
1114
  // src/counters/tests.ts
1039
- import { existsSync as existsSync9, mkdtempSync as mkdtempSync2, readFileSync as readFileSync9, rmSync as rmSync4 } from "fs";
1115
+ import { existsSync as existsSync9, mkdtempSync as mkdtempSync2, readFileSync as readFileSync10, rmSync as rmSync4 } from "fs";
1040
1116
  import { tmpdir as tmpdir2 } from "os";
1041
- import { join as join7, resolve as resolve11 } from "path";
1117
+ import { join as join8, resolve as resolve12 } from "path";
1042
1118
  var COUNTER = "testFailures";
1043
1119
  var VITEST_LINE = /^\s*Tests {2,}(.+?)\s*$/;
1044
1120
  var VITEST_TOTAL = /\(\d+\)$/;
@@ -1078,7 +1154,7 @@ var fromReport = (path) => {
1078
1154
  }
1079
1155
  let report;
1080
1156
  try {
1081
- report = JSON.parse(readFileSync9(path, "utf8"));
1157
+ report = JSON.parse(readFileSync10(path, "utf8"));
1082
1158
  } catch (error) {
1083
1159
  throw new CounterError(
1084
1160
  COUNTER,
@@ -1102,14 +1178,14 @@ var fromReport = (path) => {
1102
1178
  };
1103
1179
  var reportPathFor = (cwd, params, command) => {
1104
1180
  const named = params.reportPath;
1105
- if (typeof named === "string" && named !== "") return { own: false, path: resolve11(cwd, named) };
1181
+ if (typeof named === "string" && named !== "") return { own: false, path: resolve12(cwd, named) };
1106
1182
  if (!command.includes(PLACEHOLDER)) {
1107
1183
  throw new CounterError(
1108
1184
  COUNTER,
1109
1185
  `report: "${VITEST_JSON}" needs somewhere to put the report \u2014 write ${PLACEHOLDER} into the command (--outputFile=${PLACEHOLDER}) or give the entry a "reportPath"`
1110
1186
  );
1111
1187
  }
1112
- return { own: true, path: join7(mkdtempSync2(join7(tmpdir2(), "geonosis-report-")), "report.json") };
1188
+ return { own: true, path: join8(mkdtempSync2(join8(tmpdir2(), "geonosis-report-")), "report.json") };
1113
1189
  };
1114
1190
  var testFailures = {
1115
1191
  id: COUNTER,
@@ -1158,7 +1234,7 @@ var testFailures = {
1158
1234
  run(command.replaceAll(PLACEHOLDER, path));
1159
1235
  return fromReport(path);
1160
1236
  } finally {
1161
- if (own) rmSync4(join7(path, ".."), { force: true, recursive: true });
1237
+ if (own) rmSync4(join8(path, ".."), { force: true, recursive: true });
1162
1238
  }
1163
1239
  }
1164
1240
  };
@@ -1198,8 +1274,8 @@ var typecheckErrors = {
1198
1274
  };
1199
1275
 
1200
1276
  // src/counters/walk.ts
1201
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
1202
- import { resolve as resolve12 } from "path";
1277
+ import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
1278
+ import { resolve as resolve13 } from "path";
1203
1279
  var DEFAULT_REPORT2 = ".geonosis/walk-report.json";
1204
1280
  var CLASSES = /* @__PURE__ */ new Set([
1205
1281
  "buy-box-above-fold",
@@ -1243,7 +1319,7 @@ var walkFindings = {
1243
1319
  },
1244
1320
  run: async ({ cwd, params }) => {
1245
1321
  const relative = stringParam("walkFindings", params, "report", DEFAULT_REPORT2);
1246
- const path = resolve12(cwd, relative);
1322
+ const path = resolve13(cwd, relative);
1247
1323
  if (!existsSync10(path)) {
1248
1324
  throw new CounterError(
1249
1325
  "walkFindings",
@@ -1252,7 +1328,7 @@ var walkFindings = {
1252
1328
  }
1253
1329
  let report;
1254
1330
  try {
1255
- report = JSON.parse(readFileSync10(path, "utf8"));
1331
+ report = JSON.parse(readFileSync11(path, "utf8"));
1256
1332
  } catch (error) {
1257
1333
  throw new CounterError(
1258
1334
  "walkFindings",
@@ -1282,6 +1358,7 @@ var COUNTERS = [
1282
1358
  fastTierMs,
1283
1359
  knipIssues,
1284
1360
  lawLineCount,
1361
+ suppressionCount,
1285
1362
  oxlintErrors,
1286
1363
  oxlintRule,
1287
1364
  oxlintWarnings,
@@ -1361,6 +1438,7 @@ export {
1361
1438
  CONFIG_FILE,
1362
1439
  keyOf,
1363
1440
  loadConfig,
1441
+ resolveBaseline,
1364
1442
  CounterError,
1365
1443
  runCommand,
1366
1444
  runProve,
package/dist/cli.js CHANGED
@@ -5,9 +5,80 @@ import {
5
5
  formatReport,
6
6
  runProve,
7
7
  runRatchet
8
- } from "./chunk-LSYVFUP4.js";
8
+ } from "./chunk-HBM6F3GO.js";
9
9
 
10
10
  // src/cli.ts
11
+ import process from "process";
12
+ var USAGE = `geonosis-ratchet [--cwd <dir>] [--tier <name>] [--prove] [--exclusive]
13
+
14
+ Debt as a number that may only shrink. Runs the counters named in geonosis.ratchet.json, compares
15
+ each against gate-baseline.json, and REWRITES the baseline down when a number shrank \u2014 so the win is
16
+ locked in the same commit that earned it.
17
+
18
+ --cwd <dir> the repo to measure (default: the working directory)
19
+ --tier <name> only the counters in that tier (default: every counter)
20
+ --prove plant a finding for each counter and require it to be read
21
+ --exclusive hold the machine-wide heavy lock for the run
22
+ --exclusive-timeout <secs> how long to wait for that lock
23
+ --hold <ms> take the lock, wait, give it back \u2014 the --prove self-test's slow thing
24
+ --help, -h this text
25
+
26
+ Exit codes: 0 no counter grew \xB7 1 a counter grew \xB7 2 the run could not measure.
27
+
28
+ Every flag it does not know is REFUSED. This bin used to let anything unrecognised fall through to a
29
+ full measurement, so \`--help\` ran the whole ratchet \u2014 a run that can rewrite the baseline, started
30
+ by a typo.`;
31
+ var KNOWN = /* @__PURE__ */ new Set([
32
+ "--cwd",
33
+ "--exclusive",
34
+ "--exclusive-timeout",
35
+ "--hold",
36
+ "--prove",
37
+ "--tier"
38
+ ]);
39
+ var TAKES_A_VALUE = /* @__PURE__ */ new Set(["--cwd", "--exclusive-timeout", "--hold", "--tier"]);
40
+ var CONFIG_SHAPE = `geonosis-ratchet reads geonosis.ratchet.json and gate-baseline.json
41
+
42
+ geonosis.ratchet.json
43
+ baseline string? the file holding the numbers (default "gate-baseline.json")
44
+ counters CounterEntry[] required, and every entry is:
45
+ counter string required \u2014 the id of a counter this build ships
46
+ key string? the baseline key it writes (default: the counter id).
47
+ Two entries writing one key are refused: the loser's debt
48
+ would vanish into the baseline
49
+ tiers string[]? the tiers it runs in (default: all of them). A malformed
50
+ list is refused, never quietly matched to no tier
51
+ \u2026 each counter reads its own further keys
52
+
53
+ gate-baseline.json
54
+ <key> number one number per counter key. Written DOWN in place when a
55
+ number shrank, so the win lands in the commit that earned it`;
56
+ var argv = process.argv.slice(2);
57
+ if (argv.includes("--help") || argv.includes("-h")) {
58
+ process.stdout.write(`${USAGE}
59
+ `);
60
+ process.exit(0);
61
+ }
62
+ if (argv.includes("--print-config-shape")) {
63
+ process.stdout.write(`${CONFIG_SHAPE}
64
+ `);
65
+ process.exit(0);
66
+ }
67
+ var refuse = (message) => {
68
+ process.stderr.write(`geonosis-ratchet: ${message}
69
+ `);
70
+ process.exit(2);
71
+ };
72
+ for (let at = 0; at < argv.length; at += 1) {
73
+ const arg = argv[at];
74
+ if (KNOWN.has(arg)) {
75
+ if (TAKES_A_VALUE.has(arg)) at += 1;
76
+ continue;
77
+ }
78
+ refuse(
79
+ `${arg} is not an option it takes. Run geonosis-ratchet --help for the ones it does \u2014 an unrecognised argument is refused rather than measured, because a run here can rewrite the baseline.`
80
+ );
81
+ }
11
82
  var numberAfter = (flag) => {
12
83
  const at = process.argv.indexOf(flag);
13
84
  return at === -1 ? void 0 : Number(process.argv[at + 1] ?? Number.NaN);
package/dist/index.d.ts CHANGED
@@ -41,6 +41,22 @@ type CounterProbe = {
41
41
  params?: Record<string, unknown>;
42
42
  };
43
43
  type Counter = {
44
+ /**
45
+ * Which lines of a run this counter actually COUNTED, for the report to cite when the number
46
+ * grew. Absent means the tail of the run is shown, which is right for a counter whose tool prints
47
+ * nothing but findings and wrong for every counter that reads one rule or one severity out of a
48
+ * mixed run: the tail is whatever printed LAST, and dielime got two unrelated warnings under a
49
+ * +2 on one rule while the two real errors sat further up.
50
+ *
51
+ * It takes the output rather than being handed the lines during the run so that it stays a pure
52
+ * function of what the tool said — the same reading the count was taken from, testable on its
53
+ * own. Returning nothing falls back to the tail: a citation nobody could make is no reason to
54
+ * show the reader nothing at all.
55
+ */
56
+ evidence?: (context: {
57
+ output: string;
58
+ params: Record<string, unknown>;
59
+ }) => string[];
44
60
  id: string;
45
61
  /**
46
62
  * Whether this counter's number is a measured QUANTITY — bytes, milliseconds — rather than a
@@ -151,6 +167,16 @@ declare const CONFIG_FILE = "geonosis.ratchet.json";
151
167
  /** The baseline key an entry writes. Defaults to the counter's own id. */
152
168
  declare const keyOf: (entry: CounterConfig) => string;
153
169
  declare const loadConfig: (cwd: string) => RatchetConfig;
170
+ /**
171
+ * #120: the ONE answer to "where does the baseline live". Three tools guessed instead of asking —
172
+ * rails' deny rail rendered `<root>/gate-baseline.json` over a repo whose baseline is
173
+ * `scripts/gate-baseline.json` (measured on sandbox-exec: the tampering write LANDED), and mcp's
174
+ * preamble said "no gate-baseline.json here" over twenty ratcheted counters. Anything that SPEAKS
175
+ * about the baseline resolves it here; `loadConfig` stays the strict door for anything that RUNS
176
+ * counters. Never throws: a missing or unreadable config means the default, because naming the
177
+ * baseline must work in repos that have not adopted the ratchet yet.
178
+ */
179
+ declare const resolveBaseline: (cwd: string) => string;
154
180
 
155
181
  declare const COUNTERS: Counter[];
156
182
  declare const counterById: (id: string) => Counter;
@@ -244,4 +270,4 @@ declare const formatProve: ({ proofs, proven }: ProveResult) => string;
244
270
  */
245
271
  declare const runCommand: (cwd: string, counterId: string, env?: NodeJS.ProcessEnv) => (command: string) => CommandResult;
246
272
 
247
- export { CONFIG_FILE, COUNTERS, type CommandResult, type Counter, type CounterConfig, type CounterContext, CounterError, type CounterProbe, type Holder, type Measurement, type Proof, type ProveResult, type RatchetConfig, type RatchetResult, type Verdict, acquireExclusive, counterById, formatProve, formatReport, heavyLockPath, keyOf, loadConfig, runCommand, runProve, runRatchet };
273
+ export { CONFIG_FILE, COUNTERS, type CommandResult, type Counter, type CounterConfig, type CounterContext, CounterError, type CounterProbe, type Holder, type Measurement, type Proof, type ProveResult, type RatchetConfig, type RatchetResult, type Verdict, acquireExclusive, counterById, formatProve, formatReport, heavyLockPath, keyOf, loadConfig, resolveBaseline, runCommand, runProve, runRatchet };
package/dist/index.js CHANGED
@@ -9,10 +9,11 @@ import {
9
9
  heavyLockPath,
10
10
  keyOf,
11
11
  loadConfig,
12
+ resolveBaseline,
12
13
  runCommand,
13
14
  runProve,
14
15
  runRatchet
15
- } from "./chunk-LSYVFUP4.js";
16
+ } from "./chunk-HBM6F3GO.js";
16
17
  export {
17
18
  CONFIG_FILE,
18
19
  COUNTERS,
@@ -24,6 +25,7 @@ export {
24
25
  heavyLockPath,
25
26
  keyOf,
26
27
  loadConfig,
28
+ resolveBaseline,
27
29
  runCommand,
28
30
  runProve,
29
31
  runRatchet
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/ratchet",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "types": "./dist/index.d.ts",
5
5
  "description": "Debt as a number that may only shrink — one ratchet, pluggable counters.",
6
6
  "keywords": [