@hominis/fireforge 0.52.0 → 0.54.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 (248) hide show
  1. package/CHANGELOG.md +45 -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-remove.d.ts +26 -4
  54. package/dist/src/commands/patch/staged-dependency-remove.js +60 -23
  55. package/dist/src/commands/patch/staged-dependency.js +35 -24
  56. package/dist/src/commands/patch/tier.js +2 -2
  57. package/dist/src/commands/patch-rename-map.d.ts +14 -3
  58. package/dist/src/commands/patch-rename-map.js +21 -3
  59. package/dist/src/commands/re-export-adjacent.d.ts +4 -5
  60. package/dist/src/commands/re-export-lint.d.ts +4 -3
  61. package/dist/src/commands/re-export-lint.js +4 -3
  62. package/dist/src/commands/re-export-options.js +8 -2
  63. package/dist/src/commands/re-export-preflight.js +1 -2
  64. package/dist/src/commands/re-export-purity-scope.d.ts +1 -1
  65. package/dist/src/commands/re-export-purity-scope.js +13 -7
  66. package/dist/src/commands/re-export-retired-declarations.d.ts +22 -0
  67. package/dist/src/commands/re-export-retired-declarations.js +113 -0
  68. package/dist/src/commands/re-export-single.js +2 -2
  69. package/dist/src/commands/re-export-stale-registrations.d.ts +27 -6
  70. package/dist/src/commands/re-export-stale-registrations.js +65 -21
  71. package/dist/src/commands/re-export.js +4 -0
  72. package/dist/src/commands/rebase/continue.js +3 -3
  73. package/dist/src/commands/register.js +5 -4
  74. package/dist/src/commands/run.js +11 -4
  75. package/dist/src/commands/status.js +1 -1
  76. package/dist/src/commands/test-build-furnace.d.ts +1 -2
  77. package/dist/src/commands/test-build-furnace.js +1 -2
  78. package/dist/src/commands/test-build-phase.d.ts +8 -0
  79. package/dist/src/commands/test-build-phase.js +42 -7
  80. package/dist/src/commands/test-control-restore.d.ts +40 -0
  81. package/dist/src/commands/test-control-restore.js +86 -0
  82. package/dist/src/commands/test-dispatch.js +68 -7
  83. package/dist/src/commands/test-multi-verdict.js +1 -0
  84. package/dist/src/commands/test-orchestrate.d.ts +6 -0
  85. package/dist/src/commands/test-orchestrate.js +32 -1
  86. package/dist/src/commands/test-perf-scope.d.ts +1 -1
  87. package/dist/src/commands/test-perf-scope.js +1 -1
  88. package/dist/src/commands/test-prelock.d.ts +18 -8
  89. package/dist/src/commands/test-prelock.js +42 -14
  90. package/dist/src/commands/test-register.js +4 -3
  91. package/dist/src/commands/test-results.d.ts +10 -1
  92. package/dist/src/commands/test-results.js +20 -3
  93. package/dist/src/commands/test-run.d.ts +6 -0
  94. package/dist/src/commands/test-run.js +23 -4
  95. package/dist/src/commands/test-shards.d.ts +7 -3
  96. package/dist/src/commands/test-shards.js +24 -7
  97. package/dist/src/commands/test-stale-gate.d.ts +6 -1
  98. package/dist/src/commands/test-stale-gate.js +40 -2
  99. package/dist/src/commands/test-verdict.d.ts +5 -3
  100. package/dist/src/commands/test.js +14 -2
  101. package/dist/src/commands/token.js +2 -2
  102. package/dist/src/commands/verify.js +1 -1
  103. package/dist/src/commands/watch.js +1 -1
  104. package/dist/src/commands/wire.js +1 -1
  105. package/dist/src/core/build-audit-resolve.d.ts +4 -5
  106. package/dist/src/core/build-audit-resolve.js +4 -5
  107. package/dist/src/core/build-audit-skip.d.ts +2 -2
  108. package/dist/src/core/build-audit.d.ts +3 -4
  109. package/dist/src/core/build-baseline-types.d.ts +24 -3
  110. package/dist/src/core/build-baseline-validate.d.ts +30 -1
  111. package/dist/src/core/build-baseline-validate.js +127 -35
  112. package/dist/src/core/build-baseline.d.ts +8 -2
  113. package/dist/src/core/build-baseline.js +25 -16
  114. package/dist/src/core/build-jar-escalation.d.ts +4 -4
  115. package/dist/src/core/build-jar-escalation.js +4 -4
  116. package/dist/src/core/build-prepare.js +4 -4
  117. package/dist/src/core/config-validate.js +4 -4
  118. package/dist/src/core/coverage-extend.d.ts +5 -3
  119. package/dist/src/core/coverage-extend.js +8 -3
  120. package/dist/src/core/coverage-records.d.ts +4 -5
  121. package/dist/src/core/coverage-records.js +4 -5
  122. package/dist/src/core/destructive.d.ts +2 -2
  123. package/dist/src/core/destructive.js +2 -2
  124. package/dist/src/core/discard-baseline.d.ts +2 -2
  125. package/dist/src/core/discard-baseline.js +3 -3
  126. package/dist/src/core/engine-session-lock.d.ts +1 -1
  127. package/dist/src/core/engine-session-lock.js +1 -1
  128. package/dist/src/core/file-lock-owner.d.ts +5 -5
  129. package/dist/src/core/file-lock-owner.js +5 -5
  130. package/dist/src/core/file-lock-queue.js +6 -8
  131. package/dist/src/core/file-lock-shared.d.ts +4 -4
  132. package/dist/src/core/file-lock-shared.js +4 -4
  133. package/dist/src/core/file-lock.js +7 -10
  134. package/dist/src/core/firefox.js +2 -2
  135. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +1 -1
  136. package/dist/src/core/furnace-apply-overwrite-warn.js +1 -1
  137. package/dist/src/core/furnace-apply.js +3 -3
  138. package/dist/src/core/furnace-operation.js +5 -5
  139. package/dist/src/core/furnace-registration-jar-inplace.js +5 -6
  140. package/dist/src/core/furnace-registration-rename.d.ts +5 -5
  141. package/dist/src/core/furnace-registration-rename.js +5 -5
  142. package/dist/src/core/furnace-registration-validate.d.ts +2 -1
  143. package/dist/src/core/furnace-registration-validate.js +2 -1
  144. package/dist/src/core/furnace-registration.js +2 -2
  145. package/dist/src/core/git-base.js +4 -3
  146. package/dist/src/core/git-diff.js +1 -1
  147. package/dist/src/core/git.js +1 -1
  148. package/dist/src/core/harness-orphans.d.ts +6 -6
  149. package/dist/src/core/harness-orphans.js +6 -6
  150. package/dist/src/core/harness-port-reservation.d.ts +60 -0
  151. package/dist/src/core/harness-port-reservation.js +122 -0
  152. package/dist/src/core/history-log-file.d.ts +9 -9
  153. package/dist/src/core/history-log-file.js +15 -15
  154. package/dist/src/core/history-log.d.ts +3 -3
  155. package/dist/src/core/history-log.js +3 -3
  156. package/dist/src/core/launchable-build.d.ts +39 -0
  157. package/dist/src/core/launchable-build.js +85 -0
  158. package/dist/src/core/marionette-port.js +1 -3
  159. package/dist/src/core/moz-manifest-rules.js +3 -2
  160. package/dist/src/core/negative-control-compiled.d.ts +13 -3
  161. package/dist/src/core/negative-control-compiled.js +27 -5
  162. package/dist/src/core/negative-control-diff.js +31 -6
  163. package/dist/src/core/parser-fallback.d.ts +5 -5
  164. package/dist/src/core/parser-fallback.js +5 -5
  165. package/dist/src/core/patch-identifier-suggest.d.ts +6 -0
  166. package/dist/src/core/patch-identifier-suggest.js +11 -4
  167. package/dist/src/core/patch-lint-checkjs.d.ts +1 -1
  168. package/dist/src/core/patch-lint-checkjs.js +1 -1
  169. package/dist/src/core/patch-lint-css.js +2 -2
  170. package/dist/src/core/patch-lint-file-size.d.ts +5 -7
  171. package/dist/src/core/patch-lint-file-size.js +5 -7
  172. package/dist/src/core/patch-lint-ignore.d.ts +3 -3
  173. package/dist/src/core/patch-lint-ignore.js +3 -3
  174. package/dist/src/core/patch-lint-prettier.d.ts +4 -6
  175. package/dist/src/core/patch-lint-prettier.js +4 -6
  176. package/dist/src/core/patch-lint-projection.d.ts +1 -1
  177. package/dist/src/core/patch-lint-projection.js +1 -1
  178. package/dist/src/core/patch-lint-queue-types.d.ts +2 -2
  179. package/dist/src/core/patch-lint-queue-types.js +2 -2
  180. package/dist/src/core/patch-manifest-consistency.d.ts +2 -2
  181. package/dist/src/core/patch-manifest-consistency.js +2 -2
  182. package/dist/src/core/patch-manifest-io.d.ts +5 -1
  183. package/dist/src/core/patch-manifest-io.js +12 -10
  184. package/dist/src/core/patch-manifest-key-order.d.ts +3 -4
  185. package/dist/src/core/patch-manifest-key-order.js +3 -4
  186. package/dist/src/core/patch-parse.d.ts +3 -3
  187. package/dist/src/core/patch-parse.js +3 -3
  188. package/dist/src/core/patch-policy.d.ts +1 -1
  189. package/dist/src/core/patch-policy.js +1 -1
  190. package/dist/src/core/patch-source-metadata.d.ts +1 -1
  191. package/dist/src/core/patch-source-metadata.js +1 -1
  192. package/dist/src/core/patch-subset.d.ts +5 -3
  193. package/dist/src/core/patch-subset.js +8 -7
  194. package/dist/src/core/patch-transform.d.ts +2 -2
  195. package/dist/src/core/patch-transform.js +2 -2
  196. package/dist/src/core/rebase-session.js +4 -6
  197. package/dist/src/core/register-xpcshell-test.d.ts +28 -7
  198. package/dist/src/core/register-xpcshell-test.js +76 -27
  199. package/dist/src/core/run-log.d.ts +8 -8
  200. package/dist/src/core/run-log.js +8 -8
  201. package/dist/src/core/status-classify.d.ts +1 -2
  202. package/dist/src/core/status-classify.js +1 -2
  203. package/dist/src/core/test-harness-crash.d.ts +5 -3
  204. package/dist/src/core/test-harness-crash.js +13 -14
  205. package/dist/src/core/test-harness-infra.d.ts +32 -0
  206. package/dist/src/core/test-harness-infra.js +69 -0
  207. package/dist/src/core/test-harness-signature.d.ts +6 -0
  208. package/dist/src/core/test-stale-check.d.ts +38 -0
  209. package/dist/src/core/test-stale-check.js +82 -1
  210. package/dist/src/core/test-stall-triage.js +5 -7
  211. package/dist/src/core/token-category.js +2 -2
  212. package/dist/src/core/token-inventory.js +3 -3
  213. package/dist/src/core/token-manager.d.ts +2 -2
  214. package/dist/src/core/token-manager.js +5 -7
  215. package/dist/src/core/token-variant.d.ts +6 -6
  216. package/dist/src/core/token-variant.js +7 -7
  217. package/dist/src/core/typecheck-shim.js +1 -1
  218. package/dist/src/core/wire-destroy.d.ts +1 -1
  219. package/dist/src/core/wire-destroy.js +1 -1
  220. package/dist/src/core/wire-dom-fragment.js +1 -1
  221. package/dist/src/core/wire-init.d.ts +1 -1
  222. package/dist/src/core/wire-init.js +1 -1
  223. package/dist/src/core/wire-subscript.d.ts +1 -1
  224. package/dist/src/core/wire-subscript.js +1 -1
  225. package/dist/src/core/wire-utils.d.ts +2 -2
  226. package/dist/src/core/wire-utils.js +3 -3
  227. package/dist/src/errors/base.d.ts +1 -1
  228. package/dist/src/errors/base.js +1 -1
  229. package/dist/src/errors/rebase.d.ts +1 -1
  230. package/dist/src/errors/rebase.js +1 -1
  231. package/dist/src/types/commands/options.d.ts +38 -3
  232. package/dist/src/types/config.d.ts +5 -7
  233. package/dist/src/utils/options.js +2 -2
  234. package/dist/src/utils/process.js +2 -2
  235. package/dist/src/utils/regex.d.ts +2 -2
  236. package/dist/src/utils/regex.js +3 -3
  237. package/dist/src/utils/sleep.d.ts +5 -6
  238. package/dist/src/utils/sleep.js +5 -6
  239. package/dist/src/utils/validation.js +2 -2
  240. package/docs/configuration.md +28 -23
  241. package/docs/exit-codes.md +8 -11
  242. package/docs/furnace.md +3 -3
  243. package/docs/lifecycle-invariants.md +36 -45
  244. package/docs/machine-output.md +41 -24
  245. package/docs/patch-workflow.md +140 -29
  246. package/docs/run-logs.md +8 -9
  247. package/docs/testing.md +114 -48
  248. package/package.json +1 -1
