@hominis/fireforge 0.39.0 → 0.41.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 (279) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +35 -5
  3. package/dist/bin/fireforge.js +25 -4
  4. package/dist/build-info.json +9 -0
  5. package/dist/src/cli.js +27 -3
  6. package/dist/src/commands/bootstrap.js +5 -1
  7. package/dist/src/commands/discard.js +2 -22
  8. package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
  9. package/dist/src/commands/doctor-check-core.d.ts +20 -0
  10. package/dist/src/commands/doctor-check-core.js +28 -0
  11. package/dist/src/commands/doctor-external-toolchains.js +2 -0
  12. package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
  13. package/dist/src/commands/doctor-furnace.js +13 -11
  14. package/dist/src/commands/doctor-orphaned-harness.js +2 -1
  15. package/dist/src/commands/doctor.js +2 -2
  16. package/dist/src/commands/export-all.js +9 -1
  17. package/dist/src/commands/export-flow.js +27 -9
  18. package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
  19. package/dist/src/commands/export-placement-conflicts.js +74 -0
  20. package/dist/src/commands/export-placement-gate.js +1 -1
  21. package/dist/src/commands/export-shared.d.ts +8 -1
  22. package/dist/src/commands/export-shared.js +30 -10
  23. package/dist/src/commands/export.js +2 -2
  24. package/dist/src/commands/furnace/apply.js +4 -3
  25. package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
  26. package/dist/src/commands/furnace/chrome-doc.js +13 -2
  27. package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
  28. package/dist/src/commands/furnace/create-browser-test.js +5 -0
  29. package/dist/src/commands/furnace/create.js +2 -2
  30. package/dist/src/commands/furnace/diff.js +2 -1
  31. package/dist/src/commands/furnace/init.js +4 -6
  32. package/dist/src/commands/furnace/override.js +2 -2
  33. package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
  34. package/dist/src/commands/furnace/remove-mutations.js +171 -0
  35. package/dist/src/commands/furnace/remove.js +21 -128
  36. package/dist/src/commands/furnace/rename-browser-test.js +54 -5
  37. package/dist/src/commands/furnace/scan.js +27 -36
  38. package/dist/src/commands/furnace/status.js +30 -28
  39. package/dist/src/commands/furnace/validate.js +25 -18
  40. package/dist/src/commands/import.js +6 -20
  41. package/dist/src/commands/lint-per-patch.js +73 -51
  42. package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
  43. package/dist/src/commands/lint-per-run-checkjs.js +102 -0
  44. package/dist/src/commands/lint-report.d.ts +21 -0
  45. package/dist/src/commands/lint-report.js +62 -0
  46. package/dist/src/commands/lint.js +26 -22
  47. package/dist/src/commands/manifest.js +3 -1
  48. package/dist/src/commands/package.js +9 -16
  49. package/dist/src/commands/patch/compact.js +5 -5
  50. package/dist/src/commands/patch/delete.js +5 -5
  51. package/dist/src/commands/patch/lint-ignore.js +3 -2
  52. package/dist/src/commands/patch/move-files-create.js +22 -4
  53. package/dist/src/commands/patch/move-files-into.js +14 -10
  54. package/dist/src/commands/patch/move-files.js +6 -5
  55. package/dist/src/commands/patch/rename.d.ts +2 -2
  56. package/dist/src/commands/patch/rename.js +152 -51
  57. package/dist/src/commands/patch/reorder.d.ts +9 -0
  58. package/dist/src/commands/patch/reorder.js +14 -10
  59. package/dist/src/commands/patch/split-plan.d.ts +13 -6
  60. package/dist/src/commands/patch/split-plan.js +22 -12
  61. package/dist/src/commands/patch/split.d.ts +1 -1
  62. package/dist/src/commands/patch/split.js +23 -9
  63. package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
  64. package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
  65. package/dist/src/commands/patch/staged-dependency.js +112 -14
  66. package/dist/src/commands/re-export-adjacent.d.ts +34 -0
  67. package/dist/src/commands/re-export-adjacent.js +105 -0
  68. package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
  69. package/dist/src/commands/re-export-bulk-scan.js +142 -2
  70. package/dist/src/commands/re-export-drift.d.ts +74 -0
  71. package/dist/src/commands/re-export-drift.js +234 -0
  72. package/dist/src/commands/re-export-files.js +5 -3
  73. package/dist/src/commands/re-export-lint.d.ts +97 -0
  74. package/dist/src/commands/re-export-lint.js +210 -0
  75. package/dist/src/commands/re-export-options.js +11 -0
  76. package/dist/src/commands/re-export-register.d.ts +18 -0
  77. package/dist/src/commands/re-export-register.js +61 -0
  78. package/dist/src/commands/re-export-scan.js +13 -7
  79. package/dist/src/commands/re-export-single.d.ts +20 -0
  80. package/dist/src/commands/re-export-single.js +261 -0
  81. package/dist/src/commands/re-export.d.ts +0 -4
  82. package/dist/src/commands/re-export.js +123 -289
  83. package/dist/src/commands/rebase/abort.js +13 -4
  84. package/dist/src/commands/rebase/continue.js +11 -4
  85. package/dist/src/commands/rebase/index.js +19 -29
  86. package/dist/src/commands/rebase/patch-loop.js +12 -0
  87. package/dist/src/commands/reset.js +4 -23
  88. package/dist/src/commands/resolve.js +2 -9
  89. package/dist/src/commands/run.js +29 -23
  90. package/dist/src/commands/source.js +3 -9
  91. package/dist/src/commands/status-check.d.ts +48 -0
  92. package/dist/src/commands/status-check.js +106 -0
  93. package/dist/src/commands/status-json.d.ts +31 -0
  94. package/dist/src/commands/status-json.js +76 -0
  95. package/dist/src/commands/status-output.d.ts +1 -0
  96. package/dist/src/commands/status-output.js +10 -1
  97. package/dist/src/commands/status-ownership.d.ts +52 -0
  98. package/dist/src/commands/status-ownership.js +57 -0
  99. package/dist/src/commands/status.js +190 -170
  100. package/dist/src/commands/test-build-phase.d.ts +19 -0
  101. package/dist/src/commands/test-build-phase.js +168 -0
  102. package/dist/src/commands/test-diagnose.js +30 -2
  103. package/dist/src/commands/test-modes.d.ts +9 -1
  104. package/dist/src/commands/test-modes.js +46 -13
  105. package/dist/src/commands/test-register.js +59 -27
  106. package/dist/src/commands/test-run.d.ts +29 -3
  107. package/dist/src/commands/test-run.js +59 -7
  108. package/dist/src/commands/test-verdict.d.ts +32 -0
  109. package/dist/src/commands/test-verdict.js +48 -0
  110. package/dist/src/commands/test.d.ts +9 -0
  111. package/dist/src/commands/test.js +100 -123
  112. package/dist/src/commands/token-coverage.js +4 -9
  113. package/dist/src/commands/token.js +4 -1
  114. package/dist/src/commands/tree.d.ts +19 -0
  115. package/dist/src/commands/tree.js +320 -0
  116. package/dist/src/commands/verify.js +2 -2
  117. package/dist/src/commands/watch.js +22 -21
  118. package/dist/src/commands/wire.js +1 -1
  119. package/dist/src/core/ast-utils.d.ts +11 -0
  120. package/dist/src/core/ast-utils.js +11 -0
  121. package/dist/src/core/build-audit-platform.js +6 -1
  122. package/dist/src/core/build-audit-registration.js +2 -0
  123. package/dist/src/core/build-audit-resolve.d.ts +13 -0
  124. package/dist/src/core/build-audit-resolve.js +34 -13
  125. package/dist/src/core/build-audit.js +4 -20
  126. package/dist/src/core/build-baseline-types.d.ts +29 -8
  127. package/dist/src/core/build-baseline-types.js +2 -1
  128. package/dist/src/core/build-baseline.d.ts +10 -2
  129. package/dist/src/core/build-baseline.js +39 -8
  130. package/dist/src/core/build-prepare.d.ts +4 -0
  131. package/dist/src/core/build-prepare.js +7 -1
  132. package/dist/src/core/config-paths.d.ts +1 -1
  133. package/dist/src/core/config-paths.js +2 -0
  134. package/dist/src/core/config-validate-test-toolchains.js +3 -2
  135. package/dist/src/core/config-validate.js +45 -30
  136. package/dist/src/core/config.js +16 -3
  137. package/dist/src/core/coverage-extend.d.ts +89 -0
  138. package/dist/src/core/coverage-extend.js +188 -0
  139. package/dist/src/core/destructive.js +3 -1
  140. package/dist/src/core/discard-baseline.js +18 -4
  141. package/dist/src/core/engine-precondition.d.ts +35 -0
  142. package/dist/src/core/engine-precondition.js +50 -0
  143. package/dist/src/core/engine-session-lock.d.ts +38 -1
  144. package/dist/src/core/engine-session-lock.js +78 -4
  145. package/dist/src/core/file-lock.d.ts +7 -0
  146. package/dist/src/core/file-lock.js +20 -30
  147. package/dist/src/core/firefox.js +5 -2
  148. package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
  149. package/dist/src/core/furnace-apply-helpers.js +12 -3
  150. package/dist/src/core/furnace-apply-output.js +9 -0
  151. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
  152. package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
  153. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
  154. package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
  155. package/dist/src/core/furnace-apply.d.ts +16 -4
  156. package/dist/src/core/furnace-apply.js +39 -43
  157. package/dist/src/core/furnace-config.js +2 -0
  158. package/dist/src/core/furnace-css-fragments.js +2 -0
  159. package/dist/src/core/furnace-jsconfig.js +14 -7
  160. package/dist/src/core/furnace-registration-ast.js +8 -8
  161. package/dist/src/core/furnace-registration-remove.js +6 -3
  162. package/dist/src/core/furnace-registration.js +9 -12
  163. package/dist/src/core/furnace-rollback.js +1 -1
  164. package/dist/src/core/furnace-scanner.js +2 -1
  165. package/dist/src/core/furnace-stale-export.js +2 -1
  166. package/dist/src/core/furnace-state-persist.d.ts +4 -6
  167. package/dist/src/core/furnace-validate-compatibility.js +4 -7
  168. package/dist/src/core/furnace-validate-helpers.js +33 -1
  169. package/dist/src/core/furnace-validate-registration.js +16 -0
  170. package/dist/src/core/furnace-validate.js +4 -2
  171. package/dist/src/core/git-diff.js +37 -2
  172. package/dist/src/core/lint-cache.d.ts +15 -5
  173. package/dist/src/core/lint-cache.js +49 -14
  174. package/dist/src/core/mach-build-artifacts.d.ts +63 -1
  175. package/dist/src/core/mach-build-artifacts.js +108 -2
  176. package/dist/src/core/mach-resource-shim.js +1 -0
  177. package/dist/src/core/marionette-port.d.ts +16 -0
  178. package/dist/src/core/marionette-port.js +93 -8
  179. package/dist/src/core/marionette-preflight.js +6 -5
  180. package/dist/src/core/patch-apply.d.ts +20 -13
  181. package/dist/src/core/patch-apply.js +32 -33
  182. package/dist/src/core/patch-export.d.ts +2 -2
  183. package/dist/src/core/patch-export.js +4 -6
  184. package/dist/src/core/patch-lint-checkjs.d.ts +42 -2
  185. package/dist/src/core/patch-lint-checkjs.js +132 -8
  186. package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
  187. package/dist/src/core/patch-lint-creators.d.ts +30 -0
  188. package/dist/src/core/patch-lint-creators.js +45 -0
  189. package/dist/src/core/patch-lint-cross.d.ts +1 -17
  190. package/dist/src/core/patch-lint-cross.js +16 -32
  191. package/dist/src/core/patch-lint-css.js +64 -16
  192. package/dist/src/core/patch-lint-diff.d.ts +8 -0
  193. package/dist/src/core/patch-lint-diff.js +31 -0
  194. package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
  195. package/dist/src/core/patch-lint-jsdoc.js +15 -5
  196. package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
  197. package/dist/src/core/patch-lint-module-registration.js +88 -0
  198. package/dist/src/core/patch-lint-mozbuild.js +99 -11
  199. package/dist/src/core/patch-lint-ownership.d.ts +21 -0
  200. package/dist/src/core/patch-lint-ownership.js +30 -0
  201. package/dist/src/core/patch-lint-reexports.d.ts +2 -1
  202. package/dist/src/core/patch-lint-reexports.js +1 -0
  203. package/dist/src/core/patch-lint.d.ts +31 -0
  204. package/dist/src/core/patch-lint.js +114 -8
  205. package/dist/src/core/patch-lock.d.ts +12 -1
  206. package/dist/src/core/patch-lock.js +40 -4
  207. package/dist/src/core/patch-manifest-consistency.js +15 -3
  208. package/dist/src/core/patch-manifest.d.ts +1 -1
  209. package/dist/src/core/patch-parse.d.ts +8 -0
  210. package/dist/src/core/patch-parse.js +10 -0
  211. package/dist/src/core/patch-policy.d.ts +7 -0
  212. package/dist/src/core/patch-policy.js +4 -1
  213. package/dist/src/core/patch-transform.d.ts +10 -0
  214. package/dist/src/core/patch-transform.js +13 -2
  215. package/dist/src/core/rebase-session.d.ts +44 -2
  216. package/dist/src/core/rebase-session.js +114 -17
  217. package/dist/src/core/register-browser-content.js +2 -1
  218. package/dist/src/core/register-shared-css.js +2 -1
  219. package/dist/src/core/smoke-patterns.d.ts +24 -16
  220. package/dist/src/core/smoke-patterns.js +26 -14
  221. package/dist/src/core/status-binary.d.ts +55 -0
  222. package/dist/src/core/status-binary.js +124 -0
  223. package/dist/src/core/status-classify.d.ts +22 -1
  224. package/dist/src/core/status-classify.js +112 -70
  225. package/dist/src/core/test-harness-crash.d.ts +32 -1
  226. package/dist/src/core/test-harness-crash.js +59 -4
  227. package/dist/src/core/test-path-scope.js +2 -0
  228. package/dist/src/core/test-stale-check.js +1 -20
  229. package/dist/src/core/token-category.d.ts +40 -0
  230. package/dist/src/core/token-category.js +264 -0
  231. package/dist/src/core/token-manager.d.ts +7 -0
  232. package/dist/src/core/token-manager.js +48 -196
  233. package/dist/src/core/toolchain-preflight.js +24 -26
  234. package/dist/src/core/tree-cow.d.ts +18 -0
  235. package/dist/src/core/tree-cow.js +71 -0
  236. package/dist/src/core/tree-guard.d.ts +35 -0
  237. package/dist/src/core/tree-guard.js +179 -0
  238. package/dist/src/core/tree-store.d.ts +129 -0
  239. package/dist/src/core/tree-store.js +452 -0
  240. package/dist/src/core/typecheck-shim.d.ts +14 -0
  241. package/dist/src/core/typecheck-shim.js +41 -2
  242. package/dist/src/core/typecheck.js +2 -1
  243. package/dist/src/core/wire-destroy.js +14 -3
  244. package/dist/src/core/wire-init.js +37 -40
  245. package/dist/src/core/wire-subscript.js +11 -3
  246. package/dist/src/core/wire-utils.js +4 -4
  247. package/dist/src/core/xpcshell-appdir.js +3 -0
  248. package/dist/src/core/xpcshell-profile-dir.d.ts +12 -0
  249. package/dist/src/core/xpcshell-profile-dir.js +45 -0
  250. package/dist/src/errors/base.d.ts +13 -0
  251. package/dist/src/errors/base.js +13 -0
  252. package/dist/src/errors/git.d.ts +8 -0
  253. package/dist/src/errors/git.js +17 -0
  254. package/dist/src/errors/rebase.d.ts +13 -0
  255. package/dist/src/errors/rebase.js +17 -0
  256. package/dist/src/index.d.ts +2 -0
  257. package/dist/src/index.js +1 -0
  258. package/dist/src/types/commands/options.d.ts +119 -3
  259. package/dist/src/types/commands/patches.d.ts +8 -0
  260. package/dist/src/types/config.d.ts +19 -0
  261. package/dist/src/types/furnace.d.ts +7 -0
  262. package/dist/src/utils/build-info.d.ts +31 -0
  263. package/dist/src/utils/build-info.js +98 -0
  264. package/dist/src/utils/concurrency.d.ts +8 -0
  265. package/dist/src/utils/concurrency.js +21 -0
  266. package/dist/src/utils/errors.d.ts +39 -0
  267. package/dist/src/utils/errors.js +55 -0
  268. package/dist/src/utils/fs.js +5 -12
  269. package/dist/src/utils/logger.d.ts +14 -2
  270. package/dist/src/utils/logger.js +43 -13
  271. package/dist/src/utils/options.d.ts +21 -4
  272. package/dist/src/utils/options.js +26 -6
  273. package/dist/src/utils/package-root.js +7 -4
  274. package/dist/src/utils/process-group.js +13 -2
  275. package/dist/src/utils/stdio-drain.d.ts +28 -0
  276. package/dist/src/utils/stdio-drain.js +54 -0
  277. package/dist/src/utils/validation.d.ts +33 -2
  278. package/dist/src/utils/validation.js +57 -2
  279. package/package.json +13 -13
