@hominis/fireforge 0.49.1 → 0.50.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 (336) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +1 -1
  3. package/dist/bin/fireforge.js +6 -0
  4. package/dist/build-info.json +4 -4
  5. package/dist/src/commands/build-prelock.d.ts +20 -0
  6. package/dist/src/commands/build-prelock.js +50 -0
  7. package/dist/src/commands/build.d.ts +3 -1
  8. package/dist/src/commands/build.js +13 -6
  9. package/dist/src/commands/check-report.d.ts +28 -0
  10. package/dist/src/commands/check-report.js +78 -0
  11. package/dist/src/commands/check-run.d.ts +28 -0
  12. package/dist/src/commands/check-run.js +104 -0
  13. package/dist/src/commands/check-steps.d.ts +67 -0
  14. package/dist/src/commands/check-steps.js +175 -0
  15. package/dist/src/commands/check.d.ts +32 -0
  16. package/dist/src/commands/check.js +160 -0
  17. package/dist/src/commands/config.js +5 -6
  18. package/dist/src/commands/doctor-engine-eslint.js +6 -1
  19. package/dist/src/commands/doctor-source-pin.js +4 -1
  20. package/dist/src/commands/export-adopt-check.d.ts +38 -0
  21. package/dist/src/commands/export-adopt-check.js +98 -0
  22. package/dist/src/commands/export-flow.js +29 -14
  23. package/dist/src/commands/export-placement-gate.d.ts +6 -2
  24. package/dist/src/commands/export-shared.d.ts +2 -1
  25. package/dist/src/commands/export-staged-registration.d.ts +46 -0
  26. package/dist/src/commands/export-staged-registration.js +91 -0
  27. package/dist/src/commands/forward-import-remedy.d.ts +32 -0
  28. package/dist/src/commands/forward-import-remedy.js +63 -0
  29. package/dist/src/commands/furnace/apply.js +3 -3
  30. package/dist/src/commands/furnace/chrome-doc-remove.d.ts +2 -1
  31. package/dist/src/commands/furnace/chrome-doc-remove.js +2 -2
  32. package/dist/src/commands/furnace/chrome-doc.d.ts +2 -1
  33. package/dist/src/commands/furnace/chrome-doc.js +2 -2
  34. package/dist/src/commands/furnace/create-templates.d.ts +8 -3
  35. package/dist/src/commands/furnace/create-templates.js +8 -23
  36. package/dist/src/commands/furnace/create.js +2 -2
  37. package/dist/src/commands/furnace/deploy-named.d.ts +48 -0
  38. package/dist/src/commands/furnace/deploy-named.js +107 -0
  39. package/dist/src/commands/furnace/deploy.d.ts +3 -2
  40. package/dist/src/commands/furnace/deploy.js +50 -86
  41. package/dist/src/commands/furnace/diff.js +26 -3
  42. package/dist/src/commands/furnace/index.js +66 -41
  43. package/dist/src/commands/furnace/init.js +13 -6
  44. package/dist/src/commands/furnace/override.js +4 -2
  45. package/dist/src/commands/furnace/preview.js +4 -3
  46. package/dist/src/commands/furnace/refresh.js +2 -2
  47. package/dist/src/commands/furnace/remove.js +4 -3
  48. package/dist/src/commands/furnace/rename-dry-run.d.ts +29 -0
  49. package/dist/src/commands/furnace/rename-dry-run.js +127 -0
  50. package/dist/src/commands/furnace/rename-helpers.d.ts +7 -0
  51. package/dist/src/commands/furnace/rename-helpers.js +15 -1
  52. package/dist/src/commands/furnace/rename-registrations.d.ts +41 -0
  53. package/dist/src/commands/furnace/rename-registrations.js +93 -0
  54. package/dist/src/commands/furnace/rename.d.ts +2 -1
  55. package/dist/src/commands/furnace/rename.js +28 -66
  56. package/dist/src/commands/furnace/scan.d.ts +9 -4
  57. package/dist/src/commands/furnace/scan.js +11 -9
  58. package/dist/src/commands/furnace/sync.js +3 -2
  59. package/dist/src/commands/furnace/validate-report.d.ts +45 -0
  60. package/dist/src/commands/furnace/validate-report.js +91 -0
  61. package/dist/src/commands/furnace/validate.d.ts +1 -1
  62. package/dist/src/commands/furnace/validate.js +127 -114
  63. package/dist/src/commands/furnace/validation-output.js +14 -3
  64. package/dist/src/commands/lint-per-patch.js +31 -60
  65. package/dist/src/commands/lint-report.d.ts +8 -4
  66. package/dist/src/commands/lint-report.js +15 -5
  67. package/dist/src/commands/lint.js +15 -34
  68. package/dist/src/commands/manifest.js +2 -0
  69. package/dist/src/commands/patch/delete-gap-notice.js +3 -1
  70. package/dist/src/commands/patch/delete.d.ts +1 -1
  71. package/dist/src/commands/patch/delete.js +13 -98
  72. package/dist/src/commands/patch/index.d.ts +1 -1
  73. package/dist/src/commands/patch/index.js +4 -2
  74. package/dist/src/commands/patch/lint-ignore.d.ts +5 -1
  75. package/dist/src/commands/patch/lint-ignore.js +65 -28
  76. package/dist/src/commands/patch/move-files-create.js +26 -17
  77. package/dist/src/commands/patch/move-files-into.js +5 -3
  78. package/dist/src/commands/patch/move-files.js +3 -1
  79. package/dist/src/commands/patch/removal-dependents.d.ts +63 -0
  80. package/dist/src/commands/patch/removal-dependents.js +133 -0
  81. package/dist/src/commands/patch/rename.js +7 -0
  82. package/dist/src/commands/patch/retire.d.ts +34 -0
  83. package/dist/src/commands/patch/retire.js +128 -0
  84. package/dist/src/commands/patch/split-plan.d.ts +7 -7
  85. package/dist/src/commands/patch/split-plan.js +45 -28
  86. package/dist/src/commands/patch/split-staged.d.ts +108 -0
  87. package/dist/src/commands/patch/split-staged.js +202 -0
  88. package/dist/src/commands/patch/split.d.ts +5 -0
  89. package/dist/src/commands/patch/split.js +51 -15
  90. package/dist/src/commands/patch/staged-dependency-batch.d.ts +48 -0
  91. package/dist/src/commands/patch/staged-dependency-batch.js +176 -0
  92. package/dist/src/commands/patch/staged-dependency-entries.d.ts +36 -0
  93. package/dist/src/commands/patch/staged-dependency-entries.js +52 -0
  94. package/dist/src/commands/patch/staged-dependency-from.d.ts +14 -0
  95. package/dist/src/commands/patch/staged-dependency-from.js +108 -0
  96. package/dist/src/commands/patch/staged-dependency.js +14 -41
  97. package/dist/src/commands/re-export-adjacent-registrations.d.ts +20 -0
  98. package/dist/src/commands/re-export-adjacent-registrations.js +75 -0
  99. package/dist/src/commands/re-export-adjacent.d.ts +45 -2
  100. package/dist/src/commands/re-export-adjacent.js +156 -37
  101. package/dist/src/commands/re-export-drift.d.ts +36 -8
  102. package/dist/src/commands/re-export-drift.js +73 -49
  103. package/dist/src/commands/re-export-files.js +11 -8
  104. package/dist/src/commands/re-export-lint.d.ts +2 -1
  105. package/dist/src/commands/re-export-lint.js +23 -10
  106. package/dist/src/commands/re-export-options.js +45 -4
  107. package/dist/src/commands/re-export-preflight.d.ts +23 -0
  108. package/dist/src/commands/re-export-preflight.js +160 -0
  109. package/dist/src/commands/re-export-register.js +42 -17
  110. package/dist/src/commands/re-export-removed.d.ts +12 -1
  111. package/dist/src/commands/re-export-removed.js +41 -11
  112. package/dist/src/commands/re-export-scan-owner.d.ts +44 -0
  113. package/dist/src/commands/re-export-scan-owner.js +116 -0
  114. package/dist/src/commands/re-export-scan.d.ts +17 -9
  115. package/dist/src/commands/re-export-scan.js +56 -23
  116. package/dist/src/commands/re-export-single.js +22 -6
  117. package/dist/src/commands/re-export-summary.d.ts +28 -0
  118. package/dist/src/commands/re-export-summary.js +36 -0
  119. package/dist/src/commands/re-export.js +37 -24
  120. package/dist/src/commands/rebase/index.js +17 -17
  121. package/dist/src/commands/run.js +16 -2
  122. package/dist/src/commands/status-check.d.ts +55 -16
  123. package/dist/src/commands/status-check.js +92 -37
  124. package/dist/src/commands/status-json.d.ts +22 -9
  125. package/dist/src/commands/status-json.js +47 -32
  126. package/dist/src/commands/status-output.js +3 -1
  127. package/dist/src/commands/status-scope.d.ts +37 -0
  128. package/dist/src/commands/status-scope.js +54 -0
  129. package/dist/src/commands/status.js +66 -23
  130. package/dist/src/commands/test-browser-preflight.js +19 -3
  131. package/dist/src/commands/test-build-furnace.d.ts +21 -0
  132. package/dist/src/commands/test-build-furnace.js +48 -0
  133. package/dist/src/commands/test-build-phase.js +47 -41
  134. package/dist/src/commands/test-checkjs.js +7 -0
  135. package/dist/src/commands/test-diagnose.js +7 -0
  136. package/dist/src/commands/test-modes.d.ts +16 -1
  137. package/dist/src/commands/test-modes.js +24 -1
  138. package/dist/src/commands/test-multi-verdict.d.ts +61 -0
  139. package/dist/src/commands/test-multi-verdict.js +111 -0
  140. package/dist/src/commands/test-orchestrate.d.ts +43 -0
  141. package/dist/src/commands/test-orchestrate.js +222 -0
  142. package/dist/src/commands/test-perf-scope.d.ts +29 -0
  143. package/dist/src/commands/test-perf-scope.js +83 -0
  144. package/dist/src/commands/test-prelock.d.ts +51 -0
  145. package/dist/src/commands/test-prelock.js +177 -0
  146. package/dist/src/commands/test-register.js +54 -10
  147. package/dist/src/commands/test-run.js +4 -1
  148. package/dist/src/commands/test-stale-gate.js +57 -27
  149. package/dist/src/commands/test-verdict.d.ts +10 -1
  150. package/dist/src/commands/test-verdict.js +28 -1
  151. package/dist/src/commands/test.d.ts +6 -1
  152. package/dist/src/commands/test.js +66 -60
  153. package/dist/src/commands/token-lint.d.ts +14 -0
  154. package/dist/src/commands/token-lint.js +111 -0
  155. package/dist/src/commands/token.js +10 -0
  156. package/dist/src/commands/verify-scope.d.ts +28 -0
  157. package/dist/src/commands/verify-scope.js +67 -0
  158. package/dist/src/commands/verify.d.ts +10 -2
  159. package/dist/src/commands/verify.js +59 -35
  160. package/dist/src/core/build-baseline-types.d.ts +46 -0
  161. package/dist/src/core/build-baseline-validate.js +19 -0
  162. package/dist/src/core/build-baseline.d.ts +5 -1
  163. package/dist/src/core/build-baseline.js +3 -1
  164. package/dist/src/core/build-overwrite-guard.d.ts +50 -0
  165. package/dist/src/core/build-overwrite-guard.js +84 -4
  166. package/dist/src/core/build-prepare.d.ts +7 -0
  167. package/dist/src/core/build-prepare.js +14 -8
  168. package/dist/src/core/build-run-record.d.ts +74 -0
  169. package/dist/src/core/build-run-record.js +136 -0
  170. package/dist/src/core/checkjs-support-heads.d.ts +41 -0
  171. package/dist/src/core/checkjs-support-heads.js +104 -0
  172. package/dist/src/core/config-paths.d.ts +2 -2
  173. package/dist/src/core/config-paths.js +8 -0
  174. package/dist/src/core/config-validate-check.d.ts +8 -0
  175. package/dist/src/core/config-validate-check.js +143 -0
  176. package/dist/src/core/config-validate-file-size.d.ts +9 -6
  177. package/dist/src/core/config-validate-file-size.js +91 -2
  178. package/dist/src/core/config-validate-history.d.ts +11 -0
  179. package/dist/src/core/config-validate-history.js +38 -0
  180. package/dist/src/core/config-validate-patch-policy.js +100 -0
  181. package/dist/src/core/config-validate-test-toolchains.js +7 -0
  182. package/dist/src/core/config-validate-token-scales.d.ts +6 -0
  183. package/dist/src/core/config-validate-token-scales.js +50 -0
  184. package/dist/src/core/config-validate.js +14 -5
  185. package/dist/src/core/config.d.ts +5 -0
  186. package/dist/src/core/config.js +16 -0
  187. package/dist/src/core/coverage-dispatch.d.ts +24 -0
  188. package/dist/src/core/coverage-dispatch.js +44 -0
  189. package/dist/src/core/coverage-extend.d.ts +70 -45
  190. package/dist/src/core/coverage-extend.js +199 -144
  191. package/dist/src/core/coverage-records.d.ts +97 -0
  192. package/dist/src/core/coverage-records.js +350 -0
  193. package/dist/src/core/destructive.d.ts +7 -4
  194. package/dist/src/core/destructive.js +10 -10
  195. package/dist/src/core/engine-session-lock.d.ts +10 -1
  196. package/dist/src/core/engine-session-lock.js +28 -8
  197. package/dist/src/core/export-moved-code-shape.d.ts +49 -11
  198. package/dist/src/core/export-moved-code-shape.js +135 -44
  199. package/dist/src/core/file-lock-queue.d.ts +67 -0
  200. package/dist/src/core/file-lock-queue.js +221 -0
  201. package/dist/src/core/file-lock.d.ts +4 -25
  202. package/dist/src/core/file-lock.js +105 -138
  203. package/dist/src/core/furnace-apply-output.d.ts +10 -0
  204. package/dist/src/core/furnace-apply-output.js +31 -1
  205. package/dist/src/core/furnace-apply.js +12 -2
  206. package/dist/src/core/furnace-composed-source.d.ts +76 -0
  207. package/dist/src/core/furnace-composed-source.js +167 -0
  208. package/dist/src/core/furnace-config-validate-exceptions.d.ts +9 -0
  209. package/dist/src/core/furnace-config-validate-exceptions.js +71 -0
  210. package/dist/src/core/furnace-config-validate.js +8 -0
  211. package/dist/src/core/furnace-css-fragments.d.ts +10 -0
  212. package/dist/src/core/furnace-css-fragments.js +26 -0
  213. package/dist/src/core/furnace-operation.d.ts +3 -3
  214. package/dist/src/core/furnace-registration-remove.d.ts +22 -1
  215. package/dist/src/core/furnace-registration-remove.js +53 -17
  216. package/dist/src/core/furnace-registration-rename.d.ts +26 -0
  217. package/dist/src/core/furnace-registration-rename.js +77 -0
  218. package/dist/src/core/furnace-registration.d.ts +14 -4
  219. package/dist/src/core/furnace-registration.js +44 -10
  220. package/dist/src/core/furnace-staleness.d.ts +7 -0
  221. package/dist/src/core/furnace-staleness.js +34 -24
  222. package/dist/src/core/furnace-validate-accessibility.d.ts +4 -2
  223. package/dist/src/core/furnace-validate-accessibility.js +10 -1
  224. package/dist/src/core/furnace-validate-exceptions.d.ts +40 -0
  225. package/dist/src/core/furnace-validate-exceptions.js +43 -0
  226. package/dist/src/core/furnace-validate-icon-button.d.ts +7 -0
  227. package/dist/src/core/furnace-validate-icon-button.js +49 -0
  228. package/dist/src/core/furnace-validate.js +1 -1
  229. package/dist/src/core/harness-port-fallback.d.ts +60 -0
  230. package/dist/src/core/harness-port-fallback.js +96 -0
  231. package/dist/src/core/history-log-file.d.ts +61 -0
  232. package/dist/src/core/history-log-file.js +157 -0
  233. package/dist/src/core/history-log.d.ts +2 -1
  234. package/dist/src/core/history-log.js +2 -1
  235. package/dist/src/core/json-preserving-edit.d.ts +22 -0
  236. package/dist/src/core/json-preserving-edit.js +199 -0
  237. package/dist/src/core/mach.js +8 -1
  238. package/dist/src/core/negative-control-diff.d.ts +39 -0
  239. package/dist/src/core/negative-control-diff.js +136 -0
  240. package/dist/src/core/negative-control.d.ts +138 -0
  241. package/dist/src/core/negative-control.js +356 -0
  242. package/dist/src/core/objdir-install-manifests.d.ts +73 -0
  243. package/dist/src/core/objdir-install-manifests.js +226 -0
  244. package/dist/src/core/patch-export-update.d.ts +2 -0
  245. package/dist/src/core/patch-export-update.js +1 -0
  246. package/dist/src/core/patch-export.js +3 -4
  247. package/dist/src/core/patch-lint-checkjs-any-import.d.ts +37 -0
  248. package/dist/src/core/patch-lint-checkjs-any-import.js +61 -0
  249. package/dist/src/core/patch-lint-checkjs.js +8 -11
  250. package/dist/src/core/patch-lint-code-lines.d.ts +59 -0
  251. package/dist/src/core/patch-lint-code-lines.js +105 -0
  252. package/dist/src/core/patch-lint-cross.js +12 -4
  253. package/dist/src/core/patch-lint-css.d.ts +8 -2
  254. package/dist/src/core/patch-lint-css.js +10 -4
  255. package/dist/src/core/patch-lint-file-size.d.ts +12 -5
  256. package/dist/src/core/patch-lint-file-size.js +22 -11
  257. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +18 -0
  258. package/dist/src/core/patch-lint-forward-registration-extended.js +35 -10
  259. package/dist/src/core/patch-lint-forward-registration.js +10 -0
  260. package/dist/src/core/patch-lint-ignore.d.ts +152 -0
  261. package/dist/src/core/patch-lint-ignore.js +299 -0
  262. package/dist/src/core/patch-lint-size-thresholds.d.ts +52 -0
  263. package/dist/src/core/patch-lint-size-thresholds.js +110 -0
  264. package/dist/src/core/patch-lint-size.d.ts +117 -0
  265. package/dist/src/core/patch-lint-size.js +323 -0
  266. package/dist/src/core/patch-lint-staged-registration.d.ts +18 -1
  267. package/dist/src/core/patch-lint-staged-registration.js +20 -0
  268. package/dist/src/core/patch-lint-token-scale.d.ts +39 -0
  269. package/dist/src/core/patch-lint-token-scale.js +109 -0
  270. package/dist/src/core/patch-lint.d.ts +11 -83
  271. package/dist/src/core/patch-lint.js +64 -286
  272. package/dist/src/core/patch-lock.js +7 -6
  273. package/dist/src/core/patch-manifest-consistency.js +7 -6
  274. package/dist/src/core/patch-manifest-io.d.ts +6 -3
  275. package/dist/src/core/patch-manifest-io.js +19 -8
  276. package/dist/src/core/patch-manifest-validate.js +34 -2
  277. package/dist/src/core/patch-order-legal.d.ts +25 -0
  278. package/dist/src/core/patch-order-legal.js +83 -0
  279. package/dist/src/core/patch-policy-content.d.ts +40 -0
  280. package/dist/src/core/patch-policy-content.js +216 -0
  281. package/dist/src/core/patch-policy.d.ts +16 -3
  282. package/dist/src/core/patch-policy.js +16 -9
  283. package/dist/src/core/patch-retired.d.ts +35 -0
  284. package/dist/src/core/patch-retired.js +54 -0
  285. package/dist/src/core/patch-subset.d.ts +16 -0
  286. package/dist/src/core/patch-subset.js +66 -0
  287. package/dist/src/core/preflight-wait.d.ts +20 -0
  288. package/dist/src/core/preflight-wait.js +38 -0
  289. package/dist/src/core/rebase-from-version.d.ts +46 -0
  290. package/dist/src/core/rebase-from-version.js +71 -0
  291. package/dist/src/core/run-log.d.ts +20 -0
  292. package/dist/src/core/run-log.js +89 -3
  293. package/dist/src/core/run-profile.d.ts +7 -0
  294. package/dist/src/core/run-profile.js +7 -1
  295. package/dist/src/core/smoke-patterns.d.ts +5 -0
  296. package/dist/src/core/smoke-patterns.js +13 -0
  297. package/dist/src/core/test-harness-crash.d.ts +18 -1
  298. package/dist/src/core/test-harness-crash.js +6 -4
  299. package/dist/src/core/test-harness-signature.d.ts +17 -0
  300. package/dist/src/core/test-harness-signature.js +4 -0
  301. package/dist/src/core/test-run-completeness.d.ts +57 -0
  302. package/dist/src/core/test-run-completeness.js +144 -0
  303. package/dist/src/core/test-stale-check.d.ts +27 -1
  304. package/dist/src/core/test-stale-check.js +39 -49
  305. package/dist/src/core/test-stale-exempt.d.ts +40 -0
  306. package/dist/src/core/test-stale-exempt.js +70 -0
  307. package/dist/src/core/token-lint.d.ts +50 -0
  308. package/dist/src/core/token-lint.js +126 -0
  309. package/dist/src/core/tree-guard.js +3 -0
  310. package/dist/src/core/typecheck-shim.d.ts +1 -1
  311. package/dist/src/core/typecheck-shim.js +90 -2
  312. package/dist/src/index.d.ts +8 -3
  313. package/dist/src/index.js +3 -1
  314. package/dist/src/types/commands/index.d.ts +2 -2
  315. package/dist/src/types/commands/options.d.ts +148 -21
  316. package/dist/src/types/commands/patches.d.ts +75 -1
  317. package/dist/src/types/config.d.ts +192 -0
  318. package/dist/src/types/config.js +9 -0
  319. package/dist/src/types/furnace.d.ts +43 -1
  320. package/dist/src/types/furnace.js +7 -0
  321. package/dist/src/types/index.d.ts +1 -1
  322. package/dist/src/utils/options.d.ts +34 -6
  323. package/dist/src/utils/options.js +86 -5
  324. package/dist/src/utils/path-glob.d.ts +15 -0
  325. package/dist/src/utils/path-glob.js +47 -0
  326. package/dist/src/utils/regex.d.ts +15 -3
  327. package/dist/src/utils/regex.js +37 -16
  328. package/docs/configuration.md +282 -9
  329. package/docs/exit-codes.md +18 -1
  330. package/docs/furnace.md +97 -3
  331. package/docs/lifecycle-invariants.md +52 -19
  332. package/docs/machine-output.md +74 -12
  333. package/docs/patch-workflow.md +297 -22
  334. package/docs/run-logs.md +20 -1
  335. package/docs/testing.md +276 -20
  336. package/package.json +2 -2
