@hominis/fireforge 0.39.0 → 0.41.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 (279) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +35 -5
  3. package/dist/bin/fireforge.js +25 -4
  4. package/dist/build-info.json +9 -0
  5. package/dist/src/cli.js +27 -3
  6. package/dist/src/commands/bootstrap.js +5 -1
  7. package/dist/src/commands/discard.js +2 -22
  8. package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
  9. package/dist/src/commands/doctor-check-core.d.ts +20 -0
  10. package/dist/src/commands/doctor-check-core.js +28 -0
  11. package/dist/src/commands/doctor-external-toolchains.js +2 -0
  12. package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
  13. package/dist/src/commands/doctor-furnace.js +13 -11
  14. package/dist/src/commands/doctor-orphaned-harness.js +2 -1
  15. package/dist/src/commands/doctor.js +2 -2
  16. package/dist/src/commands/export-all.js +9 -1
  17. package/dist/src/commands/export-flow.js +27 -9
  18. package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
  19. package/dist/src/commands/export-placement-conflicts.js +74 -0
  20. package/dist/src/commands/export-placement-gate.js +1 -1
  21. package/dist/src/commands/export-shared.d.ts +8 -1
  22. package/dist/src/commands/export-shared.js +30 -10
  23. package/dist/src/commands/export.js +2 -2
  24. package/dist/src/commands/furnace/apply.js +4 -3
  25. package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
  26. package/dist/src/commands/furnace/chrome-doc.js +13 -2
  27. package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
  28. package/dist/src/commands/furnace/create-browser-test.js +5 -0
  29. package/dist/src/commands/furnace/create.js +2 -2
  30. package/dist/src/commands/furnace/diff.js +2 -1
  31. package/dist/src/commands/furnace/init.js +4 -6
  32. package/dist/src/commands/furnace/override.js +2 -2
  33. package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
  34. package/dist/src/commands/furnace/remove-mutations.js +171 -0
  35. package/dist/src/commands/furnace/remove.js +21 -128
  36. package/dist/src/commands/furnace/rename-browser-test.js +54 -5
  37. package/dist/src/commands/furnace/scan.js +27 -36
  38. package/dist/src/commands/furnace/status.js +30 -28
  39. package/dist/src/commands/furnace/validate.js +25 -18
  40. package/dist/src/commands/import.js +6 -20
  41. package/dist/src/commands/lint-per-patch.js +73 -51
  42. package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
  43. package/dist/src/commands/lint-per-run-checkjs.js +102 -0
  44. package/dist/src/commands/lint-report.d.ts +21 -0
  45. package/dist/src/commands/lint-report.js +62 -0
  46. package/dist/src/commands/lint.js +26 -22
  47. package/dist/src/commands/manifest.js +3 -1
  48. package/dist/src/commands/package.js +9 -16
  49. package/dist/src/commands/patch/compact.js +5 -5
  50. package/dist/src/commands/patch/delete.js +5 -5
  51. package/dist/src/commands/patch/lint-ignore.js +3 -2
  52. package/dist/src/commands/patch/move-files-create.js +22 -4
  53. package/dist/src/commands/patch/move-files-into.js +14 -10
  54. package/dist/src/commands/patch/move-files.js +6 -5
  55. package/dist/src/commands/patch/rename.d.ts +2 -2
  56. package/dist/src/commands/patch/rename.js +152 -51
  57. package/dist/src/commands/patch/reorder.d.ts +9 -0
  58. package/dist/src/commands/patch/reorder.js +14 -10
  59. package/dist/src/commands/patch/split-plan.d.ts +13 -6
  60. package/dist/src/commands/patch/split-plan.js +22 -12
  61. package/dist/src/commands/patch/split.d.ts +1 -1
  62. package/dist/src/commands/patch/split.js +23 -9
  63. package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
  64. package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
  65. package/dist/src/commands/patch/staged-dependency.js +112 -14
  66. package/dist/src/commands/re-export-adjacent.d.ts +34 -0
  67. package/dist/src/commands/re-export-adjacent.js +105 -0
  68. package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
  69. package/dist/src/commands/re-export-bulk-scan.js +142 -2
  70. package/dist/src/commands/re-export-drift.d.ts +74 -0
  71. package/dist/src/commands/re-export-drift.js +234 -0
  72. package/dist/src/commands/re-export-files.js +5 -3
  73. package/dist/src/commands/re-export-lint.d.ts +97 -0
  74. package/dist/src/commands/re-export-lint.js +210 -0
  75. package/dist/src/commands/re-export-options.js +11 -0
  76. package/dist/src/commands/re-export-register.d.ts +18 -0
  77. package/dist/src/commands/re-export-register.js +61 -0
  78. package/dist/src/commands/re-export-scan.js +13 -7
  79. package/dist/src/commands/re-export-single.d.ts +20 -0
  80. package/dist/src/commands/re-export-single.js +261 -0
  81. package/dist/src/commands/re-export.d.ts +0 -4
  82. package/dist/src/commands/re-export.js +123 -289
  83. package/dist/src/commands/rebase/abort.js +13 -4
  84. package/dist/src/commands/rebase/continue.js +11 -4
  85. package/dist/src/commands/rebase/index.js +19 -29
  86. package/dist/src/commands/rebase/patch-loop.js +12 -0
  87. package/dist/src/commands/reset.js +4 -23
  88. package/dist/src/commands/resolve.js +2 -9
  89. package/dist/src/commands/run.js +29 -23
  90. package/dist/src/commands/source.js +3 -9
  91. package/dist/src/commands/status-check.d.ts +48 -0
  92. package/dist/src/commands/status-check.js +106 -0
  93. package/dist/src/commands/status-json.d.ts +31 -0
  94. package/dist/src/commands/status-json.js +76 -0
  95. package/dist/src/commands/status-output.d.ts +1 -0
  96. package/dist/src/commands/status-output.js +10 -1
  97. package/dist/src/commands/status-ownership.d.ts +52 -0
  98. package/dist/src/commands/status-ownership.js +57 -0
  99. package/dist/src/commands/status.js +190 -170
  100. package/dist/src/commands/test-build-phase.d.ts +19 -0
  101. package/dist/src/commands/test-build-phase.js +168 -0
  102. package/dist/src/commands/test-diagnose.js +30 -2
  103. package/dist/src/commands/test-modes.d.ts +9 -1
  104. package/dist/src/commands/test-modes.js +46 -13
  105. package/dist/src/commands/test-register.js +59 -27
  106. package/dist/src/commands/test-run.d.ts +29 -3
  107. package/dist/src/commands/test-run.js +59 -7
  108. package/dist/src/commands/test-verdict.d.ts +32 -0
  109. package/dist/src/commands/test-verdict.js +48 -0
  110. package/dist/src/commands/test.d.ts +9 -0
  111. package/dist/src/commands/test.js +100 -123
  112. package/dist/src/commands/token-coverage.js +4 -9
  113. package/dist/src/commands/token.js +4 -1
  114. package/dist/src/commands/tree.d.ts +19 -0
  115. package/dist/src/commands/tree.js +320 -0
  116. package/dist/src/commands/verify.js +2 -2
  117. package/dist/src/commands/watch.js +22 -21
  118. package/dist/src/commands/wire.js +1 -1
  119. package/dist/src/core/ast-utils.d.ts +11 -0
  120. package/dist/src/core/ast-utils.js +11 -0
  121. package/dist/src/core/build-audit-platform.js +6 -1
  122. package/dist/src/core/build-audit-registration.js +2 -0
  123. package/dist/src/core/build-audit-resolve.d.ts +13 -0
  124. package/dist/src/core/build-audit-resolve.js +34 -13
  125. package/dist/src/core/build-audit.js +4 -20
  126. package/dist/src/core/build-baseline-types.d.ts +29 -8
  127. package/dist/src/core/build-baseline-types.js +2 -1
  128. package/dist/src/core/build-baseline.d.ts +10 -2
  129. package/dist/src/core/build-baseline.js +39 -8
  130. package/dist/src/core/build-prepare.d.ts +4 -0
  131. package/dist/src/core/build-prepare.js +7 -1
  132. package/dist/src/core/config-paths.d.ts +1 -1
  133. package/dist/src/core/config-paths.js +2 -0
  134. package/dist/src/core/config-validate-test-toolchains.js +3 -2
  135. package/dist/src/core/config-validate.js +45 -30
  136. package/dist/src/core/config.js +16 -3
  137. package/dist/src/core/coverage-extend.d.ts +89 -0
  138. package/dist/src/core/coverage-extend.js +188 -0
  139. package/dist/src/core/destructive.js +3 -1
  140. package/dist/src/core/discard-baseline.js +18 -4
  141. package/dist/src/core/engine-precondition.d.ts +35 -0
  142. package/dist/src/core/engine-precondition.js +50 -0
  143. package/dist/src/core/engine-session-lock.d.ts +38 -1
  144. package/dist/src/core/engine-session-lock.js +78 -4
  145. package/dist/src/core/file-lock.d.ts +7 -0
  146. package/dist/src/core/file-lock.js +20 -30
  147. package/dist/src/core/firefox.js +5 -2
  148. package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
  149. package/dist/src/core/furnace-apply-helpers.js +12 -3
  150. package/dist/src/core/furnace-apply-output.js +9 -0
  151. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
  152. package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
  153. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
  154. package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
  155. package/dist/src/core/furnace-apply.d.ts +16 -4
  156. package/dist/src/core/furnace-apply.js +39 -43
  157. package/dist/src/core/furnace-config.js +2 -0
  158. package/dist/src/core/furnace-css-fragments.js +2 -0
  159. package/dist/src/core/furnace-jsconfig.js +14 -7
  160. package/dist/src/core/furnace-registration-ast.js +8 -8
  161. package/dist/src/core/furnace-registration-remove.js +6 -3
  162. package/dist/src/core/furnace-registration.js +9 -12
  163. package/dist/src/core/furnace-rollback.js +1 -1
  164. package/dist/src/core/furnace-scanner.js +2 -1
  165. package/dist/src/core/furnace-stale-export.js +2 -1
  166. package/dist/src/core/furnace-state-persist.d.ts +4 -6
  167. package/dist/src/core/furnace-validate-compatibility.js +4 -7
  168. package/dist/src/core/furnace-validate-helpers.js +33 -1
  169. package/dist/src/core/furnace-validate-registration.js +16 -0
  170. package/dist/src/core/furnace-validate.js +4 -2
  171. package/dist/src/core/git-diff.js +37 -2
  172. package/dist/src/core/lint-cache.d.ts +15 -5
  173. package/dist/src/core/lint-cache.js +49 -14
  174. package/dist/src/core/mach-build-artifacts.d.ts +63 -1
  175. package/dist/src/core/mach-build-artifacts.js +108 -2
  176. package/dist/src/core/mach-resource-shim.js +1 -0
  177. package/dist/src/core/marionette-port.d.ts +16 -0
  178. package/dist/src/core/marionette-port.js +93 -8
  179. package/dist/src/core/marionette-preflight.js +6 -5
  180. package/dist/src/core/patch-apply.d.ts +20 -13
  181. package/dist/src/core/patch-apply.js +32 -33
  182. package/dist/src/core/patch-export.d.ts +2 -2
  183. package/dist/src/core/patch-export.js +4 -6
  184. package/dist/src/core/patch-lint-checkjs.d.ts +42 -2
  185. package/dist/src/core/patch-lint-checkjs.js +132 -8
  186. package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
  187. package/dist/src/core/patch-lint-creators.d.ts +30 -0
  188. package/dist/src/core/patch-lint-creators.js +45 -0
  189. package/dist/src/core/patch-lint-cross.d.ts +1 -17
  190. package/dist/src/core/patch-lint-cross.js +16 -32
  191. package/dist/src/core/patch-lint-css.js +64 -16
  192. package/dist/src/core/patch-lint-diff.d.ts +8 -0
  193. package/dist/src/core/patch-lint-diff.js +31 -0
  194. package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
  195. package/dist/src/core/patch-lint-jsdoc.js +15 -5
  196. package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
  197. package/dist/src/core/patch-lint-module-registration.js +88 -0
  198. package/dist/src/core/patch-lint-mozbuild.js +99 -11
  199. package/dist/src/core/patch-lint-ownership.d.ts +21 -0
  200. package/dist/src/core/patch-lint-ownership.js +30 -0
  201. package/dist/src/core/patch-lint-reexports.d.ts +2 -1
  202. package/dist/src/core/patch-lint-reexports.js +1 -0
  203. package/dist/src/core/patch-lint.d.ts +31 -0
  204. package/dist/src/core/patch-lint.js +114 -8
  205. package/dist/src/core/patch-lock.d.ts +12 -1
  206. package/dist/src/core/patch-lock.js +40 -4
  207. package/dist/src/core/patch-manifest-consistency.js +15 -3
  208. package/dist/src/core/patch-manifest.d.ts +1 -1
  209. package/dist/src/core/patch-parse.d.ts +8 -0
  210. package/dist/src/core/patch-parse.js +10 -0
  211. package/dist/src/core/patch-policy.d.ts +7 -0
  212. package/dist/src/core/patch-policy.js +4 -1
  213. package/dist/src/core/patch-transform.d.ts +10 -0
  214. package/dist/src/core/patch-transform.js +13 -2
  215. package/dist/src/core/rebase-session.d.ts +44 -2
  216. package/dist/src/core/rebase-session.js +114 -17
  217. package/dist/src/core/register-browser-content.js +2 -1
  218. package/dist/src/core/register-shared-css.js +2 -1
  219. package/dist/src/core/smoke-patterns.d.ts +24 -16
  220. package/dist/src/core/smoke-patterns.js +26 -14
  221. package/dist/src/core/status-binary.d.ts +55 -0
  222. package/dist/src/core/status-binary.js +124 -0
  223. package/dist/src/core/status-classify.d.ts +22 -1
  224. package/dist/src/core/status-classify.js +112 -70
  225. package/dist/src/core/test-harness-crash.d.ts +32 -1
  226. package/dist/src/core/test-harness-crash.js +59 -4
  227. package/dist/src/core/test-path-scope.js +2 -0
  228. package/dist/src/core/test-stale-check.js +1 -20
  229. package/dist/src/core/token-category.d.ts +40 -0
  230. package/dist/src/core/token-category.js +264 -0
  231. package/dist/src/core/token-manager.d.ts +7 -0
  232. package/dist/src/core/token-manager.js +48 -196
  233. package/dist/src/core/toolchain-preflight.js +24 -26
  234. package/dist/src/core/tree-cow.d.ts +18 -0
  235. package/dist/src/core/tree-cow.js +71 -0
  236. package/dist/src/core/tree-guard.d.ts +35 -0
  237. package/dist/src/core/tree-guard.js +179 -0
  238. package/dist/src/core/tree-store.d.ts +129 -0
  239. package/dist/src/core/tree-store.js +452 -0
  240. package/dist/src/core/typecheck-shim.d.ts +14 -0
  241. package/dist/src/core/typecheck-shim.js +41 -2
  242. package/dist/src/core/typecheck.js +2 -1
  243. package/dist/src/core/wire-destroy.js +14 -3
  244. package/dist/src/core/wire-init.js +37 -40
  245. package/dist/src/core/wire-subscript.js +11 -3
  246. package/dist/src/core/wire-utils.js +4 -4
  247. package/dist/src/core/xpcshell-appdir.js +3 -0
  248. package/dist/src/core/xpcshell-profile-dir.d.ts +12 -0
  249. package/dist/src/core/xpcshell-profile-dir.js +45 -0
  250. package/dist/src/errors/base.d.ts +13 -0
  251. package/dist/src/errors/base.js +13 -0
  252. package/dist/src/errors/git.d.ts +8 -0
  253. package/dist/src/errors/git.js +17 -0
  254. package/dist/src/errors/rebase.d.ts +13 -0
  255. package/dist/src/errors/rebase.js +17 -0
  256. package/dist/src/index.d.ts +2 -0
  257. package/dist/src/index.js +1 -0
  258. package/dist/src/types/commands/options.d.ts +119 -3
  259. package/dist/src/types/commands/patches.d.ts +8 -0
  260. package/dist/src/types/config.d.ts +19 -0
  261. package/dist/src/types/furnace.d.ts +7 -0
  262. package/dist/src/utils/build-info.d.ts +31 -0
  263. package/dist/src/utils/build-info.js +98 -0
  264. package/dist/src/utils/concurrency.d.ts +8 -0
  265. package/dist/src/utils/concurrency.js +21 -0
  266. package/dist/src/utils/errors.d.ts +39 -0
  267. package/dist/src/utils/errors.js +55 -0
  268. package/dist/src/utils/fs.js +5 -12
  269. package/dist/src/utils/logger.d.ts +14 -2
  270. package/dist/src/utils/logger.js +43 -13
  271. package/dist/src/utils/options.d.ts +21 -4
  272. package/dist/src/utils/options.js +26 -6
  273. package/dist/src/utils/package-root.js +7 -4
  274. package/dist/src/utils/process-group.js +13 -2
  275. package/dist/src/utils/stdio-drain.d.ts +28 -0
  276. package/dist/src/utils/stdio-drain.js +54 -0
  277. package/dist/src/utils/validation.d.ts +33 -2
  278. package/dist/src/utils/validation.js +57 -2
  279. package/package.json +13 -13
