@hominis/fireforge 0.42.0 → 0.43.1

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 +76 -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 +165 -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
@@ -22,6 +22,7 @@
22
22
  * above so the command layer can retry them with a bounded budget instead
23
23
  * of reporting phantom test failures (or phantom passes).
24
24
  */
25
+ import { hasKnownTeardownNoise } from './mach-known-noise-filter.js';
25
26
  const TEST_START_PATTERN = /\bTEST-START\b/;
26
27
  /**
27
28
  * Structured-log execution marker (`mach xpcshell-test` mozlog output uses
@@ -452,6 +453,21 @@ export function classifyHarnessRun(exitCode, output, requestedPaths) {
452
453
  const firstRealFailure = realFailures[0];
453
454
  const failureBlocks = collectUnexpectedFailureBlocks(output);
454
455
  const secondaryHarnessSignature = firstRealFailure !== undefined ? detectSecondaryHarnessNoise(output) : undefined;
456
+ // Checked BEFORE signature detection: a suite that ran clean
457
+ // and then died in KNOWN upstream teardown noise is a PASS, not a crash.
458
+ // The teardown traceback matches the startup-traceback cluster, and the
459
+ // existing green-summary veto over that cluster requires a `SUITE_END`
460
+ // marker — which this very traceback is what prevents from printing. So
461
+ // a substantively green suite (`Ran N checks` / `Unexpected results: 0`,
462
+ // no unexpected lines) was reported CRASH, indistinguishable at the
463
+ // summary level from a red run. Every hard-evidence veto still applies:
464
+ // a crash marker, a non-zero unexpected count, any real TEST-UNEXPECTED
465
+ // line, the shutdown-re-entry shape, or a requested file that never
466
+ // started keeps the run failing. Only the missing shutdown marker is
467
+ // forgiven, and only when the recognized teardown traceback explains it.
468
+ if (isGreenSuiteEndedByKnownTeardownNoise(output, counts, realFailures, requestedPaths)) {
469
+ return { kind: 'tests-ran-ok', note: 'harness teardown noise ignored', ...counts };
470
+ }
455
471
  const signature = detectHarnessCrashSignature(output);
456
472
  if (signature) {
457
473
  return { kind: 'harness-crash', signature, ...counts };
@@ -505,6 +521,40 @@ export function classifyHarnessRun(exitCode, output, requestedPaths) {
505
521
  ...(secondaryHarnessSignature !== undefined ? { secondaryHarnessSignature } : {}),
506
522
  };
507
523
  }
524
+ /**
525
+ * True when the capture shows a substantively GREEN suite whose only
526
+ * unexplained residue is the recognized mozsystemmonitor teardown
527
+ * traceback.
528
+ *
529
+ * Requires, all of them: tests actually executed; the summary printed
530
+ * `Ran N checks` with an explicit `Unexpected results: 0`; no non-zero
531
+ * unexpected count anywhere; no real `TEST-UNEXPECTED-*`/assertion line;
532
+ * no crash marker; every requested test file both started and ended; and
533
+ * the recognized teardown traceback is present. Anything short of that
534
+ * falls through to the normal (failing) chain.
535
+ */
536
+ function isGreenSuiteEndedByKnownTeardownNoise(output, counts, realFailures, requestedPaths) {
537
+ if (!hasKnownTeardownNoise(output))
538
+ return false;
539
+ if (!hasExecutionSignal(output))
540
+ return false;
541
+ if (counts.checks === undefined || counts.unexpected !== 0)
542
+ return false;
543
+ if (!GREEN_UNEXPECTED_SUMMARY_PATTERN.test(output))
544
+ return false;
545
+ if (NONZERO_UNEXPECTED_SUMMARY_PATTERN.test(output))
546
+ return false;
547
+ if (realFailures.length > 0)
548
+ return false;
549
+ if (findCrashMarkerLine(output) !== undefined)
550
+ return false;
551
+ // The post-green shutdown re-entry shape is deliberately a crash verdict
552
+ // on an otherwise-green log; it must not be swept up here.
553
+ if (SHUTDOWN_REENTRY_PATTERN.test(output))
554
+ return false;
555
+ const completeness = analyzeTestCompleteness(output, requestedPaths);
556
+ return completeness.neverStarted.length === 0 && completeness.neverEnded.length === 0;
557
+ }
508
558
  /**
509
559
  * Parses the numeric counts from the harness's embedded result summary
510
560
  * (`Ran N checks` / `Unexpected results: N`). The LAST occurrence of each
@@ -547,15 +597,16 @@ export function formatFireforgeVerdictLine(verdict, shards) {
547
597
  const counts = (verdict.checks !== undefined ? ` checks=${verdict.checks}` : '') +
548
598
  (verdict.unexpected !== undefined ? ` unexpected=${verdict.unexpected}` : '');
549
599
  const shardSuffix = shards ? ` shards=${shards.passed}/${shards.total}` : '';
600
+ const note = verdict.note !== undefined ? ` (${verdict.note})` : '';
550
601
  if (verdict.kind === 'tests-ran-ok') {
551
- return `FIREFORGE-VERDICT: PASS${counts}${shardSuffix}`;
602
+ return `FIREFORGE-VERDICT: PASS${counts}${shardSuffix}${note}`;
552
603
  }
553
604
  const reason = verdict.kind === 'harness-crash'
554
605
  ? 'crash'
555
606
  : verdict.kind === 'no-tests'
556
607
  ? 'no-tests'
557
608
  : 'test-failures';
558
- return `FIREFORGE-VERDICT: FAIL reason=${reason}${counts}${shardSuffix}`;
609
+ return `FIREFORGE-VERDICT: FAIL reason=${reason}${counts}${shardSuffix}${note}`;
559
610
  }
560
611
  /**
561
612
  * Builds the operator-facing failure message for a green-looking summary
@@ -592,21 +643,135 @@ export function buildHarnessCrashMessage(signature, attempts, commandLabel = 'ma
592
643
  'If it persists across many runs, inspect the mach virtualenv (mach resyncs psutil on its own; ' +
593
644
  'patching it manually does not stick).');
594
645
  }
646
+ /**
647
+ * The three recorded causes of the `timed out … with no output` / `Ran 0
648
+ * checks` signature, ordered so one stall points at the discriminating
649
+ * next step. Printed verbatim under the hint.
650
+ */
651
+ const NO_OUTPUT_STALL_TRIAGE = [
652
+ ' 1. A sleeping or locked display on an unattended HEADED run (macOS). The browser never ' +
653
+ 'paints and never reaches its first test.',
654
+ ' 2. A headless SWGL compositor failure — re-run with --headless to separate this from (1): ' +
655
+ 'if --headless passes, the stall was display/compositor-side, not product-side.',
656
+ ' 3. A broken `chrome://` image on the startup page, which stalls first paint. Run a ' +
657
+ 'known-good control test first; if the control stalls too, the cause is not the test ' +
658
+ 'under investigation.',
659
+ ];
595
660
  /**
596
661
  * Optional hint appended to the harness-crash message when a HEADED run on
597
- * macOS died at the no-output timeout: the display may have slept and frozen
598
- * the headed browser before any test started (FORGE F17). Pure — the
599
- * platform is injected so unit tests need no mocking.
662
+ * macOS died at the no-output timeout.
663
+ *
664
+ * When `displayState` names a MEASURED display state, the hint states it
665
+ * as fact rather than as one of three possibilities — that is the whole
666
+ * point of probing: an operator staring at a bare test failure should not
667
+ * have to rediscover that their machine dimmed. `caffeinate` is described
668
+ * accurately (it prevents sleep; it cannot wake an already-sleeping
669
+ * display), because the previous wording sent operators to a command that
670
+ * could not have helped them.
671
+ *
672
+ * Pure — the platform and the probed state are injected so unit tests need
673
+ * no mocking.
674
+ *
675
+ * @param signature - The recognized crash shape
676
+ * @param context - Run mode, platform, and the probed display state
677
+ * @returns The hint text, or undefined when the shape does not apply
600
678
  */
