@hominis/fireforge 0.51.0 → 0.53.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 (254) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/build-info.json +4 -4
  3. package/dist/src/commands/bootstrap.js +2 -3
  4. package/dist/src/commands/build-prelock.d.ts +3 -3
  5. package/dist/src/commands/build-prelock.js +3 -3
  6. package/dist/src/commands/build.js +4 -0
  7. package/dist/src/commands/declarations-report.d.ts +9 -3
  8. package/dist/src/commands/declarations-report.js +18 -11
  9. package/dist/src/commands/discard.js +3 -3
  10. package/dist/src/commands/doctor-furnace-config-sync.js +4 -4
  11. package/dist/src/commands/doctor-furnace.js +5 -5
  12. package/dist/src/commands/export-adopt-check.d.ts +5 -6
  13. package/dist/src/commands/export-adopt-check.js +5 -6
  14. package/dist/src/commands/export-flow.js +6 -9
  15. package/dist/src/commands/export-staged-registration.d.ts +4 -4
  16. package/dist/src/commands/export-staged-registration.js +4 -4
  17. package/dist/src/commands/forward-import-remedy.d.ts +2 -3
  18. package/dist/src/commands/forward-import-remedy.js +2 -3
  19. package/dist/src/commands/furnace/create-dry-run.d.ts +2 -2
  20. package/dist/src/commands/furnace/deploy-fragment-includers.d.ts +2 -2
  21. package/dist/src/commands/furnace/deploy-fragment-includers.js +2 -2
  22. package/dist/src/commands/furnace/index.js +2 -2
  23. package/dist/src/commands/lint.js +4 -4
  24. package/dist/src/commands/patch/compact.js +1 -1
  25. package/dist/src/commands/patch/lint-ignore-from.d.ts +3 -4
  26. package/dist/src/commands/patch/lint-ignore-from.js +3 -4
  27. package/dist/src/commands/patch/move-files-adopt.d.ts +33 -0
  28. package/dist/src/commands/patch/move-files-adopt.js +65 -0
  29. package/dist/src/commands/patch/move-files-create.js +41 -20
  30. package/dist/src/commands/patch/move-files-into.d.ts +18 -0
  31. package/dist/src/commands/patch/move-files-into.js +125 -60
  32. package/dist/src/commands/patch/move-files-retire.d.ts +39 -0
  33. package/dist/src/commands/patch/move-files-retire.js +91 -0
  34. package/dist/src/commands/patch/move-files.d.ts +3 -0
  35. package/dist/src/commands/patch/move-files.js +42 -12
  36. package/dist/src/commands/patch/move-size-tier.d.ts +27 -0
  37. package/dist/src/commands/patch/move-size-tier.js +66 -0
  38. package/dist/src/commands/patch/rename.js +1 -1
  39. package/dist/src/commands/patch/reorder-projection.d.ts +17 -0
  40. package/dist/src/commands/patch/reorder-projection.js +49 -0
  41. package/dist/src/commands/patch/reorder-shift.d.ts +30 -0
  42. package/dist/src/commands/patch/reorder-shift.js +307 -0
  43. package/dist/src/commands/patch/reorder.d.ts +9 -15
  44. package/dist/src/commands/patch/reorder.js +52 -50
  45. package/dist/src/commands/patch/split-plan.d.ts +31 -3
  46. package/dist/src/commands/patch/split-plan.js +36 -12
  47. package/dist/src/commands/patch/split-staged.d.ts +2 -2
  48. package/dist/src/commands/patch/split-staged.js +2 -2
  49. package/dist/src/commands/patch/split.js +18 -5
  50. package/dist/src/commands/patch/staged-dependency-entries.d.ts +9 -1
  51. package/dist/src/commands/patch/staged-dependency-entries.js +32 -4
  52. package/dist/src/commands/patch/staged-dependency-from.js +15 -9
  53. package/dist/src/commands/patch/staged-dependency.js +13 -9
  54. package/dist/src/commands/patch/tier.js +2 -2
  55. package/dist/src/commands/patch-rename-map.d.ts +14 -3
  56. package/dist/src/commands/patch-rename-map.js +21 -3
  57. package/dist/src/commands/re-export-adjacent.d.ts +4 -5
  58. package/dist/src/commands/re-export-lint.d.ts +4 -3
  59. package/dist/src/commands/re-export-lint.js +4 -3
  60. package/dist/src/commands/re-export-preflight.js +1 -2
  61. package/dist/src/commands/re-export-purity-scope.d.ts +1 -1
  62. package/dist/src/commands/re-export-purity-scope.js +13 -7
  63. package/dist/src/commands/re-export-retired-declarations.d.ts +22 -0
  64. package/dist/src/commands/re-export-retired-declarations.js +113 -0
  65. package/dist/src/commands/re-export-single.js +2 -2
  66. package/dist/src/commands/re-export-stale-registrations.d.ts +27 -6
  67. package/dist/src/commands/re-export-stale-registrations.js +65 -21
  68. package/dist/src/commands/re-export.js +4 -0
  69. package/dist/src/commands/rebase/continue.js +3 -3
  70. package/dist/src/commands/register.js +5 -4
  71. package/dist/src/commands/status-output.js +5 -1
  72. package/dist/src/commands/status.js +1 -1
  73. package/dist/src/commands/test-build-furnace.d.ts +1 -2
  74. package/dist/src/commands/test-build-furnace.js +1 -2
  75. package/dist/src/commands/test-build-phase.d.ts +10 -0
  76. package/dist/src/commands/test-build-phase.js +51 -16
  77. package/dist/src/commands/test-control-restore.d.ts +40 -0
  78. package/dist/src/commands/test-control-restore.js +86 -0
  79. package/dist/src/commands/test-dispatch.js +83 -13
  80. package/dist/src/commands/test-multi-verdict.js +1 -0
  81. package/dist/src/commands/test-orchestrate.d.ts +11 -0
  82. package/dist/src/commands/test-orchestrate.js +33 -2
  83. package/dist/src/commands/test-perf-scope.d.ts +1 -1
  84. package/dist/src/commands/test-perf-scope.js +1 -1
  85. package/dist/src/commands/test-prelock.d.ts +7 -8
  86. package/dist/src/commands/test-prelock.js +15 -12
  87. package/dist/src/commands/test-register.js +2 -2
  88. package/dist/src/commands/test-results.d.ts +10 -1
  89. package/dist/src/commands/test-results.js +20 -3
  90. package/dist/src/commands/test-run.d.ts +6 -0
  91. package/dist/src/commands/test-run.js +23 -4
  92. package/dist/src/commands/test-shards.d.ts +7 -3
  93. package/dist/src/commands/test-shards.js +24 -7
  94. package/dist/src/commands/test-stale-gate.d.ts +6 -1
  95. package/dist/src/commands/test-stale-gate.js +47 -2
  96. package/dist/src/commands/test-verdict.d.ts +5 -3
  97. package/dist/src/commands/test.js +14 -1
  98. package/dist/src/commands/token.js +2 -2
  99. package/dist/src/commands/verify.js +1 -1
  100. package/dist/src/commands/watch.js +1 -1
  101. package/dist/src/commands/wire.js +1 -1
  102. package/dist/src/core/build-audit-resolve.d.ts +4 -5
  103. package/dist/src/core/build-audit-resolve.js +4 -5
  104. package/dist/src/core/build-audit-skip.d.ts +2 -2
  105. package/dist/src/core/build-audit.d.ts +3 -4
  106. package/dist/src/core/build-baseline-types.d.ts +37 -3
  107. package/dist/src/core/build-baseline-validate.d.ts +30 -1
  108. package/dist/src/core/build-baseline-validate.js +134 -33
  109. package/dist/src/core/build-baseline.d.ts +8 -2
  110. package/dist/src/core/build-baseline.js +32 -16
  111. package/dist/src/core/build-jar-escalation.d.ts +4 -4
  112. package/dist/src/core/build-jar-escalation.js +4 -4
  113. package/dist/src/core/build-prepare.js +4 -4
  114. package/dist/src/core/config-validate.js +4 -4
  115. package/dist/src/core/coverage-extend.d.ts +5 -3
  116. package/dist/src/core/coverage-extend.js +8 -3
  117. package/dist/src/core/coverage-records.d.ts +4 -5
  118. package/dist/src/core/coverage-records.js +4 -5
  119. package/dist/src/core/destructive.d.ts +2 -2
  120. package/dist/src/core/destructive.js +2 -2
  121. package/dist/src/core/discard-baseline.d.ts +2 -2
  122. package/dist/src/core/discard-baseline.js +3 -3
  123. package/dist/src/core/engine-session-lock.d.ts +1 -1
  124. package/dist/src/core/engine-session-lock.js +1 -1
  125. package/dist/src/core/file-lock-owner.d.ts +5 -5
  126. package/dist/src/core/file-lock-owner.js +5 -5
  127. package/dist/src/core/file-lock-queue.js +6 -8
  128. package/dist/src/core/file-lock-shared.d.ts +4 -4
  129. package/dist/src/core/file-lock-shared.js +4 -4
  130. package/dist/src/core/file-lock.js +7 -10
  131. package/dist/src/core/firefox.js +2 -2
  132. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +1 -1
  133. package/dist/src/core/furnace-apply-overwrite-warn.js +1 -1
  134. package/dist/src/core/furnace-apply.js +3 -3
  135. package/dist/src/core/furnace-config-validate-exceptions.d.ts +4 -3
  136. package/dist/src/core/furnace-config-validate-exceptions.js +26 -6
  137. package/dist/src/core/furnace-operation.js +5 -5
  138. package/dist/src/core/furnace-registration-jar-inplace.js +5 -6
  139. package/dist/src/core/furnace-registration-rename.d.ts +5 -5
  140. package/dist/src/core/furnace-registration-rename.js +5 -5
  141. package/dist/src/core/furnace-registration-validate.d.ts +2 -1
  142. package/dist/src/core/furnace-registration-validate.js +2 -1
  143. package/dist/src/core/furnace-registration.js +2 -2
  144. package/dist/src/core/furnace-validate-accessibility.d.ts +1 -1
  145. package/dist/src/core/furnace-validate-accessibility.js +22 -11
  146. package/dist/src/core/furnace-validate-exceptions.d.ts +2 -1
  147. package/dist/src/core/furnace-validate-exceptions.js +31 -13
  148. package/dist/src/core/git-base.js +4 -3
  149. package/dist/src/core/git-diff.js +1 -1
  150. package/dist/src/core/git.js +1 -1
  151. package/dist/src/core/harness-orphans.d.ts +6 -6
  152. package/dist/src/core/harness-orphans.js +6 -6
  153. package/dist/src/core/harness-port-reservation.d.ts +60 -0
  154. package/dist/src/core/harness-port-reservation.js +122 -0
  155. package/dist/src/core/history-log-file.d.ts +9 -9
  156. package/dist/src/core/history-log-file.js +15 -15
  157. package/dist/src/core/history-log.d.ts +3 -3
  158. package/dist/src/core/history-log.js +3 -3
  159. package/dist/src/core/launchable-build.d.ts +39 -0
  160. package/dist/src/core/launchable-build.js +85 -0
  161. package/dist/src/core/marionette-port.js +1 -3
  162. package/dist/src/core/moz-manifest-rules.js +3 -2
  163. package/dist/src/core/negative-control-compiled.d.ts +22 -3
  164. package/dist/src/core/negative-control-compiled.js +41 -5
  165. package/dist/src/core/parser-fallback.d.ts +5 -5
  166. package/dist/src/core/parser-fallback.js +5 -5
  167. package/dist/src/core/patch-identifier-suggest.d.ts +6 -0
  168. package/dist/src/core/patch-identifier-suggest.js +11 -4
  169. package/dist/src/core/patch-lint-checkjs.d.ts +1 -1
  170. package/dist/src/core/patch-lint-checkjs.js +1 -1
  171. package/dist/src/core/patch-lint-css.js +2 -2
  172. package/dist/src/core/patch-lint-file-size.d.ts +5 -7
  173. package/dist/src/core/patch-lint-file-size.js +5 -7
  174. package/dist/src/core/patch-lint-ignore.d.ts +3 -3
  175. package/dist/src/core/patch-lint-ignore.js +3 -3
  176. package/dist/src/core/patch-lint-prettier.d.ts +4 -6
  177. package/dist/src/core/patch-lint-prettier.js +4 -6
  178. package/dist/src/core/patch-lint-projection.d.ts +1 -1
  179. package/dist/src/core/patch-lint-projection.js +1 -1
  180. package/dist/src/core/patch-lint-queue-types.d.ts +2 -2
  181. package/dist/src/core/patch-lint-queue-types.js +2 -2
  182. package/dist/src/core/patch-manifest-consistency.d.ts +2 -2
  183. package/dist/src/core/patch-manifest-consistency.js +2 -2
  184. package/dist/src/core/patch-manifest-io.d.ts +5 -1
  185. package/dist/src/core/patch-manifest-io.js +12 -10
  186. package/dist/src/core/patch-manifest-key-order.d.ts +3 -4
  187. package/dist/src/core/patch-manifest-key-order.js +3 -4
  188. package/dist/src/core/patch-parse.d.ts +3 -3
  189. package/dist/src/core/patch-parse.js +3 -3
  190. package/dist/src/core/patch-policy.d.ts +1 -1
  191. package/dist/src/core/patch-policy.js +1 -1
  192. package/dist/src/core/patch-source-metadata.d.ts +1 -1
  193. package/dist/src/core/patch-source-metadata.js +1 -1
  194. package/dist/src/core/patch-subset.d.ts +5 -3
  195. package/dist/src/core/patch-subset.js +8 -7
  196. package/dist/src/core/patch-transform.d.ts +2 -2
  197. package/dist/src/core/patch-transform.js +2 -2
  198. package/dist/src/core/rebase-session.js +4 -6
  199. package/dist/src/core/register-xpcshell-test.d.ts +28 -7
  200. package/dist/src/core/register-xpcshell-test.js +76 -27
  201. package/dist/src/core/run-log.d.ts +8 -8
  202. package/dist/src/core/run-log.js +8 -8
  203. package/dist/src/core/status-classify.d.ts +1 -2
  204. package/dist/src/core/status-classify.js +1 -2
  205. package/dist/src/core/test-harness-crash.d.ts +5 -3
  206. package/dist/src/core/test-harness-crash.js +13 -14
  207. package/dist/src/core/test-harness-infra.d.ts +32 -0
  208. package/dist/src/core/test-harness-infra.js +69 -0
  209. package/dist/src/core/test-harness-signature.d.ts +6 -0
  210. package/dist/src/core/test-run-completeness.d.ts +12 -5
  211. package/dist/src/core/test-run-completeness.js +15 -8
  212. package/dist/src/core/test-stale-check.d.ts +48 -1
  213. package/dist/src/core/test-stale-check.js +97 -2
  214. package/dist/src/core/test-stall-triage.js +5 -7
  215. package/dist/src/core/token-category.js +2 -2
  216. package/dist/src/core/token-inventory.js +3 -3
  217. package/dist/src/core/token-manager.d.ts +2 -2
  218. package/dist/src/core/token-manager.js +5 -7
  219. package/dist/src/core/token-variant.d.ts +6 -6
  220. package/dist/src/core/token-variant.js +7 -7
  221. package/dist/src/core/typecheck-shim.d.ts +2 -1
  222. package/dist/src/core/typecheck-shim.js +29 -5
  223. package/dist/src/core/wire-destroy.d.ts +1 -1
  224. package/dist/src/core/wire-destroy.js +1 -1
  225. package/dist/src/core/wire-dom-fragment.js +1 -1
  226. package/dist/src/core/wire-init.d.ts +1 -1
  227. package/dist/src/core/wire-init.js +1 -1
  228. package/dist/src/core/wire-subscript.d.ts +1 -1
  229. package/dist/src/core/wire-subscript.js +1 -1
  230. package/dist/src/core/wire-utils.d.ts +2 -2
  231. package/dist/src/core/wire-utils.js +3 -3
  232. package/dist/src/errors/base.d.ts +1 -1
  233. package/dist/src/errors/base.js +1 -1
  234. package/dist/src/errors/rebase.d.ts +1 -1
  235. package/dist/src/errors/rebase.js +1 -1
  236. package/dist/src/types/commands/options.d.ts +26 -3
  237. package/dist/src/types/config.d.ts +5 -7
  238. package/dist/src/types/furnace.d.ts +9 -0
  239. package/dist/src/utils/options.js +2 -2
  240. package/dist/src/utils/process.js +2 -2
  241. package/dist/src/utils/regex.d.ts +2 -2
  242. package/dist/src/utils/regex.js +3 -3
  243. package/dist/src/utils/sleep.d.ts +5 -6
  244. package/dist/src/utils/sleep.js +5 -6
  245. package/dist/src/utils/validation.js +2 -2
  246. package/docs/configuration.md +40 -26
  247. package/docs/exit-codes.md +8 -11
  248. package/docs/furnace.md +21 -3
  249. package/docs/lifecycle-invariants.md +36 -45
  250. package/docs/machine-output.md +43 -22
  251. package/docs/patch-workflow.md +129 -24
  252. package/docs/run-logs.md +8 -9
  253. package/docs/testing.md +118 -44
  254. package/package.json +1 -1
