@hominis/fireforge 0.30.1 → 0.32.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 (152) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +22 -0
  3. package/dist/src/commands/export-all.js +9 -16
  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 +46 -1
  10. package/dist/src/commands/export.js +52 -113
  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 +3 -1
  20. package/dist/src/commands/lint-per-patch.js +265 -74
  21. package/dist/src/commands/lint.d.ts +1 -58
  22. package/dist/src/commands/lint.js +193 -88
  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-files.js +4 -1
  42. package/dist/src/commands/re-export-scan.js +8 -1
  43. package/dist/src/commands/re-export.js +8 -1
  44. package/dist/src/commands/rebase/summary.d.ts +1 -5
  45. package/dist/src/commands/rebase/summary.js +1 -1
  46. package/dist/src/commands/status-output.js +77 -68
  47. package/dist/src/commands/test-diagnose.d.ts +23 -0
  48. package/dist/src/commands/test-diagnose.js +210 -0
  49. package/dist/src/commands/test-run.d.ts +68 -0
  50. package/dist/src/commands/test-run.js +97 -0
  51. package/dist/src/commands/test.js +214 -263
  52. package/dist/src/commands/token.js +15 -1
  53. package/dist/src/commands/wire.js +109 -78
  54. package/dist/src/core/build-audit.d.ts +1 -1
  55. package/dist/src/core/build-audit.js +2 -46
  56. package/dist/src/core/build-baseline-types.d.ts +38 -0
  57. package/dist/src/core/build-baseline-types.js +10 -0
  58. package/dist/src/core/build-baseline.d.ts +1 -31
  59. package/dist/src/core/build-prepare.d.ts +1 -1
  60. package/dist/src/core/build-prepare.js +2 -45
  61. package/dist/src/core/config-paths.d.ts +0 -8
  62. package/dist/src/core/config-paths.js +4 -4
  63. package/dist/src/core/config-state.d.ts +0 -6
  64. package/dist/src/core/config-state.js +1 -1
  65. package/dist/src/core/config-validate-patch-policy.js +12 -13
  66. package/dist/src/core/config-validate.js +74 -28
  67. package/dist/src/core/engine-changes.d.ts +24 -0
  68. package/dist/src/core/engine-changes.js +64 -0
  69. package/dist/src/core/firefox-cache.d.ts +0 -5
  70. package/dist/src/core/firefox-cache.js +1 -1
  71. package/dist/src/core/firefox-download.d.ts +0 -6
  72. package/dist/src/core/firefox-download.js +1 -1
  73. package/dist/src/core/furnace-apply-helpers.d.ts +1 -8
  74. package/dist/src/core/furnace-apply-helpers.js +11 -20
  75. package/dist/src/core/furnace-apply.d.ts +1 -1
  76. package/dist/src/core/furnace-apply.js +1 -1
  77. package/dist/src/core/furnace-checksum-utils.d.ts +7 -0
  78. package/dist/src/core/furnace-checksum-utils.js +15 -0
  79. package/dist/src/core/furnace-config-validate.d.ts +31 -0
  80. package/dist/src/core/furnace-config-validate.js +133 -0
  81. package/dist/src/core/furnace-config.d.ts +4 -32
  82. package/dist/src/core/furnace-config.js +15 -111
  83. package/dist/src/core/furnace-constants.d.ts +0 -10
  84. package/dist/src/core/furnace-constants.js +2 -2
  85. package/dist/src/core/furnace-css-fragments.d.ts +79 -0
  86. package/dist/src/core/furnace-css-fragments.js +243 -0
  87. package/dist/src/core/furnace-jsconfig.d.ts +63 -0
  88. package/dist/src/core/furnace-jsconfig.js +191 -0
  89. package/dist/src/core/furnace-validate-helpers.d.ts +16 -14
  90. package/dist/src/core/furnace-validate-helpers.js +40 -1
  91. package/dist/src/core/furnace-validate-registration.js +16 -1
  92. package/dist/src/core/furnace-validate.js +54 -2
  93. package/dist/src/core/git-base.d.ts +15 -0
  94. package/dist/src/core/git-base.js +32 -0
  95. package/dist/src/core/git-diff.d.ts +8 -0
  96. package/dist/src/core/git-diff.js +224 -59
  97. package/dist/src/core/git-file-ops.d.ts +39 -12
  98. package/dist/src/core/git-file-ops.js +84 -3
  99. package/dist/src/core/lint-cache.d.ts +0 -13
  100. package/dist/src/core/lint-cache.js +5 -5
  101. package/dist/src/core/mach.d.ts +22 -1
  102. package/dist/src/core/mach.js +27 -2
  103. package/dist/src/core/manifest-register.d.ts +5 -16
  104. package/dist/src/core/manifest-register.js +3 -1
  105. package/dist/src/core/patch-lint-checkjs.d.ts +75 -21
  106. package/dist/src/core/patch-lint-checkjs.js +263 -71
  107. package/dist/src/core/patch-lint-css.d.ts +23 -0
  108. package/dist/src/core/patch-lint-css.js +172 -0
  109. package/dist/src/core/patch-lint-jsdoc.js +63 -4
  110. package/dist/src/core/patch-lint-observer.d.ts +37 -0
  111. package/dist/src/core/patch-lint-observer.js +168 -0
  112. package/dist/src/core/patch-lint.d.ts +34 -11
  113. package/dist/src/core/patch-lint.js +24 -161
  114. package/dist/src/core/patch-manifest-io.d.ts +16 -0
  115. package/dist/src/core/patch-manifest-io.js +44 -2
  116. package/dist/src/core/patch-manifest-validate.d.ts +1 -8
  117. package/dist/src/core/patch-manifest-validate.js +1 -1
  118. package/dist/src/core/patch-manifest.d.ts +1 -1
  119. package/dist/src/core/patch-manifest.js +1 -1
  120. package/dist/src/core/patch-policy.d.ts +0 -4
  121. package/dist/src/core/patch-policy.js +10 -4
  122. package/dist/src/core/register-browser-content.d.ts +1 -1
  123. package/dist/src/core/register-module.d.ts +1 -1
  124. package/dist/src/core/register-result.d.ts +21 -0
  125. package/dist/src/core/register-result.js +9 -0
  126. package/dist/src/core/register-shared-css.d.ts +1 -1
  127. package/dist/src/core/register-test-manifest.d.ts +1 -1
  128. package/dist/src/core/test-harness-crash.d.ts +61 -0
  129. package/dist/src/core/test-harness-crash.js +140 -0
  130. package/dist/src/core/test-stale-check.d.ts +1 -1
  131. package/dist/src/core/test-stale-check.js +2 -46
  132. package/dist/src/core/test-xpcshell-retry.d.ts +9 -2
  133. package/dist/src/core/test-xpcshell-retry.js +10 -3
  134. package/dist/src/core/token-dark-mode.js +14 -26
  135. package/dist/src/core/token-manager.d.ts +4 -0
  136. package/dist/src/core/token-manager.js +70 -16
  137. package/dist/src/core/typecheck-shim.d.ts +3 -22
  138. package/dist/src/core/typecheck-shim.js +69 -7
  139. package/dist/src/core/wire-utils.js +37 -44
  140. package/dist/src/types/commands/index.d.ts +1 -1
  141. package/dist/src/types/commands/options.d.ts +122 -0
  142. package/dist/src/types/config.d.ts +11 -2
  143. package/dist/src/types/furnace.d.ts +12 -1
  144. package/dist/src/utils/elapsed.d.ts +0 -2
  145. package/dist/src/utils/elapsed.js +1 -1
  146. package/dist/src/utils/fs.d.ts +0 -5
  147. package/dist/src/utils/fs.js +1 -1
  148. package/dist/src/utils/regex.d.ts +0 -6
  149. package/dist/src/utils/regex.js +3 -3
  150. package/dist/src/utils/validation.d.ts +0 -8
  151. package/dist/src/utils/validation.js +2 -2
  152. package/package.json +6 -4
