@codyswann/lisa 4.34.4 → 4.34.6

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 (99) hide show
  1. package/all/copy-overwrite/scripts/lisa-gates.mjs +40 -1
  2. package/all/copy-overwrite/scripts/lisa-hooks/block-managed-file-edits.sh +436 -9
  3. package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +636 -101
  4. package/all/copy-overwrite/scripts/lisa-test-node.mjs +44 -1
  5. package/dist/codex/scripts/block-no-verify.sh +608 -99
  6. package/dist/configs/vitest/scratch-global-setup.d.ts +29 -0
  7. package/dist/configs/vitest/scratch-global-setup.d.ts.map +1 -1
  8. package/dist/configs/vitest/scratch-global-setup.js +77 -12
  9. package/dist/configs/vitest/scratch-global-setup.js.map +1 -1
  10. package/dist/core/config-shadowing.d.ts +72 -0
  11. package/dist/core/config-shadowing.d.ts.map +1 -0
  12. package/dist/core/config-shadowing.js +152 -0
  13. package/dist/core/config-shadowing.js.map +1 -0
  14. package/dist/core/config.d.ts +16 -1
  15. package/dist/core/config.d.ts.map +1 -1
  16. package/dist/core/config.js +1 -0
  17. package/dist/core/config.js.map +1 -1
  18. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  19. package/dist/core/lisa-owned-hash-ledger.js +10 -0
  20. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  21. package/dist/core/lisa.d.ts +21 -0
  22. package/dist/core/lisa.d.ts.map +1 -1
  23. package/dist/core/lisa.js +46 -0
  24. package/dist/core/lisa.js.map +1 -1
  25. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  26. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  27. package/dist/core/upstream-evidence-manifest.js +19 -11
  28. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  29. package/dist/opencode/plugin-templates/block-no-verify.sh +636 -101
  30. package/dist/strategies/copy-overwrite.d.ts +42 -0
  31. package/dist/strategies/copy-overwrite.d.ts.map +1 -1
  32. package/dist/strategies/copy-overwrite.js +69 -5
  33. package/dist/strategies/copy-overwrite.js.map +1 -1
  34. package/expo/copy-overwrite/scripts/check-lighthouse-details.mjs +138 -11
  35. package/expo/create-only/.github/workflows/deploy.yml +51 -4
  36. package/package.json +4 -4
  37. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  39. package/plugins/lisa/hooks/block-managed-file-edits.sh +436 -9
  40. package/plugins/lisa/hooks/block-no-verify.agy.sh +608 -99
  41. package/plugins/lisa/hooks/block-no-verify.sh +636 -101
  42. package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +608 -99
  43. package/plugins/lisa-agy/plugin.json +1 -1
  44. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  47. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-copilot/hooks/block-managed-file-edits.sh +436 -9
  51. package/plugins/lisa-copilot/hooks/block-no-verify.sh +636 -101
  52. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-cursor/hooks/block-managed-file-edits.sh +436 -9
  54. package/plugins/lisa-cursor/hooks/block-no-verify.sh +636 -101
  55. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-expo-agy/plugin.json +1 -1
  58. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  63. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  68. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  72. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  73. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  77. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  78. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  82. package/plugins/lisa-rails-agy/plugin.json +1 -1
  83. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  87. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  88. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  92. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  93. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  95. package/plugins/src/base/hooks/block-managed-file-edits.sh +436 -9
  96. package/plugins/src/base/hooks/block-no-verify.agy.sh +608 -99
  97. package/plugins/src/base/hooks/block-no-verify.sh +636 -101
  98. package/rails/create-only/.github/workflows/deploy.yml +45 -1
  99. package/scripts/two-channel-couplings.json +1 -1
