@hominis/fireforge 0.40.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +16 -10
  3. package/dist/bin/fireforge.js +25 -4
  4. package/dist/build-info.json +9 -0
  5. package/dist/src/cli.js +20 -3
  6. package/dist/src/commands/bootstrap.js +5 -1
  7. package/dist/src/commands/discard.js +2 -22
  8. package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
  9. package/dist/src/commands/doctor-check-core.d.ts +20 -0
  10. package/dist/src/commands/doctor-check-core.js +28 -0
  11. package/dist/src/commands/doctor-external-toolchains.js +2 -0
  12. package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
  13. package/dist/src/commands/doctor-furnace.js +13 -11
  14. package/dist/src/commands/doctor-orphaned-harness.js +2 -1
  15. package/dist/src/commands/doctor.js +2 -2
  16. package/dist/src/commands/export-all.js +9 -1
  17. package/dist/src/commands/export-flow.js +13 -3
  18. package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
  19. package/dist/src/commands/export-placement-conflicts.js +74 -0
  20. package/dist/src/commands/export-placement-gate.js +1 -1
  21. package/dist/src/commands/export-shared.d.ts +8 -1
  22. package/dist/src/commands/export-shared.js +16 -7
  23. package/dist/src/commands/export.js +2 -2
  24. package/dist/src/commands/furnace/apply.js +4 -3
  25. package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
  26. package/dist/src/commands/furnace/chrome-doc.js +13 -2
  27. package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
  28. package/dist/src/commands/furnace/create-browser-test.js +5 -0
  29. package/dist/src/commands/furnace/create.js +2 -2
  30. package/dist/src/commands/furnace/diff.js +2 -1
  31. package/dist/src/commands/furnace/init.js +4 -6
  32. package/dist/src/commands/furnace/override.js +2 -2
  33. package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
  34. package/dist/src/commands/furnace/remove-mutations.js +171 -0
  35. package/dist/src/commands/furnace/remove.js +21 -128
  36. package/dist/src/commands/furnace/rename-browser-test.js +54 -5
  37. package/dist/src/commands/furnace/scan.js +27 -36
  38. package/dist/src/commands/furnace/status.js +30 -28
  39. package/dist/src/commands/furnace/validate.js +25 -18
  40. package/dist/src/commands/import.js +6 -20
  41. package/dist/src/commands/lint-per-patch.js +12 -59
  42. package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
  43. package/dist/src/commands/lint-per-run-checkjs.js +102 -0
  44. package/dist/src/commands/lint.js +4 -4
  45. package/dist/src/commands/manifest.js +1 -1
  46. package/dist/src/commands/package.js +9 -16
  47. package/dist/src/commands/patch/compact.js +5 -5
  48. package/dist/src/commands/patch/delete.js +5 -5
  49. package/dist/src/commands/patch/lint-ignore.js +3 -2
  50. package/dist/src/commands/patch/move-files-create.js +22 -4
  51. package/dist/src/commands/patch/move-files-into.js +14 -10
  52. package/dist/src/commands/patch/move-files.js +6 -5
  53. package/dist/src/commands/patch/rename.d.ts +2 -2
  54. package/dist/src/commands/patch/rename.js +151 -50
  55. package/dist/src/commands/patch/reorder.d.ts +9 -0
  56. package/dist/src/commands/patch/reorder.js +14 -10
  57. package/dist/src/commands/patch/split-plan.d.ts +13 -6
  58. package/dist/src/commands/patch/split-plan.js +22 -12
  59. package/dist/src/commands/patch/split.d.ts +1 -1
  60. package/dist/src/commands/patch/split.js +23 -9
  61. package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
  62. package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
  63. package/dist/src/commands/patch/staged-dependency.js +10 -0
  64. package/dist/src/commands/re-export-adjacent.js +5 -6
  65. package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
  66. package/dist/src/commands/re-export-bulk-scan.js +142 -2
  67. package/dist/src/commands/re-export-drift.d.ts +74 -0
  68. package/dist/src/commands/re-export-drift.js +234 -0
  69. package/dist/src/commands/re-export-files.js +5 -3
  70. package/dist/src/commands/re-export-lint.d.ts +97 -0
  71. package/dist/src/commands/re-export-lint.js +210 -0
  72. package/dist/src/commands/re-export-options.js +8 -0
  73. package/dist/src/commands/re-export-register.d.ts +18 -0
  74. package/dist/src/commands/re-export-register.js +61 -0
  75. package/dist/src/commands/re-export-scan.js +13 -7
  76. package/dist/src/commands/re-export-single.d.ts +20 -0
  77. package/dist/src/commands/re-export-single.js +261 -0
  78. package/dist/src/commands/re-export.d.ts +0 -4
  79. package/dist/src/commands/re-export.js +113 -268
  80. package/dist/src/commands/rebase/abort.js +13 -4
  81. package/dist/src/commands/rebase/continue.js +11 -4
  82. package/dist/src/commands/rebase/index.js +19 -29
  83. package/dist/src/commands/rebase/patch-loop.js +12 -0
  84. package/dist/src/commands/reset.js +4 -23
  85. package/dist/src/commands/resolve.js +2 -9
  86. package/dist/src/commands/run.js +11 -21
  87. package/dist/src/commands/source.js +3 -9
  88. package/dist/src/commands/status-check.d.ts +13 -0
  89. package/dist/src/commands/status-check.js +31 -13
  90. package/dist/src/commands/status-json.d.ts +31 -0
  91. package/dist/src/commands/status-json.js +76 -0
  92. package/dist/src/commands/status-output.d.ts +1 -0
  93. package/dist/src/commands/status-output.js +10 -1
  94. package/dist/src/commands/status-ownership.d.ts +52 -0
  95. package/dist/src/commands/status-ownership.js +57 -0
  96. package/dist/src/commands/status.js +184 -175
  97. package/dist/src/commands/test-build-phase.d.ts +19 -0
  98. package/dist/src/commands/test-build-phase.js +168 -0
  99. package/dist/src/commands/test-diagnose.js +30 -2
  100. package/dist/src/commands/test-modes.d.ts +9 -1
  101. package/dist/src/commands/test-modes.js +46 -13
  102. package/dist/src/commands/test-register.js +59 -27
  103. package/dist/src/commands/test-run.d.ts +29 -3
  104. package/dist/src/commands/test-run.js +41 -4
  105. package/dist/src/commands/test-verdict.d.ts +32 -0
  106. package/dist/src/commands/test-verdict.js +48 -0
  107. package/dist/src/commands/test.d.ts +9 -0
  108. package/dist/src/commands/test.js +100 -123
  109. package/dist/src/commands/token-coverage.js +4 -9
  110. package/dist/src/commands/tree.d.ts +3 -0
  111. package/dist/src/commands/tree.js +171 -25
  112. package/dist/src/commands/verify.js +2 -2
  113. package/dist/src/commands/watch.js +22 -21
  114. package/dist/src/commands/wire.js +1 -1
  115. package/dist/src/core/ast-utils.d.ts +11 -0
  116. package/dist/src/core/ast-utils.js +11 -0
  117. package/dist/src/core/branding.d.ts +16 -0
  118. package/dist/src/core/branding.js +18 -2
  119. package/dist/src/core/build-audit-platform.js +6 -1
  120. package/dist/src/core/build-audit-registration.js +2 -0
  121. package/dist/src/core/build-audit-resolve.d.ts +13 -0
  122. package/dist/src/core/build-audit-resolve.js +34 -13
  123. package/dist/src/core/build-audit.js +4 -20
  124. package/dist/src/core/build-baseline-types.d.ts +29 -8
  125. package/dist/src/core/build-baseline-types.js +2 -1
  126. package/dist/src/core/build-baseline.d.ts +10 -2
  127. package/dist/src/core/build-baseline.js +39 -8
  128. package/dist/src/core/build-prepare.d.ts +4 -0
  129. package/dist/src/core/build-prepare.js +7 -1
  130. package/dist/src/core/config-paths.d.ts +1 -1
  131. package/dist/src/core/config-paths.js +2 -0
  132. package/dist/src/core/config-validate-test-toolchains.js +3 -2
  133. package/dist/src/core/config-validate.js +34 -36
  134. package/dist/src/core/config.js +16 -3
  135. package/dist/src/core/coverage-extend.d.ts +89 -0
  136. package/dist/src/core/coverage-extend.js +188 -0
  137. package/dist/src/core/destructive.js +3 -1
  138. package/dist/src/core/discard-baseline.js +18 -4
  139. package/dist/src/core/engine-precondition.d.ts +35 -0
  140. package/dist/src/core/engine-precondition.js +50 -0
  141. package/dist/src/core/engine-session-lock.d.ts +38 -1
  142. package/dist/src/core/engine-session-lock.js +78 -4
  143. package/dist/src/core/file-lock.d.ts +7 -0
  144. package/dist/src/core/file-lock.js +20 -30
  145. package/dist/src/core/firefox.js +5 -2
  146. package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
  147. package/dist/src/core/furnace-apply-helpers.js +12 -3
  148. package/dist/src/core/furnace-apply-output.js +9 -0
  149. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
  150. package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
  151. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
  152. package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
  153. package/dist/src/core/furnace-apply.d.ts +16 -4
  154. package/dist/src/core/furnace-apply.js +39 -43
  155. package/dist/src/core/furnace-config.js +2 -0
  156. package/dist/src/core/furnace-css-fragments.js +2 -0
  157. package/dist/src/core/furnace-jsconfig.js +14 -7
  158. package/dist/src/core/furnace-registration-ast.js +8 -8
  159. package/dist/src/core/furnace-registration-remove.js +6 -3
  160. package/dist/src/core/furnace-registration.js +9 -12
  161. package/dist/src/core/furnace-rollback.js +1 -1
  162. package/dist/src/core/furnace-scanner.js +2 -1
  163. package/dist/src/core/furnace-stale-export.js +2 -1
  164. package/dist/src/core/furnace-state-persist.d.ts +4 -6
  165. package/dist/src/core/furnace-validate-compatibility.js +4 -7
  166. package/dist/src/core/furnace-validate-helpers.js +33 -1
  167. package/dist/src/core/furnace-validate-registration.js +16 -0
  168. package/dist/src/core/furnace-validate.js +4 -2
  169. package/dist/src/core/git-diff.js +37 -2
  170. package/dist/src/core/lint-cache.d.ts +1 -1
  171. package/dist/src/core/lint-cache.js +33 -10
  172. package/dist/src/core/mach-build-artifacts.d.ts +63 -1
  173. package/dist/src/core/mach-build-artifacts.js +108 -2
  174. package/dist/src/core/mach-mozconfig.js +9 -1
  175. package/dist/src/core/mach-resource-shim.js +1 -0
  176. package/dist/src/core/marionette-port.d.ts +16 -0
  177. package/dist/src/core/marionette-port.js +93 -8
  178. package/dist/src/core/marionette-preflight.js +6 -5
  179. package/dist/src/core/patch-apply.d.ts +20 -13
  180. package/dist/src/core/patch-apply.js +32 -33
  181. package/dist/src/core/patch-export.js +3 -5
  182. package/dist/src/core/patch-lint-checkjs.d.ts +25 -3
  183. package/dist/src/core/patch-lint-checkjs.js +78 -7
  184. package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
  185. package/dist/src/core/patch-lint-creators.d.ts +30 -0
  186. package/dist/src/core/patch-lint-creators.js +45 -0
  187. package/dist/src/core/patch-lint-cross.d.ts +1 -17
  188. package/dist/src/core/patch-lint-cross.js +16 -32
  189. package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
  190. package/dist/src/core/patch-lint-jsdoc.js +15 -5
  191. package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
  192. package/dist/src/core/patch-lint-module-registration.js +88 -0
  193. package/dist/src/core/patch-lint-mozbuild.js +99 -11
  194. package/dist/src/core/patch-lint-reexports.d.ts +1 -0
  195. package/dist/src/core/patch-lint-reexports.js +1 -0
  196. package/dist/src/core/patch-lint.d.ts +8 -0
  197. package/dist/src/core/patch-lint.js +84 -6
  198. package/dist/src/core/patch-lock.d.ts +12 -1
  199. package/dist/src/core/patch-lock.js +40 -4
  200. package/dist/src/core/patch-manifest-consistency.js +15 -3
  201. package/dist/src/core/patch-parse.d.ts +8 -0
  202. package/dist/src/core/patch-parse.js +10 -0
  203. package/dist/src/core/patch-policy.d.ts +7 -0
  204. package/dist/src/core/patch-policy.js +4 -1
  205. package/dist/src/core/patch-transform.d.ts +10 -0
  206. package/dist/src/core/patch-transform.js +13 -2
  207. package/dist/src/core/rebase-session.d.ts +44 -2
  208. package/dist/src/core/rebase-session.js +114 -17
  209. package/dist/src/core/register-browser-content.js +2 -1
  210. package/dist/src/core/register-shared-css.js +2 -1
  211. package/dist/src/core/smoke-patterns.d.ts +0 -8
  212. package/dist/src/core/smoke-patterns.js +3 -2
  213. package/dist/src/core/status-binary.d.ts +55 -0
  214. package/dist/src/core/status-binary.js +124 -0
  215. package/dist/src/core/status-classify.d.ts +6 -1
  216. package/dist/src/core/status-classify.js +101 -73
  217. package/dist/src/core/test-harness-crash.d.ts +32 -1
  218. package/dist/src/core/test-harness-crash.js +59 -4
  219. package/dist/src/core/test-path-scope.js +2 -0
  220. package/dist/src/core/test-stale-check.js +1 -20
  221. package/dist/src/core/toolchain-preflight.js +24 -26
  222. package/dist/src/core/tree-guard.d.ts +1 -1
  223. package/dist/src/core/tree-guard.js +67 -12
  224. package/dist/src/core/tree-store.d.ts +86 -10
  225. package/dist/src/core/tree-store.js +310 -72
  226. package/dist/src/core/typecheck.js +2 -1
  227. package/dist/src/core/wire-destroy.js +14 -3
  228. package/dist/src/core/wire-init.js +37 -40
  229. package/dist/src/core/wire-subscript.js +11 -3
  230. package/dist/src/core/wire-utils.js +4 -4
  231. package/dist/src/core/xpcshell-appdir.js +3 -0
  232. package/dist/src/errors/base.d.ts +13 -0
  233. package/dist/src/errors/base.js +13 -0
  234. package/dist/src/errors/rebase.d.ts +13 -0
  235. package/dist/src/errors/rebase.js +17 -0
  236. package/dist/src/types/commands/options.d.ts +86 -1
  237. package/dist/src/types/commands/patches.d.ts +8 -0
  238. package/dist/src/types/furnace.d.ts +7 -0
  239. package/dist/src/utils/build-info.d.ts +31 -0
  240. package/dist/src/utils/build-info.js +98 -0
  241. package/dist/src/utils/concurrency.d.ts +8 -0
  242. package/dist/src/utils/concurrency.js +21 -0
  243. package/dist/src/utils/errors.d.ts +39 -0
  244. package/dist/src/utils/errors.js +55 -0
  245. package/dist/src/utils/fs.js +5 -12
  246. package/dist/src/utils/logger.d.ts +14 -2
  247. package/dist/src/utils/logger.js +43 -13
  248. package/dist/src/utils/options.d.ts +21 -4
  249. package/dist/src/utils/options.js +26 -6
  250. package/dist/src/utils/package-root.js +7 -4
  251. package/dist/src/utils/process-group.js +13 -2
  252. package/dist/src/utils/stdio-drain.d.ts +28 -0
  253. package/dist/src/utils/stdio-drain.js +54 -0
  254. package/dist/src/utils/validation.d.ts +23 -2
  255. package/dist/src/utils/validation.js +29 -3
  256. package/package.json +9 -9
