@hominis/fireforge 0.45.0 → 0.46.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (662) hide show
  1. package/CHANGELOG.md +409 -446
  2. package/dist/bin/fireforge.d.ts +1 -1
  3. package/dist/bin/fireforge.js +21 -15
  4. package/dist/build-info.json +4 -4
  5. package/dist/src/cli-usage-error.d.ts +46 -0
  6. package/dist/src/cli-usage-error.js +81 -0
  7. package/dist/src/cli.d.ts +7 -2
  8. package/dist/src/cli.js +47 -47
  9. package/dist/src/commands/bootstrap-checks.js +2 -1
  10. package/dist/src/commands/bootstrap.js +3 -4
  11. package/dist/src/commands/build.js +27 -17
  12. package/dist/src/commands/config.js +9 -9
  13. package/dist/src/commands/destructive-decision.d.ts +19 -0
  14. package/dist/src/commands/destructive-decision.js +31 -0
  15. package/dist/src/commands/discard.js +5 -5
  16. package/dist/src/commands/doctor-check-core.d.ts +4 -10
  17. package/dist/src/commands/doctor-check-core.js +0 -8
  18. package/dist/src/commands/doctor-external-toolchains.js +2 -2
  19. package/dist/src/commands/doctor-furnace-config-sync.d.ts +2 -2
  20. package/dist/src/commands/doctor-furnace-config-sync.js +20 -20
  21. package/dist/src/commands/doctor-furnace-jar.d.ts +1 -1
  22. package/dist/src/commands/doctor-furnace-jar.js +1 -1
  23. package/dist/src/commands/doctor-furnace.js +22 -22
  24. package/dist/src/commands/doctor-orphaned-harness.d.ts +10 -17
  25. package/dist/src/commands/doctor-orphaned-harness.js +14 -23
  26. package/dist/src/commands/doctor-patch-manifest.d.ts +2 -1
  27. package/dist/src/commands/doctor-patch-manifest.js +7 -6
  28. package/dist/src/commands/doctor-post-rebase-audit.js +3 -3
  29. package/dist/src/commands/doctor-source-pin.d.ts +12 -12
  30. package/dist/src/commands/doctor-source-pin.js +14 -14
  31. package/dist/src/commands/doctor-working-tree.d.ts +3 -3
  32. package/dist/src/commands/doctor-working-tree.js +3 -3
  33. package/dist/src/commands/doctor.d.ts +3 -1
  34. package/dist/src/commands/doctor.js +15 -16
  35. package/dist/src/commands/download.js +50 -37
  36. package/dist/src/commands/export-all.js +29 -12
  37. package/dist/src/commands/export-flow.d.ts +5 -5
  38. package/dist/src/commands/export-flow.js +29 -59
  39. package/dist/src/commands/export-placement-conflicts.d.ts +3 -3
  40. package/dist/src/commands/export-placement-conflicts.js +12 -12
  41. package/dist/src/commands/export-placement-gate.d.ts +2 -2
  42. package/dist/src/commands/export-placement-gate.js +7 -13
  43. package/dist/src/commands/export-placement-policy.d.ts +1 -1
  44. package/dist/src/commands/export-placement-policy.js +4 -6
  45. package/dist/src/commands/export-shared.d.ts +51 -32
  46. package/dist/src/commands/export-shared.js +17 -32
  47. package/dist/src/commands/export.js +14 -5
  48. package/dist/src/commands/furnace/apply.js +16 -16
  49. package/dist/src/commands/furnace/authoring-config.d.ts +13 -0
  50. package/dist/src/commands/furnace/authoring-config.js +19 -0
  51. package/dist/src/commands/furnace/chrome-doc-remove.d.ts +1 -1
  52. package/dist/src/commands/furnace/chrome-doc-remove.js +2 -2
  53. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +14 -14
  54. package/dist/src/commands/furnace/chrome-doc-templates.js +17 -17
  55. package/dist/src/commands/furnace/chrome-doc-tests.d.ts +2 -2
  56. package/dist/src/commands/furnace/chrome-doc-tests.js +4 -4
  57. package/dist/src/commands/furnace/chrome-doc.d.ts +8 -8
  58. package/dist/src/commands/furnace/chrome-doc.js +18 -18
  59. package/dist/src/commands/furnace/create-browser-test.d.ts +1 -1
  60. package/dist/src/commands/furnace/create-browser-test.js +7 -12
  61. package/dist/src/commands/furnace/create-dry-run.d.ts +4 -4
  62. package/dist/src/commands/furnace/create-dry-run.js +6 -9
  63. package/dist/src/commands/furnace/create-features.d.ts +2 -2
  64. package/dist/src/commands/furnace/create-features.js +2 -2
  65. package/dist/src/commands/furnace/create-mochikit.d.ts +4 -4
  66. package/dist/src/commands/furnace/create-mochikit.js +7 -7
  67. package/dist/src/commands/furnace/create-templates.d.ts +31 -15
  68. package/dist/src/commands/furnace/create-templates.js +20 -19
  69. package/dist/src/commands/furnace/create-xpcshell.d.ts +5 -5
  70. package/dist/src/commands/furnace/create-xpcshell.js +8 -8
  71. package/dist/src/commands/furnace/create.d.ts +1 -1
  72. package/dist/src/commands/furnace/create.js +35 -29
  73. package/dist/src/commands/furnace/deploy.js +15 -6
  74. package/dist/src/commands/furnace/diff.js +7 -7
  75. package/dist/src/commands/furnace/index.js +1 -1
  76. package/dist/src/commands/furnace/init.js +17 -16
  77. package/dist/src/commands/furnace/list.js +2 -2
  78. package/dist/src/commands/furnace/override.d.ts +1 -1
  79. package/dist/src/commands/furnace/override.js +43 -34
  80. package/dist/src/commands/furnace/preview.js +17 -17
  81. package/dist/src/commands/furnace/refresh.js +15 -7
  82. package/dist/src/commands/furnace/remove-mutations.d.ts +9 -10
  83. package/dist/src/commands/furnace/remove-mutations.js +19 -19
  84. package/dist/src/commands/furnace/remove-state.d.ts +1 -1
  85. package/dist/src/commands/furnace/remove-state.js +1 -1
  86. package/dist/src/commands/furnace/remove.js +4 -8
  87. package/dist/src/commands/furnace/rename-browser-test.js +10 -15
  88. package/dist/src/commands/furnace/rename-xpcshell.d.ts +2 -2
  89. package/dist/src/commands/furnace/rename-xpcshell.js +3 -3
  90. package/dist/src/commands/furnace/rename.js +36 -36
  91. package/dist/src/commands/furnace/scan.js +3 -3
  92. package/dist/src/commands/furnace/status.js +19 -16
  93. package/dist/src/commands/furnace/sync.js +6 -6
  94. package/dist/src/commands/furnace/test-file-name.d.ts +28 -0
  95. package/dist/src/commands/furnace/test-file-name.js +39 -0
  96. package/dist/src/commands/furnace/validate.js +11 -12
  97. package/dist/src/commands/furnace/validation-output.d.ts +21 -9
  98. package/dist/src/commands/furnace/validation-output.js +3 -8
  99. package/dist/src/commands/import.js +22 -29
  100. package/dist/src/commands/lint-per-patch.d.ts +2 -2
  101. package/dist/src/commands/lint-per-patch.js +27 -12
  102. package/dist/src/commands/lint-per-run-checkjs.d.ts +14 -15
  103. package/dist/src/commands/lint-per-run-checkjs.js +14 -15
  104. package/dist/src/commands/lint-report.d.ts +3 -3
  105. package/dist/src/commands/lint-report.js +4 -4
  106. package/dist/src/commands/lint.d.ts +5 -4
  107. package/dist/src/commands/lint.js +36 -36
  108. package/dist/src/commands/manifest.d.ts +4 -4
  109. package/dist/src/commands/manifest.js +1 -1
  110. package/dist/src/commands/package.js +4 -6
  111. package/dist/src/commands/patch/compact.d.ts +2 -2
  112. package/dist/src/commands/patch/compact.js +19 -20
  113. package/dist/src/commands/patch/delete.d.ts +1 -1
  114. package/dist/src/commands/patch/delete.js +23 -33
  115. package/dist/src/commands/patch/entry-projection.d.ts +13 -0
  116. package/dist/src/commands/patch/entry-projection.js +24 -0
  117. package/dist/src/commands/patch/lint-ignore.d.ts +3 -12
  118. package/dist/src/commands/patch/lint-ignore.js +33 -44
  119. package/dist/src/commands/patch/move-files-create.d.ts +2 -2
  120. package/dist/src/commands/patch/move-files-create.js +29 -15
  121. package/dist/src/commands/patch/move-files-into.d.ts +2 -2
  122. package/dist/src/commands/patch/move-files-into.js +41 -37
  123. package/dist/src/commands/patch/move-files.js +5 -14
  124. package/dist/src/commands/patch/patch-context.d.ts +1 -1
  125. package/dist/src/commands/patch/rename.d.ts +4 -4
  126. package/dist/src/commands/patch/rename.js +35 -49
  127. package/dist/src/commands/patch/reorder.js +35 -48
  128. package/dist/src/commands/patch/split-plan.d.ts +7 -8
  129. package/dist/src/commands/patch/split-plan.js +10 -13
  130. package/dist/src/commands/patch/split.d.ts +2 -2
  131. package/dist/src/commands/patch/split.js +50 -48
  132. package/dist/src/commands/patch/staged-dependency-validate.d.ts +1 -1
  133. package/dist/src/commands/patch/staged-dependency-validate.js +3 -3
  134. package/dist/src/commands/patch/staged-dependency.d.ts +1 -1
  135. package/dist/src/commands/patch/staged-dependency.js +22 -28
  136. package/dist/src/commands/patch/tier.d.ts +2 -2
  137. package/dist/src/commands/patch/tier.js +20 -30
  138. package/dist/src/commands/patch-rename-map.d.ts +19 -0
  139. package/dist/src/commands/patch-rename-map.js +36 -0
  140. package/dist/src/commands/pending-resolution.d.ts +15 -0
  141. package/dist/src/commands/pending-resolution.js +25 -0
  142. package/dist/src/commands/re-export-adjacent.d.ts +25 -3
  143. package/dist/src/commands/re-export-adjacent.js +40 -24
  144. package/dist/src/commands/re-export-bulk-scan.d.ts +3 -3
  145. package/dist/src/commands/re-export-bulk-scan.js +23 -27
  146. package/dist/src/commands/re-export-drift.d.ts +10 -10
  147. package/dist/src/commands/re-export-drift.js +33 -33
  148. package/dist/src/commands/re-export-files.d.ts +2 -3
  149. package/dist/src/commands/re-export-files.js +52 -44
  150. package/dist/src/commands/re-export-lint.d.ts +29 -30
  151. package/dist/src/commands/re-export-lint.js +36 -29
  152. package/dist/src/commands/re-export-options.d.ts +1 -1
  153. package/dist/src/commands/re-export-options.js +1 -1
  154. package/dist/src/commands/re-export-register.d.ts +1 -1
  155. package/dist/src/commands/re-export-register.js +1 -1
  156. package/dist/src/commands/re-export-scan.d.ts +1 -1
  157. package/dist/src/commands/re-export-scan.js +12 -9
  158. package/dist/src/commands/re-export-single.d.ts +23 -4
  159. package/dist/src/commands/re-export-single.js +39 -31
  160. package/dist/src/commands/re-export.js +35 -17
  161. package/dist/src/commands/rebase/abort.js +9 -14
  162. package/dist/src/commands/rebase/continue.js +20 -19
  163. package/dist/src/commands/rebase/index.d.ts +1 -1
  164. package/dist/src/commands/rebase/index.js +5 -5
  165. package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
  166. package/dist/src/commands/rebase/patch-loop.js +20 -24
  167. package/dist/src/commands/register.js +5 -5
  168. package/dist/src/commands/reset.js +1 -1
  169. package/dist/src/commands/resolve.d.ts +2 -2
  170. package/dist/src/commands/resolve.js +21 -13
  171. package/dist/src/commands/run.d.ts +0 -13
  172. package/dist/src/commands/run.js +38 -51
  173. package/dist/src/commands/setup-support.d.ts +16 -4
  174. package/dist/src/commands/setup-support.js +20 -20
  175. package/dist/src/commands/setup.js +4 -10
  176. package/dist/src/commands/source.js +9 -2
  177. package/dist/src/commands/status-check.d.ts +3 -3
  178. package/dist/src/commands/status-check.js +6 -6
  179. package/dist/src/commands/status-json.d.ts +14 -13
  180. package/dist/src/commands/status-json.js +9 -8
  181. package/dist/src/commands/status-output.d.ts +3 -3
  182. package/dist/src/commands/status-output.js +5 -5
  183. package/dist/src/commands/status-ownership.d.ts +3 -3
  184. package/dist/src/commands/status-ownership.js +3 -3
  185. package/dist/src/commands/status.js +25 -26
  186. package/dist/src/commands/test-build-phase.d.ts +3 -3
  187. package/dist/src/commands/test-build-phase.js +48 -24
  188. package/dist/src/commands/test-diagnose.d.ts +1 -1
  189. package/dist/src/commands/test-diagnose.js +20 -20
  190. package/dist/src/commands/test-modes.d.ts +4 -4
  191. package/dist/src/commands/test-modes.js +4 -5
  192. package/dist/src/commands/test-register.js +5 -3
  193. package/dist/src/commands/test-run.d.ts +11 -11
  194. package/dist/src/commands/test-run.js +40 -33
  195. package/dist/src/commands/test-stale-gate.d.ts +12 -12
  196. package/dist/src/commands/test-stale-gate.js +17 -17
  197. package/dist/src/commands/test-verdict.d.ts +19 -5
  198. package/dist/src/commands/test-verdict.js +26 -11
  199. package/dist/src/commands/test.d.ts +3 -3
  200. package/dist/src/commands/test.js +73 -36
  201. package/dist/src/commands/token-coverage.js +3 -3
  202. package/dist/src/commands/token-list.d.ts +26 -0
  203. package/dist/src/commands/token-list.js +153 -0
  204. package/dist/src/commands/token.js +26 -10
  205. package/dist/src/commands/tree.js +26 -21
  206. package/dist/src/commands/typecheck.d.ts +6 -6
  207. package/dist/src/commands/typecheck.js +16 -16
  208. package/dist/src/commands/verify.d.ts +3 -3
  209. package/dist/src/commands/verify.js +10 -10
  210. package/dist/src/commands/watch.js +11 -11
  211. package/dist/src/commands/wire.js +17 -16
  212. package/dist/src/core/ast-utils.d.ts +7 -7
  213. package/dist/src/core/ast-utils.js +8 -8
  214. package/dist/src/core/branding.d.ts +5 -12
  215. package/dist/src/core/branding.js +6 -5
  216. package/dist/src/core/browser-wire.d.ts +1 -1
  217. package/dist/src/core/browser-wire.js +10 -7
  218. package/dist/src/core/build-audit-platform.d.ts +32 -3
  219. package/dist/src/core/build-audit-platform.js +153 -13
  220. package/dist/src/core/build-audit-registration.d.ts +5 -5
  221. package/dist/src/core/build-audit-registration.js +10 -9
  222. package/dist/src/core/build-audit-resolve.d.ts +5 -5
  223. package/dist/src/core/build-audit-resolve.js +6 -6
  224. package/dist/src/core/build-audit-skip.d.ts +57 -0
  225. package/dist/src/core/build-audit-skip.js +112 -0
  226. package/dist/src/core/build-audit-transforms.d.ts +2 -2
  227. package/dist/src/core/build-audit-transforms.js +3 -3
  228. package/dist/src/core/build-audit.d.ts +21 -8
  229. package/dist/src/core/build-audit.js +142 -40
  230. package/dist/src/core/build-baseline-types.d.ts +19 -18
  231. package/dist/src/core/build-baseline-types.js +1 -1
  232. package/dist/src/core/build-baseline.d.ts +48 -31
  233. package/dist/src/core/build-baseline.js +20 -43
  234. package/dist/src/core/build-jar-escalation.d.ts +8 -8
  235. package/dist/src/core/build-jar-escalation.js +18 -17
  236. package/dist/src/core/build-overwrite-guard.d.ts +4 -4
  237. package/dist/src/core/build-overwrite-guard.js +14 -14
  238. package/dist/src/core/build-prepare.d.ts +4 -12
  239. package/dist/src/core/build-prepare.js +22 -26
  240. package/dist/src/core/config-mutate.js +4 -4
  241. package/dist/src/core/config-paths.d.ts +2 -6
  242. package/dist/src/core/config-paths.js +4 -2
  243. package/dist/src/core/config-state.d.ts +0 -6
  244. package/dist/src/core/config-state.js +3 -21
  245. package/dist/src/core/config-validate-build-audit.d.ts +8 -0
  246. package/dist/src/core/config-validate-build-audit.js +80 -0
  247. package/dist/src/core/config-validate-file-size.d.ts +1 -1
  248. package/dist/src/core/config-validate-file-size.js +3 -3
  249. package/dist/src/core/config-validate-patch-policy.d.ts +2 -2
  250. package/dist/src/core/config-validate-patch-policy.js +3 -3
  251. package/dist/src/core/config-validate-test-toolchains.d.ts +2 -2
  252. package/dist/src/core/config-validate-test-toolchains.js +4 -5
  253. package/dist/src/core/config-validate.js +22 -12
  254. package/dist/src/core/config.d.ts +12 -12
  255. package/dist/src/core/config.js +21 -25
  256. package/dist/src/core/coverage-extend.d.ts +16 -16
  257. package/dist/src/core/coverage-extend.js +20 -20
  258. package/dist/src/core/destructive.d.ts +14 -14
  259. package/dist/src/core/destructive.js +9 -9
  260. package/dist/src/core/diff-hunks.d.ts +2 -2
  261. package/dist/src/core/diff-hunks.js +7 -7
  262. package/dist/src/core/discard-baseline.d.ts +10 -10
  263. package/dist/src/core/discard-baseline.js +22 -23
  264. package/dist/src/core/display-state.d.ts +11 -11
  265. package/dist/src/core/display-state.js +12 -12
  266. package/dist/src/core/engine-changes.d.ts +2 -2
  267. package/dist/src/core/engine-changes.js +2 -2
  268. package/dist/src/core/engine-precondition.d.ts +5 -13
  269. package/dist/src/core/engine-precondition.js +5 -10
  270. package/dist/src/core/engine-session-lock.d.ts +18 -18
  271. package/dist/src/core/engine-session-lock.js +50 -54
  272. package/dist/src/core/export-moved-code-shape.d.ts +7 -7
  273. package/dist/src/core/export-moved-code-shape.js +10 -10
  274. package/dist/src/core/file-lock-owner.d.ts +103 -0
  275. package/dist/src/core/file-lock-owner.js +259 -0
  276. package/dist/src/core/file-lock.d.ts +14 -19
  277. package/dist/src/core/file-lock.js +90 -128
  278. package/dist/src/core/firefox-cache.d.ts +13 -1
  279. package/dist/src/core/firefox-cache.js +61 -36
  280. package/dist/src/core/firefox-download.d.ts +15 -0
  281. package/dist/src/core/firefox-download.js +38 -22
  282. package/dist/src/core/firefox-extract.d.ts +8 -7
  283. package/dist/src/core/firefox-extract.js +45 -27
  284. package/dist/src/core/firefox.d.ts +34 -36
  285. package/dist/src/core/firefox.js +13 -44
  286. package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
  287. package/dist/src/core/furnace-apply-dry-run.js +2 -8
  288. package/dist/src/core/furnace-apply-ftl.d.ts +5 -5
  289. package/dist/src/core/furnace-apply-ftl.js +8 -8
  290. package/dist/src/core/furnace-apply-helpers.d.ts +9 -20
  291. package/dist/src/core/furnace-apply-helpers.js +28 -39
  292. package/dist/src/core/furnace-apply-output.js +1 -1
  293. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +9 -9
  294. package/dist/src/core/furnace-apply-overwrite-warn.js +13 -11
  295. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +1 -1
  296. package/dist/src/core/furnace-apply-undeploy-actions.js +1 -1
  297. package/dist/src/core/furnace-apply.d.ts +4 -4
  298. package/dist/src/core/furnace-apply.js +50 -29
  299. package/dist/src/core/furnace-checksum-utils.d.ts +5 -5
  300. package/dist/src/core/furnace-checksum-utils.js +5 -5
  301. package/dist/src/core/furnace-config-custom.d.ts +1 -1
  302. package/dist/src/core/furnace-config-custom.js +2 -2
  303. package/dist/src/core/furnace-config-order.js +1 -1
  304. package/dist/src/core/furnace-config-tokens.d.ts +3 -3
  305. package/dist/src/core/furnace-config-tokens.js +3 -3
  306. package/dist/src/core/furnace-config-validate.js +2 -2
  307. package/dist/src/core/furnace-config.d.ts +5 -5
  308. package/dist/src/core/furnace-config.js +40 -53
  309. package/dist/src/core/furnace-constants.d.ts +6 -6
  310. package/dist/src/core/furnace-constants.js +9 -8
  311. package/dist/src/core/furnace-css-fragments.d.ts +6 -6
  312. package/dist/src/core/furnace-css-fragments.js +11 -11
  313. package/dist/src/core/furnace-dir-entry.d.ts +20 -0
  314. package/dist/src/core/furnace-dir-entry.js +21 -0
  315. package/dist/src/core/furnace-jsconfig.d.ts +7 -7
  316. package/dist/src/core/furnace-jsconfig.js +22 -20
  317. package/dist/src/core/furnace-operation.d.ts +22 -19
  318. package/dist/src/core/furnace-operation.js +38 -34
  319. package/dist/src/core/furnace-precondition.d.ts +10 -10
  320. package/dist/src/core/furnace-precondition.js +8 -8
  321. package/dist/src/core/furnace-refresh.d.ts +18 -6
  322. package/dist/src/core/furnace-refresh.js +5 -8
  323. package/dist/src/core/furnace-registration-ast.d.ts +2 -3
  324. package/dist/src/core/furnace-registration-ast.js +9 -12
  325. package/dist/src/core/furnace-registration-remove.d.ts +4 -4
  326. package/dist/src/core/furnace-registration-remove.js +6 -6
  327. package/dist/src/core/furnace-registration-validate.d.ts +2 -2
  328. package/dist/src/core/furnace-registration-validate.js +2 -2
  329. package/dist/src/core/furnace-registration.d.ts +7 -6
  330. package/dist/src/core/furnace-registration.js +29 -36
  331. package/dist/src/core/furnace-rollback.d.ts +1 -1
  332. package/dist/src/core/furnace-rollback.js +5 -5
  333. package/dist/src/core/furnace-scanner.js +3 -6
  334. package/dist/src/core/furnace-stale-export.d.ts +17 -16
  335. package/dist/src/core/furnace-stale-export.js +16 -15
  336. package/dist/src/core/furnace-staleness.d.ts +2 -2
  337. package/dist/src/core/furnace-staleness.js +2 -2
  338. package/dist/src/core/furnace-state-persist.d.ts +12 -12
  339. package/dist/src/core/furnace-state-persist.js +10 -10
  340. package/dist/src/core/furnace-step-errors.d.ts +6 -6
  341. package/dist/src/core/furnace-step-errors.js +6 -6
  342. package/dist/src/core/furnace-validate-accessibility.js +14 -14
  343. package/dist/src/core/furnace-validate-compatibility.js +9 -9
  344. package/dist/src/core/furnace-validate-registration.d.ts +2 -2
  345. package/dist/src/core/furnace-validate-registration.js +18 -20
  346. package/dist/src/core/furnace-validate-structure.d.ts +1 -1
  347. package/dist/src/core/furnace-validate-structure.js +4 -4
  348. package/dist/src/core/furnace-validate.js +9 -9
  349. package/dist/src/core/furnace-version-drift.d.ts +5 -5
  350. package/dist/src/core/furnace-version-drift.js +5 -5
  351. package/dist/src/core/git-base.d.ts +6 -4
  352. package/dist/src/core/git-base.js +29 -7
  353. package/dist/src/core/git-diff-new-file.d.ts +40 -0
  354. package/dist/src/core/git-diff-new-file.js +119 -0
  355. package/dist/src/core/git-diff.d.ts +13 -13
  356. package/dist/src/core/git-diff.js +121 -189
  357. package/dist/src/core/git-file-ops.d.ts +6 -13
  358. package/dist/src/core/git-file-ops.js +11 -35
  359. package/dist/src/core/git-readonly-index.d.ts +12 -14
  360. package/dist/src/core/git-readonly-index.js +12 -16
  361. package/dist/src/core/git-status.d.ts +9 -2
  362. package/dist/src/core/git-status.js +23 -11
  363. package/dist/src/core/git.d.ts +3 -9
  364. package/dist/src/core/git.js +28 -57
  365. package/dist/src/core/harness-orphans.d.ts +17 -17
  366. package/dist/src/core/harness-orphans.js +28 -27
  367. package/dist/src/core/history-log.d.ts +19 -0
  368. package/dist/src/core/history-log.js +29 -0
  369. package/dist/src/core/license-headers.d.ts +19 -18
  370. package/dist/src/core/license-headers.js +29 -28
  371. package/dist/src/core/mach-build-artifacts.d.ts +21 -21
  372. package/dist/src/core/mach-build-artifacts.js +23 -23
  373. package/dist/src/core/mach-error-hints.d.ts +5 -10
  374. package/dist/src/core/mach-error-hints.js +14 -14
  375. package/dist/src/core/mach-known-noise-filter.d.ts +42 -38
  376. package/dist/src/core/mach-known-noise-filter.js +57 -56
  377. package/dist/src/core/mach-mozconfig.d.ts +6 -6
  378. package/dist/src/core/mach-mozconfig.js +17 -16
  379. package/dist/src/core/mach-python.d.ts +3 -0
  380. package/dist/src/core/mach-python.js +13 -7
  381. package/dist/src/core/mach-resource-shim.d.ts +24 -12
  382. package/dist/src/core/mach-resource-shim.js +124 -21
  383. package/dist/src/core/mach.d.ts +59 -73
  384. package/dist/src/core/mach.js +47 -115
  385. package/dist/src/core/marionette-port.d.ts +15 -30
  386. package/dist/src/core/marionette-port.js +46 -89
  387. package/dist/src/core/marionette-preflight.d.ts +6 -6
  388. package/dist/src/core/marionette-preflight.js +32 -50
  389. package/dist/src/core/markdown-table.d.ts +9 -9
  390. package/dist/src/core/markdown-table.js +14 -14
  391. package/dist/src/core/mochitest-server-port.d.ts +8 -8
  392. package/dist/src/core/mochitest-server-port.js +17 -17
  393. package/dist/src/core/module-resolution-preflight.d.ts +8 -8
  394. package/dist/src/core/module-resolution-preflight.js +11 -11
  395. package/dist/src/core/moz-manifest-helpers.d.ts +7 -7
  396. package/dist/src/core/moz-manifest-helpers.js +7 -7
  397. package/dist/src/core/moz-manifest-register.d.ts +1 -1
  398. package/dist/src/core/moz-manifest-register.js +2 -2
  399. package/dist/src/core/moz-manifest-rules.js +46 -42
  400. package/dist/src/core/moz-manifest-tokenizers.js +1 -1
  401. package/dist/src/core/ownership-table.d.ts +5 -5
  402. package/dist/src/core/ownership-table.js +7 -7
  403. package/dist/src/core/parser-fallback.d.ts +3 -3
  404. package/dist/src/core/parser-fallback.js +3 -3
  405. package/dist/src/core/patch-apply-fuzz.d.ts +5 -5
  406. package/dist/src/core/patch-apply-fuzz.js +13 -13
  407. package/dist/src/core/patch-apply.d.ts +13 -14
  408. package/dist/src/core/patch-apply.js +36 -42
  409. package/dist/src/core/patch-export-coverage.d.ts +0 -9
  410. package/dist/src/core/patch-export-coverage.js +1 -30
  411. package/dist/src/core/patch-export-update.d.ts +21 -4
  412. package/dist/src/core/patch-export-update.js +7 -6
  413. package/dist/src/core/patch-export.d.ts +20 -30
  414. package/dist/src/core/patch-export.js +55 -77
  415. package/dist/src/core/patch-files.d.ts +0 -4
  416. package/dist/src/core/patch-files.js +0 -11
  417. package/dist/src/core/patch-identifier-suggest.d.ts +1 -1
  418. package/dist/src/core/patch-identifier-suggest.js +4 -4
  419. package/dist/src/core/patch-lint-binary.d.ts +12 -18
  420. package/dist/src/core/patch-lint-cache.d.ts +18 -2
  421. package/dist/src/core/patch-lint-cache.js +16 -19
  422. package/dist/src/core/patch-lint-checkjs.d.ts +55 -38
  423. package/dist/src/core/patch-lint-checkjs.js +78 -64
  424. package/dist/src/core/patch-lint-chrome-jsdoc.d.ts +8 -9
  425. package/dist/src/core/patch-lint-chrome-jsdoc.js +10 -11
  426. package/dist/src/core/patch-lint-creators.d.ts +5 -9
  427. package/dist/src/core/patch-lint-creators.js +3 -3
  428. package/dist/src/core/patch-lint-cross.d.ts +27 -19
  429. package/dist/src/core/patch-lint-cross.js +67 -110
  430. package/dist/src/core/patch-lint-css.d.ts +1 -1
  431. package/dist/src/core/patch-lint-css.js +15 -11
  432. package/dist/src/core/patch-lint-diff-tag.d.ts +5 -5
  433. package/dist/src/core/patch-lint-diff-tag.js +5 -5
  434. package/dist/src/core/patch-lint-diff.d.ts +4 -4
  435. package/dist/src/core/patch-lint-diff.js +4 -4
  436. package/dist/src/core/patch-lint-file-size.d.ts +3 -3
  437. package/dist/src/core/patch-lint-file-size.js +6 -6
  438. package/dist/src/core/patch-lint-forward-registration.d.ts +58 -0
  439. package/dist/src/core/patch-lint-forward-registration.js +210 -0
  440. package/dist/src/core/patch-lint-jsdoc.d.ts +2 -2
  441. package/dist/src/core/patch-lint-jsdoc.js +46 -46
  442. package/dist/src/core/patch-lint-module-registration.d.ts +3 -9
  443. package/dist/src/core/patch-lint-module-registration.js +1 -1
  444. package/dist/src/core/patch-lint-mozbuild.d.ts +1 -1
  445. package/dist/src/core/patch-lint-mozbuild.js +10 -10
  446. package/dist/src/core/patch-lint-observer.d.ts +3 -3
  447. package/dist/src/core/patch-lint-observer.js +7 -7
  448. package/dist/src/core/patch-lint-ownership.d.ts +6 -6
  449. package/dist/src/core/patch-lint-ownership.js +6 -6
  450. package/dist/src/core/patch-lint-prettier.d.ts +6 -6
  451. package/dist/src/core/patch-lint-prettier.js +7 -7
  452. package/dist/src/core/patch-lint-queue-types.d.ts +60 -0
  453. package/dist/src/core/patch-lint-queue-types.js +12 -0
  454. package/dist/src/core/patch-lint-staged-registration.d.ts +61 -0
  455. package/dist/src/core/patch-lint-staged-registration.js +90 -0
  456. package/dist/src/core/patch-lint-unmanaged-companion.d.ts +23 -23
  457. package/dist/src/core/patch-lint-unmanaged-companion.js +26 -26
  458. package/dist/src/core/patch-lint.d.ts +41 -35
  459. package/dist/src/core/patch-lint.js +52 -49
  460. package/dist/src/core/patch-lock.d.ts +1 -1
  461. package/dist/src/core/patch-lock.js +11 -11
  462. package/dist/src/core/patch-manifest-consistency.d.ts +7 -7
  463. package/dist/src/core/patch-manifest-consistency.js +16 -17
  464. package/dist/src/core/patch-manifest-files-affected.d.ts +1 -1
  465. package/dist/src/core/patch-manifest-files-affected.js +3 -3
  466. package/dist/src/core/patch-manifest-io.d.ts +14 -14
  467. package/dist/src/core/patch-manifest-io.js +35 -34
  468. package/dist/src/core/patch-manifest-query.d.ts +1 -1
  469. package/dist/src/core/patch-manifest-query.js +4 -4
  470. package/dist/src/core/patch-manifest-resolve.d.ts +5 -5
  471. package/dist/src/core/patch-manifest-resolve.js +7 -7
  472. package/dist/src/core/patch-manifest-validate.js +3 -3
  473. package/dist/src/core/patch-manifest.d.ts +2 -2
  474. package/dist/src/core/patch-manifest.js +2 -2
  475. package/dist/src/core/patch-parse.d.ts +32 -21
  476. package/dist/src/core/patch-parse.js +40 -15
  477. package/dist/src/core/patch-policy.js +15 -27
  478. package/dist/src/core/patch-registration-refs.d.ts +4 -4
  479. package/dist/src/core/patch-registration-refs.js +7 -7
  480. package/dist/src/core/patch-source-metadata.d.ts +1 -3
  481. package/dist/src/core/patch-source-metadata.js +0 -4
  482. package/dist/src/core/patch-transform.d.ts +8 -16
  483. package/dist/src/core/patch-transform.js +13 -23
  484. package/dist/src/core/process-cpu-time.d.ts +1 -1
  485. package/dist/src/core/process-cpu-time.js +6 -6
  486. package/dist/src/core/rebase-session.d.ts +9 -15
  487. package/dist/src/core/rebase-session.js +13 -25
  488. package/dist/src/core/register-browser-content.js +3 -2
  489. package/dist/src/core/register-browser-test.js +2 -1
  490. package/dist/src/core/register-module.js +2 -1
  491. package/dist/src/core/register-result.d.ts +1 -1
  492. package/dist/src/core/register-result.js +1 -1
  493. package/dist/src/core/register-scaffold.d.ts +2 -2
  494. package/dist/src/core/register-scaffold.js +8 -7
  495. package/dist/src/core/register-shared-css.d.ts +3 -3
  496. package/dist/src/core/register-shared-css.js +6 -5
  497. package/dist/src/core/register-xpcshell-test.d.ts +2 -2
  498. package/dist/src/core/register-xpcshell-test.js +2 -2
  499. package/dist/src/core/run-log-redact.d.ts +38 -0
  500. package/dist/src/core/run-log-redact.js +69 -0
  501. package/dist/src/core/run-log.d.ts +21 -10
  502. package/dist/src/core/run-log.js +76 -27
  503. package/dist/src/core/shared-ftl.d.ts +2 -2
  504. package/dist/src/core/shared-ftl.js +6 -6
  505. package/dist/src/core/signal-critical.d.ts +2 -2
  506. package/dist/src/core/signal-critical.js +4 -6
  507. package/dist/src/core/smoke-patterns.d.ts +5 -6
  508. package/dist/src/core/smoke-patterns.js +15 -20
  509. package/dist/src/core/state-file.d.ts +15 -4
  510. package/dist/src/core/state-file.js +14 -1
  511. package/dist/src/core/status-binary.d.ts +8 -8
  512. package/dist/src/core/status-binary.js +12 -12
  513. package/dist/src/core/status-classify.d.ts +17 -17
  514. package/dist/src/core/status-classify.js +13 -12
  515. package/dist/src/core/test-changed-prefs.d.ts +12 -12
  516. package/dist/src/core/test-changed-prefs.js +40 -25
  517. package/dist/src/core/test-harness-crash.d.ts +39 -39
  518. package/dist/src/core/test-harness-crash.js +69 -69
  519. package/dist/src/core/test-harness-output.d.ts +7 -7
  520. package/dist/src/core/test-harness-output.js +7 -7
  521. package/dist/src/core/test-harness-verdict-notes.d.ts +13 -14
  522. package/dist/src/core/test-harness-verdict-notes.js +4 -4
  523. package/dist/src/core/test-path-scope.d.ts +8 -8
  524. package/dist/src/core/test-path-scope.js +11 -11
  525. package/dist/src/core/test-stale-check.d.ts +22 -23
  526. package/dist/src/core/test-stale-check.js +31 -30
  527. package/dist/src/core/test-stale-symlink.d.ts +1 -1
  528. package/dist/src/core/test-stale-symlink.js +1 -1
  529. package/dist/src/core/test-stall-triage.d.ts +9 -9
  530. package/dist/src/core/test-stall-triage.js +22 -22
  531. package/dist/src/core/test-xpcshell-retry.d.ts +26 -7
  532. package/dist/src/core/test-xpcshell-retry.js +14 -11
  533. package/dist/src/core/token-category.d.ts +35 -2
  534. package/dist/src/core/token-category.js +67 -58
  535. package/dist/src/core/token-coverage.js +4 -5
  536. package/dist/src/core/token-dark-mode.d.ts +9 -9
  537. package/dist/src/core/token-dark-mode.js +13 -25
  538. package/dist/src/core/token-docs.d.ts +1 -1
  539. package/dist/src/core/token-inventory.d.ts +56 -0
  540. package/dist/src/core/token-inventory.js +109 -0
  541. package/dist/src/core/token-manager.d.ts +4 -4
  542. package/dist/src/core/token-manager.js +22 -22
  543. package/dist/src/core/token-scaffold.d.ts +4 -4
  544. package/dist/src/core/token-scaffold.js +4 -4
  545. package/dist/src/core/token-variant.d.ts +14 -14
  546. package/dist/src/core/token-variant.js +52 -53
  547. package/dist/src/core/toolchain-preflight.d.ts +15 -15
  548. package/dist/src/core/toolchain-preflight.js +23 -23
  549. package/dist/src/core/tree-cow.d.ts +3 -5
  550. package/dist/src/core/tree-cow.js +8 -8
  551. package/dist/src/core/tree-guard.d.ts +3 -3
  552. package/dist/src/core/tree-guard.js +23 -23
  553. package/dist/src/core/tree-store.d.ts +15 -15
  554. package/dist/src/core/tree-store.js +39 -38
  555. package/dist/src/core/typecheck-shim.d.ts +16 -15
  556. package/dist/src/core/typecheck-shim.js +26 -25
  557. package/dist/src/core/typecheck.d.ts +12 -12
  558. package/dist/src/core/typecheck.js +33 -33
  559. package/dist/src/core/wire-destroy.d.ts +2 -2
  560. package/dist/src/core/wire-destroy.js +5 -5
  561. package/dist/src/core/wire-dom-fragment.d.ts +3 -3
  562. package/dist/src/core/wire-dom-fragment.js +12 -23
  563. package/dist/src/core/wire-init.d.ts +2 -2
  564. package/dist/src/core/wire-init.js +7 -7
  565. package/dist/src/core/wire-subscript.d.ts +1 -1
  566. package/dist/src/core/wire-subscript.js +3 -3
  567. package/dist/src/core/wire-utils.d.ts +9 -9
  568. package/dist/src/core/wire-utils.js +16 -28
  569. package/dist/src/core/xpcshell-appdir.d.ts +20 -20
  570. package/dist/src/core/xpcshell-appdir.js +28 -30
  571. package/dist/src/core/xpcshell-profile-dir.d.ts +3 -3
  572. package/dist/src/core/xpcshell-profile-dir.js +7 -7
  573. package/dist/src/errors/base.d.ts +69 -16
  574. package/dist/src/errors/base.js +95 -26
  575. package/dist/src/errors/build.d.ts +6 -6
  576. package/dist/src/errors/build.js +26 -34
  577. package/dist/src/errors/codes.d.ts +10 -10
  578. package/dist/src/errors/codes.js +10 -10
  579. package/dist/src/errors/config.d.ts +0 -14
  580. package/dist/src/errors/config.js +13 -34
  581. package/dist/src/errors/download.d.ts +13 -0
  582. package/dist/src/errors/download.js +53 -24
  583. package/dist/src/errors/furnace.d.ts +2 -2
  584. package/dist/src/errors/furnace.js +8 -9
  585. package/dist/src/errors/git.d.ts +9 -9
  586. package/dist/src/errors/git.js +21 -31
  587. package/dist/src/errors/patch.d.ts +3 -3
  588. package/dist/src/errors/patch.js +11 -13
  589. package/dist/src/errors/rebase.d.ts +1 -1
  590. package/dist/src/errors/rebase.js +8 -8
  591. package/dist/src/errors/run.d.ts +1 -1
  592. package/dist/src/errors/run.js +1 -1
  593. package/dist/src/index.d.ts +4 -4
  594. package/dist/src/index.js +4 -4
  595. package/dist/src/types/commands/options.d.ts +155 -200
  596. package/dist/src/types/commands/patches.d.ts +19 -19
  597. package/dist/src/types/commands/project.d.ts +3 -3
  598. package/dist/src/types/config.d.ts +52 -19
  599. package/dist/src/types/furnace.d.ts +17 -17
  600. package/dist/src/types/index.d.ts +0 -1
  601. package/dist/src/types/json.d.ts +3 -3
  602. package/dist/src/types/json.js +2 -2
  603. package/dist/src/types/typecheck.d.ts +3 -3
  604. package/dist/src/utils/assert.d.ts +4 -16
  605. package/dist/src/utils/assert.js +7 -24
  606. package/dist/src/utils/build-info.d.ts +6 -6
  607. package/dist/src/utils/build-info.js +6 -6
  608. package/dist/src/utils/child-env.d.ts +7 -6
  609. package/dist/src/utils/child-env.js +5 -5
  610. package/dist/src/utils/concurrency.d.ts +2 -2
  611. package/dist/src/utils/concurrency.js +2 -2
  612. package/dist/src/utils/elapsed.js +10 -1
  613. package/dist/src/utils/errors.d.ts +8 -8
  614. package/dist/src/utils/errors.js +8 -8
  615. package/dist/src/utils/fs.d.ts +1 -1
  616. package/dist/src/utils/fs.js +15 -20
  617. package/dist/src/utils/hash.d.ts +12 -0
  618. package/dist/src/utils/hash.js +16 -0
  619. package/dist/src/utils/line-dispatch.d.ts +15 -0
  620. package/dist/src/utils/line-dispatch.js +48 -0
  621. package/dist/src/utils/logger.d.ts +4 -4
  622. package/dist/src/utils/logger.js +11 -11
  623. package/dist/src/utils/machine-output.js +2 -2
  624. package/dist/src/utils/options.d.ts +14 -20
  625. package/dist/src/utils/options.js +26 -27
  626. package/dist/src/utils/package-root.js +5 -14
  627. package/dist/src/utils/paths.d.ts +4 -4
  628. package/dist/src/utils/paths.js +4 -4
  629. package/dist/src/utils/platform.d.ts +0 -4
  630. package/dist/src/utils/process-group.d.ts +10 -9
  631. package/dist/src/utils/process-group.js +37 -36
  632. package/dist/src/utils/process.d.ts +70 -55
  633. package/dist/src/utils/process.js +129 -179
  634. package/dist/src/utils/ps-duration.d.ts +4 -4
  635. package/dist/src/utils/ps-duration.js +9 -9
  636. package/dist/src/utils/regex.d.ts +4 -4
  637. package/dist/src/utils/regex.js +7 -7
  638. package/dist/src/utils/sleep.d.ts +26 -0
  639. package/dist/src/utils/sleep.js +31 -0
  640. package/dist/src/utils/stdio-drain.d.ts +7 -7
  641. package/dist/src/utils/stdio-drain.js +8 -8
  642. package/dist/src/utils/validation.d.ts +12 -49
  643. package/dist/src/utils/validation.js +26 -36
  644. package/docs/README.md +2 -2
  645. package/docs/configuration.md +163 -69
  646. package/docs/exit-codes.md +66 -49
  647. package/docs/furnace.md +20 -19
  648. package/docs/lifecycle-invariants.md +414 -247
  649. package/docs/machine-output.md +58 -34
  650. package/docs/patch-workflow.md +48 -44
  651. package/docs/run-logs.md +72 -28
  652. package/docs/testing.md +105 -98
  653. package/docs/verification-trees.md +23 -22
  654. package/package.json +7 -6
  655. package/dist/src/commands/rebase.d.ts +0 -4
  656. package/dist/src/commands/rebase.js +0 -5
  657. package/dist/src/core/patch-artifact-normalize.d.ts +0 -21
  658. package/dist/src/core/patch-artifact-normalize.js +0 -24
  659. package/dist/src/core/patch-lint-reexports.d.ts +0 -7
  660. package/dist/src/core/patch-lint-reexports.js +0 -8
  661. package/dist/src/core/wire-targets.d.ts +0 -7
  662. package/dist/src/core/wire-targets.js +0 -8
