@geonosis/ratchet 1.3.0 → 1.4.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,13 +1,76 @@
1
+ // src/core/envelope.ts
2
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import { fileURLToPath } from "url";
5
+ var ENVELOPES_DIR = ".geonosis/envelopes";
6
+ var envelopePath = (root, tool) => join(root, ENVELOPES_DIR, `${tool}.json`);
7
+ var UnbalancedEnvelope = class extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = "UnbalancedEnvelope";
11
+ }
12
+ };
13
+ var isCount = (value) => Number.isSafeInteger(value) && value >= 0;
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 writeEnvelope = ({
16
+ envelope,
17
+ next,
18
+ root
19
+ }) => {
20
+ if (envelope.tool.trim() === "") {
21
+ throw new UnbalancedEnvelope(
22
+ `an envelope with no tool name cannot be filed or reported against. Next: ${next}`
23
+ );
24
+ }
25
+ if (envelope.version.trim() === "") {
26
+ throw new UnbalancedEnvelope(
27
+ `${envelope.tool}: an envelope that cannot name the build that wrote it dates nothing, and a stale one reads exactly like a fresh one. Next: ${next}`
28
+ );
29
+ }
30
+ if (!isCount(envelope.considered) || !isCount(envelope.read)) {
31
+ throw new UnbalancedEnvelope(
32
+ `${envelope.tool}: considered ${envelope.considered} and read ${envelope.read} \u2014 a census is a whole number of things, and arithmetic over anything else balances by accident. Next: ${next}`
33
+ );
34
+ }
35
+ if (envelope.considered !== envelope.read + envelope.refused.length + envelope.excused.length) {
36
+ throw new UnbalancedEnvelope(unbalancedMessage(envelope, next));
37
+ }
38
+ const at = envelopePath(root, envelope.tool);
39
+ mkdirSync(dirname(at), { recursive: true });
40
+ writeFileSync(at, `${JSON.stringify(envelope, void 0, 2)}
41
+ `);
42
+ return at;
43
+ };
44
+ var UNKNOWN = "unknown";
45
+ var versionIn = (dir) => {
46
+ try {
47
+ const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
48
+ return typeof manifest.version === "string" ? manifest.version : void 0;
49
+ } catch {
50
+ return void 0;
51
+ }
52
+ };
53
+ var versionOf = (moduleUrl) => {
54
+ let dir = dirname(fileURLToPath(moduleUrl));
55
+ for (; ; ) {
56
+ const found = versionIn(dir);
57
+ if (found !== void 0) return found;
58
+ const up = dirname(dir);
59
+ if (up === dir) return UNKNOWN;
60
+ dir = up;
61
+ }
62
+ };
63
+
1
64
  // src/core/lock.ts
2
65
  import { randomUUID } from "crypto";