@@ -0,0 +1,102 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Per-run checkJs program controller, shared by `lint --per-patch` and
4
+ * `re-export` (FORGE J1).
5
+ *
6
+ * The dominant fixed cost of patch lint is one `ts.createProgram` over the
7
+ * queue's patch-owned `.sys.mjs` files (~37 s on a 290-patch consumer
8
+ * queue). This controller builds that program at most ONCE per command
9
+ * invocation (lazily, promise-memoised) and lets callers slice per-patch
10
+ * findings out of the grouped result. Three levers keep the cost paid only
11
+ * when it buys something:
12
+ *
13
+ * - **Lazy build**: nothing is built until the first cache miss asks.
14
+ * - **Root scoping** (`rootScopePatches`): with `--patches <subset>` the
15
+ * program's roots are only the subset's owned files; the full queue
16
+ * stays resolvable, so cross-patch imports type-check identically while
17
+ * unrelated files are never parsed (FORGE J1c).
18
+ * - **Warm-run probe**: run-level ("global") checkJs findings come only
19
+ * from a missing `typescript` package or an unreadable shim — never
20
+ * from the built program — so an all-cache-hit run satisfies "warm
21
+ * never reports less than cold" (FORGE F5) via
22
+ * `probeCheckJsGlobalIssues` instead of building anything.
23
+ */
24
+ import { invokePatchLintCheckJsGrouped, probeCheckJsGlobalIssues, runCheckJsTestFilesGrouped, } from '../core/patch-lint-checkjs.js';
25
+ import { isTestScriptFile, resolvePatchOwnedSysMjs, resolvePatchOwnedTestScripts, } from '../core/patch-lint-ownership.js';
26
+ /**
27
+ * Builds the per-run checkJs program controller when `patchLint.checkJs` is
28
+ * enabled, or returns undefined. With `rootScopePatches` set (the
29
+ * `--patches` subset, by patch filename), the program roots at only the
30
+ * subset's owned files and the test-file pass runs only the subset's own
31
+ * scripts (head.js discovery still spans the queue).
32
+ */
33
+ export function buildPerRunCheckJs(projectRoot, paths, config, ctx, rootScopePatches) {
34
+ const patchLint = config.patchLint;
35
+ if (!patchLint?.checkJs)
36
+ return undefined;
37
+ const testFilesEnabled = patchLint.checkJsTestFiles === true;
38
+ const ownedByPatch = new Map();
39
+ for (const entry of ctx.entries) {
40
+ const owned = new Set();
41
+ for (const f of entry.newFiles.keys()) {
42
+ if (f.endsWith('.sys.mjs'))
43
+ owned.add(f);
44
+ else if (testFilesEnabled && isTestScriptFile(f))
45
+ owned.add(f);
46
+ }
47
+ if (owned.size > 0)
48
+ ownedByPatch.set(entry.filename, owned);
49
+ }
50
+ const resolutionSet = new Set();
51
+ for (const files of ownedByPatch.values()) {
52
+ for (const f of files)
53
+ resolutionSet.add(f);
54
+ }
55
+ // The subset's owned files, split by program kind; undefined = full queue.
56
+ let scopedRoots;
57
+ if (rootScopePatches !== undefined) {
58
+ scopedRoots = new Set();
59
+ for (const [patchFilename, files] of ownedByPatch) {
60
+ if (!rootScopePatches.has(patchFilename))
61
+ continue;
62
+ for (const f of files)
63
+ scopedRoots.add(f);
64
+ }
65
+ }
66
+ // One build for the whole run: the queue-wide `.sys.mjs` program plus —
67
+ // when `patchLint.checkJsTestFiles` is on (FORGE G5) — one small
68
+ // script-scope program per patch-owned test file, merged by file.
69
+ const buildAll = async () => {
70
+ const sys = await invokePatchLintCheckJsGrouped(paths.engine, resolvePatchOwnedSysMjs(new Set(), ctx), patchLint, projectRoot, scopedRoots);
71
+ if (!testFilesEnabled)
72
+ return sys;
73
+ const tests = await runCheckJsTestFilesGrouped(paths.engine, resolvePatchOwnedTestScripts(new Set(), ctx), patchLint, projectRoot, scopedRoots);
74
+ const byFile = new Map(sys.byFile);
75
+ for (const [rel, list] of tests.byFile) {
76
+ byFile.set(rel, [...(byFile.get(rel) ?? []), ...list]);
77
+ }
78
+ return { byFile, global: [...sys.global, ...tests.global] };
79
+ };
80
+ // Memoise the *promise*, not the resolved value: under the bounded pool
81
+ // several patches can reach `getGrouped` before the first build resolves, and
82
+ // `??=` on the promise (a synchronous expression) guarantees a single build.
83
+ let groupedPromise;
84
+ const getGrouped = () => (groupedPromise ??= buildAll());
85
+ return {
86
+ ownedByPatch,
87
+ resolutionSet,
88
+ getGrouped,
89
+ getGlobal: async () => groupedPromise !== undefined
90
+ ? (await groupedPromise).global
91
+ : probeCheckJsGlobalIssues(patchLint, projectRoot),
92
+ sliceFor: async (files) => {
93
+ const grouped = await getGrouped();
94
+ const issues = [];
95
+ for (const rel of files) {
96
+ issues.push(...(grouped.byFile.get(rel) ?? []));
97
+ }
98
+ return issues;
99
+ },
100
+ isRelevant: (file) => file.endsWith('.sys.mjs') || (testFilesEnabled && isTestScriptFile(file)),
101
+ };
102
+ }
@@ -10,7 +10,7 @@ import { getAllDiff, getDiffForFilesAgainstHead } from '../core/git-diff.js';
10
10
  import { expandUntrackedDirectoryEntries, getModifiedFilesInDir, getUntrackedFiles, getUntrackedFilesInDir, getWorkingTreeStatus, } from '../core/git-status.js';
