@hominis/fireforge 0.51.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/build-info.json +4 -4
  3. package/dist/src/commands/bootstrap.js +2 -3
  4. package/dist/src/commands/build-prelock.d.ts +3 -3
  5. package/dist/src/commands/build-prelock.js +3 -3
  6. package/dist/src/commands/build.js +4 -0
  7. package/dist/src/commands/declarations-report.d.ts +9 -3
  8. package/dist/src/commands/declarations-report.js +18 -11
  9. package/dist/src/commands/discard.js +3 -3
  10. package/dist/src/commands/doctor-furnace-config-sync.js +4 -4
  11. package/dist/src/commands/doctor-furnace.js +5 -5
  12. package/dist/src/commands/export-adopt-check.d.ts +5 -6
  13. package/dist/src/commands/export-adopt-check.js +5 -6
  14. package/dist/src/commands/export-flow.js +6 -9
  15. package/dist/src/commands/export-staged-registration.d.ts +4 -4
  16. package/dist/src/commands/export-staged-registration.js +4 -4
  17. package/dist/src/commands/forward-import-remedy.d.ts +2 -3
  18. package/dist/src/commands/forward-import-remedy.js +2 -3
  19. package/dist/src/commands/furnace/create-dry-run.d.ts +2 -2
  20. package/dist/src/commands/furnace/deploy-fragment-includers.d.ts +2 -2
  21. package/dist/src/commands/furnace/deploy-fragment-includers.js +2 -2
  22. package/dist/src/commands/furnace/index.js +2 -2
  23. package/dist/src/commands/lint.js +4 -4
  24. package/dist/src/commands/patch/compact.js +1 -1
  25. package/dist/src/commands/patch/lint-ignore-from.d.ts +3 -4
  26. package/dist/src/commands/patch/lint-ignore-from.js +3 -4
  27. package/dist/src/commands/patch/move-files-adopt.d.ts +33 -0
  28. package/dist/src/commands/patch/move-files-adopt.js +65 -0
  29. package/dist/src/commands/patch/move-files-create.js +41 -20
  30. package/dist/src/commands/patch/move-files-into.d.ts +18 -0
  31. package/dist/src/commands/patch/move-files-into.js +125 -60
  32. package/dist/src/commands/patch/move-files-retire.d.ts +39 -0
  33. package/dist/src/commands/patch/move-files-retire.js +91 -0
  34. package/dist/src/commands/patch/move-files.d.ts +3 -0
  35. package/dist/src/commands/patch/move-files.js +42 -12
  36. package/dist/src/commands/patch/move-size-tier.d.ts +27 -0
  37. package/dist/src/commands/patch/move-size-tier.js +66 -0
  38. package/dist/src/commands/patch/rename.js +1 -1
  39. package/dist/src/commands/patch/reorder-projection.d.ts +17 -0
  40. package/dist/src/commands/patch/reorder-projection.js +49 -0
  41. package/dist/src/commands/patch/reorder-shift.d.ts +30 -0
  42. package/dist/src/commands/patch/reorder-shift.js +307 -0
  43. package/dist/src/commands/patch/reorder.d.ts +9 -15
  44. package/dist/src/commands/patch/reorder.js +52 -50
  45. package/dist/src/commands/patch/split-plan.d.ts +31 -3
  46. package/dist/src/commands/patch/split-plan.js +36 -12
  47. package/dist/src/commands/patch/split-staged.d.ts +2 -2
  48. package/dist/src/commands/patch/split-staged.js +2 -2
  49. package/dist/src/commands/patch/split.js +18 -5
  50. package/dist/src/commands/patch/staged-dependency-entries.d.ts +9 -1
  51. package/dist/src/commands/patch/staged-dependency-entries.js +32 -4
  52. package/dist/src/commands/patch/staged-dependency-from.js +15 -9
  53. package/dist/src/commands/patch/staged-dependency.js +13 -9
  54. package/dist/src/commands/patch/tier.js +2 -2
  55. package/dist/src/commands/patch-rename-map.d.ts +14 -3
  56. package/dist/src/commands/patch-rename-map.js +21 -3
  57. package/dist/src/commands/re-export-adjacent.d.ts +4 -5
  58. package/dist/src/commands/re-export-lint.d.ts +4 -3
  59. package/dist/src/commands/re-export-lint.js +4 -3
  60. package/dist/src/commands/re-export-preflight.js +1 -2
  61. package/dist/src/commands/re-export-purity-scope.d.ts +1 -1
  62. package/dist/src/commands/re-export-purity-scope.js +13 -7
  63. package/dist/src/commands/re-export-retired-declarations.d.ts +22 -0
  64. package/dist/src/commands/re-export-retired-declarations.js +113 -0
  65. package/dist/src/commands/re-export-single.js +2 -2
  66. package/dist/src/commands/re-export-stale-registrations.d.ts +27 -6
  67. package/dist/src/commands/re-export-stale-registrations.js +65 -21
  68. package/dist/src/commands/re-export.js +4 -0
  69. package/dist/src/commands/rebase/continue.js +3 -3
  70. package/dist/src/commands/register.js +5 -4
  71. package/dist/src/commands/status-output.js +5 -1
  72. package/dist/src/commands/status.js +1 -1
  73. package/dist/src/commands/test-build-furnace.d.ts +1 -2
  74. package/dist/src/commands/test-build-furnace.js +1 -2
  75. package/dist/src/commands/test-build-phase.d.ts +10 -0
  76. package/dist/src/commands/test-build-phase.js +51 -16
  77. package/dist/src/commands/test-control-restore.d.ts +40 -0
  78. package/dist/src/commands/test-control-restore.js +86 -0
  79. package/dist/src/commands/test-dispatch.js +83 -13
  80. package/dist/src/commands/test-multi-verdict.js +1 -0
  81. package/dist/src/commands/test-orchestrate.d.ts +11 -0
  82. package/dist/src/commands/test-orchestrate.js +33 -2
  83. package/dist/src/commands/test-perf-scope.d.ts +1 -1
  84. package/dist/src/commands/test-perf-scope.js +1 -1
  85. package/dist/src/commands/test-prelock.d.ts +7 -8
  86. package/dist/src/commands/test-prelock.js +15 -12
  87. package/dist/src/commands/test-register.js +2 -2
  88. package/dist/src/commands/test-results.d.ts +10 -1
  89. package/dist/src/commands/test-results.js +20 -3
  90. package/dist/src/commands/test-run.d.ts +6 -0
  91. package/dist/src/commands/test-run.js +23 -4
  92. package/dist/src/commands/test-shards.d.ts +7 -3
  93. package/dist/src/commands/test-shards.js +24 -7
  94. package/dist/src/commands/test-stale-gate.d.ts +6 -1
  95. package/dist/src/commands/test-stale-gate.js +47 -2
  96. package/dist/src/commands/test-verdict.d.ts +5 -3
  97. package/dist/src/commands/test.js +14 -1
  98. package/dist/src/commands/token.js +2 -2
  99. package/dist/src/commands/verify.js +1 -1
  100. package/dist/src/commands/watch.js +1 -1
  101. package/dist/src/commands/wire.js +1 -1
  102. package/dist/src/core/build-audit-resolve.d.ts +4 -5
  103. package/dist/src/core/build-audit-resolve.js +4 -5
  104. package/dist/src/core/build-audit-skip.d.ts +2 -2
  105. package/dist/src/core/build-audit.d.ts +3 -4
  106. package/dist/src/core/build-baseline-types.d.ts +37 -3
  107. package/dist/src/core/build-baseline-validate.d.ts +30 -1
  108. package/dist/src/core/build-baseline-validate.js +134 -33
  109. package/dist/src/core/build-baseline.d.ts +8 -2
  110. package/dist/src/core/build-baseline.js +32 -16
  111. package/dist/src/core/build-jar-escalation.d.ts +4 -4
  112. package/dist/src/core/build-jar-escalation.js +4 -4
  113. package/dist/src/core/build-prepare.js +4 -4
  114. package/dist/src/core/config-validate.js +4 -4
  115. package/dist/src/core/coverage-extend.d.ts +5 -3
  116. package/dist/src/core/coverage-extend.js +8 -3
  117. package/dist/src/core/coverage-records.d.ts +4 -5
  118. package/dist/src/core/coverage-records.js +4 -5
  119. package/dist/src/core/destructive.d.ts +2 -2
  120. package/dist/src/core/destructive.js +2 -2
  121. package/dist/src/core/discard-baseline.d.ts +2 -2
  122. package/dist/src/core/discard-baseline.js +3 -3
  123. package/dist/src/core/engine-session-lock.d.ts +1 -1
  124. package/dist/src/core/engine-session-lock.js +1 -1
  125. package/dist/src/core/file-lock-owner.d.ts +5 -5
  126. package/dist/src/core/file-lock-owner.js +5 -5
  127. package/dist/src/core/file-lock-queue.js +6 -8
  128. package/dist/src/core/file-lock-shared.d.ts +4 -4
  129. package/dist/src/core/file-lock-shared.js +4 -4
  130. package/dist/src/core/file-lock.js +7 -10
  131. package/dist/src/core/firefox.js +2 -2
  132. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +1 -1
  133. package/dist/src/core/furnace-apply-overwrite-warn.js +1 -1
  134. package/dist/src/core/furnace-apply.js +3 -3
  135. package/dist/src/core/furnace-config-validate-exceptions.d.ts +4 -3
  136. package/dist/src/core/furnace-config-validate-exceptions.js +26 -6
  137. package/dist/src/core/furnace-operation.js +5 -5
  138. package/dist/src/core/furnace-registration-jar-inplace.js +5 -6
  139. package/dist/src/core/furnace-registration-rename.d.ts +5 -5
  140. package/dist/src/core/furnace-registration-rename.js +5 -5
  141. package/dist/src/core/furnace-registration-validate.d.ts +2 -1
  142. package/dist/src/core/furnace-registration-validate.js +2 -1
  143. package/dist/src/core/furnace-registration.js +2 -2
  144. package/dist/src/core/furnace-validate-accessibility.d.ts +1 -1
  145. package/dist/src/core/furnace-validate-accessibility.js +22 -11
  146. package/dist/src/core/furnace-validate-exceptions.d.ts +2 -1
  147. package/dist/src/core/furnace-validate-exceptions.js +31 -13
  148. package/dist/src/core/git-base.js +4 -3
  149. package/dist/src/core/git-diff.js +1 -1
  150. package/dist/src/core/git.js +1 -1
  151. package/dist/src/core/harness-orphans.d.ts +6 -6
  152. package/dist/src/core/harness-orphans.js +6 -6
  153. package/dist/src/core/harness-port-reservation.d.ts +60 -0
  154. package/dist/src/core/harness-port-reservation.js +122 -0
  155. package/dist/src/core/history-log-file.d.ts +9 -9
  156. package/dist/src/core/history-log-file.js +15 -15
  157. package/dist/src/core/history-log.d.ts +3 -3
  158. package/dist/src/core/history-log.js +3 -3
  159. package/dist/src/core/launchable-build.d.ts +39 -0
  160. package/dist/src/core/launchable-build.js +85 -0
  161. package/dist/src/core/marionette-port.js +1 -3
  162. package/dist/src/core/moz-manifest-rules.js +3 -2
  163. package/dist/src/core/negative-control-compiled.d.ts +22 -3
  164. package/dist/src/core/negative-control-compiled.js +41 -5
  165. package/dist/src/core/parser-fallback.d.ts +5 -5
  166. package/dist/src/core/parser-fallback.js +5 -5
  167. package/dist/src/core/patch-identifier-suggest.d.ts +6 -0
  168. package/dist/src/core/patch-identifier-suggest.js +11 -4
  169. package/dist/src/core/patch-lint-checkjs.d.ts +1 -1
  170. package/dist/src/core/patch-lint-checkjs.js +1 -1
  171. package/dist/src/core/patch-lint-css.js +2 -2
  172. package/dist/src/core/patch-lint-file-size.d.ts +5 -7
  173. package/dist/src/core/patch-lint-file-size.js +5 -7
  174. package/dist/src/core/patch-lint-ignore.d.ts +3 -3
  175. package/dist/src/core/patch-lint-ignore.js +3 -3
  176. package/dist/src/core/patch-lint-prettier.d.ts +4 -6
  177. package/dist/src/core/patch-lint-prettier.js +4 -6
  178. package/dist/src/core/patch-lint-projection.d.ts +1 -1
  179. package/dist/src/core/patch-lint-projection.js +1 -1
  180. package/dist/src/core/patch-lint-queue-types.d.ts +2 -2
  181. package/dist/src/core/patch-lint-queue-types.js +2 -2
  182. package/dist/src/core/patch-manifest-consistency.d.ts +2 -2
  183. package/dist/src/core/patch-manifest-consistency.js +2 -2
  184. package/dist/src/core/patch-manifest-io.d.ts +5 -1
  185. package/dist/src/core/patch-manifest-io.js +12 -10
  186. package/dist/src/core/patch-manifest-key-order.d.ts +3 -4
  187. package/dist/src/core/patch-manifest-key-order.js +3 -4
  188. package/dist/src/core/patch-parse.d.ts +3 -3
  189. package/dist/src/core/patch-parse.js +3 -3
  190. package/dist/src/core/patch-policy.d.ts +1 -1
  191. package/dist/src/core/patch-policy.js +1 -1
  192. package/dist/src/core/patch-source-metadata.d.ts +1 -1
  193. package/dist/src/core/patch-source-metadata.js +1 -1
  194. package/dist/src/core/patch-subset.d.ts +5 -3
  195. package/dist/src/core/patch-subset.js +8 -7
  196. package/dist/src/core/patch-transform.d.ts +2 -2
  197. package/dist/src/core/patch-transform.js +2 -2
  198. package/dist/src/core/rebase-session.js +4 -6
  199. package/dist/src/core/register-xpcshell-test.d.ts +28 -7
  200. package/dist/src/core/register-xpcshell-test.js +76 -27
  201. package/dist/src/core/run-log.d.ts +8 -8
  202. package/dist/src/core/run-log.js +8 -8
  203. package/dist/src/core/status-classify.d.ts +1 -2
  204. package/dist/src/core/status-classify.js +1 -2
  205. package/dist/src/core/test-harness-crash.d.ts +5 -3
  206. package/dist/src/core/test-harness-crash.js +13 -14
  207. package/dist/src/core/test-harness-infra.d.ts +32 -0
  208. package/dist/src/core/test-harness-infra.js +69 -0
  209. package/dist/src/core/test-harness-signature.d.ts +6 -0
  210. package/dist/src/core/test-run-completeness.d.ts +12 -5
  211. package/dist/src/core/test-run-completeness.js +15 -8
  212. package/dist/src/core/test-stale-check.d.ts +48 -1
  213. package/dist/src/core/test-stale-check.js +97 -2
  214. package/dist/src/core/test-stall-triage.js +5 -7
  215. package/dist/src/core/token-category.js +2 -2
  216. package/dist/src/core/token-inventory.js +3 -3
  217. package/dist/src/core/token-manager.d.ts +2 -2
  218. package/dist/src/core/token-manager.js +5 -7
  219. package/dist/src/core/token-variant.d.ts +6 -6
  220. package/dist/src/core/token-variant.js +7 -7
  221. package/dist/src/core/typecheck-shim.d.ts +2 -1
  222. package/dist/src/core/typecheck-shim.js +29 -5
  223. package/dist/src/core/wire-destroy.d.ts +1 -1
  224. package/dist/src/core/wire-destroy.js +1 -1
  225. package/dist/src/core/wire-dom-fragment.js +1 -1
  226. package/dist/src/core/wire-init.d.ts +1 -1
  227. package/dist/src/core/wire-init.js +1 -1
  228. package/dist/src/core/wire-subscript.d.ts +1 -1
  229. package/dist/src/core/wire-subscript.js +1 -1
  230. package/dist/src/core/wire-utils.d.ts +2 -2
  231. package/dist/src/core/wire-utils.js +3 -3
  232. package/dist/src/errors/base.d.ts +1 -1
  233. package/dist/src/errors/base.js +1 -1
  234. package/dist/src/errors/rebase.d.ts +1 -1
  235. package/dist/src/errors/rebase.js +1 -1
  236. package/dist/src/types/commands/options.d.ts +26 -3
  237. package/dist/src/types/config.d.ts +5 -7
  238. package/dist/src/types/furnace.d.ts +9 -0
  239. package/dist/src/utils/options.js +2 -2
  240. package/dist/src/utils/process.js +2 -2
  241. package/dist/src/utils/regex.d.ts +2 -2
  242. package/dist/src/utils/regex.js +3 -3
  243. package/dist/src/utils/sleep.d.ts +5 -6
  244. package/dist/src/utils/sleep.js +5 -6
  245. package/dist/src/utils/validation.js +2 -2
  246. package/docs/configuration.md +40 -26
  247. package/docs/exit-codes.md +8 -11
  248. package/docs/furnace.md +21 -3
  249. package/docs/lifecycle-invariants.md +36 -45
  250. package/docs/machine-output.md +43 -22
  251. package/docs/patch-workflow.md +129 -24
  252. package/docs/run-logs.md +8 -9
  253. package/docs/testing.md +118 -44
  254. package/package.json +1 -1
