@hominis/fireforge 0.27.1 → 0.27.3

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 (47) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +5 -5
  3. package/dist/src/cli.js +5 -1
  4. package/dist/src/commands/build.js +61 -1
  5. package/dist/src/commands/doctor-working-tree.js +5 -1
  6. package/dist/src/commands/download.js +178 -112
  7. package/dist/src/commands/export-all.js +3 -2
  8. package/dist/src/commands/export-flow.d.ts +2 -0
  9. package/dist/src/commands/export-flow.js +2 -0
  10. package/dist/src/commands/export.js +5 -4
  11. package/dist/src/commands/import.js +2 -1
  12. package/dist/src/commands/re-export.js +6 -6
  13. package/dist/src/commands/rebase/continue.js +2 -0
  14. package/dist/src/commands/rebase/index.d.ts +2 -2
  15. package/dist/src/commands/rebase/index.js +9 -4
  16. package/dist/src/commands/rebase/patch-loop.js +5 -5
  17. package/dist/src/commands/rebase/summary.js +7 -2
  18. package/dist/src/commands/resolve.js +2 -1
  19. package/dist/src/commands/status-output.d.ts +13 -0
  20. package/dist/src/commands/status-output.js +186 -0
  21. package/dist/src/commands/status.js +4 -247
  22. package/dist/src/commands/verify.js +32 -16
  23. package/dist/src/core/build-prepare.js +12 -4
  24. package/dist/src/core/firefox-archive.js +7 -3
  25. package/dist/src/core/firefox-cache.d.ts +1 -1
  26. package/dist/src/core/firefox-cache.js +12 -5
  27. package/dist/src/core/firefox.js +1 -1
  28. package/dist/src/core/git.js +7 -2
  29. package/dist/src/core/ownership-table.d.ts +3 -1
  30. package/dist/src/core/ownership-table.js +31 -7
  31. package/dist/src/core/patch-export.d.ts +4 -0
  32. package/dist/src/core/patch-export.js +4 -0
  33. package/dist/src/core/patch-manifest-consistency.d.ts +1 -1
  34. package/dist/src/core/patch-manifest-consistency.js +4 -2
  35. package/dist/src/core/patch-manifest-query.d.ts +4 -3
  36. package/dist/src/core/patch-manifest-query.js +12 -4
  37. package/dist/src/core/patch-manifest-validate.js +22 -4
  38. package/dist/src/core/patch-source-metadata.d.ts +8 -0
  39. package/dist/src/core/patch-source-metadata.js +17 -0
  40. package/dist/src/core/rebase-session.d.ts +8 -3
  41. package/dist/src/core/rebase-session.js +1 -1
  42. package/dist/src/core/status-classify.d.ts +4 -1
  43. package/dist/src/core/status-classify.js +4 -5
  44. package/dist/src/errors/download.d.ts +11 -0
  45. package/dist/src/errors/download.js +33 -1
  46. package/dist/src/types/commands/patches.d.ts +9 -1
  47. package/package.json +1 -1
@@ -28,7 +28,7 @@ import { info } from '../utils/logger.js';
28
28
  * {@link import('../core/patch-lint.js').collectNewFileCreatorsByPath};
29
29
  * paths with a `.length > 1` owner list become duplicate-create conflicts
30
30
  */
