@hominis/fireforge 0.45.0 → 0.46.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 (359) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/dist/bin/fireforge.js +6 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/src/cli-usage-error.d.ts +46 -0
  5. package/dist/src/cli-usage-error.js +81 -0
  6. package/dist/src/cli.d.ts +5 -1
  7. package/dist/src/cli.js +20 -21
  8. package/dist/src/commands/bootstrap.js +1 -2
  9. package/dist/src/commands/build.js +15 -6
  10. package/dist/src/commands/destructive-decision.d.ts +19 -0
  11. package/dist/src/commands/destructive-decision.js +31 -0
  12. package/dist/src/commands/doctor-check-core.d.ts +0 -6
  13. package/dist/src/commands/doctor-check-core.js +0 -8
  14. package/dist/src/commands/doctor-orphaned-harness.d.ts +0 -6
  15. package/dist/src/commands/doctor-orphaned-harness.js +4 -12
  16. package/dist/src/commands/doctor-post-rebase-audit.js +1 -1
  17. package/dist/src/commands/doctor.d.ts +2 -0
  18. package/dist/src/commands/doctor.js +7 -8
  19. package/dist/src/commands/download.js +35 -24
  20. package/dist/src/commands/export-all.js +21 -4
  21. package/dist/src/commands/export-flow.js +15 -45
  22. package/dist/src/commands/export-placement-conflicts.js +3 -3
  23. package/dist/src/commands/export-placement-gate.js +2 -8
  24. package/dist/src/commands/export-placement-policy.js +3 -5
  25. package/dist/src/commands/export-shared.d.ts +42 -23
  26. package/dist/src/commands/export-shared.js +4 -19
  27. package/dist/src/commands/export.js +10 -1
  28. package/dist/src/commands/furnace/authoring-config.d.ts +13 -0
  29. package/dist/src/commands/furnace/authoring-config.js +19 -0
  30. package/dist/src/commands/furnace/chrome-doc-remove.js +1 -1
  31. package/dist/src/commands/furnace/create-browser-test.js +3 -8
  32. package/dist/src/commands/furnace/create-dry-run.js +2 -5
  33. package/dist/src/commands/furnace/create-templates.d.ts +17 -1
  34. package/dist/src/commands/furnace/create-templates.js +2 -1
  35. package/dist/src/commands/furnace/create.js +25 -19
  36. package/dist/src/commands/furnace/deploy.js +11 -2
  37. package/dist/src/commands/furnace/init.js +2 -1
  38. package/dist/src/commands/furnace/override.js +28 -19
  39. package/dist/src/commands/furnace/refresh.js +9 -1
  40. package/dist/src/commands/furnace/remove-mutations.d.ts +6 -7
  41. package/dist/src/commands/furnace/remove-mutations.js +0 -1
  42. package/dist/src/commands/furnace/remove.js +2 -6
  43. package/dist/src/commands/furnace/rename-browser-test.js +1 -7
  44. package/dist/src/commands/furnace/rename.js +29 -29
  45. package/dist/src/commands/furnace/status.js +12 -9
  46. package/dist/src/commands/furnace/test-file-name.d.ts +28 -0
  47. package/dist/src/commands/furnace/test-file-name.js +39 -0
  48. package/dist/src/commands/furnace/validation-output.d.ts +21 -9
  49. package/dist/src/commands/furnace/validation-output.js +3 -8
  50. package/dist/src/commands/import.js +2 -9
  51. package/dist/src/commands/lint-per-patch.d.ts +2 -2
  52. package/dist/src/commands/lint-per-patch.js +19 -4
  53. package/dist/src/commands/lint-per-run-checkjs.d.ts +2 -2
  54. package/dist/src/commands/lint.d.ts +2 -1
  55. package/dist/src/commands/lint.js +1 -1
  56. package/dist/src/commands/manifest.js +1 -1
  57. package/dist/src/commands/package.js +2 -4
  58. package/dist/src/commands/patch/compact.js +14 -15
  59. package/dist/src/commands/patch/delete.js +15 -25
  60. package/dist/src/commands/patch/entry-projection.d.ts +13 -0
  61. package/dist/src/commands/patch/entry-projection.js +24 -0
  62. package/dist/src/commands/patch/lint-ignore.d.ts +0 -8
  63. package/dist/src/commands/patch/lint-ignore.js +19 -29
  64. package/dist/src/commands/patch/move-files-create.js +24 -10
  65. package/dist/src/commands/patch/move-files-into.js +38 -34
  66. package/dist/src/commands/patch/move-files.js +4 -13
  67. package/dist/src/commands/patch/rename.js +27 -41
  68. package/dist/src/commands/patch/reorder.js +25 -38
  69. package/dist/src/commands/patch/split-plan.d.ts +4 -5
  70. package/dist/src/commands/patch/split-plan.js +4 -8
  71. package/dist/src/commands/patch/split.js +45 -43
  72. package/dist/src/commands/patch/staged-dependency.js +16 -22
  73. package/dist/src/commands/patch/tier.js +15 -25
  74. package/dist/src/commands/patch-rename-map.d.ts +19 -0
  75. package/dist/src/commands/patch-rename-map.js +36 -0
  76. package/dist/src/commands/pending-resolution.d.ts +15 -0
  77. package/dist/src/commands/pending-resolution.js +25 -0
  78. package/dist/src/commands/re-export-adjacent.d.ts +24 -2
  79. package/dist/src/commands/re-export-adjacent.js +32 -16
  80. package/dist/src/commands/re-export-bulk-scan.js +6 -10
  81. package/dist/src/commands/re-export-drift.js +4 -4
  82. package/dist/src/commands/re-export-files.d.ts +2 -3
  83. package/dist/src/commands/re-export-files.js +38 -29
  84. package/dist/src/commands/re-export-lint.d.ts +3 -4
  85. package/dist/src/commands/re-export-lint.js +9 -2
  86. package/dist/src/commands/re-export-scan.js +5 -2
  87. package/dist/src/commands/re-export-single.d.ts +22 -3
  88. package/dist/src/commands/re-export-single.js +19 -11
  89. package/dist/src/commands/re-export.js +28 -11
  90. package/dist/src/commands/rebase/abort.js +3 -8
  91. package/dist/src/commands/rebase/continue.js +12 -12
  92. package/dist/src/commands/rebase/index.js +2 -2
  93. package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
  94. package/dist/src/commands/rebase/patch-loop.js +2 -7
  95. package/dist/src/commands/resolve.js +18 -10
  96. package/dist/src/commands/run.d.ts +0 -13
  97. package/dist/src/commands/run.js +12 -25
  98. package/dist/src/commands/setup-support.d.ts +16 -4
  99. package/dist/src/commands/setup-support.js +16 -16
  100. package/dist/src/commands/setup.js +3 -9
  101. package/dist/src/commands/source.js +8 -1
  102. package/dist/src/commands/status-check.js +2 -2
  103. package/dist/src/commands/status-output.js +2 -2
  104. package/dist/src/commands/status.js +3 -4
  105. package/dist/src/commands/test-build-phase.d.ts +2 -2
  106. package/dist/src/commands/test-build-phase.js +31 -7
  107. package/dist/src/commands/test-modes.d.ts +2 -2
  108. package/dist/src/commands/test-modes.js +0 -1
  109. package/dist/src/commands/test-register.js +5 -3
  110. package/dist/src/commands/test-run.js +25 -18
  111. package/dist/src/commands/test-stale-gate.js +4 -4
  112. package/dist/src/commands/test-verdict.d.ts +16 -2
  113. package/dist/src/commands/test-verdict.js +18 -3
  114. package/dist/src/commands/test.js +42 -5
  115. package/dist/src/commands/token-list.d.ts +26 -0
  116. package/dist/src/commands/token-list.js +153 -0
  117. package/dist/src/commands/token.js +17 -1
  118. package/dist/src/commands/tree.js +7 -2
  119. package/dist/src/commands/typecheck.js +9 -9
  120. package/dist/src/commands/wire.js +4 -3
  121. package/dist/src/core/branding.d.ts +0 -7
  122. package/dist/src/core/branding.js +3 -2
  123. package/dist/src/core/browser-wire.js +6 -3
  124. package/dist/src/core/build-audit-platform.d.ts +29 -0
  125. package/dist/src/core/build-audit-platform.js +140 -0
  126. package/dist/src/core/build-audit-registration.js +3 -2
  127. package/dist/src/core/build-audit-skip.d.ts +57 -0
  128. package/dist/src/core/build-audit-skip.js +112 -0
  129. package/dist/src/core/build-audit.d.ts +15 -2
  130. package/dist/src/core/build-audit.js +110 -10
  131. package/dist/src/core/build-baseline.d.ts +45 -28
  132. package/dist/src/core/build-baseline.js +6 -29
  133. package/dist/src/core/build-jar-escalation.js +3 -2
  134. package/dist/src/core/build-overwrite-guard.js +1 -1
  135. package/dist/src/core/build-prepare.d.ts +0 -8
  136. package/dist/src/core/build-prepare.js +7 -12
  137. package/dist/src/core/config-paths.d.ts +2 -6
  138. package/dist/src/core/config-paths.js +4 -2
  139. package/dist/src/core/config-state.d.ts +0 -6
  140. package/dist/src/core/config-state.js +1 -19
  141. package/dist/src/core/config-validate-build-audit.d.ts +8 -0
  142. package/dist/src/core/config-validate-build-audit.js +80 -0
  143. package/dist/src/core/config-validate-file-size.js +1 -1
  144. package/dist/src/core/config-validate-patch-policy.d.ts +2 -2
  145. package/dist/src/core/config-validate-patch-policy.js +3 -3
  146. package/dist/src/core/config-validate-test-toolchains.d.ts +2 -2
  147. package/dist/src/core/config-validate-test-toolchains.js +4 -5
  148. package/dist/src/core/config-validate.js +14 -3
  149. package/dist/src/core/config.d.ts +2 -2
  150. package/dist/src/core/config.js +11 -15
  151. package/dist/src/core/coverage-extend.js +4 -4
  152. package/dist/src/core/discard-baseline.js +7 -7
  153. package/dist/src/core/engine-precondition.d.ts +1 -9
  154. package/dist/src/core/engine-precondition.js +3 -8
  155. package/dist/src/core/engine-session-lock.js +13 -17
  156. package/dist/src/core/export-moved-code-shape.js +2 -2
  157. package/dist/src/core/file-lock-owner.d.ts +103 -0
  158. package/dist/src/core/file-lock-owner.js +259 -0
  159. package/dist/src/core/file-lock.d.ts +2 -7
  160. package/dist/src/core/file-lock.js +40 -91
  161. package/dist/src/core/firefox-cache.d.ts +13 -1
  162. package/dist/src/core/firefox-cache.js +59 -34
  163. package/dist/src/core/firefox-download.d.ts +15 -0
  164. package/dist/src/core/firefox-download.js +37 -21
  165. package/dist/src/core/firefox-extract.d.ts +6 -5
  166. package/dist/src/core/firefox-extract.js +36 -18
  167. package/dist/src/core/firefox.d.ts +30 -32
  168. package/dist/src/core/firefox.js +5 -36
  169. package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
  170. package/dist/src/core/furnace-apply-dry-run.js +1 -7
  171. package/dist/src/core/furnace-apply-helpers.d.ts +0 -11
  172. package/dist/src/core/furnace-apply-helpers.js +8 -19
  173. package/dist/src/core/furnace-apply-overwrite-warn.js +4 -2
  174. package/dist/src/core/furnace-apply.d.ts +1 -1
  175. package/dist/src/core/furnace-apply.js +30 -10
  176. package/dist/src/core/furnace-config.d.ts +1 -1
  177. package/dist/src/core/furnace-config.js +23 -36
  178. package/dist/src/core/furnace-constants.js +3 -2
  179. package/dist/src/core/furnace-dir-entry.d.ts +20 -0
  180. package/dist/src/core/furnace-dir-entry.js +21 -0
  181. package/dist/src/core/furnace-operation.d.ts +2 -0
  182. package/dist/src/core/furnace-operation.js +3 -1
  183. package/dist/src/core/furnace-refresh.d.ts +18 -6
  184. package/dist/src/core/furnace-refresh.js +3 -6
  185. package/dist/src/core/furnace-registration-ast.d.ts +0 -1
  186. package/dist/src/core/furnace-registration-ast.js +0 -3
  187. package/dist/src/core/furnace-registration.d.ts +2 -1
  188. package/dist/src/core/furnace-registration.js +22 -29
  189. package/dist/src/core/furnace-scanner.js +3 -6
  190. package/dist/src/core/furnace-state-persist.d.ts +2 -2
  191. package/dist/src/core/furnace-validate-registration.js +5 -7
  192. package/dist/src/core/git-base.d.ts +3 -1
  193. package/dist/src/core/git-base.js +25 -3
  194. package/dist/src/core/git-diff-new-file.d.ts +40 -0
  195. package/dist/src/core/git-diff-new-file.js +119 -0
  196. package/dist/src/core/git-diff.d.ts +9 -9
  197. package/dist/src/core/git-diff.js +88 -157
  198. package/dist/src/core/git-file-ops.d.ts +0 -7
  199. package/dist/src/core/git-file-ops.js +4 -28
  200. package/dist/src/core/git-readonly-index.d.ts +0 -2
  201. package/dist/src/core/git-readonly-index.js +0 -4
  202. package/dist/src/core/git-status.d.ts +7 -0
  203. package/dist/src/core/git-status.js +21 -9
  204. package/dist/src/core/git.d.ts +0 -6
  205. package/dist/src/core/git.js +14 -43
  206. package/dist/src/core/harness-orphans.js +8 -7
  207. package/dist/src/core/history-log.d.ts +19 -0
  208. package/dist/src/core/history-log.js +29 -0
  209. package/dist/src/core/mach-error-hints.d.ts +0 -5
  210. package/dist/src/core/mach-error-hints.js +1 -1
  211. package/dist/src/core/mach-known-noise-filter.d.ts +4 -3
  212. package/dist/src/core/mach-known-noise-filter.js +7 -9
  213. package/dist/src/core/mach-mozconfig.js +2 -1
  214. package/dist/src/core/mach-python.d.ts +2 -0
  215. package/dist/src/core/mach-python.js +12 -7
  216. package/dist/src/core/mach-resource-shim.d.ts +13 -1
  217. package/dist/src/core/mach-resource-shim.js +112 -9
  218. package/dist/src/core/mach.d.ts +39 -53
  219. package/dist/src/core/mach.js +13 -81
  220. package/dist/src/core/marionette-port.d.ts +0 -17
  221. package/dist/src/core/marionette-port.js +12 -58
  222. package/dist/src/core/marionette-preflight.js +9 -27
  223. package/dist/src/core/mochitest-server-port.js +3 -3
  224. package/dist/src/core/moz-manifest-rules.js +37 -33
  225. package/dist/src/core/patch-apply-fuzz.js +4 -4
  226. package/dist/src/core/patch-apply.d.ts +4 -5
  227. package/dist/src/core/patch-apply.js +6 -12
  228. package/dist/src/core/patch-export-coverage.d.ts +0 -9
  229. package/dist/src/core/patch-export-coverage.js +1 -30
  230. package/dist/src/core/patch-export-update.d.ts +18 -1
  231. package/dist/src/core/patch-export-update.js +4 -3
  232. package/dist/src/core/patch-export.d.ts +11 -21
  233. package/dist/src/core/patch-export.js +44 -66
  234. package/dist/src/core/patch-files.d.ts +0 -4
  235. package/dist/src/core/patch-files.js +0 -11
  236. package/dist/src/core/patch-lint-binary.d.ts +6 -12
  237. package/dist/src/core/patch-lint-cache.d.ts +17 -1
  238. package/dist/src/core/patch-lint-cache.js +3 -6
  239. package/dist/src/core/patch-lint-checkjs.d.ts +33 -16
  240. package/dist/src/core/patch-lint-checkjs.js +37 -24
  241. package/dist/src/core/patch-lint-creators.d.ts +2 -6
  242. package/dist/src/core/patch-lint-cross.d.ts +16 -8
  243. package/dist/src/core/patch-lint-cross.js +49 -92
  244. package/dist/src/core/patch-lint-css.js +10 -6
  245. package/dist/src/core/patch-lint-file-size.js +3 -3
  246. package/dist/src/core/patch-lint-forward-registration.d.ts +58 -0
  247. package/dist/src/core/patch-lint-forward-registration.js +210 -0
  248. package/dist/src/core/patch-lint-jsdoc.js +36 -36
  249. package/dist/src/core/patch-lint-module-registration.d.ts +2 -8
  250. package/dist/src/core/patch-lint-prettier.js +1 -1
  251. package/dist/src/core/patch-lint-queue-types.d.ts +60 -0
  252. package/dist/src/core/patch-lint-queue-types.js +12 -0
  253. package/dist/src/core/patch-lint-staged-registration.d.ts +61 -0
  254. package/dist/src/core/patch-lint-staged-registration.js +90 -0
  255. package/dist/src/core/patch-lint.d.ts +14 -9
  256. package/dist/src/core/patch-lint.js +4 -3
  257. package/dist/src/core/patch-lock.js +3 -3
  258. package/dist/src/core/patch-manifest-io.js +8 -7
  259. package/dist/src/core/patch-parse.d.ts +19 -8
  260. package/dist/src/core/patch-parse.js +25 -0
  261. package/dist/src/core/patch-policy.js +15 -27
  262. package/dist/src/core/patch-source-metadata.d.ts +1 -3
  263. package/dist/src/core/patch-source-metadata.js +0 -4
  264. package/dist/src/core/patch-transform.d.ts +0 -8
  265. package/dist/src/core/patch-transform.js +0 -10
  266. package/dist/src/core/process-cpu-time.js +1 -1
  267. package/dist/src/core/rebase-session.d.ts +2 -8
  268. package/dist/src/core/rebase-session.js +5 -17
  269. package/dist/src/core/register-browser-content.js +3 -2
  270. package/dist/src/core/register-browser-test.js +2 -1
  271. package/dist/src/core/register-module.js +2 -1
  272. package/dist/src/core/register-scaffold.js +5 -4
  273. package/dist/src/core/register-shared-css.js +2 -1
  274. package/dist/src/core/run-log-redact.d.ts +38 -0
  275. package/dist/src/core/run-log-redact.js +69 -0
  276. package/dist/src/core/run-log.d.ts +13 -2
  277. package/dist/src/core/run-log.js +67 -18
  278. package/dist/src/core/signal-critical.js +2 -4
  279. package/dist/src/core/smoke-patterns.d.ts +0 -1
  280. package/dist/src/core/smoke-patterns.js +6 -11
  281. package/dist/src/core/state-file.d.ts +12 -1
  282. package/dist/src/core/state-file.js +14 -1
  283. package/dist/src/core/status-classify.js +2 -1
  284. package/dist/src/core/test-changed-prefs.js +26 -11
  285. package/dist/src/core/test-harness-crash.js +2 -2
  286. package/dist/src/core/test-stale-check.d.ts +0 -2
  287. package/dist/src/core/test-stale-check.js +2 -2
  288. package/dist/src/core/test-xpcshell-retry.d.ts +26 -7
  289. package/dist/src/core/test-xpcshell-retry.js +14 -11
  290. package/dist/src/core/token-category.d.ts +33 -0
  291. package/dist/src/core/token-category.js +56 -48
  292. package/dist/src/core/token-dark-mode.js +2 -14
  293. package/dist/src/core/token-inventory.d.ts +56 -0
  294. package/dist/src/core/token-inventory.js +109 -0
  295. package/dist/src/core/token-manager.d.ts +1 -1
  296. package/dist/src/core/token-manager.js +1 -1
  297. package/dist/src/core/token-variant.js +21 -22
  298. package/dist/src/core/toolchain-preflight.js +1 -1
  299. package/dist/src/core/tree-cow.d.ts +0 -2
  300. package/dist/src/core/tree-cow.js +2 -2
  301. package/dist/src/core/tree-store.js +4 -5
  302. package/dist/src/core/typecheck.js +2 -2
  303. package/dist/src/core/wire-dom-fragment.js +6 -17
  304. package/dist/src/core/wire-utils.js +5 -17
  305. package/dist/src/core/xpcshell-appdir.js +2 -4
  306. package/dist/src/errors/base.d.ts +53 -1
  307. package/dist/src/errors/base.js +79 -11
  308. package/dist/src/errors/build.js +20 -28
  309. package/dist/src/errors/config.d.ts +0 -14
  310. package/dist/src/errors/config.js +13 -34
  311. package/dist/src/errors/download.d.ts +13 -0
  312. package/dist/src/errors/download.js +53 -24
  313. package/dist/src/errors/furnace.js +8 -9
  314. package/dist/src/errors/git.d.ts +8 -8
  315. package/dist/src/errors/git.js +20 -30
  316. package/dist/src/errors/patch.js +8 -10
  317. package/dist/src/errors/rebase.js +7 -7
  318. package/dist/src/types/commands/options.d.ts +77 -123
  319. package/dist/src/types/config.d.ts +34 -2
  320. package/dist/src/types/index.d.ts +0 -1
  321. package/dist/src/utils/assert.d.ts +3 -15
  322. package/dist/src/utils/assert.js +1 -18
  323. package/dist/src/utils/build-info.d.ts +1 -1
  324. package/dist/src/utils/child-env.d.ts +2 -1
  325. package/dist/src/utils/elapsed.js +10 -1
  326. package/dist/src/utils/fs.js +8 -13
  327. package/dist/src/utils/hash.d.ts +12 -0
  328. package/dist/src/utils/hash.js +16 -0
  329. package/dist/src/utils/line-dispatch.d.ts +15 -0
  330. package/dist/src/utils/line-dispatch.js +48 -0
  331. package/dist/src/utils/options.d.ts +1 -7
  332. package/dist/src/utils/options.js +6 -7
  333. package/dist/src/utils/package-root.js +3 -12
  334. package/dist/src/utils/platform.d.ts +0 -4
  335. package/dist/src/utils/process-group.d.ts +2 -1
  336. package/dist/src/utils/process-group.js +20 -19
  337. package/dist/src/utils/process.d.ts +40 -26
  338. package/dist/src/utils/process.js +115 -166
  339. package/dist/src/utils/ps-duration.js +3 -3
  340. package/dist/src/utils/sleep.d.ts +26 -0
  341. package/dist/src/utils/sleep.js +31 -0
  342. package/dist/src/utils/validation.d.ts +2 -38
  343. package/dist/src/utils/validation.js +7 -16
  344. package/docs/configuration.md +89 -1
  345. package/docs/exit-codes.md +31 -19
  346. package/docs/lifecycle-invariants.md +18 -12
  347. package/docs/machine-output.md +31 -8
  348. package/docs/patch-workflow.md +2 -1
  349. package/docs/run-logs.md +44 -4
  350. package/docs/testing.md +8 -1
  351. package/package.json +8 -7
  352. package/dist/src/commands/rebase.d.ts +0 -4
  353. package/dist/src/commands/rebase.js +0 -5
  354. package/dist/src/core/patch-artifact-normalize.d.ts +0 -21
  355. package/dist/src/core/patch-artifact-normalize.js +0 -24
  356. package/dist/src/core/patch-lint-reexports.d.ts +0 -7
  357. package/dist/src/core/patch-lint-reexports.js +0 -8
  358. package/dist/src/core/wire-targets.d.ts +0 -7
  359. package/dist/src/core/wire-targets.js +0 -8
