@hominis/fireforge 0.41.0 → 0.43.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 (201) hide show
  1. package/CHANGELOG.md +81 -34
  2. package/dist/build-info.json +4 -4
  3. package/dist/src/cli.js +21 -2
  4. package/dist/src/commands/build.js +7 -1
  5. package/dist/src/commands/config.js +16 -27
  6. package/dist/src/commands/discard.js +1 -1
  7. package/dist/src/commands/export-flow.js +13 -3
  8. package/dist/src/commands/export-placement-conflicts.js +1 -1
  9. package/dist/src/commands/export-shared.d.ts +1 -1
  10. package/dist/src/commands/export-shared.js +4 -5
  11. package/dist/src/commands/furnace/chrome-doc-remove.js +3 -0
  12. package/dist/src/commands/furnace/chrome-doc.js +3 -0
  13. package/dist/src/commands/furnace/create.js +3 -0
  14. package/dist/src/commands/furnace/override.js +3 -0
  15. package/dist/src/commands/furnace/preview.js +3 -0
  16. package/dist/src/commands/furnace/refresh.js +3 -0
  17. package/dist/src/commands/furnace/remove.js +3 -0
  18. package/dist/src/commands/furnace/rename.js +3 -0
  19. package/dist/src/commands/furnace/scan.js +3 -0
  20. package/dist/src/commands/import.js +1 -1
  21. package/dist/src/commands/lint-per-patch.js +16 -8
  22. package/dist/src/commands/lint-per-run-checkjs.d.ts +4 -4
  23. package/dist/src/commands/lint-per-run-checkjs.js +4 -4
  24. package/dist/src/commands/lint-report.js +1 -1
  25. package/dist/src/commands/lint.js +2 -2
  26. package/dist/src/commands/patch/lint-ignore.js +1 -1
  27. package/dist/src/commands/patch/move-files-create.js +5 -3
  28. package/dist/src/commands/patch/move-files-into.d.ts +1 -1
  29. package/dist/src/commands/patch/move-files-into.js +5 -3
  30. package/dist/src/commands/patch/move-files.js +1 -1
  31. package/dist/src/commands/patch/rename.js +1 -1
  32. package/dist/src/commands/patch/reorder.d.ts +1 -2
  33. package/dist/src/commands/patch/reorder.js +1 -2
  34. package/dist/src/commands/patch/split.js +5 -3
  35. package/dist/src/commands/patch/staged-dependency-validate.js +1 -1
  36. package/dist/src/commands/patch/staged-dependency.js +4 -4
  37. package/dist/src/commands/re-export-adjacent.js +1 -1
  38. package/dist/src/commands/re-export-bulk-scan.d.ts +1 -1
  39. package/dist/src/commands/re-export-bulk-scan.js +5 -4
  40. package/dist/src/commands/re-export-drift.d.ts +4 -4
  41. package/dist/src/commands/re-export-drift.js +47 -8
  42. package/dist/src/commands/re-export-lint.d.ts +7 -5
  43. package/dist/src/commands/re-export-lint.js +9 -8
  44. package/dist/src/commands/re-export-single.d.ts +1 -1
  45. package/dist/src/commands/re-export-single.js +5 -4
  46. package/dist/src/commands/re-export.js +26 -14
  47. package/dist/src/commands/reset.js +1 -1
  48. package/dist/src/commands/run.js +1 -1
  49. package/dist/src/commands/source.js +6 -6
  50. package/dist/src/commands/status-check.d.ts +1 -1
  51. package/dist/src/commands/status-check.js +1 -1
  52. package/dist/src/commands/status-json.d.ts +2 -2
  53. package/dist/src/commands/status-json.js +1 -1
  54. package/dist/src/commands/status-output.d.ts +1 -1
  55. package/dist/src/commands/status-output.js +1 -1
  56. package/dist/src/commands/status.js +21 -7
  57. package/dist/src/commands/test-build-phase.d.ts +2 -2
  58. package/dist/src/commands/test-build-phase.js +9 -6
  59. package/dist/src/commands/test-diagnose.js +8 -1
  60. package/dist/src/commands/test-modes.d.ts +2 -2
  61. package/dist/src/commands/test-modes.js +3 -3
  62. package/dist/src/commands/test-register.js +7 -2
  63. package/dist/src/commands/test-run.d.ts +6 -0
  64. package/dist/src/commands/test-run.js +32 -3
  65. package/dist/src/commands/test-stale-gate.js +7 -2
  66. package/dist/src/commands/test.js +4 -4
  67. package/dist/src/commands/tree.js +2 -2
  68. package/dist/src/commands/typecheck.d.ts +2 -1
  69. package/dist/src/commands/typecheck.js +59 -3
  70. package/dist/src/commands/verify.js +26 -1
  71. package/dist/src/core/ast-utils.js +14 -2
  72. package/dist/src/core/branding.d.ts +16 -0
  73. package/dist/src/core/branding.js +18 -2
  74. package/dist/src/core/browser-wire.js +9 -1
  75. package/dist/src/core/build-baseline-types.d.ts +1 -1
  76. package/dist/src/core/build-baseline.d.ts +1 -1
  77. package/dist/src/core/build-baseline.js +1 -1
  78. package/dist/src/core/build-overwrite-guard.d.ts +49 -0
  79. package/dist/src/core/build-overwrite-guard.js +112 -0
  80. package/dist/src/core/build-prepare.d.ts +13 -5
  81. package/dist/src/core/build-prepare.js +42 -9
  82. package/dist/src/core/config-mutate.d.ts +3 -2
  83. package/dist/src/core/config-mutate.js +5 -2
  84. package/dist/src/core/config.d.ts +3 -2
  85. package/dist/src/core/config.js +2 -0
  86. package/dist/src/core/coverage-extend.d.ts +1 -1
  87. package/dist/src/core/coverage-extend.js +1 -1
  88. package/dist/src/core/destructive.d.ts +23 -0
  89. package/dist/src/core/destructive.js +45 -4
  90. package/dist/src/core/diff-hunks.js +31 -17
  91. package/dist/src/core/discard-baseline.d.ts +1 -1
  92. package/dist/src/core/discard-baseline.js +1 -1
  93. package/dist/src/core/display-state.d.ts +52 -0
  94. package/dist/src/core/display-state.js +89 -0
  95. package/dist/src/core/engine-session-lock.d.ts +20 -0
  96. package/dist/src/core/engine-session-lock.js +84 -14
  97. package/dist/src/core/export-moved-code-shape.d.ts +60 -0
  98. package/dist/src/core/export-moved-code-shape.js +129 -0
  99. package/dist/src/core/file-lock.d.ts +44 -0
  100. package/dist/src/core/file-lock.js +157 -42
  101. package/dist/src/core/furnace-apply-helpers.d.ts +1 -1
  102. package/dist/src/core/furnace-apply-helpers.js +1 -1
  103. package/dist/src/core/furnace-apply-output.js +1 -1
  104. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +2 -2
  105. package/dist/src/core/furnace-apply-overwrite-warn.js +2 -2
  106. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +1 -1
  107. package/dist/src/core/furnace-apply-undeploy-actions.js +1 -1
  108. package/dist/src/core/furnace-apply.js +13 -1
  109. package/dist/src/core/furnace-config-custom.d.ts +2 -1
  110. package/dist/src/core/furnace-config-order.d.ts +2 -1
  111. package/dist/src/core/furnace-config-order.js +9 -4
  112. package/dist/src/core/furnace-config-validate.d.ts +4 -3
  113. package/dist/src/core/furnace-config-validate.js +3 -3
  114. package/dist/src/core/furnace-config.js +5 -0
  115. package/dist/src/core/furnace-operation.d.ts +12 -0
  116. package/dist/src/core/furnace-operation.js +125 -1
  117. package/dist/src/core/furnace-rollback.d.ts +17 -0
  118. package/dist/src/core/furnace-rollback.js +35 -1
  119. package/dist/src/core/git-base.js +7 -2
  120. package/dist/src/core/git-diff.js +11 -2
  121. package/dist/src/core/git-readonly-index.d.ts +59 -0
  122. package/dist/src/core/git-readonly-index.js +137 -0
  123. package/dist/src/core/license-headers.d.ts +1 -1
  124. package/dist/src/core/license-headers.js +1 -1
  125. package/dist/src/core/lint-cache.d.ts +15 -5
  126. package/dist/src/core/lint-cache.js +35 -8
  127. package/dist/src/core/mach-known-noise-filter.d.ts +21 -0
  128. package/dist/src/core/mach-known-noise-filter.js +23 -0
  129. package/dist/src/core/mach-mozconfig.js +21 -3
  130. package/dist/src/core/module-resolution-preflight.d.ts +51 -0
  131. package/dist/src/core/module-resolution-preflight.js +155 -0
  132. package/dist/src/core/patch-export.js +2 -1
  133. package/dist/src/core/patch-identifier-suggest.js +29 -3
  134. package/dist/src/core/patch-lint-checkjs.d.ts +5 -5
  135. package/dist/src/core/patch-lint-checkjs.js +6 -6
  136. package/dist/src/core/patch-lint-creators.d.ts +1 -1
  137. package/dist/src/core/patch-lint-creators.js +1 -1
  138. package/dist/src/core/patch-lint-cross.d.ts +1 -1
  139. package/dist/src/core/patch-lint-cross.js +4 -4
  140. package/dist/src/core/patch-lint-css.js +1 -1
  141. package/dist/src/core/patch-lint-diff.d.ts +1 -1
  142. package/dist/src/core/patch-lint-diff.js +1 -1
  143. package/dist/src/core/patch-lint-jsdoc.js +4 -2
  144. package/dist/src/core/patch-lint-module-registration.d.ts +13 -0
  145. package/dist/src/core/patch-lint-module-registration.js +13 -4
  146. package/dist/src/core/patch-lint-mozbuild.d.ts +1 -1
  147. package/dist/src/core/patch-lint-mozbuild.js +1 -1
  148. package/dist/src/core/patch-lint-ownership.d.ts +2 -2
  149. package/dist/src/core/patch-lint-ownership.js +2 -2
  150. package/dist/src/core/patch-lint.d.ts +2 -2
  151. package/dist/src/core/patch-lint.js +3 -3
  152. package/dist/src/core/patch-lock.d.ts +21 -0
  153. package/dist/src/core/patch-lock.js +29 -2
  154. package/dist/src/core/patch-manifest-io.d.ts +2 -1
  155. package/dist/src/core/patch-manifest-io.js +49 -0
  156. package/dist/src/core/patch-parse.d.ts +18 -0
  157. package/dist/src/core/patch-parse.js +24 -1
  158. package/dist/src/core/patch-policy.d.ts +2 -2
  159. package/dist/src/core/patch-policy.js +2 -2
  160. package/dist/src/core/smoke-patterns.d.ts +1 -1
  161. package/dist/src/core/status-binary.d.ts +1 -1
  162. package/dist/src/core/status-binary.js +1 -1
  163. package/dist/src/core/status-classify.d.ts +1 -1
  164. package/dist/src/core/status-classify.js +2 -2
  165. package/dist/src/core/test-harness-crash.d.ts +69 -3
  166. package/dist/src/core/test-harness-crash.js +174 -9
  167. package/dist/src/core/test-stale-check.d.ts +1 -1
  168. package/dist/src/core/test-stale-check.js +1 -1
  169. package/dist/src/core/token-category.js +1 -1
  170. package/dist/src/core/token-manager.d.ts +1 -1
  171. package/dist/src/core/token-manager.js +5 -5
  172. package/dist/src/core/tree-cow.js +1 -1
  173. package/dist/src/core/tree-guard.js +2 -2
  174. package/dist/src/core/tree-store.js +1 -1
  175. package/dist/src/core/typecheck-shim.d.ts +2 -2
  176. package/dist/src/core/typecheck-shim.js +2 -2
  177. package/dist/src/core/typecheck.js +10 -0
  178. package/dist/src/core/xpcshell-profile-dir.js +1 -1
  179. package/dist/src/errors/base.d.ts +22 -1
  180. package/dist/src/errors/base.js +32 -1
  181. package/dist/src/errors/codes.d.ts +10 -0
  182. package/dist/src/errors/codes.js +10 -0
  183. package/dist/src/types/commands/options.d.ts +27 -11
  184. package/dist/src/types/commands/patches.d.ts +1 -1
  185. package/dist/src/types/config.d.ts +2 -2
  186. package/dist/src/types/furnace.d.ts +1 -1
  187. package/dist/src/types/json.d.ts +22 -0
  188. package/dist/src/types/json.js +11 -0
  189. package/dist/src/utils/assert.d.ts +46 -0
  190. package/dist/src/utils/assert.js +83 -0
  191. package/dist/src/utils/build-info.d.ts +1 -1
  192. package/dist/src/utils/build-info.js +1 -1
  193. package/dist/src/utils/logger.d.ts +20 -0
  194. package/dist/src/utils/logger.js +26 -0
  195. package/dist/src/utils/options.d.ts +12 -0
  196. package/dist/src/utils/options.js +39 -0
  197. package/dist/src/utils/stdio-drain.d.ts +1 -1
  198. package/dist/src/utils/stdio-drain.js +1 -1
  199. package/dist/src/utils/validation.d.ts +12 -1
  200. package/dist/src/utils/validation.js +13 -1
  201. package/package.json +3 -3
