@hominis/fireforge 0.49.2 → 0.50.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 (334) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +1 -1
  3. package/dist/bin/fireforge.js +6 -0
  4. package/dist/build-info.json +4 -4
  5. package/dist/src/commands/build-prelock.d.ts +20 -0
  6. package/dist/src/commands/build-prelock.js +50 -0
  7. package/dist/src/commands/build.d.ts +3 -1
  8. package/dist/src/commands/build.js +13 -6
  9. package/dist/src/commands/check-report.d.ts +28 -0
  10. package/dist/src/commands/check-report.js +78 -0
  11. package/dist/src/commands/check-run.d.ts +28 -0
  12. package/dist/src/commands/check-run.js +104 -0
  13. package/dist/src/commands/check-steps.d.ts +67 -0
  14. package/dist/src/commands/check-steps.js +175 -0
  15. package/dist/src/commands/check.d.ts +32 -0
  16. package/dist/src/commands/check.js +160 -0
  17. package/dist/src/commands/config.js +5 -6
  18. package/dist/src/commands/doctor-engine-eslint.js +6 -1
  19. package/dist/src/commands/doctor-source-pin.js +4 -1
  20. package/dist/src/commands/export-adopt-check.d.ts +38 -0
  21. package/dist/src/commands/export-adopt-check.js +98 -0
  22. package/dist/src/commands/export-flow.js +29 -14
  23. package/dist/src/commands/export-placement-gate.d.ts +6 -2
  24. package/dist/src/commands/export-shared.d.ts +2 -1
  25. package/dist/src/commands/export-staged-registration.d.ts +46 -0
  26. package/dist/src/commands/export-staged-registration.js +91 -0
  27. package/dist/src/commands/forward-import-remedy.d.ts +32 -0
  28. package/dist/src/commands/forward-import-remedy.js +63 -0
  29. package/dist/src/commands/furnace/apply.js +3 -3
  30. package/dist/src/commands/furnace/chrome-doc-remove.d.ts +2 -1
  31. package/dist/src/commands/furnace/chrome-doc-remove.js +2 -2
  32. package/dist/src/commands/furnace/chrome-doc.d.ts +2 -1
  33. package/dist/src/commands/furnace/chrome-doc.js +2 -2
  34. package/dist/src/commands/furnace/create.js +2 -2
  35. package/dist/src/commands/furnace/deploy-named.d.ts +48 -0
  36. package/dist/src/commands/furnace/deploy-named.js +107 -0
  37. package/dist/src/commands/furnace/deploy.d.ts +3 -2
  38. package/dist/src/commands/furnace/deploy.js +50 -86
  39. package/dist/src/commands/furnace/diff.js +26 -3
  40. package/dist/src/commands/furnace/index.js +66 -41
  41. package/dist/src/commands/furnace/init.js +13 -6
  42. package/dist/src/commands/furnace/override.js +4 -2
  43. package/dist/src/commands/furnace/preview.js +4 -3
  44. package/dist/src/commands/furnace/refresh.js +2 -2
  45. package/dist/src/commands/furnace/remove.js +4 -3
  46. package/dist/src/commands/furnace/rename-dry-run.d.ts +29 -0
  47. package/dist/src/commands/furnace/rename-dry-run.js +127 -0
  48. package/dist/src/commands/furnace/rename-helpers.d.ts +7 -0
  49. package/dist/src/commands/furnace/rename-helpers.js +15 -1
  50. package/dist/src/commands/furnace/rename-registrations.d.ts +41 -0
  51. package/dist/src/commands/furnace/rename-registrations.js +93 -0
  52. package/dist/src/commands/furnace/rename.d.ts +2 -1
  53. package/dist/src/commands/furnace/rename.js +28 -66
  54. package/dist/src/commands/furnace/scan.d.ts +9 -4
  55. package/dist/src/commands/furnace/scan.js +11 -9
  56. package/dist/src/commands/furnace/sync.js +3 -2
  57. package/dist/src/commands/furnace/validate-report.d.ts +45 -0
  58. package/dist/src/commands/furnace/validate-report.js +91 -0
  59. package/dist/src/commands/furnace/validate.d.ts +1 -1
  60. package/dist/src/commands/furnace/validate.js +127 -114
  61. package/dist/src/commands/furnace/validation-output.js +14 -3
  62. package/dist/src/commands/lint-per-patch.js +31 -60
  63. package/dist/src/commands/lint-report.d.ts +8 -4
  64. package/dist/src/commands/lint-report.js +15 -5
  65. package/dist/src/commands/lint.js +15 -34
  66. package/dist/src/commands/manifest.js +2 -0
  67. package/dist/src/commands/patch/delete-gap-notice.js +3 -1
  68. package/dist/src/commands/patch/delete.d.ts +1 -1
  69. package/dist/src/commands/patch/delete.js +13 -98
  70. package/dist/src/commands/patch/index.d.ts +1 -1
  71. package/dist/src/commands/patch/index.js +4 -2
  72. package/dist/src/commands/patch/lint-ignore.d.ts +5 -1
  73. package/dist/src/commands/patch/lint-ignore.js +65 -28
  74. package/dist/src/commands/patch/move-files-create.js +26 -17
  75. package/dist/src/commands/patch/move-files-into.js +5 -3
  76. package/dist/src/commands/patch/move-files.js +3 -1
  77. package/dist/src/commands/patch/removal-dependents.d.ts +63 -0
  78. package/dist/src/commands/patch/removal-dependents.js +133 -0
  79. package/dist/src/commands/patch/rename.js +7 -0
  80. package/dist/src/commands/patch/retire.d.ts +34 -0
  81. package/dist/src/commands/patch/retire.js +128 -0
  82. package/dist/src/commands/patch/split-plan.d.ts +7 -7
  83. package/dist/src/commands/patch/split-plan.js +45 -28
  84. package/dist/src/commands/patch/split-staged.d.ts +108 -0
  85. package/dist/src/commands/patch/split-staged.js +202 -0
  86. package/dist/src/commands/patch/split.d.ts +5 -0
  87. package/dist/src/commands/patch/split.js +51 -15
  88. package/dist/src/commands/patch/staged-dependency-batch.d.ts +48 -0
  89. package/dist/src/commands/patch/staged-dependency-batch.js +176 -0
  90. package/dist/src/commands/patch/staged-dependency-entries.d.ts +36 -0
  91. package/dist/src/commands/patch/staged-dependency-entries.js +52 -0
  92. package/dist/src/commands/patch/staged-dependency-from.d.ts +14 -0
  93. package/dist/src/commands/patch/staged-dependency-from.js +108 -0
  94. package/dist/src/commands/patch/staged-dependency.js +14 -41
  95. package/dist/src/commands/re-export-adjacent-registrations.d.ts +20 -0
  96. package/dist/src/commands/re-export-adjacent-registrations.js +75 -0
  97. package/dist/src/commands/re-export-adjacent.d.ts +45 -2
  98. package/dist/src/commands/re-export-adjacent.js +156 -37
  99. package/dist/src/commands/re-export-drift.d.ts +36 -8
  100. package/dist/src/commands/re-export-drift.js +73 -49
  101. package/dist/src/commands/re-export-files.js +11 -8
  102. package/dist/src/commands/re-export-lint.d.ts +2 -1
  103. package/dist/src/commands/re-export-lint.js +23 -10
  104. package/dist/src/commands/re-export-options.js +45 -4
  105. package/dist/src/commands/re-export-preflight.d.ts +23 -0
  106. package/dist/src/commands/re-export-preflight.js +160 -0
  107. package/dist/src/commands/re-export-register.js +42 -17
  108. package/dist/src/commands/re-export-removed.d.ts +12 -1
  109. package/dist/src/commands/re-export-removed.js +41 -11
  110. package/dist/src/commands/re-export-scan-owner.d.ts +44 -0
  111. package/dist/src/commands/re-export-scan-owner.js +116 -0
  112. package/dist/src/commands/re-export-scan.d.ts +17 -9
  113. package/dist/src/commands/re-export-scan.js +56 -23
  114. package/dist/src/commands/re-export-single.js +22 -6
  115. package/dist/src/commands/re-export-summary.d.ts +28 -0
  116. package/dist/src/commands/re-export-summary.js +36 -0
  117. package/dist/src/commands/re-export.js +37 -24
  118. package/dist/src/commands/rebase/index.js +17 -17
  119. package/dist/src/commands/run.js +16 -2
  120. package/dist/src/commands/status-check.d.ts +55 -16
  121. package/dist/src/commands/status-check.js +92 -37
  122. package/dist/src/commands/status-json.d.ts +22 -9
  123. package/dist/src/commands/status-json.js +47 -32
  124. package/dist/src/commands/status-output.js +3 -1
  125. package/dist/src/commands/status-scope.d.ts +37 -0
  126. package/dist/src/commands/status-scope.js +54 -0
  127. package/dist/src/commands/status.js +66 -23
  128. package/dist/src/commands/test-browser-preflight.js +19 -3
  129. package/dist/src/commands/test-build-furnace.d.ts +21 -0
  130. package/dist/src/commands/test-build-furnace.js +48 -0
  131. package/dist/src/commands/test-build-phase.js +47 -41
  132. package/dist/src/commands/test-checkjs.js +7 -0
  133. package/dist/src/commands/test-diagnose.js +7 -0
  134. package/dist/src/commands/test-modes.d.ts +16 -1
  135. package/dist/src/commands/test-modes.js +24 -1
  136. package/dist/src/commands/test-multi-verdict.d.ts +61 -0
  137. package/dist/src/commands/test-multi-verdict.js +111 -0
  138. package/dist/src/commands/test-orchestrate.d.ts +43 -0
  139. package/dist/src/commands/test-orchestrate.js +222 -0
  140. package/dist/src/commands/test-perf-scope.d.ts +29 -0
  141. package/dist/src/commands/test-perf-scope.js +83 -0
  142. package/dist/src/commands/test-prelock.d.ts +51 -0
  143. package/dist/src/commands/test-prelock.js +177 -0
  144. package/dist/src/commands/test-register.js +54 -10
  145. package/dist/src/commands/test-run.js +4 -1
  146. package/dist/src/commands/test-stale-gate.js +57 -27
  147. package/dist/src/commands/test-verdict.d.ts +10 -1
  148. package/dist/src/commands/test-verdict.js +28 -1
  149. package/dist/src/commands/test.d.ts +6 -1
  150. package/dist/src/commands/test.js +66 -60
  151. package/dist/src/commands/token-lint.d.ts +14 -0
  152. package/dist/src/commands/token-lint.js +111 -0
  153. package/dist/src/commands/token.js +10 -0
  154. package/dist/src/commands/verify-scope.d.ts +28 -0
  155. package/dist/src/commands/verify-scope.js +67 -0
  156. package/dist/src/commands/verify.d.ts +10 -2
  157. package/dist/src/commands/verify.js +59 -35
  158. package/dist/src/core/build-baseline-types.d.ts +46 -0
  159. package/dist/src/core/build-baseline-validate.js +19 -0
  160. package/dist/src/core/build-baseline.d.ts +5 -1
  161. package/dist/src/core/build-baseline.js +3 -1
  162. package/dist/src/core/build-overwrite-guard.d.ts +50 -0
  163. package/dist/src/core/build-overwrite-guard.js +84 -4
  164. package/dist/src/core/build-prepare.d.ts +7 -0
  165. package/dist/src/core/build-prepare.js +14 -8
  166. package/dist/src/core/build-run-record.d.ts +74 -0
  167. package/dist/src/core/build-run-record.js +136 -0
  168. package/dist/src/core/checkjs-support-heads.d.ts +41 -0
  169. package/dist/src/core/checkjs-support-heads.js +104 -0
  170. package/dist/src/core/config-paths.d.ts +2 -2
  171. package/dist/src/core/config-paths.js +8 -0
  172. package/dist/src/core/config-validate-check.d.ts +8 -0
  173. package/dist/src/core/config-validate-check.js +143 -0
  174. package/dist/src/core/config-validate-file-size.d.ts +9 -6
  175. package/dist/src/core/config-validate-file-size.js +91 -2
  176. package/dist/src/core/config-validate-history.d.ts +11 -0
  177. package/dist/src/core/config-validate-history.js +38 -0
  178. package/dist/src/core/config-validate-patch-policy.js +100 -0
  179. package/dist/src/core/config-validate-test-toolchains.js +7 -0
  180. package/dist/src/core/config-validate-token-scales.d.ts +6 -0
  181. package/dist/src/core/config-validate-token-scales.js +50 -0
  182. package/dist/src/core/config-validate.js +14 -5
  183. package/dist/src/core/config.d.ts +5 -0
  184. package/dist/src/core/config.js +16 -0
  185. package/dist/src/core/coverage-dispatch.d.ts +24 -0
  186. package/dist/src/core/coverage-dispatch.js +44 -0
  187. package/dist/src/core/coverage-extend.d.ts +70 -45
  188. package/dist/src/core/coverage-extend.js +199 -144
  189. package/dist/src/core/coverage-records.d.ts +97 -0
  190. package/dist/src/core/coverage-records.js +350 -0
  191. package/dist/src/core/destructive.d.ts +7 -4
  192. package/dist/src/core/destructive.js +10 -10
  193. package/dist/src/core/engine-session-lock.d.ts +10 -1
  194. package/dist/src/core/engine-session-lock.js +28 -8
  195. package/dist/src/core/export-moved-code-shape.d.ts +49 -11
  196. package/dist/src/core/export-moved-code-shape.js +135 -44
  197. package/dist/src/core/file-lock-queue.d.ts +67 -0
  198. package/dist/src/core/file-lock-queue.js +221 -0
  199. package/dist/src/core/file-lock.d.ts +4 -25
  200. package/dist/src/core/file-lock.js +105 -138
  201. package/dist/src/core/furnace-apply-output.d.ts +10 -0
  202. package/dist/src/core/furnace-apply-output.js +31 -1
  203. package/dist/src/core/furnace-apply.js +12 -2
  204. package/dist/src/core/furnace-composed-source.d.ts +76 -0
  205. package/dist/src/core/furnace-composed-source.js +167 -0
  206. package/dist/src/core/furnace-config-validate-exceptions.d.ts +9 -0
  207. package/dist/src/core/furnace-config-validate-exceptions.js +71 -0
  208. package/dist/src/core/furnace-config-validate.js +8 -0
  209. package/dist/src/core/furnace-css-fragments.d.ts +10 -0
  210. package/dist/src/core/furnace-css-fragments.js +26 -0
  211. package/dist/src/core/furnace-operation.d.ts +3 -3
  212. package/dist/src/core/furnace-registration-remove.d.ts +22 -1
  213. package/dist/src/core/furnace-registration-remove.js +53 -17
  214. package/dist/src/core/furnace-registration-rename.d.ts +26 -0
  215. package/dist/src/core/furnace-registration-rename.js +77 -0
  216. package/dist/src/core/furnace-registration.d.ts +14 -4
  217. package/dist/src/core/furnace-registration.js +44 -10
  218. package/dist/src/core/furnace-staleness.d.ts +7 -0
  219. package/dist/src/core/furnace-staleness.js +34 -24
  220. package/dist/src/core/furnace-validate-accessibility.d.ts +4 -2
  221. package/dist/src/core/furnace-validate-accessibility.js +10 -1
  222. package/dist/src/core/furnace-validate-exceptions.d.ts +40 -0
  223. package/dist/src/core/furnace-validate-exceptions.js +43 -0
  224. package/dist/src/core/furnace-validate-icon-button.d.ts +7 -0
  225. package/dist/src/core/furnace-validate-icon-button.js +49 -0
  226. package/dist/src/core/furnace-validate.js +1 -1
  227. package/dist/src/core/harness-port-fallback.d.ts +60 -0
  228. package/dist/src/core/harness-port-fallback.js +96 -0
  229. package/dist/src/core/history-log-file.d.ts +61 -0
  230. package/dist/src/core/history-log-file.js +157 -0
  231. package/dist/src/core/history-log.d.ts +2 -1
  232. package/dist/src/core/history-log.js +2 -1
  233. package/dist/src/core/json-preserving-edit.d.ts +22 -0
  234. package/dist/src/core/json-preserving-edit.js +199 -0
  235. package/dist/src/core/mach.js +8 -1
  236. package/dist/src/core/negative-control-diff.d.ts +39 -0
  237. package/dist/src/core/negative-control-diff.js +136 -0
  238. package/dist/src/core/negative-control.d.ts +138 -0
  239. package/dist/src/core/negative-control.js +356 -0
  240. package/dist/src/core/objdir-install-manifests.d.ts +73 -0
  241. package/dist/src/core/objdir-install-manifests.js +226 -0
  242. package/dist/src/core/patch-export-update.d.ts +2 -0
  243. package/dist/src/core/patch-export-update.js +1 -0
  244. package/dist/src/core/patch-export.js +3 -4
  245. package/dist/src/core/patch-lint-checkjs-any-import.d.ts +37 -0
  246. package/dist/src/core/patch-lint-checkjs-any-import.js +61 -0
  247. package/dist/src/core/patch-lint-checkjs.js +8 -11
  248. package/dist/src/core/patch-lint-code-lines.d.ts +59 -0
  249. package/dist/src/core/patch-lint-code-lines.js +105 -0
  250. package/dist/src/core/patch-lint-cross.js +12 -4
  251. package/dist/src/core/patch-lint-css.d.ts +8 -2
  252. package/dist/src/core/patch-lint-css.js +10 -4
  253. package/dist/src/core/patch-lint-file-size.d.ts +12 -5
  254. package/dist/src/core/patch-lint-file-size.js +22 -11
  255. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +18 -0
  256. package/dist/src/core/patch-lint-forward-registration-extended.js +35 -10
  257. package/dist/src/core/patch-lint-forward-registration.js +10 -0
  258. package/dist/src/core/patch-lint-ignore.d.ts +152 -0
  259. package/dist/src/core/patch-lint-ignore.js +299 -0
  260. package/dist/src/core/patch-lint-size-thresholds.d.ts +52 -0
  261. package/dist/src/core/patch-lint-size-thresholds.js +110 -0
  262. package/dist/src/core/patch-lint-size.d.ts +117 -0
  263. package/dist/src/core/patch-lint-size.js +323 -0
  264. package/dist/src/core/patch-lint-staged-registration.d.ts +18 -1
  265. package/dist/src/core/patch-lint-staged-registration.js +20 -0
  266. package/dist/src/core/patch-lint-token-scale.d.ts +39 -0
  267. package/dist/src/core/patch-lint-token-scale.js +109 -0
  268. package/dist/src/core/patch-lint.d.ts +11 -83
  269. package/dist/src/core/patch-lint.js +64 -286
  270. package/dist/src/core/patch-lock.js +7 -6
  271. package/dist/src/core/patch-manifest-consistency.js +7 -6
  272. package/dist/src/core/patch-manifest-io.d.ts +6 -3
  273. package/dist/src/core/patch-manifest-io.js +19 -8
  274. package/dist/src/core/patch-manifest-validate.js +34 -2
  275. package/dist/src/core/patch-order-legal.d.ts +25 -0
  276. package/dist/src/core/patch-order-legal.js +83 -0
  277. package/dist/src/core/patch-policy-content.d.ts +40 -0
  278. package/dist/src/core/patch-policy-content.js +216 -0
  279. package/dist/src/core/patch-policy.d.ts +16 -3
  280. package/dist/src/core/patch-policy.js +16 -9
  281. package/dist/src/core/patch-retired.d.ts +35 -0
  282. package/dist/src/core/patch-retired.js +54 -0
  283. package/dist/src/core/patch-subset.d.ts +16 -0
  284. package/dist/src/core/patch-subset.js +66 -0
  285. package/dist/src/core/preflight-wait.d.ts +20 -0
  286. package/dist/src/core/preflight-wait.js +38 -0
  287. package/dist/src/core/rebase-from-version.d.ts +46 -0
  288. package/dist/src/core/rebase-from-version.js +71 -0
  289. package/dist/src/core/run-log.d.ts +20 -0
  290. package/dist/src/core/run-log.js +89 -3
  291. package/dist/src/core/run-profile.d.ts +7 -0
  292. package/dist/src/core/run-profile.js +7 -1
  293. package/dist/src/core/smoke-patterns.d.ts +5 -0
  294. package/dist/src/core/smoke-patterns.js +13 -0
  295. package/dist/src/core/test-harness-crash.d.ts +18 -1
  296. package/dist/src/core/test-harness-crash.js +6 -4
  297. package/dist/src/core/test-harness-signature.d.ts +17 -0
  298. package/dist/src/core/test-harness-signature.js +4 -0
  299. package/dist/src/core/test-run-completeness.d.ts +57 -0
  300. package/dist/src/core/test-run-completeness.js +144 -0
  301. package/dist/src/core/test-stale-check.d.ts +27 -1
  302. package/dist/src/core/test-stale-check.js +39 -49
  303. package/dist/src/core/test-stale-exempt.d.ts +40 -0
  304. package/dist/src/core/test-stale-exempt.js +70 -0
  305. package/dist/src/core/token-lint.d.ts +50 -0
  306. package/dist/src/core/token-lint.js +126 -0
  307. package/dist/src/core/tree-guard.js +3 -0
  308. package/dist/src/core/typecheck-shim.d.ts +1 -1
  309. package/dist/src/core/typecheck-shim.js +90 -2
  310. package/dist/src/index.d.ts +8 -3
  311. package/dist/src/index.js +3 -1
  312. package/dist/src/types/commands/index.d.ts +2 -2
  313. package/dist/src/types/commands/options.d.ts +148 -21
  314. package/dist/src/types/commands/patches.d.ts +75 -1
  315. package/dist/src/types/config.d.ts +192 -0
  316. package/dist/src/types/config.js +9 -0
  317. package/dist/src/types/furnace.d.ts +43 -1
  318. package/dist/src/types/furnace.js +7 -0
  319. package/dist/src/types/index.d.ts +1 -1
  320. package/dist/src/utils/options.d.ts +34 -6
  321. package/dist/src/utils/options.js +86 -5
  322. package/dist/src/utils/path-glob.d.ts +15 -0
  323. package/dist/src/utils/path-glob.js +47 -0
  324. package/dist/src/utils/regex.d.ts +15 -3
  325. package/dist/src/utils/regex.js +37 -16
  326. package/docs/configuration.md +282 -9
  327. package/docs/exit-codes.md +18 -1
  328. package/docs/furnace.md +97 -3
  329. package/docs/lifecycle-invariants.md +52 -19
  330. package/docs/machine-output.md +74 -12
  331. package/docs/patch-workflow.md +297 -22
  332. package/docs/run-logs.md +20 -1
  333. package/docs/testing.md +276 -20
  334. package/package.json +1 -1
