@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
@@ -25,12 +25,14 @@ import { findUnresolvedSystemModuleImports, formatUnresolvedSystemModuleImports,
25
25
  import { buildPatchQueueContext, formatPatchLintIssue, lintPatchQueue, } from '../core/patch-lint.js';
26
26
  import { loadPatchesManifest, validatePatchesManifestConsistency } from '../core/patch-manifest.js';
27
27
  import { evaluatePatchPolicy } from '../core/patch-policy.js';
28
+ import { evaluatePatchPolicyContentForQueue } from '../core/patch-policy-content.js';
28
29
  import { collectPatchRegistrationReferences } from '../core/patch-registration-refs.js';
29
30
  import { buildPatchClaims } from '../core/status-classify.js';
30
31
  import { classifyFiles } from '../core/status-classify.js';
31
32
  import { GeneralError } from '../errors/base.js';
32
33
  import { pathExists, readText } from '../utils/fs.js';
33
34
  import { info, intro, outro, success, warn } from '../utils/logger.js';
35
+ import { anyPatchInScope, isFilenameInScope, resolveVerifyScope, scopeLintIssues, scopePatches, } from './verify-scope.js';
34
36
  /**
35
37
  * Walks each patch body in the manifest, extracts the set of
36
38
  * component-shaped registration references it adds (widget paths
@@ -41,7 +43,7 @@ import { info, intro, outro, success, warn } from '../utils/logger.js';
41
43
  * registers a file that nothing in the world supplies, which fails at
42
44
  * install time.
43
45
  */
44
- async function detectDanglingRegistrations(patchesDir, engineDir, patches) {
46
+ async function detectDanglingRegistrations(patchesDir, engineDir, patches, scope) {
45
47
  // Aggregate the set of all paths any patch in the queue is responsible for
46
48
  // (per `filesAffected`). Individual patch bodies are deliberately not
47
49
  // parsed for new-file creations here: `filesAffected` is already the
@@ -54,8 +56,9 @@ async function detectDanglingRegistrations(patchesDir, engineDir, patches) {
54
56
  coveredByPatches.add(file);
55
57
  }
56
58
  }
59
+ // Every patch supplies targets; only the scoped ones are judged.
57
60
  const issues = [];
58
- for (const patch of patches) {
61
+ for (const patch of scopePatches(patches, scope)) {
59
62
  const patchPath = join(patchesDir, patch.filename);
60
63
  if (!(await pathExists(patchPath)))
61
64
  continue;
@@ -100,7 +103,7 @@ function detectCrossPatchFileClaims(manifestPatches) {
100
103
  }
101
104
  return results;
102
105
  }
103
- async function detectWorktreeOwnershipDrift(projectRoot, engineDir, patchesDir, binaryName) {
106
+ async function detectWorktreeOwnershipDrift(projectRoot, engineDir, patchesDir, binaryName, scope) {
104
107
  if (!(await pathExists(engineDir)) || !(await isGitRepository(engineDir))) {
105
108
  return { unowned: [], patchOwnedDrift: [] };
106
109
  }
@@ -108,24 +111,51 @@ async function detectWorktreeOwnershipDrift(projectRoot, engineDir, patchesDir,
108
111
  const furnacePrefixes = await collectFurnaceManagedPrefixes(projectRoot);
109
112
  const classified = await classifyFiles(entries, engineDir, patchesDir, binaryName, furnacePrefixes);
110
113
  return {
114
+ // An unowned change belongs to no patch, so no --patches scope covers it.
111
115
  unowned: [
112
116
  ...new Set(classified
113
- .filter((entry) => entry.classification === 'unmanaged')
117
+ .filter((entry) => scope === undefined && entry.classification === 'unmanaged')
114
118
  .map((entry) => entry.file)),
115
119
  ].sort(),
116
120
  patchOwnedDrift: [
117
121
  ...new Set(classified
118
- .filter((entry) => entry.classification === 'patch-owned-drift')
122
+ .filter((entry) => entry.classification === 'patch-owned-drift' &&
123
+ anyPatchInScope(scope, entry.owner === undefined ? [] : [entry.owner]))
119
124
  .map((entry) => entry.file)),
120
125
  ].sort(),
121
126
  };
122
127
  }
128
+ /** The warning groups for unowned changes and patch-owned drift. */
129
+ function buildWorktreeDriftGroups(drift) {
130
+ const groups = [];
131
+ if (drift.unowned.length > 0) {
132
+ groups.push({
133
+ title: `Unowned worktree changes (${drift.unowned.length})`,
134
+ issues: drift.unowned.map((file) => `${file} is changed in engine/ but is not listed in any patch filesAffected entry`),
135
+ errorCount: 0,
136
+ warningCount: drift.unowned.length,
137
+ });
138
+ }
139
+ if (drift.patchOwnedDrift.length > 0) {
140
+ groups.push({
141
+ title: `Patch-owned worktree drift (${drift.patchOwnedDrift.length})`,
142
+ issues: drift.patchOwnedDrift.map((file) => `${file} is claimed by exactly one patch, but engine/ no longer matches that patch output`),
143
+ errorCount: 0,
144
+ warningCount: drift.patchOwnedDrift.length,
145
+ });
146
+ }
147
+ return groups;
148
+ }
123
149
  /**
124
150
  * Collects the same queue-health findings reported by `fireforge verify`
125
151
  * without printing. Used by doctor recovery paths that need a read-only
126
152
  * "is this queue healthy?" decision before clearing stale state.
153
+ *
154
+ * @param projectRoot - Project root directory
155
+ * @param scope - `verify --patches`: keep only the findings that name these
156
+ * patches (see verify-scope.ts). Omitted, every finding counts.
127
157
  */
128
- export async function collectPatchQueueHealth(projectRoot) {
158
+ export async function collectPatchQueueHealth(projectRoot, scope) {
129
159
  const paths = getProjectPaths(projectRoot);
130
160
  const config = await loadConfig(projectRoot);
131
161
  if (!(await pathExists(paths.patches))) {
@@ -139,7 +169,7 @@ export async function collectPatchQueueHealth(projectRoot) {
139
169
  const groups = [];
140
170
  let errorCount = 0;
141
171
  let warningCount = 0;
142
- const consistencyIssues = await validatePatchesManifestConsistency(paths.patches);
172
+ const consistencyIssues = (await validatePatchesManifestConsistency(paths.patches)).filter((issue) => isFilenameInScope(scope, issue.filename));
143
173
  if (consistencyIssues.length > 0) {
144
174
  const issues = consistencyIssues.map((issue) => `[${issue.code}] ${issue.message}`);
145
175
  groups.push({
@@ -152,7 +182,10 @@ export async function collectPatchQueueHealth(projectRoot) {
152
182
  }
153
183
  const manifest = await loadPatchesManifest(paths.patches);
154
184
  if (manifest) {
155
- const policyIssues = evaluatePatchPolicy(config, manifest);
185
+ const policyIssues = [
186
+ ...evaluatePatchPolicy(config, manifest),
187
+ ...(await evaluatePatchPolicyContentForQueue(config, projectRoot, paths.patches, manifest)),
188
+ ].filter((issue) => isFilenameInScope(scope, issue.filename));
156
189
  if (policyIssues.length > 0) {
157
190
  const policyErrors = policyIssues.filter((issue) => issue.severity === 'error').length;
158
191
  const policyWarnings = policyIssues.length - policyErrors;
@@ -174,7 +207,7 @@ export async function collectPatchQueueHealth(projectRoot) {
174
207
  // to an existing module is named as text instead of surfacing later as a
175
208
  // bare `xpcshell return code: -11` with zero output.
176
209
  if (await pathExists(paths.engine)) {
177
- const unresolved = await findUnresolvedSystemModuleImports(paths.engine, manifest.patches.flatMap((patch) => patch.filesAffected));
210
+ const unresolved = await findUnresolvedSystemModuleImports(paths.engine, scopePatches(manifest.patches, scope).flatMap((patch) => patch.filesAffected));
178
211
  if (unresolved.length > 0) {
179
212
  groups.push({
180
213
  title: `Unresolvable system-module imports (${unresolved.length})`,
@@ -185,7 +218,7 @@ export async function collectPatchQueueHealth(projectRoot) {
185
218
  errorCount += unresolved.length;
186
219
  }
187
220
  }
188
- const crossClaims = detectCrossPatchFileClaims(manifest.patches);
221
+ const crossClaims = detectCrossPatchFileClaims(manifest.patches).filter((claim) => anyPatchInScope(scope, claim.filenames));
189
222
  if (crossClaims.length > 0) {
190
223
  groups.push({
191
224
  title: `Cross-patch filesAffected conflicts (${crossClaims.length})`,
@@ -197,7 +230,7 @@ export async function collectPatchQueueHealth(projectRoot) {
197
230
  }
198
231
  }
199
232
  const ctx = await buildPatchQueueContext(paths.patches);
200
- const lintIssues = lintPatchQueue(ctx);
233
+ const lintIssues = scopeLintIssues(lintPatchQueue(ctx), scope);
201
234
  if (lintIssues.length > 0) {
202
235
  const lintErrors = lintIssues.filter((issue) => issue.severity === 'error').length;
203
236
  const lintWarnings = lintIssues.filter((issue) => issue.severity === 'warning').length;
@@ -214,26 +247,11 @@ export async function collectPatchQueueHealth(projectRoot) {
214
247
  warningCount += lintWarnings;
215
248
  }
216
249
  if (manifest) {
217
- const worktreeDrift = await detectWorktreeOwnershipDrift(projectRoot, paths.engine, paths.patches, config.binaryName);
218
- if (worktreeDrift.unowned.length > 0) {
219
- groups.push({
220
- title: `Unowned worktree changes (${worktreeDrift.unowned.length})`,
221
- issues: worktreeDrift.unowned.map((file) => `${file} is changed in engine/ but is not listed in any patch filesAffected entry`),
222
- errorCount: 0,
223
- warningCount: worktreeDrift.unowned.length,
224
- });
225
- warningCount += worktreeDrift.unowned.length;
226
- }
227
- if (worktreeDrift.patchOwnedDrift.length > 0) {
228
- groups.push({
229
- title: `Patch-owned worktree drift (${worktreeDrift.patchOwnedDrift.length})`,
230
- issues: worktreeDrift.patchOwnedDrift.map((file) => `${file} is claimed by exactly one patch, but engine/ no longer matches that patch output`),
231
- errorCount: 0,
232
- warningCount: worktreeDrift.patchOwnedDrift.length,
233
- });
234
- warningCount += worktreeDrift.patchOwnedDrift.length;
235
- }
236
- const registrationIssues = await detectDanglingRegistrations(paths.patches, paths.engine, manifest.patches);
250
+ const worktreeDrift = await detectWorktreeOwnershipDrift(projectRoot, paths.engine, paths.patches, config.binaryName, scope);
251
+ const driftGroups = buildWorktreeDriftGroups(worktreeDrift);
252
+ groups.push(...driftGroups);
253
+ warningCount += driftGroups.reduce((sum, group) => sum + group.warningCount, 0);
254
+ const registrationIssues = await detectDanglingRegistrations(paths.patches, paths.engine, manifest.patches, scope);
237
255
  if (registrationIssues.length > 0) {
238
256
  groups.push({
239
257
  title: `Dangling registration references (${registrationIssues.length})`,
@@ -256,15 +274,20 @@ export async function collectPatchQueueHealth(projectRoot) {
256
274
  * Read-only. Exits non-zero on any error-severity finding.
257
275
  *
258
276
  * @param projectRoot - Project root directory
277
+ * @param options - `patches`: the raw `--patches` values
259
278
  */
260
- export async function verifyCommand(projectRoot) {
279
+ export async function verifyCommand(projectRoot, options = {}) {
261
280
  intro('FireForge Verify');
281
+ const scope = await resolveVerifyScope(getProjectPaths(projectRoot).patches, options.patches);
282
+ if (scope !== undefined) {
283
+ info(`Judging only: ${[...scope.patches].join(', ')}`);
284
+ }
262
285
  // `verify` is read-only to the OPERATOR but not to git: the working-tree
263
286
  // status probe refreshes (and rewrites) `engine/.git/index`, making this
264
287
  // command a second writer of the primary checkout and invalidating a
265
288
  // concurrent `fireforge test`'s engine fingerprint. A private index keeps
266
289
  // every refresh off the primary checkout.
267
- const health = await withPrivateGitIndex(getProjectPaths(projectRoot).engine, () => collectPatchQueueHealth(projectRoot));
290
+ const health = await withPrivateGitIndex(getProjectPaths(projectRoot).engine, () => collectPatchQueueHealth(projectRoot, scope));
268
291
  if (!health.hasPatchesDirectory) {
269
292
  info('No patches directory. Nothing to verify.');
270
293
  outro('Verify clean');
@@ -298,7 +321,8 @@ export function registerVerify(program, { getProjectRoot, withErrorHandling }) {
298
321
  program
299
322
  .command('verify')
300
323
  .description('Read-only fsck for the patch queue (manifest + cross-patch lint)')
301
- .action(withErrorHandling(async () => {
302
- await verifyCommand(getProjectRoot());
324
+ .option('--patches <names...>', 'Judge only findings that name these patches (filename, stem, order number, manifest name, or slug; repeat or comma-separate). Queue-wide findings stay; unowned worktree changes drop out')
325
+ .action(withErrorHandling(async (options) => {
326
+ await verifyCommand(getProjectRoot(), options);
303
327
  }));
304
328
  }
@@ -95,8 +95,28 @@ export interface BuildBaseline {
95
95
  * replaces coverage with a dropped-path notice, or refuses when the caller
96
96
  * required --extend-coverage. Legacy packageable-only anchors cannot retain
97
97
  * coverage because they cannot prove test scripts and fixtures unchanged.
98
+ *
99
+ * Since 0.50.0 retention is decided per staged path: see
100
+ * {@link BuildBaseline.testCoverageRecords} and
101
+ * {@link BuildBaseline.fullCoverageAnchor}.
98
102
  */
99
103
  testPackagingCoverage?: TestPackagingCoverage;
104
+ /**
105
+ * Per-path anchors of a scoped claim, keyed by the request path in
106
+ * {@link BuildBaseline.testPackagingCoverage}. Each record holds that
107
+ * path's own staging inputs and their content at the build that staged
108
+ * it, so a later scoped build keeps the path while those inputs are
109
+ * unchanged, whatever moved elsewhere in the engine. A listed path without
110
+ * a record falls back to the baseline-wide anchor. Absent on full claims.
111
+ */
112
+ testCoverageRecords?: Record<string, CoverageRecord>;
113
+ /**
114
+ * The anchor of the last full-coverage build, carried forward by scoped
115
+ * builds once something outside their scope moved. A request path outside
116
+ * the scoped list stays covered while its own staging inputs are unchanged
117
+ * against this anchor. The check runs at dispatch, per requested path.
118
+ */
119
+ fullCoverageAnchor?: CoverageAnchor;
100
120
  /**
101
121
  * Hex-encoded SHA-256 of `engine/mozconfig` as it stood for this build.
102
122
  * Recorded because the mozconfig is regenerated from project-side
@@ -141,5 +161,31 @@ export interface StaticComponentsBaseline {
141
161
  */
142
162
  fingerprints: Record<string, string>;
143
163
  }
164
+ /**
165
+ * Content anchor of one packaging claim. A path's staged content is
166
+ * `fingerprints[path]` when recorded, otherwise its content at
167
+ * `engineHeadSha`.
168
+ */
169
+ export interface CoverageAnchor {
170
+ /** Engine HEAD the staged content is relative to. */
171
+ engineHeadSha: string;
172
+ /** Hex SHA-256 of `engine/mozconfig` the claim was configured with. */
173
+ mozconfigHash: string;
174
+ /**
175
+ * Hex SHA-256 (or {@link DELETED_FILE_FINGERPRINT}) per engine-relative
176
+ * path whose staged content differs from `engineHeadSha`.
177
+ */
178
+ fingerprints: Record<string, string>;
179
+ }
180
+ /** Anchor of one staged request path. See {@link BuildBaseline.testCoverageRecords}. */
181
+ export interface CoverageRecord extends CoverageAnchor {
182
+ /**
183
+ * Engine-relative paths (files or directories) whose content decides what
184
+ * the harness stages for the request path: its manifest directory plus
185
+ * whatever a manifest there names outside it. `fingerprints` is
186
+ * restricted to these.
187
+ */
188
+ inputs: string[];
189
+ }
144
190
  /** Coverage claim of the packaged test runtime. See {@link BuildBaseline.testPackagingCoverage}. */
145
191
  export type TestPackagingCoverage = 'full' | string[];
@@ -18,6 +18,20 @@ function isFingerprintMap(value) {
18
18
  return (isObject(value) &&
19
19
  Object.entries(value).every(([path, fingerprint]) => isBaselinePath(path) && isFingerprint(fingerprint)));
20
20
  }
21
+ function isCoverageAnchor(value) {
22
+ return (isObject(value) &&
23
+ typeof value['engineHeadSha'] === 'string' &&
24
+ isFingerprint(value['mozconfigHash']) &&
25
+ isFingerprintMap(value['fingerprints']));
26
+ }
27
+ function isCoverageRecordMap(value) {
28
+ return (isObject(value) &&
29
+ Object.entries(value).every(([path, record]) => isBaselinePath(path) &&
30
+ isCoverageAnchor(record) &&
31
+ isObject(record) &&
32
+ Array.isArray(record['inputs']) &&
33
+ record['inputs'].every(isBaselinePath)));
34
+ }
21
35
  /** Validates persisted fields without converting malformed coverage into a full claim. */
22
36
  export function isBuildBaseline(value) {
23
37
  if (!isObject(value))
@@ -45,6 +59,11 @@ export function isBuildBaseline(value) {
45
59
  return false;
46
60
  if (value['recordedBy'] !== undefined && typeof value['recordedBy'] !== 'string')
47
61
  return false;
62
+ if (value['testCoverageRecords'] !== undefined &&
63
+ !isCoverageRecordMap(value['testCoverageRecords']))
64
+ return false;
65
+ if (value['fullCoverageAnchor'] !== undefined && !isCoverageAnchor(value['fullCoverageAnchor']))
66
+ return false;
48
67
  const components = value['staticComponentsBaseline'];
49
68
  return (components === undefined ||
50
69
  (isObject(components) &&
@@ -15,7 +15,7 @@
15
15
  * on successful build completion. A failed build does not update it, so a
16
16
  * subsequent run still audits against the last known-good tree.
17
17
  */
18
- import { type BuildBaseline, type TestPackagingCoverage } from './build-baseline-types.js';
18
+ import { type BuildBaseline, type CoverageAnchor, type CoverageRecord, type TestPackagingCoverage } from './build-baseline-types.js';
19
19
  /**
20
20
  * Which mach build produced the baseline being written. Decides whether
21
21
  * the `jar.mn` half of {@link BuildBaseline.buildInputFingerprints} is
@@ -52,6 +52,10 @@ export interface WriteBuildBaselineOptions {
52
52
  * Omitted → field left off the marker.
53
53
  */
54
54
  testPackagingCoverage?: TestPackagingCoverage | undefined;
55
+ /** Per-path anchors of a scoped claim. Omitted → field left off the marker. */
56
+ testCoverageRecords?: Record<string, CoverageRecord> | undefined;
57
+ /** Carried full-build anchor of a narrowed claim. Omitted → field left off. */
58
+ fullCoverageAnchor?: CoverageAnchor | undefined;
55
59
  /**
56
60
  * The baseline this write replaces, when the caller has it. A scoped
57
61
  * (non-`'full'`) write carries its `staticComponentsBaseline` forward
@@ -81,7 +81,7 @@ export async function readBuildBaseline(projectRoot, invalid = 'ignore') {
81
81
  * @param options - See {@link WriteBuildBaselineOptions}
82
82
  */
83
83
  export async function writeBuildBaseline(options) {
84
- const { projectRoot, engineDir, binaryName, testPackagingCoverage, previousBaseline, recordedBy, staticComponentsHandling = 'auto', buildKind = 'full', } = options;
84
+ const { projectRoot, engineDir, binaryName, testPackagingCoverage, testCoverageRecords, fullCoverageAnchor, previousBaseline, recordedBy, staticComponentsHandling = 'auto', buildKind = 'full', } = options;
85
85
  let engineHeadSha = '';
86
86
  try {
87
87
  engineHeadSha = await getHead(engineDir);
@@ -116,6 +116,8 @@ export async function writeBuildBaseline(options) {
116
116
  ...(testInputFingerprints !== undefined ? { testInputFingerprints } : {}),
117
117
  ...(buildInputFingerprints !== undefined ? { buildInputFingerprints } : {}),
118
118
  ...(testPackagingCoverage !== undefined ? { testPackagingCoverage } : {}),
119
+ ...(testCoverageRecords !== undefined ? { testCoverageRecords } : {}),
120
+ ...(fullCoverageAnchor !== undefined ? { fullCoverageAnchor } : {}),
119
121
  ...(mozconfigHash !== undefined ? { mozconfigHash } : {}),
120
122
  ...(staticComponentsBaseline !== undefined ? { staticComponentsBaseline } : {}),
121
123
  ...(recordedBy !== undefined ? { recordedBy } : {}),
@@ -14,8 +14,17 @@
14
14
  * whose content is explained by neither a patch body nor the pristine
15
15
  * baseline. Advisory by default (a warning naming every file), refusing
16
16
  * under an explicit flag.
17
+ *
18
+ * A deployed Furnace file is checked against its component source as well
19
+ * (`deployed-unexported`): when the engine copy equals the Furnace-composed
20
+ * current source, differs from it only inside generated fragment
21
+ * expansions, or is exactly Furnace's last recorded deployment, the
22
+ * overwrite destroys nothing that is not a component source, so it is safe
23
+ * even though no patch records it yet. Without this, peers' deployed but
24
+ * unexported widgets refused every belted build on a shared checkout.
17
25
  */
18
26
  import type { FireForgeConfig } from '../types/config.js';
27
+ import { type DeployedSourceMatch } from './furnace-composed-source.js';
19
28
  import { type ClassifiedFile } from './status-classify.js';
20
29
  /** One file whose unexported content a build-prepare overwrite would destroy. */
21
30
  export interface UnexportedDriftAtRisk {
@@ -23,6 +32,24 @@ export interface UnexportedDriftAtRisk {
23
32
  classification: ClassifiedFile['classification'];
24
33
  owner?: string | undefined;
25
34
  }
35
+ /**
36
+ * A deployed Furnace file no patch records yet whose content is its
37
+ * component source's (see {@link DeployedSourceMatch}), so the overwrite is
38
+ * safe. Its patch still owes a re-export.
39
+ */
40
+ export interface DeployedUnexportedFile {
41
+ file: string;
42
+ classification: 'deployed-unexported';
43
+ match: DeployedSourceMatch;
44
+ owner?: string | undefined;
45
+ }
46
+ /** Outcome of {@link assessUnexportedDrift}. */
47
+ export interface UnexportedDriftAssessment {
48
+ /** Files whose content the overwrite would destroy unrecorded. */
49
+ atRisk: UnexportedDriftAtRisk[];
50
+ /** Files the overwrite rewrites from their own component source. */
51
+ deployedUnexported: DeployedUnexportedFile[];
52
+ }
26
53
  /**
27
54
  * Finds engine files that build-prepare is about to overwrite and whose
28
55
  * current content is recorded nowhere.
@@ -30,11 +57,34 @@ export interface UnexportedDriftAtRisk {
30
57
  * Fail-open: any probe failure is reported verbosely and yields an empty
31
58
  * list. A build must not fail because an advisory guard could not run.
32
59
  *
60
+ * @internal The at-risk half of {@link assessUnexportedDrift}, kept for the
61
+ * guard's tests; build-prepare consumes the full assessment.
62
+ *
33
63
  * @param projectRoot - Project root
34
64
  * @param config - Loaded FireForge config (reads `binaryName`)
35
65
  * @returns At-risk files, sorted by path
36
66
  */
37
67
  export declare function findUnexportedDriftAtRisk(projectRoot: string, config: FireForgeConfig): Promise<UnexportedDriftAtRisk[]>;
68
+ /**
69
+ * Classifies the engine files build-prepare is about to overwrite into the
70
+ * ones whose content is recorded nowhere (`atRisk`) and the deployed
71
+ * Furnace files no patch records yet whose content is their component
72
+ * source's (`deployedUnexported`, safe to overwrite).
73
+ *
74
+ * Fail-open like {@link findUnexportedDriftAtRisk}: a probe failure yields
75
+ * an empty assessment.
76
+ *
77
+ * @param projectRoot - Project root
78
+ * @param config - Loaded FireForge config (reads `binaryName`)
79
+ * @returns Both lists, sorted by path
80
+ */
81
+ export declare function assessUnexportedDrift(projectRoot: string, config: FireForgeConfig): Promise<UnexportedDriftAssessment>;
82
+ /**
83
+ * Renders the informational note for deployed Furnace files the overwrite
84
+ * rewrites from their own component source: safe for this build, but their
85
+ * patches still owe a re-export.
86
+ */
87
+ export declare function formatDeployedUnexportedNote(files: readonly DeployedUnexportedFile[]): string;
38
88
  /**
39
89
  * Renders the operator-facing warning (or refusal body) naming every file
40
90
  * whose unexported content the build is about to overwrite.
@@ -15,10 +15,19 @@
15
15
  * whose content is explained by neither a patch body nor the pristine
16
16
  * baseline. Advisory by default (a warning naming every file), refusing
17
17
  * under an explicit flag.
18
+ *
19
+ * A deployed Furnace file is checked against its component source as well
20
+ * (`deployed-unexported`): when the engine copy equals the Furnace-composed
21
+ * current source, differs from it only inside generated fragment
22
+ * expansions, or is exactly Furnace's last recorded deployment, the
23
+ * overwrite destroys nothing that is not a component source, so it is safe
24
+ * even though no patch records it yet. Without this, peers' deployed but
25
+ * unexported widgets refused every belted build on a shared checkout.
18
26
  */
19
27
  import { toError } from '../utils/errors.js';
20
28
  import { verbose } from '../utils/logger.js';
21
29
  import { getProjectPaths } from './config.js';
30
+ import { matchDeployedComponentFiles, } from './furnace-composed-source.js';
22
31
  import { collectFurnaceManagedPrefixes } from './furnace-config.js';
23
32
  import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from './git-status.js';
24
33
  import { classifyFiles } from './status-classify.js';
@@ -98,6 +107,40 @@ async function collectOverwrittenEntries(engineDir, status, binaryName, furnaceP
98
107
  return expanded.filter((entry) => !entry.file.endsWith('/') &&
99
108
  isOverwrittenByBuildPrepare(entry.file, binaryName, furnacePrefixes));
100
109
  }
110
+ /**
111
+ * Splits classified at-risk files into the ones whose engine content is a
112
+ * Furnace deployment of their component source and the rest.
113
+ *
114
+ * Fail-closed: when the source comparison cannot run, every file stays at
115
+ * risk, exactly as before the comparison existed.
116
+ */
117
+ async function separateDeployedUnexported(projectRoot, engineDir, atRisk) {
118
+ let matches;
119
+ try {
120
+ matches = await matchDeployedComponentFiles(projectRoot, engineDir, atRisk.map((entry) => entry.file));
121
+ }
122
+ catch (error) {
123
+ verbose(`Unexported-drift guard could not compare component sources: ${toError(error).message}`);
124
+ return { atRisk, deployedUnexported: [] };
125
+ }
126
+ const deployedUnexported = [];
127
+ const remaining = [];
128
+ for (const entry of atRisk) {
129
+ const match = matches.get(entry.file);
130
+ if (match === undefined) {
131
+ remaining.push(entry);
132
+ }
133
+ else {
134
+ deployedUnexported.push({
135
+ file: entry.file,
136
+ classification: 'deployed-unexported',
137
+ match,
138
+ owner: entry.owner,
139
+ });
140
+ }
141
+ }
142
+ return { atRisk: remaining, deployedUnexported };
143
+ }
101
144
  /**
102
145
  * Finds engine files that build-prepare is about to overwrite and whose
103
146
  * current content is recorded nowhere.
@@ -105,22 +148,42 @@ async function collectOverwrittenEntries(engineDir, status, binaryName, furnaceP
105
148
  * Fail-open: any probe failure is reported verbosely and yields an empty
106
149
  * list. A build must not fail because an advisory guard could not run.
107
150
  *
151
+ * @internal The at-risk half of {@link assessUnexportedDrift}, kept for the
152
+ * guard's tests; build-prepare consumes the full assessment.
153
+ *
108
154
  * @param projectRoot - Project root
109
155
  * @param config - Loaded FireForge config (reads `binaryName`)
110
156
  * @returns At-risk files, sorted by path
111
157
  */
112
158
  export async function findUnexportedDriftAtRisk(projectRoot, config) {
159
+ return (await assessUnexportedDrift(projectRoot, config)).atRisk;
160
+ }
161
+ /**
162
+ * Classifies the engine files build-prepare is about to overwrite into the
163
+ * ones whose content is recorded nowhere (`atRisk`) and the deployed
164
+ * Furnace files no patch records yet whose content is their component
165
+ * source's (`deployedUnexported`, safe to overwrite).
166
+ *
167
+ * Fail-open like {@link findUnexportedDriftAtRisk}: a probe failure yields
168
+ * an empty assessment.
169
+ *
170
+ * @param projectRoot - Project root
171
+ * @param config - Loaded FireForge config (reads `binaryName`)
172
+ * @returns Both lists, sorted by path
173
+ */
174
+ export async function assessUnexportedDrift(projectRoot, config) {
175
+ const empty = { atRisk: [], deployedUnexported: [] };
113
176
  const paths = getProjectPaths(projectRoot);
114
177
  try {
115
178
  const status = await getWorkingTreeStatus(paths.engine);
116
179
  if (status.length === 0)
117
- return [];
180
+ return empty;
118
181
  const furnacePrefixes = await collectFurnaceManagedPrefixes(projectRoot);
119
182
  const candidates = await collectOverwrittenEntries(paths.engine, status, config.binaryName, furnacePrefixes);
120
183
  if (candidates.length === 0)
121
- return [];
184
+ return empty;
122
185
  const classified = await classifyFiles(candidates, paths.engine, paths.patches, config.binaryName, furnacePrefixes);
123
- return classified
186
+ const atRisk = classified
124
187
  .filter((entry) => AT_RISK_CLASSIFICATIONS.has(entry.classification))
125
188
  .map((entry) => ({
126
189
  file: entry.file,
@@ -128,12 +191,29 @@ export async function findUnexportedDriftAtRisk(projectRoot, config) {
128
191
  owner: entry.owner,
129
192
  }))
130
193
  .sort((a, b) => a.file.localeCompare(b.file));
194
+ if (atRisk.length === 0 || furnacePrefixes.size === 0)
195
+ return { ...empty, atRisk };
196
+ return await separateDeployedUnexported(projectRoot, paths.engine, atRisk);
131
197
  }
132
198
  catch (error) {
133
199
  verbose(`Unexported-drift guard could not run: ${toError(error).message}`);
134
- return [];
200
+ return empty;
135
201
  }
136
202
  }
203
+ /**
204
+ * Renders the informational note for deployed Furnace files the overwrite
205
+ * rewrites from their own component source: safe for this build, but their
206
+ * patches still owe a re-export.
207
+ */
208
+ export function formatDeployedUnexportedNote(files) {
209
+ const rows = files.map((entry) => {
210
+ const owner = entry.owner !== undefined ? ` (owned by ${entry.owner})` : '';
211
+ return ` ${entry.file} [deployed-unexported: ${entry.match}]${owner}`;
212
+ });
213
+ return (`${files.length} deployed Furnace file(s) differ from their patch body but are a Furnace ` +
214
+ 'deployment of their component source, so this overwrite loses nothing; re-export their ' +
215
+ `patches when the component work is done:\n${rows.join('\n')}`);
216
+ }
137
217
  /**
138
218
  * Renders the operator-facing warning (or refusal body) naming every file
139
219
  * whose unexported content the build is about to overwrite.
@@ -10,6 +10,8 @@ import type { BuildBaseline } from './build-baseline-types.js';
10
10
  export interface BuildPreparation {
11
11
  /** Number of Furnace components applied (0 if none or no furnace.json) */
12
12
  furnaceApplied: number;
13
+ /** Names of the Furnace components applied, in apply order. */
14
+ furnaceAppliedNames?: string[];
13
15
  /** True when `mach configure` was auto-run to refresh a stale backend. */
14
16
  reconfigured: boolean;
15
17
  /** A changed packaging manifest cannot be safely handled by `mach build faster`. */
@@ -35,6 +37,11 @@ export interface PrepareBuildOptions {
35
37
  * Scripted gates want the stop. An interactive build wants the warning.
36
38
  */
37
39
  refuseUnexportedDrift?: boolean | undefined;
40
+ /**
41
+ * Skip the Furnace apply, so the build packages engine/ as it stands
42
+ * (`fireforge test --build --no-furnace-apply`).
43
+ */
44
+ skipFurnaceApply?: boolean | undefined;
38
45
  }
39
46
  /**
40
47
  * Describes an exit code in the shell's 128+N signal convention. Truncated
@@ -13,7 +13,7 @@ import { info, notice, spinner, verbose, warn } from '../utils/logger.js';
13
13
  import { isBrandingSetup, setupBranding } from './branding.js';
14
14
  import { isBuildInputPath, isJarManifestPath } from './build-audit.js';
15
15
  import { evaluateJarManifestEscalation, formatJarEscalationNotice, } from './build-jar-escalation.js';
16
- import { findUnexportedDriftAtRisk, formatUnexportedDriftWarning, } from './build-overwrite-guard.js';
16
+ import { assessUnexportedDrift, formatDeployedUnexportedNote, formatUnexportedDriftWarning, } from './build-overwrite-guard.js';
17
17
  import { hashEngineFile } from './coverage-extend.js';
18
18
  import { collectChangedEnginePaths, dropPathsMatchingFingerprints } from './engine-changes.js';
19
19
  import { applyAllComponents } from './furnace-apply.js';
@@ -156,7 +156,12 @@ export function isBackendInvalidatingFile(path) {
156
156
  * @throws GeneralError under `--refuse-unexported-drift`
157
157
  */
158
158
  async function reportUnexportedDriftBeforeOverwrite(projectRoot, config, options) {
159
- const atRisk = await findUnexportedDriftAtRisk(projectRoot, config);
159
+ const { atRisk, deployedUnexported } = await assessUnexportedDrift(projectRoot, config);
160
+ // Deployed Furnace files that are their component source's own
161
+ // deployment never block: the overwrite loses nothing. Say so, since
162
+ // their patches still owe a re-export.
163
+ if (deployedUnexported.length > 0)
164
+ info(formatDeployedUnexportedNote(deployedUnexported));
160
165
  if (atRisk.length === 0)
161
166
  return;
162
167
  const message = formatUnexportedDriftWarning(atRisk);
@@ -170,15 +175,15 @@ async function reportUnexportedDriftBeforeOverwrite(projectRoot, config, options
170
175
  * Prints every warning the apply computed, then the loud sync banner.
171
176
  *
172
177
  * @param projectRoot - Project root
173
- * @returns Number of components written to engine/
178
+ * @returns Names of the components written to engine/
174
179
  */
175
180
  async function applyFurnaceBeforeBuild(projectRoot) {
176
181
  if (!(await furnaceConfigExists(projectRoot)))
177
- return 0;
182
+ return [];
178
183
  const furnaceConfig = await loadFurnaceConfig(projectRoot);
179
184
  const hasComponents = Object.keys(furnaceConfig.overrides).length > 0 || Object.keys(furnaceConfig.custom).length > 0;
180
185
  if (!hasComponents)
181
- return 0;
186
+ return [];
182
187
  const furnaceSpinner = spinner('Applying Furnace components...');
183
188
  let result;
184
189
  try {
@@ -230,7 +235,7 @@ async function applyFurnaceBeforeBuild(projectRoot) {
230
235
  else {
231
236
  furnaceSpinner.stop('Components up to date');
232
237
  }
233
- return furnaceApplied;
238
+ return result.applied.map((entry) => entry.name);
234
239
  }
235
240
  /**
236
241
  * Runs the auto-configure step (`mach configure`) against the freshly
@@ -344,7 +349,7 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
344
349
  throw error;
345
350
  }
346
351
  }
347
- const furnaceApplied = await applyFurnaceBeforeBuild(projectRoot);
352
+ const furnaceAppliedNames = options.skipFurnaceApply === true ? [] : await applyFurnaceBeforeBuild(projectRoot);
348
353
  // Generate mozconfig
349
354
  const mozconfigSpinner = spinner('Generating mozconfig...');
350
355
  let mozconfigWritten;
@@ -374,7 +379,8 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
374
379
  reconfigured = true;
375
380
  }
376
381
  return {
377
- furnaceApplied,
382
+ furnaceApplied: furnaceAppliedNames.length,
383
+ furnaceAppliedNames,
378
384
  reconfigured,
379
385
  fullBuildRequired,
380
386
  ...(fullBuildReason !== undefined ? { fullBuildReason } : {}),