@hominis/fireforge 0.39.0 → 0.41.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 +100 -0
- package/README.md +35 -5
- package/dist/bin/fireforge.js +25 -4
- package/dist/build-info.json +9 -0
- package/dist/src/cli.js +27 -3
- package/dist/src/commands/bootstrap.js +5 -1
- package/dist/src/commands/discard.js +2 -22
- package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
- package/dist/src/commands/doctor-check-core.d.ts +20 -0
- package/dist/src/commands/doctor-check-core.js +28 -0
- package/dist/src/commands/doctor-external-toolchains.js +2 -0
- package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
- package/dist/src/commands/doctor-furnace.js +13 -11
- package/dist/src/commands/doctor-orphaned-harness.js +2 -1
- package/dist/src/commands/doctor.js +2 -2
- package/dist/src/commands/export-all.js +9 -1
- package/dist/src/commands/export-flow.js +27 -9
- package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
- package/dist/src/commands/export-placement-conflicts.js +74 -0
- package/dist/src/commands/export-placement-gate.js +1 -1
- package/dist/src/commands/export-shared.d.ts +8 -1
- package/dist/src/commands/export-shared.js +30 -10
- package/dist/src/commands/export.js +2 -2
- package/dist/src/commands/furnace/apply.js +4 -3
- package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
- package/dist/src/commands/furnace/chrome-doc.js +13 -2
- package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
- package/dist/src/commands/furnace/create-browser-test.js +5 -0
- package/dist/src/commands/furnace/create.js +2 -2
- package/dist/src/commands/furnace/diff.js +2 -1
- package/dist/src/commands/furnace/init.js +4 -6
- package/dist/src/commands/furnace/override.js +2 -2
- package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
- package/dist/src/commands/furnace/remove-mutations.js +171 -0
- package/dist/src/commands/furnace/remove.js +21 -128
- package/dist/src/commands/furnace/rename-browser-test.js +54 -5
- package/dist/src/commands/furnace/scan.js +27 -36
- package/dist/src/commands/furnace/status.js +30 -28
- package/dist/src/commands/furnace/validate.js +25 -18
- package/dist/src/commands/import.js +6 -20
- package/dist/src/commands/lint-per-patch.js +73 -51
- package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
- package/dist/src/commands/lint-per-run-checkjs.js +102 -0
- package/dist/src/commands/lint-report.d.ts +21 -0
- package/dist/src/commands/lint-report.js +62 -0
- package/dist/src/commands/lint.js +26 -22
- package/dist/src/commands/manifest.js +3 -1
- package/dist/src/commands/package.js +9 -16
- package/dist/src/commands/patch/compact.js +5 -5
- package/dist/src/commands/patch/delete.js +5 -5
- package/dist/src/commands/patch/lint-ignore.js +3 -2
- package/dist/src/commands/patch/move-files-create.js +22 -4
- package/dist/src/commands/patch/move-files-into.js +14 -10
- package/dist/src/commands/patch/move-files.js +6 -5
- package/dist/src/commands/patch/rename.d.ts +2 -2
- package/dist/src/commands/patch/rename.js +152 -51
- package/dist/src/commands/patch/reorder.d.ts +9 -0
- package/dist/src/commands/patch/reorder.js +14 -10
- package/dist/src/commands/patch/split-plan.d.ts +13 -6
- package/dist/src/commands/patch/split-plan.js +22 -12
- package/dist/src/commands/patch/split.d.ts +1 -1
- package/dist/src/commands/patch/split.js +23 -9
- package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
- package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
- package/dist/src/commands/patch/staged-dependency.js +112 -14
- package/dist/src/commands/re-export-adjacent.d.ts +34 -0
- package/dist/src/commands/re-export-adjacent.js +105 -0
- package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
- package/dist/src/commands/re-export-bulk-scan.js +142 -2
- package/dist/src/commands/re-export-drift.d.ts +74 -0
- package/dist/src/commands/re-export-drift.js +234 -0
- package/dist/src/commands/re-export-files.js +5 -3
- package/dist/src/commands/re-export-lint.d.ts +97 -0
- package/dist/src/commands/re-export-lint.js +210 -0
- package/dist/src/commands/re-export-options.js +11 -0
- package/dist/src/commands/re-export-register.d.ts +18 -0
- package/dist/src/commands/re-export-register.js +61 -0
- package/dist/src/commands/re-export-scan.js +13 -7
- package/dist/src/commands/re-export-single.d.ts +20 -0
- package/dist/src/commands/re-export-single.js +261 -0
- package/dist/src/commands/re-export.d.ts +0 -4
- package/dist/src/commands/re-export.js +123 -289
- package/dist/src/commands/rebase/abort.js +13 -4
- package/dist/src/commands/rebase/continue.js +11 -4
- package/dist/src/commands/rebase/index.js +19 -29
- package/dist/src/commands/rebase/patch-loop.js +12 -0
- package/dist/src/commands/reset.js +4 -23
- package/dist/src/commands/resolve.js +2 -9
- package/dist/src/commands/run.js +29 -23
- package/dist/src/commands/source.js +3 -9
- package/dist/src/commands/status-check.d.ts +48 -0
- package/dist/src/commands/status-check.js +106 -0
- package/dist/src/commands/status-json.d.ts +31 -0
- package/dist/src/commands/status-json.js +76 -0
- package/dist/src/commands/status-output.d.ts +1 -0
- package/dist/src/commands/status-output.js +10 -1
- package/dist/src/commands/status-ownership.d.ts +52 -0
- package/dist/src/commands/status-ownership.js +57 -0
- package/dist/src/commands/status.js +190 -170
- package/dist/src/commands/test-build-phase.d.ts +19 -0
- package/dist/src/commands/test-build-phase.js +168 -0
- package/dist/src/commands/test-diagnose.js +30 -2
- package/dist/src/commands/test-modes.d.ts +9 -1
- package/dist/src/commands/test-modes.js +46 -13
- package/dist/src/commands/test-register.js +59 -27
- package/dist/src/commands/test-run.d.ts +29 -3
- package/dist/src/commands/test-run.js +59 -7
- package/dist/src/commands/test-verdict.d.ts +32 -0
- package/dist/src/commands/test-verdict.js +48 -0
- package/dist/src/commands/test.d.ts +9 -0
- package/dist/src/commands/test.js +100 -123
- package/dist/src/commands/token-coverage.js +4 -9
- package/dist/src/commands/token.js +4 -1
- package/dist/src/commands/tree.d.ts +19 -0
- package/dist/src/commands/tree.js +320 -0
- package/dist/src/commands/verify.js +2 -2
- package/dist/src/commands/watch.js +22 -21
- package/dist/src/commands/wire.js +1 -1
- package/dist/src/core/ast-utils.d.ts +11 -0
- package/dist/src/core/ast-utils.js +11 -0
- package/dist/src/core/build-audit-platform.js +6 -1
- package/dist/src/core/build-audit-registration.js +2 -0
- package/dist/src/core/build-audit-resolve.d.ts +13 -0
- package/dist/src/core/build-audit-resolve.js +34 -13
- package/dist/src/core/build-audit.js +4 -20
- package/dist/src/core/build-baseline-types.d.ts +29 -8
- package/dist/src/core/build-baseline-types.js +2 -1
- package/dist/src/core/build-baseline.d.ts +10 -2
- package/dist/src/core/build-baseline.js +39 -8
- package/dist/src/core/build-prepare.d.ts +4 -0
- package/dist/src/core/build-prepare.js +7 -1
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-test-toolchains.js +3 -2
- package/dist/src/core/config-validate.js +45 -30
- package/dist/src/core/config.js +16 -3
- package/dist/src/core/coverage-extend.d.ts +89 -0
- package/dist/src/core/coverage-extend.js +188 -0
- package/dist/src/core/destructive.js +3 -1
- package/dist/src/core/discard-baseline.js +18 -4
- package/dist/src/core/engine-precondition.d.ts +35 -0
- package/dist/src/core/engine-precondition.js +50 -0
- package/dist/src/core/engine-session-lock.d.ts +38 -1
- package/dist/src/core/engine-session-lock.js +78 -4
- package/dist/src/core/file-lock.d.ts +7 -0
- package/dist/src/core/file-lock.js +20 -30
- package/dist/src/core/firefox.js +5 -2
- package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
- package/dist/src/core/furnace-apply-helpers.js +12 -3
- package/dist/src/core/furnace-apply-output.js +9 -0
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
- package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
- package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
- package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
- package/dist/src/core/furnace-apply.d.ts +16 -4
- package/dist/src/core/furnace-apply.js +39 -43
- package/dist/src/core/furnace-config.js +2 -0
- package/dist/src/core/furnace-css-fragments.js +2 -0
- package/dist/src/core/furnace-jsconfig.js +14 -7
- package/dist/src/core/furnace-registration-ast.js +8 -8
- package/dist/src/core/furnace-registration-remove.js +6 -3
- package/dist/src/core/furnace-registration.js +9 -12
- package/dist/src/core/furnace-rollback.js +1 -1
- package/dist/src/core/furnace-scanner.js +2 -1
- package/dist/src/core/furnace-stale-export.js +2 -1
- package/dist/src/core/furnace-state-persist.d.ts +4 -6
- package/dist/src/core/furnace-validate-compatibility.js +4 -7
- package/dist/src/core/furnace-validate-helpers.js +33 -1
- package/dist/src/core/furnace-validate-registration.js +16 -0
- package/dist/src/core/furnace-validate.js +4 -2
- package/dist/src/core/git-diff.js +37 -2
- package/dist/src/core/lint-cache.d.ts +15 -5
- package/dist/src/core/lint-cache.js +49 -14
- package/dist/src/core/mach-build-artifacts.d.ts +63 -1
- package/dist/src/core/mach-build-artifacts.js +108 -2
- package/dist/src/core/mach-resource-shim.js +1 -0
- package/dist/src/core/marionette-port.d.ts +16 -0
- package/dist/src/core/marionette-port.js +93 -8
- package/dist/src/core/marionette-preflight.js +6 -5
- package/dist/src/core/patch-apply.d.ts +20 -13
- package/dist/src/core/patch-apply.js +32 -33
- package/dist/src/core/patch-export.d.ts +2 -2
- package/dist/src/core/patch-export.js +4 -6
- package/dist/src/core/patch-lint-checkjs.d.ts +42 -2
- package/dist/src/core/patch-lint-checkjs.js +132 -8
- package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
- package/dist/src/core/patch-lint-creators.d.ts +30 -0
- package/dist/src/core/patch-lint-creators.js +45 -0
- package/dist/src/core/patch-lint-cross.d.ts +1 -17
- package/dist/src/core/patch-lint-cross.js +16 -32
- package/dist/src/core/patch-lint-css.js +64 -16
- package/dist/src/core/patch-lint-diff.d.ts +8 -0
- package/dist/src/core/patch-lint-diff.js +31 -0
- package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
- package/dist/src/core/patch-lint-jsdoc.js +15 -5
- package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
- package/dist/src/core/patch-lint-module-registration.js +88 -0
- package/dist/src/core/patch-lint-mozbuild.js +99 -11
- package/dist/src/core/patch-lint-ownership.d.ts +21 -0
- package/dist/src/core/patch-lint-ownership.js +30 -0
- package/dist/src/core/patch-lint-reexports.d.ts +2 -1
- package/dist/src/core/patch-lint-reexports.js +1 -0
- package/dist/src/core/patch-lint.d.ts +31 -0
- package/dist/src/core/patch-lint.js +114 -8
- package/dist/src/core/patch-lock.d.ts +12 -1
- package/dist/src/core/patch-lock.js +40 -4
- package/dist/src/core/patch-manifest-consistency.js +15 -3
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-parse.d.ts +8 -0
- package/dist/src/core/patch-parse.js +10 -0
- package/dist/src/core/patch-policy.d.ts +7 -0
- package/dist/src/core/patch-policy.js +4 -1
- package/dist/src/core/patch-transform.d.ts +10 -0
- package/dist/src/core/patch-transform.js +13 -2
- package/dist/src/core/rebase-session.d.ts +44 -2
- package/dist/src/core/rebase-session.js +114 -17
- package/dist/src/core/register-browser-content.js +2 -1
- package/dist/src/core/register-shared-css.js +2 -1
- package/dist/src/core/smoke-patterns.d.ts +24 -16
- package/dist/src/core/smoke-patterns.js +26 -14
- package/dist/src/core/status-binary.d.ts +55 -0
- package/dist/src/core/status-binary.js +124 -0
- package/dist/src/core/status-classify.d.ts +22 -1
- package/dist/src/core/status-classify.js +112 -70
- package/dist/src/core/test-harness-crash.d.ts +32 -1
- package/dist/src/core/test-harness-crash.js +59 -4
- package/dist/src/core/test-path-scope.js +2 -0
- package/dist/src/core/test-stale-check.js +1 -20
- package/dist/src/core/token-category.d.ts +40 -0
- package/dist/src/core/token-category.js +264 -0
- package/dist/src/core/token-manager.d.ts +7 -0
- package/dist/src/core/token-manager.js +48 -196
- package/dist/src/core/toolchain-preflight.js +24 -26
- package/dist/src/core/tree-cow.d.ts +18 -0
- package/dist/src/core/tree-cow.js +71 -0
- package/dist/src/core/tree-guard.d.ts +35 -0
- package/dist/src/core/tree-guard.js +179 -0
- package/dist/src/core/tree-store.d.ts +129 -0
- package/dist/src/core/tree-store.js +452 -0
- package/dist/src/core/typecheck-shim.d.ts +14 -0
- package/dist/src/core/typecheck-shim.js +41 -2
- package/dist/src/core/typecheck.js +2 -1
- package/dist/src/core/wire-destroy.js +14 -3
- package/dist/src/core/wire-init.js +37 -40
- package/dist/src/core/wire-subscript.js +11 -3
- package/dist/src/core/wire-utils.js +4 -4
- package/dist/src/core/xpcshell-appdir.js +3 -0
- package/dist/src/core/xpcshell-profile-dir.d.ts +12 -0
- package/dist/src/core/xpcshell-profile-dir.js +45 -0
- package/dist/src/errors/base.d.ts +13 -0
- package/dist/src/errors/base.js +13 -0
- package/dist/src/errors/git.d.ts +8 -0
- package/dist/src/errors/git.js +17 -0
- package/dist/src/errors/rebase.d.ts +13 -0
- package/dist/src/errors/rebase.js +17 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/types/commands/options.d.ts +119 -3
- package/dist/src/types/commands/patches.d.ts +8 -0
- package/dist/src/types/config.d.ts +19 -0
- package/dist/src/types/furnace.d.ts +7 -0
- package/dist/src/utils/build-info.d.ts +31 -0
- package/dist/src/utils/build-info.js +98 -0
- package/dist/src/utils/concurrency.d.ts +8 -0
- package/dist/src/utils/concurrency.js +21 -0
- package/dist/src/utils/errors.d.ts +39 -0
- package/dist/src/utils/errors.js +55 -0
- package/dist/src/utils/fs.js +5 -12
- package/dist/src/utils/logger.d.ts +14 -2
- package/dist/src/utils/logger.js +43 -13
- package/dist/src/utils/options.d.ts +21 -4
- package/dist/src/utils/options.js +26 -6
- package/dist/src/utils/package-root.js +7 -4
- package/dist/src/utils/process-group.js +13 -2
- package/dist/src/utils/stdio-drain.d.ts +28 -0
- package/dist/src/utils/stdio-drain.js +54 -0
- package/dist/src/utils/validation.d.ts +33 -2
- package/dist/src/utils/validation.js +57 -2
- package/package.json +13 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,105 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.41.0
|
|
4
|
+
|
|
5
|
+
### Release and build integrity
|
|
6
|
+
|
|
7
|
+
- Release publishing commits version bumps before building and enforces a clean tree with `FIREFORGE_PACK_STRICT=1`.
|
|
8
|
+
- Builds stamp `dist/build-info.json`; installed `--version` output includes the commit and dirty-content hash.
|
|
9
|
+
- Build baselines retain deletion tombstones, so removed packageable files and `components.conf` entries cannot look fresh.
|
|
10
|
+
- `test --build` uses a full build for new `jar.mn` registrations; mixed-harness `--build-only` packages all requested paths once.
|
|
11
|
+
- `--extend-coverage` unions scoped packaging coverage only while HEAD, packageable inputs, and `mozconfig` remain unchanged.
|
|
12
|
+
- Shared build and engine preflights now reject incomplete builds and unborn engine repositories consistently.
|
|
13
|
+
|
|
14
|
+
### Tests and verification trees
|
|
15
|
+
|
|
16
|
+
- Tests emit exactly one final `FIREFORGE-VERDICT:` line with the correct pass/failure reason, including sharded, lock-timeout, crash, and inconclusive runs.
|
|
17
|
+
- `tree exec` preserves the child verdict as the final stdout line; pre-spawn refusals emit no verdict.
|
|
18
|
+
- Engine-integrity checks run before successful verdicts.
|
|
19
|
+
- Browser preflight detects stale apps from the same objdir even after they release Marionette; `--kill-stale-marionette` can terminate them.
|
|
20
|
+
- Mach commands use process groups and reap surviving browser or harness descendants.
|
|
21
|
+
- `tree create --with-objdir` supports build-less tests after relocation and reconfiguration checks.
|
|
22
|
+
- Tree creation refuses symlinked objdirs, cleans failed clones, validates markers, and handles unreadable locks fail-closed.
|
|
23
|
+
- Tree freshness covers dirty engine content and patch bodies.
|
|
24
|
+
- Tree creation skips unwanted `obj-*` directories before cloning and uses bounded concurrency.
|
|
25
|
+
- `tree create --wait-lock` now matches its lock-refusal guidance.
|
|
26
|
+
- Verification trees permit `re-export --dry-run`; mutating operations remain blocked.
|
|
27
|
+
- Test assertion lint now recognizes real assertion calls, and critical modules have explicit coverage floors.
|
|
28
|
+
|
|
29
|
+
### Status and diagnostics
|
|
30
|
+
|
|
31
|
+
- Status classification reuses one patch context and classifies files through a bounded pool.
|
|
32
|
+
- `status --json --summary` returns counts and offenders without the full file payload.
|
|
33
|
+
- `status --json --include-ownership` adds ownership data without another scan.
|
|
34
|
+
- JSON output drains fully through slow pipes, and machine-mode refusals route to stderr.
|
|
35
|
+
- Multi-owner files remain conflicts even when Furnace-managed or generated branding paths.
|
|
36
|
+
- Lock contention and unreadable/corrupt state now produce concise, accurate diagnostics.
|
|
37
|
+
|
|
38
|
+
### Patch, export, and lint workflow
|
|
39
|
+
|
|
40
|
+
- `export-all --exclude-furnace` keeps its filtered scope after license-header repair.
|
|
41
|
+
- `re-export --expect <path>` permits intended drift and reports paths skipped by earlier refusals as not evaluated.
|
|
42
|
+
- Missing drift baselines fail closed, binary patches compare by content, and partial or fully refused re-exports exit non-zero.
|
|
43
|
+
- `re-export --dry-run` uses before/after fingerprints and cannot roll back another completed write.
|
|
44
|
+
- Re-export builds checkJs once per run and reuses the per-patch cache; `--patches` subsets narrow computation.
|
|
45
|
+
- Re-export probes and purity hashing use bounded concurrency; patch writes remain serial.
|
|
46
|
+
- Patch lint rejects new imported system modules without a projected `moz.build` registration.
|
|
47
|
+
- The checkJs cache hashes both configurable shims.
|
|
48
|
+
- Export placement refusals print projected errors and identify renumbering consequences.
|
|
49
|
+
- `patch rename` can recategorize and renumber atomically.
|
|
50
|
+
- `patch move-files` and `patch split` use whole-queue projection lint, normalize names, and provide split guidance.
|
|
51
|
+
- `patch move-files --create` names the required `--description` option.
|
|
52
|
+
- Queue-mutating patch commands support `--wait-lock` and avoid unsafe stale-lock advice.
|
|
53
|
+
- Staged-dependency declarations reject patch-name-shaped file arguments.
|
|
54
|
+
- `CommandError` passes through the top-level handler without duplicate or JSON-corrupting output.
|
|
55
|
+
|
|
56
|
+
### Furnace and validation
|
|
57
|
+
|
|
58
|
+
- Furnace computes checksums once per apply and warns before overwriting divergent patch-owned files.
|
|
59
|
+
- Furnace jsconfig synchronization includes `register: false` components.
|
|
60
|
+
- Manifest-sync failures are visible, and repairs run under the Furnace lock.
|
|
61
|
+
- `furnace validate --fix` changes only reported components and detects missing custom-element registrations.
|
|
62
|
+
- Localization validation recognizes non-Latin text while exempting emoji and punctuation.
|
|
63
|
+
- `furnace rename` uses single-pass, boundary-aware substitutions.
|
|
64
|
+
- Rollback failures retain repair breadcrumbs for `doctor --repair-furnace`.
|
|
65
|
+
- The reported no-change/apply-state disagreement was not reproducible and is regression-tested.
|
|
66
|
+
|
|
67
|
+
### Safety, performance, and maintenance
|
|
68
|
+
|
|
69
|
+
- Nano ID 3.3.18 resolves a high-severity audit finding.
|
|
70
|
+
- Rebase loading distinguishes absent and corrupt sessions; `rebase --abort` can recover invalid state.
|
|
71
|
+
- PID liveness treats `EPERM` as alive, preventing deletion through another user's lock.
|
|
72
|
+
- The moz.build parser handles comments, quoted brackets/apostrophes, and unterminated lists.
|
|
73
|
+
- Parser failures no longer silently certify validation success; internal invariant errors bypass fallback.
|
|
74
|
+
- Toolchain probes, baseline fingerprints, cache hashes, and manifest reads use bounded concurrency.
|
|
75
|
+
- Existing Git tuning remains enabled; fsmonitor stays disabled because of stale-daemon risk.
|
|
76
|
+
- A persistent checkJs daemon remains deferred until safe invalidation is cheaper; see `docs/lint-daemon-design.md`.
|
|
77
|
+
- Shared helpers replace duplicated preflights, error conversion, regex escaping, option parsing, object guards, and ESTree casts.
|
|
78
|
+
- Four local ESLint rules prevent unsafe error casts, duplicate regex escaping, and empty JSDoc.
|
|
79
|
+
- Oversized modules were split or simplified, silent degradations documented, and stale dead-code suppressions removed.
|
|
80
|
+
- The release gate pins weak critical modules so aggregate coverage cannot hide local regressions.
|
|
81
|
+
|
|
82
|
+
## 0.40.0
|
|
83
|
+
|
|
84
|
+
- **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** (FORGE G15, 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
|
+
- **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** (FORGE G15b, 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`.
|
|
88
|
+
- **Per-patch lint can now typecheck patch-adopted test `.js` files against a consumer-configurable harness shim: `patchLint.checkJsTestFiles` + `patchLint.checkJsTestShim`** (FORGE G5, 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`.
|
|
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>`** (FORGE G9, 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`.
|
|
90
|
+
- **A size finding waived by `lintIgnore` no longer vanishes without a trace in per-patch mode** (FORGE G10, 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`.
|
|
91
|
+
- **Smoke-run summaries now attribute allowlist hits per entry, with zero-hit entries flagged as removal candidates** (FORGE G8, 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`.
|
|
92
|
+
- **`lint --per-patch <patch…>` now works: positional arguments in per-patch mode select patches through the same alias resolution as `--patches`** (FORGE G14, 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`.
|
|
93
|
+
- **`patch staged-dependency --remove` now accepts `--file` + `--specifier` alone, inferring `--creates` from a unique match; missing-flag errors name the actually-missing flags** (FORGE G12, 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`.
|
|
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** (FORGE G3, 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`.
|
|
95
|
+
- **Patch-lint `raw-color-value` no longer fires on hex colors inside CSS comments that span the context/added-line boundary** (FORGE G4, 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`.
|
|
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** (FORGE G13, 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`.
|
|
97
|
+
- **New `status --check` (and `--fail-on <class,...>`) turns classification drift into a CI-enforceable non-zero exit** (FORGE G1, 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`.
|
|
98
|
+
- **`status --json` files[] entries now carry a `patch` field naming the owning patch filename (`null` when unowned)** (FORGE G11, 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`.
|
|
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** (FORGE G2, 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`.
|
|
100
|
+
- **`re-export` now retries once, after a 300 ms backoff, when a patch fails on transient git `index.lock` contention** (FORGE G7, 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`.
|
|
101
|
+
- **`export --before/--after` no longer refuses placements whose renumber never actually reaches a reserved range** (FORGE G6, 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
|
+
|
|
3
103
|
## 0.39.0
|
|
4
104
|
|
|
5
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** (FORGE F1, 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`.
|
package/README.md
CHANGED
|
@@ -86,7 +86,8 @@ Queue maintenance lives under `fireforge patch`: `patch compact` closes ordinal
|
|
|
86
86
|
(range-aware when `patchPolicy.ranges` is configured), `patch reorder` moves a patch, and
|
|
87
87
|
`patch split <source> --files <paths...> --name <name>` carves files out of a patch into a
|
|
88
88
|
new one as a single transaction — including staged-dependency owner rewrites — with
|
|
89
|
-
`--dry-run` support.
|
|
89
|
+
`--dry-run` support. `patch rename` also supports `--category` and `--order`; every
|
|
90
|
+
queue-mutating patch command accepts `--wait-lock [seconds]`.
|
|
90
91
|
|
|
91
92
|
`fireforge test <directory>` runs exactly that directory: FireForge enumerates the
|
|
92
93
|
directory's test files and passes the explicit file list to mach in one invocation, so
|
|
@@ -98,14 +99,18 @@ instances do not exercise cross-argument state (`--no-shard` restores one combin
|
|
|
98
99
|
invocation). Recognized harness crashes retry up to `--harness-retries <n>` times (default
|
|
99
100
|
2), and `--perf-samples <path>` publishes a perf-sample artifact path to the harness
|
|
100
101
|
(exported as `<BINARYNAME>_PERF_SAMPLE_JSON`).
|
|
102
|
+
Every test run ends with one machine-readable `FIREFORGE-VERDICT: PASS|FAIL ...` line;
|
|
103
|
+
automation should use it instead of the raw process code and treat a missing verdict as
|
|
104
|
+
failure.
|
|
101
105
|
Pathless test runs must choose a mode: `--auto` forwards mach's own auto-selection,
|
|
102
106
|
`--doctor` runs the Marionette preflight only, and `--canary [path]` runs one short
|
|
103
107
|
browser-chrome canary (`test.canaryPath` / `test.canaryTimeoutSeconds` in `fireforge.json`
|
|
104
108
|
provide defaults). When packageable engine files changed since the last successful
|
|
105
109
|
FireForge build, `test` fails before launching stale artifacts; use `--build` to refresh
|
|
106
|
-
or `--allow-stale-build` only for intentional out-of-band rebuilds.
|
|
107
|
-
|
|
108
|
-
|
|
110
|
+
or `--allow-stale-build` only for intentional out-of-band rebuilds. `--build-only`
|
|
111
|
+
packages mixed-harness paths once; `--extend-coverage` safely retains prior scoped
|
|
112
|
+
coverage. `--kill-stale-marionette` terminates recognized stale browsers. In dev builds,
|
|
113
|
+
files under `obj-*/dist/bin`
|
|
109
114
|
may be symlinks back into the source tree (notably prefs), so edit source prefs directly
|
|
110
115
|
and keep a backup before bisection experiments.
|
|
111
116
|
|
|
@@ -131,7 +136,32 @@ contaminates the console capture (headed non-CI launches print a warning saying
|
|
|
131
136
|
Per-patch lint type-checks plain Firefox JS against a built-in Firefox-globals shim that
|
|
132
137
|
tracks upstream WebIDL additions per release; a project's `patchLint.checkJsExtraShim` can
|
|
133
138
|
add members to the structured shim globals via TypeScript interface merging (e.g.
|
|
134
|
-
`interface ChromeUtilsShim { newApi(): any }`).
|
|
139
|
+
`interface ChromeUtilsShim { newApi(): any }`). Opting into
|
|
140
|
+
`patchLint.checkJsTestFiles: true` extends the pass to patch-owned test `.js` files
|
|
141
|
+
(`browser_*`/`test_*`/`xpcshell_*` and `/test/` paths), each checked as its own small
|
|
142
|
+
script-scope program against a loose built-in harness shim; `patchLint.checkJsTestShim`
|
|
143
|
+
points at a project `.d.ts` whose typed declarations (e.g. a real `TestUtils` interface)
|
|
144
|
+
override the loose baseline so calls to nonexistent harness members fail at export time.
|
|
145
|
+
|
|
146
|
+
For CI enforcement, `fireforge status --check` exits non-zero when any unmanaged,
|
|
147
|
+
drifted, or conflicted file exists (`--fail-on <class,...>` tunes the policy set, and
|
|
148
|
+
`--json` composes — the JSON stays parseable and its `files[]` entries name the owning
|
|
149
|
+
`patch`). `--include-ownership` adds ownership rows to JSON status output. Scan-less
|
|
150
|
+
`re-export` previews drift; `--refuse-foreign-drift --expect <path>` restricts it to
|
|
151
|
+
intended files. `lint --per-patch --report <path>` writes a machine-readable JSON report with
|
|
152
|
+
each patch's line count, tier, active size thresholds, issues, and lintIgnore-suppressed
|
|
153
|
+
issues; the size metrics (`countNonBinaryDiffLines`, `resolvePatchSizeTier`,
|
|
154
|
+
`getPatchSizeThresholds`) are also exported on the programmatic API.
|
|
155
|
+
|
|
156
|
+
For concurrent read-mostly verification beside a busy primary checkout,
|
|
157
|
+
`fireforge tree create <name>` snapshots the project — applied engine tree included,
|
|
158
|
+
`obj-*` excluded — into `.fireforge/trees/<name>` using filesystem copy-on-write (APFS
|
|
159
|
+
clonefile / btrfs-XFS reflink; ~3 s for a 156 MB applied browser tree). Read-only commands
|
|
160
|
+
and export/re-export dry runs work inside a tree; mutations are refused. `--with-objdir`
|
|
161
|
+
adds a safely relocated build for build-less tests. `tree list` reports staleness,
|
|
162
|
+
`tree remove` deletes (refusing active locks), and `tree exec <name> -- <cmd>` runs a
|
|
163
|
+
command inside a tree. Filesystems without CoW support refuse unless `--force-copy`
|
|
164
|
+
explicitly accepts a full physical copy.
|
|
135
165
|
|
|
136
166
|
## Rebasing Firefox Source
|
|
137
167
|
|
package/dist/bin/fireforge.js
CHANGED
|
@@ -17,6 +17,7 @@ import { forceReleaseFurnaceLocksForActiveOperations, isSignalRollbackInFlight,
|
|
|
17
17
|
import { waitForActiveCriticalSections } from '../src/core/signal-critical.js';
|
|
18
18
|
import { CommandError } from '../src/errors/base.js';
|
|
19
19
|
import { waitForActiveChildShutdown } from '../src/utils/process.js';
|
|
20
|
+
import { waitForStdioDrain } from '../src/utils/stdio-drain.js';
|
|
20
21
|
/**
|
|
21
22
|
* Upper bound (ms) the signal handler will wait for any in-flight critical
|
|
22
23
|
* section (e.g. rebase apply + session persist) to finish before calling
|
|
@@ -34,13 +35,32 @@ const SIGNAL_CRITICAL_SECTION_TIMEOUT_MS = 5_000;
|
|
|
34
35
|
* immediately, so an impatient operator is never stuck waiting.
|
|
35
36
|
*/
|
|
36
37
|
const CHILD_SHUTDOWN_TIMEOUT_MS = 12_000;
|
|
38
|
+
/**
|
|
39
|
+
* Upper bound (ms) a delayed exit waits for stdout/stderr to drain. When
|
|
40
|
+
* stdout is a pipe it is ASYNC, and `process.exit()` discards anything
|
|
41
|
+
* queued past the 64 KiB kernel buffer — which truncated the
|
|
42
|
+
* `status --json --fail-on` refusal payload for every piped consumer.
|
|
43
|
+
* Bounded so a stalled reader can never wedge a failing process; an
|
|
44
|
+
* EPIPE'd/closed pipe releases the wait immediately.
|
|
45
|
+
*/
|
|
46
|
+
const STDIO_FLUSH_TIMEOUT_MS = 5_000;
|
|
47
|
+
/**
|
|
48
|
+
* Drains stdio, then exits. Every delayed exit routes through here — only
|
|
49
|
+
* the second-Ctrl+C force-exit (an explicit "out NOW") and this helper's
|
|
50
|
+
* own call terminate directly.
|
|
51
|
+
*/
|
|
52
|
+
function exitAfterStdioFlush(code) {
|
|
53
|
+
void waitForStdioDrain(STDIO_FLUSH_TIMEOUT_MS).finally(() => {
|
|
54
|
+
process.exit(code);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
37
57
|
installBrokenPipeHandler();
|
|
38
58
|
process.on('unhandledRejection', (reason) => {
|
|
39
59
|
console.error('Fatal error (unhandled rejection):', reason instanceof Error ? reason.message : reason);
|
|
40
60
|
if (reason instanceof Error && reason.stack) {
|
|
41
61
|
console.error(reason.stack);
|
|
42
62
|
}
|
|
43
|
-
|
|
63
|
+
exitAfterStdioFlush(1);
|
|
44
64
|
});
|
|
45
65
|
// SIGINT / SIGTERM handlers run any in-flight furnace rollback before
|
|
46
66
|
// terminating. The library cannot call process.exit itself (the
|
|
@@ -81,7 +101,7 @@ function installFurnaceSignalHandler(signal, exitCode) {
|
|
|
81
101
|
// best-effort (errors are logged, not thrown).
|
|
82
102
|
.then(() => forceReleaseFurnaceLocksForActiveOperations())
|
|
83
103
|
.finally(() => {
|
|
84
|
-
|
|
104
|
+
exitAfterStdioFlush(exitCode);
|
|
85
105
|
});
|
|
86
106
|
});
|
|
87
107
|
}
|
|
@@ -89,9 +109,10 @@ installFurnaceSignalHandler('SIGINT', 130);
|
|
|
89
109
|
installFurnaceSignalHandler('SIGTERM', 143);
|
|
90
110
|
main().catch((error) => {
|
|
91
111
|
if (error instanceof CommandError) {
|
|
92
|
-
|
|
112
|
+
exitAfterStdioFlush(error.exitCode);
|
|
113
|
+
return;
|
|
93
114
|
}
|
|
94
115
|
// Truly unexpected — CommandError should have been thrown by withErrorHandling
|
|
95
116
|
console.error('Fatal error:', error);
|
|
96
|
-
|
|
117
|
+
exitAfterStdioFlush(1);
|
|
97
118
|
});
|
package/dist/src/cli.js
CHANGED
|
@@ -3,12 +3,13 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { Command, Help } from 'commander';
|
|
5
5
|
import { COMMAND_MANIFEST } from './commands/manifest.js';
|
|
6
|
+
import { runTreeGuardHook } from './core/tree-guard.js';
|
|
6
7
|
import { CancellationError, CommandError, FireForgeError } from './errors/base.js';
|
|
7
8
|
import { ExitCode } from './errors/codes.js';
|
|
8
9
|
import { ConfigNotFoundError } from './errors/config.js';
|
|
10
|
+
import { getCliVersion } from './utils/build-info.js';
|
|
9
11
|
import { toError } from './utils/errors.js';
|
|
10
|
-
import { cancel, error as logError, setVerbose } from './utils/logger.js';
|
|
11
|
-
import { getPackageVersion } from './utils/package-root.js';
|
|
12
|
+
import { cancel, error as logError, setMachineOutputMode, setStdoutSealed, setVerbose, } from './utils/logger.js';
|
|
12
13
|
const brokenPipeInstalledKey = Symbol.for('fireforge.cli.brokenPipeHandlerInstalled');
|
|
13
14
|
const brokenPipeListenerKey = Symbol.for('fireforge.cli.brokenPipeHandlerListener');
|
|
14
15
|
function getProcessState() {
|
|
@@ -99,6 +100,12 @@ export function withErrorHandling(handler) {
|
|
|
99
100
|
await handler(...args);
|
|
100
101
|
}
|
|
101
102
|
catch (error) {
|
|
103
|
+
// Sentinel errors have already been rendered by the command. Passing
|
|
104
|
+
// them through prevents machine-readable refusal paths from acquiring
|
|
105
|
+
// an "Unexpected error" banner and stack trace on stderr.
|
|
106
|
+
if (error instanceof CommandError) {
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
102
109
|
if (error instanceof CancellationError) {
|
|
103
110
|
cancel('Operation cancelled');
|
|
104
111
|
// 130 (128+SIGINT) is the conventional "user interrupted" code —
|
|
@@ -117,6 +124,16 @@ export function withErrorHandling(handler) {
|
|
|
117
124
|
}
|
|
118
125
|
throw new CommandError(ExitCode.GENERAL_ERROR);
|
|
119
126
|
}
|
|
127
|
+
finally {
|
|
128
|
+
// Central machine-mode and stdout-seal reset. Commands leave both
|
|
129
|
+
// ENGAGED while an error propagates (a mid-throw restore would route
|
|
130
|
+
// the styled error to stdout, corrupting the `--json`/`--raw` payload
|
|
131
|
+
// stream or displacing the FIREFORGE-VERDICT line); the reset happens
|
|
132
|
+
// here, after logError has picked its stream, so no state leaks into
|
|
133
|
+
// a subsequent in-process invocation.
|
|
134
|
+
setMachineOutputMode(false);
|
|
135
|
+
setStdoutSealed(false);
|
|
136
|
+
}
|
|
120
137
|
};
|
|
121
138
|
}
|
|
122
139
|
/** Human-readable labels for command groups, in display order. */
|
|
@@ -245,11 +262,18 @@ export function createProgram() {
|
|
|
245
262
|
.name('fireforge')
|
|
246
263
|
.description('A build tool for customizing Firefox')
|
|
247
264
|
.option('-v, --verbose', 'Enable debug output')
|
|
265
|
+
.option('--ignore-corrupt-tree-marker', 'Run even when .fireforge/tree.json exists but cannot be parsed. Without this the tree guard refuses, because an unreadable marker leaves it unknown whether this is a snapshot or the primary tree.')
|
|
248
266
|
.hook('preAction', (thisCommand) => {
|
|
249
267
|
const opts = thisCommand.opts();
|
|
250
268
|
if (opts['verbose']) {
|
|
251
269
|
setVerbose(true);
|
|
252
270
|
}
|
|
271
|
+
})
|
|
272
|
+
// Verification-tree guard (FORGE G15): when the cwd resolves into a
|
|
273
|
+
// tree snapshot, refuse mutating commands before their action runs —
|
|
274
|
+
// one hook covers every command regardless of how the cwd got there.
|
|
275
|
+
.hook('preAction', async (thisCommand, actionCommand) => {
|
|
276
|
+
await runTreeGuardHook(thisCommand.name(), actionCommand);
|
|
253
277
|
});
|
|
254
278
|
const groupedFormatter = buildGroupedHelpFormatter(COMMAND_MANIFEST);
|
|
255
279
|
program.configureHelp({
|
|
@@ -275,7 +299,7 @@ export async function main() {
|
|
|
275
299
|
const userArgs = process.argv.slice(2);
|
|
276
300
|
const hasSubcommand = userArgs.some((arg) => !arg.startsWith('-'));
|
|
277
301
|
if (!hasSubcommand && userArgs.some((arg) => arg === '--version' || arg === '-V')) {
|
|
278
|
-
process.stdout.write(`${
|
|
302
|
+
process.stdout.write(`${getCliVersion()}\n`);
|
|
279
303
|
return;
|
|
280
304
|
}
|
|
281
305
|
const program = createProgram();
|
|
@@ -10,6 +10,7 @@ import { pathExists } from '../utils/fs.js';
|
|
|
10
10
|
import { error, info, intro, outro, warn } from '../utils/logger.js';
|
|
11
11
|
import { detectBootstrapIssues, runPostBootstrapChecks } from './bootstrap-checks.js';
|
|
12
12
|
import { reportDoctorResults } from './doctor.js';
|
|
13
|
+
import { resolveDoctorSeverity } from './doctor-check-core.js';
|
|
13
14
|
/**
|
|
14
15
|
* Builds a human-readable failure message for hard failures (non-zero exit).
|
|
15
16
|
* Used only when mach bootstrap itself reports failure.
|
|
@@ -67,7 +68,10 @@ export async function bootstrapCommand(projectRoot) {
|
|
|
67
68
|
const issues = detectBootstrapIssues(output);
|
|
68
69
|
if (issues.length > 0) {
|
|
69
70
|
const checks = await runPostBootstrapChecks(issues);
|
|
70
|
-
|
|
71
|
+
// Shares one resolver with reportDoctorResults so the two consumers of
|
|
72
|
+
// this same array cannot drift apart again — before 0.41.0 they
|
|
73
|
+
// disagreed on `passed: false, warning: true` with no severity.
|
|
74
|
+
const hasErrors = checks.some((c) => resolveDoctorSeverity(c) === 'error');
|
|
71
75
|
info('');
|
|
72
76
|
if (hasErrors) {
|
|
73
77
|
warn('Bootstrap completed with issues:');
|
|
@@ -3,13 +3,12 @@ import { confirm } from '@clack/prompts';
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { getProjectPaths } from '../core/config.js';
|
|
5
5
|
import { applyDiscardBaseline, describeConflictWarning, describeDiscardBaseline, describeDiscardOutcome, planDiscardBaselines, planUpstreamDiscards, summarizeDiscardBaselines, } from '../core/discard-baseline.js';
|
|
6
|
+
import { assertEngineGitReady } from '../core/engine-precondition.js';
|
|
6
7
|
import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
|
|
7
|
-
import { getHead, isGitRepository, isMissingHeadError } from '../core/git.js';
|
|
8
8
|
import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
|
|
9
9
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
10
10
|
import { GitError } from '../errors/git.js';
|
|
11
11
|
import { toError } from '../utils/errors.js';
|
|
12
|
-
import { pathExists } from '../utils/fs.js';
|
|
13
12
|
import { info, intro, isCancel, outro, spinner, warn } from '../utils/logger.js';
|
|
14
13
|
import { pickDefined } from '../utils/options.js';
|
|
15
14
|
/**
|
|
@@ -126,26 +125,7 @@ async function discardDirectoryEntries(projectRoot, engineDir, patchesDir, dirPa
|
|
|
126
125
|
export async function discardCommand(projectRoot, file, options = {}) {
|
|
127
126
|
intro('FireForge Discard');
|
|
128
127
|
const paths = getProjectPaths(projectRoot);
|
|
129
|
-
|
|
130
|
-
if (!(await pathExists(paths.engine))) {
|
|
131
|
-
throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
|
|
132
|
-
}
|
|
133
|
-
// Check if it's a git repository
|
|
134
|
-
if (!(await isGitRepository(paths.engine))) {
|
|
135
|
-
throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
|
|
136
|
-
}
|
|
137
|
-
// Unborn-HEAD guard (mirrors status): an interrupted download leaves a
|
|
138
|
-
// repo with no baseline commit, where the entire ~300k-file tree reads
|
|
139
|
-
// as untracked — a discard against that state is never what the operator
|
|
140
|
-
// wants, and the guidance names the actual fix.
|
|
141
|
-
try {
|
|
142
|
-
await getHead(paths.engine);
|
|
143
|
-
}
|
|
144
|
-
catch (headError) {
|
|
145
|
-
if (!isMissingHeadError(headError))
|
|
146
|
-
throw headError;
|
|
147
|
-
throw new GeneralError('Engine repository has no baseline commit yet — a previous "fireforge download" was interrupted before git created the initial Firefox source commit. Re-run "fireforge download --force" to recreate the baseline repository cleanly.');
|
|
148
|
-
}
|
|
128
|
+
await assertEngineGitReady(paths.engine);
|
|
149
129
|
// Check if the file has changes
|
|
150
130
|
const statusEntries = await expandUntrackedDirectoryEntries(paths.engine, await getWorkingTreeStatus(paths.engine));
|
|
151
131
|
const statusEntry = statusEntries.find((entry) => entry.file === file || entry.originalPath === file);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { readdir } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { toError } from '../../utils/errors.js';
|
|
4
5
|
import { pathExists, readText } from '../../utils/fs.js';
|
|
5
6
|
import { ok, warning } from '../doctor-check-core.js';
|
|
6
7
|
async function readEngineText(engineDir, relativePath) {
|
|
@@ -9,17 +10,28 @@ async function readEngineText(engineDir, relativePath) {
|
|
|
9
10
|
return null;
|
|
10
11
|
return readText(fullPath);
|
|
11
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Collects every `browser.toml` under the engine's browser-chrome test tree.
|
|
15
|
+
*
|
|
16
|
+
* Reports unreadable directories separately from "none found": the consumer
|
|
17
|
+
* turns an empty result into the issue "no browser.toml files found", which is
|
|
18
|
+
* the opposite diagnosis from "the tree could not be read". A swallowed
|
|
19
|
+
* mid-walk EACCES also silently *shrank* the result set, so one unreadable
|
|
20
|
+
* subdirectory produced a false clean for that subtree.
|
|
21
|
+
*/
|
|
12
22
|
async function collectBrowserTomlFiles(root) {
|
|
13
23
|
const testRoot = join(root, 'browser/base/content/test');
|
|
14
24
|
if (!(await pathExists(testRoot)))
|
|
15
|
-
return [];
|
|
25
|
+
return { files: [], unreadable: [] };
|
|
16
26
|
const result = [];
|
|
27
|
+
const unreadable = [];
|
|
17
28
|
async function walk(absDir, relDir) {
|
|
18
29
|
let entries;
|
|
19
30
|
try {
|
|
20
31
|
entries = await readdir(absDir, { withFileTypes: true });
|
|
21
32
|
}
|
|
22
|
-
catch {
|
|
33
|
+
catch (error) {
|
|
34
|
+
unreadable.push(`${relDir || '.'} (${toError(error).message})`);
|
|
23
35
|
return;
|
|
24
36
|
}
|
|
25
37
|
for (const entry of entries) {
|
|
@@ -34,11 +46,17 @@ async function collectBrowserTomlFiles(root) {
|
|
|
34
46
|
}
|
|
35
47
|
}
|
|
36
48
|
await walk(testRoot, '');
|
|
37
|
-
return result.sort();
|
|
49
|
+
return { files: result.sort(), unreadable };
|
|
38
50
|
}
|
|
39
51
|
async function runPostRebaseAudit(ctx) {
|
|
40
52
|
const issues = [];
|
|
41
53
|
const engineDir = ctx.paths.engine;
|
|
54
|
+
// The four probes below are upstream-Firefox shape markers: each names a
|
|
55
|
+
// file and a token that a healthy post-rebase tree must still contain. A
|
|
56
|
+
// rebase that dropped a fork patch, or an upstream reorganisation, shows up
|
|
57
|
+
// as one of these going missing. The tokens are deliberately coarse — this
|
|
58
|
+
// is a smoke check that runs warn-only behind an opt-in flag, not a
|
|
59
|
+
// structural validator.
|
|
42
60
|
const mozConfigure = await readEngineText(engineDir, 'browser/moz.configure');
|
|
43
61
|
if (mozConfigure === null) {
|
|
44
62
|
issues.push('browser/moz.configure is missing');
|
|
@@ -69,7 +87,12 @@ async function runPostRebaseAudit(ctx) {
|
|
|
69
87
|
issues.push('toolkit/content/jar.mn has no widget/element exposure entries');
|
|
70
88
|
}
|
|
71
89
|
const browserTomls = await collectBrowserTomlFiles(engineDir);
|
|
72
|
-
if (browserTomls.length
|
|
90
|
+
if (browserTomls.unreadable.length > 0) {
|
|
91
|
+
issues.push(`could not read ${String(browserTomls.unreadable.length)} ` +
|
|
92
|
+
`${browserTomls.unreadable.length === 1 ? 'directory' : 'directories'} under ` +
|
|
93
|
+
`browser/base/content/test: ${browserTomls.unreadable.join(', ')}`);
|
|
94
|
+
}
|
|
95
|
+
else if (browserTomls.files.length === 0) {
|
|
73
96
|
issues.push('no browser.toml files found under browser/base/content/test');
|
|
74
97
|
}
|
|
75
98
|
if (issues.length === 0) {
|
|
@@ -87,6 +87,26 @@ export interface DoctorCheckDefinition {
|
|
|
87
87
|
*/
|
|
88
88
|
fix?: string;
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Resolves a {@link DoctorCheck} to its effective severity, implementing the
|
|
92
|
+
* field-precedence contract documented on the interface
|
|
93
|
+
* (`src/types/commands/project.ts`):
|
|
94
|
+
*
|
|
95
|
+
* 1. `severity`, when present, is authoritative — regardless of `passed`.
|
|
96
|
+
* 2. Without `severity`, `passed: false` is an error unless `warning: true`
|
|
97
|
+
* downgrades it to a warning.
|
|
98
|
+
*
|
|
99
|
+
* This is the single implementation on purpose. Before 0.41.0 there were two
|
|
100
|
+
* and they disagreed on exactly one cell: `reportDoctorResults` consulted
|
|
101
|
+
* `warning` only inside the `passed === true` branch, so the documented
|
|
102
|
+
* downgrade produced an error and a non-zero doctor exit, while
|
|
103
|
+
* `bootstrap.ts` — reading the same array one line earlier — classified it as
|
|
104
|
+
* a warning. No in-tree producer emitted that combination (`failure()` always
|
|
105
|
+
* sets `severity: 'error'`), so the contradiction was latent rather than
|
|
106
|
+
* observable; the interface is exported, so the next hand-rolled check would
|
|
107
|
+
* have hit it.
|
|
108
|
+
*/
|
|
109
|
+
export declare function resolveDoctorSeverity(check: DoctorCheck): 'ok' | 'warning' | 'error';
|
|
90
110
|
/**
|
|
91
111
|
* Builds a DoctorCheck object representing a successful "OK" check.
|
|
92
112
|
*/
|
|
@@ -4,6 +4,34 @@
|
|
|
4
4
|
* `doctor.ts` and sibling check modules. Kept separate so registries can import
|
|
5
5
|
* these symbols without creating cycles through `doctor.ts`.
|
|
6
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* Resolves a {@link DoctorCheck} to its effective severity, implementing the
|
|
9
|
+
* field-precedence contract documented on the interface
|
|
10
|
+
* (`src/types/commands/project.ts`):
|
|
11
|
+
*
|
|
12
|
+
* 1. `severity`, when present, is authoritative — regardless of `passed`.
|
|
13
|
+
* 2. Without `severity`, `passed: false` is an error unless `warning: true`
|
|
14
|
+
* downgrades it to a warning.
|
|
15
|
+
*
|
|
16
|
+
* This is the single implementation on purpose. Before 0.41.0 there were two
|
|
17
|
+
* and they disagreed on exactly one cell: `reportDoctorResults` consulted
|
|
18
|
+
* `warning` only inside the `passed === true` branch, so the documented
|
|
19
|
+
* downgrade produced an error and a non-zero doctor exit, while
|
|
20
|
+
* `bootstrap.ts` — reading the same array one line earlier — classified it as
|
|
21
|
+
* a warning. No in-tree producer emitted that combination (`failure()` always
|
|
22
|
+
* sets `severity: 'error'`), so the contradiction was latent rather than
|
|
23
|
+
* observable; the interface is exported, so the next hand-rolled check would
|
|
24
|
+
* have hit it.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveDoctorSeverity(check) {
|
|
27
|
+
if (check.severity !== undefined) {
|
|
28
|
+
return check.severity;
|
|
29
|
+
}
|
|
30
|
+
if (check.warning) {
|
|
31
|
+
return 'warning';
|
|
32
|
+
}
|
|
33
|
+
return check.passed ? 'ok' : 'error';
|
|
34
|
+
}
|
|
7
35
|
/**
|
|
8
36
|
* Builds a DoctorCheck object representing a successful "OK" check.
|
|
9
37
|
*/
|
|
@@ -15,6 +15,8 @@ async function resolveDeclaredTool(projectRoot, tool) {
|
|
|
15
15
|
return found.exitCode === 0 ? line?.trim() : undefined;
|
|
16
16
|
}
|
|
17
17
|
catch {
|
|
18
|
+
// `xcrun -find` is absent or the tool is unknown — either way the tool has
|
|
19
|
+
// no resolvable path, which is what `undefined` means to the caller.
|
|
18
20
|
return undefined;
|
|
19
21
|
}
|
|
20
22
|
}
|