@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.
Files changed (253) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +16 -10
  3. package/dist/bin/fireforge.js +25 -4
  4. package/dist/build-info.json +9 -0
  5. package/dist/src/cli.js +20 -3
  6. package/dist/src/commands/bootstrap.js +5 -1
  7. package/dist/src/commands/discard.js +2 -22
  8. package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
  9. package/dist/src/commands/doctor-check-core.d.ts +20 -0
  10. package/dist/src/commands/doctor-check-core.js +28 -0
  11. package/dist/src/commands/doctor-external-toolchains.js +2 -0
  12. package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
  13. package/dist/src/commands/doctor-furnace.js +13 -11
  14. package/dist/src/commands/doctor-orphaned-harness.js +2 -1
  15. package/dist/src/commands/doctor.js +2 -2
  16. package/dist/src/commands/export-all.js +9 -1
  17. package/dist/src/commands/export-flow.js +13 -3
  18. package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
  19. package/dist/src/commands/export-placement-conflicts.js +74 -0
  20. package/dist/src/commands/export-placement-gate.js +1 -1
  21. package/dist/src/commands/export-shared.d.ts +8 -1
  22. package/dist/src/commands/export-shared.js +16 -7
  23. package/dist/src/commands/export.js +2 -2
  24. package/dist/src/commands/furnace/apply.js +4 -3
  25. package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
  26. package/dist/src/commands/furnace/chrome-doc.js +13 -2
  27. package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
  28. package/dist/src/commands/furnace/create-browser-test.js +5 -0
  29. package/dist/src/commands/furnace/create.js +2 -2
  30. package/dist/src/commands/furnace/diff.js +2 -1
  31. package/dist/src/commands/furnace/init.js +4 -6
  32. package/dist/src/commands/furnace/override.js +2 -2
  33. package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
  34. package/dist/src/commands/furnace/remove-mutations.js +171 -0
  35. package/dist/src/commands/furnace/remove.js +21 -128
  36. package/dist/src/commands/furnace/rename-browser-test.js +54 -5
  37. package/dist/src/commands/furnace/scan.js +27 -36
  38. package/dist/src/commands/furnace/status.js +30 -28
  39. package/dist/src/commands/furnace/validate.js +25 -18
  40. package/dist/src/commands/import.js +6 -20
  41. package/dist/src/commands/lint-per-patch.js +12 -59
  42. package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
  43. package/dist/src/commands/lint-per-run-checkjs.js +102 -0
  44. package/dist/src/commands/lint.js +4 -4
  45. package/dist/src/commands/manifest.js +1 -1
  46. package/dist/src/commands/package.js +9 -16
  47. package/dist/src/commands/patch/compact.js +5 -5
  48. package/dist/src/commands/patch/delete.js +5 -5
  49. package/dist/src/commands/patch/lint-ignore.js +3 -2
  50. package/dist/src/commands/patch/move-files-create.js +22 -4
  51. package/dist/src/commands/patch/move-files-into.js +14 -10
  52. package/dist/src/commands/patch/move-files.js +6 -5
  53. package/dist/src/commands/patch/rename.d.ts +2 -2
  54. package/dist/src/commands/patch/rename.js +151 -50
  55. package/dist/src/commands/patch/reorder.d.ts +9 -0
  56. package/dist/src/commands/patch/reorder.js +14 -10
  57. package/dist/src/commands/patch/split-plan.d.ts +13 -6
  58. package/dist/src/commands/patch/split-plan.js +22 -12
  59. package/dist/src/commands/patch/split.d.ts +1 -1
  60. package/dist/src/commands/patch/split.js +23 -9
  61. package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
  62. package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
  63. package/dist/src/commands/patch/staged-dependency.js +10 -0
  64. package/dist/src/commands/re-export-adjacent.js +5 -6
  65. package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
  66. package/dist/src/commands/re-export-bulk-scan.js +142 -2
  67. package/dist/src/commands/re-export-drift.d.ts +74 -0
  68. package/dist/src/commands/re-export-drift.js +234 -0
  69. package/dist/src/commands/re-export-files.js +5 -3
  70. package/dist/src/commands/re-export-lint.d.ts +97 -0
  71. package/dist/src/commands/re-export-lint.js +210 -0
  72. package/dist/src/commands/re-export-options.js +8 -0
  73. package/dist/src/commands/re-export-register.d.ts +18 -0
  74. package/dist/src/commands/re-export-register.js +61 -0
  75. package/dist/src/commands/re-export-scan.js +13 -7
  76. package/dist/src/commands/re-export-single.d.ts +20 -0
  77. package/dist/src/commands/re-export-single.js +261 -0
  78. package/dist/src/commands/re-export.d.ts +0 -4
  79. package/dist/src/commands/re-export.js +113 -268
  80. package/dist/src/commands/rebase/abort.js +13 -4
  81. package/dist/src/commands/rebase/continue.js +11 -4
  82. package/dist/src/commands/rebase/index.js +19 -29
  83. package/dist/src/commands/rebase/patch-loop.js +12 -0
  84. package/dist/src/commands/reset.js +4 -23
  85. package/dist/src/commands/resolve.js +2 -9
  86. package/dist/src/commands/run.js +11 -21
  87. package/dist/src/commands/source.js +3 -9
  88. package/dist/src/commands/status-check.d.ts +13 -0
  89. package/dist/src/commands/status-check.js +31 -13
  90. package/dist/src/commands/status-json.d.ts +31 -0
  91. package/dist/src/commands/status-json.js +76 -0
  92. package/dist/src/commands/status-output.d.ts +1 -0
  93. package/dist/src/commands/status-output.js +10 -1
  94. package/dist/src/commands/status-ownership.d.ts +52 -0
  95. package/dist/src/commands/status-ownership.js +57 -0
  96. package/dist/src/commands/status.js +184 -175
  97. package/dist/src/commands/test-build-phase.d.ts +19 -0
  98. package/dist/src/commands/test-build-phase.js +168 -0
  99. package/dist/src/commands/test-diagnose.js +30 -2
  100. package/dist/src/commands/test-modes.d.ts +9 -1
  101. package/dist/src/commands/test-modes.js +46 -13
  102. package/dist/src/commands/test-register.js +59 -27
  103. package/dist/src/commands/test-run.d.ts +29 -3
  104. package/dist/src/commands/test-run.js +41 -4
  105. package/dist/src/commands/test-verdict.d.ts +32 -0
  106. package/dist/src/commands/test-verdict.js +48 -0
  107. package/dist/src/commands/test.d.ts +9 -0
  108. package/dist/src/commands/test.js +100 -123
  109. package/dist/src/commands/token-coverage.js +4 -9
  110. package/dist/src/commands/tree.d.ts +3 -0
  111. package/dist/src/commands/tree.js +171 -25
  112. package/dist/src/commands/verify.js +2 -2
  113. package/dist/src/commands/watch.js +22 -21
  114. package/dist/src/commands/wire.js +1 -1
  115. package/dist/src/core/ast-utils.d.ts +11 -0
  116. package/dist/src/core/ast-utils.js +11 -0
  117. package/dist/src/core/build-audit-platform.js +6 -1
  118. package/dist/src/core/build-audit-registration.js +2 -0
  119. package/dist/src/core/build-audit-resolve.d.ts +13 -0
  120. package/dist/src/core/build-audit-resolve.js +34 -13
  121. package/dist/src/core/build-audit.js +4 -20
  122. package/dist/src/core/build-baseline-types.d.ts +29 -8
  123. package/dist/src/core/build-baseline-types.js +2 -1
  124. package/dist/src/core/build-baseline.d.ts +10 -2
  125. package/dist/src/core/build-baseline.js +39 -8
  126. package/dist/src/core/build-prepare.d.ts +4 -0
  127. package/dist/src/core/build-prepare.js +7 -1
  128. package/dist/src/core/config-paths.d.ts +1 -1
  129. package/dist/src/core/config-paths.js +2 -0
  130. package/dist/src/core/config-validate-test-toolchains.js +3 -2
  131. package/dist/src/core/config-validate.js +34 -36
  132. package/dist/src/core/config.js +16 -3
  133. package/dist/src/core/coverage-extend.d.ts +89 -0
  134. package/dist/src/core/coverage-extend.js +188 -0
  135. package/dist/src/core/destructive.js +3 -1
  136. package/dist/src/core/discard-baseline.js +18 -4
  137. package/dist/src/core/engine-precondition.d.ts +35 -0
  138. package/dist/src/core/engine-precondition.js +50 -0
  139. package/dist/src/core/engine-session-lock.d.ts +38 -1
  140. package/dist/src/core/engine-session-lock.js +78 -4
  141. package/dist/src/core/file-lock.d.ts +7 -0
  142. package/dist/src/core/file-lock.js +20 -30
  143. package/dist/src/core/firefox.js +5 -2
  144. package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
  145. package/dist/src/core/furnace-apply-helpers.js +12 -3
  146. package/dist/src/core/furnace-apply-output.js +9 -0
  147. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
  148. package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
  149. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
  150. package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
  151. package/dist/src/core/furnace-apply.d.ts +16 -4
  152. package/dist/src/core/furnace-apply.js +39 -43
  153. package/dist/src/core/furnace-config.js +2 -0
  154. package/dist/src/core/furnace-css-fragments.js +2 -0
  155. package/dist/src/core/furnace-jsconfig.js +14 -7
  156. package/dist/src/core/furnace-registration-ast.js +8 -8
  157. package/dist/src/core/furnace-registration-remove.js +6 -3
  158. package/dist/src/core/furnace-registration.js +9 -12
  159. package/dist/src/core/furnace-rollback.js +1 -1
  160. package/dist/src/core/furnace-scanner.js +2 -1
  161. package/dist/src/core/furnace-stale-export.js +2 -1
  162. package/dist/src/core/furnace-state-persist.d.ts +4 -6
  163. package/dist/src/core/furnace-validate-compatibility.js +4 -7
  164. package/dist/src/core/furnace-validate-helpers.js +33 -1
  165. package/dist/src/core/furnace-validate-registration.js +16 -0
  166. package/dist/src/core/furnace-validate.js +4 -2
  167. package/dist/src/core/git-diff.js +37 -2
  168. package/dist/src/core/lint-cache.d.ts +1 -1
  169. package/dist/src/core/lint-cache.js +33 -10
  170. package/dist/src/core/mach-build-artifacts.d.ts +63 -1
  171. package/dist/src/core/mach-build-artifacts.js +108 -2
  172. package/dist/src/core/mach-resource-shim.js +1 -0
  173. package/dist/src/core/marionette-port.d.ts +16 -0
  174. package/dist/src/core/marionette-port.js +93 -8
  175. package/dist/src/core/marionette-preflight.js +6 -5
  176. package/dist/src/core/patch-apply.d.ts +20 -13
  177. package/dist/src/core/patch-apply.js +32 -33
  178. package/dist/src/core/patch-export.js +3 -5
  179. package/dist/src/core/patch-lint-checkjs.d.ts +25 -3
  180. package/dist/src/core/patch-lint-checkjs.js +78 -7
  181. package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
  182. package/dist/src/core/patch-lint-creators.d.ts +30 -0
  183. package/dist/src/core/patch-lint-creators.js +45 -0
  184. package/dist/src/core/patch-lint-cross.d.ts +1 -17
  185. package/dist/src/core/patch-lint-cross.js +16 -32
  186. package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
  187. package/dist/src/core/patch-lint-jsdoc.js +15 -5
  188. package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
  189. package/dist/src/core/patch-lint-module-registration.js +88 -0
  190. package/dist/src/core/patch-lint-mozbuild.js +99 -11
  191. package/dist/src/core/patch-lint-reexports.d.ts +1 -0
  192. package/dist/src/core/patch-lint-reexports.js +1 -0
  193. package/dist/src/core/patch-lint.d.ts +8 -0
  194. package/dist/src/core/patch-lint.js +84 -6
  195. package/dist/src/core/patch-lock.d.ts +12 -1
  196. package/dist/src/core/patch-lock.js +40 -4
  197. package/dist/src/core/patch-manifest-consistency.js +15 -3
  198. package/dist/src/core/patch-parse.d.ts +8 -0
  199. package/dist/src/core/patch-parse.js +10 -0
  200. package/dist/src/core/patch-policy.d.ts +7 -0
  201. package/dist/src/core/patch-policy.js +4 -1
  202. package/dist/src/core/patch-transform.d.ts +10 -0
  203. package/dist/src/core/patch-transform.js +13 -2
  204. package/dist/src/core/rebase-session.d.ts +44 -2
  205. package/dist/src/core/rebase-session.js +114 -17
  206. package/dist/src/core/register-browser-content.js +2 -1
  207. package/dist/src/core/register-shared-css.js +2 -1
  208. package/dist/src/core/smoke-patterns.d.ts +0 -8
  209. package/dist/src/core/smoke-patterns.js +3 -2
  210. package/dist/src/core/status-binary.d.ts +55 -0
  211. package/dist/src/core/status-binary.js +124 -0
  212. package/dist/src/core/status-classify.d.ts +6 -1
  213. package/dist/src/core/status-classify.js +101 -73
  214. package/dist/src/core/test-harness-crash.d.ts +32 -1
  215. package/dist/src/core/test-harness-crash.js +59 -4
  216. package/dist/src/core/test-path-scope.js +2 -0
  217. package/dist/src/core/test-stale-check.js +1 -20
  218. package/dist/src/core/toolchain-preflight.js +24 -26
  219. package/dist/src/core/tree-guard.d.ts +1 -1
  220. package/dist/src/core/tree-guard.js +67 -12
  221. package/dist/src/core/tree-store.d.ts +86 -10
  222. package/dist/src/core/tree-store.js +310 -72
  223. package/dist/src/core/typecheck.js +2 -1
  224. package/dist/src/core/wire-destroy.js +14 -3
  225. package/dist/src/core/wire-init.js +37 -40
  226. package/dist/src/core/wire-subscript.js +11 -3
  227. package/dist/src/core/wire-utils.js +4 -4
  228. package/dist/src/core/xpcshell-appdir.js +3 -0
  229. package/dist/src/errors/base.d.ts +13 -0
  230. package/dist/src/errors/base.js +13 -0
  231. package/dist/src/errors/rebase.d.ts +13 -0
  232. package/dist/src/errors/rebase.js +17 -0
  233. package/dist/src/types/commands/options.d.ts +86 -1
  234. package/dist/src/types/commands/patches.d.ts +8 -0
  235. package/dist/src/types/furnace.d.ts +7 -0
  236. package/dist/src/utils/build-info.d.ts +31 -0
  237. package/dist/src/utils/build-info.js +98 -0
  238. package/dist/src/utils/concurrency.d.ts +8 -0
  239. package/dist/src/utils/concurrency.js +21 -0
  240. package/dist/src/utils/errors.d.ts +39 -0
  241. package/dist/src/utils/errors.js +55 -0
  242. package/dist/src/utils/fs.js +5 -12
  243. package/dist/src/utils/logger.d.ts +14 -2
  244. package/dist/src/utils/logger.js +43 -13
  245. package/dist/src/utils/options.d.ts +21 -4
  246. package/dist/src/utils/options.js +26 -6
  247. package/dist/src/utils/package-root.js +7 -4
  248. package/dist/src/utils/process-group.js +13 -2
  249. package/dist/src/utils/stdio-drain.d.ts +28 -0
  250. package/dist/src/utils/stdio-drain.js +54 -0
  251. package/dist/src/utils/validation.d.ts +23 -2
  252. package/dist/src/utils/validation.js +29 -3
  253. 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
