-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathcheck-lint-coverage.mjs
More file actions
129 lines (117 loc) · 4.87 KB
/
Copy pathcheck-lint-coverage.mjs
File metadata and controls
129 lines (117 loc) · 4.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#!/usr/bin/env node
/**
* Validates that every workspace package is actually linted by CI.
*
* The sibling of scripts/check-type-check-coverage.mjs, for the same reason:
* `pnpm lint` runs `turbo run lint`, and turbo silently skips any package with
* no `lint` script — so a package without one is not "clean", it is unlinted.
* That is how `apps/console`, the largest surface in the repo, went unlinted
* while carrying 14 ESLint errors that nobody had ever seen (#2923).
*
* The stakes are concrete: `eslint.config.js` sets three `object-ui/*` rules to
* `error` specifically so a new violation fails CI (ADR-0054 Phase 5, #2879,
* and the objectql.ts type-discipline ratchet). Those ratchets are worthless in
* a package that never runs ESLint.
*
* Run: node scripts/check-lint-coverage.mjs
* Exit: 0 = OK, 1 = coverage regressed or the list is stale
*/
import { readFileSync, readdirSync, statSync } from "fs";
import { resolve, dirname, join } from "path";
import { fileURLToPath } from "url";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
// ── Known gaps ───────────────────────────────────────────────────────────────
// Packages with outstanding ESLint *errors*, so they cannot carry a `lint`
// script without turning CI red. Warnings do not matter here — ESLint only
// fails on errors unless `--max-warnings` is set, and it deliberately is not
// (7,724 warnings repo-wide are dominated by `no-explicit-any`; see #2923).
//
// Every entry is real debt: fix the errors, add `"lint": "eslint ."`, then
// delete the entry.
const DEBT = {
"@object-ui/console": {
errors: 14,
issue: 2927,
note: "8x react-hooks/purity (Date.now during render) + 4x react-hooks/static-components (components created during render reset state) + no-useless-assignment + prefer-const",
},
"@object-ui/runner": {
errors: 3,
issue: 2927,
note: "3x react-hooks/static-components in LayoutRenderer",
},
};
// ── Collect workspace packages ───────────────────────────────────────────────
const GROUPS = ["packages", "apps", "examples"];
function collect() {
const out = [];
for (const group of GROUPS) {
let entries;
try {
entries = readdirSync(resolve(root, group));
} catch {
continue;
}
for (const entry of entries) {
const dir = join(group, entry);
const manifest = resolve(root, dir, "package.json");
try {
statSync(manifest);
} catch {
continue;
}
const pkg = JSON.parse(readFileSync(manifest, "utf8"));
if (!pkg.name) continue;
out.push({
name: pkg.name,
dir,
hasScript: Boolean(pkg.scripts?.lint),
});
}
}
return out;
}
const packages = collect();
const byName = new Map(packages.map((p) => [p.name, p]));
const errors = [];
// 1. Undeclared gap — a package that never runs ESLint.
for (const pkg of packages) {
if (pkg.hasScript) continue;
if (DEBT[pkg.name]) continue;
errors.push(
`${pkg.name} (${pkg.dir}) has no "lint" script, so \`pnpm lint\` skips it entirely.\n` +
` Add "lint": "eslint ." to its package.json. If it still has ESLint errors,\n` +
` add it to DEBT in scripts/check-lint-coverage.mjs with an error count.`
);
}
// 2. Ratchet — a declared gap that has been closed must leave the list.
for (const name of Object.keys(DEBT)) {
const pkg = byName.get(name);
if (!pkg) {
errors.push(`${name} is listed in DEBT but is not a workspace package any more — delete the entry.`);
} else if (pkg.hasScript) {
errors.push(
`${name} now has a "lint" script — delete its DEBT entry so the gap cannot reopen` +
`${DEBT[name].issue ? ` (and close #${DEBT[name].issue} if it is done)` : ""}.`
);
}
}
// ── Report ───────────────────────────────────────────────────────────────────
const linted = packages.filter((p) => p.hasScript).length;
const debtCount = Object.keys(DEBT).length;
const debtErrors = Object.values(DEBT).reduce((sum, d) => sum + d.errors, 0);
if (errors.length === 0) {
console.log(
`✅ lint coverage: ${linted}/${packages.length} packages linted, ` +
`${debtCount} with outstanding errors (${debtErrors} total).`
);
process.exit(0);
}
console.error("❌ lint coverage regressed:\n");
for (const message of errors) {
console.error(` • ${message}`);
}
console.error(
"\nA package with no `lint` script is not clean — turbo skips it and CI sees nothing.\n" +
"See https://github.com/objectstack-ai/objectui/issues/2923 for why this guard exists."
);
process.exit(1);