@codyswann/lisa 4.45.2 → 4.45.4
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/all/copy-overwrite/scripts/lisa-hooks/block-blind-automerge.sh +523 -70
- package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +160 -2
- package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +115 -12
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +10 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +19 -12
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +40 -1
- package/dist/opencode/plugin-templates/parity-safety-net.sh +115 -12
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-github-write-issue/SKILL.md +16 -0
- package/plugins/lisa/hooks/block-blind-automerge.agy.sh +5 -4
- package/plugins/lisa/hooks/block-blind-automerge.sh +523 -70
- package/plugins/lisa/hooks/block-direct-issue-create.sh +160 -2
- package/plugins/lisa/hooks/parity-safety-net.sh +115 -12
- package/plugins/lisa/rules/eager/ready-role-filing.md +3 -2
- package/plugins/lisa/rules/reference/ready-role-filing.md +20 -2
- package/plugins/lisa/skills/lisa-github-write-issue/SKILL.md +16 -0
- package/plugins/lisa-agy/hooks/block-blind-automerge.agy.sh +5 -4
- package/plugins/lisa-agy/hooks/block-blind-automerge.sh +523 -70
- package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +160 -2
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +115 -12
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-github-write-issue/SKILL.md +16 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/block-blind-automerge.sh +523 -70
- package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +160 -2
- package/plugins/lisa-copilot/hooks/parity-safety-net.sh +115 -12
- package/plugins/lisa-copilot/rules/eager/ready-role-filing.md +3 -2
- package/plugins/lisa-copilot/rules/reference/ready-role-filing.md +20 -2
- package/plugins/lisa-copilot/skills/lisa-github-write-issue/SKILL.md +16 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-blind-automerge.sh +523 -70
- package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +160 -2
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +115 -12
- package/plugins/lisa-cursor/rules/ready-role-filing-reference.mdc +20 -2
- package/plugins/lisa-cursor/rules/ready-role-filing.mdc +3 -2
- package/plugins/lisa-cursor/skills/lisa-github-write-issue/SKILL.md +16 -0
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/block-blind-automerge.agy.sh +5 -4
- package/plugins/src/base/hooks/block-blind-automerge.sh +523 -70
- package/plugins/src/base/hooks/block-direct-issue-create.sh +160 -2
- package/plugins/src/base/hooks/parity-safety-net.sh +115 -12
- package/plugins/src/base/rules/eager/ready-role-filing.md +3 -2
- package/plugins/src/base/rules/reference/ready-role-filing.md +20 -2
- package/plugins/src/base/skills/lisa-github-write-issue/SKILL.md +16 -0
- package/scripts/check-typecheck-tests.mjs +229 -72
- package/scripts/lib/dependency-tree.mjs +136 -0
- package/typescript/copy-contents/.husky/pre-push +45 -0
|
@@ -27,6 +27,33 @@
|
|
|
27
27
|
* threshold-ratchet as an `allow-list` family, so *adding* an entry is flagged
|
|
28
28
|
* by a gate somebody already reads.
|
|
29
29
|
*
|
|
30
|
+
* IT REFUSES TO ANSWER FROM AN UNRESOLVABLE TREE (CodySwannGT/lisa#3913). The
|
|
31
|
+
* second of those ratchet directions is the destructive one: it names files and
|
|
32
|
+
* tells a reader to delete their quarantine entries. With an empty
|
|
33
|
+
* `node_modules` the compiler cannot start, nothing is compiled, no diagnostic
|
|
34
|
+
* is parsed — and "no diagnostic for this file" is exactly the shape of "this
|
|
35
|
+
* file was fixed". The gate reported all 370 quarantined files as cleared and
|
|
36
|
+
* instructed their removal, which is a destructive edit ordered on the strength
|
|
37
|
+
* of a comparison whose inputs never resolved.
|
|
38
|
+
*
|
|
39
|
+
* So there are two ways this refuses instead of reporting, and the second is
|
|
40
|
+
* the general one:
|
|
41
|
+
*
|
|
42
|
+
* - the compiler is not present at the path this spawns it from; and
|
|
43
|
+
* - the compiler signalled failure and produced NOT ONE parseable
|
|
44
|
+
* diagnostic, which also covers a broken tsconfig, an OOM and a crash.
|
|
45
|
+
*
|
|
46
|
+
* Both exit non-zero with the denial first and NO file list, because a gate
|
|
47
|
+
* whose only surface is an exit code cannot invent a third one — see
|
|
48
|
+
* `scripts/lib/dependency-tree.mjs` for why the wording is a contract rather
|
|
49
|
+
* than a string.
|
|
50
|
+
*
|
|
51
|
+
* `--root` EXISTS SO THIS CAN BE POINTED AT A FIXTURE. Deriving the root solely
|
|
52
|
+
* from `import.meta.url` made the failure above reproducible only by copying
|
|
53
|
+
* this file somewhere else, and `check-empty-subject-guards.mjs` reads that
|
|
54
|
+
* token out of this header to decide the gate is probeable at all — without it
|
|
55
|
+
* this sits in that sweep's declared blind spot.
|
|
56
|
+
*
|
|
30
57
|
* WHAT IS DELIBERATELY NOT DONE HERE: the declared `lib` is not widened. tsc
|
|
31
58
|
* emits "Try changing the 'lib' compiler option to 'es2023' or later" on the
|
|
32
59
|
* ES2023 array APIs, and taking that suggestion would have erased the
|
|
@@ -38,110 +65,240 @@
|
|
|
38
65
|
import { spawnSync } from "node:child_process";
|
|
39
66
|
import { readFileSync } from "node:fs";
|
|
40
67
|
import * as path from "node:path";
|
|
68
|
+
import process from "node:process";
|
|
41
69
|
import { fileURLToPath } from "node:url";
|
|
42
70
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
71
|
+
import {
|
|
72
|
+
cannotMeasure,
|
|
73
|
+
INSTALL_COMMAND,
|
|
74
|
+
missingDependencies,
|
|
75
|
+
ranVacuously,
|
|
76
|
+
} from "./lib/dependency-tree.mjs";
|
|
77
|
+
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
78
|
+
|
|
79
|
+
const DEFAULT_ROOT = path.resolve(
|
|
80
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
81
|
+
".."
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
/** The gate's name, as it prints in both the report and the refusal. */
|
|
85
|
+
const GATE = "type-correctness (tests)";
|
|
86
|
+
|
|
87
|
+
/** Where the compiler is spawned from, relative to the root. */
|
|
88
|
+
const TSC = path.join("node_modules", "typescript", "bin", "tsc");
|
|
46
89
|
|
|
47
90
|
/** Matches `path/to/file.ts(12,34): error TS1234: message`. */
|
|
48
91
|
const ERROR_LINE = /^(?<file>[^(]+)\((?<line>\d+),\d+\): error (?<code>TS\d+):/;
|
|
49
92
|
|
|
50
93
|
/**
|
|
51
94
|
* Read the quarantined paths.
|
|
52
|
-
* @
|
|
95
|
+
* @param {string} root - The directory holding the quarantine.
|
|
96
|
+
* @returns {{files: string[], reason: string} | null} The parsed quarantine, or
|
|
97
|
+
* `null` when it could not be read — an unreadable list is a refusal, not a
|
|
98
|
+
* pass, because that file is what makes this gate green.
|
|
53
99
|
*/
|
|
54
|
-
function readQuarantine() {
|
|
100
|
+
function readQuarantine(root) {
|
|
101
|
+
const quarantine = path.join(root, "typecheck-quarantine.json");
|
|
55
102
|
try {
|
|
56
|
-
const parsed = JSON.parse(readFileSync(
|
|
103
|
+
const parsed = JSON.parse(readFileSync(quarantine, "utf-8"));
|
|
57
104
|
return { files: parsed.files ?? [], reason: parsed.reason ?? "" };
|
|
58
105
|
} catch (error) {
|
|
59
106
|
console.error(
|
|
60
|
-
`check-typecheck-tests: cannot read ${path.relative(
|
|
107
|
+
`check-typecheck-tests: cannot read ${path.relative(root, quarantine)} — ${error.message}`
|
|
61
108
|
);
|
|
62
109
|
console.error(
|
|
63
110
|
"That file is what makes this gate green, so an unreadable one is a refusal, not a pass."
|
|
64
111
|
);
|
|
65
|
-
|
|
112
|
+
return null;
|
|
66
113
|
}
|
|
67
114
|
}
|
|
68
115
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
console.error(
|
|
85
|
-
`check-typecheck-tests: could not run tsc — ${tsc.error.message}`
|
|
86
|
-
);
|
|
87
|
-
process.exit(1);
|
|
116
|
+
/**
|
|
117
|
+
* Parse tsc's output into a per-file error count.
|
|
118
|
+
* @param {string} output - Combined stdout and stderr.
|
|
119
|
+
* @returns {Map<string, number>} Errors keyed by forward-slashed path.
|
|
120
|
+
*/
|
|
121
|
+
function parseDiagnostics(output) {
|
|
122
|
+
/** @type {Map<string, number>} */
|
|
123
|
+
const errorsByFile = new Map();
|
|
124
|
+
for (const line of output.split("\n")) {
|
|
125
|
+
const match = ERROR_LINE.exec(line.trim());
|
|
126
|
+
if (!match?.groups) continue;
|
|
127
|
+
const file = match.groups.file.replace(/\\/g, "/");
|
|
128
|
+
errorsByFile.set(file, (errorsByFile.get(file) ?? 0) + 1);
|
|
129
|
+
}
|
|
130
|
+
return errorsByFile;
|
|
88
131
|
}
|
|
89
132
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
133
|
+
/**
|
|
134
|
+
* The refusal issued when nothing was compiled.
|
|
135
|
+
*
|
|
136
|
+
* The denial names the destructive remedy explicitly. "Nothing type-checked" is
|
|
137
|
+
* the inference a reader draws from an empty diagnostic set, and the gate's own
|
|
138
|
+
* cleared-entry message is what turns that inference into a deletion — so this
|
|
139
|
+
* has to deny both, not just the first.
|
|
140
|
+
*
|
|
141
|
+
* NO FILE IS NAMED HERE, deliberately. A list of paths is what makes the
|
|
142
|
+
* destructive message actionable, and a refusal that still printed 370 paths
|
|
143
|
+
* would be followed exactly as often as the finding it replaces.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} because - Why the measurement could not happen.
|
|
146
|
+
* @param {number} quarantined - How many entries went unexamined.
|
|
147
|
+
* @param {string} fix - The action that repairs THIS cause. Named per call
|
|
148
|
+
* site rather than fixed, because telling someone to install dependencies
|
|
149
|
+
* they already have is how a refusal gets dismissed as noise.
|
|
150
|
+
* @returns {string} The refusal block.
|
|
151
|
+
*/
|
|
152
|
+
function refuseUnmeasurable(because, quarantined, fix) {
|
|
153
|
+
return cannotMeasure({
|
|
154
|
+
gate: GATE,
|
|
155
|
+
denial: `that any of the ${quarantined} quarantined file(s) now type-check`,
|
|
156
|
+
because,
|
|
157
|
+
remedy:
|
|
158
|
+
`${fix} Do NOT edit typecheck-quarantine.json on the strength of ` +
|
|
159
|
+
`this — no file was compiled, so nothing was established about any ` +
|
|
160
|
+
`entry in it.`,
|
|
161
|
+
});
|
|
98
162
|
}
|
|
99
163
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Run the gate.
|
|
166
|
+
* @param {string} root - Directory to resolve the project and quarantine under.
|
|
167
|
+
* @returns {number} The process exit code.
|
|
168
|
+
*/
|
|
169
|
+
export function main(root) {
|
|
170
|
+
const quarantine = readQuarantine(root);
|
|
171
|
+
if (quarantine === null) return 1;
|
|
172
|
+
const quarantined = quarantine.files;
|
|
173
|
+
const exempt = new Set(quarantined);
|
|
104
174
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
);
|
|
175
|
+
// PRECONDITION. Checked before the spawn rather than inferred from it: the
|
|
176
|
+
// spawn SUCCEEDS with an empty `node_modules` — node starts, and it is node
|
|
177
|
+
// that cannot find the module — so `tsc.error` never fires and the failure
|
|
178
|
+
// arrives as an empty diagnostic set indistinguishable from a clean run.
|
|
179
|
+
const missing = missingDependencies(root, [TSC]);
|
|
180
|
+
if (missing.length > 0) {
|
|
181
|
+
console.error(
|
|
182
|
+
refuseUnmeasurable(
|
|
183
|
+
`the TypeScript compiler is not installed — no file at ${missing[0]}.`,
|
|
184
|
+
quarantined.length,
|
|
185
|
+
`Run \`${INSTALL_COMMAND}\`, then re-run this gate.`
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
return 1;
|
|
189
|
+
}
|
|
110
190
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
191
|
+
const tsc = spawnSync(
|
|
192
|
+
process.execPath,
|
|
193
|
+
[
|
|
194
|
+
path.join(root, TSC),
|
|
195
|
+
"--noEmit",
|
|
196
|
+
"-p",
|
|
197
|
+
path.join(root, "tsconfig.tests.json"),
|
|
198
|
+
],
|
|
199
|
+
{ cwd: root, encoding: "utf-8", maxBuffer: 64 * 1024 * 1024 }
|
|
115
200
|
);
|
|
116
|
-
|
|
117
|
-
|
|
201
|
+
|
|
202
|
+
if (tsc.error) {
|
|
203
|
+
console.error(
|
|
204
|
+
`check-typecheck-tests: could not run tsc — ${tsc.error.message}`
|
|
205
|
+
);
|
|
206
|
+
return 1;
|
|
118
207
|
}
|
|
119
|
-
console.error("");
|
|
120
|
-
console.error(
|
|
121
|
-
"Fix them. Do NOT add them to typecheck-quarantine.json — that list"
|
|
122
|
-
);
|
|
123
|
-
console.error(
|
|
124
|
-
"only shrinks, and the threshold-ratchet flags additions to it."
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
208
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
console.error(
|
|
131
|
-
`❌ ${cleared.length} quarantined file(s) now type-check and must leave the list:`
|
|
209
|
+
const errorsByFile = parseDiagnostics(
|
|
210
|
+
`${tsc.stdout ?? ""}\n${tsc.stderr ?? ""}`
|
|
132
211
|
);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
212
|
+
|
|
213
|
+
// POSTCONDITION, and the general form of the same defect. The compiler
|
|
214
|
+
// signalled failure and produced nothing this gate can read, so the
|
|
215
|
+
// comparison below would run against an empty set — the exact shape of "every
|
|
216
|
+
// quarantined file is fixed". A ZERO exit with no diagnostics is not this: it
|
|
217
|
+
// is a clean run, and it reports normally.
|
|
218
|
+
if (ranVacuously(tsc.status, errorsByFile.size)) {
|
|
219
|
+
const firstLine = `${tsc.stderr ?? ""}${tsc.stdout ?? ""}`
|
|
220
|
+
.split("\n")
|
|
221
|
+
.map(line => line.trim())
|
|
222
|
+
.find(Boolean);
|
|
223
|
+
console.error(
|
|
224
|
+
refuseUnmeasurable(
|
|
225
|
+
`tsc exited ${tsc.status} without emitting one diagnostic this gate ` +
|
|
226
|
+
`could parse, so the comparison had no subject.` +
|
|
227
|
+
`${firstLine ? ` First output line: ${firstLine}` : ""}`,
|
|
228
|
+
quarantined.length,
|
|
229
|
+
"Fix the cause named above, then re-run this gate."
|
|
230
|
+
)
|
|
231
|
+
);
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const offenders = [...errorsByFile.keys()]
|
|
236
|
+
.filter(file => !exempt.has(file))
|
|
237
|
+
.sort();
|
|
238
|
+
const cleared = quarantined.filter(file => !errorsByFile.has(file)).sort();
|
|
239
|
+
|
|
240
|
+
const totalErrors = [...errorsByFile.values()].reduce((sum, n) => sum + n, 0);
|
|
241
|
+
console.log(
|
|
242
|
+
`${GATE}: ${errorsByFile.size} file(s) with ${totalErrors} error(s); ` +
|
|
243
|
+
`${quarantined.length} quarantined.`
|
|
137
244
|
);
|
|
138
|
-
|
|
139
|
-
|
|
245
|
+
|
|
246
|
+
if (offenders.length > 0) {
|
|
247
|
+
console.error("");
|
|
248
|
+
console.error(
|
|
249
|
+
`❌ ${offenders.length} file(s) outside the quarantine have type errors:`
|
|
250
|
+
);
|
|
251
|
+
for (const file of offenders) {
|
|
252
|
+
console.error(` ${file} (${errorsByFile.get(file)})`);
|
|
253
|
+
}
|
|
254
|
+
console.error("");
|
|
255
|
+
console.error(
|
|
256
|
+
"Fix them. Do NOT add them to typecheck-quarantine.json — that list"
|
|
257
|
+
);
|
|
258
|
+
console.error(
|
|
259
|
+
"only shrinks, and the threshold-ratchet flags additions to it."
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (cleared.length > 0) {
|
|
264
|
+
console.error("");
|
|
265
|
+
console.error(
|
|
266
|
+
`❌ ${cleared.length} quarantined file(s) now type-check and must leave the list:`
|
|
267
|
+
);
|
|
268
|
+
for (const file of cleared) console.error(` ${file}`);
|
|
269
|
+
console.error("");
|
|
270
|
+
console.error(
|
|
271
|
+
"Remove them from typecheck-quarantine.json. A list that keeps entries"
|
|
272
|
+
);
|
|
273
|
+
console.error(
|
|
274
|
+
"after they are fixed would re-authorise them if they ever regressed."
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (offenders.length > 0 || cleared.length > 0) return 1;
|
|
279
|
+
|
|
280
|
+
console.log(
|
|
281
|
+
"✅ No type errors outside the quarantine, and no stale quarantine entries."
|
|
140
282
|
);
|
|
283
|
+
return 0;
|
|
141
284
|
}
|
|
142
285
|
|
|
143
|
-
|
|
286
|
+
/**
|
|
287
|
+
* Resolve the root from `--root <dir>`, defaulting to the repository.
|
|
288
|
+
* @param {ReadonlyArray<string>} argv - Arguments after the script name.
|
|
289
|
+
* @returns {string} The absolute root.
|
|
290
|
+
*/
|
|
291
|
+
export function rootFrom(argv) {
|
|
292
|
+
const flag = argv.indexOf("--root");
|
|
293
|
+
if (flag === -1) return DEFAULT_ROOT;
|
|
294
|
+
const value = argv[flag + 1];
|
|
295
|
+
if (value === undefined) {
|
|
296
|
+
console.error("check-typecheck-tests: --root requires a value");
|
|
297
|
+
process.exit(2);
|
|
298
|
+
}
|
|
299
|
+
return path.resolve(value);
|
|
300
|
+
}
|
|
144
301
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
302
|
+
if (invokedAsScript(import.meta.url)) {
|
|
303
|
+
process.exit(main(rootFrom(process.argv.slice(2))));
|
|
304
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dependency-tree — decide whether a gate's dependency tree is resolvable, and
|
|
3
|
+
* say "cannot measure" in one voice when it is not (CodySwannGT/lisa#3913).
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* ## The defect this exists for
|
|
7
|
+
*
|
|
8
|
+
* The `type-correctness` gate spawns the compiler by PATH:
|
|
9
|
+
* `node node_modules/typescript/bin/tsc`. With an empty `node_modules` the
|
|
10
|
+
* SPAWN still succeeds — node starts, and it is node that then cannot find the
|
|
11
|
+
* module — so the gate's `if (tsc.error)` guard does not fire. node's
|
|
12
|
+
* "Cannot find module" goes to stderr and does not match the gate's
|
|
13
|
+
* `file(line,col): error TSxxxx:` pattern, so the parsed-diagnostics map comes
|
|
14
|
+
* back EMPTY. Every quarantined file then satisfies "has no errors", and the
|
|
15
|
+
* gate reports:
|
|
16
|
+
*
|
|
17
|
+
* ```
|
|
18
|
+
* ❌ 370 quarantined file(s) now type-check and must leave the list:
|
|
19
|
+
* Remove them from typecheck-quarantine.json.
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Following that deletes 370 live quarantine entries on the word of a checker
|
|
23
|
+
* that compiled nothing. The comparison was not wrong — it was correct about an
|
|
24
|
+
* input it was never entitled to accept.
|
|
25
|
+
*
|
|
26
|
+
* ## Why "cannot measure" is a THIRD outcome, not a failure message
|
|
27
|
+
*
|
|
28
|
+
* This is the #3888 family: a control that could not establish its subject must
|
|
29
|
+
* not render identically to one that established it and found nothing. The
|
|
30
|
+
* sibling exemplar is the review gate's `UNDETERMINED` verdict in
|
|
31
|
+
* `check-skipped-required-checks.mjs`, whose own comment states the rule this
|
|
32
|
+
* module follows — resolving an unestablished subject to `success` "would be a
|
|
33
|
+
* new instance of exactly that".
|
|
34
|
+
*
|
|
35
|
+
* A gate whose only surface is an exit code cannot emit a third code that
|
|
36
|
+
* anything reads, so the fallback is fixed: **fail closed, with the denial as
|
|
37
|
+
* the first clause.** Never pass. A reader who skims one line must come away
|
|
38
|
+
* knowing the finding was NOT made, because the natural inference from silence
|
|
39
|
+
* about errors is that there were none.
|
|
40
|
+
*
|
|
41
|
+
* ## The message contract
|
|
42
|
+
*
|
|
43
|
+
* {@link cannotMeasure} takes the clauses rather than a finished string so that
|
|
44
|
+
* no call site can ship three of the four. Every message names:
|
|
45
|
+
*
|
|
46
|
+
* 1. the outcome, in a distinct word (`CANNOT MEASURE`);
|
|
47
|
+
* 2. the inference a reader would otherwise draw, DENIED in words;
|
|
48
|
+
* 3. why the measurement could not happen;
|
|
49
|
+
* 4. what to do — including, where one exists, the destructive remedy that
|
|
50
|
+
* must NOT be followed.
|
|
51
|
+
*
|
|
52
|
+
* Clause 2 is the one that is easy to drop and the only one that stops a
|
|
53
|
+
* destructive edit, so it is a required parameter.
|
|
54
|
+
*
|
|
55
|
+
* @module scripts/lib/dependency-tree
|
|
56
|
+
*/
|
|
57
|
+
import { existsSync } from "node:fs";
|
|
58
|
+
import path from "node:path";
|
|
59
|
+
|
|
60
|
+
/** The install command that repairs an unresolvable tree. */
|
|
61
|
+
export const INSTALL_COMMAND = "bun install --frozen-lockfile";
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The distinct outcome word. Deliberately not "ERROR" or "FAILED": those are
|
|
65
|
+
* what the gate prints when it DID measure and found something, which is the
|
|
66
|
+
* confusion this whole module exists to prevent.
|
|
67
|
+
*/
|
|
68
|
+
export const CANNOT_MEASURE = "CANNOT MEASURE";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Names the dependencies a gate needs that are not present under `root`.
|
|
72
|
+
*
|
|
73
|
+
* Presence is tested at the exact path the gate will spawn or import, not by
|
|
74
|
+
* asking whether `node_modules` exists. A partially installed tree — the case
|
|
75
|
+
* that produced the `knip` sibling of this defect, where `node_modules` was
|
|
76
|
+
* present but two binaries were not — has a `node_modules` directory and still
|
|
77
|
+
* cannot answer the question.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} root - Directory the gate resolves dependencies against.
|
|
80
|
+
* @param {ReadonlyArray<string>} relativePaths - Paths under `root` to require,
|
|
81
|
+
* each already spelled the way the gate will use it.
|
|
82
|
+
* @returns {string[]} The missing paths, in the order given.
|
|
83
|
+
*/
|
|
84
|
+
export function missingDependencies(root, relativePaths) {
|
|
85
|
+
return relativePaths.filter(
|
|
86
|
+
relative => !existsSync(path.join(root, relative))
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* True when a child process signalled failure without producing anything the
|
|
92
|
+
* caller could read as a finding.
|
|
93
|
+
*
|
|
94
|
+
* This is the general form of the defect, and it catches more than an empty
|
|
95
|
+
* `node_modules`: a broken `tsconfig`, an out-of-memory compiler, a crash. The
|
|
96
|
+
* three cases are distinguishable in principle and identical in what they
|
|
97
|
+
* license — nothing.
|
|
98
|
+
*
|
|
99
|
+
* The one ambiguous input resolves the safe way. If a compiler ever emitted
|
|
100
|
+
* real diagnostics in a format the caller's pattern did not match, this reports
|
|
101
|
+
* "could not measure" rather than "no errors" — and "I could not read the
|
|
102
|
+
* output" is the honest account of that too.
|
|
103
|
+
*
|
|
104
|
+
* A zero exit with no diagnostics is NOT this: that is a clean run, and the
|
|
105
|
+
* caller is entitled to report it.
|
|
106
|
+
*
|
|
107
|
+
* @param {number | null} status - The child's exit status.
|
|
108
|
+
* @param {number} findingCount - How many findings the caller parsed out.
|
|
109
|
+
* @returns {boolean} Whether the run was vacuous.
|
|
110
|
+
*/
|
|
111
|
+
export function ranVacuously(status, findingCount) {
|
|
112
|
+
return status !== 0 && findingCount === 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Builds the operator-readable `CANNOT MEASURE` block.
|
|
117
|
+
*
|
|
118
|
+
* Returned rather than printed so callers can route it to stderr, a report, or
|
|
119
|
+
* a test assertion without this module deciding.
|
|
120
|
+
*
|
|
121
|
+
* @param {object} clauses - The four required clauses.
|
|
122
|
+
* @param {string} clauses.gate - The gate's own name, as it prints elsewhere.
|
|
123
|
+
* @param {string} clauses.denial - What this is NOT reporting, phrased as the
|
|
124
|
+
* inference a reader would otherwise draw. Required: it is the clause that
|
|
125
|
+
* stops a destructive edit.
|
|
126
|
+
* @param {string} clauses.because - Why the measurement could not happen.
|
|
127
|
+
* @param {string} clauses.remedy - What the reader should do instead.
|
|
128
|
+
* @returns {string} The block, newline-separated, no trailing newline.
|
|
129
|
+
*/
|
|
130
|
+
export function cannotMeasure({ gate, denial, because, remedy }) {
|
|
131
|
+
return [
|
|
132
|
+
`${CANNOT_MEASURE} — ${gate} did not run, and is NOT reporting ${denial}.`,
|
|
133
|
+
`Why: ${because}`,
|
|
134
|
+
`Do this: ${remedy}`,
|
|
135
|
+
].join("\n");
|
|
136
|
+
}
|
|
@@ -126,6 +126,51 @@ if ! command -v jq >/dev/null 2>&1; then
|
|
|
126
126
|
exit 1
|
|
127
127
|
fi
|
|
128
128
|
|
|
129
|
+
# The DEPENDENCY TREE is proved here too, for the same reason and one worse
|
|
130
|
+
# consequence (CodySwannGT/lisa#3913).
|
|
131
|
+
#
|
|
132
|
+
# Every gate below resolves its tool out of `node_modules` — knip, vitest, tsc.
|
|
133
|
+
# An empty tree does not stop those gates; it makes them answer from nothing,
|
|
134
|
+
# and an empty answer is not silent. Upstream, the `type-correctness` gate is
|
|
135
|
+
# what showed this belongs here rather than inside each gate: with no compiler
|
|
136
|
+
# to run it parsed zero diagnostics, concluded from that emptiness that every
|
|
137
|
+
# quarantined file now compiles, and instructed a reader to delete the
|
|
138
|
+
# quarantine entries protecting them — a destructive edit ordered by a control
|
|
139
|
+
# whose inputs never resolved. Its sibling `knip:check` reports unlisted
|
|
140
|
+
# binaries from the identical cause. That one merely LOOKS odd and nobody acts
|
|
141
|
+
# on it, which is exactly why it went unfixed while the dangerous one hid
|
|
142
|
+
# behind it.
|
|
143
|
+
#
|
|
144
|
+
# So this is not a convenience check for a missing install. Absence of the
|
|
145
|
+
# dependencies and absence of findings must not produce the same observable. A
|
|
146
|
+
# check that cannot RUN is not a skip and not a pass — it blocks.
|
|
147
|
+
if [ -f package.json ]; then
|
|
148
|
+
LISA_DECLARED_DEPS="$(jq -r '((.dependencies // {}) + (.devDependencies // {})) | length' package.json 2>/dev/null)"
|
|
149
|
+
# A non-numeric or absent answer counts as zero: this guard may not be the
|
|
150
|
+
# thing that blocks a push over an unreadable package.json.
|
|
151
|
+
case "$LISA_DECLARED_DEPS" in
|
|
152
|
+
"" | *[!0-9]*) LISA_DECLARED_DEPS=0 ;;
|
|
153
|
+
esac
|
|
154
|
+
# `ls -A` covers absent and present-but-empty in one test, which is the
|
|
155
|
+
# distinction the gates themselves cannot make.
|
|
156
|
+
if [ "$LISA_DECLARED_DEPS" -gt 0 ] && [ -z "$(ls -A node_modules 2>/dev/null)" ]; then
|
|
157
|
+
echo "❌ Push blocked: CANNOT MEASURE — dependencies are not installed."
|
|
158
|
+
echo ""
|
|
159
|
+
echo " package.json declares $LISA_DECLARED_DEPS dependencies and node_modules is"
|
|
160
|
+
echo " absent or empty, so the gates below cannot resolve the tools they"
|
|
161
|
+
echo " run: tsc, knip and vitest."
|
|
162
|
+
echo ""
|
|
163
|
+
echo " This is NOT a finding about your code, and nothing below it is"
|
|
164
|
+
echo " either."
|
|
165
|
+
echo ""
|
|
166
|
+
echo "To fix:"
|
|
167
|
+
echo " bun install --frozen-lockfile # or your package manager's equivalent"
|
|
168
|
+
echo ""
|
|
169
|
+
exit 1
|
|
170
|
+
fi
|
|
171
|
+
unset LISA_DECLARED_DEPS
|
|
172
|
+
fi
|
|
173
|
+
|
|
129
174
|
# BEGIN: push destination guard
|
|
130
175
|
# ---------------------------------------------------------------------------
|
|
131
176
|
# Refuse a push whose RESOLVED destination is a deploy branch it was not aimed
|