@@ -21,6 +21,22 @@ import { warn } from '../../utils/logger.js';
21
21
  import {} from '../export-flow.js';
22
22
  import { projectEntryBody } from './entry-projection.js';
23
23
  import { describeStagedMaintenance, findDeclarationsMadeUnused, keptRelocatedDeclarations, relocatedSourceDeclarations, rewriteSplitOwners, withoutRelocatedDeclarations, } from './split-staged.js';
24
+ /**
25
+ * The `reason` a move records on a forward declaration it writes, so the
26
+ * declaration explains itself in `patches.json` and `staged-dependency
27
+ * --add --from` accepts it without a hand-written `--reason`.
28
+ *
29
+ * @param source - Patch the files left
30
+ * @param destination - Patch the files moved into
31
+ * @param operation - The command that moved them
32
+ */
33
+ export function moveDeclarationReason(source, destination, operation) {
34
+ return `${source} → ${destination} by ${operation}`;
35
+ }
36
+ /** The {@link moveDeclarationReason} of a split plan. */
37
+ export function splitDeclarationReason(plan) {
38
+ return moveDeclarationReason(plan.source.filename, plan.placement.newFilename, plan.operation ?? 'patch split');
39
+ }
24
40
  /**
25
41
  * Refuses the split unless the source patch currently owns every requested
26
42
  * file. Splitting out a file the source does not own would produce a new patch
@@ -118,7 +134,7 @@ function buildProjectedSplitEntries(baseCtx, plan) {
118
134
  * them once declared. Auto-declaring keeps the two in lock-step and lets a
119
135
  * sound split read as sound.
120
136
  */
