@codyswann/lisa 4.15.1 → 4.17.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 (79) hide show
  1. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +62 -9
  2. package/dist/cli/doctor-override-floor-conflicts.d.ts +15 -0
  3. package/dist/cli/doctor-override-floor-conflicts.d.ts.map +1 -0
  4. package/dist/cli/doctor-override-floor-conflicts.js +225 -0
  5. package/dist/cli/doctor-override-floor-conflicts.js.map +1 -0
  6. package/dist/cli/doctor.d.ts.map +1 -1
  7. package/dist/cli/doctor.js +8 -0
  8. package/dist/cli/doctor.js.map +1 -1
  9. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  10. package/dist/core/lisa-owned-hash-ledger.js +2 -0
  11. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  12. package/dist/core/override-floors.d.ts +146 -0
  13. package/dist/core/override-floors.d.ts.map +1 -0
  14. package/dist/core/override-floors.js +200 -0
  15. package/dist/core/override-floors.js.map +1 -0
  16. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  17. package/dist/core/upstream-evidence-manifest.js +8 -2
  18. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  19. package/dist/opencode/plugin-templates/parity-safety-net.sh +62 -9
  20. package/dist/strategies/package-lisa.d.ts +59 -0
  21. package/dist/strategies/package-lisa.d.ts.map +1 -1
  22. package/dist/strategies/package-lisa.js +112 -130
  23. package/dist/strategies/package-lisa.js.map +1 -1
  24. package/package.json +1 -1
  25. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  27. package/plugins/lisa/hooks/parity-safety-net.sh +62 -9
  28. package/plugins/lisa-agy/hooks/parity-safety-net.sh +62 -9
  29. package/plugins/lisa-agy/plugin.json +1 -1
  30. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  33. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +62 -9
  37. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +62 -9
  39. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  41. package/plugins/lisa-expo-agy/plugin.json +1 -1
  42. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  47. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  51. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  52. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  56. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  57. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  61. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  62. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  66. package/plugins/lisa-rails-agy/plugin.json +1 -1
  67. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  71. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  72. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  76. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  77. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  79. package/plugins/src/base/hooks/parity-safety-net.sh +62 -9
@@ -61,12 +61,20 @@
61
61
  # can match — including one whose target sits in the SAME quoted run as the
62
62
  # prose, which the #3106 scoping deliberately does not reach. Upstream exempts
63
63
  # those through an engine-only DISPLAY_COMMANDS list that a grep hook cannot
64
- # replicate. The same text-scan limit means the rm guard treats
64
+ # replicate. WHAT TO DO WHEN YOU HIT IT (issue #3191): do not reword the prose.
65
+ # When the destructive text is a commit message, an issue body or a results
66
+ # table, the string IS the deliverable and there is no narrower spelling that
67
+ # still says what it must say. Write the text to a file with the Write tool and
68
+ # pass it BY PATH — `gh issue comment --body-file <file>`, `git commit -F
69
+ # <file>` — because these guards scan the COMMAND and never the file. block()
70
+ # prints this at the wall so the operator is told rather than left to find it.
71
+ # The same text-scan limit means the rm guard treats
65
72
  # a substitution-wrapped catastrophic delete as verdict-neutral (issue #1982): an
66
73
  # executable `echo "$(rm -rf /)"` is blocked, but so are inert twins the shell
67
74
  # would never expand — a single-quoted `echo '$(rm -rf /)'` or an escaped
68
75
  # `echo "\$(rm -rf /)"` — because the scan has no quote-context awareness. That
69
- # over-block stays inside this accepted class. Workaround: quote-break the string
76
+ # over-block stays inside this accepted class. Workaround: the write-to-a-file
77
+ # and pass-by-path form above, or quote-break the string,
70
78
  # or use the gh-writer heredoc form, whose payload is stripped before the guards
71
79
  # run — spelled with a QUOTE-PAIR delimiter (`<<'EOF'`). That exact spelling is
72
80
  # what the SAFE path recognises; `<<"EOF"` and `<<\EOF` do not reach it, and
@@ -115,15 +123,58 @@ if [ -z "$command_str" ]; then
115
123
  exit 0
116
124
  fi
117
125
 
