@geonosis/lint-parity 1.3.0 → 2.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.
@@ -1,16 +1,100 @@
1
+ // src/envelope.ts
2
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import { fileURLToPath } from "url";
5
+ var ENVELOPES_DIR = ".geonosis/envelopes";
6
+ var envelopePath = (root, tool) => join(root, ENVELOPES_DIR, `${tool}.json`);
7
+ var UnbalancedEnvelope = class extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = "UnbalancedEnvelope";
11
+ }
12
+ };
13
+ var isCount = (value) => Number.isSafeInteger(value) && value >= 0;
14
+ var unbalancedMessage = (envelope, next) => `${envelope.tool}: considered ${envelope.considered} but accounts for ${envelope.read + envelope.refused.length + envelope.excused.length} \u2014 ${envelope.read} read + ${envelope.refused.length} refused + ${envelope.excused.length} excused. A run that has lost count of its own inputs cannot say what it measured, so no verdict was rendered and no envelope was written. Next: ${next}`;
15
+ var writeEnvelope = ({
16
+ envelope,
17
+ next,
18
+ root
19
+ }) => {
20
+ if (envelope.tool.trim() === "") {
21
+ throw new UnbalancedEnvelope(
22
+ `an envelope with no tool name cannot be filed or reported against. Next: ${next}`
23
+ );
24
+ }
25
+ if (envelope.version.trim() === "") {
26
+ throw new UnbalancedEnvelope(
27
+ `${envelope.tool}: an envelope that cannot name the build that wrote it dates nothing, and a stale one reads exactly like a fresh one. Next: ${next}`
28
+ );
29
+ }
30
+ if (!isCount(envelope.considered) || !isCount(envelope.read)) {
31
+ throw new UnbalancedEnvelope(
32
+ `${envelope.tool}: considered ${envelope.considered} and read ${envelope.read} \u2014 a census is a whole number of things, and arithmetic over anything else balances by accident. Next: ${next}`
33
+ );
34
+ }
35
+ if (envelope.considered !== envelope.read + envelope.refused.length + envelope.excused.length) {
36
+ throw new UnbalancedEnvelope(unbalancedMessage(envelope, next));
37
+ }
38
+ const at = envelopePath(root, envelope.tool);
39
+ mkdirSync(dirname(at), { recursive: true });
40
+ writeFileSync(at, `${JSON.stringify(envelope, void 0, 2)}
41
+ `);
42
+ return at;
43
+ };
44
+ var UNKNOWN = "unknown";
45
+ var versionIn = (dir) => {
46
+ try {
47
+ const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
48
+ return typeof manifest.version === "string" ? manifest.version : void 0;
49
+ } catch {
50
+ return void 0;
51
+ }
52
+ };
53
+ var versionOf = (moduleUrl) => {
54
+ let dir = dirname(fileURLToPath(moduleUrl));
55
+ for (; ; ) {
56
+ const found = versionIn(dir);
57
+ if (found !== void 0) return found;
58
+ const up = dirname(dir);
59
+ if (up === dir) return UNKNOWN;
60
+ dir = up;
61
+ }
62
+ };
63
+ var PARITY_TOOL = "lint-parity";
64
+ var PARITY_NEXT = "oxlint --debug=files --config <each config> <paths> and diff the two lists \u2014 a parity run only compares the files BOTH configs opened, and the ones only one of them opened are the silence this refuses over";
65
+ var parityEnvelope = (census, findings, durationMs) => ({
66
+ considered: census.both.length + census.onlyInA.length + census.onlyInB.length,
67
+ durationMs,
68
+ excused: [
69
+ ...census.onlyInA.map((path) => ({
70
+ path,
71
+ reason: "config B would not lint this file, so the two runs cannot be compared over it"
72
+ })),
73
+ ...census.onlyInB.map((path) => ({
74
+ path,
75
+ reason: "config A would not lint this file, so the two runs cannot be compared over it"
76
+ }))
77
+ ],
78
+ findings,
79
+ read: census.both.length,
80
+ refused: [],
81
+ tool: PARITY_TOOL,
82
+ version: versionOf(import.meta.url)
83
+ });
84
+
1
85
  // src/parity.ts
2
86
  import { spawnSync } from "child_process";
3
87
  import {
4
88
  cpSync,
5
89
  existsSync,
6
- mkdirSync,
90
+ mkdirSync as mkdirSync2,
7
91
  mkdtempSync,
8
- readFileSync,
92
+ readFileSync as readFileSync2,
9
93
  rmSync,
10
- writeFileSync
94
+ writeFileSync as writeFileSync2
11
95
  } from "fs";
