@hominis/fireforge 0.49.1 → 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 (336) hide show
  1. package/CHANGELOG.md +41 -0
  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-templates.d.ts +8 -3
  35. package/dist/src/commands/furnace/create-templates.js +8 -23
  36. package/dist/src/commands/furnace/create.js +2 -2
  37. package/dist/src/commands/furnace/deploy-named.d.ts +48 -0
  38. package/dist/src/commands/furnace/deploy-named.js +107 -0
  39. package/dist/src/commands/furnace/deploy.d.ts +3 -2
  40. package/dist/src/commands/furnace/deploy.js +50 -86
  41. package/dist/src/commands/furnace/diff.js +26 -3
  42. package/dist/src/commands/furnace/index.js +66 -41
  43. package/dist/src/commands/furnace/init.js +13 -6
  44. package/dist/src/commands/furnace/override.js +4 -2
  45. package/dist/src/commands/furnace/preview.js +4 -3
  46. package/dist/src/commands/furnace/refresh.js +2 -2
  47. package/dist/src/commands/furnace/remove.js +4 -3
  48. package/dist/src/commands/furnace/rename-dry-run.d.ts +29 -0
  49. package/dist/src/commands/furnace/rename-dry-run.js +127 -0
  50. package/dist/src/commands/furnace/rename-helpers.d.ts +7 -0
  51. package/dist/src/commands/furnace/rename-helpers.js +15 -1
  52. package/dist/src/commands/furnace/rename-registrations.d.ts +41 -0
  53. package/dist/src/commands/furnace/rename-registrations.js +93 -0
  54. package/dist/src/commands/furnace/rename.d.ts +2 -1
  55. package/dist/src/commands/furnace/rename.js +28 -66
  56. package/dist/src/commands/furnace/scan.d.ts +9 -4
  57. package/dist/src/commands/furnace/scan.js +11 -9
  58. package/dist/src/commands/furnace/sync.js +3 -2
  59. package/dist/src/commands/furnace/validate-report.d.ts +45 -0
  60. package/dist/src/commands/furnace/validate-report.js +91 -0
  61. package/dist/src/commands/furnace/validate.d.ts +1 -1
  62. package/dist/src/commands/furnace/validate.js +127 -114
  63. package/dist/src/commands/furnace/validation-output.js +14 -3
  64. package/dist/src/commands/lint-per-patch.js +31 -60
  65. package/dist/src/commands/lint-report.d.ts +8 -4
  66. package/dist/src/commands/lint-report.js +15 -5
  67. package/dist/src/commands/lint.js +15 -34
  68. package/dist/src/commands/manifest.js +2 -0
  69. package/dist/src/commands/patch/delete-gap-notice.js +3 -1
  70. package/dist/src/commands/patch/delete.d.ts +1 -1
  71. package/dist/src/commands/patch/delete.js +13 -98
  72. package/dist/src/commands/patch/index.d.ts +1 -1
  73. package/dist/src/commands/patch/index.js +4 -2
  74. package/dist/src/commands/patch/lint-ignore.d.ts +5 -1
  75. package/dist/src/commands/patch/lint-ignore.js +65 -28
  76. package/dist/src/commands/patch/move-files-create.js +26 -17
  77. package/dist/src/commands/patch/move-files-into.js +5 -3
  78. package/dist/src/commands/patch/move-files.js +3 -1
  79. package/dist/src/commands/patch/removal-dependents.d.ts +63 -0
  80. package/dist/src/commands/patch/removal-dependents.js +133 -0
  81. package/dist/src/commands/patch/rename.js +7 -0
  82. package/dist/src/commands/patch/retire.d.ts +34 -0
  83. package/dist/src/commands/patch/retire.js +128 -0
  84. package/dist/src/commands/patch/split-plan.d.ts +7 -7
  85. package/dist/src/commands/patch/split-plan.js +45 -28
  86. package/dist/src/commands/patch/split-staged.d.ts +108 -0
  87. package/dist/src/commands/patch/split-staged.js +202 -0
  88. package/dist/src/commands/patch/split.d.ts +5 -0
  89. package/dist/src/commands/patch/split.js +51 -15
  90. package/dist/src/commands/patch/staged-dependency-batch.d.ts +48 -0
  91. package/dist/src/commands/patch/staged-dependency-batch.js +176 -0
  92. package/dist/src/commands/patch/staged-dependency-entries.d.ts +36 -0
  93. package/dist/src/commands/patch/staged-dependency-entries.js +52 -0
  94. package/dist/src/commands/patch/staged-dependency-from.d.ts +14 -0
  95. package/dist/src/commands/patch/staged-dependency-from.js +108 -0
  96. package/dist/src/commands/patch/staged-dependency.js +14 -41
  97. package/dist/src/commands/re-export-adjacent-registrations.d.ts +20 -0
  98. package/dist/src/commands/re-export-adjacent-registrations.js +75 -0
  99. package/dist/src/commands/re-export-adjacent.d.ts +45 -2
  100. package/dist/src/commands/re-export-adjacent.js +156 -37
  101. package/dist/src/commands/re-export-drift.d.ts +36 -8
  102. package/dist/src/commands/re-export-drift.js +73 -49
  103. package/dist/src/commands/re-export-files.js +11 -8
  104. package/dist/src/commands/re-export-lint.d.ts +2 -1
  105. package/dist/src/commands/re-export-lint.js +23 -10
  106. package/dist/src/commands/re-export-options.js +45 -4
  107. package/dist/src/commands/re-export-preflight.d.ts +23 -0
  108. package/dist/src/commands/re-export-preflight.js +160 -0
  109. package/dist/src/commands/re-export-register.js +42 -17
  110. package/dist/src/commands/re-export-removed.d.ts +12 -1
  111. package/dist/src/commands/re-export-removed.js +41 -11
  112. package/dist/src/commands/re-export-scan-owner.d.ts +44 -0
  113. package/dist/src/commands/re-export-scan-owner.js +116 -0
  114. package/dist/src/commands/re-export-scan.d.ts +17 -9
  115. package/dist/src/commands/re-export-scan.js +56 -23
  116. package/dist/src/commands/re-export-single.js +22 -6
  117. package/dist/src/commands/re-export-summary.d.ts +28 -0
  118. package/dist/src/commands/re-export-summary.js +36 -0
  119. package/dist/src/commands/re-export.js +37 -24
  120. package/dist/src/commands/rebase/index.js +17 -17
  121. package/dist/src/commands/run.js +16 -2
  122. package/dist/src/commands/status-check.d.ts +55 -16
  123. package/dist/src/commands/status-check.js +92 -37
  124. package/dist/src/commands/status-json.d.ts +22 -9
  125. package/dist/src/commands/status-json.js +47 -32
  126. package/dist/src/commands/status-output.js +3 -1
  127. package/dist/src/commands/status-scope.d.ts +37 -0
  128. package/dist/src/commands/status-scope.js +54 -0
  129. package/dist/src/commands/status.js +66 -23
  130. package/dist/src/commands/test-browser-preflight.js +19 -3
  131. package/dist/src/commands/test-build-furnace.d.ts +21 -0
  132. package/dist/src/commands/test-build-furnace.js +48 -0
  133. package/dist/src/commands/test-build-phase.js +47 -41
  134. package/dist/src/commands/test-checkjs.js +7 -0
  135. package/dist/src/commands/test-diagnose.js +7 -0
  136. package/dist/src/commands/test-modes.d.ts +16 -1
  137. package/dist/src/commands/test-modes.js +24 -1
  138. package/dist/src/commands/test-multi-verdict.d.ts +61 -0
  139. package/dist/src/commands/test-multi-verdict.js +111 -0
  140. package/dist/src/commands/test-orchestrate.d.ts +43 -0
  141. package/dist/src/commands/test-orchestrate.js +222 -0
  142. package/dist/src/commands/test-perf-scope.d.ts +29 -0
  143. package/dist/src/commands/test-perf-scope.js +83 -0
  144. package/dist/src/commands/test-prelock.d.ts +51 -0
  145. package/dist/src/commands/test-prelock.js +177 -0
  146. package/dist/src/commands/test-register.js +54 -10
  147. package/dist/src/commands/test-run.js +4 -1
  148. package/dist/src/commands/test-stale-gate.js +57 -27
  149. package/dist/src/commands/test-verdict.d.ts +10 -1
  150. package/dist/src/commands/test-verdict.js +28 -1
  151. package/dist/src/commands/test.d.ts +6 -1
  152. package/dist/src/commands/test.js +66 -60
  153. package/dist/src/commands/token-lint.d.ts +14 -0
  154. package/dist/src/commands/token-lint.js +111 -0
  155. package/dist/src/commands/token.js +10 -0
  156. package/dist/src/commands/verify-scope.d.ts +28 -0
  157. package/dist/src/commands/verify-scope.js +67 -0
  158. package/dist/src/commands/verify.d.ts +10 -2
  159. package/dist/src/commands/verify.js +59 -35
  160. package/dist/src/core/build-baseline-types.d.ts +46 -0
  161. package/dist/src/core/build-baseline-validate.js +19 -0
  162. package/dist/src/core/build-baseline.d.ts +5 -1
  163. package/dist/src/core/build-baseline.js +3 -1
  164. package/dist/src/core/build-overwrite-guard.d.ts +50 -0
  165. package/dist/src/core/build-overwrite-guard.js +84 -4
  166. package/dist/src/core/build-prepare.d.ts +7 -0
  167. package/dist/src/core/build-prepare.js +14 -8
  168. package/dist/src/core/build-run-record.d.ts +74 -0
  169. package/dist/src/core/build-run-record.js +136 -0
  170. package/dist/src/core/checkjs-support-heads.d.ts +41 -0
  171. package/dist/src/core/checkjs-support-heads.js +104 -0
  172. package/dist/src/core/config-paths.d.ts +2 -2
  173. package/dist/src/core/config-paths.js +8 -0
  174. package/dist/src/core/config-validate-check.d.ts +8 -0
  175. package/dist/src/core/config-validate-check.js +143 -0
  176. package/dist/src/core/config-validate-file-size.d.ts +9 -6
  177. package/dist/src/core/config-validate-file-size.js +91 -2
  178. package/dist/src/core/config-validate-history.d.ts +11 -0
  179. package/dist/src/core/config-validate-history.js +38 -0
  180. package/dist/src/core/config-validate-patch-policy.js +100 -0
  181. package/dist/src/core/config-validate-test-toolchains.js +7 -0
  182. package/dist/src/core/config-validate-token-scales.d.ts +6 -0
  183. package/dist/src/core/config-validate-token-scales.js +50 -0
  184. package/dist/src/core/config-validate.js +14 -5
  185. package/dist/src/core/config.d.ts +5 -0
  186. package/dist/src/core/config.js +16 -0
  187. package/dist/src/core/coverage-dispatch.d.ts +24 -0
  188. package/dist/src/core/coverage-dispatch.js +44 -0
  189. package/dist/src/core/coverage-extend.d.ts +70 -45
  190. package/dist/src/core/coverage-extend.js +199 -144
  191. package/dist/src/core/coverage-records.d.ts +97 -0
  192. package/dist/src/core/coverage-records.js +350 -0
  193. package/dist/src/core/destructive.d.ts +7 -4
  194. package/dist/src/core/destructive.js +10 -10
  195. package/dist/src/core/engine-session-lock.d.ts +10 -1
  196. package/dist/src/core/engine-session-lock.js +28 -8
  197. package/dist/src/core/export-moved-code-shape.d.ts +49 -11
  198. package/dist/src/core/export-moved-code-shape.js +135 -44
  199. package/dist/src/core/file-lock-queue.d.ts +67 -0
  200. package/dist/src/core/file-lock-queue.js +221 -0
  201. package/dist/src/core/file-lock.d.ts +4 -25
  202. package/dist/src/core/file-lock.js +105 -138
  203. package/dist/src/core/furnace-apply-output.d.ts +10 -0
  204. package/dist/src/core/furnace-apply-output.js +31 -1
  205. package/dist/src/core/furnace-apply.js +12 -2
  206. package/dist/src/core/furnace-composed-source.d.ts +76 -0
  207. package/dist/src/core/furnace-composed-source.js +167 -0
  208. package/dist/src/core/furnace-config-validate-exceptions.d.ts +9 -0
  209. package/dist/src/core/furnace-config-validate-exceptions.js +71 -0
  210. package/dist/src/core/furnace-config-validate.js +8 -0
  211. package/dist/src/core/furnace-css-fragments.d.ts +10 -0
  212. package/dist/src/core/furnace-css-fragments.js +26 -0
  213. package/dist/src/core/furnace-operation.d.ts +3 -3
  214. package/dist/src/core/furnace-registration-remove.d.ts +22 -1
  215. package/dist/src/core/furnace-registration-remove.js +53 -17
  216. package/dist/src/core/furnace-registration-rename.d.ts +26 -0
  217. package/dist/src/core/furnace-registration-rename.js +77 -0
  218. package/dist/src/core/furnace-registration.d.ts +14 -4
  219. package/dist/src/core/furnace-registration.js +44 -10
  220. package/dist/src/core/furnace-staleness.d.ts +7 -0
  221. package/dist/src/core/furnace-staleness.js +34 -24
  222. package/dist/src/core/furnace-validate-accessibility.d.ts +4 -2
  223. package/dist/src/core/furnace-validate-accessibility.js +10 -1
  224. package/dist/src/core/furnace-validate-exceptions.d.ts +40 -0
  225. package/dist/src/core/furnace-validate-exceptions.js +43 -0
  226. package/dist/src/core/furnace-validate-icon-button.d.ts +7 -0
  227. package/dist/src/core/furnace-validate-icon-button.js +49 -0
  228. package/dist/src/core/furnace-validate.js +1 -1
  229. package/dist/src/core/harness-port-fallback.d.ts +60 -0
  230. package/dist/src/core/harness-port-fallback.js +96 -0
  231. package/dist/src/core/history-log-file.d.ts +61 -0
  232. package/dist/src/core/history-log-file.js +157 -0
  233. package/dist/src/core/history-log.d.ts +2 -1
  234. package/dist/src/core/history-log.js +2 -1
  235. package/dist/src/core/json-preserving-edit.d.ts +22 -0
  236. package/dist/src/core/json-preserving-edit.js +199 -0
  237. package/dist/src/core/mach.js +8 -1
  238. package/dist/src/core/negative-control-diff.d.ts +39 -0
  239. package/dist/src/core/negative-control-diff.js +136 -0
  240. package/dist/src/core/negative-control.d.ts +138 -0
  241. package/dist/src/core/negative-control.js +356 -0
  242. package/dist/src/core/objdir-install-manifests.d.ts +73 -0
  243. package/dist/src/core/objdir-install-manifests.js +226 -0
  244. package/dist/src/core/patch-export-update.d.ts +2 -0
  245. package/dist/src/core/patch-export-update.js +1 -0
  246. package/dist/src/core/patch-export.js +3 -4
  247. package/dist/src/core/patch-lint-checkjs-any-import.d.ts +37 -0
  248. package/dist/src/core/patch-lint-checkjs-any-import.js +61 -0
  249. package/dist/src/core/patch-lint-checkjs.js +8 -11
  250. package/dist/src/core/patch-lint-code-lines.d.ts +59 -0
  251. package/dist/src/core/patch-lint-code-lines.js +105 -0
  252. package/dist/src/core/patch-lint-cross.js +12 -4
  253. package/dist/src/core/patch-lint-css.d.ts +8 -2
  254. package/dist/src/core/patch-lint-css.js +10 -4
  255. package/dist/src/core/patch-lint-file-size.d.ts +12 -5
  256. package/dist/src/core/patch-lint-file-size.js +22 -11
  257. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +18 -0
  258. package/dist/src/core/patch-lint-forward-registration-extended.js +35 -10
  259. package/dist/src/core/patch-lint-forward-registration.js +10 -0
  260. package/dist/src/core/patch-lint-ignore.d.ts +152 -0
  261. package/dist/src/core/patch-lint-ignore.js +299 -0
  262. package/dist/src/core/patch-lint-size-thresholds.d.ts +52 -0
  263. package/dist/src/core/patch-lint-size-thresholds.js +110 -0
  264. package/dist/src/core/patch-lint-size.d.ts +117 -0
  265. package/dist/src/core/patch-lint-size.js +323 -0
  266. package/dist/src/core/patch-lint-staged-registration.d.ts +18 -1
  267. package/dist/src/core/patch-lint-staged-registration.js +20 -0
  268. package/dist/src/core/patch-lint-token-scale.d.ts +39 -0
  269. package/dist/src/core/patch-lint-token-scale.js +109 -0
  270. package/dist/src/core/patch-lint.d.ts +11 -83
  271. package/dist/src/core/patch-lint.js +64 -286
  272. package/dist/src/core/patch-lock.js +7 -6
  273. package/dist/src/core/patch-manifest-consistency.js +7 -6
  274. package/dist/src/core/patch-manifest-io.d.ts +6 -3
  275. package/dist/src/core/patch-manifest-io.js +19 -8
  276. package/dist/src/core/patch-manifest-validate.js +34 -2
  277. package/dist/src/core/patch-order-legal.d.ts +25 -0
  278. package/dist/src/core/patch-order-legal.js +83 -0
  279. package/dist/src/core/patch-policy-content.d.ts +40 -0
  280. package/dist/src/core/patch-policy-content.js +216 -0
  281. package/dist/src/core/patch-policy.d.ts +16 -3
  282. package/dist/src/core/patch-policy.js +16 -9
  283. package/dist/src/core/patch-retired.d.ts +35 -0
  284. package/dist/src/core/patch-retired.js +54 -0
  285. package/dist/src/core/patch-subset.d.ts +16 -0
  286. package/dist/src/core/patch-subset.js +66 -0
  287. package/dist/src/core/preflight-wait.d.ts +20 -0
  288. package/dist/src/core/preflight-wait.js +38 -0
  289. package/dist/src/core/rebase-from-version.d.ts +46 -0
  290. package/dist/src/core/rebase-from-version.js +71 -0
  291. package/dist/src/core/run-log.d.ts +20 -0
  292. package/dist/src/core/run-log.js +89 -3
  293. package/dist/src/core/run-profile.d.ts +7 -0
  294. package/dist/src/core/run-profile.js +7 -1
  295. package/dist/src/core/smoke-patterns.d.ts +5 -0
  296. package/dist/src/core/smoke-patterns.js +13 -0
  297. package/dist/src/core/test-harness-crash.d.ts +18 -1
  298. package/dist/src/core/test-harness-crash.js +6 -4
  299. package/dist/src/core/test-harness-signature.d.ts +17 -0
  300. package/dist/src/core/test-harness-signature.js +4 -0
  301. package/dist/src/core/test-run-completeness.d.ts +57 -0
  302. package/dist/src/core/test-run-completeness.js +144 -0
  303. package/dist/src/core/test-stale-check.d.ts +27 -1
  304. package/dist/src/core/test-stale-check.js +39 -49
  305. package/dist/src/core/test-stale-exempt.d.ts +40 -0
  306. package/dist/src/core/test-stale-exempt.js +70 -0
  307. package/dist/src/core/token-lint.d.ts +50 -0
  308. package/dist/src/core/token-lint.js +126 -0
  309. package/dist/src/core/tree-guard.js +3 -0
  310. package/dist/src/core/typecheck-shim.d.ts +1 -1
  311. package/dist/src/core/typecheck-shim.js +90 -2
  312. package/dist/src/index.d.ts +8 -3
  313. package/dist/src/index.js +3 -1
  314. package/dist/src/types/commands/index.d.ts +2 -2
  315. package/dist/src/types/commands/options.d.ts +148 -21
  316. package/dist/src/types/commands/patches.d.ts +75 -1
  317. package/dist/src/types/config.d.ts +192 -0
  318. package/dist/src/types/config.js +9 -0
  319. package/dist/src/types/furnace.d.ts +43 -1
  320. package/dist/src/types/furnace.js +7 -0
  321. package/dist/src/types/index.d.ts +1 -1
  322. package/dist/src/utils/options.d.ts +34 -6
  323. package/dist/src/utils/options.js +86 -5
  324. package/dist/src/utils/path-glob.d.ts +15 -0
  325. package/dist/src/utils/path-glob.js +47 -0
  326. package/dist/src/utils/regex.d.ts +15 -3
  327. package/dist/src/utils/regex.js +37 -16
  328. package/docs/configuration.md +282 -9
  329. package/docs/exit-codes.md +18 -1
  330. package/docs/furnace.md +97 -3
  331. package/docs/lifecycle-invariants.md +52 -19
  332. package/docs/machine-output.md +74 -12
  333. package/docs/patch-workflow.md +297 -22
  334. package/docs/run-logs.md +20 -1
  335. package/docs/testing.md +276 -20
  336. package/package.json +2 -2