@@ -88,7 +88,7 @@ async function runPostRebaseAudit(ctx) {
88
88
  }
89
89
  const browserTomls = await collectBrowserTomlFiles(engineDir);
90
90
  if (browserTomls.unreadable.length > 0) {
91
- issues.push(`could not read ${String(browserTomls.unreadable.length)} ` +
91
+ issues.push(`could not read ${browserTomls.unreadable.length} ` +
92
92
  `${browserTomls.unreadable.length === 1 ? 'directory' : 'directories'} under ` +
93
93
  `browser/base/content/test: ${browserTomls.unreadable.join(', ')}`);
94
94
  }
@@ -21,6 +21,8 @@ export declare function validateCheckDependencies(checks: readonly DoctorCheckDe
21
21
  * context-population dependency chain (see {@link DOCTOR_CHECKS}) must
22
22
  * also update this list, which gives us a single place to notice and
23
23
  * think through the consequences.
24
+ *
25
+ * @internal Exported only so tests can reach it; not part of the public surface.
24
26
  */
25
27
  export declare const DOCTOR_CHECK_ORDER: readonly string[];
26
28
  /**
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { Command } from 'commander';
3
- import { configExists, getProjectPaths, loadConfig, loadState, updateState, } from '../core/config.js';
3
+ import { configExists, getProjectPaths, loadConfig, loadState } from '../core/config.js';
4
4
  import { furnaceConfigExists as checkFurnaceConfigExists } from '../core/furnace-config.js';
5
5
  import { getCurrentBranch, getHead, isGitRepository, isMissingHeadError } from '../core/git.js';
6
6
  import { ensureGit } from '../core/git-base.js';
@@ -14,7 +14,7 @@ import { pathExists } from '../utils/fs.js';
14
14
  import { error, info, intro, outro, success, warn } from '../utils/logger.js';
15
15
  import { addWaitLockOption } from '../utils/options.js';
16
16
  import { findExecutable } from '../utils/process.js';
17
- import { failure, ok, resolveDoctorSeverity, warning } from './doctor-check-core.js';
17
+ import { failure, ok, warning } from './doctor-check-core.js';
18
18
  import { EXTERNAL_TOOLCHAIN_DOCTOR_CHECK } from './doctor-external-toolchains.js';
19
19
  import { FURNACE_DOCTOR_CHECKS } from './doctor-furnace.js';
20
20
  import { ORPHANED_HARNESS_DOCTOR_CHECK } from './doctor-orphaned-harness.js';
@@ -22,6 +22,7 @@ import { PATCH_MANIFEST_CONSISTENCY_CHECK } from './doctor-patch-manifest.js';
22
22
  import { POST_REBASE_AUDIT_CHECK } from './doctor-post-rebase-audit.js';
23
23
  import { SOURCE_PIN_DOCTOR_CHECK } from './doctor-source-pin.js';
24
24
  import { inspectEngineWorkingTree } from './doctor-working-tree.js';
25
+ import { clearPendingResolution } from './pending-resolution.js';
25
26
  import { collectPatchQueueHealth } from './verify.js';
26
27
  /**
27
28
  * Runs a single check definition, converting thrown errors into
@@ -202,11 +203,7 @@ const DOCTOR_CHECKS = [
202
203
  if (health.errorCount > 0) {
203
204
  return failure('Pending Resolution', `Refusing to clear pending resolution for ${patchFilename}: patch queue health check found ${health.errorCount} error(s).`, 'Run "fireforge verify" for details, fix the queue, then retry "fireforge doctor --clear-resolution".');
204
205
  }
205
- await updateState(ctx.projectRoot, (current) => {
206
- const next = { ...current };
207
- delete next.pendingResolution;
208
- return next;
209
- });
206
+ await clearPendingResolution(ctx.projectRoot);
210
207
  return ok('Pending Resolution');
211
208
  }
212
209
  return failure('Pending Resolution', `You are currently resolving a conflict for patch ${patchFilename}.`, 'Build and Export commands may behave unexpectedly until "fireforge resolve" is completed. If the queue now verifies cleanly, run "fireforge doctor --clear-resolution" to discard the stale marker.');
@@ -322,6 +319,8 @@ validateCheckDependencies(DOCTOR_CHECKS);
322
319
  * context-population dependency chain (see {@link DOCTOR_CHECKS}) must
323
320
  * also update this list, which gives us a single place to notice and
324
321
  * think through the consequences.
322
+ *
323
+ * @internal Exported only so tests can reach it; not part of the public surface.
325
324
  */
326
325
  export const DOCTOR_CHECK_ORDER = DOCTOR_CHECKS.map((check) => check.name);
327
326
  /**
@@ -336,7 +335,7 @@ export function reportDoctorResults(checks, mutations = []) {
336
335
  let warningCount = 0;
337
336
  let failedCount = 0;
338
337
  for (const check of checks) {
339
- const severity = resolveDoctorSeverity(check);
338
+ const severity = check.severity;
340
339
  if (severity === 'warning') {
341
340
  warn(`! ${check.name}: ${check.message}`);
342
341
  if (check.fix) {
@@ -110,11 +110,6 @@ function closeRestoreSpinner(restoreSpinner, result) {
110
110
  }
111
111
  restoreSpinner.stop('Patch-touched files restored');
112
112
  }
113
- async function clearStaleFurnaceApplyState(projectRoot) {
114
- // --force installs a new baseCommit, which invalidates every applied
115
- // checksum in furnace-state.json.
116
- await clearAppliedFurnaceState(projectRoot);
117
- }
118
113
  async function activateReplacementEngine(args) {
119
114
  const { engineDir, replacementDir, backupDir } = args;
120
115
  await rename(engineDir, backupDir);
@@ -155,28 +150,39 @@ async function restorePreviousEngine(args) {
155
150
  }
156
151
  }
157
152
  async function downloadAndExtractFirefox(args) {
158
- const { version, product, engineDir, cacheDir, sha256, candidate } = args;
153
+ const { version, product, engineDir, cacheDir, sha256, candidate, allowUnverifiedDownload } = args;
159
154
  let s = spinner(`Downloading Firefox ${version}...`);
160
155
  let lastPercent = 0;
161
156
  const phaseState = { value: 'download' };
162
157
  try {
163
- await downloadFirefoxSource(version, product, engineDir, cacheDir, (downloaded, total) => {
164
- if (total <= 0)
165
- return;
166
- const percent = Math.floor((downloaded / total) * 100);
167
- if (percent !== lastPercent && percent % 5 === 0) {
168
- s.message(`Downloading Firefox ${version}... ${percent}% (${formatBytes(downloaded)} / ${formatBytes(total)})`);
169
- lastPercent = percent;
170
- }
171
- }, (phase) => {
172
- if (phase === 'extract' && phaseState.value === 'download') {
173
- s.stop(`Firefox ${version} downloaded`);
174
- phaseState.value = 'extract';
175
- s = spinner(`Extracting Firefox ${version}... (decompressing ~600 MB of source; typically 30–90s)`);
176
- }
177
- }, sha256, (message) => {
178
- s.message(message);
179
- }, candidate);
158
+ await downloadFirefoxSource({
159
+ version: version,
160
+ product: product,
161
+ destDir: engineDir,
162
+ cacheDir: cacheDir,
163
+ onProgress: (downloaded, total) => {
164
+ if (total <= 0)
165
+ return;
166
+ const percent = Math.floor((downloaded / total) * 100);
167
+ if (percent !== lastPercent && percent % 5 === 0) {
168
+ s.message(`Downloading Firefox ${version}... ${percent}% (${formatBytes(downloaded)} / ${formatBytes(total)})`);
169
+ lastPercent = percent;
170
+ }
171
+ },
172
+ onPhase: (phase) => {
173
+ if (phase === 'extract' && phaseState.value === 'download') {
174
+ s.stop(`Firefox ${version} downloaded`);
175
+ phaseState.value = 'extract';
176
+ s = spinner(`Extracting Firefox ${version}... (decompressing ~600 MB of source; typically 30–90s)`);
177
+ }
178
+ },
179
+ expectedSha256: sha256,
180
+ onPhaseProgress: (message) => {
181
+ s.message(message);
182
+ },
183
+ candidate: candidate,
184
+ integrity: allowUnverifiedDownload === undefined ? undefined : { allowUnverifiedDownload },
185
+ });
180
186
  s.stop(phaseState.value === 'extract'
181
187
  ? `Firefox ${version} extracted`
182
188
  : `Firefox ${version} downloaded`);
@@ -235,7 +241,9 @@ async function initializeDownloadedEngine(args) {
235
241
  throw error;
236
242
  }
237
243
  if (replacementActivated) {
238
- await clearStaleFurnaceApplyState(projectRoot);
244
+ // --force installs a new baseCommit, which invalidates every applied
245
+ // checksum in furnace-state.json.
246
+ await clearAppliedFurnaceState(projectRoot);
239
247
  }
240
248
  await updateState(projectRoot, {
241
249
  downloadedVersion: version,
@@ -389,6 +397,9 @@ export async function downloadCommand(projectRoot, options) {
389
397
  ...(config.firefox.candidate !== undefined
390
398
  ? { candidate: config.firefox.candidate }
391
399
  : {}),
400
+ ...(config.firefox.allowUnverifiedDownload !== undefined
401
+ ? { allowUnverifiedDownload: config.firefox.allowUnverifiedDownload }
402
+ : {}),
392
403
  });
393
404
  if (replacementEngineDir && backupEngineDir) {
394
405
  warn('Activating replacement engine directory...');
@@ -19,10 +19,20 @@ import { info, intro, outro, spinner } from '../utils/logger.js';
19
19
  import { addWaitLockOption, pickDefined, resolveWaitLockSeconds } from '../utils/options.js';
20
20
  import { renderDryRunPreview } from './export-flow.js';
21
21
  import { autoFixLicenseHeaders, promptExportPatchMetadata, runPatchLint, runSupersedeAndOverlapGates, } from './export-shared.js';
22
+ /**
23
+ * The engine paths an export-all entry owns: its current file plus, for a
24
+ * rename, the path it came from. Spelled once because three call sites had
25
+ * copied the same `filter((value): value is string => !!value)` narrowing.
26
+ * @param entry - Export entry carrying `file` and an optional `originalPath`.
27
+ * @returns Both paths when the entry was renamed, otherwise just `file`.
28
+ */
29
+ function ownedPaths(entry) {
30
+ return [entry.file, entry.originalPath].filter((value) => !!value);
31
+ }
22
32
  async function checkBrandingManagedFiles(paths, config) {
23
33
  const changedFiles = await getWorkingTreeStatus(paths.engine);
24
34
  const brandingManagedFiles = changedFiles
25
- .flatMap((entry) => [entry.file, entry.originalPath].filter((value) => !!value))
35
+ .flatMap((entry) => ownedPaths(entry))
26
36
  .filter((file) => isBrandingManagedPath(file, config.binaryName));
27
37
  if (brandingManagedFiles.length > 0) {
28
38
  throw new GeneralError('Export-all refuses to capture tool-managed branding changes by default.\n\n' +
@@ -52,7 +62,7 @@ async function resolveFurnaceExclusionPolicy(paths, projectRoot, excludeFurnace)
52
62
  const rawStatus = await getWorkingTreeStatus(paths.engine);
53
63
  const changedFiles = await expandUntrackedDirectoryEntries(paths.engine, rawStatus);
54
64
  const furnaceManagedFiles = changedFiles
55
- .flatMap((entry) => [entry.file, entry.originalPath].filter((value) => !!value))
65
+ .flatMap((entry) => ownedPaths(entry))
56
66
  .filter((file) => [...prefixes].some((prefix) => file.startsWith(prefix)));
57
67
  if (furnaceManagedFiles.length === 0)
58
68
  return new Set();
@@ -196,7 +206,7 @@ export async function exportAllCommand(projectRoot, options = {}) {
196
206
  const allChanged = await expandUntrackedDirectoryEntries(paths.engine, rawChanged);
197
207
  const nonFurnacePaths = [
198
208
  ...new Set(allChanged
199
- .flatMap((entry) => [entry.file, entry.originalPath].filter((value) => !!value))
209
+ .flatMap((entry) => ownedPaths(entry))
200
210
  .filter((file) => !furnaceExcluded.has(file))),
201
211
  ].sort();
202
212
  scopedExportPaths = nonFurnacePaths;
@@ -254,7 +264,14 @@ export async function exportAllCommand(projectRoot, options = {}) {
254
264
  const patchQueueCtx = (await pathExists(paths.patches))
255
265
  ? await buildPatchQueueContext(paths.patches)
256
266
  : undefined;
257
- await runPatchLint(paths.engine, filesAffected, diff, config, options.skipLint, patchQueueCtx);
267
+ await runPatchLint({
268
+ engineDir: paths.engine,
269
+ filesAffected,
270
+ diffContent: diff,
271
+ config,
272
+ skipLint: options.skipLint,
273
+ patchQueueCtx,
274
+ });
258
275
  // Dry-run: enumerate filename, metadata, and supersede coverage without
259
276
  // writing. Mirrors `fireforge export --dry-run` so the same preview
260
277
  // surface is available for both targeted and aggregate exports. Runs
@@ -10,14 +10,12 @@
10
10
  import { join } from 'node:path';
11
11
  import {} from '../core/destructive.js';
12
12
  import { detectMovedCodeOverlaps, formatAdoptThenSplitRemedy, } from '../core/export-moved-code-shape.js';
13
- import { normalizePatchArtifact } from '../core/patch-artifact-normalize.js';
14
- import { findAllPatchesForFilesWithDetails, patchNameSlug, planExport, } from '../core/patch-export.js';
15
- import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, lintPatchQueue, } from '../core/patch-lint.js';
13
+ import { buildPlanExportInput, findAllPatchesForFilesWithDetails, patchNameSlug, planExport, } from '../core/patch-export.js';
14
+ import { buildPatchQueueContext, lintPatchQueue } from '../core/patch-lint.js';
16
15
  import { withPatchDirectoryLock } from '../core/patch-lock.js';
17
16
  import { addPatchToManifest, loadPatchesManifest, loadPatchesManifestForWrite, renumberPatchesInManifest, resolvePatchIdentifier, savePatchesManifest, } from '../core/patch-manifest.js';
18
- import { requirePatchOrder } from '../core/patch-parse.js';
17
+ import { formatPatchOrder, requirePatchOrder } from '../core/patch-parse.js';
19
18
  import { applyRenameMapToManifest, buildProjectedManifest, enforcePatchPolicy, } from '../core/patch-policy.js';
20
- import { buildNewFileTextProjection } from '../core/patch-transform.js';
21
19
  import { GeneralError, InvalidArgumentError } from '../errors/base.js';
22
20
  import { toError } from '../utils/errors.js';
23
21
  import { pathExists, readText, removeFile, writeText } from '../utils/fs.js';
@@ -25,6 +23,8 @@ import { info, warn } from '../utils/logger.js';
25
23
  import { groupProjectedPlacementErrors } from './export-placement-conflicts.js';
26
24
  import { assertPlacementAvoidsReservedRanges } from './export-placement-policy.js';
27
25
  import { findPartialOwnershipOverlap } from './export-shared.js';
26
+ import { projectEntryBody } from './patch/entry-projection.js';
27
+ import { getSortedRenameEntries, renameMapsEqual } from './patch-rename-map.js';
28
28
  function buildFilenameForPlacement(category, name, order, width) {
29
29
  const padded = String(order).padStart(Math.max(3, width), '0');
30
30
  return `${padded}-${category}-${patchNameSlug(name, category)}.patch`;
@@ -35,9 +35,6 @@ function prefixWidthForPatches(manifestPatches, requestedOrder) {
35
35
  return Math.max(width, match?.[1]?.length ?? 3, String(requestedOrder).length);
36
36
  }, 3);
37
37
  }
38
- function getSortedRenameEntries(renameMap) {
39
- return Array.from(renameMap.entries()).sort((a, b) => a[1].newOrder - b[1].newOrder);
40
- }
41
38
  /**
42
39
  * Structural equality for placement plans — used by placement-mode export
43
40
  * and `patch split` to verify the queue did not change between the
@@ -47,20 +44,7 @@ export function placementPlansEqual(left, right) {
47
44
  if (left.insertionOrder !== right.insertionOrder || left.newFilename !== right.newFilename) {
48
45
  return false;
49
46
  }
50
- const leftEntries = getSortedRenameEntries(left.renameMap);
51
- const rightEntries = getSortedRenameEntries(right.renameMap);
52
- if (leftEntries.length !== rightEntries.length) {
53
- return false;
54
- }
55
- return leftEntries.every(([leftFilename, leftEntry], index) => {
56
- const rightTuple = rightEntries[index];
57
- if (!rightTuple)
58
- return false;
59
- const [rightFilename, rightEntry] = rightTuple;
60
- return (leftFilename === rightFilename &&
61
- leftEntry.newFilename === rightEntry.newFilename &&
62
- leftEntry.newOrder === rightEntry.newOrder);
63
- });
47
+ return renameMapsEqual(left.renameMap, right.renameMap);
64
48
  }
65
49
  /**
66
50
  * Computes the shift map that moves existing patches out of the requested
@@ -72,7 +56,7 @@ export function computePlacementPlan(manifestPatches, newPatchCategory, newPatch
72
56
  // Failing fast here prevents a NaN requestedOrder from producing a
73
57
  // filename like "NaN-ui-foo.patch".
74
58
  if (!Number.isInteger(requestedOrder) || requestedOrder <= 0) {
75
- throw new InvalidArgumentError(`computePlacementPlan requires a positive integer order, got ${String(requestedOrder)}.`, 'requestedOrder');
59
+ throw new InvalidArgumentError(`computePlacementPlan requires a positive integer order, got ${requestedOrder}.`, 'requestedOrder');
76
60
  }
77
61
  const sorted = [...manifestPatches].sort((a, b) => a.order - b.order);
78
62
  const renameMap = new Map();
@@ -90,7 +74,7 @@ export function computePlacementPlan(manifestPatches, newPatchCategory, newPatch
90
74
  }
91
75
  const newOrder = cursor + 1;
92
76
  const currentRest = patch.filename.replace(/^\d+-/, '');
93
- const newFilename = `${String(newOrder).padStart(prefixWidth, '0')}-${currentRest}`;
77
+ const newFilename = `${formatPatchOrder(newOrder, prefixWidth)}-${currentRest}`;
94
78
  renameMap.set(patch.filename, { newOrder, newFilename });
95
79
  cursor = newOrder;
96
80
  }
@@ -109,11 +93,11 @@ export function computePlacementPlan(manifestPatches, newPatchCategory, newPatch
109
93
  */
110
94
  export function computeExactPlacementPlan(manifestPatches, newPatchCategory, newPatchName, requestedOrder) {
111
95
  if (!Number.isInteger(requestedOrder) || requestedOrder <= 0) {
112
- throw new InvalidArgumentError(`--order must be a positive integer, got ${String(requestedOrder)}.`, '--order');
96
+ throw new InvalidArgumentError(`--order must be a positive integer, got ${requestedOrder}.`, '--order');
113
97
  }
114
98
  const occupied = manifestPatches.find((patch) => patch.order === requestedOrder);
115
99
  if (occupied) {
116
- throw new InvalidArgumentError(`--order ${String(requestedOrder)} is already occupied by ${occupied.filename}. ` +
100
+ throw new InvalidArgumentError(`--order ${requestedOrder} is already occupied by ${occupied.filename}. ` +
117
101
  'Choose an unused order or use --before/--after for positional insertion.', '--order');
118
102
  }
119
103
  return {
@@ -140,7 +124,7 @@ export async function resolvePlacementPlan(patchesDir, options, category, name,
140
124
  // function is called directly from the command body which could
141
125
  // reach here with a NaN/0/negative value passed in via test harness.
142
126
  if (!Number.isInteger(options.order) || options.order <= 0) {
143
- throw new InvalidArgumentError(`--order must be a positive integer, got ${String(options.order)}.`, '--order');
127
+ throw new InvalidArgumentError(`--order must be a positive integer, got ${options.order}.`, '--order');
144
128
  }
145
129
  return computeExactPlacementPlan(existingPatches, category, name, options.order);
146
130
  }
@@ -190,9 +174,7 @@ export async function projectPlacementForLint(patchesDir, plan, diff, config) {
190
174
  filename: plan.newFilename,
191
175
  order: plan.insertionOrder,
192
176
  metadata: null,
193
- diff,
194
- newFiles: buildNewFileTextProjection(diff),
195
- modifiedFileAdditions: buildModifiedFileAdditionsFromDiff(diff),
177
+ ...projectEntryBody(diff),
196
178
  });
197
179
  projectedEntries.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
198
180
  const policyRest = baseCtx.patchPolicy ? { patchPolicy: baseCtx.patchPolicy } : {};
@@ -294,7 +276,7 @@ export async function commitPlacementExport(input) {
294
276
  }
295
277
  // Normalize identically to commitExportedPatch — the two export
296
278
  // paths must produce one artifact contract for the same diff.
297
- await writeText(patchPath, normalizePatchArtifact(input.diff));
279
+ await writeText(patchPath, input.diff);
298
280
  await addPatchToManifest(input.patchesDir, {
299
281
  ...input.metadata,
300
282
  filename: currentPlan.newFilename,
@@ -380,19 +362,7 @@ export async function renderDryRunPreview(input) {
380
362
  const overlap = manifest !== null
381
363
  ? findPartialOwnershipOverlap(manifest, input.filesAffected, supersedingFilenames)
382
364
  : new Map();
383
- const plan = await planExport({
384
- patchesDir: input.patchesDir,
385
- category: input.category,
386
- name: input.name,
387
- description: input.description,
388
- filesAffected: input.filesAffected,
389
- sourceEsrVersion: input.sourceEsrVersion,
390
- ...(input.sourceProduct !== undefined ? { sourceProduct: input.sourceProduct } : {}),
391
- ...(input.sourceVersion !== undefined ? { sourceVersion: input.sourceVersion } : {}),
392
- ...(input.tier !== undefined ? { tier: input.tier } : {}),
393
- ...(input.lintIgnore !== undefined ? { lintIgnore: input.lintIgnore } : {}),
394
- ...(input.config !== undefined ? { config: input.config } : {}),
395
- });
365
+ const plan = await planExport(buildPlanExportInput(input));
396
366
  if (input.config !== undefined) {
397
367
  enforcePatchPolicy({
398
368
  config: input.config,
@@ -426,7 +396,7 @@ export async function renderDryRunPreview(input) {
426
396
  }
427
397
  if (overlap.size > 0) {
428
398
  const entries = [...overlap.entries()].sort(([a], [b]) => a.localeCompare(b));
429
- warn(`\n[dry-run] Would create cross-patch ownership overlap on ${String(entries.length)} file${entries.length === 1 ? '' : 's'}:`);
399
+ warn(`\n[dry-run] Would create cross-patch ownership overlap on ${entries.length} file${entries.length === 1 ? '' : 's'}:`);
430
400
  for (const [file, owners] of entries) {
431
401
  warn(` - ${file} already claimed by: ${owners.join(', ')}`);
432
402
  }
@@ -66,10 +66,10 @@ export function groupProjectedPlacementErrors(projectedErrors, baselineErrors, p
66
66
  details.push(` ${formatPatchLintIssue(issue)}`);
67
67
  }
68
68
  };
69
- pushGroup(`errors in the exported patch content (${String(exported.length)}):`, exported);
69
+ pushGroup(`errors in the exported patch content (${exported.length}):`, exported);
70
70
  pushGroup(`consequences of renumbering existing patches to make room at ordinal ` +
71
- `${String(plan.insertionOrder)} (${String(renumbering.length)}) — these come from ` +
71
+ `${plan.insertionOrder} (${renumbering.length}) — these come from ` +
72
72
  'renumbering, NOT from the exported content:', renumbering);
73
- pushGroup(`errors already present in the queue before this export (${String(preExisting.length)}):`, preExisting);
73
+ pushGroup(`errors already present in the queue before this export (${preExisting.length}):`, preExisting);
74
74
  return details;
75
75
  }
@@ -10,7 +10,7 @@ import { loadPatchesManifest } from '../core/patch-manifest.js';
10
10
  import { applyRenameMapToManifest, buildProjectedManifest, enforcePatchPolicy, } from '../core/patch-policy.js';
11
11
  import { buildPatchSourceMetadata } from '../core/patch-source-metadata.js';
12
12
  import { InvalidArgumentError } from '../errors/base.js';
13
- import { outro } from '../utils/logger.js';
13
+ import { proceedAfterDecision } from './destructive-decision.js';
14
14
  import { placementSummary, projectPlacementForLint, resolvePlacementPlan, } from './export-flow.js';
15
15
  /**
16
16
  * Spreadable optional metadata (`tier`, `lintIgnore`) derived from the
@@ -85,14 +85,8 @@ export async function gatePlacementPlan(args) {
85
85
  unsafeOverride: options.forceUnsafe === true,
86
86
  conflicts,
87
87
  });
88
- if (decision === 'dry-run') {
89
- outro('Dry run complete — no changes made');
88
+ if (!proceedAfterDecision(decision, 'Export cancelled'))
90
89
  return 'stop';
91
- }
92
- if (decision === 'declined') {
93
- outro('Export cancelled');
94
- return 'stop';
95
- }
96
90
  }
97
91
  else if (conflicts && options.forceUnsafe !== true) {
98
92
  s.stop();
@@ -2,10 +2,8 @@
2
2
  /**
3
3
  * Policy-aware checks for export placement plans.
4
4
  */
5
+ import { formatOrderRange, formatPatchOrder } from '../core/patch-parse.js';
5
6
  import { InvalidArgumentError } from '../errors/base.js';
6
- function reservedRangeLabel(range) {
7
- return `${String(range.from).padStart(3, '0')}-${String(range.to).padStart(3, '0')}`;
8
- }
9
7
  function findReservedRange(config, order) {
10
8
  return (config.patchPolicy?.reservedRanges?.find((range) => order >= range.from && order <= range.to) ??
11
9
  null);
@@ -42,11 +40,11 @@ export function assertPlacementAvoidsReservedRanges(plan, manifestPatches, confi
42
40
  (oldOrder !== undefined ? findReservedRange(config, oldOrder) : null);
43
41
  if (reserved === null)
44
42
  continue;
45
- const label = reservedRangeLabel(reserved);
43
+ const label = formatOrderRange(reserved);
46
44
  const freeOrder = firstFreeOrderBelowReservedRange(manifestPatches, reserved);
47
45
  if (freeOrder !== null) {
48
46
  throw new InvalidArgumentError(`Positional insert would renumber the reserved range ${label}; ` +
49
- `pass --order ${String(freeOrder).padStart(3, '0')} (first free order below the ` +
47
+ `pass --order ${formatPatchOrder(freeOrder)} (first free order below the ` +
50
48
  'reserved block) to place the new patch without renumbering reserved patches.', 'export placement');
51
49
  }
52
50
  throw new InvalidArgumentError(`Positional insert would renumber the reserved range ${label}; ` +
@@ -2,29 +2,43 @@ import type { PatchesManifest, PatchLintIssue } from '../types/commands/index.js
2
2
  import type { ExportOptions, PatchCategory } from '../types/commands/index.js';
3
3
  import type { FireForgeConfig } from '../types/config.js';
4
4
  import type { SpinnerHandle } from '../utils/logger.js';
5
+ export interface RunPatchLintOptions {
6
+ /** Engine root directory. */
7
+ engineDir: string;
8
+ /** Files touched by the patch. */
9
+ filesAffected: string[];
10
+ /** Raw unified diff string. */
11
+ diffContent: string;
12
+ /** Project configuration. */
13
+ config: FireForgeConfig;
14
+ /** If true, downgrade errors to warnings. */
15
+ skipLint?: boolean | undefined;
16
+ /** Optional cross-patch context for ownership resolution. */
17
+ patchQueueCtx?: import('../core/patch-lint-cross.js').PatchQueueContext | undefined;
18
+ /**
19
+ * Optional per-patch set of `check` IDs to suppress (threaded from
20
+ * `PatchMetadata.lintIgnore`). Surgical alternative to `--skip-lint` when
21
+ * exactly one advisory rule does not apply to a specific patch — e.g.
22
+ * `large-patch-lines` on a cohesive branding bundle that genuinely cannot
23
+ * be split.
24
+ */
25
+ ignoreChecks?: ReadonlySet<string> | undefined;
26
+ /**
27
+ * Optional explicit tier override (threaded from `PatchMetadata.tier`).
28
+ * Forces the branding-tier thresholds when set, independent of the
29
+ * auto-detect allowlist. When the branding tier is applied, a single
30
+ * `info()` line surfaces the choice so the tier decision is visible rather
31
+ * than silent.
32
+ */
33
+ patchTier?: 'branding' | undefined;
34
+ }
5
35
  /**
6
36
  * Runs the full patch lint pipeline and reports results. Warnings are always
7
37
  * displayed. Errors block the export unless skipLint is true.
8
38
  *
9
- * @param engineDir - Engine root directory
10
- * @param filesAffected - Files touched by the patch
11
- * @param diffContent - Raw unified diff string
12
- * @param config - Project configuration
13
- * @param skipLint - If true, downgrade errors to warnings
14
- * @param patchQueueCtx - Optional cross-patch context for ownership
15
- * resolution
16
- * @param ignoreChecks - Optional per-patch set of `check` IDs to suppress
17
- * (threaded from `PatchMetadata.lintIgnore`). Surgical alternative to
18
- * `--skip-lint` when exactly one advisory rule does not apply to a
19
- * specific patch — e.g. `large-patch-lines` on a cohesive branding bundle
20
- * that genuinely cannot be split.
21
- * @param patchTier - Optional explicit tier override (threaded from
22
- * `PatchMetadata.tier`). Forces the branding-tier thresholds when set,
23
- * independent of the auto-detect allowlist. When the branding tier is
24
- * applied, a single `info()` line surfaces the choice so the tier decision
25
- * is visible rather than silent.
39
+ * @param options - See {@link RunPatchLintOptions}
26
40
  */
27
- export declare function runPatchLint(engineDir: string, filesAffected: string[], diffContent: string, config: FireForgeConfig, skipLint?: boolean, patchQueueCtx?: import('../core/patch-lint-cross.js').PatchQueueContext, ignoreChecks?: ReadonlySet<string>, patchTier?: 'branding'): Promise<void>;
41
+ export declare function runPatchLint(options: RunPatchLintOptions): Promise<void>;
28
42
  /**
29
43
  * Prints a patch-lint issue list with `runPatchLint`'s exact severity
30
44
  * buckets and skip-lint semantics, throwing on errors unless `skipLint`.
@@ -32,17 +46,22 @@ export declare function runPatchLint(engineDir: string, filesAffected: string[],
32
46
  * stored issues through the identical reporter without re-linting.
33
47
  */
34
48
  export declare function reportPatchLintOutcome(issues: PatchLintIssue[], skipLint?: boolean): void;
49
+ /** Patch identity resolved for an export, from flags or interactive prompts. */
50
+ export interface ExportPatchMetadata {
51
+ /** Normalized patch name (without order prefix or extension). */
52
+ patchName: string;
53
+ /** Category the patch is filed under. */
54
+ selectedCategory: PatchCategory;
55
+ /** Human-readable patch description. */
56
+ description: string;
57
+ }
35
58
  /**
36
59
  * Resolves patch metadata interactively or from flags, with shared validation.
37
60
  * @param options - Export command options
38
61
  * @param isInteractive - Whether interactive prompts are allowed
39
62
  * @param commandName - Command name for error/help text
40
63
  */
41
- export declare function promptExportPatchMetadata(options: ExportOptions, isInteractive: boolean, commandName: 'export' | 'export-all', config?: FireForgeConfig): Promise<{
42
- patchName: string;
43
- selectedCategory: PatchCategory;
44
- description: string;
45
- } | null>;
64
+ export declare function promptExportPatchMetadata(options: ExportOptions, isInteractive: boolean, commandName: 'export' | 'export-all', config?: FireForgeConfig): Promise<ExportPatchMetadata | null>;
46
65
  /**
47
66
  * Confirms whether an export may supersede existing patches.
48
67
  * @param patchesDir - Patches directory
@@ -15,25 +15,10 @@ import { describePatchNameProblem, normalizePatchDisplayName, PATCH_CATEGORIES,
15
15
  * Runs the full patch lint pipeline and reports results. Warnings are always
16
16
  * displayed. Errors block the export unless skipLint is true.
17
17
  *
18
- * @param engineDir - Engine root directory
19
- * @param filesAffected - Files touched by the patch
20
- * @param diffContent - Raw unified diff string
21
- * @param config - Project configuration
22
- * @param skipLint - If true, downgrade errors to warnings
23
- * @param patchQueueCtx - Optional cross-patch context for ownership
24
- * resolution
25
- * @param ignoreChecks - Optional per-patch set of `check` IDs to suppress
26
- * (threaded from `PatchMetadata.lintIgnore`). Surgical alternative to
27
- * `--skip-lint` when exactly one advisory rule does not apply to a
28
- * specific patch — e.g. `large-patch-lines` on a cohesive branding bundle
29
- * that genuinely cannot be split.
30
- * @param patchTier - Optional explicit tier override (threaded from
31
- * `PatchMetadata.tier`). Forces the branding-tier thresholds when set,
32
- * independent of the auto-detect allowlist. When the branding tier is
33
- * applied, a single `info()` line surfaces the choice so the tier decision
34
- * is visible rather than silent.
18
+ * @param options - See {@link RunPatchLintOptions}
35
19
  */
36
- export async function runPatchLint(engineDir, filesAffected, diffContent, config, skipLint, patchQueueCtx, ignoreChecks, patchTier) {
20
+ export async function runPatchLint(options) {
21
+ const { engineDir, filesAffected, diffContent, config, skipLint, patchQueueCtx, ignoreChecks, patchTier, } = options;
37
22
  // Compute the tier decision independently of the lint pipeline so it can
38
23
  // be surfaced even when the rule body emitted no issues — a branding patch
39
24
  // under the soft threshold still benefits from operators knowing which
@@ -353,7 +338,7 @@ export async function guardOwnershipOverlap(args) {
353
338
  return true;
354
339
  s.stop();
355
340
  const entries = [...overlap.entries()].sort(([a], [b]) => a.localeCompare(b));
356
- warn(`This export would create cross-patch ownership overlap on ${String(entries.length)} file${entries.length === 1 ? '' : 's'}:`);
341
+ warn(`This export would create cross-patch ownership overlap on ${entries.length} file${entries.length === 1 ? '' : 's'}:`);
357
342
  for (const [file, owners] of entries) {
358
343
  warn(` - ${file} already claimed by: ${owners.join(', ')}`);
359
344
  }
@@ -258,7 +258,16 @@ export async function exportCommand(projectRoot, files, options) {
258
258
  const patchQueueCtx = (await pathExists(paths.patches))
259
259
  ? await buildPatchQueueContext(paths.patches)
260
260
  : undefined;
261
- await runPatchLint(paths.engine, filesAffected, diff, config, options.skipLint, patchQueueCtx, exportIgnoreChecks, options.tier);
261
+ await runPatchLint({
262
+ engineDir: paths.engine,
263
+ filesAffected,
264
+ diffContent: diff,
265
+ config,
266
+ skipLint: options.skipLint,
267
+ patchQueueCtx,
268
+ ignoreChecks: exportIgnoreChecks,
269
+ patchTier: options.tier,
270
+ });
262
271
  // Resolve placement (if any flag was given). Placement is mutually
263
272
  // exclusive with supersede — the semantics overlap confusingly.
264
273
  let placementPlan = null;
@@ -0,0 +1,13 @@
1
+ import type { FurnaceConfig } from '../../types/index.js';
2
+ /**
3
+ * Loads `furnace.json` for an authoring command, defaulting to an empty
4
+ * configuration when the project has none yet.
5
+ *
6
+ * `furnace create` and `furnace override` both need this: they are the two
7
+ * commands that may run before any furnace configuration exists, and both must
8
+ * treat "no file" as "nothing registered yet" rather than an error. They had a
9
+ * byte-identical private copy each.
10
+ * @param projectRoot - Project root to load from.
11
+ * @returns The parsed configuration, or a fresh default.
12
+ */
13
+ export declare function loadAuthoringFurnaceConfig(projectRoot: string): Promise<FurnaceConfig>;
@@ -0,0 +1,19 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { createDefaultFurnaceConfig, furnaceConfigExists, loadFurnaceConfig, } from '../../core/furnace-config.js';
3
+ /**
4
+ * Loads `furnace.json` for an authoring command, defaulting to an empty
5
+ * configuration when the project has none yet.
6
+ *
7
+ * `furnace create` and `furnace override` both need this: they are the two
8
+ * commands that may run before any furnace configuration exists, and both must
9
+ * treat "no file" as "nothing registered yet" rather than an error. They had a
10
+ * byte-identical private copy each.
11
+ * @param projectRoot - Project root to load from.
12
+ * @returns The parsed configuration, or a fresh default.
13
+ */
14
+ export async function loadAuthoringFurnaceConfig(projectRoot) {
15
+ if (await furnaceConfigExists(projectRoot)) {
16
+ return loadFurnaceConfig(projectRoot);
17
+ }
18
+ return createDefaultFurnaceConfig();
19
+ }
@@ -10,7 +10,7 @@ import { loadConfig } from '../../core/config.js';
10
10
  import { stdioIsInteractive } from '../../core/destructive.js';
11
11
  import { completeJournalRollback, runFurnaceMutation, } from '../../core/furnace-operation.js';
12
12
  import { assertFurnaceEngineReady } from '../../core/furnace-precondition.js';
13
- import { createRollbackJournal, snapshotDir, snapshotFile } from '../../core/furnace-rollback.js';
13
+ import { createRollbackJournal, snapshotDir, snapshotFile, } from '../../core/furnace-rollback.js';
14
14
  import { FurnaceError } from '../../errors/furnace.js';
15
15
  import { pathExists, readText, removeDir, removeFile, writeText } from '../../utils/fs.js';
16
16
  import { cancel, info, intro, isCancel, note, outro } from '../../utils/logger.js';