12
96
  import { tmpdir } from "os";
13
- import { dirname, join, resolve } from "path";
97
+ import { dirname as dirname2, join as join2, resolve } from "path";
14
98
  var ANSI = /\[[0-9;]*m/g;
15
99
  var SUMMARY_COUNT = /^(\d+) problems?$/m;
16
100
  var RULE_IN_LINE = /\[(?:Error|Warning)\/([^\]]+)\]$/;
@@ -21,7 +105,7 @@ var resolveOxlint = (from) => {
21
105
  for (; ; ) {
22
106
  const bin = resolve(dir, "node_modules/.bin/oxlint");
23
107
  if (existsSync(bin)) return bin;
24
- const parent = dirname(dir);
108
+ const parent = dirname2(dir);
25
109
  if (parent === dir) return "oxlint";
26
110
  dir = parent;
27
111
  }
@@ -156,7 +240,7 @@ var formatSummary = (parity) => {
156
240
  ].join("\n");
157
241
  };
158
242
  var rulesNamedBy = (config) => {
159
- const parsed = JSON.parse(readFileSync(config, "utf8"));
243
+ const parsed = JSON.parse(readFileSync2(config, "utf8"));
160
244
  return [
161
245
  .../* @__PURE__ */ new Set([
162
246
  ...Object.keys(parsed.rules ?? {}),
@@ -197,10 +281,10 @@ var verdictOf = (changed, claimed) => {
197
281
  return claimed ? "unproven" : "unclaimed";
198
282
  };
199
283
  var readManifest = (corpus) => {
200
- const path = join(corpus, MANIFEST_FILE);
284
+ const path = join2(corpus, MANIFEST_FILE);
201
285
  let parsed;
202
286
  try {
203
- parsed = JSON.parse(readFileSync(path, "utf8"));
287
+ parsed = JSON.parse(readFileSync2(path, "utf8"));
204
288
  } catch (error) {
205
289
  throw new ParityError(`could not read ${path}: ${error.message}`);
206
290
  }
@@ -219,8 +303,8 @@ var corpusOf = ({
219
303
  }) => {
220
304
  const manifest = readManifest(corpus);
221
305
  const exercised = new Set(manifest.rules);
222
- const root = mkdtempSync(join(tmpdir(), "geonosis-corpus-"));
223
- const here = join(root, "corpus");
306
+ const root = mkdtempSync(join2(tmpdir(), "geonosis-corpus-"));
307
+ const here = join2(root, "corpus");
224
308
  const foundIn = (config, side) => {
225
309
  const counts = /* @__PURE__ */ new Map();
226
310
  for (const finding of readRun(
@@ -395,6 +479,44 @@ var lint = ({
395
479
  throw new ParityError(`could not run ${oxlint}: ${run.error.message}`);
396
480
  return { code: run.status ?? -1, output: `${run.stdout ?? ""}${run.stderr ?? ""}` };
397
481
  };
482
+ var filesLintedBy = ({
483
+ config,
484
+ cwd,
485
+ oxlint,
486
+ paths
487
+ }) => {
488
+ const run = spawnSync(oxlint, ["--debug=files", "--config", config, ...paths], {
489
+ cwd,
490
+ encoding: "utf8",
491
+ env: { ...process.env, NO_COLOR: "1" },
492
+ maxBuffer: 64 * 1024 * 1024
493
+ });
494
+ if (run.error !== void 0) {
495
+ throw new ParityError(`could not run ${oxlint}: ${run.error.message}`);
496
+ }
497
+ if (run.status !== 0) {
498
+ throw new ParityError(
499
+ `oxlint --debug=files exited ${String(run.status)} under ${config}, so which files it would read is not known:
500
+ ${`${run.stdout ?? ""}${run.stderr ?? ""}`.trim()}`
501
+ );
502
+ }
503
+ return (run.stdout ?? "").split("\n").map((line) => line.trim().replace(`${cwd}/`, "").replace(/^\.\//, "")).filter((line) => line !== "").toSorted();
504
+ };
505
+ var fileCensus = ({
506
+ configA,
507
+ configB,
508
+ cwd,
509
+ oxlint,
510
+ paths
511
+ }) => {
512
+ const inA = filesLintedBy({ config: configA, cwd, oxlint, paths });
513
+ const inB = new Set(filesLintedBy({ config: configB, cwd, oxlint, paths }));
514
+ return {
515
+ both: inA.filter((file) => inB.has(file)),
516
+ onlyInA: inA.filter((file) => !inB.has(file)),
517
+ onlyInB: [...inB].filter((file) => !inA.includes(file)).toSorted()
518
+ };
519
+ };
398
520
  var parityOf = ({
399
521
  configA,
400
522
  configB,
@@ -408,18 +530,21 @@ var parityOf = ({
408
530
  b: readRun(lint({ config: configB, cwd, oxlint, paths }), cwd, "B")
409
531
  });
410
532
  if (out !== void 0) {
411
- mkdirSync(out, { recursive: true });
412
- writeFileSync(join(out, "a.txt"), asFile(parity.a));
413
- writeFileSync(join(out, "b.txt"), asFile(parity.b));
414
- writeFileSync(join(out, "only-in-a.txt"), asFile(parity.onlyInA));
415
- writeFileSync(join(out, "only-in-b.txt"), asFile(parity.onlyInB));
416
- writeFileSync(join(out, "changed.txt"), asFile(changedLines(parity.messageChanged)));
417
- writeFileSync(join(out, "SUMMARY.md"), formatSummary(parity));
533
+ mkdirSync2(out, { recursive: true });
534
+ writeFileSync2(join2(out, "a.txt"), asFile(parity.a));
535
+ writeFileSync2(join2(out, "b.txt"), asFile(parity.b));
536
+ writeFileSync2(join2(out, "only-in-a.txt"), asFile(parity.onlyInA));
537
+ writeFileSync2(join2(out, "only-in-b.txt"), asFile(parity.onlyInB));
538
+ writeFileSync2(join2(out, "changed.txt"), asFile(changedLines(parity.messageChanged)));
539
+ writeFileSync2(join2(out, "SUMMARY.md"), formatSummary(parity));
418
540
  }
419
541
  return parity;
420
542
  };
421
543
 
422
544
  export {
545
+ writeEnvelope,
546
+ PARITY_NEXT,
547
+ parityEnvelope,
423
548
  resolveOxlint,
424
549
  ParityError,
425
550
  normaliseFindings,
@@ -434,5 +559,7 @@ export {
434
559
  corpusOf,
435
560
  formatCorpus,
436
561
  parseParityArgs,
562
+ filesLintedBy,
563
+ fileCensus,
437
564
  parityOf
438
565
  };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,10 @@
1
+ /** The files each config would actually read, split three ways. */
2
+ type FileCensus = {
3
+ both: string[];
4
+ onlyInA: string[];
5
+ onlyInB: string[];
6
+ };
7
+
1
8
  /**
2
9
  * The consumer's own oxlint, walking up from a directory, not one `npx` might fetch: a run under a
3
10
  * different binary from the one the repo gates with compares two things nobody ships. Exported
@@ -174,6 +181,35 @@ type ParityArgs = {
174
181
  paths: string[];
175
182
  };
176
183
  declare const parseParityArgs: (argv: string[]) => ParityArgs;
184
+ /**
185
+ * The files a config would actually lint, asked of oxlint itself.
186
+ *
187
+ * `--debug=files` prints one path per line and exits 0 without linting anything: measured here,
188
+ * `--config narrow.json` (whose `ignorePatterns` name `b.ts`) printed one file where
189
+ * `--config wide.json` printed two. Nothing else in a `--format=unix` run carries this — that
190
+ * output is findings and only findings, which is why a parity run could report agreement over a
191
+ * tree the second config never opened.
192
+ *
193
+ * The argv is deliberately the same SHAPE `lint` uses, config and paths passed through exactly as
194
+ * they arrived. It has to be: oxlint's ignore resolution is sensitive to that shape — an absolute
195
+ * `--config` with a relative path argument applies no `ignorePatterns` at all, where the same
196
+ * config named relatively does. A census taken under different arguments would describe a run
197
+ * nobody made, which is the failure this whole envelope is about.
198
+ */
199
+ declare const filesLintedBy: ({ config, cwd, oxlint, paths, }: {
200
+ config: string;
201
+ cwd: string;
202
+ oxlint: string;
203
+ paths: string[];
204
+ }) => string[];
205
+ /** Which files both configs would read, and which each would read alone. */
206
+ declare const fileCensus: ({ configA, configB, cwd, oxlint, paths, }: {
207
+ configA: string;
208
+ configB: string;
209
+ cwd: string;
210
+ oxlint: string;
211
+ paths: string[];
212
+ }) => FileCensus;
177
213
  declare const parityOf: ({ configA, configB, cwd, out, oxlint, paths, }: {
178
214
  configA: string;
179
215
  configB: string;
@@ -183,4 +219,4 @@ declare const parityOf: ({ configA, configB, cwd, out, oxlint, paths, }: {
183
219
  paths: string[];
184
220
  }) => Parity;
185
221
 
186
- export { type CorpusClaim, type CorpusManifest, type CorpusReport, MANIFEST_FILE, type MessageChange, type Parity, type ParityArgs, ParityError, type RuleReach, type RuleTally, compareFindings, corpusOf, formatCorpus, formatSummary, manifestOf, normaliseFindings, parityOf, parseParityArgs, readManifest, readRun, resolveOxlint, ruleIdOf, rulesNamedBy };
222
+ export { type CorpusClaim, type CorpusManifest, type CorpusReport, type FileCensus, MANIFEST_FILE, type MessageChange, type Parity, type ParityArgs, ParityError, type RuleReach, type RuleTally, compareFindings, corpusOf, fileCensus, filesLintedBy, formatCorpus, formatSummary, manifestOf, normaliseFindings, parityOf, parseParityArgs, readManifest, readRun, resolveOxlint, ruleIdOf, rulesNamedBy };
package/dist/index.js CHANGED
@@ -3,6 +3,8 @@ import {
3
3
  ParityError,
4
4
  compareFindings,
5
5
  corpusOf,
6
+ fileCensus,
7
+ filesLintedBy,
6
8
  formatCorpus,
7
9
  formatSummary,
8
10
  manifestOf,
@@ -14,12 +16,14 @@ import {
14
16
  resolveOxlint,
15
17
  ruleIdOf,
16
18
  rulesNamedBy
17
- } from "./chunk-DFMYYU5B.js";
19
+ } from "./chunk-D7OGKQ2C.js";
18
20
  export {
19
21
  MANIFEST_FILE,
20
22
  ParityError,
21
23
  compareFindings,
22
24
  corpusOf,
25
+ fileCensus,
26
+ filesLintedBy,
23
27
  formatCorpus,
24
28
  formatSummary,
25
29
  manifestOf,
@@ -1,11 +1,15 @@
1
1
  import {
2
+ PARITY_NEXT,
2
3
  corpusOf,
4
+ fileCensus,
3
5
  formatCorpus,
4
6
  formatSummary,
7
+ parityEnvelope,
5
8
  parityOf,
6
9
  parseParityArgs,
7
- resolveOxlint
8
- } from "./chunk-DFMYYU5B.js";
10
+ resolveOxlint,
11
+ writeEnvelope
12
+ } from "./chunk-D7OGKQ2C.js";
9
13
 
10
14
  // src/parity-cli.ts
11
15
  import { mkdirSync, writeFileSync } from "fs";
@@ -76,6 +80,7 @@ and it reads them as oxlint does rather than interpreting them itself.
76
80
  }
77
81
  return report.neither.length > 0 || !report.claimsHeld ? 1 : 0;
78
82
  }
83
+ const startedAt = Date.now();
79
84
  const parity = parityOf({
80
85
  configA,
81
86
  configB,
@@ -85,6 +90,17 @@ and it reads them as oxlint does rather than interpreting them itself.
85
90
  paths: args.paths
86
91
  });
87
92
  process.stdout.write(`${formatSummary(parity)}
93
+ `);
94
+ const at = writeEnvelope({
95
+ envelope: parityEnvelope(
96
+ fileCensus({ configA, configB, cwd, oxlint, paths: args.paths }),
97
+ parity.onlyInA,
98
+ Date.now() - startedAt
99
+ ),
100
+ next: PARITY_NEXT,
101
+ root: cwd
102
+ });
103
+ process.stdout.write(`envelope: ${at}
88
104
  `);
89
105
  return parity.lost ? 1 : 0;
90
106
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/lint-parity",
3
- "version": "1.3.0",
3
+ "version": "2.0.0",
4
4
  "types": "./dist/index.d.ts",
5
5
  "description": "Findings parity and rule reach over any two oxlint configs — the diff a fork is deleted on.",
6
6
  "keywords": [
@@ -43,6 +43,9 @@
43
43
  "publishConfig": {
44
44
  "access": "public"
45
45
  },
46
+ "devDependencies": {
47
+ "@geonosis/ratchet": "2.0.0"
48
+ },
46
49
  "scripts": {
47
50
  "build": "tsup",
48
51
  "typecheck": "tsc --noEmit"