@@ -12,19 +12,25 @@
12
12
  * re-export's engine read + diff regeneration roundtrip is wasted.
13
13
  *
14
14
  * Modes are mutually exclusive: exactly one of `--add`, `--remove`, or
15
- * `--clear` must be supplied per invocation. The read-modify-write
15
+ * `--clear` must be supplied per invocation. `--add <rule> --max <n>
16
+ * --reason <text>` writes a size waiver with a ceiling (see
17
+ * `core/patch-lint-ignore.ts`) instead of a bare check ID; an `--add` for a
18
+ * rule that already has an entry replaces it, so the same command moves a
19
+ * ceiling or widens it back to a bare waiver. The read-modify-write
16
20
  * happens inside the patch directory lock via {@link mutatePatchMetadata}
17
21
  * so a concurrent writer cannot interleave between the read and the
18
22
  * write, which matters when an operator scripts repeated invocations or
19
23
  * runs `--add` and `--remove` back-to-back.
20
24
  */
21
- import { Command } from 'commander';
25
+ import { Command, Option } from 'commander';
22
26
  import { confirmDestructive } from '../../core/destructive.js';
23
27
  import { appendHistoryBestEffort } from '../../core/history-log.js';
24
28
  import { mutatePatchMetadata } from '../../core/patch-export.js';
