@tacuchi/agent-workflow-cli 25.8.0 → 25.9.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 (502) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +5 -1
  3. package/dist/adapters/git-cli.js +225 -17
  4. package/dist/adapters/git-cli.js.map +1 -1
  5. package/dist/adapters/node-file-system.js +48 -0
  6. package/dist/adapters/node-file-system.js.map +1 -1
  7. package/dist/application/amend-service.js +51 -14
  8. package/dist/application/amend-service.js.map +1 -1
  9. package/dist/application/branch-resolver.js +6 -3
  10. package/dist/application/branch-resolver.js.map +1 -1
  11. package/dist/application/capability/design-handler.js +124 -43
  12. package/dist/application/capability/design-handler.js.map +1 -1
  13. package/dist/application/capability/dispatcher.js +4 -1
  14. package/dist/application/capability/dispatcher.js.map +1 -1
  15. package/dist/application/capability/durable-effect.js +3 -0
  16. package/dist/application/capability/durable-effect.js.map +1 -1
  17. package/dist/application/check-branch-service.js +158 -19
  18. package/dist/application/check-branch-service.js.map +1 -1
  19. package/dist/application/checkpoint/markdown.js +2 -2
  20. package/dist/application/checkpoint/markdown.js.map +1 -1
  21. package/dist/application/checkpoint/state-reader.js +12 -0
  22. package/dist/application/checkpoint/state-reader.js.map +1 -1
  23. package/dist/application/checkpoint-service.js +12 -4
  24. package/dist/application/checkpoint-service.js.map +1 -1
  25. package/dist/application/checkpoint-write-service.js +91 -17
  26. package/dist/application/checkpoint-write-service.js.map +1 -1
  27. package/dist/application/claims-ledger.js +137 -11
  28. package/dist/application/claims-ledger.js.map +1 -1
  29. package/dist/application/claims-recovery.js +85 -44
  30. package/dist/application/claims-recovery.js.map +1 -1
  31. package/dist/application/database-tool-catalog.js +33 -0
  32. package/dist/application/database-tool-catalog.js.map +1 -1
  33. package/dist/application/design/consumer-document.js +2 -2
  34. package/dist/application/design/consumer-document.js.map +1 -1
  35. package/dist/application/design/design-content-gate-service.js +20 -2
  36. package/dist/application/design/design-content-gate-service.js.map +1 -1
  37. package/dist/application/design/design-gate-service.js +2 -0
  38. package/dist/application/design/design-gate-service.js.map +1 -1
  39. package/dist/application/design/design-index-service.js +4 -0
  40. package/dist/application/design/design-index-service.js.map +1 -1
  41. package/dist/application/design/design-publish-service.js +1 -4
  42. package/dist/application/design/design-publish-service.js.map +1 -1
  43. package/dist/application/design/design-record-service.js +13 -7
  44. package/dist/application/design/design-record-service.js.map +1 -1
  45. package/dist/application/design/design-resolver-service.js +22 -3
  46. package/dist/application/design/design-resolver-service.js.map +1 -1
  47. package/dist/application/design/design-simple-service.js +24 -5
  48. package/dist/application/design/design-simple-service.js.map +1 -1
  49. package/dist/application/design/digest.js +41 -0
  50. package/dist/application/design/digest.js.map +1 -0
  51. package/dist/application/dev-only-services.js +166 -20
  52. package/dist/application/dev-only-services.js.map +1 -1
  53. package/dist/application/doc-branch-ledger.js +145 -0
  54. package/dist/application/doc-branch-ledger.js.map +1 -0
  55. package/dist/application/docs-canon-service.js +1 -1
  56. package/dist/application/docs-canon-service.js.map +1 -1
  57. package/dist/application/doctor/dist-integrity.js +113 -0
  58. package/dist/application/doctor/dist-integrity.js.map +1 -0
  59. package/dist/application/doctor/hosts.js +5 -2
  60. package/dist/application/doctor/hosts.js.map +1 -1
  61. package/dist/application/doctor/prepare.js +4 -1
  62. package/dist/application/doctor/prepare.js.map +1 -1
  63. package/dist/application/doctor/provider-installation.js +39 -19
  64. package/dist/application/doctor/provider-installation.js.map +1 -1
  65. package/dist/application/doctor/provider-mcps.js +9 -1
  66. package/dist/application/doctor/provider-mcps.js.map +1 -1
  67. package/dist/application/doctor/provider-skills.js +25 -0
  68. package/dist/application/doctor/provider-skills.js.map +1 -1
  69. package/dist/application/doctor/provider-visibility.js +76 -3
  70. package/dist/application/doctor/provider-visibility.js.map +1 -1
  71. package/dist/application/doctor/provider-workspace-block.js +156 -0
  72. package/dist/application/doctor/provider-workspace-block.js.map +1 -0
  73. package/dist/application/doctor/repair-runner.js +47 -3
  74. package/dist/application/doctor/repair-runner.js.map +1 -1
  75. package/dist/application/doctor/report.js +24 -4
  76. package/dist/application/doctor/report.js.map +1 -1
  77. package/dist/application/doctor/skills-toml-migrate.js +76 -0
  78. package/dist/application/doctor/skills-toml-migrate.js.map +1 -0
  79. package/dist/application/export-catalog-check.js +200 -0
  80. package/dist/application/export-catalog-check.js.map +1 -0
  81. package/dist/application/export-service.js +716 -79
  82. package/dist/application/export-service.js.map +1 -1
  83. package/dist/application/fix-git-service.js +285 -64
  84. package/dist/application/fix-git-service.js.map +1 -1
  85. package/dist/application/flow/advance.js +202 -25
  86. package/dist/application/flow/advance.js.map +1 -1
  87. package/dist/application/flow/annul-service.js +3 -1
  88. package/dist/application/flow/annul-service.js.map +1 -1
  89. package/dist/application/flow/batch-commit.js +189 -0
  90. package/dist/application/flow/batch-commit.js.map +1 -0
  91. package/dist/application/flow/checkout-observation.js +63 -10
  92. package/dist/application/flow/checkout-observation.js.map +1 -1
  93. package/dist/application/flow/close-artifacts.js +105 -0
  94. package/dist/application/flow/close-artifacts.js.map +1 -0
  95. package/dist/application/flow/close-at-boundary.js +25 -14
  96. package/dist/application/flow/close-at-boundary.js.map +1 -1
  97. package/dist/application/flow/close-document-guidance.js +70 -0
  98. package/dist/application/flow/close-document-guidance.js.map +1 -0
  99. package/dist/application/flow/flow-service.js +46 -3
  100. package/dist/application/flow/flow-service.js.map +1 -1
  101. package/dist/application/flow/internal-actions.js +300 -82
  102. package/dist/application/flow/internal-actions.js.map +1 -1
  103. package/dist/application/flow/internal-drive.js +24 -1
  104. package/dist/application/flow/internal-drive.js.map +1 -1
  105. package/dist/application/flow/plan-entry.js +82 -0
  106. package/dist/application/flow/plan-entry.js.map +1 -0
  107. package/dist/application/flow/prove.js +32 -1
  108. package/dist/application/flow/prove.js.map +1 -1
  109. package/dist/application/flow/quick-checkouts.js +48 -0
  110. package/dist/application/flow/quick-checkouts.js.map +1 -0
  111. package/dist/application/flow/reinfer-batch.js +177 -0
  112. package/dist/application/flow/reinfer-batch.js.map +1 -0
  113. package/dist/application/flow/reopen-run.js +12 -5
  114. package/dist/application/flow/reopen-run.js.map +1 -1
  115. package/dist/application/flow/run-state-service.js +2 -0
  116. package/dist/application/flow/run-state-service.js.map +1 -1
  117. package/dist/application/flow/submit.js +583 -116
  118. package/dist/application/flow/submit.js.map +1 -1
  119. package/dist/application/generate-launch-service.js +16 -6
  120. package/dist/application/generate-launch-service.js.map +1 -1
  121. package/dist/application/git-flow-service.js +34 -3
  122. package/dist/application/git-flow-service.js.map +1 -1
  123. package/dist/application/history-publications.js +24 -0
  124. package/dist/application/history-publications.js.map +1 -1
  125. package/dist/application/history-reconcile-service.js +45 -0
  126. package/dist/application/history-reconcile-service.js.map +1 -0
  127. package/dist/application/history-table.js +61 -12
  128. package/dist/application/history-table.js.map +1 -1
  129. package/dist/application/history-update-service.js +25 -4
  130. package/dist/application/history-update-service.js.map +1 -1
  131. package/dist/application/hook-branch-check.js +25 -3
  132. package/dist/application/hook-branch-check.js.map +1 -1
  133. package/dist/application/hook-git-commit-advisor.js +3 -4
  134. package/dist/application/hook-git-commit-advisor.js.map +1 -1
  135. package/dist/application/hook-turn-start.js +51 -0
  136. package/dist/application/hook-turn-start.js.map +1 -0
  137. package/dist/application/host-memory/codex.js +1 -1
  138. package/dist/application/host-memory/codex.js.map +1 -1
  139. package/dist/application/hub-registry.js +135 -0
  140. package/dist/application/hub-registry.js.map +1 -0
  141. package/dist/application/local-proposal.js +72 -19
  142. package/dist/application/local-proposal.js.map +1 -1
  143. package/dist/application/mcp-host-reader.js +1 -1
  144. package/dist/application/mcp-host-reader.js.map +1 -1
  145. package/dist/application/mcp-host-writer.js +16 -10
  146. package/dist/application/mcp-host-writer.js.map +1 -1
  147. package/dist/application/merge-state-service.js +150 -34
  148. package/dist/application/merge-state-service.js.map +1 -1
  149. package/dist/application/multiroot/claude.js +17 -10
  150. package/dist/application/multiroot/claude.js.map +1 -1
  151. package/dist/application/multiroot/codex.js +17 -11
  152. package/dist/application/multiroot/codex.js.map +1 -1
  153. package/dist/application/multiroot-service.js +52 -28
  154. package/dist/application/multiroot-service.js.map +1 -1
  155. package/dist/application/parsers/execution-batches.js +64 -0
  156. package/dist/application/parsers/execution-batches.js.map +1 -0
  157. package/dist/application/parsers/phases.js +23 -2
  158. package/dist/application/parsers/phases.js.map +1 -1
  159. package/dist/application/parsers/project-block.js +146 -17
  160. package/dist/application/parsers/project-block.js.map +1 -1
  161. package/dist/application/parsers/tasks.js +16 -2
  162. package/dist/application/parsers/tasks.js.map +1 -1
  163. package/dist/application/parsers/toml.js +19 -0
  164. package/dist/application/parsers/toml.js.map +1 -0
  165. package/dist/application/paths-service.js +13 -16
  166. package/dist/application/paths-service.js.map +1 -1
  167. package/dist/application/persist-service.js +118 -5
  168. package/dist/application/persist-service.js.map +1 -1
  169. package/dist/application/plan-exec-batch-service.js +162 -22
  170. package/dist/application/plan-exec-batch-service.js.map +1 -1
  171. package/dist/application/plan-exec-decision-service.js +61 -12
  172. package/dist/application/plan-exec-decision-service.js.map +1 -1
  173. package/dist/application/plan-exec-plan-diff.js +105 -0
  174. package/dist/application/plan-exec-plan-diff.js.map +1 -0
  175. package/dist/application/plan-lint-service.js +2 -0
  176. package/dist/application/plan-lint-service.js.map +1 -1
  177. package/dist/application/plan-passes.js +41 -0
  178. package/dist/application/plan-passes.js.map +1 -0
  179. package/dist/application/preparation-receipts.js +58 -0
  180. package/dist/application/preparation-receipts.js.map +1 -0
  181. package/dist/application/project-md-upsert-service.js +191 -52
  182. package/dist/application/project-md-upsert-service.js.map +1 -1
  183. package/dist/application/project-tab-data.js +40 -10
  184. package/dist/application/project-tab-data.js.map +1 -1
  185. package/dist/application/release-data/artifacts.js +15 -7
  186. package/dist/application/release-data/artifacts.js.map +1 -1
  187. package/dist/application/release-data/bundles.js +52 -6
  188. package/dist/application/release-data/bundles.js.map +1 -1
  189. package/dist/application/release-data/common.js +2 -2
  190. package/dist/application/release-data/common.js.map +1 -1
  191. package/dist/application/release-data/rollback.js +7 -0
  192. package/dist/application/release-data/rollback.js.map +1 -0
  193. package/dist/application/release-data/sessions.js +2 -2
  194. package/dist/application/release-data/sessions.js.map +1 -1
  195. package/dist/application/release-pass-ledger.js +1 -1
  196. package/dist/application/render/history-row.js +2 -0
  197. package/dist/application/render/history-row.js.map +1 -1
  198. package/dist/application/render/project-block.js +28 -11
  199. package/dist/application/render/project-block.js.map +1 -1
  200. package/dist/application/reseal-service.js +1 -0
  201. package/dist/application/reseal-service.js.map +1 -1
  202. package/dist/application/resume-service.js +55 -8
  203. package/dist/application/resume-service.js.map +1 -1
  204. package/dist/application/retirement/apply.js +25 -7
  205. package/dist/application/retirement/apply.js.map +1 -1
  206. package/dist/application/retirement/attribution.js +121 -48
  207. package/dist/application/retirement/attribution.js.map +1 -1
  208. package/dist/application/retirement/prepare.js +17 -11
  209. package/dist/application/retirement/prepare.js.map +1 -1
  210. package/dist/application/retirement/preview.js +2 -1
  211. package/dist/application/retirement/preview.js.map +1 -1
  212. package/dist/application/self/codex-plugin.js +1 -1
  213. package/dist/application/self/codex-plugin.js.map +1 -1
  214. package/dist/application/self/host-states.js +3 -3
  215. package/dist/application/self/host-states.js.map +1 -1
  216. package/dist/application/self/install-hooks.js +1 -1
  217. package/dist/application/self/install-hooks.js.map +1 -1
  218. package/dist/application/self/mcp-config.js +19 -11
  219. package/dist/application/self/mcp-config.js.map +1 -1
  220. package/dist/application/self/opencode-plugin.js +1 -0
  221. package/dist/application/self/opencode-plugin.js.map +1 -1
  222. package/dist/application/self/uninstall.js +1 -1
  223. package/dist/application/self/uninstall.js.map +1 -1
  224. package/dist/application/semantic-operation/protocol.js +66 -5
  225. package/dist/application/semantic-operation/protocol.js.map +1 -1
  226. package/dist/application/session-archive-service.js +113 -0
  227. package/dist/application/session-archive-service.js.map +1 -0
  228. package/dist/application/session-binding-service.js +35 -0
  229. package/dist/application/session-binding-service.js.map +1 -1
  230. package/dist/application/session-close-service.js +390 -54
  231. package/dist/application/session-close-service.js.map +1 -1
  232. package/dist/application/session-create-service.js +97 -5
  233. package/dist/application/session-create-service.js.map +1 -1
  234. package/dist/application/session-custody-recorder.js +15 -12
  235. package/dist/application/session-custody-recorder.js.map +1 -1
  236. package/dist/application/session-custody-service.js +1 -0
  237. package/dist/application/session-custody-service.js.map +1 -1
  238. package/dist/application/session-narrative.js +42 -12
  239. package/dist/application/session-narrative.js.map +1 -1
  240. package/dist/application/session-pause-service.js +46 -0
  241. package/dist/application/session-pause-service.js.map +1 -0
  242. package/dist/application/session-resolver.js +36 -11
  243. package/dist/application/session-resolver.js.map +1 -1
  244. package/dist/application/session-resume-service.js +134 -13
  245. package/dist/application/session-resume-service.js.map +1 -1
  246. package/dist/application/sessions-service.js +4 -0
  247. package/dist/application/sessions-service.js.map +1 -1
  248. package/dist/application/sessions-sweep-service.js +77 -0
  249. package/dist/application/sessions-sweep-service.js.map +1 -0
  250. package/dist/application/settle-service.js +3 -1
  251. package/dist/application/settle-service.js.map +1 -1
  252. package/dist/application/skills-resolver-service.js +1 -1
  253. package/dist/application/skills-resolver-service.js.map +1 -1
  254. package/dist/application/source-add-service.js +37 -0
  255. package/dist/application/source-add-service.js.map +1 -0
  256. package/dist/application/source-boundary-policy.js +97 -8
  257. package/dist/application/source-boundary-policy.js.map +1 -1
  258. package/dist/application/source-pipeline.js +68 -0
  259. package/dist/application/source-pipeline.js.map +1 -0
  260. package/dist/application/source-remove-service.js +15 -3
  261. package/dist/application/source-remove-service.js.map +1 -1
  262. package/dist/application/sources-service.js +71 -16
  263. package/dist/application/sources-service.js.map +1 -1
  264. package/dist/application/status-service.js +55 -5
  265. package/dist/application/status-service.js.map +1 -1
  266. package/dist/application/unit-dependencies.js +100 -0
  267. package/dist/application/unit-dependencies.js.map +1 -0
  268. package/dist/application/unit-membership.js +37 -0
  269. package/dist/application/unit-membership.js.map +1 -0
  270. package/dist/application/unit-residue.js +72 -0
  271. package/dist/application/unit-residue.js.map +1 -0
  272. package/dist/application/visibility-doctor-service.js +66 -4
  273. package/dist/application/visibility-doctor-service.js.map +1 -1
  274. package/dist/application/working-branch-service.js +6 -0
  275. package/dist/application/working-branch-service.js.map +1 -1
  276. package/dist/application/workline-graph.js +1 -1
  277. package/dist/application/workline-graph.js.map +1 -1
  278. package/dist/application/workline-index-service.js +118 -17
  279. package/dist/application/workline-index-service.js.map +1 -1
  280. package/dist/application/workspace-commit-service.js +218 -0
  281. package/dist/application/workspace-commit-service.js.map +1 -0
  282. package/dist/application/workspace-init-service.js +64 -34
  283. package/dist/application/workspace-init-service.js.map +1 -1
  284. package/dist/application/workspace-local-config.js +90 -0
  285. package/dist/application/workspace-local-config.js.map +1 -0
  286. package/dist/application/workspace-materialization-service.js +52 -3
  287. package/dist/application/workspace-materialization-service.js.map +1 -1
  288. package/dist/application/workspace-migrate/apply.js +525 -4
  289. package/dist/application/workspace-migrate/apply.js.map +1 -1
  290. package/dist/application/workspace-migrate/markers.js +9 -6
  291. package/dist/application/workspace-migrate/markers.js.map +1 -1
  292. package/dist/application/workspace-migrate/plan.js +97 -5
  293. package/dist/application/workspace-migrate/plan.js.map +1 -1
  294. package/dist/application/workspace-move-service.js +270 -0
  295. package/dist/application/workspace-move-service.js.map +1 -0
  296. package/dist/application/workspace-untrack-service.js +28 -0
  297. package/dist/application/workspace-untrack-service.js.map +1 -0
  298. package/dist/application/worktree-service.js +654 -105
  299. package/dist/application/worktree-service.js.map +1 -1
  300. package/dist/cli/ascii.js +2 -0
  301. package/dist/cli/ascii.js.map +1 -1
  302. package/dist/cli/commands/add-source.js +38 -0
  303. package/dist/cli/commands/add-source.js.map +1 -0
  304. package/dist/cli/commands/amend.js +14 -3
  305. package/dist/cli/commands/amend.js.map +1 -1
  306. package/dist/cli/commands/capability.js +11 -0
  307. package/dist/cli/commands/capability.js.map +1 -1
  308. package/dist/cli/commands/designs.js +14 -5
  309. package/dist/cli/commands/designs.js.map +1 -1
  310. package/dist/cli/commands/dev-only.js +19 -6
  311. package/dist/cli/commands/dev-only.js.map +1 -1
  312. package/dist/cli/commands/doc-branch.js +184 -0
  313. package/dist/cli/commands/doc-branch.js.map +1 -0
  314. package/dist/cli/commands/doctor.js +5 -1
  315. package/dist/cli/commands/doctor.js.map +1 -1
  316. package/dist/cli/commands/export.js +78 -18
  317. package/dist/cli/commands/export.js.map +1 -1
  318. package/dist/cli/commands/fix-git.js +89 -14
  319. package/dist/cli/commands/fix-git.js.map +1 -1
  320. package/dist/cli/commands/flow.js +120 -15
  321. package/dist/cli/commands/flow.js.map +1 -1
  322. package/dist/cli/commands/generate-launch.js +14 -2
  323. package/dist/cli/commands/generate-launch.js.map +1 -1
  324. package/dist/cli/commands/history-update.js +1 -1
  325. package/dist/cli/commands/history-update.js.map +1 -1
  326. package/dist/cli/commands/history.js +14 -0
  327. package/dist/cli/commands/history.js.map +1 -0
  328. package/dist/cli/commands/hook.js +15 -2
  329. package/dist/cli/commands/hook.js.map +1 -1
  330. package/dist/cli/commands/index.js +18 -1
  331. package/dist/cli/commands/index.js.map +1 -1
  332. package/dist/cli/commands/merge-state.js +2 -2
  333. package/dist/cli/commands/merge-state.js.map +1 -1
  334. package/dist/cli/commands/persist.js +6 -5
  335. package/dist/cli/commands/persist.js.map +1 -1
  336. package/dist/cli/commands/project-md-upsert.js +5 -3
  337. package/dist/cli/commands/project-md-upsert.js.map +1 -1
  338. package/dist/cli/commands/release-pass.js +3 -3
  339. package/dist/cli/commands/release-pass.js.map +1 -1
  340. package/dist/cli/commands/resume.js +12 -3
  341. package/dist/cli/commands/resume.js.map +1 -1
  342. package/dist/cli/commands/self.js +7 -2
  343. package/dist/cli/commands/self.js.map +1 -1
  344. package/dist/cli/commands/session-close.js +64 -9
  345. package/dist/cli/commands/session-close.js.map +1 -1
  346. package/dist/cli/commands/session-create.js +4 -2
  347. package/dist/cli/commands/session-create.js.map +1 -1
  348. package/dist/cli/commands/session-pause.js +14 -0
  349. package/dist/cli/commands/session-pause.js.map +1 -0
  350. package/dist/cli/commands/sessions.js +16 -3
  351. package/dist/cli/commands/sessions.js.map +1 -1
  352. package/dist/cli/commands/set-branch.js +28 -4
  353. package/dist/cli/commands/set-branch.js.map +1 -1
  354. package/dist/cli/commands/set-edit-mode.js +20 -0
  355. package/dist/cli/commands/set-edit-mode.js.map +1 -0
  356. package/dist/cli/commands/set-pipeline.js +35 -0
  357. package/dist/cli/commands/set-pipeline.js.map +1 -0
  358. package/dist/cli/commands/sources.js +4 -0
  359. package/dist/cli/commands/sources.js.map +1 -1
  360. package/dist/cli/commands/status.js +35 -2
  361. package/dist/cli/commands/status.js.map +1 -1
  362. package/dist/cli/commands/unknown-flags.js +1 -0
  363. package/dist/cli/commands/unknown-flags.js.map +1 -1
  364. package/dist/cli/commands/visibility.js +1 -0
  365. package/dist/cli/commands/visibility.js.map +1 -1
  366. package/dist/cli/commands/workspace-commit.js +62 -0
  367. package/dist/cli/commands/workspace-commit.js.map +1 -0
  368. package/dist/cli/commands/workspace-init.js +39 -2
  369. package/dist/cli/commands/workspace-init.js.map +1 -1
  370. package/dist/cli/commands/workspace-migrate.js +31 -3
  371. package/dist/cli/commands/workspace-migrate.js.map +1 -1
  372. package/dist/cli/commands/workspace-move.js +42 -0
  373. package/dist/cli/commands/workspace-move.js.map +1 -0
  374. package/dist/cli/commands/worktree.js +4 -4
  375. package/dist/cli/commands/worktree.js.map +1 -1
  376. package/dist/cli/context-id.js +6 -0
  377. package/dist/cli/context-id.js.map +1 -1
  378. package/dist/cli/full-cli.js +555 -0
  379. package/dist/cli/full-cli.js.map +1 -0
  380. package/dist/cli/help-groups.js +75 -16
  381. package/dist/cli/help-groups.js.map +1 -1
  382. package/dist/cli/hook-stdin-cache.js +12 -0
  383. package/dist/cli/hook-stdin-cache.js.map +1 -0
  384. package/dist/cli/main.js +47 -453
  385. package/dist/cli/main.js.map +1 -1
  386. package/dist/cli/parser.js +4 -0
  387. package/dist/cli/parser.js.map +1 -1
  388. package/dist/cli/tui/app.js +1 -1
  389. package/dist/cli/tui/app.js.map +1 -1
  390. package/dist/cli/tui/components/workspace-init-form.js +4 -1
  391. package/dist/cli/tui/components/workspace-init-form.js.map +1 -1
  392. package/dist/cli/tui/data/recommended-skills.js +2 -2
  393. package/dist/cli/tui/data/recommended-skills.js.map +1 -1
  394. package/dist/cli/tui/data/workflow-content.js +6 -1
  395. package/dist/cli/tui/data/workflow-content.js.map +1 -1
  396. package/dist/cli/tui/tabs/mcp-tab.js +4 -3
  397. package/dist/cli/tui/tabs/mcp-tab.js.map +1 -1
  398. package/dist/cli/tui/tabs/project-tab.js +7 -2
  399. package/dist/cli/tui/tabs/project-tab.js.map +1 -1
  400. package/dist/dist-manifest.json +4117 -0
  401. package/dist/domain/capability/protocol.js.map +1 -1
  402. package/dist/domain/decision-note.js +13 -0
  403. package/dist/domain/decision-note.js.map +1 -1
  404. package/dist/domain/design/artifact.js +22 -3
  405. package/dist/domain/design/artifact.js.map +1 -1
  406. package/dist/domain/design/capability.js +1 -0
  407. package/dist/domain/design/capability.js.map +1 -1
  408. package/dist/domain/design/expansion.js +1 -1
  409. package/dist/domain/design/expansion.js.map +1 -1
  410. package/dist/domain/design/maturity.js +16 -0
  411. package/dist/domain/design/maturity.js.map +1 -1
  412. package/dist/domain/design/reference.js +5 -0
  413. package/dist/domain/design/reference.js.map +1 -1
  414. package/dist/domain/doctor/operations.js +28 -2
  415. package/dist/domain/doctor/operations.js.map +1 -1
  416. package/dist/domain/effective-contract.js +3 -1
  417. package/dist/domain/effective-contract.js.map +1 -1
  418. package/dist/domain/elicitation.js +1 -1
  419. package/dist/domain/flow/answer.js +265 -130
  420. package/dist/domain/flow/answer.js.map +1 -1
  421. package/dist/domain/flow/authority.js +193 -34
  422. package/dist/domain/flow/authority.js.map +1 -1
  423. package/dist/domain/flow/batch-review.js +65 -0
  424. package/dist/domain/flow/batch-review.js.map +1 -0
  425. package/dist/domain/flow/decision-schema.js +47 -0
  426. package/dist/domain/flow/decision-schema.js.map +1 -0
  427. package/dist/domain/flow/directive.js +16 -0
  428. package/dist/domain/flow/directive.js.map +1 -1
  429. package/dist/domain/flow/execution-result.js +91 -2
  430. package/dist/domain/flow/execution-result.js.map +1 -1
  431. package/dist/domain/flow/run-state.js +352 -24
  432. package/dist/domain/flow/run-state.js.map +1 -1
  433. package/dist/domain/flow/test-run-evidence.js +285 -0
  434. package/dist/domain/flow/test-run-evidence.js.map +1 -0
  435. package/dist/domain/flow/unchanged-phase.js +15 -0
  436. package/dist/domain/flow/unchanged-phase.js.map +1 -0
  437. package/dist/domain/harnesses.js +14 -5
  438. package/dist/domain/harnesses.js.map +1 -1
  439. package/dist/domain/isolation-unit.js +2 -2
  440. package/dist/domain/isolation-unit.js.map +1 -1
  441. package/dist/domain/lineage.js +7 -2
  442. package/dist/domain/lineage.js.map +1 -1
  443. package/dist/domain/obligation-correspondence.js +4 -2
  444. package/dist/domain/obligation-correspondence.js.map +1 -1
  445. package/dist/domain/plan-handoff.js +5 -0
  446. package/dist/domain/plan-handoff.js.map +1 -0
  447. package/dist/domain/proposal.js +18 -7
  448. package/dist/domain/proposal.js.map +1 -1
  449. package/dist/domain/reservation.js +5 -3
  450. package/dist/domain/reservation.js.map +1 -1
  451. package/dist/domain/retirement/proposal.js +1 -0
  452. package/dist/domain/retirement/proposal.js.map +1 -1
  453. package/dist/domain/session/custody.js +10 -6
  454. package/dist/domain/session/custody.js.map +1 -1
  455. package/dist/domain/session/narrative.js +4 -1
  456. package/dist/domain/session/narrative.js.map +1 -1
  457. package/dist/domain/skills.js +2 -0
  458. package/dist/domain/skills.js.map +1 -1
  459. package/dist/domain/sql-lexer.js +1 -1
  460. package/dist/domain/sql-lexer.js.map +1 -1
  461. package/dist/runtime/namespace-resolver.js +4 -0
  462. package/dist/runtime/namespace-resolver.js.map +1 -1
  463. package/dist/runtime/workline-marker.js +35 -8
  464. package/dist/runtime/workline-marker.js.map +1 -1
  465. package/dist/runtime/workspace-resolution.js +123 -0
  466. package/dist/runtime/workspace-resolution.js.map +1 -0
  467. package/package.json +5 -4
  468. package/skills/w/SKILL.md +3 -1
  469. package/skills/w/artifacts/artifacts-core/CHECKPOINT.md +1 -1
  470. package/skills/w/artifacts/artifacts-core/SCRIPTS.rollback.sql +8 -0
  471. package/skills/w/artifacts/artifacts-core/SCRIPTS.sql +4 -8
  472. package/skills/w/commands/export-diagrams.md +3 -3
  473. package/skills/w/commands/export-manuals.md +6 -6
  474. package/skills/w/commands/export-reports.md +3 -3
  475. package/skills/w/commands/export-scripts.md +5 -5
  476. package/skills/w/commands/fix-git.md +12 -13
  477. package/skills/w/commands/persist.md +2 -2
  478. package/skills/w/commands/plan-exec.md +4 -2
  479. package/skills/w/commands/spec-new.md +1 -1
  480. package/skills/w/commands/workspace-init.md +2 -0
  481. package/skills/w/context/MANIFEST.json +1 -1
  482. package/skills/w/exports/export-diagrams/EXPORT.md +3 -3
  483. package/skills/w/exports/export-manuals/EXPORT.md +14 -16
  484. package/skills/w/exports/export-reports/EXPORT.md +3 -3
  485. package/skills/w/exports/export-scripts/EXPORT.md +45 -21
  486. package/skills/w/harness/HARNESS.md +2 -2
  487. package/skills/w/hooks/hooks.template.json +6 -4
  488. package/skills/w/loops/CHASSIS.md +11 -5
  489. package/skills/w/loops/CODE-POLICIES.md +7 -4
  490. package/skills/w/loops/plan-exec-loop/LOOP.md +23 -22
  491. package/skills/w/loops/plan-new-loop/LOOP.md +23 -31
  492. package/skills/w/loops/plan-refine-loop/LOOP.md +8 -2
  493. package/skills/w/loops/quick-loop/LOOP.md +6 -5
  494. package/skills/w/modules/DESIGN-REFERENCES.md +9 -11
  495. package/skills/w/modules/PLAN-EXECUTION-BATCHES.md +4 -4
  496. package/skills/w/modules/SESSION-NUMBERING.md +2 -0
  497. package/skills/w/roles/design/CONTRACT.md +6 -4
  498. package/skills/w/roles/design/ROLE.md +4 -2
  499. package/skills/w/roles/git/ROLE.md +16 -9
  500. package/skills/w/roles/research/ROLE.md +1 -1
  501. package/skills/w/roles/sql/ROLE.md +16 -14
  502. package/skills/w/schemas/design/ui-screen.v1.schema.json +9 -0
