@hominis/fireforge 0.44.4 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (378) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/dist/bin/fireforge.js +6 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/src/cli-usage-error.d.ts +46 -0
  5. package/dist/src/cli-usage-error.js +81 -0
  6. package/dist/src/cli.d.ts +5 -1
  7. package/dist/src/cli.js +20 -21
  8. package/dist/src/commands/bootstrap.js +1 -2
  9. package/dist/src/commands/build.js +15 -6
  10. package/dist/src/commands/destructive-decision.d.ts +19 -0
  11. package/dist/src/commands/destructive-decision.js +31 -0
  12. package/dist/src/commands/doctor-check-core.d.ts +0 -6
  13. package/dist/src/commands/doctor-check-core.js +0 -8
  14. package/dist/src/commands/doctor-orphaned-harness.d.ts +0 -6
  15. package/dist/src/commands/doctor-orphaned-harness.js +4 -12
  16. package/dist/src/commands/doctor-post-rebase-audit.js +1 -1
  17. package/dist/src/commands/doctor-source-pin.d.ts +29 -0
  18. package/dist/src/commands/doctor-source-pin.js +81 -0
  19. package/dist/src/commands/doctor.d.ts +2 -0
  20. package/dist/src/commands/doctor.js +9 -8
  21. package/dist/src/commands/download.js +35 -24
  22. package/dist/src/commands/export-all.js +21 -4
  23. package/dist/src/commands/export-flow.js +15 -60
  24. package/dist/src/commands/export-placement-conflicts.js +3 -3
  25. package/dist/src/commands/export-placement-gate.js +2 -8
  26. package/dist/src/commands/export-placement-policy.js +3 -5
  27. package/dist/src/commands/export-shared.d.ts +42 -23
  28. package/dist/src/commands/export-shared.js +4 -19
  29. package/dist/src/commands/export.js +10 -1
  30. package/dist/src/commands/furnace/authoring-config.d.ts +13 -0
  31. package/dist/src/commands/furnace/authoring-config.js +19 -0
  32. package/dist/src/commands/furnace/chrome-doc-remove.js +1 -1
  33. package/dist/src/commands/furnace/create-browser-test.js +3 -8
  34. package/dist/src/commands/furnace/create-dry-run.js +2 -5
  35. package/dist/src/commands/furnace/create-templates.d.ts +17 -1
  36. package/dist/src/commands/furnace/create-templates.js +2 -1
  37. package/dist/src/commands/furnace/create.js +25 -19
  38. package/dist/src/commands/furnace/deploy.js +11 -2
  39. package/dist/src/commands/furnace/init.js +2 -1
  40. package/dist/src/commands/furnace/override.js +28 -19
  41. package/dist/src/commands/furnace/refresh.js +9 -1
  42. package/dist/src/commands/furnace/remove-mutations.d.ts +6 -7
  43. package/dist/src/commands/furnace/remove-mutations.js +0 -1
  44. package/dist/src/commands/furnace/remove.js +2 -6
  45. package/dist/src/commands/furnace/rename-browser-test.js +1 -7
  46. package/dist/src/commands/furnace/rename.js +29 -29
  47. package/dist/src/commands/furnace/status.js +12 -9
  48. package/dist/src/commands/furnace/test-file-name.d.ts +28 -0
  49. package/dist/src/commands/furnace/test-file-name.js +39 -0
  50. package/dist/src/commands/furnace/validation-output.d.ts +21 -9
  51. package/dist/src/commands/furnace/validation-output.js +3 -8
  52. package/dist/src/commands/import.js +2 -9
  53. package/dist/src/commands/lint-per-patch.d.ts +2 -2
  54. package/dist/src/commands/lint-per-patch.js +19 -4
  55. package/dist/src/commands/lint-per-run-checkjs.d.ts +2 -2
  56. package/dist/src/commands/lint.d.ts +2 -1
  57. package/dist/src/commands/lint.js +1 -1
  58. package/dist/src/commands/manifest.js +1 -1
  59. package/dist/src/commands/package.js +2 -4
  60. package/dist/src/commands/patch/compact.js +14 -15
  61. package/dist/src/commands/patch/delete.js +15 -25
  62. package/dist/src/commands/patch/entry-projection.d.ts +13 -0
  63. package/dist/src/commands/patch/entry-projection.js +24 -0
  64. package/dist/src/commands/patch/lint-ignore.d.ts +0 -8
  65. package/dist/src/commands/patch/lint-ignore.js +19 -29
  66. package/dist/src/commands/patch/move-files-create.js +24 -10
  67. package/dist/src/commands/patch/move-files-into.js +38 -37
  68. package/dist/src/commands/patch/move-files.js +4 -13
  69. package/dist/src/commands/patch/rename.js +27 -41
  70. package/dist/src/commands/patch/reorder.js +25 -38
  71. package/dist/src/commands/patch/split-plan.d.ts +4 -5
  72. package/dist/src/commands/patch/split-plan.js +4 -11
  73. package/dist/src/commands/patch/split.js +45 -43
  74. package/dist/src/commands/patch/staged-dependency.js +16 -22
  75. package/dist/src/commands/patch/tier.js +15 -25
  76. package/dist/src/commands/patch-rename-map.d.ts +19 -0
  77. package/dist/src/commands/patch-rename-map.js +36 -0
  78. package/dist/src/commands/pending-resolution.d.ts +15 -0
  79. package/dist/src/commands/pending-resolution.js +25 -0
  80. package/dist/src/commands/re-export-adjacent.d.ts +32 -2
  81. package/dist/src/commands/re-export-adjacent.js +51 -10
  82. package/dist/src/commands/re-export-bulk-scan.js +6 -10
  83. package/dist/src/commands/re-export-drift.d.ts +8 -0
  84. package/dist/src/commands/re-export-drift.js +65 -4
  85. package/dist/src/commands/re-export-files.d.ts +2 -3
  86. package/dist/src/commands/re-export-files.js +38 -32
  87. package/dist/src/commands/re-export-lint.d.ts +3 -4
  88. package/dist/src/commands/re-export-lint.js +9 -2
  89. package/dist/src/commands/re-export-options.js +19 -5
  90. package/dist/src/commands/re-export-register.js +5 -1
  91. package/dist/src/commands/re-export-scan.js +8 -8
  92. package/dist/src/commands/re-export-single.d.ts +22 -3
  93. package/dist/src/commands/re-export-single.js +19 -11
  94. package/dist/src/commands/re-export.js +44 -27
  95. package/dist/src/commands/rebase/abort.js +3 -8
  96. package/dist/src/commands/rebase/continue.js +12 -12
  97. package/dist/src/commands/rebase/index.js +2 -2
  98. package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
  99. package/dist/src/commands/rebase/patch-loop.js +2 -7
  100. package/dist/src/commands/resolve.js +18 -10
  101. package/dist/src/commands/run.d.ts +0 -13
  102. package/dist/src/commands/run.js +12 -25
  103. package/dist/src/commands/setup-support.d.ts +16 -4
  104. package/dist/src/commands/setup-support.js +16 -16
  105. package/dist/src/commands/setup.js +3 -9
  106. package/dist/src/commands/source.js +8 -1
  107. package/dist/src/commands/status-check.js +2 -2
  108. package/dist/src/commands/status-output.js +2 -2
  109. package/dist/src/commands/status.js +3 -4
  110. package/dist/src/commands/test-build-phase.d.ts +2 -2
  111. package/dist/src/commands/test-build-phase.js +33 -8
  112. package/dist/src/commands/test-diagnose.js +14 -5
  113. package/dist/src/commands/test-modes.d.ts +2 -2
  114. package/dist/src/commands/test-modes.js +0 -1
  115. package/dist/src/commands/test-register.js +6 -3
  116. package/dist/src/commands/test-run.js +29 -21
  117. package/dist/src/commands/test-stale-gate.js +8 -5
  118. package/dist/src/commands/test-verdict.d.ts +16 -2
  119. package/dist/src/commands/test-verdict.js +18 -3
  120. package/dist/src/commands/test.js +51 -7
  121. package/dist/src/commands/token-list.d.ts +26 -0
  122. package/dist/src/commands/token-list.js +153 -0
  123. package/dist/src/commands/token.js +17 -1
  124. package/dist/src/commands/tree.js +7 -2
  125. package/dist/src/commands/typecheck.js +9 -9
  126. package/dist/src/commands/wire.js +4 -3
  127. package/dist/src/core/branding.d.ts +0 -7
  128. package/dist/src/core/branding.js +3 -2
  129. package/dist/src/core/browser-wire.js +6 -3
  130. package/dist/src/core/build-audit-platform.d.ts +29 -0
  131. package/dist/src/core/build-audit-platform.js +140 -0
  132. package/dist/src/core/build-audit-registration.js +3 -2
  133. package/dist/src/core/build-audit-skip.d.ts +57 -0
  134. package/dist/src/core/build-audit-skip.js +112 -0
  135. package/dist/src/core/build-audit.d.ts +15 -2
  136. package/dist/src/core/build-audit.js +154 -12
  137. package/dist/src/core/build-baseline.d.ts +45 -28
  138. package/dist/src/core/build-baseline.js +6 -29
  139. package/dist/src/core/build-jar-escalation.d.ts +64 -0
  140. package/dist/src/core/build-jar-escalation.js +128 -0
  141. package/dist/src/core/build-overwrite-guard.js +1 -1
  142. package/dist/src/core/build-prepare.d.ts +5 -2
  143. package/dist/src/core/build-prepare.js +34 -6
  144. package/dist/src/core/config-paths.d.ts +2 -6
  145. package/dist/src/core/config-paths.js +4 -2
  146. package/dist/src/core/config-state.d.ts +0 -6
  147. package/dist/src/core/config-state.js +1 -19
  148. package/dist/src/core/config-validate-build-audit.d.ts +8 -0
  149. package/dist/src/core/config-validate-build-audit.js +80 -0
  150. package/dist/src/core/config-validate-file-size.d.ts +12 -0
  151. package/dist/src/core/config-validate-file-size.js +65 -0
  152. package/dist/src/core/config-validate-patch-policy.d.ts +2 -2
  153. package/dist/src/core/config-validate-patch-policy.js +3 -3
  154. package/dist/src/core/config-validate-test-toolchains.d.ts +2 -2
  155. package/dist/src/core/config-validate-test-toolchains.js +4 -5
  156. package/dist/src/core/config-validate.js +20 -3
  157. package/dist/src/core/config.d.ts +2 -2
  158. package/dist/src/core/config.js +11 -15
  159. package/dist/src/core/coverage-extend.js +4 -4
  160. package/dist/src/core/discard-baseline.js +7 -7
  161. package/dist/src/core/engine-precondition.d.ts +1 -9
  162. package/dist/src/core/engine-precondition.js +3 -8
  163. package/dist/src/core/engine-session-lock.js +13 -17
  164. package/dist/src/core/export-moved-code-shape.js +2 -2
  165. package/dist/src/core/file-lock-owner.d.ts +103 -0
  166. package/dist/src/core/file-lock-owner.js +259 -0
  167. package/dist/src/core/file-lock.d.ts +2 -7
  168. package/dist/src/core/file-lock.js +40 -91
  169. package/dist/src/core/firefox-cache.d.ts +13 -1
  170. package/dist/src/core/firefox-cache.js +59 -34
  171. package/dist/src/core/firefox-download.d.ts +15 -0
  172. package/dist/src/core/firefox-download.js +37 -21
  173. package/dist/src/core/firefox-extract.d.ts +6 -5
  174. package/dist/src/core/firefox-extract.js +36 -18
  175. package/dist/src/core/firefox.d.ts +30 -32
  176. package/dist/src/core/firefox.js +5 -36
  177. package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
  178. package/dist/src/core/furnace-apply-dry-run.js +1 -7
  179. package/dist/src/core/furnace-apply-helpers.d.ts +0 -11
  180. package/dist/src/core/furnace-apply-helpers.js +8 -19
  181. package/dist/src/core/furnace-apply-overwrite-warn.js +4 -2
  182. package/dist/src/core/furnace-apply.d.ts +1 -1
  183. package/dist/src/core/furnace-apply.js +30 -10
  184. package/dist/src/core/furnace-config.d.ts +1 -1
  185. package/dist/src/core/furnace-config.js +23 -36
  186. package/dist/src/core/furnace-constants.js +3 -2
  187. package/dist/src/core/furnace-dir-entry.d.ts +20 -0
  188. package/dist/src/core/furnace-dir-entry.js +21 -0
  189. package/dist/src/core/furnace-operation.d.ts +2 -0
  190. package/dist/src/core/furnace-operation.js +3 -1
  191. package/dist/src/core/furnace-refresh.d.ts +18 -6
  192. package/dist/src/core/furnace-refresh.js +3 -6
  193. package/dist/src/core/furnace-registration-ast.d.ts +0 -1
  194. package/dist/src/core/furnace-registration-ast.js +0 -3
  195. package/dist/src/core/furnace-registration.d.ts +2 -1
  196. package/dist/src/core/furnace-registration.js +22 -29
  197. package/dist/src/core/furnace-scanner.js +3 -6
  198. package/dist/src/core/furnace-state-persist.d.ts +2 -2
  199. package/dist/src/core/furnace-validate-registration.js +5 -7
  200. package/dist/src/core/git-base.d.ts +3 -1
  201. package/dist/src/core/git-base.js +25 -3
  202. package/dist/src/core/git-diff-new-file.d.ts +40 -0
  203. package/dist/src/core/git-diff-new-file.js +119 -0
  204. package/dist/src/core/git-diff.d.ts +9 -9
  205. package/dist/src/core/git-diff.js +88 -157
  206. package/dist/src/core/git-file-ops.d.ts +0 -7
  207. package/dist/src/core/git-file-ops.js +4 -28
  208. package/dist/src/core/git-readonly-index.d.ts +0 -2
  209. package/dist/src/core/git-readonly-index.js +0 -4
  210. package/dist/src/core/git-status.d.ts +7 -0
  211. package/dist/src/core/git-status.js +21 -9
  212. package/dist/src/core/git.d.ts +0 -6
  213. package/dist/src/core/git.js +14 -43
  214. package/dist/src/core/harness-orphans.d.ts +73 -0
  215. package/dist/src/core/harness-orphans.js +187 -0
  216. package/dist/src/core/history-log.d.ts +19 -0
  217. package/dist/src/core/history-log.js +29 -0
  218. package/dist/src/core/mach-error-hints.d.ts +0 -5
  219. package/dist/src/core/mach-error-hints.js +1 -1
  220. package/dist/src/core/mach-known-noise-filter.d.ts +25 -3
  221. package/dist/src/core/mach-known-noise-filter.js +31 -10
  222. package/dist/src/core/mach-mozconfig.js +2 -1
  223. package/dist/src/core/mach-python.d.ts +2 -0
  224. package/dist/src/core/mach-python.js +12 -7
  225. package/dist/src/core/mach-resource-shim.d.ts +13 -1
  226. package/dist/src/core/mach-resource-shim.js +112 -9
  227. package/dist/src/core/mach.d.ts +46 -53
  228. package/dist/src/core/mach.js +31 -83
  229. package/dist/src/core/marionette-port.d.ts +0 -17
  230. package/dist/src/core/marionette-port.js +12 -58
  231. package/dist/src/core/marionette-preflight.js +9 -27
  232. package/dist/src/core/mochitest-server-port.js +3 -3
  233. package/dist/src/core/moz-manifest-rules.js +37 -33
  234. package/dist/src/core/patch-apply-fuzz.js +4 -4
  235. package/dist/src/core/patch-apply.d.ts +4 -5
  236. package/dist/src/core/patch-apply.js +55 -32
  237. package/dist/src/core/patch-export-coverage.d.ts +0 -9
  238. package/dist/src/core/patch-export-coverage.js +1 -30
  239. package/dist/src/core/patch-export-update.d.ts +18 -1
  240. package/dist/src/core/patch-export-update.js +4 -3
  241. package/dist/src/core/patch-export.d.ts +11 -21
  242. package/dist/src/core/patch-export.js +44 -66
  243. package/dist/src/core/patch-files.d.ts +0 -4
  244. package/dist/src/core/patch-files.js +0 -11
  245. package/dist/src/core/patch-lint-binary.d.ts +6 -12
  246. package/dist/src/core/patch-lint-cache.d.ts +17 -1
  247. package/dist/src/core/patch-lint-cache.js +3 -6
  248. package/dist/src/core/patch-lint-checkjs.d.ts +33 -16
  249. package/dist/src/core/patch-lint-checkjs.js +43 -25
  250. package/dist/src/core/patch-lint-creators.d.ts +2 -6
  251. package/dist/src/core/patch-lint-cross.d.ts +16 -8
  252. package/dist/src/core/patch-lint-cross.js +49 -92
  253. package/dist/src/core/patch-lint-css.js +10 -6
  254. package/dist/src/core/patch-lint-file-size.d.ts +52 -0
  255. package/dist/src/core/patch-lint-file-size.js +53 -0
  256. package/dist/src/core/patch-lint-forward-registration.d.ts +58 -0
  257. package/dist/src/core/patch-lint-forward-registration.js +210 -0
  258. package/dist/src/core/patch-lint-jsdoc.js +36 -36
  259. package/dist/src/core/patch-lint-module-registration.d.ts +2 -8
  260. package/dist/src/core/patch-lint-prettier.d.ts +36 -0
  261. package/dist/src/core/patch-lint-prettier.js +126 -0
  262. package/dist/src/core/patch-lint-queue-types.d.ts +60 -0
  263. package/dist/src/core/patch-lint-queue-types.js +12 -0
  264. package/dist/src/core/patch-lint-staged-registration.d.ts +61 -0
  265. package/dist/src/core/patch-lint-staged-registration.js +90 -0
  266. package/dist/src/core/patch-lint-unmanaged-companion.d.ts +106 -0
  267. package/dist/src/core/patch-lint-unmanaged-companion.js +214 -0
  268. package/dist/src/core/patch-lint.d.ts +14 -9
  269. package/dist/src/core/patch-lint.js +27 -27
  270. package/dist/src/core/patch-lock.js +3 -3
  271. package/dist/src/core/patch-manifest-io.js +8 -7
  272. package/dist/src/core/patch-parse.d.ts +19 -8
  273. package/dist/src/core/patch-parse.js +25 -0
  274. package/dist/src/core/patch-policy.js +15 -27
  275. package/dist/src/core/patch-source-metadata.d.ts +1 -3
  276. package/dist/src/core/patch-source-metadata.js +0 -4
  277. package/dist/src/core/patch-transform.d.ts +21 -8
  278. package/dist/src/core/patch-transform.js +38 -10
  279. package/dist/src/core/process-cpu-time.js +1 -1
  280. package/dist/src/core/rebase-session.d.ts +2 -8
  281. package/dist/src/core/rebase-session.js +5 -17
  282. package/dist/src/core/register-browser-content.js +3 -2
  283. package/dist/src/core/register-browser-test.js +2 -1
  284. package/dist/src/core/register-module.js +2 -1
  285. package/dist/src/core/register-scaffold.js +5 -4
  286. package/dist/src/core/register-shared-css.js +2 -1
  287. package/dist/src/core/run-log-redact.d.ts +38 -0
  288. package/dist/src/core/run-log-redact.js +69 -0
  289. package/dist/src/core/run-log.d.ts +13 -2
  290. package/dist/src/core/run-log.js +67 -18
  291. package/dist/src/core/signal-critical.js +2 -4
  292. package/dist/src/core/smoke-patterns.d.ts +0 -1
  293. package/dist/src/core/smoke-patterns.js +18 -11
  294. package/dist/src/core/state-file.d.ts +12 -1
  295. package/dist/src/core/state-file.js +14 -1
  296. package/dist/src/core/status-classify.js +2 -1
  297. package/dist/src/core/test-changed-prefs.js +26 -11
  298. package/dist/src/core/test-harness-crash.js +61 -48
  299. package/dist/src/core/test-harness-verdict-notes.d.ts +55 -0
  300. package/dist/src/core/test-harness-verdict-notes.js +48 -0
  301. package/dist/src/core/test-stale-check.d.ts +23 -8
  302. package/dist/src/core/test-stale-check.js +77 -4
  303. package/dist/src/core/test-stall-triage.js +37 -19
  304. package/dist/src/core/test-xpcshell-retry.d.ts +26 -7
  305. package/dist/src/core/test-xpcshell-retry.js +14 -11
  306. package/dist/src/core/token-category.d.ts +33 -0
  307. package/dist/src/core/token-category.js +56 -48
  308. package/dist/src/core/token-dark-mode.js +2 -14
  309. package/dist/src/core/token-inventory.d.ts +56 -0
  310. package/dist/src/core/token-inventory.js +109 -0
  311. package/dist/src/core/token-manager.d.ts +1 -1
  312. package/dist/src/core/token-manager.js +1 -1
  313. package/dist/src/core/token-variant.js +21 -22
  314. package/dist/src/core/toolchain-preflight.js +1 -1
  315. package/dist/src/core/tree-cow.d.ts +0 -2
  316. package/dist/src/core/tree-cow.js +2 -2
  317. package/dist/src/core/tree-store.js +4 -5
  318. package/dist/src/core/typecheck.js +2 -2
  319. package/dist/src/core/wire-dom-fragment.js +6 -17
  320. package/dist/src/core/wire-utils.js +5 -17
  321. package/dist/src/core/xpcshell-appdir.js +2 -4
  322. package/dist/src/errors/base.d.ts +53 -1
  323. package/dist/src/errors/base.js +79 -11
  324. package/dist/src/errors/build.d.ts +28 -0
  325. package/dist/src/errors/build.js +51 -10
  326. package/dist/src/errors/config.d.ts +0 -14
  327. package/dist/src/errors/config.js +13 -34
  328. package/dist/src/errors/download.d.ts +13 -0
  329. package/dist/src/errors/download.js +53 -24
  330. package/dist/src/errors/furnace.js +8 -9
  331. package/dist/src/errors/git.d.ts +8 -8
  332. package/dist/src/errors/git.js +20 -30
  333. package/dist/src/errors/patch.js +8 -10
  334. package/dist/src/errors/rebase.js +7 -7
  335. package/dist/src/types/commands/options.d.ts +96 -123
  336. package/dist/src/types/commands/patches.d.ts +17 -6
  337. package/dist/src/types/config.d.ts +75 -2
  338. package/dist/src/types/index.d.ts +0 -1
  339. package/dist/src/utils/assert.d.ts +3 -15
  340. package/dist/src/utils/assert.js +1 -18
  341. package/dist/src/utils/build-info.d.ts +1 -1
  342. package/dist/src/utils/child-env.d.ts +2 -1
  343. package/dist/src/utils/elapsed.js +10 -1
  344. package/dist/src/utils/fs.d.ts +4 -2
  345. package/dist/src/utils/fs.js +11 -14
  346. package/dist/src/utils/hash.d.ts +12 -0
  347. package/dist/src/utils/hash.js +16 -0
  348. package/dist/src/utils/line-dispatch.d.ts +15 -0
  349. package/dist/src/utils/line-dispatch.js +48 -0
  350. package/dist/src/utils/options.d.ts +1 -7
  351. package/dist/src/utils/options.js +6 -7
  352. package/dist/src/utils/package-root.js +3 -12
  353. package/dist/src/utils/platform.d.ts +0 -4
  354. package/dist/src/utils/process-group.d.ts +2 -1
  355. package/dist/src/utils/process-group.js +20 -19
  356. package/dist/src/utils/process.d.ts +40 -26
  357. package/dist/src/utils/process.js +115 -166
  358. package/dist/src/utils/ps-duration.js +3 -3
  359. package/dist/src/utils/sleep.d.ts +26 -0
  360. package/dist/src/utils/sleep.js +31 -0
  361. package/dist/src/utils/validation.d.ts +2 -38
  362. package/dist/src/utils/validation.js +7 -16
  363. package/docs/configuration.md +136 -1
  364. package/docs/exit-codes.md +34 -20
  365. package/docs/lifecycle-invariants.md +18 -12
  366. package/docs/machine-output.md +31 -8
  367. package/docs/patch-workflow.md +2 -1
  368. package/docs/run-logs.md +44 -4
  369. package/docs/testing.md +70 -11
  370. package/package.json +8 -7
  371. package/dist/src/commands/rebase.d.ts +0 -4
  372. package/dist/src/commands/rebase.js +0 -5
  373. package/dist/src/core/patch-artifact-normalize.d.ts +0 -21
  374. package/dist/src/core/patch-artifact-normalize.js +0 -24
  375. package/dist/src/core/patch-lint-reexports.d.ts +0 -7
  376. package/dist/src/core/patch-lint-reexports.js +0 -8
  377. package/dist/src/core/wire-targets.d.ts +0 -7
  378. package/dist/src/core/wire-targets.js +0 -8