@@ -3,307 +3,474 @@
3
3
  # Mutation lifecycle invariants (internal)
4
4
 
5
5
  Internal architecture notes for contributors. FireForge's safety behavior
6
- around destructive operations is deliberately spread across small modules;
7
- this document is the one place that states the invariants those modules
8
- uphold **together**, and which module owns which piece. If you add a new
9
- mutating command, read the decision table at the bottom first.
6
+ around destructive operations is spread across small modules on purpose. This
7
+ document is the one place that states the invariants those modules uphold
8
+ together, and says which module owns which piece. If you add a new mutating
9
+ command, read the decision table at the bottom first.
10
10
 
11
11
  ## The invariants
12
12
 
13
13
  1. **Only `bin/fireforge.ts` terminates the process.** Library code
14
- propagates `CommandError` / `FireForgeError`; it never calls
15
- `process.exit`. Enforced by the process-boundary test.
16
- 2. **At most one real engine/patch mutation runs at a time** per resource:
17
- CLI build/test/export/re-export/furnace mutations first take the
18
- `.fireforge/engine-session.lock`, then subsystem locks such as build,
19
- furnace, or patch-directory locks. Furnace mutations serialize on a
20
- per-root lock, patch directory mutations (filename allocation + manifest
21
- writes) on a per-directory lock. Dry-runs hold the engine-session lock
22
- too — a `re-export --dry-run` over an untracked binary legally touches
23
- the git index, and holding the lock serializes even "read-only"
14
+ propagates `CommandError` and `FireForgeError`. It never calls
15
+ `process.exit`. This is enforced at lint time by
16
+ `no-restricted-properties` (`eslint.config.js`), which bans
17
+ `process.exit` everywhere and grants a `bin/**` override.
18
+
19
+ 2. **At most one real engine or patch mutation runs at a time** per
20
+ resource. CLI build, test, export, re-export and furnace mutations first
21
+ take the `.fireforge/engine-session.lock`, then subsystem locks such as
22
+ build, furnace, or patch-directory locks. Furnace mutations serialize on a
23
+ per-root lock, and patch directory mutations (filename allocation plus
24
+ manifest writes) on a per-directory lock. Dry runs hold the engine-session
25
+ lock too, because a `re-export --dry-run` over an untracked binary legally
26
+ touches the git index, and holding the lock serializes even "read-only"
24
27
  previews against real writers.
