@codyswann/lisa 4.45.14 → 4.45.15

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.
Files changed (61) hide show
  1. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  2. package/dist/core/lisa-owned-hash-ledger.js +10 -0
  3. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  4. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  5. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  6. package/dist/core/upstream-evidence-manifest.js +8 -1
  7. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  8. package/package.json +5 -5
  9. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  10. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  11. package/plugins/lisa-agy/plugin.json +1 -1
  12. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  13. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  14. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  15. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  21. package/plugins/lisa-expo-agy/plugin.json +1 -1
  22. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  26. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  27. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  31. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  32. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  36. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  37. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  41. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  42. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-rails-agy/plugin.json +1 -1
  47. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  51. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  52. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  56. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  57. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  59. package/scripts/run-artifact-checks.mjs +215 -0
  60. package/typescript/copy-contents/.husky/post-merge +114 -0
  61. 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
@@ -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)).message
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(cwd, runStryker(cwd, scope.selected));
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