@codyswann/lisa 3.47.5 โ†’ 3.47.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 (85) hide show
  1. package/all/copy-overwrite/scripts/lib/gate-failure-diagnosis.mjs +270 -0
  2. package/all/copy-overwrite/scripts/lisa-gates.mjs +30 -0
  3. package/all/copy-overwrite/scripts/lisa-run-gates.mjs +294 -34
  4. package/all/copy-overwrite/scripts/lisa-work-item.mjs +83 -26
  5. package/dist/configs/eslint/expo.d.ts +50 -1
  6. package/dist/configs/eslint/expo.d.ts.map +1 -1
  7. package/dist/configs/eslint/expo.js +86 -5
  8. package/dist/configs/eslint/expo.js.map +1 -1
  9. package/dist/core/downstream-names.d.ts +83 -0
  10. package/dist/core/downstream-names.d.ts.map +1 -0
  11. package/dist/core/downstream-names.js +289 -0
  12. package/dist/core/downstream-names.js.map +1 -0
  13. package/dist/core/downstream-references.d.ts +21 -6
  14. package/dist/core/downstream-references.d.ts.map +1 -1
  15. package/dist/core/downstream-references.js +19 -16
  16. package/dist/core/downstream-references.js.map +1 -1
  17. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  18. package/dist/core/lisa-owned-hash-ledger.js +14 -0
  19. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  20. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  21. package/dist/core/upstream-evidence-manifest.js +21 -9
  22. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  23. package/dist/migrations/ensure-tsconfig-local-includes.d.ts +1 -1
  24. package/dist/migrations/ensure-tsconfig-local-includes.js +1 -1
  25. package/expo/copy-overwrite/eslint.config.ts +9 -0
  26. package/expo/copy-overwrite/eslint.expo.ts +92 -4
  27. package/package.json +5 -2
  28. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  30. package/plugins/lisa/rules/reference/design-value-binding.md +4 -0
  31. package/plugins/lisa-agy/plugin.json +1 -1
  32. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  34. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  35. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-copilot/rules/reference/design-value-binding.md +4 -0
  39. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-cursor/rules/design-value-binding-reference.mdc +4 -0
  41. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-expo-agy/plugin.json +1 -1
  44. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  49. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  54. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  59. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  64. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-rails-agy/plugin.json +1 -1
  69. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  74. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  78. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  79. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  81. package/plugins/src/base/rules/reference/design-value-binding.md +4 -0
  82. package/scripts/lisa-mutation.mjs +13 -0
  83. package/typescript/copy-contents/.husky/pre-push +7 -1
  84. package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +496 -122
  85. package/typescript/package-lisa/package.lisa.json +1 -0