@@ -0,0 +1,307 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * `fireforge patch reorder --shift <n> --from <order> [--through <order>]`
4
+ * renumbers a run of patches up by `n` in one gap-checked transaction, so
5
+ * an insert has room.
6
+ *
7
+ * Opening room for four inserts after 103 meant moving 104 to 128 up by
8
+ * four. `patch rename --order` moves one patch, and under
9
+ * `patchPolicy.allowGaps: false` every single step refused on the gap it
10
+ * left, so the only way through was one `patch split --after` per insert.
11
+ * The shift moves the whole run at once (the retired and reserved slots
12
+ * inside it move with it), and under `allowGaps: false` records the orders
13
+ * it opens as `patches.json#reserved` slots, so the queue stays valid and
14
+ * the inserts that follow fill them.
15
+ *
16
+ * Same contract as `patch compact`: a projected policy check, a projected
17
+ * cross-patch lint (reporting only what the shift itself adds), a
18
+ * confirmation, and a commit under the patch-directory lock that refuses
19
+ * if the plan changed while it waited.
20
+ */
21
+ import { loadConfig } from '../../core/config.js';
22
+ import { confirmDestructive } from '../../core/destructive.js';
23
+ import { appendHistoryBestEffort } from '../../core/history-log.js';
24
+ import { buildPatchQueueContext, formatPatchLintIssue, lintPatchQueue, } from '../../core/patch-lint.js';
25
+ import { computeProjectedLintRegressions } from '../../core/patch-lint-projection.js';
26
+ import { withPatchDirectoryLock } from '../../core/patch-lock.js';
27
+ import { loadPatchesManifest, loadPatchesManifestForWrite, renumberPatchesInManifest, savePatchesManifest, } from '../../core/patch-manifest.js';
28
+ import { formatPatchOrder } from '../../core/patch-parse.js';
29
+ import { applyRenameMapToManifest, enforcePatchPolicy } from '../../core/patch-policy.js';
30
+ import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
31
+ import { toError } from '../../utils/errors.js';
32
+ import { info, intro, outro, warn } from '../../utils/logger.js';
33
+ import { resolveWaitLockSeconds } from '../../utils/options.js';
34
+ import { proceedAfterDecision } from '../destructive-decision.js';
35
+ import { getSortedRenameEntries, renameMapsEqual, writeRenameMapFile, } from '../patch-rename-map.js';
36
+ import { requirePatchQueue } from './patch-context.js';
37
+ import { projectReorder, rebuildFilenameForOrder } from './reorder-projection.js';
38
+ /** Owner label recorded on the reservations a shift opens. */
39
+ const SHIFT_RESERVATION_OWNER = 'patch reorder --shift';
40
+ /** What occupies an order: a patch, a retired slot or a reservation. */
41
+ function occupantAt(manifest, order) {
42
+ const patch = manifest.patches.find((entry) => entry.order === order);
43
+ if (patch !== undefined)
44
+ return patch.filename;
45
+ const retired = manifest.retired?.find((slot) => slot.order === order);
46
+ if (retired !== undefined)
47
+ return `the retired slot of ${retired.filename}`;
48
+ const reserved = manifest.reserved?.find((slot) => slot.order === order);
49
+ if (reserved !== undefined)
50
+ return `the reservation held by ${reserved.owner}`;
51
+ return undefined;
52
+ }
53
+ /** The category of whatever occupies `order`. */
54
+ function categoryAt(manifest, order) {
55
+ return (manifest.patches.find((entry) => entry.order === order)?.category ??
56
+ manifest.retired?.find((slot) => slot.order === order)?.category ??
57
+ manifest.reserved?.find((slot) => slot.order === order)?.category);
58
+ }
59
+ /**
60
+ * The last order of the run that starts at `from`: consecutive occupied
61
+ * orders (patches and slots) of the first occupant's category.
62
+ */
63
+ function defaultThrough(manifest, from) {
64
+ const category = categoryAt(manifest, from);
65
+ let through = from;
66
+ while (occupantAt(manifest, through + 1) !== undefined &&
67
+ categoryAt(manifest, through + 1) === category) {
68
+ through++;
69
+ }
70
+ return through;
71
+ }
72
+ /**
73
+ * Plans a shift against `manifest`, refusing a run with no patch, a
74
+ * `--through` before `--from`, and a shift that lands on an occupied order
75
+ * past the run.
76
+ *
77
+ * @param manifest - The queue
78
+ * @param config - Project configuration (`patchPolicy.allowGaps`)
79
+ * @param options - `shift`, `from` and optional `through`
80
+ */
81
+ function planShift(manifest, config, options) {
82
+ const { shift, from } = options;
83
+ if (occupantAt(manifest, from) === undefined) {
84
+ throw new InvalidArgumentError(`No patch or slot holds order ${formatPatchOrder(from)}; --from names the first order of the run to shift.`, '--from');
85
+ }
86
+ const through = options.through ?? defaultThrough(manifest, from);
87
+ if (through < from) {
88
+ throw new InvalidArgumentError(`--through ${through} is before --from ${from}.`, '--through');
89
+ }
90
+ const inRun = (order) => order >= from && order <= through;
91
+ const members = manifest.patches.filter((patch) => inRun(patch.order));
92
+ if (members.length === 0) {
93
+ throw new InvalidArgumentError(`No patch sits between orders ${formatPatchOrder(from)} and ${formatPatchOrder(through)}; nothing to shift.`, '--from');
94
+ }
95
+ for (let order = through + 1; order <= through + shift; order++) {
96
+ const occupant = occupantAt(manifest, order);
97
+ if (occupant !== undefined) {
98
+ throw new InvalidArgumentError(`--shift ${shift} would move the run ${formatPatchOrder(from)}-${formatPatchOrder(through)} ` +
99
+ `onto order ${formatPatchOrder(order)}, which ${occupant} holds. Extend the run with ` +
100
+ '--through, or shift by fewer orders.', '--shift');
101
+ }
102
+ }
103
+ const renameMap = new Map();
104
+ for (const patch of members) {
105
+ const newOrder = patch.order + shift;
106
+ renameMap.set(patch.filename, {
107
+ newOrder,
108
+ newFilename: rebuildFilenameForOrder(patch, newOrder),
109
+ });
110
+ }
111
+ const moveSlot = (slot) => inRun(slot.order) ? { ...slot, order: slot.order + shift } : slot;
112
+ const retired = (manifest.retired ?? []).map(moveSlot);
113
+ const reserved = (manifest.reserved ?? []).map(moveSlot);
114
+ // Every order below the shifted run's new start is free after the shift
115
+ // (the collision check above saw to the ones past the old run), so under
116
+ // `allowGaps: false` each one is held until an insert fills it.
117
+ const opened = [];
118
+ const category = categoryAt(manifest, from);
119
+ if (config.patchPolicy?.allowGaps === false && category !== undefined) {
120
+ const reservedAt = new Date().toISOString();
121
+ for (let order = from; order < from + shift; order++) {
122
+ opened.push(order);
123
+ reserved.push({
124
+ order,
125
+ category,
126
+ owner: SHIFT_RESERVATION_OWNER,
127
+ reservedAt,
128
+ reason: `opened by patch reorder --shift ${shift} --from ${from}`,
129
+ });
130
+ }
131
+ }
132
+ const byOrder = (a, b) => a.order - b.order;
133
+ return {
134
+ shift,
135
+ from,
136
+ through,
137
+ renameMap,
138
+ retired: retired.sort(byOrder),
139
+ reserved: reserved.sort(byOrder),
140
+ opened,
141
+ };
142
+ }
143
+ /** The manifest a shift leaves: renamed rows with the plan's slot lists. */
144
+ function projectShiftManifest(manifest, plan) {
145
+ const renamed = applyRenameMapToManifest(manifest, plan.renameMap);
146
+ return {
147
+ version: renamed.version,
148
+ patches: renamed.patches,
149
+ ...(plan.retired.length > 0 ? { retired: plan.retired } : {}),
150
+ ...(plan.reserved.length > 0 ? { reserved: plan.reserved } : {}),
151
+ };
152
+ }
153
+ /** True when two plans make the same renames and leave the same slot orders. */
154
+ function shiftPlansEqual(left, right) {
155
+ const orders = (slots) => slots.map((slot) => slot.order).join(',');
156
+ return (renameMapsEqual(left.renameMap, right.renameMap) &&
157
+ orders(left.retired) === orders(right.retired) &&
158
+ orders(left.reserved) === orders(right.reserved));
159
+ }
160
+ /** Cross-patch lint errors the shift itself introduces. */
161
+ async function projectShiftLint(patchesDir, config, renameMap) {
162
+ const baseCtx = await buildPatchQueueContext(patchesDir, config);
163
+ const isError = (issue) => issue.severity === 'error';
164
+ const regressions = computeProjectedLintRegressions(lintPatchQueue(baseCtx).filter(isError),
165
+ // The projection keeps the baseline's queue-wide lint settings, so the
166
+ // two sides compare symmetric rule sets.
167
+ lintPatchQueue({ ...baseCtx, ...projectReorder(baseCtx, renameMap) }).filter(isError));
168
+ return regressions.length === 0
169
+ ? null
170
+ : {
171
+ reason: `shift would introduce ${regressions.length} cross-patch lint error(s)`,
172
+ details: regressions.map(formatPatchLintIssue),
173
+ };
174
+ }
175
+ /** Lines describing a plan, for the dry run and the confirmation prompt. */
176
+ function describeShift(manifest, plan) {
177
+ const lines = [
178
+ `shift orders ${formatPatchOrder(plan.from)}-${formatPatchOrder(plan.through)} up by ${plan.shift}`,
179
+ `${plan.renameMap.size} patch(es) would be renamed:`,
180
+ ...getSortedRenameEntries(plan.renameMap).map(([from, entry]) => ` ${from} → ${entry.newFilename} (order ${entry.newOrder})`),
181
+ ];
182
+ const inRun = (order) => order >= plan.from && order <= plan.through;
183
+ for (const slot of manifest.retired ?? []) {
184
+ if (inRun(slot.order)) {
185
+ lines.push(`retired slot ${formatPatchOrder(slot.order)} (${slot.filename}) moves to ${formatPatchOrder(slot.order + plan.shift)}`);
186
+ }
187
+ }
188
+ for (const slot of manifest.reserved ?? []) {
189
+ if (inRun(slot.order)) {
190
+ lines.push(`reservation ${formatPatchOrder(slot.order)} (held by ${slot.owner}) moves to ` +
191
+ `${formatPatchOrder(slot.order + plan.shift)}; tell its holder the new order`);
192
+ }
193
+ }
194
+ if (plan.opened.length > 0) {
195
+ lines.push(`opened orders ${plan.opened.map((order) => formatPatchOrder(order)).join(', ')} are ` +
196
+ 'reserved (patchPolicy.allowGaps: false); "fireforge export --order <n>" or ' +
197
+ '"patch split --order <n>" fills them, "patch reserve <n> --release" drops one');
198
+ }
199
+ return lines;
200
+ }
201
+ /**
202
+ * Commits a confirmed shift under the patch-directory lock: re-plans
203
+ * against the current manifest (refusing a changed plan), renumbers, then
204
+ * writes the plan's slot lists. A failed slot write inverts the renumber.
205
+ */
206
+ async function commitShift(patchesDir, config, plan, options) {
207
+ await withPatchDirectoryLock(patchesDir, async () => {
208
+ const current = await loadPatchesManifest(patchesDir);
209
+ if (!current)
210
+ throw new GeneralError('Manifest disappeared while waiting for lock.');
211
+ const currentPlan = planShift(current, config, {
212
+ shift: plan.shift,
213
+ from: plan.from,
214
+ through: plan.through,
215
+ });
216
+ if (!shiftPlansEqual(plan, currentPlan)) {
217
+ throw new GeneralError('Patch queue changed while waiting for confirmation. Re-run reorder --shift to recompute the plan.');
218
+ }
219
+ enforcePatchPolicy({
220
+ config,
221
+ manifest: projectShiftManifest(current, plan),
222
+ command: 'patch reorder --shift',
223
+ forceUnsafe: options.forceUnsafe === true,
224
+ });
225
+ await renumberPatchesInManifest(patchesDir, plan.renameMap);
226
+ try {
227
+ const renumbered = await loadPatchesManifestForWrite(patchesDir);
228
+ if (!renumbered)
229
+ throw new GeneralError('Manifest disappeared during reorder --shift.');
230
+ delete renumbered.retired;
231
+ delete renumbered.reserved;
232
+ await savePatchesManifest(patchesDir, {
233
+ ...renumbered,
234
+ ...(plan.retired.length > 0 ? { retired: plan.retired } : {}),
235
+ ...(plan.reserved.length > 0 ? { reserved: plan.reserved } : {}),
236
+ });
237
+ }
238
+ catch (error) {
239
+ const inverse = new Map([...plan.renameMap].map(([oldFilename, entry]) => [
240
+ entry.newFilename,
241
+ { newFilename: oldFilename, newOrder: entry.newOrder - plan.shift },
242
+ ]));
243
+ try {
244
+ await renumberPatchesInManifest(patchesDir, inverse);
245
+ await savePatchesManifest(patchesDir, current);
246
+ }
247
+ catch (rollbackError) {
248
+ warn(`Rollback warning: could not invert the shift: ${toError(rollbackError).message}`);
249
+ }
250
+ throw error;
251
+ }
252
+ await appendHistoryBestEffort(patchesDir, {
253
+ operation: 'patch-reorder-shift',
254
+ args: {
255
+ shift: plan.shift,
256
+ from: plan.from,
257
+ through: plan.through,
258
+ renames: getSortedRenameEntries(plan.renameMap).map(([from, entry]) => ({
259
+ from,
260
+ to: entry.newFilename,
261
+ order: entry.newOrder,
262
+ })),
263
+ ...(plan.opened.length > 0 ? { reservedOpened: plan.opened } : {}),
264
+ },
265
+ ...(options.yes === true ? { yes: true } : {}),
266
+ ...(options.forceUnsafe === true ? { unsafeOverride: true } : {}),
267
+ result: 'ok',
268
+ }, 'patch reorder --shift committed');
269
+ }, { waitLockSeconds: resolveWaitLockSeconds(options.waitLock), command: 'patch reorder' });
270
+ }
271
+ /**
272
+ * Runs `patch reorder --shift <n> --from <order> [--through <order>]`.
273
+ *
274
+ * @param projectRoot - Project root directory
275
+ * @param options - Reorder options with `shift` and `from` set
276
+ */
277
+ export async function patchReorderShiftCommand(projectRoot, options) {
278
+ intro(options.dryRun ? 'FireForge patch reorder --shift (dry run)' : 'FireForge patch reorder --shift');
279
+ const config = await loadConfig(projectRoot);
280
+ const { paths, manifest } = await requirePatchQueue(projectRoot);
281
+ const plan = planShift(manifest, config, options);
282
+ enforcePatchPolicy({
283
+ config,
284
+ manifest: projectShiftManifest(manifest, plan),
285
+ command: 'patch reorder --shift',
286
+ forceUnsafe: options.forceUnsafe === true,
287
+ });
288
+ const conflicts = await projectShiftLint(paths.patches, config, plan.renameMap);
289
+ const decision = await confirmDestructive({
290
+ operation: 'patch-reorder-shift',
291
+ title: `Shift ${plan.renameMap.size} patch(es) from order ${formatPatchOrder(plan.from)} up by ${plan.shift}`,
292
+ summary: describeShift(manifest, plan),
293
+ yes: options.yes === true,
294
+ dryRun: options.dryRun === true,
295
+ unsafeOverride: options.forceUnsafe === true,
296
+ conflicts,
297
+ });
298
+ if (!proceedAfterDecision(decision, 'Reorder cancelled'))
299
+ return;
300
+ await commitShift(paths.patches, config, plan, options);
301
+ if (options.mapOut !== undefined) {
302
+ const written = await writeRenameMapFile(options.mapOut, plan.renameMap);
303
+ info(`Wrote the old-to-new filename map (${plan.renameMap.size} rename(s)) to ${written}`);
304
+ }
305
+ info(`Shifted ${plan.renameMap.size} patch(es) up by ${plan.shift}.`);
306
+ outro('Reorder complete');
307
+ }
@@ -6,31 +6,25 @@
6
6
  * renumbered to make room. Pre-flights the projected order through
