@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
@@ -1,62 +1,87 @@
1
1
  /**
2
- * Retains prior test staging only when HEAD, mozconfig and every recorded
3
- * dirty input outside the newly rebuilt scope are unchanged. Complete
4
- * input fingerprints are required: packageable-only legacy records cannot
2
+ * Decides which earlier packaging claims a scoped `fireforge test --build`
3
+ * keeps. Retention is per staged path (see `coverage-records.ts`): an
4
+ * earlier path stays covered while its own staging inputs are unchanged,
5
+ * whatever moved elsewhere in the engine, and a full claim narrows to
6
+ * per-path checks once something outside the new scope changed instead of
7
+ * collapsing to this build's paths. `--extend-coverage` turns any drop into
8
+ * a refusal before the build.
9
+ *
10
+ * Fingerprints must be complete: a legacy packageable-only record cannot
5
11
  * prove that a test fixture or manifest still matches its staged copy.
6
12
  */
7
- import { type BuildBaseline, type TestPackagingCoverage } from './build-baseline-types.js';
8
- /** Why `--extend-coverage` could not union onto the recorded claim. */
9
- export type ExtendAnchorReason = 'no-baseline' | 'no-fingerprints' | 'no-mozconfig-hash' | 'head-moved' | 'fingerprint-diverged' | 'mozconfig-changed';
10
- /** Outcome of an anchor check: either extend is sound, or it is refused. */
11
- export type ExtendAnchorResult = {
12
- ok: true;
13
- } | {
14
- ok: false;
15
- reason: ExtendAnchorReason;
16
- detail: string[];
17
- };
13
+ import type { BuildBaseline, CoverageAnchor, CoverageRecord, TestPackagingCoverage } from './build-baseline-types.js';
14
+ export { hashEngineFile } from './coverage-records.js';
15
+ /** What a dropped full claim is called in notices and refusals. */
16
+ export declare const FULL_CLAIM_LABEL = "full coverage";
17
+ /** An earlier claim this build cannot keep, and why. */
18
+ export interface DroppedClaim {
19
+ /** Request path, or {@link FULL_CLAIM_LABEL}. */
20
+ path: string;
21
+ /** Staging inputs whose content changed since the claim was staged. */
22
+ changed: string[];
23
+ /** Why the claim cannot be verified at all, when no input list explains it. */
24
+ reason?: string;
25
+ }
26
+ /** What a scoped build keeps of the recorded coverage. */
27
+ export interface CoverageRetentionPlan {
28
+ /** The previous claim was full and nothing outside this build's scope moved. */
29
+ keepFull: boolean;
30
+ /** Earlier per-path records kept, refreshed for this build's paths. */
31
+ retained: Record<string, CoverageRecord>;
32
+ /** Full-build anchor carried for dispatch-time per-path checks. */
33
+ fullAnchor: CoverageAnchor | undefined;
34
+ /** Changed inputs that narrowed a full claim to per-path checks. */
35
+ narrowedBy: string[];
36
+ /** Earlier claims this build cannot keep. */
37
+ dropped: DroppedClaim[];
38
+ }
39
+ /** The baseline-wide anchor a baseline's own fields describe, when complete. */
40
+ export declare function baselineAnchor(baseline: BuildBaseline): CoverageAnchor | undefined;
18
41
  /**
19
- * Verifies the parts of the anchor knowable before the build runs: a usable
20
- * previous baseline, an unchanged engine HEAD, and byte-identical content
21
- * for every path that baseline fingerprinted.
42
+ * Plans which earlier claims a scoped build over `requested` keeps. Runs
43
+ * before the build lock, so a refusal costs neither build time nor the
44
+ * lock. The mozconfig half runs later, via {@link applyMozconfigAnchor}.
45
+ *
22
46
  * @param engineDir - Path to the engine directory
23
- * @param previousBaseline - Baseline recorded by the previous build
47
+ * @param previous - Baseline recorded by the previous build
48
+ * @param requested - Normalized request paths this build packages
24
49
  */
