@geonosis/ratchet 0.1.1 → 0.2.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 +78 -2
- package/dist/{chunk-PUJWT3QM.js → chunk-BTWIR7DK.js} +47 -9
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,22 @@ geonosis-ratchet --cwd apps/web
|
|
|
18
18
|
|
|
19
19
|
Exit 1 when a number grew, 2 when a counter could not measure at all, 0 otherwise.
|
|
20
20
|
|
|
21
|
+
### A baseline is not proof the gate still gates
|
|
22
|
+
|
|
23
|
+
The ratchet compares a number against a number. Swapping the tool that produces it — a vendored lint
|
|
24
|
+
plugin for a published one, one version for the next — can hold every number and still have stopped
|
|
25
|
+
firing, because a rule that fires nowhere counts zero exactly like a rule with nothing to find. Check
|
|
26
|
+
that separately, with [`@geonosis/lint-parity`](https://www.npmjs.com/package/@geonosis/lint-parity):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx geonosis-lint-parity \
|
|
30
|
+
--corpus node_modules/@geonosis/oxlint-plugin-biological-architecture/corpus \
|
|
31
|
+
--a oxlintrc.old.json --b oxlintrc.new.json --out proofs/reach
|
|
32
|
+
npx geonosis-lint-parity --a oxlintrc.old.json --b oxlintrc.new.json --out proofs/parity -- apps packages
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Reach first — parity between two configs that both fire nothing is perfect parity — then the tree.
|
|
36
|
+
|
|
21
37
|
## Config
|
|
22
38
|
|
|
23
39
|
`geonosis.ratchet.json` at the repo root names the counters and how to run them:
|
|
@@ -57,10 +73,64 @@ A counter whose command cannot run is a hard error, never a silent zero. A gate
|
|
|
57
73
|
has not passed. Nor is a number believed on its own: `oxlintErrors` reads the finding lines AND the
|
|
58
74
|
summary oxlint printed, and raises when they disagree.
|
|
59
75
|
|
|
76
|
+
### The warnings-only window, and `expectFormat`
|
|
77
|
+
|
|
78
|
+
The last-resort backstop under the oxlint counters — output in no shape we know *and* a non-zero
|
|
79
|
+
exit means the format moved, so refuse — has one gap, and it is worth naming rather than pretending
|
|
80
|
+
otherwise. **oxlint exits 0 when a run found only warnings.** On such a run the backstop cannot
|
|
81
|
+
fire: an unrecognised format reads as zero warnings, and a zero that only shrinks is a zero the
|
|
82
|
+
ratchet banks by rewriting the baseline down.
|
|
83
|
+
|
|
84
|
+
`--deny-warnings` is **not** the fix. It makes warnings exit non-zero by making them errors, which
|
|
85
|
+
moves every warning into `oxlintErrors` and changes what both numbers mean — a bigger lie than the
|
|
86
|
+
one it patches, told to the whole baseline.
|
|
87
|
+
|
|
88
|
+
The fix is to say which shape you asked for:
|
|
89
|
+
|
|
90
|
+
```jsonc
|
|
91
|
+
{ "counter": "oxlintWarnings", "command": "npx oxlint --format=unix .", "expectFormat": "unix" }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
With `expectFormat` set, output that matches none of the three shapes and is not empty once npm's
|
|
95
|
+
own chatter is stripped (`> …` script echoes, `npm warn …`, `npm notice …` update nags, blank
|
|
96
|
+
lines) is refused **even on a clean exit**. `npm ERR!` is not stripped — that is npm saying the
|
|
97
|
+
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
|
|
98
|
+
the behaviour it had. Valid values: `unix`, `agent`, `default`.
|
|
99
|
+
|
|
60
100
|
When a number **grew**, the last ten lines of that counter's command output print under the
|
|
61
101
|
`<-- REGRESSED` line, indented — so the report says what grew, not only that something did. A
|
|
62
102
|
counter that reads a file rather than running a command (`lawLineCount`) prints nothing extra.
|
|
63
103
|
|
|
104
|
+
### `testFailures` reads the runner's summary, never the exit code
|
|
105
|
+
|
|
106
|
+
The counter looks for the runner's own count — `N failed` — and **refuses when it cannot parse
|
|
107
|
+
one**. It never reads the process exit code, because a test runner exiting 0 over a red suite is
|
|
108
|
+
commoner than anyone expects: `@cloudflare/vitest-pool-workers` 0.22 on vitest 4.1 exited 0 with
|
|
109
|
+
failing tests on every workerd suite of a consumer, and every gate that trusted the exit code
|
|
110
|
+
reported green over red for weeks. A refusal is loud and stops the run; a trusted 0 is silent and
|
|
111
|
+
banks the red as a win.
|
|
112
|
+
|
|
113
|
+
For the same reason, **give every test package its own `testFailures` entry**, each with its own
|
|
114
|
+
`key`. One entry over one workspace measures one workspace; the suites it does not run are not zero
|
|
115
|
+
failures, they are unmeasured — and unmeasured reads exactly like green.
|
|
116
|
+
|
|
117
|
+
```jsonc
|
|
118
|
+
{ "counter": "testFailures", "key": "testFailuresApi", "command": "bun --cwd apps/api test", "tiers": ["full"] }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`examples/during-day.ratchet.json` is four test entries for that reason: `apps/web` plus the three
|
|
122
|
+
workerd packages that were previously outside every gate.
|
|
123
|
+
|
|
124
|
+
### `oxlintRule` counts a warned rule twice, on purpose
|
|
125
|
+
|
|
126
|
+
A rule parked at `"warn"` as ratcheted debt appears in two numbers: once inside `oxlintWarnings`,
|
|
127
|
+
and once under its own `oxlintRule` key, which measures it at `"error"` through a strict temp copy of
|
|
128
|
+
the config. So arming a rule at warn raises **both**, and fixing one finding lowers **both**.
|
|
129
|
+
|
|
130
|
+
That is not double-counting the total; the second key exists so the debt is visible per rule instead
|
|
131
|
+
of hidden inside a lump sum that a different rule's warning could mask. dielime today: `oxlintWarnings`
|
|
132
|
+
12 → 135 when `no-raw-html-atoms` was armed, with its own key at 123.
|
|
133
|
+
|
|
64
134
|
## Running it where it will actually run
|
|
65
135
|
|
|
66
136
|
- **Counters run in the caller's environment.** They inherit the shell the ratchet was started in,
|
|
@@ -76,6 +146,12 @@ counter that reads a file rather than running a command (`lawLineCount`) prints
|
|
|
76
146
|
`minimumReleaseAgeExclude` into `pnpm-workspace.yaml` — rather than lowering the cooldown. The
|
|
77
147
|
cooldown is protecting every other dependency in the tree; the exclusion is scoped to the one you
|
|
78
148
|
chose to trust.
|
|
149
|
+
- **Bumping to a new version: keep both exclusions until the install is done, then drop the old one
|
|
150
|
+
in the same commit.** `minimumReleaseAgeExclude` is matched against what the lockfile is being
|
|
151
|
+
asked to resolve, so removing the old pair first makes the resolver walk back through the version
|
|
152
|
+
it is replacing and refuse it — an install that fails for the version you are leaving, not the one
|
|
153
|
+
you are taking. Old and new together, install, then delete the old line before committing: an
|
|
154
|
+
exclusion left behind is a cooldown quietly off for a package nobody is watching any more.
|
|
79
155
|
|
|
80
156
|
## Counters
|
|
81
157
|
|
|
@@ -83,8 +159,8 @@ Every counter takes its `command` from the config, so the toolchain stays the re
|
|
|
83
159
|
|
|
84
160
|
| id | counts | params |
|
|
85
161
|
| --- | --- | --- |
|
|
86
|
-
| `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` |
|
|
87
|
-
| `oxlintRule` | one named rule's findings; with `config`, after forcing
|
|
162
|
+
| `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` |
|
|
163
|
+
| `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` |
|
|
88
164
|
| `typecheckErrors` | `error TS` occurrences | `command` |
|
|
89
165
|
| `testFailures` | the runner's own failure summary; throws when neither a pass nor a fail count is readable | `command` |
|
|
90
166
|
| `unformattedFiles` | paths `--list-different` names that exist on disk | `command` |
|
|
@@ -112,6 +112,25 @@ var UNIX_FINDING = /^\S[^\n]*:\d+:\d+: .*\[(Error|Warning)\/[^\]\n]+\]$/gm;
|
|
|
112
112
|
var UNIX_SUMMARY = /^(\d+) problems?$/m;
|
|
113
113
|
var AGENT_FINDING = /^\S[^\n]*:\d+:\d+: (error|warning) /gm;
|
|
114
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");
|
|
115
134
|
var total = (reading) => reading.errors + reading.warnings;
|
|
116
135
|
var countFindings = (output, pattern, errorToken) => {
|
|
117
136
|
let errors = 0;
|
|
@@ -122,7 +141,7 @@ var countFindings = (output, pattern, errorToken) => {
|
|
|
122
141
|
}
|
|
123
142
|
return { errors, warnings };
|
|
124
143
|
};
|
|
125
|
-
var readFindings = (counter, { code, output }) => {
|
|
144
|
+
var readFindings = (counter, { code, output }, expect) => {
|
|
126
145
|
const unix = countFindings(output, UNIX_FINDING, "Error");
|
|
127
146
|
const agent = countFindings(output, AGENT_FINDING, "error");
|
|
128
147
|
const problems = UNIX_SUMMARY.exec(output);
|
|
@@ -148,22 +167,42 @@ ${output.trim()}`);
|
|
|
148
167
|
return summary;
|
|
149
168
|
}
|
|
150
169
|
if (total(agent) > 0) return agent;
|
|
151
|
-
if (code === 0)
|
|
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
|
+
}
|
|
152
176
|
return refuse(`the tool exited ${code} and printed no findings and no summary`);
|
|
153
177
|
};
|
|
154
178
|
var oxlintErrors = {
|
|
155
179
|
id: "oxlintErrors",
|
|
156
180
|
run: async ({ params, run }) => {
|
|
157
181
|
const command = stringParam("oxlintErrors", params, "command", DEFAULT_COMMAND);
|
|
158
|
-
|
|
182
|
+
const expect = expectedFormat("oxlintErrors", params);
|
|
183
|
+
return readFindings("oxlintErrors", run(command), expect).errors;
|
|
159
184
|
}
|
|
160
185
|
};
|
|
161
186
|
var oxlintWarnings = {
|
|
162
187
|
id: "oxlintWarnings",
|
|
163
188
|
run: async ({ params, run }) => {
|
|
164
189
|
const command = stringParam("oxlintWarnings", params, "command", DEFAULT_COMMAND);
|
|
165
|
-
|
|
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
|
+
);
|
|
166
203
|
}
|
|
204
|
+
const named = new RegExp(`\\(${escapeForRegex(rule)}\\)`);
|
|
205
|
+
return lines.filter((line) => named.test(line)).length;
|
|
167
206
|
};
|
|
168
207
|
var oxlintRule = {
|
|
169
208
|
id: "oxlintRule",
|
|
@@ -171,19 +210,18 @@ var oxlintRule = {
|
|
|
171
210
|
const rule = stringParam("oxlintRule", params, "rule");
|
|
172
211
|
const command = stringParam("oxlintRule", params, "command", DEFAULT_COMMAND);
|
|
173
212
|
const config = typeof params.config === "string" ? params.config : "";
|
|
174
|
-
|
|
175
|
-
|
|
213
|
+
const expect = expectedFormat("oxlintRule", params);
|
|
214
|
+
if (config === "") return countRule(run(command), rule, expect);
|
|
176
215
|
const source = resolve3(cwd, config);
|
|
177
216
|
if (!existsSync3(source)) throw new CounterError("oxlintRule", `no config at ${config}`);
|
|
178
217
|
const strictName = `.oxlintrc.ratchet-${key}.json`;
|
|
179
218
|
const strict = readFileSync2(source, "utf8").replace(
|
|
180
|
-
new RegExp(`("[^"]*${escapeForRegex(rule)}"\\s*:\\s*)"(warn|off)"`),
|
|
219
|
+
new RegExp(`("[^"]*${escapeForRegex(rule)}"\\s*:\\s*\\[?\\s*)"(warn|off)"`),
|
|
181
220
|
'$1"error"'
|
|
182
221
|
);
|
|
183
222
|
writeFileSync(resolve3(cwd, strictName), strict);
|
|
184
223
|
try {
|
|
185
|
-
|
|
186
|
-
return countMatches(output, new RegExp(`\\(${escapeForRegex(rule)}\\)`));
|
|
224
|
+
return countRule(run(command.replace("{config}", strictName)), rule, expect);
|
|
187
225
|
} finally {
|
|
188
226
|
rmSync(resolve3(cwd, strictName), { force: true });
|
|
189
227
|
}
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED