@hominis/fireforge 0.35.0 → 0.36.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 (355) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +34 -9
  3. package/dist/bin/fireforge.js +21 -6
  4. package/dist/src/cli.js +22 -5
  5. package/dist/src/commands/bootstrap-checks.js +0 -1
  6. package/dist/src/commands/bootstrap.d.ts +2 -1
  7. package/dist/src/commands/bootstrap.js +16 -7
  8. package/dist/src/commands/build.js +4 -3
  9. package/dist/src/commands/config.js +30 -10
  10. package/dist/src/commands/discard.js +18 -3
  11. package/dist/src/commands/doctor/post-rebase-audit.js +0 -1
  12. package/dist/src/commands/doctor-check-core.js +0 -1
  13. package/dist/src/commands/doctor-external-toolchains.d.ts +3 -0
  14. package/dist/src/commands/doctor-external-toolchains.js +54 -0
  15. package/dist/src/commands/doctor-furnace-jar.js +0 -1
  16. package/dist/src/commands/doctor-furnace-manifest-sync.js +0 -1
  17. package/dist/src/commands/doctor-furnace.js +2 -2
  18. package/dist/src/commands/doctor-working-tree.js +0 -1
  19. package/dist/src/commands/doctor.js +4 -1
  20. package/dist/src/commands/download.js +28 -13
  21. package/dist/src/commands/export-all.js +5 -3
  22. package/dist/src/commands/export-flow.js +12 -7
  23. package/dist/src/commands/export-placement-gate.js +0 -1
  24. package/dist/src/commands/export-placement-policy.js +0 -1
  25. package/dist/src/commands/export-shared.d.ts +5 -1
  26. package/dist/src/commands/export-shared.js +18 -6
  27. package/dist/src/commands/export.js +15 -10
  28. package/dist/src/commands/furnace/apply.js +22 -6
  29. package/dist/src/commands/furnace/chrome-doc-remove.js +0 -1
  30. package/dist/src/commands/furnace/chrome-doc-templates.js +0 -1
  31. package/dist/src/commands/furnace/chrome-doc-tests.js +0 -1
  32. package/dist/src/commands/furnace/chrome-doc.js +0 -1
  33. package/dist/src/commands/furnace/create-browser-test.js +0 -1
  34. package/dist/src/commands/furnace/create-dry-run.js +0 -1
  35. package/dist/src/commands/furnace/create-features.js +0 -1
  36. package/dist/src/commands/furnace/create-mochikit.js +0 -1
  37. package/dist/src/commands/furnace/create-readback.js +0 -1
  38. package/dist/src/commands/furnace/create-templates.js +0 -1
  39. package/dist/src/commands/furnace/create-validation.js +0 -1
  40. package/dist/src/commands/furnace/create-xpcshell.js +0 -1
  41. package/dist/src/commands/furnace/create.js +0 -1
  42. package/dist/src/commands/furnace/deploy.js +7 -83
  43. package/dist/src/commands/furnace/diff.js +0 -1
  44. package/dist/src/commands/furnace/index.js +59 -25
  45. package/dist/src/commands/furnace/init.js +0 -1
  46. package/dist/src/commands/furnace/list.js +0 -1
  47. package/dist/src/commands/furnace/override.js +0 -1
  48. package/dist/src/commands/furnace/preview.js +3 -3
  49. package/dist/src/commands/furnace/refresh.js +0 -1
  50. package/dist/src/commands/furnace/remove-state.d.ts +15 -0
  51. package/dist/src/commands/furnace/remove-state.js +47 -1
  52. package/dist/src/commands/furnace/remove.js +3 -9
  53. package/dist/src/commands/furnace/rename-browser-test.js +1 -5
  54. package/dist/src/commands/furnace/rename-helpers.d.ts +6 -0
  55. package/dist/src/commands/furnace/rename-helpers.js +30 -1
  56. package/dist/src/commands/furnace/rename-xpcshell.js +7 -6
  57. package/dist/src/commands/furnace/rename.js +15 -36
  58. package/dist/src/commands/furnace/scan.js +0 -1
  59. package/dist/src/commands/furnace/status.js +0 -1
  60. package/dist/src/commands/furnace/sync.js +76 -4
  61. package/dist/src/commands/furnace/validate.js +0 -1
  62. package/dist/src/commands/furnace/validation-output.js +0 -1
  63. package/dist/src/commands/import.js +35 -9
  64. package/dist/src/commands/lint-per-patch.js +19 -7
  65. package/dist/src/commands/lint.js +13 -3
  66. package/dist/src/commands/manifest.js +1 -2
  67. package/dist/src/commands/package.js +2 -1
  68. package/dist/src/commands/patch/compact.js +1 -1
  69. package/dist/src/commands/patch/delete.js +1 -1
  70. package/dist/src/commands/patch/index.js +1 -1
  71. package/dist/src/commands/patch/lint-ignore.js +21 -6
  72. package/dist/src/commands/patch/move-files.js +1 -1
  73. package/dist/src/commands/patch/patch-context.js +0 -1
  74. package/dist/src/commands/patch/rename.js +4 -4
  75. package/dist/src/commands/patch/reorder.js +3 -4
  76. package/dist/src/commands/patch/split-plan.js +2 -1
  77. package/dist/src/commands/patch/split.js +4 -4
  78. package/dist/src/commands/patch/staged-dependency.js +1 -1
  79. package/dist/src/commands/patch/tier.js +18 -5
  80. package/dist/src/commands/re-export-bulk-scan.js +0 -1
  81. package/dist/src/commands/re-export-files.js +1 -1
  82. package/dist/src/commands/re-export-options.js +0 -1
  83. package/dist/src/commands/re-export-scan.js +0 -1
  84. package/dist/src/commands/re-export.js +6 -4
  85. package/dist/src/commands/rebase/abort.js +2 -9
  86. package/dist/src/commands/rebase/confirm.js +0 -1
  87. package/dist/src/commands/rebase/conflict-summary.js +0 -1
  88. package/dist/src/commands/rebase/continue.js +0 -1
  89. package/dist/src/commands/rebase/index.d.ts +1 -1
  90. package/dist/src/commands/rebase/index.js +10 -15
  91. package/dist/src/commands/rebase/patch-loop.js +38 -7
  92. package/dist/src/commands/rebase/summary.js +2 -3
  93. package/dist/src/commands/rebase.js +0 -1
  94. package/dist/src/commands/register.js +1 -1
  95. package/dist/src/commands/reset.js +16 -9
  96. package/dist/src/commands/resolve.js +2 -2
  97. package/dist/src/commands/run.js +4 -4
  98. package/dist/src/commands/setup-support.js +0 -1
  99. package/dist/src/commands/setup.js +1 -2
  100. package/dist/src/commands/source.js +1 -2
  101. package/dist/src/commands/status-output.js +1 -12
  102. package/dist/src/commands/status.js +147 -133
  103. package/dist/src/commands/test-appdir.js +0 -1
  104. package/dist/src/commands/test-diagnose.js +18 -2
  105. package/dist/src/commands/test-modes.d.ts +15 -0
  106. package/dist/src/commands/test-modes.js +61 -0
  107. package/dist/src/commands/test-register.d.ts +4 -0
  108. package/dist/src/commands/test-register.js +64 -0
  109. package/dist/src/commands/test-run.d.ts +23 -7
  110. package/dist/src/commands/test-run.js +15 -14
  111. package/dist/src/commands/test.d.ts +0 -4
  112. package/dist/src/commands/test.js +85 -86
  113. package/dist/src/commands/token-coverage.js +0 -1
  114. package/dist/src/commands/token.js +1 -2
  115. package/dist/src/commands/typecheck.js +1 -1
  116. package/dist/src/commands/verify.js +3 -9
  117. package/dist/src/commands/watch.js +5 -1
  118. package/dist/src/commands/wire.js +1 -1
  119. package/dist/src/core/ast-utils.js +0 -1
  120. package/dist/src/core/brand-validation.js +0 -1
  121. package/dist/src/core/branding.js +0 -1
  122. package/dist/src/core/browser-wire.js +0 -1
  123. package/dist/src/core/build-audit-platform.js +0 -1
  124. package/dist/src/core/build-audit-registration.js +0 -1
  125. package/dist/src/core/build-audit-resolve.js +0 -1
  126. package/dist/src/core/build-audit-transforms.js +0 -1
  127. package/dist/src/core/build-audit.js +0 -1
  128. package/dist/src/core/build-baseline-types.js +0 -1
  129. package/dist/src/core/build-baseline.js +0 -1
  130. package/dist/src/core/build-prepare.js +2 -2
  131. package/dist/src/core/config-mutate.js +0 -1
  132. package/dist/src/core/config-paths.d.ts +2 -2
  133. package/dist/src/core/config-paths.js +6 -1
  134. package/dist/src/core/config-state.js +0 -1
  135. package/dist/src/core/config-validate-patch-policy.js +0 -1
  136. package/dist/src/core/config-validate-test-toolchains.d.ts +8 -0
  137. package/dist/src/core/config-validate-test-toolchains.js +105 -0
  138. package/dist/src/core/config-validate.js +3 -1
  139. package/dist/src/core/config.js +0 -1
  140. package/dist/src/core/destructive.js +0 -1
  141. package/dist/src/core/diff-hunks.js +13 -7
  142. package/dist/src/core/engine-changes.js +0 -1
  143. package/dist/src/core/engine-session-lock.d.ts +12 -0
  144. package/dist/src/core/engine-session-lock.js +45 -0
  145. package/dist/src/core/file-lock.d.ts +8 -0
  146. package/dist/src/core/file-lock.js +157 -48
  147. package/dist/src/core/firefox-archive.d.ts +4 -0
  148. package/dist/src/core/firefox-archive.js +6 -1
  149. package/dist/src/core/firefox-cache.js +60 -4
  150. package/dist/src/core/firefox-download.js +0 -1
  151. package/dist/src/core/firefox-extract.js +75 -16
  152. package/dist/src/core/firefox-ignorefile.js +0 -1
  153. package/dist/src/core/firefox.d.ts +12 -0
  154. package/dist/src/core/firefox.js +64 -2
  155. package/dist/src/core/furnace-apply-dry-run.js +0 -1
  156. package/dist/src/core/furnace-apply-ftl.d.ts +7 -3
  157. package/dist/src/core/furnace-apply-ftl.js +10 -5
  158. package/dist/src/core/furnace-apply-helpers.js +0 -1
  159. package/dist/src/core/furnace-apply-output.js +2 -2
  160. package/dist/src/core/furnace-apply.d.ts +7 -1
  161. package/dist/src/core/furnace-apply.js +47 -13
  162. package/dist/src/core/furnace-checksum-utils.js +0 -1
  163. package/dist/src/core/furnace-config-array-utils.js +0 -1
  164. package/dist/src/core/furnace-config-custom.js +0 -1
  165. package/dist/src/core/furnace-config-order.js +0 -1
  166. package/dist/src/core/furnace-config-tokens.js +0 -1
  167. package/dist/src/core/furnace-config-validate.js +0 -1
  168. package/dist/src/core/furnace-config.d.ts +11 -0
  169. package/dist/src/core/furnace-config.js +37 -1
  170. package/dist/src/core/furnace-constants.js +0 -1
  171. package/dist/src/core/furnace-css-fragments.js +0 -1
  172. package/dist/src/core/furnace-graph-utils.js +0 -1
  173. package/dist/src/core/furnace-jsconfig.d.ts +2 -3
  174. package/dist/src/core/furnace-jsconfig.js +25 -18
  175. package/dist/src/core/furnace-marker.js +0 -1
  176. package/dist/src/core/furnace-operation.js +0 -1
  177. package/dist/src/core/furnace-refresh.js +0 -1
  178. package/dist/src/core/furnace-registration-ast.d.ts +14 -0
  179. package/dist/src/core/furnace-registration-ast.js +1 -2
  180. package/dist/src/core/furnace-registration-remove.js +0 -1
  181. package/dist/src/core/furnace-registration-validate.js +1 -5
  182. package/dist/src/core/furnace-registration.js +0 -1
  183. package/dist/src/core/furnace-rollback.js +0 -1
  184. package/dist/src/core/furnace-scanner.js +0 -1
  185. package/dist/src/core/furnace-staleness.js +0 -1
  186. package/dist/src/core/furnace-state-persist.d.ts +72 -0
  187. package/dist/src/core/furnace-state-persist.js +118 -0
  188. package/dist/src/core/furnace-step-errors.d.ts +23 -0
  189. package/dist/src/core/furnace-step-errors.js +25 -0
  190. package/dist/src/core/furnace-stories.js +0 -1
  191. package/dist/src/core/furnace-validate-accessibility.js +0 -1
  192. package/dist/src/core/furnace-validate-checks.js +0 -1
  193. package/dist/src/core/furnace-validate-compatibility.js +0 -1
  194. package/dist/src/core/furnace-validate-helpers.js +0 -1
  195. package/dist/src/core/furnace-validate-registration.js +7 -7
  196. package/dist/src/core/furnace-validate-structure.d.ts +8 -0
  197. package/dist/src/core/furnace-validate-structure.js +11 -2
  198. package/dist/src/core/furnace-validate.js +0 -1
  199. package/dist/src/core/furnace-version-drift.d.ts +6 -3
  200. package/dist/src/core/furnace-version-drift.js +6 -4
  201. package/dist/src/core/git-base.js +0 -1
  202. package/dist/src/core/git-diff.js +0 -1
  203. package/dist/src/core/git-file-ops.js +0 -1
  204. package/dist/src/core/git-status.d.ts +13 -0
  205. package/dist/src/core/git-status.js +32 -2
  206. package/dist/src/core/git.d.ts +10 -1
  207. package/dist/src/core/git.js +63 -6
  208. package/dist/src/core/license-headers.js +0 -1
  209. package/dist/src/core/lint-cache.js +0 -1
  210. package/dist/src/core/lint-projection.js +0 -1
  211. package/dist/src/core/mach-build-artifacts.js +0 -1
  212. package/dist/src/core/mach-error-hints.js +0 -1
  213. package/dist/src/core/mach-mozconfig.js +0 -1
  214. package/dist/src/core/mach-python.js +0 -1
  215. package/dist/src/core/mach-resource-shim.js +0 -1
  216. package/dist/src/core/mach.js +0 -1
  217. package/dist/src/core/manifest-helpers.js +0 -1
  218. package/dist/src/core/manifest-register.js +0 -1
  219. package/dist/src/core/manifest-rules.js +0 -1
  220. package/dist/src/core/manifest-tokenizers.js +0 -1
  221. package/dist/src/core/marionette-port.d.ts +8 -0
  222. package/dist/src/core/marionette-port.js +34 -3
  223. package/dist/src/core/marionette-preflight.js +0 -1
  224. package/dist/src/core/markdown-table.js +0 -1
  225. package/dist/src/core/ownership-table.js +2 -9
  226. package/dist/src/core/parser-fallback.js +0 -1
  227. package/dist/src/core/patch-apply-fuzz.d.ts +27 -9
  228. package/dist/src/core/patch-apply-fuzz.js +71 -34
  229. package/dist/src/core/patch-apply.d.ts +30 -0
  230. package/dist/src/core/patch-apply.js +101 -18
  231. package/dist/src/core/patch-artifact-normalize.d.ts +18 -5
  232. package/dist/src/core/patch-artifact-normalize.js +19 -15
  233. package/dist/src/core/patch-export-coverage.js +0 -1
  234. package/dist/src/core/patch-export-metadata.js +0 -1
  235. package/dist/src/core/patch-export-update.js +2 -3
  236. package/dist/src/core/patch-export.d.ts +2 -6
  237. package/dist/src/core/patch-export.js +24 -48
  238. package/dist/src/core/patch-files.js +0 -1
  239. package/dist/src/core/patch-identifier-suggest.js +0 -1
  240. package/dist/src/core/patch-lint-checkjs.js +0 -1
  241. package/dist/src/core/patch-lint-chrome-jsdoc.js +0 -1
  242. package/dist/src/core/patch-lint-cross.js +0 -1
  243. package/dist/src/core/patch-lint-css.js +0 -1
  244. package/dist/src/core/patch-lint-diff-tag.js +0 -1
  245. package/dist/src/core/patch-lint-diff.d.ts +2 -1
  246. package/dist/src/core/patch-lint-diff.js +17 -32
  247. package/dist/src/core/patch-lint-imports.js +0 -1
  248. package/dist/src/core/patch-lint-jsdoc.js +0 -1
  249. package/dist/src/core/patch-lint-observer.js +0 -1
  250. package/dist/src/core/patch-lint-ownership.js +0 -1
  251. package/dist/src/core/patch-lint-reexports.js +0 -1
  252. package/dist/src/core/patch-lint.d.ts +37 -2
  253. package/dist/src/core/patch-lint.js +47 -34
  254. package/dist/src/core/patch-lock.js +0 -1
  255. package/dist/src/core/patch-manifest-consistency.js +9 -1
  256. package/dist/src/core/patch-manifest-io.d.ts +16 -0
  257. package/dist/src/core/patch-manifest-io.js +30 -5
  258. package/dist/src/core/patch-manifest-query.js +18 -10
  259. package/dist/src/core/patch-manifest-resolve.js +0 -1
  260. package/dist/src/core/patch-manifest-validate.js +0 -1
  261. package/dist/src/core/patch-manifest.d.ts +1 -1
  262. package/dist/src/core/patch-manifest.js +1 -2
  263. package/dist/src/core/patch-parse.d.ts +59 -0
  264. package/dist/src/core/patch-parse.js +209 -92
  265. package/dist/src/core/patch-policy.js +0 -1
  266. package/dist/src/core/patch-registration-refs.js +16 -26
  267. package/dist/src/core/patch-source-metadata.js +0 -1
  268. package/dist/src/core/patch-transform.d.ts +4 -0
  269. package/dist/src/core/patch-transform.js +30 -38
  270. package/dist/src/core/rebase-session.js +0 -1
  271. package/dist/src/core/register-browser-content.js +0 -1
  272. package/dist/src/core/register-module.js +0 -1
  273. package/dist/src/core/register-result.js +0 -1
  274. package/dist/src/core/register-scaffold.js +0 -1
  275. package/dist/src/core/register-shared-css.js +0 -1
  276. package/dist/src/core/register-test-manifest.js +0 -1
  277. package/dist/src/core/register-xpcshell-test.js +0 -1
  278. package/dist/src/core/shared-ftl.js +0 -1
  279. package/dist/src/core/signal-critical.js +0 -1
  280. package/dist/src/core/smoke-patterns.js +0 -1
  281. package/dist/src/core/state-file.js +0 -1
  282. package/dist/src/core/status-classify.d.ts +16 -0
  283. package/dist/src/core/status-classify.js +24 -16
  284. package/dist/src/core/test-harness-crash.d.ts +64 -7
  285. package/dist/src/core/test-harness-crash.js +196 -15
  286. package/dist/src/core/test-harness-output.js +0 -1
  287. package/dist/src/core/test-path-scope.d.ts +21 -13
  288. package/dist/src/core/test-path-scope.js +43 -28
  289. package/dist/src/core/test-stale-check.js +0 -1
  290. package/dist/src/core/test-stale-symlink.js +0 -1
  291. package/dist/src/core/test-xpcshell-retry.js +0 -1
  292. package/dist/src/core/token-coverage.js +0 -1
  293. package/dist/src/core/token-dark-mode.js +0 -1
  294. package/dist/src/core/token-docs.js +0 -1
  295. package/dist/src/core/token-manager.js +0 -1
  296. package/dist/src/core/token-scaffold.js +0 -1
  297. package/dist/src/core/token-variant.js +0 -1
  298. package/dist/src/core/toolchain-preflight.d.ts +31 -10
  299. package/dist/src/core/toolchain-preflight.js +97 -36
  300. package/dist/src/core/typecheck-shim.js +13 -1
  301. package/dist/src/core/typecheck.js +0 -1
  302. package/dist/src/core/wire-destroy.js +0 -1
  303. package/dist/src/core/wire-dom-fragment.js +0 -1
  304. package/dist/src/core/wire-init.js +0 -1
  305. package/dist/src/core/wire-subscript.js +0 -1
  306. package/dist/src/core/wire-targets.js +0 -1
  307. package/dist/src/core/wire-utils.js +0 -1
  308. package/dist/src/core/xpcshell-appdir.js +1 -4
  309. package/dist/src/errors/base.d.ts +29 -0
  310. package/dist/src/errors/base.js +35 -1
  311. package/dist/src/errors/build.js +0 -1
  312. package/dist/src/errors/codes.d.ts +6 -0
  313. package/dist/src/errors/codes.js +6 -1
  314. package/dist/src/errors/config.js +0 -1
  315. package/dist/src/errors/download.js +0 -1
  316. package/dist/src/errors/furnace.d.ts +11 -1
  317. package/dist/src/errors/furnace.js +6 -5
  318. package/dist/src/errors/git.js +0 -1
  319. package/dist/src/errors/patch.d.ts +17 -0
  320. package/dist/src/errors/patch.js +29 -1
  321. package/dist/src/errors/rebase.js +0 -1
  322. package/dist/src/errors/run.js +1 -1
  323. package/dist/src/index.js +0 -1
  324. package/dist/src/types/cli.js +0 -1
  325. package/dist/src/types/commands/index.js +0 -1
  326. package/dist/src/types/commands/options.d.ts +13 -5
  327. package/dist/src/types/commands/options.js +0 -1
  328. package/dist/src/types/commands/patches.d.ts +8 -0
  329. package/dist/src/types/commands/patches.js +0 -1
  330. package/dist/src/types/commands/project.d.ts +11 -2
  331. package/dist/src/types/commands/project.js +0 -1
  332. package/dist/src/types/config.d.ts +32 -0
  333. package/dist/src/types/config.js +0 -1
  334. package/dist/src/types/furnace.d.ts +10 -0
  335. package/dist/src/types/furnace.js +0 -1
  336. package/dist/src/types/index.js +0 -1
  337. package/dist/src/types/typecheck.js +0 -1
  338. package/dist/src/utils/elapsed.js +0 -1
  339. package/dist/src/utils/errors.js +0 -1
  340. package/dist/src/utils/fs.js +37 -5
  341. package/dist/src/utils/logger.d.ts +7 -0
  342. package/dist/src/utils/logger.js +85 -3
  343. package/dist/src/utils/options.d.ts +15 -0
  344. package/dist/src/utils/options.js +25 -1
  345. package/dist/src/utils/package-root.js +11 -4
  346. package/dist/src/utils/parse.js +0 -1
  347. package/dist/src/utils/paths.js +0 -1
  348. package/dist/src/utils/platform.js +0 -1
  349. package/dist/src/utils/process.d.ts +33 -4
  350. package/dist/src/utils/process.js +183 -28
  351. package/dist/src/utils/regex.d.ts +13 -3
  352. package/dist/src/utils/regex.js +53 -4
  353. package/dist/src/utils/validation.d.ts +0 -12
  354. package/dist/src/utils/validation.js +13 -26
  355. package/package.json +8 -7
