@codyswann/lisa 3.48.6 → 3.49.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 (68) hide show
  1. package/dist/cli/doctor-hook-copy-parity.d.ts +8 -0
  2. package/dist/cli/doctor-hook-copy-parity.d.ts.map +1 -0
  3. package/dist/cli/doctor-hook-copy-parity.js +121 -0
  4. package/dist/cli/doctor-hook-copy-parity.js.map +1 -0
  5. package/dist/cli/doctor.d.ts.map +1 -1
  6. package/dist/cli/doctor.js +6 -0
  7. package/dist/cli/doctor.js.map +1 -1
  8. package/dist/core/hook-copy-parity.d.ts +81 -0
  9. package/dist/core/hook-copy-parity.d.ts.map +1 -0
  10. package/dist/core/hook-copy-parity.js +111 -0
  11. package/dist/core/hook-copy-parity.js.map +1 -0
  12. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  13. package/dist/core/upstream-evidence-manifest.js +14 -2
  14. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  15. package/package.json +1 -1
  16. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  18. package/plugins/lisa-agy/plugin.json +1 -1
  19. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  22. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  28. package/plugins/lisa-expo-agy/plugin.json +1 -1
  29. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  38. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  39. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  44. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  49. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-rails-agy/plugin.json +1 -1
  54. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  59. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  64. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  66. package/scripts/check-derived-artifacts.mjs +22 -4
  67. package/scripts/generate-upstream-evidence-manifest.mjs +10 -1
  68. package/scripts/lib/upstream-manifest-staleness.mjs +235 -0
