@hominis/fireforge 0.39.0 → 0.41.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 (279) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +35 -5
  3. package/dist/bin/fireforge.js +25 -4
  4. package/dist/build-info.json +9 -0
  5. package/dist/src/cli.js +27 -3
  6. package/dist/src/commands/bootstrap.js +5 -1
  7. package/dist/src/commands/discard.js +2 -22
  8. package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
  9. package/dist/src/commands/doctor-check-core.d.ts +20 -0
  10. package/dist/src/commands/doctor-check-core.js +28 -0
  11. package/dist/src/commands/doctor-external-toolchains.js +2 -0
  12. package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
  13. package/dist/src/commands/doctor-furnace.js +13 -11
  14. package/dist/src/commands/doctor-orphaned-harness.js +2 -1
  15. package/dist/src/commands/doctor.js +2 -2
  16. package/dist/src/commands/export-all.js +9 -1
  17. package/dist/src/commands/export-flow.js +27 -9
  18. package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
  19. package/dist/src/commands/export-placement-conflicts.js +74 -0
  20. package/dist/src/commands/export-placement-gate.js +1 -1
  21. package/dist/src/commands/export-shared.d.ts +8 -1
  22. package/dist/src/commands/export-shared.js +30 -10
  23. package/dist/src/commands/export.js +2 -2
  24. package/dist/src/commands/furnace/apply.js +4 -3
  25. package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
  26. package/dist/src/commands/furnace/chrome-doc.js +13 -2
  27. package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
  28. package/dist/src/commands/furnace/create-browser-test.js +5 -0
  29. package/dist/src/commands/furnace/create.js +2 -2
  30. package/dist/src/commands/furnace/diff.js +2 -1
  31. package/dist/src/commands/furnace/init.js +4 -6
  32. package/dist/src/commands/furnace/override.js +2 -2
  33. package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
  34. package/dist/src/commands/furnace/remove-mutations.js +171 -0
  35. package/dist/src/commands/furnace/remove.js +21 -128
  36. package/dist/src/commands/furnace/rename-browser-test.js +54 -5
  37. package/dist/src/commands/furnace/scan.js +27 -36
  38. package/dist/src/commands/furnace/status.js +30 -28
  39. package/dist/src/commands/furnace/validate.js +25 -18
  40. package/dist/src/commands/import.js +6 -20
  41. package/dist/src/commands/lint-per-patch.js +73 -51
  42. package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
  43. package/dist/src/commands/lint-per-run-checkjs.js +102 -0
  44. package/dist/src/commands/lint-report.d.ts +21 -0
  45. package/dist/src/commands/lint-report.js +62 -0
  46. package/dist/src/commands/lint.js +26 -22
  47. package/dist/src/commands/manifest.js +3 -1
  48. package/dist/src/commands/package.js +9 -16
  49. package/dist/src/commands/patch/compact.js +5 -5
  50. package/dist/src/commands/patch/delete.js +5 -5
  51. package/dist/src/commands/patch/lint-ignore.js +3 -2
  52. package/dist/src/commands/patch/move-files-create.js +22 -4
  53. package/dist/src/commands/patch/move-files-into.js +14 -10
  54. package/dist/src/commands/patch/move-files.js +6 -5
  55. package/dist/src/commands/patch/rename.d.ts +2 -2
  56. package/dist/src/commands/patch/rename.js +152 -51
  57. package/dist/src/commands/patch/reorder.d.ts +9 -0
  58. package/dist/src/commands/patch/reorder.js +14 -10
  59. package/dist/src/commands/patch/split-plan.d.ts +13 -6
  60. package/dist/src/commands/patch/split-plan.js +22 -12
  61. package/dist/src/commands/patch/split.d.ts +1 -1
  62. package/dist/src/commands/patch/split.js +23 -9
  63. package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
  64. package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
  65. package/dist/src/commands/patch/staged-dependency.js +112 -14
  66. package/dist/src/commands/re-export-adjacent.d.ts +34 -0
  67. package/dist/src/commands/re-export-adjacent.js +105 -0
  68. package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
  69. package/dist/src/commands/re-export-bulk-scan.js +142 -2
  70. package/dist/src/commands/re-export-drift.d.ts +74 -0
  71. package/dist/src/commands/re-export-drift.js +234 -0
  72. package/dist/src/commands/re-export-files.js +5 -3
  73. package/dist/src/commands/re-export-lint.d.ts +97 -0
  74. package/dist/src/commands/re-export-lint.js +210 -0
  75. package/dist/src/commands/re-export-options.js +11 -0
  76. package/dist/src/commands/re-export-register.d.ts +18 -0
  77. package/dist/src/commands/re-export-register.js +61 -0
  78. package/dist/src/commands/re-export-scan.js +13 -7
  79. package/dist/src/commands/re-export-single.d.ts +20 -0
  80. package/dist/src/commands/re-export-single.js +261 -0
  81. package/dist/src/commands/re-export.d.ts +0 -4
  82. package/dist/src/commands/re-export.js +123 -289
  83. package/dist/src/commands/rebase/abort.js +13 -4
  84. package/dist/src/commands/rebase/continue.js +11 -4
  85. package/dist/src/commands/rebase/index.js +19 -29
  86. package/dist/src/commands/rebase/patch-loop.js +12 -0
  87. package/dist/src/commands/reset.js +4 -23
  88. package/dist/src/commands/resolve.js +2 -9
  89. package/dist/src/commands/run.js +29 -23
  90. package/dist/src/commands/source.js +3 -9
  91. package/dist/src/commands/status-check.d.ts +48 -0
  92. package/dist/src/commands/status-check.js +106 -0
  93. package/dist/src/commands/status-json.d.ts +31 -0
  94. package/dist/src/commands/status-json.js +76 -0
  95. package/dist/src/commands/status-output.d.ts +1 -0
  96. package/dist/src/commands/status-output.js +10 -1
  97. package/dist/src/commands/status-ownership.d.ts +52 -0
  98. package/dist/src/commands/status-ownership.js +57 -0
  99. package/dist/src/commands/status.js +190 -170
  100. package/dist/src/commands/test-build-phase.d.ts +19 -0
  101. package/dist/src/commands/test-build-phase.js +168 -0
  102. package/dist/src/commands/test-diagnose.js +30 -2
  103. package/dist/src/commands/test-modes.d.ts +9 -1
  104. package/dist/src/commands/test-modes.js +46 -13
  105. package/dist/src/commands/test-register.js +59 -27
  106. package/dist/src/commands/test-run.d.ts +29 -3
  107. package/dist/src/commands/test-run.js +59 -7
  108. package/dist/src/commands/test-verdict.d.ts +32 -0
  109. package/dist/src/commands/test-verdict.js +48 -0
  110. package/dist/src/commands/test.d.ts +9 -0
  111. package/dist/src/commands/test.js +100 -123
  112. package/dist/src/commands/token-coverage.js +4 -9
  113. package/dist/src/commands/token.js +4 -1
  114. package/dist/src/commands/tree.d.ts +19 -0
  115. package/dist/src/commands/tree.js +320 -0
  116. package/dist/src/commands/verify.js +2 -2
  117. package/dist/src/commands/watch.js +22 -21
  118. package/dist/src/commands/wire.js +1 -1
  119. package/dist/src/core/ast-utils.d.ts +11 -0
  120. package/dist/src/core/ast-utils.js +11 -0
  121. package/dist/src/core/build-audit-platform.js +6 -1
  122. package/dist/src/core/build-audit-registration.js +2 -0
  123. package/dist/src/core/build-audit-resolve.d.ts +13 -0
  124. package/dist/src/core/build-audit-resolve.js +34 -13
  125. package/dist/src/core/build-audit.js +4 -20
  126. package/dist/src/core/build-baseline-types.d.ts +29 -8
  127. package/dist/src/core/build-baseline-types.js +2 -1
  128. package/dist/src/core/build-baseline.d.ts +10 -2
  129. package/dist/src/core/build-baseline.js +39 -8
  130. package/dist/src/core/build-prepare.d.ts +4 -0
  131. package/dist/src/core/build-prepare.js +7 -1
  132. package/dist/src/core/config-paths.d.ts +1 -1
  133. package/dist/src/core/config-paths.js +2 -0
  134. package/dist/src/core/config-validate-test-toolchains.js +3 -2
  135. package/dist/src/core/config-validate.js +45 -30
  136. package/dist/src/core/config.js +16 -3
  137. package/dist/src/core/coverage-extend.d.ts +89 -0
  138. package/dist/src/core/coverage-extend.js +188 -0
  139. package/dist/src/core/destructive.js +3 -1
  140. package/dist/src/core/discard-baseline.js +18 -4
  141. package/dist/src/core/engine-precondition.d.ts +35 -0
  142. package/dist/src/core/engine-precondition.js +50 -0
  143. package/dist/src/core/engine-session-lock.d.ts +38 -1
  144. package/dist/src/core/engine-session-lock.js +78 -4
  145. package/dist/src/core/file-lock.d.ts +7 -0
  146. package/dist/src/core/file-lock.js +20 -30
  147. package/dist/src/core/firefox.js +5 -2
  148. package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
  149. package/dist/src/core/furnace-apply-helpers.js +12 -3
  150. package/dist/src/core/furnace-apply-output.js +9 -0
  151. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
  152. package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
  153. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
  154. package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
  155. package/dist/src/core/furnace-apply.d.ts +16 -4
  156. package/dist/src/core/furnace-apply.js +39 -43
  157. package/dist/src/core/furnace-config.js +2 -0
  158. package/dist/src/core/furnace-css-fragments.js +2 -0
  159. package/dist/src/core/furnace-jsconfig.js +14 -7
  160. package/dist/src/core/furnace-registration-ast.js +8 -8
  161. package/dist/src/core/furnace-registration-remove.js +6 -3
  162. package/dist/src/core/furnace-registration.js +9 -12
  163. package/dist/src/core/furnace-rollback.js +1 -1
  164. package/dist/src/core/furnace-scanner.js +2 -1
  165. package/dist/src/core/furnace-stale-export.js +2 -1
  166. package/dist/src/core/furnace-state-persist.d.ts +4 -6
  167. package/dist/src/core/furnace-validate-compatibility.js +4 -7
  168. package/dist/src/core/furnace-validate-helpers.js +33 -1
  169. package/dist/src/core/furnace-validate-registration.js +16 -0
  170. package/dist/src/core/furnace-validate.js +4 -2
  171. package/dist/src/core/git-diff.js +37 -2
  172. package/dist/src/core/lint-cache.d.ts +15 -5
  173. package/dist/src/core/lint-cache.js +49 -14
  174. package/dist/src/core/mach-build-artifacts.d.ts +63 -1
  175. package/dist/src/core/mach-build-artifacts.js +108 -2
  176. package/dist/src/core/mach-resource-shim.js +1 -0
  177. package/dist/src/core/marionette-port.d.ts +16 -0
  178. package/dist/src/core/marionette-port.js +93 -8
  179. package/dist/src/core/marionette-preflight.js +6 -5
  180. package/dist/src/core/patch-apply.d.ts +20 -13
  181. package/dist/src/core/patch-apply.js +32 -33
  182. package/dist/src/core/patch-export.d.ts +2 -2
  183. package/dist/src/core/patch-export.js +4 -6
  184. package/dist/src/core/patch-lint-checkjs.d.ts +42 -2
  185. package/dist/src/core/patch-lint-checkjs.js +132 -8
  186. package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
  187. package/dist/src/core/patch-lint-creators.d.ts +30 -0
  188. package/dist/src/core/patch-lint-creators.js +45 -0
  189. package/dist/src/core/patch-lint-cross.d.ts +1 -17
  190. package/dist/src/core/patch-lint-cross.js +16 -32
  191. package/dist/src/core/patch-lint-css.js +64 -16
  192. package/dist/src/core/patch-lint-diff.d.ts +8 -0
  193. package/dist/src/core/patch-lint-diff.js +31 -0
  194. package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
  195. package/dist/src/core/patch-lint-jsdoc.js +15 -5
  196. package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
  197. package/dist/src/core/patch-lint-module-registration.js +88 -0
  198. package/dist/src/core/patch-lint-mozbuild.js +99 -11
  199. package/dist/src/core/patch-lint-ownership.d.ts +21 -0
  200. package/dist/src/core/patch-lint-ownership.js +30 -0
  201. package/dist/src/core/patch-lint-reexports.d.ts +2 -1
  202. package/dist/src/core/patch-lint-reexports.js +1 -0
  203. package/dist/src/core/patch-lint.d.ts +31 -0
  204. package/dist/src/core/patch-lint.js +114 -8
  205. package/dist/src/core/patch-lock.d.ts +12 -1
  206. package/dist/src/core/patch-lock.js +40 -4
  207. package/dist/src/core/patch-manifest-consistency.js +15 -3
  208. package/dist/src/core/patch-manifest.d.ts +1 -1
  209. package/dist/src/core/patch-parse.d.ts +8 -0
  210. package/dist/src/core/patch-parse.js +10 -0
  211. package/dist/src/core/patch-policy.d.ts +7 -0
  212. package/dist/src/core/patch-policy.js +4 -1
  213. package/dist/src/core/patch-transform.d.ts +10 -0
  214. package/dist/src/core/patch-transform.js +13 -2
  215. package/dist/src/core/rebase-session.d.ts +44 -2
  216. package/dist/src/core/rebase-session.js +114 -17
  217. package/dist/src/core/register-browser-content.js +2 -1
  218. package/dist/src/core/register-shared-css.js +2 -1
  219. package/dist/src/core/smoke-patterns.d.ts +24 -16
  220. package/dist/src/core/smoke-patterns.js +26 -14
  221. package/dist/src/core/status-binary.d.ts +55 -0
  222. package/dist/src/core/status-binary.js +124 -0
  223. package/dist/src/core/status-classify.d.ts +22 -1
  224. package/dist/src/core/status-classify.js +112 -70
  225. package/dist/src/core/test-harness-crash.d.ts +32 -1
  226. package/dist/src/core/test-harness-crash.js +59 -4
  227. package/dist/src/core/test-path-scope.js +2 -0
  228. package/dist/src/core/test-stale-check.js +1 -20
  229. package/dist/src/core/token-category.d.ts +40 -0
  230. package/dist/src/core/token-category.js +264 -0
  231. package/dist/src/core/token-manager.d.ts +7 -0
  232. package/dist/src/core/token-manager.js +48 -196
  233. package/dist/src/core/toolchain-preflight.js +24 -26
  234. package/dist/src/core/tree-cow.d.ts +18 -0
  235. package/dist/src/core/tree-cow.js +71 -0
  236. package/dist/src/core/tree-guard.d.ts +35 -0
  237. package/dist/src/core/tree-guard.js +179 -0
  238. package/dist/src/core/tree-store.d.ts +129 -0
  239. package/dist/src/core/tree-store.js +452 -0
  240. package/dist/src/core/typecheck-shim.d.ts +14 -0
  241. package/dist/src/core/typecheck-shim.js +41 -2
  242. package/dist/src/core/typecheck.js +2 -1
  243. package/dist/src/core/wire-destroy.js +14 -3
  244. package/dist/src/core/wire-init.js +37 -40
  245. package/dist/src/core/wire-subscript.js +11 -3
  246. package/dist/src/core/wire-utils.js +4 -4
  247. package/dist/src/core/xpcshell-appdir.js +3 -0
  248. package/dist/src/core/xpcshell-profile-dir.d.ts +12 -0
  249. package/dist/src/core/xpcshell-profile-dir.js +45 -0
  250. package/dist/src/errors/base.d.ts +13 -0
  251. package/dist/src/errors/base.js +13 -0
  252. package/dist/src/errors/git.d.ts +8 -0
  253. package/dist/src/errors/git.js +17 -0
  254. package/dist/src/errors/rebase.d.ts +13 -0
  255. package/dist/src/errors/rebase.js +17 -0
  256. package/dist/src/index.d.ts +2 -0
  257. package/dist/src/index.js +1 -0
  258. package/dist/src/types/commands/options.d.ts +119 -3
  259. package/dist/src/types/commands/patches.d.ts +8 -0
  260. package/dist/src/types/config.d.ts +19 -0
  261. package/dist/src/types/furnace.d.ts +7 -0
  262. package/dist/src/utils/build-info.d.ts +31 -0
  263. package/dist/src/utils/build-info.js +98 -0
  264. package/dist/src/utils/concurrency.d.ts +8 -0
  265. package/dist/src/utils/concurrency.js +21 -0
  266. package/dist/src/utils/errors.d.ts +39 -0
  267. package/dist/src/utils/errors.js +55 -0
  268. package/dist/src/utils/fs.js +5 -12
  269. package/dist/src/utils/logger.d.ts +14 -2
  270. package/dist/src/utils/logger.js +43 -13
  271. package/dist/src/utils/options.d.ts +21 -4
  272. package/dist/src/utils/options.js +26 -6
  273. package/dist/src/utils/package-root.js +7 -4
  274. package/dist/src/utils/process-group.js +13 -2
  275. package/dist/src/utils/stdio-drain.d.ts +28 -0
  276. package/dist/src/utils/stdio-drain.js +54 -0
  277. package/dist/src/utils/validation.d.ts +33 -2
  278. package/dist/src/utils/validation.js +57 -2
  279. package/package.json +13 -13
