@hominis/fireforge 0.40.0 → 0.42.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 (256) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +16 -10
  3. package/dist/bin/fireforge.js +25 -4
  4. package/dist/build-info.json +9 -0
  5. package/dist/src/cli.js +20 -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 +13 -3
  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 +16 -7
  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 +12 -59
  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.js +4 -4
  45. package/dist/src/commands/manifest.js +1 -1
  46. package/dist/src/commands/package.js +9 -16
  47. package/dist/src/commands/patch/compact.js +5 -5
  48. package/dist/src/commands/patch/delete.js +5 -5
  49. package/dist/src/commands/patch/lint-ignore.js +3 -2
  50. package/dist/src/commands/patch/move-files-create.js +22 -4
  51. package/dist/src/commands/patch/move-files-into.js +14 -10
  52. package/dist/src/commands/patch/move-files.js +6 -5
  53. package/dist/src/commands/patch/rename.d.ts +2 -2
  54. package/dist/src/commands/patch/rename.js +151 -50
  55. package/dist/src/commands/patch/reorder.d.ts +9 -0
  56. package/dist/src/commands/patch/reorder.js +14 -10
  57. package/dist/src/commands/patch/split-plan.d.ts +13 -6
  58. package/dist/src/commands/patch/split-plan.js +22 -12
  59. package/dist/src/commands/patch/split.d.ts +1 -1
  60. package/dist/src/commands/patch/split.js +23 -9
  61. package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
  62. package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
  63. package/dist/src/commands/patch/staged-dependency.js +10 -0
  64. package/dist/src/commands/re-export-adjacent.js +5 -6
  65. package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
  66. package/dist/src/commands/re-export-bulk-scan.js +142 -2
  67. package/dist/src/commands/re-export-drift.d.ts +74 -0
  68. package/dist/src/commands/re-export-drift.js +234 -0
  69. package/dist/src/commands/re-export-files.js +5 -3
  70. package/dist/src/commands/re-export-lint.d.ts +97 -0
  71. package/dist/src/commands/re-export-lint.js +210 -0
  72. package/dist/src/commands/re-export-options.js +8 -0
  73. package/dist/src/commands/re-export-register.d.ts +18 -0
  74. package/dist/src/commands/re-export-register.js +61 -0
  75. package/dist/src/commands/re-export-scan.js +13 -7
  76. package/dist/src/commands/re-export-single.d.ts +20 -0
  77. package/dist/src/commands/re-export-single.js +261 -0
  78. package/dist/src/commands/re-export.d.ts +0 -4
  79. package/dist/src/commands/re-export.js +113 -268
  80. package/dist/src/commands/rebase/abort.js +13 -4
  81. package/dist/src/commands/rebase/continue.js +11 -4
  82. package/dist/src/commands/rebase/index.js +19 -29
  83. package/dist/src/commands/rebase/patch-loop.js +12 -0
  84. package/dist/src/commands/reset.js +4 -23
  85. package/dist/src/commands/resolve.js +2 -9
  86. package/dist/src/commands/run.js +11 -21
  87. package/dist/src/commands/source.js +3 -9
  88. package/dist/src/commands/status-check.d.ts +13 -0
  89. package/dist/src/commands/status-check.js +31 -13
  90. package/dist/src/commands/status-json.d.ts +31 -0
  91. package/dist/src/commands/status-json.js +76 -0
  92. package/dist/src/commands/status-output.d.ts +1 -0
  93. package/dist/src/commands/status-output.js +10 -1
  94. package/dist/src/commands/status-ownership.d.ts +52 -0
  95. package/dist/src/commands/status-ownership.js +57 -0
  96. package/dist/src/commands/status.js +184 -175
  97. package/dist/src/commands/test-build-phase.d.ts +19 -0
  98. package/dist/src/commands/test-build-phase.js +168 -0
  99. package/dist/src/commands/test-diagnose.js +30 -2
  100. package/dist/src/commands/test-modes.d.ts +9 -1
  101. package/dist/src/commands/test-modes.js +46 -13
  102. package/dist/src/commands/test-register.js +59 -27
  103. package/dist/src/commands/test-run.d.ts +29 -3
  104. package/dist/src/commands/test-run.js +41 -4
  105. package/dist/src/commands/test-verdict.d.ts +32 -0
  106. package/dist/src/commands/test-verdict.js +48 -0
  107. package/dist/src/commands/test.d.ts +9 -0
  108. package/dist/src/commands/test.js +100 -123
  109. package/dist/src/commands/token-coverage.js +4 -9
  110. package/dist/src/commands/tree.d.ts +3 -0
  111. package/dist/src/commands/tree.js +171 -25
  112. package/dist/src/commands/verify.js +2 -2
  113. package/dist/src/commands/watch.js +22 -21
  114. package/dist/src/commands/wire.js +1 -1
  115. package/dist/src/core/ast-utils.d.ts +11 -0
  116. package/dist/src/core/ast-utils.js +11 -0
  117. package/dist/src/core/branding.d.ts +16 -0
  118. package/dist/src/core/branding.js +18 -2
  119. package/dist/src/core/build-audit-platform.js +6 -1
  120. package/dist/src/core/build-audit-registration.js +2 -0
  121. package/dist/src/core/build-audit-resolve.d.ts +13 -0
  122. package/dist/src/core/build-audit-resolve.js +34 -13
  123. package/dist/src/core/build-audit.js +4 -20
  124. package/dist/src/core/build-baseline-types.d.ts +29 -8
  125. package/dist/src/core/build-baseline-types.js +2 -1
  126. package/dist/src/core/build-baseline.d.ts +10 -2
  127. package/dist/src/core/build-baseline.js +39 -8
  128. package/dist/src/core/build-prepare.d.ts +4 -0
  129. package/dist/src/core/build-prepare.js +7 -1
  130. package/dist/src/core/config-paths.d.ts +1 -1
  131. package/dist/src/core/config-paths.js +2 -0
  132. package/dist/src/core/config-validate-test-toolchains.js +3 -2
  133. package/dist/src/core/config-validate.js +34 -36
  134. package/dist/src/core/config.js +16 -3
  135. package/dist/src/core/coverage-extend.d.ts +89 -0
  136. package/dist/src/core/coverage-extend.js +188 -0
  137. package/dist/src/core/destructive.js +3 -1
  138. package/dist/src/core/discard-baseline.js +18 -4
  139. package/dist/src/core/engine-precondition.d.ts +35 -0
  140. package/dist/src/core/engine-precondition.js +50 -0
  141. package/dist/src/core/engine-session-lock.d.ts +38 -1
  142. package/dist/src/core/engine-session-lock.js +78 -4
  143. package/dist/src/core/file-lock.d.ts +7 -0
  144. package/dist/src/core/file-lock.js +20 -30
  145. package/dist/src/core/firefox.js +5 -2
  146. package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
  147. package/dist/src/core/furnace-apply-helpers.js +12 -3
  148. package/dist/src/core/furnace-apply-output.js +9 -0
  149. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
  150. package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
  151. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
  152. package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
  153. package/dist/src/core/furnace-apply.d.ts +16 -4
  154. package/dist/src/core/furnace-apply.js +39 -43
  155. package/dist/src/core/furnace-config.js +2 -0
  156. package/dist/src/core/furnace-css-fragments.js +2 -0
  157. package/dist/src/core/furnace-jsconfig.js +14 -7
  158. package/dist/src/core/furnace-registration-ast.js +8 -8
  159. package/dist/src/core/furnace-registration-remove.js +6 -3
  160. package/dist/src/core/furnace-registration.js +9 -12
  161. package/dist/src/core/furnace-rollback.js +1 -1
  162. package/dist/src/core/furnace-scanner.js +2 -1
  163. package/dist/src/core/furnace-stale-export.js +2 -1
  164. package/dist/src/core/furnace-state-persist.d.ts +4 -6
  165. package/dist/src/core/furnace-validate-compatibility.js +4 -7
  166. package/dist/src/core/furnace-validate-helpers.js +33 -1
  167. package/dist/src/core/furnace-validate-registration.js +16 -0
  168. package/dist/src/core/furnace-validate.js +4 -2
  169. package/dist/src/core/git-diff.js +37 -2
  170. package/dist/src/core/lint-cache.d.ts +1 -1
  171. package/dist/src/core/lint-cache.js +33 -10
  172. package/dist/src/core/mach-build-artifacts.d.ts +63 -1
  173. package/dist/src/core/mach-build-artifacts.js +108 -2
  174. package/dist/src/core/mach-mozconfig.js +9 -1
  175. package/dist/src/core/mach-resource-shim.js +1 -0
  176. package/dist/src/core/marionette-port.d.ts +16 -0
  177. package/dist/src/core/marionette-port.js +93 -8
  178. package/dist/src/core/marionette-preflight.js +6 -5
  179. package/dist/src/core/patch-apply.d.ts +20 -13
  180. package/dist/src/core/patch-apply.js +32 -33
  181. package/dist/src/core/patch-export.js +3 -5
  182. package/dist/src/core/patch-lint-checkjs.d.ts +25 -3
  183. package/dist/src/core/patch-lint-checkjs.js +78 -7
  184. package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
  185. package/dist/src/core/patch-lint-creators.d.ts +30 -0
  186. package/dist/src/core/patch-lint-creators.js +45 -0
  187. package/dist/src/core/patch-lint-cross.d.ts +1 -17
  188. package/dist/src/core/patch-lint-cross.js +16 -32
  189. package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
  190. package/dist/src/core/patch-lint-jsdoc.js +15 -5
  191. package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
  192. package/dist/src/core/patch-lint-module-registration.js +88 -0
  193. package/dist/src/core/patch-lint-mozbuild.js +99 -11
  194. package/dist/src/core/patch-lint-reexports.d.ts +1 -0
  195. package/dist/src/core/patch-lint-reexports.js +1 -0
  196. package/dist/src/core/patch-lint.d.ts +8 -0
  197. package/dist/src/core/patch-lint.js +84 -6
  198. package/dist/src/core/patch-lock.d.ts +12 -1
  199. package/dist/src/core/patch-lock.js +40 -4
  200. package/dist/src/core/patch-manifest-consistency.js +15 -3
  201. package/dist/src/core/patch-parse.d.ts +8 -0
  202. package/dist/src/core/patch-parse.js +10 -0
  203. package/dist/src/core/patch-policy.d.ts +7 -0
  204. package/dist/src/core/patch-policy.js +4 -1
  205. package/dist/src/core/patch-transform.d.ts +10 -0
  206. package/dist/src/core/patch-transform.js +13 -2
  207. package/dist/src/core/rebase-session.d.ts +44 -2
  208. package/dist/src/core/rebase-session.js +114 -17
  209. package/dist/src/core/register-browser-content.js +2 -1
  210. package/dist/src/core/register-shared-css.js +2 -1
  211. package/dist/src/core/smoke-patterns.d.ts +0 -8
  212. package/dist/src/core/smoke-patterns.js +3 -2
  213. package/dist/src/core/status-binary.d.ts +55 -0
  214. package/dist/src/core/status-binary.js +124 -0
  215. package/dist/src/core/status-classify.d.ts +6 -1
  216. package/dist/src/core/status-classify.js +101 -73
  217. package/dist/src/core/test-harness-crash.d.ts +32 -1
  218. package/dist/src/core/test-harness-crash.js +59 -4
  219. package/dist/src/core/test-path-scope.js +2 -0
  220. package/dist/src/core/test-stale-check.js +1 -20
  221. package/dist/src/core/toolchain-preflight.js +24 -26
  222. package/dist/src/core/tree-guard.d.ts +1 -1
  223. package/dist/src/core/tree-guard.js +67 -12
  224. package/dist/src/core/tree-store.d.ts +86 -10
  225. package/dist/src/core/tree-store.js +310 -72
  226. package/dist/src/core/typecheck.js +2 -1
  227. package/dist/src/core/wire-destroy.js +14 -3
  228. package/dist/src/core/wire-init.js +37 -40
  229. package/dist/src/core/wire-subscript.js +11 -3
  230. package/dist/src/core/wire-utils.js +4 -4
  231. package/dist/src/core/xpcshell-appdir.js +3 -0
  232. package/dist/src/errors/base.d.ts +13 -0
  233. package/dist/src/errors/base.js +13 -0
  234. package/dist/src/errors/rebase.d.ts +13 -0
  235. package/dist/src/errors/rebase.js +17 -0
  236. package/dist/src/types/commands/options.d.ts +86 -1
  237. package/dist/src/types/commands/patches.d.ts +8 -0
  238. package/dist/src/types/furnace.d.ts +7 -0
  239. package/dist/src/utils/build-info.d.ts +31 -0
  240. package/dist/src/utils/build-info.js +98 -0
  241. package/dist/src/utils/concurrency.d.ts +8 -0
  242. package/dist/src/utils/concurrency.js +21 -0
  243. package/dist/src/utils/errors.d.ts +39 -0
  244. package/dist/src/utils/errors.js +55 -0
  245. package/dist/src/utils/fs.js +5 -12
  246. package/dist/src/utils/logger.d.ts +14 -2
  247. package/dist/src/utils/logger.js +43 -13
  248. package/dist/src/utils/options.d.ts +21 -4
  249. package/dist/src/utils/options.js +26 -6
  250. package/dist/src/utils/package-root.js +7 -4
  251. package/dist/src/utils/process-group.js +13 -2
  252. package/dist/src/utils/stdio-drain.d.ts +28 -0
  253. package/dist/src/utils/stdio-drain.js +54 -0
  254. package/dist/src/utils/validation.d.ts +23 -2
  255. package/dist/src/utils/validation.js +29 -3
  256. package/package.json +9 -9