25
- export declare function checkExtendCoverageAnchor(engineDir: string, previousBaseline: BuildBaseline | undefined, rebuiltPaths?: readonly string[]): Promise<ExtendAnchorResult>;
50
+ export declare function planCoverageRetention(engineDir: string, previous: BuildBaseline | undefined, requested: string[]): Promise<CoverageRetentionPlan>;
26
51
  /**
27
- * Verifies the mozconfig half of the anchor. Split from
28
- * {@link checkExtendCoverageAnchor} because it can only run after
29
- * `prepareBuildEnvironment` regenerates `engine/mozconfig` (the file this
30
- * build will configure with), and must still run before mach, so a refusal
31
- * costs no build time.
52
+ * Applies the mozconfig half of the anchor: a claim staged under a different
53
+ * `engine/mozconfig` is dropped. Runs after `prepareBuildEnvironment`
54
+ * regenerated the file this build configures with, and before mach.
55
+ *
32
56
  * @param engineDir - Path to the engine directory
33
- * @param previousBaseline - Baseline recorded by the previous build
57
+ * @param plan - Plan from {@link planCoverageRetention}
34
58
  */
35
- export declare function checkExtendMozconfigAnchor(engineDir: string, previousBaseline: BuildBaseline | undefined): Promise<ExtendAnchorResult>;
59
+ export declare function applyMozconfigAnchor(engineDir: string, plan: CoverageRetentionPlan): Promise<CoverageRetentionPlan>;
60
+ /** Fields a scoped build writes onto its baseline. */
61
+ export interface RecordedCoverage {
62
+ testPackagingCoverage: TestPackagingCoverage;
63
+ testCoverageRecords?: Record<string, CoverageRecord>;
64
+ fullCoverageAnchor?: CoverageAnchor;
65
+ }
36
66
  /**
37
- * Unions the newly built paths onto the recorded claim. A `'full'` claim, or
38
- * an absent one (which means full), stays `'full'`, since it already covers
39
- * everything.
67
+ * The coverage a green scoped build records: `'full'` when the plan kept a
68
+ * full claim, otherwise this build's paths with fresh records plus every
69
+ * retained earlier path, and the carried full anchor.
40
70
  *
41
- * @param previous - Coverage recorded by the previous build
71
+ * @param engineDir - Path to the engine directory
72
+ * @param plan - Plan after {@link applyMozconfigAnchor}
42
73
  * @param requested - Normalized request paths this build packaged
43
74
  */
44
- export declare function unionTestPackagingCoverage(previous: TestPackagingCoverage | undefined, requested: readonly string[]): TestPackagingCoverage;
75
+ export declare function recordScopedCoverage(engineDir: string, plan: CoverageRetentionPlan, requested: string[]): Promise<RecordedCoverage>;
76
+ /** Renders one dropped claim with its cause. */
77
+ export declare function describeDroppedClaim(claim: DroppedClaim): string;
45
78
  /**
46
- * Renders the fail-closed refusal for an anchor check that did not pass.
47
- * Kept separate from the probes so tests can assert the structured result
48
- * without matching copy.
49
- * @param result - Failing result from an anchor check
79
+ * The notice a scoped build prints about earlier coverage, or undefined
80
+ * when nothing was dropped or narrowed.
50
81
  */
51
- export declare function formatExtendCoverageRefusal(result: {
52
- reason: ExtendAnchorReason;
53
- detail: string[];
54
- }): string;
82
+ export declare function formatCoverageRetentionNotice(plan: CoverageRetentionPlan): string | undefined;
55
83
  /**
56
- * Hex-encoded SHA-256 of an engine-relative file, matching the digest the
57
- * baseline writer produces. `undefined` on any IO error, which every caller
58
- * treats as "cannot prove unchanged", i.e. a refusal.
59
- * @param engineDir - Path to the engine directory
60
- * @param relPath - Engine-relative POSIX path
84
+ * The `--extend-coverage` refusal: the flag asks that no earlier claim be
85
+ * dropped, and the plan would drop some.
61
86
  */
62
- export declare function hashEngineFile(engineDir: string, relPath: string): Promise<string | undefined>;
87
+ export declare function formatExtendCoverageRefusal(dropped: readonly DroppedClaim[]): string;
@@ -1,179 +1,234 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
- * Retains prior test staging only when HEAD, mozconfig and every recorded
4
- * dirty input outside the newly rebuilt scope are unchanged. Complete
5
- * input fingerprints are required: packageable-only legacy records cannot
3
+ * Decides which earlier packaging claims a scoped `fireforge test --build`
4
+ * keeps. Retention is per staged path (see `coverage-records.ts`): an
5
+ * earlier path stays covered while its own staging inputs are unchanged,
6
+ * whatever moved elsewhere in the engine, and a full claim narrows to
7
+ * per-path checks once something outside the new scope changed instead of
8
+ * collapsing to this build's paths. `--extend-coverage` turns any drop into
9
+ * a refusal before the build.
10
+ *
11
+ * Fingerprints must be complete: a legacy packageable-only record cannot
6
12
  * prove that a test fixture or manifest still matches its staged copy.
