@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
|
@@ -15,31 +15,41 @@
|
|
|
15
15
|
* Lives in a sibling module to keep `doctor-furnace.ts` under the
|
|
16
16
|
* per-file LOC budget.
|
|
17
17
|
*/
|
|
18
|
-
import { readdir,
|
|
18
|
+
import { readdir, rmdir } from 'node:fs/promises';
|
|
19
19
|
import { join } from 'node:path';
|
|
20
|
+
import { withFileLock } from '../core/file-lock.js';
|
|
20
21
|
import { getFurnacePaths, loadFurnaceConfig, writeFurnaceConfig } from '../core/furnace-config.js';
|
|
22
|
+
import { getFurnaceLockPath } from '../core/furnace-operation.js';
|
|
21
23
|
import { toError } from '../utils/errors.js';
|
|
22
|
-
import {
|
|
24
|
+
import { pathExistsStrict, readJson } from '../utils/fs.js';
|
|
23
25
|
import { failure, ok, warning } from './doctor-check-core.js';
|
|
26
|
+
/**
|
|
27
|
+
* Lists component directories, distinguishing "none" from "could not look".
|
|
28
|
+
*
|
|
29
|
+
* A missing directory is genuinely empty. A `readdir` failure is not: this
|
|
30
|
+
* check exists to detect the concurrent-override orphan race, and swallowing
|
|
31
|
+
* the error into `[]` made it report clean for the one condition it is here
|
|
32
|
+
* to catch. The caller surfaces the failure instead.
|
|
33
|
+
*/
|
|
24
34
|
async function listComponentDirs(dir) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
35
|
+
// `pathExistsStrict`, not `pathExists`: the latter answers `false` for EVERY
|
|
36
|
+
// access error, so an unreadable components directory reported "no orphans",
|
|
37
|
+
// i.e. a clean check — the same fail-open the comment above rejects for
|
|
38
|
+
// `readdir`. Only ENOENT means genuinely absent.
|
|
39
|
+
if (!(await pathExistsStrict(dir)))
|
|
32
40
|
return [];
|
|
33
|
-
}
|
|
41
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
42
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
34
43
|
}
|
|
35
44
|
async function recoverOverrideConfig(overrideDir, name) {
|
|
36
45
|
// `furnace override` writes `override.json` alongside the copied files.
|
|
37
46
|
// That sidecar is enough to reconstruct the furnace.json entry lost to
|
|
38
47
|
// a concurrent-write race (eval 2: second override wrote back the
|
|
39
48
|
// outer-snapshot config, dropping the sibling write's addition).
|
|
49
|
+
// No `pathExists` precheck: it collapses "absent" and "unreadable" into the
|
|
50
|
+
// same `false`, and both outcomes lead here anyway — a missing file lands in
|
|
51
|
+
// the catch below with the same `undefined` result.
|
|
40
52
|
const sidecarPath = join(overrideDir, name, 'override.json');
|
|
41
|
-
if (!(await pathExists(sidecarPath)))
|
|
42
|
-
return undefined;
|
|
43
53
|
try {
|
|
44
54
|
const raw = await readJson(sidecarPath);
|
|
45
55
|
const type = raw.type;
|
|
@@ -59,11 +69,18 @@ async function recoverOverrideConfig(overrideDir, name) {
|
|
|
59
69
|
baseVersion,
|
|
60
70
|
};
|
|
61
71
|
if (typeof raw.baseCommit === 'string') {
|
|
72
|
+
// `baseCommit` is already declared optional on OverrideComponentConfig,
|
|
73
|
+
// so the intersection this replaced widened nothing.
|
|
62
74
|
restored.baseCommit = raw.baseCommit;
|
|
63
75
|
}
|
|
64
76
|
return restored;
|
|
65
77
|
}
|
|
66
78
|
catch {
|
|
79
|
+
// Unreadable or malformed sidecar. Returning `undefined` here is the same
|
|
80
|
+
// value as "no sidecar present", so the caller reports the override as
|
|
81
|
+
// unrecoverable — which is the safe direction (it never invents config),
|
|
82
|
+
// but the operator sees "no valid override.json" rather than "the
|
|
83
|
+
// override.json is corrupt". The remediation is identical either way.
|
|
67
84
|
return undefined;
|
|
68
85
|
}
|
|
69
86
|
}
|
|
@@ -95,61 +112,139 @@ function formatOrphanSummary(orphans) {
|
|
|
95
112
|
const description = [overrideLabel, customLabel].filter(Boolean).join(' and ');
|
|
96
113
|
return `Found orphaned component ${overrideCount + customCount === 1 ? 'directory' : 'directories'} on disk: ${description}. furnace.json does not list these — a previous mutation may have lost a concurrent write.`;
|
|
97
114
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
115
|
+
function emptyOutcome() {
|
|
116
|
+
return {
|
|
117
|
+
restored: [],
|
|
118
|
+
unrecoverable: [],
|
|
119
|
+
reconciled: [],
|
|
120
|
+
deleted: [],
|
|
121
|
+
retained: [],
|
|
122
|
+
errors: [],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Re-registers override orphans and deletes empty custom orphan directories,
|
|
127
|
+
* under ONE furnace lock held across both.
|
|
128
|
+
*
|
|
129
|
+
* Two things this must not do, both of which reproduce the lost-write race the
|
|
130
|
+
* check exists to clean up after:
|
|
131
|
+
*
|
|
132
|
+
* 1. Write back a decision made before the lock. The orphan list is computed
|
|
133
|
+
* from a config snapshot loaded well before we hold anything, so by the
|
|
134
|
+
* time we can write, a concurrent `furnace override` may have registered
|
|
135
|
+
* the same name — writing our sidecar-reconstructed entry over it discards
|
|
136
|
+
* a live write, and a concurrent `furnace create` registering the name
|
|
137
|
+
* under `custom` would leave it in BOTH maps, a state nothing validates.
|
|
138
|
+
* Every orphan is therefore re-checked against `freshConfig` inside the
|
|
139
|
+
* lock and skipped when it is no longer orphaned.
|
|
140
|
+
* 2. Delete custom directories outside the lock. `repairCustomOrphans` used to
|
|
141
|
+
* run after the lock was released, so a `furnace create` that had
|
|
142
|
+
* registered a name and not yet populated its directory had that directory
|
|
143
|
+
* deleted underneath it.
|
|
144
|
+
*/
|
|
145
|
+
async function repairOrphans(projectRoot, orphans) {
|
|
146
|
+
const outcome = emptyOutcome();
|
|
147
|
+
if (orphans.overrides.length === 0 && orphans.customNames.length === 0)
|
|
148
|
+
return outcome;
|
|
149
|
+
const lockPath = getFurnaceLockPath(projectRoot);
|
|
150
|
+
// One catch covers four distinct phases; naming the one that failed keeps
|
|
151
|
+
// the report honest — a lock timeout used to render as "failed while
|
|
152
|
+
// writing furnace.json", the phase least likely to have been reached.
|
|
153
|
+
// Held in an object rather than two `let`s: the lock callback mutates them
|
|
154
|
+
// from inside a closure, and TS's control-flow analysis does not propagate
|
|
155
|
+
// that through the call — it would narrow both to their initial literals in
|
|
156
|
+
// the catch below.
|
|
157
|
+
const progress = { phase: 'waiting for the furnace lock', persisted: false };
|
|
158
|
+
try {
|
|
159
|
+
await withFileLock(lockPath, async () => {
|
|
160
|
+
progress.phase = 'loading furnace.json under the lock';
|
|
161
|
+
const freshConfig = await loadFurnaceConfig(projectRoot);
|
|
162
|
+
repairOverridesLocked(freshConfig, orphans.overrides, outcome);
|
|
163
|
+
if (outcome.restored.length > 0) {
|
|
164
|
+
progress.phase = 'writing furnace.json';
|
|
165
|
+
await writeFurnaceConfig(projectRoot, freshConfig);
|
|
166
|
+
}
|
|
167
|
+
// From here the restore (if any) is on disk; a later failure must not
|
|
168
|
+
// report the whole repair as if it never happened.
|
|
169
|
+
progress.persisted = true;
|
|
170
|
+
progress.phase = 'cleaning custom orphan directories';
|
|
171
|
+
await deleteEmptyCustomOrphans(projectRoot, freshConfig, orphans.customNames, outcome);
|
|
172
|
+
}, {
|
|
173
|
+
onTimeoutMessage: `Timed out waiting for the furnace lock at ${lockPath}. ` +
|
|
174
|
+
'Another fireforge furnace command may be running; retry the repair once it finishes.',
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
if (progress.persisted) {
|
|
179
|
+
outcome.errors.push(`repair failed while ${progress.phase}: ${toError(err).message}`);
|
|
180
|
+
return outcome;
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
...emptyOutcome(),
|
|
184
|
+
unrecoverable: outcome.unrecoverable,
|
|
185
|
+
repairError: `while ${progress.phase}: ${toError(err).message}`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return outcome;
|
|
189
|
+
}
|
|
190
|
+
/** Override half of {@link repairOrphans}; mutates `freshConfig` in place. */
|
|
191
|
+
function repairOverridesLocked(freshConfig, orphans, outcome) {
|
|
104
192
|
for (const orphan of orphans) {
|
|
193
|
+
if (orphan.name in freshConfig.overrides || orphan.name in freshConfig.custom) {
|
|
194
|
+
// Registered while we waited for the lock — no longer an orphan, and
|
|
195
|
+
// writing over it would either discard that write or duplicate the name
|
|
196
|
+
// across `overrides` and `custom`.
|
|
197
|
+
outcome.reconciled.push(orphan.name);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
105
200
|
if (orphan.recoveredConfig) {
|
|
106
201
|
freshConfig.overrides[orphan.name] = orphan.recoveredConfig;
|
|
107
|
-
restored.push(orphan.name);
|
|
202
|
+
outcome.restored.push(orphan.name);
|
|
108
203
|
}
|
|
109
204
|
else {
|
|
110
|
-
unrecoverable.push(orphan.name);
|
|
205
|
+
outcome.unrecoverable.push(orphan.name);
|
|
111
206
|
}
|
|
112
207
|
}
|
|
113
|
-
if (restored.length > 0) {
|
|
114
|
-
try {
|
|
115
|
-
await writeFurnaceConfig(projectRoot, freshConfig);
|
|
116
|
-
}
|
|
117
|
-
catch (err) {
|
|
118
|
-
return { restored: [], unrecoverable, writeError: toError(err).message };
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return { restored, unrecoverable };
|
|
122
208
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const retained = [];
|
|
126
|
-
const errors = [];
|
|
209
|
+
/** Custom half of {@link repairOrphans}; must run under the same lock. */
|
|
210
|
+
async function deleteEmptyCustomOrphans(projectRoot, freshConfig, customNames, outcome) {
|
|
127
211
|
if (customNames.length === 0)
|
|
128
|
-
return
|
|
212
|
+
return;
|
|
129
213
|
const furnacePaths = getFurnacePaths(projectRoot);
|
|
130
214
|
for (const name of customNames) {
|
|
215
|
+
if (name in freshConfig.custom || name in freshConfig.overrides) {
|
|
216
|
+
// A name that was orphaned in BOTH trees is reconciled once, not once
|
|
217
|
+
// per tree — the report reads "Skipped 2 names (x, x)" otherwise.
|
|
218
|
+
if (!outcome.reconciled.includes(name))
|
|
219
|
+
outcome.reconciled.push(name);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
131
222
|
const dir = join(furnacePaths.customDir, name);
|
|
132
223
|
let entries;
|
|
133
224
|
try {
|
|
134
225
|
entries = await readdir(dir, { withFileTypes: true });
|
|
135
226
|
}
|
|
136
227
|
catch (err) {
|
|
137
|
-
errors.push(`${name}: ${toError(err).message}`);
|
|
228
|
+
outcome.errors.push(`${name}: ${toError(err).message}`);
|
|
138
229
|
continue;
|
|
139
230
|
}
|
|
140
231
|
if (entries.length > 0) {
|
|
141
|
-
retained.push(name);
|
|
232
|
+
outcome.retained.push(name);
|
|
142
233
|
continue;
|
|
143
234
|
}
|
|
144
235
|
try {
|
|
145
|
-
|
|
146
|
-
|
|
236
|
+
// `rmdir`, not `rm`: `rm` without `{ recursive: true }` throws EISDIR on
|
|
237
|
+
// a directory, so on a real filesystem this branch always failed and the
|
|
238
|
+
// "deleted N empty directories" report was unreachable. `rmdir` is also
|
|
239
|
+
// the exact semantics wanted here — it refuses a non-empty directory,
|
|
240
|
+
// which keeps the emptiness check above from being the only safeguard.
|
|
241
|
+
await rmdir(dir);
|
|
242
|
+
outcome.deleted.push(name);
|
|
147
243
|
}
|
|
148
244
|
catch (err) {
|
|
149
|
-
errors.push(`${name}: ${toError(err).message}`);
|
|
245
|
+
outcome.errors.push(`${name}: ${toError(err).message}`);
|
|
150
246
|
}
|
|
151
247
|
}
|
|
152
|
-
return { deleted, retained, errors };
|
|
153
248
|
}
|
|
154
249
|
export const furnaceManifestSyncCheck = {
|
|
155
250
|
name: 'Furnace manifest sync',
|
|
@@ -169,12 +264,15 @@ export const furnaceManifestSyncCheck = {
|
|
|
169
264
|
if (!ctx.options.repairFurnace) {
|
|
170
265
|
return warning('Furnace manifest sync', summary, 'Run "fireforge doctor --repair-furnace" to re-register override orphans from their override.json sidecars (custom orphans are listed for manual follow-up).');
|
|
171
266
|
}
|
|
172
|
-
const repairResult = await
|
|
173
|
-
if (repairResult.
|
|
174
|
-
return failure('Furnace manifest sync', `Repair failed
|
|
267
|
+
const repairResult = await repairOrphans(ctx.projectRoot, orphans);
|
|
268
|
+
if (repairResult.repairError) {
|
|
269
|
+
return failure('Furnace manifest sync', `Repair failed ${repairResult.repairError}`, 'Fix the underlying error and retry the doctor command.');
|
|
175
270
|
}
|
|
176
|
-
const customRepair =
|
|
177
|
-
const { restored, unrecoverable } = repairResult;
|
|
271
|
+
const customRepair = repairResult;
|
|
272
|
+
const { restored, unrecoverable, reconciled } = repairResult;
|
|
273
|
+
const reconciledDetail = reconciled.length > 0
|
|
274
|
+
? ` Skipped ${reconciled.length} name${reconciled.length === 1 ? '' : 's'} (${reconciled.join(', ')}) that furnace.json already listed by the time the repair held the furnace lock — a concurrent furnace command registered ${reconciled.length === 1 ? 'it' : 'them'} first.`
|
|
275
|
+
: '';
|
|
178
276
|
const restoreDetail = restored.length > 0
|
|
179
277
|
? `Re-registered ${restored.length} override${restored.length === 1 ? '' : 's'} (${restored.join(', ')}) from their override.json sidecars.`
|
|
180
278
|
: '';
|
|
@@ -190,7 +288,7 @@ export const furnaceManifestSyncCheck = {
|
|
|
190
288
|
const customErrorDetail = customRepair.errors.length > 0
|
|
191
289
|
? ` Could not inspect or delete ${customRepair.errors.length} custom orphan ${customRepair.errors.length === 1 ? 'directory' : 'directories'} (${customRepair.errors.join('; ')}).`
|
|
192
290
|
: '';
|
|
193
|
-
return warning('Furnace manifest sync', `${restoreDetail}${unrecoverableDetail}${customDetail}${retainedCustomDetail}${customErrorDetail}`.trim() ||
|
|
291
|
+
return warning('Furnace manifest sync', `${restoreDetail}${unrecoverableDetail}${customDetail}${retainedCustomDetail}${customErrorDetail}${reconciledDetail}`.trim() ||
|
|
194
292
|
'Nothing to repair (orphans surfaced but all were already recoverable).');
|
|
195
293
|
},
|
|
196
294
|
};
|
|
@@ -8,7 +8,7 @@ import { CUSTOM_ELEMENTS_JS, JAR_MN, resolveFtlDir } from '../core/furnace-const
|
|
|
8
8
|
import { getFurnaceLockPath, runFurnaceMutation } from '../core/furnace-operation.js';
|
|
9
9
|
import { hasBlockingStepErrors } from '../core/furnace-step-errors.js';
|
|
10
10
|
import { validateAllComponents } from '../core/furnace-validate.js';
|
|
11
|
-
import { toError } from '../utils/errors.js';
|
|
11
|
+
import { isProcessAlive, toError } from '../utils/errors.js';
|
|
12
12
|
import { pathExists } from '../utils/fs.js';
|
|
13
13
|
import { failure, ok, warning } from './doctor-check-core.js';
|
|
14
14
|
import { furnaceStaleJarRegistrationCheck } from './doctor-furnace-jar.js';
|
|
@@ -120,6 +120,9 @@ async function collectFurnaceDrift(projectRoot, engineDir, config, ftlDir) {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
catch {
|
|
123
|
+
// A drift probe that cannot complete is reported AS drift: the doctor check
|
|
124
|
+
// must not claim a component is clean when it failed to look. Mirrors the
|
|
125
|
+
// documented twin above.
|
|
123
126
|
drifted.push(name);
|
|
124
127
|
}
|
|
125
128
|
}
|
|
@@ -428,18 +431,11 @@ async function readFurnaceLockPid(lockPath) {
|
|
|
428
431
|
return Number.isFinite(pid) ? pid : null;
|
|
429
432
|
}
|
|
430
433
|
catch {
|
|
434
|
+
// No PID file, or it is unreadable/unparseable. The caller falls back to the
|
|
435
|
+
// age-only staleness heuristic, which is why this is `null` and not a throw.
|
|
431
436
|
return null;
|
|
432
437
|
}
|
|
433
438
|
}
|
|
434
|
-
function isProcessStillRunning(pid) {
|
|
435
|
-
try {
|
|
436
|
-
process.kill(pid, 0);
|
|
437
|
-
return true;
|
|
438
|
-
}
|
|
439
|
-
catch {
|
|
440
|
-
return false;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
439
|
/**
|
|
444
440
|
* "Furnace stale lock" check: detect and (under `--repair-furnace`)
|
|
445
441
|
* remove a `.fireforge/furnace.lock` directory whose owner process is no
|
|
@@ -474,7 +470,13 @@ const furnaceStaleLockCheck = {
|
|
|
474
470
|
// this bucket; the age gate avoids false positives on a lock
|
|
475
471
|
// that was just acquired by a concurrent process that hadn't
|
|
476
472
|
// written its PID yet.
|
|
477
|
-
|
|
473
|
+
//
|
|
474
|
+
// `isProcessAlive` treats EPERM as ALIVE. The local copy it replaced
|
|
475
|
+
// returned false for EPERM, so a furnace lock held by a live process
|
|
476
|
+
// owned by another uid was reported as "owner is no longer running"
|
|
477
|
+
// and deleted by --repair-furnace, dropping mutual exclusion under a
|
|
478
|
+
// concurrent furnace operation.
|
|
479
|
+
const ownerDead = pid !== null && !isProcessAlive(pid);
|
|
478
480
|
const pidMissingAndOld = pid === null && (ageMs ?? 0) > 60_000;
|
|
479
481
|
const isStale = ownerDead || pidMissingAndOld;
|
|
480
482
|
if (!isStale) {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* doctor AND/OR `test --canary`, and adding a system-wide process scan to
|
|
21
21
|
* the canary hot path buys no extra coverage over the doctor check.
|
|
22
22
|
*/
|
|
23
|
+
import { toError } from '../utils/errors.js';
|
|
23
24
|
import { exec } from '../utils/process.js';
|
|
24
25
|
import { ok, warning } from './doctor-check-core.js';
|
|
25
26
|
/**
|
|
@@ -104,7 +105,7 @@ async function runOrphanedHarnessCheck() {
|
|
|
104
105
|
psOutput = await listSystemProcesses();
|
|
105
106
|
}
|
|
106
107
|
catch (error) {
|
|
107
|
-
return warning(CHECK_NAME, `Could not scan system processes (${error
|
|
108
|
+
return warning(CHECK_NAME, `Could not scan system processes (${toError(error).message}); skipping the orphaned-worker check.`);
|
|
108
109
|
}
|
|
109
110
|
const orphans = findOrphanedHarnessWorkers(psOutput);
|
|
110
111
|
if (orphans.length === 0) {
|
|
@@ -13,7 +13,7 @@ import { pathExists } from '../utils/fs.js';
|
|
|
13
13
|
import { error, info, intro, outro, success, warn } from '../utils/logger.js';
|
|
14
14
|
import { findExecutable } from '../utils/process.js';
|
|
15
15
|
import { POST_REBASE_AUDIT_CHECK } from './doctor/post-rebase-audit.js';
|
|
16
|
-
import { failure, ok, warning } from './doctor-check-core.js';
|
|
16
|
+
import { failure, ok, resolveDoctorSeverity, warning } from './doctor-check-core.js';
|
|
17
17
|
import { EXTERNAL_TOOLCHAIN_DOCTOR_CHECK } from './doctor-external-toolchains.js';
|
|
18
18
|
import { FURNACE_DOCTOR_CHECKS } from './doctor-furnace.js';
|
|
19
19
|
import { ORPHANED_HARNESS_DOCTOR_CHECK } from './doctor-orphaned-harness.js';
|
|
@@ -406,7 +406,7 @@ export function reportDoctorResults(checks) {
|
|
|
406
406
|
let warningCount = 0;
|
|
407
407
|
let failedCount = 0;
|
|
408
408
|
for (const check of checks) {
|
|
409
|
-
const severity =
|
|
409
|
+
const severity = resolveDoctorSeverity(check);
|
|
410
410
|
if (severity === 'warning') {
|
|
411
411
|
warn(`! ${check.name}: ${check.message}`);
|
|
412
412
|
if (check.fix) {
|
|
@@ -195,6 +195,10 @@ export async function exportAllCommand(projectRoot, options = {}) {
|
|
|
195
195
|
// post-hoc string surgery on the aggregate diff, which keeps the
|
|
196
196
|
// output shape aligned with the single-file `export` command.
|
|
197
197
|
let diff;
|
|
198
|
+
// Retain the exact scope used to build the aggregate. Interactive
|
|
199
|
+
// license-header repair mutates engine files and therefore has to rebuild
|
|
200
|
+
// the diff, but it must not silently widen an --exclude-furnace export.
|
|
201
|
+
let scopedExportPaths;
|
|
198
202
|
if (furnaceExcluded.size > 0) {
|
|
199
203
|
const rawChanged = await getWorkingTreeStatus(paths.engine);
|
|
200
204
|
const allChanged = await expandUntrackedDirectoryEntries(paths.engine, rawChanged);
|
|
@@ -203,6 +207,7 @@ export async function exportAllCommand(projectRoot, options = {}) {
|
|
|
203
207
|
.flatMap((entry) => [entry.file, entry.originalPath].filter((value) => !!value))
|
|
204
208
|
.filter((file) => !furnaceExcluded.has(file))),
|
|
205
209
|
].sort();
|
|
210
|
+
scopedExportPaths = nonFurnacePaths;
|
|
206
211
|
if (nonFurnacePaths.length === 0) {
|
|
207
212
|
info(`Excluded ${furnaceExcluded.size} furnace-managed file(s) from export; no non-Furnace changes remain.`);
|
|
208
213
|
outro('Nothing to export');
|
|
@@ -234,7 +239,10 @@ export async function exportAllCommand(projectRoot, options = {}) {
|
|
|
234
239
|
// report-only under --dry-run so the preview never mutates engine/)
|
|
235
240
|
const headersAdded = await autoFixLicenseHeaders(paths.engine, diff, config, isInteractive, isDryRun);
|
|
236
241
|
if (headersAdded) {
|
|
237
|
-
diff =
|
|
242
|
+
diff =
|
|
243
|
+
scopedExportPaths === undefined
|
|
244
|
+
? await getAllDiff(paths.engine)
|
|
245
|
+
: await getDiffForFilesAgainstHead(paths.engine, scopedExportPaths);
|
|
238
246
|
}
|
|
239
247
|
const metadata = await promptExportPatchMetadata(options, isInteractive, 'export-all', config);
|
|
240
248
|
if (!metadata)
|
|
@@ -20,6 +20,7 @@ import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
|
20
20
|
import { toError } from '../utils/errors.js';
|
|
21
21
|
import { pathExists, readText, removeFile, writeText } from '../utils/fs.js';
|
|
22
22
|
import { info, warn } from '../utils/logger.js';
|
|
23
|
+
import { groupProjectedPlacementErrors } from './export-placement-conflicts.js';
|
|
23
24
|
import { assertPlacementAvoidsReservedRanges } from './export-placement-policy.js';
|
|
24
25
|
import { findPartialOwnershipOverlap } from './export-shared.js';
|
|
25
26
|
function buildFilenameForPlacement(category, name, order, width) {
|
|
@@ -74,14 +75,22 @@ export function computePlacementPlan(manifestPatches, newPatchCategory, newPatch
|
|
|
74
75
|
const sorted = [...manifestPatches].sort((a, b) => a.order - b.order);
|
|
75
76
|
const renameMap = new Map();
|
|
76
77
|
const prefixWidth = prefixWidthForPatches(sorted, requestedOrder);
|
|
77
|
-
//
|
|
78
|
+
// Patches at requestedOrder or later shift up by one, but only until the
|
|
79
|
+
// cascade reaches a free ordinal — a gap absorbs the insert, so the tail
|
|
80
|
+
// beyond it (including any reserved block) keeps its numbering.
|
|
81
|
+
let cursor = requestedOrder;
|
|
78
82
|
for (const patch of sorted) {
|
|
79
|
-
if (patch.order
|
|
80
|
-
|
|
81
|
-
const currentRest = patch.filename.replace(/^\d+-/, '');
|
|
82
|
-
const newFilename = `${String(newOrder).padStart(prefixWidth, '0')}-${currentRest}`;
|
|
83
|
-
renameMap.set(patch.filename, { newOrder, newFilename });
|
|
83
|
+
if (patch.order < requestedOrder) {
|
|
84
|
+
continue;
|
|
84
85
|
}
|
|
86
|
+
if (patch.order > cursor) {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
const newOrder = cursor + 1;
|
|
90
|
+
const currentRest = patch.filename.replace(/^\d+-/, '');
|
|
91
|
+
const newFilename = `${String(newOrder).padStart(prefixWidth, '0')}-${currentRest}`;
|
|
92
|
+
renameMap.set(patch.filename, { newOrder, newFilename });
|
|
93
|
+
cursor = newOrder;
|
|
85
94
|
}
|
|
86
95
|
const newFilename = buildFilenameForPlacement(newPatchCategory, newPatchName, requestedOrder, prefixWidth);
|
|
87
96
|
return {
|
|
@@ -199,15 +208,24 @@ export async function projectPlacementForLint(patchesDir, plan, diff, config) {
|
|
|
199
208
|
modifiedFileAdditions: buildModifiedFileAdditionsFromDiff(diff),
|
|
200
209
|
});
|
|
201
210
|
projectedEntries.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
|
|
211
|
+
const policyRest = baseCtx.patchPolicy ? { patchPolicy: baseCtx.patchPolicy } : {};
|
|
202
212
|
const projectedIssues = lintPatchQueue({
|
|
203
213
|
entries: projectedEntries,
|
|
204
|
-
...
|
|
214
|
+
...policyRest,
|
|
205
215
|
}).filter((i) => i.severity === 'error');
|
|
206
216
|
if (projectedIssues.length === 0)
|
|
207
217
|
return null;
|
|
218
|
+
// Baseline lint over the UN-renumbered queue: an error present here is
|
|
219
|
+
// pre-existing breakage, not a consequence of this placement — calling
|
|
220
|
+
// it one would misattribute (FORGE K9). Entries are already in memory,
|
|
221
|
+
// so this second pass costs no additional IO.
|
|
222
|
+
const baselineIssues = lintPatchQueue({
|
|
223
|
+
entries: baseCtx.entries,
|
|
224
|
+
...policyRest,
|
|
225
|
+
}).filter((i) => i.severity === 'error');
|
|
208
226
|
return {
|
|
209
227
|
reason: `placement would introduce ${projectedIssues.length} cross-patch lint error(s)`,
|
|
210
|
-
details: projectedIssues
|
|
228
|
+
details: groupProjectedPlacementErrors(projectedIssues, baselineIssues, plan),
|
|
211
229
|
};
|
|
212
230
|
}
|
|
213
231
|
/**
|
|
@@ -240,7 +258,7 @@ export async function commitPlacementExport(input) {
|
|
|
240
258
|
}
|
|
241
259
|
const conflicts = await projectPlacementForLint(input.patchesDir, currentPlan, input.diff, input.config);
|
|
242
260
|
if (conflicts && input.unsafeOverride !== true) {
|
|
243
|
-
throw new InvalidArgumentError(`Refusing to run export: ${conflicts.reason}.
|
|
261
|
+
throw new InvalidArgumentError(`Refusing to run export: ${conflicts.reason}:\n ${conflicts.details.join('\n ')}\n` +
|
|
244
262
|
'If the conflict names files owned by another patch (e.g. duplicate-new-file-creation), ' +
|
|
245
263
|
're-run the export with an explicit file list that leaves those files with their owner — ' +
|
|
246
264
|
'do NOT bypass with --force-unsafe. Pass --force-unsafe only for a reviewed placement conflict.', '--force-unsafe');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PatchRenameEntry } from '../core/patch-manifest.js';
|
|
2
|
+
import type { PatchLintIssue } from '../types/commands/index.js';
|
|
3
|
+
/** Inputs {@link groupProjectedPlacementErrors} needs from the plan. */
|
|
4
|
+
export interface PlacementAttributionPlan {
|
|
5
|
+
insertionOrder: number;
|
|
6
|
+
newFilename: string;
|
|
7
|
+
renameMap: Map<string, PatchRenameEntry>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Partitions the projected placement errors into three labeled groups —
|
|
11
|
+
* defects in the exported content, consequences of the renumbering, and
|
|
12
|
+
* errors the queue already had — rendered as indented detail lines for
|
|
13
|
+
* {@link ConflictReport.details}. Grouping travels through the plain
|
|
14
|
+
* `details: string[]`, so both refusal sites and confirmDestructive's
|
|
15
|
+
* conflict printer show it without changes.
|
|
16
|
+
*/
|
|
17
|
+
export declare function groupProjectedPlacementErrors(projectedErrors: readonly PatchLintIssue[], baselineErrors: readonly PatchLintIssue[], plan: PlacementAttributionPlan): string[];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Attribution of projected placement lint errors (FORGE K9), split out of
|
|
4
|
+
* export-flow.ts for the line budget. A placement projection lints one
|
|
5
|
+
* merged queue — the renumbered existing patches plus a synthetic entry
|
|
6
|
+
* for the pending patch — so its errors used to render as one flat list
|
|
7
|
+
* that read as though the NEW patch caused every one of them. A
|
|
8
|
+
* renumbering that re-evaluates pre-existing staged relationships (455
|
|
9
|
+
* importing a module created by 456, legal only at their current
|
|
10
|
+
* ordinals) then sent the operator auditing a perfectly fine export.
|
|
11
|
+
*
|
|
12
|
+
* Consumed by export-flow.ts; no top-level registrar is exported and none
|
|
13
|
+
* is wanted.
|
|
14
|
+
*/
|
|
15
|
+
import { formatPatchLintIssue } from '../core/patch-lint.js';
|
|
16
|
+
/**
|
|
17
|
+
* Identity for baseline comparison: check + site file + implicated
|
|
18
|
+
* patches, with projected filenames mapped BACK through the rename map so
|
|
19
|
+
* a pre-existing error keeps its identity across the renumber. Never
|
|
20
|
+
* parses the fingerprint — it is documented rename-sensitive.
|
|
21
|
+
*/
|
|
22
|
+
function issueKey(issue, mapBackToOldName) {
|
|
23
|
+
const patches = (issue.patches ?? []).map(mapBackToOldName).sort((a, b) => a.localeCompare(b));
|
|
24
|
+
return `${issue.check}|${issue.file}|${patches.join(',')}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Partitions the projected placement errors into three labeled groups —
|
|
28
|
+
* defects in the exported content, consequences of the renumbering, and
|
|
29
|
+
* errors the queue already had — rendered as indented detail lines for
|
|
30
|
+
* {@link ConflictReport.details}. Grouping travels through the plain
|
|
31
|
+
* `details: string[]`, so both refusal sites and confirmDestructive's
|
|
32
|
+
* conflict printer show it without changes.
|
|
33
|
+
*/
|
|
34
|
+
export function groupProjectedPlacementErrors(projectedErrors, baselineErrors, plan) {
|
|
35
|
+
const oldNameByNew = new Map();
|
|
36
|
+
for (const [oldFilename, rename] of plan.renameMap) {
|
|
37
|
+
oldNameByNew.set(rename.newFilename, oldFilename);
|
|
38
|
+
}
|
|
39
|
+
const mapBack = (name) => oldNameByNew.get(name) ?? name;
|
|
40
|
+
const baselineKeys = new Set(baselineErrors.map((issue) => issueKey(issue, (name) => name)));
|
|
41
|
+
const exported = [];
|
|
42
|
+
const renumbering = [];
|
|
43
|
+
const preExisting = [];
|
|
44
|
+
for (const issue of projectedErrors) {
|
|
45
|
+
// The synthetic entry's filename is unique in the projection, so
|
|
46
|
+
// membership in `patches` attributes exactly. Issues from older rules
|
|
47
|
+
// without the field fall back to a message scan, then to "the queue".
|
|
48
|
+
const implicatesNewPatch = issue.patches?.includes(plan.newFilename) ?? issue.message.includes(plan.newFilename);
|
|
49
|
+
if (implicatesNewPatch) {
|
|
50
|
+
exported.push(issue);
|
|
51
|
+
}
|
|
52
|
+
else if (baselineKeys.has(issueKey(issue, mapBack))) {
|
|
53
|
+
preExisting.push(issue);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
renumbering.push(issue);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const details = [];
|
|
60
|
+
const pushGroup = (header, issues) => {
|
|
61
|
+
if (issues.length === 0)
|
|
62
|
+
return;
|
|
63
|
+
details.push(header);
|
|
64
|
+
for (const issue of issues) {
|
|
65
|
+
details.push(` ${formatPatchLintIssue(issue)}`);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
pushGroup(`errors in the exported patch content (${String(exported.length)}):`, exported);
|
|
69
|
+
pushGroup(`consequences of renumbering existing patches to make room at ordinal ` +
|
|
70
|
+
`${String(plan.insertionOrder)} (${String(renumbering.length)}) — these come from ` +
|
|
71
|
+
'renumbering, NOT from the exported content:', renumbering);
|
|
72
|
+
pushGroup(`errors already present in the queue before this export (${String(preExisting.length)}):`, preExisting);
|
|
73
|
+
return details;
|
|
74
|
+
}
|
|
@@ -96,7 +96,7 @@ export async function gatePlacementPlan(args) {
|
|
|
96
96
|
}
|
|
97
97
|
else if (conflicts && options.forceUnsafe !== true) {
|
|
98
98
|
s.stop();
|
|
99
|
-
throw new InvalidArgumentError(`Refusing to run export: ${conflicts.reason}.
|
|
99
|
+
throw new InvalidArgumentError(`Refusing to run export: ${conflicts.reason}:\n ${conflicts.details.join('\n ')}\n` +
|
|
100
100
|
'If the conflict names files owned by another patch (e.g. duplicate-new-file-creation), ' +
|
|
101
101
|
're-run the export with an explicit file list that leaves those files with their owner — ' +
|
|
102
102
|
'do NOT bypass with --force-unsafe. Pass --force-unsafe only for a reviewed placement conflict.', '--force-unsafe');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PatchesManifest } from '../types/commands/index.js';
|
|
1
|
+
import type { PatchesManifest, PatchLintIssue } from '../types/commands/index.js';
|
|
2
2
|
import type { ExportOptions, PatchCategory } from '../types/commands/index.js';
|
|
3
3
|
import type { FireForgeConfig } from '../types/config.js';
|
|
4
4
|
import type { SpinnerHandle } from '../utils/logger.js';
|
|
@@ -25,6 +25,13 @@ import type { SpinnerHandle } from '../utils/logger.js';
|
|
|
25
25
|
* visible rather than silent.
|
|
26
26
|
*/
|
|
27
27
|
export declare function runPatchLint(engineDir: string, filesAffected: string[], diffContent: string, config: FireForgeConfig, skipLint?: boolean, patchQueueCtx?: import('../core/patch-lint-cross.js').PatchQueueContext, ignoreChecks?: ReadonlySet<string>, patchTier?: 'branding'): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Prints a patch-lint issue list with `runPatchLint`'s exact severity
|
|
30
|
+
* buckets and skip-lint semantics, throwing on errors unless `skipLint`.
|
|
31
|
+
* Factored out so `re-export`'s cached-lint path (FORGE J1) can replay
|
|
32
|
+
* stored issues through the identical reporter without re-linting.
|
|
33
|
+
*/
|
|
34
|
+
export declare function reportPatchLintOutcome(issues: PatchLintIssue[], skipLint?: boolean): void;
|
|
28
35
|
/**
|
|
29
36
|
* Resolves patch metadata interactively or from flags, with shared validation.
|
|
30
37
|
* @param options - Export command options
|