@hominis/fireforge 0.50.0 → 0.51.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 (205) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/build-info.json +4 -4
  3. package/dist/src/commands/check-report.d.ts +17 -4
  4. package/dist/src/commands/check-report.js +19 -6
  5. package/dist/src/commands/check-run.d.ts +5 -0
  6. package/dist/src/commands/check-run.js +9 -1
  7. package/dist/src/commands/check-steps.d.ts +8 -0
  8. package/dist/src/commands/check-steps.js +31 -6
  9. package/dist/src/commands/check.js +1 -1
  10. package/dist/src/commands/declarations-report.d.ts +43 -0
  11. package/dist/src/commands/declarations-report.js +76 -0
  12. package/dist/src/commands/export-placement-gate.js +8 -0
  13. package/dist/src/commands/furnace/rename-registrations.js +7 -2
  14. package/dist/src/commands/furnace/validate.js +26 -1
  15. package/dist/src/commands/lint-per-patch.js +6 -2
  16. package/dist/src/commands/lint-per-run-checkjs.js +2 -2
  17. package/dist/src/commands/lint-report.js +7 -2
  18. package/dist/src/commands/manifest.js +2 -0
  19. package/dist/src/commands/park.d.ts +22 -0
  20. package/dist/src/commands/park.js +232 -0
  21. package/dist/src/commands/patch/delete.js +11 -6
  22. package/dist/src/commands/patch/index.d.ts +3 -3
  23. package/dist/src/commands/patch/index.js +8 -4
  24. package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
  25. package/dist/src/commands/patch/lint-ignore-from.js +142 -0
  26. package/dist/src/commands/patch/lint-ignore.js +39 -29
  27. package/dist/src/commands/patch/move-files-create.js +11 -4
  28. package/dist/src/commands/patch/move-files-into.js +10 -4
  29. package/dist/src/commands/patch/move-files.js +3 -1
  30. package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
  31. package/dist/src/commands/patch/removal-created-files.js +83 -0
  32. package/dist/src/commands/patch/rename.js +15 -3
  33. package/dist/src/commands/patch/reserve.d.ts +33 -0
  34. package/dist/src/commands/patch/reserve.js +152 -0
  35. package/dist/src/commands/patch/retire.js +11 -6
  36. package/dist/src/commands/patch/split-plan.d.ts +10 -1
  37. package/dist/src/commands/patch/split-plan.js +16 -1
  38. package/dist/src/commands/patch/split.js +14 -5
  39. package/dist/src/commands/patch/stage.d.ts +34 -0
  40. package/dist/src/commands/patch/stage.js +153 -0
  41. package/dist/src/commands/patch/staged-dependency-remove.d.ts +31 -0
  42. package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
  43. package/dist/src/commands/patch/staged-dependency.js +40 -98
  44. package/dist/src/commands/re-export-adjacent.d.ts +5 -0
  45. package/dist/src/commands/re-export-adjacent.js +4 -3
  46. package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
  47. package/dist/src/commands/re-export-bulk-scan.js +22 -7
  48. package/dist/src/commands/re-export-hunks.d.ts +35 -0
  49. package/dist/src/commands/re-export-hunks.js +220 -0
  50. package/dist/src/commands/re-export-options.js +29 -2
  51. package/dist/src/commands/re-export-purity-scope.d.ts +18 -0
  52. package/dist/src/commands/re-export-purity-scope.js +131 -0
  53. package/dist/src/commands/re-export-register.js +15 -1
  54. package/dist/src/commands/re-export-removed.d.ts +1 -1
  55. package/dist/src/commands/re-export-removed.js +17 -2
  56. package/dist/src/commands/re-export-scan.d.ts +2 -0
  57. package/dist/src/commands/re-export-scan.js +4 -1
  58. package/dist/src/commands/re-export-single.js +102 -42
  59. package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
  60. package/dist/src/commands/re-export-stale-registrations.js +62 -0
  61. package/dist/src/commands/re-export.js +35 -34
  62. package/dist/src/commands/test-build-furnace.d.ts +3 -2
  63. package/dist/src/commands/test-build-furnace.js +8 -2
  64. package/dist/src/commands/test-build-phase.js +24 -7
  65. package/dist/src/commands/test-checkjs.js +3 -2
  66. package/dist/src/commands/test-dispatch.d.ts +39 -0
  67. package/dist/src/commands/test-dispatch.js +211 -0
  68. package/dist/src/commands/test-furnace-reach.d.ts +37 -0
  69. package/dist/src/commands/test-furnace-reach.js +132 -0
  70. package/dist/src/commands/test-orchestrate.d.ts +9 -0
  71. package/dist/src/commands/test-orchestrate.js +22 -4
  72. package/dist/src/commands/test-perf-scope.d.ts +21 -0
  73. package/dist/src/commands/test-perf-scope.js +55 -0
  74. package/dist/src/commands/test-prelock.js +100 -4
  75. package/dist/src/commands/test-register.js +18 -3
  76. package/dist/src/commands/test-results.d.ts +62 -0
  77. package/dist/src/commands/test-results.js +62 -0
  78. package/dist/src/commands/test-run.d.ts +13 -55
  79. package/dist/src/commands/test-run.js +73 -101
  80. package/dist/src/commands/test-shards.d.ts +110 -0
  81. package/dist/src/commands/test-shards.js +255 -0
  82. package/dist/src/commands/test-stale-gate.d.ts +5 -1
  83. package/dist/src/commands/test-stale-gate.js +4 -2
  84. package/dist/src/commands/test-verdict.d.ts +2 -1
  85. package/dist/src/commands/test.d.ts +1 -1
  86. package/dist/src/commands/test.js +65 -75
  87. package/dist/src/commands/token-lint.js +48 -8
  88. package/dist/src/commands/verify.js +9 -2
  89. package/dist/src/core/build-prepare.d.ts +6 -0
  90. package/dist/src/core/build-prepare.js +29 -3
  91. package/dist/src/core/config-paths.d.ts +1 -1
  92. package/dist/src/core/config-paths.js +2 -0
  93. package/dist/src/core/config-validate-check.js +83 -7
  94. package/dist/src/core/config-validate-test-toolchains.js +14 -0
  95. package/dist/src/core/config-validate-token-scales.d.ts +3 -0
  96. package/dist/src/core/config-validate-token-scales.js +47 -5
  97. package/dist/src/core/config-validate.js +6 -0
  98. package/dist/src/core/coverage-extend.js +2 -2
  99. package/dist/src/core/coverage-records.d.ts +11 -0
  100. package/dist/src/core/coverage-records.js +34 -0
  101. package/dist/src/core/engine-session-lock.d.ts +6 -0
  102. package/dist/src/core/engine-session-lock.js +10 -2
  103. package/dist/src/core/file-lock-shared.d.ts +57 -0
  104. package/dist/src/core/file-lock-shared.js +117 -0
  105. package/dist/src/core/file-lock.d.ts +9 -0
  106. package/dist/src/core/file-lock.js +58 -2
  107. package/dist/src/core/furnace-apply.js +10 -10
  108. package/dist/src/core/furnace-config-validate-exceptions.d.ts +10 -3
  109. package/dist/src/core/furnace-config-validate-exceptions.js +39 -8
  110. package/dist/src/core/furnace-config-validate.js +7 -1
  111. package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
  112. package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
  113. package/dist/src/core/furnace-validate-accessibility.d.ts +4 -2
  114. package/dist/src/core/furnace-validate-accessibility.js +61 -27
  115. package/dist/src/core/furnace-validate-exceptions.d.ts +12 -4
  116. package/dist/src/core/furnace-validate-exceptions.js +41 -4
  117. package/dist/src/core/furnace-validate-icon-button.js +30 -1
  118. package/dist/src/core/history-log-file.js +8 -1
  119. package/dist/src/core/line-prefixer.d.ts +18 -0
  120. package/dist/src/core/line-prefixer.js +34 -0
  121. package/dist/src/core/mach.d.ts +7 -0
  122. package/dist/src/core/mach.js +24 -9
  123. package/dist/src/core/module-resolution-preflight.d.ts +19 -3
  124. package/dist/src/core/module-resolution-preflight.js +24 -6
  125. package/dist/src/core/negative-control-compiled.d.ts +27 -0
  126. package/dist/src/core/negative-control-compiled.js +73 -0
  127. package/dist/src/core/negative-control-diff.d.ts +14 -2
  128. package/dist/src/core/negative-control-diff.js +49 -6
  129. package/dist/src/core/negative-control.d.ts +12 -0
  130. package/dist/src/core/negative-control.js +48 -5
  131. package/dist/src/core/park-store.d.ts +63 -0
  132. package/dist/src/core/park-store.js +216 -0
  133. package/dist/src/core/patch-export-update.d.ts +4 -0
  134. package/dist/src/core/patch-export-update.js +12 -3
  135. package/dist/src/core/patch-export.d.ts +3 -2
  136. package/dist/src/core/patch-export.js +21 -14
  137. package/dist/src/core/patch-files.js +26 -8
  138. package/dist/src/core/patch-lint-checkjs-any-import.d.ts +6 -4
  139. package/dist/src/core/patch-lint-checkjs-any-import.js +9 -7
  140. package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
  141. package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
  142. package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
  143. package/dist/src/core/patch-lint-checkjs.js +13 -3
  144. package/dist/src/core/patch-lint-cross.d.ts +3 -1
  145. package/dist/src/core/patch-lint-cross.js +5 -0
  146. package/dist/src/core/patch-lint-css.js +14 -10
  147. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +5 -0
  148. package/dist/src/core/patch-lint-forward-registration-extended.js +1 -1
  149. package/dist/src/core/patch-lint-ignore.d.ts +56 -12
  150. package/dist/src/core/patch-lint-ignore.js +173 -43
  151. package/dist/src/core/patch-lint-ownership.d.ts +10 -0
  152. package/dist/src/core/patch-lint-ownership.js +26 -0
  153. package/dist/src/core/patch-lint-size.js +9 -1
  154. package/dist/src/core/patch-lint-token-scale.d.ts +26 -8
  155. package/dist/src/core/patch-lint-token-scale.js +206 -29
  156. package/dist/src/core/patch-lint.js +2 -2
  157. package/dist/src/core/patch-manifest-io.js +8 -1
  158. package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
  159. package/dist/src/core/patch-manifest-key-order.js +61 -0
  160. package/dist/src/core/patch-manifest-stage.d.ts +46 -0
  161. package/dist/src/core/patch-manifest-stage.js +96 -0
  162. package/dist/src/core/patch-manifest-validate.js +27 -1
  163. package/dist/src/core/patch-order-legal.d.ts +2 -1
  164. package/dist/src/core/patch-order-legal.js +7 -2
  165. package/dist/src/core/patch-policy.d.ts +6 -2
  166. package/dist/src/core/patch-policy.js +14 -7
  167. package/dist/src/core/patch-retired.d.ts +26 -4
  168. package/dist/src/core/patch-retired.js +40 -2
  169. package/dist/src/core/run-log.d.ts +6 -0
  170. package/dist/src/core/run-log.js +17 -1
  171. package/dist/src/core/test-harness-crash.d.ts +2 -1
  172. package/dist/src/core/test-harness-crash.js +5 -8
  173. package/dist/src/core/test-harness-signature.d.ts +16 -0
  174. package/dist/src/core/test-harness-signature.js +19 -1
  175. package/dist/src/core/test-raw-log.d.ts +66 -0
  176. package/dist/src/core/test-raw-log.js +122 -0
  177. package/dist/src/core/test-run-completeness.d.ts +36 -3
  178. package/dist/src/core/test-run-completeness.js +57 -17
  179. package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
  180. package/dist/src/core/test-xpcshell-retry.js +2 -1
  181. package/dist/src/core/token-lint.d.ts +5 -2
  182. package/dist/src/core/token-lint.js +8 -3
  183. package/dist/src/core/token-sibling-sheets.d.ts +46 -0
  184. package/dist/src/core/token-sibling-sheets.js +119 -0
  185. package/dist/src/core/tree-guard.js +2 -0
  186. package/dist/src/core/typecheck-shim.d.ts +5 -1
  187. package/dist/src/core/typecheck-shim.js +115 -2
  188. package/dist/src/core/typecheck.js +3 -1
  189. package/dist/src/index.d.ts +9 -3
  190. package/dist/src/index.js +4 -1
  191. package/dist/src/types/commands/index.d.ts +2 -2
  192. package/dist/src/types/commands/options.d.ts +68 -7
  193. package/dist/src/types/commands/patches.d.ts +43 -0
  194. package/dist/src/types/config.d.ts +74 -2
  195. package/dist/src/types/furnace.d.ts +20 -0
  196. package/dist/src/types/index.d.ts +1 -1
  197. package/dist/src/utils/options.d.ts +3 -2
  198. package/dist/src/utils/options.js +9 -4
  199. package/docs/configuration.md +94 -9
  200. package/docs/furnace.md +43 -1
  201. package/docs/lifecycle-invariants.md +13 -11
  202. package/docs/machine-output.md +64 -18
  203. package/docs/patch-workflow.md +174 -13
  204. package/docs/testing.md +73 -4
  205. package/package.json +3 -3