7
7
  * `lintPatchQueue` so reorders that would introduce a forward-import fail
8
8
  * before any bytes move.
9
+ *
10
+ * `fireforge patch reorder --shift <n> --from <order> [--through <order>]`
11
+ * renumbers a whole run instead (see reorder-shift.ts). Every mode takes
12
+ * `--map-out <file>` for the committed old-to-new filename map.
9
13
  */
10
14
  import { Command } from 'commander';
11
- import { type PatchQueueContext } from '../../core/patch-lint.js';
12
- import { type PatchRenameEntry } from '../../core/patch-manifest.js';
13
15
  import type { CommandContext } from '../../types/cli.js';
14
- import type { PatchMetadata, PatchReorderOptions } from '../../types/commands/index.js';
15
- /** Builds a new patch filename by replacing the numeric prefix with `newOrder`. */
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
- */
22
- export declare function projectReorder(base: PatchQueueContext, renameMap: Map<string, PatchRenameEntry>): PatchQueueContext;
16
+ import type { PatchReorderOptions } from '../../types/commands/index.js';
23
17
  /**
24
18
  * Runs the `patch reorder` command: computes a rename map moving the
25
19
  * target patch to the requested slot, projects the new order through
26
20
  * cross-patch lint, confirms, and then renames under the patch directory
27
- * lock.
21
+ * lock. With `--shift` it renumbers a run instead (see reorder-shift.ts).
28
22
  *
29
23
  * @param projectRoot - Project root directory
30
- * @param identifier - Patch filename or ordinal number to move
31
- * @param options - Command options (mutually exclusive --to/--before/--after)
24
+ * @param identifier - Patch filename or ordinal number to move (none with `--shift`)
25
+ * @param options - Command options (mutually exclusive --to/--before/--after/--shift)
32
26
  */
