@hominis/fireforge 0.40.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/CHANGELOG.md +79 -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/build-audit-platform.js +6 -1
  118. package/dist/src/core/build-audit-registration.js +2 -0
  119. package/dist/src/core/build-audit-resolve.d.ts +13 -0
  120. package/dist/src/core/build-audit-resolve.js +34 -13
  121. package/dist/src/core/build-audit.js +4 -20
  122. package/dist/src/core/build-baseline-types.d.ts +29 -8
  123. package/dist/src/core/build-baseline-types.js +2 -1
  124. package/dist/src/core/build-baseline.d.ts +10 -2
  125. package/dist/src/core/build-baseline.js +39 -8
  126. package/dist/src/core/build-prepare.d.ts +4 -0
  127. package/dist/src/core/build-prepare.js +7 -1
  128. package/dist/src/core/config-paths.d.ts +1 -1
  129. package/dist/src/core/config-paths.js +2 -0
  130. package/dist/src/core/config-validate-test-toolchains.js +3 -2
  131. package/dist/src/core/config-validate.js +34 -36
  132. package/dist/src/core/config.js +16 -3
  133. package/dist/src/core/coverage-extend.d.ts +89 -0
  134. package/dist/src/core/coverage-extend.js +188 -0
  135. package/dist/src/core/destructive.js +3 -1
  136. package/dist/src/core/discard-baseline.js +18 -4
  137. package/dist/src/core/engine-precondition.d.ts +35 -0
  138. package/dist/src/core/engine-precondition.js +50 -0
  139. package/dist/src/core/engine-session-lock.d.ts +38 -1
  140. package/dist/src/core/engine-session-lock.js +78 -4
  141. package/dist/src/core/file-lock.d.ts +7 -0
  142. package/dist/src/core/file-lock.js +20 -30
  143. package/dist/src/core/firefox.js +5 -2
  144. package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
  145. package/dist/src/core/furnace-apply-helpers.js +12 -3
  146. package/dist/src/core/furnace-apply-output.js +9 -0
  147. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
  148. package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
  149. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
  150. package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
  151. package/dist/src/core/furnace-apply.d.ts +16 -4
  152. package/dist/src/core/furnace-apply.js +39 -43
  153. package/dist/src/core/furnace-config.js +2 -0
  154. package/dist/src/core/furnace-css-fragments.js +2 -0
  155. package/dist/src/core/furnace-jsconfig.js +14 -7
  156. package/dist/src/core/furnace-registration-ast.js +8 -8
  157. package/dist/src/core/furnace-registration-remove.js +6 -3
  158. package/dist/src/core/furnace-registration.js +9 -12
  159. package/dist/src/core/furnace-rollback.js +1 -1
  160. package/dist/src/core/furnace-scanner.js +2 -1
  161. package/dist/src/core/furnace-stale-export.js +2 -1
  162. package/dist/src/core/furnace-state-persist.d.ts +4 -6
  163. package/dist/src/core/furnace-validate-compatibility.js +4 -7
  164. package/dist/src/core/furnace-validate-helpers.js +33 -1
  165. package/dist/src/core/furnace-validate-registration.js +16 -0
  166. package/dist/src/core/furnace-validate.js +4 -2
  167. package/dist/src/core/git-diff.js +37 -2
  168. package/dist/src/core/lint-cache.d.ts +1 -1
  169. package/dist/src/core/lint-cache.js +33 -10
  170. package/dist/src/core/mach-build-artifacts.d.ts +63 -1
  171. package/dist/src/core/mach-build-artifacts.js +108 -2
  172. package/dist/src/core/mach-resource-shim.js +1 -0
  173. package/dist/src/core/marionette-port.d.ts +16 -0
  174. package/dist/src/core/marionette-port.js +93 -8
  175. package/dist/src/core/marionette-preflight.js +6 -5
  176. package/dist/src/core/patch-apply.d.ts +20 -13
  177. package/dist/src/core/patch-apply.js +32 -33
  178. package/dist/src/core/patch-export.js +3 -5
  179. package/dist/src/core/patch-lint-checkjs.d.ts +25 -3
  180. package/dist/src/core/patch-lint-checkjs.js +78 -7
  181. package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
  182. package/dist/src/core/patch-lint-creators.d.ts +30 -0
  183. package/dist/src/core/patch-lint-creators.js +45 -0
  184. package/dist/src/core/patch-lint-cross.d.ts +1 -17
  185. package/dist/src/core/patch-lint-cross.js +16 -32
  186. package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
  187. package/dist/src/core/patch-lint-jsdoc.js +15 -5
  188. package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
  189. package/dist/src/core/patch-lint-module-registration.js +88 -0
  190. package/dist/src/core/patch-lint-mozbuild.js +99 -11
  191. package/dist/src/core/patch-lint-reexports.d.ts +1 -0
  192. package/dist/src/core/patch-lint-reexports.js +1 -0
  193. package/dist/src/core/patch-lint.d.ts +8 -0
  194. package/dist/src/core/patch-lint.js +84 -6
  195. package/dist/src/core/patch-lock.d.ts +12 -1
  196. package/dist/src/core/patch-lock.js +40 -4
  197. package/dist/src/core/patch-manifest-consistency.js +15 -3
  198. package/dist/src/core/patch-parse.d.ts +8 -0
  199. package/dist/src/core/patch-parse.js +10 -0
  200. package/dist/src/core/patch-policy.d.ts +7 -0
  201. package/dist/src/core/patch-policy.js +4 -1
  202. package/dist/src/core/patch-transform.d.ts +10 -0
  203. package/dist/src/core/patch-transform.js +13 -2
  204. package/dist/src/core/rebase-session.d.ts +44 -2
  205. package/dist/src/core/rebase-session.js +114 -17
  206. package/dist/src/core/register-browser-content.js +2 -1
  207. package/dist/src/core/register-shared-css.js +2 -1
  208. package/dist/src/core/smoke-patterns.d.ts +0 -8
  209. package/dist/src/core/smoke-patterns.js +3 -2
  210. package/dist/src/core/status-binary.d.ts +55 -0
  211. package/dist/src/core/status-binary.js +124 -0
  212. package/dist/src/core/status-classify.d.ts +6 -1
  213. package/dist/src/core/status-classify.js +101 -73
  214. package/dist/src/core/test-harness-crash.d.ts +32 -1
  215. package/dist/src/core/test-harness-crash.js +59 -4
  216. package/dist/src/core/test-path-scope.js +2 -0
  217. package/dist/src/core/test-stale-check.js +1 -20
  218. package/dist/src/core/toolchain-preflight.js +24 -26
  219. package/dist/src/core/tree-guard.d.ts +1 -1
  220. package/dist/src/core/tree-guard.js +67 -12
  221. package/dist/src/core/tree-store.d.ts +86 -10
  222. package/dist/src/core/tree-store.js +310 -72
  223. package/dist/src/core/typecheck.js +2 -1
  224. package/dist/src/core/wire-destroy.js +14 -3
  225. package/dist/src/core/wire-init.js +37 -40
  226. package/dist/src/core/wire-subscript.js +11 -3
  227. package/dist/src/core/wire-utils.js +4 -4
  228. package/dist/src/core/xpcshell-appdir.js +3 -0
  229. package/dist/src/errors/base.d.ts +13 -0
  230. package/dist/src/errors/base.js +13 -0
  231. package/dist/src/errors/rebase.d.ts +13 -0
  232. package/dist/src/errors/rebase.js +17 -0
  233. package/dist/src/types/commands/options.d.ts +86 -1
  234. package/dist/src/types/commands/patches.d.ts +8 -0
  235. package/dist/src/types/furnace.d.ts +7 -0
  236. package/dist/src/utils/build-info.d.ts +31 -0
  237. package/dist/src/utils/build-info.js +98 -0
  238. package/dist/src/utils/concurrency.d.ts +8 -0
  239. package/dist/src/utils/concurrency.js +21 -0
  240. package/dist/src/utils/errors.d.ts +39 -0
  241. package/dist/src/utils/errors.js +55 -0
  242. package/dist/src/utils/fs.js +5 -12
  243. package/dist/src/utils/logger.d.ts +14 -2
  244. package/dist/src/utils/logger.js +43 -13
  245. package/dist/src/utils/options.d.ts +21 -4
  246. package/dist/src/utils/options.js +26 -6
  247. package/dist/src/utils/package-root.js +7 -4
  248. package/dist/src/utils/process-group.js +13 -2
  249. package/dist/src/utils/stdio-drain.d.ts +28 -0
  250. package/dist/src/utils/stdio-drain.js +54 -0
  251. package/dist/src/utils/validation.d.ts +23 -2
  252. package/dist/src/utils/validation.js +29 -3
  253. package/package.json +9 -9