@@ -1,5 +1,16 @@
1
1
  import type { CustomComponentConfig, DryRunAction, OverrideComponentConfig, StepError } from '../types/furnace.js';
2
2
  import { type RollbackJournal } from './furnace-rollback.js';
3
+ interface DirectoryEntry {
4
+ isFile(): boolean;
5
+ isSymbolicLink?(): boolean;
6
+ name: string;
7
+ }
8
+ /**
9
+ * True for a plain-file directory entry — symlinks and directories are
10
+ * never copy candidates. Exported for the patch-owned overwrite probe
11
+ * (FORGE J6), which walks the same override copy-candidate set as apply.
12
+ */
13
+ export declare function isRegularFile(entry: DirectoryEntry): boolean;
3
14
  /**
4
15
  * Filter deciding which files in an override workspace directory are candidates
5
16
  * for copying into the engine. Exported so `furnace remove` can invert apply
@@ -62,7 +73,7 @@ export declare function undeployOverrideFiles(engineDir: string, config: Overrid
62
73
  removed: string[];
63
74
  }>;
64
75
  /** Compares current component file checksums against the previously recorded state. */
65
- export declare function hasComponentChanged(componentDir: string, previousChecksums: Record<string, string>): Promise<boolean>;
76
+ export declare function hasComponentChanged(componentDir: string, previousChecksums: Record<string, string>, currentChecksums?: Record<string, string>): Promise<boolean>;
66
77
  /**
67
78
  * Detects whether an override component's deployed files are missing from the
68
79
  * engine or differ from the source. Used as a guard before skipping apply on a
@@ -15,7 +15,12 @@ import { recordCreatedDir, snapshotFile } from './furnace-rollback.js';
15
15
  import { checkRegistrationConsistency } from './furnace-validate-registration.js';
16
16
  import { isGitRepository } from './git.js';
17
17
  import { fileExistsInHead, restoreTrackedPath } from './git-file-ops.js';
18
- function isRegularFile(entry) {
18
+ /**
19
+ * True for a plain-file directory entry — symlinks and directories are
20
+ * never copy candidates. Exported for the patch-owned overwrite probe
21
+ * (FORGE J6), which walks the same override copy-candidate set as apply.
22
+ */
23
+ export function isRegularFile(entry) {
19
24
  if (!entry.isFile())
20
25
  return false;
21
26
  if (typeof entry.isSymbolicLink === 'function' && entry.isSymbolicLink())
@@ -177,8 +182,12 @@ export async function undeployOverrideFiles(engineDir, config, deletedFiles, ftl
177
182
  return { restored, removed };
178
183
  }
179
184
  /** Compares current component file checksums against the previously recorded state. */
180
- export async function hasComponentChanged(componentDir, previousChecksums) {
181
- const current = await computeComponentChecksums(componentDir);
185
+ export async function hasComponentChanged(componentDir, previousChecksums, currentChecksums) {
186
+ const current = currentChecksums ?? (await computeComponentChecksums(componentDir));
187
+ return componentChecksumsChanged(current, previousChecksums);
188
+ }
189
+ /** Pure checksum-map comparison used when the caller already walked the component. */
190
+ function componentChecksumsChanged(current, previousChecksums) {
182
191
  const currentKeys = Object.keys(current);
183
192
  const previousKeys = Object.keys(previousChecksums);
184
193
  if (currentKeys.length !== previousKeys.length) {
@@ -51,6 +51,15 @@ export function logApplyResult(result, isDryRun) {
51
51
  }
52
52
  }
53
53
  }
54
+ // Patch-owned overwrite warnings (FORGE J6) print on every non-dry-run
55
+ // outcome — including the rolled-back branch, where the overwrite
56
+ // happened before the rollback restored it and the operator still needs
57
+ // to know the deployed copy was momentarily replaced.
58
+ if (!isDryRun && result.warnings !== undefined) {
59
+ for (const line of result.warnings) {
60
+ warn(line);
61
+ }
62
+ }
54
63
  for (const err of result.errors) {
55
64
  error(`${err.name} — ${err.error}`);
56
65
  }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Patch-owned overwrite warnings for `furnace apply` (FORGE J6).
3
+ *
4
+ * For Furnace-managed components, apply OVERWRITES the deployed engine
5
+ * copies with the `components/` sources. A fix made directly in `engine/`
6
+ * and exported into a patch — but never back-ported to the component
7
+ * source — is silently undone by the next apply, and the only prior
8
+ * signal was a drift line that read like "someone's uncommitted work".
9
+ * This module detects that exact case BEFORE the copy: the deployed
10
+ * bytes differ from the component source AND the file is patch-owned,
11
+ * and produces a loud per-file warning naming the file, the owning
12
+ * patch, and the consequence.
13
+ *
14
+ * The per-file byte comparison already exists —
15
+ * `checkRegistrationConsistency` for custom components (fragment- and
16
+ * ftl-aware) and the copy-candidate walk for overrides — this module
17
+ * only surfaces what was previously computed and thrown away.
18
+ */
19
+ import type { CustomComponentConfig, OverrideComponentConfig } from '../types/furnace.js';
20
+ /** One deployed, patch-owned file about to be overwritten with different bytes. */
21
+ export interface PatchOwnedOverwriteWarning {
22
+ component: string;
23
+ /** Engine-relative path of the deployed file being replaced. */
24
+ file: string;
25
+ /** Patch filenames claiming the path in patches.json. */
26
+ owners: string[];
27
+ }
28
+ /**
29
+ * Loads the file → owning-patch map for the overwrite warning (FORGE J6).
30
+ * A missing or unreadable patches manifest degrades to an empty map — the
31
+ * warning is advisory and must never block an apply.
32
+ */
33
+ export declare function loadPatchClaimsForApply(root: string): Promise<ReadonlyMap<string, readonly string[]>>;
34
+ /** Appends formatted overwrite warnings onto the run's result. */
35
+ export declare function recordOverwriteWarnings(result: {
36
+ warnings?: string[];
37
+ }, warnings: readonly PatchOwnedOverwriteWarning[]): void;
38
+ /** Renders the operator-facing warning line for one overwrite. */
39
+ export declare function formatPatchOwnedOverwriteWarning(warning: PatchOwnedOverwriteWarning): string;
40
+ /** Inputs for {@link findPatchOwnedOverwrites}, discriminated by component type. */
41
+ export type PatchOwnedOverwriteProbe = {
42
+ type: 'custom';
43
+ root: string;
44
+ name: string;
45
+ config: CustomComponentConfig;
46
+ ftlDir: string;
47
+ patchClaims: ReadonlyMap<string, readonly string[]>;
48
+ } | {
49
+ type: 'override';
50
+ engineDir: string;
51
+ name: string;
52
+ componentDir: string;
53
+ config: OverrideComponentConfig;
54
+ ftlDir: string;
55
+ patchClaims: ReadonlyMap<string, readonly string[]>;
56
+ };
57
+ /**
58
+ * Finds deployed files this component's apply is about to overwrite whose
59
+ * engine bytes differ from the component source AND that are patch-owned.
60
+ * Probe failures degrade to a verbose line and an empty list — the apply
61
+ * itself must not fail because the warning probe could not run.
62
+ */
63
+ export declare function findPatchOwnedOverwrites(args: PatchOwnedOverwriteProbe): Promise<PatchOwnedOverwriteWarning[]>;
@@ -0,0 +1,123 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Patch-owned overwrite warnings for `furnace apply` (FORGE J6).
4
+ *
5
+ * For Furnace-managed components, apply OVERWRITES the deployed engine
6
+ * copies with the `components/` sources. A fix made directly in `engine/`
7
+ * and exported into a patch — but never back-ported to the component
8
+ * source — is silently undone by the next apply, and the only prior
9
+ * signal was a drift line that read like "someone's uncommitted work".
10
+ * This module detects that exact case BEFORE the copy: the deployed
11
+ * bytes differ from the component source AND the file is patch-owned,
12
+ * and produces a loud per-file warning naming the file, the owning
13
+ * patch, and the consequence.
14
+ *
15
+ * The per-file byte comparison already exists —
16
+ * `checkRegistrationConsistency` for custom components (fragment- and
17
+ * ftl-aware) and the copy-candidate walk for overrides — this module
18
+ * only surfaces what was previously computed and thrown away.
19
+ */
20
+ import { readdir } from 'node:fs/promises';
21
+ import { join, relative } from 'node:path';
22
+ import { toError } from '../utils/errors.js';
23
+ import { pathExists, readText } from '../utils/fs.js';
24
+ import { verbose } from '../utils/logger.js';
25
+ import { getProjectPaths } from './config.js';
26
+ import { getOverrideEngineTargetPath, isOverrideCopyCandidate, isRegularFile, } from './furnace-apply-helpers.js';
27
+ import { FTL_DIR } from './furnace-constants.js';
28
+ import { checkRegistrationConsistency } from './furnace-validate-registration.js';
29
+ import { loadPatchesManifest } from './patch-manifest.js';
30
+ import { buildPatchClaims } from './status-classify.js';
31
+ /**
32
+ * Loads the file → owning-patch map for the overwrite warning (FORGE J6).
33
+ * A missing or unreadable patches manifest degrades to an empty map — the
34
+ * warning is advisory and must never block an apply.
35
+ */
36
+ export async function loadPatchClaimsForApply(root) {
37
+ try {
38
+ const manifest = await loadPatchesManifest(getProjectPaths(root).patches);
39
+ return manifest ? buildPatchClaims(manifest.patches) : new Map();
40
+ }
41
+ catch {
42
+ return new Map();
43
+ }
44
+ }
45
+ /** Appends formatted overwrite warnings onto the run's result. */
46
+ export function recordOverwriteWarnings(result, warnings) {
47
+ if (warnings.length === 0)
48
+ return;
49
+ result.warnings ??= [];
50
+ result.warnings.push(...warnings.map(formatPatchOwnedOverwriteWarning));
51
+ }
52
+ /** Renders the operator-facing warning line for one overwrite. */
53
+ export function formatPatchOwnedOverwriteWarning(warning) {
54
+ const owners = warning.owners.join(', ');
55
+ return (`${warning.component}: overwriting deployed ${warning.file} — its engine content differs ` +
56
+ `from the components/ source, and the path is owned by patch ${owners}. If the engine ` +
57
+ `edit was an intentional shipped fix, port it into the component source and re-export ` +
58
+ `the patch — the deployed copy is being replaced now.`);
59
+ }
60
+ /** Custom components: reuse the validate oracle's drifted-file list. */
61
+ async function findCustomOverwrites(args) {
62
+ const { root, name, config, ftlDir, patchClaims } = args;
63
+ const status = await checkRegistrationConsistency(root, name, config, ftlDir);
64
+ const warnings = [];
65
+ for (const entryName of status.driftedFiles) {
66
+ const engineRel = entryName.endsWith('.ftl')
67
+ ? `${ftlDir || FTL_DIR}/${entryName}`
68
+ : `${config.targetPath}/${entryName}`;
69
+ const owners = patchClaims.get(engineRel);
70
+ if (owners !== undefined && owners.length > 0) {
71
+ warnings.push({ component: name, file: engineRel, owners: [...owners] });
72
+ }
73
+ }
74
+ return warnings;
75
+ }
76
+ /** Override components: byte-compare each copy candidate against its engine target. */
77
+ async function findOverrideOverwrites(args) {
78
+ const { engineDir, name, componentDir, config, ftlDir, patchClaims } = args;
79
+ const warnings = [];
80
+ const entries = await readdir(componentDir, { withFileTypes: true, encoding: 'utf8' });
81
+ for (const entry of entries) {
82
+ if (!isRegularFile(entry))
83
+ continue;
84
+ if (!isOverrideCopyCandidate(entry.name, config.type))
85
+ continue;
86
+ const enginePath = getOverrideEngineTargetPath(engineDir, config, entry.name, ftlDir);
87
+ // A missing target is a fresh deploy, not a lost engine edit.
88
+ if (!(await pathExists(enginePath)))
89
+ continue;
90
+ const engineRel = relative(engineDir, enginePath).replace(/\\/g, '/');
91
+ const owners = patchClaims.get(engineRel);
92
+ if (owners === undefined || owners.length === 0)
93
+ continue;
94
+ const [engineContent, sourceContent] = await Promise.all([
95
+ readText(enginePath),
96
+ readText(join(componentDir, entry.name)),
97
+ ]);
98
+ if (engineContent !== sourceContent) {
99
+ warnings.push({ component: name, file: engineRel, owners: [...owners] });
100
+ }
101
+ }
102
+ return warnings;
103
+ }
104
+ /**
105
+ * Finds deployed files this component's apply is about to overwrite whose
106
+ * engine bytes differ from the component source AND that are patch-owned.
107
+ * Probe failures degrade to a verbose line and an empty list — the apply
108
+ * itself must not fail because the warning probe could not run.
109
+ */
110
+ export async function findPatchOwnedOverwrites(args) {
111
+ if (args.patchClaims.size === 0)
112
+ return [];
113
+ try {
114
+ if (args.type === 'custom') {
115
+ return await findCustomOverwrites(args);
116
+ }
117
+ return await findOverrideOverwrites(args);
118
+ }
119
+ catch (error) {
120
+ verbose(`Skipping patch-owned overwrite probe for ${args.name}: ${toError(error).message}`);
121
+ return [];
122
+ }
123
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Dry-run action builders for files deleted from a component workspace
3
+ * since the last apply. Split out of `furnace-apply.ts` (at the per-file
4
+ * line budget after the FORGE J6 overwrite-warning wiring); the real
5
+ * undeploy paths stay in `furnace-apply-helpers.ts`.
6
+ */
7
+ import type { CustomComponentConfig, DryRunAction, OverrideComponentConfig } from '../types/furnace.js';
8
+ /** Plans the engine-restore actions for deleted override workspace files. */
9
+ export declare function buildOverrideUndeployActions(name: string, config: OverrideComponentConfig, engineDir: string, deletedFiles: string[], ftlDir: string): DryRunAction[];
10
+ /** Plans the engine-removal + deregistration actions for deleted custom workspace files. */
11
+ export declare function buildCustomUndeployActions(name: string, config: CustomComponentConfig, engineDir: string, deletedFiles: string[], ftlDir: string): DryRunAction[];
@@ -0,0 +1,49 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Dry-run action builders for files deleted from a component workspace
4
+ * since the last apply. Split out of `furnace-apply.ts` (at the per-file
5
+ * line budget after the FORGE J6 overwrite-warning wiring); the real
6
+ * undeploy paths stay in `furnace-apply-helpers.ts`.
7
+ */
8
+ import { join } from 'node:path';
9
+ import { getOverrideEngineTargetPath } from './furnace-apply-helpers.js';
10
+ /** Plans the engine-restore actions for deleted override workspace files. */
11
+ export function buildOverrideUndeployActions(name, config, engineDir, deletedFiles, ftlDir) {
12
+ return deletedFiles.map((fileName) => ({
13
+ component: name,
14
+ action: 'undeploy-restore',
15
+ target: getOverrideEngineTargetPath(engineDir, config, fileName, ftlDir),
16
+ description: `Restore engine/${fileName.endsWith('.ftl') ? `${ftlDir}/${fileName}` : `${config.basePath}/${fileName}`} to Firefox baseline`,
17
+ }));
18
+ }
19
+ /** Plans the engine-removal + deregistration actions for deleted custom workspace files. */
20
+ export function buildCustomUndeployActions(name, config, engineDir, deletedFiles, ftlDir) {
21
+ const actions = [];
22
+ for (const fileName of deletedFiles) {
23
+ const enginePath = fileName.endsWith('.ftl')
24
+ ? join(engineDir, ftlDir, fileName)
25
+ : join(engineDir, config.targetPath, fileName);
26
+ actions.push({
27
+ component: name,
28
+ action: 'undeploy-remove',
29
+ target: enginePath,
30
+ description: `Remove orphaned ${fileName} from engine`,
31
+ });
32
+ }
33
+ // jar.mn re-sync planned for any custom-file deletion when registered.
34
+ if (config.register && deletedFiles.some((f) => f.endsWith('.mjs') || f.endsWith('.css'))) {
35
+ actions.push({
36
+ component: name,
37
+ action: 'unregister-jar',
38
+ description: `Re-sync ${name} jar.mn entries to drop deleted files`,
39
+ });
40
+ }
41
+ if (config.register && deletedFiles.some((f) => f === `${name}.mjs`)) {
42
+ actions.push({
43
+ component: name,
44
+ action: 'unregister-ce',
45
+ description: `Deregister ${name} from customElements.js (.mjs deleted)`,
46
+ });
47
+ }
48
+ return actions;
49
+ }
@@ -2,6 +2,21 @@ import type { ApplyResult, DryRunAction } from '../types/furnace.js';
2
2
  import { type FurnaceOperationContext } from './furnace-operation.js';
3
3
  import { type RollbackJournal } from './furnace-rollback.js';
4
4
  export { computeComponentChecksums, extractComponentChecksums, hasComponentChanged, hasCustomEngineDrift, hasOverrideEngineDrift, prefixChecksums, } from './furnace-apply-helpers.js';
5
+ /**
6
+ * Result of {@link applyAllComponents}.
7
+ *
8
+ * Named because the anonymous intersection this replaced forced eight
9
+ * `Awaited<ReturnType<typeof applyAllComponents>>` indirections across
10
+ * `furnace/deploy.ts`, `furnace/preview.ts`, `furnace-state-persist.ts` and
11
+ * `build-prepare.ts` — none of which could be read without opening this file.
12
+ *
13
+ * `actions` is populated only on a dry run; `rollbackJournal` only when
14
+ * `persistState: false` hands journal ownership back to the caller.
15
+ */
16
+ export type ApplyAllComponentsResult = ApplyResult & {
17
+ actions?: DryRunAction[];
18
+ rollbackJournal?: RollbackJournal;
19
+ };
5
20
  /**
6
21
  * Applies all override and custom components to the engine source tree.
7
22
  *
@@ -28,10 +43,7 @@ export declare function applyAllComponents(root: string, dryRun?: boolean, optio
28
43
  persistState?: boolean;
29
44
  operationContext?: FurnaceOperationContext;
30
45
  componentName?: string;
31
- }): Promise<ApplyResult & {
32
- actions?: DryRunAction[];
33
- rollbackJournal?: RollbackJournal;
34
- }>; /**
46
+ }): Promise<ApplyAllComponentsResult>; /**
35
47
  * Shared context threaded through the batch apply loops. Bundles the state
36
48
  * both loops mutate (result/actions/checksums) with the immutable run
37
49
  * parameters — the previous 12–13 positional parameters let call sites
@@ -5,7 +5,9 @@ import { toError } from '../utils/errors.js';
5
5
  import { pathExists } from '../utils/fs.js';
6
6
  import { info } from '../utils/logger.js';
7
7
  import { getProjectPaths, loadConfig } from './config.js';
8
- import { applyCustomComponent, applyOverrideComponent, computeComponentChecksums, diffDeletedFiles, extractComponentChecksums, getOverrideEngineTargetPath, hasComponentChanged, hasCustomEngineDrift, hasOverrideEngineDrift, prefixChecksums, undeployCustomFiles, undeployOverrideFiles, } from './furnace-apply-helpers.js';
8
+ import { applyCustomComponent, applyOverrideComponent, computeComponentChecksums, diffDeletedFiles, extractComponentChecksums, hasComponentChanged, hasCustomEngineDrift, hasOverrideEngineDrift, prefixChecksums, undeployCustomFiles, undeployOverrideFiles, } from './furnace-apply-helpers.js';
9
+ import { findPatchOwnedOverwrites, loadPatchClaimsForApply, recordOverwriteWarnings, } from './furnace-apply-overwrite-warn.js';
10
+ import { buildCustomUndeployActions, buildOverrideUndeployActions, } from './furnace-apply-undeploy-actions.js';
9
11
  import { getFurnacePaths, loadFurnaceConfig, loadFurnaceState, updateFurnaceState, } from './furnace-config.js';
10
12
  import { CUSTOM_ELEMENTS_JS, JAR_MN, resolveFtlDir } from './furnace-constants.js';
11
13
  import { topologicalSortCustom } from './furnace-graph-utils.js';
@@ -22,14 +24,6 @@ function addMissingComponentError(result, name, directoryPath) {
22
24
  error: `Component directory not found: ${directoryPath}`,
23
25
  });
24
26
  }
25
- function buildOverrideUndeployActions(name, config, engineDir, deletedFiles, ftlDir) {
26
- return deletedFiles.map((fileName) => ({
27
- component: name,
28
- action: 'undeploy-restore',
29
- target: getOverrideEngineTargetPath(engineDir, config, fileName, ftlDir),
30
- description: `Restore engine/${fileName.endsWith('.ftl') ? `${ftlDir}/${fileName}` : `${config.basePath}/${fileName}`} to Firefox baseline`,
31
- }));
32
- }
33
27
  async function applyOverrideBatch(ctx) {
34
28
  const { config, furnacePaths, state, engineDir, ftlDir, dryRun } = ctx;
35
29
  const { result, allActions, newChecksums, rollbackJournal, componentName } = ctx;
@@ -47,8 +41,9 @@ async function applyOverrideBatch(ctx) {
47
41
  continue;
48
42
  }
49
43
  const previous = extractComponentChecksums(state.appliedChecksums, 'override', name);
44
+ const currentChecksums = await computeComponentChecksums(componentDir);
50
45
  if (!dryRun) {
51
- const changed = await hasComponentChanged(componentDir, previous);
46
+ const changed = await hasComponentChanged(componentDir, previous, currentChecksums);
52
47
  if (!changed) {
53
48
  // Fast path holds only if the engine still reflects what we deployed.
54
49
  // reset/download/manual edits can silently erase engine files; the
@@ -67,7 +62,6 @@ async function applyOverrideBatch(ctx) {
67
62
  // Compute which files (if any) were removed from the workspace since
68
63
  // the last apply. We do this for both dry-run and real runs so the
69
64
  // planned-actions output stays honest.
70
- const currentChecksums = await computeComponentChecksums(componentDir);
71
65
  const deletedFiles = diffDeletedFiles(previous, currentChecksums);
72
66
  if (dryRun) {
73
67
  if (deletedFiles.length > 0) {
@@ -78,6 +72,20 @@ async function applyOverrideBatch(ctx) {
78
72
  const { restored, removed } = await undeployOverrideFiles(engineDir, overrideConfig, deletedFiles, ftlDir, rollbackJournal);
79
73
  filesAffectedTotal.push(...restored, ...removed);
80
74
  }
75
+ if (!dryRun) {
76
+ // Runs unconditionally — including when the component source
77
+ // changed — because the lost-work case is precisely a deployed
78
+ // engine edit that the incoming copy is about to replace (J6).
79
+ recordOverwriteWarnings(result, await findPatchOwnedOverwrites({
80
+ type: 'override',
81
+ engineDir,
82
+ name,
83
+ componentDir,
84
+ config: overrideConfig,
85
+ ftlDir,
86
+ patchClaims: ctx.patchClaims,
87
+ }));
88
+ }
81
89
  const { affectedPaths: filesAffected, actions } = await applyOverrideComponent(engineDir, name, componentDir, overrideConfig, ftlDir, dryRun, rollbackJournal);
82
90
  if (dryRun && actions) {
83
91
  allActions.push(...actions);
@@ -96,36 +104,6 @@ async function applyOverrideBatch(ctx) {
96
104
  }
97
105
  }
98
106
  }
99
- function buildCustomUndeployActions(name, config, engineDir, deletedFiles, ftlDir) {
100
- const actions = [];
101
- for (const fileName of deletedFiles) {
102
- const enginePath = fileName.endsWith('.ftl')
103
- ? join(engineDir, ftlDir, fileName)
104
- : join(engineDir, config.targetPath, fileName);
105
- actions.push({
106
- component: name,
107
- action: 'undeploy-remove',
108
- target: enginePath,
109
- description: `Remove orphaned ${fileName} from engine`,
110
- });
111
- }
112
- // jar.mn re-sync planned for any custom-file deletion when registered.
113
- if (config.register && deletedFiles.some((f) => f.endsWith('.mjs') || f.endsWith('.css'))) {
114
- actions.push({
115
- component: name,
116
- action: 'unregister-jar',
117
- description: `Re-sync ${name} jar.mn entries to drop deleted files`,
118
- });
119
- }
120
- if (config.register && deletedFiles.some((f) => f === `${name}.mjs`)) {
121
- actions.push({
122
- component: name,
123
- action: 'unregister-ce',
124
- description: `Deregister ${name} from customElements.js (.mjs deleted)`,
125
- });
126
- }
127
- return actions;
128
- }
129
107
  /**
130
108
  * After undeploying deleted files, the in-engine jar.mn and
131
109
  * customElements.js still carry entries for the removed files. Re-sync them
@@ -222,8 +200,9 @@ async function applyCustomBatch(ctx, root, markerComment) {
222
200
  continue;
223
201
  }
224
202
  const previous = extractComponentChecksums(state.appliedChecksums, 'custom', name);
203
+ const currentChecksums = await computeComponentChecksums(componentDir);
225
204
  if (!dryRun) {
226
- const changed = await hasComponentChanged(componentDir, previous);
205
+ const changed = await hasComponentChanged(componentDir, previous, currentChecksums);
227
206
  if (!changed) {
228
207
  // As with overrides, the checksum record is not sufficient on its
229
208
  // own: a reset/download that cleared the engine must trigger a
@@ -241,7 +220,6 @@ async function applyCustomBatch(ctx, root, markerComment) {
241
220
  // Diff against previous to find files the developer has deleted from
242
221
  // the workspace since last apply. Run for both dry-run and real apply
243
222
  // so plan output and execution stay aligned.
244
- const currentChecksums = await computeComponentChecksums(componentDir);
245
223
  const deletedFiles = diffDeletedFiles(previous, currentChecksums);
246
224
  if (dryRun) {
247
225
  if (deletedFiles.length > 0) {
@@ -252,6 +230,19 @@ async function applyCustomBatch(ctx, root, markerComment) {
252
230
  const removed = await undeployCustomFiles(engineDir, customConfig, deletedFiles, ftlDir, rollbackJournal);
253
231
  filesAffectedTotal.push(...removed);
254
232
  }
233
+ if (!dryRun) {
234
+ // Runs unconditionally — the `changed === true` path used to skip
235
+ // drift detection entirely, which is exactly when a deployed
236
+ // engine-only fix gets silently replaced (J6).
237
+ recordOverwriteWarnings(result, await findPatchOwnedOverwrites({
238
+ type: 'custom',
239
+ root,
240
+ name,
241
+ config: customConfig,
242
+ ftlDir,
243
+ patchClaims: ctx.patchClaims,
244
+ }));
245
+ }
255
246
  const { affectedPaths: filesAffected, stepErrors, actions, } = await applyCustomComponent(engineDir, name, componentDir, customConfig, ftlDir, dryRun, rollbackJournal, markerComment !== undefined ? { markerComment } : {});
256
247
  if (dryRun && actions) {
257
248
  allActions.push(...actions);
@@ -345,6 +336,10 @@ export async function applyAllComponents(root, dryRun = false, options) {
345
336
  throw new FurnaceError(`Component "${componentName}" not found in furnace.json. Run "fireforge furnace list" to see registered components.`, componentName);
346
337
  }
347
338
  }
339
+ // Patch-claim map for the overwrite warning (FORGE J6): loaded once per
340
+ // apply. A missing/unreadable manifest degrades to an empty map — the
341
+ // warning is advisory and must never block the apply.
342
+ const patchClaims = await loadPatchClaimsForApply(root);
348
343
  const batchContext = {
349
344
  config,
350
345
  furnacePaths,
@@ -357,6 +352,7 @@ export async function applyAllComponents(root, dryRun = false, options) {
357
352
  newChecksums,
358
353
  rollbackJournal,
359
354
  componentName,
355
+ patchClaims,
360
356
  };
361
357
  await applyOverrideBatch(batchContext);
362
358
  await applyCustomBatch(batchContext, root, markerComment);
@@ -360,6 +360,8 @@ export async function writeFurnaceConfig(root, config) {
360
360
  existing = raw;
361
361
  }
362
362
  catch {
363
+ // A missing or corrupt furnace.json means there is no prior document to
364
+ // preserve key order from; the writer falls back to canonical ordering.
363
365
  existing = undefined;
364
366
  }
365
367
  }
@@ -194,6 +194,8 @@ export async function validateCssFragments(componentDir, tagName, sharedDir, eng
194
194
  entries = await readdir(componentDir);
195
195
  }
196
196
  catch {
197
+ // An unreadable component directory contributes no fragment issues; return
198
+ // what has been collected so far rather than failing validation.
197
199
  return issues;
198
200
  }
199
201
  for (const fileName of entries) {
@@ -24,6 +24,7 @@ import { readdir } from 'node:fs/promises';
24
24
  import { dirname, join, relative, resolve } from 'node:path';
25
25
  import { applyEdits, modify, parse } from 'jsonc-parser';
26
26
  import { FurnaceError } from '../errors/furnace.js';
27
+ import { toError } from '../utils/errors.js';
27
28
  import { pathExists, readText, writeTextIfChanged } from '../utils/fs.js';
28
29
  import { info } from '../utils/logger.js';
29
30
  import { normalizePathSlashes } from '../utils/paths.js';
@@ -32,8 +33,14 @@ import { getFurnacePaths } from './furnace-config.js';
32
33
  const CHROME_ELEMENTS_URL_PREFIX = 'chrome://global/content/elements/';
33
34
  /**
34
35
  * Computes the desired managed `paths` entries: one per `.mjs` file of
35
- * every registered custom component, keyed by its deployed chrome URL and
36
- * mapped to the workspace source relative to the jsconfig directory.
36
+ * every custom component — registered or not — keyed by its deployed
37
+ * chrome URL and mapped to the workspace source relative to the jsconfig
38
+ * directory. `register: false` components (notably `kind: "library"`
39
+ * base-class modules, which REQUIRE register: false) are included because
40
+ * deploy copies their files and writes jar.mn entries under
41
+ * `content/global/elements/` unconditionally — only the customElements.js
42
+ * registration is gated on `register` — so their chrome URLs are just as
43
+ * real as a registered component's.
37
44
  *
38
45
  * Workspace sources (not deployed engine copies) are the mapping target —
39
46
  * they are the files developers edit, and they exist even when the engine
@@ -42,9 +49,7 @@ const CHROME_ELEMENTS_URL_PREFIX = 'chrome://global/content/elements/';
42
49
  async function computeDesiredChromePathEntries(config, customDir, jsconfigAbsPath) {
43
50
  const jsconfigDir = dirname(jsconfigAbsPath);
44
51
  const entries = {};
45
- for (const [name, customConfig] of Object.entries(config.custom)) {
46
- if (!customConfig.register)
47
- continue;
52
+ for (const name of Object.keys(config.custom)) {
48
53
  const componentDir = join(customDir, name);
49
54
  if (!(await pathExists(componentDir)))
50
55
  continue;
@@ -113,13 +118,15 @@ export async function syncFurnaceJsconfigPaths(root, config, options) {
113
118
  const parseErrors = [];
114
119
  let jsconfig;
115
120
  try {
121
+ // No cast: the target is declared `unknown` and is validated below, so
122
+ // an assertion here would be discarded on assignment anyway.
116
123
  jsconfig = parse(jsconfigText, parseErrors, {
117
124
  allowTrailingComma: true,
118
125
  disallowComments: false,
119
126
  });
120
127
  }
121
128
  catch (error) {
122
- throw new FurnaceError(`Could not parse ${jsconfigRel} as JSONC: ${error instanceof Error ? error.message : String(error)}.`);
129
+ throw new FurnaceError(`Could not parse ${jsconfigRel} as JSONC: ${toError(error).message}.`);
123
130
  }
124
131
  if (parseErrors.length > 0 ||
125
132
  typeof jsconfig !== 'object' ||
@@ -151,7 +158,7 @@ export async function syncFurnaceJsconfigPaths(root, config, options) {
151
158
  // a hand-written `./` prefix) is not rewritten as "stale" on every run.
152
159
  // The existing value is kept verbatim when equivalent — no churn either
153
160
  // way; only a genuinely different target updates (to the `./` form).
154
- if (!samePathValue(value[0] ?? '', want[0] ?? '')) {
161
+ if (!samePathValue(value[0], want[0] ?? '')) {
155
162
  result.updated.push(key);
156
163
  nextPaths[key] = want;
157
164
  }