@@ -20,7 +20,11 @@ export interface BuildArtifactCheck {
20
20
  }
21
21
  /**
22
22
  * Checks if build artifacts exist in the engine directory.
23
- * Looks for obj-* directories with a dist subdirectory.
23
+ * Looks for obj-* directories with a dist subdirectory. Detection is
24
+ * deliberately symlink-agnostic — building/running against a symlinked
25
+ * objdir in place is the user's own arrangement; the clone path, where a
26
+ * symlink would route writes into the original build, refuses separately
27
+ * (`assertCloneSafeObjdir` in tree-store.ts).
24
28
  * @param engineDir - Path to the engine directory
25
29
  * @returns Build artifact check result
26
30
  */
@@ -112,3 +116,61 @@ export interface MozinfoRewriteResult {
112
116
  * @returns Result object; callers inspect `rewritten` and surface `reason`.
113
117
  */
114
118
  export declare function attemptMozinfoRewrite(engineDir: string, objDir: string): Promise<MozinfoRewriteResult>;
119
+ /** Per-command wording for {@link assertBuildArtifacts}. */
120
+ export interface BuildArtifactPreflightOptions {
121
+ /** Label passed to {@link buildArtifactMismatchMessage} (e.g. `'Tests'`). */
122
+ label: string;
123
+ /** Sentence naming what needs a build, e.g. `'Tests require a completed build.'` */
124
+ requirement: string;
125
+ /** Follow-up telling the operator what to run, shown after `requirement`. */
126
+ remediation: string;
127
+ /**
128
+ * Whether a missing/incomplete build is fatal. `fireforge build` runs
129
+ * legitimately with no artifacts and only enforces this under `--ui`, so it
130
+ * opts in rather than out.
131
+ */
132
+ requireExisting?: boolean;
133
+ }
134
+ /**
135
+ * Shared build-artifact preflight: rejects ambiguous multi-objdir checkouts,
136
+ * artifacts whose metadata points at another tree, and (when
137
+ * `requireExisting`) missing or incomplete builds — each with the actionable
138
+ * message for that case.
139
+ *
140
+ * Takes an already-probed {@link BuildArtifactCheck} rather than probing
141
+ * itself, so it stays a pure validator: callers keep their own
142
+ * `hasBuildArtifacts` call (which their suites already stub) and this function
143
+ * needs no test double of its own.
144
+ *
145
+ * Five commands carried a character-identical copy of this ladder, including
146
+ * the same `ambiguous && objDirs && objDirs.length > 0` expression that
147
+ * `BuildArtifactCheck`'s boolean-plus-optionals shape forces at every reader.
148
+ * The shape itself is unchanged — it has several producers and a documented
149
+ * refusal contract (see the containment predicates below) — but it is now
150
+ * destructured in exactly one place.
151
+ */
152
+ export declare function assertBuildArtifacts(engineDir: string, buildCheck: BuildArtifactCheck, options: BuildArtifactPreflightOptions): void;
153
+ /**
154
+ * Post-`mach configure` relocation check for a cloned objdir: confirms
155
+ * configure actually regenerated `<engineDir>/<objDir>` and that none of
156
+ * the four configure-generated root files (`config.status`, `backend.mk`,
157
+ * `Makefile`, `config/autoconf.mk`) still names `forbiddenDir` (the
158
+ * primary engine a relocated clone must never consult). Exit code 0 alone
159
+ * proves neither — a stray MOZCONFIG/MOZ_OBJDIR can steer configure at a
160
+ * different objdir entirely. Nested Makefiles are products of the verified
161
+ * `config.status` and are not scanned; `.deps` files are build products a
162
+ * configure cannot regenerate and are explicitly out of scope — any
163
+ * primary-path strings they retain are read-only staleness corrected by
164
+ * the first in-tree rebuild. Pure checker: returns a human-readable
165
+ * violation, or `undefined` when clean; callers own the error type and
166
+ * remediation copy. Unreadable metadata is itself a violation (fail
167
+ * closed).
168
+ */
169
+ export declare function findObjdirRelocationViolation(args: {
170
+ /** The relocated (tree) engine directory configure ran in. */
171
+ engineDir: string;
172
+ /** The obj-* directory name configure was expected to target. */
173
+ objDir: string;
174
+ /** The primary engine dir whose absolute path must not appear. */
175
+ forbiddenDir: string;
176
+ }): Promise<string | undefined>;
@@ -1,8 +1,10 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { readdir } from 'node:fs/promises';
3
3
  import { join, relative, resolve, sep } from 'node:path';
4
+ import { GeneralError } from '../errors/base.js';
5
+ import { AmbiguousBuildArtifactsError } from '../errors/build.js';
4
6
  import { toError } from '../utils/errors.js';
5
- import { pathExists, readJson, writeJson } from '../utils/fs.js';
7
+ import { pathExists, readJson, readText, writeJson } from '../utils/fs.js';
6
8
  import { verbose } from '../utils/logger.js';
7
9
  import { getPlatform } from '../utils/platform.js';
8
10
  import { isObject, isString } from '../utils/validation.js';
@@ -33,7 +35,11 @@ function validateBuildMozinfo(data) {
33
35
  }
34
36
  /**
35
37
  * Checks if build artifacts exist in the engine directory.
36
- * Looks for obj-* directories with a dist subdirectory.
38
+ * Looks for obj-* directories with a dist subdirectory. Detection is
39
+ * deliberately symlink-agnostic — building/running against a symlinked
40
+ * objdir in place is the user's own arrangement; the clone path, where a
41
+ * symlink would route writes into the original build, refuses separately
42
+ * (`assertCloneSafeObjdir` in tree-store.ts).
37
43
  * @param engineDir - Path to the engine directory
38
44
  * @returns Build artifact check result
39
45
  */
@@ -135,6 +141,8 @@ export async function hasRunnableBundle(engineDir, binaryName, objDir) {
135
141
  entries = await readdir(distDir, { withFileTypes: true });
136
142
  }
137
143
  catch {
144
+ // No readable dist directory means no runnable bundle, which is exactly
145
+ // what the caller needs to know.
138
146
  return { runnable: false };
139
147
  }
140
148
  for (const entry of entries) {
@@ -285,3 +293,101 @@ export async function attemptMozinfoRewrite(engineDir, objDir) {
285
293
  ...(newMozconfig ? { newMozconfig } : {}),
286
294
  };
287
295
  }
296
+ /**
297
+ * Shared build-artifact preflight: rejects ambiguous multi-objdir checkouts,
298
+ * artifacts whose metadata points at another tree, and (when
299
+ * `requireExisting`) missing or incomplete builds — each with the actionable
300
+ * message for that case.
301
+ *
302
+ * Takes an already-probed {@link BuildArtifactCheck} rather than probing
303
+ * itself, so it stays a pure validator: callers keep their own
304
+ * `hasBuildArtifacts` call (which their suites already stub) and this function
305
+ * needs no test double of its own.
306
+ *
307
+ * Five commands carried a character-identical copy of this ladder, including
308
+ * the same `ambiguous && objDirs && objDirs.length > 0` expression that
309
+ * `BuildArtifactCheck`'s boolean-plus-optionals shape forces at every reader.
310
+ * The shape itself is unchanged — it has several producers and a documented
311
+ * refusal contract (see the containment predicates below) — but it is now
312
+ * destructured in exactly one place.
313
+ */
314
+ export function assertBuildArtifacts(engineDir, buildCheck, options) {
315
+ if (buildCheck.ambiguous && buildCheck.objDirs && buildCheck.objDirs.length > 0) {
316
+ throw new AmbiguousBuildArtifactsError(buildCheck.objDirs);
317
+ }
318
+ const mismatchMessage = buildArtifactMismatchMessage(engineDir, buildCheck, options.label);
319
+ if (mismatchMessage) {
320
+ throw new GeneralError(mismatchMessage);
321
+ }
322
+ if (options.requireExisting && !buildCheck.exists) {
323
+ const detail = buildCheck.objDir
324
+ ? `Build artifacts incomplete in ${buildCheck.objDir}/`
325
+ : 'No build artifacts found (obj-*/ directory missing)';
326
+ throw new GeneralError(`${options.requirement} ${detail}\n\n${options.remediation}`);
327
+ }
328
+ }
329
+ /**
330
+ * Post-`mach configure` relocation check for a cloned objdir: confirms
331
+ * configure actually regenerated `<engineDir>/<objDir>` and that none of
332
+ * the four configure-generated root files (`config.status`, `backend.mk`,
333
+ * `Makefile`, `config/autoconf.mk`) still names `forbiddenDir` (the
334
+ * primary engine a relocated clone must never consult). Exit code 0 alone
335
+ * proves neither — a stray MOZCONFIG/MOZ_OBJDIR can steer configure at a
336
+ * different objdir entirely. Nested Makefiles are products of the verified
337
+ * `config.status` and are not scanned; `.deps` files are build products a
338
+ * configure cannot regenerate and are explicitly out of scope — any
339
+ * primary-path strings they retain are read-only staleness corrected by
340
+ * the first in-tree rebuild. Pure checker: returns a human-readable
341
+ * violation, or `undefined` when clean; callers own the error type and
342
+ * remediation copy. Unreadable metadata is itself a violation (fail
343
+ * closed).
344
+ */
345
+ export async function findObjdirRelocationViolation(args) {
346
+ const { engineDir, objDir, forbiddenDir } = args;
347
+ const objDirPath = join(engineDir, objDir);
348
+ if (!(await pathExists(join(objDirPath, 'config.status')))) {
349
+ return (`${objDir}/config.status was not written — mach configure may have targeted a ` +
350
+ 'different objdir (check MOZCONFIG / MOZ_OBJDIR)');
351
+ }
352
+ let mozinfo;
353
+ try {
354
+ mozinfo = validateBuildMozinfo(await readJson(join(objDirPath, 'mozinfo.json')));
355
+ }
356
+ catch (error) {
357
+ return `${objDir}/mozinfo.json could not be read after configure: ${toError(error).message}`;
358
+ }
359
+ const expectedSrcDir = resolve(engineDir);
360
+ const expectedObjDir = resolve(objDirPath);
361
+ const actualSrcDir = mozinfo.topsrcdir ? resolve(mozinfo.topsrcdir) : undefined;
362
+ const actualObjDir = mozinfo.topobjdir ? resolve(mozinfo.topobjdir) : undefined;
363
+ if (actualSrcDir !== expectedSrcDir) {
364
+ return `${objDir}/mozinfo.json topsrcdir resolves to ${actualSrcDir ?? '(absent)'}, expected ${expectedSrcDir}`;
365
+ }
366
+ if (actualObjDir !== expectedObjDir) {
367
+ return `${objDir}/mozinfo.json topobjdir resolves to ${actualObjDir ?? '(absent)'}, expected ${expectedObjDir}`;
368
+ }
369
+ // The canonical no-primary-paths assertion (mirrors the opt-in real-mach
370
+ // proof in scripts/run-full-firefox-integration.mjs): substring search on
371
+ // the resolved primary engine dir is collision-safe against the tree's own
372
+ // paths — `<primary>/.fireforge/trees/<name>/engine` never contains the
373
+ // substring `<primary>/engine`. Scans exactly the configure-generated
374
+ // root files; `config.status` is mandatory (checked above), the rest are
375
+ // optional-if-absent because not every configure backend writes them.
376
+ const forbidden = resolve(forbiddenDir);
377
+ for (const name of ['config.status', 'backend.mk', 'Makefile', 'config/autoconf.mk']) {
378
+ const filePath = join(objDirPath, ...name.split('/'));
379
+ if (name !== 'config.status' && !(await pathExists(filePath)))
380
+ continue;
381
+ let content;
382
+ try {
383
+ content = await readText(filePath);
384
+ }
385
+ catch (error) {
386
+ return `${objDir}/${name} could not be read after configure: ${toError(error).message}`;
387
+ }
388
+ if (content.includes(forbidden)) {
389
+ return `${objDir}/${name} still contains the primary engine path ${forbidden}`;
390
+ }
391
+ }
392
+ return undefined;
393
+ }
@@ -461,6 +461,7 @@ async function listSubdirs(dir) {
461
461
  return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
462
462
  }
463
463
  catch {
464
+ // An unreadable directory contributes no subdirectories to the shim scan.
464
465
  return [];
465
466
  }
466
467
  }
