@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
@@ -15,24 +15,21 @@
15
15
  * the worktree.
16
16
  */
17
17
  import { join } from 'node:path';
18
- import { appendHistory, assertConfirmationAvailable, confirmDestructive, } from '../../core/destructive.js';
19
- import { normalizePatchArtifact } from '../../core/patch-artifact-normalize.js';
20
- import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, collectForwardImportEdges, formatPatchLintIssue, lintPatchQueue, } from '../../core/patch-lint.js';
18
+ import { assertConfirmationAvailable, confirmDestructive, } from '../../core/destructive.js';
19
+ import { appendHistoryBestEffort } from '../../core/history-log.js';
20
+ import { buildPatchQueueContext, collectForwardImportEdges, formatPatchLintIssue, lintPatchQueue, } from '../../core/patch-lint.js';
21
21
  import { computeProjectedLintRegressions } from '../../core/patch-lint-projection.js';
22
22
  import { withPatchDirectoryLock } from '../../core/patch-lock.js';
23
23
  import { loadPatchesManifest, savePatchesManifest, validatePatchesManifest, } from '../../core/patch-manifest.js';
24
- import { buildNewFileTextProjection } from '../../core/patch-transform.js';
25
24
  import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
26
25
  import { toError } from '../../utils/errors.js';
27
26
  import { readText, writeText } from '../../utils/fs.js';
28
27
  import { info, outro, success, warn } from '../../utils/logger.js';
29
28
  import { resolveWaitLockSeconds } from '../../utils/options.js';
29
+ import { proceedAfterDecision } from '../destructive-decision.js';
30
30
  import { runPatchLint } from '../export-shared.js';
31
+ import { projectEntryBody } from './entry-projection.js';
31
32
  import { buildSplitDiff, findOwnerRewriteHolders, mergeStagedForwardImports, rewriteSplitOwners, } from './split-plan.js';