31
- export function buildOwnershipTable(manifestPatches, worktreeFiles, newFileCreatorsByPath) {
31
+ export function buildOwnershipTable(manifestPatches, worktreeFiles, newFileCreatorsByPath, classifications = new Map()) {
32
32
  const ownersByPath = new Map();
33
33
  for (const patch of manifestPatches) {
34
34
  for (const file of patch.filesAffected) {
@@ -86,6 +86,13 @@ export function buildOwnershipTable(manifestPatches, worktreeFiles, newFileCreat
86
86
  conflict: isFilesAffectedConflict || isDuplicateCreateConflict,
87
87
  conflictReason,
88
88
  unmanaged: false,
89
+ state: isFilesAffectedConflict || isDuplicateCreateConflict
90
+ ? 'conflict'
91
+ : classifications.get(path) === 'patch-backed'
92
+ ? 'patch-backed'
93
+ : classifications.get(path) === 'patch-owned-drift'
94
+ ? 'patch-owned-drift'
95
+ : 'owned',
89
96
  });
90
97
  }
91
98
  for (const path of unmanagedOnly) {
@@ -95,6 +102,7 @@ export function buildOwnershipTable(manifestPatches, worktreeFiles, newFileCreat
95
102
  conflict: false,
96
103
  conflictReason: null,
97
104
  unmanaged: true,
105
+ state: 'unmanaged',
98
106
  });
99
107
  }
100
108
  rows.sort((a, b) => a.path.localeCompare(b.path));
@@ -115,6 +123,22 @@ function renderConflictCell(row) {
115
123
  return 'CONFLICT (dup-create)';
116
124
  return 'CONFLICT';
117
125
  }
126
+ function renderStateCell(row) {
127
+ if (row.conflict)
128
+ return renderConflictCell(row);
129
+ switch (row.state) {
130
+ case 'owned':
131
+ return 'owned';
132
+ case 'patch-backed':
133
+ return 'patch-backed';
134
+ case 'patch-owned-drift':
135
+ return 'patch-owned drift';
136
+ case 'unmanaged':
137
+ return 'unmanaged';
138
+ case 'conflict':
139
+ return renderConflictCell(row);
140
+ }
141
+ }
118
142
  /**
119
143
  * Renders the ownership table as a GitHub-flavored Markdown pipe table.
120
144
  * Using markdown-table's own serializer would require a seed document to
@@ -128,17 +152,17 @@ export function renderOwnershipTable(rows) {
128
152
  }
129
153
  const pathHeader = 'path';
130
154
  const ownerHeader = 'owning patch';
131
- const conflictHeader = 'conflict';
155
+ const stateHeader = 'state';
132
156
  const pathWidth = Math.max(pathHeader.length, ...rows.map((r) => r.path.length));
133
157
  const ownerWidth = Math.max(ownerHeader.length, ...rows.map((r) => (r.unmanaged ? 1 : r.owners.join(', ').length)));
134
- const conflictWidth = Math.max(conflictHeader.length, ...rows.map((r) => renderConflictCell(r).length), 8);
158
+ const stateWidth = Math.max(stateHeader.length, ...rows.map((r) => renderStateCell(r).length), 8);
135
159
  const pad = (text, width) => text + ' '.repeat(width - text.length);
136
- info(`| ${pad(pathHeader, pathWidth)} | ${pad(ownerHeader, ownerWidth)} | ${pad(conflictHeader, conflictWidth)} |`);
137
- info(`| ${'-'.repeat(pathWidth)} | ${'-'.repeat(ownerWidth)} | ${'-'.repeat(conflictWidth)} |`);
160
+ info(`| ${pad(pathHeader, pathWidth)} | ${pad(ownerHeader, ownerWidth)} | ${pad(stateHeader, stateWidth)} |`);
161
+ info(`| ${'-'.repeat(pathWidth)} | ${'-'.repeat(ownerWidth)} | ${'-'.repeat(stateWidth)} |`);
138
162
  for (const row of rows) {
139
163
  const ownerCell = row.unmanaged ? '-' : row.owners.join(', ');
140
- const conflictCell = renderConflictCell(row);
141
- info(`| ${pad(row.path, pathWidth)} | ${pad(ownerCell, ownerWidth)} | ${pad(conflictCell, conflictWidth)} |`);
164
+ const stateCell = renderStateCell(row);
165
+ info(`| ${pad(row.path, pathWidth)} | ${pad(ownerCell, ownerWidth)} | ${pad(stateCell, stateWidth)} |`);
142
166
  }
143
167
  }
144
168
  //# sourceMappingURL=ownership-table.js.map
@@ -36,6 +36,8 @@ export interface CommitExportedPatchInput {
36
36
  diff: string;
37
37
  filesAffected: string[];
38
38
  sourceEsrVersion: string;
39
+ sourceProduct?: FireForgeConfig['firefox']['product'];
40
+ sourceVersion?: string;
39
41
  /** Optional `PatchMetadata.tier` opt-in (only `"branding"` recognised). */
40
42
  tier?: 'branding';
41
43
  /** Optional `PatchMetadata.lintIgnore` (empty array treated as absent). */
@@ -126,6 +128,8 @@ export interface PlanExportInput {
126
128
  description: string;
127
129
  filesAffected: string[];
128
130
  sourceEsrVersion: string;
131
+ sourceProduct?: FireForgeConfig['firefox']['product'];
132
+ sourceVersion?: string;
129
133
  /**
130
134
  * Optional `PatchMetadata.tier` opt-in carried from the CLI flag.
131
135
  * Only `"branding"` is currently recognised. When provided the field
@@ -75,6 +75,8 @@ export async function commitExportedPatch(input) {
75
75
  description: input.description,
76
76
  filesAffected: input.filesAffected,
77
77
  sourceEsrVersion: input.sourceEsrVersion,
78
+ ...(input.sourceProduct !== undefined ? { sourceProduct: input.sourceProduct } : {}),
79
+ ...(input.sourceVersion !== undefined ? { sourceVersion: input.sourceVersion } : {}),
78
80
  ...(input.tier !== undefined ? { tier: input.tier } : {}),
79
81
  ...(input.lintIgnore !== undefined ? { lintIgnore: input.lintIgnore } : {}),
80
82
  ...(input.config !== undefined ? { config: input.config } : {}),
@@ -262,6 +264,8 @@ async function computeExportPlanUnderLock(input) {
262
264
  description: input.description,
263
265
  createdAt: new Date().toISOString(),
264
266
  sourceEsrVersion: input.sourceEsrVersion,
267
+ ...(input.sourceProduct !== undefined ? { sourceProduct: input.sourceProduct } : {}),
268
+ sourceVersion: input.sourceVersion ?? input.sourceEsrVersion,
265
269
  filesAffected: input.filesAffected,
266
270
  ...(input.tier !== undefined ? { tier: input.tier } : {}),
267
271
  ...(input.lintIgnore !== undefined && input.lintIgnore.length > 0
@@ -37,7 +37,7 @@ export interface RebuildPatchesManifestResult {
37
37
  * Existing metadata is preserved when possible; missing entries are recovered
38
38
  * from filename structure, patch contents, and file mtimes.
39
39
  * @param patchesDir - Path to the patches directory
40
- * @param fallbackSourceEsrVersion - ESR version to use for recovered entries
40
+ * @param fallbackSourceEsrVersion - source version to use for recovered legacy entries
41
41
  * @returns {@link RebuildPatchesManifestResult} — the persisted manifest
42
42
  * plus the filenames that were reconstructed from generic defaults.
43
43
  */
@@ -84,7 +84,7 @@ export async function validatePatchesManifestConsistency(patchesDir) {
84
84
  * Existing metadata is preserved when possible; missing entries are recovered
85
85
  * from filename structure, patch contents, and file mtimes.
86
86
  * @param patchesDir - Path to the patches directory
87
- * @param fallbackSourceEsrVersion - ESR version to use for recovered entries
87
+ * @param fallbackSourceEsrVersion - source version to use for recovered legacy entries
88
88
  * @returns {@link RebuildPatchesManifestResult} — the persisted manifest
89
89
  * plus the filenames that were reconstructed from generic defaults.
90
90
  */
@@ -135,9 +135,11 @@ export async function rebuildPatchesManifest(patchesDir, fallbackSourceEsrVersio
135
135
  category: existing?.category ?? inferred.category,
136
136
  name: existing?.name ?? inferred.name,
137
137
  description: existing?.description ??
138
- `Recovered manifest entry for ${patch.filename}. Review description and ESR version.`,
138
+ `Recovered manifest entry for ${patch.filename}. Review description and source version.`,
139
139
  createdAt: existing?.createdAt ?? new Date(patchStats.mtimeMs).toISOString(),
140
140
  sourceEsrVersion: existing?.sourceEsrVersion ?? fallbackSourceEsrVersion,
141
+ sourceVersion: existing?.sourceVersion ?? existing?.sourceEsrVersion ?? fallbackSourceEsrVersion,
142
+ ...(existing?.sourceProduct !== undefined ? { sourceProduct: existing.sourceProduct } : {}),
141
143
  filesAffected,
142
144
  };
143
145
  if (existing?.lintIgnore !== undefined)
@@ -2,6 +2,7 @@
2
2
  * Query helpers: finding patches by file, integrity checks, version compat, stamping.
3
3
  */
4
4
  import type { PatchesManifest, PatchInfo, PatchMetadata } from '../types/commands/index.js';
5
+ import type { FirefoxProduct } from '../types/config.js';
5
6
  /**
6
7
  * Gets all file paths claimed by patches other than the excluded one.
7
8
  * @param manifest - The patches manifest
@@ -10,7 +11,7 @@ import type { PatchesManifest, PatchInfo, PatchMetadata } from '../types/command
10
11
  */
11
12
  export declare function getClaimedFiles(manifest: PatchesManifest, excludeFilename: string): Set<string>;
12
13
  /**
13
- * Checks ESR version compatibility.
14
+ * Checks Firefox source version compatibility.
14
15
  * @param patchVersion - Version the patch was created for
15
16
  * @param currentVersion - Current project version
16
17
  * @returns Warning message if versions differ, null if compatible
@@ -39,10 +40,10 @@ export declare function validatePatchIntegrity(patchesDir: string, engineDir: st
39
40
  targetFile: string | null;
40
41
  }>>;
41
42
  /**
42
- * Stamps multiple patches with a new `sourceEsrVersion` in a single
43
+ * Stamps multiple patches with a new source version in a single
43
44
  * manifest read-modify-write cycle.
44
45
  * @param patchesDir - Path to the patches directory
45
46
  * @param filenames - Patch filenames to update
46
47
  * @param newVersion - Version string to set (e.g. "140.9.0esr")
47
48
  */
48
- export declare function stampPatchVersions(patchesDir: string, filenames: string[], newVersion: string): Promise<void>;
49
+ export declare function stampPatchVersions(patchesDir: string, filenames: string[], newVersion: string, newProduct?: FirefoxProduct): Promise<void>;
@@ -25,7 +25,7 @@ export function getClaimedFiles(manifest, excludeFilename) {
25
25
  return claimed;
26
26
  }
27
27
  /**
28
- * Checks ESR version compatibility.
28
+ * Checks Firefox source version compatibility.
29
29
  * @param patchVersion - Version the patch was created for
30
30
  * @param currentVersion - Current project version
31
31
  * @returns Warning message if versions differ, null if compatible
@@ -99,21 +99,29 @@ export async function validatePatchIntegrity(patchesDir, engineDir) {
99
99
  return issues;
100
100
  }
101
101
  /**
102
- * Stamps multiple patches with a new `sourceEsrVersion` in a single
102
+ * Stamps multiple patches with a new source version in a single
103
103
  * manifest read-modify-write cycle.
104
104
  * @param patchesDir - Path to the patches directory
105
105
  * @param filenames - Patch filenames to update
106
106
  * @param newVersion - Version string to set (e.g. "140.9.0esr")
107
107
  */
108
- export async function stampPatchVersions(patchesDir, filenames, newVersion) {
108
+ export async function stampPatchVersions(patchesDir, filenames, newVersion, newProduct) {
109
109
  const manifest = await loadPatchesManifest(patchesDir);
110
110
  if (!manifest)
111
111
  return;
112
112
  const filenameSet = new Set(filenames);
113
113
  let modified = false;
114
114
  for (const patch of manifest.patches) {
115
- if (filenameSet.has(patch.filename) && patch.sourceEsrVersion !== newVersion) {
115
+ if (!filenameSet.has(patch.filename))
116
+ continue;
117
+ if (patch.sourceEsrVersion !== newVersion ||
118
+ patch.sourceVersion !== newVersion ||
119
+ (newProduct !== undefined && patch.sourceProduct !== newProduct)) {
116
120
  patch.sourceEsrVersion = newVersion;
121
+ patch.sourceVersion = newVersion;
122
+ if (newProduct !== undefined) {
123
+ patch.sourceProduct = newProduct;
124
+ }
117
125
  modified = true;
118
126
  }
119
127
  }
@@ -3,7 +3,7 @@
3
3
  * Schema validation for patches.json manifest data.
4
4
  */
5
5
  import { parseObject } from '../utils/parse.js';
6
- import { isArray, isObject, isValidFirefoxVersion, PATCH_CATEGORIES } from '../utils/validation.js';
6
+ import { isArray, isObject, isValidFirefoxProduct, isValidFirefoxVersion, PATCH_CATEGORIES, } from '../utils/validation.js';
7
7
  function parseForwardImports(data, label) {
8
8
  if (!isArray(data)) {
9
9
  throw new Error(`${label} must be an array`);
@@ -45,10 +45,25 @@ export function validatePatchMetadata(data, index) {
45
45
  const name = rec.string('name');
46
46
  const description = rec.string('description');
47
47
  const createdAt = rec.string('createdAt');
48
- const sourceEsrVersion = rec.string('sourceEsrVersion');
48
+ const sourceEsrVersion = rec.optionalString('sourceEsrVersion');
49
+ const sourceVersion = rec.optionalString('sourceVersion') ?? sourceEsrVersion;
50
+ if (sourceVersion === undefined) {
51
+ throw new Error(`patches[${index}] must include sourceVersion or legacy sourceEsrVersion`);
52
+ }
53
+ const sourceProductRaw = rec.optionalString('sourceProduct');
54
+ let sourceProduct;
55
+ if (sourceProductRaw !== undefined) {
56
+ if (!isValidFirefoxProduct(sourceProductRaw)) {
57
+ throw new Error(`patches[${index}].sourceProduct must be one of: firefox, firefox-esr, firefox-beta, firefox-devedition`);
58
+ }
59
+ sourceProduct = sourceProductRaw;
60
+ }
49
61
  const order = rec.nonNegativeInteger('order');
50
62
  const category = rec.validatedString('category', (value) => /^[a-z][a-z0-9-]*$/.test(value), 'a lowercase category identifier (letters, numbers, hyphens)');
51
- if (!isValidFirefoxVersion(sourceEsrVersion)) {
63
+ if (!isValidFirefoxVersion(sourceVersion)) {
64
+ throw new Error(`patches[${index}].sourceVersion must be a valid Firefox version string`);
65
+ }
66
+ if (sourceEsrVersion !== undefined && !isValidFirefoxVersion(sourceEsrVersion)) {
52
67
  throw new Error(`patches[${index}].sourceEsrVersion must be a valid Firefox version string`);
53
68
  }
54
69
  const filesAffected = rec.stringArray('filesAffected');
@@ -78,9 +93,12 @@ export function validatePatchMetadata(data, index) {
78
93
  name,
79
94
  description,
80
95
  createdAt,
81
- sourceEsrVersion,
96
+ sourceEsrVersion: sourceEsrVersion ?? sourceVersion,
97
+ sourceVersion,
82
98
  filesAffected,
83
99
  };
100
+ if (sourceProduct !== undefined)
101
+ result.sourceProduct = sourceProduct;
84
102
  if (lintIgnore !== undefined)
85
103
  result.lintIgnore = lintIgnore;
86
104
  if (tier !== undefined)
@@ -0,0 +1,8 @@
1
+ import type { PatchMetadata } from '../types/commands/index.js';
2
+ import type { FirefoxConfig, FirefoxProduct } from '../types/config.js';
3
+ /** Metadata fields stamped on new or refreshed patch entries. */
4
+ export declare function buildPatchSourceMetadata(firefox: Pick<FirefoxConfig, 'product' | 'version'>): Pick<PatchMetadata, 'sourceEsrVersion' | 'sourceProduct' | 'sourceVersion'>;
5
+ /** Backward-compatible source version reader for legacy manifests. */
6
+ export declare function getPatchSourceVersion(patch: Pick<PatchMetadata, 'sourceEsrVersion' | 'sourceVersion'>): string;
7
+ /** Backward-compatible source product reader for legacy manifests. */
8
+ export declare function getPatchSourceProduct(patch: Pick<PatchMetadata, 'sourceProduct'>): FirefoxProduct | undefined;
@@ -0,0 +1,17 @@
1
+ /** Metadata fields stamped on new or refreshed patch entries. */
2
+ export function buildPatchSourceMetadata(firefox) {
3
+ return {
4
+ sourceEsrVersion: firefox.version,
5
+ sourceProduct: firefox.product,
6
+ sourceVersion: firefox.version,
7
+ };
8
+ }
9
+ /** Backward-compatible source version reader for legacy manifests. */
10
+ export function getPatchSourceVersion(patch) {
11
+ return patch.sourceVersion ?? patch.sourceEsrVersion;
12
+ }
13
+ /** Backward-compatible source product reader for legacy manifests. */
14
+ export function getPatchSourceProduct(patch) {
15
+ return patch.sourceProduct;
16
+ }
17
+ //# sourceMappingURL=patch-source-metadata.js.map
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Rebase session persistence for multi-patch ESR version upgrades.
2
+ * Rebase session persistence for multi-patch Firefox source upgrades.
3
3
  * Session state is stored at `.fireforge/rebase-session.json` and
4
4
  * survives across CLI invocations so the user can fix conflicts and
5
5
  * resume with `fireforge rebase --continue`.
6
6
  */
7
+ import type { FirefoxProduct } from '../types/config.js';
7
8
  export type RebasePatchStatus = 'pending' | 'applied-clean' | 'applied-fuzz' | 'failed' | 'resolved' | 'skipped';
8
9
  export interface RebasePatchEntry {
9
10
  filename: string;
@@ -18,9 +19,13 @@ export interface RebasePatchEntry {
18
19
  export interface RebaseSession {
19
20
  /** ISO timestamp when the rebase started. */
20
21
  startedAt: string;
21
- /** ESR version being rebased FROM. */
22
+ /** Source product being rebased FROM. */
23
+ fromProduct?: FirefoxProduct;
24
+ /** Source product being rebased TO. */
25
+ toProduct?: FirefoxProduct;
26
+ /** Source version being rebased FROM. */
22
27
  fromVersion: string;
23
- /** ESR version being rebased TO. */
28
+ /** Source version being rebased TO. */
24
29
  toVersion: string;
25
30
  /** Commit hash recorded before the rebase started (for --abort). */
26
31
  preRebaseCommit: string;
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
- * Rebase session persistence for multi-patch ESR version upgrades.
3
+ * Rebase session persistence for multi-patch Firefox source upgrades.
4
4
  * Session state is stored at `.fireforge/rebase-session.json` and
5
5
  * survives across CLI invocations so the user can fix conflicts and
6
6
  * resume with `fireforge rebase --continue`.
@@ -9,6 +9,9 @@
9
9
  * Classification buckets for engine file changes:
10
10
  * - `patch-backed`: content matches the expected post-patch state —
11
11
  * normal after `fireforge import`.
12
+ * - `patch-owned-drift`: the file is claimed by exactly one patch, but
13
+ * the live engine content no longer matches that patch's expected
14
+ * post-apply content.
12
15
  * - `unmanaged`: edits not explained by any patch or tool — local
13
16
  * drift to export or discard.
14
17
  * - `branding`: files under tool-managed branding paths, written by
@@ -23,7 +26,7 @@
23
26
  * in `--json`, which misled scripts built on top of the JSON view
24
27
  * into treating the file as routine local drift.
25
28
  */
26
- export type FileClassification = 'patch-backed' | 'unmanaged' | 'branding' | 'furnace' | 'conflict';
29
+ export type FileClassification = 'patch-backed' | 'patch-owned-drift' | 'unmanaged' | 'branding' | 'furnace' | 'conflict';
27
30
  export interface StatusFile {
28
31
  status: string;
29
32
  file: string;
@@ -123,7 +123,7 @@ export async function classifyFiles(files, engineDir, patchesDir, binaryName, fu
123
123
  const expected = await computePatchedContent(patchesDir, engineDir, entry.file);
124
124
  results.push({
125
125
  ...entry,
126
- classification: expected === null ? 'patch-backed' : 'unmanaged',
126
+ classification: expected === null ? 'patch-backed' : 'patch-owned-drift',
127
127
  });
128
128
  continue;
129
129
  }
@@ -135,13 +135,12 @@ export async function classifyFiles(files, engineDir, patchesDir, binaryName, fu
135
135
  ]);
136
136
  results.push({
137
137
  ...entry,
138
- classification: actual === expected ? 'patch-backed' : 'unmanaged',
138
+ classification: actual === expected ? 'patch-backed' : 'patch-owned-drift',
139
139
  });
140
140
  }
141
141
  catch (error) {
142
- verbose(`Treating ${entry.file} as unmanaged because patch-backed classification failed: ${toError(error).message}`);
143
- // If we can't read the file, treat as unmanaged
144
- results.push({ ...entry, classification: 'unmanaged' });
142
+ verbose(`Treating ${entry.file} as patch-owned drift because patch-backed classification failed: ${toError(error).message}`);
143
+ results.push({ ...entry, classification: 'patch-owned-drift' });
145
144
  }
146
145
  }
147
146
  return results;
@@ -1,3 +1,4 @@
1
+ import type { FirefoxProduct } from '../types/config.js';
1
2
  import { FireForgeError } from './base.js';
2
3
  /**
3
4
  * Error thrown when Firefox source download fails.
@@ -8,6 +9,16 @@ export declare class DownloadError extends FireForgeError {
8
9
  constructor(message: string, url?: string | undefined, cause?: Error);
9
10
  get userMessage(): string;
10
11
  }
12
+ /**
13
+ * Error thrown when a pinned Firefox source archive checksum does not match.
14
+ */
15
+ export declare class ChecksumMismatchError extends DownloadError {
16
+ readonly product: FirefoxProduct;
17
+ readonly expectedSha256: string;
18
+ readonly actualSha256: string;
19
+ constructor(product: FirefoxProduct, expectedSha256: string, actualSha256: string, url: string);
20
+ get userMessage(): string;
21
+ }
11
22
  /**
12
23
  * Error thrown when extraction of the downloaded archive fails.
13
24
  */
@@ -1,4 +1,3 @@
1
- // SPDX-License-Identifier: EUPL-1.2
2
1
  import { FireForgeError } from './base.js';
3
2
  import { ExitCode } from './codes.js';
4
3
  /**
@@ -23,6 +22,39 @@ export class DownloadError extends FireForgeError {
23
22
  return msg;
24
23
  }
25
24
  }
25
+ /**
26
+ * Error thrown when a pinned Firefox source archive checksum does not match.
27
+ */
28
+ export class ChecksumMismatchError extends DownloadError {
29
+ product;
30
+ expectedSha256;
31
+ actualSha256;
32
+ constructor(product, expectedSha256, actualSha256, url) {
33
+ super(`Downloaded archive SHA-256 mismatch: expected ${expectedSha256}, got ${actualSha256}`, url);
34
+ this.product = product;
35
+ this.expectedSha256 = expectedSha256;
36
+ this.actualSha256 = actualSha256;
37
+ }
38
+ get userMessage() {
39
+ let msg = `Download Error: Firefox source archive checksum mismatch.\n\n` +
40
+ `Product: ${this.product}\n` +
41
+ `URL: ${this.url}\n` +
42
+ `Expected SHA-256: ${this.expectedSha256}\n` +
43
+ `Actual SHA-256: ${this.actualSha256}`;
44
+ msg += '\n\nTo fix this:\n';
45
+ msg += ' 1. Verify firefox.product, firefox.version, and firefox.sha256 in fireforge.json\n';
46
+ msg += ' 2. Compare the pinned hash with Mozilla SHA256SUMMARY for the resolved archive\n';
47
+ if (this.product === 'firefox-devedition') {
48
+ msg +=
49
+ ' 3. Developer Edition archives should resolve under https://archive.mozilla.org/pub/devedition/releases/\n';
50
+ msg += ' 4. Re-run "fireforge download --force" after correcting the source settings';
51
+ }
52
+ else {
53
+ msg += ' 3. Re-run "fireforge download --force" after correcting the source settings';
54
+ }
55
+ return msg;
56
+ }
57
+ }
26
58
  /**
27
59
  * Error thrown when extraction of the downloaded archive fails.
28
60
  */
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Types for the patch system: patch metadata, manifests, lint, and import results.
3
3
  */
4
+ import type { FirefoxProduct } from '../config.js';
4
5
  /**
5
6
  * Patch categories for organizational classification.
6
7
  */
@@ -47,8 +48,15 @@ export interface PatchMetadata {
47
48
  description: string;
48
49
  /** ISO timestamp of when the patch was created */
49
50
  createdAt: string;
50
- /** ESR version the patch was created against (e.g., "140.9.0esr") */
51
+ /**
52
+ * Deprecated compatibility alias for the source version the patch was
53
+ * created against. New writes also include `sourceVersion`.
54
+ */
51
55
  sourceEsrVersion: string;
56
+ /** Firefox source product the patch was created against. */
57
+ sourceProduct?: FirefoxProduct;
58
+ /** Firefox source version the patch was created against. */
59
+ sourceVersion?: string;
52
60
  /** Array of file paths affected by this patch */
53
61
  filesAffected: string[];
54
62
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.27.1",
3
+ "version": "0.27.3",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",