@codyswann/lisa 4.73.8 → 4.74.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 +52 -3
- package/all/copy-overwrite/scripts/lib/github-attestation-recovery.mjs +48 -35
- package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +15 -1
- package/all/copy-overwrite/scripts/lib/npm-update-hosted-hook.mjs +56 -27
- package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +78 -1
- package/all/copy-overwrite/scripts/lib/npm-update-native-process.mjs +29 -10
- package/all/copy-overwrite/scripts/lisa-automation-provenance.mjs +86 -37
- package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +11 -8
- package/all/create-only/.github/workflows/continuous-gates.yml +2 -2
- package/all/create-only/.github/workflows/workflow-load-failure-sweep.yml +1 -1
- package/cdk/create-only/.github/workflows/ci.yml +2 -2
- package/cdk/create-only/.github/workflows/deploy.yml +3 -3
- package/cdk/package-lisa/package.lisa.json +1 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +26 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/lisa-owned-templates.d.ts +9 -2
- package/dist/core/lisa-owned-templates.d.ts.map +1 -1
- package/dist/core/lisa-owned-templates.js +13 -3
- package/dist/core/lisa-owned-templates.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 +59 -37
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/copy-overwrite/jest.setup.pre.js +15 -1
- package/expo/create-only/.github/workflows/ci.yml +3 -3
- package/expo/create-only/.github/workflows/deploy.yml +4 -4
- package/expo/create-only/.github/workflows/nightly-e2e-bypass-reaper.yml +1 -1
- package/expo/create-only/.github/workflows/playwright-e2e.yml +1 -1
- package/expo/create-only/eas.json +1 -1
- package/harper-fabric/copy-overwrite/.github/workflows/ci.yml +1 -1
- package/harper-fabric/create-only/.github/workflows/deploy.yml +3 -3
- package/harper-fabric/create-only/.github/workflows/zap-baseline.yml +1 -1
- package/harper-fabric/package-lisa/package.lisa.json +2 -2
- package/nestjs/create-only/.github/workflows/ci.yml +2 -2
- package/nestjs/create-only/.github/workflows/deploy.yml +5 -5
- package/package.json +6 -6
- package/phaser/copy-overwrite/.github/workflows/ci.yml +1 -1
- package/phaser/package-lisa/package.lisa.json +2 -2
- 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-parity-sentry-sdk-setup/SKILL.md +104 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
- package/plugins/lisa/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
- package/plugins/lisa/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
- package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
- 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/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
- package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
- package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
- 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/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
- package/plugins/src/base/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
- package/scripts/check-shell-guard-refusal-coverage.mjs +65 -7
- package/scripts/generate-lisa-owned-hash-ledger.mjs +3 -0
- package/scripts/lib/shell-guard-shard-runner.mjs +381 -0
- package/scripts/lib/shell-guard-shards.mjs +383 -0
- package/scripts/lib/shell-guard-terminal-reporter.mjs +89 -0
- package/scripts/two-channel-couplings.json +1 -1
- package/scripts/update-node-version.ts +52 -6
- package/typescript/copy-overwrite/.nvmrc +1 -1
- package/typescript/create-only/.github/workflows/ci.yml +1 -1
- package/typescript/create-only/.github/workflows/review-evidence.yml +1 -1
- package/typescript/create-only/.github/workflows/third-party-review-evidence.yml +1 -1
- package/typescript/package-lisa/package.lisa.json +1 -1
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/** Validate exhaustive native-shard receipts before one original guard judge. */
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { tracerIndex } from "./shell-guard-refusal-coverage.mjs";
|
|
6
|
+
|
|
7
|
+
export const sha256 = bytes => createHash("sha256").update(bytes).digest("hex");
|
|
8
|
+
const encoded = value => JSON.stringify(value);
|
|
9
|
+
const nonnegativeInteger = value => Number.isSafeInteger(value) && value >= 0;
|
|
10
|
+
export const identityHash = value => sha256(encoded(value));
|
|
11
|
+
const hex = (value, count) =>
|
|
12
|
+
typeof value === "string" &&
|
|
13
|
+
new RegExp(`^[a-f0-9]{${count}}$`, "u").test(value);
|
|
14
|
+
const relative = file =>
|
|
15
|
+
typeof file === "string" &&
|
|
16
|
+
file.length > 0 &&
|
|
17
|
+
!file.includes("\\") &&
|
|
18
|
+
!path.posix.isAbsolute(file) &&
|
|
19
|
+
path.posix.normalize(file) === file &&
|
|
20
|
+
!file.split("/").some(part => part === ".." || part === "." || part === "");
|
|
21
|
+
|
|
22
|
+
export function parseShard(value) {
|
|
23
|
+
const match = /^(\d+)\/(\d+)$/u.exec(value ?? "");
|
|
24
|
+
assert(match, "shard must be I/3");
|
|
25
|
+
const index = Number(match[1]),
|
|
26
|
+
total = Number(match[2]);
|
|
27
|
+
assert(
|
|
28
|
+
Number.isSafeInteger(index) && total === 3 && index >= 1 && index <= total,
|
|
29
|
+
"invalid shard index/total"
|
|
30
|
+
);
|
|
31
|
+
return { index, total };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function validateSnapshot(snapshot) {
|
|
35
|
+
assert(hex(snapshot.head, 40) && hex(snapshot.tree, 40));
|
|
36
|
+
assert(
|
|
37
|
+
snapshot.provider &&
|
|
38
|
+
/^\d+$/u.test(snapshot.provider.runId) &&
|
|
39
|
+
/^\d+$/u.test(snapshot.provider.attempt)
|
|
40
|
+
);
|
|
41
|
+
assert(
|
|
42
|
+
hex(snapshot.provider.sourceSha, 40) &&
|
|
43
|
+
snapshot.provider.sourceSha === snapshot.head
|
|
44
|
+
);
|
|
45
|
+
assert(
|
|
46
|
+
hex(snapshot.provider.prHead, 40) &&
|
|
47
|
+
typeof snapshot.provider.workflowRef === "string" &&
|
|
48
|
+
snapshot.provider.workflowRef.length > 0
|
|
49
|
+
);
|
|
50
|
+
assert(Array.isArray(snapshot.candidates) && snapshot.candidates.length > 0);
|
|
51
|
+
const paths = snapshot.candidates.map(row => row.path);
|
|
52
|
+
assert(new Set(paths).size === paths.length && paths.every(relative));
|
|
53
|
+
assert(
|
|
54
|
+
paths.every((file, at) => at === 0 || paths[at - 1].localeCompare(file) < 0)
|
|
55
|
+
);
|
|
56
|
+
assert(snapshot.candidates.every(row => hex(row.sha256, 64)));
|
|
57
|
+
assert(Array.isArray(snapshot.population) && snapshot.population.length > 0);
|
|
58
|
+
assert(
|
|
59
|
+
new Set(snapshot.population.map(row => row.path)).size ===
|
|
60
|
+
snapshot.population.length
|
|
61
|
+
);
|
|
62
|
+
assert(
|
|
63
|
+
snapshot.population.every(
|
|
64
|
+
row =>
|
|
65
|
+
relative(row.path) &&
|
|
66
|
+
hex(row.sha256, 64) &&
|
|
67
|
+
nonnegativeInteger(row.size) &&
|
|
68
|
+
typeof row.canRefuse === "boolean" &&
|
|
69
|
+
typeof row.toolBoundary === "boolean"
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
assert(snapshot.candidateHash === identityHash(snapshot.candidates));
|
|
73
|
+
assert(snapshot.populationHash === identityHash(snapshot.population));
|
|
74
|
+
assert(
|
|
75
|
+
Object.keys(snapshot.scriptHashes).length > 0 &&
|
|
76
|
+
Object.entries(snapshot.scriptHashes).every(
|
|
77
|
+
([file, hash]) => relative(file) && hex(hash, 64)
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
assert(snapshot.runtime && Object.keys(snapshot.runtime).length > 0);
|
|
81
|
+
return snapshot;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function reportedPath(root, file) {
|
|
85
|
+
assert(
|
|
86
|
+
typeof file === "string" && path.isAbsolute(file),
|
|
87
|
+
"native reporter path must be absolute"
|
|
88
|
+
);
|
|
89
|
+
const rel = path.relative(root, file).split(path.sep).join("/");
|
|
90
|
+
assert(relative(rel), "reported file outside checkout");
|
|
91
|
+
return rel;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** JSON suite counters include nested suites. File identity is testResults[].name. */
|
|
95
|
+
export function completeReportedFiles(root, report, terminal) {
|
|
96
|
+
assert(
|
|
97
|
+
report &&
|
|
98
|
+
report.success === true &&
|
|
99
|
+
report.numFailedTests === 0 &&
|
|
100
|
+
report.numFailedTestSuites === 0,
|
|
101
|
+
"native JSON report unsuccessful/incomplete"
|
|
102
|
+
);
|
|
103
|
+
assert(
|
|
104
|
+
Array.isArray(report.testResults) && report.testResults.length > 0,
|
|
105
|
+
"missing native file report"
|
|
106
|
+
);
|
|
107
|
+
assert(
|
|
108
|
+
terminal &&
|
|
109
|
+
terminal.schema === 1 &&
|
|
110
|
+
terminal.complete === true &&
|
|
111
|
+
terminal.reason === "passed" &&
|
|
112
|
+
terminal.unhandledErrorCount === 0 &&
|
|
113
|
+
Array.isArray(terminal.files),
|
|
114
|
+
"missing/incomplete lifecycle receipt"
|
|
115
|
+
);
|
|
116
|
+
const files = report.testResults.map(file => {
|
|
117
|
+
assert(
|
|
118
|
+
file.status === "passed" && Array.isArray(file.assertionResults),
|
|
119
|
+
"native file failed/malformed"
|
|
120
|
+
);
|
|
121
|
+
assert(
|
|
122
|
+
file.assertionResults.every(
|
|
123
|
+
test =>
|
|
124
|
+
["passed", "skipped", "todo"].includes(test.status) &&
|
|
125
|
+
Array.isArray(test.failureMessages) &&
|
|
126
|
+
test.failureMessages.length === 0
|
|
127
|
+
),
|
|
128
|
+
"native assertion failed/pending/malformed"
|
|
129
|
+
);
|
|
130
|
+
return {
|
|
131
|
+
path: reportedPath(root, file.name),
|
|
132
|
+
assertions: file.assertionResults.length,
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
assert(
|
|
136
|
+
new Set(files.map(file => file.path)).size === files.length,
|
|
137
|
+
"duplicate native file"
|
|
138
|
+
);
|
|
139
|
+
const lifecycle = terminal.files.map(file => {
|
|
140
|
+
assert(
|
|
141
|
+
file.complete === true &&
|
|
142
|
+
Number.isSafeInteger(file.tests) &&
|
|
143
|
+
file.tests >= 0,
|
|
144
|
+
"unresolved native task"
|
|
145
|
+
);
|
|
146
|
+
assert(
|
|
147
|
+
Array.isArray(file.tasks) &&
|
|
148
|
+
file.tasks.length > 0 &&
|
|
149
|
+
file.tasks.every(terminalTask),
|
|
150
|
+
"unresolved native task state"
|
|
151
|
+
);
|
|
152
|
+
assert(
|
|
153
|
+
file.tasks.filter(task => task.type === "test").length === file.tests
|
|
154
|
+
);
|
|
155
|
+
assert(["passed", "skipped"].includes(file.moduleState));
|
|
156
|
+
assert(
|
|
157
|
+
Array.isArray(file.cases) &&
|
|
158
|
+
file.cases.length === file.tests &&
|
|
159
|
+
file.cases.every(test => ["passed", "skipped"].includes(test.state)),
|
|
160
|
+
"pending/failed native case"
|
|
161
|
+
);
|
|
162
|
+
return { path: reportedPath(root, file.name), assertions: file.tests };
|
|
163
|
+
});
|
|
164
|
+
const sort = rows =>
|
|
165
|
+
rows.slice().sort((a, b) => a.path.localeCompare(b.path));
|
|
166
|
+
assert.deepEqual(
|
|
167
|
+
sort(files),
|
|
168
|
+
sort(lifecycle),
|
|
169
|
+
"JSON/lifecycle file roster mismatch"
|
|
170
|
+
);
|
|
171
|
+
for (const roster of [terminal.queuedFiles, terminal.collectedFiles]) {
|
|
172
|
+
assert(Array.isArray(roster) && new Set(roster).size === roster.length);
|
|
173
|
+
assert.deepEqual(
|
|
174
|
+
roster.map(file => reportedPath(root, file)).sort(),
|
|
175
|
+
lifecycle.map(file => file.path).sort(),
|
|
176
|
+
"native queued/collected/ended roster mismatch"
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
assert(
|
|
180
|
+
report.numTotalTests ===
|
|
181
|
+
files.reduce((count, file) => count + file.assertions, 0)
|
|
182
|
+
);
|
|
183
|
+
return files.map(file => file.path).sort((a, b) => a.localeCompare(b));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function terminalTask(task) {
|
|
187
|
+
return (
|
|
188
|
+
task &&
|
|
189
|
+
["test", "suite"].includes(task.type) &&
|
|
190
|
+
(["pass", "skip"].includes(task.state) ||
|
|
191
|
+
(task.state === "todo" && task.mode === "todo") ||
|
|
192
|
+
(task.state === null && ["skip", "todo"].includes(task.mode)))
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Preserve null status and repeated records, but neither counts as extra proof. */
|
|
197
|
+
export function strictTrace(raw, population) {
|
|
198
|
+
assert(typeof raw === "string");
|
|
199
|
+
const current = new Map(population.map(guard => [guard.path, guard.sha256]));
|
|
200
|
+
for (const line of raw.split("\n")) {
|
|
201
|
+
if (line.trim() === "") continue;
|
|
202
|
+
const row = JSON.parse(line);
|
|
203
|
+
assert(
|
|
204
|
+
row &&
|
|
205
|
+
typeof row === "object" &&
|
|
206
|
+
current.has(row.script) &&
|
|
207
|
+
current.get(row.script) === row.sha256,
|
|
208
|
+
"unknown/stale guard observation"
|
|
209
|
+
);
|
|
210
|
+
assert(
|
|
211
|
+
row.status === null ||
|
|
212
|
+
(Number.isInteger(row.status) && row.status >= 0 && row.status <= 255),
|
|
213
|
+
"invalid child status"
|
|
214
|
+
);
|
|
215
|
+
assert(typeof row.origin === "string", "missing origin");
|
|
216
|
+
}
|
|
217
|
+
return raw;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function nativeArgv(root, artifactRoot, shard, candidates) {
|
|
221
|
+
return [
|
|
222
|
+
"run",
|
|
223
|
+
`--shard=${shard.index}/${shard.total}`,
|
|
224
|
+
"--reporter=dot",
|
|
225
|
+
"--reporter=json",
|
|
226
|
+
`--reporter=${path.join(root, "scripts/lib/shell-guard-terminal-reporter.mjs")}`,
|
|
227
|
+
`--outputFile.json=${path.join(artifactRoot, "results.json")}`,
|
|
228
|
+
...candidates.map(row => row.path),
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function validateBundle({
|
|
233
|
+
snapshot,
|
|
234
|
+
manifest,
|
|
235
|
+
reportRaw,
|
|
236
|
+
terminalRaw,
|
|
237
|
+
traceRaw,
|
|
238
|
+
indexRaw,
|
|
239
|
+
executionRaw,
|
|
240
|
+
stdoutRaw,
|
|
241
|
+
stderrRaw,
|
|
242
|
+
}) {
|
|
243
|
+
validateSnapshot(snapshot);
|
|
244
|
+
assert(manifest && manifest.schema === 1 && manifest.complete === true);
|
|
245
|
+
assert.deepEqual(
|
|
246
|
+
manifest.snapshot,
|
|
247
|
+
snapshot,
|
|
248
|
+
"stale source/run/attempt/runtime/roster identity"
|
|
249
|
+
);
|
|
250
|
+
parseShard(`${manifest.shard.index}/${manifest.shard.total}`);
|
|
251
|
+
assert(
|
|
252
|
+
manifest.child.status === 0 &&
|
|
253
|
+
manifest.child.signal === null &&
|
|
254
|
+
manifest.child.error === null,
|
|
255
|
+
"child failed/killed/not started"
|
|
256
|
+
);
|
|
257
|
+
assert(manifest.producerJob === "guard-trace", "wrong native producer job");
|
|
258
|
+
assert(
|
|
259
|
+
typeof manifest.artifactRoot === "string" &&
|
|
260
|
+
path.isAbsolute(manifest.artifactRoot)
|
|
261
|
+
);
|
|
262
|
+
assert(
|
|
263
|
+
manifest.child.command ===
|
|
264
|
+
path.join(manifest.checkoutRoot, "node_modules/.bin/vitest"),
|
|
265
|
+
"wrong child executable"
|
|
266
|
+
);
|
|
267
|
+
assert.deepEqual(
|
|
268
|
+
manifest.child.argv,
|
|
269
|
+
nativeArgv(
|
|
270
|
+
manifest.checkoutRoot,
|
|
271
|
+
manifest.artifactRoot,
|
|
272
|
+
manifest.shard,
|
|
273
|
+
snapshot.candidates
|
|
274
|
+
),
|
|
275
|
+
"incomplete/altered native argv"
|
|
276
|
+
);
|
|
277
|
+
assert(
|
|
278
|
+
Number.isFinite(Date.parse(manifest.child.startedAt)) &&
|
|
279
|
+
Number.isFinite(Date.parse(manifest.child.endedAt)) &&
|
|
280
|
+
Date.parse(manifest.child.endedAt) >=
|
|
281
|
+
Date.parse(manifest.child.startedAt),
|
|
282
|
+
"missing/reversed execution times"
|
|
283
|
+
);
|
|
284
|
+
assert(
|
|
285
|
+
manifest.traceSha256 === sha256(traceRaw) &&
|
|
286
|
+
manifest.reportSha256 === sha256(reportRaw) &&
|
|
287
|
+
manifest.terminalSha256 === sha256(terminalRaw),
|
|
288
|
+
"artifact digest mismatch"
|
|
289
|
+
);
|
|
290
|
+
assert(
|
|
291
|
+
typeof manifest.checkoutRoot === "string" &&
|
|
292
|
+
path.isAbsolute(manifest.checkoutRoot)
|
|
293
|
+
);
|
|
294
|
+
for (const [name, raw] of Object.entries({
|
|
295
|
+
"guard-index.json": indexRaw,
|
|
296
|
+
"execution.json": executionRaw,
|
|
297
|
+
"stdout.log": stdoutRaw,
|
|
298
|
+
"stderr.log": stderrRaw,
|
|
299
|
+
})) {
|
|
300
|
+
assert(
|
|
301
|
+
(typeof raw === "string" || Buffer.isBuffer(raw)) &&
|
|
302
|
+
manifest.rawHashes[name] === sha256(raw),
|
|
303
|
+
`raw receipt digest mismatch: ${name}`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
assert.deepEqual(
|
|
307
|
+
JSON.parse(indexRaw),
|
|
308
|
+
tracerIndex(snapshot.population),
|
|
309
|
+
"narrowed/stale tracer index"
|
|
310
|
+
);
|
|
311
|
+
assert(
|
|
312
|
+
sha256(indexRaw) ===
|
|
313
|
+
sha256(JSON.stringify(tracerIndex(snapshot.population))),
|
|
314
|
+
"canonical tracer index bytes changed"
|
|
315
|
+
);
|
|
316
|
+
assert.deepEqual(
|
|
317
|
+
JSON.parse(executionRaw),
|
|
318
|
+
manifest.child,
|
|
319
|
+
"execution record/manifest mismatch"
|
|
320
|
+
);
|
|
321
|
+
// Report paths belong to that producer's checkout; runners need not share a path.
|
|
322
|
+
const terminal = JSON.parse(terminalRaw);
|
|
323
|
+
const files = completeReportedFiles(
|
|
324
|
+
manifest.checkoutRoot,
|
|
325
|
+
JSON.parse(reportRaw),
|
|
326
|
+
terminal
|
|
327
|
+
);
|
|
328
|
+
assert(
|
|
329
|
+
Array.isArray(terminal.startedFiles) &&
|
|
330
|
+
new Set(terminal.startedFiles).size === terminal.startedFiles.length
|
|
331
|
+
);
|
|
332
|
+
// Vitest4.1.11 starts with ALL filtered specs BEFORE native sharding.
|
|
333
|
+
assert.deepEqual(
|
|
334
|
+
terminal.startedFiles
|
|
335
|
+
.map(file => reportedPath(manifest.checkoutRoot, file))
|
|
336
|
+
.sort((a, b) => a.localeCompare(b)),
|
|
337
|
+
snapshot.candidates.map(row => row.path),
|
|
338
|
+
"complete pre-shard native collection differs from canonical candidates"
|
|
339
|
+
);
|
|
340
|
+
assert.deepEqual(manifest.reportedFiles, files);
|
|
341
|
+
assert(
|
|
342
|
+
files.every(file =>
|
|
343
|
+
snapshot.candidates.some(candidate => candidate.path === file)
|
|
344
|
+
),
|
|
345
|
+
"native filter collected a noncandidate file"
|
|
346
|
+
);
|
|
347
|
+
assert(manifest.reportedFileCount === files.length && files.length > 0);
|
|
348
|
+
strictTrace(traceRaw, snapshot.population);
|
|
349
|
+
return { files, trace: traceRaw, shard: manifest.shard };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function mergeBundles(snapshot, bundles) {
|
|
353
|
+
validateSnapshot(snapshot);
|
|
354
|
+
assert(
|
|
355
|
+
Array.isArray(bundles) && bundles.length === 3,
|
|
356
|
+
"all three complete shards required"
|
|
357
|
+
);
|
|
358
|
+
const seen = new Set(),
|
|
359
|
+
files = new Set(),
|
|
360
|
+
validated = [];
|
|
361
|
+
for (const bundle of bundles) {
|
|
362
|
+
const value = validateBundle({ ...bundle, snapshot });
|
|
363
|
+
assert(!seen.has(value.shard.index), "duplicate shard identity");
|
|
364
|
+
seen.add(value.shard.index);
|
|
365
|
+
for (const file of value.files) {
|
|
366
|
+
assert(!files.has(file), "duplicate reported file across shards");
|
|
367
|
+
files.add(file);
|
|
368
|
+
}
|
|
369
|
+
validated.push(value);
|
|
370
|
+
}
|
|
371
|
+
assert.deepEqual([...seen].sort(), [1, 2, 3]);
|
|
372
|
+
assert.deepEqual(
|
|
373
|
+
[...files].sort((a, b) => a.localeCompare(b)),
|
|
374
|
+
snapshot.candidates.map(row => row.path),
|
|
375
|
+
"missing/extra candidate files"
|
|
376
|
+
);
|
|
377
|
+
// ONE existing judge receives this trace. Empty individual traces are valid.
|
|
378
|
+
return `${validated
|
|
379
|
+
.sort((a, b) => a.shard.index - b.shard.index)
|
|
380
|
+
.map(value => value.trace.trimEnd())
|
|
381
|
+
.filter(Boolean)
|
|
382
|
+
.join("\n")}\n`;
|
|
383
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** Companion to the unchanged native JSON reporter: refuses unresolved tasks. */
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { terminalTask } from "./shell-guard-shards.mjs";
|
|
4
|
+
|
|
5
|
+
export default class TerminalReporter {
|
|
6
|
+
queuedFiles = [];
|
|
7
|
+
collectedFiles = [];
|
|
8
|
+
onTestModuleQueued(module) {
|
|
9
|
+
this.queuedFiles.push(module.moduleId);
|
|
10
|
+
}
|
|
11
|
+
onTestModuleCollected(module) {
|
|
12
|
+
this.collectedFiles.push(module.moduleId);
|
|
13
|
+
}
|
|
14
|
+
onTestRunStart(specifications) {
|
|
15
|
+
this.startedFiles = specifications.map(
|
|
16
|
+
specification => specification.moduleId
|
|
17
|
+
);
|
|
18
|
+
writeFileSync(
|
|
19
|
+
`${process.env.LISA_GUARD_TERMINAL_REPORT}.started.json`,
|
|
20
|
+
JSON.stringify({
|
|
21
|
+
schema: 1,
|
|
22
|
+
startedFiles: this.startedFiles,
|
|
23
|
+
startedAt: new Date().toISOString(),
|
|
24
|
+
})
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
onTestRunEnd(modules, unhandledErrors, reason) {
|
|
28
|
+
const files = modules.map(module => {
|
|
29
|
+
let tests = 0;
|
|
30
|
+
const tasks = [];
|
|
31
|
+
const inspect = task => {
|
|
32
|
+
if (task.type === "test") tests += 1;
|
|
33
|
+
tasks.push({
|
|
34
|
+
type: task.type,
|
|
35
|
+
mode: task.mode,
|
|
36
|
+
state: task.result?.state ?? null,
|
|
37
|
+
});
|
|
38
|
+
for (const child of task.tasks ?? []) inspect(child);
|
|
39
|
+
};
|
|
40
|
+
inspect(module.task);
|
|
41
|
+
const moduleState = module.state();
|
|
42
|
+
const cases = [...module.children.allTests()].map(test => ({
|
|
43
|
+
name: test.fullName,
|
|
44
|
+
mode: test.options.mode,
|
|
45
|
+
state: test.result().state,
|
|
46
|
+
}));
|
|
47
|
+
const complete =
|
|
48
|
+
["passed", "skipped"].includes(moduleState) &&
|
|
49
|
+
cases.every(test => ["passed", "skipped"].includes(test.state)) &&
|
|
50
|
+
tasks.every(terminalTask);
|
|
51
|
+
return {
|
|
52
|
+
name: module.moduleId,
|
|
53
|
+
moduleState,
|
|
54
|
+
tests,
|
|
55
|
+
complete,
|
|
56
|
+
tasks,
|
|
57
|
+
cases,
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
const complete =
|
|
61
|
+
reason === "passed" &&
|
|
62
|
+
files.length > 0 &&
|
|
63
|
+
files.every(file => file.complete) &&
|
|
64
|
+
Array.isArray(unhandledErrors) &&
|
|
65
|
+
unhandledErrors.length === 0 &&
|
|
66
|
+
Array.isArray(this.startedFiles) &&
|
|
67
|
+
JSON.stringify(this.queuedFiles.slice().sort()) ===
|
|
68
|
+
JSON.stringify(files.map(file => file.name).sort()) &&
|
|
69
|
+
JSON.stringify(this.collectedFiles.slice().sort()) ===
|
|
70
|
+
JSON.stringify(files.map(file => file.name).sort());
|
|
71
|
+
// Collector precreates an owned directory; the reporter path is explicit.
|
|
72
|
+
writeFileSync(
|
|
73
|
+
process.env.LISA_GUARD_TERMINAL_REPORT,
|
|
74
|
+
JSON.stringify({
|
|
75
|
+
schema: 1,
|
|
76
|
+
complete,
|
|
77
|
+
reason,
|
|
78
|
+
unhandledErrorCount: Array.isArray(unhandledErrors)
|
|
79
|
+
? unhandledErrors.length
|
|
80
|
+
: null,
|
|
81
|
+
startedFiles: this.startedFiles,
|
|
82
|
+
queuedFiles: this.queuedFiles,
|
|
83
|
+
collectedFiles: this.collectedFiles,
|
|
84
|
+
files,
|
|
85
|
+
endedAt: new Date().toISOString(),
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -22,6 +22,15 @@ import * as path from "node:path";
|
|
|
22
22
|
// Literals named once — each was repeated enough times that a typo in one
|
|
23
23
|
// copy would diverge silently.
|
|
24
24
|
const VERSION_REPLACEMENT = "$1{{version}}$2";
|
|
25
|
+
const PRESERVE_RUNTIME_MARKER = "lisa-preserve-node-version:";
|
|
26
|
+
|
|
27
|
+
// These workflows execute a separately qualified, hash-bound updater runtime.
|
|
28
|
+
// Their Node22 identities cannot advance with host defaults without a complete
|
|
29
|
+
// updater requalification (CodySwannGT/lisa#4367).
|
|
30
|
+
const QUALIFIED_UPDATER_WORKFLOWS = new Set([
|
|
31
|
+
".github/workflows/npm-updater.yml",
|
|
32
|
+
".github/workflows/npm-updater-runtime-qualification.yml",
|
|
33
|
+
]);
|
|
25
34
|
|
|
26
35
|
const PROJECT_ROOT = path.resolve(import.meta.dirname, "..");
|
|
27
36
|
|
|
@@ -74,6 +83,7 @@ function filesToUpdate(): readonly string[] {
|
|
|
74
83
|
encoding: "utf8",
|
|
75
84
|
})
|
|
76
85
|
.split("\0")
|
|
86
|
+
.filter(file => !QUALIFIED_UPDATER_WORKFLOWS.has(file))
|
|
77
87
|
.filter(
|
|
78
88
|
file =>
|
|
79
89
|
[
|
|
@@ -115,6 +125,40 @@ function getMajorVersion(version: string): string {
|
|
|
115
125
|
return version.split(".")[0];
|
|
116
126
|
}
|
|
117
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Replace shared defaults while retaining explicitly qualified workflow pins.
|
|
130
|
+
* @param content - Original configuration bytes
|
|
131
|
+
* @param pattern - Runtime expression with two ownership-preserving captures
|
|
132
|
+
* @param replacement - Resolved replacement containing $1 and $2 captures
|
|
133
|
+
* @param workflow - Whether inline workflow qualification markers apply
|
|
134
|
+
* @returns Updated bytes and the number of changed, non-qualified values
|
|
135
|
+
*/
|
|
136
|
+
function replaceRuntimeMatches(
|
|
137
|
+
content: string,
|
|
138
|
+
pattern: RegExp,
|
|
139
|
+
replacement: string,
|
|
140
|
+
workflow: boolean
|
|
141
|
+
): { content: string; changes: number } {
|
|
142
|
+
let changes = 0;
|
|
143
|
+
const updated = content.replace(
|
|
144
|
+
pattern,
|
|
145
|
+
(match: string, first: string, second: string, offset: number) => {
|
|
146
|
+
const lineEnd = content.indexOf("\n", offset);
|
|
147
|
+
const line = content.slice(
|
|
148
|
+
offset,
|
|
149
|
+
lineEnd < 0 ? content.length : lineEnd
|
|
150
|
+
);
|
|
151
|
+
if (workflow && line.includes(PRESERVE_RUNTIME_MARKER)) return match;
|
|
152
|
+
const next = replacement.replace(/\$([12])/g, (_token, group: string) =>
|
|
153
|
+
group === "1" ? first : second
|
|
154
|
+
);
|
|
155
|
+
changes += Number(next !== match);
|
|
156
|
+
return next;
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
return { content: updated, changes };
|
|
160
|
+
}
|
|
161
|
+
|
|
118
162
|
/**
|
|
119
163
|
* Updates a file with the new Node version
|
|
120
164
|
*/
|
|
@@ -150,12 +194,14 @@ function updateFile(
|
|
|
150
194
|
.replace("{{version}}", version)
|
|
151
195
|
.replace("{{major}}", majorVersion);
|
|
152
196
|
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
197
|
+
const result = replaceRuntimeMatches(
|
|
198
|
+
content,
|
|
199
|
+
pattern,
|
|
200
|
+
resolvedReplacement,
|
|
201
|
+
/\.ya?ml$/.test(filePath)
|
|
202
|
+
);
|
|
203
|
+
totalChanges += result.changes;
|
|
204
|
+
content = result.content;
|
|
159
205
|
});
|
|
160
206
|
|
|
161
207
|
if (content !== originalContent) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
24.21.0
|
|
@@ -90,7 +90,7 @@ jobs:
|
|
|
90
90
|
uses: actions/setup-node@v6
|
|
91
91
|
with:
|
|
92
92
|
package-manager-cache: false
|
|
93
|
-
node-version: ${{ vars.NODE_VERSION || '
|
|
93
|
+
node-version: ${{ vars.NODE_VERSION || '24.21.0' }}
|
|
94
94
|
|
|
95
95
|
- name: 🕵️ Prove review evidence, or a posted substitute, at this head
|
|
96
96
|
env:
|