@hominis/fireforge 0.42.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 (199) hide show
  1. package/CHANGELOG.md +75 -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/browser-wire.js +9 -1
  73. package/dist/src/core/build-baseline-types.d.ts +1 -1
  74. package/dist/src/core/build-baseline.d.ts +1 -1
  75. package/dist/src/core/build-baseline.js +1 -1
  76. package/dist/src/core/build-overwrite-guard.d.ts +49 -0
  77. package/dist/src/core/build-overwrite-guard.js +112 -0
  78. package/dist/src/core/build-prepare.d.ts +13 -5
  79. package/dist/src/core/build-prepare.js +42 -9
  80. package/dist/src/core/config-mutate.d.ts +3 -2
  81. package/dist/src/core/config-mutate.js +5 -2
  82. package/dist/src/core/config.d.ts +3 -2
  83. package/dist/src/core/config.js +2 -0
  84. package/dist/src/core/coverage-extend.d.ts +1 -1
  85. package/dist/src/core/coverage-extend.js +1 -1
  86. package/dist/src/core/destructive.d.ts +23 -0
  87. package/dist/src/core/destructive.js +45 -4
  88. package/dist/src/core/diff-hunks.js +31 -17
  89. package/dist/src/core/discard-baseline.d.ts +1 -1
  90. package/dist/src/core/discard-baseline.js +1 -1
  91. package/dist/src/core/display-state.d.ts +52 -0
  92. package/dist/src/core/display-state.js +89 -0
  93. package/dist/src/core/engine-session-lock.d.ts +20 -0
  94. package/dist/src/core/engine-session-lock.js +84 -14
  95. package/dist/src/core/export-moved-code-shape.d.ts +60 -0
  96. package/dist/src/core/export-moved-code-shape.js +129 -0
  97. package/dist/src/core/file-lock.d.ts +44 -0
  98. package/dist/src/core/file-lock.js +157 -42
  99. package/dist/src/core/furnace-apply-helpers.d.ts +1 -1
  100. package/dist/src/core/furnace-apply-helpers.js +1 -1
  101. package/dist/src/core/furnace-apply-output.js +1 -1
  102. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +2 -2
  103. package/dist/src/core/furnace-apply-overwrite-warn.js +2 -2
  104. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +1 -1
  105. package/dist/src/core/furnace-apply-undeploy-actions.js +1 -1
  106. package/dist/src/core/furnace-apply.js +13 -1
  107. package/dist/src/core/furnace-config-custom.d.ts +2 -1
  108. package/dist/src/core/furnace-config-order.d.ts +2 -1
  109. package/dist/src/core/furnace-config-order.js +9 -4
  110. package/dist/src/core/furnace-config-validate.d.ts +4 -3
  111. package/dist/src/core/furnace-config-validate.js +3 -3
  112. package/dist/src/core/furnace-config.js +5 -0
  113. package/dist/src/core/furnace-operation.d.ts +12 -0
  114. package/dist/src/core/furnace-operation.js +125 -1
  115. package/dist/src/core/furnace-rollback.d.ts +17 -0
  116. package/dist/src/core/furnace-rollback.js +35 -1
  117. package/dist/src/core/git-base.js +7 -2
  118. package/dist/src/core/git-diff.js +11 -2
  119. package/dist/src/core/git-readonly-index.d.ts +59 -0
  120. package/dist/src/core/git-readonly-index.js +137 -0
  121. package/dist/src/core/license-headers.d.ts +1 -1
  122. package/dist/src/core/license-headers.js +1 -1
  123. package/dist/src/core/lint-cache.d.ts +15 -5
  124. package/dist/src/core/lint-cache.js +35 -8
  125. package/dist/src/core/mach-known-noise-filter.d.ts +21 -0
  126. package/dist/src/core/mach-known-noise-filter.js +23 -0
  127. package/dist/src/core/mach-mozconfig.js +12 -2
  128. package/dist/src/core/module-resolution-preflight.d.ts +51 -0
  129. package/dist/src/core/module-resolution-preflight.js +155 -0
  130. package/dist/src/core/patch-export.js +2 -1
  131. package/dist/src/core/patch-identifier-suggest.js +29 -3
  132. package/dist/src/core/patch-lint-checkjs.d.ts +5 -5
  133. package/dist/src/core/patch-lint-checkjs.js +6 -6
  134. package/dist/src/core/patch-lint-creators.d.ts +1 -1
  135. package/dist/src/core/patch-lint-creators.js +1 -1
  136. package/dist/src/core/patch-lint-cross.d.ts +1 -1
  137. package/dist/src/core/patch-lint-cross.js +4 -4
  138. package/dist/src/core/patch-lint-css.js +1 -1
  139. package/dist/src/core/patch-lint-diff.d.ts +1 -1
  140. package/dist/src/core/patch-lint-diff.js +1 -1
  141. package/dist/src/core/patch-lint-jsdoc.js +4 -2
  142. package/dist/src/core/patch-lint-module-registration.d.ts +13 -0
  143. package/dist/src/core/patch-lint-module-registration.js +13 -4
  144. package/dist/src/core/patch-lint-mozbuild.d.ts +1 -1
  145. package/dist/src/core/patch-lint-mozbuild.js +1 -1
  146. package/dist/src/core/patch-lint-ownership.d.ts +2 -2
  147. package/dist/src/core/patch-lint-ownership.js +2 -2
  148. package/dist/src/core/patch-lint.d.ts +2 -2
  149. package/dist/src/core/patch-lint.js +3 -3
  150. package/dist/src/core/patch-lock.d.ts +21 -0
  151. package/dist/src/core/patch-lock.js +29 -2
  152. package/dist/src/core/patch-manifest-io.d.ts +2 -1
  153. package/dist/src/core/patch-manifest-io.js +49 -0
  154. package/dist/src/core/patch-parse.d.ts +18 -0
  155. package/dist/src/core/patch-parse.js +24 -1
  156. package/dist/src/core/patch-policy.d.ts +2 -2
  157. package/dist/src/core/patch-policy.js +2 -2
  158. package/dist/src/core/smoke-patterns.d.ts +1 -1
  159. package/dist/src/core/status-binary.d.ts +1 -1
  160. package/dist/src/core/status-binary.js +1 -1
  161. package/dist/src/core/status-classify.d.ts +1 -1
  162. package/dist/src/core/status-classify.js +2 -2
  163. package/dist/src/core/test-harness-crash.d.ts +69 -3
  164. package/dist/src/core/test-harness-crash.js +174 -9
  165. package/dist/src/core/test-stale-check.d.ts +1 -1
  166. package/dist/src/core/test-stale-check.js +1 -1
  167. package/dist/src/core/token-category.js +1 -1
  168. package/dist/src/core/token-manager.d.ts +1 -1
  169. package/dist/src/core/token-manager.js +5 -5
  170. package/dist/src/core/tree-cow.js +1 -1
  171. package/dist/src/core/tree-guard.js +2 -2
  172. package/dist/src/core/tree-store.js +1 -1
  173. package/dist/src/core/typecheck-shim.d.ts +2 -2
  174. package/dist/src/core/typecheck-shim.js +2 -2
  175. package/dist/src/core/typecheck.js +10 -0
  176. package/dist/src/core/xpcshell-profile-dir.js +1 -1
  177. package/dist/src/errors/base.d.ts +22 -1
  178. package/dist/src/errors/base.js +32 -1
  179. package/dist/src/errors/codes.d.ts +10 -0
  180. package/dist/src/errors/codes.js +10 -0
  181. package/dist/src/types/commands/options.d.ts +27 -11
  182. package/dist/src/types/commands/patches.d.ts +1 -1
  183. package/dist/src/types/config.d.ts +2 -2
  184. package/dist/src/types/furnace.d.ts +1 -1
  185. package/dist/src/types/json.d.ts +22 -0
  186. package/dist/src/types/json.js +11 -0
  187. package/dist/src/utils/assert.d.ts +46 -0
  188. package/dist/src/utils/assert.js +83 -0
  189. package/dist/src/utils/build-info.d.ts +1 -1
  190. package/dist/src/utils/build-info.js +1 -1
  191. package/dist/src/utils/logger.d.ts +20 -0
  192. package/dist/src/utils/logger.js +26 -0
  193. package/dist/src/utils/options.d.ts +12 -0
  194. package/dist/src/utils/options.js +39 -0
  195. package/dist/src/utils/stdio-drain.d.ts +1 -1
  196. package/dist/src/utils/stdio-drain.js +1 -1
  197. package/dist/src/utils/validation.d.ts +12 -1
  198. package/dist/src/utils/validation.js +13 -1
  199. package/package.json +3 -3
