@geonosis/ratchet 2.6.0 → 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,21 @@
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
+
3
19
  ## 2.6.0
4
20
 
5
21
  ### Minor Changes
@@ -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,7 +116,7 @@ var versionOf = (moduleUrl) => {
71
116
  };
72
117
 
73
118
  // src/core/heavy-slot.ts
74
- import { readFileSync as readFileSync2, rmSync as rmSync2 } from "fs";
119
+ import { readFileSync as readFileSync3, rmSync as rmSync2 } from "fs";
75
120
  import { availableParallelism, freemem, tmpdir } from "os";
76
121
  import { join as join2 } from "path";
77
122
 
@@ -106,7 +151,7 @@ var claimFile = (path, holder) => {
106
151
  // src/core/heavy-slot.ts
107
152
  var availableMemoryMb = () => {
108
153
  try {
109
- const found = /^MemAvailable:\s+(\d+) kB/m.exec(readFileSync2("/proc/meminfo", "utf8"));
154
+ const found = /^MemAvailable:\s+(\d+) kB/m.exec(readFileSync3("/proc/meminfo", "utf8"));
110
155
  if (found?.[1] !== void 0) return Math.floor(Number(found[1]) / 1024);
111
156
  } catch {
112
157
  }
@@ -116,7 +161,7 @@ var DEFAULT_MEMORY_MB_PER_SLOT = 512;
116
161
  var DEFAULT_HEAVY_SLOTS = Math.max(1, availableParallelism() - 1);
117
162
  var perSlotMbDeclaredIn = (root) => {
118
163
  try {
119
- const parsed = JSON.parse(readFileSync2(join2(root, "geonosis.json"), "utf8"));
164
+ const parsed = JSON.parse(readFileSync3(join2(root, "geonosis.json"), "utf8"));
120
165
  const declared = parsed.stack?.memoryMbPerSlot;
121
166
  return typeof declared === "number" && Number.isFinite(declared) && declared > 0 ? declared : void 0;
122
167
  } catch {
@@ -149,7 +194,7 @@ var heavySlotDir = () => {
149
194
  var slotPath = (dir, at) => join2(dir, `slot-${at}.json`);
150
195
  var holderOf = (path) => {
151
196
  try {
152
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
197
+ const parsed = JSON.parse(readFileSync3(path, "utf8"));
153
198
  return typeof parsed.pid === "number" ? {
154
199
  command: parsed.command ?? "unknown",
155
200
  cwd: parsed.cwd ?? "somewhere",
@@ -171,6 +216,13 @@ var namedHolders = (dir) => Array.from({ length: heavySlots() }, (_unused, at) =
171
216
  var describeHolders = (holders) => holders.map((one) => `${one.pid} ${one.command} since ${one.startedAt}`).join(", ");
172
217
  var HeavySlotTimeout = class extends Error {
173
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
+ };
174
226
  var takeSlot = async ({
175
227
  command,
176
228
  cwd = process.cwd(),
@@ -180,6 +232,7 @@ var takeSlot = async ({
180
232
  `),
181
233
  waitMs = Number(process.env.GEONOSIS_HEAVY_WAIT_MS ?? "") || DEFAULT_WAIT_MS
182
234
  }) => {
235
+ if (inherited()) return () => void 0;
183
236
  const until = Date.now() + waitMs;
184
237
  let told = false;
185
238
  for (; ; ) {
@@ -193,8 +246,10 @@ var takeSlot = async ({
193
246
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
194
247
  };
195
248
  if (claim(path, mine)) {
249
+ process.env[HEAVY_SLOT_HELD] = path;
196
250
  return () => {
197
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];
198
253
  };
199
254
  }
200
255
  }
@@ -213,7 +268,7 @@ var takeSlot = async ({
213
268
  };
214
269
 
215
270
  // src/core/lock.ts
216
- import { readFileSync as readFileSync3, rmSync as rmSync3 } from "fs";
271
+ import { readFileSync as readFileSync4, rmSync as rmSync3 } from "fs";
217
272
  import { homedir } from "os";
218
273
  import { isAbsolute, join as join3 } from "path";
219
274
  var heavyLockPath = () => {
@@ -225,7 +280,7 @@ var heavyLockPath = () => {
225
280
  };
226
281
  var holderOf2 = (path) => {
227
282
  try {
228
- const parsed = JSON.parse(readFileSync3(path, "utf8"));
283
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
229
284
  return typeof parsed.pid === "number" ? {
230
285
  cwd: parsed.cwd ?? "somewhere",
231
286
  pid: parsed.pid,
@@ -280,51 +335,6 @@ var acquireExclusive = async ({
280
335
  }
281
336
  };
282
337
 
283
- // src/core/config.ts
284
- import { existsSync, readFileSync as readFileSync4 } from "fs";
285
- import { resolve as resolve2 } from "path";
286
- var CONFIG_FILE = "geonosis.ratchet.json";
287
- var keyOf = (entry) => entry.key ?? entry.counter;
288
- var loadConfig = (cwd) => {
289
- const path = resolve2(cwd, CONFIG_FILE);
290
- if (!existsSync(path)) {
291
- throw new Error(`no ${CONFIG_FILE} in ${cwd} \u2014 the ratchet has nothing to count`);
292
- }
293
- const parsed = JSON.parse(readFileSync4(path, "utf8"));
294
- if (!Array.isArray(parsed.counters)) {
295
- throw new Error(`${CONFIG_FILE} has no "counters" array`);
296
- }
297
- for (const entry of parsed.counters) {
298
- if (typeof entry.counter !== "string") {
299
- throw new Error(`${CONFIG_FILE}: every counter entry needs a "counter" id`);
300
- }
301
- if (entry.tiers !== void 0) {
302
- const named = Array.isArray(entry.tiers) && entry.tiers.length > 0 && entry.tiers.every((tier) => typeof tier === "string" && tier.length > 0);
303
- if (!named) {
304
- throw new Error(
305
- `${CONFIG_FILE}: "${keyOf(entry)}" has "tiers" that is not a non-empty list of tier names`
306
- );
307
- }
308
- }
309
- }
310
- const keys = parsed.counters.map(keyOf);
311
- const duplicate = keys.find((key, at) => keys.indexOf(key) !== at);
312
- if (duplicate !== void 0) {
313
- throw new Error(
314
- `${CONFIG_FILE}: two counters both write "${duplicate}" \u2014 give one of them a distinct "key"`
315
- );
316
- }
317
- return { baseline: parsed.baseline ?? "gate-baseline.json", counters: parsed.counters };
318
- };
319
- var resolveBaseline = (cwd) => {
320
- try {
321
- const parsed = JSON.parse(readFileSync4(resolve2(cwd, CONFIG_FILE), "utf8"));
322
- return typeof parsed.baseline === "string" ? parsed.baseline : "gate-baseline.json";
323
- } catch {
324
- return "gate-baseline.json";
325
- }
326
- };
327
-
328
338
  // src/core/types.ts
329
339
  var CounterError = class extends Error {
330
340
  constructor(counter, message) {
@@ -2216,6 +2226,10 @@ var formatProve = ({ proofs, proven }) => {
2216
2226
  };
2217
2227
 
2218
2228
  export {
2229
+ CONFIG_FILE,
2230
+ keyOf,
2231
+ loadConfig,
2232
+ resolveBaseline,
2219
2233
  ENVELOPES_DIR,
2220
2234
  envelopePath,
2221
2235
  UnbalancedEnvelope,
@@ -2233,10 +2247,6 @@ export {
2233
2247
  takeSlot,
2234
2248
  heavyLockPath,
2235
2249
  acquireExclusive,
2236
- CONFIG_FILE,
2237
- keyOf,
2238
- loadConfig,
2239
- resolveBaseline,
2240
2250
  CounterError,
2241
2251
  runCommand,
2242
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-Z2T6MHHQ.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.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  takeSlot,
29
29
  versionOf,
30
30
  writeEnvelope
31
- } from "./chunk-Z2T6MHHQ.js";
31
+ } from "./chunk-GJD2676B.js";
32
32
  export {
33
33
  CONFIG_FILE,
34
34
  COUNTERS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/ratchet",
3
- "version": "2.6.0",
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": [