@hominis/fireforge 0.42.0 → 0.43.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 (199) hide show
  1. package/CHANGELOG.md +75 -34
  2. package/dist/build-info.json +4 -4
  3. package/dist/src/cli.js +21 -2
  4. package/dist/src/commands/build.js +7 -1
  5. package/dist/src/commands/config.js +16 -27
  6. package/dist/src/commands/discard.js +1 -1
  7. package/dist/src/commands/export-flow.js +13 -3
  8. package/dist/src/commands/export-placement-conflicts.js +1 -1
  9. package/dist/src/commands/export-shared.d.ts +1 -1
  10. package/dist/src/commands/export-shared.js +4 -5
  11. package/dist/src/commands/furnace/chrome-doc-remove.js +3 -0
  12. package/dist/src/commands/furnace/chrome-doc.js +3 -0
  13. package/dist/src/commands/furnace/create.js +3 -0
  14. package/dist/src/commands/furnace/override.js +3 -0
  15. package/dist/src/commands/furnace/preview.js +3 -0
  16. package/dist/src/commands/furnace/refresh.js +3 -0
  17. package/dist/src/commands/furnace/remove.js +3 -0
  18. package/dist/src/commands/furnace/rename.js +3 -0
  19. package/dist/src/commands/furnace/scan.js +3 -0
  20. package/dist/src/commands/import.js +1 -1
  21. package/dist/src/commands/lint-per-patch.js +16 -8
  22. package/dist/src/commands/lint-per-run-checkjs.d.ts +4 -4
  23. package/dist/src/commands/lint-per-run-checkjs.js +4 -4
  24. package/dist/src/commands/lint-report.js +1 -1
  25. package/dist/src/commands/lint.js +2 -2
  26. package/dist/src/commands/patch/lint-ignore.js +1 -1
  27. package/dist/src/commands/patch/move-files-create.js +5 -3
  28. package/dist/src/commands/patch/move-files-into.d.ts +1 -1
  29. package/dist/src/commands/patch/move-files-into.js +5 -3
  30. package/dist/src/commands/patch/move-files.js +1 -1
  31. package/dist/src/commands/patch/rename.js +1 -1
  32. package/dist/src/commands/patch/reorder.d.ts +1 -2
  33. package/dist/src/commands/patch/reorder.js +1 -2
  34. package/dist/src/commands/patch/split.js +5 -3
  35. package/dist/src/commands/patch/staged-dependency-validate.js +1 -1
  36. package/dist/src/commands/patch/staged-dependency.js +4 -4
  37. package/dist/src/commands/re-export-adjacent.js +1 -1
  38. package/dist/src/commands/re-export-bulk-scan.d.ts +1 -1
  39. package/dist/src/commands/re-export-bulk-scan.js +5 -4
  40. package/dist/src/commands/re-export-drift.d.ts +4 -4
  41. package/dist/src/commands/re-export-drift.js +47 -8
  42. package/dist/src/commands/re-export-lint.d.ts +7 -5
  43. package/dist/src/commands/re-export-lint.js +9 -8
  44. package/dist/src/commands/re-export-single.d.ts +1 -1
  45. package/dist/src/commands/re-export-single.js +5 -4
  46. package/dist/src/commands/re-export.js +26 -14
  47. package/dist/src/commands/reset.js +1 -1
  48. package/dist/src/commands/run.js +1 -1
  49. package/dist/src/commands/source.js +6 -6
  50. package/dist/src/commands/status-check.d.ts +1 -1
  51. package/dist/src/commands/status-check.js +1 -1
  52. package/dist/src/commands/status-json.d.ts +2 -2
  53. package/dist/src/commands/status-json.js +1 -1
  54. package/dist/src/commands/status-output.d.ts +1 -1
  55. package/dist/src/commands/status-output.js +1 -1
  56. package/dist/src/commands/status.js +21 -7
  57. package/dist/src/commands/test-build-phase.d.ts +2 -2
  58. package/dist/src/commands/test-build-phase.js +9 -6
  59. package/dist/src/commands/test-diagnose.js +8 -1
  60. package/dist/src/commands/test-modes.d.ts +2 -2
  61. package/dist/src/commands/test-modes.js +3 -3
  62. package/dist/src/commands/test-register.js +7 -2
  63. package/dist/src/commands/test-run.d.ts +6 -0
  64. package/dist/src/commands/test-run.js +32 -3
  65. package/dist/src/commands/test-stale-gate.js +7 -2
  66. package/dist/src/commands/test.js +4 -4
  67. package/dist/src/commands/tree.js +2 -2
  68. package/dist/src/commands/typecheck.d.ts +2 -1
  69. package/dist/src/commands/typecheck.js +59 -3
  70. package/dist/src/commands/verify.js +26 -1
  71. package/dist/src/core/ast-utils.js +14 -2
  72. package/dist/src/core/browser-wire.js +9 -1
  73. package/dist/src/core/build-baseline-types.d.ts +1 -1
  74. package/dist/src/core/build-baseline.d.ts +1 -1
  75. package/dist/src/core/build-baseline.js +1 -1
  76. package/dist/src/core/build-overwrite-guard.d.ts +49 -0
  77. package/dist/src/core/build-overwrite-guard.js +112 -0
  78. package/dist/src/core/build-prepare.d.ts +13 -5
  79. package/dist/src/core/build-prepare.js +42 -9
  80. package/dist/src/core/config-mutate.d.ts +3 -2
  81. package/dist/src/core/config-mutate.js +5 -2
  82. package/dist/src/core/config.d.ts +3 -2
  83. package/dist/src/core/config.js +2 -0
  84. package/dist/src/core/coverage-extend.d.ts +1 -1
  85. package/dist/src/core/coverage-extend.js +1 -1
  86. package/dist/src/core/destructive.d.ts +23 -0
  87. package/dist/src/core/destructive.js +45 -4
  88. package/dist/src/core/diff-hunks.js +31 -17
  89. package/dist/src/core/discard-baseline.d.ts +1 -1
  90. package/dist/src/core/discard-baseline.js +1 -1
  91. package/dist/src/core/display-state.d.ts +52 -0
  92. package/dist/src/core/display-state.js +89 -0
  93. package/dist/src/core/engine-session-lock.d.ts +20 -0
  94. package/dist/src/core/engine-session-lock.js +84 -14
  95. package/dist/src/core/export-moved-code-shape.d.ts +60 -0
  96. package/dist/src/core/export-moved-code-shape.js +129 -0
  97. package/dist/src/core/file-lock.d.ts +44 -0
  98. package/dist/src/core/file-lock.js +157 -42
  99. package/dist/src/core/furnace-apply-helpers.d.ts +1 -1
  100. package/dist/src/core/furnace-apply-helpers.js +1 -1
  101. package/dist/src/core/furnace-apply-output.js +1 -1
  102. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +2 -2
  103. package/dist/src/core/furnace-apply-overwrite-warn.js +2 -2
  104. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +1 -1
  105. package/dist/src/core/furnace-apply-undeploy-actions.js +1 -1
  106. package/dist/src/core/furnace-apply.js +13 -1
  107. package/dist/src/core/furnace-config-custom.d.ts +2 -1
  108. package/dist/src/core/furnace-config-order.d.ts +2 -1
  109. package/dist/src/core/furnace-config-order.js +9 -4
  110. package/dist/src/core/furnace-config-validate.d.ts +4 -3
  111. package/dist/src/core/furnace-config-validate.js +3 -3
  112. package/dist/src/core/furnace-config.js +5 -0
  113. package/dist/src/core/furnace-operation.d.ts +12 -0
  114. package/dist/src/core/furnace-operation.js +125 -1
  115. package/dist/src/core/furnace-rollback.d.ts +17 -0
  116. package/dist/src/core/furnace-rollback.js +35 -1
  117. package/dist/src/core/git-base.js +7 -2
  118. package/dist/src/core/git-diff.js +11 -2
  119. package/dist/src/core/git-readonly-index.d.ts +59 -0
  120. package/dist/src/core/git-readonly-index.js +137 -0
  121. package/dist/src/core/license-headers.d.ts +1 -1
  122. package/dist/src/core/license-headers.js +1 -1
  123. package/dist/src/core/lint-cache.d.ts +15 -5
  124. package/dist/src/core/lint-cache.js +35 -8
  125. package/dist/src/core/mach-known-noise-filter.d.ts +21 -0
  126. package/dist/src/core/mach-known-noise-filter.js +23 -0
  127. package/dist/src/core/mach-mozconfig.js +12 -2
  128. package/dist/src/core/module-resolution-preflight.d.ts +51 -0
  129. package/dist/src/core/module-resolution-preflight.js +155 -0
  130. package/dist/src/core/patch-export.js +2 -1
  131. package/dist/src/core/patch-identifier-suggest.js +29 -3
  132. package/dist/src/core/patch-lint-checkjs.d.ts +5 -5
  133. package/dist/src/core/patch-lint-checkjs.js +6 -6
  134. package/dist/src/core/patch-lint-creators.d.ts +1 -1
  135. package/dist/src/core/patch-lint-creators.js +1 -1
  136. package/dist/src/core/patch-lint-cross.d.ts +1 -1
  137. package/dist/src/core/patch-lint-cross.js +4 -4
  138. package/dist/src/core/patch-lint-css.js +1 -1
  139. package/dist/src/core/patch-lint-diff.d.ts +1 -1
  140. package/dist/src/core/patch-lint-diff.js +1 -1
  141. package/dist/src/core/patch-lint-jsdoc.js +4 -2
  142. package/dist/src/core/patch-lint-module-registration.d.ts +13 -0
  143. package/dist/src/core/patch-lint-module-registration.js +13 -4
  144. package/dist/src/core/patch-lint-mozbuild.d.ts +1 -1
  145. package/dist/src/core/patch-lint-mozbuild.js +1 -1
  146. package/dist/src/core/patch-lint-ownership.d.ts +2 -2
  147. package/dist/src/core/patch-lint-ownership.js +2 -2
  148. package/dist/src/core/patch-lint.d.ts +2 -2
  149. package/dist/src/core/patch-lint.js +3 -3
  150. package/dist/src/core/patch-lock.d.ts +21 -0
  151. package/dist/src/core/patch-lock.js +29 -2
  152. package/dist/src/core/patch-manifest-io.d.ts +2 -1
  153. package/dist/src/core/patch-manifest-io.js +49 -0
  154. package/dist/src/core/patch-parse.d.ts +18 -0
  155. package/dist/src/core/patch-parse.js +24 -1
  156. package/dist/src/core/patch-policy.d.ts +2 -2
  157. package/dist/src/core/patch-policy.js +2 -2
  158. package/dist/src/core/smoke-patterns.d.ts +1 -1
  159. package/dist/src/core/status-binary.d.ts +1 -1
  160. package/dist/src/core/status-binary.js +1 -1
  161. package/dist/src/core/status-classify.d.ts +1 -1
  162. package/dist/src/core/status-classify.js +2 -2
  163. package/dist/src/core/test-harness-crash.d.ts +69 -3
  164. package/dist/src/core/test-harness-crash.js +174 -9
  165. package/dist/src/core/test-stale-check.d.ts +1 -1
  166. package/dist/src/core/test-stale-check.js +1 -1
  167. package/dist/src/core/token-category.js +1 -1
  168. package/dist/src/core/token-manager.d.ts +1 -1
  169. package/dist/src/core/token-manager.js +5 -5
  170. package/dist/src/core/tree-cow.js +1 -1
  171. package/dist/src/core/tree-guard.js +2 -2
  172. package/dist/src/core/tree-store.js +1 -1
  173. package/dist/src/core/typecheck-shim.d.ts +2 -2
  174. package/dist/src/core/typecheck-shim.js +2 -2
  175. package/dist/src/core/typecheck.js +10 -0
  176. package/dist/src/core/xpcshell-profile-dir.js +1 -1
  177. package/dist/src/errors/base.d.ts +22 -1
  178. package/dist/src/errors/base.js +32 -1
  179. package/dist/src/errors/codes.d.ts +10 -0
  180. package/dist/src/errors/codes.js +10 -0
  181. package/dist/src/types/commands/options.d.ts +27 -11
  182. package/dist/src/types/commands/patches.d.ts +1 -1
  183. package/dist/src/types/config.d.ts +2 -2
  184. package/dist/src/types/furnace.d.ts +1 -1
  185. package/dist/src/types/json.d.ts +22 -0
  186. package/dist/src/types/json.js +11 -0
  187. package/dist/src/utils/assert.d.ts +46 -0
  188. package/dist/src/utils/assert.js +83 -0
  189. package/dist/src/utils/build-info.d.ts +1 -1
  190. package/dist/src/utils/build-info.js +1 -1
  191. package/dist/src/utils/logger.d.ts +20 -0
  192. package/dist/src/utils/logger.js +26 -0
  193. package/dist/src/utils/options.d.ts +12 -0
  194. package/dist/src/utils/options.js +39 -0
  195. package/dist/src/utils/stdio-drain.d.ts +1 -1
  196. package/dist/src/utils/stdio-drain.js +1 -1
  197. package/dist/src/utils/validation.d.ts +12 -1
  198. package/dist/src/utils/validation.js +13 -1
  199. package/package.json +3 -3
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Pre-test build phase for `fireforge test`, split out of `test.ts` (at
4
4
  * the per-file line budget): the incremental protected mach build with