126
+ # Guidance printed under an ordinary destructive-guard refusal.
127
+ #
128
+ # It teaches the remedy at the wall, the way block_heredoc() does (gardener
129
+ # #1789, extended by issue #3191). It used to end in "narrow the command so it
130
+ # no longer matches the guard", which is advice the accepted prose class CANNOT
131
+ # follow: when the destructive text is a commit message, an issue body or a
132
+ # results table, the string IS the deliverable and there is no narrower spelling
133
+ # that still says what it must say. An operator acts on a printed remedy, so a
134
+ # remedy that cannot be performed is worse than none — it spends the detour and
135
+ # teaches that the refusal was a formality. The remedy that does work is the
136
+ # sibling heredoc one, one surface over: write the text with the Write tool,
137
+ # then pass it BY PATH. The guards scan the COMMAND and never the file, so the
138
+ # path form carries the text through intact.
139
+ #
140
+ # Both branches are printed on every ordinary refusal, deliberately: this guard
141
+ # is a text scan and CANNOT tell which one applies, so naming both and letting
142
+ # the reader pick is the only honest shape. Deciding for them is what the
143
+ # engine-only DISPLAY_COMMANDS list does, and the header explains why a grep
144
+ # hook cannot have one.
145
+ DESTRUCTIVE_GUIDANCE="$(
146
+ cat <<'EOF'
147
+ This command matched a destructive-operation guard. The guard is a text scan,
148
+ not a shell engine, so it cannot tell a destructive command you are RUNNING
149
+ from the same text quoted as CONTENT you are writing. Take the branch that
150
+ describes yours:
151
+
152
+ - The destructive text is only QUOTED CONTENT — a commit message, an issue or
153
+ PR body, a report, a results table, a note. Do not try to reword it; the
154
+ string is the deliverable and no narrower spelling still says what it must.
155
+ Write the text to a file with the Write tool and pass it BY PATH instead:
156
+ `gh issue comment --body-file <file>`, `gh pr create --body-file <file>`,
157
+ `git commit -F <file>`, or read the file back with the Read tool. This guard
158
+ scans the command, never the file, so the path form goes through intact.
159
+
160
+ - You genuinely intend to run the destructive operation. Ask the user to
161
+ confirm, then run it manually outside the agent.
162
+ EOF
163
+ )"
164
+
118
165
  # block() prints the reason to stderr (surfaced to the model) and exits 2 so the
119
166
  # Bash tool call is denied. $1 = human-readable reason for the block.
167
+ #
168
+ # $2 = optional guidance REPLACING $DESTRUCTIVE_GUIDANCE. block_heredoc() passes
169
+ # its own, because the two remedies are mutually exclusive by construction
170
+ # (issue #1958 F6): a heredoc payload IS executable shell, so the quoted-content
171
+ # branch is exactly the misdirection that finding removed, and the file-based
172
+ # execution advice is exactly what an ordinary prose refusal must not be given.
120
173
  block() {
121
174
  cat >&2 <<EOF
122
175
  Blocked by safety-net: $1
123
176
 
124
- This command matched a destructive-operation guard. If it is genuinely safe and
125
- intentional, ask the user to confirm, then run it manually outside the agent, or
126
- narrow the command so it no longer matches the guard.
177
+ ${2:-$DESTRUCTIVE_GUIDANCE}
127
178
  EOF
128
179
  exit 2
129
180
  }
@@ -199,20 +250,22 @@ EOF
199
250
  # depends on the command shape (issue #1958): `git commit -m "$(cat <<EOF …)"`
200
251
  # attempts get the commit -F text; every other heredoc denial gets the
201
252
  # file-based execution guidance instead — the commit text is misleading there.
253
+ # Both are passed as block()'s SECOND argument, which REPLACES the ordinary
254
+ # destructive guidance rather than appending to it (issue #3191): that guidance
255
+ # offers a quoted-content branch, and a heredoc payload is executable shell, so
256
+ # printing it here would reintroduce the misdirection #1958 F6 removed.
202
257
  # The git-commit detection inlines the GIT_GLOBAL_OPTS shape (defined later in
203
258
  # this file, after the heredoc dispatch runs) so `git -C <path> commit` and
204
259
  # `git -c k=v commit` spellings are still recognized.
