@geonosis/ratchet 2.5.3 → 2.6.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,53 @@
1
1
  # @geonosis/ratchet
2
2
 
3
+ ## 2.6.1
4
+
5
+ ### Patch Changes
6
+
7
+ - e1db50f: `geonosis-ratchet` reads `geonosis.ratchet.json` BEFORE it queues for a heavy slot. The slot
8
+ directory is machine-wide — it bounds heavy work on the box, not per repo — so a second repo's
9
+ `geonosis-verify` holds it legitimately, and a run with nothing to count was spending the resource
10
+ that bounds heavy work in order to discover it had no heavy work, waiting `GEONOSIS_HEAVY_WAIT_MS`
11
+ (20 minutes by default) to then exit 2. Measured 2026-09-07: with during.day's verify holding the
12
+ only slot on a memory-clamped one-slot box, this kit's own
13
+ `registry.test.ts > passes the child bin exit code through verbatim` — which spawns a ratchet into a
14
+ config-less directory expecting an instant refusal — sat for 979,800 ms against a 60,000 ms timeout,
15
+ three runs running. It now refuses in about a second with every slot held, and a bin case holds it
16
+ there: a config-less run must name `geonosis.ratchet.json` and must never say it is waiting.
17
+ - db89b0b: Two Stop-hook defects, both caught by making the instrument go red once. A heavy slot is inherited down a process tree: the holder marks its environment with the slot's path, and a descendant whose mark names a living ancestor's slot takes none of its own — a ratchet inside a verify waited on the slot its parent held, for ever, on a one-slot runner (CI's), and now waits on nobody; a tool that takes twice in one process still gets two. The Stop hook judges a run over a dirty tree: a tree's identity is HEAD plus every change on it, the hook's own files aside; a run launched over that identity is read at the next turn end and not re-run while the tree stands, where before an uncommitted edit meant every turn end launched the tier again and none was judged. The kit's own fast tier now builds first, so the hook never judges bins whose `dist` is older than the `src` under test.
18
+
19
+ ## 2.6.0
20
+
21
+ ### Minor Changes
22
+
23
+ - 58565d7: Plan 029, the factory floor: the gate learns to run things at once, and the heavy budget stops
24
+ being one laptop's number.
25
+
26
+ `@geonosis/verify` — `TierStep` carries `group`, the number that says which steps run at once, so a
27
+ tier can declare `{ "parallel": [ … ] }` groups and `{ "command": "… {workspace} …", "per":
28
+ "workspace" }` steps. New: `affected.ts` (`affected`, `affectedOf`, `workspacesIn`, `changedSince`,
29
+ `mergeBase`, `ROOT_SHAPED`, types `Affected` and `Workspace`), `PerWorkspace`, `ParallelStep`,
30
+ `CommandStep` on `config`, and `planSteps`, `runGroups`, `shellRunAsync`, `PlannedStep`,
31
+ `AsyncStepRun` on `run`. `GateReport` gains an optional `affected` block. `runSteps`, `shellRun` and
32
+ `stepIds` are unchanged. The bin gains `--affected`, `--since <ref>` and `--jobs <n>`.
33
+
34
+ `@geonosis/ratchet` and `@geonosis/release` — `heavySlots()` and `DEFAULT_HEAVY_SLOTS` beside
35
+ `HEAVY_SLOTS`, which is no longer the constant 2: the budget is `GEONOSIS_HEAVY_SLOTS` or
36
+ `availableParallelism() - 1`, read on every claim rather than at module load (D-073).
37
+
38
+ `@geonosis/ledger` — `shape.ts`: `shapeCheck`, `readManifest`, `exportsOf`, `formatShape` and the
39
+ types `BriefManifest`, `ShapeRefusal`, `ShapeOutcome` — the lock that refuses an engineer diff
40
+ touching a locked test, renaming an export or adding a file the brief did not name (D-072). Not yet
41
+ on the package's public entry: the CLI door lands with W115's dispatch unit.
42
+
43
+ `geonosis` — the plugin's post-edit hook takes its formatter from `geonosis.json → stack.format`
44
+ (D-070) and the stop hook reads the machine's heavy budget.
45
+ - 58565d7: The heavy budget is memory-aware (D-081, amending D-073). `heavyBudget()` — on `@geonosis/ratchet`'s door, copied into `@geonosis/release` and the plugin's Stop hook and held to one answer by a test — is the smaller of one less than the cores and the memory the machine can spend divided by what one run holds: `geonosis.json → stack.memoryMbPerSlot`, 512 MB when nothing is declared. `GEONOSIS_HEAVY_SLOTS` still wins outright; `GEONOSIS_HEAVY_MEMORY_MB` is the memory to spend instead of the machine's. `heavySlots()` is the budget's number read fresh, and `describeBudget()` names the number and both inputs. `geonosis-verify --jobs` defaults to it, its report carries it as `heavy`, and the run prints it; the kit's own vitest workers read it too. Measured on a 4-core, 3.4 GB box: three slots over 1.4 GB swapped; two do not.
46
+
47
+ ### Patch Changes
48
+
49
+ - 58565d7: The oxlint counters read the graphical format's Unicode markers — `×` for an error, `⚠` for a warning — beside the ASCII `x` and `!` a pipe gets. CI's terminal printed the Unicode pair, `oxlintRule` saw two findings it could attribute to no rule, and refused as its contract says; a laptop's vitest child never printed them.
50
+
3
51
  ## 2.5.3
4
52
 
5
53
  ## 2.5.2
@@ -1,6 +1,51 @@
1
+ // src/core/config.ts
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { resolve } from "path";
4
+ var CONFIG_FILE = "geonosis.ratchet.json";
5
+ var keyOf = (entry) => entry.key ?? entry.counter;
6
+ var loadConfig = (cwd) => {
7
+ const path = resolve(cwd, CONFIG_FILE);
8
+ if (!existsSync(path)) {
9
+ throw new Error(`no ${CONFIG_FILE} in ${cwd} \u2014 the ratchet has nothing to count`);
10
+ }
11
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
12
+ if (!Array.isArray(parsed.counters)) {
13
+ throw new Error(`${CONFIG_FILE} has no "counters" array`);
14
+ }
15
+ for (const entry of parsed.counters) {
16
+ if (typeof entry.counter !== "string") {
17
+ throw new Error(`${CONFIG_FILE}: every counter entry needs a "counter" id`);
18
+ }
19
+ if (entry.tiers !== void 0) {
20
+ const named = Array.isArray(entry.tiers) && entry.tiers.length > 0 && entry.tiers.every((tier) => typeof tier === "string" && tier.length > 0);
21
+ if (!named) {
22
+ throw new Error(
23
+ `${CONFIG_FILE}: "${keyOf(entry)}" has "tiers" that is not a non-empty list of tier names`
24
+ );
25
+ }
26
+ }
27
+ }
28
+ const keys = parsed.counters.map(keyOf);
29
+ const duplicate = keys.find((key, at) => keys.indexOf(key) !== at);
30
+ if (duplicate !== void 0) {
31
+ throw new Error(
32
+ `${CONFIG_FILE}: two counters both write "${duplicate}" \u2014 give one of them a distinct "key"`
33
+ );
34
+ }
35
+ return { baseline: parsed.baseline ?? "gate-baseline.json", counters: parsed.counters };
36
+ };
37
+ var resolveBaseline = (cwd) => {
38
+ try {
39
+ const parsed = JSON.parse(readFileSync(resolve(cwd, CONFIG_FILE), "utf8"));
40
+ return typeof parsed.baseline === "string" ? parsed.baseline : "gate-baseline.json";
41
+ } catch {
42
+ return "gate-baseline.json";
43
+ }
44
+ };
45
+
1
46
  // src/core/envelope.ts
2
- import { mkdirSync, readFileSync, writeFileSync } from "fs";
3
- import { dirname, join, resolve } from "path";
47
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
48
+ import { dirname, join, resolve as resolve2 } from "path";
4
49
  import { fileURLToPath } from "url";
5
50
  var ENVELOPES_DIR = ".geonosis/envelopes";
6
51
  var envelopePath = (root, tool) => join(root, ENVELOPES_DIR, `${tool}.json`);
@@ -15,7 +60,7 @@ var unbalancedMessage = (envelope, next) => `${envelope.tool}: considered ${enve
15
60
  var FORBIDDEN_ROOT = "GEONOSIS_ENVELOPES_FORBIDDEN_ROOT";
16
61
  var refuseForbiddenRoot = (root) => {
17
62
  const forbidden = process.env[FORBIDDEN_ROOT];
18
- if (forbidden === void 0 || resolve(forbidden) !== resolve(root)) return;
63
+ if (forbidden === void 0 || resolve2(forbidden) !== resolve2(root)) return;
19
64
  throw new UnbalancedEnvelope(
20
65
  `${root} is off limits to envelope writers in this process (${FORBIDDEN_ROOT}) \u2014 a run that writes one into a shared root races every other run reading it, and leaves a file the next one takes for real. Point this at a scratch root of its own: tooling/scratch-dir.ts.`
21
66
  );
@@ -53,7 +98,7 @@ var writeEnvelope = ({
53
98
  var UNKNOWN = "unknown";
54
99
  var versionIn = (dir) => {
55
100
  try {
56
- const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
101
+ const manifest = JSON.parse(readFileSync2(join(dir, "package.json"), "utf8"));
57
102
  return typeof manifest.version === "string" ? manifest.version : void 0;
58
103
  } catch {
59
104
  return void 0;
@@ -71,8 +116,8 @@ var versionOf = (moduleUrl) => {
71
116
  };
72
117
 
73
118
  // src/core/heavy-slot.ts
74
- import { readFileSync as readFileSync2, rmSync as rmSync2 } from "fs";
75
- import { tmpdir } from "os";
119
+ import { readFileSync as readFileSync3, rmSync as rmSync2 } from "fs";
120
+ import { availableParallelism, freemem, tmpdir } from "os";
76
121
  import { join as join2 } from "path";
77
122
 
78
123
  // src/core/file-claim.ts
@@ -104,7 +149,42 @@ var claimFile = (path, holder) => {
104
149
  };
105
150
 
106
151
  // src/core/heavy-slot.ts
107
- var HEAVY_SLOTS = 2;
152
+ var availableMemoryMb = () => {
153
+ try {
154
+ const found = /^MemAvailable:\s+(\d+) kB/m.exec(readFileSync3("/proc/meminfo", "utf8"));
155
+ if (found?.[1] !== void 0) return Math.floor(Number(found[1]) / 1024);
156
+ } catch {
157
+ }
158
+ return Math.floor(freemem() / 1048576);
159
+ };
160
+ var DEFAULT_MEMORY_MB_PER_SLOT = 512;
161
+ var DEFAULT_HEAVY_SLOTS = Math.max(1, availableParallelism() - 1);
162
+ var perSlotMbDeclaredIn = (root) => {
163
+ try {
164
+ const parsed = JSON.parse(readFileSync3(join2(root, "geonosis.json"), "utf8"));
165
+ const declared = parsed.stack?.memoryMbPerSlot;
166
+ return typeof declared === "number" && Number.isFinite(declared) && declared > 0 ? declared : void 0;
167
+ } catch {
168
+ return void 0;
169
+ }
170
+ };
171
+ var heavyBudget = (input = {}) => {
172
+ const declared = Number(process.env.GEONOSIS_HEAVY_SLOTS ?? "");
173
+ const spendable = Number(process.env.GEONOSIS_HEAVY_MEMORY_MB ?? "");
174
+ const reading = {
175
+ availableMemoryMb: input.availableMemoryMb ?? (Number.isFinite(spendable) && spendable > 0 ? spendable : availableMemoryMb()),
176
+ cores: input.cores ?? availableParallelism(),
177
+ perSlotMb: input.perSlotMb ?? perSlotMbDeclaredIn(input.root ?? process.cwd()) ?? DEFAULT_MEMORY_MB_PER_SLOT
178
+ };
179
+ if (Number.isInteger(declared) && declared > 0)
180
+ return { ...reading, by: "declared", slots: declared };
181
+ const byCores = Math.max(1, reading.cores - 1);
182
+ const byMemory = Math.max(1, Math.floor(reading.availableMemoryMb / reading.perSlotMb));
183
+ return byMemory < byCores ? { ...reading, by: "memory", slots: byMemory } : { ...reading, by: "cores", slots: byCores };
184
+ };
185
+ var describeBudget = (budget) => `${budget.slots} heavy slot(s), bound by ${budget.by === "declared" ? "GEONOSIS_HEAVY_SLOTS" : budget.by} \u2014 ${budget.cores} cores, ${budget.availableMemoryMb} MB available, ${budget.perSlotMb} MB per slot`;
186
+ var heavySlots = () => heavyBudget().slots;
187
+ var HEAVY_SLOTS = heavySlots();
108
188
  var DEFAULT_WAIT_MS = 20 * 6e4;
109
189
  var heavySlotDir = () => {
110
190
  const named = process.env.GEONOSIS_HEAVY_SLOT_DIR;
@@ -114,7 +194,7 @@ var heavySlotDir = () => {
114
194
  var slotPath = (dir, at) => join2(dir, `slot-${at}.json`);
115
195
  var holderOf = (path) => {
116
196
  try {
117
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
197
+ const parsed = JSON.parse(readFileSync3(path, "utf8"));
118
198
  return typeof parsed.pid === "number" ? {
119
199
  command: parsed.command ?? "unknown",
120
200
  cwd: parsed.cwd ?? "somewhere",
@@ -130,12 +210,19 @@ var reapIfDead = (path) => {
130
210
  if (holder !== void 0 && !alive(holder.pid)) rmSync2(path, { force: true });
131
211
  };
132
212
  var claim = (path, holder) => claimFile(path, holder);
133
- var namedHolders = (dir) => Array.from({ length: HEAVY_SLOTS }, (_unused, at) => holderOf(slotPath(dir, at))).filter(
213
+ var namedHolders = (dir) => Array.from({ length: heavySlots() }, (_unused, at) => holderOf(slotPath(dir, at))).filter(
134
214
  (holder) => holder !== void 0
135
215
  );
136
216
  var describeHolders = (holders) => holders.map((one) => `${one.pid} ${one.command} since ${one.startedAt}`).join(", ");
137
217
  var HeavySlotTimeout = class extends Error {
138
218
  };
219
+ var HEAVY_SLOT_HELD = "GEONOSIS_HEAVY_SLOT_HELD";
220
+ var inherited = () => {
221
+ const named = process.env[HEAVY_SLOT_HELD];
222
+ if (named === void 0 || named === "") return false;
223
+ const holder = holderOf(named);
224
+ return holder !== void 0 && holder.pid !== process.pid && alive(holder.pid);
225
+ };
139
226
  var takeSlot = async ({
140
227
  command,
141
228
  cwd = process.cwd(),
@@ -145,10 +232,11 @@ var takeSlot = async ({
145
232
  `),
