@hominis/fireforge 0.30.0 → 0.31.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 (141) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/README.md +22 -5
  3. package/dist/src/commands/export-all.js +5 -15
  4. package/dist/src/commands/export-flow.d.ts +6 -0
  5. package/dist/src/commands/export-flow.js +6 -1
  6. package/dist/src/commands/export-placement-gate.d.ts +38 -0
  7. package/dist/src/commands/export-placement-gate.js +105 -0
  8. package/dist/src/commands/export-shared.d.ts +28 -0
  9. package/dist/src/commands/export-shared.js +36 -0
  10. package/dist/src/commands/export.js +47 -112
  11. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +0 -13
  12. package/dist/src/commands/furnace/chrome-doc-templates.js +1 -1
  13. package/dist/src/commands/furnace/create-dry-run.d.ts +1 -1
  14. package/dist/src/commands/furnace/create.d.ts +1 -2
  15. package/dist/src/commands/furnace/deploy.js +36 -114
  16. package/dist/src/commands/furnace/refresh.js +52 -32
  17. package/dist/src/commands/furnace/sync.js +2 -0
  18. package/dist/src/commands/import.js +108 -73
  19. package/dist/src/commands/lint-per-patch.d.ts +1 -1
  20. package/dist/src/commands/lint-per-patch.js +119 -78
  21. package/dist/src/commands/lint.d.ts +1 -58
  22. package/dist/src/commands/lint.js +96 -84
  23. package/dist/src/commands/patch/compact.d.ts +5 -2
  24. package/dist/src/commands/patch/compact.js +85 -25
  25. package/dist/src/commands/patch/delete.js +17 -17
  26. package/dist/src/commands/patch/index.js +2 -0
  27. package/dist/src/commands/patch/lint-ignore.js +3 -16
  28. package/dist/src/commands/patch/move-files.js +2 -0
  29. package/dist/src/commands/patch/patch-context.d.ts +41 -0
  30. package/dist/src/commands/patch/patch-context.js +53 -0
  31. package/dist/src/commands/patch/rename.js +10 -15
  32. package/dist/src/commands/patch/reorder.d.ts +0 -2
  33. package/dist/src/commands/patch/reorder.js +18 -19
  34. package/dist/src/commands/patch/split-plan.d.ts +66 -0
  35. package/dist/src/commands/patch/split-plan.js +178 -0
  36. package/dist/src/commands/patch/split.d.ts +30 -0
  37. package/dist/src/commands/patch/split.js +283 -0
  38. package/dist/src/commands/patch/staged-dependency.d.ts +1 -7
  39. package/dist/src/commands/patch/staged-dependency.js +4 -17
  40. package/dist/src/commands/patch/tier.js +4 -17
  41. package/dist/src/commands/re-export-scan.js +8 -1
  42. package/dist/src/commands/rebase/summary.d.ts +1 -5
  43. package/dist/src/commands/rebase/summary.js +1 -1
  44. package/dist/src/commands/status-output.js +77 -68
  45. package/dist/src/commands/test-diagnose.d.ts +23 -0
  46. package/dist/src/commands/test-diagnose.js +210 -0
  47. package/dist/src/commands/test-run.d.ts +58 -0
  48. package/dist/src/commands/test-run.js +88 -0
  49. package/dist/src/commands/test.js +169 -257
  50. package/dist/src/commands/token.js +15 -1
  51. package/dist/src/commands/wire.js +109 -78
  52. package/dist/src/core/build-audit.d.ts +1 -1
  53. package/dist/src/core/build-audit.js +2 -46
  54. package/dist/src/core/build-baseline-types.d.ts +38 -0
  55. package/dist/src/core/build-baseline-types.js +10 -0
  56. package/dist/src/core/build-baseline.d.ts +1 -31
  57. package/dist/src/core/build-prepare.d.ts +1 -1
  58. package/dist/src/core/build-prepare.js +2 -45
  59. package/dist/src/core/config-paths.d.ts +0 -8
  60. package/dist/src/core/config-paths.js +4 -4
  61. package/dist/src/core/config-state.d.ts +0 -6
  62. package/dist/src/core/config-state.js +1 -1
  63. package/dist/src/core/config-validate-patch-policy.js +12 -13
  64. package/dist/src/core/config-validate.js +48 -28
  65. package/dist/src/core/engine-changes.d.ts +24 -0
  66. package/dist/src/core/engine-changes.js +64 -0
  67. package/dist/src/core/firefox-cache.d.ts +0 -5
  68. package/dist/src/core/firefox-cache.js +1 -1
  69. package/dist/src/core/firefox-download.d.ts +0 -6
  70. package/dist/src/core/firefox-download.js +1 -1
  71. package/dist/src/core/furnace-apply-helpers.d.ts +1 -8
  72. package/dist/src/core/furnace-apply-helpers.js +11 -20
  73. package/dist/src/core/furnace-apply.d.ts +1 -1
  74. package/dist/src/core/furnace-apply.js +1 -1
  75. package/dist/src/core/furnace-checksum-utils.d.ts +7 -0
  76. package/dist/src/core/furnace-checksum-utils.js +15 -0
  77. package/dist/src/core/furnace-config-validate.d.ts +31 -0
  78. package/dist/src/core/furnace-config-validate.js +133 -0
  79. package/dist/src/core/furnace-config.d.ts +4 -32
  80. package/dist/src/core/furnace-config.js +15 -111
  81. package/dist/src/core/furnace-constants.d.ts +0 -10
  82. package/dist/src/core/furnace-constants.js +2 -2
  83. package/dist/src/core/furnace-css-fragments.d.ts +79 -0
  84. package/dist/src/core/furnace-css-fragments.js +243 -0
  85. package/dist/src/core/furnace-jsconfig.d.ts +63 -0
  86. package/dist/src/core/furnace-jsconfig.js +171 -0
  87. package/dist/src/core/furnace-validate-helpers.d.ts +16 -14
  88. package/dist/src/core/furnace-validate-helpers.js +40 -1
  89. package/dist/src/core/furnace-validate-registration.js +16 -1
  90. package/dist/src/core/furnace-validate.js +54 -2
  91. package/dist/src/core/git-file-ops.d.ts +0 -12
  92. package/dist/src/core/git-file-ops.js +2 -2
  93. package/dist/src/core/lint-cache.d.ts +3 -13
  94. package/dist/src/core/lint-cache.js +11 -5
  95. package/dist/src/core/mach.d.ts +5 -1
  96. package/dist/src/core/mach.js +6 -2
  97. package/dist/src/core/manifest-register.d.ts +5 -16
  98. package/dist/src/core/manifest-register.js +3 -1
  99. package/dist/src/core/patch-lint-checkjs.js +53 -7
  100. package/dist/src/core/patch-lint-jsdoc.js +63 -4
  101. package/dist/src/core/patch-lint-observer.d.ts +37 -0
  102. package/dist/src/core/patch-lint-observer.js +168 -0
  103. package/dist/src/core/patch-lint.js +132 -125
  104. package/dist/src/core/patch-manifest-io.d.ts +16 -0
  105. package/dist/src/core/patch-manifest-io.js +44 -2
  106. package/dist/src/core/patch-manifest-validate.d.ts +1 -8
  107. package/dist/src/core/patch-manifest-validate.js +1 -1
  108. package/dist/src/core/patch-manifest.d.ts +1 -1
  109. package/dist/src/core/patch-manifest.js +1 -1
  110. package/dist/src/core/patch-policy.d.ts +0 -4
  111. package/dist/src/core/patch-policy.js +10 -4
  112. package/dist/src/core/register-browser-content.d.ts +1 -1
  113. package/dist/src/core/register-module.d.ts +1 -1
  114. package/dist/src/core/register-result.d.ts +21 -0
  115. package/dist/src/core/register-result.js +9 -0
  116. package/dist/src/core/register-shared-css.d.ts +1 -1
  117. package/dist/src/core/register-test-manifest.d.ts +1 -1
  118. package/dist/src/core/test-harness-crash.d.ts +61 -0
  119. package/dist/src/core/test-harness-crash.js +140 -0
  120. package/dist/src/core/test-stale-check.d.ts +1 -1
  121. package/dist/src/core/test-stale-check.js +2 -46
  122. package/dist/src/core/test-xpcshell-retry.d.ts +1 -1
  123. package/dist/src/core/test-xpcshell-retry.js +4 -2
  124. package/dist/src/core/token-dark-mode.js +14 -26
  125. package/dist/src/core/token-manager.d.ts +4 -0
  126. package/dist/src/core/token-manager.js +70 -16
  127. package/dist/src/core/typecheck-shim.d.ts +0 -21
  128. package/dist/src/core/typecheck-shim.js +26 -4
  129. package/dist/src/core/wire-utils.js +37 -44
  130. package/dist/src/types/commands/index.d.ts +1 -1
  131. package/dist/src/types/commands/options.d.ts +105 -0
  132. package/dist/src/types/furnace.d.ts +12 -1
  133. package/dist/src/utils/elapsed.d.ts +0 -2
  134. package/dist/src/utils/elapsed.js +1 -1
  135. package/dist/src/utils/fs.d.ts +0 -5
  136. package/dist/src/utils/fs.js +1 -1
  137. package/dist/src/utils/regex.d.ts +0 -6
  138. package/dist/src/utils/regex.js +3 -3
  139. package/dist/src/utils/validation.d.ts +0 -8
  140. package/dist/src/utils/validation.js +2 -2
  141. package/package.json +6 -4