@@ -22,8 +22,9 @@
22
22
  */
23
23
  import { readdir } from 'node:fs/promises';
24
24
  import { dirname, join, relative, resolve } from 'node:path';
25
+ import { applyEdits, modify, parse } from 'jsonc-parser';
25
26
  import { FurnaceError } from '../errors/furnace.js';
26
- import { pathExists, readJson, writeJson } from '../utils/fs.js';
27
+ import { pathExists, readText, writeTextIfChanged } from '../utils/fs.js';
27
28
  import { info } from '../utils/logger.js';
28
29
  import { normalizePathSlashes } from '../utils/paths.js';
29
30
  import { getFurnacePaths } from './furnace-config.js';
@@ -89,9 +90,8 @@ function isManagedEntry(key, value, jsconfigDir, customDir) {
89
90
  * writing.
90
91
  *
91
92
  * The consumer owns the jsconfig file: a missing file is an error with
92
- * guidance rather than a silent scaffold, and JSONC (comments/trailing
93
- * commas) is unsupported for the managed file — `readJson` is a strict
94
- * JSON parser, so the error message says so explicitly.
93
+ * guidance rather than a silent scaffold. JSONC comments and trailing commas
94
+ * are preserved; Furnace edits only `compilerOptions.paths`.
95
95
  *
96
96
  * @param root - Project root directory
97
97
  * @param config - Loaded Furnace configuration (must carry `typecheckJsconfig`)
@@ -109,19 +109,31 @@ export async function syncFurnaceJsconfigPaths(root, config, options) {
109
109
  `"compilerOptions.paths" entries under ${CHROME_ELEMENTS_URL_PREFIX}*), ` +
110
110
  'or remove the setting.');
111
111
  }
