@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
@@ -0,0 +1,61 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * The one key order every `patches.json` writer uses for a patch row and
4
+ * for the manifest itself. Writers used to disagree: `export` spread
5
+ * `sourceProduct` before `sourceVersion` while a full-manifest save put it
6
+ * after `filesAffected`, so a no-op rewrite left a key-order-only hunk in
7
+ * the manifest.
8
+ */
9
+ /** Patch-row keys in the order of the `PatchMetadata` interface. */
10
+ const PATCH_ROW_KEY_ORDER = [
11
+ 'filename',
12
+ 'order',
13
+ 'category',
14
+ 'name',
15
+ 'description',
16
+ 'createdAt',
17
+ 'sourceEsrVersion',
18
+ 'sourceProduct',
19
+ 'sourceVersion',
20
+ 'filesAffected',
21
+ 'lintIgnore',
22
+ 'tier',
23
+ 'stagedDependencies',
24
+ ];
25
+ /** Manifest keys in the order of the `PatchesManifest` interface. */
26
+ const MANIFEST_KEY_ORDER = ['version', 'patches', 'retired', 'reserved'];
27
+ function reorder(value, order) {
28
+ const source = value;
29
+ const out = {};
30
+ for (const key of order) {
31
+ if (Object.hasOwn(source, key))
32
+ out[key] = source[key];
33
+ }
34
+ // Keys this build does not know keep their relative order after the
35
+ // known ones, so a newer writer's fields survive an older rewrite.
36
+ for (const key of Object.keys(source)) {
37
+ if (!Object.hasOwn(out, key))
38
+ out[key] = source[key];
39
+ }
40
+ return out;
41
+ }
42
+ /**
43
+ * Returns a copy of a patch row with its keys in canonical order.
44
+ *
45
+ * @param row - A `PatchMetadata` row, typed or raw
46
+ * @returns The same fields in canonical key order
47
+ */
48
+ export function canonicalizePatchRow(row) {
49
+ return reorder(row, PATCH_ROW_KEY_ORDER);
50
+ }
51
+ /**
52
+ * Returns a copy of a manifest with its top-level keys and every row in
53
+ * canonical order.
54
+ *
55
+ * @param manifest - A `PatchesManifest`, typed or raw
56
+ * @returns The manifest in canonical key order
57
+ */
58
+ export function canonicalizeManifest(manifest) {
59
+ const ordered = reorder(manifest, MANIFEST_KEY_ORDER);
60
+ return { ...ordered, patches: manifest.patches.map((row) => canonicalizePatchRow(row)) };
61
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The manifest `patch stage` writes into the git index: HEAD's
3
+ * `patches.json` with only the named patches' rows (and their retired and
4
+ * reserved slots) taken from the working copy.
5
+ *
6
+ * Sessions sharing one checkout each touch their own rows of the shared
7
+ * manifest. Staging the whole file commits every session's rows under one
8
+ * message; staging hunks with `git apply --cached --unidiff-zero` lands a
9
+ * kept insertion at its new-side line number once a dropped hunk above it
10
+ * shifted the file, and committed invalid JSON. Merging rows from the
11
+ * parsed manifests has neither failure: the result is built from structure,
12
+ * then validated before anything is staged.
13
+ */
14
+ import type { PatchesManifest } from '../types/commands/index.js';
15
+ /** What staging does to one named patch's row. */
16
+ export type StagedRowChange = 'added' | 'updated' | 'removed' | 'unchanged';
17
+ /** The staged manifest and the per-patch row changes it carries. */
18
+ export interface StagedManifest {
19
+ manifest: PatchesManifest;
20
+ changes: Array<{
21
+ filename: string;
22
+ change: StagedRowChange;
23
+ }>;
24
+ }
25
+ /**
26
+ * Merges the named rows of `working` into `head`.
27
+ *
28
+ * Rows: every unnamed row comes from HEAD; a named row comes from the
29
+ * working copy, or leaves when the working copy no longer has it.
30
+ * Retired slots: a slot whose `filename` is named follows the working
31
+ * copy, the rest follow HEAD. Reserved slots follow HEAD. A slot whose
32
+ * order a staged row now fills is dropped, as every manifest write does.
33
+ *
34
+ * @param head - HEAD's manifest, or null when HEAD has none
35
+ * @param working - The working-copy manifest
36
+ * @param filenames - Patch filenames whose rows to take from the working copy
37
+ * @returns The validated manifest and the row changes
38
+ * @throws Error when two rows would hold one order, or the merge does not
39
+ * validate
40
+ */
41
+ export declare function mergeStagedManifest(head: PatchesManifest | null, working: PatchesManifest, filenames: ReadonlySet<string>): StagedManifest;
42
+ /**
43
+ * Serializes a staged manifest exactly as the manifest writers do, so the
44
+ * staged blob and a later full save of the same content are byte-identical.
45
+ */
46
+ export declare function serializeStagedManifest(manifest: PatchesManifest): string;
@@ -0,0 +1,96 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * The manifest `patch stage` writes into the git index: HEAD's
4
+ * `patches.json` with only the named patches' rows (and their retired and
5
+ * reserved slots) taken from the working copy.
6
+ *
7
+ * Sessions sharing one checkout each touch their own rows of the shared
8
+ * manifest. Staging the whole file commits every session's rows under one
9
+ * message; staging hunks with `git apply --cached --unidiff-zero` lands a
10
+ * kept insertion at its new-side line number once a dropped hunk above it
11
+ * shifted the file, and committed invalid JSON. Merging rows from the
12
+ * parsed manifests has neither failure: the result is built from structure,
13
+ * then validated before anything is staged.
14
+ */
15
+ import { toError } from '../utils/errors.js';
16
+ import { canonicalizeManifest } from './patch-manifest-key-order.js';
17
+ import { validatePatchesManifest } from './patch-manifest-validate.js';
18
+ import { carryReservedSlots, carryRetiredSlots } from './patch-retired.js';
19
+ /**
20
+ * Merges the named rows of `working` into `head`.
21
+ *
22
+ * Rows: every unnamed row comes from HEAD; a named row comes from the
23
+ * working copy, or leaves when the working copy no longer has it.
24
+ * Retired slots: a slot whose `filename` is named follows the working
25
+ * copy, the rest follow HEAD. Reserved slots follow HEAD. A slot whose
26
+ * order a staged row now fills is dropped, as every manifest write does.
27
+ *
28
+ * @param head - HEAD's manifest, or null when HEAD has none
29
+ * @param working - The working-copy manifest
30
+ * @param filenames - Patch filenames whose rows to take from the working copy
31
+ * @returns The validated manifest and the row changes
32
+ * @throws Error when two rows would hold one order, or the merge does not
33
+ * validate
34
+ */
35
+ export function mergeStagedManifest(head, working, filenames) {
36
+ const headRows = head?.patches ?? [];
37
+ const changes = [...filenames].sort().map((filename) => {
38
+ const before = headRows.find((row) => row.filename === filename);
39
+ const after = working.patches.find((row) => row.filename === filename);
40
+ const change = before === undefined
41
+ ? 'added'
42
+ : after === undefined
43
+ ? 'removed'
44
+ : JSON.stringify(canonicalRow(before)) === JSON.stringify(canonicalRow(after))
45
+ ? 'unchanged'
46
+ : 'updated';
47
+ return { filename, change };
48
+ });
49
+ const rows = [
50
+ ...headRows.filter((row) => !filenames.has(row.filename)),
51
+ ...working.patches.filter((row) => filenames.has(row.filename)),
52
+ ].sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
53
+ assertNoCollision(rows);
54
+ const retiredSource = [
55
+ ...(head?.retired ?? []).filter((slot) => !filenames.has(slot.filename)),
56
+ ...(working.retired ?? []).filter((slot) => filenames.has(slot.filename)),
57
+ ];
58
+ const retired = carryRetiredSlots({ retired: retiredSource }, rows);
59
+ const reserved = carryReservedSlots(head, rows);
60
+ const merged = {
61
+ version: 1,
62
+ patches: rows,
63
+ ...(retired !== undefined ? { retired } : {}),
64
+ ...(reserved !== undefined ? { reserved } : {}),
65
+ };
66
+ try {
67
+ validatePatchesManifest(JSON.parse(JSON.stringify(merged)));
68
+ }
69
+ catch (error) {
70
+ throw new Error(`the staged manifest would not validate: ${toError(error).message}`, {
71
+ cause: error,
72
+ });
73
+ }
74
+ return { manifest: canonicalizeManifest(merged), changes };
75
+ }
76
+ function canonicalRow(row) {
77
+ return canonicalizeManifest({ patches: [row] }).patches[0];
78
+ }
79
+ function assertNoCollision(rows) {
80
+ const seen = new Map();
81
+ for (const row of rows) {
82
+ const holder = seen.get(row.order);
83
+ if (holder !== undefined) {
84
+ throw new Error(`order ${String(row.order)} would be held by both ${holder} and ${row.filename}; ` +
85
+ 'stage the patch that moved off that order as well');
86
+ }
87
+ seen.set(row.order, row.filename);
88
+ }
89
+ }
90
+ /**
91
+ * Serializes a staged manifest exactly as the manifest writers do, so the
92
+ * staged blob and a later full save of the same content are byte-identical.
93
+ */
94
+ export function serializeStagedManifest(manifest) {
95
+ return `${JSON.stringify(manifest, null, 2)}\n`;
96
+ }
@@ -4,6 +4,8 @@
4
4
  */