5
- * its baseline write, and the FORGE J9 `--build-only` union-build shape.
5
+ * its baseline write, and the `--build-only` union-build shape.
6
6
  */
7
7
  import { readBuildBaseline, writeBuildBaseline } from '../core/build-baseline.js';
8
8
  import { prepareBuildEnvironment } from '../core/build-prepare.js';
@@ -12,7 +12,7 @@ import { buildHarnessCrashMessage } from '../core/test-harness-crash.js';
12
12
  import { GeneralError, InvalidArgumentError } from '../errors/base.js';
13
13
  import { BuildError } from '../errors/build.js';
14
14
  import { toError } from '../utils/errors.js';
15
- import { info, spinner, verbose } from '../utils/logger.js';
15
+ import { info, notice, spinner, verbose } from '../utils/logger.js';
16
16
  import { reportBuildOnlyCompletion } from './test-modes.js';
17
17
  import { enforceStaleBuildGate, enforceStaticComponentsGate } from './test-stale-gate.js';
18
18
  import { emitPassVerdict } from './test-verdict.js';
@@ -28,7 +28,7 @@ async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries
28
28
  const preparation = await prepareBuildEnvironment(projectRoot, paths, projectConfig, {
29
29
  previousBaseline,
30
30
  });
31
- // The mozconfig half of the --extend-coverage anchor (FORGE L1) can
31
+ // The mozconfig half of the --extend-coverage anchor can
32
32
  // only be checked once prepareBuildEnvironment has regenerated