@@ -1,14 +1,24 @@
1
- import { basename, relative, sep } from "node:path";
2
- import { CORRELATIVE_SOURCE, isCorrelative } from "../domain/correlative.js";
1
+ import { createHash } from "node:crypto";
2
+ import { basename, isAbsolute, join, relative, sep } from "node:path";
3
+ import { isCorrelative, leadingCorrelative, sameCorrelative } from "../domain/correlative.js";
4
+ import { baseDigest } from "../domain/proposal.js";
5
+ import { FOLDER_RESERVATION_MARKER, reservationMarker } from "../domain/reservation.js";
6
+ import { appendClaimEvent } from "./claims-ledger.js";
3
7
  import { localDateIso } from "./dates.js";
4
8
  import { runNextNumber } from "./dev-only-services.js";
5
9
  import { resolveDocsCanon } from "./docs-canon-service.js";
10
+ import { checkExportCatalog } from "./export-catalog-check.js";
6
11
  import { appendPublications, publicationRows } from "./history-publications.js";
12
+ import { publishedCorrelatives } from "./history-publications.js";
7
13
  import { withCwdLock } from "./lock-service.js";
8
14
  import { runReleaseData } from "./release-data-service.js";
15
+ import { readScriptsArtifacts } from "./release-data/artifacts.js";
16
+ import { listGraduatedBundles, listStandaloneSql } from "./release-data/bundles.js";
17
+ import { collectFilesByExt } from "./release-data/common.js";
9
18
  import { derivePasses, readReleasePasses } from "./release-pass-ledger.js";