112
+ const jsconfigText = await readText(jsconfigAbs);
113
+ const parseErrors = [];
112
114
  let jsconfig;
113
115
  try {
114
- jsconfig = await readJson(jsconfigAbs);
116
+ jsconfig = parse(jsconfigText, parseErrors, {
117
+ allowTrailingComma: true,
118
+ disallowComments: false,
119
+ });
115
120
  }
116
121
  catch (error) {
117
- throw new FurnaceError(`Could not parse ${jsconfigRel} as JSON: ${error instanceof Error ? error.message : String(error)}. ` +
118
- 'Furnace manages paths entries only in plain-JSON jsconfig files — JSONC comments ' +
119
- 'and trailing commas are not supported for the managed file.');
122
+ throw new FurnaceError(`Could not parse ${jsconfigRel} as JSONC: ${error instanceof Error ? error.message : String(error)}.`);
120
123
  }
124
+ if (parseErrors.length > 0 ||
125
+ typeof jsconfig !== 'object' ||
126
+ jsconfig === null ||
127
+ Array.isArray(jsconfig)) {
128
+ throw new FurnaceError(`Could not parse ${jsconfigRel} as JSONC: expected an object jsconfig file.`);
129
+ }
130
+ const jsconfigObject = jsconfig;
121
131
  const furnacePaths = getFurnacePaths(root);