7
13
  */
8
- import { readFile } from 'node:fs/promises';
9
- import { join } from 'node:path';
10
- import { getNodeErrorCode, toError } from '../utils/errors.js';
11
- import { sha256Hex } from '../utils/hash.js';
14
+ import { toError } from '../utils/errors.js';
12
15
  import { verbose } from '../utils/logger.js';
13
- import { DELETED_FILE_FINGERPRINT, } from './build-baseline-types.js';
14
- import { getHead, isMissingHeadError } from './git.js';
15
- import { git } from './git-base.js';
16
- import { getUntrackedFiles } from './git-status.js';
16
+ import { collectStagingInputs, createCoverageRecord, findChangedInputs, hashEngineFile, isUnderPath, narrowAnchor, normalizeRequestPath, readEngineContentState, refreshAnchor, } from './coverage-records.js';
17
+ export { hashEngineFile } from './coverage-records.js';
17
18
  /** Filename of the generated mozconfig, relative to the engine directory. */
18
19
  const MOZCONFIG_FILENAME = 'mozconfig';
19
- /** How many diverged paths a refusal names before truncating. */
20
- const MAX_DIVERGED_REPORTED = 5;
20
+ /** How many paths a notice or refusal names before truncating. */
21
+ const MAX_PATHS_REPORTED = 5;
22
+ /** What a dropped full claim is called in notices and refusals. */
23
+ export const FULL_CLAIM_LABEL = 'full coverage';
24
+ const UNVERIFIABLE = 'engine HEAD or the recorded anchor cannot be compared';
25
+ const NO_FINGERPRINTS = 'the record has no complete staging-input fingerprints';
26
+ /** The baseline-wide anchor a baseline's own fields describe, when complete. */
27
+ export function baselineAnchor(baseline) {
28
+ if (baseline.testInputFingerprints === undefined || baseline.mozconfigHash === undefined) {
29
+ return undefined;
30
+ }
31
+ return {
32
+ engineHeadSha: baseline.engineHeadSha,
33
+ mozconfigHash: baseline.mozconfigHash,
34
+ fingerprints: baseline.testInputFingerprints,
35
+ };
36
+ }
37
+ function emptyPlan() {
38
+ return { keepFull: false, retained: {}, fullAnchor: undefined, narrowedBy: [], dropped: [] };
39
+ }
40
+ /** True when a request path in `requested` stages `path` afresh. */
41
+ function rebuiltBy(path, rebuilt) {
42
+ const normalized = normalizeRequestPath(path);
43
+ return rebuilt.some((request) => isUnderPath(normalized, request));
44
+ }
45
+ async function planFullClaim(state, previous, requested) {
46
+ const plan = emptyPlan();
47
+ const anchor = previous.fullCoverageAnchor ?? baselineAnchor(previous);
48
+ if (anchor === undefined) {
49
+ plan.dropped.push({ path: FULL_CLAIM_LABEL, changed: [], reason: NO_FINGERPRINTS });
50
+ return plan;
51
+ }
52
+ const changed = await findChangedInputs(state, anchor, undefined, requested);
53
+ const refreshed = changed === undefined ? undefined : await refreshAnchor(state, anchor, requested);
54
+ if (changed === undefined || refreshed === undefined) {
55
+ plan.dropped.push({ path: FULL_CLAIM_LABEL, changed: [], reason: UNVERIFIABLE });
56
+ return plan;
57
+ }
58
+ plan.keepFull = changed.length === 0;
59
+ plan.fullAnchor = refreshed;
60
+ plan.narrowedBy = changed;
61
+ return plan;
62
+ }
63
+ async function retainRecord(state, previous, path, requested) {
64
+ let record = previous.testCoverageRecords?.[path];
65
+ if (record === undefined) {
66
+ // A path recorded before per-path anchors existed: the baseline-wide
67
+ // anchor, restricted to the path's own inputs, says the same thing.
68
+ const anchor = baselineAnchor(previous);
69
+ if (anchor === undefined)
70
+ return { path, changed: [], reason: NO_FINGERPRINTS };
71
+ const inputs = await collectStagingInputs(state.engineDir, path);
72
+ record = await narrowAnchor(state, anchor, inputs);
73
+ if (record === undefined)
74
+ return { path, changed: [], reason: UNVERIFIABLE };
75
+ }
76
+ const changed = await findChangedInputs(state, record, record.inputs, requested);
77
+ if (changed === undefined)
78
+ return { path, changed: [], reason: UNVERIFIABLE };
79
+ if (changed.length > 0)
80
+ return { path, changed };
81
+ const refreshed = await refreshAnchor(state, record, requested);
82
+ return refreshed === undefined
83
+ ? { path, changed: [], reason: UNVERIFIABLE }
84
+ : { record: refreshed };
85
+ }
21
86
  /**
22
- * Verifies the parts of the anchor knowable before the build runs: a usable
23
- * previous baseline, an unchanged engine HEAD, and byte-identical content
24
- * for every path that baseline fingerprinted.
87
+ * Plans which earlier claims a scoped build over `requested` keeps. Runs
88
+ * before the build lock, so a refusal costs neither build time nor the
89
+ * lock. The mozconfig half runs later, via {@link applyMozconfigAnchor}.
90
+ *
25
91
  * @param engineDir - Path to the engine directory
26
- * @param previousBaseline - Baseline recorded by the previous build
92
+ * @param previous - Baseline recorded by the previous build
93
+ * @param requested - Normalized request paths this build packages
27
94
  */
