@hominis/fireforge 0.30.1 → 0.32.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 +36 -0
- package/README.md +22 -0
- package/dist/src/commands/export-all.js +9 -16
- package/dist/src/commands/export-flow.d.ts +6 -0
- package/dist/src/commands/export-flow.js +6 -1
- package/dist/src/commands/export-placement-gate.d.ts +38 -0
- package/dist/src/commands/export-placement-gate.js +105 -0
- package/dist/src/commands/export-shared.d.ts +28 -0
- package/dist/src/commands/export-shared.js +46 -1
- package/dist/src/commands/export.js +52 -113
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +0 -13
- package/dist/src/commands/furnace/chrome-doc-templates.js +1 -1
- package/dist/src/commands/furnace/create-dry-run.d.ts +1 -1
- package/dist/src/commands/furnace/create.d.ts +1 -2
- package/dist/src/commands/furnace/deploy.js +36 -114
- package/dist/src/commands/furnace/refresh.js +52 -32
- package/dist/src/commands/furnace/sync.js +2 -0
- package/dist/src/commands/import.js +108 -73
- package/dist/src/commands/lint-per-patch.d.ts +3 -1
- package/dist/src/commands/lint-per-patch.js +265 -74
- package/dist/src/commands/lint.d.ts +1 -58
- package/dist/src/commands/lint.js +193 -88
- package/dist/src/commands/patch/compact.d.ts +5 -2
- package/dist/src/commands/patch/compact.js +85 -25
- package/dist/src/commands/patch/delete.js +17 -17
- package/dist/src/commands/patch/index.js +2 -0
- package/dist/src/commands/patch/lint-ignore.js +3 -16
- package/dist/src/commands/patch/move-files.js +2 -0
- package/dist/src/commands/patch/patch-context.d.ts +41 -0
- package/dist/src/commands/patch/patch-context.js +53 -0
- package/dist/src/commands/patch/rename.js +10 -15
- package/dist/src/commands/patch/reorder.d.ts +0 -2
- package/dist/src/commands/patch/reorder.js +18 -19
- package/dist/src/commands/patch/split-plan.d.ts +66 -0
- package/dist/src/commands/patch/split-plan.js +178 -0
- package/dist/src/commands/patch/split.d.ts +30 -0
- package/dist/src/commands/patch/split.js +283 -0
- package/dist/src/commands/patch/staged-dependency.d.ts +1 -7
- package/dist/src/commands/patch/staged-dependency.js +4 -17
- package/dist/src/commands/patch/tier.js +4 -17
- package/dist/src/commands/re-export-files.js +4 -1
- package/dist/src/commands/re-export-scan.js +8 -1
- package/dist/src/commands/re-export.js +8 -1
- package/dist/src/commands/rebase/summary.d.ts +1 -5
- package/dist/src/commands/rebase/summary.js +1 -1
- package/dist/src/commands/status-output.js +77 -68
- package/dist/src/commands/test-diagnose.d.ts +23 -0
- package/dist/src/commands/test-diagnose.js +210 -0
- package/dist/src/commands/test-run.d.ts +68 -0
- package/dist/src/commands/test-run.js +97 -0
- package/dist/src/commands/test.js +214 -263
- package/dist/src/commands/token.js +15 -1
- package/dist/src/commands/wire.js +109 -78
- package/dist/src/core/build-audit.d.ts +1 -1
- package/dist/src/core/build-audit.js +2 -46
- package/dist/src/core/build-baseline-types.d.ts +38 -0
- package/dist/src/core/build-baseline-types.js +10 -0
- package/dist/src/core/build-baseline.d.ts +1 -31
- package/dist/src/core/build-prepare.d.ts +1 -1
- package/dist/src/core/build-prepare.js +2 -45
- package/dist/src/core/config-paths.d.ts +0 -8
- package/dist/src/core/config-paths.js +4 -4
- package/dist/src/core/config-state.d.ts +0 -6
- package/dist/src/core/config-state.js +1 -1
- package/dist/src/core/config-validate-patch-policy.js +12 -13
- package/dist/src/core/config-validate.js +74 -28
- package/dist/src/core/engine-changes.d.ts +24 -0
- package/dist/src/core/engine-changes.js +64 -0
- package/dist/src/core/firefox-cache.d.ts +0 -5
- package/dist/src/core/firefox-cache.js +1 -1
- package/dist/src/core/firefox-download.d.ts +0 -6
- package/dist/src/core/firefox-download.js +1 -1
- package/dist/src/core/furnace-apply-helpers.d.ts +1 -8
- package/dist/src/core/furnace-apply-helpers.js +11 -20
- package/dist/src/core/furnace-apply.d.ts +1 -1
- package/dist/src/core/furnace-apply.js +1 -1
- package/dist/src/core/furnace-checksum-utils.d.ts +7 -0
- package/dist/src/core/furnace-checksum-utils.js +15 -0
- package/dist/src/core/furnace-config-validate.d.ts +31 -0
- package/dist/src/core/furnace-config-validate.js +133 -0
- package/dist/src/core/furnace-config.d.ts +4 -32
- package/dist/src/core/furnace-config.js +15 -111
- package/dist/src/core/furnace-constants.d.ts +0 -10
- package/dist/src/core/furnace-constants.js +2 -2
- package/dist/src/core/furnace-css-fragments.d.ts +79 -0
- package/dist/src/core/furnace-css-fragments.js +243 -0
- package/dist/src/core/furnace-jsconfig.d.ts +63 -0
- package/dist/src/core/furnace-jsconfig.js +191 -0
- package/dist/src/core/furnace-validate-helpers.d.ts +16 -14
- package/dist/src/core/furnace-validate-helpers.js +40 -1
- package/dist/src/core/furnace-validate-registration.js +16 -1
- package/dist/src/core/furnace-validate.js +54 -2
- package/dist/src/core/git-base.d.ts +15 -0
- package/dist/src/core/git-base.js +32 -0
- package/dist/src/core/git-diff.d.ts +8 -0
- package/dist/src/core/git-diff.js +224 -59
- package/dist/src/core/git-file-ops.d.ts +39 -12
- package/dist/src/core/git-file-ops.js +84 -3
- package/dist/src/core/lint-cache.d.ts +0 -13
- package/dist/src/core/lint-cache.js +5 -5
- package/dist/src/core/mach.d.ts +22 -1
- package/dist/src/core/mach.js +27 -2
- package/dist/src/core/manifest-register.d.ts +5 -16
- package/dist/src/core/manifest-register.js +3 -1
- package/dist/src/core/patch-lint-checkjs.d.ts +75 -21
- package/dist/src/core/patch-lint-checkjs.js +263 -71
- package/dist/src/core/patch-lint-css.d.ts +23 -0
- package/dist/src/core/patch-lint-css.js +172 -0
- package/dist/src/core/patch-lint-jsdoc.js +63 -4
- package/dist/src/core/patch-lint-observer.d.ts +37 -0
- package/dist/src/core/patch-lint-observer.js +168 -0
- package/dist/src/core/patch-lint.d.ts +34 -11
- package/dist/src/core/patch-lint.js +24 -161
- package/dist/src/core/patch-manifest-io.d.ts +16 -0
- package/dist/src/core/patch-manifest-io.js +44 -2
- package/dist/src/core/patch-manifest-validate.d.ts +1 -8
- package/dist/src/core/patch-manifest-validate.js +1 -1
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-manifest.js +1 -1
- package/dist/src/core/patch-policy.d.ts +0 -4
- package/dist/src/core/patch-policy.js +10 -4
- package/dist/src/core/register-browser-content.d.ts +1 -1
- package/dist/src/core/register-module.d.ts +1 -1
- package/dist/src/core/register-result.d.ts +21 -0
- package/dist/src/core/register-result.js +9 -0
- package/dist/src/core/register-shared-css.d.ts +1 -1
- package/dist/src/core/register-test-manifest.d.ts +1 -1
- package/dist/src/core/test-harness-crash.d.ts +61 -0
- package/dist/src/core/test-harness-crash.js +140 -0
- package/dist/src/core/test-stale-check.d.ts +1 -1
- package/dist/src/core/test-stale-check.js +2 -46
- package/dist/src/core/test-xpcshell-retry.d.ts +9 -2
- package/dist/src/core/test-xpcshell-retry.js +10 -3
- package/dist/src/core/token-dark-mode.js +14 -26
- package/dist/src/core/token-manager.d.ts +4 -0
- package/dist/src/core/token-manager.js +70 -16
- package/dist/src/core/typecheck-shim.d.ts +3 -22
- package/dist/src/core/typecheck-shim.js +69 -7
- package/dist/src/core/wire-utils.js +37 -44
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +122 -0
- package/dist/src/types/config.d.ts +11 -2
- package/dist/src/types/furnace.d.ts +12 -1
- package/dist/src/utils/elapsed.d.ts +0 -2
- package/dist/src/utils/elapsed.js +1 -1
- package/dist/src/utils/fs.d.ts +0 -5
- package/dist/src/utils/fs.js +1 -1
- package/dist/src/utils/regex.d.ts +0 -6
- package/dist/src/utils/regex.js +3 -3
- package/dist/src/utils/validation.d.ts +0 -8
- package/dist/src/utils/validation.js +2 -2
- package/package.json +6 -4
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared preamble for the patch subcommands: every mutation command starts
|
|
3
|
+
* by loading the project paths and the patches manifest and (for the
|
|
4
|
+
* single-patch commands) resolving the operator-supplied identifier. The
|
|
5
|
+
* sequence and its error wording were previously copied into each command;
|
|
6
|
+
* this module is the single source for both.
|
|
7
|
+
*/
|
|
8
|
+
import type { PatchesManifest, PatchMetadata } from '../../types/commands/index.js';
|
|
9
|
+
import type { ProjectPaths } from '../../types/config.js';
|
|
10
|
+
/** Resolved project paths plus the non-empty patches manifest. */
|
|
11
|
+
export interface PatchQueueContext {
|
|
12
|
+
/** Project paths resolved from the project root. */
|
|
13
|
+
paths: ProjectPaths;
|
|
14
|
+
/** The loaded manifest; guaranteed to contain at least one patch. */
|
|
15
|
+
manifest: PatchesManifest;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Loads the project paths and the patches manifest, throwing the shared
|
|
19
|
+
* command-preamble errors when the patches directory is missing or the
|
|
20
|
+
* manifest has no patches.
|
|
21
|
+
*
|
|
22
|
+
* @param projectRoot - Root directory of the project
|
|
23
|
+
* @param options - Optional overrides for the preamble error wording
|
|
24
|
+
* @param options.missingDirMessage - Replacement for the default
|
|
25
|
+
* "Patches directory not found." error (e.g. `patch delete` appends
|
|
26
|
+
* "No patches to delete.")
|
|
27
|
+
* @returns The resolved paths and the non-empty manifest
|
|
28
|
+
*/
|
|
29
|
+
export declare function requirePatchQueue(projectRoot: string, options?: {
|
|
30
|
+
missingDirMessage?: string;
|
|
31
|
+
}): Promise<PatchQueueContext>;
|
|
32
|
+
/**
|
|
33
|
+
* Resolves an operator-supplied patch identifier (order number, filename,
|
|
34
|
+
* or unique name fragment) against the manifest, throwing the shared
|
|
35
|
+
* not-found error with suggestions when no patch matches.
|
|
36
|
+
*
|
|
37
|
+
* @param identifier - Identifier as passed on the command line
|
|
38
|
+
* @param patches - Manifest rows to resolve against
|
|
39
|
+
* @returns The matching manifest row
|
|
40
|
+
*/
|
|
41
|
+
export declare function requirePatchTarget(identifier: string, patches: PatchMetadata[]): PatchMetadata;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Shared preamble for the patch subcommands: every mutation command starts
|
|
4
|
+
* by loading the project paths and the patches manifest and (for the
|
|
5
|
+
* single-patch commands) resolving the operator-supplied identifier. The
|
|
6
|
+
* sequence and its error wording were previously copied into each command;
|
|
7
|
+
* this module is the single source for both.
|
|
8
|
+
*/
|
|
9
|
+
import { getProjectPaths } from '../../core/config.js';
|
|
10
|
+
import { formatPatchNotFoundError } from '../../core/patch-identifier-suggest.js';
|
|
11
|
+
import { loadPatchesManifest, resolvePatchIdentifier } from '../../core/patch-manifest.js';
|
|
12
|
+
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
13
|
+
import { pathExists } from '../../utils/fs.js';
|
|
14
|
+
/**
|
|
15
|
+
* Loads the project paths and the patches manifest, throwing the shared
|
|
16
|
+
* command-preamble errors when the patches directory is missing or the
|
|
17
|
+
* manifest has no patches.
|
|
18
|
+
*
|
|
19
|
+
* @param projectRoot - Root directory of the project
|
|
20
|
+
* @param options - Optional overrides for the preamble error wording
|
|
21
|
+
* @param options.missingDirMessage - Replacement for the default
|
|
22
|
+
* "Patches directory not found." error (e.g. `patch delete` appends
|
|
23
|
+
* "No patches to delete.")
|
|
24
|
+
* @returns The resolved paths and the non-empty manifest
|
|
25
|
+
*/
|
|
26
|
+
export async function requirePatchQueue(projectRoot, options = {}) {
|
|
27
|
+
const paths = getProjectPaths(projectRoot);
|
|
28
|
+
if (!(await pathExists(paths.patches))) {
|
|
29
|
+
throw new GeneralError(options.missingDirMessage ?? 'Patches directory not found.');
|
|
30
|
+
}
|
|
31
|
+
const manifest = await loadPatchesManifest(paths.patches);
|
|
32
|
+
if (!manifest || manifest.patches.length === 0) {
|
|
33
|
+
throw new GeneralError('No patches in manifest.');
|
|
34
|
+
}
|
|
35
|
+
return { paths, manifest };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolves an operator-supplied patch identifier (order number, filename,
|
|
39
|
+
* or unique name fragment) against the manifest, throwing the shared
|
|
40
|
+
* not-found error with suggestions when no patch matches.
|
|
41
|
+
*
|
|
42
|
+
* @param identifier - Identifier as passed on the command line
|
|
43
|
+
* @param patches - Manifest rows to resolve against
|
|
44
|
+
* @returns The matching manifest row
|
|
45
|
+
*/
|
|
46
|
+
export function requirePatchTarget(identifier, patches) {
|
|
47
|
+
const target = resolvePatchIdentifier(identifier, patches);
|
|
48
|
+
if (!target) {
|
|
49
|
+
throw new InvalidArgumentError(formatPatchNotFoundError(identifier, patches), identifier);
|
|
50
|
+
}
|
|
51
|
+
return target;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=patch-context.js.map
|
|
@@ -18,18 +18,18 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { rename as fsRename } from 'node:fs/promises';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
|
-
import {
|
|
21
|
+
import { loadConfig } from '../../core/config.js';
|
|
22
22
|
import { appendHistory, confirmDestructive } from '../../core/destructive.js';
|
|
23
23
|
import { sanitizeName } from '../../core/patch-export.js';
|
|
24
|
-
import { formatPatchNotFoundError } from '../../core/patch-identifier-suggest.js';
|
|
25
24
|
import { withPatchDirectoryLock } from '../../core/patch-lock.js';
|
|
26
|
-
import { loadPatchesManifest,
|
|
25
|
+
import { loadPatchesManifest, rewriteStagedDependencyOwners, savePatchesManifest, } from '../../core/patch-manifest.js';
|
|
27
26
|
import { buildProjectedManifest, enforcePatchPolicy } from '../../core/patch-policy.js';
|
|
28
27
|
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
29
28
|
import { toError } from '../../utils/errors.js';
|
|
30
29
|
import { pathExists } from '../../utils/fs.js';
|
|
31
30
|
import { info, intro, outro, warn } from '../../utils/logger.js';
|
|
32
31
|
import { pickDefined } from '../../utils/options.js';
|
|
32
|
+
import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
|
|
33
33
|
/**
|
|
34
34
|
* Pulls the ordinal-string + category prefix out of a patch filename so
|
|
35
35
|
* the rename keeps the existing ordinal padding verbatim. Returning the
|
|
@@ -85,6 +85,11 @@ async function commitRenameUnderLock(input) {
|
|
|
85
85
|
name: newName,
|
|
86
86
|
...(descriptionChanging ? { description: newDescription ?? '' } : {}),
|
|
87
87
|
};
|
|
88
|
+
// Staged-dependency owners on other patches reference the old
|
|
89
|
+
// filename; remap them so forward-import declarations survive the
|
|
90
|
+
// rename instead of dangling.
|
|
91
|
+
const ownerLookup = (old) => old === target.filename ? newFilename : undefined;
|
|
92
|
+
fresh.patches = fresh.patches.map((p) => rewriteStagedDependencyOwners(p, ownerLookup));
|
|
88
93
|
}
|
|
89
94
|
else {
|
|
90
95
|
fresh.patches[idx] = {
|
|
@@ -151,19 +156,9 @@ export async function patchRenameCommand(projectRoot, identifier, options = {})
|
|
|
151
156
|
if (options.to === undefined || options.to.trim() === '') {
|
|
152
157
|
throw new InvalidArgumentError('Specify --to <new-name>. The new name is sanitised into the filename slug the same way `export --name` is.', 'patch rename');
|
|
153
158
|
}
|
|
154
|
-
const paths = getProjectPaths(projectRoot);
|
|
155
159
|
const config = await loadConfig(projectRoot);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
const manifest = await loadPatchesManifest(paths.patches);
|
|
160
|
-
if (!manifest || manifest.patches.length === 0) {
|
|
161
|
-
throw new GeneralError('No patches in manifest.');
|
|
162
|
-
}
|
|
163
|
-
const target = resolvePatchIdentifier(identifier, manifest.patches);
|
|
164
|
-
if (!target) {
|
|
165
|
-
throw new InvalidArgumentError(formatPatchNotFoundError(identifier, manifest.patches), identifier);
|
|
166
|
-
}
|
|
160
|
+
const { paths, manifest } = await requirePatchQueue(projectRoot);
|
|
161
|
+
const target = requirePatchTarget(identifier, manifest.patches);
|
|
167
162
|
const split = splitPatchFilename(target.filename);
|
|
168
163
|
if (!split) {
|
|
169
164
|
throw new GeneralError(`Cannot rename ${target.filename}: filename does not match the expected {ordinal}-{category}-{slug}.patch convention. Re-export the patch instead.`);
|
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
import { Command } from 'commander';
|
|
11
11
|
import type { CommandContext } from '../../types/cli.js';
|
|
12
12
|
import type { PatchMetadata, PatchReorderOptions } from '../../types/commands/index.js';
|
|
13
|
-
/** Zero-pads an ordinal number to the given width. */
|
|
14
|
-
export declare function padOrder(value: number, width: number): string;
|
|
15
13
|
/** Builds a new patch filename by replacing the numeric prefix with `newOrder`. */
|
|
16
14
|
export declare function rebuildFilenameForOrder(existing: PatchMetadata, newOrder: number): string;
|
|
17
15
|
/**
|
|
@@ -9,21 +9,20 @@
|
|
|
9
9
|
* before any bytes move.
|
|
10
10
|
*/
|
|
11
11
|
import { Option } from 'commander';
|
|
12
|
-
import {
|
|
12
|
+
import { loadConfig } from '../../core/config.js';
|
|
13
13
|
import { appendHistory, confirmDestructive, } from '../../core/destructive.js';
|
|
14
|
-
import { formatPatchNotFoundError } from '../../core/patch-identifier-suggest.js';
|
|
15
14
|
import { buildPatchQueueContext, lintPatchQueue, } from '../../core/patch-lint.js';
|
|
16
15
|
import { withPatchDirectoryLock } from '../../core/patch-lock.js';
|
|
17
|
-
import { loadPatchesManifest, renumberPatchesInManifest, resolvePatchIdentifier, } from '../../core/patch-manifest.js';
|
|
16
|
+
import { loadPatchesManifest, renumberPatchesInManifest, resolvePatchIdentifier, rewriteStagedDependencyOwners, } from '../../core/patch-manifest.js';
|
|
18
17
|
import { applyRenameMapToManifest, enforcePatchPolicy } from '../../core/patch-policy.js';
|
|
19
18
|
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
20
19
|
import { toError } from '../../utils/errors.js';
|
|
21
|
-
import { pathExists } from '../../utils/fs.js';
|
|
22
20
|
import { info, intro, outro, warn } from '../../utils/logger.js';
|
|
23
21
|
import { pickDefined } from '../../utils/options.js';
|
|
24
22
|
import { parsePositiveIntegerFlag } from '../../utils/validation.js';
|
|
23
|
+
import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
|
|
25
24
|
/** Zero-pads an ordinal number to the given width. */
|
|
26
|
-
|
|
25
|
+
function padOrder(value, width) {
|
|
27
26
|
return String(value).padStart(width, '0');
|
|
28
27
|
}
|
|
29
28
|
/** Builds a new patch filename by replacing the numeric prefix with `newOrder`. */
|
|
@@ -123,12 +122,22 @@ function renameMapsEqual(left, right) {
|
|
|
123
122
|
* can run against the projected state without touching disk.
|
|
124
123
|
*/
|
|
125
124
|
function projectReorder(base, renameMap) {
|
|
125
|
+
const ownerLookup = (oldFilename) => renameMap.get(oldFilename)?.newFilename;
|
|
126
126
|
const projectedEntries = base.entries.map((entry) => {
|
|
127
|
+
// Project staged-dependency owner references through the rename map on
|
|
128
|
+
// every entry — owners point at *other* patches' filenames, so a
|
|
129
|
+
// projection that skips non-renamed entries would lint against stale
|
|
130
|
+
// owners and report false forward-import regressions.
|
|
131
|
+
const metadata = entry.metadata
|
|
132
|
+
? rewriteStagedDependencyOwners(entry.metadata, ownerLookup)
|
|
133
|
+
: entry.metadata;
|
|
127
134
|
const rename = renameMap.get(entry.filename);
|
|
128
|
-
if (!rename)
|
|
129
|
-
return entry;
|
|
135
|
+
if (!rename) {
|
|
136
|
+
return metadata === entry.metadata ? entry : { ...entry, metadata };
|
|
137
|
+
}
|
|
130
138
|
return {
|
|
131
139
|
...entry,
|
|
140
|
+
metadata,
|
|
132
141
|
filename: rename.newFilename,
|
|
133
142
|
order: rename.newOrder,
|
|
134
143
|
};
|
|
@@ -268,19 +277,9 @@ export async function patchReorderCommand(projectRoot, identifier, options = {})
|
|
|
268
277
|
if (specifiedTargets > 1) {
|
|
269
278
|
throw new InvalidArgumentError('--to, --before, and --after are mutually exclusive.', 'patch reorder');
|
|
270
279
|
}
|
|
271
|
-
const paths = getProjectPaths(projectRoot);
|
|
272
280
|
const config = await loadConfig(projectRoot);
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
const manifest = await loadPatchesManifest(paths.patches);
|
|
277
|
-
if (!manifest || manifest.patches.length === 0) {
|
|
278
|
-
throw new GeneralError('No patches in manifest.');
|
|
279
|
-
}
|
|
280
|
-
const target = resolvePatchIdentifier(identifier, manifest.patches);
|
|
281
|
-
if (!target) {
|
|
282
|
-
throw new InvalidArgumentError(formatPatchNotFoundError(identifier, manifest.patches), identifier);
|
|
283
|
-
}
|
|
281
|
+
const { paths, manifest } = await requirePatchQueue(projectRoot);
|
|
282
|
+
const target = requirePatchTarget(identifier, manifest.patches);
|
|
284
283
|
const { destinationOrder, anchorFilename } = resolveDestination(target, manifest.patches, options);
|
|
285
284
|
const renameMap = computeRenameMap(manifest.patches, target, destinationOrder);
|
|
286
285
|
if (renameMap.size === 0) {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Planning helpers for `fireforge patch split`: ownership guards, diff
|
|
3
|
+
* construction from the worktree, staged-dependency owner-rewrite
|
|
4
|
+
* discovery, cross-patch lint projection, and policy-manifest projection.
|
|
5
|
+
* Split out of `split.ts` to keep both files within the per-file line
|
|
6
|
+
* budget; consumed only by the split command.
|
|
7
|
+
*/
|
|
8
|
+
import { type ConflictReport } from '../../core/destructive.js';
|
|
9
|
+
import { buildProjectedManifest } from '../../core/patch-policy.js';
|
|
10
|
+
import type { PatchCategory, PatchMetadata } from '../../types/commands/index.js';
|
|
11
|
+
import type { FireForgeConfig } from '../../types/config.js';
|
|
12
|
+
import { type PlacementPlan } from '../export-flow.js';
|
|
13
|
+
/** Everything the commit step needs, computed and confirmed up front. */
|
|
14
|
+
export interface SplitPlan {
|
|
15
|
+
source: PatchMetadata;
|
|
16
|
+
movedFiles: string[];
|
|
17
|
+
remainingFiles: string[];
|
|
18
|
+
movedDiff: string;
|
|
19
|
+
remainingDiff: string;
|
|
20
|
+
placement: PlacementPlan;
|
|
21
|
+
/** Effective placement flags (with the after-source default applied). */
|
|
22
|
+
placementOptions: {
|
|
23
|
+
order?: number;
|
|
24
|
+
before?: string;
|
|
25
|
+
after?: string;
|
|
26
|
+
};
|
|
27
|
+
category: PatchCategory;
|
|
28
|
+
name: string;
|
|
29
|
+
description: string;
|
|
30
|
+
/** Patches (by current filename) whose staged-dependency owners re-point to the new patch. */
|
|
31
|
+
ownerRewrites: string[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
*
|
|
35
|
+
*/
|
|
36
|
+
export declare function assertSourceOwnsFiles(source: PatchMetadata, files: readonly string[]): void;
|
|
37
|
+
/**
|
|
38
|
+
*
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildSplitDiff(engineDir: string, files: readonly string[], label: string, sourceFilename: string): Promise<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Finds patches declaring a staged-dependency forward-import whose `owner`
|
|
43
|
+
* is the source patch and whose `creates` path moves to the new patch.
|
|
44
|
+
*/
|
|
45
|
+
export declare function findOwnerRewriteHolders(patches: readonly PatchMetadata[], sourceFilename: string, movedSet: ReadonlySet<string>): string[];
|
|
46
|
+
/** Rewrites split-affected owners on one manifest row. */
|
|
47
|
+
export declare function rewriteSplitOwners(patch: PatchMetadata, sourceFilename: string, movedSet: ReadonlySet<string>, newFilename: string): PatchMetadata;
|
|
48
|
+
/**
|
|
49
|
+
* Projects the full split (renumber + shrunken source + synthetic new
|
|
50
|
+
* patch + owner rewrites) through cross-patch lint, reporting only the
|
|
51
|
+
* regressions the split itself would introduce.
|
|
52
|
+
*/
|
|
53
|
+
export declare function runProjectedSplitLint(patchesDir: string, plan: SplitPlan): Promise<ConflictReport | null>;
|
|
54
|
+
/** Builds the projected manifest for policy enforcement. */
|
|
55
|
+
export declare function projectSplitManifest(manifest: {
|
|
56
|
+
version: 1;
|
|
57
|
+
patches: PatchMetadata[];
|
|
58
|
+
}, plan: SplitPlan, newMetadata: PatchMetadata): ReturnType<typeof buildProjectedManifest>;
|
|
59
|
+
/**
|
|
60
|
+
*
|
|
61
|
+
*/
|
|
62
|
+
export declare function buildNewPatchMetadata(plan: SplitPlan, config: FireForgeConfig): PatchMetadata;
|
|
63
|
+
/**
|
|
64
|
+
*
|
|
65
|
+
*/
|
|
66
|
+
export declare function buildSplitSummary(plan: SplitPlan): string[];
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Planning helpers for `fireforge patch split`: ownership guards, diff
|
|
4
|
+
* construction from the worktree, staged-dependency owner-rewrite
|
|
5
|
+
* discovery, cross-patch lint projection, and policy-manifest projection.
|
|
6
|
+
* Split out of `split.ts` to keep both files within the per-file line
|
|
7
|
+
* budget; consumed only by the split command.
|
|
8
|
+
*/
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { getDiffForFilesAgainstHead } from '../../core/git-diff.js';
|
|
11
|
+
import { computeProjectedLintRegressions } from '../../core/lint-projection.js';
|
|
12
|
+
import { extractAffectedFiles } from '../../core/patch-apply.js';
|
|
13
|
+
import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, detectNewFilesInDiff, lintPatchQueue, } from '../../core/patch-lint.js';
|
|
14
|
+
import { rewriteStagedDependencyOwners } from '../../core/patch-manifest.js';
|
|
15
|
+
import { applyRenameMapToManifest, buildProjectedManifest } from '../../core/patch-policy.js';
|
|
16
|
+
import { buildPatchSourceMetadata } from '../../core/patch-source-metadata.js';
|
|
17
|
+
import { extractNewFileContentFromDiff } from '../../core/patch-transform.js';
|
|
18
|
+
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
19
|
+
import { pathExists } from '../../utils/fs.js';
|
|
20
|
+
import { warn } from '../../utils/logger.js';
|
|
21
|
+
/**
|
|
22
|
+
*
|
|
23
|
+
*/
|
|
24
|
+
export function assertSourceOwnsFiles(source, files) {
|
|
25
|
+
const owned = new Set(source.filesAffected);
|
|
26
|
+
const missing = files.filter((file) => !owned.has(file));
|
|
27
|
+
if (missing.length > 0) {
|
|
28
|
+
throw new InvalidArgumentError(`${source.filename} does not currently own ${missing.length} requested file(s): ${missing.join(', ')}. ` +
|
|
29
|
+
'Run "fireforge status --ownership" to inspect current ownership.', '--files');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
*
|
|
34
|
+
*/
|
|
35
|
+
export async function buildSplitDiff(engineDir, files, label, sourceFilename) {
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
if (!(await pathExists(join(engineDir, file)))) {
|
|
38
|
+
throw new GeneralError(`Cannot split ${sourceFilename}: ${label} file is missing from the engine worktree: ${file}. ` +
|
|
39
|
+
'Run "fireforge import" (or restore the file) so the worktree reflects the patch content first.');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const diff = await getDiffForFilesAgainstHead(engineDir, [...files]);
|
|
43
|
+
if (!diff.trim()) {
|
|
44
|
+
throw new GeneralError(`Cannot split ${sourceFilename}: the ${label} file set produces an empty diff against HEAD. ` +
|
|
45
|
+
'The worktree must currently carry the patch content (run "fireforge import" first).');
|
|
46
|
+
}
|
|
47
|
+
const actual = new Set(extractAffectedFiles(diff));
|
|
48
|
+
const noHunks = files.filter((file) => !actual.has(file));
|
|
49
|
+
if (noHunks.length > 0) {
|
|
50
|
+
throw new GeneralError(`Cannot split ${sourceFilename}: ${noHunks.length} ${label} file(s) produced no diff hunks ` +
|
|
51
|
+
`(${noHunks.join(', ')}). The worktree does not carry their patch content.`);
|
|
52
|
+
}
|
|
53
|
+
return diff;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Finds patches declaring a staged-dependency forward-import whose `owner`
|
|
57
|
+
* is the source patch and whose `creates` path moves to the new patch.
|
|
58
|
+
*/
|
|
59
|
+
export function findOwnerRewriteHolders(patches, sourceFilename, movedSet) {
|
|
60
|
+
return patches
|
|
61
|
+
.filter((patch) => (patch.stagedDependencies?.forwardImports ?? []).some((fi) => fi.owner === sourceFilename && movedSet.has(fi.creates)))
|
|
62
|
+
.map((patch) => patch.filename);
|
|
63
|
+
}
|
|
64
|
+
/** Rewrites split-affected owners on one manifest row. */
|
|
65
|
+
export function rewriteSplitOwners(patch, sourceFilename, movedSet, newFilename) {
|
|
66
|
+
const forwardImports = patch.stagedDependencies?.forwardImports;
|
|
67
|
+
if (!forwardImports?.some((fi) => fi.owner === sourceFilename && movedSet.has(fi.creates))) {
|
|
68
|
+
return patch;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
...patch,
|
|
72
|
+
stagedDependencies: {
|
|
73
|
+
...patch.stagedDependencies,
|
|
74
|
+
forwardImports: forwardImports.map((fi) => fi.owner === sourceFilename && movedSet.has(fi.creates) ? { ...fi, owner: newFilename } : fi),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function buildEntryProjection(diff) {
|
|
79
|
+
const newFiles = new Map();
|
|
80
|
+
for (const path of detectNewFilesInDiff(diff)) {
|
|
81
|
+
newFiles.set(path, extractNewFileContentFromDiff(diff, path));
|
|
82
|
+
}
|
|
83
|
+
return { diff, newFiles, modifiedFileAdditions: buildModifiedFileAdditionsFromDiff(diff) };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Projects the full split (renumber + shrunken source + synthetic new
|
|
87
|
+
* patch + owner rewrites) through cross-patch lint, reporting only the
|
|
88
|
+
* regressions the split itself would introduce.
|
|
89
|
+
*/
|
|
90
|
+
export async function runProjectedSplitLint(patchesDir, plan) {
|
|
91
|
+
const movedSet = new Set(plan.movedFiles);
|
|
92
|
+
const ownerLookup = (old) => plan.placement.renameMap.get(old)?.newFilename;
|
|
93
|
+
const baseCtx = await buildPatchQueueContext(patchesDir);
|
|
94
|
+
const projectedEntries = baseCtx.entries.map((entry) => {
|
|
95
|
+
let metadata = entry.metadata;
|
|
96
|
+
if (metadata) {
|
|
97
|
+
metadata = rewriteStagedDependencyOwners(metadata, ownerLookup);
|
|
98
|
+
metadata = rewriteSplitOwners(metadata, plan.source.filename, movedSet, plan.placement.newFilename);
|
|
99
|
+
}
|
|
100
|
+
const rename = plan.placement.renameMap.get(entry.filename);
|
|
101
|
+
const base = rename
|
|
102
|
+
? { ...entry, metadata, filename: rename.newFilename, order: rename.newOrder }
|
|
103
|
+
: { ...entry, metadata };
|
|
104
|
+
if (entry.filename !== plan.source.filename)
|
|
105
|
+
return base;
|
|
106
|
+
return { ...base, ...buildEntryProjection(plan.remainingDiff) };
|
|
107
|
+
});
|
|
108
|
+
projectedEntries.push({
|
|
109
|
+
filename: plan.placement.newFilename,
|
|
110
|
+
order: plan.placement.insertionOrder,
|
|
111
|
+
metadata: null,
|
|
112
|
+
...buildEntryProjection(plan.movedDiff),
|
|
113
|
+
});
|
|
114
|
+
projectedEntries.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
|
|
115
|
+
const baselineIssues = lintPatchQueue(baseCtx).filter((i) => i.severity === 'error');
|
|
116
|
+
const projectedIssues = lintPatchQueue({ entries: projectedEntries }).filter((i) => i.severity === 'error');
|
|
117
|
+
const regressions = computeProjectedLintRegressions(baselineIssues, projectedIssues);
|
|
118
|
+
if (baselineIssues.length > 0 && regressions.length === 0) {
|
|
119
|
+
warn(`Note: projected queue still has ${baselineIssues.length} pre-existing cross-patch ` +
|
|
120
|
+
'error(s) unrelated to this split. Run "fireforge verify" to list them.');
|
|
121
|
+
}
|
|
122
|
+
if (regressions.length === 0)
|
|
123
|
+
return null;
|
|
124
|
+
return {
|
|
125
|
+
reason: `split would introduce ${regressions.length} cross-patch lint error(s)`,
|
|
126
|
+
details: regressions.map((i) => `[${i.check}] ${i.file}: ${i.message}`),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** Builds the projected manifest for policy enforcement. */
|
|
130
|
+
export function projectSplitManifest(manifest, plan, newMetadata) {
|
|
131
|
+
const movedSet = new Set(plan.movedFiles);
|
|
132
|
+
const renamed = applyRenameMapToManifest(manifest, plan.placement.renameMap);
|
|
133
|
+
const effectiveSourceFilename = plan.placement.renameMap.get(plan.source.filename)?.newFilename ?? plan.source.filename;
|
|
134
|
+
const patched = renamed.patches.map((patch) => {
|
|
135
|
+
const withOwners = rewriteSplitOwners(patch, effectiveSourceFilename, movedSet, plan.placement.newFilename);
|
|
136
|
+
if (patch.filename !== effectiveSourceFilename)
|
|
137
|
+
return withOwners;
|
|
138
|
+
return { ...withOwners, filesAffected: plan.remainingFiles };
|
|
139
|
+
});
|
|
140
|
+
return buildProjectedManifest(renamed, [...patched, newMetadata]);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
*
|
|
144
|
+
*/
|
|
145
|
+
export function buildNewPatchMetadata(plan, config) {
|
|
146
|
+
return {
|
|
147
|
+
filename: plan.placement.newFilename,
|
|
148
|
+
order: plan.placement.insertionOrder,
|
|
149
|
+
category: plan.category,
|
|
150
|
+
name: plan.name,
|
|
151
|
+
description: plan.description,
|
|
152
|
+
createdAt: new Date().toISOString(),
|
|
153
|
+
...buildPatchSourceMetadata(config.firefox),
|
|
154
|
+
filesAffected: plan.movedFiles,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
*
|
|
159
|
+
*/
|
|
160
|
+
export function buildSplitSummary(plan) {
|
|
161
|
+
const summary = [
|
|
162
|
+
`split ${plan.source.filename}`,
|
|
163
|
+
`moved files (${plan.movedFiles.length}): ${plan.movedFiles.join(', ')}`,
|
|
164
|
+
`source keeps (${plan.remainingFiles.length}): ${plan.remainingFiles.join(', ')}`,
|
|
165
|
+
`new patch: ${plan.placement.newFilename} (order ${plan.placement.insertionOrder})`,
|
|
166
|
+
];
|
|
167
|
+
if (plan.placement.renameMap.size > 0) {
|
|
168
|
+
summary.push(`${plan.placement.renameMap.size} existing patch(es) renumbered to make room:`);
|
|
169
|
+
for (const [oldName, entry] of [...plan.placement.renameMap.entries()].sort((a, b) => a[1].newOrder - b[1].newOrder)) {
|
|
170
|
+
summary.push(` ${oldName} → ${entry.newFilename}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (plan.ownerRewrites.length > 0) {
|
|
174
|
+
summary.push(`staged-dependency owners re-pointed to the new patch in: ${plan.ownerRewrites.join(', ')}`);
|
|
175
|
+
}
|
|
176
|
+
return summary;
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=split-plan.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fireforge patch split <source> --files <p...> --name <n>` — moves files
|
|
3
|
+
* out of an existing patch into a brand-new patch as one transaction
|
|
4
|
+
* (field report A4).
|
|
5
|
+
*
|
|
6
|
+
* Before this command, splitting a patch whose files have inbound
|
|
7
|
+
* forward-imports required a precise manual order: re-point each
|
|
8
|
+
* staged-dependency owner at a not-yet-created patch, shrink the source
|
|
9
|
+
* via `re-export --files --allow-shrink`, then `export --order` — any
|
|
10
|
+
* other order refused or needed `--force-unsafe`. Split performs the
|
|
11
|
+
* shrink, the new-patch creation, and the dependent owner rewrites under
|
|
12
|
+
* one patch-directory lock with rollback, validating only the final
|
|
13
|
+
* projection.
|
|
14
|
+
*
|
|
15
|
+
* Preconditions match `re-export`: the engine worktree must currently
|
|
16
|
+
* reflect both patches' content, because both bodies are regenerated from
|
|
17
|
+
* the worktree.
|
|
18
|
+
*/
|
|
19
|
+
import { Command } from 'commander';
|
|
20
|
+
import type { CommandContext } from '../../types/cli.js';
|
|
21
|
+
import type { PatchSplitOptions } from '../../types/commands/index.js';
|
|
22
|
+
/**
|
|
23
|
+
* Runs the `patch split` command: plans the split, lints the projection,
|
|
24
|
+
* confirms, and commits transactionally.
|
|
25
|
+
*/
|
|
26
|
+
export declare function patchSplitCommand(projectRoot: string, sourceId: string, options: PatchSplitOptions): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Registers the `patch split` subcommand on the `patch` parent.
|
|
29
|
+
*/
|
|
30
|
+
export declare function registerPatchSplit(parent: Command, context: CommandContext): void;
|