32
- function projectEntryBody(diff) {
33
- const newFiles = buildNewFileTextProjection(diff);
34
- return { diff, newFiles, modifiedFileAdditions: buildModifiedFileAdditionsFromDiff(diff) };
35
- }
36
33
  /**
37
34
  * Projects the post-move queue through cross-patch lint, auto-declaring the
38
35
  * forward edges the move introduces into the target (an importer that used
@@ -121,9 +118,9 @@ async function commitMoveInto(patchesDir, plan, waitLockSeconds) {
121
118
  let sourceWritten = false;
122
119
  let targetWritten = false;
123
120
  try {
124
- await writeText(sourcePath, normalizePatchArtifact(plan.sourceDiff));
121
+ await writeText(sourcePath, plan.sourceDiff);
125
122
  sourceWritten = true;
126
- await writeText(targetPath, normalizePatchArtifact(plan.targetDiff));
123
+ await writeText(targetPath, plan.targetDiff);
127
124
  targetWritten = true;
128
125
  const updatedPatches = manifest.patches.map((patch) => {
129
126
  let updated = rewriteSplitOwners(patch, plan.source.filename, movedSet, plan.target.filename);
@@ -140,21 +137,16 @@ async function commitMoveInto(patchesDir, plan, waitLockSeconds) {
140
137
  });
141
138
  const updated = validatePatchesManifest({ ...manifest, patches: updatedPatches });
142
139
  await savePatchesManifest(patchesDir, updated);
143
- try {
144
- await appendHistory(patchesDir, {
145
- operation: 'patch-move-files',
146
- args: {
147
- source: plan.source.filename,
148
- target: plan.target.filename,
149
- files: plan.movedFiles,
150
- ownerRewrites: plan.ownerRewrites,
151
- },
152
- result: 'ok',
153
- });
154
- }
155
- catch (historyError) {
156
- warn(`History log append failed after patch move-files committed: ${toError(historyError).message}`);
157
- }
140
+ await appendHistoryBestEffort(patchesDir, {
141
+ operation: 'patch-move-files',
142
+ args: {
143
+ source: plan.source.filename,
144
+ target: plan.target.filename,
145
+ files: plan.movedFiles,
146
+ ownerRewrites: plan.ownerRewrites,
147
+ },
148
+ result: 'ok',
149
+ }, `patch move-files committed`);
158
150
  }
159
151
  catch (error) {
160
152
  if (targetWritten) {
@@ -199,8 +191,26 @@ export async function runMoveFilesInto(args) {
199
191
  // parity) resolves cross-patch imports and sibling head.js harness roots
200
192
  // instead of linting each projected body blind.
201
193
  const patchQueueCtx = await buildPatchQueueContext(patchesDir, args.config);
202
- await runPatchLint(enginePath, sourceAfter, sourceDiff, args.config, options.skipLint, patchQueueCtx, source.lintIgnore ? new Set(source.lintIgnore) : undefined, source.tier);
203
- await runPatchLint(enginePath, targetAfter, targetDiff, args.config, options.skipLint, patchQueueCtx, target.lintIgnore ? new Set(target.lintIgnore) : undefined, target.tier);
194
+ await runPatchLint({
195
+ engineDir: enginePath,
196
+ filesAffected: sourceAfter,
197
+ diffContent: sourceDiff,
198
+ config: args.config,
199
+ skipLint: options.skipLint,
200
+ patchQueueCtx,
201
+ ignoreChecks: source.lintIgnore ? new Set(source.lintIgnore) : undefined,
202
+ patchTier: source.tier,
203
+ });
204
+ await runPatchLint({
205
+ engineDir: enginePath,
206
+ filesAffected: targetAfter,
207
+ diffContent: targetDiff,
208
+ config: args.config,
209
+ skipLint: options.skipLint,
210
+ patchQueueCtx,
211
+ ignoreChecks: target.lintIgnore ? new Set(target.lintIgnore) : undefined,
212
+ patchTier: target.tier,
213
+ });
204
214
  const manifest = await loadPatchesManifest(patchesDir);
205
215
  const plan = {
206
216
  source,
@@ -231,14 +241,8 @@ export async function runMoveFilesInto(args) {
231
241
  unsafeOverride: options.forceUnsafe === true,
232
242
  conflicts,
233
243
  });
234
- if (decision === 'dry-run') {
235
- outro('Dry run complete — no changes made');
236
- return;
237
- }
238
- if (decision === 'declined') {
239
- outro('Move cancelled');
244
+ if (!proceedAfterDecision(decision, 'Move cancelled'))
240
245
  return;
241
- }
242
246
  await commitMoveInto(patchesDir, plan, resolveWaitLockSeconds(options.waitLock));
243
247
  success(`Moved ${files.length} file(s) from ${source.filename} to ${target.filename}`);
244
248
  if (plan.ownerRewrites.length > 0) {
@@ -13,13 +13,10 @@ import { loadPatchesManifest, resolvePatchIdentifier } from '../../core/patch-ma
13
13
  import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
14
14
  import { pathExists } from '../../utils/fs.js';
15
15
  import { info, intro, note, warn } from '../../utils/logger.js';
16
- import { addWaitLockOption, commanderArgParser, pickDefined } from '../../utils/options.js';
16
+ import { addWaitLockOption, commanderArgParser, pickDefined, stringListOption, } from '../../utils/options.js';
17
+ import { parsePositiveIntegerFlag } from '../../utils/validation.js';
17
18
  import { patchMoveFilesCreateCommand } from './move-files-create.js';
18
19
  import { runMoveFilesInto } from './move-files-into.js';
19
- function collectOption(value, previous) {
20
- previous.push(value);
21
- return previous;
22
- }
23
20
  function normalizeFileList(files) {
24
21
  const cleaned = (files ?? []).map((file) => file.trim()).filter((file) => file.length > 0);
25
22
  return [...new Set(cleaned)].sort((left, right) => left.localeCompare(right));
@@ -165,15 +162,9 @@ export function registerPatchMoveFiles(parent, context) {
165
162
  .command('move-files <from> <to>')
166
163
  .description('Preview the re-export commands needed to move file ownership between patches, ' +
167
164
  'or create the target patch and move the files in one transaction with --create --order <n>.')
168
- .option('--file <path>', 'File path relative to engine/ to move (repeatable)', collectOption, [])
165
+ .option('--file <path>', 'File path relative to engine/ to move (repeatable)', ...stringListOption())
169
166
  .option('--create', 'Create <to> as a new patch and move the files into it (requires --order)')
170
- .option('--order <n>', 'Exact sparse order for the created patch (only valid with --create)', commanderArgParser((raw) => {
171
- const n = Number.parseInt(raw, 10);
172
- if (!Number.isInteger(n) || n <= 0) {
173
- throw new InvalidArgumentError(`--order must be a positive integer, got "${raw}".`, '--order');
174
- }
175
- return n;
176
- }))
167
+ .option('--order <n>', 'Exact sparse order for the created patch (only valid with --create)', commanderArgParser((raw) => parsePositiveIntegerFlag('--order', raw)))
177
168
  .option('--category <category>', "Category for the created patch (default: the source patch's)")
178
169
  .option('-d, --description <desc>', 'Description for the created patch')
179
170
  .option('--dry-run', 'Show what would happen without writing')
@@ -19,7 +19,8 @@ import { rename as fsRename } from 'node:fs/promises';
19
19
  import { join } from 'node:path';
20
20
  import { Command } from 'commander';
21
21
  import { loadConfig } from '../../core/config.js';
22
- import { appendHistory, confirmDestructive } from '../../core/destructive.js';
22
+ import { confirmDestructive } from '../../core/destructive.js';
23
+ import { appendHistoryBestEffort } from '../../core/history-log.js';
23
24
  import { patchNameSlug } from '../../core/patch-export.js';
24
25
  import { buildPatchQueueContext, formatPatchLintIssue, lintPatchQueue, } from '../../core/patch-lint.js';
25
26
  import { withPatchDirectoryLock } from '../../core/patch-lock.js';
@@ -30,6 +31,8 @@ import { toError } from '../../utils/errors.js';
30
31
  import { pathExists } from '../../utils/fs.js';
31
32
  import { info, intro, outro, warn } from '../../utils/logger.js';
32
33
  import { addWaitLockOption, commanderArgParser, pickDefined, resolveWaitLockSeconds, } from '../../utils/options.js';
34
+ import { parsePositiveIntegerFlag } from '../../utils/validation.js';
35
+ import { proceedAfterDecision } from '../destructive-decision.js';
33
36
  import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
34
37
  import { projectReorder } from './reorder.js';
35
38
  /**
@@ -76,7 +79,7 @@ async function commitRenameUnderLock(input) {
76
79
  if (orderChanging && input.newOrder !== undefined) {
77
80
  const orderHolder = fresh.patches.find((p) => p.order === input.newOrder && p.filename !== target.filename);
78
81
  if (orderHolder) {
79
- throw new InvalidArgumentError(`Order ${String(input.newOrder)} was claimed by ${orderHolder.filename} concurrently. Pick an unused order, or use "fireforge patch reorder" to renumber siblings.`, '--order');
82
+ throw new InvalidArgumentError(`Order ${input.newOrder} was claimed by ${orderHolder.filename} concurrently. Pick an unused order, or use "fireforge patch reorder" to renumber siblings.`, '--order');
80
83
  }
81
84
  }
82
85
  let oldPath;
@@ -136,28 +139,23 @@ async function commitRenameUnderLock(input) {
136
139
  else {
137
140
  await savePatchesManifest(patchesDir, fresh);
138
141
  }
139
- try {
140
- await appendHistory(patchesDir, {
141
- operation: 'patch-rename',
142
- args: {
143
- oldFilename: target.filename,
144
- newFilename,
145
- oldName: target.name,
146
- newName,
147
- ...(descriptionChanging ? { oldDescription: target.description, newDescription } : {}),
148
- ...(orderChanging ? { oldOrder: target.order, newOrder: input.newOrder } : {}),
149
- ...(categoryChanging
150
- ? { oldCategory: target.category, newCategory: input.newCategory }
151
- : {}),
152
- },
153
- ...(input.yes === true ? { yes: true } : {}),
154
- ...(input.forceUnsafe === true ? { unsafeOverride: true } : {}),
155
- result: 'ok',
156
- });
157
- }
158
- catch (historyError) {
159
- warn(`History log append failed after patch rename committed (${newFilename}): ${toError(historyError).message}`);
160
- }
142
+ await appendHistoryBestEffort(patchesDir, {
143
+ operation: 'patch-rename',
144
+ args: {
145
+ oldFilename: target.filename,
146
+ newFilename,
147
+ oldName: target.name,
148
+ newName,
149
+ ...(descriptionChanging ? { oldDescription: target.description, newDescription } : {}),
150
+ ...(orderChanging ? { oldOrder: target.order, newOrder: input.newOrder } : {}),
151
+ ...(categoryChanging
152
+ ? { oldCategory: target.category, newCategory: input.newCategory }
153
+ : {}),
154
+ },
155
+ ...(input.yes === true ? { yes: true } : {}),
156
+ ...(input.forceUnsafe === true ? { unsafeOverride: true } : {}),
157
+ result: 'ok',
158
+ }, `patch rename committed (${newFilename})`);
161
159
  }, { waitLockSeconds: input.waitLockSeconds, command: 'patch rename' });
162
160
  }
163
161
  /** Validates the flags and resolves the new filename/identity fields. */