@@ -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
+ }
@@ -3,7 +3,7 @@ import { join } from 'node:path';
3
3
  import { MozconfigError } from '../errors/build.js';
4
4
  import { pathExists, readText, writeText } from '../utils/fs.js';
5
5
  import { getPlatform } from '../utils/platform.js';
6
- import { BrandingMozconfigMismatchError } from './branding.js';
6
+ import { BrandingMozconfigMismatchError, splitAppId } from './branding.js';
7
7
  /**
8
8
  * Replaces template variables in a string.
9
9
  * @param content - Content with ${variable} placeholders
@@ -95,6 +95,14 @@ export async function generateMozconfig(configsDir, engineDir, config) {
95
95
  binaryName: config.binaryName,
96
96
  };
97
97
  let content = '';
98
+ // Bundle identity: branding configure.sh carries only the LEAF of appId
99
+ // (see splitAppId in branding.ts — upstream composes the mac bundle id as
100
+ // <distribution-id>.<MOZ_MACBUNDLE_ID>); the prefix travels here so the
101
+ // two halves can never drift apart.
102
+ const { distributionId } = splitAppId(config.appId);
103
+ content +=
104
+ `# FireForge identity (generated from fireforge.json appId)\n` +
105
+ `ac_add_options --with-distribution-id=${distributionId}\n\n`;
98
106
  // Read common config if it exists
99
107
  if (await pathExists(commonPath)) {
100
108
  const commonContent = await readText(commonPath);
@@ -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
  }
@@ -10,6 +10,7 @@ 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
+ import { escapeRegex } from '../utils/regex.js';
13
14
  export { mutatePatchMetadata, updatePatchMetadata } from './patch-export-metadata.js';
14
15
  export { updatePatchAndMetadata } from './patch-export-update.js';
15
16
  /**
@@ -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;
@@ -61,9 +61,17 @@ export interface GroupedCheckJsResult {
61
61
  * the built-in Firefox-globals shim (from `patchLint.checkJsExtraShim`)
62
62
  * @param projectRoot - Absolute project root for resolving `extraShimPath`
63
63
  * @param mode - Strictness preset plus allowlisted compiler-option overrides
64
+ * @param builtinShimSuffix - Optional shim text appended AFTER the consumer shim
65
+ * @param rootScope - When set, only these repo-relative files become program
66
+ * ROOTS; resolution (and the host allowlist) still spans all of
67
+ * `resolutionOwned`, so a subset root's cross-patch imports type-check
68
+ * against the real owning sources while unrelated owned files are never
69
+ * parsed. `.mjs` files are module-scoped, so a root's diagnostics are
70
+ * identical whether other owned files are roots or mere resolution
71
+ * targets (pinned by the rootScope parity test — FORGE J1c).
64
72
  * @returns Diagnostics grouped per owning file plus run-level errors
65
73
  */