@@ -2,14 +2,12 @@
2
2
  /**
3
3
  * Optional Prettier pass over patch-owned `.sys.mjs` modules.
4
4
  *
5
- * Until 0.45.0 no tier of the chain ran Prettier on `.sys.mjs` at all, so
6
- * module formatting drift was caught by nothing and survived to a human
7
- * reviewer. Worse, the obvious manual check was misleading in the same
8
- * direction: a fork's root `.prettierignore` typically excludes `engine/`,
5
+ * Without this pass no tier of the chain runs Prettier on `.sys.mjs`, so
6
+ * module formatting drift survives to a human reviewer. The obvious manual
7
+ * check is misleading in the same direction: a fork's root `.prettierignore` typically excludes `engine/`,
9
8
  * so `prettier --check --config engine/.prettierrc.js <engine files>` run
10
9
  * from the repo root reports "all files use Prettier code style" for files
11
- * that fail the identical check run from inside `engine/`. A root-level
12
- * spot-check falsely refuted a correct reviewer finding on exactly this.
10
+ * that fail the identical check run from inside `engine/`.
13
11
  *
14
12
  * This pass therefore runs prettier with `cwd` set to the engine directory,
15
13
  * so the engine's own `.prettierrc*` and `.prettierignore` resolve the way
@@ -8,7 +8,7 @@ import type { PatchLintIssue } from '../types/commands/index.js';
8
8
  * whose message text can drift between otherwise-equivalent runs (for
9
9
  * example because the message embeds later-owner filenames or positional
10
10
  * detail). Falling back to the full tuple keeps the helper conservative
11
- * for older/non-fingerprinted rules: if their message changes, we would
11
+ * for non-fingerprinted rules: if their message changes, we would
12
12
  * rather surface a potential regression than silently swallow it.
13
13
  *
14
14
  * Consumption order within the projected list is stable (the input
@@ -7,7 +7,7 @@
7
7
  * whose message text can drift between otherwise-equivalent runs (for
8
8
  * example because the message embeds later-owner filenames or positional
9
9
  * detail). Falling back to the full tuple keeps the helper conservative
10
- * for older/non-fingerprinted rules: if their message changes, we would
10
+ * for non-fingerprinted rules: if their message changes, we would
11
11
  * rather surface a potential regression than silently swallow it.
12
12
  *
13
13
  * Consumption order within the projected list is stable (the input
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * A leaf module by design: it imports nothing, so the individual rule
5
5
  * modules can describe the slice of `PatchQueueContext` they read without
6
- * importing `patch-lint-cross.ts` (which imports them back). Previously each
7
- * rule re-declared its own private `{ entries }` interface. Sharing them here
6
+ * importing `patch-lint-cross.ts` (which imports them back). Sharing them
7
+ * here instead of re-declaring a private `{ entries }` interface per rule
8
8
  * keeps the dependency edge one-way and `dpdm` clean while stating the shape
9
9
  * once.
10
10
  */
