@geonosis/ratchet 2.5.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @geonosis/ratchet
2
2
 
3
+ ## 2.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 7f24da6: A machine-wide heavy budget, two slots, held by every tool that spends one (#385): measured load 48
8
+ with during.day's Stop-hook tier, this kit's Stop-hook tier, its ratchet and an engineer's suite all
9
+ running at once — nothing held the two-slot budget across the two repos, and three kit tests read
10
+ red under it that were green alone. `@geonosis/ratchet` ships `takeSlot`/`heavySlotDir` (a lock
11
+ directory under `os.tmpdir()/geonosis-heavy`, two slot files each holding `{ pid, command, cwd,
12
+ startedAt }`, a dead holder reaped on the next take, a taker that finds both held waiting once
13
+ before refusing naming them); `geonosis-verify` (each tier run) and `geonosis-ratchet` (the run, not
14
+ `--prove`'s inner unit) take a slot for the run's duration. `@geonosis/release`'s `smoke
15
+ baseline`/`smoke run` take one too, from their own copy (this layer imports nothing, same posture as
16
+ `envelope.ts`). The plugin's Stop hook reads a fresh green `gate-report.<tier>.json` — its own
17
+ recorded commit equal to the current `HEAD` with a clean tree — and reports PASS without re-running;
18
+ otherwise it peeks the same two slots (never claims one itself, or the verify child it is about to
19
+ spawn would be racing against its own reservation) and blocks naming both holders rather than
20
+ running a third tier.
21
+
3
22
  ## 2.5.0
4
23
 
5
24
  ## 2.4.3
@@ -9,11 +9,19 @@ import { dirname, join } from 'node:path'
9
9
  import { fileURLToPath } from 'node:url'
10
10
 
11
11
  const here = dirname(fileURLToPath(import.meta.url))
12
- const newest = (dir) =>
12
+ // tsup never reads a test file, so one counting as source made every bin refuse a build that had
13
+ // not gone stale (#399).
14
+ const NOT_SOURCE = (name) =>
15
+ name.endsWith('.test.ts') ||
16
+ name.endsWith('.test.tsx') ||
17
+ name === '__tests__' ||
18
+ name === '__fixtures__'
19
+ const newest = (dir, skip) =>
13
20
  existsSync(dir)
14
21
  ? readdirSync(dir, { withFileTypes: true }).reduce((most, entry) => {
22
+ if (skip !== undefined && skip(entry.name)) return most
15
23
  const at = join(dir, entry.name)
16
- return Math.max(most, entry.isDirectory() ? newest(at) : statSync(at).mtimeMs)
24
+ return Math.max(most, entry.isDirectory() ? newest(at, skip) : statSync(at).mtimeMs)
17
25
  }, 0)
18
26
  : 0
19
27
  const src = join(here, '..', 'src')
@@ -30,7 +38,7 @@ if (existsSync(src)) {
30
38
  )
31
39
  process.exit(2)
32
40
  }
33
- if (newest(src) > built) {
41
+ if (newest(src, NOT_SOURCE) > built) {
34
42
  process.stderr.write(
35
43
  'geonosis-ratchet: dist is older than src — run pnpm build before trusting this bin.\n',
36
44
  )
@@ -70,23 +70,53 @@ var versionOf = (moduleUrl) => {
70
70
  }
71
71
  };
72
72
 
73
- // src/core/lock.ts
73
+ // src/core/heavy-slot.ts
74
+ import { readFileSync as readFileSync2, rmSync as rmSync2 } from "fs";
75
+ import { tmpdir } from "os";
76
+ import { join as join2 } from "path";
77
+
78
+ // src/core/file-claim.ts
74
79
  import { randomUUID } from "crypto";
75
- import { linkSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
76
- import { homedir } from "os";
77
- import { dirname as dirname2, isAbsolute, join as join2 } from "path";
78
- var heavyLockPath = () => {
79
- const named = process.env.GEONOSIS_HEAVY_LOCK;
80
- if (named !== void 0 && named !== "") return named;
81
- const declared = process.env.XDG_CACHE_HOME;
82
- const cache = declared !== void 0 && isAbsolute(declared) ? declared : join2(homedir(), ".cache");
83
- return join2(cache, "geonosis", "heavy.lock");
80
+ import { linkSync, mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
81
+ import { dirname as dirname2 } from "path";
82
+ var alive = (pid) => {
83
+ try {
84
+ process.kill(pid, 0);
85
+ return true;
86
+ } catch (error) {
87
+ return error.code === "EPERM";
88
+ }
84
89
  };
85
90
  var sleep = (ms) => new Promise((done) => setTimeout(done, ms));
91
+ var claimFile = (path, holder) => {
92
+ mkdirSync2(dirname2(path), { recursive: true });
93
+ const staging = `${path}.${holder.pid}.${randomUUID()}`;
94
+ try {
95
+ writeFileSync2(staging, JSON.stringify(holder));
96
+ linkSync(staging, path);
97
+ return true;
98
+ } catch (error) {
99
+ if (error.code === "EEXIST") return false;
100
+ throw error;
101
+ } finally {
102
+ rmSync(staging, { force: true });
103
+ }
104
+ };
105
+
106
+ // src/core/heavy-slot.ts
107
+ var HEAVY_SLOTS = 2;
108
+ var DEFAULT_WAIT_MS = 20 * 6e4;
109
+ var heavySlotDir = () => {
110
+ const named = process.env.GEONOSIS_HEAVY_SLOT_DIR;
111
+ if (named !== void 0 && named !== "") return named;
112
+ return join2(tmpdir(), "geonosis-heavy");
113
+ };
114
+ var slotPath = (dir, at) => join2(dir, `slot-${at}.json`);
86
115
  var holderOf = (path) => {
87
116
  try {
88
117
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
89
118
  return typeof parsed.pid === "number" ? {
119
+ command: parsed.command ?? "unknown",
90
120
  cwd: parsed.cwd ?? "somewhere",
91
121
  pid: parsed.pid,
92
122
  startedAt: parsed.startedAt ?? "unknown"
@@ -95,12 +125,79 @@ var holderOf = (path) => {
95
125
  return void 0;
96
126
  }
97
127
  };
98
- var alive = (pid) => {
128
+ var reapIfDead = (path) => {
129
+ const holder = holderOf(path);
130
+ if (holder !== void 0 && !alive(holder.pid)) rmSync2(path, { force: true });
131
+ };
132
+ var claim = (path, holder) => claimFile(path, holder);
133
+ var namedHolders = (dir) => Array.from({ length: HEAVY_SLOTS }, (_unused, at) => holderOf(slotPath(dir, at))).filter(
134
+ (holder) => holder !== void 0
135
+ );
136
+ var describeHolders = (holders) => holders.map((one) => `${one.pid} ${one.command} since ${one.startedAt}`).join(", ");
137
+ var HeavySlotTimeout = class extends Error {
138
+ };
139
+ var takeSlot = async ({
140
+ command,
141
+ cwd = process.cwd(),
142
+ dir = heavySlotDir(),
143
+ pollMs = 250,
144
+ say = (line) => process.stderr.write(`${line}
145
+ `),
146
+ waitMs = Number(process.env.GEONOSIS_HEAVY_WAIT_MS ?? "") || DEFAULT_WAIT_MS
147
+ }) => {
148
+ const until = Date.now() + waitMs;
149
+ let told = false;
150
+ for (; ; ) {
151
+ for (let at = 0; at < HEAVY_SLOTS; at += 1) {
152
+ const path = slotPath(dir, at);
153
+ reapIfDead(path);
154
+ const mine = {
155
+ command,
156
+ cwd,
157
+ pid: process.pid,
158
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
159
+ };
160
+ if (claim(path, mine)) {
161
+ return () => {
162
+ if (holderOf(path)?.pid === process.pid) rmSync2(path, { force: true });
163
+ };
164
+ }
165
+ }
166
+ const holders = namedHolders(dir);
167
+ if (!told) {
168
+ told = true;
169
+ say(`waiting for a heavy slot: ${holders.length} held \u2014 ${describeHolders(holders)}`);
170
+ }
171
+ if (Date.now() >= until) {
172
+ throw new HeavySlotTimeout(
173
+ `waited ${waitMs} ms for a heavy slot: ${holders.length} held \u2014 ${describeHolders(holders)}`
174
+ );
175
+ }
176
+ await sleep(pollMs);
177
+ }
178
+ };
179
+
180
+ // src/core/lock.ts
181
+ import { readFileSync as readFileSync3, rmSync as rmSync3 } from "fs";
182
+ import { homedir } from "os";
183
+ import { isAbsolute, join as join3 } from "path";
184
+ var heavyLockPath = () => {
185
+ const named = process.env.GEONOSIS_HEAVY_LOCK;
186
+ if (named !== void 0 && named !== "") return named;
187
+ const declared = process.env.XDG_CACHE_HOME;
188
+ const cache = declared !== void 0 && isAbsolute(declared) ? declared : join3(homedir(), ".cache");
189
+ return join3(cache, "geonosis", "heavy.lock");
190
+ };
191
+ var holderOf2 = (path) => {
99
192
  try {
100
- process.kill(pid, 0);
101
- return true;
102
- } catch (error) {
103
- return error.code === "EPERM";
193
+ const parsed = JSON.parse(readFileSync3(path, "utf8"));
194
+ return typeof parsed.pid === "number" ? {
195
+ cwd: parsed.cwd ?? "somewhere",
196
+ pid: parsed.pid,
197
+ startedAt: parsed.startedAt ?? "unknown"
198
+ } : void 0;
199
+ } catch {
200
+ return void 0;
104
201
  }
105
202
  };
106
203
  var heldFor = (holder) => {
@@ -108,25 +205,7 @@ var heldFor = (holder) => {
108
205
  if (Number.isNaN(since)) return "an unknown time";
109
206
  return `${Math.round((Date.now() - since) / 1e3)}s`;
110
207
  };
111
- var write = (path) => {
112
- mkdirSync2(dirname2(path), { recursive: true });
113
- const mine = {
114
- cwd: process.cwd(),
115
- pid: process.pid,
116
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
117
- };
118
- const staging = `${path}.${process.pid}.${randomUUID()}`;
119
- try {
120
- writeFileSync2(staging, JSON.stringify(mine));
121
- linkSync(staging, path);
122
- return true;
123
- } catch (error) {
124
- if (error.code === "EEXIST") return false;
125
- throw error;
126
- } finally {
127
- rmSync(staging, { force: true });
128
- }
129
- };
208
+ var write = (path) => claimFile(path, { cwd: process.cwd(), pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
130
209
  var acquireExclusive = async ({
131
210
  noticeMs = 15e3,
132
211
  path = heavyLockPath(),
@@ -138,16 +217,16 @@ var acquireExclusive = async ({
138
217
  const until = Date.now() + timeoutSeconds * 1e3;
139
218
  let told = 0;
140
219
  const release = () => {
141
- if (holderOf(path)?.pid === process.pid) rmSync(path, { force: true });
220
+ if (holderOf2(path)?.pid === process.pid) rmSync3(path, { force: true });
142
221
  };
143
222
  for (; ; ) {
144
223
  if (write(path)) return release;
145
- const holder = holderOf(path);
224
+ const holder = holderOf2(path);
146
225
  if (holder === void 0 || !alive(holder.pid)) {
147
226
  say(
148
227
  `geonosis-ratchet: taking over a stale heavy lock (pid ${holder?.pid ?? "unreadable"} is gone)`
149
228
  );
150
- rmSync(path, { force: true });
229
+ rmSync3(path, { force: true });
151
230
  continue;
152
231
  }
153
232
  if (Date.now() >= until) {
@@ -167,7 +246,7 @@ var acquireExclusive = async ({
167
246
  };
168
247
 
169
248
  // src/core/config.ts
170
- import { existsSync, readFileSync as readFileSync3 } from "fs";
249
+ import { existsSync, readFileSync as readFileSync4 } from "fs";
171
250
  import { resolve as resolve2 } from "path";
172
251
  var CONFIG_FILE = "geonosis.ratchet.json";
173
252
  var keyOf = (entry) => entry.key ?? entry.counter;
@@ -176,7 +255,7 @@ var loadConfig = (cwd) => {
176
255
  if (!existsSync(path)) {
177
256
  throw new Error(`no ${CONFIG_FILE} in ${cwd} \u2014 the ratchet has nothing to count`);
178
257
  }
179
- const parsed = JSON.parse(readFileSync3(path, "utf8"));
258
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
180
259
  if (!Array.isArray(parsed.counters)) {
181
260
  throw new Error(`${CONFIG_FILE} has no "counters" array`);
182
261
  }
@@ -204,7 +283,7 @@ var loadConfig = (cwd) => {
204
283
  };
205
284
  var resolveBaseline = (cwd) => {
206
285
  try {
207
- const parsed = JSON.parse(readFileSync3(resolve2(cwd, CONFIG_FILE), "utf8"));
286
+ const parsed = JSON.parse(readFileSync4(resolve2(cwd, CONFIG_FILE), "utf8"));
208
287
  return typeof parsed.baseline === "string" ? parsed.baseline : "gate-baseline.json";
209
288
  } catch {
210
289
  return "gate-baseline.json";
@@ -250,9 +329,9 @@ ${output.trim()}`
250
329
  };
