@geonosis/ratchet 0.1.2 → 0.2.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/README.md +158 -2
- package/dist/{chunk-BTWIR7DK.js → chunk-XYTUTPQI.js} +356 -19
- package/dist/cli.js +35 -4
- package/dist/index.js +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,13 +11,104 @@ pnpm add -D @geonosis/ratchet
|
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
geonosis-ratchet
|
|
14
|
+
geonosis-ratchet --prove # first: prove every counter CAN report a finding
|
|
15
|
+
geonosis-ratchet # then: every counter
|
|
15
16
|
geonosis-ratchet --tier fast # only the counters in that tier
|
|
17
|
+
geonosis-ratchet --exclusive # one heavy run at a time on this machine
|
|
16
18
|
geonosis-ratchet --cwd apps/web
|
|
17
19
|
```
|
|
18
20
|
|
|
19
21
|
Exit 1 when a number grew, 2 when a counter could not measure at all, 0 otherwise.
|
|
20
22
|
|
|
23
|
+
## `--prove` is the first thing to run after installing
|
|
24
|
+
|
|
25
|
+
A counter that reads 0 because the tree is clean and a counter that reads 0 because it can no longer
|
|
26
|
+
read the tool are the same number. The second is worse than no gate at all: it reports green, and
|
|
27
|
+
when it shrinks the ratchet writes the lie into the baseline.
|
|
28
|
+
|
|
29
|
+
This is not hypothetical. On one day, 2026-08-29, five gates were found reporting green while
|
|
30
|
+
measuring nothing: this package's own `oxlintErrors` read 0 under the `--format=unix` its config
|
|
31
|
+
asked for, and had since it was written; a lint rule sat at `"error"` for months with no
|
|
32
|
+
configuration to fire on; a parity run said PASS about rules it never exercised; a test runner
|
|
33
|
+
exited 0 over failing suites for weeks; and a `git rebase | tail` hid a conflict behind a pipe's
|
|
34
|
+
exit status.
|
|
35
|
+
|
|
36
|
+
So every counter ships a **probe**: a known-bad input it must be able to read.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
geonosis-ratchet --prove
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
PROVEN oxlintErrors: reads 1 on a planted finding
|
|
44
|
+
PROVEN typecheckErrors: reads 1 on a planted finding
|
|
45
|
+
PROVEN lawLineCount: reads 3 on a planted finding
|
|
46
|
+
|
|
47
|
+
prove PASS — every counter read the finding its probe planted.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Each configured counter's probe plants its input in a temp directory — never in your repo — and the
|
|
51
|
+
counter runs there exactly as it runs in a real measurement. The tools come from your repo's
|
|
52
|
+
`node_modules/.bin`, so what is proven is the toolchain you will actually measure with.
|
|
53
|
+
|
|
54
|
+
- `PROVEN <key>: reads N on a planted finding` — the gate has been seen red.
|
|
55
|
+
- `CANNOT FAIL <key>: read 0` — the input was planted and the counter still read nothing.
|
|
56
|
+
- `CANNOT MEASURE <key>: <error>` — the probe threw, or the counter ships none.
|
|
57
|
+
|
|
58
|
+
The run stops at the first counter that reads 0 or throws, and exits **2**. Put it in front of the
|
|
59
|
+
ratchet in whatever script your CI runs:
|
|
60
|
+
|
|
61
|
+
```jsonc
|
|
62
|
+
{ "verify": "… && geonosis-ratchet --prove && geonosis-ratchet" }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A counter you wrote yourself takes a `probe` beside its `run`: `{ input: (dir) => void, command?:
|
|
66
|
+
(dir) => string, params?, expect: number }`. `expect` is what the planted input is worth, and it is
|
|
67
|
+
at least 1 — a probe that plants nothing proves nothing.
|
|
68
|
+
|
|
69
|
+
### A baseline is not proof the gate still gates
|
|
70
|
+
|
|
71
|
+
The ratchet compares a number against a number. Swapping the tool that produces it — a vendored lint
|
|
72
|
+
plugin for a published one, one version for the next — can hold every number and still have stopped
|
|
73
|
+
firing, because a rule that fires nowhere counts zero exactly like a rule with nothing to find. Check
|
|
74
|
+
that separately, with [`@geonosis/lint-parity`](https://www.npmjs.com/package/@geonosis/lint-parity):
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx geonosis-lint-parity \
|
|
78
|
+
--corpus node_modules/@geonosis/oxlint-plugin-biological-architecture/corpus \
|
|
79
|
+
--a oxlintrc.old.json --b oxlintrc.new.json --out proofs/reach
|
|
80
|
+
npx geonosis-lint-parity --a oxlintrc.old.json --b oxlintrc.new.json --out proofs/parity -- apps packages
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Reach first — parity between two configs that both fire nothing is perfect parity — then the tree.
|
|
84
|
+
|
|
85
|
+
## `--exclusive`: one heavy run at a time on a machine
|
|
86
|
+
|
|
87
|
+
Three sessions on one laptop each started their own verify, and a 25-minute run took two hours and
|
|
88
|
+
came back with failures that were about the load, not the code. "Announce before anything heavy" is
|
|
89
|
+
a habit that works right up until the moment somebody is concentrating; `--exclusive` is the same
|
|
90
|
+
agreement as code.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
geonosis-ratchet --exclusive --exclusive-timeout 1800
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
It takes `~/.cache/geonosis/heavy.lock` before measuring anything, writing the pid, cwd and start
|
|
97
|
+
time into it, and gives it back in a `finally` and on SIGINT/SIGTERM. A second run waits instead of
|
|
98
|
+
contending, and says every 15 seconds who has the lock and for how long. A holder whose process is
|
|
99
|
+
gone — killed, crashed, a lid closed mid-run — is stale, and is taken over with a printed note
|
|
100
|
+
rather than making everyone wait for a pid that no longer exists. The wait is bounded:
|
|
101
|
+
`--exclusive-timeout` seconds (default 1800) and then exit 2, because a run that hangs silently
|
|
102
|
+
looks exactly like a run that is broken.
|
|
103
|
+
|
|
104
|
+
**Put it on the script your sessions actually run**, so nothing has to remember:
|
|
105
|
+
|
|
106
|
+
```jsonc
|
|
107
|
+
{ "verify": "… && geonosis-ratchet --prove && geonosis-ratchet --exclusive" }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`GEONOSIS_HEAVY_LOCK` moves the lock file — for a sandbox with no writable home, or for a test.
|
|
111
|
+
|
|
21
112
|
## Config
|
|
22
113
|
|
|
23
114
|
`geonosis.ratchet.json` at the repo root names the counters and how to run them:
|
|
@@ -85,6 +176,36 @@ When a number **grew**, the last ten lines of that counter's command output prin
|
|
|
85
176
|
`<-- REGRESSED` line, indented — so the report says what grew, not only that something did. A
|
|
86
177
|
counter that reads a file rather than running a command (`lawLineCount`) prints nothing extra.
|
|
87
178
|
|
|
179
|
+
### `testFailures` reads the runner's summary, never the exit code
|
|
180
|
+
|
|
181
|
+
The counter looks for the runner's own count — `N failed` — and **refuses when it cannot parse
|
|
182
|
+
one**. It never reads the process exit code, because a test runner exiting 0 over a red suite is
|
|
183
|
+
commoner than anyone expects: `@cloudflare/vitest-pool-workers` 0.22 on vitest 4.1 exited 0 with
|
|
184
|
+
failing tests on every workerd suite of a consumer, and every gate that trusted the exit code
|
|
185
|
+
reported green over red for weeks. A refusal is loud and stops the run; a trusted 0 is silent and
|
|
186
|
+
banks the red as a win.
|
|
187
|
+
|
|
188
|
+
For the same reason, **give every test package its own `testFailures` entry**, each with its own
|
|
189
|
+
`key`. One entry over one workspace measures one workspace; the suites it does not run are not zero
|
|
190
|
+
failures, they are unmeasured — and unmeasured reads exactly like green.
|
|
191
|
+
|
|
192
|
+
```jsonc
|
|
193
|
+
{ "counter": "testFailures", "key": "testFailuresApi", "command": "bun --cwd apps/api test", "tiers": ["full"] }
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`examples/during-day.ratchet.json` is four test entries for that reason: `apps/web` plus the three
|
|
197
|
+
workerd packages that were previously outside every gate.
|
|
198
|
+
|
|
199
|
+
### `oxlintRule` counts a warned rule twice, on purpose
|
|
200
|
+
|
|
201
|
+
A rule parked at `"warn"` as ratcheted debt appears in two numbers: once inside `oxlintWarnings`,
|
|
202
|
+
and once under its own `oxlintRule` key, which measures it at `"error"` through a strict temp copy of
|
|
203
|
+
the config. So arming a rule at warn raises **both**, and fixing one finding lowers **both**.
|
|
204
|
+
|
|
205
|
+
That is not double-counting the total; the second key exists so the debt is visible per rule instead
|
|
206
|
+
of hidden inside a lump sum that a different rule's warning could mask. dielime today: `oxlintWarnings`
|
|
207
|
+
12 → 135 when `no-raw-html-atoms` was armed, with its own key at 123.
|
|
208
|
+
|
|
88
209
|
## Running it where it will actually run
|
|
89
210
|
|
|
90
211
|
- **Counters run in the caller's environment.** They inherit the shell the ratchet was started in,
|
|
@@ -95,11 +216,46 @@ counter that reads a file rather than running a command (`lawLineCount`) prints
|
|
|
95
216
|
packages first.** In a fresh worktree the `dist/*.d.ts` files do not exist yet, and `tsc` reports
|
|
96
217
|
a false +N of missing-module errors that has nothing to do with the change under test. `pnpm build
|
|
97
218
|
&& geonosis-ratchet` is the shape; a bare `geonosis-ratchet` in a clean checkout is not.
|
|
219
|
+
|
|
220
|
+
The counter no longer reports that number. When tsc's TS2307 or TS2305 diagnostics name a package
|
|
221
|
+
of this workspace — read from `pnpm-workspace.yaml`, or `workspaces` in the root `package.json` —
|
|
222
|
+
it refuses:
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
geonosis-ratchet: typecheckErrors: workspace packages not built: @dielime/shared —
|
|
226
|
+
build them before measuring typecheckErrors
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Mixed output refuses too. Counting the readable half would produce a number that is neither the
|
|
230
|
+
debt nor the build error, banked into the baseline as though somebody had measured it. An
|
|
231
|
+
unresolved module that is NOT a workspace package — `lodash` — is counted as before: that is a
|
|
232
|
+
dependency to fix, not a build to run.
|
|
98
233
|
- **A release-age cooldown will refuse a package published minutes ago.** If pnpm's
|
|
99
234
|
`minimumReleaseAge` blocks `@geonosis/*`, exclude the scope for that project — pnpm writes
|
|
100
235
|
`minimumReleaseAgeExclude` into `pnpm-workspace.yaml` — rather than lowering the cooldown. The
|
|
101
236
|
cooldown is protecting every other dependency in the tree; the exclusion is scoped to the one you
|
|
102
237
|
chose to trust.
|
|
238
|
+
- **Bumping to a new version: keep BOTH exclusion pairs — old and new — through the install, then
|
|
239
|
+
drop the old one and re-verify.** The policy check is applied to the entries in the LOCKFILE, not
|
|
240
|
+
only to what you asked for, so removing the old pair first makes the resolver walk back through
|
|
241
|
+
the version you are leaving and refuse it — an install that fails for the version you are
|
|
242
|
+
replacing, not the one you are taking. The order that works, learned under load on dielime's 0.2.0
|
|
243
|
+
bump:
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
# both pairs in pnpm-workspace.yaml's minimumReleaseAgeExclude
|
|
247
|
+
pnpm install
|
|
248
|
+
grep -c "@geonosis/.*@<old>" pnpm-lock.yaml # must be 0 before the old pair goes
|
|
249
|
+
# now delete the old pair
|
|
250
|
+
pnpm install --frozen-lockfile
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
An exclusion left behind is a cooldown quietly off for a package nobody is watching any more, so
|
|
254
|
+
the deletion belongs in the same commit as the bump.
|
|
255
|
+
|
|
256
|
+
- **That policy check walks every entry in the lockfile and looks hung. It is not.** ~2,300 entries
|
|
257
|
+
took ~90 seconds on a loaded laptop with no output while it worked. Wait for it rather than
|
|
258
|
+
killing the install and re-running into the same wait.
|
|
103
259
|
|
|
104
260
|
## Counters
|
|
105
261
|
|
|
@@ -109,7 +265,7 @@ Every counter takes its `command` from the config, so the toolchain stays the re
|
|
|
109
265
|
| --- | --- | --- |
|
|
110
266
|
| `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` |
|
|
111
267
|
| `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` |
|
|
112
|
-
| `typecheckErrors` | `error TS` occurrences | `command` |
|
|
268
|
+
| `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` |
|
|
113
269
|
| `testFailures` | the runner's own failure summary; throws when neither a pass nor a fail count is readable | `command` |
|
|
114
270
|
| `unformattedFiles` | paths `--list-different` names that exist on disk | `command` |
|
|
115
271
|
| `cloneCount` | jscpd's `Found N clones` | `command` |
|
|
@@ -23,9 +23,23 @@ var stringsParam = (params, name, fallback) => {
|
|
|
23
23
|
var countMatches = (text, pattern) => text.match(new RegExp(pattern.source, `${pattern.flags.replace("g", "")}g`))?.length ?? 0;
|
|
24
24
|
var escapeForRegex = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
25
25
|
|
|
26
|
+
// src/counters/plant.ts
|
|
27
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
28
|
+
import { dirname, join } from "path";
|
|
29
|
+
var plant = (dir, relative, contents) => {
|
|
30
|
+
const path = join(dir, relative);
|
|
31
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
32
|
+
writeFileSync(path, contents);
|
|
33
|
+
};
|
|
34
|
+
var captured = (sample) => ({
|
|
35
|
+
command: () => "cat sample.txt",
|
|
36
|
+
input: (dir) => plant(dir, "sample.txt", sample)
|
|
37
|
+
});
|
|
38
|
+
|
|
26
39
|
// src/counters/arch.ts
|
|
27
40
|
var archViolations = {
|
|
28
41
|
id: "archViolations",
|
|
42
|
+
probe: { ...captured("\u2717 cell imports cell\nok\n"), expect: 1 },
|
|
29
43
|
run: async ({ params, run }) => {
|
|
30
44
|
const command = stringParam("archViolations", params, "command");
|
|
31
45
|
const match = new RegExp(stringParam("archViolations", params, "match", "^\u2717"));
|
|
@@ -37,6 +51,7 @@ var archViolations = {
|
|
|
37
51
|
var CLONES = /Found (\d+) clones?/;
|
|
38
52
|
var cloneCount = {
|
|
39
53
|
id: "cloneCount",
|
|
54
|
+
probe: { ...captured("Found 1 clone.\n"), expect: 1 },
|
|
40
55
|
run: async ({ params, run }) => {
|
|
41
56
|
const command = stringParam("cloneCount", params, "command", "npx jscpd .");
|
|
42
57
|
const output = run(command).output;
|
|
@@ -60,6 +75,7 @@ var DEFAULT_HEADINGS = [
|
|
|
60
75
|
];
|
|
61
76
|
var knipIssues = {
|
|
62
77
|
id: "knipIssues",
|
|
78
|
+
probe: { ...captured("Unused files (1)\nsrc/gone.ts\n"), expect: 1 },
|
|
63
79
|
run: async ({ params, run }) => {
|
|
64
80
|
const command = stringParam("knipIssues", params, "command", "npx knip --reporter compact");
|
|
65
81
|
const output = run(command).output;
|
|
@@ -69,6 +85,7 @@ var knipIssues = {
|
|
|
69
85
|
var ISSUES = /(\d+) issues? found/;
|
|
70
86
|
var boundaryIssues = {
|
|
71
87
|
id: "boundaryIssues",
|
|
88
|
+
probe: { ...captured("Checking...\n1 issue found\n"), expect: 1 },
|
|
72
89
|
run: async ({ params, run }) => {
|
|
73
90
|
const command = stringParam("boundaryIssues", params, "command", "npx turbo boundaries");
|
|
74
91
|
return Number(run(command).output.match(ISSUES)?.[1] ?? 0);
|
|
@@ -80,6 +97,11 @@ import { existsSync } from "fs";
|
|
|
80
97
|
import { resolve } from "path";
|
|
81
98
|
var unformattedFiles = {
|
|
82
99
|
id: "unformattedFiles",
|
|
100
|
+
probe: {
|
|
101
|
+
command: () => "oxfmt --list-different .",
|
|
102
|
+
expect: 1,
|
|
103
|
+
input: (dir) => plant(dir, "a.ts", "export const a = {b:1,\n c:2}\n")
|
|
104
|
+
},
|
|
83
105
|
run: async ({ cwd, params, run }) => {
|
|
84
106
|
const command = stringParam(
|
|
85
107
|
"unformattedFiles",
|
|
@@ -96,6 +118,11 @@ import { existsSync as existsSync2, readFileSync } from "fs";
|
|
|
96
118
|
import { resolve as resolve2 } from "path";
|
|
97
119
|
var lawLineCount = {
|
|
98
120
|
id: "lawLineCount",
|
|
121
|
+
probe: {
|
|
122
|
+
expect: 3,
|
|
123
|
+
input: (dir) => plant(dir, "CLAUDE.md", "one\ntwo\nthree\n"),
|
|
124
|
+
params: { path: "CLAUDE.md" }
|
|
125
|
+
},
|
|
99
126
|
run: async ({ cwd, params }) => {
|
|
100
127
|
const relative = stringParam("lawLineCount", params, "path", "CLAUDE.md");
|
|
101
128
|
const path = resolve2(cwd, relative);
|
|
@@ -105,9 +132,23 @@ var lawLineCount = {
|
|
|
105
132
|
};
|
|
106
133
|
|
|
107
134
|
// src/counters/oxlint.ts
|
|
108
|
-
import { existsSync as existsSync3, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
|
|
135
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
109
136
|
import { resolve as resolve3 } from "path";
|
|
110
137
|
var DEFAULT_COMMAND = "npx oxlint --format=unix --config .oxlintrc.json .";
|
|
138
|
+
var PROBE_COMMAND = "oxlint --format=unix --config .oxlintrc.json .";
|
|
139
|
+
var oxlintProbe = (severity, rule, source) => ({
|
|
140
|
+
command: () => PROBE_COMMAND,
|
|
141
|
+
input: (dir) => {
|
|
142
|
+
plant(
|
|
143
|
+
dir,
|
|
144
|
+
".oxlintrc.json",
|
|
145
|
+
JSON.stringify({ plugins: ["typescript"], rules: { [rule]: severity } })
|
|
146
|
+
);
|
|
147
|
+
plant(dir, "src/a.ts", source);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
var ONE_ANY = "export const cast = (value: any): string => String(value)\n";
|
|
151
|
+
var ONE_LOOSE = "export const loose = (a: unknown, b: unknown): boolean => a == b\n";
|
|
111
152
|
var UNIX_FINDING = /^\S[^\n]*:\d+:\d+: .*\[(Error|Warning)\/[^\]\n]+\]$/gm;
|
|
112
153
|
var UNIX_SUMMARY = /^(\d+) problems?$/m;
|
|
113
154
|
var AGENT_FINDING = /^\S[^\n]*:\d+:\d+: (error|warning) /gm;
|
|
@@ -177,6 +218,11 @@ ${output.trim()}`);
|
|
|
177
218
|
};
|
|
178
219
|
var oxlintErrors = {
|
|
179
220
|
id: "oxlintErrors",
|
|
221
|
+
probe: {
|
|
222
|
+
...oxlintProbe("error", "typescript/no-explicit-any", ONE_ANY),
|
|
223
|
+
expect: 1,
|
|
224
|
+
params: { expectFormat: "unix" }
|
|
225
|
+
},
|
|
180
226
|
run: async ({ params, run }) => {
|
|
181
227
|
const command = stringParam("oxlintErrors", params, "command", DEFAULT_COMMAND);
|
|
182
228
|
const expect = expectedFormat("oxlintErrors", params);
|
|
@@ -185,6 +231,13 @@ var oxlintErrors = {
|
|
|
185
231
|
};
|
|
186
232
|
var oxlintWarnings = {
|
|
187
233
|
id: "oxlintWarnings",
|
|
234
|
+
// A warning, so the run exits 0 — the window `expectFormat` exists to close is also the window a
|
|
235
|
+
// probe has to survive.
|
|
236
|
+
probe: {
|
|
237
|
+
...oxlintProbe("warn", "eqeqeq", ONE_LOOSE),
|
|
238
|
+
expect: 1,
|
|
239
|
+
params: { expectFormat: "unix" }
|
|
240
|
+
},
|
|
188
241
|
run: async ({ params, run }) => {
|
|
189
242
|
const command = stringParam("oxlintWarnings", params, "command", DEFAULT_COMMAND);
|
|
190
243
|
const expect = expectedFormat("oxlintWarnings", params);
|
|
@@ -206,6 +259,13 @@ ${result.output.trim()}`
|
|
|
206
259
|
};
|
|
207
260
|
var oxlintRule = {
|
|
208
261
|
id: "oxlintRule",
|
|
262
|
+
// The probe names its OWN rule: which rule a repo tracks is its business, and a probe that had to
|
|
263
|
+
// make the repo's rule fire would need the repo's plugin loadable from a scratch directory.
|
|
264
|
+
probe: {
|
|
265
|
+
...oxlintProbe("error", "typescript/no-explicit-any", ONE_ANY),
|
|
266
|
+
expect: 1,
|
|
267
|
+
params: { expectFormat: "unix", rule: "no-explicit-any" }
|
|
268
|
+
},
|
|
209
269
|
run: async ({ cwd, key, params, run }) => {
|
|
210
270
|
const rule = stringParam("oxlintRule", params, "rule");
|
|
211
271
|
const command = stringParam("oxlintRule", params, "command", DEFAULT_COMMAND);
|
|
@@ -219,7 +279,7 @@ var oxlintRule = {
|
|
|
219
279
|
new RegExp(`("[^"]*${escapeForRegex(rule)}"\\s*:\\s*\\[?\\s*)"(warn|off)"`),
|
|
220
280
|
'$1"error"'
|
|
221
281
|
);
|
|
222
|
-
|
|
282
|
+
writeFileSync2(resolve3(cwd, strictName), strict);
|
|
223
283
|
try {
|
|
224
284
|
return countRule(run(command.replace("{config}", strictName)), rule, expect);
|
|
225
285
|
} finally {
|
|
@@ -233,6 +293,9 @@ var DEFAULT_PATTERNS = ["^(apps|packages)/[^/]+/src/"];
|
|
|
233
293
|
var NOT_RUNTIME = /(\.test\.|\.spec\.|__tests__\/|__fixtures__\/|\.d\.ts$)/;
|
|
234
294
|
var runtimeCodeShipped = {
|
|
235
295
|
id: "runtimeCodeShipped",
|
|
296
|
+
// Its failing reading is 1, not 0: the finding to plant is a diff of documentation with no
|
|
297
|
+
// runtime file in it, which is exactly the tick this counter exists to refuse.
|
|
298
|
+
probe: { ...captured("docs/design.md\nplans/019.md\n"), expect: 1 },
|
|
236
299
|
run: async ({ params, run }) => {
|
|
237
300
|
const command = stringParam(
|
|
238
301
|
"runtimeCodeShipped",
|
|
@@ -251,10 +314,11 @@ var runtimeCodeShipped = {
|
|
|
251
314
|
// src/counters/sum-of-counts.ts
|
|
252
315
|
var sumOfCounts = {
|
|
253
316
|
id: "sumOfCounts",
|
|
317
|
+
probe: { ...captured("src/a.ts:1\nsrc/b.ts:0\n"), expect: 1 },
|
|
254
318
|
run: async ({ params, run }) => {
|
|
255
319
|
const command = stringParam("sumOfCounts", params, "command");
|
|
256
320
|
const match = new RegExp(stringParam("sumOfCounts", params, "match", ":(\\d+)$"), "gm");
|
|
257
|
-
return [...run(command).output.matchAll(match)].map(([,
|
|
321
|
+
return [...run(command).output.matchAll(match)].map(([, digits]) => Number(digits ?? 0)).filter((count) => Number.isFinite(count)).reduce((sum, count) => sum + count, 0);
|
|
258
322
|
}
|
|
259
323
|
};
|
|
260
324
|
|
|
@@ -263,6 +327,7 @@ var FAILED = /(\d+)\s+fail(?:ed|ing|s)?\b/;
|
|
|
263
327
|
var PASSED = /(\d+)\s+pass(?:ed|ing|es)?\b/;
|
|
264
328
|
var testFailures = {
|
|
265
329
|
id: "testFailures",
|
|
330
|
+
probe: { ...captured(" Tests 1 failed | 0 passed (1)\n"), expect: 1 },
|
|
266
331
|
run: async ({ params, run }) => {
|
|
267
332
|
const command = stringParam("testFailures", params, "command", "npx vitest run");
|
|
268
333
|
const output = run(command).output;
|
|
@@ -277,12 +342,102 @@ ${output.trim().slice(-500)}`
|
|
|
277
342
|
}
|
|
278
343
|
};
|
|
279
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
|
+
|
|
280
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
|
+
);
|
|
421
|
+
return [...new Set(named)].toSorted();
|
|
422
|
+
};
|
|
281
423
|
var typecheckErrors = {
|
|
282
424
|
id: "typecheckErrors",
|
|
283
|
-
|
|
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 }) => {
|
|
284
431
|
const command = stringParam("typecheckErrors", params, "command", "npx tsc --noEmit");
|
|
285
|
-
|
|
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/);
|
|
286
441
|
}
|
|
287
442
|
};
|
|
288
443
|
|
|
@@ -312,17 +467,104 @@ var counterById = (id) => {
|
|
|
312
467
|
return counter;
|
|
313
468
|
};
|
|
314
469
|
|
|
470
|
+
// src/lock.ts
|
|
471
|
+
import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync4, rmSync as rmSync2, writeSync } from "fs";
|
|
472
|
+
import { homedir } from "os";
|
|
473
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
474
|
+
var heavyLockPath = () => process.env.GEONOSIS_HEAVY_LOCK ?? join3(homedir(), ".cache", "geonosis", "heavy.lock");
|
|
475
|
+
var sleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
476
|
+
var holderOf = (path) => {
|
|
477
|
+
try {
|
|
478
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
479
|
+
return typeof parsed.pid === "number" ? {
|
|
480
|
+
cwd: parsed.cwd ?? "somewhere",
|
|
481
|
+
pid: parsed.pid,
|
|
482
|
+
startedAt: parsed.startedAt ?? "unknown"
|
|
483
|
+
} : void 0;
|
|
484
|
+
} catch {
|
|
485
|
+
return void 0;
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
var alive = (pid) => {
|
|
489
|
+
try {
|
|
490
|
+
process.kill(pid, 0);
|
|
491
|
+
return true;
|
|
492
|
+
} catch (error) {
|
|
493
|
+
return error.code === "EPERM";
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
var heldFor = (holder) => {
|
|
497
|
+
const since = Date.parse(holder.startedAt);
|
|
498
|
+
if (Number.isNaN(since)) return "an unknown time";
|
|
499
|
+
return `${Math.round((Date.now() - since) / 1e3)}s`;
|
|
500
|
+
};
|
|
501
|
+
var write = (path) => {
|
|
502
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
503
|
+
try {
|
|
504
|
+
const handle = openSync(path, "wx");
|
|
505
|
+
const mine = {
|
|
506
|
+
cwd: process.cwd(),
|
|
507
|
+
pid: process.pid,
|
|
508
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
509
|
+
};
|
|
510
|
+
writeSync(handle, JSON.stringify(mine));
|
|
511
|
+
closeSync(handle);
|
|
512
|
+
return true;
|
|
513
|
+
} catch (error) {
|
|
514
|
+
if (error.code === "EEXIST") return false;
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
var acquireExclusive = async ({
|
|
519
|
+
noticeMs = 15e3,
|
|
520
|
+
path = heavyLockPath(),
|
|
521
|
+
pollMs = 250,
|
|
522
|
+
say = (line) => process.stderr.write(`${line}
|
|
523
|
+
`),
|
|
524
|
+
timeoutSeconds = 1800
|
|
525
|
+
} = {}) => {
|
|
526
|
+
const until = Date.now() + timeoutSeconds * 1e3;
|
|
527
|
+
let told = 0;
|
|
528
|
+
const release = () => {
|
|
529
|
+
if (holderOf(path)?.pid === process.pid) rmSync2(path, { force: true });
|
|
530
|
+
};
|
|
531
|
+
for (; ; ) {
|
|
532
|
+
if (write(path)) return release;
|
|
533
|
+
const holder = holderOf(path);
|
|
534
|
+
if (holder === void 0 || !alive(holder.pid)) {
|
|
535
|
+
say(
|
|
536
|
+
`geonosis-ratchet: taking over a stale heavy lock (pid ${holder?.pid ?? "unreadable"} is gone)`
|
|
537
|
+
);
|
|
538
|
+
rmSync2(path, { force: true });
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (Date.now() >= until) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
`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`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
const now = Date.now();
|
|
547
|
+
if (now - told >= noticeMs) {
|
|
548
|
+
told = now;
|
|
549
|
+
say(
|
|
550
|
+
`geonosis-ratchet: waiting for the heavy lock \u2014 pid ${holder.pid} in ${holder.cwd}, held for ${heldFor(holder)}`
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
await sleep(pollMs);
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
|
|
315
557
|
// src/config.ts
|
|
316
|
-
import { existsSync as
|
|
317
|
-
import { resolve as
|
|
558
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
559
|
+
import { resolve as resolve5 } from "path";
|
|
318
560
|
var CONFIG_FILE = "geonosis.ratchet.json";
|
|
319
561
|
var keyOf = (entry) => entry.key ?? entry.counter;
|
|
320
562
|
var loadConfig = (cwd) => {
|
|
321
|
-
const path =
|
|
322
|
-
if (!
|
|
563
|
+
const path = resolve5(cwd, CONFIG_FILE);
|
|
564
|
+
if (!existsSync5(path)) {
|
|
323
565
|
throw new Error(`no ${CONFIG_FILE} in ${cwd} \u2014 the ratchet has nothing to count`);
|
|
324
566
|
}
|
|
325
|
-
const parsed = JSON.parse(
|
|
567
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
326
568
|
if (!Array.isArray(parsed.counters)) {
|
|
327
569
|
throw new Error(`${CONFIG_FILE} has no "counters" array`);
|
|
328
570
|
}
|
|
@@ -352,11 +594,12 @@ var loadConfig = (cwd) => {
|
|
|
352
594
|
// src/shell.ts
|
|
353
595
|
import { execSync } from "child_process";
|
|
354
596
|
var ANSI = /\[[0-9;]*m/g;
|
|
355
|
-
var runCommand = (cwd, counterId) => (command) => {
|
|
597
|
+
var runCommand = (cwd, counterId, env) => (command) => {
|
|
356
598
|
try {
|
|
357
599
|
const output = execSync(`${command} 2>&1`, {
|
|
358
600
|
cwd,
|
|
359
601
|
encoding: "utf8",
|
|
602
|
+
env,
|
|
360
603
|
maxBuffer: 64 * 1024 * 1024,
|
|
361
604
|
stdio: ["ignore", "pipe", "pipe"]
|
|
362
605
|
});
|
|
@@ -376,9 +619,99 @@ ${output.trim()}`
|
|
|
376
619
|
}
|
|
377
620
|
};
|
|
378
621
|
|
|
622
|
+
// src/prove.ts
|
|
623
|
+
import { existsSync as existsSync6, mkdtempSync, rmSync as rmSync3 } from "fs";
|
|
624
|
+
import { tmpdir } from "os";
|
|
625
|
+
import { delimiter, dirname as dirname3, join as join4, resolve as resolve6 } from "path";
|
|
626
|
+
var toolPath = (cwd) => {
|
|
627
|
+
const dirs = [];
|
|
628
|
+
let dir = resolve6(cwd);
|
|
629
|
+
for (; ; ) {
|
|
630
|
+
const bin = join4(dir, "node_modules", ".bin");
|
|
631
|
+
if (existsSync6(bin)) dirs.push(bin);
|
|
632
|
+
const parent = dirname3(dir);
|
|
633
|
+
if (parent === dir) break;
|
|
634
|
+
dir = parent;
|
|
635
|
+
}
|
|
636
|
+
return [...dirs, process.env.PATH ?? ""].join(delimiter);
|
|
637
|
+
};
|
|
638
|
+
var NO_PROBE = "no probe \u2014 a counter nobody has seen read a planted finding has not been shown to measure";
|
|
639
|
+
var proofOf = async (counter, key, path) => {
|
|
640
|
+
const probe = counter.probe;
|
|
641
|
+
if (probe === void 0)
|
|
642
|
+
return { counter: counter.id, key, reason: NO_PROBE, verdict: "cannot-measure" };
|
|
643
|
+
const dir = mkdtempSync(join4(tmpdir(), "geonosis-prove-"));
|
|
644
|
+
try {
|
|
645
|
+
probe.input(dir);
|
|
646
|
+
const command = probe.command?.(dir);
|
|
647
|
+
const reading = await counter.run({
|
|
648
|
+
cwd: dir,
|
|
649
|
+
key,
|
|
650
|
+
params: { ...probe.params, ...command === void 0 ? {} : { command } },
|
|
651
|
+
run: runCommand(dir, counter.id, { ...process.env, PATH: path })
|
|
652
|
+
});
|
|
653
|
+
if (reading === 0) return { counter: counter.id, key, reading, verdict: "cannot-fail" };
|
|
654
|
+
if (reading < probe.expect) {
|
|
655
|
+
return { counter: counter.id, expected: probe.expect, key, reading, verdict: "misread" };
|
|
656
|
+
}
|
|
657
|
+
return { counter: counter.id, key, reading, verdict: "proven" };
|
|
658
|
+
} catch (error) {
|
|
659
|
+
return { counter: counter.id, key, reason: error.message, verdict: "cannot-measure" };
|
|
660
|
+
} finally {
|
|
661
|
+
rmSync3(dir, { force: true, recursive: true });
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
var outsideTier = (entry, tier) => entry.tiers !== void 0 && !entry.tiers.includes(tier);
|
|
665
|
+
var runProve = async ({
|
|
666
|
+
counters,
|
|
667
|
+
cwd,
|
|
668
|
+
tier
|
|
669
|
+
}) => {
|
|
670
|
+
const config = loadConfig(cwd);
|
|
671
|
+
const byId = new Map(counters.map((one) => [one.id, one]));
|
|
672
|
+
const path = toolPath(cwd);
|
|
673
|
+
const proofs = [];
|
|
674
|
+
for (const entry of config.counters) {
|
|
675
|
+
const counter = byId.get(entry.counter);
|
|
676
|
+
if (counter === void 0) {
|
|
677
|
+
throw new Error(
|
|
678
|
+
`no counter implements "${entry.counter}" \u2014 known ids: ${[...byId.keys()].toSorted().join(", ")}`
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
const key = keyOf(entry);
|
|
682
|
+
if (tier !== void 0 && outsideTier(entry, tier)) {
|
|
683
|
+
proofs.push({ key, tier, verdict: "skipped" });
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
const proof = await proofOf(counter, key, path);
|
|
687
|
+
proofs.push(proof);
|
|
688
|
+
if (proof.verdict !== "proven" && proof.verdict !== "skipped") {
|
|
689
|
+
return { proofs, proven: false };
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return { proofs, proven: true };
|
|
693
|
+
};
|
|
694
|
+
var formatProve = ({ proofs, proven }) => {
|
|
695
|
+
const lines = proofs.map((one) => {
|
|
696
|
+
if (one.verdict === "skipped") return ` SKIP ${one.key}: not measured by --tier ${one.tier}`;
|
|
697
|
+
if (one.verdict === "cannot-measure") return ` CANNOT MEASURE ${one.key}: ${one.reason}`;
|
|
698
|
+
if (one.verdict === "cannot-fail") return ` CANNOT FAIL ${one.key}: read 0`;
|
|
699
|
+
if (one.verdict === "misread") {
|
|
700
|
+
return ` MISREAD ${one.key}: read ${one.reading} where its probe planted ${one.expected}`;
|
|
701
|
+
}
|
|
702
|
+
return ` PROVEN ${one.key}: reads ${one.reading} on a planted finding`;
|
|
703
|
+
});
|
|
704
|
+
lines.push(
|
|
705
|
+
"",
|
|
706
|
+
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."
|
|
707
|
+
);
|
|
708
|
+
return `${lines.join("\n")}
|
|
709
|
+
`;
|
|
710
|
+
};
|
|
711
|
+
|
|
379
712
|
// src/ratchet.ts
|
|
380
|
-
import { existsSync as
|
|
381
|
-
import { resolve as
|
|
713
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
714
|
+
import { resolve as resolve7 } from "path";
|
|
382
715
|
var EVIDENCE_LINES = 10;
|
|
383
716
|
var recorded = (run) => {
|
|
384
717
|
let output = "";
|
|
@@ -396,18 +729,18 @@ var verdictOf = (now, baseline) => {
|
|
|
396
729
|
if (now < baseline) return "shrank";
|
|
397
730
|
return "held";
|
|
398
731
|
};
|
|
399
|
-
var
|
|
732
|
+
var outsideTier2 = (entry, tier) => entry.tiers !== void 0 && !entry.tiers.includes(tier);
|
|
400
733
|
var runRatchet = async ({
|
|
401
734
|
counters,
|
|
402
735
|
cwd,
|
|
403
736
|
tier
|
|
404
737
|
}) => {
|
|
405
738
|
const config = loadConfig(cwd);
|
|
406
|
-
const baselinePath =
|
|
407
|
-
if (!
|
|
739
|
+
const baselinePath = resolve7(cwd, config.baseline);
|
|
740
|
+
if (!existsSync7(baselinePath)) {
|
|
408
741
|
throw new Error(`no ${config.baseline} in ${cwd} \u2014 nothing to ratchet against`);
|
|
409
742
|
}
|
|
410
|
-
const baseline = JSON.parse(
|
|
743
|
+
const baseline = JSON.parse(readFileSync6(baselinePath, "utf8"));
|
|
411
744
|
const byId = new Map(counters.map((one) => [one.id, one]));
|
|
412
745
|
const measurements = [];
|
|
413
746
|
for (const entry of config.counters) {
|
|
@@ -418,7 +751,7 @@ var runRatchet = async ({
|
|
|
418
751
|
);
|
|
419
752
|
}
|
|
420
753
|
const key = keyOf(entry);
|
|
421
|
-
if (tier !== void 0 &&
|
|
754
|
+
if (tier !== void 0 && outsideTier2(entry, tier)) {
|
|
422
755
|
measurements.push({ key, tier, verdict: "skipped" });
|
|
423
756
|
continue;
|
|
424
757
|
}
|
|
@@ -446,7 +779,7 @@ var runRatchet = async ({
|
|
|
446
779
|
for (const one of measurements) {
|
|
447
780
|
if (one.verdict !== "skipped") next[one.key] = one.now;
|
|
448
781
|
}
|
|
449
|
-
|
|
782
|
+
writeFileSync3(baselinePath, `${JSON.stringify(next, null, 2)}
|
|
450
783
|
`);
|
|
451
784
|
return { measurements, rewritten: true };
|
|
452
785
|
}
|
|
@@ -485,10 +818,14 @@ export {
|
|
|
485
818
|
CounterError,
|
|
486
819
|
COUNTERS,
|
|
487
820
|
counterById,
|
|
821
|
+
heavyLockPath,
|
|
822
|
+
acquireExclusive,
|
|
488
823
|
CONFIG_FILE,
|
|
489
824
|
keyOf,
|
|
490
825
|
loadConfig,
|
|
491
826
|
runCommand,
|
|
827
|
+
runProve,
|
|
828
|
+
formatProve,
|
|
492
829
|
runRatchet,
|
|
493
830
|
formatReport
|
|
494
831
|
};
|
package/dist/cli.js
CHANGED
|
@@ -1,21 +1,52 @@
|
|
|
1
1
|
import {
|
|
2
2
|
COUNTERS,
|
|
3
|
+
acquireExclusive,
|
|
4
|
+
formatProve,
|
|
3
5
|
formatReport,
|
|
6
|
+
runProve,
|
|
4
7
|
runRatchet
|
|
5
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-XYTUTPQI.js";
|
|
6
9
|
|
|
7
10
|
// src/cli.ts
|
|
8
11
|
var cwdFlag = process.argv.indexOf("--cwd");
|
|
9
12
|
var cwd = cwdFlag === -1 ? process.cwd() : process.argv[cwdFlag + 1] ?? process.cwd();
|
|
10
13
|
var tierFlag = process.argv.indexOf("--tier");
|
|
11
14
|
var tier = tierFlag === -1 ? void 0 : process.argv[tierFlag + 1];
|
|
15
|
+
var proving = process.argv.includes("--prove");
|
|
16
|
+
var exclusive = process.argv.includes("--exclusive");
|
|
17
|
+
var timeoutFlag = process.argv.indexOf("--exclusive-timeout");
|
|
18
|
+
var timeoutSeconds = timeoutFlag === -1 ? void 0 : Number(process.argv[timeoutFlag + 1] ?? Number.NaN);
|
|
19
|
+
var measure = async () => {
|
|
20
|
+
if (proving) {
|
|
21
|
+
const proof = await runProve({ counters: COUNTERS, cwd, tier });
|
|
22
|
+
process.stdout.write(formatProve(proof));
|
|
23
|
+
return proof.proven ? 0 : 2;
|
|
24
|
+
}
|
|
25
|
+
const result = await runRatchet({ counters: COUNTERS, cwd, tier });
|
|
26
|
+
process.stdout.write(formatReport(result));
|
|
27
|
+
return result.measurements.some((one) => one.verdict === "grew") ? 1 : 0;
|
|
28
|
+
};
|
|
12
29
|
try {
|
|
13
30
|
if (tierFlag !== -1 && (tier === void 0 || tier.startsWith("--"))) {
|
|
14
31
|
throw new Error("--tier needs a tier name");
|
|
15
32
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
33
|
+
if (timeoutSeconds !== void 0 && !(timeoutSeconds > 0)) {
|
|
34
|
+
throw new Error("--exclusive-timeout needs a number of seconds");
|
|
35
|
+
}
|
|
36
|
+
const release = exclusive ? await acquireExclusive({ timeoutSeconds }) : () => void 0;
|
|
37
|
+
const giveBack = () => {
|
|
38
|
+
release();
|
|
39
|
+
process.exit(130);
|
|
40
|
+
};
|
|
41
|
+
process.on("SIGINT", giveBack);
|
|
42
|
+
process.on("SIGTERM", giveBack);
|
|
43
|
+
let code = 2;
|
|
44
|
+
try {
|
|
45
|
+
code = await measure();
|
|
46
|
+
} finally {
|
|
47
|
+
release();
|
|
48
|
+
}
|
|
49
|
+
process.exit(code);
|
|
19
50
|
} catch (error) {
|
|
20
51
|
process.stderr.write(`geonosis-ratchet: ${error.message}
|
|
21
52
|
`);
|
package/dist/index.js
CHANGED
|
@@ -2,21 +2,29 @@ import {
|
|
|
2
2
|
CONFIG_FILE,
|
|
3
3
|
COUNTERS,
|
|
4
4
|
CounterError,
|
|
5
|
+
acquireExclusive,
|
|
5
6
|
counterById,
|
|
7
|
+
formatProve,
|
|
6
8
|
formatReport,
|
|
9
|
+
heavyLockPath,
|
|
7
10
|
keyOf,
|
|
8
11
|
loadConfig,
|
|
9
12
|
runCommand,
|
|
13
|
+
runProve,
|
|
10
14
|
runRatchet
|
|
11
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-XYTUTPQI.js";
|
|
12
16
|
export {
|
|
13
17
|
CONFIG_FILE,
|
|
14
18
|
COUNTERS,
|
|
15
19
|
CounterError,
|
|
20
|
+
acquireExclusive,
|
|
16
21
|
counterById,
|
|
22
|
+
formatProve,
|
|
17
23
|
formatReport,
|
|
24
|
+
heavyLockPath,
|
|
18
25
|
keyOf,
|
|
19
26
|
loadConfig,
|
|
20
27
|
runCommand,
|
|
28
|
+
runProve,
|
|
21
29
|
runRatchet
|
|
22
30
|
};
|