@hominis/fireforge 0.51.0 → 0.53.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 (254) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/build-info.json +4 -4
  3. package/dist/src/commands/bootstrap.js +2 -3
  4. package/dist/src/commands/build-prelock.d.ts +3 -3
  5. package/dist/src/commands/build-prelock.js +3 -3
  6. package/dist/src/commands/build.js +4 -0
  7. package/dist/src/commands/declarations-report.d.ts +9 -3
  8. package/dist/src/commands/declarations-report.js +18 -11
  9. package/dist/src/commands/discard.js +3 -3
  10. package/dist/src/commands/doctor-furnace-config-sync.js +4 -4
  11. package/dist/src/commands/doctor-furnace.js +5 -5
  12. package/dist/src/commands/export-adopt-check.d.ts +5 -6
  13. package/dist/src/commands/export-adopt-check.js +5 -6
  14. package/dist/src/commands/export-flow.js +6 -9
  15. package/dist/src/commands/export-staged-registration.d.ts +4 -4
  16. package/dist/src/commands/export-staged-registration.js +4 -4
  17. package/dist/src/commands/forward-import-remedy.d.ts +2 -3
  18. package/dist/src/commands/forward-import-remedy.js +2 -3
  19. package/dist/src/commands/furnace/create-dry-run.d.ts +2 -2
  20. package/dist/src/commands/furnace/deploy-fragment-includers.d.ts +2 -2
  21. package/dist/src/commands/furnace/deploy-fragment-includers.js +2 -2
  22. package/dist/src/commands/furnace/index.js +2 -2
  23. package/dist/src/commands/lint.js +4 -4
  24. package/dist/src/commands/patch/compact.js +1 -1
  25. package/dist/src/commands/patch/lint-ignore-from.d.ts +3 -4
  26. package/dist/src/commands/patch/lint-ignore-from.js +3 -4
  27. package/dist/src/commands/patch/move-files-adopt.d.ts +33 -0
  28. package/dist/src/commands/patch/move-files-adopt.js +65 -0
  29. package/dist/src/commands/patch/move-files-create.js +41 -20
  30. package/dist/src/commands/patch/move-files-into.d.ts +18 -0
  31. package/dist/src/commands/patch/move-files-into.js +125 -60
  32. package/dist/src/commands/patch/move-files-retire.d.ts +39 -0
  33. package/dist/src/commands/patch/move-files-retire.js +91 -0
  34. package/dist/src/commands/patch/move-files.d.ts +3 -0
  35. package/dist/src/commands/patch/move-files.js +42 -12
  36. package/dist/src/commands/patch/move-size-tier.d.ts +27 -0
  37. package/dist/src/commands/patch/move-size-tier.js +66 -0
  38. package/dist/src/commands/patch/rename.js +1 -1
  39. package/dist/src/commands/patch/reorder-projection.d.ts +17 -0
  40. package/dist/src/commands/patch/reorder-projection.js +49 -0
  41. package/dist/src/commands/patch/reorder-shift.d.ts +30 -0
  42. package/dist/src/commands/patch/reorder-shift.js +307 -0
  43. package/dist/src/commands/patch/reorder.d.ts +9 -15
  44. package/dist/src/commands/patch/reorder.js +52 -50
  45. package/dist/src/commands/patch/split-plan.d.ts +31 -3
  46. package/dist/src/commands/patch/split-plan.js +36 -12
  47. package/dist/src/commands/patch/split-staged.d.ts +2 -2
  48. package/dist/src/commands/patch/split-staged.js +2 -2
  49. package/dist/src/commands/patch/split.js +18 -5
  50. package/dist/src/commands/patch/staged-dependency-entries.d.ts +9 -1
  51. package/dist/src/commands/patch/staged-dependency-entries.js +32 -4
  52. package/dist/src/commands/patch/staged-dependency-from.js +15 -9
  53. package/dist/src/commands/patch/staged-dependency.js +13 -9
  54. package/dist/src/commands/patch/tier.js +2 -2
  55. package/dist/src/commands/patch-rename-map.d.ts +14 -3
  56. package/dist/src/commands/patch-rename-map.js +21 -3
  57. package/dist/src/commands/re-export-adjacent.d.ts +4 -5
  58. package/dist/src/commands/re-export-lint.d.ts +4 -3
  59. package/dist/src/commands/re-export-lint.js +4 -3
  60. package/dist/src/commands/re-export-preflight.js +1 -2
  61. package/dist/src/commands/re-export-purity-scope.d.ts +1 -1
  62. package/dist/src/commands/re-export-purity-scope.js +13 -7
  63. package/dist/src/commands/re-export-retired-declarations.d.ts +22 -0
  64. package/dist/src/commands/re-export-retired-declarations.js +113 -0
  65. package/dist/src/commands/re-export-single.js +2 -2
  66. package/dist/src/commands/re-export-stale-registrations.d.ts +27 -6
  67. package/dist/src/commands/re-export-stale-registrations.js +65 -21
  68. package/dist/src/commands/re-export.js +4 -0
  69. package/dist/src/commands/rebase/continue.js +3 -3
  70. package/dist/src/commands/register.js +5 -4
  71. package/dist/src/commands/status-output.js +5 -1
  72. package/dist/src/commands/status.js +1 -1
  73. package/dist/src/commands/test-build-furnace.d.ts +1 -2
  74. package/dist/src/commands/test-build-furnace.js +1 -2
  75. package/dist/src/commands/test-build-phase.d.ts +10 -0
  76. package/dist/src/commands/test-build-phase.js +51 -16
  77. package/dist/src/commands/test-control-restore.d.ts +40 -0
  78. package/dist/src/commands/test-control-restore.js +86 -0
  79. package/dist/src/commands/test-dispatch.js +83 -13
  80. package/dist/src/commands/test-multi-verdict.js +1 -0
  81. package/dist/src/commands/test-orchestrate.d.ts +11 -0
  82. package/dist/src/commands/test-orchestrate.js +33 -2
  83. package/dist/src/commands/test-perf-scope.d.ts +1 -1
  84. package/dist/src/commands/test-perf-scope.js +1 -1
  85. package/dist/src/commands/test-prelock.d.ts +7 -8
  86. package/dist/src/commands/test-prelock.js +15 -12
  87. package/dist/src/commands/test-register.js +2 -2
  88. package/dist/src/commands/test-results.d.ts +10 -1
  89. package/dist/src/commands/test-results.js +20 -3
  90. package/dist/src/commands/test-run.d.ts +6 -0
  91. package/dist/src/commands/test-run.js +23 -4
  92. package/dist/src/commands/test-shards.d.ts +7 -3
  93. package/dist/src/commands/test-shards.js +24 -7
  94. package/dist/src/commands/test-stale-gate.d.ts +6 -1
  95. package/dist/src/commands/test-stale-gate.js +47 -2
  96. package/dist/src/commands/test-verdict.d.ts +5 -3
  97. package/dist/src/commands/test.js +14 -1
  98. package/dist/src/commands/token.js +2 -2
  99. package/dist/src/commands/verify.js +1 -1
  100. package/dist/src/commands/watch.js +1 -1
  101. package/dist/src/commands/wire.js +1 -1
  102. package/dist/src/core/build-audit-resolve.d.ts +4 -5
  103. package/dist/src/core/build-audit-resolve.js +4 -5
  104. package/dist/src/core/build-audit-skip.d.ts +2 -2
  105. package/dist/src/core/build-audit.d.ts +3 -4
  106. package/dist/src/core/build-baseline-types.d.ts +37 -3
  107. package/dist/src/core/build-baseline-validate.d.ts +30 -1
  108. package/dist/src/core/build-baseline-validate.js +134 -33
  109. package/dist/src/core/build-baseline.d.ts +8 -2
  110. package/dist/src/core/build-baseline.js +32 -16
  111. package/dist/src/core/build-jar-escalation.d.ts +4 -4
  112. package/dist/src/core/build-jar-escalation.js +4 -4
  113. package/dist/src/core/build-prepare.js +4 -4
  114. package/dist/src/core/config-validate.js +4 -4
  115. package/dist/src/core/coverage-extend.d.ts +5 -3
  116. package/dist/src/core/coverage-extend.js +8 -3
  117. package/dist/src/core/coverage-records.d.ts +4 -5
  118. package/dist/src/core/coverage-records.js +4 -5
  119. package/dist/src/core/destructive.d.ts +2 -2
  120. package/dist/src/core/destructive.js +2 -2
  121. package/dist/src/core/discard-baseline.d.ts +2 -2
  122. package/dist/src/core/discard-baseline.js +3 -3
  123. package/dist/src/core/engine-session-lock.d.ts +1 -1
  124. package/dist/src/core/engine-session-lock.js +1 -1
  125. package/dist/src/core/file-lock-owner.d.ts +5 -5
  126. package/dist/src/core/file-lock-owner.js +5 -5
  127. package/dist/src/core/file-lock-queue.js +6 -8
  128. package/dist/src/core/file-lock-shared.d.ts +4 -4
  129. package/dist/src/core/file-lock-shared.js +4 -4
  130. package/dist/src/core/file-lock.js +7 -10
  131. package/dist/src/core/firefox.js +2 -2
  132. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +1 -1
  133. package/dist/src/core/furnace-apply-overwrite-warn.js +1 -1
  134. package/dist/src/core/furnace-apply.js +3 -3
  135. package/dist/src/core/furnace-config-validate-exceptions.d.ts +4 -3
  136. package/dist/src/core/furnace-config-validate-exceptions.js +26 -6
  137. package/dist/src/core/furnace-operation.js +5 -5
  138. package/dist/src/core/furnace-registration-jar-inplace.js +5 -6
  139. package/dist/src/core/furnace-registration-rename.d.ts +5 -5
  140. package/dist/src/core/furnace-registration-rename.js +5 -5
  141. package/dist/src/core/furnace-registration-validate.d.ts +2 -1
  142. package/dist/src/core/furnace-registration-validate.js +2 -1
  143. package/dist/src/core/furnace-registration.js +2 -2
  144. package/dist/src/core/furnace-validate-accessibility.d.ts +1 -1
  145. package/dist/src/core/furnace-validate-accessibility.js +22 -11
  146. package/dist/src/core/furnace-validate-exceptions.d.ts +2 -1
  147. package/dist/src/core/furnace-validate-exceptions.js +31 -13
  148. package/dist/src/core/git-base.js +4 -3
  149. package/dist/src/core/git-diff.js +1 -1
  150. package/dist/src/core/git.js +1 -1
  151. package/dist/src/core/harness-orphans.d.ts +6 -6
  152. package/dist/src/core/harness-orphans.js +6 -6
  153. package/dist/src/core/harness-port-reservation.d.ts +60 -0
  154. package/dist/src/core/harness-port-reservation.js +122 -0
  155. package/dist/src/core/history-log-file.d.ts +9 -9
  156. package/dist/src/core/history-log-file.js +15 -15
  157. package/dist/src/core/history-log.d.ts +3 -3
  158. package/dist/src/core/history-log.js +3 -3
  159. package/dist/src/core/launchable-build.d.ts +39 -0
  160. package/dist/src/core/launchable-build.js +85 -0
  161. package/dist/src/core/marionette-port.js +1 -3
  162. package/dist/src/core/moz-manifest-rules.js +3 -2
  163. package/dist/src/core/negative-control-compiled.d.ts +22 -3
  164. package/dist/src/core/negative-control-compiled.js +41 -5
  165. package/dist/src/core/parser-fallback.d.ts +5 -5
  166. package/dist/src/core/parser-fallback.js +5 -5
  167. package/dist/src/core/patch-identifier-suggest.d.ts +6 -0
  168. package/dist/src/core/patch-identifier-suggest.js +11 -4
  169. package/dist/src/core/patch-lint-checkjs.d.ts +1 -1
  170. package/dist/src/core/patch-lint-checkjs.js +1 -1
  171. package/dist/src/core/patch-lint-css.js +2 -2
  172. package/dist/src/core/patch-lint-file-size.d.ts +5 -7
  173. package/dist/src/core/patch-lint-file-size.js +5 -7
  174. package/dist/src/core/patch-lint-ignore.d.ts +3 -3
  175. package/dist/src/core/patch-lint-ignore.js +3 -3
  176. package/dist/src/core/patch-lint-prettier.d.ts +4 -6
  177. package/dist/src/core/patch-lint-prettier.js +4 -6
  178. package/dist/src/core/patch-lint-projection.d.ts +1 -1
  179. package/dist/src/core/patch-lint-projection.js +1 -1
  180. package/dist/src/core/patch-lint-queue-types.d.ts +2 -2
  181. package/dist/src/core/patch-lint-queue-types.js +2 -2
  182. package/dist/src/core/patch-manifest-consistency.d.ts +2 -2
  183. package/dist/src/core/patch-manifest-consistency.js +2 -2
  184. package/dist/src/core/patch-manifest-io.d.ts +5 -1
  185. package/dist/src/core/patch-manifest-io.js +12 -10
  186. package/dist/src/core/patch-manifest-key-order.d.ts +3 -4
  187. package/dist/src/core/patch-manifest-key-order.js +3 -4
  188. package/dist/src/core/patch-parse.d.ts +3 -3
  189. package/dist/src/core/patch-parse.js +3 -3
  190. package/dist/src/core/patch-policy.d.ts +1 -1
  191. package/dist/src/core/patch-policy.js +1 -1
  192. package/dist/src/core/patch-source-metadata.d.ts +1 -1
  193. package/dist/src/core/patch-source-metadata.js +1 -1
  194. package/dist/src/core/patch-subset.d.ts +5 -3
  195. package/dist/src/core/patch-subset.js +8 -7
  196. package/dist/src/core/patch-transform.d.ts +2 -2
  197. package/dist/src/core/patch-transform.js +2 -2
  198. package/dist/src/core/rebase-session.js +4 -6
  199. package/dist/src/core/register-xpcshell-test.d.ts +28 -7
  200. package/dist/src/core/register-xpcshell-test.js +76 -27
  201. package/dist/src/core/run-log.d.ts +8 -8
  202. package/dist/src/core/run-log.js +8 -8
  203. package/dist/src/core/status-classify.d.ts +1 -2
  204. package/dist/src/core/status-classify.js +1 -2
  205. package/dist/src/core/test-harness-crash.d.ts +5 -3
  206. package/dist/src/core/test-harness-crash.js +13 -14
  207. package/dist/src/core/test-harness-infra.d.ts +32 -0
  208. package/dist/src/core/test-harness-infra.js +69 -0
  209. package/dist/src/core/test-harness-signature.d.ts +6 -0
  210. package/dist/src/core/test-run-completeness.d.ts +12 -5
  211. package/dist/src/core/test-run-completeness.js +15 -8
  212. package/dist/src/core/test-stale-check.d.ts +48 -1
  213. package/dist/src/core/test-stale-check.js +97 -2
  214. package/dist/src/core/test-stall-triage.js +5 -7
  215. package/dist/src/core/token-category.js +2 -2
  216. package/dist/src/core/token-inventory.js +3 -3
  217. package/dist/src/core/token-manager.d.ts +2 -2
  218. package/dist/src/core/token-manager.js +5 -7
  219. package/dist/src/core/token-variant.d.ts +6 -6
  220. package/dist/src/core/token-variant.js +7 -7
  221. package/dist/src/core/typecheck-shim.d.ts +2 -1
  222. package/dist/src/core/typecheck-shim.js +29 -5
  223. package/dist/src/core/wire-destroy.d.ts +1 -1
  224. package/dist/src/core/wire-destroy.js +1 -1
  225. package/dist/src/core/wire-dom-fragment.js +1 -1
  226. package/dist/src/core/wire-init.d.ts +1 -1
  227. package/dist/src/core/wire-init.js +1 -1
  228. package/dist/src/core/wire-subscript.d.ts +1 -1
  229. package/dist/src/core/wire-subscript.js +1 -1
  230. package/dist/src/core/wire-utils.d.ts +2 -2
  231. package/dist/src/core/wire-utils.js +3 -3
  232. package/dist/src/errors/base.d.ts +1 -1
  233. package/dist/src/errors/base.js +1 -1
  234. package/dist/src/errors/rebase.d.ts +1 -1
  235. package/dist/src/errors/rebase.js +1 -1
  236. package/dist/src/types/commands/options.d.ts +26 -3
  237. package/dist/src/types/config.d.ts +5 -7
  238. package/dist/src/types/furnace.d.ts +9 -0
  239. package/dist/src/utils/options.js +2 -2
  240. package/dist/src/utils/process.js +2 -2
  241. package/dist/src/utils/regex.d.ts +2 -2
  242. package/dist/src/utils/regex.js +3 -3
  243. package/dist/src/utils/sleep.d.ts +5 -6
  244. package/dist/src/utils/sleep.js +5 -6
  245. package/dist/src/utils/validation.js +2 -2
  246. package/docs/configuration.md +40 -26
  247. package/docs/exit-codes.md +8 -11
  248. package/docs/furnace.md +21 -3
  249. package/docs/lifecycle-invariants.md +36 -45
  250. package/docs/machine-output.md +43 -22
  251. package/docs/patch-workflow.md +129 -24
  252. package/docs/run-logs.md +8 -9
  253. package/docs/testing.md +118 -44
  254. package/package.json +1 -1