121
- function computeSplitStagedDependencyAdditions(projectedEntries, newFilename) {
137
+ function computeSplitStagedDependencyAdditions(projectedEntries, newFilename, reason) {
122
138
  const additions = new Map();
123
139
  for (const edge of collectForwardImportEdges({ entries: projectedEntries })) {
124
140
  if (edge.owner !== newFilename)
@@ -128,6 +144,7 @@ function computeSplitStagedDependencyAdditions(projectedEntries, newFilename) {
128
144
  specifier: edge.specifier,
129
145
  creates: edge.creates,
130
146
  owner: newFilename,
147
+ reason,
131
148
  };
132
149
  const list = additions.get(edge.entry) ?? [];
133
150
  const dup = list.some((d) => d.file === decl.file &&
@@ -179,15 +196,14 @@ export function runProjectedSplitLint(plan, baseCtx) {
179
196
  const projectedEntries = buildProjectedSplitEntries(baseCtx, plan);
180
197
  // Discover and auto-declare the forward edges this split introduces into
181
198
  // the new patch, then inject them before linting so they resolve.
182
- const stagedDependencyAdditions = computeSplitStagedDependencyAdditions(projectedEntries, plan.placement.newFilename);
199
+ const stagedDependencyAdditions = computeSplitStagedDependencyAdditions(projectedEntries, plan.placement.newFilename, splitDeclarationReason(plan));
183
200
  injectStagedDependencyAdditions(projectedEntries, stagedDependencyAdditions);
184
201
  const baselineAll = lintPatchQueue(baseCtx);
185
- const projectedAll = lintPatchQueue({
186
- entries: projectedEntries,
187
- // Keep the projection patch-policy-aware whenever the baseline is, so
188
- // computeProjectedLintRegressions compares symmetric rule sets.
189
- ...(baseCtx.patchPolicy ? { patchPolicy: baseCtx.patchPolicy } : {}),
190
- });
202
+ // Keep every queue-wide lint setting of the baseline (patch policy,
203
+ // forward-registration scope, token scales), so
204
+ // computeProjectedLintRegressions compares symmetric rule sets and a
205
+ // test-manifest suite entry is projected as the committed gate sees it.
206
+ const projectedAll = lintPatchQueue({ ...baseCtx, entries: projectedEntries });
191
207
  const baselineIssues = baselineAll.filter((i) => i.severity === 'error');
192
208
  const projectedIssues = projectedAll.filter((i) => i.severity === 'error');
193
209
  const oldNameByNew = new Map([...plan.placement.renameMap].map(([oldName, entry]) => [entry.newFilename, oldName]));
@@ -257,6 +273,12 @@ export function buildSplitSummary(plan) {
257
273
  const summary = [
258
274
  `split ${plan.source.filename}`,
259
275
  `moved files (${plan.movedFiles.length}): ${plan.movedFiles.join(', ')}`,
276
+ ...(plan.adoptedFiles?.length
277
+ ? [
278
+ `of which adopted from the worktree (no patch owned them) (${plan.adoptedFiles.length}): ` +
279
+ plan.adoptedFiles.join(', '),
280
+ ]
281
+ : []),
260
282
  `source keeps (${plan.remainingFiles.length}): ${plan.remainingFiles.join(', ')}`,
261
283
  `new patch: ${plan.placement.newFilename} (order ${plan.placement.insertionOrder})`,
262
284
  ];
@@ -274,9 +296,11 @@ export function buildSplitSummary(plan) {
274
296
  }
275
297
  /**
276
298
  * Maps a declaration the split projection proposes back onto the queue as
277
- * it stands: renumbered patches take their current names, and an owner
278
- * that is the patch the split would create is dropped (it does not exist
279
- * until the split runs).
299
+ * it stands: renumbered patches take their current names. An owner that is
300
+ * the patch the split would create keeps the planned filename, so the
301
+ * report declares the owner the rerun creates (`staged-dependency --add`
302
+ * accepts an owner the queue does not hold yet, and the move then lands
303
+ * with the declaration already right).
280
304
  */
281
305
  export function splitDeclarationRenamer(plan) {
282
306
  const oldNameByNew = new Map([...plan.placement.renameMap].map(([oldName, entry]) => [entry.newFilename, oldName]));
@@ -284,6 +308,6 @@ export function splitDeclarationRenamer(plan) {
284
308
  return ({ owner, ...declaration }) => ({
285
309
  ...declaration,
286
310
  patch: current(declaration.patch),
287
- ...(owner !== plan.placement.newFilename ? { owner: current(owner) } : {}),
311
+ owner: owner === plan.placement.newFilename ? owner : current(owner),
288
312
  });
289
313
  }
@@ -14,8 +14,8 @@
14
14
  * the imported file landed in one patch) is dropped
15
15
  * ({@link findDeclarationsMadeUnused}).
16
16
  *
17
- * Without this the move wrote the bodies and left the queue audit and the
18
- * per-patch lint to report `queue-dependency-creates-unowned` and
17
+ * Without this the move would write the bodies and leave the queue audit
18
+ * and the per-patch lint to report `queue-dependency-creates-unowned` and
19
19
  * `staged-dependency-unused` afterwards, one `--remove`/`--add` pair per
20
20
  * entry to repair by hand.
21
21
  */
@@ -15,8 +15,8 @@
15
15
  * the imported file landed in one patch) is dropped
16
16
  * ({@link findDeclarationsMadeUnused}).
17
17
  *
18
- * Without this the move wrote the bodies and left the queue audit and the
19
- * per-patch lint to report `queue-dependency-creates-unowned` and
18
+ * Without this the move would write the bodies and leave the queue audit
19
+ * and the per-patch lint to report `queue-dependency-creates-unowned` and
20
20
  * `staged-dependency-unused` afterwards, one `--remove`/`--add` pair per
21
21
  * entry to repair by hand.
22
22
  */
@@ -36,8 +36,10 @@ import { normalizePatchDisplayName, parsePositiveIntegerFlag } from '../../utils
36
36
  import { withDeclarationsReport } from '../declarations-report.js';
37
37
  import { proceedAfterDecision } from '../destructive-decision.js';
38
38
  import { placementPlansEqual, resolvePlacementPlan } from '../export-flow.js';
39
- import { runPatchLint } from '../export-shared.js';
40
- import { assertSourceOwnsFiles, buildNewPatchMetadata, buildSplitDiff, buildSplitSummary, mergeStagedForwardImports, projectSplitManifest, runProjectedSplitLint, splitDeclarationRenamer, } from './split-plan.js';
39
+ import { writeRenameMapFile } from '../patch-rename-map.js';
40
+ import { assertAdoptedStillUnowned } from './move-files-adopt.js';
41
+ import { runMoveSideLint } from './move-size-tier.js';
42
+ import { assertSourceOwnsFiles, buildNewPatchMetadata, buildSplitDiff, buildSplitSummary, mergeStagedForwardImports, projectSplitManifest, runProjectedSplitLint, splitDeclarationReason, splitDeclarationRenamer, } from './split-plan.js';
41
43
  import { describeStagedMaintenance, emptyStagedMaintenance, findOwnerRewriteHolders, rewriteSplitOwners, withoutDroppedDeclarations, withoutRelocatedDeclarations, } from './split-staged.js';
42
44
  /**
43
45
  * History payload for the declarations a split re-homed or dropped, omitted
@@ -79,6 +81,7 @@ export async function commitPatchSplit(patchesDir, plan, newMetadata, options, c
79
81
  if (!current || current.filesAffected.join('\n') !== plan.source.filesAffected.join('\n')) {
80
82
  throw new InvalidArgumentError('Patch queue changed while waiting for split confirmation. Re-run the command.', 'patch split');
81
83
  }
84
+ assertAdoptedStillUnowned(manifest.patches, plan.adoptedFiles, 'patch split');
82
85
  const currentPlacement = await resolvePlacementPlan(patchesDir, plan.placementOptions, plan.category, plan.name, config);
83
86
  if (!placementPlansEqual(currentPlacement, plan.placement)) {
84
87
  throw new InvalidArgumentError('Patch queue changed while waiting for split confirmation. Re-run the command.', 'patch split');
@@ -266,9 +269,11 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
266
269
  // instead of linting each projected body blind.
267
270
  const patchQueueCtx = await buildPatchQueueContext(paths.patches, config);
268
271
  const ignoreChecks = buildLintIgnoreChecks(source.lintIgnore);
269
- await runPatchLint({
272
+ await runMoveSideLint({
270
273
  engineDir: paths.engine,
271
274
  filesAffected: remainingFiles,
275
+ filesBefore: source.filesAffected,
276
+ role: 'source',
272
277
  diffContent: remainingDiff,
273
278
  config,
274
279
  skipLint: options.skipLint,
@@ -276,9 +281,10 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
276
281
  ignoreChecks,
277
282
  patchTier: source.tier,
278
283
  });
279
- await runPatchLint({
284
+ await runMoveSideLint({
280
285
  engineDir: paths.engine,
281
286
  filesAffected: movedFiles,
287
+ role: 'new patch',
282
288
  diffContent: movedDiff,
283
289
  config,
284
290
  skipLint: options.skipLint,
@@ -294,6 +300,7 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
294
300
  regressions: projected.regressions,
295
301
  out: options.declarationsOut,
296
302
  rename: splitDeclarationRenamer(plan),
303
+ reason: splitDeclarationReason(plan),
297
304
  });
298
305
  const newMetadata = buildNewPatchMetadata(plan, config);
299
306
  const decision = await confirmDestructive({
@@ -308,6 +315,10 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
308
315
  if (!proceedAfterDecision(decision, 'Split cancelled'))
309
316
  return;
310
317
  await commitPatchSplit(paths.patches, plan, newMetadata, options, config);
318
+ if (options.mapOut !== undefined) {
319
+ const written = await writeRenameMapFile(options.mapOut, plan.placement.renameMap);
320
+ info(`Wrote the old-to-new filename map (${plan.placement.renameMap.size} rename(s)) to ${written}`);
321
+ }
311
322
  success(`Split ${source.filename}: ${placement.newFilename} now owns ${plan.movedFiles.length} file(s)`);
312
323
  reportSplitStagedChanges(plan);
313
324
  outro('Split complete');
@@ -331,7 +342,8 @@ export function registerPatchSplit(parent, context) {
331
342
  .option('-y, --yes', 'Skip confirmation prompt (required for non-TTY)')
332
343
  .option('--force-unsafe', 'Bypass projected-lint refusals')
333
344
  .option('--skip-lint', 'Skip per-patch lint of the projected bodies')
334
- .option('--declarations-out <path>', 'Where a refusal writes its one --from-ready staged-declaration report (default: .fireforge/declarations/<timestamp>.json)');
345
+ .option('--declarations-out <path>', 'Where a refusal writes its one --from-ready staged-declaration report (default: .fireforge/declarations/<timestamp>.json)')
346
+ .option('--map-out <file>', 'Write the old-to-new filename map of the patches the split renumbers as JSON');
335
347
  addWaitLockOption(command).action(withErrorHandling(async (sourceId, options) => {
336
348
  await patchSplitCommand(getProjectRoot(), sourceId, {
337
349
  files: options.files,
@@ -347,6 +359,7 @@ export function registerPatchSplit(parent, context) {
347
359
  forceUnsafe: options.forceUnsafe,
348
360
  skipLint: options.skipLint,
349
361
  declarationsOut: options.declarationsOut,
362
+ mapOut: options.mapOut,
350
363
  waitLock: options.waitLock,
351
364
  }),
352
365
  });
@@ -29,8 +29,16 @@ export declare function registrationView(dependency: PatchStagedRegistration): S
29
29
  export declare function dependencyLabel(kind: StagedDependencyKind, view: StagedEntryView): string;
30
30
  /**
31
31
  * Applies one add/remove/clear to a declaration list: `add` appends unless
32
- * an entry with the same file, matcher, creates and owner exists.
32
+ * an entry with the same file, matcher, creates and owner exists. A
33
+ * matching entry whose `reason` differs from the addition's takes
34
+ * the new reason in place, keeping its position in the list; an addition
35
+ * without a reason leaves the existing one alone.
33
36
  */
34
37
  export declare function applyMode<T>(existing: readonly T[], mode: StagedDependencyMode, target: StagedEntryView | undefined, toView: (entry: T) => StagedEntryView, addition: T | undefined): T[];
38
+ /**
39
+ * Number of declarations in `after` whose key matches one in `before` but
40
+ * whose `reason` differs: the in-place reason updates of an `--add`.
41
+ */
42
+ export declare function countReasonUpdates(before: PatchStagedDependencies | undefined, after: PatchStagedDependencies | undefined): number;
35
43
  /** Number of declarations of both kinds. */
36
44
  export declare function countStagedEntries(staged: PatchStagedDependencies | undefined): number;
@@ -32,7 +32,10 @@ function matchesTarget(view, target) {
32
32
  }
33
33
  /**
34
34
  * Applies one add/remove/clear to a declaration list: `add` appends unless
35
- * an entry with the same file, matcher, creates and owner exists.
35
+ * an entry with the same file, matcher, creates and owner exists. A
36
+ * matching entry whose `reason` differs from the addition's takes
37
+ * the new reason in place, keeping its position in the list; an addition
38
+ * without a reason leaves the existing one alone.
36
39
  */
37
40
  export function applyMode(existing, mode, target, toView, addition) {
38
41
  if (mode === 'clear')
@@ -41,10 +44,35 @@ export function applyMode(existing, mode, target, toView, addition) {
41
44
  return [...existing];
42
45
  if (mode === 'remove')
43
46
  return existing.filter((entry) => !matchesTarget(toView(entry), target));
44
- const seen = new Set(existing.map((entry) => dependencyKey(toView(entry))));
45
- if (seen.has(dependencyKey(target)) || addition === undefined)
47
+ if (addition === undefined)
46
48
  return [...existing];
47
- return [...existing, addition];
49
+ const key = dependencyKey(target);
50
+ const index = existing.findIndex((entry) => dependencyKey(toView(entry)) === key);
51
+ if (index === -1)
52
+ return [...existing, addition];
53
+ const current = existing[index];
54
+ if (target.reason === undefined || toView(current).reason === target.reason) {
55
+ return [...existing];
56
+ }
57
+ const next = [...existing];
58
+ next[index] = addition;
59
+ return next;
60
+ }
61
+ /**
62
+ * Number of declarations in `after` whose key matches one in `before` but
63
+ * whose `reason` differs: the in-place reason updates of an `--add`.
64
+ */
65
+ export function countReasonUpdates(before, after) {
66
+ const updates = (left, right, toView) => {
67
+ const reasons = new Map((left ?? []).map((entry) => [dependencyKey(toView(entry)), toView(entry).reason]));
68
+ return (right ?? []).filter((entry) => {
69
+ const view = toView(entry);
70
+ const key = dependencyKey(view);
71
+ return reasons.has(key) && reasons.get(key) !== view.reason;
72
+ }).length;
73
+ };
74
+ return (updates(before?.forwardImports, after?.forwardImports, importView) +
75
+ updates(before?.registrations, after?.registrations, registrationView));
48
76
  }
49
77
  /** Number of declarations of both kinds. */
50
78
  export function countStagedEntries(staged) {
@@ -12,7 +12,7 @@ import { info, outro } from '../../utils/logger.js';
12
12
  import { resolveWaitLockSeconds } from '../../utils/options.js';
13
13
  import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
14
14
  import { readStagedDependencyBatch, stagedDependencyDelta, } from './staged-dependency-batch.js';
15
- import { applyMode, countStagedEntries, importView, registrationView, } from './staged-dependency-entries.js';
15
+ import { applyMode, countReasonUpdates, countStagedEntries, importView, registrationView, } from './staged-dependency-entries.js';
16
16
  import { validateStagedDependencyAdd } from './staged-dependency-validate.js';
17
17
  /** Flags that describe one declaration and so cannot combine with `--from`. */
18
18
  const SINGLE_DECLARATION_FLAGS = [
@@ -23,7 +23,10 @@ const SINGLE_DECLARATION_FLAGS = [
23
23
  ['--creates', 'creates'],
24
24
  ['--owner', 'owner'],
25
25
  ];
26
- /** Adds every `--from` declaration not already present, keyed like a single `--add`. */
26
+ /**
27
+ * Adds every `--from` declaration not already present, keyed like a single
28
+ * `--add`; a present one whose `reason` differs takes the new reason in place.
29
+ */
27
30
  function mergeBatch(staged, batch) {
28
31
  let forwardImports = [...(staged?.forwardImports ?? [])];
29
32
  for (const entry of batch.forwardImports) {
@@ -68,13 +71,16 @@ export async function runBatchAdd(projectRoot, identifier, mode, options) {
68
71
  return;
69
72
  }
70
73
  const existing = targetPatch.stagedDependencies;
71
- const projectedCount = countStagedEntries(mergeBatch(existing, batch));
72
- const describe = (beforeCount, afterCount) => `added ${afterCount - beforeCount} of ${supplied} staged declaration(s) from ${from}` +
73
- (supplied > afterCount - beforeCount
74
- ? ` (${supplied - (afterCount - beforeCount)} already present or repeated)`
75
- : '');
74
+ const describe = (before, after) => {
75
+ const added = countStagedEntries(after) - countStagedEntries(before);
76
+ const updated = countReasonUpdates(before, after);
77
+ const unchanged = supplied - added - updated;
78
+ return (`added ${added} of ${supplied} staged declaration(s) from ${from}` +
79
+ (updated > 0 ? `, updated the reason of ${updated}` : '') +
80
+ (unchanged > 0 ? ` (${unchanged} already present or repeated)` : ''));
81
+ };
76
82
  if (options.dryRun === true) {
77
- info(`[dry-run] ${targetPatch.filename}: ${describe(countStagedEntries(existing), projectedCount)}.`);
83
+ info(`[dry-run] ${targetPatch.filename}: ${describe(existing, mergeBatch(existing, batch))}.`);
78
84
  outro('Dry run complete — no changes made');
79
85
  return;
80
86
  }
@@ -92,7 +98,7 @@ export async function runBatchAdd(projectRoot, identifier, mode, options) {
92
98
  }
93
99
  const before = result.before.stagedDependencies;
94
100
  const after = result.after.stagedDependencies;
95
- info(`${targetPatch.filename}: ${describe(countStagedEntries(before), countStagedEntries(after))}.`);
101
+ info(`${targetPatch.filename}: ${describe(before, after)}.`);
96
102
  await appendHistoryBestEffort(paths.patches, {
97
103
  operation: 'patch-staged-dependency',
98
104
  args: {
@@ -4,7 +4,10 @@
4
4
  *
5
5
  * A removal names its entries by any of `--file`, `--specifier`/`--line`,
6
6
  * `--creates` and `--owner`, and must give at least `--creates` or the
7
- * `--file` + matcher pair. Registration lines compare whitespace-
7
+ * `--file` + matcher pair. Without `--kind` the matcher picks the kind
8
+ * (`--specifier` an import, `--line` a registration), and a selection with
9
+ * neither searches both kinds, so `--creates` alone removes every
10
+ * declaration for that path. Registration lines compare whitespace-
8
11
  * insensitively (runs of whitespace collapse, ends trimmed), the way the
9
12
  * lint compares them, so a line copied from a patch hunk matches the stored
10
13
  * one whatever its padding. A selection that matches nothing is refused
@@ -12,6 +15,24 @@
12
15
  */
13
16
  import type { PatchStagedDependencies, PatchStagedDependencyOptions } from '../../types/commands/index.js';
14
17
  import { type StagedDependencyKind, type StagedEntryView } from './staged-dependency-entries.js';
18
+ /** One stored declaration together with its kind. */
19
+ export interface KindedEntryView {
20
+ kind: StagedDependencyKind;
21
+ view: StagedEntryView;
22
+ }
23
+ /** Singular noun for a declaration of the given kinds. */
24
+ export declare function declarationNoun(kinds: readonly StagedDependencyKind[]): string;
25
+ /**
26
+ * The kinds a `--remove` searches. `--kind` wins when given (and is
27
+ * validated by the caller); otherwise `--specifier` selects imports,
28
+ * `--line` registrations, and a selection with neither matcher searches
29
+ * both kinds.
30
+ *
31
+ * @param options - Command options
32
+ * @returns The kinds to search, or undefined when `--kind` was given
33
+ * @throws InvalidArgumentError when `--specifier` and `--line` are both given without `--kind`
34
+ */
35
+ export declare function inferRemovalKinds(options: PatchStagedDependencyOptions): readonly StagedDependencyKind[] | undefined;
15
36
  /**
16
37
  * Resolves the staged entries a `--remove` invocation names. The returned
17
38
  * views are the stored entries themselves, so the removal matches them
@@ -20,12 +41,13 @@ import { type StagedDependencyKind, type StagedEntryView } from './staged-depend
20
41
  * - `--file` + matcher identifying one entry infers `--creates`.
21
42
  * - `--file` + matcher identifying several entries without `--creates`
22
43
  * refuses with the candidate list.
23
- * - `--creates` alone selects every declaration for that created path.
44
+ * - `--creates` alone selects every declaration for that created path, of
45
+ * every kind searched.
24
46
  *
25
- * @param kind - Declaration kind (`--kind`)
47
+ * @param kinds - Declaration kinds to search
26
48
  * @param options - Command options
27
49
  * @param staged - The target patch's current staged dependencies
28
50
  * @returns The matching stored entries (never empty)
29
51
  * @throws InvalidArgumentError when the selectors are insufficient or match nothing
30
52
  */
31
- export declare function selectRemovalEntries(kind: StagedDependencyKind, options: PatchStagedDependencyOptions, staged: PatchStagedDependencies | undefined): StagedEntryView[];
53
+ export declare function selectRemovalEntries(kinds: readonly StagedDependencyKind[], options: PatchStagedDependencyOptions, staged: PatchStagedDependencies | undefined): KindedEntryView[];
@@ -5,7 +5,10 @@
5
5
  *
6
6
  * A removal names its entries by any of `--file`, `--specifier`/`--line`,
7
7
  * `--creates` and `--owner`, and must give at least `--creates` or the
8
- * `--file` + matcher pair. Registration lines compare whitespace-
8
+ * `--file` + matcher pair. Without `--kind` the matcher picks the kind
9
+ * (`--specifier` an import, `--line` a registration), and a selection with
10
+ * neither searches both kinds, so `--creates` alone removes every
11
+ * declaration for that path. Registration lines compare whitespace-
9
12
  * insensitively (runs of whitespace collapse, ends trimmed), the way the
10
13
  * lint compares them, so a line copied from a patch hunk matches the stored
11
14
  * one whatever its padding. A selection that matches nothing is refused
@@ -18,10 +21,38 @@ import { dependencyLabel, importView, registrationView, } from './staged-depende
18
21
  function normalizeRegistrationLine(line) {
19
22
  return line.trim().replace(/\s+/g, ' ');
20
23
  }
21
- function stagedViews(kind, staged) {
22
- return kind === 'import'
23
- ? (staged?.forwardImports ?? []).map(importView)
24
- : (staged?.registrations ?? []).map(registrationView);
24
+ function stagedViews(kinds, staged) {
25
+ return kinds.flatMap((kind) => kind === 'import'
26
+ ? (staged?.forwardImports ?? []).map((entry) => ({ kind, view: importView(entry) }))
27
+ : (staged?.registrations ?? []).map((entry) => ({ kind, view: registrationView(entry) })));
28
+ }
29
+ /** Singular noun for a declaration of the given kinds. */
30
+ export function declarationNoun(kinds) {
31
+ if (kinds.length !== 1)
32
+ return 'staged declaration';
33
+ return kinds[0] === 'import' ? 'staged forward-import' : 'staged registration';
34
+ }
35
+ /**
36
+ * The kinds a `--remove` searches. `--kind` wins when given (and is
37
+ * validated by the caller); otherwise `--specifier` selects imports,
38
+ * `--line` registrations, and a selection with neither matcher searches
39
+ * both kinds.
40
+ *
41
+ * @param options - Command options
42
+ * @returns The kinds to search, or undefined when `--kind` was given
43
+ * @throws InvalidArgumentError when `--specifier` and `--line` are both given without `--kind`
44
+ */
45
+ export function inferRemovalKinds(options) {
46
+ if (options.kind !== undefined)
47
+ return undefined;
48
+ if (options.specifier !== undefined && options.line !== undefined) {
49
+ throw new InvalidArgumentError('--specifier names an import and --line a registration; pass one of them, or --kind.', '--specifier/--line');
50
+ }
51
+ if (options.specifier !== undefined)
52
+ return ['import'];
53
+ if (options.line !== undefined)
54
+ return ['registration'];
55
+ return ['import', 'registration'];
25
56
  }
26
57
  /**
27
58
  * Resolves the staged entries a `--remove` invocation names. The returned
@@ -31,47 +62,53 @@ function stagedViews(kind, staged) {
31
62
  * - `--file` + matcher identifying one entry infers `--creates`.
32
63
  * - `--file` + matcher identifying several entries without `--creates`
33
64
  * refuses with the candidate list.
34
- * - `--creates` alone selects every declaration for that created path.
65
+ * - `--creates` alone selects every declaration for that created path, of
66
+ * every kind searched.
35
67
  *
36
- * @param kind - Declaration kind (`--kind`)
68
+ * @param kinds - Declaration kinds to search
37
69
  * @param options - Command options
38
70
  * @param staged - The target patch's current staged dependencies
39
71
  * @returns The matching stored entries (never empty)
40
72
  * @throws InvalidArgumentError when the selectors are insufficient or match nothing
41
73
  */
42
- export function selectRemovalEntries(kind, options, staged) {
43
- const matcherFlag = kind === 'import' ? '--specifier' : '--line';
44
- const matcher = kind === 'import' ? options.specifier : options.line;
74
+ export function selectRemovalEntries(kinds, options, staged) {
75
+ const single = kinds.length === 1 ? kinds[0] : undefined;
76
+ const matcherFlag = single === 'import'
77
+ ? '--specifier'
78
+ : single === 'registration'
79
+ ? '--line'
80
+ : '--specifier or --line';
81
+ const matcher = single === 'import' ? options.specifier : single === 'registration' ? options.line : undefined;
45
82
  const hasPair = Boolean(options.file) && Boolean(matcher);
46
83
  if (!options.creates && !hasPair) {
47
- throw new InvalidArgumentError(`--remove${kind === 'registration' ? ' --kind registration' : ''} requires --creates, ` +
84
+ throw new InvalidArgumentError(`--remove${options.kind === 'registration' ? ' --kind registration' : ''} requires --creates, ` +
48
85
  `or --file with ${matcherFlag}; --creates alone removes every declaration for that path.`, matcher && !options.file
49
86
  ? '--file'
50
87
  : options.file && !matcher
51
88
  ? matcherFlag
52
89
  : `--creates or --file + ${matcherFlag}`);
53
90
  }
54
- const wanted = matcher === undefined ? undefined : comparableMatcher(kind, matcher);
55
- const views = stagedViews(kind, staged);
56
- const candidates = views.filter((view) => (!options.file || view.file === options.file) &&
57
- (wanted === undefined || comparableMatcher(kind, view.matcher) === wanted) &&
91
+ const views = stagedViews(kinds, staged);
92
+ const candidates = views.filter(({ kind, view }) => (!options.file || view.file === options.file) &&
93
+ (matcher === undefined ||
94
+ comparableMatcher(kind, view.matcher) === comparableMatcher(kind, matcher)) &&
58
95
  (!options.creates || view.creates === options.creates) &&
59
96
  (options.owner === undefined || view.owner === options.owner));
97
+ const label = ({ kind, view }) => ` - ${dependencyLabel(kind, view)}`;
60
98
  if (candidates.length === 0) {
61
- const noun = kind === 'import' ? 'staged forward-import' : 'staged registration';
99
+ const noun = declarationNoun(kinds);
62
100
  const existing = views.length === 0
63
101
  ? `This patch has no ${noun}s.`
64
- : `This patch's ${noun}s:\n${views.map((view) => ` - ${dependencyLabel(kind, view)}`).join('\n')}`;
102
+ : `This patch's ${noun}s:\n${views.map(label).join('\n')}`;
65
103
  throw new InvalidArgumentError(`--remove matched no ${noun}. ${existing}`, '--remove');
66
104
  }
67
105
  if (candidates.length > 1 && !options.creates) {
68
- const list = candidates.map((view) => ` - ${dependencyLabel(kind, view)}`).join('\n');
69
- const noun = kind === 'import' ? 'staged forward-imports' : 'staged registrations';
70
- throw new GeneralError(`--remove matches ${candidates.length} ${noun} on this patch; pass --creates to pick one:\n${list}`);
106
+ throw new GeneralError(`--remove matches ${candidates.length} ${declarationNoun(kinds)}s on this patch; ` +
107
+ `pass --creates to pick one:\n${candidates.map(label).join('\n')}`);
71
108
  }
72
- const single = candidates.length === 1 ? candidates[0] : undefined;
73
- if (single !== undefined && !options.creates) {
74
- info(`Inferred --creates ${single.creates} from the single matching staged entry.`);
109
+ const only = candidates.length === 1 ? candidates[0] : undefined;
110
+ if (only !== undefined && !options.creates) {
111
+ info(`Inferred --creates ${only.view.creates} from the single matching staged entry.`);
75
112
  }
76
113
  return candidates;
77
114
  }