@hominis/fireforge 0.49.2 → 0.51.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 (400) hide show
  1. package/CHANGELOG.md +87 -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 +41 -0
  10. package/dist/src/commands/check-report.js +91 -0
  11. package/dist/src/commands/check-run.d.ts +33 -0
  12. package/dist/src/commands/check-run.js +112 -0
  13. package/dist/src/commands/check-steps.d.ts +75 -0
  14. package/dist/src/commands/check-steps.js +200 -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/declarations-report.d.ts +43 -0
  19. package/dist/src/commands/declarations-report.js +76 -0
  20. package/dist/src/commands/doctor-engine-eslint.js +6 -1
  21. package/dist/src/commands/doctor-source-pin.js +4 -1
  22. package/dist/src/commands/export-adopt-check.d.ts +38 -0
  23. package/dist/src/commands/export-adopt-check.js +98 -0
  24. package/dist/src/commands/export-flow.js +29 -14
  25. package/dist/src/commands/export-placement-gate.d.ts +6 -2
  26. package/dist/src/commands/export-placement-gate.js +8 -0
  27. package/dist/src/commands/export-shared.d.ts +2 -1
  28. package/dist/src/commands/export-staged-registration.d.ts +46 -0
  29. package/dist/src/commands/export-staged-registration.js +91 -0
  30. package/dist/src/commands/forward-import-remedy.d.ts +32 -0
  31. package/dist/src/commands/forward-import-remedy.js +63 -0
  32. package/dist/src/commands/furnace/apply.js +3 -3
  33. package/dist/src/commands/furnace/chrome-doc-remove.d.ts +2 -1
  34. package/dist/src/commands/furnace/chrome-doc-remove.js +2 -2
  35. package/dist/src/commands/furnace/chrome-doc.d.ts +2 -1
  36. package/dist/src/commands/furnace/chrome-doc.js +2 -2
  37. package/dist/src/commands/furnace/create.js +2 -2
  38. package/dist/src/commands/furnace/deploy-named.d.ts +48 -0
  39. package/dist/src/commands/furnace/deploy-named.js +107 -0
  40. package/dist/src/commands/furnace/deploy.d.ts +3 -2
  41. package/dist/src/commands/furnace/deploy.js +50 -86
  42. package/dist/src/commands/furnace/diff.js +26 -3
  43. package/dist/src/commands/furnace/index.js +66 -41
  44. package/dist/src/commands/furnace/init.js +13 -6
  45. package/dist/src/commands/furnace/override.js +4 -2
  46. package/dist/src/commands/furnace/preview.js +4 -3
  47. package/dist/src/commands/furnace/refresh.js +2 -2
  48. package/dist/src/commands/furnace/remove.js +4 -3
  49. package/dist/src/commands/furnace/rename-dry-run.d.ts +29 -0
  50. package/dist/src/commands/furnace/rename-dry-run.js +127 -0
  51. package/dist/src/commands/furnace/rename-helpers.d.ts +7 -0
  52. package/dist/src/commands/furnace/rename-helpers.js +15 -1
  53. package/dist/src/commands/furnace/rename-registrations.d.ts +41 -0
  54. package/dist/src/commands/furnace/rename-registrations.js +98 -0
  55. package/dist/src/commands/furnace/rename.d.ts +2 -1
  56. package/dist/src/commands/furnace/rename.js +28 -66
  57. package/dist/src/commands/furnace/scan.d.ts +9 -4
  58. package/dist/src/commands/furnace/scan.js +11 -9
  59. package/dist/src/commands/furnace/sync.js +3 -2
  60. package/dist/src/commands/furnace/validate-report.d.ts +45 -0
  61. package/dist/src/commands/furnace/validate-report.js +91 -0
  62. package/dist/src/commands/furnace/validate.d.ts +1 -1
  63. package/dist/src/commands/furnace/validate.js +151 -113
  64. package/dist/src/commands/furnace/validation-output.js +14 -3
  65. package/dist/src/commands/lint-per-patch.js +36 -61
  66. package/dist/src/commands/lint-per-run-checkjs.js +2 -2
  67. package/dist/src/commands/lint-report.d.ts +8 -4
  68. package/dist/src/commands/lint-report.js +20 -5
  69. package/dist/src/commands/lint.js +15 -34
  70. package/dist/src/commands/manifest.js +4 -0
  71. package/dist/src/commands/park.d.ts +22 -0
  72. package/dist/src/commands/park.js +232 -0
  73. package/dist/src/commands/patch/delete-gap-notice.js +3 -1
  74. package/dist/src/commands/patch/delete.d.ts +1 -1
  75. package/dist/src/commands/patch/delete.js +24 -104
  76. package/dist/src/commands/patch/index.d.ts +3 -3
  77. package/dist/src/commands/patch/index.js +10 -4
  78. package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
  79. package/dist/src/commands/patch/lint-ignore-from.js +142 -0
  80. package/dist/src/commands/patch/lint-ignore.d.ts +5 -1
  81. package/dist/src/commands/patch/lint-ignore.js +78 -31
  82. package/dist/src/commands/patch/move-files-create.js +34 -18
  83. package/dist/src/commands/patch/move-files-into.js +15 -7
  84. package/dist/src/commands/patch/move-files.js +6 -2
  85. package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
  86. package/dist/src/commands/patch/removal-created-files.js +83 -0
  87. package/dist/src/commands/patch/removal-dependents.d.ts +63 -0
  88. package/dist/src/commands/patch/removal-dependents.js +133 -0
  89. package/dist/src/commands/patch/rename.js +20 -1
  90. package/dist/src/commands/patch/reserve.d.ts +33 -0
  91. package/dist/src/commands/patch/reserve.js +152 -0
  92. package/dist/src/commands/patch/retire.d.ts +34 -0
  93. package/dist/src/commands/patch/retire.js +133 -0
  94. package/dist/src/commands/patch/split-plan.d.ts +17 -8
  95. package/dist/src/commands/patch/split-plan.js +60 -28
  96. package/dist/src/commands/patch/split-staged.d.ts +108 -0
  97. package/dist/src/commands/patch/split-staged.js +202 -0
  98. package/dist/src/commands/patch/split.d.ts +5 -0
  99. package/dist/src/commands/patch/split.js +62 -17
  100. package/dist/src/commands/patch/stage.d.ts +34 -0
  101. package/dist/src/commands/patch/stage.js +153 -0
  102. package/dist/src/commands/patch/staged-dependency-batch.d.ts +48 -0
  103. package/dist/src/commands/patch/staged-dependency-batch.js +176 -0
  104. package/dist/src/commands/patch/staged-dependency-entries.d.ts +36 -0
  105. package/dist/src/commands/patch/staged-dependency-entries.js +52 -0
  106. package/dist/src/commands/patch/staged-dependency-from.d.ts +14 -0
  107. package/dist/src/commands/patch/staged-dependency-from.js +108 -0
  108. package/dist/src/commands/patch/staged-dependency-remove.d.ts +31 -0
  109. package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
  110. package/dist/src/commands/patch/staged-dependency.js +54 -139
  111. package/dist/src/commands/re-export-adjacent-registrations.d.ts +20 -0
  112. package/dist/src/commands/re-export-adjacent-registrations.js +75 -0
  113. package/dist/src/commands/re-export-adjacent.d.ts +50 -2
  114. package/dist/src/commands/re-export-adjacent.js +160 -40
  115. package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
  116. package/dist/src/commands/re-export-bulk-scan.js +22 -7
  117. package/dist/src/commands/re-export-drift.d.ts +36 -8
  118. package/dist/src/commands/re-export-drift.js +73 -49
  119. package/dist/src/commands/re-export-files.js +11 -8
  120. package/dist/src/commands/re-export-hunks.d.ts +35 -0
  121. package/dist/src/commands/re-export-hunks.js +220 -0
  122. package/dist/src/commands/re-export-lint.d.ts +2 -1
  123. package/dist/src/commands/re-export-lint.js +23 -10
  124. package/dist/src/commands/re-export-options.js +74 -6
  125. package/dist/src/commands/re-export-preflight.d.ts +23 -0
  126. package/dist/src/commands/re-export-preflight.js +160 -0
  127. package/dist/src/commands/re-export-purity-scope.d.ts +18 -0
  128. package/dist/src/commands/re-export-purity-scope.js +131 -0
  129. package/dist/src/commands/re-export-register.js +56 -17
  130. package/dist/src/commands/re-export-removed.d.ts +13 -2
  131. package/dist/src/commands/re-export-removed.js +57 -12
  132. package/dist/src/commands/re-export-scan-owner.d.ts +44 -0
  133. package/dist/src/commands/re-export-scan-owner.js +116 -0
  134. package/dist/src/commands/re-export-scan.d.ts +19 -9
  135. package/dist/src/commands/re-export-scan.js +60 -24
  136. package/dist/src/commands/re-export-single.js +122 -46
  137. package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
  138. package/dist/src/commands/re-export-stale-registrations.js +62 -0
  139. package/dist/src/commands/re-export-summary.d.ts +28 -0
  140. package/dist/src/commands/re-export-summary.js +36 -0
  141. package/dist/src/commands/re-export.js +71 -57
  142. package/dist/src/commands/rebase/index.js +17 -17
  143. package/dist/src/commands/run.js +16 -2
  144. package/dist/src/commands/status-check.d.ts +55 -16
  145. package/dist/src/commands/status-check.js +92 -37
  146. package/dist/src/commands/status-json.d.ts +22 -9
  147. package/dist/src/commands/status-json.js +47 -32
  148. package/dist/src/commands/status-output.js +3 -1
  149. package/dist/src/commands/status-scope.d.ts +37 -0
  150. package/dist/src/commands/status-scope.js +54 -0
  151. package/dist/src/commands/status.js +66 -23
  152. package/dist/src/commands/test-browser-preflight.js +19 -3
  153. package/dist/src/commands/test-build-furnace.d.ts +22 -0
  154. package/dist/src/commands/test-build-furnace.js +54 -0
  155. package/dist/src/commands/test-build-phase.js +64 -41
  156. package/dist/src/commands/test-checkjs.js +10 -2
  157. package/dist/src/commands/test-diagnose.js +7 -0
  158. package/dist/src/commands/test-dispatch.d.ts +39 -0
  159. package/dist/src/commands/test-dispatch.js +211 -0
  160. package/dist/src/commands/test-furnace-reach.d.ts +37 -0
  161. package/dist/src/commands/test-furnace-reach.js +132 -0
  162. package/dist/src/commands/test-modes.d.ts +16 -1
  163. package/dist/src/commands/test-modes.js +24 -1
  164. package/dist/src/commands/test-multi-verdict.d.ts +61 -0
  165. package/dist/src/commands/test-multi-verdict.js +111 -0
  166. package/dist/src/commands/test-orchestrate.d.ts +52 -0
  167. package/dist/src/commands/test-orchestrate.js +240 -0
  168. package/dist/src/commands/test-perf-scope.d.ts +50 -0
  169. package/dist/src/commands/test-perf-scope.js +138 -0
  170. package/dist/src/commands/test-prelock.d.ts +51 -0
  171. package/dist/src/commands/test-prelock.js +273 -0
  172. package/dist/src/commands/test-register.js +69 -10
  173. package/dist/src/commands/test-results.d.ts +62 -0
  174. package/dist/src/commands/test-results.js +62 -0
  175. package/dist/src/commands/test-run.d.ts +13 -55
  176. package/dist/src/commands/test-run.js +74 -99
  177. package/dist/src/commands/test-shards.d.ts +110 -0
  178. package/dist/src/commands/test-shards.js +255 -0
  179. package/dist/src/commands/test-stale-gate.d.ts +5 -1
  180. package/dist/src/commands/test-stale-gate.js +60 -28
  181. package/dist/src/commands/test-verdict.d.ts +11 -1
  182. package/dist/src/commands/test-verdict.js +28 -1
  183. package/dist/src/commands/test.d.ts +6 -1
  184. package/dist/src/commands/test.js +120 -124
  185. package/dist/src/commands/token-lint.d.ts +14 -0
  186. package/dist/src/commands/token-lint.js +151 -0
  187. package/dist/src/commands/token.js +10 -0
  188. package/dist/src/commands/verify-scope.d.ts +28 -0
  189. package/dist/src/commands/verify-scope.js +67 -0
  190. package/dist/src/commands/verify.d.ts +10 -2
  191. package/dist/src/commands/verify.js +67 -36
  192. package/dist/src/core/build-baseline-types.d.ts +46 -0
  193. package/dist/src/core/build-baseline-validate.js +19 -0
  194. package/dist/src/core/build-baseline.d.ts +5 -1
  195. package/dist/src/core/build-baseline.js +3 -1
  196. package/dist/src/core/build-overwrite-guard.d.ts +50 -0
  197. package/dist/src/core/build-overwrite-guard.js +84 -4
  198. package/dist/src/core/build-prepare.d.ts +13 -0
  199. package/dist/src/core/build-prepare.js +42 -10
  200. package/dist/src/core/build-run-record.d.ts +74 -0
  201. package/dist/src/core/build-run-record.js +136 -0
  202. package/dist/src/core/checkjs-support-heads.d.ts +41 -0
  203. package/dist/src/core/checkjs-support-heads.js +104 -0
  204. package/dist/src/core/config-paths.d.ts +2 -2
  205. package/dist/src/core/config-paths.js +10 -0
  206. package/dist/src/core/config-validate-check.d.ts +8 -0
  207. package/dist/src/core/config-validate-check.js +219 -0
  208. package/dist/src/core/config-validate-file-size.d.ts +9 -6
  209. package/dist/src/core/config-validate-file-size.js +91 -2
  210. package/dist/src/core/config-validate-history.d.ts +11 -0
  211. package/dist/src/core/config-validate-history.js +38 -0
  212. package/dist/src/core/config-validate-patch-policy.js +100 -0
  213. package/dist/src/core/config-validate-test-toolchains.js +21 -0
  214. package/dist/src/core/config-validate-token-scales.d.ts +9 -0
  215. package/dist/src/core/config-validate-token-scales.js +92 -0
  216. package/dist/src/core/config-validate.js +20 -5
  217. package/dist/src/core/config.d.ts +5 -0
  218. package/dist/src/core/config.js +16 -0
  219. package/dist/src/core/coverage-dispatch.d.ts +24 -0
  220. package/dist/src/core/coverage-dispatch.js +44 -0
  221. package/dist/src/core/coverage-extend.d.ts +70 -45
  222. package/dist/src/core/coverage-extend.js +199 -144
  223. package/dist/src/core/coverage-records.d.ts +108 -0
  224. package/dist/src/core/coverage-records.js +384 -0
  225. package/dist/src/core/destructive.d.ts +7 -4
  226. package/dist/src/core/destructive.js +10 -10
  227. package/dist/src/core/engine-session-lock.d.ts +16 -1
  228. package/dist/src/core/engine-session-lock.js +38 -10
  229. package/dist/src/core/export-moved-code-shape.d.ts +49 -11
  230. package/dist/src/core/export-moved-code-shape.js +135 -44
  231. package/dist/src/core/file-lock-queue.d.ts +67 -0
  232. package/dist/src/core/file-lock-queue.js +221 -0
  233. package/dist/src/core/file-lock-shared.d.ts +57 -0
  234. package/dist/src/core/file-lock-shared.js +117 -0
  235. package/dist/src/core/file-lock.d.ts +13 -25
  236. package/dist/src/core/file-lock.js +163 -140
  237. package/dist/src/core/furnace-apply-output.d.ts +10 -0
  238. package/dist/src/core/furnace-apply-output.js +31 -1
  239. package/dist/src/core/furnace-apply.js +22 -12
  240. package/dist/src/core/furnace-composed-source.d.ts +76 -0
  241. package/dist/src/core/furnace-composed-source.js +167 -0
  242. package/dist/src/core/furnace-config-validate-exceptions.d.ts +16 -0
  243. package/dist/src/core/furnace-config-validate-exceptions.js +102 -0
  244. package/dist/src/core/furnace-config-validate.js +14 -0
  245. package/dist/src/core/furnace-css-fragments.d.ts +10 -0
  246. package/dist/src/core/furnace-css-fragments.js +26 -0
  247. package/dist/src/core/furnace-operation.d.ts +3 -3
  248. package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
  249. package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
  250. package/dist/src/core/furnace-registration-remove.d.ts +22 -1
  251. package/dist/src/core/furnace-registration-remove.js +53 -17
  252. package/dist/src/core/furnace-registration-rename.d.ts +26 -0
  253. package/dist/src/core/furnace-registration-rename.js +77 -0
  254. package/dist/src/core/furnace-registration.d.ts +14 -4
  255. package/dist/src/core/furnace-registration.js +44 -10
  256. package/dist/src/core/furnace-staleness.d.ts +7 -0
  257. package/dist/src/core/furnace-staleness.js +34 -24
  258. package/dist/src/core/furnace-validate-accessibility.d.ts +8 -4
  259. package/dist/src/core/furnace-validate-accessibility.js +71 -28
  260. package/dist/src/core/furnace-validate-exceptions.d.ts +48 -0
  261. package/dist/src/core/furnace-validate-exceptions.js +80 -0
  262. package/dist/src/core/furnace-validate-icon-button.d.ts +7 -0
  263. package/dist/src/core/furnace-validate-icon-button.js +78 -0
  264. package/dist/src/core/furnace-validate.js +1 -1
  265. package/dist/src/core/harness-port-fallback.d.ts +60 -0
  266. package/dist/src/core/harness-port-fallback.js +96 -0
  267. package/dist/src/core/history-log-file.d.ts +61 -0
  268. package/dist/src/core/history-log-file.js +164 -0
  269. package/dist/src/core/history-log.d.ts +2 -1
  270. package/dist/src/core/history-log.js +2 -1
  271. package/dist/src/core/json-preserving-edit.d.ts +22 -0
  272. package/dist/src/core/json-preserving-edit.js +199 -0
  273. package/dist/src/core/line-prefixer.d.ts +18 -0
  274. package/dist/src/core/line-prefixer.js +34 -0
  275. package/dist/src/core/mach.d.ts +7 -0
  276. package/dist/src/core/mach.js +32 -10
  277. package/dist/src/core/module-resolution-preflight.d.ts +19 -3
  278. package/dist/src/core/module-resolution-preflight.js +24 -6
  279. package/dist/src/core/negative-control-compiled.d.ts +27 -0
  280. package/dist/src/core/negative-control-compiled.js +73 -0
  281. package/dist/src/core/negative-control-diff.d.ts +51 -0
  282. package/dist/src/core/negative-control-diff.js +179 -0
  283. package/dist/src/core/negative-control.d.ts +150 -0
  284. package/dist/src/core/negative-control.js +399 -0
  285. package/dist/src/core/objdir-install-manifests.d.ts +73 -0
  286. package/dist/src/core/objdir-install-manifests.js +226 -0
  287. package/dist/src/core/park-store.d.ts +63 -0
  288. package/dist/src/core/park-store.js +216 -0
  289. package/dist/src/core/patch-export-update.d.ts +6 -0
  290. package/dist/src/core/patch-export-update.js +13 -3
  291. package/dist/src/core/patch-export.d.ts +3 -2
  292. package/dist/src/core/patch-export.js +22 -16
  293. package/dist/src/core/patch-files.js +26 -8
  294. package/dist/src/core/patch-lint-checkjs-any-import.d.ts +39 -0
  295. package/dist/src/core/patch-lint-checkjs-any-import.js +63 -0
  296. package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
  297. package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
  298. package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
  299. package/dist/src/core/patch-lint-checkjs.js +20 -13
  300. package/dist/src/core/patch-lint-code-lines.d.ts +59 -0
  301. package/dist/src/core/patch-lint-code-lines.js +105 -0
  302. package/dist/src/core/patch-lint-cross.d.ts +3 -1
  303. package/dist/src/core/patch-lint-cross.js +17 -4
  304. package/dist/src/core/patch-lint-css.d.ts +8 -2
  305. package/dist/src/core/patch-lint-css.js +24 -14
  306. package/dist/src/core/patch-lint-file-size.d.ts +12 -5
  307. package/dist/src/core/patch-lint-file-size.js +22 -11
  308. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +23 -0
  309. package/dist/src/core/patch-lint-forward-registration-extended.js +36 -11
  310. package/dist/src/core/patch-lint-forward-registration.js +10 -0
  311. package/dist/src/core/patch-lint-ignore.d.ts +196 -0
  312. package/dist/src/core/patch-lint-ignore.js +429 -0
  313. package/dist/src/core/patch-lint-ownership.d.ts +10 -0
  314. package/dist/src/core/patch-lint-ownership.js +26 -0
  315. package/dist/src/core/patch-lint-size-thresholds.d.ts +52 -0
  316. package/dist/src/core/patch-lint-size-thresholds.js +110 -0
  317. package/dist/src/core/patch-lint-size.d.ts +117 -0
  318. package/dist/src/core/patch-lint-size.js +331 -0
  319. package/dist/src/core/patch-lint-staged-registration.d.ts +18 -1
  320. package/dist/src/core/patch-lint-staged-registration.js +20 -0
  321. package/dist/src/core/patch-lint-token-scale.d.ts +57 -0
  322. package/dist/src/core/patch-lint-token-scale.js +286 -0
  323. package/dist/src/core/patch-lint.d.ts +11 -83
  324. package/dist/src/core/patch-lint.js +66 -288
  325. package/dist/src/core/patch-lock.js +7 -6
  326. package/dist/src/core/patch-manifest-consistency.js +7 -6
  327. package/dist/src/core/patch-manifest-io.d.ts +6 -3
  328. package/dist/src/core/patch-manifest-io.js +27 -9
  329. package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
  330. package/dist/src/core/patch-manifest-key-order.js +61 -0
  331. package/dist/src/core/patch-manifest-stage.d.ts +46 -0
  332. package/dist/src/core/patch-manifest-stage.js +96 -0
  333. package/dist/src/core/patch-manifest-validate.js +61 -3
  334. package/dist/src/core/patch-order-legal.d.ts +26 -0
  335. package/dist/src/core/patch-order-legal.js +88 -0
  336. package/dist/src/core/patch-policy-content.d.ts +40 -0
  337. package/dist/src/core/patch-policy-content.js +216 -0
  338. package/dist/src/core/patch-policy.d.ts +22 -5
  339. package/dist/src/core/patch-policy.js +26 -12
  340. package/dist/src/core/patch-retired.d.ts +57 -0
  341. package/dist/src/core/patch-retired.js +92 -0
  342. package/dist/src/core/patch-subset.d.ts +16 -0
  343. package/dist/src/core/patch-subset.js +66 -0
  344. package/dist/src/core/preflight-wait.d.ts +20 -0
  345. package/dist/src/core/preflight-wait.js +38 -0
  346. package/dist/src/core/rebase-from-version.d.ts +46 -0
  347. package/dist/src/core/rebase-from-version.js +71 -0
  348. package/dist/src/core/run-log.d.ts +26 -0
  349. package/dist/src/core/run-log.js +106 -4
  350. package/dist/src/core/run-profile.d.ts +7 -0
  351. package/dist/src/core/run-profile.js +7 -1
  352. package/dist/src/core/smoke-patterns.d.ts +5 -0
  353. package/dist/src/core/smoke-patterns.js +13 -0
  354. package/dist/src/core/test-harness-crash.d.ts +19 -1
  355. package/dist/src/core/test-harness-crash.js +8 -9
  356. package/dist/src/core/test-harness-signature.d.ts +33 -0
  357. package/dist/src/core/test-harness-signature.js +23 -1
  358. package/dist/src/core/test-raw-log.d.ts +66 -0
  359. package/dist/src/core/test-raw-log.js +122 -0
  360. package/dist/src/core/test-run-completeness.d.ts +90 -0
  361. package/dist/src/core/test-run-completeness.js +184 -0
  362. package/dist/src/core/test-stale-check.d.ts +27 -1
  363. package/dist/src/core/test-stale-check.js +39 -49
  364. package/dist/src/core/test-stale-exempt.d.ts +40 -0
  365. package/dist/src/core/test-stale-exempt.js +70 -0
  366. package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
  367. package/dist/src/core/test-xpcshell-retry.js +2 -1
  368. package/dist/src/core/token-lint.d.ts +53 -0
  369. package/dist/src/core/token-lint.js +131 -0
  370. package/dist/src/core/token-sibling-sheets.d.ts +46 -0
  371. package/dist/src/core/token-sibling-sheets.js +119 -0
  372. package/dist/src/core/tree-guard.js +5 -0
  373. package/dist/src/core/typecheck-shim.d.ts +6 -2
  374. package/dist/src/core/typecheck-shim.js +205 -4
  375. package/dist/src/core/typecheck.js +3 -1
  376. package/dist/src/index.d.ts +14 -3
  377. package/dist/src/index.js +6 -1
  378. package/dist/src/types/commands/index.d.ts +2 -2
  379. package/dist/src/types/commands/options.d.ts +212 -24
  380. package/dist/src/types/commands/patches.d.ts +118 -1
  381. package/dist/src/types/config.d.ts +264 -0
  382. package/dist/src/types/config.js +9 -0
  383. package/dist/src/types/furnace.d.ts +63 -1
  384. package/dist/src/types/furnace.js +7 -0
  385. package/dist/src/types/index.d.ts +1 -1
  386. package/dist/src/utils/options.d.ts +36 -7
  387. package/dist/src/utils/options.js +94 -8
  388. package/dist/src/utils/path-glob.d.ts +15 -0
  389. package/dist/src/utils/path-glob.js +47 -0
  390. package/dist/src/utils/regex.d.ts +15 -3
  391. package/dist/src/utils/regex.js +37 -16
  392. package/docs/configuration.md +369 -11
  393. package/docs/exit-codes.md +18 -1
  394. package/docs/furnace.md +139 -3
  395. package/docs/lifecycle-invariants.md +65 -30
  396. package/docs/machine-output.md +120 -12
  397. package/docs/patch-workflow.md +458 -22
  398. package/docs/run-logs.md +20 -1
  399. package/docs/testing.md +346 -21
  400. package/package.json +3 -3
