@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,91 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * `patch move-files --retire-empty`: fold every file of a patch into
4
+ * another and retire the emptied patch's slot in the same transaction.
5
+ *
6
+ * Without the flag `move-files` refuses to empty the source, and
7
+ * `patch retire` on an emptied patch refuses while another patch's
8
+ * declaration still names it as the owner. With the flag the move re-points
9
+ * those declarations to the destination first (after the fold the
10
+ * destination creates every file the source did), carries the source's own declarations over with its files, and then
11
+ * retires the source the way `patch retire` does: the row and `.patch` file
12
+ * leave, and the order stays spent as a `patches.json#retired` slot so
13
+ * `patchPolicy.allowGaps: false` sees no gap.
14
+ */
15
+ import { rewriteStagedDependencyOwners } from '../../core/patch-manifest.js';
16
+ /**
17
+ * Concatenates declaration lists without duplicates, dropping every entry
18
+ * owned by `destination` itself: after the fold the file it waits for
19
+ * arrives in the same patch, so it is no longer a forward edge.
20
+ */
21
+ function foldDeclarations(lists, destination) {
22
+ const keys = new Set();
23
+ const merged = [];
24
+ for (const entry of lists.flatMap((list) => list ?? [])) {
25
+ if (entry.owner === destination)
26
+ continue;
27
+ const key = JSON.stringify(entry);
28
+ if (keys.has(key))
29
+ continue;
30
+ keys.add(key);
31
+ merged.push(entry);
32
+ }
33
+ return merged;
34
+ }
35
+ /** The destination row carrying its own and the source's declarations. */
36
+ function foldStaged(destination, source) {
37
+ const own = destination.stagedDependencies;
38
+ const forwardImports = foldDeclarations([own?.forwardImports, source?.forwardImports], destination.filename);
39
+ const registrations = foldDeclarations([own?.registrations, source?.registrations], destination.filename);
40
+ const folded = { ...destination };
41
+ delete folded.stagedDependencies;
42
+ if (forwardImports.length === 0 && registrations.length === 0)
43
+ return folded;
44
+ folded.stagedDependencies = {
45
+ ...(forwardImports.length > 0 ? { forwardImports } : {}),
46
+ ...(registrations.length > 0 ? { registrations } : {}),
47
+ };
48
+ return folded;
49
+ }
50
+ /**
51
+ * Re-points every declaration whose owner is `source` to `destination` and
52
+ * carries the source's own declarations onto the destination. The source
53
+ * row is returned unchanged; the caller retires it.
54
+ *
55
+ * @param patches - Manifest rows
56
+ * @param source - Filename of the patch the fold empties
57
+ * @param destination - Filename of the patch that receives its files
58
+ */
59
+ export function foldSourceRows(patches, source, destination) {
60
+ const lookup = (owner) => owner === source ? destination : undefined;
61
+ const sourceRow = patches.find((patch) => patch.filename === source);
62
+ const sourceStaged = sourceRow
63
+ ? rewriteStagedDependencyOwners(sourceRow, lookup).stagedDependencies
64
+ : undefined;
65
+ const repointed = [];
66
+ const rows = patches.map((patch) => {
67
+ if (patch.filename === source)
68
+ return patch;
69
+ const rewritten = rewriteStagedDependencyOwners(patch, lookup);
70
+ if (rewritten !== patch)
71
+ repointed.push(patch.filename);
72
+ return patch.filename === destination ? foldStaged(rewritten, sourceStaged) : rewritten;
73
+ });
74
+ return { patches: rows, repointed };
75
+ }
76
+ /**
77
+ * The retired slot the emptied source leaves, shaped as `patch retire`
78
+ * writes it.
79
+ *
80
+ * @param source - The emptied source row
81
+ * @param destination - Filename of the patch that took its files
82
+ */
83
+ export function retiredSlotForFold(source, destination) {
84
+ return {
85
+ order: source.order,
86
+ category: source.category,
87
+ filename: source.filename,
88
+ retiredAt: new Date().toISOString(),
89
+ reason: `folded into ${destination} by patch move-files --retire-empty`,
90
+ };
91
+ }
@@ -3,6 +3,9 @@
3
3
  * re-export choreography needed to move file ownership between two