10
- import { approvalDigest, buildSemanticRequest, parseSemanticResponse, readEnvelopeScope, } from "./semantic-operation/protocol.js";
19
+ import { approvalDigest, buildSemanticRequest, parseSemanticResponse, readEnvelopeScope, semanticDigest, } from "./semantic-operation/protocol.js";
11
20
  import { publishArtifacts } from "./semantic-operation/publish.js";
21
+ import { resolveSessionTarget } from "./session-resolver.js";
12
22
  export const EXPORT_CATEGORIES = [
13
23
  "diagrams",
14
24
  "manuals",
@@ -20,7 +30,7 @@ export const EXPORT_CATEGORIES = [
20
30
  * command guide. Keep its semantic anchors exported so a small parity guard
21
31
  * catches doctrine that drifts away from what the CLI actually sends.
22
32
  */
23
- export const SCRIPTS_FINAL_STATE_CONTRACT = "Un dossier con 00-ROLLBACK.sql y README.md obligatorios, más los forwards NN-<nombre>.sql numerados de forma continua desde 01. El CLI NUNCA ejecuta SQL. El bundle publica el ESTADO FINAL NETO de la secuencia, no una réplica por sesión: lo que nace y muere dentro de la secuencia se omite; lo migrado va directo a su forma final; lo que el contexto declara retirado se omite aunque ningún script lo elimine. 00-ROLLBACK.sql invierte ese ESTADO FINAL en orden seguro para las dependencias, no el reverso literal de los forwards. Reconciliá contra el código además de las sesiones y la base. Excluí identidades concretas y semillas de prueba; conservá sólo objetos compartidos y necesarios para el estado final. Un bundle previo que entra al origen es MATERIAL A RECONCILIAR, no historia intocable: dos bundles que se contradicen publican el estado final neto resultante, nunca su suma cronológica.";
33
+ export const SCRIPTS_FINAL_STATE_CONTRACT = "Un dossier con README.md y rollback/00-global/00-ROLLBACK.sql obligatorios. Forwards NN-<nombre>.sql en 01-ddl-tablas/, 02-ddl-funciones/, 03-migracion/, 04-inserts/ o 05-grants/, numerados desde 01 dentro de cada carpeta; por cada forward, rollback/<categoría>/NN-<nombre>.rollback.sql. Ningún .sql en la raíz ni directamente en rollback/. El CLI NUNCA ejecuta SQL. El bundle publica el ESTADO FINAL NETO de la secuencia, no una réplica por sesión: lo que nace y muere dentro de la secuencia se omite; lo migrado va directo a su forma final; lo que el contexto declara retirado se omite aunque ningún script lo elimine. 00-ROLLBACK.sql invierte ese ESTADO FINAL en orden seguro para las dependencias, no el reverso literal de los forwards. Reconciliá contra el código además de las sesiones y la base. Excluí identidades concretas y semillas de prueba; conservá sólo objetos compartidos y necesarios para el estado final. Un bundle previo que entra al origen es MATERIAL A RECONCILIAR, no historia intocable: dos bundles que se contradicen publican el estado final neto resultante, nunca su suma cronológica.";
24
34
  export const SCRIPTS_FINAL_STATE_CONTRACT_ANCHORS = [
25
35
  "ESTADO FINAL NETO",
26
36
  "orden seguro para las dependencias",
@@ -54,19 +64,27 @@ const POLICIES = {
54
64
  scripts: {
55
65
  dir: "docs/scripts",
56
66
  shape: "dossier",
57
- required: ["00-ROLLBACK.sql", "README.md"],
58
- extensions: [".sql", ".md"],
67
+ required: ["rollback/00-global/00-ROLLBACK.sql", "README.md"],
68
+ extensions: [".sql", ".md", ".json"],
59
69
  contract: SCRIPTS_FINAL_STATE_CONTRACT,
60
70
  },
61
71
  };
62
- const LIMITS = { max_artifacts: 64, max_artifact_bytes: 512 * 1024 };
63
- const FORWARD_RE = /^(\d{2})-[^/]+\.sql$/;
72
+ const FORWARD_MAX_BYTES = 512 * 1024;
73
+ const LIMITS = { max_artifacts: 64, max_artifact_bytes: 4 * 1024 * 1024 };
74
+ const SCRIPT_CATEGORIES = [
75
+ "01-ddl-tablas",
76
+ "02-ddl-funciones",
77
+ "03-migracion",
78
+ "04-inserts",
79
+ "05-grants",
80
+ ];
81
+ const FORWARD_RE = /^(\d{2})-(?!.*\.rollback\.sql$)[^/]+\.sql$/;
64
82
  const EXPORT_BASES = ["sessions", "bundles", "workspace"];
65
83
  // ── prepare ──────────────────────────────────────────────────────────────────
66
84
  export async function prepareExport(fs, env, paths, category, selection = {},
67
85
  // Injected so the midnight boundary is testable: a preparation that outlives
68
86
  // the day used to rename its own destination on the next stage.
69
- now = () => new Date()) {
87
+ now = () => new Date(), catalog) {
70
88
  const canon = await resolveDocsCanon(fs, paths, EXPORT_CATEGORIES);
71
89
  if (!canon.ok) {
72
90
  return {
@@ -79,15 +97,23 @@ now = () => new Date()) {
79
97
  };
80
98
  }
81
99
  const policy = resolvePolicy(category, canon.canon[category]);
100
+ const existingManuals = category === "manuals" && (await fs.exists(join(paths.workspaceDir(), policy.dir)))
101
+ ? (await fs.list(join(paths.workspaceDir(), policy.dir)))
102
+ .filter((entry) => entry.type === "file" && entry.name.endsWith(".md"))
103
+ .map((entry) => `${policy.dir}/${entry.name}`)
104
+ : [];
82
105
  const resolved = await resolveMaterial(fs, env, paths, category, selection);
83
106
  if (!resolved.ok)
84
107
  return resolved;
85
108
  const material = resolved.value;
109
+ if (category === "scripts") {
110
+ material.sessions = await sessionsWithSql(fs, paths, material.sessions);
111
+ if (materialCount(material) === 0)
112
+ return { ok: false, failure: emptyOrigin(material) };
113
+ }
86
114
  // Pinned when the answer echoed them, derived only on a first preparation:
87
115
  // re-deriving either at `validate` renames the very unit the answer wrote to,
88
116
  // and neither is workspace state that a stale check should be defending.
89
- const next = selection.next ??
90
- (await runNextNumber(fs, env, paths, { directory: policy.dir, dryRun: true })).next;
91
117
  const date = selection.date ?? localDateIso(now());
92
118
  // Rejected HERE and not only when the envelope comes back: `prepare` used to
93
119
  // accept any string, mint `…-export-manuals-lunes` and let the composer be
@@ -103,8 +129,68 @@ now = () => new Date()) {
103
129
  },
104
130
  };
105
131
  }
132
+ if (selection.catalog !== undefined) {
133
+ if (catalog === undefined)
134
+ return {
135
+ ok: false,
136
+ failure: {
137
+ code: "EXPORT_CATALOG_UNAVAILABLE",
138
+ message: `--catalog ${selection.catalog} no tiene catálogo de sólo lectura disponible`,
139
+ action: "revisá la conexión y repetí prepare",
140
+ },
141
+ };
142
+ const read = await catalog.lookupColumns(selection.catalog, ["public"]);
143
+ if (!read.ok)
144
+ return {
145
+ ok: false,
146
+ failure: {
147
+ code: "EXPORT_CATALOG_UNAVAILABLE",
148
+ message: `no se pudo consultar '${selection.catalog}' (${read.code}): ${read.message}`,
149
+ action: "revisá la conexión de sólo lectura y repetí prepare",
150
+ },
151
+ };
152
+ }
153
+ const scriptsMaterial = category === "scripts" ? await sqlMaterial(fs, paths, material) : null;
154
+ const reservationOwner = await exportReservationOwner(fs, paths, category, policy.dir, date, selection, scriptsMaterial);
155
+ if ("failure" in reservationOwner)
156
+ return { ok: false, failure: reservationOwner.failure };
157
+ const owner = selection.reservationOwner ?? reservationOwner.owner;
158
+ const materialDigest = selection.reservationMaterialDigest ?? semanticDigest(scriptsMaterial);
159
+ const marker = owner === null ? null : reservationMarker(owner, materialDigest);
160
+ if (category === "scripts" && selection.next !== undefined) {
161
+ const markerPath = join(paths.workspaceDir(), policy.dir, `${selection.next}-export-scripts-${date}`, FOLDER_RESERVATION_MARKER);
162
+ if (!(await fs.exists(markerPath)) || (await fs.readText(markerPath)) !== marker) {
163
+ return {
164
+ ok: false,
165
+ failure: {
166
+ code: "EXPORT_NUMBER_TAKEN",
167
+ message: `la reserva ${selection.next} no está intacta`,
168
+ action: "volvé a correr prepare sobre el material vigente",
169
+ },
170
+ };
171
+ }
172
+ }
173
+ const next = selection.next ??
174
+ (await runNextNumber(fs, env, paths, {
175
+ directory: policy.dir,
176
+ ...(owner === null
177
+ ? { dryRun: true }
178
+ : {
179
+ claim: {
180
+ name: `export-scripts-${date}`,
181
+ owner,
182
+ folder: true,
183
+ material: materialDigest,
184
+ },
185
+ }),
186
+ })).next;
106
187
  const unit = policy.shape === "dossier" ? `${policy.dir}/${next}-export-${category}-${date}` : policy.dir;
107
188
  const scope = {
189
+ ...(selection.code !== undefined ? { code: selection.code } : {}),
190
+ ...(selection.catalog !== undefined ? { catalog: selection.catalog } : {}),
191
+ ...(owner === null
192
+ ? {}
193
+ : { reservationOwner: owner, reservationMaterialDigest: materialDigest }),
108
194
  ...(selection.sessions !== undefined ? { sessions: selection.sessions } : {}),
109
195
  ...(selection.since !== undefined ? { since: selection.since } : {}),
110
196
  ...(selection.source !== undefined ? { source: selection.source } : {}),
@@ -114,6 +200,16 @@ now = () => new Date()) {
114
200
  date,
115
201
  next,
116
202
  };
203
+ scope.seal = Object.fromEntries(Object.entries(scope).map(([key, value]) => [key, baseDigest(JSON.stringify(value))]));
204
+ const bundleOrigin = category === "scripts" ? await manifestOrigin(fs, paths, material) : undefined;
205
+ const allBundles = category === "scripts" ? await listGraduatedBundles(fs, paths.workspaceDir(), paths) : [];
206
+ const availableBundles = category === "scripts" ? allBundles.map((bundle) => basename(bundle.path)) : undefined;
207
+ const looseSql = category === "scripts" ? await listStandaloneSql(fs, paths.workspaceDir(), paths) : [];
208
+ const coveredSql = new Set(allBundles.flatMap((bundle) => bundle.origin_standalone_sql ?? []));
209
+ const unbundledSql = looseSql.filter((file) => !coveredSql.has(relative(paths.workspaceDir(), file.path).split(sep).join("/")));
210
+ const bundleWarnings = category === "scripts" && selection.environment !== undefined
211
+ ? await environmentWarnings(fs, paths, selection.environment, material.bundles)
212
+ : [];
117
213
  const inventory = {
118
214
  category,
119
215
  destination: unit,
@@ -131,6 +227,14 @@ now = () => new Date()) {
131
227
  excluded: material.excluded,
132
228
  exclude_unmatched: material.unmatched,
133
229
  environment: material.environment,
230
+ ...(bundleOrigin === undefined
231
+ ? {}
232
+ : {
233
+ bundle_origin: bundleOrigin,
234
+ available_bundles: availableBundles,
235
+ unbundled_sql: unbundledSql,
236
+ bundle_warnings: bundleWarnings,
237
+ }),
134
238
  date,
135
239
  };
136
240
  const readSet = materialPaths(material);
@@ -142,22 +246,172 @@ now = () => new Date()) {
142
246
  // — and the folder this workspace publishes to. The scope rides along so an
143
247
  // altered echo cannot pass as the original one.
144
248
  inputs: {
145
- corpus: material.sessions,
146
- bundles: material.bundles,
147
- standalone: material.standalone,
249
+ ...(scriptsMaterial === null
250
+ ? { corpus: material.sessions, bundles: material.bundles, standalone: material.standalone }
251
+ : { sql: scriptsMaterial }),
148
252
  dir: policy.dir,
253
+ ...(category === "manuals" ? { existing_manuals: existingManuals } : {}),
149
254
  scope,
255
+ workspace: paths.workspaceDir(),
150
256
  },
151
257
  sealed: "el material del alcance o el destino declarado de la categoría",
152
258
  scope,
153
- contract: `${policy.contract} Respondé artifacts con paths dentro de ${unit}${policy.overwritable === null ? "" : ` (o exactamente ${policy.overwritable})`}. El NNN es consultivo: el CLI reasigna el número dentro del lock. Copiá 'scope' TAL CUAL en tu respuesta: validate y apply lo leen en vez de re-derivarlo.`,
259
+ contract: `${policy.contract} Cada pieza divisible (incluidos forwards) admite ${FORWARD_MAX_BYTES} B y se divide en más archivos; un informe, README.md, RUNBOOK.md o rollback/00-global/00-ROLLBACK.sql admite hasta ${LIMITS.max_artifact_bytes} B. Respondé artifacts con paths dentro de ${unit}${policy.overwritable === null ? "" : ` (o exactamente ${policy.overwritable})`}. El NNN aprobado es el número publicado: nunca se reasigna en apply. Copiá 'scope' TAL CUAL, incluido scope.seal, en tu respuesta.${category === "scripts" ? " Declará decisions.supersedes y decisions.requires como listas de nombres de bundles existentes; NO incluyas bundle.json: lo genera el CLI con los digests del origen y lo sella en validate." : ""}`,
154
260
  inventory,
155
- allowedDestinations: [unit, ...(policy.overwritable === null ? [] : [policy.overwritable])],
261
+ allowedDestinations: [
262
+ unit,
263
+ ...(category === "manuals"
264
+ ? [policy.dir]
265
+ : policy.overwritable === null
266
+ ? []
267
+ : [policy.overwritable]),
268
+ ],
156
269
  limits: LIMITS,
157
270
  readSet,
158
271
  readSetBytes: readSet.length,
159
272
  });