@@ -7,15 +7,25 @@ import { getPackageVersion } from '../utils/package-root.js';
7
7
  import { getFurnacePaths } from './furnace-config.js';
8
8
  import { git } from './git-base.js';
9
9
  import { collectNewFileCreatorsByPath } from './patch-lint.js';
10
- // Schema 2 (FORGE G10): entries additionally carry the lintIgnore-suppressed
10
+ // Schema 2: entries additionally carry the lintIgnore-suppressed
11
11
  // issues and the measured non-binary line count, so a warm run can report
12
12
  // waived size measurements identically to a cold one (the F5-class hazard:
13
13
  // a cache hit must never surface LESS than a fresh lint).
14
- // Schema 3 (FORGE H2): the key additionally content-hashes the
14
+ // Schema 3: the key additionally content-hashes the
15
15
  // `patchLint.checkJsTestShim` file exactly as `checkJsExtraShim`'s already
16
16
  // was — before this, editing the test shim in place replayed every cached
17
17
  // verdict (the same warm-run-reports-less-than-cold hazard class).
18
- export const LINT_CACHE_SCHEMA_VERSION = 3;
18
+ // Schema 4: each entry RECORDS the lint-ignore waiver set that
19
+ // produced it, and a lookup whose effective waiver set differs is refused
20
+ // even when the key matches. The key already hashes `patch.lintIgnore`, so
21
+ // this is belt-and-braces on purpose: a waiver change is the one mutation
22
+ // that makes a replayed verdict actively WRONG (an operator writes a
23
+ // waiver, re-runs, and is shown the finding they just waived — or worse,
24
+ // a pre-waiver verdict replayed under a post-waiver key), and the class
25
+ // must not depend on every future key input staying complete. Bumping the
26
+ // schema also discards every pre-4 entry, which is itself the cure for a
27
+ // cache that already holds a mismatched verdict.
28
+ export const LINT_CACHE_SCHEMA_VERSION = 4;
19
29
  const LINT_IMPLEMENTATION_VERSION = 1;