@@ -68,6 +68,48 @@ export declare class CopyOverwriteStrategy implements ICopyStrategy {
68
68
  * @returns A preserved result when the copy is kept, else undefined
69
69
  */
70
70
  private preserveOwnedHostCopy;
71
+ /**
72
+ * Create a managed file the destination does not have.
73
+ *
74
+ * Its own method because this branch is where CodySwannGT/lisa#3501 lives.
75
+ * Every guard in {@link apply} asks whether THIS path exists, and each is
76
+ * blind to a differently-spelled file that outranks it: a repo configuring
77
+ * knip as `knip.ts` has no `knip.json`, so the template lands as a NEW file
78
+ * that knip then prefers, silently replacing the repo's own settings with
79
+ * stack defaults. The question has to be asked here, before the write,
80
+ * because there is no later branch — the create path is the whole defect.
81
+ * @param sourcePath - Packaged template path
82
+ * @param destPath - Installed file path, which does not yet exist
83
+ * @param relativePath - Repo-relative path for reporting
84
+ * @param context - Strategy context with config
85
+ * @returns The `copied` result, or `shadowed` when Lisa stands down
86
+ */
87
+ private createMissingFile;
88
+ /**
89
+ * The existing config file this template would outrank, if any.
90
+ *
91
+ * **Seed templates only, and the exclusion is the safety argument.** A
92
+ * template that declares `IS replaced on each` is a governance artifact —
93
+ * `eslint.config.ts`, `vitest.config.ts`, `commitlint.config.cjs`,
94
+ * `sgconfig.yml` — where writing the file IS the guardrail. Standing down
95
+ * because a same-family file exists would let any host silently disable
96
+ * Lisa's enforcement by dropping in an `eslint.config.js`, with nothing
97
+ * reporting it: #2374's undeliverable-fix incident re-entering through the
98
+ * door opened to fix #3501. For those, a same-family host file is a conflict
99
+ * to surface, never a reason not to write.
100
+ *
101
+ * Reuses `declaresReplacedEveryRun` rather than introducing a second notion
102
+ * of who owns a file. Two overlapping ownership definitions is how the next
103
+ * defect of this shape gets built.
104
+ *
105
+ * Returns a result only when Lisa is standing down, so the caller falls
106
+ * through to its normal create path in every other case.
107
+ * @param sourcePath - Packaged template path, read for its own declaration
108
+ * @param destPath - Installed file path, whose directory is the search root
109
+ * @param relativePath - Repo-relative path of the managed template
110
+ * @returns A `shadowed` result when the template is withheld, else undefined
111
+ */
112
+ private standDownForHostConfig;
71
113
  /**
72
114
  * Hold back a Lisa-owned refresh that cannot be proved to be an upgrade.
73
115
  *
@@ -1 +1 @@
1
- {"version":3,"file":"copy-overwrite.d.ts","sourceRoot":"","sources":["../../src/strategies/copy-overwrite.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAY7D,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAG9E;;;;;;GAMG;AACH,qBAAa,qBAAsB,YAAW,aAAa;IACzD,QAAQ,CAAC,IAAI,EAAG,gBAAgB,CAAU;IAE1C;;;;;;;OAOG;IACG,KAAK,CACT,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,mBAAmB,CAAC;IA8H/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;YACW,mBAAmB;IAwBjC;;;;;;;;;;;;;;;;;OAiBG;YACW,qBAAqB;IAkBnC;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;YACW,mBAAmB;CAkClC"}
1
+ {"version":3,"file":"copy-overwrite.d.ts","sourceRoot":"","sources":["../../src/strategies/copy-overwrite.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAgB7D,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAG9E;;;;;;GAMG;AACH,qBAAa,qBAAsB,YAAW,aAAa;IACzD,QAAQ,CAAC,IAAI,EAAG,gBAAgB,CAAU;IAE1C;;;;;;;OAOG;IACG,KAAK,CACT,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,mBAAmB,CAAC;IA+H/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;YACW,mBAAmB;IAwBjC;;;;;;;;;;;;;;;;;OAiBG;YACW,qBAAqB;IAkBnC;;;;;;;;;;;;;;;OAeG;YACW,iBAAiB;IAoB/B;;;;;;;;;;;;;;;;;;;;;;;OAuBG;YACW,sBAAsB;IA2BpC;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;YACW,mBAAmB;CAkClC"}
@@ -1,6 +1,7 @@
1
1
  import * as fse from "fs-extra";
2
2
  import { copyFile, readFile } from "node:fs/promises";
3
3
  import { mayRefreshTemplate } from "../core/config.js";
4
+ import { shadowedConfigNote, shadowedConfigSibling, } from "../core/config-shadowing.js";
4
5
  import { declaresReplacedEveryRun, isLisaOwnedTemplate, } from "../core/lisa-owned-templates.js";
5
6
  import { classifyHostCopy, describePreserved, describeUnclassifiable, mayRefreshLisaOwned, } from "../core/lisa-owned-provenance.js";
6
7
  import { filesIdentical, ensureParentDir } from "../utils/file-operations.js";
@@ -25,11 +26,7 @@ export class CopyOverwriteStrategy {
25
26
  const { config, backupFile, promptOverwrite } = context;
26
27
  const destExists = await fse.pathExists(destPath);
27
28
  if (!destExists) {
28
- if (!config.dryRun) {
29
- await ensureParentDir(destPath);
30
- await copyFile(sourcePath, destPath);
31
- }
32
- return { relativePath, strategy: this.name, action: "copied" };
29
+ return this.createMissingFile(sourcePath, destPath, relativePath, context);
33
30
  }
34
31
  if (await filesIdentical(sourcePath, destPath)) {
35
32
  return { relativePath, strategy: this.name, action: "skipped" };
@@ -192,6 +189,73 @@ export class CopyOverwriteStrategy {
192
189
  }
193
190
  return this.preserveIfHostAhead(sourcePath, destPath, relativePath, context.hashLedger);
194
191
  }
192
+ /**
193
+ * Create a managed file the destination does not have.
194
+ *
195
+ * Its own method because this branch is where CodySwannGT/lisa#3501 lives.
196
+ * Every guard in {@link apply} asks whether THIS path exists, and each is
197
+ * blind to a differently-spelled file that outranks it: a repo configuring
198
+ * knip as `knip.ts` has no `knip.json`, so the template lands as a NEW file
199
+ * that knip then prefers, silently replacing the repo's own settings with
200
+ * stack defaults. The question has to be asked here, before the write,
201
+ * because there is no later branch — the create path is the whole defect.
202
+ * @param sourcePath - Packaged template path
203
+ * @param destPath - Installed file path, which does not yet exist
204
+ * @param relativePath - Repo-relative path for reporting
205
+ * @param context - Strategy context with config
206
+ * @returns The `copied` result, or `shadowed` when Lisa stands down
207
+ */
208
+ async createMissingFile(sourcePath, destPath, relativePath, context) {
209
+ const shadowed = await this.standDownForHostConfig(sourcePath, destPath, relativePath);
210
+ if (shadowed !== undefined)
211
+ return shadowed;
212
+ if (!context.config.dryRun) {
213
+ await ensureParentDir(destPath);
214
+ await copyFile(sourcePath, destPath);
215
+ }
216
+ return { relativePath, strategy: this.name, action: "copied" };
217
+ }
218
+ /**
219
+ * The existing config file this template would outrank, if any.
220
+ *
221
+ * **Seed templates only, and the exclusion is the safety argument.** A
222
+ * template that declares `IS replaced on each` is a governance artifact —
223
+ * `eslint.config.ts`, `vitest.config.ts`, `commitlint.config.cjs`,
224
+ * `sgconfig.yml` — where writing the file IS the guardrail. Standing down
225
+ * because a same-family file exists would let any host silently disable
226
+ * Lisa's enforcement by dropping in an `eslint.config.js`, with nothing
227
+ * reporting it: #2374's undeliverable-fix incident re-entering through the
228
+ * door opened to fix #3501. For those, a same-family host file is a conflict
229
+ * to surface, never a reason not to write.
230
+ *
231
+ * Reuses `declaresReplacedEveryRun` rather than introducing a second notion
232
+ * of who owns a file. Two overlapping ownership definitions is how the next
233
+ * defect of this shape gets built.
234
+ *
235
+ * Returns a result only when Lisa is standing down, so the caller falls
236
+ * through to its normal create path in every other case.
237
+ * @param sourcePath - Packaged template path, read for its own declaration
238
+ * @param destPath - Installed file path, whose directory is the search root
239
+ * @param relativePath - Repo-relative path of the managed template
240
+ * @returns A `shadowed` result when the template is withheld, else undefined
241
+ */
242
+ async standDownForHostConfig(sourcePath, destPath, relativePath) {
243
+ // Read once, ahead of any probe: the declaration is a property of the
244
+ // template, not of the candidate being tested.
245
+ if (declaresReplacedEveryRun(await readFile(sourcePath, "utf8"))) {
246
+ return undefined;
247
+ }
248
+ const projectRoot = destPath.slice(0, destPath.length - relativePath.length);
249
+ const shadowed = await shadowedConfigSibling(relativePath, candidate => fse.pathExists(`${projectRoot}${candidate}`));
250
+ if (shadowed === undefined)
251
+ return undefined;
252
+ return {
253
+ relativePath,
254
+ strategy: this.name,
255
+ action: "shadowed",
256
+ note: shadowedConfigNote(relativePath, shadowed.tool, shadowed.sibling),
257
+ };
258
+ }
195
259
  /**
196
260
  * Hold back a Lisa-owned refresh that cannot be proved to be an upgrade.
197
261
  *
@@ -1 +1 @@
1
- {"version":3,"file":"copy-overwrite.js","sourceRoot":"","sources":["../../src/strategies/copy-overwrite.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"copy-overwrite.js","sourceRoot":"","sources":["../../src/strategies/copy-overwrite.ts"],"names":[],"mappings":"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"}
@@ -9,7 +9,16 @@
9
9
  * table. Its score is effectively binary, so a score floor makes unchanged
10
10
  * pages alternate between green and red as runner timing changes. LHCI writes
11
11
  * the measurement into each LHR; this checker applies a real millisecond
12
- * ceiling to every collected run.
12
+ * ceiling to that evidence.
13
+ *
14
+ * The ceiling is asserted against the **median of a URL's runs**, not against
15
+ * any single run. A forced-reflow regression is deterministic — code that
16
+ * reads layout inside a loop does so on every load — so it moves the median.
17
+ * A single cold run does not: the first Lighthouse run of a URL, immediately
18
+ * after the runs of the previous URL, routinely reports a warmup spike that no
19
+ * later run of the same URL reproduces. Asserting a maximum made that spike
20
+ * indistinguishable from a regression, so the gate's verdict was uncorrelated
21
+ * with the change under review.
13
22
  * @module scripts/check-lighthouse-details
14
23
  */
15
24
 
@@ -21,6 +30,29 @@ const CONFIG_FILE = "lighthouserc-config.json";
21
30
  const REPORT_DIRECTORY = ".lighthouseci";
22
31
  const REPORT_PATTERN = /^lhr-.*\.json$/;
23
32
  const DEFAULT_MAX_FORCED_REFLOW_MS = 100;
33
+ const RUN_COUNT_SETTING = "collect.numberOfRuns";
34
+
35
+ /**
36
+ * Fewest runs per URL that a median can absorb a warmup spike from.
37
+ *
38
+ * With two samples the median is their mean, so one cold spike still drags the
39
+ * statistic halfway to itself — the defect survives, attenuated. Three is the
40
+ * smallest count at which a single outlier cannot move the median at all.
41
+ */
42
+ const MINIMUM_SAMPLES_PER_URL = 3;
43
+
44
+ /**
45
+ * LHR fields naming the audited page, best first.
46
+ *
47
+ * Lighthouse renamed `finalUrl` to `finalDisplayedUrl` and reports differ by
48
+ * version, so grouping cannot depend on any single one of them.
49
+ */
50
+ const URL_FIELDS = [
51
+ "finalUrl",
52
+ "finalDisplayedUrl",
53
+ "requestedUrl",
54
+ "mainDocumentUrl",
55
+ ];
24
56
 
25
57
  /**
26
58
  * Read the bottom-up aggregate table from one forced-reflow audit.
@@ -66,10 +98,43 @@ function readAggregateMeasurements(details) {
66
98
  });
67
99
  }
68
100
 
101
+ /**
102
+ * Identify which page one Lighthouse result measured.
103
+ * Samples are grouped by this value, so an unidentifiable report is a hard
104
+ * failure rather than a group of its own — a report grouped alone would be
105
+ * reported as a run-count shortfall, which names the wrong remedy.
106
+ * @param {Record<string, unknown>} report - Parsed LHR
107
+ * @returns {string} Audited URL
108
+ */
109
+ function readReportUrl(report) {
110
+ for (const field of URL_FIELDS) {
111
+ const value = report?.[field];
112
+ if (typeof value === "string" && value.length > 0) {
113
+ return value;
114
+ }
115
+ }
116
+ throw new Error(
117
+ `no URL to group runs by; expected one of ${URL_FIELDS.join(", ")}`
118
+ );
119
+ }
120
+
121
+ /**
122
+ * Middle value of a sample set, averaging the two middles when even.
123
+ * @param {readonly number[]} values - Non-empty sample set
124
+ * @returns {number} Median
125
+ */
126
+ function median(values) {
127
+ const sorted = [...values].sort((first, second) => first - second);
128
+ const middle = Math.floor(sorted.length / 2);
129
+ return sorted.length % 2 === 0
130
+ ? (sorted[middle - 1] + sorted[middle]) / 2
131
+ : sorted[middle];
132
+ }
133
+
69
134
  /**
70
135
  * Read the host threshold, falling back to Lisa's shipped default.
71
136
  * @param {string} projectRoot - Host project root
72
- * @returns {number} Maximum forced-reflow duration per Lighthouse run
137
+ * @returns {number} Maximum median forced-reflow duration per URL
73
138
  */
74
139
  function readMaximum(projectRoot) {
75
140
  const configPath = path.join(projectRoot, CONFIG_FILE);
@@ -116,10 +181,12 @@ function readReport(reportPath) {
116
181
  }
117
182
  }
118
183
 
119
- /** Run the detail budget against the current project. */
120
- function main() {
121
- const projectRoot = process.cwd();
122
- const maximum = readMaximum(projectRoot);
184
+ /**
185
+ * List the Lighthouse result files, refusing to pass on absent evidence.
186
+ * @param {string} projectRoot - Host project root
187
+ * @returns {string[]} LHR paths, sorted
188
+ */
189
+ function readReportPaths(projectRoot) {
123
190
  const reportDirectory = path.join(projectRoot, REPORT_DIRECTORY);
124
191
  if (!fs.existsSync(reportDirectory)) {
125
192
  throw new Error(
@@ -134,9 +201,19 @@ function main() {
134
201
  if (reportPaths.length === 0) {
135
202
  throw new Error(`${REPORT_DIRECTORY} contains no Lighthouse result JSON`);
136
203
  }
204
+ return reportPaths;
205
+ }
137
206
 
207
+ /**
208
+ * Total the forced-reflow evidence in every report, grouped by audited URL.
209
+ * Evidence-integrity problems are collected rather than thrown so that one
210
+ * malformed report names itself instead of masking the rest.
211
+ * @param {readonly string[]} reportPaths - LHR paths
212
+ * @returns {{ samplesByUrl: Map<string, number[]>, failures: string[] }} Totals and integrity failures
213
+ */
214
+ function collectSamples(reportPaths) {
215
+ const samplesByUrl = new Map();
138
216
  const failures = [];
139
- let observedMaximum = 0;
140
217
  for (const reportPath of reportPaths) {
141
218
  const report = readReport(reportPath);
142
219
  const audit = report?.audits?.[AUDIT_ID];
@@ -145,8 +222,10 @@ function main() {
145
222
  continue;
146
223
  }
147
224
  let measurements;
225
+ let url;
148
226
  try {
149
227
  measurements = readAggregateMeasurements(audit.details);
228
+ url = readReportUrl(report);
150
229
  } catch (error) {
151
230
  failures.push(
152
231
  `${path.basename(reportPath)}: ${error instanceof Error ? error.message : String(error)}`
@@ -163,21 +242,69 @@ function main() {
163
242
  (sum, measurement) => sum + measurement,
164
243
  0
165
244
  );
166
- observedMaximum = Math.max(observedMaximum, total);
167
- if (total > maximum) {
245
+ const samples = samplesByUrl.get(url);
246
+ if (samples) {
247
+ samples.push(total);
248
+ } else {
249
+ samplesByUrl.set(url, [total]);
250
+ }
251
+ }
252
+ return { samplesByUrl, failures };
253
+ }
254
+
255
+ /**
256
+ * Assert the configured ceiling against each URL's median.
257
+ * A URL with too few runs fails rather than falling back to a maximum, because
258
+ * the median of one sample is that sample: without this, `numberOfRuns: 1`
259
+ * would silently restore a per-run maximum under the name of a median.
260
+ * @param {Map<string, number[]>} samplesByUrl - Per-URL run totals
261
+ * @param {number} maximum - Configured millisecond ceiling
262
+ * @returns {{ failures: string[], largestMedian: number }} Budget failures and the largest median seen
263
+ */
264
+ function assertMedians(samplesByUrl, maximum) {
265
+ const failures = [];
266
+ let largestMedian = 0;
267
+ for (const [url, samples] of samplesByUrl) {
268
+ if (samples.length < MINIMUM_SAMPLES_PER_URL) {
269
+ failures.push(
270
+ `${url}: ${samples.length} run${samples.length === 1 ? "" : "s"} is fewer than the ${MINIMUM_SAMPLES_PER_URL} needed for a median; raise ${RUN_COUNT_SETTING} in ${CONFIG_FILE} to at least ${MINIMUM_SAMPLES_PER_URL}`
271
+ );
272
+ continue;
273
+ }
274
+ const middle = median(samples);
275
+ largestMedian = Math.max(largestMedian, middle);
276
+ if (middle > maximum) {
168
277
  failures.push(
169
- `${report.finalUrl ?? path.basename(reportPath)}: ${total.toFixed(1)} ms exceeds ${maximum} ms`
278
+ `${url}: median ${middle.toFixed(1)} ms of ${samples.length} runs exceeds ${maximum} ms`
170
279
  );
171
280
  }
172
281
  }
282
+ return { failures, largestMedian };
283
+ }
173
284
 
285
+ /** Run the detail budget against the current project. */
286
+ function main() {
287
+ const projectRoot = process.cwd();
288
+ const maximum = readMaximum(projectRoot);
289
+ const reportPaths = readReportPaths(projectRoot);
290
+
291
+ const { samplesByUrl, failures: evidenceFailures } =
292
+ collectSamples(reportPaths);
293
+ if (evidenceFailures.length > 0) {
294
+ throw new Error(
295
+ `forced-reflow detail budget failed:\n${evidenceFailures.map(failure => `- ${failure}`).join("\n")}`
296
+ );
297
+ }
298
+
299
+ const { failures, largestMedian } = assertMedians(samplesByUrl, maximum);
174
300
  if (failures.length > 0) {
175
301
  throw new Error(
176
302
  `forced-reflow detail budget failed:\n${failures.map(failure => `- ${failure}`).join("\n")}`
177
303
  );
178
304
  }
305
+
179
306
  console.log(
180
- `lighthouse detail budget passed: ${reportPaths.length} report(s), forced reflow <= ${maximum} ms (max ${observedMaximum.toFixed(1)} ms)`
307
+ `lighthouse detail budget passed: ${reportPaths.length} report(s) across ${samplesByUrl.size} URL(s), forced reflow median <= ${maximum} ms (largest median ${largestMedian.toFixed(1)} ms)`
181
308
  );
182
309
  }
183
310
 
@@ -211,14 +211,61 @@ jobs:
211
211
  release,
212
212
  check_app_config_changes,
213
213
  ]
214
- # Run if EAS is set up, regardless of whether trigger_eas_build ran or was skipped
214
+ # Runs when EAS is set up, regardless of whether trigger_eas_build ran or
215
+ # was skipped — and, deliberately, EVEN WHEN THE RELEASE DID NOT SUCCEED.
216
+ #
217
+ # This condition used to end in `needs.release.result == 'success'`, which
218
+ # made a failed release SKIP this job. GitHub renders a skipped job as
219
+ # neutral and treats a skipped required check as satisfied, so a deploy that
220
+ # never happened left no signal anywhere: the branch still had its release
221
+ # commit, versions still matched across branches, and nothing prompted
222
+ # anyone to go read CI history for skipped Deploy jobs. The release result
223
+ # is now read by the guard STEP below, which fails this job loudly (#3467).
224
+ #
225
+ # `!cancelled()` rather than `always()`: it still suppresses the implicit
226
+ # `success()`, so the job runs on a failed release, while a run an operator
227
+ # cancelled does not get a verdict it never earned. EAS not being set up is
228
+ # a genuine "not applicable" and still skips.
215
229
  if: |
216
- always() &&
217
- needs.check_eas_setup.outputs.has_eas_setup == 'true' &&
218
- needs.release.result == 'success'
230
+ !cancelled() &&
231
+ needs.check_eas_setup.outputs.has_eas_setup == 'true'
219
232
  permissions:
220
233
  contents: read
221
234
  steps:
235
+ # Deploy-outcome guard. Deliberately the FIRST step: it decides whether the
236
+ # rest of this job may proceed, and it reports a verdict on EVERY run, so
237
+ # this check is green or red and never the third, silent state.
238
+ #
239
+ # Keep the `run:` body byte-identical to the guard in every other Lisa
240
+ # deploy workflow — an operator reads the same words whichever stack they
241
+ # are on, and a test asserts the copies have not drifted apart.
242
+ - name: 🚨 Confirm the release shipped
243
+ env:
244
+ DEPLOY_ENVIRONMENT: ${{ needs.determine_environment.outputs.environment }}
245
+ RELEASE_RESULT: ${{ needs.release.result }}
246
+ run: |
247
+ if [ "${RELEASE_RESULT}" = "success" ]; then
248
+ echo "✅ The release succeeded. Deploying to ${DEPLOY_ENVIRONMENT}."
249
+ exit 0
250
+ fi
251
+ {
252
+ echo "### 🚨 Nothing new was deployed to ${DEPLOY_ENVIRONMENT}"
253
+ echo ""
254
+ echo "The **Release** step did not succeed (it reported \`${RELEASE_RESULT}\`), so no"
255
+ echo "new version reached \`${DEPLOY_ENVIRONMENT}\`. Whatever was already running"
256
+ echo "there is untouched and still working — this change simply has not shipped."
257
+ echo ""
258
+ echo "**What to do next**"
259
+ echo ""
260
+ echo "1. Open the **Release** job in this run and read the step marked in red."
261
+ echo "2. The most common cause is that this version number was already released from"
262
+ echo " another branch. The fix is to raise \`version\` in \`package.json\` above every"
263
+ echo " version already released, then merge again."
264
+ echo "3. If anything here is unclear, send this run's link to whoever looks after"
265
+ echo " deployments. Do not merge anything else into this branch until it is fixed."
266
+ } >> "${GITHUB_STEP_SUMMARY:-/dev/stdout}"
267
+ echo "::error title=Nothing new was deployed to ${DEPLOY_ENVIRONMENT}::The release did not succeed (${RELEASE_RESULT}), so this change did not ship and the previous version is still running. Open the Release job in this run to see why, fix it, then merge again."
268
+ exit 1
222
269
  - name: Checkout repository
223
270
  uses: actions/checkout@v6
224
271
  with:
package/package.json CHANGED
@@ -166,7 +166,7 @@
166
166
  "zod-validation-error": "^4.0.0"
167
167
  },
168
168
  "name": "@codyswann/lisa",
169
- "version": "4.34.4",
169
+ "version": "4.34.6",
170
170
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
171
171
  "main": "dist/index.js",
172
172
  "exports": {
@@ -314,7 +314,7 @@
314
314
  "test": "tests"
315
315
  },
316
316
  "types": "./dist/index.d.ts",
317
- "lisaReleaseCommit": "5c67eb33f92bb76e47023e4588b92dd9bd178cfd",
318
- "gitHead": "5c67eb33f92bb76e47023e4588b92dd9bd178cfd",
319
- "lisaReleaseTag": "v4.34.4"
317
+ "lisaReleaseCommit": "825a67c494db413e3fb3305e1db7c2048d8ecdfa",
318
+ "gitHead": "825a67c494db413e3fb3305e1db7c2048d8ecdfa",
319
+ "lisaReleaseTag": "v4.34.6"
320
320
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.34.4",
3
+ "version": "4.34.6",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.34.4",
3
+ "version": "4.34.6",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"