@@ -21,10 +21,11 @@ command, read the decision table at the bottom first.
21
21
  take the `.fireforge/engine-session.lock`, then subsystem locks such as
22
22
  build, furnace, or patch-directory locks. Furnace mutations serialize on a
23
23
  per-root lock, and patch directory mutations (filename allocation plus
24
- manifest writes) on a per-directory lock. Dry runs hold the engine-session
25
- lock too, because a `re-export --dry-run` over an untracked binary legally
26
- touches the git index, and holding the lock serializes even "read-only"
27
- previews against real writers.
24
+ manifest writes) on a per-directory lock. `re-export --dry-run` is the
25
+ exception: it writes nothing, runs its git plumbing on a private index
26
+ (so even the `--intent-to-add` staging an untracked binary needs never
27
+ reaches the shared `.git/index`), and serializes only against other dry
28
+ runs, so a preview no longer queues behind a peer's build or test run.
28
29
 
29
30
  3. **A furnace mutation that does not finish restores the engine or says
30
31
  so.** Both a signal (SIGINT or SIGTERM) and a thrown error trigger a
@@ -348,13 +349,14 @@ real-procfs `withFileLock` reap of a live-PID stale record.
348
349
  The project-wide engine-session lock under `.fireforge/`, stamped with PID,
349
350
  command, and start time metadata. CLI `build`, `test`, `export`, `re-export`,