29
+ import { addedLintIgnoreEntries, formatLintIgnoreEntry, formatLintIgnoreList, LINT_IGNORE_CEILING_RULES, lintIgnoreRule, removeLintIgnoreRules, upsertLintIgnoreEntries, } from '../../core/patch-lint-ignore.js';
25
30
  import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
26
31
  import { info, intro, outro, warn } from '../../utils/logger.js';
27
- import { addWaitLockOption, resolveWaitLockSeconds, stringListOption, } from '../../utils/options.js';
32
+ import { addWaitLockOption, commanderArgParser, resolveWaitLockSeconds, stringListOption, } from '../../utils/options.js';
33
+ import { parsePositiveIntegerFlag } from '../../utils/validation.js';
28
34
  import { proceedAfterDecision } from '../destructive-decision.js';
29
35
  import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
30
36
  /**
@@ -44,19 +50,10 @@ const LINT_IGNORE_REVIEW_WARNING = "This lint waiver is subject to the project's
44
50
  * present" contract).
45
51
  */
46
52
  function applyMode(existing, mode, values) {
47
- const existingSet = new Set(existing);
48
- if (mode === 'add') {
49
- for (const v of values)
50
- existingSet.add(v);
51
- const merged = [...existingSet];
52
- return merged.length > 0 ? merged : undefined;
53
- }
54
- if (mode === 'remove') {
55
- for (const v of values)
56
- existingSet.delete(v);
57
- const remaining = [...existingSet];
58
- return remaining.length > 0 ? remaining : undefined;
59
- }
53
+ if (mode === 'add')
54
+ return upsertLintIgnoreEntries(existing, values);
55
+ if (mode === 'remove')
56
+ return removeLintIgnoreRules(existing, values.map(lintIgnoreRule));
60
57
  // mode === 'clear'
61
58
  return undefined;
62
59
  }