160
- return { ok: true, value: { category, request, dir: policy.dir, scope, next, unit } };
273
+ return {
274
+ ok: true,
275
+ value: {
276
+ category,
277
+ request,
278
+ dir: policy.dir,
279
+ scope,
280
+ next,
281
+ unit,
282
+ ...(category === "manuals" ? { existingManuals } : {}),
283
+ ...(bundleOrigin === undefined
284
+ ? {}
285
+ : {
286
+ bundleOrigin,
287
+ availableBundles: availableBundles ?? [],
288
+ reservationOwner: owner ?? "",
289
+ reservationMaterialDigest: materialDigest,
290
+ }),
291
+ },
292
+ };
293
+ }
294
+ async function exportReservationOwner(fs, paths, category, dir, date, selection, sql) {
295
+ if (category !== "scripts")
296
+ return { owner: null };
297
+ if (selection.code !== undefined) {
298
+ const session = await resolveSessionTarget(fs, paths, {
299
+ code: selection.code,
300
+ intent: "write",
301
+ });
302
+ if (session.outcome !== "resolved")
303
+ return {
304
+ failure: {
305
+ code: "EXPORT_SCOPE_INVALID",
306
+ message: `--code ${selection.code} no resuelve una sesión activa`,
307
+ action: "seleccioná una sesión activa o quitá --code",
308
+ },
309
+ };
310
+ return { owner: session.session.folder };
311
+ }
312
+ return {
313
+ owner: `operation-${semanticDigest({
314
+ category,
315
+ dir,
316
+ date,
317
+ sql,
318
+ selection: {
319
+ sessions: selection.sessions,
320
+ since: selection.since,
321
+ source: selection.source,
322
+ from: selection.from,
323
+ exclude: selection.exclude,
324
+ environment: selection.environment,
325
+ catalog: selection.catalog,
326
+ date,
327
+ },
328
+ })}`,
329
+ };
330
+ }
331
+ async function manifestOrigin(fs, paths, material) {
332
+ const digest = async (path) => ({
333
+ path,
334
+ digest: `sha256:${createHash("sha256")
335
+ .update(await fs.readBytes(path))
336
+ .digest("hex")}`,
337
+ });
338
+ const sessions = [];
339
+ for (const session of material.sessions) {
340
+ const relativeRoot = session.path ?? session.folder;
341
+ const root = isAbsolute(relativeRoot) ? relativeRoot : join(paths.workspaceDir(), relativeRoot);
342
+ const files = [];
343
+ for (const sql of await readScriptsArtifacts(fs, root)) {
344
+ files.push({ path: sql.name, digest: (await digest(sql.path)).digest });
345
+ }
346
+ sessions.push({
347
+ session: session.folder,
348
+ files: files.sort((a, b) => a.path.localeCompare(b.path)),
349
+ });
350
+ }
351
+ const standalone_sql = await Promise.all(material.standalone.map(async (file) => ({
352
+ path: relative(paths.workspaceDir(), file.path).split(sep).join("/"),
353
+ digest: (await digest(file.path)).digest,
354
+ })));
355
+ const bundles = [];
356
+ for (const bundle of material.bundles) {
357
+ const files = await Promise.all((await collectFilesByExt(fs, bundle.path, "")).map(async (path) => ({
358
+ path: relative(bundle.path, path).split(sep).join("/"),
359
+ digest: (await digest(path)).digest,
360
+ })));
361
+ bundles.push({
362
+ bundle: basename(bundle.path),
363
+ files: files.sort((a, b) => a.path.localeCompare(b.path)),
364
+ });
365
+ }
366
+ return {
367
+ sessions,
368
+ standalone_sql: standalone_sql.sort((a, b) => a.path.localeCompare(b.path)),
369
+ bundles,
370
+ };
371
+ }
372
+ /** Script seals depend on SQL bytes and paths, never on session metadata. */
373
+ async function sessionsWithSql(fs, paths, sessions) {
374
+ const kept = [];
375
+ for (const session of sessions) {
376
+ const root = session.path ?? session.folder;
377
+ if ((await readScriptsArtifacts(fs, isAbsolute(root) ? root : join(paths.workspaceDir(), root)))
378
+ .length > 0)
379
+ kept.push(session);
380
+ }
381
+ return kept;
382
+ }
383
+ async function sqlMaterial(fs, paths, material) {
384
+ const entries = [];
385
+ for (const session of material.sessions) {
386
+ const path = session.path ?? session.folder;
387
+ const root = isAbsolute(path) ? path : join(paths.workspaceDir(), path);
388
+ for (const sql of await readScriptsArtifacts(fs, root)) {
389
+ entries.push({
390
+ origin: session.folder,
391
+ path: sql.name,
392
+ digest: baseDigest(await fs.readText(sql.path)),
393
+ });
394
+ }
395
+ }
396
+ for (const file of material.standalone) {
397
+ entries.push({
398
+ origin: "standalone-sql",
399
+ path: file.path,
400
+ digest: baseDigest(await fs.readText(file.path)),
401
+ });
402
+ }
403
+ for (const bundle of material.bundles) {
404
+ for (const file of await collectFilesByExt(fs, bundle.path, "")) {
405
+ entries.push({
406
+ origin: basename(bundle.path),
407
+ path: relative(bundle.path, file).split(sep).join("/"),
408
+ digest: `sha256:${createHash("sha256")
409
+ .update(await fs.readBytes(file))
410
+ .digest("hex")}`,
411
+ });
412
+ }
413
+ }
414
+ return entries.sort((a, b) => `${a.origin}/${a.path}`.localeCompare(`${b.origin}/${b.path}`));
161
415
  }
