@hominis/fireforge 0.49.2 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (334) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +1 -1
  3. package/dist/bin/fireforge.js +6 -0
  4. package/dist/build-info.json +4 -4
  5. package/dist/src/commands/build-prelock.d.ts +20 -0
  6. package/dist/src/commands/build-prelock.js +50 -0
  7. package/dist/src/commands/build.d.ts +3 -1
  8. package/dist/src/commands/build.js +13 -6
  9. package/dist/src/commands/check-report.d.ts +28 -0
  10. package/dist/src/commands/check-report.js +78 -0
  11. package/dist/src/commands/check-run.d.ts +28 -0
  12. package/dist/src/commands/check-run.js +104 -0
  13. package/dist/src/commands/check-steps.d.ts +67 -0
  14. package/dist/src/commands/check-steps.js +175 -0
  15. package/dist/src/commands/check.d.ts +32 -0
  16. package/dist/src/commands/check.js +160 -0
  17. package/dist/src/commands/config.js +5 -6
  18. package/dist/src/commands/doctor-engine-eslint.js +6 -1
  19. package/dist/src/commands/doctor-source-pin.js +4 -1
  20. package/dist/src/commands/export-adopt-check.d.ts +38 -0
  21. package/dist/src/commands/export-adopt-check.js +98 -0
  22. package/dist/src/commands/export-flow.js +29 -14
  23. package/dist/src/commands/export-placement-gate.d.ts +6 -2
  24. package/dist/src/commands/export-shared.d.ts +2 -1
  25. package/dist/src/commands/export-staged-registration.d.ts +46 -0
  26. package/dist/src/commands/export-staged-registration.js +91 -0
  27. package/dist/src/commands/forward-import-remedy.d.ts +32 -0
  28. package/dist/src/commands/forward-import-remedy.js +63 -0
  29. package/dist/src/commands/furnace/apply.js +3 -3
  30. package/dist/src/commands/furnace/chrome-doc-remove.d.ts +2 -1
  31. package/dist/src/commands/furnace/chrome-doc-remove.js +2 -2
  32. package/dist/src/commands/furnace/chrome-doc.d.ts +2 -1
  33. package/dist/src/commands/furnace/chrome-doc.js +2 -2
  34. package/dist/src/commands/furnace/create.js +2 -2
  35. package/dist/src/commands/furnace/deploy-named.d.ts +48 -0
  36. package/dist/src/commands/furnace/deploy-named.js +107 -0
  37. package/dist/src/commands/furnace/deploy.d.ts +3 -2
  38. package/dist/src/commands/furnace/deploy.js +50 -86
  39. package/dist/src/commands/furnace/diff.js +26 -3
  40. package/dist/src/commands/furnace/index.js +66 -41
  41. package/dist/src/commands/furnace/init.js +13 -6
  42. package/dist/src/commands/furnace/override.js +4 -2
  43. package/dist/src/commands/furnace/preview.js +4 -3
  44. package/dist/src/commands/furnace/refresh.js +2 -2
  45. package/dist/src/commands/furnace/remove.js +4 -3
  46. package/dist/src/commands/furnace/rename-dry-run.d.ts +29 -0
  47. package/dist/src/commands/furnace/rename-dry-run.js +127 -0
  48. package/dist/src/commands/furnace/rename-helpers.d.ts +7 -0
  49. package/dist/src/commands/furnace/rename-helpers.js +15 -1
  50. package/dist/src/commands/furnace/rename-registrations.d.ts +41 -0
  51. package/dist/src/commands/furnace/rename-registrations.js +93 -0
  52. package/dist/src/commands/furnace/rename.d.ts +2 -1
  53. package/dist/src/commands/furnace/rename.js +28 -66
  54. package/dist/src/commands/furnace/scan.d.ts +9 -4
  55. package/dist/src/commands/furnace/scan.js +11 -9
  56. package/dist/src/commands/furnace/sync.js +3 -2
  57. package/dist/src/commands/furnace/validate-report.d.ts +45 -0
  58. package/dist/src/commands/furnace/validate-report.js +91 -0
  59. package/dist/src/commands/furnace/validate.d.ts +1 -1
  60. package/dist/src/commands/furnace/validate.js +127 -114
  61. package/dist/src/commands/furnace/validation-output.js +14 -3
  62. package/dist/src/commands/lint-per-patch.js +31 -60
  63. package/dist/src/commands/lint-report.d.ts +8 -4
  64. package/dist/src/commands/lint-report.js +15 -5
  65. package/dist/src/commands/lint.js +15 -34
  66. package/dist/src/commands/manifest.js +2 -0
  67. package/dist/src/commands/patch/delete-gap-notice.js +3 -1
  68. package/dist/src/commands/patch/delete.d.ts +1 -1
  69. package/dist/src/commands/patch/delete.js +13 -98
  70. package/dist/src/commands/patch/index.d.ts +1 -1
  71. package/dist/src/commands/patch/index.js +4 -2
  72. package/dist/src/commands/patch/lint-ignore.d.ts +5 -1
  73. package/dist/src/commands/patch/lint-ignore.js +65 -28
  74. package/dist/src/commands/patch/move-files-create.js +26 -17
  75. package/dist/src/commands/patch/move-files-into.js +5 -3
  76. package/dist/src/commands/patch/move-files.js +3 -1
  77. package/dist/src/commands/patch/removal-dependents.d.ts +63 -0
  78. package/dist/src/commands/patch/removal-dependents.js +133 -0
  79. package/dist/src/commands/patch/rename.js +7 -0
  80. package/dist/src/commands/patch/retire.d.ts +34 -0
  81. package/dist/src/commands/patch/retire.js +128 -0
  82. package/dist/src/commands/patch/split-plan.d.ts +7 -7
  83. package/dist/src/commands/patch/split-plan.js +45 -28
  84. package/dist/src/commands/patch/split-staged.d.ts +108 -0
  85. package/dist/src/commands/patch/split-staged.js +202 -0
  86. package/dist/src/commands/patch/split.d.ts +5 -0
  87. package/dist/src/commands/patch/split.js +51 -15
  88. package/dist/src/commands/patch/staged-dependency-batch.d.ts +48 -0
  89. package/dist/src/commands/patch/staged-dependency-batch.js +176 -0
  90. package/dist/src/commands/patch/staged-dependency-entries.d.ts +36 -0
  91. package/dist/src/commands/patch/staged-dependency-entries.js +52 -0
  92. package/dist/src/commands/patch/staged-dependency-from.d.ts +14 -0
  93. package/dist/src/commands/patch/staged-dependency-from.js +108 -0
  94. package/dist/src/commands/patch/staged-dependency.js +14 -41
  95. package/dist/src/commands/re-export-adjacent-registrations.d.ts +20 -0
  96. package/dist/src/commands/re-export-adjacent-registrations.js +75 -0
  97. package/dist/src/commands/re-export-adjacent.d.ts +45 -2
  98. package/dist/src/commands/re-export-adjacent.js +156 -37
  99. package/dist/src/commands/re-export-drift.d.ts +36 -8
  100. package/dist/src/commands/re-export-drift.js +73 -49
  101. package/dist/src/commands/re-export-files.js +11 -8
  102. package/dist/src/commands/re-export-lint.d.ts +2 -1
  103. package/dist/src/commands/re-export-lint.js +23 -10
  104. package/dist/src/commands/re-export-options.js +45 -4
  105. package/dist/src/commands/re-export-preflight.d.ts +23 -0
  106. package/dist/src/commands/re-export-preflight.js +160 -0
  107. package/dist/src/commands/re-export-register.js +42 -17
  108. package/dist/src/commands/re-export-removed.d.ts +12 -1
  109. package/dist/src/commands/re-export-removed.js +41 -11
  110. package/dist/src/commands/re-export-scan-owner.d.ts +44 -0
  111. package/dist/src/commands/re-export-scan-owner.js +116 -0
  112. package/dist/src/commands/re-export-scan.d.ts +17 -9
  113. package/dist/src/commands/re-export-scan.js +56 -23
  114. package/dist/src/commands/re-export-single.js +22 -6
  115. package/dist/src/commands/re-export-summary.d.ts +28 -0
  116. package/dist/src/commands/re-export-summary.js +36 -0
  117. package/dist/src/commands/re-export.js +37 -24
  118. package/dist/src/commands/rebase/index.js +17 -17
  119. package/dist/src/commands/run.js +16 -2
  120. package/dist/src/commands/status-check.d.ts +55 -16
  121. package/dist/src/commands/status-check.js +92 -37
  122. package/dist/src/commands/status-json.d.ts +22 -9
  123. package/dist/src/commands/status-json.js +47 -32
  124. package/dist/src/commands/status-output.js +3 -1
  125. package/dist/src/commands/status-scope.d.ts +37 -0
  126. package/dist/src/commands/status-scope.js +54 -0
  127. package/dist/src/commands/status.js +66 -23
  128. package/dist/src/commands/test-browser-preflight.js +19 -3
  129. package/dist/src/commands/test-build-furnace.d.ts +21 -0
  130. package/dist/src/commands/test-build-furnace.js +48 -0
  131. package/dist/src/commands/test-build-phase.js +47 -41
  132. package/dist/src/commands/test-checkjs.js +7 -0
  133. package/dist/src/commands/test-diagnose.js +7 -0
  134. package/dist/src/commands/test-modes.d.ts +16 -1
  135. package/dist/src/commands/test-modes.js +24 -1
  136. package/dist/src/commands/test-multi-verdict.d.ts +61 -0
  137. package/dist/src/commands/test-multi-verdict.js +111 -0
  138. package/dist/src/commands/test-orchestrate.d.ts +43 -0
  139. package/dist/src/commands/test-orchestrate.js +222 -0
  140. package/dist/src/commands/test-perf-scope.d.ts +29 -0
  141. package/dist/src/commands/test-perf-scope.js +83 -0
  142. package/dist/src/commands/test-prelock.d.ts +51 -0
  143. package/dist/src/commands/test-prelock.js +177 -0
  144. package/dist/src/commands/test-register.js +54 -10
  145. package/dist/src/commands/test-run.js +4 -1
  146. package/dist/src/commands/test-stale-gate.js +57 -27
  147. package/dist/src/commands/test-verdict.d.ts +10 -1
  148. package/dist/src/commands/test-verdict.js +28 -1
  149. package/dist/src/commands/test.d.ts +6 -1
  150. package/dist/src/commands/test.js +66 -60
  151. package/dist/src/commands/token-lint.d.ts +14 -0
  152. package/dist/src/commands/token-lint.js +111 -0
  153. package/dist/src/commands/token.js +10 -0
  154. package/dist/src/commands/verify-scope.d.ts +28 -0
  155. package/dist/src/commands/verify-scope.js +67 -0
  156. package/dist/src/commands/verify.d.ts +10 -2
  157. package/dist/src/commands/verify.js +59 -35
  158. package/dist/src/core/build-baseline-types.d.ts +46 -0
  159. package/dist/src/core/build-baseline-validate.js +19 -0
  160. package/dist/src/core/build-baseline.d.ts +5 -1
  161. package/dist/src/core/build-baseline.js +3 -1
  162. package/dist/src/core/build-overwrite-guard.d.ts +50 -0
  163. package/dist/src/core/build-overwrite-guard.js +84 -4
  164. package/dist/src/core/build-prepare.d.ts +7 -0
  165. package/dist/src/core/build-prepare.js +14 -8
  166. package/dist/src/core/build-run-record.d.ts +74 -0
  167. package/dist/src/core/build-run-record.js +136 -0
  168. package/dist/src/core/checkjs-support-heads.d.ts +41 -0
  169. package/dist/src/core/checkjs-support-heads.js +104 -0
  170. package/dist/src/core/config-paths.d.ts +2 -2
  171. package/dist/src/core/config-paths.js +8 -0
  172. package/dist/src/core/config-validate-check.d.ts +8 -0
  173. package/dist/src/core/config-validate-check.js +143 -0
  174. package/dist/src/core/config-validate-file-size.d.ts +9 -6
  175. package/dist/src/core/config-validate-file-size.js +91 -2
  176. package/dist/src/core/config-validate-history.d.ts +11 -0
  177. package/dist/src/core/config-validate-history.js +38 -0
  178. package/dist/src/core/config-validate-patch-policy.js +100 -0
  179. package/dist/src/core/config-validate-test-toolchains.js +7 -0
  180. package/dist/src/core/config-validate-token-scales.d.ts +6 -0
  181. package/dist/src/core/config-validate-token-scales.js +50 -0
  182. package/dist/src/core/config-validate.js +14 -5
  183. package/dist/src/core/config.d.ts +5 -0
  184. package/dist/src/core/config.js +16 -0
  185. package/dist/src/core/coverage-dispatch.d.ts +24 -0
  186. package/dist/src/core/coverage-dispatch.js +44 -0
  187. package/dist/src/core/coverage-extend.d.ts +70 -45
  188. package/dist/src/core/coverage-extend.js +199 -144
  189. package/dist/src/core/coverage-records.d.ts +97 -0
  190. package/dist/src/core/coverage-records.js +350 -0
  191. package/dist/src/core/destructive.d.ts +7 -4
  192. package/dist/src/core/destructive.js +10 -10
  193. package/dist/src/core/engine-session-lock.d.ts +10 -1
  194. package/dist/src/core/engine-session-lock.js +28 -8
  195. package/dist/src/core/export-moved-code-shape.d.ts +49 -11
  196. package/dist/src/core/export-moved-code-shape.js +135 -44
  197. package/dist/src/core/file-lock-queue.d.ts +67 -0
  198. package/dist/src/core/file-lock-queue.js +221 -0
  199. package/dist/src/core/file-lock.d.ts +4 -25
  200. package/dist/src/core/file-lock.js +105 -138
  201. package/dist/src/core/furnace-apply-output.d.ts +10 -0
  202. package/dist/src/core/furnace-apply-output.js +31 -1
  203. package/dist/src/core/furnace-apply.js +12 -2
  204. package/dist/src/core/furnace-composed-source.d.ts +76 -0
  205. package/dist/src/core/furnace-composed-source.js +167 -0
  206. package/dist/src/core/furnace-config-validate-exceptions.d.ts +9 -0
  207. package/dist/src/core/furnace-config-validate-exceptions.js +71 -0
  208. package/dist/src/core/furnace-config-validate.js +8 -0
  209. package/dist/src/core/furnace-css-fragments.d.ts +10 -0
  210. package/dist/src/core/furnace-css-fragments.js +26 -0
  211. package/dist/src/core/furnace-operation.d.ts +3 -3
  212. package/dist/src/core/furnace-registration-remove.d.ts +22 -1
  213. package/dist/src/core/furnace-registration-remove.js +53 -17
  214. package/dist/src/core/furnace-registration-rename.d.ts +26 -0
  215. package/dist/src/core/furnace-registration-rename.js +77 -0
  216. package/dist/src/core/furnace-registration.d.ts +14 -4
  217. package/dist/src/core/furnace-registration.js +44 -10
  218. package/dist/src/core/furnace-staleness.d.ts +7 -0
  219. package/dist/src/core/furnace-staleness.js +34 -24
  220. package/dist/src/core/furnace-validate-accessibility.d.ts +4 -2
  221. package/dist/src/core/furnace-validate-accessibility.js +10 -1
  222. package/dist/src/core/furnace-validate-exceptions.d.ts +40 -0
  223. package/dist/src/core/furnace-validate-exceptions.js +43 -0
  224. package/dist/src/core/furnace-validate-icon-button.d.ts +7 -0
  225. package/dist/src/core/furnace-validate-icon-button.js +49 -0
  226. package/dist/src/core/furnace-validate.js +1 -1
  227. package/dist/src/core/harness-port-fallback.d.ts +60 -0
  228. package/dist/src/core/harness-port-fallback.js +96 -0
  229. package/dist/src/core/history-log-file.d.ts +61 -0
  230. package/dist/src/core/history-log-file.js +157 -0
  231. package/dist/src/core/history-log.d.ts +2 -1
  232. package/dist/src/core/history-log.js +2 -1
  233. package/dist/src/core/json-preserving-edit.d.ts +22 -0
  234. package/dist/src/core/json-preserving-edit.js +199 -0
  235. package/dist/src/core/mach.js +8 -1
  236. package/dist/src/core/negative-control-diff.d.ts +39 -0
  237. package/dist/src/core/negative-control-diff.js +136 -0
  238. package/dist/src/core/negative-control.d.ts +138 -0
  239. package/dist/src/core/negative-control.js +356 -0
  240. package/dist/src/core/objdir-install-manifests.d.ts +73 -0
  241. package/dist/src/core/objdir-install-manifests.js +226 -0
  242. package/dist/src/core/patch-export-update.d.ts +2 -0
  243. package/dist/src/core/patch-export-update.js +1 -0
  244. package/dist/src/core/patch-export.js +3 -4
  245. package/dist/src/core/patch-lint-checkjs-any-import.d.ts +37 -0
  246. package/dist/src/core/patch-lint-checkjs-any-import.js +61 -0
  247. package/dist/src/core/patch-lint-checkjs.js +8 -11
  248. package/dist/src/core/patch-lint-code-lines.d.ts +59 -0
  249. package/dist/src/core/patch-lint-code-lines.js +105 -0
  250. package/dist/src/core/patch-lint-cross.js +12 -4
  251. package/dist/src/core/patch-lint-css.d.ts +8 -2
  252. package/dist/src/core/patch-lint-css.js +10 -4
  253. package/dist/src/core/patch-lint-file-size.d.ts +12 -5
  254. package/dist/src/core/patch-lint-file-size.js +22 -11
  255. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +18 -0
  256. package/dist/src/core/patch-lint-forward-registration-extended.js +35 -10
  257. package/dist/src/core/patch-lint-forward-registration.js +10 -0
  258. package/dist/src/core/patch-lint-ignore.d.ts +152 -0
  259. package/dist/src/core/patch-lint-ignore.js +299 -0
  260. package/dist/src/core/patch-lint-size-thresholds.d.ts +52 -0
  261. package/dist/src/core/patch-lint-size-thresholds.js +110 -0
  262. package/dist/src/core/patch-lint-size.d.ts +117 -0
  263. package/dist/src/core/patch-lint-size.js +323 -0
  264. package/dist/src/core/patch-lint-staged-registration.d.ts +18 -1
  265. package/dist/src/core/patch-lint-staged-registration.js +20 -0
  266. package/dist/src/core/patch-lint-token-scale.d.ts +39 -0
  267. package/dist/src/core/patch-lint-token-scale.js +109 -0
  268. package/dist/src/core/patch-lint.d.ts +11 -83
  269. package/dist/src/core/patch-lint.js +64 -286
  270. package/dist/src/core/patch-lock.js +7 -6
  271. package/dist/src/core/patch-manifest-consistency.js +7 -6
  272. package/dist/src/core/patch-manifest-io.d.ts +6 -3
  273. package/dist/src/core/patch-manifest-io.js +19 -8
  274. package/dist/src/core/patch-manifest-validate.js +34 -2
  275. package/dist/src/core/patch-order-legal.d.ts +25 -0
  276. package/dist/src/core/patch-order-legal.js +83 -0
  277. package/dist/src/core/patch-policy-content.d.ts +40 -0
  278. package/dist/src/core/patch-policy-content.js +216 -0
  279. package/dist/src/core/patch-policy.d.ts +16 -3
  280. package/dist/src/core/patch-policy.js +16 -9
  281. package/dist/src/core/patch-retired.d.ts +35 -0
  282. package/dist/src/core/patch-retired.js +54 -0
  283. package/dist/src/core/patch-subset.d.ts +16 -0
  284. package/dist/src/core/patch-subset.js +66 -0
  285. package/dist/src/core/preflight-wait.d.ts +20 -0
  286. package/dist/src/core/preflight-wait.js +38 -0
  287. package/dist/src/core/rebase-from-version.d.ts +46 -0
  288. package/dist/src/core/rebase-from-version.js +71 -0
  289. package/dist/src/core/run-log.d.ts +20 -0
  290. package/dist/src/core/run-log.js +89 -3
  291. package/dist/src/core/run-profile.d.ts +7 -0
  292. package/dist/src/core/run-profile.js +7 -1
  293. package/dist/src/core/smoke-patterns.d.ts +5 -0
  294. package/dist/src/core/smoke-patterns.js +13 -0
  295. package/dist/src/core/test-harness-crash.d.ts +18 -1
  296. package/dist/src/core/test-harness-crash.js +6 -4
  297. package/dist/src/core/test-harness-signature.d.ts +17 -0
  298. package/dist/src/core/test-harness-signature.js +4 -0
  299. package/dist/src/core/test-run-completeness.d.ts +57 -0
  300. package/dist/src/core/test-run-completeness.js +144 -0
  301. package/dist/src/core/test-stale-check.d.ts +27 -1
  302. package/dist/src/core/test-stale-check.js +39 -49
  303. package/dist/src/core/test-stale-exempt.d.ts +40 -0
  304. package/dist/src/core/test-stale-exempt.js +70 -0
  305. package/dist/src/core/token-lint.d.ts +50 -0
  306. package/dist/src/core/token-lint.js +126 -0
  307. package/dist/src/core/tree-guard.js +3 -0
  308. package/dist/src/core/typecheck-shim.d.ts +1 -1
  309. package/dist/src/core/typecheck-shim.js +90 -2
  310. package/dist/src/index.d.ts +8 -3
  311. package/dist/src/index.js +3 -1
  312. package/dist/src/types/commands/index.d.ts +2 -2
  313. package/dist/src/types/commands/options.d.ts +148 -21
  314. package/dist/src/types/commands/patches.d.ts +75 -1
  315. package/dist/src/types/config.d.ts +192 -0
  316. package/dist/src/types/config.js +9 -0
  317. package/dist/src/types/furnace.d.ts +43 -1
  318. package/dist/src/types/furnace.js +7 -0
  319. package/dist/src/types/index.d.ts +1 -1
  320. package/dist/src/utils/options.d.ts +34 -6
  321. package/dist/src/utils/options.js +86 -5
  322. package/dist/src/utils/path-glob.d.ts +15 -0
  323. package/dist/src/utils/path-glob.js +47 -0
  324. package/dist/src/utils/regex.d.ts +15 -3
  325. package/dist/src/utils/regex.js +37 -16
  326. package/docs/configuration.md +282 -9
  327. package/docs/exit-codes.md +18 -1
  328. package/docs/furnace.md +97 -3
  329. package/docs/lifecycle-invariants.md +52 -19
  330. package/docs/machine-output.md +74 -12
  331. package/docs/patch-workflow.md +297 -22
  332. package/docs/run-logs.md +20 -1
  333. package/docs/testing.md +276 -20
  334. package/package.json +1 -1
