@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
@@ -3,10 +3,13 @@ import { join } from 'node:path';
3
3
  import { loadConfig } from '../core/config.js';
4
4
  import { getDiffForFilesAgainstHead } from '../core/git-diff.js';
5
5
  import { withPrivateGitIndex } from '../core/git-readonly-index.js';
6
- import { buildPatchQueueContext, countNonBinaryDiffLines, formatPatchLintIssue, lintExportedPatch, lintPatchQueue, resolvePatchSizeTier, } from '../core/patch-lint.js';
6
+ import { buildPatchQueueContext, formatPatchLintIssue, lintExportedPatch, lintPatchQueue, measurePatchSize, resolvePatchSizeTier, } from '../core/patch-lint.js';
7
7
  import { buildPerPatchLintCacheKey, getCachedPerPatchLintIssues, getPerPatchLintCacheHeadSha, loadPerPatchLintCache, savePerPatchLintCache, setCachedPerPatchLintIssues, } from '../core/patch-lint-cache.js';
8
+ import { buildLintIgnoreChecks, evaluateLintIgnoreCeilings, formatLintIgnoreEntry, isLintIgnoreCeiling, lintIgnoreCacheTokens, removableCeilingIssues, } from '../core/patch-lint-ignore.js';
8
9
  import { loadPatchesManifest } from '../core/patch-manifest.js';
9
10
  import { evaluatePatchPolicy } from '../core/patch-policy.js';
11
+ import { evaluatePatchPolicyContent } from '../core/patch-policy-content.js';
12
+ import { selectPatchSubset } from '../core/patch-subset.js';
10
13
  import { GeneralError } from '../errors/base.js';
11
14
  import { mapWithConcurrency } from '../utils/concurrency.js';
12
15
  import { pathExists } from '../utils/fs.js';
@@ -47,7 +50,7 @@ async function lintQueuedPatch(patch, lintCtx) {
47
50
  usedCheckJs: false,
48
51
  };
49
52
  }
50
- const ignore = patch.lintIgnore?.length ? new Set(patch.lintIgnore) : undefined;
53
+ const ignore = buildLintIgnoreChecks(patch.lintIgnore);
51
54
  let cacheKey;