- * Runs the `patch rename` command: relabels filename + manifest entry
148
- * for a single patch atomically.
149
- *
150
- * @param projectRoot - Project root directory
151
- * @param identifier - Patch filename, ordinal, or manifest `name`
152
- * @param options - Command options (`--to <new-name>` is required)
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 newSlug = patchNameSlug(options.to, split.category);
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
- const newFilename = `${split.ordinalStr}-${split.category}-${newSlug}.patch`;
172
- const filenameChanging = newFilename !== target.filename;
173
- const nameChanging = options.to !== target.name;
174
- const descriptionChanging = options.description !== undefined && options.description !== target.description;
175
- if (!filenameChanging && !nameChanging && !descriptionChanging) {
176
- info(`${target.filename}: nothing to change — filename already "${target.filename}" ` +
177
- `(slug of "${options.to}" is "${newSlug}"), name already "${target.name}"` +
178
- (options.description !== undefined ? ', description already matches' : '') +
179
- '.');
180
- outro(isDryRun ? 'Dry run complete — no changes made' : 'Patch rename (no-op)');
181
- return;
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
- // Pre-flight collision check against the manifest snapshot we already
184
- // loaded. The authoritative check happens again inside the lock to
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 + manifest name (and optional description) update without rewriting the .patch body.')
265
- .requiredOption('--to <new-name>', 'New human-readable name, category-prefixed slug, or full filename stem (normalised into the filename slug)')
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
- .action(withErrorHandling(async (name, options) => {
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). Pass --force-unsafe to override.`, '--force-unsafe');
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((i) => `[${i.check}] ${i.file}: ${i.message}`),
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
- .action(withErrorHandling(async (name, options) => {
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(patchesDir: string, plan: SplitPlan): Promise<{
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
- async function buildProjectedSplitEntries(patchesDir, plan) {
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 { baseCtx, entries };
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 async function runProjectedSplitLint(patchesDir, plan) {
185
- const { baseCtx, entries: projectedEntries } = await buildProjectedSplitEntries(patchesDir, plan);
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({ entries: projectedEntries }).filter((i) => i.severity === 'error');
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((i) => `[${i.check}] ${i.file}: ${i.message}`),
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.