@hominis/fireforge 0.44.4 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (378) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/dist/bin/fireforge.js +6 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/src/cli-usage-error.d.ts +46 -0
  5. package/dist/src/cli-usage-error.js +81 -0
  6. package/dist/src/cli.d.ts +5 -1
  7. package/dist/src/cli.js +20 -21
  8. package/dist/src/commands/bootstrap.js +1 -2
  9. package/dist/src/commands/build.js +15 -6
  10. package/dist/src/commands/destructive-decision.d.ts +19 -0
  11. package/dist/src/commands/destructive-decision.js +31 -0
  12. package/dist/src/commands/doctor-check-core.d.ts +0 -6
  13. package/dist/src/commands/doctor-check-core.js +0 -8
  14. package/dist/src/commands/doctor-orphaned-harness.d.ts +0 -6
  15. package/dist/src/commands/doctor-orphaned-harness.js +4 -12
  16. package/dist/src/commands/doctor-post-rebase-audit.js +1 -1
  17. package/dist/src/commands/doctor-source-pin.d.ts +29 -0
  18. package/dist/src/commands/doctor-source-pin.js +81 -0
  19. package/dist/src/commands/doctor.d.ts +2 -0
  20. package/dist/src/commands/doctor.js +9 -8
  21. package/dist/src/commands/download.js +35 -24
  22. package/dist/src/commands/export-all.js +21 -4
  23. package/dist/src/commands/export-flow.js +15 -60
  24. package/dist/src/commands/export-placement-conflicts.js +3 -3
  25. package/dist/src/commands/export-placement-gate.js +2 -8
  26. package/dist/src/commands/export-placement-policy.js +3 -5
  27. package/dist/src/commands/export-shared.d.ts +42 -23
  28. package/dist/src/commands/export-shared.js +4 -19
  29. package/dist/src/commands/export.js +10 -1
  30. package/dist/src/commands/furnace/authoring-config.d.ts +13 -0
  31. package/dist/src/commands/furnace/authoring-config.js +19 -0
  32. package/dist/src/commands/furnace/chrome-doc-remove.js +1 -1
  33. package/dist/src/commands/furnace/create-browser-test.js +3 -8
  34. package/dist/src/commands/furnace/create-dry-run.js +2 -5
  35. package/dist/src/commands/furnace/create-templates.d.ts +17 -1
  36. package/dist/src/commands/furnace/create-templates.js +2 -1
  37. package/dist/src/commands/furnace/create.js +25 -19
  38. package/dist/src/commands/furnace/deploy.js +11 -2
  39. package/dist/src/commands/furnace/init.js +2 -1
  40. package/dist/src/commands/furnace/override.js +28 -19
  41. package/dist/src/commands/furnace/refresh.js +9 -1
  42. package/dist/src/commands/furnace/remove-mutations.d.ts +6 -7
  43. package/dist/src/commands/furnace/remove-mutations.js +0 -1
  44. package/dist/src/commands/furnace/remove.js +2 -6
  45. package/dist/src/commands/furnace/rename-browser-test.js +1 -7
  46. package/dist/src/commands/furnace/rename.js +29 -29
  47. package/dist/src/commands/furnace/status.js +12 -9
  48. package/dist/src/commands/furnace/test-file-name.d.ts +28 -0
  49. package/dist/src/commands/furnace/test-file-name.js +39 -0
  50. package/dist/src/commands/furnace/validation-output.d.ts +21 -9
  51. package/dist/src/commands/furnace/validation-output.js +3 -8
  52. package/dist/src/commands/import.js +2 -9
  53. package/dist/src/commands/lint-per-patch.d.ts +2 -2
  54. package/dist/src/commands/lint-per-patch.js +19 -4
  55. package/dist/src/commands/lint-per-run-checkjs.d.ts +2 -2
  56. package/dist/src/commands/lint.d.ts +2 -1
  57. package/dist/src/commands/lint.js +1 -1
  58. package/dist/src/commands/manifest.js +1 -1
  59. package/dist/src/commands/package.js +2 -4
  60. package/dist/src/commands/patch/compact.js +14 -15
  61. package/dist/src/commands/patch/delete.js +15 -25
  62. package/dist/src/commands/patch/entry-projection.d.ts +13 -0
  63. package/dist/src/commands/patch/entry-projection.js +24 -0
  64. package/dist/src/commands/patch/lint-ignore.d.ts +0 -8
  65. package/dist/src/commands/patch/lint-ignore.js +19 -29
  66. package/dist/src/commands/patch/move-files-create.js +24 -10
  67. package/dist/src/commands/patch/move-files-into.js +38 -37
  68. package/dist/src/commands/patch/move-files.js +4 -13
  69. package/dist/src/commands/patch/rename.js +27 -41
  70. package/dist/src/commands/patch/reorder.js +25 -38
  71. package/dist/src/commands/patch/split-plan.d.ts +4 -5
  72. package/dist/src/commands/patch/split-plan.js +4 -11
  73. package/dist/src/commands/patch/split.js +45 -43
  74. package/dist/src/commands/patch/staged-dependency.js +16 -22
  75. package/dist/src/commands/patch/tier.js +15 -25
  76. package/dist/src/commands/patch-rename-map.d.ts +19 -0
  77. package/dist/src/commands/patch-rename-map.js +36 -0
  78. package/dist/src/commands/pending-resolution.d.ts +15 -0
  79. package/dist/src/commands/pending-resolution.js +25 -0
  80. package/dist/src/commands/re-export-adjacent.d.ts +32 -2
  81. package/dist/src/commands/re-export-adjacent.js +51 -10
  82. package/dist/src/commands/re-export-bulk-scan.js +6 -10
  83. package/dist/src/commands/re-export-drift.d.ts +8 -0
  84. package/dist/src/commands/re-export-drift.js +65 -4
  85. package/dist/src/commands/re-export-files.d.ts +2 -3
  86. package/dist/src/commands/re-export-files.js +38 -32
  87. package/dist/src/commands/re-export-lint.d.ts +3 -4
  88. package/dist/src/commands/re-export-lint.js +9 -2
  89. package/dist/src/commands/re-export-options.js +19 -5
  90. package/dist/src/commands/re-export-register.js +5 -1
  91. package/dist/src/commands/re-export-scan.js +8 -8
  92. package/dist/src/commands/re-export-single.d.ts +22 -3
  93. package/dist/src/commands/re-export-single.js +19 -11
  94. package/dist/src/commands/re-export.js +44 -27
  95. package/dist/src/commands/rebase/abort.js +3 -8
  96. package/dist/src/commands/rebase/continue.js +12 -12
  97. package/dist/src/commands/rebase/index.js +2 -2
  98. package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
  99. package/dist/src/commands/rebase/patch-loop.js +2 -7
  100. package/dist/src/commands/resolve.js +18 -10
  101. package/dist/src/commands/run.d.ts +0 -13
  102. package/dist/src/commands/run.js +12 -25
  103. package/dist/src/commands/setup-support.d.ts +16 -4
  104. package/dist/src/commands/setup-support.js +16 -16
  105. package/dist/src/commands/setup.js +3 -9
  106. package/dist/src/commands/source.js +8 -1
  107. package/dist/src/commands/status-check.js +2 -2
  108. package/dist/src/commands/status-output.js +2 -2
  109. package/dist/src/commands/status.js +3 -4
  110. package/dist/src/commands/test-build-phase.d.ts +2 -2
  111. package/dist/src/commands/test-build-phase.js +33 -8
  112. package/dist/src/commands/test-diagnose.js +14 -5
  113. package/dist/src/commands/test-modes.d.ts +2 -2
  114. package/dist/src/commands/test-modes.js +0 -1
  115. package/dist/src/commands/test-register.js +6 -3
  116. package/dist/src/commands/test-run.js +29 -21
  117. package/dist/src/commands/test-stale-gate.js +8 -5
  118. package/dist/src/commands/test-verdict.d.ts +16 -2
  119. package/dist/src/commands/test-verdict.js +18 -3
  120. package/dist/src/commands/test.js +51 -7
  121. package/dist/src/commands/token-list.d.ts +26 -0
  122. package/dist/src/commands/token-list.js +153 -0
  123. package/dist/src/commands/token.js +17 -1
  124. package/dist/src/commands/tree.js +7 -2
  125. package/dist/src/commands/typecheck.js +9 -9
  126. package/dist/src/commands/wire.js +4 -3
  127. package/dist/src/core/branding.d.ts +0 -7
  128. package/dist/src/core/branding.js +3 -2
  129. package/dist/src/core/browser-wire.js +6 -3
  130. package/dist/src/core/build-audit-platform.d.ts +29 -0
  131. package/dist/src/core/build-audit-platform.js +140 -0
  132. package/dist/src/core/build-audit-registration.js +3 -2
  133. package/dist/src/core/build-audit-skip.d.ts +57 -0
  134. package/dist/src/core/build-audit-skip.js +112 -0
  135. package/dist/src/core/build-audit.d.ts +15 -2
  136. package/dist/src/core/build-audit.js +154 -12
  137. package/dist/src/core/build-baseline.d.ts +45 -28
  138. package/dist/src/core/build-baseline.js +6 -29
  139. package/dist/src/core/build-jar-escalation.d.ts +64 -0
  140. package/dist/src/core/build-jar-escalation.js +128 -0
  141. package/dist/src/core/build-overwrite-guard.js +1 -1
  142. package/dist/src/core/build-prepare.d.ts +5 -2
  143. package/dist/src/core/build-prepare.js +34 -6
  144. package/dist/src/core/config-paths.d.ts +2 -6
  145. package/dist/src/core/config-paths.js +4 -2
  146. package/dist/src/core/config-state.d.ts +0 -6
  147. package/dist/src/core/config-state.js +1 -19
  148. package/dist/src/core/config-validate-build-audit.d.ts +8 -0
  149. package/dist/src/core/config-validate-build-audit.js +80 -0
  150. package/dist/src/core/config-validate-file-size.d.ts +12 -0
  151. package/dist/src/core/config-validate-file-size.js +65 -0
  152. package/dist/src/core/config-validate-patch-policy.d.ts +2 -2
  153. package/dist/src/core/config-validate-patch-policy.js +3 -3
  154. package/dist/src/core/config-validate-test-toolchains.d.ts +2 -2
  155. package/dist/src/core/config-validate-test-toolchains.js +4 -5
  156. package/dist/src/core/config-validate.js +20 -3
  157. package/dist/src/core/config.d.ts +2 -2
  158. package/dist/src/core/config.js +11 -15
  159. package/dist/src/core/coverage-extend.js +4 -4
  160. package/dist/src/core/discard-baseline.js +7 -7
  161. package/dist/src/core/engine-precondition.d.ts +1 -9
  162. package/dist/src/core/engine-precondition.js +3 -8
  163. package/dist/src/core/engine-session-lock.js +13 -17
  164. package/dist/src/core/export-moved-code-shape.js +2 -2
  165. package/dist/src/core/file-lock-owner.d.ts +103 -0
  166. package/dist/src/core/file-lock-owner.js +259 -0
  167. package/dist/src/core/file-lock.d.ts +2 -7
  168. package/dist/src/core/file-lock.js +40 -91
  169. package/dist/src/core/firefox-cache.d.ts +13 -1
  170. package/dist/src/core/firefox-cache.js +59 -34
  171. package/dist/src/core/firefox-download.d.ts +15 -0
  172. package/dist/src/core/firefox-download.js +37 -21
  173. package/dist/src/core/firefox-extract.d.ts +6 -5
  174. package/dist/src/core/firefox-extract.js +36 -18
  175. package/dist/src/core/firefox.d.ts +30 -32
  176. package/dist/src/core/firefox.js +5 -36
  177. package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
  178. package/dist/src/core/furnace-apply-dry-run.js +1 -7
  179. package/dist/src/core/furnace-apply-helpers.d.ts +0 -11
  180. package/dist/src/core/furnace-apply-helpers.js +8 -19
  181. package/dist/src/core/furnace-apply-overwrite-warn.js +4 -2
  182. package/dist/src/core/furnace-apply.d.ts +1 -1
  183. package/dist/src/core/furnace-apply.js +30 -10
  184. package/dist/src/core/furnace-config.d.ts +1 -1
  185. package/dist/src/core/furnace-config.js +23 -36
  186. package/dist/src/core/furnace-constants.js +3 -2
  187. package/dist/src/core/furnace-dir-entry.d.ts +20 -0
  188. package/dist/src/core/furnace-dir-entry.js +21 -0
  189. package/dist/src/core/furnace-operation.d.ts +2 -0
  190. package/dist/src/core/furnace-operation.js +3 -1
  191. package/dist/src/core/furnace-refresh.d.ts +18 -6
  192. package/dist/src/core/furnace-refresh.js +3 -6
  193. package/dist/src/core/furnace-registration-ast.d.ts +0 -1
  194. package/dist/src/core/furnace-registration-ast.js +0 -3
  195. package/dist/src/core/furnace-registration.d.ts +2 -1
  196. package/dist/src/core/furnace-registration.js +22 -29
  197. package/dist/src/core/furnace-scanner.js +3 -6
  198. package/dist/src/core/furnace-state-persist.d.ts +2 -2
  199. package/dist/src/core/furnace-validate-registration.js +5 -7
  200. package/dist/src/core/git-base.d.ts +3 -1
  201. package/dist/src/core/git-base.js +25 -3
  202. package/dist/src/core/git-diff-new-file.d.ts +40 -0
  203. package/dist/src/core/git-diff-new-file.js +119 -0
  204. package/dist/src/core/git-diff.d.ts +9 -9
  205. package/dist/src/core/git-diff.js +88 -157
  206. package/dist/src/core/git-file-ops.d.ts +0 -7
  207. package/dist/src/core/git-file-ops.js +4 -28
  208. package/dist/src/core/git-readonly-index.d.ts +0 -2
  209. package/dist/src/core/git-readonly-index.js +0 -4
  210. package/dist/src/core/git-status.d.ts +7 -0
  211. package/dist/src/core/git-status.js +21 -9
  212. package/dist/src/core/git.d.ts +0 -6
  213. package/dist/src/core/git.js +14 -43
  214. package/dist/src/core/harness-orphans.d.ts +73 -0
  215. package/dist/src/core/harness-orphans.js +187 -0
  216. package/dist/src/core/history-log.d.ts +19 -0
  217. package/dist/src/core/history-log.js +29 -0
  218. package/dist/src/core/mach-error-hints.d.ts +0 -5
  219. package/dist/src/core/mach-error-hints.js +1 -1
  220. package/dist/src/core/mach-known-noise-filter.d.ts +25 -3
  221. package/dist/src/core/mach-known-noise-filter.js +31 -10
  222. package/dist/src/core/mach-mozconfig.js +2 -1
  223. package/dist/src/core/mach-python.d.ts +2 -0
  224. package/dist/src/core/mach-python.js +12 -7
  225. package/dist/src/core/mach-resource-shim.d.ts +13 -1
  226. package/dist/src/core/mach-resource-shim.js +112 -9
  227. package/dist/src/core/mach.d.ts +46 -53
  228. package/dist/src/core/mach.js +31 -83
  229. package/dist/src/core/marionette-port.d.ts +0 -17
  230. package/dist/src/core/marionette-port.js +12 -58
  231. package/dist/src/core/marionette-preflight.js +9 -27
  232. package/dist/src/core/mochitest-server-port.js +3 -3
  233. package/dist/src/core/moz-manifest-rules.js +37 -33
  234. package/dist/src/core/patch-apply-fuzz.js +4 -4
  235. package/dist/src/core/patch-apply.d.ts +4 -5
  236. package/dist/src/core/patch-apply.js +55 -32
  237. package/dist/src/core/patch-export-coverage.d.ts +0 -9
  238. package/dist/src/core/patch-export-coverage.js +1 -30
  239. package/dist/src/core/patch-export-update.d.ts +18 -1
  240. package/dist/src/core/patch-export-update.js +4 -3
  241. package/dist/src/core/patch-export.d.ts +11 -21
  242. package/dist/src/core/patch-export.js +44 -66
  243. package/dist/src/core/patch-files.d.ts +0 -4
  244. package/dist/src/core/patch-files.js +0 -11
  245. package/dist/src/core/patch-lint-binary.d.ts +6 -12
  246. package/dist/src/core/patch-lint-cache.d.ts +17 -1
  247. package/dist/src/core/patch-lint-cache.js +3 -6
  248. package/dist/src/core/patch-lint-checkjs.d.ts +33 -16
  249. package/dist/src/core/patch-lint-checkjs.js +43 -25
  250. package/dist/src/core/patch-lint-creators.d.ts +2 -6
  251. package/dist/src/core/patch-lint-cross.d.ts +16 -8
  252. package/dist/src/core/patch-lint-cross.js +49 -92
  253. package/dist/src/core/patch-lint-css.js +10 -6
  254. package/dist/src/core/patch-lint-file-size.d.ts +52 -0
  255. package/dist/src/core/patch-lint-file-size.js +53 -0
  256. package/dist/src/core/patch-lint-forward-registration.d.ts +58 -0
  257. package/dist/src/core/patch-lint-forward-registration.js +210 -0
  258. package/dist/src/core/patch-lint-jsdoc.js +36 -36
  259. package/dist/src/core/patch-lint-module-registration.d.ts +2 -8
  260. package/dist/src/core/patch-lint-prettier.d.ts +36 -0
  261. package/dist/src/core/patch-lint-prettier.js +126 -0
  262. package/dist/src/core/patch-lint-queue-types.d.ts +60 -0
  263. package/dist/src/core/patch-lint-queue-types.js +12 -0
  264. package/dist/src/core/patch-lint-staged-registration.d.ts +61 -0
  265. package/dist/src/core/patch-lint-staged-registration.js +90 -0
  266. package/dist/src/core/patch-lint-unmanaged-companion.d.ts +106 -0
  267. package/dist/src/core/patch-lint-unmanaged-companion.js +214 -0
  268. package/dist/src/core/patch-lint.d.ts +14 -9
  269. package/dist/src/core/patch-lint.js +27 -27
  270. package/dist/src/core/patch-lock.js +3 -3
  271. package/dist/src/core/patch-manifest-io.js +8 -7
  272. package/dist/src/core/patch-parse.d.ts +19 -8
  273. package/dist/src/core/patch-parse.js +25 -0
  274. package/dist/src/core/patch-policy.js +15 -27
  275. package/dist/src/core/patch-source-metadata.d.ts +1 -3
  276. package/dist/src/core/patch-source-metadata.js +0 -4
  277. package/dist/src/core/patch-transform.d.ts +21 -8
  278. package/dist/src/core/patch-transform.js +38 -10
  279. package/dist/src/core/process-cpu-time.js +1 -1
  280. package/dist/src/core/rebase-session.d.ts +2 -8
  281. package/dist/src/core/rebase-session.js +5 -17
  282. package/dist/src/core/register-browser-content.js +3 -2
  283. package/dist/src/core/register-browser-test.js +2 -1
  284. package/dist/src/core/register-module.js +2 -1
  285. package/dist/src/core/register-scaffold.js +5 -4
  286. package/dist/src/core/register-shared-css.js +2 -1
  287. package/dist/src/core/run-log-redact.d.ts +38 -0
  288. package/dist/src/core/run-log-redact.js +69 -0
  289. package/dist/src/core/run-log.d.ts +13 -2
  290. package/dist/src/core/run-log.js +67 -18
  291. package/dist/src/core/signal-critical.js +2 -4
  292. package/dist/src/core/smoke-patterns.d.ts +0 -1
  293. package/dist/src/core/smoke-patterns.js +18 -11
  294. package/dist/src/core/state-file.d.ts +12 -1
  295. package/dist/src/core/state-file.js +14 -1
  296. package/dist/src/core/status-classify.js +2 -1
  297. package/dist/src/core/test-changed-prefs.js +26 -11
  298. package/dist/src/core/test-harness-crash.js +61 -48
  299. package/dist/src/core/test-harness-verdict-notes.d.ts +55 -0
  300. package/dist/src/core/test-harness-verdict-notes.js +48 -0
  301. package/dist/src/core/test-stale-check.d.ts +23 -8
  302. package/dist/src/core/test-stale-check.js +77 -4
  303. package/dist/src/core/test-stall-triage.js +37 -19
  304. package/dist/src/core/test-xpcshell-retry.d.ts +26 -7
  305. package/dist/src/core/test-xpcshell-retry.js +14 -11
  306. package/dist/src/core/token-category.d.ts +33 -0
  307. package/dist/src/core/token-category.js +56 -48
  308. package/dist/src/core/token-dark-mode.js +2 -14
  309. package/dist/src/core/token-inventory.d.ts +56 -0
  310. package/dist/src/core/token-inventory.js +109 -0
  311. package/dist/src/core/token-manager.d.ts +1 -1
  312. package/dist/src/core/token-manager.js +1 -1
  313. package/dist/src/core/token-variant.js +21 -22
  314. package/dist/src/core/toolchain-preflight.js +1 -1
  315. package/dist/src/core/tree-cow.d.ts +0 -2
  316. package/dist/src/core/tree-cow.js +2 -2
  317. package/dist/src/core/tree-store.js +4 -5
  318. package/dist/src/core/typecheck.js +2 -2
  319. package/dist/src/core/wire-dom-fragment.js +6 -17
  320. package/dist/src/core/wire-utils.js +5 -17
  321. package/dist/src/core/xpcshell-appdir.js +2 -4
  322. package/dist/src/errors/base.d.ts +53 -1
  323. package/dist/src/errors/base.js +79 -11
  324. package/dist/src/errors/build.d.ts +28 -0
  325. package/dist/src/errors/build.js +51 -10
  326. package/dist/src/errors/config.d.ts +0 -14
  327. package/dist/src/errors/config.js +13 -34
  328. package/dist/src/errors/download.d.ts +13 -0
  329. package/dist/src/errors/download.js +53 -24
  330. package/dist/src/errors/furnace.js +8 -9
  331. package/dist/src/errors/git.d.ts +8 -8
  332. package/dist/src/errors/git.js +20 -30
  333. package/dist/src/errors/patch.js +8 -10
  334. package/dist/src/errors/rebase.js +7 -7
  335. package/dist/src/types/commands/options.d.ts +96 -123
  336. package/dist/src/types/commands/patches.d.ts +17 -6
  337. package/dist/src/types/config.d.ts +75 -2
  338. package/dist/src/types/index.d.ts +0 -1
  339. package/dist/src/utils/assert.d.ts +3 -15
  340. package/dist/src/utils/assert.js +1 -18
  341. package/dist/src/utils/build-info.d.ts +1 -1
  342. package/dist/src/utils/child-env.d.ts +2 -1
  343. package/dist/src/utils/elapsed.js +10 -1
  344. package/dist/src/utils/fs.d.ts +4 -2
  345. package/dist/src/utils/fs.js +11 -14
  346. package/dist/src/utils/hash.d.ts +12 -0
  347. package/dist/src/utils/hash.js +16 -0
  348. package/dist/src/utils/line-dispatch.d.ts +15 -0
  349. package/dist/src/utils/line-dispatch.js +48 -0
  350. package/dist/src/utils/options.d.ts +1 -7
  351. package/dist/src/utils/options.js +6 -7
  352. package/dist/src/utils/package-root.js +3 -12
  353. package/dist/src/utils/platform.d.ts +0 -4
  354. package/dist/src/utils/process-group.d.ts +2 -1
  355. package/dist/src/utils/process-group.js +20 -19
  356. package/dist/src/utils/process.d.ts +40 -26
  357. package/dist/src/utils/process.js +115 -166
  358. package/dist/src/utils/ps-duration.js +3 -3
  359. package/dist/src/utils/sleep.d.ts +26 -0
  360. package/dist/src/utils/sleep.js +31 -0
  361. package/dist/src/utils/validation.d.ts +2 -38
  362. package/dist/src/utils/validation.js +7 -16
  363. package/docs/configuration.md +136 -1
  364. package/docs/exit-codes.md +34 -20
  365. package/docs/lifecycle-invariants.md +18 -12
  366. package/docs/machine-output.md +31 -8
  367. package/docs/patch-workflow.md +2 -1
  368. package/docs/run-logs.md +44 -4
  369. package/docs/testing.md +70 -11
  370. package/package.json +8 -7
  371. package/dist/src/commands/rebase.d.ts +0 -4
  372. package/dist/src/commands/rebase.js +0 -5
  373. package/dist/src/core/patch-artifact-normalize.d.ts +0 -21
  374. package/dist/src/core/patch-artifact-normalize.js +0 -24
  375. package/dist/src/core/patch-lint-reexports.d.ts +0 -7
  376. package/dist/src/core/patch-lint-reexports.js +0 -8
  377. package/dist/src/core/wire-targets.d.ts +0 -7
  378. package/dist/src/core/wire-targets.js +0 -8