@@ -2,12 +2,12 @@
2
2
  import { confirm } from '@clack/prompts';
3
3
  import { Command } from 'commander';
4
4
  import { getProjectPaths } from '../core/config.js';
5
+ import { assertEngineGitReady } from '../core/engine-precondition.js';
5
6
  import { clearAppliedFurnaceState } from '../core/furnace-config.js';
6
- import { getHead, hasChanges, isGitRepository, isMissingHeadError, resetChanges, } from '../core/git.js';
7
+ import { hasChanges, resetChanges } from '../core/git.js';
7
8
  import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
8
9
  import { warnIfStaticComponentsStale } from '../core/test-stale-check.js';
9
- import { GeneralError, InvalidArgumentError } from '../errors/base.js';
10
- import { pathExists } from '../utils/fs.js';
10
+ import { InvalidArgumentError } from '../errors/base.js';
11
11
  import { cancel, info, intro, isCancel, outro, spinner, warn } from '../utils/logger.js';
12
12
  import { pickDefined } from '../utils/options.js';
13
13
  /**
@@ -18,26 +18,7 @@ import { pickDefined } from '../utils/options.js';
18
18
  export async function resetCommand(projectRoot, options) {
19
19
  intro('FireForge Reset');
20
20
  const paths = getProjectPaths(projectRoot);
21
- // Check if engine exists
22
- if (!(await pathExists(paths.engine))) {
23
- throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
24
- }
25
- // Check if it's a git repository
26
- if (!(await isGitRepository(paths.engine))) {
27
- throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
28
- }
29
- // Unborn-HEAD guard (mirrors status): an interrupted download leaves a
30
- // repo with no baseline commit, where the entire ~300k-file tree reads
31
- // as untracked — a reset against that state is never what the operator
32
- // wants, and the guidance names the actual fix.
33
- try {
34
- await getHead(paths.engine);
35
- }
36
- catch (headError) {
37
- if (!isMissingHeadError(headError))
38
- throw headError;
39
- throw new GeneralError('Engine repository has no baseline commit yet — a previous "fireforge download" was interrupted before git created the initial Firefox source commit. Re-run "fireforge download --force" to recreate the baseline repository cleanly.');
40
- }
21
+ await assertEngineGitReady(paths.engine);
41
22
  // Check for changes
42
23
  const hasUncommittedChanges = await hasChanges(paths.engine);
43
24
  if (!hasUncommittedChanges) {
@@ -3,7 +3,7 @@ import { join } from 'node:path';
3
3
  import { confirm } from '@clack/prompts';
4
4
  import { Command } from 'commander';
5
5
  import { getProjectPaths, loadConfig, loadState, updateState } from '../core/config.js';
6
- import { isGitRepository } from '../core/git.js';
6
+ import { assertEngineGitReady } from '../core/engine-precondition.js';
7
7
  import { getStagedDiffForFiles } from '../core/git-diff.js';
8
8
  import { stageFiles, unstageFiles } from '../core/git-file-ops.js';
9
9
  import { extractAffectedFiles } from '../core/patch-apply.js';
@@ -30,14 +30,7 @@ export async function resolveCommand(projectRoot, options = {}) {
30
30
  }
31
31
  const { patchFilename } = state.pendingResolution;
32
32
  info(`Resolving conflict for patch: ${patchFilename}`);
33
- // Check if engine exists
34
- if (!(await pathExists(paths.engine))) {
35
- throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
36
- }
37
- // Check if it's a git repository
38
- if (!(await isGitRepository(paths.engine))) {
39
- throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
40
- }
33
+ await assertEngineGitReady(paths.engine);
41
34
  // Non-interactive mode requires an explicit `--yes` to proceed: the
42
35
  // operator is asserting the manual merge is complete and the
43
36
  // refreshed diff is the one to record. Without `--yes`, an accidental
@@ -5,16 +5,17 @@ import { join } from 'node:path';
5
5
  import { Command } from 'commander';
6
6
  import { getProjectPaths, loadConfig } from '../core/config.js';
7
7
  import { warnIfFurnaceStale } from '../core/furnace-staleness.js';
8
- import { buildArtifactMismatchMessage, hasBuildArtifacts, hasRunnableBundle, run, runMachSmoke, } from '../core/mach.js';
8
+ import { hasBuildArtifacts, hasRunnableBundle, run, runMachSmoke } from '../core/mach.js';
9
+ import { assertBuildArtifacts } from '../core/mach-build-artifacts.js';
9
10
  import { compileAllowlistFromFile, compileAllowlistFromStrings, matchAllowlist, matchesSmokeError, } from '../core/smoke-patterns.js';
10
11
  import { GeneralError, InvalidArgumentError } from '../errors/base.js';
11
- import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
12
+ import { BuildError } from '../errors/build.js';
12
13
  import { ExitCode } from '../errors/codes.js';
13
14
  import { SmokeRunError } from '../errors/run.js';
14
15
  import { toError } from '../utils/errors.js';
15
16
  import { pathExists, removeDir, removeFile } from '../utils/fs.js';
16
17
  import { info, intro, verbose, warn } from '../utils/logger.js';
17
- import { commanderArgParser, pickDefined } from '../utils/options.js';
18
+ import { commanderArgParser, pickDefined, stringListOption } from '../utils/options.js';
18
19
  /**
19
20
  * Exit code returned by smoke-run mode when the captured console stream
20
21
  * produced one or more error lines that did NOT match the operator's
@@ -81,20 +82,12 @@ export async function runCommand(projectRoot, options = {}) {
81
82
  throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
82
83
  }
83
84
  const buildCheck = await hasBuildArtifacts(paths.engine);
84
- if (buildCheck.ambiguous && buildCheck.objDirs && buildCheck.objDirs.length > 0) {
85
- throw new AmbiguousBuildArtifactsError(buildCheck.objDirs);
86
- }
87
- const mismatchMessage = buildArtifactMismatchMessage(paths.engine, buildCheck, 'Run');
88
- if (mismatchMessage) {
89
- throw new GeneralError(mismatchMessage);
90
- }
91
- if (!buildCheck.exists) {
92
- const detail = buildCheck.objDir
93
- ? `Build artifacts incomplete in ${buildCheck.objDir}/`
94
- : 'No build artifacts found (obj-*/ directory missing)';
95
- throw new GeneralError(`Run requires a completed build. ${detail}\n\n` +
96
- "Run 'fireforge build' first, then rerun 'fireforge run'.");
97
- }
85
+ assertBuildArtifacts(paths.engine, buildCheck, {
86
+ label: 'Run',
87
+ requirement: 'Run requires a completed build.',
88
+ remediation: "Run 'fireforge build' first, then rerun 'fireforge run'.",
89
+ requireExisting: true,
90
+ });
98
91
  // `hasBuildArtifacts` only checks for an `obj-*/dist/` directory; a