@@ -27,6 +27,22 @@ export interface MarionettePortProbeResult {
27
27
  /** Details about the holder, when the probe recovered them. */
28
28
  holder?: MarionettePortHolder;
29
29
  }
30
+ /** A Firefox-family process launched from this project's built bundle. */
31
+ export interface RunningBundleProcess {
32
+ pid: number;
33
+ commandLine: string;
34
+ }
35
+ /** Pure parser kept exported so platform output handling is regression-testable. */
36
+ export declare function parseProcessList(stdout: string, launchableBinary: string): RunningBundleProcess[];
37
+ /**
38
+ * Refuses a browser-harness launch when this objdir's app is already alive,
39
+ * or terminates the parent when the operator explicitly opted into stale
40
+ * browser cleanup. This closes the no-listening-port variant of the stale
41
+ * Marionette process failure.
42
+ */
43
+ export declare function ensureLaunchableBrowserNotRunning(launchableBinary: string, options?: {
44
+ killStaleBrowser?: boolean;
45
+ }): Promise<void>;
30
46
  /**
31
47
  * Probes whether the Marionette port is currently bound by a
32
48
  * listener. The probe is best-effort: missing tooling returns
@@ -45,6 +45,88 @@ const BROWSER_BASENAMES = new Set([
45
45
  'mybrowser',
46
46
  'thunderbird',
47
47
  ]);
48
+ /**
49
+ * Finds browser processes whose command line names the exact launchable
50
+ * binary from this project's objdir. Unlike the Marionette-port probe, this
51
+ * also catches a wedged browser that survived a harness timeout but no longer
52
+ * owns the control port. Best-effort: unavailable process-list tooling yields
53
+ * an empty result rather than blocking tests.
54
+ */
55
+ async function findRunningBundleProcesses(launchableBinary) {
56
+ try {
57
+ if (process.platform === 'win32') {
58
+ const escapedPath = launchableBinary.replaceAll("'", "''");
59
+ const script = `Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -eq '${escapedPath}' } | ` +
60
+ 'ForEach-Object { Write-Output ("$($_.ProcessId) $($_.CommandLine)") }';
61
+ const result = await exec('powershell.exe', [
62
+ '-NoProfile',
63
+ '-NonInteractive',
64
+ '-Command',
65
+ script,
66
+ ]);
67
+ return parseProcessList(result.stdout, launchableBinary);
68
+ }
69
+ const result = await exec('ps', ['-axo', 'pid=,args=']);
70
+ return parseProcessList(result.stdout, launchableBinary);
71
+ }
72
+ catch {
73
+ return [];
74
+ }
75
+ }
76
+ /** Pure parser kept exported so platform output handling is regression-testable. */
77
+ export function parseProcessList(stdout, launchableBinary) {
78
+ const matches = [];
79
+ for (const line of stdout.split(/\r?\n/)) {
80
+ const match = /^\s*(\d+)\s+(.+)$/.exec(line);
81
+ if (!match?.[1] || !match[2]?.includes(launchableBinary))
82
+ continue;
83
+ const pid = Number(match[1]);
84
+ if (!Number.isSafeInteger(pid) || pid <= 0 || pid === process.pid)
85
+ continue;
86
+ matches.push({ pid, commandLine: match[2] });
87
+ }
88
+ return matches;
89
+ }
90
+ /**
91
+ * Refuses a browser-harness launch when this objdir's app is already alive,
92
+ * or terminates the parent when the operator explicitly opted into stale
93
+ * browser cleanup. This closes the no-listening-port variant of the stale
94
+ * Marionette process failure.
95
+ */
96
+ export async function ensureLaunchableBrowserNotRunning(launchableBinary, options = {}) {
97
+ const processes = await findRunningBundleProcesses(launchableBinary);
98
+ if (processes.length === 0)
99
+ return;
100
+ const parent = processes.find((candidate) => !candidate.commandLine.includes('-contentproc'));
101
+ const holder = parent ?? processes[0];
102
+ if (!holder)
103
+ return;
104
+ if (options.killStaleBrowser) {
105
+ try {
106
+ if (process.platform === 'win32') {
107
+ await exec('powershell.exe', [
108
+ '-NoProfile',
109
+ '-NonInteractive',
110
+ '-Command',
111
+ `Stop-Process -Id ${holder.pid} -Force`,
112
+ ]);
113
+ }
114
+ else {
115
+ process.kill(holder.pid, 'SIGTERM');
116
+ }
117
+ return;
118
+ }
119
+ catch (error) {
120
+ throw new GeneralError(`A browser from this objdir is still running (PID ${holder.pid}), but FireForge could not terminate it: ${toError(error).message}`);
121
+ }
122
+ }
123
+ const killHint = process.platform === 'win32'
124
+ ? `Stop-Process -Id ${holder.pid} -Force`
125
+ : `kill ${holder.pid} # or "kill -9 ${holder.pid}" if it doesn't exit`;
126
+ throw new GeneralError(`A browser from this project's objdir is already running (PID ${holder.pid}). ` +
127
+ 'It may have survived a previously timed-out or interrupted "fireforge test" run and can wedge the next headed launch. ' +
128
+ `Stop it with "${killHint}", or retry with "--kill-stale-marionette".`);
129
+ }
48
130
  /**
49
131
  * Returns `true` when the holder's command basename or command-line
50
132
  * flags clearly identify it as a Firefox-family browser with
@@ -110,13 +192,12 @@ async function probeWithLsof(port) {
110
192
  }
111
193
  return { inUse: true, holder: { pid, command, commandLine } };
112
194
  }
113
- catch (error) {
114
- // `lsof` missing, or stdout parse failed. Treat as "unknown" ⇒
115
- // port probe is silently skipped.
116
- const message = toError(error).message;
117
- if (/ENOENT|not found|command not found/i.test(message)) {
118
- return { inUse: false };
119
- }
195
+ catch {
196
+ // `lsof` missing, or its stdout did not parse. Either way the port state
197
+ // is unknown, and an unknown port is reported as free so the probe never
198
+ // blocks a run on its own uncertainty. (The errno test that used to sit
199
+ // here returned the same value as this fallthrough, so it decided
200
+ // nothing.)
120
201
  return { inUse: false };
121
202
  }
122
203
  }
@@ -156,6 +237,8 @@ async function probeWithPowerShell(port) {
156
237
  return { inUse: true, holder: { pid, command, commandLine } };
157
238
  }
158
239
  catch {
240
+ // Unparseable netstat/PowerShell output. The port state is unknown, and an unknown port
241
+ // is reported free so the probe never blocks a run on its own uncertainty.
159
242
  return { inUse: false };
160
243
  }
161
244
  }
@@ -172,6 +255,8 @@ export async function probeMarionettePort(port = DEFAULT_MARIONETTE_PORT) {
172
255
  platform = getPlatform();
173
256
  }
174
257
  catch {
258
+ // Unrecognised host platform — no probe strategy applies, so the port is
259
+ // reported free rather than blocking the run.
175
260
  return { inUse: false };
176
261
  }
177
262
  if (platform === 'darwin' || platform === 'linux') {
@@ -241,7 +326,7 @@ export async function ensureMarionettePortAvailable(port = DEFAULT_MARIONETTE_PO
241
326
  }
242
327
  catch (error) {
243
328
  throw new GeneralError(`Marionette port ${port} is held by stale browser ${holder.command} (PID ${holder.pid}), ` +
244
- `but FireForge could not terminate it: ${error instanceof Error ? error.message : String(error)}`);
329
+ `but FireForge could not terminate it: ${toError(error).message}`);
245
330
  }
246
331
  }
247
332
  /**
@@ -17,6 +17,7 @@ import { mkdtemp, rm } from 'node:fs/promises';
17
17
  import net from 'node:net';
18
18
  import { tmpdir } from 'node:os';
19
19
  import { join } from 'node:path';
20
+ import { toError } from '../utils/errors.js';
20
21
  import { pathExists } from '../utils/fs.js';
21
22
  import { info, warn } from '../utils/logger.js';
22
23
  import { ensureMach } from './mach.js';
@@ -75,7 +76,7 @@ export async function runMarionettePreflight(engineDir, options = {}) {
75
76
  await ensureMach(engineDir);
76
77
  }
77
78
  catch (error) {
78
- return fail('mach-available', `mach not available in engine: ${error.message}`, elapsed());
79
+ return fail('mach-available', `mach not available in engine: ${toError(error).message}`, elapsed());
79
80
  }
80
81
  // Layer 3: Python that mach requires is discoverable.
81
82
  let python;
@@ -83,7 +84,7 @@ export async function runMarionettePreflight(engineDir, options = {}) {
83
84
  python = await getPython(engineDir);
84
85
  }
85
86
  catch (error) {
86
- return fail('python-available', `Python interpreter required by mach is not available: ${error.message}`, elapsed());
87
+ return fail('python-available', `Python interpreter required by mach is not available: ${toError(error).message}`, elapsed());
87
88
  }
88
89
  // Layer 4: throwaway browser profile directory is creatable.
89
90
  let profileDir;
@@ -91,7 +92,7 @@ export async function runMarionettePreflight(engineDir, options = {}) {
91
92
  profileDir = await mkdtemp(join(tmpdir(), 'fireforge-marionette-'));
92
93
  }
93
94
  catch (error) {
94
- return fail('profile-creatable', `Could not create a throwaway browser profile in ${tmpdir()}: ${error.message}`, elapsed());
95
+ return fail('profile-creatable', `Could not create a throwaway browser profile in ${tmpdir()}: ${toError(error).message}`, elapsed());
95
96
  }
96
97
  let child;
97
98
  let stderrTail = '';
@@ -123,7 +124,7 @@ export async function runMarionettePreflight(engineDir, options = {}) {
123
124
  });
124
125
  }
125
126
  catch (error) {
126
- return fail('browser-spawns', `Could not spawn mach run: ${error.message}`, elapsed());
127
+ return fail('browser-spawns', `Could not spawn mach run: ${toError(error).message}`, elapsed());
127
128
  }
128
129
  const spawnedChild = child;
129
130
  child.stderr?.on('data', (data) => {
@@ -183,7 +184,7 @@ export async function runMarionettePreflight(engineDir, options = {}) {
183
184
  await rm(profileDir, { recursive: true, force: true });
184
185
  }
185
186
  catch (error) {
186
- warn(`Could not clean up marionette preflight profile: ${error.message}`);
187
+ warn(`Could not clean up marionette preflight profile: ${toError(error).message}`);
187
188
  }
188
189
  }
189
190
  }
@@ -2,7 +2,7 @@
2
2
  * Patch orchestration — coordinates patch discovery, application, and validation.
3
3
  * Pure parsing, content transformation, and lock management are in separate modules.
4
4
  */