601
679
  export function headedNoOutputTimeoutHint(signature, context) {
602
680
  if (context.platform !== 'darwin' || context.headless)
603
681
  return undefined;
604
682
  if (!signature.reason.includes('no-output timeout'))
605
683
  return undefined;
606
- return ('Hint: this was a HEADED run on macOS that died at the no-output timeout. ' +
607
- 'A common cause is the display going to sleep mid-run, which freezes the headed browser ' +
608
- 'before any test starts. Wrap headed runs in `caffeinate -dimsu <command>` to keep the ' +
609
- 'display awake, or pass --headless.');
684
+ const lead = context.displayState === 'asleep'
685
+ ? 'Hint: this was a HEADED run on macOS and the display was MEASURED ASLEEP ' +
686
+ '(IODisplayWrangler below its awake power state). A headed browser on a sleeping ' +
687
+ 'display never paints and never starts a test, so this stall is environmental — not a ' +
688
+ 'product or test failure.'
689
+ : context.displayState === 'awake'
690
+ ? 'Hint: this was a HEADED run on macOS that died at the no-output timeout. The display ' +
691
+ 'was measured AWAKE, so the sleeping-display cause below is ruled out for this run.'
692
+ : 'Hint: this was a HEADED run on macOS that died at the no-output timeout. The display ' +
693
+ 'state could not be measured, so all three causes below remain open.';
694
+ const remedy = 'Note that `caffeinate -disu` PREVENTS sleep; it cannot WAKE a display that is already ' +
695
+ 'asleep. Wake the display (or run on an attended machine), or pass --headless.';
696
+ return `${lead}\n\nKnown causes of this exact signature:\n${NO_OUTPUT_STALL_TRIAGE.join('\n')}\n\n${remedy}`;
697
+ }
698
+ /**
699
+ * Verdict-line note for a headed no-output stall whose display was
700
+ * measured asleep. Returned as the parenthetical the verdict line carries,
701
+ * so the one greppable line an automated consumer reads already names the
702
+ * environmental cause instead of a bare `FAIL reason=crash`.
703
+ */
704
+ export function headedDisplayAsleepVerdictNote(signature, context) {
705
+ if (context.platform !== 'darwin' || context.headless)
706
+ return undefined;
707
+ if (context.displayState !== 'asleep')
708
+ return undefined;
709
+ if (!signature.reason.includes('no-output timeout'))
710
+ return undefined;
711
+ return 'headed run stalled with the display asleep';
712
+ }
713
+ /**
714
+ * Exit codes that mean "the child died on SIGSEGV": Node reports a
715
+ * signal-killed child as the negated signal number, while a shell layer
716
+ * in between reports `128 + signal`. Both shapes reach FireForge
717
+ * depending on how mach wrapped the harness.
718
+ */
719
+ const SIGSEGV_EXIT_CODES = new Set([-11, 139]);
720
+ /**
721
+ * True when a harness run died on SIGSEGV having produced no test evidence
722
+ * whatsoever.
723
+ *
724
+ * `xpcshell return code: -11` with zero output is the signature of an
725
+ * `.sys.mjs` that a packaged module imports but whose `EXTRA_JS_MODULES`
726
+ * registration never landed: the module loader faults before any logging
727
+ * exists, so there is no import error, no stack, no test output — just a
728
+ * dead process. It is indistinguishable from a genuine product crash
729
+ * unless someone names the cause.
730
+ *
731
+ * "Produced no evidence" is deliberately semantic rather than a byte
732
+ * budget: no execution signal, no real failure line, and no mozcrash
733
+ * marker. A run that got far enough to start a test, report a failure, or
734
+ * leave a crash dump has evidence of its own, and the regular diagnosis
735
+ * chain reads it — this branch is only for the case where there is
736
+ * nothing else to say.
737
+ *
738
+ * Pure; exported for direct unit testing.
739
+ *
740
+ * @param exitCode - The harness process exit code
741
+ * @param output - Combined stdout/stderr from the run
742
+ */
743
+ export function isSilentSegfault(exitCode, output) {
744
+ if (!SIGSEGV_EXIT_CODES.has(exitCode))
745
+ return false;
746
+ if (hasExecutionSignal(output))
747
+ return false;
748
+ if (realUnexpectedFailureLines(output).length > 0)
749
+ return false;
750
+ return findCrashMarkerLine(output) === undefined;
751
+ }
752
+ /**
753
+ * Names the known cause of a silent SIGSEGV so the operator checks
754
+ * moz.build registration FIRST instead of paying a full rebuild cycle to
755
+ * rediscover it.
756
+ *
757
+ * @param exitCode - The harness process exit code
758
+ * @param requestedPaths - Test paths the run requested
759
+ */
760
+ export function buildSilentSegfaultMessage(exitCode, requestedPaths) {
761
+ return (`The test harness died on SIGSEGV (exit ${exitCode}) having printed NO output at all.\n\n` +
762
+ 'Known cause, check this FIRST: a `.sys.mjs` imported from a packaged module whose ' +
763
+ '`EXTRA_JS_MODULES` (or namespaced module list) registration is missing from moz.build. ' +
764
+ 'The module loader faults before any logging exists, so an unregistered module produces ' +
765
+ 'exactly this shape — a dead process with no import error and no stack.\n\n' +
766
+ 'What to do, in order:\n' +
767
+ ' 1. Run `fireforge lint --per-patch`: the unregistered-system-module check names a new ' +
768
+ 'module imported as a resource URL with no moz.build line.\n' +
769
+ ' 2. Run `fireforge verify`: its module-resolution preflight resolves every ' +
770
+ '`resource:///modules/…` specifier the queue-owned modules import, including imports ' +
771
+ 'added to an EXISTING module — the shape the new-file lint cannot see, and the one that ' +
772
+ 'recurs.\n' +
773
+ ' 3. Only then look for a product crash: a real one prints a crash dump or a stack.\n\n' +
774
+ `Requested paths: ${requestedPaths.join(', ')}`);
610
775
  }
