@documentation-robotics/cli 0.2.0 → 0.4.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 (335) hide show
  1. package/README.md +46 -16
  2. package/dist/analysis/relationship-classifier.js +2 -9
  3. package/dist/analysis/relationship-classifier.js.map +2 -2
  4. package/dist/analyzers/active-analyzer.js +62 -0
  5. package/dist/analyzers/active-analyzer.js.map +7 -0
  6. package/dist/analyzers/call-graph-utils.js +2 -2
  7. package/dist/analyzers/call-graph-utils.js.map +2 -2
  8. package/dist/analyzers/cards/build.js +191 -0
  9. package/dist/analyzers/cards/build.js.map +7 -0
  10. package/dist/analyzers/cards/classify.js +42 -0
  11. package/dist/analyzers/cards/classify.js.map +7 -0
  12. package/dist/analyzers/cards/edge-index.js +44 -0
  13. package/dist/analyzers/cards/edge-index.js.map +7 -0
  14. package/dist/analyzers/cards/extract.js +138 -0
  15. package/dist/analyzers/cards/extract.js.map +7 -0
  16. package/dist/analyzers/cards/facts.js +46 -0
  17. package/dist/analyzers/cards/facts.js.map +7 -0
  18. package/dist/analyzers/cards/graph.js +42 -0
  19. package/dist/analyzers/cards/graph.js.map +7 -0
  20. package/dist/analyzers/cards/index.js +8 -0
  21. package/dist/analyzers/cards/index.js.map +7 -0
  22. package/dist/analyzers/cards/routes.js +96 -0
  23. package/dist/analyzers/cards/routes.js.map +7 -0
  24. package/dist/analyzers/cards/tier0.js +18 -0
  25. package/dist/analyzers/cards/tier0.js.map +7 -0
  26. package/dist/analyzers/cards/types.js +1 -0
  27. package/dist/analyzers/cards/types.js.map +7 -0
  28. package/dist/analyzers/cbm-analyzer.js +270 -174
  29. package/dist/analyzers/cbm-analyzer.js.map +2 -2
  30. package/dist/analyzers/cbm-responses.js +342 -0
  31. package/dist/analyzers/cbm-responses.js.map +7 -0
  32. package/dist/analyzers/code-changes.js +159 -0
  33. package/dist/analyzers/code-changes.js.map +7 -0
  34. package/dist/analyzers/edge-relationships.js +49 -0
  35. package/dist/analyzers/edge-relationships.js.map +7 -0
  36. package/dist/analyzers/extraction-plan.js +541 -0
  37. package/dist/analyzers/extraction-plan.js.map +7 -0
  38. package/dist/analyzers/index-job.js +229 -0
  39. package/dist/analyzers/index-job.js.map +7 -0
  40. package/dist/analyzers/mapping-loader.js +9 -0
  41. package/dist/analyzers/mapping-loader.js.map +2 -2
  42. package/dist/analyzers/tier0-rules.js +153 -0
  43. package/dist/analyzers/tier0-rules.js.map +7 -0
  44. package/dist/analyzers/types.js.map +1 -1
  45. package/dist/audit/relationships/ai/prompts.js +10 -10
  46. package/dist/audit/relationships/ai/prompts.js.map +2 -2
  47. package/dist/audit/relationships/analysis/layer-templates.js +52 -52
  48. package/dist/audit/relationships/analysis/layer-templates.js.map +1 -1
  49. package/dist/cli.js +23 -11
  50. package/dist/cli.js.map +2 -2
  51. package/dist/coding-agents/default-permissions.js +31 -1
  52. package/dist/coding-agents/default-permissions.js.map +2 -2
  53. package/dist/coding-agents/drbot-mcp.js +269 -0
  54. package/dist/coding-agents/drbot-mcp.js.map +7 -0
  55. package/dist/commands/add.js +7 -4
  56. package/dist/commands/add.js.map +2 -2
  57. package/dist/commands/analyzer.js +143 -40
  58. package/dist/commands/analyzer.js.map +2 -2
  59. package/dist/commands/changeset-verify.js +63 -0
  60. package/dist/commands/changeset-verify.js.map +7 -0
  61. package/dist/commands/changeset.js +80 -29
  62. package/dist/commands/changeset.js.map +2 -2
  63. package/dist/commands/delete.js +10 -4
  64. package/dist/commands/delete.js.map +2 -2
  65. package/dist/commands/farm.js +176 -60
  66. package/dist/commands/farm.js.map +2 -2
  67. package/dist/commands/init.js +9 -26
  68. package/dist/commands/init.js.map +2 -2
  69. package/dist/commands/mcp.js +40 -21
  70. package/dist/commands/mcp.js.map +3 -3
  71. package/dist/commands/relationship.js +208 -37
  72. package/dist/commands/relationship.js.map +2 -2
  73. package/dist/commands/reports.js +3 -3
  74. package/dist/commands/reports.js.map +2 -2
  75. package/dist/commands/update.js +52 -32
  76. package/dist/commands/update.js.map +2 -2
  77. package/dist/commands/upgrade.js +99 -103
  78. package/dist/commands/upgrade.js.map +3 -3
  79. package/dist/commands/validate.js +19 -3
  80. package/dist/commands/validate.js.map +2 -2
  81. package/dist/commands/version.js +1 -1
  82. package/dist/commands/visualize.js +34 -15
  83. package/dist/commands/visualize.js.map +2 -2
  84. package/dist/core/annotation-store.js +15 -5
  85. package/dist/core/annotation-store.js.map +2 -2
  86. package/dist/core/catalog-migration.js +34 -0
  87. package/dist/core/catalog-migration.js.map +7 -0
  88. package/dist/core/changeset-analysis.js +421 -0
  89. package/dist/core/changeset-analysis.js.map +7 -0
  90. package/dist/core/changeset-base-index.js +160 -0
  91. package/dist/core/changeset-base-index.js.map +7 -0
  92. package/dist/core/changeset-brief-compact.js +256 -0
  93. package/dist/core/changeset-brief-compact.js.map +7 -0
  94. package/dist/core/changeset-brief-delta.js +102 -0
  95. package/dist/core/changeset-brief-delta.js.map +7 -0
  96. package/dist/core/changeset-graph.js +281 -0
  97. package/dist/core/changeset-graph.js.map +7 -0
  98. package/dist/core/changeset-only-guard.js +32 -0
  99. package/dist/core/changeset-only-guard.js.map +7 -0
  100. package/dist/core/changeset-summary.js +68 -0
  101. package/dist/core/changeset-summary.js.map +7 -0
  102. package/dist/core/changeset-validator.js +66 -20
  103. package/dist/core/changeset-validator.js.map +2 -2
  104. package/dist/core/changeset-verify.js +848 -0
  105. package/dist/core/changeset-verify.js.map +7 -0
  106. package/dist/core/changeset.js +56 -9
  107. package/dist/core/changeset.js.map +2 -2
  108. package/dist/core/element-type-migration.js +145 -0
  109. package/dist/core/element-type-migration.js.map +7 -0
  110. package/dist/core/extraction-observability.js +161 -0
  111. package/dist/core/extraction-observability.js.map +7 -0
  112. package/dist/core/farm-manifest.js +12 -6
  113. package/dist/core/farm-manifest.js.map +2 -2
  114. package/dist/core/farm-sync-engine.js +1 -1
  115. package/dist/core/farm-sync-engine.js.map +2 -2
  116. package/dist/core/farm-sync-state.js +10 -2
  117. package/dist/core/farm-sync-state.js.map +2 -2
  118. package/dist/core/migration-registry.js +88 -47
  119. package/dist/core/migration-registry.js.map +2 -2
  120. package/dist/core/model-upgrade.js +102 -0
  121. package/dist/core/model-upgrade.js.map +7 -0
  122. package/dist/core/model.js +82 -30
  123. package/dist/core/model.js.map +2 -2
  124. package/dist/core/mutation-handler.js +71 -6
  125. package/dist/core/mutation-handler.js.map +2 -2
  126. package/dist/core/project-init.js +53 -0
  127. package/dist/core/project-init.js.map +7 -0
  128. package/dist/core/relationship-migration-table.js +6639 -0
  129. package/dist/core/relationship-migration-table.js.map +7 -0
  130. package/dist/core/relationship-migration.js +272 -0
  131. package/dist/core/relationship-migration.js.map +7 -0
  132. package/dist/core/relationship-prune.js +49 -0
  133. package/dist/core/relationship-prune.js.map +7 -0
  134. package/dist/core/relationship-staging.js +161 -0
  135. package/dist/core/relationship-staging.js.map +7 -0
  136. package/dist/core/relationships.js +26 -4
  137. package/dist/core/relationships.js.map +2 -2
  138. package/dist/core/report-data-model.js +7 -13
  139. package/dist/core/report-data-model.js.map +2 -2
  140. package/dist/core/staged-changeset-storage.js +145 -18
  141. package/dist/core/staged-changeset-storage.js.map +2 -2
  142. package/dist/core/staged-source-reference.js +28 -0
  143. package/dist/core/staged-source-reference.js.map +7 -0
  144. package/dist/core/staging-area.js +166 -24
  145. package/dist/core/staging-area.js.map +2 -2
  146. package/dist/core/upgrade-plan.js +117 -0
  147. package/dist/core/upgrade-plan.js.map +7 -0
  148. package/dist/core/virtual-projection.js +13 -5
  149. package/dist/core/virtual-projection.js.map +2 -2
  150. package/dist/export/archimate-exporter.js +1 -0
  151. package/dist/export/archimate-exporter.js.map +2 -2
  152. package/dist/generated/compiled-validators.js +2 -2
  153. package/dist/generated/compiled-validators.js.map +2 -2
  154. package/dist/generated/layer-registry.js +16 -16
  155. package/dist/generated/layer-registry.js.map +2 -2
  156. package/dist/generated/node-types.js +186 -168
  157. package/dist/generated/node-types.js.map +2 -2
  158. package/dist/generated/relationship-index.js +2468 -7913
  159. package/dist/generated/relationship-index.js.map +2 -2
  160. package/dist/integrations/claude_code/.claude-plugin/plugin.json +24 -0
  161. package/dist/integrations/claude_code/agents/dr-architect.md +30 -3
  162. package/dist/integrations/claude_code/agents/dr-extractor.md +8 -2
  163. package/dist/integrations/claude_code/commands/dr-analyzer.md +565 -0
  164. package/dist/integrations/claude_code/commands/dr-changeset.md +2 -1
  165. package/dist/integrations/claude_code/commands/dr-map.md +20 -1
  166. package/dist/integrations/claude_code/commands/dr-plan.md +66 -0
  167. package/dist/integrations/claude_code/commands/dr-relate.md +106 -96
  168. package/dist/integrations/claude_code/commands/dr-verify.md +45 -2
  169. package/dist/integrations/claude_code/docs/END_TO_END_WALKTHROUGH.md +14 -0
  170. package/dist/integrations/claude_code/docs/MCP_SERVER.md +47 -26
  171. package/dist/integrations/claude_code/docs/QUICK_START.md +37 -8
  172. package/dist/integrations/claude_code/docs/README.md +68 -2
  173. package/dist/integrations/claude_code/plugin-agents/dr-advisor.md +255 -0
  174. package/dist/integrations/claude_code/plugin-agents/dr-architect.md +306 -0
  175. package/dist/integrations/claude_code/skills/dr_01_motivation_layer/SKILL.md +93 -72
  176. package/dist/integrations/claude_code/skills/dr_02_business_layer/SKILL.md +163 -126
  177. package/dist/integrations/claude_code/skills/dr_03_product_layer/SKILL.md +161 -126
  178. package/dist/integrations/claude_code/skills/dr_04_security_layer/SKILL.md +153 -133
  179. package/dist/integrations/claude_code/skills/dr_05_application_layer/SKILL.md +129 -100
  180. package/dist/integrations/claude_code/skills/dr_06_technology_layer/SKILL.md +111 -94
  181. package/dist/integrations/claude_code/skills/dr_07_api_layer/SKILL.md +143 -157
  182. package/dist/integrations/claude_code/skills/dr_08_data_model_layer/SKILL.md +117 -33
  183. package/dist/integrations/claude_code/skills/dr_09_dataset_layer/SKILL.md +74 -25
  184. package/dist/integrations/claude_code/skills/dr_10_ux_layer/SKILL.md +88 -12
  185. package/dist/integrations/claude_code/skills/dr_11_navigation_layer/SKILL.md +83 -24
  186. package/dist/integrations/claude_code/skills/dr_12_apm_layer/SKILL.md +57 -12
  187. package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +95 -15
  188. package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +178 -0
  189. package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +8 -0
  190. package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +6 -1
  191. package/dist/integrations/github_copilot/agents/dr-architect.md +3 -1
  192. package/dist/integrations/github_copilot/skills/dr_01_motivation_layer/SKILL.md +182 -102
  193. package/dist/integrations/github_copilot/skills/dr_02_business_layer/SKILL.md +266 -148
  194. package/dist/integrations/github_copilot/skills/dr_03_product_layer/SKILL.md +547 -86
  195. package/dist/integrations/github_copilot/skills/dr_04_security_layer/SKILL.md +164 -169
  196. package/dist/integrations/github_copilot/skills/dr_05_application_layer/SKILL.md +420 -150
  197. package/dist/integrations/github_copilot/skills/dr_06_technology_layer/SKILL.md +331 -138
  198. package/dist/integrations/github_copilot/skills/dr_07_api_layer/SKILL.md +265 -201
  199. package/dist/integrations/github_copilot/skills/dr_08_data_model_layer/SKILL.md +181 -58
  200. package/dist/integrations/github_copilot/skills/dr_09_dataset_layer/SKILL.md +209 -57
  201. package/dist/integrations/github_copilot/skills/dr_10_ux_layer/SKILL.md +277 -112
  202. package/dist/integrations/github_copilot/skills/dr_11_navigation_layer/SKILL.md +219 -70
  203. package/dist/integrations/github_copilot/skills/dr_12_apm_layer/SKILL.md +189 -105
  204. package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +225 -102
  205. package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +177 -0
  206. package/dist/integrations/github_copilot/skills/dr_changeset_reviewer/SKILL.md +3 -0
  207. package/dist/integrations/hash-utils.js.map +2 -2
  208. package/dist/mcp/api-key-manager.js +14 -1
  209. package/dist/mcp/api-key-manager.js.map +2 -2
  210. package/dist/mcp/changeset-only.js +45 -0
  211. package/dist/mcp/changeset-only.js.map +7 -0
  212. package/dist/mcp/project-root.js +85 -0
  213. package/dist/mcp/project-root.js.map +7 -0
  214. package/dist/mcp/server-instructions.js +27 -0
  215. package/dist/mcp/server-instructions.js.map +7 -0
  216. package/dist/mcp/strict-schema.js +52 -0
  217. package/dist/mcp/strict-schema.js.map +7 -0
  218. package/dist/mcp/tool-registry.js +55 -5
  219. package/dist/mcp/tool-registry.js.map +2 -2
  220. package/dist/mcp/tools/changeset-analysis-tools.js +98 -0
  221. package/dist/mcp/tools/changeset-analysis-tools.js.map +7 -0
  222. package/dist/mcp/tools/changeset-commit.js +224 -0
  223. package/dist/mcp/tools/changeset-commit.js.map +7 -0
  224. package/dist/mcp/tools/changeset-editing.js +260 -0
  225. package/dist/mcp/tools/changeset-editing.js.map +7 -0
  226. package/dist/mcp/tools/changeset-lifecycle.js +83 -0
  227. package/dist/mcp/tools/changeset-lifecycle.js.map +7 -0
  228. package/dist/mcp/tools/changeset-shared.js +31 -0
  229. package/dist/mcp/tools/changeset-shared.js.map +7 -0
  230. package/dist/mcp/tools/changeset-show.js +17 -1
  231. package/dist/mcp/tools/changeset-show.js.map +2 -2
  232. package/dist/mcp/tools/changeset-verify.js +49 -0
  233. package/dist/mcp/tools/changeset-verify.js.map +7 -0
  234. package/dist/mcp/tools/codebase-extract.js +836 -0
  235. package/dist/mcp/tools/codebase-extract.js.map +7 -0
  236. package/dist/mcp/tools/codebase-index.js +125 -0
  237. package/dist/mcp/tools/codebase-index.js.map +7 -0
  238. package/dist/mcp/tools/codebase-shared.js +265 -0
  239. package/dist/mcp/tools/codebase-shared.js.map +7 -0
  240. package/dist/mcp/tools/codebase-verify.js +48 -0
  241. package/dist/mcp/tools/codebase-verify.js.map +7 -0
  242. package/dist/mcp/tools/index.js +34 -1
  243. package/dist/mcp/tools/index.js.map +2 -2
  244. package/dist/mcp/tools/model-add.js +191 -74
  245. package/dist/mcp/tools/model-add.js.map +2 -2
  246. package/dist/mcp/tools/model-delete.js +2 -2
  247. package/dist/mcp/tools/model-delete.js.map +2 -2
  248. package/dist/mcp/tools/model-search.js +14 -0
  249. package/dist/mcp/tools/model-search.js.map +2 -2
  250. package/dist/mcp/tools/model-show.js +29 -2
  251. package/dist/mcp/tools/model-show.js.map +2 -2
  252. package/dist/mcp/tools/model-trace.js +93 -30
  253. package/dist/mcp/tools/model-trace.js.map +2 -2
  254. package/dist/mcp/tools/model-update.js +46 -26
  255. package/dist/mcp/tools/model-update.js.map +2 -2
  256. package/dist/mcp/tools/model-validate.js +46 -4
  257. package/dist/mcp/tools/model-validate.js.map +2 -2
  258. package/dist/mcp/tools/project-status-collectors.js +381 -0
  259. package/dist/mcp/tools/project-status-collectors.js.map +7 -0
  260. package/dist/mcp/tools/project-status.js +237 -0
  261. package/dist/mcp/tools/project-status.js.map +7 -0
  262. package/dist/mcp/tools/project-tools.js +505 -0
  263. package/dist/mcp/tools/project-tools.js.map +7 -0
  264. package/dist/mcp/tools/relationship-tools.js +167 -0
  265. package/dist/mcp/tools/relationship-tools.js.map +7 -0
  266. package/dist/mcp/tools/review-open.js +210 -0
  267. package/dist/mcp/tools/review-open.js.map +7 -0
  268. package/dist/mcp/tools/shared.js +35 -4
  269. package/dist/mcp/tools/shared.js.map +2 -2
  270. package/dist/mcp/tools/spec-type.js +90 -0
  271. package/dist/mcp/tools/spec-type.js.map +7 -0
  272. package/dist/reports/model-report-orchestrator.js +31 -2
  273. package/dist/reports/model-report-orchestrator.js.map +2 -2
  274. package/dist/schemas/bundled/analyzers/cbm.json +1200 -70
  275. package/dist/schemas/bundled/analyzers/manifest.json +2 -2
  276. package/dist/schemas/bundled/api.json +1029 -1658
  277. package/dist/schemas/bundled/apm.json +499 -734
  278. package/dist/schemas/bundled/application.json +735 -371
  279. package/dist/schemas/bundled/base.json +3 -220
  280. package/dist/schemas/bundled/business.json +452 -366
  281. package/dist/schemas/bundled/data-model.json +395 -411
  282. package/dist/schemas/bundled/data-store.json +334 -558
  283. package/dist/schemas/bundled/manifest.json +18 -18
  284. package/dist/schemas/bundled/motivation.json +349 -368
  285. package/dist/schemas/bundled/navigation.json +415 -731
  286. package/dist/schemas/bundled/product.json +325 -159
  287. package/dist/schemas/bundled/security.json +864 -1232
  288. package/dist/schemas/bundled/technology.json +574 -703
  289. package/dist/schemas/bundled/testing.json +865 -1646
  290. package/dist/schemas/bundled/ux.json +739 -1352
  291. package/dist/server/schemas.js +8 -0
  292. package/dist/server/schemas.js.map +2 -2
  293. package/dist/server/server.js +686 -231
  294. package/dist/server/server.js.map +3 -3
  295. package/dist/server/viewer-registry.js +264 -0
  296. package/dist/server/viewer-registry.js.map +7 -0
  297. package/dist/server-entry.js +2 -1
  298. package/dist/server-entry.js.map +2 -2
  299. package/dist/telemetry/index.js +1 -1
  300. package/dist/utils/ansi-suppressor.js +6 -8
  301. package/dist/utils/ansi-suppressor.js.map +2 -2
  302. package/dist/utils/attribute-patch.js +41 -0
  303. package/dist/utils/attribute-patch.js.map +7 -0
  304. package/dist/utils/error-hints.js +29 -0
  305. package/dist/utils/error-hints.js.map +7 -0
  306. package/dist/utils/errors.js +9 -3
  307. package/dist/utils/errors.js.map +2 -2
  308. package/dist/utils/id-generator.js +5 -2
  309. package/dist/utils/id-generator.js.map +2 -2
  310. package/dist/utils/project-paths.js +26 -1
  311. package/dist/utils/project-paths.js.map +2 -2
  312. package/dist/utils/source-reference.js +64 -31
  313. package/dist/utils/source-reference.js.map +2 -2
  314. package/dist/utils/spec-version.js +2 -2
  315. package/dist/utils/spec-version.js.map +1 -1
  316. package/dist/validators/composed-reference-validator.js +5 -27
  317. package/dist/validators/composed-reference-validator.js.map +2 -2
  318. package/dist/validators/model-warnings.js +28 -0
  319. package/dist/validators/model-warnings.js.map +7 -0
  320. package/dist/validators/reference-validator.js +0 -7
  321. package/dist/validators/reference-validator.js.map +2 -2
  322. package/dist/validators/relationship-schema-validator.js +3 -3
  323. package/dist/validators/relationship-schema-validator.js.map +2 -2
  324. package/dist/validators/schema-validator.js +27 -9
  325. package/dist/validators/schema-validator.js.map +2 -2
  326. package/dist/validators/types.js.map +1 -1
  327. package/dist/viewer/README.md +6 -6
  328. package/dist/viewer/assets/main-CUOQGGRA.css +1 -0
  329. package/dist/viewer/assets/main-DvH3gYrI.js +84 -0
  330. package/dist/viewer/index.html +2 -2
  331. package/dist/viewer/manifest.json +10 -10
  332. package/dist/viewer/version.json +1 -1
  333. package/package.json +2 -1
  334. package/dist/viewer/assets/main-B7AOQXXX.js +0 -81
  335. package/dist/viewer/assets/main-vdrFrG7q.css +0 -1
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/mcp/tools/changeset-analysis-tools.ts"],
4
+ "sourcesContent": ["/**\n * changeset_impact, changeset_brief \u2014 pressure-test a planned change against the model.\n * See `cli/src/core/changeset-analysis.ts` and docs/CHANGE_MODELING.md.\n */\n\nimport { z } from \"zod\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport { AnnotationStore } from \"../../core/annotation-store.js\";\nimport {\n buildBrief,\n computeImpact,\n computeUnwired,\n DEFAULT_HUB_THRESHOLD,\n type BriefDrift,\n} from \"../../core/changeset-analysis.js\";\nimport { briefDelta } from \"../../core/changeset-brief-delta.js\";\nimport { BRIEF_SECTIONS, briefSections, compactBrief } from \"../../core/changeset-brief-compact.js\";\nimport { describeDrift } from \"../../core/changeset-base-index.js\";\nimport { getErrorMessage } from \"../../utils/errors.js\";\nimport { jsonResult, loadModel, rootPathSchema, runTool, type McpToolDefinition } from \"./shared.js\";\nimport { loadChangesetGraph } from \"./changeset-shared.js\";\n\nconst changesetIdSchema = z.string().optional().describe(\"Changeset ID or name. Defaults to the active changeset.\");\n\n// ---------------------------------------------------------------------------\n// changeset_impact\n// ---------------------------------------------------------------------------\n\nconst impactInputSchema = {\n changesetId: changesetIdSchema,\n depth: z.number().int().min(1).max(6).optional().describe(\"Hops to walk out from the changed elements (default 2).\"),\n maxElements: z.number().int().positive().optional().describe(\"Stop after this many reached elements (default 200).\"),\n from: z\n .array(z.string())\n .optional()\n .describe(\n \"Element ids or paths to walk from instead of every changed element. Ids the projected model does not have \" +\n \"are listed in unknownFrom.\"\n ),\n hubThreshold: z\n .number()\n .int()\n .min(0)\n .optional()\n .describe(\n `An element with more relationships than this is a hub: listed in hubs with its degree and not walked ` +\n `through (default ${DEFAULT_HUB_THRESHOLD}).`\n ),\n expandHubs: z.boolean().optional().describe(\"Walk through hubs too (default false).\"),\n rootPath: rootPathSchema,\n};\n\nexport type ChangesetImpactArgs = z.infer<z.ZodObject<typeof impactInputSchema>>;\n\nexport async function changesetImpactHandler(args: ChangesetImpactArgs): Promise<CallToolResult> {\n return runTool(async () => {\n const model = await loadModel(args.rootPath);\n const { changeset, graph } = await loadChangesetGraph(model, args.changesetId);\n const report = computeImpact(graph, {\n depth: args.depth,\n maxElements: args.maxElements,\n from: args.from,\n hubThreshold: args.hubThreshold,\n expandHubs: args.expandHubs,\n });\n return jsonResult({ changeset: { id: changeset.id, name: changeset.name }, ...report });\n });\n}\n\nexport const changesetImpactTool: McpToolDefinition<typeof impactInputSchema> = {\n name: \"changeset_impact\",\n description:\n \"Walk the model outward from everything a changeset adds, changes or removes (relationships included), or \" +\n \"from the elements named in `from`, and list the existing elements it reaches, by layer, with how each was \" +\n \"reached and its source files, plus how many each hop reached (perHop). These are what the change can break \" +\n \"or must update. Hubs (elements with more than hubThreshold relationships) are listed with their degree and \" +\n \"not walked through unless expandHubs is set.\",\n inputSchema: impactInputSchema,\n handler: changesetImpactHandler,\n};\n\n// ---------------------------------------------------------------------------\n// changeset_brief\n// ---------------------------------------------------------------------------\n\nconst briefInputSchema = {\n changesetId: changesetIdSchema,\n unwired: z\n .enum([\"summary\", \"full\", \"none\"])\n .optional()\n .describe(\n \"Unwired connections (catalog connections an added or updated element has no relationship for). The compact \" +\n \"brief counts them per layer unless this is set. summary (the full brief's default) counts them per element \" +\n \"and layer and lists the ones in layers the change already reaches whose other type the model has (existing \" +\n \"or added), counting the rest as typesNotInModel; full lists them all; none omits them.\"\n ),\n unwiredLayers: z\n .array(z.string())\n .optional()\n .describe(\"Only show unwired connections whose other end is in these layers (and list them).\"),\n detail: z\n .enum([\"compact\", \"full\"])\n .optional()\n .describe(\n \"compact (default): counts by layer, reach, gaps one line each, intent as ids, drift, unwired counts per \" +\n \"layer, and each changed element with its connections as `predicate \u2192 id` lines. full: the whole brief \" +\n \"(attributes, connection names and layers, unwired listing, code per element).\"\n ),\n sections: z\n .array(z.enum(BRIEF_SECTIONS))\n .optional()\n .describe(\n \"Return only these sections, each in full, with a small header (revision, the changeset's id, name and status, \" +\n \"summary counts). [\\\"changes\\\"] is the build view: each changed element with its name, description, attributes, \" +\n \"changed fields (before and after), files and connections as `predicate \u2192 id` lines. Others: [\\\"drift\\\", \" +\n \"\\\"alreadyInBase\\\"], [\\\"unwired\\\"], [\\\"layers\\\"] (the full brief's layers, large). With since, the delta comes too.\"\n ),\n layers: z\n .array(z.string())\n .optional()\n .describe(\n \"With sections: only the elements in these layers (canonical layer names), in changes, layers, gaps, unwired, \" +\n \"intent and feedback. Combines with ids (either keeps an element).\"\n ),\n ids: z\n .array(z.string())\n .optional()\n .describe(\"With sections: only these changed elements (ids), in changes, layers, gaps, unwired, intent and feedback.\"),\n since: z\n .union([z.number().int().min(-1), z.string().regex(/^(-1|\\d+):[0-9a-f]+$/, \"a revisionToken from a brief\")])\n .optional()\n .describe(\n \"A revision (or, better, the revisionToken) an earlier brief returned. Adds a delta: elements added, removed \" +\n \"or updated (with the fields), relationships added or removed, changes unstaged, gaps opened and closed, \" +\n \"unwired connections wired, layers newly reached or no longer reached, intent newly traced since then. \" +\n \"Unstaging does not invalidate it. With the token, the delta warns when a change up to that revision was \" +\n \"rewritten in place since.\"\n ),\n rootPath: rootPathSchema,\n};\n\nexport type ChangesetBriefArgs = z.infer<z.ZodObject<typeof briefInputSchema>>;\n\nexport async function changesetBriefHandler(args: ChangesetBriefArgs): Promise<CallToolResult> {\n return runTool(async () => {\n const model = await loadModel(args.rootPath);\n const { manager, changeset, graph } = await loadChangesetGraph(model, args.changesetId);\n\n const annotations = await new AnnotationStore(model.rootPath).list();\n\n let drift: BriefDrift | undefined;\n if (changeset.status === \"staged\") {\n // Each part is reported on failure and the brief goes on: the hash check, then the structured view.\n let hash: { isDrifted: boolean; warnings: string[]; baseSnapshotId?: string; currentSnapshotId?: string };\n try {\n hash = await manager.detectDrift(changeset.id);\n } catch (error) {\n hash = { isDrifted: false, warnings: [`Drift check failed: ${getErrorMessage(error)}`] };\n }\n try {\n drift = await describeDrift(model, graph, hash);\n } catch (error) {\n drift = {\n isDrifted: hash.isDrifted,\n warnings: [...hash.warnings, `Could not name what drifted: ${getErrorMessage(error)}`],\n };\n }\n }\n\n if (args.detail === \"full\") {\n const brief = buildBrief({ graph, annotations, drift, unwired: args.unwired, unwiredLayers: args.unwiredLayers });\n // The full brief is the largest result: same JSON, short objects kept on one line.\n if (args.since === undefined) return jsonResult(brief, { compact: true });\n return jsonResult({ ...brief, delta: await briefDelta(model, { graph, brief }, args.since) }, { compact: true });\n }\n\n // Compact: the unwired listing only when asked for; otherwise counted per layer.\n const sections = args.sections ?? [];\n const listUnwired = args.unwired !== undefined || args.unwiredLayers !== undefined || sections.includes(\"unwired\");\n const brief = buildBrief({\n graph,\n annotations,\n drift,\n unwired: listUnwired ? (args.unwired ?? \"summary\") : \"none\",\n unwiredLayers: args.unwiredLayers,\n });\n const delta = args.since === undefined ? undefined : await briefDelta(model, { graph, brief }, args.since);\n if (sections.length > 0) {\n // Only what was asked for; an unwired listing asked for by its own arguments counts as asked.\n const named = listUnwired && !sections.includes(\"unwired\") && args.unwired !== \"none\" ? [...sections, \"unwired\" as const] : sections;\n return jsonResult(briefSections(brief, named, { delta, filter: { layers: args.layers, ids: args.ids } }), { compact: true });\n }\n const compact = compactBrief(brief, { unwired: listUnwired ? undefined : computeUnwired(graph), delta });\n if (args.layers?.length || args.ids?.length) {\n compact.note =\n \"layers and ids narrow sections output only, so the whole compact brief is returned. Pass sections too, \" +\n 'e.g. sections: [\"changes\"] with them for those elements\\' build view.';\n }\n return jsonResult(compact, { compact: true });\n });\n}\n\nexport const changesetBriefTool: McpToolDefinition<typeof briefInputSchema> = {\n name: \"changeset_brief\",\n description:\n \"Render a changeset as the spec for its change. By default a compact summary: counts by layer and operation; \" +\n \"which layers the change reaches and which it does not; gaps, one line each (unconnected additions, missing \" +\n \"required relationships, connections its peers have); intent as ids (traced, untraced, unanchored: changes \" +\n \"that reach no goal, requirement or feature, or only an added one that itself traces to nothing); drift since \" +\n \"the changeset was created; unwired connections (catalog connections an added or updated element has no \" +\n \"relationship for) counted per layer; the files of the changed elements; open review annotations; and each \" +\n \"changed element with its type, operation and connections as `predicate \u2192 id` lines (for a plan of more \" +\n \"than 60 elements, counts per type with the first ids of each, and long id lists cut with their counts). \" +\n \"detail: \\\"full\\\" returns the whole brief (attributes, connection names, the unwired listing, code per \" +\n \"element); sections: [...] returns only the sections named, in full, with a small header. It returns a revision and a revisionToken: pass the token back as since on the \" +\n \"next pass to get what changed. Use it to pressure-test a plan and, at the start of a build, as its spec.\",\n inputSchema: briefInputSchema,\n handler: changesetBriefHandler,\n};\n"],
5
+ "mappings": "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",
6
+ "names": ["brief"]
7
+ }
@@ -0,0 +1,224 @@
1
+ import { z } from "zod";
2
+ import { Model } from "../../core/model.js";
3
+ import { BaseSnapshotManager } from "../../core/base-snapshot-manager.js";
4
+ import { ChangesetGraph } from "../../core/changeset-graph.js";
5
+ import { describeDrift } from "../../core/changeset-base-index.js";
6
+ import { StagingAreaManager } from "../../core/staging-area.js";
7
+ import { isChangesetOnly, CHANGESET_ONLY_ENV } from "../../core/changeset-only-guard.js";
8
+ import { CLIError, ErrorCategory, getErrorMessage } from "../../utils/errors.js";
9
+ import { jsonResult, loadModel, reloadModel, rootPathSchema, runTool } from "./shared.js";
10
+ import { resolveChangeset } from "./changeset-shared.js";
11
+ import { isExtractionChangeset } from "./codebase-extract.js";
12
+ const CONFIRMATION_TIMEOUT_MS = 10 * 6e4;
13
+ function otherWaysToLand() {
14
+ return [
15
+ "Ask the user to commit it in the viewer: call review_open, give them its url, and they press Apply on the changeset",
16
+ "Or ask them to run changeset_commit again from an interactive Claude Code session, where the confirmation dialog can be shown"
17
+ ];
18
+ }
19
+ const inputSchema = {
20
+ changeset: z.string().min(1).describe("Changeset ID or name to commit. Required: it need not be the active changeset."),
21
+ force: z.boolean().optional().describe("Commit even though the base model changed since the changeset was created (drift). The confirmation dialog shows the drift. Validation still blocks."),
22
+ rootPath: rootPathSchema
23
+ }, OPERATION_LABELS = {
24
+ add: "adds",
25
+ update: "updates",
26
+ delete: "deletes",
27
+ "relationship-add": "relationship adds",
28
+ "relationship-delete": "relationship deletes"
29
+ };
30
+ function summarizeChangeset(changeset) {
31
+ const byOperation = {}, layers = /* @__PURE__ */ new Set();
32
+ for (const change of changeset.changes)
33
+ byOperation[change.type] = (byOperation[change.type] ?? 0) + 1, change.layerName && layers.add(change.layerName);
34
+ return { byOperation, layers: [...layers].sort() };
35
+ }
36
+ function driftLines(drift, hash, forced) {
37
+ const overlapping = drift ? drift.overlapping.elements.length + drift.overlapping.relationships.length : 0;
38
+ if (!hash.isDrifted && !drift?.isDrifted)
39
+ return ["Drift: none. The base model has not changed under this changeset."];
40
+ const lines = ["Drift: the base model changed since this changeset was created."], counts = drift?.baseChanges?.counts;
41
+ if (counts) {
42
+ const parts = [
43
+ [counts.elementsAdded, "elements added"],
44
+ [counts.elementsRemoved, "elements removed"],
45
+ [counts.elementsChanged, "elements changed"],
46
+ [counts.relationshipsAdded, "relationships added"],
47
+ [counts.relationshipsRemoved, "relationships removed"]
48
+ ].filter(([n]) => n > 0).map(([n, label]) => `${n} ${label}`);
49
+ parts.length > 0 && lines.push(` In the base: ${parts.join(", ")}.`);
50
+ }
51
+ if (drift && overlapping > 0) {
52
+ const listed = [
53
+ ...drift.overlapping.elements.map((e) => `${e.id} (${e.reason})`),
54
+ ...drift.overlapping.relationships.map((r) => `${r.source} --${r.predicate}--> ${r.target} (${r.reason})`)
55
+ ];
56
+ lines.push(` ${overlapping} staged change(s) overlap the base: ${listed.slice(0, 5).join(", ")}${overlapping > 5 ? ", ..." : ""}.`);
57
+ }
58
+ return hash.isDrifted ? forced && lines.push(" Committing anyway: force was passed.") : lines.push(" The drift check still passes, so this does not block the commit."), lines;
59
+ }
60
+ const EXTRACTION_MARKER = /Staged by codebase_extract/;
61
+ async function madeByExtraction(rootPath, changeset) {
62
+ return EXTRACTION_MARKER.test(changeset.description ?? "") ? !0 : !!changeset.id && await isExtractionChangeset(rootPath, changeset.id, changeset.name);
63
+ }
64
+ async function asBuiltFirstWarning(manager, rootPath, changeset) {
65
+ if (!await madeByExtraction(rootPath, changeset))
66
+ return;
67
+ const plans = [];
68
+ for (const other of await manager.list())
69
+ other.id === changeset.id || other.status !== "staged" || other.changes.length === 0 || await madeByExtraction(rootPath, other) || plans.push(other);
70
+ if (plans.length === 0)
71
+ return;
72
+ const names = plans.map((plan) => `"${plan.name}"`).join(", ");
73
+ return `Warning: "${changeset.name}" is an as-built changeset from codebase_extract, and the plan ${names} is still staged. After verify, land the plan instead, and discard this one or keep it as a record. Landing this one first changes the plan's base: the plan then needs force, and its descriptions won't apply to the elements this one adds.`;
74
+ }
75
+ function confirmationMessage(changeset, drift, hash, preExistingErrors, forced, warnings = []) {
76
+ const { byOperation, layers } = summarizeChangeset(changeset), counts = Object.entries(byOperation).map(([type, n]) => `${n} ${OPERATION_LABELS[type] ?? type}`).join(", ");
77
+ return [
78
+ `Commit changeset "${changeset.name}" to the base model?`,
79
+ "",
80
+ ...warnings.flatMap((warning) => [warning, ""]),
81
+ `Intent: ${changeset.description?.trim() || "(no description)"}`,
82
+ `Changes: ${changeset.changes.length} (${counts})`,
83
+ `Layers: ${layers.join(", ") || "none"}`,
84
+ ...driftLines(drift, hash, forced),
85
+ preExistingErrors > 0 ? `Validation: passed. ${preExistingErrors} error(s) already in the base model are not counted.` : "Validation: passed.",
86
+ "",
87
+ "Committing writes these changes to the base model and marks the changeset committed."
88
+ ].join(`
89
+ `);
90
+ }
91
+ async function confirmedState(model, changeset) {
92
+ return JSON.stringify({
93
+ changes: changeset.changes.map((c) => [c.sequenceNumber ?? null, c.type, c.elementId]),
94
+ lastSequence: changeset.lastSequence ?? null,
95
+ modified: changeset.modified,
96
+ status: changeset.status,
97
+ baseElements: await new BaseSnapshotManager().captureSnapshot(model),
98
+ baseRelationships: model.getBaseRelationships().map((r) => `${r.source}::${r.predicate}::${r.target}`).sort()
99
+ });
100
+ }
101
+ function refusal(message, suggestions) {
102
+ return new CLIError(message, ErrorCategory.USER, suggestions, { operation: "changeset_commit" });
103
+ }
104
+ function commitError(error, name) {
105
+ if (error instanceof CLIError)
106
+ return error;
107
+ const message = getErrorMessage(error);
108
+ return message.includes("drifted") ? new CLIError(`Changeset '${name}' was not committed: ${message.replace(/Use --force/, "Pass force: true")}`, ErrorCategory.USER, [
109
+ 'Read the drift with "changeset_brief" and tell the user what changed in the base',
110
+ "If they still want it landed, call changeset_commit again with force: true"
111
+ ]) : message.startsWith("Validation failed") ? new CLIError(`Changeset '${name}' was not committed. ${message.replace(/, or skip validation with --no-validate\./, ".")}`, ErrorCategory.VALIDATION, ['Fix the plan (model_update, changeset_unstage, ...) and check it with "model_validate"']) : new CLIError(`Changeset '${name}' was not committed: ${message}`, ErrorCategory.SYSTEM);
112
+ }
113
+ async function askForConfirmation(context, message) {
114
+ return context.server.elicitInput({
115
+ mode: "form",
116
+ message,
117
+ requestedSchema: {
118
+ type: "object",
119
+ properties: {
120
+ confirm: {
121
+ type: "boolean",
122
+ title: "Commit this changeset",
123
+ description: "Write the staged changes to the base model.",
124
+ default: !1
125
+ }
126
+ },
127
+ required: ["confirm"]
128
+ }
129
+ }, { relatedRequestId: context.requestId, signal: context.signal, timeout: CONFIRMATION_TIMEOUT_MS });
130
+ }
131
+ async function changesetCommitHandler(args, context) {
132
+ return runTool(async () => {
133
+ if (isChangesetOnly())
134
+ throw refusal(`This session can't commit a changeset: it is in changeset-only mode (${CHANGESET_ONLY_ENV}), where no person can confirm the commit.`, ["Tell the user the changeset is ready to land", ...otherWaysToLand()]);
135
+ const rootPath = (await loadModel(args.rootPath)).rootPath;
136
+ let model = await Model.load(rootPath, { lazyLoad: !1 }), manager = new StagingAreaManager(model.rootPath, model);
137
+ const changeset = await resolveChangeset(manager, args.changeset, "changeset");
138
+ if (changeset.status !== "staged")
139
+ throw new CLIError(`Changeset '${changeset.name}' is ${changeset.status}; only a staged changeset can be committed`, ErrorCategory.USER, [
140
+ 'Use "changeset_list" to find a staged changeset'
141
+ ]);
142
+ if (changeset.changes.length === 0)
143
+ return jsonResult({ id: changeset.id, name: changeset.name, status: "nothing-to-commit", stagedChanges: 0 });
144
+ if (!context?.server?.getClientCapabilities()?.elicitation?.form)
145
+ throw refusal(`Changeset '${changeset.name}' was not committed: committing needs a person to confirm it, and this client can't show a confirmation dialog (it does not support MCP elicitation).`, otherWaysToLand());
146
+ const hash = await manager.detectDrift(changeset.id);
147
+ let drift;
148
+ try {
149
+ drift = await describeDrift(model, await ChangesetGraph.build(model, changeset), hash);
150
+ } catch (error) {
151
+ console.error(`[mcp] changeset_commit could not describe drift: ${getErrorMessage(error)}`);
152
+ }
153
+ if (hash.isDrifted && args.force !== !0)
154
+ throw new CLIError(`Changeset '${changeset.name}' was not committed: the base model changed since it was created.
155
+ ` + driftLines(drift, hash, !1).slice(1).join(`
156
+ `), ErrorCategory.USER, [
157
+ 'Read the drift with "changeset_brief" and tell the user what changed in the base',
158
+ "If they still want it landed, call changeset_commit again with force: true; the dialog will show the drift"
159
+ ], { operation: "changeset_commit" });
160
+ let preExistingErrors = 0;
161
+ try {
162
+ preExistingErrors = (await manager.commit(model, changeset.id, { dryRun: !0, validate: !0, force: args.force === !0 })).validation.preExistingErrors ?? 0;
163
+ } catch (error) {
164
+ throw commitError(error, changeset.name);
165
+ }
166
+ const warnings = [], asBuiltFirst = await asBuiltFirstWarning(manager, rootPath, changeset);
167
+ asBuiltFirst && warnings.push(asBuiltFirst);
168
+ const shown = await confirmedState(model, changeset);
169
+ let answer;
170
+ try {
171
+ answer = await askForConfirmation(context, confirmationMessage(changeset, drift, hash, preExistingErrors, args.force === !0, warnings));
172
+ } catch (error) {
173
+ throw refusal(`Changeset '${changeset.name}' was not committed: the confirmation could not be shown (${getErrorMessage(error)}).`, otherWaysToLand());
174
+ }
175
+ if (answer.action !== "accept" || answer.content?.confirm !== !0) {
176
+ const how = answer.action === "accept" ? "did not confirm it" : answer.action === "decline" ? "declined" : "cancelled";
177
+ throw refusal(`Changeset '${changeset.name}' was not committed: the user ${how}. Nothing was written.`, [
178
+ "Ask the user what should change before landing it, or leave it staged"
179
+ ]);
180
+ }
181
+ model = await Model.load(rootPath, { lazyLoad: !1 }), manager = new StagingAreaManager(model.rootPath, model);
182
+ const current = await manager.load(changeset.id);
183
+ if (!current || await confirmedState(model, current) !== shown)
184
+ throw refusal(`Changeset '${changeset.name}' was not committed: the changeset or the base model changed while the user was confirming. Nothing was written.`, ['Show the user the changeset as it is now ("changeset_brief"), then ask again with changeset_commit']);
185
+ const wasActive = await manager.getActiveId() === changeset.id;
186
+ let result;
187
+ try {
188
+ result = await manager.commit(model, changeset.id, { validate: !0, force: args.force === !0 });
189
+ } catch (error) {
190
+ throw commitError(error, changeset.name);
191
+ }
192
+ return wasActive && await manager.clearActive(), await reloadModel(args.rootPath), jsonResult({
193
+ id: changeset.id,
194
+ name: changeset.name,
195
+ status: "committed",
196
+ committed: result.committed,
197
+ skipped: result.skipped ?? 0,
198
+ ...result.skippedDetails?.length ? { skippedDetails: result.skippedDetails.slice(0, 10) } : {},
199
+ failed: result.failed,
200
+ preExistingErrors: result.validation.preExistingErrors ?? 0,
201
+ deactivated: wasActive,
202
+ // StagingAreaManager.commit reports driftWarning only on a dry run, so use the check made above.
203
+ ...result.driftWarning || hash.isDrifted ? { driftOverridden: !0 } : {},
204
+ ...warnings.length > 0 ? { warnings } : {}
205
+ });
206
+ });
207
+ }
208
+ const changesetCommitTool = {
209
+ name: "changeset_commit",
210
+ description: "Land a staged changeset: write its changes to the base model and mark it committed. Only after a person confirms it: the server asks the user in a confirmation dialog (MCP elicitation) showing the changeset's intent, counts, layers and drift, and commits only if they confirm. Refused when the client can't show the dialog, when the user declines or cancels, and in changeset-only sessions. Validation errors the changeset introduces block it; drift blocks it unless force is passed. Call it only when the user asks to land the change.",
211
+ inputSchema,
212
+ _meta: { "anthropic/requiresUserInteraction": !0 },
213
+ handler: changesetCommitHandler
214
+ };
215
+ export {
216
+ CONFIRMATION_TIMEOUT_MS,
217
+ asBuiltFirstWarning,
218
+ changesetCommitHandler,
219
+ changesetCommitTool,
220
+ confirmationMessage,
221
+ otherWaysToLand,
222
+ summarizeChangeset
223
+ };
224
+ //# sourceMappingURL=changeset-commit.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/mcp/tools/changeset-commit.ts"],
4
+ "sourcesContent": ["/**\n * changeset_commit \u2014 land a staged changeset in the base model, once a person confirms it.\n *\n * Landing is the human's step (motivation.requirement.landing-a-change-needs-a-humans-confirmation),\n * so the tool is gated twice:\n * - It is listed with `_meta[\"anthropic/requiresUserInteraction\"]`. Claude Code then shows a\n * permission prompt on every call, whatever the permission mode.\n * - It always asks the client for confirmation (`elicitation/create`), showing the changeset's\n * name, intent, counts by operation, the layers it touches and any drift. It commits only on\n * `accept` with `confirm: true`. A decline, a cancel, a client without form elicitation, or a\n * changeset-only session (DrBot, where no person can answer) is refused, and nothing is written.\n *\n * The commit itself is `StagingAreaManager.commit`, the code `dr changeset commit` and the viewer's\n * Apply (`POST /api/changesets/:id/commit`) run. Like the viewer, it works on a freshly loaded model\n * and may commit any staged changeset, active or not. Like the CLI, it validates (errors the\n * changeset introduces block; the base model's own are counted) and refuses a drifted changeset\n * unless `force` is passed. Landing an as-built changeset from codebase_extract while a plan is\n * staged is allowed, but the dialog warns that the plan should land instead. Both are checked with a dry run before the person is asked, so they are\n * never asked to confirm a commit that would fail. After they accept, the changeset and the base are\n * read again and compared with what the dialog showed; any change refuses the commit.\n */\n\nimport { z } from \"zod\";\nimport type { CallToolResult, ElicitResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport { Model } from \"../../core/model.js\";\nimport type { Changeset, StagedChange } from \"../../core/changeset.js\";\nimport { BaseSnapshotManager } from \"../../core/base-snapshot-manager.js\";\nimport { ChangesetGraph } from \"../../core/changeset-graph.js\";\nimport { describeDrift, type ChangesetDrift } from \"../../core/changeset-base-index.js\";\nimport { StagingAreaManager, type DriftReport } from \"../../core/staging-area.js\";\nimport { isChangesetOnly, CHANGESET_ONLY_ENV } from \"../../core/changeset-only-guard.js\";\nimport { CLIError, ErrorCategory, getErrorMessage } from \"../../utils/errors.js\";\nimport {\n jsonResult,\n loadModel,\n reloadModel,\n rootPathSchema,\n runTool,\n type McpToolContext,\n type McpToolDefinition,\n} from \"./shared.js\";\nimport { resolveChangeset } from \"./changeset-shared.js\";\nimport { isExtractionChangeset } from \"./codebase-extract.js\";\n\n/** How long the person has to answer the confirmation dialog before the commit is refused. */\nexport const CONFIRMATION_TIMEOUT_MS = 10 * 60_000;\n\n/**\n * How the user can land the changeset without this tool: the viewer, or an interactive session.\n * Never the CLI: the developer types no dr command (round 3 found agents passing it on).\n */\nexport function otherWaysToLand(): string[] {\n return [\n \"Ask the user to commit it in the viewer: call review_open, give them its url, and they press Apply on the changeset\",\n \"Or ask them to run changeset_commit again from an interactive Claude Code session, where the confirmation dialog can be shown\",\n ];\n}\n\nconst inputSchema = {\n changeset: z\n .string()\n .min(1)\n .describe(\"Changeset ID or name to commit. Required: it need not be the active changeset.\"),\n force: z\n .boolean()\n .optional()\n .describe(\n \"Commit even though the base model changed since the changeset was created (drift). The confirmation \" +\n \"dialog shows the drift. Validation still blocks.\"\n ),\n rootPath: rootPathSchema,\n};\n\nexport type ChangesetCommitArgs = z.infer<z.ZodObject<typeof inputSchema>>;\n\nconst OPERATION_LABELS: Record<string, string> = {\n add: \"adds\",\n update: \"updates\",\n delete: \"deletes\",\n \"relationship-add\": \"relationship adds\",\n \"relationship-delete\": \"relationship deletes\",\n};\n\n/** Counts by operation and the layers touched, in a stable order. */\nexport function summarizeChangeset(changeset: Changeset): { byOperation: Record<string, number>; layers: string[] } {\n const byOperation: Record<string, number> = {};\n const layers = new Set<string>();\n for (const change of changeset.changes) {\n byOperation[change.type] = (byOperation[change.type] ?? 0) + 1;\n if (change.layerName) layers.add(change.layerName);\n }\n return { byOperation, layers: [...layers].sort() };\n}\n\nfunction driftLines(drift: ChangesetDrift | undefined, hash: DriftReport, forced: boolean): string[] {\n const overlapping = drift ? drift.overlapping.elements.length + drift.overlapping.relationships.length : 0;\n if (!hash.isDrifted && !drift?.isDrifted) return [\"Drift: none. The base model has not changed under this changeset.\"];\n const lines = [\"Drift: the base model changed since this changeset was created.\"];\n const counts = drift?.baseChanges?.counts;\n if (counts) {\n const parts = [\n [counts.elementsAdded, \"elements added\"],\n [counts.elementsRemoved, \"elements removed\"],\n [counts.elementsChanged, \"elements changed\"],\n [counts.relationshipsAdded, \"relationships added\"],\n [counts.relationshipsRemoved, \"relationships removed\"],\n ]\n .filter(([n]) => (n as number) > 0)\n .map(([n, label]) => `${n} ${label}`);\n if (parts.length > 0) lines.push(` In the base: ${parts.join(\", \")}.`);\n }\n if (drift && overlapping > 0) {\n const listed = [\n ...drift.overlapping.elements.map((e) => `${e.id} (${e.reason})`),\n ...drift.overlapping.relationships.map((r) => `${r.source} --${r.predicate}--> ${r.target} (${r.reason})`),\n ];\n lines.push(\n ` ${overlapping} staged change(s) overlap the base: ${listed.slice(0, 5).join(\", \")}${overlapping > 5 ? \", ...\" : \"\"}.`\n );\n }\n if (!hash.isDrifted) lines.push(\" The drift check still passes, so this does not block the commit.\");\n else if (forced) lines.push(\" Committing anyway: force was passed.\");\n return lines;\n}\n\n/** The description codebase_extract gives the changesets it creates. */\nconst EXTRACTION_MARKER = /Staged by codebase_extract/;\n\n/**\n * Whether codebase_extract made this changeset: its description says so, or extraction keeps a\n * record for it (its cards file, or waiting cards adopted under this name).\n */\nasync function madeByExtraction(rootPath: string, changeset: Changeset): Promise<boolean> {\n if (EXTRACTION_MARKER.test(changeset.description ?? \"\")) return true;\n return !!changeset.id && (await isExtractionChangeset(rootPath, changeset.id, changeset.name));\n}\n\n/**\n * The warning shown when an extraction (as-built) changeset is about to land while a plan is\n * staged. The verify workflow lands the plan and discards the as-built changeset (or keeps it as a\n * record); landing the as-built first drifts the plan's base, so the plan then needs force, and\n * its descriptions don't apply to the elements the as-built side already added.\n */\nexport async function asBuiltFirstWarning(\n manager: StagingAreaManager,\n rootPath: string,\n changeset: Changeset\n): Promise<string | undefined> {\n if (!(await madeByExtraction(rootPath, changeset))) return undefined;\n const plans: Changeset[] = [];\n for (const other of await manager.list()) {\n if (other.id === changeset.id || other.status !== \"staged\" || other.changes.length === 0) continue;\n if (!(await madeByExtraction(rootPath, other))) plans.push(other);\n }\n if (plans.length === 0) return undefined;\n const names = plans.map((plan) => `\"${plan.name}\"`).join(\", \");\n return (\n `Warning: \"${changeset.name}\" is an as-built changeset from codebase_extract, and the plan ${names} is still ` +\n `staged. After verify, land the plan instead, and discard this one or keep it as a record. Landing this one ` +\n `first changes the plan's base: the plan then needs force, and its descriptions won't apply to the elements ` +\n `this one adds.`\n );\n}\n\n/** The text of the confirmation dialog. */\nexport function confirmationMessage(\n changeset: Changeset,\n drift: ChangesetDrift | undefined,\n hash: DriftReport,\n preExistingErrors: number,\n forced: boolean,\n warnings: string[] = []\n): string {\n const { byOperation, layers } = summarizeChangeset(changeset);\n const counts = Object.entries(byOperation)\n .map(([type, n]) => `${n} ${OPERATION_LABELS[type] ?? type}`)\n .join(\", \");\n return [\n `Commit changeset \"${changeset.name}\" to the base model?`,\n \"\",\n ...warnings.flatMap((warning) => [warning, \"\"]),\n `Intent: ${changeset.description?.trim() || \"(no description)\"}`,\n `Changes: ${changeset.changes.length} (${counts})`,\n `Layers: ${layers.join(\", \") || \"none\"}`,\n ...driftLines(drift, hash, forced),\n preExistingErrors > 0\n ? `Validation: passed. ${preExistingErrors} error(s) already in the base model are not counted.`\n : \"Validation: passed.\",\n \"\",\n \"Committing writes these changes to the base model and marks the changeset committed.\",\n ].join(\"\\n\");\n}\n\n/**\n * A fingerprint of what the person confirms: the changeset's staged changes, its modified time and\n * status, and the base model's elements (the drift snapshot) and committed relationships.\n */\nasync function confirmedState(model: Model, changeset: Changeset): Promise<string> {\n return JSON.stringify({\n changes: (changeset.changes as StagedChange[]).map((c) => [c.sequenceNumber ?? null, c.type, c.elementId]),\n lastSequence: changeset.lastSequence ?? null,\n modified: changeset.modified,\n status: changeset.status,\n baseElements: await new BaseSnapshotManager().captureSnapshot(model),\n baseRelationships: model\n .getBaseRelationships()\n .map((r) => `${r.source}::${r.predicate}::${r.target}`)\n .sort(),\n });\n}\n\nfunction refusal(message: string, suggestions: string[]): CLIError {\n return new CLIError(message, ErrorCategory.USER, suggestions, { operation: \"changeset_commit\" });\n}\n\n/** Wraps a StagingAreaManager error (plain Error) so the agent sees what blocked the commit. */\nfunction commitError(error: unknown, name: string): CLIError {\n if (error instanceof CLIError) return error;\n const message = getErrorMessage(error);\n if (message.includes(\"drifted\")) {\n return new CLIError(`Changeset '${name}' was not committed: ${message.replace(/Use --force/, \"Pass force: true\")}`, ErrorCategory.USER, [\n 'Read the drift with \"changeset_brief\" and tell the user what changed in the base',\n \"If they still want it landed, call changeset_commit again with force: true\",\n ]);\n }\n if (message.startsWith(\"Validation failed\")) {\n return new CLIError(\n `Changeset '${name}' was not committed. ${message.replace(/, or skip validation with --no-validate\\./, \".\")}`,\n ErrorCategory.VALIDATION,\n ['Fix the plan (model_update, changeset_unstage, ...) and check it with \"model_validate\"']\n );\n }\n return new CLIError(`Changeset '${name}' was not committed: ${message}`, ErrorCategory.SYSTEM);\n}\n\nasync function askForConfirmation(context: McpToolContext, message: string): Promise<ElicitResult> {\n return context.server!.elicitInput(\n {\n mode: \"form\",\n message,\n requestedSchema: {\n type: \"object\",\n properties: {\n confirm: {\n type: \"boolean\",\n title: \"Commit this changeset\",\n description: \"Write the staged changes to the base model.\",\n default: false,\n },\n },\n required: [\"confirm\"],\n },\n },\n { relatedRequestId: context.requestId, signal: context.signal, timeout: CONFIRMATION_TIMEOUT_MS }\n );\n}\n\nexport async function changesetCommitHandler(\n args: ChangesetCommitArgs,\n context?: McpToolContext\n): Promise<CallToolResult> {\n return runTool(async () => {\n if (isChangesetOnly()) {\n throw refusal(\n `This session can't commit a changeset: it is in changeset-only mode (${CHANGESET_ONLY_ENV}), ` +\n \"where no person can confirm the commit.\",\n [\"Tell the user the changeset is ready to land\", ...otherWaysToLand()]\n );\n }\n\n // The cached model names the project; the commit runs on a fresh read, like the viewer's.\n const rootPath = (await loadModel(args.rootPath)).rootPath;\n let model = await Model.load(rootPath, { lazyLoad: false });\n let manager = new StagingAreaManager(model.rootPath, model);\n const changeset = await resolveChangeset(manager, args.changeset, \"changeset\");\n if (changeset.status !== \"staged\") {\n throw new CLIError(`Changeset '${changeset.name}' is ${changeset.status}; only a staged changeset can be committed`, ErrorCategory.USER, [\n 'Use \"changeset_list\" to find a staged changeset',\n ]);\n }\n if (changeset.changes.length === 0) {\n return jsonResult({ id: changeset.id, name: changeset.name, status: \"nothing-to-commit\", stagedChanges: 0 });\n }\n\n const server = context?.server;\n if (!server?.getClientCapabilities()?.elicitation?.form) {\n throw refusal(\n `Changeset '${changeset.name}' was not committed: committing needs a person to confirm it, and this client ` +\n \"can't show a confirmation dialog (it does not support MCP elicitation).\",\n otherWaysToLand()\n );\n }\n\n // Check drift and validation before asking, so the person never confirms a commit that would fail.\n const hash = await manager.detectDrift(changeset.id);\n let drift: ChangesetDrift | undefined;\n try {\n drift = await describeDrift(model, await ChangesetGraph.build(model, changeset), hash);\n } catch (error) {\n console.error(`[mcp] changeset_commit could not describe drift: ${getErrorMessage(error)}`);\n }\n if (hash.isDrifted && args.force !== true) {\n // The CLI's rule: a drifted changeset needs --force. Say what drifted so the person can decide.\n throw new CLIError(\n `Changeset '${changeset.name}' was not committed: the base model changed since it was created.\\n` +\n driftLines(drift, hash, false).slice(1).join(\"\\n\"),\n ErrorCategory.USER,\n [\n 'Read the drift with \"changeset_brief\" and tell the user what changed in the base',\n \"If they still want it landed, call changeset_commit again with force: true; the dialog will show the drift\",\n ],\n { operation: \"changeset_commit\" }\n );\n }\n let preExistingErrors = 0;\n try {\n const dryRun = await manager.commit(model, changeset.id, { dryRun: true, validate: true, force: args.force === true });\n preExistingErrors = dryRun.validation.preExistingErrors ?? 0;\n } catch (error) {\n throw commitError(error, changeset.name);\n }\n\n // An as-built changeset landing while the plan is staged: the plan should land instead.\n const warnings: string[] = [];\n const asBuiltFirst = await asBuiltFirstWarning(manager, rootPath, changeset);\n if (asBuiltFirst) warnings.push(asBuiltFirst);\n\n // What the person is shown; checked again after they answer.\n const shown = await confirmedState(model, changeset);\n let answer: ElicitResult;\n try {\n answer = await askForConfirmation(\n context!,\n confirmationMessage(changeset, drift, hash, preExistingErrors, args.force === true, warnings)\n );\n } catch (error) {\n throw refusal(\n `Changeset '${changeset.name}' was not committed: the confirmation could not be shown (${getErrorMessage(error)}).`,\n otherWaysToLand()\n );\n }\n if (answer.action !== \"accept\" || answer.content?.confirm !== true) {\n const how = answer.action === \"accept\" ? \"did not confirm it\" : answer.action === \"decline\" ? \"declined\" : \"cancelled\";\n throw refusal(`Changeset '${changeset.name}' was not committed: the user ${how}. Nothing was written.`, [\n \"Ask the user what should change before landing it, or leave it staged\",\n ]);\n }\n\n // The person may have taken a while. Commit only what they saw: re-read the changeset and the\n // base, and refuse if either changed. The commit itself checks drift and validation again.\n model = await Model.load(rootPath, { lazyLoad: false });\n manager = new StagingAreaManager(model.rootPath, model);\n const current = await manager.load(changeset.id);\n if (!current || (await confirmedState(model, current)) !== shown) {\n throw refusal(\n `Changeset '${changeset.name}' was not committed: the changeset or the base model changed while the user ` +\n \"was confirming. Nothing was written.\",\n ['Show the user the changeset as it is now (\"changeset_brief\"), then ask again with changeset_commit']\n );\n }\n const wasActive = (await manager.getActiveId()) === changeset.id;\n let result;\n try {\n result = await manager.commit(model, changeset.id, { validate: true, force: args.force === true });\n } catch (error) {\n throw commitError(error, changeset.name);\n }\n if (wasActive) await manager.clearActive();\n await reloadModel(args.rootPath);\n\n return jsonResult({\n id: changeset.id,\n name: changeset.name,\n status: \"committed\",\n committed: result.committed,\n skipped: result.skipped ?? 0,\n ...(result.skippedDetails?.length ? { skippedDetails: result.skippedDetails.slice(0, 10) } : {}),\n failed: result.failed,\n preExistingErrors: result.validation.preExistingErrors ?? 0,\n deactivated: wasActive,\n // StagingAreaManager.commit reports driftWarning only on a dry run, so use the check made above.\n ...(result.driftWarning || hash.isDrifted ? { driftOverridden: true } : {}),\n ...(warnings.length > 0 ? { warnings } : {}),\n });\n });\n}\n\nexport const changesetCommitTool: McpToolDefinition<typeof inputSchema> = {\n name: \"changeset_commit\",\n description:\n \"Land a staged changeset: write its changes to the base model and mark it committed. Only after a person \" +\n \"confirms it: the server asks the user in a confirmation dialog (MCP elicitation) showing the changeset's \" +\n \"intent, counts, layers and drift, and commits only if they confirm. Refused when the client can't show the \" +\n \"dialog, when the user declines or cancels, and in changeset-only sessions. Validation errors the changeset \" +\n \"introduces block it; drift blocks it unless force is passed. Call it only when the user asks to land the change.\",\n inputSchema,\n _meta: { \"anthropic/requiresUserInteraction\": true },\n handler: changesetCommitHandler,\n};\n"],
5
+ "mappings": "AAsBA,SAAS,SAAS;AAElB,SAAS,aAAa;AAEtB,SAAS,2BAA2B;AACpC,SAAS,sBAAsB;AAC/B,SAAS,qBAA0C;AACnD,SAAS,0BAA4C;AACrD,SAAS,iBAAiB,0BAA0B;AACpD,SAAS,UAAU,eAAe,uBAAuB;AACzD,SACE,YACA,WACA,aACA,gBACA,eAGK;AACP,SAAS,wBAAwB;AACjC,SAAS,6BAA6B;AAG/B,MAAM,0BAA0B,KAAK;AAMtC,SAAU,kBAAe;AAC7B,SAAO;IACL;IACA;;AAEJ;AAEA,MAAM,cAAc;EAClB,WAAW,EACR,OAAM,EACN,IAAI,CAAC,EACL,SAAS,gFAAgF;EAC5F,OAAO,EACJ,QAAO,EACP,SAAQ,EACR,SACC,sJACoD;EAExD,UAAU;GAKN,mBAA2C;EAC/C,KAAK;EACL,QAAQ;EACR,QAAQ;EACR,oBAAoB;EACpB,uBAAuB;;AAInB,SAAU,mBAAmB,WAAoB;AACrD,QAAM,cAAsC,CAAA,GACtC,SAAS,oBAAI,IAAG;AACtB,aAAW,UAAU,UAAU;AAC7B,gBAAY,OAAO,IAAI,KAAK,YAAY,OAAO,IAAI,KAAK,KAAK,GACzD,OAAO,aAAW,OAAO,IAAI,OAAO,SAAS;AAEnD,SAAO,EAAE,aAAa,QAAQ,CAAC,GAAG,MAAM,EAAE,KAAI,EAAE;AAClD;AAEA,SAAS,WAAW,OAAmC,MAAmB,QAAe;AACvF,QAAM,cAAc,QAAQ,MAAM,YAAY,SAAS,SAAS,MAAM,YAAY,cAAc,SAAS;AACzG,MAAI,CAAC,KAAK,aAAa,CAAC,OAAO;AAAW,WAAO,CAAC,mEAAmE;AACrH,QAAM,QAAQ,CAAC,iEAAiE,GAC1E,SAAS,OAAO,aAAa;AACnC,MAAI,QAAQ;AACV,UAAM,QAAQ;MACZ,CAAC,OAAO,eAAe,gBAAgB;MACvC,CAAC,OAAO,iBAAiB,kBAAkB;MAC3C,CAAC,OAAO,iBAAiB,kBAAkB;MAC3C,CAAC,OAAO,oBAAoB,qBAAqB;MACjD,CAAC,OAAO,sBAAsB,uBAAuB;MAEpD,OAAO,CAAC,CAAC,CAAC,MAAO,IAAe,CAAC,EACjC,IAAI,CAAC,CAAC,GAAG,KAAK,MAAM,GAAG,CAAC,IAAI,KAAK,EAAE;AACtC,IAAI,MAAM,SAAS,KAAG,MAAM,KAAK,kBAAkB,MAAM,KAAK,IAAI,CAAC,GAAG;EACxE;AACA,MAAI,SAAS,cAAc,GAAG;AAC5B,UAAM,SAAS;MACb,GAAG,MAAM,YAAY,SAAS,IAAI,CAAC,MAAM,GAAG,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG;MAChE,GAAG,MAAM,YAAY,cAAc,IAAI,CAAC,MAAM,GAAG,EAAE,MAAM,MAAM,EAAE,SAAS,OAAO,EAAE,MAAM,KAAK,EAAE,MAAM,GAAG;;AAE3G,UAAM,KACJ,KAAK,WAAW,uCAAuC,OAAO,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,GAAG,cAAc,IAAI,UAAU,EAAE,GAAG;EAE5H;AACA,SAAK,KAAK,YACD,UAAQ,MAAM,KAAK,wCAAwC,IAD/C,MAAM,KAAK,oEAAoE,GAE7F;AACT;AAGA,MAAM,oBAAoB;AAM1B,eAAe,iBAAiB,UAAkB,WAAoB;AACpE,SAAI,kBAAkB,KAAK,UAAU,eAAe,EAAE,IAAU,KACzD,CAAC,CAAC,UAAU,MAAO,MAAM,sBAAsB,UAAU,UAAU,IAAI,UAAU,IAAI;AAC9F;AAQA,eAAsB,oBACpB,SACA,UACA,WAAoB;AAEpB,MAAI,CAAE,MAAM,iBAAiB,UAAU,SAAS;AAAI;AACpD,QAAM,QAAqB,CAAA;AAC3B,aAAW,SAAS,MAAM,QAAQ,KAAI;AACpC,IAAI,MAAM,OAAO,UAAU,MAAM,MAAM,WAAW,YAAY,MAAM,QAAQ,WAAW,KACjF,MAAM,iBAAiB,UAAU,KAAK,KAAI,MAAM,KAAK,KAAK;AAElE,MAAI,MAAM,WAAW;AAAG;AACxB,QAAM,QAAQ,MAAM,IAAI,CAAC,SAAS,IAAI,KAAK,IAAI,GAAG,EAAE,KAAK,IAAI;AAC7D,SACE,aAAa,UAAU,IAAI,kEAAkE,KAAK;AAKtG;AAGM,SAAU,oBACd,WACA,OACA,MACA,mBACA,QACA,WAAqB,CAAA,GAAE;AAEvB,QAAM,EAAE,aAAa,OAAM,IAAK,mBAAmB,SAAS,GACtD,SAAS,OAAO,QAAQ,WAAW,EACtC,IAAI,CAAC,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,iBAAiB,IAAI,KAAK,IAAI,EAAE,EAC3D,KAAK,IAAI;AACZ,SAAO;IACL,qBAAqB,UAAU,IAAI;IACnC;IACA,GAAG,SAAS,QAAQ,CAAC,YAAY,CAAC,SAAS,EAAE,CAAC;IAC9C,WAAW,UAAU,aAAa,KAAI,KAAM,kBAAkB;IAC9D,YAAY,UAAU,QAAQ,MAAM,KAAK,MAAM;IAC/C,WAAW,OAAO,KAAK,IAAI,KAAK,MAAM;IACtC,GAAG,WAAW,OAAO,MAAM,MAAM;IACjC,oBAAoB,IAChB,uBAAuB,iBAAiB,yDACxC;IACJ;IACA;IACA,KAAK;CAAI;AACb;AAMA,eAAe,eAAe,OAAc,WAAoB;AAC9D,SAAO,KAAK,UAAU;IACpB,SAAU,UAAU,QAA2B,IAAI,CAAC,MAAM,CAAC,EAAE,kBAAkB,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC;IACzG,cAAc,UAAU,gBAAgB;IACxC,UAAU,UAAU;IACpB,QAAQ,UAAU;IAClB,cAAc,MAAM,IAAI,oBAAmB,EAAG,gBAAgB,KAAK;IACnE,mBAAmB,MAChB,qBAAoB,EACpB,IAAI,CAAC,MAAM,GAAG,EAAE,MAAM,KAAK,EAAE,SAAS,KAAK,EAAE,MAAM,EAAE,EACrD,KAAI;GACR;AACH;AAEA,SAAS,QAAQ,SAAiB,aAAqB;AACrD,SAAO,IAAI,SAAS,SAAS,cAAc,MAAM,aAAa,EAAE,WAAW,mBAAkB,CAAE;AACjG;AAGA,SAAS,YAAY,OAAgB,MAAY;AAC/C,MAAI,iBAAiB;AAAU,WAAO;AACtC,QAAM,UAAU,gBAAgB,KAAK;AACrC,SAAI,QAAQ,SAAS,SAAS,IACrB,IAAI,SAAS,cAAc,IAAI,wBAAwB,QAAQ,QAAQ,eAAe,kBAAkB,CAAC,IAAI,cAAc,MAAM;IACtI;IACA;GACD,IAEC,QAAQ,WAAW,mBAAmB,IACjC,IAAI,SACT,cAAc,IAAI,wBAAwB,QAAQ,QAAQ,6CAA6C,GAAG,CAAC,IAC3G,cAAc,YACd,CAAC,wFAAwF,CAAC,IAGvF,IAAI,SAAS,cAAc,IAAI,wBAAwB,OAAO,IAAI,cAAc,MAAM;AAC/F;AAEA,eAAe,mBAAmB,SAAyB,SAAe;AACxE,SAAO,QAAQ,OAAQ,YACrB;IACE,MAAM;IACN;IACA,iBAAiB;MACf,MAAM;MACN,YAAY;QACV,SAAS;UACP,MAAM;UACN,OAAO;UACP,aAAa;UACb,SAAS;;;MAGb,UAAU,CAAC,SAAS;;KAGxB,EAAE,kBAAkB,QAAQ,WAAW,QAAQ,QAAQ,QAAQ,SAAS,wBAAuB,CAAE;AAErG;AAEA,eAAsB,uBACpB,MACA,SAAwB;AAExB,SAAO,QAAQ,YAAW;AACxB,QAAI,gBAAe;AACjB,YAAM,QACJ,wEAAwE,kBAAkB,8CAE1F,CAAC,gDAAgD,GAAG,gBAAe,CAAE,CAAC;AAK1E,UAAM,YAAY,MAAM,UAAU,KAAK,QAAQ,GAAG;AAClD,QAAI,QAAQ,MAAM,MAAM,KAAK,UAAU,EAAE,UAAU,GAAK,CAAE,GACtD,UAAU,IAAI,mBAAmB,MAAM,UAAU,KAAK;AAC1D,UAAM,YAAY,MAAM,iBAAiB,SAAS,KAAK,WAAW,WAAW;AAC7E,QAAI,UAAU,WAAW;AACvB,YAAM,IAAI,SAAS,cAAc,UAAU,IAAI,QAAQ,UAAU,MAAM,8CAA8C,cAAc,MAAM;QACvI;OACD;AAEH,QAAI,UAAU,QAAQ,WAAW;AAC/B,aAAO,WAAW,EAAE,IAAI,UAAU,IAAI,MAAM,UAAU,MAAM,QAAQ,qBAAqB,eAAe,EAAC,CAAE;AAI7G,QAAI,CADW,SAAS,QACX,sBAAqB,GAAI,aAAa;AACjD,YAAM,QACJ,cAAc,UAAU,IAAI,yJAE5B,gBAAe,CAAE;AAKrB,UAAM,OAAO,MAAM,QAAQ,YAAY,UAAU,EAAE;AACnD,QAAI;AACJ,QAAI;AACF,cAAQ,MAAM,cAAc,OAAO,MAAM,eAAe,MAAM,OAAO,SAAS,GAAG,IAAI;IACvF,SAAS,OAAO;AACd,cAAQ,MAAM,oDAAoD,gBAAgB,KAAK,CAAC,EAAE;IAC5F;AACA,QAAI,KAAK,aAAa,KAAK,UAAU;AAEnC,YAAM,IAAI,SACR,cAAc,UAAU,IAAI;IAC1B,WAAW,OAAO,MAAM,EAAK,EAAE,MAAM,CAAC,EAAE,KAAK;CAAI,GACnD,cAAc,MACd;QACE;QACA;SAEF,EAAE,WAAW,mBAAkB,CAAE;AAGrC,QAAI,oBAAoB;AACxB,QAAI;AAEF,2BADe,MAAM,QAAQ,OAAO,OAAO,UAAU,IAAI,EAAE,QAAQ,IAAM,UAAU,IAAM,OAAO,KAAK,UAAU,GAAI,CAAE,GAC1F,WAAW,qBAAqB;IAC7D,SAAS,OAAO;AACd,YAAM,YAAY,OAAO,UAAU,IAAI;IACzC;AAGA,UAAM,WAAqB,CAAA,GACrB,eAAe,MAAM,oBAAoB,SAAS,UAAU,SAAS;AAC3E,IAAI,gBAAc,SAAS,KAAK,YAAY;AAG5C,UAAM,QAAQ,MAAM,eAAe,OAAO,SAAS;AACnD,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,mBACb,SACA,oBAAoB,WAAW,OAAO,MAAM,mBAAmB,KAAK,UAAU,IAAM,QAAQ,CAAC;IAEjG,SAAS,OAAO;AACd,YAAM,QACJ,cAAc,UAAU,IAAI,6DAA6D,gBAAgB,KAAK,CAAC,MAC/G,gBAAe,CAAE;IAErB;AACA,QAAI,OAAO,WAAW,YAAY,OAAO,SAAS,YAAY,IAAM;AAClE,YAAM,MAAM,OAAO,WAAW,WAAW,uBAAuB,OAAO,WAAW,YAAY,aAAa;AAC3G,YAAM,QAAQ,cAAc,UAAU,IAAI,iCAAiC,GAAG,0BAA0B;QACtG;OACD;IACH;AAIA,YAAQ,MAAM,MAAM,KAAK,UAAU,EAAE,UAAU,GAAK,CAAE,GACtD,UAAU,IAAI,mBAAmB,MAAM,UAAU,KAAK;AACtD,UAAM,UAAU,MAAM,QAAQ,KAAK,UAAU,EAAE;AAC/C,QAAI,CAAC,WAAY,MAAM,eAAe,OAAO,OAAO,MAAO;AACzD,YAAM,QACJ,cAAc,UAAU,IAAI,oHAE5B,CAAC,oGAAoG,CAAC;AAG1G,UAAM,YAAa,MAAM,QAAQ,YAAW,MAAQ,UAAU;AAC9D,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,QAAQ,OAAO,OAAO,UAAU,IAAI,EAAE,UAAU,IAAM,OAAO,KAAK,UAAU,GAAI,CAAE;IACnG,SAAS,OAAO;AACd,YAAM,YAAY,OAAO,UAAU,IAAI;IACzC;AACA,WAAI,aAAW,MAAM,QAAQ,YAAW,GACxC,MAAM,YAAY,KAAK,QAAQ,GAExB,WAAW;MAChB,IAAI,UAAU;MACd,MAAM,UAAU;MAChB,QAAQ;MACR,WAAW,OAAO;MAClB,SAAS,OAAO,WAAW;MAC3B,GAAI,OAAO,gBAAgB,SAAS,EAAE,gBAAgB,OAAO,eAAe,MAAM,GAAG,EAAE,EAAC,IAAK,CAAA;MAC7F,QAAQ,OAAO;MACf,mBAAmB,OAAO,WAAW,qBAAqB;MAC1D,aAAa;;MAEb,GAAI,OAAO,gBAAgB,KAAK,YAAY,EAAE,iBAAiB,GAAI,IAAK,CAAA;MACxE,GAAI,SAAS,SAAS,IAAI,EAAE,SAAQ,IAAK,CAAA;KAC1C;EACH,CAAC;AACH;AAEO,MAAM,sBAA6D;EACxE,MAAM;EACN,aACE;EAKF;EACA,OAAO,EAAE,qCAAqC,GAAI;EAClD,SAAS;;",
6
+ "names": []
7
+ }
@@ -0,0 +1,260 @@
1
+ import { z } from "zod";
2
+ import { strictItem } from "../strict-schema.js";
3
+ import { ChangesetGraph } from "../../core/changeset-graph.js";
4
+ import { StagingAreaManager } from "../../core/staging-area.js";
5
+ import { stagedRelationshipChangesAtEnds, stagedRelationshipTriple } from "../../core/relationship-staging.js";
6
+ import { CLIError, ErrorCategory, getErrorMessage } from "../../utils/errors.js";
7
+ import { jsonResult, loadModel, reloadModel, rootPathSchema, runTool } from "./shared.js";
8
+ import { resolveChangeset } from "./changeset-shared.js";
9
+ const changesetIdSchema = z.string().optional().describe("Changeset ID or name. Defaults to the active changeset.");
10
+ function requireStaged(changeset, action) {
11
+ if (changeset.status !== "staged")
12
+ throw new CLIError(`Changeset '${changeset.name}' is ${changeset.status}; cannot ${action}`, ErrorCategory.USER, [
13
+ 'Use "changeset_list" to find a staged changeset, or "changeset_create" to start a new one'
14
+ ]);
15
+ }
16
+ async function asUserError(fn, suggestions) {
17
+ try {
18
+ return await fn();
19
+ } catch (error) {
20
+ throw error instanceof CLIError ? error : new CLIError(getErrorMessage(error), ErrorCategory.USER, suggestions);
21
+ }
22
+ }
23
+ function summarize(change) {
24
+ const triple = change.type.startsWith("relationship-") ? stagedRelationshipTriple(change) : null;
25
+ return {
26
+ type: change.type,
27
+ elementId: change.elementId,
28
+ layer: change.layerName,
29
+ ...triple ? { relationship: triple } : {}
30
+ };
31
+ }
32
+ const updateInputSchema = {
33
+ changesetId: changesetIdSchema,
34
+ name: z.string().min(1).optional().describe("New name. Must not be another changeset's name."),
35
+ description: z.string().optional().describe("New description: the change's intent and what done looks like. An empty string clears it."),
36
+ rootPath: rootPathSchema
37
+ };
38
+ async function changesetUpdateHandler(args) {
39
+ return runTool(async () => {
40
+ if (args.name === void 0 && args.description === void 0)
41
+ throw new CLIError("Nothing to update", ErrorCategory.USER, ['Pass "name" and/or "description"']);
42
+ const model = await loadModel(args.rootPath), manager = new StagingAreaManager(model.rootPath, model), changeset = await resolveChangeset(manager, args.changesetId), updated = await asUserError(() => manager.update(changeset.id, { name: args.name, description: args.description }), ['Use "changeset_list" to see the names already taken']);
43
+ return await reloadModel(args.rootPath), jsonResult({
44
+ id: updated.id,
45
+ name: updated.name,
46
+ ...changeset.name !== updated.name ? { previousName: changeset.name } : {},
47
+ description: updated.description ?? null,
48
+ status: updated.status,
49
+ changes: updated.changes.length
50
+ });
51
+ });
52
+ }
53
+ const changesetUpdateTool = {
54
+ name: "changeset_update",
55
+ description: "Rename a changeset or change its description (the change's intent). Staged changes are untouched. Defaults to the active changeset.",
56
+ inputSchema: updateInputSchema,
57
+ handler: changesetUpdateHandler
58
+ }, elementIdField = z.string().optional().describe("Drop every staged change to this element (its add, updates or delete). ID or path."), sourceField = z.string().optional().describe("Drop staged relationship changes (adds and deletes) from this element: to target when given, otherwise to any element. The element's own changes are not touched."), targetField = z.string().optional().describe("Drop staged relationship changes (adds and deletes) to this element: from source when given, otherwise from any element. The element's own changes are not touched."), predicateField = z.string().optional().describe("Only relationship changes with this predicate. Omit for any."), keepRelationshipsField = z.boolean().optional().describe("With elementId: keep the staged relationship changes that depend on the element's change. By default, unstaging an element's add also drops staged relationships to it (they would dangle), and unstaging its delete also drops the relationship removals its cascade staged (removals staged before the delete are kept)."), unstageItemSchema = strictItem("A changes[] item", {
59
+ elementId: elementIdField,
60
+ source: sourceField,
61
+ target: targetField,
62
+ predicate: predicateField,
63
+ keepRelationships: keepRelationshipsField
64
+ }), unstageInputSchema = {
65
+ changesetId: changesetIdSchema,
66
+ elementId: elementIdField,
67
+ source: sourceField,
68
+ target: targetField,
69
+ predicate: predicateField,
70
+ keepRelationships: keepRelationshipsField,
71
+ changes: z.array(unstageItemSchema).min(1).optional().describe("Several unstages at once, each {elementId} or {source, predicate?, target} or {source} or {target}, with the same meaning as the single form. All are removed in one write, with a result per item: unstaged, already-unstaged (an earlier item took its changes), not-staged, or invalid with the reason. Cannot be combined with the single-form fields."),
72
+ rootPath: rootPathSchema
73
+ };
74
+ function describeEnds(args) {
75
+ return args.source && args.target ? `from ${args.source} to ${args.target}` : args.source ? `from ${args.source}` : `to ${args.target}`;
76
+ }
77
+ function checkUnstageSpec(spec) {
78
+ const byElement = spec.elementId !== void 0, byRelationship = spec.source !== void 0 || spec.target !== void 0;
79
+ if (byElement === byRelationship)
80
+ throw new CLIError("Name either an element or a relationship to unstage", ErrorCategory.USER, [
81
+ `Pass "elementId" to drop an element's staged changes`,
82
+ 'Or pass "source" and/or "target" (and optionally "predicate") to drop staged relationship changes'
83
+ ]);
84
+ if (byRelationship && !spec.source && !spec.target)
85
+ throw new CLIError('"source" or "target" must not be empty', ErrorCategory.USER);
86
+ }
87
+ function selectUnstage(spec, graph, changeset) {
88
+ const changes = changeset.changes, keys = /* @__PURE__ */ new Set(), notes = [];
89
+ if (spec.elementId !== void 0) {
90
+ const element = graph.getElement(spec.elementId), ids = /* @__PURE__ */ new Set([spec.elementId, ...element ? [element.id, element.uuid ?? element.id] : []]), own = changes.filter((c) => !c.type.startsWith("relationship-") && ids.has(c.elementId));
91
+ for (const change of own)
92
+ keys.add(change.elementId);
93
+ const dependentType = own.some((c) => c.type === "add") ? "relationship-add" : own.some((c) => c.type === "delete") ? "relationship-delete" : null, touching = dependentType ? changes.filter((c) => {
94
+ if (c.type !== dependentType)
95
+ return !1;
96
+ const triple = stagedRelationshipTriple(c);
97
+ return !!triple && (ids.has(triple.source) || ids.has(triple.target) || element && (graph.resolve(triple.source) === element.id || graph.resolve(triple.target) === element.id));
98
+ }) : [], seq2 = (c) => c.sequenceNumber ?? changes.indexOf(c), deleteSeqs = own.filter((c) => c.type === "delete").map(seq2), firstDelete = deleteSeqs.length > 0 ? Math.min(...deleteSeqs) : -1, dependent = dependentType === "relationship-delete" ? touching.filter((c) => seq2(c) > firstDelete) : touching, deliberate = touching.filter((c) => !dependent.includes(c));
99
+ if (deliberate.length > 0 && notes.push(`${deliberate.length} relationship removal(s) on ${spec.elementId} staged before its delete were kept (${deliberate.map((c) => c.elementId).join(", ")}); unstage them by source and target if they should go too`), dependent.length > 0)
100
+ if (spec.keepRelationships)
101
+ notes.push(`${dependent.length} staged relationship change(s) on ${spec.elementId} were kept (keepRelationships)` + (dependentType === "relationship-add" ? "; they will dangle unless the element is staged again" : ""));
102
+ else
103
+ for (const change of dependent)
104
+ keys.add(change.elementId);
105
+ return { keys, notes };
106
+ }
107
+ const matched = stagedRelationshipChangesAtEnds(graph, changeset, {
108
+ source: spec.source || void 0,
109
+ target: spec.target || void 0,
110
+ predicate: spec.predicate
111
+ }), seq = (c) => c.sequenceNumber ?? changes.indexOf(c), deleteSeq = /* @__PURE__ */ new Map();
112
+ for (const change of changes) {
113
+ if (change.type !== "delete")
114
+ continue;
115
+ const id = graph.resolve(change.elementId);
116
+ deleteSeq.set(id, Math.min(deleteSeq.get(id) ?? 1 / 0, seq(change)));
117
+ }
118
+ const deletedEndOf = (change) => {
119
+ if (change.type !== "relationship-delete")
120
+ return;
121
+ const triple = stagedRelationshipTriple(change);
122
+ if (triple)
123
+ return [graph.resolve(triple.source), graph.resolve(triple.target)].find((end) => deleteSeq.has(end) && deleteSeq.get(end) < seq(change));
124
+ }, cascaded = matched.filter((c) => deletedEndOf(c) !== void 0), deletedEnds = [...new Set(cascaded.map((c) => deletedEndOf(c)))].join(", "), cascadeList = cascaded.map((c) => c.elementId).join(", "), bothEnds = !!spec.source && !!spec.target;
125
+ for (const change of matched)
126
+ !bothEnds && cascaded.includes(change) || keys.add(change.elementId);
127
+ return cascaded.length > 0 && bothEnds ? notes.push(`Unstaged ${cascaded.length} relationship removal(s) cascaded from the staged delete of ${deletedEnds} (${cascadeList}): those relationships now dangle, so the changeset will not commit until they are removed again or the delete is unstaged ("changeset_unstage" with elementId)`) : cascaded.length > 0 && notes.push(`Kept ${cascaded.length} relationship removal(s) cascaded from the staged delete of ${deletedEnds} (${cascadeList}): dropping them would leave those relationships dangling. Unstage the delete ("changeset_unstage" with elementId) to restore them, or name both source and target to drop one anyway`), { keys, notes };
128
+ }
129
+ function notStagedMessage(spec, changesetName) {
130
+ return spec.elementId !== void 0 ? `No staged change to ${spec.elementId} in '${changesetName}'` : `No staged relationship change ${describeEnds(spec)}${spec.predicate ? ` (${spec.predicate})` : ""} in '${changesetName}'`;
131
+ }
132
+ const SINGLE_FORM_FIELDS = ["elementId", "source", "target", "predicate", "keepRelationships"];
133
+ async function changesetUnstageHandler(args) {
134
+ return runTool(async () => {
135
+ if (args.changes) {
136
+ const mixed = SINGLE_FORM_FIELDS.filter((field) => args[field] !== void 0);
137
+ if (mixed.length > 0)
138
+ throw new CLIError(`"changes" cannot be combined with ${mixed.map((f) => `"${f}"`).join(", ")}`, ErrorCategory.USER, [
139
+ 'Put those fields on the items of the "changes" list'
140
+ ]);
141
+ return unstageBatch(args.changes, args.changesetId, args.rootPath);
142
+ }
143
+ checkUnstageSpec(args);
144
+ const model = await loadModel(args.rootPath), manager = new StagingAreaManager(model.rootPath, model), changeset = await resolveChangeset(manager, args.changesetId);
145
+ requireStaged(changeset, "unstage changes");
146
+ const graph = await ChangesetGraph.build(model, changeset), changes = changeset.changes, { keys, notes } = selectUnstage(args, graph, changeset), removed = changes.filter((c) => keys.has(c.elementId));
147
+ if (removed.length === 0)
148
+ return jsonResult({
149
+ status: "not-staged",
150
+ changeset: changeset.name,
151
+ message: notStagedMessage(args, changeset.name),
152
+ suggestions: ['Use "changeset_show" to list the staged changes'],
153
+ ...notes.length > 0 ? { notes } : {}
154
+ });
155
+ for (const key of keys)
156
+ await manager.unstage(changeset.id, key);
157
+ return await reloadModel(args.rootPath), jsonResult({
158
+ status: "unstaged",
159
+ changeset: changeset.name,
160
+ unstaged: removed.map(summarize),
161
+ remainingChanges: changes.length - removed.length,
162
+ ...notes.length > 0 ? { notes } : {}
163
+ });
164
+ });
165
+ }
166
+ async function unstageBatch(specs, changesetId, rootPath) {
167
+ const model = await loadModel(rootPath), manager = new StagingAreaManager(model.rootPath, model), changeset = await resolveChangeset(manager, changesetId);
168
+ requireStaged(changeset, "unstage changes");
169
+ const graph = await ChangesetGraph.build(model, changeset), changes = changeset.changes, claimed = /* @__PURE__ */ new Set(), results = [];
170
+ for (const [index, spec] of specs.entries()) {
171
+ const asSent = {
172
+ index,
173
+ status: "unstaged",
174
+ ...spec.elementId !== void 0 ? { elementId: spec.elementId } : {},
175
+ ...spec.source !== void 0 ? { source: spec.source } : {},
176
+ ...spec.target !== void 0 ? { target: spec.target } : {},
177
+ ...spec.predicate !== void 0 ? { predicate: spec.predicate } : {}
178
+ };
179
+ let selection;
180
+ try {
181
+ checkUnstageSpec(spec), selection = selectUnstage(spec, graph, changeset);
182
+ } catch (error) {
183
+ results.push({
184
+ ...asSent,
185
+ status: "invalid",
186
+ reason: getErrorMessage(error),
187
+ ...error instanceof CLIError && error.suggestions?.length ? { suggestions: error.suggestions } : {}
188
+ });
189
+ continue;
190
+ }
191
+ const notes = [...selection.notes], fresh = [...selection.keys].filter((key) => !claimed.has(key)), earlier = selection.keys.size - fresh.length;
192
+ for (const key of fresh)
193
+ claimed.add(key);
194
+ const removed = changes.filter((c) => fresh.includes(c.elementId)), status = removed.length > 0 ? "unstaged" : earlier > 0 ? "already-unstaged" : "not-staged";
195
+ earlier > 0 && notes.push(`${earlier} of its change(s) were already unstaged by an earlier item`), results.push({
196
+ ...asSent,
197
+ status,
198
+ ...removed.length > 0 ? { unstaged: removed.map(summarize) } : {},
199
+ ...status === "not-staged" ? { message: notStagedMessage(spec, changeset.name) } : {},
200
+ ...notes.length > 0 ? { notes } : {}
201
+ });
202
+ }
203
+ claimed.size > 0 && (await manager.unstageMany(changeset.id, [...claimed]), await reloadModel(rootPath));
204
+ const counts = {};
205
+ for (const result of results)
206
+ counts[result.status] = (counts[result.status] ?? 0) + 1;
207
+ const removedCount = changes.filter((c) => claimed.has(c.elementId)).length;
208
+ return jsonResult({
209
+ changeset: changeset.name,
210
+ counts,
211
+ results,
212
+ unstagedChanges: removedCount,
213
+ remainingChanges: changes.length - removedCount,
214
+ ...results.some((r) => r.status === "not-staged") ? { suggestions: ['Use "changeset_show" to list the staged changes'] } : {}
215
+ });
216
+ }
217
+ const changesetUnstageTool = {
218
+ name: "changeset_unstage",
219
+ description: "Undo part of a staged changeset: drop the staged changes to one element (elementId), or the staged relationship adds/deletes between source and target, or every one from source alone or to target alone (optionally one predicate). To drop several, send changes: [...] (one write, a result per item). The base model is not touched. Defaults to the active changeset. Use changeset_show to see what is staged.",
220
+ inputSchema: unstageInputSchema,
221
+ handler: changesetUnstageHandler
222
+ }, discardInputSchema = {
223
+ changesetId: z.string().describe("Changeset ID or name to discard. Required: discard is never implied."),
224
+ confirm: z.boolean().describe("Must be true. Discarding deletes the changeset's staged changes permanently (only its name, description and a 'discarded' status remain), so confirm only when the plan is to be abandoned."),
225
+ rootPath: rootPathSchema
226
+ };
227
+ async function changesetDiscardHandler(args) {
228
+ return runTool(async () => {
229
+ const model = await loadModel(args.rootPath), manager = new StagingAreaManager(model.rootPath, model), changeset = await resolveChangeset(manager, args.changesetId);
230
+ if (requireStaged(changeset, "discard it"), args.confirm !== !0)
231
+ throw new CLIError(`Discarding '${changeset.name}' deletes its ${changeset.changes.length} staged change(s) permanently`, ErrorCategory.USER, [
232
+ 'Pass "confirm": true to discard it',
233
+ 'Use "changeset_unstage" to drop individual changes instead',
234
+ 'Use "changeset_deactivate" to stop staging into it and keep its changes'
235
+ ]);
236
+ const wasActive = await manager.getActiveId() === changeset.id, discardedChanges = changeset.changes.length;
237
+ return await manager.discard(changeset.id), wasActive && await manager.clearActive(), await reloadModel(args.rootPath), jsonResult({
238
+ id: changeset.id,
239
+ name: changeset.name,
240
+ status: "discarded",
241
+ discardedChanges,
242
+ deactivated: wasActive
243
+ });
244
+ });
245
+ }
246
+ const changesetDiscardTool = {
247
+ name: "changeset_discard",
248
+ description: "Abandon a staged changeset: PERMANENTLY deletes its staged changes and marks it discarded (deactivating it if active). Not recoverable, so it requires confirm: true. The base model is not touched. To undo part of a plan use changeset_unstage; to pause it use changeset_deactivate.",
249
+ inputSchema: discardInputSchema,
250
+ handler: changesetDiscardHandler
251
+ };
252
+ export {
253
+ changesetDiscardHandler,
254
+ changesetDiscardTool,
255
+ changesetUnstageHandler,
256
+ changesetUnstageTool,
257
+ changesetUpdateHandler,
258
+ changesetUpdateTool
259
+ };
260
+ //# sourceMappingURL=changeset-editing.js.map