@@ -6,6 +6,7 @@
6
6
  * into it would make the dependency graph cyclic.
7
7
  */
8
8
  /** Shape of the on-disk baseline marker. */
9
+ export declare const DELETED_FILE_FINGERPRINT = "<deleted>";
9
10
  export interface BuildBaseline {
10
11
  /** SHA of engine HEAD at the time the build succeeded. */
11
12
  engineHeadSha: string;
@@ -32,7 +33,8 @@ export interface BuildBaseline {
32
33
  *
33
34
  * Keys are engine-relative POSIX paths. Values are hex-encoded
34
35
  * SHA-256 digests of the file contents at the moment the baseline
35
- * was recorded.
36
+ * was recorded, or {@link DELETED_FILE_FINGERPRINT} when the successful
37
+ * build observed a tracked deletion.
36
38
  */
37
39
  packageableFingerprints?: Record<string, string>;
38
40
  /**
@@ -56,14 +58,32 @@ export interface BuildBaseline {
56
58
  * checkouts are refused up-front (`AmbiguousBuildArtifactsError`), so at
57
59
  * most one obj dir exists per project.
58
60
  *
59
- * Union/"shared coverage" across successive scoped builds was evaluated
60
- * and rejected as unsound — every baseline write refreshes
61
- * `packageableFingerprints` for ALL dirty packageable paths, so a union
62
- * would whitewash an earlier scope's edited fixtures while
63
- * `obj-*`/`_tests/` still holds its stale staging; coverage therefore
64
- * REPLACES.
61
+ * Union/"shared coverage" across successive scoped builds is unsound in
62
+ * general — every baseline write refreshes `packageableFingerprints` for
63
+ * ALL dirty packageable paths, so a blind union would whitewash an
64
+ * earlier scope's edited fixtures while `obj-*`/`_tests/` still holds its
65
+ * stale staging; coverage therefore REPLACES by default.
66
+ *
67
+ * The one exception is `test --build --extend-coverage` (FORGE L1), which
68
+ * unions only after proving the previous record's anchor still holds:
69
+ * same engine HEAD, same {@link BuildBaseline.mozconfigHash}, and every
70
+ * previously fingerprinted path byte-identical — i.e. the wholesale
71
+ * fingerprint refresh is a no-op for everything the earlier scope's
72
+ * staging depended on. Any divergence refuses fail-closed. See
73
+ * `src/core/coverage-extend.ts`, which also documents the one boundary
74
+ * that guard does not cover (dirty non-packageable fixtures).
65
75
  */
66
76
  testPackagingCoverage?: TestPackagingCoverage;
77
+ /**
78
+ * Hex-encoded SHA-256 of `engine/mozconfig` as it stood for this build.
79
+ * Recorded because the mozconfig is REGENERATED from project-side
80
+ * `configs/*.mozconfig` templates plus `fireforge.json` on every build,
81
+ * so `engineHeadSha` does not cover it: two builds at the same engine SHA
82
+ * can configure differently. Consumed by the `--extend-coverage` anchor.
83
+ * Missing on pre-0.41.0 baselines, which `--extend-coverage` refuses
84
+ * (one plain build re-records it).
85
+ */
86
+ mozconfigHash?: string;
67
87
  /**
68
88
  * Anchor for the compiled StaticComponents table: the engine HEAD SHA of
69
89
  * the last FULL-coverage build plus content fingerprints of the
@@ -94,7 +114,8 @@ export interface StaticComponentsBaseline {
94
114
  engineHeadSha: string;
95
115
  /**
96
116
  * Hex-encoded SHA-256 per `components.conf` path that was dirty
97
- * (modified-against-HEAD or untracked) when the full build ran. Keys are
117
+ * (modified-against-HEAD or untracked) when the full build ran. A tracked
118
+ * deletion is represented by {@link DELETED_FILE_FINGERPRINT}. Keys are
98
119
  * engine-relative POSIX paths.
99
120
  */
100
121
  fingerprints: Record<string, string>;
@@ -6,4 +6,5 @@
6
6
  * itself depends on `build-audit.ts` at runtime, and a type import back
7
7
  * into it would make the dependency graph cyclic.
8
8
  */
9
- export {};
9
+ /** Shape of the on-disk baseline marker. */
10
+ export const DELETED_FILE_FINGERPRINT = '<deleted>';
@@ -15,7 +15,7 @@
15
15
  * on successful build completion; a failed build does not update it, so a
16
16
  * subsequent run still audits against the last known-good tree.
17
17
  */
18
- import type { BuildBaseline, TestPackagingCoverage } from './build-baseline-types.js';
18
+ import { type BuildBaseline, type TestPackagingCoverage } from './build-baseline-types.js';
19
19
  /** Name of the last-build marker file under `.fireforge/`. */
20
20
  export declare const BUILD_BASELINE_FILENAME = "last-build.json";
21
21
  /**
@@ -47,5 +47,13 @@ export declare function readBuildBaseline(projectRoot: string): Promise<BuildBas
47
47
  * `staticComponentsBaseline` forward verbatim — `mach build faster`
48
48
  * does not rebake `components.conf` registrations, so the last FULL
49
49
  * build stays the honest anchor for the compiled StaticComponents table.
50
+ * @param recordedBy - Invocation shape that produced this baseline.
51
+ * @param staticComponentsHandling - `'auto'` (default) refreshes the
52
+ * static-components anchor whenever the coverage claim is `'full'` or
53
+ * absent. `'refresh'` records it even for a scoped coverage claim whose
54
+ * implementation escalated to a full build. `'carry-forward'` always keeps the previous anchor: needed by
55
+ * `--extend-coverage` (FORGE L1), whose union can EVALUATE to `'full'`
56
+ * while the build that produced it was still a scoped `mach build
57
+ * faster` that did not rebake the compiled table.
50
58
  */
51
- export declare function writeBuildBaseline(projectRoot: string, engineDir: string, binaryName: string, testPackagingCoverage?: TestPackagingCoverage, previousBaseline?: BuildBaseline, recordedBy?: string): Promise<void>;
59
+ export declare function writeBuildBaseline(projectRoot: string, engineDir: string, binaryName: string, testPackagingCoverage?: TestPackagingCoverage, previousBaseline?: BuildBaseline, recordedBy?: string, staticComponentsHandling?: 'auto' | 'refresh' | 'carry-forward'): Promise<void>;
@@ -19,14 +19,22 @@
19
19
  import { createHash } from 'node:crypto';
20
20
  import { readFile } from 'node:fs/promises';
21
21
  import { join } from 'node:path';
22
- import { toError } from '../utils/errors.js';
22
+ import { mapWithConcurrency } from '../utils/concurrency.js';
23
+ import { getNodeErrorCode, toError } from '../utils/errors.js';
23
24
  import { pathExists, readJson, writeJson } from '../utils/fs.js';
24
25
  import { verbose } from '../utils/logger.js';
25
26
  import { isPackageablePath, isXpcomManifestPath } from './build-audit.js';
27
+ import { DELETED_FILE_FINGERPRINT, } from './build-baseline-types.js';
26
28
  import { FIREFORGE_DIR } from './config-paths.js';
29
+ import { hashEngineFile } from './coverage-extend.js';
27
30
  import { getHead, hasChanges, isMissingHeadError } from './git.js';
28
31
  import { git } from './git-base.js';
29
32
  import { getUntrackedFiles } from './git-status.js';
33
+ /*
34
+ * Keep the IO fan-out bounded: a patched Firefox checkout can carry hundreds
35
+ * of dirty packageable paths and serial reads add directly to every build.
36
+ */
37
+ const FINGERPRINT_IO_CONCURRENCY = 16;
30
38
  /** Name of the last-build marker file under `.fireforge/`. */
31
39
  export const BUILD_BASELINE_FILENAME = 'last-build.json';
32
40
  /**
@@ -73,8 +81,16 @@ export async function readBuildBaseline(projectRoot) {
73
81
  * `staticComponentsBaseline` forward verbatim — `mach build faster`
74
82
  * does not rebake `components.conf` registrations, so the last FULL
75
83
  * build stays the honest anchor for the compiled StaticComponents table.
84
+ * @param recordedBy - Invocation shape that produced this baseline.
85
+ * @param staticComponentsHandling - `'auto'` (default) refreshes the
86
+ * static-components anchor whenever the coverage claim is `'full'` or
87
+ * absent. `'refresh'` records it even for a scoped coverage claim whose
88
+ * implementation escalated to a full build. `'carry-forward'` always keeps the previous anchor: needed by
89
+ * `--extend-coverage` (FORGE L1), whose union can EVALUATE to `'full'`
90
+ * while the build that produced it was still a scoped `mach build
91
+ * faster` that did not rebake the compiled table.
76
92
  */
77
- export async function writeBuildBaseline(projectRoot, engineDir, binaryName, testPackagingCoverage, previousBaseline, recordedBy) {
93
+ export async function writeBuildBaseline(projectRoot, engineDir, binaryName, testPackagingCoverage, previousBaseline, recordedBy, staticComponentsHandling = 'auto') {
78
94
  let engineHeadSha = '';
79
95
  try {
80
96
  engineHeadSha = await getHead(engineDir);
@@ -93,15 +109,19 @@ export async function writeBuildBaseline(projectRoot, engineDir, binaryName, tes
93
109
  // just recompiled the StaticComponents table, so it anchors the table to
94
110
  // the current engine state. A scoped write did not — carry the previous
95
111
  // anchor forward verbatim.
96
- const staticComponentsBaseline = testPackagingCoverage === undefined || testPackagingCoverage === 'full'
112
+ const staticComponentsBaseline = staticComponentsHandling === 'refresh' ||
113
+ (staticComponentsHandling === 'auto' &&
114
+ (testPackagingCoverage === undefined || testPackagingCoverage === 'full'))
97
115
  ? await collectStaticComponentsBaseline(engineDir, engineHeadSha)
98
116
  : previousBaseline?.staticComponentsBaseline;
117
+ const mozconfigHash = await hashEngineFile(engineDir, 'mozconfig');
99
118
  const baseline = {
100
119
  engineHeadSha,
101
120
  builtAt: new Date().toISOString(),
102
121
  binaryName,
103
122
  ...(packageableFingerprints !== undefined ? { packageableFingerprints } : {}),
104
123
  ...(testPackagingCoverage !== undefined ? { testPackagingCoverage } : {}),
124
+ ...(mozconfigHash !== undefined ? { mozconfigHash } : {}),
105
125
  ...(staticComponentsBaseline !== undefined ? { staticComponentsBaseline } : {}),
106
126
  ...(recordedBy !== undefined ? { recordedBy } : {}),
107
127
  };
@@ -165,18 +185,29 @@ async function collectDirtyFingerprints(engineDir, includePath, contextLabel) {
165
185
  if (included.length === 0) {
166
186
  return {};
167
187
  }
168
- const fingerprints = {};
169
- for (const relPath of included) {
188
+ const entries = await mapWithConcurrency(included, FINGERPRINT_IO_CONCURRENCY, async (relPath) => {
170
189
  try {
171
190
  const buffer = await readFile(join(engineDir, relPath));
172
- fingerprints[relPath] = createHash('sha256').update(buffer).digest('hex');
191
+ return [relPath, createHash('sha256').update(buffer).digest('hex')];
173
192
  }
174
193
  catch (fileError) {
194
+ if (getNodeErrorCode(fileError) === 'ENOENT') {
195
+ // A path reported by git but absent on disk is normally a tracked
196
+ // deletion, not a failed fingerprint. Recording the tombstone lets
197
+ // the next stale check prove that deletion was already built.
198
+ return [relPath, DELETED_FILE_FINGERPRINT];
199
+ }
175
200
  // A file that disappeared between status probe and hash is
176
- // expected in concurrent scenarios; skip it without failing the
177
- // whole baseline write.
201
+ // expected in concurrent scenarios; non-ENOENT failures remain
202
+ // untrusted and are skipped without failing the whole baseline write.
178
203
  verbose(`Build baseline: skipping ${contextLabel} for ${relPath} — ${toError(fileError).message}`);
204
+ return undefined;
179
205
  }
206
+ });
207
+ const fingerprints = {};
208
+ for (const entry of entries) {
209
+ if (entry !== undefined)
210
+ fingerprints[entry[0]] = entry[1];
180
211
  }
181
212
  return fingerprints;
182
213
  }
@@ -12,6 +12,8 @@ export interface BuildPreparation {
12
12
  furnaceApplied: number;
13
13
  /** True when `mach configure` was auto-run to refresh a stale backend. */
14
14
  reconfigured: boolean;
15
+ /** A changed packaging manifest cannot be safely handled by `mach build faster`. */
16
+ fullBuildRequired?: boolean;
15
17
  }
16
18
  /** Options for {@link prepareBuildEnvironment}. */
17
19
  export interface PrepareBuildOptions {
@@ -24,6 +26,8 @@ export interface PrepareBuildOptions {
24
26
  */
25
27
  previousBaseline?: BuildBaseline | undefined;
26
28
  }
29
+ /** Packaging manifests whose graph/destination directories need a full build. */
30
+ export declare function requiresFullBuildForIncrementalTest(path: string): boolean;
27
31
  /**
28
32
  * Describes an exit code in the shell's 128+N signal convention. Truncated
29
33
  * configure/build logs with a signal-shaped exit (e.g. 144 = 128+16, SIGURG
@@ -20,6 +20,10 @@ import { generateMozconfig, runMachCapture } from './mach.js';
20
20
  import { explainMachError } from './mach-error-hints.js';
21
21
  /** Path fragments of files whose edits invalidate the recursive-make backend. */
22
22
  const BACKEND_INVALIDATING_SUFFIXES = ['moz.build', 'moz.configure', 'Makefile.in'];
23
+ /** Packaging manifests whose graph/destination directories need a full build. */
24
+ export function requiresFullBuildForIncrementalTest(path) {
25
+ return path === 'jar.mn' || path.endsWith('/jar.mn');
26
+ }
23
27
  /**
24
28
  * Extracts the tail of captured `mach configure` output so the underlying
25
29
  * mozbuild failure (e.g. `mozbuild.util.UnsortedError: ... is not in sorted
@@ -112,9 +116,11 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
112
116
  // before the build step. Prevents incremental builds from silently skipping
113
117
  // work against a stale recursive-make backend.
114
118
  let reconfigured = false;
119
+ let fullBuildRequired = false;
115
120
  if (options.previousBaseline) {
116
121
  const changed = await collectChangedEnginePaths(paths.engine, options.previousBaseline, 'Auto-configure');
117
122
  const invalidating = changed.filter(isBackendInvalidatingFile);
123
+ fullBuildRequired = changed.some(requiresFullBuildForIncrementalTest);
118
124
  if (invalidating.length > 0) {
119
125
  info(`Backend config changed; running backend regeneration first (${invalidating.length} file${invalidating.length === 1 ? '' : 's'} touched).`);
120
126
  info(`Backend command: mach configure`);
@@ -233,5 +239,5 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
233
239
  mozconfigSpinner.error('Failed to generate mozconfig');
234
240
  throw error;
235
241
  }
236
- return { furnaceApplied, reconfigured };
242
+ return { furnaceApplied, reconfigured, fullBuildRequired };
237
243
  }
@@ -11,7 +11,7 @@ export declare const STATE_FILENAME = "state.json";
11
11
  /** Supported top-level fireforge.json keys backed by the current schema. */
12
12
  export declare const SUPPORTED_CONFIG_ROOT_KEYS: readonly ["name", "vendor", "appId", "binaryName", "firefox", "build", "test", "externalToolchains", "license", "wire", "patchLint", "patchPolicy", "typecheck", "markerComment"];
13
13
  /** Supported config paths that can be read or set without --force. */
14
- export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "firefox.sha256", "firefox.candidate", "build", "build.jobs", "test", "test.canaryPath", "test.canaryTimeoutSeconds", "externalToolchains", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.checkJsStrict", "patchLint.checkJsCompilerOptions", "patchLint.checkJsExtraShim", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "patchLint.chromeScriptJsDoc", "patchPolicy", "typecheck", "typecheck.projects", "typecheck.extraShim", "markerComment"];
14
+ export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "firefox.sha256", "firefox.candidate", "build", "build.jobs", "test", "test.canaryPath", "test.canaryTimeoutSeconds", "externalToolchains", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.checkJsStrict", "patchLint.checkJsCompilerOptions", "patchLint.checkJsExtraShim", "patchLint.checkJsTestFiles", "patchLint.checkJsTestShim", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "patchLint.chromeScriptJsDoc", "patchPolicy", "typecheck", "typecheck.projects", "typecheck.extraShim", "markerComment"];
15
15
  /**
16
16
  * Gets all project paths based on a root directory.
17
17
  * @param root - Root directory of the project
@@ -59,6 +59,8 @@ export const SUPPORTED_CONFIG_PATHS = [
59
59
  'patchLint.checkJsStrict',
60
60
  'patchLint.checkJsCompilerOptions',
61
61
  'patchLint.checkJsExtraShim',
62
+ 'patchLint.checkJsTestFiles',
63
+ 'patchLint.checkJsTestShim',
62
64
  'patchLint.rawColorAllowlist',
63
65
  'patchLint.jsdocClassMethods',
64
66
  'patchLint.testAssertionFloor',
@@ -2,6 +2,7 @@
2
2
  import { ConfigError } from '../errors/config.js';
3
3
  import { parseObject } from '../utils/parse.js';
4
4
  import { isContainedRelativePath } from '../utils/paths.js';
5
+ import { isObject } from '../utils/validation.js';
5
6
  function optionalConfigString(rec, key, label) {
6
7
  const value = rec.raw(key);
7
8
  if (value === undefined)
@@ -56,7 +57,7 @@ export function parseExternalToolchainsBlock(rec, config) {
56
57
  throw new ConfigError('Config field "externalToolchains" must be an array');
57
58
  }
58
59
  config.externalToolchains = raw.map((entry, index) => {
59
- if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
60
+ if (!isObject(entry)) {
60
61
  throw new ConfigError(`Config field "externalToolchains[${String(index)}]" must be an object`);
61
62
  }
62
63
  const item = entry;
@@ -76,7 +77,7 @@ export function parseExternalToolchainsBlock(rec, config) {
76
77
  }
77
78
  function parseExternalToolRequirement(tool, index, toolIndex) {
78
79
  const label = `externalToolchains[${String(index)}].tools[${String(toolIndex)}]`;
79
- if (typeof tool !== 'object' || tool === null || Array.isArray(tool)) {
80
+ if (!isObject(tool)) {
80
81
  throw new ConfigError(`Config field "${label}" must be an object`);
81
82
  }
82
83
  const toolRec = tool;
@@ -6,7 +6,7 @@ import { ConfigError } from '../errors/config.js';
6
6
  import { verbose } from '../utils/logger.js';
7
7
  import { parseObject } from '../utils/parse.js';
8
8
  import { isContainedRelativePath, isExplicitAbsolutePath } from '../utils/paths.js';
9
- import { isValidAppId, isValidFirefoxCandidate, isValidFirefoxVersion, isValidProjectLicense, PROJECT_LICENSES, validateFirefoxProductVersionCompatibility, } from '../utils/validation.js';
9
+ import { isObject, isValidAppId, isValidFirefoxCandidate, isValidFirefoxVersion, isValidProjectLicense, PROJECT_LICENSES, validateFirefoxProductVersionCompatibility, } from '../utils/validation.js';
10
10
  import { SUPPORTED_CONFIG_ROOT_KEYS } from './config-paths.js';
11
11
  import { parsePatchPolicyBlock } from './config-validate-patch-policy.js';
12
12
  import { parseExternalToolchainsBlock, parseTestBlock } from './config-validate-test-toolchains.js';
@@ -257,7 +257,7 @@ const PATCH_LINT_CHECKJS_COMPILER_OPTION_KEYS = [
257
257
  * be typed from their real sources without an ambient stub shim.
258
258
  */
259
259
  function parseCheckJsPathsMapping(raw) {
260
- if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
260
+ if (!isObject(raw)) {
261
261
  throw new ConfigError('Config field "patchLint.checkJsCompilerOptions.paths" must be a plain object');
262
262
  }
263
263
  const rec = raw;
@@ -274,7 +274,7 @@ function parseCheckJsPathsMapping(raw) {
274
274
  return out;
275
275
  }
276
276
  function parsePatchLintCheckJsCompilerOptions(raw) {
277
- if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
277
+ if (!isObject(raw)) {
278
278
  throw new ConfigError('Config field "patchLint.checkJsCompilerOptions" must be a plain object');
279
279
  }
280
280
  const rec = raw;
@@ -304,22 +304,35 @@ function parseSeverityGate(raw, label) {
304
304
  }
305
305
  return raw;
306
306
  }
307
+ /**
308
+ * Reads an optional boolean `patchLint` field, rejecting a non-boolean with
309
+ * the field-named message. Three identical inline copies of this shape were
310
+ * part of what held `parsePatchLintBlock` at complexity 25/30.
311
+ */
312
+ function optionalPatchLintBoolean(rec, key) {
313
+ const value = rec.raw(key);
314
+ if (value === undefined)
315
+ return undefined;
316
+ if (typeof value !== 'boolean') {
317
+ throw new ConfigError(`Config field "patchLint.${key}" must be a boolean`);
318
+ }
319
+ return value;
320
+ }
321
+ /** Severity-gate fields, each parsed and assigned identically. */
322
+ const PATCH_LINT_SEVERITY_GATE_KEYS = [
323
+ 'jsdocClassMethods',
324
+ 'testAssertionFloor',
325
+ 'chromeScriptJsDoc',
326
+ 'undefinedIdentifiers',
327
+ ];
307
328
  function parsePatchLintBlock(rec) {
308
329
  const out = {};
309
- const checkJs = rec.raw('checkJs');
310
- if (checkJs !== undefined) {
311
- if (typeof checkJs !== 'boolean') {
312
- throw new ConfigError('Config field "patchLint.checkJs" must be a boolean');
313
- }
330
+ const checkJs = optionalPatchLintBoolean(rec, 'checkJs');
331
+ if (checkJs !== undefined)
314
332
  out.checkJs = checkJs;
315
- }
316
- const checkJsStrict = rec.raw('checkJsStrict');
317
- if (checkJsStrict !== undefined) {
318
- if (typeof checkJsStrict !== 'boolean') {
319
- throw new ConfigError('Config field "patchLint.checkJsStrict" must be a boolean');
320
- }
333
+ const checkJsStrict = optionalPatchLintBoolean(rec, 'checkJsStrict');
334
+ if (checkJsStrict !== undefined)
321
335
  out.checkJsStrict = checkJsStrict;
322
- }
323
336
  const checkJsCompilerOptionsRaw = rec.raw('checkJsCompilerOptions');
324
337
  if (checkJsCompilerOptionsRaw !== undefined) {
325
338
  out.checkJsCompilerOptions = parsePatchLintCheckJsCompilerOptions(checkJsCompilerOptionsRaw);
@@ -328,6 +341,13 @@ function parsePatchLintBlock(rec) {
328
341
  if (checkJsExtraShim !== undefined) {
329
342
  out.checkJsExtraShim = parseShimPath(checkJsExtraShim, 'patchLint.checkJsExtraShim');
330
343
  }
344
+ const checkJsTestFiles = optionalPatchLintBoolean(rec, 'checkJsTestFiles');
345
+ if (checkJsTestFiles !== undefined)
346
+ out.checkJsTestFiles = checkJsTestFiles;
347
+ const checkJsTestShim = rec.raw('checkJsTestShim');
348
+ if (checkJsTestShim !== undefined) {
349
+ out.checkJsTestShim = parseShimPath(checkJsTestShim, 'patchLint.checkJsTestShim');
350
+ }
331
351
  const rawColorAllowlist = rec.raw('rawColorAllowlist');
332
352
  if (rawColorAllowlist !== undefined) {
333
353
  if (!Array.isArray(rawColorAllowlist) ||
@@ -336,21 +356,10 @@ function parsePatchLintBlock(rec) {
336
356
  }
337
357
  out.rawColorAllowlist = rawColorAllowlist;
338
358
  }
339
- const jsdocClassMethods = parseSeverityGate(rec.raw('jsdocClassMethods'), 'patchLint.jsdocClassMethods');
340
- if (jsdocClassMethods !== undefined) {
341
- out.jsdocClassMethods = jsdocClassMethods;
342
- }
343
- const testAssertionFloor = parseSeverityGate(rec.raw('testAssertionFloor'), 'patchLint.testAssertionFloor');
344
- if (testAssertionFloor !== undefined) {
345
- out.testAssertionFloor = testAssertionFloor;
346
- }
347
- const chromeScriptJsDoc = parseSeverityGate(rec.raw('chromeScriptJsDoc'), 'patchLint.chromeScriptJsDoc');
348
- if (chromeScriptJsDoc !== undefined) {
349
- out.chromeScriptJsDoc = chromeScriptJsDoc;
350
- }
351
- const undefinedIdentifiers = parseSeverityGate(rec.raw('undefinedIdentifiers'), 'patchLint.undefinedIdentifiers');
352
- if (undefinedIdentifiers !== undefined) {
353
- out.undefinedIdentifiers = undefinedIdentifiers;
359
+ for (const key of PATCH_LINT_SEVERITY_GATE_KEYS) {
360
+ const gate = parseSeverityGate(rec.raw(key), `patchLint.${key}`);
361
+ if (gate !== undefined)
362
+ out[key] = gate;
354
363
  }
355
364
  if (out.checkJsStrict === true && out.checkJs !== true) {
356
365
  throw new ConfigError('Config field "patchLint.checkJsStrict" requires "patchLint.checkJs": true');
@@ -358,6 +367,12 @@ function parsePatchLintBlock(rec) {
358
367
  if (out.checkJsCompilerOptions !== undefined && out.checkJsStrict !== true) {
359
368
  throw new ConfigError('Config field "patchLint.checkJsCompilerOptions" requires "patchLint.checkJsStrict": true');
360
369
  }
370
+ if (out.checkJsTestFiles === true && out.checkJs !== true) {
371
+ throw new ConfigError('Config field "patchLint.checkJsTestFiles" requires "patchLint.checkJs": true');
372
+ }
373
+ if (out.checkJsTestShim !== undefined && out.checkJsTestFiles !== true) {
374
+ throw new ConfigError('Config field "patchLint.checkJsTestShim" requires "patchLint.checkJsTestFiles": true');
375
+ }
361
376
  return out;
362
377
  }
363
378
  /**
@@ -12,6 +12,7 @@ import { basename } from 'node:path';
12
12
  import { ConfigError, ConfigNotFoundError } from '../errors/config.js';
13
13
  import { toError } from '../utils/errors.js';
14
14
  import * as fsUtils from '../utils/fs.js';
15
+ import { isObject } from '../utils/validation.js';
15
16
  import { getProjectPaths } from './config-paths.js';
16
17
  import { validateConfig } from './config-validate.js';
17
18
  import { createSiblingLockPath, withFileLock } from './file-lock.js';
@@ -21,9 +22,21 @@ export { CONFIG_FILENAME, FIREFORGE_DIR, getProjectPaths, STATE_FILENAME, SUPPOR
21
22
  export { loadState, saveState, updateState } from './config-state.js';
22
23
  export { validateConfig } from './config-validate.js';
23
24
  // ---- config I/O (stays here because it bridges paths + validation) ----
25
+ /**
26
+ * Config-file existence probe.
27
+ *
28
+ * Uses {@link pathExistsStrict} deliberately: a permission error probing
29
+ * `fireforge.json` must propagate rather than read as "no config here", which
30
+ * is what plain `pathExists` would report.
31
+ *
32
+ * The `fsUtils as typeof fsUtils & { pathExistsStrict?: … }` cast this
33
+ * replaced re-declared a real, non-optional export as optional so a partial
34
+ * `vi.mock` of `utils/fs.js` would fall back to `pathExists` — production code
35
+ * shaped around a test double, and shaped around the *wrong* function, since
36
+ * the two differ precisely on whether EACCES propagates.
37
+ */
24
38
  async function configPathExists(path) {
25
- const fs = fsUtils;
26
- return (fs.pathExistsStrict ?? fsUtils.pathExists)(path);
39
+ return fsUtils.pathExistsStrict(path);
27
40
  }
28
41
  /**
29
42
  * Checks if a fireforge.json exists in the given directory.
@@ -79,7 +92,7 @@ export async function loadRawConfigDocument(root) {
79
92
  }
80
93
  try {
81
94
  const data = await fsUtils.readJson(paths.config);
82
- if (data === null || typeof data !== 'object' || Array.isArray(data)) {
95
+ if (!isObject(data)) {
83
96
  throw new ConfigError(`Invalid fireforge.json at ${paths.config}: expected an object`);
84
97
  }
85
98
  return data;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * `test --build/--build-only --extend-coverage` (FORGE L1): unions the
3
+ * requested paths into the recorded `testPackagingCoverage` instead of
4
+ * replacing it, under an anchor guard.
5
+ *
6
+ * Coverage REPLACES by default for a documented reason
7
+ * (`build-baseline-types.ts`): every baseline write refreshes
8
+ * `packageableFingerprints` for ALL dirty packageable paths, so a blind
9
+ * union would whitewash an earlier scope's edited fixtures while
10
+ * `obj-*`/`_tests/` still holds that scope's stale staging. Extending is
11
+ * honest only while everything the PREVIOUS record vouched for is still
12
+ * true, which is what {@link checkExtendCoverageAnchor} and
13
+ * {@link checkExtendMozconfigAnchor} verify:
14
+ *
15
+ * 1. engine HEAD is unchanged — every committed edit is excluded;
16
+ * 2. every path the previous baseline fingerprinted still hashes to the
17
+ * recorded digest — the earlier scope's staging inputs are byte-identical.
18
+ * Files that became dirty SINCE that build are fine: the current
19
+ * whole-tree `mach build faster` repackages `dist/` and vouches for them;
20
+ * 3. the generated `engine/mozconfig` is unchanged. Engine HEAD does not
21
+ * cover this — the mozconfig is regenerated from project-side
22
+ * `configs/*.mozconfig` templates plus `fireforge.json` on every build,
23
+ * so two builds at the same engine SHA can configure differently.
24
+ *
25
+ * Every failure refuses fail-closed; the operator's remedy is always a plain
26
+ * scoped build, which resets the claim to the paths it actually vouches for.
27
+ *
28
+ * Known boundary, documented rather than closed: dirty NON-packageable
29
+ * fixtures under previously covered paths (plain test files, manifests
30
+ * `isPackageablePath` rejects) are invisible to check 2. Editing one
31
+ * uncommitted between two builds leaves the earlier scope's `_tests/`
32
+ * staging stale while extend still vouches for it. Plain REPLACE has no
33
+ * such window, because the earlier scope simply stops being covered.
34
+ */
35
+ import { type BuildBaseline, type TestPackagingCoverage } from './build-baseline-types.js';
36
+ /** Why `--extend-coverage` could not union onto the recorded claim. */
37
+ export type ExtendAnchorReason = 'no-baseline' | 'no-fingerprints' | 'no-mozconfig-hash' | 'head-moved' | 'fingerprint-diverged' | 'mozconfig-changed';
38
+ /** Outcome of an anchor check: either extend is sound, or it is refused. */
39
+ export type ExtendAnchorResult = {
40
+ ok: true;
41
+ } | {
42
+ ok: false;
43
+ reason: ExtendAnchorReason;
44
+ detail: string[];
45
+ };
46
+ /**
47
+ * Verifies the parts of the anchor knowable before the build runs: a usable
48
+ * previous baseline, an unchanged engine HEAD, and byte-identical content
49
+ * for every path that baseline fingerprinted.
50
+ * @param engineDir - Path to the engine directory
51
+ * @param previousBaseline - Baseline recorded by the previous build
52
+ */
53
+ export declare function checkExtendCoverageAnchor(engineDir: string, previousBaseline: BuildBaseline | undefined): Promise<ExtendAnchorResult>;
54
+ /**
55
+ * Verifies the mozconfig half of the anchor. Split from
56
+ * {@link checkExtendCoverageAnchor} because it can only run AFTER
57
+ * `prepareBuildEnvironment` regenerates `engine/mozconfig` — that file is
58
+ * the one this build will configure with — and must still run BEFORE mach,
59
+ * so a refusal costs no build time.
60
+ * @param engineDir - Path to the engine directory
61
+ * @param previousBaseline - Baseline recorded by the previous build
62
+ */
63
+ export declare function checkExtendMozconfigAnchor(engineDir: string, previousBaseline: BuildBaseline | undefined): Promise<ExtendAnchorResult>;
64
+ /**
65
+ * Unions the newly built paths onto the recorded claim. A `'full'` claim
66
+ * (or a pre-0.37.0 absent one, which means full historically) stays
67
+ * `'full'` — it already covers everything.
68
+ * @param previous - Coverage recorded by the previous build
69
+ * @param requested - Normalized request paths this build packaged
70
+ */
71
+ export declare function unionTestPackagingCoverage(previous: TestPackagingCoverage | undefined, requested: readonly string[]): TestPackagingCoverage;
72
+ /**
73
+ * Renders the fail-closed refusal for an anchor check that did not pass.
74
+ * Kept separate from the probes so tests can assert the structured result
75
+ * without matching copy.
76
+ * @param result - Failing result from an anchor check
77
+ */
78
+ export declare function formatExtendCoverageRefusal(result: {
79
+ reason: ExtendAnchorReason;
80
+ detail: string[];
81
+ }): string;
82
+ /**
83
+ * Hex-encoded SHA-256 of an engine-relative file, matching the digest the
84
+ * baseline writer produces. `undefined` on any IO error, which every caller
85
+ * treats as "cannot prove unchanged" — i.e. a refusal.
86
+ * @param engineDir - Path to the engine directory
87
+ * @param relPath - Engine-relative POSIX path
88
+ */
89
+ export declare function hashEngineFile(engineDir: string, relPath: string): Promise<string | undefined>;