@@ -22,9 +22,11 @@ import { assertConfirmationAvailable, confirmDestructive } from '../../core/dest
22
22
  import { appendHistoryBestEffort } from '../../core/history-log.js';
23
23
  import { formatPatchNotFoundError } from '../../core/patch-identifier-suggest.js';
24
24
  import { buildPatchQueueContext } from '../../core/patch-lint.js';
25
+ import { buildLintIgnoreChecks } from '../../core/patch-lint-ignore.js';
25
26
  import { withPatchDirectoryLock } from '../../core/patch-lock.js';
26
27
  import { loadPatchesManifest, renumberPatchesInManifest, resolvePatchIdentifier, savePatchesManifest, validatePatchesManifest, } from '../../core/patch-manifest.js';
27
28
  import { enforcePatchPolicy } from '../../core/patch-policy.js';
29
+ import { manifestWithRetiredSlots } from '../../core/patch-retired.js';
28
30
  import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
29
31
  import { toError } from '../../utils/errors.js';
30
32
  import { readText, removeFile, writeText } from '../../utils/fs.js';
@@ -34,7 +36,32 @@ import { normalizePatchDisplayName, parsePositiveIntegerFlag } from '../../utils
34
36
  import { proceedAfterDecision } from '../destructive-decision.js';
35
37
  import { placementPlansEqual, resolvePlacementPlan } from '../export-flow.js';