205
260
  block_heredoc() {
206
261
  if scan -E "$command_str" \
207
262
  '(^|[^[:alnum:]_-])git[[:space:]]+(-[^;&|[:space:]]+([[:space:]]+[^-;&|[:space:]][^;&|[:space:]]*)?[[:space:]]+)*commit([^[:alnum:]_-]|$)'; then
208
- block "$1
209
- Heredoc commit invocations are blocked (the payload is executable shell).
263
+ block "$1" "Heredoc commit invocations are blocked (the payload is executable shell).
210
264
  Fix: write the commit message to a file and run \`git commit -F <file>\`.
211
265
  Every commit must also carry a Co-authored-by trailer for a supported agent
212
266
  (Claude/Codex/OpenCode) — the commit-msg hook enforces this."
213
267
  fi
214
- block "$1
215
- Heredoc payloads are blocked here (the payload is executable shell).
268
+ block "$1" "Heredoc payloads are blocked here (the payload is executable shell).
216
269
  Fix: write the payload to a file with the Write tool, then execute that file
217
270
  directly (for example \`python3 <file>\` or \`bash <file>\`)."
218
271
  }
@@ -1,5 +1,6 @@
1
1
  import type { FileOperationResult, ProjectType } from "../core/config.js";
2
2
  import type { ICopyStrategy, StrategyContext } from "./strategy.interface.js";
3
+ import { type OverrideFloorAudit } from "../core/override-floors.js";
3
4
  /**
4
5
  * @file package-lisa.ts
5
6
  * @description Package.lisa.json strategy for governance-driven package.json management
@@ -24,6 +25,34 @@ import type { ICopyStrategy, StrategyContext } from "./strategy.interface.js";
24
25
  * Child types override parent values in each section.
25
26
  * @module strategies
26
27
  */
28
+ /** Which of the two apply paths an override-floor audit simulated. */
29
+ export type ApplyPathLabel = "postinstall" | "full apply";
30
+ /** One apply path's answer: an audit, or the reason there is none. */
31
+ export interface OverrideFloorPathAudit {
32
+ /** Apply path simulated. */
33
+ readonly applyPath: ApplyPathLabel;
34
+ /** What the path inspected and found, or null when it could not run. */
35
+ readonly audit: OverrideFloorAudit | null;
36
+ /** Why the path could not run, or null when it did. */
37
+ readonly error: string | null;
38
+ }
39
+ /** Everything one override-floor audit of a project looked at. */
40
+ export interface OverrideFloorAuditReport {
41
+ /**
42
+ * Literal (non-`$name`) entries across the resolved template's
43
+ * `force.overrides` and `force.resolutions` — the floors Lisa would write.
44
+ *
45
+ * Zero is the inert case, not a clean one: it means template resolution
46
+ * produced nothing to compare against, so the audit below cannot have
47
+ * verified anything. Callers must not read an empty conflict list as a pass
48
+ * without reading this first.
49
+ */
50
+ readonly lisaFloors: number;
51
+ /** Project types the apply itself would detect here. */
52
+ readonly detectedTypes: readonly ProjectType[];
53
+ /** One entry per apply path. */
54
+ readonly paths: readonly OverrideFloorPathAudit[];
55
+ }
27
56
  /**
28
57
  * Package.lisa.json strategy: Manage package.json via separate template files
29
58
  * - Loads templates from all applicable types in inheritance chain
@@ -58,6 +87,36 @@ export declare class PackageLisaStrategy implements ICopyStrategy {
58
87
  * @returns Package document that apply would persist
59
88
  */
60
89
  planPackageJson(projectJson: Record<string, unknown>, detectedTypes: readonly ProjectType[], lisaDir: string, securityPinsOnly?: boolean): Promise<Record<string, unknown>>;
90
+ /**
91
+ * Report every `$name` override that would resolve BELOW a Lisa floor, before
92
+ * an apply refuses over it.
93
+ * @remarks
94
+ * Deliberately re-runs the apply's own resolution — the same type detection,
95
+ * the same template inheritance chain, the same force/preserve/adopt phases —
96
+ * rather than re-deriving the answer from `package.lisa.json` and the host
97
+ * manifest side by side. A second implementation of "what would the merged
98
+ * manifest look like" is a second thing to keep in step with this file, and
99
+ * the whole value of the report is that it agrees with the refusal.
100
+ *
101
+ * BOTH apply paths are simulated. The postinstall path applies a template
102
+ * restricted to security pins and pulls in the direct dependencies backing
103
+ * them, so it can accept a manifest the unrestricted path refuses and vice
104
+ * versa. Reporting only one leaves an operator refused by the other with a
105
+ * doctor line that said nothing.
106
+ * @param projectDir - Host project directory containing package.json
107
+ * @param lisaDir - Installed Lisa package root
108
+ * @returns What was inspected on each path, and every conflict found
109
+ */
110
+ auditOverrideFloors(projectDir: string, lisaDir: string): Promise<OverrideFloorAuditReport>;
111
+ /**
112
+ * Simulate one apply path and audit the manifest it would produce.
113
+ * @param projectJson - Parsed host package.json
114
+ * @param merged - Fully resolved template for the detected types
115
+ * @param applyPath - Which path to simulate
116
+ * @returns The path's audit, or the reason it could not be produced
117
+ * @private
118
+ */
119
+ private auditOneApplyPath;
61
120
  /**
62
121
  * Apply package-lisa strategy: Load templates from inheritance chain, apply to package.json
63
122
  * @remarks
@@ -1 +1 @@
1
- {"version":3,"file":"package-lisa.d.ts","sourceRoot":"","sources":["../../src/strategies/package-lisa.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAE1E,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AA8B9E;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;;;;;;GAMG;AACH,qBAAa,mBAAoB,YAAW,aAAa;IAUrD,OAAO,CAAC,QAAQ,CAAC,mBAAmB;IATtC,QAAQ,CAAC,IAAI,EAAG,cAAc,CAAU;IAExC;;;;;OAKG;gBAEgB,mBAAmB,GAAE,MAAM,MAA0B;IAGxE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAkB;IAC/C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAmB;IACjD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAc;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAc;IACvC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAmB;IACjD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAc;IACvC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA0C;IAC5E,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAGhC;IAEF;;;;;;;OAOG;IACG,eAAe,CACnB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,aAAa,EAAE,SAAS,WAAW,EAAE,EACrC,OAAO,EAAE,MAAM,EACf,gBAAgB,UAAQ,GACvB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAmBnC;;;;;;;;;;;;;OAaG;IACG,KAAK,CACT,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,mBAAmB,CAAC;IA+D/B;;;;;;;;;OASG;YACW,iBAAiB;IAmB/B;;;;;;;;;OASG;YACW,iBAAiB;IAwC/B;;;;;;;;OAQG;YACW,gBAAgB;IAkC9B;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,OAAO,CAAC,sBAAsB;IAsB9B;;;;;;;;;OASG;IACH,OAAO,CAAC,wBAAwB;IAsChC;;;;;;OAMG;YACW,kBAAkB;IA0DhC;;;;;;OAMG;YACW,+BAA+B;IAkB7C;;;;;;OAMG;YACW,wBAAwB;IAgBtC;;;;;;OAMG;YACW,8BAA8B;IAmB5C;;;;;;OAMG;YACW,gBAAgB;IAW9B;;;;;;;;;OASG;YACW,qBAAqB;IA0CnC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,mBAAmB;IAgB3B;;;;;;;;OAQG;IACH,OAAO,CAAC,cAAc;IAgBtB;;;;;;;OAOG;IACH,OAAO,CAAC,kBAAkB;IAmB1B;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,aAAa;IAkDrB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,mBAAmB;IAwB3B;;;;;;;OAOG;IACH,OAAO,CAAC,kBAAkB;IAsB1B;;;;;;;;OAQG;IACH,OAAO,CAAC,iBAAiB;CA2B1B"}
1
+ {"version":3,"file":"package-lisa.d.ts","sourceRoot":"","sources":["../../src/strategies/package-lisa.ts"],"names":[],"mappings":"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"}
@@ -8,32 +8,9 @@ import { PROJECT_TYPE_HIERARCHY, PROJECT_TYPE_ORDER } from "../core/config.js";
8
8
  import { ensureParentDir } from "../utils/file-operations.js";
9
9
  import { readJson, writeJson, deepMerge, readJsonOrNull, } from "../utils/json-utils.js";
10
10
  import { JsonMergeError } from "../errors/index.js";
11
+ import { asRecord, auditSelfReferenceRewrites, collectDirectDependencyNames, describeSelfReferenceRemedy, DIRECT_DEPENDENCY_SECTIONS, OVERRIDE_SECTIONS, } from "../core/override-floors.js";
11
12
  import { LISA_PACKAGE_NAME } from "../core/self-apply.js";
12
13
  import { getPackageVersion } from "../cli/version.js";
13
- /**
14
- * @file package-lisa.ts
15
- * @description Package.lisa.json strategy for governance-driven package.json management
16
- *
17
- * Implements a two-file approach to package.json governance:
18
- * - Source: package.lisa.json files in type directories (all/, typescript/, expo/, etc.)
19
- * - Destination: project's package.json
20
- *
21
- * Behavior is defined in package.lisa.json:
22
- * - force: Lisa's values completely replace project's values
23
- * - adopt: Lisa reclaims a key still holding a value Lisa itself wrote
24
- * - defaults: Project's values preserved; Lisa's used only if missing
25
- * - merge: Arrays concatenated and deduplicated
26
- * - remove: Retired keys deleted from their section
27
- *
28
- * A governed script CI invokes is shipped as a pair — `lint:lisa` forced, and
29
- * `lint` merely defaulted to invoke it — so the host owns the composition point
30
- * and anything chained onto it survives an apply. See the reserved-base section
31
- * of `wiki/documentation/specs/package-lisa-json.md`.
32
- *
33
- * Inheritance chain: all → typescript → specific types (expo, nestjs, cdk, npm-package)
34
- * Child types override parent values in each section.
35
- * @module strategies
36
- */
37
14
  /**
38
15
  * Package.lisa.json strategy: Manage package.json via separate template files
39
16
  * - Loads templates from all applicable types in inheritance chain
@@ -78,6 +55,66 @@ export class PackageLisaStrategy {
78
55
  assertManifestIsInstallable(result.packageJson, this.PACKAGE_JSON);
79
56
  return result.packageJson;
80
57
  }
58
+ /**
59
+ * Report every `$name` override that would resolve BELOW a Lisa floor, before
60
+ * an apply refuses over it.
61
+ * @remarks
62
+ * Deliberately re-runs the apply's own resolution — the same type detection,
63
+ * the same template inheritance chain, the same force/preserve/adopt phases —
64
+ * rather than re-deriving the answer from `package.lisa.json` and the host
65
+ * manifest side by side. A second implementation of "what would the merged
66
+ * manifest look like" is a second thing to keep in step with this file, and
67
+ * the whole value of the report is that it agrees with the refusal.
68
+ *
69
+ * BOTH apply paths are simulated. The postinstall path applies a template
70
+ * restricted to security pins and pulls in the direct dependencies backing
71
+ * them, so it can accept a manifest the unrestricted path refuses and vice
72
+ * versa. Reporting only one leaves an operator refused by the other with a
73
+ * doctor line that said nothing.
74
+ * @param projectDir - Host project directory containing package.json
75
+ * @param lisaDir - Installed Lisa package root
76
+ * @returns What was inspected on each path, and every conflict found
77
+ */
78
+ async auditOverrideFloors(projectDir, lisaDir) {
79
+ const projectJson = (await readJsonOrNull(path.join(projectDir, this.PACKAGE_JSON))) ?? {};
80
+ const detectedTypes = await this.detectProjectTypes(projectDir);
81
+ const merged = await this.loadAndMergeTemplates(lisaDir, detectedTypes);
82
+ return {
83
+ lisaFloors: countTemplateFloors(merged),
84
+ detectedTypes,
85
+ paths: [
86
+ this.auditOneApplyPath(projectJson, merged, "postinstall"),
87
+ this.auditOneApplyPath(projectJson, merged, "full apply"),
88
+ ],
89
+ };
90
+ }
91
+ /**
92
+ * Simulate one apply path and audit the manifest it would produce.
93
+ * @param projectJson - Parsed host package.json
94
+ * @param merged - Fully resolved template for the detected types
95
+ * @param applyPath - Which path to simulate
96
+ * @returns The path's audit, or the reason it could not be produced
97
+ * @private
98
+ */
99
+ auditOneApplyPath(projectJson, merged, applyPath) {
100
+ const restricted = applyPath === "postinstall" || projectJson.name === LISA_PACKAGE_NAME;
101
+ const effective = restricted ? this.restrictToSecurityPins(merged) : merged;
102
+ try {
103
+ const forced = this.applyTemplate(projectJson, effective, this.PACKAGE_JSON, restricted);
104
+ return {
105
+ applyPath,
106
+ audit: auditSelfReferenceRewrites(forced.packageJson),
107
+ error: null,
108
+ };
109
+ }
110
+ catch (error) {
111
+ return {
112
+ applyPath,
113
+ audit: null,
114
+ error: error instanceof Error ? error.message : String(error),
115
+ };
116
+ }
117
+ }
81
118
  /**
82
119
  * Apply package-lisa strategy: Load templates from inheritance chain, apply to package.json
83
120
  * @remarks
@@ -928,15 +965,6 @@ function describeUnrunGates(after, template) {
928
965
  ];
929
966
  });
930
967
  }
931
- /** package.json sections whose keys are treated as direct dependencies. */
932
- const DIRECT_DEPENDENCY_SECTIONS = [
933
- "dependencies",
934
- "devDependencies",
935
- "optionalDependencies",
936
- "peerDependencies",
937
- ];
938
- /** package.json sections that carry npm-style version overrides. */
939
- const OVERRIDE_SECTIONS = ["overrides", "resolutions"];
940
968
  /**
941
969
  * Every section whose forced values are a version FLOOR rather than an
942
970
  * assignment, and which therefore may never be merged downwards.
@@ -1206,85 +1234,55 @@ function rangeFloor(range) {
1206
1234
  * @returns Planned package.json plus operator-visible notes.
1207
1235
  */
1208
1236
  function planSelfReferencingOverrideNormalization(pkg, fileName) {
1209
- const directDeps = collectDirectDependencyNames(pkg);
1210
- if (directDeps.size === 0) {
1237
+ const audit = auditSelfReferenceRewrites(pkg);
1238
+ const refused = audit.conflicts[0];
1239
+ if (refused !== undefined) {
1240
+ throw new JsonMergeError(fileName, formatSelfReferenceRefusal(refused));
1241
+ }
1242
+ if (audit.rewritable.length === 0) {
1211
1243
  return { packageJson: pkg, notes: [] };
1212
1244
  }
1213
- const directRanges = collectDirectDependencyRanges(pkg);
1214
- const rewritePlan = OVERRIDE_SECTIONS.reduce((acc, section) => {
1215
- const entries = pkg[section];
1216
- if (entries === null ||
1217
- typeof entries !== "object" ||
1218
- Array.isArray(entries)) {
1219
- return acc;
1220
- }
1221
- const pairs = Object.entries(entries);
1222
- const sectionRewrites = pairs.flatMap(([name, value]) => {
1223
- if (!needsSelfReferenceRewrite(directDeps, name, value)) {
1224
- return [];
1225
- }
1226
- const directRange = directRanges.get(name);
1227
- if (directRange === undefined) {
1228
- return [];
1229
- }
1230
- const literalRange = value;
1231
- assertSelfReferenceRewriteDoesNotWiden(fileName, section, name, literalRange, directRange);
1232
- return [{ section, name, literalRange, directRange }];
1233
- });
1234
- if (sectionRewrites.length === 0) {
1235
- return acc;
1245
+ const sections = OVERRIDE_SECTIONS.flatMap(section => {
1246
+ const rewrites = audit.rewritable.filter(rewrite => rewrite.section === section);
1247
+ if (rewrites.length === 0) {
1248
+ return [];
1236
1249
  }
1237
- const normalized = Object.fromEntries(pairs.map(([name, value]) => sectionRewrites.some(rewrite => rewrite.name === name)
1250
+ const normalized = Object.fromEntries(Object.entries(asRecord(pkg[section])).map(([name, value]) => rewrites.some(rewrite => rewrite.name === name)
1238
1251
  ? [name, `$${name}`]
1239
1252
  : [name, value]));
1240
- return {
1241
- sections: { ...acc.sections, [section]: normalized },
1242
- rewrites: [...acc.rewrites, ...sectionRewrites],
1243
- };
1244
- }, { sections: {}, rewrites: [] });
1245
- const packageJson = Object.keys(rewritePlan.sections).length > 0
1246
- ? { ...pkg, ...rewritePlan.sections }
1247
- : pkg;
1253
+ return [[section, normalized]];
1254
+ });
1248
1255
  return {
1249
- packageJson,
1250
- notes: rewritePlan.rewrites.map(formatSelfReferenceRewriteNote),
1256
+ packageJson: { ...pkg, ...Object.fromEntries(sections) },
1257
+ notes: audit.rewritable.map(formatSelfReferenceRewriteNote),
1251
1258
  };
1252
1259
  }
1253
1260
  /**
1254
- * Decide whether an override/resolution entry needs npm `$name` normalization.
1255
- * @param directDeps - Direct dependency names in the merged package
1256
- * @param name - Override/resolution key
1257
- * @param value - Override/resolution value
1258
- * @returns True when the entry is a literal direct-dependency collision
1259
- */
1260
- function needsSelfReferenceRewrite(directDeps, name, value) {
1261
- return (directDeps.has(name) &&
1262
- typeof value === "string" &&
1263
- value.length > 0 &&
1264
- !value.startsWith("$"));
1265
- }
1266
- /**
1267
- * Refuse a `$name` rewrite unless the direct dependency range is no wider.
1268
- * @param fileName - Basename used in error messages
1269
- * @param section - Override section carrying the entry
1270
- * @param name - Package name being rewritten
1271
- * @param literalRange - Literal override/resolution range to replace
1272
- * @param directRange - Direct dependency range `$name` would resolve to
1273
- * @throws JsonMergeError when the rewrite is unprovable or widening
1261
+ * Explain a refused `$name` rewrite, ending with a remedy Lisa has TESTED.
1262
+ * @remarks
1263
+ * The old message ended "Pin the direct dependency to <the override> or choose
1264
+ * a direct range that is no wider" — advice naming no concrete range which, for
1265
+ * a bounded floor, is not satisfiable by the obvious reading of it. An operator
1266
+ * who follows a refusal's own instructions and is refused a second time
1267
+ * concludes the guard is broken rather than the range wrong
1268
+ * (CodySwannGT/lisa#3191), which is how a correct security control gets routed
1269
+ * around. The replacement names ONE range and has run it back through the same
1270
+ * predicate that produced the refusal.
1271
+ * @param conflict - The conflict that stopped the apply
1272
+ * @returns Human-readable refusal naming both ranges and the verified raise
1274
1273
  */
1275
- function assertSelfReferenceRewriteDoesNotWiden(fileName, section, name, literalRange, directRange) {
1276
- if (!semver.validRange(literalRange) || !semver.validRange(directRange)) {
1277
- throw new JsonMergeError(fileName, `${section}.${name} cannot be rewritten to "$${name}" because Lisa ` +
1278
- `cannot prove the direct dependency range ${JSON.stringify(directRange)} ` +
1279
- `is no wider than the literal override ${JSON.stringify(literalRange)}.`);
1280
- }
1281
- if (!semver.subset(directRange, literalRange)) {
1282
- throw new JsonMergeError(fileName, `${section}.${name} would widen if rewritten to "$${name}": the literal ` +
1283
- `override ${JSON.stringify(literalRange)} would resolve to direct ` +
1284
- `dependency range ${JSON.stringify(directRange)}. Pin the direct ` +
1285
- `dependency to ${JSON.stringify(literalRange)} or choose a direct range ` +
1286
- `that is no wider than the override.`);
1274
+ function formatSelfReferenceRefusal(conflict) {
1275
+ const remedy = describeSelfReferenceRemedy(conflict);
1276
+ if (conflict.verdict === "unprovable") {
1277
+ return (`${conflict.section}.${conflict.name} cannot be rewritten to ` +
1278
+ `"$${conflict.name}" because Lisa cannot prove the direct dependency ` +
1279
+ `range ${JSON.stringify(conflict.directRange)} is no wider than the ` +
1280
+ `literal override ${JSON.stringify(conflict.floorRange)}. ${remedy}`);
1287
1281
  }
1282
+ return (`${conflict.section}.${conflict.name} would widen if rewritten to ` +
1283
+ `"$${conflict.name}": the literal override ` +
1284
+ `${JSON.stringify(conflict.floorRange)} would resolve to direct ` +
1285
+ `dependency range ${JSON.stringify(conflict.directRange)}. ${remedy}`);
1288
1286
  }
1289
1287
  /**
1290
1288
  * Format the operator-visible explanation for a safe `$name` rewrite.
@@ -1293,21 +1291,10 @@ function assertSelfReferenceRewriteDoesNotWiden(fileName, section, name, literal
1293
1291
  */
1294
1292
  function formatSelfReferenceRewriteNote(rewrite) {
1295
1293
  return (`Normalized ${rewrite.section}.${rewrite.name}: replaced literal ` +
1296
- `${JSON.stringify(rewrite.literalRange)} with "$${rewrite.name}" ` +
1294
+ `${JSON.stringify(rewrite.floorRange)} with "$${rewrite.name}" ` +
1297
1295
  `resolving to direct dependency range ` +
1298
1296
  `${JSON.stringify(rewrite.directRange)}.`);
1299
1297
  }
1300
- /**
1301
- * Narrow an unknown value to a plain object record, returning {} otherwise.
1302
- * @param value - Candidate value
1303
- * @returns The value as a record, or an empty record when it is not a plain object
1304
- */
1305
- function asRecord(value) {
1306
- if (value !== null && typeof value === "object" && !Array.isArray(value)) {
1307
- return value;
1308
- }
1309
- return {};
1310
- }
1311
1298
  /**
1312
1299
  * Recursively collect every `$name` reference found in a single JSON value. npm
1313
1300
  * overrides/resolutions use a `"$name"` string value to mean "resolve to the
@@ -1351,23 +1338,6 @@ function collectLiteralOverrideNames(sections) {
1351
1338
  : []);
1352
1339
  }));
1353
1340
  }
1354
- /**
1355
- * Collect the names of every direct dependency declared across a package.json's
1356
- * dependency sections.
1357
- * @param pkg - Merged package.json object
1358
- * @returns Set of direct dependency names
1359
- */
1360
- function collectDirectDependencyNames(pkg) {
1361
- return new Set(DIRECT_DEPENDENCY_SECTIONS.flatMap(section => Object.keys(asRecord(pkg[section]))));
1362
- }
1363
- /**
1364
- * Collect direct dependency ranges by package name.
1365
- * @param pkg - Merged package.json object
1366
- * @returns Package name to string range map
1367
- */
1368
- function collectDirectDependencyRanges(pkg) {
1369
- return new Map(DIRECT_DEPENDENCY_SECTIONS.flatMap(section => Object.entries(asRecord(pkg[section])).flatMap(([name, value]) => typeof value === "string" ? [[name, value]] : [])));
1370
- }
1371
1341
  /**
1372
1342
  * Fail the apply when the merged package.json carries a `$name` self-reference
1373
1343
  * in overrides/resolutions without the backing direct dependency. Writing such
@@ -1452,4 +1422,16 @@ function assertManifestIsInstallable(pkg, fileName) {
1452
1422
  assertNoDanglingDollarRefs(pkg, fileName);
1453
1423
  }
1454
1424
  /* eslint-enable max-lines -- Re-enable after comprehensive package merge strategy */
1425
+ /**
1426
+ * Count the literal version floors a resolved template would write.
1427
+ *
1428
+ * A `$name` entry is not counted: it carries no floor of its own, so it can
1429
+ * never be the thing a host range resolves below.
1430
+ * @param template - Fully resolved package.lisa.json template
1431
+ * @returns Number of literal entries across force.overrides and force.resolutions
1432
+ */
1433
+ function countTemplateFloors(template) {
1434
+ return OVERRIDE_SECTIONS.reduce((total, section) => total +
1435
+ Object.values(asRecord(template.force[section])).filter(value => typeof value === "string" && !value.startsWith("$")).length, 0);
1436
+ }
1455
1437
  //# sourceMappingURL=package-lisa.js.map