@@ -1,17 +1,16 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { dirname, join } from 'node:path';
3
- import { toError } from '../utils/errors.js';
4
3
  import { pathExists, readText } from '../utils/fs.js';
5
- import { verbose } from '../utils/logger.js';
6
- import { hasRawCssColors, stripJsComments } from '../utils/regex.js';
7
- import { loadFurnaceConfig } from './furnace-config.js';
4
+ import { stripJsComments } from '../utils/regex.js';
8
5
  import { containsUpstreamLicenseText, getLicenseHeader, hasAnyLicenseHeader, hasAnyLicenseHeaderAnyStyle, } from './license-headers.js';
9
6
  import { invokePatchLintCheckJs } from './patch-lint-checkjs.js';
10
7
  import { lintChromeScriptJsDocForFile } from './patch-lint-chrome-jsdoc.js';
8
+ import { lintPatchedCss } from './patch-lint-css.js';
11
9
  import { detectNewFilesInDiff, extractAddedLinesPerFile } from './patch-lint-diff.js';
12
10
  import { AGGREGATE_PATCH_FILE } from './patch-lint-diff-tag.js';
13
11
  import { hasRelativeImport } from './patch-lint-imports.js';
14
12
  import { validateExportJsDoc } from './patch-lint-jsdoc.js';
13
+ import { lintObserverTopics } from './patch-lint-observer.js';
15
14
  import { resolvePatchOwnedChromeScripts, resolvePatchOwnedSysMjs } from './patch-lint-ownership.js';
16
15
  // ---------------------------------------------------------------------------
17
16
  // Cross-patch lint re-exports
@@ -24,6 +23,10 @@ import { resolvePatchOwnedChromeScripts, resolvePatchOwnedSysMjs } from './patch
24
23
  // public surface from `patch-lint-reexports.ts` so callers continue to
25
24
  // import from a single module.