122
132
  const desired = await computeDesiredChromePathEntries(config, furnacePaths.customDir, jsconfigAbs);
123
133
  const jsconfigDir = dirname(jsconfigAbs);
124
- const currentPaths = { ...(jsconfig.compilerOptions?.paths ?? {}) };
134
+ const currentPaths = {
135
+ ...(jsconfigObject.compilerOptions?.paths ?? {}),
136
+ };
125
137
  const nextPaths = {};
126
138
  // Preserve every unmanaged entry verbatim; prune managed entries that are
127
139
  // no longer desired; update managed entries whose target moved.
@@ -156,14 +168,10 @@ export async function syncFurnaceJsconfigPaths(root, config, options) {
156
168
  result.changed = result.added.length > 0 || result.updated.length > 0 || result.pruned.length > 0;
157
169
  if (!result.changed || options?.dryRun === true)
158
170
  return result;
159
- const nextJsconfig = {
160
- ...jsconfig,
161
- compilerOptions: {
162
- ...(jsconfig.compilerOptions ?? {}),
163
- paths: nextPaths,
164
- },
165
- };
166
- await writeJson(jsconfigAbs, nextJsconfig);
171
+ const edits = modify(jsconfigText, ['compilerOptions', 'paths'], nextPaths, {
172
+ formattingOptions: { insertSpaces: true, tabSize: 2, eol: '\n' },
173
+ });
174
+ await writeTextIfChanged(jsconfigAbs, applyEdits(jsconfigText, edits));
167
175
  return result;
168
176
  }