52
55
  if (cache) {
53
56
  cacheKey = await buildPerPatchLintCacheKey({
@@ -60,7 +63,7 @@ async function lintQueuedPatch(patch, lintCtx) {
60
63
  queueContext: ctx,
61
64
  ...(engineHeadSha === undefined ? {} : { engineHeadSha }),
62
65
  });
63
- const cached = getCachedPerPatchLintIssues(cache, patch.filename, cacheKey, ignore);
66
+ const cached = getCachedPerPatchLintIssues(cache, patch.filename, cacheKey, lintIgnoreCacheTokens(ignore));
64
67
  if (cached) {
65
68
  // Returning before the empty-diff probe below is safe: the cache key
66
69
  // hashes every affected engine file's content plus engineHeadSha, so
@@ -106,8 +109,13 @@ async function lintQueuedPatch(patch, lintCtx) {
106
109
  }
107
110
  lintOptions.precomputedCheckJs = precomputedCheckJs;
108
111
  }
112
+ // Measured from the working tree, like every other rule here: the files
113
+ // on disk are the would-be export, so a size tier fires mid-edit rather
114
+ // than first at the re-export.
115
+ const patchSize = await measurePatchSize(paths.engine, diff, existing, config);
109
116
  const patchIssues = await lintExportedPatch(paths.engine, existing, diff, config, {
110
117
  ...lintOptions,
118
+ patchSizeMeasurement: patchSize,
111
119
  patchQueueCtx: ctx,
112
120
  ...(ignore ? { ignoreChecks: ignore } : {}),
113
121
  ...(patch.tier ? { patchTier: patch.tier } : {}),
@@ -117,11 +125,11 @@ async function lintQueuedPatch(patch, lintCtx) {
117
125
  existingFiles: existing,
118
126
  rawIssues: patchIssues,
119
127
  suppressedIssues,
120
- lineCount: countNonBinaryDiffLines(diff).textLines,
128
+ lineCount: patchSize.lines,
121
129
  usedCheckJs,
122
130
  };
123
131
  if (cache && cacheKey) {
124
- result.cacheWrite = { key: cacheKey, lintIgnore: [...(ignore ?? [])] };
132
+ result.cacheWrite = { key: cacheKey, lintIgnore: lintIgnoreCacheTokens(ignore) };
125
133
  }
126
134
  return result;
127
135
  }
@@ -183,6 +191,9 @@ async function applyPerPatchResults(subset, results, issues, checkJs, cache, hel
183
191
  for (const issue of result.rawIssues) {
184
192
  issues.push({ ...issue, file: `${patch.filename} :: ${issue.file}` });
185
193
  }
194
+ // A ceiling the patch no longer needs is reported as a notice naming
195
+ // the command that drops it.
196
+ issues.push(...removableCeilingIssues(patch.filename, evaluateLintIgnoreCeilings(patch.lintIgnore, [...result.rawIssues, ...result.suppressedIssues], { lines: result.lineCount, files: patch.filesAffected.length })));
186
197
  // A waived size finding still reports its measurement: the finding stays
187
198
  // suppressed (no exit-code / --max-warnings effect) but the current count
188
199
  // is readable from the tool that enforces it, so a waiver's cited size
@@ -194,7 +205,9 @@ async function applyPerPatchResults(subset, results, issues, checkJs, cache, hel
194
205
  heldNotices.push({ check: suppressedIssue.check, patch: patch.filename, suppressed: true });
195
206
  continue;
196
207
  }
197
- info(`NOTICE [${suppressedIssue.check}] ${patch.filename}: suppressed by lintIgnore — ${suppressedIssue.message}`);
208
+ const ceiling = patch.lintIgnore?.find((entry) => isLintIgnoreCeiling(entry) && entry.rule === suppressedIssue.check);
209
+ const waiver = ceiling === undefined ? 'lintIgnore' : `lintIgnore (${formatLintIgnoreEntry(ceiling)})`;
210
+ info(`NOTICE [${suppressedIssue.check}] ${patch.filename}: suppressed by ${waiver} — ${suppressedIssue.message}`);
198
211
  }
199
212
  totals.suppressed += result.suppressedIssues.length;
200
213
  if (result.status === 'cached')
@@ -309,57 +322,9 @@ function reportNoticeSummary(heldNotices, issues) {
309
322
  info(`NOTICE [${check}]: ${entry.count} across ${entry.patches.size} patch(es)${suppressed} (see --report)`);
310
323
  }
311
324
  }
312
- /**
313
- * Resolves the `--patches <name…>` subset filter against the manifest,
314
- * matching each requested name tolerantly (exact filename, filename ±
315
- * `.patch`, the bare order number, or the manifest `name` field). Throws listing the available
316
- * patches when a requested name matches none, so a typo fails loud rather
317
- * than silently linting nothing.
318
- */
319
- function selectPatchSubset(manifest, requested) {
320
- const normalizedRequests = requested
321
- .flatMap((entry) => entry.split(','))
322
- .map((entry) => entry.trim())
323
- .filter((entry) => entry.length > 0);
324
- const patchAliases = (p) => {
325
- const aliases = new Set([p.filename, p.filename.replace(/\.patch$/, ''), p.name]);
326
- // The bare order number, both as the manifest records it and as the
327
- // filename spells it (`2` and `002`). The refusal message offers stems,
328
- // and an operator reading `102-ui-canvas-tiles.patch` in a lint finding
329
- // reaches for `102` first.
330
- aliases.add(String(p.order));
331
- const match = /^(\d+)-([a-z]+)-(.+)\.patch$/.exec(p.filename);
332
- if (match?.[1])
333
- aliases.add(match[1]);
334
- if (match?.[2] && match[3]) {
335
- aliases.add(`${match[2]}-${match[3]}`);
336
- aliases.add(match[3]);
337
- }
338
- return aliases;
339
- };
340
- const matches = (p, name) => {
341
- const stem = name.replace(/\.patch$/, '');
342
- return (patchAliases(p).has(name) || patchAliases(p).has(stem) || patchAliases(p).has(`${stem}.patch`));
343
- };
344
- const selected = [];
345
- const seen = new Set();
346
- for (const name of normalizedRequests) {
347
- const found = manifest.patches.filter((p) => matches(p, name));
348
- if (found.length === 0) {
349
- const available = manifest.patches.map((p) => p.filename).join(', ');
350
- throw new GeneralError(`No patch in the queue matches "${name}". In --per-patch mode, positional arguments ` +
351
- `and --patches select patches (filename, stem, order number, manifest name, or slug) — not engine ` +
352
- `files; drop --per-patch to lint engine paths. Available patches: ${available}`);
353
- }
354
- for (const p of found) {
355
- if (!seen.has(p.filename)) {
356
- seen.add(p.filename);
357
- selected.push(p);
358
- }
359
- }
360
- }
361
- return selected;
362
- }
325
+ /** The refusal sentence `selectPatchSubset` adds for a `lint --per-patch` miss. */
326
+ const PER_PATCH_SUBSET_HINT = 'In --per-patch mode, positional arguments and --patches select patches (filename, stem, ' +
327
+ 'order number, manifest name, or slug) — not engine files; drop --per-patch to lint engine paths.';
363
328
  /**
364
329
  * Lints each patch in the queue as its own isolated diff, honouring
365
330
  * per-patch `lintIgnore` entries. Cross-patch rules still run once over
@@ -382,7 +347,7 @@ async function lintPerPatchInner(projectRoot, paths, options = {}) {
382
347
  return;
383
348
  }
384
349
  const subset = options.patches && options.patches.length > 0
385
- ? selectPatchSubset(manifest, options.patches)
350
+ ? selectPatchSubset(manifest, options.patches, PER_PATCH_SUBSET_HINT)
386
351
  : manifest.patches;
387
352
  const subsetNames = new Set(subset.map((p) => p.filename));
388
353
  const isSubset = subset.length !== manifest.patches.length;
@@ -405,7 +370,11 @@ async function lintPerPatchInner(projectRoot, paths, options = {}) {
405
370
  const cache = options.noCache === true ? undefined : await loadPerPatchLintCache(projectRoot);
406
371
  const engineHeadSha = cache ? await getPerPatchLintCacheHeadSha(paths.engine) : undefined;
407
372
  const issues = [];
408
- for (const issue of evaluatePatchPolicy(config, manifest)) {
373
+ const policyIssues = [
374
+ ...evaluatePatchPolicy(config, manifest),
375
+ ...(await evaluatePatchPolicyContent(config, projectRoot, manifest, ctx.entries)),
376
+ ];
377
+ for (const issue of policyIssues) {
409
378
  if (isSubset && !subsetNames.has(issue.filename))
410
379
  continue;
411
380
  issues.push({
@@ -433,10 +402,12 @@ async function lintPerPatchInner(projectRoot, paths, options = {}) {
433
402
  });
434
403
  const heldNotices = options.notices === 'summary' ? [] : undefined;
435
404
  const { linted, skipped, cacheDirty, reusedCacheEntries, suppressed } = await applyPerPatchResults(subset, results, issues, checkJs, cache, heldNotices);
405
+ const queueIssues = [];
436
406
  for (const issue of lintPatchQueue(ctx)) {
437
407
  if (isSubset && !subsetTouchedFiles.has(issue.file))
438
408
  continue;
439
409
  issues.push(issue);
410
+ queueIssues.push(issue);
440
411
  }
441
412
  if (cache && cacheDirty)
442
413
  await savePerPatchLintCache(projectRoot, cache);
@@ -447,7 +418,7 @@ async function lintPerPatchInner(projectRoot, paths, options = {}) {
447
418
  info(`Suppressed ${suppressed} issue(s) via per-patch lintIgnore.`);
448
419
  }
449
420
  if (options.report !== undefined) {
450
- await writePerPatchLintReport(options.report, subset, results);
421
+ await writePerPatchLintReport(options.report, subset, results, config, queueIssues);
451
422
  }
452
423
  reportPerPatchOutcome(issues, linted, skipped, options, heldNotices);
453
424
  }
@@ -1,4 +1,5 @@
1
1
  import type { PatchLintIssue, PatchMetadata } from '../types/commands/index.js';
2
+ import type { FireForgeConfig } from '../types/config.js';
2
3
  /**
3
4
  * Structural view of one per-patch lint outcome. Mirrors the relevant
4
5
  * fields of lint-per-patch's QueuedPatchResult without importing it:
@@ -14,8 +15,11 @@ export interface PerPatchReportResult {
14
15
  }
15
16
  /**
16
17
  * Writes the per-patch lint report JSON. Called after the per-patch
17
- * results are assembled in patch order. Queue-level and policy findings
18
- * are patch-attributed by their `file`/`filename` fields upstream and
19
- * are not duplicated here. The report is the per-patch view.
18
+ * results are assembled in patch order. Policy findings are
19
+ * patch-attributed by their `filename` field upstream and are not
20
+ * duplicated here. Cross-patch findings (`forward-import`,
21
+ * `forward-registration`, …) go in `queueIssues`, with the
22
+ * `stagedDeclarations` that discharge them, so the report can be handed to
23
+ * `patch staged-dependency --from` as is.
20
24
  */
21
- export declare function writePerPatchLintReport(reportPath: string, subset: readonly PatchMetadata[], results: readonly PerPatchReportResult[]): Promise<void>;
25
+ export declare function writePerPatchLintReport(reportPath: string, subset: readonly PatchMetadata[], results: readonly PerPatchReportResult[], config: Pick<FireForgeConfig, 'patchLint'>, queueIssues?: readonly PatchLintIssue[]): Promise<void>;
@@ -10,16 +10,21 @@
10
10
  * none is wanted.
11
11
  */
12
12
  import { getPatchSizeThresholds, resolvePatchSizeTier, } from '../core/patch-lint.js';
13
+ import { evaluateLintIgnoreCeilings, } from '../core/patch-lint-ignore.js';
13
14
  import { writeJson } from '../utils/fs.js';
14
15
  import { info } from '../utils/logger.js';
15
16
  const LINT_REPORT_SCHEMA_VERSION = 1;
16
17
  /**
17
18
  * Writes the per-patch lint report JSON. Called after the per-patch
18
- * results are assembled in patch order. Queue-level and policy findings
19
- * are patch-attributed by their `file`/`filename` fields upstream and
20
- * are not duplicated here. The report is the per-patch view.
19
+ * results are assembled in patch order. Policy findings are
20
+ * patch-attributed by their `filename` field upstream and are not
21
+ * duplicated here. Cross-patch findings (`forward-import`,
22
+ * `forward-registration`, …) go in `queueIssues`, with the
23
+ * `stagedDeclarations` that discharge them, so the report can be handed to
24
+ * `patch staged-dependency --from` as is.
21
25
  */
22
- export async function writePerPatchLintReport(reportPath, subset, results) {
26
+ export async function writePerPatchLintReport(reportPath, subset, results, config, queueIssues = []) {
27
+ const metric = config.patchLint?.patchSizeMetric ?? 'diff';
23
28
  const patches = [];
24
29
  let errors = 0;
25
30
  let warnings = 0;
@@ -36,12 +41,16 @@ export async function writePerPatchLintReport(reportPath, subset, results) {
36
41
  filename: patch.filename,
37
42
  name: patch.name,
38
43
  status: result.status,
44
+ metric,
39
45
  lineCount: result.lineCount,
40
46
  filesAffected: patch.filesAffected.length,
41
47
  tier,
42
- thresholds: getPatchSizeThresholds(tier.tier),
48
+ thresholds: getPatchSizeThresholds(tier.tier, config),
43
49
  issues: result.rawIssues,
44
50
  suppressedIssues: result.suppressedIssues,
51
+ lintIgnoreCeilings: result.status === 'skipped'
52
+ ? []
53
+ : evaluateLintIgnoreCeilings(patch.lintIgnore, [...result.rawIssues, ...result.suppressedIssues], { lines: result.lineCount, files: patch.filesAffected.length }),
45
54
  });
46
55
  if (result.status === 'skipped')
47
56
  skipped++;
@@ -55,6 +64,7 @@ export async function writePerPatchLintReport(reportPath, subset, results) {
55
64
  schemaVersion: LINT_REPORT_SCHEMA_VERSION,
56
65
  generatedAt: new Date().toISOString(),
57
66
  patches,
67
+ queueIssues,
58
68
  totals: { linted, skipped, errors, warnings, suppressed },
59
69
  });
60
70
  info(`Per-patch lint report written to ${reportPath}.`);
@@ -10,9 +10,10 @@ import { getStatusWithCodes, hasChanges } from '../core/git.js';
10
10
  import { getAllDiff, getDiffForFilesAgainstHead } from '../core/git-diff.js';
11
11
  import { expandUntrackedDirectoryEntries, getModifiedFilesInDir, getUntrackedFiles, getUntrackedFilesInDir, getWorkingTreeStatus, } from '../core/git-status.js';
12
12
  import { extractAffectedFiles } from '../core/patch-apply.js';
13
- import { buildPatchQueueContext, countNonBinaryDiffLines, formatPatchLintIssue, lintExportedPatch, lintPatchQueue, lintPatchSize, } from '../core/patch-lint.js';
13
+ import { buildPatchQueueContext, formatPatchLintIssue, lintExportedPatch, lintPatchQueue, lintPatchSize, measurePatchSize, } from '../core/patch-lint.js';
14
14
  import { clearPerPatchLintCache } from '../core/patch-lint-cache.js';
15
15
  import { collectDiffFilePaths, tagLintIssues } from '../core/patch-lint-diff-tag.js';
16
+ import { buildLintIgnoreChecks, filterLintIgnoredByOwners, partitionLintIgnoredIssues, } from '../core/patch-lint-ignore.js';
16
17
  import { GeneralError, InvalidArgumentError } from '../errors/base.js';
17
18
  import { pathExists } from '../utils/fs.js';
18
19
  import { info, intro, outro, success, warn } from '../utils/logger.js';
@@ -186,25 +187,7 @@ function buildMaxWarningsMessage(count, maxWarnings, scope) {
186
187
  * @returns Filtered issue list and the count of dropped findings.
187
188
  */
188
189
  export function applyAggregateLintIgnoreSuppression(issues, ctx) {
189
- const suppressionsByFile = new Map();
190
- for (const entry of ctx.entries) {
191
- const ignoreList = entry.metadata?.lintIgnore;
192
- if (!ignoreList || ignoreList.length === 0)
193
- continue;
194
- for (const f of entry.metadata?.filesAffected ?? []) {
195
- let bucket = suppressionsByFile.get(f);
196
- if (!bucket) {
197
- bucket = new Set();
198
- suppressionsByFile.set(f, bucket);
199
- }
200
- for (const id of ignoreList)
201
- bucket.add(id);
202
- }
203
- }
204
- if (suppressionsByFile.size === 0) {
205
- return { issues, dropped: 0 };
206
- }
207
- const filtered = issues.filter((issue) => !suppressionsByFile.get(issue.file)?.has(issue.check));
190
+ const filtered = filterLintIgnoredByOwners(issues, ctx.entries.map((entry) => entry.metadata));
208
191
  return { issues: filtered, dropped: issues.length - filtered.length };
209
192
  }
210
193
  /**
@@ -283,7 +266,7 @@ function downgradeAggregateSizeRules(issues, files, ctx) {
283
266
  * @param ctx - Patch queue context used to attribute file → owning patch
284
267
  * @returns Size issues, each attributed to its owning patch by message prefix
285
268
  */
286
- async function lintOwningPatchSizes(engineDir, filesAffected, ctx) {
269
+ async function lintOwningPatchSizes(engineDir, filesAffected, ctx, config) {
287
270
  const listed = new Set(filesAffected);
288
271
  const owners = new Map();
289
272
  const ownedListed = new Set();
@@ -302,26 +285,24 @@ async function lintOwningPatchSizes(engineDir, filesAffected, ctx) {
302
285
  owners.set(entry.filename, entry);
303
286
  }
304
287
  const issues = [];
305
- const lineCountForFiles = async (relPaths) => {
288
+ const sizeForFiles = async (relPaths) => {
306
289
  const existing = [];
307
290
  for (const f of relPaths) {
308
291
  if (await pathExists(join(engineDir, f)))
309
292
  existing.push(f);
310
293
  }
311
- if (existing.length === 0)
312
- return 0;
313
- const diff = await getDiffForFilesAgainstHead(engineDir, existing);
314
- return countNonBinaryDiffLines(diff).textLines;
294
+ const diff = existing.length > 0 ? await getDiffForFilesAgainstHead(engineDir, existing) : '';
295
+ // The file count stays the owner's whole `filesAffected`, as before.
296
+ return measurePatchSize(engineDir, diff, relPaths, config);
315
297
  };
316
298
  for (const entry of owners.values()) {
317
299
  const md = entry.metadata;
318
300
  if (!md)
319
301
  continue;
320
- const lineCount = await lineCountForFiles(md.filesAffected);
321
- const ignore = md.lintIgnore?.length ? new Set(md.lintIgnore) : undefined;
322
- for (const issue of lintPatchSize(md.filesAffected, lineCount, md.tier)) {
323
- if (ignore?.has(issue.check))
324
- continue;
302
+ const size = await sizeForFiles(md.filesAffected);
303
+ const ignore = buildLintIgnoreChecks(md.lintIgnore);
304
+ const sized = lintPatchSize(md.filesAffected, size, md.tier, config);
305
+ for (const issue of partitionLintIgnoredIssues(sized, ignore).kept) {
325
306
  issues.push({ ...issue, message: `${entry.filename}: ${issue.message}` });
326
307
  }
327
308
  }
@@ -329,8 +310,8 @@ async function lintOwningPatchSizes(engineDir, filesAffected, ctx) {
329
310
  // unit so a genuinely oversized fresh change still surfaces.
330
311
  const unowned = filesAffected.filter((f) => !ownedListed.has(f));
331
312
  if (unowned.length > 0) {
332
- const lineCount = await lineCountForFiles(unowned);
333
- issues.push(...lintPatchSize(unowned, lineCount));
313
+ const size = await sizeForFiles(unowned);
314
+ issues.push(...lintPatchSize(unowned, size, undefined, config));
334
315
  }
335
316
  return issues;
336
317
  }
@@ -460,7 +441,7 @@ export async function lintCommand(projectRoot, files, options = {}) {
460
441
  })),
461
442
  ];
462
443
  if (files.length > 0 && ctx) {
463
- issues.push(...(await lintOwningPatchSizes(paths.engine, filesAffected, ctx)));
444
+ issues.push(...(await lintOwningPatchSizes(paths.engine, filesAffected, ctx, config)));
464
445
  }
465
446
  // Cross-patch rules operate over the whole queue, so run them whenever a
466
447
  // patches directory exists. They surface duplicate /dev/null creations
@@ -1,5 +1,6 @@
1
1
  import { registerBootstrap } from './bootstrap.js';
2
2
  import { registerBuild } from './build.js';
3
+ import { registerCheck } from './check.js';
3
4
  import { registerConfig } from './config.js';
4
5
  import { registerDiscard } from './discard.js';
5
6
  import { registerDoctor } from './doctor.js';
@@ -60,5 +61,6 @@ export const COMMAND_MANIFEST = [
60
61
  { name: 'lint', group: 'diagnostics', register: registerLint },
61
62
  { name: 'typecheck', group: 'diagnostics', register: registerTypecheck },
62
63
  { name: 'verify', group: 'diagnostics', register: registerVerify },
64
+ { name: 'check', group: 'diagnostics', register: registerCheck },
63
65
  { name: 'furnace', group: 'components', register: registerFurnace },
64
66
  ];
@@ -42,5 +42,7 @@ function formatGapNotice(targetFilename, message) {
42
42
  const where = orders ? `a gap at ${orders}` : 'a numeric gap';
43
43
  return (`deleting ${targetFilename} leaves ${range} with ${where}; the queue will fail ` +
44
44
  'numeric-gap (patchPolicy.allowGaps is false) until ' +
45
- `"fireforge export --order ${fill}" fills it or "fireforge patch compact" renumbers.`);
45
+ `"fireforge export --order ${fill}" fills it or "fireforge patch compact" renumbers. ` +
46
+ `"fireforge patch retire ${targetFilename}" removes the patch and keeps its order as a ` +
47
+ 'retired slot instead, so no gap opens.');
46
48
  }
@@ -4,7 +4,7 @@
4
4
  * Destructive: refuses when a later patch imports a module owned by the
5
5
  * target (that would leave a dangling forward import), prompts for
6
6
  * confirmation interactively, requires `--yes` for non-TTY, supports
7
- * `--dry-run`, and appends to `patches/.fireforge-history.jsonl` on success.
7
+ * `--dry-run`, and appends to the patch history log on success.
8
8
  */
9
9
  import { Command } from 'commander';
10
10
  import type { CommandContext } from '../../types/cli.js';
@@ -5,14 +5,13 @@
5
5
  * Destructive: refuses when a later patch imports a module owned by the
6
6
  * target (that would leave a dangling forward import), prompts for
7
7
  * confirmation interactively, requires `--yes` for non-TTY, supports
8
- * `--dry-run`, and appends to `patches/.fireforge-history.jsonl` on success.
8
+ * `--dry-run`, and appends to the patch history log on success.
9
9
  */
10
- import { basename } from 'node:path';
11
10
  import { Command } from 'commander';
12
11
  import { loadConfig } from '../../core/config.js';
13
12
  import { confirmDestructive } from '../../core/destructive.js';
14
13
  import { appendHistoryBestEffort } from '../../core/history-log.js';
15
- import { buildPatchQueueContext, extractImportSpecifiersWithLines, findForwardImportIgnoreLines, isForwardImportableFile, } from '../../core/patch-lint.js';
14
+ import { buildPatchQueueContext } from '../../core/patch-lint.js';
16
15
  import { withPatchDirectoryLock } from '../../core/patch-lock.js';
17
16
  import { removePatchFileAndManifest } from '../../core/patch-manifest.js';
18
17
  import { GeneralError } from '../../errors/base.js';
@@ -21,6 +20,7 @@ import { addWaitLockOption, pickDefined, resolveWaitLockSeconds } from '../../ut
21
20
  import { proceedAfterDecision } from '../destructive-decision.js';
22
21
  import { describeProjectedGaps } from './delete-gap-notice.js';
23
22
  import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
23
+ import { collectRemovalDependents, queueSnapshot, removalConflict, warnDanglingStagedOwners, } from './removal-dependents.js';
24
24
  /**
25
25
  * Runs the `patch delete` command: removes a patch file and its manifest
26
26
  * row atomically, refusing when a later patch imports a leaf owned by the
@@ -37,81 +37,15 @@ export async function patchDeleteCommand(projectRoot, identifier, options = {})
37
37
  });
38
38
  const target = requirePatchTarget(identifier, manifest.patches);
39
39
  // Build the full queue context once so we can scan each patch's newFiles
40
- // without re-parsing for the dependency check below.
40
+ // without re-parsing for the dependency check below. Hard refusal: an
41
+ // import owned by a later patch that points at any of the target's
42
+ // newly-created files is a dependency on the target.
41
43
  const baseCtx = await buildPatchQueueContext(paths.patches);
42
- // Hard refusal: an import owned by a later patch that points at any of
43
- // the target's newly-created files is a dependency on the target. That
44
- // check is built directly from `baseCtx` rather than by diffing lint runs
45
- // over a projected state.
46
- const targetEntry = baseCtx.entries.find((e) => e.filename === target.filename);
47
- if (!targetEntry || JSON.stringify(targetEntry.metadata) !== JSON.stringify(target)) {
48
- throw new GeneralError('Patch queue changed while preparing deletion. Re-run patch delete to review the current target.');
49
- }
50
- const targetNewFileLeaves = new Set();
51
- for (const fullPath of targetEntry.newFiles.keys()) {
52
- targetNewFileLeaves.add(basename(fullPath));
53
- }
54
- // Scan every later patch's new files and its added lines on pre-existing
55
- // files for import specifiers that resolve to a leaf owned by the target.
56
- // Uses the shared specifier extractor so dynamic `import()` and
57
- // `ChromeUtils.defineESModuleGetters` are picked up: the forward-import
58
- // lint rule already covers those forms, and delete safety must match the
59
- // same set or it silently drops dependencies.
60
- //
61
- // Both source-site maps are covered: `newFiles` (files the later patch
62
- // creates) and `modifiedFileAdditions` (added lines against files that
63
- // already exist). Scanning only newFiles misses a later patch adding
64
- // `import "./TargetHelper.sys.mjs"` to an existing file.
65
- const dependents = [];
66
- const scanSite = (entryFilename, sitePath, content) => {
67
- if (!isForwardImportableFile(sitePath))
68
- return false;
69
- const ignoredLines = findForwardImportIgnoreLines(content);
70
- const specifiers = extractImportSpecifiersWithLines(content);
71
- for (const { specifier, line } of specifiers) {
72
- if (ignoredLines.has(line))
73
- continue;
74
- const cleaned = specifier.split(/[?#]/)[0] ?? specifier;
75
- const leaf = basename(cleaned);
76
- if (!leaf || !isForwardImportableFile(leaf))
77
- continue;
78
- if (targetNewFileLeaves.has(leaf)) {
79
- dependents.push(`${entryFilename} (${sitePath}) imports "${specifier}" which would be deleted`);
80
- return true;
81
- }
82
- }
83
- return false;
84
- };
85
- for (const entry of baseCtx.entries) {
86
- if (entry.filename === target.filename)
87
- continue;
88
- if (entry.order < target.order)
89
- continue;
90
- let matched = false;
91
- for (const [newFile, content] of entry.newFiles) {
92
- if (scanSite(entry.filename, newFile, content)) {
93
- matched = true;
94
- break;
95
- }
96
- }
97
- if (matched)
98
- continue;
99
- for (const [modifiedPath, addedContent] of entry.modifiedFileAdditions) {
100
- if (scanSite(entry.filename, modifiedPath, addedContent))
101
- break;
102
- }
103
- }
104
- // Staged-dependency declarations on other patches may name the deleted
105
- // patch as their forward-import owner. The dangling reference also
106
- // surfaces via cross-patch lint later, but warning here puts the exact
107
- // cleanup command in front of the operator at decision time.
108
- const danglingOwnerHolders = baseCtx.entries.filter((entry) => entry.filename !== target.filename &&
109
- (entry.metadata?.stagedDependencies?.forwardImports ?? []).some((fi) => fi.owner === target.filename));
110
- for (const holder of danglingOwnerHolders) {
111
- warn(`${holder.filename} declares a staged dependency with owner ${target.filename}; ` +
112
- `after the delete, update it via "fireforge patch staged-dependency ${holder.filename} --remove ..." ` +
113
- 'or re-point the owner at the patch that will create the file.');
114
- }
44
+ const dependents = collectRemovalDependents(baseCtx, target, {
45
+ noun: 'deletion',
46
+ verb: 'delete',
47
+ });
48
+ warnDanglingStagedOwners(baseCtx, target, 'delete');
115
49
  // Under `patchPolicy.allowGaps: false` the removal can open a numeric gap
116
50
  // that every later lint and re-export refuses on. Naming it here, in
117
51
  // dry-run and at the prompt, puts the refill/compact choice in front of
@@ -120,20 +54,7 @@ export async function patchDeleteCommand(projectRoot, identifier, options = {})
120
54
  for (const line of gapNotices) {
121
55
  warn(line);
122
56
  }
123
- const conflicts = dependents.length > 0
124
- ? {
125
- // The wording spells out the runtime impact: `git apply`
126
- // does not resolve imports and succeeds even when a later patch
127
- // imports a file the target created, so the queue re-imports
128
- // cleanly. The breakage surfaces at browser startup when
129
- // `ChromeUtils.importESModule` cannot locate the deleted module.
130
- // Operators who plan to re-introduce the imported files (rename,
131
- // refactor) need to know this is the impact model rather than a
132
- // patch-application failure.
133
- 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.`,
134
- details: dependents,
135
- }
136
- : null;
57
+ const conflicts = removalConflict(dependents, target);
137
58
  const summary = [
138
59
  `delete ${target.filename} (category: ${target.category}, order: ${target.order})`,
139
60
  `description: ${target.description || '(none)'}`,
@@ -164,15 +85,9 @@ export async function patchDeleteCommand(projectRoot, identifier, options = {})
164
85
  // failure to the caller would mislead.
165
86
  await withPatchDirectoryLock(paths.patches, async () => {
166
87
  const current = await buildPatchQueueContext(paths.patches);
167
- const snapshot = (context) => JSON.stringify(context.entries.map(({ filename, order, diff, metadata }) => ({
168
- filename,
169
- order,
170
- diff,
171
- metadata,
172
- })));
173
88
  // A changed body or manifest invalidates the dependency scan approved
174
89
  // before the prompt. Recheck all of its inputs while holding the lock.
175
- if (snapshot(current) !== snapshot(baseCtx)) {
90
+ if (queueSnapshot(current) !== queueSnapshot(baseCtx)) {
176
91
  throw new GeneralError('Patch queue changed while waiting for confirmation or the lock. Re-run patch delete to review the current dependencies.');
177
92
  }
178
93
  await removePatchFileAndManifest(paths.patches, target.filename);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `fireforge patch <verb>` parent command. Groups single-patch mutations and
3
3
  * planners (`compact`, `delete`, `lint-ignore`, `move-files`, `rename`,
4
- * `reorder`, `staged-dependency`, `tier`) so they do not clutter the
4
+ * `reorder`, `retire`, `staged-dependency`, `tier`) so they do not clutter the
5
5
  * top-level command list. Queue-level verbs like `lint`, `export`, `verify`,
6
6
  * and `status` stay flat.
7
7
  */
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * `fireforge patch <verb>` parent command. Groups single-patch mutations and
4
4
  * planners (`compact`, `delete`, `lint-ignore`, `move-files`, `rename`,
5
- * `reorder`, `staged-dependency`, `tier`) so they do not clutter the
5
+ * `reorder`, `retire`, `staged-dependency`, `tier`) so they do not clutter the
6
6
  * top-level command list. Queue-level verbs like `lint`, `export`, `verify`,
7
7
  * and `status` stay flat.
8
8
  */
@@ -13,6 +13,7 @@ import { registerPatchLintIgnore } from './lint-ignore.js';
13
13
  import { registerPatchMoveFiles } from './move-files.js';
14
14
  import { registerPatchRename } from './rename.js';
15
15
  import { registerPatchReorder } from './reorder.js';
16
+ import { registerPatchRetire } from './retire.js';
16
17
  import { registerPatchSplit } from './split.js';
17
18
  import { registerPatchStagedDependency } from './staged-dependency.js';
18
19
  import { registerPatchTier } from './tier.js';
@@ -25,7 +26,7 @@ import { registerPatchTier } from './tier.js';
25
26
  export function registerPatch(program, context) {
26
27
  const patch = program
27
28
  .command('patch')
28
- .description('Manage individual patches in the queue (compact, delete, lint-ignore, move-files, rename, reorder, staged-dependency, tier)')
29
+ .description('Manage individual patches in the queue (compact, delete, lint-ignore, move-files, rename, reorder, retire, staged-dependency, tier)')
29
30
  // Match `fireforge furnace`'s no-args contract: print the group's help
30
31
  // and exit 0. Without this default action, commander routes
31
32
  // `fireforge patch` (no subcommand) through its own help-then-exit-1
@@ -40,6 +41,7 @@ export function registerPatch(program, context) {
40
41
  registerPatchMoveFiles(patch, context);
41
42
  registerPatchRename(patch, context);
42
43
  registerPatchReorder(patch, context);
44
+ registerPatchRetire(patch, context);
43
45
  registerPatchSplit(patch, context);
44
46
  registerPatchStagedDependency(patch, context);
45
47
  registerPatchTier(patch, context);
@@ -11,7 +11,11 @@
11
11
  * re-export's engine read + diff regeneration roundtrip is wasted.
12
12
  *
13
13
  * Modes are mutually exclusive: exactly one of `--add`, `--remove`, or
14
- * `--clear` must be supplied per invocation. The read-modify-write
14
+ * `--clear` must be supplied per invocation. `--add <rule> --max <n>
15
+ * --reason <text>` writes a size waiver with a ceiling (see
16
+ * `core/patch-lint-ignore.ts`) instead of a bare check ID; an `--add` for a
17
+ * rule that already has an entry replaces it, so the same command moves a
18
+ * ceiling or widens it back to a bare waiver. The read-modify-write
15
19
  * happens inside the patch directory lock via {@link mutatePatchMetadata}
16
20
  * so a concurrent writer cannot interleave between the read and the
17
21
  * write, which matters when an operator scripts repeated invocations or