@hominis/fireforge 0.40.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 +79 -0
- package/README.md +16 -10
- package/dist/bin/fireforge.js +25 -4
- package/dist/build-info.json +9 -0
- package/dist/src/cli.js +20 -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 +13 -3
- 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 +16 -7
- 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 +12 -59
- 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.js +4 -4
- package/dist/src/commands/manifest.js +1 -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 +151 -50
- 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 +10 -0
- package/dist/src/commands/re-export-adjacent.js +5 -6
- 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 +8 -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 +113 -268
- 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 +11 -21
- package/dist/src/commands/source.js +3 -9
- package/dist/src/commands/status-check.d.ts +13 -0
- package/dist/src/commands/status-check.js +31 -13
- 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 +184 -175
- 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 +41 -4
- 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/tree.d.ts +3 -0
- package/dist/src/commands/tree.js +171 -25
- 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 +34 -36
- 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 +1 -1
- package/dist/src/core/lint-cache.js +33 -10
- 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.js +3 -5
- package/dist/src/core/patch-lint-checkjs.d.ts +25 -3
- package/dist/src/core/patch-lint-checkjs.js +78 -7
- 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-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-reexports.d.ts +1 -0
- package/dist/src/core/patch-lint-reexports.js +1 -0
- package/dist/src/core/patch-lint.d.ts +8 -0
- package/dist/src/core/patch-lint.js +84 -6
- 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-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 +0 -8
- package/dist/src/core/smoke-patterns.js +3 -2
- 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 +6 -1
- package/dist/src/core/status-classify.js +101 -73
- 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/toolchain-preflight.js +24 -26
- package/dist/src/core/tree-guard.d.ts +1 -1
- package/dist/src/core/tree-guard.js +67 -12
- package/dist/src/core/tree-store.d.ts +86 -10
- package/dist/src/core/tree-store.js +310 -72
- 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/errors/base.d.ts +13 -0
- package/dist/src/errors/base.js +13 -0
- package/dist/src/errors/rebase.d.ts +13 -0
- package/dist/src/errors/rebase.js +17 -0
- package/dist/src/types/commands/options.d.ts +86 -1
- package/dist/src/types/commands/patches.d.ts +8 -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 +23 -2
- package/dist/src/utils/validation.js +29 -3
- package/package.json +9 -9
|
@@ -22,15 +22,17 @@ import { Command } from 'commander';
|
|
|
22
22
|
import { loadConfig } from '../../core/config.js';
|
|
23
23
|
import { appendHistory, confirmDestructive } from '../../core/destructive.js';
|
|
24
24
|
import { patchNameSlug } from '../../core/patch-export.js';
|
|
25
|
+
import { buildPatchQueueContext, formatPatchLintIssue, lintPatchQueue, } from '../../core/patch-lint.js';
|
|
25
26
|
import { withPatchDirectoryLock } from '../../core/patch-lock.js';
|
|
26
27
|
import { loadPatchesManifest, rewriteStagedDependencyOwners, savePatchesManifest, } from '../../core/patch-manifest.js';
|
|
27
|
-
import { buildProjectedManifest, enforcePatchPolicy } from '../../core/patch-policy.js';
|
|
28
|
+
import { buildProjectedManifest, enforcePatchPolicy, getPatchPolicyCategories, isCategoryAllowedByConfig, } from '../../core/patch-policy.js';
|
|
28
29
|
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
29
30
|
import { toError } from '../../utils/errors.js';
|
|
30
31
|
import { pathExists } from '../../utils/fs.js';
|
|
31
32
|
import { info, intro, outro, warn } from '../../utils/logger.js';
|
|
32
|
-
import { pickDefined } from '../../utils/options.js';
|
|
33
|
+
import { addWaitLockOption, commanderArgParser, pickDefined, resolveWaitLockSeconds, } from '../../utils/options.js';
|
|
33
34
|
import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
|
|
35
|
+
import { projectReorder } from './reorder.js';
|
|
34
36
|
/**
|
|
35
37
|
* Pulls the ordinal-string + category prefix out of a patch filename so
|
|
36
38
|
* the rename keeps the existing ordinal padding verbatim. Returning the
|
|
@@ -54,7 +56,11 @@ function splitPatchFilename(filename) {
|
|
|
54
56
|
* rolls the filesystem rename back.
|
|
55
57
|
*/
|
|
56
58
|
async function commitRenameUnderLock(input) {
|
|
57
|
-
const { patchesDir, target, newFilename, newName, newDescription, filenameChanging, nameChanging, descriptionChanging, } = input;
|
|
59
|
+
const { patchesDir, target, newFilename, newName, newDescription, filenameChanging, nameChanging, descriptionChanging, orderChanging, categoryChanging, } = input;
|
|
60
|
+
const identityUpdates = {
|
|
61
|
+
...(orderChanging && input.newOrder !== undefined ? { order: input.newOrder } : {}),
|
|
62
|
+
...(categoryChanging && input.newCategory !== undefined ? { category: input.newCategory } : {}),
|
|
63
|
+
};
|
|
58
64
|
await withPatchDirectoryLock(patchesDir, async () => {
|
|
59
65
|
const fresh = await loadPatchesManifest(patchesDir);
|
|
60
66
|
if (!fresh) {
|
|
@@ -68,6 +74,12 @@ async function commitRenameUnderLock(input) {
|
|
|
68
74
|
if (!before) {
|
|
69
75
|
throw new GeneralError(`Patch ${target.filename} disappeared from the manifest during rename.`);
|
|
70
76
|
}
|
|
77
|
+
if (orderChanging && input.newOrder !== undefined) {
|
|
78
|
+
const orderHolder = fresh.patches.find((p) => p.order === input.newOrder && p.filename !== target.filename);
|
|
79
|
+
if (orderHolder) {
|
|
80
|
+
throw new InvalidArgumentError(`Order ${String(input.newOrder)} was claimed by ${orderHolder.filename} concurrently. Pick an unused order, or use "fireforge patch reorder" to renumber siblings.`, '--order');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
71
83
|
let oldPath;
|
|
72
84
|
let newPath;
|
|
73
85
|
if (filenameChanging) {
|
|
@@ -84,6 +96,7 @@ async function commitRenameUnderLock(input) {
|
|
|
84
96
|
...before,
|
|
85
97
|
filename: newFilename,
|
|
86
98
|
name: newName,
|
|
99
|
+
...identityUpdates,
|
|
87
100
|
...(descriptionChanging ? { description: newDescription ?? '' } : {}),
|
|
88
101
|
};
|
|
89
102
|
// Staged-dependency owners on other patches reference the old
|
|
@@ -95,6 +108,7 @@ async function commitRenameUnderLock(input) {
|
|
|
95
108
|
else {
|
|
96
109
|
fresh.patches[idx] = {
|
|
97
110
|
...before,
|
|
111
|
+
...identityUpdates,
|
|
98
112
|
...(nameChanging ? { name: newName } : {}),
|
|
99
113
|
...(descriptionChanging ? { description: newDescription ?? '' } : {}),
|
|
100
114
|
};
|
|
@@ -132,6 +146,10 @@ async function commitRenameUnderLock(input) {
|
|
|
132
146
|
oldName: target.name,
|
|
133
147
|
newName,
|
|
134
148
|
...(descriptionChanging ? { oldDescription: target.description, newDescription } : {}),
|
|
149
|
+
...(orderChanging ? { oldOrder: target.order, newOrder: input.newOrder } : {}),
|
|
150
|
+
...(categoryChanging
|
|
151
|
+
? { oldCategory: target.category, newCategory: input.newCategory }
|
|
152
|
+
: {}),
|
|
135
153
|
},
|
|
136
154
|
...(input.yes === true ? { yes: true } : {}),
|
|
137
155
|
...(input.forceUnsafe === true ? { unsafeOverride: true } : {}),
|
|
@@ -141,66 +159,134 @@ async function commitRenameUnderLock(input) {
|
|
|
141
159
|
catch (historyError) {
|
|
142
160
|
warn(`History log append failed after patch rename committed (${newFilename}): ${toError(historyError).message}`);
|
|
143
161
|
}
|
|
144
|
-
});
|
|
162
|
+
}, { waitLockSeconds: input.waitLockSeconds, command: 'patch rename' });
|
|
145
163
|
}
|
|
146
|
-
/**
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
export async function patchRenameCommand(projectRoot, identifier, options = {}) {
|
|
155
|
-
const isDryRun = options.dryRun === true;
|
|
156
|
-
intro(isDryRun ? 'FireForge patch rename (dry run)' : 'FireForge patch rename');
|
|
157
|
-
if (options.to === undefined || options.to.trim() === '') {
|
|
158
|
-
throw new InvalidArgumentError('Specify --to <new-name>. The new name is sanitised into the filename slug the same way `export --name` is.', 'patch rename');
|
|
164
|
+
/** Validates the flags and resolves the new filename/identity fields. */
|
|
165
|
+
function resolveRenamePlan(target, options, config) {
|
|
166
|
+
const hasTo = options.to !== undefined && options.to.trim() !== '';
|
|
167
|
+
if (!hasTo &&
|
|
168
|
+
options.category === undefined &&
|
|
169
|
+
options.order === undefined &&
|
|
170
|
+
options.description === undefined) {
|
|
171
|
+
throw new InvalidArgumentError('Specify at least one of --to <new-name>, --category <category>, --order <n>, or --description. --to is sanitised into the filename slug the same way `export --name` is.', 'patch rename');
|
|
159
172
|
}
|
|
160
|
-
const config = await loadConfig(projectRoot);
|
|
161
|
-
const { paths, manifest } = await requirePatchQueue(projectRoot);
|
|
162
|
-
const target = requirePatchTarget(identifier, manifest.patches);
|
|
163
173
|
const split = splitPatchFilename(target.filename);
|
|
164
174
|
if (!split) {
|
|
165
175
|
throw new GeneralError(`Cannot rename ${target.filename}: filename does not match the expected {ordinal}-{category}-{slug}.patch convention. Re-export the patch instead.`);
|
|
166
176
|
}
|
|
167
|
-
const
|
|
177
|
+
const newCategory = options.category ?? split.category;
|
|
178
|
+
if (options.category !== undefined && !isCategoryAllowedByConfig(config, options.category)) {
|
|
179
|
+
throw new InvalidArgumentError(`Invalid category. Must be one of: ${getPatchPolicyCategories(config).join(', ')}`, '--category');
|
|
180
|
+
}
|
|
181
|
+
// Keep the existing slug verbatim when --to is absent: re-running
|
|
182
|
+
// `patchNameSlug` against a different category could strip a
|
|
183
|
+
// category-looking prefix out of an established slug.
|
|
184
|
+
const newSlug = hasTo && options.to !== undefined ? patchNameSlug(options.to, newCategory) : split.slug;
|
|
168
185
|
if (newSlug === '') {
|
|
169
186
|
throw new InvalidArgumentError('--to must contain at least one alphanumeric character after sanitisation.', 'patch rename');
|
|
170
187
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
188
|
+
// Preserve the ordinal's zero-padding width; a wider order simply
|
|
189
|
+
// prints unpadded-longer.
|
|
190
|
+
const newOrdinalStr = options.order !== undefined
|
|
191
|
+
? String(options.order).padStart(split.ordinalStr.length, '0')
|
|
192
|
+
: split.ordinalStr;
|
|
193
|
+
const newFilename = `${newOrdinalStr}-${newCategory}-${newSlug}.patch`;
|
|
194
|
+
return {
|
|
195
|
+
hasTo,
|
|
196
|
+
newCategory,
|
|
197
|
+
newSlug,
|
|
198
|
+
newFilename,
|
|
199
|
+
filenameChanging: newFilename !== target.filename,
|
|
200
|
+
nameChanging: hasTo && options.to !== target.name,
|
|
201
|
+
descriptionChanging: options.description !== undefined && options.description !== target.description,
|
|
202
|
+
orderChanging: options.order !== undefined && options.order !== target.order,
|
|
203
|
+
categoryChanging: options.category !== undefined &&
|
|
204
|
+
(options.category !== target.category || options.category !== split.category),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Pre-flight refusals: order collision (with a pointer to the verb that
|
|
209
|
+
* renumbers siblings — `--order` means "this exact unused sparse slot",
|
|
210
|
+
* mirroring `export --order`), projected cross-patch lint on an order
|
|
211
|
+
* change (forward imports resolve by queue position, FORGE J10), and the
|
|
212
|
+
* filename collision. The authoritative collision checks run again inside
|
|
213
|
+
* the lock to close the TOCTOU window.
|
|
214
|
+
*/
|
|
215
|
+
async function assertRenamePreconditions(patchesDir, manifest, target, plan, options, config) {
|
|
216
|
+
if (plan.orderChanging && options.order !== undefined) {
|
|
217
|
+
const holder = manifest.patches.find((p) => p.order === options.order && p.filename !== target.filename);
|
|
218
|
+
if (holder) {
|
|
219
|
+
throw new InvalidArgumentError(`Order ${String(options.order)} is already used by ${holder.filename}. Pick an unused order, or use "fireforge patch reorder ${target.filename} --to ${String(options.order)}" to renumber siblings.`, '--order');
|
|
220
|
+
}
|
|
221
|
+
if (options.forceUnsafe !== true) {
|
|
222
|
+
const baseCtx = await buildPatchQueueContext(patchesDir, config);
|
|
223
|
+
const projected = projectReorder(baseCtx, new Map([[target.filename, { newFilename: plan.newFilename, newOrder: options.order }]]));
|
|
224
|
+
const projectedErrors = lintPatchQueue(projected).filter((i) => i.severity === 'error');
|
|
225
|
+
if (projectedErrors.length > 0) {
|
|
226
|
+
throw new InvalidArgumentError(`Refusing to run patch rename: the order change would introduce ${String(projectedErrors.length)} cross-patch lint error(s):\n ${projectedErrors
|
|
227
|
+
.map(formatPatchLintIssue)
|
|
228
|
+
.join('\n ')}\nPass --force-unsafe to override.`, '--force-unsafe');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
182
231
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// close the TOCTOU window — surface a helpful error here when the
|
|
186
|
-
// collision is obvious so the operator does not get surprised by a
|
|
187
|
-
// late refusal after a confirmation prompt.
|
|
188
|
-
if (filenameChanging) {
|
|
189
|
-
const collision = manifest.patches.find((p) => p.filename === newFilename && p.filename !== target.filename);
|
|
232
|
+
if (plan.filenameChanging) {
|
|
233
|
+
const collision = manifest.patches.find((p) => p.filename === plan.newFilename && p.filename !== target.filename);
|
|
190
234
|
if (collision) {
|
|
191
|
-
throw new InvalidArgumentError(`Cannot rename to "${newFilename}" — a different patch already uses that filename.`, 'patch rename');
|
|
235
|
+
throw new InvalidArgumentError(`Cannot rename to "${plan.newFilename}" — a different patch already uses that filename.`, 'patch rename');
|
|
192
236
|
}
|
|
193
237
|
}
|
|
238
|
+
}
|
|
239
|
+
/** Change-summary lines shown by the confirmation prompt. */
|
|
240
|
+
function buildRenameSummary(target, plan, options) {
|
|
194
241
|
const summary = [];
|
|
195
|
-
if (filenameChanging) {
|
|
196
|
-
summary.push(`rename ${target.filename} → ${newFilename}`);
|
|
242
|
+
if (plan.filenameChanging) {
|
|
243
|
+
summary.push(`rename ${target.filename} → ${plan.newFilename}`);
|
|
197
244
|
}
|
|
198
|
-
if (nameChanging) {
|
|
245
|
+
if (plan.nameChanging) {
|
|
199
246
|
summary.push(`name: "${target.name}" → "${options.to}"`);
|
|
200
247
|
}
|
|
201
|
-
if (descriptionChanging) {
|
|
248
|
+
if (plan.descriptionChanging) {
|
|
202
249
|
summary.push(`description: "${target.description || '(none)'}" → "${options.description ?? '(none)'}"`);
|
|
203
250
|
}
|
|
251
|
+
if (plan.orderChanging && options.order !== undefined) {
|
|
252
|
+
summary.push(`order: ${String(target.order)} → ${String(options.order)}`);
|
|
253
|
+
}
|
|
254
|
+
if (plan.categoryChanging) {
|
|
255
|
+
summary.push(`category: ${target.category} → ${plan.newCategory}`);
|
|
256
|
+
}
|
|
257
|
+
return summary;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Runs the `patch rename` command: relabels filename + manifest entry
|
|
261
|
+
* (name, category, order, description) for a single patch atomically.
|
|
262
|
+
*
|
|
263
|
+
* @param projectRoot - Project root directory
|
|
264
|
+
* @param identifier - Patch filename, ordinal, or manifest `name`
|
|
265
|
+
* @param options - Command options (at least one change flag required)
|
|
266
|
+
*/
|
|
267
|
+
export async function patchRenameCommand(projectRoot, identifier, options = {}) {
|
|
268
|
+
const isDryRun = options.dryRun === true;
|
|
269
|
+
intro(isDryRun ? 'FireForge patch rename (dry run)' : 'FireForge patch rename');
|
|
270
|
+
const config = await loadConfig(projectRoot);
|
|
271
|
+
const { paths, manifest } = await requirePatchQueue(projectRoot);
|
|
272
|
+
const target = requirePatchTarget(identifier, manifest.patches);
|
|
273
|
+
const plan = resolveRenamePlan(target, options, config);
|
|
274
|
+
const { hasTo, newCategory, newSlug, newFilename, filenameChanging, nameChanging, descriptionChanging, orderChanging, categoryChanging, } = plan;
|
|
275
|
+
if (!filenameChanging &&
|
|
276
|
+
!nameChanging &&
|
|
277
|
+
!descriptionChanging &&
|
|
278
|
+
!orderChanging &&
|
|
279
|
+
!categoryChanging) {
|
|
280
|
+
info(`${target.filename}: nothing to change — filename already "${target.filename}"` +
|
|
281
|
+
(hasTo && options.to !== undefined ? ` (slug of "${options.to}" is "${newSlug}")` : '') +
|
|
282
|
+
`, name already "${target.name}"` +
|
|
283
|
+
(options.description !== undefined ? ', description already matches' : '') +
|
|
284
|
+
'.');
|
|
285
|
+
outro(isDryRun ? 'Dry run complete — no changes made' : 'Patch rename (no-op)');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
await assertRenamePreconditions(paths.patches, manifest, target, plan, options, config);
|
|
289
|
+
const summary = buildRenameSummary(target, plan, options);
|
|
204
290
|
enforcePatchPolicy({
|
|
205
291
|
config,
|
|
206
292
|
manifest: buildProjectedManifest(manifest, manifest.patches.map((entry) => entry.filename === target.filename
|
|
@@ -208,6 +294,8 @@ export async function patchRenameCommand(projectRoot, identifier, options = {})
|
|
|
208
294
|
...entry,
|
|
209
295
|
filename: newFilename,
|
|
210
296
|
name: nameChanging ? (options.to ?? entry.name) : entry.name,
|
|
297
|
+
...(orderChanging && options.order !== undefined ? { order: options.order } : {}),
|
|
298
|
+
...(categoryChanging ? { category: newCategory } : {}),
|
|
211
299
|
...(descriptionChanging ? { description: options.description ?? '' } : {}),
|
|
212
300
|
}
|
|
213
301
|
: entry)),
|
|
@@ -234,14 +322,19 @@ export async function patchRenameCommand(projectRoot, identifier, options = {})
|
|
|
234
322
|
patchesDir: paths.patches,
|
|
235
323
|
target,
|
|
236
324
|
newFilename,
|
|
237
|
-
newName: options.to,
|
|
325
|
+
newName: hasTo && options.to !== undefined ? options.to : target.name,
|
|
238
326
|
...(options.description !== undefined ? { newDescription: options.description } : {}),
|
|
327
|
+
...(orderChanging && options.order !== undefined ? { newOrder: options.order } : {}),
|
|
328
|
+
...(categoryChanging ? { newCategory } : {}),
|
|
239
329
|
filenameChanging,
|
|
240
330
|
nameChanging,
|
|
241
331
|
descriptionChanging,
|
|
332
|
+
orderChanging,
|
|
333
|
+
categoryChanging,
|
|
242
334
|
...(options.yes === true ? { yes: true } : {}),
|
|
243
335
|
...(options.forceUnsafe === true ? { forceUnsafe: true } : {}),
|
|
244
336
|
config,
|
|
337
|
+
waitLockSeconds: resolveWaitLockSeconds(options.waitLock),
|
|
245
338
|
});
|
|
246
339
|
if (filenameChanging) {
|
|
247
340
|
info(`${target.filename} → ${newFilename}`);
|
|
@@ -259,15 +352,23 @@ export async function patchRenameCommand(projectRoot, identifier, options = {})
|
|
|
259
352
|
*/
|
|
260
353
|
export function registerPatchRename(parent, context) {
|
|
261
354
|
const { getProjectRoot, withErrorHandling } = context;
|
|
262
|
-
parent
|
|
355
|
+
const command = parent
|
|
263
356
|
.command('rename <name>')
|
|
264
|
-
.description('Rename a patch: filename
|
|
265
|
-
.
|
|
357
|
+
.description('Rename a patch: filename, manifest name, category, order, and/or description update in one transaction without rewriting the .patch body.')
|
|
358
|
+
.option('--to <new-name>', 'New human-readable name, category-prefixed slug, or full filename stem (normalised into the filename slug)')
|
|
359
|
+
.option('--category <category>', 'New category for the patch (validated against configured categories); rewrites the filename prefix and manifest row in one transaction')
|
|
360
|
+
.option('--order <n>', 'Move the patch to this exact unused order; refuses on collision — use "patch reorder" to renumber siblings', commanderArgParser((raw) => {
|
|
361
|
+
const n = Number.parseInt(raw, 10);
|
|
362
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
363
|
+
throw new InvalidArgumentError(`--order must be a positive integer, got "${raw}".`, '--order');
|
|
364
|
+
}
|
|
365
|
+
return n;
|
|
366
|
+
}))
|
|
266
367
|
.option('-d, --description <text>', 'Replacement description (omit to leave description unchanged)')
|
|
267
368
|
.option('--dry-run', 'Show what would change without writing')
|
|
268
369
|
.option('-y, --yes', 'Skip confirmation prompt (required for non-TTY)')
|
|
269
|
-
.option('--force-unsafe', 'Bypass force-mode patchPolicy refusals')
|
|
270
|
-
|
|
370
|
+
.option('--force-unsafe', 'Bypass force-mode patchPolicy refusals');
|
|
371
|
+
addWaitLockOption(command).action(withErrorHandling(async (name, options) => {
|
|
271
372
|
await patchRenameCommand(getProjectRoot(), name, pickDefined(options));
|
|
272
373
|
}));
|
|
273
374
|
}
|
|
@@ -8,10 +8,19 @@
|
|
|
8
8
|
* before any bytes move.
|
|
9
9
|
*/
|
|
10
10
|
import { Command } from 'commander';
|
|
11
|
+
import { type PatchQueueContext } from '../../core/patch-lint.js';
|
|
12
|
+
import { type PatchRenameEntry } from '../../core/patch-manifest.js';
|
|
11
13
|
import type { CommandContext } from '../../types/cli.js';
|
|
12
14
|
import type { PatchMetadata, PatchReorderOptions } from '../../types/commands/index.js';
|
|
13
15
|
/** Builds a new patch filename by replacing the numeric prefix with `newOrder`. */
|
|
14
16
|
export declare function rebuildFilenameForOrder(existing: PatchMetadata, newOrder: number): string;
|
|
17
|
+
/**
|
|
18
|
+
* Applies a rename map to a {@link PatchQueueContext} so cross-patch lint
|
|
19
|
+
* can run against the projected state without touching disk. Exported for
|
|
20
|
+
* `patch rename --order`, which projects the same single-row move shape
|
|
21
|
+
* (FORGE J10).
|
|
22
|
+
*/
|
|
23
|
+
export declare function projectReorder(base: PatchQueueContext, renameMap: Map<string, PatchRenameEntry>): PatchQueueContext;
|
|
15
24
|
/**
|
|
16
25
|
* Runs the `patch reorder` command: computes a rename map moving the
|
|
17
26
|
* target patch to the requested slot, projects the new order through
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
import { Command, Option } from 'commander';
|
|
12
12
|
import { loadConfig } from '../../core/config.js';
|
|
13
13
|
import { appendHistory, confirmDestructive, } from '../../core/destructive.js';
|
|
14
|
-
import { buildPatchQueueContext, lintPatchQueue, } from '../../core/patch-lint.js';
|
|
14
|
+
import { buildPatchQueueContext, formatPatchLintIssue, lintPatchQueue, } from '../../core/patch-lint.js';
|
|
15
15
|
import { withPatchDirectoryLock } from '../../core/patch-lock.js';
|
|
16
16
|
import { loadPatchesManifest, renumberPatchesInManifest, resolvePatchIdentifier, rewriteStagedDependencyOwners, } from '../../core/patch-manifest.js';
|
|
17
17
|
import { applyRenameMapToManifest, enforcePatchPolicy } from '../../core/patch-policy.js';
|
|
18
18
|
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
19
19
|
import { toError } from '../../utils/errors.js';
|
|
20
20
|
import { info, intro, outro, warn } from '../../utils/logger.js';
|
|
21
|
-
import { commanderArgParser, pickDefined } from '../../utils/options.js';
|
|
21
|
+
import { addWaitLockOption, commanderArgParser, pickDefined, resolveWaitLockSeconds, } from '../../utils/options.js';
|
|
22
22
|
import { parsePositiveIntegerFlag } from '../../utils/validation.js';
|
|
23
23
|
import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
|
|
24
24
|
/** Zero-pads an ordinal number to the given width. */
|
|
@@ -119,9 +119,11 @@ function renameMapsEqual(left, right) {
|
|
|
119
119
|
}
|
|
120
120
|
/**
|
|
121
121
|
* Applies a rename map to a {@link PatchQueueContext} so cross-patch lint
|
|
122
|
-
* can run against the projected state without touching disk.
|
|
122
|
+
* can run against the projected state without touching disk. Exported for
|
|
123
|
+
* `patch rename --order`, which projects the same single-row move shape
|
|
124
|
+
* (FORGE J10).
|
|
123
125
|
*/
|
|
124
|
-
function projectReorder(base, renameMap) {
|
|
126
|
+
export function projectReorder(base, renameMap) {
|
|
125
127
|
const ownerLookup = (oldFilename) => renameMap.get(oldFilename)?.newFilename;
|
|
126
128
|
const projectedEntries = base.entries.map((entry) => {
|
|
127
129
|
// Project staged-dependency owner references through the rename map on
|
|
@@ -235,7 +237,9 @@ async function commitReorderPlan(patchesDir, target, renameMap, anchorFilename,
|
|
|
235
237
|
const currentProjected = projectReorder(await buildPatchQueueContext(patchesDir), currentRenameMap);
|
|
236
238
|
const currentConflicts = lintPatchQueue(currentProjected).filter((i) => i.severity === 'error');
|
|
237
239
|
if (currentConflicts.length > 0 && options.forceUnsafe !== true) {
|
|
238
|
-
throw new InvalidArgumentError(`Refusing to run patch reorder: reorder would introduce ${currentConflicts.length} cross-patch lint error(s)
|
|
240
|
+
throw new InvalidArgumentError(`Refusing to run patch reorder: reorder would introduce ${currentConflicts.length} cross-patch lint error(s):\n ${currentConflicts
|
|
241
|
+
.map(formatPatchLintIssue)
|
|
242
|
+
.join('\n ')}\nPass --force-unsafe to override.`, '--force-unsafe');
|
|
239
243
|
}
|
|
240
244
|
await renumberPatchesInManifest(patchesDir, currentRenameMap);
|
|
241
245
|
// Append the history record inside the lock so two concurrent
|
|
@@ -252,7 +256,7 @@ async function commitReorderPlan(patchesDir, target, renameMap, anchorFilename,
|
|
|
252
256
|
catch (historyError) {
|
|
253
257
|
warn(`History log append failed after patch reorder committed: ${toError(historyError).message}`);
|
|
254
258
|
}
|
|
255
|
-
});
|
|
259
|
+
}, { waitLockSeconds: resolveWaitLockSeconds(options.waitLock), command: 'patch reorder' });
|
|
256
260
|
}
|
|
257
261
|
/**
|
|
258
262
|
* Runs the `patch reorder` command: computes a rename map moving the
|
|
@@ -302,7 +306,7 @@ export async function patchReorderCommand(projectRoot, identifier, options = {})
|
|
|
302
306
|
const conflicts = errorIssues.length > 0
|
|
303
307
|
? {
|
|
304
308
|
reason: `reorder would introduce ${errorIssues.length} cross-patch lint error(s)`,
|
|
305
|
-
details: errorIssues.map(
|
|
309
|
+
details: errorIssues.map(formatPatchLintIssue),
|
|
306
310
|
}
|
|
307
311
|
: null;
|
|
308
312
|
const renameEntries = getSortedRenameEntries(renameMap);
|
|
@@ -369,7 +373,7 @@ export async function patchReorderCommand(projectRoot, identifier, options = {})
|
|
|
369
373
|
*/
|
|
370
374
|
export function registerPatchReorder(parent, context) {
|
|
371
375
|
const { getProjectRoot, withErrorHandling } = context;
|
|
372
|
-
parent
|
|
376
|
+
const command = parent
|
|
373
377
|
.command('reorder <name>')
|
|
374
378
|
.description('Move a patch to a different position in the queue (destructive)')
|
|
375
379
|
.addOption(new Option('--to <order>', 'Destination ordinal').argParser(commanderArgParser((v) => parsePositiveIntegerFlag('--to', v))))
|
|
@@ -377,8 +381,8 @@ export function registerPatchReorder(parent, context) {
|
|
|
377
381
|
.option('--after <anchor>', 'Place the patch immediately after <anchor>')
|
|
378
382
|
.option('--dry-run', 'Show what would happen without writing')
|
|
379
383
|
.option('-y, --yes', 'Skip confirmation prompt (required for non-TTY)')
|
|
380
|
-
.option('--force-unsafe', 'Bypass the refusal when the projected order introduces a lint error')
|
|
381
|
-
|
|
384
|
+
.option('--force-unsafe', 'Bypass the refusal when the projected order introduces a lint error');
|
|
385
|
+
addWaitLockOption(command).action(withErrorHandling(async (name, options) => {
|
|
382
386
|
await patchReorderCommand(getProjectRoot(), name, pickDefined(options));
|
|
383
387
|
}));
|
|
384
388
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* budget; consumed only by the split command.
|
|
7
7
|
*/
|
|
8
8
|
import { type ConflictReport } from '../../core/destructive.js';
|
|
9
|
+
import { buildPatchQueueContext } from '../../core/patch-lint.js';
|
|
9
10
|
import { buildProjectedManifest } from '../../core/patch-policy.js';
|
|
10
11
|
import type { PatchStagedForwardImport } from '../../types/commands/index.js';
|
|
11
12
|
import type { PatchCategory, PatchMetadata } from '../../types/commands/index.js';
|
|
@@ -40,11 +41,15 @@ export interface SplitPlan {
|
|
|
40
41
|
stagedDependencyAdditions: Map<string, PatchStagedForwardImport[]>;
|
|
41
42
|
}
|
|
42
43
|
/**
|
|
43
|
-
*
|
|
44
|
+
* Refuses the split unless the source patch currently owns every requested
|
|
45
|
+
* file. Splitting out a file the source does not own would produce a new patch
|
|
46
|
+
* whose diff cannot apply.
|
|
44
47
|
*/
|
|
45
48
|
export declare function assertSourceOwnsFiles(source: PatchMetadata, files: readonly string[]): void;
|
|
46
49
|
/**
|
|
47
|
-
*
|
|
50
|
+
* Generates the diff for one side of a split by re-deriving it from the engine
|
|
51
|
+
* working tree, scoped to `files`. Re-derived rather than sliced out of the
|
|
52
|
+
* source patch so hunk context is correct for the new file set.
|
|
48
53
|
*/
|
|
49
54
|
export declare function buildSplitDiff(engineDir: string, files: readonly string[], label: string, sourceFilename: string): Promise<string>;
|
|
50
55
|
/**
|
|
@@ -63,20 +68,22 @@ export declare function mergeStagedForwardImports(patch: PatchMetadata, decls: r
|
|
|
63
68
|
* patch are auto-declared (and the declarations returned) so the projection
|
|
64
69
|
* matches the real per-patch gate the split leaves behind.
|
|
65
70
|
*/
|
|
66
|
-
export declare function runProjectedSplitLint(
|
|
71
|
+
export declare function runProjectedSplitLint(plan: SplitPlan, baseCtx: Awaited<ReturnType<typeof buildPatchQueueContext>>): {
|
|
67
72
|
conflicts: ConflictReport | null;
|
|
68
73
|
stagedDependencyAdditions: Map<string, PatchStagedForwardImport[]>;
|
|
69
|
-
}
|
|
74
|
+
};
|
|
70
75
|
/** Builds the projected manifest for policy enforcement. */
|
|
71
76
|
export declare function projectSplitManifest(manifest: {
|
|
72
77
|
version: 1;
|
|
73
78
|
patches: PatchMetadata[];
|
|
74
79
|
}, plan: SplitPlan, newMetadata: PatchMetadata): ReturnType<typeof buildProjectedManifest>;
|
|
75
80
|
/**
|
|
76
|
-
*
|
|
81
|
+
* Builds the manifest row for the patch created by a split, carrying the
|
|
82
|
+
* source's provenance (product, version, category) onto the new patch.
|
|
77
83
|
*/
|
|
78
84
|
export declare function buildNewPatchMetadata(plan: SplitPlan, config: FireForgeConfig): PatchMetadata;
|
|
79
85
|
/**
|
|
80
|
-
*
|
|
86
|
+
* Renders the operator-facing summary lines for a planned split — the text
|
|
87
|
+
* shown by `--dry-run` and by the destructive-operation confirmation prompt.
|
|
81
88
|
*/
|
|
82
89
|
export declare function buildSplitSummary(plan: SplitPlan): string[];
|
|
@@ -11,7 +11,7 @@ import {} from '../../core/destructive.js';
|
|
|
11
11
|
import { getDiffForFilesAgainstHead } from '../../core/git-diff.js';
|
|
12
12
|
import { computeProjectedLintRegressions } from '../../core/lint-projection.js';
|
|
13
13
|
import { extractAffectedFiles } from '../../core/patch-apply.js';
|
|
14
|
-
import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, collectForwardImportEdges, detectNewFilesInDiff, lintPatchQueue, } from '../../core/patch-lint.js';
|
|
14
|
+
import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, collectForwardImportEdges, detectNewFilesInDiff, formatPatchLintIssue, lintPatchQueue, } from '../../core/patch-lint.js';
|
|
15
15
|
import { rewriteStagedDependencyOwners } from '../../core/patch-manifest.js';
|
|
16
16
|
import { applyRenameMapToManifest, buildProjectedManifest } from '../../core/patch-policy.js';
|
|
17
17
|
import { buildPatchSourceMetadata } from '../../core/patch-source-metadata.js';
|
|
@@ -21,7 +21,9 @@ import { pathExists } from '../../utils/fs.js';
|
|
|
21
21
|
import { warn } from '../../utils/logger.js';
|
|
22
22
|
import {} from '../export-flow.js';
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* Refuses the split unless the source patch currently owns every requested
|
|
25
|
+
* file. Splitting out a file the source does not own would produce a new patch
|
|
26
|
+
* whose diff cannot apply.
|
|
25
27
|
*/
|
|
26
28
|
export function assertSourceOwnsFiles(source, files) {
|
|
27
29
|
const owned = new Set(source.filesAffected);
|
|
@@ -32,7 +34,9 @@ export function assertSourceOwnsFiles(source, files) {
|
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
/**
|
|
35
|
-
*
|
|
37
|
+
* Generates the diff for one side of a split by re-deriving it from the engine
|
|
38
|
+
* working tree, scoped to `files`. Re-derived rather than sliced out of the
|
|
39
|
+
* source patch so hunk context is correct for the new file set.
|
|
36
40
|
*/
|
|
37
41
|
export async function buildSplitDiff(engineDir, files, label, sourceFilename) {
|
|
38
42
|
for (const file of files) {
|
|
@@ -85,10 +89,9 @@ function buildEntryProjection(diff) {
|
|
|
85
89
|
return { diff, newFiles, modifiedFileAdditions: buildModifiedFileAdditionsFromDiff(diff) };
|
|
86
90
|
}
|
|
87
91
|
/** Builds the projected post-split queue entries (renumber + shrunken source + new patch). */
|
|
88
|
-
|
|
92
|
+
function buildProjectedSplitEntries(baseCtx, plan) {
|
|
89
93
|
const movedSet = new Set(plan.movedFiles);
|
|
90
94
|
const ownerLookup = (old) => plan.placement.renameMap.get(old)?.newFilename;
|
|
91
|
-
const baseCtx = await buildPatchQueueContext(patchesDir);
|
|
92
95
|
const entries = baseCtx.entries.map((entry) => {
|
|
93
96
|
let metadata = entry.metadata;
|
|
94
97
|
if (metadata) {
|
|
@@ -110,7 +113,7 @@ async function buildProjectedSplitEntries(patchesDir, plan) {
|
|
|
110
113
|
...buildEntryProjection(plan.movedDiff),
|
|
111
114
|
});
|
|
112
115
|
entries.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
|
|
113
|
-
return
|
|
116
|
+
return entries;
|
|
114
117
|
}
|
|
115
118
|
/**
|
|
116
119
|
* Computes the staged forward-import declarations the split introduces:
|
|
@@ -181,14 +184,19 @@ function injectStagedDependencyAdditions(entries, additions) {
|
|
|
181
184
|
* patch are auto-declared (and the declarations returned) so the projection
|
|
182
185
|
* matches the real per-patch gate the split leaves behind.
|
|
183
186
|
*/
|
|
184
|
-
export
|
|
185
|
-
const
|
|
187
|
+
export function runProjectedSplitLint(plan, baseCtx) {
|
|
188
|
+
const projectedEntries = buildProjectedSplitEntries(baseCtx, plan);
|
|
186
189
|
// Discover and auto-declare the forward edges this split introduces into
|
|
187
190
|
// the new patch, then inject them before linting so they resolve.
|
|
188
191
|
const stagedDependencyAdditions = computeSplitStagedDependencyAdditions(projectedEntries, plan.placement.newFilename);
|
|
189
192
|
injectStagedDependencyAdditions(projectedEntries, stagedDependencyAdditions);
|
|
190
193
|
const baselineIssues = lintPatchQueue(baseCtx).filter((i) => i.severity === 'error');
|
|
191
|
-
const projectedIssues = lintPatchQueue({
|
|
194
|
+
const projectedIssues = lintPatchQueue({
|
|
195
|
+
entries: projectedEntries,
|
|
196
|
+
// Keep the projection patch-policy-aware whenever the baseline is, so
|
|
197
|
+
// computeProjectedLintRegressions compares symmetric rule sets.
|
|
198
|
+
...(baseCtx.patchPolicy ? { patchPolicy: baseCtx.patchPolicy } : {}),
|
|
199
|
+
}).filter((i) => i.severity === 'error');
|
|
192
200
|
const regressions = computeProjectedLintRegressions(baselineIssues, projectedIssues);
|
|
193
201
|
if (baselineIssues.length > 0 && regressions.length === 0) {
|
|
194
202
|
warn(`Note: projected queue still has ${baselineIssues.length} pre-existing cross-patch ` +
|
|
@@ -198,7 +206,7 @@ export async function runProjectedSplitLint(patchesDir, plan) {
|
|
|
198
206
|
? null
|
|
199
207
|
: {
|
|
200
208
|
reason: `split would introduce ${regressions.length} cross-patch lint error(s)`,
|
|
201
|
-
details: regressions.map(
|
|
209
|
+
details: regressions.map(formatPatchLintIssue),
|
|
202
210
|
};
|
|
203
211
|
return { conflicts, stagedDependencyAdditions };
|
|
204
212
|
}
|
|
@@ -216,7 +224,8 @@ export function projectSplitManifest(manifest, plan, newMetadata) {
|
|
|
216
224
|
return buildProjectedManifest(renamed, [...patched, newMetadata]);
|
|
217
225
|
}
|
|
218
226
|
/**
|
|
219
|
-
*
|
|
227
|
+
* Builds the manifest row for the patch created by a split, carrying the
|
|
228
|
+
* source's provenance (product, version, category) onto the new patch.
|
|
220
229
|
*/
|
|
221
230
|
export function buildNewPatchMetadata(plan, config) {
|
|
222
231
|
return {
|
|
@@ -231,7 +240,8 @@ export function buildNewPatchMetadata(plan, config) {
|
|
|
231
240
|
};
|
|
232
241
|
}
|
|
233
242
|
/**
|
|
234
|
-
*
|
|
243
|
+
* Renders the operator-facing summary lines for a planned split — the text
|
|
244
|
+
* shown by `--dry-run` and by the destructive-operation confirmation prompt.
|
|
235
245
|
*/
|
|
236
246
|
export function buildSplitSummary(plan) {
|
|
237
247
|
const summary = [
|
|
@@ -28,7 +28,7 @@ import { type SplitPlan } from './split-plan.js';
|
|
|
28
28
|
* failure the steps are rolled back in reverse order. Exported for
|
|
29
29
|
* `patch move-files --create`, which commits the same transaction shape.
|
|
30
30
|
*/
|
|
31
|
-
export declare function commitPatchSplit(patchesDir: string, plan: SplitPlan, newMetadata: PatchMetadata, options: Pick<PatchSplitOptions, 'yes' | 'forceUnsafe'>, config: FireForgeConfig): Promise<void>;
|
|
31
|
+
export declare function commitPatchSplit(patchesDir: string, plan: SplitPlan, newMetadata: PatchMetadata, options: Pick<PatchSplitOptions, 'yes' | 'forceUnsafe' | 'waitLock'>, config: FireForgeConfig): Promise<void>;
|
|
32
32
|
/**
|
|
33
33
|
* Runs the `patch split` command: plans the split, lints the projection,
|
|
34
34
|
* confirms, and commits transactionally.
|