@@ -0,0 +1,128 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * `fireforge patch retire <name>`: removes a patch from the queue and keeps
4
+ * its order number spent as a retired slot in `patches.json#retired`.
5
+ *
6
+ * The case it exists for: a rename leaves a patch owning only files that no
7
+ * longer exist. `patch delete` opens a numeric gap that
8
+ * `patchPolicy.allowGaps: false` refuses, and `patch compact` renumbers
9
+ * every later patch, including ones a peer session has in flight. Retiring
10
+ * removes the patch file and row like a delete, and records the order as a
11
+ * placeholder the gap check counts as occupied. Nothing else reads the
12
+ * placeholder (see `core/patch-retired.ts`), and a later
13
+ * `export --order <n>` that takes the number fills it.
14
+ *
15
+ * Shares the delete's dependency refusal, dry-run, confirmation and history
16
+ * contract, so the two differ only in what happens to the order number.
17
+ */
18
+ import { Command } from 'commander';
19
+ import { loadConfig } from '../../core/config.js';
20
+ import { confirmDestructive } from '../../core/destructive.js';
21
+ import { appendHistoryBestEffort } from '../../core/history-log.js';
22
+ import { buildPatchQueueContext } from '../../core/patch-lint.js';
23
+ import { withPatchDirectoryLock } from '../../core/patch-lock.js';
24
+ import { removePatchFileAndManifest } from '../../core/patch-manifest.js';
25
+ import { formatPatchOrder } from '../../core/patch-parse.js';
26
+ import { GeneralError } from '../../errors/base.js';
27
+ import { info, intro, outro } from '../../utils/logger.js';
28
+ import { addWaitLockOption, pickDefined, resolveWaitLockSeconds } from '../../utils/options.js';
29
+ import { proceedAfterDecision } from '../destructive-decision.js';
30
+ import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
31
+ import { collectRemovalDependents, queueSnapshot, removalConflict, warnDanglingStagedOwners, } from './removal-dependents.js';
32
+ /**
33
+ * Runs the `patch retire` command.
34
+ *
35
+ * @param projectRoot - Project root directory
36
+ * @param identifier - Patch filename, ordinal, or manifest name to retire
37
+ * @param options - Command options
38
+ */
39
+ export async function patchRetireCommand(projectRoot, identifier, options = {}) {
40
+ intro(options.dryRun ? 'FireForge patch retire (dry run)' : 'FireForge patch retire');
41
+ const { paths, manifest } = await requirePatchQueue(projectRoot, {
42
+ missingDirMessage: 'Patches directory not found. No patches to retire.',
43
+ });
44
+ const target = requirePatchTarget(identifier, manifest.patches);
45
+ const baseCtx = await buildPatchQueueContext(paths.patches);
46
+ const dependents = collectRemovalDependents(baseCtx, target, {
47
+ noun: 'retirement',
48
+ verb: 'retire',
49
+ });
50
+ warnDanglingStagedOwners(baseCtx, target, 'retire');
51
+ const order = formatPatchOrder(target.order);
52
+ const summary = [
53
+ `retire ${target.filename} (category: ${target.category}, order: ${order})`,
54
+ `description: ${target.description || '(none)'}`,
55
+ `order ${order} stays spent as a retired slot in patches.json; ` +
56
+ `"fireforge export --order ${order}" can fill it later`,
57
+ `files currently claimed by this patch (${target.filesAffected.length}):`,
58
+ ];
59
+ for (const file of target.filesAffected) {
60
+ summary.push(` ${file} → will become unmanaged`);
61
+ }
62
+ const decision = await confirmDestructive({
63
+ operation: 'patch-retire',
64
+ title: `Retire ${target.filename}`,
65
+ summary,
66
+ yes: options.yes === true,
67
+ dryRun: options.dryRun === true,
68
+ unsafeOverride: options.forceUnsafe === true,
69
+ conflicts: removalConflict(dependents, target),
70
+ });
71
+ if (!proceedAfterDecision(decision, 'Retire cancelled'))
72
+ return;
73
+ const slot = {
74
+ order: target.order,
75
+ category: target.category,
76
+ filename: target.filename,
77
+ retiredAt: new Date().toISOString(),
78
+ ...(options.reason !== undefined ? { reason: options.reason } : {}),
79
+ };
80
+ // Same locking and history contract as `patch delete`: the dependency
81
+ // scan approved before the prompt is re-checked under the lock, and the
82
+ // history record is appended inside it.
83
+ await withPatchDirectoryLock(paths.patches, async () => {
84
+ const current = await buildPatchQueueContext(paths.patches);
85
+ if (queueSnapshot(current) !== queueSnapshot(baseCtx)) {
86
+ throw new GeneralError('Patch queue changed while waiting for confirmation or the lock. Re-run patch retire to review the current dependencies.');
87
+ }
88
+ await removePatchFileAndManifest(paths.patches, target.filename, slot);
89
+ await appendHistoryBestEffort(paths.patches, {
90
+ operation: 'patch-retire',
91
+ args: {
92
+ filename: target.filename,
93
+ order: target.order,
94
+ filesAffected: target.filesAffected,
95
+ ...(options.reason !== undefined ? { reason: options.reason } : {}),
96
+ },
97
+ ...(options.yes === true ? { yes: true } : {}),
98
+ ...(options.forceUnsafe === true ? { unsafeOverride: true } : {}),
99
+ result: 'ok',
100
+ }, `patch retire committed (${target.filename})`);
101
+ }, { waitLockSeconds: resolveWaitLockSeconds(options.waitLock), command: 'patch retire' });
102
+ info(`Retired ${target.filename}; order ${order} is kept as a retired slot.`);
103
+ if ((await loadConfig(projectRoot)).patchPolicy?.allowGaps !== false) {
104
+ info('patchPolicy.allowGaps is not false, so no check reads the retired slot; ' +
105
+ '"patch delete" would have had the same effect on this queue.');
106
+ }
107
+ outro('Retire complete');
108
+ }
109
+ /**
110
+ * Registers the `patch retire` subcommand on the `patch` parent.
111
+ *
112
+ * @param parent - Parent Commander command
113
+ * @param context - Shared CLI registration context
114
+ */
115
+ export function registerPatchRetire(parent, context) {
116
+ const { getProjectRoot, withErrorHandling } = context;
117
+ const command = parent
118
+ .command('retire <name>')
119
+ .description('Remove a patch from the queue but keep its order number as a retired slot, ' +
120
+ 'so patchPolicy.allowGaps: false sees no gap and no later patch is renumbered (destructive)')
121
+ .option('--reason <text>', 'Rationale recorded on the retired slot')
122
+ .option('--dry-run', 'Show what would happen without writing')
123
+ .option('-y, --yes', 'Skip confirmation prompt (required for non-TTY)')
124
+ .option('--force-unsafe', 'Bypass the refusal when a later patch depends on this patch (last resort)');
125
+ addWaitLockOption(command).action(withErrorHandling(async (name, options) => {
126
+ await patchRetireCommand(getProjectRoot(), name, pickDefined(options));
127
+ }));
128
+ }
@@ -11,6 +11,7 @@ import type { PatchStagedForwardImport } from '../../types/commands/index.js';
11
11
  import type { PatchCategory, PatchesManifest, PatchMetadata } from '../../types/commands/index.js';
