@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.
Files changed (152) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +22 -0
  3. package/dist/src/commands/export-all.js +9 -16
  4. package/dist/src/commands/export-flow.d.ts +6 -0
  5. package/dist/src/commands/export-flow.js +6 -1
  6. package/dist/src/commands/export-placement-gate.d.ts +38 -0
  7. package/dist/src/commands/export-placement-gate.js +105 -0
  8. package/dist/src/commands/export-shared.d.ts +28 -0
  9. package/dist/src/commands/export-shared.js +46 -1
  10. package/dist/src/commands/export.js +52 -113
  11. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +0 -13
  12. package/dist/src/commands/furnace/chrome-doc-templates.js +1 -1
  13. package/dist/src/commands/furnace/create-dry-run.d.ts +1 -1
  14. package/dist/src/commands/furnace/create.d.ts +1 -2
  15. package/dist/src/commands/furnace/deploy.js +36 -114
  16. package/dist/src/commands/furnace/refresh.js +52 -32
  17. package/dist/src/commands/furnace/sync.js +2 -0
  18. package/dist/src/commands/import.js +108 -73
  19. package/dist/src/commands/lint-per-patch.d.ts +3 -1
  20. package/dist/src/commands/lint-per-patch.js +265 -74
  21. package/dist/src/commands/lint.d.ts +1 -58
  22. package/dist/src/commands/lint.js +193 -88
  23. package/dist/src/commands/patch/compact.d.ts +5 -2
  24. package/dist/src/commands/patch/compact.js +85 -25
  25. package/dist/src/commands/patch/delete.js +17 -17
  26. package/dist/src/commands/patch/index.js +2 -0
  27. package/dist/src/commands/patch/lint-ignore.js +3 -16
  28. package/dist/src/commands/patch/move-files.js +2 -0
  29. package/dist/src/commands/patch/patch-context.d.ts +41 -0
  30. package/dist/src/commands/patch/patch-context.js +53 -0
  31. package/dist/src/commands/patch/rename.js +10 -15
  32. package/dist/src/commands/patch/reorder.d.ts +0 -2
  33. package/dist/src/commands/patch/reorder.js +18 -19
  34. package/dist/src/commands/patch/split-plan.d.ts +66 -0
  35. package/dist/src/commands/patch/split-plan.js +178 -0
  36. package/dist/src/commands/patch/split.d.ts +30 -0
  37. package/dist/src/commands/patch/split.js +283 -0
  38. package/dist/src/commands/patch/staged-dependency.d.ts +1 -7
  39. package/dist/src/commands/patch/staged-dependency.js +4 -17
  40. package/dist/src/commands/patch/tier.js +4 -17
  41. package/dist/src/commands/re-export-files.js +4 -1
  42. package/dist/src/commands/re-export-scan.js +8 -1
  43. package/dist/src/commands/re-export.js +8 -1
  44. package/dist/src/commands/rebase/summary.d.ts +1 -5
  45. package/dist/src/commands/rebase/summary.js +1 -1
  46. package/dist/src/commands/status-output.js +77 -68
  47. package/dist/src/commands/test-diagnose.d.ts +23 -0
  48. package/dist/src/commands/test-diagnose.js +210 -0
  49. package/dist/src/commands/test-run.d.ts +68 -0
  50. package/dist/src/commands/test-run.js +97 -0
  51. package/dist/src/commands/test.js +214 -263
  52. package/dist/src/commands/token.js +15 -1
  53. package/dist/src/commands/wire.js +109 -78
  54. package/dist/src/core/build-audit.d.ts +1 -1
  55. package/dist/src/core/build-audit.js +2 -46
  56. package/dist/src/core/build-baseline-types.d.ts +38 -0
  57. package/dist/src/core/build-baseline-types.js +10 -0
  58. package/dist/src/core/build-baseline.d.ts +1 -31
  59. package/dist/src/core/build-prepare.d.ts +1 -1
  60. package/dist/src/core/build-prepare.js +2 -45
  61. package/dist/src/core/config-paths.d.ts +0 -8
  62. package/dist/src/core/config-paths.js +4 -4
  63. package/dist/src/core/config-state.d.ts +0 -6
  64. package/dist/src/core/config-state.js +1 -1
  65. package/dist/src/core/config-validate-patch-policy.js +12 -13
  66. package/dist/src/core/config-validate.js +74 -28
  67. package/dist/src/core/engine-changes.d.ts +24 -0
  68. package/dist/src/core/engine-changes.js +64 -0
  69. package/dist/src/core/firefox-cache.d.ts +0 -5
  70. package/dist/src/core/firefox-cache.js +1 -1
  71. package/dist/src/core/firefox-download.d.ts +0 -6
  72. package/dist/src/core/firefox-download.js +1 -1
  73. package/dist/src/core/furnace-apply-helpers.d.ts +1 -8
  74. package/dist/src/core/furnace-apply-helpers.js +11 -20
  75. package/dist/src/core/furnace-apply.d.ts +1 -1
  76. package/dist/src/core/furnace-apply.js +1 -1
  77. package/dist/src/core/furnace-checksum-utils.d.ts +7 -0
  78. package/dist/src/core/furnace-checksum-utils.js +15 -0
  79. package/dist/src/core/furnace-config-validate.d.ts +31 -0
  80. package/dist/src/core/furnace-config-validate.js +133 -0
  81. package/dist/src/core/furnace-config.d.ts +4 -32
  82. package/dist/src/core/furnace-config.js +15 -111
  83. package/dist/src/core/furnace-constants.d.ts +0 -10
  84. package/dist/src/core/furnace-constants.js +2 -2
  85. package/dist/src/core/furnace-css-fragments.d.ts +79 -0
  86. package/dist/src/core/furnace-css-fragments.js +243 -0
  87. package/dist/src/core/furnace-jsconfig.d.ts +63 -0
  88. package/dist/src/core/furnace-jsconfig.js +191 -0
  89. package/dist/src/core/furnace-validate-helpers.d.ts +16 -14
  90. package/dist/src/core/furnace-validate-helpers.js +40 -1
  91. package/dist/src/core/furnace-validate-registration.js +16 -1
  92. package/dist/src/core/furnace-validate.js +54 -2
  93. package/dist/src/core/git-base.d.ts +15 -0
  94. package/dist/src/core/git-base.js +32 -0
  95. package/dist/src/core/git-diff.d.ts +8 -0
  96. package/dist/src/core/git-diff.js +224 -59
  97. package/dist/src/core/git-file-ops.d.ts +39 -12
  98. package/dist/src/core/git-file-ops.js +84 -3
  99. package/dist/src/core/lint-cache.d.ts +0 -13
  100. package/dist/src/core/lint-cache.js +5 -5
  101. package/dist/src/core/mach.d.ts +22 -1
  102. package/dist/src/core/mach.js +27 -2
  103. package/dist/src/core/manifest-register.d.ts +5 -16
  104. package/dist/src/core/manifest-register.js +3 -1
  105. package/dist/src/core/patch-lint-checkjs.d.ts +75 -21
  106. package/dist/src/core/patch-lint-checkjs.js +263 -71
  107. package/dist/src/core/patch-lint-css.d.ts +23 -0
  108. package/dist/src/core/patch-lint-css.js +172 -0
  109. package/dist/src/core/patch-lint-jsdoc.js +63 -4
  110. package/dist/src/core/patch-lint-observer.d.ts +37 -0
  111. package/dist/src/core/patch-lint-observer.js +168 -0
  112. package/dist/src/core/patch-lint.d.ts +34 -11
  113. package/dist/src/core/patch-lint.js +24 -161
  114. package/dist/src/core/patch-manifest-io.d.ts +16 -0
  115. package/dist/src/core/patch-manifest-io.js +44 -2
  116. package/dist/src/core/patch-manifest-validate.d.ts +1 -8
  117. package/dist/src/core/patch-manifest-validate.js +1 -1
  118. package/dist/src/core/patch-manifest.d.ts +1 -1
  119. package/dist/src/core/patch-manifest.js +1 -1
  120. package/dist/src/core/patch-policy.d.ts +0 -4
  121. package/dist/src/core/patch-policy.js +10 -4
  122. package/dist/src/core/register-browser-content.d.ts +1 -1
  123. package/dist/src/core/register-module.d.ts +1 -1
  124. package/dist/src/core/register-result.d.ts +21 -0
  125. package/dist/src/core/register-result.js +9 -0
  126. package/dist/src/core/register-shared-css.d.ts +1 -1
  127. package/dist/src/core/register-test-manifest.d.ts +1 -1
  128. package/dist/src/core/test-harness-crash.d.ts +61 -0
  129. package/dist/src/core/test-harness-crash.js +140 -0
  130. package/dist/src/core/test-stale-check.d.ts +1 -1
  131. package/dist/src/core/test-stale-check.js +2 -46
  132. package/dist/src/core/test-xpcshell-retry.d.ts +9 -2
  133. package/dist/src/core/test-xpcshell-retry.js +10 -3
  134. package/dist/src/core/token-dark-mode.js +14 -26
  135. package/dist/src/core/token-manager.d.ts +4 -0
  136. package/dist/src/core/token-manager.js +70 -16
  137. package/dist/src/core/typecheck-shim.d.ts +3 -22
  138. package/dist/src/core/typecheck-shim.js +69 -7
  139. package/dist/src/core/wire-utils.js +37 -44
  140. package/dist/src/types/commands/index.d.ts +1 -1
  141. package/dist/src/types/commands/options.d.ts +122 -0
  142. package/dist/src/types/config.d.ts +11 -2
  143. package/dist/src/types/furnace.d.ts +12 -1
  144. package/dist/src/utils/elapsed.d.ts +0 -2
  145. package/dist/src/utils/elapsed.js +1 -1
  146. package/dist/src/utils/fs.d.ts +0 -5
  147. package/dist/src/utils/fs.js +1 -1
  148. package/dist/src/utils/regex.d.ts +0 -6
  149. package/dist/src/utils/regex.js +3 -3
  150. package/dist/src/utils/validation.d.ts +0 -8
  151. package/dist/src/utils/validation.js +2 -2
  152. 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 { getProjectPaths, loadConfig } from '../../core/config.js';
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, resolvePatchIdentifier, savePatchesManifest, } from '../../core/patch-manifest.js';
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
- if (!(await pathExists(paths.patches))) {
157
- throw new GeneralError('Patches directory not found.');
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 { getProjectPaths, loadConfig } from '../../core/config.js';
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
- export function padOrder(value, width) {
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
- if (!(await pathExists(paths.patches))) {
274
- throw new GeneralError('Patches directory not found.');
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;