@@ -1,7 +1,6 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
- * Foreign-drift preview + `--refuse-foreign-drift` gate for `re-export`
4
- * (FORGE J2).
3
+ * Foreign-drift preview + `--refuse-foreign-drift` gate for `re-export`.
5
4
  *
6
5
  * A scan-less re-export rebuilds each patch body as a whole-file
7
6
  * `git diff HEAD` over the owned files — so a CONCURRENT session's
@@ -21,6 +20,7 @@
21
20
  * payload lines — because a refresh legitimately shifts hunk offsets and
22
21
  * context without changing what the patch does.
23
22
  */
23
+ import { stat } from 'node:fs/promises';
24
24
  import { join } from 'node:path';
25
25
  import { parseDiffSections } from '../core/patch-parse.js';
26
26
  import { toError } from '../utils/errors.js';
@@ -160,7 +160,7 @@ export function computeForeignDrift(oldBody, newBody, previousFilesAffected) {
160
160
  * records a refusal and returns `true` so the caller skips the write.
161
161
  */
162
162
  export async function reportForeignDrift(args) {
163
- const { patch, patchesDir, newDiffContent, ctx } = args;
163
+ const { patch, patchesDir, engineDir, newDiffContent, ctx } = args;
164
164
  const oldPath = join(patchesDir, patch.filename);
165
165
  let oldBody;
166
166
  try {
@@ -181,16 +181,20 @@ export async function reportForeignDrift(args) {
181
181
  if (ctx.expectedDriftFiles.has(entry.file))
182
182
  ctx.expectedSeen.add(entry.file);
183
183
  }
184
+ const recentlyEdited = await findFilesEditedSinceLastExport(engineDir, oldPath, drift.map((d) => d.file));
184
185
  const totalLines = drift.reduce((sum, d) => sum + d.addedLines + d.removedLines, 0);
185
186
  const lineNoun = totalLines === 1 ? 'line' : 'lines';
186
187
  warn(`${patch.filename}: refreshed body absorbs ${String(totalLines)} ${lineNoun} not present in the old patch body (${String(drift.length)} file(s)):`);
187
188
  for (const entry of drift.slice(0, MAX_FILES_REPORTED)) {
188
189
  const expectedTag = ctx.expectedDriftFiles.has(entry.file) ? ' (expected via --expect)' : '';
190
+ const authorshipTag = recentlyEdited.has(entry.file)
191
+ ? ' [edited since your last export]'
192
+ : ' [unchanged since your last export — another session may own it]';
189
193
  if (entry.binaryChanged) {
190
- info(` ${entry.file}: binary content changed${expectedTag}`);
194
+ info(` ${entry.file}: binary content changed${expectedTag}${authorshipTag}`);
191
195
  continue;
192
196
  }
193
- info(` ${entry.file}: +${String(entry.addedLines)}/-${String(entry.removedLines)} newly captured line(s)${expectedTag}`);
197
+ info(` ${entry.file}: +${String(entry.addedLines)}/-${String(entry.removedLines)} newly captured line(s)${expectedTag}${authorshipTag}`);
194
198
  for (const summary of entry.hunkSummaries) {
195
199
  info(` ${summary}`);
196
200
  }
@@ -212,12 +216,47 @@ export async function reportForeignDrift(args) {
212
216
  return true;
213
217
  }
214
218
  warn(' If these are intentional engine edits, this is the normal re-export flow. ' +
215
- 'FireForge cannot infer authorship; if another session owns them, re-run with ' +
216
- "--refuse-foreign-drift, or commit/stash that session's edits first.");
219
+ "FireForge cannot PROVE authorship; the per-file tags above compare each file's " +
220
+ 'modification time against this patch\'s last export, so "edited since your last ' +
221
+ 'export" marks lines you most likely wrote yourself. If another session owns them, ' +
222
+ "re-run with --refuse-foreign-drift, or commit/stash that session's edits first.");
217
223
  return false;
218
224
  }
219
225
  /**
220
- * Fail-closed baseline handling (FORGE L6): a missing or unreadable old
226
+ * Splits the drifting files into "you probably wrote this" and "this
227
+ * predates your last export". `--refuse-foreign-drift` calls every
228
+ * absorbed line "foreign" — including the operator's OWN additions
229
+ * from the same session — and "foreign" reads as "another session's",
230
+ * which is precisely the case where proceeding would be wrong. Authorship
231
+ * is unknowable, but RECENCY is not: a file whose mtime is newer than the
232
+ * patch body this run is refreshing changed after the last export, which
233
+ * on a single-operator slice is the operator's own edit. Returns the
234
+ * subset of `files` that qualifies; an unstattable path is reported as
235
+ * NOT recently edited, so the cautious wording is the fallback.
236
+ */
237
+ async function findFilesEditedSinceLastExport(engineDir, patchBodyPath, files) {
238
+ const recent = new Set();
239
+ let patchMtimeMs;
240
+ try {
241
+ patchMtimeMs = (await stat(patchBodyPath)).mtimeMs;
242
+ }
243
+ catch (error) {
244
+ verbose(`Could not stat ${patchBodyPath} for drift recency: ${toError(error).message}`);
245
+ return recent;
246
+ }
247
+ for (const file of files) {
248
+ try {
249
+ if ((await stat(join(engineDir, file))).mtimeMs > patchMtimeMs)
250
+ recent.add(file);
251
+ }
252
+ catch (error) {
253
+ verbose(`Could not stat ${file} for drift recency: ${toError(error).message}`);
254
+ }
255
+ }
256
+ return recent;
257
+ }
258
+ /**
259
+ * Fail-closed baseline handling: a missing or unreadable old
221
260
  * patch body means the drift comparison CANNOT run, so under
222
261
  * `--refuse-foreign-drift` the patch is refused rather than written on the
223
262
  * strength of a check that never happened. Without the flag this stays the
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Hoisted + cached lint context for `re-export` (FORGE J1).
2
+ * Hoisted + cached lint context for `re-export`.
3
3
  *
4
4
  * The per-patch loop used to rebuild the whole patch-queue context AND a
5
5
  * fresh queue-wide checkJs TypeScript program for every patch — ~37 s of
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * - builds the queue context and the {@link PerRunCheckJs} program
10
10
  * controller ONCE per invocation (`buildReExportLintContext`) and slices
11
- * per-patch findings out of it (the FORGE I3 shape `move-files`/`split`
11
+ * per-patch findings out of it (the shape `move-files`/`split`
12
12
  * already use, plus the program-level hoist `lint --per-patch` has);
13
13
  * - reuses the existing per-patch lint RESULT cache
14
14
  * (`.fireforge/lint-cache/per-patch-v1.json`) across invocations —
@@ -17,7 +17,7 @@
17
17
  * hashes engine HEAD, per-file content, patch body, config, shims, and
18
18
  * ownership, so a repeat single re-export is a warm hit (~4 s instead
19
19
  * of ~41 s). `--no-cache` opts out; run-level checkJs globals are
20
- * never cached (FORGE F5) and are emitted once per invocation;
20
+ * never cached and are emitted once per invocation;
21
21
  * - keeps the in-memory queue context honest after each write
22
22
  * (`refreshQueueCtxEntry`), so later iterations of an `--all` run lint
23
23
  * against the just-refreshed body instead of the stale one.
@@ -34,7 +34,7 @@ export interface ReExportLintContext {
34
34
  paths: ReturnType<typeof getProjectPaths>;
35
35
  config: FireForgeConfig;
36
36
  /** Whole-queue context, built WITH config so the forward-import hints
37
- * match `lint --per-patch` (FORGE F14); undefined when no patches dir. */
37
+ * match `lint --per-patch`; undefined when no patches dir. */
38
38
  patchQueueCtx: PatchQueueContext | undefined;
39
39
  /** Queue-wide checkJs controller; undefined when checkJs is off. */
40
40
  checkJs: PerRunCheckJs | undefined;
@@ -46,13 +46,15 @@ export interface ReExportLintContext {
46
46
  /** Run-level checkJs globals are emitted once per invocation. */
47
47
  globalsEmitted: boolean;
48
48
  }
49
- /** Builds the once-per-invocation lint context (FORGE J1). */
49
+ /** Builds the once-per-invocation lint context. */
50
50
  export declare function buildReExportLintContext(projectRoot: string, paths: ReturnType<typeof getProjectPaths>, config: Awaited<ReturnType<typeof loadConfig>>, noCache: boolean): Promise<ReExportLintContext>;
51
51
  /** Fresh lint payload carried to the post-write cache store. */
52
52
  export interface ReExportLintResult {
53
53
  issues: PatchLintIssue[];
54
54
  suppressed: PatchLintIssue[];
55
55
  lineCount: number;
56
+ /** Waiver ids in force for this lint run. */
57
+ lintIgnore: string[];
56
58
  }
57
59
  /**
58
60
  * Lints one re-exported patch body using the hoisted context: cache hit →
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
- * Hoisted + cached lint context for `re-export` (FORGE J1).
3
+ * Hoisted + cached lint context for `re-export`.
4
4
  *
5
5
  * The per-patch loop used to rebuild the whole patch-queue context AND a
6
6
  * fresh queue-wide checkJs TypeScript program for every patch — ~37 s of
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * - builds the queue context and the {@link PerRunCheckJs} program
11
11
  * controller ONCE per invocation (`buildReExportLintContext`) and slices
12
- * per-patch findings out of it (the FORGE I3 shape `move-files`/`split`
12
+ * per-patch findings out of it (the shape `move-files`/`split`
13
13
  * already use, plus the program-level hoist `lint --per-patch` has);
14
14
  * - reuses the existing per-patch lint RESULT cache
15
15
  * (`.fireforge/lint-cache/per-patch-v1.json`) across invocations —
@@ -18,7 +18,7 @@
18
18
  * hashes engine HEAD, per-file content, patch body, config, shims, and
19
19
  * ownership, so a repeat single re-export is a warm hit (~4 s instead
20
20
  * of ~41 s). `--no-cache` opts out; run-level checkJs globals are
21
- * never cached (FORGE F5) and are emitted once per invocation;
21
+ * never cached and are emitted once per invocation;
22
22
  * - keeps the in-memory queue context honest after each write
23
23
  * (`refreshQueueCtxEntry`), so later iterations of an `--all` run lint
24
24
  * against the just-refreshed body instead of the stale one.
@@ -33,7 +33,7 @@ import { pathExists } from '../utils/fs.js';
33
33
  import { info } from '../utils/logger.js';
34
34
  import { reportPatchLintOutcome } from './export-shared.js';
35
35
  import { buildPerRunCheckJs } from './lint-per-run-checkjs.js';
36
- /** Builds the once-per-invocation lint context (FORGE J1). */
36
+ /** Builds the once-per-invocation lint context. */
37
37
  export async function buildReExportLintContext(projectRoot, paths, config, noCache) {
38
38
  const patchQueueCtx = (await pathExists(paths.patches))
39
39
  ? await buildPatchQueueContext(paths.patches, config)
@@ -93,7 +93,7 @@ export async function lintReExportedPatch(args) {
93
93
  : 'Lint: branding threshold tier applied (patch is all under browser/branding/ plus registration siblings).');
94
94
  }
95
95
  // Run-level checkJs globals surface exactly once per invocation, never
96
- // from the cache (FORGE F5). On an all-warm run this is a cheap probe,
96
+ // from the cache. On an all-warm run this is a cheap probe,
97
97
  // not a program build.
98
98
  let globalIssues = [];
99
99
  if (checkJs && !lintCtx.globalsEmitted) {
@@ -111,7 +111,7 @@ export async function lintReExportedPatch(args) {
111
111
  queueContext: patchQueueCtx,
112
112
  ...(lintCtx.engineHeadSha === undefined ? {} : { engineHeadSha: lintCtx.engineHeadSha }),
113
113
  });
114
- const cached = getCachedPerPatchLintIssues(cache, patch.filename, cacheKey);
114
+ const cached = getCachedPerPatchLintIssues(cache, patch.filename, cacheKey, args.ignoreChecks);
115
115
  if (cached) {
116
116
  lintCtx.reusedCacheEntries++;
117
117
  reportPatchLintOutcome([...globalIssues, ...cached.issues], args.skipLint);
@@ -131,7 +131,7 @@ export async function lintReExportedPatch(args) {
131
131
  else {
132
132
  // Fresh `--scan` adoption created checkJs-relevant files the hoisted
133
133
  // program has never seen — fall back to a per-patch build with the
134
- // usual report scope so nothing is silently unchecked (FORGE F5).
134
+ // usual report scope so nothing is silently unchecked.
135
135
  lintOptions.checkJsReportScope = resolvePatchOwnedSysMjs(detectNewFilesInDiff(diffContent));
136
136
  }
137
137
  }
@@ -144,6 +144,7 @@ export async function lintReExportedPatch(args) {
144
144
  issues,
145
145
  suppressed: suppressedIssues,
146
146
  lineCount: countNonBinaryDiffLines(diffContent).textLines,
147
+ lintIgnore: [...(args.ignoreChecks ?? [])],
147
148
  };
148
149
  }
149
150
  /**
@@ -166,7 +167,7 @@ export async function storeReExportLintResult(lintCtx, patchFilename, projectedM
166
167
  queueContext: patchQueueCtx,
167
168
  ...(lintCtx.engineHeadSha === undefined ? {} : { engineHeadSha: lintCtx.engineHeadSha }),
168
169
  });
169
- setCachedPerPatchLintIssues(cache, patchFilename, cacheKey, result.issues, result.suppressed, result.lineCount);
170
+ setCachedPerPatchLintIssues(cache, patchFilename, cacheKey, result.issues, result.suppressed, result.lineCount, result.lintIgnore);
170
171
  lintCtx.cacheDirty = true;
171
172
  }
172
173
  /**
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Single-patch refresh core for `fireforge re-export`, split out of
3
- * `re-export.ts` (at the per-file line budget after the FORGE J1/J2
3
+ * `re-export.ts` (at the per-file line budget after the
4
4
  * hoisted-lint and foreign-drift wiring). The orchestrator loop calls
5
5
  * {@link reExportSinglePatchWithIndexLockRetry} per selected patch.
6
6
  */
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
3
  * Single-patch refresh core for `fireforge re-export`, split out of
4
- * `re-export.ts` (at the per-file line budget after the FORGE J1/J2
4
+ * `re-export.ts` (at the per-file line budget after the
5
5
  * hoisted-lint and foreign-drift wiring). The orchestrator loop calls
6
6
  * {@link reExportSinglePatchWithIndexLockRetry} per selected patch.
7
7
  */
@@ -133,7 +133,7 @@ async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, co
133
133
  warn(`Skipped ${patch.filename}: no changes (files unchanged from HEAD)`);
134
134
  return false;
135
135
  }
136
- // Foreign-drift preview + optional hard stop (FORGE J2): show which
136
+ // Foreign-drift preview + optional hard stop: show which
137
137
  // payload lines are about to enter the body that the old body did not
138
138
  // carry — the case where a concurrent session's uncommitted edits in
139
139
  // OWNED files would be silently absorbed. Always previews (including
@@ -141,6 +141,7 @@ async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, co
141
141
  if (await reportForeignDrift({
142
142
  patch,
143
143
  patchesDir: paths.patches,
144
+ engineDir: paths.engine,
144
145
  newDiffContent: diffContent,
145
146
  ctx: driftCtx,
146
147
  })) {
@@ -155,7 +156,7 @@ async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, co
155
156
  });
156
157
  // Lint against the ONCE-per-invocation hoisted context (queue context +
157
158
  // queue-wide checkJs program + per-patch result cache) instead of
158
- // rebuilding ~37 s of setup for every patch in the loop (FORGE J1).
159
+ // rebuilding ~37 s of setup for every patch in the loop.
159
160
  const projectedMetadata = { ...patch, ...updates };
160
161
  const lintResult = await lintReExportedPatch({
161
162
  lintCtx,
@@ -200,7 +201,7 @@ async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, co
200
201
  * leave body and `filesAffected` disagreeing), then keeps the run's
201
202
  * in-memory state honest: the queue context entry is refreshed and the
202
203
  * fresh lint result is stored keyed against the just-written body — the
203
- * state a repeat invocation will observe (FORGE J1) — and the in-memory
204
+ * state a repeat invocation will observe — and the in-memory
204
205
  * manifest row mirrors the on-disk write for later loop iterations
205
206
  * (notably `--all --scan`, where `getClaimedFiles` reads this manifest).
206
207
  */
@@ -3,6 +3,7 @@ import { multiselect } from '@clack/prompts';
3
3
  import { getProjectPaths, loadConfig } from '../core/config.js';
4
4
  import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
5
5
  import { isGitRepository } from '../core/git.js';
6
+ import { formatPatchNotFoundError } from '../core/patch-identifier-suggest.js';
6
7
  import { loadPatchesManifest, resolvePatchIdentifier, stampPatchVersions, } from '../core/patch-manifest.js';
7
8
  import { GeneralError, InvalidArgumentError } from '../errors/base.js';
8
9
  import { elapsedSince } from '../utils/elapsed.js';
@@ -24,8 +25,9 @@ async function resolveSelectedPatches(patches, options, manifest) {
24
25
  for (const identifier of patches) {
25
26
  const match = resolvePatchIdentifier(identifier, manifest.patches);
26
27
  if (!match) {
27
- const available = manifest.patches.map((p) => p.filename).join(', ');
28
- throw new InvalidArgumentError(`Patch "${identifier}" not found in manifest.\n\nAvailable patches: ${available}`, identifier);
28
+ // Suggest, never dump: a wrong guess used to print the
29
+ // whole ~300-entry manifest, burying the error it was reporting.
30
+ throw new InvalidArgumentError(formatPatchNotFoundError(identifier, manifest.patches), identifier);
29
31
  }
30
32
  selectedPatches.push(match);
31
33
  }
@@ -123,7 +125,7 @@ export async function reExportCommand(projectRoot, patches, options) {
123
125
  }
124
126
  const config = await loadConfig(projectRoot);
125
127
  // Classification inputs for the scan-less adjacency advisory, computed
126
- // once for the whole run (FORGE G2).
128
+ // once for the whole run.
127
129
  const adjacentCtx = {
128
130
  binaryName: config.binaryName,
129
131
  furnacePrefixes: await collectFurnaceManagedPrefixes(paths.root),
@@ -131,7 +133,7 @@ export async function reExportCommand(projectRoot, patches, options) {
131
133
  refusals: [],
132
134
  };
133
135
  const driftCtx = buildForeignDriftContext(options);
134
- // Hoisted lint context, one per run (FORGE J1): queue context + checkJs
136
+ // Hoisted lint context, one per run: queue context + checkJs
135
137
  // program + per-patch result cache, shared across every loop iteration.
136
138
  const lintCtx = await buildReExportLintContext(projectRoot, paths, config, options.noCache === true);
137
139
  let reExported = 0;
@@ -189,8 +191,8 @@ export async function reExportCommand(projectRoot, patches, options) {
189
191
  });
190
192
  }
191
193
  /**
192
- * Foreign-drift guard context, one per run (FORGE J2). `--expect` paths
193
- * (FORGE L6) are normalized once here so the per-patch comparison matches
194
+ * Foreign-drift guard context, one per run. `--expect` paths
195
+ * are normalized once here so the per-patch comparison matches
194
196
  * them against engine-relative diff-section paths.
195
197
  */
196
198
  function buildForeignDriftContext(options) {
@@ -203,7 +205,7 @@ function buildForeignDriftContext(options) {
203
205
  };
204
206
  }
205
207
  /**
206
- * Names `--expect` paths that never drifted this run (FORGE L6). A typo'd
208
+ * Names `--expect` paths that never drifted this run. A typo'd
207
209
  * `--expect` path silently degrades the flag back to refusing the slice it
208
210
  * was meant to admit, so surface the mismatch — but only as a warning: an
209
211
  * expected file legitimately shows no drift when the slice was already
@@ -217,11 +219,16 @@ function warnUnseenExpectedDrift(driftCtx) {
217
219
  warn(`--expect path(s) were not evaluated because no selected patch reached the drift check (for example, an earlier refusal): ${unseen.join(', ')}`);
218
220
  return;
219
221
  }
220
- warn(`--expect path(s) showed no drift this run (typo, or already captured?): ${unseen.join(', ')}`);
222
+ warn('--expect path(s) showed no drift this run: ' +
223
+ `${unseen.join(', ')}. ` +
224
+ 'Causes, in rough order of likelihood: a typo in the path; the slice was already ' +
225
+ 'captured by an earlier export in this session; or the engine content already matches ' +
226
+ 'the patch body — a concurrent session may have exported the same change, so the drift ' +
227
+ 'converged before this run looked.');
221
228
  }
222
229
  /**
223
- * Turns the run's collected `--refuse-adjacent-unmanaged` (FORGE G2) and
224
- * `--refuse-foreign-drift` (FORGE J2) refusals into the non-zero exit,
230
+ * Turns the run's collected `--refuse-adjacent-unmanaged` and
231
+ * `--refuse-foreign-drift` refusals into the non-zero exit,
225
232
  * naming every refused patch and the remedy.
226
233
  */
227
234
  function throwRunLevelRefusals(adjacentCtx, driftCtx, progress) {
@@ -240,13 +247,18 @@ function throwRunLevelRefusals(adjacentCtx, driftCtx, progress) {
240
247
  ? ` ${String(unreadable.length)} of these refused because the old patch body was missing or unreadable, so the drift comparison could not run (fail-closed).`
241
248
  : '';
242
249
  throw new GeneralError(`Refused ${String(driftCtx.refusals.length)} patch(es) whose refreshed body would absorb ` +
243
- `engine edits not present in the old patch body (--refuse-foreign-drift): ${names}.${unreadablePart} ` +
244
- `Commit or stash the foreign edits (another session may own them), name the slice's ` +
245
- `intended files with --expect <path>, or re-run without the flag to capture them intentionally.`);
250
+ `engine edits not present in the old patch body (--refuse-foreign-drift): ${names}.${unreadablePart}\n\n` +
251
+ 'The flag cannot tell WHO wrote those lines, and the two populations have opposite ' +
252
+ 'remedies:\n' +
253
+ ' - Lines you added yourself since the last export (the per-file report tags these ' +
254
+ '"edited since your last export"): name them with --expect <path> so this run captures ' +
255
+ 'them, or re-run without the flag to capture every drifting file.\n' +
256
+ " - Lines another session owns: do NOT proceed — commit or stash that session's work " +
257
+ 'first, then re-run.');
246
258
  }
247
259
  }
248
260
  /**
249
- * Prints the end-of-run summary and enforces the FORGE H8 exit contract:
261
+ * Prints the end-of-run summary and enforces the exit contract:
250
262
  * a partial run exits non-zero BY DEFAULT (adjudicated for 0.41.0, per the
251
263
  * follow-up 0.40.0's G7 recorded). "Re-exported 2 of 3" used to print and
252
264
  * exit 0, letting a partial refresh ride an `&&` chain as success. The
@@ -70,7 +70,7 @@ export async function resetCommand(projectRoot, options) {
70
70
  s.stop('Changes reset');
71
71
  // The reset may have moved components.conf away from what the last full
72
72
  // build compiled in — surface that now instead of at the next test
73
- // refusal (FORGE F13).
73
+ // refusal.
74
74
  await warnIfStaticComponentsStale(projectRoot, paths.engine);
75
75
  outro('Working tree restored to clean state');
76
76
  }
@@ -312,7 +312,7 @@ function reportSmokeSummary(args) {
312
312
  info(` Allowlisted error hits (suppressed): ${String(args.allowlistedErrorHits)}`);
313
313
  info(` Allowlisted lines total: ${String(args.allowlistedTotalHits)}`);
314
314
  info(` Child exit code: ${String(args.exitCode)}`);
315
- // Per-entry attribution (FORGE G8): first-match credit per line, with
315
+ // Per-entry attribution: first-match credit per line, with
316
316
  // zero-hit entries always visible — an allowlist entry whose suppressed
317
317
  // shape changed upstream is only detectable as a 0× row.
318
318
  if (args.allowlist.length > 0) {
@@ -4,15 +4,14 @@ import { configExists, loadRawConfigDocument, validateConfig, withConfigFileLock
4
4
  import { resolveArchive } from '../core/firefox-archive.js';
5
5
  import { GeneralError, InvalidArgumentError } from '../errors/base.js';
6
6
  import { info, intro, outro, success } from '../utils/logger.js';
7
- import { FIREFOX_PRODUCTS, isValidFirefoxCandidate, isValidFirefoxProduct, } from '../utils/validation.js';
8
- function isRecord(value) {
9
- return typeof value === 'object' && value !== null && !Array.isArray(value);
10
- }
7
+ import { FIREFOX_PRODUCTS, isJsonObject, isObject, isValidFirefoxCandidate, isValidFirefoxProduct, } from '../utils/validation.js';
11
8
  function cloneRawConfig(raw) {
12
9
  const cloned = structuredClone(raw);
13
- if (!isRecord(cloned)) {
10
+ if (!isObject(cloned)) {
14
11
  throw new GeneralError('Cannot update fireforge.json: config clone was not an object.');
15
12
  }
13
+ // A structured clone of a JSON document is itself a JSON document; the
14
+ // object check above is the only invariant left to establish.
16
15
  return cloned;
17
16
  }
18
17
  function parseSourceProduct(product) {
@@ -44,7 +43,8 @@ export async function sourceSetCommand(projectRoot, options) {
44
43
  const written = await withConfigFileLock(projectRoot, async () => {
45
44
  const raw = await loadRawConfigDocument(projectRoot);
46
45
  const updated = cloneRawConfig(raw);
47
- const firefox = isRecord(updated['firefox']) ? { ...updated['firefox'] } : {};
46
+ const existingFirefox = updated['firefox'];
47
+ const firefox = isJsonObject(existingFirefox) ? { ...existingFirefox } : {};
48
48
  firefox['version'] = options.version;
49
49
  firefox['product'] = options.product;
50
50
  if (options.clearSha256 === true) {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `status --check` enforcement policy (FORGE G1). Turns the classified
2
+ * `status --check` enforcement policy. Turns the classified
3
3
  * status view into a CI-enforceable non-zero exit: the default policy
4
4
  * fails on any classification outside {patch-backed, branding, furnace},
5
5
  * and `--fail-on <class,...>` replaces that set for finer control.
@@ -12,7 +12,7 @@ const ALL_CLASSIFICATIONS = [
12
12
  * Default `--check` policy: everything outside {patch-backed, branding,
13
13
  * furnace, binary-unsupported} fails. `binary-unsupported` is deliberately
14
14
  * excluded — an honest "cannot compare" must not keep the gate permanently
15
- * red (FORGE J3); strict CI can opt in via `--fail-on binary-unsupported`.
15
+ * red; strict CI can opt in via `--fail-on binary-unsupported`.
16
16
  */
17
17
  const DEFAULT_FAIL_CLASSIFICATIONS = [
18
18
  'unmanaged',
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Two shapes share one counting pass:
7
7
  * - the full payload (`renderJsonStatus`) with the per-file `files[]` list;
8
- * - the `--summary` gate payload (`renderJsonSummaryStatus`, FORGE K8),
8
+ * - the `--summary` gate payload (`renderJsonSummaryStatus`),
9
9
  * which omits `files[]` entirely — engine-clean gates need the verdict,
10
10
  * per-class counts, and offender names, not a payload that grows with
11
11
  * the queue (175 KB+ observed on a 290-patch consumer).
@@ -15,7 +15,7 @@ import { type StatusCheckPolicy } from './status-check.js';
15
15
  import type { OwnershipJsonBlock } from './status-ownership.js';
16
16
  /**
17
17
  * Full `--json` payload: summary plus the per-file `files[]` list, and —
18
- * under `--include-ownership` (FORGE L3) — the additive `ownership` block.
18
+ * under `--include-ownership` — the additive `ownership` block.
19
19
  * Additive within schemaVersion 1: a consumer that does not ask for the
20
20
  * block never sees the key.
21
21
  */
@@ -16,7 +16,7 @@ function countByClassification(classified) {
16
16
  }
17
17
  /**
18
18
  * Full `--json` payload: summary plus the per-file `files[]` list, and —
19
- * under `--include-ownership` (FORGE L3) — the additive `ownership` block.
19
+ * under `--include-ownership` — the additive `ownership` block.
20
20
  * Additive within schemaVersion 1: a consumer that does not ask for the
21
21
  * block never sees the key.
22
22
  */
@@ -14,7 +14,7 @@ export declare function renderUnmanagedOnly(unmanagedFiles: ClassifiedFile[], to
14
14
  /** Renders the default classified status buckets. */
15
15
  export declare function renderDefaultStatus(totalModified: number, buckets: ClassifiedBuckets, projectRoot: string, binaryName: string): Promise<void>;
16
16
  /**
17
- * Renders `fireforge status --test-coverage` (FORGE F11): a READ-ONLY view
17
+ * Renders `fireforge status --test-coverage`: a READ-ONLY view
18
18
  * of the last build baseline's test-packaging coverage. Before this
19
19
  * existed, the only way to learn the recorded coverage scope — which
20
20
  * concurrent sessions sharing one engine tree overwrite constantly — was
@@ -191,7 +191,7 @@ export async function renderDefaultStatus(totalModified, buckets, projectRoot, b
191
191
  outro(parts.join(', '));
192
192
  }
193
193
  /**
194
- * Renders `fireforge status --test-coverage` (FORGE F11): a READ-ONLY view
194
+ * Renders `fireforge status --test-coverage`: a READ-ONLY view
195
195
  * of the last build baseline's test-packaging coverage. Before this
196
196
  * existed, the only way to learn the recorded coverage scope — which
197
197
  * concurrent sessions sharing one engine tree overwrite constantly — was
@@ -2,6 +2,7 @@
2
2
  import { Command } from 'commander';
3
3
  import { readBuildBaseline } from '../core/build-baseline.js';
4
4
  import { getProjectPaths, loadConfig } from '../core/config.js';
5
+ import { formatEngineSessionLockStatus, readEngineSessionLockStatus, } from '../core/engine-session-lock.js';
5
6
  import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
6
7
  import { getHead, getStatusWithCodes, isGitRepository, isMissingHeadError } from '../core/git.js';
7
8
  import { getUntrackedFilesInDir, resolveMaxUntrackedFilesPerDir } from '../core/git-status.js';
@@ -96,7 +97,7 @@ async function classifyStatusFiles(files, paths, projectRoot, binaryName) {
96
97
  * The one worktree scan every classifying mode shares: porcelain status,
97
98
  * collapsed-directory expansion, atomic-temp-file filtering, and the
98
99
  * truncation banner. `--ownership` used to carry its own copy of this
99
- * block (FORGE L3); a non-git engine degrades to an empty list there,
100
+ * block; a non-git engine degrades to an empty list there,
100
101
  * which is why the git probe is a parameter rather than a hard guard.
101
102
  * @param engineDir - Path to the engine directory
102
103
  * @param requireGitRepository - When false, a non-git engine yields `[]`
@@ -114,7 +115,7 @@ async function scanEngineStatusFiles(engineDir, requireGitRepository) {
114
115
  * Emits the `--json` payload (full or `--summary`) and applies the
115
116
  * `--check` policy, which stays the SOLE exit driver on this path.
116
117
  *
117
- * `--include-ownership` (FORGE L3) is a MODIFIER, not a mode: it appends an
118
+ * `--include-ownership` is a MODIFIER, not a mode: it appends an
118
119
  * ownership block without touching exit semantics, so an ownership conflict
119
120
  * still fails only the human `--ownership` mode.
120
121
  * @param classified - Classified worktree entries
@@ -201,27 +202,39 @@ async function runStatusCommandBody(projectRoot, options) {
201
202
  options.ownership,
202
203
  options.testCoverage,
203
204
  options.json,
205
+ options.lock,
204
206
  ].filter((v) => v === true).length;
205
207
  if (modeCount > 1) {
206
- throw new GeneralError('Cannot use --raw, --unmanaged, --ownership, --test-coverage, and --json together. Pick at most one.');
208
+ throw new GeneralError('Cannot use --raw, --unmanaged, --ownership, --test-coverage, --lock, and --json together. Pick at most one.');
207
209
  }
208
- // --summary elides the files[] payload for gate consumers (FORGE K8);
210
+ // --summary elides the files[] payload for gate consumers;
209
211
  // it only makes sense on the JSON shape.
210
212
  if (options.summary === true && options.json !== true) {
211
213
  throw new InvalidArgumentError('--summary requires --json.', '--summary');
212
214
  }
213
215
  // --include-ownership adds a block to the JSON payload; it is meaningless
214
216
  // on the human views, where --ownership is the mode that renders the
215
- // table (FORGE L3).
217
+ // table.
216
218
  if (options.includeOwnership === true && options.json !== true) {
217
219
  throw new InvalidArgumentError('--include-ownership requires --json. Use --ownership for the human table.', '--include-ownership');
218
220
  }
219
- // --check / --fail-on enforcement policy (FORGE G1). Applies only
221
+ // --check / --fail-on enforcement policy. Applies only
220
222
  // where classification runs (the default view and --json).
221
223
  const checkPolicy = resolveStatusCheckPolicy(options);
222
224
  if (!options.raw && !options.json) {
223
225
  intro('FireForge Status');
224
226
  }
227
+ // Lock visibility. Placed before every engine/git guard: the
228
+ // whole point is to answer "who is holding this?" on a checkout that a
229
+ // concurrent command is busy with, and that answer must not depend on
230
+ // engine/ being readable.
231
+ if (options.lock === true) {
232
+ for (const line of formatEngineSessionLockStatus(await readEngineSessionLockStatus(projectRoot))) {
233
+ info(line);
234
+ }
235
+ outro('Lock status');
236
+ return;
237
+ }
225
238
  // Test-coverage mode needs only the project root (the baseline lives
226
239
  // in .fireforge/), so it short-circuits before any engine/git guards.
227
240
  if (options.testCoverage) {
@@ -247,7 +260,7 @@ async function runStatusCommandBody(projectRoot, options) {
247
260
  }
248
261
  const manifest = await loadPatchesManifest(paths.patches);
249
262
  // Same scan and the SAME classification pass as every other mode
250
- // (FORGE L3). This branch keeps its historical guard shape on purpose:
263
+ //. This branch keeps its historical guard shape on purpose:
251
264
  // no baseline-commit assertion, and a non-git engine degrades to an
252
265
  // empty table rather than the recovery banner.
253
266
  const rawFilesOwnership = await scanEngineStatusFiles(paths.engine, false);
@@ -336,6 +349,7 @@ export function registerStatus(program, { getProjectRoot, withErrorHandling }) {
336
349
  .command('status')
337
350
  .description('Show modified files in engine/')
338
351
  .option('--raw', 'Show raw worktree status without patch classification')
352
+ .option('--lock', 'Report the engine session lock instead of file status: holder pid, its command, how long it has held the lock, and how many waiters are queued behind it (read-only; never acquires it)')
339
353
  .option('--unmanaged', 'Show only unmanaged changes (not covered by patches or tools)')
340
354
  .option('--ownership', 'Show a flat path → owning patch table (flags files claimed by multiple patches)')
341
355
  .option('--test-coverage', 'Show what the last recorded build covers for test packaging (full or scoped paths)')
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Pre-test build phase for `fireforge test`, split out of `test.ts` (at
3
3
  * the per-file line budget): the incremental protected mach build with
4
- * its baseline write, and the FORGE J9 `--build-only` union-build shape.
4
+ * its baseline write, and the `--build-only` union-build shape.
5
5
  */
6
6
  import type { getProjectPaths, loadConfig } from '../core/config.js';
7
7
  import type { TestOptions } from '../types/commands/index.js';
@@ -9,7 +9,7 @@ import type { HarnessClassification } from './test-modes.js';
9
9
  /**
10
10
  * Runs the pre-test build (or the stale-build gate when no build was
11
11
  * requested). Returns `true` when the run is COMPLETE — the `--build-only`
12
- * union-build shape (FORGE J9), which packages every requested path
12
+ * union-build shape, which packages every requested path
13
13
  * (mixed harnesses legal, nothing dispatches), prints the per-harness
14
14
  * next steps, and emits the run's single `PASS` verdict.
15
15
  */