@@ -4,8 +4,8 @@
4
4
  *
5
5
  * A leaf module by design: it imports nothing, so the individual rule
6
6
  * modules can describe the slice of `PatchQueueContext` they read without
7
- * importing `patch-lint-cross.ts` (which imports them back). Previously each
8
- * rule re-declared its own private `{ entries }` interface. Sharing them here
7
+ * importing `patch-lint-cross.ts` (which imports them back). Sharing them
8
+ * here instead of re-declaring a private `{ entries }` interface per rule
9
9
  * keeps the dependency edge one-way and `dpdm` clean while stating the shape
10
10
  * once.
11
11
  */
@@ -20,7 +20,7 @@ export declare function validatePatchesManifestConsistency(patchesDir: string):
20
20
  *
21
21
  * Drift that is only in `filesAffected` is a derived value disagreeing with
22
22
  * the diff it describes, and the narrow repair fixes exactly that. Naming the
23
- * whole-manifest rebuild for it (as this hint used to, unconditionally) puts
23
+ * whole-manifest rebuild for it puts
24
24
  * an operator one keystroke from rewriting every row in the manifest to
25
25
  * correct one list.
26
26
  */
@@ -80,7 +80,7 @@ export interface RebuildPatchesManifestOptions extends PatchDirectoryLockOptions
80
80
  * becomes a whole-manifest reinvention. Pass `allowMetadataLoss` to accept it.