@@ -0,0 +1,175 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * The step plan of `fireforge check`: which commands run, with which
4
+ * arguments, in which order constraints, and how each one's output is
5
+ * condensed into its table row.
6
+ *
7
+ * Every built-in step is the real command run as a child `fireforge`
8
+ * process, not an in-process call. That is what keeps the gate's output
9
+ * sane (each command owns its stdout, its machine mode and its error
10
+ * boundary) and what avoids double locking: `check` itself takes no lock,
11
+ * so each child takes exactly the locks its command takes standalone.
12
+ * None of the built-in steps takes the engine session lock, which is why
13
+ * they can run side by side.
14
+ */
15
+ import { join } from 'node:path';
16
+ import { GeneralError } from '../errors/base.js';
17
+ /**
18
+ * How to re-run this CLI as a child: the same Node, the same loader flags
19
+ * (minus debugger flags, whose port the parent already holds), the same
20
+ * entry script.
21
+ */
22
+ function fireforgeInvocation() {
23
+ const entry = process.argv[1];
24
+ if (entry === undefined) {
25
+ throw new GeneralError('Cannot resolve the fireforge CLI entry point for check steps.');
26
+ }
27
+ const loaderFlags = process.execArgv.filter((flag) => !flag.startsWith('--inspect'));
28
+ return { command: process.execPath, prefix: [...loaderFlags, entry] };
29
+ }
30
+ /** Strips clack's gutter glyphs so a captured line reads as plain text. */
31
+ function plainLine(line) {
32
+ return line.replace(/^[\s│■●◆◇▲└┌|]+/u, '').trim();
33
+ }
34
+ /**
35
+ * The generic detail: nothing on success, and on failure the last line
36
+ * the step printed (its refusal, usually), cut to fit a table cell.
37
+ */
38
+ export function lastOutputLine(combined, exitCode) {
39
+ if (exitCode === 0)
40
+ return '';
41
+ const lines = combined
42
+ .split(/\r?\n/)
43
+ .map(plainLine)
44
+ .filter((line) => line.length > 0);
45
+ const last = lines.at(-1) ?? '';
46
+ return last.length > 120 ? `${last.slice(0, 117)}...` : last;
47
+ }
48
+ function parseJsonDocument(stdout) {
49
+ try {
50
+ const parsed = JSON.parse(stdout);
51
+ return typeof parsed === 'object' && parsed !== null
52
+ ? parsed
53
+ : undefined;
54
+ }
55
+ catch {
56
+ return undefined;
57
+ }
58
+ }
59
+ /** Status detail from its `--json --summary` document: the failing classes. */
60
+ export function summarizeStatus(output) {
61
+ const doc = parseJsonDocument(output.stdout);
62
+ if (doc?.summary === undefined)
63
+ return lastOutputLine(output.combined, output.exitCode);
64
+ const offenders = doc.check?.offenders ?? [];
65
+ if (offenders.length > 0) {
66
+ return offenders.map((entry) => `${entry.count} ${entry.classification}`).join(', ');
67
+ }
68
+ return `${doc.summary.total ?? 0} changed file(s), none failing`;
69
+ }
70
+ /** Furnace detail from its `--json` document: counts after exceptions. */
71
+ export function summarizeFurnace(output) {
72
+ const doc = parseJsonDocument(output.stdout);
73
+ if (doc?.summary === undefined)
74
+ return lastOutputLine(output.combined, output.exitCode);
75
+ const { errors, warnings, excepted, staleExceptions } = doc.summary;
76
+ const extra = [
77
+ excepted > 0 ? `${excepted} excepted` : '',
78
+ staleExceptions > 0 ? `${staleExceptions} stale exception(s)` : '',
79
+ ].filter((part) => part.length > 0);
80
+ return [`${errors} error(s), ${warnings} warning(s)`, ...extra].join(', ');
81
+ }
82
+ /** Token-coverage detail: the coverage percentage it reports. */
83
+ export function summarizeTokenCoverage(output) {
84
+ const match = /Token coverage: (\d+)%/.exec(output.combined);
85
+ if (match?.[1] !== undefined)
86
+ return `coverage ${match[1]}%`;
87
+ if (output.combined.includes('No modified CSS files'))
88
+ return 'no modified CSS';
89
+ return lastOutputLine(output.combined, output.exitCode);
90
+ }
91
+ function generic(output) {
92
+ return lastOutputLine(output.combined, output.exitCode);
93
+ }
94
+ /** The status step's `--check` flags from `check.status`. */
95
+ export function statusPolicyArgs(policy) {
96
+ const args = [];
97
+ if (policy?.failOn !== undefined)
98
+ args.push('--fail-on', policy.failOn.join(','));
99
+ if (policy?.allow !== undefined)
100
+ args.push('--allow', policy.allow.join(','));
101
+ if (policy?.failOnUnknown === true)
102
+ args.push('--fail-on-unknown');
103
+ return args;
104
+ }
105
+ function builtinStep(invocation, projectRoot, name, args, summarize, extra = {}) {
106
+ return {
107
+ name,
108
+ kind: 'builtin',
109
+ command: invocation.command,
110
+ args: [...invocation.prefix, ...args],
111
+ display: `fireforge ${args.join(' ')}`,
112
+ cwd: projectRoot,
113
+ after: extra.after ?? [],
114
+ skipReason: extra.skipReason,
115
+ summarize,
116
+ };
117
+ }
118
+ /** Runs a project command line through the platform shell. */
119
+ function shellInvocation(commandLine) {
120
+ return process.platform === 'win32'
121
+ ? { command: 'cmd.exe', args: ['/d', '/s', '/c', commandLine] }
122
+ : { command: '/bin/sh', args: ['-c', commandLine] };
123
+ }
124
+ /**
125
+ * Builds the full plan: the six built-in steps (with the ones this project
126
+ * cannot run marked skipped) followed by `check.extraSteps[]`.
127
+ *
128
+ * `furnace-validate` waits for `typecheck`, the one built-in step that
129
+ * writes: it regenerates a stale Furnace jsconfig, whose drift
130
+ * `furnace validate` reports. Run side by side, the two would race on that
131
+ * file and the verdict would depend on which won.
132
+ */
133
+ export function buildCheckPlan(input) {
134
+ const { projectRoot, config } = input;
135
+ const invocation = fireforgeInvocation();
136
+ const check = config.check ?? {};
137
+ const patchArgs = input.patches !== undefined ? ['--patches', input.patches.join(',')] : [];
138
+ const lintWarnings = check.maxWarnings?.lint !== undefined ? ['--max-warnings', String(check.maxWarnings.lint)] : [];
139
+ const furnaceWarnings = check.maxWarnings?.furnace !== undefined
140
+ ? ['--max-warnings', String(check.maxWarnings.furnace)]
141
+ : [];
142
+ const plan = [
143
+ builtinStep(invocation, projectRoot, 'status', ['status', '--json', '--summary', '--check', ...statusPolicyArgs(check.status), ...patchArgs], summarizeStatus),
144
+ builtinStep(invocation, projectRoot, 'verify', ['verify', ...patchArgs], generic),
145
+ builtinStep(invocation, projectRoot, 'lint', ['lint', '--per-patch', '--notices', 'summary', ...lintWarnings, ...patchArgs], generic),
146
+ builtinStep(invocation, projectRoot, 'typecheck', ['typecheck'], generic, {
147
+ skipReason: config.typecheck === undefined ? 'no "typecheck" block in fireforge.json' : undefined,
148
+ }),
149
+ builtinStep(invocation, projectRoot, 'furnace-validate', ['furnace', 'validate', '--json', ...furnaceWarnings], summarizeFurnace, {
150
+ after: ['typecheck'],
151
+ skipReason: input.furnaceConfigured ? undefined : 'no furnace.json',
152
+ }),
153
+ builtinStep(invocation, projectRoot, 'token-coverage', ['token', 'coverage'], summarizeTokenCoverage),
154
+ ];
155
+ for (const step of check.extraSteps ?? []) {
156
+ const shell = shellInvocation(step.command);
157
+ plan.push({
158
+ name: step.name,
159
+ kind: 'extra',
160
+ command: shell.command,
161
+ args: shell.args,
162
+ display: step.command,
163
+ cwd: step.cwd !== undefined ? join(projectRoot, step.cwd) : projectRoot,
164
+ timeoutMs: step.timeout !== undefined ? step.timeout * 1000 : undefined,
165
+ after: [],
166
+ summarize: generic,
167
+ });
168
+ }
169
+ for (const name of check.skip ?? []) {
170
+ const step = plan.find((entry) => entry.name === name);
171
+ if (step !== undefined)
172
+ step.skipReason = 'skipped by check.skip in fireforge.json';
173
+ }
174
+ return plan;
175
+ }
@@ -0,0 +1,32 @@
1
+ import { Command } from 'commander';
2
+ import type { CommandContext } from '../types/cli.js';
3
+ import { type CheckStepPlan } from './check-steps.js';
4
+ /** Options Commander forwards from `fireforge check`. */
5
+ export interface CheckCommandOptions {
6
+ json?: boolean;
7
+ /** Judge only these patches in status, verify and lint. */
8
+ patches?: string[];
9
+ /** Comma-separated step names to leave out. */
10
+ skip?: string;
11
+ /** Comma-separated step names to run, leaving out the rest. */
12
+ only?: string;
13
+ /** Raw `--jobs` value. */
14
+ jobs?: string;
15
+ }
16
+ /**
17
+ * Applies `--only` and `--skip` to the plan. A step left out this way is
18
+ * dropped from the table rather than shown as skipped: the caller asked
19
+ * for a narrower gate, not a report of what it chose not to run.
20
+ */
21
+ export declare function selectCheckSteps(plan: CheckStepPlan[], options: Pick<CheckCommandOptions, 'only' | 'skip'>): CheckStepPlan[];
22
+ /** Parses `--jobs`: a positive integer. Defaults to one slot per CPU. */
23
+ export declare function parseCheckJobs(raw: string | undefined): number;
24
+ /**
25
+ * Runs the composed gate.
26
+ *
27
+ * @param projectRoot - Project root directory
28
+ * @param options - See {@link CheckCommandOptions}
29
+ */
30
+ export declare function checkCommand(projectRoot: string, options?: CheckCommandOptions): Promise<void>;
31
+ /** Registers the `check` command on the CLI program. */
32
+ export declare function registerCheck(program: Command, { getProjectRoot, withErrorHandling }: CommandContext): void;
@@ -0,0 +1,160 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * `fireforge check`: the composed static gate. Runs status, verify,
4
+ * per-patch lint, typecheck, Furnace validation and token coverage, plus
5
+ * the project's `check.extraSteps[]`, in parallel, and ends with one
6
+ * `FIREFORGE-VERDICT:` line over a per-step table (or one JSON document
7
+ * under `--json`).
8
+ *
9
+ * Each step is the real command in a child process (see check-steps.ts for
10
+ * why), so a step behaves exactly as it does when run on its own.
11
+ */
12
+ import { availableParallelism } from 'node:os';
13
+ import { Command } from 'commander';
14
+ import { getProjectPaths, loadConfig } from '../core/config.js';
15
+ import { furnaceConfigExists } from '../core/furnace-config.js';
16
+ import { loadPatchesManifest } from '../core/patch-manifest.js';
17
+ import { selectPatchSubset } from '../core/patch-subset.js';
18
+ import { GeneralError, InvalidArgumentError, LockContentionError } from '../errors/base.js';
19
+ import { ConfigError } from '../errors/config.js';
20
+ import { toError } from '../utils/errors.js';
21
+ import { info, intro, isMachineOutputMode, note, outro, setMachineOutputMode, } from '../utils/logger.js';
22
+ import { formatCheckTable, judgeCheck, writeCheckJson, writeCheckVerdictLine, } from './check-report.js';
23
+ import { runCheckSteps } from './check-run.js';
24
+ import { buildCheckPlan } from './check-steps.js';
25
+ import { resolveStatusCheckPolicy } from './status-check.js';
26
+ function parseNameList(raw) {
27
+ if (raw === undefined)
28
+ return undefined;
29
+ return raw
30
+ .split(',')
31
+ .map((name) => name.trim())
32
+ .filter((name) => name.length > 0);
33
+ }
34
+ /**
35
+ * Applies `--only` and `--skip` to the plan. A step left out this way is
36
+ * dropped from the table rather than shown as skipped: the caller asked
37
+ * for a narrower gate, not a report of what it chose not to run.
38
+ */
39
+ export function selectCheckSteps(plan, options) {
40
+ const known = plan.map((step) => step.name);
41
+ const only = parseNameList(options.only);
42
+ const skip = parseNameList(options.skip);
43
+ for (const [flag, names] of [
44
+ ['--only', only],
45
+ ['--skip', skip],
46
+ ]) {
47
+ const unknown = (names ?? []).filter((name) => !known.includes(name));
48
+ if (unknown.length > 0) {
49
+ throw new InvalidArgumentError(`Unknown ${flag} step "${unknown.join('", "')}". Steps: ${known.join(', ')}.`, flag);
50
+ }
51
+ }
52
+ return plan.filter((step) => (only === undefined || only.includes(step.name)) && skip?.includes(step.name) !== true);
53
+ }
54
+ /** Parses `--jobs`: a positive integer. Defaults to one slot per CPU. */
55
+ export function parseCheckJobs(raw) {
56
+ if (raw === undefined)
57
+ return availableParallelism();
58
+ const jobs = Number(raw);
59
+ if (!Number.isInteger(jobs) || jobs < 1) {
60
+ throw new InvalidArgumentError('--jobs must be a positive integer.', '--jobs');
61
+ }
62
+ return jobs;
63
+ }
64
+ /**
65
+ * Refuses a `check.status` policy the status step would refuse, before any
66
+ * step runs, as the config error it is.
67
+ */
68
+ function assertStatusPolicy(config) {
69
+ const policy = config.check?.status;
70
+ if (policy === undefined)
71
+ return;
72
+ try {
73
+ resolveStatusCheckPolicy({
74
+ check: true,
75
+ ...(policy.failOn !== undefined ? { failOn: policy.failOn.join(',') } : {}),
76
+ ...(policy.allow !== undefined ? { allow: policy.allow.join(',') } : {}),
77
+ ...(policy.failOnUnknown === true ? { failOnUnknown: true } : {}),
78
+ });
79
+ }
80
+ catch (error) {
81
+ throw new ConfigError(`Config field "check.status" is invalid: ${toError(error).message}`);
82
+ }
83
+ }
84
+ /** Resolves `--patches` once, so a typo fails here rather than in three steps. */
85
+ async function resolvePatchScope(patchesDir, requested) {
86
+ if (requested === undefined || requested.length === 0)
87
+ return undefined;
88
+ const manifest = await loadPatchesManifest(patchesDir);
89
+ if (!manifest || manifest.patches.length === 0) {
90
+ throw new GeneralError('--patches needs a patch queue, but patches.json lists no patches.');
91
+ }
92
+ return selectPatchSubset(manifest, requested, '--patches selects patches (filename, stem, order number, manifest name, or slug).').map((patch) => patch.filename);
93
+ }
94
+ /**
95
+ * Runs the composed gate.
96
+ *
97
+ * @param projectRoot - Project root directory
98
+ * @param options - See {@link CheckCommandOptions}
99
+ */
100
+ export async function checkCommand(projectRoot, options = {}) {
101
+ const previousMachineOutputMode = isMachineOutputMode();
102
+ // `--json` owns stdout from the first byte. On a throw the mode stays
103
+ // engaged and withErrorHandling resets it after routing the refusal.
104
+ if (options.json === true)
105
+ setMachineOutputMode(true);
106
+ await runCheck(projectRoot, options);
107
+ if (isMachineOutputMode() !== previousMachineOutputMode) {
108
+ setMachineOutputMode(previousMachineOutputMode);
109
+ }
110
+ }
111
+ async function runCheck(projectRoot, options) {
112
+ intro('FireForge Check');
113
+ const jobs = parseCheckJobs(options.jobs);
114
+ const config = await loadConfig(projectRoot);
115
+ assertStatusPolicy(config);
116
+ const patches = await resolvePatchScope(getProjectPaths(projectRoot).patches, options.patches);
117
+ const plan = selectCheckSteps(buildCheckPlan({
118
+ projectRoot,
119
+ config,
120
+ furnaceConfigured: await furnaceConfigExists(projectRoot),
121
+ patches,
122
+ }), options);
123
+ const runnable = plan.filter((step) => step.skipReason === undefined).length;
124
+ info(`Running ${runnable} step(s), up to ${Math.min(jobs, Math.max(1, runnable))} at once${patches ? `, judging ${patches.join(', ')}` : ''}.`);
125
+ const results = await runCheckSteps(plan, jobs);
126
+ const verdict = judgeCheck(results);
127
+ if (options.json === true) {
128
+ writeCheckJson(results, verdict);
129
+ }
130
+ else {
131
+ note(formatCheckTable(results), 'Steps');
132
+ for (const result of results) {
133
+ if (result.status === 'fail' && result.outputTail) {
134
+ note(result.outputTail, `${result.name}: ${result.command}`);
135
+ }
136
+ }
137
+ outro(verdict.passed ? 'Check passed' : `Check failed: ${verdict.failed.join(', ')}`);
138
+ writeCheckVerdictLine(verdict);
139
+ }
140
+ if (verdict.reason === 'lock-timeout') {
141
+ throw new LockContentionError(`fireforge check: ${verdict.failed.join(', ')} could not take their locks in time. Re-run when the holder is done.`);
142
+ }
143
+ if (!verdict.passed) {
144
+ throw new GeneralError(`fireforge check failed: ${verdict.failed.join(', ')}. Run a failing step on its own for its full output.`);
145
+ }
146
+ }
147
+ /** Registers the `check` command on the CLI program. */
148
+ export function registerCheck(program, { getProjectRoot, withErrorHandling }) {
149
+ program
150
+ .command('check')
151
+ .description('Composed static gate: status, verify, per-patch lint, typecheck, Furnace validation, token coverage and fireforge.json check.extraSteps in parallel, with one FIREFORGE-VERDICT line')
152
+ .option('--json', 'Write one JSON document (verdict plus a result per step) instead of the table')
153
+ .option('--patches <names...>', 'Judge only these patches in the status, verify and lint steps (filename, stem, order number, manifest name, or slug)')
154
+ .option('--only <steps>', 'Comma-separated steps to run, leaving out the rest')
155
+ .option('--skip <steps>', 'Comma-separated steps to leave out')
156
+ .option('--jobs <n>', 'Steps to run at once (default: one per CPU)')
157
+ .action(withErrorHandling(async (options) => {
158
+ await checkCommand(getProjectRoot(), options);
159
+ }));
160
+ }
@@ -35,6 +35,7 @@ const STRING_TYPED_KEYS = new Set([
35
35
  'firefox.candidate',
36
36
  'license',
37
37
  'wire.subscriptDir',
38
+ 'history.path',
38
39
  ]);