@@ -15,7 +15,7 @@
15
15
  * on successful build completion. A failed build does not update it, so a
16
16
  * subsequent run still audits against the last known-good tree.
17
17
  */
18
- import { type BuildBaseline, type CoverageAnchor, type CoverageRecord, type TestPackagingCoverage } from './build-baseline-types.js';
18
+ import { type BuildBaseline, type ControlBuildStamp, type CoverageAnchor, type CoverageRecord, type TestPackagingCoverage } from './build-baseline-types.js';
19
19
  /**
20
20
  * Which mach build produced the baseline being written. Decides whether
21
21
  * the `jar.mn` half of {@link BuildBaseline.buildInputFingerprints} is
@@ -35,7 +35,7 @@ export declare function getBuildBaselinePath(projectRoot: string): string;
35
35
  * Reads the last-build baseline if present. Returns undefined when no
36
36
  * previous successful build has been recorded. Builds can replace invalid
37
37
  * markers; dispatch preflights pass `refuse` so invalid coverage never
38
- * becomes the legacy implicit full claim.
38
+ * becomes the implicit full claim of a baseline without coverage.
39
39
  * @param projectRoot - Root directory of the project
40
40
  */
41
41
  export declare function readBuildBaseline(projectRoot: string, invalid?: 'ignore' | 'refuse'): Promise<BuildBaseline | undefined>;