@@ -0,0 +1,235 @@
1
+ /**
2
+ * upstream-manifest-staleness — turn "the manifest is out of date" into "here is
3
+ * what moved underneath it".
4
+ *
5
+ * @remarks
6
+ * `src/core/upstream-evidence-manifest.ts` is derived from two things: the set
7
+ * of TRACKED files (`git ls-files`, so the git index), and the working-tree
8
+ * bytes of every tracked file under a packaged-evidence prefix. Its `--check`
9
+ * regenerates and compares byte-for-byte, so it can say *that* the file is out
10
+ * of date, and until CodySwannGT/lisa#2852 that is all it said:
11
+ *
12
+ * ```
13
+ * src/core/upstream-evidence-manifest.ts is stale; run bun run build:upstream-evidence-manifest
14
+ * ```
15
+ *
16
+ * The refusal is accurate and points away from the cause. It names the file
17
+ * that is out of date, so the obvious response is to regenerate that file — and
18
+ * when the cause is an input that moves again afterwards, regenerating
19
+ * reproduces the identical refusal. That is a loop with no exit signposted from
20
+ * inside it, and it cost a full cycle.
21
+ *
22
+ * There are exactly two ways an input can move, and they need opposite fixes:
23
+ *
24
+ * - **The bytes of a hash-pinned file changed.** Overwhelmingly this is
25
+ * `lint-staged`, which runs `oxlint --fix`, `eslint --fix` and
26
+ * `prettier --write` over every staged file at commit time — *after* the
27
+ * author regenerated, and *before* the artifact gate runs. The fix is to
28
+ * regenerate again now, after the reformat.
29
+ * - **The tracked set changed.** A file staged (or `git rm`'d) after the
30
+ * manifest was generated is a membership change, and regenerating before the
31
+ * next `git add` will produce the same wrong answer. The fix is to stage
32
+ * first and regenerate second.
33
+ *
34
+ * A third outcome is possible and worth naming: no input moved at all, which
35
+ * means the generated file itself was rewritten after generation — a hand edit,
36
+ * or a formatter reaching the artifact.
37
+ *
38
+ * ## What this module deliberately does not claim
39
+ *
40
+ * The obvious story — "the manifest hashes the ledger, so a stale manifest means
41
+ * a stale ledger" — is false, and was measured false while fixing #2852:
42
+ * `src/core/lisa-owned-hash-ledger.ts` lives outside every packaged-evidence
43
+ * prefix, so the manifest records its PATH and never its bytes. Mutating the
44
+ * ledger's contents leaves the manifest check passing. The two artifacts are
45
+ * independent in content; what couples them is a shared INPUT. Editing a
46
+ * `copy-overwrite` template moves bytes both of them record, so both go stale
47
+ * together — which is why the message below sends a template change to
48
+ * regenerate the ledger too, and says nothing of the sort for any other path.
49
+ *
50
+ * @module scripts/lib/upstream-manifest-staleness
51
+ */
52
+
53
+ /** Marker identifying a template whose bytes the Lisa-owned hash ledger records. */
54
+ const COPY_OVERWRITE = "/copy-overwrite/";
55
+
56
+ /** Longest list of paths a refusal prints before summarizing the remainder. */
57
+ const MAX_LISTED_PATHS = 10;
58
+
59
+ /**
60
+ * @typedef {object} ManifestInputs
61
+ * @property {Map<string, string>} evidence - Hash-pinned path to sha256 digest.
62
+ * @property {Set<string>} surface - Every tracked path recorded at generation.
63
+ */
64
+
65
+ /**
66
+ * @typedef {object} ManifestDiagnosis
67
+ * @property {string[]} changed - Pinned paths whose recorded digest no longer matches.
68
+ * @property {string[]} added - Paths present now and absent when the file was generated.
69
+ * @property {string[]} removed - Paths recorded then and absent now.
70
+ */
71
+
72
+ /**
73
+ * The body of one `Object.freeze({ … })` block in the generated module.
74
+ *
75
+ * Sliced by export name rather than matched with one regex over the whole file,
76
+ * because the public-commit block renders `<sha>: true,` — the same shape as a
77
+ * tracked-surface entry. A single sweep reads 40-character SHAs as file paths
78
+ * and then reports thousands of phantom removals.
79
+ * @param {string} source - Generated module source.
80
+ * @param {string} exportName - Name of the exported constant to slice.
81
+ * @returns {string} The block body, or an empty string when it is absent.
82
+ */
83
+ function blockFor(source, exportName) {
84
+ const start = source.indexOf(`export const ${exportName}`);
85
+ if (start === -1) return "";
86
+ const open = source.indexOf("Object.freeze({", start);
87
+ if (open === -1) return "";
88
+ const close = source.indexOf("\n });", open);
89
+ if (close === -1) return "";
90
+ return source.slice(open, close);
91
+ }
92
+
93
+ /**
94
+ * The inputs a generated manifest says it was built from.
95
+ * @param {string} source - Contents of `src/core/upstream-evidence-manifest.ts`.
96
+ * @returns {ManifestInputs} Recorded evidence digests and tracked-surface paths.
97
+ */
98
+ export function parseManifestInputs(source) {
99
+ const evidence = new Map();
100
+ for (const match of blockFor(source, "UPSTREAM_EVIDENCE_MANIFEST").matchAll(
101
+ /^ {4}"([^"]+)":\n {6}"([a-f0-9]{64})",$/gmu
102
+ )) {
103
+ evidence.set(match[1], match[2]);
104
+ }
105
+
106
+ const surface = new Set();
107
+ for (const match of blockFor(source, "UPSTREAM_SURFACE_MANIFEST").matchAll(
108
+ /^ {4}(?:"([^"]+)"|([A-Za-z_$][A-Za-z0-9_$]*)): true,$/gmu
109
+ )) {
110
+ surface.add(match[1] ?? match[2]);
111
+ }
112
+
113
+ return { evidence, surface };
114
+ }
115
+
116
+ /**
117
+ * What moved between the checked-in manifest and a fresh regeneration.
118
+ *
119
+ * Direction matters: `current` is what is on disk and `fresh` is the truth, so
120
+ * a path in `fresh` alone was staged after generation and a path in `current`
121
+ * alone was removed after it.
122
+ * @param {string} current - The manifest as it exists on disk.
123
+ * @param {string} fresh - The manifest as it would be generated right now.
124
+ * @returns {ManifestDiagnosis} Inputs that changed, were added, or were removed.
125
+ */
126
+ export function diagnoseStaleManifest(current, fresh) {
127
+ const before = parseManifestInputs(current);
128
+ const after = parseManifestInputs(fresh);
129
+
130
+ const changed = [...after.evidence.entries()]
131
+ .filter(
132
+ ([file, digest]) =>
133
+ before.evidence.has(file) && before.evidence.get(file) !== digest
134
+ )
135
+ .map(([file]) => file)
136
+ .sort();
137
+ const added = [...after.surface]
138
+ .filter(file => !before.surface.has(file))
139
+ .sort();
140
+ const removed = [...before.surface]
141
+ .filter(file => !after.surface.has(file))
142
+ .sort();
143
+
144
+ return { changed, added, removed };
145
+ }
146
+
147
+ /**
148
+ * Render at most `MAX_LISTED_PATHS` paths, then say how many were withheld.
149
+ * @param {readonly string[]} paths - Paths to render, already sorted.
150
+ * @returns {string} Indented lines, one path each.
151
+ */
152
+ function listPaths(paths) {
153
+ const shown = paths
154
+ .slice(0, MAX_LISTED_PATHS)
155
+ .map(file => ` ${file}`)
156
+ .join("\n");
157
+ const withheld = paths.length - MAX_LISTED_PATHS;
158
+ return withheld > 0 ? `${shown}\n …and ${withheld} more` : shown;
159
+ }
160
+
161
+ /**
162
+ * The operator-facing refusal for a stale manifest.
163
+ *
164
+ * Written for whoever is standing at the gate, which is not necessarily whoever
165
+ * wrote the generator: it names the input that moved, why it usually moves, and
166
+ * the command that clears it.
167
+ * @param {ManifestDiagnosis} diagnosis - Output of {@link diagnoseStaleManifest}.
168
+ * @returns {string} Multi-line refusal text.
169
+ */
170
+ export function describeStaleManifest(diagnosis) {
171
+ const sections = ["src/core/upstream-evidence-manifest.ts is stale."];
172
+
173
+ if (diagnosis.changed.length > 0) {
174
+ sections.push(
175
+ " Its inputs moved after it was generated. These files no longer have\n" +
176
+ " the bytes it recorded:\n" +
177
+ `${listPaths(diagnosis.changed)}\n` +
178
+ " At commit time lint-staged reformats every staged file BEFORE this\n" +
179
+ " check runs, so a manifest generated first is stale by the time it is\n" +
180
+ " checked. Regenerate now, after the reformat."
181
+ );
182
+ }
183
+
184
+ if (diagnosis.added.length > 0 || diagnosis.removed.length > 0) {
185
+ const movements = [];
186
+ if (diagnosis.added.length > 0) {
187
+ movements.push(
188
+ ` Staged since it was generated:\n${listPaths(diagnosis.added)}`
189
+ );
190
+ }
191
+ if (diagnosis.removed.length > 0) {
192
+ movements.push(
193
+ ` Removed since it was generated:\n${listPaths(diagnosis.removed)}`
194
+ );
195
+ }
196
+ sections.push(
197
+ " The tracked file set moved after it was generated.\n" +
198
+ `${movements.join("\n")}\n` +
199
+ " The manifest reads TRACKED files, so `git add` (or `git rm`) first and\n" +
200
+ " regenerate second — the other order records the pre-change file set."
201
+ );
202
+ }
203
+
204
+ if (
205
+ diagnosis.changed.length === 0 &&
206
+ diagnosis.added.length === 0 &&
207
+ diagnosis.removed.length === 0
208
+ ) {
209
+ sections.push(
210
+ " None of its inputs moved — only the generated file's own bytes differ,\n" +
211
+ " so something rewrote it after generation: a hand edit, or a formatter\n" +
212
+ " reaching the artifact. It is generated; do not edit it by hand."
213
+ );
214
+ }
215
+
216
+ sections.push(
217
+ " Fix: bun run build:upstream-evidence-manifest\n" +
218
+ " Then: git add src/core/upstream-evidence-manifest.ts"
219
+ );
220
+
221
+ if (
222
+ [...diagnosis.changed, ...diagnosis.added, ...diagnosis.removed].some(
223
+ file => file.includes(COPY_OVERWRITE)
224
+ )
225
+ ) {
226
+ sections.push(
227
+ " A copy-overwrite template is among the files above, so the Lisa-owned\n" +
228
+ " hash ledger records those same bytes and is stale for the same reason.\n" +
229
+ " Regenerate it in this commit too, or its own check fails next:\n" +
230
+ " bun run build:lisa-owned-hash-ledger"
231
+ );
232
+ }
233
+
234
+ return sections.join("\n\n");
235
+ }