25
- 3. **A furnace mutation that does not finish restores the engine or says so.**
26
- Both a signal (SIGINT/SIGTERM) and a thrown error trigger a journal-based
27
- rollback; if rollback is incomplete or uncertain, a `pendingRepair` marker
28
- blocks all further furnace mutations until
28
+
29
+ 3. **A furnace mutation that does not finish restores the engine or says
30
+ so.** Both a signal (SIGINT or SIGTERM) and a thrown error trigger a
31
+ journal-based rollback. If the rollback is incomplete or uncertain, a
32
+ `pendingRepair` marker blocks all further furnace mutations until
29
33
  `fireforge doctor --repair-furnace` reconciles.
30
34
 
31
35
  The thrown-error half landed in 0.43.0. Before it, `runFurnaceMutation`'s
32
- `finally` only deregistered the operation — and because it also set
36
+ `finally` only deregistered the operation, and because it also set
33
37
  `completed`, a body that threw removed itself from the signal handler's
34
- view as well, so a throw outside a body's own catch left the checkout
35
- torn with no marker. `applyAllComponents` was the live instance: it rolls
36
- back on collected `result.errors`/`stepErrors`, but a throw from its
37
- pre/post-loop work escaped both. The wrapper now runs cleanups, restores
38
- the journal, and rethrows the **original** error (a rollback failure
39
- becomes a marker plus a warning, never a replacement error).
38
+ view as well. A throw outside a body's own catch therefore left the
39
+ checkout torn with no marker. `applyAllComponents` was the live instance:
40
+ it rolls back on collected `result.errors` and `stepErrors`, but a throw
41
+ from its pre-loop or post-loop work escaped both. The wrapper now runs
42
+ cleanups, restores the journal, and rethrows the original error. A
43
+ rollback failure becomes a marker plus a warning, never a replacement
44
+ error.
40
45
 
