@hominis/fireforge 0.49.1 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (336) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +1 -1
  3. package/dist/bin/fireforge.js +6 -0
  4. package/dist/build-info.json +4 -4
  5. package/dist/src/commands/build-prelock.d.ts +20 -0
  6. package/dist/src/commands/build-prelock.js +50 -0
  7. package/dist/src/commands/build.d.ts +3 -1
  8. package/dist/src/commands/build.js +13 -6
  9. package/dist/src/commands/check-report.d.ts +28 -0
  10. package/dist/src/commands/check-report.js +78 -0
  11. package/dist/src/commands/check-run.d.ts +28 -0
  12. package/dist/src/commands/check-run.js +104 -0
  13. package/dist/src/commands/check-steps.d.ts +67 -0
  14. package/dist/src/commands/check-steps.js +175 -0
  15. package/dist/src/commands/check.d.ts +32 -0
  16. package/dist/src/commands/check.js +160 -0
  17. package/dist/src/commands/config.js +5 -6
  18. package/dist/src/commands/doctor-engine-eslint.js +6 -1
  19. package/dist/src/commands/doctor-source-pin.js +4 -1
  20. package/dist/src/commands/export-adopt-check.d.ts +38 -0
  21. package/dist/src/commands/export-adopt-check.js +98 -0
  22. package/dist/src/commands/export-flow.js +29 -14
  23. package/dist/src/commands/export-placement-gate.d.ts +6 -2
  24. package/dist/src/commands/export-shared.d.ts +2 -1
  25. package/dist/src/commands/export-staged-registration.d.ts +46 -0
  26. package/dist/src/commands/export-staged-registration.js +91 -0
  27. package/dist/src/commands/forward-import-remedy.d.ts +32 -0
  28. package/dist/src/commands/forward-import-remedy.js +63 -0
  29. package/dist/src/commands/furnace/apply.js +3 -3
  30. package/dist/src/commands/furnace/chrome-doc-remove.d.ts +2 -1
  31. package/dist/src/commands/furnace/chrome-doc-remove.js +2 -2
  32. package/dist/src/commands/furnace/chrome-doc.d.ts +2 -1
  33. package/dist/src/commands/furnace/chrome-doc.js +2 -2
  34. package/dist/src/commands/furnace/create-templates.d.ts +8 -3
  35. package/dist/src/commands/furnace/create-templates.js +8 -23
  36. package/dist/src/commands/furnace/create.js +2 -2
  37. package/dist/src/commands/furnace/deploy-named.d.ts +48 -0
  38. package/dist/src/commands/furnace/deploy-named.js +107 -0
  39. package/dist/src/commands/furnace/deploy.d.ts +3 -2
  40. package/dist/src/commands/furnace/deploy.js +50 -86
  41. package/dist/src/commands/furnace/diff.js +26 -3
  42. package/dist/src/commands/furnace/index.js +66 -41
  43. package/dist/src/commands/furnace/init.js +13 -6
  44. package/dist/src/commands/furnace/override.js +4 -2
  45. package/dist/src/commands/furnace/preview.js +4 -3
  46. package/dist/src/commands/furnace/refresh.js +2 -2
  47. package/dist/src/commands/furnace/remove.js +4 -3
  48. package/dist/src/commands/furnace/rename-dry-run.d.ts +29 -0
  49. package/dist/src/commands/furnace/rename-dry-run.js +127 -0
  50. package/dist/src/commands/furnace/rename-helpers.d.ts +7 -0
  51. package/dist/src/commands/furnace/rename-helpers.js +15 -1
  52. package/dist/src/commands/furnace/rename-registrations.d.ts +41 -0
  53. package/dist/src/commands/furnace/rename-registrations.js +93 -0
  54. package/dist/src/commands/furnace/rename.d.ts +2 -1
  55. package/dist/src/commands/furnace/rename.js +28 -66
  56. package/dist/src/commands/furnace/scan.d.ts +9 -4
  57. package/dist/src/commands/furnace/scan.js +11 -9
  58. package/dist/src/commands/furnace/sync.js +3 -2
  59. package/dist/src/commands/furnace/validate-report.d.ts +45 -0
  60. package/dist/src/commands/furnace/validate-report.js +91 -0
  61. package/dist/src/commands/furnace/validate.d.ts +1 -1
  62. package/dist/src/commands/furnace/validate.js +127 -114
  63. package/dist/src/commands/furnace/validation-output.js +14 -3
  64. package/dist/src/commands/lint-per-patch.js +31 -60
  65. package/dist/src/commands/lint-report.d.ts +8 -4
  66. package/dist/src/commands/lint-report.js +15 -5
  67. package/dist/src/commands/lint.js +15 -34
  68. package/dist/src/commands/manifest.js +2 -0
  69. package/dist/src/commands/patch/delete-gap-notice.js +3 -1
  70. package/dist/src/commands/patch/delete.d.ts +1 -1
  71. package/dist/src/commands/patch/delete.js +13 -98
  72. package/dist/src/commands/patch/index.d.ts +1 -1
  73. package/dist/src/commands/patch/index.js +4 -2
  74. package/dist/src/commands/patch/lint-ignore.d.ts +5 -1
  75. package/dist/src/commands/patch/lint-ignore.js +65 -28
  76. package/dist/src/commands/patch/move-files-create.js +26 -17
  77. package/dist/src/commands/patch/move-files-into.js +5 -3
  78. package/dist/src/commands/patch/move-files.js +3 -1
  79. package/dist/src/commands/patch/removal-dependents.d.ts +63 -0
  80. package/dist/src/commands/patch/removal-dependents.js +133 -0
  81. package/dist/src/commands/patch/rename.js +7 -0
  82. package/dist/src/commands/patch/retire.d.ts +34 -0
  83. package/dist/src/commands/patch/retire.js +128 -0
  84. package/dist/src/commands/patch/split-plan.d.ts +7 -7
  85. package/dist/src/commands/patch/split-plan.js +45 -28
  86. package/dist/src/commands/patch/split-staged.d.ts +108 -0
  87. package/dist/src/commands/patch/split-staged.js +202 -0
  88. package/dist/src/commands/patch/split.d.ts +5 -0
  89. package/dist/src/commands/patch/split.js +51 -15
  90. package/dist/src/commands/patch/staged-dependency-batch.d.ts +48 -0
  91. package/dist/src/commands/patch/staged-dependency-batch.js +176 -0
  92. package/dist/src/commands/patch/staged-dependency-entries.d.ts +36 -0
  93. package/dist/src/commands/patch/staged-dependency-entries.js +52 -0
  94. package/dist/src/commands/patch/staged-dependency-from.d.ts +14 -0
  95. package/dist/src/commands/patch/staged-dependency-from.js +108 -0
  96. package/dist/src/commands/patch/staged-dependency.js +14 -41
  97. package/dist/src/commands/re-export-adjacent-registrations.d.ts +20 -0
  98. package/dist/src/commands/re-export-adjacent-registrations.js +75 -0
  99. package/dist/src/commands/re-export-adjacent.d.ts +45 -2
  100. package/dist/src/commands/re-export-adjacent.js +156 -37
  101. package/dist/src/commands/re-export-drift.d.ts +36 -8
  102. package/dist/src/commands/re-export-drift.js +73 -49
  103. package/dist/src/commands/re-export-files.js +11 -8
  104. package/dist/src/commands/re-export-lint.d.ts +2 -1
  105. package/dist/src/commands/re-export-lint.js +23 -10
  106. package/dist/src/commands/re-export-options.js +45 -4
  107. package/dist/src/commands/re-export-preflight.d.ts +23 -0
  108. package/dist/src/commands/re-export-preflight.js +160 -0
  109. package/dist/src/commands/re-export-register.js +42 -17
  110. package/dist/src/commands/re-export-removed.d.ts +12 -1
  111. package/dist/src/commands/re-export-removed.js +41 -11
  112. package/dist/src/commands/re-export-scan-owner.d.ts +44 -0
  113. package/dist/src/commands/re-export-scan-owner.js +116 -0
  114. package/dist/src/commands/re-export-scan.d.ts +17 -9
  115. package/dist/src/commands/re-export-scan.js +56 -23
  116. package/dist/src/commands/re-export-single.js +22 -6
  117. package/dist/src/commands/re-export-summary.d.ts +28 -0
  118. package/dist/src/commands/re-export-summary.js +36 -0
  119. package/dist/src/commands/re-export.js +37 -24
  120. package/dist/src/commands/rebase/index.js +17 -17
  121. package/dist/src/commands/run.js +16 -2
  122. package/dist/src/commands/status-check.d.ts +55 -16
  123. package/dist/src/commands/status-check.js +92 -37
  124. package/dist/src/commands/status-json.d.ts +22 -9
  125. package/dist/src/commands/status-json.js +47 -32
  126. package/dist/src/commands/status-output.js +3 -1
  127. package/dist/src/commands/status-scope.d.ts +37 -0
  128. package/dist/src/commands/status-scope.js +54 -0
  129. package/dist/src/commands/status.js +66 -23
  130. package/dist/src/commands/test-browser-preflight.js +19 -3
  131. package/dist/src/commands/test-build-furnace.d.ts +21 -0
  132. package/dist/src/commands/test-build-furnace.js +48 -0
  133. package/dist/src/commands/test-build-phase.js +47 -41
  134. package/dist/src/commands/test-checkjs.js +7 -0
  135. package/dist/src/commands/test-diagnose.js +7 -0
  136. package/dist/src/commands/test-modes.d.ts +16 -1
  137. package/dist/src/commands/test-modes.js +24 -1
  138. package/dist/src/commands/test-multi-verdict.d.ts +61 -0
  139. package/dist/src/commands/test-multi-verdict.js +111 -0
  140. package/dist/src/commands/test-orchestrate.d.ts +43 -0
  141. package/dist/src/commands/test-orchestrate.js +222 -0
  142. package/dist/src/commands/test-perf-scope.d.ts +29 -0
  143. package/dist/src/commands/test-perf-scope.js +83 -0
  144. package/dist/src/commands/test-prelock.d.ts +51 -0
  145. package/dist/src/commands/test-prelock.js +177 -0
  146. package/dist/src/commands/test-register.js +54 -10
  147. package/dist/src/commands/test-run.js +4 -1
  148. package/dist/src/commands/test-stale-gate.js +57 -27
  149. package/dist/src/commands/test-verdict.d.ts +10 -1
  150. package/dist/src/commands/test-verdict.js +28 -1
  151. package/dist/src/commands/test.d.ts +6 -1
  152. package/dist/src/commands/test.js +66 -60
  153. package/dist/src/commands/token-lint.d.ts +14 -0
  154. package/dist/src/commands/token-lint.js +111 -0
  155. package/dist/src/commands/token.js +10 -0
  156. package/dist/src/commands/verify-scope.d.ts +28 -0
  157. package/dist/src/commands/verify-scope.js +67 -0
  158. package/dist/src/commands/verify.d.ts +10 -2
  159. package/dist/src/commands/verify.js +59 -35
  160. package/dist/src/core/build-baseline-types.d.ts +46 -0
  161. package/dist/src/core/build-baseline-validate.js +19 -0
  162. package/dist/src/core/build-baseline.d.ts +5 -1
  163. package/dist/src/core/build-baseline.js +3 -1
  164. package/dist/src/core/build-overwrite-guard.d.ts +50 -0
  165. package/dist/src/core/build-overwrite-guard.js +84 -4
  166. package/dist/src/core/build-prepare.d.ts +7 -0
  167. package/dist/src/core/build-prepare.js +14 -8
  168. package/dist/src/core/build-run-record.d.ts +74 -0
  169. package/dist/src/core/build-run-record.js +136 -0
  170. package/dist/src/core/checkjs-support-heads.d.ts +41 -0
  171. package/dist/src/core/checkjs-support-heads.js +104 -0
  172. package/dist/src/core/config-paths.d.ts +2 -2
  173. package/dist/src/core/config-paths.js +8 -0
  174. package/dist/src/core/config-validate-check.d.ts +8 -0
  175. package/dist/src/core/config-validate-check.js +143 -0
  176. package/dist/src/core/config-validate-file-size.d.ts +9 -6
  177. package/dist/src/core/config-validate-file-size.js +91 -2
  178. package/dist/src/core/config-validate-history.d.ts +11 -0
  179. package/dist/src/core/config-validate-history.js +38 -0
  180. package/dist/src/core/config-validate-patch-policy.js +100 -0
  181. package/dist/src/core/config-validate-test-toolchains.js +7 -0
  182. package/dist/src/core/config-validate-token-scales.d.ts +6 -0
  183. package/dist/src/core/config-validate-token-scales.js +50 -0
  184. package/dist/src/core/config-validate.js +14 -5
  185. package/dist/src/core/config.d.ts +5 -0
  186. package/dist/src/core/config.js +16 -0
  187. package/dist/src/core/coverage-dispatch.d.ts +24 -0
  188. package/dist/src/core/coverage-dispatch.js +44 -0
  189. package/dist/src/core/coverage-extend.d.ts +70 -45
  190. package/dist/src/core/coverage-extend.js +199 -144
  191. package/dist/src/core/coverage-records.d.ts +97 -0
  192. package/dist/src/core/coverage-records.js +350 -0
  193. package/dist/src/core/destructive.d.ts +7 -4
  194. package/dist/src/core/destructive.js +10 -10
  195. package/dist/src/core/engine-session-lock.d.ts +10 -1
  196. package/dist/src/core/engine-session-lock.js +28 -8
  197. package/dist/src/core/export-moved-code-shape.d.ts +49 -11
  198. package/dist/src/core/export-moved-code-shape.js +135 -44
  199. package/dist/src/core/file-lock-queue.d.ts +67 -0
  200. package/dist/src/core/file-lock-queue.js +221 -0
  201. package/dist/src/core/file-lock.d.ts +4 -25
  202. package/dist/src/core/file-lock.js +105 -138
  203. package/dist/src/core/furnace-apply-output.d.ts +10 -0
  204. package/dist/src/core/furnace-apply-output.js +31 -1
  205. package/dist/src/core/furnace-apply.js +12 -2
  206. package/dist/src/core/furnace-composed-source.d.ts +76 -0
  207. package/dist/src/core/furnace-composed-source.js +167 -0
  208. package/dist/src/core/furnace-config-validate-exceptions.d.ts +9 -0
  209. package/dist/src/core/furnace-config-validate-exceptions.js +71 -0
  210. package/dist/src/core/furnace-config-validate.js +8 -0
  211. package/dist/src/core/furnace-css-fragments.d.ts +10 -0
  212. package/dist/src/core/furnace-css-fragments.js +26 -0
  213. package/dist/src/core/furnace-operation.d.ts +3 -3
  214. package/dist/src/core/furnace-registration-remove.d.ts +22 -1
  215. package/dist/src/core/furnace-registration-remove.js +53 -17
  216. package/dist/src/core/furnace-registration-rename.d.ts +26 -0
  217. package/dist/src/core/furnace-registration-rename.js +77 -0
  218. package/dist/src/core/furnace-registration.d.ts +14 -4
  219. package/dist/src/core/furnace-registration.js +44 -10
  220. package/dist/src/core/furnace-staleness.d.ts +7 -0
  221. package/dist/src/core/furnace-staleness.js +34 -24
  222. package/dist/src/core/furnace-validate-accessibility.d.ts +4 -2
  223. package/dist/src/core/furnace-validate-accessibility.js +10 -1
  224. package/dist/src/core/furnace-validate-exceptions.d.ts +40 -0
  225. package/dist/src/core/furnace-validate-exceptions.js +43 -0
  226. package/dist/src/core/furnace-validate-icon-button.d.ts +7 -0
  227. package/dist/src/core/furnace-validate-icon-button.js +49 -0
  228. package/dist/src/core/furnace-validate.js +1 -1
  229. package/dist/src/core/harness-port-fallback.d.ts +60 -0
  230. package/dist/src/core/harness-port-fallback.js +96 -0
  231. package/dist/src/core/history-log-file.d.ts +61 -0
  232. package/dist/src/core/history-log-file.js +157 -0
  233. package/dist/src/core/history-log.d.ts +2 -1
  234. package/dist/src/core/history-log.js +2 -1
  235. package/dist/src/core/json-preserving-edit.d.ts +22 -0
  236. package/dist/src/core/json-preserving-edit.js +199 -0
  237. package/dist/src/core/mach.js +8 -1
  238. package/dist/src/core/negative-control-diff.d.ts +39 -0
  239. package/dist/src/core/negative-control-diff.js +136 -0
  240. package/dist/src/core/negative-control.d.ts +138 -0
  241. package/dist/src/core/negative-control.js +356 -0
  242. package/dist/src/core/objdir-install-manifests.d.ts +73 -0
  243. package/dist/src/core/objdir-install-manifests.js +226 -0
  244. package/dist/src/core/patch-export-update.d.ts +2 -0
  245. package/dist/src/core/patch-export-update.js +1 -0
  246. package/dist/src/core/patch-export.js +3 -4
  247. package/dist/src/core/patch-lint-checkjs-any-import.d.ts +37 -0
  248. package/dist/src/core/patch-lint-checkjs-any-import.js +61 -0
  249. package/dist/src/core/patch-lint-checkjs.js +8 -11
  250. package/dist/src/core/patch-lint-code-lines.d.ts +59 -0
  251. package/dist/src/core/patch-lint-code-lines.js +105 -0
  252. package/dist/src/core/patch-lint-cross.js +12 -4
  253. package/dist/src/core/patch-lint-css.d.ts +8 -2
  254. package/dist/src/core/patch-lint-css.js +10 -4
  255. package/dist/src/core/patch-lint-file-size.d.ts +12 -5
  256. package/dist/src/core/patch-lint-file-size.js +22 -11
  257. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +18 -0
  258. package/dist/src/core/patch-lint-forward-registration-extended.js +35 -10
  259. package/dist/src/core/patch-lint-forward-registration.js +10 -0
  260. package/dist/src/core/patch-lint-ignore.d.ts +152 -0
  261. package/dist/src/core/patch-lint-ignore.js +299 -0
  262. package/dist/src/core/patch-lint-size-thresholds.d.ts +52 -0
  263. package/dist/src/core/patch-lint-size-thresholds.js +110 -0
  264. package/dist/src/core/patch-lint-size.d.ts +117 -0
  265. package/dist/src/core/patch-lint-size.js +323 -0
  266. package/dist/src/core/patch-lint-staged-registration.d.ts +18 -1
  267. package/dist/src/core/patch-lint-staged-registration.js +20 -0
  268. package/dist/src/core/patch-lint-token-scale.d.ts +39 -0
  269. package/dist/src/core/patch-lint-token-scale.js +109 -0
  270. package/dist/src/core/patch-lint.d.ts +11 -83
  271. package/dist/src/core/patch-lint.js +64 -286
  272. package/dist/src/core/patch-lock.js +7 -6
  273. package/dist/src/core/patch-manifest-consistency.js +7 -6
  274. package/dist/src/core/patch-manifest-io.d.ts +6 -3
  275. package/dist/src/core/patch-manifest-io.js +19 -8
  276. package/dist/src/core/patch-manifest-validate.js +34 -2
  277. package/dist/src/core/patch-order-legal.d.ts +25 -0
  278. package/dist/src/core/patch-order-legal.js +83 -0
  279. package/dist/src/core/patch-policy-content.d.ts +40 -0
  280. package/dist/src/core/patch-policy-content.js +216 -0
  281. package/dist/src/core/patch-policy.d.ts +16 -3
  282. package/dist/src/core/patch-policy.js +16 -9
  283. package/dist/src/core/patch-retired.d.ts +35 -0
  284. package/dist/src/core/patch-retired.js +54 -0
  285. package/dist/src/core/patch-subset.d.ts +16 -0
  286. package/dist/src/core/patch-subset.js +66 -0
  287. package/dist/src/core/preflight-wait.d.ts +20 -0
  288. package/dist/src/core/preflight-wait.js +38 -0
  289. package/dist/src/core/rebase-from-version.d.ts +46 -0
  290. package/dist/src/core/rebase-from-version.js +71 -0
  291. package/dist/src/core/run-log.d.ts +20 -0
  292. package/dist/src/core/run-log.js +89 -3
  293. package/dist/src/core/run-profile.d.ts +7 -0
  294. package/dist/src/core/run-profile.js +7 -1
  295. package/dist/src/core/smoke-patterns.d.ts +5 -0
  296. package/dist/src/core/smoke-patterns.js +13 -0
  297. package/dist/src/core/test-harness-crash.d.ts +18 -1
  298. package/dist/src/core/test-harness-crash.js +6 -4
  299. package/dist/src/core/test-harness-signature.d.ts +17 -0
  300. package/dist/src/core/test-harness-signature.js +4 -0
  301. package/dist/src/core/test-run-completeness.d.ts +57 -0
  302. package/dist/src/core/test-run-completeness.js +144 -0
  303. package/dist/src/core/test-stale-check.d.ts +27 -1
  304. package/dist/src/core/test-stale-check.js +39 -49
  305. package/dist/src/core/test-stale-exempt.d.ts +40 -0
  306. package/dist/src/core/test-stale-exempt.js +70 -0
  307. package/dist/src/core/token-lint.d.ts +50 -0
  308. package/dist/src/core/token-lint.js +126 -0
  309. package/dist/src/core/tree-guard.js +3 -0
  310. package/dist/src/core/typecheck-shim.d.ts +1 -1
  311. package/dist/src/core/typecheck-shim.js +90 -2
  312. package/dist/src/index.d.ts +8 -3
  313. package/dist/src/index.js +3 -1
  314. package/dist/src/types/commands/index.d.ts +2 -2
  315. package/dist/src/types/commands/options.d.ts +148 -21
  316. package/dist/src/types/commands/patches.d.ts +75 -1
  317. package/dist/src/types/config.d.ts +192 -0
  318. package/dist/src/types/config.js +9 -0
  319. package/dist/src/types/furnace.d.ts +43 -1
  320. package/dist/src/types/furnace.js +7 -0
  321. package/dist/src/types/index.d.ts +1 -1
  322. package/dist/src/utils/options.d.ts +34 -6
  323. package/dist/src/utils/options.js +86 -5
  324. package/dist/src/utils/path-glob.d.ts +15 -0
  325. package/dist/src/utils/path-glob.js +47 -0
  326. package/dist/src/utils/regex.d.ts +15 -3
  327. package/dist/src/utils/regex.js +37 -16
  328. package/docs/configuration.md +282 -9
  329. package/docs/exit-codes.md +18 -1
  330. package/docs/furnace.md +97 -3
  331. package/docs/lifecycle-invariants.md +52 -19
  332. package/docs/machine-output.md +74 -12
  333. package/docs/patch-workflow.md +297 -22
  334. package/docs/run-logs.md +20 -1
  335. package/docs/testing.md +276 -20
  336. package/package.json +2 -2