4
4
  * patches. With `--create --order <n>` the target patch is created and
5
5
  * the files move into it as one transaction (see move-files-create.ts).
6
+ * `--scan-file` adopts new, unowned engine files into the destination in
7
+ * the same transaction, and `--retire-empty` lets the move empty the
8
+ * source and retire its slot.
6
9
  */
7
10
  import { Command } from 'commander';
8
11
  import type { CommandContext } from '../../types/cli.js';
@@ -4,6 +4,9 @@
4
4
  * re-export choreography needed to move file ownership between two
5
5
  * patches. With `--create --order <n>` the target patch is created and
6
6
  * the files move into it as one transaction (see move-files-create.ts).
7
+ * `--scan-file` adopts new, unowned engine files into the destination in
8
+ * the same transaction, and `--retire-empty` lets the move empty the
9
+ * source and retire its slot.
7
10
  */
8
11
  import { relative } from 'node:path';
9
12
  import { Command } from 'commander';
@@ -15,6 +18,7 @@ import { pathExists } from '../../utils/fs.js';
15
18
  import { info, intro, note, warn } from '../../utils/logger.js';
16
19
  import { addWaitLockOption, commanderArgParser, pickDefined, stringListOption, } from '../../utils/options.js';
17
20
  import { parsePositiveIntegerFlag } from '../../utils/validation.js';
21
+ import { resolveAdoptedFiles } from './move-files-adopt.js';
18
22
  import { patchMoveFilesCreateCommand } from './move-files-create.js';
19
23
  import { runMoveFilesInto } from './move-files-into.js';