@@ -144,39 +144,12 @@ async function ensureSubscriptSourceExists(projectRoot, subscriptDir, name, dryR
144
144
  }
145
145
  }
146
146
  /**
147
- * Wires a chrome subscript into the browser.
148
- *
149
- * @param projectRoot - Root directory of the project
150
- * @param name - Subscript name (without .js extension)
151
- * @param options - Command options
147
+ * Validates the `--dom` fragment argument and computes its engine-root-
148
+ * relative path. Accepts absolute, repo-root-relative (`engine/...`), and
149
+ * engine-relative inputs; rejects missing files and paths escaping
150
+ * engine/. Returns undefined when `--dom` was not supplied.
152
151
  */
153
- export async function wireCommand(projectRoot, name, options = {}) {
154
- intro('Wire');
155
- validateWireName(name);
156
- if (options.after !== undefined) {
157
- // --after references an existing init block by its subscript name, so
158
- // it must follow the same naming rules as `name` itself. Without this
159
- // check, a caller could sneak a path-traversal segment in through
160
- // --after and have it forwarded unchanged to the lookup layer.
161
- validateWireName(options.after);
162
- }
163
- // Validate init/destroy expressions BEFORE the dry-run/real fork so
164
- // both paths enforce the same contract. Pre-0.16.0, validation only
165
- // ran inside `addInitToBrowserInit`/`addDestroyToBrowserInit` (the
166
- // real-execution path), so `--dry-run --init 'void 0'` succeeded and
167
- // rendered a plausible-looking preview even though the real run would
168
- // reject the same arguments. Dropping `void 0` into the template
169
- // silently (or breaking out of the string literal) was already
170
- // prevented downstream — this hoist just makes the failure surface
171
- // identical in preview mode.
172
- if (options.init !== undefined) {
173
- validateWireExpression(options.init, 'init expression');
174
- }
175
- if (options.destroy !== undefined) {
176
- validateWireExpression(options.destroy, 'destroy expression');
177
- }
178
- consumeParserFallbackEvents();
179
- const subscriptDir = await resolveWireSubscriptDir(projectRoot, options);
152
+ async function resolveDomFragmentPath(projectRoot, dom) {
180
153
  // Validate DOM fragment file exists and compute path relative to engine root.
181
154
  //
182
155
  // Accepts three shapes:
@@ -193,12 +166,11 @@ export async function wireCommand(projectRoot, name, options = {}) {
193
166
  // packaging-breaking nonsense. For absolute inputs this pre-existing
194
167
  // contract was fine — `toRootRelativePath` handles absolute candidates
195
168
  // correctly — so we only strip the prefix when the input is relative.
196
- let domFilePath;
197
- if (options.dom) {
169
+ if (!dom)
170
+ return undefined;
171
+ {
198
172
  const paths = getProjectPaths(projectRoot);
199
- const domCandidate = isExplicitAbsolutePath(options.dom)
200
- ? options.dom
201
- : stripEnginePrefix(options.dom);
173
+ const domCandidate = isExplicitAbsolutePath(dom) ? dom : stripEnginePrefix(dom);
202
174
  // `pathExists` resolves relative paths against CWD, so an engine-
203
175
  // relative `domCandidate` (e.g. `browser/base/content/foo.inc.xhtml`)
204
176
  // would be probed inside the operator's shell directory rather than
@@ -213,50 +185,21 @@ export async function wireCommand(projectRoot, name, options = {}) {
213
185
  ? domCandidate
214
186
  : join(paths.engine, domCandidate);
215
187
  if (!(await pathExists(domProbePath))) {
216
- throw new InvalidArgumentError(`DOM fragment file not found: ${options.dom}`, 'dom');
188
+ throw new InvalidArgumentError(`DOM fragment file not found: ${dom}`, 'dom');
217
189
  }
218
190
  if (!isPathInsideRoot(paths.engine, domCandidate)) {
219
- throw new InvalidArgumentError(`DOM fragment file must stay within engine/: ${options.dom}`, 'dom');
191
+ throw new InvalidArgumentError(`DOM fragment file must stay within engine/: ${dom}`, 'dom');
220
192
  }
221
- domFilePath = toRootRelativePath(paths.engine, domCandidate);
222
- }
223
- // Resolve the chrome document the `#include` directive will land in.
224
- // Only consulted when `--dom` is supplied — we still resolve it here so
225
- // the dry-run plan can print the target accurately.
226
- //
227
- // `stripEnginePrefix` is applied so `--target engine/browser/base/browser.xhtml`
228
- // and `--target browser/base/browser.xhtml` are treated identically,
229
- // matching the `--dom` normalization above. Absolute `--target` paths
230
- // stay absolute (the containment check downstream rejects them).
231
- const normalizedTarget = options.target !== undefined && !isExplicitAbsolutePath(options.target)
232
- ? stripEnginePrefix(options.target)
233
- : options.target;
234
- if (normalizedTarget !== undefined && !isContainedRelativePath(normalizedTarget)) {
235
- throw new InvalidArgumentError(`Target chrome document must stay within engine/: ${options.target ?? ''}`, 'target');
193
+ return toRootRelativePath(paths.engine, domCandidate);
236
194
  }
237
- const domTargetPath = await resolveDomTargetPath(projectRoot, normalizedTarget);
238
- if (domFilePath) {
239
- await assertDomTargetIsWireable(projectRoot, domFilePath, domTargetPath);
240
- }
241
- // Verify the subscript file exists in engine/ (skip for dry-run:
242
- // dry-run is meant to preview the mutation plan without requiring
243
- // the subscript to already exist, matching the "plan before write"
244
- // pattern operators rely on for setup scripts).
245
- //
246
- // Dry-run keeps the existence check advisory rather than fatal: the
247
- // "wire first, create file after" workflow is a legitimate use of
248
- // preview, but operators who run dry-run over a typo were surprised
249
- // when the real command then refused with `Subscript file not
250
- // found`. 2026-04-23 eval (Finding in eval 2): dry-run produced a
251
- // plausible plan and the non-dry-run invocation then errored. The
252
- // info line surfaces the mismatch in preview mode so the operator
253
- // can act on the warning before re-running without --dry-run.
254
- await ensureSubscriptSourceExists(projectRoot, subscriptDir, name, options.dryRun ?? false);
255
- if (options.dryRun) {
256
- printWireDryRun(getProjectPaths(projectRoot).engine, name, subscriptDir, domFilePath, domTargetPath, options);
257
- return;
258
- }
259
- const result = await wireSubscript(projectRoot, name, {
195
+ }
196
+ /**
197
+ * Builds the wireSubscript option bag from the command flags, omitting
198
+ * every key whose flag is absent (exactOptionalPropertyTypes) and the
199
+ * defaults the lower layer already assumes.
200
+ */
201
+ function buildWireSubscriptOptions(options, domFilePath, domTargetPath, subscriptDir) {
202
+ return {
260
203
  ...(options.init !== undefined ? { init: options.init } : {}),
261
204
  ...(options.destroy !== undefined ? { destroy: options.destroy } : {}),
262
205
  ...(domFilePath !== undefined ? { domFilePath } : {}),
@@ -264,12 +207,25 @@ export async function wireCommand(projectRoot, name, options = {}) {
264
207
  ...(options.after !== undefined ? { after: options.after } : {}),
265
208
  ...(subscriptDir !== DEFAULT_BROWSER_SUBSCRIPT_DIR ? { subscriptDir } : {}),
266
209
  dryRun: false,
267
- });
210
+ };
211
+ }
212
+ /**
213
+ * Surfaces any legacy parser fallbacks the wiring run consumed, so the
214
+ * operator knows which files were mutated by the regex path rather than
215
+ * the AST path.
216
+ */
217
+ function reportParserFallbacks() {
268
218
  const parserFallbacks = consumeParserFallbackEvents();
269
219
  if (parserFallbacks.length > 0) {
270
220
  const contexts = [...new Set(parserFallbacks.map((event) => event.context))];
271
221
  info(`Legacy parser fallback was used for ${contexts.length} file${contexts.length === 1 ? '' : 's'}: ${contexts.join(', ')}`);
272
222
  }
223
+ }
224
+ /**
225
+ * Prints the per-mutation success/skip rows for a completed (non-dry-run)
226
+ * wire invocation.
227
+ */
228
+ function reportWireResult(result, name, options, domFilePath, domTargetPath) {
273
229
  if (result.subscriptAdded) {
274
230
  success(`Added loadSubScript for ${name}.js to browser-main.js`);
275
231
  }
@@ -306,6 +262,81 @@ export async function wireCommand(projectRoot, name, options = {}) {
306
262
  else {
307
263
  success(`Registered ${name}.js in ${result.jarMnResult.manifest}`);
308
264
  }
265
+ }
266
+ /**
267
+ * Wires a chrome subscript into the browser.
268
+ *
269
+ * @param projectRoot - Root directory of the project
270
+ * @param name - Subscript name (without .js extension)
271
+ * @param options - Command options
272
+ */
273
+ export async function wireCommand(projectRoot, name, options = {}) {
274
+ intro('Wire');
275
+ validateWireName(name);
276
+ if (options.after !== undefined) {
277
+ // --after references an existing init block by its subscript name, so
278
+ // it must follow the same naming rules as `name` itself. Without this
279
+ // check, a caller could sneak a path-traversal segment in through
280
+ // --after and have it forwarded unchanged to the lookup layer.
281
+ validateWireName(options.after);
282
+ }
283
+ // Validate init/destroy expressions BEFORE the dry-run/real fork so
284
+ // both paths enforce the same contract. Pre-0.16.0, validation only
285
+ // ran inside `addInitToBrowserInit`/`addDestroyToBrowserInit` (the
286
+ // real-execution path), so `--dry-run --init 'void 0'` succeeded and
287
+ // rendered a plausible-looking preview even though the real run would
288
+ // reject the same arguments. Dropping `void 0` into the template
289
+ // silently (or breaking out of the string literal) was already
290
+ // prevented downstream — this hoist just makes the failure surface
291
+ // identical in preview mode.
292
+ if (options.init !== undefined) {
293
+ validateWireExpression(options.init, 'init expression');
294
+ }
295
+ if (options.destroy !== undefined) {
296
+ validateWireExpression(options.destroy, 'destroy expression');
297
+ }
298
+ consumeParserFallbackEvents();
299
+ const subscriptDir = await resolveWireSubscriptDir(projectRoot, options);
300
+ const domFilePath = await resolveDomFragmentPath(projectRoot, options.dom);
301
+ // Resolve the chrome document the `#include` directive will land in.
302
+ // Only consulted when `--dom` is supplied — we still resolve it here so
303
+ // the dry-run plan can print the target accurately.
304
+ //
305
+ // `stripEnginePrefix` is applied so `--target engine/browser/base/browser.xhtml`
306
+ // and `--target browser/base/browser.xhtml` are treated identically,
307
+ // matching the `--dom` normalization above. Absolute `--target` paths
308
+ // stay absolute (the containment check downstream rejects them).
309
+ const normalizedTarget = options.target !== undefined && !isExplicitAbsolutePath(options.target)
310
+ ? stripEnginePrefix(options.target)
311
+ : options.target;
312
+ if (normalizedTarget !== undefined && !isContainedRelativePath(normalizedTarget)) {
313
+ throw new InvalidArgumentError(`Target chrome document must stay within engine/: ${options.target ?? ''}`, 'target');
314
+ }
315
+ const domTargetPath = await resolveDomTargetPath(projectRoot, normalizedTarget);
316
+ if (domFilePath) {
317
+ await assertDomTargetIsWireable(projectRoot, domFilePath, domTargetPath);
318
+ }
319
+ // Verify the subscript file exists in engine/ (skip for dry-run:
320
+ // dry-run is meant to preview the mutation plan without requiring
321
+ // the subscript to already exist, matching the "plan before write"
322
+ // pattern operators rely on for setup scripts).
323
+ //
324
+ // Dry-run keeps the existence check advisory rather than fatal: the
325
+ // "wire first, create file after" workflow is a legitimate use of
326
+ // preview, but operators who run dry-run over a typo were surprised
327
+ // when the real command then refused with `Subscript file not
328
+ // found`. 2026-04-23 eval (Finding in eval 2): dry-run produced a
329
+ // plausible plan and the non-dry-run invocation then errored. The
330
+ // info line surfaces the mismatch in preview mode so the operator
331
+ // can act on the warning before re-running without --dry-run.
332
+ await ensureSubscriptSourceExists(projectRoot, subscriptDir, name, options.dryRun ?? false);
333
+ if (options.dryRun) {
334
+ printWireDryRun(getProjectPaths(projectRoot).engine, name, subscriptDir, domFilePath, domTargetPath, options);
335
+ return;
336
+ }
337
+ const result = await wireSubscript(projectRoot, name, buildWireSubscriptOptions(options, domFilePath, domTargetPath, subscriptDir));
338
+ reportParserFallbacks();
339
+ reportWireResult(result, name, options, domFilePath, domTargetPath);
309
340
  outro('Wiring complete');
310
341
  }
311
342
  /** Registers the wire command on the CLI program. */
@@ -1,4 +1,4 @@
1
- import type { BuildBaseline } from './build-baseline.js';
1
+ import type { BuildBaseline } from './build-baseline-types.js';
2
2
  /** Result of a single artifact lookup. */
3
3
  export interface AuditEntry {
4
4
  /** Engine-relative source file path (POSIX separators). */
@@ -35,16 +35,13 @@
35
35
  */
36
36
  import { stat } from 'node:fs/promises';
37
37
  import { basename, join } from 'node:path';
38
- import { toError } from '../utils/errors.js';
39
38
  import { pathExists } from '../utils/fs.js';
40
39
  import { info, verbose, warn } from '../utils/logger.js';
41
40
  import { detectPlatformGate } from './build-audit-platform.js';
42
41
  import { collectSameBasenameCandidates, findRegisteredTarget, resolveArtifactByRegistration, } from './build-audit-registration.js';
43
42
  import { countTrailingSegmentMatches, isTestPath, resolveBestArtifact, } from './build-audit-resolve.js';
44
43
  import { resolveArtifactByKnownTransform } from './build-audit-transforms.js';
45
- import { hasChanges, isMissingHeadError } from './git.js';
46
- import { git } from './git-base.js';
47
- import { getUntrackedFiles } from './git-status.js';
44
+ import { collectChangedEnginePaths } from './engine-changes.js';
48
45
  /** Path extensions that are conventionally packaged into the Firefox bundle. */
49
46
  const PACKAGEABLE_EXTENSIONS = [
50
47
  '.js',
@@ -114,47 +111,6 @@ export function isPackageablePath(sourcePath) {
114
111
  }
115
112
  return false;
116
113
  }
117
- /**
118
- * Collects engine-relative paths changed since the baseline's HEAD SHA.
119
- * Always includes modified + untracked workdir paths. When the baseline is
120
- * missing or the engine has no HEAD yet, falls back to workdir-only diffs.
121
- */
122
- async function collectChangedFiles(engineDir, baseline) {
123
- const collected = new Set();
124
- if (baseline?.engineHeadSha) {
125
- try {
126
- const output = await git(['diff', '--name-only', `${baseline.engineHeadSha}..HEAD`], engineDir);
127
- for (const line of output.split('\n')) {
128
- const trimmed = line.trim();
129
- if (trimmed)
130
- collected.add(trimmed);
131
- }
132
- }
133
- catch (error) {
134
- if (!isMissingHeadError(error)) {
135
- verbose(`Audit: could not diff against baseline SHA — ${toError(error).message}`);
136
- }
137
- }
138
- }
139
- try {
140
- if (await hasChanges(engineDir)) {
141
- const worktreeDiff = await git(['diff', '--name-only', 'HEAD'], engineDir);
142
- for (const line of worktreeDiff.split('\n')) {
143
- const trimmed = line.trim();
144
- if (trimmed)
145
- collected.add(trimmed);
146
- }
147
- const untracked = await getUntrackedFiles(engineDir);
148
- for (const file of untracked) {
149
- collected.add(file);
150
- }
151
- }
152
- }
153
- catch (error) {
154
- verbose(`Audit: could not enumerate workdir changes — ${toError(error).message}`);
155
- }
156
- return [...collected].sort();
157
- }
158
114
  /*
159
115
  * Finds the unique obj-star directory with a dist subtree, or undefined
160
116
  * when zero or multiple match. The ambiguous case is already rejected
@@ -500,7 +456,7 @@ export async function auditBuildArtifacts(projectRoot, engineDir, baseline) {
500
456
  }
501
457
  const testsRoot = await resolveTestsRoot(engineDir);
502
458
  const testsPackaged = await hasPackagedTestsMarker(testsRoot);
503
- const changed = await collectChangedFiles(engineDir, baseline);
459
+ const changed = await collectChangedEnginePaths(engineDir, baseline, 'Audit');
504
460
  if (changed.length === 0) {
505
461
  return summary;
506
462
  }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Type of the last-build baseline marker, split out of `build-baseline.ts`
3
+ * so its consumers (`build-audit`, `build-prepare`, `test-stale-check`) can
4
+ * import the shape without importing the I/O module — `build-baseline.ts`
5
+ * itself depends on `build-audit.ts` at runtime, and a type import back
6
+ * into it would make the dependency graph cyclic.
7
+ */
8
+ /** Shape of the on-disk baseline marker. */
9
+ export interface BuildBaseline {
10
+ /** SHA of engine HEAD at the time the build succeeded. */
11
+ engineHeadSha: string;
12
+ /**
13
+ * ISO-8601 timestamp of when the baseline was recorded. Informational —
14
+ * downstream code keys off `engineHeadSha` for diffs, but the timestamp
15
+ * helps operators reason about stale markers.
16
+ */
17
+ builtAt: string;
18
+ /**
19
+ * The binaryName used at build time. Captured so the dist-tree audit
20
+ * can resolve the expected bundle root under obj-star/dist/ even when
21
+ * the project has since been renamed.
22
+ */
23
+ binaryName: string;
24
+ /**
25
+ * Content hash per packageable engine path that was dirty at build
26
+ * time (modified-against-HEAD or untracked). Used by
27
+ * `checkStaleBuildForTest` to distinguish "this file's content was
28
+ * already in `dist/` when the build completed" from "this file has
29
+ * been edited since". Missing on baselines written before 0.16.0; the
30
+ * stale-check falls back to the path-only comparison in that case,
31
+ * so older baselines retain their existing behavior.
32
+ *
33
+ * Keys are engine-relative POSIX paths. Values are hex-encoded
34
+ * SHA-256 digests of the file contents at the moment the baseline
35
+ * was recorded.
36
+ */
37
+ packageableFingerprints?: Record<string, string>;
38
+ }
@@ -0,0 +1,10 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Type of the last-build baseline marker, split out of `build-baseline.ts`
4
+ * so its consumers (`build-audit`, `build-prepare`, `test-stale-check`) can
5
+ * import the shape without importing the I/O module — `build-baseline.ts`
6
+ * itself depends on `build-audit.ts` at runtime, and a type import back
7
+ * into it would make the dependency graph cyclic.
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=build-baseline-types.js.map
@@ -15,37 +15,7 @@
15
15
  * on successful build completion; a failed build does not update it, so a
16
16
  * subsequent run still audits against the last known-good tree.
17
17
  */
18
- /** Shape of the on-disk baseline marker. */
19
- export interface BuildBaseline {
20
- /** SHA of engine HEAD at the time the build succeeded. */
21
- engineHeadSha: string;
22
- /**
23
- * ISO-8601 timestamp of when the baseline was recorded. Informational —
24
- * downstream code keys off `engineHeadSha` for diffs, but the timestamp
25
- * helps operators reason about stale markers.
26
- */
27
- builtAt: string;
28
- /**
29
- * The binaryName used at build time. Captured so the dist-tree audit
30
- * can resolve the expected bundle root under obj-star/dist/ even when
31
- * the project has since been renamed.
32
- */
33
- binaryName: string;
34
- /**
35
- * Content hash per packageable engine path that was dirty at build
36
- * time (modified-against-HEAD or untracked). Used by
37
- * `checkStaleBuildForTest` to distinguish "this file's content was
38
- * already in `dist/` when the build completed" from "this file has
39
- * been edited since". Missing on baselines written before 0.16.0; the
40
- * stale-check falls back to the path-only comparison in that case,
41
- * so older baselines retain their existing behavior.
42
- *
43
- * Keys are engine-relative POSIX paths. Values are hex-encoded
44
- * SHA-256 digests of the file contents at the moment the baseline
45
- * was recorded.
46
- */
47
- packageableFingerprints?: Record<string, string>;
48
- }
18
+ import type { BuildBaseline } from './build-baseline-types.js';
49
19
  /** Name of the last-build marker file under `.fireforge/`. */
50
20
  export declare const BUILD_BASELINE_FILENAME = "last-build.json";
51
21
  /**
@@ -3,7 +3,7 @@
3
3
  * story cleanup, branding setup, Furnace component application, and mozconfig generation.
4
4
  */
5
5
  import type { FireForgeConfig, ProjectPaths } from '../types/config.js';
6
- import type { BuildBaseline } from './build-baseline.js';
6
+ import type { BuildBaseline } from './build-baseline-types.js';
7
7
  /**
8
8
  * Result of the build preparation phase.
9
9
  */
@@ -9,13 +9,11 @@ import { toError } from '../utils/errors.js';
9
9
  import { pathExists } from '../utils/fs.js';
10
10
  import { info, spinner, verbose, warn } from '../utils/logger.js';
11
11
  import { isBrandingSetup, setupBranding } from './branding.js';
12
+ import { collectChangedEnginePaths } from './engine-changes.js';
12
13
  import { applyAllComponents } from './furnace-apply.js';
13
14
  import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, loadFurnaceState, } from './furnace-config.js';
14
15
  import { runFurnaceMutation } from './furnace-operation.js';
15
16
  import { cleanStories } from './furnace-stories.js';
16
- import { hasChanges, isMissingHeadError } from './git.js';
17
- import { git } from './git-base.js';
18
- import { getUntrackedFiles } from './git-status.js';
19
17
  import { generateMozconfig, runMach } from './mach.js';
20
18
  /** Path fragments of files whose edits invalidate the recursive-make backend. */
21
19
  const BACKEND_INVALIDATING_SUFFIXES = ['moz.build', 'moz.configure', 'Makefile.in'];
@@ -30,47 +28,6 @@ export function isBackendInvalidatingFile(path) {
30
28
  }
31
29
  return false;
32
30
  }
33
- /**
34
- * Collects engine-relative paths of files changed since the baseline's HEAD
35
- * SHA plus any workdir modifications. Defensive — git failures surface as
36
- * verbose lines and return the files collected so far. An empty result
37
- * means "no drift we can prove" rather than "no drift occurred".
38
- */
39
- async function collectBackendRelevantChanges(engineDir, baseline) {
40
- const collected = new Set();
41
- if (baseline.engineHeadSha) {
42
- try {
43
- const diff = await git(['diff', '--name-only', `${baseline.engineHeadSha}..HEAD`], engineDir);
44
- for (const line of diff.split('\n')) {
45
- const trimmed = line.trim();
46
- if (trimmed)
47
- collected.add(trimmed);
48
- }
49
- }
50
- catch (error) {
51
- if (!isMissingHeadError(error)) {
52
- verbose(`Auto-configure: could not diff engine against baseline — ${toError(error).message}`);
53
- }
54
- }
55
- }
56
- try {
57
- if (await hasChanges(engineDir)) {
58
- const worktreeDiff = await git(['diff', '--name-only', 'HEAD'], engineDir);
59
- for (const line of worktreeDiff.split('\n')) {
60
- const trimmed = line.trim();
61
- if (trimmed)
62
- collected.add(trimmed);
63
- }
64
- for (const file of await getUntrackedFiles(engineDir)) {
65
- collected.add(file);
66
- }
67
- }
68
- }
69
- catch (error) {
70
- verbose(`Auto-configure: could not enumerate workdir changes — ${toError(error).message}`);
71
- }
72
- return [...collected];
73
- }
74
31
  /**
75
32
  * Runs the shared pre-flight steps for build and package commands:
76
33
  * 1. Cleans Furnace stories from engine (prevents leaking into production)
@@ -101,7 +58,7 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
101
58
  // work against a stale recursive-make backend.
102
59
  let reconfigured = false;
103
60
  if (options.previousBaseline) {
104
- const changed = await collectBackendRelevantChanges(paths.engine, options.previousBaseline);
61
+ const changed = await collectChangedEnginePaths(paths.engine, options.previousBaseline, 'Auto-configure');
105
62
  const invalidating = changed.filter(isBackendInvalidatingFile);
106
63
  if (invalidating.length > 0) {
107
64
  info(`Backend config changed; running backend regeneration first (${invalidating.length} file${invalidating.length === 1 ? '' : 's'} touched).`);
@@ -8,14 +8,6 @@ export declare const CONFIG_FILENAME = "fireforge.json";
8
8
  export declare const FIREFORGE_DIR = ".fireforge";
9
9
  /** Name of the state file */
10
10
  export declare const STATE_FILENAME = "state.json";
11
- /** Name of the engine directory */
12
- export declare const ENGINE_DIR = "engine";
13
- /** Name of the patches directory */
14
- export declare const PATCHES_DIR = "patches";
15
- /** Name of the configs directory */
16
- export declare const CONFIGS_DIR = "configs";
17
- /** Name of the source directory */
18
- export declare const SRC_DIR = "src";
19
11
  /** Supported top-level fireforge.json keys backed by the current schema. */
20
12
  export declare const SUPPORTED_CONFIG_ROOT_KEYS: readonly ["name", "vendor", "appId", "binaryName", "firefox", "build", "license", "wire", "patchLint", "patchPolicy", "typecheck", "markerComment"];
21
13
  /** Supported config paths that can be read or set without --force. */
@@ -10,13 +10,13 @@ export const FIREFORGE_DIR = '.fireforge';
10
10
  /** Name of the state file */
11
11
  export const STATE_FILENAME = 'state.json';
12
12
  /** Name of the engine directory */
13
- export const ENGINE_DIR = 'engine';
13
+ const ENGINE_DIR = 'engine';
14
14
  /** Name of the patches directory */
15
- export const PATCHES_DIR = 'patches';
15
+ const PATCHES_DIR = 'patches';
16
16
  /** Name of the configs directory */
17
- export const CONFIGS_DIR = 'configs';
17
+ const CONFIGS_DIR = 'configs';
18
18
  /** Name of the source directory */
19
- export const SRC_DIR = 'src';
19
+ const SRC_DIR = 'src';
20
20
  /** Supported top-level fireforge.json keys backed by the current schema. */
21
21
  export const SUPPORTED_CONFIG_ROOT_KEYS = [
22
22
  'name',
@@ -2,12 +2,6 @@
2
2
  * Project state file management (.fireforge/state.json).
3
3
  */
4
4
  import type { FireForgeState } from '../types/config.js';
5
- /**
6
- * Validates a parsed project state object and returns a typed FireForgeState.
7
- * @param data - Parsed JSON state data
8
- * @returns Validated FireForgeState
9
- */
10
- export declare function validateFireForgeState(data: unknown): FireForgeState;
11
5
  /**
12
6
  * Loads the fireforge state, or returns defaults if it doesn't exist.
13
7
  * @param root - Root directory of the project
@@ -70,7 +70,7 @@ function sanitizeProjectState(data) {
70
70
  * @param data - Parsed JSON state data
71
71
  * @returns Validated FireForgeState
72
72
  */
73
- export function validateFireForgeState(data) {
73
+ function validateFireForgeState(data) {
74
74
  const result = sanitizeProjectState(data);
75
75
  if (result.issues.length > 0) {
76
76
  throw new ConfigError(`Invalid FireForge state: ${result.issues.join('; ')}`);
@@ -28,7 +28,13 @@ function parsePatchPolicyCategory(raw, label) {
28
28
  }
29
29
  return raw;
30
30
  }
31
- function parsePatchPolicyRange(raw, label) {
31
+ /**
32
+ * Shared head of every patch-policy range shape: the value must be an
33
+ * object carrying positive integer `from`/`to` endpoints with
34
+ * `to >= from`. Returns the parsed record so callers can read their
35
+ * shape-specific fields from it.
36
+ */
37
+ function parseRangeBounds(raw, label) {
32
38
  let rec;
33
39
  try {
34
40
  rec = parseObject(raw, label);
@@ -41,6 +47,10 @@ function parsePatchPolicyRange(raw, label) {
41
47
  if (to < from) {
42
48
  throw new ConfigError(`Config field "${label}.to" must be greater than or equal to from`);
43
49
  }
50
+ return { rec, from, to };
51
+ }
52
+ function parsePatchPolicyRange(raw, label) {
53
+ const { rec, from, to } = parseRangeBounds(raw, label);
44
54
  return {
45
55
  from,
46
56
  to,
@@ -90,18 +100,7 @@ function parseReservedAllowedPatch(raw, label) {
90
100
  return out;
91
101
  }
92
102
  function parsePatchPolicyReservedRange(raw, label) {
93
- let rec;
94
- try {
95
- rec = parseObject(raw, label);
96
- }
97
- catch {
98
- throw new ConfigError(`Config field "${label}" must be an object`);
99
- }
100
- const from = parsePositiveRangeEndpoint(rec.raw('from'), `${label}.from`);
101
- const to = parsePositiveRangeEndpoint(rec.raw('to'), `${label}.to`);
102
- if (to < from) {
103
- throw new ConfigError(`Config field "${label}.to" must be greater than or equal to from`);
104
- }
103
+ const { rec, from, to } = parseRangeBounds(raw, label);
105
104
  const allowedRaw = rec.raw('allowed');
106
105
  if (!Array.isArray(allowedRaw)) {
107
106
  throw new ConfigError(`Config field "${label}.allowed" must be an array`);