@@ -8,17 +8,17 @@ import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
8
8
  import { getHead, getStatusWithCodes, isGitRepository, isMissingHeadError } from '../core/git.js';
9
9
  import { listUntrackedFilesInDirs, resolveMaxUntrackedFilesPerDir } from '../core/git-status.js';
10
10
  import { renderOwnershipTable } from '../core/ownership-table.js';
11
- import { loadPatchesManifest } from '../core/patch-manifest.js';
12
11
  import { readProcessCpuSeconds } from '../core/process-cpu-time.js';
13
12
  import { classifyFiles } from '../core/status-classify.js';
14
13
  import { GeneralError, InvalidArgumentError } from '../errors/base.js';
15
14
  import { FIREFORGE_TMP_PATH_PATTERN, pathExists } from '../utils/fs.js';
16
15
  import { info, intro, isMachineOutputMode, outro, setMachineOutputMode, warn, } from '../utils/logger.js';
17
16
  import { emitMachineError } from '../utils/machine-output.js';
18
- import { resolveStatusCheckPolicy, runStatusCheck, } from './status-check.js';
17
+ import { policyNeedsOwnership, resolveStatusCheckPolicy, runStatusCheck, } from './status-check.js';
19
18
  import { renderJsonStatus, renderJsonSummaryStatus } from './status-json.js';