5
- import type { ImportSummary, PatchResult } from '../types/commands/index.js';
5
+ import type { ImportSummary, PatchInfo, PatchMetadata, PatchResult } from '../types/commands/index.js';
6
6
  export { PatchError } from '../errors/patch.js';
7
7
  export { countPatches, discoverPatches, getAllTargetFilesFromPatch, getTargetFileFromPatch, isNewFilePatch, } from './patch-files.js';
8
8
  export { withPatchDirectoryLock } from './patch-lock.js';
@@ -73,18 +73,25 @@ export declare function matchesUntilFilename(patchFilename: string, needle: stri
73
73
  */
74
74
  export declare function applyPatchesWithContinue(patchesDir: string, engineDir: string, optionsOrContinue?: ApplyPatchesOptions | boolean): Promise<ImportSummary>;
75
75
  /**
76
- * Computes the cumulative patched content for a file.
77
- * @param patchesDir - Path to the patches directory
78
- * @param engineDir - Path to the engine directory
79
- * @param filePath - File path to compute content for
80
- * @returns Content after all patches applied, or null if file doesn't exist
76
+ * Batched patched-content computation shared by status classification,
77
+ * import's unmanaged-dirty guard, and discard baseline planning.
81
78
  */
82
- export declare function computePatchedContent(patchesDir: string, engineDir: string, filePath: string): Promise<string | null>;
79
+ export interface PatchedContentContext {
80
+ /** Manifest rows, so callers don't re-load the manifest per file. */
81
+ manifestPatches: readonly PatchMetadata[];
82
+ /** Cumulative patched content for a file (null if absent at HEAD with no creator). */
83
+ computePatched: (filePath: string) => Promise<string | null>;
84
+ /** Patches affecting a file, sorted by order (from the shared inverted index). */
85
+ getAffectingPatches: (filePath: string) => readonly PatchInfo[];
86
+ /** Patch body text, memoized per patch path across the whole batch. */
87
+ readPatchBody: (patch: PatchInfo) => Promise<string>;
88
+ }
83
89
  /**
84
- * Builds a batched variant of {@link computePatchedContent} that loads the
85
- * manifest and discovers patch files ONCE for many lookups. The per-call
86
- * function re-runs `loadPatchesManifest` + `discoverPatches` for every
87
- * file — O(dirtyFiles × patches) redundant IO when classifying a broad
88
- * engine edit session during `import`.
90
+ * Builds a batched patched-content context that loads the manifest and
91
+ * discovers patch files ONCE for many lookups, and memoizes each patch
92
+ * body read across the batch. The old per-call helper re-ran
93
+ * `loadPatchesManifest` + `discoverPatches` and re-read every affecting
94
+ * patch body for every file — O(dirtyFiles × patches) redundant IO when
95
+ * classifying a broad engine edit session during `status` or `import`.
89
96
  */
90
- export declare function createPatchedContentComputer(patchesDir: string, engineDir: string): Promise<(filePath: string) => Promise<string | null>>;
97
+ export declare function createPatchedContentContext(patchesDir: string, engineDir: string): Promise<PatchedContentContext>;
@@ -14,9 +14,9 @@ import { exec } from '../utils/process.js';
14
14
  import { applyPatchIdempotent, reversePatch } from './git.js';
15
15
  import { getFileContentFromHead } from './git-file-ops.js';
16
16
  import { discoverPatches } from './patch-files.js';
17
- import { findPatchesAffectingFile, loadPatchesManifest } from './patch-manifest.js';
17
+ import { loadPatchesManifest } from './patch-manifest.js';
18
18
  import { extractAffectedFiles, extractConflictingFiles, isNewFileInPatch } from './patch-parse.js';
19
- import { applyPatchToContent, extractNewFileContent } from './patch-transform.js';
19
+ import { applyPatchTextToContent, extractNewFileContent } from './patch-transform.js';
20
20
  // Re-export from split modules so existing import sites continue working
21
21
  export { PatchError } from '../errors/patch.js';
22
22
  export { countPatches, discoverPatches, getAllTargetFilesFromPatch, getTargetFileFromPatch, isNewFilePatch, } from './patch-files.js';
@@ -394,33 +394,14 @@ export async function applyPatchesWithContinue(patchesDir, engineDir, optionsOrC
394
394
  };
395
395
  }