33
33
  // engine/mozconfig — that is the file this build will configure with —
34
34
  // and must still be checked before mach, so a refusal costs no build.
@@ -40,7 +40,10 @@ async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries
40
40
  }
41
41
  const buildKind = preparation.fullBuildRequired ? 'full' : 'faster';
42
42
  if (preparation.fullBuildRequired) {
43
- info('A jar.mn registration changed since the last successful build; escalating this pre-test build to a full mach build so new install-manifest destinations are created.');
43
+ // Warning severity, not info: agent-facing output filters
44
+ // keep warnings and errors only, and this is the line that explains an
45
+ // otherwise unexplained multi-minute build.
46
+ notice('A jar.mn registration changed since the last successful build; escalating this pre-test build to a full mach build so new install-manifest destinations are created.');
44
47
  }
45
48
  const s = spinner(preparation.fullBuildRequired
46
49
  ? 'Running required full build...'
@@ -120,7 +123,7 @@ function describeBuildInvocation(extending, coverage) {
120
123
  /**
121
124
  * Runs the pre-test build (or the stale-build gate when no build was
122
125
  * requested). Returns `true` when the run is COMPLETE — the `--build-only`
123
- * union-build shape (FORGE J9), which packages every requested path
126
+ * union-build shape, which packages every requested path
124
127
  * (mixed harnesses legal, nothing dispatches), prints the per-harness
125
128
  * next steps, and emits the run's single `PASS` verdict.
126
129
  */
@@ -152,7 +155,7 @@ export async function runTestBuildPhase(projectRoot, paths, projectConfig, harne
152
155
  await runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries, coverage, extending ? { requestedPaths: normalizedPaths } : undefined);
153
156
  info('');
154
157
  if (options.buildOnly) {
155
- // Union build for mixed harnesses (FORGE J9): the coverage claim
158
+ // Union build for mixed harnesses: the coverage claim
156
159
  // above lists every requested path, so each harness half can run
157
160
  // build-less against this packaging. Nothing dispatches here; the
158
161
  // run still ends with exactly one verdict line.
@@ -7,7 +7,7 @@
7
7
  * `test-harness-crash.ts` for single and sharded invocations. Split out of
8
8
  * `test.ts` to keep both files within the per-file line budget.
9
9
  */
10
- import { buildGreenSummaryRejectedMessage, buildHarnessCrashMessage, buildNoTestsRanMessage, headedNoOutputTimeoutHint, } from '../core/test-harness-crash.js';
10
+ import { buildGreenSummaryRejectedMessage, buildHarnessCrashMessage, buildNoTestsRanMessage, buildSilentSegfaultMessage, headedNoOutputTimeoutHint, isSilentSegfault, } from '../core/test-harness-crash.js';
11
11
  import { buildHarnessEarlyExitMessage, classifyHarnessEarlyExit, completePostRebuildFailureContext, prependPostRebuildFailureContext, } from '../core/test-harness-output.js';
12
12
  import { GeneralError } from '../errors/base.js';
13
13
  import { BuildError } from '../errors/build.js';
@@ -144,6 +144,12 @@ function handleNonZeroTestExit(result, normalizedPaths, appdirInjectionAttempted
144
144
  const throwGeneral = (message) => {
145
145
  throw new GeneralError(withContext(message));
146
146
  };
147
+ // First, because there is nothing else to go on: a SIGSEGV with zero
148
+ // output has exactly one known cause and no other evidence.
149
+ // Every branch below keys on output text this shape does not have.
150
+ if (isSilentSegfault(result.exitCode, combinedOutput)) {
151
+ throwGeneral(buildSilentSegfaultMessage(result.exitCode, normalizedPaths));
152
+ }
147
153
  if (/UNKNOWN TEST\b/i.test(combinedOutput)) {
148
154
  throwGeneral(buildUnknownTestMessage(normalizedPaths));
149
155
  }
@@ -214,6 +220,7 @@ function applySingleRunOutcome(outcome, normalizedPaths, binaryName, postRebuild
214
220
  const hint = headedNoOutputTimeoutHint(outcome.verdict.signature, {
215
221
  headless,
216
222
  platform: getPlatform(),
223
+ ...(outcome.displayState !== undefined ? { displayState: outcome.displayState } : {}),
217
224
  });
218
225
  throw new GeneralError(hint ? `${base}\n\n${hint}` : base);
219
226
  }
@@ -21,13 +21,13 @@ export interface HarnessClassification {
21
21
  * Harness classification is a pure path→manifest lookup and must precede
22
22
  * the build dispatch: a mixed xpcshell+mochitest request can never
23
23
  * dispatch, so refusing it before spending minutes in a pre-test build
24
- * is strictly better (FORGE F7). A nonexistent path classifies as
24
+ * is strictly better. A nonexistent path classifies as
25
25
  * nonXpcshell (findNearestXpcshellManifest returns null), so classifying
26
26
  * before assertTestPathsExist is harmless — existence is still asserted
27
27
  * after the stale/coverage gates, preserving their precedence over
28
28
  * missing-path errors. `xpcshellOnly` (non-empty request, zero
29
29
  * nonXpcshell paths) gates the Marionette preflight and the mochitest
30
- * client flags (FORGE F10); pathless runs keep the full-suite behavior.
30
+ * client flags; pathless runs keep the full-suite behavior.
31
31
  */
32
32
  export declare function classifyBeforeDispatch(engineDir: string, normalizedPaths: string[], options?: {
33
33
  allowMixed?: boolean;
@@ -96,19 +96,19 @@ function buildMixedHarnessMessage(classification) {
96
96
  * Harness classification is a pure path→manifest lookup and must precede
97
97
  * the build dispatch: a mixed xpcshell+mochitest request can never
98
98
  * dispatch, so refusing it before spending minutes in a pre-test build
99
- * is strictly better (FORGE F7). A nonexistent path classifies as
99
+ * is strictly better. A nonexistent path classifies as
100
100
  * nonXpcshell (findNearestXpcshellManifest returns null), so classifying
101
101
  * before assertTestPathsExist is harmless — existence is still asserted
102
102
  * after the stale/coverage gates, preserving their precedence over
103
103
  * missing-path errors. `xpcshellOnly` (non-empty request, zero
104
104
  * nonXpcshell paths) gates the Marionette preflight and the mochitest
105
- * client flags (FORGE F10); pathless runs keep the full-suite behavior.
105
+ * client flags; pathless runs keep the full-suite behavior.
106
106
  */
107
107
  export async function classifyBeforeDispatch(engineDir, normalizedPaths, options) {
108
108
  const classification = await classifyTestHarnesses(engineDir, normalizedPaths);
109
109
  if (classification.xpcshell.length > 0 && classification.nonXpcshell.length > 0) {
110
110
  // `--build-only` never dispatches, so a mixed request is legal there —
111
- // packaging the union is the whole point (FORGE J9).
111
+ // packaging the union is the whole point.
112
112
  if (options?.allowMixed !== true) {
113
113
  throw new GeneralError(buildMixedHarnessMessage(classification));
114
114
  }
@@ -29,7 +29,7 @@ const TEST_HELP_TEXT = [
29
29
  '--doctor) ends with exactly one raw stdout line',
30
30
  ' FIREFORGE-VERDICT: PASS|FAIL [reason=crash|test-failures|',
31
31
  ' no-tests|preflight|inconclusive|lock-timeout] [checks=<n>]',
32
- ' [unexpected=<n>] [shards=<p>/<t>]',
32
+ ' [unexpected=<n>] [shards=<p>/<t>] [(<note>)]',
33
33
  'The status follows the harness classifier, not the raw exit code:',
34
34
  'a crash-classified run says FAIL reason=crash even at exit 0, and',
35
35
  'a green-summary-override pass says PASS despite a non-zero mach',
@@ -41,7 +41,12 @@ const TEST_HELP_TEXT = [
41
41
  'engine-mutating command held the lock past the wait budget. A sharded',
42
42
  'aggregate reason is the most structural failing shard (crash,',
43
43
  'then no-tests, then test-failures). Count keys are omitted when',
44
- 'the embedded summary did not print them. Key on this line',
44
+ 'the embedded summary did not print them. A trailing parenthetical',
45
+ 'is an advisory note, never part of the status: "(harness teardown',
46
+ 'noise ignored)" marks a clean suite whose only residue was the',
47
+ 'recognized upstream mozsystemmonitor teardown traceback, and',
48
+ '"(headed run stalled with the display asleep)" marks a no-output',
49
+ 'stall whose display was measured asleep. Key on this line',
45
50
  'instead of mach internals.',
46
51
  ].join('\n');
47
52
  /** Registers the test command on the CLI program. */
@@ -10,6 +10,7 @@
10
10
  * harness runs cost startup time but make results reproducible; the
11
11
  * combined invocation stays available via `--no-shard`.
12
12
  */
13
+ import { type DisplaySleepState } from '../core/display-state.js';
13
14
  import { type MachCommandResult } from '../core/mach.js';
14
15
  import { type HarnessRunVerdict } from '../core/test-harness-crash.js';
15
16
  /** Default bounded retry budget for recognized harness crashes. */
@@ -48,6 +49,11 @@ export interface TestRunOutcome {
48
49
  attempts: number;
49
50
  /** Whether xpcshell appdir injection was attempted for this invocation. */
50
51
  appdirInjectionAttempted: boolean;
52
+ /**
53
+ * Display power state measured for a HEADED no-output stall;
54
+ * undefined when the shape did not apply and no probe ran.
55
+ */
56
+ displayState?: DisplaySleepState | undefined;
51
57
  }
52
58
  /**
53
59
  * Runs one mach test invocation for `paths`, retrying recognized harness
@@ -11,8 +11,9 @@
11
11
  * harness runs cost startup time but make results reproducible; the
12
12
  * combined invocation stays available via `--no-shard`.
13
13
  */
14
+ import { probeDisplaySleepState } from '../core/display-state.js';
14
15
  import { mochitestWithOutput, testWithOutput, xpcshellTestWithOutput, } from '../core/mach.js';
15
- import { buildHarnessCrashMessage, classifyHarnessRun, headedNoOutputTimeoutHint, } from '../core/test-harness-crash.js';
16
+ import { buildHarnessCrashMessage, classifyHarnessRun, headedDisplayAsleepVerdictNote, headedNoOutputTimeoutHint, } from '../core/test-harness-crash.js';
16
17
  import { retryAfterXpcshellSymlinkRepair } from '../core/test-xpcshell-retry.js';
17
18
  import { withXpcshellProfileDir } from '../core/xpcshell-profile-dir.js';
18
19
  import { BuildError } from '../errors/build.js';
@@ -37,7 +38,7 @@ function dispatchForSuite(suite) {
37
38
  */
38
39
  export async function runTestsWithRetries(ctx, paths) {
39
40
  // Pure mochitest dispatches keep their env byte-identical — the profile
40
- // variable is xpcshell-harness-specific (FORGE G15b). Generic `mach
41
+ // variable is xpcshell-harness-specific. Generic `mach
41
42
  // test` runs may include xpcshell paths, so they get the isolation too.
42
43
  if (ctx.suite === 'mochitest') {
43
44
  return runTestsWithRetriesInner(ctx, paths, ctx.env);
@@ -71,7 +72,34 @@ async function runTestsWithRetriesInner(ctx, paths, env) {
71
72
  `${verdict.signature?.line ?? ''}\n` +
72
73
  `Retrying (attempt ${attempts + 1} of ${maxAttempts})...`);
73
74
  }
74
- return { result, verdict, attempts, appdirInjectionAttempted };
75
+ const displayState = await probeDisplayStateForStall(verdict, ctx.headless);
76
+ if (displayState !== undefined) {
77
+ const note = headedDisplayAsleepVerdictNote(verdict.signature, {
78
+ headless: ctx.headless,
79
+ platform: getPlatform(),
80
+ displayState,
81
+ });
82
+ if (note !== undefined)
83
+ verdict = { ...verdict, note };
84
+ }
85
+ return { result, verdict, attempts, appdirInjectionAttempted, displayState };
86
+ }
87
+ /**
88
+ * Probes the display's power state for a HEADED no-output stall, and only
89
+ * for that shape. Every other outcome — a passing run, a real
90
+ * test failure, a headless run, any other crash shape — skips the probe
91
+ * entirely, so the common paths spawn nothing.
92
+ *
93
+ * @returns The measured state, or undefined when the shape does not apply
94
+ */
95
+ async function probeDisplayStateForStall(verdict, headless) {
96
+ if (headless)
97
+ return undefined;
98
+ if (verdict.kind !== 'harness-crash')
99
+ return undefined;
100
+ if (!verdict.signature?.reason.includes('no-output timeout'))
101
+ return undefined;
102
+ return probeDisplaySleepState(getPlatform());
75
103
  }
76
104
  const SHARD_STATUS_LABEL = {
77
105
  'tests-ran-ok': 'PASS',
@@ -119,6 +147,7 @@ export async function runShardedTests(ctx, groups, diagnoseShardFailure) {
119
147
  const hint = headedNoOutputTimeoutHint(outcome.verdict.signature, {
120
148
  headless: ctx.headless,
121
149
  platform: getPlatform(),
150
+ ...(outcome.displayState !== undefined ? { displayState: outcome.displayState } : {}),
122
151
  });
123
152
  warn(hint ? `${base}\n\n${hint}` : base);
124
153
  }
@@ -44,8 +44,13 @@ export async function enforceStaleBuildGate(projectRoot, engineDir, options, nor
44
44
  await enforceStaticComponentsGate(engineDir, stale.baseline, options);
45
45
  const staleMessage = stale.stale
46
46
  ? `${formatStaleBuildWarning(stale)}\n\n` +
47
- 'Run `fireforge test --build` to refresh the packaged runtime first, or pass ' +
48
- '`--allow-stale-build` if you intentionally rebuilt out-of-band and accept the risk.'
47
+ 'Run `fireforge test --build` to refresh the packaged runtime first — that is almost ' +
48
+ 'always the right move, because the usual cause is your own edit landing between the ' +
49
+ 'last build and this run.\n\n' +
50
+ '`--allow-stale-build` is NOT the general escape hatch: it accepts a packaging that ' +
51
+ 'predates the edit under test, so the run tests the OLD code. Pass it only when you ' +
52
+ 'rebuilt out-of-band (outside FireForge) and know the packaged runtime already contains ' +
53
+ 'the change.'
49
54
  : undefined;
50
55
  if (staleMessage !== undefined) {
51
56
  if (options.allowStaleBuild === true) {
@@ -12,7 +12,7 @@ import { assertObjdirMatchesTreeMarker } from '../core/tree-store.js';
12
12
  import { GeneralError } from '../errors/base.js';
13
13
  import { BuildError } from '../errors/build.js';
14
14
  import { pathExists } from '../utils/fs.js';
15
- import { info, intro, outro, success, verbose } from '../utils/logger.js';
15
+ import { info, intro, notice, outro, success, verbose } from '../utils/logger.js';
16
16
  import { stripEnginePrefix } from '../utils/paths.js';
17
17
  import { runTestBuildPhase } from './test-build-phase.js';
18
18
  import { diagnoseShardOutcome, finalizeSingleRunOutcome } from './test-diagnose.js';
@@ -174,7 +174,7 @@ function appendMarionetteForwardingArgs(extraArgs, options, forwardedPort, xpcsh
174
174
  // Manifest classification says every requested path is xpcshell —
175
175
  // xpcshell ignores the browser Marionette path entirely, and the
176
176
  // mochitest client flags previously forwarded here made mach reject
177
- // the dispatch (FORGE F10).
177
+ // the dispatch.
178
178
  info(`--marionette-port=${options.marionettePort} applied to the preflight probe only: the requested paths are xpcshell-only, and xpcshell ignores the browser Marionette port. Not forwarding --setpref=marionette.port or --marionette to mach.`);
179
179
  return;
180
180
  }
@@ -202,7 +202,7 @@ async function ensureTestMarionettePortAvailable(port, binaryName, options, skip
202
202
  if (skip.xpcshellOnly && !skip.doctor) {
203
203
  // xpcshell does not bind the browser Marionette port, so a developer's
204
204
  // interactive browser holding 2828 must not kill an xpcshell run
205
- // (FORGE F10). --doctor keeps the preflight: its probe launches a
205
+ //. --doctor keeps the preflight: its probe launches a
206
206
  // Marionette browser regardless of the requested harness.
207
207
  const message = 'Skipping the Marionette stale-port preflight: all requested paths are xpcshell ' +
208
208
  '(xpcshell does not bind the browser Marionette port).';
@@ -431,7 +431,7 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}) {
431
431
  // bug to an upstream headless regression), so a one-line notice states
432
432
  // what sharding does and does not exercise.
433
433
  if (canaryPath === undefined && dispatchGroups.length > 1 && options.shard !== false) {
434
- info(`Sharding: running ${dispatchGroups.length} test path arguments in isolated browser instances ` +
434
+ notice(`Sharding: running ${dispatchGroups.length} test path arguments in isolated browser instances ` +
435
435
  '(one mach invocation per argument; a directory argument keeps its files in one instance). ' +
436
436
  'Cross-argument state is NOT exercised — pass --no-shard for a combined single-instance run.');
437
437
  const generationBefore = await snapshotEngineGeneration(paths.engine);
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
- * `fireforge tree` — copy-on-write verification clones (FORGE G15).
3
+ * `fireforge tree` — copy-on-write verification clones.
4
4
  *
5
5
  * Trees enable concurrent READ-MOSTLY verification (lint, typecheck,
6
6
  * status, verify, doctor, export dry-runs) beside a busy primary tree.
@@ -263,7 +263,7 @@ async function treeExecCommand(projectRoot, name, args) {
263
263
  resolvePromise(code ?? 1);
264
264
  });
265
265
  }).finally(() => {
266
- // FORGE L2: with `stdio: 'inherit'` the child owned stdout — including
266
+ // With `stdio: 'inherit'` the child owned stdout — including
267
267
  // any FIREFORGE-VERDICT line it emitted as its LAST stdout write. From
268
268
  // here on the parent must not write stdout again, or its own refusal
269
269
  // text (the GeneralError below rendered by withErrorHandling) would
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import { Command } from 'commander';
18
18
  import type { CommandContext } from '../types/cli.js';
19
+ import type { PatchLintIssue } from '../types/commands/index.js';
19
20
  import type { TypecheckConfig } from '../types/config.js';
20
21
  import type { TypecheckProjectResult } from '../types/typecheck.js';
21
22
  /** Command-line options Commander forwards from `fireforge typecheck`. */
@@ -45,7 +46,7 @@ export declare function typecheckCommand(projectRoot: string, options: Typecheck
45
46
  * throws on errors. Extracted so it can be exercised directly by
46
47
  * the CLI test without spawning a child process.
47
48
  */
48
- export declare function reportResults(projectRoot: string, results: ReadonlyArray<TypecheckProjectResult>): void;
49
+ export declare function reportResults(projectRoot: string, results: ReadonlyArray<TypecheckProjectResult>, patchLintIssues?: ReadonlyArray<PatchLintIssue>): void;
49
50
  /**
50
51
  * Registers the `typecheck` command on the CLI program.
51
52
  */
@@ -19,9 +19,13 @@ import { Command } from 'commander';
19
19
  import { getProjectPaths, loadConfig } from '../core/config.js';
20
20
  import { furnaceConfigExists, loadFurnaceConfig } from '../core/furnace-config.js';
21
21
  import { findJsconfigPathsDrift, syncFurnaceJsconfigPaths } from '../core/furnace-jsconfig.js';
22
+ import { withPrivateGitIndex } from '../core/git-readonly-index.js';
23
+ import { buildPatchQueueContext } from '../core/patch-lint.js';
22
24
  import { relativeForDisplay, runTypecheck } from '../core/typecheck.js';
23
25
  import { GeneralError } from '../errors/base.js';
26
+ import { pathExists } from '../utils/fs.js';
24
27
  import { info, intro, outro, success, warn } from '../utils/logger.js';
28
+ import { buildPerRunCheckJs } from './lint-per-run-checkjs.js';
25
29
  /**
26
30
  * Resolves the project list to type-check. `--project` wins over
27
31
  * config; if neither is set, throws a clear error pointing at both
@@ -65,6 +69,13 @@ function formatIssue(projectRoot, issue) {
65
69
  */
66
70
  export async function typecheckCommand(projectRoot, options) {
67
71
  intro('FireForge typecheck');
72
+ // Read-only to the operator, an index WRITER to git without this scope
73
+ //: the Furnace jsconfig reconciler and the project resolution
74
+ // below touch `engine/`, and any git plumbing they reach would refresh
75
+ // the primary checkout's index under a concurrent `fireforge test`.
76
+ return withPrivateGitIndex(getProjectPaths(projectRoot).engine, () => runTypecheckCommandBody(projectRoot, options));
77
+ }
78
+ async function runTypecheckCommandBody(projectRoot, options) {
68
79
  // Validate project is initialised. `loadConfig` throws on missing
69
80
  // fireforge.json — withErrorHandling at the CLI layer renders the
70
81
  // resulting `ConfigNotFoundError` cleanly, so we don't need to
@@ -81,7 +92,38 @@ export async function typecheckCommand(projectRoot, options) {
81
92
  await regenerateStaleGeneratedJsconfig(projectRoot);
82
93
  info(`Running typecheck across ${String(cfg.projects.length)} project(s): ${cfg.projects.join(', ')}`);
83
94
  const results = await runTypecheck(projectRoot, cfg);
84
- reportResults(projectRoot, results);
95
+ // Fold the EXPORT-TIME authority into this pass. Per-patch lint runs
96
+ // checkJs over the queue's patch-owned modules in relative isolation and
97
+ // resolves imported typedefs differently from the whole-project pass, so
98
+ // `typecheck` could report 0 errors across every
99
+ // project while the very next `export` failed per-patch lint with
100
+ // `checkjs-type-error` findings. Two authorities that disagree, with the
101
+ // stricter one speaking only at export time, is not a usable CI gate:
102
+ // running both here makes `typecheck` green MEAN export-clean types.
103
+ const patchIssues = await collectPatchLintCheckJsIssues(projectRoot);
104
+ reportResults(projectRoot, results, patchIssues);
105
+ }
106
+ /**
107
+ * Runs the per-patch-lint checkJs pass — the same program `export` and
108
+ * `lint --per-patch` use — and returns its findings.
109
+ *
110
+ * Returns an empty list when `patchLint.checkJs` is off or the project has
111
+ * no patch queue: there is no second authority to reconcile then.
112
+ */
113
+ async function collectPatchLintCheckJsIssues(projectRoot) {
114
+ const paths = getProjectPaths(projectRoot);
115
+ const config = await loadConfig(projectRoot);
116
+ if (config.patchLint?.checkJs !== true)
117
+ return [];
118
+ if (!(await pathExists(paths.patches)))
119
+ return [];
120
+ const ctx = await buildPatchQueueContext(paths.patches, config);
121
+ const checkJs = buildPerRunCheckJs(projectRoot, paths, config, ctx);
122
+ if (checkJs === undefined)
123
+ return [];
124
+ info('Also running the per-patch checkJs pass (the authority `export` enforces).');
125
+ const grouped = await checkJs.getGrouped();
126
+ return [...grouped.global, ...[...grouped.byFile.values()].flat()];
85
127
  }
86
128
  /**
87
129
  * Staleness-checks and regenerates the Furnace-managed jsconfig
@@ -109,9 +151,19 @@ async function regenerateStaleGeneratedJsconfig(projectRoot) {
109
151
  * throws on errors. Extracted so it can be exercised directly by
110
152
  * the CLI test without spawning a child process.
111
153
  */
112
- export function reportResults(projectRoot, results) {
154
+ export function reportResults(projectRoot, results, patchLintIssues = []) {
113
155
  let totalErrors = 0;
114
156
  let totalWarnings = 0;
157
+ // Per-patch checkJs findings are reported in the SAME tally:
158
+ // a split report would let an operator read "0 errors" off the project
159
+ // summary and still be refused at export.
160
+ for (const issue of patchLintIssues) {
161
+ if (issue.severity === 'error')
162
+ totalErrors += 1;
163
+ else
164
+ totalWarnings += 1;
165
+ warn(`[per-patch ${issue.check}] ${issue.file}: ${issue.message}`);
166
+ }
115
167
  for (const result of results) {
116
168
  const errors = result.issues.filter((i) => i.category === 'error');
117
169
  const warnings = result.issues.filter((i) => i.category === 'warning');
@@ -122,7 +174,11 @@ export function reportResults(projectRoot, results) {
122
174
  for (const issue of errors)
123
175
  warn(formatIssue(projectRoot, issue));
124
176
  }
125
- const summary = `Typecheck: ${String(totalErrors)} error(s), ${String(totalWarnings)} warning(s) across ${String(results.length)} project(s)`;
177
+ const summary = `Typecheck: ${String(totalErrors)} error(s), ${String(totalWarnings)} warning(s) across ` +
178
+ `${String(results.length)} project(s)` +
179
+ (patchLintIssues.length > 0
180
+ ? ` (including ${String(patchLintIssues.length)} per-patch checkJs finding(s))`
181
+ : '');
126
182
  if (totalErrors === 0) {
127
183
  success(summary);
128
184
  outro(totalWarnings > 0 ? 'Typecheck passed with warnings' : 'Typecheck passed');
@@ -19,7 +19,9 @@ import { Command } from 'commander';
19
19
  import { getProjectPaths, loadConfig } from '../core/config.js';
20
20
  import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
21
21
  import { isGitRepository } from '../core/git.js';
22
+ import { withPrivateGitIndex } from '../core/git-readonly-index.js';
22
23
  import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
24
+ import { findUnresolvedSystemModuleImports, formatUnresolvedSystemModuleImports, } from '../core/module-resolution-preflight.js';
23
25
  import { buildPatchQueueContext, formatPatchLintIssue, lintPatchQueue, } from '../core/patch-lint.js';
24
26
  import { loadPatchesManifest, validatePatchesManifestConsistency } from '../core/patch-manifest.js';
25
27
  import { evaluatePatchPolicy } from '../core/patch-policy.js';
@@ -167,6 +169,24 @@ export async function collectPatchQueueHealth(projectRoot) {
167
169
  errorCount += policyErrors;
168
170
  warningCount += policyWarnings;
169
171
  }
172
+ // Engine-aware module-resolution preflight. The queue-level
173
+ // `unregistered-system-module` rule only sees modules a patch NEWLY
174
+ // CREATES; this asks the same question of the engine, so an import
175
+ // ADDED to an existing module — the shape that recurred and cost a
176
+ // rebuild cycle — is named as text instead of surfacing later as a
177
+ // bare `xpcshell return code: -11` with zero output.
178
+ if (await pathExists(paths.engine)) {
179
+ const unresolved = await findUnresolvedSystemModuleImports(paths.engine, manifest.patches.flatMap((patch) => patch.filesAffected));
180
+ if (unresolved.length > 0) {
181
+ groups.push({
182
+ title: `Unresolvable system-module imports (${unresolved.length})`,
183
+ issues: formatUnresolvedSystemModuleImports(unresolved),
184
+ errorCount: unresolved.length,
185
+ warningCount: 0,
186
+ });
187
+ errorCount += unresolved.length;
188
+ }
189
+ }
170
190
  const crossClaims = detectCrossPatchFileClaims(manifest.patches);
171
191
  if (crossClaims.length > 0) {
172
192
  groups.push({
@@ -241,7 +261,12 @@ export async function collectPatchQueueHealth(projectRoot) {
241
261
  */
242
262
  export async function verifyCommand(projectRoot) {
243
263
  intro('FireForge Verify');
244
- const health = await collectPatchQueueHealth(projectRoot);
264
+ // `verify` is read-only to the OPERATOR but not to git: the working-tree
265
+ // status probe refreshes (and rewrites) `engine/.git/index`, making this
266
+ // command a second writer of the primary checkout and invalidating a
267
+ // concurrent `fireforge test`'s engine fingerprint. A private index keeps
268
+ // every refresh off the primary checkout.
269
+ const health = await withPrivateGitIndex(getProjectPaths(projectRoot).engine, () => collectPatchQueueHealth(projectRoot));
245
270
  if (!health.hasPatchesDirectory) {
246
271
  info('No patches directory. Nothing to verify.');
247
272
  outro('Verify clean');
@@ -1,6 +1,18 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import * as acorn from 'acorn';
3
3
  import { walk } from 'estree-walker';
4
+ /**
5
+ * Bridges acorn's return type to the positioned ESTree program type.
6
+ * `acorn.parse` produces nodes that carry both the ESTree shape and
7
+ * `start`/`end` offsets at runtime, but its static `acorn.Program` type is
8
+ * nominally distinct from `estree.Program`, so the compiler cannot verify
9
+ * the conversion structurally — hence the double assertion. This helper is
10
+ * the single sanctioned crossing point between the two type worlds.
11
+ */
12
+ function toPositionedProgram(program) {
13
+ // eslint-disable-next-line no-restricted-syntax -- the sanctioned acorn→estree bridge this rule's message points at
14
+ return program;
15
+ }
4
16
  /**
5
17
  * Parse JavaScript source as a **script** (not an ES module).
6
18
  * All Mozilla chrome JS files (`browser-main.js`, `browser-init.js`,
@@ -17,7 +29,7 @@ export function parseScript(content, onComment) {
17
29
  };
18
30
  if (onComment)
19
31
  opts.onComment = onComment;
20
- return acorn.parse(content, opts);
32
+ return toPositionedProgram(acorn.parse(content, opts));
21
33
  }
22
34
  /**
23
35
  * Parse JavaScript source as an **ES module**.
@@ -35,7 +47,7 @@ export function parseModule(content, onComment) {
35
47
  };
36
48
  if (onComment)
37
49
  opts.onComment = onComment;
38
- return acorn.parse(content, opts);
50
+ return toPositionedProgram(acorn.parse(content, opts));
39
51
  }
40
52
  /**
41
53
  * Convenience cast from `acorn.Node` (or the generic ESTree union returned
@@ -5,7 +5,7 @@ import { toError } from '../utils/errors.js';
5
5
  import { verbose } from '../utils/logger.js';
6
6
  import { toRootRelativePath } from '../utils/paths.js';
7
7
  import { getProjectPaths, loadConfig } from './config.js';
8
- import { createRollbackJournal, restoreRollbackJournal, snapshotFile } from './furnace-rollback.js';
8
+ import { assertSnapshotted, createRollbackJournal, restoreRollbackJournal, snapshotFile, } from './furnace-rollback.js';
9
9
  import { registerBrowserContent } from './manifest-register.js';
10
10
  import { DEFAULT_DOM_TARGET } from './wire-dom-fragment.js';
11
11
  import { addDestroyToBrowserInit, addDomFragment, addInitToBrowserInit, addSubscriptToBrowserMain, } from './wire-targets.js';
@@ -82,15 +82,22 @@ export async function wireSubscript(root, name, options = {}) {
82
82
  }
83
83
  try {
84
84
  // 1. Add subscript to browser-main.js
85
+ assertSnapshotted(journal, join(engineDir, 'browser/base/content/browser-main.js'), 'browser wire');
85
86
  const subscriptAdded = await addSubscriptToBrowserMain(engineDir, name, marker);
86
87
  // 2. Add init expression to browser-init.js (if provided)
87
88
  let initAdded = false;
88
89
  if (options.init) {
90
+ // The snapshot above is taken when init or destroy is `!== undefined`
91
+ // while these writes fire on truthiness — close, but two separate
92
+ // conditions over the same file, which is exactly the pairing that
93
+ // drifts when a third writer is added later.
94
+ assertSnapshotted(journal, join(engineDir, 'browser/base/content/browser-init.js'), 'browser wire init');
89
95
  initAdded = await addInitToBrowserInit(engineDir, options.init, options.after, marker);
90
96
  }
91
97
  // 3. Add destroy expression to browser-init.js onUnload() (if provided)
92
98
  let destroyAdded = false;
93
99
  if (options.destroy) {
100
+ assertSnapshotted(journal, join(engineDir, 'browser/base/content/browser-init.js'), 'browser wire destroy');
94
101
  destroyAdded = await addDestroyToBrowserInit(engineDir, options.destroy, marker);
95
102
  }
96
103
  // 4. Add #include directive to the top-level chrome document (if provided)
@@ -99,6 +106,7 @@ export async function wireSubscript(root, name, options = {}) {
99
106
  domInserted = await addDomFragment(engineDir, toRootRelativePath(engineDir, options.domFilePath), options.domTargetPath);
100
107
  }
101
108
  // 5. Register in jar.mn
109
+ assertSnapshotted(journal, join(engineDir, 'browser/base/jar.mn'), 'browser wire jar.mn');
102
110
  const jarMnResult = await registerBrowserContent(engineDir, `${name}.js`, undefined, jarMnSourcePath);
103
111
  return {
104
112
  subscriptAdded,
@@ -64,7 +64,7 @@ export interface BuildBaseline {
64
64
  * earlier scope's edited fixtures while `obj-*`/`_tests/` still holds its
65
65
  * stale staging; coverage therefore REPLACES by default.
66
66
  *
67
- * The one exception is `test --build --extend-coverage` (FORGE L1), which
67
+ * The one exception is `test --build --extend-coverage`, which
68
68
  * unions only after proving the previous record's anchor still holds:
69
69
  * same engine HEAD, same {@link BuildBaseline.mozconfigHash}, and every
70
70
  * previously fingerprinted path byte-identical — i.e. the wholesale
@@ -52,7 +52,7 @@ export declare function readBuildBaseline(projectRoot: string): Promise<BuildBas
52
52
  * static-components anchor whenever the coverage claim is `'full'` or
53
53
  * absent. `'refresh'` records it even for a scoped coverage claim whose
54
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'`
55
+ * `--extend-coverage`, whose union can EVALUATE to `'full'`
56
56
  * while the build that produced it was still a scoped `mach build
57
57
  * faster` that did not rebake the compiled table.
58
58
  */
@@ -86,7 +86,7 @@ export async function readBuildBaseline(projectRoot) {
86
86
  * static-components anchor whenever the coverage claim is `'full'` or
87
87
  * absent. `'refresh'` records it even for a scoped coverage claim whose
88
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'`
89
+ * `--extend-coverage`, whose union can EVALUATE to `'full'`
90
90
  * while the build that produced it was still a scoped `mach build
91
91
  * faster` that did not rebake the compiled table.
92
92
  */
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Loud guard against silently reverting UNEXPORTED engine drift.
3
+ *
4
+ * `build-prepare` rewrites engine files from FireForge-owned sources
5
+ * before every build: the branding tree, every Furnace-managed component,
6
+ * and `mozconfig`. On a multi-session checkout that is a silent
7
+ * destructive write. The recorded incident: one session edited a file in
8
+ * `engine/` while another held the engine lock; the lock-holder's build
9
+ * rewrote the file back to its patch baseline; the editing session's later
10
+ * `re-export --wait-lock` then captured a HYBRID file — and every gate
11
+ * passed on the hybrid capture. Only a hand-run grep of the patch body
12
+ * caught it.
13
+ *
14
+ * The `setupBranding` half of this class was closed in `31fdf744` (the
15
+ * branding writer preserves unmanaged lines). The general shape — any
16
+ * build-prepare overwrite destroying local drift that no patch records —
17
+ * stayed silent, and silence is what made the hybrid capture possible.
18
+ *
19
+ * This guard runs BEFORE the writes, classifies the engine's dirty files
20
+ * with the same classifier `fireforge status` uses, and reports the ones
21
+ * whose content is explained by neither a patch body nor the pristine
22
+ * baseline. Advisory by default (a warning naming every file), refusing
23
+ * under an explicit flag.
24
+ */
25
+ import type { FireForgeConfig } from '../types/config.js';
26
+ import { type ClassifiedFile } from './status-classify.js';
27
+ /** One file whose unexported content a build-prepare overwrite would destroy. */
28
+ export interface UnexportedDriftAtRisk {
29
+ file: string;
30
+ classification: ClassifiedFile['classification'];
31
+ owner?: string | undefined;
32
+ }
33
+ /**
34
+ * Finds engine files that build-prepare is about to overwrite and whose
35
+ * current content is recorded nowhere.
36
+ *
37
+ * Fail-open: any probe failure is reported verbosely and yields an empty
38
+ * list. A build must not fail because an advisory guard could not run.
39
+ *
40
+ * @param projectRoot - Project root
41
+ * @param config - Loaded FireForge config (reads `binaryName`)
42
+ * @returns At-risk files, sorted by path
43
+ */
44
+ export declare function findUnexportedDriftAtRisk(projectRoot: string, config: FireForgeConfig): Promise<UnexportedDriftAtRisk[]>;
45
+ /**
46
+ * Renders the operator-facing warning (or refusal body) naming every file
47
+ * whose unexported content the build is about to overwrite.
48
+ */
49
+ export declare function formatUnexportedDriftWarning(files: readonly UnexportedDriftAtRisk[]): string;