33
- export declare function patchReorderCommand(projectRoot: string, identifier: string, options?: PatchReorderOptions): Promise<void>;
27
+ export declare function patchReorderCommand(projectRoot: string, identifier: string | undefined, options?: PatchReorderOptions): Promise<void>;
34
28
  /**
35
29
  * Registers the `patch reorder` subcommand on the `patch` parent.
36
30
  *
@@ -7,6 +7,10 @@
7
7
  * renumbered to make room. Pre-flights the projected order through
8
8
  * `lintPatchQueue` so reorders that would introduce a forward-import fail
9
9
  * before any bytes move.
10
+ *
11
+ * `fireforge patch reorder --shift <n> --from <order> [--through <order>]`
12
+ * renumbers a whole run instead (see reorder-shift.ts). Every mode takes
13
+ * `--map-out <file>` for the committed old-to-new filename map.
10
14
  */
11
15
  import { Command, Option } from 'commander';
12
16
  import { loadConfig } from '../../core/config.js';
@@ -14,27 +18,17 @@ import { confirmDestructive, } from '../../core/destructive.js';
14
18
  import { appendHistoryBestEffort } from '../../core/history-log.js';
15
19
  import { buildPatchQueueContext, formatPatchLintIssue, lintPatchQueue, } from '../../core/patch-lint.js';
