@hominis/fireforge 0.17.0 → 0.18.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 +53 -0
- package/README.md +60 -33
- package/dist/src/commands/build.js +18 -4
- package/dist/src/commands/doctor-furnace-manifest-sync.d.ts +18 -0
- package/dist/src/commands/doctor-furnace-manifest-sync.js +159 -0
- package/dist/src/commands/doctor-furnace.js +2 -0
- package/dist/src/commands/doctor-working-tree.d.ts +29 -0
- package/dist/src/commands/doctor-working-tree.js +93 -0
- package/dist/src/commands/doctor.js +22 -12
- package/dist/src/commands/export-all.js +74 -4
- package/dist/src/commands/export-shared.d.ts +7 -1
- package/dist/src/commands/export-shared.js +21 -3
- package/dist/src/commands/furnace/create-xpcshell.js +4 -2
- package/dist/src/commands/furnace/override.js +23 -13
- package/dist/src/commands/furnace/preview.js +38 -0
- package/dist/src/commands/furnace/remove.js +75 -1
- package/dist/src/commands/furnace/rename-xpcshell.d.ts +35 -0
- package/dist/src/commands/furnace/rename-xpcshell.js +97 -0
- package/dist/src/commands/furnace/rename.js +32 -4
- package/dist/src/commands/lint.js +19 -6
- package/dist/src/commands/patch/delete.js +4 -1
- package/dist/src/commands/patch/reorder.js +4 -1
- package/dist/src/commands/re-export-files.js +3 -1
- package/dist/src/commands/re-export.js +4 -1
- package/dist/src/commands/rebase/index.js +19 -1
- package/dist/src/commands/register.js +11 -0
- package/dist/src/commands/status.js +44 -5
- package/dist/src/commands/test.js +68 -16
- package/dist/src/commands/token-coverage.js +10 -3
- package/dist/src/commands/verify.js +81 -6
- package/dist/src/commands/watch.js +43 -7
- package/dist/src/commands/wire.js +16 -0
- package/dist/src/core/browser-wire.js +21 -4
- package/dist/src/core/build-audit.js +10 -0
- package/dist/src/core/furnace-constants.d.ts +14 -0
- package/dist/src/core/furnace-constants.js +16 -0
- package/dist/src/core/furnace-validate.js +67 -1
- package/dist/src/core/git-base.d.ts +27 -2
- package/dist/src/core/git-base.js +41 -3
- package/dist/src/core/git-diff.js +21 -2
- package/dist/src/core/git.js +53 -14
- package/dist/src/core/mach.d.ts +26 -8
- package/dist/src/core/mach.js +24 -8
- package/dist/src/core/manifest-rules.js +10 -1
- package/dist/src/core/manifest-tokenizers.d.ts +6 -0
- package/dist/src/core/manifest-tokenizers.js +28 -0
- package/dist/src/core/marionette-preflight.d.ts +16 -0
- package/dist/src/core/marionette-preflight.js +19 -0
- package/dist/src/core/patch-lint-diff-tag.d.ts +20 -0
- package/dist/src/core/patch-lint-diff-tag.js +25 -0
- package/dist/src/core/patch-lint.d.ts +47 -2
- package/dist/src/core/patch-lint.js +94 -18
- package/dist/src/core/patch-manifest-consistency.js +15 -2
- package/dist/src/core/patch-manifest-io.js +10 -0
- package/dist/src/core/patch-manifest-resolve.d.ts +20 -1
- package/dist/src/core/patch-manifest-resolve.js +29 -2
- package/dist/src/core/patch-manifest-validate.js +25 -1
- package/dist/src/core/patch-registration-refs.d.ts +42 -0
- package/dist/src/core/patch-registration-refs.js +117 -0
- package/dist/src/core/token-coverage.js +24 -0
- package/dist/src/core/wire-destroy.d.ts +7 -3
- package/dist/src/core/wire-destroy.js +11 -6
- package/dist/src/core/wire-init.d.ts +9 -3
- package/dist/src/core/wire-init.js +18 -6
- package/dist/src/core/wire-subscript.d.ts +7 -3
- package/dist/src/core/wire-subscript.js +11 -4
- package/dist/src/core/xpcshell-appdir.d.ts +19 -5
- package/dist/src/core/xpcshell-appdir.js +46 -20
- package/dist/src/errors/git.d.ts +20 -0
- package/dist/src/errors/git.js +39 -0
- package/dist/src/types/commands/patches.d.ts +23 -0
- package/dist/src/types/furnace.d.ts +9 -0
- package/dist/src/utils/parse.d.ts +7 -0
- package/dist/src/utils/parse.js +15 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,62 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.0
|
|
4
|
+
|
|
5
|
+
### Compatibility
|
|
6
|
+
|
|
7
|
+
- **No re-export required for existing 0.17.0 patches.** Every fix in this release is additive or defensive: the patch body format is unchanged, the `patches.json` schema only gains two optional fields (both already round-tripped through the validator in 0.17.0), and `fireforge import`/`verify` against a 0.17.0-exported queue continues to behave identically. Operators can upgrade in place.
|
|
8
|
+
- **No breaking API changes for downstream `import '@hominis/fireforge'` consumers.** `src/core/mach.ts`'s `build()` / `buildUI()` signatures changed from `Promise<number>` to `Promise<MachCommandResult>` so `fireforge build` can annotate the tail of mach's output (see below), but neither function is exported from `src/index.ts` — the public surface stays the same.
|
|
9
|
+
- **Minor cosmetic drift on wire-generated code and token coverage percentages.** `fireforge wire` now stamps `// <BINARY>: wire-<step> <name>` marker comments (see below). Patches exported before 0.18.0 that captured the pre-fix `// <name> init — must be first…` form continue to apply cleanly — the wire mutator's idempotency check is keyed on the call expression, not the comment, so re-running `wire` against a 0.17.0-wired tree is still a no-op and will not double up. Operators who want the marker in existing patches can either re-run `wire` after deleting the old block, or hand-edit the patch once. Token coverage percentages shift upward on any project that copies `--moz-*` CSS baselines (now counted as allowlisted platform vars instead of unknown) — CI thresholds pinned to 0.17.0 numbers may need a one-time adjustment.
|
|
10
|
+
|
|
11
|
+
### Hardening
|
|
12
|
+
|
|
13
|
+
- **`fireforge lint` (aggregate) + `fireforge export-all` — EISDIR crashes on patches that introduce new directories.** `resolveLintDiff` in `src/commands/lint.ts` and `resolveFurnaceExclusionPolicy` / the `--exclude-furnace` branch in `src/commands/export-all.ts` both blended `getModifiedFiles()` + `getUntrackedFiles()` (or raw `getWorkingTreeStatus()`) before handing the path list to `getDiffForFilesAgainstHead`. `git status --porcelain=v1 -z` collapses untracked directories to `?? browser/modules/<fork>/` entries, so `generateNewFileDiff` inside the diff pass called `readText()` on a directory path and crashed with `EISDIR: illegal operation on a directory, read` — the eval's imported hominis queue and its fresh-project register-wire workflow both hit this. The fix plumbs `expandUntrackedDirectoryEntries` (already present for `status --ownership`) through both call sites so only file paths reach the diff pass, and `getDiffForFilesAgainstHead` in `src/core/git-diff.ts` now runs a belt-and-suspenders expansion via `getUntrackedFilesInDir` before reading any candidate as a file — a single bad call site cannot re-introduce the crash at that layer. `fireforge lint --per-patch` already worked; aggregate mode and `export-all` now do too.
|
|
14
|
+
- **`fireforge doctor` — ownership-aware working-tree classification.** `runEngineGitChecks` in `src/commands/doctor.ts` previously emitted a single generic `Engine working tree has <N> local changes` warning for any dirty count and told the operator to `export, discard, or reset them as appropriate`. That advice was destructive on a freshly-imported patch stack (eval: a correctly imported 126-file queue on `hominis` triggered the warning, and the suggested `discard` / `reset` recovery would have dropped the entire import) and spurious on a fresh project with tool-managed branding (eval: 63/64 dirty files after a successful build were classified as `branding` by `fireforge status`, yet `doctor` still told the operator to export / discard them). The new `inspectEngineWorkingTree` helper in `src/commands/doctor-working-tree.ts` (split out to keep `doctor.ts` under max-lines) reuses the shared `classifyFiles` pipeline — same partitioning as `fireforge status --ownership`: `branding` / `furnace` / `patch-backed` / `conflict` / `unmanaged`. Doctor now warns only on the `unmanaged` bucket (and raises a loud distinct warning on `conflict`, with a pointer at `status --ownership` + `verify`); tool-managed buckets pass with an ownership summary (`N tool-managed changes (X patch-backed, Y branding), 0 unmanaged`). A missing config falls back to the legacy count-only warning so a broken project still gets a useful row. Ordering dependency on `fireforge.json is valid` is declared explicitly via `dependsOn`.
|
|
15
|
+
- **`fireforge furnace remove` — drops the locale jar.mn chrome registration for localized custom components.** Before this fix, `furnaceRemoveCommand` in `src/commands/furnace/remove.ts` deleted the `.ftl` file and the component directory but never dropped the `locale/@AB_CD@/<chromeSubPath>/<tag>.ftl` entry that `applyCustomFtlFile` wrote during deploy. The eval's `furnace remove moz-eval-chip --yes` left `engine/browser/locales/jar.mn` pointing at the now-missing FTL; `furnace validate` still passed, and later builds/packaging would have tripped over the stale reference. The fix threads the existing `removeCustomFtlJarMnEntry` helper from `src/core/furnace-apply-ftl.ts` into the localized branch of `furnaceRemoveCommand`, snapshot-protected by the same rollback journal that guards the FTL-file removal so a mid-remove failure restores both together. `fireforge furnace validate` after remove now reports a clean state without a follow-up `apply --dry-run` cleanup plan.
|
|
16
|
+
- **`fireforge furnace rename` — rewires the locale jar.mn entry on localized renames.** `updateEngineRegistrations` in `src/commands/furnace/rename.ts` renamed the `.ftl` file on disk but left the locale jar.mn pointing at `locale/.../${oldName}.ftl`. `fireforge furnace validate` passed anyway because the validator only checked the toolkit `jar.mn` (for `.mjs`/`.css` entries), not the locale jar.mn — the eval reported "validate passed" for two full minutes while the engine carried a stale registration for a file that no longer existed. The fix, gated on `customConfig.localized`, calls `removeLocaleFtlJarMnEntry(oldName)` + `addLocaleFtlJarMnEntry(newName)` against `resolveFtlLocaleJarMnPath(config.ftlBasePath)` inside the same rollback journal as the FTL file rename, so both changes land together or roll back together. Non-localized renames are unaffected.
|
|
17
|
+
- **`fireforge furnace override` — concurrent invocations no longer silently drop each other's manifest entries.** The eval ran two `furnace override` commands in parallel against the same fresh project (one `moz-button`, one `moz-card`): both exited `0`, both override directories landed on disk, but `furnace.json` kept only one entry. Root cause: `performOverrideMutations` acquired the furnace lock via `runFurnaceMutation` but `saveOverrideConfig` wrote back the outer-snapshot config object — each invocation had loaded an empty-overrides state BEFORE entering the lock, and the second writer clobbered the first writer's addition. `saveOverrideConfig` now re-reads fresh furnace.json state inside the lock via `loadAuthoringFurnaceConfig`, splices the new entry plus the stock-bucket removal onto the fresh state, and writes that back — so a sibling writer's addition survives into the final manifest. A concurrent-writer regression test under `furnace-override.test.ts` pins the contract; other furnace mutations (`remove`, `rename`, `deploy`) already re-read inside the lock and need no equivalent fix.
|
|
18
|
+
- **`fireforge doctor --repair-furnace` — detects and recovers orphaned override directories.** New `Furnace manifest sync` check in the new `src/commands/doctor-furnace-manifest-sync.ts` module (spliced into `FURNACE_DOCTOR_CHECKS` so `doctor-furnace.ts` stays under max-lines). Without `--repair-furnace` the check warns when `components/overrides/<name>/` or `components/custom/<name>/` exists on disk but is absent from `furnace.json` — the exact residue left by the concurrent-override race above or by any interrupted mutation. With `--repair-furnace`, override orphans are re-registered by reading the `override.json` sidecar `furnace override` writes during the copy phase (`type`, `description`, `basePath`, `baseVersion`, optional `baseCommit`), folding the recovered entry into a fresh furnace.json read, and persisting. Custom orphans are listed for the operator to either `furnace create` against or delete manually, because custom components have no equivalent sidecar — fabricating `targetPath` / `register` / `localized` values is worse than the orphaned directory itself. Doctor now catches the residue from eval 2's concurrent-override reproduction and the symmetric case where an operator SIGINT's a mutation mid-flight.
|
|
19
|
+
- **`fireforge build` — annotates mach's trailing `config.status is out of date` notice instead of leaving it to read as build failure.** mach's post-build guard prints `config.status is out of date with respect to browser/moz.configure` when a tool-managed edit (most commonly `setupBranding`'s `patchMozConfigure` on first build) landed on `moz.configure` before the build. The guard runs AFTER `Your build was successful!` so operators reading the tail of a successful build saw contradictory signals — the eval recorded this on both a fresh project's first build and a pre-existing hominis build. `build()` and `buildUI()` in `src/core/mach.ts` now return the captured `MachCommandResult` (stdout/stderr tail + exit code) instead of just an exit code; `buildCommand` in `src/commands/build.ts` scans `result.stdout`/`stderr` for `/config\.status is out of date/i` and, on match, emits a one-line `info(...)` explaining the notice is a known side effect of tool-managed branding edits and does not require a rebuild. Test-command `buildUI` callers in `src/commands/test.ts` pick up the new return shape; no caller outside the CLI sees the signature change (neither function is exported from `src/index.ts`).
|
|
20
|
+
- **`fireforge test --doctor` — triple-path PASS footer survives non-TTY clack rendering.** The eval's `test --doctor` captures on both fresh and hominis projects showed `● Running marionette preflight...` and then exited 0 with nothing else — the 0.17.0 `outro(\`Marionette preflight: PASS (...)\`)`was dropped by clack somewhere in the non-TTY render pipeline (exact root cause could not be isolated from captures alone). Rather than chase the rendering bug, the fix triple-writes the result:`success(\`Marionette preflight: PASS (...)\`)`, then `outro('Test completed')`closes the intro frame, then`process.stdout.write(\`${summary}\n\`)`guarantees the line reaches stdout regardless of clack's buffering. A test in`test.test.ts` asserts all three emissions.
|
|
21
|
+
- **`fireforge test` — fork-owned module import failures beat the branding stale-build diagnosis.** The eval's hominis xpcshell test failed loading `resource:///modules/hominis/HominisStore.sys.mjs`; FireForge reported "stale build artifacts, run fireforge build --ui" because the harness teardown also printed a branding warning that matched the narrower stale-build pattern. Re-running the build did nothing — the real failure was that the new module wasn't registered in `browser/modules/hominis/moz.build`. `handleNonZeroTestExit` now checks for `Failed to load resource:///modules/<binaryName>/` BEFORE the branding stale-build branch and surfaces a fork-module-specific diagnostic pointing at `browser/modules/<binary>/moz.build` + `fireforge register`, so the operator sees the correct recovery path on the first try. Existing branding-stale diagnosis is untouched.
|
|
22
|
+
- **`fireforge token coverage` — `--moz-*` platform variables allowlisted by default; untracked CSS directories now scanned.** Two independent token-coverage fixes:
|
|
23
|
+
- **Platform allowlist** — `src/core/token-coverage.ts` seeds a default `platformPrefixes: ['--moz-']` and treats matches as `allowlisted` rather than `unknown`. A CSS-only Furnace override of `moz-button` with one fork token previously reported 1% coverage because the copied upstream baseline referenced 84 `--moz-*` platform vars; coverage now reports 100% on the same diff, and the one unknown was correctly declared by the fork. Forks can override through a new optional `platformPrefixes?: string[]` field on `furnace.json` — extending the list (e.g. `['--moz-', '--in-content-']`) for forks with additional platform prefixes, or setting an explicit empty array to restore the pre-0.18 strict contract. The field is added to the public `FurnaceConfig` type.
|
|
24
|
+
- **Untracked-directory expansion** — `src/commands/token-coverage.ts` now reads `getWorkingTreeStatus` + `expandUntrackedDirectoryEntries` instead of `getStatusWithCodes` so CSS files inside a `?? dir/` untracked directory (the common shape for a freshly-imported patch stack adding a new theme tree) show up in the scan. The eval's hominis run saw `?? browser/branding/hominis/content/aboutDialog.css` and `?? browser/themes/shared/hominis-tokens.css` collapse to directory entries that didn't end in `.css`; coverage reported "No modified CSS files". Both CSS files are now scanned.
|
|
25
|
+
- **`fireforge patch reorder` + `fireforge patch delete` — accept manifest `name` handle in addition to filename and ordinal.** `resolvePatchIdentifier` in `src/core/patch-manifest-resolve.ts` previously rejected the `name` field even though CLI help says `<name>` and the manifest stores a `name` on every entry. Operators had to copy the full filename from `patches.json` before every queue mutation. The resolver now matches, in order: ordinal number → exact filename → filename-with-`.patch`-appended → `name` field. Filename lookup still wins when a manifest happens to have a `name` identical to a different filename — legacy scripts passing filenames keep working. Error messages on both commands now list filenames AND their `name` counterparts so operators see both forms as valid identifiers.
|
|
26
|
+
- **`fireforge patch reorder` — postcondition assert on each Phase 2 rename.** The eval reported that a two-patch swap updated `patches.json` but left the files at their pre-rename names, and `fireforge verify` then failed ENOENT opening the manifest-renamed file. The code path in `renumberPatchesInManifest` (two-phase staging: stage → final + manifest write) appears correct against both the integration test in `patch-mutations.integration.test.ts` and the shipped 0.17.0 dist — but the eval's operation log shows the manifest was updated while the rename was missing, so a filesystem-specific failure mode exists that the existing integration test does not exercise. Defense-in-depth lands a postcondition `pathExists(newFilename)` assert after every `rename()` in Phase 2, so any silent rename failure aborts the reorder before the manifest write and triggers the Phase 2 rollback (which itself reverses completed final-name moves and unwinds the staging). A second integration test reproduces the exact eval scenario (`001-infra` + `002-ui` swap via `--to 1 --yes`) and asserts both filenames exist on disk after completion.
|
|
27
|
+
- **`fireforge register --dry-run` — honours the rule-level idempotency decision.** `registerFile` in `src/core/manifest-rules.ts` returned `{ skipped: true }` when the rule's `isRegistered(...)` check found the file already present in its manifest, but `registerCommand` in `src/commands/register.ts` ignored the flag on the dry-run branch and always printed `[dry-run] Would register <path>`. The next real invocation correctly reported `Already registered: <path>`. Automation that consumed the dry-run as a plan mis-predicted unnecessary work. Dry-run now prints `[dry-run] Already registered: <path> in <manifest>` when `result.skipped` is true — mirroring the real-run copy — and otherwise prints the registration plan as before.
|
|
28
|
+
- **`fireforge wire` — `// <BINARY>:` marker comments in wire-generated code.** `lintModificationComments` in `src/core/patch-lint.ts` checks JS/MJS additions for a `// <binaryName.toUpperCase()>:` marker (case-insensitive) and flags `missing-modification-comment` on upstream file edits that lack it. `fireforge wire` itself wrote comments like `// <name> init — must be first, before Firefox subsystem` — generic text that trips the patch-lint rule on the first export after wiring, so the eval saw `[missing-modification-comment] browser/base/content/browser-init.js` on content generated by FireForge's own command. Each wire mutator (`addInitAST`/`legacyAddInit`, `addDestroyAST`/`legacyAddDestroy`, `addSubscriptAST`/`legacyAddSubscript`) now accepts a `marker: string` parameter (default `'FIREFORGE:'` for back-compat with programmatic callers); `browser-wire.ts:wireSubscript` computes the project-scoped marker as `${config.binaryName.toUpperCase()}:` from `fireforge.json` and threads it through. Emitted blocks now carry `// FRESHFORGE: wire-init <name> — must be first, …` (or the caller's equivalent uppercased binaryName), satisfying the lint rule on the first export. Patches exported before 0.18.0 that captured the old comment format still apply cleanly — patch content is unchanged — they just won't have the marker; operators who want the marker in existing patches can re-run `wire` after deleting the old block (idempotency is keyed on the call expression, so re-running alone is a no-op). `.inc.xhtml` fragments inserted by `--dom` are preprocessor directives, not JS — `lintModificationComments` only checks JS, so no marker is needed there.
|
|
29
|
+
- **`fireforge build --ui` + `fireforge build` — `.inc.xhtml` fragments excluded from the packaging audit.** `isPackageablePath` in `src/core/build-audit.ts` treated `.inc.xhtml` as a `.xhtml` file, so a wire-introduced `browser/base/content/<name>.inc.xhtml` (consumed via `#include` from a registered chrome document and resolved at packaging time) got flagged as "missing packaged artifact" on the next UI build. `register --dry-run` already refuses to register these fragments with explicit guidance — the build audit now matches that contract with an early `sourcePath.endsWith('.inc.xhtml') → false` return. The two sibling checks now agree about this file type.
|
|
30
|
+
- **`fireforge register` — empty single-line `EXTRA_JS_MODULES += []` lists expand before insertion.** `tokenizeMozBuildList` in `src/core/manifest-tokenizers.ts` tracked opening `[` and closing `]` on separate lines. A freshly-scaffolded `browser/modules/<fork>/moz.build` that wrote `EXTRA_JS_MODULES += []` on one line returned `null` from the tokenizer because the scanner never saw a line STARTING with `]`, and `registerFireForgeModule` failed with `Could not find EXTRA_JS_MODULES in moz.build` — blocking the documented `browser/modules/<fork>/*.sys.mjs` workflow at the first module. The tokenizer now detects the single-line empty-list form `...+= []` at open time, rewrites `lines[i]` to `...+= [` and splices in a `]` at `i + 1`, and emits matched `list-open` / `list-close` tokens for the canonical multi-line shape. Downstream `findAlphabeticalMozBuildPosition` and the register helpers then place the new entry inside the brackets as normal.
|
|
31
|
+
- **`fireforge wire --dry-run` — notice when the subscript file is absent.** The dry-run branch in `src/commands/wire.ts` deliberately skips the subscript-file existence check to support the "wire first, create the file after" scaffolding workflow. Pre-0.18 that deliberate skip produced a plausible plan even when the subscript was genuinely missing, and the real `wire` invocation then refused with `Subscript file not found`. The skip stays (the workflow is legitimate) but dry-run now emits `Note: <subscriptDir>/<name>.js does not exist yet — the real wire command will require it before writing.` so the operator sees the absence in the preview and can create the file before re-running without `--dry-run`.
|
|
32
|
+
- **`fireforge status --unmanaged` + `fireforge register` — browser-chrome test files route to browser.toml, not jar.mn.** The browser-content pattern in `src/core/manifest-rules.ts` matched every `browser/base/content/**/*.{js,mjs,xhtml,css}` path, including test implementation files at `browser/base/content/test/<dir>/browser_*.js`. `status --unmanaged` therefore proposed `fireforge register browser/base/content/test/forgeqa/browser_forgeqa_qa_browser.js` which, if run, would cluttered jar.mn with a test-file chrome URI (the right place for those files is the sibling `browser.toml`). The pattern now excludes the `test/` subtree via a negative lookahead, so these paths fall through to `getUnregistrableAdvice`, which already returns the correct browser.toml-centric guidance for individual test files. `status`, `register`, and `register --dry-run` all now agree that test JS belongs in the test manifest.
|
|
33
|
+
- **`fireforge export-all --exclude-furnace` — refuses patches that would register a furnace component without carrying its source files; `fireforge verify` now flags dangling registrations that slipped through earlier versions.** Finding 1 (High). The evaluator's fresh-project smoke sequence (`furnace init` → `token add` → `furnace override moz-button -t css-only` → `furnace create moz-qa-panel --localized --with-tests --test-style mochikit` → `furnace deploy` → `export-all --exclude-furnace`) produced a patch whose `toolkit/content/customElements.js`, `toolkit/content/jar.mn`, and `toolkit/locales/jar.mn` hunks registered `moz-qa-panel` even though the widget sources were filtered out. `fireforge verify` reported "Verify clean" for the broken queue. The fix lands in two layers: a new `collectPatchRegistrationReferences` scanner in `src/core/patch-registration-refs.ts` extracts component-shaped registration references from any patch body, and `verifyCommand` in `src/commands/verify.ts` now cross-checks each extracted reference against both the aggregate `filesAffected` coverage and the engine working tree — any miss fires a `dangling-registration` error naming the specific patch + target path. `export-all --exclude-furnace` in `src/commands/export-all.ts` calls the same scanner before writing and refuses with a clear message when the hunks would register a furnace-managed component not included in the patch. Tests in `patch-registration-refs.test.ts` + `verify.integration.test.ts` pin both halves.
|
|
34
|
+
- **`fireforge status --unmanaged` — tolerates a missing parent moz.build instead of exiting non-zero.** Finding 2 (Medium). `printUnregisteredWarnings` in `src/commands/status.ts` awaited `Promise.all` on `isFileRegistered` checks, and the manifest-rules layer threw `GeneralError("Manifest not found: ...")` synchronously when the parent `moz.build` was absent — exactly the state an operator hits after scaffolding a new engine module and before running `register`. Status is a read-only reporter; the non-zero exit broke `status --unmanaged` as a discovery tool. The fix wraps each `isFileRegistered` call in a narrowly scoped try/catch, buckets missing-manifest cases into a distinct "registration manifest does not exist yet" warning list, and lets the command exit cleanly. Other error shapes still propagate. A regression test in `status.test.ts` pins the new contract.
|
|
35
|
+
- **`fireforge lint --since HEAD --only-introduced` — aggregate patch-size findings tag as `[introduced]` when the diff set is non-empty.** Finding 4 (Low). The `large-patch-files` and `large-patch-lines` rules emit findings with the synthetic `file: '(patch)'` placeholder, which never matched any entry in the `diffFiles` set. `tagLintIssues` in `src/core/patch-lint-diff-tag.ts` therefore tagged them `[cumulative]` even when the aggregate was entirely the operator's own diff — reading as "pre-existing drift" to an operator asking "what did this task introduce?" The tagger now recognises the synthetic placeholder (exported as `AGGREGATE_PATCH_FILE`) and promotes it to `introduced` whenever the diff set is non-empty. Cumulative semantics are preserved for the empty-diff case. Tests in `patch-lint-diff-tag.test.ts` pin both branches.
|
|
36
|
+
- **`fireforge furnace remove` / `rename` / `validate` — xpcshell scaffolds are cleaned up, renamed, and flagged as orphans.** Finding 5 (Medium). `furnace create --with-tests --xpcshell` scaffolds at `browser/base/content/test/<binary>-xpcshell/<name>/`, but the pre-0.18.1 `furnace remove` only touched the sibling browser-mochitest tree and `furnace rename` never reached the scaffold at all. Operators who ran create → rename → remove were left with an orphan scaffold whose filenames still referenced the original pre-rename component. The path template is now centralised in `xpcshellTestParentDir` in `src/core/furnace-constants.ts`; `create-xpcshell.ts` consumes it, a new `cleanupCustomXpcshellTestFiles` helper in `src/commands/furnace/remove.ts` removes the scaffold under the journal contract, a new `renameXpcshellTestFiles` helper in `src/commands/furnace/rename-xpcshell.ts` (extracted to keep `rename.ts` under the per-file LOC budget) updates the directory, test filename, TOML section header, and word-boundary tag references in the test body, and `findOrphanXpcshellScaffolds` in `src/core/furnace-validate.ts` walks the parent directory reporting any entry whose name is not in furnace.json as an `orphan-xpcshell-scaffold` error. The validator degrades silently on projects that never used xpcshell scaffolding. Test coverage lands in `furnace-validate-xpcshell-orphan.test.ts`.
|
|
37
|
+
- **`fireforge doctor --repair-patches-manifest` — recovery guidance no longer contradicts the docs by telling operators to hand-edit patches.json.** Finding 6 (Low). The per-filename warning emitted on recovered manifest entries ended with "Edit patches.json to restore the original description if you have it backed up." — which directly contradicts the README and Hominis docs that treat the manifest as FireForge-owned. The reworded warning now points at `fireforge re-export <filename> --description "<your description>"` (or the equivalent `fireforge export` invocation) to overwrite the reconstructed metadata through the tool, and explicitly warns against hand-editing. Test coverage in `doctor.test.ts` pins both the presence of the new guidance and the absence of the old "Edit patches.json" string.
|
|
38
|
+
- **`fireforge test --doctor` — PASS/FAIL line written directly to stdout, bypassing any clack flush races.** Finding 7 (Medium). The 0.18.0 "belt-and-suspenders" approach still reproducibly dropped the PASS footer under non-TTY capture — the eval log showed only the intro and `Running marionette preflight...` banner before exit. A new `formatMarionettePreflightLine` helper in `src/core/marionette-preflight.ts` returns the raw banner as a plain string, and `testCommand` in `src/commands/test.ts` writes both the "Running marionette preflight..." intro and the final PASS/FAIL summary via `process.stdout.write` as the authoritative emissions so non-TTY captures always see the summary. The clack `success()` + `outro()` calls are retained for TTY framing. Test coverage in `test.test.ts` now spies on `process.stdout.write` and asserts both the intro and PASS lines are emitted.
|
|
39
|
+
- **`fireforge test` — xpcshell appdir probe prefers the macOS `.app/Contents/Resources/<value>` layout on Darwin.** Finding 8 (High). `resolveAbsoluteAppPath` in `src/core/xpcshell-appdir.ts` probed `dist/bin/<value>` first on every platform. On macOS `dist/bin` is a symlink to `<App>.app/Contents/MacOS/`, so `dist/bin/browser` resolved to the _binaries_ directory rather than the Resources tree where `resource:///modules/` is rooted — the auto-injection logged success, but the injected path did not match where modules actually live. The probe now branches on `process.platform`: on Darwin it prefers `<App>.app/Contents/Resources/<value>` first and falls back to `dist/bin/<value>`; other platforms keep the historical order. The operator-facing `buildXpcshellAppdirMessage` in `src/commands/test.ts` also gains a macOS-specific note recommending the `<appname>-appdir = "browser"` xpcshell.toml migration, which is the most reliable fix on rebranded macOS builds. Test coverage in `xpcshell-appdir.test.ts` now exercises both probe orders via `process.platform` branches.
|
|
40
|
+
- **`fireforge download` — git-indexing timeouts raise a typed `GitIndexingTimeoutError` with env-var recovery guidance, and the monolithic → chunked transition is visible in non-TTY logs.** Finding 10 (High). On a loaded filesystem the fresh Firefox source indexing legitimately exceeded the 10-minute monolithic `git add -A` budget; the chunked fallback then hit its own `AbortSignal.timeout` and surfaced a generic `AbortError: The operation was aborted` with no recovery direction. `src/core/git-base.ts` now exposes `FIREFORGE_GIT_ADD_TIMEOUT_MS` and `FIREFORGE_GIT_ADD_CHUNK_TIMEOUT_MS` environment variables that override the monolithic (default 10 min) and chunked (default grew to 30 min) timeouts respectively; the chunked pass's timeout is wrapped into a typed `GitIndexingTimeoutError` in `src/errors/git.ts` that names the elapsed budget, points at the environment variable override, and explains the `fireforge download --force` resume path. The fallback-transition progress banner now names the specific timeout so operators watching a non-TTY log see exactly when the monolithic attempt lost. Test coverage in `git-performance.test.ts` pins both the new error type and the refreshed transition banner.
|
|
41
|
+
- **`fireforge rebase --dry-run` — refuses when the engine has no baseline commit.** Finding 11 (Medium). A partially-initialised engine (e.g. the aftermath of an aborted `download --force`) has `.git/` in place but no valid HEAD; the real `rebase --yes` immediately failed with `fatal: ambiguous argument 'HEAD'` even though dry-run had previously reported "Dry run complete" suggesting the rebase was ready to run. `handleFreshStart` in `src/commands/rebase/index.ts` now calls `getHead(paths.engine)` and, on `isMissingHeadError`, throws a clear `GeneralError` pointing at `fireforge download --force`. The check runs ahead of the dry-run early-exit so dry-run and real-run preconditions stay in sync. Test coverage in `rebase.test.ts` pins both the dry-run and real-run refusal paths.
|
|
42
|
+
- **`fireforge watch` — resolved watchman directory is prepended to the mach subprocess PATH.** Finding 12 (Medium). `fireforge watch` located watchman via the parent shell PATH but the `mach watch` subprocess inherited the Node parent's PATH, which on macOS frequently omits `/opt/homebrew/bin`. `mach watch` then failed at the `watch-project` subscription step with a confusing `FasterBuildException: timed out`. `watch.ts` now resolves watchman's absolute path via `findExecutable`, prepends the containing directory to the subprocess PATH, and threads the composed env through a new optional `options.env` parameter on `watchWithOutput` in `src/core/mach.ts`. The watch-failure diagnostic also gains a line naming the resolved watchman path so operators can distinguish "FireForge didn't find watchman" from "FireForge found watchman but mach still failed to reach it." Test coverage in `watch.test.ts` pins the env plumbing and the de-duplication branch.
|
|
43
|
+
- **`fireforge furnace preview` — first-run banner frames the unavoidable npm noise from `mach storybook`.** Finding 13 (Low). `mach storybook` internally drives a ~1000-package `npm install` when the Storybook workspace's `node_modules/` is absent and emits `npm error code ELSPROBLEMS` + `UNMET DEPENDENCY` lines verbatim. Operators on a fresh Firefox checkout read the npm block as a fatal error even though the command recovers and Storybook starts. `furnacePreviewCommand` in `src/commands/furnace/preview.ts` now checks for the absent `node_modules` before spawning mach and emits a clear pre-banner explaining the npm output is an expected one-time first-run cost; an explicit "Storybook stopped cleanly." line fires on clean exits so the npm noise is visually terminated. The `--install` path skips the banner because `mach storybook upgrade` already runs its install before `mach storybook` launches. Test coverage in `furnace-preview.test.ts` pins both the presence of the banner when node_modules is absent and its absence when node_modules is already populated.
|
|
44
|
+
|
|
45
|
+
### Internal
|
|
46
|
+
|
|
47
|
+
- **Module extractions preserve the per-file LOC budget.** `doctor-working-tree.ts` hosts `inspectEngineWorkingTree` (the ownership-aware working-tree check), `doctor-furnace-manifest-sync.ts` hosts the orphan override / custom detection + repair, `patch-registration-refs.ts` hosts the dangling-registration scanner shared between `verify` and `export-all`, and `rename-xpcshell.ts` hosts the xpcshell scaffold rename path. All spliced into their respective orchestrators from the hosting file, keeping `doctor.ts`, `doctor-furnace.ts`, `rename.ts`, and `preview.ts` under the 500-line `max-lines` rule. The drift test in `manifest.test.ts` classifies each new file as a helper so the top-level command drift check stays clean.
|
|
48
|
+
- **Expanded test coverage.** Every eval3 fix ships with at least one regression test exercising the failing path: `patch-registration-refs.test.ts` + `verify.integration.test.ts` (Finding 1), `status.test.ts` (Finding 2), `patch-lint-diff-tag.test.ts` (Finding 4), `furnace-validate-xpcshell-orphan.test.ts` (Finding 5), `doctor.test.ts` (Finding 6), `test.test.ts` (Finding 7), `xpcshell-appdir.test.ts` (Finding 8), `git-performance.test.ts` (Finding 10), `rebase.test.ts` (Finding 11), `watch.test.ts` (Finding 12), `furnace-preview.test.ts` (Finding 13).
|
|
49
|
+
|
|
50
|
+
### Internal (0.18.0 original)
|
|
51
|
+
|
|
52
|
+
- **Two small module extractions preserve the per-file LOC budget.** `doctor-working-tree.ts` hosts `inspectEngineWorkingTree` (the ownership-aware working-tree check), and `doctor-furnace-manifest-sync.ts` hosts the orphan override / custom detection + repair. Both are spliced into their respective registries from the hosting file, keeping `doctor.ts` and `doctor-furnace.ts` under the 500-line `max-lines` rule. The drift test in `manifest.test.ts` is extended to classify both new files as helpers so the top-level command drift check stays clean.
|
|
53
|
+
- **Expanded test coverage — 34 new assertions.** Every fix above ships with at least one new test exercising the regression path: `src/core/__tests__/git-diff.test.ts` gains a `?? dir/` expansion case; `src/commands/__tests__/doctor.test.ts` gains patch-backed / conflict / orphan-override / manifest-sync-repair cases; `furnace-remove.test.ts` + `furnace-rename.test.ts` pin the locale jar.mn re-wire / removal; `furnace-override.test.ts` pins the concurrent-writer read-modify-write; `build.test.ts` pins the stale-configure annotation; `test.test.ts` pins the fork-module diagnostic + triple-path PASS footer; `token-coverage.test.ts` pins the `--moz-*` allowlist + untracked-dir expansion; `patch-manifest-helpers.test.ts` pins name-field resolution; `patch-mutations.integration.test.ts` pins the two-patch swap postcondition; `register.test.ts` pins dry-run idempotency; `wire-init.test.ts` pins the marker; `manifest-tokenizers.test.ts` + `register-module.test.ts` pin the empty-list expansion; `wire.test.ts` pins the dry-run missing-subscript notice; `manifest-register.test.ts` pins the test-file pattern exclusion.
|
|
54
|
+
|
|
3
55
|
## 0.17.0
|
|
4
56
|
|
|
5
57
|
### Eval-driven hardening
|
|
6
58
|
|
|
59
|
+
- **`fireforge lint --per-patch` — branding-tier detection tolerates the one-line registration sibling + explicit `tier: "branding"` opt-in, and `lintIgnore` now round-trips through the manifest loader.** The 0.16.0 branding tier (notice 3000 / warning 8000 / error 20000) was gated on `isBrandingOnlyPatch` in `src/core/patch-lint.ts` returning true only when _every_ entry in `filesAffected` began with `browser/branding/`. Real-world branding patches follow the Firefox convention of also touching `browser/moz.configure` (to register the new branding flavor with the top-level configure), so the 2026-04-21 external audit reported that `lint --per-patch` still fired `ERROR [large-patch-lines] 001 (patch): Patch is 15665 lines (hard limit: 3000)` on a minimum branding diff — the 0.16.0 fix only helped forks with trivially scoped branding patches. Two mechanisms land together: (a) the auto-detector now accepts a narrow allowlist of branding-registration siblings (`browser/moz.configure`, `browser/confvars.sh`) in addition to the `browser/branding/` prefix, guarded by a "≥1 file under `browser/branding/`" requirement so a config-only patch cannot accidentally qualify; (b) a new optional `tier?: 'branding'` field on `PatchMetadata` forces the branding thresholds regardless of `filesAffected`, threaded through `runPatchLint`, `export`, `export-all`, `re-export`, `re-export --files`, and `lint --per-patch` — mirroring how `lintIgnore` expresses per-patch intent. Precedence stays `test > branding > general` (tests still win even with explicit `tier: "branding"`). A one-line `info()` surfaces the tier choice when branding applies, distinguishing "all files under browser/branding/ + registration siblings" from "operator declared tier: branding". Coupled with a pre-existing latent bug uncovered during the audit: `validatePatchMetadata` in `src/core/patch-manifest-validate.ts` returned a fresh object enumerating only the required fields, silently stripping `lintIgnore` on every manifest load — so the 0.16.0 escape hatch only round-tripped through tests that mocked `loadPatchesManifest` directly. Real operator edits to `patches.json` evaporated. The validator now preserves `lintIgnore` (via a new `optionalStringArray` helper in `src/utils/parse.ts`) and `tier`, and `rebuildPatchesManifest` in `src/core/patch-manifest-consistency.ts` preserves both fields on `doctor --repair-patches-manifest` so the repair path no longer strips them. Unknown `tier` values are rejected at load time with a clear message rather than silently stripped — a typo like `"Branding"` surfaces as a loader error, matching the strict validation already applied to `category` and `sourceEsrVersion`.
|
|
7
60
|
- **`fireforge config` — serialised writes behind a sidecar lock.** The 2026-04-21 eval reproduced silent data loss by running two `fireforge config` invocations in parallel against the same `fireforge.json`: both commands exited `0`, but only one key survived. Atomic-rename writes (`writeJson`'s temp file + rename) prevented torn files but not lost updates: each writer read the pre-state, mutated its own copy, and the second rename clobbered the first writer's change. A new `withConfigFileLock(projectRoot, operation)` helper in `src/core/config.ts` wraps the read-modify-write cycle in `src/commands/config.ts:configCommand` — both the strict-validated and `--force` write branches now take the lock. Reads (`loadConfig`, `loadRawConfigDocument`) stay lock-free: atomic rename means readers always see either the pre- or post-state, so only writers need to be serialised. Stale-lock recovery reuses the existing PID-alive probe from `withFileLock` so a crashed `fireforge config` does not wedge the next command.
|
|
8
61
|
- **`fireforge token add --mode override` — dark values land inside the nested `:root { }`.** The previous `insertDarkModeOverride` in `src/core/token-manager.ts` found the outer `@media (prefers-color-scheme: dark) { }` block's closing `}` and spliced the dark-value declaration before that line — which is _after_ the nested `:root { }` had already closed, producing a declaration outside any rule block. The generated tokens CSS was syntactically malformed after every legitimate `token add --mode override` call, and `token coverage` no longer recognised the added tokens. The fix walks the comment-stripped line array to find the `:root {` opener _inside_ the `@media` block, then depth-counts to its own closing `}`, and inserts there. A fallback path (warn + synthesise a fresh nested `:root` before the outer close) handles malformed scaffolds where the nested `:root` was removed, so we never silently drop a dark value or emit a top-level declaration. The test coverage in `src/core/__tests__/token-manager.test.ts` now pins the invariant that the dark entry's line index must be _less_ than the inner `:root`'s closing `}`, so a future refactor cannot regress to the outer-block landing.
|
|
9
62
|
- **`fireforge furnace rename` — cleans up deployed widgets, renames mochikit test + chrome.toml.** The previous `renameTestFiles` helper in `src/commands/furnace/rename.ts` handled the `browser/base/content/test/<binaryName>/` browser-chrome layout but not the `toolkit/content/tests/widgets/` mochikit layout, and `performRenameMutations` made no attempt to clear `engine/<oldTargetPath>/` after deployment. The 2026-04-21 eval renamed `ff-chip-row` → `ff-chip-stack` and ended up with `engine/toolkit/content/widgets/ff-chip-row/` still deployed, `test_ff-chip-row.html` still importing `chrome://global/content/elements/ff-chip-row.mjs`, and the `chrome.toml` entry still naming the old file. Two new helpers pick up the slack: `removeStaleDeployedComponentDir` snapshots + removes `engine/<oldTargetPath>/` so the next `furnace apply` is the sole writer of the new deployment; `renameMochikitTestFiles` snapshots the old scaffold, rewrites the chrome URI + class-test identifiers to the new name, and updates the widgets `chrome.toml` entry. Both run under the same rollback journal as the rest of the rename, so a later failure restores every touched path.
|
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ Inspired by [fern.js](https://github.com/ghostery/user-agent-desktop) and [Melon
|
|
|
36
36
|
- **Python 3** (required by Firefox's `mach` build system).
|
|
37
37
|
- **Git**
|
|
38
38
|
- Platform build tools: Xcode on macOS, `build-essential` on Linux, Visual Studio Build Tools on Windows.
|
|
39
|
-
- **Watchman** (optional, only required by `fireforge watch`). Install via `brew install watchman` (macOS), `dnf install watchman` (Fedora), or follow the upstream [Meta docs](https://facebook.github.io/watchman/). `fireforge doctor` surfaces a warning row when it is not on `PATH` so the dependency is visible during the usual onboarding sweep rather than at the watch-mode failure site.
|
|
39
|
+
- **Watchman** (optional, only required by `fireforge watch`). Install via `brew install watchman` (macOS), `dnf install watchman` (Fedora), or follow the upstream [Meta docs](https://facebook.github.io/watchman/). `fireforge doctor` surfaces a warning row when it is not on `PATH` so the dependency is visible during the usual onboarding sweep rather than at the watch-mode failure site. `fireforge watch` resolves watchman's absolute path via `which` / `where` and prepends its directory to the subprocess `PATH` it hands mach, so a homebrew-installed watchman at `/opt/homebrew/bin/watchman` (absent from the Node subprocess's default `PATH` on macOS) is still visible to `mach watch` without the operator having to re-export `PATH` manually.
|
|
40
40
|
|
|
41
41
|
### Setup
|
|
42
42
|
|
|
@@ -83,6 +83,10 @@ npx fireforge download --force
|
|
|
83
83
|
npx fireforge rebase
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
`fireforge download` indexes the extracted Firefox source into a fresh git repository — a one-time 1–3 minute pass on a cold SSD, longer on slow or loaded disks. The monolithic `git add -A` is capped at 10 minutes by default and falls back to a per-directory chunked pass (30 minutes per chunk) when the cap hits. If indexing still times out, the command now raises `GitIndexingTimeoutError` with recovery guidance: extend the cap via `FIREFORGE_GIT_ADD_TIMEOUT_MS` (monolithic) and/or `FIREFORGE_GIT_ADD_CHUNK_TIMEOUT_MS` (chunked) in milliseconds, e.g. `FIREFORGE_GIT_ADD_TIMEOUT_MS=1800000 fireforge download --force` for a 30-minute monolithic budget, then re-run `fireforge download --force` — the resume path picks up from the partial git state so the repeat is not wasted work.
|
|
87
|
+
|
|
88
|
+
`fireforge rebase --dry-run` refuses when the engine has no baseline commit yet (e.g. the aftermath of an aborted `download --force`), so dry-run and real-run preconditions stay in sync.
|
|
89
|
+
|
|
86
90
|
## Patch Workflow
|
|
87
91
|
|
|
88
92
|
Patches live in `patches/`, applied by numeric filename prefix and tracked in `patches/patches.json`:
|
|
@@ -193,8 +197,12 @@ This re-exports the fixed patch and clears the conflict state. The command is de
|
|
|
193
197
|
"description": "Replaces default Firefox branding with custom logo",
|
|
194
198
|
"createdAt": "2025-01-15T10:30:00Z",
|
|
195
199
|
"sourceEsrVersion": "140.9.0esr",
|
|
196
|
-
"filesAffected": [
|
|
197
|
-
|
|
200
|
+
"filesAffected": [
|
|
201
|
+
"browser/branding/official/logo.png",
|
|
202
|
+
"browser/themes/custom-shared/tokens.css"
|
|
203
|
+
],
|
|
204
|
+
"lintIgnore": ["large-patch-lines", "large-patch-files"],
|
|
205
|
+
"tier": "branding"
|
|
198
206
|
}
|
|
199
207
|
]
|
|
200
208
|
}
|
|
@@ -202,6 +210,8 @@ This re-exports the fixed patch and clears the conflict state. The command is de
|
|
|
202
210
|
|
|
203
211
|
The optional `lintIgnore` field lists lint check IDs to suppress for that patch specifically. Useful for the class of patch that is advisory-noisy by nature — a cohesive branding bundle, a localised-resource pack, an auto-generated manifest — where `--skip-lint` is too blunt and a per-line marker cannot exist (the `.patch` body is regenerated on every export). Threaded through `export`, `re-export`, `re-export --files`, and `lint --per-patch`. Unknown check IDs are a no-op.
|
|
204
212
|
|
|
213
|
+
The optional `tier` field (only `"branding"` recognised) forces the branding threshold tier for the `large-patch-lines` rule regardless of what `filesAffected` looks like. The automatic branding-tier detection already fires when every file is under `browser/branding/` plus a narrow allowlist of branding-registration siblings (`browser/moz.configure`, `browser/confvars.sh`) — covering the canonical Firefox fork shape. Declare `tier: "branding"` only when the patch legitimately also touches a non-allowlisted sibling the auto-detector cannot reach (a fork-specific theme override under `browser/themes/<name>/`, a vendor-specific icon resource, etc.). Precedence is `test > branding > general`: a patch of all-tests always gets the more permissive test-tier thresholds even if it declares `tier: "branding"`. Unknown tier values are rejected at load time rather than silently stripped, so a typo surfaces as a loader error. Prefer `tier` over `lintIgnore: ["large-patch-lines"]` when the patch is legitimately branding-shaped — `tier` keeps the rule running at the correct thresholds (so the warning still surfaces if the patch crosses them); `lintIgnore` drops the rule entirely.
|
|
214
|
+
|
|
205
215
|
If the manifest drifts after an interrupted export or manual edits, `fireforge import` will stop rather then silently applying a stale stack. Use `fireforge doctor --repair-patches-manifest` to rebuild it from disk. Because the rebuild is deterministic, the result will always be consistent with what is actually on the filesystem.
|
|
206
216
|
|
|
207
217
|
</details>
|
|
@@ -213,24 +223,24 @@ If the manifest drifts after an interrupted export or manual edits, `fireforge i
|
|
|
213
223
|
|
|
214
224
|
By default, a standalone `fireforge lint` (no arguments) lints the **aggregate** `git diff HEAD` — i.e. every applied patch summed — with tool-managed branding paths (`browser/branding/<binaryName>/`) excluded. A fresh-setup workspace carries a large generated branding diff that operators did not author directly, and letting it through tripped the patch-size and license-header rules on content that matches the `branding` bucket in `fireforge status`. When the exclusion fires the command prints a one-line note naming the excluded count so the filter is visible. On a repo where `fireforge import` or `fireforge rebase` has just applied the full queue, the patch-size rules (`large-patch-lines`, `large-patch-files`) fire against the sum, which reads as "my queue is broken" when it is really an artefact of aggregation. Use `fireforge lint --per-patch` to rescope the diff to each patch's own `filesAffected`, honouring the patch's own `lintIgnore`. Cross-patch rules (`duplicate-new-file-creation`, `forward-import`) still run once over the whole queue either way. Pass explicit file paths to narrow the scope further — explicit-path mode does lint branding files (the operator's explicit request wins over the branding exclusion); the three modes (aggregate, file-scoped, per-patch) are mutually exclusive.
|
|
215
225
|
|
|
216
|
-
| Check | Scope
|
|
217
|
-
| ------------------------------ |
|
|
218
|
-
| `missing-license-header` | New files (JS/CSS/FTL)
|
|
219
|
-
| `relative-import` | JS/MJS files
|
|
220
|
-
| `token-prefix-violation` | CSS files (with furnace)
|
|
221
|
-
| `raw-color-value` | Introduced CSS color values (allowlist via `patchLint.rawColorAllowlist`)
|
|
222
|
-
| `duplicate-new-file-creation` | Same path created by multiple patches
|
|
223
|
-
| `forward-import` | Patch imports from a later-patch file
|
|
224
|
-
| `missing-jsdoc` | Exports in patch-owned `.sys.mjs`
|
|
225
|
-
| `jsdoc-param-mismatch` | Exports in patch-owned `.sys.mjs`
|
|
226
|
-
| `jsdoc-missing-returns` | Exports in patch-owned `.sys.mjs`
|
|
227
|
-
| `checkjs-type-error` | Patch-owned `.sys.mjs` (opt-in)
|
|
228
|
-
| `missing-modification-comment` | Modified upstream JS/MJS
|
|
229
|
-
| `modified-file-missing-header` | Modified upstream files (JS/CSS/FTL)
|
|
230
|
-
| `file-too-large` | New files (tiered: 500/750/900 general, 1200/1400/1600 test)
|
|
231
|
-
| `observer-topic-naming` | Observer topics with binaryName
|
|
232
|
-
| `large-patch-files` | Patches affecting >5 files
|
|
233
|
-
| `large-patch-lines` | Patch line count (tiered: 800/1500/3000 general, 1500/3000/6000 test)
|
|
226
|
+
| Check | Scope | Severity |
|
|
227
|
+
| ------------------------------ | ----------------------------------------------------------------------------------------------- | ------------------------ |
|
|
228
|
+
| `missing-license-header` | New files (JS/CSS/FTL) | error |
|
|
229
|
+
| `relative-import` | JS/MJS files | error |
|
|
230
|
+
| `token-prefix-violation` | CSS files (with furnace) | error |
|
|
231
|
+
| `raw-color-value` | Introduced CSS color values (allowlist via `patchLint.rawColorAllowlist`) | error |
|
|
232
|
+
| `duplicate-new-file-creation` | Same path created by multiple patches | error |
|
|
233
|
+
| `forward-import` | Patch imports from a later-patch file | error |
|
|
234
|
+
| `missing-jsdoc` | Exports in patch-owned `.sys.mjs` | error |
|
|
235
|
+
| `jsdoc-param-mismatch` | Exports in patch-owned `.sys.mjs` | error |
|
|
236
|
+
| `jsdoc-missing-returns` | Exports in patch-owned `.sys.mjs` | error |
|
|
237
|
+
| `checkjs-type-error` | Patch-owned `.sys.mjs` (opt-in) | error |
|
|
238
|
+
| `missing-modification-comment` | Modified upstream JS/MJS | warning |
|
|
239
|
+
| `modified-file-missing-header` | Modified upstream files (JS/CSS/FTL) | warning |
|
|
240
|
+
| `file-too-large` | New files (tiered: 500/750/900 general, 1200/1400/1600 test) | notice / warning / error |
|
|
241
|
+
| `observer-topic-naming` | Observer topics with binaryName | warning |
|
|
242
|
+
| `large-patch-files` | Patches affecting >5 files | warning |
|
|
243
|
+
| `large-patch-lines` | Patch line count (tiered: 800/1500/3000 general, 1500/3000/6000 test, 3000/8000/20000 branding) | notice / warning / error |
|
|
234
244
|
|
|
235
245
|
**JSDoc validation** uses AST-based analysis (Acorn) to validate exported APIs in patch-owned `.sys.mjs` files. A file is "patch-owned" if it was newly created by the current diff or by an existing patch in the queue. Functions must document every `@param` (names must match) and include `@returns` when the function returns a value. Exported constants and classes require a JSDoc block.
|
|
236
246
|
|
|
@@ -253,17 +263,18 @@ fireforge status --json # machine-readable classified output
|
|
|
253
263
|
|
|
254
264
|
Then fix with the appropriate primitive:
|
|
255
265
|
|
|
256
|
-
| Problem | Fix
|
|
257
|
-
| ---------------------------------------------- |
|
|
258
|
-
| Two patches each creating the same file | `fireforge patch delete <duplicate>` or `fireforge re-export --files`
|
|
259
|
-
| A patch imports from a module in a later patch | `fireforge patch reorder <later> --before <importer>`
|
|
260
|
-
| Wrong patch ordering | `fireforge patch reorder <patch> --to <N>`
|
|
261
|
-
| Ordinal gaps after deletes/splits | `fireforge patch compact`
|
|
262
|
-
| A patch claims files that belong elsewhere | `fireforge re-export --files <subset> <patch>`
|
|
263
|
-
| Manifest references a missing patch file | `fireforge doctor --repair-patches-manifest`
|
|
264
|
-
|
|
|
266
|
+
| Problem | Fix |
|
|
267
|
+
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
268
|
+
| Two patches each creating the same file | `fireforge patch delete <duplicate>` or `fireforge re-export --files` |
|
|
269
|
+
| A patch imports from a module in a later patch | `fireforge patch reorder <later> --before <importer>` |
|
|
270
|
+
| Wrong patch ordering | `fireforge patch reorder <patch> --to <N>` |
|
|
271
|
+
| Ordinal gaps after deletes/splits | `fireforge patch compact` |
|
|
272
|
+
| A patch claims files that belong elsewhere | `fireforge re-export --files <subset> <patch>` |
|
|
273
|
+
| Manifest references a missing patch file | `fireforge doctor --repair-patches-manifest` |
|
|
274
|
+
| Dangling widget / locale registration in patch | Re-run `fireforge export` without `--exclude-furnace` to capture the source files, or revert furnace changes |
|
|
275
|
+
| Unmanaged changes you want to discard | `fireforge discard <file>` or `fireforge reset` |
|
|
265
276
|
|
|
266
|
-
Every destructive command defaults to an interactive confirmation with a change summary. `--dry-run` previews without writing; `--yes` skips the prompt for CI; `--force-unsafe` bypasses structural refusals when you have context the linter cannot see. Do not hand-edit `patches.json` as the file is owned by FireForge.
|
|
277
|
+
Every destructive command defaults to an interactive confirmation with a change summary. `--dry-run` previews without writing; `--yes` skips the prompt for CI; `--force-unsafe` bypasses structural refusals when you have context the linter cannot see. Do not hand-edit `patches.json` as the file is owned by FireForge — `doctor --repair-patches-manifest` reconstructs missing metadata, and `fireforge re-export <filename> --description "<text>"` overwrites recovered entries with operator-supplied metadata through the tool. `fireforge verify` cross-checks every registration hunk in each patch body against the files the queue and engine supply, so a patch that registers a widget / locale without carrying its source surfaces as a `dangling-registration` error rather than slipping through as "Verify clean"; `fireforge export-all --exclude-furnace` refuses up-front when it would produce that shape.
|
|
267
278
|
|
|
268
279
|
## Wiring Custom Code
|
|
269
280
|
|
|
@@ -427,6 +438,8 @@ fireforge patch reorder 003-ui-sidebar.patch --before 001-branding-logo.patch
|
|
|
427
438
|
fireforge patch compact
|
|
428
439
|
```
|
|
429
440
|
|
|
441
|
+
`delete` and `reorder` accept three identifier forms for their target: the ordinal number (`fireforge patch reorder 3 --to 1`), the full filename (`003-ui-sidebar-tweaks.patch`), or the manifest `name` handle the patch was exported with (`ui-sidebar-tweaks`). Anchors passed to `--before` / `--after` accept the same three forms.
|
|
442
|
+
|
|
430
443
|
All subcommands support `--dry-run` and `--yes`.
|
|
431
444
|
|
|
432
445
|
### Additional workflow commands
|
|
@@ -464,6 +477,8 @@ fireforge lint --since main --only-introduced
|
|
|
464
477
|
|
|
465
478
|
The failure message reports how many cumulative errors were suppressed by the flag so a branch that passed only because of the flag still tells the operator what was hidden. Without `--since`, `--only-introduced` is rejected up-front — there is no introduced-vs-cumulative distinction to scope to.
|
|
466
479
|
|
|
480
|
+
Aggregate patch-size findings (`large-patch-files`, `large-patch-lines`) describe the whole diff rather than a single file. Under `--since` + a non-empty diff they tag `[introduced]` (the aggregate IS the introduced work); with an empty diff they tag `[cumulative]` (the finding describes drift accumulated across earlier commits).
|
|
481
|
+
|
|
467
482
|
### Post-build audit and auto-configure
|
|
468
483
|
|
|
469
484
|
`fireforge build` is a transactional step: after a successful mach build it audits the dist bundle against engine-relative paths touched since the last successful build, and warns per file that is packageable-by-convention (`.js`/`.mjs`/`.css`/`.ftl`/`.xhtml`/`app/profile/…`) but has no matching artifact or whose dist mtime is older than the source. Ends every build with a `Packaged: N updated, M stale, K missing, S skipped` summary. The audit is warn-only — it never fails a build that mach reported green.
|
|
@@ -551,6 +566,18 @@ Design tokens imported from the fork's palette are enforced by `tokenPrefix`, bu
|
|
|
551
566
|
|
|
552
567
|
Both rules compose with the existing `tokenPrefix` / `tokenAllowlist` checks and apply to both component validation and patch-stack lint.
|
|
553
568
|
|
|
569
|
+
### Platform variables in Furnace (token coverage)
|
|
570
|
+
|
|
571
|
+
`fireforge token coverage` partitions `var(--…)` usages into three buckets: fork-owned tokens (matching `tokenPrefix`), allowlisted exceptions (explicit `tokenAllowlist` entries, plus platform prefixes), and unknown. Platform prefixes default to `['--moz-']` so upstream Firefox variables in copied baselines (e.g. the CSS that lands under `toolkit/content/widgets/<name>/` after `furnace override <name> -t css-only`) don't drag the fork-owned coverage percentage down.
|
|
572
|
+
|
|
573
|
+
```json
|
|
574
|
+
{
|
|
575
|
+
"platformPrefixes": ["--moz-", "--in-content-"]
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
Set this in `furnace.json` to extend the list (forks with additional platform prefixes) or pass an empty array to restore the pre-0.18 strict contract where `--moz-*` counted as unknown. Allowlisted usages are reported separately and are NOT counted toward the coverage denominator, so a 100% fork-owned project with a copied upstream baseline reads as `100%` instead of `1%`.
|
|
580
|
+
|
|
554
581
|
### Test harness options
|
|
555
582
|
|
|
556
583
|
`fireforge furnace create --with-tests` scaffolds a **browser-chrome mochitest**. Use this when the component renders UI that depends on the tab strip (`openLinkIn` → `URILoadingHelper`, `gBrowser`, etc.).
|
|
@@ -569,9 +596,9 @@ The two flags can be combined — `--with-tests --xpcshell` writes both harnesse
|
|
|
569
596
|
|
|
570
597
|
### xpcshell appdir auto-injection on rebranded forks
|
|
571
598
|
|
|
572
|
-
`fireforge test` auto-resolves and injects `--app-path=<absolute>` into the underlying `mach test` invocation when the nearest `xpcshell.toml` sets `firefox-appdir = "browser"` and the active build's `appname` is anything other than `firefox`. Without this, every `resource:///modules/<name>.sys.mjs` import inside the harness throws `Failed to load resource:///modules/…` because the upstream xpcshell harness reads the appdir override under the appname-keyed manifest field (`<appname>-appdir`) — the literal `firefox-appdir = "browser"` directive is silently ignored on rebranded forks, `appPath` falls back to `xrePath`, and `resource:///` resolves one level above the real app root. The resolver walks each test path to its nearest manifest, reads `mozinfo.json` for the active appname, prefers any `<appname>-appdir` already in the manifest, and otherwise probes `<objDir>/dist/
|
|
599
|
+
`fireforge test` auto-resolves and injects `--app-path=<absolute>` into the underlying `mach test` invocation when the nearest `xpcshell.toml` sets `firefox-appdir = "browser"` and the active build's `appname` is anything other than `firefox`. Without this, every `resource:///modules/<name>.sys.mjs` import inside the harness throws `Failed to load resource:///modules/…` because the upstream xpcshell harness reads the appdir override under the appname-keyed manifest field (`<appname>-appdir`) — the literal `firefox-appdir = "browser"` directive is silently ignored on rebranded forks, `appPath` falls back to `xrePath`, and `resource:///` resolves one level above the real app root. The resolver walks each test path to its nearest manifest, reads `mozinfo.json` for the active appname, prefers any `<appname>-appdir` already in the manifest, and otherwise probes the platform-appropriate layout for the absolute target: on macOS, `<objDir>/dist/<bundle>.app/Contents/Resources/<value>` is preferred over `<objDir>/dist/bin/<value>` (the `dist/bin` symlink on Darwin points at the binaries directory, not the Resources tree where modules live); on other platforms `dist/bin/<value>` is preferred and the macOS bundle layout is the fallback. Operator overrides via `--mach-arg=--app-path=…` always win and skip the resolver silently. Mismatches across multiple test paths and unresolvable manifest values surface as warnings rather than guesses, so triage reaches the underlying cause.
|
|
573
600
|
|
|
574
|
-
The durable fix is to add `<appname>-appdir = "browser"` alongside `firefox-appdir = "browser"` in the manifest — the harness then reads the appname-keyed value directly without auto-injection. The xpcshell appdir hint that fires when the symptom persists despite injection lists this option first.
|
|
601
|
+
The durable fix is to add `<appname>-appdir = "browser"` alongside `firefox-appdir = "browser"` in the manifest — the harness then reads the appname-keyed value directly without auto-injection. This is the most reliable fix on rebranded macOS builds where mach's xpcshell runner binds `-a` to the `.app/Contents/Resources` default and may not honour `--app-path` overrides. The xpcshell appdir hint that fires when the symptom persists despite injection lists this option first.
|
|
575
602
|
|
|
576
603
|
### Smoke-run mode (`fireforge run --smoke-exit`)
|
|
577
604
|
|
|
@@ -127,7 +127,7 @@ export async function buildCommand(projectRoot, options) {
|
|
|
127
127
|
}
|
|
128
128
|
info(''); // Empty line before build output
|
|
129
129
|
const startTime = Date.now();
|
|
130
|
-
let
|
|
130
|
+
let result;
|
|
131
131
|
try {
|
|
132
132
|
// Hold the per-project build lock across the mach invocation so two
|
|
133
133
|
// overlapping `fireforge build` / `fireforge build --ui` commands
|
|
@@ -138,7 +138,7 @@ export async function buildCommand(projectRoot, options) {
|
|
|
138
138
|
// backend — not a clue that a concurrent build was the cause. The
|
|
139
139
|
// lock turns the second invocation's failure into an explicit
|
|
140
140
|
// refusal naming the holder PID.
|
|
141
|
-
|
|
141
|
+
result = await withBuildLock(projectRoot, async () => {
|
|
142
142
|
if (options.ui) {
|
|
143
143
|
return buildUI(paths.engine);
|
|
144
144
|
}
|
|
@@ -152,9 +152,23 @@ export async function buildCommand(projectRoot, options) {
|
|
|
152
152
|
const minutes = Math.floor(duration / 60000);
|
|
153
153
|
const seconds = Math.floor((duration % 60000) / 1000);
|
|
154
154
|
const timeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
155
|
-
if (exitCode !== 0) {
|
|
155
|
+
if (result.exitCode !== 0) {
|
|
156
156
|
error(`Build failed after ${timeStr}`);
|
|
157
|
-
throw new BuildError(`Build failed with exit code ${exitCode}`, options.ui ? 'mach build faster' : 'mach build');
|
|
157
|
+
throw new BuildError(`Build failed with exit code ${result.exitCode}`, options.ui ? 'mach build faster' : 'mach build');
|
|
158
|
+
}
|
|
159
|
+
// Tool-managed branding edits that land on `browser/moz.configure`
|
|
160
|
+
// before the build cause mach's post-build guard to print
|
|
161
|
+
// "config.status is out of date … Be sure to run |mach build|" even
|
|
162
|
+
// though the build itself completed cleanly. 2026-04-21 eval finding:
|
|
163
|
+
// operators read that as "your build is stale" and either rebuilt
|
|
164
|
+
// (wasting ~10 minutes) or doubted the Fireforge "Build completed"
|
|
165
|
+
// footer. Annotate the captured output so the operator knows the
|
|
166
|
+
// warning is expected and not actionable.
|
|
167
|
+
const staleConfigurePattern = /config\.status is out of date/i;
|
|
168
|
+
if (staleConfigurePattern.test(result.stdout) || staleConfigurePattern.test(result.stderr)) {
|
|
169
|
+
info('Note: mach reported "config.status is out of date" after this build. ' +
|
|
170
|
+
'That notice is a known side effect of tool-managed branding edits applied before the build ' +
|
|
171
|
+
'and does not require a rebuild — the Fireforge exit code is authoritative.');
|
|
158
172
|
}
|
|
159
173
|
// Warn-only post-build audit: surfaces silent packaging drops (files
|
|
160
174
|
// edited in engine/ but never registered for packaging) against the
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Furnace manifest sync" doctor check.
|
|
3
|
+
*
|
|
4
|
+
* Surfaces orphaned component directories on disk whose names are missing
|
|
5
|
+
* from `furnace.json`. The motivating eval case is the concurrent-override
|
|
6
|
+
* race (eval 2) where two parallel `furnace override` commands both left
|
|
7
|
+
* their directory on disk but only the second reached
|
|
8
|
+
* `writeFurnaceConfig`. Under `--repair-furnace`, override orphans are
|
|
9
|
+
* re-registered from the `override.json` sidecar the command wrote during
|
|
10
|
+
* the copy phase; custom orphans are listed for the operator to either
|
|
11
|
+
* re-run `furnace create` against or delete manually, because custom
|
|
12
|
+
* components have no similar persisted metadata.
|
|
13
|
+
*
|
|
14
|
+
* Lives in a sibling module to keep `doctor-furnace.ts` under the
|
|
15
|
+
* per-file LOC budget.
|
|
16
|
+
*/
|
|
17
|
+
import type { DoctorCheckDefinition } from './doctor.js';
|
|
18
|
+
export declare const furnaceManifestSyncCheck: DoctorCheckDefinition;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* "Furnace manifest sync" doctor check.
|
|
4
|
+
*
|
|
5
|
+
* Surfaces orphaned component directories on disk whose names are missing
|
|
6
|
+
* from `furnace.json`. The motivating eval case is the concurrent-override
|
|
7
|
+
* race (eval 2) where two parallel `furnace override` commands both left
|
|
8
|
+
* their directory on disk but only the second reached
|
|
9
|
+
* `writeFurnaceConfig`. Under `--repair-furnace`, override orphans are
|
|
10
|
+
* re-registered from the `override.json` sidecar the command wrote during
|
|
11
|
+
* the copy phase; custom orphans are listed for the operator to either
|
|
12
|
+
* re-run `furnace create` against or delete manually, because custom
|
|
13
|
+
* components have no similar persisted metadata.
|
|
14
|
+
*
|
|
15
|
+
* Lives in a sibling module to keep `doctor-furnace.ts` under the
|
|
16
|
+
* per-file LOC budget.
|
|
17
|
+
*/
|
|
18
|
+
import { readdir } from 'node:fs/promises';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { getFurnacePaths, loadFurnaceConfig, writeFurnaceConfig } from '../core/furnace-config.js';
|
|
21
|
+
import { toError } from '../utils/errors.js';
|
|
22
|
+
import { pathExists, readJson } from '../utils/fs.js';
|
|
23
|
+
import { failure, ok, warning } from './doctor.js';
|
|
24
|
+
async function listComponentDirs(dir) {
|
|
25
|
+
if (!(await pathExists(dir)))
|
|
26
|
+
return [];
|
|
27
|
+
try {
|
|
28
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
29
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function recoverOverrideConfig(overrideDir, name) {
|
|
36
|
+
// `furnace override` writes `override.json` alongside the copied files.
|
|
37
|
+
// That sidecar is enough to reconstruct the furnace.json entry lost to
|
|
38
|
+
// a concurrent-write race (eval 2: second override wrote back the
|
|
39
|
+
// outer-snapshot config, dropping the sibling write's addition).
|
|
40
|
+
const sidecarPath = join(overrideDir, name, 'override.json');
|
|
41
|
+
if (!(await pathExists(sidecarPath)))
|
|
42
|
+
return undefined;
|
|
43
|
+
try {
|
|
44
|
+
const raw = await readJson(sidecarPath);
|
|
45
|
+
const type = raw.type;
|
|
46
|
+
const description = raw.description;
|
|
47
|
+
const basePath = raw.basePath;
|
|
48
|
+
const baseVersion = raw.baseVersion;
|
|
49
|
+
if ((type !== 'css-only' && type !== 'full') ||
|
|
50
|
+
typeof description !== 'string' ||
|
|
51
|
+
typeof basePath !== 'string' ||
|
|
52
|
+
typeof baseVersion !== 'string') {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const restored = {
|
|
56
|
+
type: type,
|
|
57
|
+
description,
|
|
58
|
+
basePath,
|
|
59
|
+
baseVersion,
|
|
60
|
+
};
|
|
61
|
+
if (typeof raw.baseCommit === 'string') {
|
|
62
|
+
restored.baseCommit = raw.baseCommit;
|
|
63
|
+
}
|
|
64
|
+
return restored;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function collectOrphans(projectRoot, config) {
|
|
71
|
+
const furnacePaths = getFurnacePaths(projectRoot);
|
|
72
|
+
const overrideDirs = await listComponentDirs(furnacePaths.overridesDir);
|
|
73
|
+
const overrideOrphans = [];
|
|
74
|
+
for (const name of overrideDirs) {
|
|
75
|
+
if (name in config.overrides)
|
|
76
|
+
continue;
|
|
77
|
+
const recoveredConfig = await recoverOverrideConfig(furnacePaths.overridesDir, name);
|
|
78
|
+
overrideOrphans.push({ name, recoveredConfig });
|
|
79
|
+
}
|
|
80
|
+
const customDirs = await listComponentDirs(furnacePaths.customDir);
|
|
81
|
+
const customOrphans = customDirs.filter((name) => !(name in config.custom));
|
|
82
|
+
return { overrides: overrideOrphans, customNames: customOrphans };
|
|
83
|
+
}
|
|
84
|
+
function formatOrphanSummary(orphans) {
|
|
85
|
+
const overrideCount = orphans.overrides.length;
|
|
86
|
+
const customCount = orphans.customNames.length;
|
|
87
|
+
const overrideLabel = overrideCount > 0
|
|
88
|
+
? `${overrideCount} override${overrideCount === 1 ? '' : 's'} ` +
|
|
89
|
+
`(${orphans.overrides.map((o) => o.name).join(', ')})`
|
|
90
|
+
: '';
|
|
91
|
+
const customLabel = customCount > 0
|
|
92
|
+
? `${customCount} custom ${customCount === 1 ? 'directory' : 'directories'} ` +
|
|
93
|
+
`(${orphans.customNames.join(', ')})`
|
|
94
|
+
: '';
|
|
95
|
+
const description = [overrideLabel, customLabel].filter(Boolean).join(' and ');
|
|
96
|
+
return `Found orphaned component ${overrideCount + customCount === 1 ? 'directory' : 'directories'} on disk: ${description}. furnace.json does not list these — a previous mutation may have lost a concurrent write.`;
|
|
97
|
+
}
|
|
98
|
+
async function repairOrphanOverrides(projectRoot, orphans) {
|
|
99
|
+
const restored = [];
|
|
100
|
+
const unrecoverable = [];
|
|
101
|
+
if (orphans.length === 0)
|
|
102
|
+
return { restored, unrecoverable };
|
|
103
|
+
const freshConfig = await loadFurnaceConfig(projectRoot);
|
|
104
|
+
for (const orphan of orphans) {
|
|
105
|
+
if (orphan.recoveredConfig) {
|
|
106
|
+
freshConfig.overrides[orphan.name] = orphan.recoveredConfig;
|
|
107
|
+
restored.push(orphan.name);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
unrecoverable.push(orphan.name);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (restored.length > 0) {
|
|
114
|
+
try {
|
|
115
|
+
await writeFurnaceConfig(projectRoot, freshConfig);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
return { restored: [], unrecoverable, writeError: toError(err).message };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { restored, unrecoverable };
|
|
122
|
+
}
|
|
123
|
+
export const furnaceManifestSyncCheck = {
|
|
124
|
+
name: 'Furnace manifest sync',
|
|
125
|
+
dependsOn: ['Furnace configuration'],
|
|
126
|
+
skipIf: (ctx) => !ctx.furnaceConfigExists || !ctx.furnaceConfig,
|
|
127
|
+
run: async (ctx) => {
|
|
128
|
+
const config = ctx.furnaceConfig;
|
|
129
|
+
if (!config)
|
|
130
|
+
return [];
|
|
131
|
+
const orphans = await collectOrphans(ctx.projectRoot, config);
|
|
132
|
+
const overrideCount = orphans.overrides.length;
|
|
133
|
+
const customCount = orphans.customNames.length;
|
|
134
|
+
if (overrideCount === 0 && customCount === 0) {
|
|
135
|
+
return ok('Furnace manifest sync');
|
|
136
|
+
}
|
|
137
|
+
const summary = formatOrphanSummary(orphans);
|
|
138
|
+
if (!ctx.options.repairFurnace) {
|
|
139
|
+
return warning('Furnace manifest sync', summary, 'Run "fireforge doctor --repair-furnace" to re-register override orphans from their override.json sidecars (custom orphans are listed for manual follow-up).');
|
|
140
|
+
}
|
|
141
|
+
const repairResult = await repairOrphanOverrides(ctx.projectRoot, orphans.overrides);
|
|
142
|
+
if (repairResult.writeError) {
|
|
143
|
+
return failure('Furnace manifest sync', `Repair failed while writing furnace.json: ${repairResult.writeError}`, 'Fix the underlying filesystem error and retry the doctor command.');
|
|
144
|
+
}
|
|
145
|
+
const { restored, unrecoverable } = repairResult;
|
|
146
|
+
const restoreDetail = restored.length > 0
|
|
147
|
+
? `Re-registered ${restored.length} override${restored.length === 1 ? '' : 's'} (${restored.join(', ')}) from their override.json sidecars.`
|
|
148
|
+
: '';
|
|
149
|
+
const unrecoverableDetail = unrecoverable.length > 0
|
|
150
|
+
? ` Could not recover ${unrecoverable.length} override${unrecoverable.length === 1 ? '' : 's'} without a valid override.json (${unrecoverable.join(', ')}) — delete components/overrides/<name> or re-run "fireforge furnace override" to restore the entry.`
|
|
151
|
+
: '';
|
|
152
|
+
const customDetail = customCount > 0
|
|
153
|
+
? ` ${customCount} custom ${customCount === 1 ? 'directory requires' : 'directories require'} manual action: re-run "fireforge furnace create" or delete components/custom/<name>/ to reconcile.`
|
|
154
|
+
: '';
|
|
155
|
+
return warning('Furnace manifest sync', `${restoreDetail}${unrecoverableDetail}${customDetail}`.trim() ||
|
|
156
|
+
'Nothing to repair (orphans surfaced but all were already recoverable).');
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
//# sourceMappingURL=doctor-furnace-manifest-sync.js.map
|
|
@@ -10,6 +10,7 @@ import { validateAllComponents } from '../core/furnace-validate.js';
|
|
|
10
10
|
import { toError } from '../utils/errors.js';
|
|
11
11
|
import { pathExists } from '../utils/fs.js';
|
|
12
12
|
import { failure, ok, warning } from './doctor.js';
|
|
13
|
+
import { furnaceManifestSyncCheck } from './doctor-furnace-manifest-sync.js';
|
|
13
14
|
const ENGINE_REPAIRABLE_OPERATIONS = [
|
|
14
15
|
'preview-teardown',
|
|
15
16
|
'apply-rollback',
|
|
@@ -507,5 +508,6 @@ export const FURNACE_DOCTOR_CHECKS = [
|
|
|
507
508
|
furnaceStaleLockCheck,
|
|
508
509
|
furnaceEngineStateCheck,
|
|
509
510
|
furnaceComponentValidationCheck,
|
|
511
|
+
furnaceManifestSyncCheck,
|
|
510
512
|
];
|
|
511
513
|
//# sourceMappingURL=doctor-furnace.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ownership-aware working-tree check for `fireforge doctor`.
|
|
3
|
+
*
|
|
4
|
+
* Partitions engine-tree dirtiness into `branding`, `patch-backed`,
|
|
5
|
+
* `furnace`, `conflict`, and `unmanaged` buckets, and only warns on the
|
|
6
|
+
* last two — everything else is tool-managed state that the operator
|
|
7
|
+
* did not author directly.
|
|
8
|
+
*
|
|
9
|
+
* Split out of `doctor.ts` so that file stays under the per-file LOC
|
|
10
|
+
* budget; see the call site in `runEngineGitChecks`.
|
|
11
|
+
*/
|
|
12
|
+
import type { DoctorCheck } from '../types/commands/index.js';
|
|
13
|
+
import type { DoctorCheckContext } from './doctor.js';
|
|
14
|
+
/**
|
|
15
|
+
* Inspects the engine working tree and returns a single
|
|
16
|
+
* `DoctorCheck`. Ownership-aware: patch-backed / branding / furnace
|
|
17
|
+
* rows are reported as OK with an ownership summary; unmanaged drift
|
|
18
|
+
* warns; cross-patch conflicts warn loudly with a pointer at
|
|
19
|
+
* `fireforge status --ownership` + `fireforge verify`.
|
|
20
|
+
*
|
|
21
|
+
* Before 0.16.1 this check warned on every dirty row regardless of
|
|
22
|
+
* ownership and told the operator to export/discard/reset — advice
|
|
23
|
+
* that was actively destructive on a patch-backed import (eval
|
|
24
|
+
* Finding: a correctly imported 126-file patch stack was reported as
|
|
25
|
+
* unhealthy and the suggested fix would have dropped the entire
|
|
26
|
+
* import). Returns `undefined` when the worktree is clean so the
|
|
27
|
+
* caller can emit its own ok() row.
|
|
28
|
+
*/
|
|
29
|
+
export declare function inspectEngineWorkingTree(ctx: DoctorCheckContext): Promise<DoctorCheck | undefined>;
|