@@ -85,6 +85,12 @@ export interface WriteBuildBaselineOptions {
85
85
  * escalation no full build has honoured.
86
86
  */
87
87
  buildKind?: BaselineBuildKind | undefined;
88
+ /**
89
+ * Stamp for a build that compiled a negative control into the binary.
90
+ * Omitted → field left off, which is how every other build clears it.
91
+ * See {@link BuildBaseline.controlBuild}.
92
+ */
93
+ controlBuild?: ControlBuildStamp | undefined;
88
94
  }
89
95
  /**
90
96
  * Records a successful build by writing a fresh baseline marker. Captures
@@ -26,12 +26,12 @@ import { sha256Hex } from '../utils/hash.js';
26
26
  import { verbose } from '../utils/logger.js';
27
27
  import { isBuildInputPath, isJarManifestPath, isPackageablePath, isXpcomManifestPath, } from './build-audit.js';
28
28
  import { DELETED_FILE_FINGERPRINT, } from './build-baseline-types.js';
29
- import { isBuildBaseline } from './build-baseline-validate.js';
29
+ import { describeBuildBaselineProblem, findBuildBaselineProblem, isCoverageProblem, } from './build-baseline-validate.js';
30
30
  import { FIREFORGE_DIR } from './config-paths.js';
31
31
  import { hashEngineFile } from './coverage-extend.js';
32
32
  import { getHead, hasChanges, isMissingHeadError } from './git.js';
33
33
  import { git } from './git-base.js';
34
- import { getUntrackedFiles } from './git-status.js';
34
+ import { historyRecordIdentity } from './history-log-file.js';
35
35
  /*
36
36
  * Keep the IO fan-out bounded: a patched Firefox checkout can carry hundreds
37
37
  * of dirty packageable paths and serial reads add directly to every build.
@@ -51,27 +51,41 @@ export function getBuildBaselinePath(projectRoot) {
51
51
  * Reads the last-build baseline if present. Returns undefined when no
52
52
  * previous successful build has been recorded. Builds can replace invalid
53
53
  * markers; dispatch preflights pass `refuse` so invalid coverage never
54
- * becomes the legacy implicit full claim.
54
+ * becomes the implicit full claim of a baseline without coverage.
55
55
  * @param projectRoot - Root directory of the project
56
56
  */