20
30
  const LINT_CACHE_DIRNAME = 'lint-cache';
21
31
  const PER_PATCH_CACHE_FILENAME = 'per-patch-v1.json';
@@ -108,7 +118,7 @@ export async function buildPerPatchLintCacheKey(input) {
108
118
  : { path: shimPath, hash: await fileHash(resolve(input.projectRoot, shimPath)) };
109
119
  // Both shims feed compiled checkJs programs, so both are content-hashed —
110
120
  // the config block alone only carries their PATHS, and an in-place edit
111
- // must invalidate the programs it feeds (FORGE H2).
121
+ // must invalidate the programs it feeds.
112
122
  const [extraShim, testShim] = await Promise.all([
113
123
  shimHashOf(input.config.patchLint?.checkJsExtraShim),
114
124
  shimHashOf(input.config.patchLint?.checkJsTestShim),
@@ -118,7 +128,7 @@ export async function buildPerPatchLintCacheKey(input) {
118
128
  engineHeadSha: input.engineHeadSha ?? null,
119
129
  lintImplementationVersion: LINT_IMPLEMENTATION_VERSION,
120
130
  // Deliberately the PLAIN semver, not the +g<sha> build identity
121
- // (FORGE K2): identity churns every commit at the same version and
131
+ //: identity churns every commit at the same version and
122
132
  // would invalidate the cache for no correctness gain.
123
133
  packageVersion: input.packageVersion ?? getPackageVersion(),
124
134
  patchFile: await fileHash(join(input.patchesDir, input.patch.filename)),
@@ -144,6 +154,8 @@ function isCacheEntry(value) {
144
154
  Array.isArray(entry.issues) &&
145
155
  Array.isArray(entry.suppressed) &&
146
156
  typeof entry.lineCount === 'number' &&
157
+ Array.isArray(entry.lintIgnore) &&
158
+ entry.lintIgnore.every((id) => typeof id === 'string') &&
147
159
  typeof entry.updatedAt === 'string');
148
160
  }
149
161
  /** Loads the per-patch lint cache, treating missing or invalid files as empty. */
@@ -187,11 +199,25 @@ export async function savePerPatchLintCache(projectRoot, cache) {
187
199
  export async function clearPerPatchLintCache(projectRoot) {
188
200
  await writeJson(getPerPatchLintCachePath(projectRoot), createEmptyPerPatchLintCache());
189
201
  }
190
- /** Returns the cached lint payload for a patch when the stored key still matches. */
191
- export function getCachedPerPatchLintIssues(cache, patchFilename, key) {
202
+ /** Canonical (sorted, de-duplicated) form of a waiver set for comparison. */
203
+ function normalizeLintIgnoreSet(lintIgnore) {
204
+ return [...new Set(lintIgnore ?? [])].sort((a, b) => a.localeCompare(b));
205
+ }
206
+ /**
207
+ * Returns the cached lint payload for a patch when the stored key still
208
+ * matches AND the entry was computed under the same lint-ignore waiver set
209
+ *. A waiver change makes a replayed verdict actively wrong, so
210
+ * the waiver set is compared explicitly rather than trusted to the key.
211
+ */
212
+ export function getCachedPerPatchLintIssues(cache, patchFilename, key, lintIgnore) {
192
213
  const entry = cache.entries[patchFilename];
193
214
  if (!entry || entry.key !== key)
194
215
  return undefined;
216
+ const expected = normalizeLintIgnoreSet(lintIgnore);
217
+ const stored = normalizeLintIgnoreSet(entry.lintIgnore);
218
+ if (expected.length !== stored.length || expected.some((id, i) => id !== stored[i])) {
219
+ return undefined;
220
+ }
195
221
  return {
196
222
  issues: entry.issues.map((issue) => ({ ...issue })),
197
223
  suppressed: entry.suppressed.map((issue) => ({ ...issue })),
@@ -199,13 +225,14 @@ export function getCachedPerPatchLintIssues(cache, patchFilename, key) {
199
225
  };
200
226
  }
201
227
  /** Stores the per-patch lint payload after a successful lint calculation. */
202
- export function setCachedPerPatchLintIssues(cache, patchFilename, key, issues, suppressed, lineCount) {
228
+ export function setCachedPerPatchLintIssues(cache, patchFilename, key, issues, suppressed, lineCount, lintIgnore) {
203
229
  cache.entries[patchFilename] = {
204
230
  key,
205
231
  patchFilename,
206
232
  issues: issues.map((issue) => ({ ...issue })),
207
233
  suppressed: suppressed.map((issue) => ({ ...issue })),
208
234
  lineCount,
235
+ lintIgnore: normalizeLintIgnoreSet(lintIgnore),
209
236
  updatedAt: new Date().toISOString(),
210
237
  };
211
238
  }
@@ -27,6 +27,27 @@
27
27
  * - The hold buffer is bounded; on overflow the block is flushed verbatim
28
28
  * and the filter returns to pass-through, so output is never lost.
29
29
  */
30
+ /**
31
+ * True when CAPTURED output carries the documented mozsystemmonitor
32
+ * teardown traceback.
33
+ *
34
+ * The echo filter above only affects what a human sees. The CLASSIFIER
35
+ * reads the raw capture, and it needs the same recognition to tell "the
36
+ * suite finished clean and then upstream fell over at shutdown" from "the
37
+ * suite did not finish". Deliberately the same two-signal test the echo
38
+ * filter applies — a novel attribute or a traceback from anywhere but
39
+ * `resourcemonitor.py` is NOT this incident and must keep failing the run.
40
+ *
41
+ * The shutdown-marker precondition the echo filter adds is intentionally
42
+ * omitted: the whole point at the classification layer is the case where
43
+ * the teardown crash prevented the shutdown marker from printing.
44
+ *
45
+ * Pure; exported for the classifier and for direct unit testing.
46
+ *
47
+ * @param output - Raw captured stdout/stderr
48
+ * @returns True when the recognized teardown traceback is present
49
+ */
50
+ export declare function hasKnownTeardownNoise(output: string): boolean;
30
51
  /**
31
52
  * Shared across the stdout and stderr filter instances of ONE mach run:
32
53
  * SUITE_END typically arrives on stdout while the teardown traceback lands
@@ -49,6 +49,29 @@ const SHUTDOWN_MARKER_PATTERN = /\bSUITE_END\b/;
49
49
  function isRecognizedTeardownNoise(block) {
50
50
  return (KNOWN_TEARDOWN_ATTRIBUTE_ERROR_PATTERN.test(block) && RESOURCEMONITOR_FRAME_PATTERN.test(block));
51
51
  }
52
+ /**
53
+ * True when CAPTURED output carries the documented mozsystemmonitor
54
+ * teardown traceback.
55
+ *
56
+ * The echo filter above only affects what a human sees. The CLASSIFIER
57
+ * reads the raw capture, and it needs the same recognition to tell "the
58
+ * suite finished clean and then upstream fell over at shutdown" from "the
59
+ * suite did not finish". Deliberately the same two-signal test the echo
60
+ * filter applies — a novel attribute or a traceback from anywhere but
61
+ * `resourcemonitor.py` is NOT this incident and must keep failing the run.
62
+ *
63
+ * The shutdown-marker precondition the echo filter adds is intentionally
64
+ * omitted: the whole point at the classification layer is the case where
65
+ * the teardown crash prevented the shutdown marker from printing.
66
+ *
67
+ * Pure; exported for the classifier and for direct unit testing.
68
+ *
69
+ * @param output - Raw captured stdout/stderr
70
+ * @returns True when the recognized teardown traceback is present
71
+ */
72
+ export function hasKnownTeardownNoise(output) {
73
+ return isRecognizedTeardownNoise(output);
74
+ }
52
75
  /** Fresh per-run context — hand the same instance to both stream filters. */
53
76
  export function createTeardownNoiseContext() {
54
77
  return { shutdownSeen: false };
@@ -1,9 +1,10 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { join } from 'node:path';
3
3
  import { MozconfigError } from '../errors/build.js';
4
- import { pathExists, readText, writeText } from '../utils/fs.js';
4
+ import { pathExists, readText, writeTextIfChanged } from '../utils/fs.js';
5
+ import { verbose } from '../utils/logger.js';
5
6
  import { getPlatform } from '../utils/platform.js';
6
- import { BrandingMozconfigMismatchError } from './branding.js';
7
+ import { BrandingMozconfigMismatchError, splitAppId } from './branding.js';
7
8
  /**
8
9
  * Replaces template variables in a string.
9
10
  * @param content - Content with ${variable} placeholders
@@ -95,6 +96,14 @@ export async function generateMozconfig(configsDir, engineDir, config) {
95
96
  binaryName: config.binaryName,
96
97
  };
97
98
  let content = '';
99
+ // Bundle identity: branding configure.sh carries only the LEAF of appId
100
+ // (see splitAppId in branding.ts — upstream composes the mac bundle id as
101
+ // <distribution-id>.<MOZ_MACBUNDLE_ID>); the prefix travels here so the
102
+ // two halves can never drift apart.
103
+ const { distributionId } = splitAppId(config.appId);
104
+ content +=
105
+ `# FireForge identity (generated from fireforge.json appId)\n` +
106
+ `ac_add_options --with-distribution-id=${distributionId}\n\n`;
98
107
  // Read common config if it exists
99
108
  if (await pathExists(commonPath)) {
100
109
  const commonContent = await readText(commonPath);
@@ -106,7 +115,16 @@ export async function generateMozconfig(configsDir, engineDir, config) {
106
115
  }
107
116
  const platformContent = await readText(platformPath);
108
117
  content += `# Platform configuration (${platform})\n${replaceVariables(platformContent, variables)}`;
109
- await writeText(outputPath, content);
118
+ // Write-if-changed: `mozconfig` is a mach CONFIGURE INPUT, so
119
+ // its mtime — not its content — is what `config.status` compares against.
120
+ // Rewriting a byte-identical file on every build-capable invocation made
121
+ // mach re-run `configure` plus backend regeneration ("Backend config
122
+ // changed; N files touched") on EVERY run, each run re-arming the next.
123
+ // Only touch the file when the rendered content actually differs.
124
+ const wrote = await writeTextIfChanged(outputPath, content);
125
+ if (!wrote) {
126
+ verbose(`mozconfig already up to date (content unchanged): ${outputPath}`);
127
+ }
110
128
  // Preflight: the mozconfig we just wrote must reference the branding
111
129
  // directory FireForge actually set up. Catching the drift here (after the
112
130
  // write, before anything consumes mozconfig) keeps `generateMozconfig`
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Engine-aware resolution preflight for queue-owned system modules.
3
+ *
4
+ * An `.sys.mjs` that a packaged module imports but whose `EXTRA_JS_MODULES`
5
+ * registration never landed kills every xpcshell suite
6
+ * with `xpcshell return code: -11` and ZERO output — no import error, no
7
+ * stack, nothing. The 0.41.0 `unregistered-system-module` check catches
8
+ * this class, but only in one shape: a module NEWLY CREATED by a patch in
9
+ * the projected queue. The class recurred through the other shape — three
10
+ * modules imported from an ALREADY-EXISTING module before their moz.build
11
+ * lines landed — and cost a full rebuild cycle before anyone recognised it.
12
+ *
13
+ * This preflight closes that gap by asking the question against the ENGINE
14
+ * rather than against a projected diff: for every `resource:///modules/…`
15
+ * specifier imported by any queue-owned module, does the module it names
16
+ * exist, and is it registered in a moz.build that covers it?
17
+ *
18
+ * Scope is the queue-owned file set on purpose. A specifier that resolves
19
+ * to no owned file is an UPSTREAM Firefox module, which FireForge does not
20
+ * police and cannot cheaply enumerate; policing only what the fork owns is
21
+ * both the actionable set and the one that produced every recorded
22
+ * incident.
23
+ */
24
+ /** Why a queue-owned module import does not resolve to a packaged module. */
25
+ export type UnresolvedModuleReason = 'missing-file' | 'unregistered';
26
+ /** One unresolvable or unregistered system-module import. */
27
+ export interface UnresolvedModuleImport {
28
+ /** Engine-relative path of the module the specifier names. */
29
+ module: string;
30
+ /** The `resource://` specifier as written. */
31
+ specifier: string;
32
+ /** Engine-relative paths of the files that import it, sorted. */
33
+ importers: string[];
34
+ reason: UnresolvedModuleReason;
35
+ }
36
+ /**
37
+ * Resolves every `resource://` system-module import made by the
38
+ * queue-owned files and reports the ones a packaged browser could not
39
+ * load.
40
+ *
41
+ * @param engineDir - Path to the engine checkout
42
+ * @param ownedFiles - Engine-relative paths the patch queue owns
43
+ * @returns Findings sorted by module path; empty when everything resolves
44
+ */
45
+ export declare function findUnresolvedSystemModuleImports(engineDir: string, ownedFiles: readonly string[]): Promise<UnresolvedModuleImport[]>;
46
+ /**
47
+ * Renders findings as operator-facing lines, naming the unregistered or
48
+ * unresolvable module, so the failure is a sentence instead of a bare
49
+ * `-11`.
50
+ */
51
+ export declare function formatUnresolvedSystemModuleImports(findings: readonly UnresolvedModuleImport[]): string[];
@@ -0,0 +1,155 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Engine-aware resolution preflight for queue-owned system modules.
4
+ *
5
+ * An `.sys.mjs` that a packaged module imports but whose `EXTRA_JS_MODULES`
6
+ * registration never landed kills every xpcshell suite
7
+ * with `xpcshell return code: -11` and ZERO output — no import error, no
8
+ * stack, nothing. The 0.41.0 `unregistered-system-module` check catches
9
+ * this class, but only in one shape: a module NEWLY CREATED by a patch in
10
+ * the projected queue. The class recurred through the other shape — three
11
+ * modules imported from an ALREADY-EXISTING module before their moz.build
12
+ * lines landed — and cost a full rebuild cycle before anyone recognised it.
13
+ *
14
+ * This preflight closes that gap by asking the question against the ENGINE
15
+ * rather than against a projected diff: for every `resource:///modules/…`
16
+ * specifier imported by any queue-owned module, does the module it names
17
+ * exist, and is it registered in a moz.build that covers it?
18
+ *
19
+ * Scope is the queue-owned file set on purpose. A specifier that resolves
20
+ * to no owned file is an UPSTREAM Firefox module, which FireForge does not
21
+ * police and cannot cheaply enumerate; policing only what the fork owns is
22
+ * both the actionable set and the one that produced every recorded
23
+ * incident.
24
+ */
25
+ import { dirname, join } from 'node:path';
26
+ import { toError } from '../utils/errors.js';
27
+ import { pathExists, readText } from '../utils/fs.js';
28
+ import { verbose } from '../utils/logger.js';
29
+ import { escapeRegex } from '../utils/regex.js';
30
+ import { extractResourceModuleSpecifiers, moduleSourceSuffix, } from './patch-lint-module-registration.js';
31
+ /** Extensions whose contents are scanned for resource-module imports. */
32
+ const IMPORTABLE_EXTENSIONS = ['.mjs', '.sys.mjs', '.js', '.jsm'];
33
+ /**
34
+ * Reads a file, returning undefined (with a verbose note) rather than
35
+ * throwing: a preflight must not fail a command over one unreadable path.
36
+ */
37
+ async function readIfPresent(path) {
38
+ try {
39
+ if (!(await pathExists(path)))
40
+ return undefined;
41
+ return await readText(path);
42
+ }
43
+ catch (error) {
44
+ verbose(`Module resolution preflight: could not read ${path} — ${toError(error).message}`);
45
+ return undefined;
46
+ }
47
+ }
48
+ /**
49
+ * True when some `moz.build` at or above `moduleRelPath`'s directory (up
50
+ * to, and including, the engine root) mentions the module's basename.
51
+ *
52
+ * Walking ancestors — rather than checking only the sibling `moz.build` —
53
+ * matches how Mozilla's build system actually works: a module list may sit
54
+ * in a parent directory's `moz.build` with a path-qualified entry. The
55
+ * check is a quoted-basename match for the same reason the queue-level
56
+ * rule uses one: parsing moz.build's Python is not worth it for a
57
+ * preflight, and a quoted basename has no meaningful false-positive shape.
58
+ */
59
+ async function isRegisteredInAncestorMozBuild(engineDir, moduleRelPath, mozBuildCache) {
60
+ const leaf = moduleRelPath.slice(moduleRelPath.lastIndexOf('/') + 1);
61
+ // The entry may be the bare basename (`"Foo.sys.mjs"`, sibling
62
+ // moz.build) or path-qualified relative to the moz.build that declares
63
+ // it (`"sub/Foo.sys.mjs"`, ancestor moz.build). Both are the same
64
+ // registration; anchor on the closing quote and require a path boundary
65
+ // so `NotFoo.sys.mjs` cannot satisfy `Foo.sys.mjs`.
66
+ const quotedLeaf = new RegExp(`["'](?:[^"']*/)?${escapeRegex(leaf)}["']`);
67
+ let dir = dirname(moduleRelPath);
68
+ for (;;) {
69
+ const key = dir === '.' ? '' : dir;
70
+ if (!mozBuildCache.has(key)) {
71
+ mozBuildCache.set(key, await readIfPresent(join(engineDir, key, 'moz.build')));
72
+ }
73
+ const content = mozBuildCache.get(key);
74
+ if (content !== undefined && quotedLeaf.test(content))
75
+ return true;
76
+ if (dir === '.' || dir === '' || dir === '/')
77
+ return false;
78
+ const parent = dirname(dir);
79
+ if (parent === dir)
80
+ return false;
81
+ dir = parent;
82
+ }
83
+ }
84
+ /**
85
+ * Resolves every `resource://` system-module import made by the
86
+ * queue-owned files and reports the ones a packaged browser could not
87
+ * load.
88
+ *
89
+ * @param engineDir - Path to the engine checkout
90
+ * @param ownedFiles - Engine-relative paths the patch queue owns
91
+ * @returns Findings sorted by module path; empty when everything resolves
92
+ */
93
+ export async function findUnresolvedSystemModuleImports(engineDir, ownedFiles) {
94
+ const owned = [...new Set(ownedFiles)];
95
+ /** specifier → importing owned files. */
96
+ const importersBySpecifier = new Map();
97
+ for (const file of owned) {
98
+ if (!IMPORTABLE_EXTENSIONS.some((extension) => file.endsWith(extension)))
99
+ continue;
100
+ const content = await readIfPresent(join(engineDir, file));
101
+ if (content === undefined)
102
+ continue;
103
+ for (const specifier of extractResourceModuleSpecifiers(content)) {
104
+ const importers = importersBySpecifier.get(specifier) ?? new Set();
105
+ importers.add(file);
106
+ importersBySpecifier.set(specifier, importers);
107
+ }
108
+ }
109
+ const mozBuildCache = new Map();
110
+ const findings = [];
111
+ for (const [specifier, importers] of importersBySpecifier) {
112
+ const suffix = moduleSourceSuffix(specifier);
113
+ if (suffix === undefined)
114
+ continue;
115
+ // Only fork-owned targets are policed; anything else is upstream.
116
+ const target = owned.find((file) => file.endsWith(suffix));
117
+ if (target === undefined)
118
+ continue;
119
+ const sortedImporters = [...importers].sort((a, b) => a.localeCompare(b));
120
+ if (!(await pathExists(join(engineDir, target)))) {
121
+ findings.push({
122
+ module: target,
123
+ specifier,
124
+ importers: sortedImporters,
125
+ reason: 'missing-file',
126
+ });
127
+ continue;
128
+ }
129
+ if (!(await isRegisteredInAncestorMozBuild(engineDir, target, mozBuildCache))) {
130
+ findings.push({
131
+ module: target,
132
+ specifier,
133
+ importers: sortedImporters,
134
+ reason: 'unregistered',
135
+ });
136
+ }
137
+ }
138
+ // Deterministic order regardless of Map iteration or owned-file order.
139
+ return findings.sort((a, b) => a.module.localeCompare(b.module) || a.specifier.localeCompare(b.specifier));
140
+ }
141
+ /**
142
+ * Renders findings as operator-facing lines, naming the unregistered or
143
+ * unresolvable module, so the failure is a sentence instead of a bare
144
+ * `-11`.
145
+ */
146
+ export function formatUnresolvedSystemModuleImports(findings) {
147
+ return findings.map((finding) => finding.reason === 'missing-file'
148
+ ? `${finding.module}: imported as ${finding.specifier} by ${finding.importers.join(', ')}, ` +
149
+ 'but the file does not exist in engine/. A packaged browser cannot load it; xpcshell ' +
150
+ 'dies on SIGSEGV with no output.'
151
+ : `${finding.module}: imported as ${finding.specifier} by ${finding.importers.join(', ')}, ` +
152
+ 'but no moz.build at or above its directory registers it. Add it to EXTRA_JS_MODULES ' +
153
+ '(or the appropriate namespaced module list) — until then a packaged browser cannot ' +
154
+ 'load it and xpcshell dies on SIGSEGV with no output.');
155
+ }
@@ -8,6 +8,7 @@ import { discoverPatches, withPatchDirectoryLock } from './patch-apply.js';
8
8
  import { normalizePatchArtifact } from './patch-artifact-normalize.js';
9
9
  import { findAllPatchesForFilesWithDetails, } from './patch-export-coverage.js';
10
10
  import { addPatchToManifest, loadPatchesManifestForWrite, PATCHES_MANIFEST, savePatchesManifest, } from './patch-manifest.js';
11
+ import { requirePatchOrder } from './patch-parse.js';
11
12
  import { allocatePolicyOrder, enforcePatchPolicy } from './patch-policy.js';
12
13
  export { findAllPatchesForFiles, findAllPatchesForFilesWithDetails, findSupersededPatches, isPatchFullyCovered, } from './patch-export-coverage.js';
13
14
  import { escapeRegex } from '../utils/regex.js';
@@ -264,7 +265,7 @@ async function computeExportPlanUnderLock(input) {
264
265
  const patchPath = join(input.patchesDir, patchFilename);
265
266
  const metadata = {
266
267
  filename: patchFilename,
267
- order: parseInt(patchFilename.split('-')[0] ?? '0', 10),
268
+ order: requirePatchOrder(patchFilename),
268
269
  category: input.category,
269
270
  name: input.name,
270
271
  description: input.description,
@@ -18,6 +18,11 @@
18
18
  */
19
19
  /** Maximum Levenshtein distance accepted as a "did you mean" suggestion. */
20
20
  const SUGGESTION_DISTANCE_THRESHOLD = 3;
21
+ /**
22
+ * Shortest side accepted for a prefix relation. Below this a prefix match
23
+ * is noise: a one-character ordinal prefixes almost anything.
24
+ */
25
+ const MIN_PREFIX_MATCH_LENGTH = 4;
21
26
  /** Maximum number of suggestions to surface in the error message. */
22
27
  const SUGGESTION_LIMIT = 3;
23
28
  /**
@@ -83,10 +88,31 @@ function collectAcceptedIdentifiers(patches) {
83
88
  * candidate whose distance exceeds {@link SUGGESTION_DISTANCE_THRESHOLD}.
84
89
  */
85
90
  function rankSuggestions(identifier, candidates) {
91
+ const needle = identifier.toLowerCase();
86
92
  return candidates
87
- .map((candidate) => ({ candidate, distance: levenshtein(identifier, candidate) }))
88
- .filter((entry) => entry.distance <= SUGGESTION_DISTANCE_THRESHOLD)
89
- .sort((a, b) => a.distance - b.distance || a.candidate.localeCompare(b.candidate))
93
+ .map((candidate) => {
94
+ const lower = candidate.toLowerCase();
95
+ // Prefix relation in EITHER direction is a stronger signal than edit
96
+ // distance and is not bounded by it: a partial ordinal or
97
+ // an abbreviated slug ("ui-foo" for "002-ui-foo.patch") is many edits
98
+ // away from the full identifier yet is obviously the intended target.
99
+ // `lower.startsWith(needle)`: the operator typed an abbreviation of
100
+ // a real identifier. The reverse relation is only meaningful when
101
+ // the candidate is substantial — otherwise the single-character
102
+ // ordinal "9" would "match" every input starting with a 9.
103
+ const prefixMatch = needle.length >= MIN_PREFIX_MATCH_LENGTH &&
104
+ (lower.startsWith(needle) ||
105
+ (lower.length >= MIN_PREFIX_MATCH_LENGTH && needle.startsWith(lower)));
106
+ return {
107
+ candidate,
108
+ prefixMatch,
109
+ distance: levenshtein(identifier, candidate),
110
+ };
111
+ })
112
+ .filter((entry) => entry.prefixMatch || entry.distance <= SUGGESTION_DISTANCE_THRESHOLD)
113
+ .sort((a, b) => Number(b.prefixMatch) - Number(a.prefixMatch) ||
114
+ a.distance - b.distance ||
115
+ a.candidate.localeCompare(b.candidate))
90
116
  .slice(0, SUGGESTION_LIMIT)
91
117
  .map((entry) => entry.candidate);
92
118
  }
@@ -32,7 +32,7 @@ export interface CheckJsMode {
32
32
  compilerOptions?: PatchLintCheckJsCompilerOptions;
33
33
  /**
34
34
  * How to report undefined free identifiers (TS2304/TS2552). Default
35
- * 'warning' — see `patchLint.undefinedIdentifiers` (FORGE F12).
35
+ * 'warning' — see `patchLint.undefinedIdentifiers`.
36
36
  */
37
37
  undefinedIdentifiers?: PatchLintSeverityGate;
38
38
  }
@@ -68,7 +68,7 @@ export interface GroupedCheckJsResult {
68
68
  * against the real owning sources while unrelated owned files are never
69
69
  * parsed. `.mjs` files are module-scoped, so a root's diagnostics are
70
70
  * identical whether other owned files are roots or mere resolution
71
- * targets (pinned by the rootScope parity test — FORGE J1c).
71
+ * targets (pinned by the rootScope parity test).
72
72
  * @returns Diagnostics grouped per owning file plus run-level errors
73
73
  */
74
74
  export declare function runCheckJsGrouped(repoDir: string, resolutionOwned: Set<string>, extraShimPath?: string, projectRoot?: string, mode?: CheckJsMode, builtinShimSuffix?: string, rootScope?: ReadonlySet<string>): Promise<GroupedCheckJsResult>;
@@ -102,7 +102,7 @@ export declare function runCheckJs(repoDir: string, patchOwnedFiles: Set<string>
102
102
  */
103
103
  export declare function invokePatchLintCheckJs(repoDir: string, patchOwnedFiles: Set<string>, patchLint: PatchLintConfig, projectRoot: string, reportScope?: ReadonlySet<string>): Promise<PatchLintIssue[]>;
104
104
  /**
105
- * Runs the checkJs pass over patch-owned test `.js` files (FORGE G5,
105
+ * Runs the checkJs pass over patch-owned test `.js` files (
106
106
  * `patchLint.checkJsTestFiles`). Each test file gets its OWN small
107
107
  * program — mochitest scripts share top-level scope only with their
108
108
  * directory's `head*.js` helpers, so one multi-script program would emit
@@ -131,7 +131,7 @@ export declare function runCheckJsTestFilesGrouped(repoDir: string, testFiles: S
131
131
  * @param patchLint - The resolved `patchLint` config block
132
132
  * @param projectRoot - FireForge project root for shim resolution
133
133
  * @param rootScope - Optional subset of files to use as program roots
134
- * (`--patches`); resolution still spans the whole queue (FORGE J1c)
134
+ * (`--patches`); resolution still spans the whole queue
135
135
  */
136
136
  export declare function invokePatchLintCheckJsGrouped(repoDir: string, patchOwnedFiles: Set<string>, patchLint: PatchLintConfig, projectRoot: string, rootScope?: ReadonlySet<string>): Promise<GroupedCheckJsResult>;
137
137
  /**
@@ -141,7 +141,7 @@ export declare function invokePatchLintCheckJsGrouped(repoDir: string, patchOwne
141
141
  * when `checkJsTestFiles` is on). The built program itself never
142
142
  * contributes globals (see {@link runCheckJsGrouped}), so a warm
143
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
144
+ * invariant with this probe instead of building the whole
145
145
  * TypeScript program. Issue objects are byte-identical to the build
146
146
  * path's, deduplicated by message like {@link runCheckJsTestFilesGrouped}.
147
147
  */
@@ -154,7 +154,7 @@ function createPathsResolver(ts, paths, baseDir, fileExists, onResolved) {
154
154
  * against the real owning sources while unrelated owned files are never
155
155
  * parsed. `.mjs` files are module-scoped, so a root's diagnostics are
156
156
  * identical whether other owned files are roots or mere resolution
157
- * targets (pinned by the rootScope parity test — FORGE J1c).
157
+ * targets (pinned by the rootScope parity test).
158
158
  * @returns Diagnostics grouped per owning file plus run-level errors
159
159
  */
160
160
  export async function runCheckJsGrouped(repoDir, resolutionOwned, extraShimPath, projectRoot, mode, builtinShimSuffix, rootScope) {
@@ -206,7 +206,7 @@ export async function runCheckJsGrouped(repoDir, resolutionOwned, extraShimPath,
206
206
  let shimSource;
207
207
  try {
208
208
  const composed = await composeShimSource(projectRoot ?? repoDir, extraShimPath);
209
- // The suffix (e.g. the loose test-harness baseline, FORGE G5) goes
209
+ // The suffix (e.g. the loose test-harness baseline) goes
210
210
  // AFTER the consumer's extra shim: TypeScript resolves conflicting
211
211
  // `declare var` redeclarations to the FIRST declaration, so a typed
212
212
  // consumer declaration must precede the loose fallback to win.
@@ -420,7 +420,7 @@ export async function invokePatchLintCheckJs(repoDir, patchOwnedFiles, patchLint
420
420
  return runCheckJs(repoDir, patchOwnedFiles, patchLint.checkJsExtraShim, projectRoot, modeFromPatchLintConfig(patchLint), reportScope);
421
421
  }
422
422
  /**
423
- * Runs the checkJs pass over patch-owned test `.js` files (FORGE G5,
423
+ * Runs the checkJs pass over patch-owned test `.js` files (
424
424
  * `patchLint.checkJsTestFiles`). Each test file gets its OWN small
425
425
  * program — mochitest scripts share top-level scope only with their
426
426
  * directory's `head*.js` helpers, so one multi-script program would emit
@@ -446,7 +446,7 @@ export async function runCheckJsTestFilesGrouped(repoDir, testFiles, patchLint,
446
446
  for (const file of files) {
447
447
  // Under a rootScope only the scoped files get their own program, but
448
448
  // head.js helper discovery still spans the full owned set so a scoped
449
- // test keeps its cross-patch harness globals (FORGE J1c).
449
+ // test keeps its cross-patch harness globals.
450
450
  if (rootScope !== undefined && !rootScope.has(file))
451
451
  continue;
452
452
  const dir = file.slice(0, file.lastIndexOf('/') + 1);
@@ -498,7 +498,7 @@ function modeFromPatchLintConfig(patchLint) {
498
498
  * @param patchLint - The resolved `patchLint` config block
499
499
  * @param projectRoot - FireForge project root for shim resolution
500
500
  * @param rootScope - Optional subset of files to use as program roots
501
- * (`--patches`); resolution still spans the whole queue (FORGE J1c)
501
+ * (`--patches`); resolution still spans the whole queue
502
502
  */
503
503
  export async function invokePatchLintCheckJsGrouped(repoDir, patchOwnedFiles, patchLint, projectRoot, rootScope) {
504
504
  return runCheckJsGrouped(repoDir, patchOwnedFiles, patchLint.checkJsExtraShim, projectRoot, modeFromPatchLintConfig(patchLint), undefined, rootScope);
@@ -510,7 +510,7 @@ export async function invokePatchLintCheckJsGrouped(repoDir, patchOwnedFiles, pa
510
510
  * when `checkJsTestFiles` is on). The built program itself never
511
511
  * contributes globals (see {@link runCheckJsGrouped}), so a warm
512
512
  * all-cache-hit run can satisfy the "warm never reports less than cold"
513
- * invariant (FORGE F5) with this probe instead of building the whole
513
+ * invariant with this probe instead of building the whole
514
514
  * TypeScript program. Issue objects are byte-identical to the build
515
515
  * path's, deduplicated by message like {@link runCheckJsTestFilesGrouped}.
516
516
  */
@@ -3,7 +3,7 @@
3
3
  * `patch-lint-cross.ts` (at the per-file line budget). Re-exported from
4
4
  * there so callers keep importing from `patch-lint.ts` unchanged.
5
5
  *
6
- * Memoised per context object (FORGE J1): ownership resolution and
6
+ * Memoised per context object: ownership resolution and
7
7
  * cache-key fingerprints call this 2-3× per linted patch, and each call
8
8
  * used to re-run `detectNewFilesInDiff` over EVERY queue entry's diff.
9
9
  * Keyed weakly so a discarded context frees its map; a caller that
@@ -4,7 +4,7 @@
4
4
  * `patch-lint-cross.ts` (at the per-file line budget). Re-exported from
5
5
  * there so callers keep importing from `patch-lint.ts` unchanged.
6
6
  *
7
- * Memoised per context object (FORGE J1): ownership resolution and
7
+ * Memoised per context object: ownership resolution and
8
8
  * cache-key fingerprints call this 2-3× per linted patch, and each call
9
9
  * used to re-run `detectNewFilesInDiff` over EVERY queue entry's diff.
10
10
  * Keyed weakly so a discarded context frees its map; a caller that
@@ -62,7 +62,7 @@ export interface PatchQueueContext {
62
62
  * Optional patchPolicy config. When present, the forward-import rule
63
63
  * validates its "Closest legal ordinal" suggestion against the importing
64
64
  * patch's category range and suppresses it when no legal ordinal exists
65
- * (FORGE F14). Absent = current behavior (hint always printed).
65
+ *. Absent = current behavior (hint always printed).
66
66
  */
67
67
  patchPolicy?: PatchPolicyConfig;
68
68
  }
@@ -85,7 +85,7 @@ export async function buildPatchQueueContext(patchesDir, config) {
85
85
  }
86
86
  // The creators map (split out of `lintPatchQueueDuplicateCreations` so
87
87
  // `status --ownership` can consume the structured data) lives in
88
- // `patch-lint-creators.ts` with its per-context memoisation (FORGE J1);
88
+ // `patch-lint-creators.ts` with its per-context memoisation;
89
89
  // re-exported here so callers keep importing from `patch-lint.ts`.
90
90
  export { collectNewFileCreatorsByPath, invalidateNewFileCreatorsCache, } from './patch-lint-creators.js';
91
91
  /**
@@ -276,7 +276,7 @@ export function extractImportSpecifiersWithLines(source) {
276
276
  * `modifiedFileAdditions`: a patch that inserts one property into an
277
277
  * existing getter object never carries the `defineESModuleGetters(` opener
278
278
  * in its added lines, so the balanced-brace walk in
279
- * {@link extractImportSpecifiersWithLines} cannot see it (FORGE F3). The
279
+ * {@link extractImportSpecifiersWithLines} cannot see it. The
280
280
  * URL-scheme restriction keeps ordinary object literals
281
281
  * (`label: "Foo.sys.mjs"`) out; the leaf-extension filter downstream does
282
282
  * the rest.
@@ -474,7 +474,7 @@ export function lintPatchQueueForwardImports(ctx) {
474
474
  // when the only fix is reordering. When patchPolicy is available and
475
475
  // that ordinal falls outside the importing patch's category range,
476
476
  // `patch reorder` would refuse it — suppress the hint and point at
477
- // the staged-dependency remedy instead (FORGE F14).
477
+ // the staged-dependency remedy instead.
478
478
  const suggestedOrder = Math.max(...laterOwners.map((o) => o.order)) + 1;
479
479
  const category = entry.metadata?.category ?? /^\d+-([a-z]+)-/.exec(entry.filename)?.[1];
480
480
  let ordinalHint = `Closest legal ordinal that satisfies this dependency: ${suggestedOrder}.`;
@@ -487,7 +487,7 @@ export function lintPatchQueueForwardImports(ctx) {
487
487
  'creating patch(es); declaring the staged dependency is the recommended remedy.';
488
488
  }
489
489
  // The exact per-owner invocation turns the refusal into a paste-and-run
490
- // remedy instead of a flag-discovery exercise (FORGE F3).
490
+ // remedy instead of a flag-discovery exercise.
491
491
  const stagedCommands = laterOwners
492
492
  .map((o) => `fireforge patch staged-dependency ${entry.filename} --add ` +
493
493
  `--file ${sitePath} --specifier "${specifier}" ` +