350
351
  `furnace apply`, `furnace deploy` and `furnace sync` take it before subsystem
351
- locks, unconditionally, including dry runs: a `re-export --dry-run` over an
352
- untracked binary legally touches the git index (temporary `--intent-to-add`
353
- staging), so holding the lock serializes even "read-only" previews against
354
- real writers. Dry-run purity is separately enforced at runtime by
355
- `withDryRunPurityGuard` (`re-export-bulk-scan.ts`), which fingerprints the
356
- engine tree and every patch artifact around the dry run and fails hard on
357
- divergence.
352
+ locks. `re-export --dry-run` does not: it runs inside `withPrivateGitIndex`,
353
+ so its temporary `--intent-to-add` staging lands on a throwaway index, and it
354
+ serializes only against other dry runs. Dry-run purity is enforced at runtime
355
+ by `withDryRunPurityGuard` (`re-export-bulk-scan.ts`). For `re-export` it
356
+ fingerprints what the dry run could touch (the engine HEAD and index, the
357
+ selected patches' bodies, their `patches.json` rows and the engine files they
358
+ own) and fails hard on divergence, naming a foreign writer when the history
359
+ log or the engine-session lock shows one in the window (`re-export-purity-scope.ts`).
358
360
 
359
361
  Contention fails fast after about a second by default, with the fixed message
360
362
  "Another FireForge engine-mutating command is already running. Wait for it to
