@geonosis/ratchet 0.1.0 → 0.1.2
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 +48 -3
- package/dist/{chunk-VX2PL7QG.js → chunk-BTWIR7DK.js} +112 -13
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -54,7 +54,52 @@ code, and its baseline survives a rewrite another counter earned: printing OK fo
|
|
|
54
54
|
took is how a red main goes unnoticed for three commits.
|
|
55
55
|
|
|
56
56
|
A counter whose command cannot run is a hard error, never a silent zero. A gate that cannot measure
|
|
57
|
-
has not passed.
|
|
57
|
+
has not passed. Nor is a number believed on its own: `oxlintErrors` reads the finding lines AND the
|
|
58
|
+
summary oxlint printed, and raises when they disagree.
|
|
59
|
+
|
|
60
|
+
### The warnings-only window, and `expectFormat`
|
|
61
|
+
|
|
62
|
+
The last-resort backstop under the oxlint counters — output in no shape we know *and* a non-zero
|
|
63
|
+
exit means the format moved, so refuse — has one gap, and it is worth naming rather than pretending
|
|
64
|
+
otherwise. **oxlint exits 0 when a run found only warnings.** On such a run the backstop cannot
|
|
65
|
+
fire: an unrecognised format reads as zero warnings, and a zero that only shrinks is a zero the
|
|
66
|
+
ratchet banks by rewriting the baseline down.
|
|
67
|
+
|
|
68
|
+
`--deny-warnings` is **not** the fix. It makes warnings exit non-zero by making them errors, which
|
|
69
|
+
moves every warning into `oxlintErrors` and changes what both numbers mean — a bigger lie than the
|
|
70
|
+
one it patches, told to the whole baseline.
|
|
71
|
+
|
|
72
|
+
The fix is to say which shape you asked for:
|
|
73
|
+
|
|
74
|
+
```jsonc
|
|
75
|
+
{ "counter": "oxlintWarnings", "command": "npx oxlint --format=unix .", "expectFormat": "unix" }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
With `expectFormat` set, output that matches none of the three shapes and is not empty once npm's
|
|
79
|
+
own chatter is stripped (`> …` script echoes, `npm warn …`, `npm notice …` update nags, blank
|
|
80
|
+
lines) is refused **even on a clean exit**. `npm ERR!` is not stripped — that is npm saying the
|
|
81
|
+
command never ran, and it must never read as zero. Unset is the default and changes nothing, so a repo that never opts in keeps exactly
|
|
82
|
+
the behaviour it had. Valid values: `unix`, `agent`, `default`.
|
|
83
|
+
|
|
84
|
+
When a number **grew**, the last ten lines of that counter's command output print under the
|
|
85
|
+
`<-- REGRESSED` line, indented — so the report says what grew, not only that something did. A
|
|
86
|
+
counter that reads a file rather than running a command (`lawLineCount`) prints nothing extra.
|
|
87
|
+
|
|
88
|
+
## Running it where it will actually run
|
|
89
|
+
|
|
90
|
+
- **Counters run in the caller's environment.** They inherit the shell the ratchet was started in,
|
|
91
|
+
nothing more. If a child command needs `NODE_OPTIONS` — a TypeScript shim, a loader — put it on
|
|
92
|
+
the script that invokes `geonosis-ratchet`, not on the counter's own line, and not only in your
|
|
93
|
+
interactive shell.
|
|
94
|
+
- **A `typecheckErrors` counter needs the same precondition CI gives it: build the workspace
|
|
95
|
+
packages first.** In a fresh worktree the `dist/*.d.ts` files do not exist yet, and `tsc` reports
|
|
96
|
+
a false +N of missing-module errors that has nothing to do with the change under test. `pnpm build
|
|
97
|
+
&& geonosis-ratchet` is the shape; a bare `geonosis-ratchet` in a clean checkout is not.
|
|
98
|
+
- **A release-age cooldown will refuse a package published minutes ago.** If pnpm's
|
|
99
|
+
`minimumReleaseAge` blocks `@geonosis/*`, exclude the scope for that project — pnpm writes
|
|
100
|
+
`minimumReleaseAgeExclude` into `pnpm-workspace.yaml` — rather than lowering the cooldown. The
|
|
101
|
+
cooldown is protecting every other dependency in the tree; the exclusion is scoped to the one you
|
|
102
|
+
chose to trust.
|
|
58
103
|
|
|
59
104
|
## Counters
|
|
60
105
|
|
|
@@ -62,8 +107,8 @@ Every counter takes its `command` from the config, so the toolchain stays the re
|
|
|
62
107
|
|
|
63
108
|
| id | counts | params |
|
|
64
109
|
| --- | --- | --- |
|
|
65
|
-
| `oxlintErrors` / `oxlintWarnings` |
|
|
66
|
-
| `oxlintRule` | one named rule's findings; with `config`, after forcing
|
|
110
|
+
| `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
|
+
| `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` |
|
|
67
112
|
| `typecheckErrors` | `error TS` occurrences | `command` |
|
|
68
113
|
| `testFailures` | the runner's own failure summary; throws when neither a pass nor a fail count is readable | `command` |
|
|
69
114
|
| `unformattedFiles` | paths `--list-different` names that exist on disk | `command` |
|
|
@@ -108,19 +108,101 @@ var lawLineCount = {
|
|
|
108
108
|
import { existsSync as existsSync3, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
|
|
109
109
|
import { resolve as resolve3 } from "path";
|
|
110
110
|
var DEFAULT_COMMAND = "npx oxlint --format=unix --config .oxlintrc.json .";
|
|
111
|
+
var UNIX_FINDING = /^\S[^\n]*:\d+:\d+: .*\[(Error|Warning)\/[^\]\n]+\]$/gm;
|
|
112
|
+
var UNIX_SUMMARY = /^(\d+) problems?$/m;
|
|
113
|
+
var AGENT_FINDING = /^\S[^\n]*:\d+:\d+: (error|warning) /gm;
|
|
114
|
+
var DEFAULT_SUMMARY = /^Found (\d+) warnings? and (\d+) errors?\.$/m;
|
|
115
|
+
var DEFAULT_FINDING = /^\s*[x!] [^\s(]+\([^)\n]+\): /m;
|
|
116
|
+
var FINDING_SHAPES = [UNIX_FINDING, AGENT_FINDING, DEFAULT_FINDING].map(
|
|
117
|
+
(shape) => new RegExp(shape.source)
|
|
118
|
+
);
|
|
119
|
+
var RULE_TOKEN = /\([^()\n]+\)/;
|
|
120
|
+
var findingLinesOf = (output) => output.split("\n").filter((line) => FINDING_SHAPES.some((shape) => shape.test(line)));
|
|
121
|
+
var FORMATS = ["agent", "default", "unix"];
|
|
122
|
+
var isFormat = (value) => typeof value === "string" && FORMATS.some((one) => one === value);
|
|
123
|
+
var expectedFormat = (counter, params) => {
|
|
124
|
+
const value = params.expectFormat;
|
|
125
|
+
if (value === void 0) return void 0;
|
|
126
|
+
if (isFormat(value)) return value;
|
|
127
|
+
throw new CounterError(
|
|
128
|
+
counter,
|
|
129
|
+
`expectFormat must be one of ${FORMATS.join(", ")} \u2014 got ${JSON.stringify(value)}`
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
var NOISE = /^(> .*|npm warn .*|npm notice.*|\s*)$/;
|
|
133
|
+
var spokenByTheTool = (output) => output.split("\n").filter((line) => !NOISE.test(line)).join("\n");
|
|
134
|
+
var total = (reading) => reading.errors + reading.warnings;
|
|
135
|
+
var countFindings = (output, pattern, errorToken) => {
|
|
136
|
+
let errors = 0;
|
|
137
|
+
let warnings = 0;
|
|
138
|
+
for (const match of output.matchAll(new RegExp(pattern.source, pattern.flags))) {
|
|
139
|
+
if (match[1] === errorToken) errors += 1;
|
|
140
|
+
else warnings += 1;
|
|
141
|
+
}
|
|
142
|
+
return { errors, warnings };
|
|
143
|
+
};
|
|
144
|
+
var readFindings = (counter, { code, output }, expect) => {
|
|
145
|
+
const unix = countFindings(output, UNIX_FINDING, "Error");
|
|
146
|
+
const agent = countFindings(output, AGENT_FINDING, "error");
|
|
147
|
+
const problems = UNIX_SUMMARY.exec(output);
|
|
148
|
+
const found = DEFAULT_SUMMARY.exec(output);
|
|
149
|
+
const refuse = (why) => {
|
|
150
|
+
throw new CounterError(counter, `${why} \u2014 oxlint's output changed:
|
|
151
|
+
${output.trim()}`);
|
|
152
|
+
};
|
|
153
|
+
if (total(unix) > 0 || problems !== null) {
|
|
154
|
+
if (problems?.[1] === void 0) return refuse('unix findings with no "N problems" summary');
|
|
155
|
+
if (Number(problems[1]) !== total(unix)) {
|
|
156
|
+
return refuse(`the summary says ${problems[1]} problems, the lines say ${total(unix)}`);
|
|
157
|
+
}
|
|
158
|
+
return unix;
|
|
159
|
+
}
|
|
160
|
+
if (found?.[1] !== void 0 && found[2] !== void 0) {
|
|
161
|
+
const summary = { errors: Number(found[2]), warnings: Number(found[1]) };
|
|
162
|
+
if (total(agent) > 0 && (agent.errors !== summary.errors || agent.warnings !== summary.warnings)) {
|
|
163
|
+
return refuse(
|
|
164
|
+
`the summary says ${summary.errors} errors and ${summary.warnings} warnings, the lines say ${agent.errors} and ${agent.warnings}`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return summary;
|
|
168
|
+
}
|
|
169
|
+
if (total(agent) > 0) return agent;
|
|
170
|
+
if (code === 0) {
|
|
171
|
+
if (expect !== void 0 && spokenByTheTool(output) !== "") {
|
|
172
|
+
return refuse(`asked for --format=${expect} and got output in no shape this counter reads`);
|
|
173
|
+
}
|
|
174
|
+
return { errors: 0, warnings: 0 };
|
|
175
|
+
}
|
|
176
|
+
return refuse(`the tool exited ${code} and printed no findings and no summary`);
|
|
177
|
+
};
|
|
111
178
|
var oxlintErrors = {
|
|
112
179
|
id: "oxlintErrors",
|
|
113
180
|
run: async ({ params, run }) => {
|
|
114
181
|
const command = stringParam("oxlintErrors", params, "command", DEFAULT_COMMAND);
|
|
115
|
-
|
|
182
|
+
const expect = expectedFormat("oxlintErrors", params);
|
|
183
|
+
return readFindings("oxlintErrors", run(command), expect).errors;
|
|
116
184
|
}
|
|
117
185
|
};
|
|
118
186
|
var oxlintWarnings = {
|
|
119
187
|
id: "oxlintWarnings",
|
|
120
188
|
run: async ({ params, run }) => {
|
|
121
189
|
const command = stringParam("oxlintWarnings", params, "command", DEFAULT_COMMAND);
|
|
122
|
-
|
|
190
|
+
const expect = expectedFormat("oxlintWarnings", params);
|
|
191
|
+
return readFindings("oxlintWarnings", run(command), expect).warnings;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
var countRule = (result, rule, expect) => {
|
|
195
|
+
const reading = readFindings("oxlintRule", result, expect);
|
|
196
|
+
const lines = findingLinesOf(result.output);
|
|
197
|
+
if (total(reading) > 0 && !lines.some((line) => RULE_TOKEN.test(line))) {
|
|
198
|
+
throw new CounterError(
|
|
199
|
+
"oxlintRule",
|
|
200
|
+
`${total(reading)} findings, not one of them attributed to a "plugin(rule)" this counter can read \u2014 oxlint's output changed:
|
|
201
|
+
${result.output.trim()}`
|
|
202
|
+
);
|
|
123
203
|
}
|
|
204
|
+
const named = new RegExp(`\\(${escapeForRegex(rule)}\\)`);
|
|
205
|
+
return lines.filter((line) => named.test(line)).length;
|
|
124
206
|
};
|
|
125
207
|
var oxlintRule = {
|
|
126
208
|
id: "oxlintRule",
|
|
@@ -128,19 +210,18 @@ var oxlintRule = {
|
|
|
128
210
|
const rule = stringParam("oxlintRule", params, "rule");
|
|
129
211
|
const command = stringParam("oxlintRule", params, "command", DEFAULT_COMMAND);
|
|
130
212
|
const config = typeof params.config === "string" ? params.config : "";
|
|
131
|
-
|
|
132
|
-
|
|
213
|
+
const expect = expectedFormat("oxlintRule", params);
|
|
214
|
+
if (config === "") return countRule(run(command), rule, expect);
|
|
133
215
|
const source = resolve3(cwd, config);
|
|
134
216
|
if (!existsSync3(source)) throw new CounterError("oxlintRule", `no config at ${config}`);
|
|
135
217
|
const strictName = `.oxlintrc.ratchet-${key}.json`;
|
|
136
218
|
const strict = readFileSync2(source, "utf8").replace(
|
|
137
|
-
new RegExp(`("[^"]*${escapeForRegex(rule)}"\\s*:\\s*)"(warn|off)"`),
|
|
219
|
+
new RegExp(`("[^"]*${escapeForRegex(rule)}"\\s*:\\s*\\[?\\s*)"(warn|off)"`),
|
|
138
220
|
'$1"error"'
|
|
139
221
|
);
|
|
140
222
|
writeFileSync(resolve3(cwd, strictName), strict);
|
|
141
223
|
try {
|
|
142
|
-
|
|
143
|
-
return countMatches(output, new RegExp(`\\(${escapeForRegex(rule)}\\)`));
|
|
224
|
+
return countRule(run(command.replace("{config}", strictName)), rule, expect);
|
|
144
225
|
} finally {
|
|
145
226
|
rmSync(resolve3(cwd, strictName), { force: true });
|
|
146
227
|
}
|
|
@@ -298,6 +379,18 @@ ${output.trim()}`
|
|
|
298
379
|
// src/ratchet.ts
|
|
299
380
|
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
300
381
|
import { resolve as resolve5 } from "path";
|
|
382
|
+
var EVIDENCE_LINES = 10;
|
|
383
|
+
var recorded = (run) => {
|
|
384
|
+
let output = "";
|
|
385
|
+
return {
|
|
386
|
+
last: () => output.split("\n").map((line) => line.trimEnd()).filter((line) => line !== "").slice(-EVIDENCE_LINES),
|
|
387
|
+
run: (command) => {
|
|
388
|
+
const result = run(command);
|
|
389
|
+
output = result.output;
|
|
390
|
+
return result;
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
};
|
|
301
394
|
var verdictOf = (now, baseline) => {
|
|
302
395
|
if (now > baseline) return "grew";
|
|
303
396
|
if (now < baseline) return "shrank";
|
|
@@ -335,13 +428,16 @@ var runRatchet = async ({
|
|
|
335
428
|
`${config.baseline} has no number for "${key}" \u2014 add it before enabling the counter`
|
|
336
429
|
);
|
|
337
430
|
}
|
|
338
|
-
const
|
|
339
|
-
|
|
431
|
+
const recorder = recorded(runCommand(cwd, entry.counter));
|
|
432
|
+
const now = await counter.run({ cwd, key, params: entry, run: recorder.run });
|
|
433
|
+
const verdict = verdictOf(now, limit);
|
|
434
|
+
measurements.push({
|
|
435
|
+
baseline: limit,
|
|
436
|
+
evidence: verdict === "grew" ? recorder.last() : [],
|
|
340
437
|
key,
|
|
341
|
-
|
|
342
|
-
|
|
438
|
+
now,
|
|
439
|
+
verdict
|
|
343
440
|
});
|
|
344
|
-
measurements.push({ baseline: limit, key, now, verdict: verdictOf(now, limit) });
|
|
345
441
|
}
|
|
346
442
|
const grew = measurements.some((one) => one.verdict === "grew");
|
|
347
443
|
const shrank = measurements.some((one) => one.verdict === "shrank");
|
|
@@ -363,7 +459,10 @@ var formatReport = ({ measurements, rewritten }) => {
|
|
|
363
459
|
return ` SKIP ${one.key}: not measured by --tier ${one.tier}`;
|
|
364
460
|
}
|
|
365
461
|
const head = ` ${one.key.padEnd(WIDTH)} ${String(one.now).padStart(5)} (baseline ${one.baseline})`;
|
|
366
|
-
if (one.verdict === "grew")
|
|
462
|
+
if (one.verdict === "grew") {
|
|
463
|
+
const regressed = `${head} <-- REGRESSED +${one.now - one.baseline}`;
|
|
464
|
+
return [regressed, ...one.evidence.map((line) => ` ${line}`)].join("\n");
|
|
465
|
+
}
|
|
367
466
|
if (one.verdict === "shrank") return `${head} <-- improved -${one.baseline - one.now}`;
|
|
368
467
|
return head;
|
|
369
468
|
});
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geonosis/ratchet",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Debt as a number that may only shrink — one ratchet, pluggable counters.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ratchet",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"type": "module",
|
|
22
22
|
"main": "dist/index.js",
|
|
23
23
|
"bin": {
|
|
24
|
-
"geonosis-ratchet": "
|
|
24
|
+
"geonosis-ratchet": "bin/geonosis-ratchet.mjs"
|
|
25
25
|
},
|
|
26
26
|
"exports": {
|
|
27
27
|
".": "./dist/index.js"
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
"bin",
|
|
31
31
|
"dist"
|
|
32
32
|
],
|
|
33
|
-
"scripts": {
|
|
34
|
-
"build": "tsup",
|
|
35
|
-
"typecheck": "tsc --noEmit"
|
|
36
|
-
},
|
|
37
33
|
"engines": {
|
|
38
34
|
"node": ">=22"
|
|
39
35
|
},
|
|
40
36
|
"publishConfig": {
|
|
41
37
|
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup",
|
|
41
|
+
"typecheck": "tsc --noEmit"
|
|
42
42
|
}
|
|
43
|
-
}
|
|
43
|
+
}
|