@codyswann/lisa 4.4.21 → 4.6.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/all/copy-overwrite/scripts/lisa-gates.mjs +125 -33
- package/all/copy-overwrite/scripts/lisa-run-gates.mjs +112 -4
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +3 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +8 -4
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -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-cursor/.claude-plugin/plugin.json +1 -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/scripts/check-workflow-package-paths.mjs +218 -57
- package/scripts/lib/workflow-contract-probe.mjs +445 -0
- package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +75 -1
|
@@ -37,27 +37,39 @@
|
|
|
37
37
|
* and are never extracted. A host-relative fallback is exactly what made #2951
|
|
38
38
|
* invisible here, so it cannot be what rescues a step from this check.
|
|
39
39
|
*
|
|
40
|
-
* ##
|
|
40
|
+
* ## Existence is not the contract (#2982)
|
|
41
41
|
*
|
|
42
|
-
* Existence is necessary and NOT sufficient
|
|
43
|
-
*
|
|
44
|
-
* its contract with it: greening the path in #2951 produced a prover that
|
|
42
|
+
* Existence is necessary and NOT sufficient. A file can keep its path and
|
|
43
|
+
* change its contract: greening the path in #2951 produced a prover that
|
|
45
44
|
* scanned zero tracked files and reported success, which was strictly worse
|
|
46
|
-
* than the red it replaced. Only the PATH is legible from a workflow, so
|
|
47
|
-
* contract
|
|
48
|
-
* invent
|
|
49
|
-
*
|
|
45
|
+
* than the red it replaced. Only the PATH is legible from a workflow, so the
|
|
46
|
+
* contract needs a DECLARATION — which #2960 said it did not have and would not
|
|
47
|
+
* invent, and which #2982 added alongside the floor in the same file.
|
|
48
|
+
*
|
|
49
|
+
* Every package path a step resolves now declares what the artifact there is.
|
|
50
|
+
* `reference` means the step does not run it (a directory named in error
|
|
51
|
+
* prose), and existence is the whole claim. `executed` means the step runs it,
|
|
52
|
+
* and carries at least one probe: an invocation of the RELEASED artifact — out
|
|
53
|
+
* of the tarball, never this checkout, because running the local copy
|
|
54
|
+
* reproduces exactly the blindness #2951 had — plus a signal that proves the
|
|
55
|
+
* run was not vacuous. An undeclared path FAILS, so a new one forces a decision
|
|
56
|
+
* instead of inheriting silence. See `scripts/lib/workflow-contract-probe.mjs`
|
|
57
|
+
* for the two probe shapes and what each one's count means.
|
|
50
58
|
*
|
|
51
59
|
* ## Refusing to pass on nothing
|
|
52
60
|
*
|
|
53
|
-
* Discovering zero workflows,
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
61
|
+
* Discovering zero workflows, extracting zero package paths, or — wherever
|
|
62
|
+
* released artifacts are available to run — executing zero contract probes is
|
|
63
|
+
* exit 2 and never a clean pass. So is a registry it could not reach, and so is
|
|
64
|
+
* a probe killed at its deadline, because a killed child returns empty streams
|
|
65
|
+
* and would otherwise read as an artifact that emitted no signal. A gate that
|
|
66
|
+
* passes because it did nothing is the failure this file exists to prevent, and
|
|
67
|
+
* it would be perverse to reproduce it here. Every run prints how many paths it
|
|
68
|
+
* examined and how many probes it executed.
|
|
57
69
|
*
|
|
58
70
|
* CLI:
|
|
59
71
|
* node scripts/check-workflow-package-paths.mjs [--root <dir>]
|
|
60
|
-
* [--releases <n>] [--listing <file>] [--json]
|
|
72
|
+
* [--releases <n>] [--listing <file>] [--extracted <dir>] [--json]
|
|
61
73
|
*
|
|
62
74
|
* With no flags it reads the floor declared in
|
|
63
75
|
* `.github/workflow-package-floor.json`, the latest release, and one midpoint
|
|
@@ -65,15 +77,23 @@
|
|
|
65
77
|
* ad-hoc sweep rather than the promise CI enforces.
|
|
66
78
|
*
|
|
67
79
|
* `--listing` supplies release file listings from a JSON file instead of the
|
|
68
|
-
* registry (`{ "<version>": ["package/a", ...] }`)
|
|
69
|
-
*
|
|
80
|
+
* registry (`{ "<version>": ["package/a", ...] }`). It drives the path half
|
|
81
|
+
* offline and supplies no artifacts, so it runs no probes. `--extracted <dir>`
|
|
82
|
+
* supplies already-unpacked releases at `<dir>/<version>/package/...` and drives
|
|
83
|
+
* BOTH halves offline. The networked path is what CI runs, and it always
|
|
84
|
+
* probes.
|
|
70
85
|
*
|
|
71
86
|
* Exit codes:
|
|
72
|
-
* 0 — every step's package paths resolve
|
|
73
|
-
*
|
|
87
|
+
* 0 — every step's package paths resolve, and every released artifact a step
|
|
88
|
+
* executes still honours its declared contract.
|
|
89
|
+
* 1 — a step has no resolvable package path in some release, a resolved path
|
|
90
|
+
* declares no contract, a declaration names a path nothing references, or
|
|
91
|
+
* a released artifact violated its declared contract.
|
|
74
92
|
* 2 — operational: unknown flag, a flag missing its value, `--root` absent or
|
|
75
93
|
* not a git repository, git unavailable, zero workflows discovered, zero
|
|
76
|
-
* package paths extracted,
|
|
94
|
+
* package paths extracted, a malformed declaration, a probe killed at its
|
|
95
|
+
* deadline, zero probes executed where artifacts were available, or the
|
|
96
|
+
* registry could not be read.
|
|
77
97
|
*
|
|
78
98
|
* @module scripts/check-workflow-package-paths
|
|
79
99
|
*/
|
|
@@ -84,12 +104,17 @@ import {
|
|
|
84
104
|
readFileSync,
|
|
85
105
|
writeFileSync,
|
|
86
106
|
} from "node:fs";
|
|
87
|
-
import { mkdtempSync } from "node:fs";
|
|
107
|
+
import { mkdtempSync, statSync } from "node:fs";
|
|
88
108
|
import { tmpdir } from "node:os";
|
|
89
109
|
import path from "node:path";
|
|
90
110
|
|
|
91
111
|
import { boundedExecFileSync } from "./lib/bounded-spawn.mjs";
|
|
92
112
|
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
113
|
+
import {
|
|
114
|
+
declarationGaps,
|
|
115
|
+
probeReleases,
|
|
116
|
+
readContractDeclaration,
|
|
117
|
+
} from "./lib/workflow-contract-probe.mjs";
|
|
93
118
|
|
|
94
119
|
/** The package whose released layout a workflow may make claims about. */
|
|
95
120
|
const PACKAGE_NAME = "@codyswann/lisa";
|
|
@@ -130,7 +155,7 @@ const EXIT_OPERATIONAL = 2;
|
|
|
130
155
|
*/
|
|
131
156
|
function usage(message) {
|
|
132
157
|
process.stderr.write(
|
|
133
|
-
`${message}\n\nUsage: node scripts/check-workflow-package-paths.mjs [--root <dir>] [--releases <n>] [--listing <file>] [--json]\n`
|
|
158
|
+
`${message}\n\nUsage: node scripts/check-workflow-package-paths.mjs [--root <dir>] [--releases <n>] [--listing <file>] [--extracted <dir>] [--json]\n`
|
|
134
159
|
);
|
|
135
160
|
process.exit(EXIT_OPERATIONAL);
|
|
136
161
|
}
|
|
@@ -138,13 +163,14 @@ function usage(message) {
|
|
|
138
163
|
/**
|
|
139
164
|
* Parse argv into options.
|
|
140
165
|
* @param {readonly string[]} argv - Arguments after the script name
|
|
141
|
-
* @returns {{root: string, releases: number|null, listing: string|null, json: boolean}}
|
|
166
|
+
* @returns {{root: string, releases: number|null, listing: string|null, extracted: string|null, json: boolean}}
|
|
142
167
|
*/
|
|
143
168
|
function parseArgs(argv) {
|
|
144
169
|
const options = {
|
|
145
170
|
root: process.cwd(),
|
|
146
171
|
releases: null,
|
|
147
172
|
listing: null,
|
|
173
|
+
extracted: null,
|
|
148
174
|
json: false,
|
|
149
175
|
};
|
|
150
176
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -154,11 +180,17 @@ function parseArgs(argv) {
|
|
|
154
180
|
continue;
|
|
155
181
|
}
|
|
156
182
|
const value = argv[index + 1];
|
|
157
|
-
if (
|
|
183
|
+
if (
|
|
184
|
+
flag === "--root" ||
|
|
185
|
+
flag === "--releases" ||
|
|
186
|
+
flag === "--listing" ||
|
|
187
|
+
flag === "--extracted"
|
|
188
|
+
) {
|
|
158
189
|
if (value === undefined) usage(`${flag} needs a value`);
|
|
159
190
|
index += 1;
|
|
160
191
|
if (flag === "--root") options.root = path.resolve(value);
|
|
161
192
|
if (flag === "--listing") options.listing = path.resolve(value);
|
|
193
|
+
if (flag === "--extracted") options.extracted = path.resolve(value);
|
|
162
194
|
if (flag === "--releases") {
|
|
163
195
|
const parsed = Number.parseInt(value, 10);
|
|
164
196
|
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
@@ -322,38 +354,89 @@ function chooseReleases(versions, floor, recent) {
|
|
|
322
354
|
}
|
|
323
355
|
|
|
324
356
|
/**
|
|
325
|
-
* Read the
|
|
357
|
+
* Read the declaration that governs this check: the floor and the contracts.
|
|
358
|
+
* @remarks
|
|
359
|
+
* One file, two properties of the same promise. #2960 declared how far back the
|
|
360
|
+
* workflows keep working; #2982 declared what the artifacts there must still
|
|
361
|
+
* do. Splitting them into two files would give one property two declaration
|
|
362
|
+
* surfaces, which is this repository's recurring defect.
|
|
326
363
|
* @param {string} root - Repository root
|
|
327
|
-
* @returns {string
|
|
364
|
+
* @returns {{floor: string, contracts: Record<string, object>, fixtures: Record<string, object>}}
|
|
328
365
|
* @throws {Error} When the declaration is missing or malformed
|
|
329
366
|
*/
|
|
330
|
-
function
|
|
367
|
+
function readDeclaration(root) {
|
|
331
368
|
const file = path.join(root, FLOOR_CONFIG);
|
|
332
369
|
if (!existsSync(file)) {
|
|
333
370
|
throw new Error(
|
|
334
|
-
`${FLOOR_CONFIG} is missing. It declares the oldest release these workflows promise to keep working against; without it this check would be guessing, and a guessed floor is how a gate ends up measuring nothing.`
|
|
371
|
+
`${FLOOR_CONFIG} is missing. It declares the oldest release these workflows promise to keep working against, and what each artifact they resolve must still do; without it this check would be guessing, and a guessed floor is how a gate ends up measuring nothing.`
|
|
335
372
|
);
|
|
336
373
|
}
|
|
337
374
|
const declared = JSON.parse(readFileSync(file, "utf8"));
|
|
338
375
|
if (typeof declared.floor !== "string" || declared.floor.length === 0) {
|
|
339
376
|
throw new Error(`${FLOOR_CONFIG} has no string "floor"`);
|
|
340
377
|
}
|
|
341
|
-
|
|
378
|
+
const { contracts, fixtures } = readContractDeclaration(declared);
|
|
379
|
+
return { floor: declared.floor, contracts, fixtures };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Every file under a directory, as `/`-separated paths relative to it.
|
|
384
|
+
* @param {string} root - Directory to walk
|
|
385
|
+
* @param {string} prefix - Accumulated relative prefix
|
|
386
|
+
* @returns {readonly string[]} Relative file paths
|
|
387
|
+
*/
|
|
388
|
+
function walkFiles(root, prefix = "") {
|
|
389
|
+
return readdirSync(path.join(root, prefix), { withFileTypes: true }).flatMap(
|
|
390
|
+
entry => {
|
|
391
|
+
const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
392
|
+
return entry.isDirectory()
|
|
393
|
+
? walkFiles(root, relative)
|
|
394
|
+
: [`package/${relative}`];
|
|
395
|
+
}
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Read already-unpacked releases from `<dir>/<version>/package/`.
|
|
401
|
+
* @param {string} dir - Directory holding one subdirectory per version
|
|
402
|
+
* @returns {Record<string, {entries: readonly string[], root: string|null}>}
|
|
403
|
+
*/
|
|
404
|
+
function readExtractedReleases(dir) {
|
|
405
|
+
return Object.fromEntries(
|
|
406
|
+
readdirSync(dir, { withFileTypes: true })
|
|
407
|
+
.filter(entry => entry.isDirectory())
|
|
408
|
+
.map(entry => {
|
|
409
|
+
const packageRoot = path.join(dir, entry.name, "package");
|
|
410
|
+
return [
|
|
411
|
+
entry.name,
|
|
412
|
+
{ entries: walkFiles(packageRoot), root: packageRoot },
|
|
413
|
+
];
|
|
414
|
+
})
|
|
415
|
+
);
|
|
342
416
|
}
|
|
343
417
|
|
|
344
418
|
/**
|
|
345
|
-
*
|
|
419
|
+
* Download and unpack each chosen release, memoising both.
|
|
420
|
+
* @remarks
|
|
421
|
+
* The tarball is UNPACKED, not merely listed, because a contract probe has to
|
|
422
|
+
* run the released artifact. Measured 2026-08-24: three releases in the current
|
|
423
|
+
* window are ~30 MB compressed and ~175 MB unpacked, downloaded once and cached
|
|
424
|
+
* — the same order of cost #2960 already paid for the listings alone.
|
|
346
425
|
* @param {readonly string[]} chosen - Versions to read
|
|
347
|
-
* @param {string} cacheDir - Where
|
|
348
|
-
* @returns {Record<string, readonly string[]>}
|
|
426
|
+
* @param {string} cacheDir - Where releases are memoised
|
|
427
|
+
* @returns {Record<string, {entries: readonly string[], root: string}>}
|
|
349
428
|
*/
|
|
350
|
-
function
|
|
429
|
+
function fetchReleases(chosen, cacheDir) {
|
|
351
430
|
mkdirSync(cacheDir, { recursive: true });
|
|
352
|
-
const
|
|
431
|
+
const releases = {};
|
|
353
432
|
for (const version of chosen) {
|
|
433
|
+
const unpacked = path.join(cacheDir, version, "package");
|
|
354
434
|
const cached = path.join(cacheDir, `${version}.json`);
|
|
355
|
-
if (existsSync(cached)) {
|
|
356
|
-
|
|
435
|
+
if (existsSync(cached) && existsSync(unpacked)) {
|
|
436
|
+
releases[version] = {
|
|
437
|
+
entries: JSON.parse(readFileSync(cached, "utf8")),
|
|
438
|
+
root: unpacked,
|
|
439
|
+
};
|
|
357
440
|
continue;
|
|
358
441
|
}
|
|
359
442
|
const staging = mkdtempSync(path.join(tmpdir(), "lisa-release-"));
|
|
@@ -366,20 +449,25 @@ function fetchReleaseListings(chosen, cacheDir) {
|
|
|
366
449
|
if (tarball === undefined) {
|
|
367
450
|
throw new Error(`npm pack produced no tarball for ${version}`);
|
|
368
451
|
}
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
375
|
-
}
|
|
376
|
-
)
|
|
452
|
+
const archive = path.join(staging, tarball);
|
|
453
|
+
const entries = boundedExecFileSync("tar", ["-tzf", archive], {
|
|
454
|
+
encoding: "utf8",
|
|
455
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
456
|
+
})
|
|
377
457
|
.split("\n")
|
|
378
458
|
.filter(Boolean);
|
|
379
|
-
|
|
459
|
+
const destination = path.join(cacheDir, version);
|
|
460
|
+
mkdirSync(destination, { recursive: true });
|
|
461
|
+
boundedExecFileSync("tar", ["-xzf", archive, "-C", destination], {
|
|
462
|
+
timeout: 300_000,
|
|
463
|
+
});
|
|
464
|
+
if (!existsSync(unpacked) || !statSync(unpacked).isDirectory()) {
|
|
465
|
+
throw new Error(`unpacking ${version} produced no package/ directory`);
|
|
466
|
+
}
|
|
380
467
|
writeFileSync(cached, JSON.stringify(entries));
|
|
468
|
+
releases[version] = { entries, root: unpacked };
|
|
381
469
|
}
|
|
382
|
-
return
|
|
470
|
+
return releases;
|
|
383
471
|
}
|
|
384
472
|
|
|
385
473
|
/**
|
|
@@ -436,30 +524,62 @@ function main() {
|
|
|
436
524
|
);
|
|
437
525
|
}
|
|
438
526
|
|
|
439
|
-
let
|
|
527
|
+
let declaration;
|
|
528
|
+
let releases;
|
|
440
529
|
try {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
: fetchReleaseListings(
|
|
444
|
-
chooseReleases(
|
|
445
|
-
publishedVersions(),
|
|
446
|
-
readFloor(options.root),
|
|
447
|
-
options.releases
|
|
448
|
-
),
|
|
449
|
-
path.join(options.root, "node_modules", ".cache", "lisa-releases")
|
|
450
|
-
);
|
|
530
|
+
declaration = readDeclaration(options.root);
|
|
531
|
+
releases = readReleases(options, declaration.floor);
|
|
451
532
|
} catch (error) {
|
|
452
533
|
usage(
|
|
453
534
|
`Could not read released package layouts: ${error.message}. Refusing to report a clean scan that never ran.`
|
|
454
535
|
);
|
|
455
536
|
}
|
|
456
537
|
|
|
457
|
-
const
|
|
538
|
+
const listings = Object.fromEntries(
|
|
539
|
+
Object.entries(releases).map(([version, release]) => [
|
|
540
|
+
version,
|
|
541
|
+
release.entries,
|
|
542
|
+
])
|
|
543
|
+
);
|
|
544
|
+
const gaps = declarationGaps(claims.groups, declaration.contracts);
|
|
545
|
+
const probeable = Object.entries(releases).filter(
|
|
546
|
+
([, release]) => release.root !== null
|
|
547
|
+
);
|
|
548
|
+
const contracts =
|
|
549
|
+
gaps.undeclared.length > 0
|
|
550
|
+
? {
|
|
551
|
+
executed: 0,
|
|
552
|
+
violations: [],
|
|
553
|
+
deferred: [],
|
|
554
|
+
operational: [],
|
|
555
|
+
probed: [],
|
|
556
|
+
}
|
|
557
|
+
: probeReleases({
|
|
558
|
+
groups: claims.groups,
|
|
559
|
+
releases: probeable.map(([version, release]) => ({
|
|
560
|
+
version,
|
|
561
|
+
root: release.root,
|
|
562
|
+
contains: candidate => releaseContains(release.entries, candidate),
|
|
563
|
+
})),
|
|
564
|
+
contracts: declaration.contracts,
|
|
565
|
+
fixtures: declaration.fixtures,
|
|
566
|
+
workRoot: mkdtempSync(path.join(tmpdir(), "lisa-contract-probe-")),
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
const breakages = [
|
|
570
|
+
...findBreakages(claims.groups, listings),
|
|
571
|
+
...gaps.undeclared,
|
|
572
|
+
...gaps.stale,
|
|
573
|
+
...contracts.violations,
|
|
574
|
+
];
|
|
458
575
|
const report = {
|
|
459
576
|
workflowsExamined: claims.workflowCount,
|
|
460
577
|
packagePathsExamined: claims.pathCount,
|
|
461
578
|
stepsExamined: claims.groups.length,
|
|
462
579
|
releasesExamined: Object.keys(listings),
|
|
580
|
+
contractProbesExecuted: contracts.executed,
|
|
581
|
+
contractProbesDeferred: contracts.deferred,
|
|
582
|
+
contractProbesRun: contracts.probed,
|
|
463
583
|
unattributed: claims.unattributed,
|
|
464
584
|
breakages,
|
|
465
585
|
};
|
|
@@ -468,14 +588,55 @@ function main() {
|
|
|
468
588
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
469
589
|
} else {
|
|
470
590
|
process.stdout.write(
|
|
471
|
-
`Examined ${report.packagePathsExamined} package path(s) across ${report.stepsExamined} step(s) in ${report.workflowsExamined} workflow(s), against release(s) ${report.releasesExamined.join(", ")}.\n`
|
|
591
|
+
`Examined ${report.packagePathsExamined} package path(s) across ${report.stepsExamined} step(s) in ${report.workflowsExamined} workflow(s), against release(s) ${report.releasesExamined.join(", ")}. Executed ${report.contractProbesExecuted} contract probe(s).\n`
|
|
472
592
|
);
|
|
593
|
+
for (const line of contracts.deferred)
|
|
594
|
+
process.stdout.write(` · ${line}\n`);
|
|
473
595
|
for (const line of breakages) process.stdout.write(` ✗ ${line}\n`);
|
|
474
596
|
}
|
|
475
597
|
|
|
598
|
+
if (contracts.operational.length > 0) {
|
|
599
|
+
usage(
|
|
600
|
+
`Contract probes could not be performed:\n ${contracts.operational.join("\n ")}`
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
if (
|
|
604
|
+
probeable.length > 0 &&
|
|
605
|
+
contracts.executed === 0 &&
|
|
606
|
+
breakages.length === 0
|
|
607
|
+
) {
|
|
608
|
+
usage(
|
|
609
|
+
`Released artifacts were available and ZERO contract probes ran. An empty inspection and a satisfied contract look identical from here, so this is a failure rather than a pass. Declare a probe on the executed paths in ${FLOOR_CONFIG}.`
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
476
613
|
process.exit(breakages.length === 0 ? 0 : 1);
|
|
477
614
|
}
|
|
478
615
|
|
|
616
|
+
/**
|
|
617
|
+
* Resolve the releases to judge against, from whichever source was requested.
|
|
618
|
+
* @param {{listing: string|null, extracted: string|null, releases: number|null, root: string}} options - Parsed flags
|
|
619
|
+
* @param {string} floor - The declared compatibility floor
|
|
620
|
+
* @returns {Record<string, {entries: readonly string[], root: string|null}>} Releases
|
|
621
|
+
*/
|
|
622
|
+
function readReleases(options, floor) {
|
|
623
|
+
if (options.extracted !== null)
|
|
624
|
+
return readExtractedReleases(options.extracted);
|
|
625
|
+
if (options.listing !== null) {
|
|
626
|
+
const listing = JSON.parse(readFileSync(options.listing, "utf8"));
|
|
627
|
+
return Object.fromEntries(
|
|
628
|
+
Object.entries(listing).map(([version, entries]) => [
|
|
629
|
+
version,
|
|
630
|
+
{ entries, root: null },
|
|
631
|
+
])
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
return fetchReleases(
|
|
635
|
+
chooseReleases(publishedVersions(), floor, options.releases),
|
|
636
|
+
path.join(options.root, "node_modules", ".cache", "lisa-releases")
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
479
640
|
if (invokedAsScript(import.meta.url)) {
|
|
480
641
|
main();
|
|
481
642
|
}
|