12
12
  import type { FireForgeConfig } from '../../types/config.js';
13
13
  import { type PlacementPlan } from '../export-flow.js';
14
+ import { type SplitStagedMaintenance } from './split-staged.js';
14
15
  /** Everything the commit step needs, computed and confirmed up front. */
15
16
  export interface SplitPlan {
16
17
  source: PatchMetadata;
@@ -38,6 +39,11 @@ export interface SplitPlan {
38
39
  * and persisted on commit so the real per-patch gate stays clean.
39
40
  */
40
41
  stagedDependencyAdditions: Map<string, PatchStagedForwardImport[]>;
42
+ /**
43
+ * Declarations the move re-homes on the new patch or drops as made
44
+ * unused. Populated by {@link runProjectedSplitLint}.
45
+ */
46
+ stagedMaintenance: SplitStagedMaintenance;
41
47
  }
42
48
  /**
43
49
  * Refuses the split unless the source patch currently owns every requested
@@ -51,13 +57,6 @@ export declare function assertSourceOwnsFiles(source: PatchMetadata, files: read
51
57
  * source patch so hunk context is correct for the new file set.
52
58
  */
53
59
  export declare function buildSplitDiff(engineDir: string, files: readonly string[], label: string, sourceFilename: string): Promise<string>;
54
- /**
55
- * Finds patches declaring a staged-dependency forward-import whose `owner`
56
- * is the source patch and whose `creates` path moves to the new patch.
57
- */
58
- export declare function findOwnerRewriteHolders(patches: readonly PatchMetadata[], sourceFilename: string, movedSet: ReadonlySet<string>): string[];
59
- /** Rewrites split-affected owners on one manifest row. */
60
- export declare function rewriteSplitOwners(patch: PatchMetadata, sourceFilename: string, movedSet: ReadonlySet<string>, newFilename: string): PatchMetadata;
61
60
  /** Merges `decls` into a patch's `stagedDependencies.forwardImports` (no duplicates). */
62
61
  export declare function mergeStagedForwardImports(patch: PatchMetadata, decls: readonly PatchStagedForwardImport[]): PatchMetadata;
63
62
  /**
@@ -70,6 +69,7 @@ export declare function mergeStagedForwardImports(patch: PatchMetadata, decls: r
70
69
  export declare function runProjectedSplitLint(plan: SplitPlan, baseCtx: PatchQueueContext): {
71
70
  conflicts: ConflictReport | null;
72
71
  stagedDependencyAdditions: Map<string, PatchStagedForwardImport[]>;
72
+ stagedMaintenance: SplitStagedMaintenance;
73
73
  };
74
74
  /** Builds the projected manifest for policy enforcement. */
75
75
  export declare function projectSplitManifest(manifest: {
@@ -20,6 +20,7 @@ import { pathExists } from '../../utils/fs.js';
20
20
  import { warn } from '../../utils/logger.js';
21
21
  import {} from '../export-flow.js';
22
22
  import { projectEntryBody } from './entry-projection.js';
23
+ import { describeStagedMaintenance, findDeclarationsMadeUnused, keptRelocatedDeclarations, relocatedSourceDeclarations, rewriteSplitOwners, withoutRelocatedDeclarations, } from './split-staged.js';
23
24
  /**
24
25
  * Refuses the split unless the source patch currently owns every requested
25
26
  * file. Splitting out a file the source does not own would produce a new patch
@@ -58,29 +59,6 @@ export async function buildSplitDiff(engineDir, files, label, sourceFilename) {
58
59
  }
59
60
  return diff;
60
61
  }
61
- /**
62
- * Finds patches declaring a staged-dependency forward-import whose `owner`
63
- * is the source patch and whose `creates` path moves to the new patch.
64
- */
65
- export function findOwnerRewriteHolders(patches, sourceFilename, movedSet) {
66
- return patches
67
- .filter((patch) => (patch.stagedDependencies?.forwardImports ?? []).some((fi) => fi.owner === sourceFilename && movedSet.has(fi.creates)))
68
- .map((patch) => patch.filename);
69
- }
70
- /** Rewrites split-affected owners on one manifest row. */
71
- export function rewriteSplitOwners(patch, sourceFilename, movedSet, newFilename) {
72
- const forwardImports = patch.stagedDependencies?.forwardImports;
73
- if (!forwardImports?.some((fi) => fi.owner === sourceFilename && movedSet.has(fi.creates))) {
74
- return patch;
75
- }
76
- return {
77
- ...patch,
78
- stagedDependencies: {
79
- ...patch.stagedDependencies,
80
- forwardImports: forwardImports.map((fi) => fi.owner === sourceFilename && movedSet.has(fi.creates) ? { ...fi, owner: newFilename } : fi),
81
- },
82
- };
83
- }
84
62
  /** Builds the projected post-split queue entries (renumber + shrunken source + new patch). */
85
63
  function buildProjectedSplitEntries(baseCtx, plan) {
86
64
  const movedSet = new Set(plan.movedFiles);
@@ -90,6 +68,9 @@ function buildProjectedSplitEntries(baseCtx, plan) {
90
68
  if (metadata) {
91
69
  metadata = rewriteStagedDependencyOwners(metadata, ownerLookup);
92
70
  metadata = rewriteSplitOwners(metadata, plan.source.filename, movedSet, plan.placement.newFilename);
71
+ if (entry.filename === plan.source.filename) {
72
+ metadata = withoutRelocatedDeclarations(metadata, movedSet);
73
+ }
93
74
  }
94
75
  const rename = plan.placement.renameMap.get(entry.filename);
95
76
  const base = rename
@@ -99,10 +80,26 @@ function buildProjectedSplitEntries(baseCtx, plan) {
99
80
  return base;
100
81
  return { ...base, ...projectEntryBody(plan.remainingDiff) };
101
82
  });
83
+ // Declarations whose declaring file moved travel with it, so the new
84
+ // patch's projected entry carries them (and only then needs metadata).
85
+ const relocated = relocatedSourceDeclarations(plan.source, movedSet, ownerLookup);
86
+ const hasRelocated = (relocated.forwardImports?.length ?? 0) + (relocated.registrations?.length ?? 0) > 0;
102
87
  entries.push({
103
88
  filename: plan.placement.newFilename,
104
89
  order: plan.placement.insertionOrder,
105
- metadata: null,
90
+ metadata: hasRelocated
91
+ ? {
92
+ filename: plan.placement.newFilename,
93
+ order: plan.placement.insertionOrder,
94
+ category: plan.category,
95
+ name: plan.name,
96
+ description: plan.description,
97
+ createdAt: '',
98
+ sourceEsrVersion: plan.source.sourceEsrVersion,
99
+ filesAffected: plan.movedFiles,
100
+ stagedDependencies: relocated,
101
+ }
102
+ : null,
106
103
  ...projectEntryBody(plan.movedDiff),
107
104
  });
108
105
  entries.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
@@ -184,13 +181,29 @@ export function runProjectedSplitLint(plan, baseCtx) {
184
181
  // the new patch, then inject them before linting so they resolve.
185
182
  const stagedDependencyAdditions = computeSplitStagedDependencyAdditions(projectedEntries, plan.placement.newFilename);
186
183
  injectStagedDependencyAdditions(projectedEntries, stagedDependencyAdditions);
187
- const baselineIssues = lintPatchQueue(baseCtx).filter((i) => i.severity === 'error');
188
- const projectedIssues = lintPatchQueue({
184
+ const baselineAll = lintPatchQueue(baseCtx);
185
+ const projectedAll = lintPatchQueue({
189
186
  entries: projectedEntries,
190
187
  // Keep the projection patch-policy-aware whenever the baseline is, so
191
188
  // computeProjectedLintRegressions compares symmetric rule sets.
192
189
  ...(baseCtx.patchPolicy ? { patchPolicy: baseCtx.patchPolicy } : {}),
193
- }).filter((i) => i.severity === 'error');
190
+ });
191
+ const baselineIssues = baselineAll.filter((i) => i.severity === 'error');
192
+ const projectedIssues = projectedAll.filter((i) => i.severity === 'error');
193
+ const oldNameByNew = new Map([...plan.placement.renameMap].map(([oldName, entry]) => [entry.newFilename, oldName]));
194
+ const stagedMaintenance = {
195
+ relocated: relocatedSourceDeclarations(plan.source, new Set(plan.movedFiles), (old) => plan.placement.renameMap.get(old)?.newFilename),
196
+ dropped: findDeclarationsMadeUnused({
197
+ baselineEntries: baseCtx.entries,
198
+ projectedEntries,
199
+ baselineIssues: baselineAll,
200
+ projectedIssues: projectedAll,
201
+ sourceFilename: plan.source.filename,
202
+ newFilename: plan.placement.newFilename,
203
+ mapBack: (name) => oldNameByNew.get(name) ?? name,
204
+ movedSet: new Set(plan.movedFiles),
205
+ }),
206
+ };
194
207
  const regressions = computeProjectedLintRegressions(baselineIssues, projectedIssues);
195
208
  if (baselineIssues.length > 0 && regressions.length === 0) {
196
209
  warn(`Note: projected queue still has ${baselineIssues.length} pre-existing cross-patch ` +
@@ -202,7 +215,7 @@ export function runProjectedSplitLint(plan, baseCtx) {
202
215
  reason: `split would introduce ${regressions.length} cross-patch lint error(s)`,
203
216
  details: regressions.map(formatPatchLintIssue),
204
217
  };
205
- return { conflicts, stagedDependencyAdditions };
218
+ return { conflicts, stagedDependencyAdditions, stagedMaintenance };
206
219
  }
207
220
  /** Builds the projected manifest for policy enforcement. */
208
221
  export function projectSplitManifest(manifest, plan, newMetadata) {
@@ -222,6 +235,8 @@ export function projectSplitManifest(manifest, plan, newMetadata) {
222
235
  * source's provenance (product, version, category) onto the new patch.
223
236
  */
224
237
  export function buildNewPatchMetadata(plan, config) {
238
+ const staged = keptRelocatedDeclarations(plan.stagedMaintenance, plan.placement.newFilename);
239
+ const hasStaged = (staged.forwardImports?.length ?? 0) + (staged.registrations?.length ?? 0) > 0;
225
240
  return {
226
241
  filename: plan.placement.newFilename,
227
242
  order: plan.placement.insertionOrder,
@@ -231,6 +246,7 @@ export function buildNewPatchMetadata(plan, config) {
231
246
  createdAt: new Date().toISOString(),
232
247
  ...buildPatchSourceMetadata(config.firefox),
233
248
  filesAffected: plan.movedFiles,
249
+ ...(hasStaged ? { stagedDependencies: staged } : {}),
234
250
  };
235
251
  }
236
252
  /**
@@ -253,5 +269,6 @@ export function buildSplitSummary(plan) {
253
269
  if (plan.ownerRewrites.length > 0) {
254
270
  summary.push(`staged-dependency owners re-pointed to the new patch in: ${plan.ownerRewrites.join(', ')}`);
255
271
  }
272
+ summary.push(...describeStagedMaintenance(plan.stagedMaintenance, plan.source.filename, plan.placement.newFilename));
256
273
  return summary;
257
274
  }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Staged-dependency upkeep for `patch split` and `patch move-files
3
+ * --create`: the declarations a file move invalidates.
4
+ *
5
+ * Moving files into a new patch changes three kinds of declaration:
6
+ *
7
+ * - an `owner` that named the source for a `creates` path that moved now
8
+ * names the new patch (both kinds, see {@link rewriteSplitOwners});
9
+ * - a declaration the source holds for a site in a moved file belongs to
10
+ * the new patch, because the importing or registering line moved with
11
+ * the file ({@link relocatedSourceDeclarations});
12
+ * - a declaration that was in use before the move and is not after it
13
+ * (the new patch now applies before the importer, or the importer and
14
+ * the imported file landed in one patch) is dropped
15
+ * ({@link findDeclarationsMadeUnused}).
16
+ *
17
+ * Without this the move wrote the bodies and left the queue audit and the
18
+ * per-patch lint to report `queue-dependency-creates-unowned` and
19
+ * `staged-dependency-unused` afterwards, one `--remove`/`--add` pair per
20
+ * entry to repair by hand.
21
+ */
22
+ import type { PatchQueueEntry } from '../../core/patch-lint.js';
23
+ import type { PatchLintIssue, PatchMetadata, PatchStagedDependencies, PatchStagedForwardImport, PatchStagedRegistration } from '../../types/commands/index.js';
24
+ /** One declaration of either kind. */
25
+ export type StagedDeclaration = PatchStagedForwardImport | PatchStagedRegistration;
26
+ /** A declaration the move made unused, named by its post-move holder. */
27
+ export interface DroppedStagedDeclaration {
28
+ /** Holder patch filename after the move (and any renumber). */
29
+ holder: string;
30
+ /** The declaration as the projected queue holds it. */
31
+ declaration: StagedDeclaration;
32
+ }
33
+ /** Declaration changes a split or `move-files --create` commits. */
34
+ export interface SplitStagedMaintenance {
35
+ /** Source declarations whose declaring file moved, re-homed on the new patch. */
36
+ relocated: PatchStagedDependencies;
37
+ /** Declarations in use before the move and unused after it. */
38
+ dropped: DroppedStagedDeclaration[];
39
+ }
40
+ /** An empty maintenance plan. */
41
+ export declare function emptyStagedMaintenance(): SplitStagedMaintenance;
42
+ /**
43
+ * The source's declarations whose declaring file moves, with owners passed
44
+ * through `ownerLookup` (the renumber the placement implies).
45
+ *
46
+ * @param source - The source patch's current manifest row
47
+ * @param movedSet - Files moving to the new patch
48
+ * @param ownerLookup - Old → new filename for renumbered patches
49
+ */
50
+ export declare function relocatedSourceDeclarations(source: PatchMetadata, movedSet: ReadonlySet<string>, ownerLookup: (oldFilename: string) => string | undefined): PatchStagedDependencies;
51
+ /**
52
+ * Removes from the source row the declarations whose declaring file moved
53
+ * (they are re-homed on the new patch).
54
+ */
55
+ export declare function withoutRelocatedDeclarations(source: PatchMetadata, movedSet: ReadonlySet<string>): PatchMetadata;
56
+ /**
57
+ * Re-points `owner` from the source to the new patch on every declaration
58
+ * (forward-import and registration alike) whose `creates` path moved.
59
+ */
60
+ export declare function rewriteSplitOwners(patch: PatchMetadata, sourceFilename: string, movedSet: ReadonlySet<string>, newFilename: string): PatchMetadata;
61
+ /**
62
+ * Finds patches holding a declaration (either kind) whose `owner` is the
63
+ * source patch and whose `creates` path moves to the new patch.
64
+ */
65
+ export declare function findOwnerRewriteHolders(patches: readonly PatchMetadata[], sourceFilename: string, movedSet: ReadonlySet<string>): string[];
66
+ /** Inputs to {@link findDeclarationsMadeUnused}. */
67
+ export interface MadeUnusedInput {
68
+ /** Queue entries before the move. */
69
+ baselineEntries: readonly PatchQueueEntry[];
70
+ /** Queue entries after the move, relocations and owner rewrites applied. */
71
+ projectedEntries: readonly PatchQueueEntry[];
72
+ /** Every finding of the baseline queue lint (all severities). */
73
+ baselineIssues: readonly PatchLintIssue[];
74
+ /** Every finding of the projected queue lint (all severities). */
75
+ projectedIssues: readonly PatchLintIssue[];
76
+ /** Source patch filename before the move. */
77
+ sourceFilename: string;
78
+ /** The new patch's filename. */
79
+ newFilename: string;
80
+ /** Projected → pre-move filename for renumbered patches. */
81
+ mapBack: (projectedFilename: string) => string;
82
+ /** Files moving to the new patch. */
83
+ movedSet: ReadonlySet<string>;
84
+ }
85
+ /**
86
+ * Declarations the projected queue reports `staged-dependency-unused` for
87
+ * that the baseline queue did not: the ones the move itself made unused.
88
+ * Only declarations the move touches are candidates (their `creates` path
89
+ * moved, or they were re-homed on the new patch), so pre-existing stale
90
+ * declarations elsewhere are left for the operator.
91
+ */
92
+ export declare function findDeclarationsMadeUnused(input: MadeUnusedInput): DroppedStagedDeclaration[];
93
+ /**
94
+ * Removes the dropped declarations held by `patch` (matched by its
95
+ * post-move filename and the declaration's projected identity).
96
+ */
97
+ export declare function withoutDroppedDeclarations(patch: PatchMetadata, dropped: readonly DroppedStagedDeclaration[]): PatchMetadata;
98
+ /** The relocated declarations the new patch keeps (relocated minus dropped). */
99
+ export declare function keptRelocatedDeclarations(maintenance: SplitStagedMaintenance, newFilename: string): PatchStagedDependencies;
100
+ /**
101
+ * Operator-facing lines naming every declaration the move re-homes or
102
+ * drops. Empty when the move touches no declaration.
103
+ *
104
+ * @param maintenance - The planned changes
105
+ * @param sourceFilename - Source patch filename
106
+ * @param newFilename - The new patch's filename
107
+ */
108
+ export declare function describeStagedMaintenance(maintenance: SplitStagedMaintenance, sourceFilename: string, newFilename: string): string[];
@@ -0,0 +1,202 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Staged-dependency upkeep for `patch split` and `patch move-files
4
+ * --create`: the declarations a file move invalidates.
5
+ *
6
+ * Moving files into a new patch changes three kinds of declaration:
7
+ *
8
+ * - an `owner` that named the source for a `creates` path that moved now
9
+ * names the new patch (both kinds, see {@link rewriteSplitOwners});
10
+ * - a declaration the source holds for a site in a moved file belongs to
11
+ * the new patch, because the importing or registering line moved with
12
+ * the file ({@link relocatedSourceDeclarations});
13
+ * - a declaration that was in use before the move and is not after it
14
+ * (the new patch now applies before the importer, or the importer and
15
+ * the imported file landed in one patch) is dropped
16
+ * ({@link findDeclarationsMadeUnused}).
17
+ *
18
+ * Without this the move wrote the bodies and left the queue audit and the
19
+ * per-patch lint to report `queue-dependency-creates-unowned` and
20
+ * `staged-dependency-unused` afterwards, one `--remove`/`--add` pair per
21
+ * entry to repair by hand.
22
+ */
23
+ import { stagedDependencyUnusedFingerprint } from '../../core/patch-lint-staged-registration.js';
24
+ /** An empty maintenance plan. */
25
+ export function emptyStagedMaintenance() {
26
+ return { relocated: {}, dropped: [] };
27
+ }
28
+ function declarationsOf(staged) {
29
+ return [...(staged?.forwardImports ?? []), ...(staged?.registrations ?? [])];
30
+ }
31
+ function matcherOf(declaration) {
32
+ return 'specifier' in declaration ? `i:${declaration.specifier}` : `r:${declaration.line}`;
33
+ }
34
+ /** Identity of a declaration, owner included. */
35
+ function declarationKey(declaration) {
36
+ return [
37
+ declaration.file,
38
+ matcherOf(declaration),
39
+ declaration.creates,
40
+ declaration.owner ?? '',
41
+ ].join('\0');
42
+ }
43
+ /** Identity without the owner, which a renumber or split may rewrite. */
44
+ function declarationSiteKey(declaration) {
45
+ return [declaration.file, matcherOf(declaration), declaration.creates].join('\0');
46
+ }
47
+ function rebuildStaged(staged, keep, rewrite = (declaration) => declaration) {
48
+ const forwardImports = (staged?.forwardImports ?? [])
49
+ .filter(keep)
50
+ .map((entry) => rewrite(entry));
51
+ const registrations = (staged?.registrations ?? [])
52
+ .filter(keep)
53
+ .map((entry) => rewrite(entry));
54
+ return {
55
+ ...(forwardImports.length > 0 ? { forwardImports } : {}),
56
+ ...(registrations.length > 0 ? { registrations } : {}),
57
+ };
58
+ }
59
+ function hasDeclarations(staged) {
60
+ return declarationsOf(staged).length > 0;
61
+ }
62
+ /** Writes `staged` onto `patch`, dropping the field when it is empty. */
63
+ function withStaged(patch, staged) {
64
+ if (hasDeclarations(staged))
65
+ return { ...patch, stagedDependencies: staged };
66
+ if (patch.stagedDependencies === undefined)
67
+ return patch;
68
+ const rest = { ...patch };
69
+ delete rest.stagedDependencies;
70
+ return rest;
71
+ }
72
+ /**
73
+ * The source's declarations whose declaring file moves, with owners passed
74
+ * through `ownerLookup` (the renumber the placement implies).
75
+ *
76
+ * @param source - The source patch's current manifest row
77
+ * @param movedSet - Files moving to the new patch
78
+ * @param ownerLookup - Old → new filename for renumbered patches
79
+ */
80
+ export function relocatedSourceDeclarations(source, movedSet, ownerLookup) {
81
+ return rebuildStaged(source.stagedDependencies, (declaration) => movedSet.has(declaration.file), (declaration) => {
82
+ const owner = declaration.owner === undefined ? undefined : ownerLookup(declaration.owner);
83
+ return owner === undefined ? { ...declaration } : { ...declaration, owner };
84
+ });
85
+ }
86
+ /**
87
+ * Removes from the source row the declarations whose declaring file moved
88
+ * (they are re-homed on the new patch).
89
+ */
90
+ export function withoutRelocatedDeclarations(source, movedSet) {
91
+ if (!declarationsOf(source.stagedDependencies).some((d) => movedSet.has(d.file)))
92
+ return source;
93
+ return withStaged(source, rebuildStaged(source.stagedDependencies, (declaration) => !movedSet.has(declaration.file)));
94
+ }
95
+ /**
96
+ * Re-points `owner` from the source to the new patch on every declaration
97
+ * (forward-import and registration alike) whose `creates` path moved.
98
+ */
99
+ export function rewriteSplitOwners(patch, sourceFilename, movedSet, newFilename) {
100
+ const matches = (declaration) => declaration.owner === sourceFilename && movedSet.has(declaration.creates);
101
+ if (!declarationsOf(patch.stagedDependencies).some(matches))
102
+ return patch;
103
+ return {
104
+ ...patch,
105
+ stagedDependencies: {
106
+ ...patch.stagedDependencies,
107
+ ...rebuildStaged(patch.stagedDependencies, () => true, (declaration) => matches(declaration) ? { ...declaration, owner: newFilename } : declaration),
108
+ },
109
+ };
110
+ }
111
+ /**
112
+ * Finds patches holding a declaration (either kind) whose `owner` is the
113
+ * source patch and whose `creates` path moves to the new patch.
114
+ */
115
+ export function findOwnerRewriteHolders(patches, sourceFilename, movedSet) {
116
+ return patches
117
+ .filter((patch) => declarationsOf(patch.stagedDependencies).some((declaration) => declaration.owner === sourceFilename && movedSet.has(declaration.creates)))
118
+ .map((patch) => patch.filename);
119
+ }
120
+ /**
121
+ * Declarations the projected queue reports `staged-dependency-unused` for
122
+ * that the baseline queue did not: the ones the move itself made unused.
123
+ * Only declarations the move touches are candidates (their `creates` path
124
+ * moved, or they were re-homed on the new patch), so pre-existing stale
125
+ * declarations elsewhere are left for the operator.
126
+ */
127
+ export function findDeclarationsMadeUnused(input) {
128
+ const unusedFingerprints = (issues) => new Set(issues
129
+ .filter((issue) => issue.check === 'staged-dependency-unused')
130
+ .map((issue) => issue.fingerprint ?? ''));
131
+ const baselineUnused = unusedFingerprints(input.baselineIssues);
132
+ const projectedUnused = unusedFingerprints(input.projectedIssues);
133
+ const baselineByHolder = new Map(input.baselineEntries.map((entry) => [entry.filename, entry.metadata]));
134
+ const dropped = [];
135
+ for (const entry of input.projectedEntries) {
136
+ const isNewPatch = entry.filename === input.newFilename;
137
+ for (const declaration of declarationsOf(entry.metadata?.stagedDependencies)) {
138
+ if (!isNewPatch && !input.movedSet.has(declaration.creates))
139
+ continue;
140
+ if (!projectedUnused.has(stagedDependencyUnusedFingerprint(entry.filename, declaration))) {
141
+ continue;
142
+ }
143
+ const originalHolder = isNewPatch ? input.sourceFilename : input.mapBack(entry.filename);
144
+ const original = declarationsOf(baselineByHolder.get(originalHolder)?.stagedDependencies).find((candidate) => declarationSiteKey(candidate) === declarationSiteKey(declaration));
145
+ if (original !== undefined &&
146
+ baselineUnused.has(stagedDependencyUnusedFingerprint(originalHolder, original))) {
147
+ continue;
148
+ }
149
+ dropped.push({ holder: entry.filename, declaration });
150
+ }
151
+ }
152
+ return dropped;
153
+ }
154
+ /**
155
+ * Removes the dropped declarations held by `patch` (matched by its
156
+ * post-move filename and the declaration's projected identity).
157
+ */
158
+ export function withoutDroppedDeclarations(patch, dropped) {
159
+ const keys = new Set(dropped
160
+ .filter((entry) => entry.holder === patch.filename)
161
+ .map((entry) => declarationKey(entry.declaration)));
162
+ if (keys.size === 0)
163
+ return patch;
164
+ return withStaged(patch, rebuildStaged(patch.stagedDependencies, (declaration) => !keys.has(declarationKey(declaration))));
165
+ }
166
+ /** The relocated declarations the new patch keeps (relocated minus dropped). */
167
+ export function keptRelocatedDeclarations(maintenance, newFilename) {
168
+ const keys = new Set(maintenance.dropped
169
+ .filter((entry) => entry.holder === newFilename)
170
+ .map((entry) => declarationKey(entry.declaration)));
171
+ return rebuildStaged(maintenance.relocated, (declaration) => !keys.has(declarationKey(declaration)));
172
+ }
173
+ function describeDeclaration(declaration) {
174
+ return 'specifier' in declaration
175
+ ? `${declaration.file} imports "${declaration.specifier}" (${declaration.creates})`
176
+ : `${declaration.file} registers "${declaration.line}" (${declaration.creates})`;
177
+ }
178
+ /**
179
+ * Operator-facing lines naming every declaration the move re-homes or
180
+ * drops. Empty when the move touches no declaration.
181
+ *
182
+ * @param maintenance - The planned changes
183
+ * @param sourceFilename - Source patch filename
184
+ * @param newFilename - The new patch's filename
185
+ */
186
+ export function describeStagedMaintenance(maintenance, sourceFilename, newFilename) {
187
+ const lines = [];
188
+ const kept = declarationsOf(keptRelocatedDeclarations(maintenance, newFilename));
189
+ if (kept.length > 0) {
190
+ lines.push(`staged-dependency declarations moved from ${sourceFilename} to ${newFilename} (${kept.length}):`);
191
+ for (const declaration of kept)
192
+ lines.push(` ${describeDeclaration(declaration)}`);
193
+ }
194
+ if (maintenance.dropped.length > 0) {
195
+ lines.push(`staged-dependency declarations dropped because the move made them unused (${maintenance.dropped.length}):`);
196
+ for (const { holder, declaration } of maintenance.dropped) {
197
+ const shownHolder = holder === newFilename ? `${sourceFilename} → ${newFilename}` : holder;
198
+ lines.push(` ${shownHolder}: ${describeDeclaration(declaration)}`);
199
+ }
200
+ }
201
+ return lines;
202
+ }
@@ -19,6 +19,11 @@ import type { CommandContext } from '../../types/cli.js';
19
19
  import type { PatchMetadata, PatchSplitOptions } from '../../types/commands/index.js';
20
20
  import type { FireForgeConfig } from '../../types/config.js';
21
21
  import { type SplitPlan } from './split-plan.js';
22
+ /**
23
+ * Prints the owner rewrites and the declarations the move re-homed or
24
+ * dropped, after a committed split or `move-files --create`.
25
+ */
26
+ export declare function reportSplitStagedChanges(plan: SplitPlan): void;
22
27
  /**
23
28
  * Commits a confirmed split under the patch directory lock: renumber →
24
29
  * write new patch body → write shrunken source body → single manifest