57
57
  export async function readBuildBaseline(projectRoot, invalid = 'ignore') {
58
58
  const path = getBuildBaselinePath(projectRoot);
59
+ let problem;
59
60
  try {
60
61
  const value = await readJson(path);
61
- if (!isBuildBaseline(value))
62
- throw new Error('invalid baseline fields');
62
+ problem = findBuildBaselineProblem(value);
63
+ if (problem !== undefined) {
64
+ throw new Error(`invalid baseline field ${describeBuildBaselineProblem(problem)}`);
65
+ }
63
66
  return value;
64
67
  }
65
68
  catch (error) {
66
69
  if (getNodeErrorCode(error) === 'ENOENT')
67
70
  return undefined;
68
71
  if (invalid === 'refuse') {
69
- throw new PreflightRefusalError(`Build baseline ${path} is invalid: ${toError(error).message}. Run "fireforge build" to record a fresh baseline before dispatching tests.`, 'invalid-build-baseline');
72
+ // A coverage field is what a scoped `test --build` writes, so a fresh
73
+ // scoped build over the requested paths replaces it as well as a full
74
+ // one does.
75
+ const remedy = problem !== undefined && isCoverageProblem(problem)
76
+ ? 'The field is part of the packaging-coverage claim a scoped "fireforge test --build" records. Rerun this test with --build to record a fresh claim for its paths, or run "fireforge build" to record a full baseline.'
77
+ : 'Run "fireforge build" to record a fresh baseline before dispatching tests.';
78
+ throw new PreflightRefusalError(`Build baseline ${path} is invalid: ${toError(error).message}. ${remedy}`, 'invalid-build-baseline');
70
79
  }
71
80
  verbose(`Ignoring invalid build baseline ${path}: ${toError(error).message}`);
72
81
  return undefined;
73
82
  }
74
83
  }
84
+ /** This process's {@link BuildBaseline.recorder} stamp. */
85
+ function baselineRecorder() {
86
+ const { pid, invocationId, sessionId } = historyRecordIdentity();
87
+ return { pid, invocationId, ...(sessionId !== undefined ? { sessionId } : {}) };
88
+ }
75
89
  /**
76
90
  * Records a successful build by writing a fresh baseline marker. Captures
77
91
  * engine HEAD SHA (or an empty string when the engine has no HEAD yet) and
@@ -81,7 +95,7 @@ export async function readBuildBaseline(projectRoot, invalid = 'ignore') {
81
95
  * @param options - See {@link WriteBuildBaselineOptions}
82
96
  */
