@codyswann/lisa 4.42.6 → 4.43.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.
Files changed (186) hide show
  1. package/README.md +4 -0
  2. package/all/copy-overwrite/scripts/check-orphaned-branches.mjs +7 -0
  3. package/all/copy-overwrite/scripts/lib/process-tree-runner.mjs +12 -2
  4. package/all/copy-overwrite/scripts/lisa-cross-worktree-guard.mjs +4 -0
  5. package/all/copy-overwrite/scripts/lisa-environment-prepare.mjs +2 -0
  6. package/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.mjs +31 -3
  7. package/all/copy-overwrite/scripts/lisa-run-gates.mjs +4 -0
  8. package/all/copy-overwrite/scripts/lisa-work-item.mjs +13 -0
  9. package/dist/cli/doctor-readiness-delivery.d.ts.map +1 -1
  10. package/dist/cli/doctor-readiness-delivery.js +13 -6
  11. package/dist/cli/doctor-readiness-delivery.js.map +1 -1
  12. package/dist/cli/doctor-readiness-deploy-outcome.d.ts +9 -0
  13. package/dist/cli/doctor-readiness-deploy-outcome.d.ts.map +1 -0
  14. package/dist/cli/doctor-readiness-deploy-outcome.js +87 -0
  15. package/dist/cli/doctor-readiness-deploy-outcome.js.map +1 -0
  16. package/dist/cli/doctor-readiness-workflows.d.ts +8 -0
  17. package/dist/cli/doctor-readiness-workflows.d.ts.map +1 -1
  18. package/dist/cli/doctor-readiness-workflows.js +1 -0
  19. package/dist/cli/doctor-readiness-workflows.js.map +1 -1
  20. package/dist/configs/vitest/base.d.ts +1 -1
  21. package/dist/configs/vitest/base.d.ts.map +1 -1
  22. package/dist/configs/vitest/base.js +1 -0
  23. package/dist/configs/vitest/base.js.map +1 -1
  24. package/dist/configs/vitest/cdk.d.ts +2 -2
  25. package/dist/configs/vitest/cdk.d.ts.map +1 -1
  26. package/dist/configs/vitest/cdk.js +5 -2
  27. package/dist/configs/vitest/cdk.js.map +1 -1
  28. package/dist/configs/vitest/coverage-reports-directory.d.ts +47 -0
  29. package/dist/configs/vitest/coverage-reports-directory.d.ts.map +1 -0
  30. package/dist/configs/vitest/coverage-reports-directory.js +131 -0
  31. package/dist/configs/vitest/coverage-reports-directory.js.map +1 -0
  32. package/dist/configs/vitest/harper-fabric.d.ts +2 -2
  33. package/dist/configs/vitest/harper-fabric.d.ts.map +1 -1
  34. package/dist/configs/vitest/harper-fabric.js +5 -2
  35. package/dist/configs/vitest/harper-fabric.js.map +1 -1
  36. package/dist/configs/vitest/nestjs.d.ts +2 -2
  37. package/dist/configs/vitest/nestjs.d.ts.map +1 -1
  38. package/dist/configs/vitest/nestjs.js +5 -2
  39. package/dist/configs/vitest/nestjs.js.map +1 -1
  40. package/dist/configs/vitest/phaser.d.ts +2 -2
  41. package/dist/configs/vitest/phaser.d.ts.map +1 -1
  42. package/dist/configs/vitest/phaser.js +5 -2
  43. package/dist/configs/vitest/phaser.js.map +1 -1
  44. package/dist/configs/vitest/typescript.d.ts +2 -2
  45. package/dist/configs/vitest/typescript.d.ts.map +1 -1
  46. package/dist/configs/vitest/typescript.js +5 -2
  47. package/dist/configs/vitest/typescript.js.map +1 -1
  48. package/dist/core/deploy-release-dependence.d.ts +72 -0
  49. package/dist/core/deploy-release-dependence.d.ts.map +1 -0
  50. package/dist/core/deploy-release-dependence.js +236 -0
  51. package/dist/core/deploy-release-dependence.js.map +1 -0
  52. package/dist/core/github-actions-condition.d.ts +78 -0
  53. package/dist/core/github-actions-condition.d.ts.map +1 -0
  54. package/dist/core/github-actions-condition.js +307 -0
  55. package/dist/core/github-actions-condition.js.map +1 -0
  56. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  57. package/dist/core/lisa-owned-hash-ledger.js +24 -0
  58. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  59. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  60. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  61. package/dist/core/upstream-evidence-manifest.js +45 -24
  62. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  63. package/dist/migrations/deploy-outcome-guard-edit.d.ts +11 -0
  64. package/dist/migrations/deploy-outcome-guard-edit.d.ts.map +1 -0
  65. package/dist/migrations/deploy-outcome-guard-edit.js +184 -0
  66. package/dist/migrations/deploy-outcome-guard-edit.js.map +1 -0
  67. package/dist/migrations/deploy-outcome-guard-yaml.d.ts +91 -0
  68. package/dist/migrations/deploy-outcome-guard-yaml.d.ts.map +1 -0
  69. package/dist/migrations/deploy-outcome-guard-yaml.js +180 -0
  70. package/dist/migrations/deploy-outcome-guard-yaml.js.map +1 -0
  71. package/dist/migrations/ensure-deploy-outcome-guard.d.ts +41 -0
  72. package/dist/migrations/ensure-deploy-outcome-guard.d.ts.map +1 -0
  73. package/dist/migrations/ensure-deploy-outcome-guard.js +238 -0
  74. package/dist/migrations/ensure-deploy-outcome-guard.js.map +1 -0
  75. package/dist/migrations/index.d.ts +1 -0
  76. package/dist/migrations/index.d.ts.map +1 -1
  77. package/dist/migrations/index.js +3 -0
  78. package/dist/migrations/index.js.map +1 -1
  79. package/expo/copy-overwrite/scripts/bdd/baseline.mjs +26 -7
  80. package/package.json +5 -4
  81. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  83. package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +11 -0
  84. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +3 -0
  85. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +3 -0
  86. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/providers.mjs +4 -0
  87. package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -0
  88. package/plugins/lisa/hooks/threshold-ratchet.mjs +6 -0
  89. package/plugins/lisa/hooks/worktree-binding-guard.mjs +31 -3
  90. package/plugins/lisa/scripts/design-variable-ids.mjs +2 -0
  91. package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +11 -0
  92. package/plugins/lisa/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +3 -0
  93. package/plugins/lisa/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +3 -0
  94. package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +4 -0
  95. package/plugins/lisa/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -0
  96. package/plugins/lisa-agy/plugin.json +1 -1
  97. package/plugins/lisa-agy/scripts/design-variable-ids.mjs +2 -0
  98. package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +11 -0
  99. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +3 -0
  100. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +3 -0
  101. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +4 -0
  102. package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -0
  103. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  104. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  105. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  106. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  108. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  109. package/plugins/lisa-copilot/hooks/threshold-ratchet.mjs +6 -0
  110. package/plugins/lisa-copilot/hooks/worktree-binding-guard.mjs +31 -3
  111. package/plugins/lisa-copilot/scripts/design-variable-ids.mjs +2 -0
  112. package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +11 -0
  113. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +3 -0
  114. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +3 -0
  115. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/providers.mjs +4 -0
  116. package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -0
  117. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  118. package/plugins/lisa-cursor/hooks/threshold-ratchet.mjs +6 -0
  119. package/plugins/lisa-cursor/hooks/worktree-binding-guard.mjs +31 -3
  120. package/plugins/lisa-cursor/scripts/design-variable-ids.mjs +2 -0
  121. package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +11 -0
  122. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +3 -0
  123. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +3 -0
  124. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +4 -0
  125. package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -0
  126. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  127. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  128. package/plugins/lisa-expo-agy/plugin.json +1 -1
  129. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  130. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  131. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  132. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  133. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  134. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  135. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  136. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  137. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  138. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  139. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  140. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  141. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  142. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  143. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  144. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  145. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  146. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  147. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  148. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  149. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  150. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  151. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  152. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  153. package/plugins/lisa-rails-agy/plugin.json +1 -1
  154. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  155. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  156. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  157. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  158. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  159. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  160. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  161. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  162. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  163. package/plugins/lisa-wiki/scripts/ingest-git.mjs +2 -0
  164. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  165. package/plugins/lisa-wiki-agy/scripts/ingest-git.mjs +2 -0
  166. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  167. package/plugins/lisa-wiki-copilot/scripts/ingest-git.mjs +2 -0
  168. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  169. package/plugins/lisa-wiki-cursor/scripts/ingest-git.mjs +2 -0
  170. package/plugins/src/base/hooks/threshold-ratchet.mjs +6 -0
  171. package/plugins/src/base/hooks/worktree-binding-guard.mjs +31 -3
  172. package/plugins/src/base/scripts/design-variable-ids.mjs +2 -0
  173. package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +11 -0
  174. package/plugins/src/base/skills/lisa-secrets-access/scripts/bootstrap-store.mjs +3 -0
  175. package/plugins/src/base/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +3 -0
  176. package/plugins/src/base/skills/lisa-secrets-access/scripts/providers.mjs +4 -0
  177. package/plugins/src/base/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -0
  178. package/plugins/src/wiki/scripts/ingest-git.mjs +2 -0
  179. package/rails/copy-overwrite/scripts/check-threshold-ratchet.mjs +6 -0
  180. package/scripts/check-merge-driver-registration.mjs +2 -0
  181. package/scripts/check-probe-absence-direction.mjs +611 -0
  182. package/scripts/check-test-case-executed.mjs +184 -0
  183. package/scripts/install-generated-artifact-merge-driver.mjs +7 -1
  184. package/scripts/lib/shell-guard-refusal-coverage.mjs +18 -19
  185. package/typescript/copy-overwrite/scripts/check-threshold-ratchet.mjs +6 -0
  186. package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +4 -0
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Proves that a NAMED test case actually EXECUTED, from a vitest JSON report.
4
+ *
5
+ * ## Why this exists
6
+ *
7
+ * An assertion about a configuration is not an assertion about execution, and
8
+ * CodySwannGT/lisa#3935 is what that gap costs. A 100,000-entry temp-scan
9
+ * benchmark was moved off the pre-push gate on the stated grounds that CI still
10
+ * ran it. It did not: the case is `it.runIf(process.platform === "darwin")` and
11
+ * every CI runner is `ubuntu-latest`, so CI skipped it, and had always skipped
12
+ * it. The check written to guard the move asserted that the pull-request task
13
+ * did not EXCLUDE the file — true, and hollow, because a platform guard one
14
+ * layer down skipped the case anyway. Not excluding a file is not running it.
15
+ *
16
+ * A skipped case is a green case. Nothing in the pipeline reports it, because
17
+ * from the runner's point of view nothing went wrong. That is the whole failure
18
+ * mode: the surface reads as covered while running nothing.
19
+ *
20
+ * So this script asserts the only thing that settles the question — that the
21
+ * case appears in the report, that it PASSED rather than being skipped or
22
+ * todo-ed, and that it consumed real wall-clock time. A lane that runs it is
23
+ * then a lane that can PROVE it ran it, and a platform guard that silently
24
+ * removes the case turns a green job red with the reason on screen.
25
+ *
26
+ * ## Vitest's report vocabulary, as observed
27
+ *
28
+ * Measured on vitest 4.1.9 against a deliberate `it.runIf(false)` probe: a
29
+ * guarded-off case is present in `testResults[].assertionResults[]` with
30
+ * `status: "skipped"` and NO `duration` field, and is counted in
31
+ * `numPendingTests`. A case whose whole FILE was never collected — an
32
+ * `--exclude` glob, a moved file, a typo in the path — is absent from the
33
+ * report entirely. Both are execution failures and both are reported here,
34
+ * separately, because they have different fixes: one is a platform or flag
35
+ * guard, the other is a path or exclusion.
36
+ *
37
+ * `fullName` joins `ancestorTitles` to the title with a SPACE. `vitest list`
38
+ * prints the same case with ` > ` between them, and the two are easy to
39
+ * confuse — measured, both forms, on the same suite. A caller that passes the
40
+ * wrong form gets a case that is absent for a reason having nothing to do with
41
+ * execution, so an ABSENT failure PRINTS the names the report did contain
42
+ * rather than asserting the case was never collected. A gate whose diagnosis is
43
+ * confidently wrong costs more than one that says less.
44
+ *
45
+ * ## Usage
46
+ *
47
+ * node scripts/check-test-case-executed.mjs \
48
+ * --report <vitest --reporter=json --outputFile target> \
49
+ * --name "<full test name>" [--name "<another>"]
50
+ *
51
+ * Exit 0 only when EVERY named case is present, passed, and timed. Any other
52
+ * outcome exits 1 and names which case and which way it failed.
53
+ * @module scripts/check-test-case-executed
54
+ */
55
+ import { readFileSync } from "node:fs";
56
+
57
+ import { invokedAsScript } from "./lib/invoked-as-script.mjs";
58
+
59
+ /**
60
+ * Statuses vitest reports for a case that did not actually run.
61
+ *
62
+ * A Map rather than an object literal because the key is a status string read
63
+ * out of a JSON file. On an object, a report carrying `"constructor"` or
64
+ * `"toString"` resolves to something inherited from `Object.prototype` and the
65
+ * message below stringifies it as though it were advice — an absurd input, but
66
+ * this gate's whole job is to be right about a file it did not write.
67
+ */
68
+ const NON_EXECUTED_STATUS_ADVICE = new Map([
69
+ [
70
+ "skipped",
71
+ "the case was SKIPPED — a platform guard, a `runIf` condition, or an `it.skip` removed it; a skipped case is a green case and proves nothing",
72
+ ],
73
+ ["todo", "the case is marked TODO — it has no body to execute"],
74
+ [
75
+ "failed",
76
+ "the case RAN and FAILED — read the failure above; this gate only reports that it was not a passing execution",
77
+ ],
78
+ ]);
79
+
80
+ /**
81
+ * Parse `--report <path>` and one or more `--name <full test name>` flags.
82
+ * @param {readonly string[]} argv - Arguments after the script path
83
+ * @returns {{report: string, names: readonly string[]}} Validated invocation
84
+ */
85
+ export function parseArguments(argv) {
86
+ const names = [];
87
+ let report;
88
+ for (let index = 0; index < argv.length; index += 1) {
89
+ const flag = argv[index];
90
+ const value = argv[index + 1];
91
+ if (flag !== "--report" && flag !== "--name")
92
+ throw new Error(`Unknown argument: ${String(flag)}`);
93
+ if (value === undefined || value.startsWith("--"))
94
+ throw new Error(`${flag} requires a value`);
95
+ if (flag === "--report") report = value;
96
+ else names.push(value);
97
+ index += 1;
98
+ }
99
+ if (report === undefined) throw new Error("--report is required");
100
+ if (names.length === 0) throw new Error("At least one --name is required");
101
+ return { report, names };
102
+ }
103
+
104
+ /**
105
+ * Read every case the report contains, keyed by its full name.
106
+ * @param {string} reportPath - Path to a vitest JSON report
107
+ * @returns {Map<string, {status: string, duration?: number}>} Reported cases
108
+ */
109
+ export function reportedCases(reportPath) {
110
+ let parsed;
111
+ try {
112
+ parsed = JSON.parse(readFileSync(reportPath, "utf8"));
113
+ } catch (cause) {
114
+ throw new Error(
115
+ `Cannot read the vitest JSON report at ${reportPath}. A missing report means the run never produced one, which is itself proof the case did not execute. Cause: ${cause instanceof Error ? cause.message : String(cause)}`
116
+ );
117
+ }
118
+ const files = Array.isArray(parsed?.testResults) ? parsed.testResults : [];
119
+ return new Map(
120
+ files.flatMap(file =>
121
+ (Array.isArray(file?.assertionResults) ? file.assertionResults : []).map(
122
+ assertion => [
123
+ String(assertion?.fullName),
124
+ {
125
+ status: String(assertion?.status),
126
+ ...(typeof assertion?.duration === "number"
127
+ ? { duration: assertion.duration }
128
+ : {}),
129
+ },
130
+ ]
131
+ )
132
+ )
133
+ );
134
+ }
135
+
136
+ /**
137
+ * Explain why one named case does not count as executed, if it does not.
138
+ * @param {string} name - Full test name the lane claims to run
139
+ * @param {{status: string, duration?: number} | undefined} reported - Its report entry
140
+ * @param {readonly string[]} [available] - Every name the report did contain
141
+ * @returns {string | undefined} Operator-readable failure, or undefined when it ran
142
+ */
143
+ export function executionFailure(name, reported, available = []) {
144
+ if (reported === undefined)
145
+ return `"${name}" is ABSENT from the report — nothing in this lane ran it. Either its file was excluded, moved or renamed, or this name is written wrong: vitest's JSON reporter joins suite titles to the case title with a SPACE, while \`vitest list\` prints them with " > ". The report contained ${available.length === 0 ? "no cases at all" : available.map(reportedName => `"${reportedName}"`).join(", ")}.`;
146
+ if (reported.status !== "passed")
147
+ return `"${name}" did not execute: ${NON_EXECUTED_STATUS_ADVICE.get(reported.status) ?? `the runner reported status "${reported.status}"`}.`;
148
+ if (typeof reported.duration !== "number" || reported.duration <= 0)
149
+ return `"${name}" is reported as passed but consumed no measurable time, so there is no evidence it did any work.`;
150
+ return undefined;
151
+ }
152
+
153
+ /**
154
+ * Prove every named case executed, printing the measured time for each.
155
+ * @param {readonly string[]} argv - Arguments after the script path
156
+ * @returns {readonly string[]} Failures, empty when every case executed
157
+ */
158
+ export function checkTestCasesExecuted(argv) {
159
+ const { names, report } = parseArguments(argv);
160
+ const cases = reportedCases(report);
161
+ const available = [...cases.keys()];
162
+ return names.flatMap(name => {
163
+ const failure = executionFailure(name, cases.get(name), available);
164
+ if (failure !== undefined) return [failure];
165
+ process.stdout.write(
166
+ `EXECUTED ${name} — ${(cases.get(name)?.duration ?? 0).toFixed(1)}ms\n`
167
+ );
168
+ return [];
169
+ });
170
+ }
171
+
172
+ if (invokedAsScript(import.meta.url)) {
173
+ try {
174
+ const failures = checkTestCasesExecuted(process.argv.slice(2));
175
+ for (const failure of failures)
176
+ process.stderr.write(`NOT EXECUTED ${failure}\n`);
177
+ process.exit(failures.length === 0 ? 0 : 1);
178
+ } catch (error) {
179
+ process.stderr.write(
180
+ `${error instanceof Error ? error.message : String(error)}\n`
181
+ );
182
+ process.exit(1);
183
+ }
184
+ }
@@ -116,7 +116,13 @@ function mappedPaths(root) {
116
116
  }