16
20
  import { withPatchDirectoryLock } from '../../core/patch-lock.js';
17
- import { loadPatchesManifest, renumberPatchesInManifest, resolvePatchIdentifier, rewriteStagedDependencyOwners, } from '../../core/patch-manifest.js';
21
+ import { loadPatchesManifest, renumberPatchesInManifest, resolvePatchIdentifier, } from '../../core/patch-manifest.js';
18
22
  import { applyRenameMapToManifest, enforcePatchPolicy } from '../../core/patch-policy.js';
19
23
  import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
20
24
  import { info, intro, outro } from '../../utils/logger.js';
21
25
  import { addWaitLockOption, commanderArgParser, pickDefined, resolveWaitLockSeconds, } from '../../utils/options.js';
22
26
  import { parsePositiveIntegerFlag } from '../../utils/validation.js';
23
27
  import { proceedAfterDecision } from '../destructive-decision.js';
24
- import { getSortedRenameEntries, renameMapsEqual } from '../patch-rename-map.js';
28
+ import { getSortedRenameEntries, renameMapsEqual, writeRenameMapFile, } from '../patch-rename-map.js';
25
29
  import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
26
- /** Zero-pads an ordinal number to the given width. */
27
- function padOrder(value, width) {
28
- return String(value).padStart(width, '0');
29
- }
30
- /** Builds a new patch filename by replacing the numeric prefix with `newOrder`. */
31
- export function rebuildFilenameForOrder(existing, newOrder) {
32
- const currentPrefixMatch = /^(\d+)-/.exec(existing.filename);
33
- const currentPrefix = currentPrefixMatch?.[1] ?? '001';
34
- const width = Math.max(3, currentPrefix.length, String(newOrder).length);
35
- const rest = existing.filename.replace(/^\d+-/, '');
36
- return `${padOrder(newOrder, width)}-${rest}`;
37
- }
30
+ import { projectReorder, rebuildFilenameForOrder } from './reorder-projection.js';
31
+ import { patchReorderShiftCommand } from './reorder-shift.js';
38
32
  /**
39
33
  * Computes a rename map that moves `target` to `destinationOrder` with a
40
34
  * minimal shift: only the contiguous run of patches whose current order
@@ -100,35 +94,6 @@ function computeRenameMap(manifestPatches, target, destinationOrder) {
100
94
  });
101
95
  return renames;
102
96
  }
103
- /**
104
- * Applies a rename map to a {@link PatchQueueContext} so cross-patch lint
105
- * can run against the projected state without touching disk. Exported for
106
- * `patch rename --order`, which projects the same single-row move shape.
107
- */
108
- export function projectReorder(base, renameMap) {
109
- const ownerLookup = (oldFilename) => renameMap.get(oldFilename)?.newFilename;
110
- const projectedEntries = base.entries.map((entry) => {
111
- // Project staged-dependency owner references through the rename map on
112
- // every entry. Owners point at other patches' filenames, so a
113
- // projection that skips non-renamed entries would lint against stale
114
- // owners and report false forward-import regressions.
115
- const metadata = entry.metadata
116
- ? rewriteStagedDependencyOwners(entry.metadata, ownerLookup)
117
- : entry.metadata;
118
- const rename = renameMap.get(entry.filename);
119
- if (!rename) {
120
- return metadata === entry.metadata ? entry : { ...entry, metadata };
121
- }
122
- return {
123
- ...entry,
124
- metadata,
125
- filename: rename.newFilename,
126
- order: rename.newOrder,
127
- };
128
- });
129
- projectedEntries.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
130
- return { entries: projectedEntries };
131
- }
132
97
  /**
133
98
  * Resolves `--to <N>`, `--before <anchor>`, and `--after <anchor>` into a
134
99
  * concrete destination order and, for the anchor variants, the anchor
@@ -186,7 +151,7 @@ function resolveDestination(target, manifestPatches, options) {
186
151
  */