396
396
  /**
397
- * Computes the cumulative patched content for a file.
398
- * @param patchesDir - Path to the patches directory
399
- * @param engineDir - Path to the engine directory
400
- * @param filePath - File path to compute content for
401
- * @returns Content after all patches applied, or null if file doesn't exist
402
- */
403
- export async function computePatchedContent(patchesDir, engineDir, filePath) {
404
- let content = await getFileContentFromHead(engineDir, filePath);
405
- // Find all patches affecting this file
406
- const affectingPatches = await findPatchesAffectingFile(patchesDir, filePath);
407
- if (affectingPatches.length === 0) {
408
- return content;
409
- }
410
- // Apply each patch in order
411
- for (const { patch } of affectingPatches) {
412
- content = await applyPatchToContent(content, patch.path, filePath);
413
- }
414
- return content;
415
- }
416
- /**
417
- * Builds a batched variant of {@link computePatchedContent} that loads the
418
- * manifest and discovers patch files ONCE for many lookups. The per-call
419
- * function re-runs `loadPatchesManifest` + `discoverPatches` for every
420
- * file — O(dirtyFiles × patches) redundant IO when classifying a broad
421
- * engine edit session during `import`.
397
+ * Builds a batched patched-content context that loads the manifest and
398
+ * discovers patch files ONCE for many lookups, and memoizes each patch
399
+ * body read across the batch. The old per-call helper re-ran
400
+ * `loadPatchesManifest` + `discoverPatches` and re-read every affecting
401
+ * patch body for every file — O(dirtyFiles × patches) redundant IO when
402
+ * classifying a broad engine edit session during `status` or `import`.
422
403
  */