117
117
  if (patterns.length === 0) return [];
118
118
  const attr = git(["check-attr", "merge", "--", ...patterns], root);
119
- if (!attr.ok) return [];
119
+ // probe-direction: fail-closed — when `check-attr` cannot be asked, fall back
120
+ // to the patterns `.gitattributes` actually declares. Returning [] instead
121
+ // made the caller print "nothing in this working tree maps to the driver —
122
+ // nothing to register" and exit 0, so a git that failed was reported as a
123
+ // checkout that needs nothing. The fallback costs a redundant registration;
124
+ // the empty list cost a silently skipped one.
125
+ if (!attr.ok) return patterns;
120
126
  return attr.stdout
121
127
  .split("\n")
122
128
  .filter(line => line.endsWith(`merge: ${GENERATED_ARTIFACT_DRIVER}`))
@@ -117,26 +117,25 @@ export function guardPopulation(root) {
117
117
  .filter(file => file.length > 0)
118
118
  .filter(file => !NOT_GUARDS.some(prefix => file.startsWith(prefix)))
119
119
  .sort((left, right) => left.localeCompare(right));
120
- return tracked.flatMap(file => {
121
- let bytes;
122
- try {
123
- bytes = readFileSync(path.join(root, file));
124
- } catch {
125
- return [];
126
- }
120
+ return tracked.map(file => {
121
+ // probe-direction: fail-closed — an unreadable TRACKED guard RAISES rather
122
+ // than dropping out of the roster. `git ls-files` has already proved the
123
+ // path is tracked, so a read failure is "could not ask", and the previous
124
+ // `catch { return []; }` spent it as "this guard does not exist" — which
125
+ // silently shrinks the population the coverage check is measured against,
126
+ // so a guard stops being covered with nothing saying so. This is the
127
+ // population, not a sample: it must be all of it or an error.
128
+ const bytes = readFileSync(path.join(root, file));
127
129
  const source = bytes.toString("utf8");
128
- return [
129
- {
130
- path: file,
131
- sha256: createHash("sha256").update(bytes).digest("hex"),
132
- size: bytes.length,
133
- canRefuse: /\bexit[ \t]+[1-9]\d*\b/u.test(source),
134
- toolBoundary:
135
- (file.includes("/hooks/") ||
136
- /tool_input|"file_path"/u.test(source)) &&
137
- /\bexit[ \t]+2\b/u.test(source),
138
- },
139
- ];
130
+ return {
131
+ path: file,
132
+ sha256: createHash("sha256").update(bytes).digest("hex"),
133
+ size: bytes.length,
134
+ canRefuse: /\bexit[ \t]+[1-9]\d*\b/u.test(source),
135
+ toolBoundary:
136
+ (file.includes("/hooks/") || /tool_input|"file_path"/u.test(source)) &&
137
+ /\bexit[ \t]+2\b/u.test(source),
138
+ };
140
139
  });
141
140
  }
142
141
 
@@ -124,6 +124,10 @@ function git(args, cwd) {
124
124
  timeout: 30_000,
125
125
  });
126
126
  } catch {
127
+ // probe-direction: fail-closed — null never reaches a verdict as "no
128
+ // weakening found". `changePlan` turns it into `undeterminable`, which
129
+ // REFUSES, and `isPromotion` falls to the strict reading that keeps the
130
+ // allow list pinned to the baseline.
127
131
  return null;
128
132
  }
129
133
  }
@@ -169,6 +173,8 @@ function resolvePlan(mode, root, baseRef, onlyFiles) {
169
173
  try {
170
174
  return fs.readFileSync(path.join(root, f), "utf-8");
171
175
  } catch {
176
+ // probe-direction: fail-closed — an unreadable current file compares as
177
+ // absent against the baseline, which is the strict side of the ratchet.
172
178
  return null;
173
179
  }
174
180
  },
@@ -1349,6 +1349,10 @@ export const selectUninstrumentableMutateTargets = (cwd, patterns) => {
1349
1349
  .filter(file => file && isMutateTarget(file, patterns))
1350
1350
  .filter(file => !isStrykerParseable(file));
1351
1351
  } catch {
1352
+ // probe-direction: neutral — this pre-empts a Stryker crash with a better
1353
+ // message; it does not authorise anything. When the index cannot be read the
1354
+ // run proceeds and Stryker still aborts on the same file, so the empty list
1355
+ // costs a worse error, not a weaker gate.
1352
1356
  // Same reasoning as the count above: an unreadable index says nothing
1353
1357
  // about the config, and blocking a push on it would be a lie about why.
1354
1358
  return [];