20
19
  import { renderDefaultStatus, renderTestCoverageStatus, renderUnmanagedOnly, } from './status-output.js';
21
- import { buildOwnershipJsonBlock, collectOwnershipRows, summarizeOwnership, } from './status-ownership.js';
20
+ import { buildOwnershipJsonBlock, summarizeOwnership } from './status-ownership.js';
21
+ import { collectScopedOwnershipRows, ownershipConflictPaths, resolveStatusScope, scopeClassified, } from './status-scope.js';
22
22
  /**
23
23
  * Renders raw worktree status as machine-parseable porcelain-style output.
24
24
  * Each line is: STATUS<tab>FILE
@@ -121,26 +121,47 @@ async function scanEngineStatusFiles(engineDir, requireGitRepository) {
121
121
  * policy, which stays the only exit driver on this path.
122
122
  *
123
123
  * `--include-ownership` is a modifier rather than a mode: it appends an
124
- * ownership block without touching exit semantics, so an ownership
125
- * conflict still fails only the human `--ownership` mode.
124
+ * ownership block without touching exit semantics. Ownership conflicts
125
+ * fail this path only when the policy names `ownership-conflict` (or
126
+ * leaves it unnamed under `--fail-on-unknown`).
126
127
  *
127
- * @param classified - Classified worktree entries
128
+ * @param classified - Classified worktree entries, unscoped
128
129
  * @param files - The same entries pre-classification, for ownership rows
129
130
  * @param paths - Resolved project paths
130
131
  * @param options - Status display options
131
132
  * @param checkPolicy - Resolved `--check`/`--fail-on` policy
133
+ * @param scope - The `--patches` scope, when given
132
134
  */