20
24
  function normalizeFileList(files) {
@@ -53,13 +57,25 @@ function assertTargetDoesNotAlreadyOwnFiles(target, files) {
53
57
  return;
54
58
  throw new InvalidArgumentError(`${target.filename} already owns ${duplicates.length} requested file(s):\n${formatFiles(duplicates)}`, 'patch move-files');
55
59
  }
56
- function computeFileMovePlan(source, target, files) {
60
+ function computeFileMovePlan(source, target, files, adoptedFiles) {
57
61
  const moved = new Set(files);
58
62
  return {
59
63
  sourceAfter: source.filesAffected.filter((file) => !moved.has(file)).sort(),
60
- targetAfter: [...new Set([...target.filesAffected, ...files])].sort(),
64
+ targetAfter: [...new Set([...target.filesAffected, ...files, ...adoptedFiles])].sort(),
61
65
  };
62
66
  }
67
+ /** Refuses a move that empties the source unless `--retire-empty` asks for it, and the flag on a move that does not. */
68
+ function assertEmptySourceIntent(source, sourceAfter, retireEmpty) {
69
+ if (sourceAfter.length === 0 && !retireEmpty) {
70
+ throw new InvalidArgumentError(`${source.filename} would have no filesAffected after this move. Pass --retire-empty to ` +
71
+ 'fold it into the destination in one transaction: the declarations it owns re-point to ' +
72
+ 'the destination and its order stays spent as a retired slot.', 'patch move-files');
73
+ }
74
+ if (sourceAfter.length > 0 && retireEmpty) {
75
+ throw new InvalidArgumentError(`--retire-empty applies only to a move that empties the source; ${source.filename} would ` +
76
+ `keep ${sourceAfter.length} file(s).`, '--retire-empty');
77
+ }
78
+ }
63
79
  /**
64
80
  * Builds and prints a safe, no-write file ownership move plan.
65
81
  *
@@ -75,6 +91,10 @@ export async function patchMoveFilesCommand(projectRoot, fromIdentifier, toIdent
75
91
  if (options.create !== true && options.order !== undefined) {
76
92
  throw new InvalidArgumentError('--order is only valid together with --create.', '--order');
77
93
  }
94
+ if (options.create === true && options.retireEmpty === true) {
95
+ throw new InvalidArgumentError('--retire-empty folds the source into an existing patch. To move a whole patch to a new ' +
96
+ 'name or order, use "fireforge patch rename" or "fireforge patch reorder".', '--retire-empty');
97
+ }
78
98
  if (options.create === true && options.order !== undefined) {
79
99
  await patchMoveFilesCreateCommand(projectRoot, fromIdentifier, toIdentifier, {
80
100
  ...options,
@@ -111,10 +131,10 @@ export async function patchMoveFilesCommand(projectRoot, fromIdentifier, toIdent
111
131
  }
112
132
  assertPatchHasFiles(source, files);
113
133
  assertTargetDoesNotAlreadyOwnFiles(target, files);
114
- const { sourceAfter, targetAfter } = computeFileMovePlan(source, target, files);
115
- if (sourceAfter.length === 0) {
116
- throw new InvalidArgumentError(`${source.filename} would have no filesAffected after this move. Delete or re-export that patch explicitly instead.`, 'patch move-files');
117
- }
134
+ const adoptedFiles = await resolveAdoptedFiles(options.scanFile, manifest.patches, paths.engine, files);
135
+ const { sourceAfter, targetAfter } = computeFileMovePlan(source, target, files, adoptedFiles);
136
+ const retireSource = options.retireEmpty === true;
137
+ assertEmptySourceIntent(source, sourceAfter, retireSource);
118
138
  for (const file of files) {
119
139
  if (!(await pathExists(`${paths.engine}/${file}`))) {
120
140
  warn(`engine/${file} does not exist in the current worktree; re-export may drop or fail it.`);
@@ -124,19 +144,23 @@ export async function patchMoveFilesCommand(projectRoot, fromIdentifier, toIdent
124
144
  info(`Moving ownership in ${relativePatchesDir}/patches.json.`);
125
145
  info(`Move ${files.length} file(s) from ${source.filename} to ${target.filename}:`);
126
146
  note(formatFiles(files), 'Files');
127
- note(formatFiles(sourceAfter), `${source.filename} files after move`);
147
+ if (adoptedFiles.length > 0)
148
+ note(formatFiles(adoptedFiles), 'Adopted new files');
149
+ note(retireSource ? '(none: retired)' : formatFiles(sourceAfter), `${source.filename} files after move`);
128
150
  note(formatFiles(targetAfter), `${target.filename} files after move`);
129
151
  // Manual fallback (e.g. when the worktree cannot carry both patches'
130
152
  // content right now): the equivalent re-export choreography, in
131
153
  // executable order. The shrink must land before the grow, or the
132
154
  // projected cross-patch lint refuses the pair with
133
155
  // duplicate-new-file-creation.
134
- const applySource = formatReExportCommand(source.filename, sourceAfter, [
135
- '--allow-shrink',
136
- '--yes',
137
- ]);
138
156
  const applyTarget = formatReExportCommand(target.filename, targetAfter, ['--yes']);
139
- note(`1. ${applySource}\n2. ${applyTarget}\n(the shrink must land first)`, 'Equivalent manual commands');
157
+ if (!retireSource) {
158
+ const applySource = formatReExportCommand(source.filename, sourceAfter, [
159
+ '--allow-shrink',
160
+ '--yes',
161
+ ]);
162
+ note(`1. ${applySource}\n2. ${applyTarget}\n(the shrink must land first)`, 'Equivalent manual commands');
163
+ }
140
164
  const config = await loadConfig(projectRoot);
141
165
  await runMoveFilesInto({
142
166
  enginePath: paths.engine,
@@ -146,6 +170,8 @@ export async function patchMoveFilesCommand(projectRoot, fromIdentifier, toIdent
146
170
  files,
147
171
  sourceAfter,
148
172
  targetAfter,
173
+ adoptedFiles,
174
+ retireSource,
149
175
  options,
150
176
  config,
151
177
  });
@@ -171,6 +197,8 @@ export function registerPatchMoveFiles(parent, context) {
171
197
  .option('-d, --description <desc>', 'Description for the created patch')
172
198
  .option('--dry-run', 'Show what would happen without writing')
173
199
  .option('-y, --yes', 'Skip confirmation prompt (required for non-TTY)')
200
+ .option('--scan-file <path>', 'New engine file no patch owns, adopted into <to> in the same transaction (repeatable)', ...stringListOption())
201
+ .option('--retire-empty', 'Allow the move to empty <from>: re-point the declarations it owns to <to> and retire its slot')
174
202
  .option('--force-unsafe', 'Bypass projected-lint refusals')
175
203
  .option('--skip-lint', 'Skip per-patch lint of the projected bodies')
176
204
  .option('--declarations-out <path>', 'Where a refusal writes its one --from-ready staged-declaration report (default: .fireforge/declarations/<timestamp>.json)');
@@ -187,6 +215,8 @@ export function registerPatchMoveFiles(parent, context) {
187
215
  forceUnsafe: options.forceUnsafe,
188
216
  skipLint: options.skipLint,
189
217
  declarationsOut: options.declarationsOut,
218
+ scanFile: options.scanFile,
219
+ retireEmpty: options.retireEmpty,
190
220
  waitLock: options.waitLock,
191
221
  }),
192
222
  });
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Names a patch-size tier change in a move's lint refusal.
3
+ *
4
+ * `large-patch-lines` judges a patch by its tier: a patch of test files
5
+ * only is held to the test tier, everything else to the general tier
6
+ * (with test lines split out under `addedCode`). A move that takes a
7
+ * source's last product file out turns it into a test-only patch, and the
8
+ * test tier then counts every line against its own ceiling, where a
9
+ * `share: "test"` waiver no longer applies. A plain size error would hide
10
+ * that reordering the moves (tests first) fixes it, so the wrapper below appends which tier the patch moves into, its count and the
11
+ * ceiling that count is held to.
12
+ */
13
+ import { type RunPatchLintOptions } from '../export-shared.js';
14
+ /** One side of a move, linted on its post-transaction body. */
15
+ export interface MoveSideLintOptions extends RunPatchLintOptions {
16
+ /** The patch's files before the move; absent for a patch the move creates. */
17
+ filesBefore?: readonly string[];
18
+ /** How the refusal names the patch (`source`, `destination`). */
19
+ role: string;
20
+ }
21
+ /**
22
+ * Runs {@link runPatchLint} on one side of a move and, when the lint
23
+ * refuses, appends {@link describeMoveTierChange}'s line to the refusal.
24
+ *
25
+ * @param options - The side's lint options, its files before the move and its role
26
+ */
27
+ export declare function runMoveSideLint(options: MoveSideLintOptions): Promise<void>;
@@ -0,0 +1,66 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Names a patch-size tier change in a move's lint refusal.
4
+ *
5
+ * `large-patch-lines` judges a patch by its tier: a patch of test files
6
+ * only is held to the test tier, everything else to the general tier
7
+ * (with test lines split out under `addedCode`). A move that takes a
8
+ * source's last product file out turns it into a test-only patch, and the
9
+ * test tier then counts every line against its own ceiling, where a
10
+ * `share: "test"` waiver no longer applies. A plain size error would hide
11
+ * that reordering the moves (tests first) fixes it, so the wrapper below appends which tier the patch moves into, its count and the
12
+ * ceiling that count is held to.
13
+ */
14
+ import { getPatchSizeThresholds, measurePatchSize, resolvePatchSizeTier, } from '../../core/patch-lint-size.js';
15
+ import { GeneralError } from '../../errors/base.js';
16
+ import { runPatchLint } from '../export-shared.js';
17
+ /**
18
+ * Describes a tier change that leaves the patch above the new tier's
19
+ * `large-patch-lines` error ceiling, or returns undefined when the tier
20
+ * does not change, the rule is waived, or the count fits.
21
+ *
22
+ * @param options - The side's lint options and its files before the move
23
+ */
24
+ async function describeMoveTierChange(options) {
25
+ const { filesBefore, filesAffected, patchTier, ignoreChecks, config } = options;
26
+ if (filesBefore === undefined || ignoreChecks?.has('large-patch-lines') === true) {
27
+ return undefined;
28
+ }
29
+ const before = resolvePatchSizeTier(filesBefore, patchTier).tier;
30
+ const after = resolvePatchSizeTier(filesAffected, patchTier).tier;
31
+ if (before === after)
32
+ return undefined;
33
+ const measurement = await measurePatchSize(options.engineDir, options.diffContent, filesAffected, config);
34
+ // Under `addedCode` a general-tier patch holds only its non-test lines to
35
+ // the general ceiling (the test lines meet the test tier's).
36
+ const count = measurement.metric === 'addedCode' && after === 'general'
37
+ ? measurement.lines - measurement.testLines
38
+ : measurement.lines;
39
+ const limit = getPatchSizeThresholds(after, config).lines.error;
40
+ if (count <= limit)
41
+ return undefined;
42
+ const advice = after === 'test'
43
+ ? 'Move its test files out before its last product file (or in the same move), ' +
44
+ 'so it never stands as a test-only patch above that ceiling.'
45
+ : 'Order the moves so it does not change tier while above that ceiling.';
46
+ return `Tier change: the ${options.role} becomes ${after} tier (it was ${before} tier): ${count} lines against ${limit}. ${advice}`;
47
+ }
48
+ /**
49
+ * Runs {@link runPatchLint} on one side of a move and, when the lint
50
+ * refuses, appends {@link describeMoveTierChange}'s line to the refusal.
51
+ *
52
+ * @param options - The side's lint options, its files before the move and its role
53
+ */
54
+ export async function runMoveSideLint(options) {
55
+ try {
56
+ await runPatchLint(options);
57
+ }
58
+ catch (error) {
59
+ if (!(error instanceof GeneralError))
60
+ throw error;
61
+ const note = await describeMoveTierChange(options);
62
+ if (note === undefined)
63
+ throw error;
64
+ throw new GeneralError(`${error.message}\n${note}`);
65
+ }
66
+ }
@@ -35,7 +35,7 @@ import { addWaitLockOption, commanderArgParser, pickDefined, resolveWaitLockSeco
35
35
  import { parsePositiveIntegerFlag } from '../../utils/validation.js';
36
36
  import { proceedAfterDecision } from '../destructive-decision.js';
37
37
  import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
38
- import { projectReorder } from './reorder.js';
38
+ import { projectReorder } from './reorder-projection.js';
39
39
  /**
40
40
  * Pulls the ordinal-string + category prefix out of a patch filename so
41
41
  * the rename keeps the existing ordinal padding verbatim. Returning the
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Rename-map helpers shared by `patch reorder` (single-patch moves and
3
+ * `--shift`), `patch compact` and `patch rename --order`: the filename a
4
+ * patch takes at a new order, and the queue context projected through a
5
+ * rename map for cross-patch lint.
6
+ */
7
+ import type { PatchQueueContext } from '../../core/patch-lint.js';
8
+ import { type PatchRenameEntry } from '../../core/patch-manifest.js';
9
+ import type { PatchMetadata } from '../../types/commands/index.js';
10
+ /** Builds a new patch filename by replacing the numeric prefix with `newOrder`. */
11
+ export declare function rebuildFilenameForOrder(existing: PatchMetadata, newOrder: number): string;
12
+ /**
13
+ * Applies a rename map to a {@link PatchQueueContext} so cross-patch lint
14
+ * can run against the projected state without touching disk. Exported for
15
+ * `patch rename --order`, which projects the same single-row move shape.
16
+ */
17
+ export declare function projectReorder(base: PatchQueueContext, renameMap: Map<string, PatchRenameEntry>): PatchQueueContext;
@@ -0,0 +1,49 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Rename-map helpers shared by `patch reorder` (single-patch moves and
4
+ * `--shift`), `patch compact` and `patch rename --order`: the filename a
5
+ * patch takes at a new order, and the queue context projected through a
6
+ * rename map for cross-patch lint.
7
+ */
8
+ import { rewriteStagedDependencyOwners } from '../../core/patch-manifest.js';
9
+ /** Zero-pads an ordinal number to the given width. */
10
+ function padOrder(value, width) {
11
+ return String(value).padStart(width, '0');
12
+ }
13
+ /** Builds a new patch filename by replacing the numeric prefix with `newOrder`. */
14
+ export function rebuildFilenameForOrder(existing, newOrder) {
15
+ const currentPrefixMatch = /^(\d+)-/.exec(existing.filename);
16
+ const currentPrefix = currentPrefixMatch?.[1] ?? '001';
17
+ const width = Math.max(3, currentPrefix.length, String(newOrder).length);
18
+ const rest = existing.filename.replace(/^\d+-/, '');
19
+ return `${padOrder(newOrder, width)}-${rest}`;
20
+ }
21
+ /**
22
+ * Applies a rename map to a {@link PatchQueueContext} so cross-patch lint
23
+ * can run against the projected state without touching disk. Exported for
24
+ * `patch rename --order`, which projects the same single-row move shape.
25
+ */
26
+ export function projectReorder(base, renameMap) {
27
+ const ownerLookup = (oldFilename) => renameMap.get(oldFilename)?.newFilename;
28
+ const projectedEntries = base.entries.map((entry) => {
29
+ // Project staged-dependency owner references through the rename map on
30
+ // every entry. Owners point at other patches' filenames, so a
31
+ // projection that skips non-renamed entries would lint against stale
32
+ // owners and report false forward-import regressions.
33
+ const metadata = entry.metadata
34
+ ? rewriteStagedDependencyOwners(entry.metadata, ownerLookup)
35
+ : entry.metadata;
36
+ const rename = renameMap.get(entry.filename);
37
+ if (!rename) {
38
+ return metadata === entry.metadata ? entry : { ...entry, metadata };
39
+ }
40
+ return {
41
+ ...entry,
42
+ metadata,
43
+ filename: rename.newFilename,
44
+ order: rename.newOrder,
45
+ };
46
+ });
47
+ projectedEntries.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
48
+ return { entries: projectedEntries };
49
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * `fireforge patch reorder --shift <n> --from <order> [--through <order>]`
3
+ * renumbers a run of patches up by `n` in one gap-checked transaction, so
4
+ * an insert has room.
5
+ *
6
+ * Opening room for four inserts after 103 meant moving 104 to 128 up by
7
+ * four. `patch rename --order` moves one patch, and under
8
+ * `patchPolicy.allowGaps: false` every single step refused on the gap it
9
+ * left, so the only way through was one `patch split --after` per insert.
10
+ * The shift moves the whole run at once (the retired and reserved slots
11
+ * inside it move with it), and under `allowGaps: false` records the orders
12
+ * it opens as `patches.json#reserved` slots, so the queue stays valid and
13
+ * the inserts that follow fill them.
14
+ *
15
+ * Same contract as `patch compact`: a projected policy check, a projected
16
+ * cross-patch lint (reporting only what the shift itself adds), a
17
+ * confirmation, and a commit under the patch-directory lock that refuses
18
+ * if the plan changed while it waited.
19
+ */
20
+ import type { PatchReorderOptions } from '../../types/commands/index.js';
21
+ /**
22
+ * Runs `patch reorder --shift <n> --from <order> [--through <order>]`.
23
+ *
24
+ * @param projectRoot - Project root directory
25
+ * @param options - Reorder options with `shift` and `from` set
26
+ */
27
+ export declare function patchReorderShiftCommand(projectRoot: string, options: PatchReorderOptions & {
28
+ shift: number;
29
+ from: number;
30
+ }): Promise<void>;