41
46
  Both paths race for the same journal, so each operation carries a
42
- `rollbackState` claimed synchronously before either path's first await;
43
- whichever gets there first restores and the other skips. Bodies that
44
- already restore on their own — the eight `furnace/*` command bodies,
45
- `furnace-apply`, `preview` — call `ctx.markRolledBack()` so the wrapper
46
- keeps its hands off. One deliberate asymmetry: on the throw path a body
47
- that never registered a journal writes **no** marker, because a
48
- pre-flight refusal would otherwise block every later mutation behind a
49
- repair with nothing to reconcile; the signal path still writes one there,
47
+ `rollbackState` claimed synchronously before either path's first await.
48
+ Whichever gets there first restores, and the other skips. Bodies that
49
+ already restore on their own (the eight `furnace/*` command bodies,
50
+ `furnace-apply`, `preview`) call `ctx.markRolledBack()` so the wrapper
51
+ keeps its hands off. There is one deliberate asymmetry: on the throw path,
52
+ a body that never registered a journal writes no marker, because a
53
+ preflight refusal would otherwise block every later mutation behind a
54
+ repair with nothing to reconcile. The signal path still writes one there,
50
55
  since an interrupt genuinely may have landed mid-write.
51
56
 
52
- 4. **A signal during a compound "apply + persist state" pair never tears the
53
- pair.** Rebase-style operations intentionally leave the engine mutated,
54
- so rollback is the wrong tool; instead the exit is _held_ (bounded) until
57
+ 4. **A signal during a compound "apply plus persist" pair never tears the
58
+ pair.** Rebase-style operations intentionally leave the engine mutated, so
59
+ rollback is the wrong tool. Instead the exit is held, with a bound, until
55
60
  the bookkeeping write lands, so `--continue` never sees stale progress.