28
- export async function checkExtendCoverageAnchor(engineDir, previousBaseline, rebuiltPaths = []) {
29
- if (previousBaseline === undefined) {
30
- return { ok: false, reason: 'no-baseline', detail: [] };
31
- }
32
- if (previousBaseline.mozconfigHash === undefined) {
33
- return { ok: false, reason: 'no-mozconfig-hash', detail: [] };
34
- }
35
- const recordedFingerprints = previousBaseline.testInputFingerprints;
36
- const rebuilt = (file) => rebuiltPaths.some((path) => file === path || file.startsWith(path.replace(/\/$/, '') + '/'));
37
- if (recordedFingerprints === undefined) {
38
- return { ok: false, reason: 'no-fingerprints', detail: [] };
39
- }
40
- const currentHead = await readEngineHead(engineDir);
41
- if (currentHead === undefined || previousBaseline.engineHeadSha === '') {
42
- // An unborn or unreadable HEAD gives nothing to anchor to.
43
- return { ok: false, reason: 'head-moved', detail: [] };
95
+ export async function planCoverageRetention(engineDir, previous, requested) {
96
+ if (previous === undefined)
97
+ return emptyPlan();
98
+ const coverage = previous.testPackagingCoverage ?? 'full';
99
+ const rebuilt = requested.map(normalizeRequestPath).filter((path) => path !== '');
100
+ let state;
101
+ try {
102
+ state = await readEngineContentState(engineDir);
44
103
  }
45
- if (currentHead !== previousBaseline.engineHeadSha) {
104
+ catch (error) {
105
+ verbose(`Coverage retention: engine state unreadable — ${toError(error).message}`);
106
+ const paths = coverage === 'full' ? [FULL_CLAIM_LABEL] : coverage;
46
107
  return {
47
- ok: false,
48
- reason: 'head-moved',
49
- detail: [`recorded ${previousBaseline.engineHeadSha}`, `current ${currentHead}`],
108
+ ...emptyPlan(),
109
+ dropped: paths.map((path) => ({ path, changed: [], reason: UNVERIFIABLE })),
50
110
  };
51
111
  }
52
- const diverged = [];
53
- for (const [relPath, recordedHash] of Object.entries(recordedFingerprints)) {
54
- if (rebuilt(relPath))
55
- continue;
56
- const liveHash = await hashEngineFile(engineDir, relPath);
57
- if (liveHash !== recordedHash)
58
- diverged.push(relPath);
59
- }
60
- // New dirty test inputs are absent from the previous fingerprint map.
61
- // They invalidate retained staging too; explicitly rebuilt inputs are safe.
62
- if (previousBaseline.testInputFingerprints !== undefined) {
63
- const changed = (await git(['diff', '--name-only', 'HEAD'], engineDir)).split('\n');
64
- changed.push(...(await getUntrackedFiles(engineDir)));
65
- for (const file of changed.filter(Boolean)) {
66
- if (file === MOZCONFIG_FILENAME || rebuilt(file) || Object.hasOwn(recordedFingerprints, file))
67
- continue;
68
- // A retained manifest may stage a shared fixture outside test-named
69
- // directories. Without its full dependency graph, refuse conservatively.
70
- diverged.push(file);
112
+ if (coverage === 'full')
113
+ return planFullClaim(state, previous, rebuilt);
114
+ const plan = emptyPlan();
115
+ if (previous.fullCoverageAnchor !== undefined) {
116
+ plan.fullAnchor = await refreshAnchor(state, previous.fullCoverageAnchor, rebuilt);
117
+ if (plan.fullAnchor === undefined) {
118
+ plan.dropped.push({ path: FULL_CLAIM_LABEL, changed: [], reason: UNVERIFIABLE });
71
119
  }
72
120
  }
73
- if (diverged.length > 0) {
74
- return { ok: false, reason: 'fingerprint-diverged', detail: diverged.sort() };
121
+ for (const path of coverage) {
122
+ if (rebuiltBy(path, rebuilt))
123
+ continue;
124
+ const outcome = await retainRecord(state, previous, path, rebuilt);
125
+ if ('record' in outcome)
126
+ plan.retained[path] = outcome.record;
127
+ else
128
+ plan.dropped.push(outcome);
75
129
  }
76
- return { ok: true };
130
+ return plan;
77
131
  }
78
132
  /**
79
- * Verifies the mozconfig half of the anchor. Split from
80
- * {@link checkExtendCoverageAnchor} because it can only run after
81
- * `prepareBuildEnvironment` regenerates `engine/mozconfig` (the file this
82
- * build will configure with), and must still run before mach, so a refusal
83
- * costs no build time.
133
+ * Applies the mozconfig half of the anchor: a claim staged under a different
134
+ * `engine/mozconfig` is dropped. Runs after `prepareBuildEnvironment`
135
+ * regenerated the file this build configures with, and before mach.
136
+ *
84
137
  * @param engineDir - Path to the engine directory
85
- * @param previousBaseline - Baseline recorded by the previous build
138
+ * @param plan - Plan from {@link planCoverageRetention}
86
139
  */
87
- export async function checkExtendMozconfigAnchor(engineDir, previousBaseline) {
88
- const recorded = previousBaseline?.mozconfigHash;
89
- if (recorded === undefined) {
90
- return { ok: false, reason: 'no-mozconfig-hash', detail: [] };
91
- }
140
+ export async function applyMozconfigAnchor(engineDir, plan) {
92
141
  const live = await hashEngineFile(engineDir, MOZCONFIG_FILENAME);
93
- if (live === undefined || live !== recorded) {
94
- return { ok: false, reason: 'mozconfig-changed', detail: [] };
95
- }
96
- return { ok: true };
142
+ const reason = 'engine/mozconfig differs from the build that staged it';
143
+ const next = { ...plan, retained: {}, dropped: [...plan.dropped] };
144
+ for (const [path, record] of Object.entries(plan.retained)) {
145
+ if (live !== undefined && record.mozconfigHash === live)
146
+ next.retained[path] = record;
147
+ else
148
+ next.dropped.push({ path, changed: [], reason });
149
+ }
150
+ if (plan.fullAnchor !== undefined &&
151
+ (live === undefined || plan.fullAnchor.mozconfigHash !== live)) {
152
+ next.fullAnchor = undefined;
153
+ next.keepFull = false;
154
+ next.narrowedBy = [];
155
+ next.dropped.push({ path: FULL_CLAIM_LABEL, changed: [], reason });
156
+ }
157
+ return next;
97
158
  }
98
159
  /**
99
- * Unions the newly built paths onto the recorded claim. A `'full'` claim, or
100
- * an absent one (which means full), stays `'full'`, since it already covers
101
- * everything.
160
+ * The coverage a green scoped build records: `'full'` when the plan kept a
161
+ * full claim, otherwise this build's paths with fresh records plus every
162
+ * retained earlier path, and the carried full anchor.
102
163
  *
103
- * @param previous - Coverage recorded by the previous build
164
+ * @param engineDir - Path to the engine directory
165
+ * @param plan - Plan after {@link applyMozconfigAnchor}
104
166
  * @param requested - Normalized request paths this build packaged
105
167
  */
106
- export function unionTestPackagingCoverage(previous, requested) {
107
- if (previous === undefined || previous === 'full') {
108
- return 'full';
109
- }
110
- return [...new Set([...previous, ...requested])].sort();
111
- }
112
- /**
113
- * Renders the fail-closed refusal for an anchor check that did not pass.
114
- * Kept separate from the probes so tests can assert the structured result
115
- * without matching copy.
116
- * @param result - Failing result from an anchor check
117
- */
118
- export function formatExtendCoverageRefusal(result) {
119
- const remedy = 'Re-run without --extend-coverage: a plain scoped build REPLACES the coverage claim ' +
120
- 'with the paths it actually vouches for.';
121
- switch (result.reason) {
122
- case 'no-baseline':
123
- return `--extend-coverage has nothing to extend: no previous build baseline is recorded. ${remedy}`;
124
- case 'no-mozconfig-hash':
125
- return ('--extend-coverage refused: the recorded baseline predates the mozconfig anchor, so it ' +
126
- `cannot be proven that this build configures identically. ${remedy}`);
127
- case 'no-fingerprints':
128
- return ('--extend-coverage refused: the recorded baseline carries no complete test-input fingerprints, so ' +
129
- `the earlier scope's staging inputs cannot be verified unchanged. ${remedy}`);
130
- case 'head-moved':
131
- return ('--extend-coverage refused: engine HEAD moved since the recorded build ' +
132
- `(${result.detail.join(', ') || 'no usable HEAD'}), so the earlier scope's packaging is no ` +
133
- `longer anchored. ${remedy}`);
134
- case 'fingerprint-diverged':
135
- return ('--extend-coverage refused: ' +
136
- `${result.detail.length} staging input(s) the recorded build staged have changed ` +
137
- `since (${formatDivergedPaths(result.detail)}). Extending would vouch for the earlier scope ` +
138
- `while obj-*/_tests/ still holds its stale staging. ${remedy}`);
139
- case 'mozconfig-changed':
140
- return ('--extend-coverage refused: engine/mozconfig differs from the recorded build (it is ' +
141
- 'regenerated from configs/*.mozconfig and fireforge.json on every build, so engine HEAD ' +
142
- `does not cover it). ${remedy}`);
168
+ export async function recordScopedCoverage(engineDir, plan, requested) {
169
+ if (plan.keepFull)
170
+ return { testPackagingCoverage: 'full' };
171
+ const records = { ...plan.retained };
172
+ try {
173
+ const state = await readEngineContentState(engineDir);
174
+ const mozconfigHash = await hashEngineFile(engineDir, MOZCONFIG_FILENAME);
175
+ for (const path of requested) {
176
+ const record = mozconfigHash === undefined
177
+ ? undefined
178
+ : await createCoverageRecord(state, path, mozconfigHash);
179
+ if (record !== undefined)
180
+ records[path] = record;
181
+ }
143
182
  }
183
+ catch (error) {
184
+ verbose(`Coverage records: could not anchor this build's paths — ${toError(error).message}`);
185
+ }
186
+ const kept = Object.keys(plan.retained);
187
+ const coverage = kept.length === 0 ? [...requested] : [...new Set([...kept, ...requested])].sort();
188
+ return {
189
+ testPackagingCoverage: coverage,
190
+ ...(Object.keys(records).length > 0 ? { testCoverageRecords: records } : {}),
191
+ ...(plan.fullAnchor !== undefined ? { fullCoverageAnchor: plan.fullAnchor } : {}),
192
+ };
144
193
  }
145
- function formatDivergedPaths(paths) {
146
- const head = paths.slice(0, MAX_DIVERGED_REPORTED).join(', ');
147
- const extra = paths.length - Math.min(paths.length, MAX_DIVERGED_REPORTED);
194
+ function capList(paths) {
195
+ const head = paths.slice(0, MAX_PATHS_REPORTED).join(', ');
196
+ const extra = paths.length - Math.min(paths.length, MAX_PATHS_REPORTED);
148
197
  return extra > 0 ? `${head}, … (+${extra} more)` : head;
149
198
  }
199
+ /** Renders one dropped claim with its cause. */
200
+ export function describeDroppedClaim(claim) {
201
+ const cause = claim.changed.length > 0
202
+ ? `staging inputs changed: ${capList(claim.changed)}`
203
+ : (claim.reason ?? UNVERIFIABLE);
204
+ return `${claim.path} (${cause})`;
205
+ }
150
206
  /**
151
- * Hex-encoded SHA-256 of an engine-relative file, matching the digest the
152
- * baseline writer produces. `undefined` on any IO error, which every caller
153
- * treats as "cannot prove unchanged", i.e. a refusal.
154
- * @param engineDir - Path to the engine directory
155
- * @param relPath - Engine-relative POSIX path
207
+ * The notice a scoped build prints about earlier coverage, or undefined
208
+ * when nothing was dropped or narrowed.
156
209
  */
157
- export async function hashEngineFile(engineDir, relPath) {
158
- try {
159
- const buffer = await readFile(join(engineDir, relPath));
160
- return sha256Hex(buffer);
161
- }
162
- catch (error) {
163
- if (getNodeErrorCode(error) === 'ENOENT') {
164
- return DELETED_FILE_FINGERPRINT;
165
- }
166
- verbose(`Extend-coverage anchor: could not hash ${relPath} — ${toError(error).message}`);
167
- return undefined;
168
- }
210
+ export function formatCoverageRetentionNotice(plan) {
211
+ const lines = [];
212
+ if (plan.dropped.length > 0) {
213
+ const kept = Object.keys(plan.retained);
214
+ lines.push(`Scoped build drops previous packaging coverage for ${plan.dropped.map(describeDroppedClaim).join('; ')}.` +
215
+ (kept.length > 0 ? ` Kept: ${capList(kept)}.` : '') +
216
+ ' Rebuild a dropped path (alone or with this one) before running it without --build.');
217
+ }
218
+ if (!plan.keepFull && plan.fullAnchor !== undefined && plan.narrowedBy.length > 0) {
219
+ lines.push(`Full packaging coverage is now checked per path: ${capList(plan.narrowedBy)} changed outside ` +
220
+ "this build's scope, so a later run is refused only for paths whose own staging inputs include a changed file.");
221
+ }
222
+ return lines.length > 0 ? lines.join('\n') : undefined;
169
223
  }
170
- async function readEngineHead(engineDir) {
171
- try {
172
- return await getHead(engineDir);
173
- }
174
- catch (error) {
175
- if (isMissingHeadError(error))
176
- return undefined;
177
- throw error;
178
- }
224
+ /**
225
+ * The `--extend-coverage` refusal: the flag asks that no earlier claim be
226
+ * dropped, and the plan would drop some.
227
+ */
228
+ export function formatExtendCoverageRefusal(dropped) {
229
+ return (`--extend-coverage refused: this build would drop earlier packaging coverage for ` +
230
+ `${dropped.length} claim(s): ${dropped.map(describeDroppedClaim).join('; ')}. ` +
231
+ 'Extending would vouch for staging that obj-*/_tests/ no longer matches. Rebuild those paths ' +
232
+ 'together with this one, or drop the flag: without it the dropped claims are named and the ' +
233
+ 'rest of the coverage is kept.');
179
234
  }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Per-path packaging-coverage anchors for scoped `fireforge test --build`.
3
+ *
4
+ * A scoped build used to carry one claim anchored to the whole engine, so
5
+ * any edit outside the new scope (a peer's, on a shared engine, nearly
6
+ * always) replaced it. Each staged request path now carries its own anchor
7
+ * restricted to its own staging inputs, and a claim is dropped only when
8
+ * one of those inputs changed.
9
+ *
10
+ * The staging inputs of a request path are its manifest directory (the
11
+ * nearest directory at or above it that holds a test manifest) plus every
12
+ * path a manifest there names outside that directory (`support-files`,
13
+ * `head`, `!/`-rooted shared fixtures). Without the full mozbuild graph
14
+ * that is the closest sound boundary: an edit outside it cannot change
15
+ * what the harness stages for that path.
16
+ */
17
+ import { type CoverageAnchor, type CoverageRecord } from './build-baseline-types.js';
18
+ /**
19
+ * Hex-encoded SHA-256 of an engine-relative file, matching the digest the
20
+ * baseline writer produces. A missing file hashes to
21
+ * {@link DELETED_FILE_FINGERPRINT}. `undefined` on any other IO error,
22
+ * which every caller treats as "cannot prove unchanged".
23
+ * @param engineDir - Path to the engine directory
24
+ * @param relPath - Engine-relative POSIX path
25
+ */
26
+ export declare function hashEngineFile(engineDir: string, relPath: string): Promise<string | undefined>;
27
+ /** True for a test manifest path (by basename). */
28
+ export declare function isTestManifestPath(path: string): boolean;
29
+ /** Normalizes a request path: forward slashes, no `./`, no trailing slash. */
30
+ export declare function normalizeRequestPath(path: string): string;
31
+ /** True when `file` is `prefix` or sits beneath it. The empty prefix is the whole tree. */
32
+ export declare function isUnderPath(file: string, prefix: string): boolean;
33
+ /**
34
+ * Engine state one coverage decision reads. Shared across every anchor a
35
+ * decision verifies, so the git probes run once and each file is hashed
36
+ * once.
37
+ */
38
+ export interface EngineContentState {
39
+ engineDir: string;
40
+ /** Current engine HEAD, or undefined for an unborn branch. */
41
+ head: string | undefined;
42
+ /** Engine-relative paths dirty against HEAD (tracked and untracked). */
43
+ dirty: ReadonlySet<string>;
44
+ hashes: Map<string, Promise<string | undefined>>;
45
+ diffs: Map<string, Promise<ReadonlySet<string> | undefined>>;
46
+ }
47
+ /**
48
+ * Reads HEAD and the dirty set. Throws on a git failure other than an
49
+ * unborn HEAD: callers treat that as "nothing can be proven unchanged".
50
+ * @param engineDir - Path to the engine directory
51
+ */
52
+ export declare function readEngineContentState(engineDir: string): Promise<EngineContentState>;
53
+ /**
54
+ * Inputs whose live content differs from what the anchor staged, sorted.
55
+ * Undefined when the anchor cannot be compared, which every caller treats as
56
+ * "changed".
57
+ *
58
+ * @param state - Engine state from {@link readEngineContentState}
59
+ * @param anchor - The claim's anchor
60
+ * @param inputs - Staging inputs to compare, or undefined for the whole tree
61
+ * @param rebuilt - Request paths the current build stages afresh
62
+ */
63
+ export declare function findChangedInputs(state: EngineContentState, anchor: CoverageAnchor, inputs: readonly string[] | undefined, rebuilt?: readonly string[]): Promise<string[] | undefined>;
64
+ /**
65
+ * The anchor after the current build restaged `rebuilt`: recorded content
66
+ * for those paths becomes their live content. Undefined when that content
67
+ * cannot be read or the anchor cannot be compared.
68
+ */
69
+ export declare function refreshAnchor<T extends CoverageAnchor>(state: EngineContentState, anchor: T, rebuilt: readonly string[]): Promise<T | undefined>;
70
+ /**
71
+ * Engine-relative paths a manifest names outside its own directory. Bare
72
+ * names stay inside it. `!/` and `/` prefixes are engine-root relative, as
73
+ * in mozbuild manifests. A glob keeps the directory part before its first
74
+ * wildcard segment.
75
+ */
76
+ export declare function manifestExternalReferences(manifestPath: string, text: string): string[];
77
+ /**
78
+ * Staging inputs of one request path. See the module comment.
79
+ * @param engineDir - Path to the engine directory
80
+ * @param requestPath - Engine-relative request path (file or directory)
81
+ */
82
+ export declare function collectStagingInputs(engineDir: string, requestPath: string): Promise<string[]>;
83
+ /**
84
+ * Records the anchor of a request path this build staged. Undefined when
85
+ * HEAD is unborn or an input's content cannot be read: the path is still
86
+ * claimed, and later retention falls back to the baseline-wide anchor.
87
+ *
88
+ * @param state - Engine state after the build
89
+ * @param requestPath - Engine-relative request path
90
+ * @param mozconfigHash - Hash of the mozconfig the build configured with
91
+ */
92
+ export declare function createCoverageRecord(state: EngineContentState, requestPath: string, mozconfigHash: string): Promise<CoverageRecord | undefined>;
93
+ /**
94
+ * Restricts an anchor to `inputs`. `extraDirty` adds paths whose live content
95
+ * becomes the recorded one (the dirty set of a record being created).
96
+ */
97
+ export declare function narrowAnchor(state: EngineContentState, anchor: CoverageAnchor, inputs: string[], extraDirty?: readonly string[]): Promise<CoverageRecord | undefined>;