99
92
  // build that configured but hasn't yet produced the launchable binary
100
93
  // (common in a long real Firefox compile that the operator stopped
@@ -354,10 +347,7 @@ export function registerRun(program, { getProjectRoot, withErrorHandling }) {
354
347
  }
355
348
  return parsed;
356
349
  }))
357
- .option('--console-allow <regex>', 'Allowlist regex (repeatable). Lines that match any entry do not count toward the smoke exit code.', (value, acc) => {
358
- acc.push(value);
359
- return acc;
360
- }, [])
350
+ .option('--console-allow <regex>', 'Allowlist regex (repeatable). Lines that match any entry do not count toward the smoke exit code.', ...stringListOption())
361
351
  .option('--console-allow-file <path>', 'Newline-delimited allowlist regex file. Blank lines and # comments are ignored.')
362
352
  .option('--capture-console <file>', 'Mirror captured console output to <file> for post-exit inspection.')
363
353
  .option('--headless', 'Launch the browser with --headless. Recommended for --smoke-exit on a shared desktop: input into a headed smoke window contaminates the console capture.')
@@ -4,13 +4,7 @@ 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 { isValidFirefoxCandidate, isValidFirefoxProduct } from '../utils/validation.js';
8
- const SOURCE_PRODUCTS = [
9
- 'firefox',
10
- 'firefox-esr',
11
- 'firefox-beta',
12
- 'firefox-devedition',
13
- ];
7
+ import { FIREFOX_PRODUCTS, isValidFirefoxCandidate, isValidFirefoxProduct, } from '../utils/validation.js';
14
8
  function isRecord(value) {
15
9
  return typeof value === 'object' && value !== null && !Array.isArray(value);
16
10
  }