56
- 5. **No lock outlives the process it belongs to** (best effort, two layers):
57
- the signal pipeline force-releases furnace locks before exiting, and
58
- stale-lock recovery (PID-first, then age) unblocks the next command for
59
- anything that escaped.
60
- 6. **Destructive patch operations are gated and audited**: explicit change
61
- summary, confirmation (or `--yes`), `--dry-run`, hard refusal on
62
- structural conflicts (`--force-unsafe` to override), and a JSONL history
63
- entry appended only after the mutation succeeded. This covers metadata
64
- mutations too: `patch tier` and `patch lint-ignore` route through
65
- `confirmDestructive` (they used to accept `--yes` without ever
66
- prompting, so the flag only appeared in the history record). Enforced
67
- by `patch-tier-and-lint-ignore.integration.test.ts`.
68
- 7. **A verification tree never mutates shared state**. Trees
69
- are full project-root CoW snapshots under `.fireforge/trees/<name>`
70
- with NO merge-back model. Enforcement is layered: the `.fireforge/
71
- tree.json` marker plus a default-deny command-verdict table
72
- (`src/core/tree-guard.ts`) refuse every mutating command through one
73
- commander `preAction` hook in `cli.ts` — a newly added command is
74
- refused in trees until classified (drift test). Because a tree is a
75
- full project root, its build and engine-session locks key on the TREE
76
- root, so in-tree reads never contend with the primary. `tree create`
77
- snapshots under the PRIMARY engine-session lock (never captures a
78
- mid-mutation state) — and with `--with-objdir` additionally under the
79
- primary BUILD lock, under which the objdir is resolved and its build
80
- artifacts validated immediately before cloning (any pre-lock probe is
81
- a fast-fail courtesy whose result is discarded, never the
82
- authoritative answer), so a concurrent `fireforge build` can neither
83
- tear the obj-\* snapshot nor swap it between preflight and clone.
84
- A symlinked (or engine-escaping) primary obj-\* is refused BEFORE any
85
- copying: every `cp` mode the clone uses preserves symlinks, so the
86
- clone would carry a link back to the original build and the mozinfo
87
- rewrite plus `_virtualenvs` removal would mutate that original
88
- through it — `assertCloneSafeObjdir` (tree-store.ts) checks lstat and
89
- realpath containment (lexical `resolve()` cannot see either) on the
90
- primary objdir and again on the tree's copy before any write goes
91
- through it, pinned by `tree-store.integration.test.ts` and
92
- `tree.integration.test.ts`;
93
- tree create/remove serialise on a primary-side
94
- `.fireforge/trees.lock`; `tree remove` refuses while a live PID holds
95
- a tree lock and path-contains its `rm -rf` target.
61
+
62
+ 5. **No lock outlives the process it belongs to** (best effort, in two
63
+ layers): the signal pipeline force-releases furnace locks before exiting,
64
+ and stale-lock recovery (PID first, then age) unblocks the next command
65
+ for anything that escaped.
66
+
67
+ 6. **Destructive patch operations are gated and audited.** That means an
68
+ explicit change summary, a confirmation (or `--yes`), `--dry-run`, a hard
69
+ refusal on structural conflicts (`--force-unsafe` to override), and a
70
+ JSONL history entry appended only after the mutation succeeded. This
71
+ covers metadata mutations too: `patch tier` and `patch lint-ignore` route
72
+ through `confirmDestructive`. They used to accept `--yes` without ever
73
+ prompting, so the flag only showed up in the history record. Enforced by
74
+ `patch-tier-and-lint-ignore.integration.test.ts`.
75
+
76
+ 7. **A verification tree never mutates shared state.** Trees are full
77
+ project-root CoW snapshots under `.fireforge/trees/<name>`, with no
78
+ merge-back model.
79
+
80
+ Enforcement is layered. The `.fireforge/tree.json` marker plus a
81
+ default-deny command-verdict table (`src/core/tree-guard.ts`) refuse every
82
+ mutating command through one commander `preAction` hook in `cli.ts`, so a
83
+ newly added command is refused in trees until someone classifies it (there
84
+ is a drift test for this). Because a tree is a full project root, its
85
+ build and engine-session locks key on the tree root, so in-tree reads
86
+ never contend with the primary.
87
+
88
+ `tree create` snapshots under the primary engine-session lock, so it never
89
+ captures a mid-mutation state. With `--with-objdir` it additionally holds
90
+ the primary build lock, under which the objdir is resolved and its build
91
+ artifacts validated immediately before cloning. Any probe before the lock
92
+ is a fast-fail courtesy whose result is discarded, never the authoritative
93
+ answer. A concurrent `fireforge build` can therefore neither tear the
94
+ `obj-*` snapshot nor swap it between preflight and clone.
95
+
96
+ A symlinked (or engine-escaping) primary `obj-*` is refused before any
97
+ copying. Every `cp` mode the clone uses preserves symlinks, so the clone
98
+ would carry a link back to the original build, and the mozinfo rewrite
99
+ plus `_virtualenvs` removal would mutate that original through it.
100
+ `assertCloneSafeObjdir` (`tree-store.ts`) checks lstat and realpath
101
+ containment, which a lexical `resolve()` cannot see, on the primary objdir
102
+ and again on the tree's copy before any write goes through it. This is
103
+ pinned by `tree-store.integration.test.ts` and `tree.integration.test.ts`.
104
+ Tree create and remove serialise on a primary-side `.fireforge/trees.lock`,
105
+ and `tree remove` refuses while a live PID holds a tree lock and checks
106
+ that its `rm -rf` target is contained in the tree path.
96
107
 
97
108
  In-tree `test` (0.41.0) is the one conditional carve-out, and it is
98
- marker-gated: mach objdirs embed absolute `topsrcdir`/`topobjdir`
109
+ marker-gated. mach objdirs embed absolute `topsrcdir` and `topobjdir`
99
110
  paths, so `tree create --with-objdir` rewrites the cloned objdir's
100
- `mozinfo.json` to the tree via the same safe-relocation rewriter
101
- `build --rewrite-mozinfo` uses, scrubs the cloned `_virtualenvs`
102
- (primary shebangs; mach rebuilds them in-tree), runs `mach configure`
103
- inside the tree so the configure-generated root files are regenerated
104
- against the tree's paths rather than retaining the primary's — and
105
- verifies exactly that set afterwards (config.status, backend.mk,
106
- Makefile, config/autoconf.mk, via `findObjdirRelocationViolation`,
107
- pinned by `mach-objdir-relocation.test.ts`; nested Makefiles are
108
- products of the verified config.status, and `.deps` build products
109
- are out of scope as read-only staleness corrected by the first
110
- in-tree rebuild) — and copies
111
- `last-build.json` so the in-tree stale-build gates anchor correctly.
112
- Both relocation steps are fail-closed — a rewrite refusal or a failed
113
- in-tree configure aborts the create and removes the partial tree —
114
- and only after both succeed is the marker's `clonedObjdir` field
115
- written, which is what the guard's `test` predicate consults. At run
116
- time, `test` additionally refuses when the objdir its preflight found
117
- is not the one the marker vouches for
118
- (`assertObjdirMatchesTreeMarker`), so an objdir that appeared through
119
- any path other than `tree create --with-objdir` is never run against.
120
- `test --build` stays refused (a tree is a snapshot with no merge-back
121
- model), and a corrupt marker refuses `test` like every conditional
122
- verdict.
123
-
124
- **Both halves of that enforcement fail CLOSED on an unreadable state,
125
- because "we cannot tell" is not "there is nothing there."** A marker
126
- that exists but does not parse reports `corrupt`, not `absent`, and
127
- refuses the command — reading it as absent granted a snapshot the
128
- full mutating command set on the strength of a file that failed to
129
- parse. A lock directory whose owner cannot be identified reports
130
- `unknown`, not free, and refuses removal — `withFileLock` creates the
131
- lock directory before writing its owner record and treats that write
132
- as non-fatal, so a live holder legitimately produces a lock with no
133
- readable PID (a lock directory that vanished between the probe and the
134
- owner read is the one exception: that is a clean release, and reads as
135
- free). Each has one named escape (`--ignore-corrupt-tree-marker`,
136
- `tree remove --force`); neither has a silent one. The corrupt-marker
137
- refusal spares the unconditionally-**allowed** read-only commands
111
+ `mozinfo.json` to the tree using the same safe-relocation rewriter that
112
+ `build --rewrite-mozinfo` uses, scrubs the cloned `_virtualenvs` (they
113
+ carry primary shebangs, and mach rebuilds them in-tree), and runs
114
+ `mach configure` inside the tree so the configure-generated root files are
115
+ regenerated against the tree's paths rather than keeping the primary's. It
116
+ then verifies exactly that set afterwards (config.status, backend.mk,
117
+ Makefile, config/autoconf.mk, via `findObjdirRelocationViolation`, pinned
118
+ by `mach-objdir-relocation.test.ts`). Nested Makefiles are products of the
119
+ verified config.status, and `.deps` build products are out of scope as
120
+ read-only staleness that the first in-tree rebuild corrects. Finally it
121
+ copies `last-build.json` so the in-tree stale-build gates anchor
122
+ correctly.
123
+
124
+ Both relocation steps fail closed: a rewrite refusal or a failed in-tree
125
+ configure aborts the create and removes the partial tree. Only after both
126
+ succeed is the marker's `clonedObjdir` field written, and that field is
127
+ what the guard's `test` predicate consults. At run time, `test`
128
+ additionally refuses when the objdir its preflight found is not the one
129
+ the marker vouches for (`assertObjdirMatchesTreeMarker`), so an objdir
130
+ that appeared through any path other than `tree create --with-objdir` is
131
+ never run against. `test --build` stays refused, since a tree is a
132
+ snapshot with no merge-back model, and a corrupt marker refuses `test`
133
+ like every other conditional verdict.
134
+
135
+ **Both halves of that enforcement fail closed on an unreadable state,
136
+ because "we cannot tell" is not "there is nothing there."** A marker that
137
+ exists but does not parse reports `corrupt`, not `absent`, and refuses the
138
+ command. Reading it as absent granted a snapshot the full mutating command
139
+ set on the strength of a file that failed to parse. A lock directory whose
140
+ owner cannot be identified reports `unknown`, not free, and refuses
141
+ removal. Since 0.45.1 `withFileLock` treats its owner-record write as
142
+ fatal (`writeLockOwner` in `file-lock-owner.ts` removes the just-created
143
+ directory and throws a `GeneralError` if the record cannot be written or
144
+ does not read back as written), so an owner-less lock is only ever left by
145
+ a pre-0.45.1 holder or by a crash inside that write. It is still refused,
146
+ for the same reason. The one exception is a lock directory that vanished
147
+ between the probe and the owner read: that is a clean release, and reads
148
+ as free.
149
+
150
+ Each case has one named escape (`--ignore-corrupt-tree-marker`,
151
+ `tree remove --force`), and neither has a silent one. The corrupt-marker
152
+ refusal spares the read-only commands that are unconditionally allowed
138
153
  (`status`, `lint`, `typecheck`, `verify`): their verdict never consults
