@hominis/fireforge 0.45.0 → 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 (359) hide show
  1. package/CHANGELOG.md +80 -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.d.ts +2 -0
  18. package/dist/src/commands/doctor.js +7 -8
  19. package/dist/src/commands/download.js +35 -24
  20. package/dist/src/commands/export-all.js +21 -4
  21. package/dist/src/commands/export-flow.js +15 -45
  22. package/dist/src/commands/export-placement-conflicts.js +3 -3
  23. package/dist/src/commands/export-placement-gate.js +2 -8
  24. package/dist/src/commands/export-placement-policy.js +3 -5
  25. package/dist/src/commands/export-shared.d.ts +42 -23
  26. package/dist/src/commands/export-shared.js +4 -19
  27. package/dist/src/commands/export.js +10 -1
  28. package/dist/src/commands/furnace/authoring-config.d.ts +13 -0
  29. package/dist/src/commands/furnace/authoring-config.js +19 -0
  30. package/dist/src/commands/furnace/chrome-doc-remove.js +1 -1
  31. package/dist/src/commands/furnace/create-browser-test.js +3 -8
  32. package/dist/src/commands/furnace/create-dry-run.js +2 -5
  33. package/dist/src/commands/furnace/create-templates.d.ts +17 -1
  34. package/dist/src/commands/furnace/create-templates.js +2 -1
  35. package/dist/src/commands/furnace/create.js +25 -19
  36. package/dist/src/commands/furnace/deploy.js +11 -2
  37. package/dist/src/commands/furnace/init.js +2 -1
  38. package/dist/src/commands/furnace/override.js +28 -19
  39. package/dist/src/commands/furnace/refresh.js +9 -1
  40. package/dist/src/commands/furnace/remove-mutations.d.ts +6 -7
  41. package/dist/src/commands/furnace/remove-mutations.js +0 -1
  42. package/dist/src/commands/furnace/remove.js +2 -6
  43. package/dist/src/commands/furnace/rename-browser-test.js +1 -7
  44. package/dist/src/commands/furnace/rename.js +29 -29
  45. package/dist/src/commands/furnace/status.js +12 -9
  46. package/dist/src/commands/furnace/test-file-name.d.ts +28 -0
  47. package/dist/src/commands/furnace/test-file-name.js +39 -0
  48. package/dist/src/commands/furnace/validation-output.d.ts +21 -9
  49. package/dist/src/commands/furnace/validation-output.js +3 -8
  50. package/dist/src/commands/import.js +2 -9
  51. package/dist/src/commands/lint-per-patch.d.ts +2 -2
  52. package/dist/src/commands/lint-per-patch.js +19 -4
  53. package/dist/src/commands/lint-per-run-checkjs.d.ts +2 -2
  54. package/dist/src/commands/lint.d.ts +2 -1
  55. package/dist/src/commands/lint.js +1 -1
  56. package/dist/src/commands/manifest.js +1 -1
  57. package/dist/src/commands/package.js +2 -4
  58. package/dist/src/commands/patch/compact.js +14 -15
  59. package/dist/src/commands/patch/delete.js +15 -25
  60. package/dist/src/commands/patch/entry-projection.d.ts +13 -0
  61. package/dist/src/commands/patch/entry-projection.js +24 -0
  62. package/dist/src/commands/patch/lint-ignore.d.ts +0 -8
  63. package/dist/src/commands/patch/lint-ignore.js +19 -29
  64. package/dist/src/commands/patch/move-files-create.js +24 -10
  65. package/dist/src/commands/patch/move-files-into.js +38 -34
  66. package/dist/src/commands/patch/move-files.js +4 -13
  67. package/dist/src/commands/patch/rename.js +27 -41
  68. package/dist/src/commands/patch/reorder.js +25 -38
  69. package/dist/src/commands/patch/split-plan.d.ts +4 -5
  70. package/dist/src/commands/patch/split-plan.js +4 -8
  71. package/dist/src/commands/patch/split.js +45 -43
  72. package/dist/src/commands/patch/staged-dependency.js +16 -22
  73. package/dist/src/commands/patch/tier.js +15 -25
  74. package/dist/src/commands/patch-rename-map.d.ts +19 -0
  75. package/dist/src/commands/patch-rename-map.js +36 -0
  76. package/dist/src/commands/pending-resolution.d.ts +15 -0
  77. package/dist/src/commands/pending-resolution.js +25 -0
  78. package/dist/src/commands/re-export-adjacent.d.ts +24 -2
  79. package/dist/src/commands/re-export-adjacent.js +32 -16
  80. package/dist/src/commands/re-export-bulk-scan.js +6 -10
  81. package/dist/src/commands/re-export-drift.js +4 -4
  82. package/dist/src/commands/re-export-files.d.ts +2 -3
  83. package/dist/src/commands/re-export-files.js +38 -29
  84. package/dist/src/commands/re-export-lint.d.ts +3 -4
  85. package/dist/src/commands/re-export-lint.js +9 -2
  86. package/dist/src/commands/re-export-scan.js +5 -2
  87. package/dist/src/commands/re-export-single.d.ts +22 -3
  88. package/dist/src/commands/re-export-single.js +19 -11
  89. package/dist/src/commands/re-export.js +28 -11
  90. package/dist/src/commands/rebase/abort.js +3 -8
  91. package/dist/src/commands/rebase/continue.js +12 -12
  92. package/dist/src/commands/rebase/index.js +2 -2
  93. package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
  94. package/dist/src/commands/rebase/patch-loop.js +2 -7
  95. package/dist/src/commands/resolve.js +18 -10
  96. package/dist/src/commands/run.d.ts +0 -13
  97. package/dist/src/commands/run.js +12 -25
  98. package/dist/src/commands/setup-support.d.ts +16 -4
  99. package/dist/src/commands/setup-support.js +16 -16
  100. package/dist/src/commands/setup.js +3 -9
  101. package/dist/src/commands/source.js +8 -1
  102. package/dist/src/commands/status-check.js +2 -2
  103. package/dist/src/commands/status-output.js +2 -2
  104. package/dist/src/commands/status.js +3 -4
  105. package/dist/src/commands/test-build-phase.d.ts +2 -2
  106. package/dist/src/commands/test-build-phase.js +31 -7
  107. package/dist/src/commands/test-modes.d.ts +2 -2
  108. package/dist/src/commands/test-modes.js +0 -1
  109. package/dist/src/commands/test-register.js +5 -3
  110. package/dist/src/commands/test-run.js +25 -18
  111. package/dist/src/commands/test-stale-gate.js +4 -4
  112. package/dist/src/commands/test-verdict.d.ts +16 -2
  113. package/dist/src/commands/test-verdict.js +18 -3
  114. package/dist/src/commands/test.js +42 -5
  115. package/dist/src/commands/token-list.d.ts +26 -0
  116. package/dist/src/commands/token-list.js +153 -0
  117. package/dist/src/commands/token.js +17 -1
  118. package/dist/src/commands/tree.js +7 -2
  119. package/dist/src/commands/typecheck.js +9 -9
  120. package/dist/src/commands/wire.js +4 -3
  121. package/dist/src/core/branding.d.ts +0 -7
  122. package/dist/src/core/branding.js +3 -2
  123. package/dist/src/core/browser-wire.js +6 -3
  124. package/dist/src/core/build-audit-platform.d.ts +29 -0
  125. package/dist/src/core/build-audit-platform.js +140 -0
  126. package/dist/src/core/build-audit-registration.js +3 -2
  127. package/dist/src/core/build-audit-skip.d.ts +57 -0
  128. package/dist/src/core/build-audit-skip.js +112 -0
  129. package/dist/src/core/build-audit.d.ts +15 -2
  130. package/dist/src/core/build-audit.js +110 -10
  131. package/dist/src/core/build-baseline.d.ts +45 -28
  132. package/dist/src/core/build-baseline.js +6 -29
  133. package/dist/src/core/build-jar-escalation.js +3 -2
  134. package/dist/src/core/build-overwrite-guard.js +1 -1
  135. package/dist/src/core/build-prepare.d.ts +0 -8
  136. package/dist/src/core/build-prepare.js +7 -12
  137. package/dist/src/core/config-paths.d.ts +2 -6
  138. package/dist/src/core/config-paths.js +4 -2
  139. package/dist/src/core/config-state.d.ts +0 -6
  140. package/dist/src/core/config-state.js +1 -19
  141. package/dist/src/core/config-validate-build-audit.d.ts +8 -0
  142. package/dist/src/core/config-validate-build-audit.js +80 -0
  143. package/dist/src/core/config-validate-file-size.js +1 -1
  144. package/dist/src/core/config-validate-patch-policy.d.ts +2 -2
  145. package/dist/src/core/config-validate-patch-policy.js +3 -3
  146. package/dist/src/core/config-validate-test-toolchains.d.ts +2 -2
  147. package/dist/src/core/config-validate-test-toolchains.js +4 -5
  148. package/dist/src/core/config-validate.js +14 -3
  149. package/dist/src/core/config.d.ts +2 -2
  150. package/dist/src/core/config.js +11 -15
  151. package/dist/src/core/coverage-extend.js +4 -4
  152. package/dist/src/core/discard-baseline.js +7 -7
  153. package/dist/src/core/engine-precondition.d.ts +1 -9
  154. package/dist/src/core/engine-precondition.js +3 -8
  155. package/dist/src/core/engine-session-lock.js +13 -17
  156. package/dist/src/core/export-moved-code-shape.js +2 -2
  157. package/dist/src/core/file-lock-owner.d.ts +103 -0
  158. package/dist/src/core/file-lock-owner.js +259 -0
  159. package/dist/src/core/file-lock.d.ts +2 -7
  160. package/dist/src/core/file-lock.js +40 -91
  161. package/dist/src/core/firefox-cache.d.ts +13 -1
  162. package/dist/src/core/firefox-cache.js +59 -34
  163. package/dist/src/core/firefox-download.d.ts +15 -0
  164. package/dist/src/core/firefox-download.js +37 -21
  165. package/dist/src/core/firefox-extract.d.ts +6 -5
  166. package/dist/src/core/firefox-extract.js +36 -18
  167. package/dist/src/core/firefox.d.ts +30 -32
  168. package/dist/src/core/firefox.js +5 -36
  169. package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
  170. package/dist/src/core/furnace-apply-dry-run.js +1 -7
  171. package/dist/src/core/furnace-apply-helpers.d.ts +0 -11
  172. package/dist/src/core/furnace-apply-helpers.js +8 -19
  173. package/dist/src/core/furnace-apply-overwrite-warn.js +4 -2
  174. package/dist/src/core/furnace-apply.d.ts +1 -1
  175. package/dist/src/core/furnace-apply.js +30 -10
  176. package/dist/src/core/furnace-config.d.ts +1 -1
  177. package/dist/src/core/furnace-config.js +23 -36
  178. package/dist/src/core/furnace-constants.js +3 -2
  179. package/dist/src/core/furnace-dir-entry.d.ts +20 -0
  180. package/dist/src/core/furnace-dir-entry.js +21 -0
  181. package/dist/src/core/furnace-operation.d.ts +2 -0
  182. package/dist/src/core/furnace-operation.js +3 -1
  183. package/dist/src/core/furnace-refresh.d.ts +18 -6
  184. package/dist/src/core/furnace-refresh.js +3 -6
  185. package/dist/src/core/furnace-registration-ast.d.ts +0 -1
  186. package/dist/src/core/furnace-registration-ast.js +0 -3
  187. package/dist/src/core/furnace-registration.d.ts +2 -1
  188. package/dist/src/core/furnace-registration.js +22 -29
  189. package/dist/src/core/furnace-scanner.js +3 -6
  190. package/dist/src/core/furnace-state-persist.d.ts +2 -2
  191. package/dist/src/core/furnace-validate-registration.js +5 -7
  192. package/dist/src/core/git-base.d.ts +3 -1
  193. package/dist/src/core/git-base.js +25 -3
  194. package/dist/src/core/git-diff-new-file.d.ts +40 -0
  195. package/dist/src/core/git-diff-new-file.js +119 -0
  196. package/dist/src/core/git-diff.d.ts +9 -9
  197. package/dist/src/core/git-diff.js +88 -157
  198. package/dist/src/core/git-file-ops.d.ts +0 -7
  199. package/dist/src/core/git-file-ops.js +4 -28
  200. package/dist/src/core/git-readonly-index.d.ts +0 -2
  201. package/dist/src/core/git-readonly-index.js +0 -4
  202. package/dist/src/core/git-status.d.ts +7 -0
  203. package/dist/src/core/git-status.js +21 -9
  204. package/dist/src/core/git.d.ts +0 -6
  205. package/dist/src/core/git.js +14 -43
  206. package/dist/src/core/harness-orphans.js +8 -7
  207. package/dist/src/core/history-log.d.ts +19 -0
  208. package/dist/src/core/history-log.js +29 -0
  209. package/dist/src/core/mach-error-hints.d.ts +0 -5
  210. package/dist/src/core/mach-error-hints.js +1 -1
  211. package/dist/src/core/mach-known-noise-filter.d.ts +4 -3
  212. package/dist/src/core/mach-known-noise-filter.js +7 -9
  213. package/dist/src/core/mach-mozconfig.js +2 -1
  214. package/dist/src/core/mach-python.d.ts +2 -0
  215. package/dist/src/core/mach-python.js +12 -7
  216. package/dist/src/core/mach-resource-shim.d.ts +13 -1
  217. package/dist/src/core/mach-resource-shim.js +112 -9
  218. package/dist/src/core/mach.d.ts +39 -53
  219. package/dist/src/core/mach.js +13 -81
  220. package/dist/src/core/marionette-port.d.ts +0 -17
  221. package/dist/src/core/marionette-port.js +12 -58
  222. package/dist/src/core/marionette-preflight.js +9 -27
  223. package/dist/src/core/mochitest-server-port.js +3 -3
  224. package/dist/src/core/moz-manifest-rules.js +37 -33
  225. package/dist/src/core/patch-apply-fuzz.js +4 -4
  226. package/dist/src/core/patch-apply.d.ts +4 -5
  227. package/dist/src/core/patch-apply.js +6 -12
  228. package/dist/src/core/patch-export-coverage.d.ts +0 -9
  229. package/dist/src/core/patch-export-coverage.js +1 -30
  230. package/dist/src/core/patch-export-update.d.ts +18 -1
  231. package/dist/src/core/patch-export-update.js +4 -3
  232. package/dist/src/core/patch-export.d.ts +11 -21
  233. package/dist/src/core/patch-export.js +44 -66
  234. package/dist/src/core/patch-files.d.ts +0 -4
  235. package/dist/src/core/patch-files.js +0 -11
  236. package/dist/src/core/patch-lint-binary.d.ts +6 -12
  237. package/dist/src/core/patch-lint-cache.d.ts +17 -1
  238. package/dist/src/core/patch-lint-cache.js +3 -6
  239. package/dist/src/core/patch-lint-checkjs.d.ts +33 -16
  240. package/dist/src/core/patch-lint-checkjs.js +37 -24
  241. package/dist/src/core/patch-lint-creators.d.ts +2 -6
  242. package/dist/src/core/patch-lint-cross.d.ts +16 -8
  243. package/dist/src/core/patch-lint-cross.js +49 -92
  244. package/dist/src/core/patch-lint-css.js +10 -6
  245. package/dist/src/core/patch-lint-file-size.js +3 -3
  246. package/dist/src/core/patch-lint-forward-registration.d.ts +58 -0
  247. package/dist/src/core/patch-lint-forward-registration.js +210 -0
  248. package/dist/src/core/patch-lint-jsdoc.js +36 -36
  249. package/dist/src/core/patch-lint-module-registration.d.ts +2 -8
  250. package/dist/src/core/patch-lint-prettier.js +1 -1
  251. package/dist/src/core/patch-lint-queue-types.d.ts +60 -0
  252. package/dist/src/core/patch-lint-queue-types.js +12 -0
  253. package/dist/src/core/patch-lint-staged-registration.d.ts +61 -0
  254. package/dist/src/core/patch-lint-staged-registration.js +90 -0
  255. package/dist/src/core/patch-lint.d.ts +14 -9
  256. package/dist/src/core/patch-lint.js +4 -3
  257. package/dist/src/core/patch-lock.js +3 -3
  258. package/dist/src/core/patch-manifest-io.js +8 -7
  259. package/dist/src/core/patch-parse.d.ts +19 -8
  260. package/dist/src/core/patch-parse.js +25 -0
  261. package/dist/src/core/patch-policy.js +15 -27
  262. package/dist/src/core/patch-source-metadata.d.ts +1 -3
  263. package/dist/src/core/patch-source-metadata.js +0 -4
  264. package/dist/src/core/patch-transform.d.ts +0 -8
  265. package/dist/src/core/patch-transform.js +0 -10
  266. package/dist/src/core/process-cpu-time.js +1 -1
  267. package/dist/src/core/rebase-session.d.ts +2 -8
  268. package/dist/src/core/rebase-session.js +5 -17
  269. package/dist/src/core/register-browser-content.js +3 -2
  270. package/dist/src/core/register-browser-test.js +2 -1
  271. package/dist/src/core/register-module.js +2 -1
  272. package/dist/src/core/register-scaffold.js +5 -4
  273. package/dist/src/core/register-shared-css.js +2 -1
  274. package/dist/src/core/run-log-redact.d.ts +38 -0
  275. package/dist/src/core/run-log-redact.js +69 -0
  276. package/dist/src/core/run-log.d.ts +13 -2
  277. package/dist/src/core/run-log.js +67 -18
  278. package/dist/src/core/signal-critical.js +2 -4
  279. package/dist/src/core/smoke-patterns.d.ts +0 -1
  280. package/dist/src/core/smoke-patterns.js +6 -11
  281. package/dist/src/core/state-file.d.ts +12 -1
  282. package/dist/src/core/state-file.js +14 -1
  283. package/dist/src/core/status-classify.js +2 -1
  284. package/dist/src/core/test-changed-prefs.js +26 -11
  285. package/dist/src/core/test-harness-crash.js +2 -2
  286. package/dist/src/core/test-stale-check.d.ts +0 -2
  287. package/dist/src/core/test-stale-check.js +2 -2
  288. package/dist/src/core/test-xpcshell-retry.d.ts +26 -7
  289. package/dist/src/core/test-xpcshell-retry.js +14 -11
  290. package/dist/src/core/token-category.d.ts +33 -0
  291. package/dist/src/core/token-category.js +56 -48
  292. package/dist/src/core/token-dark-mode.js +2 -14
  293. package/dist/src/core/token-inventory.d.ts +56 -0
  294. package/dist/src/core/token-inventory.js +109 -0
  295. package/dist/src/core/token-manager.d.ts +1 -1
  296. package/dist/src/core/token-manager.js +1 -1
  297. package/dist/src/core/token-variant.js +21 -22
  298. package/dist/src/core/toolchain-preflight.js +1 -1
  299. package/dist/src/core/tree-cow.d.ts +0 -2
  300. package/dist/src/core/tree-cow.js +2 -2
  301. package/dist/src/core/tree-store.js +4 -5
  302. package/dist/src/core/typecheck.js +2 -2
  303. package/dist/src/core/wire-dom-fragment.js +6 -17
  304. package/dist/src/core/wire-utils.js +5 -17
  305. package/dist/src/core/xpcshell-appdir.js +2 -4
  306. package/dist/src/errors/base.d.ts +53 -1
  307. package/dist/src/errors/base.js +79 -11
  308. package/dist/src/errors/build.js +20 -28
  309. package/dist/src/errors/config.d.ts +0 -14
  310. package/dist/src/errors/config.js +13 -34
  311. package/dist/src/errors/download.d.ts +13 -0
  312. package/dist/src/errors/download.js +53 -24
  313. package/dist/src/errors/furnace.js +8 -9
  314. package/dist/src/errors/git.d.ts +8 -8
  315. package/dist/src/errors/git.js +20 -30
  316. package/dist/src/errors/patch.js +8 -10
  317. package/dist/src/errors/rebase.js +7 -7
  318. package/dist/src/types/commands/options.d.ts +77 -123
  319. package/dist/src/types/config.d.ts +34 -2
  320. package/dist/src/types/index.d.ts +0 -1
  321. package/dist/src/utils/assert.d.ts +3 -15
  322. package/dist/src/utils/assert.js +1 -18
  323. package/dist/src/utils/build-info.d.ts +1 -1
  324. package/dist/src/utils/child-env.d.ts +2 -1
  325. package/dist/src/utils/elapsed.js +10 -1
  326. package/dist/src/utils/fs.js +8 -13
  327. package/dist/src/utils/hash.d.ts +12 -0
  328. package/dist/src/utils/hash.js +16 -0
  329. package/dist/src/utils/line-dispatch.d.ts +15 -0
  330. package/dist/src/utils/line-dispatch.js +48 -0
  331. package/dist/src/utils/options.d.ts +1 -7
  332. package/dist/src/utils/options.js +6 -7
  333. package/dist/src/utils/package-root.js +3 -12
  334. package/dist/src/utils/platform.d.ts +0 -4
  335. package/dist/src/utils/process-group.d.ts +2 -1
  336. package/dist/src/utils/process-group.js +20 -19
  337. package/dist/src/utils/process.d.ts +40 -26
  338. package/dist/src/utils/process.js +115 -166
  339. package/dist/src/utils/ps-duration.js +3 -3
  340. package/dist/src/utils/sleep.d.ts +26 -0
  341. package/dist/src/utils/sleep.js +31 -0
  342. package/dist/src/utils/validation.d.ts +2 -38
  343. package/dist/src/utils/validation.js +7 -16
  344. package/docs/configuration.md +89 -1
  345. package/docs/exit-codes.md +31 -19
  346. package/docs/lifecycle-invariants.md +18 -12
  347. package/docs/machine-output.md +31 -8
  348. package/docs/patch-workflow.md +2 -1
  349. package/docs/run-logs.md +44 -4
  350. package/docs/testing.md +8 -1
  351. package/package.json +8 -7
  352. package/dist/src/commands/rebase.d.ts +0 -4
  353. package/dist/src/commands/rebase.js +0 -5
  354. package/dist/src/core/patch-artifact-normalize.d.ts +0 -21
  355. package/dist/src/core/patch-artifact-normalize.js +0 -24
  356. package/dist/src/core/patch-lint-reexports.d.ts +0 -7
  357. package/dist/src/core/patch-lint-reexports.js +0 -8
  358. package/dist/src/core/wire-targets.d.ts +0 -7
  359. package/dist/src/core/wire-targets.js +0 -8