3
- import { linkSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
66
+ import { linkSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
4
67
  import { homedir } from "os";
5
- import { dirname, join } from "path";
6
- var heavyLockPath = () => process.env.GEONOSIS_HEAVY_LOCK ?? join(homedir(), ".cache", "geonosis", "heavy.lock");
68
+ import { dirname as dirname2, join as join2 } from "path";
69
+ var heavyLockPath = () => process.env.GEONOSIS_HEAVY_LOCK ?? join2(homedir(), ".cache", "geonosis", "heavy.lock");
7
70
  var sleep = (ms) => new Promise((done) => setTimeout(done, ms));
8
71
  var holderOf = (path) => {
9
72
  try {
10
- const parsed = JSON.parse(readFileSync(path, "utf8"));
73
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
11
74
  return typeof parsed.pid === "number" ? {
12
75
  cwd: parsed.cwd ?? "somewhere",
13
76
  pid: parsed.pid,
@@ -31,7 +94,7 @@ var heldFor = (holder) => {
31
94
  return `${Math.round((Date.now() - since) / 1e3)}s`;
32
95
  };
33
96
  var write = (path) => {
34
- mkdirSync(dirname(path), { recursive: true });
97
+ mkdirSync2(dirname2(path), { recursive: true });
35
98
  const mine = {
36
99
  cwd: process.cwd(),
37
100
  pid: process.pid,
@@ -39,7 +102,7 @@ var write = (path) => {
39
102
  };
40
103
  const staging = `${path}.${process.pid}.${randomUUID()}`;
41
104
  try {
42
- writeFileSync(staging, JSON.stringify(mine));
105
+ writeFileSync2(staging, JSON.stringify(mine));
43
106
  linkSync(staging, path);
44
107
  return true;
45
108
  } catch (error) {
@@ -89,7 +152,7 @@ var acquireExclusive = async ({
89
152
  };
90
153
 
91
154
  // src/core/config.ts
92
- import { existsSync, readFileSync as readFileSync2 } from "fs";
155
+ import { existsSync, readFileSync as readFileSync3 } from "fs";
93
156
  import { resolve } from "path";
94
157
  var CONFIG_FILE = "geonosis.ratchet.json";
95
158
  var keyOf = (entry) => entry.key ?? entry.counter;
@@ -98,7 +161,7 @@ var loadConfig = (cwd) => {
98
161
  if (!existsSync(path)) {
99
162
  throw new Error(`no ${CONFIG_FILE} in ${cwd} \u2014 the ratchet has nothing to count`);
100
163
  }
101
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
164
+ const parsed = JSON.parse(readFileSync3(path, "utf8"));
102
165
  if (!Array.isArray(parsed.counters)) {
103
166
  throw new Error(`${CONFIG_FILE} has no "counters" array`);
104
167
  }
@@ -126,7 +189,7 @@ var loadConfig = (cwd) => {
126
189
  };
127
190
  var resolveBaseline = (cwd) => {
128
191
  try {
129
- const parsed = JSON.parse(readFileSync2(resolve(cwd, CONFIG_FILE), "utf8"));
192
+ const parsed = JSON.parse(readFileSync3(resolve(cwd, CONFIG_FILE), "utf8"));
130
193
  return typeof parsed.baseline === "string" ? parsed.baseline : "gate-baseline.json";
131
194
  } catch {
132
195
  return "gate-baseline.json";
@@ -172,13 +235,13 @@ ${output.trim()}`
172
235
  };
173
236
 
174
237
  // src/core/prove.ts
175
- import { existsSync as existsSync2, mkdtempSync, rmSync as rmSync2 } from "fs";
238
+ import { existsSync as existsSync2, mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
176
239
  import { tmpdir } from "os";
177
- import { delimiter, dirname as dirname3, join as join2, resolve as resolve3 } from "path";
240
+ import { delimiter, dirname as dirname4, join as join3, resolve as resolve3 } from "path";
178
241
 
179
242
  // src/core/exclusive.ts
180
243
  import { spawn } from "child_process";
181
- import { dirname as dirname2, resolve as resolve2 } from "path";
244
+ import { dirname as dirname3, resolve as resolve2 } from "path";
182
245
  var MARK = /^exclusive-hold (start|end) (\d+)$/gm;
183
246
  var KEY = "exclusive";
184
247
  var spanOf = (output) => {
@@ -193,7 +256,7 @@ var hold = (cli, holdMs, lockPath) => new Promise((done) => {
193
256
  process.execPath,
194
257
  [cli, "--exclusive", "--exclusive-timeout", "60", "--hold", String(holdMs)],
195
258
  {
196
- cwd: dirname2(resolve2(cli)),
259
+ cwd: dirname3(resolve2(cli)),
197
260
  env: { ...process.env, GEONOSIS_HEAVY_LOCK: lockPath },
198
261
  stdio: ["ignore", "pipe", "pipe"]
199
262
  }
@@ -245,9 +308,9 @@ var toolPath = (cwd) => {
245
308
  const dirs = [];
246
309
  let dir = resolve3(cwd);
247
310
  for (; ; ) {
248
- const bin = join2(dir, "node_modules", ".bin");
311
+ const bin = join3(dir, "node_modules", ".bin");
249
312
  if (existsSync2(bin)) dirs.push(bin);
250
- const parent = dirname3(dir);
313
+ const parent = dirname4(dir);
251
314
  if (parent === dir) break;
252
315
  dir = parent;
253
316
  }
@@ -255,7 +318,7 @@ var toolPath = (cwd) => {
255
318
  };
256
319
  var NO_PROBE = "no probe \u2014 a counter nobody has seen read a planted finding has not been shown to measure";
257
320
  var oneProofOf = async (counter, key, path, probe) => {
258
- const dir = mkdtempSync(join2(tmpdir(), "geonosis-prove-"));
321
+ const dir = mkdtempSync(join3(tmpdir(), "geonosis-prove-"));
259
322
  try {
260
323
  probe.input(dir);
261
324
  const command = probe.command?.(dir);
@@ -322,11 +385,43 @@ var runProve = async ({
322
385
  if (taken.some((one) => one.verdict !== "proven" && one.verdict !== "skipped")) {
323
386
  return { proofs, proven: false };
324
387
  }
388
+ const customized = (counter.readingParams ?? []).filter((one) => entry[one] !== void 0);
389
+ if (customized.length > 0) {
390
+ const declared = entry.probe;
391
+ const named = customized.map((one) => `${one} ${JSON.stringify(entry[one])}`).join(", ");
392
+ if (declared === void 0 || typeof declared.sample !== "string" || typeof declared.expect !== "number") {
393
+ proofs.push({
394
+ counter: counter.id,
395
+ key,
396
+ reason: `the configured ${named} is not exercised by this counter's shipped probe \u2014 declare probe: { sample, expect } beside it, a sample this reading must count`,
397
+ verdict: "unproven"
398
+ });
399
+ return { proofs, proven: false };
400
+ }
401
+ const sample = declared.sample;
402
+ const entryProbe = {
403
+ command: () => "cat sample.txt",
404
+ expect: declared.expect,
405
+ input: (dir) => writeFileSync3(join3(dir, "sample.txt"), sample),
406
+ name: "configured reading",
407
+ params: Object.fromEntries(customized.map((one) => [one, entry[one]]))
408
+ };
409
+ const proof = await oneProofOf(counter, key, path, entryProbe);
410
+ const said = proof.verdict === "cannot-fail" ? {
411
+ counter: counter.id,
412
+ expected: declared.expect,
413
+ key,
414
+ reading: 0,
415
+ verdict: "misread"
416
+ } : proof;
417
+ proofs.push(said);
418
+ if (said.verdict !== "proven") return { proofs, proven: false };
419
+ }
325
420
  }
326
421
  if (exclusiveVia !== void 0) {
327
- const dir = mkdtempSync(join2(tmpdir(), "geonosis-lock-"));
422
+ const dir = mkdtempSync(join3(tmpdir(), "geonosis-lock-"));
328
423
  try {
329
- const proof = await proveExclusive({ cli: exclusiveVia, lockPath: join2(dir, "heavy.lock") });
424
+ const proof = await proveExclusive({ cli: exclusiveVia, lockPath: join3(dir, "heavy.lock") });
330
425
  proofs.push(proof);
331
426
  if (proof.verdict !== "serialised") return { proofs, proven: false };
332
427
  } finally {
@@ -337,20 +432,23 @@ var runProve = async ({
337
432
  };
338
433
 
339
434
  // src/core/ratchet.ts
340
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
435
+ import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
341
436
  import { resolve as resolve4 } from "path";
342
437
  var EVIDENCE_LINES = 10;
343
438
  var recorded = (counter, params, run) => {
344
439
  let output = "";
440
+ let lastCommand;
345
441
  const trimmed = (lines) => lines.map((line) => line.trimEnd()).filter((line) => line !== "").slice(-EVIDENCE_LINES);
346
442
  return {
347
443
  cited: () => {
348
444
  const counted = counter.evidence === void 0 ? [] : counter.evidence({ output, params });
349
445
  return trimmed(counted.length > 0 ? counted : output.split("\n"));
350
446
  },
447
+ lastCommand: () => lastCommand,
351
448
  run: (command) => {
352
449
  const result = run(command);
353
450
  output = result.output;
451
+ lastCommand = command;
354
452
  return result;
355
453
  }
356
454
  };
@@ -386,9 +484,10 @@ var runRatchet = async ({
386
484
  if (!existsSync3(baselinePath)) {
387
485
  throw new Error(`no ${config.baseline} in ${cwd} \u2014 nothing to ratchet against`);
388
486
  }
389
- const baseline = JSON.parse(readFileSync3(baselinePath, "utf8"));
487
+ const baseline = JSON.parse(readFileSync4(baselinePath, "utf8"));
390
488
  const byId = new Map(counters.map((one) => [one.id, one]));
391
489
  const measurements = [];
490
+ const refusals = [];
392
491
  for (const entry of config.counters) {
393
492
  const counter = byId.get(entry.counter);
394
493
  if (counter === void 0) {
@@ -409,7 +508,17 @@ var runRatchet = async ({
409
508
  }
410
509
  const tolerance = toleranceOf(entry, key, counter);
411
510
  const recorder = recorded(counter, entry, runCommand(cwd, entry.counter));
412
- const now = await counter.run({ cwd, key, params: entry, run: recorder.run });
511
+ let now;
512
+ try {
513
+ now = await counter.run({ cwd, key, params: entry, run: recorder.run });
514
+ } catch (error) {
515
+ const ran = recorder.lastCommand();
516
+ refusals.push({
517
+ key,
518
+ reason: `${error.message}${ran === void 0 ? "" : `. Next: run \`${ran}\` in ${cwd} and read what it prints`}`
519
+ });
520
+ continue;
521
+ }
413
522
  const verdict = verdictOf(now, limit, tolerance);
414
523
  measurements.push({
415
524
  baseline: limit,
@@ -421,16 +530,16 @@ var runRatchet = async ({
421
530
  }
422
531
  const grew = measurements.some((one) => one.verdict === "grew");
423
532
  const shrank = measurements.some((one) => one.verdict === "shrank");
424
- if (shrank && !grew) {
533
+ if (shrank && !grew && refusals.length === 0) {
425
534
  const next = { ...baseline };
426
535
  for (const one of measurements) {
427
536
  if (one.verdict === "shrank") next[one.key] = one.now;
428
537
  }
429
- writeFileSync2(baselinePath, `${JSON.stringify(next, null, 2)}
538
+ writeFileSync4(baselinePath, `${JSON.stringify(next, null, 2)}
430
539
  `);
431
- return { measurements, rewritten: true };
540
+ return { considered: config.counters.length, measurements, refusals, rewritten: true };
432
541
  }
433
- return { measurements, rewritten: false };
542
+ return { considered: config.counters.length, measurements, refusals, rewritten: false };
434
543
  };
435
544
 
436
545
  // src/counters/params.ts
@@ -449,12 +558,12 @@ var countMatches = (text, pattern) => text.match(new RegExp(pattern.source, `${p
449
558
  var escapeForRegex = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
450
559
 
451
560
  // src/counters/plant.ts
452
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
453
- import { dirname as dirname4, join as join3 } from "path";
561
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
562
+ import { dirname as dirname5, join as join4 } from "path";
454
563
  var plant = (dir, relative, contents) => {
455
- const path = join3(dir, relative);
456
- mkdirSync2(dirname4(path), { recursive: true });
457
- writeFileSync3(path, contents);
564
+ const path = join4(dir, relative);
565
+ mkdirSync3(dirname5(path), { recursive: true });
566
+ writeFileSync5(path, contents);
458
567
  };
459
568
  var captured = (sample) => ({
460
569
  command: () => "cat sample.txt",
@@ -539,8 +648,8 @@ var bundleBytes = {
539
648
  };
540
649
 
541
650
  // src/counters/ci.ts
542
- import { readdirSync, readFileSync as readFileSync4 } from "fs";
543
- import { join as join4, resolve as resolve5 } from "path";
651
+ import { readdirSync, readFileSync as readFileSync5 } from "fs";
652
+ import { join as join5, resolve as resolve5 } from "path";
544
653
  var DISABLED = /\bif:[ \t]*false\b/;
545
654
  var WORKFLOW = /\.ya?ml$/;
546
655
  var disabledCiJobs = {
@@ -558,7 +667,7 @@ var disabledCiJobs = {
558
667
  const dir = resolve5(cwd, relative);
559
668
  let files;
560
669
  try {
561
- files = readdirSync(dir, { withFileTypes: true }).filter((entry) => WORKFLOW.test(entry.name)).map((entry) => join4(dir, entry.name));
670
+ files = readdirSync(dir, { withFileTypes: true }).filter((entry) => WORKFLOW.test(entry.name)).map((entry) => join5(dir, entry.name));
562
671
  } catch {
563
672
  return 0;
564
673
  }
@@ -566,7 +675,7 @@ var disabledCiJobs = {
566
675
  for (const file of files) {
567
676
  let contents;
568
677
  try {
569
- contents = readFileSync4(file, "utf8");
678
+ contents = readFileSync5(file, "utf8");
570
679
  } catch (error) {
571
680
  throw new CounterError(
572
681
  "disabledCiJobs",
@@ -589,10 +698,11 @@ var cloneCount = {
589
698
  const output = run(command).output;
590
699
  const found = output.match(CLONES)?.[1];
591
700
  if (found === void 0) {
701
+ const said = output.trim();
592
702
  throw new CounterError(
593
703
  "cloneCount",
594
- `no "Found N clones" line:
595
- ${output.trim().slice(-500)}`
704
+ said === "" ? 'the command printed nothing at all, so there is no "Found N clones" line to read and no output to show \u2014 it is not installed, or it never ran' : `no "Found N clones" line in what it printed:
705
+ ${said.slice(-500)}`
596
706
  );
597
707
  }
598
708
  return Number(found);
@@ -646,7 +756,7 @@ var unformattedFiles = {
646
756
  };
647
757
 
648
758
  // src/counters/gate-report.ts
649
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
759
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
650
760
  import { resolve as resolve7 } from "path";
651
761
  var DEFAULT_REPORT = ".geonosis/gate-report.json";
652
762
  var fastTierMs = {
@@ -680,7 +790,7 @@ var fastTierMs = {
680
790
  }
681
791
  let report;
682
792
  try {
683
- report = JSON.parse(readFileSync5(path, "utf8"));
793
+ report = JSON.parse(readFileSync6(path, "utf8"));
684
794
  } catch (error) {
685
795
  throw new CounterError(
686
796
  "fastTierMs",
@@ -702,7 +812,7 @@ var fastTierMs = {
702
812
  };
703
813
 
704
814
  // src/counters/law.ts
705
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
815
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
706
816
  import { resolve as resolve8 } from "path";
707
817
  var lawLineCount = {
708
818
  id: "lawLineCount",
@@ -715,12 +825,12 @@ var lawLineCount = {
715
825
  const relative = stringParam("lawLineCount", params, "path", "CLAUDE.md");
716
826
  const path = resolve8(cwd, relative);
717
827
  if (!existsSync6(path)) throw new CounterError("lawLineCount", `no law file at ${relative}`);
718
- return readFileSync6(path, "utf8").replace(/\n$/, "").split("\n").length;
828
+ return readFileSync7(path, "utf8").replace(/\n$/, "").split("\n").length;
719
829
  }
720
830
  };
721
831
 
722
832
  // src/counters/oxlint.ts
723
- import { existsSync as existsSync7, readFileSync as readFileSync7, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
833
+ import { existsSync as existsSync7, readFileSync as readFileSync8, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
724
834
  import { resolve as resolve9 } from "path";
725
835
  var DEFAULT_COMMAND = "npx oxlint --format=unix --config .oxlintrc.json .";
726
836
  var PROBE_COMMAND = "oxlint --format=unix --config .oxlintrc.json .";
@@ -887,11 +997,11 @@ var oxlintRule = {
887
997
  const source = resolve9(cwd, config);
888
998
  if (!existsSync7(source)) throw new CounterError("oxlintRule", `no config at ${config}`);
889
999
  const strictName = `.oxlintrc.ratchet-${key}.json`;
890
- const strict = readFileSync7(source, "utf8").replace(
1000
+ const strict = readFileSync8(source, "utf8").replace(
891
1001
  new RegExp(`("[^"]*${escapeForRegex(rule)}"\\s*:\\s*\\[?\\s*)"(warn|off)"`),
892
1002
  '$1"error"'
893
1003
  );
894
- writeFileSync4(resolve9(cwd, strictName), strict);
1004
+ writeFileSync6(resolve9(cwd, strictName), strict);
895
1005
  try {
896
1006
  return countRule(run(command.replace("{config}", strictName)), rule, expect);
897
1007
  } finally {
@@ -925,15 +1035,15 @@ var runtimeCodeShipped = {
925
1035
 
926
1036
  // src/counters/scripts.ts
927
1037
  import { readdirSync as readdirSync3 } from "fs";
928
- import { join as join6 } from "path";
1038
+ import { join as join7 } from "path";
929
1039
 
930
1040
  // src/counters/workspace.ts
931
- import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync8 } from "fs";
932
- import { join as join5, resolve as resolve10 } from "path";
1041
+ import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync9 } from "fs";
1042
+ import { join as join6, resolve as resolve10 } from "path";
933
1043
  var SKIP = /^(node_modules|\.)/;
934
1044
  var childDirs = (dir) => {
935
1045
  try {
936
- return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !SKIP.test(entry.name)).map((entry) => join5(dir, entry.name));
1046
+ return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !SKIP.test(entry.name)).map((entry) => join6(dir, entry.name));
937
1047
  } catch {
938
1048
  return [];
939
1049
  }
@@ -943,14 +1053,14 @@ var expand = (root, pattern) => {
943
1053
  const segments = pattern.split("/").filter((one) => one !== "" && one !== ".");
944
1054
  let dirs = [root];
945
1055
  for (const segment of segments) {
946
- dirs = segment === "*" ? dirs.flatMap((dir) => childDirs(dir)) : segment === "**" ? dirs.flatMap((dir) => descendants(dir, 3)) : dirs.map((dir) => join5(dir, segment)).filter((dir) => existsSync8(dir));
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));
947
1057
  }
948
1058
  return dirs;
949
1059
  };
950
1060
  var QUOTED = /^['"]|['"]$/g;
951
1061
  var cleaned = (value) => value.replace(/#.*$/, "").trim().replaceAll(QUOTED, "");
952
1062
  var pnpmPatterns = (path) => {
953
- const lines = readFileSync8(path, "utf8").split("\n");
1063
+ const lines = readFileSync9(path, "utf8").split("\n");
954
1064
  const at = lines.findIndex((line) => line.startsWith("packages:"));
955
1065
  if (at === -1) return [];
956
1066
  const inline = lines[at]?.slice("packages:".length).trim() ?? "";
@@ -969,15 +1079,15 @@ var pnpmPatterns = (path) => {
969
1079
  return patterns;
970
1080
  };
971
1081
  var npmPatterns = (path) => {
972
- const parsed = JSON.parse(readFileSync8(path, "utf8"));
1082
+ const parsed = JSON.parse(readFileSync9(path, "utf8"));
973
1083
  const declared = Array.isArray(parsed.workspaces) ? parsed.workspaces : parsed.workspaces?.packages ?? [];
974
1084
  return declared.filter((one) => typeof one === "string");
975
1085
  };
976
1086
  var nameOf = (dir) => {
977
- const manifest = join5(dir, "package.json");
1087
+ const manifest = join6(dir, "package.json");
978
1088
  if (!existsSync8(manifest)) return void 0;
979
1089
  try {
980
- const { name } = JSON.parse(readFileSync8(manifest, "utf8"));
1090
+ const { name } = JSON.parse(readFileSync9(manifest, "utf8"));
981
1091
  return typeof name === "string" && name !== "" ? name : void 0;
982
1092
  } catch {
983
1093
  return void 0;
@@ -985,15 +1095,15 @@ var nameOf = (dir) => {
985
1095
  };
986
1096
  var workspaceDirs = (cwd) => {
987
1097
  const root = resolve10(cwd);
988
- const pnpm = join5(root, "pnpm-workspace.yaml");
989
- const manifest = join5(root, "package.json");
1098
+ const pnpm = join6(root, "pnpm-workspace.yaml");
1099
+ const manifest = join6(root, "package.json");
990
1100
  const patterns = existsSync8(pnpm) ? pnpmPatterns(pnpm) : existsSync8(manifest) ? npmPatterns(manifest) : [];
991
- const dirs = patterns.filter((pattern) => !pattern.startsWith("!")).flatMap((pattern) => expand(root, pattern)).filter((dir) => existsSync8(join5(dir, "package.json")));
1101
+ const dirs = patterns.filter((pattern) => !pattern.startsWith("!")).flatMap((pattern) => expand(root, pattern)).filter((dir) => existsSync8(join6(dir, "package.json")));
992
1102
  return [...new Set(dirs)];
993
1103
  };
994
1104
  var manifestOf = (dir) => {
995
1105
  try {
996
- const parsed = JSON.parse(readFileSync8(join5(dir, "package.json"), "utf8"));
1106
+ const parsed = JSON.parse(readFileSync9(join6(dir, "package.json"), "utf8"));
997
1107
  return typeof parsed === "object" && parsed !== null ? parsed : void 0;
998
1108
  } catch {
999
1109
  return void 0;
@@ -1023,7 +1133,7 @@ var holdsTests = (dir, depth = 6) => {
1023
1133
  if (entry.isDirectory()) {
1024
1134
  if (entry.name === TEST_DIR) return true;
1025
1135
  if (SKIP2.test(entry.name) || depth === 0) continue;
1026
- if (holdsTests(join6(dir, entry.name), depth - 1)) return true;
1136
+ if (holdsTests(join7(dir, entry.name), depth - 1)) return true;
1027
1137
  continue;
1028
1138
  }
1029
1139
  if (TEST_FILE.test(entry.name)) return true;
@@ -1064,16 +1174,26 @@ var packagesWithoutTypecheck = {
1064
1174
  var sumOfCounts = {
1065
1175
  id: "sumOfCounts",
1066
1176
  probe: { ...captured("src/a.ts:1\nsrc/b.ts:0\n"), expect: 1 },
1177
+ readingParams: ["match"],
1067
1178
  run: async ({ params, run }) => {
1068
1179
  const command = stringParam("sumOfCounts", params, "command");
1069
- const match = new RegExp(stringParam("sumOfCounts", params, "match", ":(\\d+)$"), "gm");
1070
- return [...run(command).output.matchAll(match)].map(([, digits]) => Number(digits ?? 0)).filter((count) => Number.isFinite(count)).reduce((sum, count) => sum + count, 0);
1180
+ const pattern = stringParam("sumOfCounts", params, "match", ":(\\d+)$");
1181
+ const match = new RegExp(pattern, "gm");
1182
+ const output = run(command).output;
1183
+ const found = [...output.matchAll(match)];
1184
+ if (found.length === 0 && output.trim() !== "") {
1185
+ throw new CounterError(
1186
+ "sumOfCounts",
1187
+ `its match ${pattern} read nothing from ${output.trim().split("\n").length} line(s) of output (first: ${JSON.stringify(output.trim().split("\n")[0])}) \u2014 a match that cannot read the tool is not a zero`
1188
+ );
1189
+ }
1190
+ return found.map(([, digits]) => Number(digits ?? 0)).filter((count) => Number.isFinite(count)).reduce((sum, count) => sum + count, 0);
1071
1191
  }
1072
1192
  };
1073
1193
 
1074
1194
  // src/counters/suppressions.ts
1075
- import { readdirSync as readdirSync4, readFileSync as readFileSync9 } from "fs";
1076
- import { join as join7, resolve as resolve11 } from "path";
1195
+ import { readdirSync as readdirSync4, readFileSync as readFileSync10 } from "fs";
1196
+ import { join as join8, resolve as resolve11 } from "path";
1077
1197
  var CODE_FILE = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts|vue|svelte|astro)$/;
1078
1198
  var SKIP3 = /^(?:node_modules|dist|coverage|\.[^.]|__fixtures__)/;
1079
1199
  var SUPPRESSION = /(?:eslint|oxlint|biome)-(?:disable|ignore)(?:-next-line|-line)?|@ts-expect-error|@ts-ignore/g;
@@ -1088,12 +1208,12 @@ var countIn = (dir, depth = 12) => {
1088
1208
  for (const entry of entries) {
1089
1209
  if (SKIP3.test(entry.name)) continue;
1090
1210
  if (entry.isDirectory()) {
1091
- if (depth > 0) found += countIn(join7(dir, entry.name), depth - 1);
1211
+ if (depth > 0) found += countIn(join8(dir, entry.name), depth - 1);
1092
1212
  continue;
1093
1213
  }
1094
1214
  if (!CODE_FILE.test(entry.name)) continue;
1095
1215
  try {
1096
- found += readFileSync9(join7(dir, entry.name), "utf8").match(SUPPRESSION)?.length ?? 0;
1216
+ found += readFileSync10(join8(dir, entry.name), "utf8").match(SUPPRESSION)?.length ?? 0;
1097
1217
  } catch {
1098
1218
  }
1099
1219
  }
@@ -1112,9 +1232,9 @@ var suppressionCount = {
1112
1232
  };
1113
1233
 
1114
1234
  // src/counters/tests.ts
1115
- import { existsSync as existsSync9, mkdtempSync as mkdtempSync2, readFileSync as readFileSync10, rmSync as rmSync4 } from "fs";
1235
+ import { existsSync as existsSync9, mkdtempSync as mkdtempSync2, readFileSync as readFileSync11, rmSync as rmSync4 } from "fs";
1116
1236
  import { tmpdir as tmpdir2 } from "os";
1117
- import { join as join8, resolve as resolve12 } from "path";
1237
+ import { join as join9, resolve as resolve12 } from "path";
1118
1238
  var COUNTER = "testFailures";
1119
1239
  var VITEST_LINE = /^\s*Tests {2,}(.+?)\s*$/;
1120
1240
  var VITEST_TOTAL = /\(\d+\)$/;
@@ -1154,7 +1274,7 @@ var fromReport = (path) => {
1154
1274
  }
1155
1275
  let report;
1156
1276
  try {
1157
- report = JSON.parse(readFileSync10(path, "utf8"));
1277
+ report = JSON.parse(readFileSync11(path, "utf8"));
1158
1278
  } catch (error) {
1159
1279
  throw new CounterError(
1160
1280
  COUNTER,
@@ -1185,7 +1305,7 @@ var reportPathFor = (cwd, params, command) => {
1185
1305
  `report: "${VITEST_JSON}" needs somewhere to put the report \u2014 write ${PLACEHOLDER} into the command (--outputFile=${PLACEHOLDER}) or give the entry a "reportPath"`
1186
1306
  );
1187
1307
  }
1188
- return { own: true, path: join8(mkdtempSync2(join8(tmpdir2(), "geonosis-report-")), "report.json") };
1308
+ return { own: true, path: join9(mkdtempSync2(join9(tmpdir2(), "geonosis-report-")), "report.json") };
1189
1309
  };
1190
1310
  var testFailures = {
1191
1311
  id: COUNTER,
@@ -1234,7 +1354,7 @@ var testFailures = {
1234
1354
  run(command.replaceAll(PLACEHOLDER, path));
1235
1355
  return fromReport(path);
1236
1356
  } finally {
1237
- if (own) rmSync4(join8(path, ".."), { force: true, recursive: true });
1357
+ if (own) rmSync4(join9(path, ".."), { force: true, recursive: true });
1238
1358
  }
1239
1359
  }
1240
1360
  };
@@ -1274,7 +1394,7 @@ var typecheckErrors = {
1274
1394
  };
1275
1395
 
1276
1396
  // src/counters/walk.ts
1277
- import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
1397
+ import { existsSync as existsSync10, readFileSync as readFileSync12 } from "fs";
1278
1398
  import { resolve as resolve13 } from "path";
1279
1399
  var DEFAULT_REPORT2 = ".geonosis/walk-report.json";
1280
1400
  var CLASSES = /* @__PURE__ */ new Set([
@@ -1328,7 +1448,7 @@ var walkFindings = {
1328
1448
  }
1329
1449
  let report;
1330
1450
  try {
1331
- report = JSON.parse(readFileSync11(path, "utf8"));
1451
+ report = JSON.parse(readFileSync12(path, "utf8"));
1332
1452
  } catch (error) {
1333
1453
  throw new CounterError(
1334
1454
  "walkFindings",
@@ -1383,7 +1503,7 @@ var counterById = (id) => {
1383
1503
 
1384
1504
  // src/report.ts
1385
1505
  var WIDTH = 28;
1386
- var formatReport = ({ measurements, rewritten }) => {
1506
+ var formatReport = ({ measurements, refusals, rewritten }) => {
1387
1507
  const lines = measurements.map((one) => {
1388
1508
  if (one.verdict === "skipped") {
1389
1509
  return ` SKIP ${one.key}: not measured by --tier ${one.tier}`;
@@ -1396,8 +1516,14 @@ var formatReport = ({ measurements, rewritten }) => {
1396
1516
  if (one.verdict === "shrank") return `${head} <-- improved -${one.baseline - one.now}`;
1397
1517
  return head;
1398
1518
  });
1519
+ lines.push(...refusals.map((one) => ` CANNOT MEASURE ${one.key}: ${one.reason}`));
1399
1520
  const grew = measurements.filter((one) => one.verdict === "grew");
1400
- if (grew.length > 0) {
1521
+ if (refusals.length > 0) {
1522
+ lines.push(
1523
+ "",
1524
+ `ratchet FAIL \u2014 ${refusals.length} counter(s) could not be measured, and a gate that cannot measure has not passed. The baseline was left exactly as it was. Next: run each counter's own command by hand \u2014 ${refusals.map((one) => one.key).join(", ")} \u2014 and fix what it prints.`
1525
+ );
1526
+ } else if (grew.length > 0) {
1401
1527
  lines.push(
1402
1528
  "",
1403
1529
  "ratchet FAIL \u2014 debt grew. Fix it, or say in the commit message why the baseline goes up."
@@ -1419,6 +1545,7 @@ var formatProve = ({ proofs, proven }) => {
1419
1545
  }
1420
1546
  if (one.verdict === "cannot-measure") return ` CANNOT MEASURE ${one.key}: ${one.reason}`;
1421
1547
  if (one.verdict === "cannot-fail") return ` CANNOT FAIL ${one.key}: read 0`;
1548
+ if (one.verdict === "unproven") return ` UNPROVEN ${one.key}: ${one.reason}`;
1422
1549
  if (one.verdict === "misread") {
1423
1550
  return ` MISREAD ${one.key}: read ${one.reading} where its probe planted ${one.expected}`;
1424
1551
  }
@@ -1433,6 +1560,11 @@ var formatProve = ({ proofs, proven }) => {
1433
1560
  };
1434
1561
 
1435
1562
  export {
1563
+ ENVELOPES_DIR,
1564
+ envelopePath,
1565
+ UnbalancedEnvelope,
1566
+ writeEnvelope,
1567
+ versionOf,
1436
1568
  heavyLockPath,
1437
1569
  acquireExclusive,
1438
1570
  CONFIG_FILE,
package/dist/cli.js CHANGED
@@ -4,8 +4,10 @@ import {
4
4
  formatProve,
5
5
  formatReport,
6
6
  runProve,
7
- runRatchet
8
- } from "./chunk-HBM6F3GO.js";
7
+ runRatchet,
8
+ versionOf,
9
+ writeEnvelope
10
+ } from "./chunk-ESN6ULZZ.js";
9
11
 
10
12
  // src/cli.ts
11
13
  import process from "process";
@@ -100,6 +102,19 @@ var holdTheLock = async (ms) => {
100
102
  `);
101
103
  return 0;
102
104
  };
105
+ var ENVELOPE_NEXT = "geonosis-ratchet --print-config-shape, then count the entries in geonosis.ratchet.json \u2014 a run that measured fewer counters than the config named is a bug in this tool, not in the tree it measured";
106
+ var envelopeOf = (result, durationMs) => ({
107
+ considered: result.considered,
108
+ durationMs,
109
+ excused: result.measurements.flatMap(
110
+ (one) => one.verdict === "skipped" ? [{ path: one.key, reason: `not measured by --tier ${one.tier}` }] : []
111
+ ),
112
+ findings: result.measurements.filter((one) => one.verdict === "grew"),
113
+ read: result.measurements.filter((one) => one.verdict !== "skipped").length,
114
+ refused: result.refusals.map((one) => ({ path: one.key, reason: one.reason })),
115
+ tool: "ratchet",
116
+ version: versionOf(import.meta.url)
117
+ });
103
118
  var measure = async () => {
104
119
  if (holdMs !== void 0) return holdTheLock(holdMs);
105
120
  if (proving) {
@@ -112,8 +127,17 @@ var measure = async () => {
112
127
  process.stdout.write(formatProve(proof));
113
128
  return proof.proven ? 0 : 2;
114
129
  }
130
+ const startedAt = Date.now();
115
131
  const result = await runRatchet({ counters: COUNTERS, cwd, tier });
116
132
  process.stdout.write(formatReport(result));
133
+ const at = writeEnvelope({
134
+ envelope: envelopeOf(result, Date.now() - startedAt),
135
+ next: ENVELOPE_NEXT,
136
+ root: cwd
137
+ });
138
+ process.stdout.write(`envelope: ${at}
139
+ `);
140
+ if (result.refusals.length > 0) return 2;
117
141
  return result.measurements.some((one) => one.verdict === "grew") ? 1 : 0;
118
142
  };
119
143
  try {
package/dist/index.d.ts CHANGED
@@ -76,6 +76,14 @@ type Counter = {
76
76
  * nobody configured says nothing about the mode they did.
77
77
  */
78
78
  probe?: CounterProbe | CounterProbe[];
79
+ /**
80
+ * The params that change WHAT this counter reads (a `match`), as opposed to where it looks.
81
+ * #140: a configured reading the probe never exercises can be wrong in a way the probe cannot
82
+ * see — during.day's mis-escaped match read 0, was certified PROVEN, and the false win was
83
+ * banked into the baseline. A config customizing one of these must declare `probe: { sample,
84
+ * expect }` beside it, or the prove line says UNPROVEN.
85
+ */
86
+ readingParams?: string[];
79
87
  run: (context: CounterContext) => Promise<number>;
80
88
  };
81
89
  /** One line of `geonosis.ratchet.json`'s `counters` array. */
@@ -134,6 +142,11 @@ type Proof = {
134
142
  key: string;
135
143
  reason: string;
136
144
  verdict: 'cannot-measure';
145
+ } | {
146
+ counter: string;
147
+ key: string;
148
+ reason: string;
149
+ verdict: 'unproven';
137
150
  }
138
151
  /**
139
152
  * Not a counter: `--exclusive` itself, measured by running two of it. A lock is a claim about the
@@ -157,8 +170,24 @@ type ProveResult = {
157
170
  /** False as soon as one counter could not be shown to read its own planted finding. */
158
171
  proven: boolean;
159
172
  };
173
+ /** A counter whose tool could not be read. Never a number, and never a silent zero. */
174
+ type Refusal = {
175
+ key: string;
176
+ reason: string;
177
+ };
160
178
  type RatchetResult = {
179
+ /**
180
+ * How many counters the config named. The denominator: `considered === measurements.length +
181
+ * refusals.length`, and a run whose two do not add up has lost one somewhere between the config
182
+ * and the report.
183
+ */
184
+ considered: number;
161
185
  measurements: Measurement[];
186
+ /**
187
+ * Every counter that blew up, not just the first. The throw that used to leave the run at the
188
+ * first failure also left every counter behind it unrun and unreported.
189
+ */
190
+ refusals: Refusal[];
162
191
  /** True when the baseline file was rewritten because something shrank. */
163
192
  rewritten: boolean;
164
193
  };
@@ -178,6 +207,80 @@ declare const loadConfig: (cwd: string) => RatchetConfig;
178
207
  */
179
208
  declare const resolveBaseline: (cwd: string) => string;
180
209
 
210
+ /**
211
+ * The report envelope: what a tool CONSIDERED, against what it actually read.
212
+ *
213
+ * Four denominator bugs landed in one day — a migrations run that reported on three of four files,
214
+ * a plan check that printed `PASS — 0 plan(s)` over a directory of twenty-one, a parity run over a
215
+ * tree the second config ignored, a chains validator that walked a list it had already filtered.
216
+ * Every one of them was GREEN, and every one of them was green because the tool published the
217
+ * numerator and nobody published the denominator.
218
+ *
219
+ * So every gate writes this, and the law is arithmetic:
220
+ *
221
+ * considered === read + refused.length + excused.length
222
+ *
223
+ * checked HERE, at write time. A tool whose own accounting does not add up has not measured what it
224
+ * says it measured, so it renders no verdict at all: the write throws, the file is not created, and
225
+ * the bin exits 2. That is deliberately harsher than a failing gate — a wrong verdict is worse than
226
+ * no verdict, and a tool that has lost count of its own inputs cannot tell which it is.
227
+ *
228
+ * `refused` and `excused` are both "considered and NOT read". They differ in whose fault it is:
229
+ * refused is the tool unable to read the thing (an unparseable migration, a counter whose command
230
+ * blew up), excused is the thing legitimately exempt (a contract-migration marker the gate believed,
231
+ * a step the run never reached, a plan written before the contract). Both carry a reason, because a
232
+ * bucket without reasons is just a smaller silence.
233
+ */
234
+ type EnvelopeEntry = {
235
+ /** What was not read — a file, a counter key, a step id. Whatever the tool names its inputs by. */
236
+ path: string;
237
+ /** Why it was not read, in a sentence a reader can act on. */
238
+ reason: string;
239
+ };
240
+ type ReportEnvelope = {
241
+ /** Everything this run took as its input. The denominator. */
242
+ considered: number;
243
+ durationMs: number;
244
+ excused: EnvelopeEntry[];
245
+ /** Whatever the tool found. Shape is the tool's own; nothing here reads into it. */
246
+ findings: unknown[];
247
+ /** How many of `considered` the run actually read. The numerator. */
248
+ read: number;
249
+ refused: EnvelopeEntry[];
250
+ /** The name the envelope is filed under, and the name a refusal is reported against. */
251
+ tool: string;
252
+ /** The build that wrote it. An envelope that cannot name its build dates nothing. */
253
+ version: string;
254
+ };
255
+ /** Where every tool writes, and the one directory the doctor's envelope check reads. */
256
+ declare const ENVELOPES_DIR = ".geonosis/envelopes";
257
+ declare const envelopePath: (root: string, tool: string) => string;
258
+ /** A run whose accounting does not add up. Never a verdict — the refusal to render one. */
259
+ declare class UnbalancedEnvelope extends Error {
260
+ constructor(message: string);
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
+ declare const writeEnvelope: ({ envelope, next, root, }: {
269
+ envelope: ReportEnvelope;
270
+ /** The command a reader should run to see the accounting for themselves. */
271
+ next: string;
272
+ root: string;
273
+ }) => string;
274
+ /**
275
+ * The version of the package a module belongs to, found by walking up to the nearest manifest.
276
+ *
277
+ * Called with `import.meta.url`, it answers the same in `src/` under vitest and in the bundled
278
+ * `dist/` a bin loads — which is the whole point, because the envelope a consumer's CI writes is
279
+ * written by the build, and a version baked in at compile time would be the version of whoever
280
+ * bundled it.
281
+ */
282
+ declare const versionOf: (moduleUrl: string) => string;
283
+
181
284
  declare const COUNTERS: Counter[];
182
285
  declare const counterById: (id: string) => Counter;
183
286
 
@@ -251,7 +354,7 @@ declare const runRatchet: ({ counters, cwd, tier, }: {
251
354
  * the tier left out says so by name — printing a number for it would be the stale OK this exists
252
355
  * to prevent.
253
356
  */
254
- declare const formatReport: ({ measurements, rewritten }: RatchetResult) => string;
357
+ declare const formatReport: ({ measurements, refusals, rewritten }: RatchetResult) => string;
255
358
  declare const formatProve: ({ proofs, proven }: ProveResult) => string;
256
359
 
257
360
  /**
@@ -270,4 +373,4 @@ declare const formatProve: ({ proofs, proven }: ProveResult) => string;
270
373
  */
271
374
  declare const runCommand: (cwd: string, counterId: string, env?: NodeJS.ProcessEnv) => (command: string) => CommandResult;
272
375
 
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 };
376
+ export { CONFIG_FILE, COUNTERS, type CommandResult, type Counter, type CounterConfig, type CounterContext, CounterError, type CounterProbe, ENVELOPES_DIR, type EnvelopeEntry, type Holder, type Measurement, type Proof, type ProveResult, type RatchetConfig, type RatchetResult, type Refusal, type ReportEnvelope, UnbalancedEnvelope, type Verdict, acquireExclusive, counterById, envelopePath, formatProve, formatReport, heavyLockPath, keyOf, loadConfig, resolveBaseline, runCommand, runProve, runRatchet, versionOf, writeEnvelope };
package/dist/index.js CHANGED
@@ -2,8 +2,11 @@ import {
2
2
  CONFIG_FILE,
3
3
  COUNTERS,
4
4
  CounterError,
5
+ ENVELOPES_DIR,
6
+ UnbalancedEnvelope,
5
7
  acquireExclusive,
6
8
  counterById,
9
+ envelopePath,
7
10
  formatProve,
8
11
  formatReport,
9
12
  heavyLockPath,
@@ -12,14 +15,19 @@ import {
12
15
  resolveBaseline,
13
16
  runCommand,
14
17
  runProve,
15
- runRatchet
16
- } from "./chunk-HBM6F3GO.js";
18
+ runRatchet,
19
+ versionOf,
20
+ writeEnvelope
21
+ } from "./chunk-ESN6ULZZ.js";
17
22
  export {
18
23
  CONFIG_FILE,
19
24
  COUNTERS,
20
25
  CounterError,
26
+ ENVELOPES_DIR,
27
+ UnbalancedEnvelope,
21
28
  acquireExclusive,
22
29
  counterById,
30
+ envelopePath,
23
31
  formatProve,
24
32
  formatReport,
25
33
  heavyLockPath,
@@ -28,5 +36,7 @@ export {
28
36
  resolveBaseline,
29
37
  runCommand,
30
38
  runProve,
31
- runRatchet
39
+ runRatchet,
40
+ versionOf,
41
+ writeEnvelope
32
42
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/ratchet",
3
- "version": "1.3.0",
3
+ "version": "1.4.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": [