36
38
  import { runPatchLint } from '../export-shared.js';
37
- import { assertSourceOwnsFiles, buildNewPatchMetadata, buildSplitDiff, buildSplitSummary, findOwnerRewriteHolders, mergeStagedForwardImports, projectSplitManifest, rewriteSplitOwners, runProjectedSplitLint, } from './split-plan.js';
39
+ import { assertSourceOwnsFiles, buildNewPatchMetadata, buildSplitDiff, buildSplitSummary, mergeStagedForwardImports, projectSplitManifest, runProjectedSplitLint, } from './split-plan.js';
40
+ import { describeStagedMaintenance, emptyStagedMaintenance, findOwnerRewriteHolders, rewriteSplitOwners, withoutDroppedDeclarations, withoutRelocatedDeclarations, } from './split-staged.js';
41
+ /**
42
+ * History payload for the declarations a split re-homed or dropped, omitted
43
+ * when it touched none so unaffected records keep their old shape.
44
+ */
45
+ function stagedMaintenanceHistory(plan) {
46
+ const { relocated, dropped } = plan.stagedMaintenance;
47
+ const relocatedCount = (relocated.forwardImports?.length ?? 0) + (relocated.registrations?.length ?? 0);
48
+ return {
49
+ ...(relocatedCount > 0 ? { relocatedDeclarations: relocated } : {}),
50
+ ...(dropped.length > 0 ? { droppedDeclarations: dropped } : {}),
51
+ };
52
+ }
53
+ /**
54
+ * Prints the owner rewrites and the declarations the move re-homed or
55
+ * dropped, after a committed split or `move-files --create`.
56
+ */
57
+ export function reportSplitStagedChanges(plan) {
58
+ if (plan.ownerRewrites.length > 0) {
59
+ info(`Re-pointed staged-dependency owners in: ${plan.ownerRewrites.join(', ')}`);
60
+ }
61
+ for (const line of describeStagedMaintenance(plan.stagedMaintenance, plan.source.filename, plan.placement.newFilename)) {
62
+ info(line);
63
+ }
64
+ }
38
65
  /**
39
66
  * Commits a confirmed split under the patch directory lock: renumber →
40
67
  * write new patch body → write shrunken source body → single manifest
@@ -84,13 +111,17 @@ export async function commitPatchSplit(patchesDir, plan, newMetadata, options, c
84
111
  if (decls?.length) {
85
112
  withOwners = mergeStagedForwardImports(withOwners, decls);
86
113
  }
114
+ withOwners = withoutDroppedDeclarations(withOwners, plan.stagedMaintenance.dropped);
87
115
  if (patch.filename !== effectiveSourceFilename)
88
116
  return withOwners;
89
- return { ...withOwners, filesAffected: plan.remainingFiles };
117
+ return {
118
+ ...withoutRelocatedDeclarations(withOwners, movedSet),
119
+ filesAffected: plan.remainingFiles,
120
+ };
90
121
  });
91
122
  updatedPatches.push(newMetadata);
92
123
  updatedPatches.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
93
- const updated = validatePatchesManifest({ ...fresh, patches: updatedPatches });
124
+ const updated = validatePatchesManifest(manifestWithRetiredSlots(fresh, updatedPatches));
94
125
  await savePatchesManifest(patchesDir, updated);
95
126
  await appendHistoryBestEffort(patchesDir, {
96
127
  operation: 'patch-split',
@@ -100,6 +131,7 @@ export async function commitPatchSplit(patchesDir, plan, newMetadata, options, c
100
131
  order: plan.placement.insertionOrder,
101
132
  files: plan.movedFiles,
102
133
  ownerRewrites: plan.ownerRewrites,
134
+ ...stagedMaintenanceHistory(plan),
103
135
  renames: [...plan.placement.renameMap.entries()].map(([from, entry]) => ({
104
136
  from,
105
137
  to: entry.newFilename,
@@ -212,16 +244,27 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
212
244
  name: displayName,
213
245
  description: options.description ?? '',
214
246
  ownerRewrites: findOwnerRewriteHolders(manifest.patches, source.filename, movedSet),
215
- // Populated by runProjectedSplitLint below (forward edges into the new patch).
247
+ // Populated by runProjectedSplitLint below (forward edges into the new
248
+ // patch, and the declarations the move re-homes or makes unused).
216
249
  stagedDependencyAdditions: new Map(),
250
+ stagedMaintenance: emptyStagedMaintenance(),
217
251
  };
252
+ // Policy first: the projected manifest (orders, ranges, the gap rule)
253
+ // does not depend on the lint below, so a refused --order fails before
254
+ // the per-patch and projected lint passes instead of after them.
255
+ enforcePatchPolicy({
256
+ config,
257
+ manifest: projectSplitManifest(manifest, plan, buildNewPatchMetadata(plan, config)),
258
+ command: 'patch split',
259
+ forceUnsafe: options.forceUnsafe === true,
260
+ });
218
261
  // Per-patch lint both projected bodies, threading the source patch's
219
262
  // tier/lintIgnore so an intentional-advisory patch can still split.
220
263
  // The whole-queue context (built once, with the config for committed-gate
221
264
  // parity) resolves cross-patch imports and sibling head.js harness roots
222
265
  // instead of linting each projected body blind.
223
266
  const patchQueueCtx = await buildPatchQueueContext(paths.patches, config);
224
- const ignoreChecks = source.lintIgnore ? new Set(source.lintIgnore) : undefined;
267
+ const ignoreChecks = buildLintIgnoreChecks(source.lintIgnore);
225
268
  await runPatchLint({
226
269
  engineDir: paths.engine,
227
270
  filesAffected: remainingFiles,
@@ -242,15 +285,10 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
242
285
  ignoreChecks,
243
286
  patchTier: source.tier,
244
287
  });
245
- const { conflicts, stagedDependencyAdditions } = runProjectedSplitLint(plan, patchQueueCtx);
288
+ const { conflicts, stagedDependencyAdditions, stagedMaintenance } = runProjectedSplitLint(plan, patchQueueCtx);
246
289
  plan.stagedDependencyAdditions = stagedDependencyAdditions;
290
+ plan.stagedMaintenance = stagedMaintenance;
247
291
  const newMetadata = buildNewPatchMetadata(plan, config);
248
- enforcePatchPolicy({
249
- config,
250
- manifest: projectSplitManifest(manifest, plan, newMetadata),
251
- command: 'patch split',
252
- forceUnsafe: options.forceUnsafe === true,
253
- });
254
292
  const decision = await confirmDestructive({
255
293
  operation: 'patch-split',
256
294
  title: `Split ${plan.movedFiles.length} file(s) out of ${source.filename} into ${placement.newFilename}`,
@@ -264,9 +302,7 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
264
302
  return;
265
303
  await commitPatchSplit(paths.patches, plan, newMetadata, options, config);
266
304
  success(`Split ${source.filename}: ${placement.newFilename} now owns ${plan.movedFiles.length} file(s)`);
267
- if (plan.ownerRewrites.length > 0) {
268
- info(`Re-pointed staged-dependency owners in: ${plan.ownerRewrites.join(', ')}`);
269
- }
305
+ reportSplitStagedChanges(plan);
270
306
  outro('Split complete');
271
307
  }
272
308
  /**
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Batch input and history delta for `patch staged-dependency`.
3
+ *
4
+ * `--from <json-file>` lets one `--add` call declare many edges under one
5
+ * lock hold. Turning on `patchLint.forwardRegistration: "extended"` on a real
6
+ * queue surfaced hundreds of undeclared registrations, each printed with its
7
+ * own command, and one call per edge was the only way to declare them. The
8
+ * file is either a `lint --per-patch --report` JSON (its findings carry the
9
+ * `stagedDeclarations` that discharge them) or a plain array of
10
+ * declarations.
11
+ *
12
+ * The history record of every call holds only the delta (`added` /
13
+ * `removed`). Recording the patch's whole list before and after grew the
14
+ * log quadratically with the list: 849 single-edge calls took one consumer's
15
+ * log from 36 MB to 184 MB.
16
+ */
17
+ import type { PatchStagedDependencies, PatchStagedForwardImport, PatchStagedRegistration } from '../../types/commands/index.js';
18
+ /** Declarations read from a `--from` file for one target patch. */
19
+ export interface StagedDependencyBatch {
20
+ forwardImports: PatchStagedForwardImport[];
21
+ registrations: PatchStagedRegistration[];
22
+ /** Entries naming a different patch, left out. */
23
+ skippedForOtherPatches: number;
24
+ }
25
+ /**
26
+ * Reads the declarations a `--from` file supplies for `targetFilename`.
27
+ *
28
+ * Every entry takes `file`, `creates`, optional `owner` and `reason`, and
29
+ * either `specifier` (an import) or `line` (a registration); `kind` may
30
+ * name the shape explicitly. An entry with a `patch` field naming another
31
+ * patch is skipped and counted, so one report can be applied patch by
32
+ * patch. `defaultReason` (the `--reason` flag) fills entries without one.
33
+ *
34
+ * @param path - The JSON file
35
+ * @param targetFilename - Filename of the patch being edited
36
+ * @param defaultReason - Reason for entries that carry none
37
+ */
38
+ export declare function readStagedDependencyBatch(path: string, targetFilename: string, defaultReason?: string): Promise<StagedDependencyBatch>;
39
+ /**
40
+ * The history delta between two committed `stagedDependencies` values:
41
+ * the declarations the call added and the ones it removed. Each side is
42
+ * omitted when empty. Older log records carry `before`/`after` instead;
43
+ * a reader should accept either.
44
+ */
45
+ export declare function stagedDependencyDelta(before: PatchStagedDependencies | undefined, after: PatchStagedDependencies | undefined): {
46
+ added?: PatchStagedDependencies;
47
+ removed?: PatchStagedDependencies;
48
+ };
@@ -0,0 +1,176 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Batch input and history delta for `patch staged-dependency`.
4
+ *
5
+ * `--from <json-file>` lets one `--add` call declare many edges under one
6
+ * lock hold. Turning on `patchLint.forwardRegistration: "extended"` on a real
7
+ * queue surfaced hundreds of undeclared registrations, each printed with its
8
+ * own command, and one call per edge was the only way to declare them. The
9
+ * file is either a `lint --per-patch --report` JSON (its findings carry the
10
+ * `stagedDeclarations` that discharge them) or a plain array of
11
+ * declarations.
12
+ *
13
+ * The history record of every call holds only the delta (`added` /
14
+ * `removed`). Recording the patch's whole list before and after grew the
15
+ * log quadratically with the list: 849 single-edge calls took one consumer's
16
+ * log from 36 MB to 184 MB.
17
+ */
18
+ import { InvalidArgumentError } from '../../errors/base.js';
19
+ import { toError } from '../../utils/errors.js';
20
+ import { readJson } from '../../utils/fs.js';
21
+ import { isArray, isObject } from '../../utils/validation.js';
22
+ function optionalString(record, key, label) {
23
+ const value = record[key];
24
+ if (value === undefined)
25
+ return undefined;
26
+ if (typeof value !== 'string' || value.length === 0) {
27
+ throw new InvalidArgumentError(`${label}.${key} must be a non-empty string.`, '--from');
28
+ }
29
+ return value;
30
+ }
31
+ function requiredString(record, key, label) {
32
+ const value = optionalString(record, key, label);
33
+ if (value === undefined) {
34
+ throw new InvalidArgumentError(`${label} is missing "${key}".`, '--from');
35
+ }
36
+ return value;
37
+ }
38
+ /**
39
+ * Collects the declaration records a parsed file holds: a lint report's
40
+ * `stagedDeclarations` (from `queueIssues` and every patch's `issues`), or
41
+ * the elements of a plain array.
42
+ */
43
+ function collectRecords(data) {
44
+ if (isArray(data)) {
45
+ return data.map((entry, index) => {
46
+ if (!isObject(entry)) {
47
+ throw new InvalidArgumentError(`--from entry [${index}] must be an object.`, '--from');
48
+ }
49
+ return { record: entry, label: `--from entry [${index}]` };
50
+ });
51
+ }
52
+ if (!isObject(data)) {
53
+ throw new InvalidArgumentError('--from must hold a JSON array of declarations or a "lint --per-patch --report" document.', '--from');
54
+ }
55
+ const issues = [];
56
+ if (isArray(data['queueIssues']))
57
+ issues.push(...data['queueIssues']);
58
+ if (isArray(data['patches'])) {
59
+ for (const patch of data['patches']) {
60
+ if (isObject(patch) && isArray(patch['issues']))
61
+ issues.push(...patch['issues']);
62
+ }
63
+ }
64
+ const records = [];
65
+ for (const issue of issues) {
66
+ if (!isObject(issue) || !isArray(issue['stagedDeclarations']))
67
+ continue;
68
+ for (const declaration of issue['stagedDeclarations']) {
69
+ if (isObject(declaration)) {
70
+ records.push({
71
+ record: declaration,
72
+ label: `stagedDeclarations of [${String(issue['check'])}] ${String(issue['file'])}`,
73
+ });
74
+ }
75
+ }
76
+ }
77
+ return records;
78
+ }
79
+ /**
80
+ * Reads the declarations a `--from` file supplies for `targetFilename`.
81
+ *
82
+ * Every entry takes `file`, `creates`, optional `owner` and `reason`, and
83
+ * either `specifier` (an import) or `line` (a registration); `kind` may
84
+ * name the shape explicitly. An entry with a `patch` field naming another
85
+ * patch is skipped and counted, so one report can be applied patch by
86
+ * patch. `defaultReason` (the `--reason` flag) fills entries without one.
87
+ *
88
+ * @param path - The JSON file
89
+ * @param targetFilename - Filename of the patch being edited
90
+ * @param defaultReason - Reason for entries that carry none
91
+ */
92
+ export async function readStagedDependencyBatch(path, targetFilename, defaultReason) {
93
+ let data;
94
+ try {
95
+ data = await readJson(path);
96
+ }
97
+ catch (error) {
98
+ throw new InvalidArgumentError(`--from ${path} could not be read as JSON: ${toError(error).message}`, '--from');
99
+ }
100
+ const batch = {
101
+ forwardImports: [],
102
+ registrations: [],
103
+ skippedForOtherPatches: 0,
104
+ };
105
+ for (const { record, label } of collectRecords(data)) {
106
+ const patch = optionalString(record, 'patch', label);
107
+ if (patch !== undefined && patch !== targetFilename) {
108
+ batch.skippedForOtherPatches += 1;
109
+ continue;
110
+ }
111
+ const specifier = optionalString(record, 'specifier', label);
112
+ const line = optionalString(record, 'line', label);
113
+ const kind = record['kind'] ?? (line !== undefined ? 'registration' : 'import');
114
+ if (kind !== 'import' && kind !== 'registration') {
115
+ throw new InvalidArgumentError(`${label}.kind must be "import" or "registration".`, '--from');
116
+ }
117
+ const file = requiredString(record, 'file', label);
118
+ const creates = requiredString(record, 'creates', label);
119
+ const owner = optionalString(record, 'owner', label);
120
+ const reason = optionalString(record, 'reason', label) ?? defaultReason;
121
+ const extras = {
122
+ ...(owner !== undefined ? { owner } : {}),
123
+ ...(reason !== undefined ? { reason } : {}),
124
+ };
125
+ if (kind === 'import') {
126
+ if (specifier === undefined || line !== undefined) {
127
+ throw new InvalidArgumentError(`${label} is an import declaration and needs "specifier" (not "line").`, '--from');
128
+ }
129
+ batch.forwardImports.push({ file, specifier, creates, ...extras });
130
+ }
131
+ else {
132
+ if (line === undefined || specifier !== undefined) {
133
+ throw new InvalidArgumentError(`${label} is a registration declaration and needs "line" (not "specifier").`, '--from');
134
+ }
135
+ batch.registrations.push({ file, line, creates, ...extras });
136
+ }
137
+ }
138
+ return batch;
139
+ }
140
+ function entryKey(entry) {
141
+ return JSON.stringify(entry);
142
+ }
143
+ function difference(left, right) {
144
+ const keys = new Set((right ?? []).map(entryKey));
145
+ return (left ?? []).filter((entry) => !keys.has(entryKey(entry)));
146
+ }
147
+ function nonEmpty(staged) {
148
+ const forwardImports = staged.forwardImports ?? [];
149
+ const registrations = staged.registrations ?? [];
150
+ if (forwardImports.length === 0 && registrations.length === 0)
151
+ return undefined;
152
+ return {
153
+ ...(forwardImports.length > 0 ? { forwardImports } : {}),
154
+ ...(registrations.length > 0 ? { registrations } : {}),
155
+ };
156
+ }
157
+ /**
158
+ * The history delta between two committed `stagedDependencies` values:
159
+ * the declarations the call added and the ones it removed. Each side is
160
+ * omitted when empty. Older log records carry `before`/`after` instead;
161
+ * a reader should accept either.
162
+ */
163
+ export function stagedDependencyDelta(before, after) {
164
+ const added = nonEmpty({
165
+ forwardImports: difference(after?.forwardImports, before?.forwardImports),
166
+ registrations: difference(after?.registrations, before?.registrations),
167
+ });
168
+ const removed = nonEmpty({
169
+ forwardImports: difference(before?.forwardImports, after?.forwardImports),
170
+ registrations: difference(before?.registrations, after?.registrations),
171
+ });
172
+ return {
173
+ ...(added !== undefined ? { added } : {}),
174
+ ...(removed !== undefined ? { removed } : {}),
175
+ };
176
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Kind-neutral views and list operations over staged-dependency
3
+ * declarations, shared by `patch staged-dependency` and its `--from` batch
4
+ * path. Split out of `staged-dependency.ts` for the per-file line budget.
5
+ */
6
+ import type { PatchStagedDependencies, PatchStagedForwardImport, PatchStagedRegistration } from '../../types/commands/index.js';
7
+ /** The command's mutation mode. */
8
+ export type StagedDependencyMode = 'add' | 'remove' | 'clear';
9
+ /** The declaration shape (`--kind`). */
10
+ export type StagedDependencyKind = 'import' | 'registration';
11
+ /**
12
+ * One staged-dependency entry of either kind. The `matcher` field is the
13
+ * kind-specific middle term: the import specifier for import entries, the
14
+ * registration line for registration entries. Key/removal/label logic is
15
+ * shared across kinds through this shape.
16
+ */
17
+ export interface StagedEntryView {
18
+ file: string;
19
+ matcher: string;
20
+ creates: string;
21
+ owner?: string;
22
+ reason?: string;
23
+ }
24
+ /** View of an import declaration. */
25
+ export declare function importView(dependency: PatchStagedForwardImport): StagedEntryView;
26
+ /** View of a registration declaration. */
27
+ export declare function registrationView(dependency: PatchStagedRegistration): StagedEntryView;
28
+ /** One-line operator label for a declaration. */
29
+ export declare function dependencyLabel(kind: StagedDependencyKind, view: StagedEntryView): string;
30
+ /**
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.
33
+ */
34
+ export declare function applyMode<T>(existing: readonly T[], mode: StagedDependencyMode, target: StagedEntryView | undefined, toView: (entry: T) => StagedEntryView, addition: T | undefined): T[];
35
+ /** Number of declarations of both kinds. */
36
+ export declare function countStagedEntries(staged: PatchStagedDependencies | undefined): number;
@@ -0,0 +1,52 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Kind-neutral views and list operations over staged-dependency
4
+ * declarations, shared by `patch staged-dependency` and its `--from` batch
5
+ * path. Split out of `staged-dependency.ts` for the per-file line budget.
6
+ */
7
+ /** View of an import declaration. */
8
+ export function importView(dependency) {
9
+ return { ...dependency, matcher: dependency.specifier };
10
+ }
11
+ /** View of a registration declaration. */
12
+ export function registrationView(dependency) {
13
+ return { ...dependency, matcher: dependency.line };
14
+ }
15
+ function dependencyKey(view) {
16
+ return [view.file, view.matcher, view.creates, view.owner ?? ''].join('\0');
17
+ }
18
+ /** One-line operator label for a declaration. */
19
+ export function dependencyLabel(kind, view) {
20
+ const owner = view.owner ? ` owner=${view.owner}` : '';
21
+ const reason = view.reason ? ` reason="${view.reason}"` : '';
22
+ if (kind === 'import') {
23
+ return `${view.file} imports "${view.matcher}" from ${view.creates}${owner}${reason}`;
24
+ }
25
+ return `${view.file} registers "${view.matcher}" for ${view.creates}${owner}${reason}`;
26
+ }
27
+ function matchesTarget(view, target) {
28
+ return (view.file === target.file &&
29
+ view.matcher === target.matcher &&
30
+ view.creates === target.creates &&
31
+ (target.owner === undefined || view.owner === target.owner));
32
+ }
33
+ /**
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.
36
+ */
37
+ export function applyMode(existing, mode, target, toView, addition) {
38
+ if (mode === 'clear')
39
+ return [];
40
+ if (!target)
41
+ return [...existing];
42
+ if (mode === 'remove')
43
+ 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)
46
+ return [...existing];
47
+ return [...existing, addition];
48
+ }
49
+ /** Number of declarations of both kinds. */
50
+ export function countStagedEntries(staged) {
51
+ return (staged?.forwardImports?.length ?? 0) + (staged?.registrations?.length ?? 0);
52
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `patch staged-dependency --add --from <json-file>`: adds every
3
+ * declaration a file supplies for one patch in a single manifest update
4
+ * under one lock hold. The file format and the history delta live in
5
+ * `staged-dependency-batch.ts`.
6
+ */
7
+ import type { PatchStagedDependencyOptions } from '../../types/commands/index.js';
8
+ import { type StagedDependencyMode } from './staged-dependency-entries.js';
9
+ /**
10
+ * `--add --from <json-file>`: every declaration the file supplies for the
11
+ * target patch, validated up front and written in one manifest update
12
+ * under one lock hold, with one delta history record.
13
+ */
14
+ export declare function runBatchAdd(projectRoot: string, identifier: string, mode: StagedDependencyMode, options: PatchStagedDependencyOptions): Promise<void>;
@@ -0,0 +1,108 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * `patch staged-dependency --add --from <json-file>`: adds every
4
+ * declaration a file supplies for one patch in a single manifest update
5
+ * under one lock hold. The file format and the history delta live in
6
+ * `staged-dependency-batch.ts`.
7
+ */
8
+ import { appendHistoryBestEffort } from '../../core/history-log.js';
9
+ import { mutatePatchMetadata } from '../../core/patch-export.js';
10
+ import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
11
+ import { info, outro } from '../../utils/logger.js';
12
+ import { resolveWaitLockSeconds } from '../../utils/options.js';
13
+ import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
14
+ import { readStagedDependencyBatch, stagedDependencyDelta, } from './staged-dependency-batch.js';
15
+ import { applyMode, countStagedEntries, importView, registrationView, } from './staged-dependency-entries.js';
16
+ import { validateStagedDependencyAdd } from './staged-dependency-validate.js';
17
+ /** Flags that describe one declaration and so cannot combine with `--from`. */
18
+ const SINGLE_DECLARATION_FLAGS = [
19
+ ['--kind', 'kind'],
20
+ ['--file', 'file'],
21
+ ['--specifier', 'specifier'],
22
+ ['--line', 'line'],
23
+ ['--creates', 'creates'],
24
+ ['--owner', 'owner'],
25
+ ];
26
+ /** Adds every `--from` declaration not already present, keyed like a single `--add`. */
27
+ function mergeBatch(staged, batch) {
28
+ let forwardImports = [...(staged?.forwardImports ?? [])];
29
+ for (const entry of batch.forwardImports) {
30
+ forwardImports = applyMode(forwardImports, 'add', importView(entry), importView, entry);
31
+ }
32
+ let registrations = [...(staged?.registrations ?? [])];
33
+ for (const entry of batch.registrations) {
34
+ registrations = applyMode(registrations, 'add', registrationView(entry), registrationView, entry);
35
+ }
36
+ return {
37
+ ...(forwardImports.length > 0 ? { forwardImports } : {}),
38
+ ...(registrations.length > 0 ? { registrations } : {}),
39
+ };
40
+ }
41
+ /**
42
+ * `--add --from <json-file>`: every declaration the file supplies for the
43
+ * target patch, validated up front and written in one manifest update
44
+ * under one lock hold, with one delta history record.
45
+ */
46
+ export async function runBatchAdd(projectRoot, identifier, mode, options) {
47
+ const from = options.from ?? '';
48
+ if (mode !== 'add') {
49
+ throw new InvalidArgumentError('--from only applies to --add.', '--from');
50
+ }
51
+ const clashing = SINGLE_DECLARATION_FLAGS.filter(([, key]) => options[key] !== undefined);
52
+ if (clashing.length > 0) {
53
+ throw new InvalidArgumentError(`--from supplies the declarations itself; drop ${clashing.map(([flag]) => flag).join(', ')}.`, '--from');
54
+ }
55
+ const { paths, manifest } = await requirePatchQueue(projectRoot);
56
+ const targetPatch = requirePatchTarget(identifier, manifest.patches);
57
+ const batch = await readStagedDependencyBatch(from, targetPatch.filename, options.reason);
58
+ for (const entry of [...batch.forwardImports, ...batch.registrations]) {
59
+ validateStagedDependencyAdd(entry, manifest.patches);
60
+ }
61
+ const supplied = batch.forwardImports.length + batch.registrations.length;
62
+ if (batch.skippedForOtherPatches > 0) {
63
+ info(`Skipped ${batch.skippedForOtherPatches} declaration(s) in ${from} that name another patch.`);
64
+ }
65
+ if (supplied === 0) {
66
+ info(`${from} holds no declarations for ${targetPatch.filename}; nothing to add.`);
67
+ outro('Patch staged-dependency complete');
68
+ return;
69
+ }
70
+ 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
+ : '');
76
+ if (options.dryRun === true) {
77
+ info(`[dry-run] ${targetPatch.filename}: ${describe(countStagedEntries(existing), projectedCount)}.`);
78
+ outro('Dry run complete — no changes made');
79
+ return;
80
+ }
81
+ const result = await mutatePatchMetadata(paths.patches, targetPatch.filename, (current) => {
82
+ const after = mergeBatch(current.stagedDependencies, batch);
83
+ if (countStagedEntries(after) === 0)
84
+ return { unset: ['stagedDependencies'] };
85
+ return { set: { stagedDependencies: after } };
86
+ }, {
87
+ waitLockSeconds: resolveWaitLockSeconds(options.waitLock),
88
+ command: 'patch staged-dependency',
89
+ });
90
+ if (!result) {
91
+ throw new GeneralError(`Patch ${targetPatch.filename} disappeared from the manifest during the update. Re-run after investigating.`);
92
+ }
93
+ const before = result.before.stagedDependencies;
94
+ const after = result.after.stagedDependencies;
95
+ info(`${targetPatch.filename}: ${describe(countStagedEntries(before), countStagedEntries(after))}.`);
96
+ await appendHistoryBestEffort(paths.patches, {
97
+ operation: 'patch-staged-dependency',
98
+ args: {
99
+ filename: targetPatch.filename,
100
+ mode,
101
+ from,
102
+ ...stagedDependencyDelta(before, after),
103
+ },
104
+ ...(options.yes === true ? { yes: true } : {}),
105
+ result: 'ok',
106
+ }, `patch staged-dependency committed (${targetPatch.filename})`);
107
+ outro('Patch staged-dependency complete');
108
+ }