66
- export declare function runCheckJsGrouped(repoDir: string, resolutionOwned: Set<string>, extraShimPath?: string, projectRoot?: string, mode?: CheckJsMode, builtinShimSuffix?: string): Promise<GroupedCheckJsResult>;
74
+ export declare function runCheckJsGrouped(repoDir: string, resolutionOwned: Set<string>, extraShimPath?: string, projectRoot?: string, mode?: CheckJsMode, builtinShimSuffix?: string, rootScope?: ReadonlySet<string>): Promise<GroupedCheckJsResult>;
67
75
  /**
68
76
  * Flattens a {@link runCheckJsGrouped} run into a single issue list. When
69
77
  * `reportScope` is supplied, only diagnostics from files in that set are
@@ -110,7 +118,7 @@ export declare function invokePatchLintCheckJs(repoDir: string, patchOwnedFiles:
110
118
  * @param patchLint - The resolved `patchLint` config block
111
119
  * @param projectRoot - FireForge project root for shim resolution
112
120
  */
113
- export declare function runCheckJsTestFilesGrouped(repoDir: string, testFiles: Set<string>, patchLint: PatchLintConfig, projectRoot: string): Promise<GroupedCheckJsResult>;
121
+ export declare function runCheckJsTestFilesGrouped(repoDir: string, testFiles: Set<string>, patchLint: PatchLintConfig, projectRoot: string, rootScope?: ReadonlySet<string>): Promise<GroupedCheckJsResult>;
114
122
  /**
115
123
  * Grouped variant of {@link invokePatchLintCheckJs}: builds one queue-wide
116
124
  * checkJs program over `patchOwnedFiles` and returns its findings grouped by
@@ -122,5 +130,19 @@ export declare function runCheckJsTestFilesGrouped(repoDir: string, testFiles: S
122
130
  * @param patchOwnedFiles - Every patch-owned `.sys.mjs` in the queue
123
131
  * @param patchLint - The resolved `patchLint` config block
124
132
  * @param projectRoot - FireForge project root for shim resolution
133
+ * @param rootScope - Optional subset of files to use as program roots
134
+ * (`--patches`); resolution still spans the whole queue (FORGE J1c)
125
135
  */
