@hominis/fireforge 0.41.0 → 0.43.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 +81 -34
- package/dist/build-info.json +4 -4
- package/dist/src/cli.js +21 -2
- package/dist/src/commands/build.js +7 -1
- package/dist/src/commands/config.js +16 -27
- package/dist/src/commands/discard.js +1 -1
- package/dist/src/commands/export-flow.js +13 -3
- package/dist/src/commands/export-placement-conflicts.js +1 -1
- package/dist/src/commands/export-shared.d.ts +1 -1
- package/dist/src/commands/export-shared.js +4 -5
- package/dist/src/commands/furnace/chrome-doc-remove.js +3 -0
- package/dist/src/commands/furnace/chrome-doc.js +3 -0
- package/dist/src/commands/furnace/create.js +3 -0
- package/dist/src/commands/furnace/override.js +3 -0
- package/dist/src/commands/furnace/preview.js +3 -0
- package/dist/src/commands/furnace/refresh.js +3 -0
- package/dist/src/commands/furnace/remove.js +3 -0
- package/dist/src/commands/furnace/rename.js +3 -0
- package/dist/src/commands/furnace/scan.js +3 -0
- package/dist/src/commands/import.js +1 -1
- package/dist/src/commands/lint-per-patch.js +16 -8
- package/dist/src/commands/lint-per-run-checkjs.d.ts +4 -4
- package/dist/src/commands/lint-per-run-checkjs.js +4 -4
- package/dist/src/commands/lint-report.js +1 -1
- package/dist/src/commands/lint.js +2 -2
- package/dist/src/commands/patch/lint-ignore.js +1 -1
- package/dist/src/commands/patch/move-files-create.js +5 -3
- package/dist/src/commands/patch/move-files-into.d.ts +1 -1
- package/dist/src/commands/patch/move-files-into.js +5 -3
- package/dist/src/commands/patch/move-files.js +1 -1
- package/dist/src/commands/patch/rename.js +1 -1
- package/dist/src/commands/patch/reorder.d.ts +1 -2
- package/dist/src/commands/patch/reorder.js +1 -2
- package/dist/src/commands/patch/split.js +5 -3
- package/dist/src/commands/patch/staged-dependency-validate.js +1 -1
- package/dist/src/commands/patch/staged-dependency.js +4 -4
- package/dist/src/commands/re-export-adjacent.js +1 -1
- package/dist/src/commands/re-export-bulk-scan.d.ts +1 -1
- package/dist/src/commands/re-export-bulk-scan.js +5 -4
- package/dist/src/commands/re-export-drift.d.ts +4 -4
- package/dist/src/commands/re-export-drift.js +47 -8
- package/dist/src/commands/re-export-lint.d.ts +7 -5
- package/dist/src/commands/re-export-lint.js +9 -8
- package/dist/src/commands/re-export-single.d.ts +1 -1
- package/dist/src/commands/re-export-single.js +5 -4
- package/dist/src/commands/re-export.js +26 -14
- package/dist/src/commands/reset.js +1 -1
- package/dist/src/commands/run.js +1 -1
- package/dist/src/commands/source.js +6 -6
- package/dist/src/commands/status-check.d.ts +1 -1
- package/dist/src/commands/status-check.js +1 -1
- package/dist/src/commands/status-json.d.ts +2 -2
- package/dist/src/commands/status-json.js +1 -1
- package/dist/src/commands/status-output.d.ts +1 -1
- package/dist/src/commands/status-output.js +1 -1
- package/dist/src/commands/status.js +21 -7
- package/dist/src/commands/test-build-phase.d.ts +2 -2
- package/dist/src/commands/test-build-phase.js +9 -6
- package/dist/src/commands/test-diagnose.js +8 -1
- package/dist/src/commands/test-modes.d.ts +2 -2
- package/dist/src/commands/test-modes.js +3 -3
- package/dist/src/commands/test-register.js +7 -2
- package/dist/src/commands/test-run.d.ts +6 -0
- package/dist/src/commands/test-run.js +32 -3
- package/dist/src/commands/test-stale-gate.js +7 -2
- package/dist/src/commands/test.js +4 -4
- package/dist/src/commands/tree.js +2 -2
- package/dist/src/commands/typecheck.d.ts +2 -1
- package/dist/src/commands/typecheck.js +59 -3
- package/dist/src/commands/verify.js +26 -1
- package/dist/src/core/ast-utils.js +14 -2
- package/dist/src/core/branding.d.ts +16 -0
- package/dist/src/core/branding.js +18 -2
- package/dist/src/core/browser-wire.js +9 -1
- package/dist/src/core/build-baseline-types.d.ts +1 -1
- package/dist/src/core/build-baseline.d.ts +1 -1
- package/dist/src/core/build-baseline.js +1 -1
- package/dist/src/core/build-overwrite-guard.d.ts +49 -0
- package/dist/src/core/build-overwrite-guard.js +112 -0
- package/dist/src/core/build-prepare.d.ts +13 -5
- package/dist/src/core/build-prepare.js +42 -9
- package/dist/src/core/config-mutate.d.ts +3 -2
- package/dist/src/core/config-mutate.js +5 -2
- package/dist/src/core/config.d.ts +3 -2
- package/dist/src/core/config.js +2 -0
- package/dist/src/core/coverage-extend.d.ts +1 -1
- package/dist/src/core/coverage-extend.js +1 -1
- package/dist/src/core/destructive.d.ts +23 -0
- package/dist/src/core/destructive.js +45 -4
- package/dist/src/core/diff-hunks.js +31 -17
- package/dist/src/core/discard-baseline.d.ts +1 -1
- package/dist/src/core/discard-baseline.js +1 -1
- package/dist/src/core/display-state.d.ts +52 -0
- package/dist/src/core/display-state.js +89 -0
- package/dist/src/core/engine-session-lock.d.ts +20 -0
- package/dist/src/core/engine-session-lock.js +84 -14
- package/dist/src/core/export-moved-code-shape.d.ts +60 -0
- package/dist/src/core/export-moved-code-shape.js +129 -0
- package/dist/src/core/file-lock.d.ts +44 -0
- package/dist/src/core/file-lock.js +157 -42
- package/dist/src/core/furnace-apply-helpers.d.ts +1 -1
- package/dist/src/core/furnace-apply-helpers.js +1 -1
- package/dist/src/core/furnace-apply-output.js +1 -1
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +2 -2
- package/dist/src/core/furnace-apply-overwrite-warn.js +2 -2
- package/dist/src/core/furnace-apply-undeploy-actions.d.ts +1 -1
- package/dist/src/core/furnace-apply-undeploy-actions.js +1 -1
- package/dist/src/core/furnace-apply.js +13 -1
- package/dist/src/core/furnace-config-custom.d.ts +2 -1
- package/dist/src/core/furnace-config-order.d.ts +2 -1
- package/dist/src/core/furnace-config-order.js +9 -4
- package/dist/src/core/furnace-config-validate.d.ts +4 -3
- package/dist/src/core/furnace-config-validate.js +3 -3
- package/dist/src/core/furnace-config.js +5 -0
- package/dist/src/core/furnace-operation.d.ts +12 -0
- package/dist/src/core/furnace-operation.js +125 -1
- package/dist/src/core/furnace-rollback.d.ts +17 -0
- package/dist/src/core/furnace-rollback.js +35 -1
- package/dist/src/core/git-base.js +7 -2
- package/dist/src/core/git-diff.js +11 -2
- package/dist/src/core/git-readonly-index.d.ts +59 -0
- package/dist/src/core/git-readonly-index.js +137 -0
- package/dist/src/core/license-headers.d.ts +1 -1
- package/dist/src/core/license-headers.js +1 -1
- package/dist/src/core/lint-cache.d.ts +15 -5
- package/dist/src/core/lint-cache.js +35 -8
- package/dist/src/core/mach-known-noise-filter.d.ts +21 -0
- package/dist/src/core/mach-known-noise-filter.js +23 -0
- package/dist/src/core/mach-mozconfig.js +21 -3
- package/dist/src/core/module-resolution-preflight.d.ts +51 -0
- package/dist/src/core/module-resolution-preflight.js +155 -0
- package/dist/src/core/patch-export.js +2 -1
- package/dist/src/core/patch-identifier-suggest.js +29 -3
- package/dist/src/core/patch-lint-checkjs.d.ts +5 -5
- package/dist/src/core/patch-lint-checkjs.js +6 -6
- package/dist/src/core/patch-lint-creators.d.ts +1 -1
- package/dist/src/core/patch-lint-creators.js +1 -1
- package/dist/src/core/patch-lint-cross.d.ts +1 -1
- package/dist/src/core/patch-lint-cross.js +4 -4
- package/dist/src/core/patch-lint-css.js +1 -1
- package/dist/src/core/patch-lint-diff.d.ts +1 -1
- package/dist/src/core/patch-lint-diff.js +1 -1
- package/dist/src/core/patch-lint-jsdoc.js +4 -2
- package/dist/src/core/patch-lint-module-registration.d.ts +13 -0
- package/dist/src/core/patch-lint-module-registration.js +13 -4
- package/dist/src/core/patch-lint-mozbuild.d.ts +1 -1
- package/dist/src/core/patch-lint-mozbuild.js +1 -1
- package/dist/src/core/patch-lint-ownership.d.ts +2 -2
- package/dist/src/core/patch-lint-ownership.js +2 -2
- package/dist/src/core/patch-lint.d.ts +2 -2
- package/dist/src/core/patch-lint.js +3 -3
- package/dist/src/core/patch-lock.d.ts +21 -0
- package/dist/src/core/patch-lock.js +29 -2
- package/dist/src/core/patch-manifest-io.d.ts +2 -1
- package/dist/src/core/patch-manifest-io.js +49 -0
- package/dist/src/core/patch-parse.d.ts +18 -0
- package/dist/src/core/patch-parse.js +24 -1
- package/dist/src/core/patch-policy.d.ts +2 -2
- package/dist/src/core/patch-policy.js +2 -2
- package/dist/src/core/smoke-patterns.d.ts +1 -1
- package/dist/src/core/status-binary.d.ts +1 -1
- package/dist/src/core/status-binary.js +1 -1
- package/dist/src/core/status-classify.d.ts +1 -1
- package/dist/src/core/status-classify.js +2 -2
- package/dist/src/core/test-harness-crash.d.ts +69 -3
- package/dist/src/core/test-harness-crash.js +174 -9
- package/dist/src/core/test-stale-check.d.ts +1 -1
- package/dist/src/core/test-stale-check.js +1 -1
- package/dist/src/core/token-category.js +1 -1
- package/dist/src/core/token-manager.d.ts +1 -1
- package/dist/src/core/token-manager.js +5 -5
- package/dist/src/core/tree-cow.js +1 -1
- package/dist/src/core/tree-guard.js +2 -2
- package/dist/src/core/tree-store.js +1 -1
- package/dist/src/core/typecheck-shim.d.ts +2 -2
- package/dist/src/core/typecheck-shim.js +2 -2
- package/dist/src/core/typecheck.js +10 -0
- package/dist/src/core/xpcshell-profile-dir.js +1 -1
- package/dist/src/errors/base.d.ts +22 -1
- package/dist/src/errors/base.js +32 -1
- package/dist/src/errors/codes.d.ts +10 -0
- package/dist/src/errors/codes.js +10 -0
- package/dist/src/types/commands/options.d.ts +27 -11
- package/dist/src/types/commands/patches.d.ts +1 -1
- package/dist/src/types/config.d.ts +2 -2
- package/dist/src/types/furnace.d.ts +1 -1
- package/dist/src/types/json.d.ts +22 -0
- package/dist/src/types/json.js +11 -0
- package/dist/src/utils/assert.d.ts +46 -0
- package/dist/src/utils/assert.js +83 -0
- package/dist/src/utils/build-info.d.ts +1 -1
- package/dist/src/utils/build-info.js +1 -1
- package/dist/src/utils/logger.d.ts +20 -0
- package/dist/src/utils/logger.js +26 -0
- package/dist/src/utils/options.d.ts +12 -0
- package/dist/src/utils/options.js +39 -0
- package/dist/src/utils/stdio-drain.d.ts +1 -1
- package/dist/src/utils/stdio-drain.js +1 -1
- package/dist/src/utils/validation.d.ts +12 -1
- package/dist/src/utils/validation.js +13 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.43.0
|
|
4
|
+
|
|
5
|
+
### Verdict correctness
|
|
6
|
+
|
|
7
|
+
- A suite that ends clean (`Ran N checks` / `Unexpected results: 0`, no unexpected lines) and then dies in the recognized upstream mozsystemmonitor teardown traceback is now a `PASS … (harness teardown noise ignored)`. The classifier's green-summary veto required a `SUITE_END` marker, and that teardown crash is precisely what prevents the marker from printing — so a substantively green suite was indistinguishable at the summary level from a red one. Every hard-evidence veto still fails the run: a crash marker, a non-zero unexpected count, a real `TEST-UNEXPECTED-*` line, the shutdown-re-entry shape, or a requested file that never started. An unrecognized teardown traceback is not forgiven.
|
|
8
|
+
- `engine/mozconfig` is written only when the rendered content changes. `mozconfig` is a mach _configure input_, so its mtime — not its content — is compared against `config.status`; regenerating a byte-identical file on every build-capable invocation made mach re-run `configure` plus backend regeneration ("Backend config changed; N files touched") on every run, each run re-arming the next. **Consumers who raised hang-canary time bounds to absorb this can restore them.**
|
|
9
|
+
- Read-only commands (`verify`, `lint --per-patch`, `typecheck`) no longer write the primary engine checkout. `git status` and `git diff HEAD` refresh and rewrite `.git/index`, which made these commands a second writer and tripped a concurrent `fireforge test`'s engine-fingerprint refusal (`FAIL reason=inconclusive`) on suites that had actually passed. They now run their git plumbing against a private `GIT_INDEX_FILE` seeded from the repository's own index, and `typecheck` never emits an incremental `.tsbuildinfo` sidecar. The engine-change refusal itself is unchanged. A transient `.git/index.lock` during the engine-generation probe is retried rather than latched as "unmeasurable"; a durable failure (a non-git `engine/`) still reports immediately.
|
|
10
|
+
|
|
11
|
+
### Diagnostics
|
|
12
|
+
|
|
13
|
+
- An xpcshell run that dies on SIGSEGV having produced no test evidence now names its known cause — an `.sys.mjs` imported from a packaged module whose `EXTRA_JS_MODULES` registration is missing — instead of surfacing as a bare `return code: -11` with no output.
|
|
14
|
+
- `fireforge verify` gained an engine-aware module-resolution preflight: every `resource://` system-module specifier the queue-owned modules import (including lazy `ChromeUtils.defineESModuleGetters` specifiers, whose failure surfaces at runtime only as an `undefined` binding with no logged import error) is resolved against `engine/`, and an unregistered or missing target is named as text. The 0.41.0 `unregistered-system-module` check only sees modules a patch newly creates; the class recurred through imports added to an existing module, which this covers.
|
|
15
|
+
- The headed no-output stall now probes the display's power state on macOS and states a measured-asleep display as fact in both the failure message and the verdict line (`(headed run stalled with the display asleep)`). It also prints the three recorded causes of that signature — sleeping display, headless SWGL compositor failure, broken `chrome://` image on the startup page — so one stall points at the discriminating next step. The previous advice to wrap runs in `caffeinate` is corrected: `caffeinate` prevents sleep, it cannot wake an already-sleeping display.
|
|
16
|
+
- FireForge's own escalation and explanatory notices (the jar.mn full-build escalation, backend regeneration, the Furnace sync banner, the sharding notice) are emitted at warning severity with a `[FireForge] NOTICE:` prefix, so agent-facing output filters that keep only warnings and errors no longer drop the one line that explains a multi-minute build.
|
|
17
|
+
|
|
18
|
+
### Messages and refusals
|
|
19
|
+
|
|
20
|
+
- The stale-build refusal suggests `--build` first and frames `--allow-stale-build` as for out-of-band rebuilds only, stating plainly that it accepts a packaging predating the edit under test.
|
|
21
|
+
- `--refuse-foreign-drift` no longer calls the operator's own additions "foreign". The per-file drift report tags each file `[edited since your last export]` or `[unchanged since your last export — another session may own it]` by comparing the engine file's mtime against the patch body being refreshed, and the refusal separates the two populations and their opposite remedies.
|
|
22
|
+
- The `--expect` "showed no drift" warning names the converged case: a concurrent session may already have exported the same change.
|
|
23
|
+
- Unknown patch identifiers on `re-export` and `--scan-files` now suggest the nearest matches (prefix and edit distance) instead of printing the full manifest. Prefix matching also resolves typed abbreviations that edit distance misses; a one-character ordinal can no longer prefix-match everything.
|
|
24
|
+
- `patch move-files --create`, `patch move-files`, and `patch split` refuse a prompt-less non-TTY run **up front**, naming `--yes`, instead of after minutes of diff and projected-lint work.
|
|
25
|
+
|
|
26
|
+
### Features
|
|
27
|
+
|
|
28
|
+
- `--wait-lock` is accepted by every command. Lock-taking commands honour it as before; lock-free ones (`status`, `patch staged-dependency`, and the rest) accept and ignore it, saying so in their help — a scripted sequence that blanket-appends the flag no longer dies with a usage error. A malformed value is still a usage error everywhere.
|
|
29
|
+
- `fireforge status --lock` reports the engine session lock without acquiring it: holder pid, the command it is running, how long it has held the lock, whether the holder is still alive, and how many waiters are queued. Waiters advertise themselves, so the `--wait-lock` progress line now also states queue position ("2 ahead of you (queue of 3)").
|
|
30
|
+
- An export refused by projected cross-patch lint now detects the new-file-plus-moved-code shape — new files carrying lines an existing patch still contains — and prints the working adopt-then-split sequence (`re-export --scan --scan-file`, then `patch move-files --create --order N`) with real arguments.
|
|
31
|
+
- `fireforge typecheck` runs the per-patch checkJs pass — the authority `export` enforces — and reports its findings in the same tally. A green `typecheck` now means export-clean types; previously the two authorities could disagree and only the stricter one spoke, at export time.
|
|
32
|
+
- The per-patch lint cache records the lint-ignore waiver set that produced each entry and refuses a replay under a different waiver set, so writing a waiver can never be answered with the pre-waiver verdict. The cache schema bump discards any entry already holding a mismatched result.
|
|
33
|
+
- FireForge now checks its own internal invariants at runtime. A new assertion primitive (`src/utils/assert.ts`) guards the state transitions the mutation lifecycle depends on — the rollback journal being registered before the first mutation, the furnace lock being held before the body runs, manifest order matching the filenames it is written alongside — and a violation stops the run at the first inconsistency instead of continuing to write. **Assertion failures exit with the new code 11 (`INTERNAL_ERROR`)** rather than the generic 1, and print a stack trace: they are FireForge bugs, not operator errors, so CI can escalate them instead of retrying. Nothing an operator can misconfigure reaches this path — user, filesystem, and subprocess failures keep their existing typed errors and exit codes.
|
|
34
|
+
|
|
35
|
+
### Hardening
|
|
36
|
+
|
|
37
|
+
- `build-prepare` warns — naming every file — before overwriting engine content that matches neither a patch body nor the pristine baseline, and `fireforge build --refuse-unexported-drift` makes it a hard stop. On a multi-session checkout the silent overwrite is what let an unexported edit be reverted and a later re-export capture a half-reverted hybrid that every gate then passed.
|
|
38
|
+
- **A furnace mutation that fails now rolls back the engine, not just one that is interrupted.** `runFurnaceMutation` previously restored its rollback journal on SIGINT/SIGTERM only; on a thrown error its `finally` merely deregistered the operation — and because it also marked it completed, the operation vanished from the signal handler's view too. Bodies that catch and restore on their own were unaffected, but `applyAllComponents` rolls back on _collected_ component errors, so anything thrown from its pre- or post-loop work (patch-claim loading, the post-apply consistency checks, the state write) escaped with the engine torn and no `pendingRepair` breadcrumb — reachable from `furnace apply`, `sync`, `deploy`, `build-prepare`, and `doctor --repair-furnace`. The wrapper now runs cleanup callbacks, restores the journal, and rethrows the **original** error; a rollback that itself fails becomes a `pendingRepair` marker plus a warning rather than replacing the error you needed to read. A pre-flight refusal that never registered a journal deliberately writes no marker, so a validation failure cannot block later mutations behind a repair with nothing to reconcile.
|
|
39
|
+
|
|
40
|
+
### Release hygiene
|
|
41
|
+
|
|
42
|
+
- The composed macOS bundle identity is now pinned end-to-end: `MOZ_MACBUNDLE_ID` (the leaf, from branding `configure.sh`) recomposed with `--with-distribution-id` (the prefix, from the generated mozconfig) must equal the configured `appId` exactly. Each half was already unit-tested after the 0.42.0 fix; nothing pinned the composition, which is the property that shipped broken as `org.hominis.org.hominis.browser`. Verified present in the 0.43.0 lineage.
|
|
43
|
+
|
|
44
|
+
## 0.42.0
|
|
45
|
+
|
|
46
|
+
### macOS bundle identity
|
|
47
|
+
|
|
48
|
+
- `MOZ_MACBUNDLE_ID` in the generated branding `configure.sh` now carries only the LEAF segment of `appId`, and the generated mozconfig emits `ac_add_options --with-distribution-id=<appId prefix>` — upstream `toolkit/moz.configure` composes `CFBundleIdentifier` as `<distribution-id>.<MOZ_MACBUNDLE_ID>`, so writing the full reverse-domain `appId` double-prefixed the shipped bundle id (observed: `org.mozilla.org.hominis.browser`). A `configure.sh` carrying the full `appId` now reads as stale and is rewritten.
|
|
49
|
+
|
|
3
50
|
## 0.41.0
|
|
4
51
|
|
|
5
52
|
### Release and build integrity
|
|
@@ -82,43 +129,43 @@
|
|
|
82
129
|
## 0.40.0
|
|
83
130
|
|
|
84
131
|
- **Dependencies: adopted all five open dependabot PRs (#48, #47, #45, #44, #42) after Go-based TypeScript 6 compatibility review** — the pinned `typescript ~6.0.0` (6.0.3 installed) constrained which upgrades were feasible; every bumped tool checked out: `typescript-eslint` declares peer `typescript >=4.8.4 <6.1.0`, `knip` carries no TypeScript dependency at all (oxc parser), and `dpdm` bundles its own nested TypeScript 5.9.3, so none touch the root compiler. Production: `acorn` 8.17.0→8.18.0 (lockfile-only) and `magic-string` 0.30.21→**1.1.0** — a major that only drops the CJS/UMD builds for pure ESM (FireForge is pure ESM; the five `MagicString` call sites use the unchanged core API). Dev: `@types/node` 26.1.2, `dpdm` 4.3.0, `eslint` 10.8.0, `eslint-plugin-jsdoc` 63.3.3, `knip` 6.29.0, `lint-staged` 17.3.0, `prettier` 3.9.6, `typescript-eslint` 8.66.0, `vite` 8.2.1, and `eslint-plugin-simple-import-sort` 13→**14** (breaking only for string-literal export names, which the codebase does not use); four of these resolved one patch/minor past PR #48's snapshot within the existing caret ranges (typescript-eslint 8.66.0 re-verified against the same `<6.1.0` peer window). CI: the SHA-pinned actions group (checkout 7.0.1 with `--unset`-escaping and branch-trim fixes, setup-node, codeql init/analyze). knip 6.29's stricter re-export tracking flagged seven type re-exports (`PatchCoverage`, `ClearablePatchMetadataField`, `PatchMetadataMutation{,Result}` in `patch-export.ts`; `ForwardImportEdge` in `patch-lint-reexports.ts`; `PatchManifestRowMutation{,Result}` in `patch-manifest.ts`) that no module, test, or public-API entry consumed via the barrel hop — a real 6.26 blind spot, so the dead re-exports were removed (the types stay exported from their defining modules). Verified by a full green `release:check`.
|
|
85
|
-
- **New `fireforge tree` command: copy-on-write verification clones for concurrent read-mostly work** (
|
|
132
|
+
- **New `fireforge tree` command: copy-on-write verification clones for concurrent read-mostly work** (feature — every consumer parallelisation attempt dead-ended on the ONE applied engine tree: `engine/` is gitignored by design so git worktrees provide no isolation, and the repo-root build lock plus one objdir kept all fix/test/export work serial; measured on APFS, `engine/browser` — 11,513 files, 156 MB — clones via clonefile in ~3 s): `tree create <name>` snapshots the project (fireforge.json, configs, src, patches, components, and `engine/` with `obj-*` excluded) into `.fireforge/trees/<name>` via filesystem CoW (APFS clonefile / btrfs-XFS reflink, probed BY DOING in the destination directory), taken under the primary engine-session lock so a mid-mutation state is never captured; filesystems without CoW REFUSE honestly, with `--force-copy` opting into a full physical copy explicitly (never silently). A tree is a full project root, so `status`, `lint`, `typecheck`, `verify`, `doctor`, and `export --dry-run` run inside it unmodified with their own per-tree locks — while every mutating command (`export`, `re-export`, `patch *`, `token *`, `furnace *`, `build`, headed `run`, …) is refused via the `.fireforge/tree.json` marker and a default-deny verdict table enforced in one commander preAction hook (a drift test forces every new command to be classified); patches and exports stay strictly serial in the primary tree with no merge-back model. `tree list [--json]` reports staleness against the primary engine HEAD and `patches.json` fingerprint; `tree remove` refuses while a live process holds a tree lock and path-contains its delete; `tree exec <name> -- <cmd>` runs a command inside a tree; Windows is refused (POSIX clonefile/reflink only). **Deferred to 0.41.0 with the design recorded: `--with-objdir` and in-tree `test`** — mach objdirs embed absolute `topsrcdir`/`topobjdir` paths (FireForge's own `mach-build-artifacts.ts` parses them), so a cloned objdir would silently operate against the primary tree; a `mozinfo.json` preflight is the planned gate. New invariant 7 in `docs/lifecycle-invariants.md`. Pinned by `tree-cow.test.ts` (per-platform cp argv, probe verdicts), `tree-store.integration.test.ts` (snapshot contents/exclusions, independence, staleness, live-holder refusal), `tree-guard.test.ts` (verdict drift gate), and `tree.integration.test.ts` (real clone + real-program guard refusal).
|
|
86
133
|
- **Previously deferred items, re-adjudicated for 0.40.0**: auto-`caffeinate` for headed macOS dispatches stays deferred (the 0.39.0 F17 message + operator rule works; auto-wrap adds TTY/permission complexity for little gain); real co-ownership of one upstream file by two patches stays deferred (design-level — shared-file diffs are pristine-baseline so co-ownership remains unrepresentable; the consumer's single-owner consolidation plus audit-side per-patch entry lists stands); `furnace create` maintaining the consumer jsconfig path mapping for new widget modules stays deferred this release (the JSONC-preserving edit machinery exists, but the mapping shape is consumer-project-specific — revisit with a concrete config contract).
|
|
87
|
-
- **Concurrent xpcshell runs no longer collide on Firefox's fixed profile directory** (
|
|
88
|
-
- **Per-patch lint can now typecheck patch-adopted test `.js` files against a consumer-configurable harness shim: `patchLint.checkJsTestFiles` + `patchLint.checkJsTestShim`** (
|
|
89
|
-
- **Patch size metrics are now on the public API — `countNonBinaryDiffLines`, `resolvePatchSizeTier`, and the new `getPatchSizeThresholds` — plus a machine-readable per-patch report via `lint --per-patch --report <path>`** (
|
|
90
|
-
- **A size finding waived by `lintIgnore` no longer vanishes without a trace in per-patch mode** (
|
|
91
|
-
- **Smoke-run summaries now attribute allowlist hits per entry, with zero-hit entries flagged as removal candidates** (
|
|
92
|
-
- **`lint --per-patch <patch…>` now works: positional arguments in per-patch mode select patches through the same alias resolution as `--patches`** (
|
|
93
|
-
- **`patch staged-dependency --remove` now accepts `--file` + `--specifier` alone, inferring `--creates` from a unique match; missing-flag errors name the actually-missing flags** (
|
|
94
|
-
- **`token add` can no longer silently strand or skip a token when its category is wrong — every bypass around the missing-category refusal is closed** (
|
|
95
|
-
- **Patch-lint `raw-color-value` no longer fires on hex colors inside CSS comments that span the context/added-line boundary** (
|
|
96
|
-
- **`export --name ui-foo --category ui` now lands manifest `name: "foo"` in one step, and `--name <stem>.patch` no longer trips the dot validator** (
|
|
97
|
-
- **New `status --check` (and `--fail-on <class,...>`) turns classification drift into a CI-enforceable non-zero exit** (
|
|
98
|
-
- **`status --json` files[] entries now carry a `patch` field naming the owning patch filename (`null` when unowned)** (
|
|
99
|
-
- **Scan-less `re-export`'s adjacency notice is now classification-aware, and the new `--refuse-adjacent-unmanaged` flag upgrades it to a non-zero refusal that skips the write** (
|
|
100
|
-
- **`re-export` now retries once, after a 300 ms backoff, when a patch fails on transient git `index.lock` contention** (
|
|
101
|
-
- **`export --before/--after` no longer refuses placements whose renumber never actually reaches a reserved range** (
|
|
134
|
+
- **Concurrent xpcshell runs no longer collide on Firefox's fixed profile directory** (bug — the harness defaults `XPCSHELL_TEST_PROFILE_DIR` to a fixed `$TMPDIR/firefox/xpcshellprofile`, so two overlapping `fireforge test` invocations corrupted each other's profiles; FireForge itself never set the variable): every harness invocation that can dispatch xpcshell (single runs, each shard, and generic `mach test` runs) now exports a fresh `mkdtemp` profile dir (`fireforge-xpcshell-profile-*`) into the mach environment via the new `withXpcshellProfileDir` helper and removes it after the run (best-effort — a cleanup failure warns and leaves the dir to the OS tmp reaper); the crash/symlink-repair retries within one invocation deliberately reuse the same directory, an operator-provided `XPCSHELL_TEST_PROFILE_DIR` is respected verbatim (and never deleted), and pure mochitest dispatches keep a byte-identical env. This is the independently-shippable half of the G15 verification-tree design — it also unblocks concurrent test runs from separate checkouts today. Pinned by uniqueness/opt-out/override/retry-reuse cases in `test-run-suite-dispatch.test.ts` and the lifecycle cases in `xpcshell-profile-dir.test.ts`.
|
|
135
|
+
- **Per-patch lint can now typecheck patch-adopted test `.js` files against a consumer-configurable harness shim: `patchLint.checkJsTestFiles` + `patchLint.checkJsTestShim`** (enforcement gap — honest framing: the consumer's seven TS2339s on `TestUtils.waitForCondition` were invisible to `lint --per-patch --max-warnings 0` not because the shim differed from the jsconfig projects', but because test `.js` files were never in the checkJs program at all — only new `.sys.mjs` modules were; the harness global that does not exist should fail at the patch boundary where the test was authored): opt-in (`checkJsTestFiles` requires `checkJs`; new failure surface, nonzero compile cost) extends the check set to patch-owned `browser_*`/`test_*`/`xpcshell_*` and `/test/`-path `.js` files, each checked as its OWN small script-scope program (mochitest scripts share top-level scope only with their directory's `head*.js` helpers, which are included as roots) against the built-in Firefox shim, a new loose built-in test-harness shim (`TestUtils`, `BrowserTestUtils`, `add_task`, `ok`/`is`, …), and the consumer's `checkJsTestShim` `.d.ts` — whose typed declarations compose BEFORE the loose baseline and therefore win, so a typed `TestUtils` there is exactly what turns the consumer's case into an export-time error. Wired identically into `lint --per-patch` (one merged lazy build per run) and the export/re-export path so the two agree; `typecheck.projectOverrides` remains typecheck-only by design (loading real jsconfig projects into per-patch lint's empty-tree compiler host would pull the Firefox tree in). Pinned by the was-never-checked gap pin, the TS2339-surfaces case, and the loose-baseline pass in `lint-per-patch-checkjs.integration.test.ts`, plus config cross-field validation in `config.test.ts`.
|
|
136
|
+
- **Patch size metrics are now on the public API — `countNonBinaryDiffLines`, `resolvePatchSizeTier`, and the new `getPatchSizeThresholds` — plus a machine-readable per-patch report via `lint --per-patch --report <path>`** (feature — the size counter `large-patch-lines` fires on was unreachable programmatically (`"./*": null` blocks deep imports and `src/index.ts` never exported it), so the consumer MIRRORED it to build waiver-freshness checks, a silent-divergence risk by construction): the three functions and the `PatchSizeTierDecision` type join the entry exports (pinned by `public-api.test.ts`), and the `--report` JSON (schemaVersion 1; requires `--per-patch`) carries per patch: filename, name, status (`linted`/`cached`/`skipped`), `lineCount` (non-binary diff lines), `filesAffected` count, the tier decision, the ACTIVE thresholds for that tier, the surfaced issues, and the lintIgnore-suppressed issues, plus run totals. Pinned by the report-shape assertions in `lint.integration.test.ts`.
|
|
137
|
+
- **A size finding waived by `lintIgnore` no longer vanishes without a trace in per-patch mode** (observability — the waiver filter dropped the whole issue including the measurement embedded in its message, so a waived patch's CURRENT size could not be read back from the tool that enforces it and waiver-cited counts had to be calibrated by hand; aggregate mode printed a suppressed count while per-patch printed nothing): the finding stays suppressed (no exit-code or `--max-warnings` effect) but its measurement is reported as `NOTICE [large-patch-lines] <patch>: suppressed by lintIgnore — Patch is N lines (hard limit: M) …` (likewise `large-patch-files` / `file-too-large`), the run summary adds `Suppressed k issue(s) via per-patch lintIgnore.` mirroring the aggregate wording, and suppressed issues plus the measured line count are carried through the per-patch lint cache (schema bump to v2, one-time cold cache) so a warm run can never report less than a cold one — the same F5-class hazard the run-level checkJs fix closed. Also available structurally in the `--report` JSON. Pinned by the waived-measurement case in `lint.integration.test.ts` and the cache round-trip in `lint-cache.test.ts`.
|
|
138
|
+
- **Smoke-run summaries now attribute allowlist hits per entry, with zero-hit entries flagged as removal candidates** (observability — `matchesAllowlist` returned a bare boolean and `compileAllowlistFromFile` discarded source line numbers, so an allowlist entry that silently stopped matching was undetectable and the consumer rebuilt per-entry replay in ~190 lines against a retained capture): compiled entries now carry their verbatim source and origin (`allow.txt:12`, `--console-allow #2` — file origins keep the ORIGINAL line numbers, comments included) via the new `CompiledAllowlistEntry` shape, `matchAllowlist` returns the first matching index (deterministic first-match attribution, replacing the boolean `matchesAllowlist`), and the post-run summary prints one `N× <origin> <source>` row per entry with `0×` rows suffixed `(never matched — candidate for removal)`. The three scalar counters and the exit contract (12/13) are unchanged; the human summary is the supported surface (no JSON sidecar this release). Pinned by the attribution rows in `run.test.ts` and the origin/first-match cases in `smoke-patterns.test.ts`.
|
|
139
|
+
- **`lint --per-patch <patch…>` now works: positional arguments in per-patch mode select patches through the same alias resolution as `--patches`** (ergonomics — the natural invocation `lint --per-patch a.patch b.patch` was rejected with the misleading "cannot be combined with explicit engine file paths" while the suggested form required retyping the list behind a flag): positionals are appended after explicit `--patches` entries and resolved tolerantly (filename, stem, manifest name, category-prefixed slug, or bare slug); a non-matching positional fails loud with `No patch in the queue matches "…". In --per-patch mode, positional arguments and --patches select patches … drop --per-patch to lint engine paths.` plus the available-patches listing, so an engine path can never silently lint the wrong thing. The `--patches` help notes the positional equivalence. Pinned by the positional-subset and unmatched-positional cases in `lint.test.ts`.
|
|
140
|
+
- **`patch staged-dependency --remove` now accepts `--file` + `--specifier` alone, inferring `--creates` from a unique match; missing-flag errors name the actually-missing flags** (ergonomics + message — the remove path demanded the full triple even when file+specifier identified exactly one entry, and every `Invalid Argument` printed `Argument: patch staged-dependency`, restating the command instead of the offending flag): a unique file+specifier(+`--owner`) match infers its `creates` with an `Inferred --creates …` notice; an ambiguous match refuses listing every candidate (`pass --creates to pick one`); no match keeps the honest exit-0 "no staged forward-import matched" summary. `--kind registration` gets the identical treatment keyed on `--line`. All `Argument:` lines in this command now name flags (`--specifier, --creates`, `--kind`, `--add/--remove/--clear`, …), and the `-y/--yes` help no longer claims a confirmation prompt exists (message-only — the command never prompted; the flag records scripted consent in the history log). Pinned by the inference, ambiguity, no-match, and attribution cases in `patch-tier-and-lint-ignore.integration.test.ts`.
|
|
141
|
+
- **`token add` can no longer silently strand or skip a token when its category is wrong — every bypass around the missing-category refusal is closed** (bug + claim partially REFUTED — the reported plain-path "missing category exits 0" is contradicted by the pre-existing `assertTokenCategoryExists` throw, pinned since before 0.39.0; the 19 observed silent no-ops reproduce only through three real bypasses, all fixed): (1) category banners now match EXACTLY (`/* = Name = */` text equality, multi-line block-line equality) instead of by substring, so a TOC comment mentioning "Colors — Terminal" or a `Colors — Canvas` banner can no longer satisfy a lookup for a different name — assert and writer share the matcher (new `token-category.ts`); (2) the whole-file idempotency check is replaced by a per-category one — a token already in the TARGET category skips with its location named (`already exists in category "X" (line N)`), while a token declared elsewhere in the base `:root` block REFUSES with `Token "…" is already declared outside category "…" (<file>:<line>, section "…")` instead of silently skipping and discarding `--create-category`; dark-`@media`/variant companion blocks are excluded from ownership; the exact name must match (`--foo` no longer collides with `--x-foo`); (3) `--variant` now validates `--category` against the file instead of silently discarding it, and `--variant` + `--create-category` is refused. Dry-run agrees with the real run on all three. Behavior change: loose banner matches that previously "worked" now fail loud with the existing available-categories listing. Pinned by the bypass matrix in `token-manager.test.ts`.
|
|
142
|
+
- **Patch-lint `raw-color-value` no longer fires on hex colors inside CSS comments that span the context/added-line boundary** (bug — the per-patch comment stripper only saw the joined added lines, so a `/*` opened on an unchanged context line never suppressed a hex mention on an added line; two prose references to a vendored sheet's black paint inside comments tripped the rule for the consumer on 2026-07-30): the diff branch now masks comments across the full post-patch file (spaces preserve line numbers; an unclosed trailing `/*` masks to EOF) via the new `extractAddedLineNumbersPerFile` diff walker, then scans only the patch's added line numbers. Inline `fireforge-ignore: raw-color-value` waivers keep matching against pre-strip text; the no-diff branch is unchanged; and when the diff's line numbers fall outside the on-disk file (drifted patch) the scan falls back to the previous joined-added-lines behavior instead of misattributing lines. Pinned by the boundary-spanning, still-fails-in-declaration, waiver, and drift-fallback cases in `patch-lint.test.ts`.
|
|
143
|
+
- **`export --name ui-foo --category ui` now lands manifest `name: "foo"` in one step, and `--name <stem>.patch` no longer trips the dot validator** (bug + message — the filename slug pipeline has stripped redundant category prefixes since 0.33.0 (and `.patch` suffixes since 0.39.0's F9) but the manifest display name kept the raw flag value, so a bare-filename-slug naming policy required a follow-up `patch rename --to` after every export): display names are normalized after category resolution with the same require-the-category-token rule as filenames via the new `normalizePatchDisplayName` (`NNN-ui-` / `ui-` prefixes stripped case-insensitively and repeatedly; a bare leading number is never stripped, so `2-step-verification` survives; the strip falls back to the stem rather than emptying the name), announced with `Patch name normalized: "ui-foo" → "foo" (the filename carries the order and category prefix).`; a trailing `.patch` is stripped before `validatePatchName` runs. Applies to `export` and `export-all`. `patch rename` additionally gains `-d` as an alias for `--description`; multi-target `re-export --scan --scan-file` remains deferred (file→patch assignment would be ambiguous — `--scan-files <manifest>` already serves bulk assignment). Pinned in `export-shared.test.ts` and `validation.test.ts`.
|
|
144
|
+
- **New `status --check` (and `--fail-on <class,...>`) turns classification drift into a CI-enforceable non-zero exit** (enforcement gap — every 0.39.0 status variant exited 0 even while reporting unmanaged/drifted/conflicted files, so "refuse if dirty" required every consumer to parse `--json` and re-implement the classification policy; combined with the G2 gap this let two manifest-registered proving suites ship into no patch while every advertised gate stayed green): `--check` fails with exit 1 whenever any `unmanaged`, `patch-owned-drift`, or `conflict` file exists, naming each offending classification with its first three paths (`status --check failed: 2 unmanaged (a, b), 1 patch-owned-drift (c). …`); `--fail-on` replaces the policy set (implying `--check`, refusing unknown values by naming the valid six); composes with the default view and `--json` (the JSON stays parseable on stdout, the refusal routes to stderr) and is refused alongside `--raw`/`--unmanaged`/`--ownership`/`--test-coverage`. A clean tree passes. Pinned by `status-check.test.ts` and the `--check` cases in `status.test.ts`.
|
|
145
|
+
- **`status --json` files[] entries now carry a `patch` field naming the owning patch filename (`null` when unowned)** (feature — the classifier resolved every file's owner at classify time and then discarded it for non-conflict entries, so resolving 16 drifted files to their patches meant scraping the human `--ownership` box table with `awk`): single-owner `patch-backed`, `patch-owned-drift`, and healthy single-owner `furnace` entries name their owner; `branding`, unowned `furnace`, and `conflict` entries report `null` (conflicts keep the existing `claimedBy` array — a single-owner field must not misrepresent a multi-claim). Additive to `schemaVersion: 1`, so consumers pinning the schema keep parsing; the post-slice re-export loop is now scriptable straight off `--json`. Pinned by the owner-matrix case in `status.test.ts`.
|
|
146
|
+
- **Scan-less `re-export`'s adjacency notice is now classification-aware, and the new `--refuse-adjacent-unmanaged` flag upgrades it to a non-zero refusal that skips the write** (bug + message — the claim that 0.39.0 was fully silent is partially REFUTED: the v0.27.2 sibling advisory did fire for the reported shape, a new test created beside a patch's owned tests, and is pinned by the pre-fix reproduction in `re-export.integration.test.ts`; but it counted tool-managed paths as "unowned siblings" and offered no enforcement). The notice now names only genuinely unmanaged adjacent files — generated branding paths and furnace-managed prefixes are excluded, while a brand-new unowned file under the branding root stays listed (the Assets.car precedent) — with the wording `found N unmanaged file(s) adjacent to this patch's ownership (<first 3>)`. Under `--refuse-adjacent-unmanaged` (scan-less path only; refused alongside `--scan`/`--files`) each offending patch is skipped unwritten and the run fails with one refusal naming every offender, so CI cannot ship a patch refresh past an unadopted file. Residual blind spot documented in-test: a new file in a cousin directory no owned file lives in is still unreported (recursive scans are too noisy on Firefox-sized trees). Pinned in `re-export.test.ts` (branding/furnace exclusion, skip-and-refuse) and `re-export.integration.test.ts`.
|
|
147
|
+
- **`re-export` now retries once, after a 300 ms backoff, when a patch fails on transient git `index.lock` contention** (bug — an external git process touching `engine/.git/index.lock` during the binary-staging `git add -N`/`git diff --binary` window permanently failed that patch while the run reported `Re-exported 1 of 2` and exited 0; the identical immediate rerun succeeded with no hand cleanup, which is exactly the shape an internal retry should absorb): lock-class failures are detected via the new `isGitIndexLockConflict` heuristic in `errors/git.ts` (a `GitIndexLockError`, or a `GitError` whose message pairs `index.lock` with lock phrasing), the retry re-enters the per-patch export from the top so it re-reads clean state, and a second failure keeps the honest `N of M` summary. Non-lock failures never retry. Partial-failure exit code is intentionally unchanged this release; a non-zero exit (or `--strict`) for partial runs is the recommended follow-up. Pinned by the retry, double-failure, partial-success, and no-retry cases in `re-export.test.ts` and the heuristic table in `git-index-lock.test.ts`.
|
|
148
|
+
- **`export --before/--after` no longer refuses placements whose renumber never actually reaches a reserved range** (bug — `computePlacementPlan` shifted every patch at or above the insertion point by +1, so any positional insert below a `patchPolicy.reservedRanges` block dragged the reserved patches into the rename map and tripped the up-front refusal even with free ordinals in between; in a ~230-patch tree with a 900-999 reserved block, `--before`/`--after` were effectively unavailable and every insert needed an explicit `--order`): the +1 cascade now stops at the first unoccupied ordinal — a gap absorbs the insert, so the tail beyond it (including any reserved block) keeps its numbering — while a genuinely contiguous run into the block still refuses with the same single up-front error. The same cascade serves `patch split` and `patch move-files --create` placements. Pinned by the stop-at-gap and contiguous-refusal cases in `export-placement-policy.test.ts` and the rewritten sparse-queue case in `export-flow.test.ts`.
|
|
102
149
|
|
|
103
150
|
## 0.39.0
|
|
104
151
|
|
|
105
|
-
- **`fireforge discard` restores patch-claimed paths to their PATCH-APPLIED baseline instead of pristine upstream, and re-materializes patch-created files instead of deleting them** (
|
|
106
|
-
- **New per-patch lint check `mozbuild-unsorted-list` catches unsorted `EXTRA_JS_MODULES` (and sibling strict-ordered moz.build lists) before any build is dispatched** (
|
|
107
|
-
- **Forward-import detection now catches a bare getter-property line added to an existing `defineESModuleGetters` map, and every refusal names the exact `patch staged-dependency` invocation to paste** (
|
|
108
|
-
- **`patch move-files <from> <to>` into an EXISTING patch is now a real transaction** (
|
|
109
|
-
- **`lint --per-patch --max-warnings 0` on the cache-hit path: claim REFUTED; two warm-run gaps hardened** (
|
|
110
|
-
- **`patch lint-ignore --add` warns that the waiver is subject to the project's patch-policy review process** (
|
|
111
|
-
- **The mixed-harness refusal runs BEFORE the pre-test build** (
|
|
112
|
-
- **`token add --mode override` also writes existing `:root[data-theme="dark"]` / `:root[data-theme="light"]` blocks** (
|
|
113
|
-
- **A `.patch` extension on a patch-name argument no longer double-suffixes, and `patch rename` explains its no-ops** (
|
|
114
|
-
- **xpcshell-only runs skip the Marionette port preflight and the mochitest client flags** (
|
|
115
|
-
- **New read-only coverage reporter: `fireforge status --test-coverage`** (
|
|
116
|
-
- **`typecheck` and the checkJs lint pass no longer silently suppress undefined free identifiers (TS2304/TS2552)** (
|
|
117
|
-
- **`reset` and `import` warn at completion when `components.conf` diverged from the last full build** (
|
|
118
|
-
- **The forward-import "Closest legal ordinal" hint is suppressed when no legal ordinal exists in the importing patch's category range** (
|
|
119
|
-
- **`export` never offers to prepend the project license header onto a vendored third-party file** (
|
|
120
|
-
- **Signal-shaped configure/build exits are labeled as external interruptions** (
|
|
121
|
-
- **Headed macOS no-output timeouts hint at display sleep and `caffeinate`** (
|
|
152
|
+
- **`fireforge discard` restores patch-claimed paths to their PATCH-APPLIED baseline instead of pristine upstream, and re-materializes patch-created files instead of deleting them** (P0 data-loss bug — 15 `patch-backed`/`owned` engine files discarded with `--yes` came back as upstream Firefox artwork, `jar.mn` lost all 134 fork entries, and patch-created files like `hominis-start.html` were deleted outright, all under the message "File restored to original state"; recovery cost a full `reset` + 207-patch `import` + rebuild): discard now consults the manifest's ownership claims per status entry and restores claimed files by folding HEAD content through every affecting patch in order (`computePatchedContent`), leaving the post-`import` worktree convention intact (patch edits stay unstaged ` M`, patch creations stay untracked `??`); patch-deleted files are removed to match the baseline. The legacy semantics are an explicit opt-in via the new `--to-upstream` flag. Success messages name the baseline (`File restored to patch baseline (0001-ui-edit.patch)`, `File re-materialized from patch baseline (…)`, `File restored to pristine upstream (HEAD)`); directory mode applies the same rule per file and its outro buckets the outcomes (`3 file(s) restored: 1 to patch baseline, 1 re-materialized, 1 to upstream state`); dry-run lines annotate each path with its restore target. A corrupt manifest or an unreconstructible baseline REFUSES (naming `--to-upstream`) rather than silently reverting to HEAD; multi-owner (conflict) claims restore the cumulative baseline with a warning pointing at `status --ownership`. Pinned by `discard-patch-baseline.integration.test.ts` (the exact handoff shape: edit-patch + create-patch, dirty both, discard each, assert patch content and git state) and `discard-baseline.test.ts`.
|
|
153
|
+
- **New per-patch lint check `mozbuild-unsorted-list` catches unsorted `EXTRA_JS_MODULES` (and sibling strict-ordered moz.build lists) before any build is dispatched** (feature — three friction-log occurrences where an unsorted insertion surfaced only as `mozbuild.util.UnsortedError … expected "X" but got "Y"` at configure time, each costing a full multi-minute build cycle): the check reads each patch-touched `moz.build` from the engine tree and reports the exact expected/got pair mozbuild would raise (`moz.build list EXTRA_JS_MODULES is not alphabetically sorted: expected "X" but got "Y" (mozbuild will fail at configure time with UnsortedError)`), using mozbuild's case-insensitive ordering. Covered variables: `EXTRA_COMPONENTS`, `EXTRA_JS_MODULES`, `EXTRA_PP_COMPONENTS`, `EXPORTS`, `PYTHON_UNITTEST_MANIFESTS`, `TESTING_JS_MODULES`, `XPIDL_SOURCES`, including dotted namespaces (`EXTRA_JS_MODULES.<subdir>`); `DIRS` is deliberately excluded (order-meaningful). Severity error; waivable per patch via `lintIgnore: ["mozbuild-unsorted-list"]`; participates in the per-patch lint cache like every other rule.
|
|
154
|
+
- **Forward-import detection now catches a bare getter-property line added to an existing `defineESModuleGetters` map, and every refusal names the exact `patch staged-dependency` invocation to paste** (bug + message — a patch inserting `Foo: "resource://gre/modules/Foo.sys.mjs",` into a PRE-EXISTING getter object carried no `defineESModuleGetters(` opener in its added lines, so the export gate missed it and the refusal surfaced one consumer at a time downstream): added-lines-only content is additionally scanned for `Name: "resource://…"` property lines (resource/chrome/moz-src schemes, forward-importable extensions only; deduped against the existing extractors so a patch adding the whole call reports once; the `// fireforge-ignore: forward-import` marker and staged-dependency suppression apply unchanged). The `forward-import` message now embeds the exact per-owner remedy — `fireforge patch staged-dependency <patch> --add --file <site> --specifier "<url>" --creates <path> --owner <patch>` — with the fingerprint unchanged. The "reports only the first offender" half of the field report did NOT reproduce on 0.38.0 code: the rule already emits one issue per site and the export gate returns all details (pinned by a two-site test); the observed one-at-a-time behavior was an artifact of the getter shapes being missed.
|
|
155
|
+
- **`patch move-files <from> <to>` into an EXISTING patch is now a real transaction** (feature — the preview-only flow printed two `re-export --files` commands in the WRONG order (grow first) and without the `--allow-shrink`/`--yes` flags the real execution then demanded, costing three refusals per move): the two-positional form now performs the shrink→grow as one operation under the patch-directory lock with rollback, mirroring `patch split`/`--create`: both bodies are regenerated from the worktree, per-patch linted with each patch's own `lintIgnore`/`tier`, cross-patch linted as a projection (only regressions the move itself introduces refuse; `--force-unsafe` overrides), staged-dependency owners re-point to the target, and forward edges the move introduces are auto-declared. `--dry-run` previews without writing; `-y/--yes` covers non-TTY. The equivalent manual `re-export` commands are still printed — now in EXECUTABLE order (shrink first, flags included) with a note that the shrink must land first.
|
|
156
|
+
- **`lint --per-patch --max-warnings 0` on the cache-hit path: claim REFUTED; two warm-run gaps hardened** (— the reported 0-exit-despite-warnings was observed once on a 0.37-line host and does not reproduce on 0.38.0+: cached issues flow through the same `--max-warnings` comparison as fresh ones, pinned since 0.38.0 by the "cached warnings still fail per-patch lint" test; the cache key hashes every affected engine file plus the engine HEAD SHA, so an engine-side revert that would empty a patch's diff always misses the cache — now pinned explicitly). One REAL warm-run gap is fixed: an all-cache-hit run previously dropped run-level checkJs errors entirely (e.g. "typescript not installed", unreadable extra shim), letting a warm run report fewer errors than a cold one; global checkJs findings are now emitted whenever checkJs is configured, building the queue-wide program lazily on demand.
|
|
157
|
+
- **`patch lint-ignore --add` warns that the waiver is subject to the project's patch-policy review process** (message-only — a size-only waiver turned per-patch lint green and then failed a downstream policy audit as `lint-ignore-unreviewed`, a discovery costing a full multi-minute gate run): every `--add` that lands a new check id (including `--dry-run`) prints: "This lint waiver is subject to the project's patch-policy review process. Downstream policy audits that mirror lintIgnore lists in a reviewed allow-map will refuse this patch until the map is updated to include the new ID(s)." No warning on `--remove`, `--clear`, or a no-op add. (Per the upstream adjudication, the audit demanding the patch's WHOLE lintIgnore list in the review map remains intentional; this is the write-time warning only.)
|
|
158
|
+
- **The mixed-harness refusal runs BEFORE the pre-test build** (inefficiency — `fireforge test <xpcshell file> <browser file> --build` spent the full build before reporting "cannot run xpcshell and browser/mochitest paths in the same mach invocation"): harness classification is a pure path→manifest lookup and now runs immediately after path normalization, ahead of the `--build` dispatch and the stale/coverage gates; a mixed request refuses in milliseconds and `runProtectedMachBuild` is never invoked (pinned by test). Stale/coverage refusals still take precedence over missing-path errors.
|
|
159
|
+
- **`token add --mode override` also writes existing `:root[data-theme="dark"]` / `:root[data-theme="light"]` blocks** (bug — an override add wrote `:root` and the `@media (prefers-color-scheme: dark)` block but not the explicit theme-attribute blocks, so on scaffolds requiring identical token sets across all themed lists every override add was a guaranteed half-finished edit failing the downstream invariants audit): when the tokens CSS carries top-level `:root[data-theme="dark"]`/`:root[data-theme="light"]` blocks, the dark value is mirrored into the dark block and the base value into the light block (idempotent per block; files without those blocks are unchanged), with an info line naming the blocks written. The report's claim that the FireForge-generated file header documents the CLI "cannot write" those blocks did not match this repo — no such text exists in FireForge's scaffold (it is consumer-authored); nothing to drop here.
|
|
160
|
+
- **A `.patch` extension on a patch-name argument no longer double-suffixes, and `patch rename` explains its no-ops** (bug + partial refutation — `patch move-files <from> 348-ui-editor-panels.patch --create` produced `348-ui-editor-panels-patch.patch`, and the repair `rename --to 348-ui-editor-panels.patch` reported "Patch rename (no-op)"): `patchNameSlug` now strips one trailing `.patch` (case-insensitive) before sanitisation, converging `export --name`, `patch split --name`, `move-files --create`, and `rename --to` — so the double-suffix never forms and the repair rename is a REAL rename. A true no-op now says why (`nothing to change — filename already "…" (slug of "X" is "Y"), name already "…"`). The claimed positional `<from> <to>` usage string on `rename` was REFUTED: registration is `rename <name>` with a required `--to` flag on 0.38.0.
|
|
161
|
+
- **xpcshell-only runs skip the Marionette port preflight and the mochitest client flags** (bug — `fireforge test` probed port 2828 even for xpcshell path sets, so a developer's interactive browser holding the port killed xpcshell runs, and `--marionette-port <n>` alone then failed the dispatch because the mochitest-only client flags were forwarded to mach; the only working incantation was the `--mach-arg --flavor=xpcshell` crutch): when manifest classification shows every requested path is xpcshell, the stale-port preflight is skipped (with an explanatory line when `--marionette-port`/`--kill-stale-marionette` were passed) and neither `--setpref=marionette.port` nor `--marionette=127.0.0.1:<n>` is forwarded — the crutch is unnecessary. Pathless runs and `--doctor` (whose probe launches a Marionette browser regardless) keep the preflight.
|
|
162
|
+
- **New read-only coverage reporter: `fireforge status --test-coverage`** (feature — with concurrent sessions sharing one engine tree, the only way to learn the current recorded test-packaging coverage was to trip the out-of-coverage refusal on a real run): prints the last build baseline's coverage (`full`, or the scoped path list with the refusal implication), `builtAt`, `binaryName`, engine HEAD, and the new `recordedBy` field (`fireforge build`, `fireforge build --ui`, `fireforge test --build <paths>`) that 0.39.0 baselines now record — older baselines render `Recorded by: unknown` and `full (implicit — baseline predates coverage recording)`. Needs only the project root (no engine/git); mutually exclusive with the other status modes. Per the upstream adjudication, coverage REPLACES — no union model; this is strictly a reporter.
|
|
163
|
+
- **`typecheck` and the checkJs lint pass no longer silently suppress undefined free identifiers (TS2304/TS2552)** (bug — a deployed module referencing `EditorState.create(...)` with no import typechecked as "0 errors across all three projects" because the codes were unconditionally suppressed as globals-shim noise; the failure surfaced as a runtime `ReferenceError` manifesting as a hung suite): both flows now report undefined identifiers at a configurable severity — default `warning` (visible without breaking gates; blocking under `--max-warnings 0`), `error` to make them hard failures, `off` to restore the historical suppression — via `typecheck.undefinedIdentifiers` and `patchLint.undefinedIdentifiers` in fireforge.json. Shim-covered Firefox globals stay clean; reported diagnostics append a hint naming the extra-shim and config remedies. The other module-resolution suppressions (TS2307/2306/2305/2792/2580/7016) are unchanged.
|
|
164
|
+
- **`reset` and `import` warn at completion when `components.conf` diverged from the last full build** (message — after a `reset` + `import` revert train, the next gate run's unit sweep refused on the stale compiled StaticComponents table one full rebuild too late): both commands now run the existing static-components probe after a successful mutation and print: "components.conf changed relative to the last full \"fireforge build\": <paths>. The compiled StaticComponents table in obj-\* no longer matches the tree, so the next \"fireforge test\" will require a full \"fireforge build\" first — a scoped \"fireforge test --build\" cannot regenerate the compiled table." Advisory only (never throws, silent without a baseline); dry-run/cancelled/failed paths skip it.
|
|
165
|
+
- **The forward-import "Closest legal ordinal" hint is suppressed when no legal ordinal exists in the importing patch's category range** (message bug — the hint suggested ordinal 388 for a dependency that no reorder could satisfy, sending the operator into a `patch reorder` the category-range policy then refused): when `patchPolicy` is configured and the computed ordinal falls outside the importing patch's category range, the sentence becomes "No legal ordinal in the <category> category range (<from-to>) lands after the creating patch(es); declaring the staged dependency is the recommended remedy." Fingerprints are unchanged across both variants; without a policy the hint prints as before.
|
|
166
|
+
- **`export` never offers to prepend the project license header onto a vendored third-party file** (bug — the header auto-fix offered EUPL-1.2 headers for three byte-identity-pinned MIT xterm bundles; accepting (or `-y`) would have mislicensed upstream code): new-file candidates carrying a recognized third-party permissive banner (MIT, ISC, BSD-2/3-Clause, Apache-2.0 — by license text markers or a bare `SPDX-License-Identifier`) are excluded from the offer and the dry-run fix list and reported separately: "N new file(s) carry a third-party permissive license banner and were left untouched (vendored); if the missing-license-header lint check flags them, waive it with --lint-ignore missing-license-header". The lint check itself still fires — the reviewed waiver (expressible at creation since 0.38.0's `export --lint-ignore`) remains the sanctioned path.
|
|
167
|
+
- **Signal-shaped configure/build exits are labeled as external interruptions** (— the reported `--headless`→configure exit-144 mechanism was REFUTED: `--headless` only ever lands in mach TEST args and `mach configure` receives no environment from the test path, so no plausible interaction exists): configure and build failures with exit codes in the 128+N signal convention now append e.g. "Exit 144 is signal-shaped (144 - 128 = 16, SIGURG on this host): the process was likely interrupted externally (OOM-killer, terminal disconnect, display/session teardown) rather than failing on its own, and the log may be truncated mid-step", using the host's real signal table — so the next occurrence is self-describing.
|
|
168
|
+
- **Headed macOS no-output timeouts hint at display sleep and `caffeinate`** (message-only — a headed single-file run with no `caffeinate` wrapper timed out at 370 s before the first TEST-START; every subsequent `caffeinate -dimsu`-wrapped run passed): when a harness crash with the no-output-timeout shape came from a HEADED run on macOS, the failure message appends: "Hint: this was a HEADED run on macOS that died at the no-output timeout. A common cause is the display going to sleep mid-run, which freezes the headed browser before any test starts. Wrap headed runs in `caffeinate -dimsu <command>` to keep the display awake, or pass --headless." Auto-wrapping dispatches in `caffeinate` was evaluated and DEFERRED — the message half is the clearly-correct, cheap half.
|
|
122
169
|
|
|
123
170
|
## 0.38.0
|
|
124
171
|
|
|
@@ -191,7 +238,7 @@
|
|
|
191
238
|
- Changed `fireforge test <directory>` to select **exactly the named directory**: mach resolves test paths by string prefix, so a bare directory argument silently swept in prefix-named siblings (drill: `…/test/hominis` also ran `…/test/hominis-tiles` — 1224 tests instead of ~200, with no indication the scope widened). FireForge now normalizes directory arguments with a trailing `/` (which makes the prefix match exact) and, when prefix-matching sibling test directories exist, echoes what was selected and what was excluded with per-directory test-file counts plus how to include them as separate paths. Since FireForge already rejected non-existent paths, raw prefix-widening was never something an operator could invoke deliberately — it only ever happened by accident. Documented in `test --help`.
|
|
192
239
|
- Made per-file test sharding **announce itself**: multi-path `fireforge test` runs print one notice that N paths run in isolated browser instances (one mach invocation per path) and that cross-file state is NOT exercised, pointing at `--no-shard` for a combined single-instance run. The silent default cost a real misdiagnosis during the drill: a two-file cross-file pollution repro "passed" because the headed comparison run was silently sharded, briefly blaming a suite-context bug on an upstream headless regression. The `--no-shard` help text now carries the same caveat.
|
|
193
240
|
- Added a **headed-smoke contamination notice and `fireforge run --headless`**: when `--smoke-exit` launches headed on a non-CI host (CI env var unset), a one-line warning states that keyboard/mouse input during the window will contaminate the console capture and can fail the run (drill: a human interacted with the smoke window, the password-manager import scan probed an unreadable Chrome profile dir, and the resulting console errors initially read as a product regression). The new `--headless` flag forwards `--headless` to the browser in both smoke and plain launch modes for unattended checks.
|
|
194
|
-
- Accepted the **verbatim upstream MPL-2.0 block header on new JS files regardless of the project license**: the `missing-license-header` carve-out whose own comment describes the copied-from-upstream case was gated on `license === 'MPL-2.0'`, making it dead code for EUPL/GPL/0BSD projects — a file legitimately carrying Mozilla's block header (including behind a leading editor-directive comment) had no sanctioned path and consumers carried repo-side audit workarounds
|
|
241
|
+
- Accepted the **verbatim upstream MPL-2.0 block header on new JS files regardless of the project license**: the `missing-license-header` carve-out whose own comment describes the copied-from-upstream case was gated on `license === 'MPL-2.0'`, making it dead code for EUPL/GPL/0BSD projects — a file legitimately carrying Mozilla's block header (including behind a leading editor-directive comment) had no sanctioned path and consumers carried repo-side audit workarounds. Only the upstream `/* … */` block form is accepted; the `// `-style MPL header FireForge generates for MPL projects still requires the project's own header on non-MPL projects, and non-JS styles are unchanged.
|
|
195
242
|
- Added targeted tests and per-module coverage threshold pins for under-covered edge modules that the global threshold was masking: `signal-critical.ts` (first direct tests: body passthrough/cleanup, multi-section waits, bounded timeout, throwing sections), `bootstrap-checks.ts` (bootstrap output pattern detection incl. traceback/403 collapse and all origin-remote phrasings; SDK-probe branching), `discard.ts` (directory-recursion fallback incl. sibling-prefix safety, batch partial failures, dry-run/confirmation paths, Furnace-managed warnings), and the `furnace-validate-compatibility.ts` compose/CSS-hygiene warnings (`compose-not-referenced`/`compose-not-registered`, `excessive-important`, `missing-reduced-motion`).
|
|
196
243
|
|
|
197
244
|
## 0.34.0
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.
|
|
4
|
-
"commit": "
|
|
5
|
-
"shortCommit": "
|
|
3
|
+
"version": "0.43.0",
|
|
4
|
+
"commit": "8229f32cf3e1fee429a82966440480de7618bf39",
|
|
5
|
+
"shortCommit": "8229f32cf3e1",
|
|
6
6
|
"dirty": false,
|
|
7
7
|
"dirtyHash": null,
|
|
8
|
-
"buildTime": "2026-08-
|
|
8
|
+
"buildTime": "2026-08-20T14:49:17.057Z"
|
|
9
9
|
}
|
package/dist/src/cli.js
CHANGED
|
@@ -4,12 +4,13 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
4
4
|
import { Command, Help } from 'commander';
|
|
5
5
|
import { COMMAND_MANIFEST } from './commands/manifest.js';
|
|
6
6
|
import { runTreeGuardHook } from './core/tree-guard.js';
|
|
7
|
-
import { CancellationError, CommandError, FireForgeError } from './errors/base.js';
|
|
7
|
+
import { CancellationError, CommandError, FireForgeError, InternalInvariantError, } from './errors/base.js';
|
|
8
8
|
import { ExitCode } from './errors/codes.js';
|
|
9
9
|
import { ConfigNotFoundError } from './errors/config.js';
|
|
10
10
|
import { getCliVersion } from './utils/build-info.js';
|
|
11
11
|
import { toError } from './utils/errors.js';
|
|
12
12
|
import { cancel, error as logError, setMachineOutputMode, setStdoutSealed, setVerbose, } from './utils/logger.js';
|
|
13
|
+
import { ensureWaitLockOptionEverywhere } from './utils/options.js';
|
|
13
14
|
const brokenPipeInstalledKey = Symbol.for('fireforge.cli.brokenPipeHandlerInstalled');
|
|
14
15
|
const brokenPipeListenerKey = Symbol.for('fireforge.cli.brokenPipeHandlerListener');
|
|
15
16
|
function getProcessState() {
|
|
@@ -113,6 +114,17 @@ export function withErrorHandling(handler) {
|
|
|
113
114
|
// a real failure (which exits 1).
|
|
114
115
|
throw new CommandError(ExitCode.USER_CANCELLED);
|
|
115
116
|
}
|
|
117
|
+
// An invariant failure is the one FireForgeError whose stack is part
|
|
118
|
+
// of the report: the userMessage asks the operator to file the run,
|
|
119
|
+
// and without the stack there is nothing in it that locates the bug.
|
|
120
|
+
// console.error keeps it on stderr, so a --json payload stays intact.
|
|
121
|
+
if (error instanceof InternalInvariantError) {
|
|
122
|
+
logError(error.userMessage);
|
|
123
|
+
if (error.stack) {
|
|
124
|
+
console.error(error.stack);
|
|
125
|
+
}
|
|
126
|
+
throw new CommandError(error.code);
|
|
127
|
+
}
|
|
116
128
|
if (error instanceof FireForgeError) {
|
|
117
129
|
logError(error.userMessage);
|
|
118
130
|
throw new CommandError(error.code);
|
|
@@ -269,7 +281,7 @@ export function createProgram() {
|
|
|
269
281
|
setVerbose(true);
|
|
270
282
|
}
|
|
271
283
|
})
|
|
272
|
-
// Verification-tree guard
|
|
284
|
+
// Verification-tree guard: when the cwd resolves into a
|
|
273
285
|
// tree snapshot, refuse mutating commands before their action runs —
|
|
274
286
|
// one hook covers every command regardless of how the cwd got there.
|
|
275
287
|
.hook('preAction', async (thisCommand, actionCommand) => {
|
|
@@ -283,6 +295,13 @@ export function createProgram() {
|
|
|
283
295
|
for (const entry of COMMAND_MANIFEST) {
|
|
284
296
|
entry.register(program, ctx);
|
|
285
297
|
}
|
|
298
|
+
// Uniform `--wait-lock`: scripted sequences blanket-append the flag, and
|
|
299
|
+
// a subcommand that rejects it with "unknown option" kills the sequence
|
|
300
|
+
// with a usage error instead of a lock message. Applied after
|
|
301
|
+
// registration so it covers every subcommand at every depth without each
|
|
302
|
+
// registrar having to remember; commands that already declare the
|
|
303
|
+
// honoring flag keep it untouched.
|
|
304
|
+
ensureWaitLockOptionEverywhere(program);
|
|
286
305
|
return program;
|
|
287
306
|
}
|
|
288
307
|
/**
|
|
@@ -239,7 +239,12 @@ export async function buildCommand(projectRoot, options) {
|
|
|
239
239
|
// baseline to diff engine changes against dist artifacts.
|
|
240
240
|
const previousBaseline = await readBuildBaseline(projectRoot);
|
|
241
241
|
// Shared pre-flight: branding, Furnace, mozconfig, auto-configure
|
|
242
|
-
await prepareBuildEnvironment(projectRoot, paths, config, {
|
|
242
|
+
await prepareBuildEnvironment(projectRoot, paths, config, {
|
|
243
|
+
previousBaseline,
|
|
244
|
+
...(options.refuseUnexportedDrift !== undefined
|
|
245
|
+
? { refuseUnexportedDrift: options.refuseUnexportedDrift }
|
|
246
|
+
: {}),
|
|
247
|
+
});
|
|
243
248
|
const jobs = resolveJobCount(options, config.build?.jobs);
|
|
244
249
|
// Run build
|
|
245
250
|
info(`Starting ${buildType.toLowerCase()} build...`);
|
|
@@ -348,6 +353,7 @@ export function registerBuild(program, { getProjectRoot, withErrorHandling }) {
|
|
|
348
353
|
.option('-j, --jobs <n>', 'Number of parallel jobs', parseJobCount)
|
|
349
354
|
.option('--brand <name>', 'Build specific brand')
|
|
350
355
|
.option('--rewrite-mozinfo', 'On a mozinfo path mismatch, patch mozinfo.json paths in place and run mach configure instead of aborting with a clean-rebuild instruction. Refuses anything that is not a pure prefix-move.')
|
|
356
|
+
.option('--refuse-unexported-drift', 'Refuse (instead of warning) when the build would overwrite engine content recorded in neither a patch body nor the pristine baseline — the multi-session hazard where an unexported edit is silently reverted and a later re-export captures a hybrid')
|
|
351
357
|
.addHelpText('after', [
|
|
352
358
|
'',
|
|
353
359
|
'Furnace apply runs automatically before the build step, so edits in',
|
|
@@ -6,23 +6,20 @@ import { ConfigError } from '../errors/config.js';
|
|
|
6
6
|
import { toError } from '../utils/errors.js';
|
|
7
7
|
import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
8
8
|
import { pickDefined } from '../utils/options.js';
|
|
9
|
+
import { isJsonObject } from '../utils/validation.js';
|
|
9
10
|
/**
|
|
10
|
-
* Gets a nested value from
|
|
11
|
-
* @param
|
|
11
|
+
* Gets a nested value from a raw config document using dot notation.
|
|
12
|
+
* @param doc - Document to traverse
|
|
12
13
|
* @param path - Dot-separated path (e.g., "firefox.version")
|
|
13
14
|
* @returns The value at the path, or undefined if not found
|
|
14
15
|
*/
|
|
15
|
-
function getNestedValue(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (current === null || current === undefined) {
|
|
16
|
+
function getNestedValue(doc, path) {
|
|
17
|
+
let current = doc;
|
|
18
|
+
for (const part of path.split('.')) {
|
|
19
|
+
if (current === null || current === undefined || typeof current !== 'object') {
|
|
20
20
|
return undefined;
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
return undefined;
|
|
24
|
-
}
|
|
25
|
-
current = current[part];
|
|
22
|
+
current = Array.isArray(current) ? current[Number(part)] : current[part];
|
|
26
23
|
}
|
|
27
24
|
return current;
|
|
28
25
|
}
|
|
@@ -52,6 +49,7 @@ function parseValue(value, key) {
|
|
|
52
49
|
}
|
|
53
50
|
// Try to parse as JSON first (handles numbers, booleans, arrays, objects).
|
|
54
51
|
try {
|
|
52
|
+
// JSON.parse can only ever produce JSON values.
|
|
55
53
|
const parsed = JSON.parse(value);
|
|
56
54
|
if (typeof parsed !== 'string') {
|
|
57
55
|
warn(`Value "${value}" was interpreted as ${typeof parsed}. Use '"${value}"' for a string.`);
|
|
@@ -73,12 +71,9 @@ function formatValue(value) {
|
|
|
73
71
|
}
|
|
74
72
|
if (typeof value === 'string')
|
|
75
73
|
return value;
|
|
76
|
-
if (typeof value === 'number' || typeof value === 'boolean'
|
|
74
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
77
75
|
return String(value);
|
|
78
76
|
}
|
|
79
|
-
if (typeof value === 'symbol') {
|
|
80
|
-
return value.toString();
|
|
81
|
-
}
|
|
82
77
|
return JSON.stringify(value, null, 2);
|
|
83
78
|
}
|
|
84
79
|
/**
|
|
@@ -214,12 +209,6 @@ export async function configCommand(projectRoot, key, value, options = {}) {
|
|
|
214
209
|
function deepEqual(a, b) {
|
|
215
210
|
if (a === b)
|
|
216
211
|
return true;
|
|
217
|
-
if (a === null || b === null)
|
|
218
|
-
return a === b;
|
|
219
|
-
if (typeof a !== typeof b)
|
|
220
|
-
return false;
|
|
221
|
-
if (typeof a !== 'object')
|
|
222
|
-
return false;
|
|
223
212
|
if (Array.isArray(a)) {
|
|
224
213
|
if (!Array.isArray(b))
|
|
225
214
|
return false;
|
|
@@ -227,15 +216,15 @@ function deepEqual(a, b) {
|
|
|
227
216
|
return false;
|
|
228
217
|
return a.every((v, i) => deepEqual(v, b[i]));
|
|
229
218
|
}
|
|
230
|
-
if (
|
|
219
|
+
if (!isJsonObject(a) || !isJsonObject(b)) {
|
|
220
|
+
// Primitives (and null) compare by identity, handled above.
|
|
231
221
|
return false;
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
const keysB = Object.keys(br);
|
|
222
|
+
}
|
|
223
|
+
const keysA = Object.keys(a);
|
|
224
|
+
const keysB = Object.keys(b);
|
|
236
225
|
if (keysA.length !== keysB.length)
|
|
237
226
|
return false;
|
|
238
|
-
return keysA.every((k) => deepEqual(
|
|
227
|
+
return keysA.every((k) => deepEqual(a[k], b[k]));
|
|
239
228
|
}
|
|
240
229
|
/** Registers the config command on the CLI program. */
|
|
241
230
|
export function registerConfig(program, { getProjectRoot, withErrorHandling }) {
|
|
@@ -31,7 +31,7 @@ async function confirmDiscard(message, hint, options) {
|
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
33
|
* Plans the restore baseline for `entries`: the patch-applied baseline for
|
|
34
|
-
* patch-claimed paths
|
|
34
|
+
* patch-claimed paths, or pristine HEAD when `--to-upstream`
|
|
35
35
|
* explicitly requests the legacy semantics.
|
|
36
36
|
*/
|
|
37
37
|
async function planDiscards(patchesDir, engineDir, entries, options) {
|
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import {} from '../core/destructive.js';
|
|
12
|
+
import { detectMovedCodeOverlaps, formatAdoptThenSplitRemedy, } from '../core/export-moved-code-shape.js';
|
|
12
13
|
import { normalizePatchArtifact } from '../core/patch-artifact-normalize.js';
|
|
13
14
|
import { findAllPatchesForFilesWithDetails, patchNameSlug, planExport, } from '../core/patch-export.js';
|
|
14
15
|
import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, detectNewFilesInDiff, lintPatchQueue, } from '../core/patch-lint.js';
|
|
15
16
|
import { withPatchDirectoryLock } from '../core/patch-lock.js';
|
|
16
17
|
import { addPatchToManifest, loadPatchesManifest, loadPatchesManifestForWrite, renumberPatchesInManifest, resolvePatchIdentifier, savePatchesManifest, } from '../core/patch-manifest.js';
|
|
18
|
+
import { requirePatchOrder } from '../core/patch-parse.js';
|
|
17
19
|
import { applyRenameMapToManifest, buildProjectedManifest, enforcePatchPolicy, } from '../core/patch-policy.js';
|
|
18
20
|
import { extractNewFileContentFromDiff } from '../core/patch-transform.js';
|
|
19
21
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
@@ -217,15 +219,23 @@ export async function projectPlacementForLint(patchesDir, plan, diff, config) {
|
|
|
217
219
|
return null;
|
|
218
220
|
// Baseline lint over the UN-renumbered queue: an error present here is
|
|
219
221
|
// pre-existing breakage, not a consequence of this placement — calling
|
|
220
|
-
// it one would misattribute
|
|
222
|
+
// it one would misattribute. Entries are already in memory,
|
|
221
223
|
// so this second pass costs no additional IO.
|
|
222
224
|
const baselineIssues = lintPatchQueue({
|
|
223
225
|
entries: baseCtx.entries,
|
|
224
226
|
...policyRest,
|
|
225
227
|
}).filter((i) => i.severity === 'error');
|
|
228
|
+
// New-file + moved-code slice: when the refusal is caused by
|
|
229
|
+
// the source patch still carrying lines the new patch would gain, name
|
|
230
|
+
// the adopt-then-split sequence instead of leaving the operator to
|
|
231
|
+
// derive it from two individually-correct guards that together dead-end.
|
|
232
|
+
const movedCode = detectMovedCodeOverlaps(diff, baseCtx.entries);
|
|
226
233
|
return {
|
|
227
234
|
reason: `placement would introduce ${projectedIssues.length} cross-patch lint error(s)`,
|
|
228
|
-
details:
|
|
235
|
+
details: [
|
|
236
|
+
...groupProjectedPlacementErrors(projectedIssues, baselineIssues, plan),
|
|
237
|
+
...formatAdoptThenSplitRemedy(movedCode, plan.newFilename, plan.insertionOrder),
|
|
238
|
+
],
|
|
229
239
|
};
|
|
230
240
|
}
|
|
231
241
|
/**
|
|
@@ -343,7 +353,7 @@ export async function commitPlacementExport(input) {
|
|
|
343
353
|
// avoids tracking a second map during the forward pass.
|
|
344
354
|
const inverseMap = new Map();
|
|
345
355
|
for (const [oldFilename, entry] of currentPlan.renameMap) {
|
|
346
|
-
const oldOrder =
|
|
356
|
+
const oldOrder = requirePatchOrder(oldFilename);
|
|
347
357
|
inverseMap.set(entry.newFilename, {
|
|
348
358
|
newOrder: oldOrder,
|
|
349
359
|
newFilename: oldFilename,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
|
-
* Attribution of projected placement lint errors
|
|
3
|
+
* Attribution of projected placement lint errors, split out of
|
|
4
4
|
* export-flow.ts for the line budget. A placement projection lints one
|
|
5
5
|
* merged queue — the renumbered existing patches plus a synthetic entry
|
|
6
6
|
* for the pending patch — so its errors used to render as one flat list
|
|
@@ -28,7 +28,7 @@ export declare function runPatchLint(engineDir: string, filesAffected: string[],
|
|
|
28
28
|
/**
|
|
29
29
|
* Prints a patch-lint issue list with `runPatchLint`'s exact severity
|
|
30
30
|
* buckets and skip-lint semantics, throwing on errors unless `skipLint`.
|
|
31
|
-
* Factored out so `re-export`'s cached-lint path
|
|
31
|
+
* Factored out so `re-export`'s cached-lint path can replay
|
|
32
32
|
* stored issues through the identical reporter without re-linting.
|
|
33
33
|
*/
|
|
34
34
|
export declare function reportPatchLintOutcome(issues: PatchLintIssue[], skipLint?: boolean): void;
|