@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
@@ -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: {
@@ -11,7 +11,7 @@ import { info, intro, outro } from '../../utils/logger.js';
11
11
  import { addWaitLockOption, resolveWaitLockSeconds } from '../../utils/options.js';
12
12
  import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
13
13
  import { stagedDependencyDelta } from './staged-dependency-batch.js';
14
- import { applyMode, countStagedEntries, dependencyLabel, importView, registrationView, } from './staged-dependency-entries.js';
14
+ import { applyMode, countReasonUpdates, countStagedEntries, dependencyLabel, importView, registrationView, } from './staged-dependency-entries.js';
15
15
  import { runBatchAdd } from './staged-dependency-from.js';
16
16
  import { selectRemovalEntries } from './staged-dependency-remove.js';
17
17
  import { validateStagedDependencyAdd } from './staged-dependency-validate.js';
@@ -89,7 +89,9 @@ function requireRegistrationOptions(options) {
89
89
  /**
90
90
  * Renders a one-line summary of a staged-dependency metadata change.
91
91
  */
92
- function describeStagedDependencyChange(beforeCount, afterCount, mode, kind, targets) {
92
+ function describeStagedDependencyChange(before, after, mode, kind, targets) {
93
+ const beforeCount = countStagedEntries(before);
94
+ const afterCount = countStagedEntries(after);
93
95
  const noun = kind === 'import' ? 'staged forward-import' : 'staged registration';
94
96
  if (mode === 'clear') {
95
97
  return beforeCount === 0
@@ -100,9 +102,11 @@ function describeStagedDependencyChange(beforeCount, afterCount, mode, kind, tar
100
102
  if (targets.length === 0)
101
103
  return 'stagedDependencies unchanged';
102
104
  if (mode === 'add') {
103
- return afterCount === beforeCount
104
- ? `${noun} already present: ${label}`
105
- : `added ${noun}: ${label}`;
105
+ if (afterCount !== beforeCount)
106
+ return `added ${noun}: ${label}`;
107
+ return countReasonUpdates(before, after) > 0
108
+ ? `updated 1 ${noun} reason in place: ${label}`
109
+ : `${noun} already present: ${label}`;
106
110
  }
107
111
  return afterCount === beforeCount
108
112
  ? `no ${noun} matched: ${label}`
@@ -132,7 +136,7 @@ export async function patchStagedDependencyCommand(projectRoot, identifier, opti
132
136
  const importDependency = mode === 'add' && kind === 'import' ? requireForwardImportOptions(options) : undefined;
133
137
  const registrationDependency = mode === 'add' && kind === 'registration' ? requireRegistrationOptions(options) : undefined;
134
138
  // Shape-check --add declarations against the loaded queue: a
135
- // patch-name-shaped --creates/--file is refused now instead of surfacing
139
+ // patch-name-shaped --creates/--file is refused here instead of surfacing
136
140
  // later as an undischargeable staged-dependency-unused.
137
141
  const added = importDependency ?? registrationDependency;
138
142
  if (added !== undefined) {
@@ -164,7 +168,7 @@ export async function patchStagedDependencyCommand(projectRoot, identifier, opti
164
168
  };
165
169
  const existing = targetPatch.stagedDependencies;
166
170
  const projected = applyToStaged(existing);
167
- const summary = describeStagedDependencyChange(countStagedEntries(existing), countStagedEntries(projected), mode, kind, targets);
171
+ const summary = describeStagedDependencyChange(existing, projected, mode, kind, targets);
168
172
  if (isDryRun) {
169
173
  info(`[dry-run] ${targetPatch.filename}: ${summary}.`);
170
174
  outro('Dry run complete — no changes made');
@@ -184,7 +188,7 @@ export async function patchStagedDependencyCommand(projectRoot, identifier, opti
184
188
  }
185
189
  const before = result.before.stagedDependencies;
186
190
  const after = result.after.stagedDependencies;
187
- info(`${targetPatch.filename}: ${describeStagedDependencyChange(countStagedEntries(before), countStagedEntries(after), mode, kind, targets)}.`);
191
+ info(`${targetPatch.filename}: ${describeStagedDependencyChange(before, after, mode, kind, targets)}.`);
188
192
  if (mode === 'remove' && countStagedEntries(after) === countStagedEntries(before)) {
189
193
  // The selection matched when the queue was read; a peer removed the
190
194
  // entries before the locked write. Still a miss, so still a refusal.
@@ -225,7 +229,7 @@ export function registerPatchStagedDependency(parent, context) {
225
229
  .option('--line <text>', 'Registration/packaging line as the patch adds it, compared whitespace-insensitively (--kind registration)')
226
230
  .option('--creates <path>', 'Later-created file path relative to engine/ (with --remove and no --file: removes every declaration for that path)')
227
231
  .option('--owner <patch>', 'Exact later patch filename expected to create --creates')
228
- .option('--reason <text>', 'Human-readable rationale stored with the declaration (with --from: the default for entries without one)')
232
+ .option('--reason <text>', 'Human-readable rationale stored with the declaration; --add on a declaration already present replaces its reason in place (with --from: the default for entries without one)')
229
233
  .option('--from <json-file>', 'Add every declaration in a JSON file under one lock hold (--add only): a "lint --per-patch --report" ' +
230
234
  "document (its findings' stagedDeclarations for this patch) or an array of " +
231
235
  '{ file, specifier | line, creates, owner?, reason?, patch? } entries')
@@ -63,8 +63,8 @@ export async function patchTierCommand(projectRoot, identifier, options = {}) {
63
63
  : `change tier from "${before}" to "${after}"`;
64
64
  // Destructive-operation contract (docs/lifecycle-invariants.md): manifest
65
65
  // metadata mutations get summary + dry-run + confirmation/--yes + history
66
- // uniformly. This command used to accept --yes without ever prompting, so
67
- // the flag only appeared in the history record.
66
+ // uniformly, so --yes skips a real prompt rather than only appearing in
67
+ // the history record.
68
68
  const decision = await confirmDestructive({
69
69
  operation: 'patch-tier',
70
70
  title: `Patch tier: ${target.filename}`,
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Structural equality for patch rename maps, shared by the placement-mode
3
3
  * export preview/commit check (`export-flow.ts`) and `patch reorder`'s
4
- * under-lock drift check. Both previously carried a byte-identical copy of
5
- * the sorted-entries comparison. One helper keeps the two commit gates from
6
- * drifting.
4
+ * under-lock drift check. One helper for the sorted-entries comparison
5
+ * keeps the two commit gates from drifting.
7
6
  */
8
7
  import type { PatchRenameEntry } from '../core/patch-manifest.js';
9
8
  /**
@@ -17,3 +16,15 @@ export declare function getSortedRenameEntries(renameMap: Map<string, PatchRenam
17
16
  * triple matches positionally.
18
17
  */
19
18
  export declare function renameMapsEqual(left: Map<string, PatchRenameEntry>, right: Map<string, PatchRenameEntry>): boolean;
19
+ /**
20
+ * Writes a committed rename map as one JSON object, old filename to new
21
+ * filename in the queue's new order (`{}` when nothing was renumbered), so
22
+ * a session can rewrite its references to the renumbered patches from the
23
+ * file instead of from the printed summary. Used by `patch reorder
24
+ * --map-out` and `patch split --map-out`.
25
+ *
26
+ * @param path - Output path, relative to the working directory
27
+ * @param renameMap - The committed rename map
28
+ * @returns The absolute path written
29
+ */
30
+ export declare function writeRenameMapFile(path: string, renameMap: Map<string, PatchRenameEntry>): Promise<string>;
@@ -2,10 +2,11 @@
2
2
  /**
3
3
  * Structural equality for patch rename maps, shared by the placement-mode
4
4
  * export preview/commit check (`export-flow.ts`) and `patch reorder`'s
5
- * under-lock drift check. Both previously carried a byte-identical copy of
6
- * the sorted-entries comparison. One helper keeps the two commit gates from
7
- * drifting.
5
+ * under-lock drift check. One helper for the sorted-entries comparison
6
+ * keeps the two commit gates from drifting.
8
7
  */
8
+ import { dirname, resolve } from 'node:path';
9
+ import { ensureDir, writeJson } from '../utils/fs.js';
9
10
  /**
10
11
  * Rename-map entries ordered by target order, so previews, history entries
11
12
  * and equality checks all walk the queue in its final on-disk order.
@@ -34,3 +35,20 @@ export function renameMapsEqual(left, right) {
34
35
  leftEntry.newOrder === rightEntry.newOrder);
35
36
  });
36
37
  }
38
+ /**
39
+ * Writes a committed rename map as one JSON object, old filename to new
40
+ * filename in the queue's new order (`{}` when nothing was renumbered), so
41
+ * a session can rewrite its references to the renumbered patches from the
42
+ * file instead of from the printed summary. Used by `patch reorder
43
+ * --map-out` and `patch split --map-out`.
44
+ *
45
+ * @param path - Output path, relative to the working directory
46
+ * @param renameMap - The committed rename map
47
+ * @returns The absolute path written
48
+ */
49
+ export async function writeRenameMapFile(path, renameMap) {
50
+ const absolute = resolve(path);
51
+ await ensureDir(dirname(absolute));
52
+ await writeJson(absolute, Object.fromEntries(getSortedRenameEntries(renameMap).map(([from, entry]) => [from, entry.newFilename])));
53
+ return absolute;
54
+ }
@@ -62,11 +62,10 @@ export declare function findMissingFiles(engineDir: string, files: readonly stri
62
62
  /**
63
63
  * One unmanaged file and the owned directory that made it adjacent.
64
64
  *
65
- * The anchor is what an unattended run could not previously recover: the
66
- * notice named the offending file and the patch, but not which of the
67
- * patch's owned locations the file sits beside, so triaging a refusal on a
68
- * patch that owns files in several directories meant re-deriving the
69
- * adjacency by hand from the manifest.
65
+ * The anchor names which of the patch's owned locations the file sits
66
+ * beside, so an unattended run can triage a refusal on a patch that owns
67
+ * files in several directories without re-deriving the adjacency by hand
68
+ * from the manifest.
70
69
  */
71
70
  export interface AdjacentUnmanagedFile {
72
71
  /** Engine-relative path of the unmanaged file. */
@@ -108,9 +108,10 @@ export declare function storeReExportLintResult(lintCtx: ReExportLintContext, pa
108
108
  * when the projection introduces error-severity cross-patch findings the
109
109
  * current queue does not have (a forward import an earlier owner gained,
110
110
  * say). Mirrors `re-export --files` (its projected-state conflict) and the
111
- * `--scan` adoption gate, which only covered files a scan adopted: a plain
112
- * re-export of an unchanged file set with new CONTENT used to pass, and the
113
- * queue carried the error until the next `export` placement refused it.
111
+ * `--scan` adoption gate, which only covers files a scan adopted: without
112
+ * this a plain re-export of an unchanged file set with new CONTENT would
113
+ * pass, and the queue would carry the error until the next `export`
114
+ * placement refused it.
114
115
  *
115
116
  * Runs in dry-run. `--force-unsafe` downgrades the refusal to warnings;
116
117
  * `--skip-lint` does not bypass it, same as `--files`. The baseline is the
@@ -221,9 +221,10 @@ export async function storeReExportLintResult(lintCtx, patchFilename, projectedM
221
221
  * when the projection introduces error-severity cross-patch findings the
222
222
  * current queue does not have (a forward import an earlier owner gained,
223
223
  * say). Mirrors `re-export --files` (its projected-state conflict) and the
224
- * `--scan` adoption gate, which only covered files a scan adopted: a plain
225
- * re-export of an unchanged file set with new CONTENT used to pass, and the
226
- * queue carried the error until the next `export` placement refused it.
224
+ * `--scan` adoption gate, which only covers files a scan adopted: without
225
+ * this a plain re-export of an unchanged file set with new CONTENT would
226
+ * pass, and the queue would carry the error until the next `export`
227
+ * placement refused it.
227
228
  *
228
229
  * Runs in dry-run. `--force-unsafe` downgrades the refusal to warnings;
229
230
  * `--skip-lint` does not bypass it, same as `--files`. The baseline is the
@@ -6,8 +6,7 @@
6
6
  * Under contention the lock wait runs to many minutes, and an argument the
7
7
  * run was always going to refuse (an empty patch name from an unquoted
8
8
  * `"$@"`, a changed-file path passed as a positional, a path flag that
9
- * swallowed the next flag) used to be reported only after that wait. These
10
- * checks read the manifest and a path-scoped `git status` without the lock.
9
+ * swallowed the next flag) is reported before that wait: these checks read the manifest and a path-scoped `git status` without the lock.
11
10
  * The command repeats its own resolution under the lock, so a manifest that
12
11
  * changes in between is still read fresh there.
13
12
  */
@@ -13,6 +13,6 @@ export declare function fingerprintScopedDryRunState(engineDir: string, patchesD
13
13
  /**
14
14
  * Names whoever else wrote during the dry run: history records from other
15
15
  * invocations since it started, and a live holder of the engine-session
16
- * lock (which a dry run no longer takes). Empty when there is no evidence.
16
+ * lock (which a dry run does not take). Empty when there is no evidence.
17
17
  */
18
18
  export declare function describeForeignWriters(patchesDir: string, sinceMs: number): Promise<string[]>;
@@ -2,10 +2,9 @@
2
2
  /**
3
3
  * The scoped half of the `re-export --dry-run` purity guard.
4
4
  *
5
- * A dry run takes no engine-session lock since 0.51.0, so peers keep
6
- * writing while it runs. The whole-tree fingerprint then reported every
7
- * peer's write as "invariant violated … This is a FireForge bug", and an
8
- * immediate repeat was clean. The scoped fingerprint covers only what the
5
+ * A dry run takes no engine-session lock, so peers keep writing while it
6
+ * runs. A whole-tree fingerprint would report every peer's write as
7
+ * "invariant violated … This is a FireForge bug". The scoped fingerprint covers only what the
9
8
  * dry run could touch: the selected patches' bodies and manifest rows, the
10
9
  * engine files they own, the engine HEAD and the engine index. A mismatch
11
10
  * that remains is attributed to a foreign writer when the history log or
@@ -17,6 +16,7 @@ import { readEngineSessionLockStatus } from '../core/engine-session-lock.js';
17
16
  import { git } from '../core/git-base.js';
18
17
  import { historyRecordIdentity, resolveHistoryLogPath } from '../core/history-log-file.js';
19
18
  import { PATCHES_MANIFEST } from '../core/patch-manifest-io.js';
19
+ import { canonicalizePatchRow } from '../core/patch-manifest-key-order.js';
20
20
  import { GeneralError } from '../errors/base.js';
21
21
  import { getNodeErrorCode, toError } from '../utils/errors.js';
22
22
  import { sha256Hex } from '../utils/hash.js';
@@ -33,7 +33,11 @@ async function hashOrAbsent(path, label) {
33
33
  throw new GeneralError(`[dry-run] cannot fingerprint ${label} for the purity guard: ${toError(error).message}`, error);
34
34
  }
35
35
  }
36
- /** The selected rows of `patches.json`, so a peer's write to another row is not a violation. */
36
+ /**
37
+ * The selected rows of `patches.json`, so a peer's write to another row is
38
+ * not a violation. Rows are hashed in canonical key order: a peer's write
39
+ * that only canonicalizes a selected row's key order changes no content.
40
+ */
37
41
  async function selectedRowsFingerprint(patchesDir, filenames) {
38
42
  let raw;
39
43
  try {
@@ -47,7 +51,9 @@ async function selectedRowsFingerprint(patchesDir, filenames) {
47
51
  try {
48
52
  const parsed = JSON.parse(raw);
49
53
  const rows = isObject(parsed) && isArray(parsed['patches']) ? parsed['patches'] : [];
50
- const selected = rows.filter((row) => isObject(row) && typeof row['filename'] === 'string' && filenames.has(row['filename']));
54
+ const selected = rows
55
+ .filter((row) => isObject(row) && typeof row['filename'] === 'string' && filenames.has(row['filename']))
56
+ .map((row) => (isObject(row) ? canonicalizePatchRow(row) : row));
51
57
  return sha256Hex(JSON.stringify(selected));
52
58
  }
53
59
  catch {
@@ -112,7 +118,7 @@ async function foreignHistoryRecords(projectRoot, sinceMs) {
112
118
  /**
113
119
  * Names whoever else wrote during the dry run: history records from other
114
120
  * invocations since it started, and a live holder of the engine-session
115
- * lock (which a dry run no longer takes). Empty when there is no evidence.
121
+ * lock (which a dry run does not take). Empty when there is no evidence.
116
122
  */
117
123
  export async function describeForeignWriters(patchesDir, sinceMs) {
118
124
  const projectRoot = dirname(resolve(patchesDir));
@@ -0,0 +1,22 @@
1
+ import type { PatchesManifest, PatchMetadata, ReExportOptions } from '../types/commands/index.js';
2
+ import type { ProjectPaths } from '../types/config.js';
3
+ /** What {@link dropRetiredPathDeclarations} needs from the run. */
4
+ export interface RetiredPathDeclarationsArgs {
5
+ /** Resolved project paths. */
6
+ paths: ProjectPaths;
7
+ /** The queue as the run loaded it (judged in a dry run). */
8
+ manifest: PatchesManifest;
9
+ /** The run's options; `expectRemoved` holds the validated paths. */
10
+ options: ReExportOptions;
11
+ /** The run's selected patches. */
12
+ selectedPatches: readonly PatchMetadata[];
13
+ /** Name the drops without writing. */
14
+ isDryRun: boolean;
15
+ }
16
+ /**
17
+ * Drops (or, in a dry run, names) every staged declaration whose `creates`
18
+ * path this run retired. Runs after the selected patches are written, so a
19
+ * path whose owner failed to re-export is still owned and its declarations
20
+ * stay.
21
+ */
22
+ export declare function dropRetiredPathDeclarations(args: RetiredPathDeclarationsArgs): Promise<void>;