126
- export declare function invokePatchLintCheckJsGrouped(repoDir: string, patchOwnedFiles: Set<string>, patchLint: PatchLintConfig, projectRoot: string): Promise<GroupedCheckJsResult>;
136
+ export declare function invokePatchLintCheckJsGrouped(repoDir: string, patchOwnedFiles: Set<string>, patchLint: PatchLintConfig, projectRoot: string, rootScope?: ReadonlySet<string>): Promise<GroupedCheckJsResult>;
137
+ /**
138
+ * Cheap probe reproducing the only run-level ("global") checkJs findings
139
+ * the build path can produce — a missing `typescript` package and an
140
+ * unreadable consumer shim (`checkJsExtraShim`, and `checkJsTestShim`
141
+ * when `checkJsTestFiles` is on). The built program itself never
142
+ * contributes globals (see {@link runCheckJsGrouped}), so a warm
143
+ * all-cache-hit run can satisfy the "warm never reports less than cold"
144
+ * invariant (FORGE F5) with this probe instead of building the whole
145
+ * TypeScript program. Issue objects are byte-identical to the build
146
+ * path's, deduplicated by message like {@link runCheckJsTestFilesGrouped}.
147
+ */
148
+ export declare function probeCheckJsGlobalIssues(patchLint: PatchLintConfig, projectRoot: string): Promise<PatchLintIssue[]>;