@@ -0,0 +1,270 @@
1
+ // This file is managed by Lisa and IS replaced on each `lisa` run.
2
+ // Do not edit directly โ€” durable changes belong upstream in Lisa.
3
+
4
+ /**
5
+ * Say WHICH thing failed, from a failed gate's own output.
6
+ *
7
+ * ## Why a bare exit code is a defect and not just terse
8
+ *
9
+ * `coverage-adequacy โ€” bun run test:cov (exit 1)` is the same sentence whether
10
+ * the suite measured 84% against a floor of 86%, or whether four
11
+ * subprocess-heavy tests were starved of CPU and hit their wall-clock budget
12
+ * while every assertion in the run passed. Those are opposite facts: one is a
13
+ * regression that must block, the other is the machine being busy.
14
+ *
15
+ * Measured across six sightings, every one was the second kind โ€” always a
16
+ * timeout, never a coverage number below threshold โ€” and every one was
17
+ * answered by a retry, because re-running is genuinely cheaper than
18
+ * investigating a bare exit code. That reflex is rational, and it is also why a
19
+ * real coverage regression would currently be invisible: it renders as the same
20
+ * line the operator has been trained to re-run.
21
+ *
22
+ * ## Precedence: an incomplete run is not a measurement
23
+ *
24
+ * A run whose tests timed out still prints coverage numbers, and those numbers
25
+ * will be low, because the code the dead tests would have exercised went
26
+ * unexercised. Reading a threshold error off such a run reports the *effect* of
27
+ * the timeout as though it were the cause. So a timeout outranks a threshold
28
+ * reading, and a failed assertion outranks it too โ€” coverage is only a
29
+ * measurement when the suite that produced it finished.
30
+ * @module lib/gate-failure-diagnosis
31
+ */
32
+
33
+ /** What a failed gate's output was recognised as. */
34
+ export const DIAGNOSIS = Object.freeze({
35
+ /** One or more tests or hooks exceeded their wall-clock budget. */
36
+ TIMEOUT: "timeout",
37
+ /** Tests ran to completion and failed. */
38
+ ASSERTION: "assertion",
39
+ /** Every test finished and passed; a coverage floor was not met. */
40
+ THRESHOLD: "threshold",
41
+ /** Output was read and matched nothing this module knows. */
42
+ UNDIAGNOSED: "undiagnosed",
43
+ /** No output was available to read, so nothing can be said. */
44
+ UNCAPTURED: "uncaptured",
45
+ });
46
+
47
+ /** `Test timed out in 60000ms.` / `Hook timed out in 60000ms.` from vitest. */
48
+ const TIMEOUT_PATTERN = /(Test|Hook) timed out in (\d+)ms/g;
49
+
50
+ /**
51
+ * `ERROR: Coverage for statements (85.1%) does not meet global threshold (86%)`
52
+ * โ€” vitest's own wording, `global` or a quoted glob in the scope position.
53
+ */
54
+ const THRESHOLD_PATTERN =
55
+ /Coverage for (\S+) \(([\d.]+)%\) does not meet (\S+) threshold \(([\d.]+)%\)/g;
56
+
57
+ /** vitest's tally line: `Tests 4 failed | 14272 passed (14276)`. */
58
+ const TALLY_PATTERN = /Tests\s+(\d+) failed/;
59
+
60
+ /**
61
+ * A failing suite header: ` FAIL tests/unit/foo.test.ts > does a thing`.
62
+ *
63
+ * Horizontal whitespace only, never `\s`. Under the `m` flag `^\s*` can consume
64
+ * newline after newline before failing, which is super-linear backtracking on
65
+ * exactly the input this module is fed: a multi-megabyte suite transcript. The
66
+ * shipped ruleset refuses it, and this is a parser reading untrusted-sized
67
+ * output inside a git hook, so the refusal is right.
68
+ */
69
+ const FAIL_PATTERN = /^[ \t]*FAIL[ \t]+(\S+)/gm;
70
+
71
+ /**
72
+ * Which gate's property each kind of failure actually belongs to.
73
+ *
74
+ * Saying WHICH failure it was is half the repair. The other half is saying
75
+ * WHOSE it was, because the two gates involved legitimately share one prover:
76
+ * a coverage-instrumented suite proves `test-correctness` by passing and
77
+ * `coverage-adequacy` by clearing its floor, and one exit code cannot say
78
+ * which of the two it failed on. Reporting it against both is how a starved
79
+ * test suite rendered as a coverage regression โ€” and it is also why a real
80
+ * coverage regression could not be told from that flake.
81
+ *
82
+ * `undiagnosed` and `uncaptured` are deliberately absent. Nothing was
83
+ * recognised, so nothing may be attributed; the failure stays where it landed.
84
+ *
85
+ * The runner applies this only when the named gate is itself declared on the
86
+ * same command at the same moment, so a phrase in some unrelated tool's output
87
+ * can never invent an attribution.
88
+ */
89
+ export const ATTRIBUTION = Object.freeze({
90
+ timeout: "test-correctness",
91
+ assertion: "test-correctness",
92
+ threshold: "coverage-adequacy",
93
+ });
94
+
95
+ /** How many named examples a summary carries before it says "and N more". */
96
+ const MAX_EVIDENCE = 5;
97
+
98
+ /** Longest tail line quoted back when nothing else is recognised. */
99
+ const MAX_TAIL = 200;
100
+
101
+ /**
102
+ * How many trailing lines an unrecognised failure quotes back.
103
+ *
104
+ * Three rather than one, measured on this runner's own output: the last line
105
+ * of a failing task-runner chain is the runner's own `exited with code 1`,
106
+ * which says nothing the exit code did not. The line naming the artifact to
107
+ * rebuild was two above it.
108
+ */
109
+ const TAIL_LINES = 3;
110
+
111
+ /**
112
+ * One classified failure: the kind, a clause an operator can read, and the
113
+ * concrete lines that back it.
114
+ * @typedef {object} Diagnosis
115
+ * @property {string} kind One of `DIAGNOSIS`.
116
+ * @property {string} summary A single operator-readable clause.
117
+ * @property {string[]} evidence Concrete names or lines supporting the summary.
118
+ * @property {string|null} proves The gate whose property this failure belongs
119
+ * to, from `ATTRIBUTION`, or null when nothing was recognised.
120
+ */
121
+
122
+ /**
123
+ * Trim a list to `MAX_EVIDENCE`, saying how many were dropped.
124
+ * @param {string[]} items Every item found.
125
+ * @returns {string[]} At most `MAX_EVIDENCE + 1` lines.
126
+ */
127
+ function capped(items) {
128
+ const unique = [...new Set(items)];
129
+ if (unique.length <= MAX_EVIDENCE) return unique;
130
+ return [
131
+ ...unique.slice(0, MAX_EVIDENCE),
132
+ `โ€ฆand ${unique.length - MAX_EVIDENCE} more`,
133
+ ];
134
+ }
135
+
136
+ /**
137
+ * Every timeout the output reports, with the budget each one blew.
138
+ * @param {string} output The gate command's combined output.
139
+ * @returns {{count: number, budgets: number[]}} What was found.
140
+ */
141
+ function findTimeouts(output) {
142
+ const budgets = [...output.matchAll(TIMEOUT_PATTERN)].map(match =>
143
+ Number(match[2])
144
+ );
145
+ return { count: budgets.length, budgets };
146
+ }
147
+
148
+ /**
149
+ * Every coverage floor the output reports as unmet.
150
+ * @param {string} output The gate command's combined output.
151
+ * @returns {string[]} One `metric measured% < required% (scope)` per miss.
152
+ */
153
+ function findThresholdMisses(output) {
154
+ return [...output.matchAll(THRESHOLD_PATTERN)].map(
155
+ match => `${match[1]} ${match[2]}% < ${match[4]}% (${match[3]})`
156
+ );
157
+ }
158
+
159
+ /**
160
+ * The suites the output names as failing, and the tally if one was printed.
161
+ * @param {string} output The gate command's combined output.
162
+ * @returns {{tally: number|null, suites: string[]}} What was found.
163
+ */
164
+ function findFailures(output) {
165
+ const tally = TALLY_PATTERN.exec(output);
166
+ return {
167
+ tally: tally ? Number(tally[1]) : null,
168
+ suites: [...output.matchAll(FAIL_PATTERN)].map(match => match[1]),
169
+ };
170
+ }
171
+
172
+ /**
173
+ * The last lines that carry anything, for a failure nothing else recognised.
174
+ * @param {string} output The gate command's combined output.
175
+ * @returns {string[]} Up to `TAIL_LINES` trimmed lines, oldest first.
176
+ */
177
+ function tailLines(output) {
178
+ return output
179
+ .split("\n")
180
+ .map(line => line.trim())
181
+ .filter(line => line.length > 0)
182
+ .slice(-TAIL_LINES)
183
+ .map(line => line.slice(0, MAX_TAIL));
184
+ }
185
+
186
+ /**
187
+ * A timeout verdict, worded so it can never be mistaken for a coverage miss.
188
+ * @param {{count: number, budgets: number[]}} timeouts What was found.
189
+ * @param {string[]} suites Suites the output named as failing.
190
+ * @returns {Diagnosis} The verdict.
191
+ */
192
+ function timeoutVerdict(timeouts, suites) {
193
+ const budget = Math.max(...timeouts.budgets);
194
+ return {
195
+ kind: DIAGNOSIS.TIMEOUT,
196
+ summary:
197
+ `${timeouts.count} test(s)/hook(s) exceeded the ${budget}ms budget, ` +
198
+ `so the suite did not finish โ€” this is NOT a coverage shortfall`,
199
+ evidence: capped(suites),
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Classify why a gate command failed, from the output it produced.
205
+ *
206
+ * Ordered deliberately, and the order is the content of this function: a
207
+ * timeout outranks an assertion failure outranks a threshold miss, because
208
+ * coverage read off a run that did not finish measures the interruption rather
209
+ * than the code. Getting that backwards is the defect being fixed โ€” it is what
210
+ * printed "coverage-adequacy failed" six times for a machine under load.
211
+ * @param {string|null|undefined} output The command's combined output, or null
212
+ * when the runner could not capture it.
213
+ * @returns {object} What the failure was, before it is attributed.
214
+ */
215
+ function classify(output) {
216
+ if (typeof output !== "string" || output.length === 0) {
217
+ return {
218
+ kind: DIAGNOSIS.UNCAPTURED,
219
+ summary:
220
+ "no output was captured, so this failure has no diagnosis " +
221
+ "(set LISA_GATES_CAPTURE=1 and re-run to get one)",
222
+ evidence: [],
223
+ };
224
+ }
225
+
226
+ const timeouts = findTimeouts(output);
227
+ const failures = findFailures(output);
228
+ const misses = findThresholdMisses(output);
229
+
230
+ if (timeouts.count > 0) return timeoutVerdict(timeouts, failures.suites);
231
+
232
+ if ((failures.tally ?? 0) > 0 || failures.suites.length > 0) {
233
+ const count = failures.tally ?? failures.suites.length;
234
+ return {
235
+ kind: DIAGNOSIS.ASSERTION,
236
+ summary: `${count} test(s) ran and failed`,
237
+ evidence: capped(failures.suites),
238
+ };
239
+ }
240
+
241
+ if (misses.length > 0) {
242
+ return {
243
+ kind: DIAGNOSIS.THRESHOLD,
244
+ summary: `coverage is below the declared floor on ${misses.length} metric(s)`,
245
+ evidence: capped(misses),
246
+ };
247
+ }
248
+
249
+ return {
250
+ kind: DIAGNOSIS.UNDIAGNOSED,
251
+ summary: "no recognised failure signature; the command's last lines follow",
252
+ evidence: capped(tailLines(output)),
253
+ };
254
+ }
255
+
256
+ /**
257
+ * Classify a failure and say whose property it belongs to.
258
+ *
259
+ * Attribution is separated from classification on purpose. Reading a
260
+ * transcript is a fact about a tool's output; deciding which gate that fact
261
+ * indicts is a fact about the registry, and only the caller knows whether the
262
+ * indicted gate is part of the run at all.
263
+ * @param {string|null|undefined} output The command's combined output, or null
264
+ * when the runner could not capture it.
265
+ * @returns {Diagnosis} What the failure was, and whose it was.
266
+ */
267
+ export function diagnoseFailure(output) {
268
+ const verdict = classify(output);
269
+ return { ...verdict, proves: ATTRIBUTION[verdict.kind] ?? null };
270
+ }
@@ -134,6 +134,26 @@ export const GATE_FIELDS = new Set(["run", "needs", "task"]);
134
134
  * whoever remembers to type it, which is how the defect got here.
135
135
  */
136
136
 
137
+ /**
138
+ * Registry flag: this gate's task costs minutes, not seconds.
139
+ *
140
+ * It exists so that a run which is already blocked can keep going without
141
+ * doubling the cost of a failing push. When a required gate failed, the runner
142
+ * used to stop dead and print every later gate as not-run โ€” which is how one
143
+ * intermittent test failure took the work-item check and the type check down
144
+ * with it, both of which finish in well under a minute and answer questions
145
+ * that have nothing to do with a test suite. Continuing into those is nearly
146
+ * free and tells the operator everything that is wrong in one attempt instead
147
+ * of one thing per attempt. Continuing into a second full suite is not free,
148
+ * and buys information about a push that cannot land regardless.
149
+ *
150
+ * Registry-only, for the same reason `mayRewrite` is. The safe answer must not
151
+ * depend on whoever remembers to type it, and Lisa knows which of its own
152
+ * canonical gates run a whole suite. A project that points `test-correctness`
153
+ * at something fast loses nothing: the flag only ever suppresses a gate on a
154
+ * run that is already blocked.
155
+ */
156
+
137
157
  /** Prefix marking a gate, or a config key, that this project invented. */
138
158
  export const CUSTOM_PREFIX = "x-";
139
159
 
@@ -205,6 +225,7 @@ export const REGISTRY = Object.freeze({
205
225
  task: "test:unit",
206
226
  moments: PUSH_ONWARD,
207
227
  work: "tests run",
228
+ costly: true,
208
229
  },
209
230
  "test-node-suites": {
210
231
  label: "๐Ÿงช Run .mjs Suites",
@@ -219,6 +240,7 @@ export const REGISTRY = Object.freeze({
219
240
  task: "test:integration",
220
241
  moments: PUSH_ONWARD,
221
242
  work: "tests run",
243
+ costly: true,
222
244
  },
223
245
  "test-meaningfulness": {
224
246
  label: "๐Ÿงฌ Mutation Testing Gate",
@@ -226,6 +248,7 @@ export const REGISTRY = Object.freeze({
226
248
  task: "test:mutation",
227
249
  moments: PR_ONWARD,
228
250
  work: "mutants generated",
251
+ costly: true,
229
252
  },
230
253
  "coverage-adequacy": {
231
254
  label: "โœ… Verification Coverage",
@@ -233,6 +256,7 @@ export const REGISTRY = Object.freeze({
233
256
  task: "test:coverage",
234
257
  moments: PUSH_ONWARD,
235
258
  work: "files measured",
259
+ costly: true,
236
260
  },
237
261
  "e2e-browser": {
238
262
  label: "๐ŸŽญ Playwright E2E Tests",
@@ -240,6 +264,7 @@ export const REGISTRY = Object.freeze({
240
264
  task: "test:e2e",
241
265
  moments: [...PR_ONWARD, CONTINUOUS],
242
266
  work: "specs run",
267
+ costly: true,
243
268
  },
244
269
  "e2e-native": {
245
270
  label: "๐Ÿ“ฑ Maestro Native E2E",
@@ -247,6 +272,7 @@ export const REGISTRY = Object.freeze({
247
272
  task: "test:e2e:native",
248
273
  moments: [...PR_ONWARD, CONTINUOUS],
249
274
  work: "flows run",
275
+ costly: true,
250
276
  },
251
277
  // ---------------------------------------------------------------------
252
278
  // Environment facade. Lisa defines and enforces the interface; each project
@@ -297,6 +323,7 @@ export const REGISTRY = Object.freeze({
297
323
  // pinned to the per-change gate re-walks the same ground forever.
298
324
  moments: [...PR_ONWARD, CONTINUOUS],
299
325
  work: "cases generated",
326
+ costly: true,
300
327
  },
301
328
  "structural-rules": {
302
329
  label: "๐Ÿ”Ž AST Grep Scan",
@@ -381,6 +408,7 @@ export const REGISTRY = Object.freeze({
381
408
  task: "perf:load",
382
409
  moments: DEPLOY_ONLY,
383
410
  work: "requests issued",
411
+ costly: true,
384
412
  },
385
413
  accessibility: {
386
414
  label: "โ™ฟ Accessibility",
@@ -1400,6 +1428,7 @@ export function resolveMoment({
1400
1428
  work: null,
1401
1429
  evidence: null,
1402
1430
  mayRewrite: false,
1431
+ costly: false,
1403
1432
  });
1404
1433
  }
1405
1434
  continue;
@@ -1432,6 +1461,7 @@ export function resolveMoment({
1432
1461
  work: definition?.work ?? null,
1433
1462
  evidence: entry.await ? mergeEvidence(entry.evidence) : null,
1434
1463
  mayRewrite: definition?.mayRewrite === true,
1464
+ costly: definition?.costly === true,
1435
1465
  });
1436
1466
  }
1437
1467
  // Rewriters first, then alphabetical within each group. See `mayRewrite`: