@geonosis/ratchet 0.4.0 → 1.0.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/dist/cli.js CHANGED
@@ -5,9 +5,13 @@ import {
5
5
  formatReport,
6
6
  runProve,
7
7
  runRatchet
8
- } from "./chunk-QVORWCUD.js";
8
+ } from "./chunk-LSYVFUP4.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({ counters: COUNTERS, cwd, tier });
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();
@@ -0,0 +1,247 @@
1
+ /** What a command did. `code` is the exit status; `output` is stdout and stderr, merged. */
2
+ type CommandResult = {
3
+ code: number;
4
+ output: string;
5
+ };
6
+ /**
7
+ * A tool that reports findings exits non-zero — that is the tool working, not failing. A tool that
8
+ * could not be spawned at all, or whose output no longer parses, is a gate that cannot measure, and
9
+ * a gate that cannot measure has not passed. Counters throw this rather than returning zero.
10
+ */
11
+ declare class CounterError extends Error {
12
+ readonly counter: string;
13
+ constructor(counter: string, message: string);
14
+ }
15
+ type CounterContext = {
16
+ /** The repo root every command runs in and every relative path resolves against. */
17
+ cwd: string;
18
+ /** The baseline key this instance writes — several instances of one counter can coexist. */
19
+ key: string;
20
+ /** Whatever the config gave this instance. Each counter validates its own. */
21
+ params: Record<string, unknown>;
22
+ run: (command: string) => CommandResult;
23
+ };
24
+ /**
25
+ * A counter's falsification probe: the known-bad input it must be able to read.
26
+ *
27
+ * `input` plants it in a scratch directory, `command` is the command shape to run THERE (absent for
28
+ * a counter that reads a file instead), `params` is whatever else that counter needs, and `expect`
29
+ * is the reading the planted input is worth — at least 1, because a probe that plants nothing
30
+ * proves nothing.
31
+ */
32
+ type CounterProbe = {
33
+ command?: (dir: string) => string;
34
+ expect: number;
35
+ input: (dir: string) => void;
36
+ /**
37
+ * What this probe proves, when the counter has more than one way of reading. It is printed beside
38
+ * the key, so a report says WHICH mode was seen red rather than that the counter was.
39
+ */
40
+ name?: string;
41
+ params?: Record<string, unknown>;
42
+ };
43
+ type Counter = {
44
+ id: string;
45
+ /**
46
+ * Whether this counter's number is a measured QUANTITY — bytes, milliseconds — rather than a
47
+ * count of findings, and may therefore carry a `tolerance`.
48
+ *
49
+ * Opt-in, and declared by the counter rather than by the config, because the config is the wrong
50
+ * place to decide it: `"tolerance": 100` on a findings counter switches that gate off and the run
51
+ * still prints PASS. One more lint error is always one too many; forty more bytes is a dependency
52
+ * patch nobody chose. Only the counter knows which of those its number is.
53
+ */
54
+ tolerates?: true;
55
+ /**
56
+ * Absent only for a counter written outside this package. A gate that has never been seen red has
57
+ * not been shown to measure, so `--prove` refuses a configured counter that ships no probe.
58
+ *
59
+ * A counter that reads its tool in more than one way ships one probe per way: proving the mode
60
+ * nobody configured says nothing about the mode they did.
61
+ */
62
+ probe?: CounterProbe | CounterProbe[];
63
+ run: (context: CounterContext) => Promise<number>;
64
+ };
65
+ /** One line of `geonosis.ratchet.json`'s `counters` array. */
66
+ type CounterConfig = {
67
+ counter: string;
68
+ /** The baseline key. Defaults to `counter`; required when a counter is used more than once. */
69
+ key?: string;
70
+ /**
71
+ * The tiers this counter belongs to — free-form names a repo chooses, `fast` and `full` by
72
+ * convention. Absent means every tier, so a repo that never asks for one is unaffected.
73
+ */
74
+ tiers?: string[];
75
+ } & Record<string, unknown>;
76
+ type RatchetConfig = {
77
+ baseline: string;
78
+ counters: CounterConfig[];
79
+ };
80
+ type Verdict = 'grew' | 'held' | 'shrank' | 'skipped';
81
+ /**
82
+ * A counter that ran carries its number; a counter the tier left out carries none — deliberately,
83
+ * so nothing downstream can mistake a census nobody took for one that came back equal.
84
+ */
85
+ type Measurement = {
86
+ baseline: number;
87
+ /**
88
+ * The tail of what this counter's command printed, kept only when the number GREW. A report
89
+ * that says a number went up and nothing else sends the reader back to re-run the tool; the
90
+ * lines that made it go up are the answer they were going to look for.
91
+ */
92
+ evidence: string[];
93
+ key: string;
94
+ now: number;
95
+ verdict: 'grew' | 'held' | 'shrank';
96
+ } | {
97
+ key: string;
98
+ tier: string;
99
+ verdict: 'skipped';
100
+ };
101
+ /**
102
+ * What one counter's probe did. A reading of 0 is the finding this exists to catch: the counter ran,
103
+ * the input was planted, and the number came back the same as it does on a clean tree.
104
+ */
105
+ type Proof = {
106
+ counter: string;
107
+ expected: number;
108
+ key: string;
109
+ reading: number;
110
+ verdict: 'misread';
111
+ } | {
112
+ counter: string;
113
+ key: string;
114
+ reading: number;
115
+ verdict: 'cannot-fail' | 'proven';
116
+ } | {
117
+ counter: string;
118
+ key: string;
119
+ reason: string;
120
+ verdict: 'cannot-measure';
121
+ }
122
+ /**
123
+ * Not a counter: `--exclusive` itself, measured by running two of it. A lock is a claim about the
124
+ * machine, and the only place that claim can be checked is the machine.
125
+ */
126
+ | {
127
+ key: string;
128
+ reason: string;
129
+ verdict: 'interleaved';
130
+ } | {
131
+ key: string;
132
+ reason: string;
133
+ verdict: 'serialised';
134
+ } | {
135
+ key: string;
136
+ tier: string;
137
+ verdict: 'skipped';
138
+ };
139
+ type ProveResult = {
140
+ proofs: Proof[];
141
+ /** False as soon as one counter could not be shown to read its own planted finding. */
142
+ proven: boolean;
143
+ };
144
+ type RatchetResult = {
145
+ measurements: Measurement[];
146
+ /** True when the baseline file was rewritten because something shrank. */
147
+ rewritten: boolean;
148
+ };
149
+
150
+ declare const CONFIG_FILE = "geonosis.ratchet.json";
151
+ /** The baseline key an entry writes. Defaults to the counter's own id. */
152
+ declare const keyOf: (entry: CounterConfig) => string;
153
+ declare const loadConfig: (cwd: string) => RatchetConfig;
154
+
155
+ declare const COUNTERS: Counter[];
156
+ declare const counterById: (id: string) => Counter;
157
+
158
+ /** Who is running the heavy thing, since when, and where from. */
159
+ type Holder = {
160
+ cwd: string;
161
+ pid: number;
162
+ startedAt: string;
163
+ };
164
+ /**
165
+ * One path for the whole machine, because the contention is for the machine: three sessions on one
166
+ * laptop, each starting a verify, each slowing the other two down until a 25-minute run took two
167
+ * hours and produced failures that were about the load and not the code.
168
+ */
169
+ declare const heavyLockPath: () => string;
170
+ /**
171
+ * Takes the machine-wide lock, waiting for whoever has it, and gives back the release.
172
+ *
173
+ * A holder whose process is gone — killed, crashed, a laptop closed mid-run — is stale and is taken
174
+ * over with a printed note; so is a lock file nobody can parse, because a lock that can never be
175
+ * cleared is worse than no lock. The wait is bounded and says who it is waiting for: a run that
176
+ * hangs silently for half an hour is indistinguishable from a run that is broken.
177
+ */
178
+ declare const acquireExclusive: ({ noticeMs, path, pollMs, say, timeoutSeconds, }?: {
179
+ noticeMs?: number;
180
+ path?: string;
181
+ pollMs?: number;
182
+ say?: (line: string) => void;
183
+ timeoutSeconds?: number;
184
+ }) => Promise<() => void>;
185
+
186
+ /**
187
+ * D-029: every gate ships its own falsification probe. The ratchet compares a number against a
188
+ * number and cannot tell a counter that found nothing from a counter that CAN find nothing — the
189
+ * kit's own `oxlintErrors` read 0 under the format its config asked for, from inception, and every
190
+ * baseline it wrote was a census nobody took.
191
+ *
192
+ * So each counter plants a known-bad input and must read it. The run stops at the first counter that
193
+ * reads 0 or throws: the rest of the report would be about a gate whose first gate is not a gate.
194
+ */
195
+ declare const runProve: ({ counters, cwd, exclusiveVia, tier, }: {
196
+ counters: Counter[];
197
+ cwd: string;
198
+ /**
199
+ * The CLI to run two of, to see whether `--exclusive` serialises on this machine. Absent means
200
+ * the caller is a library and there is no process to run twice, so the lock is not proved.
201
+ */
202
+ exclusiveVia?: string;
203
+ tier?: string;
204
+ }) => Promise<ProveResult>;
205
+
206
+ /**
207
+ * A binary gate has two bad modes: ignore the debt, or block every commit until it is zero. Neither
208
+ * works mid-migration. This measures the debt, compares it against the baseline, and fails ONLY on
209
+ * regression — so a repo that predates a rule can adopt it the same day, and no NEW debt can land
210
+ * behind it.
211
+ *
212
+ * A `tier` runs only the counters that declare it; the rest are reported as skipped and take no
213
+ * part in the verdict, the exit code or a baseline rewrite. An iteration gate that took eleven
214
+ * seconds gets run less often than one that takes four, and a gate nobody runs enforces nothing —
215
+ * but a skipped counter that printed OK would be worse than not running at all.
216
+ */
217
+ declare const runRatchet: ({ counters, cwd, tier, }: {
218
+ counters: Counter[];
219
+ cwd: string;
220
+ tier?: string;
221
+ }) => Promise<RatchetResult>;
222
+
223
+ /**
224
+ * One line per counter, then the verdict. Reads the same in a terminal and in a CI log. A counter
225
+ * the tier left out says so by name — printing a number for it would be the stale OK this exists
226
+ * to prevent.
227
+ */
228
+ declare const formatReport: ({ measurements, rewritten }: RatchetResult) => string;
229
+ declare const formatProve: ({ proofs, proven }: ProveResult) => string;
230
+
231
+ /**
232
+ * Runs a command and merges stderr into stdout — most of these tools write their summary line to
233
+ * stderr, and on a zero exit `execSync` would otherwise hand back only stdout and the parse would
234
+ * fail. A non-zero exit is NOT an error here: a linter that found something exits non-zero, and
235
+ * that is the whole reason we are asking. Counters decide what an unparseable answer means.
236
+ *
237
+ * The command runs inside a SUBSHELL, because `<command> 2>&1` binds the redirect to the last
238
+ * command of the string only. `echo boom >&2; echo fine` merged nothing, and on a zero exit
239
+ * execSync hands back stdout alone — so a tool that prints its findings to stderr and exits 0 read
240
+ * as a tool that found nothing. That is the exact shape of a gate that has quietly stopped gating.
241
+ * On a non-zero exit the two buffers were concatenated instead, which put every stderr line after
242
+ * every stdout line and moved each diagnostic away from the line it was about. The shell keeps
243
+ * them in the order they were written. The newlines let a command end in a comment.
244
+ */
245
+ declare const runCommand: (cwd: string, counterId: string, env?: NodeJS.ProcessEnv) => (command: string) => CommandResult;
246
+
247
+ export { CONFIG_FILE, COUNTERS, type CommandResult, type Counter, type CounterConfig, type CounterContext, CounterError, type CounterProbe, type Holder, type Measurement, type Proof, type ProveResult, type RatchetConfig, type RatchetResult, type Verdict, acquireExclusive, counterById, formatProve, formatReport, heavyLockPath, keyOf, loadConfig, runCommand, runProve, runRatchet };
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  runCommand,
13
13
  runProve,
14
14
  runRatchet
15
- } from "./chunk-QVORWCUD.js";
15
+ } from "./chunk-LSYVFUP4.js";
16
16
  export {
17
17
  CONFIG_FILE,
18
18
  COUNTERS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/ratchet",
3
- "version": "0.4.0",
3
+ "version": "1.0.0",
4
4
  "description": "Debt as a number that may only shrink — one ratchet, pluggable counters.",
5
5
  "keywords": [
6
6
  "ratchet",