5
5
  import { parseObject } from '../utils/parse.js';
6
6
  import { isArray, isObject, isValidFirefoxProduct, isValidFirefoxVersion, PATCH_CATEGORIES, } from '../utils/validation.js';
7
+ import { parseLintIgnoreEntries } from './patch-lint-ignore.js';
8
+ import { canonicalizePatchRow } from './patch-manifest-key-order.js';
7
9
  import { checkDocumentVersion, describeNewerDocument } from './state-file.js';
8
10
  function parseForwardImports(data, label) {
9
11
  if (!isArray(data)) {
@@ -99,7 +101,11 @@ function validatePatchMetadata(data, index) {
99
101
  // `lintIgnore` to suppress a rule is quietly re-tripped the next time the
100
102
  // manifest validates. Preserve both `lintIgnore` and `tier` here. Future
101
103
  // optional fields follow the same template.
102
- const lintIgnore = rec.optionalStringArray('lintIgnore');
104
+ // Entries are bare check IDs or `{ rule, max, reason }` ceilings.
105
+ const rawLintIgnore = rec.raw('lintIgnore');
106
+ const lintIgnore = rawLintIgnore === undefined
107
+ ? undefined
108
+ : parseLintIgnoreEntries(rawLintIgnore, `patches[${index}].lintIgnore`);
103
109
  const rawTier = rec.raw('tier');
104
110
  let tier;
105
111
  if (rawTier !== undefined) {
@@ -129,7 +135,54 @@ function validatePatchMetadata(data, index) {
129
135
  if (rawStagedDependencies !== undefined) {
130
136
  result.stagedDependencies = parseStagedDependencies(rawStagedDependencies, `patches[${index}].stagedDependencies`);
131
137
  }
132
- return result;
138
+ return canonicalizePatchRow(result);
139
+ }
140
+ /**
141
+ * Validates the `retired` placeholders `patch retire` leaves behind. Kept on
142
+ * the manifest so every write that round-trips a loaded manifest carries
143
+ * them; dropping them would silently reopen the gaps they were retired to
144
+ * close.
145
+ */
146
+ function parseRetiredSlots(data) {
147
+ if (!isArray(data)) {
148
+ throw new Error('patches.json field "retired" must be an array');
149
+ }
150
+ return data.map((entry, index) => {
151
+ const rec = parseObject(entry, `retired[${index}]`);
152
+ const slot = {
153
+ order: rec.nonNegativeInteger('order'),
154
+ category: rec.validatedString('category', (value) => /^[a-z][a-z0-9-]*$/.test(value), 'a lowercase category identifier (letters, numbers, hyphens)'),
155
+ filename: rec.string('filename'),
156
+ retiredAt: rec.string('retiredAt'),
157
+ };
158
+ const reason = rec.optionalString('reason');
159
+ if (reason !== undefined)
160
+ slot.reason = reason;
161
+ return slot;
162
+ });
163
+ }
164
+ /**
165
+ * Validates the `reserved` placeholders `patch reserve` writes. Same
166
+ * survival rule as {@link parseRetiredSlots}: dropping one on a round trip
167
+ * would reopen the gap a peer reserved the order to close.
168
+ */
169
+ function parseReservedSlots(data) {
170
+ if (!isArray(data)) {
171
+ throw new Error('patches.json field "reserved" must be an array');
172
+ }
173
+ return data.map((entry, index) => {
174
+ const rec = parseObject(entry, `reserved[${index}]`);
175
+ const slot = {
176
+ order: rec.nonNegativeInteger('order'),
177
+ category: rec.validatedString('category', (value) => /^[a-z][a-z0-9-]*$/.test(value), 'a lowercase category identifier (letters, numbers, hyphens)'),
178
+ owner: rec.validatedString('owner', (value) => value.trim() !== '', 'a non-empty label'),
179
+ reservedAt: rec.string('reservedAt'),
180
+ };
181
+ const reason = rec.optionalString('reason');
182
+ if (reason !== undefined)
183
+ slot.reason = reason;
184
+ return slot;
185
+ });
133
186
  }
134
187
  /** Manifest schema version this build writes and understands. */
135
188
  const PATCHES_MANIFEST_VERSION = 1;
@@ -156,10 +209,15 @@ export function validatePatchesManifest(data) {
156
209
  if (!isArray(data['patches'])) {
157
210
  throw new Error('patches.json field "patches" must be an array');
158
211
  }
159
- return {
212
+ const manifest = {
160
213
  version: 1,
161
214
  patches: data['patches'].map((patch, index) => validatePatchMetadata(patch, index)),
162
215
  };
216
+ if (data['retired'] !== undefined)
217
+ manifest.retired = parseRetiredSlots(data['retired']);
218
+ if (data['reserved'] !== undefined)
219
+ manifest.reserved = parseReservedSlots(data['reserved']);
220
+ return manifest;
163
221
  }
164
222
  /**
165
223
  * Infers patch metadata from a filename pattern.
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The earliest order a new patch can take after a given order, under the
3
+ * queue's occupancy and `patchPolicy`.
4
+ *
5
+ * A refusal that says "export it as its own later patch" leaves the
6
+ * operator to find a number that is free, inside the category's range, and
7
+ * (with `allowGaps: false`) does not open a gap. Each wrong guess costs a
8
+ * full export attempt. This answers it once, with the same policy
9
+ * evaluation the export itself will run.
10
+ */
11
+ import type { PatchesManifest } from '../types/commands/index.js';
12
+ import type { FireForgeConfig } from '../types/config.js';
13
+ /**
14
+ * Smallest order strictly greater than `after` that no patch occupies and
15
+ * that adds no order-related `patchPolicy` finding (collision, category
16
+ * range, reserved range, numeric gap) for a `category` patch. A retired
17
+ * slot counts as free, since a patch may fill it; a reservation does not,
18
+ * since it is held for its owner's export. Returns null when no such
19
+ * order exists inside the category's configured ranges.
20
+ *
21
+ * @param config - Project config (only `patchPolicy` is read)
22
+ * @param manifest - Current manifest
23
+ * @param category - Category the new patch would carry
24
+ * @param after - The order the new patch must come after
25
+ */
26
+ export declare function earliestLegalOrderAfter(config: Pick<FireForgeConfig, 'patchPolicy'>, manifest: PatchesManifest, category: string, after: number): number | null;
@@ -0,0 +1,88 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * The earliest order a new patch can take after a given order, under the
4
+ * queue's occupancy and `patchPolicy`.
5
+ *
6
+ * A refusal that says "export it as its own later patch" leaves the
7
+ * operator to find a number that is free, inside the category's range, and
8
+ * (with `allowGaps: false`) does not open a gap. Each wrong guess costs a
9
+ * full export attempt. This answers it once, with the same policy
10
+ * evaluation the export itself will run.
11
+ */
12
+ import { formatPatchOrder } from './patch-parse.js';
13
+ import { evaluatePatchPolicy } from './patch-policy.js';
14
+ import { reservedOrderSet } from './patch-retired.js';
15
+ /** Policy codes an order choice alone can trip. */
16
+ const ORDER_CODES = new Set([
17
+ 'order-collision',
18
+ 'category-range',
19
+ 'reserved-range',
20
+ 'numeric-gap',
21
+ ]);
22
+ /** Upper bound on the search when no policy range bounds it. */
23
+ const UNBOUNDED_SEARCH = 1000;
24
+ const GAP_ORDERS_PATTERN = /numeric gap\(s\): (?<orders>[^.]+)\./;
25
+ /**
26
+ * Order-related findings as comparable keys. A `numeric-gap` finding
27
+ * becomes one key per missing order, so filling one gap of several reads as
28
+ * removing a key rather than as a new, differently worded finding.
29
+ */
30
+ function orderIssueKeys(config, manifest) {
31
+ const keys = new Set();
32
+ for (const issue of evaluatePatchPolicy(config, manifest)) {
33
+ if (!ORDER_CODES.has(issue.code))
34
+ continue;
35
+ const gaps = issue.code === 'numeric-gap' ? GAP_ORDERS_PATTERN.exec(issue.message) : null;
36
+ if (gaps?.groups?.['orders'] === undefined) {
37
+ keys.add(`${issue.code}|${issue.message}`);
38
+ continue;
39
+ }
40
+ for (const order of gaps.groups['orders'].split(',')) {
41
+ keys.add(`numeric-gap|${issue.filename}|${order.trim()}`);
42
+ }
43
+ }
44
+ return keys;
45
+ }
46
+ /**
47
+ * Smallest order strictly greater than `after` that no patch occupies and
48
+ * that adds no order-related `patchPolicy` finding (collision, category
49
+ * range, reserved range, numeric gap) for a `category` patch. A retired
50
+ * slot counts as free, since a patch may fill it; a reservation does not,
51
+ * since it is held for its owner's export. Returns null when no such
52
+ * order exists inside the category's configured ranges.
53
+ *
54
+ * @param config - Project config (only `patchPolicy` is read)
55
+ * @param manifest - Current manifest
56
+ * @param category - Category the new patch would carry
57
+ * @param after - The order the new patch must come after
58
+ */
59
+ export function earliestLegalOrderAfter(config, manifest, category, after) {
60
+ const occupied = new Set([
61
+ ...manifest.patches.map((patch) => patch.order),
62
+ ...reservedOrderSet(manifest),
63
+ ]);
64
+ const ranges = config.patchPolicy?.ranges.filter((range) => range.category === category) ?? [];
65
+ const limit = ranges.length > 0 ? Math.max(...ranges.map((range) => range.to)) : after + UNBOUNDED_SEARCH;
66
+ const baseline = config.patchPolicy ? orderIssueKeys(config, manifest) : new Set();
67
+ for (let order = after + 1; order <= limit; order++) {
68
+ if (occupied.has(order))
69
+ continue;
70
+ if (!config.patchPolicy)
71
+ return order;
72
+ const probe = {
73
+ filename: `${formatPatchOrder(order)}-${category}-probe.patch`,
74
+ order,
75
+ category,
76
+ name: 'probe',
77
+ description: 'probe',
78
+ createdAt: '',
79
+ sourceEsrVersion: '',
80
+ filesAffected: [],
81
+ };
82
+ const projected = { ...manifest, patches: [...manifest.patches, probe] };
83
+ const introduced = [...orderIssueKeys(config, projected)].filter((key) => !baseline.has(key));
84
+ if (introduced.length === 0)
85
+ return order;
86
+ }
87
+ return null;
88
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Registration-only patch policy: the `patchPolicy` rules that need the
3
+ * patch bodies, not just the manifest.
4
+ *
5
+ * - `upstreamAllowlist`: which upstream files (files no patch in the queue
6
+ * creates) a non-reserved patch may modify.
7
+ * - `addOnly` allowlist entries: for shared registration files, a patch may
8
+ * only add lines, and each non-blank added line must carry the marker.
9
+ * - `scopeRules`: limit a class of patch (a category, or a filename pattern
10
+ * such as `-tests\.patch$`) to the files its globs name.
11
+ * - `verifyDocumentPaths`: every reserved allowlist entry's `adr` /
12
+ * `documentation` path must exist.
13
+ *
14
+ * Every rule is opt-in, and none can be evaluated from `patches.json`
15
+ * alone, which is why they live apart from `patch-policy.ts`: that module
16
+ * answers synchronously over a (possibly projected) manifest, this one reads
17
+ * patch bodies and the filesystem. `verify` and `lint --per-patch` run both.
18
+ */
19
+ import type { PatchesManifest } from '../types/commands/index.js';
20
+ import type { FireForgeConfig } from '../types/config.js';
21
+ import { type PatchQueueEntry } from './patch-lint-cross.js';
22
+ import type { PatchPolicyIssue } from './patch-policy.js';
23
+ /** True when the policy sets any rule this module evaluates. */
24
+ export declare function hasPatchPolicyContentRules(config: FireForgeConfig): boolean;
25
+ /**
26
+ * Evaluates the body-dependent `patchPolicy` rules over a patch queue.
27
+ *
28
+ * @param config - Project configuration
29
+ * @param projectRoot - Project root, for the document-path check
30
+ * @param manifest - Current manifest (category and order per patch)
31
+ * @param entries - Patch queue entries with their bodies
32
+ * @returns Findings, in the same shape {@link evaluatePatchPolicy} returns
33
+ */
34
+ export declare function evaluatePatchPolicyContent(config: FireForgeConfig, projectRoot: string, manifest: PatchesManifest, entries: readonly PatchQueueEntry[]): Promise<PatchPolicyIssue[]>;
35
+ /**
36
+ * {@link evaluatePatchPolicyContent} for a caller without a queue context
37
+ * in hand (`verify`): reads the patch bodies only when a body-dependent
38
+ * rule is configured.
39
+ */
40
+ export declare function evaluatePatchPolicyContentForQueue(config: FireForgeConfig, projectRoot: string, patchesDir: string, manifest: PatchesManifest): Promise<PatchPolicyIssue[]>;
@@ -0,0 +1,216 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Registration-only patch policy: the `patchPolicy` rules that need the
4
+ * patch bodies, not just the manifest.
5
+ *
6
+ * - `upstreamAllowlist`: which upstream files (files no patch in the queue
7
+ * creates) a non-reserved patch may modify.
8
+ * - `addOnly` allowlist entries: for shared registration files, a patch may
9
+ * only add lines, and each non-blank added line must carry the marker.
10
+ * - `scopeRules`: limit a class of patch (a category, or a filename pattern
11
+ * such as `-tests\.patch$`) to the files its globs name.
12
+ * - `verifyDocumentPaths`: every reserved allowlist entry's `adr` /
13
+ * `documentation` path must exist.
14
+ *
15
+ * Every rule is opt-in, and none can be evaluated from `patches.json`
16
+ * alone, which is why they live apart from `patch-policy.ts`: that module
17
+ * answers synchronously over a (possibly projected) manifest, this one reads
18
+ * patch bodies and the filesystem. `verify` and `lint --per-patch` run both.
19
+ */
20
+ import { join } from 'node:path';
21
+ import { pathExists } from '../utils/fs.js';
22
+ import { matchesAnyGlob } from '../utils/path-glob.js';
23
+ import { buildPatchQueueContext } from './patch-lint-cross.js';
24
+ import { parseDiffSections } from './patch-parse.js';
25
+ /** True when the policy sets any rule this module evaluates. */
26
+ export function hasPatchPolicyContentRules(config) {
27
+ const cfg = config.patchPolicy;
28
+ return (cfg !== undefined &&
29
+ (cfg.upstreamAllowlist !== undefined ||
30
+ cfg.scopeRules !== undefined ||
31
+ cfg.verifyDocumentPaths === true));
32
+ }
33
+ function normalizeAllowEntry(entry) {
34
+ return typeof entry === 'string' ? { path: entry } : entry;
35
+ }
36
+ function isReservedOrder(cfg, order) {
37
+ return (cfg.reservedRanges ?? []).some((range) => order >= range.from && order <= range.to);
38
+ }
39
+ /**
40
+ * The upstream files a patch body edits, keyed by path: every section that
41
+ * does not create its file, minus paths some patch in the queue creates. A
42
+ * rename counts against both of its paths.
43
+ */
44
+ function collectUpstreamEdits(diff, queueCreated) {
45
+ const edits = new Map();
46
+ for (const section of parseDiffSections(diff)) {
47
+ if (section.isNewFile)
48
+ continue;
49
+ const paths = new Set([section.sourcePath, section.targetPath]);
50
+ for (const path of paths) {
51
+ if (queueCreated.has(path))
52
+ continue;
53
+ const edit = edits.get(path) ?? { added: [], removed: [], deleted: false, binary: false };
54
+ edit.deleted ||= section.isDeletedFile;
55
+ edit.binary ||= section.isBinary;
56
+ for (const hunk of section.hunks) {
57
+ for (const line of hunk.lines) {
58
+ if (line.startsWith('+'))
59
+ edit.added.push(line.slice(1));
60
+ else if (line.startsWith('-'))
61
+ edit.removed.push(line.slice(1));
62
+ }
63
+ }
64
+ edits.set(path, edit);
65
+ }
66
+ }
67
+ return edits;
68
+ }
69
+ /**
70
+ * Why an add-only file's edit breaks the mode, or undefined when it holds.
71
+ * Blank lines are free in both directions: they carry no registration.
72
+ */
73
+ function addOnlyViolation(edit, marker) {
74
+ if (edit.deleted)
75
+ return 'deletes the file';
76
+ if (edit.binary)
77
+ return 'carries a binary change';
78
+ const removed = edit.removed.filter((line) => line.trim() !== '');
79
+ if (removed.length > 0) {
80
+ return `removes ${String(removed.length)} line(s), first: ${JSON.stringify(removed[0])}`;
81
+ }
82
+ const needle = marker.toLowerCase();
83
+ const unmarked = edit.added.filter((line) => line.trim() !== '' && !line.toLowerCase().includes(needle));
84
+ if (unmarked.length > 0) {
85
+ return (`adds ${String(unmarked.length)} line(s) without the "${marker}" marker, ` +
86
+ `first: ${JSON.stringify(unmarked[0])}`);
87
+ }
88
+ return undefined;
89
+ }
90
+ function evaluateUpstreamEdits(cfg, filename, edits, severity) {
91
+ const allowlist = (cfg.upstreamAllowlist ?? []).map(normalizeAllowEntry);
92
+ const issues = [];
93
+ const outside = [];
94
+ for (const [path, edit] of [...edits].sort(([a], [b]) => a.localeCompare(b))) {
95
+ const matches = allowlist.filter((entry) => matchesAnyGlob(path, [entry.path]));
96
+ if (matches.length === 0) {
97
+ outside.push(path);
98
+ continue;
99
+ }
100
+ // Strictest wins: any add-only match makes the file add-only.
101
+ const addOnly = matches.find((entry) => entry.addOnly === true);
102
+ if (!addOnly)
103
+ continue;
104
+ const marker = addOnly.marker ?? cfg.addOnlyMarker ?? '';
105
+ const violation = addOnlyViolation(edit, marker);
106
+ if (violation !== undefined) {
107
+ issues.push({
108
+ code: 'add-only',
109
+ filename,
110
+ severity,
111
+ message: `${filename} edits ${path}, which patchPolicy.upstreamAllowlist marks add-only ` +
112
+ `(added lines carrying "${marker}"), but ${violation}.`,
113
+ });
114
+ }
115
+ }
116
+ if (outside.length > 0) {
117
+ issues.push({
118
+ code: 'upstream-allowlist',
119
+ filename,
120
+ severity,
121
+ message: `${filename} modifies upstream file(s) outside patchPolicy.upstreamAllowlist: ` +
122
+ `${outside.join(', ')}. Move the change into a fork-owned file, or add a reviewed allowlist entry.`,
123
+ });
124
+ }
125
+ return issues;
126
+ }
127
+ function evaluateScopeRules(cfg, filename, category, files, severity) {
128
+ const issues = [];
129
+ for (const [index, rule] of (cfg.scopeRules ?? []).entries()) {
130
+ if (rule.category !== undefined && rule.category !== category)
131
+ continue;
132
+ if (rule.filenamePattern !== undefined && !new RegExp(rule.filenamePattern).test(filename)) {
133
+ continue;
134
+ }
135
+ const outside = files.filter((file) => !matchesAnyGlob(file, rule.files));
136
+ if (outside.length === 0)
137
+ continue;
138
+ issues.push({
139
+ code: 'scope',
140
+ filename,
141
+ severity,
142
+ message: `${filename} is scoped by patchPolicy.scopeRules[${String(index)}] to ` +
143
+ `${rule.files.join(', ')}, but touches: ${outside.join(', ')}. ` +
144
+ 'Move those files to a patch of the class that owns them.',
145
+ });
146
+ }
147
+ return issues;
148
+ }
149
+ async function evaluateDocumentPaths(cfg, projectRoot, severity) {
150
+ if (cfg.verifyDocumentPaths !== true)
151
+ return [];
152
+ const issues = [];
153
+ for (const range of cfg.reservedRanges ?? []) {
154
+ for (const allowed of range.allowed) {
155
+ for (const [field, path] of [
156
+ ['adr', allowed.adr],
157
+ ['documentation', allowed.documentation],
158
+ ]) {
159
+ if (path === undefined || (await pathExists(join(projectRoot, path))))
160
+ continue;
161
+ issues.push({
162
+ code: 'reserved-document-missing',
163
+ filename: allowed.filename,
164
+ severity,
165
+ message: `${allowed.filename}'s reserved-range allowlist entry names ${field} "${path}", ` +
166
+ 'which does not exist. Restore the document or correct the path.',
167
+ });
168
+ }
169
+ }
170
+ }
171
+ return issues;
172
+ }
173
+ /**
174
+ * Evaluates the body-dependent `patchPolicy` rules over a patch queue.
175
+ *
176
+ * @param config - Project configuration
177
+ * @param projectRoot - Project root, for the document-path check
178
+ * @param manifest - Current manifest (category and order per patch)
179
+ * @param entries - Patch queue entries with their bodies
180
+ * @returns Findings, in the same shape {@link evaluatePatchPolicy} returns
181
+ */
182
+ export async function evaluatePatchPolicyContent(config, projectRoot, manifest, entries) {
183
+ const cfg = config.patchPolicy;
184
+ if (!cfg || !hasPatchPolicyContentRules(config))
185
+ return [];
186
+ const severity = cfg.mutationMode === 'warn' ? 'warning' : 'error';
187
+ const queueCreated = new Set();
188
+ for (const entry of entries)
189
+ for (const file of entry.createdFiles)
190
+ queueCreated.add(file);
191
+ const bodies = new Map(entries.map((entry) => [entry.filename, entry.diff]));
192
+ const issues = [];
193
+ for (const patch of manifest.patches) {
194
+ const diff = bodies.get(patch.filename);
195
+ if (cfg.upstreamAllowlist !== undefined && diff !== undefined) {
196
+ if (!isReservedOrder(cfg, patch.order)) {
197
+ const edits = collectUpstreamEdits(diff, queueCreated);
198
+ issues.push(...evaluateUpstreamEdits(cfg, patch.filename, edits, severity));
199
+ }
200
+ }
201
+ issues.push(...evaluateScopeRules(cfg, patch.filename, patch.category, patch.filesAffected, severity));
202
+ }
203
+ issues.push(...(await evaluateDocumentPaths(cfg, projectRoot, severity)));
204
+ return issues;
205
+ }
206
+ /**
207
+ * {@link evaluatePatchPolicyContent} for a caller without a queue context
208
+ * in hand (`verify`): reads the patch bodies only when a body-dependent
209
+ * rule is configured.
210
+ */
211
+ export async function evaluatePatchPolicyContentForQueue(config, projectRoot, patchesDir, manifest) {
212
+ if (!hasPatchPolicyContentRules(config))
213
+ return [];
214
+ const { entries } = await buildPatchQueueContext(patchesDir);
215
+ return evaluatePatchPolicyContent(config, projectRoot, manifest, entries);
216
+ }