611
776
  /**
612
777
  * Builds the message for a run that produced no `TEST-START` despite
@@ -111,7 +111,7 @@ export declare function formatStaticComponentsRefusal(changedManifests: string[]
111
111
  /**
112
112
  * Formats the post-mutation advisory printed by `fireforge reset` /
113
113
  * `fireforge import` when `components.conf` diverged from the last full
114
- * build (FORGE F13). Same probe as {@link formatStaticComponentsRefusal},
114
+ * build. Same probe as {@link formatStaticComponentsRefusal},
115
115
  * different moment: this fires at mutation time so the operator learns a
116
116
  * full build is needed BEFORE the next gate run refuses.
117
117
  */
@@ -229,7 +229,7 @@ export function formatStaticComponentsRefusal(changedManifests) {
229
229
  /**
230
230
  * Formats the post-mutation advisory printed by `fireforge reset` /
231
231
  * `fireforge import` when `components.conf` diverged from the last full
232
- * build (FORGE F13). Same probe as {@link formatStaticComponentsRefusal},
232
+ * build. Same probe as {@link formatStaticComponentsRefusal},
233
233
  * different moment: this fires at mutation time so the operator learns a
234
234
  * full build is needed BEFORE the next gate run refuses.
235
235
  */
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
3
  * Category banner and token-declaration helpers for the tokens CSS file
4
- * (FORGE G3). Split out of `token-manager.ts` to stay inside the per-file
4
+ *. Split out of `token-manager.ts` to stay inside the per-file
5
5
  * line budget.
6
6
  *
7
7
  * Banner matching is EXACT: the text between the `=` runs (single-line
@@ -51,7 +51,7 @@ export interface AddTokenResult {
51
51
  /**
52
52
  * When the add skipped because the token already lives in the TARGET
53
53
  * category, names where the existing base-`:root` declaration sits so
54
- * the skip message can point at it (FORGE G3).
54
+ * the skip message can point at it.
55
55
  */
56
56
  skippedExisting?: TokenDeclarationLocation;
57
57
  }
@@ -117,7 +117,7 @@ async function addVariantTokenToCSS(engineDir, options, tokensCssPath, variant)
117
117
  return { added: true };
118
118
  }
119
119
  /**
120
- * Evaluates base-`:root` idempotency for one add (FORGE G3): a token
120
+ * Evaluates base-`:root` idempotency for one add: a token
121
121
  * already declared in the TARGET category skips; a token declared
122
122
  * anywhere else in the base block refuses loud — the pre-0.40.0
123
123
  * whole-file substring check silently skipped (and discarded
@@ -151,7 +151,7 @@ export async function validateTokenAdd(root, options) {
151
151
  validateDarkValue(options);
152
152
  // Variant mode targets a `:root<attr>` block, not a category section —
153
153
  // but the required --category must still name a REAL category rather
154
- // than being silently discarded (FORGE G3 bypass 2).
154
+ // than being silently discarded (bypass 2).
155
155
  if (normalizeVariantOption(options) !== undefined) {
156
156
  await assertTokenCategoryExists(engineDir, tokensCssPath, options.category, false);
157
157
  return;
@@ -201,7 +201,7 @@ export async function addToken(root, options) {
201
201
  // base token is not short-circuited by the global idempotency check.
202
202
  if (normalizedVariant !== undefined) {
203
203
  // The required --category must name a real category even though the
204
- // declaration routes into the variant block (FORGE G3 bypass 2).
204
+ // declaration routes into the variant block (bypass 2).
205
205
  await assertTokenCategoryExists(engineDir, tokensCssPath, options.category, false);
206
206
  const { added } = await addVariantTokenToCSS(engineDir, options, tokensCssPath, normalizedVariant);
207
207
  return {
@@ -241,7 +241,7 @@ export async function addToken(root, options) {
241
241
  * pair the media query with `:root[data-theme="dark"]` /
242
242
  * `:root[data-theme="light"]` blocks (viewer-toggle theming) require every
243
243
  * themed list to declare identical token sets, so an override add that only
244
- * wrote the media block was a guaranteed half-finished edit (FORGE F8).
244
+ * wrote the media block was a guaranteed half-finished edit.
245
245
  */
246
246
  const THEME_ATTRIBUTE_VARIANTS = [
247
247
  { variant: '[data-theme="dark"]', pick: 'dark' },
@@ -298,7 +298,7 @@ async function addTokenToCSS(engineDir, options, tokensCssPath) {
298
298
  await assertTokenCategoryExists(engineDir, tokensCssPath, options.category, options.createCategory === true);
299
299
  const content = await readText(filePath);
300
300
  const lines = content.split('\n');
301
- // Per-category idempotency (FORGE G3): a declaration in the TARGET
301
+ // Per-category idempotency: a declaration in the TARGET
302
302
  // category skips; a declaration elsewhere in the base :root block
303
303
  // throws instead of silently skipping (and silently discarding
304
304
  // --create-category).
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
- * Copy-on-write cloning for `fireforge tree` (FORGE G15).
3
+ * Copy-on-write cloning for `fireforge tree`.
4
4
  *
5
5
  * Capability is probed BY DOING — a tiny `cp -c` (APFS clonefile) or
6
6
  * `cp --reflink=always` (btrfs/XFS) in the actual destination directory —
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
- * Read-only guard for verification trees (FORGE G15).
3
+ * Read-only guard for verification trees.
4
4
  *
5
5
  * A tree is a snapshot: its `patches/` and `components/` are copies with
6
6
  * no merge-back model, so any command that mutates project or engine
@@ -66,7 +66,7 @@ const TREE_CONDITIONAL_CHECKS = {
66
66
  'export-all': ({ options }) => options['dryRun'] === true,
67
67
  // Same rule for refreshing existing patches: the dry-run projection is a
68
68
  // read (proven side-effect free and runtime-enforced by
69
- // withDryRunPurityGuard — FORGE H1/H3); a real re-export writes patches/.
69
+ // withDryRunPurityGuard); a real re-export writes patches/.
70
70
  're-export': ({ options }) => options['dryRun'] === true,
71
71
  // `config` with no positional value prints; with a value it writes.
72
72
  config: ({ args }) => args.filter((a) => typeof a === 'string').length <= 1,
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
- * Verification-tree storage and lifecycle for `fireforge tree` (FORGE G15).
3
+ * Verification-tree storage and lifecycle for `fireforge tree`.
4
4
  *
5
5
  * A tree is a FULL project-root CoW snapshot under
6
6
  * `.fireforge/trees/<name>` — `getProjectRoot()` walks up from cwd, so
@@ -15,7 +15,7 @@ export declare const SHIM_FILENAME = "__fireforge_firefox_globals.d.ts";
15
15
  * Loose declarations for Firefox test-harness globals (mochitest
16
16
  * browser-chrome and xpcshell), appended AFTER the composed
17
17
  * Firefox-globals + consumer shim when `patchLint.checkJsTestFiles`
18
- * extends the checkJs pass to patch-adopted test scripts (FORGE G5).
18
+ * extends the checkJs pass to patch-adopted test scripts.
19
19
  * Deliberately `any`-typed — the pragmatic posture matches the main
20
20
  * shim. A consumer that wants TYPED harness members (so e.g. a call to
21
21
  * a method the harness does not declare fails at the patch boundary)
@@ -40,7 +40,7 @@ export declare const TEST_HARNESS_SHIM = "\n// \u2500\u2500 FireForge test-harne
40
40
  export declare const SUPPRESSED_DIAGNOSTIC_CODES: ReadonlySet<number>;
41
41
  /**
42
42
  * Undefined-free-identifier codes, split out of
43
- * {@link SUPPRESSED_DIAGNOSTIC_CODES} (FORGE F12). Unconditional
43
+ * {@link SUPPRESSED_DIAGNOSTIC_CODES}. Unconditional
44
44
  * suppression let a module reference a name with no import or declaration
45
45
  * anywhere and still typecheck with 0 errors — the failure then surfaced
46
46
  * as a runtime `ReferenceError`. Both flows now report these at a
@@ -133,7 +133,7 @@ declare module 'chrome:*';
133
133
  * Loose declarations for Firefox test-harness globals (mochitest
134
134
  * browser-chrome and xpcshell), appended AFTER the composed
135
135
  * Firefox-globals + consumer shim when `patchLint.checkJsTestFiles`
136
- * extends the checkJs pass to patch-adopted test scripts (FORGE G5).
136
+ * extends the checkJs pass to patch-adopted test scripts.
137
137
  * Deliberately `any`-typed — the pragmatic posture matches the main
138
138
  * shim. A consumer that wants TYPED harness members (so e.g. a call to
139
139
  * a method the harness does not declare fails at the patch boundary)
@@ -189,7 +189,7 @@ export const SUPPRESSED_DIAGNOSTIC_CODES = new Set([
189
189
  ]);
190
190
  /**
191
191
  * Undefined-free-identifier codes, split out of
192
- * {@link SUPPRESSED_DIAGNOSTIC_CODES} (FORGE F12). Unconditional
192
+ * {@link SUPPRESSED_DIAGNOSTIC_CODES}. Unconditional
193
193
  * suppression let a module reference a name with no import or declaration
194
194
  * anywhere and still typecheck with 0 errors — the failure then surfaced
195
195
  * as a runtime `ReferenceError`. Both flows now report these at a
@@ -222,8 +222,18 @@ async function runTypecheckForProject(ts, projectRoot, projectPath, shimSource,
222
222
  noEmit: true,
223
223
  allowJs: parsed.options.allowJs ?? true,
224
224
  checkJs: parsed.options.checkJs ?? true,
225
+ // No incremental sidecar, ever. A user jsconfig under `engine/` that
226
+ // sets `incremental` (or names a `tsBuildInfoFile`)
227
+ // would have this command drop a `.tsbuildinfo` inside the primary
228
+ // engine checkout — a second writer that invalidates a concurrent
229
+ // `fireforge test`'s engine fingerprint. This command emits nothing,
230
+ // so the sidecar buys nothing either.
231
+ incremental: false,
225
232
  // skipLibCheck is not forced; the user owns it via their jsconfig.
226
233
  };
234
+ // `exactOptionalPropertyTypes` forbids assigning `undefined` to an
235
+ // optional `string`, so the sidecar path is dropped rather than unset.
236
+ delete options.tsBuildInfoFile;
227
237
  // 5) The synthetic shim file. Use a project-rooted path with a
228
238
  // hidden-style prefix so it is unlikely to collide with any real
229
239
  // file — and never write it to disk. The CompilerHost below serves
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
- * Per-invocation xpcshell profile directory (FORGE G15b).
3
+ * Per-invocation xpcshell profile directory.
4
4
  *
5
5
  * Firefox's xpcshell harness defaults `XPCSHELL_TEST_PROFILE_DIR` to a
6
6
  * FIXED path (`$TMPDIR/firefox/xpcshellprofile`), so two overlapping
@@ -25,6 +25,27 @@ export declare abstract class FireForgeError extends Error {
25
25
  export declare class GeneralError extends FireForgeError {
26
26
  readonly code: 1;
27
27
  }
28
+ /**
29
+ * Raised when one of FireForge's own internal invariants did not hold —
30
+ * the failure of a runtime assertion from `src/utils/assert.ts`.
31
+ *
32
+ * Dedicated subclass (rather than raw GeneralError) because an invariant
33
+ * failure is categorically different from every other error the CLI
34
+ * raises: it is a bug in FireForge, not something the operator did or can
35
+ * fix. That difference is worth carrying all the way to the exit status
36
+ * (see {@link ExitCode.INTERNAL_ERROR}) so a CI job can escalate it
37
+ * instead of retrying it, and it is the one error class the CLI boundary
38
+ * prints a stack trace for — the stack is the report.
39
+ *
40
+ * Assertions exist so that a violated invariant stops the run at the
41
+ * first inconsistency instead of continuing to write. In the mutating
42
+ * paths that means the surrounding rollback journal restores the engine
43
+ * before this reaches the operator; see `docs/lifecycle-invariants.md`.
44
+ */
45
+ export declare class InternalInvariantError extends FireForgeError {
46
+ readonly code: 11;
47
+ get userMessage(): string;
48
+ }
28
49
  /**
29
50
  * Raised when a legacy regex/brace-depth fallback parser decides it
30
51
  * cannot safely perform its mutation — e.g. because a block it expected
@@ -85,7 +106,7 @@ export declare class ExecTimeoutError extends FireForgeError {
85
106
  }
86
107
  /**
87
108
  * Error thrown when a file-lock wait times out because another process
88
- * holds the lock (FORGE H5).
109
+ * holds the lock.
89
110
  *
90
111
  * Dedicated subclass so the CLI boundary renders lock contention as the
91
112
  * one-line reason-first/remedy-second refusal it is — before this, the
@@ -36,6 +36,37 @@ export class FireForgeError extends Error {
36
36
  export class GeneralError extends FireForgeError {
37
37
  code = ExitCode.GENERAL_ERROR;
38
38
  }
39
+ /**
40
+ * Raised when one of FireForge's own internal invariants did not hold —
41
+ * the failure of a runtime assertion from `src/utils/assert.ts`.
42
+ *
43
+ * Dedicated subclass (rather than raw GeneralError) because an invariant
44
+ * failure is categorically different from every other error the CLI
45
+ * raises: it is a bug in FireForge, not something the operator did or can
46
+ * fix. That difference is worth carrying all the way to the exit status
47
+ * (see {@link ExitCode.INTERNAL_ERROR}) so a CI job can escalate it
48
+ * instead of retrying it, and it is the one error class the CLI boundary
49
+ * prints a stack trace for — the stack is the report.
50
+ *
51
+ * Assertions exist so that a violated invariant stops the run at the
52
+ * first inconsistency instead of continuing to write. In the mutating
53
+ * paths that means the surrounding rollback journal restores the engine
54
+ * before this reaches the operator; see `docs/lifecycle-invariants.md`.
55
+ */
56
+ export class InternalInvariantError extends FireForgeError {
57
+ code = ExitCode.INTERNAL_ERROR;
58
+ get userMessage() {
59
+ return (`Internal error: ${this.message}\n\n` +
60
+ 'This is a bug in FireForge itself, not a problem with your project — ' +
61
+ 'no configuration change on your side would have prevented it. The ' +
62
+ 'operation stopped at the first inconsistency rather than continuing ' +
63
+ 'to write.\n\n' +
64
+ 'To report this:\n' +
65
+ ' 1. Keep the full output above, including the stack trace.\n' +
66
+ ' 2. Note the command you ran and what the project looked like at the time.\n' +
67
+ ' 3. Open an issue at https://github.com/HominisBrowser/FireForge/issues');
68
+ }
69
+ }
39
70
  /**
40
71
  * Raised when a legacy regex/brace-depth fallback parser decides it
41
72
  * cannot safely perform its mutation — e.g. because a block it expected
@@ -115,7 +146,7 @@ export class ExecTimeoutError extends FireForgeError {
115
146
  }
116
147
  /**
117
148
  * Error thrown when a file-lock wait times out because another process
118
- * holds the lock (FORGE H5).
149
+ * holds the lock.
119
150
  *
120
151
  * Dedicated subclass so the CLI boundary renders lock contention as the
121
152
  * one-line reason-first/remedy-second refusal it is — before this, the
@@ -25,6 +25,16 @@ export declare const ExitCode: {
25
25
  readonly FURNACE_ERROR: 9;
26
26
  /** Patch conflict resolution error */
27
27
  readonly RESOLUTION_ERROR: 10;
28
+ /**
29
+ * An internal invariant did not hold — a bug in FireForge itself, not a
30
+ * user or environment failure. Distinct from GENERAL_ERROR because the
31
+ * two call for opposite responses: a GENERAL_ERROR usually means the
32
+ * operator must change something (fix a config, free a lock, retry), and
33
+ * an INTERNAL_ERROR means nothing the operator can change would have
34
+ * helped and the run should be reported instead. Scripts and CI that
35
+ * retry on failure want to retry the former and escalate the latter.
36
+ */
37
+ readonly INTERNAL_ERROR: 11;
28
38
  /**
29
39
  * `fireforge run --smoke-exit` observed one or more unallowed console
30
40
  * error lines inside the smoke window. Distinct from BUILD_ERROR so CI
@@ -26,6 +26,16 @@ export const ExitCode = {
26
26
  FURNACE_ERROR: 9,
27
27
  /** Patch conflict resolution error */
28
28
  RESOLUTION_ERROR: 10,
29
+ /**
30
+ * An internal invariant did not hold — a bug in FireForge itself, not a
31
+ * user or environment failure. Distinct from GENERAL_ERROR because the
32
+ * two call for opposite responses: a GENERAL_ERROR usually means the
33
+ * operator must change something (fix a config, free a lock, retry), and
34
+ * an INTERNAL_ERROR means nothing the operator can change would have
35
+ * helped and the run should be reported instead. Scripts and CI that
36
+ * retry on failure want to retry the former and escalate the latter.
37
+ */
38
+ INTERNAL_ERROR: 11,
29
39
  /**
30
40
  * `fireforge run --smoke-exit` observed one or more unallowed console
31
41
  * error lines inside the smoke window. Distinct from BUILD_ERROR so CI