@hominis/fireforge 0.49.2 → 0.50.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 (334) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +1 -1
  3. package/dist/bin/fireforge.js +6 -0
  4. package/dist/build-info.json +4 -4
  5. package/dist/src/commands/build-prelock.d.ts +20 -0
  6. package/dist/src/commands/build-prelock.js +50 -0
  7. package/dist/src/commands/build.d.ts +3 -1
  8. package/dist/src/commands/build.js +13 -6
  9. package/dist/src/commands/check-report.d.ts +28 -0
  10. package/dist/src/commands/check-report.js +78 -0
  11. package/dist/src/commands/check-run.d.ts +28 -0
  12. package/dist/src/commands/check-run.js +104 -0
  13. package/dist/src/commands/check-steps.d.ts +67 -0
  14. package/dist/src/commands/check-steps.js +175 -0
  15. package/dist/src/commands/check.d.ts +32 -0
  16. package/dist/src/commands/check.js +160 -0
  17. package/dist/src/commands/config.js +5 -6
  18. package/dist/src/commands/doctor-engine-eslint.js +6 -1
  19. package/dist/src/commands/doctor-source-pin.js +4 -1
  20. package/dist/src/commands/export-adopt-check.d.ts +38 -0
  21. package/dist/src/commands/export-adopt-check.js +98 -0
  22. package/dist/src/commands/export-flow.js +29 -14
  23. package/dist/src/commands/export-placement-gate.d.ts +6 -2
  24. package/dist/src/commands/export-shared.d.ts +2 -1
  25. package/dist/src/commands/export-staged-registration.d.ts +46 -0
  26. package/dist/src/commands/export-staged-registration.js +91 -0
  27. package/dist/src/commands/forward-import-remedy.d.ts +32 -0
  28. package/dist/src/commands/forward-import-remedy.js +63 -0
  29. package/dist/src/commands/furnace/apply.js +3 -3
  30. package/dist/src/commands/furnace/chrome-doc-remove.d.ts +2 -1
  31. package/dist/src/commands/furnace/chrome-doc-remove.js +2 -2
  32. package/dist/src/commands/furnace/chrome-doc.d.ts +2 -1
  33. package/dist/src/commands/furnace/chrome-doc.js +2 -2
  34. package/dist/src/commands/furnace/create.js +2 -2
  35. package/dist/src/commands/furnace/deploy-named.d.ts +48 -0
  36. package/dist/src/commands/furnace/deploy-named.js +107 -0
  37. package/dist/src/commands/furnace/deploy.d.ts +3 -2
  38. package/dist/src/commands/furnace/deploy.js +50 -86
  39. package/dist/src/commands/furnace/diff.js +26 -3
  40. package/dist/src/commands/furnace/index.js +66 -41
  41. package/dist/src/commands/furnace/init.js +13 -6
  42. package/dist/src/commands/furnace/override.js +4 -2
  43. package/dist/src/commands/furnace/preview.js +4 -3
  44. package/dist/src/commands/furnace/refresh.js +2 -2
  45. package/dist/src/commands/furnace/remove.js +4 -3
  46. package/dist/src/commands/furnace/rename-dry-run.d.ts +29 -0
  47. package/dist/src/commands/furnace/rename-dry-run.js +127 -0
  48. package/dist/src/commands/furnace/rename-helpers.d.ts +7 -0
  49. package/dist/src/commands/furnace/rename-helpers.js +15 -1
  50. package/dist/src/commands/furnace/rename-registrations.d.ts +41 -0
  51. package/dist/src/commands/furnace/rename-registrations.js +93 -0
  52. package/dist/src/commands/furnace/rename.d.ts +2 -1
  53. package/dist/src/commands/furnace/rename.js +28 -66
  54. package/dist/src/commands/furnace/scan.d.ts +9 -4
  55. package/dist/src/commands/furnace/scan.js +11 -9
  56. package/dist/src/commands/furnace/sync.js +3 -2
  57. package/dist/src/commands/furnace/validate-report.d.ts +45 -0
  58. package/dist/src/commands/furnace/validate-report.js +91 -0
  59. package/dist/src/commands/furnace/validate.d.ts +1 -1
  60. package/dist/src/commands/furnace/validate.js +127 -114
  61. package/dist/src/commands/furnace/validation-output.js +14 -3
  62. package/dist/src/commands/lint-per-patch.js +31 -60
  63. package/dist/src/commands/lint-report.d.ts +8 -4
  64. package/dist/src/commands/lint-report.js +15 -5
  65. package/dist/src/commands/lint.js +15 -34
  66. package/dist/src/commands/manifest.js +2 -0
  67. package/dist/src/commands/patch/delete-gap-notice.js +3 -1
  68. package/dist/src/commands/patch/delete.d.ts +1 -1
  69. package/dist/src/commands/patch/delete.js +13 -98
  70. package/dist/src/commands/patch/index.d.ts +1 -1
  71. package/dist/src/commands/patch/index.js +4 -2
  72. package/dist/src/commands/patch/lint-ignore.d.ts +5 -1
  73. package/dist/src/commands/patch/lint-ignore.js +65 -28
  74. package/dist/src/commands/patch/move-files-create.js +26 -17
  75. package/dist/src/commands/patch/move-files-into.js +5 -3
  76. package/dist/src/commands/patch/move-files.js +3 -1
  77. package/dist/src/commands/patch/removal-dependents.d.ts +63 -0
  78. package/dist/src/commands/patch/removal-dependents.js +133 -0
  79. package/dist/src/commands/patch/rename.js +7 -0
  80. package/dist/src/commands/patch/retire.d.ts +34 -0
  81. package/dist/src/commands/patch/retire.js +128 -0
  82. package/dist/src/commands/patch/split-plan.d.ts +7 -7
  83. package/dist/src/commands/patch/split-plan.js +45 -28
  84. package/dist/src/commands/patch/split-staged.d.ts +108 -0
  85. package/dist/src/commands/patch/split-staged.js +202 -0
  86. package/dist/src/commands/patch/split.d.ts +5 -0
  87. package/dist/src/commands/patch/split.js +51 -15
  88. package/dist/src/commands/patch/staged-dependency-batch.d.ts +48 -0
  89. package/dist/src/commands/patch/staged-dependency-batch.js +176 -0
  90. package/dist/src/commands/patch/staged-dependency-entries.d.ts +36 -0
  91. package/dist/src/commands/patch/staged-dependency-entries.js +52 -0
  92. package/dist/src/commands/patch/staged-dependency-from.d.ts +14 -0
  93. package/dist/src/commands/patch/staged-dependency-from.js +108 -0
  94. package/dist/src/commands/patch/staged-dependency.js +14 -41
  95. package/dist/src/commands/re-export-adjacent-registrations.d.ts +20 -0
  96. package/dist/src/commands/re-export-adjacent-registrations.js +75 -0
  97. package/dist/src/commands/re-export-adjacent.d.ts +45 -2
  98. package/dist/src/commands/re-export-adjacent.js +156 -37
  99. package/dist/src/commands/re-export-drift.d.ts +36 -8
  100. package/dist/src/commands/re-export-drift.js +73 -49
  101. package/dist/src/commands/re-export-files.js +11 -8
  102. package/dist/src/commands/re-export-lint.d.ts +2 -1
  103. package/dist/src/commands/re-export-lint.js +23 -10
  104. package/dist/src/commands/re-export-options.js +45 -4
  105. package/dist/src/commands/re-export-preflight.d.ts +23 -0
  106. package/dist/src/commands/re-export-preflight.js +160 -0
  107. package/dist/src/commands/re-export-register.js +42 -17
  108. package/dist/src/commands/re-export-removed.d.ts +12 -1
  109. package/dist/src/commands/re-export-removed.js +41 -11
  110. package/dist/src/commands/re-export-scan-owner.d.ts +44 -0
  111. package/dist/src/commands/re-export-scan-owner.js +116 -0
  112. package/dist/src/commands/re-export-scan.d.ts +17 -9
  113. package/dist/src/commands/re-export-scan.js +56 -23
  114. package/dist/src/commands/re-export-single.js +22 -6
  115. package/dist/src/commands/re-export-summary.d.ts +28 -0
  116. package/dist/src/commands/re-export-summary.js +36 -0
  117. package/dist/src/commands/re-export.js +37 -24
  118. package/dist/src/commands/rebase/index.js +17 -17
  119. package/dist/src/commands/run.js +16 -2
  120. package/dist/src/commands/status-check.d.ts +55 -16
  121. package/dist/src/commands/status-check.js +92 -37
  122. package/dist/src/commands/status-json.d.ts +22 -9
  123. package/dist/src/commands/status-json.js +47 -32
  124. package/dist/src/commands/status-output.js +3 -1
  125. package/dist/src/commands/status-scope.d.ts +37 -0
  126. package/dist/src/commands/status-scope.js +54 -0
  127. package/dist/src/commands/status.js +66 -23
  128. package/dist/src/commands/test-browser-preflight.js +19 -3
  129. package/dist/src/commands/test-build-furnace.d.ts +21 -0
  130. package/dist/src/commands/test-build-furnace.js +48 -0
  131. package/dist/src/commands/test-build-phase.js +47 -41
  132. package/dist/src/commands/test-checkjs.js +7 -0
  133. package/dist/src/commands/test-diagnose.js +7 -0
  134. package/dist/src/commands/test-modes.d.ts +16 -1
  135. package/dist/src/commands/test-modes.js +24 -1
  136. package/dist/src/commands/test-multi-verdict.d.ts +61 -0
  137. package/dist/src/commands/test-multi-verdict.js +111 -0
  138. package/dist/src/commands/test-orchestrate.d.ts +43 -0
  139. package/dist/src/commands/test-orchestrate.js +222 -0
  140. package/dist/src/commands/test-perf-scope.d.ts +29 -0
  141. package/dist/src/commands/test-perf-scope.js +83 -0
  142. package/dist/src/commands/test-prelock.d.ts +51 -0
  143. package/dist/src/commands/test-prelock.js +177 -0
  144. package/dist/src/commands/test-register.js +54 -10
  145. package/dist/src/commands/test-run.js +4 -1
  146. package/dist/src/commands/test-stale-gate.js +57 -27
  147. package/dist/src/commands/test-verdict.d.ts +10 -1
  148. package/dist/src/commands/test-verdict.js +28 -1
  149. package/dist/src/commands/test.d.ts +6 -1
  150. package/dist/src/commands/test.js +66 -60
  151. package/dist/src/commands/token-lint.d.ts +14 -0
  152. package/dist/src/commands/token-lint.js +111 -0
  153. package/dist/src/commands/token.js +10 -0
  154. package/dist/src/commands/verify-scope.d.ts +28 -0
  155. package/dist/src/commands/verify-scope.js +67 -0
  156. package/dist/src/commands/verify.d.ts +10 -2
  157. package/dist/src/commands/verify.js +59 -35
  158. package/dist/src/core/build-baseline-types.d.ts +46 -0
  159. package/dist/src/core/build-baseline-validate.js +19 -0
  160. package/dist/src/core/build-baseline.d.ts +5 -1
  161. package/dist/src/core/build-baseline.js +3 -1
  162. package/dist/src/core/build-overwrite-guard.d.ts +50 -0
  163. package/dist/src/core/build-overwrite-guard.js +84 -4
  164. package/dist/src/core/build-prepare.d.ts +7 -0
  165. package/dist/src/core/build-prepare.js +14 -8
  166. package/dist/src/core/build-run-record.d.ts +74 -0
  167. package/dist/src/core/build-run-record.js +136 -0
  168. package/dist/src/core/checkjs-support-heads.d.ts +41 -0
  169. package/dist/src/core/checkjs-support-heads.js +104 -0
  170. package/dist/src/core/config-paths.d.ts +2 -2
  171. package/dist/src/core/config-paths.js +8 -0
  172. package/dist/src/core/config-validate-check.d.ts +8 -0
  173. package/dist/src/core/config-validate-check.js +143 -0
  174. package/dist/src/core/config-validate-file-size.d.ts +9 -6
  175. package/dist/src/core/config-validate-file-size.js +91 -2
  176. package/dist/src/core/config-validate-history.d.ts +11 -0
  177. package/dist/src/core/config-validate-history.js +38 -0
  178. package/dist/src/core/config-validate-patch-policy.js +100 -0
  179. package/dist/src/core/config-validate-test-toolchains.js +7 -0
  180. package/dist/src/core/config-validate-token-scales.d.ts +6 -0
  181. package/dist/src/core/config-validate-token-scales.js +50 -0
  182. package/dist/src/core/config-validate.js +14 -5
  183. package/dist/src/core/config.d.ts +5 -0
  184. package/dist/src/core/config.js +16 -0
  185. package/dist/src/core/coverage-dispatch.d.ts +24 -0
  186. package/dist/src/core/coverage-dispatch.js +44 -0
  187. package/dist/src/core/coverage-extend.d.ts +70 -45
  188. package/dist/src/core/coverage-extend.js +199 -144
  189. package/dist/src/core/coverage-records.d.ts +97 -0
  190. package/dist/src/core/coverage-records.js +350 -0
  191. package/dist/src/core/destructive.d.ts +7 -4
  192. package/dist/src/core/destructive.js +10 -10
  193. package/dist/src/core/engine-session-lock.d.ts +10 -1
  194. package/dist/src/core/engine-session-lock.js +28 -8
  195. package/dist/src/core/export-moved-code-shape.d.ts +49 -11
  196. package/dist/src/core/export-moved-code-shape.js +135 -44
  197. package/dist/src/core/file-lock-queue.d.ts +67 -0
  198. package/dist/src/core/file-lock-queue.js +221 -0
  199. package/dist/src/core/file-lock.d.ts +4 -25
  200. package/dist/src/core/file-lock.js +105 -138
  201. package/dist/src/core/furnace-apply-output.d.ts +10 -0
  202. package/dist/src/core/furnace-apply-output.js +31 -1
  203. package/dist/src/core/furnace-apply.js +12 -2
  204. package/dist/src/core/furnace-composed-source.d.ts +76 -0
  205. package/dist/src/core/furnace-composed-source.js +167 -0
  206. package/dist/src/core/furnace-config-validate-exceptions.d.ts +9 -0
  207. package/dist/src/core/furnace-config-validate-exceptions.js +71 -0
  208. package/dist/src/core/furnace-config-validate.js +8 -0
  209. package/dist/src/core/furnace-css-fragments.d.ts +10 -0
  210. package/dist/src/core/furnace-css-fragments.js +26 -0
  211. package/dist/src/core/furnace-operation.d.ts +3 -3
  212. package/dist/src/core/furnace-registration-remove.d.ts +22 -1
  213. package/dist/src/core/furnace-registration-remove.js +53 -17
  214. package/dist/src/core/furnace-registration-rename.d.ts +26 -0
  215. package/dist/src/core/furnace-registration-rename.js +77 -0
  216. package/dist/src/core/furnace-registration.d.ts +14 -4
  217. package/dist/src/core/furnace-registration.js +44 -10
  218. package/dist/src/core/furnace-staleness.d.ts +7 -0
  219. package/dist/src/core/furnace-staleness.js +34 -24
  220. package/dist/src/core/furnace-validate-accessibility.d.ts +4 -2
  221. package/dist/src/core/furnace-validate-accessibility.js +10 -1
  222. package/dist/src/core/furnace-validate-exceptions.d.ts +40 -0
  223. package/dist/src/core/furnace-validate-exceptions.js +43 -0
  224. package/dist/src/core/furnace-validate-icon-button.d.ts +7 -0
  225. package/dist/src/core/furnace-validate-icon-button.js +49 -0
  226. package/dist/src/core/furnace-validate.js +1 -1
  227. package/dist/src/core/harness-port-fallback.d.ts +60 -0
  228. package/dist/src/core/harness-port-fallback.js +96 -0
  229. package/dist/src/core/history-log-file.d.ts +61 -0
  230. package/dist/src/core/history-log-file.js +157 -0
  231. package/dist/src/core/history-log.d.ts +2 -1
  232. package/dist/src/core/history-log.js +2 -1
  233. package/dist/src/core/json-preserving-edit.d.ts +22 -0
  234. package/dist/src/core/json-preserving-edit.js +199 -0
  235. package/dist/src/core/mach.js +8 -1
  236. package/dist/src/core/negative-control-diff.d.ts +39 -0
  237. package/dist/src/core/negative-control-diff.js +136 -0
  238. package/dist/src/core/negative-control.d.ts +138 -0
  239. package/dist/src/core/negative-control.js +356 -0
  240. package/dist/src/core/objdir-install-manifests.d.ts +73 -0
  241. package/dist/src/core/objdir-install-manifests.js +226 -0
  242. package/dist/src/core/patch-export-update.d.ts +2 -0
  243. package/dist/src/core/patch-export-update.js +1 -0
  244. package/dist/src/core/patch-export.js +3 -4
  245. package/dist/src/core/patch-lint-checkjs-any-import.d.ts +37 -0
  246. package/dist/src/core/patch-lint-checkjs-any-import.js +61 -0
  247. package/dist/src/core/patch-lint-checkjs.js +8 -11
  248. package/dist/src/core/patch-lint-code-lines.d.ts +59 -0
  249. package/dist/src/core/patch-lint-code-lines.js +105 -0
  250. package/dist/src/core/patch-lint-cross.js +12 -4
  251. package/dist/src/core/patch-lint-css.d.ts +8 -2
  252. package/dist/src/core/patch-lint-css.js +10 -4
  253. package/dist/src/core/patch-lint-file-size.d.ts +12 -5
  254. package/dist/src/core/patch-lint-file-size.js +22 -11
  255. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +18 -0
  256. package/dist/src/core/patch-lint-forward-registration-extended.js +35 -10
  257. package/dist/src/core/patch-lint-forward-registration.js +10 -0
  258. package/dist/src/core/patch-lint-ignore.d.ts +152 -0
  259. package/dist/src/core/patch-lint-ignore.js +299 -0
  260. package/dist/src/core/patch-lint-size-thresholds.d.ts +52 -0
  261. package/dist/src/core/patch-lint-size-thresholds.js +110 -0
  262. package/dist/src/core/patch-lint-size.d.ts +117 -0
  263. package/dist/src/core/patch-lint-size.js +323 -0
  264. package/dist/src/core/patch-lint-staged-registration.d.ts +18 -1
  265. package/dist/src/core/patch-lint-staged-registration.js +20 -0
  266. package/dist/src/core/patch-lint-token-scale.d.ts +39 -0
  267. package/dist/src/core/patch-lint-token-scale.js +109 -0
  268. package/dist/src/core/patch-lint.d.ts +11 -83
  269. package/dist/src/core/patch-lint.js +64 -286
  270. package/dist/src/core/patch-lock.js +7 -6
  271. package/dist/src/core/patch-manifest-consistency.js +7 -6
  272. package/dist/src/core/patch-manifest-io.d.ts +6 -3
  273. package/dist/src/core/patch-manifest-io.js +19 -8
  274. package/dist/src/core/patch-manifest-validate.js +34 -2
  275. package/dist/src/core/patch-order-legal.d.ts +25 -0
  276. package/dist/src/core/patch-order-legal.js +83 -0
  277. package/dist/src/core/patch-policy-content.d.ts +40 -0
  278. package/dist/src/core/patch-policy-content.js +216 -0
  279. package/dist/src/core/patch-policy.d.ts +16 -3
  280. package/dist/src/core/patch-policy.js +16 -9
  281. package/dist/src/core/patch-retired.d.ts +35 -0
  282. package/dist/src/core/patch-retired.js +54 -0
  283. package/dist/src/core/patch-subset.d.ts +16 -0
  284. package/dist/src/core/patch-subset.js +66 -0
  285. package/dist/src/core/preflight-wait.d.ts +20 -0
  286. package/dist/src/core/preflight-wait.js +38 -0
  287. package/dist/src/core/rebase-from-version.d.ts +46 -0
  288. package/dist/src/core/rebase-from-version.js +71 -0
  289. package/dist/src/core/run-log.d.ts +20 -0
  290. package/dist/src/core/run-log.js +89 -3
  291. package/dist/src/core/run-profile.d.ts +7 -0
  292. package/dist/src/core/run-profile.js +7 -1
  293. package/dist/src/core/smoke-patterns.d.ts +5 -0
  294. package/dist/src/core/smoke-patterns.js +13 -0
  295. package/dist/src/core/test-harness-crash.d.ts +18 -1
  296. package/dist/src/core/test-harness-crash.js +6 -4
  297. package/dist/src/core/test-harness-signature.d.ts +17 -0
  298. package/dist/src/core/test-harness-signature.js +4 -0
  299. package/dist/src/core/test-run-completeness.d.ts +57 -0
  300. package/dist/src/core/test-run-completeness.js +144 -0
  301. package/dist/src/core/test-stale-check.d.ts +27 -1
  302. package/dist/src/core/test-stale-check.js +39 -49
  303. package/dist/src/core/test-stale-exempt.d.ts +40 -0
  304. package/dist/src/core/test-stale-exempt.js +70 -0
  305. package/dist/src/core/token-lint.d.ts +50 -0
  306. package/dist/src/core/token-lint.js +126 -0
  307. package/dist/src/core/tree-guard.js +3 -0
  308. package/dist/src/core/typecheck-shim.d.ts +1 -1
  309. package/dist/src/core/typecheck-shim.js +90 -2
  310. package/dist/src/index.d.ts +8 -3
  311. package/dist/src/index.js +3 -1
  312. package/dist/src/types/commands/index.d.ts +2 -2
  313. package/dist/src/types/commands/options.d.ts +148 -21
  314. package/dist/src/types/commands/patches.d.ts +75 -1
  315. package/dist/src/types/config.d.ts +192 -0
  316. package/dist/src/types/config.js +9 -0
  317. package/dist/src/types/furnace.d.ts +43 -1
  318. package/dist/src/types/furnace.js +7 -0
  319. package/dist/src/types/index.d.ts +1 -1
  320. package/dist/src/utils/options.d.ts +34 -6
  321. package/dist/src/utils/options.js +86 -5
  322. package/dist/src/utils/path-glob.d.ts +15 -0
  323. package/dist/src/utils/path-glob.js +47 -0
  324. package/dist/src/utils/regex.d.ts +15 -3
  325. package/dist/src/utils/regex.js +37 -16
  326. package/docs/configuration.md +282 -9
  327. package/docs/exit-codes.md +18 -1
  328. package/docs/furnace.md +97 -3
  329. package/docs/lifecycle-invariants.md +52 -19
  330. package/docs/machine-output.md +74 -12
  331. package/docs/patch-workflow.md +297 -22
  332. package/docs/run-logs.md +20 -1
  333. package/docs/testing.md +276 -20
  334. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import type { CustomComponentConfig, ValidationIssue } from '../types/furnace.js';
