@hominis/fireforge 0.44.4 → 0.46.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 (378) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/dist/bin/fireforge.js +6 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/src/cli-usage-error.d.ts +46 -0
  5. package/dist/src/cli-usage-error.js +81 -0
  6. package/dist/src/cli.d.ts +5 -1
  7. package/dist/src/cli.js +20 -21
  8. package/dist/src/commands/bootstrap.js +1 -2
  9. package/dist/src/commands/build.js +15 -6
  10. package/dist/src/commands/destructive-decision.d.ts +19 -0
  11. package/dist/src/commands/destructive-decision.js +31 -0
  12. package/dist/src/commands/doctor-check-core.d.ts +0 -6
  13. package/dist/src/commands/doctor-check-core.js +0 -8
  14. package/dist/src/commands/doctor-orphaned-harness.d.ts +0 -6
  15. package/dist/src/commands/doctor-orphaned-harness.js +4 -12
  16. package/dist/src/commands/doctor-post-rebase-audit.js +1 -1
  17. package/dist/src/commands/doctor-source-pin.d.ts +29 -0
  18. package/dist/src/commands/doctor-source-pin.js +81 -0
  19. package/dist/src/commands/doctor.d.ts +2 -0
  20. package/dist/src/commands/doctor.js +9 -8
  21. package/dist/src/commands/download.js +35 -24
  22. package/dist/src/commands/export-all.js +21 -4
  23. package/dist/src/commands/export-flow.js +15 -60
  24. package/dist/src/commands/export-placement-conflicts.js +3 -3
  25. package/dist/src/commands/export-placement-gate.js +2 -8
  26. package/dist/src/commands/export-placement-policy.js +3 -5
  27. package/dist/src/commands/export-shared.d.ts +42 -23
  28. package/dist/src/commands/export-shared.js +4 -19
  29. package/dist/src/commands/export.js +10 -1
  30. package/dist/src/commands/furnace/authoring-config.d.ts +13 -0
  31. package/dist/src/commands/furnace/authoring-config.js +19 -0
  32. package/dist/src/commands/furnace/chrome-doc-remove.js +1 -1
  33. package/dist/src/commands/furnace/create-browser-test.js +3 -8
  34. package/dist/src/commands/furnace/create-dry-run.js +2 -5
  35. package/dist/src/commands/furnace/create-templates.d.ts +17 -1
  36. package/dist/src/commands/furnace/create-templates.js +2 -1
  37. package/dist/src/commands/furnace/create.js +25 -19
  38. package/dist/src/commands/furnace/deploy.js +11 -2
  39. package/dist/src/commands/furnace/init.js +2 -1
  40. package/dist/src/commands/furnace/override.js +28 -19
  41. package/dist/src/commands/furnace/refresh.js +9 -1
  42. package/dist/src/commands/furnace/remove-mutations.d.ts +6 -7
  43. package/dist/src/commands/furnace/remove-mutations.js +0 -1
  44. package/dist/src/commands/furnace/remove.js +2 -6
  45. package/dist/src/commands/furnace/rename-browser-test.js +1 -7
  46. package/dist/src/commands/furnace/rename.js +29 -29
  47. package/dist/src/commands/furnace/status.js +12 -9
  48. package/dist/src/commands/furnace/test-file-name.d.ts +28 -0
  49. package/dist/src/commands/furnace/test-file-name.js +39 -0
  50. package/dist/src/commands/furnace/validation-output.d.ts +21 -9
  51. package/dist/src/commands/furnace/validation-output.js +3 -8
  52. package/dist/src/commands/import.js +2 -9
  53. package/dist/src/commands/lint-per-patch.d.ts +2 -2
  54. package/dist/src/commands/lint-per-patch.js +19 -4
  55. package/dist/src/commands/lint-per-run-checkjs.d.ts +2 -2
  56. package/dist/src/commands/lint.d.ts +2 -1
  57. package/dist/src/commands/lint.js +1 -1
  58. package/dist/src/commands/manifest.js +1 -1
  59. package/dist/src/commands/package.js +2 -4
  60. package/dist/src/commands/patch/compact.js +14 -15
  61. package/dist/src/commands/patch/delete.js +15 -25
  62. package/dist/src/commands/patch/entry-projection.d.ts +13 -0
  63. package/dist/src/commands/patch/entry-projection.js +24 -0
  64. package/dist/src/commands/patch/lint-ignore.d.ts +0 -8
  65. package/dist/src/commands/patch/lint-ignore.js +19 -29
  66. package/dist/src/commands/patch/move-files-create.js +24 -10
  67. package/dist/src/commands/patch/move-files-into.js +38 -37
  68. package/dist/src/commands/patch/move-files.js +4 -13
  69. package/dist/src/commands/patch/rename.js +27 -41
  70. package/dist/src/commands/patch/reorder.js +25 -38
  71. package/dist/src/commands/patch/split-plan.d.ts +4 -5
  72. package/dist/src/commands/patch/split-plan.js +4 -11
  73. package/dist/src/commands/patch/split.js +45 -43
  74. package/dist/src/commands/patch/staged-dependency.js +16 -22
  75. package/dist/src/commands/patch/tier.js +15 -25
  76. package/dist/src/commands/patch-rename-map.d.ts +19 -0
  77. package/dist/src/commands/patch-rename-map.js +36 -0
  78. package/dist/src/commands/pending-resolution.d.ts +15 -0
  79. package/dist/src/commands/pending-resolution.js +25 -0
  80. package/dist/src/commands/re-export-adjacent.d.ts +32 -2
  81. package/dist/src/commands/re-export-adjacent.js +51 -10
  82. package/dist/src/commands/re-export-bulk-scan.js +6 -10
  83. package/dist/src/commands/re-export-drift.d.ts +8 -0
  84. package/dist/src/commands/re-export-drift.js +65 -4
  85. package/dist/src/commands/re-export-files.d.ts +2 -3
  86. package/dist/src/commands/re-export-files.js +38 -32
  87. package/dist/src/commands/re-export-lint.d.ts +3 -4
  88. package/dist/src/commands/re-export-lint.js +9 -2
  89. package/dist/src/commands/re-export-options.js +19 -5
  90. package/dist/src/commands/re-export-register.js +5 -1
  91. package/dist/src/commands/re-export-scan.js +8 -8
  92. package/dist/src/commands/re-export-single.d.ts +22 -3
  93. package/dist/src/commands/re-export-single.js +19 -11
  94. package/dist/src/commands/re-export.js +44 -27
  95. package/dist/src/commands/rebase/abort.js +3 -8
  96. package/dist/src/commands/rebase/continue.js +12 -12
  97. package/dist/src/commands/rebase/index.js +2 -2
  98. package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
  99. package/dist/src/commands/rebase/patch-loop.js +2 -7
  100. package/dist/src/commands/resolve.js +18 -10
  101. package/dist/src/commands/run.d.ts +0 -13
  102. package/dist/src/commands/run.js +12 -25
  103. package/dist/src/commands/setup-support.d.ts +16 -4
  104. package/dist/src/commands/setup-support.js +16 -16
  105. package/dist/src/commands/setup.js +3 -9
  106. package/dist/src/commands/source.js +8 -1
  107. package/dist/src/commands/status-check.js +2 -2
  108. package/dist/src/commands/status-output.js +2 -2
  109. package/dist/src/commands/status.js +3 -4
  110. package/dist/src/commands/test-build-phase.d.ts +2 -2
  111. package/dist/src/commands/test-build-phase.js +33 -8
  112. package/dist/src/commands/test-diagnose.js +14 -5
  113. package/dist/src/commands/test-modes.d.ts +2 -2
  114. package/dist/src/commands/test-modes.js +0 -1
  115. package/dist/src/commands/test-register.js +6 -3
  116. package/dist/src/commands/test-run.js +29 -21
  117. package/dist/src/commands/test-stale-gate.js +8 -5
  118. package/dist/src/commands/test-verdict.d.ts +16 -2
  119. package/dist/src/commands/test-verdict.js +18 -3
  120. package/dist/src/commands/test.js +51 -7
  121. package/dist/src/commands/token-list.d.ts +26 -0
  122. package/dist/src/commands/token-list.js +153 -0
  123. package/dist/src/commands/token.js +17 -1
  124. package/dist/src/commands/tree.js +7 -2
  125. package/dist/src/commands/typecheck.js +9 -9
  126. package/dist/src/commands/wire.js +4 -3
  127. package/dist/src/core/branding.d.ts +0 -7
  128. package/dist/src/core/branding.js +3 -2
  129. package/dist/src/core/browser-wire.js +6 -3
  130. package/dist/src/core/build-audit-platform.d.ts +29 -0
  131. package/dist/src/core/build-audit-platform.js +140 -0
  132. package/dist/src/core/build-audit-registration.js +3 -2
  133. package/dist/src/core/build-audit-skip.d.ts +57 -0
  134. package/dist/src/core/build-audit-skip.js +112 -0
  135. package/dist/src/core/build-audit.d.ts +15 -2
  136. package/dist/src/core/build-audit.js +154 -12
  137. package/dist/src/core/build-baseline.d.ts +45 -28
  138. package/dist/src/core/build-baseline.js +6 -29
  139. package/dist/src/core/build-jar-escalation.d.ts +64 -0
  140. package/dist/src/core/build-jar-escalation.js +128 -0
  141. package/dist/src/core/build-overwrite-guard.js +1 -1
  142. package/dist/src/core/build-prepare.d.ts +5 -2
  143. package/dist/src/core/build-prepare.js +34 -6
  144. package/dist/src/core/config-paths.d.ts +2 -6
  145. package/dist/src/core/config-paths.js +4 -2
  146. package/dist/src/core/config-state.d.ts +0 -6
  147. package/dist/src/core/config-state.js +1 -19
  148. package/dist/src/core/config-validate-build-audit.d.ts +8 -0
  149. package/dist/src/core/config-validate-build-audit.js +80 -0
  150. package/dist/src/core/config-validate-file-size.d.ts +12 -0
  151. package/dist/src/core/config-validate-file-size.js +65 -0
  152. package/dist/src/core/config-validate-patch-policy.d.ts +2 -2
  153. package/dist/src/core/config-validate-patch-policy.js +3 -3
  154. package/dist/src/core/config-validate-test-toolchains.d.ts +2 -2
  155. package/dist/src/core/config-validate-test-toolchains.js +4 -5
  156. package/dist/src/core/config-validate.js +20 -3
  157. package/dist/src/core/config.d.ts +2 -2
  158. package/dist/src/core/config.js +11 -15
  159. package/dist/src/core/coverage-extend.js +4 -4
  160. package/dist/src/core/discard-baseline.js +7 -7
  161. package/dist/src/core/engine-precondition.d.ts +1 -9
  162. package/dist/src/core/engine-precondition.js +3 -8
  163. package/dist/src/core/engine-session-lock.js +13 -17
  164. package/dist/src/core/export-moved-code-shape.js +2 -2
  165. package/dist/src/core/file-lock-owner.d.ts +103 -0
  166. package/dist/src/core/file-lock-owner.js +259 -0
  167. package/dist/src/core/file-lock.d.ts +2 -7
  168. package/dist/src/core/file-lock.js +40 -91
  169. package/dist/src/core/firefox-cache.d.ts +13 -1
  170. package/dist/src/core/firefox-cache.js +59 -34
  171. package/dist/src/core/firefox-download.d.ts +15 -0
  172. package/dist/src/core/firefox-download.js +37 -21
  173. package/dist/src/core/firefox-extract.d.ts +6 -5
  174. package/dist/src/core/firefox-extract.js +36 -18
  175. package/dist/src/core/firefox.d.ts +30 -32
  176. package/dist/src/core/firefox.js +5 -36
  177. package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
  178. package/dist/src/core/furnace-apply-dry-run.js +1 -7
  179. package/dist/src/core/furnace-apply-helpers.d.ts +0 -11
  180. package/dist/src/core/furnace-apply-helpers.js +8 -19
  181. package/dist/src/core/furnace-apply-overwrite-warn.js +4 -2
  182. package/dist/src/core/furnace-apply.d.ts +1 -1
  183. package/dist/src/core/furnace-apply.js +30 -10
  184. package/dist/src/core/furnace-config.d.ts +1 -1
  185. package/dist/src/core/furnace-config.js +23 -36
  186. package/dist/src/core/furnace-constants.js +3 -2
  187. package/dist/src/core/furnace-dir-entry.d.ts +20 -0
  188. package/dist/src/core/furnace-dir-entry.js +21 -0
  189. package/dist/src/core/furnace-operation.d.ts +2 -0
  190. package/dist/src/core/furnace-operation.js +3 -1
  191. package/dist/src/core/furnace-refresh.d.ts +18 -6
  192. package/dist/src/core/furnace-refresh.js +3 -6
  193. package/dist/src/core/furnace-registration-ast.d.ts +0 -1
  194. package/dist/src/core/furnace-registration-ast.js +0 -3
  195. package/dist/src/core/furnace-registration.d.ts +2 -1
  196. package/dist/src/core/furnace-registration.js +22 -29
  197. package/dist/src/core/furnace-scanner.js +3 -6
  198. package/dist/src/core/furnace-state-persist.d.ts +2 -2
  199. package/dist/src/core/furnace-validate-registration.js +5 -7
  200. package/dist/src/core/git-base.d.ts +3 -1
  201. package/dist/src/core/git-base.js +25 -3
  202. package/dist/src/core/git-diff-new-file.d.ts +40 -0
  203. package/dist/src/core/git-diff-new-file.js +119 -0
  204. package/dist/src/core/git-diff.d.ts +9 -9
  205. package/dist/src/core/git-diff.js +88 -157
  206. package/dist/src/core/git-file-ops.d.ts +0 -7
  207. package/dist/src/core/git-file-ops.js +4 -28
  208. package/dist/src/core/git-readonly-index.d.ts +0 -2
  209. package/dist/src/core/git-readonly-index.js +0 -4
  210. package/dist/src/core/git-status.d.ts +7 -0
  211. package/dist/src/core/git-status.js +21 -9
  212. package/dist/src/core/git.d.ts +0 -6
  213. package/dist/src/core/git.js +14 -43
  214. package/dist/src/core/harness-orphans.d.ts +73 -0
  215. package/dist/src/core/harness-orphans.js +187 -0
  216. package/dist/src/core/history-log.d.ts +19 -0
  217. package/dist/src/core/history-log.js +29 -0
  218. package/dist/src/core/mach-error-hints.d.ts +0 -5
  219. package/dist/src/core/mach-error-hints.js +1 -1
  220. package/dist/src/core/mach-known-noise-filter.d.ts +25 -3
  221. package/dist/src/core/mach-known-noise-filter.js +31 -10
  222. package/dist/src/core/mach-mozconfig.js +2 -1
  223. package/dist/src/core/mach-python.d.ts +2 -0
  224. package/dist/src/core/mach-python.js +12 -7
  225. package/dist/src/core/mach-resource-shim.d.ts +13 -1
  226. package/dist/src/core/mach-resource-shim.js +112 -9
  227. package/dist/src/core/mach.d.ts +46 -53
  228. package/dist/src/core/mach.js +31 -83
  229. package/dist/src/core/marionette-port.d.ts +0 -17
  230. package/dist/src/core/marionette-port.js +12 -58
  231. package/dist/src/core/marionette-preflight.js +9 -27
  232. package/dist/src/core/mochitest-server-port.js +3 -3
  233. package/dist/src/core/moz-manifest-rules.js +37 -33
  234. package/dist/src/core/patch-apply-fuzz.js +4 -4
  235. package/dist/src/core/patch-apply.d.ts +4 -5
  236. package/dist/src/core/patch-apply.js +55 -32
  237. package/dist/src/core/patch-export-coverage.d.ts +0 -9
  238. package/dist/src/core/patch-export-coverage.js +1 -30
  239. package/dist/src/core/patch-export-update.d.ts +18 -1
  240. package/dist/src/core/patch-export-update.js +4 -3
  241. package/dist/src/core/patch-export.d.ts +11 -21
  242. package/dist/src/core/patch-export.js +44 -66
  243. package/dist/src/core/patch-files.d.ts +0 -4
  244. package/dist/src/core/patch-files.js +0 -11
  245. package/dist/src/core/patch-lint-binary.d.ts +6 -12
  246. package/dist/src/core/patch-lint-cache.d.ts +17 -1
  247. package/dist/src/core/patch-lint-cache.js +3 -6
  248. package/dist/src/core/patch-lint-checkjs.d.ts +33 -16
  249. package/dist/src/core/patch-lint-checkjs.js +43 -25
  250. package/dist/src/core/patch-lint-creators.d.ts +2 -6
  251. package/dist/src/core/patch-lint-cross.d.ts +16 -8
  252. package/dist/src/core/patch-lint-cross.js +49 -92
  253. package/dist/src/core/patch-lint-css.js +10 -6
  254. package/dist/src/core/patch-lint-file-size.d.ts +52 -0
  255. package/dist/src/core/patch-lint-file-size.js +53 -0
  256. package/dist/src/core/patch-lint-forward-registration.d.ts +58 -0
  257. package/dist/src/core/patch-lint-forward-registration.js +210 -0
  258. package/dist/src/core/patch-lint-jsdoc.js +36 -36
  259. package/dist/src/core/patch-lint-module-registration.d.ts +2 -8
  260. package/dist/src/core/patch-lint-prettier.d.ts +36 -0
  261. package/dist/src/core/patch-lint-prettier.js +126 -0
  262. package/dist/src/core/patch-lint-queue-types.d.ts +60 -0
  263. package/dist/src/core/patch-lint-queue-types.js +12 -0
  264. package/dist/src/core/patch-lint-staged-registration.d.ts +61 -0
  265. package/dist/src/core/patch-lint-staged-registration.js +90 -0
  266. package/dist/src/core/patch-lint-unmanaged-companion.d.ts +106 -0
  267. package/dist/src/core/patch-lint-unmanaged-companion.js +214 -0
  268. package/dist/src/core/patch-lint.d.ts +14 -9
  269. package/dist/src/core/patch-lint.js +27 -27
  270. package/dist/src/core/patch-lock.js +3 -3
  271. package/dist/src/core/patch-manifest-io.js +8 -7
  272. package/dist/src/core/patch-parse.d.ts +19 -8
  273. package/dist/src/core/patch-parse.js +25 -0
  274. package/dist/src/core/patch-policy.js +15 -27
  275. package/dist/src/core/patch-source-metadata.d.ts +1 -3
  276. package/dist/src/core/patch-source-metadata.js +0 -4
  277. package/dist/src/core/patch-transform.d.ts +21 -8
  278. package/dist/src/core/patch-transform.js +38 -10
  279. package/dist/src/core/process-cpu-time.js +1 -1
  280. package/dist/src/core/rebase-session.d.ts +2 -8
  281. package/dist/src/core/rebase-session.js +5 -17
  282. package/dist/src/core/register-browser-content.js +3 -2
  283. package/dist/src/core/register-browser-test.js +2 -1
  284. package/dist/src/core/register-module.js +2 -1
  285. package/dist/src/core/register-scaffold.js +5 -4
  286. package/dist/src/core/register-shared-css.js +2 -1
  287. package/dist/src/core/run-log-redact.d.ts +38 -0
  288. package/dist/src/core/run-log-redact.js +69 -0
  289. package/dist/src/core/run-log.d.ts +13 -2
  290. package/dist/src/core/run-log.js +67 -18
  291. package/dist/src/core/signal-critical.js +2 -4
  292. package/dist/src/core/smoke-patterns.d.ts +0 -1
  293. package/dist/src/core/smoke-patterns.js +18 -11
  294. package/dist/src/core/state-file.d.ts +12 -1
  295. package/dist/src/core/state-file.js +14 -1
  296. package/dist/src/core/status-classify.js +2 -1
  297. package/dist/src/core/test-changed-prefs.js +26 -11
  298. package/dist/src/core/test-harness-crash.js +61 -48
  299. package/dist/src/core/test-harness-verdict-notes.d.ts +55 -0
  300. package/dist/src/core/test-harness-verdict-notes.js +48 -0
  301. package/dist/src/core/test-stale-check.d.ts +23 -8
  302. package/dist/src/core/test-stale-check.js +77 -4
  303. package/dist/src/core/test-stall-triage.js +37 -19
  304. package/dist/src/core/test-xpcshell-retry.d.ts +26 -7
  305. package/dist/src/core/test-xpcshell-retry.js +14 -11
  306. package/dist/src/core/token-category.d.ts +33 -0
  307. package/dist/src/core/token-category.js +56 -48
  308. package/dist/src/core/token-dark-mode.js +2 -14
  309. package/dist/src/core/token-inventory.d.ts +56 -0
  310. package/dist/src/core/token-inventory.js +109 -0
  311. package/dist/src/core/token-manager.d.ts +1 -1
  312. package/dist/src/core/token-manager.js +1 -1
  313. package/dist/src/core/token-variant.js +21 -22
  314. package/dist/src/core/toolchain-preflight.js +1 -1
  315. package/dist/src/core/tree-cow.d.ts +0 -2
  316. package/dist/src/core/tree-cow.js +2 -2
  317. package/dist/src/core/tree-store.js +4 -5
  318. package/dist/src/core/typecheck.js +2 -2
  319. package/dist/src/core/wire-dom-fragment.js +6 -17
  320. package/dist/src/core/wire-utils.js +5 -17
  321. package/dist/src/core/xpcshell-appdir.js +2 -4
  322. package/dist/src/errors/base.d.ts +53 -1
  323. package/dist/src/errors/base.js +79 -11
  324. package/dist/src/errors/build.d.ts +28 -0
  325. package/dist/src/errors/build.js +51 -10
  326. package/dist/src/errors/config.d.ts +0 -14
  327. package/dist/src/errors/config.js +13 -34
  328. package/dist/src/errors/download.d.ts +13 -0
  329. package/dist/src/errors/download.js +53 -24
  330. package/dist/src/errors/furnace.js +8 -9
  331. package/dist/src/errors/git.d.ts +8 -8
  332. package/dist/src/errors/git.js +20 -30
  333. package/dist/src/errors/patch.js +8 -10
  334. package/dist/src/errors/rebase.js +7 -7
  335. package/dist/src/types/commands/options.d.ts +96 -123
  336. package/dist/src/types/commands/patches.d.ts +17 -6
  337. package/dist/src/types/config.d.ts +75 -2
  338. package/dist/src/types/index.d.ts +0 -1
  339. package/dist/src/utils/assert.d.ts +3 -15
  340. package/dist/src/utils/assert.js +1 -18
  341. package/dist/src/utils/build-info.d.ts +1 -1
  342. package/dist/src/utils/child-env.d.ts +2 -1
  343. package/dist/src/utils/elapsed.js +10 -1
  344. package/dist/src/utils/fs.d.ts +4 -2
  345. package/dist/src/utils/fs.js +11 -14
  346. package/dist/src/utils/hash.d.ts +12 -0
  347. package/dist/src/utils/hash.js +16 -0
  348. package/dist/src/utils/line-dispatch.d.ts +15 -0
  349. package/dist/src/utils/line-dispatch.js +48 -0
  350. package/dist/src/utils/options.d.ts +1 -7
  351. package/dist/src/utils/options.js +6 -7
  352. package/dist/src/utils/package-root.js +3 -12
  353. package/dist/src/utils/platform.d.ts +0 -4
  354. package/dist/src/utils/process-group.d.ts +2 -1
  355. package/dist/src/utils/process-group.js +20 -19
  356. package/dist/src/utils/process.d.ts +40 -26
  357. package/dist/src/utils/process.js +115 -166
  358. package/dist/src/utils/ps-duration.js +3 -3
  359. package/dist/src/utils/sleep.d.ts +26 -0
  360. package/dist/src/utils/sleep.js +31 -0
  361. package/dist/src/utils/validation.d.ts +2 -38
  362. package/dist/src/utils/validation.js +7 -16
  363. package/docs/configuration.md +136 -1
  364. package/docs/exit-codes.md +34 -20
  365. package/docs/lifecycle-invariants.md +18 -12
  366. package/docs/machine-output.md +31 -8
  367. package/docs/patch-workflow.md +2 -1
  368. package/docs/run-logs.md +44 -4
  369. package/docs/testing.md +70 -11
  370. package/package.json +8 -7
  371. package/dist/src/commands/rebase.d.ts +0 -4
  372. package/dist/src/commands/rebase.js +0 -5
  373. package/dist/src/core/patch-artifact-normalize.d.ts +0 -21
  374. package/dist/src/core/patch-artifact-normalize.js +0 -24
  375. package/dist/src/core/patch-lint-reexports.d.ts +0 -7
  376. package/dist/src/core/patch-lint-reexports.js +0 -8
  377. package/dist/src/core/wire-targets.d.ts +0 -7
  378. package/dist/src/core/wire-targets.js +0 -8