423
- export async function createPatchedContentComputer(patchesDir, engineDir) {
404
+ export async function createPatchedContentContext(patchesDir, engineDir) {
424
405
  const manifest = await loadPatchesManifest(patchesDir);
425
406
  const patches = await discoverPatches(patchesDir);
426
407
  const patchByFilename = new Map(patches.map((p) => [p.filename, p]));
@@ -438,11 +419,29 @@ export async function createPatchedContentComputer(patchesDir, engineDir) {
438
419
  for (const list of affectingByFile.values()) {
439
420
  list.sort((a, b) => a.order - b.order);
440
421
  }
441
- return async (filePath) => {
442
- let content = await getFileContentFromHead(engineDir, filePath);
443
- for (const patch of affectingByFile.get(filePath) ?? []) {
444
- content = await applyPatchToContent(content, patch.path, filePath);
422
+ // Promise-memoized so concurrent classification workers never read the
423
+ // same body twice (memoizing the promise, not the value, closes the
424
+ // window between two workers missing the cache at once).
425
+ const bodyPromises = new Map();
426
+ const readPatchBody = (patch) => {
427
+ let body = bodyPromises.get(patch.path);
428
+ if (!body) {
429
+ body = readText(patch.path);
430
+ bodyPromises.set(patch.path, body);
445
431
  }
446
- return content;
432
+ return body;
433
+ };
434
+ const getAffectingPatches = (filePath) => affectingByFile.get(filePath) ?? [];
435
+ return {
436
+ manifestPatches: manifest?.patches ?? [],
437
+ getAffectingPatches,
438
+ readPatchBody,
439
+ computePatched: async (filePath) => {
440
+ let content = await getFileContentFromHead(engineDir, filePath);
441
+ for (const patch of getAffectingPatches(filePath)) {
442
+ content = applyPatchTextToContent(content, await readPatchBody(patch), filePath);
443
+ }
444
+ return content;
445
+ },
447
446
  };
448
447
  }
@@ -1,8 +1,8 @@
1
1
  import type { PatchCategory, PatchesManifest, PatchInfo, PatchMetadata } from '../types/commands/index.js';
2
2
  import type { FireForgeConfig } from '../types/config.js';
3
3
  import { type SupersedeCoverageDetail } from './patch-export-coverage.js';
4
- export { findAllPatchesForFiles, findAllPatchesForFilesWithDetails, findSupersededPatches, isPatchFullyCovered, type PatchCoverage, type SupersedeCoverageDetail, } from './patch-export-coverage.js';
5
- export { type ClearablePatchMetadataField, mutatePatchMetadata, type PatchMetadataMutation, type PatchMetadataMutationResult, updatePatchMetadata, } from './patch-export-metadata.js';
4
+ export { findAllPatchesForFiles, findAllPatchesForFilesWithDetails, findSupersededPatches, isPatchFullyCovered, type SupersedeCoverageDetail, } from './patch-export-coverage.js';
5
+ export { mutatePatchMetadata, updatePatchMetadata } from './patch-export-metadata.js';
6
6
  export { updatePatchAndMetadata } from './patch-export-update.js';
7
7
  /**
8
8
  * Gets the next patch number for a new patch.
@@ -10,7 +10,8 @@ import { findAllPatchesForFilesWithDetails, } from './patch-export-coverage.js';
10
10
  import { addPatchToManifest, loadPatchesManifestForWrite, PATCHES_MANIFEST, savePatchesManifest, } from './patch-manifest.js';
11
11
  import { allocatePolicyOrder, enforcePatchPolicy } from './patch-policy.js';
12
12
  export { findAllPatchesForFiles, findAllPatchesForFilesWithDetails, findSupersededPatches, isPatchFullyCovered, } from './patch-export-coverage.js';
13
- export { mutatePatchMetadata, updatePatchMetadata, } from './patch-export-metadata.js';
13
+ import { escapeRegex } from '../utils/regex.js';
14
+ export { mutatePatchMetadata, updatePatchMetadata } from './patch-export-metadata.js';
14
15
  export { updatePatchAndMetadata } from './patch-export-update.js';
15
16
  /**
16
17
  * Gets the next patch number for a new patch.
@@ -45,9 +46,6 @@ export function sanitizeName(name) {
45
46
  .replace(/^-+|-+$/g, '')
46
47
  .slice(0, 50);
47
48
  }
48
- function escapeRegExp(value) {
49
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
50
- }
51
49
  /**
52
50
  * Strips a leading `NNN-<category>-` prefix from a sanitized name slug.
53
51
  * Operators frequently pass the DESIRED FILENAME stem to `--name`
@@ -59,8 +57,8 @@ function escapeRegExp(value) {
59
57
  */
60
58
  export function stripRedundantCategoryPrefix(sanitizedName, category) {
61
59
  const prefixes = [
62
- new RegExp(`^\\d+-${escapeRegExp(category)}-`),
63
- new RegExp(`^${escapeRegExp(category)}-`),
60
+ new RegExp(`^\\d+-${escapeRegex(category)}-`),
61
+ new RegExp(`^${escapeRegex(category)}-`),
64
62
  ];
65
63
  let stripped = sanitizedName;
66
64
  let changed = true;