187
152
  async function commitReorderPlan(commitOptions) {
188
153
  const { patchesDir, target, renameMap, anchorFilename, options, config, buildHistoryEntry } = commitOptions;
189
- await withPatchDirectoryLock(patchesDir, async () => {
154
+ return withPatchDirectoryLock(patchesDir, async () => {
190
155
  const currentManifest = await loadPatchesManifest(patchesDir);
191
156
  if (!currentManifest || currentManifest.patches.length === 0) {
192
157
  throw new GeneralError('Patch queue changed while waiting for confirmation. Re-run reorder.');
@@ -239,19 +204,45 @@ async function commitReorderPlan(commitOptions) {
239
204
  // succeeded and is not reversible, so surfacing it as a command
240
205
  // failure would mislead.
241
206
  await appendHistoryBestEffort(patchesDir, buildHistoryEntry(currentRenameMap), `patch reorder committed`);
207
+ return currentRenameMap;
242
208
  }, { waitLockSeconds: resolveWaitLockSeconds(options.waitLock), command: 'patch reorder' });
243
209
  }
210
+ /**
211
+ * Validates the `--shift` flag set and runs the shift, or returns false
212
+ * when no shift flag was given.
213
+ */
214
+ async function runShiftMode(projectRoot, identifier, options) {
215
+ const { shift, from } = options;
216
+ if (shift === undefined && from === undefined && options.through === undefined)
217
+ return false;
218
+ if (shift === undefined || from === undefined) {
219
+ throw new InvalidArgumentError('--shift <n> and --from <order> go together (--through <order> is optional).', shift === undefined ? '--shift' : '--from');
220
+ }
221
+ if (identifier !== undefined ||
222
+ options.to !== undefined ||
223
+ options.before !== undefined ||
224
+ options.after !== undefined) {
225
+ throw new InvalidArgumentError('--shift renumbers a run of orders and takes no patch name, --to, --before or --after.', '--shift');
226
+ }
227
+ await patchReorderShiftCommand(projectRoot, { ...options, shift, from });
228
+ return true;
229
+ }
244
230
  /**
245
231
  * Runs the `patch reorder` command: computes a rename map moving the
246
232
  * target patch to the requested slot, projects the new order through
247
233
  * cross-patch lint, confirms, and then renames under the patch directory
248
- * lock.
234
+ * lock. With `--shift` it renumbers a run instead (see reorder-shift.ts).
249
235
  *
250
236
  * @param projectRoot - Project root directory
251
- * @param identifier - Patch filename or ordinal number to move
252
- * @param options - Command options (mutually exclusive --to/--before/--after)
237
+ * @param identifier - Patch filename or ordinal number to move (none with `--shift`)
238
+ * @param options - Command options (mutually exclusive --to/--before/--after/--shift)
253
239
  */
254
240
  export async function patchReorderCommand(projectRoot, identifier, options = {}) {
241
+ if (await runShiftMode(projectRoot, identifier, options))
242
+ return;
243
+ if (identifier === undefined) {
244
+ throw new InvalidArgumentError('Name the patch to move, or renumber a run with --shift <n> --from <order>.', 'patch reorder');
245
+ }
255
246
  intro(options.dryRun ? 'FireForge patch reorder (dry run)' : 'FireForge patch reorder');
256
247
  const specifiedTargets = [
257
248
  options.to !== undefined,
@@ -338,7 +329,7 @@ export async function patchReorderCommand(projectRoot, identifier, options = {})
338
329
  result: 'ok',
339
330
  };
340
331
  };
341
- await commitReorderPlan({
332
+ const committed = await commitReorderPlan({
342
333
  patchesDir: paths.patches,
343
334
  target,
344
335
  renameMap,
@@ -347,6 +338,10 @@ export async function patchReorderCommand(projectRoot, identifier, options = {})
347
338
  config,
348
339
  buildHistoryEntry,
349
340
  });
341
+ if (options.mapOut !== undefined) {
342
+ const written = await writeRenameMapFile(options.mapOut, committed);
343
+ info(`Wrote the old-to-new filename map (${committed.size} rename(s)) to ${written}`);
344
+ }
350
345
  info(`Reordered ${renameMap.size} patch(es).`);
351
346
  outro('Reorder complete');
352
347
  }
@@ -359,11 +354,18 @@ export async function patchReorderCommand(projectRoot, identifier, options = {})
359
354
  export function registerPatchReorder(parent, context) {
360
355
  const { getProjectRoot, withErrorHandling } = context;
361
356
  const command = parent
362
- .command('reorder <name>')
363
- .description('Move a patch to a different position in the queue (destructive)')
357
+ .command('reorder [name]')
358
+ .description('Move a patch to a different position in the queue, or shift a run of patches up with ' +
359
+ '--shift <n> --from <order> (destructive)')
364
360
  .addOption(new Option('--to <order>', 'Destination ordinal').argParser(commanderArgParser((v) => parsePositiveIntegerFlag('--to', v))))
365
361
  .option('--before <anchor>', 'Place the patch immediately before <anchor>')
366
362
  .option('--after <anchor>', 'Place the patch immediately after <anchor>')
363
+ .addOption(new Option('--shift <n>', 'Renumber the run starting at --from up by <n> orders (no patch name); under ' +
364
+ 'patchPolicy.allowGaps: false the opened orders are reserved').argParser(commanderArgParser((v) => parsePositiveIntegerFlag('--shift', v))))
365
+ .addOption(new Option('--from <order>', 'First order of the run --shift moves').argParser(commanderArgParser((v) => parsePositiveIntegerFlag('--from', v))))
366
+ .addOption(new Option('--through <order>', 'Last order of the run --shift moves (default: the end of the run of consecutive ' +
367
+ "orders of --from's category)").argParser(commanderArgParser((v) => parsePositiveIntegerFlag('--through', v))))
368
+ .option('--map-out <file>', 'Write the committed old-to-new filename map as JSON')
367
369
  .option('--dry-run', 'Show what would happen without writing')
368
370
  .option('-y, --yes', 'Skip confirmation prompt (required for non-TTY)')
369
371
  .option('--force-unsafe', 'Bypass the refusal when the projected order introduces a lint error');
@@ -45,7 +45,33 @@ export interface SplitPlan {
45
45
  * unused. Populated by {@link runProjectedSplitLint}.
46
46
  */
47
47
  stagedMaintenance: SplitStagedMaintenance;
48
+ /**
49
+ * The command running the plan, named in the `reason` of every
50
+ * declaration it writes. Absent: `patch split`.
51
+ */
52
+ operation?: MoveOperation;
53
+ /**
54
+ * New engine files no patch owned that `move-files --scan-file` adopts
55
+ * into the new patch in the same transaction. Already part of
56
+ * `movedFiles`; listed apart only for the summary and the commit's
57
+ * still-unowned check.
58
+ */
59
+ adoptedFiles?: string[];
48
60
  }
61
+ /** The commands that move files between patches and declare the edges they open. */
62
+ export type MoveOperation = 'patch split' | 'patch move-files';
63
+ /**
64
+ * The `reason` a move records on a forward declaration it writes, so the
65
+ * declaration explains itself in `patches.json` and `staged-dependency
66
+ * --add --from` accepts it without a hand-written `--reason`.
67
+ *
68
+ * @param source - Patch the files left
69
+ * @param destination - Patch the files moved into
70
+ * @param operation - The command that moved them
71
+ */
72
+ export declare function moveDeclarationReason(source: string, destination: string, operation: MoveOperation): string;
73
+ /** The {@link moveDeclarationReason} of a split plan. */
74
+ export declare function splitDeclarationReason(plan: SplitPlan): string;
49
75
  /**
50
76
  * Refuses the split unless the source patch currently owns every requested
51
77
  * file. Splitting out a file the source does not own would produce a new patch
@@ -90,8 +116,10 @@ export declare function buildNewPatchMetadata(plan: SplitPlan, config: FireForge
90
116
  export declare function buildSplitSummary(plan: SplitPlan): string[];
91
117
  /**
92
118
  * Maps a declaration the split projection proposes back onto the queue as
93
- * it stands: renumbered patches take their current names, and an owner
94
- * that is the patch the split would create is dropped (it does not exist
95
- * until the split runs).
119
+ * it stands: renumbered patches take their current names. An owner that is
120
+ * the patch the split would create keeps the planned filename, so the
121
+ * report declares the owner the rerun creates (`staged-dependency --add`
122
+ * accepts an owner the queue does not hold yet, and the move then lands
123
+ * with the declaration already right).
96
124
  */
97
125
  export declare function splitDeclarationRenamer(plan: SplitPlan): DeclarationRenamer;