83
97
  export async function writeBuildBaseline(options) {
84
- const { projectRoot, engineDir, binaryName, testPackagingCoverage, testCoverageRecords, fullCoverageAnchor, previousBaseline, recordedBy, staticComponentsHandling = 'auto', buildKind = 'full', } = options;
98
+ const { projectRoot, engineDir, binaryName, testPackagingCoverage, testCoverageRecords, fullCoverageAnchor, previousBaseline, recordedBy, staticComponentsHandling = 'auto', buildKind = 'full', controlBuild, } = options;
85
99
  let engineHeadSha = '';
86
100
  try {
87
101
  engineHeadSha = await getHead(engineDir);
@@ -98,7 +112,7 @@ export async function writeBuildBaseline(options) {
98
112
  const packageableFingerprints = await collectPackageableFingerprints(engineDir);
99
113
  const testInputFingerprints = await collectDirtyFingerprints(engineDir, () => true, 'test staging fingerprint');
100
114
  const buildInputFingerprints = await collectBuildInputFingerprints(engineDir, previousBaseline, buildKind);
101
- // A full-coverage write (and the legacy no-claim `fireforge build` shape)
115
+ // A full-coverage write (and a no-claim `fireforge build` record)
102
116
  // just recompiled the StaticComponents table, so it anchors the table to
103
117
  // the current engine state. A scoped write did not, so carry the previous
104
118
  // anchor forward verbatim.
@@ -121,6 +135,8 @@ export async function writeBuildBaseline(options) {
121
135
  ...(mozconfigHash !== undefined ? { mozconfigHash } : {}),
122
136
  ...(staticComponentsBaseline !== undefined ? { staticComponentsBaseline } : {}),
123
137
  ...(recordedBy !== undefined ? { recordedBy } : {}),
138
+ recorder: baselineRecorder(),
139
+ ...(controlBuild !== undefined ? { controlBuild } : {}),
124
140
  };
125
141
  await writeJson(getBuildBaselinePath(projectRoot), baseline);
126
142
  }
@@ -195,14 +211,14 @@ async function collectDirtyFingerprints(engineDir, includePath, contextLabel) {
195
211
  try {
196
212
  const dirtyPaths = new Set();
197
213
  if (await hasChanges(engineDir)) {
198
- const worktreeDiff = await git(['diff', '--name-only', 'HEAD'], engineDir);
199
- for (const line of worktreeDiff.split('\n')) {
200
- const trimmed = line.trim();
201
- if (trimmed)
202
- dirtyPaths.add(trimmed);
203
- }
204
- for (const untracked of await getUntrackedFiles(engineDir)) {
205
- dirtyPaths.add(untracked);
214
+ // NUL-separated and unquoted: git's default output backslash-quotes a
215
+ // non-ASCII path (`"a/\303\251.js"`), which is neither the file on
216
+ // disk nor a key the baseline validator accepts.
217
+ const worktreeDiff = await git(['-c', 'core.quotePath=false', 'diff', '--name-only', '-z', 'HEAD'], engineDir);
218
+ const untracked = await git(['-c', 'core.quotePath=false', 'ls-files', '--others', '--exclude-standard', '-z'], engineDir);
219
+ for (const path of `${worktreeDiff}\0${untracked}`.split('\0')) {
220
+ if (path.length > 0)
221
+ dirtyPaths.add(path);
206
222
  }
207
223
  }
208
224
  const included = [...dirtyPaths].filter(includePath);
@@ -2,10 +2,10 @@
2
2
  * Decides whether a changed `jar.mn` really forces the pre-test build to
3
3
  * escalate from `mach build faster` to a full `mach build`.
4
4
  *
5
- * The 0.41.0 rule was `any changed jar.mn ⇒ full build`, which costs ~10
6
- * minutes against ~20 s for a chrome-only slice and is paid twice per
7
- * green/no-change pair on a Furnace-touching slice. A downstream experiment
8
- * (two clean runs, 2026-08-29, against 0.44.4) showed the rule is over-broad
5
+ * Escalating on every changed jar.mn (`any changed jar.mn ⇒ full build`)
6
+ * costs ~10 minutes against ~20 s for a chrome-only slice and is paid twice
7
+ * per green/no-change pair on a Furnace-touching slice. A downstream
8
+ * experiment (two clean runs, 2026-08-29) showed that rule is over-broad
9
9
  * for the common case: a jar-only registration of a new content file added
10
10
  * to an existing `dist/bin` manifest was installed by a plain
11
11
  * `fireforge build --ui` both times: `faster/install_dist_bin_browser` named
@@ -3,10 +3,10 @@
3
3
  * Decides whether a changed `jar.mn` really forces the pre-test build to
4
4
  * escalate from `mach build faster` to a full `mach build`.
5
5
  *
6
- * The 0.41.0 rule was `any changed jar.mn ⇒ full build`, which costs ~10
7
- * minutes against ~20 s for a chrome-only slice and is paid twice per
8
- * green/no-change pair on a Furnace-touching slice. A downstream experiment
9
- * (two clean runs, 2026-08-29, against 0.44.4) showed the rule is over-broad
6
+ * Escalating on every changed jar.mn (`any changed jar.mn ⇒ full build`)
7
+ * costs ~10 minutes against ~20 s for a chrome-only slice and is paid twice
8
+ * per green/no-change pair on a Furnace-touching slice. A downstream
9
+ * experiment (two clean runs, 2026-08-29) showed that rule is over-broad
10
10
  * for the common case: a jar-only registration of a new content file added
11
11
  * to an existing `dist/bin` manifest was installed by a plain
12
12
  * `fireforge build --ui` both times: `faster/install_dist_bin_browser` named
@@ -122,7 +122,7 @@ async function decideJarEscalation(engineDir, changed) {
122
122
  // build. Path-shape only: `evaluateJarManifestEscalation` decides whether a
123
123
  // given changed manifest actually forces one (a new manifest, or one that
124
124
  // redirects its install base directory), so an entry added to an existing
125
- // `dist/bin` manifest no longer costs a ~10-minute build.
125
+ // `dist/bin` manifest does not cost a ~10-minute build.
126
126
  const decision = await evaluateJarManifestEscalation(engineDir, changed.filter(isJarManifestPath));
127
127
  if (decision.cleared.length > 0) {
128
128
  verbose(`jar escalation: ${decision.cleared.length} changed jar.mn add entries to an existing dist/bin manifest; not escalating for ${decision.cleared.join(', ')}.`);
@@ -241,9 +241,9 @@ async function applyFurnaceBeforeBuild(projectRoot, only) {
241
241
  }
242
242
  // The apply computed these (patch-owned engine files whose content
243
243
  // differed from components/ and were replaced). `furnace apply`
244
- // prints them; this path used to drop them, so a negative control
245
- // that edited a deployed Furnace file ran against the shipped code
246
- // with a green result and only a checksum could tell.
244
+ // prints them, and so does this path: dropped silently, a negative
245
+ // control that edited a deployed Furnace file would run against the
246
+ // shipped code with a green result and only a checksum could tell.
247
247
  const overwriteCount = logApplyWarnings(result.warnings);
248
248
  if (overwriteCount > 0) {
249
249
  notice(`Furnace: ${overwriteCount} Furnace-managed engine file${overwriteCount === 1 ? '' : 's'} differed from components/ and ${overwriteCount === 1 ? 'was' : 'were'} REPLACED before this build. An engine-side edit to a Furnace-managed file (for example a negative control) did not run; edit the component source under components/ instead, or restore the edit after the build.`);
@@ -74,10 +74,10 @@ function parseFirefoxBlock(rec) {
74
74
  throw new ConfigError('Config field "firefox.version" must be a valid Firefox version (e.g., "145.0")');
75
75
  }
76
76
  const firefoxProduct = requireConfigString(firefoxRec, 'product', 'firefox.product');
77
- // Uses the shared guard rather than a fourth copy of the member list. This
78
- // is the fireforge.json read path: a product added to the union but not to
79
- // the copy that used to live here would have been silently rejected from
80
- // every project config.
77
+ // Uses the shared guard rather than a separate copy of the member list.
78
+ // This is the fireforge.json read path: a product added to the union but
79
+ // not to a local copy would be silently rejected from every project
80
+ // config.
81
81
  if (!isValidFirefoxProduct(firefoxProduct)) {
82
82
  throw new ConfigError(`Config field "firefox.product" must be one of: ${FIREFOX_PRODUCTS.join(', ')}`);
83
83
  }
@@ -7,7 +7,7 @@
7
7
  * collapsing to this build's paths. `--extend-coverage` turns any drop into
8
8
  * a refusal before the build.
9
9
  *
10
- * Fingerprints must be complete: a legacy packageable-only record cannot
10
+ * Fingerprints must be complete: a record of packageable inputs only cannot
11
11
  * prove that a test fixture or manifest still matches its staged copy.
12
12
  */
13
13
  import type { BuildBaseline, CoverageAnchor, CoverageRecord, TestPackagingCoverage } from './build-baseline-types.js';
@@ -70,9 +70,11 @@ export interface RecordedCoverage {
70
70
  *
71
71
  * @param engineDir - Path to the engine directory
72
72
  * @param plan - Plan after {@link applyMozconfigAnchor}
73
- * @param requested - Normalized request paths this build packaged
73
+ * @param requestedPaths - Request paths this build packaged. Normalized
74
+ * here (no trailing slash, no `./`): the baseline validator rejects the
75
+ * raw CLI shapes, and a baseline it rejects refuses every later run.
74
76
  */
75
- export declare function recordScopedCoverage(engineDir: string, plan: CoverageRetentionPlan, requested: string[]): Promise<RecordedCoverage>;
77
+ export declare function recordScopedCoverage(engineDir: string, plan: CoverageRetentionPlan, requestedPaths: string[]): Promise<RecordedCoverage>;
76
78
  /** Renders one dropped claim with its cause. */
77
79
  export declare function describeDroppedClaim(claim: DroppedClaim): string;
78
80
  /**
@@ -8,7 +8,7 @@
8
8
  * collapsing to this build's paths. `--extend-coverage` turns any drop into
9
9
  * a refusal before the build.
10
10
  *
11
- * Fingerprints must be complete: a legacy packageable-only record cannot
11
+ * Fingerprints must be complete: a record of packageable inputs only cannot
12
12
  * prove that a test fixture or manifest still matches its staged copy.
13
13
  */
14
14
  import { toError } from '../utils/errors.js';
@@ -163,11 +163,16 @@ export async function applyMozconfigAnchor(engineDir, plan) {
163
163
  *
164
164
  * @param engineDir - Path to the engine directory
165
165
  * @param plan - Plan after {@link applyMozconfigAnchor}
166
- * @param requested - Normalized request paths this build packaged
166
+ * @param requestedPaths - Request paths this build packaged. Normalized
167
+ * here (no trailing slash, no `./`): the baseline validator rejects the
168
+ * raw CLI shapes, and a baseline it rejects refuses every later run.
167
169
  */
168
- export async function recordScopedCoverage(engineDir, plan, requested) {
170
+ export async function recordScopedCoverage(engineDir, plan, requestedPaths) {
169
171
  if (plan.keepFull)
170
172
  return { testPackagingCoverage: 'full' };
173
+ const requested = [
174
+ ...new Set(requestedPaths.map(normalizeRequestPath).filter((path) => path.length > 0)),
175
+ ];
171
176
  const records = { ...plan.retained };
172
177
  try {
173
178
  const state = await readEngineContentState(engineDir);
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * Per-path packaging-coverage anchors for scoped `fireforge test --build`.
3
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.
4
+ * Each staged request path carries its own anchor restricted to its own
5
+ * staging inputs, and a claim is dropped only when one of those inputs
6
+ * changed. One claim anchored to the whole engine would be replaced by any
7
+ * edit outside the new scope (a peer's, on a shared engine, nearly always).
9
8
  *
10
9
  * The staging inputs of a request path are its manifest directory (the
11
10
  * nearest directory at or above it that holds a test manifest) plus every
@@ -2,11 +2,10 @@
2
2
  /**
3
3
  * Per-path packaging-coverage anchors for scoped `fireforge test --build`.
4
4
  *
5
- * A scoped build used to carry one claim anchored to the whole engine, so
6
- * any edit outside the new scope (a peer's, on a shared engine, nearly
7
- * always) replaced it. Each staged request path now carries its own anchor
8
- * restricted to its own staging inputs, and a claim is dropped only when
9
- * one of those inputs changed.
5
+ * Each staged request path carries its own anchor restricted to its own
6
+ * staging inputs, and a claim is dropped only when one of those inputs
7
+ * changed. One claim anchored to the whole engine would be replaced by any
8
+ * edit outside the new scope (a peer's, on a shared engine, nearly always).
10
9
  *
11
10
  * The staging inputs of a request path are its manifest directory (the
12
11
  * nearest directory at or above it that holds a test manifest) plus every
@@ -128,8 +128,8 @@ export declare function confirmDestructive(input: DestructiveOpInput): Promise<D
128
128
  /**
129
129
  * Appends a single JSONL record to the project's patch history log
130
130
  * (`.fireforge/history.jsonl` unless `fireforge.json#history.path` says
131
- * otherwise; a log at the pre-0.50 `patches/.fireforge-history.jsonl` is
132
- * moved there first).
131
+ * otherwise; a log found at `patches/.fireforge-history.jsonl` is moved
132
+ * there first).
133
133
  *
134
134
  * Call order matters: append only after the mutation succeeds, never
135
135
  * pre-emptively, so rolled-back failures do not leave ghost entries. The log
@@ -159,8 +159,8 @@ export async function confirmDestructive(input) {
159
159
  /**
160
160
  * Appends a single JSONL record to the project's patch history log
161
161
  * (`.fireforge/history.jsonl` unless `fireforge.json#history.path` says
162
- * otherwise; a log at the pre-0.50 `patches/.fireforge-history.jsonl` is
163
- * moved there first).
162
+ * otherwise; a log found at `patches/.fireforge-history.jsonl` is moved
163
+ * there first).
164
164
  *
165
165
  * Call order matters: append only after the mutation succeeds, never
166
166
  * pre-emptively, so rolled-back failures do not leave ghost entries. The log
@@ -30,7 +30,7 @@ export interface DiscardBaselinePlan {
30
30
  /** Rename/copy only: baseline content for `originalPath` when that side is claimed. */
31
31
  expectedOriginalContent?: string | null;
32
32
  }
33
- /** Builds an all-unmanaged plan set, the `--to-upstream` (legacy) shape. */
33
+ /** Builds an all-unmanaged plan set, the `--to-upstream` shape. */
34
34
  export declare function planUpstreamDiscards(entries: readonly GitStatusEntry[]): DiscardBaselinePlan[];
35
35
  /**
36
36
  * Plans the restore target for each status entry. Entries claimed by the
@@ -47,7 +47,7 @@ export declare function planDiscardBaselines(patchesDir: string, engineDir: stri
47
47
  * Applies one restore plan. Patch-claimed paths end up exactly as `import`
48
48
  * leaves them: patched content as an unstaged worktree state (tracked files
49
49
  * read ` M`, created files `??`), nothing staged. Unmanaged plans delegate
50
- * to the legacy {@link discardStatusEntry} mechanics.
50
+ * to the plain git {@link discardStatusEntry} mechanics.
51
51
  */
52
52
  export declare function applyDiscardBaseline(engineDir: string, plan: DiscardBaselinePlan): Promise<void>;
53
53
  /** One-line annotation for dry-run listings and confirmation summaries. */
@@ -24,7 +24,7 @@ import { createPatchedContentContext } from './patch-apply.js';
24
24
  import { loadPatchesManifestState } from './patch-manifest-io.js';
25
25
  import { parseDiffSections } from './patch-parse.js';
26
26
  import { buildPatchClaims } from './status-classify.js';
27
- /** Builds an all-unmanaged plan set, the `--to-upstream` (legacy) shape. */
27
+ /** Builds an all-unmanaged plan set, the `--to-upstream` shape. */
28
28
  export function planUpstreamDiscards(entries) {
29
29
  return entries.map((entry) => ({ entry, kind: 'unmanaged', owners: [], conflicted: false }));
30
30
  }
@@ -151,7 +151,7 @@ export async function planDiscardBaselines(patchesDir, engineDir, entries) {
151
151
  * Applies one restore plan. Patch-claimed paths end up exactly as `import`
152
152
  * leaves them: patched content as an unstaged worktree state (tracked files
153
153
  * read ` M`, created files `??`), nothing staged. Unmanaged plans delegate
154
- * to the legacy {@link discardStatusEntry} mechanics.
154
+ * to the plain git {@link discardStatusEntry} mechanics.
155
155
  */
156
156
  export async function applyDiscardBaseline(engineDir, plan) {
157
157
  if (plan.kind === 'unmanaged') {
@@ -173,7 +173,7 @@ export async function applyDiscardBaseline(engineDir, plan) {
173
173
  await writeText(join(engineDir, entry.file), plan.expectedContent);
174
174
  }
175
175
  // A rename/copy's original side restores to its baseline too: the patch
176
- // baseline when claimed, else the legacy HEAD restore (matching what
176
+ // baseline when claimed, else the HEAD restore (matching what
177
177
  // discardStatusEntry does for that side).
178
178
  if (entry.originalPath !== undefined) {
179
179
  if (plan.expectedOriginalContent !== undefined) {
@@ -77,7 +77,7 @@ export declare function generationDeltaPaths(before: string, after: string): {
77
77
  /**
78
78
  * Describes what moved between two generation tokens, in operator terms.
79
79
  *
80
- * The refusal used to say only that the engine changed, which left the
80
+ * A refusal that said only that the engine changed would leave the
81
81
  * operator to reconstruct the writer by hand: `find -newermt` against the
82
82
  * run window, minus the objdir, minus git's own bookkeeping. Everything
83
83
  * needed to answer it is already in the two tokens, so the refusal answers
@@ -281,7 +281,7 @@ export function generationDeltaPaths(before, after) {
281
281
  /**
282
282
  * Describes what moved between two generation tokens, in operator terms.
283
283
  *
284
- * The refusal used to say only that the engine changed, which left the
284
+ * A refusal that said only that the engine changed would leave the
285
285
  * operator to reconstruct the writer by hand: `find -newermt` against the
286
286
  * run window, minus the objdir, minus git's own bookkeeping. Everything
287
287
  * needed to answer it is already in the two tokens, so the refusal answers
@@ -46,11 +46,11 @@ export declare function readLockOwner(lockPath: string): Promise<LockOwner>;
46
46
  * time, own start tick, diagnostic metadata) into a lock directory it has
47
47
  * just created, and reads the record back to prove it landed.
48
48
  *
49
- * The write is fatal. It used to be best-effort, on the theory that stale
50
- * recovery would fall back to the age heuristic, but that heuristic reaps
51
- * any owner-less lock after five minutes, and the build lock legitimately
52
- * holds for hours. A holder whose record failed to write was therefore
53
- * reaped mid-build and a second process entered the critical section. On
49
+ * The write is fatal rather than best-effort. Stale recovery could fall
50
+ * back to the age heuristic, but that heuristic reaps any owner-less lock
51
+ * after five minutes, and the build lock legitimately holds for hours. A
52
+ * holder whose record failed to write would be reaped mid-build and a
53
+ * second process would enter the critical section. On
54
54
  * failure the just-created lock directory is removed (nothing else can own
55
55
  * it yet) and a {@link GeneralError} names the cause.
56
56
  *
@@ -141,11 +141,11 @@ async function readBackLockOwner(lockPath) {
141
141
  * time, own start tick, diagnostic metadata) into a lock directory it has
142
142
  * just created, and reads the record back to prove it landed.
143
143
  *
144
- * The write is fatal. It used to be best-effort, on the theory that stale
145
- * recovery would fall back to the age heuristic, but that heuristic reaps
146
- * any owner-less lock after five minutes, and the build lock legitimately
147
- * holds for hours. A holder whose record failed to write was therefore
148
- * reaped mid-build and a second process entered the critical section. On
144
+ * The write is fatal rather than best-effort. Stale recovery could fall
145
+ * back to the age heuristic, but that heuristic reaps any owner-less lock
146
+ * after five minutes, and the build lock legitimately holds for hours. A
147
+ * holder whose record failed to write would be reaped mid-build and a
148
+ * second process would enter the critical section. On
149
149
  * failure the just-created lock directory is removed (nothing else can own
150
150
  * it yet) and a {@link GeneralError} names the cause.
151
151
  *
@@ -3,18 +3,16 @@
3
3
  * Waiter tickets for `withFileLock`: who is queued for a contended lock, in
4
4
  * what order, and whose turn it is.
5
5
  *
6
- * The queue started as a display: waiters advertised themselves so the wait
7
- * line could say "You are next", but the grant itself was a free-for-all of
8
- * `mkdir` polls. A waiter whose poll interval had backed off to 2 s lost
9
- * every free moment to later arrivals polling at 100 ms, so a run told it
10
- * was next for twenty minutes watched three later runs take the lock in
11
- * turn. The tickets now decide the grant: only the head of the queue may
12
- * attempt the lock, so the position printed is the position honoured.
6
+ * The tickets decide the grant: only the head of the queue may attempt the
7
+ * lock, so the position printed is the position honoured. A free-for-all of
8
+ * `mkdir` polls would let later arrivals polling at 100 ms take every free
9
+ * moment from a waiter whose poll interval had backed off to 2 s, so a run
10
+ * told it was next could watch later runs take the lock in turn.
13
11
  *
14
12
  * A ticket is a file in `<lock>.waiters/` named
15
13
  * `<startedAtMs>-<pid>-<uuid>`. Arrival order is the wait start, with the
16
14
  * whole name as a tie-break so every reader sorts the queue identically.
17
- * Because a ticket now blocks everyone behind it, a ticket whose owner is
15
+ * Because a ticket blocks everyone behind it, a ticket whose owner is
18
16
  * gone must not hold the queue: a dead PID (or a reused one, where the
19
17
  * start-tick check can tell) drops out and its ticket is removed, and a
20
18
  * ticket its owner has stopped refreshing drops out without being removed,
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Shared holds on a FireForge directory lock.
3
3
  *
4
- * Every lock used to be exclusive, so two build-less `fireforge test` runs
5
- * on one objdir, each with its own profile and (under auto ports) its own
6
- * ports, still waited for each other: the gate's browser tiers ran strictly
7
- * serially on a 14-core host. A shared hold lets such runs overlap while
4
+ * Two build-less `fireforge test` runs on one objdir, each with its own
5
+ * profile and (under auto ports) its own ports, have no reason to wait for
6
+ * each other; with only exclusive holds the gate's browser tiers would run
7
+ * strictly serially on a many-core host. A shared hold lets such runs overlap while
8
8
  * anything that writes (a build, a re-export, a test run that builds)
9
9
  * still holds the lock alone.
10
10
  *
@@ -2,10 +2,10 @@
2
2
  /**
3
3
  * Shared holds on a FireForge directory lock.
4
4
  *
5
- * Every lock used to be exclusive, so two build-less `fireforge test` runs
6
- * on one objdir, each with its own profile and (under auto ports) its own
7
- * ports, still waited for each other: the gate's browser tiers ran strictly
8
- * serially on a 14-core host. A shared hold lets such runs overlap while
5
+ * Two build-less `fireforge test` runs on one objdir, each with its own
6
+ * profile and (under auto ports) its own ports, have no reason to wait for
7
+ * each other; with only exclusive holds the gate's browser tiers would run
8
+ * strictly serially on a many-core host. A shared hold lets such runs overlap while
9
9
  * anything that writes (a build, a re-export, a test run that builds)
10
10
  * still holds the lock alone.
11
11
  *
@@ -269,10 +269,9 @@ async function buildLockTimeoutError(lockPath, onTimeoutMessage, cause, queue) {
269
269
  *
270
270
  * The queue is read at the moment the budget expires rather than taken from
271
271
  * the last progress probe. A probe snapshot is up to one `waitProgressMs`
272
- * stale, and, the reason this changed, it may not exist at all: a short
273
- * budget can expire before the first probe lands (a slow runner's
274
- * `registerWaiter` alone can outlast it), and a caller that configured no
275
- * probe at all never had one. Both used to lose the position silently.
272
+ * stale, and it may not exist at all: a short budget can expire before the
273
+ * first probe lands (a slow runner's `registerWaiter` alone can outlast it),
274
+ * and a caller that configured no probe at all never has one.
276
275
  */
277
276
  function formatQueuePositionReached(queue) {
278
277
  if (queue.depth === 0)
@@ -423,12 +422,10 @@ export async function withFileLock(lockPath, operation, options = {}) {
423
422
  // buys a fresh budget, because the wait is working. Only a queue
424
423
  // that stops moving starves, which is the case the timeout is for.
425
424
  //
426
- // The first observation is a seed, never an advance. `bestAhead`
427
- // used to start `undefined`, so the opening probe always satisfied
428
- // this test and handed out a free full budget: a `--wait-lock 300`
429
- // run outlived its 300 s even against a queue that never moved
430
- // once, which is the "it does not reliably give up after N
431
- // seconds" behaviour a downstream fork reported.
425
+ // The first observation is a seed, never an advance. Counted as an
426
+ // advance, the opening probe would hand out a free full budget: a
427
+ // `--wait-lock 300` run would outlive its 300 s even against a queue
428
+ // that never moved once.
432
429
  if (hardDeadline !== undefined) {
433
430
  if (bestAhead === undefined) {
434
431
  bestAhead = probe.queue.ahead;
@@ -96,8 +96,8 @@ export async function downloadFirefoxSource(options) {
96
96
  await removeDir(tempDir);
97
97
  // Only throw away the ~600 MB cached archive when the failure indicts
98
98
  // the archive, not the environment. ENOSPC, a permission problem in
99
- // destDir, or tar disappearing mid-run used to invalidate a perfectly
100
- // good download and force a full re-fetch on retry.
99
+ // destDir, or tar disappearing mid-run leaves a perfectly good download
100
+ // that a retry can reuse without a full re-fetch.
101
101
  if (isLikelyArchiveCorruptionError(error)) {
102
102
  await invalidateArchiveCache(archive, cacheDir);
103
103
  }
@@ -13,7 +13,7 @@
13
13
  * The per-file byte comparison already exists:
14
14
  * `checkRegistrationConsistency` for custom components (fragment- and
15
15
  * ftl-aware) and the copy-candidate walk for overrides. This module only
16
- * surfaces what was previously computed and thrown away.
16
+ * surfaces what those comparisons compute.
17
17
  */
18
18
  import type { CustomComponentConfig, OverrideComponentConfig } from '../types/furnace.js';
19
19
  /** One deployed, patch-owned file about to be overwritten with different bytes. */
@@ -14,7 +14,7 @@
14
14
  * The per-file byte comparison already exists:
15
15
  * `checkRegistrationConsistency` for custom components (fragment- and
16
16
  * ftl-aware) and the copy-candidate walk for overrides. This module only
17
- * surfaces what was previously computed and thrown away.
17
+ * surfaces what those comparisons compute.
18
18
  */
19
19
  import { readdir } from 'node:fs/promises';
20
20
  import { join, relative } from 'node:path';
@@ -258,9 +258,9 @@ async function applyCustomBatch(ctx, root, markerComment) {
258
258
  filesAffectedTotal.push(...removed);
259
259
  }
260
260
  if (!dryRun) {
261
- // Runs unconditionally. The `changed === true` path used to skip
262
- // drift detection entirely, which is when a deployed engine-only
263
- // fix gets silently replaced (J6).
261
+ // Runs unconditionally, including on the `changed === true` path:
262
+ // that is when a deployed engine-only fix gets silently replaced
263
+ // (J6).
264
264
  recordOverwriteWarnings(result, await findPatchOwnedOverwrites({
265
265
  type: 'custom',
266
266
  root,
@@ -1,8 +1,9 @@
1
1
  import { type FurnaceOptInCheck, type FurnaceValidationException } from '../types/furnace.js';
2
2
  /**
3
- * Parses `validationExceptions[]`: `{ component, check, tag?, reason }`
4
- * objects, no other keys, every field a non-empty string, and no two
5
- * entries for the same component, check and tag.
3
+ * Parses `validationExceptions[]`: `{ component, check, tag?, file?, reason }`
4
+ * objects, no other keys, every field a non-empty string, at most one of
5
+ * `tag` and `file` (a bare file name of the component), and no two entries
6
+ * for the same component, check and scope.
6
7
  */
7
8
  export declare function parseValidationExceptions(raw: unknown): FurnaceValidationException[];
8
9
  /**