39
40
  /**
40
41
  * Parses a string value into the appropriate type.
@@ -131,12 +132,10 @@ export async function configCommand(projectRoot, key, value, options = {}) {
131
132
  // Readers stay lock-free (see `withConfigFileLock`).
132
133
  unchanged = await withConfigFileLock(projectRoot, async () => {
133
134
  // Short-circuit when the new value matches the current on-disk
134
- // value. Running every set through `mutateConfig` + `writeConfig`
135
- // round-trips through `JSON.stringify` and rewrites the file even
136
- // when nothing changed semantically, reordering top-level keys
137
- // (`license`, `markerComment`, …) on every harmless re-set. The
138
- // check uses the raw on-disk document so forced keys round-trip the
139
- // same as known keys.
135
+ // value, so a harmless re-set neither rewrites the file nor bumps
136
+ // its mtime. The check uses the raw on-disk document so forced keys
137
+ // round-trip the same as known keys. A real change is written by
138
+ // `writeConfigDocument`, which edits only the changed value in place.
140
139
  const rawConfig = await loadRawConfigDocument(projectRoot);
141
140
  const currentValue = getNestedValue(rawConfig, key);
142
141
  if (deepEqual(currentValue, parsedValue)) {
@@ -8,5 +8,10 @@ export const ENGINE_ESLINT_DOCTOR_CHECK = {
8
8
  skipIf: (ctx) => !ctx.engineExists,
9
9
  run: async (ctx) => (await pathExists(join(ctx.paths.engine, 'node_modules/eslint/bin/eslint.js')))
10
10
  ? ok('Engine ESLint installed')
11
- : warning('Engine ESLint installed', 'Engine-local ESLint is missing after source extraction; an engine-eslint ratchet cannot run.', 'Run "cd engine && ./mach eslint --setup" after each source refresh, before building or running the gate.'),
11
+ : warning('Engine ESLint installed', 'Engine-local ESLint is missing after source extraction; an engine-eslint ratchet cannot run.',
12
+ // `./mach npm ci` installs only the node dependencies the ratchet
13
+ // runs. `./mach eslint --setup` also builds mach's Python `lint`
14
+ // virtualenv, whose pinned requirements can stop resolving on PyPI
15
+ // and fail the whole step with node_modules still missing.
16
+ 'Run "cd engine && ./mach npm ci --no-audit --no-fund" after each source refresh, before building or running the gate. It installs engine/node_modules without the Python lint virtualenv "./mach eslint --setup" also builds.'),
12
17
  };
@@ -58,7 +58,10 @@ async function runSourcePinCheck(ctx) {
58
58
  }
59
59
  const downloaded = ctx.state.downloadedVersion;
60
60
  const mismatches = [];
61
- if (actual !== undefined && actual !== pinned) {
61
+ // Firefox's version.txt never carries the `esr` suffix (an ESR 153.4.0esr
62
+ // tree reads 153.4.0), so the engine comparison drops it from the pin. The
63
+ // last download recorded the full pin, so that comparison keeps it.
64
+ if (actual !== undefined && actual !== pinned && actual !== pinned.replace(/esr$/, '')) {
62
65
  mismatches.push(`engine baseline browser/config/version.txt reads ${actual}`);
63
66
  }
64
67
  if (downloaded !== undefined && downloaded !== pinned) {
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The advice an export placement refusal appends for new files: "register
3
+ * first" when a new system module only lacks its moz.build line, and
4
+ * adopt-then-split for a real moved-code block, but only after checking
5
+ * that the adopt itself would pass.
6
+ *
7
+ * The advice used to be offered on the moved-code heuristic alone. On one
8
+ * queue it told the operator to adopt six new modules into an existing
9
+ * patch when the only error was the missing registration; the adopt then
10
+ * refused one module for importing later patches, and without it breached
11
+ * the source patch's hard size limit. Split out of export-flow.ts for the
12
+ * line budget.
13
+ */
14
+ import { type PatchQueueContext } from '../core/patch-lint.js';
15
+ import type { PatchLintIssue } from '../types/commands/index.js';
16
+ /**
17
+ * Why adopting `diff` into `sourceFilename` would itself be refused: the
18
+ * forward imports the adopted files would give the source patch, and the
19
+ * source patch's hard size limit. Empty when the adopt is clean.
20
+ *
21
+ * @param baseCtx - The current queue
22
+ * @param sourceFilename - The patch the adopt would grow
23
+ * @param diff - The pending export's diff
24
+ */
25
+ export declare function findAdoptBlockers(baseCtx: PatchQueueContext, sourceFilename: string, diff: string): string[];
26
+ /**
27
+ * Detail lines for a placement refusal: register-first and moved-code
28
+ * advice, each only when the projection supports it.
29
+ *
30
+ * @param baseCtx - The current queue
31
+ * @param diff - The pending export's diff
32
+ * @param projectedIssues - Error findings of the placement projection
33
+ * @param plan - The placement's new filename and order
34
+ */
35
+ export declare function describePlacementAdvice(baseCtx: PatchQueueContext, diff: string, projectedIssues: readonly PatchLintIssue[], plan: {
36
+ newFilename: string;
37
+ insertionOrder: number;
38
+ }): string[];
@@ -0,0 +1,98 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * The advice an export placement refusal appends for new files: "register
4
+ * first" when a new system module only lacks its moz.build line, and
5
+ * adopt-then-split for a real moved-code block, but only after checking
6
+ * that the adopt itself would pass.
7
+ *
8
+ * The advice used to be offered on the moved-code heuristic alone. On one
9
+ * queue it told the operator to adopt six new modules into an existing
10
+ * patch when the only error was the missing registration; the adopt then
11
+ * refused one module for importing later patches, and without it breached
12
+ * the source patch's hard size limit. Split out of export-flow.ts for the
13
+ * line budget.
14
+ */
15
+ import { detectMovedCodeOverlaps, formatMovedCodeAdvice } from '../core/export-moved-code-shape.js';
16
+ import { countNonBinaryDiffLines, getPatchSizeThresholds, lintPatchQueue, resolvePatchSizeTier, } from '../core/patch-lint.js';
17
+ import { computeProjectedLintRegressions } from '../core/patch-lint-projection.js';
18
+ import { projectEntryBody } from './patch/entry-projection.js';
19
+ /**
20
+ * Why adopting `diff` into `sourceFilename` would itself be refused: the
21
+ * forward imports the adopted files would give the source patch, and the
22
+ * source patch's hard size limit. Empty when the adopt is clean.
23
+ *
24
+ * @param baseCtx - The current queue
25
+ * @param sourceFilename - The patch the adopt would grow
26
+ * @param diff - The pending export's diff
27
+ */
28
+ export function findAdoptBlockers(baseCtx, sourceFilename, diff) {
29
+ const source = baseCtx.entries.find((entry) => entry.filename === sourceFilename);
30
+ if (source === undefined)
31
+ return [];
32
+ const blockers = [];
33
+ const pending = projectEntryBody(diff);
34
+ const adopted = {
35
+ ...source,
36
+ diff: `${source.diff}\n${diff}`,
37
+ newFiles: new Map([...source.newFiles, ...pending.newFiles]),
38
+ createdFiles: new Set([...source.createdFiles, ...pending.createdFiles]),
39
+ modifiedFileAdditions: new Map([
40
+ ...source.modifiedFileAdditions,
41
+ ...pending.modifiedFileAdditions,
42
+ ]),
43
+ };
44
+ const isError = (issue) => issue.severity === 'error';
45
+ const baseline = lintPatchQueue(baseCtx).filter(isError);
46
+ const projected = lintPatchQueue({
47
+ ...baseCtx,
48
+ entries: baseCtx.entries.map((entry) => (entry.filename === sourceFilename ? adopted : entry)),
49
+ }).filter(isError);
50
+ for (const issue of computeProjectedLintRegressions(baseline, projected)) {
51
+ if (issue.check !== 'forward-import' || !issue.patches?.includes(sourceFilename))
52
+ continue;
53
+ for (const declaration of issue.stagedDeclarations ?? []) {
54
+ blockers.push(`${declaration.file} imports "${declaration.specifier ?? ''}", created by ` +
55
+ `${declaration.owner}, which applies after ${sourceFilename} (forward-import)`);
56
+ }
57
+ }
58
+ if (!(source.metadata?.lintIgnore ?? []).includes('large-patch-lines')) {
59
+ const tier = resolvePatchSizeTier([...(source.metadata?.filesAffected ?? []), ...pending.newFiles.keys()], source.metadata?.tier);
60
+ const limit = getPatchSizeThresholds(tier.tier).lines.error;
61
+ const lines = countNonBinaryDiffLines(source.diff).textLines + countNonBinaryDiffLines(diff).textLines;
62
+ if (lines > limit) {
63
+ blockers.push(`${sourceFilename} would grow to about ${lines} lines, over its ${tier.tier} hard limit ` +
64
+ `of ${limit} (large-patch-lines)`);
65
+ }
66
+ }
67
+ return blockers;
68
+ }
69
+ /**
70
+ * Detail lines for a placement refusal: register-first and moved-code
71
+ * advice, each only when the projection supports it.
72
+ *
73
+ * @param baseCtx - The current queue
74
+ * @param diff - The pending export's diff
75
+ * @param projectedIssues - Error findings of the placement projection
76
+ * @param plan - The placement's new filename and order
77
+ */
78
+ export function describePlacementAdvice(baseCtx, diff, projectedIssues, plan) {
79
+ const newFiles = projectEntryBody(diff).newFiles;
80
+ const exported = projectedIssues.filter((issue) => issue.patches?.includes(plan.newFilename) ?? issue.message.includes(plan.newFilename));
81
+ const unregisteredModules = [
82
+ ...new Set(exported
83
+ .filter((issue) => issue.check === 'unregistered-system-module' && newFiles.has(issue.file))
84
+ .map((issue) => issue.file)),
85
+ ];
86
+ const registrationResolvesAll = unregisteredModules.length > 0 &&
87
+ exported.every((issue) => issue.check === 'unregistered-system-module');
88
+ const overlaps = registrationResolvesAll ? [] : detectMovedCodeOverlaps(diff, baseCtx.entries);
89
+ const strongest = overlaps[0];
90
+ return formatMovedCodeAdvice({
91
+ overlaps,
92
+ newPatchName: plan.newFilename,
93
+ insertionOrder: plan.insertionOrder,
94
+ unregisteredModules,
95
+ registrationResolvesAll,
96
+ adoptBlockers: strongest !== undefined ? findAdoptBlockers(baseCtx, strongest.sourcePatch, diff) : [],
97
+ });
98
+ }
@@ -7,11 +7,11 @@
7
7
  * per-file / per-function line budgets and makes each step individually