@@ -32,29 +32,29 @@ const ATTRIBUTE_FRAGMENT_PATTERN = /^\[[a-zA-Z][a-zA-Z0-9_-]*(?:=(?:"[a-zA-Z0-9_
32
32
  const QUALIFIER_PATTERN = /^:[a-zA-Z][a-zA-Z0-9-]*(?:\(([^()]*)\))?$/;
33
33
  /**
34
34
  * Splits a run of consecutive attribute fragments into its groups, or
35
- * returns `null` when the run is not exactly that — a group that does not
35
+ * returns `undefined` when the run is not exactly that — a group that does not
36
36
  * open with `[`, an unclosed group, an empty run, or a group whose name or
37
37
  * value is not identifier-safe.
38
38
  *
39
39
  * @param run - The `[…][…]` text with no `:root` prefix
40
- * @returns The individual fragments, or null when the run is not valid
40
+ * @returns The individual fragments, or undefined when the run is not valid
41
41
  */
42
42
  function splitAttributeGroups(run) {
43
43
  const groups = [];
44
44
  let cursor = 0;
45
45
  while (cursor < run.length) {
46
46
  if (run[cursor] !== '[')
47
- return null;
47
+ return undefined;
48
48
  const close = run.indexOf(']', cursor);
49
49
  if (close === -1)
50
- return null;
50
+ return undefined;
51
51
  const group = run.slice(cursor, close + 1);
52
52
  if (!ATTRIBUTE_FRAGMENT_PATTERN.test(group))
53
- return null;
53
+ return undefined;
54
54
  groups.push(group);
55
55
  cursor = close + 1;
56
56
  }
57
- return groups.length > 0 ? groups : null;
57
+ return groups.length > 0 ? groups : undefined;
58
58
  }
59
59
  /** Rewrites `=value` / `='value'` to the double-quoted Mozilla convention. */
60
60
  function normalizeAttributeFragment(group) {
@@ -96,10 +96,10 @@ export function validateVariantSelector(raw) {
96
96
  // The parse must have consumed the WHOLE fragment: anything it declined to
97
97
  // read (a stray space, a trailing combinator) is text the matcher would
98
98
  // not see either, so accepting it would author a block nothing can find.
99
- if (parts === null || `${parts.attributes}${parts.qualifier}` !== `:root${value}`)
99
+ if (parts === undefined || `${parts.attributes}${parts.qualifier}` !== `:root${value}`)
100
100
  return reject;
101
101
  const groups = splitAttributeGroups(parts.attributes.slice(':root'.length));
102
- if (groups === null)
102
+ if (groups === undefined)
103
103
  return reject;
104
104
  let qualifier = '';
105
105
  if (parts.qualifier !== '') {
@@ -112,7 +112,7 @@ export function validateVariantSelector(raw) {
112
112
  }
113
113
  else {
114
114
  const innerGroups = splitAttributeGroups(inner);
115
- if (innerGroups === null)
115
+ if (innerGroups === undefined)
116
116
  return reject;
117
117
  const name = parts.qualifier.slice(0, parts.qualifier.indexOf('('));
118
118
  qualifier = `${name}(${innerGroups.map(normalizeAttributeFragment).join('')})`;
@@ -131,7 +131,7 @@ function canonicalSelector(selector) {
131
131
  }
132
132
  /**
133
133
  * Splits the `:root[…]` selector on `line` into its attribute run and any
134
- * trailing qualifier, or `null` when the line carries none.
134
+ * trailing qualifier, or `undefined` when the line carries none.
135
135
  *
136
136
  * Index arithmetic rather than `/:root\[[^{]*\]/`: `[^{]` also matches `]`,
137
137
  * so that pattern backtracks quadratically on a line repeating `:root[`
@@ -155,7 +155,7 @@ function canonicalSelector(selector) {
155
155
  function rootAttributeSelector(line) {
156
156
  const start = line.indexOf(':root[');
157
157
  if (start === -1)
158
- return null;
158
+ return undefined;
159
159
  const brace = line.indexOf('{', start);
160
160
  const limit = brace === -1 ? line.length : brace;
161
161
  let cursor = start + ':root'.length;
@@ -168,13 +168,13 @@ function rootAttributeSelector(line) {
168
168
  end = cursor;
169
169
  }
170
170
  if (end === start + ':root'.length)
171
- return null;
171
+ return undefined;
172
172
  return { attributes: line.slice(start, end), qualifier: line.slice(end, limit).trim() };
173
173
  }
174
174
  /**
175
175
  * Finds the top-level `:root<variant>` block whose attribute selector
176
176
  * matches `variant` (compared canonically). Returns the opening-brace and
177
- * closing-brace line indices, or `null` when no such block exists.
177
+ * closing-brace line indices, or `undefined` when no such block exists.
178
178
  *
179
179
  * Scans a comment-stripped mirror of `lines` so braces inside comments do
180
180
  * not offset the depth counter; the returned indices line up with the
@@ -189,10 +189,9 @@ function findVariantBlock(lines, variant) {
189
189
  const requested = rootAttributeSelector(`:root${variant} {`);
190
190
  const want = canonicalSelector(requested?.attributes ?? `:root${variant}`);
191
191
  const wantQualifier = canonicalSelector(requested?.qualifier ?? '');
192
- for (let i = 0; i < stripped.length; i++) {
193
- const line = stripped[i] ?? '';
192
+ for (const [i, line] of stripped.entries()) {
194
193
  const selector = rootAttributeSelector(line);
195
- if (selector === null || canonicalSelector(selector.attributes) !== want)
194
+ if (selector === undefined || canonicalSelector(selector.attributes) !== want)
196
195
  continue;
197
196
  // An UNqualified request still matches a qualified block: that is the
198
197
  // 0.44.2 behaviour `variantBlockQualifier` exists to report, since
@@ -219,7 +218,7 @@ function findVariantBlock(lines, variant) {
219
218
  continue;
220
219
  return { open: openLine, close, qualifier: selector.qualifier };
221
220
  }
222
- return null;
221
+ return undefined;
223
222
  }
224
223
  /**
225
224
  * Returns the line index at which a brand-new `:root<variant>` block should
@@ -229,8 +228,8 @@ function findVariantBlock(lines, variant) {
229
228
  */
230
229
  function findVariantBlockInsertionPoint(lines) {
231
230
  const stripped = stripBlockCommentsInLines(lines);
232
- for (let i = 0; i < stripped.length; i++) {
233
- if (/^\s*:root\s*\{/.test(stripped[i] ?? '')) {
231
+ for (const [i, line] of stripped.entries()) {
232
+ if (/^\s*:root\s*\{/.test(line)) {
234
233
  const close = findBlockCloseIndex(stripped, i);
235
234
  if (close !== -1)
236
235
  return close + 1;
@@ -245,7 +244,7 @@ function findVariantBlockInsertionPoint(lines) {
245
244
  * the block, which is only wanted for the explicit `--variant` flow.
246
245
  */
247
246
  export function variantBlockExists(lines, variant) {
248
- return findVariantBlock(lines, variant) !== null;
247
+ return findVariantBlock(lines, variant) !== undefined;
249
248
  }
250
249
  /**
251
250
  * The pseudo-class qualifier on the matched `:root<variant>` block, or
@@ -300,8 +299,8 @@ export function findVariantBlockDeclaration(lines, variant, tokenName) {
300
299
  .join('\n')
301
300
  .replace(/\/\*[\s\S]*?\*\//g, (match) => match.replace(/[^\n]/g, ' '))
302
301
  .split('\n');
303
- for (let i = 0; i < stripped.length; i++) {
304
- if ((stripped[i] ?? '').includes(`${tokenName}:`))
302
+ for (const [i, line] of stripped.entries()) {
303
+ if (line.includes(`${tokenName}:`))
305
304
  return { line: block.open + i + 1 };
306
305
  }
307
306
  return undefined;
@@ -121,7 +121,7 @@ export function formatMajorVersionHopNotice(previousVersion, newVersion) {
121
121
  return undefined;
122
122
  if (previousMajor === newMajor)
123
123
  return undefined;
124
- return (`Firefox major version changed (${String(previousMajor)} → ${String(newMajor)}): ` +
124
+ return (`Firefox major version changed (${previousMajor} → ${newMajor}): ` +
125
125
  'upstream toolchain minimums (cbindgen, Rust, …) may have moved with it. ' +
126
126
  'Consider running "fireforge bootstrap" before the next build.');
127
127
  }
@@ -2,8 +2,6 @@
2
2
  export type CloneExecutor = (command: string, args: string[]) => Promise<void>;
3
3
  /** How this host can materialise a tree. */
4
4
  export type CowCapability = 'clonefile' | 'reflink' | 'none';
5
- /** Per-platform `cp` argv prefix for a CoW copy of one entry. */
6
- export declare function cowCopyArgs(capability: Exclude<CowCapability, 'none'>): string[];
7
5
  /**
8
6
  * Probes CoW support in `dir` by cloning a tiny probe file there.
9
7
  * Non-darwin/linux platforms report 'none' without probing.
@@ -20,11 +20,11 @@ import { exec } from '../utils/process.js';
20
20
  const defaultExecutor = async (command, args) => {
21
21
  const result = await exec(command, args);
22
22
  if (result.exitCode !== 0) {
23
- throw new Error(`${command} ${args.join(' ')} failed (exit ${String(result.exitCode)}): ${result.stderr}`);
23
+ throw new Error(`${command} ${args.join(' ')} failed (exit ${result.exitCode}): ${result.stderr}`);
24
24
  }
25
25
  };
26
26
  /** Per-platform `cp` argv prefix for a CoW copy of one entry. */
27
- export function cowCopyArgs(capability) {
27
+ function cowCopyArgs(capability) {
28
28
  return capability === 'clonefile' ? ['-c', '-R', '-p'] : ['--reflink=always', '-a'];
29
29
  }
30
30
  /**
@@ -19,6 +19,7 @@ import { GeneralError } from '../errors/base.js';
19
19
  import { mapWithConcurrency } from '../utils/concurrency.js';
20
20
  import { getNodeErrorCode, isProcessAlive, toError } from '../utils/errors.js';
21
21
  import { pathExists, readJson, writeJson } from '../utils/fs.js';
22
+ import { sha256Hex } from '../utils/hash.js';
22
23
  import { verbose, warn } from '../utils/logger.js';
23
24
  import { isObject } from '../utils/validation.js';
24
25
  import { getBuildBaselinePath } from './build-baseline.js';
@@ -128,7 +129,7 @@ export async function readTreeMarker(root) {
128
129
  const code = getNodeErrorCode(error);
129
130
  if (code === 'ENOENT' || code === 'ENOTDIR')
130
131
  return { kind: 'absent' };
131
- verbose(`Unreadable tree marker at ${markerPath}: ${String(error)}`);
132
+ verbose(`Unreadable tree marker at ${markerPath}: ${toError(error).message}`);
132
133
  return { kind: 'corrupt', reason: `the marker could not be read (${toError(error).message})` };
133
134
  }
134
135
  }
@@ -178,9 +179,7 @@ export async function computePrimaryFingerprint(primaryRoot) {
178
179
  }
179
180
  let engineFingerprint;
180
181
  try {
181
- engineFingerprint = createHash('sha256')
182
- .update(await getAllDiff(engineDir))
183
- .digest('hex');
182
+ engineFingerprint = sha256Hex(await getAllDiff(engineDir));
184
183
  }
185
184
  catch {
186
185
  engineFingerprint = null;
@@ -461,7 +460,7 @@ export async function removeTree(primaryRoot, name, options = {}) {
461
460
  ]) {
462
461
  const state = await inspectTreeLock(lockPath);
463
462
  if (state.kind === 'held') {
464
- throw new GeneralError(`Refusing to remove tree "${name}": a live process (pid ${String(state.pid)}) holds ${lockPath}. ` +
463
+ throw new GeneralError(`Refusing to remove tree "${name}": a live process (pid ${state.pid}) holds ${lockPath}. ` +
465
464
  'Wait for it to finish, then retry.');
466
465
  }
467
466
  if (state.kind === 'unknown' && options.force !== true) {
@@ -144,7 +144,7 @@ export async function runTypecheck(projectRoot, cfg) {
144
144
  */
145
145
  function resolveProjectExtraShim(cfg, projectPath) {
146
146
  const overrides = cfg.projectOverrides;
147
- if (overrides && Object.prototype.hasOwnProperty.call(overrides, projectPath)) {
147
+ if (overrides && Object.hasOwn(overrides, projectPath)) {
148
148
  const value = overrides[projectPath];
149
149
  return value === null ? undefined : value;
150
150
  }
@@ -298,7 +298,7 @@ async function runTypecheckForProject(ts, projectRoot, projectPath, shimSource,
298
298
  }
299
299
  issues.push(diagnosticToIssue(ts, diag, absConfig, projectPath));
300
300
  }
301
- verbose(`typecheck: ${projectPath} — analyzed ${String(parsed.fileNames.length)} file(s), found ${String(issues.length)} issue(s)`);
301
+ verbose(`typecheck: ${projectPath} — analyzed ${parsed.fileNames.length} file(s), found ${issues.length} issue(s)`);
302
302
  return {
303
303
  project: projectPath,
304
304
  issues,
@@ -12,6 +12,7 @@ import { dirname, join, relative } from 'node:path';
12
12
  import { GeneralError } from '../errors/base.js';
13
13
  import { pathExists, readText, writeText } from '../utils/fs.js';
14
14
  import { toRootRelativePath } from '../utils/paths.js';
15
+ import { normalizePathSlashes } from '../utils/paths.js';
15
16
  import { escapeRegex } from '../utils/regex.js';
16
17
  import { tokenizeXhtml } from './wire-utils.js';
17
18
  export const DEFAULT_DOM_TARGET = 'browser/base/content/browser.xhtml';
@@ -22,23 +23,11 @@ export function addDomFragmentTokenized(content, includeDirective) {
22
23
  const lines = content.split('\n');
23
24
  const tokens = tokenizeXhtml(lines);
24
25
  // Find the #include browser-sets.inc token
25
- let insertIndex = -1;
26
- for (let i = 0; i < tokens.length; i++) {
27
- const token = tokens[i];
28
- if (token && token.type === 'macro' && token.raw.includes('browser-sets.inc')) {
29
- insertIndex = i;
30
- break;
31
- }
32
- }
26
+ let insertIndex = tokens.findIndex((token) => token.type === 'macro' && token.raw.includes('browser-sets.inc'));
33
27
  if (insertIndex === -1) {
34
28
  // Fallback: after <html:body>
35
- for (let i = 0; i < tokens.length; i++) {
36
- const token = tokens[i];
37
- if (token && token.type === 'xml' && /<html:body/.test(token.raw)) {
38
- insertIndex = i + 1;
39
- break;
40
- }
41
- }
29
+ const bodyIndex = tokens.findIndex((token) => token.type === 'xml' && /<html:body/.test(token.raw));
30
+ insertIndex = bodyIndex === -1 ? -1 : bodyIndex + 1;
42
31
  }
43
32
  if (insertIndex === -1) {
44
33
  throw new GeneralError('Could not find insertion point in chrome document');
@@ -69,7 +58,7 @@ export async function probeDomFragmentInsertionPoint(engineDir, domFilePath, tar
69
58
  }
70
59
  const safeDomFilePath = toRootRelativePath(engineDir, domFilePath);
71
60
  const targetDir = dirname(targetPath);
72
- const includePath = relative(targetDir, safeDomFilePath).replace(/\\/g, '/');
61
+ const includePath = normalizePathSlashes(relative(targetDir, safeDomFilePath));
73
62
  const includeDirective = `#include ${includePath}`;
74
63
  const content = await readText(targetAbsPath);
75
64
  if (new RegExp(`^${escapeRegex(includeDirective)}$`, 'm').test(content)) {
@@ -109,7 +98,7 @@ export async function addDomFragment(engineDir, domFilePath, targetPath = DEFAUL
109
98
  // contains it, so this must track the chrome doc's location, not a
110
99
  // hardcoded `browser/base/content/`.
111
100
  const targetDir = dirname(targetPath);
112
- const includePath = relative(targetDir, safeDomFilePath).replace(/\\/g, '/');
101
+ const includePath = normalizePathSlashes(relative(targetDir, safeDomFilePath));
113
102
  const includeDirective = `#include ${includePath}`;
114
103
  let content = await readText(targetAbsPath);
115
104
  // Idempotency: check if the #include directive already exists (line-anchored to avoid substring matches)
@@ -160,7 +160,7 @@ export function tokenizeXhtml(lines) {
160
160
  }
161
161
  // ---------------------------------------------------------------------------
162
162
  // Legacy (line-based) helpers — shared by fallback implementations in
163
- // wire-targets.ts. Extracted to reduce duplication and make the brace-
163
+ // the wire target modules. Extracted to reduce duplication and make the brace-
164
164
  // walking logic independently testable.
165
165
  // ---------------------------------------------------------------------------
166
166
  /**
@@ -179,24 +179,12 @@ export function tokenizeXhtml(lines) {
179
179
  * found (or, under `requireBrace`, no brace follows the signature).
180
180
  */
181
181
  export function findMethodBraceIndex(lines, pattern, options) {
182
- let methodLine = -1;
183
- for (let i = 0; i < lines.length; i++) {
184
- if (pattern.test(lines[i] ?? '')) {
185
- methodLine = i;
186
- break;
187
- }
188
- }
182
+ const methodLine = lines.findIndex((line) => pattern.test(line));
189
183
  if (methodLine === -1)
190
184
  return null;
191
- let braceIndex = methodLine;
192
- let braceFound = false;
193
- for (let i = methodLine; i < lines.length; i++) {
194
- if (lines[i]?.includes('{')) {
195
- braceIndex = i;
196
- braceFound = true;
197
- break;
198
- }
199
- }
185
+ const braceOffset = lines.slice(methodLine).findIndex((line) => line.includes('{'));
186
+ const braceFound = braceOffset !== -1;
187
+ const braceIndex = braceFound ? methodLine + braceOffset : methodLine;
200
188
  if (!braceFound && options?.requireBrace) {
201
189
  return null;
202
190
  }
@@ -64,8 +64,7 @@ export function parseAppdirFromToml(tomlText, key) {
64
64
  // comment further along the file is not mistaken for the directive.
65
65
  const escapedKey = escapeRegex(key);
66
66
  const keyPattern = new RegExp('^\\s*' + escapedKey + '\\s*=\\s*(.+?)\\s*(?:[#;].*)?$');
67
- for (let i = 0; i < lines.length; i += 1) {
68
- const line = lines[i] ?? '';
67
+ for (const [i, line] of lines.entries()) {
69
68
  const sectionMatch = /^\s*\[([^\]]+)\]\s*$/.exec(line);
70
69
  if (sectionMatch) {
71
70
  sectionSeen = true;
@@ -290,8 +289,7 @@ export async function resolveXpcshellAppdirArg(engineDir, testPaths, objDirName)
290
289
  * wins.
291
290
  */
292
291
  export function operatorAlreadySetAppPath(extraArgs) {
293
- for (let i = 0; i < extraArgs.length; i += 1) {
294
- const arg = extraArgs[i] ?? '';
292
+ for (const [i, arg] of extraArgs.entries()) {
295
293
  if (arg === '--app-path' && i + 1 < extraArgs.length)
296
294
  return true;
297
295
  if (arg.startsWith('--app-path='))
@@ -19,12 +19,45 @@ export declare abstract class FireForgeError extends Error {
19
19
  */
20
20
  get userMessage(): string;
21
21
  }
22
+ /**
23
+ * Renders the trailing "To fix this:" block shared by every FireForge error
24
+ * that offers remedies.
25
+ *
26
+ * Numbering is derived from the array rather than written into each string.
27
+ * Several classes vary their advice by context (`FurnaceError` drops the
28
+ * validate step, `ChecksumMismatchError` adds a Developer Edition note), and
29
+ * hand-numbered branches had to renumber every later step by hand — one
30
+ * conditional away from printing "2." twice.
31
+ *
32
+ * @param steps - The remedy lines, in order, without their numbers
33
+ * @returns The block, starting with the blank-line separator
34
+ */
35
+ export declare function remedies(steps: readonly string[]): string;
22
36
  /**
23
37
  * General error for unexpected failures.
24
38
  */
25
39
  export declare class GeneralError extends FireForgeError {
26
40
  readonly code: 1;
27
41
  }
42
+ /**
43
+ * A `fireforge test` preflight gate that refused before any harness ran.
44
+ *
45
+ * Exists to carry a `note`, not to change behaviour: it keeps
46
+ * {@link ExitCode.GENERAL_ERROR}, so `docs/exit-codes.md` and every CI
47
+ * consumer keying on the exit status are untouched. The note is a stable
48
+ * kebab-case refusal class that the `FIREFORGE-VERDICT:` line reports as an
49
+ * additive `note=` key. Without it every preflight refusal reached an
50
+ * unattended reader as the single token `reason=preflight`, which covers
51
+ * everything from a missing build to a peer's packaging record — so the one
52
+ * line that survives a truncating pipe could not say WHICH gate fired.
53
+ */
54
+ export declare class PreflightRefusalError extends GeneralError {
55
+ /** Stable refusal class, e.g. `stale-browser`. */
56
+ readonly note: string;
57
+ constructor(message: string,
58
+ /** Stable refusal class, e.g. `stale-browser`. */
59
+ note: string);
60
+ }
28
61
  /**
29
62
  * Raised when one of FireForge's own internal invariants did not hold —
30
63
  * the failure of a runtime assertion from `src/utils/assert.ts`.
@@ -92,14 +125,33 @@ export declare class ExecTimeoutError extends FireForgeError {
92
125
  /** Timeout that elapsed, in milliseconds. */
93
126
  readonly timeoutMs: number;
94
127
  readonly code: 1;
128
+ /** Standard output captured before the deadline fired (empty when the exec path does not capture). */
129
+ readonly stdout: string;
130
+ /** Standard error captured before the deadline fired (empty when the exec path does not capture). */
131
+ readonly stderr: string;
132
+ /** True when either captured stream was cut off at the exec layer's collector cap. */
133
+ readonly outputTruncated: boolean;
95
134
  constructor(
96
135
  /** Executable that was spawned (argv[0]). */
97
136
  command: string,
98
137
  /** Arguments the executable was spawned with. */
99
138
  args: readonly string[],
100
139
  /** Timeout that elapsed, in milliseconds. */
101
- timeoutMs: number, cause?: unknown);
140
+ timeoutMs: number, cause?: unknown, output?: ExecTimeoutOutput);
102
141
  get userMessage(): string;
142
+ /**
143
+ * The last few lines the command printed before it was killed, so an
144
+ * operator can see what it was doing (a `git add -A` stuck on one path,
145
+ * a build step that hung) instead of a bare budget number. Prefers
146
+ * stderr; falls back to stdout when stderr is silent.
147
+ */
148
+ private outputTailForUser;
149
+ }
150
+ /** Output captured before an exec timeout fired; see {@link ExecTimeoutError}. */
151
+ export interface ExecTimeoutOutput {
152
+ stdout: string;
153
+ stderr: string;
154
+ truncated: boolean;
103
155
  }
104
156
  /**
105
157
  * Error thrown when a file-lock wait times out because another process holds
@@ -17,10 +17,11 @@ export class FireForgeError extends Error {
17
17
  if (cause !== undefined) {
18
18
  this.cause = cause;
19
19
  }
20
- // Maintains proper stack trace in V8 environments
21
- if (typeof Error.captureStackTrace === 'function') {
22
- Error.captureStackTrace(this, this.constructor);
23
- }
20
+ // Trims the constructor frames off the stack. `engines` pins Node >= 22,
21
+ // so the V8-only `captureStackTrace` is always present — the former
22
+ // `typeof === 'function'` guard was an unreachable branch that coverage
23
+ // could never close.
24
+ Error.captureStackTrace(this, this.constructor);
24
25
  }
25
26
  /**
26
27
  * User-friendly error message with context and suggested fixes.
@@ -30,12 +31,49 @@ export class FireForgeError extends Error {
30
31
  return this.message;
31
32
  }
32
33
  }
34
+ /**
35
+ * Renders the trailing "To fix this:" block shared by every FireForge error
36
+ * that offers remedies.
37
+ *
38
+ * Numbering is derived from the array rather than written into each string.
39
+ * Several classes vary their advice by context (`FurnaceError` drops the
40
+ * validate step, `ChecksumMismatchError` adds a Developer Edition note), and
41
+ * hand-numbered branches had to renumber every later step by hand — one
42
+ * conditional away from printing "2." twice.
43
+ *
44
+ * @param steps - The remedy lines, in order, without their numbers
45
+ * @returns The block, starting with the blank-line separator
46
+ */
47
+ export function remedies(steps) {
48
+ return `\n\nTo fix this:\n${steps.map((step, index) => ` ${index + 1}. ${step}`).join('\n')}`;
49
+ }
33
50
  /**
34
51
  * General error for unexpected failures.
35
52
  */
36
53
  export class GeneralError extends FireForgeError {
37
54
  code = ExitCode.GENERAL_ERROR;
38
55
  }
56
+ /**
57
+ * A `fireforge test` preflight gate that refused before any harness ran.
58
+ *
59
+ * Exists to carry a `note`, not to change behaviour: it keeps
60
+ * {@link ExitCode.GENERAL_ERROR}, so `docs/exit-codes.md` and every CI
61
+ * consumer keying on the exit status are untouched. The note is a stable
62
+ * kebab-case refusal class that the `FIREFORGE-VERDICT:` line reports as an
63
+ * additive `note=` key. Without it every preflight refusal reached an
64
+ * unattended reader as the single token `reason=preflight`, which covers
65
+ * everything from a missing build to a peer's packaging record — so the one
66
+ * line that survives a truncating pipe could not say WHICH gate fired.
67
+ */
68
+ export class PreflightRefusalError extends GeneralError {
69
+ note;
70
+ constructor(message,
71
+ /** Stable refusal class, e.g. `stale-browser`. */
72
+ note) {
73
+ super(message);
74
+ this.note = note;
75
+ }
76
+ }
39
77
  /**
40
78
  * Raised when one of FireForge's own internal invariants did not hold —
41
79
  * the failure of a runtime assertion from `src/utils/assert.ts`.
@@ -102,11 +140,9 @@ export class InvalidArgumentError extends FireForgeError {
102
140
  this.argument = argument;
103
141
  }
104
142
  get userMessage() {
105
- let msg = `Invalid Argument: ${this.message}`;
106
- if (this.argument) {
107
- msg += `\n\nArgument: ${this.argument}`;
108
- }
109
- return msg;
143
+ return this.argument
144
+ ? `Invalid Argument: ${this.message}\n\nArgument: ${this.argument}`
145
+ : `Invalid Argument: ${this.message}`;
110
146
  }
111
147
  }
112
148
  /**
@@ -123,24 +159,56 @@ export class ExecTimeoutError extends FireForgeError {
123
159
  args;
124
160
  timeoutMs;
125
161
  code = ExitCode.GENERAL_ERROR;
162
+ /** Standard output captured before the deadline fired (empty when the exec path does not capture). */
163
+ stdout;
164
+ /** Standard error captured before the deadline fired (empty when the exec path does not capture). */
165
+ stderr;
166
+ /** True when either captured stream was cut off at the exec layer's collector cap. */
167
+ outputTruncated;
126
168
  constructor(
127
169
  /** Executable that was spawned (argv[0]). */
128
170
  command,
129
171
  /** Arguments the executable was spawned with. */
130
172
  args,
131
173
  /** Timeout that elapsed, in milliseconds. */
132
- timeoutMs, cause) {
174
+ timeoutMs, cause, output) {
133
175
  super(`Command timed out after ${Math.round(timeoutMs / 1000)}s: ${[command, ...args].join(' ')}`, cause);
134
176
  this.command = command;
135
177
  this.args = args;
136
178
  this.timeoutMs = timeoutMs;
179
+ this.stdout = output?.stdout ?? '';
180
+ this.stderr = output?.stderr ?? '';
181
+ this.outputTruncated = output?.truncated ?? false;
137
182
  }
138
183
  get userMessage() {
139
184
  return (`${this.message}\n\n` +
140
185
  'The command was killed because it exceeded its time budget, not because it failed on its own. ' +
141
- 'If the host is just slow or loaded, re-running may succeed.');
186
+ 'If the host is just slow or loaded, re-running may succeed.' +
187
+ this.outputTailForUser());
188
+ }
189
+ /**
190
+ * The last few lines the command printed before it was killed, so an
191
+ * operator can see what it was doing (a `git add -A` stuck on one path,
192
+ * a build step that hung) instead of a bare budget number. Prefers
193
+ * stderr; falls back to stdout when stderr is silent.
194
+ */
195
+ outputTailForUser() {
196
+ const source = this.stderr.trim().length > 0 ? this.stderr : this.stdout;
197
+ const lines = source
198
+ .trimEnd()
199
+ .split(/\r?\n/)
200
+ .filter((line) => line.length > 0);
201
+ if (lines.length === 0)
202
+ return '';
203
+ const tail = lines.slice(-EXEC_TIMEOUT_TAIL_LINES);
204
+ const label = this.stderr.trim().length > 0 ? 'stderr' : 'stdout';
205
+ const omitted = lines.length > tail.length ? ` (last ${tail.length} of ${lines.length} lines)` : '';
206
+ const truncatedNote = this.outputTruncated ? ' [captured output was truncated]' : '';
207
+ return `\n\nOutput before the timeout (${label}${omitted})${truncatedNote}:\n${tail.map((line) => ` ${line}`).join('\n')}`;
142
208
  }
143
209
  }
210
+ /** How many trailing output lines {@link ExecTimeoutError.userMessage} shows. */
211
+ const EXEC_TIMEOUT_TAIL_LINES = 20;
144
212
  /**
145
213
  * Error thrown when a file-lock wait times out because another process holds
146
214
  * the lock.
@@ -9,6 +9,34 @@ export declare class BuildError extends FireForgeError {
9
9
  constructor(message: string, command?: string | undefined, cause?: Error);
10
10
  get userMessage(): string;
11
11
  }
12
+ /**
13
+ * A test SUITE went red — assertions failed, or sharded runs did not all
14
+ * pass. Distinct from its {@link BuildError} parent only in what it TELLS
15
+ * the operator, deliberately not in its exit code: `docs/exit-codes.md`
16
+ * documents 5 as "a suite that FAILED", and consumers key CI on it.
17
+ *
18
+ * The parent's remedies are all build remedies — "check the build output",
19
+ * "fireforge bootstrap", "delete obj-* directories" — and none of them apply
20
+ * to a failing assertion. Worse, the first points at the wrong half of the
21
+ * log, which is exactly the half a `tail` keeps. Ground truth for a test red
22
+ * is the harness's own output, so the remedies here name the failing tests,
23
+ * the `FIREFORGE-VERDICT:` line, and the run log that survives a pipe.
24
+ *
25
+ * Note that `machineErrorCode` derives the `--json` error code from the class
26
+ * name, so this reports as `test-failure` rather than `build`.
27
+ */
28
+ export declare class TestFailureError extends BuildError {
29
+ /** Rendered TEST-UNEXPECTED blocks, when the verdict carried any. */
30
+ readonly failureBlocks?: string | undefined;
31
+ /** The run's complete raw log, when one was opened. */
32
+ readonly logPath?: string | undefined;
33
+ constructor(message: string, command?: string,
34
+ /** Rendered TEST-UNEXPECTED blocks, when the verdict carried any. */
35
+ failureBlocks?: string | undefined,
36
+ /** The run's complete raw log, when one was opened. */
37
+ logPath?: string | undefined);
38
+ get userMessage(): string;
39
+ }
12
40
  /**
13
41
  * Error thrown when mach is not available.
14
42
  */