11
11
  import { clearPerPatchLintCache } from '../core/lint-cache.js';
12
12
  import { extractAffectedFiles } from '../core/patch-apply.js';
13
- import { buildPatchQueueContext, countNonBinaryDiffLines, lintExportedPatch, lintPatchQueue, lintPatchSize, } from '../core/patch-lint.js';
13
+ import { buildPatchQueueContext, countNonBinaryDiffLines, formatPatchLintIssue, lintExportedPatch, lintPatchQueue, lintPatchSize, } from '../core/patch-lint.js';
14
14
  import { collectDiffFilePaths, tagLintIssues } from '../core/patch-lint-diff-tag.js';
15
15
  import { GeneralError } from '../errors/base.js';
16
16
  import { pathExists } from '../utils/fs.js';
@@ -361,13 +361,13 @@ async function reportLintOutcome(engineDir, issues, options) {
361
361
  const notices = issues.filter((i) => i.severity === 'notice');
362
362
  const tagPrefix = (issue) => sinceActive && issue.tag ? `[${issue.tag}] ` : '';
363
363
  for (const issue of notices) {
364
- info(`${tagPrefix(issue)}NOTICE [${issue.check}] ${issue.file}: ${issue.message}`);
364
+ info(`${tagPrefix(issue)}NOTICE ${formatPatchLintIssue(issue)}`);
365
365
  }
366
366
  for (const issue of warnings) {
367
- warn(`${tagPrefix(issue)}[${issue.check}] ${issue.file}: ${issue.message}`);
367
+ warn(`${tagPrefix(issue)}${formatPatchLintIssue(issue)}`);
368
368
  }
369
369
  for (const issue of errors) {
370
- warn(`${tagPrefix(issue)}ERROR [${issue.check}] ${issue.file}: ${issue.message}`);
370
+ warn(`${tagPrefix(issue)}ERROR ${formatPatchLintIssue(issue)}`);
371
371
  }
372
372
  if (sinceActive) {
373
373
  const introducedErrors = errors.filter((i) => i.tag === 'introduced').length;
@@ -11,7 +11,7 @@ import { registerImport } from './import.js';
11
11
  import { registerLint } from './lint.js';
12
12
  import { registerPackage } from './package.js';
13
13
  import { registerPatch } from './patch/index.js';
14
- import { registerReExport } from './re-export.js';
14
+ import { registerReExport } from './re-export-register.js';
15
15
  import { registerRebase } from './rebase.js';
16
16
  import { registerRegister } from './register.js';
17
17
  import { registerReset } from './reset.js';
@@ -3,10 +3,11 @@ import { Command } from 'commander';
3
3
  import { validateBrandOverride } from '../core/brand-validation.js';
4
4
  import { prepareBuildEnvironment } from '../core/build-prepare.js';
5
5
  import { getProjectPaths, loadConfig } from '../core/config.js';
6
- import { buildArtifactMismatchMessage, hasBuildArtifacts, machPackageCapture, } from '../core/mach.js';
6
+ import { hasBuildArtifacts, machPackageCapture } from '../core/mach.js';
7
+ import { assertBuildArtifacts } from '../core/mach-build-artifacts.js';
7
8
  import { explainMachError } from '../core/mach-error-hints.js';
8
9
  import { GeneralError } from '../errors/base.js';
9
- import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
10
+ import { BuildError } from '../errors/build.js';
10
11
  import { pathExists } from '../utils/fs.js';
11
12
  import { error, info, intro, outro, verbose } from '../utils/logger.js';
12
13
  import { pickDefined } from '../utils/options.js';
@@ -27,20 +28,12 @@ export async function packageCommand(projectRoot, options) {
27
28
  throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
28
29
  }
29
30
  const buildCheck = await hasBuildArtifacts(paths.engine);
30
- if (buildCheck.ambiguous && buildCheck.objDirs && buildCheck.objDirs.length > 0) {
31
- throw new AmbiguousBuildArtifactsError(buildCheck.objDirs);
32
- }
33
- const mismatchMessage = buildArtifactMismatchMessage(paths.engine, buildCheck, 'Package');
34
- if (mismatchMessage) {
35
- throw new GeneralError(mismatchMessage);
36
- }
37
- if (!buildCheck.exists) {
38
- const detail = buildCheck.objDir
39
- ? `Build artifacts incomplete in ${buildCheck.objDir}/`
40
- : 'No build artifacts found (obj-*/ directory missing)';
41
- throw new GeneralError(`Packaging requires a completed build. ${detail}\n\n` +
42
- "Run 'fireforge build' first, then rerun 'fireforge package'.");
43
- }
31
+ assertBuildArtifacts(paths.engine, buildCheck, {
32
+ label: 'Package',
33
+ requirement: 'Packaging requires a completed build.',
34
+ remediation: "Run 'fireforge build' first, then rerun 'fireforge package'.",
35
+ requireExisting: true,
36
+ });
44
37
  // Log brand info if specified
45
38
  if (options.brand) {
46
39
  verbose(`Packaging with brand: ${options.brand}`);
@@ -18,7 +18,7 @@ import { applyRenameMapToManifest, enforcePatchPolicy } from '../../core/patch-p
18
18
  import { GeneralError } from '../../errors/base.js';
19
19
  import { toError } from '../../utils/errors.js';
20
20
  import { info, intro, outro, warn } from '../../utils/logger.js';
21
- import { pickDefined } from '../../utils/options.js';
21
+ import { addWaitLockOption, pickDefined, resolveWaitLockSeconds } from '../../utils/options.js';
22
22
  import { requirePatchQueue } from './patch-context.js';
23
23
  import { rebuildFilenameForOrder } from './reorder.js';
24
24
  /** True when `order` falls inside a configured reserved range. */
@@ -185,7 +185,7 @@ export async function patchCompactCommand(projectRoot, options = {}) {
185
185
  catch (historyError) {
186
186
  warn(`History log append failed after patch compact committed: ${toError(historyError).message}`);
187
187
  }
188
- });
188
+ }, { waitLockSeconds: resolveWaitLockSeconds(options.waitLock), command: 'patch compact' });
189
189
  info(`Compacted ${renameMap.size} patch(es).`);