169
177
  /**
@@ -188,4 +196,3 @@ export async function reportJsconfigPathsSync(root, config, dryRun) {
188
196
  info(`${dryRun ? '[dry-run] Would update' : 'Updated'} ${config.typecheckJsconfig} chrome-module paths: ` +
189
197
  `+${sync.added.length} added, ~${sync.updated.length} updated, -${sync.pruned.length} pruned`);
190
198
  }
191
- //# sourceMappingURL=furnace-jsconfig.js.map
@@ -20,4 +20,3 @@ export function resolveFurnaceMarkerComment(forgeConfig) {
20
20
  return forgeConfig.binaryName.toUpperCase();
21
21
  return undefined;
22
22
  }
23
- //# sourceMappingURL=furnace-marker.js.map
@@ -252,4 +252,3 @@ export function __resetFurnaceOperationStateForTests() {
252
252
  nextOperationToken = 1;
253
253
  signalRollbackInFlight = false;
254
254
  }
255
- //# sourceMappingURL=furnace-operation.js.map
@@ -126,4 +126,3 @@ export async function refreshOverrideFile(engineDir, overridePath, engineRelPath
126
126
  }
127
127
  return { fileName, status: 'merged' };
128
128
  }
129
- //# sourceMappingURL=furnace-refresh.js.map
@@ -2,6 +2,20 @@
2
2
  * AST-based custom element registration updates for customElements.js.
3
3
  * Removal logic is in furnace-registration-remove.ts.
4
4
  */
5
+ /**
6
+ * Returns true when `content` already contains a registration for `tagName`.
7
+ *
8
+ * Tolerates trailing line comments (e.g. a project marker like `// MYBROWSER:`)
9
+ * that an operator may have appended to entries written by a previous apply.
10
+ * Without this, a re-apply would insert a duplicate entry, and the second
11
+ * `setElementCreationCallback` at window-load would throw `NotSupportedError`.
12
+ *
13
+ * Matches any of:
14
+ * 1. `setElementCreationCallback("tag"` / `setElementCreationCallback('tag'`
15
+ * 2. Single-line array entry: `["tag", "..."]` — column 0 only, comments allowed after
16
+ * 3. Multi-line array entry: `"tag",` on its own line, optional trailing `//` comment
17
+ */
18
+ export declare function isTagAlreadyRegistered(content: string, tagName: string): boolean;
5
19
  /**
6
20
  * Options that shape the lines `addCustomElementRegistration` writes into
7
21
  * `customElements.js`. Kept optional so existing call sites keep working.
@@ -26,7 +26,7 @@ import { validateRegistrationPlacement, validateTagName } from './furnace-regist
26
26
  * 2. Single-line array entry: `["tag", "..."]` — column 0 only, comments allowed after
27
27
  * 3. Multi-line array entry: `"tag",` on its own line, optional trailing `//` comment
28
28
  */