@@ -53,21 +53,21 @@ if (options.json === true) {
53
53
 
54
54
  ## Current surfaces
55
55
 
56
- | Surface | Shape | Notes |
57
- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
58
- | `status --json` | `{ schemaVersion: 1, … }` | Error envelope on three engine preconditions |
59
- | `status --json` under `--check` | adds `check: { enabled, failOn, allow?, failOnUnknown?, failed, offenders }` | Since 0.50.0 on the full payload too, not only `--summary`. `allow` and `failOnUnknown` appear only when set. An offender failing only through `--fail-on-unknown` carries `unnamed: true`. `--patches` adds `scope: { patches }` |
60
- | `furnace validate --json` | `{ schemaVersion: 1, passed, summary, maxWarnings?, components, issues, excepted, staleExceptions }` | `issues` counts after `validationExceptions`, stale-exception warnings included. `excepted` carries each suppressed finding with its `reason` |
61
- | `check --json` | `{ schemaVersion: 1, verdict, reason?, summary, steps }` | One entry per step: `name`, `kind`, `command`, `status` (`pass`/`fail`/`skipped`), `exitCode`, `durationMs`, `detail`, and on failure `outputTail` (`timedOut` when killed at its budget). No verdict line under `--json` |
62
- | `check` verdict line | `FIREFORGE-VERDICT: PASS\|FAIL [reason=step-failures\|lock-timeout failed=<steps>] steps=<n> skipped=<n>` | Not JSON. The last stdout line, after the step table, sealed like `test`'s. Its `reason=` set is its own, not `test`'s |
63
- | `tree list --json` | `{ schemaVersion: 1, trees }` | Error envelope added in 0.44.0 |
64
- | `token list --json` | `{ schemaVersion: 1, tokensCssPath, categories }` | Categories in file order, each with its tokens (`name`, `line`, `value`) |
65
- | `token show --json` | `{ schemaVersion: 1, name, category, declarations }` | One entry per declaring block, with its selector trail and line |
66
- | `token lint --json` | `{ schemaVersion: 1, tokensCssPath, tokenPrefix, filesScanned, findings }` | Each finding has `check`, `file`, `line`, `token`, `message`. The document is written even when findings exist; the exit is then non-zero, as with `status --check --json` |
67
- | `test` verdict line | `FIREFORGE-VERDICT: PASS\|FAIL reason=… [note=<class>] [started=<n>/<m> first-unstarted=<path>] [runs=<p>/<n>] [control=<outcome>] [shuffle=<seed>] [furnace-applied=<names>\|furnace-apply=skipped] [log=<path>]` | Not JSON. A single final stdout line, sealed by `setStdoutSealed` so nothing can displace it. `log=` names the run's own complete output under `.fireforge/logs/`, so a piped or truncated run still leaves a re-readable artifact |
68
- | `lint --per-patch --report <path>` | `{ schemaVersion: 1, patches, queueIssues, totals, … }` | Written to a file. `queueIssues` holds the cross-patch findings; `patch staged-dependency --add --from` reads back their `stagedDeclarations` |
69
- | `status --raw` | `git status --porcelain` lines | Not JSON, but still a machine surface: it engages the same stdout discipline, so rules 3 and 4 apply and rules 1 and 2 do not. A clean tree prints nothing |
70
- | `build` log announcement | `Full build output: <path>` | Not JSON. `build` prints no verdict line, so it names its run log directly on the way out |
56
+ | Surface | Shape | Notes |
57
+ | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
58
+ | `status --json` | `{ schemaVersion: 1, … }` | Error envelope on three engine preconditions |
59
+ | `status --json` under `--check` | adds `check: { enabled, failOn, allow?, failOnUnknown?, failed, offenders }` | Since 0.50.0 on the full payload too, not only `--summary`. `allow` and `failOnUnknown` appear only when set. An offender failing only through `--fail-on-unknown` carries `unnamed: true`. `--patches` adds `scope: { patches }` |
60
+ | `furnace validate --json` | `{ schemaVersion: 1, passed, summary, maxWarnings?, components, issues, excepted, staleExceptions }` | `issues` counts after `validationExceptions`, stale-exception warnings included. `excepted` carries each suppressed finding with its `reason` |
61
+ | `check --json` | `{ schemaVersion: 1, verdict, reason?, summary, steps }` | One entry per step: `name`, `kind`, `command`, `status` (`pass`/`fail`/`skipped`), `exitCode`, `durationMs`, `detail`, and on failure `outputTail` (`timedOut` when killed at its budget). No verdict line under `--json` |
62
+ | `check` verdict line | `FIREFORGE-VERDICT: PASS\|FAIL [reason=step-failures\|lock-timeout\|skipped-steps failed=<steps>] steps=<n> skipped=<n> [skipped-steps=<steps>]` | Not JSON. The last stdout line, after the step table, sealed like `test`'s. Its `reason=` set is its own, not `test`'s |
63
+ | `tree list --json` | `{ schemaVersion: 1, trees }` | Error envelope added in 0.44.0 |
64
+ | `token list --json` | `{ schemaVersion: 1, tokensCssPath, categories }` | Categories in file order, each with its tokens (`name`, `line`, `value`) |
65
+ | `token show --json` | `{ schemaVersion: 1, name, category, declarations }` | One entry per declaring block, with its selector trail and line |
66
+ | `token lint --json` | `{ schemaVersion: 1, tokensCssPath, tokenPrefix, filesScanned, findings }` | Each finding has `check`, `file`, `line`, `token`, `message`. The document is written even when findings exist; the exit is then non-zero, as with `status --check --json` |
67
+ | `test` verdict line | `FIREFORGE-VERDICT: PASS\|FAIL reason=… [note=<class>] [started=<n>/<m> first-unstarted=<path> started-source=<src>] [shards=<p>/<t> [failed-shards=<labels>]] [runs=<p>/<n>] [control=<outcome>] [shuffle=<seed>] [furnace-applied=<names>\|furnace-apply=skipped] [log=<path>]` | Not JSON. A single final stdout line, sealed by `setStdoutSealed` so nothing can displace it. `log=` names the run's own complete output under `.fireforge/logs/`, so a piped or truncated run still leaves a re-readable artifact |
68
+ | `lint --per-patch --report <path>` | `{ schemaVersion: 1, patches, queueIssues, totals, … }` | Written to a file. `queueIssues` holds the cross-patch findings; `patch staged-dependency --add --from` reads back their `stagedDeclarations` |
69
+ | `status --raw` | `git status --porcelain` lines | Not JSON, but still a machine surface: it engages the same stdout discipline, so rules 3 and 4 apply and rules 1 and 2 do not. A clean tree prints nothing |
70
+ | `build` log announcement | `Full build output: <path>` | Not JSON. `build` prints no verdict line, so it names its run log directly on the way out |
71
71
 
72
72
  `--raw` is why `cli.ts` reads `process.argv` for `--json` or `--raw` before
73
73
  dispatching. Rule 3 is about who owns stdout, and that question is settled
@@ -80,6 +80,19 @@ it. Follow the rules above rather than inventing a shape.
80
80
 
81
81
  The run logs behind `log=` are specified in [`run-logs.md`](run-logs.md).
82
82
 
83
+ Additive keys since 0.51.0 outside `test`:
84
+
85
+ - `check --json`: a skipped step carries `skipKind`, `config` when
86
+ `check.skip` names it and `unavailable` when the project has nothing for
87
+ it to run. The verdict line adds `skipped-steps=<steps>` whenever a step
88
+ is skipped, and `reason=skipped-steps` when `check.failOnSkipped` fails a
89
+ run on an `unavailable` skip alone.
90
+ - `furnace validate --json`: a finding about particular elements carries
91
+ `tags` (`no-keyboard-handler` names each synthetic element with an
92
+ `@click`). An exception that matched an error adds a
93
+ `validation-exception-on-error` error, and a full run under
94
+ `furnace.json#minComponents` a `component-floor` error on `furnace.json`.
95
+
83
96
  ## Versioning
84
97
 
85
98
  `schemaVersion` is `MACHINE_OUTPUT_SCHEMA_VERSION` in
@@ -159,6 +172,35 @@ that has none. A run refused only for a non-zero `unexpected=` count or a
159
172
  mid-test shutdown carries neither. Absent on every other line. See
160
173
  [`testing.md`](testing.md#the-verdict-line).
161
174
 
175
+ `started-source=raw|stream|capture` follows them and names the evidence the
176
+ count came from: the mozlog structured log (a run with `--results` or
177
+ `--shard-size`), the complete streamed output (every other run), or, only
178
+ when nothing was streamed, the last 2 MB of the capture.
179
+
180
+ ### Shards: `FIREFORGE-SHARD:` and `failed-shards=`
181
+
182
+ A sharded run prints one `FIREFORGE-SHARD:` line per finished shard, before
183
+ the verdict line:
184
+
185
+ ```
186
+ FIREFORGE-SHARD: PASS|FAIL [reason=…] [checks=<n>] [unexpected=<n>] shard=<i>/<n> path=<label> [failed-files=<path>,…]
187
+ ```
188
+
189
+ `path=` is the shard's path argument (whitespace replaced by `_`), or a
190
+ `--shard-size`/`--jobs` chunk's first file plus `+<k>`. `failed-files=`
191
+ appears under `--shard-size` and names the chunk's files that failed. The
192
+ aggregate verdict appends `failed-shards=<label>,…` after `shards=` when any
193
+ shard did not pass; under `--shard-size` `shards=` counts files and
194
+ `failed-shards=` names files. Both are absent on a green run.
195
+
196
+ ### Additive keys of parallel and per-file runs
197
+
198
+ `jobs=<n>` marks a `--jobs` run, `lock=shared` a run that held the engine
199
+ session lock shared, and `results=<path>` names the `--results` document. A
200
+ run whose engine changed mid-run only in files the running browser cannot
201
+ see keeps its verdict and appends `drift-tolerated=<n>`. A negative control
202
+ on compiled source appends `control-build=compiled`.
203
+
162
204
  ### The additive `shuffle=` key
163
205
 
164
206
  A `fireforge test --shuffle [seed]` run records the seed used to permute
@@ -210,8 +252,10 @@ the verdict `reason=inconclusive` only when HEAD moved or a changed path is
210
252
  packaged by the running build: a source an objdir install manifest names, a
211
253
  directory one installs from, or a test file. Edits that no manifest installs
212
254
  (compiled sources, build files, docs) leave the sample standing with a
213
- warning naming them. Other runs keep the strict rule: any change is
214
- inconclusive.
255
+ warning naming them. Other runs are inconclusive only when a changed path is
256
+ one the running browser can see: a file the objdir links to its source, one
257
+ staged at dispatch, or a test file or staging input of the run; anything
258
+ else leaves the verdict standing with `drift-tolerated=<n>`.
215
259
 
216
260
  ### Additive Furnace keys
217
261
 
@@ -219,4 +263,6 @@ A `fireforge test --build` or `--build-only` run whose pre-test Furnace apply
219
263
  deployed components appends `furnace-applied=<name>,<name>…`: the components
220
264
  whose `components/` source changed since their last deploy, which the
221
265
  runtime under test therefore includes. `--no-furnace-apply` skips that apply
222
- and appends `furnace-apply=skipped`. Both are absent otherwise.
266
+ and appends `furnace-apply=skipped`. `--furnace-apply reached` appends
267
+ `furnace-skipped=<names>` for components with undeployed edits that no
268
+ requested test reaches. All are absent otherwise.
@@ -37,6 +37,14 @@ directories. The manifest is JSON:
37
37
  The command can be dry-run, refuses ambiguous ownership, and reports each
38
38
  file-to-patch assignment before refreshing the patch.
39
39
 
40
+ `re-export --dry-run` takes no engine-session lock: it writes nothing, so it
41
+ does not queue behind a peer's build or test run, and its git plumbing runs on
42
+ a private index. Its purity guard covers what the dry run could touch (the
43
+ selected patches, their `patches.json` rows, the engine files they own, and
44
+ the engine HEAD and index). A peer writing any of those during the preview is
45
+ named from the history log or the engine-session lock, and the preview is
46
+ simply rerun.
47
+
40
48
  To adopt files into several patches in one call, name each file's owner
41
49
  with `--scan-file <patch>:<path>`. A call with more than one target patch
42
50
  requires the prefix on every `--scan-file`. Selected patches that get no
@@ -68,6 +76,29 @@ module a later patch creates. They also refuse a forward registration
68
76
  module), in the same refusal and with the `patch staged-dependency` command
69
77
  that declares it, so one declaration pass covers both.
70
78
 
79
+ ### Exporting by hunk
80
+
81
+ Re-export captures whole files, so on a shared engine a peer's unexported
82
+ line in a shared manifest or chrome document lands in the exporting patch.
83
+ Two flags export a file by hunk instead. Each takes one explicit patch and
84
+ cannot combine with `--scan`, `--files` or `--all`:
85
+
86
+ - `--hunks-from <diff>` exports each file the diff names as the patch's
87
+ current body for it plus only the diff's hunks. Files the diff does not
88
+ name keep their old section.
89
+ - `--exclude-hunks <diff>` exports each named file as the working tree minus
90
+ the diff's hunks. The other files export as usual.
91
+
92
+ Hunks are placed by their context, so a diff taken before a peer's edit
93
+ above it still applies, and a hunk whose context is gone refuses. The
94
+ section is diffed by git against HEAD on a throwaway index, and the engine
95
+ is not touched. A spine patch can then export one change's lines at a time:
96
+
97
+ ```sh
98
+ git -C engine diff -- browser/base/content/hominis.js > my-change.diff # trim to your hunks
99
+ fireforge re-export 200 --hunks-from my-change.diff
100
+ ```
101
+
71
102
  ### Re-export belts and arguments
72
103
 
73
104
  `--refuse-adjacent-unmanaged` refuses a patch when an unmanaged file is
@@ -82,6 +113,10 @@ session's unexported test in `test/hominis-tiles/` no longer refuses a
82
113
  patch that owns `test/hominis/browser.toml`. The refusal names every file
83
114
  with its anchor (`beside engine/<dir>` or `registered by engine/<carrier>`).
84
115
 
116
+ Beside `--scan` and `--scan-file` the belt judges the adjacent files the
117
+ scan did not adopt, so one call can adopt a new widget and its test and
118
+ still refuse a peer's neighbour. It cannot combine with `--files`.
119
+
85
120
  Three carve-outs keep the belt armed for everything they do not name. Each
86
121
  requires `--refuse-adjacent-unmanaged`, and each admitted file is still
87
122
  listed:
@@ -142,7 +177,10 @@ deleted whose engine copy is back at HEAD:
142
177
  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
143
178
  ```
144
179
 
145
- The removal must have exactly one selected owner. A patch-created file must
180
+ Each removal must have exactly one selected owner, and one call may retire
181
+ paths from several selected patches (`re-export 102 202 --expect-removed a
182
+ --expect-removed b`). A path whose owner was left out of the selection is
183
+ refused with that owner named. A patch-created file must
146
184
  be absent from disk and engine HEAD, and have a `new file mode` section in
147
185
  that owner's patch. An upstream file must be tracked in HEAD, match it on
148
186
  disk, and have a section in that owner's patch. Without the retirement, a
@@ -161,7 +199,18 @@ later patch creates names the earliest legal order for exporting those
161
199
  files as their own patch (free, inside the category range, and not opening
162
200
  a gap under `allowGaps: false`), and prints the exact `patch
163
201
  staged-dependency --add` command for each edge, with a `--reason`
164
- placeholder.
202
+ placeholder. A refused `--scan` adoption, `patch move-files` or `patch split`
203
+ also writes every declaration it proposes into one lint-report-shaped JSON
204
+ (`--declarations-out <path>`, by default under `.fireforge/declarations/`)
205
+ and names the single `patch staged-dependency <holder> --add --from <file>`
206
+ call per holding patch that discharges them all. Moving tests out of a patch
207
+ that owns a shared manifest no longer needs one declaration typed per
208
+ manifest line.
209
+
210
+ A re-export also drops a staged registration whose `creates` path no patch
211
+ in the queue owns any more, for example after a merge deleted the file it
212
+ registered. The dry run names the drop, and the write records it in the
213
+ history log (`re-export-staged-prune`).
165
214
 
166
215
  An `export --order` (or `--before`/`--after`) refused on a new system
167
216
  module that no patch registers in moz.build says "register first". When the
@@ -214,6 +263,40 @@ Under `fireforge patch`:
214
263
  occupied. A patch that later takes the order (`export --order <n>`, a
215
264
  rename or a renumber) fills the slot, which then disappears. `patch
216
265
  compact` renumbers through retired slots like any other gap.
266
+ - `patch delete` and `patch retire` name each file the patch creates that is
267
+ still on disk, in the dry run and at the prompt: once the patch is gone
268
+ nothing owns it, so `status` would report it unmanaged. Delete those files
269
+ first, or pass `--delete-created` to remove them as part of the confirmed
270
+ operation (under the engine lock as well). Files the patch only modifies
271
+ keep their edits and become unmanaged, as before.
272
+ - `patch reserve <order> --category <c> --owner <label> [--reason <text>]`
273
+ holds an order for a patch a session has agreed to export but has not
274
+ exported yet. The reservation lives in `patches.json#reserved`. Like a
275
+ retired slot, the gap check counts it as occupied, so `export --order 571`
276
+ is legal while a peer holds 569 and 570. Unlike a retired slot, the
277
+ earliest-legal-order search and a default `export` skip it, because it is
278
+ held for its owner. The export that lands on the order fills it, with a
279
+ notice naming the owner. `patch reserve <order> --release` drops a
280
+ reservation whose patch will not come. An order a patch, a retired slot or
281
+ another reservation holds, or one outside the category's range, is
282
+ refused.
283
+ - `patch stage <name>...` stages the named patches' `.patch` files and only
284
+ their rows of `patches/patches.json` in the project repository's git index.
285
+ Every other row stays as at HEAD. The staged manifest is built from HEAD's
286
+ rows plus the named working rows (with their retired slots; a reservation
287
+ a staged row fills is dropped), validated, and written with `git
288
+ hash-object` and `git update-index`. A collision (two rows at one order) or
289
+ an invalid result is refused before the index is touched. The working copy
290
+ is never changed and nothing is committed. Name a patch the working copy
291
+ no longer has by its HEAD filename to stage its removal, and name both
292
+ filenames of a renamed patch.
293
+ - `patch staged-dependency <name> --remove` names the entries to remove by
294
+ any of `--file`, `--specifier`/`--line`, `--creates` and `--owner`. It needs
295
+ `--creates`, or `--file` with the matcher. `--creates` alone removes every
296
+ declaration for that created path, which is the cleanup after a merge
297
+ deletes it. Registration lines compare whitespace-insensitively, as the
298
+ lint does. A selection that matches nothing is refused with exit 8 (it used
299
+ to print "no staged registration matched" and exit 0), on a dry run too.
217
300
  - `patch staged-dependency <name> --add --from <json-file>` adds many
218
301
  declarations in one call under one lock hold. The file is a `lint
219
302
  --per-patch --report` document (its findings' `stagedDeclarations` for
@@ -223,6 +306,24 @@ reason?, patch? }` entries; entries naming another patch are skipped, and
223
306
  only its delta (`added` / `removed`) in the history; records written
224
307
  before 0.50.0 carry the whole `before` / `after` lists instead.
225
308
 
309
+ ### Parking a session's edits
310
+
311
+ Several sessions edit the same files, and a peer's `re-export
312
+ --refuse-foreign-drift` refuses while another session's hunks sit in its
313
+ patch's files. `fireforge park <patch>` sets them aside: every drifted file of
314
+ the patch goes back to its queue-applied baseline (or, with `--hunks <diff>`,
315
+ only the named hunks come out of the working tree), and both sides of each
316
+ file are kept under `.fireforge/park/<label>/`. After the owner commits,
317
+ `fireforge park --restore <label>` three-way merges the parked edits back on
318
+ top of the commit. A conflict leaves markers, keeps the park and exits
319
+ non-zero. `park --list` and `park --drop <label>` manage parks, `--dry-run`
320
+ previews, and park takes the engine lock.
321
+
322
+ Before parking it warns when a hunk it takes out removes a Fluent message id,
323
+ or a whole file, that files outside the patch still reference. A parked
324
+ string still used by markup another patch owns makes `document.l10n.ready`
325
+ reject in every new window, so move it somewhere you own first.
326
+
226
327
  Every queue-mutating patch command accepts `--wait-lock [seconds]`. See
227
328
  [`configuration.md`](configuration.md#lock-waits) for how the budget
228
329
  resolves.
@@ -260,7 +361,15 @@ programmatic API, with the file-size helpers `isTestFile`,
260
361
  `file-too-large` measures in a patch: the JS files it creates, and never a
261
362
  modified file or a new file of another kind. The rule selects its files
262
363
  through that same function, so an audit that uses it cannot drift from the
263
- rule.
364
+ rule. Since 0.51.0 the API also exports the measurement the size rules
365
+ themselves take, `measurePatchSize(engineDir, diff, filesAffected, config)`
366
+ (lines, test-file lines, files and registration files under the configured
367
+ metric), `isPatchSizeRegistrationFile(file, globs)`, the per-line
368
+ `codeLineMask(file, content)` behind `countCodeLines`, and
369
+ `evaluatePatchPolicyContentForQueue(config, projectRoot, patchesDir,
370
+ manifest)`, the body-dependent `patchPolicy` rules `verify` runs. A waiver
371
+ audit or a policy probe can then measure a patch exactly as the lint does,
372
+ without importing `dist/` files by path.
264
373
 
265
374
  `lint --per-patch` measures sizes on the working tree, the way export and
266
375
  re-export do: each patch's diff is taken against the engine baseline from
@@ -272,10 +381,13 @@ thresholds.
272
381
 
273
382
  Per-patch lint type-checks plain Firefox JS against a built-in
274
383
  Firefox-globals shim that tracks upstream WebIDL additions per release. A
275
- `resource://` or `chrome://` import the per-patch program cannot resolve (a
276
- module another patch creates, outside the file's program) is typed `any`;
277
- an implicit-`any` finding in a file with such an import names it, since the
278
- whole-tree `fireforge typecheck` sees the real type. See
384
+ `resource://` or `chrome://` import resolves to the module the queue
385
+ creates under that name; a test script's own program sees the modules
386
+ created up to the test's patch. An import the program cannot resolve (a
387
+ module a later patch creates, an upstream module, or a basename two
388
+ patches create) is typed `any`; an implicit-`any` finding in a file with
389
+ such an import names it, since the whole-tree `fireforge typecheck` sees
390
+ the real type. See
279
391
  [`configuration.md`](configuration.md#patchlint) for the project hooks
280
392
  (`checkJsExtraShim`, `checkJsTestFiles`, `checkJsTestShim`).
281
393
 
@@ -328,11 +440,49 @@ longer reaches its own warning band, `lint --per-patch` prints a
328
440
  `lint-ignore-removable` notice naming the `patch lint-ignore --remove`
329
441
  command, and the `--report` JSON carries each ceiling under
330
442
  `lintIgnoreCeilings` with its current `measurement` and a `state` of
331
- `within`, `exceeded` or `removable`. An `--add` for a rule that already has
332
- an entry replaces it, so the same command moves a ceiling or widens it back
333
- to a bare waiver. `export` and `re-export --lint-ignore <id>` only append
334
- bare IDs, and never widen an existing ceiling. The manifest refuses a
335
- ceiling on any other rule, and a second entry for the same rule.
443
+ `within`, `exceeded` or `removable`. A notice never exceeds a ceiling: it
444
+ gates nothing, so a notice-band measurement past `max` surfaces as the plain
445
+ notice it is. The report's `lintIgnoreWaivers` lists every entry, bare IDs
446
+ included (`kind: "bare"`, `max: null`), with the same `measurement` and
447
+ `state`, so an audit does not have to re-measure bare waivers from
448
+ `suppressedIssues`. For a bare waiver the state is informational:
449
+ `removable` also covers a rule this lint does not run. An `--add` for a rule
450
+ that already has an entry replaces it, so the same command moves a ceiling
451
+ or widens it back to a bare waiver. `export` and `re-export --lint-ignore
452
+ <id>` only append bare IDs, and never widen an existing ceiling. The manifest
453
+ refuses a ceiling on any other rule, and a second entry for the same rule.
454
+
455
+ Under `patchSizeMetric: "addedCode"` a general-tier patch's
456
+ `large-patch-lines` findings carry `share: "product"` (non-test files) or
457
+ `share: "test"` (test files, held to the test tier). A ceiling can name the
458
+ share it bounds, and `large-patch-lines` may then carry one ceiling per
459
+ share instead of a single one:
460
+
461
+ ```json
462
+ "lintIgnore": [
463
+ { "rule": "large-patch-lines", "share": "product", "max": 900, "reason": "…" },
464
+ { "rule": "large-patch-lines", "share": "test", "max": 4000, "reason": "…",
465
+ "meta": { "kind": "structural", "owner": "tiles" } }
466
+ ]
467
+ ```
468
+
469
+ A ceiling's optional `meta` is a free-form JSON object FireForge keeps
470
+ verbatim and never reads, for a project's own waiver bookkeeping (kind,
471
+ owner, exit condition). The report echoes it.
472
+
473
+ Several ceilings go in one call, under one lock hold and one manifest write:
474
+ `--add <rule>=<max>` gives each entry its own ceiling (`--reason` applies to
475
+ all of them, `--share` scopes a `large-patch-lines` one), and `--from
476
+ <json-file>` adds every entry of a JSON array (check IDs, or `{ rule, max,
477
+ reason?, share?, meta?, patch? }` objects; entries naming another patch are
478
+ skipped, and `--reason` fills objects without one). Given a `lint --per-patch
479
+ --report` document instead, `--from` rebaselines each of the patch's
480
+ ceilings to its current `measurement`.
481
+
482
+ ```sh
483
+ fireforge patch lint-ignore 310-ui-sidebar --add file-too-large=1200 --add large-patch-files=40 --reason "Rebaseline 2026-10"
484
+ fireforge patch lint-ignore 310-ui-sidebar --from lint-report.json
485
+ ```
336
486
 
