@geonosis/ratchet 0.1.0 → 0.1.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 CHANGED
@@ -54,7 +54,28 @@ 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
+ When a number **grew**, the last ten lines of that counter's command output print under the
61
+ `<-- REGRESSED` line, indented — so the report says what grew, not only that something did. A
62
+ counter that reads a file rather than running a command (`lawLineCount`) prints nothing extra.
63
+
64
+ ## Running it where it will actually run
65
+
66
+ - **Counters run in the caller's environment.** They inherit the shell the ratchet was started in,
67
+ nothing more. If a child command needs `NODE_OPTIONS` — a TypeScript shim, a loader — put it on
68
+ the script that invokes `geonosis-ratchet`, not on the counter's own line, and not only in your
69
+ interactive shell.
70
+ - **A `typecheckErrors` counter needs the same precondition CI gives it: build the workspace
71
+ packages first.** In a fresh worktree the `dist/*.d.ts` files do not exist yet, and `tsc` reports
72
+ a false +N of missing-module errors that has nothing to do with the change under test. `pnpm build
73
+ && geonosis-ratchet` is the shape; a bare `geonosis-ratchet` in a clean checkout is not.
74
+ - **A release-age cooldown will refuse a package published minutes ago.** If pnpm's
75
+ `minimumReleaseAge` blocks `@geonosis/*`, exclude the scope for that project — pnpm writes
76
+ `minimumReleaseAgeExclude` into `pnpm-workspace.yaml` — rather than lowering the cooldown. The
77
+ cooldown is protecting every other dependency in the tree; the exclusion is scoped to the one you
78
+ chose to trust.
58
79
 
59
80
  ## Counters
60
81
 
@@ -62,7 +83,7 @@ Every counter takes its `command` from the config, so the toolchain stays the re
62
83
 
63
84
  | id | counts | params |
64
85
  | --- | --- | --- |
65
- | `oxlintErrors` / `oxlintWarnings` | `: error ` / `: warning ` lines | `command` |
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` |
66
87
  | `oxlintRule` | one named rule's findings; with `config`, after forcing it to `error` in a temp copy, so debt cannot grow behind a downgrade | `rule`, `config`, `command` |
67
88
  | `typecheckErrors` | `error TS` occurrences | `command` |
68
89
  | `testFailures` | the runner's own failure summary; throws when neither a pass nor a fail count is readable | `command` |
@@ -108,18 +108,61 @@ 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 total = (reading) => reading.errors + reading.warnings;
116
+ var countFindings = (output, pattern, errorToken) => {
117
+ let errors = 0;
118
+ let warnings = 0;
119
+ for (const match of output.matchAll(new RegExp(pattern.source, pattern.flags))) {
120
+ if (match[1] === errorToken) errors += 1;
121
+ else warnings += 1;
122
+ }
123
+ return { errors, warnings };
124
+ };
125
+ var readFindings = (counter, { code, output }) => {
126
+ const unix = countFindings(output, UNIX_FINDING, "Error");
127
+ const agent = countFindings(output, AGENT_FINDING, "error");
128
+ const problems = UNIX_SUMMARY.exec(output);
129
+ const found = DEFAULT_SUMMARY.exec(output);
130
+ const refuse = (why) => {
131
+ throw new CounterError(counter, `${why} \u2014 oxlint's output changed:
132
+ ${output.trim()}`);
133
+ };
134
+ if (total(unix) > 0 || problems !== null) {
135
+ if (problems?.[1] === void 0) return refuse('unix findings with no "N problems" summary');
136
+ if (Number(problems[1]) !== total(unix)) {
137
+ return refuse(`the summary says ${problems[1]} problems, the lines say ${total(unix)}`);
138
+ }
139
+ return unix;
140
+ }
141
+ if (found?.[1] !== void 0 && found[2] !== void 0) {
142
+ const summary = { errors: Number(found[2]), warnings: Number(found[1]) };
143
+ if (total(agent) > 0 && (agent.errors !== summary.errors || agent.warnings !== summary.warnings)) {
144
+ return refuse(
145
+ `the summary says ${summary.errors} errors and ${summary.warnings} warnings, the lines say ${agent.errors} and ${agent.warnings}`
146
+ );
147
+ }
148
+ return summary;
149
+ }
150
+ if (total(agent) > 0) return agent;
151
+ if (code === 0) return { errors: 0, warnings: 0 };
152
+ return refuse(`the tool exited ${code} and printed no findings and no summary`);
153
+ };
111
154
  var oxlintErrors = {
112
155
  id: "oxlintErrors",
113
156
  run: async ({ params, run }) => {
114
157
  const command = stringParam("oxlintErrors", params, "command", DEFAULT_COMMAND);
115
- return countMatches(run(command).output, /: error /);
158
+ return readFindings("oxlintErrors", run(command)).errors;
116
159
  }
117
160
  };
118
161
  var oxlintWarnings = {
119
162
  id: "oxlintWarnings",
120
163
  run: async ({ params, run }) => {
121
164
  const command = stringParam("oxlintWarnings", params, "command", DEFAULT_COMMAND);
122
- return countMatches(run(command).output, /: warning /);
165
+ return readFindings("oxlintWarnings", run(command)).warnings;
123
166
  }
124
167
  };
125
168
  var oxlintRule = {
@@ -298,6 +341,18 @@ ${output.trim()}`
298
341
  // src/ratchet.ts