@@ -66,29 +63,62 @@ function applyMode(existing, mode, values) {
66
63
  * message format directly without mocking the logger transport.
67
64
  */
68
65
  function describeChange(before, after, mode, values) {
69
- const beforeSet = new Set(before);
70
- const afterSet = new Set(after);
71
66
  if (mode === 'clear') {
72
67
  return before.length === 0
73
68
  ? 'lintIgnore was already empty — no change'
74
- : `lintIgnore cleared (was ${before.join(', ')})`;
69
+ : `lintIgnore cleared (was ${formatLintIgnoreList(before)})`;
75
70
  }
76
- const currentLabel = before.length > 0 ? `[${before.join(', ')}]` : '(empty)';
71
+ const currentLabel = before.length > 0 ? `[${formatLintIgnoreList(before)}]` : '(empty)';
77
72
  if (mode === 'add') {
78
- const added = values.filter((v) => !beforeSet.has(v));
73
+ const added = addedLintIgnoreEntries(before, after);
79
74
  if (added.length === 0) {
80
75
  // Surface the existing list so a no-op `--add` does not require a
81
76
  // follow-up `patches.json` read to confirm what was already present.
82
77
  return `lintIgnore unchanged (current: ${currentLabel}; all requested IDs already present)`;
83
78
  }
84
- return `lintIgnore += ${added.join(', ')} → ${[...afterSet].join(', ') || '(empty)'}`;
79
+ return `lintIgnore += ${formatLintIgnoreList(added)} → ${formatLintIgnoreList(after)}`;
85
80
  }
86
81
  // mode === 'remove'
87
- const removed = values.filter((v) => beforeSet.has(v));
82
+ const requested = new Set(values.map(lintIgnoreRule));
83
+ const removed = before.filter((entry) => requested.has(lintIgnoreRule(entry)));
88
84
  if (removed.length === 0) {
89
85
  return `lintIgnore unchanged (current: ${currentLabel}; none of the requested IDs were present)`;
90
86
  }
91
- return `lintIgnore −= ${removed.join(', ')} → ${[...afterSet].join(', ') || '(empty)'}`;
87
+ return `lintIgnore −= ${formatLintIgnoreList(removed)} → ${formatLintIgnoreList(after)}`;
88
+ }
89
+ /**
90
+ * Turns the mode's IDs into manifest entries, applying `--max`/`--reason`
91
+ * to `--add`. Refuses a ceiling flag without `--add`, a `--max` without a
92
+ * `--reason` (and the reverse), and a ceiling on a rule that carries no
93
+ * measurement, so a waiver that could never suppress anything is not
94
+ * written.
95
+ */
96
+ function resolveRequestedEntries(mode, options) {
97
+ const hasMax = options.max !== undefined;
98
+ const hasReason = options.reason !== undefined;
99
+ if ((hasMax || hasReason) && mode !== 'add') {
100
+ throw new InvalidArgumentError('--max and --reason only apply to --add.', hasMax ? '--max' : '--reason');
101
+ }
102
+ if (hasMax !== hasReason) {
103
+ throw new InvalidArgumentError('A lintIgnore ceiling needs both --max <n> and --reason <text>.', hasMax ? '--reason' : '--max');
104
+ }
105
+ if (mode === 'remove')
106
+ return [...(options.remove ?? [])];
107
+ if (mode !== 'add')
108
+ return [];
109
+ const ids = options.add ?? [];
110
+ if (options.max === undefined || options.reason === undefined)
111
+ return [...ids];
112
+ if (options.reason.trim() === '') {
113
+ throw new InvalidArgumentError('--reason must not be empty.', '--reason');
114
+ }
115
+ const unmeasured = ids.filter((id) => !LINT_IGNORE_CEILING_RULES.includes(id));
116
+ if (unmeasured.length > 0) {
117
+ throw new InvalidArgumentError(`--max only applies to the measured size rules (${LINT_IGNORE_CEILING_RULES.join(', ')}); ` +
118
+ `got ${unmeasured.join(', ')}.`, '--max');
119
+ }
120
+ const { max, reason } = options;
121
+ return ids.map((rule) => ({ rule, max, reason }));
92
122
  }
93
123
  /**
94
124
  * Runs the `patch lint-ignore` command: reads the patch's existing
@@ -116,7 +146,7 @@ export async function patchLintIgnoreCommand(projectRoot, identifier, options =
116
146
  throw new InvalidArgumentError('Specify --add <id>, --remove <id>, or --clear.', 'patch lint-ignore');
117
147
  }
118
148
  const mode = adding ? 'add' : removing ? 'remove' : 'clear';
119
- const values = mode === 'add' ? (options.add ?? []) : mode === 'remove' ? (options.remove ?? []) : [];
149
+ const values = resolveRequestedEntries(mode, options);
120
150
  const { paths, manifest } = await requirePatchQueue(projectRoot);
121
151
  const target = requirePatchTarget(identifier, manifest.patches);
122
152
  // Destructive-operation contract (docs/lifecycle-invariants.md): manifest
@@ -126,7 +156,7 @@ export async function patchLintIgnoreCommand(projectRoot, identifier, options =
126
156
  // of mutation an operator should approve consciously.
127
157
  const currentList = target.lintIgnore ?? [];
128
158
  const projectedList = applyMode(currentList, mode, values) ?? [];
129
- const addsNewIds = mode === 'add' && projectedList.length > currentList.length;
159
+ const addsNewIds = mode === 'add' && addedLintIgnoreEntries(currentList, projectedList).length > 0;
130
160
  const projectedSummary = describeChange(currentList, projectedList, mode, values);
131
161
  const decision = await confirmDestructive({
132
162
  operation: 'patch-lint-ignore',
@@ -158,7 +188,7 @@ export async function patchLintIgnoreCommand(projectRoot, identifier, options =
158
188
  const existing = result.before.lintIgnore ?? [];
159
189
  const projected = result.after.lintIgnore ?? [];
160
190
  info(`${target.filename}: ${describeChange(existing, projected, mode, values)}.`);
161
- if (mode === 'add' && projected.length > existing.length) {
191
+ if (mode === 'add' && addedLintIgnoreEntries(existing, projected).length > 0) {
162
192
  warn(LINT_IGNORE_REVIEW_WARNING);
163
193
  }
164
194
  await appendHistoryBestEffort(paths.patches, {
@@ -166,7 +196,7 @@ export async function patchLintIgnoreCommand(projectRoot, identifier, options =
166
196
  args: {
167
197
  filename: target.filename,
168
198
  mode,
169
- values: [...values],
199
+ values: values.map(formatLintIgnoreEntry),
170
200
  before: existing,
171
201
  after: projected,
172
202
  },
@@ -189,6 +219,9 @@ export function registerPatchLintIgnore(parent, context) {
189
219
  .option('--add <check-id>', 'Lint check ID to add to the patch lintIgnore list (repeatable)', ...stringListOption())
190
220
  .option('--remove <check-id>', 'Lint check ID to remove from the patch lintIgnore list (repeatable)', ...stringListOption())
191
221
  .option('--clear', 'Drop the lintIgnore field entirely')
222
+ .addOption(new Option('--max <n>', 'With --add: write a size waiver with a ceiling, suppressing the rule only while its ' +
223
+ 'measurement is at most <n> (file-too-large, large-patch-lines, large-patch-files)').argParser(commanderArgParser((v) => parsePositiveIntegerFlag('--max', v))))
224
+ .option('--reason <text>', 'With --add --max: why the ceiling exists (required with --max)')
192
225
  .option('--dry-run', 'Show what would change without writing')
193
226
  .option('-y, --yes', 'Skip confirmation prompt (required for non-TTY)');
194
227
  addWaitLockOption(command).action(withErrorHandling(async (name, options) => {
@@ -203,6 +236,10 @@ export function registerPatchLintIgnore(parent, context) {
203
236
  normalized.remove = options.remove;
204
237
  if (options.clear === true)
205
238
  normalized.clear = true;
239
+ if (options.max !== undefined)
240
+ normalized.max = options.max;
241
+ if (options.reason !== undefined)
242
+ normalized.reason = options.reason;
206
243
  if (options.dryRun === true)
207
244
  normalized.dryRun = true;
208
245
  if (options.yes === true)
@@ -14,6 +14,7 @@ import { getProjectPaths, loadConfig } from '../../core/config.js';
14
14
  import { assertConfirmationAvailable, confirmDestructive } from '../../core/destructive.js';
15
15
  import { formatPatchNotFoundError } from '../../core/patch-identifier-suggest.js';
16
16
  import { buildPatchQueueContext } from '../../core/patch-lint.js';
17
+ import { buildLintIgnoreChecks } from '../../core/patch-lint-ignore.js';
17
18
  import { loadPatchesManifest, resolvePatchIdentifier } from '../../core/patch-manifest.js';
18
19
  import { formatPatchOrder } from '../../core/patch-parse.js';
19
20
  import { enforcePatchPolicy } from '../../core/patch-policy.js';
@@ -23,8 +24,9 @@ import { normalizePatchDisplayName } from '../../utils/validation.js';
23
24
  import { proceedAfterDecision } from '../destructive-decision.js';
24
25
  import { resolvePlacementPlan } from '../export-flow.js';
25
26
  import { runPatchLint } from '../export-shared.js';
26
- import { commitPatchSplit } from './split.js';
27
- import { assertSourceOwnsFiles, buildNewPatchMetadata, buildSplitDiff, buildSplitSummary, findOwnerRewriteHolders, projectSplitManifest, runProjectedSplitLint, } from './split-plan.js';
27
+ import { commitPatchSplit, reportSplitStagedChanges } from './split.js';
28
+ import { assertSourceOwnsFiles, buildNewPatchMetadata, buildSplitDiff, buildSplitSummary, projectSplitManifest, runProjectedSplitLint, } from './split-plan.js';
29
+ import { emptyStagedMaintenance, findOwnerRewriteHolders } from './split-staged.js';
28
30
  /**
29
31
  * Runs `patch move-files --create --order <n>`: plans the create+move as a
30
32
  * split, lints the projection, confirms, and commits transactionally.
@@ -85,9 +87,26 @@ export async function patchMoveFilesCreateCommand(projectRoot, fromIdentifier, n
85
87
  name: displayName,
86
88
  description: options.description ?? '',
87
89
  ownerRewrites: findOwnerRewriteHolders(manifest.patches, source.filename, movedSet),
88
- // Populated by runProjectedSplitLint below (forward edges into the new patch).
90
+ // Populated by runProjectedSplitLint below (forward edges into the new
91
+ // patch, and the declarations the move re-homes or makes unused).
89
92
  stagedDependencyAdditions: new Map(),
93
+ stagedMaintenance: emptyStagedMaintenance(),
90
94
  };
95
+ // Policy first: the projected manifest (orders, ranges, the gap rule)
96
+ // does not depend on the lint below, so a refused --order fails before
97
+ // the per-patch and projected lint passes instead of after them.
98
+ enforcePatchPolicy({
99
+ config,
100
+ manifest: projectSplitManifest(manifest, plan, buildNewPatchMetadata(plan, config)),
101
+ command: 'patch move-files --create',
102
+ forceUnsafe: options.forceUnsafe === true,
103
+ hints: {
104
+ 'description-required': 'Pass --description "<text>" (or -d) on this command to set the created patch\'s description.',
105
+ 'numeric-gap': 'With patchPolicy.allowGaps: false, orders are contiguous within a range: --order must ' +
106
+ "fill an existing gap or a retired slot, or take the number right after the range's last " +
107
+ 'patch. A number reserved past a gap cannot be used until the numbers before it exist.',
108
+ },
109
+ });
91
110
  // Per-patch lint both projected bodies, threading the source patch's
92
111
  // tier/lintIgnore so an intentional-advisory patch can still move files.
93
112
  // The whole-queue context (built once, with the config so it carries the
@@ -96,7 +115,7 @@ export async function patchMoveFilesCreateCommand(projectRoot, fromIdentifier, n
96
115
  // resolve exactly as they will after the move lands. Without it the
97
116
  // projection lint is blind.
98
117
  const patchQueueCtx = await buildPatchQueueContext(paths.patches, config);
99
- const ignoreChecks = source.lintIgnore ? new Set(source.lintIgnore) : undefined;
118
+ const ignoreChecks = buildLintIgnoreChecks(source.lintIgnore);
100
119
  await runPatchLint({
101
120
  engineDir: paths.engine,
102
121
  filesAffected: remainingFiles,
@@ -117,18 +136,10 @@ export async function patchMoveFilesCreateCommand(projectRoot, fromIdentifier, n
117
136
  ignoreChecks,
118
137
  patchTier: source.tier,
119
138
  });
120
- const { conflicts, stagedDependencyAdditions } = runProjectedSplitLint(plan, patchQueueCtx);
139
+ const { conflicts, stagedDependencyAdditions, stagedMaintenance } = runProjectedSplitLint(plan, patchQueueCtx);
121
140
  plan.stagedDependencyAdditions = stagedDependencyAdditions;
141
+ plan.stagedMaintenance = stagedMaintenance;
122
142
  const newMetadata = buildNewPatchMetadata(plan, config);
123
- enforcePatchPolicy({
124
- config,
125
- manifest: projectSplitManifest(manifest, plan, newMetadata),
126
- command: 'patch move-files --create',
127
- forceUnsafe: options.forceUnsafe === true,
128
- hints: {
129
- 'description-required': 'Pass --description "<text>" (or -d) on this command to set the created patch\'s description.',
130
- },
131
- });
132
143
  const decision = await confirmDestructive({
133
144
  operation: 'patch-move-files-create',
134
145
  title: `Create ${placement.newFilename} (order ${placement.insertionOrder}) and move ${movedFiles.length} file(s) out of ${source.filename}`,
@@ -143,8 +154,6 @@ export async function patchMoveFilesCreateCommand(projectRoot, fromIdentifier, n
143
154
  await commitPatchSplit(paths.patches, plan, newMetadata, options, config);
144
155
  success(`Created ${placement.newFilename} (order ${formatPatchOrder(placement.insertionOrder)}) ` +
145
156
  `and moved ${plan.movedFiles.length} file(s) out of ${source.filename}`);
146
- if (plan.ownerRewrites.length > 0) {
147
- info(`Re-pointed staged-dependency owners in: ${plan.ownerRewrites.join(', ')}`);
148
- }
157
+ reportSplitStagedChanges(plan);
149
158
  outro('Move complete');
150
159
  }
@@ -18,6 +18,7 @@ import { join } from 'node:path';
18
18
  import { assertConfirmationAvailable, confirmDestructive, } from '../../core/destructive.js';
19
19
  import { appendHistoryBestEffort } from '../../core/history-log.js';
20
20
  import { buildPatchQueueContext, collectForwardImportEdges, formatPatchLintIssue, lintPatchQueue, } from '../../core/patch-lint.js';
21
+ import { buildLintIgnoreChecks } from '../../core/patch-lint-ignore.js';
21
22
  import { computeProjectedLintRegressions } from '../../core/patch-lint-projection.js';
22
23
  import { withPatchDirectoryLock } from '../../core/patch-lock.js';
23
24
  import { loadPatchesManifest, savePatchesManifest, validatePatchesManifest, } from '../../core/patch-manifest.js';
@@ -29,7 +30,8 @@ import { resolveWaitLockSeconds } from '../../utils/options.js';
29
30
  import { proceedAfterDecision } from '../destructive-decision.js';
30
31
  import { runPatchLint } from '../export-shared.js';
31
32
  import { projectEntryBody } from './entry-projection.js';
32
- import { buildSplitDiff, findOwnerRewriteHolders, mergeStagedForwardImports, rewriteSplitOwners, } from './split-plan.js';
33
+ import { buildSplitDiff, mergeStagedForwardImports } from './split-plan.js';
34
+ import { findOwnerRewriteHolders, rewriteSplitOwners } from './split-staged.js';
33
35
  /**
34
36
  * Projects the post-move queue through cross-patch lint, auto-declaring the
35
37
  * forward edges the move introduces into the target (an importer that used
@@ -198,7 +200,7 @@ export async function runMoveFilesInto(args) {
198
200
  config: args.config,
199
201
  skipLint: options.skipLint,
200
202
  patchQueueCtx,
201
- ignoreChecks: source.lintIgnore ? new Set(source.lintIgnore) : undefined,
203
+ ignoreChecks: buildLintIgnoreChecks(source.lintIgnore),
202
204
  patchTier: source.tier,
203
205
  });
204
206
  await runPatchLint({
@@ -208,7 +210,7 @@ export async function runMoveFilesInto(args) {
208
210
  config: args.config,
209
211
  skipLint: options.skipLint,
210
212
  patchQueueCtx,
211
- ignoreChecks: target.lintIgnore ? new Set(target.lintIgnore) : undefined,
213
+ ignoreChecks: buildLintIgnoreChecks(target.lintIgnore),
212
214
  patchTier: target.tier,
213
215
  });
214
216
  const manifest = await loadPatchesManifest(patchesDir);
@@ -164,7 +164,9 @@ export function registerPatchMoveFiles(parent, context) {
164
164
  'or create the target patch and move the files in one transaction with --create --order <n>.')
165
165
  .option('--file <path>', 'File path relative to engine/ to move (repeatable)', ...stringListOption())
166
166
  .option('--create', 'Create <to> as a new patch and move the files into it (requires --order)')
167
- .option('--order <n>', 'Exact sparse order for the created patch (only valid with --create)', commanderArgParser((raw) => parsePositiveIntegerFlag('--order', raw)))
167
+ .option('--order <n>', 'Exact sparse order for the created patch (only valid with --create). With ' +
168
+ 'patchPolicy.allowGaps: false, orders are contiguous within a range: pick a gap, a ' +
169
+ "retired slot, or the number after the range's last patch; checked before any lint runs", commanderArgParser((raw) => parsePositiveIntegerFlag('--order', raw)))
168
170
  .option('--category <category>', "Category for the created patch (default: the source patch's)")
169
171
  .option('-d, --description <desc>', 'Description for the created patch')
170
172
  .option('--dry-run', 'Show what would happen without writing')
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The dependency scan `patch delete` and `patch retire` share: which later
3
+ * patches import a file the removed patch creates, and which staged
4
+ * declarations name it as their owner. Both commands take the patch out of
5
+ * the queue, so both must refuse and warn on exactly the same evidence.
6
+ */
7
+ import type { ConflictReport } from '../../core/destructive.js';
8
+ import { type PatchQueueContext } from '../../core/patch-lint.js';
9
+ import type { PatchMetadata } from '../../types/commands/index.js';
10
+ /**
11
+ * Lists every later patch site that imports a file `target` creates.
12
+ *
13
+ * Both source-site maps are covered: `newFiles` (files the later patch
14
+ * creates) and `modifiedFileAdditions` (added lines against files that
15
+ * already exist). Scanning only newFiles misses a later patch adding
16
+ * `import "./TargetHelper.sys.mjs"` to an existing file. Uses the shared
17
+ * specifier extractor so dynamic `import()` and
18
+ * `ChromeUtils.defineESModuleGetters` are picked up: the forward-import lint
19
+ * rule already covers those forms, and removal safety must match the same
20
+ * set or it silently drops dependencies.
21
+ *
22
+ * @param baseCtx - Queue context built before the removal
23
+ * @param target - The patch about to leave the queue
24
+ * @param command - Names for the "queue changed" refusal, e.g.
25
+ * `{ noun: 'deletion', verb: 'delete' }`
26
+ * @returns One description per dependent patch
27
+ */
28
+ export declare function collectRemovalDependents(baseCtx: PatchQueueContext, target: PatchMetadata, command: {
29
+ noun: string;
30
+ verb: string;
31
+ }): string[];
32
+ /**
33
+ * Warns once per patch whose staged forward-import names `target` as its
34
+ * owner. The dangling reference also surfaces via cross-patch lint later,
35
+ * but warning here puts the exact cleanup command in front of the operator
36
+ * at decision time.
37
+ *
38
+ * @param baseCtx - Queue context built before the removal
39
+ * @param target - The patch about to leave the queue
40
+ * @param verb - Command name used in the warning ("delete", "retire")
41
+ */
42
+ export declare function warnDanglingStagedOwners(baseCtx: PatchQueueContext, target: PatchMetadata, verb: string): void;
43
+ /**
44
+ * The structural conflict a removal with dependents raises.
45
+ *
46
+ * The wording spells out the runtime impact: `git apply` does not resolve
47
+ * imports and succeeds even when a later patch imports a file the target
48
+ * created, so the queue re-imports cleanly. The breakage surfaces at
49
+ * browser startup when `ChromeUtils.importESModule` cannot locate the
50
+ * deleted module. Operators who plan to re-introduce the imported files
51
+ * (rename, refactor) need to know this is the impact model rather than a
52
+ * patch-application failure.
53
+ *
54
+ * @param dependents - Result of {@link collectRemovalDependents}
55
+ * @param target - The patch about to leave the queue
56
+ */
57
+ export declare function removalConflict(dependents: readonly string[], target: PatchMetadata): ConflictReport | null;
58
+ /**
59
+ * Snapshot of the queue inputs a removal's dependency scan read, compared
60
+ * again under the lock so a change between the prompt and the commit
61
+ * refuses instead of committing an unreviewed removal.
62
+ */
63
+ export declare function queueSnapshot(context: PatchQueueContext): string;
@@ -0,0 +1,133 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * The dependency scan `patch delete` and `patch retire` share: which later
4
+ * patches import a file the removed patch creates, and which staged
5
+ * declarations name it as their owner. Both commands take the patch out of
6
+ * the queue, so both must refuse and warn on exactly the same evidence.
7
+ */
8
+ import { basename } from 'node:path';
9
+ import { extractImportSpecifiersWithLines, findForwardImportIgnoreLines, isForwardImportableFile, } from '../../core/patch-lint.js';
10
+ import { GeneralError } from '../../errors/base.js';
11
+ import { warn } from '../../utils/logger.js';
12
+ /**
13
+ * Lists every later patch site that imports a file `target` creates.
14
+ *
15
+ * Both source-site maps are covered: `newFiles` (files the later patch
16
+ * creates) and `modifiedFileAdditions` (added lines against files that
17
+ * already exist). Scanning only newFiles misses a later patch adding
18
+ * `import "./TargetHelper.sys.mjs"` to an existing file. Uses the shared
19
+ * specifier extractor so dynamic `import()` and
20
+ * `ChromeUtils.defineESModuleGetters` are picked up: the forward-import lint
21
+ * rule already covers those forms, and removal safety must match the same
22
+ * set or it silently drops dependencies.
23
+ *
24
+ * @param baseCtx - Queue context built before the removal
25
+ * @param target - The patch about to leave the queue
26
+ * @param command - Names for the "queue changed" refusal, e.g.
27
+ * `{ noun: 'deletion', verb: 'delete' }`
28
+ * @returns One description per dependent patch
29
+ */
30
+ export function collectRemovalDependents(baseCtx, target, command) {
31
+ const targetEntry = baseCtx.entries.find((e) => e.filename === target.filename);
32
+ if (!targetEntry || JSON.stringify(targetEntry.metadata) !== JSON.stringify(target)) {
33
+ throw new GeneralError(`Patch queue changed while preparing ${command.noun}. Re-run patch ${command.verb} to review the current target.`);
34
+ }
35
+ const targetNewFileLeaves = new Set();
36
+ for (const fullPath of targetEntry.newFiles.keys()) {
37
+ targetNewFileLeaves.add(basename(fullPath));
38
+ }
39
+ const dependents = [];
40
+ const scanSite = (entryFilename, sitePath, content) => {
41
+ if (!isForwardImportableFile(sitePath))
42
+ return false;
43
+ const ignoredLines = findForwardImportIgnoreLines(content);
44
+ const specifiers = extractImportSpecifiersWithLines(content);
45
+ for (const { specifier, line } of specifiers) {
46
+ if (ignoredLines.has(line))
47
+ continue;
48
+ const cleaned = specifier.split(/[?#]/)[0] ?? specifier;
49
+ const leaf = basename(cleaned);
50
+ if (!leaf || !isForwardImportableFile(leaf))
51
+ continue;
52
+ if (targetNewFileLeaves.has(leaf)) {
53
+ dependents.push(`${entryFilename} (${sitePath}) imports "${specifier}" which would be deleted`);
54
+ return true;
55
+ }
56
+ }
57
+ return false;
58
+ };
59
+ for (const entry of baseCtx.entries) {
60
+ if (entry.filename === target.filename)
61
+ continue;
62
+ if (entry.order < target.order)
63
+ continue;
64
+ let matched = false;
65
+ for (const [newFile, content] of entry.newFiles) {
66
+ if (scanSite(entry.filename, newFile, content)) {
67
+ matched = true;
68
+ break;
69
+ }
70
+ }
71
+ if (matched)
72
+ continue;
73
+ for (const [modifiedPath, addedContent] of entry.modifiedFileAdditions) {
74
+ if (scanSite(entry.filename, modifiedPath, addedContent))
75
+ break;
76
+ }
77
+ }
78
+ return dependents;
79
+ }
80
+ /**
81
+ * Warns once per patch whose staged forward-import names `target` as its
82
+ * owner. The dangling reference also surfaces via cross-patch lint later,
83
+ * but warning here puts the exact cleanup command in front of the operator
84
+ * at decision time.
85
+ *
86
+ * @param baseCtx - Queue context built before the removal
87
+ * @param target - The patch about to leave the queue
88
+ * @param verb - Command name used in the warning ("delete", "retire")
89
+ */
90
+ export function warnDanglingStagedOwners(baseCtx, target, verb) {
91
+ const holders = baseCtx.entries.filter((entry) => entry.filename !== target.filename &&
92
+ (entry.metadata?.stagedDependencies?.forwardImports ?? []).some((fi) => fi.owner === target.filename));
93
+ for (const holder of holders) {
94
+ warn(`${holder.filename} declares a staged dependency with owner ${target.filename}; ` +
95
+ `after the ${verb}, update it via "fireforge patch staged-dependency ${holder.filename} --remove ..." ` +
96
+ 'or re-point the owner at the patch that will create the file.');
97
+ }
98
+ }
99
+ /**
100
+ * The structural conflict a removal with dependents raises.
101
+ *
102
+ * The wording spells out the runtime impact: `git apply` does not resolve
103
+ * imports and succeeds even when a later patch imports a file the target
104
+ * created, so the queue re-imports cleanly. The breakage surfaces at
105
+ * browser startup when `ChromeUtils.importESModule` cannot locate the
106
+ * deleted module. Operators who plan to re-introduce the imported files
107
+ * (rename, refactor) need to know this is the impact model rather than a
108
+ * patch-application failure.
109
+ *
110
+ * @param dependents - Result of {@link collectRemovalDependents}
111
+ * @param target - The patch about to leave the queue
112
+ */
113
+ export function removalConflict(dependents, target) {
114
+ if (dependents.length === 0)
115
+ return null;
116
+ return {
117
+ reason: `${dependents.length} later patch(es) contain import statements that reference files created by ${target.filename}. Patch application itself will still succeed, but runtime imports will fail at browser startup until those files are re-introduced.`,
118
+ details: [...dependents],
119
+ };
120
+ }
121
+ /**
122
+ * Snapshot of the queue inputs a removal's dependency scan read, compared
123
+ * again under the lock so a change between the prompt and the commit
124
+ * refuses instead of committing an unreviewed removal.
125
+ */
126
+ export function queueSnapshot(context) {
127
+ return JSON.stringify(context.entries.map(({ filename, order, diff, metadata }) => ({
128
+ filename,
129
+ order,
130
+ diff,
131
+ metadata,
132
+ })));
133
+ }
@@ -26,6 +26,7 @@ import { buildPatchQueueContext, formatPatchLintIssue, lintPatchQueue, } from '.
26
26
  import { withPatchDirectoryLock } from '../../core/patch-lock.js';
27
27
  import { loadPatchesManifest, rewriteStagedDependencyOwners, savePatchesManifest, } from '../../core/patch-manifest.js';
28
28
  import { buildProjectedManifest, enforcePatchPolicy, getPatchPolicyCategories, isCategoryAllowedByConfig, } from '../../core/patch-policy.js';
29
+ import { carryRetiredSlots } from '../../core/patch-retired.js';
29
30
  import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
30
31
  import { toError } from '../../utils/errors.js';
31
32
  import { pathExists } from '../../utils/fs.js';
@@ -115,6 +116,12 @@ async function commitRenameUnderLock(input) {
115
116
  ...(descriptionChanging ? { description: newDescription ?? '' } : {}),
116
117
  };
117
118
  }
119
+ // A rename onto a retired order fills that slot.
120
+ const retired = carryRetiredSlots(fresh, fresh.patches);
121
+ if (retired === undefined)
122
+ delete fresh.retired;
123
+ else
124
+ fresh.retired = retired;
118
125
  enforcePatchPolicy({
119
126
  config: input.config,
120
127
  manifest: buildProjectedManifest(fresh, fresh.patches),
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `fireforge patch retire <name>`: removes a patch from the queue and keeps
3
+ * its order number spent as a retired slot in `patches.json#retired`.
4
+ *
5
+ * The case it exists for: a rename leaves a patch owning only files that no
6
+ * longer exist. `patch delete` opens a numeric gap that
7
+ * `patchPolicy.allowGaps: false` refuses, and `patch compact` renumbers
8
+ * every later patch, including ones a peer session has in flight. Retiring
9
+ * removes the patch file and row like a delete, and records the order as a
10
+ * placeholder the gap check counts as occupied. Nothing else reads the
11
+ * placeholder (see `core/patch-retired.ts`), and a later
12
+ * `export --order <n>` that takes the number fills it.
13
+ *
14
+ * Shares the delete's dependency refusal, dry-run, confirmation and history
15
+ * contract, so the two differ only in what happens to the order number.
16
+ */
17
+ import { Command } from 'commander';
18
+ import type { CommandContext } from '../../types/cli.js';
19
+ import type { PatchRetireOptions } from '../../types/commands/index.js';
20
+ /**
21
+ * Runs the `patch retire` command.
22
+ *
23
+ * @param projectRoot - Project root directory
24
+ * @param identifier - Patch filename, ordinal, or manifest name to retire
25
+ * @param options - Command options
26
+ */
27
+ export declare function patchRetireCommand(projectRoot: string, identifier: string, options?: PatchRetireOptions): Promise<void>;
28
+ /**
29
+ * Registers the `patch retire` subcommand on the `patch` parent.
30
+ *
31
+ * @param parent - Parent Commander command
32
+ * @param context - Shared CLI registration context
33
+ */
34
+ export declare function registerPatchRetire(parent: Command, context: CommandContext): void;