337
487
  ## CI enforcement
338
488
 
@@ -369,9 +519,20 @@ them (filename, stem, order number, manifest name or slug). A peer's drifted
369
519
  patch then no longer turns this session's gate red. The limit is the
370
520
  point: an unmanaged file belongs to no patch, so a scoped run never judges
371
521
  unexported files, this session's included. Run the unscoped gate before a
372
- commit that should cover them. `verify --patches` keeps findings about the
522
+ commit that should cover them. `lint --per-patch --patches` keeps every
523
+ queue finding that names a scoped patch, even on a file the patch no longer
524
+ touches (a stale `staged-dependency-unused` declaration, since 0.51.0). `verify --patches` keeps findings about the
373
525
  queue as a whole (a `patches.json` that does not parse) whatever the scope.
374
526
 
527
+ `verify` resolves every `resource:///modules/…` and `resource://gre/modules/…`
528
+ `.sys.mjs` specifier the queue's files import, and fails on a fork module that
529
+ is missing or that no `moz.build` at or above its directory registers. Such
530
+ an import otherwise crashes every browser start of the objdir, or kills
531
+ xpcshell with no output. An unscoped run also scans the engine worktree:
532
+ changed files are read for imports, and untracked files count as fork
533
+ modules. That covers a chrome script's lazy getter for a module that neither
534
+ file has exported yet.
535
+
375
536
  `fireforge check` composes the static gates into one command: `status`
