@hominis/fireforge 0.43.0 → 0.43.1

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.
package/CHANGELOG.md CHANGED
@@ -36,6 +36,7 @@
36
36
 
37
37
  - `build-prepare` warns — naming every file — before overwriting engine content that matches neither a patch body nor the pristine baseline, and `fireforge build --refuse-unexported-drift` makes it a hard stop. On a multi-session checkout the silent overwrite is what let an unexported edit be reverted and a later re-export capture a half-reverted hybrid that every gate then passed.
38
38
  - **A furnace mutation that fails now rolls back the engine, not just one that is interrupted.** `runFurnaceMutation` previously restored its rollback journal on SIGINT/SIGTERM only; on a thrown error its `finally` merely deregistered the operation — and because it also marked it completed, the operation vanished from the signal handler's view too. Bodies that catch and restore on their own were unaffected, but `applyAllComponents` rolls back on _collected_ component errors, so anything thrown from its pre- or post-loop work (patch-claim loading, the post-apply consistency checks, the state write) escaped with the engine torn and no `pendingRepair` breadcrumb — reachable from `furnace apply`, `sync`, `deploy`, `build-prepare`, and `doctor --repair-furnace`. The wrapper now runs cleanup callbacks, restores the journal, and rethrows the **original** error; a rollback that itself fails becomes a `pendingRepair` marker plus a warning rather than replacing the error you needed to read. A pre-flight refusal that never registered a journal deliberately writes no marker, so a validation failure cannot block later mutations behind a repair with nothing to reconcile.
39
+ - **The unexported-drift guard false-positived on collapsed untracked directories, and `fireforge build --refuse-unexported-drift` was therefore unusable on any consumer whose build-prepare-owned tree contains a wholly-untracked directory — every scripted build hard-failed on a false alarm.** Git reports a directory in which _every_ file is untracked as a single `?? dir/` entry instead of listing the files under it. `findUnexportedDriftAtRisk` passed raw `git status` entries to the classifier; a directory has no content to compare, so it matched neither a patch body nor the pristine baseline and classified `unmanaged` — an at-risk bucket — before the guard's branding exclusion could apply. The guard now expands collapsed untracked directories through the same `expandUntrackedDirectoryEntries` helper `status`, `reset`, `discard`, `verify`, `lint`, and `export-all` use, so it and `fireforge status --unmanaged` answer the question about the same tree identically by construction. Two subsystems disagreeing was the defect; the agreement is what the regression test asserts. A genuinely unmanaged file inside such a directory is still reported, and a collapsed directory that merely _contains_ a build-prepare-owned prefix is walked only within that prefix, so the expansion never enumerates an unrelated subtree.
39
40
 
40
41
  ### Release hygiene
41
42
 
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.43.0",
4
- "commit": "8229f32cf3e1fee429a82966440480de7618bf39",
5
- "shortCommit": "8229f32cf3e1",
3
+ "version": "0.43.1",
4
+ "commit": "b6b4729474ae50675c5ebcaf25db388b6a6c2352",
5
+ "shortCommit": "b6b4729474ae",
6
6
  "dirty": false,
7
7
  "dirtyHash": null,
8
- "buildTime": "2026-08-20T14:49:17.057Z"
8
+ "buildTime": "2026-08-20T16:28:46.617Z"
9
9
  }
@@ -27,7 +27,7 @@ import { toError } from '../utils/errors.js';
27
27
  import { verbose } from '../utils/logger.js';
28
28
  import { getProjectPaths } from './config.js';
29
29
  import { collectFurnaceManagedPrefixes } from './furnace-config.js';
30
- import { getWorkingTreeStatus } from './git-status.js';
30
+ import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from './git-status.js';
31
31
  import { classifyFiles } from './status-classify.js';
32
32
  /**
33
33
  * Classifications whose on-disk content is explained by NEITHER a patch
@@ -39,6 +39,14 @@ import { classifyFiles } from './status-classify.js';
39
39
  * preserves unmanaged lines rather than overwriting them.
40
40
  */
41
41
  const AT_RISK_CLASSIFICATIONS = new Set(['unmanaged', 'patch-owned-drift', 'conflict']);