@@ -8,12 +8,6 @@ import type { FireForgeState } from '../types/config.js';
8
8
  * @returns FireForge state
9
9
  */
10
10
  export declare function loadState(root: string): Promise<FireForgeState>;
11
- /**
12
- * Saves the fireforge state.
13
- * @param root - Root directory of the project
14
- * @param state - State to save
15
- */
16
- export declare function saveState(root: string, state: FireForgeState): Promise<void>;
17
11
  /**
18
12
  * Updates specific fields in the fireforge state.
19
13
  * @param root - Root directory of the project
@@ -24,13 +24,7 @@ function sanitizeProjectState(data) {
24
24
  const state = {};
25
25
  const issues = [];
26
26
  const recoveredFields = [];
27
- const stringFields = [
28
- 'brand',
29
- 'buildMode',
30
- 'lastBuild',
31
- 'downloadedVersion',
32
- 'baseCommit',
33
- ];
27
+ const stringFields = ['brand', 'buildMode', 'downloadedVersion', 'baseCommit'];
34
28
  for (const key of stringFields) {
35
29
  const value = data[key];
36
30
  if (value === undefined) {
@@ -132,18 +126,6 @@ export async function loadState(root) {
132
126
  const paths = getProjectPaths(root);
133
127
  return loadStateFromPath(paths.state);
134
128
  }
135
- /**
136
- * Saves the fireforge state.
137
- * @param root - Root directory of the project
138
- * @param state - State to save
139
- */
140
- export async function saveState(root, state) {
141
- const paths = getProjectPaths(root);
142
- const validatedState = validateFireForgeState(state);
143
- await withStateFileLock(paths.state, async () => {
144
- await writeJson(paths.state, validatedState);
145
- });
146
- }
147
129
  /**
148
130
  * Updates specific fields in the fireforge state.
149
131
  * @param root - Root directory of the project
@@ -0,0 +1,8 @@
1
+ import type { BuildAuditConfig } from '../types/config.js';
2
+ /**
3
+ * Parses the `buildAudit` block.
4
+ *
5
+ * @param raw - The raw `buildAudit` value from fireforge.json
6
+ * @returns The validated block
7
+ */
8
+ export declare function parseBuildAuditConfig(raw: unknown): BuildAuditConfig;
@@ -0,0 +1,80 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Validation for the `buildAudit` block.
4
+ *
5
+ * Split out of `config-validate.ts`, which is at its per-file line budget,
6
+ * following the `config-validate-file-size.ts` precedent.
7
+ *
8
+ * The rules here are stricter than the schema strictly needs, and
9
+ * deliberately so: every entry is a standing suppression of a warning the
10
+ * audit would otherwise raise. A malformed one must fail loudly at config
11
+ * load rather than quietly admit nothing (leaving the operator believing a
12
+ * path is carved out) or quietly admit everything.
13
+ */
14
+ import { ConfigError } from '../errors/config.js';
15
+ import { isContainedRelativePath } from '../utils/paths.js';
16
+ import { isObject } from '../utils/validation.js';
17
+ /** Parses one `buildAudit.unpackaged[]` entry. */
18
+ function parseUnpackagedEntry(raw, field) {
19
+ if (!isObject(raw)) {
20
+ throw new ConfigError(`Config field "${field}" must be a plain object with "path" and "reason"`);
21
+ }
22
+ for (const key of Object.keys(raw)) {
23
+ if (key !== 'path' && key !== 'reason') {
24
+ throw new ConfigError(`Config field "${field}" has unknown key "${key}"`);
25
+ }
26
+ }
27
+ const path = raw['path'];
28
+ if (typeof path !== 'string' || path.trim().length === 0) {
29
+ throw new ConfigError(`Config field "${field}.path" must be a non-empty string`);
30
+ }
31
+ if (!isContainedRelativePath(path)) {
32
+ throw new ConfigError(`Config field "${field}.path" must be an engine-relative path`);
33
+ }
34
+ if (path.includes('**')) {
35
+ // A `**` carve-out admits an entire subtree, which is how a reviewed
36
+ // exception quietly becomes a blanket one. `*` inside a single segment
37
+ // is enough for the shapes this exists for.
38
+ throw new ConfigError(`Config field "${field}.path" must not use "**" — a carve-out may glob within one path segment only`);
39
+ }
40
+ const reason = raw['reason'];
41
+ if (typeof reason !== 'string' || reason.trim().length === 0) {
42
+ // Required on purpose: this is the one audit class FireForge cannot
43
+ // derive from the tree, so the declaration IS the evidence.
44
+ throw new ConfigError(`Config field "${field}.reason" must be a non-empty string explaining why the file is never packaged`);
45
+ }
46
+ return { path, reason };
47
+ }
48
+ /**
49
+ * Parses the `buildAudit` block.
50
+ *
51
+ * @param raw - The raw `buildAudit` value from fireforge.json
52
+ * @returns The validated block
53
+ */
54
+ export function parseBuildAuditConfig(raw) {
55
+ if (!isObject(raw)) {
56
+ throw new ConfigError('Config field "buildAudit" must be a plain object');
57
+ }
58
+ for (const key of Object.keys(raw)) {
59
+ if (key !== 'unpackaged') {
60
+ throw new ConfigError(`Config field "buildAudit" has unknown key "${key}"`);
61
+ }
62
+ }
63
+ const unpackaged = raw['unpackaged'];
64
+ if (unpackaged === undefined)
65
+ return {};
66
+ if (!Array.isArray(unpackaged)) {
67
+ throw new ConfigError('Config field "buildAudit.unpackaged" must be an array');
68
+ }
69
+ const entries = unpackaged.map((entry, index) => parseUnpackagedEntry(entry, `buildAudit.unpackaged[${String(index)}]`));
70
+ const seen = new Set();
71
+ for (const entry of entries) {
72
+ if (seen.has(entry.path)) {
73
+ // Two rows for one path means one of the two reasons is not the
74
+ // operative one, and nothing says which.
75
+ throw new ConfigError(`Config field "buildAudit.unpackaged" declares "${entry.path}" more than once`);
76
+ }
77
+ seen.add(entry.path);
78
+ }
79
+ return { unpackaged: entries };
80
+ }
@@ -52,7 +52,7 @@ export function parsePatchLintFileSizeThresholds(raw) {
52
52
  const merged = { ...DEFAULT_FILE_SIZE_THRESHOLDS[tier], ...parsed };
53
53
  if (merged.notice > merged.warning || merged.warning > merged.error) {
54
54
  throw new ConfigError(`Config field "patchLint.fileSizeThresholds.${tier}" must satisfy notice <= warning <= error ` +
55
- `(resolved: ${String(merged.notice)}/${String(merged.warning)}/${String(merged.error)})`);
55
+ `(resolved: ${merged.notice}/${merged.warning}/${merged.error})`);
56
56
  }