376
537
  (as `--json --summary --check`), `verify`, `lint --per-patch`,
377
538
  `typecheck`, `furnace validate` and `token coverage`, plus the project's
package/docs/testing.md CHANGED
@@ -107,6 +107,21 @@ read from the objdir, not from `engine/`, so a control on it stays green
107
107
  and reports `missed`: that result is the signal to target the file the
108
108
  browser actually loads.
109
109
 
110
+ A control on compiled source (C/C++, Rust, IDL, WebIDL, IPDL or YAML that no
111
+ install manifest ships) can only reach the binary through a build. Without
112
+ `--build` it is refused before the lock. With `--build` it is applied before
113
+ the pre-test build, so the binary under test carries it, stays applied for
114
+ every `--repeat` run, and is restored with a fresh mtime so the next build
115
+ recompiles the original. The verdict adds `control-build=compiled`. A
116
+ build-less control whose packaging is not current is refused before the
117
+ lock with the advice to add `--build`, instead of ending `inconclusive`
118
+ after a full queue.
119
+
120
+ Besides `git diff` output, a control may be a plain unified diff
121
+ (`diff -u`, no `diff --git` lines; the path comes from `---`/`+++`) or a
122
+ `git diff --no-index <engine file> <edited copy>`: when the new side names a
123
+ copy outside `engine/` and `components/`, the control targets the old side.
124
+
110
125
  ### The restore safety model