139
154
  marker fields, so an unreadable marker cannot change their answer, and
140
155
  blocking them left the operator unable to diagnose the very tree that
141
- needs inspecting. `conditional` verdicts can write and stay refused.
142
- Enforced by
143
- `tree-guard.test.ts` (verdict drift gate + corrupt-marker refusal),
144
- `tree-store.integration.test.ts` (marker tri-state, unknown-owner
145
- refusal, rewrite-then-reconfigure-then-marker ordering),
156
+ needs inspecting. `conditional` verdicts can write, and stay refused.
157
+ Enforced by `tree-guard.test.ts` (verdict drift gate and corrupt-marker
158
+ refusal), `tree-store.integration.test.ts` (marker tri-state,
159
+ unknown-owner refusal, rewrite-then-reconfigure-then-marker ordering),
146
160
  `tree.test.ts` (under-lock objdir re-validation ordering) and
147
161
  `tree.integration.test.ts` (real-program refusal).
148
162
 
149
- 8. **A verdict is only reported when it was actually verified.** `fireforge
150
- test` brackets the harness run with `snapshotEngineGeneration` and
151
- fails the run if `engine/` changed underneath it. The probe can
152
- legitimately fail — `engine/` need not be a git checkout — so a probe
153
- that fails on **both** sides warns and proceeds; a one-sided
154
- transition throws, because either the second probe measured nothing
155
- about a checkout that had something to measure, or there was no
156
- baseline to compare against. Enforced by
157
- `engine-session-lock.test.ts`.
158
-
159
- A probe that fails on a **transient** `.git/index.lock` — another
160
- writer held the index for the instant we looked — is retried on a
161
- bounded budget rather than latched as "unmeasurable";
162
- a durable failure (a non-git `engine/`, an unreadable `.git`) still
163
- reports immediately. Enforced by
163
+ 8. **A verdict is only reported when it was actually verified.**
164
+ `fireforge test` brackets the harness run with `snapshotEngineGeneration`
165
+ and fails the run if `engine/` changed underneath it. The probe can
166
+ legitimately fail, since `engine/` need not be a git checkout, so a probe
167
+ that fails on both sides warns and proceeds. A one-sided transition
168
+ throws, because either the second probe measured nothing about a checkout
169
+ that had something to measure, or there was no baseline to compare
170
+ against. Enforced by `engine-session-lock.test.ts`.
171
+
172
+ A probe that fails on a transient `.git/index.lock`, meaning another
173
+ writer held the index for the instant we looked, is retried on a bounded
174
+ budget rather than latched as unmeasurable. A durable failure (a non-git
175
+ `engine/`, an unreadable `.git`) still reports immediately. Enforced by
164
176
  `engine-generation-probe-retry.test.ts`.
165
177
 
166
- The same invariant reaches across the `tree exec` boundary.
167
- `tree exec` hands stdout to the child (`stdio: 'inherit'`), so the
168
- child's `FIREFORGE-VERDICT:` line reaches the caller byte-for-byte; the
169
- parent therefore seals stdout the moment the spawn settles, so its own
170
- "exited with code N" refusal cannot print after the verdict and displace
171
- it. A refusal raised BEFORE the child spawns emits no verdict at all —
172
- consumers must read a missing verdict as a failed step, never a pass.
178
+ The same invariant reaches across the `tree exec` boundary. `tree exec`
179
+ hands stdout to the child (`stdio: 'inherit'`), so the child's
180
+ `FIREFORGE-VERDICT:` line reaches the caller byte for byte. The parent
181
+ therefore seals stdout the moment the spawn settles, so its own "exited
182
+ with code N" refusal cannot print after the verdict and displace it. A
183
+ refusal raised before the child spawns emits no verdict at all, so
184
+ consumers have to read a missing verdict as a failed step, never a pass.
173
185
 
174
186
  9. **FireForge's own read-only commands are not writers of the primary
175
- engine checkout**. `verify`, `lint --per-patch`, and
176
- `typecheck` are read-only to the operator but not to git: `git status`
177
- and `git diff HEAD` refresh and rewrite `.git/index`, and the
178
- untracked-binary diff path stages and unstages for real. Each of those
179
- commands therefore runs its whole body inside `withPrivateGitIndex`,
180
- which seeds a temp `GIT_INDEX_FILE` from the repository's own index so
181
- every refresh lands there and is discarded; `typecheck` additionally
182
- forces `incremental: false` and drops `tsBuildInfoFile`, so no sidecar
183
- is written under `engine/`. The scope is fail-open — an unresolvable
184
- git dir simply does not install it — and non-reentrant. This exists so
187
+ engine checkout.** `verify`, `lint --per-patch` and `typecheck` are
188
+ read-only to the operator but not to git: `git status` and `git diff HEAD`
189
+ refresh and rewrite `.git/index`, and the untracked-binary diff path
190
+ stages and unstages for real. Each of those commands therefore runs its
191
+ whole body inside `withPrivateGitIndex`, which seeds a temp
192
+ `GIT_INDEX_FILE` from the repository's own index so that every refresh
193
+ lands there and is discarded. `typecheck` additionally forces
194
+ `incremental: false` and drops `tsBuildInfoFile`, so no sidecar is written
195
+ under `engine/`. The scope fails open, meaning an unresolvable git dir
196
+ simply does not install it, and it is not reentrant. This exists so that
185
197
  invariant 8 stays evidential: a concurrent `fireforge test` must not be
186
198
  invalidated by FireForge's own read-only lane. Enforced by
187
- `git-readonly-index.test.ts` (real repository, real index mtime).
188
-
189
- 10. **Lock queues are observable, and leaving one is as reliable as
190
- joining it**. A waiter that contends registers a file in
191
- `<lock>.waiters/` named `<startedAtMs>-<pid>-<uuid>` and deregisters
192
- in a `finally` that covers both outcomes — acquired the lock, or timed
193
- out. Depth counts only entries whose PID is still alive, so a killed
194
- waiter cannot inflate it forever, and the registry is advisory:
195
- failures to register are logged verbosely and never fail an
196
- acquisition. `fireforge status --lock` and the `--wait-lock` progress
197
- line read it. Enforced by `lock-visibility.test.ts` (real concurrent
198
- waiter) and `engine-session-lock.test.ts` (queue position in the wait
199
- line).
199
+ `git-readonly-index.test.ts`, against a real repository and a real index
200
+ mtime.
201
+
202
+ 10. **Lock queues are observable, and leaving one is as reliable as joining
203
+ it.** A waiter that contends registers a file in `<lock>.waiters/` named
204
+ `<startedAtMs>-<pid>-<uuid>` and deregisters in a `finally` that covers
205
+ both outcomes: acquired the lock, or timed out. Depth counts only entries
206
+ whose PID is still alive, so a killed waiter cannot inflate it forever,
207
+ and the registry is advisory: failures to register are logged verbosely
208
+ and never fail an acquisition. `fireforge status --lock` and the
209
+ `--wait-lock` progress line read it. Enforced by `lock-visibility.test.ts`
210
+ (a real concurrent waiter) and `engine-session-lock.test.ts` (queue
211
+ position in the wait line).
200
212
 
201
213
  11. **A manifest repair preserves what it cannot recompute, or refuses.**
202
- `doctor --repair-patches-manifest` is a MERGE: only `filesAffected` and
214
+ `doctor --repair-patches-manifest` is a merge: only `filesAffected` and
203
215
  `order` are derived from the patch files, and every other field on an
204
- existing entry is carried forward by spreading it, so a field a `.patch`
205
- body cannot express — `stagedDependencies`, `lintIgnore`, `tier`, and
206
- whatever is added to `PatchMetadata` next — survives by construction
207
- rather than by being listed. The one case where nothing can be carried
208
- forward is a `patches.json` that exists but does not parse; the rebuild
209
- refuses it, naming what would be reinvented, unless
210
- `--allow-metadata-loss` is passed. `doctor --repair-files-affected` is
211
- the narrow repair for drift that is only in the derived list: it goes
212
- through `mutatePatchRowsInManifest`, so untouched rows keep their exact
213
- JSON. Both repairs take the patch-directory lock (invariant 2), both
214
- support `--dry-run`, and a repair that wrote is reported before the
215
- doctor summary in every branch — the run can still exit non-zero on an
216
- unrelated check, and a non-zero exit otherwise reads as "nothing
217
- happened". Enforced by `patch-manifest-repair.test.ts` (real temp
218
- queues) and the repair cases in `doctor.test.ts`.
216
+ existing entry is carried forward by spreading it. A field a `.patch`
217
+ body cannot express (`stagedDependencies`, `lintIgnore`, `tier`, and
218
+ whatever is added to `PatchMetadata` next) therefore survives without
219
+ having to be listed. The one case where nothing can be carried forward is
220
+ a `patches.json` that exists but does not parse. The rebuild refuses it,
221
+ naming what would be reinvented, unless `--allow-metadata-loss` is
222
+ passed. `doctor --repair-files-affected` is the narrow repair for drift
223
+ that is only in the derived list: it goes through
224
+ `mutatePatchRowsInManifest`, so untouched rows keep their exact JSON.
225
+ Both repairs take the patch-directory lock (invariant 2), both support
226
+ `--dry-run`, and a repair that wrote is reported before the doctor
227
+ summary in every branch. The run can still exit non-zero on an unrelated
228
+ check, and a non-zero exit otherwise reads as "nothing happened".
229
+ Enforced by `patch-manifest-repair.test.ts` (real temp queues) and the
230
+ repair cases in `doctor.test.ts`.
219
231
 
220
232
  ## Lock release (0.44.0)
221
233
 
222
- `releaseLockIfOwned(lockPath)` in `src/core/file-lock.ts` is the ownership-
223
- checked removal for callers that hold no acquisition token — currently the
224
- signal-time furnace sweeper (`forceReleaseFurnaceLocksForActiveOperations`).
225
- It verifies the PID before removing, which is the half of `releaseLock`'s
226
- guard a sweeper can still perform: the sweeper knows which lock paths its own
227
- operations opened but not the per-acquisition UUID they minted. The bare `rm`
228
- it replaced could delete a lock a DIFFERENT process had acquired in the window
229
- between our operation dying and the sweep running.
234
+ `releaseLockIfOwned(lockPath)` in `src/core/file-lock.ts` is the
235
+ ownership-checked removal for callers that hold no acquisition token.
236
+ Currently that is the signal-time furnace sweeper
237
+ (`forceReleaseFurnaceLocksForActiveOperations`). It verifies the PID before
238
+ removing, which is the half of `releaseLock`'s guard a sweeper can still
239
+ perform: the sweeper knows which lock paths its own operations opened, but
240
+ not the per-acquisition UUID they minted. The bare `rm` it replaced could
241
+ delete a lock that a different process had acquired in the window between our
242
+ operation dying and the sweep running.
230
243
 