251
330
 
252
331
  // src/core/prove.ts
253
- import { existsSync as existsSync2, mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
254
- import { tmpdir } from "os";
255
- import { delimiter, dirname as dirname4, join as join3, resolve as resolve4 } from "path";
332
+ import { existsSync as existsSync2, mkdtempSync, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
333
+ import { tmpdir as tmpdir2 } from "os";
334
+ import { delimiter, dirname as dirname4, join as join4, resolve as resolve4 } from "path";
256
335
 
257
336
  // src/core/exclusive.ts
258
337
  import { spawn } from "child_process";
@@ -323,7 +402,7 @@ var toolPath = (cwd) => {
323
402
  const dirs = [];
324
403
  let dir = resolve4(cwd);
325
404
  for (; ; ) {
326
- const bin = join3(dir, "node_modules", ".bin");
405
+ const bin = join4(dir, "node_modules", ".bin");
327
406
  if (existsSync2(bin)) dirs.push(bin);
328
407
  const parent = dirname4(dir);
329
408
  if (parent === dir) break;
@@ -333,7 +412,7 @@ var toolPath = (cwd) => {
333
412
  };
334
413
  var NO_PROBE = "no probe \u2014 a counter nobody has seen read a planted finding has not been shown to measure";
335
414
  var oneProofOf = async (counter, key, path, probe) => {
336
- const dir = mkdtempSync(join3(tmpdir(), "geonosis-prove-"));
415
+ const dir = mkdtempSync(join4(tmpdir2(), "geonosis-prove-"));
337
416
  try {
338
417
  probe.input(dir);
339
418
  const command = probe.command?.(dir);
@@ -351,7 +430,7 @@ var oneProofOf = async (counter, key, path, probe) => {
351
430
  } catch (error) {
352
431
  return { counter: counter.id, key, reason: error.message, verdict: "cannot-measure" };
353
432
  } finally {
354
- rmSync2(dir, { force: true, recursive: true });
433
+ rmSync4(dir, { force: true, recursive: true });
355
434
  }
356
435
  };
357
436
  var probesOf = (counter) => {
@@ -417,7 +496,7 @@ var runProve = async ({
417
496
  const entryProbe = {
418
497
  command: () => "cat sample.txt",
419
498
  expect: declared.expect,
420
- input: (dir) => writeFileSync3(join3(dir, "sample.txt"), sample),
499
+ input: (dir) => writeFileSync3(join4(dir, "sample.txt"), sample),
421
500
  name: "configured reading",
422
501
  params: Object.fromEntries(customized.map((one) => [one, entry[one]]))
423
502
  };
@@ -434,20 +513,20 @@ var runProve = async ({
434
513
  }
435
514
  }
436
515
  if (exclusiveVia !== void 0) {
437
- const dir = mkdtempSync(join3(tmpdir(), "geonosis-lock-"));
516
+ const dir = mkdtempSync(join4(tmpdir2(), "geonosis-lock-"));
438
517
  try {
439
- const proof = await proveExclusive({ cli: exclusiveVia, lockPath: join3(dir, "heavy.lock") });
518
+ const proof = await proveExclusive({ cli: exclusiveVia, lockPath: join4(dir, "heavy.lock") });
440
519
  proofs.push(proof);
441
520
  if (proof.verdict !== "serialised") return { proofs, proven: false };
442
521
  } finally {
443
- rmSync2(dir, { force: true, recursive: true });
522
+ rmSync4(dir, { force: true, recursive: true });
444
523
  }
445
524
  }
446
525
  return { proofs, proven: true };
447
526
  };
448
527
 
449
528
  // src/core/ratchet.ts
450
- import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
529
+ import { existsSync as existsSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
451
530
  import { resolve as resolve5 } from "path";
452
531
  var EVIDENCE_LINES = 10;
453
532
  var recorded = (counter, params, run) => {
@@ -499,7 +578,7 @@ var runRatchet = async ({
499
578
  if (!existsSync3(baselinePath)) {
500
579
  throw new Error(`no ${config.baseline} in ${cwd} \u2014 nothing to ratchet against`);
501
580
  }
502
- const baseline = JSON.parse(readFileSync4(baselinePath, "utf8"));
581
+ const baseline = JSON.parse(readFileSync5(baselinePath, "utf8"));
503
582
  const byId = new Map(counters.map((one) => [one.id, one]));
504
583
  const measurements = [];
505
584
  const refusals = [];
@@ -581,9 +660,9 @@ var escapeForRegex = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
581
660
 
582
661
  // src/counters/plant.ts
583
662
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
584
- import { dirname as dirname5, join as join4 } from "path";
663
+ import { dirname as dirname5, join as join5 } from "path";
585
664
  var plant = (dir, relative, contents) => {
586
- const path = join4(dir, relative);
665
+ const path = join5(dir, relative);
587
666
  mkdirSync3(dirname5(path), { recursive: true });
588
667
  writeFileSync5(path, contents);
589
668
  };
@@ -674,8 +753,8 @@ var bundleBytes = {
674
753
  };
675
754
 
676
755
  // src/counters/ci.ts
677
- import { readdirSync, readFileSync as readFileSync5 } from "fs";
678
- import { join as join5, resolve as resolve6 } from "path";
756
+ import { readdirSync, readFileSync as readFileSync6 } from "fs";
757
+ import { join as join6, resolve as resolve6 } from "path";
679
758
  var DISABLED = /\bif:[ \t]*false\b/;
680
759
  var WORKFLOW = /\.ya?ml$/;
681
760
  var disabledCiJobs = {
@@ -693,7 +772,7 @@ var disabledCiJobs = {
693
772
  const dir = resolve6(cwd, relative);
694
773
  let files;
695
774
  try {
696
- files = readdirSync(dir, { withFileTypes: true }).filter((entry) => WORKFLOW.test(entry.name)).map((entry) => join5(dir, entry.name));
775
+ files = readdirSync(dir, { withFileTypes: true }).filter((entry) => WORKFLOW.test(entry.name)).map((entry) => join6(dir, entry.name));
697
776
  } catch {
698
777
  return 0;
699
778
  }
@@ -701,7 +780,7 @@ var disabledCiJobs = {
701
780
  for (const file of files) {
702
781
  let contents;
703
782
  try {
704
- contents = readFileSync5(file, "utf8");
783
+ contents = readFileSync6(file, "utf8");
705
784
  } catch (error) {
706
785
  throw new CounterError(
707
786
  "disabledCiJobs",
@@ -790,10 +869,10 @@ ${said.slice(-500)}`
790
869
  return Number(found);
791
870
  }
792
871
  };
793
- var HEADING = /^([A-Z][A-Za-z ]+?) \((\d+)\)\s*$/gm;
872
+ var HEADING_LINE = /^([A-Z][A-Za-z ]+?) \((\d+)\)\s*$/;
794
873
  var headingsIn = (output) => {
795
874
  const found = /* @__PURE__ */ new Map();
796
- for (const [, heading = "", count = "0"] of output.matchAll(HEADING)) {
875
+ for (const [, heading = "", count = "0"] of output.matchAll(new RegExp(HEADING_LINE, "gm"))) {
797
876
  found.set(heading, Number(count));
798
877
  }
799
878
  return found;
@@ -804,7 +883,6 @@ var excusesIn = (params) => {
804
883
  typeof declared === "object" && declared !== null && !Array.isArray(declared) ? Object.keys(declared) : []
805
884
  );
806
885
  };
807
- var HEADING_LINE = /^([A-Z][A-Za-z ]+?) \((\d+)\)\s*$/;
808
886
  var UNRESOLVED = "Unresolved imports";
809
887
  var KNIP_DEFAULT = "npx knip --reporter compact";
810
888
  var knipEvidence = ({
@@ -864,7 +942,7 @@ var boundaryIssues = {
864
942
  };
865
943
 
866
944
  // src/counters/dx.ts
867
- import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
945
+ import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
868
946
  import { resolve as resolve7 } from "path";
869
947
  var DEFAULT_BUMP_REPORT = ".geonosis/bump-report.json";
870
948
  var bumpFile = (id, cwd, params) => {
@@ -877,7 +955,7 @@ var bumpFile = (id, cwd, params) => {
877
955
  );
878
956
  }
879
957
  try {
880
- return JSON.parse(readFileSync6(path, "utf8"));
958
+ return JSON.parse(readFileSync7(path, "utf8"));
881
959
  } catch (error) {
882
960
  throw new CounterError(id, `${relative} is not readable JSON: ${error.message}`);
883
961
  }
@@ -963,7 +1041,7 @@ var openBacklogRows = {
963
1041
  if (!existsSync4(at)) {
964
1042
  throw new CounterError("openBacklogRows", `no register at ${relative}`);
965
1043
  }
966
- const lines = readFileSync6(at, "utf8").split("\n");
1044
+ const lines = readFileSync7(at, "utf8").split("\n");
967
1045
  return lines.filter((line, index) => {
968
1046
  if (!ROW.test(line) || SEPARATOR.test(line)) return false;
969
1047
  if (SEPARATOR.test(lines[index + 1] ?? "")) return false;
@@ -995,7 +1073,7 @@ var unformattedFiles = {
995
1073
  };
996
1074
 
997
1075
  // src/counters/gate-report.ts
998
- import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
1076
+ import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
999
1077
  import { resolve as resolve9 } from "path";
1000
1078
  var DEFAULT_REPORT = ".geonosis/gate-report.json";
1001
1079
  var fastTierMs = {
@@ -1029,7 +1107,7 @@ var fastTierMs = {
1029
1107
  }
1030
1108
  let report;
1031
1109
  try {
1032
- report = JSON.parse(readFileSync7(path, "utf8"));
1110
+ report = JSON.parse(readFileSync8(path, "utf8"));
1033
1111
  } catch (error) {
1034
1112
  throw new CounterError(
1035
1113
  "fastTierMs",
@@ -1051,7 +1129,7 @@ var fastTierMs = {
1051
1129
  };
1052
1130
 
1053
1131
  // src/counters/law.ts
1054
- import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
1132
+ import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
1055
1133
  import { resolve as resolve10 } from "path";
1056
1134
  var lawLineCount = {
1057
1135
  id: "lawLineCount",
@@ -1064,12 +1142,12 @@ var lawLineCount = {
1064
1142
  const relative = stringParam("lawLineCount", params, "path", "CLAUDE.md");
1065
1143
  const path = resolve10(cwd, relative);
1066
1144
  if (!existsSync7(path)) throw new CounterError("lawLineCount", `no law file at ${relative}`);
1067
- return readFileSync8(path, "utf8").replace(/\n$/, "").split("\n").length;
1145
+ return readFileSync9(path, "utf8").replace(/\n$/, "").split("\n").length;
1068
1146
  }
1069
1147
  };
1070
1148
 
1071
1149
  // src/counters/oxlint.ts
1072
- import { existsSync as existsSync8, readFileSync as readFileSync9, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
1150
+ import { existsSync as existsSync8, readFileSync as readFileSync10, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
1073
1151
  import { resolve as resolve11 } from "path";
1074
1152
  var DEFAULT_COMMAND = "npx oxlint --format=unix --config .oxlintrc.json .";
1075
1153
  var PROBE_COMMAND = "oxlint --format=unix --config .oxlintrc.json .";
@@ -1236,7 +1314,7 @@ var oxlintRule = {
1236
1314
  const source = resolve11(cwd, config);
1237
1315
  if (!existsSync8(source)) throw new CounterError("oxlintRule", `no config at ${config}`);
1238
1316
  const strictName = `.oxlintrc.ratchet-${key}.json`;
1239
- const strict = readFileSync9(source, "utf8").replace(
1317
+ const strict = readFileSync10(source, "utf8").replace(
1240
1318
  new RegExp(`("[^"]*${escapeForRegex(rule)}"\\s*:\\s*\\[?\\s*)"(warn|off)"`),
1241
1319
  '$1"error"'
1242
1320
  );
@@ -1244,15 +1322,15 @@ var oxlintRule = {
1244
1322
  try {
1245
1323
  return countRule(run(command.replace("{config}", strictName)), rule, expect);
1246
1324
  } finally {
1247
- rmSync3(resolve11(cwd, strictName), { force: true });
1325
+ rmSync5(resolve11(cwd, strictName), { force: true });
1248
1326
  }
1249
1327
  }
1250
1328
  };
1251
1329
 
1252
1330
  // src/counters/probes.ts
1253
- import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
1331
+ import { existsSync as existsSync9, readFileSync as readFileSync11 } from "fs";
1254
1332
  import { createRequire } from "module";
1255
- import { join as join6, resolve as resolve12 } from "path";
1333
+ import { join as join7, resolve as resolve12 } from "path";
1256
1334
  import { pathToFileURL } from "url";
1257
1335
  var ID2 = "probelessRules";
1258
1336
  var OFF = /* @__PURE__ */ new Set([0, "0", "allow", "off", false]);
@@ -1274,7 +1352,7 @@ var enabledIn = (config, namespace) => {
1274
1352
  var probedBy = async (cwd, plugin) => {
1275
1353
  let entry;
1276
1354
  try {
1277
- entry = createRequire(join6(cwd, "noop.js")).resolve(plugin);
1355
+ entry = createRequire(join7(cwd, "noop.js")).resolve(plugin);
1278
1356
  } catch (error) {
1279
1357
  throw new CounterError(
1280
1358
  ID2,
@@ -1334,7 +1412,7 @@ var probelessRules = {
1334
1412
  if (!existsSync9(path)) throw new CounterError(ID2, `no oxlint config at ${relative}`);
1335
1413
  let config;
1336
1414
  try {
1337
- config = JSON.parse(readFileSync10(path, "utf8"));
1415
+ config = JSON.parse(readFileSync11(path, "utf8"));
1338
1416
  } catch (error) {
1339
1417
  throw new CounterError(ID2, `${relative} does not parse: ${error.message}`);
1340
1418
  }
@@ -1368,15 +1446,15 @@ var runtimeCodeShipped = {
1368
1446
 
1369
1447
  // src/counters/scripts.ts
1370
1448
  import { readdirSync as readdirSync3 } from "fs";
1371
- import { join as join8 } from "path";
1449
+ import { join as join9 } from "path";
1372
1450
 
1373
1451
  // src/counters/workspace.ts
1374
- import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync11 } from "fs";
1375
- import { join as join7, resolve as resolve13 } from "path";
1452
+ import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync12 } from "fs";
1453
+ import { join as join8, resolve as resolve13 } from "path";
1376
1454
  var SKIP = /^(node_modules|\.)/;
1377
1455
  var childDirs = (dir) => {
1378
1456
  try {
1379
- return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !SKIP.test(entry.name)).map((entry) => join7(dir, entry.name));
1457
+ return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !SKIP.test(entry.name)).map((entry) => join8(dir, entry.name));
1380
1458
  } catch {
1381
1459
  return [];
1382
1460
  }
@@ -1386,14 +1464,14 @@ var expand = (root, pattern) => {
1386
1464
  const segments = pattern.split("/").filter((one) => one !== "" && one !== ".");
1387
1465
  let dirs = [root];
1388
1466
  for (const segment of segments) {
1389
- dirs = segment === "*" ? dirs.flatMap((dir) => childDirs(dir)) : segment === "**" ? dirs.flatMap((dir) => descendants(dir, 3)) : dirs.map((dir) => join7(dir, segment)).filter((dir) => existsSync10(dir));
1467
+ dirs = segment === "*" ? dirs.flatMap((dir) => childDirs(dir)) : segment === "**" ? dirs.flatMap((dir) => descendants(dir, 3)) : dirs.map((dir) => join8(dir, segment)).filter((dir) => existsSync10(dir));
1390
1468
  }
1391
1469
  return dirs;
1392
1470
  };
1393
1471
  var QUOTED = /^['"]|['"]$/g;
1394
1472
  var cleaned = (value) => value.replace(/#.*$/, "").trim().replaceAll(QUOTED, "");
1395
1473
  var pnpmPatterns = (path) => {
1396
- const lines = readFileSync11(path, "utf8").split("\n");
1474
+ const lines = readFileSync12(path, "utf8").split("\n");
1397
1475
  const at = lines.findIndex((line) => line.startsWith("packages:"));
1398
1476
  if (at === -1) return [];
1399
1477
  const inline = lines[at]?.slice("packages:".length).trim() ?? "";
@@ -1412,31 +1490,25 @@ var pnpmPatterns = (path) => {
1412
1490
  return patterns;
1413
1491
  };
1414
1492
  var npmPatterns = (path) => {
1415
- const parsed = JSON.parse(readFileSync11(path, "utf8"));
1493
+ const parsed = JSON.parse(readFileSync12(path, "utf8"));
1416
1494
  const declared = Array.isArray(parsed.workspaces) ? parsed.workspaces : parsed.workspaces?.packages ?? [];
1417
1495
  return declared.filter((one) => typeof one === "string");
1418
1496
  };
1419
1497
  var nameOf = (dir) => {
1420
- const manifest = join7(dir, "package.json");
1421
- if (!existsSync10(manifest)) return void 0;
1422
- try {
1423
- const { name } = JSON.parse(readFileSync11(manifest, "utf8"));
1424
- return typeof name === "string" && name !== "" ? name : void 0;
1425
- } catch {
1426
- return void 0;
1427
- }
1498
+ const name = manifestOf(dir)?.["name"];
1499
+ return typeof name === "string" && name !== "" ? name : void 0;
1428
1500
  };
1429
1501
  var workspaceDirs = (cwd) => {
1430
1502
  const root = resolve13(cwd);
1431
- const pnpm = join7(root, "pnpm-workspace.yaml");
1432
- const manifest = join7(root, "package.json");
1503
+ const pnpm = join8(root, "pnpm-workspace.yaml");
1504
+ const manifest = join8(root, "package.json");
1433
1505
  const patterns = existsSync10(pnpm) ? pnpmPatterns(pnpm) : existsSync10(manifest) ? npmPatterns(manifest) : [];
1434
- const dirs = patterns.filter((pattern) => !pattern.startsWith("!")).flatMap((pattern) => expand(root, pattern)).filter((dir) => existsSync10(join7(dir, "package.json")));
1506
+ const dirs = patterns.filter((pattern) => !pattern.startsWith("!")).flatMap((pattern) => expand(root, pattern)).filter((dir) => existsSync10(join8(dir, "package.json")));
1435
1507
  return [...new Set(dirs)];
1436
1508
  };
1437
1509
  var manifestOf = (dir) => {
1438
1510
  try {
1439
- const parsed = JSON.parse(readFileSync11(join7(dir, "package.json"), "utf8"));
1511
+ const parsed = JSON.parse(readFileSync12(join8(dir, "package.json"), "utf8"));
1440
1512
  return typeof parsed === "object" && parsed !== null ? parsed : void 0;
1441
1513
  } catch {
1442
1514
  return void 0;
@@ -1466,7 +1538,7 @@ var holdsTests = (dir, depth = 6) => {
1466
1538
  if (entry.isDirectory()) {
1467
1539
  if (entry.name === TEST_DIR) return true;
1468
1540
  if (SKIP2.test(entry.name) || depth === 0) continue;
1469
- if (holdsTests(join8(dir, entry.name), depth - 1)) return true;
1541
+ if (holdsTests(join9(dir, entry.name), depth - 1)) return true;
1470
1542
  continue;
1471
1543
  }
1472
1544
  if (TEST_FILE.test(entry.name)) return true;
@@ -1504,8 +1576,8 @@ var packagesWithoutTypecheck = {
1504
1576
  };
1505
1577
 
1506
1578
  // src/counters/seams.ts
1507
- import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync12 } from "fs";
1508
- import { join as join9, resolve as resolve14 } from "path";
1579
+ import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync13 } from "fs";
1580
+ import { join as join10, resolve as resolve14 } from "path";
1509
1581
  var CONFIG = "geonosis.json";
1510
1582
  var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".turbo", "coverage", "dist", "node_modules"]);
1511
1583
  var asPattern = (glob) => new RegExp(
@@ -1515,22 +1587,22 @@ var asPattern = (glob) => new RegExp(
1515
1587
  );
1516
1588
  var filesUnder = (root, at = root) => readdirSync4(at, { withFileTypes: true }).flatMap((entry) => {
1517
1589
  if (SKIP_DIRS.has(entry.name)) return [];
1518
- const full = join9(at, entry.name);
1590
+ const full = join10(at, entry.name);
1519
1591
  if (entry.isDirectory()) return filesUnder(root, full);
1520
1592
  return entry.isFile() ? [full.slice(root.length + 1)] : [];
1521
1593
  });
1522
1594
  var seamGlobsIn = (root) => {
1523
- const at = join9(root, CONFIG);
1595
+ const at = join10(root, CONFIG);
1524
1596
  if (!existsSync11(at)) return [];
1525
1597
  try {
1526
- const seams = JSON.parse(readFileSync12(at, "utf8")).adoption?.seams;
1598
+ const seams = JSON.parse(readFileSync13(at, "utf8")).adoption?.seams;
1527
1599
  return Array.isArray(seams) ? seams.filter((one) => typeof one === "string") : [];
1528
1600
  } catch {
1529
1601
  return [];
1530
1602
  }
1531
1603
  };
1532
1604
  var linesIn = (path) => {
1533
- const body = readFileSync12(path, "utf8");
1605
+ const body = readFileSync13(path, "utf8");
1534
1606
  return body === "" ? 0 : body.replace(/\n$/, "").split("\n").length;
1535
1607
  };
1536
1608
  var adoptionSeamLines = {
@@ -1549,7 +1621,7 @@ var adoptionSeamLines = {
1549
1621
  const root = resolve14(cwd);
1550
1622
  const globs = seamGlobsIn(root).map(asPattern);
1551
1623
  if (globs.length === 0) return 0;
1552
- return filesUnder(root).filter((one) => globs.some((pattern) => pattern.test(one))).reduce((total2, one) => total2 + linesIn(join9(root, one)), 0);
1624
+ return filesUnder(root).filter((one) => globs.some((pattern) => pattern.test(one))).reduce((total2, one) => total2 + linesIn(join10(root, one)), 0);
1553
1625
  }
1554
1626
  };
1555
1627
 
@@ -1575,12 +1647,14 @@ var sumOfCounts = {
1575
1647
  };
1576
1648
 
1577
1649
  // src/counters/suppressions.ts
1578
- import { readdirSync as readdirSync5, readFileSync as readFileSync13 } from "fs";
1579
- import { join as join10, resolve as resolve15 } from "path";
1650
+ import { resolve as resolve15 } from "path";
1651
+
1652
+ // src/counters/code-files.ts
1653
+ import { readdirSync as readdirSync5, readFileSync as readFileSync14 } from "fs";
1654
+ import { join as join11 } from "path";
1580
1655
  var CODE_FILE = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts|vue|svelte|astro)$/;
1581
1656
  var SKIP3 = /^(?:node_modules|dist|coverage|\.[^.]|__fixtures__)/;
1582
- var SUPPRESSION = /(?:eslint|oxlint|biome)-(?:disable|ignore)(?:-next-line|-line)?|@ts-expect-error|@ts-ignore/g;
1583
- var countIn = (dir, depth = 12) => {
1657
+ var countInCodeFiles = (dir, countInFile, depth = 12) => {
1584
1658
  let entries;
1585
1659
  try {
1586
1660
  entries = readdirSync5(dir, { withFileTypes: true });
@@ -1591,17 +1665,21 @@ var countIn = (dir, depth = 12) => {
1591
1665
  for (const entry of entries) {
1592
1666
  if (SKIP3.test(entry.name)) continue;
1593
1667
  if (entry.isDirectory()) {
1594
- if (depth > 0) found += countIn(join10(dir, entry.name), depth - 1);
1668
+ if (depth > 0) found += countInCodeFiles(join11(dir, entry.name), countInFile, depth - 1);
1595
1669
  continue;
1596
1670
  }
1597
1671
  if (!CODE_FILE.test(entry.name)) continue;
1598
1672
  try {
1599
- found += readFileSync13(join10(dir, entry.name), "utf8").match(SUPPRESSION)?.length ?? 0;
1673
+ found += countInFile(readFileSync14(join11(dir, entry.name), "utf8"));
1600
1674
  } catch {
1601
1675
  }
1602
1676
  }
1603
1677
  return found;
1604
1678
  };
1679
+
1680
+ // src/counters/suppressions.ts
1681
+ var SUPPRESSION = /(?:eslint|oxlint|biome)-(?:disable|ignore)(?:-next-line|-line)?|@ts-expect-error|@ts-ignore/g;
1682
+ var countIn = (dir) => countInCodeFiles(dir, (text) => text.match(SUPPRESSION)?.length ?? 0);
1605
1683
  var suppressionCount = {
1606
1684
  id: "suppressionCount",
1607
1685
  probe: {
@@ -1615,8 +1693,8 @@ var suppressionCount = {
1615
1693
  };
1616
1694
 
1617
1695
  // src/counters/surface.ts
1618
- import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
1619
- import { join as join11, resolve as resolve16 } from "path";
1696
+ import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
1697
+ import { join as join12, resolve as resolve16 } from "path";
1620
1698
  var DECLARED = /^export\s+(?:declare\s+)?(?:abstract\s+)?(?:const|function|class|type|interface|enum|let|var)\s+([$\w]+)/;
1621
1699
  var BRACED = /^export\s+(?:type\s+)?\{([^}]*)\}(?:\s+from\s+(['"])(.+?)\2)?/;
1622
1700
  var nameOf2 = (spec) => {
@@ -1649,11 +1727,11 @@ var exportedSurfaceIn = (source) => {
1649
1727
  };
1650
1728
  var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1651
1729
  var declarationEntriesOf = (dir) => {
1652
- const at = join11(dir, "package.json");
1730
+ const at = join12(dir, "package.json");
1653
1731
  if (!existsSync12(at)) {
1654
1732
  throw new CounterError("floorSurface", `no package.json in ${dir} \u2014 that is not a package`);
1655
1733
  }
1656
- const manifest = JSON.parse(readFileSync14(at, "utf8"));
1734
+ const manifest = JSON.parse(readFileSync15(at, "utf8"));
1657
1735
  const found = [];
1658
1736
  const walk = (value) => {
1659
1737
  if (typeof value === "string" && value.endsWith(".d.ts")) found.push(value);
@@ -1701,16 +1779,16 @@ var floorSurface = {
1701
1779
  `${relative} promises ${entry} and it is not there \u2014 build before measuring, or the surface reads as empty`
1702
1780
  );
1703
1781
  }
1704
- for (const name of exportedSurfaceIn(readFileSync14(at, "utf8")).keys()) names.add(name);
1782
+ for (const name of exportedSurfaceIn(readFileSync15(at, "utf8")).keys()) names.add(name);
1705
1783
  }
1706
1784
  return names.size;
1707
1785
  }
1708
1786
  };
1709
1787
 
1710
1788
  // src/counters/tests.ts
1711
- import { existsSync as existsSync13, mkdtempSync as mkdtempSync2, readFileSync as readFileSync15, rmSync as rmSync4 } from "fs";
1712
- import { tmpdir as tmpdir2 } from "os";
1713
- import { join as join12, resolve as resolve17 } from "path";
1789
+ import { existsSync as existsSync13, mkdtempSync as mkdtempSync2, readFileSync as readFileSync16, rmSync as rmSync6 } from "fs";
1790
+ import { tmpdir as tmpdir3 } from "os";
1791
+ import { join as join13, resolve as resolve17 } from "path";
1714
1792
  var COUNTER = "testFailures";
1715
1793
  var VITEST_LINE = /^\s*Tests {2,}(.+?)\s*$/;
1716
1794
  var VITEST_TOTAL = /\(\d+\)$/;
@@ -1750,7 +1828,7 @@ var fromReport = (path) => {
1750
1828
  }
1751
1829
  let report;
1752
1830
  try {
1753
- report = JSON.parse(readFileSync15(path, "utf8"));
1831
+ report = JSON.parse(readFileSync16(path, "utf8"));
1754
1832
  } catch (error) {
1755
1833
  throw new CounterError(
1756
1834
  COUNTER,
@@ -1781,7 +1859,7 @@ var reportPathFor = (cwd, params, command) => {
1781
1859
  `report: "${VITEST_JSON}" needs somewhere to put the report \u2014 write ${PLACEHOLDER} into the command (--outputFile=${PLACEHOLDER}) or give the entry a "reportPath"`
1782
1860
  );
1783
1861
  }
1784
- return { own: true, path: join12(mkdtempSync2(join12(tmpdir2(), "geonosis-report-")), "report.json") };
1862
+ return { own: true, path: join13(mkdtempSync2(join13(tmpdir3(), "geonosis-report-")), "report.json") };
1785
1863
  };
1786
1864
  var testFailures = {
1787
1865
  id: COUNTER,
@@ -1830,16 +1908,14 @@ var testFailures = {
1830
1908
  run(command.replaceAll(PLACEHOLDER, path));
1831
1909
  return fromReport(path);
1832
1910
  } finally {
1833
- if (own) rmSync4(join12(path, ".."), { force: true, recursive: true });
1911
+ if (own) rmSync6(join13(path, ".."), { force: true, recursive: true });
1834
1912
  }
1835
1913
  }
1836
1914
  };
1837
1915
 
1838
1916
  // src/counters/todos.ts
1839
- import { existsSync as existsSync14, readdirSync as readdirSync6, readFileSync as readFileSync16 } from "fs";
1840
- import { join as join13, resolve as resolve18 } from "path";
1841
- var CODE_FILE2 = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts|vue|svelte|astro)$/;
1842
- var SKIP4 = /^(?:node_modules|dist|coverage|\.[^.]|__fixtures__)/;
1917
+ import { existsSync as existsSync14, readdirSync as readdirSync6 } from "fs";
1918
+ import { resolve as resolve18 } from "path";
1843
1919
  var PLAN_FILE = /^(\d{3,})-[a-z\d][a-z\d.-]*\.md$/;
1844
1920
  var plansIn = (dir) => {
1845
1921
  if (!existsSync14(dir)) return /* @__PURE__ */ new Set();
@@ -1850,34 +1926,14 @@ var plansIn = (dir) => {
1850
1926
  }
1851
1927
  return found;
1852
1928
  };
1853
- var countIn2 = (dir, marker, plans, depth = 12) => {
1854
- let entries;
1855
- try {
1856
- entries = readdirSync6(dir, { withFileTypes: true });
1857
- } catch {
1858
- return 0;
1859
- }
1929
+ var countIn2 = (dir, marker, plans) => countInCodeFiles(dir, (text) => {
1860
1930
  let found = 0;
1861
- for (const entry of entries) {
1862
- if (SKIP4.test(entry.name)) continue;
1863
- if (entry.isDirectory()) {
1864
- if (depth > 0) found += countIn2(join13(dir, entry.name), marker, plans, depth - 1);
1865
- continue;
1866
- }
1867
- if (!CODE_FILE2.test(entry.name)) continue;
1868
- let text = "";
1869
- try {
1870
- text = readFileSync16(join13(dir, entry.name), "utf8");
1871
- } catch {
1872
- continue;
1873
- }
1874
- for (const match of text.matchAll(marker)) {
1875
- const cited = match[1];
1876
- if (cited === void 0 || !plans.has(String(Number(cited)))) found += 1;
1877
- }
1931
+ for (const match of text.matchAll(marker)) {
1932
+ const cited = match[1];
1933
+ if (cited === void 0 || !plans.has(String(Number(cited)))) found += 1;
1878
1934
  }
1879
1935
  return found;
1880
- };
1936
+ });
1881
1937
  var orphanTodos = {
1882
1938
  id: "orphanTodos",
1883
1939
  probe: {
@@ -2128,6 +2184,11 @@ export {
2128
2184
  UnbalancedEnvelope,
2129
2185
  writeEnvelope,
2130
2186
  versionOf,
2187
+ HEAVY_SLOTS,
2188
+ heavySlotDir,
2189
+ describeHolders,
2190
+ HeavySlotTimeout,
2191
+ takeSlot,
2131
2192
  heavyLockPath,
2132
2193
  acquireExclusive,
2133
2194
  CONFIG_FILE,
package/dist/cli.js CHANGED
@@ -5,9 +5,10 @@ import {
5
5
  formatReport,
6
6
  runProve,
7
7
  runRatchet,
8
+ takeSlot,
8
9
  versionOf,
9
10
  writeEnvelope
10
- } from "./chunk-2EAYMSC5.js";
11
+ } from "./chunk-WENVCC67.js";
11
12
 
12
13
  // src/cli.ts
13
14
  import process from "process";
@@ -131,6 +132,10 @@ var measure = async () => {
131
132
  ` : formatProve(proof));
132
133
  return proof.proven ? 0 : 2;
133
134
  }
135
+ releaseHeavySlot = await takeSlot({
136
+ command: `geonosis-ratchet${tier === void 0 ? "" : ` --tier ${tier}`}`,
137
+ cwd
138
+ });
134
139
  const startedAt = Date.now();
135
140
  const result = await runRatchet({ counters: COUNTERS, cwd, tier });
136
141
  if (!json) process.stdout.write(formatReport(result));
@@ -152,6 +157,7 @@ var measure = async () => {
152
157
  }
153
158
  return result.measurements.some((one) => one.verdict === "grew") ? 1 : 0;
154
159
  };
160
+ var releaseHeavySlot = () => void 0;
155
161
  try {
156
162
  if (tierFlag !== -1 && (tier === void 0 || tier.startsWith("--"))) {
157
163
  throw new Error("--tier needs a tier name");
@@ -165,6 +171,7 @@ try {
165
171
  const release = exclusive ? await acquireExclusive({ timeoutSeconds }) : () => void 0;
166
172
  const giveBack = () => {
167
173
  release();
174
+ releaseHeavySlot();
168
175
  process.exit(130);
169
176
  };
170
177
  process.on("SIGINT", giveBack);
@@ -174,6 +181,7 @@ try {
174
181
  code = await measure();
175
182
  } finally {
176
183
  release();
184
+ releaseHeavySlot();
177
185
  }
178
186
  process.exit(code);
179
187
  } catch (error) {
package/dist/index.d.ts CHANGED
@@ -288,6 +288,42 @@ declare const versionOf: (moduleUrl: string) => string;
288
288
  declare const COUNTERS: Counter[];
289
289
  declare const counterById: (id: string) => Counter;
290
290
 
291
+ /** Who is spending a heavy slot, running what, since when. */
292
+ type HeavySlotHolder = {
293
+ command: string;
294
+ cwd: string;
295
+ pid: number;
296
+ startedAt: string;
297
+ };
298
+ /**
299
+ * Two, not one: `core/lock.ts`'s `acquireExclusive` serialises a whole run against every other; this
300
+ * is the machine's OWN ceiling — measured 2026-09-05, load 48 with during.day's Stop-hook tier, this
301
+ * kit's Stop-hook tier, this kit's ratchet and an engineer's suite all running at once. A machine that
302
+ * can carry two heavy things at a time carries two; a third waits.
303
+ */
304
+ declare const HEAVY_SLOTS = 2;
305
+ declare const heavySlotDir: () => string;
306
+ declare const describeHolders: (holders: HeavySlotHolder[]) => string;
307
+ /** A wait that ran out. The message names every holder, because the caller's own refusal is this text. */
308
+ declare class HeavySlotTimeout extends Error {
309
+ }
310
+ /**
311
+ * Claims one of the two machine-wide heavy slots, waiting for whoever holds them.
312
+ *
313
+ * A dead holder is reaped before a slot is tried, so a killed run never wedges the budget for
314
+ * whoever asks next. Both held is announced ONCE — not on every poll, which would turn a half-hour
315
+ * wait into a half hour of identical lines — and the wait is bounded, so a taker that can never get
316
+ * in refuses instead of hanging the process that called it.
317
+ */
318
+ declare const takeSlot: ({ command, cwd, dir, pollMs, say, waitMs, }: {
319
+ command: string;
320
+ cwd?: string;
321
+ dir?: string;
322
+ pollMs?: number;
323
+ say?: (line: string) => void;
324
+ waitMs?: number;
325
+ }) => Promise<() => void>;
326
+
291
327
  /** Who is running the heavy thing, since when, and where from. */
292
328
  type Holder = {
293
329
  cwd: string;
@@ -377,4 +413,4 @@ declare const formatProve: ({ proofs, proven }: ProveResult) => string;
377
413
  */
378
414
  declare const runCommand: (cwd: string, counterId: string, env?: NodeJS.ProcessEnv) => (command: string) => CommandResult;
379
415
 
380
- 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 };
416
+ export { CONFIG_FILE, COUNTERS, type CommandResult, type Counter, type CounterConfig, type CounterContext, CounterError, type CounterProbe, ENVELOPES_DIR, type EnvelopeEntry, HEAVY_SLOTS, type HeavySlotHolder, HeavySlotTimeout, type Holder, type Measurement, type Proof, type ProveResult, type RatchetConfig, type RatchetResult, type Refusal, type ReportEnvelope, UnbalancedEnvelope, type Verdict, acquireExclusive, counterById, describeHolders, envelopePath, formatProve, formatReport, heavyLockPath, heavySlotDir, keyOf, loadConfig, resolveBaseline, runCommand, runProve, runRatchet, takeSlot, versionOf, writeEnvelope };
package/dist/index.js CHANGED
@@ -3,40 +3,50 @@ import {
3
3
  COUNTERS,
4
4
  CounterError,
5
5
  ENVELOPES_DIR,
6
+ HEAVY_SLOTS,
7
+ HeavySlotTimeout,
6
8
  UnbalancedEnvelope,
7
9
  acquireExclusive,
8
10
  counterById,
11
+ describeHolders,
9
12
  envelopePath,
10
13
  formatProve,
11
14
  formatReport,
12
15
  heavyLockPath,
16
+ heavySlotDir,
13
17
  keyOf,
14
18
  loadConfig,
15
19
  resolveBaseline,
16
20
  runCommand,
17
21
  runProve,
18
22
  runRatchet,
23
+ takeSlot,
19
24
  versionOf,
20
25
  writeEnvelope
21
- } from "./chunk-2EAYMSC5.js";
26
+ } from "./chunk-WENVCC67.js";
22
27
  export {
23
28
  CONFIG_FILE,
24
29
  COUNTERS,
25
30
  CounterError,
26
31
  ENVELOPES_DIR,
32
+ HEAVY_SLOTS,
33
+ HeavySlotTimeout,
27
34
  UnbalancedEnvelope,
28
35
  acquireExclusive,
29
36
  counterById,
37
+ describeHolders,
30
38
  envelopePath,
31
39
  formatProve,
32
40
  formatReport,
33
41
  heavyLockPath,
42
+ heavySlotDir,
34
43
  keyOf,
35
44
  loadConfig,
36
45
  resolveBaseline,
37
46
  runCommand,
38
47
  runProve,
39
48
  runRatchet,
49
+ takeSlot,
40
50
  versionOf,
41
51
  writeEnvelope
42
52
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/ratchet",
3
- "version": "2.5.0",
3
+ "version": "2.5.1",
4
4
  "types": "./dist/index.d.ts",
5
5
  "description": "Debt as a number that may only shrink — one ratchet, pluggable counters.",
6
6
  "keywords": [