@@ -17,7 +17,8 @@ import { topologicalSortCustom } from './furnace-graph-utils.js';
17
17
  import { resolveFurnaceMarkerComment } from './furnace-marker.js';
18
18
  import { recordFurnaceRollbackFailure } from './furnace-operation.js';
19
19
  import { assertFurnaceEngineDirReady } from './furnace-precondition.js';
20
- import { addJarMnEntries, removeCustomElementRegistration, removeJarMnEntries, } from './furnace-registration.js';
20
+ import { addJarMnEntries, removeCustomElementRegistration } from './furnace-registration.js';
21
+ import { removeJarMnFileEntries } from './furnace-registration-jar-inplace.js';
21
22
  import { createRollbackJournal, restoreRollbackJournalOrThrow, snapshotFile, } from './furnace-rollback.js';
22
23
  import { blockingStepErrors, hasBlockingStepErrors } from './furnace-step-errors.js';
23
24
  import { runPostApplyConsistencyChecks } from './furnace-validate-registration.js';
@@ -39,6 +40,16 @@ function addMissingComponentError(result, name, directoryPath) {
39
40
  error: `Component directory not found: ${directoryPath}`,
40
41
  });
41
42
  }
43
+ /**
44
+ * The skip both batch loops record when a component's source is unchanged
45
+ * since the last recorded apply and its engine copy still matches. `reason`
46
+ * keeps its long-standing text for API consumers; `kind` lets summaries
47
+ * name it.
48
+ */
49
+ const ALREADY_CURRENT_SKIP = {
50
+ reason: 'No changes since last apply',
51
+ kind: 'already-current',
52
+ };
42
53
  async function applyOverrideBatch(ctx) {
43
54
  const { config, furnacePaths, state, engineDir, ftlDir, dryRun } = ctx;
44
55
  const { result, allActions, newChecksums, rollbackJournal, componentName } = ctx;
@@ -66,7 +77,7 @@ async function applyOverrideBatch(ctx) {
66
77
  const cachedEngine = extractComponentChecksums(state.engineChecksums, 'override', name);
67
78
  const drifted = await hasOverrideEngineDrift({ engineDir, componentDir, ftlDir }, overrideConfig, cachedEngine);
68
79
  if (!drifted) {
69
- result.skipped.push({ name, reason: 'No changes since last apply' });
80
+ result.skipped.push({ name, ...ALREADY_CURRENT_SKIP });
70
81
  recordComponentChecksums(newChecksums, previous, 'override', name);
71
82
  continue;
72
83
  }
@@ -122,8 +133,8 @@ async function applyOverrideBatch(ctx) {
122
133
  /**
123
134
  * After undeploying deleted files, the in-engine jar.mn and
124
135
  * customElements.js still carry entries for the removed files. Re-sync them
125
- * by removing all of `name`'s jar.mn entries and re-adding only those that
126
- * still exist in the workspace. If the .mjs itself was deleted, also drop
136
+ * by removing only the deleted files' jar.mn lines in place and adding any
137
+ * live file that has none. If the .mjs itself was deleted, also drop
127
138
  * the customElements.js registration. Snapshots are taken under the same
128
139
  * journal so the undo path is symmetric with apply.
129
140
  */
@@ -134,18 +145,17 @@ async function reconcileCustomRegistrationAfterUndeploy(args) {
134
145
  const deletedRegistrationFiles = deletedFiles.filter((f) => f.endsWith('.mjs') || f.endsWith('.css'));
135
146
  if (deletedRegistrationFiles.length === 0)
136
147
  return;
137
- // jar.mn re-sync. addJarMnEntries is idempotent on duplicates but does not
138
- // drop stale entries, so we need a remove-then-add cycle. The journal
139
- // snapshot before each mutation gives us a clean rollback target.
148
+ // jar.mn re-sync: drop only the deleted files' lines, so the rest of the
149
+ // widget's block keeps its order and the owning patch carries no reorder
150
+ // hunks. addJarMnEntries is idempotent, so the add pass inserts only a
151
+ // live file that has no line yet. The journal snapshot before the
152
+ // mutation gives us a clean rollback target.
140
153
  if (rollbackJournal) {
141
154
  await snapshotFile(rollbackJournal, join(engineDir, JAR_MN));
142
155
  }
143
- await removeJarMnEntries(engineDir, name);
156
+ await removeJarMnFileEntries(engineDir, name, deletedRegistrationFiles);
144
157
  const liveJarFiles = Object.keys(currentChecksums).filter((f) => f.endsWith('.mjs') || f.endsWith('.css'));
145
158
  if (liveJarFiles.length > 0) {
146
- // applyCustomComponent has already added live entries, and the remove
147
- // above dropped them too, so re-add now to leave jar.mn in the correct
148
- // state.
149
159
  await addJarMnEntries(engineDir, name, liveJarFiles);
150
160
  }
151
161
  filesAffected.push(JAR_MN);
@@ -226,7 +236,7 @@ async function applyCustomBatch(ctx, root, markerComment) {
226
236
  // re-apply even though the workspace is unchanged.
227
237
  const drifted = await hasCustomEngineDrift({ root, name, componentDir, ftlDir }, customConfig);
228
238
  if (!drifted) {
229
- result.skipped.push({ name, reason: 'No changes since last apply' });
239
+ result.skipped.push({ name, ...ALREADY_CURRENT_SKIP });
230
240
  recordComponentChecksums(newChecksums, previous, 'custom', name);
231
241
  continue;
232
242
  }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The Furnace-composed form of a component source, and the questions that
3
+ * need it.
4
+ *
5
+ * `furnace apply` does not copy every source verbatim: a stylesheet with
6
+ * `@fireforge-include` directives is deployed with each fragment body from
7
+ * `components/shared/` inlined between the directive and its
8
+ * `@fireforge-end-include` marker. Anything that compares a deployed engine
9
+ * file with its source has to compare against that composed form, or a
10
+ * freshly deployed widget reads as a large difference it is not:
11
+ *
12
+ * - the unexported-drift belt (`build`/`test --refuse-unexported-drift`),
13
+ * which must not refuse to overwrite a deployed file with the very bytes
14
+ * it already holds;
15
+ * - `furnace diff`, which must show what the next apply would change.
16
+ */
17
+ import type { FurnaceConfig } from '../types/furnace.js';
18
+ /**
19
+ * Composes one component source into the bytes apply deploys: a stylesheet
20
+ * carrying include directives is fragment-expanded, anything else is
21
+ * returned as is.
22
+ *
23
+ * @param content - Workspace source content
24
+ * @param fileName - Source file name (only `.css` is ever expanded)
25
+ * @param sharedDir - Absolute path of `components/shared/`
26
+ * @returns The composed content
27
+ * @throws FurnaceError when an included fragment is missing or nested
28
+ */
29
+ export declare function composeComponentSource(content: string, fileName: string, sharedDir: string): Promise<string>;
30
+ /** The workspace source apply deploys to one engine path. */
31
+ export interface FurnaceDeployedSource {
32
+ type: 'custom' | 'override';
33
+ component: string;
34
+ /** Source file name inside the component directory. */
35
+ fileName: string;
36
+ /** Absolute path of the workspace source. */
37
+ sourcePath: string;
38
+ }
39
+ /**
40
+ * Maps every engine-relative path `furnace apply` writes to the workspace
41
+ * source it writes there: custom `.mjs`/`.css` into `targetPath`, a custom
42
+ * component's own `.ftl` into the Fluent tree (never for `sharedFtl`), and
43
+ * each override copy candidate onto its engine target.
44
+ *
45
+ * @param root - Project root
46
+ * @param config - Loaded furnace.json
47
+ * @returns Engine-relative path (POSIX) → deployed source
48
+ */
49
+ export declare function mapDeployedComponentSources(root: string, config: FurnaceConfig): Promise<Map<string, FurnaceDeployedSource>>;
50
+ /**
51
+ * Why a deployed engine file is safe to overwrite from its component
52
+ * source even though no patch body or baseline explains it:
53
+ *
54
+ * - `composed-source`: it already equals the Furnace-composed current
55
+ * source, so the overwrite writes the same bytes;
56
+ * - `fragment-refresh`: it differs from the composed source only inside
57
+ * Furnace-generated fragment expansions (a fragment changed since it was
58
+ * deployed), so the overwrite only refreshes generated text;
59
+ * - `previous-deploy`: it is exactly the content Furnace's last recorded
60
+ * apply deployed (the source has moved on since), so the overwrite loses
61
+ * nothing that was not already a component source.
62
+ */
63
+ export type DeployedSourceMatch = 'composed-source' | 'fragment-refresh' | 'previous-deploy';
64
+ /**
65
+ * For each engine-relative path, decides whether its current engine content
66
+ * is a Furnace deployment of its component source (see
67
+ * {@link DeployedSourceMatch}). Paths with no component source, an
68
+ * unreadable engine copy, or content explained by none of the matches are
69
+ * absent from the result.
70
+ *
71
+ * @param root - Project root
72
+ * @param engineDir - Absolute engine directory
73
+ * @param files - Engine-relative paths to classify
74
+ * @returns Engine-relative path → match, for the matching paths only
75
+ */
76
+ export declare function matchDeployedComponentFiles(root: string, engineDir: string, files: readonly string[]): Promise<Map<string, DeployedSourceMatch>>;
@@ -0,0 +1,167 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * The Furnace-composed form of a component source, and the questions that
4
+ * need it.
5
+ *
6
+ * `furnace apply` does not copy every source verbatim: a stylesheet with
7
+ * `@fireforge-include` directives is deployed with each fragment body from
8
+ * `components/shared/` inlined between the directive and its
9
+ * `@fireforge-end-include` marker. Anything that compares a deployed engine
10
+ * file with its source has to compare against that composed form, or a
11
+ * freshly deployed widget reads as a large difference it is not:
12
+ *
13
+ * - the unexported-drift belt (`build`/`test --refuse-unexported-drift`),
14
+ * which must not refuse to overwrite a deployed file with the very bytes
15
+ * it already holds;
16
+ * - `furnace diff`, which must show what the next apply would change.
17
+ */
18
+ import { readdir } from 'node:fs/promises';
19
+ import { join } from 'node:path';
20
+ import { pathExists, readText } from '../utils/fs.js';
21
+ import { sha256Hex } from '../utils/hash.js';
22
+ import { normalizePathSlashes } from '../utils/paths.js';
23
+ import { isOverrideCopyCandidate } from './furnace-apply-helpers.js';
24
+ import { extractComponentChecksums, normalizeForChecksum } from './furnace-checksum-utils.js';
25
+ import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, loadFurnaceState, } from './furnace-config.js';
26
+ import { resolveFtlDir } from './furnace-constants.js';
27
+ import { expandCssFragments, listFragmentIncludes, stripExpandedFragments, } from './furnace-css-fragments.js';
28
+ import { isRegularFile } from './furnace-dir-entry.js';
29
+ /**
30
+ * Composes one component source into the bytes apply deploys: a stylesheet
31
+ * carrying include directives is fragment-expanded, anything else is
32
+ * returned as is.
33
+ *
34
+ * @param content - Workspace source content
35
+ * @param fileName - Source file name (only `.css` is ever expanded)
36
+ * @param sharedDir - Absolute path of `components/shared/`
37
+ * @returns The composed content
38
+ * @throws FurnaceError when an included fragment is missing or nested
39
+ */
40
+ export async function composeComponentSource(content, fileName, sharedDir) {
41
+ if (!fileName.endsWith('.css') || listFragmentIncludes(content).length === 0)
42
+ return content;
43
+ return (await expandCssFragments(content, sharedDir)).expanded;
44
+ }
45
+ /**
46
+ * Maps every engine-relative path `furnace apply` writes to the workspace
47
+ * source it writes there: custom `.mjs`/`.css` into `targetPath`, a custom
48
+ * component's own `.ftl` into the Fluent tree (never for `sharedFtl`), and
49
+ * each override copy candidate onto its engine target.
50
+ *
51
+ * @param root - Project root
52
+ * @param config - Loaded furnace.json
53
+ * @returns Engine-relative path (POSIX) → deployed source
54
+ */
55
+ export async function mapDeployedComponentSources(root, config) {
56
+ const furnacePaths = getFurnacePaths(root);
57
+ const ftlDir = resolveFtlDir(config.ftlBasePath);
58
+ const sources = new Map();
59
+ for (const [component, custom] of Object.entries(config.custom)) {
60
+ const componentDir = join(furnacePaths.customDir, component);
61
+ for (const fileName of await listSourceFiles(componentDir)) {
62
+ let engineRel;
63
+ if (fileName.endsWith('.mjs') || fileName.endsWith('.css')) {
64
+ engineRel = normalizePathSlashes(join(custom.targetPath, fileName));
65
+ }
66
+ else if (fileName === `${component}.ftl` && custom.localized && !custom.sharedFtl) {
67
+ engineRel = normalizePathSlashes(join(ftlDir, fileName));
68
+ }
69
+ if (engineRel === undefined)
70
+ continue;
71
+ sources.set(engineRel, {
72
+ type: 'custom',
73
+ component,
74
+ fileName,
75
+ sourcePath: join(componentDir, fileName),
76
+ });
77
+ }
78
+ }
79
+ for (const [component, override] of Object.entries(config.overrides)) {
80
+ const componentDir = join(furnacePaths.overridesDir, component);
81
+ for (const fileName of await listSourceFiles(componentDir)) {
82
+ if (!isOverrideCopyCandidate(fileName, override.type))
83
+ continue;
84
+ // Same target rule as `getOverrideEngineTargetPath`, engine-relative.
85
+ const base = fileName.endsWith('.ftl') ? ftlDir : override.basePath;
86
+ sources.set(normalizePathSlashes(join(base, fileName)), {
87
+ type: 'override',
88
+ component,
89
+ fileName,
90
+ sourcePath: join(componentDir, fileName),
91
+ });
92
+ }
93
+ }
94
+ return sources;
95
+ }
96
+ async function listSourceFiles(componentDir) {
97
+ if (!(await pathExists(componentDir)))
98
+ return [];
99
+ const entries = await readdir(componentDir, { withFileTypes: true, encoding: 'utf8' });
100
+ return entries.filter((entry) => isRegularFile(entry)).map((entry) => entry.name);
101
+ }
102
+ /**
103
+ * Classifies one deployed engine file against its component source.
104
+ *
105
+ * @returns The match, or undefined when the engine copy is explained by
106
+ * none of them (a real, unrecorded engine edit)
107
+ */
108
+ async function matchDeployedSource(engineContent, source, sharedDir, appliedChecksums) {
109
+ const sourceContent = await readText(source.sourcePath);
110
+ const engine = normalizeForChecksum(engineContent);
111
+ let composed;
112
+ try {
113
+ composed = await composeComponentSource(sourceContent, source.fileName, sharedDir);
114
+ }
115
+ catch {
116
+ // A missing fragment: no composed form exists. The other two
117
+ // comparisons below still apply.
118
+ }
119
+ if (composed !== undefined && normalizeForChecksum(composed) === engine) {
120
+ return 'composed-source';
121
+ }
122
+ const strippedEngine = stripExpandedFragments(engine);
123
+ if (listFragmentIncludes(sourceContent).length > 0 &&
124
+ strippedEngine === stripExpandedFragments(normalizeForChecksum(sourceContent))) {
125
+ return 'fragment-refresh';
126
+ }
127
+ const recorded = extractComponentChecksums(appliedChecksums, source.type, source.component)[source.fileName];
128
+ // Apply records the checksum of the (unexpanded) source it deployed, so
129
+ // an engine copy with its expansions collapsed hashes to that value
130
+ // exactly when it is that deployment.
131
+ if (recorded !== undefined && sha256Hex(strippedEngine) === recorded) {
132
+ return 'previous-deploy';
133
+ }
134
+ return undefined;
135
+ }
136
+ /**
137
+ * For each engine-relative path, decides whether its current engine content
138
+ * is a Furnace deployment of its component source (see
139
+ * {@link DeployedSourceMatch}). Paths with no component source, an
140
+ * unreadable engine copy, or content explained by none of the matches are
141
+ * absent from the result.
142
+ *
143
+ * @param root - Project root
144
+ * @param engineDir - Absolute engine directory
145
+ * @param files - Engine-relative paths to classify
146
+ * @returns Engine-relative path → match, for the matching paths only
147
+ */
148
+ export async function matchDeployedComponentFiles(root, engineDir, files) {
149
+ const matches = new Map();
150
+ if (files.length === 0 || !(await furnaceConfigExists(root)))
151
+ return matches;
152
+ const sources = await mapDeployedComponentSources(root, await loadFurnaceConfig(root));
153
+ const { sharedDir } = getFurnacePaths(root);
154
+ const { appliedChecksums } = await loadFurnaceState(root);
155
+ for (const file of files) {
156
+ const source = sources.get(file);
157
+ if (source === undefined)
158
+ continue;
159
+ const enginePath = join(engineDir, file);
160
+ if (!(await pathExists(enginePath)))
161
+ continue;
162
+ const match = await matchDeployedSource(await readText(enginePath), source, sharedDir, appliedChecksums);
163
+ if (match !== undefined)
164
+ matches.set(file, match);
165
+ }
166
+ return matches;
167
+ }
@@ -0,0 +1,16 @@
1
+ import { type FurnaceOptInCheck, type FurnaceValidationException } from '../types/furnace.js';
2
+ /**
3
+ * Parses `validationExceptions[]`: `{ component, check, tag?, reason }`
4
+ * objects, no other keys, every field a non-empty string, and no two
5
+ * entries for the same component, check and tag.
6
+ */
7
+ export declare function parseValidationExceptions(raw: unknown): FurnaceValidationException[];
8
+ /**
9
+ * Parses `tokenSiblingSheets`: an object from a sheet glob to a non-empty
10
+ * list of partner paths or globs.
11
+ */
12
+ export declare function parseTokenSiblingSheets(raw: unknown): Record<string, string[]>;
13
+ /** Parses `minComponents`: a positive integer. */
14
+ export declare function parseMinComponents(raw: unknown): number;
15
+ /** Parses `optInChecks[]`: names from {@link FURNACE_OPT_IN_CHECKS}. */
16
+ export declare function parseOptInChecks(raw: unknown): FurnaceOptInCheck[];
@@ -0,0 +1,102 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Validators for the `furnace validate` and `token lint` policy fields of
4
+ * furnace.json: `validationExceptions[]`, `optInChecks[]`, `minComponents`
5
+ * and `tokenSiblingSheets`.
6
+ *
7
+ * Strict on purpose, like `buildAudit.unpackaged`: every exception is a
8
+ * standing suppression of a finding, so a malformed one must fail at load
9
+ * rather than quietly suppress nothing (or everything).
10
+ */
11
+ import { FurnaceError } from '../errors/furnace.js';
12
+ import { FURNACE_OPT_IN_CHECKS, } from '../types/furnace.js';
13
+ import { isArray, isObject, isString } from '../utils/validation.js';
14
+ const EXCEPTION_KEYS = new Set(['component', 'check', 'tag', 'reason']);
15
+ function requireNonEmptyString(entry, key, field) {
16
+ const value = entry[key];
17
+ if (!isString(value) || value.trim().length === 0) {
18
+ throw new FurnaceError(`Furnace config: "${field}.${key}" must be a non-empty string`);
19
+ }
20
+ return value;
21
+ }
22
+ /**
23
+ * Parses `validationExceptions[]`: `{ component, check, tag?, reason }`
24
+ * objects, no other keys, every field a non-empty string, and no two
25
+ * entries for the same component, check and tag.
26
+ */
27
+ export function parseValidationExceptions(raw) {
28
+ if (!isArray(raw)) {
29
+ throw new FurnaceError('Furnace config: "validationExceptions" must be an array');
30
+ }
31
+ const seen = new Set();
32
+ return raw.map((entry, index) => {
33
+ const field = `validationExceptions[${String(index)}]`;
34
+ if (!isObject(entry)) {
35
+ throw new FurnaceError(`Furnace config: "${field}" must be an object with "component", "check" and "reason"`);
36
+ }
37
+ for (const key of Object.keys(entry)) {
38
+ if (!EXCEPTION_KEYS.has(key)) {
39
+ throw new FurnaceError(`Furnace config: "${field}" has unknown key "${key}"`);
40
+ }
41
+ }
42
+ const exception = {
43
+ component: requireNonEmptyString(entry, 'component', field),
44
+ check: requireNonEmptyString(entry, 'check', field),
45
+ reason: requireNonEmptyString(entry, 'reason', field),
46
+ };
47
+ if (entry['tag'] !== undefined)
48
+ exception.tag = requireNonEmptyString(entry, 'tag', field);
49
+ const identity = `${exception.component}\u0000${exception.check}\u0000${exception.tag ?? ''}`;
50
+ if (seen.has(identity)) {
51
+ const on = exception.tag !== undefined ? ` on <${exception.tag}>` : '';
52
+ throw new FurnaceError(`Furnace config: "${field}" repeats the exception for ${exception.component} [${exception.check}]${on}`);
53
+ }
54
+ seen.add(identity);
55
+ return exception;
56
+ });
57
+ }
58
+ function isOptInCheck(value) {
59
+ return FURNACE_OPT_IN_CHECKS.includes(value);
60
+ }
61
+ /**
62
+ * Parses `tokenSiblingSheets`: an object from a sheet glob to a non-empty
63
+ * list of partner paths or globs.
64
+ */
65
+ export function parseTokenSiblingSheets(raw) {
66
+ if (!isObject(raw)) {
67
+ throw new FurnaceError('Furnace config: "tokenSiblingSheets" must be an object');
68
+ }
69
+ const out = {};
70
+ for (const [pattern, partners] of Object.entries(raw)) {
71
+ const valid = isArray(partners) &&
72
+ partners.length > 0 &&
73
+ partners.every((partner) => isString(partner) && partner.trim() !== '');
74
+ if (pattern.includes('..') || !valid || partners.some((p) => String(p).includes('..'))) {
75
+ throw new FurnaceError(`Furnace config: "tokenSiblingSheets.${pattern}" must be a non-empty list of engine-relative paths or globs (no "..")`);
76
+ }
77
+ out[pattern] = partners;
78
+ }
79
+ return out;
80
+ }
81
+ /** Parses `minComponents`: a positive integer. */
82
+ export function parseMinComponents(raw) {
83
+ if (typeof raw !== 'number' || !Number.isInteger(raw) || raw < 1) {
84
+ throw new FurnaceError('Furnace config: "minComponents" must be a positive integer');
85
+ }
86
+ return raw;
87
+ }
88
+ /** Parses `optInChecks[]`: names from {@link FURNACE_OPT_IN_CHECKS}. */
89
+ export function parseOptInChecks(raw) {
90
+ if (!isArray(raw)) {
91
+ throw new FurnaceError('Furnace config: "optInChecks" must be an array');
92
+ }
93
+ const checks = [];
94
+ for (const value of raw) {
95
+ if (!isString(value) || !isOptInCheck(value)) {
96
+ throw new FurnaceError(`Furnace config: "optInChecks" entry ${JSON.stringify(value)} is not an opt-in check. Valid: ${FURNACE_OPT_IN_CHECKS.join(', ')}`);
97
+ }
98
+ if (!checks.includes(value))
99
+ checks.push(value);
100
+ }
101
+ return checks;
102
+ }
@@ -8,6 +8,7 @@
8
8
  import { FurnaceError } from '../errors/furnace.js';
9
9
  import { isJsonObject, isString } from '../utils/validation.js';
10
10
  import { parseStringArray } from './furnace-config-array-utils.js';
11
+ import { parseMinComponents, parseOptInChecks, parseTokenSiblingSheets, parseValidationExceptions, } from './furnace-config-validate-exceptions.js';
11
12
  /**
12
13
  * Validates an override component config object.
13
14
  * @param data - Raw data to validate
@@ -93,6 +94,9 @@ export function applyOptionalFurnaceFields(migrated, config) {
93
94
  if (migrated['platformPrefixes'] !== undefined) {
94
95
  config.platformPrefixes = parseStringArray(migrated['platformPrefixes'], 'platformPrefixes');
95
96
  }
97
+ if (migrated['tokenSiblingSheets'] !== undefined) {
98
+ config.tokenSiblingSheets = parseTokenSiblingSheets(migrated['tokenSiblingSheets']);
99
+ }
96
100
  if (migrated['runtimeVariables'] !== undefined) {
97
101
  config.runtimeVariables = parseStringArray(migrated['runtimeVariables'], 'runtimeVariables');
98
102
  }
@@ -129,4 +133,14 @@ export function applyOptionalFurnaceFields(migrated, config) {
129
133
  }
130
134
  config.scanPaths = paths;
131
135
  }
136
+ // `furnace validate` policy: reviewed exceptions and opt-in checks.
137
+ if (migrated['validationExceptions'] !== undefined) {
138
+ config.validationExceptions = parseValidationExceptions(migrated['validationExceptions']);
139
+ }
140
+ if (migrated['optInChecks'] !== undefined) {
141
+ config.optInChecks = parseOptInChecks(migrated['optInChecks']);
142
+ }
143
+ if (migrated['minComponents'] !== undefined) {
144
+ config.minComponents = parseMinComponents(migrated['minComponents']);
145
+ }
132
146
  }
@@ -35,6 +35,16 @@ export declare function listFragmentIncludes(css: string): string[];
35
35
  * against its workspace source and to re-expand idempotently.
36
36
  */
37
37
  export declare function stripExpandedFragments(css: string): string;
38
+ /**
39
+ * Marks the lines of a stylesheet that belong to a fenced fragment
40
+ * expansion: the directive, the inlined body and the end marker. Index `i`
41
+ * is line `i + 1` of `css`. Fences are found exactly as
42
+ * {@link stripExpandedFragments} finds them, so a bare directive or an
43
+ * unterminated fence marks nothing. The patch-size metric reads this
44
+ * off a file's post-image, because a hunk can start inside a span and
45
+ * carry no directive of its own.
46
+ */
47
+ export declare function expandedFragmentLineMask(css: string): boolean[];
38
48
  /**
39
49
  * Expands every `@fireforge-include` directive in `css` with the current
40
50
  * content of its fragment file from `sharedDir`, fencing each expansion
@@ -78,6 +78,32 @@ export function stripExpandedFragments(css) {
78
78
  }
79
79
  return out.join('\n');
80
80
  }
81
+ /**
82
+ * Marks the lines of a stylesheet that belong to a fenced fragment
83
+ * expansion: the directive, the inlined body and the end marker. Index `i`
84
+ * is line `i + 1` of `css`. Fences are found exactly as
85
+ * {@link stripExpandedFragments} finds them, so a bare directive or an
86
+ * unterminated fence marks nothing. The patch-size metric reads this
87
+ * off a file's post-image, because a hunk can start inside a span and
88
+ * carry no directive of its own.
89
+ */
90
+ export function expandedFragmentLineMask(css) {
91
+ const lines = css.split('\n');
92
+ const mask = lines.map(() => false);
93
+ for (let i = 0; i < lines.length; i++) {
94
+ const inc = INCLUDE_PATTERN.exec(lines[i] ?? '');
95
+ if (!inc?.[1])
96
+ continue;
97
+ for (let j = i + 1; j < lines.length; j++) {
98
+ if (END_INCLUDE_PATTERN.exec(lines[j] ?? '')?.[1] !== inc[1])
99
+ continue;
100
+ mask.fill(true, i, j + 1);
101
+ i = j;
102
+ break;
103
+ }
104
+ }
105
+ return mask;
106
+ }
81
107
  /**
82
108
  * Expands every `@fireforge-include` directive in `css` with the current
83
109
  * content of its fragment file from `sharedDir`, fencing each expansion
@@ -63,9 +63,9 @@ export interface RunFurnaceMutationOptions {
63
63
  * never passed: `furnace deploy --wait-lock 1800` waited 1800 s for the
64
64
  * engine session lock and then met `.fireforge/furnace.lock` with a fixed
65
65
  * 30 s, which is the worst shape a budget can have: it pays the whole
66
- * wait and still refuses. When absent, {@link sessionWaitLockMs} supplies
67
- * the session-wide budget so the twelve furnace mutators that declare no
68
- * flag are covered too.
66
+ * wait and still refuses. Every furnace leaf that takes this lock threads
67
+ * its `--wait-lock` here. When absent, {@link sessionWaitLockMs} supplies
68
+ * the session-wide budget (`FIREFORGE_WAIT_LOCK`).
69
69
  */
70
70
  lockTimeoutMs?: number;
71
71
  /**
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Removes the lines registering `files` for `tagName`, leaving every other
3
+ * line of the block where it is. Returns how many lines were removed.
4
+ */
5
+ export declare function removeJarMnFileEntries(engineDir: string, tagName: string, files: readonly string[]): Promise<number>;
6
+ /**
7
+ * Rewrites `oldTag`'s lines in place as `newTag`'s. A line whose renamed
8
+ * file is in `liveFiles` keeps its position and layout; one whose file is
9
+ * gone is dropped. Returns the renamed files that already had a line; the
10
+ * caller's `addJarMnEntries` pass then inserts only the rest.
11
+ */
12
+ export declare function renameJarMnEntriesInPlace(engineDir: string, oldTag: string, newTag: string, liveFiles: readonly string[], renameFile: (file: string) => string): Promise<string[]>;
@@ -0,0 +1,80 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * In-place edits to a widget's `toolkit/content/jar.mn` block.
4
+ *
5
+ * Undeploying one file and renaming a component used to drop every line of
6
+ * the widget and re-add the live ones, which re-sorted the whole block and
7
+ * left the owning patch carrying reorder hunks. These helpers touch only
8
+ * the lines that change: a deleted file's line goes, a renamed component's
9
+ * lines are rewritten where they stand, and anything still missing is left
10
+ * for `addJarMnEntries` to insert.
11
+ */
12
+ import { join } from 'node:path';
13
+ import { pathExists, readText, writeText } from '../utils/fs.js';
14
+ import { escapeRegex } from '../utils/regex.js';
15
+ import { JAR_MN } from './furnace-constants.js';
16
+ /**
17
+ * The file a widget jar.mn line registers: the `(widgets/<tag>/<file>)`
18
+ * source mapping, or the legacy `content/global/elements/<tag>.<ext>`
19
+ * target written before the mapping existed. Undefined when the line is
20
+ * not one of this tag's.
21
+ */
22
+ function registeredFile(line, tagName) {
23
+ const tag = escapeRegex(tagName);
24
+ const source = new RegExp(`\\(widgets/${tag}/([^)]+)\\)`).exec(line);
25
+ if (source)
26
+ return source[1];
27
+ const legacy = new RegExp(`content/global/elements/(${tag}\\.[^\\s]+)`).exec(line);
28
+ return legacy?.[1];
29
+ }
30
+ /**
31
+ * Removes the lines registering `files` for `tagName`, leaving every other
32
+ * line of the block where it is. Returns how many lines were removed.
33
+ */
34
+ export async function removeJarMnFileEntries(engineDir, tagName, files) {
35
+ const filePath = join(engineDir, JAR_MN);
36
+ if (files.length === 0 || !(await pathExists(filePath)))
37
+ return 0;
38
+ const doomed = new Set(files);
39
+ const lines = (await readText(filePath)).split('\n');
40
+ const kept = lines.filter((line) => {
41
+ const file = registeredFile(line, tagName);
42
+ return file === undefined || !doomed.has(file);
43
+ });
44
+ const removed = lines.length - kept.length;
45
+ if (removed > 0)
46
+ await writeText(filePath, kept.join('\n'));
47
+ return removed;
48
+ }
49
+ /**
50
+ * Rewrites `oldTag`'s lines in place as `newTag`'s. A line whose renamed
51
+ * file is in `liveFiles` keeps its position and layout; one whose file is
52
+ * gone is dropped. Returns the renamed files that already had a line; the
53
+ * caller's `addJarMnEntries` pass then inserts only the rest.
54
+ */
55
+ export async function renameJarMnEntriesInPlace(engineDir, oldTag, newTag, liveFiles, renameFile) {
56
+ const filePath = join(engineDir, JAR_MN);
57
+ if (!(await pathExists(filePath)))
58
+ return [];
59
+ const live = new Set(liveFiles);
60
+ const covered = [];
61
+ const out = [];
62
+ for (const line of (await readText(filePath)).split('\n')) {
63
+ const file = registeredFile(line, oldTag);
64
+ if (file === undefined) {
65
+ out.push(line);
66
+ continue;
67
+ }
68
+ const renamed = renameFile(file);
69
+ if (!live.has(renamed) || covered.includes(renamed))
70
+ continue;
71
+ // Textual substitution keeps the indent, column spacing and any
72
+ // trailing comment the line carried.
73
+ out.push(line
74
+ .replace(`content/global/elements/${file}`, `content/global/elements/${renamed}`)
75
+ .replace(`(widgets/${oldTag}/${file})`, `(widgets/${newTag}/${renamed})`));
76
+ covered.push(renamed);
77
+ }
78
+ await writeText(filePath, out.join('\n'));
79
+ return covered;
80
+ }