8
8
  * testable without dragging the whole command harness along for the ride.
9
9
  */
10
- import { join } from 'node:path';
10
+ import { dirname, join } from 'node:path';
11
11
  import {} from '../core/destructive.js';
12
- import { detectMovedCodeOverlaps, formatAdoptThenSplitRemedy, } from '../core/export-moved-code-shape.js';
13
12
  import { buildPlanExportInput, findAllPatchesForFilesWithDetails, patchNameSlug, planExport, } from '../core/patch-export.js';
14
13
  import { buildPatchQueueContext, lintPatchQueue } from '../core/patch-lint.js';
14
+ import { formatLintIgnoreList } from '../core/patch-lint-ignore.js';
15
15
  import { withPatchDirectoryLock } from '../core/patch-lock.js';
16
16
  import { addPatchToManifest, loadPatchesManifest, loadPatchesManifestForWrite, renumberPatchesInManifest, resolvePatchIdentifier, savePatchesManifest, } from '../core/patch-manifest.js';
17
17
  import { formatPatchOrder, requirePatchOrder } from '../core/patch-parse.js';
@@ -20,9 +20,11 @@ import { GeneralError, InvalidArgumentError } from '../errors/base.js';
20
20
  import { toError } from '../utils/errors.js';
21
21
  import { pathExists, readText, removeFile, writeText } from '../utils/fs.js';