@@ -0,0 +1,214 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Recognizes the one shape where the undefined-identifier hint sends the
4
+ * operator at the wrong remedy — and where taking that remedy would MASK a
5
+ * real bug.
6
+ *
7
+ * The shape: helpers are hoisted out of duplicated test files into a new
8
+ * `head_settings.js` that a managed `head.js` pulls in with
9
+ * `loadSubScript`. Until that companion is exported it belongs to no patch,
10
+ * so the per-patch checkJs program — which only ever loads patch-owned
11
+ * roots — cannot see any of its declarations. Every use of every hoisted
12
+ * helper is then reported as `Cannot find name`, and the generic hint
13
+ * offers three remedies of which the shim is the easiest: adding the
14
+ * globals to `extraShim` makes all of the warnings disappear AND makes a
15
+ * genuinely missing helper undetectable forever after.
16
+ *
17
+ * The warnings themselves are correct — the per-patch program's honest view
18
+ * of a file that is in no patch. Only the advice is wrong. FireForge
19
+ * already knows ownership, so when every undefined name in a file is
20
+ * declared at the top level of an UNMANAGED file that a managed head loads,
21
+ * it can name that file and the adoption command instead.
22
+ *
23
+ * Detection is deliberately conservative on both halves: a companion must
24
+ * be reachable from an actual `loadSubScript` call, and EVERY undefined
25
+ * name must resolve inside it. One unresolved name means the file may
26
+ * genuinely be missing a helper, and the generic hint stays.
27
+ */
28
+ import { readFile } from 'node:fs/promises';
29
+ import { join } from 'node:path';
30
+ import { UNDEFINED_IDENTIFIER_HINT } from './typecheck-shim.js';
31
+ /**
32
+ * String literals passed anywhere inside a `loadSubScript(...)` call.
33
+ *
34
+ * The mochitest idiom is
35
+ * `Services.scriptloader.loadSubScript(getRootDirectory(gTestPath) + "head_settings.js", this)`,
36
+ * so the useful signal is the `.js` literal inside the call, not a
37
+ * resolvable URL — the path half is computed at runtime.
38
+ */
39
+ export function findLoadSubScriptTargets(source) {
40
+ const targets = [];
41
+ // Scanned to the statement terminator rather than to a closing paren:
42
+ // the idiomatic argument is itself a call (`getRootDirectory(gTestPath) +
43
+ // "head_settings.js"`), so a non-greedy `\)` stops on the INNER paren and
44
+ // never reaches the literal.
45
+ for (const call of source.matchAll(/loadSubScript\s*\(/g)) {
46
+ const from = call.index + call[0].length;
47
+ const semicolon = source.indexOf(';', from);
48
+ const to = semicolon === -1 ? from + CALL_SCAN_LIMIT : Math.min(semicolon, from + CALL_SCAN_LIMIT);
49
+ for (const literal of source.slice(from, to).matchAll(/["'`]([^"'`]*\.js)["'`]/g)) {
50
+ const value = literal[1];
51
+ if (value !== undefined && value.length > 0)
52
+ targets.push(value);
53
+ }
54
+ }
55
+ return targets;
56
+ }
57
+ /** How far past a `loadSubScript(` the argument scan looks. */
58
+ const CALL_SCAN_LIMIT = 400;
59
+ /**
60
+ * Top-level declaration names in a script-scope file.
61
+ *
62
+ * Regex rather than a TS program on purpose: this runs only to decide the
63
+ * WORDING of a hint, the file is by definition outside every program the
64
+ * pass built, and a missed declaration degrades to the generic hint rather
65
+ * than to a wrong answer. Matching is anchored at column zero so a nested
66
+ * declaration inside a function body is not mistaken for a global.
67
+ */
68
+ export function collectTopLevelDeclarations(source) {
69
+ const names = new Set();
70
+ const patterns = [
71
+ /^(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)/gm,
72
+ /^(?:const|let|var)\s+([A-Za-z_$][\w$]*)/gm,
73
+ /^class\s+([A-Za-z_$][\w$]*)/gm,
74
+ ];
75
+ for (const pattern of patterns) {
76
+ for (const match of source.matchAll(pattern)) {
77
+ const name = match[1];
78
+ if (name !== undefined)
79
+ names.add(name);
80
+ }
81
+ }
82
+ return names;
83
+ }
84
+ /**
85
+ * Extracts the identifier from a `Cannot find name 'X'` diagnostic message,
86
+ * or undefined when the message is not one.
87
+ */
88
+ function extractUndefinedIdentifier(message) {
89
+ const match = /Cannot find name '([^']+)'/.exec(message);
90
+ return match?.[1];
91
+ }
92
+ /**
93
+ * Resolves the unmanaged companions reachable from a set of managed head
94
+ * files, given a reader for repo-relative file content.
95
+ *
96
+ * A candidate qualifies only when it is NOT in `ownedFiles` and its content
97
+ * can be read — an unreadable or absent target is simply not a companion,
98
+ * and the generic hint applies.
99
+ *
100
+ * @param heads - Repo-relative managed head files, with their sources
101
+ * @param ownedFiles - Every patch-owned repo-relative path
102
+ * @param readFile - Reads a repo-relative file, or returns undefined
103
+ */
104
+ export async function resolveUnmanagedCompanions(heads, ownedFiles, readFile) {
105
+ const companions = [];
106
+ const seen = new Set();
107
+ for (const head of heads) {
108
+ const dir = head.file.slice(0, head.file.lastIndexOf('/') + 1);
109
+ for (const target of findLoadSubScriptTargets(head.source)) {
110
+ // Only same-directory targets: the runtime path is computed from the
111
+ // test's own directory, and a bare basename is what that idiom yields.
112
+ const candidate = `${dir}${target.slice(target.lastIndexOf('/') + 1)}`;
113
+ if (candidate === head.file || ownedFiles.has(candidate) || seen.has(candidate))
114
+ continue;
115
+ seen.add(candidate);
116
+ const source = await readFile(candidate);
117
+ if (source === undefined)
118
+ continue;
119
+ companions.push({
120
+ file: candidate,
121
+ loadedBy: head.file,
122
+ declarations: collectTopLevelDeclarations(source),
123
+ });
124
+ }
125
+ }
126
+ return companions;
127
+ }
128
+ /**
129
+ * The replacement hint, when every undefined name resolves in one
130
+ * unmanaged companion.
131
+ *
132
+ * States the ownership fact first (that is the diagnosis), then the
133
+ * adoption command, and explicitly rules the shim OUT — the shim is what an
134
+ * operator reaches for by default here, and it is the one remedy that
135
+ * permanently hides a genuinely missing helper.
136
+ */
137
+ export function formatUnmanagedCompanionHint(companion) {
138
+ return (`(undefined identifier — every one of them is declared in ${companion.file}, which ` +
139
+ `${companion.loadedBy} loads via loadSubScript but which NO patch owns yet, so the ` +
140
+ `per-patch program cannot see it. Adopt it with ` +
141
+ `"fireforge re-export <patch> --scan --scan-file ${companion.file}". Do NOT add these ` +
142
+ `globals to the extra shim: that silences these warnings and every future genuinely ` +
143
+ `missing helper with them.)`);
144
+ }
145
+ /**
146
+ * Picks the companion that explains EVERY undefined identifier in
147
+ * `messages`, or undefined when none does.
148
+ *
149
+ * @param messages - Diagnostic messages reported for one file
150
+ * @param companions - Candidates from {@link resolveUnmanagedCompanions}
151
+ */
152
+ export function explainUndefinedIdentifiers(messages, companions) {
153
+ const names = messages
154
+ .map((message) => extractUndefinedIdentifier(message))
155
+ .filter((name) => name !== undefined);
156
+ if (names.length === 0)
157
+ return undefined;
158
+ // Only the undefined-identifier diagnostics are explained here; a file
159
+ // carrying OTHER type errors still gets those reported untouched.
160
+ return companions.find((companion) => names.every((name) => companion.declarations.has(name)));
161
+ }
162
+ /**
163
+ * Reads the head files in the owned test set and resolves the UNMANAGED
164
+ * companions they pull in with `loadSubScript`. Hoisting shared helpers
165
+ * into such a companion leaves it unowned until export, and every use of
166
+ * every helper then reports `Cannot find name` from an honest per-patch
167
+ * program — see `patch-lint-unmanaged-companion.ts` for why the generic
168
+ * hint is actively harmful there.
169
+ *
170
+ * Best-effort: an unreadable head simply contributes no companions.
171
+ */
172
+ export async function collectUnmanagedCompanions(repoDir, files, ownedFiles) {
173
+ const heads = [];
174
+ for (const file of files) {
175
+ const base = file.split('/').pop() ?? '';
176
+ if (!/^head(?:_.*)?\.js$/.test(base))
177
+ continue;
178
+ try {
179
+ heads.push({ file, source: await readFile(join(repoDir, file), 'utf8') });
180
+ }
181
+ catch {
182
+ continue;
183
+ }
184
+ }
185
+ if (heads.length === 0)
186
+ return [];
187
+ return resolveUnmanagedCompanions(heads, ownedFiles, async (rel) => {
188
+ try {
189
+ return await readFile(join(repoDir, rel), 'utf8');
190
+ }
191
+ catch {
192
+ return undefined;
193
+ }
194
+ });
195
+ }
196
+ /**
197
+ * Swaps the generic undefined-identifier hint for the ownership hint when
198
+ * one unmanaged companion explains EVERY undefined name in the file. Any
199
+ * other issue in the list is returned untouched.
200
+ */
201
+ export function retargetUnmanagedCompanionHints(issues, companions) {
202
+ if (companions.length === 0)
203
+ return [...issues];
204
+ const undefinedIssues = issues.filter((issue) => issue.message.includes(UNDEFINED_IDENTIFIER_HINT));
205
+ if (undefinedIssues.length === 0)
206
+ return [...issues];
207
+ const companion = explainUndefinedIdentifiers(undefinedIssues.map((issue) => issue.message), companions);
208
+ if (!companion)
209
+ return [...issues];
210
+ const hint = formatUnmanagedCompanionHint(companion);
211
+ return issues.map((issue) => issue.message.includes(UNDEFINED_IDENTIFIER_HINT)
212
+ ? { ...issue, message: issue.message.replace(UNDEFINED_IDENTIFIER_HINT, hint) }
213
+ : issue);
214
+ }
@@ -2,7 +2,8 @@ import type { PatchLintIssue } from '../types/commands/index.js';
2
2
  import type { FireForgeConfig, ProjectLicense } from '../types/config.js';
3
3
  import { type CommentStyle } from './license-headers.js';
4
4
  import { lintPatchedCss } from './patch-lint-css.js';
5
- export * from './patch-lint-reexports.js';
5
+ export * from './patch-lint-cross.js';
6
+ export { buildModifiedFileAdditionsFromDiff, detectNewFilesInDiff } from './patch-lint-diff.js';
6
7
  export { lintPatchedCss };
7
8
  /**
8
9
  * Canonical `[check] file: message` rendering of a patch-lint issue — the
@@ -132,6 +133,17 @@ export type PatchSizeTierDecision = {
132
133
  * defensive-for-tests choice.
133
134
  */
134
135
  export declare function resolvePatchSizeTier(filesAffected: ReadonlyArray<string>, patchTier?: 'branding'): PatchSizeTierDecision;
136
+ /** Line and file-count thresholds governing one patch-size tier. */
137
+ export interface PatchSizeThresholds {
138
+ /** Added-line counts at which notice/warning/error issues fire. */
139
+ lines: {
140
+ notice: number;
141
+ warning: number;
142
+ error: number;
143
+ };
144
+ /** Maximum number of files a patch may touch before an issue fires. */
145
+ maxFiles: number;
146
+ }
135
147
  /**
136
148
  * Read-only view of the size thresholds a tier enforces.
137
149
  * Public-API companion to {@link resolvePatchSizeTier} /
@@ -139,14 +151,7 @@ export declare function resolvePatchSizeTier(filesAffected: ReadonlyArray<string
139
151
  * checks against the SAME numbers `large-patch-lines` /
140
152
  * `large-patch-files` fire on instead of mirroring them.
141
153
  */
142
- export declare function getPatchSizeThresholds(tier: 'general' | 'test' | 'branding'): {
143
- lines: {
144
- notice: number;
145
- warning: number;
146
- error: number;
147
- };
148
- maxFiles: number;
149
- };
154
+ export declare function getPatchSizeThresholds(tier: 'general' | 'test' | 'branding'): PatchSizeThresholds;
150
155
  /**
151
156
  * Checks patch size and emits advisory warnings.
152
157
  *
@@ -8,11 +8,13 @@ import { lintChromeScriptJsDocForFile } from './patch-lint-chrome-jsdoc.js';
8
8
  import { lintPatchedCss } from './patch-lint-css.js';
9
9
  import { detectNewFilesInDiff, extractAddedLinesPerFile } from './patch-lint-diff.js';
10
10
  import { AGGREGATE_PATCH_FILE } from './patch-lint-diff-tag.js';
11
+ import { formatFileTooLargeMessage, resolveFileSizeThresholds } from './patch-lint-file-size.js';
11
12
  import { hasRelativeImport } from './patch-lint-imports.js';
12
13
  import { validateExportJsDoc } from './patch-lint-jsdoc.js';
13
14
  import { lintMozBuildSortedLists } from './patch-lint-mozbuild.js';
14
15
  import { lintObserverTopics } from './patch-lint-observer.js';
15
16
  import { resolvePatchOwnedChromeScripts, resolvePatchOwnedSysMjs, resolvePatchOwnedTestScripts, } from './patch-lint-ownership.js';
17
+ import { invokePatchLintPrettier } from './patch-lint-prettier.js';
16
18
  // ---------------------------------------------------------------------------
17
19
  // Cross-patch lint re-exports
18
20
  // ---------------------------------------------------------------------------
@@ -20,9 +22,10 @@ import { resolvePatchOwnedChromeScripts, resolvePatchOwnedSysMjs, resolvePatchOw
20
22
  // The cross-patch lint infrastructure (queue context builder,
21
23
  // duplicate-creation and forward-import rules, ignore marker) lives in
22
24
  // `patch-lint-cross.ts` so the per-patch and cross-patch rule bodies each
23
- // stay within the per-file line budget. The public surface is re-exported
24
- // from `patch-lint-reexports.ts` so callers keep importing from one module.
25
- export * from './patch-lint-reexports.js';
25
+ // stay within the per-file line budget. Its surface is re-exported here so
26
+ // callers keep importing from one module.
27
+ export * from './patch-lint-cross.js';
28
+ export { buildModifiedFileAdditionsFromDiff, detectNewFilesInDiff } from './patch-lint-diff.js';
26
29
  // The CSS rule bodies live in `patch-lint-css.ts` (same per-file-budget
27
30
  // split as the other rule families); re-export the imported binding so
28
31
  // callers and tests keep importing `lintPatchedCss` from this module.
@@ -31,10 +34,6 @@ export { lintPatchedCss };
31
34
  // Helpers
32
35
  // ---------------------------------------------------------------------------
33
36
  const JS_EXTENSIONS = ['.js', '.mjs', '.jsm'];
34
- const FILE_SIZE_THRESHOLDS = {
35
- general: { notice: 500, warning: 750, error: 900 },
36
- test: { notice: 1200, warning: 1400, error: 1600 },
37
- };
38
37
  /**
39
38
  * Counts the lines of `content` the way `wc -l` reports them: a trailing
40
39
  * newline terminates the last line rather than starting an empty extra one.
@@ -407,31 +406,22 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, pa
407
406
  if (isNew) {
408
407
  const lineCount = countContentLines(content);
409
408
  const isTest = isTestFile(file);
410
- const thresholds = isTest ? FILE_SIZE_THRESHOLDS.test : FILE_SIZE_THRESHOLDS.general;
409
+ const thresholds = resolveFileSizeThresholds(config.patchLint?.fileSizeThresholds, isTest);
411
410
  const label = isTest ? 'Test file' : 'New file';
412
411
  const verb = isTest ? 'splitting' : 'decomposing';
413
- if (lineCount > thresholds.error) {
414
- issues.push({
415
- file,
416
- check: 'file-too-large',
417
- message: `${label} has ${lineCount} lines (hard limit: ${thresholds.error}). Consider ${verb}.`,
418
- severity: 'error',
419
- });
420
- }
421
- else if (lineCount > thresholds.warning) {
412
+ const band = lineCount > thresholds.error
413
+ ? 'error'
414
+ : lineCount > thresholds.warning
415
+ ? 'warning'
416
+ : lineCount > thresholds.notice
417
+ ? 'notice'
418
+ : undefined;
419
+ if (band !== undefined) {
422
420
  issues.push({
423
421
  file,
424
422
  check: 'file-too-large',
425
- message: `${label} has ${lineCount} lines (soft limit: ${thresholds.warning}, hard limit: ${thresholds.error}). Consider ${verb}.`,
426
- severity: 'warning',
427
- });
428
- }
429
- else if (lineCount > thresholds.notice) {
430
- issues.push({
431
- file,
432
- check: 'file-too-large',
433
- message: `${label} has ${lineCount} lines (soft limit: ${thresholds.warning}, hard limit: ${thresholds.error}). Consider ${verb}.`,
434
- severity: 'notice',
423
+ message: formatFileTooLargeMessage({ label, lineCount, thresholds, band, verb }),
424
+ severity: band,
435
425
  });
436
426
  }
437
427
  }
@@ -715,6 +705,16 @@ export async function lintExportedPatch(repoDir, affectedFiles, diffContent, con
715
705
  }
716
706
  }
717
707
  }
708
+ // Prettier over patch-owned .sys.mjs. Deliberately NOT part of the
709
+ // precomputedCheckJs short-circuit: it is a separate tool with its own
710
+ // gate, and a queue-wide program has no equivalent to share. Scoped to
711
+ // the files THIS patch owns, run from engine/ so the engine's own config
712
+ // and ignore file decide (see `patch-lint-prettier.ts`).
713
+ const prettierGate = config.patchLint?.prettier ?? 'off';
714
+ if (prettierGate !== 'off' && patchOwnedFiles.size > 0) {
715
+ const scoped = [...patchOwnedFiles].filter((file) => !options?.checkJsReportScope || options.checkJsReportScope.has(file));
716
+ issues.push(...(await invokePatchLintPrettier(repoDir, dirname(repoDir), scoped.sort(), prettierGate)));
717
+ }
718
718
  // Filter out ignored checks last so every rule still runs (keeps the
719
719
  // implementation uniform) but suppressed rules do not surface. We do not
720
720
  // reclassify severities — an ignored error simply drops, mirroring how
@@ -16,12 +16,12 @@ const PATCH_DIRECTORY_LOCK = '.fireforge-patches.lock';
16
16
  * session lock's progress line so operators read one shape for both locks.
17
17
  */
18
18
  function formatWaitProgressLine(waitedMs, timeoutMs, holder) {
19
- const progress = `${String(Math.round(waitedMs / 1000))}s of up to ${String(Math.round(timeoutMs / 1000))}s`;
19
+ const progress = `${Math.round(waitedMs / 1000)}s of up to ${Math.round(timeoutMs / 1000)}s`;
20
20
  if (holder === undefined) {
21
21
  return `Waiting for the FireForge patch directory lock — ${progress}.`;
22
22
  }
23
23
  const details = holder.metadata.length > 0 ? ` (${holder.metadata.join(', ')})` : '';
24
- return `Waiting for the FireForge patch directory lock held by PID ${String(holder.pid)}${details} — ${progress}.`;
24
+ return `Waiting for the FireForge patch directory lock held by PID ${holder.pid}${details} — ${progress}.`;
25
25
  }
26
26
  /**
27
27
  * Asserts that this process is inside {@link withPatchDirectoryLock} for the
@@ -47,7 +47,7 @@ export async function assertPatchDirectoryLockHeld(patchesDir, context) {
47
47
  const status = await readLockStatus(join(patchesDir, PATCH_DIRECTORY_LOCK));
48
48
  assert(status.held, () => `patch directory lock is held before ${context}`);
49
49
  assert(status.holder === undefined || status.holder.pid === process.pid, () => `patch directory lock is owned by this process before ${context} ` +
50
- `(held by PID ${String(status.holder?.pid)}, we are ${String(process.pid)})`);
50
+ `(held by PID ${status.holder?.pid}, we are ${process.pid})`);
51
51
  }
52
52
  /**
53
53
  * Runs a patch directory mutation while holding an exclusive filesystem lock.
@@ -98,17 +98,18 @@ export async function mutatePatchRowsInManifest(patchesDir, filenames, mutator)
98
98
  if (!(await pathExistsStrict(manifestPath)))
99
99
  return null;
100
100
  const rawManifest = await readJson(manifestPath);
101
+ // `validatePatchesManifest` has already rejected a manifest that is not an
102
+ // object with an array of object rows (it throws the corruption error the
103
+ // caller expects), so the shape re-checks below are FireForge's own
104
+ // invariants, not input validation: failing them means the validator and
105
+ // this walker disagree about the manifest schema, which is a bug here.
101
106
  const beforeManifest = validatePatchesManifest(rawManifest);
102
- if (!isObject(rawManifest) || !isArray(rawManifest['patches'])) {
103
- throw new Error('patches.json must be a JSON object with a patches array');
104
- }
107
+ assert(isObject(rawManifest) && isArray(rawManifest['patches']), 'validated patches.json is a JSON object with a patches array');
105
108
  const filenameSet = new Set(filenames);
106
109
  if (filenameSet.size === 0)
107
110
  return [];
108
- const rawPatches = rawManifest['patches'].map((entry) => {
109
- if (!isObject(entry)) {
110
- throw new Error('patches.json patches entries must be objects');
111
- }
111
+ const rawPatches = rawManifest['patches'].map((entry, index) => {
112
+ assert(isObject(entry), () => `validated patches.json row ${index} is an object`);
112
113
  return { ...entry };
113
114
  });
114
115
  const changedIndexes = [];
@@ -1,14 +1,25 @@
1
1
  /**
2
- * Pure parsing functions for extracting information from patch files. All
3
- * functions are synchronous and operate on string content.
2
+ * Renders a patch order as the zero-padded prefix used in filenames and in
3
+ * every message that names an order.
4
4
  *
5
- * This module is the ONE unified-diff walker in the codebase. Hand-rolled
6
- * `diff --git` walkers diverge in ways that matter: CRLF handling (a
7
- * CRLF-saved patch file silently fails target-file matching and
8
- * `\ No newline` detection on Windows), quoted-path support, and greedy path
9
- * captures that mis-split any path containing ` b/`. New diff-shaped parsing
10
- * must build on {@link parseDiffSections} rather than re-walking lines.
5
+ * Eleven call sites across `patch-policy`, `patch-export`, `export-flow`,
6
+ * `export-placement-policy` and `patch move-files` spelled this as
7
+ * `String(order).padStart(3, '0')`, so the padding width lived in eleven
8
+ * places while the filenames it has to agree with are produced in one.
9
+ * @param order - Numeric patch order.
10
+ * @param width - Prefix width; defaults to the on-disk convention.
11
+ * @returns The zero-padded order, e.g. `007`.
11
12
  */
13
+ export declare function formatPatchOrder(order: number, width?: number): string;
14
+ /**
15
+ * Renders an inclusive order range the way `patchPolicy` diagnostics name it.
16
+ * @param range - Inclusive `from`/`to` order bounds.
17
+ * @returns The range label, e.g. `100-199`.
18
+ */
19
+ export declare function formatOrderRange(range: {
20
+ from: number;
21
+ to: number;
22
+ }): string;
12
23
  /**
13
24
  * Extracts the order number from a patch filename.
14
25
  * Expects format like "001-description.patch"
@@ -11,6 +11,31 @@ import { assert } from '../utils/assert.js';
11
11
  * captures that mis-split any path containing ` b/`. New diff-shaped parsing
12
12
  * must build on {@link parseDiffSections} rather than re-walking lines.
13
13
  */
14
+ /** Width of the zero-padded numeric prefix on a patch filename ("001-…"). */
15
+ const PATCH_ORDER_WIDTH = 3;
16
+ /**
17
+ * Renders a patch order as the zero-padded prefix used in filenames and in
18
+ * every message that names an order.
19
+ *
20
+ * Eleven call sites across `patch-policy`, `patch-export`, `export-flow`,
21
+ * `export-placement-policy` and `patch move-files` spelled this as
22
+ * `String(order).padStart(3, '0')`, so the padding width lived in eleven
23
+ * places while the filenames it has to agree with are produced in one.
24
+ * @param order - Numeric patch order.
25
+ * @param width - Prefix width; defaults to the on-disk convention.
26
+ * @returns The zero-padded order, e.g. `007`.
27
+ */
28
+ export function formatPatchOrder(order, width = PATCH_ORDER_WIDTH) {
29
+ return String(order).padStart(width, '0');
30
+ }
31
+ /**
32
+ * Renders an inclusive order range the way `patchPolicy` diagnostics name it.
33
+ * @param range - Inclusive `from`/`to` order bounds.
34
+ * @returns The range label, e.g. `100-199`.
35
+ */
36
+ export function formatOrderRange(range) {
37
+ return `${formatPatchOrder(range.from)}-${formatPatchOrder(range.to)}`;
38
+ }
14
39
  /**
15
40
  * Extracts the order number from a patch filename.
16
41
  * Expects format like "001-description.patch"
@@ -11,6 +11,7 @@ import { InvalidArgumentError } from '../errors/base.js';
11
11
  import { warn } from '../utils/logger.js';
12
12
  import { PATCH_CATEGORIES } from '../utils/validation.js';
13
13
  import { rewriteStagedDependencyOwners } from './patch-manifest-io.js';
14
+ import { formatOrderRange, formatPatchOrder } from './patch-parse.js';
14
15
  /** Default patch filename contract used when a policy omits `filenamePattern`. */
15
16
  const DEFAULT_PATCH_POLICY_FILENAME_PATTERN = '^(?<order>\\d{3})-(?<category>[a-z][a-z0-9-]*)-(?<slug>[a-z0-9-]+)\\.patch$';
16
17
  function policy(config) {
@@ -19,13 +20,6 @@ function policy(config) {
19
20
  function mutationMode(config) {
20
21
  return policy(config)?.mutationMode ?? 'error';
21
22
  }
22
- function issueSeverity(config) {
23
- return mutationMode(config) === 'warn' ? 'warning' : 'error';
24
- }
25
- /** Returns true when the loaded config includes an opt-in patch policy. */
26
- function hasPatchPolicy(config) {
27
- return policy(config) !== undefined;
28
- }
29
23
  /** Returns valid categories for prompts and CLI validation under the config. */
30
24
  export function getPatchPolicyCategories(config) {
31
25
  const cfg = policy(config);
@@ -42,9 +36,6 @@ export function isCategoryAllowedByConfig(config, category) {
42
36
  return PATCH_CATEGORIES.includes(category);
43
37
  return cfg.ranges.some((range) => range.category === category);
44
38
  }
45
- function rangeLabel(range) {
46
- return `${String(range.from).padStart(3, '0')}-${String(range.to).padStart(3, '0')}`;
47
- }
48
39
  /**
49
40
  * Human-readable label for a category's configured ranges, e.g. `300-399`.
50
41
  * Exported for the forward-import hint, which must render the
@@ -54,7 +45,7 @@ export function categoryRangeLabel(ranges, category) {
54
45
  const matches = ranges.filter((range) => range.category === category);
55
46
  if (matches.length === 0)
56
47
  return '(no configured range)';
57
- return matches.map(rangeLabel).join(', ');
48
+ return matches.map(formatOrderRange).join(', ');
58
49
  }
59
50
  function reservedRangeForOrder(cfg, order) {
60
51
  return cfg.reservedRanges?.find((range) => order >= range.from && order <= range.to) ?? null;
@@ -73,9 +64,6 @@ export function categoryRangeForOrder(cfg, category, order) {
73
64
  function anyRangeForOrder(cfg, order) {
74
65
  return cfg.ranges.find((range) => order >= range.from && order <= range.to) ?? null;
75
66
  }
76
- function compileFilenamePattern(cfg) {
77
- return new RegExp(cfg.filenamePattern ?? DEFAULT_PATCH_POLICY_FILENAME_PATTERN);
78
- }
79
67
  function parseFilenameWithPolicy(cfg, patch, severity) {
80
68
  if (cfg.filenamePattern === undefined) {
81
69
  const defaultMatch = /^(?<order>\d{3})-(?<rest>[a-z0-9-]+)\.patch$/.exec(patch.filename);
@@ -117,7 +105,7 @@ function parseFilenameWithPolicy(cfg, patch, severity) {
117
105
  }
118
106
  return { order: Number.parseInt(orderRaw, 10), category };
119
107
  }
120
- const pattern = compileFilenamePattern(cfg);
108
+ const pattern = new RegExp(cfg.filenamePattern ?? DEFAULT_PATCH_POLICY_FILENAME_PATTERN);
121
109
  const match = pattern.exec(patch.filename);
122
110
  if (!match) {
123
111
  return [
@@ -188,7 +176,7 @@ function evaluatePatchMetadata(cfg, patch, severity) {
188
176
  code: 'reserved-range',
189
177
  filename: patch.filename,
190
178
  severity,
191
- message: `${patch.filename} is in reserved range ${rangeLabel(reserved)}. ` +
179
+ message: `${patch.filename} is in reserved range ${formatOrderRange(reserved)}. ` +
192
180
  'Reserved ranges require an exact patchPolicy.reservedRanges[].allowed filename exception.',
193
181
  });
194
182
  return issues;
@@ -198,7 +186,7 @@ function evaluatePatchMetadata(cfg, patch, severity) {
198
186
  code: 'reserved-documentation',
199
187
  filename: patch.filename,
200
188
  severity,
201
- message: `${patch.filename} is allowlisted for reserved range ${rangeLabel(reserved)}, ` +
189
+ message: `${patch.filename} is allowlisted for reserved range ${formatOrderRange(reserved)}, ` +
202
190
  'but the allowlist entry must include either "adr" or "documentation".',
203
191
  });
204
192
  }
@@ -210,7 +198,7 @@ function evaluatePatchMetadata(cfg, patch, severity) {
210
198
  code: 'reserved-files',
211
199
  filename: patch.filename,
212
200
  severity,
213
- message: `${patch.filename} is allowlisted for reserved range ${rangeLabel(reserved)}, ` +
201
+ message: `${patch.filename} is allowlisted for reserved range ${formatOrderRange(reserved)}, ` +
214
202
  `but touches file(s) outside its reserved allowlist: ${extraFiles.join(', ')}.`,
215
203
  });
216
204
  }
@@ -222,8 +210,8 @@ function evaluatePatchMetadata(cfg, patch, severity) {
222
210
  const owner = anyRangeForOrder(cfg, patch.order);
223
211
  const expected = categoryRangeLabel(cfg.ranges, patch.category);
224
212
  const actual = owner !== null
225
- ? `${String(patch.order).padStart(3, '0')} is configured for ${owner.category} (${rangeLabel(owner)})`
226
- : `${String(patch.order).padStart(3, '0')} is outside all configured ranges`;
213
+ ? `${formatPatchOrder(patch.order)} is configured for ${owner.category} (${formatOrderRange(owner)})`
214
+ : `${formatPatchOrder(patch.order)} is outside all configured ranges`;
227
215
  issues.push({
228
216
  code: 'category-range',
229
217
  filename: patch.filename,
@@ -262,10 +250,10 @@ function evaluateGaps(cfg, patches, severity) {
262
250
  if (missing.length > 0) {
263
251
  issues.push({
264
252
  code: 'numeric-gap',
265
- filename: `${range.category}:${rangeLabel(range)}`,
253
+ filename: `${range.category}:${formatOrderRange(range)}`,
266
254
  severity,
267
- message: `${range.category} range ${rangeLabel(range)} has numeric gap(s): ` +
268
- missing.map((order) => String(order).padStart(3, '0')).join(', ') +
255
+ message: `${range.category} range ${formatOrderRange(range)} has numeric gap(s): ` +
256
+ missing.map((order) => formatPatchOrder(order)).join(', ') +
269
257
  '. patchPolicy.allowGaps is false.',
270
258
  });
271
259
  }
@@ -286,9 +274,9 @@ function evaluateOrderCollisions(patches, severity) {
286
274
  const filenames = matches.map((patch) => patch.filename).sort((a, b) => a.localeCompare(b));
287
275
  issues.push({
288
276
  code: 'order-collision',
289
- filename: String(order).padStart(3, '0'),
277
+ filename: formatPatchOrder(order),
290
278
  severity,
291
- message: `patchPolicy requires unique numeric orders; order ${String(order).padStart(3, '0')} ` +
279
+ message: `patchPolicy requires unique numeric orders; order ${formatPatchOrder(order)} ` +
292
280
  `is used by: ${filenames.join(', ')}.`,
293
281
  });
294
282
  }
@@ -299,7 +287,7 @@ export function evaluatePatchPolicy(config, manifest) {
299
287
  const cfg = policy(config);
300
288
  if (!cfg)
301
289
  return [];
302
- const severity = issueSeverity(config);
290
+ const severity = mutationMode(config) === 'warn' ? 'warning' : 'error';
303
291
  const issues = manifest.patches.flatMap((patch) => evaluatePatchMetadata(cfg, patch, severity));
304
292
  issues.push(...evaluateOrderCollisions(manifest.patches, severity));
305
293
  issues.push(...evaluateGaps(cfg, manifest.patches, severity));
@@ -335,7 +323,7 @@ export function applyRenameMapToManifest(manifest, renameMap) {
335
323
  }
336
324
  /** Enforces patch policy according to the configured mutation mode. */
337
325
  export function enforcePatchPolicy(input) {
338
- if (!hasPatchPolicy(input.config))
326
+ if (policy(input.config) === undefined)
339
327
  return;
340
328
  const issues = evaluatePatchPolicy(input.config, input.manifest);
341
329
  if (issues.length === 0)
@@ -1,8 +1,6 @@
1
1
  import type { PatchMetadata } from '../types/commands/index.js';
2
- import type { FirefoxConfig, FirefoxProduct } from '../types/config.js';
2
+ import type { FirefoxConfig } from '../types/config.js';
3
3
  /** Metadata fields stamped on new or refreshed patch entries. */
4
4
  export declare function buildPatchSourceMetadata(firefox: Pick<FirefoxConfig, 'product' | 'version'>): Pick<PatchMetadata, 'sourceEsrVersion' | 'sourceProduct' | 'sourceVersion'>;
5
5
  /** Backward-compatible source version reader for legacy manifests. */
6
6
  export declare function getPatchSourceVersion(patch: Pick<PatchMetadata, 'sourceEsrVersion' | 'sourceVersion'>): string;
7
- /** Backward-compatible source product reader for legacy manifests. */
8
- export declare function getPatchSourceProduct(patch: Pick<PatchMetadata, 'sourceProduct'>): FirefoxProduct | undefined;
@@ -10,7 +10,3 @@ export function buildPatchSourceMetadata(firefox) {
10
10
  export function getPatchSourceVersion(patch) {
11
11
  return patch.sourceVersion ?? patch.sourceEsrVersion;
12
12
  }
13
- /** Backward-compatible source product reader for legacy manifests. */
14
- export function getPatchSourceProduct(patch) {
15
- return patch.sourceProduct;
16
- }