162
416
  /** The category's policy with the folder this workspace actually publishes to. */
163
417
  function resolvePolicy(category, dir) {
@@ -180,7 +434,7 @@ function checkComposableSelection(category, selection) {
180
434
  // Belonging comes FIRST: telling a category that does not compose its origin
181
435
  // which bases exist would send it to fix a value that was never going to be
182
436
  // read, and it would be rejected again on the next invocation.
183
- const named = ["from", "exclude", "environment"].filter((key) => selection[key] !== undefined);
437
+ const named = ["from", "exclude", "environment", "code", "catalog"].filter((key) => selection[key] !== undefined);
184
438
  if (category !== "scripts" && named.length > 0) {
185
439
  return {
186
440
  code: "EXPORT_SCOPE_INVALID",
@@ -350,6 +604,29 @@ async function filterByEnvironment(fs, paths, environment, bundles) {
350
604
  },
351
605
  };
352
606
  }
607
+ async function environmentWarnings(fs, paths, environment, bundles) {
608
+ const passes = derivePasses((await readReleasePasses(fs, paths)).events);
609
+ const recorded = new Set(passes
610
+ .filter((pass) => pass.application.axis === "applied" &&
611
+ pass.application.environments.includes(environment))
612
+ .flatMap((pass) => pass.artifacts.map((artifact) => basename(artifact.replaceAll("\\", "/")))));
613
+ const warnings = [];
614
+ for (const bundle of bundles) {
615
+ const name = basename(bundle.path);
616
+ for (const newer of bundle.superseded_by ?? []) {
617
+ warnings.push({ code: "BUNDLE_SUPERSEDED", bundle: name, detail: `superado por ${newer}` });
618
+ }
619
+ for (const requirement of bundle.requires ?? []) {
620
+ if (!recorded.has(requirement))
621
+ warnings.push({
622
+ code: "BUNDLE_REQUIRES_UNRECORDED",
623
+ bundle: name,
624
+ detail: `${requirement} no consta aplicado en ${environment}`,
625
+ });
626
+ }
627
+ }
628
+ return warnings;
629
+ }
353
630
  /** One spelling for a path that two different producers wrote. */
354
631
  function slashed(path) {
355
632
  return path.split(sep).join("/");
@@ -398,6 +675,14 @@ export function readExportScope(raw) {
398
675
  return {
399
676
  ok: true,
400
677
  value: {
678
+ ...(scope.code !== undefined ? { code: scope.code } : {}),
679
+ ...(scope.catalog !== undefined ? { catalog: scope.catalog } : {}),
680
+ ...(scope.reservationOwner !== undefined
681
+ ? { reservationOwner: scope.reservationOwner }
682
+ : {}),
683
+ ...(scope.reservationMaterialDigest !== undefined
684
+ ? { reservationMaterialDigest: scope.reservationMaterialDigest }
685
+ : {}),
401
686
  ...(scope.sessions !== undefined ? { sessions: scope.sessions } : {}),
402
687
  ...(scope.since !== undefined ? { since: scope.since } : {}),
403
688
  ...(scope.source !== undefined ? { source: scope.source } : {}),
@@ -406,11 +691,44 @@ export function readExportScope(raw) {
406
691
  ...(scope.environment !== undefined ? { environment: scope.environment } : {}),
407
692
  date: scope.date,
408
693
  next: scope.next,
694
+ ...(scope.seal !== undefined ? { seal: scope.seal } : {}),
409
695
  },
410
696
  };
411
697
  }
412
698
  /** Why this echo is not the scope `prepare` emitted, or `null` when it is. */
413
699
  function scopeShapeError(scope) {
700
+ const allowed = [
701
+ "code",
702
+ "catalog",
703
+ "reservationOwner",
704
+ "reservationMaterialDigest",
705
+ "sessions",
706
+ "since",
707
+ "source",
708
+ "from",
709
+ "exclude",
710
+ "environment",
711
+ "date",
712
+ "next",
713
+ "seal",
714
+ ];
715
+ const unknown = Object.keys(scope).find((key) => !allowed.includes(key));
716
+ if (unknown !== undefined)
717
+ return `'${unknown}' no es una clave de scope`;
718
+ if (scope.seal !== undefined) {
719
+ if (typeof scope.seal !== "object" || scope.seal === null || Array.isArray(scope.seal))
720
+ return "'seal' debe contener los sellos por clave";
721
+ const seals = scope.seal;
722
+ for (const key of new Set([
723
+ ...Object.keys(scope).filter((k) => k !== "seal"),
724
+ ...Object.keys(seals),
725
+ ])) {
726
+ if (!(key in scope) ||
727
+ typeof seals[key] !== "string" ||
728
+ seals[key] !== baseDigest(JSON.stringify(scope[key])))
729
+ return `'${key}' no coincide con scope.seal`;
730
+ }
731
+ }
414
732
  if (typeof scope.date !== "string" || !DATE_RE.test(scope.date)) {
415
733
  return "'date' tiene que ser YYYY-MM-DD";
416
734
  }
@@ -426,7 +744,15 @@ function scopeShapeError(scope) {
426
744
  if (scope.from !== undefined && !EXPORT_BASES.includes(scope.from)) {
427
745
  return `'from' tiene que ser ${EXPORT_BASES.join(", ")}`;
428
746
  }
429
- for (const key of ["since", "source", "environment"]) {
747
+ for (const key of [
748
+ "since",
749
+ "source",
750
+ "environment",
751
+ "code",
752
+ "catalog",
753
+ "reservationOwner",
754
+ "reservationMaterialDigest",
755
+ ]) {
430
756
  if (scope[key] !== undefined && typeof scope[key] !== "string") {
431
757
  return `'${key}' tiene que ser texto`;
432
758
  }
@@ -440,7 +766,7 @@ function malformedScope(why) {
440
766
  return {
441
767
  ok: false,
442
768
  failure: {
443
- code: "SEMANTIC_RESPONSE_INVALID",
769
+ code: "EXPORT_SCOPE_MISMATCH",
444
770
  message: `el 'scope' del sobre no tiene la forma que prepare emitió: ${why}`,
445
771
  action: "copiá el 'scope' del request TAL CUAL, sin reescribirlo",
446
772
  },
@@ -469,6 +795,8 @@ export function conflictingScopeFlags(echoed, flags) {
469
795
  ["--date", "date"],
470
796
  ["--from", "from"],
471
797
  ["--environment", "environment"],
798
+ ["--code", "code"],
799
+ ["--catalog", "catalog"],
472
800
  ]) {
473
801
  if (flags[key] !== undefined && flags[key] !== echoed[key])
474
802
  conflicts.push(flag);
@@ -477,36 +805,195 @@ export function conflictingScopeFlags(echoed, flags) {
477
805
  }
478
806
  // ── validate ─────────────────────────────────────────────────────────────────
479
807
  export function validateExport(raw, prepared) {
808
+ const echoed = readExportScope(raw);
809
+ if (!echoed.ok)
810
+ return echoed;
811
+ if (echoed.value === null && prepared.scope.seal !== undefined) {
812
+ return {
813
+ ok: false,
814
+ failure: {
815
+ code: "EXPORT_SCOPE_MISMATCH",
816
+ message: "falta 'scope' en el sobre",
817
+ action: "copiá el scope completo del request",
818
+ },
819
+ };
820
+ }
821
+ if (echoed.value !== null) {
822
+ const keys = new Set([...Object.keys(prepared.scope), ...Object.keys(echoed.value)]);
823
+ for (const key of keys) {
824
+ if (JSON.stringify(prepared.scope[key]) !==
825
+ JSON.stringify(echoed.value[key])) {
826
+ return {
827
+ ok: false,
828
+ failure: {
829
+ code: "EXPORT_SCOPE_MISMATCH",
830
+ message: `el alcance cambió en '${key}'`,
831
+ action: "copiá scope y scope.seal del request original sin modificar ninguna clave",
832
+ },
833
+ };
834
+ }
835
+ }
836
+ }
480
837
  const parsed = parseSemanticResponse(raw, prepared.request);
481
838
  if (!parsed.ok)
482
839
  return parsed;
840
+ const assembled = assembleExportArtifacts(parsed.value, prepared);
841
+ if (!assembled.ok)
842
+ return assembled;
483
843
  // From what was prepared, never from the policy table: the folder is the
484
844
  // workspace's, and re-reading it here would let the two stages disagree.
485
845
  const policy = resolvePolicy(prepared.category, prepared.dir);
486
- const artifacts = parsed.value.artifacts ?? [];
846
+ const artifacts = assembled.value;
487
847
  const inUnit = artifacts.filter((a) => a.path !== policy.overwritable);
488
- const overwrites = artifacts.some((a) => a.path === policy.overwritable)
489
- ? policy.overwritable
490
- : null;
491
- const shape = checkShape(inUnit, policy, prepared.unit);
848
+ const manual = prepared.category === "manuals" ? manualShape(artifacts, policy, prepared.unit) : null;
849
+ const replacements = manual === null
850
+ ? []
851
+ : artifacts
852
+ .filter((artifact) => (prepared.existingManuals ?? []).includes(artifact.path))
853
+ .map((artifact) => artifact.path);
854
+ const overwrites = manual === null
855
+ ? artifacts.some((a) => a.path === policy.overwritable)
856
+ ? policy.overwritable
857
+ : null
858
+ : replacements.includes(policy.overwritable ?? "")
859
+ ? policy.overwritable
860
+ : null;
861
+ const shape = manual === null ? checkShape(inUnit, policy, prepared.unit) : manual.failure;
492
862
  if (shape !== null)
493
863
  return { ok: false, failure: shape };
864
+ if (policy.shape === "document" &&
865
+ !(inUnit[0]?.path.startsWith(`${policy.dir}/${prepared.next}-`) &&
866
+ /^[^/]+\.md$/.test(inUnit[0].path.slice(`${policy.dir}/${prepared.next}-`.length)))) {
867
+ return {
868
+ ok: false,
869
+ failure: reject(`el informe debe estar en ${policy.dir}/${prepared.next}-<slug>.md`),
870
+ };
871
+ }
494
872
  return {
495
873
  ok: true,
496
874
  value: {
497
875
  preview: {
498
876
  category: prepared.category,
499
- destination: prepared.unit,
877
+ destination: manual !== null && manual.mode !== "regenerate" ? policy.dir : prepared.unit,
500
878
  files: artifacts.map((a) => ({
501
879
  path: a.path,
502
880
  bytes: Buffer.byteLength(a.content, "utf8"),
503
881
  })),
504
882
  overwrites,
883
+ ...(manual === null ? {} : { mode: manual.mode, replacements }),
884
+ },
885
+ approval_digest: approvalDigest({ ...parsed.value, artifacts }),
886
+ },
887
+ };
888
+ }
889
+ /** When requested, the target catalog is consulted again over the proposed forwards. */
890
+ export async function validateExportWithCatalog(raw, prepared, catalog) {
891
+ const validated = validateExport(raw, prepared);
892
+ if (!validated.ok || prepared.scope.catalog === undefined)
893
+ return validated;
894
+ if (catalog === undefined)
895
+ return {
896
+ ok: false,
897
+ failure: {
898
+ code: "EXPORT_CATALOG_UNAVAILABLE",
899
+ message: `no hay catálogo disponible para '${prepared.scope.catalog}'`,
900
+ action: "revisá la conexión y repetí validate",
901
+ },
902
+ };
903
+ const parsed = parseSemanticResponse(raw, prepared.request);
904
+ if (!parsed.ok)
905
+ return parsed;
906
+ const assembled = assembleExportArtifacts(parsed.value, prepared);
907
+ if (!assembled.ok)
908
+ return assembled;
909
+ const checked = await checkExportCatalog(assembled.value, prepared.unit, prepared.scope.catalog, catalog);
910
+ if (!checked.ok)
911
+ return checked;
912
+ return {
913
+ ok: true,
914
+ value: {
915
+ ...validated.value,
916
+ preview: {
917
+ ...validated.value.preview,
918
+ unverified: checked.value.unverified,
505
919
  },
506
- approval_digest: approvalDigest(parsed.value),
507
920
  },
508
921
  };
509
922
  }
923
+ function manualShape(artifacts, policy, unit) {
924
+ const files = artifacts.filter((artifact) => artifact.path !== policy.overwritable);
925
+ if (files.length === 0)
926
+ return { mode: "complement", failure: null };
927
+ const direct = files.every((artifact) => {
928
+ const name = artifact.path.slice(policy.dir.length + 1);
929
+ return (artifact.path.startsWith(`${policy.dir}/`) &&
930
+ !name.includes("/") &&
931
+ /^[a-z0-9]+(?:-[a-z0-9]+)*\.md$/.test(name));
932
+ });
933
+ if (direct)
934
+ return { mode: "flat", failure: null };
935
+ if (files.every((artifact) => artifact.path.startsWith(`${unit}/`))) {
936
+ return { mode: "regenerate", failure: checkShape(files, policy, unit) };
937
+ }
938
+ return {
939
+ mode: "flat",
940
+ failure: reject("manuals admite sólo INDEX.md, archivos <slug>.md planos o un dossier numerado"),
941
+ };
942
+ }
943
+ function assembleExportArtifacts(response, prepared) {
944
+ const artifacts = response.artifacts ?? [];
945
+ if (prepared.category !== "scripts")
946
+ return { ok: true, value: artifacts };
947
+ if (artifacts.some((a) => a.path === `${prepared.unit}/bundle.json`)) {
948
+ return {
949
+ ok: false,
950
+ failure: reject("bundle.json lo genera el CLI; no lo incluyas en artifacts"),
951
+ };
952
+ }
953
+ const decisions = response.decisions ?? {};
954
+ const bundles = new Set(prepared.availableBundles ?? []);
955
+ const lists = { supersedes: [], requires: [] };
956
+ for (const key of ["supersedes", "requires"]) {
957
+ const value = decisions[key];
958
+ if (!Array.isArray(value) || !value.every((name) => typeof name === "string")) {
959
+ return {
960
+ ok: false,
961
+ failure: reject(`decisions.${key} debe ser una lista de nombres de bundles`),
962
+ };
963
+ }
964
+ const names = value;
965
+ if (names.includes(basename(prepared.unit)))
966
+ return { ok: false, failure: reject(`${key} incluye el bundle que se está publicando`) };
967
+ const missing = names.filter((name) => !bundles.has(name));
968
+ if (missing.length > 0)
969
+ return {
970
+ ok: false,
971
+ failure: reject(`${key}: bundle(s) inexistente(s): ${missing.join(", ")}`),
972
+ };
973
+ if (new Set(names).size !== names.length)
974
+ return { ok: false, failure: reject(`${key} repite un bundle`) };
975
+ lists[key] = [...names].sort();
976
+ }
977
+ const overlap = lists.supersedes.filter((name) => lists.requires.includes(name));
978
+ if (overlap.length > 0)
979
+ return {
980
+ ok: false,
981
+ failure: reject(`no se puede superar y requerir el mismo bundle: ${overlap.join(", ")}`),
982
+ };
983
+ const manifest = {
984
+ version: 1,
985
+ supersedes: lists.supersedes,
986
+ requires: lists.requires,
987
+ origin: prepared.bundleOrigin ?? { sessions: [], standalone_sql: [], bundles: [] },
988
+ };
989
+ return {
990
+ ok: true,
991
+ value: [
992
+ ...artifacts,
993
+ { path: `${prepared.unit}/bundle.json`, content: `${JSON.stringify(manifest, null, 2)}\n` },
994
+ ],
995
+ };
996
+ }
510
997
  function checkShape(artifacts, policy, unit) {
511
998
  if (artifacts.length === 0)
512
999
  return reject("la propuesta no trae ningún artefacto del dossier");
@@ -515,6 +1002,17 @@ function checkShape(artifacts, policy, unit) {
515
1002
  }
516
1003
  const names = artifacts.map((a) => a.path.slice(unit.length + 1));
517
1004
  for (const artifact of artifacts) {
1005
+ const name = artifact.path.slice(unit.length + 1);
1006
+ const indivisible = policy.shape === "document" ||
1007
+ ["README.md", "RUNBOOK.md", "00-ROLLBACK.sql", "rollback/00-global/00-ROLLBACK.sql"].includes(name);
1008
+ const bytes = Buffer.byteLength(artifact.content, "utf8");
1009
+ if (!indivisible && bytes > FORWARD_MAX_BYTES) {
1010
+ return {
1011
+ code: "EXPORT_LIMIT_EXCEEDED",
1012
+ message: `'${artifact.path}' pesa ${bytes} B y el máximo por pieza divisible es ${FORWARD_MAX_BYTES} B`,
1013
+ action: "partí el forward en archivos NN-<nombre>.sql consecutivos y repetí validate",
1014
+ };
1015
+ }
518
1016
  if (!policy.extensions.some((ext) => artifact.path.endsWith(ext))) {
519
1017
  return reject(`'${artifact.path}' no usa una extensión permitida (${policy.extensions.join(", ")})`);
520
1018
  }
@@ -522,30 +1020,119 @@ function checkShape(artifacts, policy, unit) {
522
1020
  return reject(`'${artifact.path}' está vacío`);
523
1021
  }
524
1022
  }
1023
+ if (policy.required.includes("rollback/00-global/00-ROLLBACK.sql")) {
1024
+ return checkScriptsStructure(artifacts, unit);
1025
+ }
525
1026
  for (const required of policy.required) {
526
1027
  if (!names.includes(required))
527
1028
  return reject(`falta '${required}' en el dossier`);
528
1029
  }
529
- return policy.required.includes("00-ROLLBACK.sql") ? checkForwards(names) : null;
530
- }
531
- /** Forwards numbered continuously from 01 — a gap makes the apply order unreadable. */
532
- function checkForwards(names) {
533
- const forwards = names
534
- .map((name) => FORWARD_RE.exec(name)?.[1])
535
- .filter((n) => n !== undefined && n !== "00")
536
- .map((n) => Number.parseInt(n, 10))
537
- .sort((a, b) => a - b);
538
- if (forwards.length === 0)
539
- return reject("el bundle no trae ningún forward NN-<nombre>.sql");
540
- for (let i = 0; i < forwards.length; i++) {
541
- if (forwards[i] !== i + 1) {
542
- return reject(`la numeración de forwards no es continua desde 01: ${forwards.join(", ")}`);
1030
+ return null;
1031
+ }
1032
+ /** Check all structural errors together, including broken citations in delivery instructions. */
1033
+ function checkScriptsStructure(artifacts, unit) {
1034
+ const names = new Set(artifacts.map((a) => a.path.slice(unit.length + 1)));
1035
+ // The CLI generates this file in the next phase; agents must not supply it.
1036
+ names.add("bundle.json");
1037
+ const failures = [
1038
+ ...["README.md", "rollback/00-global/00-ROLLBACK.sql"]
1039
+ .filter((required) => !names.has(required))
1040
+ .map((required) => `falta '${required}' en el dossier`),
1041
+ ...scriptLayoutFailures(names),
1042
+ ...scriptCitationFailures(artifacts, unit, names),
1043
+ ];
1044
+ return failures.length === 0 ? null : reject(failures.join("; "));
1045
+ }
1046
+ function scriptLayoutFailures(names) {
1047
+ const failures = [];
1048
+ const forwards = new Set();
1049
+ const reverses = new Set();
1050
+ for (const name of names) {
1051
+ const classified = classifyScriptPath(name);
1052
+ if (classified.kind === "forward")
1053
+ forwards.add(name);
1054
+ if (classified.kind === "reverse")
1055
+ reverses.add(classified.forward);
1056
+ if (classified.kind === "invalid")
1057
+ failures.push(classified.why);
1058
+ }
1059
+ if (forwards.size === 0)
1060
+ failures.push("el bundle no trae ningún forward NN-<nombre>.sql");
1061
+ for (const forward of forwards) {
1062
+ const inverse = `rollback/${forward.replace(/\.sql$/, ".rollback.sql")}`;
1063
+ if (!names.has(inverse))
1064
+ failures.push(`falta '${inverse}' para '${forward}'`);
1065
+ }
1066
+ for (const reverse of reverses) {
1067
+ if (!forwards.has(reverse))
1068
+ failures.push(`rollback huérfano para '${reverse}'`);
1069
+ }
1070
+ return [...failures, ...scriptNumberingFailures(forwards)];
1071
+ }
1072
+ function classifyScriptPath(path) {
1073
+ if (!path.endsWith(".sql") || path === "rollback/00-global/00-ROLLBACK.sql")
1074
+ return { kind: "other" };
1075
+ if (path.startsWith("rollback/")) {
1076
+ const match = /^rollback\/(01-ddl-tablas|02-ddl-funciones|03-migracion|04-inserts|05-grants)\/(\d{2}-[^/]+)\.rollback\.sql$/.exec(path);
1077
+ return match
1078
+ ? { kind: "reverse", forward: `${match[1]}/${match[2]}.sql` }
1079
+ : { kind: "invalid", why: `'${path}' no es un rollback acoplado válido` };
1080
+ }
1081
+ const [folder, filename, extra] = path.split("/");
1082
+ if (extra === undefined &&
1083
+ filename !== undefined &&
1084
+ SCRIPT_CATEGORIES.includes(folder) &&
1085
+ FORWARD_RE.test(filename))
1086
+ return { kind: "forward" };
1087
+ return {
1088
+ kind: "invalid",
1089
+ why: `'${path}' no es un forward NN-<nombre>.sql en una carpeta de categoría`,
1090
+ };
1091
+ }
1092
+ function scriptNumberingFailures(forwards) {
1093
+ const failures = [];
1094
+ for (const category of SCRIPT_CATEGORIES) {
1095
+ const numbers = [...forwards]
1096
+ .filter((name) => name.startsWith(`${category}/`))
1097
+ .map((name) => Number.parseInt(name.slice(category.length + 1, category.length + 3), 10))
1098
+ .sort((a, b) => a - b);
1099
+ if (numbers.some((n, i) => n !== i + 1))
1100
+ failures.push(`numeración de forwards no continua desde 01 en ${category}: ${numbers.join(", ")}`);
1101
+ }
1102
+ return failures;
1103
+ }
1104
+ function scriptCitationFailures(artifacts, unit, names) {
1105
+ const failures = [];
1106
+ for (const artifact of artifacts) {
1107
+ const name = artifact.path.slice(unit.length + 1);
1108
+ if (name !== "README.md" && !/(^|\/)RUNBOOK\.md$/i.test(name))
1109
+ continue;
1110
+ for (const citation of citedBundleFiles(artifact.content)) {
1111
+ if (!names.has(citation))
1112
+ failures.push(`${name} cita '${citation}' que no existe en el bundle`);
543
1113
  }
544
1114
  }
545
- return null;
1115
+ return failures;
1116
+ }
1117
+ function citedBundleFiles(text) {
1118
+ const names = new Set();
1119
+ for (const match of text.matchAll(/`([^`]+)`|\[[^\]]+\]\(([^)]+)\)/g)) {
1120
+ const raw = match[1] ?? match[2] ?? "";
1121
+ if (/(?:^|\/)\d{3,}-[^/]+\//.test(raw))
1122
+ continue; // another bundle is not an internal citation
1123
+ for (const token of raw.split(/\s+/)) {
1124
+ const candidate = token
1125
+ .trim()
1126
+ .replace(/^['"`]+|['"`,;]+$/g, "")
1127
+ .replace(/^\.\//, "");
1128
+ if (/^(?:[\w-]+\/)*[\w.-]+\.(?:sql|md|json)$/i.test(candidate))
1129
+ names.add(candidate);
1130
+ }
1131
+ }
1132
+ return [...names];
546
1133
  }
547
- export async function applyExport(fs, env, paths, input) {
548
- const validated = validateExport(input.raw, input.prepared);
1134
+ export async function applyExport(fs, _env, paths, input, catalog) {
1135
+ const validated = await validateExportWithCatalog(input.raw, input.prepared, catalog);
549
1136
  if (!validated.ok)
550
1137
  return validated;
551
1138
  if (validated.value.approval_digest !== input.approval) {
@@ -558,12 +1145,14 @@ export async function applyExport(fs, env, paths, input) {
558
1145
  },
559
1146
  };
560
1147
  }
561
- if (validated.value.preview.overwrites !== null && input.allowOverwrite !== true) {
1148
+ const replacing = validated.value.preview.replacements ??
1149
+ (validated.value.preview.overwrites === null ? [] : [validated.value.preview.overwrites]);
1150
+ if (replacing.length > 0 && input.allowOverwrite !== true) {
562
1151
  return {
563
1152
  ok: false,
564
1153
  failure: {
565
1154
  code: "OVERWRITE_NOT_AUTHORIZED",
566
- message: `la propuesta reemplaza '${validated.value.preview.overwrites}'`,
1155
+ message: `la propuesta reemplaza ${replacing.join(", ")}`,
567
1156
  action: "confirmá con --overwrite si querés reemplazarlo, o quitalo de la propuesta",
568
1157
  },
569
1158
  };
@@ -571,18 +1160,94 @@ export async function applyExport(fs, env, paths, input) {
571
1160
  const parsed = parseSemanticResponse(input.raw, input.prepared.request);
572
1161
  if (!parsed.ok)
573
1162
  return parsed;
1163
+ const assembled = assembleExportArtifacts(parsed.value, input.prepared);
1164
+ if (!assembled.ok)
1165
+ return assembled;
574
1166
  const policy = resolvePolicy(input.prepared.category, input.prepared.dir);
575
1167
  const result = await withCwdLock(fs, paths, async () => {
576
- const minted = (await runNextNumber(fs, env, paths, { directory: policy.dir })).next;
577
- const artifacts = (parsed.value.artifacts ?? []).map((artifact) => renumber(artifact, input.prepared, minted, policy));
1168
+ const number = input.prepared.next;
1169
+ const folderMarker = input.prepared.category === "scripts"
1170
+ ? join(paths.workspaceDir(), input.prepared.unit, FOLDER_RESERVATION_MARKER)
1171
+ : null;
1172
+ const expected = input.prepared.reservationOwner === undefined
1173
+ ? null
1174
+ : reservationMarker(input.prepared.reservationOwner, input.prepared.reservationMaterialDigest);
1175
+ if (folderMarker !== null &&
1176
+ (!(await fs.exists(folderMarker)) || (await fs.readText(folderMarker)) !== expected)) {
1177
+ return {
1178
+ ok: false,
1179
+ failure: {
1180
+ code: "EXPORT_NUMBER_TAKEN",
1181
+ message: `la reserva ${number} ya no pertenece a esta operación`,
1182
+ action: "volvé a correr prepare",
1183
+ },
1184
+ };
1185
+ }
1186
+ const entries = (await fs.exists(join(paths.workspaceDir(), policy.dir)))
1187
+ ? await fs.list(join(paths.workspaceDir(), policy.dir))
1188
+ : [];
1189
+ const usedNumbers = await publishedCorrelatives(fs, paths.cwdHistoryFile(), policy.dir);
1190
+ for (const pass of (await readReleasePasses(fs, paths)).events) {
1191
+ if (pass.event !== "linked" || !pass.artifact.startsWith(`${policy.dir}/`))
1192
+ continue;
1193
+ const recorded = leadingCorrelative(pass.artifact.slice(policy.dir.length + 1).split("/")[0] ?? "");
1194
+ if (recorded !== null)
1195
+ usedNumbers.add(recorded);
1196
+ }
1197
+ const occupant = entries.find((entry) => {
1198
+ const found = leadingCorrelative(entry.name);
1199
+ return found !== null && sameCorrelative(found, number);
1200
+ });
1201
+ if (validated.value.preview.mode !== "complement" &&
1202
+ validated.value.preview.mode !== "flat" &&
1203
+ ((occupant !== undefined &&
1204
+ (folderMarker === null || occupant.name !== basename(input.prepared.unit))) ||
1205
+ [...usedNumbers].some((n) => sameCorrelative(n, number)))) {
1206
+ return {
1207
+ ok: false,
1208
+ failure: {
1209
+ code: "EXPORT_NUMBER_TAKEN",
1210
+ message: `el correlativo ${number} ya está ocupado por '${occupant?.name ?? "una publicación registrada"}'`,
1211
+ action: "volvé a correr prepare y validate con un número nuevo",
1212
+ },
1213
+ };
1214
+ }
1215
+ const artifacts = assembled.value.map((artifact) => ({
1216
+ ...artifact,
1217
+ overwrite: input.allowOverwrite === true && replacing.includes(artifact.path),
1218
+ }));
578
1219
  // Whole dossier or nothing: `publishArtifacts` restores every previous
579
1220
  // state on the first failure.
580
- const published = await publishArtifacts(fs, paths.workspaceDir(), artifacts, {
581
- overwrite: input.allowOverwrite === true,
582
- });
1221
+ const alreadyWritten = folderMarker !== null &&
1222
+ (await Promise.all(artifacts.map(async (artifact) => {
1223
+ const target = join(paths.workspaceDir(), artifact.path);
1224
+ try {
1225
+ return (await fs.readText(target)) === artifact.content;
1226
+ }
1227
+ catch {
1228
+ return false;
1229
+ }
1230
+ }))).every(Boolean);
1231
+ const published = alreadyWritten
1232
+ ? { ok: true, value: { written: artifacts.map((artifact) => artifact.path) } }
1233
+ : await publishArtifacts(fs, paths.workspaceDir(), artifacts, { overwrite: false });
583
1234
  // Under the SAME lock as the write, for the same reason as in `persist`: two
584
1235
  // concurrent publications outside it would lose one of the two rows.
585
1236
  if (published.ok) {
1237
+ if (folderMarker !== null && input.prepared.reservationOwner !== undefined) {
1238
+ await appendClaimEvent(fs, paths, {
1239
+ at: new Date().toISOString(),
1240
+ event: "published",
1241
+ claim: {
1242
+ category: basename(policy.dir),
1243
+ correlative: number,
1244
+ name: basename(input.prepared.unit).slice(number.length + 1),
1245
+ owner: input.prepared.reservationOwner,
1246
+ },
1247
+ cause: "aw export-scripts apply: dossier aprobado publicado",
1248
+ });
1249
+ await fs.remove(folderMarker);
1250
+ }
586
1251
  await appendPublications(fs, paths.cwdHistoryFile(), publicationRows(published.value.written, `export-${input.prepared.category}`));
587
1252
  }
588
1253
  return published;
@@ -601,34 +1266,6 @@ export async function applyExport(fs, env, paths, input) {
601
1266
  return result;
602
1267
  return { ok: true, value: { category: input.prepared.category, written: result.value.written } };
603
1268
  }
604
- /**
605
- * The number in the answer was consultative. The real one is minted inside the
606
- * lock, so the destination is rebuilt here — never trusted from the proposal.
607
- */
608
- function renumber(artifact, prepared, minted, policy) {
609
- if (artifact.path === policy.overwritable)
610
- return artifact;
611
- if (policy.shape === "document") {
612
- const name = artifact.path
613
- .slice(policy.dir.length + 1)
614
- .replace(new RegExp(`^${CORRELATIVE_SOURCE}-`), "");
615
- return { path: `${policy.dir}/${minted}-${name}`, content: artifact.content };
616
- }
617
- // The number to move is the UNIT's own, which is its LAST segment — never the
618
- // first `/NNN-` of the path. Since the category's folder became configurable,
619
- // a canon that is itself numbered (`docs/003-manuales`) would eat the
620
- // replacement: the export would be approved into `docs/003-manuales/…` and
621
- // written into `docs/001-manuales/…`, a folder outside `allowed_destinations`
622
- // that nothing downstream re-checks.
623
- const unitName = prepared.unit
624
- .slice(policy.dir.length + 1)
625
- .replace(new RegExp(`^${CORRELATIVE_SOURCE}-`), `${minted}-`);
626
- const unit = `${policy.dir}/${unitName}`;
627
- return {
628
- path: `${unit}/${artifact.path.slice(prepared.unit.length + 1)}`,
629
- content: artifact.content,
630
- };
631
- }
632
1269
  function reject(message) {
633
1270
  return {
634
1271
  code: "EXPORT_SHAPE_INVALID",