57
57
  out[tier] = parsed;
58
58
  }
@@ -2,6 +2,6 @@
2
2
  * Validation helpers for `fireforge.json#patchPolicy`.
3
3
  */
4
4
  import type { PatchPolicyConfig } from '../types/config.js';
5
- import { parseObject } from '../utils/parse.js';
5
+ import { type ParsedRecord } from '../utils/parse.js';
6
6
  /** Parses and validates the optional patch policy config block. */
7
- export declare function parsePatchPolicyBlock(rec: ReturnType<typeof parseObject>): PatchPolicyConfig;
7
+ export declare function parsePatchPolicyBlock(rec: ParsedRecord): PatchPolicyConfig;
@@ -108,7 +108,7 @@ function parsePatchPolicyReservedRange(raw, label) {
108
108
  return {
109
109
  from,
110
110
  to,
111
- allowed: allowedRaw.map((entry, index) => parseReservedAllowedPatch(entry, `${label}.allowed[${String(index)}]`)),
111
+ allowed: allowedRaw.map((entry, index) => parseReservedAllowedPatch(entry, `${label}.allowed[${index}]`)),
112
112
  };
113
113
  }
114
114
  function assertPolicyRangesDoNotOverlap(ranges, label) {
@@ -160,14 +160,14 @@ export function parsePatchPolicyBlock(rec) {
160
160
  if (!Array.isArray(rangesRaw) || rangesRaw.length === 0) {
161
161
  throw new ConfigError('Config field "patchPolicy.ranges" must be a non-empty array');
162
162
  }
163
- out.ranges = rangesRaw.map((entry, index) => parsePatchPolicyRange(entry, `patchPolicy.ranges[${String(index)}]`));
163
+ out.ranges = rangesRaw.map((entry, index) => parsePatchPolicyRange(entry, `patchPolicy.ranges[${index}]`));
164
164
  assertPolicyRangesDoNotOverlap(out.ranges, 'patchPolicy.ranges');
165
165
  const reservedRangesRaw = rec.raw('reservedRanges');
166
166
  if (reservedRangesRaw !== undefined) {
167
167
  if (!Array.isArray(reservedRangesRaw)) {
168
168
  throw new ConfigError('Config field "patchPolicy.reservedRanges" must be an array');
169
169
  }
170
- out.reservedRanges = reservedRangesRaw.map((entry, index) => parsePatchPolicyReservedRange(entry, `patchPolicy.reservedRanges[${String(index)}]`));
170
+ out.reservedRanges = reservedRangesRaw.map((entry, index) => parsePatchPolicyReservedRange(entry, `patchPolicy.reservedRanges[${index}]`));
171
171
  assertPolicyRangesDoNotOverlap(out.reservedRanges, 'patchPolicy.reservedRanges');
172
172
  }
173
173
  return out;
@@ -1,6 +1,6 @@
1
1
  import type { FireForgeConfig } from '../types/config.js';
2
- import { parseObject } from '../utils/parse.js';
3
- type ConfigRecord = ReturnType<typeof parseObject>;
2
+ import type { ParsedRecord } from '../utils/parse.js';
3
+ type ConfigRecord = ParsedRecord;
4
4
  /** Parses the optional `test` block. */
5
5
  export declare function parseTestBlock(rec: ConfigRecord, config: FireForgeConfig): void;
6
6
  /** Parses optional project-specific external toolchain doctor probes. */
@@ -1,6 +1,5 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { ConfigError } from '../errors/config.js';
3
- import { parseObject } from '../utils/parse.js';
4
3
  import { isContainedRelativePath } from '../utils/paths.js';
5
4
  import { isObject } from '../utils/validation.js';
6
5
  function optionalConfigString(rec, key, label) {
@@ -58,16 +57,16 @@ export function parseExternalToolchainsBlock(rec, config) {
58
57
  }
59
58
  config.externalToolchains = raw.map((entry, index) => {
60
59
  if (!isObject(entry)) {
61
- throw new ConfigError(`Config field "externalToolchains[${String(index)}]" must be an object`);
60
+ throw new ConfigError(`Config field "externalToolchains[${index}]" must be an object`);
62
61
  }
63
62
  const item = entry;
64
63
  const name = item['name'];
65
64
  if (typeof name !== 'string' || name.trim() === '') {
66
- throw new ConfigError(`Config field "externalToolchains[${String(index)}].name" must be a non-empty string`);
65
+ throw new ConfigError(`Config field "externalToolchains[${index}].name" must be a non-empty string`);
67
66
  }
68
67
  const tools = item['tools'];
69
68
  if (!Array.isArray(tools) || tools.length === 0) {
70
- throw new ConfigError(`Config field "externalToolchains[${String(index)}].tools" must be a non-empty array`);
69
+ throw new ConfigError(`Config field "externalToolchains[${index}].tools" must be a non-empty array`);
71
70
  }
72
71
  return {
73
72
  name,
@@ -76,7 +75,7 @@ export function parseExternalToolchainsBlock(rec, config) {
76
75
  });
77
76
  }
78
77
  function parseExternalToolRequirement(tool, index, toolIndex) {
79
- const label = `externalToolchains[${String(index)}].tools[${String(toolIndex)}]`;
78
+ const label = `externalToolchains[${index}].tools[${toolIndex}]`;
80
79
  if (!isObject(tool)) {
81
80
  throw new ConfigError(`Config field "${label}" must be an object`);
82
81
  }
@@ -8,6 +8,7 @@ import { parseObject } from '../utils/parse.js';
8
8
  import { isContainedRelativePath, isExplicitAbsolutePath } from '../utils/paths.js';
9
9
  import { describeProductVersionIncompatibility, FIREFOX_PRODUCTS, isObject, isValidAppId, isValidFirefoxCandidate, isValidFirefoxProduct, isValidFirefoxVersion, isValidProjectLicense, PROJECT_LICENSES, } from '../utils/validation.js';
10
10
  import { SUPPORTED_CONFIG_ROOT_KEYS } from './config-paths.js';
11
+ import { parseBuildAuditConfig } from './config-validate-build-audit.js';
11
12
  import { parsePatchLintFileSizeThresholds } from './config-validate-file-size.js';
12
13
  import { parsePatchPolicyBlock } from './config-validate-patch-policy.js';
13
14
  import { parseExternalToolchainsBlock, parseTestBlock } from './config-validate-test-toolchains.js';
@@ -54,7 +55,8 @@ function parseIdentityFields(rec) {
54
55
  /**
55
56
  * Parses and validates the required `firefox` block: version shape,
56
57
  * product allowlist, product/version cross-compatibility, and the
57
- * optional sha256 digest (normalized to lowercase).
58
+ * optional sha256 digest (normalized to lowercase), optional
59
+ * `allowUnverifiedDownload` opt-out, optional candidate directory.
58
60
  */
59
61
  function parseFirefoxBlock(rec) {
60
62
  let firefoxRec;
@@ -85,6 +87,10 @@ function parseFirefoxBlock(rec) {
85
87
  if (firefoxSha256 !== undefined && !/^[a-f0-9]{64}$/i.test(firefoxSha256)) {
86
88
  throw new ConfigError('Config field "firefox.sha256" must be a 64-character SHA-256 hex digest');
87
89
  }
90
+ const allowUnverifiedDownload = firefoxRec.raw('allowUnverifiedDownload');
91
+ if (allowUnverifiedDownload !== undefined && typeof allowUnverifiedDownload !== 'boolean') {
92
+ throw new ConfigError('Config field "firefox.allowUnverifiedDownload" must be a boolean');
93
+ }
88
94
  const firefoxCandidate = optionalConfigString(firefoxRec, 'candidate', 'firefox.candidate');
89
95
  if (firefoxCandidate !== undefined && !isValidFirefoxCandidate(firefoxCandidate)) {
90
96
  throw new ConfigError('Config field "firefox.candidate" must look like "buildN" (e.g. "build2")');
@@ -93,6 +99,7 @@ function parseFirefoxBlock(rec) {
93
99
  version: firefoxVersion,
94
100
  product: firefoxProduct,
95
101
  ...(firefoxSha256 !== undefined ? { sha256: firefoxSha256.toLowerCase() } : {}),
102
+ ...(allowUnverifiedDownload !== undefined ? { allowUnverifiedDownload } : {}),
96
103
  ...(firefoxCandidate !== undefined ? { candidate: firefoxCandidate } : {}),
97
104
  };
98
105
  }
@@ -155,6 +162,10 @@ export function validateConfig(data) {
155
162
  const firefox = parseFirefoxBlock(rec);
156
163
  const config = { ...identity, firefox };
157
164
  parseBuildBlock(rec, config);
165
+ const buildAuditRaw = rec.raw('buildAudit');
166
+ if (buildAuditRaw !== undefined) {
167
+ config.buildAudit = parseBuildAuditConfig(buildAuditRaw);
168
+ }
158
169
  parseTestBlock(rec, config);
159
170
  parseExternalToolchainsBlock(rec, config);
160
171
  parseWireBlock(rec, config);
@@ -425,10 +436,10 @@ function parseTypecheckBlock(rec) {
425
436
  for (let i = 0; i < projectsRaw.length; i++) {
426
437
  const entry = projectsRaw[i];
427
438
  if (typeof entry !== 'string' || entry.trim() === '') {
428
- throw new ConfigError(`Config field "typecheck.projects[${String(i)}]" must be a non-empty string`);
439
+ throw new ConfigError(`Config field "typecheck.projects[${i}]" must be a non-empty string`);
429
440
  }
430
441
  if (!isContainedRelativePath(entry)) {
431
- throw new ConfigError(`Config field "typecheck.projects[${String(i)}]" must be a project-relative path`);
442
+ throw new ConfigError(`Config field "typecheck.projects[${i}]" must be a project-relative path`);
432
443
  }
433
444
  projects.push(entry);
434
445
  }
@@ -10,8 +10,8 @@
10
10
  import type { FireForgeConfig } from '../types/config.js';
11
11
  import type { JsonObject } from '../types/json.js';
12
12
  export { mutateConfig } from './config-mutate.js';
13
- export { CONFIG_FILENAME, FIREFORGE_DIR, getProjectPaths, STATE_FILENAME, SUPPORTED_CONFIG_PATHS, SUPPORTED_CONFIG_ROOT_KEYS, } from './config-paths.js';
14
- export { loadState, saveState, updateState } from './config-state.js';
13
+ export { FIREFORGE_DIR, getProjectPaths, SUPPORTED_CONFIG_PATHS, SUPPORTED_CONFIG_ROOT_KEYS, } from './config-paths.js';
14
+ export { loadState, updateState } from './config-state.js';
15
15
  export { validateConfig } from './config-validate.js';
16
16
  /**
17
17
  * Checks if a fireforge.json exists in the given directory.
@@ -18,20 +18,10 @@ import { validateConfig } from './config-validate.js';
18
18
  import { createSiblingLockPath, withFileLock } from './file-lock.js';
19
19
  // ---- re-exports ----
20
20
  export { mutateConfig } from './config-mutate.js';
21
- export { CONFIG_FILENAME, FIREFORGE_DIR, getProjectPaths, STATE_FILENAME, SUPPORTED_CONFIG_PATHS, SUPPORTED_CONFIG_ROOT_KEYS, } from './config-paths.js';
22
- export { loadState, saveState, updateState } from './config-state.js';
21
+ export { FIREFORGE_DIR, getProjectPaths, SUPPORTED_CONFIG_PATHS, SUPPORTED_CONFIG_ROOT_KEYS, } from './config-paths.js';
22
+ export { loadState, updateState } from './config-state.js';
23
23
  export { validateConfig } from './config-validate.js';
24
24
  // ---- config I/O (stays here because it bridges paths + validation) ----
25
- /**
26
- * Config-file existence probe.
27
- *
28
- * Uses {@link pathExistsStrict} deliberately: a permission error probing
29
- * `fireforge.json` must propagate rather than read as "no config here",
30
- * which is what plain `pathExists` would report.
31
- */
32
- async function configPathExists(path) {
33
- return fsUtils.pathExistsStrict(path);
34
- }
35
25
  /**
36
26
  * Checks if a fireforge.json exists in the given directory.
37
27
  * @param root - Root directory to check
@@ -39,7 +29,9 @@ async function configPathExists(path) {
39
29
  */
40
30
  export async function configExists(root) {
41
31
  const paths = getProjectPaths(root);
42
- return configPathExists(paths.config);
32
+ // Strict probe: a permission error on `fireforge.json` must propagate
33
+ // rather than read as "no config here", which plain `pathExists` reports.
34
+ return fsUtils.pathExistsStrict(paths.config);
43
35
  }
44
36
  /**
45
37
  * Loads and validates the fireforge.json configuration.
@@ -49,7 +41,9 @@ export async function configExists(root) {
49
41
  */
50
42
  export async function loadConfig(root) {
51
43
  const paths = getProjectPaths(root);
52
- if (!(await configPathExists(paths.config))) {
44
+ // Strict probe: a permission error on `fireforge.json` must propagate
45
+ // rather than read as "no config here", which plain `pathExists` reports.
46
+ if (!(await fsUtils.pathExistsStrict(paths.config))) {
53
47
  throw new ConfigNotFoundError(paths.config);
54
48
  }
55
49
  try {
@@ -81,7 +75,9 @@ export async function loadConfig(root) {
81
75
  */
82
76
  export async function loadRawConfigDocument(root) {
83
77
  const paths = getProjectPaths(root);
84
- if (!(await configPathExists(paths.config))) {
78
+ // Strict probe: a permission error on `fireforge.json` must propagate
79
+ // rather than read as "no config here", which plain `pathExists` reports.
80
+ if (!(await fsUtils.pathExistsStrict(paths.config))) {
85
81
  throw new ConfigNotFoundError(paths.config);
86
82
  }
87
83
  try {
@@ -34,10 +34,10 @@
34
34
  * staging stale while extend still vouches for it. Plain REPLACE has no such
35
35
  * window, because the earlier scope simply stops being covered.
36
36
  */
37
- import { createHash } from 'node:crypto';
38
37
  import { readFile } from 'node:fs/promises';
39
38
  import { join } from 'node:path';
40
39
  import { getNodeErrorCode, toError } from '../utils/errors.js';
40
+ import { sha256Hex } from '../utils/hash.js';
41
41
  import { verbose } from '../utils/logger.js';
42
42
  import { DELETED_FILE_FINGERPRINT, } from './build-baseline-types.js';
43
43
  import { getHead, isMissingHeadError } from './git.js';
@@ -144,7 +144,7 @@ export function formatExtendCoverageRefusal(result) {
144
144
  `longer anchored. ${remedy}`);
145
145
  case 'fingerprint-diverged':
146
146
  return ('--extend-coverage refused: ' +
147
- `${String(result.detail.length)} packageable file(s) the recorded build staged have changed ` +
147
+ `${result.detail.length} packageable file(s) the recorded build staged have changed ` +
148
148
  `since (${formatDivergedPaths(result.detail)}). Extending would vouch for the earlier scope ` +
149
149
  `while obj-*/_tests/ still holds its stale staging. ${remedy}`);
150
150
  case 'mozconfig-changed':
@@ -156,7 +156,7 @@ export function formatExtendCoverageRefusal(result) {
156
156
  function formatDivergedPaths(paths) {
157
157
  const head = paths.slice(0, MAX_DIVERGED_REPORTED).join(', ');
158
158
  const extra = paths.length - Math.min(paths.length, MAX_DIVERGED_REPORTED);
159
- return extra > 0 ? `${head}, … (+${String(extra)} more)` : head;
159
+ return extra > 0 ? `${head}, … (+${extra} more)` : head;
160
160
  }
161
161
  /**
162
162
  * Hex-encoded SHA-256 of an engine-relative file, matching the digest the
@@ -168,7 +168,7 @@ function formatDivergedPaths(paths) {
168
168
  export async function hashEngineFile(engineDir, relPath) {
169
169
  try {
170
170
  const buffer = await readFile(join(engineDir, relPath));
171
- return createHash('sha256').update(buffer).digest('hex');
171
+ return sha256Hex(buffer);
172
172
  }
173
173
  catch (error) {
174
174
  if (getNodeErrorCode(error) === 'ENOENT') {
@@ -226,22 +226,22 @@ export function summarizeDiscardBaselines(plans, succeeded) {
226
226
  const deleted = plans.filter((p) => p.kind === 'patch-deleted').length;
227
227
  const unmanaged = plans.filter((p) => p.kind === 'unmanaged').length;
228
228
  if (patchBacked + created + deleted === 0) {
229
- return `${String(succeeded)} file(s) restored to upstream state`;
229
+ return `${succeeded} file(s) restored to upstream state`;
230
230
  }
231
231
  const parts = [];
232
232
  if (patchBacked > 0)
233
- parts.push(`${String(patchBacked)} to patch baseline`);
233
+ parts.push(`${patchBacked} to patch baseline`);
234
234
  if (created > 0)
235
- parts.push(`${String(created)} re-materialized`);
235
+ parts.push(`${created} re-materialized`);
236
236
  if (deleted > 0)
237
- parts.push(`${String(deleted)} removed per patch baseline`);
237
+ parts.push(`${deleted} removed per patch baseline`);
238
238
  if (unmanaged > 0)
239
- parts.push(`${String(unmanaged)} to upstream state`);
240
- return `${String(succeeded)} file(s) restored: ${parts.join(', ')}`;
239
+ parts.push(`${unmanaged} to upstream state`);
240
+ return `${succeeded} file(s) restored: ${parts.join(', ')}`;
241
241
  }
242
242
  /** Warn text for a multi-owner (conflicted) restore. */
243
243
  export function describeConflictWarning(plan) {
244
- return (`${plan.entry.file} is claimed by ${String(plan.owners.length)} patches ` +
244
+ return (`${plan.entry.file} is claimed by ${plan.owners.length} patches ` +
245
245
  `(${plan.owners.join(', ')}). Restored the cumulative baseline; run ` +
246
246
  '"fireforge status --ownership" to resolve ownership.');
247
247
  }
@@ -19,13 +19,6 @@ export interface EngineGitReadyOptions {
19
19
  */
20
20
  unbornHeadSuffix?: string;
21
21
  }
22
- /**
23
- * Error factory for callers whose domain uses a class other than
24
- * `GeneralError`. The furnace family raises `FurnaceError`, which carries
25
- * its own `userMessage` and exit code 9, so a helper hardcoding
26
- * `GeneralError` would silently change both.
27
- */
28
- export type PreconditionErrorFactory = (message: string) => Error;
29
22
  /**
30
23
  * Asserts only that the engine checkout EXISTS.
31
24
  *
@@ -36,9 +29,8 @@ export type PreconditionErrorFactory = (message: string) => Error;
36
29
  * would refuse runs that work today.
37
30
  *
38
31
  * @param engineDir - Absolute path to the engine checkout
39
- * @param errorFactory - Optional domain-specific error constructor
40
32
  */
41
- export declare function assertEngineExists(engineDir: string, errorFactory?: PreconditionErrorFactory): Promise<void>;
33
+ export declare function assertEngineExists(engineDir: string): Promise<void>;
42
34
  /**
43
35
  * Asserts that `engineDir` exists, is a git repository, and has a baseline
44
36
  * commit — in that order, so the operator gets the most specific applicable
@@ -20,8 +20,6 @@ const UNBORN_HEAD_REMEDIATION = 'Engine repository has no baseline commit yet
20
20
  // No terminating period: the suffix below supplies its own punctuation, and
21
21
  // hardcoding one here rendered as "…cleanly., then retry the rebase."
22
22
  'Re-run "fireforge download --force" to recreate the baseline repository cleanly';
23
- /** Default factory: the message every non-furnace command already used. */
24
- const defaultErrorFactory = (message) => new GeneralError(message);
25
23
  /**
26
24
  * Asserts only that the engine checkout EXISTS.
27
25
  *
@@ -32,11 +30,10 @@ const defaultErrorFactory = (message) => new GeneralError(message);
32
30
  * would refuse runs that work today.
33
31
  *
34
32
  * @param engineDir - Absolute path to the engine checkout
35
- * @param errorFactory - Optional domain-specific error constructor
36
33
  */
37
- export async function assertEngineExists(engineDir, errorFactory = defaultErrorFactory) {
34
+ export async function assertEngineExists(engineDir) {
38
35
  if (!(await pathExists(engineDir))) {
39
- throw errorFactory('Firefox source not found. Run "fireforge download" first.');
36
+ throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
40
37
  }
41
38
  }
42
39
  /**
@@ -48,9 +45,7 @@ export async function assertEngineExists(engineDir, errorFactory = defaultErrorF
48
45
  * @param options - Optional per-command message tailoring
49
46
  */
50
47
  export async function assertEngineGitReady(engineDir, options = {}) {
51
- if (!(await pathExists(engineDir))) {
52
- throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
53
- }
48
+ await assertEngineExists(engineDir);
54
49
  if (!(await isGitRepository(engineDir))) {
55
50
  throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
56
51
  }
@@ -3,6 +3,7 @@ import { join } from 'node:path';
3
3
  import { InconclusiveVerdictError } from '../errors/base.js';
4
4
  import { toError } from '../utils/errors.js';
5
5
  import { info, warn } from '../utils/logger.js';
6
+ import { sleep } from '../utils/sleep.js';
6
7
  import { readLockStatus, withFileLock } from './file-lock.js';
7
8
  import { git } from './git-base.js';
8
9
  const ENGINE_SESSION_LOCK_PATH = join('.fireforge', 'engine-session.lock');
@@ -21,19 +22,19 @@ const MAX_WAIT_LOCK_MS = 3600 * 1000;
21
22
  * owner file degrades to the anonymous form.
22
23
  */
23
24
  function formatWaitProgressLine(waitedMs, timeoutMs, holder, queue) {
24
- const progress = `${String(Math.round(waitedMs / 1000))}s of up to ${String(Math.round(timeoutMs / 1000))}s`;
25
+ const progress = `${Math.round(waitedMs / 1000)}s of up to ${Math.round(timeoutMs / 1000)}s`;
25
26
  // Queue position: under several concurrent sessions the wait
26
27
  // line alone left operators inferring their place from `ps`.
27
28
  const position = queue === undefined || queue.depth === 0
28
29
  ? ''
29
30
  : queue.ahead === 0
30
- ? ` You are next in a queue of ${String(queue.depth)}.`
31
- : ` ${String(queue.ahead)} ahead of you (queue of ${String(queue.depth)}).`;
31
+ ? ` You are next in a queue of ${queue.depth}.`
32
+ : ` ${queue.ahead} ahead of you (queue of ${queue.depth}).`;
32
33
  if (holder === undefined) {
33
34
  return `Waiting for the FireForge engine lock — ${progress}.${position}`;
34
35
  }
35
36
  const details = holder.metadata.length > 0 ? ` (${holder.metadata.join(', ')})` : '';
36
- return `Waiting for the FireForge engine lock held by PID ${String(holder.pid)}${details} — ${progress}.${position}`;
37
+ return `Waiting for the FireForge engine lock held by PID ${holder.pid}${details} — ${progress}.${position}`;
37
38
  }
38
39
  /**
39
40
  * Formats the notice printed when the wait budget is EXTENDED because the
@@ -45,10 +46,10 @@ function formatWaitProgressLine(waitedMs, timeoutMs, holder, queue) {
45
46
  * number they typed would otherwise conclude the timeout is broken.
46
47
  */
47
48
  export function formatWaitExtendedLine(ahead, budgetMs, requestedMs) {
48
- const position = ahead === 0 ? 'you are now next' : `${String(ahead)} still ahead of you`;
49
+ const position = ahead === 0 ? 'you are now next' : `${ahead} still ahead of you`;
49
50
  return (`The queue advanced (${position}), so the engine-lock wait was extended to ` +
50
- `${String(Math.round(budgetMs / 1000))}s total (you asked for ` +
51
- `${String(Math.round(requestedMs / 1000))}s). A queue that stops moving still ` +
51
+ `${Math.round(budgetMs / 1000)}s total (you asked for ` +
52
+ `${Math.round(requestedMs / 1000)}s). A queue that stops moving still ` +
52
53
  `gives up on the budget you asked for.`);
53
54
  }
54
55
  /**
@@ -128,15 +129,15 @@ export function formatEngineSessionLockStatus(snapshot, holderCpuSeconds) {
128
129
  }
129
130
  const elapsed = snapshot.heldForMs === undefined
130
131
  ? 'unknown duration'
131
- : `${String(Math.round(snapshot.heldForMs / 1000))}s`;
132
+ : `${Math.round(snapshot.heldForMs / 1000)}s`;
132
133
  const holder = snapshot.holder;
133
134
  const identity = holder === undefined
134
135
  ? 'held by an unidentified process (no readable owner record)'
135
- : `held by PID ${String(holder.pid)}${holder.alive ? '' : ' (NOT RUNNING — stale lock)'}` +
136
+ : `held by PID ${holder.pid}${holder.alive ? '' : ' (NOT RUNNING — stale lock)'}` +
136
137
  (holder.metadata.length > 0 ? ` (${holder.metadata.join(', ')})` : '');
137
138
  const lines = [
138
139
  `Engine session lock: ${identity}, for ${elapsed}.`,
139
- `Queue depth: ${String(snapshot.queueDepth)} waiter(s).`,
140
+ `Queue depth: ${snapshot.queueDepth} waiter(s).`,
140
141
  ];
141
142
  // Liveness answers "does the process exist"; it does not answer "is it
142
143
  // getting anywhere". A holder alive for minutes having used a fraction of
@@ -182,7 +183,7 @@ export async function snapshotEngineGeneration(engineDir) {
182
183
  // and is reported immediately.
183
184
  if (!isIndexLockError(error) || attempt === GENERATION_PROBE_ATTEMPTS - 1)
184
185
  break;
185
- await delay(GENERATION_PROBE_RETRY_MS);
186
+ await sleep(GENERATION_PROBE_RETRY_MS);
186
187
  }
187
188
  }
188
189
  return `${UNAVAILABLE_PREFIX}${toError(lastError).message}`;
@@ -191,11 +192,6 @@ export async function snapshotEngineGeneration(engineDir) {
191
192
  const GENERATION_PROBE_ATTEMPTS = 3;
192
193
  /** Pause between generation-probe attempts. */
193
194
  const GENERATION_PROBE_RETRY_MS = 250;
194
- function delay(ms) {
195
- return new Promise((resolveDelay) => {
196
- setTimeout(resolveDelay, ms);
197
- });
198
- }
199
195
  /**
200
196
  * True when a git failure is the transient index-lock contention shape
201
197
  * (`Unable to create '…/.git/index.lock': File exists`) rather than a
@@ -290,7 +286,7 @@ export function describeEngineGenerationDelta(before, after) {
290
286
  return;
291
287
  const head = records.slice(0, DELTA_ENTRY_LIMIT);
292
288
  const truncated = records.length - head.length;
293
- lines.push(`${label}: ${head.join(', ')}${truncated > 0 ? `, … (+${String(truncated)} more)` : ''}`);
289
+ lines.push(`${label}: ${head.join(', ')}${truncated > 0 ? `, … (+${truncated} more)` : ''}`);
294
290
  };
295
291
  render('Working-tree entries that appeared', appeared);
296
292
  render('Working-tree entries that went away', disappeared);
@@ -114,14 +114,14 @@ export function formatAdoptThenSplitRemedy(overlaps, newPatchName, insertionOrde
114
114
  const suggestedName = newPatchName.replace(/^\d+-/, '').replace(/\.patch$/, '');
115
115
  return [
116
116
  '',
117
- `This looks like a new-file + moved-code slice: ${String(strongest.sharedLines)} substantive ` +
117
+ `This looks like a new-file + moved-code slice: ${strongest.sharedLines} substantive ` +
118
118
  `line(s) the new file(s) would add are still carried by ${strongest.sourcePatch}. ` +
119
119
  'Exporting them as their own patch cannot pass cross-patch lint while the source patch ' +
120
120
  'still owns the same code, and re-exporting cannot adopt them while they are unmanaged — ' +
121
121
  'each guard is right, and together they dead-end.',
122
122
  'Working sequence (adopt, then split):',
123
123
  ` 1. fireforge re-export ${strongest.sourcePatch} --scan ${fileArgs}`,
124
- ` 2. fireforge patch move-files ${strongest.sourcePatch} ${suggestedName} --create --order ${String(insertionOrder)} ${moveArgs}`,
124
+ ` 2. fireforge patch move-files ${strongest.sourcePatch} ${suggestedName} --create --order ${insertionOrder} ${moveArgs}`,
125
125
  'Step 1 adopts the new files into the patch that already owns the moved lines; step 2 ' +
126
126
  'splits them back out as one transaction, so the queue is never in a state where two ' +
127
127
  'patches own the same code.',