1
+ import type { CustomComponentConfig, FurnaceOptInCheck, ValidationIssue } from '../types/furnace.js';
2
2
  /**
3
3
  * Validates accessibility patterns in a component's .mjs file.
4
4
  * Checks for ARIA roles, keyboard handlers, l10n, and focus delegation.
@@ -11,8 +11,10 @@ import type { CustomComponentConfig, ValidationIssue } from '../types/furnace.js
11
11
  * (`keyboardCovered: true`) or via `composes` naming a native-interactive
12
12
  * inner element. Optional so stock/override callers and test fixtures
13
13
  * without config in scope can continue to call without changes.
14
+ * @param optInChecks - `furnace.json#optInChecks`. `icon-button-label`
15
+ * adds the icon-only `<moz-button>` naming check.
14
16
  */
15
- export declare function validateAccessibility(componentDir: string, tagName: string, customConfig?: CustomComponentConfig): Promise<ValidationIssue[]>;
17
+ export declare function validateAccessibility(componentDir: string, tagName: string, customConfig?: CustomComponentConfig, optInChecks?: readonly FurnaceOptInCheck[]): Promise<ValidationIssue[]>;
16
18
  /**
17
19
  * Detects hardcoded user-visible template text that should usually be
18
20
  * localized.
@@ -2,6 +2,7 @@
2
2
  import { join } from 'node:path';
3
3
  import { pathExists, readText } from '../utils/fs.js';
4
4
  import { createIssue } from './furnace-validate-helpers.js';
5
+ import { countUnlabelledIconButtons } from './furnace-validate-icon-button.js';
5
6
  import { collectLitTemplateRegions, parseTemplateAttributes } from './lit-template-regions.js';
6
7
  /**
7
8
  * Validates accessibility patterns in a component's .mjs file.
@@ -15,8 +16,10 @@ import { collectLitTemplateRegions, parseTemplateAttributes } from './lit-templa
15
16
  * (`keyboardCovered: true`) or via `composes` naming a native-interactive
16
17
  * inner element. Optional so stock/override callers and test fixtures
17
18
  * without config in scope can continue to call without changes.
19
+ * @param optInChecks - `furnace.json#optInChecks`. `icon-button-label`
20
+ * adds the icon-only `<moz-button>` naming check.
18
21
  */
