@hominis/fireforge 0.16.5 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/README.md +46 -24
- package/dist/src/commands/build.js +33 -10
- package/dist/src/commands/config.js +32 -20
- 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 +23 -12
- package/dist/src/commands/export-all.js +11 -3
- package/dist/src/commands/export-shared.d.ts +7 -1
- package/dist/src/commands/export-shared.js +21 -3
- package/dist/src/commands/furnace/chrome-doc-tests.js +9 -2
- package/dist/src/commands/furnace/create-templates.d.ts +11 -0
- package/dist/src/commands/furnace/create-templates.js +11 -2
- package/dist/src/commands/furnace/init.js +97 -9
- package/dist/src/commands/furnace/override.js +23 -13
- package/dist/src/commands/furnace/remove.js +8 -0
- package/dist/src/commands/furnace/rename.js +133 -4
- package/dist/src/commands/lint.js +70 -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/register.js +11 -0
- package/dist/src/commands/resolve.d.ts +25 -1
- package/dist/src/commands/resolve.js +25 -15
- package/dist/src/commands/status.js +100 -122
- package/dist/src/commands/test.js +68 -14
- package/dist/src/commands/token-coverage.js +10 -3
- package/dist/src/commands/wire.js +50 -8
- package/dist/src/core/browser-wire.js +21 -4
- package/dist/src/core/build-audit.js +10 -0
- package/dist/src/core/config.d.ts +33 -0
- package/dist/src/core/config.js +43 -0
- package/dist/src/core/furnace-config.d.ts +23 -2
- package/dist/src/core/furnace-config.js +26 -3
- package/dist/src/core/git-diff.js +21 -2
- package/dist/src/core/mach.d.ts +43 -6
- package/dist/src/core/mach.js +57 -7
- 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-port.d.ts +50 -0
- package/dist/src/core/marionette-port.js +215 -0
- package/dist/src/core/patch-lint.d.ts +47 -2
- package/dist/src/core/patch-lint.js +89 -14
- package/dist/src/core/patch-manifest-consistency.d.ts +21 -1
- package/dist/src/core/patch-manifest-consistency.js +31 -3
- 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/status-classify.d.ts +54 -0
- package/dist/src/core/status-classify.js +134 -0
- package/dist/src/core/token-coverage.js +24 -0
- package/dist/src/core/token-dark-mode.d.ts +49 -0
- package/dist/src/core/token-dark-mode.js +182 -0
- package/dist/src/core/token-manager.js +17 -33
- package/dist/src/core/wire-destroy.d.ts +7 -3
- package/dist/src/core/wire-destroy.js +11 -6
- package/dist/src/core/wire-dom-fragment.d.ts +17 -0
- package/dist/src/core/wire-dom-fragment.js +40 -0
- 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/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,5 +1,61 @@
|
|
|
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
|
+
|
|
34
|
+
### Internal
|
|
35
|
+
|
|
36
|
+
- **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.
|
|
37
|
+
- **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.
|
|
38
|
+
|
|
39
|
+
## 0.17.0
|
|
40
|
+
|
|
41
|
+
### Eval-driven hardening
|
|
42
|
+
|
|
43
|
+
- **`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`.
|
|
44
|
+
- **`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.
|
|
45
|
+
- **`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.
|
|
46
|
+
- **`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.
|
|
47
|
+
- **`fireforge furnace create --with-tests` — scaffolded mochikit test runs to completion.** `generateMochikitTestContent` in `src/commands/furnace/create-templates.ts` previously emitted `SimpleTest.waitForExplicitFinish()` alongside an `add_task(...)` and no explicit `SimpleTest.finish()`. The test harness waits forever: `waitForExplicitFinish()` tells the harness not to finish on script end, and the `add_task`-managed finish never fires because the scaffold has no explicit `finish()` body. The 2026-04-21 eval's `fireforge test --headless toolkit/content/tests/widgets/test_ff-chip-row.html` hung until the operator SIGINT'd it. The fix removes the `waitForExplicitFinish()` call — `add_task` already calls `SimpleTest.finish()` when every queued task resolves, matching the convention upstream widget tests (`toolkit/content/tests/widgets/test_moz-button.html` and siblings) use. A regression test in `src/commands/furnace/__tests__/create-mochikit.test.ts` pins the contract that the generated content must not contain `waitForExplicitFinish`.
|
|
48
|
+
- **`fireforge furnace chrome-doc create --with-tests` — packaging test probes the correct packaged CSS path.** `generateChromeDocPackagingTest` in `src/commands/furnace/chrome-doc-tests.ts` probed `<AppDir>/chrome/browser/skin/classic/browser/<name>-chrome.css`, but the `jar.inc.mn` entry in `chrome-doc-templates.ts:chromeDocJarIncMnCssEntry` registers the file at `content/browser/<name>-chrome.css` — so the packaged location is `.../chrome/browser/content/browser/<name>-chrome.css`, not the skin layout. The 2026-04-21 eval's `fireforge test --build` against a scaffolded chrome-doc failed with a spurious "missing" assertion even though the file was correctly packaged. Both the primary probe and the macOS `.app`-bundle fallback now name the `content/browser/` layout, and a negative-match test guards against anyone pinning it back to `skin/classic/browser/` in a future refactor.
|
|
49
|
+
- **`fireforge status --json` — cross-patch ownership conflicts surface as `conflict` with `claimedBy`.** `classifyFiles` in `src/commands/status.ts` tracked patch ownership as a `Set<string>` and collapsed multi-owner paths into the single-owner branch, where the content-compare then routed them into `unmanaged` when the engine content didn't match any single patch's expected result. The 2026-04-21 eval's `status --json` run reported `"classification": "unmanaged"` on two files that `status --ownership` correctly labelled `CONFLICT` — scripts built on the JSON view mis-diagnosed the drift and could have taken the wrong corrective action. The classifier now builds a `Map<string, string[]>` (filename → claiming patch filenames), emits `classification: "conflict"` for entries claimed by two or more patches, and attaches `claimedBy: string[]` to those entries so machine consumers can read the ownership set directly. The human default-mode output now surfaces a `Cross-patch ownership conflicts` section at the top pointing at `status --ownership` and `re-export --files` for recovery. Single-claim entries stay byte-identical to the pre-0.16.0 JSON shape (no unconditional `claimedBy` field) so parsers unaware of the new classification continue to work.
|
|
50
|
+
- **`fireforge furnace init --ftl-base-path` — rejects file-shaped values up-front.** `validateFtlBasePath` in `src/commands/furnace/init.ts` only enforced syntactic safety (no absolute paths, no `..`, no null bytes). A plausible-but-invalid value like `browser/forgefresh.ftl` passed the gate, and the next localized `furnace create` scaffolded a component whose generated `.mjs` referenced `insertFTLIfNeeded("<name>.ftl")` while `furnace.json` never got the component entry — the scaffold was orphaned, every follow-up command failed with "not found in furnace.json", and the 2026-04-21 eval recorded this as Finding #5 (apparent non-registration) whose real root cause was Finding #6 (bad `ftlBasePath`). The validator now refuses any value whose basename carries `.ftl`, `.properties`, or `.dtd` with a message that names the file and points at a locale directory (`toolkit/locales/en-US/toolkit/global` or `browser/locales/en-US/browser`). When the engine directory is present on disk, it additionally probes whether the resolved path is a directory and warns (non-blocking) if the path does not yet exist — a fresh project that has not `fireforge download`-ed yet is legitimate.
|
|
51
|
+
- **`fireforge furnace init` — defaults `tokenPrefix` from `fireforge.json`'s `binaryName`.** `createDefaultFurnaceConfig` previously accepted no arguments and omitted `tokenPrefix` entirely, so every fresh project that ran `furnace init` → `token add` → `token coverage` got `0 tokens` and "all unknown" reports until the operator discovered the missing key and hand-edited `furnace.json`. The helper now accepts `{ binaryName }` and, when passed, seeds `tokenPrefix: \`--${binaryName}-\``so the coverage scan has a prefix to key off immediately.`furnaceInitCommand`best-effort-loads`fireforge.json`and threads the binaryName through — a project that initialises Furnace before`fireforge setup`completes still gets a valid prefix-less default (and`token coverage` continues to warn when invoked without a prefix set).
|
|
52
|
+
- **`fireforge build` + `fireforge build --ui` — per-project build lock prevents overlap.** A second `fireforge build --ui` launched while a full `fireforge build` was still running against the same engine tree raced the `obj-*` directory and failed immediately with `No rule to make target 'XUL'` — mach's downstream consequence of an incomplete backend, not a clue that the overlap was the root cause. A new `withBuildLock(projectRoot, operation)` in `src/core/mach.ts` backs onto `withFileLock` at `.fireforge-build.lock` beside the project root; `buildCommand` in `src/commands/build.ts` wraps both `build()` and `buildUI()` call paths in the lock. The refusal message names the holder PID and points at the stale-lock recovery path. Timeouts are bumped to 24h (a slow full build legitimately exceeds the default 30s) but the lock releases on process exit or via PID-alive stale recovery so a crashed build cannot permanently wedge the next invocation. A dedicated integration test in `src/core/__tests__/build-lock.integration.test.ts` pins the serialisation, throw-propagation, and stale-recovery contracts.
|
|
53
|
+
- **`fireforge wire --dry-run` — insertion-point probe runs in both modes.** The dry-run branch previously skipped the `pathExists(join(paths.engine, domTargetPath))` check that the real-run branch ran, and even with existence confirmed the real run could still throw `Could not find insertion point in chrome document` from deep inside `addDomFragment` when the resolved chrome doc offered neither `#include browser-sets.inc` nor `<html:body>`. The 2026-04-21 eval's `wire ... --dom ... --dry-run` previewed a plausible plan targeting `tokenHostDocuments[0]`, then the same command without `--dry-run` failed against a `furnace chrome-doc create`-scaffolded document that lacked both anchors. A new `probeDomFragmentInsertionPoint` helper in `src/core/wire-dom-fragment.ts` reads the chrome doc and runs the same tokenised + legacy insertion-point scan the real run uses; `wireCommand` now calls it in both modes and surfaces the `Could not find insertion point` error before printing the plan. The dry-run preview now refuses the same cases the real run would refuse, before any operator commits to executing.
|
|
54
|
+
- **`fireforge resolve` — `--yes` escape hatch + clearer two-step messaging.** The command refused any non-interactive invocation even after a CI-assisted manual merge was complete, because the TTY guard ran unconditionally — scripted recovery flows could complete the merge but could not then record the refreshed patch body. A new `--yes` / `-y` flag in `src/commands/resolve.ts` skips the interactive `confirm(...)` prompt and passes through in non-interactive mode; the unconditional TTY refusal fires only when the flag is absent. The command description changes from `Update a broken patch with manual fixes and continue` to `Update a broken patch with manual fixes (then run "fireforge import" to resume the queue)`, and the post-success info line now names the second-step command explicitly — the old copy implied a one-step flow where operators sometimes believed resolve continued the queue itself. Help snapshot under `src/__tests__/__snapshots__/help.test.ts.snap` is refreshed to match; resolve tests cover both the `--yes`-in-non-TTY path and the continuation messaging.
|
|
55
|
+
- **`fireforge test` — marionette port probe catches stale browsers before mach launches.** An interrupted `fireforge test --headless` run can leave a `<binaryName> -marionette` child listening on port `2828` with parent PID `1`. The next `fireforge test` run — potentially in a sibling FireForge project — fails immediately with a mach Marionette bind error that points nowhere near the real cause, and the generic "delete obj-\* and rebuild" guidance wastes operator time. A new `probeMarionettePort` / `assertMarionettePortAvailable` pair in `src/core/marionette-port.ts` runs `lsof -i tcp:2828 -sTCP:LISTEN` (POSIX) or `Get-NetTCPConnection` (Windows) before every test launch; when the holder's basename or command line identifies a Firefox-family browser (including `binaryName` from `fireforge.json` for branded forks), the probe raises a targeted `GeneralError` naming the PID and the exact `kill` command. Unrelated listeners produce a softer "this is not a FireForge-launched browser" error so the operator can tell the two cases apart. The probe is best-effort: missing `lsof`/PowerShell falls back to `{ inUse: false }` rather than failing the test run itself.
|
|
56
|
+
- **`fireforge lint` (default) — aggregate-mode skips tool-managed branding.** `resolveLintDiff` in `src/commands/lint.ts` passed the full `getAllDiff(engineDir)` to `lintExportedPatch` when no file list was supplied, so a fresh-setup workspace with 63 modified branding files fired `large-patch-lines`, `large-patch-files`, and `missing-license-header` on tool-managed content the operator never authored. The 2026-04-21 eval's first `fireforge lint` on a newly-built `fresh/` tree failed with blocking patch-lint errors despite the project being minimal-customisation. The aggregate-mode branch now loads `fireforge.json`'s `binaryName`, partitions the dirty tree with `isBrandingManagedPath`, and passes only the non-branding paths into `getDiffForFilesAgainstHead`. The operator sees a one-line `info()` naming the excluded count so the exclusion is visible, not silent. Explicit-path mode (`fireforge lint <path>`) preserves the previous behaviour — passing a branding path explicitly still lints it, so operators who need to audit generated branding content can do so.
|
|
57
|
+
- **`fireforge doctor --repair-patches-manifest` — names every reconstructed entry.** `rebuildPatchesManifest` previously returned only the rebuilt manifest, silently overwriting `description` / `createdAt` with generic fallback values when the existing entry was missing. FireForge patch files do not carry header metadata that could carry human-written descriptions forward, so full fidelity is impossible — but at least visibility is. The helper now returns `{ manifest, recoveredFilenames }` in `src/core/patch-manifest-consistency.ts`; the doctor repair path prints a per-filename `warn(...)` telling the operator exactly which manifest entry was reconstructed from generic defaults and suggesting they edit `patches.json` if they have the original description backed up. The summary row count now reports both the total patches rebuilt and the subset that needed reconstruction.
|
|
58
|
+
|
|
3
59
|
## 0.16.0
|
|
4
60
|
|
|
5
61
|
### UX, correctness, and consistency
|
package/README.md
CHANGED
|
@@ -174,7 +174,7 @@ When `fireforge import` fails on a patch, fix the `.rej` files in `engine/`, the
|
|
|
174
174
|
fireforge resolve
|
|
175
175
|
```
|
|
176
176
|
|
|
177
|
-
This re-exports the fixed patch and
|
|
177
|
+
This re-exports the fixed patch and clears the conflict state. The command is deliberately a single-patch refresh — to continue applying the remainder of the queue, run `fireforge import` afterwards. For scripted or CI-driven recovery, pass `--yes` (or `-y`) to skip the interactive "are you done?" prompt; the flag is the explicit opt-in for non-interactive use once the manual merge is complete.
|
|
178
178
|
|
|
179
179
|
<details>
|
|
180
180
|
<summary>Patch manifest format</summary>
|
|
@@ -193,8 +193,12 @@ This re-exports the fixed patch and continues applying the remaining stack.
|
|
|
193
193
|
"description": "Replaces default Firefox branding with custom logo",
|
|
194
194
|
"createdAt": "2025-01-15T10:30:00Z",
|
|
195
195
|
"sourceEsrVersion": "140.9.0esr",
|
|
196
|
-
"filesAffected": [
|
|
197
|
-
|
|
196
|
+
"filesAffected": [
|
|
197
|
+
"browser/branding/official/logo.png",
|
|
198
|
+
"browser/themes/custom-shared/tokens.css"
|
|
199
|
+
],
|
|
200
|
+
"lintIgnore": ["large-patch-lines", "large-patch-files"],
|
|
201
|
+
"tier": "branding"
|
|
198
202
|
}
|
|
199
203
|
]
|
|
200
204
|
}
|
|
@@ -202,6 +206,8 @@ This re-exports the fixed patch and continues applying the remaining stack.
|
|
|
202
206
|
|
|
203
207
|
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
208
|
|
|
209
|
+
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.
|
|
210
|
+
|
|
205
211
|
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
212
|
|
|
207
213
|
</details>
|
|
@@ -211,26 +217,26 @@ If the manifest drifts after an interrupted export or manual edits, `fireforge i
|
|
|
211
217
|
|
|
212
218
|
`fireforge lint` runs automatically during export, export-all and re-export. Use `--skip-lint` to downgrade errors to warnings. Errors block the export; warnings are printed but do not block.
|
|
213
219
|
|
|
214
|
-
By default, a standalone `fireforge lint` (no arguments) lints the **aggregate** `git diff HEAD` — i.e. every applied patch summed. 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; the three modes (aggregate, file-scoped, per-patch) are mutually exclusive.
|
|
215
|
-
|
|
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)
|
|
220
|
+
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.
|
|
221
|
+
|
|
222
|
+
| Check | Scope | Severity |
|
|
223
|
+
| ------------------------------ | ----------------------------------------------------------------------------------------------- | ------------------------ |
|
|
224
|
+
| `missing-license-header` | New files (JS/CSS/FTL) | error |
|
|
225
|
+
| `relative-import` | JS/MJS files | error |
|
|
226
|
+
| `token-prefix-violation` | CSS files (with furnace) | error |
|
|
227
|
+
| `raw-color-value` | Introduced CSS color values (allowlist via `patchLint.rawColorAllowlist`) | error |
|
|
228
|
+
| `duplicate-new-file-creation` | Same path created by multiple patches | error |
|
|
229
|
+
| `forward-import` | Patch imports from a later-patch file | error |
|
|
230
|
+
| `missing-jsdoc` | Exports in patch-owned `.sys.mjs` | error |
|
|
231
|
+
| `jsdoc-param-mismatch` | Exports in patch-owned `.sys.mjs` | error |
|
|
232
|
+
| `jsdoc-missing-returns` | Exports in patch-owned `.sys.mjs` | error |
|
|
233
|
+
| `checkjs-type-error` | Patch-owned `.sys.mjs` (opt-in) | error |
|
|
234
|
+
| `missing-modification-comment` | Modified upstream JS/MJS | warning |
|
|
235
|
+
| `modified-file-missing-header` | Modified upstream files (JS/CSS/FTL) | warning |
|
|
236
|
+
| `file-too-large` | New files (tiered: 500/750/900 general, 1200/1400/1600 test) | notice / warning / error |
|
|
237
|
+
| `observer-topic-naming` | Observer topics with binaryName | warning |
|
|
238
|
+
| `large-patch-files` | Patches affecting >5 files | warning |
|
|
239
|
+
| `large-patch-lines` | Patch line count (tiered: 800/1500/3000 general, 1500/3000/6000 test, 3000/8000/20000 branding) | notice / warning / error |
|
|
234
240
|
|
|
235
241
|
**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
242
|
|
|
@@ -409,6 +415,8 @@ fireforge config firefox.version 145.0.0esr
|
|
|
409
415
|
fireforge config customKey "value" --force
|
|
410
416
|
```
|
|
411
417
|
|
|
418
|
+
Writes are serialised behind a sidecar lock — two concurrent `fireforge config` invocations against the same `fireforge.json` (for example, parallel automation steps) queue instead of racing the read-modify-write. The lock is released automatically on process exit; stale locks from a crashed earlier command are reclaimed on the next invocation via the PID-alive probe.
|
|
419
|
+
|
|
412
420
|
### Patch queue management
|
|
413
421
|
|
|
414
422
|
```bash
|
|
@@ -425,6 +433,8 @@ fireforge patch reorder 003-ui-sidebar.patch --before 001-branding-logo.patch
|
|
|
425
433
|
fireforge patch compact
|
|
426
434
|
```
|
|
427
435
|
|
|
436
|
+
`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.
|
|
437
|
+
|
|
428
438
|
All subcommands support `--dry-run` and `--yes`.
|
|
429
439
|
|
|
430
440
|
### Additional workflow commands
|
|
@@ -440,7 +450,7 @@ fireforge watch
|
|
|
440
450
|
fireforge token add --category 'Colors — General' -- --my-color 'light-dark(#fff, #000)'
|
|
441
451
|
```
|
|
442
452
|
|
|
443
|
-
Tokens live in the Furnace-managed tokens CSS file (`engine/browser/themes/shared/<binaryName>-tokens.css`), scaffolded by `fireforge furnace init` alongside `furnace.json`. The scaffold seeds a default set of categories (`Colors — General`, `Colors — Canvas`, `Colors — Experiment`, `Spacing`); add a category by hand as a `/* = My Category = */` comment inside the `:root { … }` block if you need another. `fireforge furnace init` also registers the tokens CSS path in `patchLint.rawColorAllowlist` so raw color literals inside it are not flagged by `fireforge lint
|
|
453
|
+
Tokens live in the Furnace-managed tokens CSS file (`engine/browser/themes/shared/<binaryName>-tokens.css`), scaffolded by `fireforge furnace init` alongside `furnace.json`. The scaffold seeds a default set of categories (`Colors — General`, `Colors — Canvas`, `Colors — Experiment`, `Spacing`); add a category by hand as a `/* = My Category = */` comment inside the `:root { … }` block if you need another. `fireforge furnace init` also registers the tokens CSS path in `patchLint.rawColorAllowlist` so raw color literals inside it are not flagged by `fireforge lint`, and derives `tokenPrefix: --<binaryName>-` from `fireforge.json`'s `binaryName` so `fireforge token coverage` has a prefix to key off on the very first run. Projects that prefer a different prefix can override it in `furnace.json` after init.
|
|
444
454
|
|
|
445
455
|
### Diff-scoped lint (`lint --since`)
|
|
446
456
|
|
|
@@ -549,6 +559,18 @@ Design tokens imported from the fork's palette are enforced by `tokenPrefix`, bu
|
|
|
549
559
|
|
|
550
560
|
Both rules compose with the existing `tokenPrefix` / `tokenAllowlist` checks and apply to both component validation and patch-stack lint.
|
|
551
561
|
|
|
562
|
+
### Platform variables in Furnace (token coverage)
|
|
563
|
+
|
|
564
|
+
`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.
|
|
565
|
+
|
|
566
|
+
```json
|
|
567
|
+
{
|
|
568
|
+
"platformPrefixes": ["--moz-", "--in-content-"]
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
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%`.
|
|
573
|
+
|
|
552
574
|
### Test harness options
|
|
553
575
|
|
|
554
576
|
`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.).
|
|
@@ -5,7 +5,7 @@ import { auditBuildArtifacts } from '../core/build-audit.js';
|
|
|
5
5
|
import { readBuildBaseline, writeBuildBaseline } from '../core/build-baseline.js';
|
|
6
6
|
import { prepareBuildEnvironment } from '../core/build-prepare.js';
|
|
7
7
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
8
|
-
import { attemptMozinfoRewrite, build, buildArtifactMismatchMessage, buildUI, hasBuildArtifacts, runMach, } from '../core/mach.js';
|
|
8
|
+
import { attemptMozinfoRewrite, build, buildArtifactMismatchMessage, buildUI, hasBuildArtifacts, runMach, withBuildLock, } from '../core/mach.js';
|
|
9
9
|
import { GeneralError } from '../errors/base.js';
|
|
10
10
|
import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
11
11
|
import { toError } from '../utils/errors.js';
|
|
@@ -127,14 +127,23 @@ 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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
132
|
+
// Hold the per-project build lock across the mach invocation so two
|
|
133
|
+
// overlapping `fireforge build` / `fireforge build --ui` commands
|
|
134
|
+
// against the same engine tree serialise instead of racing through
|
|
135
|
+
// the same obj-*. 2026-04-21 eval: a `build --ui` launched during
|
|
136
|
+
// an in-progress full build hit `No rule to make target 'XUL'` in
|
|
137
|
+
// mach, which is the downstream consequence of an incomplete
|
|
138
|
+
// backend — not a clue that a concurrent build was the cause. The
|
|
139
|
+
// lock turns the second invocation's failure into an explicit
|
|
140
|
+
// refusal naming the holder PID.
|
|
141
|
+
result = await withBuildLock(projectRoot, async () => {
|
|
142
|
+
if (options.ui) {
|
|
143
|
+
return buildUI(paths.engine);
|
|
144
|
+
}
|
|
145
|
+
return build(paths.engine, jobs);
|
|
146
|
+
});
|
|
138
147
|
}
|
|
139
148
|
catch (error) {
|
|
140
149
|
throw new BuildError('Build process failed to start', options.ui ? 'mach build faster' : 'mach build', error instanceof Error ? error : undefined);
|
|
@@ -143,9 +152,23 @@ export async function buildCommand(projectRoot, options) {
|
|
|
143
152
|
const minutes = Math.floor(duration / 60000);
|
|
144
153
|
const seconds = Math.floor((duration % 60000) / 1000);
|
|
145
154
|
const timeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
146
|
-
if (exitCode !== 0) {
|
|
155
|
+
if (result.exitCode !== 0) {
|
|
147
156
|
error(`Build failed after ${timeStr}`);
|
|
148
|
-
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.');
|
|
149
172
|
}
|
|
150
173
|
// Warn-only post-build audit: surfaces silent packaging drops (files
|
|
151
174
|
// edited in engine/ but never registered for packaging) against the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { configExists, loadConfig, loadRawConfigDocument, mutateConfig, SUPPORTED_CONFIG_PATHS, SUPPORTED_CONFIG_ROOT_KEYS, writeConfig, writeConfigDocument, } from '../core/config.js';
|
|
1
|
+
import { configExists, loadConfig, loadRawConfigDocument, mutateConfig, SUPPORTED_CONFIG_PATHS, SUPPORTED_CONFIG_ROOT_KEYS, withConfigFileLock, writeConfig, writeConfigDocument, } from '../core/config.js';
|
|
2
2
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
3
3
|
import { toError } from '../utils/errors.js';
|
|
4
4
|
import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
@@ -112,25 +112,37 @@ export async function configCommand(projectRoot, key, value, options = {}) {
|
|
|
112
112
|
const parsedValue = parseValue(value, key);
|
|
113
113
|
const keyIsKnown = SUPPORTED_CONFIG_PATHS.includes(key);
|
|
114
114
|
try {
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
115
|
+
// Serialise the read → mutate → write round-trip behind the sidecar
|
|
116
|
+
// config lock so two concurrent `fireforge config` invocations can't
|
|
117
|
+
// each read the pre-state, mutate their own copy, and clobber each
|
|
118
|
+
// other on write. Before the lock, the 2026-04-21 eval reproduced
|
|
119
|
+
// silent data loss with two parallel `fireforge config <key>
|
|
120
|
+
// <value>` commands writing different keys: both exited 0, one key
|
|
121
|
+
// survived, the other vanished. Atomic file writes (temp + rename)
|
|
122
|
+
// were never enough on their own — the lost update happens before
|
|
123
|
+
// the rename, inside the read-modify step. Readers stay lock-free
|
|
124
|
+
// (see `withConfigFileLock` docstring).
|
|
125
|
+
await withConfigFileLock(projectRoot, async () => {
|
|
126
|
+
// `--force` is intended as an escape hatch for *unknown* keys; it
|
|
127
|
+
// should not also let the user write a structurally invalid value
|
|
128
|
+
// for a *known* key. Apply strict validation whenever the key is
|
|
129
|
+
// listed in SUPPORTED_CONFIG_PATHS, regardless of --force, and only
|
|
130
|
+
// skip validation for genuinely unknown key paths.
|
|
131
|
+
if (options.force && !keyIsKnown) {
|
|
132
|
+
// Seed mutation from the raw on-disk document so previously-forced
|
|
133
|
+
// keys (which `validateConfig` would strip) survive the round-trip.
|
|
134
|
+
// Without this, writing a second --force key would silently drop
|
|
135
|
+
// every earlier forced key from fireforge.json.
|
|
136
|
+
const rawConfig = await loadRawConfigDocument(projectRoot);
|
|
137
|
+
const updatedConfig = mutateConfig(rawConfig, key, parsedValue, true);
|
|
138
|
+
await writeConfigDocument(projectRoot, updatedConfig);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const config = await loadConfig(projectRoot);
|
|
142
|
+
const updatedConfig = mutateConfig(config, key, parsedValue);
|
|
143
|
+
await writeConfig(projectRoot, updatedConfig);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
134
146
|
}
|
|
135
147
|
catch (error) {
|
|
136
148
|
throw new InvalidArgumentError(`Invalid value for "${key}": ${toError(error).message}`, key);
|
|
@@ -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
|