@hominis/fireforge 0.48.0 → 0.49.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 (102) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/build-info.json +4 -4
  3. package/dist/src/cli.js +16 -28
  4. package/dist/src/commands/build.js +30 -4
  5. package/dist/src/commands/doctor-engine-eslint.d.ts +3 -0
  6. package/dist/src/commands/doctor-engine-eslint.js +12 -0
  7. package/dist/src/commands/doctor-macos-sdk.js +7 -5
  8. package/dist/src/commands/doctor-source-pin.js +9 -2
  9. package/dist/src/commands/doctor.js +2 -0
  10. package/dist/src/commands/furnace/apply.js +10 -4
  11. package/dist/src/commands/furnace/create-templates.js +0 -3
  12. package/dist/src/commands/lint.js +4 -0
  13. package/dist/src/commands/manifest.d.ts +1 -2
  14. package/dist/src/commands/patch/delete.js +18 -4
  15. package/dist/src/commands/re-export-options.js +7 -0
  16. package/dist/src/commands/re-export-register.js +1 -0
  17. package/dist/src/commands/re-export-removed.d.ts +4 -0
  18. package/dist/src/commands/re-export-removed.js +49 -0
  19. package/dist/src/commands/re-export-single.js +4 -1
  20. package/dist/src/commands/re-export.js +4 -0
  21. package/dist/src/commands/test-browser-preflight.d.ts +28 -0
  22. package/dist/src/commands/test-browser-preflight.js +181 -0
  23. package/dist/src/commands/test-build-phase.js +30 -7
  24. package/dist/src/commands/test-checkjs.d.ts +3 -0
  25. package/dist/src/commands/test-checkjs.js +27 -0
  26. package/dist/src/commands/test-diagnose.js +3 -0
  27. package/dist/src/commands/test-harness-env.d.ts +5 -6
  28. package/dist/src/commands/test-harness-env.js +27 -7
  29. package/dist/src/commands/test-modes.d.ts +2 -4
  30. package/dist/src/commands/test-register.js +11 -5
  31. package/dist/src/commands/test-run.d.ts +2 -0
  32. package/dist/src/commands/test-run.js +21 -1
  33. package/dist/src/commands/test-verdict.d.ts +2 -2
  34. package/dist/src/commands/test.js +83 -211
  35. package/dist/src/core/build-baseline-types.d.ts +12 -14
  36. package/dist/src/core/build-baseline-validate.d.ts +3 -0
  37. package/dist/src/core/build-baseline-validate.js +53 -0
  38. package/dist/src/core/build-baseline.d.ts +4 -3
  39. package/dist/src/core/build-baseline.js +23 -11
  40. package/dist/src/core/component-source-guard.d.ts +20 -0
  41. package/dist/src/core/component-source-guard.js +68 -0
  42. package/dist/src/core/coverage-extend.d.ts +5 -34
  43. package/dist/src/core/coverage-extend.js +26 -37
  44. package/dist/src/core/engine-write-boundary.d.ts +5 -0
  45. package/dist/src/core/engine-write-boundary.js +41 -0
  46. package/dist/src/core/furnace-apply-helpers.js +8 -2
  47. package/dist/src/core/furnace-apply.js +7 -0
  48. package/dist/src/core/furnace-validate-accessibility.js +9 -12
  49. package/dist/src/core/furnace-validate.js +0 -43
  50. package/dist/src/core/gecko-dom-types.d.ts +2 -0
  51. package/dist/src/core/gecko-dom-types.js +17 -0
  52. package/dist/src/core/lit-template-regions.d.ts +4 -0
  53. package/dist/src/core/lit-template-regions.js +50 -0
  54. package/dist/src/core/mach-build-artifacts.d.ts +6 -6
  55. package/dist/src/core/mach-build-artifacts.js +13 -7
  56. package/dist/src/core/mach-resource-shim.d.ts +1 -1
  57. package/dist/src/core/mach-resource-shim.js +2 -0
  58. package/dist/src/core/marionette-port.d.ts +2 -0
  59. package/dist/src/core/marionette-port.js +38 -53
  60. package/dist/src/core/mochitest-server-port.d.ts +1 -1
  61. package/dist/src/core/mochitest-server-port.js +20 -17
  62. package/dist/src/core/objdir-maintenance.d.ts +10 -0
  63. package/dist/src/core/objdir-maintenance.js +167 -0
  64. package/dist/src/core/patch-lint-checkjs.js +2 -1
  65. package/dist/src/core/patch-parse.js +8 -6
  66. package/dist/src/core/preflight-wait.d.ts +2 -0
  67. package/dist/src/core/preflight-wait.js +24 -0
  68. package/dist/src/core/process-owner.d.ts +9 -0
  69. package/dist/src/core/process-owner.js +43 -0
  70. package/dist/src/core/test-harness-crash.d.ts +1 -1
  71. package/dist/src/core/test-harness-crash.js +5 -5
  72. package/dist/src/core/test-host-state.d.ts +14 -0
  73. package/dist/src/core/test-host-state.js +80 -0
  74. package/dist/src/core/test-profile-files.d.ts +17 -0
  75. package/dist/src/core/test-profile-files.js +75 -0
  76. package/dist/src/core/test-stale-check.d.ts +6 -7
  77. package/dist/src/core/test-stale-check.js +9 -32
  78. package/dist/src/core/token-category.d.ts +7 -26
  79. package/dist/src/core/token-category.js +91 -117
  80. package/dist/src/core/token-inventory.js +12 -7
  81. package/dist/src/core/typecheck.js +6 -6
  82. package/dist/src/types/commands/options.d.ts +14 -3
  83. package/dist/src/utils/assert.js +2 -7
  84. package/dist/src/utils/child-env.d.ts +0 -3
  85. package/dist/src/utils/child-env.js +0 -3
  86. package/dist/src/utils/concurrency.d.ts +2 -2
  87. package/dist/src/utils/concurrency.js +15 -4
  88. package/dist/src/utils/fs.js +11 -1
  89. package/dist/src/utils/options.d.ts +16 -1
  90. package/dist/src/utils/options.js +5 -3
  91. package/dist/src/utils/paths.js +3 -2
  92. package/dist/src/utils/process-group.d.ts +1 -2
  93. package/dist/src/utils/process-group.js +1 -2
  94. package/dist/src/utils/process-input.js +0 -3
  95. package/dist/src/utils/process.d.ts +3 -2
  96. package/dist/src/utils/process.js +19 -7
  97. package/docs/configuration.md +12 -11
  98. package/docs/lifecycle-invariants.md +9 -5
  99. package/docs/machine-output.md +36 -19
  100. package/docs/patch-workflow.md +15 -0
  101. package/docs/testing.md +77 -12
  102. package/package.json +6 -5