81
81
  *
82
82
  * @param patchesDir - Path to the patches directory
83
- * @param fallbackSourceEsrVersion - source version to use for recovered legacy entries
83
+ * @param fallbackSourceEsrVersion - source version to use for recovered entries that carry none
84
84
  * @param options - Lock options plus `allowMetadataLoss` / `dryRun`
85
85
  * @returns {@link RebuildPatchesManifestResult}: the rebuilt manifest, the
86
86
  * filenames that were reconstructed from generic defaults, and the rows
@@ -87,7 +87,7 @@ export async function validatePatchesManifestConsistency(patchesDir) {
87
87
  *
88
88
  * Drift that is only in `filesAffected` is a derived value disagreeing with
89
89
  * the diff it describes, and the narrow repair fixes exactly that. Naming the
90
- * whole-manifest rebuild for it (as this hint used to, unconditionally) puts
90
+ * whole-manifest rebuild for it puts
91
91
  * an operator one keystroke from rewriting every row in the manifest to
92
92
  * correct one list.
93
93
  */
@@ -115,7 +115,7 @@ export function recommendManifestRepair(issues) {
115
115
  * becomes a whole-manifest reinvention. Pass `allowMetadataLoss` to accept it.
116
116
  *
117
117
  * @param patchesDir - Path to the patches directory
118
- * @param fallbackSourceEsrVersion - source version to use for recovered legacy entries
118
+ * @param fallbackSourceEsrVersion - source version to use for recovered entries that carry none
119
119
  * @param options - Lock options plus `allowMetadataLoss` / `dryRun`
120
120
  * @returns {@link RebuildPatchesManifestResult}: the rebuilt manifest, the
121
121
  * filenames that were reconstructed from generic defaults, and the rows
@@ -65,10 +65,14 @@ export declare function loadPatchesManifestForWrite(patchesDir: string): Promise
65
65
  */
66
66
  export declare function savePatchesManifest(patchesDir: string, manifest: PatchesManifest): Promise<void>;
67
67
  /**
68
- * Mutates selected manifest rows while preserving the raw JSON shape of every
68
+ * Mutates selected manifest rows while preserving the raw JSON fields of every
69
69
  * untouched row. This avoids serializing reader-only fallback fields, such as
70
70
  * `sourceVersion` derived from legacy `sourceEsrVersion`, into unrelated patch
71
71
  * entries during partial metadata updates.
72
+ *
73
+ * Every row's keys (and the manifest's own) are written in canonical order,
74
+ * not only the changed rows', so the working copy matches what `patch stage`
75
+ * and full saves write and no commit flips untouched rows' key order.
72
76
  */
73
77
  export declare function mutatePatchRowsInManifest(patchesDir: string, filenames: readonly string[], mutator: (existing: PatchMetadata, rawExisting: Readonly<JsonObject>) => PatchManifestRowMutation | null): Promise<PatchManifestRowMutationResult[] | null>;
74
78
  /**
@@ -19,7 +19,7 @@ import { pathExistsStrict, readJson, removeFile, writeJson } from '../utils/fs.j
19
19
  import { warn } from '../utils/logger.js';
20
20
  import { isArray, isObject } from '../utils/validation.js';
21
21
  import { assertPatchDirectoryLockHeld } from './patch-lock.js';
22
- import { canonicalizeManifest, canonicalizePatchRow } from './patch-manifest-key-order.js';
22
+ import { canonicalizeManifest } from './patch-manifest-key-order.js';
23
23
  import { validatePatchesManifest } from './patch-manifest-validate.js';
24
24
  import { manifestWithRetiredSlots } from './patch-retired.js';
25
25
  /** Filename for the patches manifest */
@@ -90,10 +90,14 @@ export async function savePatchesManifest(patchesDir, manifest) {
90
90
  await writeJson(manifestPath, canonicalizeManifest(manifest));
91
91
  }
92
92
  /**
93
- * Mutates selected manifest rows while preserving the raw JSON shape of every
93
+ * Mutates selected manifest rows while preserving the raw JSON fields of every
94
94
  * untouched row. This avoids serializing reader-only fallback fields, such as
95
95
  * `sourceVersion` derived from legacy `sourceEsrVersion`, into unrelated patch
96
96
  * entries during partial metadata updates.
97
+ *
98
+ * Every row's keys (and the manifest's own) are written in canonical order,
99
+ * not only the changed rows', so the working copy matches what `patch stage`
100
+ * and full saves write and no commit flips untouched rows' key order.
97
101
  */
98
102
  export async function mutatePatchRowsInManifest(patchesDir, filenames, mutator) {
99
103
  const manifestPath = join(patchesDir, PATCHES_MANIFEST);
@@ -140,16 +144,14 @@ export async function mutatePatchRowsInManifest(patchesDir, filenames, mutator)
140
144
  }
141
145
  if (changedIndexes.length === 0)
142
146
  return [];
143
- // Only the rows this call changed are put in canonical order, so a key
144
- // it sets lands where every other writer puts it; untouched rows keep
145
- // their raw shape.
146
- for (const index of changedIndexes) {
147
- rawPatches[index] = canonicalizePatchRow(rawPatches[index]);
148
- }
149
- const nextRawManifest = {
147
+ // Every row is put in canonical key order, not only the changed ones:
148
+ // `patch stage` and full saves canonicalize the whole manifest, so a
149
+ // row left in any other order flips back and forth between the
150
+ // working copy and the index. Reordering keys adds no field.
151
+ const nextRawManifest = canonicalizeManifest({
150
152
  ...rawManifest,
151
153
  patches: rawPatches,
152
- };
154
+ });
153
155
  const afterManifest = validatePatchesManifest(nextRawManifest);
154
156
  await writeJson(manifestPath, nextRawManifest);
155
157
  return changedIndexes.map((index) => ({
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * The one key order every `patches.json` writer uses for a patch row and
3
- * for the manifest itself. Writers used to disagree: `export` spread
4
- * `sourceProduct` before `sourceVersion` while a full-manifest save put it
5
- * after `filesAffected`, so a no-op rewrite left a key-order-only hunk in
6
- * the manifest.
3
+ * for the manifest itself, so that every writer (`export`, full-manifest
4
+ * saves, `patch stage`) agrees and a no-op rewrite leaves no key-order-only
5
+ * hunk in the manifest.
7
6
  */
8
7
  /**
9
8
  * Returns a copy of a patch row with its keys in canonical order.
@@ -1,10 +1,9 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
3
  * The one key order every `patches.json` writer uses for a patch row and
4
- * for the manifest itself. Writers used to disagree: `export` spread
5
- * `sourceProduct` before `sourceVersion` while a full-manifest save put it
6
- * after `filesAffected`, so a no-op rewrite left a key-order-only hunk in
7
- * the manifest.
4
+ * for the manifest itself, so that every writer (`export`, full-manifest
5
+ * saves, `patch stage`) agrees and a no-op rewrite leaves no key-order-only
6
+ * hunk in the manifest.
8
7
  */
9
8
  /** Patch-row keys in the order of the `PatchMetadata` interface. */
10
9
  const PATCH_ROW_KEY_ORDER = [
@@ -34,10 +34,10 @@ export declare function extractOrder(filename: string): number;
34
34
  * The write-side counterpart to {@link extractOrder}. That one answers
35
35
  * `Infinity` for a prefix-less filename, which sorts such a patch last and
36
36
  * is the right answer for a read: an unparseable name should not crash a
37
- * status listing. Persisting `Infinity` (or the `0` some call sites used to
38
- * fall back to) is a different matter: it puts an order in the manifest
37
+ * status listing. Persisting `Infinity` (or a fallback `0`) is a different
38
+ * matter: it puts an order in the manifest
39
39
  * that no filename on disk agrees with, and the queue then applies in an
40
- * order the operator cannot see. Callers here have always just built or
40
+ * order the operator cannot see. Callers here have just built or
41
41
  * looked up a real prefixed filename, so absence is a bug, not input.
42
42
  *
43
43
  * @param filename - Patch filename carrying a numeric order prefix
@@ -56,10 +56,10 @@ export function extractOrder(filename) {
56
56
  * The write-side counterpart to {@link extractOrder}. That one answers
57
57
  * `Infinity` for a prefix-less filename, which sorts such a patch last and
58
58
  * is the right answer for a read: an unparseable name should not crash a
59
- * status listing. Persisting `Infinity` (or the `0` some call sites used to
60
- * fall back to) is a different matter: it puts an order in the manifest
59
+ * status listing. Persisting `Infinity` (or a fallback `0`) is a different
60
+ * matter: it puts an order in the manifest
61
61
  * that no filename on disk agrees with, and the queue then applies in an
62
- * order the operator cannot see. Callers here have always just built or
62
+ * order the operator cannot see. Callers here have just built or
63
63
  * looked up a real prefixed filename, so absence is a bug, not input.
64
64
  *
65
65
  * @param filename - Patch filename carrying a numeric order prefix
@@ -42,7 +42,7 @@ export interface PatchPolicyEnforcementInput {
42
42
  }
43
43
  /** Returns valid categories for prompts and CLI validation under the config. */
44
44
  export declare function getPatchPolicyCategories(config: FireForgeConfig): string[];
45
- /** Checks whether a category is accepted by legacy defaults or the policy ranges. */
45
+ /** Checks whether a category is accepted by the built-in defaults or the policy ranges. */
46
46
  export declare function isCategoryAllowedByConfig(config: FireForgeConfig, category: string): boolean;
47
47
  /**
48
48
  * Human-readable label for a category's configured ranges, e.g. `300-399`.
@@ -28,7 +28,7 @@ export function getPatchPolicyCategories(config) {
28
28
  return [...PATCH_CATEGORIES];
29
29
  return Array.from(new Set(cfg.ranges.map((range) => range.category))).sort((a, b) => a.localeCompare(b));
30
30
  }
31
- /** Checks whether a category is accepted by legacy defaults or the policy ranges. */
31
+ /** Checks whether a category is accepted by the built-in defaults or the policy ranges. */
32
32
  export function isCategoryAllowedByConfig(config, category) {
33
33
  if (!/^[a-z][a-z0-9-]*$/.test(category))
34
34
  return false;
@@ -2,5 +2,5 @@ import type { PatchMetadata } from '../types/commands/index.js';
2
2
  import type { FirefoxConfig } from '../types/config.js';
3
3
  /** Metadata fields stamped on new or refreshed patch entries. */
4
4
  export declare function buildPatchSourceMetadata(firefox: Pick<FirefoxConfig, 'product' | 'version'>): Pick<PatchMetadata, 'sourceEsrVersion' | 'sourceProduct' | 'sourceVersion'>;
5
- /** Backward-compatible source version reader for legacy manifests. */
5
+ /** Source version reader that also accepts rows carrying only `sourceEsrVersion`. */
6
6
  export declare function getPatchSourceVersion(patch: Pick<PatchMetadata, 'sourceEsrVersion' | 'sourceVersion'>): string;
@@ -6,7 +6,7 @@ export function buildPatchSourceMetadata(firefox) {
6
6
  sourceVersion: firefox.version,
7
7
  };
8
8
  }
9
- /** Backward-compatible source version reader for legacy manifests. */
9
+ /** Source version reader that also accepts rows carrying only `sourceEsrVersion`. */
10
10
  export function getPatchSourceVersion(patch) {
11
11
  return patch.sourceVersion ?? patch.sourceEsrVersion;
12
12
  }
@@ -3,12 +3,14 @@ import type { PatchesManifest, PatchMetadata } from '../types/commands/index.js'
3
3
  * Resolves a `--patches <name…>` subset against the manifest, matching each
4
4
  * requested name tolerantly (exact filename, filename ± `.patch`, the bare
5
5
  * order number, the manifest `name` field, or the slug). Accepts repeated
6
- * values and comma lists. Throws listing the available patches when a
6
+ * values and lists separated by commas or whitespace, so a list passed as
7
+ * one shell word (`--patches "$P"` under zsh) still splits. Throws when a
7
8
  * requested name matches none, so a typo fails loud rather than silently
8
- * judging nothing.
9
+ * judging nothing; the refusal names that name and its nearest matches,
10
+ * not the whole queue.
9
11
  *
10
12
  * @param manifest - The loaded patch manifest
11
- * @param requested - Raw `--patches` values (each may be a comma list)
13
+ * @param requested - Raw `--patches` values (each may be a comma or whitespace list)
12
14
  * @param refusalHint - Sentence inserted into the no-match refusal, naming
13
15
  * what the caller's flag selects
14
16
  * @returns The matching manifest rows, de-duplicated, in request order
@@ -5,6 +5,7 @@
5
5
  * one gate selects the same patch in the others.
6
6
  */
7
7
  import { GeneralError } from '../errors/base.js';
8
+ import { formatPatchSuggestions } from './patch-identifier-suggest.js';
8
9
  /** Every spelling an operator may use for one patch. */
9
10
  function patchAliases(p) {
10
11
  const aliases = new Set([p.filename, p.filename.replace(/\.patch$/, ''), p.name]);
@@ -31,29 +32,29 @@ function matchesPatch(p, name) {
31
32
  * Resolves a `--patches <name…>` subset against the manifest, matching each
32
33
  * requested name tolerantly (exact filename, filename ± `.patch`, the bare
33
34
  * order number, the manifest `name` field, or the slug). Accepts repeated
34
- * values and comma lists. Throws listing the available patches when a
35
+ * values and lists separated by commas or whitespace, so a list passed as
36
+ * one shell word (`--patches "$P"` under zsh) still splits. Throws when a
35
37
  * requested name matches none, so a typo fails loud rather than silently
36
- * judging nothing.
38
+ * judging nothing; the refusal names that name and its nearest matches,
39
+ * not the whole queue.
37
40
  *
38
41
  * @param manifest - The loaded patch manifest
39
- * @param requested - Raw `--patches` values (each may be a comma list)
42
+ * @param requested - Raw `--patches` values (each may be a comma or whitespace list)
40
43
  * @param refusalHint - Sentence inserted into the no-match refusal, naming
41
44
  * what the caller's flag selects
42
45
  * @returns The matching manifest rows, de-duplicated, in request order
43
46
  */
44
47
  export function selectPatchSubset(manifest, requested, refusalHint = '') {
45
48
  const normalizedRequests = requested
46
- .flatMap((entry) => entry.split(','))
47
- .map((entry) => entry.trim())
49
+ .flatMap((entry) => entry.split(/[\s,]+/))
48
50
  .filter((entry) => entry.length > 0);
49
51
  const selected = [];
50
52
  const seen = new Set();
51
53
  for (const name of normalizedRequests) {
52
54
  const found = manifest.patches.filter((p) => matchesPatch(p, name));
53
55
  if (found.length === 0) {
54
- const available = manifest.patches.map((p) => p.filename).join(', ');
55
56
  throw new GeneralError(`No patch in the queue matches "${name}".${refusalHint ? ` ${refusalHint}` : ''} ` +
56
- `Available patches: ${available}`);
57
+ formatPatchSuggestions(name, manifest.patches));
57
58
  }
58
59
  for (const p of found) {
59
60
  if (!seen.has(p.filename)) {
@@ -45,8 +45,8 @@ export declare function extractNewFileContentsFromDiff(diff: string, targetFiles
45
45
  * `binary-body-not-reconstructable` queue lint, and
46
46
  * `buildPatchQueueContext` already tolerates them for real queue entries.
47
47
  *
48
- * One parse for the whole diff, rather than one per new file as the five
49
- * call sites previously did.
48
+ * One parse for the whole diff, rather than one per new file, shared by
49
+ * every call site.
50
50
  */
51
51
  export declare function buildNewFileTextProjection(diff: string): Map<string, string>;
52
52
  /**
@@ -101,8 +101,8 @@ function contentFromSections(sections) {
101
101
  * `binary-body-not-reconstructable` queue lint, and
102
102
  * `buildPatchQueueContext` already tolerates them for real queue entries.
103
103
  *
104
- * One parse for the whole diff, rather than one per new file as the five
105
- * call sites previously did.
104
+ * One parse for the whole diff, rather than one per new file, shared by
105
+ * every call site.
106
106
  */
107
107
  export function buildNewFileTextProjection(diff) {
108
108
  const newFiles = new Map();
@@ -29,9 +29,8 @@ const REBASE_PATCH_STATUSES = [
29
29
  /**
30
30
  * Drops payload that does not belong to an entry's status.
31
31
  *
32
- * Applied on read so a session written by an older FireForge, where
33
- * `rebase --continue` left `error`/`conflictingFiles` on a now-`resolved`
34
- * entry, loads cleanly and is corrected, instead of being refused by a
32
+ * Applied on read so a session whose `resolved` entry still carries
33
+ * `error`/`conflictingFiles` loads cleanly and is corrected, instead of being refused by a
35
34
  * stricter validator mid-rebase.
36
35
  *
37
36
  * @param entry - A structurally valid entry, possibly carrying stale payload
@@ -145,11 +144,10 @@ export async function readRebaseSession(projectRoot) {
145
144
  if (!isValidSession(data)) {
146
145
  return { present: true, valid: false, reason: 'the file is not a valid rebase session' };
147
146
  }
148
- // Normalize on read: an older session can carry a failure's
147
+ // Normalize on read: a session can carry a failure's
149
148
  // `error`/`conflictingFiles` on an entry whose status has since been
150
149
  // flipped to `resolved`. Correcting it here is what lets the validator
151
- // stay permissive, so an in-flight rebase from an older FireForge still
152
- // loads.
150
+ // stay permissive, so such an in-flight rebase still loads.
153
151
  return {
154
152
  present: true,
155
153
  valid: true,
@@ -2,7 +2,8 @@
2
2
  * xpcshell test-file registration. Without it, xpcshell test files (e.g.
3
3
  * `<module-dir>/test/unit/test_*.js`) are rejected by `fireforge register`
4
4
  * as "Unknown file pattern". This writer inserts the `["test_*.js"]` section
5
- * into the directory's `xpcshell.toml` (alphabetically, idempotently). With
5
+ * into the directory's `xpcshell.toml` (alphabetically or after the
6
+ * `--after` entry, idempotently). With
6
7
  * `--create-manifest` it also creates the manifest and wires
7
8
  * `XPCSHELL_TESTS_MANIFESTS` into the nearest moz.build.
8
9
  */
@@ -14,16 +15,36 @@ import type { RegisterResult } from './register-result.js';
14
15
  * missing rather than the file as unregistered).
15
16
  */
16
17
  export declare function isXpcshellTestRegistered(engineDir: string, dirRel: string, fileName: string): Promise<boolean>;
18
+ /** Where a new section goes and what placed it there. */
19
+ export interface XpcshellSectionPlacement {
20
+ /** The manifest content with the section inserted. */
21
+ content: string;
22
+ /** The `["…"]` header of the `--after` entry the section follows. */
23
+ previousEntry?: string | undefined;
24
+ /** `--after` named no entry, so the section went in sort order. */
25
+ afterFallback?: boolean | undefined;
26
+ }
17
27
  /**
18
- * Inserts the `["<fileName>"]` section into the manifest content in
19
- * mozbuild's case-insensitive sort order relative to the other test
20
- * sections, keeping one blank line between sections. Exported for direct
21
- * unit testing.
28
+ * Places the `["<fileName>"]` section in the manifest content.
29
+ *
30
+ * Without `after`, the section goes in mozbuild's case-insensitive sort
31
+ * order relative to the other test sections. With `after`, it goes right
32
+ * after the named entry's whole block (its keys included). Either way it
33
+ * is inserted ahead of the `#` comment lines that introduce the next
34
+ * entry, never between a comment and its entry, keeping one
35
+ * blank line between sections.
22
36
  */
23
- export declare function insertXpcshellManifestSection(content: string, fileName: string): string;
37
+ export declare function placeXpcshellManifestSection(content: string, fileName: string, after?: string): XpcshellSectionPlacement;
38
+ /**
39
+ * Inserts the `["<fileName>"]` section into the manifest content; see
40
+ * {@link placeXpcshellManifestSection}. Exported for direct unit testing.
41
+ *
42
+ * @internal
43
+ */
44
+ export declare function insertXpcshellManifestSection(content: string, fileName: string, after?: string): string;
24
45
  /**
25
46
  * Registers an xpcshell test file in its directory's `xpcshell.toml`.
26
47
  * With `createManifest`, a missing manifest is scaffolded and wired into
27
48
  * the nearest moz.build via `XPCSHELL_TESTS_MANIFESTS`.
28
49
  */
29
- export declare function registerXpcshellTest(engineDir: string, dirRel: string, fileName: string, dryRun?: boolean, createManifest?: boolean): Promise<RegisterResult>;
50
+ export declare function registerXpcshellTest(engineDir: string, dirRel: string, fileName: string, dryRun?: boolean, createManifest?: boolean, after?: string): Promise<RegisterResult>;
@@ -3,7 +3,8 @@
3
3
  * xpcshell test-file registration. Without it, xpcshell test files (e.g.
4
4
  * `<module-dir>/test/unit/test_*.js`) are rejected by `fireforge register`
5
5
  * as "Unknown file pattern". This writer inserts the `["test_*.js"]` section
6
- * into the directory's `xpcshell.toml` (alphabetically, idempotently). With
6
+ * into the directory's `xpcshell.toml` (alphabetically or after the
7
+ * `--after` entry, idempotently). With
7
8
  * `--create-manifest` it also creates the manifest and wires
8
9
  * `XPCSHELL_TESTS_MANIFESTS` into the nearest moz.build.
9
10
  */
@@ -32,40 +33,82 @@ export async function isXpcshellTestRegistered(engineDir, dirRel, fileName) {
32
33
  const content = await readText(manifestPath);
33
34
  return content.includes(`["${fileName}"]`);
34
35
  }
36
+ const SECTION_HEADER = /^\["([^"]+)"\]\s*$/;
35
37
  /**
36
- * Inserts the `["<fileName>"]` section into the manifest content in
37
- * mozbuild's case-insensitive sort order relative to the other test
38
- * sections, keeping one blank line between sections. Exported for direct
39
- * unit testing.
38
+ * The line where the block headed at `headerIndex` starts: the header
39
+ * itself, or the first of the `#` comment lines directly above it. A
40
+ * comment describes the entry below it, so nothing is inserted between
41
+ * them.
40
42
  */
41
- export function insertXpcshellManifestSection(content, fileName) {
43
+ function blockStart(lines, headerIndex) {
44
+ let start = headerIndex;
45
+ while (start > 0 && (lines[start - 1] ?? '').trimStart().startsWith('#'))
46
+ start -= 1;
47
+ return start;
48
+ }
49
+ /** Index of the section header `after` names: exact name first, then a substring. */
50
+ function findAfterHeader(lines, after) {
51
+ const headers = lines
52
+ .map((line, index) => ({ index, line, name: SECTION_HEADER.exec(line)?.[1] }))
53
+ .filter((header) => header.name !== undefined);
54
+ const exact = headers.find((header) => header.name === after || header.line.trim() === after);
55
+ return (exact ?? headers.find((header) => header.line.includes(after)))?.index ?? -1;
56
+ }
57
+ function spliceSection(lines, index, fileName) {
58
+ if (index === -1) {
59
+ // Append after the last section (or at end of file when there are no
60
+ // sections yet), keeping exactly one trailing newline.
61
+ while (lines.length > 0 && (lines.at(-1) ?? '').trim() === '')
62
+ lines.pop();
63
+ return [...lines, '', `["${fileName}"]`, ''].join('\n');
64
+ }
65
+ lines.splice(index, 0, `["${fileName}"]`, '');
66
+ return lines.join('\n');
67
+ }
68
+ /**
69
+ * Places the `["<fileName>"]` section in the manifest content.
70
+ *
71
+ * Without `after`, the section goes in mozbuild's case-insensitive sort
72
+ * order relative to the other test sections. With `after`, it goes right
73
+ * after the named entry's whole block (its keys included). Either way it
74
+ * is inserted ahead of the `#` comment lines that introduce the next
75
+ * entry, never between a comment and its entry, keeping one
76
+ * blank line between sections.
77
+ */
78
+ export function placeXpcshellManifestSection(content, fileName, after) {
42
79
  const lines = content.split('\n');
43
- const sectionHeaderPattern = /^\["([^"]+)"\]\s*$/;
44
- let insertIndex = -1;
45
- for (let i = 0; i < lines.length; i += 1) {
46
- const match = sectionHeaderPattern.exec(lines[i] ?? '');
47
- if (!match)
48
- continue;
49
- if (insertIndex === -1 && mozbuildSortCompare(fileName, match[1] ?? '') < 0) {
50
- insertIndex = i;
80
+ const headerIndexes = lines.flatMap((line, index) => (SECTION_HEADER.test(line) ? [index] : []));
81
+ if (after !== undefined) {
82
+ const afterIndex = findAfterHeader(lines, after);
83
+ if (afterIndex !== -1) {
84
+ const next = headerIndexes.find((index) => index > afterIndex);
85
+ return {
86
+ content: spliceSection(lines, next === undefined ? -1 : blockStart(lines, next), fileName),
87
+ previousEntry: (lines[afterIndex] ?? '').trim(),
88
+ };
51
89
  }
52
90
  }
53
- if (insertIndex !== -1) {
54
- lines.splice(insertIndex, 0, `["${fileName}"]`, '');
55
- return lines.join('\n');
56
- }
57
- // Append after the last section (or at end of file when there are no
58
- // sections yet), keeping exactly one trailing newline.
59
- while (lines.length > 0 && (lines.at(-1) ?? '').trim() === '')
60
- lines.pop();
61
- return [...lines, '', `["${fileName}"]`, ''].join('\n');
91
+ const before = headerIndexes.find((index) => mozbuildSortCompare(fileName, SECTION_HEADER.exec(lines[index] ?? '')?.[1] ?? '') < 0);
92
+ return {
93
+ content: spliceSection(lines, before === undefined ? -1 : blockStart(lines, before), fileName),
94
+ ...(after !== undefined ? { afterFallback: true } : {}),
95
+ };
96
+ }
97
+ /**
98
+ * Inserts the `["<fileName>"]` section into the manifest content; see
99
+ * {@link placeXpcshellManifestSection}. Exported for direct unit testing.
100
+ *
101
+ * @internal
102
+ */
103
+ export function insertXpcshellManifestSection(content, fileName, after) {
104
+ return placeXpcshellManifestSection(content, fileName, after).content;
62
105
  }
63
106
  /**
64
107
  * Registers an xpcshell test file in its directory's `xpcshell.toml`.
65
108
  * With `createManifest`, a missing manifest is scaffolded and wired into
66
109
  * the nearest moz.build via `XPCSHELL_TESTS_MANIFESTS`.
67
110
  */
68
- export async function registerXpcshellTest(engineDir, dirRel, fileName, dryRun = false, createManifest = false) {
111
+ export async function registerXpcshellTest(engineDir, dirRel, fileName, dryRun = false, createManifest = false, after) {
69
112
  const manifestRel = `${dirRel}/xpcshell.toml`;
70
113
  const manifestPath = join(engineDir, manifestRel);
71
114
  const entry = `["${fileName}"]`;
@@ -86,9 +129,15 @@ export async function registerXpcshellTest(engineDir, dirRel, fileName, dryRun =
86
129
  if (content.includes(entry)) {
87
130
  return { manifest: manifestRel, entry, skipped: true };
88
131
  }
89
- const updated = insertXpcshellManifestSection(content, fileName);
132
+ const placement = placeXpcshellManifestSection(content, fileName, after);
90
133
  if (!dryRun) {
91
- await writeText(manifestPath, updated);
134
+ await writeText(manifestPath, placement.content);
92
135
  }
93
- return { manifest: manifestRel, entry, skipped: false };
136
+ return {
137
+ manifest: manifestRel,
138
+ entry,
139
+ skipped: false,
140
+ previousEntry: placement.previousEntry,
141
+ afterFallback: placement.afterFallback,
142
+ };
94
143
  }
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Per-run output logs under `.fireforge/logs/`.
3
3
  *
4
- * A run's diagnosis used to exist in exactly one place: the terminal. Piping
5
- * through `tail`/`grep`, the ergonomic default when output is long, keeps
6
- * the summary and discards the `TEST-UNEXPECTED-FAIL` lines that say what
7
- * broke, and it launders the exit code besides. The operator rule ("never
8
- * pipe a run") was written down three times downstream and broken after each
9
- * writing, so this is not an operator problem: the tool is the only party
10
- * that can make the log survive the mistake.
4
+ * Without a log, a run's diagnosis exists in exactly one place: the terminal.
5
+ * Piping through `tail`/`grep`, the ergonomic default when output is long,
6
+ * keeps the summary and discards the `TEST-UNEXPECTED-FAIL` lines that say
7
+ * what broke, and it launders the exit code besides. An operator rule
8
+ * ("never pipe a run") does not hold in practice, so this is not an operator
9
+ * problem: the tool is the only party that can make the log survive the
10
+ * mistake.
11
11
  *
12
12
  * So `test` and `build` write their own complete copy as they stream, and the
13
13
  * `FIREFORGE-VERDICT:` line names the path. A piped, truncated, or
@@ -16,7 +16,7 @@
16
16
  * Everything here is best-effort. A log is a diagnostic aid, and a run must
17
17
  * never fail because one could not be opened, written, or pruned.
18
18
  * Every entry point swallows its errors and degrades to "no log", which is
19
- * exactly the behaviour that existed before this module.
19
+ * exactly the behaviour of a run without this module.
20
20
  */
21
21
  /** A run log sink. `write` never throws. */
22
22
  export interface RunLog {