@codyswann/lisa 4.45.14 → 4.45.16
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/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +12 -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 +10 -2
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +5 -5
- 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/run-artifact-checks.mjs +215 -0
- package/typescript/copy-contents/.husky/post-merge +114 -0
- package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +53 -17
- package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +71 -8
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Run the generated-artifact sub-checks and end with the FAILING one's words.
|
|
4
|
+
*
|
|
5
|
+
* `check:artifacts` runs six sub-checks in sequence and keeps going after one
|
|
6
|
+
* fails, which is correct — an operator wants every problem in one attempt, not
|
|
7
|
+
* one per attempt. The cost was that the failing sub-check's own diagnostic was
|
|
8
|
+
* then buried under the output of every sub-check that passed AFTER it, and the
|
|
9
|
+
* last thing printed before the summary belonged to an unrelated check that had
|
|
10
|
+
* nothing wrong with it.
|
|
11
|
+
*
|
|
12
|
+
* Measured on `origin/main` at 4.45.10 (CodySwannGT/lisa#3876). A commit whose
|
|
13
|
+
* staged bytes `lint-staged` reformatted was refused, and this is what the
|
|
14
|
+
* operator saw at the bottom of the transcript:
|
|
15
|
+
*
|
|
16
|
+
* ```
|
|
17
|
+
* $ node scripts/check-generated-artifact-merge-coverage.mjs
|
|
18
|
+
* generated-artifact merge coverage: 4 artifact(s) declared, 2 merge-driver …
|
|
19
|
+
* check:artifacts FAILED, and these are the checks that failed: upstream-evidence-manifest
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Forty lines above it, unread, the failing check had already said the whole
|
|
23
|
+
* answer — the file whose bytes moved, the cause, and both commands:
|
|
24
|
+
*
|
|
25
|
+
* ```
|
|
26
|
+
* Error: src/core/upstream-evidence-manifest.ts is stale.
|
|
27
|
+
* Its inputs moved after it was generated. …
|
|
28
|
+
* At commit time lint-staged reformats every staged file BEFORE this
|
|
29
|
+
* check runs, so a manifest generated first is stale by the time it is
|
|
30
|
+
* checked. Regenerate now, after the reformat.
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* The diagnosis was never the problem. Its POSITION was. The ticket's own note
|
|
34
|
+
* records the consequence: identifying the failing sub-check by exit code
|
|
35
|
+
* before reading anything had become "the reliable habit", which is a habit an
|
|
36
|
+
* operator should not need.
|
|
37
|
+
*
|
|
38
|
+
* So the ordering is inverted here: every passing sub-check's output first, the
|
|
39
|
+
* failing ones last, then the summary. Nothing is dropped and nothing is
|
|
40
|
+
* printed twice — the same bytes, in the order that puts the answer where a
|
|
41
|
+
* reader looks.
|
|
42
|
+
*
|
|
43
|
+
* ## Why the check names stay in `package.json`
|
|
44
|
+
*
|
|
45
|
+
* They are passed in as arguments rather than listed in this file, and that is
|
|
46
|
+
* load-bearing rather than stylistic. `undeclaredChecks` in
|
|
47
|
+
* `check-generated-artifact-merge-coverage.mjs` reads the `check:artifacts`
|
|
48
|
+
* SCRIPT BODY with `/\bcheck:[a-z0-9-]+/gu` and fails when a name appears there
|
|
49
|
+
* that no artifact declares. Moving the list in here would leave that regex
|
|
50
|
+
* matching nothing at all: the guard would pass, having examined an empty set,
|
|
51
|
+
* and a seventh sub-check could join `check:artifacts` unannounced. A guard
|
|
52
|
+
* that reports success by looking at nothing is the exact defect this
|
|
53
|
+
* repository keeps finding, so the list stays where its guard can see it.
|
|
54
|
+
* @module scripts/run-artifact-checks
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
import process from "node:process";
|
|
58
|
+
|
|
59
|
+
import { boundedSpawnSync, isChildTimeout } from "./lib/bounded-spawn.mjs";
|
|
60
|
+
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* How long one sub-check may run before the result is a timeout, not a verdict.
|
|
64
|
+
*
|
|
65
|
+
* Far above the work rather than near it. The whole aggregate completes in
|
|
66
|
+
* seconds on an idle box; this budget exists so that a sub-check killed on a
|
|
67
|
+
* saturated host is reported as KILLED rather than as a check that failed — a
|
|
68
|
+
* killed `spawnSync` child returns EMPTY streams, so without this the failure
|
|
69
|
+
* arrives looking exactly like a content problem and never mentions time.
|
|
70
|
+
*/
|
|
71
|
+
export const CHECK_BUDGET_MS = 900_000;
|
|
72
|
+
|
|
73
|
+
/** Printed immediately before the failing sub-checks' own output. */
|
|
74
|
+
export const FAILURE_HEADER =
|
|
75
|
+
"── below: what the check that FAILED actually said ──";
|
|
76
|
+
|
|
77
|
+
/** Said instead of an empty block when a failing sub-check printed nothing. */
|
|
78
|
+
export const NO_OUTPUT_NOTE =
|
|
79
|
+
" (this check produced no output at all — it did not merely fail its " +
|
|
80
|
+
"content check. A killed child returns empty streams, so treat an empty " +
|
|
81
|
+
"block as a possible kill rather than as a silent refusal.)";
|
|
82
|
+
|
|
83
|
+
/** Said when a sub-check exceeded its budget rather than reaching a verdict. */
|
|
84
|
+
export const TIMEOUT_NOTE =
|
|
85
|
+
" (this check exceeded its time budget and was killed. It reached no " +
|
|
86
|
+
"verdict, so nothing above is evidence about the artifacts.)";
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Run one sub-check and capture everything it said.
|
|
90
|
+
* @param {string} name - Package script name, e.g. `check:lisa-owned-hash-ledger`
|
|
91
|
+
* @returns {{name: string, ok: boolean, timedOut: boolean, output: string}} What it did and said.
|
|
92
|
+
*/
|
|
93
|
+
export function runCheck(name) {
|
|
94
|
+
try {
|
|
95
|
+
// `--silent` because bun otherwise echoes `$ <command>` to STDERR while
|
|
96
|
+
// the check writes its findings to STDOUT. Capturing both and printing
|
|
97
|
+
// them in stream order puts every banner after the output it introduces,
|
|
98
|
+
// which reads as though each check were labelled with its neighbour's
|
|
99
|
+
// name. The label this file prints is accurate by construction instead.
|
|
100
|
+
const result = boundedSpawnSync("bun", ["run", "--silent", name], {
|
|
101
|
+
encoding: "utf8",
|
|
102
|
+
timeout: CHECK_BUDGET_MS,
|
|
103
|
+
});
|
|
104
|
+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
105
|
+
return { name, ok: result.status === 0, timedOut: false, output };
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (isChildTimeout(error)) {
|
|
108
|
+
return { name, ok: false, timedOut: true, output: "" };
|
|
109
|
+
}
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The transcript, in the order that puts the answer where a reader looks.
|
|
116
|
+
*
|
|
117
|
+
* Passing sub-checks first, failing ones last, summary at the very end. A
|
|
118
|
+
* reader who scrolls to the bottom — which is every reader of a refused commit
|
|
119
|
+
* — lands on the check that has something to say rather than on one that does
|
|
120
|
+
* not.
|
|
121
|
+
* @param {Array<{name: string, ok: boolean, timedOut: boolean, output: string}>} results - One record per sub-check, in the order they ran
|
|
122
|
+
* @returns {{lines: string[], exitCode: number}} What to print, and what to exit with.
|
|
123
|
+
*/
|
|
124
|
+
export function renderReport(results) {
|
|
125
|
+
const failures = results.filter(result => !result.ok);
|
|
126
|
+
const passed = results.filter(result => result.ok);
|
|
127
|
+
const passedLines = passed.flatMap(result => [
|
|
128
|
+
`▸ ${result.name}`,
|
|
129
|
+
...trimmedLines(result.output),
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
if (failures.length === 0) {
|
|
133
|
+
return {
|
|
134
|
+
lines: [
|
|
135
|
+
...passedLines,
|
|
136
|
+
`check:artifacts OK - all ${String(results.length)} generated-artifact checks passed.`,
|
|
137
|
+
],
|
|
138
|
+
exitCode: 0,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const failureLines = failures.flatMap(result => [
|
|
143
|
+
"",
|
|
144
|
+
`▸ ${result.name}`,
|
|
145
|
+
...describeFailure(result),
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
lines: [
|
|
150
|
+
...passedLines,
|
|
151
|
+
"",
|
|
152
|
+
FAILURE_HEADER,
|
|
153
|
+
...failureLines,
|
|
154
|
+
"",
|
|
155
|
+
`check:artifacts FAILED, and these are the checks that failed: ${failures
|
|
156
|
+
.map(result => result.name.replace(/^check:/u, ""))
|
|
157
|
+
.join(" ")}`,
|
|
158
|
+
],
|
|
159
|
+
exitCode: 1,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* What one failing sub-check contributes to the report.
|
|
165
|
+
*
|
|
166
|
+
* An empty capture is NEVER rendered as an empty block. Two different events
|
|
167
|
+
* produce one — a check that printed nothing, and a child killed before it
|
|
168
|
+
* could print — and a blank gap reads as neither.
|
|
169
|
+
* @param {{timedOut: boolean, output: string}} result - One failing sub-check's record
|
|
170
|
+
* @returns {string[]} Its output, or the note that explains the absence of any.
|
|
171
|
+
*/
|
|
172
|
+
function describeFailure(result) {
|
|
173
|
+
if (result.timedOut) return [TIMEOUT_NOTE];
|
|
174
|
+
const lines = trimmedLines(result.output);
|
|
175
|
+
return lines.length === 0 ? [NO_OUTPUT_NOTE] : lines;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* One captured stream as lines, without the trailing blank a final newline adds.
|
|
180
|
+
* @param {string} output - Captured combined output
|
|
181
|
+
* @returns {string[]} Its lines, empty when it said nothing.
|
|
182
|
+
*/
|
|
183
|
+
function trimmedLines(output) {
|
|
184
|
+
const trimmed = output.replace(/\n+$/u, "");
|
|
185
|
+
return trimmed === "" ? [] : trimmed.split("\n");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Run every named sub-check and report.
|
|
190
|
+
* @param {string[]} names - Package script names to run, in order
|
|
191
|
+
* @param {(name: string) => {name: string, ok: boolean, timedOut: boolean, output: string}} [run] - Injected runner, for tests
|
|
192
|
+
* @returns {{lines: string[], exitCode: number}} What to print, and what to exit with.
|
|
193
|
+
*/
|
|
194
|
+
export function runArtifactChecks(names, run = runCheck) {
|
|
195
|
+
return renderReport(names.map(name => run(name)));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (invokedAsScript(import.meta.url)) {
|
|
199
|
+
const names = process.argv.slice(2);
|
|
200
|
+
if (names.length === 0) {
|
|
201
|
+
// Refused rather than reported OK. An aggregate handed nothing to run
|
|
202
|
+
// would otherwise print "all 0 generated-artifact checks passed", which is
|
|
203
|
+
// the shape of every control in this repository that reported success
|
|
204
|
+
// having examined nothing.
|
|
205
|
+
console.error(
|
|
206
|
+
"run-artifact-checks.mjs: no checks were named, so nothing was run.\n" +
|
|
207
|
+
" This cannot pass by inspecting nothing. Pass the package script\n" +
|
|
208
|
+
" names to run, as `check:artifacts` in package.json does."
|
|
209
|
+
);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
const { lines, exitCode } = runArtifactChecks(names);
|
|
213
|
+
console.log(lines.join("\n"));
|
|
214
|
+
process.exit(exitCode);
|
|
215
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Report generated-artifact staleness the moment a MERGE creates it.
|
|
2
|
+
#
|
|
3
|
+
# WHY THIS HOOK EXISTS AT ALL
|
|
4
|
+
#
|
|
5
|
+
# `git merge` does not run `pre-commit`. Measured against a throwaway
|
|
6
|
+
# repository with all five hooks installed and echoing their own name
|
|
7
|
+
# (CodySwannGT/lisa#3876):
|
|
8
|
+
#
|
|
9
|
+
# plain commit → pre-commit, prepare-commit-msg, commit-msg, post-commit
|
|
10
|
+
# git merge → prepare-commit-msg, commit-msg, post-merge
|
|
11
|
+
#
|
|
12
|
+
# `pre-commit` is absent from the merge line, and it is the hook that runs the
|
|
13
|
+
# gate registry at the `commit` moment. So a project declaring
|
|
14
|
+
# `artifact-freshness` at `commit` has that gate skipped by exactly the event
|
|
15
|
+
# most likely to stale a generated artifact: a merge that moves the tracked
|
|
16
|
+
# files the artifact derives from.
|
|
17
|
+
#
|
|
18
|
+
# The staleness was still caught — by the pre-push suite, roughly ten minutes
|
|
19
|
+
# later, after the branch had been worked on further. The ticket measured three
|
|
20
|
+
# such stalls on one branch in one session, about thirty minutes of gate time
|
|
21
|
+
# for a change whose own tests were green throughout. The regeneration takes
|
|
22
|
+
# seconds. The cost is entirely in the finding out.
|
|
23
|
+
#
|
|
24
|
+
# `post-merge` is the one hook git DOES run there, and nothing in Lisa had ever
|
|
25
|
+
# used it.
|
|
26
|
+
#
|
|
27
|
+
# WHY IT REPORTS AND NEVER BLOCKS
|
|
28
|
+
#
|
|
29
|
+
# There is nothing left to block. The merge commit already exists by the time
|
|
30
|
+
# this runs, and git ignores this hook's exit code entirely — so a non-zero exit
|
|
31
|
+
# would buy no enforcement and would only look like one. This hook exists to
|
|
32
|
+
# move a discovery earlier, not to add a gate. The gates at commit, push and
|
|
33
|
+
# pull-request are unchanged and still refuse.
|
|
34
|
+
#
|
|
35
|
+
# WHY IT ASKS THE REGISTRY RATHER THAN NAMING A COMMAND
|
|
36
|
+
#
|
|
37
|
+
# Which files are generated is project-specific, which is why
|
|
38
|
+
# `artifact-freshness` ships `declareOnly`. Hard-coding `check:artifacts` here
|
|
39
|
+
# would name a script most projects do not have and would run one project's
|
|
40
|
+
# prover in every project. Instead this asks the gate registry what THIS
|
|
41
|
+
# project declared, and stands down in silence when the answer is nothing —
|
|
42
|
+
# which is the correct answer for a project that has no generated artifacts.
|
|
43
|
+
#
|
|
44
|
+
# A `post-merge` MOMENT in the registry proper was considered and is the purer
|
|
45
|
+
# shape: a gate would then declare that it runs here, the way it declares
|
|
46
|
+
# `commit`. It is not done in this change because a new moment touches the
|
|
47
|
+
# moment vocabulary, validation, the runner, doctor, and their suites — a
|
|
48
|
+
# larger change than the discovery-timing defect being fixed. This hook reuses
|
|
49
|
+
# the declaration a project already made instead of inventing a second place to
|
|
50
|
+
# declare it.
|
|
51
|
+
|
|
52
|
+
# CI merges through the forge, not through a working copy, and the gates run
|
|
53
|
+
# there in their own right. Running this there would add seconds to every job
|
|
54
|
+
# to re-report something a required check already refuses.
|
|
55
|
+
if [ -n "${CI:-}" ]; then
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
LISA_GATES="node_modules/@codyswann/lisa/all/copy-overwrite/scripts/lisa-gates.mjs"
|
|
60
|
+
if [ ! -f "$LISA_GATES" ]; then
|
|
61
|
+
LISA_GATES="scripts/lisa-gates.mjs"
|
|
62
|
+
fi
|
|
63
|
+
if [ ! -f "$LISA_GATES" ]; then
|
|
64
|
+
LISA_GATES="all/copy-overwrite/scripts/lisa-gates.mjs"
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
if [ ! -f "$LISA_GATES" ] || ! command -v node >/dev/null 2>&1; then
|
|
68
|
+
exit 0
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# The earliest moment the project declares this gate at, because the property
|
|
72
|
+
# is one property and its prover does not change with the moment. A project
|
|
73
|
+
# that declares it only at `pull-request` still wants to know now.
|
|
74
|
+
LISA_ARTIFACT_CHECK=""
|
|
75
|
+
for LISA_MOMENT in commit push pull-request; do
|
|
76
|
+
LISA_ARTIFACT_CHECK="$(
|
|
77
|
+
node "$LISA_GATES" list --moment="$LISA_MOMENT" 2>/dev/null |
|
|
78
|
+
awk '$2 == "artifact-freshness" { $1 = ""; $2 = ""; sub(/^[[:space:]]+/, ""); print; exit }'
|
|
79
|
+
)"
|
|
80
|
+
if [ -n "$LISA_ARTIFACT_CHECK" ]; then
|
|
81
|
+
break
|
|
82
|
+
fi
|
|
83
|
+
done
|
|
84
|
+
|
|
85
|
+
if [ -z "$LISA_ARTIFACT_CHECK" ]; then
|
|
86
|
+
exit 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
if sh -c "$LISA_ARTIFACT_CHECK" >/dev/null 2>&1; then
|
|
90
|
+
exit 0
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
echo ""
|
|
94
|
+
echo "⚠️ This merge left a generated artifact stale."
|
|
95
|
+
echo ""
|
|
96
|
+
echo " The merge moved tracked files that a generated artifact derives from,"
|
|
97
|
+
echo " so the artifact now describes bytes that are no longer there. Nothing"
|
|
98
|
+
echo " is blocked yet — but the commit, push and pull-request gates will all"
|
|
99
|
+
echo " refuse until it is regenerated, and the push gate is the expensive"
|
|
100
|
+
echo " place to find out."
|
|
101
|
+
echo ""
|
|
102
|
+
echo " Not every merge does this. This one did."
|
|
103
|
+
echo ""
|
|
104
|
+
echo " See what is stale:"
|
|
105
|
+
echo " $LISA_ARTIFACT_CHECK"
|
|
106
|
+
echo ""
|
|
107
|
+
echo " Then regenerate, in that order, AFTER 'git add' of the real changes —"
|
|
108
|
+
echo " the generators read TRACKED files, so regenerating first silently"
|
|
109
|
+
echo " omits whatever the commit is about to add."
|
|
110
|
+
echo ""
|
|
111
|
+
|
|
112
|
+
# Deliberately zero. See "WHY IT REPORTS AND NEVER BLOCKS" above: git ignores
|
|
113
|
+
# this exit code, so a non-zero one would be a refusal that refuses nothing.
|
|
114
|
+
exit 0
|
|
@@ -484,6 +484,45 @@ export const NEVER_BLOCKING = Object.freeze([
|
|
|
484
484
|
VIOLATIONS.reviewCarried,
|
|
485
485
|
]);
|
|
486
486
|
|
|
487
|
+
/**
|
|
488
|
+
* Kinds that stay report-only under EVERY flag, including `--fail-on-vacuous`.
|
|
489
|
+
*
|
|
490
|
+
* ## Why this is a named array rather than a condition
|
|
491
|
+
*
|
|
492
|
+
* These two are also members of {@link NEVER_BLOCKING}, and that array's own
|
|
493
|
+
* rule is `return policy.failOnVacuous` — so reaching it would make them block
|
|
494
|
+
* the moment the flag is passed. They were kept apart by an `if` immediately
|
|
495
|
+
* above the lookup, which meant **deleting or reordering one line silently
|
|
496
|
+
* converted two deliberately report-only findings into build failures**, with
|
|
497
|
+
* nothing in the suite noticing (CodySwannGT/lisa#3902). Naming the set makes
|
|
498
|
+
* the exemption a value that can be asserted rather than a position that
|
|
499
|
+
* happens to come first.
|
|
500
|
+
*
|
|
501
|
+
* The behaviour is unchanged: same two kinds, same answer, same place in the
|
|
502
|
+
* order. Only the reason it holds moved from a line's position into a name.
|
|
503
|
+
*
|
|
504
|
+
* ## The two policies, which are different and both deliberate
|
|
505
|
+
*
|
|
506
|
+
* `reviewWaived` is report-only under the owner's ruling on
|
|
507
|
+
* CodySwannGT/lisa#3221. A waived pull request is an unreviewed one, the
|
|
508
|
+
* operator must see it, and it must never fail the build. `--fail-on-vacuous`
|
|
509
|
+
* governs checks that claimed success without proving work; it must not turn a
|
|
510
|
+
* vendor entitlement waiver into a failure through a shared array.
|
|
511
|
+
*
|
|
512
|
+
* `reviewCarried` (CodySwannGT/lisa#3658) is report-only for a second reason on
|
|
513
|
+
* top of that one: it is about ANOTHER pull request, whose own gate already
|
|
514
|
+
* reached its own verdict on its own diff. `--fail-on-vacuous` asks "did the
|
|
515
|
+
* checks on THIS pull request prove their work?", and answering it with a
|
|
516
|
+
* constituent's condition would fail an author for a diff they did not write
|
|
517
|
+
* and cannot change.
|
|
518
|
+
*
|
|
519
|
+
* Changing either policy is out of scope here; this records that they hold.
|
|
520
|
+
*/
|
|
521
|
+
export const REPORT_ONLY = Object.freeze([
|
|
522
|
+
VIOLATIONS.reviewWaived,
|
|
523
|
+
VIOLATIONS.reviewCarried,
|
|
524
|
+
]);
|
|
525
|
+
|
|
487
526
|
/**
|
|
488
527
|
* Kinds the review gate blocks on, and only when a caller asks it to.
|
|
489
528
|
*
|
|
@@ -3436,11 +3475,17 @@ function writeGuardError(argv, error) {
|
|
|
3436
3475
|
/**
|
|
3437
3476
|
* Reads the CLI switches that change which findings block.
|
|
3438
3477
|
*
|
|
3478
|
+
* Exported so a test can prove `--fail-on-vacuous` reaches `failOnVacuous`.
|
|
3479
|
+
* Without that, a case asserting a finding does NOT block under the flag
|
|
3480
|
+
* cannot tell "the flag was live and chose not to block" from "the flag never
|
|
3481
|
+
* arrived" — and the second is the vacuous test this whole file is about
|
|
3482
|
+
* (CodySwannGT/lisa#3902).
|
|
3483
|
+
*
|
|
3439
3484
|
* @param {ReadonlyArray<string>} argv - Arguments
|
|
3440
3485
|
* @param {object} result - Guard result
|
|
3441
3486
|
* @returns {{warnOnly: boolean, failOnVacuous: boolean, requireReviewEvidence: boolean}} Active policy
|
|
3442
3487
|
*/
|
|
3443
|
-
function cliPolicy(argv, result) {
|
|
3488
|
+
export function cliPolicy(argv, result) {
|
|
3444
3489
|
return {
|
|
3445
3490
|
warnOnly: result.enforcement === "warn",
|
|
3446
3491
|
// The supported alternative to a per-consumer `--json` wrapper.
|
|
@@ -3454,29 +3499,20 @@ function cliPolicy(argv, result) {
|
|
|
3454
3499
|
/**
|
|
3455
3500
|
* True when one violation blocks under the active CLI policy.
|
|
3456
3501
|
*
|
|
3502
|
+
* Exported for the same reason `cliPolicy` is: the property
|
|
3503
|
+
* CodySwannGT/lisa#3902 asks for is a MATRIX — two kinds that must not block
|
|
3504
|
+
* under `--fail-on-vacuous` beside two that must — and a suite that could
|
|
3505
|
+
* produce only one half of it would pass while the other half rotted.
|
|
3506
|
+
*
|
|
3457
3507
|
* @param {{kind: string}} violation - One violation
|
|
3458
3508
|
* @param {{warnOnly: boolean, failOnVacuous: boolean, requireReviewEvidence: boolean}} policy - Active policy
|
|
3459
3509
|
* @returns {boolean} True when the finding blocks
|
|
3460
3510
|
*/
|
|
3461
|
-
function violationBlocks(violation, policy) {
|
|
3511
|
+
export function violationBlocks(violation, policy) {
|
|
3462
3512
|
if (REVIEW_GATE_BLOCKING.includes(violation.kind)) {
|
|
3463
3513
|
return policy.requireReviewEvidence;
|
|
3464
3514
|
}
|
|
3465
|
-
|
|
3466
|
-
// governs checks that claimed success without proving work; it must not
|
|
3467
|
-
// turn a vendor entitlement waiver into a failure through a shared array.
|
|
3468
|
-
//
|
|
3469
|
-
// A carried finding (#3658) is report-only for a second reason on top of
|
|
3470
|
-
// that one: it is about ANOTHER pull request, whose own gate already
|
|
3471
|
-
// reached its own verdict on its own diff. `--fail-on-vacuous` asks "did
|
|
3472
|
-
// the checks on THIS pull request prove their work?", and answering it with
|
|
3473
|
-
// a constituent's condition would fail an author for a diff they did not
|
|
3474
|
-
// write and cannot change.
|
|
3475
|
-
if (
|
|
3476
|
-
violation.kind === VIOLATIONS.reviewWaived ||
|
|
3477
|
-
violation.kind === VIOLATIONS.reviewCarried
|
|
3478
|
-
)
|
|
3479
|
-
return false;
|
|
3515
|
+
if (REPORT_ONLY.includes(violation.kind)) return false;
|
|
3480
3516
|
if (NEVER_BLOCKING.includes(violation.kind)) return policy.failOnVacuous;
|
|
3481
3517
|
return !policy.warnOnly || ALWAYS_BLOCKING.includes(violation.kind);
|
|
3482
3518
|
}
|
|
@@ -1341,14 +1341,67 @@ const dryRunTimeoutVerdict = budgets => ({
|
|
|
1341
1341
|
" needs longer on this machine; investigate a hang if it does not.",
|
|
1342
1342
|
});
|
|
1343
1343
|
|
|
1344
|
+
/**
|
|
1345
|
+
* How many scored ranges the verdict lists before it stops naming them.
|
|
1346
|
+
*
|
|
1347
|
+
* The list is what makes the verdict self-identifying, so it has to be long
|
|
1348
|
+
* enough to recognise the change by and short enough that nobody scrolls past
|
|
1349
|
+
* the verdict to reach it.
|
|
1350
|
+
* @type {number}
|
|
1351
|
+
*/
|
|
1352
|
+
const SCORED_RANGES_LISTED = 10;
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* What this run scored, in the run's own terms.
|
|
1356
|
+
*
|
|
1357
|
+
* ## Why a verdict has to name its subject
|
|
1358
|
+
*
|
|
1359
|
+
* CodySwannGT/lisa#3878. The parser tests feed this module Stryker's exact
|
|
1360
|
+
* wording, and when the unit suite runs inside a push the vendor's line reaches
|
|
1361
|
+
* the same transcript a real gate writes to. Three readers in a row read a
|
|
1362
|
+
* FIXTURE line as a real verdict about their branch and went off to strengthen
|
|
1363
|
+
* tests for a score nothing had measured. Nothing in the vendor's wording says
|
|
1364
|
+
* which run produced it, and no amount of care fixes that from the reading end.
|
|
1365
|
+
*
|
|
1366
|
+
* The discriminator that survives a wording edit is not a marker anyone has to
|
|
1367
|
+
* spot: it is that a real verdict can name the ranges it scored and a stand-in
|
|
1368
|
+
* transcript cannot, because a stand-in never scoped a change. So the verdict
|
|
1369
|
+
* carries its own subject, and a reader's test becomes "does this name MY
|
|
1370
|
+
* files?" rather than "does this look real?".
|
|
1371
|
+
* @param {readonly string[]|null|undefined} scored - Scored `path:start-end`
|
|
1372
|
+
* ranges, `null` for a whole-list run that scoped no diff, or `undefined`
|
|
1373
|
+
* from a caller that is not the gate and therefore knows of no subject.
|
|
1374
|
+
* @returns {string} The block naming the subject, or "" when it is unknown.
|
|
1375
|
+
*/
|
|
1376
|
+
const scoredSubjectNote = scored => {
|
|
1377
|
+
if (scored === null) {
|
|
1378
|
+
return (
|
|
1379
|
+
`\n Scored: every pattern in the project's mutate list ` +
|
|
1380
|
+
`(${WHOLE_LIST_FLAG}), not a diff.`
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
// Not reachable from the gate, which always knows its scope. It IS reachable
|
|
1384
|
+
// from a direct call to the exported classifier, and inventing a subject
|
|
1385
|
+
// there would be the same lie in the other direction.
|
|
1386
|
+
if (!Array.isArray(scored) || scored.length === 0) return "";
|
|
1387
|
+
const listed = scored.slice(0, SCORED_RANGES_LISTED);
|
|
1388
|
+
const rest =
|
|
1389
|
+
scored.length > listed.length
|
|
1390
|
+
? `\n …and ${scored.length - listed.length} more`
|
|
1391
|
+
: "";
|
|
1392
|
+
const lines = listed.map(range => ` • ${range}`).join("\n");
|
|
1393
|
+
return `\n Scored ${scored.length} changed line range(s) from THIS change:\n${lines}${rest}`;
|
|
1394
|
+
};
|
|
1395
|
+
|
|
1344
1396
|
/**
|
|
1345
1397
|
* The block printed when a completed run scored under `thresholds.break`.
|
|
1346
1398
|
* @param {readonly string[]} broke - `[, score, threshold]` from Stryker.
|
|
1347
1399
|
* @param {string} output - Stryker's combined output.
|
|
1348
1400
|
* @param {{timeoutMS: number, inherited: string[]}} budgets - Budgets in force.
|
|
1401
|
+
* @param {readonly string[]|null|undefined} scored - What the run scored.
|
|
1349
1402
|
* @returns {{outcome: string, message: string}} The marker and the block.
|
|
1350
1403
|
*/
|
|
1351
|
-
const scoreBelowBreakVerdict = (broke, output, budgets) => {
|
|
1404
|
+
const scoreBelowBreakVerdict = (broke, output, budgets, scored) => {
|
|
1352
1405
|
const timedOut = timedOutMutants(output);
|
|
1353
1406
|
const clockNote =
|
|
1354
1407
|
timedOut > 0
|
|
@@ -1363,7 +1416,7 @@ const scoreBelowBreakVerdict = (broke, output, budgets) => {
|
|
|
1363
1416
|
` threshold of ${broke[2]}. This one IS a verdict about your tests.`;
|
|
1364
1417
|
return {
|
|
1365
1418
|
outcome: OUTCOMES.scoreBelowBreak,
|
|
1366
|
-
message: `${verdict}${clockNote}`,
|
|
1419
|
+
message: `${verdict}${clockNote}${scoredSubjectNote(scored)}`,
|
|
1367
1420
|
};
|
|
1368
1421
|
};
|
|
1369
1422
|
|
|
@@ -1417,9 +1470,12 @@ const runFailedVerdict = (output, budgets) => {
|
|
|
1417
1470
|
* when this machine could not capture it.
|
|
1418
1471
|
* @param {{timeoutMS: number, dryRunTimeoutMinutes: number,
|
|
1419
1472
|
* inherited: string[]}} budgets - From `resolveTimeoutBudgets`.
|
|
1473
|
+
* @param {readonly string[]|null} [scored] - What the run scored: the
|
|
1474
|
+
* `path:start-end` ranges, or `null` for a whole-list run. Passed by the gate
|
|
1475
|
+
* so the verdict names its own subject; see {@link scoredSubjectNote}.
|
|
1420
1476
|
* @returns {{outcome: string, message: string}} The marker and the block.
|
|
1421
1477
|
*/
|
|
1422
|
-
export const classifyStrykerFailure = (output, budgets) => {
|
|
1478
|
+
export const classifyStrykerFailure = (output, budgets, scored) => {
|
|
1423
1479
|
if (typeof output !== "string" || output.length === 0) {
|
|
1424
1480
|
return runFailedVerdict(null, budgets);
|
|
1425
1481
|
}
|
|
@@ -1427,7 +1483,7 @@ export const classifyStrykerFailure = (output, budgets) => {
|
|
|
1427
1483
|
return dryRunTimeoutVerdict(budgets);
|
|
1428
1484
|
}
|
|
1429
1485
|
const broke = BREAK_THRESHOLD_PATTERN.exec(output);
|
|
1430
|
-
if (broke) return scoreBelowBreakVerdict(broke, output, budgets);
|
|
1486
|
+
if (broke) return scoreBelowBreakVerdict(broke, output, budgets, scored);
|
|
1431
1487
|
return runFailedVerdict(output, budgets);
|
|
1432
1488
|
};
|
|
1433
1489
|
|
|
@@ -2311,9 +2367,11 @@ export const WHOLE_LIST_FLAG = "--all";
|
|
|
2311
2367
|
* failed on. See {@link timeoutAccounting}.
|
|
2312
2368
|
* @param {string} cwd - Project root.
|
|
2313
2369
|
* @param {{code: number, output: string|null, killedBy?: string}} result - From `runStryker`.
|
|
2370
|
+
* @param {readonly string[]|null} [scored] - What the run scored, forwarded to
|
|
2371
|
+
* the failure classification so the verdict names its own subject.
|
|
2314
2372
|
* @returns {{code: number, measured: boolean}} The exit code and whether a score was produced.
|
|
2315
2373
|
*/
|
|
2316
|
-
export const reportRun = (cwd, result) => {
|
|
2374
|
+
export const reportRun = (cwd, result, scored) => {
|
|
2317
2375
|
const accounting = accountForTimeouts(result.output, cwd);
|
|
2318
2376
|
if (result.killedBy === CHILD_DEADLINE) {
|
|
2319
2377
|
// A gate that ran and failed measured something; a gate that was KILLED
|
|
@@ -2330,7 +2388,8 @@ export const reportRun = (cwd, result) => {
|
|
|
2330
2388
|
// guess — which it did, out loud, as "mutation score below threshold", for
|
|
2331
2389
|
// dry runs that never computed a score at all.
|
|
2332
2390
|
console.error(
|
|
2333
|
-
classifyStrykerFailure(result.output, resolveTimeoutBudgets(cwd))
|
|
2391
|
+
classifyStrykerFailure(result.output, resolveTimeoutBudgets(cwd), scored)
|
|
2392
|
+
.message
|
|
2334
2393
|
);
|
|
2335
2394
|
// A failure that produced no table produced no score either — a dry run
|
|
2336
2395
|
// killed by the clock is the common case — so the unmeasured warning would
|
|
@@ -2424,7 +2483,7 @@ export const runGate = (cwd = process.cwd(), argv = []) => {
|
|
|
2424
2483
|
`🧬 ${OUTCOMES.wholeList} — Stryker over every pattern in ` +
|
|
2425
2484
|
`${declaration.source}, with no diff scoping.`
|
|
2426
2485
|
);
|
|
2427
|
-
const reported = reportRun(cwd, runStryker(cwd, []));
|
|
2486
|
+
const reported = reportRun(cwd, runStryker(cwd, []), null);
|
|
2428
2487
|
return finish(OUTCOMES.wholeList, reported.code, reported.measured);
|
|
2429
2488
|
}
|
|
2430
2489
|
|
|
@@ -2537,7 +2596,11 @@ export const runGate = (cwd = process.cwd(), argv = []) => {
|
|
|
2537
2596
|
);
|
|
2538
2597
|
for (const file of scope.selected) console.log(` • ${file}`);
|
|
2539
2598
|
|
|
2540
|
-
const reported = reportRun(
|
|
2599
|
+
const reported = reportRun(
|
|
2600
|
+
cwd,
|
|
2601
|
+
runStryker(cwd, scope.selected),
|
|
2602
|
+
scope.selected
|
|
2603
|
+
);
|
|
2541
2604
|
return finish(OUTCOMES.scoped, reported.code, reported.measured);
|
|
2542
2605
|
};
|
|
2543
2606
|
|