26
25
  export * from './patch-lint-reexports.js';
26
+ // The CSS rule bodies live in `patch-lint-css.ts` (same per-file-budget
27
+ // split as the other rule families); re-export the imported binding so
28
+ // callers and tests keep importing `lintPatchedCss` from this module.
29
+ export { lintPatchedCss };
27
30
  // ---------------------------------------------------------------------------
28
31
  // Helpers
29
32
  // ---------------------------------------------------------------------------
@@ -205,139 +208,6 @@ export function commentStyleForFile(file) {
205
208
  return null;
206
209
  }
207
210
  // ---------------------------------------------------------------------------
208
- // CSS lint
209
- // ---------------------------------------------------------------------------
210
- /**
211
- * Lints patched CSS files for introduced raw color values and non-tokenized
212
- * custom properties.
213
- *
214
- * @param repoDir - Absolute path to the engine (repository) directory
215
- * @param affectedFiles - File paths (relative to repoDir) affected by the patch
216
- * @param diffContent - Optional unified diff used to scope raw color checks to introduced lines
217
- * @returns Array of lint issues found
218
- */
219
- export async function lintPatchedCss(repoDir, affectedFiles, diffContent, config) {
220
- const cssFiles = affectedFiles.filter((f) => f.endsWith('.css'));
221
- if (cssFiles.length === 0)
222
- return [];
223
- // Load furnace config gracefully — skip token-prefix check if unavailable
224
- let tokenPrefix;
225
- let tokenAllowlist;
226
- let runtimeVariables;
227
- try {
228
- const root = join(repoDir, '..');
229
- const furnaceConfig = await loadFurnaceConfig(root);
230
- if (furnaceConfig.tokenPrefix) {
231
- tokenPrefix = furnaceConfig.tokenPrefix;
232
- tokenAllowlist = new Set(furnaceConfig.tokenAllowlist ?? []);
233
- runtimeVariables = new Set(furnaceConfig.runtimeVariables ?? []);
234
- }
235
- }
236
- catch (error) {
237
- verbose(`Skipping furnace token-prefix lint hints because furnace.json could not be loaded: ${toError(error).message}`);
238
- }
239
- const issues = [];
240
- const addedLinesByFile = diffContent ? extractAddedLinesPerFile(diffContent) : undefined;
241
- for (const file of cssFiles) {
242
- const filePath = join(repoDir, file);
243
- if (!(await pathExists(filePath)))
244
- continue;
245
- const rawCss = await readText(filePath);
246
- // Strip block comments before scanning
247
- const cssContent = rawCss.replace(/\/\*[\s\S]*?\*\//g, '');
248
- // Check only introduced raw color values when diff context is available.
249
- // Skip files on the raw-color allowlist (exact path or basename match) and
250
- // auto-exempt files under `browser/branding/` — those are the fork's
251
- // visual identity assets (app-about dialogs, installer pages, branded
252
- // CSS copied from Firefox's `unofficial` template) and belong to the
253
- // design-decision layer the design-token system does not govern.
254
- // Without this auto-exemption, every first-time setup's copied CSS
255
- // failed `raw-color-value` with no actionable fix other than manually
256
- // listing each path in `rawColorAllowlist`.
257
- const allowlist = config?.patchLint?.rawColorAllowlist;
258
- const isAllowlisted = allowlist?.some((entry) => file === entry || file.endsWith('/' + entry));
259
- const isBranding = file.startsWith('browser/branding/');
260
- if (!isAllowlisted && !isBranding) {
261
- // Strip lines with inline fireforge-ignore: raw-color-value suppression.
262
- // Check against rawCss (before comment stripping) so the CSS comment marker is still present.
263
- const sourceForSuppression = addedLinesByFile
264
- ? (addedLinesByFile.get(file) ?? []).join('\n')
265
- : rawCss;
266
- const suppressedContent = sourceForSuppression
267
- .split('\n')
268
- .filter((line) => !line.includes('fireforge-ignore: raw-color-value'))
269
- .join('\n')
270
- .replace(/\/\*[\s\S]*?\*\//g, '');
271
- if (hasRawCssColors(suppressedContent)) {
272
- issues.push({
273
- file,
274
- check: 'raw-color-value',
275
- message: 'Raw color value found. Use CSS custom properties (var(--...)) for design token consistency.',
276
- severity: 'error',
277
- });
278
- }
279
- }
280
- // Check for non-tokenized custom properties. A variable that is both
281
- // declared and consumed inside the same file is auto-exempted as a
282
- // runtime state channel (see furnace.json → runtimeVariables).
283
- //
284
- // When diff context is available, scope the `var(...)` scan to
285
- // added/modified lines only. `cssContent` (full-file) is still the
286
- // source of `localDeclarations` so vars declared anywhere in the file
287
- // are recognised as same-file refs regardless of where the consuming
288
- // `var(...)` appears. Before this scoping change, a small edit to a
289
- // Furnace override of a stock component (e.g. moz-card) produced a
290
- // `token-prefix-violation` for every stock `var(--moz-card-*)` the
291
- // upstream file already carried, because the scanner saw the full
292
- // applied file and flagged each inherited reference as if the fork
293
- // had introduced it.
294
- if (tokenPrefix) {
295
- const declarationPattern = /(?:^|[{;,\s])(--[\w-]+)\s*:/g;
296
- const localDeclarations = new Set();
297
- let declMatch;
298
- while ((declMatch = declarationPattern.exec(cssContent)) !== null) {
299
- const name = declMatch[1];
300
- if (name)
301
- localDeclarations.add(name);
302
- }
303
- const prefixScanSource = addedLinesByFile
304
- ? (addedLinesByFile.get(file) ?? []).join('\n').replace(/\/\*[\s\S]*?\*\//g, '')
305
- : cssContent;
306
- if (prefixScanSource.length > 0) {
307
- const varPattern = /var\(\s*(--[\w-]+)/g;
308
- const flaggedProps = new Set();
309
- let match;
310
- while ((match = varPattern.exec(prefixScanSource)) !== null) {
311
- const prop = match[1];
312
- if (!prop)
313
- continue;
314
- if (prop.startsWith(tokenPrefix))
315
- continue;
316
- if (tokenAllowlist?.has(prop))
317
- continue;
318
- if (runtimeVariables?.has(prop))
319
- continue;
320
- if (localDeclarations.has(prop))
321
- continue;
322
- // De-duplicate per (file, prop) pair so the same introduced var
323
- // used five times in the added hunk doesn't produce five
324
- // identical issue entries.
325
- if (flaggedProps.has(prop))
326
- continue;
327
- flaggedProps.add(prop);
328
- issues.push({
329
- file,
330
- check: 'token-prefix-violation',
331
- message: `CSS references var(${prop}) which does not match the required token prefix "${tokenPrefix}". Use a design token, add to tokenAllowlist, or (for runtime state channels) list the variable in runtimeVariables.`,
332
- severity: 'error',
333
- });
334
- }
335
- }
336
- }
337
- }
338
- return issues;
339
- }
340
- // ---------------------------------------------------------------------------
341
211
  // License header lint
342
212
  // ---------------------------------------------------------------------------
343
213
  /**
@@ -503,23 +373,10 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, pa
503
373
  });
504
374
  }
505
375
  }
506
- // 4. Observer topic naming
507
- const topicPattern = /(?:addObserver|removeObserver|notifyObservers)\s*\([^)\n]*["']([^"']+)["']/g;
508
- let topicMatch;
509
- while ((topicMatch = topicPattern.exec(strippedContent)) !== null) {
510
- const topic = topicMatch[1];
511
- if (!topic)
512
- continue;
513
- // Only flag topics that contain the binaryName but don't follow convention
514
- if (topic.toLowerCase().includes(binaryName) && !/^[\w]+-[a-z]+-[a-z]+/.test(topic)) {
515
- issues.push({
516
- file,
517
- check: 'observer-topic-naming',
518
- message: `Observer topic "${topic}" should follow "${binaryName}-<noun>-<verb>" naming convention.`,
519
- severity: 'warning',
520
- });
521
- }
522
- }
376
+ // 4. Observer topic naming. Rule body lives in `patch-lint-observer.ts`:
377
+ // argument-position-aware, multi-line-safe, and allowlists Firefox-owned
378
+ // topics so simulated upstream notifications are not flagged.
379
+ issues.push(...lintObserverTopics(strippedContent, file, binaryName));
523
380
  }
524
381
  return issues;
525
382
  }
@@ -688,9 +545,6 @@ export async function lintModifiedFileHeaders(repoDir, affectedFiles, newFiles)
688
545
  }
689
546
  return issues;
690
547
  }
691
- // ---------------------------------------------------------------------------
692
- // Orchestrator
693
- // ---------------------------------------------------------------------------
694
548
  /**
695
549
  * Runs all patch lint checks and returns combined issues.
696
550
  *
@@ -710,9 +564,11 @@ export async function lintModifiedFileHeaders(repoDir, affectedFiles, newFiles)
710
564
  * per-patch manifest context (re-export, per-patch lint) should
711
565
  * pass this; aggregate-mode callers without a specific patch
712
566
  * context skip it and fall through to auto-detection.
567
+ * @param options - Optional behaviour switches; see
568
+ * {@link LintExportedPatchOptions}.
713
569
  * @returns Array of all lint issues found
714
570
  */
715
- export async function lintExportedPatch(repoDir, affectedFiles, diffContent, config, patchQueueCtx, ignoreChecks, patchTier) {
571
+ export async function lintExportedPatch(repoDir, affectedFiles, diffContent, config, patchQueueCtx, ignoreChecks, patchTier, options) {
716
572
  const newFiles = detectNewFilesInDiff(diffContent);
717
573
  const { textLines: lineCount } = countNonBinaryDiffLines(diffContent);
718
574
  const patchOwnedFiles = resolvePatchOwnedSysMjs(newFiles, patchQueueCtx);
@@ -724,7 +580,9 @@ export async function lintExportedPatch(repoDir, affectedFiles, diffContent, con
724
580
  lintModifiedFileHeaders(repoDir, affectedFiles, newFiles),
725
581
  ]);
726
582
  const modCommentIssues = lintModificationComments(diffContent, config);
727
- const sizeIssues = lintPatchSize(affectedFiles, lineCount, patchTier);
583
+ const sizeIssues = options?.skipPatchSize
584
+ ? []
585
+ : lintPatchSize(affectedFiles, lineCount, patchTier);
728
586
  const issues = [
729
587
  ...sizeIssues,
730
588
  ...cssIssues,
@@ -733,8 +591,13 @@ export async function lintExportedPatch(repoDir, affectedFiles, diffContent, con
733
591
  ...jsIssues,
734
592
  ...modCommentIssues,
735
593
  ];
736
- if (config.patchLint?.checkJs) {
737
- issues.push(...(await invokePatchLintCheckJs(repoDir, patchOwnedFiles, config.patchLint, dirname(repoDir))));
594
+ if (options?.precomputedCheckJs) {
595
+ // Per-patch lint built one queue-wide program and already attributed
596
+ // this patch's findings — append them instead of rebuilding the program.
597
+ issues.push(...options.precomputedCheckJs);
598
+ }
599
+ else if (config.patchLint?.checkJs) {
600
+ issues.push(...(await invokePatchLintCheckJs(repoDir, patchOwnedFiles, config.patchLint, dirname(repoDir), options?.checkJsReportScope)));
738
601
  }
739
602
  // Filter out ignored checks last so every rule still runs (keeps the
740
603
  // implementation uniform) but suppressed rules do not surface. We do not
@@ -85,6 +85,22 @@ export interface PatchRenameEntry {
85
85
  /** New numeric order — must match the prefix of `newFilename`. */
86
86
  newOrder: number;
87
87
  }
88
+ /**
89
+ * Rewrites `stagedDependencies.forwardImports[].owner` references on one
90
+ * patch through a rename lookup. Owners embed exact patch filenames, so any
91
+ * renumber (compact, reorder, placement export, rename) that does not remap
92
+ * them leaves dangling references that surface as false forward-import
93
+ * errors on the next lint.
94
+ *
95
+ * Pure and allocation-conservative: returns the input object unchanged when
96
+ * no owner matches the lookup, so callers can map whole manifests cheaply.
97
+ *
98
+ * @param patch - Manifest row to rewrite
99
+ * @param renameLookup - Maps an old patch filename to its new filename, or
100
+ * undefined when the filename is not being renamed
101
+ * @returns The same row, or a copy with remapped owners
102
+ */
103
+ export declare function rewriteStagedDependencyOwners(patch: PatchMetadata, renameLookup: (oldFilename: string) => string | undefined): PatchMetadata;
88
104
  /**
89
105
  * Renames patch files on disk and rewrites the corresponding manifest rows
90
106
  * atomically-ish: file renames use a two-phase staging strategy (rename each
@@ -167,6 +167,44 @@ export async function removePatchFromManifest(patchesDir, filename) {
167
167
  await savePatchesManifest(patchesDir, manifest);
168
168
  return true;
169
169
  }
170
+ /**
171
+ * Rewrites `stagedDependencies.forwardImports[].owner` references on one
172
+ * patch through a rename lookup. Owners embed exact patch filenames, so any
173
+ * renumber (compact, reorder, placement export, rename) that does not remap
174
+ * them leaves dangling references that surface as false forward-import
175
+ * errors on the next lint.
176
+ *
177
+ * Pure and allocation-conservative: returns the input object unchanged when
178
+ * no owner matches the lookup, so callers can map whole manifests cheaply.
179
+ *
180
+ * @param patch - Manifest row to rewrite
181
+ * @param renameLookup - Maps an old patch filename to its new filename, or
182
+ * undefined when the filename is not being renamed
183
+ * @returns The same row, or a copy with remapped owners
184
+ */
185
+ export function rewriteStagedDependencyOwners(patch, renameLookup) {
186
+ const forwardImports = patch.stagedDependencies?.forwardImports;
187
+ if (!forwardImports || forwardImports.length === 0)
188
+ return patch;
189
+ const rewritten = forwardImports.map((fi) => {
190
+ if (!fi.owner)
191
+ return fi;
192
+ const newOwner = renameLookup(fi.owner);
193
+ if (newOwner === undefined || newOwner === fi.owner)
194
+ return fi;
195
+ return { ...fi, owner: newOwner };
196
+ });
197
+ const changed = rewritten.some((fi, index) => fi !== forwardImports[index]);
198
+ if (!changed)
199
+ return patch;
200
+ return {
201
+ ...patch,
202
+ stagedDependencies: {
203
+ ...patch.stagedDependencies,
204
+ forwardImports: rewritten,
205
+ },
206
+ };
207
+ }
170
208
  /**
171
209
  * Renames patch files on disk and rewrites the corresponding manifest rows
172
210
  * atomically-ish: file renames use a two-phase staging strategy (rename each
@@ -300,12 +338,16 @@ export async function renumberPatchesInManifest(patchesDir, renameMap) {
300
338
  for (const [oldFilename, entry] of renameMap) {
301
339
  filenameUpdates.set(oldFilename, entry);
302
340
  }
341
+ // Owner references live on *other* patches than the renamed ones, so every
342
+ // row is passed through the staged-dependency rewrite, not just renamed rows.
343
+ const ownerLookup = (oldFilename) => filenameUpdates.get(oldFilename)?.newFilename;
303
344
  const updatedPatches = manifest.patches.map((p) => {
304
345
  const update = filenameUpdates.get(p.filename);
346
+ const withOwners = rewriteStagedDependencyOwners(p, ownerLookup);
305
347
  if (!update)
306
- return p;
348
+ return withOwners;
307
349
  return {
308
- ...p,
350
+ ...withOwners,
309
351
  filename: update.newFilename,
310
352
  order: update.newOrder,
311
353
  };
@@ -1,14 +1,7 @@
1
1
  /**
2
2
  * Schema validation for patches.json manifest data.
3
3
  */
4
- import type { PatchCategory, PatchesManifest, PatchMetadata } from '../types/commands/index.js';
5
- /**
6
- * Validates a single patch metadata entry from raw data.
7
- * @param data - Raw data to validate
8
- * @param index - Array index for error messages
9
- * @returns Validated PatchMetadata
10
- */
11
- export declare function validatePatchMetadata(data: unknown, index: number): PatchMetadata;
4
+ import type { PatchCategory, PatchesManifest } from '../types/commands/index.js';
12
5
  /** Validates raw patches.json data and returns the typed manifest shape. */
13
6
  export declare function validatePatchesManifest(data: unknown): PatchesManifest;
14
7
  /**
@@ -39,7 +39,7 @@ function parseStagedDependencies(data, label) {
39
39
  * @param index - Array index for error messages
40
40
  * @returns Validated PatchMetadata
41
41
  */
42
- export function validatePatchMetadata(data, index) {
42
+ function validatePatchMetadata(data, index) {
43
43
  const rec = parseObject(data, `patches[${index}]`);
44
44
  const filename = rec.string('filename');
45
45
  const name = rec.string('name');
@@ -5,7 +5,7 @@
5
5
  * is an implementation detail.
6
6
  */
7
7
  export { rebuildPatchesManifest, validatePatchesManifestConsistency, } from './patch-manifest-consistency.js';
8
- export { addPatchToManifest, loadPatchesManifest, mutatePatchRowsInManifest, PATCHES_MANIFEST, type PatchManifestRowMutation, type PatchManifestRowMutationResult, type PatchRenameEntry, removePatchFileAndManifest, renumberPatchesInManifest, savePatchesManifest, } from './patch-manifest-io.js';
8
+ export { addPatchToManifest, loadPatchesManifest, mutatePatchRowsInManifest, PATCHES_MANIFEST, type PatchManifestRowMutation, type PatchManifestRowMutationResult, type PatchRenameEntry, removePatchFileAndManifest, renumberPatchesInManifest, rewriteStagedDependencyOwners, savePatchesManifest, } from './patch-manifest-io.js';
9
9
  export { checkVersionCompatibility, findPatchesAffectingFile, getClaimedFiles, stampPatchVersions, validatePatchIntegrity, } from './patch-manifest-query.js';
10
10
  export { resolvePatchIdentifier } from './patch-manifest-resolve.js';
11
11
  export { validatePatchesManifest } from './patch-manifest-validate.js';
@@ -6,7 +6,7 @@
6
6
  * is an implementation detail.
7
7
  */
8
8
  export { rebuildPatchesManifest, validatePatchesManifestConsistency, } from './patch-manifest-consistency.js';
9
- export { addPatchToManifest, loadPatchesManifest, mutatePatchRowsInManifest, PATCHES_MANIFEST, removePatchFileAndManifest, renumberPatchesInManifest, savePatchesManifest, } from './patch-manifest-io.js';
9
+ export { addPatchToManifest, loadPatchesManifest, mutatePatchRowsInManifest, PATCHES_MANIFEST, removePatchFileAndManifest, renumberPatchesInManifest, rewriteStagedDependencyOwners, savePatchesManifest, } from './patch-manifest-io.js';
10
10
  export { checkVersionCompatibility, findPatchesAffectingFile, getClaimedFiles, stampPatchVersions, validatePatchIntegrity, } from './patch-manifest-query.js';
11
11
  export { resolvePatchIdentifier } from './patch-manifest-resolve.js';
12
12
  export { validatePatchesManifest } from './patch-manifest-validate.js';
@@ -8,8 +8,6 @@
8
8
  */
9
9
  import type { PatchesManifest, PatchMetadata } from '../types/commands/index.js';
10
10
  import type { FireForgeConfig } from '../types/config.js';
11
- /** Default patch filename contract used when a policy omits `filenamePattern`. */
12
- export declare const DEFAULT_PATCH_POLICY_FILENAME_PATTERN = "^(?<order>\\d{3})-(?<category>[a-z][a-z0-9-]*)-(?<slug>[a-z0-9-]+)\\.patch$";
13
11
  /** Stable issue codes returned by patch policy evaluation. */
14
12
  export type PatchPolicyIssueCode = 'filename-pattern' | 'filename-captures' | 'filename-metadata-mismatch' | 'order-collision' | 'category-range' | 'reserved-range' | 'reserved-documentation' | 'reserved-files' | 'description-required' | 'numeric-gap';
15
13
  /** A single patch policy validation finding. */
@@ -26,8 +24,6 @@ export interface PatchPolicyEnforcementInput {
26
24
  command: string;
27
25
  forceUnsafe?: boolean;
28
26
  }
29
- /** Returns true when the loaded config includes an opt-in patch policy. */
30
- export declare function hasPatchPolicy(config: FireForgeConfig): boolean;
31
27
  /** Returns valid categories for prompts and CLI validation under the config. */
32
28
  export declare function getPatchPolicyCategories(config: FireForgeConfig): string[];
33
29
  /** Checks whether a category is accepted by legacy defaults or the policy ranges. */
@@ -10,8 +10,9 @@
10
10
  import { InvalidArgumentError } from '../errors/base.js';
11
11
  import { warn } from '../utils/logger.js';
12
12
  import { PATCH_CATEGORIES } from '../utils/validation.js';
13
+ import { rewriteStagedDependencyOwners } from './patch-manifest-io.js';
13
14
  /** Default patch filename contract used when a policy omits `filenamePattern`. */
14
- export const DEFAULT_PATCH_POLICY_FILENAME_PATTERN = '^(?<order>\\d{3})-(?<category>[a-z][a-z0-9-]*)-(?<slug>[a-z0-9-]+)\\.patch$';
15
+ const DEFAULT_PATCH_POLICY_FILENAME_PATTERN = '^(?<order>\\d{3})-(?<category>[a-z][a-z0-9-]*)-(?<slug>[a-z0-9-]+)\\.patch$';
15
16
  function policy(config) {
16
17
  return config.patchPolicy;
17
18
  }
@@ -22,7 +23,7 @@ function issueSeverity(config) {
22
23
  return mutationMode(config) === 'warn' ? 'warning' : 'error';
23
24
  }
24
25
  /** Returns true when the loaded config includes an opt-in patch policy. */
25
- export function hasPatchPolicy(config) {
26
+ function hasPatchPolicy(config) {
26
27
  return policy(config) !== undefined;
27
28
  }
28
29
  /** Returns valid categories for prompts and CLI validation under the config. */
@@ -305,12 +306,17 @@ export function buildProjectedManifest(current, patches) {
305
306
  }
306
307
  /** Applies a filename/order rename projection to a manifest without mutating it. */
307
308
  export function applyRenameMapToManifest(manifest, renameMap) {
309
+ const ownerLookup = (oldFilename) => renameMap.get(oldFilename)?.newFilename;
308
310
  return buildProjectedManifest(manifest, manifest.patches.map((patch) => {
311
+ // Staged-dependency owners reference other patches' filenames, so the
312
+ // projection rewrites them on every row to mirror what
313
+ // renumberPatchesInManifest persists.
314
+ const withOwners = rewriteStagedDependencyOwners(patch, ownerLookup);
309
315
  const rename = renameMap.get(patch.filename);
310
316
  if (!rename)
311
- return patch;
317
+ return withOwners;
312
318
  return {
313
- ...patch,
319
+ ...withOwners,
314
320
  filename: rename.newFilename,
315
321
  order: rename.newOrder,
316
322
  };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * JS/content registration in browser/base/jar.mn.
3
3
  */
4
- import type { RegisterResult } from './manifest-register.js';
4
+ import type { RegisterResult } from './register-result.js';
5
5
  /**
6
6
  * Registers a JS/content file in browser/base/jar.mn.
7
7
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Module registration in browser/modules/{binaryName}/moz.build.
3
3
  */
4
- import type { RegisterResult } from './manifest-register.js';
4
+ import type { RegisterResult } from './register-result.js';
5
5
  /**
6
6
  * Registers a module in browser/modules/{binaryName}/moz.build.
7
7
  *
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Shared result shape for manifest registration operations, split out of
3
+ * the `manifest-register.ts` barrel so the `register-*` leaf modules can
4
+ * import it without importing the barrel that re-exports them — that
5
+ * type-only back-edge made the registration dependency graph cyclic.
6
+ */
7
+ /**
8
+ * Result of a manifest registration operation.
9
+ */
10
+ export interface RegisterResult {
11
+ /** The manifest file that was modified */
12
+ manifest: string;
13
+ /** The entry that was inserted */
14
+ entry: string;
15
+ /** The entry after which the new entry was inserted (for user display) */
16
+ previousEntry?: string | undefined;
17
+ /** Whether the entry already existed (skipped) */
18
+ skipped: boolean;
19
+ /** Whether --after target was not found and fell back to alphabetical */
20
+ afterFallback?: boolean | undefined;
21
+ }
@@ -0,0 +1,9 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Shared result shape for manifest registration operations, split out of
4
+ * the `manifest-register.ts` barrel so the `register-*` leaf modules can
5
+ * import it without importing the barrel that re-exports them — that
6
+ * type-only back-edge made the registration dependency graph cyclic.
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=register-result.js.map
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * CSS registration in browser/themes/shared/jar.inc.mn.
3
3
  */
4
- import type { RegisterResult } from './manifest-register.js';
4
+ import type { RegisterResult } from './register-result.js';
5
5
  /**
6
6
  * Measures the column at which the `(source)` parenthesis opens in
7
7
  * adjacent `skin/classic/browser/<x>.css (...)` entries inside an
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Test manifest registration in browser/base/moz.build.
3
3
  */
4
- import type { RegisterResult } from './manifest-register.js';
4
+ import type { RegisterResult } from './register-result.js';
5
5
  /**
6
6
  * Registers a test manifest (browser.toml) in browser/base/moz.build.
7
7
  *
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Harness-crash classification for `fireforge test` (field reports C1/C2).
3
+ *
4
+ * The wrapped mach harness exhibits flaky non-test failures that an exit
5
+ * code (or a "did it print a summary" grep) cannot distinguish from real
6
+ * test results:
7
+ *
8
+ * - startup crashes from the mozlog resource monitor on macOS
9
+ * (`AttributeError: 'SystemResourceMonitor' object has no attribute
10
+ * 'poll_interval'`, `host_statistics64(HOST_VM_INFO64) syscall failed`,
11
+ * `(ipc/mig) array not large enough` — a psutil/macOS mismatch) which
12
+ * abort the run before any test executes;
13
+ * - hangs after browser startup that die at the no-output timeout yet
14
+ * still emit a `Passed: 0` summary;
15
+ * - post-green shutdown re-entry, where a fully green run stalls on
16
+ * "must wait for focus" and records "Application shut down (without
17
+ * crashing) in the middle of a test!" as the only unexpected failure.
18
+ *
19
+ * Classification therefore keys on `TEST-START` presence — summary lines
20
+ * never count as proof that tests ran — and recognizes the crash shapes
21
+ * above so the command layer can retry them with a bounded budget instead
22
+ * of reporting phantom test failures (or phantom passes).
23
+ */
24
+ /** How a completed harness run should be interpreted. */
25
+ export type HarnessRunClassification = 'tests-ran-ok' | 'test-failures' | 'harness-crash' | 'no-tests';
26
+ /** A recognized harness-crash shape with its evidence line. */
27
+ export interface HarnessCrashSignature {
28
+ reason: string;
29
+ line: string;
30
+ }
31
+ /** Result of {@link classifyHarnessRun}. */
32
+ export interface HarnessRunVerdict {
33
+ kind: HarnessRunClassification;
34
+ signature?: HarnessCrashSignature;
35
+ }
36
+ /**
37
+ * Detects the known harness-crash shapes in captured mach output.
38
+ * Returns undefined for anything that looks like a genuine test result.
39
+ */
40
+ export declare function detectHarnessCrashSignature(output: string): HarnessCrashSignature | undefined;
41
+ /**
42
+ * Classifies a completed harness run. The decision tree, in order:
43
+ *
44
+ * 1. A recognized crash signature wins regardless of exit code (the
45
+ * shutdown re-entry shape exits non-zero on an otherwise green run;
46
+ * the hang shape can even exit zero with a `Passed: 0` summary).
47
+ * 2. No `TEST-START` with explicit paths requested means no test ran —
48
+ * `no-tests`, even when the exit code is zero. Summary lines are not
49
+ * trusted as evidence of execution.
50
+ * 3. Exit code zero with tests started is a pass; anything else is a
51
+ * test failure for the regular diagnosis chain.
52
+ */
53
+ export declare function classifyHarnessRun(exitCode: number, output: string, requestedPaths: readonly string[]): HarnessRunVerdict;
54
+ /** Builds the operator-facing failure message after retries are exhausted. */
55
+ export declare function buildHarnessCrashMessage(signature: HarnessCrashSignature, attempts: number): string;
56
+ /**
57
+ * Builds the message for a run that produced no `TEST-START` despite
58
+ * requesting paths — including exit-code-zero runs whose `Passed: 0`
59
+ * summary would otherwise read as a silent false green.
60
+ */
61
+ export declare function buildNoTestsRanMessage(exitCode: number, requestedPaths: readonly string[]): string;