190
190
  outro('Compact complete');
191
191
  }
@@ -197,13 +197,13 @@ export async function patchCompactCommand(projectRoot, options = {}) {
197
197
  */
198
198
  export function registerPatchCompact(parent, context) {
199
199
  const { getProjectRoot, withErrorHandling } = context;
200
- parent
200
+ const command = parent
201
201
  .command('compact')
202
202
  .description('Close ordinal gaps in the patch queue (range-aware when patchPolicy.ranges is configured)')
203
203
  .option('--dry-run', 'Show what would happen without writing')
204
204
  .option('-y, --yes', 'Skip confirmation prompt (required for non-TTY)')
205
- .option('--force-unsafe', 'Bypass force-mode patchPolicy refusals')
206
- .action(withErrorHandling(async (options) => {
205
+ .option('--force-unsafe', 'Bypass force-mode patchPolicy refusals');
206
+ addWaitLockOption(command).action(withErrorHandling(async (options) => {
207
207
  await patchCompactCommand(getProjectRoot(), pickDefined(options));
208
208
  }));
209
209
  }
@@ -15,7 +15,7 @@ import { withPatchDirectoryLock } from '../../core/patch-lock.js';
15
15
  import { removePatchFileAndManifest } from '../../core/patch-manifest.js';
16
16
  import { toError } from '../../utils/errors.js';
17
17
  import { info, intro, outro, warn } from '../../utils/logger.js';
18
- import { pickDefined } from '../../utils/options.js';
18
+ import { addWaitLockOption, pickDefined, resolveWaitLockSeconds } from '../../utils/options.js';
19
19
  import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
20
20
  /**
21
21
  * Runs the `patch delete` command: removes a patch file and its manifest
@@ -181,7 +181,7 @@ export async function patchDeleteCommand(projectRoot, identifier, options = {})
181
181
  catch (historyError) {
182
182
  warn(`History log append failed after patch delete committed (${target.filename}): ${toError(historyError).message}`);
183
183
  }
184
- });
184
+ }, { waitLockSeconds: resolveWaitLockSeconds(options.waitLock), command: 'patch delete' });
185
185
  info(`Deleted ${target.filename}.`);
186
186
  outro('Delete complete');
187
187
  }
@@ -193,13 +193,13 @@ export async function patchDeleteCommand(projectRoot, identifier, options = {})
193
193
  */
194
194
  export function registerPatchDelete(parent, context) {
195
195
  const { getProjectRoot, withErrorHandling } = context;
196
- parent
196
+ const command = parent
197
197
  .command('delete <name>')
198
198
  .description('Delete a patch from the queue (destructive)')
199
199
  .option('--dry-run', 'Show what would happen without writing')
200
200
  .option('-y, --yes', 'Skip confirmation prompt (required for non-TTY)')
201
- .option('--force-unsafe', 'Bypass the refusal when a later patch depends on this patch (last resort)')
202
- .action(withErrorHandling(async (name, options) => {
201
+ .option('--force-unsafe', 'Bypass the refusal when a later patch depends on this patch (last resort)');
202
+ addWaitLockOption(command).action(withErrorHandling(async (name, options) => {
203
203
  await patchDeleteCommand(getProjectRoot(), name, pickDefined(options));
204
204
  }));
205
205
  }
@@ -25,6 +25,7 @@ import { mutatePatchMetadata } from '../../core/patch-export.js';
25
25
  import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
26
26
  import { toError } from '../../utils/errors.js';
27
27
  import { info, intro, outro, warn } from '../../utils/logger.js';
28
+ import { stringListOption } from '../../utils/options.js';
28
29
  import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
29
30
  /**
30
31
  * Printed whenever `--add` lands a new check id. The waiver this command
@@ -197,8 +198,8 @@ export function registerPatchLintIgnore(parent, context) {
197
198
  parent
198
199
  .command('lint-ignore <name>')
199
200
  .description('Edit PatchMetadata.lintIgnore on a single patch (no .patch body rewrite). One mode per invocation.')
200
- .option('--add <check-id>', 'Lint check ID to add to the patch lintIgnore list (repeatable)', (value, prev) => [...prev, value], [])
201
- .option('--remove <check-id>', 'Lint check ID to remove from the patch lintIgnore list (repeatable)', (value, prev) => [...prev, value], [])
201
+ .option('--add <check-id>', 'Lint check ID to add to the patch lintIgnore list (repeatable)', ...stringListOption())
202
+ .option('--remove <check-id>', 'Lint check ID to remove from the patch lintIgnore list (repeatable)', ...stringListOption())
202
203
  .option('--clear', 'Drop the lintIgnore field entirely')
203
204
  .option('--dry-run', 'Show what would change without writing')
204
205
  .option('-y, --yes', 'Skip confirmation prompt (required for non-TTY)')
@@ -13,10 +13,12 @@
13
13
  import { getProjectPaths, loadConfig } from '../../core/config.js';
14
14
  import { confirmDestructive } from '../../core/destructive.js';
15
15
  import { formatPatchNotFoundError } from '../../core/patch-identifier-suggest.js';
16
+ import { buildPatchQueueContext } from '../../core/patch-lint.js';
16
17
  import { loadPatchesManifest, resolvePatchIdentifier } from '../../core/patch-manifest.js';
17
18
  import { enforcePatchPolicy } from '../../core/patch-policy.js';
18
19
  import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
19
20
  import { info, intro, outro, success } from '../../utils/logger.js';
21
+ import { normalizePatchDisplayName } from '../../utils/validation.js';
20
22
  import { resolvePlacementPlan } from '../export-flow.js';
21
23
  import { runPatchLint } from '../export-shared.js';
22
24
  import { commitPatchSplit } from './split.js';
@@ -60,6 +62,13 @@ export async function patchMoveFilesCreateCommand(projectRoot, fromIdentifier, n
60
62
  const category = options.category ?? source.category;
61
63
  const placementOptions = { order: options.order };
62
64
  const placement = await resolvePlacementPlan(paths.patches, placementOptions, category, newPatchName, config);
65
+ // The filename slug pipeline (resolvePlacementPlan above) strips redundant
66
+ // category prefixes; the manifest display name must agree with the bare-slug
67
+ // naming policy, exactly as `export --name` already does (FORGE G13/H4).
68
+ const displayName = normalizePatchDisplayName(newPatchName, category);
69
+ if (displayName !== newPatchName) {
70
+ info(`Patch name normalized: "${newPatchName}" → "${displayName}" (the filename carries the order and category prefix).`);
71
+ }
63
72
  const plan = {
64
73
  source,
65
74
  movedFiles,
@@ -69,7 +78,7 @@ export async function patchMoveFilesCreateCommand(projectRoot, fromIdentifier, n
69
78
  placement,
70
79
  placementOptions,
71
80
  category,
72
- name: newPatchName,
81
+ name: displayName,
73
82
  description: options.description ?? '',
74
83
  ownerRewrites: findOwnerRewriteHolders(manifest.patches, source.filename, movedSet),
75
84
  // Populated by runProjectedSplitLint below (forward edges into the new patch).
@@ -77,10 +86,16 @@ export async function patchMoveFilesCreateCommand(projectRoot, fromIdentifier, n
77
86
  };
78
87
  // Per-patch lint both projected bodies, threading the source patch's
79
88
  // tier/lintIgnore so an intentional-advisory patch can still move files.
89
+ // The whole-queue context (built once, with the config so it carries the
90
+ // same patch-policy shape as the committed `lint --per-patch` gate) makes
91
+ // cross-patch `resource:///` imports and sibling head.js harness roots
92
+ // resolve exactly as they will after the move lands — without it the
93
+ // projection lint was blind (FORGE I3).
94
+ const patchQueueCtx = await buildPatchQueueContext(paths.patches, config);
80
95
  const ignoreChecks = source.lintIgnore ? new Set(source.lintIgnore) : undefined;
81
- await runPatchLint(paths.engine, remainingFiles, remainingDiff, config, options.skipLint, undefined, ignoreChecks, source.tier);
82
- await runPatchLint(paths.engine, movedFiles, movedDiff, config, options.skipLint, undefined, ignoreChecks, source.tier);
83
- const { conflicts, stagedDependencyAdditions } = await runProjectedSplitLint(paths.patches, plan);
96
+ await runPatchLint(paths.engine, remainingFiles, remainingDiff, config, options.skipLint, patchQueueCtx, ignoreChecks, source.tier);
97
+ await runPatchLint(paths.engine, movedFiles, movedDiff, config, options.skipLint, patchQueueCtx, ignoreChecks, source.tier);
98
+ const { conflicts, stagedDependencyAdditions } = runProjectedSplitLint(plan, patchQueueCtx);
84
99
  plan.stagedDependencyAdditions = stagedDependencyAdditions;
85
100
  const newMetadata = buildNewPatchMetadata(plan, config);
86
101
  enforcePatchPolicy({
@@ -88,6 +103,9 @@ export async function patchMoveFilesCreateCommand(projectRoot, fromIdentifier, n
88
103
  manifest: projectSplitManifest(manifest, plan, newMetadata),
89
104
  command: 'patch move-files --create',
90
105
  forceUnsafe: options.forceUnsafe === true,
106
+ hints: {
107
+ 'description-required': 'Pass --description "<text>" (or -d) on this command to set the created patch\'s description.',
108
+ },
91
109
  });
92
110
  const decision = await confirmDestructive({
93
111
  operation: 'patch-move-files-create',
@@ -18,7 +18,7 @@ import { join } from 'node:path';
18
18
  import { appendHistory, confirmDestructive } from '../../core/destructive.js';
19
19
  import { computeProjectedLintRegressions } from '../../core/lint-projection.js';
20
20
  import { normalizePatchArtifact } from '../../core/patch-artifact-normalize.js';
21
- import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, collectForwardImportEdges, detectNewFilesInDiff, lintPatchQueue, } from '../../core/patch-lint.js';
21
+ import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, collectForwardImportEdges, detectNewFilesInDiff, formatPatchLintIssue, lintPatchQueue, } from '../../core/patch-lint.js';
22
22
  import { withPatchDirectoryLock } from '../../core/patch-lock.js';
23
23
  import { loadPatchesManifest, savePatchesManifest, validatePatchesManifest, } from '../../core/patch-manifest.js';
24
24
  import { extractNewFileContentFromDiff } from '../../core/patch-transform.js';
@@ -26,6 +26,7 @@ import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
26
26
  import { toError } from '../../utils/errors.js';
27
27
  import { readText, writeText } from '../../utils/fs.js';
28
28
  import { info, outro, success, warn } from '../../utils/logger.js';
29
+ import { resolveWaitLockSeconds } from '../../utils/options.js';
29
30
  import { runPatchLint } from '../export-shared.js';
30
31
  import { buildSplitDiff, findOwnerRewriteHolders, mergeStagedForwardImports, rewriteSplitOwners, } from './split-plan.js';
31
32
  function projectEntryBody(diff) {
@@ -41,9 +42,8 @@ function projectEntryBody(diff) {
41
42
  * to live in the same patch as the moved file gains a forward edge when the
42
43
  * target is later-ordered). Reports only regressions the move itself adds.
43
44
  */
44
- async function runProjectedMoveLint(patchesDir, plan) {
45
+ function runProjectedMoveLint(plan, baseCtx) {
45
46
  const movedSet = new Set(plan.movedFiles);
46
- const baseCtx = await buildPatchQueueContext(patchesDir);
47
47
  const entries = baseCtx.entries.map((entry) => {
48
48
  let metadata = entry.metadata;
49
49
  if (metadata) {
@@ -93,7 +93,7 @@ async function runProjectedMoveLint(patchesDir, plan) {
93
93
  ? null
94
94
  : {
95
95
  reason: `move would introduce ${regressions.length} cross-patch lint error(s)`,
96
- details: regressions.map((i) => `[${i.check}] ${i.file}: ${i.message}`),
96
+ details: regressions.map(formatPatchLintIssue),
97
97
  };
98
98
  return { conflicts, stagedDependencyAdditions };
99
99
  }
@@ -103,7 +103,7 @@ async function runProjectedMoveLint(patchesDir, plan) {
103
103
  * owner rewrites + auto-declared forward imports). Rolled back in reverse
104
104
  * order on any failure.
105
105
  */
106
- async function commitMoveInto(patchesDir, plan) {
106
+ async function commitMoveInto(patchesDir, plan, waitLockSeconds) {
107
107
  await withPatchDirectoryLock(patchesDir, async () => {
108
108
  const manifest = await loadPatchesManifest(patchesDir);
109
109
  if (!manifest)
@@ -184,7 +184,7 @@ async function commitMoveInto(patchesDir, plan) {
184
184
  }
185
185
  throw error;
186
186
  }
187
- });
187
+ }, { waitLockSeconds, command: 'patch move-files' });
188
188
  }
189
189
  /**
190
190
  * Plans, lints, confirms, and commits a file-ownership move into an
@@ -196,8 +196,12 @@ export async function runMoveFilesInto(args) {
196
196
  const sourceDiff = await buildSplitDiff(enginePath, sourceAfter, 'remaining', source.filename);
197
197
  const targetDiff = await buildSplitDiff(enginePath, targetAfter, 'moved', target.filename);
198
198
  // Per-patch lint both projected bodies with each patch's own waivers.
199
- await runPatchLint(enginePath, sourceAfter, sourceDiff, args.config, options.skipLint, undefined, source.lintIgnore ? new Set(source.lintIgnore) : undefined, source.tier);
200
- await runPatchLint(enginePath, targetAfter, targetDiff, args.config, options.skipLint, undefined, target.lintIgnore ? new Set(target.lintIgnore) : undefined, target.tier);
199
+ // The whole-queue context (built once, with the config for committed-gate
200
+ // parity) resolves cross-patch imports and sibling head.js harness roots
201
+ // instead of linting each projected body blind (FORGE I3).
202
+ const patchQueueCtx = await buildPatchQueueContext(patchesDir, args.config);
203
+ await runPatchLint(enginePath, sourceAfter, sourceDiff, args.config, options.skipLint, patchQueueCtx, source.lintIgnore ? new Set(source.lintIgnore) : undefined, source.tier);
204
+ await runPatchLint(enginePath, targetAfter, targetDiff, args.config, options.skipLint, patchQueueCtx, target.lintIgnore ? new Set(target.lintIgnore) : undefined, target.tier);
201
205
  const manifest = await loadPatchesManifest(patchesDir);
202
206
  const plan = {
203
207
  source,
@@ -210,7 +214,7 @@ export async function runMoveFilesInto(args) {
210
214
  ownerRewrites: findOwnerRewriteHolders(manifest?.patches ?? [], source.filename, new Set(files)),
211
215
  stagedDependencyAdditions: new Map(),
212
216
  };
213
- const { conflicts, stagedDependencyAdditions } = await runProjectedMoveLint(patchesDir, plan);
217
+ const { conflicts, stagedDependencyAdditions } = runProjectedMoveLint(plan, patchQueueCtx);
214
218
  plan.stagedDependencyAdditions = stagedDependencyAdditions;
215
219
  const decision = await confirmDestructive({
216
220
  operation: 'patch-move-files',
@@ -236,7 +240,7 @@ export async function runMoveFilesInto(args) {
236
240
  outro('Move cancelled');
237
241
  return;
238
242
  }
239
- await commitMoveInto(patchesDir, plan);
243
+ await commitMoveInto(patchesDir, plan, resolveWaitLockSeconds(options.waitLock));
240
244
  success(`Moved ${files.length} file(s) from ${source.filename} to ${target.filename}`);
241
245
  if (plan.ownerRewrites.length > 0) {
242
246
  info(`Re-pointed staged-dependency owners in: ${plan.ownerRewrites.join(', ')}`);
@@ -13,7 +13,7 @@ import { loadPatchesManifest, resolvePatchIdentifier } from '../../core/patch-ma
13
13
  import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
14
14
  import { pathExists } from '../../utils/fs.js';
15
15
  import { info, intro, note, warn } from '../../utils/logger.js';
16
- import { commanderArgParser, pickDefined } from '../../utils/options.js';
16
+ import { addWaitLockOption, commanderArgParser, pickDefined } from '../../utils/options.js';
17
17
  import { patchMoveFilesCreateCommand } from './move-files-create.js';
18
18
  import { runMoveFilesInto } from './move-files-into.js';
19
19
  function collectOption(value, previous) {
@@ -161,7 +161,7 @@ export async function patchMoveFilesCommand(projectRoot, fromIdentifier, toIdent
161
161
  */
162
162
  export function registerPatchMoveFiles(parent, context) {
163
163
  const { getProjectRoot, withErrorHandling } = context;
164
- parent
164
+ const command = parent
165
165
  .command('move-files <from> <to>')
166
166
  .description('Preview the re-export commands needed to move file ownership between patches, ' +
167
167
  'or create the target patch and move the files in one transaction with --create --order <n>.')
@@ -175,12 +175,12 @@ export function registerPatchMoveFiles(parent, context) {
175
175
  return n;
176
176
  }))
177
177
  .option('--category <category>', "Category for the created patch (default: the source patch's)")
178
- .option('--description <desc>', 'Description for the created patch')
178
+ .option('-d, --description <desc>', 'Description for the created patch')
179
179
  .option('--dry-run', 'Show what would happen without writing')
180
180
  .option('-y, --yes', 'Skip confirmation prompt (required for non-TTY)')
181
181
  .option('--force-unsafe', 'Bypass projected-lint refusals')
182
- .option('--skip-lint', 'Skip per-patch lint of the projected bodies')
183
- .action(withErrorHandling(async (from, to, options) => {
182
+ .option('--skip-lint', 'Skip per-patch lint of the projected bodies');
183
+ addWaitLockOption(command).action(withErrorHandling(async (from, to, options) => {
184
184
  await patchMoveFilesCommand(getProjectRoot(), from, to, {
185
185
  ...pickDefined({
186
186
  file: options.file,
@@ -192,6 +192,7 @@ export function registerPatchMoveFiles(parent, context) {
192
192
  yes: options.yes,
193
193
  forceUnsafe: options.forceUnsafe,
194
194
  skipLint: options.skipLint,
195
+ waitLock: options.waitLock,
195
196
  }),
196
197
  });
197
198
  }));
@@ -20,11 +20,11 @@ import type { CommandContext } from '../../types/cli.js';
20
20
  import type { PatchRenameOptions } from '../../types/commands/index.js';
21
21
  /**
22
22
  * Runs the `patch rename` command: relabels filename + manifest entry
23
- * for a single patch atomically.
23
+ * (name, category, order, description) for a single patch atomically.
24
24
  *
25
25
  * @param projectRoot - Project root directory
26
26
  * @param identifier - Patch filename, ordinal, or manifest `name`
27
- * @param options - Command options (`--to <new-name>` is required)
27
+ * @param options - Command options (at least one change flag required)
28
28
  */
29
29
  export declare function patchRenameCommand(projectRoot: string, identifier: string, options?: PatchRenameOptions): Promise<void>;
30
30
  /**