111
126
 
112
127
  - **Only under the lock.** Apply, run and restore all happen inside the
@@ -194,7 +209,11 @@ earlier path whose own inputs and `engine/mozconfig` are unchanged, whatever
194
209
  moved elsewhere in the engine, including a peer's edits and an engine HEAD
195
210
  move that touches none of them. A path whose inputs changed is dropped and
196
211
  named, with the inputs that changed. Changed inputs this build restages are
197
- allowed.
212
+ allowed, and a scoped build of one file restages that file's test manifest
213
+ too: a manifest that gained an entry for the new file no longer drops the
214
+ claims of its sibling files. A build-less run's packaging gates also run
215
+ before the lock is requested, so a `coverage-replaced` or `stale-build`
216
+ refusal arrives before the run queues.
198
217
 
199
218
  A scoped build after a full build keeps the claim full while nothing outside
200
219
  its scope moved. Otherwise it carries the full build's anchor forward, and a
@@ -221,6 +240,15 @@ packages `engine/` as it stands, for example for a before-capture, names the
221
240
  components with undeployed edits it left out, and stamps
222
241
  `furnace-apply=skipped`.
223
242
 
243
+ On a shared engine a peer's default `test --build` deploys every session's
244
+ undeployed component edits. `--furnace-apply reached` (or
245
+ `test.furnaceApply: "reached"`) deploys only the components a requested test
246
+ reaches: the test path, the test file, a `head*.js` beside it, or any test
247
+ file under a requested directory names the component. The rest are named in
248
+ a notice and stamped `furnace-skipped=<names>`. A path-less build reaches
249
+ every component. `--furnace-apply none` is `--no-furnace-apply`; `all` is the
250
+ default.
251
+
224
252
  When `patchLint.checkJs` and `patchLint.checkJsTestFiles` are enabled, named