231
244
  `completeJournalRollback` in `src/core/furnace-operation.ts` owns the
232
245
  mutation-body rollback sequence that eight furnace commands previously
233
246
  duplicated verbatim. The ordering is the invariant: `markRolledBack()` runs
234
- BEFORE the restore, so a restore that itself fails does not leave the
235
- lifecycle wrapper replaying the same journal on the way out; and the ORIGINAL
247
+ before the restore, so a restore that itself fails does not leave the
248
+ lifecycle wrapper replaying the same journal on the way out, and the original
236
249
  error is rethrown unless the rollback is what failed.
237
250
 
238
251
  ## Who owns what
239
252
 
240
- | Module | Owns |
241
- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
242
- | `bin/fireforge.ts` | The only `process.exit` call sites; SIGINT/SIGTERM pipeline; re-entrancy guard (a second Ctrl+C exits immediately); exit codes 130/143. |
243
- | `src/core/file-lock.ts` | Generic directory-as-lock (`mkdir` atomicity), owner PID **+ per-acquisition token** file, stale recovery via **rename-aside reaping** (dead PID unblocks immediately; ageless fallback after 5 min; the probe re-runs every ~5 s while waiting, so a holder dying mid-wait cannot strand waiters until `timeoutMs`), poll + timeout. Release verifies the lock still carries this acquisition's token before removing — a reaper-replaced lock is never deleted by its previous owner. Enforced by `file-lock.test.ts` (two-waiter reap race, mid-wait holder death, foreign-owner release). Releases in `finally` — which never runs across `process.exit`, hence invariant 5's other layer. Opt-in wait ergonomics: `pollMaxMs` (poll interval doubles from `pollMs` up to the cap) and `waitProgressMs`/`onWaitProgress` (periodic progress callback carrying the holder's PID, liveness, and owner-metadata lines from the pid file). All three are inert when unset — existing callers keep byte-identical behavior. |
244
- | `src/core/engine-session-lock.ts` | Project-wide engine-session lock under `.fireforge/`, stamped with PID, command, and start time metadata. CLI `build`, `test`, `export`, `re-export`, `furnace apply`, `furnace deploy`, and `furnace sync` take it before subsystem locks — unconditionally, dry-runs included: a `re-export --dry-run` over an untracked binary legally touches the git index (temporary `--intent-to-add` staging), so holding the lock serializes even "read-only" previews against real writers. Dry-run purity is separately enforced at runtime by `withDryRunPurityGuard` (`re-export-bulk-scan.ts`), which fingerprints the engine tree and every patch artifact around the dry-run and fails hard on divergence. Contention fails fast after ~1 s by default with the fixed message "Another FireForge engine-mutating command is already running. Wait for it to finish, then retry `<command>`." — that message contract is unchanged and also closes the bounded wait. Each of those commands accepts `--wait-lock [seconds]` (bare flag = 60, valid 1..3600) for a bounded operator wait instead: polling backs off exponentially 100 ms → 2 s, and roughly every 5 s a progress line identifies the holder from the lock's owner-metadata lines (`Waiting for the FireForge engine lock held by PID 12345 (command=build, started=…) — 10s of up to 60s.`). The budget is not the engine lock's alone: it covers EVERY lock the command takes, which for the three furnace commands means the per-root furnace lock as well (see the `furnace-operation.ts` row). `FIREFORGE_WAIT_LOCK=<seconds>` sets the same budget for a whole session, parsed by the same bounds as the flag so the two cannot drift; an explicit budget at a call site always wins. Commands that take no lock accept `--wait-lock` and ignore it, so a scripted sequence can blanket-append the flag without a usage error killing the run. `fireforge test` also snapshots the engine git generation before and after the harness run and fails the verdict as invalid if the tree changes mid-run. |
245
- | `src/core/furnace-operation.ts` | `runFurnaceMutation` lifecycle wrapper: per-root furnace lock, `pendingRepair` pre-flight refusal, in-flight operation registry (journal + cleanups + `completed` flag), signal rollback with bounded timeouts, `pendingRepair` persistence, furnace-lock force-release sweep. The lock's wait budget is `options.lockTimeoutMs` (threaded from `--wait-lock` by `waitLockMutationOptions`, which does the seconds→ms conversion once) falling back to `sessionWaitLockMs()` (`FIREFORGE_WAIT_LOCK`) and then to `DEFAULT_LOCK_TIMEOUT_MS`. The fallback is what covers the twelve furnace mutators that declare no flag (`create`, `remove`, `rename`, `refresh`, `scan`, `override`, `chrome-doc`, …). A contended refusal here names the FURNACE lock, says it is the second of two, and points at `--wait-lock`/`FIREFORGE_WAIT_LOCK` — a budget that is paid in full and then refuses anyway is the worst shape a budget can have, so the refusal must not read as the engine lock's. |
246
- | `src/core/furnace-rollback.ts` | The rollback journal itself: file snapshots and their restoration. |
247
- | `src/utils/assert.ts` | The runtime-assertion primitive (`assert`, `assertDefined`, `expectDefined`) and, through `InternalInvariantError`, exit code 11. Checks state FireForge itself established — journal-before-mutation, lock-held-before-write, manifest order well-formedness — and is deliberately NOT input validation: anything derived from user input, the filesystem, or a subprocess stays a typed error from `src/errors/`. Every check routes through the helper rather than an inline `if (!c) throw` so the failure branch is covered once here instead of uncovered at every call site. |
248
- | `src/core/signal-critical.ts` | Registry of signal-deferred critical sections (`runInSignalCriticalSection`). Pure registry — installs no handlers; the bin pipeline drains it with a bounded wait. |
249
- | `src/core/patch-lock.ts` | `withPatchDirectoryLock` — serializes patch filename allocation and manifest read-modify-writes. **Not reentrant**: callers must not already hold it. |
250
- | `src/core/destructive.ts` | The destructive-operation contract: `confirmDestructive` (summary, prompt, `--yes`, `--dry-run`, conflict refusal, `--force-unsafe`) and the `.fireforge-history.jsonl` audit log. |
251
- | `src/core/tree-store.ts` | Verification-tree lifecycle: CoW snapshot contents/exclusions, the objdir clone relocation (mozinfo rewrite + caller-supplied in-tree `mach configure`, both before the marker records `clonedObjdir`; `assertObjdirMatchesTreeMarker` re-checks at `test` time), the `.fireforge/tree.json` marker and its absent/valid/**corrupt** tri-state, staleness fingerprints, the primary-side `trees.lock`, and removal that refuses both a live holder and a lock of **unknown** ownership, with rm-target containment. |
252
- | `src/core/tree-guard.ts` | Read-only enforcement inside trees: the default-deny per-command verdict table and the `preAction` hook body `runTreeGuardHook` installed by `createProgram()` (invariant 7). |
253
+ ### `bin/fireforge.ts`
254
+
255
+ The only `process.exit` call sites, the SIGINT/SIGTERM pipeline, the
256
+ re-entrancy guard (a second Ctrl+C exits immediately), and exit codes 130 and 143.
257
+
258
+ ### `src/core/file-lock.ts`
259
+
260
+ The generic directory-as-lock, using `mkdir` for atomicity. The owner file
261
+ carries the PID, a per-acquisition token, the acquisition time and the start
262
+ tick. Stale recovery works by renaming the lock aside and reaping it: a dead
263
+ PID unblocks immediately, with an age-based fallback after 5 minutes for
264
+ records with no usable liveness answer. The probe re-runs about every 5
265
+ seconds while waiting, so a holder dying mid-wait cannot strand waiters until
266
+ `timeoutMs`. Acquisition then polls until the timeout.
267
+
268
+ The owner-record write is fatal. A failed or unverifiable write releases the
269
+ just-created lock and throws a `GeneralError` instead of running the
270
+ operation. Without that, a lock with no readable owner is reaped by the age
271
+ heuristic after 5 minutes, which for a multi-hour build meant a second holder
272
+ inside the critical section. Covered by `file-lock.test.ts`, "owner record
273
+ write at acquisition": write failure, corrupt read-back, recorded acquisition
274
+ time.
275
+
276
+ Release verifies that the lock still carries this acquisition's token before
277
+ removing, so a lock that a reaper has replaced is never deleted by its
278
+ previous owner. Enforced by `file-lock.test.ts` (two-waiter reap race,
279
+ mid-wait holder death, foreign-owner release). Releases happen in a `finally`,
280
+ which never runs across `process.exit`, and that is why invariant 5 needs its
281
+ other layer.
282
+
283
+ Wait ergonomics are opt-in: `pollMaxMs` (the poll interval doubles from
284
+ `pollMs` up to the cap) and `waitProgressMs` with `onWaitProgress` (a periodic
285
+ progress callback carrying the holder's PID, liveness, and owner-metadata
286
+ lines from the pid file). All three are inert when unset, so existing callers
287
+ keep byte-identical behavior.
288
+
289
+ ### `src/core/file-lock-owner.ts`
290
+
291
+ The owner record (the `pid` file) itself: its format (PID, token,
292
+ `acquired-at-ms=` and `start-tick=` lines, plus free-form metadata, and older
293
+ records without the mechanical lines still parse), the write-and-verify
294
+ contract described above (a read-back that errors is retried before it counts
295
+ as a failed write, while a missing or mismatching record fails immediately),
296
+ and PID-reuse liveness.
297
+
298
+ `isLockOwnerAlive` is the liveness check that every stale probe, reap
299
+ re-verification, status snapshot and timeout message uses. Beyond
300
+ `isProcessAlive`, on Linux the record carries the writer's boot-relative start
301
+ tick (`/proc/self/stat` field 22), and the probe reads the candidate's tick
302
+ from `/proc/<pid>/stat`. A different tick means a different process, so the
303
+ lock is stale. A crashed build's PID handed to a long-lived daemon therefore
304
+ no longer keeps the dead lock honoured until the waiter's timeout. It uses
305
+ ticks rather than wall-clock times because `btime` moves with clock steps, and
306
+ a wall-clock comparison would reap a live holder after NTP steps the clock
307
+ forward. macOS and Windows have no cheap start-time source (a `ps` spawn per
308
+ poll is not worth it) and keep the PID-only answer, and so do records without
309
+ a start tick. Enforced by `file-lock-owner.test.ts`: backward-compatible
310
+ parsing, forged mechanical lines dropped, transient read-back error retried
311
+ versus persistent error versus mismatch, fake-procfs tick parsing, reused-PID
312
+ versus genuine-holder verdicts, stepped-clock immunity, and on Linux a
313
+ real-procfs `withFileLock` reap of a live-PID stale record.
314
+
315
+ ### `src/core/engine-session-lock.ts`
316
+
317
+ The project-wide engine-session lock under `.fireforge/`, stamped with PID,
318
+ command, and start time metadata. CLI `build`, `test`, `export`, `re-export`,
319
+ `furnace apply`, `furnace deploy` and `furnace sync` take it before subsystem
320
+ locks, unconditionally, including dry runs: a `re-export --dry-run` over an
321
+ untracked binary legally touches the git index (temporary `--intent-to-add`
322
+ staging), so holding the lock serializes even "read-only" previews against
323
+ real writers. Dry-run purity is separately enforced at runtime by
324
+ `withDryRunPurityGuard` (`re-export-bulk-scan.ts`), which fingerprints the
325
+ engine tree and every patch artifact around the dry run and fails hard on
326
+ divergence.
327
+
328
+ Contention fails fast after about a second by default, with the fixed message
329
+ "Another FireForge engine-mutating command is already running. Wait for it to
330
+ finish, then retry `<command>`." That message contract is unchanged, and it
331
+ also closes the bounded wait. Each of those commands accepts
332
+ `--wait-lock [seconds]` (bare flag means 60, valid 1 to 3600) for a bounded
333
+ operator wait instead: polling backs off exponentially from 100 ms to 2 s, and
334
+ roughly every 5 s a progress line identifies the holder from the lock's
335
+ owner-metadata lines, in the form `Waiting for the FireForge engine lock held
336
+ by PID 12345 (command=build, started=…)` followed by how long the wait has
337
+ run and how large the budget is.
338
+
339
+ The budget is not the engine lock's alone. It covers every lock the command
340
+ takes, which for the three furnace commands means the per-root furnace lock as
341
+ well (see the `furnace-operation.ts` section). `FIREFORGE_WAIT_LOCK=<seconds>`
342
+ sets the same budget for a whole session, parsed with the same bounds as the
343
+ flag so the two cannot drift, and an explicit budget at a call site always
344
+ wins. Commands that take no lock accept `--wait-lock` and ignore it, so a
345
+ scripted sequence can append the flag everywhere without a usage error killing
346
+ the run. `fireforge test` also snapshots the engine git generation before and
347
+ after the harness run, and fails the verdict as invalid if the tree changes
348
+ mid-run.
349
+
350
+ ### `src/core/furnace-operation.ts`
351
+
352
+ The `runFurnaceMutation` lifecycle wrapper: the per-root furnace lock, the
353
+ `pendingRepair` preflight refusal, the in-flight operation registry (journal,
354
+ cleanups and a `completed` flag), signal rollback with bounded timeouts,
355
+ `pendingRepair` persistence, and the furnace-lock force-release sweep.
356
+
357
+ The lock's wait budget is `options.lockTimeoutMs`, threaded from `--wait-lock`
358
+ by `waitLockMutationOptions`, which does the seconds-to-milliseconds
359
+ conversion once. It falls back to `sessionWaitLockMs()`
360
+ (`FIREFORGE_WAIT_LOCK`) and then to `DEFAULT_LOCK_TIMEOUT_MS`. That fallback
361
+ is what covers the twelve furnace mutators that declare no flag (`create`,
362
+ `remove`, `rename`, `refresh`, `scan`, `override`, `chrome-doc`, and the
363
+ rest). A contended refusal here names the furnace lock, says it is the second
364
+ of two, and points at `--wait-lock` and `FIREFORGE_WAIT_LOCK`. A budget that
365
+ is paid in full and then refuses anyway is the worst shape a budget can have,
366
+ so the refusal must not read as the engine lock's.
367
+
368
+ ### `src/core/furnace-rollback.ts`
369
+
370
+ The rollback journal itself: file snapshots and their restoration.
371
+
372
+ ### `src/utils/assert.ts`
373
+
374
+ The runtime-assertion primitive (`assert`, `expectDefined`) and, through
375
+ `InternalInvariantError`, exit code 11. It checks state FireForge itself
376
+ established (journal before mutation, lock held before write, manifest order
377
+ well-formedness) and is deliberately not input validation: anything derived
378
+ from user input, the filesystem, or a subprocess stays a typed error from
379
+ `src/errors/`. Every check routes through the helper rather than an inline
380
+ `if (!c) throw`, so the failure branch is covered once here instead of being
381
+ uncovered at every call site.
382
+
383
+ ### `src/core/signal-critical.ts`
384
+
385
+ The registry of signal-deferred critical sections
386
+ (`runInSignalCriticalSection`). It is a pure registry and installs no
387
+ handlers. The bin pipeline drains it with a bounded wait.
388
+
389
+ ### `src/core/patch-lock.ts`
390
+
391
+ `withPatchDirectoryLock`, which serializes patch filename allocation and
392
+ manifest read-modify-writes. It is not reentrant: callers must not already
393
+ hold it.
394
+
395
+ ### `src/core/destructive.ts`
396
+
397
+ The destructive-operation contract: `confirmDestructive` (summary, prompt,
398
+ `--yes`, `--dry-run`, conflict refusal, `--force-unsafe`) and the
399
+ `.fireforge-history.jsonl` audit log.
400
+
401
+ ### `src/core/tree-store.ts`
402
+
403
+ Verification-tree lifecycle: CoW snapshot contents and exclusions, the objdir
404
+ clone relocation (mozinfo rewrite plus a caller-supplied in-tree
405
+ `mach configure`, both before the marker records `clonedObjdir`, with
406
+ `assertObjdirMatchesTreeMarker` re-checking at `test` time), the
407
+ `.fireforge/tree.json` marker and its absent/valid/corrupt tri-state,
408
+ staleness fingerprints, the primary-side `trees.lock`, and removal that
409
+ refuses both a live holder and a lock of unknown ownership, with containment
410
+ checks on the removal target.
411
+
412
+ ### `src/core/tree-guard.ts`
413
+
414
+ Read-only enforcement inside trees: the default-deny per-command verdict table
415
+ and the `preAction` hook body `runTreeGuardHook` that `createProgram()`
416
+ installs (invariant 7).
253
417
 
254
418
  ## The signal pipeline, end to end
255
419
 
256
- On SIGINT/SIGTERM, `bin/fireforge.ts`:
257
-
258
- 1. If a rollback is already in flight (second Ctrl+C), exits immediately.
259
- 2. Otherwise runs **in parallel** and waits for both:
260
- - `rollbackActiveOperationsForSignal(signal)` — per active furnace
261
- operation: cleanup callbacks (15 s bound each), journal restore (15 s
262
- bound), `pendingRepair` marker if anything failed or no journal was
263
- registered yet. Operations whose body already completed are skipped, so
264
- a signal in the finally-window can't roll back a committed mutation.
265
- - `waitForActiveCriticalSections(5 s)` — holds exit for in-flight
266
- "apply + persist" pairs (invariant 4), bounded so a stuck write cannot
267
- postpone the exit the user asked for.
268
- 3. Force-releases the furnace lock directories of active operations
269
- (`withFileLock`'s `finally` will never run past `process.exit`).
270
- 4. `process.exit(130 | 143)`.
420
+ On SIGINT or SIGTERM, `bin/fireforge.ts`:
421
+
422
+ 1. Exits immediately if a rollback is already in flight (a second Ctrl+C).
423
+ 2. Otherwise runs both of these in parallel and waits for both:
424
+ - `rollbackActiveOperationsForSignal(signal)`, which for each active
425
+ furnace operation runs the cleanup callbacks (15 s bound each), restores
426
+ the journal (15 s bound), and writes a `pendingRepair` marker if anything
427
+ failed or no journal was registered yet. Operations whose body already
428
+ completed are skipped, so a signal in the finally-window cannot roll back
429
+ a committed mutation.
430
+ - `waitForActiveCriticalSections(5 s)`, which holds the exit for in-flight
431
+ "apply plus persist" pairs (invariant 4). It is bounded so that a stuck
432
+ write cannot postpone the exit the user asked for.
433
+ 3. Force-releases the furnace lock directories of active operations, since
434
+ `withFileLock`'s `finally` will never run past `process.exit`.
435
+ 4. Calls `process.exit(130 | 143)`.
271
436
 
272
437
  ## Choosing a primitive for a new mutation
273
438
 
274
- | Your operation… | Use |
275
- | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
276
- | Mutates the engine and should be _undone_ on Ctrl+C (apply, deploy, preview, remove) | `runFurnaceMutation` + `ctx.registerJournal` (plus `ctx.registerCleanup` for extra teardown) |
277
- | Intentionally leaves the engine mutated but must finish a paired bookkeeping write (rebase-style `--continue` state) | `runInSignalCriticalSection` around the apply + persist pair — keep the body short |
278
- | Touches the patch directory / manifest | `withPatchDirectoryLock` (never nested) |
279
- | Deletes or rewrites user-owned patch state | The `confirmDestructive` contract, history appended only on success |
280
- | Only reads | Nothing — dry-runs must not block or be blocked |
281
- | Runs read verification concurrently beside a busy primary | A `fireforge tree` (CoW snapshot; mutation refused inside — invariant 7) |
439
+ | Your operation | Use |
440
+ | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
441
+ | Mutates the engine and should be undone on Ctrl+C (apply, deploy, preview, remove) | `runFurnaceMutation` plus `ctx.registerJournal` (and `ctx.registerCleanup` for extra teardown) |
442
+ | Intentionally leaves the engine mutated but must finish a paired bookkeeping write (rebase-style `--continue` state) | `runInSignalCriticalSection` around the apply plus persist pair, keeping the body short |
443
+ | Touches the patch directory or manifest | `withPatchDirectoryLock` (never nested) |
444
+ | Deletes or rewrites user-owned patch state | The `confirmDestructive` contract, with history appended only on success |
445
+ | Only reads | Nothing. Dry runs must not block or be blocked |
446
+ | Runs read verification concurrently beside a busy primary | A `fireforge tree` (a CoW snapshot, with mutation refused inside, invariant 7) |
282
447
 
283
448
  ## Where this is tested
284
449
 
285
- - `src/core/__tests__/file-lock.test.ts` — lock acquisition, staleness, PID
286
- recovery.
287
- - `src/core/__tests__/furnace-operation.test.ts` — wrapper lifecycle, signal
450
+ - `src/core/__tests__/file-lock.test.ts`: lock acquisition, staleness, PID
451
+ recovery, fatal owner-record write.
452
+ - `src/core/__tests__/file-lock-owner.test.ts`: owner-record format
453
+ compatibility and PID-reuse liveness.
454
+ - `src/core/__tests__/furnace-operation.test.ts`: wrapper lifecycle, signal
288
455
  rollback outcomes, `pendingRepair` semantics, lock force-release.
289
- - `src/core/__tests__/signal-critical.test.ts` — the critical-section
290
- registry contract in isolation.
291
- - `src/core/__tests__/signal-compound-mutation-scenario.test.ts` — the
292
- composed bin-handler pipeline: exit held for a compound mutation, bounded
293
- hold for a stuck section, rollback + drain + lock release together.
294
- - `src/core/__tests__/destructive.test.ts` — the destructive-op contract.
295
- - `src/core/__tests__/tree-guard.test.ts` — invariant 7's verdict table
456
+ - `src/core/__tests__/signal-critical.test.ts`: the critical-section registry
457
+ contract in isolation.
458
+ - `src/core/__tests__/signal-compound-mutation-scenario.test.ts`: the composed
459
+ bin-handler pipeline, covering exit held for a compound mutation, the
460
+ bounded hold for a stuck section, and rollback plus drain plus lock release
461
+ together.
462
+ - `src/core/__tests__/destructive.test.ts`: the destructive-operation
463
+ contract.
464
+ - `src/core/__tests__/tree-guard.test.ts`: invariant 7's verdict table
296
465
  (default-deny drift gate, conditional predicates, corrupt-marker refusal).
297
- - `src/core/__tests__/tree-store.integration.test.ts` — clone contents,
298
- objdir rewrite/reconfigure/marker ordering, marker tri-state,
299
- unknown-owner removal refusal.
300
- - `src/commands/__tests__/tree.test.ts` — create/remove command flow,
466
+ - `src/core/__tests__/tree-store.integration.test.ts`: clone contents, objdir
467
+ rewrite/reconfigure/marker ordering, marker tri-state, unknown-owner removal
468
+ refusal.
469
+ - `src/commands/__tests__/tree.test.ts`: create and remove command flow,
301
470
  including objdir re-validation under the primary build lock, and the
302
471
  `tree exec` stdout seal (invariant 8).
303
- - `src/__tests__/tree-exec-verdict.test.ts` — invariant 8 across a real
304
- process boundary: exactly one verdict line, last on stdout, refusal on
472
+ - `src/__tests__/tree-exec-verdict.test.ts`: invariant 8 across a real process
473
+ boundary, with exactly one verdict line, last on stdout, and the refusal on
305
474
  stderr.
306
- - `src/commands/__tests__/tree.integration.test.ts` — real-program in-tree
307
- refusals and `--with-objdir` end-to-end against a synthetic objdir.
308
- - `src/__tests__/process-boundary.test.ts` — invariant 1 (`process.exit`
309
- only in bin).
475
+ - `src/commands/__tests__/tree.integration.test.ts`: real-program in-tree
476
+ refusals and `--with-objdir` end to end against a synthetic objdir.