133
- async function renderJsonMode(classified, files, paths, options, checkPolicy) {
134
- const ownership = options.includeOwnership === true
135
- ? buildOwnershipJsonBlock(await collectOwnershipRows(paths.patches, (await loadPatchesManifest(paths.patches))?.patches ?? [], files, classified))
136
- : undefined;
135
+ async function renderJsonMode(classified, files, paths, options, checkPolicy, scope) {
136
+ const rows = options.includeOwnership === true || policyNeedsOwnership(checkPolicy)
137
+ ? await collectScopedOwnershipRows(paths.patches, files, classified, scope)
138
+ : [];
139
+ const context = {
140
+ policy: checkPolicy,
141
+ ownershipConflicts: ownershipConflictPaths(rows),
142
+ ownership: options.includeOwnership === true ? buildOwnershipJsonBlock(rows) : undefined,
143
+ scope,
144
+ };
145
+ const scoped = scopeClassified(classified, scope);
137
146
  if (options.summary === true) {
138
- renderJsonSummaryStatus(classified, checkPolicy, ownership);
147
+ renderJsonSummaryStatus(scoped, context);
139
148
  }
140
149
  else {
141
- renderJsonStatus(classified, ownership);
150
+ renderJsonStatus(scoped, context);
142
151
  }
143
- runStatusCheck(classified, checkPolicy);
152
+ runStatusCheck(scoped, checkPolicy, context.ownershipConflicts);
153
+ }
154
+ /**
155
+ * Applies the `--check` policy to the human default view. Builds the
156
+ * ownership rows only when the policy can fail on `ownership-conflict`,
157
+ * which it can on a clean tree too (two patches claiming one path need no
158
+ * worktree change).
159
+ */
160
+ async function runHumanStatusCheck(classified, files, paths, checkPolicy, scope) {
161
+ const conflicts = policyNeedsOwnership(checkPolicy)
162
+ ? ownershipConflictPaths(await collectScopedOwnershipRows(paths.patches, files, classified, scope))
163
+ : [];
164
+ runStatusCheck(scopeClassified(classified, scope), checkPolicy, conflicts);
144
165
  }
145
166
  /**
146
167
  * Detects the "unborn HEAD" aftermath of an interrupted `fireforge download`:
@@ -200,8 +221,12 @@ export async function statusCommand(projectRoot, options = {}) {
200
221
  setMachineOutputMode(previousMachineOutputMode);
201
222
  }
202
223
  }
203
- /** The status body proper. The machine-mode lifecycle lives in {@link statusCommand}. */
204
- async function runStatusCommandBody(projectRoot, options) {
224
+ /**
225
+ * Refuses flag combinations no view can honour, before any output: one
226
+ * mode at most, and the JSON-only and scope modifiers only where they mean
227
+ * something.
228
+ */
229
+ function assertStatusFlagCombination(options) {
205
230
  const modeCount = [
206
231
  options.raw,
207
232
  options.unmanaged,
@@ -224,6 +249,18 @@ async function runStatusCommandBody(projectRoot, options) {
224
249
  if (options.includeOwnership === true && options.json !== true) {
225
250
  throw new InvalidArgumentError('--include-ownership requires --json. Use --ownership for the human table.', '--include-ownership');
226
251
  }
252
+ // --patches scopes the classifying views and the ownership table only.
253
+ if (options.patches !== undefined &&
254
+ (options.raw === true ||
255
+ options.unmanaged === true ||
256
+ options.testCoverage === true ||
257
+ options.lock === true)) {
258
+ throw new InvalidArgumentError('--patches cannot be combined with --raw, --unmanaged, --test-coverage, or --lock.', '--patches');
259
+ }
260
+ }
261
+ /** The status body proper. The machine-mode lifecycle lives in {@link statusCommand}. */
262
+ async function runStatusCommandBody(projectRoot, options) {
263
+ assertStatusFlagCombination(options);
227
264
  // --check / --fail-on enforcement policy. Applies only
228
265
  // where classification runs (the default view and --json).
229
266
  const checkPolicy = resolveStatusCheckPolicy(options);
@@ -253,6 +290,7 @@ async function runStatusCommandBody(projectRoot, options) {
253
290
  }
254
291
  const paths = getProjectPaths(projectRoot);
255
292
  const config = await loadConfig(projectRoot);
293
+ const scope = await resolveStatusScope(paths.patches, options.patches);
256
294
  // Shared envelope: see src/utils/machine-output.ts and
257
295
  // docs/machine-output.md. `status` was the only command that had one.
258
296
  // Ownership mode is a flat file→patch table. Sources are the manifest's
@@ -265,13 +303,12 @@ async function runStatusCommandBody(projectRoot, options) {
265
303
  // Rung 1 only. See the note below on why this branch does not take the
266
304
  // full assertEngineGitReady ladder.
267
305
  await assertEngineExists(paths.engine);
268
- const manifest = await loadPatchesManifest(paths.patches);
269
306
  // The same scan and the same classification pass as every other mode.
270
307
  // This branch keeps its historical guard shape on purpose:
271
308
  // no baseline-commit assertion, and a non-git engine degrades to an
272
309
  // empty table rather than the recovery banner.
273
310
  const rawFilesOwnership = await scanEngineStatusFiles(paths.engine, false);
274
- const rows = await collectOwnershipRows(paths.patches, manifest?.patches ?? [], rawFilesOwnership, await classifyStatusFiles(rawFilesOwnership, paths, projectRoot, config.binaryName));
311
+ const rows = await collectScopedOwnershipRows(paths.patches, rawFilesOwnership, await classifyStatusFiles(rawFilesOwnership, paths, projectRoot, config.binaryName), scope);
275
312
  renderOwnershipTable(rows);
276
313
  const { managed, unmanaged, conflicts } = summarizeOwnership(rows);
277
314
  const parts = [`${managed} managed`];
@@ -313,7 +350,7 @@ async function runStatusCommandBody(projectRoot, options) {
313
350
  // `[]` here and return before the human fallback.
314
351
  if (options.json) {
315
352
  const classified = await classifyStatusFiles(files, paths, projectRoot, config.binaryName);
316
- await renderJsonMode(classified, files, paths, options, checkPolicy);
353
+ await renderJsonMode(classified, files, paths, options, checkPolicy, scope);
317
354
  return;
318
355
  }
319
356
  // `--raw` consumers parse the native `git status --porcelain` output
@@ -326,6 +363,7 @@ async function runStatusCommandBody(projectRoot, options) {
326
363
  if (files.length === 0) {
327
364
  info('No modified files');
328
365
  outro('Working tree clean');
366
+ await runHumanStatusCheck([], files, paths, checkPolicy, scope);
329
367
  return;
330
368
  }
331
369
  // Raw mode: existing behavior
@@ -333,8 +371,10 @@ async function runStatusCommandBody(projectRoot, options) {
333
371
  renderRawStatus(files);
334
372
  return;
335
373
  }
336
- // Patch-aware classification
337
- const classified = await classifyStatusFiles(files, paths, projectRoot, config.binaryName);
374
+ // Patch-aware classification. The full scan is classified (claims need
375
+ // every file) and the buckets show only what the scope covers.
376
+ const allClassified = await classifyStatusFiles(files, paths, projectRoot, config.binaryName);
377
+ const classified = scopeClassified(allClassified, scope);
338
378
  const buckets = {
339
379
  conflict: classified.filter((f) => f.classification === 'conflict'),
340
380
  unmanaged: classified.filter((f) => f.classification === 'unmanaged'),
@@ -349,8 +389,8 @@ async function runStatusCommandBody(projectRoot, options) {
349
389
  await renderUnmanagedOnly(buckets.unmanaged, files.length, projectRoot, config.binaryName);
350
390
  return;
351
391
  }
352
- await renderDefaultStatus(files.length, buckets, projectRoot, config.binaryName);
353
- runStatusCheck(classified, checkPolicy);
392
+ await renderDefaultStatus(scope ? classified.length : files.length, buckets, projectRoot, config.binaryName);
393
+ await runHumanStatusCheck(allClassified, files, paths, checkPolicy, scope);
354
394
  }
355
395
  /** Registers the status command on the CLI program. */
356
396
  export function registerStatus(program, { getProjectRoot, withErrorHandling }) {
@@ -366,7 +406,10 @@ export function registerStatus(program, { getProjectRoot, withErrorHandling }) {
366
406
  .option('--summary', 'With --json: emit only summary counts (plus offending files when --check/--fail-on is active), omitting the per-file files[] payload')
367
407
  .option('--include-ownership', 'With --json (composes with --summary): append an ownership block (path->owning-patch rows plus managed/unmanaged/conflict counts) to the payload, so one scan serves the classification, ownership, and check views. Does not change exit semantics.')
368
408
  .option('--check', 'Exit non-zero when any unmanaged, patch-owned-drift, or conflict file exists (composes with --json; combine with --fail-on for finer policy)')
369
- .option('--fail-on <classifications>', 'Comma-separated classification list that fails --check, replacing the default set (implies --check). Valid: patch-backed, patch-owned-drift, unmanaged, branding, furnace, conflict, binary-unsupported')
409
+ .option('--fail-on <classifications>', 'Comma-separated classification list that fails --check, replacing the default set (implies --check). Valid: patch-backed, patch-owned-drift, unmanaged, branding, furnace, conflict, binary-unsupported, ownership-conflict (a path claimed by more than one patch)')
410
+ .option('--allow <classifications>', 'Comma-separated classification list that never fails --check, removed from the fail set (implies --check; same values as --fail-on)')
411
+ .option('--fail-on-unknown', 'With --allow: also fail on every present classification that neither --fail-on nor --allow names, so a classification added in a later FireForge fails closed (implies --check)')
412
+ .option('--patches <names...>', 'Judge only the named patches: keeps the files each owns or claims and the ownership rows naming it (filename, stem, order number, manifest name, or slug; repeat or comma-separate). Unmanaged files belong to no patch and drop out. Default view, --json and --ownership only')
370
413
  .action(withErrorHandling(async (options) => {
371
414
  await statusCommand(getProjectRoot(), options);
372
415
  }));
@@ -1,9 +1,10 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { join } from 'node:path';
3
3
  import { reportOrphanedHarnessProcesses } from '../core/harness-orphans.js';
4
- import { assertMarionettePortAvailable, ensureLaunchableBrowserNotRunning, ensureMarionettePortAvailable, extractForwardedMarionettePort, forwardedMachArgsIncludeMarionetteClient, shouldAutoForwardMarionettePortToMach, } from '../core/marionette-port.js';
4
+ import { deferBusyPortToHarness, resolveAutoPorts } from '../core/harness-port-fallback.js';
5
+ import { assertMarionettePortAvailable, DEFAULT_MARIONETTE_PORT, ensureLaunchableBrowserNotRunning, ensureMarionettePortAvailable, extractForwardedMarionettePort, forwardedMachArgsIncludeMarionetteClient, shouldAutoForwardMarionettePortToMach, } from '../core/marionette-port.js';
5
6
  import { formatMarionettePreflightLine, reportMarionettePreflight, runMarionettePreflight, } from '../core/marionette-preflight.js';
6
- import { ensureMochitestServerPortAvailable } from '../core/mochitest-server-port.js';
7
+ import { DEFAULT_MOCHITEST_SERVER_PORT, ensureMochitestServerPortAvailable, } from '../core/mochitest-server-port.js';
7
8
  import { waitForPreflight } from '../core/preflight-wait.js';
8
9
  import { GeneralError } from '../errors/base.js';
9
10
  import { info, outro, verbose } from '../utils/logger.js';
@@ -153,7 +154,15 @@ export async function ensureTestBrowserEnvironment(engineDir, launchablePath, xp
153
154
  // browser connect to a server that cannot serve this run's manifest,
154
155
  // which surfaces as a 370s "Ran 0 checks" stall naming nothing. xpcshell
155
156
  // does not use the httpd, so an xpcshell-only run is never blocked by it.
156
- if (!xpcshellOnly) {
157
+ // Auto ports (opt-in) step aside for a peer's listener and let the
158
+ // harness's own busy-port fallback move this run; see
159
+ // src/core/harness-port-fallback.ts.
160
+ const autoPorts = xpcshellOnly
161
+ ? undefined
162
+ : await resolveAutoPorts(engineDir, options, projectConfig);
163
+ if (!xpcshellOnly &&
164
+ !(autoPorts?.serverPorts === true &&
165
+ (await deferBusyPortToHarness(DEFAULT_MOCHITEST_SERVER_PORT, 'Mochitest server', engineDir)))) {
157
166
  await waitForPreflight(() => ensureMochitestServerPortAvailable(undefined, {
158
167
  engineDir,
159
168
  killStaleServer: options.killStaleMarionette === true,
@@ -173,6 +182,13 @@ export async function ensureTestBrowserEnvironment(engineDir, launchablePath, xp
173
182
  ? extractForwardedMarionettePort(options.machArg)
174
183
  : undefined;
175
184
  const effectivePort = options.marionettePort ?? forwardedPort;
185
+ // Never under --doctor: its handshake launches a browser on this port
186
+ // itself and would talk to the peer's browser instead.
187
+ if (autoPorts?.marionettePort === true &&
188
+ options.doctor !== true &&
189
+ (await deferBusyPortToHarness(effectivePort ?? DEFAULT_MARIONETTE_PORT, 'Marionette', engineDir))) {
190
+ return { forwardedPort, effectivePort };
191
+ }
176
192
  await waitForPreflight(() => ensureTestMarionettePortAvailable(effectivePort, projectConfig.binaryName, options, {
177
193
  xpcshellOnly,
178
194
  doctor: options.doctor === true,
@@ -0,0 +1,21 @@
1
+ /**
2
+ * What a pre-test build did with Furnace components. `prepareBuildEnvironment`
3
+ * runs `furnace apply` over the whole `components/` tree, so a
4
+ * `fireforge test --build` meant to capture the committed behaviour could
5
+ * silently ship component edits nobody had deployed yet (another session's
6
+ * included), and nothing in the test output said so. The applied names now
7
+ * ride the verdict line, and `--no-furnace-apply` packages `engine/` as it
8
+ * stands, naming the undeployed edits it left out.
9
+ */
10
+ import type { BuildPreparation } from '../core/build-prepare.js';
11
+ /**
12
+ * Reports the pre-test build's Furnace step: the components it deployed
13
+ * (notice plus `furnace-applied=<names>` on the verdict line), or, when the
14
+ * apply was skipped, the components whose undeployed edits this build
15
+ * leaves out (`furnace-apply=skipped`).
16
+ *
17
+ * @param projectRoot - Root directory of the project
18
+ * @param preparation - Result of `prepareBuildEnvironment`
19
+ * @param skipped - True under `--no-furnace-apply`
20
+ */
21
+ export declare function reportPreTestFurnace(projectRoot: string, preparation: BuildPreparation, skipped: boolean): Promise<void>;
@@ -0,0 +1,48 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * What a pre-test build did with Furnace components. `prepareBuildEnvironment`
4
+ * runs `furnace apply` over the whole `components/` tree, so a
5
+ * `fireforge test --build` meant to capture the committed behaviour could
6
+ * silently ship component edits nobody had deployed yet (another session's
7
+ * included), and nothing in the test output said so. The applied names now
8
+ * ride the verdict line, and `--no-furnace-apply` packages `engine/` as it
9
+ * stands, naming the undeployed edits it left out.
10
+ */
11
+ import { listStaleFurnaceComponents } from '../core/furnace-staleness.js';
12
+ import { toError } from '../utils/errors.js';
13
+ import { notice, verbose } from '../utils/logger.js';
14
+ import { setVerdictRunAttribute } from './test-verdict.js';
15
+ /**
16
+ * Reports the pre-test build's Furnace step: the components it deployed
17
+ * (notice plus `furnace-applied=<names>` on the verdict line), or, when the
18
+ * apply was skipped, the components whose undeployed edits this build
19
+ * leaves out (`furnace-apply=skipped`).
20
+ *
21
+ * @param projectRoot - Root directory of the project
22
+ * @param preparation - Result of `prepareBuildEnvironment`
23
+ * @param skipped - True under `--no-furnace-apply`
24
+ */
25
+ export async function reportPreTestFurnace(projectRoot, preparation, skipped) {
26
+ if (skipped) {
27
+ setVerdictRunAttribute('furnace-apply', 'skipped');
28
+ let pending = [];
29
+ try {
30
+ pending = await listStaleFurnaceComponents(projectRoot);
31
+ }
32
+ catch (error) {
33
+ verbose(`Furnace pending-edit probe failed: ${toError(error).message}`);
34
+ }
35
+ notice(pending.length > 0
36
+ ? `Furnace apply skipped (--no-furnace-apply): engine/ is packaged as it stands. ${pending.length} component(s) with undeployed components/ edits are NOT in this build: ${pending.join(', ')}.`
37
+ : 'Furnace apply skipped (--no-furnace-apply): engine/ is packaged as it stands.');
38
+ return;
39
+ }
40
+ const applied = preparation.furnaceAppliedNames ?? [];
41
+ if (applied.length === 0)
42
+ return;
43
+ setVerdictRunAttribute('furnace-applied', applied.join(','));
44
+ notice(`This pre-test build deployed ${applied.length} Furnace component(s) whose components/ source ` +
45
+ `changed since their last deploy: ${applied.join(', ')}. Those edits (another session's ` +
46
+ 'undeployed edits included) are in the runtime under test. Pass --no-furnace-apply to ' +
47
+ 'package engine/ as it stands, for example for a before-capture.');
48
+ }
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { readBuildBaseline, writeBuildBaseline } from '../core/build-baseline.js';
8
8
  import { prepareBuildEnvironment } from '../core/build-prepare.js';
9
- import { checkExtendCoverageAnchor, checkExtendMozconfigAnchor, formatExtendCoverageRefusal, unionTestPackagingCoverage, } from '../core/coverage-extend.js';
9
+ import { applyMozconfigAnchor, formatCoverageRetentionNotice, formatExtendCoverageRefusal, planCoverageRetention, recordScopedCoverage, } from '../core/coverage-extend.js';
10
10
  import { hasBuildArtifacts } from '../core/mach.js';
11
11
  import { runProtectedMachBuild, withBuildLock } from '../core/mach.js';
12
12
  import { assertHealthyPartialConfig, pruneDanglingTestLinks } from '../core/objdir-maintenance.js';
@@ -15,7 +15,7 @@ import { GeneralError, InvalidArgumentError } from '../errors/base.js';
15
15
  import { BuildError } from '../errors/build.js';
16
16
  import { toError } from '../utils/errors.js';
17
17
  import { info, notice, spinner, verbose } from '../utils/logger.js';
18
- import { checkRequestedTestTypes } from './test-checkjs.js';
18
+ import { reportPreTestFurnace } from './test-build-furnace.js';
19
19
  import { reportBuildOnlyCompletion } from './test-modes.js';
20
20
  import { enforceStaleBuildGate, enforceStaticComponentsGate } from './test-stale-gate.js';
21
21
  import { emitPassVerdict } from './test-verdict.js';
@@ -26,7 +26,7 @@ import { emitPassVerdict } from './test-verdict.js';
26
26
  * @param options - See {@link RunPreTestBuildOptions}
27
27
  */
28
28
  async function runPreTestBuild(options) {
29
- const { projectRoot, paths, projectConfig, harnessRetries, testPackagingCoverage, refuseUnexportedDrift, extend, } = options;
29
+ const { projectRoot, paths, projectConfig, harnessRetries, testPackagingCoverage, refuseUnexportedDrift, extend, skipFurnaceApply, } = options;
30
30
  await withBuildLock(projectRoot, async () => {
31
31
  // Pass the previous baseline exactly like `fireforge build` does, so
32
32
  // auto-configure runs under the same conditions on both paths. The
@@ -41,32 +41,28 @@ async function runPreTestBuild(options) {
41
41
  if (pruned > 0)
42
42
  notice(`Pruned ${pruned} dangling _tests symlink(s).`);
43
43
  }
44
- await checkRequestedTestTypes(paths, projectConfig, testPackagingCoverage === 'full' ? [] : testPackagingCoverage);
44
+ // The named-file checkJs pass runs before the engine lock is even
45
+ // requested (test-prelock.ts), so a type error refuses without a wait.
45
46
  const preparation = await prepareBuildEnvironment(projectRoot, paths, projectConfig, {
46
47
  previousBaseline,
47
48
  refuseUnexportedDrift,
49
+ skipFurnaceApply,
48
50
  });
49
- // The mozconfig half of the --extend-coverage anchor can
50
- // only be checked once prepareBuildEnvironment has regenerated
51
- // engine/mozconfig (the file this build will configure with), and must
52
- // still be checked before mach, so a refusal costs no build.
53
- let retainCoverage = false;
54
- if (testPackagingCoverage !== 'full' && previousBaseline) {
55
- const anchor = await checkExtendCoverageAnchor(paths.engine, previousBaseline, testPackagingCoverage);
56
- const mozconfigAnchor = await checkExtendMozconfigAnchor(paths.engine, previousBaseline);
57
- retainCoverage =
58
- anchor.ok && mozconfigAnchor.ok && previousBaseline.testInputFingerprints !== undefined;
59
- if (extend !== undefined && (!anchor.ok || !mozconfigAnchor.ok)) {
60
- throw new GeneralError(formatExtendCoverageRefusal(!anchor.ok ? anchor : mozconfigAnchor));
61
- }
62
- if (!retainCoverage) {
63
- const old = previousBaseline.testPackagingCoverage;
64
- const dropped = old === undefined || old === 'full'
65
- ? ['full coverage outside this scope']
66
- : old.filter((file) => !testPackagingCoverage.includes(file));
67
- if (dropped.length)
68
- notice(`Scoped build drops previous packaging coverage: ${dropped.join(', ')}. Earlier staging inputs changed or have no complete fingerprint anchor; rebuild those paths together to retain them.`);
51
+ await reportPreTestFurnace(projectRoot, preparation, skipFurnaceApply);
52
+ // Which earlier claims this scoped build keeps, per staged path. The
53
+ // mozconfig half can only be checked once prepareBuildEnvironment has
54
+ // regenerated engine/mozconfig (the file this build will configure
55
+ // with), and must still be checked before mach, so a refusal costs no
56
+ // build.
57
+ let plan;
58
+ if (testPackagingCoverage !== 'full') {
59
+ plan = await applyMozconfigAnchor(paths.engine, await planCoverageRetention(paths.engine, previousBaseline, testPackagingCoverage));
60
+ if (extend !== undefined && plan.dropped.length > 0) {
61
+ throw new GeneralError(formatExtendCoverageRefusal(plan.dropped));
69
62
  }
63
+ const retentionNotice = formatCoverageRetentionNotice(plan);
64
+ if (retentionNotice !== undefined)
65
+ notice(retentionNotice);
70
66
  }
71
67
  const buildKind = preparation.fullBuildRequired ? 'full' : 'faster';
72
68
  if (preparation.fullBuildRequired) {
@@ -103,24 +99,24 @@ async function runPreTestBuild(options) {
103
99
  // carries the static-components anchor forward, since `mach build
104
100
  // faster` does not rebake components.conf into the compiled table.
105
101
  try {
106
- // Under --extend-coverage the claim is the union of the previous
107
- // record and this build's paths, and the static-components anchor
108
- // is always carried forward: that union can evaluate to 'full'
109
- // while the build behind it was a scoped `mach build faster` that
102
+ // A scoped claim records this build's paths plus every earlier path
103
+ // the plan kept, each with its own staging-input anchor. When it
104
+ // kept anything the static-components anchor is carried forward
105
+ // explicitly: a kept full claim is still a `mach build faster` that
110
106
  // did not rebake the compiled table.
111
- const recordedCoverage = retainCoverage
112
- ? unionTestPackagingCoverage(previousBaseline?.testPackagingCoverage, testPackagingCoverage === 'full' ? [] : testPackagingCoverage)
113
- : testPackagingCoverage;
107
+ const recorded = plan === undefined || testPackagingCoverage === 'full'
108
+ ? { testPackagingCoverage }
109
+ : await recordScopedCoverage(paths.engine, plan, testPackagingCoverage);
114
110
  await writeBuildBaseline({
115
111
  projectRoot,
116
112
  engineDir: paths.engine,
117
113
  binaryName: projectConfig.binaryName,
118
- testPackagingCoverage: recordedCoverage,
114
+ ...recorded,
119
115
  previousBaseline,
120
116
  recordedBy: describeBuildInvocation(extend !== undefined, testPackagingCoverage),
121
117
  staticComponentsHandling: preparation.fullBuildRequired
122
118
  ? 'refresh'
123
- : retainCoverage
119
+ : plan !== undefined && (plan.keepFull || Object.keys(plan.retained).length > 0)
124
120
  ? 'carry-forward'
125
121
  : 'auto',
126
122
  buildKind,
@@ -138,15 +134,15 @@ async function runPreTestBuild(options) {
138
134
  });
139
135
  }
140
136
  /**
141
- * `--extend-coverage` only means something for a scoped build: it unions
142
- * this build's paths onto the recorded claim. Without `--build`/
137
+ * `--extend-coverage` only means something for a scoped build: it refuses
138
+ * a build that would drop an earlier claim. Without `--build`/
143
139
  * `--build-only` there is no build to extend from, and a path-less build
144
140
  * records `'full'`, which already covers everything. Refusing both is more
145
141
  * honest than silently ignoring the flag.
146
142
  */
147
143
  function assertExtendCoverageUsage(options, normalizedPaths) {
148
144
  if (options.build !== true && options.buildOnly !== true) {
149
- throw new InvalidArgumentError('--extend-coverage requires --build or --build-only: it unions the paths a build packages onto the recorded coverage claim.', '--extend-coverage');
145
+ throw new InvalidArgumentError('--extend-coverage requires --build or --build-only: it guards the coverage claim a build records.', '--extend-coverage');
150
146
  }
151
147
  if (normalizedPaths.length === 0) {
152
148
  throw new InvalidArgumentError('--extend-coverage requires explicit test paths: a path-less build records full coverage, which already covers every path.', '--extend-coverage');
@@ -165,6 +161,14 @@ function assertRefuseUnexportedDriftUsage(options) {
165
161
  return;
166
162
  throw new InvalidArgumentError('--refuse-unexported-drift requires --build or --build-only: it guards the pre-test build, and this run dispatches none.', '--refuse-unexported-drift');
167
163
  }
164
+ /** Refuses `--no-furnace-apply` on a run that dispatches no build, for the same reason. */
165
+ function assertNoFurnaceApplyUsage(options) {
166
+ if (options.furnaceApply !== false)
167
+ return;
168
+ if (options.build === true || options.buildOnly === true)
169
+ return;
170
+ throw new InvalidArgumentError("--no-furnace-apply requires --build or --build-only: it skips the pre-test build's Furnace apply, and this run dispatches no build.", '--no-furnace-apply');
171
+ }
168
172
  /** Renders the `recordedBy` invocation string surfaced by `status --test-coverage`. */
169
173
  function describeBuildInvocation(extending, coverage) {
170
174
  const extendFlag = extending ? ' --extend-coverage' : '';
@@ -186,6 +190,7 @@ export async function runTestBuildPhase(projectRoot, paths, projectConfig, harne
186
190
  assertExtendCoverageUsage(options, normalizedPaths);
187
191
  }
188
192
  assertRefuseUnexportedDriftUsage(options);
193
+ assertNoFurnaceApplyUsage(options);
189
194
  if (options.build || options.buildOnly) {
190
195
  // A path-less `test --build` runs (and packages for) the full suite.
191
196
  // A scoped invocation only vouches for the requested paths. A scoped
@@ -197,11 +202,11 @@ export async function runTestBuildPhase(projectRoot, paths, projectConfig, harne
197
202
  const previousBaseline = await readBuildBaseline(projectRoot);
198
203
  await enforceStaticComponentsGate(paths.engine, previousBaseline, options);
199
204
  if (extending) {
200
- // Head/fingerprint half of the anchor, before the build lock: a
201
- // refusal here costs neither build time nor the lock.
202
- const anchor = await checkExtendCoverageAnchor(paths.engine, previousBaseline, normalizedPaths);
203
- if (!anchor.ok) {
204
- throw new GeneralError(formatExtendCoverageRefusal(anchor));
205
+ // Staging-input half of the plan, before the build lock: a refusal
206
+ // here costs neither build time nor the lock.
207
+ const plan = await planCoverageRetention(paths.engine, previousBaseline, normalizedPaths);
208
+ if (plan.dropped.length > 0) {
209
+ throw new GeneralError(formatExtendCoverageRefusal(plan.dropped));
205
210
  }
206
211
  }
207
212
  }
@@ -213,6 +218,7 @@ export async function runTestBuildPhase(projectRoot, paths, projectConfig, harne
213
218
  testPackagingCoverage: coverage,
214
219
  refuseUnexportedDrift: options.refuseUnexportedDrift === true,
215
220
  extend: extending ? { requestedPaths: normalizedPaths } : undefined,
221
+ skipFurnaceApply: options.furnaceApply === false,
216
222
  });
217
223
  info('');
218
224
  if (options.buildOnly) {
@@ -1,4 +1,5 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
+ import { findUnownedSupportHeads, formatUnownedSupportHeadNotice, } from '../core/checkjs-support-heads.js';
2
3
  import { runCheckJsTestFilesGrouped } from '../core/patch-lint-checkjs.js';
3
4
  import { isTestScriptFile } from '../core/patch-lint-ownership.js';
4
5
  import { loadPatchesManifest } from '../core/patch-manifest.js';
@@ -17,6 +18,12 @@ export async function checkRequestedTestTypes(paths, config, requested) {
17
18
  const selected = new Set(scopes.flatMap((scope) => scope.dispatchPaths.filter(isTestScriptFile)));
18
19
  for (const file of selected)
19
20
  owned.add(file);
21
+ // A support head a named test brings into scope joins the pass even
22
+ // before any patch owns it, or every name it declares reads as undefined.
23
+ for (const head of await findUnownedSupportHeads(paths.engine, selected, owned)) {
24
+ owned.add(head.file);
25
+ notice(`Pre-build checkJs: ${formatUnownedSupportHeadNotice(head)}`);
26
+ }
20
27
  const result = await runCheckJsTestFilesGrouped(paths.engine, owned, config.patchLint, paths.root, selected);
21
28
  const issues = [...result.global, ...[...result.byFile.values()].flat()];
22
29
  for (const issue of issues)
@@ -11,6 +11,7 @@ import { getActiveRunLogPath } from '../core/run-log.js';
11
11
  import { describeChangedPrefNoise } from '../core/test-changed-prefs.js';
12
12
  import { buildGreenSummaryRejectedMessage, buildHarnessCrashMessage, buildNoTestsRanMessage, buildSilentSegfaultMessage, headedNoOutputTimeoutHint, isSilentSegfault, } from '../core/test-harness-crash.js';
13
13
  import { buildHarnessEarlyExitMessage, classifyHarnessEarlyExit, completePostRebuildFailureContext, prependPostRebuildFailureContext, } from '../core/test-harness-output.js';
14
+ import { buildIncompleteRunMessage } from '../core/test-run-completeness.js';
14
15
  import { GeneralError } from '../errors/base.js';
15
16
  import { TestFailureError } from '../errors/build.js';
16
17
  import { toError } from '../utils/errors.js';
@@ -249,6 +250,9 @@ function applySingleRunOutcome(outcome, normalizedPaths, binaryName, postRebuild
249
250
  }
250
251
  return;
251
252
  }
253
+ if (outcome.verdict.incomplete !== undefined) {
254
+ throw new TestFailureError(buildIncompleteRunMessage(outcome.verdict.incomplete), 'mach test', formatFailureBlocks(outcome.verdict.realFailureBlocks), getActiveRunLogPath());
255
+ }
252
256
  if (outcome.verdict.kind === 'no-tests' && outcome.result.exitCode === 0) {
253
257
  // The silent false green: exit 0 plus a "Passed: 0"-style summary with
254
258
  // zero TEST-START lines must fail rather than pass.
@@ -282,6 +286,9 @@ export function diagnoseShardOutcome(outcome, path, binaryName, postRebuildConte
282
286
  if (outcome.verdict.kind === 'no-tests' && outcome.result.exitCode === 0) {
283
287
  return buildNoTestsRanMessage(0, [path]);
284
288
  }
289
+ if (outcome.verdict.incomplete !== undefined) {
290
+ return buildIncompleteRunMessage(outcome.verdict.incomplete);
291
+ }
285
292
  if (outcome.verdict.greenSummaryRejected) {
286
293
  return buildGreenSummaryRejectedMessage(outcome.verdict.greenSummaryRejected, outcome.result.exitCode);
287
294
  }
@@ -1,6 +1,6 @@
1
1
  import type { TestOptions } from '../types/commands/index.js';
2
2
  import type { FireForgeConfig } from '../types/config.js';
3
- import type { TestRunOutcome } from './test-run.js';
3
+ import type { TestRunOutcome, TestSuite } from './test-run.js';
4
4
  /** Rejects pathless `fireforge test` unless the operator selected a pathless mode. */
5
5
  export declare function assertPathlessTestMode(testPaths: readonly string[], options: TestOptions): void;
6
6
  /** Rejects invalid combinations for mode-like `fireforge test` options. */
@@ -16,6 +16,8 @@ export interface HarnessClassification {
16
16
  xpcshell: string[];
17
17
  nonXpcshell: string[];
18
18
  }
19
+ /** Buckets normalized request paths by the harness their nearest manifest selects. */
20
+ export declare function classifyTestHarnesses(engineDir: string, normalizedPaths: readonly string[]): Promise<HarnessClassification>;
19
21
  /**
20
22
  * Harness classification is a pure path→manifest lookup and must precede
21
23
  * the build dispatch: a mixed xpcshell+mochitest request can never
@@ -40,3 +42,16 @@ export declare function classifyBeforeDispatch(engineDir: string, normalizedPath
40
42
  * run build-less without tripping the stale/coverage gate.
41
43
  */
42
44
  export declare function reportBuildOnlyCompletion(classification: HarnessClassification, normalizedPaths: readonly string[]): void;
45
+ /**
46
+ * The mach args `--restart-between-tests` forwards: mochitest's own flag,
47
+ * which restarts the browser between test files (the run shape a
48
+ * `standalone` manifest key asks for). Mochitest-only, refused otherwise
49
+ * for the reason `--shuffle` is: xpcshell has no such flag and generic
50
+ * `mach test` would hand it to every suite. Not duplicated when
51
+ * `--mach-arg` already forwards it.
52
+ *
53
+ * @param options - Parsed test options
54
+ * @param suite - The resolved dispatch target
55
+ * @param forwardedMachArgs - `--mach-arg` values after filtering
56
+ */
57
+ export declare function restartBetweenTestsArgs(options: TestOptions, suite: TestSuite, forwardedMachArgs: readonly string[]): string[];
@@ -72,7 +72,8 @@ function buildPathlessTestMessage() {
72
72
  ' - `fireforge test --doctor` to run the Marionette preflight only\n' +
73
73
  ' - `fireforge test --canary` to run the configured short harness canary');
74
74
  }
75
- async function classifyTestHarnesses(engineDir, normalizedPaths) {
75
+ /** Buckets normalized request paths by the harness their nearest manifest selects. */
76
+ export async function classifyTestHarnesses(engineDir, normalizedPaths) {
76
77
  const result = { xpcshell: [], nonXpcshell: [] };
77
78
  for (const testPath of normalizedPaths) {
78
79
  const manifest = await findNearestXpcshellManifest(engineDir, testPath);
@@ -133,3 +134,25 @@ export function reportBuildOnlyCompletion(classification, normalizedPaths) {
133
134
  info(`Run: fireforge test ${normalizedPaths.join(' ')}`);
134
135
  }
135
136
  }
137
+ /**
138
+ * The mach args `--restart-between-tests` forwards: mochitest's own flag,
139
+ * which restarts the browser between test files (the run shape a
140
+ * `standalone` manifest key asks for). Mochitest-only, refused otherwise
141
+ * for the reason `--shuffle` is: xpcshell has no such flag and generic
142
+ * `mach test` would hand it to every suite. Not duplicated when
143
+ * `--mach-arg` already forwards it.
144
+ *
145
+ * @param options - Parsed test options
146
+ * @param suite - The resolved dispatch target
147
+ * @param forwardedMachArgs - `--mach-arg` values after filtering
148
+ */
149
+ export function restartBetweenTestsArgs(options, suite, forwardedMachArgs) {
150
+ if (options.restartBetweenTests !== true)
151
+ return [];
152
+ if (suite !== 'mochitest') {
153
+ throw new GeneralError("--restart-between-tests forwards the mochitest harness's per-file browser restart; the " +
154
+ 'xpcshell harness has none, and generic `mach test` dispatch would hand it to every ' +
155
+ 'suite. Narrow the paths to browser-chrome/mochitest files (and drop --generic-mach-test).');
156
+ }
157
+ return forwardedMachArgs.includes('--restart-between-tests') ? [] : ['--restart-between-tests'];
158
+ }