@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
@@ -3,6 +3,7 @@ import { basename, join } from 'node:path';
3
3
  import { GeneralError, InvalidArgumentError } from '../errors/base.js';
4
4
  import { pathExists, readText } from '../utils/fs.js';
5
5
  import { warn } from '../utils/logger.js';
6
+ import { normalizePathSlashes } from '../utils/paths.js';
6
7
  import { escapeRegex } from '../utils/regex.js';
7
8
  import { getProjectPaths, loadConfig } from './config.js';
8
9
  import { registerBrowserContent, registerFireForgeModule, registerSharedCSS, registerTestManifest, registerToolkitWidget, } from './moz-manifest-register.js';
@@ -84,13 +85,30 @@ export function getRules(binaryName, createManifest = false) {
84
85
  },
85
86
  ];
86
87
  }
87
- async function isSharedCSSRegistered(engineDir, fileName) {
88
- const manifestPath = join(engineDir, 'browser/themes/shared/jar.inc.mn');
88
+ /**
89
+ * Reads an engine manifest and reports whether it contains any of `needles`.
90
+ *
91
+ * The five registration probes below share this shape: resolve the manifest
92
+ * relative to the engine, refuse with the manifest's own path when it is
93
+ * missing, then substring-match the entry each registrar writes.
94
+ *
95
+ * @param engineDir - Path to the engine directory
96
+ * @param manifestRel - Engine-relative manifest path (forward slashes; also
97
+ * the path named in the refusal message)
98
+ * @param needles - Entry forms that count as registered
99
+ * @returns True when the manifest contains at least one needle
100
+ * @throws GeneralError when the manifest does not exist
101
+ */
102
+ async function manifestIncludes(engineDir, manifestRel, needles) {
103
+ const manifestPath = join(engineDir, manifestRel);
89
104
  if (!(await pathExists(manifestPath))) {
90
- throw new GeneralError('Manifest not found: browser/themes/shared/jar.inc.mn');
105
+ throw new GeneralError(`Manifest not found: ${manifestRel}`);
91
106
  }
92
- const name = basename(fileName, '.css');
93
107
  const content = await readText(manifestPath);
108
+ return needles.some((needle) => content.includes(needle));
109
+ }
110
+ async function isSharedCSSRegistered(engineDir, fileName) {
111
+ const name = basename(fileName, '.css');
94
112
  // `register` writes the canonical `skin/classic/browser/<name>.css` form;
95
113
  // `furnace chrome-doc create` writes a `content/browser/<name>.css` entry
96
114
  // because the CSS is loaded by a chrome document via a `chrome://browser/
@@ -98,40 +116,26 @@ async function isSharedCSSRegistered(engineDir, fileName) {
98
116
  // chrome-doc scaffolder are not flagged as "potentially unregistered" by
99
117
  // `status` and so a re-run of `register` against the same file recognises
100
118
  // the existing entry instead of proposing a duplicate.
101
- return (content.includes(`skin/classic/browser/${name}.css`) ||
102
- content.includes(`content/browser/${name}.css`));
119
+ return manifestIncludes(engineDir, 'browser/themes/shared/jar.inc.mn', [
120
+ `skin/classic/browser/${name}.css`,
121
+ `content/browser/${name}.css`,
122
+ ]);
103
123
  }
104
124
  async function isBrowserContentRegistered(engineDir, fileName) {
105
- const manifestPath = join(engineDir, 'browser/base/jar.mn');
106
- if (!(await pathExists(manifestPath))) {
107
- throw new GeneralError('Manifest not found: browser/base/jar.mn');
108
- }
109
- const content = await readText(manifestPath);
110
- return content.includes(`content/browser/${fileName}`);
125
+ return manifestIncludes(engineDir, 'browser/base/jar.mn', [`content/browser/${fileName}`]);
111
126
  }
112
127
  async function isTestManifestRegistered(engineDir, testDir) {
113
- const manifestPath = join(engineDir, 'browser/base/moz.build');
114
- if (!(await pathExists(manifestPath))) {
115
- throw new GeneralError('Manifest not found: browser/base/moz.build');
116
- }
117
- const content = await readText(manifestPath);
118
- return content.includes(`content/test/${testDir}/browser.toml`);
128
+ return manifestIncludes(engineDir, 'browser/base/moz.build', [
129
+ `content/test/${testDir}/browser.toml`,
130
+ ]);
119
131
  }
120
132
  async function isFireForgeModuleRegistered(engineDir, fileName, moduleDir) {
121
- const manifestPath = join(engineDir, moduleDir, 'moz.build');
122
- if (!(await pathExists(manifestPath))) {
123
- throw new GeneralError(`Manifest not found: ${moduleDir}/moz.build`);
124
- }
125
- const content = await readText(manifestPath);
126
- return content.includes(`"${fileName}"`);
133
+ return manifestIncludes(engineDir, `${moduleDir}/moz.build`, [`"${fileName}"`]);
127
134
  }
128
135
  async function isToolkitWidgetRegistered(engineDir, _tagName, fileName) {
129
- const manifestPath = join(engineDir, 'toolkit/content/jar.mn');
130
- if (!(await pathExists(manifestPath))) {
131
- throw new GeneralError('Manifest not found: toolkit/content/jar.mn');
132
- }
133
- const content = await readText(manifestPath);
134
- return content.includes(`content/global/elements/${fileName}`);
136
+ return manifestIncludes(engineDir, 'toolkit/content/jar.mn', [
137
+ `content/global/elements/${fileName}`,
138
+ ]);
135
139
  }
136
140
  /**
137
141
  * Checks if a file path matches any known registrable pattern.
@@ -140,7 +144,7 @@ async function isToolkitWidgetRegistered(engineDir, _tagName, fileName) {
140
144
  * @returns True if the file matches a known registration pattern
141
145
  */
142
146
  export function matchesRegistrablePattern(filePath, binaryName) {
143
- const normalized = filePath.replace(/\\/g, '/');
147
+ const normalized = normalizePathSlashes(filePath);
144
148
  const rules = getRules(binaryName);
145
149
  return rules.some((rule) => rule.pattern.test(normalized));
146
150
  }
@@ -197,7 +201,7 @@ export async function isFileRegistered(root, filePath) {
197
201
  const { engine: engineDir } = getProjectPaths(root);
198
202
  const config = await loadConfig(root);
199
203
  const rules = getRules(config.binaryName);
200
- const normalizedPath = filePath.replace(/\\/g, '/');
204
+ const normalizedPath = normalizePathSlashes(filePath);
201
205
  for (const rule of rules) {
202
206
  const match = normalizedPath.match(rule.pattern);
203
207
  if (match) {
@@ -237,7 +241,7 @@ export async function registerFile(root, filePath, dryRun = false, after, option
237
241
  const config = await loadConfig(root);
238
242
  const rules = getRules(config.binaryName, options.createManifest === true);
239
243
  // Normalize path separators
240
- const normalizedPath = filePath.replace(/\\/g, '/');
244
+ const normalizedPath = normalizePathSlashes(filePath);
241
245
  for (const rule of rules) {
242
246
  const match = normalizedPath.match(rule.pattern);
243
247
  if (match) {
@@ -42,13 +42,13 @@ const GIT_DEFAULT_CONTEXT = 3;
42
42
  */
43
43
  export async function applyPatchWithFuzz(patchPath, engineDir, maxFuzz = 3) {
44
44
  if (!Number.isInteger(maxFuzz) || maxFuzz < 0) {
45
- throw new InvalidArgumentError(`maxFuzz must be a non-negative integer, got ${String(maxFuzz)}.`, 'maxFuzz');
45
+ throw new InvalidArgumentError(`maxFuzz must be a non-negative integer, got ${maxFuzz}.`, 'maxFuzz');
46
46
  }
47
47
  await ensureGit();
48
48
  // Try exact match first, then escalate context reduction.
49
49
  const maxSteps = Math.min(maxFuzz, GIT_DEFAULT_CONTEXT);
50
50
  for (let step = 0; step <= maxSteps; step++) {
51
- const contextArgs = step > 0 ? [`-C${String(GIT_DEFAULT_CONTEXT - step)}`] : [];
51
+ const contextArgs = step > 0 ? [`-C${GIT_DEFAULT_CONTEXT - step}`] : [];
52
52
  const check = await exec('git', ['apply', '--check', ...contextArgs, '--', patchPath], {
53
53
  cwd: engineDir,
54
54
  });
@@ -59,12 +59,12 @@ export async function applyPatchWithFuzz(patchPath, engineDir, maxFuzz = 3) {
59
59
  });
60
60
  if (apply.exitCode === 0) {
61
61
  if (step > 0) {
62
- verbose(`Patch applied with reduced context (-C${String(GIT_DEFAULT_CONTEXT - step)}): ${patchPath}`);
62
+ verbose(`Patch applied with reduced context (-C${GIT_DEFAULT_CONTEXT - step}): ${patchPath}`);
63
63
  }
64
64
  return { success: true, fuzzFactor: step };
65
65
  }
66
66
  // Unlikely: --check passed but apply failed; fall through to next step
67
- verbose(`git apply -C${String(GIT_DEFAULT_CONTEXT - step)} --check passed but apply failed: ${apply.stderr.trim()}`);
67
+ verbose(`git apply -C${GIT_DEFAULT_CONTEXT - step} --check passed but apply failed: ${apply.stderr.trim()}`);
68
68
  }
69
69
  }
70
70
  // All context-reduction levels failed → generate .rej files for manual resolution
@@ -4,10 +4,9 @@
4
4
  */
5
5
  import type { ImportSummary, PatchInfo, PatchMetadata } from '../types/commands/index.js';
6
6
  export { PatchError } from '../errors/patch.js';
7
- export { countPatches, discoverPatches, getAllTargetFilesFromPatch, getTargetFileFromPatch, isNewFilePatch, } from './patch-files.js';
7
+ export { countPatches, discoverPatches } from './patch-files.js';
8
8
  export { withPatchDirectoryLock } from './patch-lock.js';
9
- export { extractAffectedFiles, extractOrder, isNewFileInPatch } from './patch-parse.js';
10
- export { applyPatchToContent, extractNewFileContent } from './patch-transform.js';
9
+ export { extractAffectedFiles } from './patch-parse.js';
11
10
  /**
12
11
  * Options for {@link applyPatchesWithContinue}.
13
12
  */
@@ -47,10 +46,10 @@ export declare function matchesUntilFilename(patchFilename: string, needle: stri
47
46
  * on the first failure to keep the engine directory in a clean state.
48
47
  * @param patchesDir - Path to the patches directory
49
48
  * @param engineDir - Path to the engine directory
50
- * @param optionsOrContinue - Options object, or the legacy continueOnFailure boolean
49
+ * @param options - Application options
51
50
  * @returns Import summary with all results
52
51
  */
53
- export declare function applyPatchesWithContinue(patchesDir: string, engineDir: string, optionsOrContinue?: ApplyPatchesOptions | boolean): Promise<ImportSummary>;
52
+ export declare function applyPatchesWithContinue(patchesDir: string, engineDir: string, options?: ApplyPatchesOptions): Promise<ImportSummary>;
54
53
  /**
55
54
  * Batched patched-content computation shared by status classification,
56
55
  * import's unmanaged-dirty guard, and discard baseline planning.
@@ -11,17 +11,16 @@ import { pathExists, readText, writeFileAtomic, writeText } from '../utils/fs.js
11
11
  import { verbose } from '../utils/logger.js';
12
12
  import { isContainedRelativePath } from '../utils/paths.js';
13
13
  import { applyPatchIdempotent, reversePatch } from './git.js';
14
- import { getFileContentFromHead } from './git-file-ops.js';
14
+ import { getFileContentAtRef } from './git-file-ops.js';
15
15
  import { discoverPatches } from './patch-files.js';
16
16
  import { loadPatchesManifest } from './patch-manifest.js';
17
17
  import { extractAffectedFiles, extractConflictingFiles, isNewFileInPatch, parseDiffSections, } from './patch-parse.js';
18
18
  import { applyPatchTextToContent, extractNewFileContentFromDiff } from './patch-transform.js';
19
19
  // Re-export from split modules so existing import sites continue working
20
20
  export { PatchError } from '../errors/patch.js';
21
- export { countPatches, discoverPatches, getAllTargetFilesFromPatch, getTargetFileFromPatch, isNewFilePatch, } from './patch-files.js';
21
+ export { countPatches, discoverPatches } from './patch-files.js';
22
22
  export { withPatchDirectoryLock } from './patch-lock.js';
23
- export { extractAffectedFiles, extractOrder, isNewFileInPatch } from './patch-parse.js';
24
- export { applyPatchToContent, extractNewFileContent } from './patch-transform.js';
23
+ export { extractAffectedFiles } from './patch-parse.js';
25
24
  /**
26
25
  * Applies a single patch.
27
26
  *
@@ -272,15 +271,10 @@ export function matchesUntilFilename(patchFilename, needle) {
272
271
  * on the first failure to keep the engine directory in a clean state.
273
272
  * @param patchesDir - Path to the patches directory
274
273
  * @param engineDir - Path to the engine directory
275
- * @param optionsOrContinue - Options object, or the legacy continueOnFailure boolean
274
+ * @param options - Application options
276
275
  * @returns Import summary with all results
277
276
  */
278
- export async function applyPatchesWithContinue(patchesDir, engineDir, optionsOrContinue = false) {
279
- // Accept both the legacy boolean positional and the new options object so
280
- // existing call sites (tests and rebase) keep working without a rewrite.
281
- const options = typeof optionsOrContinue === 'boolean'
282
- ? { continueOnFailure: optionsOrContinue }
283
- : optionsOrContinue;
277
+ export async function applyPatchesWithContinue(patchesDir, engineDir, options = {}) {
284
278
  const continueOnFailure = options.continueOnFailure ?? false;
285
279
  const untilFilename = options.untilFilename;
286
280
  const patches = await discoverPatches(patchesDir);
@@ -406,7 +400,7 @@ export async function createPatchedContentContext(patchesDir, engineDir) {
406
400
  getAffectingPatches,
407
401
  readPatchBody,
408
402
  computePatched: async (filePath) => {
409
- let content = await getFileContentFromHead(engineDir, filePath);
403
+ let content = await getFileContentAtRef(engineDir, filePath);
410
404
  for (const patch of getAffectingPatches(filePath)) {
411
405
  content = applyPatchTextToContent(content, await readPatchBody(patch), filePath);
412
406
  }
@@ -2,15 +2,6 @@
2
2
  * Patch coverage and supersession helpers used by export planning.
3
3
  */
4
4
  import type { PatchInfo, PatchMetadata } from '../types/commands/index.js';
5
- /**
6
- * Finds patches that are completely superseded by newer patches.
7
- * A patch is superseded if all its affected files are covered by newer patches.
8
- * @param patchesDir - Path to the patches directory
9
- * @param newPatchFiles - Files affected by the new patch
10
- * @param excludeFilename - Filename to exclude from results (the new patch itself)
11
- * @returns Superseded patches
12
- */
13
- export declare function findSupersededPatches(patchesDir: string, newPatchFiles: string[], excludeFilename?: string): Promise<PatchInfo[]>;
14
5
  /**
15
6
  * Report whether a patch is fully covered by a new export, and which of its
16
7
  * files caused the coverage.
@@ -2,37 +2,8 @@
2
2
  /**
3
3
  * Patch coverage and supersession helpers used by export planning.
4
4
  */
5
- import { discoverPatches, isNewFilePatch } from './patch-apply.js';
5
+ import { discoverPatches } from './patch-apply.js';
6
6
  import { loadPatchesManifest } from './patch-manifest.js';
7
- /**
8
- * Finds patches that are completely superseded by newer patches.
9
- * A patch is superseded if all its affected files are covered by newer patches.
10
- * @param patchesDir - Path to the patches directory
11
- * @param newPatchFiles - Files affected by the new patch
12
- * @param excludeFilename - Filename to exclude from results (the new patch itself)
13
- * @returns Superseded patches
14
- */
15
- export async function findSupersededPatches(patchesDir, newPatchFiles, excludeFilename) {
16
- const manifest = await loadPatchesManifest(patchesDir);
17
- if (!manifest)
18
- return [];
19
- const patches = await discoverPatches(patchesDir);
20
- const superseded = [];
21
- for (const metadata of manifest.patches) {
22
- if (excludeFilename && metadata.filename === excludeFilename)
23
- continue;
24
- if (metadata.filesAffected.length === 1) {
25
- const affectedFile = metadata.filesAffected[0];
26
- if (affectedFile && newPatchFiles.includes(affectedFile)) {
27
- const patch = patches.find((p) => p.filename === metadata.filename);
28
- if (patch && (await isNewFilePatch(patch.path))) {
29
- superseded.push(patch);
30
- }
31
- }
32
- }
33
- }
34
- return superseded;
35
- }
36
7
  /**
37
8
  * Checks whether a patch is fully covered by a new export.
38
9
  * A patch is fully covered when every file it affects is present in the new export.
@@ -13,6 +13,22 @@ export interface UpdatePatchPolicyGate {
13
13
  command: string;
14
14
  forceUnsafe?: boolean;
15
15
  }
16
+ export interface UpdatePatchAndMetadataOptions {
17
+ /** Patch directory holding the body and `patches.json`. */
18
+ patchesDir: string;
19
+ /** Patch file to rewrite. */
20
+ filename: string;
21
+ /** New patch body; an identical body skips the write. */
22
+ newContent: string;
23
+ /** Manifest-row fields to merge into the patch's metadata. */
24
+ updates: Partial<PatchMetadata>;
25
+ /** Hook run under the lock after the mutation succeeds. */
26
+ onCommitted?: UpdatePatchCommittedHook | undefined;
27
+ /** Policy gate run against the under-lock projected manifest. */
28
+ policyGate?: UpdatePatchPolicyGate | undefined;
29
+ /** Wait budget and command name for the patch directory lock. */
30
+ lockOptions?: PatchDirectoryLockOptions | undefined;
31
+ }
16
32
  /**
17
33
  * Updates a patch file body and its manifest row under the same patch
18
34
  * directory lock. Intended for commands like `re-export --files` where the
@@ -26,7 +42,8 @@ export interface UpdatePatchPolicyGate {
26
42
  * unexpected rewrite in `git status`. Metadata still moves either way — only
27
43
  * the redundant byte-for-byte write is elided.
28
44
  *
45
+ * @param options - See {@link UpdatePatchAndMetadataOptions}
29
46
  * @returns True when the patch body on disk changed, false when it was
30
47
  * already byte-identical to the new content.
31
48
  */
32
- export declare function updatePatchAndMetadata(patchesDir: string, filename: string, newContent: string, updates: Partial<PatchMetadata>, onCommitted?: UpdatePatchCommittedHook, policyGate?: UpdatePatchPolicyGate, lockOptions?: PatchDirectoryLockOptions): Promise<boolean>;
49
+ export declare function updatePatchAndMetadata(options: UpdatePatchAndMetadataOptions): Promise<boolean>;
@@ -5,7 +5,6 @@ import { toError } from '../utils/errors.js';
5
5
  import { pathExists, readText, writeText } from '../utils/fs.js';
6
6
  import { warn } from '../utils/logger.js';
7
7
  import { withPatchDirectoryLock } from './patch-apply.js';
8
- import { normalizePatchArtifact } from './patch-artifact-normalize.js';
9
8
  import { loadPatchesManifestForWrite, mutatePatchRowsInManifest } from './patch-manifest.js';
10
9
  import { buildProjectedManifest, enforcePatchPolicy } from './patch-policy.js';
11
10
  /**
@@ -21,10 +20,12 @@ import { buildProjectedManifest, enforcePatchPolicy } from './patch-policy.js';
21
20
  * unexpected rewrite in `git status`. Metadata still moves either way — only
22
21
  * the redundant byte-for-byte write is elided.
23
22
  *
23
+ * @param options - See {@link UpdatePatchAndMetadataOptions}
24
24
  * @returns True when the patch body on disk changed, false when it was
25
25
  * already byte-identical to the new content.
26
26
  */
27
- export async function updatePatchAndMetadata(patchesDir, filename, newContent, updates, onCommitted, policyGate, lockOptions = {}) {
27
+ export async function updatePatchAndMetadata(options) {
28
+ const { patchesDir, filename, newContent, updates, onCommitted, policyGate, lockOptions = {}, } = options;
28
29
  return withPatchDirectoryLock(patchesDir, async () => {
29
30
  const manifest = await loadPatchesManifestForWrite(patchesDir);
30
31
  if (!manifest) {
@@ -49,7 +50,7 @@ export async function updatePatchAndMetadata(patchesDir, filename, newContent, u
49
50
  forceUnsafe: policyGate.forceUnsafe === true,
50
51
  });
51
52
  }
52
- const normalized = normalizePatchArtifact(newContent);
53
+ const normalized = newContent;
53
54
  const bodyChanged = normalized !== originalContent;
54
55
  let patchWritten = false;
55
56
  try {
@@ -2,9 +2,19 @@ import type { PatchCategory, PatchesManifest, PatchInfo, PatchMetadata } from '.
2
2
  import type { FireForgeConfig } from '../types/config.js';
3
3
  import { type SupersedeCoverageDetail } from './patch-export-coverage.js';
4
4
  import type { PatchDirectoryLockOptions } from './patch-lock.js';
5
- export { findAllPatchesForFiles, findAllPatchesForFilesWithDetails, findSupersededPatches, isPatchFullyCovered, type SupersedeCoverageDetail, } from './patch-export-coverage.js';
5
+ export { findAllPatchesForFiles, findAllPatchesForFilesWithDetails, } from './patch-export-coverage.js';
6
6
  export { mutatePatchMetadata, updatePatchMetadata } from './patch-export-metadata.js';
7
7
  export { updatePatchAndMetadata } from './patch-export-update.js';
8
+ /**
9
+ * Projects the planning subset out of a wider export input (commit input,
10
+ * dry-run preview input) — the fields `planExport` /
11
+ * `computeExportPlanUnderLock` read. Optional fields are copied only when
12
+ * present so the plan input never carries explicit `undefined` keys, which
13
+ * would otherwise leak into the written metadata via spread. The commit
14
+ * path and the dry-run preview both go through this one projection so
15
+ * they cannot drift field by field.
16
+ */
17
+ export declare function buildPlanExportInput(input: PlanExportInput): PlanExportInput;
8
18
  /**
9
19
  * Gets the next patch number for a new patch.
10
20
  * @param patchesDir - Path to the patches directory
@@ -91,26 +101,6 @@ export interface CommitExportedPatchResult {
91
101
  * to filename allocation or supersede detection lands in both automatically.
92
102
  */
93
103
  export declare function commitExportedPatch(input: CommitExportedPatchInput): Promise<CommitExportedPatchResult>;
94
- /**
95
- * Parses a patch filename to extract order, category, and name.
96
- * Supports both new format (001-category-name.patch) and legacy (001-name.patch).
97
- */
98
- export declare function parseFilename(filename: string): {
99
- order: number;
100
- category: PatchCategory | null;
101
- name: string;
102
- };
103
- /**
104
- * Finds an existing patch that contains the specified file.
105
- * Returns the most recent (highest order) patch if multiple exist.
106
- * @param patchesDir - Path to the patches directory
107
- * @param filePath - File path to search for
108
- * @returns The patch info and metadata, or null if not found
109
- */
110
- export declare function findExistingPatchForFile(patchesDir: string, filePath: string): Promise<{
111
- patch: PatchInfo;
112
- metadata: PatchMetadata;
113
- } | null>;
114
104
  /**
115
105
  * Updates the content of a patch file.
116
106
  * @param patchPath - Path to the patch file
@@ -3,17 +3,39 @@ import { join } from 'node:path';
3
3
  import { toError } from '../utils/errors.js';
4
4
  import { pathExists, readText, removeFile, writeText } from '../utils/fs.js';
5
5
  import { warn } from '../utils/logger.js';
6
- import { PATCH_CATEGORIES } from '../utils/validation.js';
7
6
  import { discoverPatches, withPatchDirectoryLock } from './patch-apply.js';
8
- import { normalizePatchArtifact } from './patch-artifact-normalize.js';
9
7
  import { findAllPatchesForFilesWithDetails, } from './patch-export-coverage.js';
10
- import { addPatchToManifest, findPatchesAffectingFile, loadPatchesManifestForWrite, PATCHES_MANIFEST, savePatchesManifest, } from './patch-manifest.js';
11
- import { requirePatchOrder } from './patch-parse.js';
8
+ import { addPatchToManifest, loadPatchesManifestForWrite, PATCHES_MANIFEST, savePatchesManifest, } from './patch-manifest.js';
9
+ import { formatPatchOrder, requirePatchOrder } from './patch-parse.js';
12
10
  import { allocatePolicyOrder, enforcePatchPolicy } from './patch-policy.js';
13
- export { findAllPatchesForFiles, findAllPatchesForFilesWithDetails, findSupersededPatches, isPatchFullyCovered, } from './patch-export-coverage.js';
11
+ export { findAllPatchesForFiles, findAllPatchesForFilesWithDetails, } from './patch-export-coverage.js';
14
12
  import { escapeRegex } from '../utils/regex.js';
15
13
  export { mutatePatchMetadata, updatePatchMetadata } from './patch-export-metadata.js';
16
14
  export { updatePatchAndMetadata } from './patch-export-update.js';
15
+ /**
16
+ * Projects the planning subset out of a wider export input (commit input,
17
+ * dry-run preview input) — the fields `planExport` /
18
+ * `computeExportPlanUnderLock` read. Optional fields are copied only when
19
+ * present so the plan input never carries explicit `undefined` keys, which
20
+ * would otherwise leak into the written metadata via spread. The commit
21
+ * path and the dry-run preview both go through this one projection so
22
+ * they cannot drift field by field.
23
+ */
24
+ export function buildPlanExportInput(input) {
25
+ return {
26
+ patchesDir: input.patchesDir,
27
+ category: input.category,
28
+ name: input.name,
29
+ description: input.description,
30
+ filesAffected: input.filesAffected,
31
+ sourceEsrVersion: input.sourceEsrVersion,
32
+ ...(input.sourceProduct !== undefined ? { sourceProduct: input.sourceProduct } : {}),
33
+ ...(input.sourceVersion !== undefined ? { sourceVersion: input.sourceVersion } : {}),
34
+ ...(input.tier !== undefined ? { tier: input.tier } : {}),
35
+ ...(input.lintIgnore !== undefined ? { lintIgnore: input.lintIgnore } : {}),
36
+ ...(input.config !== undefined ? { config: input.config } : {}),
37
+ };
38
+ }
17
39
  /**
18
40
  * Gets the next patch number for a new patch.
19
41
  * @param patchesDir - Path to the patches directory
@@ -110,19 +132,7 @@ export async function getNextPatchFilename(patchesDir, category, name) {
110
132
  */
111
133
  export async function commitExportedPatch(input) {
112
134
  return withPatchDirectoryLock(input.patchesDir, async () => {
113
- const plan = await computeExportPlanUnderLock({
114
- patchesDir: input.patchesDir,
115
- category: input.category,
116
- name: input.name,
117
- description: input.description,
118
- filesAffected: input.filesAffected,
119
- sourceEsrVersion: input.sourceEsrVersion,
120
- ...(input.sourceProduct !== undefined ? { sourceProduct: input.sourceProduct } : {}),
121
- ...(input.sourceVersion !== undefined ? { sourceVersion: input.sourceVersion } : {}),
122
- ...(input.tier !== undefined ? { tier: input.tier } : {}),
123
- ...(input.lintIgnore !== undefined ? { lintIgnore: input.lintIgnore } : {}),
124
- ...(input.config !== undefined ? { config: input.config } : {}),
125
- });
135
+ const plan = await computeExportPlanUnderLock(buildPlanExportInput(input));
126
136
  if (input.config !== undefined) {
127
137
  enforcePatchPolicy({
128
138
  config: input.config,
@@ -140,7 +150,20 @@ export async function commitExportedPatch(input) {
140
150
  }
141
151
  }
142
152
  try {
143
- await writeText(patchPath, normalizePatchArtifact(input.diff));
153
+ // Patch bodies are written byte-for-byte as git produced them, including the
154
+ // single-space rendering of a blank context line.
155
+ //
156
+ // Do NOT add a whitespace-trimming pass here or at any other write site.
157
+ // Stripping marker lines whose payload is pure whitespace (`/^[ +-]\s+$/` →
158
+ // bare marker) corrupts real content: Firefox sources contain whitespace-only
159
+ // lines, so a ` `/`-` line whose payload was two spaces no longer matches the
160
+ // pristine tree and the freshly exported patch fails `git apply --check`,
161
+ // while a `+` line silently changes what the patch produces. The repository
162
+ // whitespace check already exempts `patches/*.patch`
163
+ // (scripts/check-worktree-whitespace.mjs), and the byte-fidelity contract is
164
+ // pinned end-to-end by `git-diff-latin1-roundtrip.test.ts` and
165
+ // `re-export.integration.test.ts`.
166
+ await writeText(patchPath, input.diff);
144
167
  await addPatchToManifest(input.patchesDir, plan.metadata, plan.supersededPatches.map((p) => p.filename));
145
168
  for (const oldPatch of plan.supersededPatches) {
146
169
  await removeFile(oldPatch.path);
@@ -188,58 +211,13 @@ export async function commitExportedPatch(input) {
188
211
  };
189
212
  }, input.lockOptions ?? {});
190
213
  }
191
- /**
192
- * Parses a patch filename to extract order, category, and name.
193
- * Supports both new format (001-category-name.patch) and legacy (001-name.patch).
194
- */
195
- export function parseFilename(filename) {
196
- // New format: 001-ui-sidebar.patch
197
- const newMatch = /^(\d+)-([a-z]+)-(.+)\.patch$/.exec(filename);
198
- if (newMatch?.[1] && newMatch[2] && newMatch[3]) {
199
- const orderStr = newMatch[1];
200
- const category = newMatch[2];
201
- const name = newMatch[3];
202
- if (PATCH_CATEGORIES.includes(category)) {
203
- return {
204
- order: parseInt(orderStr, 10),
205
- category,
206
- name,
207
- };
208
- }
209
- }
210
- // Legacy format: 001-name.patch
211
- const legacyMatch = /^(\d+)-(.+)\.patch$/.exec(filename);
212
- if (legacyMatch?.[1] && legacyMatch[2]) {
213
- return {
214
- order: parseInt(legacyMatch[1], 10),
215
- category: null,
216
- name: legacyMatch[2],
217
- };
218
- }
219
- return { order: Infinity, category: null, name: filename };
220
- }
221
- /**
222
- * Finds an existing patch that contains the specified file.
223
- * Returns the most recent (highest order) patch if multiple exist.
224
- * @param patchesDir - Path to the patches directory
225
- * @param filePath - File path to search for
226
- * @returns The patch info and metadata, or null if not found
227
- */
228
- export async function findExistingPatchForFile(patchesDir, filePath) {
229
- const affectingPatches = await findPatchesAffectingFile(patchesDir, filePath);
230
- if (affectingPatches.length === 0) {
231
- return null;
232
- }
233
- // Return the most recent (highest order) patch
234
- return affectingPatches[affectingPatches.length - 1] ?? null;
235
- }
236
214
  /**
237
215
  * Updates the content of a patch file.
238
216
  * @param patchPath - Path to the patch file
239
217
  * @param newContent - New patch content
240
218
  */
241
219
  export async function updatePatch(patchPath, newContent) {
242
- await writeText(patchPath, normalizePatchArtifact(newContent));
220
+ await writeText(patchPath, newContent);
243
221
  }
244
222
  /**
245
223
  * Internal planning helper. Does NOT take the patch directory lock — the
@@ -259,7 +237,7 @@ async function computeExportPlanUnderLock(input) {
259
237
  ? allocatePolicyOrder(input.config, manifestBefore?.patches ?? [], input.category)
260
238
  : null;
261
239
  const patchFilename = policyOrder !== null
262
- ? `${String(policyOrder).padStart(3, '0')}-${input.category}-${patchNameSlug(input.name, input.category)}.patch`
240
+ ? `${formatPatchOrder(policyOrder)}-${input.category}-${patchNameSlug(input.name, input.category)}.patch`
263
241
  : await getNextPatchFilename(input.patchesDir, input.category, input.name);
264
242
  const patchPath = join(input.patchesDir, patchFilename);
265
243
  const metadata = {
@@ -3,10 +3,6 @@ import type { PatchInfo } from '../types/commands/index.js';
3
3
  export declare function discoverPatches(patchesDir: string): Promise<PatchInfo[]>;
4
4
  /** Counts the patch files currently present in a patch directory. */
5
5
  export declare function countPatches(patchesDir: string): Promise<number>;
6
- /** Checks whether a patch creates a new file rather than modifying an existing one. */
7
- export declare function isNewFilePatch(patchPath: string): Promise<boolean>;
8
- /** Returns the first target file path referenced by a patch, if any. */
9
- export declare function getTargetFileFromPatch(patchPath: string): Promise<string | null>;
10
6
  /**
11
7
  * Returns all target file paths referenced by a multi-file patch.
12
8
  *
@@ -24,17 +24,6 @@ export async function countPatches(patchesDir) {
24
24
  const patches = await discoverPatches(patchesDir);
25
25
  return patches.length;
26
26
  }
27
- /** Checks whether a patch creates a new file rather than modifying an existing one. */
28
- export async function isNewFilePatch(patchPath) {
29
- const content = await readText(patchPath);
30
- return content.includes('new file mode') && content.includes('--- /dev/null');
31
- }
32
- /** Returns the first target file path referenced by a patch, if any. */
33
- export async function getTargetFileFromPatch(patchPath) {
34
- const content = await readText(patchPath);
35
- const match = /^\+\+\+ b\/(.+)$/m.exec(content);
36
- return match?.[1] ?? null;
37
- }
38
27
  /**
39
28
  * Returns all target file paths referenced by a multi-file patch.
40
29
  *
@@ -21,20 +21,14 @@
21
21
  * cross-patch lint graph.
22
22
  */
23
23
  import type { PatchLintIssue } from '../types/commands/index.js';
24
+ import type { PatchQueueBodyEntry, PatchQueueView } from './patch-lint-queue-types.js';
24
25
  /**
25
- * The slice of a queue entry this rule reads. Declared structurally rather
26
- * than importing `PatchQueueEntry` from `patch-lint-cross.ts`, which imports
27
- * this module back — the local shape keeps the dependency edge one-way and
28
- * `dpdm` clean, matching `patch-lint-module-registration.ts`.
26
+ * Queue view accepted by {@link lintPatchQueueBinaryBodies}. Described
27
+ * structurally rather than by importing `PatchQueueContext` from
28
+ * `patch-lint-cross.ts`, which imports this module back — the shared leaf
29
+ * type keeps the dependency edge one-way and `dpdm` clean.
29
30
  */
30
- interface BinaryBodyQueueEntry {
31
- filename: string;
32
- diff: string;
33
- }
34
- /** Queue view accepted by {@link lintPatchQueueBinaryBodies}. */
35
- interface BinaryBodyQueueContext {
36
- entries: readonly BinaryBodyQueueEntry[];
37
- }
31
+ type BinaryBodyQueueContext = PatchQueueView<PatchQueueBodyEntry>;
38
32
  /** Lint check identifier emitted by {@link lintPatchQueueBinaryBodies}. */
39
33
  export declare const BINARY_BODY_CHECK = "binary-body-not-reconstructable";
40
34
  /**
@@ -58,5 +58,21 @@ export interface CachedPerPatchLint {
58
58
  * is compared explicitly rather than trusted to the key.
59
59
  */
60
60
  export declare function getCachedPerPatchLintIssues(cache: PerPatchLintCacheFile, patchFilename: string, key: string, lintIgnore?: Iterable<string>): CachedPerPatchLint | undefined;
61
+ export interface SetCachedPerPatchLintIssuesInput {
62
+ /** Cache file to mutate in place. */
63
+ cache: PerPatchLintCacheFile;
64
+ /** Patch the payload belongs to; also the cache entry key. */
65
+ patchFilename: string;
66
+ /** Fingerprint the payload is valid for. */
67
+ key: string;
68
+ /** Reported issues, stored as copies. */
69
+ issues: PatchLintIssue[];
70
+ /** Issues suppressed by `lintIgnore`, stored as copies. */
71
+ suppressed: PatchLintIssue[];
72
+ /** Line count the run measured. */
73
+ lineCount: number;
74
+ /** Suppression ids in force for the run, normalized on store. */
75
+ lintIgnore?: Iterable<string> | undefined;
76
+ }
61
77
  /** Stores the per-patch lint payload after a successful lint calculation. */
62
- export declare function setCachedPerPatchLintIssues(cache: PerPatchLintCacheFile, patchFilename: string, key: string, issues: PatchLintIssue[], suppressed: PatchLintIssue[], lineCount: number, lintIgnore?: Iterable<string>): void;
78
+ export declare function setCachedPerPatchLintIssues(input: SetCachedPerPatchLintIssuesInput): void;