@@ -7,6 +7,45 @@
7
7
  whose behaviour needs more than a schema line, plus the environment variables
8
8
  FireForge reads.
9
9
 
10
+ ## `firefox` — the source pin
11
+
12
+ `firefox.version` (with `firefox.product`, and optionally `firefox.sha256` and
13
+ `firefox.candidate`) is the source pin `fireforge download` resolves an archive
14
+ from. `fireforge source set` writes it.
15
+
16
+ The pin lives in `fireforge.json` beside hand-maintained policy sections, which
17
+ is worth knowing when reverting that file: `git checkout -- fireforge.json`
18
+ after an accidental reformat also reverts an **uncommitted** pin, and the tree
19
+ is then building a different version than the config claims.
20
+
21
+ ### Archive integrity: `firefox.sha256` and `firefox.allowUnverifiedDownload`
22
+
23
+ Every fresh download is checked against Mozilla's published `SHA256SUMS` for
24
+ the release, and a mismatch fails the download and discards the archive. The
25
+ check also **fails closed when the published digest cannot be obtained** — a
26
+ 404, a captive portal answering with an HTML page, a dropped connection or a
27
+ timeout. TLS alone is thin trust for the artifact that becomes the git
28
+ baseline every patch is built on, and "the checksum host was unreachable" is
29
+ exactly the condition someone positioned on the network can arrange.
30
+
31
+ - `firefox.sha256` pins the digest. It takes precedence over the published
32
+ file and is the right answer for reproducible builds and offline mirrors:
33
+ obtain it out of band and the download verifies with no network beyond the
34
+ archive itself.
35
+ - `firefox.allowUnverifiedDownload: true` accepts a download when the
36
+ published digest is **unavailable**, with a loud warning. **This disables
37
+ the integrity check for that case** — the archive is trusted on TLS alone.
38
+ Use it only for a mirror you already trust, and prefer pinning `sha256`
39
+ instead. A digest that IS fetched and does not match is rejected regardless
40
+ of this setting. Ignored when `sha256` is set.
41
+
42
+ `fireforge doctor` reports the pin beside what the checkout actually is — the
43
+ engine's own `browser/config/version.txt` and the version the last download
44
+ recorded — and warns when they disagree. It is a report, not a lock: a tree
45
+ may legitimately be mid-migration. `product`, `sha256` and `candidate` have no
46
+ recorded counterpart on disk, so the check does not claim to have verified
47
+ them.
48
+
10
49
  ## External toolchains