299
342
  import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
300
343
  import { resolve as resolve5 } from "path";
344
+ var EVIDENCE_LINES = 10;
345
+ var recorded = (run) => {
346
+ let output = "";
347
+ return {
348
+ last: () => output.split("\n").map((line) => line.trimEnd()).filter((line) => line !== "").slice(-EVIDENCE_LINES),
349
+ run: (command) => {
350
+ const result = run(command);
351
+ output = result.output;
352
+ return result;
353
+ }
354
+ };
355
+ };
301
356
  var verdictOf = (now, baseline) => {
302
357
  if (now > baseline) return "grew";
303
358
  if (now < baseline) return "shrank";
@@ -335,13 +390,16 @@ var runRatchet = async ({
335
390
  `${config.baseline} has no number for "${key}" \u2014 add it before enabling the counter`
336
391
  );
337
392
  }
338
- const now = await counter.run({
339
- cwd,
393
+ const recorder = recorded(runCommand(cwd, entry.counter));
394
+ const now = await counter.run({ cwd, key, params: entry, run: recorder.run });
395
+ const verdict = verdictOf(now, limit);
396
+ measurements.push({
397
+ baseline: limit,
398
+ evidence: verdict === "grew" ? recorder.last() : [],
340
399
  key,
341
- params: entry,
342
- run: runCommand(cwd, entry.counter)
400
+ now,
401
+ verdict
343
402
  });
344
- measurements.push({ baseline: limit, key, now, verdict: verdictOf(now, limit) });
345
403
  }
346
404
  const grew = measurements.some((one) => one.verdict === "grew");
347
405
  const shrank = measurements.some((one) => one.verdict === "shrank");
@@ -363,7 +421,10 @@ var formatReport = ({ measurements, rewritten }) => {
363
421
  return ` SKIP ${one.key}: not measured by --tier ${one.tier}`;
364
422
  }
365
423
  const head = ` ${one.key.padEnd(WIDTH)} ${String(one.now).padStart(5)} (baseline ${one.baseline})`;
366
- if (one.verdict === "grew") return `${head} <-- REGRESSED +${one.now - one.baseline}`;
424
+ if (one.verdict === "grew") {
425
+ const regressed = `${head} <-- REGRESSED +${one.now - one.baseline}`;
426
+ return [regressed, ...one.evidence.map((line) => ` ${line}`)].join("\n");
427
+ }
367
428
  if (one.verdict === "shrank") return `${head} <-- improved -${one.baseline - one.now}`;
368
429
  return head;
369
430
  });
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  COUNTERS,
3
3
  formatReport,
4
4
  runRatchet
5
- } from "./chunk-VX2PL7QG.js";
5
+ } from "./chunk-PUJWT3QM.js";
6
6
 
7
7
  // src/cli.ts
8
8
  var cwdFlag = process.argv.indexOf("--cwd");
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  loadConfig,
9
9
  runCommand,
10
10
  runRatchet
11
- } from "./chunk-VX2PL7QG.js";
11
+ } from "./chunk-PUJWT3QM.js";
12
12
  export {
13
13
  CONFIG_FILE,
14
14
  COUNTERS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/ratchet",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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": "./bin/geonosis-ratchet.mjs"
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
+ }