@@ -215,14 +213,14 @@ async function assertRenamePreconditions(patchesDir, manifest, target, plan, opt
215
213
  if (plan.orderChanging && options.order !== undefined) {
216
214
  const holder = manifest.patches.find((p) => p.order === options.order && p.filename !== target.filename);
217
215
  if (holder) {
218
- throw new InvalidArgumentError(`Order ${String(options.order)} is already used by ${holder.filename}. Pick an unused order, or use "fireforge patch reorder ${target.filename} --to ${String(options.order)}" to renumber siblings.`, '--order');
216
+ throw new InvalidArgumentError(`Order ${options.order} is already used by ${holder.filename}. Pick an unused order, or use "fireforge patch reorder ${target.filename} --to ${options.order}" to renumber siblings.`, '--order');
219
217
  }
220
218
  if (options.forceUnsafe !== true) {
221
219
  const baseCtx = await buildPatchQueueContext(patchesDir, config);
222
220
  const projected = projectReorder(baseCtx, new Map([[target.filename, { newFilename: plan.newFilename, newOrder: options.order }]]));
223
221
  const projectedErrors = lintPatchQueue(projected).filter((i) => i.severity === 'error');
224
222
  if (projectedErrors.length > 0) {
225
- throw new InvalidArgumentError(`Refusing to run patch rename: the order change would introduce ${String(projectedErrors.length)} cross-patch lint error(s):\n ${projectedErrors
223
+ throw new InvalidArgumentError(`Refusing to run patch rename: the order change would introduce ${projectedErrors.length} cross-patch lint error(s):\n ${projectedErrors
226
224
  .map(formatPatchLintIssue)
227
225
  .join('\n ')}\nPass --force-unsafe to override.`, '--force-unsafe');
228
226
  }
@@ -248,7 +246,7 @@ function buildRenameSummary(target, plan, options) {
248
246
  summary.push(`description: "${target.description || '(none)'}" → "${options.description ?? '(none)'}"`);
249
247
  }
250
248
  if (plan.orderChanging && options.order !== undefined) {
251
- summary.push(`order: ${String(target.order)} → ${String(options.order)}`);
249
+ summary.push(`order: ${target.order} → ${options.order}`);
252
250
  }
253
251
  if (plan.categoryChanging) {
254
252
  summary.push(`category: ${target.category} → ${plan.newCategory}`);
@@ -309,14 +307,8 @@ export async function patchRenameCommand(projectRoot, identifier, options = {})
309
307
  dryRun: isDryRun,
310
308
  conflicts: null,
311
309
  });
312
- if (decision === 'dry-run') {
313
- outro('Dry run complete — no changes made');
314
- return;
315
- }
316
- if (decision === 'declined') {
317
- outro('Rename cancelled');
310
+ if (!proceedAfterDecision(decision, 'Rename cancelled'))
318
311
  return;
319
- }
320
312
  await commitRenameUnderLock({
321
313
  patchesDir: paths.patches,
322
314
  target,
@@ -356,13 +348,7 @@ export function registerPatchRename(parent, context) {
356
348
  .description('Rename a patch: filename, manifest name, category, order, and/or description update in one transaction without rewriting the .patch body.')
357
349
  .option('--to <new-name>', 'New human-readable name, category-prefixed slug, or full filename stem (normalised into the filename slug)')
358
350
  .option('--category <category>', 'New category for the patch (validated against configured categories); rewrites the filename prefix and manifest row in one transaction')
359
- .option('--order <n>', 'Move the patch to this exact unused order; refuses on collision — use "patch reorder" to renumber siblings', commanderArgParser((raw) => {
360
- const n = Number.parseInt(raw, 10);
361
- if (!Number.isInteger(n) || n <= 0) {
362
- throw new InvalidArgumentError(`--order must be a positive integer, got "${raw}".`, '--order');
363
- }
364
- return n;
365
- }))
351
+ .option('--order <n>', 'Move the patch to this exact unused order; refuses on collision — use "patch reorder" to renumber siblings', commanderArgParser((raw) => parsePositiveIntegerFlag('--order', raw)))
366
352
  .option('-d, --description <text>', 'Replacement description (omit to leave description unchanged)')
367
353
  .option('--dry-run', 'Show what would change without writing')
368
354
  .option('-y, --yes', 'Skip confirmation prompt (required for non-TTY)')
@@ -10,16 +10,18 @@
10
10
  */
11
11
  import { Command, Option } from 'commander';
12
12
  import { loadConfig } from '../../core/config.js';
13
- import { appendHistory, confirmDestructive, } from '../../core/destructive.js';
13
+ import { confirmDestructive, } from '../../core/destructive.js';
14
+ import { appendHistoryBestEffort } from '../../core/history-log.js';
14
15
  import { buildPatchQueueContext, formatPatchLintIssue, lintPatchQueue, } from '../../core/patch-lint.js';
15
16
  import { withPatchDirectoryLock } from '../../core/patch-lock.js';
16
17
  import { loadPatchesManifest, renumberPatchesInManifest, resolvePatchIdentifier, rewriteStagedDependencyOwners, } from '../../core/patch-manifest.js';
17
18
  import { applyRenameMapToManifest, enforcePatchPolicy } from '../../core/patch-policy.js';
18
19
  import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
19
- import { toError } from '../../utils/errors.js';
20
- import { info, intro, outro, warn } from '../../utils/logger.js';
20
+ import { info, intro, outro } from '../../utils/logger.js';
21
21
  import { addWaitLockOption, commanderArgParser, pickDefined, resolveWaitLockSeconds, } from '../../utils/options.js';
22
22
  import { parsePositiveIntegerFlag } from '../../utils/validation.js';
23
+ import { proceedAfterDecision } from '../destructive-decision.js';
24
+ import { getSortedRenameEntries, renameMapsEqual } from '../patch-rename-map.js';
23
25
  import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
24
26
  /** Zero-pads an ordinal number to the given width. */
25
27
  function padOrder(value, width) {
@@ -98,25 +100,6 @@ function computeRenameMap(manifestPatches, target, destinationOrder) {
98
100
  });
99
101
  return renames;
100
102
  }
101
- function getSortedRenameEntries(renameMap) {
102
- return Array.from(renameMap.entries()).sort((a, b) => a[1].newOrder - b[1].newOrder);
103
- }
104
- function renameMapsEqual(left, right) {
105
- const leftEntries = getSortedRenameEntries(left);
106
- const rightEntries = getSortedRenameEntries(right);
107
- if (leftEntries.length !== rightEntries.length) {
108
- return false;
109
- }
110
- return leftEntries.every(([leftFilename, leftEntry], index) => {
111
- const rightTuple = rightEntries[index];
112
- if (!rightTuple)
113
- return false;
114
- const [rightFilename, rightEntry] = rightTuple;
115
- return (leftFilename === rightFilename &&
116
- leftEntry.newFilename === rightEntry.newFilename &&
117
- leftEntry.newOrder === rightEntry.newOrder);
118
- });
119
- }
120
103
  /**
121
104
  * Applies a rename map to a {@link PatchQueueContext} so cross-patch lint
122
105
  * can run against the projected state without touching disk. Exported for
@@ -159,7 +142,7 @@ function resolveDestination(target, manifestPatches, options) {
159
142
  // integers, but the function is reachable from tests that may pass
160
143
  // `{ to: NaN }` directly.
161
144
  if (!Number.isInteger(options.to) || options.to <= 0) {
162
- throw new InvalidArgumentError(`--to must be a positive integer, got ${String(options.to)}.`, '--to');
145
+ throw new InvalidArgumentError(`--to must be a positive integer, got ${options.to}.`, '--to');
163
146
  }
164
147
  return { destinationOrder: options.to, anchorFilename: undefined };
165
148
  }
@@ -195,7 +178,14 @@ function resolveDestination(target, manifestPatches, options) {
195
178
  }
196
179
  return { destinationOrder: anchor.order + 1, anchorFilename: anchor.filename };
197
180
  }
198
- async function commitReorderPlan(patchesDir, target, renameMap, anchorFilename, options, config, buildHistoryEntry) {
181
+ /**
182
+ * Re-validates the reorder against the freshly-loaded manifest and commits
183
+ * it, all under the patch directory lock.
184
+ *
185
+ * @param commitOptions - See {@link CommitReorderPlanOptions}
186
+ */
187
+ async function commitReorderPlan(commitOptions) {
188
+ const { patchesDir, target, renameMap, anchorFilename, options, config, buildHistoryEntry } = commitOptions;
199
189
  await withPatchDirectoryLock(patchesDir, async () => {
200
190
  const currentManifest = await loadPatchesManifest(patchesDir);
201
191
  if (!currentManifest || currentManifest.patches.length === 0) {
@@ -248,12 +238,7 @@ async function commitReorderPlan(patchesDir, target, renameMap, anchorFilename,
248
238
  // permissions) is warned but not re-thrown: the mutation has already
249
239
  // succeeded and is not reversible, so surfacing it as a command
250
240
  // failure would mislead.
251
- try {
252
- await appendHistory(patchesDir, buildHistoryEntry(currentRenameMap));
253
- }
254
- catch (historyError) {
255
- warn(`History log append failed after patch reorder committed: ${toError(historyError).message}`);
256
- }
241
+ await appendHistoryBestEffort(patchesDir, buildHistoryEntry(currentRenameMap), `patch reorder committed`);
257
242
  }, { waitLockSeconds: resolveWaitLockSeconds(options.waitLock), command: 'patch reorder' });
258
243
  }
259
244
  /**
@@ -329,14 +314,8 @@ export async function patchReorderCommand(projectRoot, identifier, options = {})
329
314
  unsafeOverride: options.forceUnsafe === true,
330
315
  conflicts,
331
316
  });
332
- if (decision === 'dry-run') {
333
- outro('Dry run complete — no changes made');
334
- return;
335
- }
336
- if (decision === 'declined') {
337
- outro('Reorder cancelled');
317
+ if (!proceedAfterDecision(decision, 'Reorder cancelled'))
338
318
  return;
339
- }
340
319
  // The history entry is built inside commitReorderPlan (still under the
341
320
  // lock) from the *final* rename map, not the pre-confirmation one, so
342
321
  // the destinationOrder and renames mirror what actually landed on disk.
@@ -359,7 +338,15 @@ export async function patchReorderCommand(projectRoot, identifier, options = {})
359
338
  result: 'ok',
360
339
  };
361
340
  };
362
- await commitReorderPlan(paths.patches, target, renameMap, anchorFilename, options, config, buildHistoryEntry);
341
+ await commitReorderPlan({
342
+ patchesDir: paths.patches,
343
+ target,
344
+ renameMap,
345
+ anchorFilename,
346
+ options,
347
+ config,
348
+ buildHistoryEntry,
349
+ });
363
350
  info(`Reordered ${renameMap.size} patch(es).`);
364
351
  outro('Reorder complete');
365
352
  }
@@ -6,10 +6,9 @@
6
6
  * budget; consumed only by the split command.
7
7
  */
8
8
  import { type ConflictReport } from '../../core/destructive.js';
9
- import { buildPatchQueueContext } from '../../core/patch-lint.js';
10
- import { buildProjectedManifest } from '../../core/patch-policy.js';
9
+ import type { PatchQueueContext } from '../../core/patch-lint-cross.js';
11
10
  import type { PatchStagedForwardImport } from '../../types/commands/index.js';
12
- import type { PatchCategory, PatchMetadata } from '../../types/commands/index.js';
11
+ import type { PatchCategory, PatchesManifest, PatchMetadata } from '../../types/commands/index.js';
13
12
  import type { FireForgeConfig } from '../../types/config.js';
14
13
  import { type PlacementPlan } from '../export-flow.js';
15
14
  /** Everything the commit step needs, computed and confirmed up front. */
@@ -68,7 +67,7 @@ export declare function mergeStagedForwardImports(patch: PatchMetadata, decls: r
68
67
  * patch are auto-declared (and the declarations returned) so the projection
69
68
  * matches the real per-patch gate the split leaves behind.
70
69
  */
71
- export declare function runProjectedSplitLint(plan: SplitPlan, baseCtx: Awaited<ReturnType<typeof buildPatchQueueContext>>): {
70
+ export declare function runProjectedSplitLint(plan: SplitPlan, baseCtx: PatchQueueContext): {
72
71
  conflicts: ConflictReport | null;
73
72
  stagedDependencyAdditions: Map<string, PatchStagedForwardImport[]>;
74
73
  };
@@ -76,7 +75,7 @@ export declare function runProjectedSplitLint(plan: SplitPlan, baseCtx: Awaited<
76
75
  export declare function projectSplitManifest(manifest: {
77
76
  version: 1;
78
77
  patches: PatchMetadata[];
79
- }, plan: SplitPlan, newMetadata: PatchMetadata): ReturnType<typeof buildProjectedManifest>;
78
+ }, plan: SplitPlan, newMetadata: PatchMetadata): PatchesManifest;
80
79
  /**
81
80
  * Builds the manifest row for the patch created by a split, carrying the
82
81
  * source's provenance (product, version, category) onto the new patch.
@@ -10,16 +10,16 @@ import { join } from 'node:path';
10
10
  import {} from '../../core/destructive.js';
11
11
  import { getDiffForFilesAgainstHead } from '../../core/git-diff.js';
12
12
  import { extractAffectedFiles } from '../../core/patch-apply.js';
13
- import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, collectForwardImportEdges, formatPatchLintIssue, lintPatchQueue, } from '../../core/patch-lint.js';
13
+ import { collectForwardImportEdges, formatPatchLintIssue, lintPatchQueue, } from '../../core/patch-lint.js';
14
14
  import { computeProjectedLintRegressions } from '../../core/patch-lint-projection.js';
15
15
  import { rewriteStagedDependencyOwners } from '../../core/patch-manifest.js';
16
16
  import { applyRenameMapToManifest, buildProjectedManifest } from '../../core/patch-policy.js';
17
17
  import { buildPatchSourceMetadata } from '../../core/patch-source-metadata.js';
18
- import { buildNewFileTextProjection } from '../../core/patch-transform.js';
19
18
  import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
20
19
  import { pathExists } from '../../utils/fs.js';
21
20
  import { warn } from '../../utils/logger.js';
22
21
  import {} from '../export-flow.js';
22
+ import { projectEntryBody } from './entry-projection.js';
23
23
  /**
24
24
  * Refuses the split unless the source patch currently owns every requested
25
25
  * file. Splitting out a file the source does not own would produce a new patch
@@ -81,10 +81,6 @@ export function rewriteSplitOwners(patch, sourceFilename, movedSet, newFilename)
81
81
  },
82
82
  };
83
83
  }
84
- function buildEntryProjection(diff) {
85
- const newFiles = buildNewFileTextProjection(diff);
86
- return { diff, newFiles, modifiedFileAdditions: buildModifiedFileAdditionsFromDiff(diff) };
87
- }
88
84
  /** Builds the projected post-split queue entries (renumber + shrunken source + new patch). */
89
85
  function buildProjectedSplitEntries(baseCtx, plan) {
90
86
  const movedSet = new Set(plan.movedFiles);
@@ -101,13 +97,13 @@ function buildProjectedSplitEntries(baseCtx, plan) {
101
97
  : { ...entry, metadata };
102
98
  if (entry.filename !== plan.source.filename)
103
99
  return base;
104
- return { ...base, ...buildEntryProjection(plan.remainingDiff) };
100
+ return { ...base, ...projectEntryBody(plan.remainingDiff) };
105
101
  });
106
102
  entries.push({
107
103
  filename: plan.placement.newFilename,
108
104
  order: plan.placement.insertionOrder,
109
105
  metadata: null,
110
- ...buildEntryProjection(plan.movedDiff),
106
+ ...projectEntryBody(plan.movedDiff),
111
107
  });
112
108
  entries.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
113
109
  return entries;
@@ -18,8 +18,8 @@
18
18
  import { join } from 'node:path';
19
19
  import { Command } from 'commander';
20
20
  import { getProjectPaths, loadConfig } from '../../core/config.js';
21
- import { appendHistory, assertConfirmationAvailable, confirmDestructive, } from '../../core/destructive.js';
22
- import { normalizePatchArtifact } from '../../core/patch-artifact-normalize.js';
21
+ import { assertConfirmationAvailable, confirmDestructive } from '../../core/destructive.js';
22
+ import { appendHistoryBestEffort } from '../../core/history-log.js';
23
23
  import { formatPatchNotFoundError } from '../../core/patch-identifier-suggest.js';
24
24
  import { buildPatchQueueContext } from '../../core/patch-lint.js';
25
25
  import { withPatchDirectoryLock } from '../../core/patch-lock.js';
@@ -30,7 +30,8 @@ import { toError } from '../../utils/errors.js';
30
30
  import { readText, removeFile, writeText } from '../../utils/fs.js';
31
31
  import { info, intro, outro, success, warn } from '../../utils/logger.js';
32
32
  import { addWaitLockOption, commanderArgParser, pickDefined, resolveWaitLockSeconds, } from '../../utils/options.js';
33
- import { normalizePatchDisplayName } from '../../utils/validation.js';
33
+ import { normalizePatchDisplayName, parsePositiveIntegerFlag } from '../../utils/validation.js';
34
+ import { proceedAfterDecision } from '../destructive-decision.js';
34
35
  import { placementPlansEqual, resolvePlacementPlan } from '../export-flow.js';
35
36
  import { runPatchLint } from '../export-shared.js';
36
37
  import { assertSourceOwnsFiles, buildNewPatchMetadata, buildSplitDiff, buildSplitSummary, findOwnerRewriteHolders, mergeStagedForwardImports, projectSplitManifest, rewriteSplitOwners, runProjectedSplitLint, } from './split-plan.js';
@@ -68,9 +69,9 @@ export async function commitPatchSplit(patchesDir, plan, newMetadata, options, c
68
69
  await renumberPatchesInManifest(patchesDir, plan.placement.renameMap);
69
70
  renumberApplied = true;
70
71
  }
71
- await writeText(newPatchPath, normalizePatchArtifact(plan.movedDiff));
72
+ await writeText(newPatchPath, plan.movedDiff);
72
73
  newPatchWritten = true;
73
- await writeText(sourcePathAfter, normalizePatchArtifact(plan.remainingDiff));
74
+ await writeText(sourcePathAfter, plan.remainingDiff);
74
75
  sourceRewritten = true;
75
76
  const fresh = await loadPatchesManifest(patchesDir);
76
77
  if (!fresh)
@@ -91,28 +92,23 @@ export async function commitPatchSplit(patchesDir, plan, newMetadata, options, c
91
92
  updatedPatches.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
92
93
  const updated = validatePatchesManifest({ ...fresh, patches: updatedPatches });
93
94
  await savePatchesManifest(patchesDir, updated);
94
- try {
95
- await appendHistory(patchesDir, {
96
- operation: 'patch-split',
97
- args: {
98
- source: effectiveSourceFilename,
99
- newFilename: plan.placement.newFilename,
100
- order: plan.placement.insertionOrder,
101
- files: plan.movedFiles,
102
- ownerRewrites: plan.ownerRewrites,
103
- renames: [...plan.placement.renameMap.entries()].map(([from, entry]) => ({
104
- from,
105
- to: entry.newFilename,
106
- })),
107
- },
108
- ...(options.yes === true ? { yes: true } : {}),
109
- ...(options.forceUnsafe === true ? { unsafeOverride: true } : {}),
110
- result: 'ok',
111
- });
112
- }
113
- catch (historyError) {
114
- warn(`History log append failed after patch split committed: ${toError(historyError).message}`);
115
- }
95
+ await appendHistoryBestEffort(patchesDir, {
96
+ operation: 'patch-split',
97
+ args: {
98
+ source: effectiveSourceFilename,
99
+ newFilename: plan.placement.newFilename,
100
+ order: plan.placement.insertionOrder,
101
+ files: plan.movedFiles,
102
+ ownerRewrites: plan.ownerRewrites,
103
+ renames: [...plan.placement.renameMap.entries()].map(([from, entry]) => ({
104
+ from,
105
+ to: entry.newFilename,
106
+ })),
107
+ },
108
+ ...(options.yes === true ? { yes: true } : {}),
109
+ ...(options.forceUnsafe === true ? { unsafeOverride: true } : {}),
110
+ result: 'ok',
111
+ }, `patch split committed`);
116
112
  }
117
113
  catch (error) {
118
114
  // Reverse-order rollback; each step warns on its own failure so the
@@ -226,8 +222,26 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
226
222
  // instead of linting each projected body blind.
227
223
  const patchQueueCtx = await buildPatchQueueContext(paths.patches, config);
228
224
  const ignoreChecks = source.lintIgnore ? new Set(source.lintIgnore) : undefined;
229
- await runPatchLint(paths.engine, remainingFiles, remainingDiff, config, options.skipLint, patchQueueCtx, ignoreChecks, source.tier);
230
- await runPatchLint(paths.engine, movedFiles, movedDiff, config, options.skipLint, patchQueueCtx, ignoreChecks, source.tier);
225
+ await runPatchLint({
226
+ engineDir: paths.engine,
227
+ filesAffected: remainingFiles,
228
+ diffContent: remainingDiff,
229
+ config,
230
+ skipLint: options.skipLint,
231
+ patchQueueCtx,
232
+ ignoreChecks,
233
+ patchTier: source.tier,
234
+ });
235
+ await runPatchLint({
236
+ engineDir: paths.engine,
237
+ filesAffected: movedFiles,
238
+ diffContent: movedDiff,
239
+ config,
240
+ skipLint: options.skipLint,
241
+ patchQueueCtx,
242
+ ignoreChecks,
243
+ patchTier: source.tier,
244
+ });
231
245
  const { conflicts, stagedDependencyAdditions } = runProjectedSplitLint(plan, patchQueueCtx);
232
246
  plan.stagedDependencyAdditions = stagedDependencyAdditions;
233
247
  const newMetadata = buildNewPatchMetadata(plan, config);
@@ -246,14 +260,8 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
246
260
  unsafeOverride: options.forceUnsafe === true,
247
261
  conflicts,
248
262
  });
249
- if (decision === 'dry-run') {
250
- outro('Dry run complete — no changes made');
251
- return;
252
- }
253
- if (decision === 'declined') {
254
- outro('Split cancelled');
263
+ if (!proceedAfterDecision(decision, 'Split cancelled'))
255
264
  return;
256
- }
257
265
  await commitPatchSplit(paths.patches, plan, newMetadata, options, config);
258
266
  success(`Split ${source.filename}: ${placement.newFilename} now owns ${plan.movedFiles.length} file(s)`);
259
267
  if (plan.ownerRewrites.length > 0) {
@@ -273,13 +281,7 @@ export function registerPatchSplit(parent, context) {
273
281
  .requiredOption('--name <name>', 'Name for the new patch')
274
282
  .option('--category <category>', "Category for the new patch (default: the source patch's)")
275
283
  .option('--description <desc>', 'Description for the new patch')
276
- .option('--order <n>', 'Exact sparse order for the new patch', commanderArgParser((raw) => {
277
- const n = Number.parseInt(raw, 10);
278
- if (!Number.isInteger(n) || n <= 0) {
279
- throw new InvalidArgumentError(`--order must be a positive integer, got "${raw}".`, '--order');
280
- }
281
- return n;
282
- }))
284
+ .option('--order <n>', 'Exact sparse order for the new patch', commanderArgParser((raw) => parsePositiveIntegerFlag('--order', raw)))
283
285
  .option('--before <patch>', 'Place the new patch before this patch')
284
286
  .option('--after <patch>', 'Place the new patch after this patch (default: the source patch)')
285
287
  .option('--dry-run', 'Show what would happen without writing')