11
50
 
12
51
  Projects with generated asset prerequisites can declare opt-in
@@ -33,6 +72,35 @@ shim that tracks upstream WebIDL additions per release.
33
72
  - `patchLint.checkJsTestShim` points at a project `.d.ts` whose typed
34
73
  declarations (e.g. a real `TestUtils` interface) override the loose
35
74
  baseline, so calls to nonexistent harness members fail at export time.
75
+ - `patchLint.prettier` (`'off'` | `'warning'` | `'error'`, default `'off'`)
76
+ runs the project's configured Prettier over patch-owned `.sys.mjs`
77
+ modules. It runs from inside `engine/`, so `engine/.prettierrc*` and
78
+ `engine/.prettierignore` decide — the same command run from the repo root
79
+ can report a false pass when the root `.prettierignore` excludes
80
+ `engine/`. Prettier is resolved from `engine/node_modules`, then the
81
+ project's, then `npx --no-install`. **Left `'off'`, formatting is out of
82
+ scope for the per-patch tier**; no other tier checks `.sys.mjs`
83
+ formatting either.
84
+ - `patchLint.fileSizeThresholds` tunes the `file-too-large` line counts per
85
+ file class:
86
+
87
+ ```json
88
+ {
89
+ "patchLint": {
90
+ "fileSizeThresholds": {
91
+ "general": { "notice": 500, "warning": 800, "error": 900 },
92
+ "test": { "warning": 1500 }
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ Every field is optional and merges over the defaults (general
99
+ `500/750/900`, test `1200/1400/1600`). The merged triple must satisfy
100
+ `notice <= warning <= error`, which is validated at config load. Note that
101
+ under the recommended gate posture `--max-warnings 0` the `warning` band is
102
+ a hard failure, not a soft limit — which is what these thresholds exist to
103
+ let you move.
36
104
 
37
105
  ## `patchPolicy`
38
106
 
@@ -44,9 +112,55 @@ gaps does not move a patch out of the band its category owns.
44
112
  `test.canaryPath` and `test.canaryTimeoutSeconds` supply the defaults for
45
113
  `fireforge test --canary`. See [`testing.md`](testing.md).
46
114
 
115
+ ## `buildAudit`
116
+
117
+ The post-build packaging audit is warn-only. Most of what it would otherwise
118
+ report as "missing packaged artifact" is classified away structurally — an
119
+ unselected `--with-branding` tree read from the generated `engine/mozconfig`,
120
+ a directory reached only through a platform-gated `DIRS +=` in an ancestor
121
+ `moz.build`, a Storybook `*.stories.mjs`. One case cannot be derived from the
122
+ tree: a source that is deliberately never packaged, such as a type-only
123
+ mirror whose header says it is never loaded. Declare those:
124
+
125
+ ```json
126
+ {
127
+ "buildAudit": {
128
+ "unpackaged": [
129
+ {
130
+ "path": "browser/base/content/hominis-tile-host-types.js",
131
+ "reason": "Type-only mirror of the tile host; never loaded, no jar.mn entry by design."
132
+ }
133
+ ]
134
+ }
135
+ }
136
+ ```
137
+
138
+ - `path` is engine-relative. A `*` may glob **within one path segment**;
139
+ `**` is refused, because a subtree carve-out is how a reviewed exception
140
+ quietly becomes a blanket one.
141
+ - `reason` is required and must be non-empty. This is the one audit class
142
+ FireForge cannot verify against the tree, so the declaration is the
143
+ evidence; an unexplained carve-out is indistinguishable from a mistake by
144
+ the time someone reads it.
145
+ - Admitted paths are **listed** in the audit output
146
+ (`admitted as unpackaged by buildAudit.unpackaged "<path>" — <reason>`),
147
+ never silenced.
148
+ - A declared path that DOES resolve to a packaged artifact is reported as a
149
+ **stale carve-out** rather than suppressed: the declaration asserts a fact
150
+ about the tree that is no longer true.
151
+ - A declaration matching nothing that changed in a given run says nothing.
152
+ Unlike `--expect-unmanaged`, whose unmet entries are reported because it
153
+ is a per-invocation flag list, this is a standing list checked only
154
+ against the files that happened to change — "not met" is the normal case.
155
+
156
+ The `Packaged:` summary line names each non-zero skip class with its count,
157
+ so a run that dismissed four unselected-branding files reads differently
158
+ from one that dismissed four unregistered sources.
159
+
47
160
  ## Design tokens
48
161
 
49
- Tokens are managed with `fireforge token add`.
162
+ Tokens are managed with `fireforge token add`; `fireforge token list` and
163
+ `fireforge token show` report what is already there.
50
164
 
51
165
  - `--create-category` declares a new category banner and inserts the token in
52
166
  one step. It is incompatible with `--variant`; author the category with the
@@ -71,6 +185,27 @@ Tokens are managed with `fireforge token add`.
71
185
  (`already exists in :root[…] (line N), unchanged`) rather than a bare
72
186
  no-op.
73
187
 
188
+ ### Reading the token file
189
+
190
+ `token add --category` refuses a category the tokens CSS does not declare, so
191
+ `token list` exists to name them without hand-parsing a `= Category =` banner
192
+ out of the file.
193
+
194
+ - `fireforge token list [--category <name>] [--json]` reports each category
195
+ banner with the tokens declared under it, in file order. Declarations above
196
+ the first banner are reported under `(no category)` rather than dropped, and
197
+ a banner with no tokens is still listed — `token add` accepts it. An unknown
198
+ `--category` is refused by naming the ones that exist, because a filter that
199
+ printed nothing would read as "this category is empty".
200
+ - `fireforge token show <token-name> [--json]` reports the owning category and
201
+ the value the token takes in **every** block that declares it — the base
202
+ `:root`, the dark `@media` mirror, and each `:root[variant]` — with the line
203
+ of each. The leading `--` is optional; with it, pass the `--` separator
204
+ first (`fireforge token show -- --my-token`).
205
+
206
+ Only the base `:root` block owns a token, so `token list` reports it once;
207
+ the dark and variant blocks mirror it and appear in `token show`.
208
+
74
209
  ## Smoke runs
75
210
 
76
211
  For unattended smoke checks, `fireforge run --smoke-exit <s> --headless`
@@ -7,25 +7,37 @@ branches on it, and CI decides whether to retry or escalate. The values live
7
7
  in `src/errors/codes.ts`; this file is the operator-facing statement of what
8
8
  they mean and which class produces each.
9
9
 
10
- | Code | Name | Meaning | Produced by |
11
- | ---: | ---------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------- |
12
- | 0 | `SUCCESS` | Completed. Also a run that prompted and the operator declined — declining is a successful run that chose not to proceed. | — |
13
- | 1 | `GENERAL_ERROR` | Unspecified failure the operator must act on. | `GeneralError`, `ParserFallbackError` |
14
- | 2 | `CONFIG_ERROR` | `fireforge.json` missing or invalid. | `ConfigError` and subclasses |
15
- | 3 | `DOWNLOAD_ERROR` | Could not download or extract Firefox source. | `DownloadError` and subclasses |
16
- | 4 | `GIT_ERROR` | A git operation failed. | `GitError` and subclasses |
17
- | 5 | `BUILD_ERROR` | `mach build` failed. | `BuildError` and subclasses |
18
- | 6 | `PATCH_ERROR` | A patch failed to apply, or the patch queue could not be mutated. | `PatchError` |
19
- | 7 | `MISSING_DEPENDENCY` | A required tool (python3, git, tar) was not found. | `PythonNotFoundError`, `GitNotFoundError`, `MachNotFoundError` |
20
- | 8 | `INVALID_ARGUMENT` | The flags were wrong. Usage problem, not an environment one. | `InvalidArgumentError` |
21
- | 9 | `FURNACE_ERROR` | A Furnace component operation failed. | `FurnaceError` |
22
- | 10 | `RESOLUTION_ERROR` | Conflict resolution failed. | `ResolutionError` |
23
- | 11 | `INTERNAL_ERROR` | A FireForge invariant did not hold. **A bug in FireForge**, not something the operator can fix. | `InternalInvariantError` |
24
- | 12 | `SMOKE_EXIT_FAILURE` | `run --smoke-exit` saw unallowed console errors in the smoke window. | `SmokeRunError` |
25
- | 13 | `SMOKE_LAUNCH_FAILURE` | `run --smoke-exit` saw a non-clean browser exit before the window elapsed. | `SmokeRunError` |
26
- | 14 | `INCONCLUSIVE` | A test run's verdict was thrown away: `engine/` moved while the harness ran. **Not red** — re-run it. | `InconclusiveVerdictError` |
27
- | 15 | `LOCK_TIMEOUT` | A lock wait expired, so the run never started. Re-queue it, with a larger `--wait-lock`. | `LockContentionError` |
28
- | 130 | `USER_CANCELLED` | The operator INTERRUPTED a prompt (Esc / Ctrl+C). 128 + SIGINT. | `CancellationError` |
10
+ | Code | Name | Meaning | Produced by |
11
+ | ---: | ---------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
12
+ | 0 | `SUCCESS` | Completed. Also a run that prompted and the operator declined — declining is a successful run that chose not to proceed. | — |
13
+ | 1 | `GENERAL_ERROR` | Unspecified failure the operator must act on. | `GeneralError` (and `PreflightRefusalError`), `ParserFallbackError`, `ExecTimeoutError` |
14
+ | 2 | `CONFIG_ERROR` | `fireforge.json` missing or invalid. | `ConfigError` and subclasses |
15
+ | 3 | `DOWNLOAD_ERROR` | Could not download or extract Firefox source. | `DownloadError` and subclasses |
16
+ | 4 | `GIT_ERROR` | A git operation failed. | `GitError` and subclasses, except `GitNotFoundError` |
17
+ | 5 | `BUILD_ERROR` | `mach build` failed, or a test suite went red. | `BuildError` and subclasses, `TestFailureError` |
18
+ | 6 | `PATCH_ERROR` | A patch failed to apply, or the patch queue could not be mutated. | `PatchError`, `PatchManifestCorruptError`, `RebaseError` and subclasses |
19
+ | 7 | `MISSING_DEPENDENCY` | A required tool (python3, git, tar) was not found. | `PythonNotFoundError`, `GitNotFoundError`, `MachNotFoundError` |
20
+ | 8 | `INVALID_ARGUMENT` | The flags were wrong. Usage problem, not an environment one. | `InvalidArgumentError`; commander usage errors (unknown command or option, missing argument) |
21
+ | 9 | `FURNACE_ERROR` | A Furnace component operation failed. | `FurnaceError` |
22
+ | 10 | `RESOLUTION_ERROR` | Conflict resolution failed. | `ResolutionError` |
23
+ | 11 | `INTERNAL_ERROR` | A FireForge invariant did not hold. **A bug in FireForge**, not something the operator can fix. | `InternalInvariantError` |
24
+ | 12 | `SMOKE_EXIT_FAILURE` | `run --smoke-exit` saw unallowed console errors in the smoke window. | `SmokeRunError` |
25
+ | 13 | `SMOKE_LAUNCH_FAILURE` | `run --smoke-exit` saw a non-clean browser exit before the window elapsed. | `SmokeRunError` |
26
+ | 14 | `INCONCLUSIVE` | A test run's verdict was thrown away: `engine/` moved while the harness ran. **Not red** — re-run it. | `InconclusiveVerdictError` |
27
+ | 15 | `LOCK_TIMEOUT` | A lock wait expired, so the run never started. Re-queue it, with a larger `--wait-lock`. | `LockContentionError` |
28
+ | 130 | `USER_CANCELLED` | The operator INTERRUPTED a prompt (Esc / Ctrl+C). 128 + SIGINT. | `CancellationError` |
29
+
30
+ A usage error commander itself detects — an unknown command, an unknown or
31
+ malformed option, a missing required argument — never reaches a command
32
+ action, so before 0.46.0 commander terminated the process with its own exit
33
+ 1 and, under `--json`, no refusal envelope. `createProgram` now installs
34
+ commander's `exitOverride()` and the entry point maps the resulting
35
+ `CommanderError` to `INVALID_ARGUMENT` (8) with the standard envelope on
36
+ stdout. `--help` and `--version` travel the same path and still exit 0, and
37
+ so does a command group invoked without a subcommand (`fireforge`,
38
+ `fireforge tree`): commander prints the group's help and the entry point
39
+ treats it as informational rather than as a wrong flag, matching the exit 0
40
+ that `patch`, `token` and `furnace` give through their own help action.
29
41
 
30
42
  ## The three distinctions that matter for CI
31
43
 
@@ -35,7 +47,9 @@ change would have helped — escalate it, do not retry.
35
47
 
36
48
  **Red vs never-ran vs thrown-away.** These are three different facts and,
37
49
  before 0.44.0, two of them shared exit 1. `BUILD_ERROR` (5) is a suite that
38
- FAILED. `LOCK_TIMEOUT` (15) is a run that never started, because another
50
+ FAILED — thrown as `TestFailureError` since 0.45.0, which keeps the code but
51
+ carries test remedies rather than build ones (`--json` reports it as
52
+ `test-failure`). `LOCK_TIMEOUT` (15) is a run that never started, because another
39
53
  FireForge process held the lock for the whole budget — nothing about the
40
54
  request was wrong, so re-queue it. `INCONCLUSIVE` (14) is a suite whose
41
55
  result was DISCARDED, because `engine/` moved underneath it — the code may
@@ -12,7 +12,9 @@ mutating command, read the decision table at the bottom first.
12
12
 
13
13
  1. **Only `bin/fireforge.ts` terminates the process.** Library code
14
14
  propagates `CommandError` / `FireForgeError`; it never calls
15
- `process.exit`. Enforced by the process-boundary test.
15
+ `process.exit`. Enforced at lint time by `no-restricted-properties`
16
+ (`eslint.config.js`), which bans `process.exit` everywhere and grants a
17
+ `bin/**` override.
16
18
  2. **At most one real engine/patch mutation runs at a time** per resource:
17
19
  CLI build/test/export/re-export/furnace mutations first take the
18
20
  `.fireforge/engine-session.lock`, then subsystem locks such as build,
@@ -127,12 +129,15 @@ tree.json` marker plus a default-deny command-verdict table
127
129
  refuses the command — reading it as absent granted a snapshot the
128
130
  full mutating command set on the strength of a file that failed to
129
131
  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`,
132
+ `unknown`, not free, and refuses removal. Since 0.45.1 `withFileLock`
133
+ treats its owner-record write as FATAL (`writeLockOwner` in
134
+ `file-lock-owner.ts` removes the just-created directory and throws a
135
+ `GeneralError` if the record cannot be written or does not read back
136
+ as written), so an owner-less lock is only ever left by a pre-0.45.1
137
+ holder or by a crash inside that write — it is still refused, because
138
+ "cannot tell" is not "nothing there" (a lock directory that vanished
139
+ between the probe and the owner read is the one exception: that is a
140
+ clean release, and reads as free). Each has one named escape (`--ignore-corrupt-tree-marker`,
136
141
  `tree remove --force`); neither has a silent one. The corrupt-marker
137
142
  refusal spares the unconditionally-**allowed** read-only commands
138
143
  (`status`, `lint`, `typecheck`, `verify`): their verdict never consults
@@ -240,11 +245,12 @@ error is rethrown unless the rollback is what failed.
240
245
  | Module | Owns |
241
246
  | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
242
247
  | `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. |
248
+ | `src/core/file-lock.ts` | Generic directory-as-lock (`mkdir` atomicity), owner PID **+ per-acquisition token + acquisition time + start tick** 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. **The owner-record write is fatal**: a failed or unverifiable write releases the just-created lock and throws a `GeneralError` instead of running the operation — a lock with no readable owner is reaped by the age heuristic after 5 min, which for a multi-hour build meant a second holder inside the critical section (`file-lock.test.ts` "owner record write at acquisition": write failure, corrupt read-back, recorded acquisition time). 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. |
249
+ | `src/core/file-lock-owner.ts` | The owner record (`pid` file) itself: format (PID, token, `acquired-at-ms=` and `start-tick=` lines, free-form metadata — older records without the mechanical lines still parse), the write-and-verify contract above (a read-back that ERRORS is retried before it counts as a failed write; a missing or mismatching record fails immediately), and **PID-reuse liveness**: `isLockOwnerAlive` is the liveness check every stale probe, reap re-verification, status snapshot and timeout message use. Beyond `isProcessAlive`, on Linux the record carries the writer's boot-relative start tick (`/proc/self/stat` field 22) and the probe reads the candidate's tick from `/proc/<pid>/stat`; a different tick is a different process, so the lock is stale — a crashed build's PID handed to a long-lived daemon no longer keeps the dead lock honoured until the waiter's timeout. Ticks, not wall-clock times: `btime` moves with clock steps, and a wall-clock comparison would reap a LIVE holder after NTP steps the clock forward. macOS and Windows have no cheap start-time source (a `ps` spawn per poll is not worth it) and keep the PID-only answer; so do records without a start tick. Enforced by `file-lock-owner.test.ts` (backward-compatible parsing, forged mechanical lines dropped, transient read-back error retried vs persistent error vs mismatch, fake-procfs tick parsing, reused-PID vs genuine-holder verdicts, stepped-clock immunity, and on Linux a real-procfs `withFileLock` reap of a live-PID stale record). |
244
250
  | `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
251
  | `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
252
  | `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. |
253
+ | `src/utils/assert.ts` | The runtime-assertion primitive (`assert`, `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
254
  | `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
255
  | `src/core/patch-lock.ts` | `withPatchDirectoryLock` — serializes patch filename allocation and manifest read-modify-writes. **Not reentrant**: callers must not already hold it. |
250
256
  | `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. |
@@ -283,7 +289,9 @@ On SIGINT/SIGTERM, `bin/fireforge.ts`:
283
289
  ## Where this is tested
284
290
 
285
291
  - `src/core/__tests__/file-lock.test.ts` — lock acquisition, staleness, PID
286
- recovery.
292
+ recovery, fatal owner-record write.
293
+ - `src/core/__tests__/file-lock-owner.test.ts` — owner-record format
294
+ compatibility and PID-reuse liveness.
287
295
  - `src/core/__tests__/furnace-operation.test.ts` — wrapper lifecycle, signal
288
296
  rollback outcomes, `pendingRepair` semantics, lock force-release.
289
297
  - `src/core/__tests__/signal-critical.test.ts` — the critical-section
@@ -305,5 +313,3 @@ On SIGINT/SIGTERM, `bin/fireforge.ts`:
305
313
  stderr.
306
314
  - `src/commands/__tests__/tree.integration.test.ts` — real-program in-tree
307
315
  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).
@@ -52,14 +52,16 @@ if (options.json === true) {
52
52
 
53
53
  ## Current surfaces
54
54
 
55
- | Surface | Shape | Notes |
56
- | ---------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
57
- | `status --json` | `{ schemaVersion: 1, … }` | Error envelope on three engine preconditions |
58
- | `tree list --json` | `{ schemaVersion: 1, trees }` | Error envelope added in 0.44.0 |
59
- | `test` verdict line | `FIREFORGE-VERDICT: PASS\|FAIL reason=… [log=<path>]` | Not JSON. A single final stdout line, sealed by `setStdoutSealed` so nothing can displace it. `log=` names the run's own complete output under `.fireforge/logs/`, so a piped or truncated run still leaves a re-readable artifact |
60
- | `lint --per-patch --report <path>` | `{ schemaVersion: 1, … }` | Written to a file, never read back by FireForge |
61
- | `status --raw` | `git status --porcelain` lines | Not JSON, but a machine surface: it engages the same stdout discipline, so rules 3 and 4 apply and rules 1 and 2 do not. A clean tree prints nothing |
62
- | `build` log announcement | `Full build output: <path>` | Not JSON. `build` prints no verdict line, so it names its run log directly on the way out |
55
+ | Surface | Shape | Notes |
56
+ | ---------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
57
+ | `status --json` | `{ schemaVersion: 1, … }` | Error envelope on three engine preconditions |
58
+ | `tree list --json` | `{ schemaVersion: 1, trees }` | Error envelope added in 0.44.0 |
59
+ | `token list --json` | `{ schemaVersion: 1, tokensCssPath, categories }` | Categories in file order, each with its tokens (`name`, `line`, `value`) |
60
+ | `token show --json` | `{ schemaVersion: 1, name, category, declarations }` | One entry per declaring block, with its selector trail and line |
61
+ | `test` verdict line | `FIREFORGE-VERDICT: PASS\|FAIL reason=… [note=<class>] [log=<path>]` | Not JSON. A single final stdout line, sealed by `setStdoutSealed` so nothing can displace it. `log=` names the run's own complete output under `.fireforge/logs/`, so a piped or truncated run still leaves a re-readable artifact |
62
+ | `lint --per-patch --report <path>` | `{ schemaVersion: 1, … }` | Written to a file, never read back by FireForge |
63
+ | `status --raw` | `git status --porcelain` lines | Not JSON, but a machine surface: it engages the same stdout discipline, so rules 3 and 4 apply and rules 1 and 2 do not. A clean tree prints nothing |
64
+ | `build` log announcement | `Full build output: <path>` | Not JSON. `build` prints no verdict line, so it names its run log directly on the way out |
63
65
 
64
66
  `--raw` is why `cli.ts` reads `process.argv` for `--json` **or** `--raw`
65
67
  before dispatching: rule 3 is about who owns stdout, and that question is
@@ -92,3 +94,24 @@ what moved.
92
94
  | `inconclusive` | A result exists but `engine/` moved under it, so it was discarded. |
93
95
  | `lock-timeout` | The run never started: the engine session lock stayed contended. |
94
96
  | `killed` | A signal terminated the run. Written from the signal handler so a log tail is always self-describing — a killed run must never be silent, or "killed", "still running" and "never started" become indistinguishable. |
97
+
98
+ ### The additive `note=` key
99
+
100
+ A `FAIL reason=preflight` line covers every gate before the harness: a
101
+ missing build, a peer's packaging record replacing this session's, a
102
+ developer browser holding the objdir. An unattended reader whose pipe kept
103
+ only the verdict line could not tell those apart, so a preflight refusal that
104
+ classifies itself also emits `note=<class>`.
105
+
106
+ `note=` is **additive**, like `log=`, `signal=` and `shards=`. `reason=` is
107
+ unchanged and still comes from the closed set above, so a consumer that
108
+ tokenises `key=value` pairs is unaffected, and a gate that names no class
109
+ emits no `note=` at all. The classes are not a closed set and may grow;
110
+ consumers should treat an unrecognised one as opaque. Current values:
111
+ `stale-browser`, `stale-browser-kill-failed`, `marionette-port-busy`,
112
+ `coverage-replaced`, `stale-build`, `stale-components`.
113
+
114
+ A preflight refusal's own text is written to stdout **before** the verdict
115
+ line, and into the run log, so the verdict stays the last stdout write while
116
+ the reason survives both a redirect and a `tail`. See
117
+ [`run-logs.md`](run-logs.md).
@@ -74,7 +74,8 @@ Every queue-mutating patch command accepts `--wait-lock [seconds]`; see
74
74
  ## Linting the queue
75
75
 
76
76
  `lint --per-patch --patches` accepts repeated flags, comma lists, full
77
- filenames/stems, manifest names, category-prefixed slugs, and bare slugs.
77
+ filenames/stems, bare order numbers, manifest names, category-prefixed slugs,
78
+ and bare slugs.
78
79
 
79
80
  `lint --per-patch --max-warnings 0` is the warning-clean form for a release
80
81
  gate.
package/docs/run-logs.md CHANGED
@@ -22,9 +22,32 @@ mistake.
22
22
 
23
23
  ## The contract
24
24
 
25
- - **Raw bytes, never the echo filter's collapsed form.** The artifact exists
26
- to be re-read by whoever is diagnosing the run; shortening it would
27
- reintroduce the loss it prevents.
25
+ - **Complete output, never the echo filter's collapsed form.** The artifact
26
+ exists to be re-read by whoever is diagnosing the run; shortening it would
27
+ reintroduce the loss it prevents. The one transformation applied is the
28
+ secret-masking pass below — it changes values, never line structure.
29
+ - **Secret-shaped values are masked in the file, and only in the file.** The
30
+ terminal already showed every byte to the operator who ran the command; the
31
+ file is retained twenty deep and gets attached to bug reports, so it gets a
32
+ narrow redaction pass (`src/core/run-log-redact.ts`), applied per line
33
+ before the write (a line split across two chunks is held until its
34
+ terminator arrives — `\n`, or a lone `\r` from progress-bar repaints — or
35
+ it exceeds 1 MiB, or the log is closed). Exactly what is redacted:
36
+ - the value of an env-style assignment `KEY=value` whose `KEY` matches
37
+ `/(TOKEN|SECRET|PASSWORD|PASSWD|API_KEY|AUTH)/i` — the value up to the
38
+ next whitespace, or a whole quoted string — is written as
39
+ `KEY=<redacted>`; the `--password=x` and `-DAUTH_TOKEN=x` spellings that
40
+ mach and configure echo from their argv are the same shape and are
41
+ masked too;
42
+ - the credential of an `Authorization:` header (`Bearer`, `Basic` and
43
+ `Token` keep the scheme word; any other value is masked whole).
44
+
45
+ Nothing else is. A bare token in a URL, a space-separated `--password foo`
46
+ argument, a JSON `"token": "…"` field, or a cookie passes through unchanged — the pass is a
47
+ seatbelt against the common accidental leak (mach echoing its environment),
48
+ not a guarantee that a log is free of secrets. Review a log before sharing
49
+ it.
50
+
28
51
  - **Opened before any preflight**, so a refusal is logged too. Those are
29
52
  exactly the runs whose only output a `tail` throws away.
30
53
  - **Retained 20 deep per command kind.** Per-kind, so a busy `test` loop
@@ -37,7 +60,24 @@ mistake.
37
60
  degrades to no log. A run must never fail over a diagnostic, so `openRunLog`
38
61
  returns `undefined` on failure, a post-open stream error latches `broken`
39
62
  and drops every later write, and pruning failures are logged verbosely
40
- only.
63
+ only. The entry point's SIGINT/SIGTERM path closes the active log (also
64
+ best-effort) before exiting, so the held partial line of a killed run
65
+ reaches disk.
66
+
67
+ ## Preflight refusals
68
+
69
+ `fireforge test` opens its run log before any preflight, so a refusal is
70
+ logged too — those are exactly the runs whose only output a `tail` throws
71
+ away. The refusal's own explanatory text is written into the log, and to
72
+ stdout, **before** the verdict line seals stdout.
73
+
74
+ Both halves are load-bearing. The verdict seal routes everything the CLI
75
+ error boundary renders afterwards to stderr, so a run captured with
76
+ `> file` used to keep `FIREFORGE-VERDICT: FAIL reason=preflight` and nothing
77
+ else; and that rendering happens after the run log is closed, so the artifact
78
+ the verdict's own `log=` key pointed at held only the pre-test build. Writing
79
+ before the seal fixes both without trading away the contract: the reason
80
+ lands on both channels, and the verdict is still the run's last stdout line.
41
81
 
42
82
  ## Where the path is announced
43
83
 
package/docs/testing.md CHANGED
@@ -49,8 +49,13 @@ components):
49
49
  - a changed `moz.build` / `moz.configure` / `Makefile.in` runs
50
50
  `mach configure` first;
51
51
  - a changed `jar.mn` escalates the whole build to a full `mach build`
52
- (minutes rather than seconds), so that new install-manifest destinations
53
- exist before the run.
52
+ (minutes rather than seconds) **only** when it is a NEW manifest
53
+ (untracked in the engine repo, so no install manifest exists for it yet)
54
+ or when its jar declaration carries a bracketed base-directory prefix,
55
+ which redirects the install destination away from the default chrome
56
+ root. An entry added to an existing `dist/bin` manifest no longer
57
+ escalates. Any probe failure (unreadable manifest, no git) escalates, and
58
+ the notice names the manifest and the reason.
54
59
 
55
60
  Each escalation is paid once per actual content change: a `jar.mn` that is
56
61
  dirty against HEAD but byte-identical to what the last full build consumed
@@ -60,20 +65,35 @@ carried forward from the previous full build, so a registration made
60
65
  between a `build --ui` and the next `test --build` still triggers the full
61
66
  build once.
62
67
 
63
- Whether a full build is really required for an entry added to an existing
64
- `jar.mn` under `dist/bin` is an open question, recorded in the 0.44.0
65
- changelog: mozbuild regenerates its backend from a newer `jar.mn` on any
66
- `mach build`, `faster` included, and the FasterMake backend consumes
67
- `dist/bin` jar manifests. The escalation stays until that is demonstrated
68
- end to end against a real objdir (`build --ui` after a jar-only
69
- registration, then a plain `test`), because relaxing a stale-artifact guard
70
- on source reading alone trades a slow build for a silently wrong test.
68
+ The open question the 0.44.0 changelog recorded here — whether a full build
69
+ is really required for an entry added to an existing `dist/bin` `jar.mn` —
70
+ was settled downstream by exactly the experiment that entry asked for: two
71
+ clean runs where a jar-only registration of a new content file was
72
+ installed by a plain `fireforge build --ui`
73
+ (`faster/install_dist_bin_browser` named the destination, the file reached
74
+ `dist/bin` and the `.app` bundle) and fetched over `chrome://` by a plain
75
+ `fireforge test`. That case no longer escalates. The two halves the
76
+ experiment did NOT exercise — a new `jar.mn` file, and a non-default
77
+ install destination — still do, for the reason the original entry gives:
78
+ relaxing a stale-artifact guard without evidence trades a slow build for a
79
+ silently wrong test.
71
80
 
72
81
  ## Resilience
73
82
 
74
83
  - Recognized harness crashes retry up to `--harness-retries <n>` times
75
84
  (default 2).
76
85
  - `--kill-stale-marionette` terminates recognized stale browsers.
86
+ - Every dispatch runs a **census of orphaned harness helpers** — `xpcshell`
87
+ (the harness httpd), `pywebsocket`, `ssltunnel`, `moz-http2` — that
88
+ survived an earlier run in this project's objdir. It runs at preflight,
89
+ before this run spawns anything, so every hit is a survivor by
90
+ construction; a match must be objdir-anchored, since `xpcshell` and
91
+ `server.js` are far too generic to report on their own. Survivors slow
92
+ every later run without appearing in its output (a three-second suite
93
+ taking minutes of wall clock is the usual symptom). Report-only by
94
+ default; `--reap-orphans` terminates them. It never refuses a run.
95
+ A different shape — reparented Python `multiprocessing` workers — is
96
+ covered by the `Orphaned harness workers` doctor check.
77
97
  - `--perf-samples <path>` publishes a perf-sample artifact path to the
78
98
  harness (exported as `<BINARYNAME>_PERF_SAMPLE_JSON`).
79
99
 
@@ -82,7 +102,7 @@ on source reading alone trades a slow build for a silently wrong test.
82
102
  Every test run ends with one machine-readable line:
83
103
 
84
104
  ```
85
- FIREFORGE-VERDICT: PASS|FAIL reason=… [log=<path>]
105
+ FIREFORGE-VERDICT: PASS|FAIL reason=… [note=<class>] [log=<path>]
86
106
  ```
87
107
 
88
108
  Automation should branch on it rather than on the raw process code, and must
@@ -91,6 +111,13 @@ the stdout rules around the line are in
91
111
  [`machine-output.md`](machine-output.md); `log=` names the run's own complete
92
112
  output, specified in [`run-logs.md`](run-logs.md).
93
113
 
114
+ `reason=preflight` covers every gate before the harness, so a refusal that
115
+ classifies itself adds an additive `note=<class>` (`stale-browser`,
116
+ `coverage-replaced`, …) naming which gate fired. The refusal's full text is
117
+ written to stdout before the verdict line and into the run log, so a
118
+ redirected run keeps the reason as well as the verdict — see
119
+ [`run-logs.md`](run-logs.md).
120
+
94
121
  Exit code 14 (`INCONCLUSIVE`) is not red: it means `engine/` moved while the
95
122
  harness ran and the result was thrown away. Exit 15 (`LOCK_TIMEOUT`) means the
96
123
  run never started. See [`exit-codes.md`](exit-codes.md).
@@ -109,6 +136,38 @@ the build path stays quiet. It is opt-in rather than automatic because it
109
136
  changes how much a third party prints, and an operator who wants the quieting
110
137
  should keep it.
111
138
 
139
+ ## Known upstream teardown noise
140
+
141
+ Recent engines can end a run with a Python traceback at harness teardown:
142
+
143
+ ```
144
+ AttributeError: 'SystemResourceMonitor' object has no attribute 'stop_time'
145
+ ```
146
+
147
+ (or `poll_interval`), raised from `mozsystemmonitor/resourcemonitor.py`. It is
148
+ an upstream defect in the resource monitor's own shutdown, it is cosmetic, and
149
+ it does not affect any verdict. It matters only because it lands exactly where
150
+ a reader looks for the failure summary.
151
+
152
+ FireForge recognizes this one signature — and only this one. Recognition
153
+ requires **all** of: an `AttributeError` on `SystemResourceMonitor` naming one
154
+ of those two attributes, a `resourcemonitor.py` stack frame, and (in a test
155
+ run) a preceding `SUITE_END`. A novel attribute, a different exception, or the
156
+ same traceback before shutdown is treated as a real failure and printed
157
+ verbatim, always.
158
+
159
+ The two phases handle it differently, deliberately:
160
+
161
+ - **Tests** collapse the traceback in the terminal echo to one labeled
162
+ `[FireForge]` line. Captures and the run log keep the **raw** traceback, and
163
+ the classifier reads the raw form — a real failure always outranks this
164
+ signature in diagnosis.
165
+ - **Builds** print it verbatim and add a note naming it. A build has no
166
+ `SUITE_END` — no boundary separating teardown from work still in progress —
167
+ so FireForge cannot distinguish cosmetic teardown noise from a real
168
+ build-time traceback there, and withholding the block would be the wrong
169
+ risk to take.
170
+
112
171
  ## Concurrency
113
172
 
114
173
  A test run holds the engine-session lock and snapshots the engine git