19
- export async function validateAccessibility(componentDir, tagName, customConfig) {
22
+ export async function validateAccessibility(componentDir, tagName, customConfig, optInChecks = []) {
20
23
  const mjsPath = join(componentDir, `${tagName}.mjs`);
21
24
  if (!(await pathExists(mjsPath)))
22
25
  return [];
@@ -56,6 +59,12 @@ export async function validateAccessibility(componentDir, tagName, customConfig)
56
59
  if (hasUnlabelledFormInput(content)) {
57
60
  issues.push(createIssue(tagName, 'warning', 'unlabelled-form-input', 'Form input without an accessible label. Add aria-label, aria-labelledby, or an associated <label> element.'));
58
61
  }
62
+ if (optInChecks.includes('icon-button-label')) {
63
+ const unlabelled = countUnlabelledIconButtons(content);
64
+ if (unlabelled > 0) {
65
+ issues.push(createIssue(tagName, 'warning', 'icon-button-label', `${unlabelled} icon-only <moz-button> without an accessible name. Add data-l10n-id (with the label in its Fluent message) or aria-label.`));
66
+ }
67
+ }
59
68
  return issues;
60
69
  }
61
70
  /** Detects whether template or script content assigns an ARIA role. */
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Applies `furnace.json#validationExceptions[]` to a `furnace validate`
3
+ * result. An exception suppresses every finding of its check on its
4
+ * component. One that suppresses nothing is stale: the finding was fixed
5
+ * (or renamed) and the review record outlived it, so it is reported as a
6
+ * warning naming the entry to delete, which keeps the list honest under
7
+ * `--max-warnings 0`.
8
+ */
9
+ import type { FurnaceValidationException, ValidationIssue } from '../types/furnace.js';
10
+ /** The check name of the warning a stale exception produces. */
11
+ export declare const STALE_VALIDATION_EXCEPTION_CHECK = "stale-validation-exception";
12
+ /** A finding an exception suppressed, with the exception's reason. */
13
+ export interface ExceptedValidationIssue extends ValidationIssue {
14
+ reason: string;
15
+ }
16
+ /** {@link applyValidationExceptions}'s result. */
17
+ export interface ValidationExceptionOutcome {
18
+ /** Findings that still count, stale-exception warnings included. */
19
+ issues: ValidationIssue[];
20
+ /** Findings an exception suppressed. */
21
+ excepted: ExceptedValidationIssue[];
22
+ /** Exceptions that matched no finding of a component this run judged. */
23
+ stale: FurnaceValidationException[];
24
+ }
25
+ /**
26
+ * Splits findings into counted and excepted, and reports stale exceptions.
27
+ *
28
+ * @param issues - Every finding of the run
29
+ * @param exceptions - `furnace.json#validationExceptions`
30
+ * @param judged - Whether the run judged an exception's component. A run
31
+ * scoped to one component cannot call another component's exception
32
+ * stale, while the full run judges them all (including components
33
+ * furnace.json no longer declares)
34
+ */
35
+ export declare function applyValidationExceptions(issues: readonly ValidationIssue[], exceptions: readonly FurnaceValidationException[] | undefined, judged: (component: string) => boolean): ValidationExceptionOutcome;
36
+ /** Counts error- and warning-severity findings. */
37
+ export declare function countValidationSeverities(issues: readonly ValidationIssue[]): {
38
+ errors: number;
39
+ warnings: number;
40
+ };
@@ -0,0 +1,43 @@
1
+ import { createIssue } from './furnace-validate-helpers.js';
2
+ /** The check name of the warning a stale exception produces. */
3
+ export const STALE_VALIDATION_EXCEPTION_CHECK = 'stale-validation-exception';
4
+ function matches(exception, issue) {
5
+ return exception.component === issue.component && exception.check === issue.check;
6
+ }
7
+ /**
8
+ * Splits findings into counted and excepted, and reports stale exceptions.
9
+ *
10
+ * @param issues - Every finding of the run
11
+ * @param exceptions - `furnace.json#validationExceptions`
12
+ * @param judged - Whether the run judged an exception's component. A run
13
+ * scoped to one component cannot call another component's exception
14
+ * stale, while the full run judges them all (including components
15
+ * furnace.json no longer declares)
16
+ */
17
+ export function applyValidationExceptions(issues, exceptions, judged) {
18
+ const outcome = { issues: [], excepted: [], stale: [] };
19
+ const list = exceptions ?? [];
20
+ const used = new Set();
21
+ for (const issue of issues) {
22
+ const exception = list.find((entry) => matches(entry, issue));
23
+ if (exception === undefined) {
24
+ outcome.issues.push(issue);
25
+ continue;
26
+ }
27
+ used.add(exception);
28
+ outcome.excepted.push({ ...issue, reason: exception.reason });
29
+ }
30
+ for (const exception of list) {
31
+ if (used.has(exception) || !judged(exception.component))
32
+ continue;
33
+ outcome.stale.push(exception);
34
+ outcome.issues.push(createIssue(exception.component, 'warning', STALE_VALIDATION_EXCEPTION_CHECK, `furnace.json validationExceptions entry for [${exception.check}] matches no finding. ` +
35
+ 'Delete the entry: the finding it records is gone.'));
36
+ }
37
+ return outcome;
38
+ }
39
+ /** Counts error- and warning-severity findings. */
40
+ export function countValidationSeverities(issues) {
41
+ const errors = issues.filter((issue) => issue.severity === 'error').length;
42
+ return { errors, warnings: issues.length - errors };
43
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Counts icon-only `<moz-button>` elements in the module's Lit templates
3
+ * that carry neither `data-l10n-id` nor `aria-label`.
4
+ *
5
+ * @param content - The component module's source
6
+ */
7
+ export declare function countUnlabelledIconButtons(content: string): number;
@@ -0,0 +1,49 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * The opt-in `icon-button-label` check (`furnace.json#optInChecks`): an
4
+ * icon-only `<moz-button>` has no visible text, so its accessible name has
5
+ * to come from `data-l10n-id` (whose FTL message carries the label or
6
+ * `.aria-label`) or from `aria-label`. Without one, screen readers announce
7
+ * just "button".
8
+ *
9
+ * Scans the Lit `html` templates of the component module only. A button
10
+ * built with `document.createElement` is not seen.
11
+ */
12
+ import { collectLitTemplateRegions, parseTemplateAttributes } from './lit-template-regions.js';
13
+ /**
14
+ * Attributes (and their Lit property-binding spellings) that give the
15
+ * button an accessible name. `parseTemplateAttributes` lowercases names,
16
+ * so `.ariaLabel=${…}` arrives as `.arialabel`.
17
+ */
18
+ const NAMING_ATTRIBUTES = ['data-l10n-id', 'aria-label', '.arialabel'];
19
+ /** Attributes that give the button a visible label, so it is not icon-only. */
20
+ const LABEL_ATTRIBUTES = ['label', '.label'];
21
+ function isIconOnly(attrs, innerText) {
22
+ if (LABEL_ATTRIBUTES.some((name) => attrs.has(name)))
23
+ return false;
24
+ if (innerText.replace(/<[^>]*>/g, '').trim().length > 0)
25
+ return false;
26
+ const type = attrs.get('type') ?? '';
27
+ return attrs.has('iconsrc') || attrs.has('.iconsrc') || /(^|\s)icon(\s|$)/.test(type);
28
+ }
29
+ /**
30
+ * Counts icon-only `<moz-button>` elements in the module's Lit templates
31
+ * that carry neither `data-l10n-id` nor `aria-label`.
32
+ *
33
+ * @param content - The component module's source
34
+ */
35
+ export function countUnlabelledIconButtons(content) {
36
+ let count = 0;
37
+ for (const region of collectLitTemplateRegions(content)) {
38
+ const pattern = /<moz-button\b((?:"[^"]*"|'[^']*'|[^'">])*)>([\s\S]*?)<\/moz-button>/gi;
39
+ for (const match of region.matchAll(pattern)) {
40
+ const attrs = parseTemplateAttributes(match[1] ?? '');
41
+ if (!isIconOnly(attrs, match[2] ?? ''))
42
+ continue;
43
+ if (NAMING_ATTRIBUTES.some((name) => attrs.has(name)))
44
+ continue;
45
+ count++;
46
+ }
47
+ }
48
+ return count;
49
+ }
@@ -49,7 +49,7 @@ export async function validateComponent(componentDir, tagName, type, config, roo
49
49
  // parameter, so this is a no-op for stock and override components.
50
50
  const customConfigForTag = type === 'custom' ? config?.custom[tagName] : undefined;
51
51
  issues.push(...(await validateStructure(componentDir, tagName, type, customConfigForTag)));
52
- issues.push(...(await validateAccessibility(componentDir, tagName, customConfigForTag)));
52
+ issues.push(...(await validateAccessibility(componentDir, tagName, customConfigForTag, config?.optInChecks)));
53
53
  issues.push(...(await validateCompatibility(componentDir, tagName, type, config, root)));
54
54
  if (root && config && type === 'override') {
55
55
  const forgeConfig = await loadConfig(root);
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `fireforge test --auto-ports` (or `test.mochitestPorts: "auto"`): let two
3
+ * checkouts run browser suites at the same time.
4
+ *
5
+ * Desktop `mach mochitest` has no `--http-port`/`--ssl-port` options; those
6
+ * exist only in the Android and iOS argument sets. What recent engines do
7
+ * have is a fallback inside `testing/mochitest/runtests.py`: before starting
8
+ * its servers the harness bind-probes the HTTP server, ssltunnel and
9
+ * WebSocket ports, and before launching the browser the Marionette port,
10
+ * and moves any that is busy to a free port, routing `mochi.test:8888`
11
+ * through its proxy to wherever the server actually landed. FireForge's
12
+ * own preflight refused before that fallback could run. In auto mode it
13
+ * steps aside for a holder that is not this checkout's own (a sibling
14
+ * checkout's live suite, or an unrelated listener), and keeps refusing for
15
+ * this checkout's own leftovers, exactly as in the default mode.
16
+ *
17
+ * Auto mode is opt-in, and it only takes effect when the engine's harness
18
+ * is seen to carry the fallback: on an engine without it, a busy port would
19
+ * still collide, so the default refusal stays in force there.
20
+ */
21
+ import type { TestOptions } from '../types/commands/index.js';
22
+ import type { FireForgeConfig } from '../types/config.js';
23
+ import { type MarionettePortHolder } from './marionette-port.js';
24
+ /** Which busy-port fallbacks the engine's mochitest harness carries. */
25
+ export interface HarnessPortFallback {
26
+ /** HTTP server, ssltunnel and WebSocket ports (`startServers`). */
27
+ serverPorts: boolean;
28
+ /** The Marionette port (before the browser launch). */
29
+ marionettePort: boolean;
30
+ }
31
+ /**
32
+ * Reads the engine's `runtests.py` for the busy-port fallbacks. An
33
+ * unreadable file reports neither, which keeps the default refusal.
34
+ */
35
+ export declare function detectHarnessPortFallback(engineDir: string): Promise<HarnessPortFallback>;
36
+ /** True when the operator asked for auto ports, by flag or by config. */
37
+ export declare function autoPortsRequested(options: TestOptions, config: FireForgeConfig): boolean;
38
+ /**
39
+ * True when the holder's command line points into THIS checkout's engine
40
+ * directory: our own leftover, which auto mode never steps aside for.
41
+ */
42
+ export declare function isThisCheckoutsHolder(holder: MarionettePortHolder, engineDir: string): boolean;
43
+ /**
44
+ * Resolves the effective auto-ports posture for a browser run: what the
45
+ * engine's harness can fall back from, or `undefined` when auto mode is
46
+ * off or the engine cannot honour it (in which case the default refusal
47
+ * stays, and an explicit request says why).
48
+ */
49
+ export declare function resolveAutoPorts(engineDir: string, options: TestOptions, config: FireForgeConfig): Promise<HarnessPortFallback | undefined>;
50
+ /**
51
+ * Steps aside for a busy port the harness will fall back from. Returns true
52
+ * (after a notice naming the holder) when the port is held by anything that
53
+ * is not this checkout's own process, false when the port is free or the
54
+ * holder is ours, so the caller's normal preflight runs.
55
+ *
56
+ * @param port - The default port the harness would use
57
+ * @param label - Human name of the port for the notice
58
+ * @param engineDir - This checkout's engine directory
59
+ */
60
+ export declare function deferBusyPortToHarness(port: number, label: string, engineDir: string): Promise<boolean>;
@@ -0,0 +1,96 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * `fireforge test --auto-ports` (or `test.mochitestPorts: "auto"`): let two
4
+ * checkouts run browser suites at the same time.
5
+ *
6
+ * Desktop `mach mochitest` has no `--http-port`/`--ssl-port` options; those
7
+ * exist only in the Android and iOS argument sets. What recent engines do
8
+ * have is a fallback inside `testing/mochitest/runtests.py`: before starting
9
+ * its servers the harness bind-probes the HTTP server, ssltunnel and
10
+ * WebSocket ports, and before launching the browser the Marionette port,
11
+ * and moves any that is busy to a free port, routing `mochi.test:8888`
12
+ * through its proxy to wherever the server actually landed. FireForge's
13
+ * own preflight refused before that fallback could run. In auto mode it
14
+ * steps aside for a holder that is not this checkout's own (a sibling
15
+ * checkout's live suite, or an unrelated listener), and keeps refusing for
16
+ * this checkout's own leftovers, exactly as in the default mode.
17
+ *
18
+ * Auto mode is opt-in, and it only takes effect when the engine's harness
19
+ * is seen to carry the fallback: on an engine without it, a busy port would
20
+ * still collide, so the default refusal stays in force there.
21
+ */
22
+ import { readFile } from 'node:fs/promises';
23
+ import { join } from 'node:path';
24
+ import { notice } from '../utils/logger.js';
25
+ import { isExplicitAbsolutePath, isPathInsideRoot } from '../utils/paths.js';
26
+ import { probeMarionettePort } from './marionette-port.js';
27
+ const RUNTESTS_PATH = 'testing/mochitest/runtests.py';
28
+ /**
29
+ * Reads the engine's `runtests.py` for the busy-port fallbacks. An
30
+ * unreadable file reports neither, which keeps the default refusal.
31
+ */
32
+ export async function detectHarnessPortFallback(engineDir) {
33
+ let source;
34
+ try {
35
+ source = await readFile(join(engineDir, RUNTESTS_PATH), 'utf8');
36
+ }
37
+ catch {
38
+ return { serverPorts: false, marionettePort: false };
39
+ }
40
+ const fallsBack = /falling back to port/.test(source);
41
+ return {
42
+ serverPorts: fallsBack && /\("httpPort",/.test(source) && /_port_in_use\(port\)/.test(source),
43
+ marionettePort: fallsBack && /_port_in_use\(marionette_port\)/.test(source),
44
+ };
45
+ }
46
+ /** True when the operator asked for auto ports, by flag or by config. */
47
+ export function autoPortsRequested(options, config) {
48
+ return options.autoPorts === true || config.test?.mochitestPorts === 'auto';
49
+ }
50
+ /**
51
+ * True when the holder's command line points into THIS checkout's engine
52
+ * directory: our own leftover, which auto mode never steps aside for.
53
+ */
54
+ export function isThisCheckoutsHolder(holder, engineDir) {
55
+ return holder.commandLine
56
+ .split(/\s+/)
57
+ .filter(isExplicitAbsolutePath)
58
+ .some((token) => isPathInsideRoot(engineDir, token));
59
+ }
60
+ /**
61
+ * Resolves the effective auto-ports posture for a browser run: what the
62
+ * engine's harness can fall back from, or `undefined` when auto mode is
63
+ * off or the engine cannot honour it (in which case the default refusal
64
+ * stays, and an explicit request says why).
65
+ */
66
+ export async function resolveAutoPorts(engineDir, options, config) {
67
+ if (!autoPortsRequested(options, config))
68
+ return undefined;
69
+ const fallback = await detectHarnessPortFallback(engineDir);
70
+ if (!fallback.serverPorts && !fallback.marionettePort) {
71
+ notice(`Auto ports requested, but engine/${RUNTESTS_PATH} has no busy-port fallback, so a busy ` +
72
+ 'mochitest or Marionette port is still refused as in the default mode.');
73
+ return undefined;
74
+ }
75
+ return fallback;
76
+ }
77
+ /**
78
+ * Steps aside for a busy port the harness will fall back from. Returns true
79
+ * (after a notice naming the holder) when the port is held by anything that
80
+ * is not this checkout's own process, false when the port is free or the
81
+ * holder is ours, so the caller's normal preflight runs.
82
+ *
83
+ * @param port - The default port the harness would use
84
+ * @param label - Human name of the port for the notice
85
+ * @param engineDir - This checkout's engine directory
86
+ */
87
+ export async function deferBusyPortToHarness(port, label, engineDir) {
88
+ const probe = await probeMarionettePort(port);
89
+ if (!probe.inUse || !probe.holder)
90
+ return false;
91
+ if (isThisCheckoutsHolder(probe.holder, engineDir))
92
+ return false;
93
+ notice(`${label} port ${port} is held by ${probe.holder.command} (PID ${probe.holder.pid}), which is ` +
94
+ "not this checkout's. Auto ports: the harness moves this run to a free port instead.");
95
+ return true;
96
+ }
@@ -0,0 +1,61 @@
1
+ /** Filename of the history log at its pre-0.50 location, inside `patches/`. */
2
+ export declare const LEGACY_HISTORY_LOG_FILENAME = ".fireforge-history.jsonl";
3
+ /** Default history log location, relative to the project root. */
4
+ export declare const DEFAULT_HISTORY_LOG_PATH: string;
5
+ /** Environment variable a caller sets to tag every record of its session. */
6
+ export declare const HISTORY_SESSION_ENV = "FIREFORGE_SESSION_ID";
7
+ /** Who wrote a history record. */
8
+ export interface HistoryRecordIdentity {
9
+ /** One id per FireForge process: every record one command writes shares it. */
10
+ invocationId: string;
11
+ /** The caller's `FIREFORGE_SESSION_ID`, when set. */
12
+ sessionId?: string;
13
+ pid: number;
14
+ ppid: number;
15
+ }
16
+ /**
17
+ * Identity fields stamped on every history record: one id per FireForge
18
+ * process (every record one command writes shares it), the process and its
19
+ * parent, and the caller's `FIREFORGE_SESSION_ID` when set. The parent PID
20
+ * is a weak hint (usually the shell); the environment variable is the
21
+ * reliable way for a session to tag its own lines.
22
+ */
23
+ export declare function historyRecordIdentity(): HistoryRecordIdentity;
24
+ /**
25
+ * Resolves the history log path for a project.
26
+ *
27
+ * @param projectRoot - Project root containing `fireforge.json`
28
+ * @returns Absolute path of the history log
29
+ */
30
+ export declare function resolveHistoryLogPath(projectRoot: string): Promise<string>;
31
+ /**
32
+ * Moves a history log left at the legacy `patches/` path to `target`.
33
+ *
34
+ * Records are never dropped, whatever else is happening:
35
+ * - The legacy file is first claimed by renaming it aside, so of two
36
+ * commands migrating at once exactly one does the work and neither
37
+ * duplicates it.
38
+ * - When `target` does not exist yet (the first run), the claimed file is
39
+ * hard-linked into place, which cannot overwrite a log another command
40
+ * created in the meantime, and keeps a large log a constant-time move.
41
+ * - When both exist (an older FireForge appended to the legacy path after
42
+ * a migration, or `history.path` changed), the legacy records are
43
+ * appended to the target in one write. They then follow newer records;
44
+ * every record carries its own `ts`.
45
+ *
46
+ * A crash between the claim and the move leaves the records in a
47
+ * `.migrating-*` file next to the legacy path, never deleted.
48
+ *
49
+ * @param patchesDir - The patches directory that may hold a legacy log
50
+ * @param target - Absolute path of the history log
51
+ */
52
+ export declare function migrateLegacyHistoryLog(patchesDir: string, target: string): Promise<void>;
53
+ /**
54
+ * Appends one serialized record to the project's history log, migrating a
55
+ * legacy log first.
56
+ *
57
+ * @param patchesDir - The project's `patches/` directory; the project root is
58
+ * its parent, as every caller derives it from `getProjectPaths`
59
+ * @param line - One JSON record, without the trailing newline
60
+ */
61
+ export declare function appendHistoryLine(patchesDir: string, line: string): Promise<void>;
@@ -0,0 +1,157 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Where the patch history log lives, and who wrote each record.
4
+ *
5
+ * The log used to be `patches/.fireforge-history.jsonl`, inside the tracked
6
+ * patch queue, so it was committed with every export until consumers
7
+ * gitignored it, and it grew to tens of megabytes. It now defaults to
8
+ * `.fireforge/history.jsonl` (FireForge's own untracked data directory), or
9
+ * `fireforge.json#history.path` when a project sets one. A log left at the
10
+ * old path is moved on the first append.
11
+ *
12
+ * Several sessions sharing one checkout all append to the same log, so each
13
+ * record carries the invocation that wrote it, plus a session hint, letting
14
+ * a session select its own lines mechanically.
15
+ */
16
+ import { randomUUID } from 'node:crypto';
17
+ import { appendFile, link, readFile, rename, rm, stat } from 'node:fs/promises';
18
+ import { dirname, join, resolve } from 'node:path';
19
+ import { getNodeErrorCode, toError } from '../utils/errors.js';
20
+ import { ensureDir } from '../utils/fs.js';
21
+ import { info, verbose } from '../utils/logger.js';
22
+ import { FIREFORGE_DIR } from './config-paths.js';
23
+ import { isValidHistoryPath } from './config-validate-history.js';
24
+ /** Filename of the history log at its pre-0.50 location, inside `patches/`. */
25
+ export const LEGACY_HISTORY_LOG_FILENAME = '.fireforge-history.jsonl';
26
+ /** Default history log location, relative to the project root. */
27
+ export const DEFAULT_HISTORY_LOG_PATH = join(FIREFORGE_DIR, 'history.jsonl');
28
+ /** Environment variable a caller sets to tag every record of its session. */
29
+ export const HISTORY_SESSION_ENV = 'FIREFORGE_SESSION_ID';
30
+ let invocationId;
31
+ /**
32
+ * Identity fields stamped on every history record: one id per FireForge
33
+ * process (every record one command writes shares it), the process and its
34
+ * parent, and the caller's `FIREFORGE_SESSION_ID` when set. The parent PID
35
+ * is a weak hint (usually the shell); the environment variable is the
36
+ * reliable way for a session to tag its own lines.
37
+ */
38
+ export function historyRecordIdentity() {
39
+ invocationId ??= randomUUID();
40
+ const session = process.env[HISTORY_SESSION_ENV]?.trim();
41
+ return {
42
+ invocationId,
43
+ ...(session !== undefined && session !== '' ? { sessionId: session } : {}),
44
+ pid: process.pid,
45
+ ppid: process.ppid,
46
+ };
47
+ }
48
+ /**
49
+ * Reads `history.path` from the project's raw `fireforge.json`.
50
+ *
51
+ * Fail-open: the writer runs after a mutation has already committed, so a
52
+ * missing or unreadable config means "use the default", never an error. An
53
+ * invalid value is refused by config validation when the command loads its
54
+ * config; here it also falls back to the default rather than writing
55
+ * somewhere validation would not allow.
56
+ */
57
+ async function readConfiguredHistoryPath(projectRoot) {
58
+ let data;
59
+ try {
60
+ data = JSON.parse(await readFile(join(projectRoot, 'fireforge.json'), 'utf-8'));
61
+ }
62
+ catch {
63
+ return undefined;
64
+ }
65
+ if (typeof data !== 'object' || data === null)
66
+ return undefined;
67
+ const history = data['history'];
68
+ if (typeof history !== 'object' || history === null)
69
+ return undefined;
70
+ const path = history['path'];
71
+ if (path === undefined)
72
+ return undefined;
73
+ if (!isValidHistoryPath(path)) {
74
+ verbose(`Ignoring invalid history.path in fireforge.json; using ${DEFAULT_HISTORY_LOG_PATH}`);
75
+ return undefined;
76
+ }
77
+ return path;
78
+ }
79
+ /**
80
+ * Resolves the history log path for a project.
81
+ *
82
+ * @param projectRoot - Project root containing `fireforge.json`
83
+ * @returns Absolute path of the history log
84
+ */
85
+ export async function resolveHistoryLogPath(projectRoot) {
86
+ const configured = await readConfiguredHistoryPath(projectRoot);
87
+ return resolve(projectRoot, configured ?? DEFAULT_HISTORY_LOG_PATH);
88
+ }
89
+ /**
90
+ * Moves a history log left at the legacy `patches/` path to `target`.
91
+ *
92
+ * Records are never dropped, whatever else is happening:
93
+ * - The legacy file is first claimed by renaming it aside, so of two
94
+ * commands migrating at once exactly one does the work and neither
95
+ * duplicates it.
96
+ * - When `target` does not exist yet (the first run), the claimed file is
97
+ * hard-linked into place, which cannot overwrite a log another command
98
+ * created in the meantime, and keeps a large log a constant-time move.
99
+ * - When both exist (an older FireForge appended to the legacy path after
100
+ * a migration, or `history.path` changed), the legacy records are
101
+ * appended to the target in one write. They then follow newer records;
102
+ * every record carries its own `ts`.
103
+ *
104
+ * A crash between the claim and the move leaves the records in a
105
+ * `.migrating-*` file next to the legacy path, never deleted.
106
+ *
107
+ * @param patchesDir - The patches directory that may hold a legacy log
108
+ * @param target - Absolute path of the history log
109
+ */
110
+ export async function migrateLegacyHistoryLog(patchesDir, target) {
111
+ const legacy = join(patchesDir, LEGACY_HISTORY_LOG_FILENAME);
112
+ if (resolve(legacy) === resolve(target))
113
+ return;
114
+ try {
115
+ await stat(legacy);
116
+ }
117
+ catch {
118
+ return;
119
+ }
120
+ const claimed = `${legacy}.migrating-${process.pid}-${randomUUID()}`;
121
+ try {
122
+ await rename(legacy, claimed);
123
+ }
124
+ catch (error) {
125
+ if (getNodeErrorCode(error) === 'ENOENT')
126
+ return;
127
+ throw error;
128
+ }
129
+ await ensureDir(dirname(target));
130
+ try {
131
+ await link(claimed, target);
132
+ }
133
+ catch (error) {
134
+ // EEXIST: both logs exist. Anything else (a filesystem without hard
135
+ // links, a target on another device): the append below creates the
136
+ // target when it is missing, so it covers that case too.
137
+ verbose(`History log migration falls back to appending (${toError(error).message})`);
138
+ await appendFile(target, await readFile(claimed));
139
+ }
140
+ await rm(claimed, { force: true });
141
+ info(`Moved the patch history log from ${legacy} to ${target}. ` +
142
+ 'Nothing reads it back; it is an audit trail only.');
143
+ }
144
+ /**
145
+ * Appends one serialized record to the project's history log, migrating a
146
+ * legacy log first.
147
+ *
148
+ * @param patchesDir - The project's `patches/` directory; the project root is
149
+ * its parent, as every caller derives it from `getProjectPaths`
150
+ * @param line - One JSON record, without the trailing newline
151
+ */
152
+ export async function appendHistoryLine(patchesDir, line) {
153
+ const target = await resolveHistoryLogPath(dirname(resolve(patchesDir)));
154
+ await migrateLegacyHistoryLog(patchesDir, target);
155
+ await ensureDir(dirname(target));
156
+ await appendFile(target, `${line}\n`, 'utf-8');
157
+ }
@@ -10,7 +10,8 @@ import type { HistoryEntry } from './destructive.js';
10
10
  * reject would report a completed, irreversible mutation as a failure and send
11
11
  * the operator looking for damage that is not there. Because that reasoning is
12
12
  * easy to lose in a copy, it now lives in one place.
13
- * @param patchesDir - The `patches/` directory holding the history log.
13
+ * @param patchesDir - The project's `patches/` directory (the log itself lives
14
+ * under `.fireforge/` or `history.path`; see `history-log-file.ts`).
14
15
  * @param entry - The entry to append.
15
16
  * @param context - What just committed, rendered into
16
17
  * `History log append failed after <context>: <reason>`, e.g.
@@ -13,7 +13,8 @@ import { appendHistory } from './destructive.js';
13
13
  * reject would report a completed, irreversible mutation as a failure and send
14
14
  * the operator looking for damage that is not there. Because that reasoning is
15
15
  * easy to lose in a copy, it now lives in one place.
16
- * @param patchesDir - The `patches/` directory holding the history log.
16
+ * @param patchesDir - The project's `patches/` directory (the log itself lives
17
+ * under `.fireforge/` or `history.path`; see `history-log-file.ts`).
17
18
  * @param entry - The entry to append.
18
19
  * @param context - What just committed, rendered into
19
20
  * `History log append failed after <context>: <reason>`, e.g.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Format-preserving rewrites of a JSON document.
3
+ *
4
+ * `fireforge source set`, `fireforge config` and `furnace init` change a
5
+ * few keys of `fireforge.json`. Re-serializing the whole document with
6
+ * `JSON.stringify` expands every compact object and array the project keeps
7
+ * on one line, so a one-key change becomes a diff across unrelated policy
8
+ * sections. This module diffs the document on disk against the wanted one
9
+ * and edits only the values that differ, in place, keeping every other byte
10
+ * (indentation, compact containers, key order, trailing newline).
11
+ *
12
+ * Inserted values follow the style of the container they land in: one line
13
+ * inside a one-line container, the container's own indentation otherwise.
14
+ */
15
+ import type { JsonObject } from '../types/json.js';
16
+ /**
17
+ * Rewrites `text` so it parses to `next`, editing only the values that
18
+ * differ. Returns `undefined` when `text` is not a JSON object or the edit
19
+ * cannot be proven equivalent, so the caller can fall back to a full
20
+ * re-serialization.
21
+ */
22
+ export declare function rewriteJsonPreservingFormat(text: string, next: JsonObject): string | undefined;