29
- function isTagAlreadyRegistered(content, tagName) {
29
+ export function isTagAlreadyRegistered(content, tagName) {
30
30
  const escaped = escapeRegex(tagName);
31
31
  if (content.includes(`setElementCreationCallback("${tagName}"`) ||
32
32
  content.includes(`setElementCreationCallback('${tagName}'`)) {
@@ -403,4 +403,3 @@ export async function validateCustomElementRegistration(engineDir, tagName, modu
403
403
  addRegistrationRegexFallback(content, tagName, modulePath, isESModule);
404
404
  }
405
405
  }
406
- //# sourceMappingURL=furnace-registration-ast.js.map
@@ -220,4 +220,3 @@ export async function removeCustomElementRegistration(engineDir, tagName) {
220
220
  await writeText(filePath, next);
221
221
  }
222
222
  }
223
- //# sourceMappingURL=furnace-registration-remove.js.map
@@ -4,7 +4,7 @@
4
4
  * Used after both AST and legacy code paths to avoid duplicating logic.
5
5
  */
6
6
  import { FurnaceError } from '../errors/furnace.js';
7
- import { stripJsComments } from '../utils/regex.js';
7
+ import { escapeRegex, stripJsComments } from '../utils/regex.js';
8
8
  /**
9
9
  * Regex for valid custom element tag names. A valid name is lowercase, starts
10
10
  * with a letter, and contains one or more hyphen-separated groups where each
@@ -88,7 +88,3 @@ function isTagInArrayConsumedInsideDOMContentLoaded(content, domContentLoadedIdx
88
88
  }
89
89
  return false;
90
90
  }
91
- function escapeRegex(value) {
92
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
93
- }
94
- //# sourceMappingURL=furnace-registration-validate.js.map
@@ -324,4 +324,3 @@ export async function validateJarMnInsertionForFiles(engineDir, tagName, files)
324
324
  throw new FurnaceError('Could not find a content/global/ section in jar.mn for element entries. The file may be malformed.', tagName);
325
325
  }
326
326
  }
327
- //# sourceMappingURL=furnace-registration.js.map
@@ -134,4 +134,3 @@ export async function restoreRollbackJournalOrThrow(journal, context) {
134
134
  throw new FurnaceError(`${context}; automatic rollback failed: ${message}`);
135
135
  }
136
136
  }
137
- //# sourceMappingURL=furnace-rollback.js.map
@@ -237,4 +237,3 @@ export async function isComponentInEngine(engineDir, tagName) {
237
237
  const componentDir = join(engineDir, WIDGETS_DIR, tagName);
238
238
  return pathExists(componentDir);
239
239
  }
240
- //# sourceMappingURL=furnace-scanner.js.map
@@ -55,4 +55,3 @@ export async function warnIfFurnaceStale(projectRoot) {
55
55
  verbose('Furnace staleness check skipped due to an error.');
56
56
  }
57
57
  }
58
- //# sourceMappingURL=furnace-staleness.js.map
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Single-component furnace state persistence.
3
+ *
4
+ * There are exactly two sanctioned ways checksums land in
5
+ * furnace-state.json:
6
+ *
7
+ * 1. The BATCH path inside `applyAllComponents` (persistState: true),
8
+ * which replaces `appliedChecksums` wholesale — correct only when the
9
+ * run covered every component.
10
+ * 2. The per-component MERGE in this module, which rewrites only the
11
+ * `<type>/<name>/…` keys of the component that was applied.
12
+ *
13
+ * Every targeted (named) apply/deploy MUST use path 2 with
14
+ * `persistState: false`. Routing a named run through the batch path wipes
15
+ * every other component's checksum state: the batch loops filter to the
16
+ * named component, so the wholesale replace persists a state file
17
+ * containing only that component. Downstream, `diffDeletedFiles` and
18
+ * `findOrphanedEngineFiles` both key on `appliedChecksums`, so the wiped
19
+ * components' stale engine files become undetectable by apply AND by
20
+ * `furnace validate` (2026-07-05 review, finding F1 — `furnace apply
21
+ * <name>` had exactly this bug while `furnace deploy <name>` documented
22
+ * and avoided it).
23
+ */
24
+ import { type applyAllComponents } from './furnace-apply.js';
25
+ import { type getFurnacePaths } from './furnace-config.js';
26
+ type ApplyAllResult = Awaited<ReturnType<typeof applyAllComponents>>;
27
+ /**
28
+ * Counts applied entries that carry at least one BLOCKING step error
29
+ * (advisory step errors are warnings and never fail a run).
30
+ */
31
+ export declare function getStepFailureCount(result: ApplyAllResult): number;
32
+ /**
33
+ * Decides whether a single-component apply/deploy completed cleanly enough
34
+ * to persist its checksums into furnace-state.json.
35
+ *
36
+ * Named runs are atomic: if any apply step fails, the rollback journal
37
+ * restores the engine to its pre-run state and this helper returns `false`
38
+ * so state is not touched. The conditions must stay in lock-step with the
39
+ * rollback trigger in `applyAllComponents` — both read step errors, so a
40
+ * future refactor cannot drift them apart and accidentally persist partial
41
+ * state.
42
+ */
43
+ export declare function shouldPersistSingleComponentState(result: ApplyAllResult, isDryRun: boolean): boolean;
44
+ /**
45
+ * Validates and narrows the applied entry a named run should persist.
46
+ *
47
+ * Guards against future refactors that might reorder or misroute the
48
+ * applied[] array: a named run persists state under a single component
49
+ * name, so the first applied entry MUST be that component. Persisting a
50
+ * different component's checksums would cause the next status/apply run to
51
+ * mis-report health for both components involved.
52
+ *
53
+ * @param commandLabel - Human label for error messages ("Deploy", "Apply")
54
+ */
55
+ export declare function getPersistableAppliedEntry(commandLabel: string, name: string, appliedEntry: ApplyAllResult['applied'][number] | undefined): {
56
+ name: string;
57
+ type: 'override' | 'custom';
58
+ };
59
+ /**
60
+ * Persists checksum state for a successfully applied named component by
61
+ * MERGING its `<type>/<name>/…` keys into the existing state — never
62
+ * replacing the whole map.
63
+ *
64
+ * @param projectRoot - Root directory of the project
65
+ * @param appliedEntry - Applied component descriptor
66
+ * @param furnacePaths - Resolved Furnace workspace paths
67
+ */
68
+ export declare function persistSingleComponentState(projectRoot: string, appliedEntry: {
69
+ name: string;
70
+ type: 'override' | 'custom';
71
+ }, furnacePaths: ReturnType<typeof getFurnacePaths>): Promise<void>;
72
+ export {};
@@ -0,0 +1,118 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Single-component furnace state persistence.
4
+ *
5
+ * There are exactly two sanctioned ways checksums land in
6
+ * furnace-state.json:
7
+ *
8
+ * 1. The BATCH path inside `applyAllComponents` (persistState: true),
9
+ * which replaces `appliedChecksums` wholesale — correct only when the
10
+ * run covered every component.
11
+ * 2. The per-component MERGE in this module, which rewrites only the
12
+ * `<type>/<name>/…` keys of the component that was applied.
13
+ *
14
+ * Every targeted (named) apply/deploy MUST use path 2 with
15
+ * `persistState: false`. Routing a named run through the batch path wipes
16
+ * every other component's checksum state: the batch loops filter to the
17
+ * named component, so the wholesale replace persists a state file
18
+ * containing only that component. Downstream, `diffDeletedFiles` and
19
+ * `findOrphanedEngineFiles` both key on `appliedChecksums`, so the wiped
20
+ * components' stale engine files become undetectable by apply AND by
21
+ * `furnace validate` (2026-07-05 review, finding F1 — `furnace apply
22
+ * <name>` had exactly this bug while `furnace deploy <name>` documented
23
+ * and avoided it).
24
+ */
25
+ import { join } from 'node:path';
26
+ import { FurnaceError } from '../errors/furnace.js';
27
+ import { computeComponentChecksums, prefixChecksums, } from './furnace-apply.js';
28
+ import { updateFurnaceState } from './furnace-config.js';
29
+ import { countEntriesWithBlockingStepErrors } from './furnace-step-errors.js';
30
+ /**
31
+ * Counts applied entries that carry at least one BLOCKING step error
32
+ * (advisory step errors are warnings and never fail a run).
33
+ */
34
+ export function getStepFailureCount(result) {
35
+ return countEntriesWithBlockingStepErrors(result.applied);
36
+ }
37
+ /**
38
+ * Decides whether a single-component apply/deploy completed cleanly enough
39
+ * to persist its checksums into furnace-state.json.
40
+ *
41
+ * Named runs are atomic: if any apply step fails, the rollback journal
42
+ * restores the engine to its pre-run state and this helper returns `false`
43
+ * so state is not touched. The conditions must stay in lock-step with the
44
+ * rollback trigger in `applyAllComponents` — both read step errors, so a
45
+ * future refactor cannot drift them apart and accidentally persist partial
46
+ * state.
47
+ */
48
+ export function shouldPersistSingleComponentState(result, isDryRun) {
49
+ if (isDryRun)
50
+ return false;
51
+ if (result.errors.length > 0)
52
+ return false;
53
+ if (getStepFailureCount(result) > 0)
54
+ return false;
55
+ return result.applied.length > 0;
56
+ }
57
+ /**
58
+ * Validates and narrows the applied entry a named run should persist.
59
+ *
60
+ * Guards against future refactors that might reorder or misroute the
61
+ * applied[] array: a named run persists state under a single component
62
+ * name, so the first applied entry MUST be that component. Persisting a
63
+ * different component's checksums would cause the next status/apply run to
64
+ * mis-report health for both components involved.
65
+ *
66
+ * @param commandLabel - Human label for error messages ("Deploy", "Apply")
67
+ */
68
+ export function getPersistableAppliedEntry(commandLabel, name, appliedEntry) {
69
+ if (!appliedEntry) {
70
+ throw new FurnaceError(`${commandLabel} for "${name}" finished without producing an applied component entry; ` +
71
+ `furnace state was not modified. Run "fireforge doctor --repair-furnace" to ` +
72
+ `reconcile state, then retry. If this persists, file a bug with the ` +
73
+ `output of "fireforge doctor".`);
74
+ }
75
+ if (appliedEntry.type !== 'override' && appliedEntry.type !== 'custom') {
76
+ throw new FurnaceError(`${commandLabel} for "${name}" returned an unsupported component type "${appliedEntry.type}"; ` +
77
+ `furnace state was not modified. Run "fireforge doctor --repair-furnace" to reconcile, ` +
78
+ `then verify the component with "fireforge furnace validate" before retrying.`);
79
+ }
80
+ if (appliedEntry.name !== name) {
81
+ throw new FurnaceError(`${commandLabel} for "${name}" returned an applied entry for a different component ` +
82
+ `("${appliedEntry.name}"); refusing to persist mismatched state. ` +
83
+ `Run "fireforge doctor --repair-furnace" to reconcile, then retry.`);
84
+ }
85
+ return {
86
+ name: appliedEntry.name,
87
+ type: appliedEntry.type,
88
+ };
89
+ }
90
+ /**
91
+ * Persists checksum state for a successfully applied named component by
92
+ * MERGING its `<type>/<name>/…` keys into the existing state — never
93
+ * replacing the whole map.
94
+ *
95
+ * @param projectRoot - Root directory of the project
96
+ * @param appliedEntry - Applied component descriptor
97
+ * @param furnacePaths - Resolved Furnace workspace paths
98
+ */
99
+ export async function persistSingleComponentState(projectRoot, appliedEntry, furnacePaths) {
100
+ const componentDir = appliedEntry.type === 'override'
101
+ ? join(furnacePaths.overridesDir, appliedEntry.name)
102
+ : join(furnacePaths.customDir, appliedEntry.name);
103
+ const checksums = await computeComponentChecksums(componentDir);
104
+ const prefixed = prefixChecksums(checksums, appliedEntry.type, appliedEntry.name);
105
+ const componentPrefix = `${appliedEntry.type}/${appliedEntry.name}/`;
106
+ await updateFurnaceState(projectRoot, (current) => ({
107
+ ...current,
108
+ appliedChecksums: {
109
+ ...Object.fromEntries(Object.entries(current.appliedChecksums ?? {}).filter(([key]) => !key.startsWith(componentPrefix))),
110
+ ...prefixed,
111
+ },
112
+ engineChecksums: {
113
+ ...Object.fromEntries(Object.entries(current.engineChecksums ?? {}).filter(([key]) => !key.startsWith(componentPrefix))),
114
+ ...prefixed,
115
+ },
116
+ lastApply: new Date().toISOString(),
117
+ }));
118
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared predicates over {@link StepError} lists.
3
+ *
4
+ * Step errors come in two severities: BLOCKING (the default — the apply is
5
+ * considered failed, triggers rollback and a non-zero exit) and ADVISORY
6
+ * (`advisory: true` — reported as a warning, never blocks; used by the
7
+ * `.ftl` helpers whose contract is graceful degradation). Every gate that
8
+ * decides rollback/failure must count only blocking errors; every reporter
9
+ * should still surface advisory ones. This module is a leaf (types-only
10
+ * imports) so both `furnace-apply.ts` and its callers can share the
11
+ * predicates without import cycles.
12
+ */
13
+ import type { StepError } from '../types/furnace.js';
14
+ /** Narrow shape shared by applied-component entries carrying step errors. */
15
+ export interface HasStepErrors {
16
+ stepErrors?: StepError[];
17
+ }
18
+ /** Returns only the blocking (non-advisory) step errors. */
19
+ export declare function blockingStepErrors(stepErrors: StepError[] | undefined): StepError[];
20
+ /** True when the entry carries at least one blocking step error. */
21
+ export declare function hasBlockingStepErrors(entry: HasStepErrors): boolean;
22
+ /** Counts applied entries that carry at least one blocking step error. */
23
+ export declare function countEntriesWithBlockingStepErrors(entries: readonly HasStepErrors[]): number;
@@ -0,0 +1,25 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Shared predicates over {@link StepError} lists.
4
+ *
5
+ * Step errors come in two severities: BLOCKING (the default — the apply is
6
+ * considered failed, triggers rollback and a non-zero exit) and ADVISORY
7
+ * (`advisory: true` — reported as a warning, never blocks; used by the
8
+ * `.ftl` helpers whose contract is graceful degradation). Every gate that
9
+ * decides rollback/failure must count only blocking errors; every reporter
10
+ * should still surface advisory ones. This module is a leaf (types-only
11
+ * imports) so both `furnace-apply.ts` and its callers can share the
12
+ * predicates without import cycles.
13
+ */
14
+ /** Returns only the blocking (non-advisory) step errors. */
15
+ export function blockingStepErrors(stepErrors) {
16
+ return (stepErrors ?? []).filter((e) => e.advisory !== true);
17
+ }
18
+ /** True when the entry carries at least one blocking step error. */
19
+ export function hasBlockingStepErrors(entry) {
20
+ return blockingStepErrors(entry.stepErrors).length > 0;
21
+ }
22
+ /** Counts applied entries that carry at least one blocking step error. */
23
+ export function countEntriesWithBlockingStepErrors(entries) {
24
+ return entries.filter((entry) => hasBlockingStepErrors(entry)).length;
25
+ }
@@ -184,4 +184,3 @@ export async function cleanStories(engineDir) {
184
184
  await removeDir(storiesDir);
185
185
  return count;
186
186
  }
187
- //# sourceMappingURL=furnace-stories.js.map
@@ -55,4 +55,3 @@ export async function validateAccessibility(componentDir, tagName, customConfig)
55
55
  }
56
56
  return issues;
57
57
  }
