@geonosis/ratchet 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -1
- package/dist/{chunk-QVORWCUD.js → chunk-FM5I6PRT.js} +632 -454
- package/dist/cli.js +24 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -196,6 +196,37 @@ failures, they are unmeasured — and unmeasured reads exactly like green.
|
|
|
196
196
|
`examples/during-day.ratchet.json` is four test entries for that reason: `apps/web` plus the three
|
|
197
197
|
workerd packages that were previously outside every gate.
|
|
198
198
|
|
|
199
|
+
### …or the runner's JSON report, which is stronger
|
|
200
|
+
|
|
201
|
+
`report: "vitest-json"` reads the runner's own machine-readable answer instead of its prose:
|
|
202
|
+
|
|
203
|
+
```jsonc
|
|
204
|
+
{
|
|
205
|
+
"counter": "testFailures",
|
|
206
|
+
"key": "testFailuresApi",
|
|
207
|
+
"command": "cd apps/api && bunx vitest run --reporter=json --outputFile={report}",
|
|
208
|
+
"report": "vitest-json"
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`{report}` is replaced with a path in a temp directory the counter makes and removes; give the entry
|
|
213
|
+
a `reportPath` instead when the report belongs somewhere your CI already collects. A command in this
|
|
214
|
+
mode with neither is refused, naming what is missing — a run whose report goes nowhere is a run
|
|
215
|
+
nobody can read.
|
|
216
|
+
|
|
217
|
+
The counter then reads `numFailedTests`, and **refuses** rather than returning a number when:
|
|
218
|
+
|
|
219
|
+
- the file is not there — a crash before the reporter wrote is not a pass, and it prints no summary
|
|
220
|
+
line either, so the summary parser had nothing to refuse on;
|
|
221
|
+
- the file is not JSON, or has no `numFailedTests` in it;
|
|
222
|
+
- the report says `success: false` and names **0** failing tests — the shape a pool that dies
|
|
223
|
+
mid-run writes. The run did not finish, so there is no number to bank.
|
|
224
|
+
|
|
225
|
+
The summary mode stays the default; nothing changes for an entry that does not ask for a report.
|
|
226
|
+
`--prove` proves BOTH: `testFailures` ships one probe per reading mode and prints them by name
|
|
227
|
+
(`PROVEN testFailuresApi (vitest-json)`), because proving the mode nobody configured says nothing
|
|
228
|
+
about the mode they did.
|
|
229
|
+
|
|
199
230
|
### `oxlintRule` counts a warned rule twice, on purpose
|
|
200
231
|
|
|
201
232
|
A rule parked at `"warn"` as ratcheted debt appears in two numbers: once inside `oxlintWarnings`,
|
|
@@ -206,6 +237,25 @@ That is not double-counting the total; the second key exists so the debt is visi
|
|
|
206
237
|
of hidden inside a lump sum that a different rule's warning could mask. dielime today: `oxlintWarnings`
|
|
207
238
|
12 → 135 when `no-raw-html-atoms` was armed, with its own key at 123.
|
|
208
239
|
|
|
240
|
+
### `--prove` also proves the lock
|
|
241
|
+
|
|
242
|
+
`--exclusive` is a claim about the machine, so it is measured on the machine, every `--prove`:
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
PROVEN exclusive: two runs of 400ms serialised, the second starting 161ms after the first finished
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The self-test runs two children of this CLI over `--hold <ms>` — an instrument that takes the lock,
|
|
249
|
+
says when it started, waits, says when it finished, and gives the lock back — and requires the second
|
|
250
|
+
to have started after the first finished. Anything else prints `CANNOT FAIL exclusive: interleaved`
|
|
251
|
+
and exits 2. It uses a lock file of its own in a temp directory, so proving the lock never takes the
|
|
252
|
+
real one out from under the runs it exists to serialise.
|
|
253
|
+
|
|
254
|
+
It is here because the lock did not work for two releases and every gate was green throughout:
|
|
255
|
+
`openSync(path, 'wx')` is atomic about the NAME and not about the holder, and a run polling in that
|
|
256
|
+
window read an unparsable lock, called it stale, and took it. A lock nobody has watched fail has not
|
|
257
|
+
been shown to work — and this one guards the running time of every other gate.
|
|
258
|
+
|
|
209
259
|
## Running it where it will actually run
|
|
210
260
|
|
|
211
261
|
- **Counters run in the caller's environment.** They inherit the shell the ratchet was started in,
|
|
@@ -266,7 +316,7 @@ Every counter takes its `command` from the config, so the toolchain stays the re
|
|
|
266
316
|
| `oxlintErrors` / `oxlintWarnings` | findings under any shape oxlint prints — `--format=unix`, the compact `agent` format, the graphical `default` — cross-checked against the tool's own summary | `command`, `expectFormat` |
|
|
267
317
|
| `oxlintRule` | one named rule's findings, counted only on lines the run reported as findings and refused when none of them attributes itself readably; with `config`, after forcing the rule to `error` in a temp copy — `"warn"`, `"off"` and the `["off", { … }]` array form alike — so debt cannot grow behind a downgrade | `rule`, `config`, `command`, `expectFormat` |
|
|
268
318
|
| `typecheckErrors` | `error TS` occurrences, refusing when TS2305/TS2307 name a workspace package of this repo — an unbuilt sibling is a missing build, not debt | `command` |
|
|
269
|
-
| `testFailures` | the runner's own failure summary; throws when
|
|
319
|
+
| `testFailures` | the runner's own failure summary — or, with `report: "vitest-json"`, `numFailedTests` out of the JSON report the command wrote; throws when nothing is readable, when the report is absent, and when the report failed with nothing failing | `command`, `report`, `reportPath` |
|
|
270
320
|
| `unformattedFiles` | paths `--list-different` names that exist on disk | `command` |
|
|
271
321
|
| `cloneCount` | jscpd's `Found N clones` | `command` |
|
|
272
322
|
| `knipIssues` | the totals under knip's unused-* headings | `command`, `headings` |
|
|
@@ -1,4 +1,131 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/core/lock.ts
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { linkSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
|
+
var heavyLockPath = () => process.env.GEONOSIS_HEAVY_LOCK ?? join(homedir(), ".cache", "geonosis", "heavy.lock");
|
|
7
|
+
var sleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
8
|
+
var holderOf = (path) => {
|
|
9
|
+
try {
|
|
10
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
11
|
+
return typeof parsed.pid === "number" ? {
|
|
12
|
+
cwd: parsed.cwd ?? "somewhere",
|
|
13
|
+
pid: parsed.pid,
|
|
14
|
+
startedAt: parsed.startedAt ?? "unknown"
|
|
15
|
+
} : void 0;
|
|
16
|
+
} catch {
|
|
17
|
+
return void 0;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var alive = (pid) => {
|
|
21
|
+
try {
|
|
22
|
+
process.kill(pid, 0);
|
|
23
|
+
return true;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return error.code === "EPERM";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var heldFor = (holder) => {
|
|
29
|
+
const since = Date.parse(holder.startedAt);
|
|
30
|
+
if (Number.isNaN(since)) return "an unknown time";
|
|
31
|
+
return `${Math.round((Date.now() - since) / 1e3)}s`;
|
|
32
|
+
};
|
|
33
|
+
var write = (path) => {
|
|
34
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
35
|
+
const mine = {
|
|
36
|
+
cwd: process.cwd(),
|
|
37
|
+
pid: process.pid,
|
|
38
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
39
|
+
};
|
|
40
|
+
const staging = `${path}.${process.pid}.${randomUUID()}`;
|
|
41
|
+
try {
|
|
42
|
+
writeFileSync(staging, JSON.stringify(mine));
|
|
43
|
+
linkSync(staging, path);
|
|
44
|
+
return true;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.code === "EEXIST") return false;
|
|
47
|
+
throw error;
|
|
48
|
+
} finally {
|
|
49
|
+
rmSync(staging, { force: true });
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var acquireExclusive = async ({
|
|
53
|
+
noticeMs = 15e3,
|
|
54
|
+
path = heavyLockPath(),
|
|
55
|
+
pollMs = 250,
|
|
56
|
+
say = (line) => process.stderr.write(`${line}
|
|
57
|
+
`),
|
|
58
|
+
timeoutSeconds = 1800
|
|
59
|
+
} = {}) => {
|
|
60
|
+
const until = Date.now() + timeoutSeconds * 1e3;
|
|
61
|
+
let told = 0;
|
|
62
|
+
const release = () => {
|
|
63
|
+
if (holderOf(path)?.pid === process.pid) rmSync(path, { force: true });
|
|
64
|
+
};
|
|
65
|
+
for (; ; ) {
|
|
66
|
+
if (write(path)) return release;
|
|
67
|
+
const holder = holderOf(path);
|
|
68
|
+
if (holder === void 0 || !alive(holder.pid)) {
|
|
69
|
+
say(
|
|
70
|
+
`geonosis-ratchet: taking over a stale heavy lock (pid ${holder?.pid ?? "unreadable"} is gone)`
|
|
71
|
+
);
|
|
72
|
+
rmSync(path, { force: true });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (Date.now() >= until) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`waited ${timeoutSeconds}s for the heavy lock held by pid ${holder.pid} in ${holder.cwd} (${heldFor(holder)}) \u2014 give it longer with --exclusive-timeout, or stop that run`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
if (now - told >= noticeMs) {
|
|
82
|
+
told = now;
|
|
83
|
+
say(
|
|
84
|
+
`geonosis-ratchet: waiting for the heavy lock \u2014 pid ${holder.pid} in ${holder.cwd}, held for ${heldFor(holder)}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
await sleep(pollMs);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// src/core/config.ts
|
|
92
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
93
|
+
import { resolve } from "path";
|
|
94
|
+
var CONFIG_FILE = "geonosis.ratchet.json";
|
|
95
|
+
var keyOf = (entry) => entry.key ?? entry.counter;
|
|
96
|
+
var loadConfig = (cwd) => {
|
|
97
|
+
const path = resolve(cwd, CONFIG_FILE);
|
|
98
|
+
if (!existsSync(path)) {
|
|
99
|
+
throw new Error(`no ${CONFIG_FILE} in ${cwd} \u2014 the ratchet has nothing to count`);
|
|
100
|
+
}
|
|
101
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
102
|
+
if (!Array.isArray(parsed.counters)) {
|
|
103
|
+
throw new Error(`${CONFIG_FILE} has no "counters" array`);
|
|
104
|
+
}
|
|
105
|
+
for (const entry of parsed.counters) {
|
|
106
|
+
if (typeof entry.counter !== "string") {
|
|
107
|
+
throw new Error(`${CONFIG_FILE}: every counter entry needs a "counter" id`);
|
|
108
|
+
}
|
|
109
|
+
if (entry.tiers !== void 0) {
|
|
110
|
+
const named = Array.isArray(entry.tiers) && entry.tiers.length > 0 && entry.tiers.every((tier) => typeof tier === "string" && tier.length > 0);
|
|
111
|
+
if (!named) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`${CONFIG_FILE}: "${keyOf(entry)}" has "tiers" that is not a non-empty list of tier names`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const keys = parsed.counters.map(keyOf);
|
|
119
|
+
const duplicate = keys.find((key, at) => keys.indexOf(key) !== at);
|
|
120
|
+
if (duplicate !== void 0) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`${CONFIG_FILE}: two counters both write "${duplicate}" \u2014 give one of them a distinct "key"`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return { baseline: parsed.baseline ?? "gate-baseline.json", counters: parsed.counters };
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// src/core/types.ts
|
|
2
129
|
var CounterError = class extends Error {
|
|
3
130
|
constructor(counter, message) {
|
|
4
131
|
super(`${counter}: ${message}`);
|
|
@@ -8,6 +135,276 @@ var CounterError = class extends Error {
|
|
|
8
135
|
counter;
|
|
9
136
|
};
|
|
10
137
|
|
|
138
|
+
// src/core/shell.ts
|
|
139
|
+
import { execSync } from "child_process";
|
|
140
|
+
var ANSI = /\[[0-9;]*m/g;
|
|
141
|
+
var runCommand = (cwd, counterId, env) => (command) => {
|
|
142
|
+
try {
|
|
143
|
+
const output = execSync(`${command} 2>&1`, {
|
|
144
|
+
cwd,
|
|
145
|
+
encoding: "utf8",
|
|
146
|
+
env,
|
|
147
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
148
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
149
|
+
});
|
|
150
|
+
return { code: 0, output: output.replaceAll(ANSI, "") };
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const failed = error;
|
|
153
|
+
const output = `${failed.stdout ?? ""}${failed.stderr ?? ""}`.replaceAll(ANSI, "");
|
|
154
|
+
const code = failed.status ?? -1;
|
|
155
|
+
if (code === 126 || code === 127 || code === -1) {
|
|
156
|
+
throw new CounterError(
|
|
157
|
+
counterId,
|
|
158
|
+
`command did not run (exit ${code}): ${command}
|
|
159
|
+
${output.trim()}`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return { code, output };
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// src/core/prove.ts
|
|
167
|
+
import { existsSync as existsSync2, mkdtempSync, rmSync as rmSync2 } from "fs";
|
|
168
|
+
import { tmpdir } from "os";
|
|
169
|
+
import { delimiter, dirname as dirname3, join as join2, resolve as resolve3 } from "path";
|
|
170
|
+
|
|
171
|
+
// src/core/exclusive.ts
|
|
172
|
+
import { spawn } from "child_process";
|
|
173
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
174
|
+
var MARK = /^exclusive-hold (start|end) (\d+)$/gm;
|
|
175
|
+
var KEY = "exclusive";
|
|
176
|
+
var spanOf = (output) => {
|
|
177
|
+
const marks = [...output.matchAll(MARK)];
|
|
178
|
+
const start = marks.find((one) => one[1] === "start")?.[2];
|
|
179
|
+
const end = marks.find((one) => one[1] === "end")?.[2];
|
|
180
|
+
if (start === void 0 || end === void 0) return void 0;
|
|
181
|
+
return { end: Number(end), start: Number(start) };
|
|
182
|
+
};
|
|
183
|
+
var hold = (cli, holdMs, lockPath) => new Promise((done) => {
|
|
184
|
+
const child = spawn(
|
|
185
|
+
process.execPath,
|
|
186
|
+
[cli, "--exclusive", "--exclusive-timeout", "60", "--hold", String(holdMs)],
|
|
187
|
+
{
|
|
188
|
+
cwd: dirname2(resolve2(cli)),
|
|
189
|
+
env: { ...process.env, GEONOSIS_HEAVY_LOCK: lockPath },
|
|
190
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
let output = "";
|
|
194
|
+
child.stdout.on("data", (chunk) => {
|
|
195
|
+
output += chunk.toString();
|
|
196
|
+
});
|
|
197
|
+
child.stderr.on("data", (chunk) => {
|
|
198
|
+
output += chunk.toString();
|
|
199
|
+
});
|
|
200
|
+
child.on("close", (code) => done({ code, output }));
|
|
201
|
+
});
|
|
202
|
+
var proveExclusive = async ({
|
|
203
|
+
cli,
|
|
204
|
+
holdMs = 400,
|
|
205
|
+
lockPath
|
|
206
|
+
}) => {
|
|
207
|
+
const runs = await Promise.all([hold(cli, holdMs, lockPath), hold(cli, holdMs, lockPath)]);
|
|
208
|
+
const spans = runs.map((one) => spanOf(one.output));
|
|
209
|
+
const [first, second] = spans;
|
|
210
|
+
if (first === void 0 || second === void 0) {
|
|
211
|
+
return {
|
|
212
|
+
counter: KEY,
|
|
213
|
+
key: KEY,
|
|
214
|
+
reason: `a run under --exclusive printed no start/end to compare:
|
|
215
|
+
${runs.map((one) => `exit ${String(one.code)}: ${one.output.trim().slice(-300)}`).join("\n")}`,
|
|
216
|
+
verdict: "cannot-measure"
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const [early, late] = first.start <= second.start ? [first, second] : [second, first];
|
|
220
|
+
const gap = late.start - early.end;
|
|
221
|
+
if (gap < 0) {
|
|
222
|
+
return {
|
|
223
|
+
key: KEY,
|
|
224
|
+
reason: `the second run started ${-gap}ms BEFORE the first finished \u2014 two runs, one lock, both holding it`,
|
|
225
|
+
verdict: "interleaved"
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
key: KEY,
|
|
230
|
+
reason: `two runs of ${holdMs}ms serialised, the second starting ${gap}ms after the first finished`,
|
|
231
|
+
verdict: "serialised"
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// src/core/prove.ts
|
|
236
|
+
var toolPath = (cwd) => {
|
|
237
|
+
const dirs = [];
|
|
238
|
+
let dir = resolve3(cwd);
|
|
239
|
+
for (; ; ) {
|
|
240
|
+
const bin = join2(dir, "node_modules", ".bin");
|
|
241
|
+
if (existsSync2(bin)) dirs.push(bin);
|
|
242
|
+
const parent = dirname3(dir);
|
|
243
|
+
if (parent === dir) break;
|
|
244
|
+
dir = parent;
|
|
245
|
+
}
|
|
246
|
+
return [...dirs, process.env.PATH ?? ""].join(delimiter);
|
|
247
|
+
};
|
|
248
|
+
var NO_PROBE = "no probe \u2014 a counter nobody has seen read a planted finding has not been shown to measure";
|
|
249
|
+
var oneProofOf = async (counter, key, path, probe) => {
|
|
250
|
+
const dir = mkdtempSync(join2(tmpdir(), "geonosis-prove-"));
|
|
251
|
+
try {
|
|
252
|
+
probe.input(dir);
|
|
253
|
+
const command = probe.command?.(dir);
|
|
254
|
+
const reading = await counter.run({
|
|
255
|
+
cwd: dir,
|
|
256
|
+
key,
|
|
257
|
+
params: { ...probe.params, ...command === void 0 ? {} : { command } },
|
|
258
|
+
run: runCommand(dir, counter.id, { ...process.env, PATH: path })
|
|
259
|
+
});
|
|
260
|
+
if (reading === 0) return { counter: counter.id, key, reading, verdict: "cannot-fail" };
|
|
261
|
+
if (reading < probe.expect) {
|
|
262
|
+
return { counter: counter.id, expected: probe.expect, key, reading, verdict: "misread" };
|
|
263
|
+
}
|
|
264
|
+
return { counter: counter.id, key, reading, verdict: "proven" };
|
|
265
|
+
} catch (error) {
|
|
266
|
+
return { counter: counter.id, key, reason: error.message, verdict: "cannot-measure" };
|
|
267
|
+
} finally {
|
|
268
|
+
rmSync2(dir, { force: true, recursive: true });
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
var probesOf = (counter) => {
|
|
272
|
+
if (counter.probe === void 0) return [];
|
|
273
|
+
return Array.isArray(counter.probe) ? counter.probe : [counter.probe];
|
|
274
|
+
};
|
|
275
|
+
var proofOf = async (counter, key, path) => {
|
|
276
|
+
const probes = probesOf(counter);
|
|
277
|
+
if (probes.length === 0) {
|
|
278
|
+
return [{ counter: counter.id, key, reason: NO_PROBE, verdict: "cannot-measure" }];
|
|
279
|
+
}
|
|
280
|
+
const proofs = [];
|
|
281
|
+
for (const probe of probes) {
|
|
282
|
+
const named = probe.name === void 0 ? key : `${key} (${probe.name})`;
|
|
283
|
+
const proof = await oneProofOf(counter, named, path, probe);
|
|
284
|
+
proofs.push(proof);
|
|
285
|
+
if (proof.verdict !== "proven") break;
|
|
286
|
+
}
|
|
287
|
+
return proofs;
|
|
288
|
+
};
|
|
289
|
+
var outsideTier = (entry, tier) => entry.tiers !== void 0 && !entry.tiers.includes(tier);
|
|
290
|
+
var runProve = async ({
|
|
291
|
+
counters,
|
|
292
|
+
cwd,
|
|
293
|
+
exclusiveVia,
|
|
294
|
+
tier
|
|
295
|
+
}) => {
|
|
296
|
+
const config = loadConfig(cwd);
|
|
297
|
+
const byId = new Map(counters.map((one) => [one.id, one]));
|
|
298
|
+
const path = toolPath(cwd);
|
|
299
|
+
const proofs = [];
|
|
300
|
+
for (const entry of config.counters) {
|
|
301
|
+
const counter = byId.get(entry.counter);
|
|
302
|
+
if (counter === void 0) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
`no counter implements "${entry.counter}" \u2014 known ids: ${[...byId.keys()].toSorted().join(", ")}`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const key = keyOf(entry);
|
|
308
|
+
if (tier !== void 0 && outsideTier(entry, tier)) {
|
|
309
|
+
proofs.push({ key, tier, verdict: "skipped" });
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const taken = await proofOf(counter, key, path);
|
|
313
|
+
proofs.push(...taken);
|
|
314
|
+
if (taken.some((one) => one.verdict !== "proven" && one.verdict !== "skipped")) {
|
|
315
|
+
return { proofs, proven: false };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (exclusiveVia !== void 0) {
|
|
319
|
+
const dir = mkdtempSync(join2(tmpdir(), "geonosis-lock-"));
|
|
320
|
+
try {
|
|
321
|
+
const proof = await proveExclusive({ cli: exclusiveVia, lockPath: join2(dir, "heavy.lock") });
|
|
322
|
+
proofs.push(proof);
|
|
323
|
+
if (proof.verdict !== "serialised") return { proofs, proven: false };
|
|
324
|
+
} finally {
|
|
325
|
+
rmSync2(dir, { force: true, recursive: true });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return { proofs, proven: true };
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// src/core/ratchet.ts
|
|
332
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
333
|
+
import { resolve as resolve4 } from "path";
|
|
334
|
+
var EVIDENCE_LINES = 10;
|
|
335
|
+
var recorded = (run) => {
|
|
336
|
+
let output = "";
|
|
337
|
+
return {
|
|
338
|
+
last: () => output.split("\n").map((line) => line.trimEnd()).filter((line) => line !== "").slice(-EVIDENCE_LINES),
|
|
339
|
+
run: (command) => {
|
|
340
|
+
const result = run(command);
|
|
341
|
+
output = result.output;
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
};
|
|
346
|
+
var verdictOf = (now, baseline) => {
|
|
347
|
+
if (now > baseline) return "grew";
|
|
348
|
+
if (now < baseline) return "shrank";
|
|
349
|
+
return "held";
|
|
350
|
+
};
|
|
351
|
+
var outsideTier2 = (entry, tier) => entry.tiers !== void 0 && !entry.tiers.includes(tier);
|
|
352
|
+
var runRatchet = async ({
|
|
353
|
+
counters,
|
|
354
|
+
cwd,
|
|
355
|
+
tier
|
|
356
|
+
}) => {
|
|
357
|
+
const config = loadConfig(cwd);
|
|
358
|
+
const baselinePath = resolve4(cwd, config.baseline);
|
|
359
|
+
if (!existsSync3(baselinePath)) {
|
|
360
|
+
throw new Error(`no ${config.baseline} in ${cwd} \u2014 nothing to ratchet against`);
|
|
361
|
+
}
|
|
362
|
+
const baseline = JSON.parse(readFileSync3(baselinePath, "utf8"));
|
|
363
|
+
const byId = new Map(counters.map((one) => [one.id, one]));
|
|
364
|
+
const measurements = [];
|
|
365
|
+
for (const entry of config.counters) {
|
|
366
|
+
const counter = byId.get(entry.counter);
|
|
367
|
+
if (counter === void 0) {
|
|
368
|
+
throw new Error(
|
|
369
|
+
`no counter implements "${entry.counter}" \u2014 known ids: ${[...byId.keys()].toSorted().join(", ")}`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
const key = keyOf(entry);
|
|
373
|
+
if (tier !== void 0 && outsideTier2(entry, tier)) {
|
|
374
|
+
measurements.push({ key, tier, verdict: "skipped" });
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const limit = baseline[key];
|
|
378
|
+
if (typeof limit !== "number") {
|
|
379
|
+
throw new Error(
|
|
380
|
+
`${config.baseline} has no number for "${key}" \u2014 add it before enabling the counter`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
const recorder = recorded(runCommand(cwd, entry.counter));
|
|
384
|
+
const now = await counter.run({ cwd, key, params: entry, run: recorder.run });
|
|
385
|
+
const verdict = verdictOf(now, limit);
|
|
386
|
+
measurements.push({
|
|
387
|
+
baseline: limit,
|
|
388
|
+
evidence: verdict === "grew" ? recorder.last() : [],
|
|
389
|
+
key,
|
|
390
|
+
now,
|
|
391
|
+
verdict
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const grew = measurements.some((one) => one.verdict === "grew");
|
|
395
|
+
const shrank = measurements.some((one) => one.verdict === "shrank");
|
|
396
|
+
if (shrank && !grew) {
|
|
397
|
+
const next = { ...baseline };
|
|
398
|
+
for (const one of measurements) {
|
|
399
|
+
if (one.verdict !== "skipped") next[one.key] = one.now;
|
|
400
|
+
}
|
|
401
|
+
writeFileSync2(baselinePath, `${JSON.stringify(next, null, 2)}
|
|
402
|
+
`);
|
|
403
|
+
return { measurements, rewritten: true };
|
|
404
|
+
}
|
|
405
|
+
return { measurements, rewritten: false };
|
|
406
|
+
};
|
|
407
|
+
|
|
11
408
|
// src/counters/params.ts
|
|
12
409
|
var stringParam = (counter, params, name, fallback) => {
|
|
13
410
|
const value = params[name];
|
|
@@ -24,12 +421,12 @@ var countMatches = (text, pattern) => text.match(new RegExp(pattern.source, `${p
|
|
|
24
421
|
var escapeForRegex = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
25
422
|
|
|
26
423
|
// src/counters/plant.ts
|
|
27
|
-
import { mkdirSync, writeFileSync } from "fs";
|
|
28
|
-
import { dirname, join } from "path";
|
|
424
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
425
|
+
import { dirname as dirname4, join as join3 } from "path";
|
|
29
426
|
var plant = (dir, relative, contents) => {
|
|
30
|
-
const path =
|
|
31
|
-
|
|
32
|
-
|
|
427
|
+
const path = join3(dir, relative);
|
|
428
|
+
mkdirSync2(dirname4(path), { recursive: true });
|
|
429
|
+
writeFileSync3(path, contents);
|
|
33
430
|
};
|
|
34
431
|
var captured = (sample) => ({
|
|
35
432
|
command: () => "cat sample.txt",
|
|
@@ -93,8 +490,8 @@ var boundaryIssues = {
|
|
|
93
490
|
};
|
|
94
491
|
|
|
95
492
|
// src/counters/format.ts
|
|
96
|
-
import { existsSync } from "fs";
|
|
97
|
-
import { resolve } from "path";
|
|
493
|
+
import { existsSync as existsSync4 } from "fs";
|
|
494
|
+
import { resolve as resolve5 } from "path";
|
|
98
495
|
var unformattedFiles = {
|
|
99
496
|
id: "unformattedFiles",
|
|
100
497
|
probe: {
|
|
@@ -109,13 +506,13 @@ var unformattedFiles = {
|
|
|
109
506
|
"command",
|
|
110
507
|
"npx oxfmt --config .oxfmtrc.json --list-different ."
|
|
111
508
|
);
|
|
112
|
-
return run(command).output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 &&
|
|
509
|
+
return run(command).output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && existsSync4(resolve5(cwd, line))).length;
|
|
113
510
|
}
|
|
114
511
|
};
|
|
115
512
|
|
|
116
513
|
// src/counters/law.ts
|
|
117
|
-
import { existsSync as
|
|
118
|
-
import { resolve as
|
|
514
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
515
|
+
import { resolve as resolve6 } from "path";
|
|
119
516
|
var lawLineCount = {
|
|
120
517
|
id: "lawLineCount",
|
|
121
518
|
probe: {
|
|
@@ -125,15 +522,15 @@ var lawLineCount = {
|
|
|
125
522
|
},
|
|
126
523
|
run: async ({ cwd, params }) => {
|
|
127
524
|
const relative = stringParam("lawLineCount", params, "path", "CLAUDE.md");
|
|
128
|
-
const path =
|
|
129
|
-
if (!
|
|
130
|
-
return
|
|
525
|
+
const path = resolve6(cwd, relative);
|
|
526
|
+
if (!existsSync5(path)) throw new CounterError("lawLineCount", `no law file at ${relative}`);
|
|
527
|
+
return readFileSync4(path, "utf8").replace(/\n$/, "").split("\n").length;
|
|
131
528
|
}
|
|
132
529
|
};
|
|
133
530
|
|
|
134
531
|
// src/counters/oxlint.ts
|
|
135
|
-
import { existsSync as
|
|
136
|
-
import { resolve as
|
|
532
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
533
|
+
import { resolve as resolve7 } from "path";
|
|
137
534
|
var DEFAULT_COMMAND = "npx oxlint --format=unix --config .oxlintrc.json .";
|
|
138
535
|
var PROBE_COMMAND = "oxlint --format=unix --config .oxlintrc.json .";
|
|
139
536
|
var oxlintProbe = (severity, rule, source) => ({
|
|
@@ -272,18 +669,18 @@ var oxlintRule = {
|
|
|
272
669
|
const config = typeof params.config === "string" ? params.config : "";
|
|
273
670
|
const expect = expectedFormat("oxlintRule", params);
|
|
274
671
|
if (config === "") return countRule(run(command), rule, expect);
|
|
275
|
-
const source =
|
|
276
|
-
if (!
|
|
672
|
+
const source = resolve7(cwd, config);
|
|
673
|
+
if (!existsSync6(source)) throw new CounterError("oxlintRule", `no config at ${config}`);
|
|
277
674
|
const strictName = `.oxlintrc.ratchet-${key}.json`;
|
|
278
|
-
const strict =
|
|
675
|
+
const strict = readFileSync5(source, "utf8").replace(
|
|
279
676
|
new RegExp(`("[^"]*${escapeForRegex(rule)}"\\s*:\\s*\\[?\\s*)"(warn|off)"`),
|
|
280
677
|
'$1"error"'
|
|
281
678
|
);
|
|
282
|
-
|
|
679
|
+
writeFileSync4(resolve7(cwd, strictName), strict);
|
|
283
680
|
try {
|
|
284
681
|
return countRule(run(command.replace("{config}", strictName)), rule, expect);
|
|
285
682
|
} finally {
|
|
286
|
-
|
|
683
|
+
rmSync3(resolve7(cwd, strictName), { force: true });
|
|
287
684
|
}
|
|
288
685
|
}
|
|
289
686
|
};
|
|
@@ -323,471 +720,231 @@ var sumOfCounts = {
|
|
|
323
720
|
};
|
|
324
721
|
|
|
325
722
|
// src/counters/tests.ts
|
|
723
|
+
import { existsSync as existsSync7, mkdtempSync as mkdtempSync2, readFileSync as readFileSync6, rmSync as rmSync4 } from "fs";
|
|
724
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
725
|
+
import { join as join4, resolve as resolve8 } from "path";
|
|
326
726
|
var FAILED = /(\d+)\s+fail(?:ed|ing|s)?\b/;
|
|
327
727
|
var PASSED = /(\d+)\s+pass(?:ed|ing|es)?\b/;
|
|
328
|
-
var
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
"testFailures",
|
|
339
|
-
`no pass/fail summary in the runner output:
|
|
728
|
+
var COUNTER = "testFailures";
|
|
729
|
+
var VITEST_JSON = "vitest-json";
|
|
730
|
+
var PLACEHOLDER = "{report}";
|
|
731
|
+
var fromSummary = (output) => {
|
|
732
|
+
const failed = output.match(FAILED)?.[1];
|
|
733
|
+
if (failed !== void 0) return Number(failed);
|
|
734
|
+
if (PASSED.test(output)) return 0;
|
|
735
|
+
throw new CounterError(
|
|
736
|
+
COUNTER,
|
|
737
|
+
`no pass/fail summary in the runner output:
|
|
340
738
|
${output.trim().slice(-500)}`
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
// src/counters/workspace.ts
|
|
346
|
-
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync3 } from "fs";
|
|
347
|
-
import { join as join2, resolve as resolve4 } from "path";
|
|
348
|
-
var SKIP = /^(node_modules|\.)/;
|
|
349
|
-
var childDirs = (dir) => {
|
|
350
|
-
try {
|
|
351
|
-
return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !SKIP.test(entry.name)).map((entry) => join2(dir, entry.name));
|
|
352
|
-
} catch {
|
|
353
|
-
return [];
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
var descendants = (dir, depth) => depth === 0 ? [dir] : [dir, ...childDirs(dir).flatMap((child) => descendants(child, depth - 1))];
|
|
357
|
-
var expand = (root, pattern) => {
|
|
358
|
-
const segments = pattern.split("/").filter((one) => one !== "" && one !== ".");
|
|
359
|
-
let dirs = [root];
|
|
360
|
-
for (const segment of segments) {
|
|
361
|
-
dirs = segment === "*" ? dirs.flatMap((dir) => childDirs(dir)) : segment === "**" ? dirs.flatMap((dir) => descendants(dir, 3)) : dirs.map((dir) => join2(dir, segment)).filter((dir) => existsSync4(dir));
|
|
362
|
-
}
|
|
363
|
-
return dirs;
|
|
364
|
-
};
|
|
365
|
-
var QUOTED = /^['"]|['"]$/g;
|
|
366
|
-
var cleaned = (value) => value.replace(/#.*$/, "").trim().replaceAll(QUOTED, "");
|
|
367
|
-
var pnpmPatterns = (path) => {
|
|
368
|
-
const lines = readFileSync3(path, "utf8").split("\n");
|
|
369
|
-
const at = lines.findIndex((line) => line.startsWith("packages:"));
|
|
370
|
-
if (at === -1) return [];
|
|
371
|
-
const inline = lines[at]?.slice("packages:".length).trim() ?? "";
|
|
372
|
-
if (inline.startsWith("[")) {
|
|
373
|
-
return inline.replace(/^\[|\]$/g, "").split(",").map(cleaned).filter((one) => one !== "");
|
|
374
|
-
}
|
|
375
|
-
const patterns = [];
|
|
376
|
-
for (const line of lines.slice(at + 1)) {
|
|
377
|
-
const item = /^\s*-\s*(.+)$/.exec(line);
|
|
378
|
-
if (item?.[1] !== void 0) {
|
|
379
|
-
patterns.push(cleaned(item[1]));
|
|
380
|
-
continue;
|
|
381
|
-
}
|
|
382
|
-
if (line.trim() !== "") break;
|
|
383
|
-
}
|
|
384
|
-
return patterns;
|
|
385
|
-
};
|
|
386
|
-
var npmPatterns = (path) => {
|
|
387
|
-
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
388
|
-
const declared = Array.isArray(parsed.workspaces) ? parsed.workspaces : parsed.workspaces?.packages ?? [];
|
|
389
|
-
return declared.filter((one) => typeof one === "string");
|
|
390
|
-
};
|
|
391
|
-
var nameOf = (dir) => {
|
|
392
|
-
const manifest = join2(dir, "package.json");
|
|
393
|
-
if (!existsSync4(manifest)) return void 0;
|
|
394
|
-
try {
|
|
395
|
-
const { name } = JSON.parse(readFileSync3(manifest, "utf8"));
|
|
396
|
-
return typeof name === "string" && name !== "" ? name : void 0;
|
|
397
|
-
} catch {
|
|
398
|
-
return void 0;
|
|
399
|
-
}
|
|
400
|
-
};
|
|
401
|
-
var workspacePackageNames = (cwd) => {
|
|
402
|
-
const root = resolve4(cwd);
|
|
403
|
-
const pnpm = join2(root, "pnpm-workspace.yaml");
|
|
404
|
-
const manifest = join2(root, "package.json");
|
|
405
|
-
const patterns = existsSync4(pnpm) ? pnpmPatterns(pnpm) : existsSync4(manifest) ? npmPatterns(manifest) : [];
|
|
406
|
-
const names = patterns.filter((pattern) => !pattern.startsWith("!")).flatMap((pattern) => expand(root, pattern)).map((dir) => nameOf(dir)).filter((name) => name !== void 0);
|
|
407
|
-
return [...new Set(names)];
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
// src/counters/typecheck.ts
|
|
411
|
-
var UNRESOLVED = /error TS(?:2305|2307): ([^\n]*)/g;
|
|
412
|
-
var SPECIFIER = /'([^']+)'/g;
|
|
413
|
-
var specifiersIn = (message) => [...message.matchAll(SPECIFIER)].map(([, quoted]) => (quoted ?? "").replaceAll('"', ""));
|
|
414
|
-
var unbuiltIn = (cwd, output) => {
|
|
415
|
-
const messages = [...output.matchAll(UNRESOLVED)].map(([, message]) => message ?? "");
|
|
416
|
-
if (messages.length === 0) return [];
|
|
417
|
-
const packages = workspacePackageNames(cwd);
|
|
418
|
-
const named = messages.flatMap((message) => specifiersIn(message)).flatMap(
|
|
419
|
-
(specifier) => packages.filter((name) => specifier === name || specifier.startsWith(`${name}/`))
|
|
420
739
|
);
|
|
421
|
-
return [...new Set(named)].toSorted();
|
|
422
|
-
};
|
|
423
|
-
var typecheckErrors = {
|
|
424
|
-
id: "typecheckErrors",
|
|
425
|
-
probe: {
|
|
426
|
-
command: () => "tsc --noEmit a.ts",
|
|
427
|
-
expect: 1,
|
|
428
|
-
input: (dir) => plant(dir, "a.ts", "export const n: number = 'not a number'\n")
|
|
429
|
-
},
|
|
430
|
-
run: async ({ cwd, params, run }) => {
|
|
431
|
-
const command = stringParam("typecheckErrors", params, "command", "npx tsc --noEmit");
|
|
432
|
-
const output = run(command).output;
|
|
433
|
-
const unbuilt = unbuiltIn(cwd, output);
|
|
434
|
-
if (unbuilt.length > 0) {
|
|
435
|
-
throw new CounterError(
|
|
436
|
-
"typecheckErrors",
|
|
437
|
-
`workspace packages not built: ${unbuilt.join(", ")} \u2014 build them before measuring typecheckErrors`
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
|
-
return countMatches(output, /error TS/);
|
|
441
|
-
}
|
|
442
740
|
};
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
cloneCount,
|
|
449
|
-
knipIssues,
|
|
450
|
-
lawLineCount,
|
|
451
|
-
oxlintErrors,
|
|
452
|
-
oxlintRule,
|
|
453
|
-
oxlintWarnings,
|
|
454
|
-
runtimeCodeShipped,
|
|
455
|
-
sumOfCounts,
|
|
456
|
-
testFailures,
|
|
457
|
-
typecheckErrors,
|
|
458
|
-
unformattedFiles
|
|
459
|
-
];
|
|
460
|
-
var counterById = (id) => {
|
|
461
|
-
const counter = COUNTERS.find((one) => one.id === id);
|
|
462
|
-
if (counter === void 0) {
|
|
463
|
-
throw new Error(
|
|
464
|
-
`no counter implements "${id}" \u2014 known ids: ${COUNTERS.map((one) => one.id).join(", ")}`
|
|
741
|
+
var fromReport = (path) => {
|
|
742
|
+
if (!existsSync7(path)) {
|
|
743
|
+
throw new CounterError(
|
|
744
|
+
COUNTER,
|
|
745
|
+
`the runner wrote no report at ${path} \u2014 a crash before the reporter is not a pass`
|
|
465
746
|
);
|
|
466
747
|
}
|
|
467
|
-
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
// src/lock.ts
|
|
471
|
-
import { randomUUID } from "crypto";
|
|
472
|
-
import { linkSync, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
473
|
-
import { homedir } from "os";
|
|
474
|
-
import { dirname as dirname2, join as join3 } from "path";
|
|
475
|
-
var heavyLockPath = () => process.env.GEONOSIS_HEAVY_LOCK ?? join3(homedir(), ".cache", "geonosis", "heavy.lock");
|
|
476
|
-
var sleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
477
|
-
var holderOf = (path) => {
|
|
478
|
-
try {
|
|
479
|
-
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
480
|
-
return typeof parsed.pid === "number" ? {
|
|
481
|
-
cwd: parsed.cwd ?? "somewhere",
|
|
482
|
-
pid: parsed.pid,
|
|
483
|
-
startedAt: parsed.startedAt ?? "unknown"
|
|
484
|
-
} : void 0;
|
|
485
|
-
} catch {
|
|
486
|
-
return void 0;
|
|
487
|
-
}
|
|
488
|
-
};
|
|
489
|
-
var alive = (pid) => {
|
|
748
|
+
let report;
|
|
490
749
|
try {
|
|
491
|
-
|
|
492
|
-
return true;
|
|
750
|
+
report = JSON.parse(readFileSync6(path, "utf8"));
|
|
493
751
|
} catch (error) {
|
|
494
|
-
|
|
752
|
+
throw new CounterError(
|
|
753
|
+
COUNTER,
|
|
754
|
+
`the report at ${path} is not JSON: ${error.message}`
|
|
755
|
+
);
|
|
495
756
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
var write = (path) => {
|
|
503
|
-
mkdirSync2(dirname2(path), { recursive: true });
|
|
504
|
-
const mine = {
|
|
505
|
-
cwd: process.cwd(),
|
|
506
|
-
pid: process.pid,
|
|
507
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
508
|
-
};
|
|
509
|
-
const staging = `${path}.${process.pid}.${randomUUID()}`;
|
|
510
|
-
try {
|
|
511
|
-
writeFileSync3(staging, JSON.stringify(mine));
|
|
512
|
-
linkSync(staging, path);
|
|
513
|
-
return true;
|
|
514
|
-
} catch (error) {
|
|
515
|
-
if (error.code === "EEXIST") return false;
|
|
516
|
-
throw error;
|
|
517
|
-
} finally {
|
|
518
|
-
rmSync2(staging, { force: true });
|
|
757
|
+
const failed = report.numFailedTests;
|
|
758
|
+
if (typeof failed !== "number") {
|
|
759
|
+
throw new CounterError(
|
|
760
|
+
COUNTER,
|
|
761
|
+
`the report at ${path} has no "numFailedTests" \u2014 it is not a vitest JSON report`
|
|
762
|
+
);
|
|
519
763
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
say = (line) => process.stderr.write(`${line}
|
|
526
|
-
`),
|
|
527
|
-
timeoutSeconds = 1800
|
|
528
|
-
} = {}) => {
|
|
529
|
-
const until = Date.now() + timeoutSeconds * 1e3;
|
|
530
|
-
let told = 0;
|
|
531
|
-
const release = () => {
|
|
532
|
-
if (holderOf(path)?.pid === process.pid) rmSync2(path, { force: true });
|
|
533
|
-
};
|
|
534
|
-
for (; ; ) {
|
|
535
|
-
if (write(path)) return release;
|
|
536
|
-
const holder = holderOf(path);
|
|
537
|
-
if (holder === void 0 || !alive(holder.pid)) {
|
|
538
|
-
say(
|
|
539
|
-
`geonosis-ratchet: taking over a stale heavy lock (pid ${holder?.pid ?? "unreadable"} is gone)`
|
|
540
|
-
);
|
|
541
|
-
rmSync2(path, { force: true });
|
|
542
|
-
continue;
|
|
543
|
-
}
|
|
544
|
-
if (Date.now() >= until) {
|
|
545
|
-
throw new Error(
|
|
546
|
-
`waited ${timeoutSeconds}s for the heavy lock held by pid ${holder.pid} in ${holder.cwd} (${heldFor(holder)}) \u2014 give it longer with --exclusive-timeout, or stop that run`
|
|
547
|
-
);
|
|
548
|
-
}
|
|
549
|
-
const now = Date.now();
|
|
550
|
-
if (now - told >= noticeMs) {
|
|
551
|
-
told = now;
|
|
552
|
-
say(
|
|
553
|
-
`geonosis-ratchet: waiting for the heavy lock \u2014 pid ${holder.pid} in ${holder.cwd}, held for ${heldFor(holder)}`
|
|
554
|
-
);
|
|
555
|
-
}
|
|
556
|
-
await sleep(pollMs);
|
|
764
|
+
if (report.success === false && failed === 0) {
|
|
765
|
+
throw new CounterError(
|
|
766
|
+
COUNTER,
|
|
767
|
+
`the report at ${path} says success: false and names 0 failing tests \u2014 the run did not finish, so there is no number to bank`
|
|
768
|
+
);
|
|
557
769
|
}
|
|
770
|
+
return failed;
|
|
558
771
|
};
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const path = resolve5(cwd, CONFIG_FILE);
|
|
567
|
-
if (!existsSync5(path)) {
|
|
568
|
-
throw new Error(`no ${CONFIG_FILE} in ${cwd} \u2014 the ratchet has nothing to count`);
|
|
569
|
-
}
|
|
570
|
-
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
571
|
-
if (!Array.isArray(parsed.counters)) {
|
|
572
|
-
throw new Error(`${CONFIG_FILE} has no "counters" array`);
|
|
573
|
-
}
|
|
574
|
-
for (const entry of parsed.counters) {
|
|
575
|
-
if (typeof entry.counter !== "string") {
|
|
576
|
-
throw new Error(`${CONFIG_FILE}: every counter entry needs a "counter" id`);
|
|
577
|
-
}
|
|
578
|
-
if (entry.tiers !== void 0) {
|
|
579
|
-
const named = Array.isArray(entry.tiers) && entry.tiers.length > 0 && entry.tiers.every((tier) => typeof tier === "string" && tier.length > 0);
|
|
580
|
-
if (!named) {
|
|
581
|
-
throw new Error(
|
|
582
|
-
`${CONFIG_FILE}: "${keyOf(entry)}" has "tiers" that is not a non-empty list of tier names`
|
|
583
|
-
);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
const keys = parsed.counters.map(keyOf);
|
|
588
|
-
const duplicate = keys.find((key, at) => keys.indexOf(key) !== at);
|
|
589
|
-
if (duplicate !== void 0) {
|
|
590
|
-
throw new Error(
|
|
591
|
-
`${CONFIG_FILE}: two counters both write "${duplicate}" \u2014 give one of them a distinct "key"`
|
|
772
|
+
var reportPathFor = (cwd, params, command) => {
|
|
773
|
+
const named = params.reportPath;
|
|
774
|
+
if (typeof named === "string" && named !== "") return { own: false, path: resolve8(cwd, named) };
|
|
775
|
+
if (!command.includes(PLACEHOLDER)) {
|
|
776
|
+
throw new CounterError(
|
|
777
|
+
COUNTER,
|
|
778
|
+
`report: "${VITEST_JSON}" needs somewhere to put the report \u2014 write ${PLACEHOLDER} into the command (--outputFile=${PLACEHOLDER}) or give the entry a "reportPath"`
|
|
592
779
|
);
|
|
593
780
|
}
|
|
594
|
-
return {
|
|
781
|
+
return { own: true, path: join4(mkdtempSync2(join4(tmpdir2(), "geonosis-report-")), "report.json") };
|
|
595
782
|
};
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
783
|
+
var testFailures = {
|
|
784
|
+
id: COUNTER,
|
|
785
|
+
probe: [
|
|
786
|
+
{ ...captured(" Tests 1 failed | 0 passed (1)\n"), expect: 1, name: "summary" },
|
|
787
|
+
{
|
|
788
|
+
// `cat` stands in for the runner: the placeholder is substituted into it, so a counter that
|
|
789
|
+
// stopped substituting would hand `cat` the literal token and read nothing.
|
|
790
|
+
command: () => `cat ${PLACEHOLDER}`,
|
|
791
|
+
expect: 1,
|
|
792
|
+
input: (dir) => plant(
|
|
793
|
+
dir,
|
|
794
|
+
"planted-report.json",
|
|
795
|
+
`${JSON.stringify({ numFailedTests: 1, numTotalTests: 1, success: false })}
|
|
796
|
+
`
|
|
797
|
+
),
|
|
798
|
+
name: VITEST_JSON,
|
|
799
|
+
params: { report: VITEST_JSON, reportPath: "planted-report.json" }
|
|
800
|
+
}
|
|
801
|
+
],
|
|
802
|
+
run: async ({ cwd, params, run }) => {
|
|
803
|
+
const command = stringParam(COUNTER, params, "command", "npx vitest run");
|
|
804
|
+
const mode = params.report;
|
|
805
|
+
if (mode === void 0) return fromSummary(run(command).output);
|
|
806
|
+
if (mode !== VITEST_JSON) {
|
|
807
|
+
throw new CounterError(
|
|
808
|
+
COUNTER,
|
|
809
|
+
`does not know the report format "${String(mode)}" \u2014 the one it reads is "${VITEST_JSON}"`
|
|
619
810
|
);
|
|
620
811
|
}
|
|
621
|
-
|
|
812
|
+
const { own, path } = reportPathFor(cwd, params, command);
|
|
813
|
+
try {
|
|
814
|
+
run(command.replaceAll(PLACEHOLDER, path));
|
|
815
|
+
return fromReport(path);
|
|
816
|
+
} finally {
|
|
817
|
+
if (own) rmSync4(join4(path, ".."), { force: true, recursive: true });
|
|
818
|
+
}
|
|
622
819
|
}
|
|
623
820
|
};
|
|
624
821
|
|
|
625
|
-
// src/
|
|
626
|
-
import { existsSync as
|
|
627
|
-
import {
|
|
628
|
-
|
|
629
|
-
var
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
if (existsSync6(bin)) dirs.push(bin);
|
|
635
|
-
const parent = dirname3(dir);
|
|
636
|
-
if (parent === dir) break;
|
|
637
|
-
dir = parent;
|
|
822
|
+
// src/counters/workspace.ts
|
|
823
|
+
import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync7 } from "fs";
|
|
824
|
+
import { join as join5, resolve as resolve9 } from "path";
|
|
825
|
+
var SKIP = /^(node_modules|\.)/;
|
|
826
|
+
var childDirs = (dir) => {
|
|
827
|
+
try {
|
|
828
|
+
return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !SKIP.test(entry.name)).map((entry) => join5(dir, entry.name));
|
|
829
|
+
} catch {
|
|
830
|
+
return [];
|
|
638
831
|
}
|
|
639
|
-
return [...dirs, process.env.PATH ?? ""].join(delimiter);
|
|
640
832
|
};
|
|
641
|
-
var
|
|
642
|
-
var
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
try {
|
|
648
|
-
probe.input(dir);
|
|
649
|
-
const command = probe.command?.(dir);
|
|
650
|
-
const reading = await counter.run({
|
|
651
|
-
cwd: dir,
|
|
652
|
-
key,
|
|
653
|
-
params: { ...probe.params, ...command === void 0 ? {} : { command } },
|
|
654
|
-
run: runCommand(dir, counter.id, { ...process.env, PATH: path })
|
|
655
|
-
});
|
|
656
|
-
if (reading === 0) return { counter: counter.id, key, reading, verdict: "cannot-fail" };
|
|
657
|
-
if (reading < probe.expect) {
|
|
658
|
-
return { counter: counter.id, expected: probe.expect, key, reading, verdict: "misread" };
|
|
659
|
-
}
|
|
660
|
-
return { counter: counter.id, key, reading, verdict: "proven" };
|
|
661
|
-
} catch (error) {
|
|
662
|
-
return { counter: counter.id, key, reason: error.message, verdict: "cannot-measure" };
|
|
663
|
-
} finally {
|
|
664
|
-
rmSync3(dir, { force: true, recursive: true });
|
|
833
|
+
var descendants = (dir, depth) => depth === 0 ? [dir] : [dir, ...childDirs(dir).flatMap((child) => descendants(child, depth - 1))];
|
|
834
|
+
var expand = (root, pattern) => {
|
|
835
|
+
const segments = pattern.split("/").filter((one) => one !== "" && one !== ".");
|
|
836
|
+
let dirs = [root];
|
|
837
|
+
for (const segment of segments) {
|
|
838
|
+
dirs = segment === "*" ? dirs.flatMap((dir) => childDirs(dir)) : segment === "**" ? dirs.flatMap((dir) => descendants(dir, 3)) : dirs.map((dir) => join5(dir, segment)).filter((dir) => existsSync8(dir));
|
|
665
839
|
}
|
|
840
|
+
return dirs;
|
|
666
841
|
};
|
|
667
|
-
var
|
|
668
|
-
var
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
);
|
|
683
|
-
}
|
|
684
|
-
const key = keyOf(entry);
|
|
685
|
-
if (tier !== void 0 && outsideTier(entry, tier)) {
|
|
686
|
-
proofs.push({ key, tier, verdict: "skipped" });
|
|
842
|
+
var QUOTED = /^['"]|['"]$/g;
|
|
843
|
+
var cleaned = (value) => value.replace(/#.*$/, "").trim().replaceAll(QUOTED, "");
|
|
844
|
+
var pnpmPatterns = (path) => {
|
|
845
|
+
const lines = readFileSync7(path, "utf8").split("\n");
|
|
846
|
+
const at = lines.findIndex((line) => line.startsWith("packages:"));
|
|
847
|
+
if (at === -1) return [];
|
|
848
|
+
const inline = lines[at]?.slice("packages:".length).trim() ?? "";
|
|
849
|
+
if (inline.startsWith("[")) {
|
|
850
|
+
return inline.replace(/^\[|\]$/g, "").split(",").map(cleaned).filter((one) => one !== "");
|
|
851
|
+
}
|
|
852
|
+
const patterns = [];
|
|
853
|
+
for (const line of lines.slice(at + 1)) {
|
|
854
|
+
const item = /^\s*-\s*(.+)$/.exec(line);
|
|
855
|
+
if (item?.[1] !== void 0) {
|
|
856
|
+
patterns.push(cleaned(item[1]));
|
|
687
857
|
continue;
|
|
688
858
|
}
|
|
689
|
-
|
|
690
|
-
proofs.push(proof);
|
|
691
|
-
if (proof.verdict !== "proven" && proof.verdict !== "skipped") {
|
|
692
|
-
return { proofs, proven: false };
|
|
693
|
-
}
|
|
859
|
+
if (line.trim() !== "") break;
|
|
694
860
|
}
|
|
695
|
-
return
|
|
861
|
+
return patterns;
|
|
696
862
|
};
|
|
697
|
-
var
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
if (one.verdict === "cannot-fail") return ` CANNOT FAIL ${one.key}: read 0`;
|
|
702
|
-
if (one.verdict === "misread") {
|
|
703
|
-
return ` MISREAD ${one.key}: read ${one.reading} where its probe planted ${one.expected}`;
|
|
704
|
-
}
|
|
705
|
-
return ` PROVEN ${one.key}: reads ${one.reading} on a planted finding`;
|
|
706
|
-
});
|
|
707
|
-
lines.push(
|
|
708
|
-
"",
|
|
709
|
-
proven ? "prove PASS \u2014 every counter read the finding its probe planted." : "prove FAIL \u2014 a gate that has never been seen red has not been shown to measure."
|
|
710
|
-
);
|
|
711
|
-
return `${lines.join("\n")}
|
|
712
|
-
`;
|
|
863
|
+
var npmPatterns = (path) => {
|
|
864
|
+
const parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
865
|
+
const declared = Array.isArray(parsed.workspaces) ? parsed.workspaces : parsed.workspaces?.packages ?? [];
|
|
866
|
+
return declared.filter((one) => typeof one === "string");
|
|
713
867
|
};
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
run: (command) => {
|
|
724
|
-
const result = run(command);
|
|
725
|
-
output = result.output;
|
|
726
|
-
return result;
|
|
727
|
-
}
|
|
728
|
-
};
|
|
868
|
+
var nameOf = (dir) => {
|
|
869
|
+
const manifest = join5(dir, "package.json");
|
|
870
|
+
if (!existsSync8(manifest)) return void 0;
|
|
871
|
+
try {
|
|
872
|
+
const { name } = JSON.parse(readFileSync7(manifest, "utf8"));
|
|
873
|
+
return typeof name === "string" && name !== "" ? name : void 0;
|
|
874
|
+
} catch {
|
|
875
|
+
return void 0;
|
|
876
|
+
}
|
|
729
877
|
};
|
|
730
|
-
var
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
878
|
+
var workspacePackageNames = (cwd) => {
|
|
879
|
+
const root = resolve9(cwd);
|
|
880
|
+
const pnpm = join5(root, "pnpm-workspace.yaml");
|
|
881
|
+
const manifest = join5(root, "package.json");
|
|
882
|
+
const patterns = existsSync8(pnpm) ? pnpmPatterns(pnpm) : existsSync8(manifest) ? npmPatterns(manifest) : [];
|
|
883
|
+
const names = patterns.filter((pattern) => !pattern.startsWith("!")).flatMap((pattern) => expand(root, pattern)).map((dir) => nameOf(dir)).filter((name) => name !== void 0);
|
|
884
|
+
return [...new Set(names)];
|
|
734
885
|
};
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
`${config.baseline} has no number for "${key}" \u2014 add it before enabling the counter`
|
|
886
|
+
|
|
887
|
+
// src/counters/typecheck.ts
|
|
888
|
+
var UNRESOLVED = /error TS(?:2305|2307): ([^\n]*)/g;
|
|
889
|
+
var SPECIFIER = /'([^']+)'/g;
|
|
890
|
+
var specifiersIn = (message) => [...message.matchAll(SPECIFIER)].map(([, quoted]) => (quoted ?? "").replaceAll('"', ""));
|
|
891
|
+
var unbuiltIn = (cwd, output) => {
|
|
892
|
+
const messages = [...output.matchAll(UNRESOLVED)].map(([, message]) => message ?? "");
|
|
893
|
+
if (messages.length === 0) return [];
|
|
894
|
+
const packages = workspacePackageNames(cwd);
|
|
895
|
+
const named = messages.flatMap((message) => specifiersIn(message)).flatMap(
|
|
896
|
+
(specifier) => packages.filter((name) => specifier === name || specifier.startsWith(`${name}/`))
|
|
897
|
+
);
|
|
898
|
+
return [...new Set(named)].toSorted();
|
|
899
|
+
};
|
|
900
|
+
var typecheckErrors = {
|
|
901
|
+
id: "typecheckErrors",
|
|
902
|
+
probe: {
|
|
903
|
+
command: () => "tsc --noEmit a.ts",
|
|
904
|
+
expect: 1,
|
|
905
|
+
input: (dir) => plant(dir, "a.ts", "export const n: number = 'not a number'\n")
|
|
906
|
+
},
|
|
907
|
+
run: async ({ cwd, params, run }) => {
|
|
908
|
+
const command = stringParam("typecheckErrors", params, "command", "npx tsc --noEmit");
|
|
909
|
+
const output = run(command).output;
|
|
910
|
+
const unbuilt = unbuiltIn(cwd, output);
|
|
911
|
+
if (unbuilt.length > 0) {
|
|
912
|
+
throw new CounterError(
|
|
913
|
+
"typecheckErrors",
|
|
914
|
+
`workspace packages not built: ${unbuilt.join(", ")} \u2014 build them before measuring typecheckErrors`
|
|
765
915
|
);
|
|
766
916
|
}
|
|
767
|
-
|
|
768
|
-
const now = await counter.run({ cwd, key, params: entry, run: recorder.run });
|
|
769
|
-
const verdict = verdictOf(now, limit);
|
|
770
|
-
measurements.push({
|
|
771
|
-
baseline: limit,
|
|
772
|
-
evidence: verdict === "grew" ? recorder.last() : [],
|
|
773
|
-
key,
|
|
774
|
-
now,
|
|
775
|
-
verdict
|
|
776
|
-
});
|
|
917
|
+
return countMatches(output, /error TS/);
|
|
777
918
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
// src/counters/index.ts
|
|
922
|
+
var COUNTERS = [
|
|
923
|
+
archViolations,
|
|
924
|
+
boundaryIssues,
|
|
925
|
+
cloneCount,
|
|
926
|
+
knipIssues,
|
|
927
|
+
lawLineCount,
|
|
928
|
+
oxlintErrors,
|
|
929
|
+
oxlintRule,
|
|
930
|
+
oxlintWarnings,
|
|
931
|
+
runtimeCodeShipped,
|
|
932
|
+
sumOfCounts,
|
|
933
|
+
testFailures,
|
|
934
|
+
typecheckErrors,
|
|
935
|
+
unformattedFiles
|
|
936
|
+
];
|
|
937
|
+
var counterById = (id) => {
|
|
938
|
+
const counter = COUNTERS.find((one) => one.id === id);
|
|
939
|
+
if (counter === void 0) {
|
|
940
|
+
throw new Error(
|
|
941
|
+
`no counter implements "${id}" \u2014 known ids: ${COUNTERS.map((one) => one.id).join(", ")}`
|
|
942
|
+
);
|
|
788
943
|
}
|
|
789
|
-
return
|
|
944
|
+
return counter;
|
|
790
945
|
};
|
|
946
|
+
|
|
947
|
+
// src/report.ts
|
|
791
948
|
var WIDTH = 28;
|
|
792
949
|
var formatReport = ({ measurements, rewritten }) => {
|
|
793
950
|
const lines = measurements.map((one) => {
|
|
@@ -816,19 +973,40 @@ var formatReport = ({ measurements, rewritten }) => {
|
|
|
816
973
|
return `${lines.join("\n")}
|
|
817
974
|
`;
|
|
818
975
|
};
|
|
976
|
+
var formatProve = ({ proofs, proven }) => {
|
|
977
|
+
const lines = proofs.map((one) => {
|
|
978
|
+
if (one.verdict === "skipped") return ` SKIP ${one.key}: not measured by --tier ${one.tier}`;
|
|
979
|
+
if (one.verdict === "serialised") return ` PROVEN ${one.key}: ${one.reason}`;
|
|
980
|
+
if (one.verdict === "interleaved") {
|
|
981
|
+
return ` CANNOT FAIL ${one.key}: interleaved \u2014 ${one.reason}`;
|
|
982
|
+
}
|
|
983
|
+
if (one.verdict === "cannot-measure") return ` CANNOT MEASURE ${one.key}: ${one.reason}`;
|
|
984
|
+
if (one.verdict === "cannot-fail") return ` CANNOT FAIL ${one.key}: read 0`;
|
|
985
|
+
if (one.verdict === "misread") {
|
|
986
|
+
return ` MISREAD ${one.key}: read ${one.reading} where its probe planted ${one.expected}`;
|
|
987
|
+
}
|
|
988
|
+
return ` PROVEN ${one.key}: reads ${one.reading} on a planted finding`;
|
|
989
|
+
});
|
|
990
|
+
lines.push(
|
|
991
|
+
"",
|
|
992
|
+
proven ? "prove PASS \u2014 every counter read the finding its probe planted." : "prove FAIL \u2014 a gate that has never been seen red has not been shown to measure."
|
|
993
|
+
);
|
|
994
|
+
return `${lines.join("\n")}
|
|
995
|
+
`;
|
|
996
|
+
};
|
|
819
997
|
|
|
820
998
|
export {
|
|
821
|
-
CounterError,
|
|
822
|
-
COUNTERS,
|
|
823
|
-
counterById,
|
|
824
999
|
heavyLockPath,
|
|
825
1000
|
acquireExclusive,
|
|
826
1001
|
CONFIG_FILE,
|
|
827
1002
|
keyOf,
|
|
828
1003
|
loadConfig,
|
|
1004
|
+
CounterError,
|
|
829
1005
|
runCommand,
|
|
830
1006
|
runProve,
|
|
831
|
-
formatProve,
|
|
832
1007
|
runRatchet,
|
|
833
|
-
|
|
1008
|
+
COUNTERS,
|
|
1009
|
+
counterById,
|
|
1010
|
+
formatReport,
|
|
1011
|
+
formatProve
|
|
834
1012
|
};
|
package/dist/cli.js
CHANGED
|
@@ -5,9 +5,13 @@ import {
|
|
|
5
5
|
formatReport,
|
|
6
6
|
runProve,
|
|
7
7
|
runRatchet
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-FM5I6PRT.js";
|
|
9
9
|
|
|
10
10
|
// src/cli.ts
|
|
11
|
+
var numberAfter = (flag) => {
|
|
12
|
+
const at = process.argv.indexOf(flag);
|
|
13
|
+
return at === -1 ? void 0 : Number(process.argv[at + 1] ?? Number.NaN);
|
|
14
|
+
};
|
|
11
15
|
var cwdFlag = process.argv.indexOf("--cwd");
|
|
12
16
|
var cwd = cwdFlag === -1 ? process.cwd() : process.argv[cwdFlag + 1] ?? process.cwd();
|
|
13
17
|
var tierFlag = process.argv.indexOf("--tier");
|
|
@@ -16,9 +20,24 @@ var proving = process.argv.includes("--prove");
|
|
|
16
20
|
var exclusive = process.argv.includes("--exclusive");
|
|
17
21
|
var timeoutFlag = process.argv.indexOf("--exclusive-timeout");
|
|
18
22
|
var timeoutSeconds = timeoutFlag === -1 ? void 0 : Number(process.argv[timeoutFlag + 1] ?? Number.NaN);
|
|
23
|
+
var holdMs = numberAfter("--hold");
|
|
24
|
+
var holdTheLock = async (ms) => {
|
|
25
|
+
process.stdout.write(`exclusive-hold start ${Date.now()}
|
|
26
|
+
`);
|
|
27
|
+
await new Promise((done) => setTimeout(done, ms));
|
|
28
|
+
process.stdout.write(`exclusive-hold end ${Date.now()}
|
|
29
|
+
`);
|
|
30
|
+
return 0;
|
|
31
|
+
};
|
|
19
32
|
var measure = async () => {
|
|
33
|
+
if (holdMs !== void 0) return holdTheLock(holdMs);
|
|
20
34
|
if (proving) {
|
|
21
|
-
const proof = await runProve({
|
|
35
|
+
const proof = await runProve({
|
|
36
|
+
counters: COUNTERS,
|
|
37
|
+
cwd,
|
|
38
|
+
exclusiveVia: process.argv[1],
|
|
39
|
+
tier
|
|
40
|
+
});
|
|
22
41
|
process.stdout.write(formatProve(proof));
|
|
23
42
|
return proof.proven ? 0 : 2;
|
|
24
43
|
}
|
|
@@ -33,6 +52,9 @@ try {
|
|
|
33
52
|
if (timeoutSeconds !== void 0 && !(timeoutSeconds > 0)) {
|
|
34
53
|
throw new Error("--exclusive-timeout needs a number of seconds");
|
|
35
54
|
}
|
|
55
|
+
if (holdMs !== void 0 && !(holdMs > 0)) {
|
|
56
|
+
throw new Error("--hold needs a number of milliseconds");
|
|
57
|
+
}
|
|
36
58
|
const release = exclusive ? await acquireExclusive({ timeoutSeconds }) : () => void 0;
|
|
37
59
|
const giveBack = () => {
|
|
38
60
|
release();
|
package/dist/index.js
CHANGED