@@ -25,7 +19,7 @@ function parseSourceProduct(product) {
25
19
  if (isValidFirefoxProduct(product)) {
26
20
  return product;
27
21
  }
28
- throw new InvalidArgumentError(`--product must be one of: ${SOURCE_PRODUCTS.join(', ')}`, '--product');
22
+ throw new InvalidArgumentError(`--product must be one of: ${FIREFOX_PRODUCTS.join(', ')}`, '--product');
29
23
  }
30
24
  function parseSourceCandidate(candidate) {
31
25
  if (isValidFirefoxCandidate(candidate)) {
@@ -99,7 +93,7 @@ export function registerSource(program, { getProjectRoot, withErrorHandling }) {
99
93
  .description('Atomically set Firefox source version, product, and optional checksum')
100
94
  .requiredOption('--version <version>', 'Firefox version to base on')
101
95
  .addOption(new Option('--product <product>', 'Firefox product')
102
- .choices([...SOURCE_PRODUCTS])
96
+ .choices([...FIREFOX_PRODUCTS])
103
97
  .makeOptionMandatory())
104
98
  .option('--sha256 <hash>', 'Pinned SHA-256 for the resolved source archive')
105
99
  .option('--clear-sha256', 'Clear any existing pinned SHA-256')
@@ -26,6 +26,19 @@ export declare function resolveStatusCheckPolicy(options: {
26
26
  ownership?: boolean;
27
27
  testCoverage?: boolean;
28
28
  }): StatusCheckPolicy;
29
+ /** One fail-set classification with a non-empty file list. */
30
+ export interface StatusCheckOffender {
31
+ classification: FileClassification;
32
+ count: number;
33
+ files: string[];
34
+ }
35
+ /**
36
+ * Collects the fail-set classifications that have offending files, in
37
+ * policy order with full file lists. Shared by the human `--check`
38
+ * verdict and the `--json --summary` payload so the two views can never
39
+ * disagree about what failed.
40
+ */
41
+ export declare function collectStatusCheckOffenders(classified: readonly ClassifiedFile[], policy: StatusCheckPolicy): StatusCheckOffender[];
29
42
  /**
30
43
  * Throws a GeneralError (exit 1) when the policy is enabled and any of
31
44
  * its classifications is non-empty, naming each offending classification
@@ -6,8 +6,14 @@ const ALL_CLASSIFICATIONS = [
6
6
  'branding',
7
7
  'furnace',
8
8
  'conflict',
9
+ 'binary-unsupported',
9
10
  ];
10
- /** Default `--check` policy: everything outside {patch-backed, branding, furnace} fails. */
11
+ /**
12
+ * Default `--check` policy: everything outside {patch-backed, branding,
13
+ * furnace, binary-unsupported} fails. `binary-unsupported` is deliberately
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`.
16
+ */
11
17
  const DEFAULT_FAIL_CLASSIFICATIONS = [
12
18
  'unmanaged',
13
19
  'patch-owned-drift',
@@ -59,6 +65,24 @@ function parseFailOnClassifications(raw) {
59
65
  return classifications;
60
66
  }
61
67
  const CHECK_FILE_PREVIEW_MAX = 3;
68
+ /**
69
+ * Collects the fail-set classifications that have offending files, in
70
+ * policy order with full file lists. Shared by the human `--check`
71
+ * verdict and the `--json --summary` payload so the two views can never
72
+ * disagree about what failed.
73
+ */
74
+ export function collectStatusCheckOffenders(classified, policy) {
75
+ const offenders = [];
76
+ for (const classification of policy.failOn) {
77
+ const files = classified
78
+ .filter((entry) => entry.classification === classification)
79
+ .map((entry) => entry.file);
80
+ if (files.length === 0)
81
+ continue;
82
+ offenders.push({ classification, count: files.length, files });
83
+ }
84
+ return offenders;
85
+ }
62
86
  /**
63
87
  * Throws a GeneralError (exit 1) when the policy is enabled and any of
64
88
  * its classifications is non-empty, naming each offending classification
@@ -68,19 +92,13 @@ const CHECK_FILE_PREVIEW_MAX = 3;
68
92
  export function runStatusCheck(classified, policy) {
69
93
  if (!policy.checkEnabled)
70
94
  return;
71
- const offending = [];
72
- for (const classification of policy.failOn) {
73
- const files = classified
74
- .filter((entry) => entry.classification === classification)
75
- .map((entry) => entry.file);
76
- if (files.length === 0)
77
- continue;
78
- const preview = files.slice(0, CHECK_FILE_PREVIEW_MAX).join(', ');
79
- const more = files.length > CHECK_FILE_PREVIEW_MAX
80
- ? `, +${String(files.length - CHECK_FILE_PREVIEW_MAX)} more`
95
+ const offending = collectStatusCheckOffenders(classified, policy).map((offender) => {
96
+ const preview = offender.files.slice(0, CHECK_FILE_PREVIEW_MAX).join(', ');
97
+ const more = offender.count > CHECK_FILE_PREVIEW_MAX
98
+ ? `, +${String(offender.count - CHECK_FILE_PREVIEW_MAX)} more`
81
99
  : '';
82
- offending.push(`${String(files.length)} ${classification} (${preview}${more})`);
83
- }
100
+ return `${String(offender.count)} ${offender.classification} (${preview}${more})`;
101
+ });
84
102
  if (offending.length === 0)
85
103
  return;
86
104
  throw new GeneralError(`status --check failed: ${offending.join(', ')}. Export or adopt unmanaged files, ` +
@@ -0,0 +1,31 @@
1
+ /**
2
+ * `status --json` payload rendering (schemaVersion 1). Helper module
3
+ * consumed by status.ts (which sits at the max-lines budget); no
4
+ * top-level registrar is exported and none is wanted.
5
+ *
6
+ * Two shapes share one counting pass:
7
+ * - the full payload (`renderJsonStatus`) with the per-file `files[]` list;
8
+ * - the `--summary` gate payload (`renderJsonSummaryStatus`, FORGE K8),
9
+ * which omits `files[]` entirely — engine-clean gates need the verdict,
10
+ * per-class counts, and offender names, not a payload that grows with
11
+ * the queue (175 KB+ observed on a 290-patch consumer).
12
+ */
13
+ import type { ClassifiedFile } from '../core/status-classify.js';
14
+ import { type StatusCheckPolicy } from './status-check.js';
15
+ import type { OwnershipJsonBlock } from './status-ownership.js';
16
+ /**
17
+ * Full `--json` payload: summary plus the per-file `files[]` list, and —
18
+ * under `--include-ownership` (FORGE L3) — the additive `ownership` block.
19
+ * Additive within schemaVersion 1: a consumer that does not ask for the
20
+ * block never sees the key.
21
+ */
22
+ export declare function renderJsonStatus(classified: readonly ClassifiedFile[], ownership?: OwnershipJsonBlock): void;
23
+ /**
24
+ * Summary-only `--json --summary` payload: counts always; when the
25
+ * `--check`/`--fail-on` policy is active, a `check` block with the active
26
+ * fail set, the verdict, and the offending files per fail-set
27
+ * classification (sourced from the same collector as the human `--check`
28
+ * message, so the two views can never disagree). The absent `files[]` is
29
+ * the mode signal within schemaVersion 1 — consumers opt in via the flag.
30
+ */
31
+ export declare function renderJsonSummaryStatus(classified: readonly ClassifiedFile[], policy: StatusCheckPolicy, ownership?: OwnershipJsonBlock): void;
@@ -0,0 +1,76 @@
1
+ import { collectStatusCheckOffenders, } from './status-check.js';
2
+ function countByClassification(classified) {
3
+ const byClassification = {
4
+ unmanaged: 0,
5
+ 'patch-owned-drift': 0,
6
+ 'patch-backed': 0,
7
+ branding: 0,
8
+ furnace: 0,
9
+ conflict: 0,
10
+ 'binary-unsupported': 0,
11
+ };
12
+ for (const file of classified) {
13
+ byClassification[file.classification]++;
14
+ }
15
+ return byClassification;
16
+ }
17
+ /**
18
+ * Full `--json` payload: summary plus the per-file `files[]` list, and —
19
+ * under `--include-ownership` (FORGE L3) — the additive `ownership` block.
20
+ * Additive within schemaVersion 1: a consumer that does not ask for the
21
+ * block never sees the key.
22
+ */
23
+ export function renderJsonStatus(classified, ownership) {
24
+ const outputFiles = classified.map((f) => {
25
+ const entry = {
26
+ file: f.file,
27
+ status: f.status.trim(),
28
+ classification: f.classification,
29
+ patch: f.owner ?? null,
30
+ };
31
+ if (f.classification === 'conflict' && f.claimedBy && f.claimedBy.length > 0) {
32
+ entry.claimedBy = [...f.claimedBy];
33
+ }
34
+ return entry;
35
+ });
36
+ const output = {
37
+ schemaVersion: 1,
38
+ summary: {
39
+ total: outputFiles.length,
40
+ byClassification: countByClassification(classified),
41
+ },
42
+ files: outputFiles,
43
+ ...(ownership !== undefined ? { ownership } : {}),
44
+ };
45
+ process.stdout.write(JSON.stringify(output, null, 2) + '\n');
46
+ }
47
+ /**
48
+ * Summary-only `--json --summary` payload: counts always; when the
49
+ * `--check`/`--fail-on` policy is active, a `check` block with the active
50
+ * fail set, the verdict, and the offending files per fail-set
51
+ * classification (sourced from the same collector as the human `--check`
52
+ * message, so the two views can never disagree). The absent `files[]` is
53
+ * the mode signal within schemaVersion 1 — consumers opt in via the flag.
54
+ */
55
+ export function renderJsonSummaryStatus(classified, policy, ownership) {
56
+ const output = {
57
+ schemaVersion: 1,
58
+ summary: {
59
+ total: classified.length,
60
+ byClassification: countByClassification(classified),
61
+ },
62
+ };
63
+ if (policy.checkEnabled) {
64
+ const offenders = collectStatusCheckOffenders(classified, policy);
65
+ output.check = {
66
+ enabled: true,
67
+ failOn: policy.failOn,
68
+ failed: offenders.length > 0,
69
+ offenders,
70
+ };
71
+ }
72
+ if (ownership !== undefined) {
73
+ output.ownership = ownership;
74
+ }
75
+ process.stdout.write(JSON.stringify(output, null, 2) + '\n');
76
+ }
@@ -7,6 +7,7 @@ export interface ClassifiedBuckets {
7
7
  patchOwnedDrift: ClassifiedFile[];
8
8
  branding: ClassifiedFile[];
9
9
  furnace: ClassifiedFile[];
10
+ binaryUnsupported: ClassifiedFile[];
10
11
  }
11
12
  /** Renders the unmanaged-only status view and registration hints. */
12
13
  export declare function renderUnmanagedOnly(unmanagedFiles: ClassifiedFile[], totalModified: number, projectRoot: string, binaryName: string): Promise<void>;
@@ -107,7 +107,7 @@ function renderConflictSection(conflict) {
107
107
  }
108
108
  /** Renders the default classified status buckets. */
109
109
  export async function renderDefaultStatus(totalModified, buckets, projectRoot, binaryName) {
110
- const { conflict, unmanaged, patchBacked, patchOwnedDrift, branding, furnace } = buckets;
110
+ const { conflict, unmanaged, patchBacked, patchOwnedDrift, branding, furnace, binaryUnsupported, } = buckets;
111
111
  info(`${totalModified} modified file${totalModified === 1 ? '' : 's'}:\n`);
112
112
  // Sections render in this fixed order, separated by a blank line
113
113
  // whenever an earlier section already printed (the pre-refactor code
@@ -163,6 +163,15 @@ export async function renderDefaultStatus(totalModified, buckets, projectRoot, b
163
163
  printStatusGroups(furnace);
164
164
  },
165
165
  },
166
+ {
167
+ files: binaryUnsupported,
168
+ label: 'binary — comparison unsupported',
169
+ render: () => {
170
+ warn('Binary — comparison unsupported:');
171
+ printStatusGroups(binaryUnsupported);
172
+ info('These patch-owned binary files carry no comparable blob hash in their patch body, so durability cannot be verified. Re-export the owning patch with a git binary body to make them comparable.');
173
+ },
174
+ },
166
175
  ];
167
176
  let printedAny = false;
168
177
  for (const section of sections) {
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Ownership-table assembly for `status`, split out of `status.ts` (FORGE
3
+ * L3) so the human `--ownership` mode and the additive `ownership` block on
4
+ * the `--json` payload build the SAME rows from the SAME scan.
5
+ *
6
+ * Before L3 the `--ownership` branch re-implemented the shared scan and
7
+ * inlined a second copy of the classification call, then discarded
8
+ * everything but each file's classification — three back-to-back `status`
9
+ * invocations in a gate therefore paid three full worktree scans. Both
10
+ * callers now hand this module the already-classified files.
11
+ *
12
+ * No renderer lives here: `renderOwnershipTable` (human) and the JSON
13
+ * serializer stay with their respective output layers.
14
+ */
15
+ import type { OwnershipRow } from '../core/ownership-table.js';
16
+ import type { ClassifiedFile, StatusFile } from '../core/status-classify.js';
17
+ import type { PatchMetadata } from '../types/commands/index.js';
18
+ /** Counts the human outro prints and the JSON block carries. */
19
+ export interface OwnershipSummary {
20
+ managed: number;
21
+ unmanaged: number;
22
+ conflicts: number;
23
+ }
24
+ /** The additive `ownership` block on the schemaVersion-1 JSON payload. */
25
+ export interface OwnershipJsonBlock {
26
+ rows: OwnershipRow[];
27
+ summary: OwnershipSummary;
28
+ }
29
+ /**
30
+ * Builds the flat path→owning-patch rows. Sources are the manifest's
31
+ * `filesAffected`, worktree drift, and the cross-patch
32
+ * duplicate-new-file-creation map produced by walking each patch body —
33
+ * the last being the alignment fix between `status --ownership` and
34
+ * `fireforge verify` (see `buildOwnershipTable`'s header).
35
+ * @param patchesDir - Path to the patches directory
36
+ * @param manifestPatches - Patch rows from patches.json (empty when absent)
37
+ * @param files - Scanned worktree entries, temp-filtered
38
+ * @param classified - The same entries, already classified
39
+ */
40
+ export declare function collectOwnershipRows(patchesDir: string, manifestPatches: readonly PatchMetadata[], files: StatusFile[], classified: readonly ClassifiedFile[]): Promise<OwnershipRow[]>;
41
+ /**
42
+ * Counts managed / unmanaged / conflicted rows. Conflicts are rows flagged
43
+ * `conflict`, not a separate collection.
44
+ * @param rows - Rows from {@link collectOwnershipRows}
45
+ */
46
+ export declare function summarizeOwnership(rows: readonly OwnershipRow[]): OwnershipSummary;
47
+ /**
48
+ * Assembles the JSON block. Rows are carried verbatim so the machine shape
49
+ * and the rendered table can never disagree.
50
+ * @param rows - Rows from {@link collectOwnershipRows}
51
+ */
52
+ export declare function buildOwnershipJsonBlock(rows: OwnershipRow[]): OwnershipJsonBlock;
@@ -0,0 +1,57 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Ownership-table assembly for `status`, split out of `status.ts` (FORGE
4
+ * L3) so the human `--ownership` mode and the additive `ownership` block on
5
+ * the `--json` payload build the SAME rows from the SAME scan.
6
+ *
7
+ * Before L3 the `--ownership` branch re-implemented the shared scan and
8
+ * inlined a second copy of the classification call, then discarded
9
+ * everything but each file's classification — three back-to-back `status`
10
+ * invocations in a gate therefore paid three full worktree scans. Both
11
+ * callers now hand this module the already-classified files.
12
+ *
13
+ * No renderer lives here: `renderOwnershipTable` (human) and the JSON
14
+ * serializer stay with their respective output layers.
15
+ */
16
+ import { buildOwnershipTable } from '../core/ownership-table.js';
17
+ import { buildPatchQueueContext, collectNewFileCreatorsByPath } from '../core/patch-lint.js';
18
+ import { pathExists } from '../utils/fs.js';
19
+ /**
20
+ * Builds the flat path→owning-patch rows. Sources are the manifest's
21
+ * `filesAffected`, worktree drift, and the cross-patch
22
+ * duplicate-new-file-creation map produced by walking each patch body —
23
+ * the last being the alignment fix between `status --ownership` and
24
+ * `fireforge verify` (see `buildOwnershipTable`'s header).
25
+ * @param patchesDir - Path to the patches directory
26
+ * @param manifestPatches - Patch rows from patches.json (empty when absent)
27
+ * @param files - Scanned worktree entries, temp-filtered
28
+ * @param classified - The same entries, already classified
29
+ */
30
+ export async function collectOwnershipRows(patchesDir, manifestPatches, files, classified) {
31
+ // Only walk patch bodies when the directory actually exists: a fresh
32
+ // project with no queue degrades to filesAffected-only behavior.
33
+ const newFileCreatorsByPath = (await pathExists(patchesDir))
34
+ ? collectNewFileCreatorsByPath(await buildPatchQueueContext(patchesDir))
35
+ : new Map();
36
+ return buildOwnershipTable([...manifestPatches], files, newFileCreatorsByPath, new Map(classified.map((entry) => [entry.file, entry.classification])));
37
+ }
38
+ /**
39
+ * Counts managed / unmanaged / conflicted rows. Conflicts are rows flagged
40
+ * `conflict`, not a separate collection.
41
+ * @param rows - Rows from {@link collectOwnershipRows}
42
+ */
43
+ export function summarizeOwnership(rows) {
44
+ return {
45
+ managed: rows.filter((r) => !r.unmanaged).length,
46
+ unmanaged: rows.filter((r) => r.unmanaged).length,
47
+ conflicts: rows.filter((r) => r.conflict).length,
48
+ };
49
+ }
50
+ /**
51
+ * Assembles the JSON block. Rows are carried verbatim so the machine shape
52
+ * and the rendered table can never disagree.
53
+ * @param rows - Rows from {@link collectOwnershipRows}
54
+ */
55
+ export function buildOwnershipJsonBlock(rows) {
56
+ return { rows, summary: summarizeOwnership(rows) };
57
+ }