58
- //# sourceMappingURL=furnace-validate-accessibility.js.map
@@ -4,4 +4,3 @@ export { validateAccessibility } from './furnace-validate-accessibility.js';
4
4
  export { validateCompatibility } from './furnace-validate-compatibility.js';
5
5
  export { checkRegistrationConsistency, validateJarMnEntries, validateRegistrationPatterns, validateTokenLink, } from './furnace-validate-registration.js';
6
6
  export { validateStructure } from './furnace-validate-structure.js';
7
- //# sourceMappingURL=furnace-validate-checks.js.map
@@ -149,4 +149,3 @@ export async function validateCompatibility(componentDir, tagName, type, config,
149
149
  }
150
150
  return issues;
151
151
  }
152
- //# sourceMappingURL=furnace-validate-compatibility.js.map
@@ -434,4 +434,3 @@ export async function findOrphanedEngineFiles(root, config, tagName, state, ftlD
434
434
  }
435
435
  return issues;
436
436
  }
437
- //# sourceMappingURL=furnace-validate-helpers.js.map
@@ -5,12 +5,13 @@ import { join } from 'node:path';
5
5
  import { toError } from '../utils/errors.js';
6
6
  import { pathExists, readText } from '../utils/fs.js';
7
7
  import { warn } from '../utils/logger.js';
8
- import { stripJsComments } from '../utils/regex.js';
8
+ import { escapeRegex, stripJsComments } from '../utils/regex.js';
9
9
  import { getProjectPaths, loadConfig } from './config.js';
10
10
  import { getFurnacePaths } from './furnace-config.js';
11
11
  import { CUSTOM_ELEMENTS_JS, FTL_DIR, JAR_MN } from './furnace-constants.js';
12
12
  import { expandCssFragments, listFragmentIncludes } from './furnace-css-fragments.js';
13
13
  import { findStaleJarMnEntries } from './furnace-registration.js';
14
+ import { isTagAlreadyRegistered } from './furnace-registration-ast.js';
14
15
  import { isTagInCorrectCustomElementsPlacement } from './furnace-registration-validate.js';
15
16
  import { getTokensCssPath } from './token-manager.js';