@@ -4,6 +4,7 @@ import { constants as osConstants } from 'node:os';
4
4
  import { StringDecoder } from 'node:string_decoder';
5
5
  import { ExecTimeoutError } from '../errors/base.js';
6
6
  import { buildChildEnv } from './child-env.js';
7
+ import { toError } from './errors.js';
7
8
  import { dispatchCompleteLines } from './line-dispatch.js';
8
9
  import { killProcessTree, sweepProcessGroup } from './process-group.js';
9
10
  // 50 MB cap per stream to prevent OOM. Large toolchain builds (e.g. Firefox, Chromium)
@@ -169,6 +170,7 @@ async function spawnTracked(spec) {
169
170
  ? installGracefulShutdownForwarder(child, spec.graceMs, killTarget)
170
171
  : undefined;
171
172
  const closure = trackChildClosure();
173
+ let rejection;
172
174
  child.on('error', (error) => {
173
175
  // Abort/startup failure: make sure a partially-started tree does not
174
176
  // outlive the dispatch (a mach dying at startup used to strand
@@ -176,15 +178,24 @@ async function spawnTracked(spec) {
176
178
  if (options.processGroup === true) {
177
179
  killProcessTree(child, 'SIGKILL', usesProcessGroup);
178
180
  }
179
- forwarder?.dispose();
180
- closure.settle();
181
- reject(toExecRejection(error, command, args, options.timeout, spec.rejectionOutput?.()));
181
+ // An abort emits error before close. Keep the dispatch tracked until
182
+ // the child closes and both teardown sweeps finish, including on timeout.
183
+ rejection = toExecRejection(error, command, args, options.timeout, spec.rejectionOutput?.());
182
184
  });
183
185
  child.on('close', (code, signal) => {
184
186
  forwarder?.dispose();
185
187
  const finish = () => {
186
188
  closure.settle();
187
- resolve(spec.result(code, signal));
189
+ if (rejection)
190
+ reject(rejection);
191
+ else {
192
+ try {
193
+ resolve(spec.result(code, signal));
194
+ }
195
+ catch (error) {
196
+ reject(toError(error));
197
+ }
198
+ }
188
199
  };
189
200
  // Group sweep first (it takes the helpers that stayed in the group),
190
201
  // then the caller's own sweep (the ones that did not), then settle.
@@ -194,7 +205,7 @@ async function spawnTracked(spec) {
194
205
  finish();
195
206
  return;
196
207
  }
197
- sweep().then(finish, finish);
208
+ Promise.resolve().then(sweep).then(finish, finish);
198
209
  };
199
210
  if (groupPid !== undefined) {
200
211
  sweepProcessGroup(groupPid).then(callerSweep, callerSweep);
@@ -265,8 +276,9 @@ const activeChildClosures = new Set();
265
276
  * Registers a child whose shutdown {@link waitForActiveChildShutdown} must
266
277
  * wait for, and returns the handle that deregisters it.
267
278
  *
268
- * @returns `settle()`. Call from both the child's `close` and `error`
269
- * handlers so a dead child never holds the shutdown wait open
279
+ * @returns `settle()`. Call after child closure and required teardown finish.
280
+ * An abort's `error` event does not prove closure; do not settle there.
281
+ * A failed start may settle when no live child or teardown remains.
270
282
  */
271
283
  export function trackChildClosure() {
272
284
  let resolveClosed;
@@ -149,17 +149,18 @@ When `patchLint.checkJs` is on, the same command also runs the per-patch
149
149
  checkJs pass that `export` enforces, so a green `typecheck` means
150
150
  export-clean types.
151
151
 
152
- Each project keeps incremental build info in `.fireforge/typecheck/`, one
153
- file per project. A warm run re-checks only the files whose content changed
154
- since the last run. The file name carries a key over everything outside the
155
- program's own files that can change a verdict: the composed shim, the
156
- TypeScript version, the compiler options, `typecheck.undefinedIdentifiers`
157
- and the FireForge version. A change to any of them selects a new file, so a
158
- stale verdict is never replayed, and the project's other build-info files are
159
- deleted. `fireforge typecheck --no-cache` checks every project from scratch
160
- and neither reads nor writes the build info. A jsconfig's own `incremental`
161
- and `tsBuildInfoFile` are still ignored, so nothing is written under
162
- `engine/`.
152
+ Project build info is written in `.fireforge/typecheck/`, outside `engine/`,
153
+ with a key covering the shim, TypeScript version, compiler options and
154
+ FireForge version. Diagnostics always use a fresh project
155
+ program: prior incremental diagnostic state is never restored. Cached and
156
+ `--no-cache` runs therefore check the same complete file set. `--no-cache`
157
+ also disables build-info writes. A jsconfig's own `incremental` and
158
+ `tsBuildInfoFile` remain ignored.
159
+
160
+ Both project and per-patch checkJs provide Gecko DOM constructor
161
+ `isInstance(value)` predicates with instance narrowing, plus
162
+ `MouseEvent.MOZ_SOURCE_PEN`. These declarations retain normal DOM member
163
+ checking; they do not turn constructors or instances into `any`.
163
164
 
164
165
  ## `patchPolicy`
165
166
 
@@ -113,11 +113,15 @@ command, read the decision table at the bottom first.
113
113
  carry primary shebangs, and mach rebuilds them in-tree), and runs
114
114
  `mach configure` inside the tree so the configure-generated root files are
115
115
  regenerated against the tree's paths rather than keeping the primary's. It
116
- then verifies exactly that set afterwards (config.status, backend.mk,
117
- Makefile, config/autoconf.mk, via `findObjdirRelocationViolation`, pinned
118
- by `mach-objdir-relocation.test.ts`). Nested Makefiles are products of the
119
- verified config.status, and `.deps` build products are out of scope as
120
- read-only staleness that the first in-tree rebuild corrects. Finally it
116
+ Before configure, relocation rewrites old-source symlinks, `.pp`/`.d`,
117
+ FasterMake and Cargo fingerprint metadata, and removes copied
118
+ `config.statusd/*/config.track` inventories. Those inventories contain
119
+ absolute WRITE destinations and must never be consumed from a clone.
120
+ Aliased objdirs or partial-config directories are refused. Afterwards
121
+ `findObjdirRelocationViolation` verifies config.status, backend.mk,
122
+ Makefile, config/autoconf.mk and every remaining objdir symlink for the
123
+ old source prefix. External toolchain links are allowed; source links
124
+ must no longer consult the old checkout. Finally it
121
125
  copies `last-build.json` so the in-tree stale-build gates anchor
122
126
  correctly.
123
127
 
@@ -86,15 +86,16 @@ moved.
86
86
  `FIREFORGE-VERDICT: FAIL reason=<reason>` uses a closed set
87
87
  (`FireforgeVerdictReason`, `src/commands/test-verdict.ts`):
88
88
 
89
- | Reason | Meaning |
90
- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
91
- | `crash` | The harness or the browser died, so there is no trustworthy suite result. |
92
- | `no-tests` | The run dispatched but nothing ran. |
93
- | `test-failures` | The suite ran and reported unexpected results. |
94
- | `preflight` | The run was refused before the harness was reached. |
95
- | `inconclusive` | A result exists, but `engine/` moved under it, so the result was discarded. |
96
- | `lock-timeout` | The run never started: the engine session lock stayed contended. |
97
- | `killed` | A signal terminated the run, or FireForge ended it because its parent process vanished. Written from the signal handler (or the parent-exit watchdog) so that a log tail always describes itself. A killed run must never be silent, or "killed", "still running" and "never started" all look alike. `signal=` names the cause: a signal name, or `parent-exit`. |
89
+ | Reason | Meaning |
90
+ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
91
+ | `crash` | The harness or the browser died, so there is no trustworthy suite result. |
92
+ | `harness-arguments` | Profile or other harness argument setup failed before any tests could run; a rebuild is not the repair. |
93
+ | `no-tests` | The run dispatched but nothing ran. |
94
+ | `test-failures` | The suite ran and reported unexpected results. |
95
+ | `preflight` | The run was refused before the harness was reached. |
96
+ | `inconclusive` | A result exists, but `engine/` moved under it or a perf run changed power source, so the result was discarded. |
97
+ | `lock-timeout` | The run never started: the engine session lock stayed contended. |
98
+ | `killed` | A signal terminated the run, or FireForge ended it because its parent process vanished. Written from the signal handler (or the parent-exit watchdog) so that a log tail always describes itself. A killed run must never be silent, or "killed", "still running" and "never started" all look alike. `signal=` names the cause: a signal name, or `parent-exit`. |
98
99
 
99
100
  ### The additive `note=` key
100
101
 
@@ -109,20 +110,19 @@ unchanged and still comes from the closed set above, so a consumer that
109
110
  tokenises `key=value` pairs is unaffected, and a gate that names no class
110
111
  emits no `note=` at all. The classes are not a closed set and may grow, so
111
112
  consumers should treat an unrecognised one as opaque. Current values:
112
- `stale-browser`, `stale-browser-kill-failed`, `marionette-port-busy`,
113
+ `browser-busy`, `stale-browser`, `stale-browser-kill-failed`, `marionette-port-busy`, `mochitest-port-busy`,
113
114
  `coverage-replaced`, `stale-build`, `stale-components`.
114
115
 
115
116
  ### The additive `shuffle=` key
116
117
 
117
- A `fireforge test --shuffle [seed]` run forwards mach's `--shuffle` (an
118
- unseeded shuffle of file order in the harness) and exports the seed FireForge
119
- drew or was given as `FIREFORGE_SHUFFLE_SEED` in the harness environment. The
120
- seed is appended to the verdict line as `shuffle=<seed>` on every emission
121
- form, so a red found by shuffling carries its own reproduction
122
- (`fireforge test --shuffle=<seed> …`) on the one line a pipe keeps. Reordering
123
- tasks _inside_ a file is harness code that reads the exported seed; FireForge
124
- owns the seed, the plumbing and the record. Absent when the run did not
125
- shuffle.
118
+ A `fireforge test --shuffle [seed]` run records the seed used to permute
119
+ FireForge's isolated path-argument shards. Repeating the seed and selection
120
+ repeats that shard order. It also forwards native mach `--shuffle`, which
121
+ randomizes files within an invocation without using this seed. `--no-shard`
122
+ disables FireForge's seeded argument permutation. The seed is exported as
123
+ `FIREFORGE_SHUFFLE_SEED` for custom in-file task code and appended as
124
+ `shuffle=<seed>` to every verdict form. This field does not claim replayable
125
+ native harness file order. Absent when shuffle was not requested.
126
126
 
127
127
  ### The additive `orphans-reaped=` key
128
128
 
@@ -141,3 +141,20 @@ A preflight refusal's own text is written to stdout before the verdict line,
141
141
  and into the run log, so the verdict stays the last stdout write while the
142
142
  reason survives both a redirect and a `tail`. See
143
143
  [`run-logs.md`](run-logs.md).
144
+
145
+ ### Additive host and perf keys
146
+
147
+ `host-load=<number>` records the one-minute load at suite startup. Each suite
148
+ also samples the highest CPU process before and after dispatch. A contention
149
+ warning appends `host-cpu-warning=true`; the diagnostic names the process
150
+ and measured CPU. These diagnostics annotate a test failure without changing
151
+ its classifier reason.
152
+
153
+ Perf runs append `power-source=ac|battery|unknown`. On macOS the source is
154
+ read from `pmset -g ps`, so charging on AC is correctly `ac`. A known source
155
+ transition appends `power-changed=true` and refuses the run as
156
+ `reason=inconclusive`. The producer's sample JSON retains its fields and
157
+ gains `fireforgeHost: { start, end, powerSourceChanged }`, where each host
158
+ state includes `load`, `power` and an optional `topProcess` with CPU and
159
+ command. Consumers should tolerate this additive object. Unknown power
160
+ state is visible but never treated as a proven transition.
@@ -56,6 +56,21 @@ requested path produces no hunk. That is checked against the full requested
56
56
  list rather than the subset left after filtering, so nothing dropped upstream
57
57
  can pass as a silent success.
58
58
 
59
+ An absent file originally created by a selected patch can be retired with
60
+ repeatable `--expect-removed <path>` on scan-less re-export:
61
+
62
+ ```sh
63
+ fireforge re-export 202 --refuse-adjacent-unmanaged --refuse-foreign-drift --expect browser/themes/shared/hominis/widgets.css --expect-removed browser/themes/shared/hominis/stop.svg
64
+ ```
65
+
66
+ The removal must have exactly one selected owner, be absent from disk and
67
+ engine HEAD, and have a `new file mode` section in that owner's patch.
68
+ Every removal is validated before writes, in dry-run too. Other paths keep
69
+ both refusal checks; retirement grants no unmanaged or foreign-drift
70
+ exception. Removing every file would empty the patch and requires `patch
71
+ drop` instead. `--expect-removed` cannot combine with `--scan`, `--files` or
72
+ `--all`.
73
+
59
74
  ## Queue maintenance
60
75
 
61
76
  Under `fireforge patch`:
package/docs/testing.md CHANGED
@@ -34,9 +34,59 @@ build, `test` fails before launching stale artifacts. Use `--build` to
34
34
  refresh, or `--allow-stale-build` only for intentional out-of-band rebuilds.
35
35
 
36
36
  `--build-only` packages mixed-harness paths once, and then each harness half
37
- can run without `--build`. `--extend-coverage` retains prior scoped coverage
38
- instead of replacing it. It is refused when the build anchor moved (engine
39
- HEAD, `engine/mozconfig`, or a previously fingerprinted packageable file).
37
+ can run without `--build`. Scoped builds retain prior coverage automatically
38
+ when engine HEAD, `engine/mozconfig` and recorded staging inputs outside the
39
+ rebuilt scope are unchanged. Changed files explicitly included in this
40
+ build are allowed. The fingerprints include test scripts and support files,
41
+ not only packageable chrome sources. A failed anchor narrows the claim and
42
+ prints the dropped paths; `--extend-coverage` instead refuses. Baselines
43
+ without complete staging-input fingerprints require a new build before they
44
+ can retain prior coverage.
45
+ New dirty inputs outside the rebuilt scope invalidate retention conservatively,
46
+ including shared fixtures outside test directories.
47
+
48
+ When `patchLint.checkJs` and `patchLint.checkJsTestFiles` are enabled, named
49
+ pre-test builds check the selected test scripts before deployment or mach.
50
+ This includes new scripts not yet owned by an exported patch.
51
+
52
+ ### Shared browsers, ports and host state
53
+
54
+ Browser ownership is checked before a pre-test deployment and before full
55
+ or UI builds. A live or unreadable parent means busy, with its owner named
56
+ when available. `--kill-stale-marionette` requires a dead parent plus harness
57
+ arguments; a profile argument alone does not prove abandonment. A plain
58
+ launchd-parented app remains unattributed and is not automatically killed.
59
+
60
+ `test --wait-browser [seconds]` waits for this objdir's browser to exit;
61
+ `test --wait-port [seconds]` waits for mochitest and Marionette listeners.
62
+ `build --wait-browser [seconds]` queues a full or UI build behind the browser.
63
+ Bare flags wait 60 seconds; explicit values accept 1–3600 seconds. They are
64
+ independent of the FireForge lock budget. On expiry the normal preflight
65
+ refusal and its remedy are preserved.
66
+
67
+ Every suite samples host load and the highest CPU process before and after
68
+ dispatch. Load at least 4 or a process at least 90% CPU produces a warning;
69
+ verdicts include `host-load` and, when needed, `host-cpu-warning=true`.
70
+ Perf runs include `power-source=ac|battery|unknown`; on macOS this comes from
71
+ `pmset -g ps`, including charging on AC. Sample JSON gains a `fireforgeHost`
72
+ object with start/end states and `powerSourceChanged`. A known power-source
73
+ transition produces `FAIL reason=inconclusive power-changed=true`.
74
+ Unknown power state is reported without inventing a transition.
75
+
76
+ ### Profile overlays
77
+
78
+ Use repeatable `--profile-file source=relative/destination` to add files to
79
+ the test profile, for example:
80
+
81
+ ```sh
82
+ fireforge test --perf-samples sample.json --profile-file arm.css=chrome/userChrome.css browser/base/content/test/perf/browser_perf.js
83
+ ```
84
+
85
+ The source resolves from the project root. Destinations must stay relative
86
+ with no traversal. FireForge stages copies and merges only those staged
87
+ directories into the harness profile, preserving existing chrome files.
88
+ A raw `--extra-profile-file` directory collision is classified as
89
+ `reason=harness-arguments` and names this remedy rather than a rebuild.
40
90
 
41
91
  ### What a pre-test build costs
42
92
 
@@ -183,15 +233,16 @@ redirected run keeps the reason as well as the verdict. See
183
233
 
184
234
  ## Seeded shuffle
185
235
 
186
- `fireforge test --shuffle` runs a mochitest selection in a seeded random file
187
- order and prints the seed; `--shuffle=<seed>` replays it. FireForge forwards
188
- mach's own `--shuffle`, exports `FIREFORGE_SHUFFLE_SEED=<seed>` to the harness
189
- so in-file task reordering (harness `head.js` code, not FireForge) can key off
190
- the same number, and stamps `shuffle=<seed>` on the verdict line. It is
191
- mochitest-only: the xpcshell harness has no shuffle, and generic `mach test`
192
- dispatch would hand the flag to every suite. Write the flag as
193
- `--shuffle=<seed>` or after the paths, because a bare `--shuffle <path>` reads
194
- the path as the seed.
236
+ `fireforge test --shuffle` deterministically permutes isolated mochitest path
237
+ arguments and prints the seed; `--shuffle=<seed>` replays that shard order for
238
+ the same input selection. A directory remains one invocation. `--no-shard`
239
+ disables this argument permutation. FireForge also forwards native mach
240
+ `--shuffle` within each invocation, whose file order remains unseeded and is
241
+ not replayed by the FireForge seed. `FIREFORGE_SHUFFLE_SEED=<seed>` is available
242
+ to custom harness `head.js` code for in-file task ordering, and `shuffle=<seed>`
243
+ is recorded on the verdict. The option is mochitest-only. Write
244
+ `--shuffle=<seed>` or place it after the paths because a bare
245
+ `--shuffle <path>` consumes the path as its optional seed.
195
246
 
196
247
  Exit code 14 (`INCONCLUSIVE`) is not red: it means `engine/` moved while the
197
248
  harness ran and the result was thrown away. Exit 15 (`LOCK_TIMEOUT`) means
@@ -256,3 +307,17 @@ instead, use a [verification tree](verification-trees.md).
256
307
  In dev builds, files under `obj-*/dist/bin` may be symlinks back into the
257
308
  source tree (prefs especially), so edit source prefs directly and keep a
258
309
  backup before bisection experiments.
310
+
311
+ ## Repository release gate
312
+
313
+ Run `npm run release:check` with the exact Node version in `.nvmrc` and npm
314
+ version in `package.json`'s `packageManager`. The gate verifies these pins
315
+ before formatting, whitespace, zero-warning lint, strict typecheck, development
316
+ and production dead-code checks, import cycles, coverage floors, and installed
317
+ package smoke tests. `prepublishOnly` and the CI quality job run the same gate.
318
+ Supported Node-version and OS matrices continue to run ordinary tests separately.
319
+
320
+ The full-Firefox integration runner is opt-in. It snapshots project files,
321
+ engine changes and the index before arming recovery; a refusal cannot erase an
322
+ existing workspace. Failed recovery fails the run, preserves affected paths and
323
+ writes recovery evidence for manual repair.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.48.0",
3
+ "version": "0.49.1",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -39,8 +39,9 @@
39
39
  "format:check": "prettier --check --ignore-unknown \"src/**/*.ts\" \"bin/**/*.ts\" \"scripts/**/*.mjs\" \"eslint-rules/**/*.js\" \"package.json\" \"eslint.config.js\" \"vitest.config.ts\" \"vitest.pack.config.ts\" \"tsconfig.json\" \"tsconfig.build.json\" \".lintstagedrc.json\" \".prettierignore\" \".prettierrc\" \".gitignore\" \"README.md\" \"CHANGELOG.md\" \"docs/**/*.md\" \"knip.json\" \"knip.production.json\"",
40
40
  "prepare": "node -e \"import('./scripts/prepare.mjs').catch((error) => { if (error && typeof error === 'object' && 'code' in error && error.code === 'ERR_MODULE_NOT_FOUND') process.exit(0); throw error; })\"",
41
41
  "prepack": "npm run build && node ./scripts/generate-build-info.mjs --loud",
42
- "release:check": "npm run format:check && npm run whitespace:check && npm run lint:ci && npm run typecheck && npm run deadcode:check && npm run deadcode:check:production && npm run cycles:check && npm run test:coverage && npm run pack:verify",
43
- "prepublishOnly": "npm run release:check"
42
+ "release:check": "npm run toolchain:check && npm run format:check && npm run whitespace:check && npm run lint:ci && npm run typecheck && npm run deadcode:check && npm run deadcode:check:production && npm run cycles:check && npm run test:coverage && npm run pack:verify",
43
+ "prepublishOnly": "npm run release:check",
44
+ "toolchain:check": "node ./scripts/check-release-toolchain.mjs"
44
45
  },
45
46
  "files": [
46
47
  "dist/",
@@ -75,9 +76,9 @@
75
76
  "eslint-plugin-simple-import-sort": "^14.0.0",
76
77
  "fast-check": "^4.6.0",
77
78
  "husky": "^9.1.7",
78
- "knip": "6.37.0",
79
+ "knip": "6.39.0",
79
80
  "lint-staged": "^17.0.4",
80
- "prettier": "3.9.8",
81
+ "prettier": "3.9.9",
81
82
  "tsx": "^4.7.0",
82
83
  "typescript": "~6.0.0",
83
84
  "typescript-eslint": "^8.0.0",