42
+ /**
43
+ * Every path prefix build-prepare rewrites from a FireForge-owned source:
44
+ * the branding tree plus every Furnace-managed prefix. (`mozconfig` is a
45
+ * single file, not a prefix, and is tested separately.)
46
+ */
47
+ function buildPrepareOwnedPrefixes(binaryName, furnacePrefixes) {
48
+ return [`browser/branding/${binaryName}/`, ...furnacePrefixes];
49
+ }
42
50
  /**
43
51
  * True when `file` sits under a path build-prepare rewrites from a
44
52
  * FireForge-owned source: the branding tree, a Furnace-managed prefix, or
@@ -47,14 +55,59 @@ const AT_RISK_CLASSIFICATIONS = new Set(['unmanaged', 'patch-owned-drift', 'conf
47
55
  function isOverwrittenByBuildPrepare(file, binaryName, furnacePrefixes) {
48
56
  if (file === 'mozconfig')
49
57
  return true;
50
- if (file.startsWith(`browser/branding/${binaryName}/`))
51
- return true;
52
- for (const prefix of furnacePrefixes) {
58
+ for (const prefix of buildPrepareOwnedPrefixes(binaryName, furnacePrefixes)) {
53
59
  if (file === prefix || file.startsWith(prefix))
54
60
  return true;
55
61
  }
56
62
  return false;
57
63
  }
64
+ /**
65
+ * Narrows the status entries to the ones build-prepare can overwrite,
66
+ * expanding collapsed untracked directories on the way.
67
+ *
68
+ * Git reports a WHOLLY untracked directory as a single `?? dir/` entry
69
+ * rather than listing the files under it. A directory entry has no
70
+ * content to compare, so `classifyFiles` matched it against neither a
71
+ * patch body nor the pristine baseline and bucketed it `unmanaged` — an
72
+ * at-risk classification. 0.43.0 shipped the guard on the raw status and
73
+ * so warned about every untracked branding subdirectory on every build,
74
+ * while `status --unmanaged` (which expands) reported the same tree
75
+ * clean. Expanding here is what makes the two agree by construction.
76
+ *
77
+ * Expansion is scoped to build-prepare-owned prefixes on both sides of
78
+ * the walk: a collapsed directory that merely CONTAINS an owned prefix
79
+ * (`components/` when `components/custom/` is Furnace-managed) is
80
+ * rewritten to the owned prefixes beneath it, so the enumeration never
81
+ * walks an unrelated subtree — and never trips the per-directory
82
+ * expansion cap on a tree whose owned prefixes are small.
83
+ */
84
+ async function collectOverwrittenEntries(engineDir, status, binaryName, furnacePrefixes) {
85
+ const ownedPrefixes = buildPrepareOwnedPrefixes(binaryName, furnacePrefixes);
86
+ const candidates = [];
87
+ for (const entry of status) {
88
+ if (isOverwrittenByBuildPrepare(entry.file, binaryName, furnacePrefixes)) {
89
+ candidates.push(entry);
90
+ continue;
91
+ }
92
+ if (!entry.isUntracked || !entry.file.endsWith('/'))
93
+ continue;
94
+ // A collapsed untracked ancestor of an owned prefix: walk only the
95
+ // owned subtrees under it, never the whole directory.
96
+ for (const prefix of ownedPrefixes) {
97
+ if (prefix.startsWith(entry.file)) {
98
+ candidates.push({ ...entry, file: prefix });
99
+ }
100
+ }
101
+ }
102
+ if (candidates.length === 0)
103
+ return [];
104
+ const expanded = await expandUntrackedDirectoryEntries(engineDir, candidates);
105
+ // Re-test after expansion: an ancestor rewrite can only widen, and a
106
+ // directory that survived expansion (nothing untracked under it any
107
+ // more) carries no content to classify.
108
+ return expanded.filter((entry) => !entry.file.endsWith('/') &&
109
+ isOverwrittenByBuildPrepare(entry.file, binaryName, furnacePrefixes));
110
+ }
58
111
  /**
59
112
  * Finds engine files that build-prepare is about to overwrite and whose
60
113
  * current content is recorded nowhere.
@@ -73,7 +126,7 @@ export async function findUnexportedDriftAtRisk(projectRoot, config) {
73
126
  if (status.length === 0)
74
127
  return [];
75
128
  const furnacePrefixes = await collectFurnaceManagedPrefixes(projectRoot);
76
- const candidates = status.filter((entry) => isOverwrittenByBuildPrepare(entry.file, config.binaryName, furnacePrefixes));
129
+ const candidates = await collectOverwrittenEntries(paths.engine, status, config.binaryName, furnacePrefixes);
77
130
  if (candidates.length === 0)
78
131
  return [];
79
132
  const classified = await classifyFiles(candidates, paths.engine, paths.patches, config.binaryName, furnacePrefixes);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.43.0",
3
+ "version": "0.43.1",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",