16
17
  /**
@@ -47,9 +48,6 @@ export async function validateRegistrationPatterns(root, config) {
47
48
  }
48
49
  return issues;
49
50
  }
50
- function escapeRegex(value) {
51
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
52
- }
53
51
  /**
54
52
  * Checks registration consistency for a single custom component.
55
53
  *
@@ -160,8 +158,11 @@ export async function checkRegistrationConsistency(root, name, config, ftlDir) {
160
158
  const cePath = join(engineDir, CUSTOM_ELEMENTS_JS);
161
159
  if (await pathExists(cePath)) {
162
160
  const ceContent = await readText(cePath);
163
- status.customElementsPresent =
164
- ceContent.includes(`"${name}"`) || ceContent.includes(`'${name}'`);
161
+ // Structure-aware check shared with the ADD path — a bare substring
162
+ // test counted any mention of the tag (a leftover comment, an
163
+ // unrelated string) as "registered", masking genuinely missing
164
+ // registrations from both validate and the re-apply drift oracle.
165
+ status.customElementsPresent = isTagAlreadyRegistered(ceContent, name);
165
166
  if (status.customElementsPresent) {
166
167
  status.customElementsCorrectBlock = isTagInCorrectCustomElementsPlacement(ceContent, name, true);
167
168
  }
@@ -396,4 +397,3 @@ export async function runPostApplyConsistencyChecks(root, config, result, ftlDir
396
397
  }
397
398
  }
398
399
  }
399
- //# sourceMappingURL=furnace-validate-registration.js.map
@@ -1,4 +1,12 @@
1
1
  import type { ComponentType, CustomComponentConfig, ValidationIssue } from '../types/furnace.js';
2
+ /**
3
+ * Returns true when file content carries unresolved three-way-merge
4
+ * conflict markers (as left behind by `furnace refresh`). Shared between
5
+ * the structure validator and `furnace sync`'s pre-apply gate — both must
6
+ * agree on what "conflicted" means so a file that validate flags can never
7
+ * slip through sync into the engine.
8
+ */
9
+ export declare function containsMergeConflictMarkers(content: string): boolean;
2
10
  /**
3
11
  * Validates the file structure of a component directory.
4
12
  * Checks for required files and naming conventions.
@@ -2,6 +2,16 @@
2
2
  import { readdir } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { pathExists, readText } from '../utils/fs.js';
5
+ /**
6
+ * Returns true when file content carries unresolved three-way-merge
7
+ * conflict markers (as left behind by `furnace refresh`). Shared between
8
+ * the structure validator and `furnace sync`'s pre-apply gate — both must
9
+ * agree on what "conflicted" means so a file that validate flags can never
10
+ * slip through sync into the engine.
11
+ */
12
+ export function containsMergeConflictMarkers(content) {
13
+ return /^<{7}\s/m.test(content) || /^>{7}\s/m.test(content) || /^={7}$/m.test(content);
14
+ }
5
15
  /**
6
16
  * Validates the file structure of a component directory.
7
17
  * Checks for required files and naming conventions.
@@ -65,7 +75,7 @@ export async function validateStructure(componentDir, tagName, type, customConfi
65
75
  !entry.name.endsWith('.ftl'))
66
76
  continue;
67
77
  const content = await readText(join(componentDir, entry.name));
68
- if (/^<{7}\s/m.test(content) || /^>{7}\s/m.test(content) || /^={7}$/m.test(content)) {
78
+ if (containsMergeConflictMarkers(content)) {
69
79
  issues.push({
70
80
  component: tagName,
71
81
  severity: 'error',
@@ -109,4 +119,3 @@ export async function validateStructure(componentDir, tagName, type, customConfi
109
119
  }
110
120
  return issues;
111
121
  }
112
- //# sourceMappingURL=furnace-validate-structure.js.map
@@ -298,4 +298,3 @@ async function findOrphanXpcshellScaffolds(root, config) {
298
298
  }
299
299
  return issues;
300
300
  }
301
- //# sourceMappingURL=furnace-validate.js.map
@@ -14,9 +14,12 @@
14
14
  * copied from `forgeConfig.firefox.version`. Any string mismatch is worth
15
15
  * surfacing — even "140.0" vs "140.9.0esr" is a real drift signal.
16
16
  *
17
- * The result is advisory: apply/deploy emit warnings but do not fail, and
18
- * status reports drift alongside the component overview. Nothing here
19
- * should be wired into a blocking code path without an operator prompt.
17
+ * The result GATES apply/deploy: both warn and then FAIL without --force
18
+ * (see furnaceApplyCommand / furnaceDeployCommand), and `furnace sync`
19
+ * re-checks it after refresh before applying. Status reports drift
20
+ * alongside the component overview. Keep this doc in sync with the gates —
21
+ * an earlier version claimed the result was advisory, which invited
22
+ * re-breaking the gate.
20
23
  */
21
24
  import type { FurnaceConfig } from '../types/furnace.js';
22
25
  /** Severity of the version drift between an override's base and the current Firefox version. */
@@ -15,9 +15,12 @@
15
15
  * copied from `forgeConfig.firefox.version`. Any string mismatch is worth
16
16
  * surfacing — even "140.0" vs "140.9.0esr" is a real drift signal.
17
17
  *
18
- * The result is advisory: apply/deploy emit warnings but do not fail, and
19
- * status reports drift alongside the component overview. Nothing here
20
- * should be wired into a blocking code path without an operator prompt.
18
+ * The result GATES apply/deploy: both warn and then FAIL without --force
19
+ * (see furnaceApplyCommand / furnaceDeployCommand), and `furnace sync`
20
+ * re-checks it after refresh before applying. Status reports drift
21
+ * alongside the component overview. Keep this doc in sync with the gates —
22
+ * an earlier version claimed the result was advisory, which invited
23
+ * re-breaking the gate.
21
24
  */
22
25
  /**
23
26
  * Parses a version string into its major, minor, and patch numeric
@@ -98,4 +101,3 @@ export function formatOverrideBaseVersionDriftError(entries) {
98
101
  'update baseVersion in furnace.json to acknowledge the new baseline, ' +
99
102
  'or pass --force to proceed despite the drift.');
100
103
  }
101
- //# sourceMappingURL=furnace-version-drift.js.map
@@ -117,4 +117,3 @@ export async function configureGitPerformance(repoDir) {
117
117
  await git(['config', 'core.fsmonitor', 'false'], repoDir);
118
118
  await git(['config', 'feature.manyFiles', 'true'], repoDir);
119
119
  }
120
- //# sourceMappingURL=git-base.js.map
@@ -476,4 +476,3 @@ export async function generateBinaryFilePatch(repoDir, filePath) {
476
476
  }
477
477
  });
478
478
  }
479
- //# sourceMappingURL=git-diff.js.map
@@ -228,4 +228,3 @@ export async function isBinaryFile(repoDir, filePath) {
228
228
  return false;
229
229
  }
230
230
  }
231
- //# sourceMappingURL=git-file-ops.js.map
@@ -12,9 +12,22 @@ export declare function parsePorcelainStatus(output: string): GitStatusEntry[];
12
12
  * @returns Parsed git status entries
13
13
  */
14
14
  export declare function getWorkingTreeStatus(repoDir: string): Promise<GitStatusEntry[]>;
15
+ /**
16
+ * Resolves the per-directory expansion cap, honoring the same
17
+ * FIREFORGE_MAX_UNTRACKED_FILES override the status command documents.
18
+ */
19
+ export declare function resolveMaxUntrackedFilesPerDir(): number;
15
20
  /**
16
21
  * Expands collapsed untracked directory entries into individual file entries.
17
22
  * Git status may report "?? dir/" instead of listing each file underneath.
23
+ *
24
+ * Expansion is capped per directory (FIREFORGE_MAX_UNTRACKED_FILES, default
25
+ * 5000) with a warning on overflow. The cap used to exist only in the
26
+ * status command's private expander; this shared one — used by reset,
27
+ * discard, verify, lint, export-all — was unbounded, so e.g. `reset
28
+ * --dry-run` after an interrupted download (unborn HEAD, the entire ~300k
29
+ * file tree untracked) enumerated and printed every file.
30
+ *
18
31
  * @param repoDir - Repository directory
19
32
  * @param entries - Parsed status entries
20
33
  * @returns Status entries with untracked directories expanded to individual files