22
22
  import { info, warn } from '../utils/logger.js';
23
+ import { describePlacementAdvice } from './export-adopt-check.js';
23
24
  import { groupProjectedPlacementErrors } from './export-placement-conflicts.js';
24
25
  import { assertPlacementAvoidsReservedRanges } from './export-placement-policy.js';
25
26
  import { findPartialOwnershipOverlap } from './export-shared.js';
27
+ import { acceptTreeRegisteredModules } from './export-staged-registration.js';
26
28
  import { projectEntryBody } from './patch/entry-projection.js';
27
29
  import { getSortedRenameEntries, renameMapsEqual } from './patch-rename-map.js';
28
30
  function buildFilenameForPlacement(category, name, order, width) {
@@ -170,18 +172,31 @@ export async function projectPlacementForLint(patchesDir, plan, diff, config) {
170
172
  // whether they live in a brand-new file or are added to an existing one.
171
173
  // Leaving either map empty lets a patch land before one of its own
172
174
  // dependencies and still pass the gate.
175
+ const pendingBody = projectEntryBody(diff);
173
176
  projectedEntries.push({
174
177
  filename: plan.newFilename,
175
178
  order: plan.insertionOrder,
176
179
  metadata: null,
177
- ...projectEntryBody(diff),
180
+ ...pendingBody,
178
181
  });
179
182
  projectedEntries.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
180
183
  const policyRest = baseCtx.patchPolicy ? { patchPolicy: baseCtx.patchPolicy } : {};
181
- const projectedIssues = lintPatchQueue({
182
- entries: projectedEntries,
183
- ...policyRest,
184
- }).filter((i) => i.severity === 'error');
184
+ // A new module whose moz.build line is declared (staged registration)
185
+ // and present in the working tree, but not yet exported by its owner, is
186
+ // accepted with a "re-export the owner next" note instead of refused.
187
+ const registration = await acceptTreeRegisteredModules({
188
+ engineDir: join(dirname(patchesDir), 'engine'),
189
+ baseCtx,
190
+ newFilename: plan.newFilename,
191
+ pendingNewFiles: new Set(pendingBody.newFiles.keys()),
192
+ projectedIssues: lintPatchQueue({
193
+ entries: projectedEntries,
194
+ ...policyRest,
195
+ }).filter((i) => i.severity === 'error'),
196
+ });
197
+ for (const note of registration.accepted)
198
+ warn(note);
199
+ const projectedIssues = registration.remaining;
185
200
  if (projectedIssues.length === 0)
186
201
  return null;
187
202
  // Baseline lint over the un-renumbered queue: an error present here is
@@ -192,16 +207,16 @@ export async function projectPlacementForLint(patchesDir, plan, diff, config) {
192
207
  entries: baseCtx.entries,
193
208
  ...policyRest,
194
209
  }).filter((i) => i.severity === 'error');
195
- // New-file + moved-code slice: when the refusal is caused by
196
- // the source patch still carrying lines the new patch would gain, name
197
- // the adopt-then-split sequence instead of leaving the operator to
198
- // derive it from two individually-correct guards that together dead-end.
199
- const movedCode = detectMovedCodeOverlaps(diff, baseCtx.entries);
210
+ // Register-first and new-file + moved-code advice: name the working
211
+ // sequence instead of leaving the operator to derive it from two
212
+ // individually-correct guards that together dead-end, but only offer
213
+ // adopt-then-split for a real moved block whose adopt would itself pass.
200
214
  return {
201
215
  reason: `placement would introduce ${projectedIssues.length} cross-patch lint error(s)`,
202
216
  details: [
203
217
  ...groupProjectedPlacementErrors(projectedIssues, baselineIssues, plan),
204
- ...formatAdoptThenSplitRemedy(movedCode, plan.newFilename, plan.insertionOrder),
218
+ ...registration.advice,
219
+ ...describePlacementAdvice(baseCtx, diff, projectedIssues, plan),
205
220
  ],
206
221
  };
207
222
  }
@@ -380,7 +395,7 @@ export async function renderDryRunPreview(input) {
380
395
  info(` tier: ${plan.metadata.tier}`);
381
396
  }
382
397
  if (plan.metadata.lintIgnore !== undefined && plan.metadata.lintIgnore.length > 0) {
383
- info(` lintIgnore: ${plan.metadata.lintIgnore.join(', ')}`);
398
+ info(` lintIgnore: ${formatLintIgnoreList(plan.metadata.lintIgnore)}`);
384
399
  }
385
400
  if (supersedeDetails.length > 0) {
386
401
  info(`\n[dry-run] Would supersede ${supersedeDetails.length} existing patch(es):`);