225
253
  pre-test builds check the selected test scripts before deployment or mach.
226
254
  This includes new scripts not yet owned by an exported patch. The check runs
@@ -247,7 +275,7 @@ launchd-parented app remains unattributed and is not automatically killed.
247
275
  `test --wait-browser [seconds]` waits for this objdir's browser to exit;
248
276
  `test --wait-port [seconds]` waits for mochitest and Marionette listeners.
249
277
  `build --wait-browser [seconds]` queues a full or UI build behind the browser.
250
- Bare flags wait 60 seconds; explicit values accept 1–3600 seconds. They are
278
+ Bare flags wait 60 seconds; explicit values accept 1–86400 seconds. They are
251
279
  independent of the FireForge lock budget, and they are spent BEFORE the
252
280
  engine session lock is requested, so a run waiting out a browser or a port
253
281
  does not hold every peer's build, test and re-export behind it. The run then
@@ -433,9 +461,16 @@ group sweep.
433
461
  Every test run ends with one machine-readable line:
434
462
 
435
463
  ```
436
- FIREFORGE-VERDICT: PASS|FAIL reason=… [note=<class>] [started=<n>/<m> first-unstarted=<path>] [runs=<p>/<n>] [control=<outcome>] [shuffle=<seed>] [orphans-reaped=<n>] [furnace-applied=<names>|furnace-apply=skipped] [log=<path>]
464
+ FIREFORGE-VERDICT: PASS|FAIL reason=… [note=<class>] [started=<n>/<m> first-unstarted=<path> started-source=<src>] [shards=<p>/<t> [failed-shards=<labels>]] [runs=<p>/<n>] [control=<outcome>] [shuffle=<seed>] [orphans-reaped=<n>] [furnace-applied=<names>|furnace-apply=skipped] [furnace-skipped=<names>] [jobs=<n>] [lock=shared] [results=<path>] [log=<path>]
437
465
  ```
438
466
 
467
+ The start count reads every line the harness printed, not the last 2 MB of
468
+ captured output: a suite printing more than that (a large browser directory,
469
+ or any run under `--full-output`) used to report files that ran and passed
470
+ as never started. `started-source=` names the evidence. A sharded run also
471
+ prints a `FIREFORGE-SHARD:` line per shard and names the shards that did not
472
+ pass as `failed-shards=`; see [`machine-output.md`](machine-output.md).
473
+
439
474
  A run is never `PASS` when its summary reports unexpected results, when the
440
475
  harness printed "Application shut down (without crashing) in the middle of a
441
476
  test", or when a requested test file never produced a start (or skip)
@@ -528,7 +563,41 @@ The two phases handle it differently, on purpose:
528
563
 
529
564
  A test run holds the engine-session lock and snapshots the engine git
530
565
  generation before and after the harness run, failing the verdict as invalid
531
- if the tree changed mid-run. The checks that need no lock run before it is
566
+ if the tree changed mid-run in a way the running browser can see: a file the
567
+ objdir links to its source, a file staged at dispatch, or a test file or
568
+ staging input of the run. A peer's edit to anything else (a compiled source,
569
+ a copied file, an unrelated test) leaves the verdict standing with a warning
570
+ and `drift-tolerated=<n>`; the next run's stale-build gate judges whether it
571
+ needs a rebuild.
572
+
573
+ `--shared-lock` (or `test.lockMode: "shared"`) holds the lock shared for a
574
+ build-less run that applies no negative control and uses auto ports, so
575
+ several such runs on one objdir overlap. Anything that writes (a build, a
576
+ re-export, a `test --build`) still holds the lock alone: it waits for shared
577
+ holders to finish, and new shared holders queue behind it.
578
+ `status --lock` counts shared holders. Orphan reaping only touches helpers
579
+ whose parent died, so overlapping runs do not reap each other's live
580
+ helpers.
581
+
582
+ `--jobs <n>` runs a request's shards on n harness instances inside one lock
583
+ hold. Each instance has its own profile and its own Marionette port, and
584
+ auto ports (required) move each instance's mochitest server ports; output
585
+ lines carry a `[job <i>]` prefix in the terminal and the run log. With
586
+ `--no-shard`, or a single directory argument, the files are split in order
587
+ into n contiguous chunks. Parallel browsers raise host load, so measure
588
+ before relying on it.
589
+
590
+ `--shard-size <n>` dispatches n files per mach invocation (mochitest restarts
591
+ the browser between files), paying the harness start once per chunk instead
592
+ of once per file, and judges each file from mozlog's structured log:
593
+ `shards=` then counts files and `failed-shards=` names failed files.
594
+
595
+ `--results <path>` writes one JSON record per test file and `--repeat` run
596
+ (status, expected status, duration, unexpected subtests, the shard, and the
597
+ run-log line where the dispatch began) from mozlog's structured log, which
598
+ each dispatch then writes with `--log-raw`, and names it on the verdict as
599
+ `results=<path>`. A `--log-raw <file>` already forwarded with `--mach-arg` is
600
+ read instead. The checks that need no lock run before it is
532
601
  requested: the launchable binary, the named-file checkJs pass, and the
533
602
  `--wait-browser`/`--wait-port` budgets. The run log is open from the start,
534
603
  so a `reason=lock-timeout` line carries `log=` too.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -76,9 +76,9 @@
76
76
  "eslint-plugin-simple-import-sort": "^14.0.0",
77
77
  "fast-check": "^4.6.0",
78
78
  "husky": "^9.1.7",
79
- "knip": "6.39.0",
79
+ "knip": "6.41.0",
80
80
  "lint-staged": "^17.0.4",
81
- "prettier": "3.9.9",
81
+ "prettier": "3.9.10",
82
82
  "tsx": "^4.7.0",
83
83
  "typescript": "~6.0.0",
84
84
  "typescript-eslint": "^8.0.0",