146
233
  waitMs = Number(process.env.GEONOSIS_HEAVY_WAIT_MS ?? "") || DEFAULT_WAIT_MS
147
234
  }) => {
235
+ if (inherited()) return () => void 0;
148
236
  const until = Date.now() + waitMs;
149
237
  let told = false;
150
238
  for (; ; ) {
151
- for (let at = 0; at < HEAVY_SLOTS; at += 1) {
239
+ for (let at = 0; at < heavySlots(); at += 1) {
152
240
  const path = slotPath(dir, at);
153
241
  reapIfDead(path);
154
242
  const mine = {
@@ -158,8 +246,10 @@ var takeSlot = async ({
158
246
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
159
247
  };
160
248
  if (claim(path, mine)) {
249
+ process.env[HEAVY_SLOT_HELD] = path;
161
250
  return () => {
162
251
  if (holderOf(path)?.pid === process.pid) rmSync2(path, { force: true });
252
+ if (process.env[HEAVY_SLOT_HELD] === path) delete process.env[HEAVY_SLOT_HELD];
163
253
  };
164
254
  }
165
255
  }
@@ -178,7 +268,7 @@ var takeSlot = async ({
178
268
  };
179
269
 
180
270
  // src/core/lock.ts
181
- import { readFileSync as readFileSync3, rmSync as rmSync3 } from "fs";
271
+ import { readFileSync as readFileSync4, rmSync as rmSync3 } from "fs";
182
272
  import { homedir } from "os";
183
273
  import { isAbsolute, join as join3 } from "path";
184
274
  var heavyLockPath = () => {
@@ -190,7 +280,7 @@ var heavyLockPath = () => {
190
280
  };
191
281
  var holderOf2 = (path) => {
192
282
  try {
193
- const parsed = JSON.parse(readFileSync3(path, "utf8"));
283
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
194
284
  return typeof parsed.pid === "number" ? {
195
285
  cwd: parsed.cwd ?? "somewhere",
196
286
  pid: parsed.pid,
@@ -245,51 +335,6 @@ var acquireExclusive = async ({
245
335
  }
246
336
  };
247
337
 
248
- // src/core/config.ts
249
- import { existsSync, readFileSync as readFileSync4 } from "fs";
250
- import { resolve as resolve2 } from "path";
251
- var CONFIG_FILE = "geonosis.ratchet.json";
252
- var keyOf = (entry) => entry.key ?? entry.counter;
253
- var loadConfig = (cwd) => {
254
- const path = resolve2(cwd, CONFIG_FILE);
255
- if (!existsSync(path)) {
256
- throw new Error(`no ${CONFIG_FILE} in ${cwd} \u2014 the ratchet has nothing to count`);
257
- }
258
- const parsed = JSON.parse(readFileSync4(path, "utf8"));
259
- if (!Array.isArray(parsed.counters)) {
260
- throw new Error(`${CONFIG_FILE} has no "counters" array`);
261
- }
262
- for (const entry of parsed.counters) {
263
- if (typeof entry.counter !== "string") {
264
- throw new Error(`${CONFIG_FILE}: every counter entry needs a "counter" id`);
265
- }
266
- if (entry.tiers !== void 0) {
267
- const named = Array.isArray(entry.tiers) && entry.tiers.length > 0 && entry.tiers.every((tier) => typeof tier === "string" && tier.length > 0);
268
- if (!named) {
269
- throw new Error(
270
- `${CONFIG_FILE}: "${keyOf(entry)}" has "tiers" that is not a non-empty list of tier names`
271
- );
272
- }
273
- }
274
- }
275
- const keys = parsed.counters.map(keyOf);
276
- const duplicate = keys.find((key, at) => keys.indexOf(key) !== at);
277
- if (duplicate !== void 0) {
278
- throw new Error(
279
- `${CONFIG_FILE}: two counters both write "${duplicate}" \u2014 give one of them a distinct "key"`
280
- );
281
- }
282
- return { baseline: parsed.baseline ?? "gate-baseline.json", counters: parsed.counters };
283
- };
284
- var resolveBaseline = (cwd) => {
285
- try {
286
- const parsed = JSON.parse(readFileSync4(resolve2(cwd, CONFIG_FILE), "utf8"));
287
- return typeof parsed.baseline === "string" ? parsed.baseline : "gate-baseline.json";
288
- } catch {
289
- return "gate-baseline.json";
290
- }
291
- };
292
-
293
338
  // src/core/types.ts
294
339
  var CounterError = class extends Error {
295
340
  constructor(counter, message) {
@@ -1168,13 +1213,15 @@ var UNIX_FINDING = /^\S[^\n]*:\d+:\d+: .*\[(Error|Warning)\/[^\]\n]+\]$/gm;
1168
1213
  var UNIX_SUMMARY = /^(\d+) problems?$/m;
1169
1214
  var AGENT_FINDING = /^\S[^\n]*:\d+:\d+: (error|warning) /gm;
1170
1215
  var DEFAULT_SUMMARY = /^Found (\d+) warnings? and (\d+) errors?\.$/m;
1171
- var DEFAULT_FINDING = /^\s*([x!]) [^\s(]+\([^)\n]+\): /m;
1216
+ var DEFAULT_FINDING = /^\s*([x!×⚠]) [^\s(]+\([^)\n]+\): /m;
1172
1217
  var FINDING_SHAPES = [UNIX_FINDING, AGENT_FINDING, DEFAULT_FINDING].map(
1173
1218
  (shape) => new RegExp(shape.source)
1174
1219
  );
1175
1220
  var RULE_TOKEN = /\([^()\n]+\)/;
1176
1221
  var SEVERITY_BY_TOKEN = {
1177
1222
  "!": "warning",
1223
+ "\xD7": "error",
1224
+ "\u26A0": "warning",
1178
1225
  Error: "error",
1179
1226
  error: "error",
1180
1227
  Warning: "warning",
@@ -2179,11 +2226,20 @@ var formatProve = ({ proofs, proven }) => {
2179
2226
  };
2180
2227
 
2181
2228
  export {
2229
+ CONFIG_FILE,
2230
+ keyOf,
2231
+ loadConfig,
2232
+ resolveBaseline,
2182
2233
  ENVELOPES_DIR,
2183
2234
  envelopePath,
2184
2235
  UnbalancedEnvelope,
2185
2236
  writeEnvelope,
2186
2237
  versionOf,
2238
+ availableMemoryMb,
2239
+ DEFAULT_MEMORY_MB_PER_SLOT,
2240
+ heavyBudget,
2241
+ describeBudget,
2242
+ heavySlots,
2187
2243
  HEAVY_SLOTS,
2188
2244
  heavySlotDir,
2189
2245
  describeHolders,
@@ -2191,10 +2247,6 @@ export {
2191
2247
  takeSlot,
2192
2248
  heavyLockPath,
2193
2249
  acquireExclusive,
2194
- CONFIG_FILE,
2195
- keyOf,
2196
- loadConfig,
2197
- resolveBaseline,
2198
2250
  CounterError,
2199
2251
  runCommand,
2200
2252
  runProve,
package/dist/cli.js CHANGED
@@ -3,12 +3,13 @@ import {
3
3
  acquireExclusive,
4
4
  formatProve,
5
5
  formatReport,
6
+ loadConfig,
6
7
  runProve,
7
8
  runRatchet,
8
9
  takeSlot,
9
10
  versionOf,
10
11
  writeEnvelope
11
- } from "./chunk-WENVCC67.js";
12
+ } from "./chunk-GJD2676B.js";
12
13
 
13
14
  // src/cli.ts
14
15
  import process from "process";
@@ -132,6 +133,7 @@ var measure = async () => {
132
133
  ` : formatProve(proof));
133
134
  return proof.proven ? 0 : 2;
134
135
  }
136
+ loadConfig(cwd);
135
137
  releaseHeavySlot = await takeSlot({
136
138
  command: `geonosis-ratchet${tier === void 0 ? "" : ` --tier ${tier}`}`,
137
139
  cwd
package/dist/index.d.ts CHANGED
@@ -288,33 +288,44 @@ 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
291
  type HeavySlotHolder = {
293
292
  command: string;
294
293
  cwd: string;
295
294
  pid: number;
296
295
  startedAt: string;
297
296
  };
297
+ /** The memory the machine can spend now: `MemAvailable` where the kernel says it, `os.freemem()` where it does not. */
298
+ declare const availableMemoryMb: () => number;
299
+ /** What one heavy run is budgeted to hold when the repo declares nothing: a vitest worker measured 334 MB and a tsc 224 MB (plan 030). */
300
+ declare const DEFAULT_MEMORY_MB_PER_SLOT = 512;
301
+ type HeavyBudget = {
302
+ availableMemoryMb: number;
303
+ /** What bound the number: a declared `GEONOSIS_HEAVY_SLOTS`, the cores, or the memory. */
304
+ by: 'cores' | 'declared' | 'memory';
305
+ cores: number;
306
+ perSlotMb: number;
307
+ slots: number;
308
+ };
298
309
  /**
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.
310
+ * The heavy budget, read fresh: the smaller of one less than the cores and the memory divided by
311
+ * what one run holds (D-081, amending D-073). A declared `GEONOSIS_HEAVY_SLOTS` wins over both;
312
+ * `GEONOSIS_HEAVY_MEMORY_MB` is the memory to spend instead of the machine's; the per-slot number
313
+ * is `geonosis.json stack.memoryMbPerSlot`, 512 when nothing is declared.
303
314
  */
304
- declare const HEAVY_SLOTS = 2;
315
+ declare const heavyBudget: (input?: {
316
+ availableMemoryMb?: number;
317
+ cores?: number;
318
+ perSlotMb?: number;
319
+ root?: string;
320
+ }) => HeavyBudget;
321
+ /** One line a report can carry: the number, and both inputs it was read from. */
322
+ declare const describeBudget: (budget: HeavyBudget) => string;
323
+ declare const heavySlots: () => number;
324
+ declare const HEAVY_SLOTS: number;
305
325
  declare const heavySlotDir: () => string;
306
326
  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
327
  declare class HeavySlotTimeout extends Error {
309
328
  }
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
329
  declare const takeSlot: ({ command, cwd, dir, pollMs, say, waitMs, }: {
319
330
  command: string;
320
331
  cwd?: string;
@@ -413,4 +424,4 @@ declare const formatProve: ({ proofs, proven }: ProveResult) => string;
413
424
  */
414
425
  declare const runCommand: (cwd: string, counterId: string, env?: NodeJS.ProcessEnv) => (command: string) => CommandResult;
415
426
 
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 };
427
+ export { CONFIG_FILE, COUNTERS, type CommandResult, type Counter, type CounterConfig, type CounterContext, CounterError, type CounterProbe, DEFAULT_MEMORY_MB_PER_SLOT, ENVELOPES_DIR, type EnvelopeEntry, HEAVY_SLOTS, type HeavyBudget, type HeavySlotHolder, HeavySlotTimeout, type Holder, type Measurement, type Proof, type ProveResult, type RatchetConfig, type RatchetResult, type Refusal, type ReportEnvelope, UnbalancedEnvelope, type Verdict, acquireExclusive, availableMemoryMb, counterById, describeBudget, describeHolders, envelopePath, formatProve, formatReport, heavyBudget, heavyLockPath, heavySlotDir, heavySlots, keyOf, loadConfig, resolveBaseline, runCommand, runProve, runRatchet, takeSlot, versionOf, writeEnvelope };
package/dist/index.js CHANGED
@@ -2,18 +2,23 @@ import {
2
2
  CONFIG_FILE,
3
3
  COUNTERS,
4
4
  CounterError,
5
+ DEFAULT_MEMORY_MB_PER_SLOT,
5
6
  ENVELOPES_DIR,
6
7
  HEAVY_SLOTS,
7
8
  HeavySlotTimeout,
8
9
  UnbalancedEnvelope,
9
10
  acquireExclusive,
11
+ availableMemoryMb,
10
12
  counterById,
13
+ describeBudget,
11
14
  describeHolders,
12
15
  envelopePath,
13
16
  formatProve,
14
17
  formatReport,
18
+ heavyBudget,
15
19
  heavyLockPath,
16
20
  heavySlotDir,
21
+ heavySlots,
17
22
  keyOf,
18
23
  loadConfig,
19
24
  resolveBaseline,
@@ -23,23 +28,28 @@ import {
23
28
  takeSlot,
24
29
  versionOf,
25
30
  writeEnvelope
26
- } from "./chunk-WENVCC67.js";
31
+ } from "./chunk-GJD2676B.js";
27
32
  export {
28
33
  CONFIG_FILE,
29
34
  COUNTERS,
30
35
  CounterError,
36
+ DEFAULT_MEMORY_MB_PER_SLOT,
31
37
  ENVELOPES_DIR,
32
38
  HEAVY_SLOTS,
33
39
  HeavySlotTimeout,
34
40
  UnbalancedEnvelope,
35
41
  acquireExclusive,
42
+ availableMemoryMb,
36
43
  counterById,
44
+ describeBudget,
37
45
  describeHolders,
38
46
  envelopePath,
39
47
  formatProve,
40
48
  formatReport,
49
+ heavyBudget,
41
50
  heavyLockPath,
42
51
  heavySlotDir,
52
+ heavySlots,
43
53
  keyOf,
44
54
  loadConfig,
45
55
  resolveBaseline,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/ratchet",
3
- "version": "2.5.3",
3
+ "version": "2.6.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": [
@@ -43,6 +43,10 @@
43
43
  },
44
44
  "scripts": {
45
45
  "build": "tsup",
46
- "typecheck": "tsc --noEmit"
46
+ "typecheck": "tsc --noEmit",
47
+ "lint": "oxlint --config ../../.oxlintrc.json .",
48
+ "test:unit": "vitest run --project unit --root ../.. --passWithNoTests \"$PWD\"",
49
+ "test:bin": "vitest run --project bin --root ../.. --passWithNoTests \"$PWD\"",
50
+ "test:exam": "vitest run --project exam --root ../.. --passWithNoTests \"$PWD\""
47
51
  }
48
52
  }