@hominis/fireforge 0.50.0 → 0.52.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 +67 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/check-report.d.ts +17 -4
- package/dist/src/commands/check-report.js +19 -6
- package/dist/src/commands/check-run.d.ts +5 -0
- package/dist/src/commands/check-run.js +9 -1
- package/dist/src/commands/check-steps.d.ts +8 -0
- package/dist/src/commands/check-steps.js +31 -6
- package/dist/src/commands/check.js +1 -1
- package/dist/src/commands/declarations-report.d.ts +43 -0
- package/dist/src/commands/declarations-report.js +76 -0
- package/dist/src/commands/export-placement-gate.js +8 -0
- package/dist/src/commands/furnace/rename-registrations.js +7 -2
- package/dist/src/commands/furnace/validate.js +26 -1
- package/dist/src/commands/lint-per-patch.js +6 -2
- package/dist/src/commands/lint-per-run-checkjs.js +2 -2
- package/dist/src/commands/lint-report.js +7 -2
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/park.d.ts +22 -0
- package/dist/src/commands/park.js +232 -0
- package/dist/src/commands/patch/delete.js +11 -6
- package/dist/src/commands/patch/index.d.ts +3 -3
- package/dist/src/commands/patch/index.js +8 -4
- package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
- package/dist/src/commands/patch/lint-ignore-from.js +142 -0
- package/dist/src/commands/patch/lint-ignore.js +39 -29
- package/dist/src/commands/patch/move-files-create.js +11 -4
- package/dist/src/commands/patch/move-files-into.js +10 -4
- package/dist/src/commands/patch/move-files.js +3 -1
- package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
- package/dist/src/commands/patch/removal-created-files.js +83 -0
- package/dist/src/commands/patch/rename.js +15 -3
- package/dist/src/commands/patch/reserve.d.ts +33 -0
- package/dist/src/commands/patch/reserve.js +152 -0
- package/dist/src/commands/patch/retire.js +11 -6
- package/dist/src/commands/patch/split-plan.d.ts +10 -1
- package/dist/src/commands/patch/split-plan.js +16 -1
- package/dist/src/commands/patch/split.js +14 -5
- package/dist/src/commands/patch/stage.d.ts +34 -0
- package/dist/src/commands/patch/stage.js +153 -0
- package/dist/src/commands/patch/staged-dependency-remove.d.ts +31 -0
- package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
- package/dist/src/commands/patch/staged-dependency.js +40 -98
- package/dist/src/commands/re-export-adjacent.d.ts +5 -0
- package/dist/src/commands/re-export-adjacent.js +4 -3
- package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
- package/dist/src/commands/re-export-bulk-scan.js +22 -7
- package/dist/src/commands/re-export-hunks.d.ts +35 -0
- package/dist/src/commands/re-export-hunks.js +220 -0
- package/dist/src/commands/re-export-options.js +29 -2
- package/dist/src/commands/re-export-purity-scope.d.ts +18 -0
- package/dist/src/commands/re-export-purity-scope.js +131 -0
- package/dist/src/commands/re-export-register.js +15 -1
- package/dist/src/commands/re-export-removed.d.ts +1 -1
- package/dist/src/commands/re-export-removed.js +17 -2
- package/dist/src/commands/re-export-scan.d.ts +2 -0
- package/dist/src/commands/re-export-scan.js +4 -1
- package/dist/src/commands/re-export-single.js +102 -42
- package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
- package/dist/src/commands/re-export-stale-registrations.js +62 -0
- package/dist/src/commands/re-export.js +35 -34
- package/dist/src/commands/status-output.js +5 -1
- package/dist/src/commands/test-build-furnace.d.ts +3 -2
- package/dist/src/commands/test-build-furnace.js +8 -2
- package/dist/src/commands/test-build-phase.d.ts +2 -0
- package/dist/src/commands/test-build-phase.js +34 -17
- package/dist/src/commands/test-checkjs.js +3 -2
- package/dist/src/commands/test-dispatch.d.ts +39 -0
- package/dist/src/commands/test-dispatch.js +220 -0
- package/dist/src/commands/test-furnace-reach.d.ts +37 -0
- package/dist/src/commands/test-furnace-reach.js +132 -0
- package/dist/src/commands/test-orchestrate.d.ts +14 -0
- package/dist/src/commands/test-orchestrate.js +25 -4
- package/dist/src/commands/test-perf-scope.d.ts +21 -0
- package/dist/src/commands/test-perf-scope.js +55 -0
- package/dist/src/commands/test-prelock.js +100 -4
- package/dist/src/commands/test-register.js +18 -3
- package/dist/src/commands/test-results.d.ts +62 -0
- package/dist/src/commands/test-results.js +62 -0
- package/dist/src/commands/test-run.d.ts +13 -55
- package/dist/src/commands/test-run.js +73 -101
- package/dist/src/commands/test-shards.d.ts +110 -0
- package/dist/src/commands/test-shards.js +255 -0
- package/dist/src/commands/test-stale-gate.d.ts +5 -1
- package/dist/src/commands/test-stale-gate.js +11 -2
- package/dist/src/commands/test-verdict.d.ts +2 -1
- package/dist/src/commands/test.d.ts +1 -1
- package/dist/src/commands/test.js +68 -75
- package/dist/src/commands/token-lint.js +48 -8
- package/dist/src/commands/verify.js +9 -2
- package/dist/src/core/build-baseline-types.d.ts +13 -0
- package/dist/src/core/build-baseline-validate.js +9 -0
- package/dist/src/core/build-baseline.js +7 -0
- package/dist/src/core/build-prepare.d.ts +6 -0
- package/dist/src/core/build-prepare.js +29 -3
- 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-check.js +83 -7
- package/dist/src/core/config-validate-test-toolchains.js +14 -0
- package/dist/src/core/config-validate-token-scales.d.ts +3 -0
- package/dist/src/core/config-validate-token-scales.js +47 -5
- package/dist/src/core/config-validate.js +6 -0
- package/dist/src/core/coverage-extend.js +2 -2
- package/dist/src/core/coverage-records.d.ts +11 -0
- package/dist/src/core/coverage-records.js +34 -0
- package/dist/src/core/engine-session-lock.d.ts +6 -0
- package/dist/src/core/engine-session-lock.js +10 -2
- package/dist/src/core/file-lock-shared.d.ts +57 -0
- package/dist/src/core/file-lock-shared.js +117 -0
- package/dist/src/core/file-lock.d.ts +9 -0
- package/dist/src/core/file-lock.js +58 -2
- package/dist/src/core/furnace-apply.js +10 -10
- package/dist/src/core/furnace-config-validate-exceptions.d.ts +11 -3
- package/dist/src/core/furnace-config-validate-exceptions.js +59 -8
- package/dist/src/core/furnace-config-validate.js +7 -1
- package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
- package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
- package/dist/src/core/furnace-validate-accessibility.d.ts +5 -3
- package/dist/src/core/furnace-validate-accessibility.js +75 -30
- package/dist/src/core/furnace-validate-exceptions.d.ts +13 -4
- package/dist/src/core/furnace-validate-exceptions.js +59 -4
- package/dist/src/core/furnace-validate-icon-button.js +30 -1
- package/dist/src/core/history-log-file.js +8 -1
- package/dist/src/core/line-prefixer.d.ts +18 -0
- package/dist/src/core/line-prefixer.js +34 -0
- package/dist/src/core/mach.d.ts +7 -0
- package/dist/src/core/mach.js +24 -9
- package/dist/src/core/module-resolution-preflight.d.ts +19 -3
- package/dist/src/core/module-resolution-preflight.js +24 -6
- package/dist/src/core/negative-control-compiled.d.ts +36 -0
- package/dist/src/core/negative-control-compiled.js +87 -0
- package/dist/src/core/negative-control-diff.d.ts +14 -2
- package/dist/src/core/negative-control-diff.js +49 -6
- package/dist/src/core/negative-control.d.ts +12 -0
- package/dist/src/core/negative-control.js +48 -5
- package/dist/src/core/park-store.d.ts +63 -0
- package/dist/src/core/park-store.js +216 -0
- package/dist/src/core/patch-export-update.d.ts +4 -0
- package/dist/src/core/patch-export-update.js +12 -3
- package/dist/src/core/patch-export.d.ts +3 -2
- package/dist/src/core/patch-export.js +21 -14
- package/dist/src/core/patch-files.js +26 -8
- package/dist/src/core/patch-lint-checkjs-any-import.d.ts +6 -4
- package/dist/src/core/patch-lint-checkjs-any-import.js +9 -7
- package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
- package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
- package/dist/src/core/patch-lint-checkjs.js +13 -3
- package/dist/src/core/patch-lint-cross.d.ts +3 -1
- package/dist/src/core/patch-lint-cross.js +5 -0
- package/dist/src/core/patch-lint-css.js +14 -10
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +5 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +1 -1
- package/dist/src/core/patch-lint-ignore.d.ts +56 -12
- package/dist/src/core/patch-lint-ignore.js +173 -43
- package/dist/src/core/patch-lint-ownership.d.ts +10 -0
- package/dist/src/core/patch-lint-ownership.js +26 -0
- package/dist/src/core/patch-lint-size.js +9 -1
- package/dist/src/core/patch-lint-token-scale.d.ts +26 -8
- package/dist/src/core/patch-lint-token-scale.js +206 -29
- package/dist/src/core/patch-lint.js +2 -2
- package/dist/src/core/patch-manifest-io.js +8 -1
- package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
- package/dist/src/core/patch-manifest-key-order.js +61 -0
- package/dist/src/core/patch-manifest-stage.d.ts +46 -0
- package/dist/src/core/patch-manifest-stage.js +96 -0
- package/dist/src/core/patch-manifest-validate.js +27 -1
- package/dist/src/core/patch-order-legal.d.ts +2 -1
- package/dist/src/core/patch-order-legal.js +7 -2
- package/dist/src/core/patch-policy.d.ts +6 -2
- package/dist/src/core/patch-policy.js +14 -7
- package/dist/src/core/patch-retired.d.ts +26 -4
- package/dist/src/core/patch-retired.js +40 -2
- package/dist/src/core/run-log.d.ts +6 -0
- package/dist/src/core/run-log.js +17 -1
- package/dist/src/core/test-harness-crash.d.ts +2 -1
- package/dist/src/core/test-harness-crash.js +5 -8
- package/dist/src/core/test-harness-signature.d.ts +16 -0
- package/dist/src/core/test-harness-signature.js +19 -1
- package/dist/src/core/test-raw-log.d.ts +66 -0
- package/dist/src/core/test-raw-log.js +122 -0
- package/dist/src/core/test-run-completeness.d.ts +47 -7
- package/dist/src/core/test-run-completeness.js +70 -23
- package/dist/src/core/test-stale-check.d.ts +10 -1
- package/dist/src/core/test-stale-check.js +15 -1
- package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
- package/dist/src/core/test-xpcshell-retry.js +2 -1
- package/dist/src/core/token-lint.d.ts +5 -2
- package/dist/src/core/token-lint.js +8 -3
- package/dist/src/core/token-sibling-sheets.d.ts +46 -0
- package/dist/src/core/token-sibling-sheets.js +119 -0
- package/dist/src/core/tree-guard.js +2 -0
- package/dist/src/core/typecheck-shim.d.ts +6 -1
- package/dist/src/core/typecheck-shim.js +139 -2
- package/dist/src/core/typecheck.js +3 -1
- package/dist/src/index.d.ts +9 -3
- package/dist/src/index.js +4 -1
- package/dist/src/types/commands/index.d.ts +2 -2
- package/dist/src/types/commands/options.d.ts +68 -7
- package/dist/src/types/commands/patches.d.ts +43 -0
- package/dist/src/types/config.d.ts +74 -2
- package/dist/src/types/furnace.d.ts +29 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/utils/options.d.ts +3 -2
- package/dist/src/utils/options.js +9 -4
- package/docs/configuration.md +110 -9
- package/docs/furnace.md +61 -1
- package/docs/lifecycle-invariants.md +13 -11
- package/docs/machine-output.md +68 -18
- package/docs/patch-workflow.md +174 -13
- package/docs/testing.md +89 -5
- package/package.json +3 -3
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* The scoped half of the `re-export --dry-run` purity guard.
|
|
4
|
+
*
|
|
5
|
+
* A dry run takes no engine-session lock since 0.51.0, so peers keep
|
|
6
|
+
* writing while it runs. The whole-tree fingerprint then reported every
|
|
7
|
+
* peer's write as "invariant violated … This is a FireForge bug", and an
|
|
8
|
+
* immediate repeat was clean. The scoped fingerprint covers only what the
|
|
9
|
+
* dry run could touch: the selected patches' bodies and manifest rows, the
|
|
10
|
+
* engine files they own, the engine HEAD and the engine index. A mismatch
|
|
11
|
+
* that remains is attributed to a foreign writer when the history log or
|
|
12
|
+
* the engine lock shows one in the window.
|
|
13
|
+
*/
|
|
14
|
+
import { readFile } from 'node:fs/promises';
|
|
15
|
+
import { dirname, join, resolve } from 'node:path';
|
|
16
|
+
import { readEngineSessionLockStatus } from '../core/engine-session-lock.js';
|
|
17
|
+
import { git } from '../core/git-base.js';
|
|
18
|
+
import { historyRecordIdentity, resolveHistoryLogPath } from '../core/history-log-file.js';
|
|
19
|
+
import { PATCHES_MANIFEST } from '../core/patch-manifest-io.js';
|
|
20
|
+
import { GeneralError } from '../errors/base.js';
|
|
21
|
+
import { getNodeErrorCode, toError } from '../utils/errors.js';
|
|
22
|
+
import { sha256Hex } from '../utils/hash.js';
|
|
23
|
+
import { isArray, isObject } from '../utils/validation.js';
|
|
24
|
+
/** Reads a file's bytes as a hash, `absent` when it does not exist. */
|
|
25
|
+
async function hashOrAbsent(path, label) {
|
|
26
|
+
try {
|
|
27
|
+
return sha256Hex(await readFile(path));
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const code = getNodeErrorCode(error);
|
|
31
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
32
|
+
return 'absent';
|
|
33
|
+
throw new GeneralError(`[dry-run] cannot fingerprint ${label} for the purity guard: ${toError(error).message}`, error);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** The selected rows of `patches.json`, so a peer's write to another row is not a violation. */
|
|
37
|
+
async function selectedRowsFingerprint(patchesDir, filenames) {
|
|
38
|
+
let raw;
|
|
39
|
+
try {
|
|
40
|
+
raw = await readFile(join(patchesDir, PATCHES_MANIFEST), 'utf8');
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (getNodeErrorCode(error) === 'ENOENT')
|
|
44
|
+
return 'absent';
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse(raw);
|
|
49
|
+
const rows = isObject(parsed) && isArray(parsed['patches']) ? parsed['patches'] : [];
|
|
50
|
+
const selected = rows.filter((row) => isObject(row) && typeof row['filename'] === 'string' && filenames.has(row['filename']));
|
|
51
|
+
return sha256Hex(JSON.stringify(selected));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// A manifest that no longer parses changed, whoever wrote it.
|
|
55
|
+
return sha256Hex(raw);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Fingerprints exactly the state a scoped `re-export --dry-run` could
|
|
60
|
+
* disturb. Every key names what it covers, so a violation reads as the
|
|
61
|
+
* path that moved.
|
|
62
|
+
*/
|
|
63
|
+
export async function fingerprintScopedDryRunState(engineDir, patchesDir, scope) {
|
|
64
|
+
const fingerprint = new Map();
|
|
65
|
+
try {
|
|
66
|
+
fingerprint.set('engine HEAD', (await git(['rev-parse', 'HEAD'], engineDir)).trim());
|
|
67
|
+
// The index as a whole: the historic purity defect was a transient
|
|
68
|
+
// `--intent-to-add` entry, which may land on any path a scan reads.
|
|
69
|
+
fingerprint.set('the engine index', sha256Hex(await git(['ls-files', '--stage', '-z'], engineDir)));
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
throw new GeneralError(`[dry-run] cannot fingerprint the engine for the purity guard: ${toError(error).message}`, error);
|
|
73
|
+
}
|
|
74
|
+
const filenames = new Set(scope.patches.map((patch) => patch.filename));
|
|
75
|
+
fingerprint.set(`${PATCHES_MANIFEST} rows of ${[...filenames].sort().join(', ')}`, await selectedRowsFingerprint(patchesDir, filenames));
|
|
76
|
+
for (const filename of [...filenames].sort()) {
|
|
77
|
+
fingerprint.set(`patches/${filename}`, await hashOrAbsent(join(patchesDir, filename), `patches/${filename}`));
|
|
78
|
+
}
|
|
79
|
+
const owned = [...new Set(scope.patches.flatMap((patch) => patch.filesAffected))].sort();
|
|
80
|
+
for (const file of owned) {
|
|
81
|
+
fingerprint.set(`engine/${file}`, await hashOrAbsent(join(engineDir, file), `engine/${file}`));
|
|
82
|
+
}
|
|
83
|
+
return fingerprint;
|
|
84
|
+
}
|
|
85
|
+
/** History records another invocation wrote at or after `sinceMs`. */
|
|
86
|
+
async function foreignHistoryRecords(projectRoot, sinceMs) {
|
|
87
|
+
let text;
|
|
88
|
+
try {
|
|
89
|
+
text = await readFile(await resolveHistoryLogPath(projectRoot), 'utf8');
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
const own = historyRecordIdentity().invocationId;
|
|
95
|
+
const writers = [];
|
|
96
|
+
for (const line of text.split('\n').slice(-500)) {
|
|
97
|
+
let record;
|
|
98
|
+
try {
|
|
99
|
+
record = JSON.parse(line);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const ts = typeof record.ts === 'string' ? Date.parse(record.ts) : Number.NaN;
|
|
105
|
+
if (!(ts >= sinceMs) || record.invocationId === own)
|
|
106
|
+
continue;
|
|
107
|
+
const session = typeof record.sessionId === 'string' ? `, session ${record.sessionId}` : '';
|
|
108
|
+
writers.push(`${String(record.operation)} (PID ${String(record.pid)}${session})`);
|
|
109
|
+
}
|
|
110
|
+
return writers;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Names whoever else wrote during the dry run: history records from other
|
|
114
|
+
* invocations since it started, and a live holder of the engine-session
|
|
115
|
+
* lock (which a dry run no longer takes). Empty when there is no evidence.
|
|
116
|
+
*/
|
|
117
|
+
export async function describeForeignWriters(patchesDir, sinceMs) {
|
|
118
|
+
const projectRoot = dirname(resolve(patchesDir));
|
|
119
|
+
const writers = await foreignHistoryRecords(projectRoot, sinceMs);
|
|
120
|
+
try {
|
|
121
|
+
const lock = await readEngineSessionLockStatus(projectRoot);
|
|
122
|
+
if (lock.held && lock.holder !== undefined && lock.holder.pid !== process.pid) {
|
|
123
|
+
const detail = lock.holder.metadata.length > 0 ? ` (${lock.holder.metadata.join(', ')})` : '';
|
|
124
|
+
writers.push(`the engine-session lock holder, PID ${lock.holder.pid}${detail}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Attribution is best effort; the violation is reported either way.
|
|
129
|
+
}
|
|
130
|
+
return writers;
|
|
131
|
+
}
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* the orchestrator would go the wrong way.
|
|
9
9
|
*/
|
|
10
10
|
import { Command, Option } from 'commander';
|
|
11
|
+
import { getProjectPaths } from '../core/config.js';
|
|
11
12
|
import { withEngineSessionLock } from '../core/engine-session-lock.js';
|
|
13
|
+
import { withPrivateGitIndex } from '../core/git-readonly-index.js';
|
|
12
14
|
import { addWaitLockOption, pickDefined, resolveWaitLockSeconds, stringListOption, } from '../utils/options.js';
|
|
13
15
|
import { reExportCommand } from './re-export.js';
|
|
14
16
|
import { preflightReExportArguments, readPathListFile } from './re-export-preflight.js';
|
|
@@ -27,7 +29,7 @@ export function registerReExport(program, { getProjectRoot, withErrorHandling })
|
|
|
27
29
|
.split(',')
|
|
28
30
|
.map((v) => v.trim())
|
|
29
31
|
.filter((v) => v.length > 0))
|
|
30
|
-
.option('--refuse-adjacent-unmanaged', 'Refuse a
|
|
32
|
+
.option('--refuse-adjacent-unmanaged', 'Refuse a re-export (non-zero exit, patch not written) when unmanaged files exist adjacent to the patch ownership, instead of warning. Adjacent means the same directory as an owned file (not a sibling or nested directory), or named by a registration line the patch adds. Beside --scan it judges the files the scan did not adopt. Mutually exclusive with --files.')
|
|
31
33
|
.option('--refuse-foreign-drift', "Refuse a re-export (non-zero exit, patch not written) when the refreshed body would add or drop lines, in a file the patch already owns, that the old patch body did not carry — protects multi-session checkouts from silently capturing another session's uncommitted edits. Beside --scan it guards each patch's existing files (adopted files count as expected). Mutually exclusive with --files.")
|
|
32
34
|
.option('--expect <path>', "With --refuse-foreign-drift, name an engine-relative file whose drift is this session's intended edit: drift confined to --expect files proceeds, drift anywhere else still refuses. Repeatable, and the whitelist applies across EVERY patch in this invocation, not per patch — so pass all the patches and all their --expect paths in one call rather than looping one patch at a time.", ...stringListOption())
|
|
33
35
|
.option('--expect-unmanaged <path>', 'With --refuse-adjacent-unmanaged, name an engine-relative file that is a REVIEWED, recorded exception to the adjacency refusal: an approved unmanaged path is still listed, but no longer refuses. Repeatable, and the carve-out applies across EVERY patch in this invocation, not per patch. Use it to keep the belt armed for everything else instead of dropping --refuse-adjacent-unmanaged entirely.', ...stringListOption())
|
|
@@ -40,6 +42,9 @@ export function registerReExport(program, { getProjectRoot, withErrorHandling })
|
|
|
40
42
|
.option('--no-cache', 'Bypass per-patch lint result cache reads and writes for this re-export')
|
|
41
43
|
.option('--allow-stale-furnace', 'Export the deployed engine copy even when the components/ source changed since the last furnace apply')
|
|
42
44
|
.option('--allow-shrink', 'Allow --files to remove paths currently owned by the patch. Required before --yes can bypass the shrink confirmation.')
|
|
45
|
+
.option('--declarations-out <path>', 'Where a refused --scan adoption writes its one --from-ready staged-declaration report (default: .fireforge/declarations/<timestamp>.json).')
|
|
46
|
+
.option('--hunks-from <diff>', "Export each file the diff names as the patch's current body plus only the diff's hunks, so another session's edits in the same file stay out. One explicit patch; not with --scan, --files or --all.")
|
|
47
|
+
.option('--exclude-hunks <diff>', "Export each file the diff names as the working tree minus the diff's hunks. One explicit patch; not with --scan, --files or --all.")
|
|
43
48
|
.option('-y, --yes', 'Skip confirmation prompts (required for non-TTY destructive writes)')
|
|
44
49
|
.option('--force-unsafe', 'Bypass cross-patch lint refusal when --files shrinks a patch')
|
|
45
50
|
.option('--stamp', "After every selected patch refreshes cleanly, stamp each re-exported patch's sourceVersion/sourceProduct in patches.json to firefox.version/firefox.product from fireforge.json. No effect on a partial run.")
|
|
@@ -51,6 +56,15 @@ export function registerReExport(program, { getProjectRoot, withErrorHandling })
|
|
|
51
56
|
// Refuse what the locked run would refuse before queueing for the
|
|
52
57
|
// lock, which can be a long wait under contention.
|
|
53
58
|
await preflightReExportArguments(projectRoot, patches, reExportOptions);
|
|
59
|
+
// A dry run writes nothing, so it does not queue behind peers' builds
|
|
60
|
+
// and test runs for the engine lock. Its git plumbing runs on a
|
|
61
|
+
// private index, so not even git's stat-cache refresh reaches the
|
|
62
|
+
// shared `.git/index`; dry runs still serialize among themselves.
|
|
63
|
+
if (reExportOptions.dryRun === true) {
|
|
64
|
+
const engineDir = getProjectPaths(projectRoot).engine;
|
|
65
|
+
await withPrivateGitIndex(engineDir, () => reExportCommand(projectRoot, patches, reExportOptions));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
54
68
|
await withEngineSessionLock(projectRoot, 're-export', () => reExportCommand(projectRoot, patches, reExportOptions), { waitLockSeconds: resolveWaitLockSeconds(options.waitLock) });
|
|
55
69
|
}));
|
|
56
70
|
}
|
|
@@ -12,4 +12,4 @@ import type { ProjectPaths } from '../types/config.js';
|
|
|
12
12
|
* `filesAffected` as it is, so without this the stale entry would stay
|
|
13
13
|
* in the manifest and in the reserved-range policy check.
|
|
14
14
|
*/
|
|
15
|
-
export declare function validateExpectedRemovals(paths: ProjectPaths, patches: PatchMetadata[], options: ReExportOptions): Promise<string[]>;
|
|
15
|
+
export declare function validateExpectedRemovals(paths: ProjectPaths, patches: PatchMetadata[], options: ReExportOptions, queue?: readonly PatchMetadata[]): Promise<string[]>;
|
|
@@ -20,7 +20,7 @@ import { normalizeEngineRelativeInput } from './re-export-scan.js';
|
|
|
20
20
|
* `filesAffected` as it is, so without this the stale entry would stay
|
|
21
21
|
* in the manifest and in the reserved-range policy check.
|
|
22
22
|
*/
|
|
23
|
-
export async function validateExpectedRemovals(paths, patches, options) {
|
|
23
|
+
export async function validateExpectedRemovals(paths, patches, options, queue = patches) {
|
|
24
24
|
const removed = [
|
|
25
25
|
...new Set((options.expectRemoved ?? []).map((file) => normalizeEngineRelativeInput(file, '--expect-removed'))),
|
|
26
26
|
];
|
|
@@ -32,7 +32,8 @@ export async function validateExpectedRemovals(paths, patches, options) {
|
|
|
32
32
|
const owners = patches.filter((patch) => patch.filesAffected.includes(file));
|
|
33
33
|
const owner = owners[0];
|
|
34
34
|
if (owners.length !== 1 || owner === undefined) {
|
|
35
|
-
throw new InvalidArgumentError(`--expect-removed ${file} must be owned by exactly one selected patch
|
|
35
|
+
throw new InvalidArgumentError(`--expect-removed ${file} must be owned by exactly one selected patch.` +
|
|
36
|
+
unselectedOwnerHint(file, patches, queue), '--expect-removed');
|
|
36
37
|
}
|
|
37
38
|
const section = parseDiffSections(await readText(join(paths.patches, owner.filename))).find((candidate) => candidate.targetPath === file);
|
|
38
39
|
if (tracked.has(file)) {
|
|
@@ -47,6 +48,20 @@ export async function validateExpectedRemovals(paths, patches, options) {
|
|
|
47
48
|
}
|
|
48
49
|
return removed;
|
|
49
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Each path retires from its own owner, so one run may retire paths from
|
|
53
|
+
* several selected patches. A path whose owner was left out of the
|
|
54
|
+
* selection names that owner, since adding it is the usual fix.
|
|
55
|
+
*/
|
|
56
|
+
function unselectedOwnerHint(file, selected, queue) {
|
|
57
|
+
const selectedNames = new Set(selected.map((patch) => patch.filename));
|
|
58
|
+
const outside = queue
|
|
59
|
+
.filter((patch) => !selectedNames.has(patch.filename) && patch.filesAffected.includes(file))
|
|
60
|
+
.map((patch) => patch.filename);
|
|
61
|
+
if (outside.length === 0)
|
|
62
|
+
return '';
|
|
63
|
+
return ` It is owned by ${outside.join(', ')}, which this run did not select; add it to the selection to retire both in one run.`;
|
|
64
|
+
}
|
|
50
65
|
async function assertCreatedFileAbsent(paths, file, owner, createdByOwner) {
|
|
51
66
|
if (await removalPathPresent(join(paths.engine, file))) {
|
|
52
67
|
throw new InvalidArgumentError(`--expect-removed ${file} is untracked in engine HEAD and still present on disk; ` +
|
|
@@ -54,6 +54,8 @@ export declare function assertScanAdoptionsHaveNoForwardImports(args: {
|
|
|
54
54
|
* too, and `patchPolicy`, so the remedy names a legal order.
|
|
55
55
|
*/
|
|
56
56
|
config?: FireForgeConfig;
|
|
57
|
+
/** `--declarations-out`: where the one `--from`-ready report goes. */
|
|
58
|
+
declarationsOut?: string | undefined;
|
|
57
59
|
}): Promise<void>;
|
|
58
60
|
/** The manifest rows a queue context carries, for remedy computations. */
|
|
59
61
|
export declare function manifestFromQueue(ctx: PatchQueueContext): PatchesManifest;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { confirm } from '@clack/prompts';
|
|
4
4
|
import { getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
5
5
|
import { listTrackedInHead } from '../core/git-file-ops.js';
|
|
@@ -15,6 +15,7 @@ import { mapWithConcurrency } from '../utils/concurrency.js';
|
|
|
15
15
|
import { pathExists } from '../utils/fs.js';
|
|
16
16
|
import { cancel, info, isCancel, warn } from '../utils/logger.js';
|
|
17
17
|
import { isContainedRelativePath, normalizePathSlashes, stripEnginePrefix, } from '../utils/paths.js';
|
|
18
|
+
import { formatDeclarationsReport, writeDeclarationsReport } from './declarations-report.js';
|
|
18
19
|
import { formatForwardImportRemedy } from './forward-import-remedy.js';
|
|
19
20
|
import { joinScanFileOwner, splitScanFileOwner } from './re-export-scan-owner.js';
|
|
20
21
|
const SCAN_ADD_COUNT_THRESHOLD = 3;
|
|
@@ -227,7 +228,9 @@ export async function assertScanAdoptionsHaveNoForwardImports(args) {
|
|
|
227
228
|
if (registrations.length > 0) {
|
|
228
229
|
parts.push(`${registrations.length} forward registration${registrations.length === 1 ? '' : 's'} the adoption would leave dangling:\n${registrationDetails}`);
|
|
229
230
|
}
|
|
231
|
+
const report = await writeDeclarationsReport(dirname(resolve(patchesDir)), [...offending, ...registrations], args.declarationsOut);
|
|
230
232
|
throw new GeneralError(`Refusing to adopt scanned files into ${patchFilename}: ${parts.join('\n')}\n` +
|
|
233
|
+
(report !== undefined ? `${formatDeclarationsReport(report).join('\n')}\n` : '') +
|
|
231
234
|
formatForwardImportRemedy({
|
|
232
235
|
reRunHint: 're-running the scan',
|
|
233
236
|
issues: [...offending, ...registrations],
|
|
@@ -5,20 +5,25 @@
|
|
|
5
5
|
* loop calls {@link reExportSinglePatchWithIndexLockRetry} per selected
|
|
6
6
|
* patch.
|
|
7
7
|
*/
|
|
8
|
+
import { join } from 'node:path';
|
|
8
9
|
import { stdioIsInteractive } from '../core/destructive.js';
|
|
9
10
|
import { enforceFreshFurnaceSources } from '../core/furnace-stale-export.js';
|
|
10
11
|
import { getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
11
12
|
import { listTrackedInHead } from '../core/git-file-ops.js';
|
|
12
13
|
import { updatePatchAndMetadata } from '../core/patch-export.js';
|
|
14
|
+
import { withoutFields } from '../core/patch-export-update.js';
|
|
13
15
|
import { buildLintIgnoreChecks, lintIgnoreRule, mergeLintIgnoreIds, } from '../core/patch-lint-ignore.js';
|
|
14
16
|
import { buildProjectedManifest, enforcePatchPolicy } from '../core/patch-policy.js';
|
|
15
17
|
import { isGitIndexLockConflict } from '../errors/git.js';
|
|
18
|
+
import { readText } from '../utils/fs.js';
|
|
16
19
|
import { info, success, warn } from '../utils/logger.js';
|
|
17
20
|
import { sleep } from '../utils/sleep.js';
|
|
18
21
|
import { findMissingFiles, reportAdjacentUnmanagedFiles } from './re-export-adjacent.js';
|
|
19
22
|
import { reportForeignDrift } from './re-export-drift.js';
|
|
23
|
+
import { buildHunkSelectedDiff, hunkSelectionOf } from './re-export-hunks.js';
|
|
20
24
|
import { assertRefreshedBodyHasNoNewQueueErrors, lintReExportedPatch, refreshQueueCtxEntry, storeReExportLintResult, } from './re-export-lint.js';
|
|
21
25
|
import { assertScanAdoptionsHaveNoForwardImports, assertScanFileAdditionsHaveDiffHunks, confirmBroadScanAdditions, scanPatchFilesForReExport, } from './re-export-scan.js';
|
|
26
|
+
import { planStaleRegistrationDrop, recordStaleRegistrationDrop, reportStaleRegistrationDrop, } from './re-export-stale-registrations.js';
|
|
22
27
|
const GIT_INDEX_LOCK_RETRY_MS = 300;
|
|
23
28
|
/**
|
|
24
29
|
* Retries a single patch re-export exactly once when the failure is
|
|
@@ -40,6 +45,59 @@ export async function reExportSinglePatchWithIndexLockRetry(args) {
|
|
|
40
45
|
return await reExportSinglePatch(args);
|
|
41
46
|
}
|
|
42
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* The `--scan` branch: scans, gates the adoption on forward imports,
|
|
50
|
+
* confirms a broad scan and, under `--refuse-adjacent-unmanaged`, judges
|
|
51
|
+
* the neighbours the scan left. Undefined means the patch is not written.
|
|
52
|
+
*/
|
|
53
|
+
async function adoptScannedFiles(args, currentFilesAffected) {
|
|
54
|
+
const { patch, paths, manifest, options, isDryRun, config, adjacentCtx } = args;
|
|
55
|
+
const scanResult = await scanPatchFilesForReExport({
|
|
56
|
+
currentFilesAffected,
|
|
57
|
+
engineDir: paths.engine,
|
|
58
|
+
manifest,
|
|
59
|
+
patchFilename: patch.filename,
|
|
60
|
+
isDryRun,
|
|
61
|
+
...(options.scanFiles !== undefined ? { scanFiles: options.scanFiles } : {}),
|
|
62
|
+
});
|
|
63
|
+
// Forward-import gate at adoption time (runs in dry-run too): refuse
|
|
64
|
+
// to adopt unmanaged files that import modules created by later
|
|
65
|
+
// patches, because the after-the-fact lint failure this leaves behind
|
|
66
|
+
// is the field-reported footgun. Applies to broad --scan, --scan-file,
|
|
67
|
+
// and the --scan-files bulk assignments alike.
|
|
68
|
+
await assertScanAdoptionsHaveNoForwardImports({
|
|
69
|
+
patchesDir: paths.patches,
|
|
70
|
+
engineDir: paths.engine,
|
|
71
|
+
patchFilename: patch.filename,
|
|
72
|
+
added: scanResult.added,
|
|
73
|
+
config,
|
|
74
|
+
declarationsOut: options.declarationsOut,
|
|
75
|
+
});
|
|
76
|
+
if (options.scanFiles === undefined) {
|
|
77
|
+
const proceed = await confirmBroadScanAdditions({
|
|
78
|
+
patchFilename: patch.filename,
|
|
79
|
+
added: scanResult.added,
|
|
80
|
+
isDryRun,
|
|
81
|
+
yes: options.yes === true,
|
|
82
|
+
isInteractive: stdioIsInteractive(),
|
|
83
|
+
});
|
|
84
|
+
if (!proceed)
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
if (adjacentCtx.refuseAdjacentUnmanaged) {
|
|
88
|
+
const adjacentArgs = {
|
|
89
|
+
patch,
|
|
90
|
+
paths,
|
|
91
|
+
manifest,
|
|
92
|
+
currentFilesAffected: scanResult.updated,
|
|
93
|
+
ctx: adjacentCtx,
|
|
94
|
+
afterScan: true,
|
|
95
|
+
};
|
|
96
|
+
if (await reportAdjacentUnmanagedFiles(adjacentArgs))
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
return { updated: scanResult.updated, added: scanResult.added };
|
|
100
|
+
}
|
|
43
101
|
async function reExportSinglePatch(args) {
|
|
44
102
|
const { patch, paths, manifest, options, isDryRun, config, adjacentCtx, driftCtx, lintCtx } = args;
|
|
45
103
|
const removed = new Set(options.expectRemoved ?? []);
|
|
@@ -50,41 +108,11 @@ async function reExportSinglePatch(args) {
|
|
|
50
108
|
info(` - ${file} (--expect-removed)`);
|
|
51
109
|
// --- Scan for new/removed files ---
|
|
52
110
|
if (options.scan) {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
isDryRun,
|
|
59
|
-
...(options.scanFiles !== undefined ? { scanFiles: options.scanFiles } : {}),
|
|
60
|
-
});
|
|
61
|
-
// Forward-import gate at adoption time (runs in dry-run too): refuse
|
|
62
|
-
// to adopt unmanaged files that import modules created by later
|
|
63
|
-
// patches, because the after-the-fact lint failure this leaves behind
|
|
64
|
-
// is the field-reported footgun. Applies to broad --scan, --scan-file,
|
|
65
|
-
// and the --scan-files bulk assignments alike.
|
|
66
|
-
await assertScanAdoptionsHaveNoForwardImports({
|
|
67
|
-
patchesDir: paths.patches,
|
|
68
|
-
engineDir: paths.engine,
|
|
69
|
-
patchFilename: patch.filename,
|
|
70
|
-
added: scanResult.added,
|
|
71
|
-
config,
|
|
72
|
-
});
|
|
73
|
-
if (options.scanFiles === undefined) {
|
|
74
|
-
const isInteractive = stdioIsInteractive();
|
|
75
|
-
const proceed = await confirmBroadScanAdditions({
|
|
76
|
-
patchFilename: patch.filename,
|
|
77
|
-
added: scanResult.added,
|
|
78
|
-
isDryRun,
|
|
79
|
-
yes: options.yes === true,
|
|
80
|
-
isInteractive,
|
|
81
|
-
});
|
|
82
|
-
if (!proceed) {
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
currentFilesAffected = scanResult.updated;
|
|
87
|
-
adoptedFiles = scanResult.added;
|
|
111
|
+
const adopted = await adoptScannedFiles(args, currentFilesAffected);
|
|
112
|
+
if (adopted === undefined)
|
|
113
|
+
return false;
|
|
114
|
+
currentFilesAffected = adopted.updated;
|
|
115
|
+
adoptedFiles = adopted.added;
|
|
88
116
|
}
|
|
89
117
|
else if (options.files === undefined) {
|
|
90
118
|
// When neither `--scan` nor `--files` is set and some of the manifest's
|
|
@@ -143,7 +171,17 @@ async function reExportSinglePatch(args) {
|
|
|
143
171
|
}
|
|
144
172
|
const untrackedSet = new Set(untrackedAbsent);
|
|
145
173
|
const existingFiles = currentFilesAffected.filter((f) => !untrackedSet.has(f));
|
|
146
|
-
|
|
174
|
+
let diffContent = await getDiffForFilesAgainstHead(paths.engine, existingFiles);
|
|
175
|
+
const selection = hunkSelectionOf(options);
|
|
176
|
+
if (selection !== undefined) {
|
|
177
|
+
diffContent = await buildHunkSelectedDiff({
|
|
178
|
+
engineDir: paths.engine,
|
|
179
|
+
selection,
|
|
180
|
+
oldBody: await readText(join(paths.patches, patch.filename)),
|
|
181
|
+
normalDiff: diffContent,
|
|
182
|
+
files: existingFiles,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
147
185
|
assertScanFileAdditionsHaveDiffHunks({
|
|
148
186
|
diffContent,
|
|
149
187
|
patchFilename: patch.filename,
|
|
@@ -173,6 +211,10 @@ async function reExportSinglePatch(args) {
|
|
|
173
211
|
return false;
|
|
174
212
|
}
|
|
175
213
|
const { updates, ignoreChecks, effectiveTier, existingIgnoreSet, flagIgnoreSet } = computeReExportUpdates(patch, options, currentFilesAffected);
|
|
214
|
+
const stalePlan = planStaleRegistrationDrop(manifest, patch, currentFilesAffected);
|
|
215
|
+
if (stalePlan !== undefined)
|
|
216
|
+
reportStaleRegistrationDrop(patch.filename, stalePlan, isDryRun);
|
|
217
|
+
const unset = applyStaleRegistrationDrop(stalePlan, updates);
|
|
176
218
|
enforceProjectedManifestPolicy({ config, manifest, patch, updates, options });
|
|
177
219
|
// Cross-patch projection gate (runs in dry-run too): the refreshed body
|
|
178
220
|
// must not introduce queue-level errors the current queue lacks, e.g. an
|
|
@@ -188,7 +230,7 @@ async function reExportSinglePatch(args) {
|
|
|
188
230
|
// Lint against the once-per-invocation hoisted context (queue context +
|
|
189
231
|
// queue-wide checkJs program + per-patch result cache) instead of
|
|
190
232
|
// rebuilding ~37 s of setup for every patch in the loop.
|
|
191
|
-
const projectedMetadata = { ...patch, ...updates };
|
|
233
|
+
const projectedMetadata = withoutFields({ ...patch, ...updates }, unset);
|
|
192
234
|
const lintResult = await lintReExportedPatch({
|
|
193
235
|
lintCtx,
|
|
194
236
|
patch,
|
|
@@ -222,6 +264,8 @@ async function reExportSinglePatch(args) {
|
|
|
222
264
|
lintCtx,
|
|
223
265
|
diffContent,
|
|
224
266
|
updates,
|
|
267
|
+
unset,
|
|
268
|
+
stalePlan,
|
|
225
269
|
projectedMetadata,
|
|
226
270
|
existingFiles,
|
|
227
271
|
lintResult,
|
|
@@ -263,7 +307,8 @@ async function commitRefreshedPatch(args) {
|
|
|
263
307
|
filename: patch.filename,
|
|
264
308
|
newContent: diffContent,
|
|
265
309
|
updates,
|
|
266
|
-
|
|
310
|
+
...(args.unset.length > 0 ? { unset: args.unset } : {}),
|
|
311
|
+
onCommitted: staleRecorder(paths.patches, patch.filename, args.stalePlan),
|
|
267
312
|
policyGate: {
|
|
268
313
|
config,
|
|
269
314
|
command: 're-export',
|
|
@@ -279,10 +324,7 @@ async function commitRefreshedPatch(args) {
|
|
|
279
324
|
if (patchIndex !== -1) {
|
|
280
325
|
const existingEntry = manifest.patches[patchIndex];
|
|
281
326
|
if (existingEntry) {
|
|
282
|
-
manifest.patches[patchIndex] = {
|
|
283
|
-
...existingEntry,
|
|
284
|
-
...updates,
|
|
285
|
-
};
|
|
327
|
+
manifest.patches[patchIndex] = withoutFields({ ...existingEntry, ...updates }, args.unset);
|
|
286
328
|
}
|
|
287
329
|
}
|
|
288
330
|
// A bulk run refreshes every patch. Saying "Re-exported" for one whose body
|
|
@@ -295,6 +337,24 @@ async function commitRefreshedPatch(args) {
|
|
|
295
337
|
info(`Unchanged ${patch.filename}`);
|
|
296
338
|
}
|
|
297
339
|
}
|
|
340
|
+
/** The history hook for a write that drops stale registrations, if any. */
|
|
341
|
+
function staleRecorder(patchesDir, patchFilename, plan) {
|
|
342
|
+
if (plan === undefined)
|
|
343
|
+
return undefined;
|
|
344
|
+
return () => recordStaleRegistrationDrop(patchesDir, patchFilename, plan);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Folds the stale-registration drop into `updates`: the kept staged block
|
|
348
|
+
* is set, or the field is returned for unsetting when nothing is left.
|
|
349
|
+
*/
|
|
350
|
+
function applyStaleRegistrationDrop(plan, updates) {
|
|
351
|
+
if (plan === undefined)
|
|
352
|
+
return [];
|
|
353
|
+
if (plan.stagedDependencies === undefined)
|
|
354
|
+
return ['stagedDependencies'];
|
|
355
|
+
updates.stagedDependencies = plan.stagedDependencies;
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
298
358
|
/**
|
|
299
359
|
* Threads the patch's own `lintIgnore` list through so the per-patch
|
|
300
360
|
* suppression honoured by export/export-all is also honoured here. A
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PatchesManifest, PatchMetadata, PatchStagedDependencies, PatchStagedRegistration } from '../types/commands/index.js';
|
|
2
|
+
/** The holder's registrations split by whether the queue still owns `creates`. */
|
|
3
|
+
export interface StaleRegistrationPlan {
|
|
4
|
+
/** Registrations whose `creates` path no patch owns. */
|
|
5
|
+
dropped: PatchStagedRegistration[];
|
|
6
|
+
/** The staged block to write, or undefined when nothing is left. */
|
|
7
|
+
stagedDependencies: PatchStagedDependencies | undefined;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Finds `patch`'s registrations whose `creates` path no patch in the queue
|
|
11
|
+
* owns, judging `patch` by the file list this re-export will write.
|
|
12
|
+
* Returns undefined when nothing is stale.
|
|
13
|
+
*/
|
|
14
|
+
export declare function planStaleRegistrationDrop(manifest: PatchesManifest, patch: PatchMetadata, currentFilesAffected: readonly string[]): StaleRegistrationPlan | undefined;
|
|
15
|
+
/** Names each dropped registration, in both a dry run and a write. */
|
|
16
|
+
export declare function reportStaleRegistrationDrop(patchFilename: string, plan: StaleRegistrationPlan, isDryRun: boolean): void;
|
|
17
|
+
/** Records the dropped declarations as a delta, like `staged-dependency --remove`. */
|
|
18
|
+
export declare function recordStaleRegistrationDrop(patchesDir: string, patchFilename: string, plan: StaleRegistrationPlan): Promise<void>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Re-export prunes a staged registration the queue can no longer satisfy.
|
|
4
|
+
*
|
|
5
|
+
* A merge that deletes a file whose jar.mn line lives in another patch
|
|
6
|
+
* left the staged registration on that patch: re-export kept it, lint then
|
|
7
|
+
* warned `staged-dependency-unused`, and the stale declaration also kept
|
|
8
|
+
* the holder measuring above a just-lowered size ceiling. When no patch in
|
|
9
|
+
* the queue owns a registration's `creates` path any more, the declaration
|
|
10
|
+
* cannot be met, so the holder's own re-export drops it and says so.
|
|
11
|
+
*/
|
|
12
|
+
import { appendHistoryBestEffort } from '../core/history-log.js';
|
|
13
|
+
import { info } from '../utils/logger.js';
|
|
14
|
+
/**
|
|
15
|
+
* Finds `patch`'s registrations whose `creates` path no patch in the queue
|
|
16
|
+
* owns, judging `patch` by the file list this re-export will write.
|
|
17
|
+
* Returns undefined when nothing is stale.
|
|
18
|
+
*/
|
|
19
|
+
export function planStaleRegistrationDrop(manifest, patch, currentFilesAffected) {
|
|
20
|
+
const registrations = patch.stagedDependencies?.registrations ?? [];
|
|
21
|
+
if (registrations.length === 0)
|
|
22
|
+
return undefined;
|
|
23
|
+
const owned = new Set(currentFilesAffected);
|
|
24
|
+
for (const entry of manifest.patches) {
|
|
25
|
+
if (entry.filename === patch.filename)
|
|
26
|
+
continue;
|
|
27
|
+
for (const file of entry.filesAffected)
|
|
28
|
+
owned.add(file);
|
|
29
|
+
}
|
|
30
|
+
const dropped = registrations.filter((registration) => !owned.has(registration.creates));
|
|
31
|
+
if (dropped.length === 0)
|
|
32
|
+
return undefined;
|
|
33
|
+
const kept = registrations.filter((registration) => owned.has(registration.creates));
|
|
34
|
+
const forwardImports = patch.stagedDependencies?.forwardImports ?? [];
|
|
35
|
+
const staged = {
|
|
36
|
+
...(forwardImports.length > 0 ? { forwardImports } : {}),
|
|
37
|
+
...(kept.length > 0 ? { registrations: kept } : {}),
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
dropped,
|
|
41
|
+
stagedDependencies: Object.keys(staged).length > 0 ? staged : undefined,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Names each dropped registration, in both a dry run and a write. */
|
|
45
|
+
export function reportStaleRegistrationDrop(patchFilename, plan, isDryRun) {
|
|
46
|
+
const verb = isDryRun ? 'would drop' : 'drops';
|
|
47
|
+
for (const registration of plan.dropped) {
|
|
48
|
+
info(`${patchFilename}: ${verb} staged registration ${registration.file} "${registration.line.trim()}": ` +
|
|
49
|
+
`no patch in the queue creates ${registration.creates} any more.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Records the dropped declarations as a delta, like `staged-dependency --remove`. */
|
|
53
|
+
export async function recordStaleRegistrationDrop(patchesDir, patchFilename, plan) {
|
|
54
|
+
await appendHistoryBestEffort(patchesDir, {
|
|
55
|
+
operation: 're-export-staged-prune',
|
|
56
|
+
args: {
|
|
57
|
+
filename: patchFilename,
|
|
58
|
+
removed: plan.dropped.map((registration) => ({ kind: 'registration', ...registration })),
|
|
59
|
+
},
|
|
60
|
+
result: 'ok',
|
|
61
|
+
}, `re-export pruned stale staged registrations on ${patchFilename}`);
|
|
62
|
+
}
|