@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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/commands/mcp.ts"],
4
- "sourcesContent": ["/**\n * MCP server command - Starts an MCP server for AI assistant integration\n *\n * Supports two transport modes:\n * - stdio (default): In-process over stdio; stdout carries the MCP JSON-RPC protocol\n * - http: HTTP server with Express, exposing MCP at /mcp endpoint with bearer token auth\n *\n * All non-protocol output goes to stderr.\n */\n\nimport { isCancel, text } from \"@clack/prompts\";\nimport { ApiKeyManager, type ApiKeyStoragePrompt } from \"../mcp/api-key-manager.js\";\nimport { type HttpTransportApp } from \"../mcp/http-transport.js\";\nimport { McpResourceRegistry } from \"../mcp/resource-registry.js\";\nimport { McpToolRegistry } from \"../mcp/tool-registry.js\";\nimport { loadModel } from \"../mcp/tools/shared.js\";\nimport { startActiveSpan } from \"../telemetry/index.js\";\nimport { CLIError, getErrorMessage } from \"../utils/errors.js\";\nimport { getCliVersion } from \"../utils/spec-version.js\";\n\nconst cliVersion = getCliVersion();\n\n/**\n * Prompt the user (via stdout/stdin) for where to store a newly generated API key.\n * Only invoked when stdin is a TTY \u2014 non-interactive launches (e.g. an MCP client\n * spawning `dr mcp`) fall back to the default path without prompting.\n */\nconst promptForKeyPath: ApiKeyStoragePrompt = async (defaultPath) => {\n const result = await text({\n message: \"Where should the MCP API key be stored?\",\n placeholder: defaultPath,\n defaultValue: defaultPath,\n });\n\n if (isCancel(result) || typeof result !== \"string\" || result.trim().length === 0) {\n return defaultPath;\n }\n return result.trim();\n};\n\nasync function createConfiguredServer() {\n const { McpServer } = await import(\"@modelcontextprotocol/sdk/server/mcp.js\");\n\n const server = new McpServer({\n name: \"documentation-robotics\",\n version: cliVersion,\n });\n\n // Register the model tool surface and spec/model manifest resources.\n new McpToolRegistry().registerAll(server);\n await new McpResourceRegistry().registerAll(server);\n\n // Warm the model cache at startup so it's loaded once and held in memory\n // for the session, rather than waiting for the first tool call. Best-effort:\n // a server started outside a DR project still starts up cleanly \u2014 the first\n // tool call that supplies a valid rootPath will load and cache it.\n try {\n await loadModel();\n } catch (error) {\n process.stderr.write(`[mcp] model warmup skipped: ${getErrorMessage(error)}\\n`);\n }\n\n return server;\n}\n\nexport interface McpCommandOptions {\n /** Force-generate a new API key, overwrite it at the configured storage path, and exit. */\n regenerateKey?: boolean;\n /** Transport type: \"stdio\" or \"http\" (default: \"stdio\") */\n transport?: \"stdio\" | \"http\";\n /** Port for HTTP transport (default: 3100, only meaningful with --transport http) */\n port?: number;\n /** Host address for HTTP transport (default: \"127.0.0.1\", only meaningful with --transport http) */\n host?: string;\n}\n\nexport async function mcpCommand(options: McpCommandOptions = {}): Promise<void> {\n const keyManager = new ApiKeyManager();\n\n try {\n if (options.regenerateKey) {\n await startActiveSpan(\"mcp.key.rotate\", async (span) => {\n const isInteractive = process.stdin.isTTY === true;\n const { key, path } = await keyManager.rotate(\n isInteractive ? promptForKeyPath : undefined\n );\n span.setAttribute(\"mcp.key.path\", path);\n process.stderr.write(`Generated new MCP API key, stored at ${path}\\n`);\n process.stderr.write(`MCP API key: ${key}\\n`);\n });\n return;\n }\n\n const isInteractive = process.stdin.isTTY === true;\n const { key, path, isNew } = await keyManager.ensureKey(\n isInteractive ? promptForKeyPath : undefined\n );\n\n if (isNew) {\n process.stderr.write(`Generated new MCP API key, stored at ${path}\\n`);\n }\n // Required every launch: print the API key to stderr (never stdout, which\n // carries the MCP protocol).\n process.stderr.write(`MCP API key: ${key}\\n`);\n\n if (options.transport === \"http\") {\n // HTTP transport implementation\n const { createMcpHttpApp, startHttpServer, closeHttpServer } = await import(\n \"../mcp/http-transport.js\"\n );\n\n const host = options.host || \"127.0.0.1\";\n const port = options.port || 3100;\n\n let httpApp: HttpTransportApp;\n const httpServer = await startActiveSpan(\n \"mcp.server.start\",\n async (span) => {\n httpApp = await createMcpHttpApp(keyManager, key, createConfiguredServer, host);\n const server = await startHttpServer(httpApp, host, port);\n\n span.setAttribute(\"mcp.server.name\", \"documentation-robotics\");\n span.setAttribute(\"mcp.server.version\", cliVersion);\n span.setAttribute(\"mcp.server.transport\", \"http\");\n span.setAttribute(\"mcp.server.host\", host);\n span.setAttribute(\"mcp.server.port\", port);\n\n return server;\n }\n );\n\n // Warn if bound to 0.0.0.0\n if (host === \"0.0.0.0\") {\n process.stderr.write(\n \"Warning: Server bound to 0.0.0.0. DNS rebinding protection is not automatic; ensure your MCP client validates the Host header.\\n\"\n );\n }\n\n process.stderr.write(`Documentation Robotics MCP server ready (http://${host}:${port}/mcp)\\n`);\n\n // Set up graceful shutdown on SIGINT/SIGTERM\n let resolveKeepAlive: () => void;\n const keepAlive = new Promise<void>((resolve) => {\n resolveKeepAlive = resolve;\n });\n\n let isShuttingDown = false;\n const shutdown = async () => {\n if (isShuttingDown) {\n return;\n }\n isShuttingDown = true;\n\n try {\n process.stderr.write(\"\\nShutting down HTTP server...\\n\");\n await closeHttpServer(httpServer, httpApp);\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : \"unknown error\";\n process.stderr.write(`[mcp] Error closing server: ${errorMsg}\\n`);\n } finally {\n resolveKeepAlive!();\n }\n };\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n\n // Keep the process alive until explicitly terminated\n await keepAlive;\n } else {\n // Stdio transport (default)\n // Validate that the environment has the correct API key configured\n if (!keyManager.validate(process.env.DR_MCP_API_KEY, key)) {\n process.stderr.write(\n \"Error: DR_MCP_API_KEY environment variable is missing or does not match the stored API key.\\n\"\n );\n process.stderr.write(\n \"Set DR_MCP_API_KEY to the key above when configuring the MCP client, e.g.:\\n\"\n );\n process.stderr.write(\n ' { \"command\": \"dr\", \"args\": [\"mcp\"], \"env\": { \"DR_MCP_API_KEY\": \"<key>\" } }\\n'\n );\n throw new CLIError(\"MCP authentication failed: invalid or missing DR_MCP_API_KEY\", 1);\n }\n\n const transport = await startActiveSpan(\n \"mcp.server.start\",\n async (span) => {\n const { StdioServerTransport } = await import(\"@modelcontextprotocol/sdk/server/stdio.js\");\n\n const server = await createConfiguredServer();\n\n span.setAttribute(\"mcp.server.name\", \"documentation-robotics\");\n span.setAttribute(\"mcp.server.version\", cliVersion);\n span.setAttribute(\"mcp.server.transport\", \"stdio\");\n\n const serverTransport = new StdioServerTransport();\n await server.connect(serverTransport);\n return serverTransport;\n }\n );\n\n process.stderr.write(\"Documentation Robotics MCP server ready (stdio)\\n\");\n\n // Keep the process alive for the lifetime of the stdio session; the\n // transport closes (and the process exits) when stdin closes. Shutdown\n // is implicit in the `mcp.server.start` span's end, so no separate\n // zero-duration \"stop\" span is recorded here.\n await new Promise<void>((resolve) => {\n transport.onclose = () => resolve();\n });\n }\n } catch (error) {\n if (error instanceof CLIError) {\n throw error;\n }\n\n let message = getErrorMessage(error);\n\n // Handle port-in-use errors with actionable message\n if (error instanceof Error && (error as NodeJS.ErrnoException).code === \"EADDRINUSE\") {\n const port = options.port || 3100;\n message = `Port ${port} is already in use. Try a different port with --port or kill the process using that port.`;\n }\n\n process.stderr.write(`Error: ${message}\\n`);\n throw new CLIError(message, 1);\n }\n}\n"],
5
- "mappings": "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",
6
- "names": ["isInteractive", "key", "path"]
4
+ "sourcesContent": ["/**\n * MCP server command - Starts an MCP server for AI assistant integration\n *\n * Supports two transport modes:\n * - stdio (default): In-process over stdio; stdout carries the MCP JSON-RPC protocol. The client\n * spawns the server as the same user, so there is no API key: anything that can start\n * `dr mcp` can already read the model files.\n * - http: HTTP server with Express, exposing MCP at /mcp endpoint with bearer token auth\n *\n * All non-protocol output goes to stderr.\n */\n\nimport { isCancel, text } from \"@clack/prompts\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { RootsListChangedNotificationSchema } from \"@modelcontextprotocol/sdk/types.js\";\nimport { ApiKeyManager, type ApiKeyStoragePrompt } from \"../mcp/api-key-manager.js\";\nimport { type HttpTransportApp } from \"../mcp/http-transport.js\";\nimport { followClientRoots, markEnvProjectRoot } from \"../mcp/project-root.js\";\nimport { McpResourceRegistry } from \"../mcp/resource-registry.js\";\nimport { SERVER_INSTRUCTIONS } from \"../mcp/server-instructions.js\";\nimport { McpToolRegistry } from \"../mcp/tool-registry.js\";\nimport { clearModelCache, loadModel } from \"../mcp/tools/shared.js\";\nimport { startActiveSpan } from \"../telemetry/index.js\";\nimport { CLIError, getErrorMessage } from \"../utils/errors.js\";\nimport { getCliVersion } from \"../utils/spec-version.js\";\n\nconst cliVersion = getCliVersion();\n\n/**\n * Prompt the user (via stdout/stdin) for where to store a newly generated API key.\n * Only invoked when stdin is a TTY \u2014 non-interactive launches (e.g. an MCP client\n * spawning `dr mcp`) fall back to the default path without prompting.\n */\nconst promptForKeyPath: ApiKeyStoragePrompt = async (defaultPath) => {\n const result = await text({\n message: \"Where should the MCP API key be stored?\",\n placeholder: defaultPath,\n defaultValue: defaultPath,\n });\n\n if (isCancel(result) || typeof result !== \"string\" || result.trim().length === 0) {\n return defaultPath;\n }\n return result.trim();\n};\n\nasync function createConfiguredServer(): Promise<McpServer> {\n const { McpServer } = await import(\"@modelcontextprotocol/sdk/server/mcp.js\");\n\n const server = new McpServer(\n { name: \"documentation-robotics\", version: cliVersion },\n { instructions: SERVER_INSTRUCTIONS }\n );\n\n // Register the model tool surface and spec/model manifest resources.\n new McpToolRegistry().registerAll(server);\n await new McpResourceRegistry().registerAll(server);\n return server;\n}\n\n/**\n * Loads the default project's model into the cache so the first tool call doesn't wait for it.\n * Best-effort: a server started outside a DR project still starts cleanly, and the first tool\n * call that supplies a valid rootPath loads and caches that model.\n */\nasync function warmModel(): Promise<void> {\n try {\n await loadModel();\n } catch (error) {\n process.stderr.write(`[mcp] model warmup skipped: ${getErrorMessage(error)}\\n`);\n }\n}\n\n/**\n * Serve the project the client has open: once the client has initialized, ask for its roots\n * (mcp/project-root.ts), and ask again whenever it says they changed. A client without roots\n * leaves the server in its working directory.\n */\nfunction serveClientRoots(server: McpServer): void {\n const inner = server.server;\n const follow = () =>\n followClientRoots(() => inner.listRoots(), { onChange: () => clearModelCache() }).then(warmModel);\n\n inner.setNotificationHandler(RootsListChangedNotificationSchema, async () => {\n await follow();\n });\n inner.oninitialized = () => {\n if (inner.getClientCapabilities()?.roots) {\n void follow();\n } else {\n void warmModel();\n }\n };\n}\n\n/**\n * DR_MCP_ROOT_PATH lets a launcher (DrBot under `dr visualize`) point the server at a model\n * without relying on the cwd its MCP client spawns it in: tools default to this directory, and\n * the client's roots are not consulted. A bad path is reported and the server keeps its own cwd.\n * Returns whether the server now serves from DR_MCP_ROOT_PATH.\n */\nexport function serveFromRootPathEnv(env: NodeJS.ProcessEnv = process.env): boolean {\n const rootPath = env.DR_MCP_ROOT_PATH;\n if (!rootPath) return false;\n try {\n process.chdir(rootPath);\n } catch (error) {\n process.stderr.write(\n `[mcp] DR_MCP_ROOT_PATH=${rootPath} is not usable (${getErrorMessage(error)}); serving from ${process.cwd()}\\n`\n );\n return false;\n }\n markEnvProjectRoot(process.cwd());\n return true;\n}\n\nexport interface McpCommandOptions {\n /** Force-generate a new API key, overwrite it at the configured storage path, and exit. */\n regenerateKey?: boolean;\n /** Transport type: \"stdio\" or \"http\" (default: \"stdio\") */\n transport?: \"stdio\" | \"http\";\n /** Port for HTTP transport (default: 3100, only meaningful with --transport http) */\n port?: number;\n /** Host address for HTTP transport (default: \"127.0.0.1\", only meaningful with --transport http) */\n host?: string;\n}\n\nexport async function mcpCommand(options: McpCommandOptions = {}): Promise<void> {\n const keyManager = new ApiKeyManager();\n\n try {\n if (options.regenerateKey) {\n await startActiveSpan(\"mcp.key.rotate\", async (span) => {\n const isInteractive = process.stdin.isTTY === true;\n const { key, path } = await keyManager.rotate(\n isInteractive ? promptForKeyPath : undefined\n );\n span.setAttribute(\"mcp.key.path\", path);\n process.stderr.write(`Generated new MCP API key, stored at ${path}\\n`);\n process.stderr.write(`MCP API key: ${key}\\n`);\n });\n return;\n }\n\n const servesEnvRoot = serveFromRootPathEnv();\n\n if (options.transport === \"http\") {\n // HTTP clients authenticate with the API key as a bearer token.\n const isInteractive = process.stdin.isTTY === true;\n const { key, path, isNew } = await keyManager.ensureKey(\n isInteractive ? promptForKeyPath : undefined\n );\n if (isNew) {\n process.stderr.write(`Generated new MCP API key, stored at ${path}\\n`);\n }\n // Print the key to stderr so a user can configure a client. DR_MCP_QUIET_KEY keeps it out\n // of logs for a launcher that already has it.\n if (!/^(1|true|yes|on)$/i.test(process.env.DR_MCP_QUIET_KEY ?? \"\")) {\n process.stderr.write(`MCP API key: ${key}\\n`);\n }\n\n // One server for every HTTP session, so the project is the env root or the cwd (client\n // roots are per session and can't move a shared working directory).\n await warmModel();\n const { createMcpHttpApp, startHttpServer, closeHttpServer } = await import(\n \"../mcp/http-transport.js\"\n );\n\n const host = options.host || \"127.0.0.1\";\n const port = options.port || 3100;\n\n let httpApp: HttpTransportApp;\n const httpServer = await startActiveSpan(\n \"mcp.server.start\",\n async (span) => {\n httpApp = await createMcpHttpApp(keyManager, key, createConfiguredServer, host);\n const server = await startHttpServer(httpApp, host, port);\n\n span.setAttribute(\"mcp.server.name\", \"documentation-robotics\");\n span.setAttribute(\"mcp.server.version\", cliVersion);\n span.setAttribute(\"mcp.server.transport\", \"http\");\n span.setAttribute(\"mcp.server.host\", host);\n span.setAttribute(\"mcp.server.port\", port);\n\n return server;\n }\n );\n\n // Warn if bound to 0.0.0.0\n if (host === \"0.0.0.0\") {\n process.stderr.write(\n \"Warning: Server bound to 0.0.0.0. DNS rebinding protection is not automatic; ensure your MCP client validates the Host header.\\n\"\n );\n }\n\n process.stderr.write(`Documentation Robotics MCP server ready (http://${host}:${port}/mcp)\\n`);\n\n // Set up graceful shutdown on SIGINT/SIGTERM\n let resolveKeepAlive: () => void;\n const keepAlive = new Promise<void>((resolve) => {\n resolveKeepAlive = resolve;\n });\n\n let isShuttingDown = false;\n const shutdown = async () => {\n if (isShuttingDown) {\n return;\n }\n isShuttingDown = true;\n\n try {\n process.stderr.write(\"\\nShutting down HTTP server...\\n\");\n await closeHttpServer(httpServer, httpApp);\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : \"unknown error\";\n process.stderr.write(`[mcp] Error closing server: ${errorMsg}\\n`);\n } finally {\n resolveKeepAlive!();\n }\n };\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n\n // Keep the process alive until explicitly terminated\n await keepAlive;\n } else {\n // Stdio transport (default). No API key: the client spawns this process as its own user.\n const transport = await startActiveSpan(\n \"mcp.server.start\",\n async (span) => {\n const { StdioServerTransport } = await import(\"@modelcontextprotocol/sdk/server/stdio.js\");\n\n const server = await createConfiguredServer();\n if (servesEnvRoot) {\n await warmModel();\n } else {\n serveClientRoots(server);\n }\n\n span.setAttribute(\"mcp.server.name\", \"documentation-robotics\");\n span.setAttribute(\"mcp.server.version\", cliVersion);\n span.setAttribute(\"mcp.server.transport\", \"stdio\");\n\n const serverTransport = new StdioServerTransport();\n await server.connect(serverTransport);\n return serverTransport;\n }\n );\n\n process.stderr.write(\"Documentation Robotics MCP server ready (stdio)\\n\");\n\n // Keep the process alive for the lifetime of the stdio session; the\n // transport closes (and the process exits) when stdin closes. Shutdown\n // is implicit in the `mcp.server.start` span's end, so no separate\n // zero-duration \"stop\" span is recorded here.\n await new Promise<void>((resolve) => {\n transport.onclose = () => resolve();\n });\n }\n } catch (error) {\n if (error instanceof CLIError) {\n throw error;\n }\n\n let message = getErrorMessage(error);\n\n // Handle port-in-use errors with actionable message\n if (error instanceof Error && (error as NodeJS.ErrnoException).code === \"EADDRINUSE\") {\n const port = options.port || 3100;\n message = `Port ${port} is already in use. Try a different port with --port or kill the process using that port.`;\n }\n\n process.stderr.write(`Error: ${message}\\n`);\n throw new CLIError(message, 1);\n }\n}\n"],
5
+ "mappings": "AAYA,SAAS,UAAU,YAAY;AAE/B,SAAS,0CAA0C;AACnD,SAAS,qBAA+C;AAExD,SAAS,mBAAmB,0BAA0B;AACtD,SAAS,2BAA2B;AACpC,SAAS,2BAA2B;AACpC,SAAS,uBAAuB;AAChC,SAAS,iBAAiB,iBAAiB;AAC3C,SAAS,uBAAuB;AAChC,SAAS,UAAU,uBAAuB;AAC1C,SAAS,qBAAqB;AAE9B,MAAM,aAAa,cAAa,GAO1B,mBAAwC,OAAO,gBAAe;AAClE,QAAM,SAAS,MAAM,KAAK;IACxB,SAAS;IACT,aAAa;IACb,cAAc;GACf;AAED,SAAI,SAAS,MAAM,KAAK,OAAO,UAAW,YAAY,OAAO,KAAI,EAAG,WAAW,IACtE,cAEF,OAAO,KAAI;AACpB;AAEA,eAAe,yBAAsB;AACnC,QAAM,EAAE,UAAS,IAAK,MAAM,OAAO,yCAAyC,GAEtE,SAAS,IAAI,UACjB,EAAE,MAAM,0BAA0B,SAAS,WAAU,GACrD,EAAE,cAAc,oBAAmB,CAAE;AAIvC,aAAI,gBAAe,EAAG,YAAY,MAAM,GACxC,MAAM,IAAI,oBAAmB,EAAG,YAAY,MAAM,GAC3C;AACT;AAOA,eAAe,YAAS;AACtB,MAAI;AACF,UAAM,UAAS;EACjB,SAAS,OAAO;AACd,YAAQ,OAAO,MAAM,+BAA+B,gBAAgB,KAAK,CAAC;CAAI;EAChF;AACF;AAOA,SAAS,iBAAiB,QAAiB;AACzC,QAAM,QAAQ,OAAO,QACf,SAAS,MACb,kBAAkB,MAAM,MAAM,UAAS,GAAI,EAAE,UAAU,MAAM,gBAAe,EAAE,CAAE,EAAE,KAAK,SAAS;AAElG,QAAM,uBAAuB,oCAAoC,YAAW;AAC1E,UAAM,OAAM;EACd,CAAC,GACD,MAAM,gBAAgB,MAAK;AACzB,IAAI,MAAM,sBAAqB,GAAI,QAC5B,OAAM,IAEN,UAAS;EAElB;AACF;AAQM,SAAU,qBAAqB,MAAyB,QAAQ,KAAG;AACvE,QAAM,WAAW,IAAI;AACrB,MAAI,CAAC;AAAU,WAAO;AACtB,MAAI;AACF,YAAQ,MAAM,QAAQ;EACxB,SAAS,OAAO;AACd,mBAAQ,OAAO,MACb,0BAA0B,QAAQ,mBAAmB,gBAAgB,KAAK,CAAC,mBAAmB,QAAQ,IAAG,CAAE;CAAI,GAE1G;EACT;AACA,4BAAmB,QAAQ,IAAG,CAAE,GACzB;AACT;AAaA,eAAsB,WAAW,UAA6B,CAAA,GAAE;AAC9D,QAAM,aAAa,IAAI,cAAa;AAEpC,MAAI;AACF,QAAI,QAAQ,eAAe;AACzB,YAAM,gBAAgB,kBAAkB,OAAO,SAAQ;AACrD,cAAM,gBAAgB,QAAQ,MAAM,UAAU,IACxC,EAAE,KAAK,KAAI,IAAK,MAAM,WAAW,OACrC,gBAAgB,mBAAmB,MAAS;AAE9C,aAAK,aAAa,gBAAgB,IAAI,GACtC,QAAQ,OAAO,MAAM,wCAAwC,IAAI;CAAI,GACrE,QAAQ,OAAO,MAAM,gBAAgB,GAAG;CAAI;MAC9C,CAAC;AACD;IACF;AAEA,UAAM,gBAAgB,qBAAoB;AAE1C,QAAI,QAAQ,cAAc,QAAQ;AAEhC,YAAM,gBAAgB,QAAQ,MAAM,UAAU,IACxC,EAAE,KAAK,MAAM,MAAK,IAAK,MAAM,WAAW,UAC5C,gBAAgB,mBAAmB,MAAS;AAE9C,MAAI,SACF,QAAQ,OAAO,MAAM,wCAAwC,IAAI;CAAI,GAIlE,qBAAqB,KAAK,QAAQ,IAAI,oBAAoB,EAAE,KAC/D,QAAQ,OAAO,MAAM,gBAAgB,GAAG;CAAI,GAK9C,MAAM,UAAS;AACf,YAAM,EAAE,kBAAkB,iBAAiB,gBAAe,IAAK,MAAM,OACnE,0BAA0B,GAGtB,OAAO,QAAQ,QAAQ,aACvB,OAAO,QAAQ,QAAQ;AAE7B,UAAI;AACJ,YAAM,aAAa,MAAM,gBACvB,oBACA,OAAO,SAAQ;AACb,kBAAU,MAAM,iBAAiB,YAAY,KAAK,wBAAwB,IAAI;AAC9E,cAAM,SAAS,MAAM,gBAAgB,SAAS,MAAM,IAAI;AAExD,oBAAK,aAAa,mBAAmB,wBAAwB,GAC7D,KAAK,aAAa,sBAAsB,UAAU,GAClD,KAAK,aAAa,wBAAwB,MAAM,GAChD,KAAK,aAAa,mBAAmB,IAAI,GACzC,KAAK,aAAa,mBAAmB,IAAI,GAElC;MACT,CAAC;AAIH,MAAI,SAAS,aACX,QAAQ,OAAO,MACb;CAAkI,GAItI,QAAQ,OAAO,MAAM,mDAAmD,IAAI,IAAI,IAAI;CAAS;AAG7F,UAAI;AACJ,YAAM,YAAY,IAAI,QAAc,CAAC,YAAW;AAC9C,2BAAmB;MACrB,CAAC;AAED,UAAI,iBAAiB;AACrB,YAAM,WAAW,YAAW;AAC1B,YAAI,iBAGJ;2BAAiB;AAEjB,cAAI;AACF,oBAAQ,OAAO,MAAM;;CAAkC,GACvD,MAAM,gBAAgB,YAAY,OAAO;UAC3C,SAAS,OAAO;AACd,kBAAM,WAAW,iBAAiB,QAAQ,MAAM,UAAU;AAC1D,oBAAQ,OAAO,MAAM,+BAA+B,QAAQ;CAAI;UAClE;AACE,6BAAiB;UACnB;;MACF;AAEA,cAAQ,GAAG,UAAU,QAAQ,GAC7B,QAAQ,GAAG,WAAW,QAAQ,GAG9B,MAAM;IACR,OAAO;AAEL,YAAM,YAAY,MAAM,gBACtB,oBACA,OAAO,SAAQ;AACb,cAAM,EAAE,qBAAoB,IAAK,MAAM,OAAO,2CAA2C,GAEnF,SAAS,MAAM,uBAAsB;AAC3C,QAAI,gBACF,MAAM,UAAS,IAEf,iBAAiB,MAAM,GAGzB,KAAK,aAAa,mBAAmB,wBAAwB,GAC7D,KAAK,aAAa,sBAAsB,UAAU,GAClD,KAAK,aAAa,wBAAwB,OAAO;AAEjD,cAAM,kBAAkB,IAAI,qBAAoB;AAChD,qBAAM,OAAO,QAAQ,eAAe,GAC7B;MACT,CAAC;AAGH,cAAQ,OAAO,MAAM;CAAmD,GAMxE,MAAM,IAAI,QAAc,CAAC,YAAW;AAClC,kBAAU,UAAU,MAAM,QAAO;MACnC,CAAC;IACH;EACF,SAAS,OAAO;AACd,QAAI,iBAAiB;AACnB,YAAM;AAGR,QAAI,UAAU,gBAAgB,KAAK;AAGnC,UAAI,iBAAiB,SAAU,MAAgC,SAAS,iBAEtE,UAAU,QADG,QAAQ,QAAQ,IACP,8FAGxB,QAAQ,OAAO,MAAM,UAAU,OAAO;CAAI,GACpC,IAAI,SAAS,SAAS,CAAC;EAC/B;AACF;",
6
+ "names": []
7
7
  }
@@ -4,11 +4,16 @@ import { StagingAreaManager } from "../core/staging-area.js";
4
4
  import { findElementLayer } from "../utils/element-utils.js";
5
5
  import { isValidLayerName } from "../core/layers.js";
6
6
  import { CLIError, ErrorCategory, handleError, handleSuccess, handleInfo, getErrorMessage } from "../utils/errors.js";
7
- import { getValidRelationships, getValidPredicatesForSource, getValidDestinationsForSourceAndPredicate } from "../generated/relationship-index.js";
7
+ import { getValidRelationships } from "../generated/relationship-index.js";
8
8
  import { normalizeNodeType } from "../generated/node-types.js";
9
9
  import { ModelReportOrchestrator } from "../reports/model-report-orchestrator.js";
10
10
  import { emitLog, SeverityNumber } from "../telemetry/index.js";
11
+ import { elementIdentifiers } from "../core/relationships.js";
11
12
  import { RelationshipCatalog } from "../core/relationship-catalog.js";
13
+ import { collectKnownIdentifiers, findDanglingRelationships } from "../core/relationship-prune.js";
14
+ import { isJson } from "../utils/globals.js";
15
+ import { ChangesetGraph } from "../core/changeset-graph.js";
16
+ import { liveRelationshipsBetween, planRelationshipAdd, stageRelationshipDeletes, validateRelationshipCombination } from "../core/relationship-staging.js";
12
17
  function constructSpecNodeId(element, elementId) {
13
18
  const layerId = element.layer_id, type = element.type;
14
19
  if (typeof layerId != "string" || !layerId)
@@ -18,16 +23,6 @@ function constructSpecNodeId(element, elementId) {
18
23
  const normalizedType = normalizeNodeType(layerId, type);
19
24
  return `${layerId}.${normalizedType}`;
20
25
  }
21
- function validateRelationshipCombination(sourceSpecNodeId, predicate, destSpecNodeId) {
22
- if (getValidRelationships(sourceSpecNodeId, predicate, destSpecNodeId).length > 0)
23
- return { valid: !0 };
24
- const suggestions = [], validPredicates = getValidPredicatesForSource(sourceSpecNodeId);
25
- if (validPredicates.length > 0 ? suggestions.push(`Valid predicates for ${sourceSpecNodeId}: ${validPredicates.join(", ")}`) : suggestions.push(`No valid relationships defined for ${sourceSpecNodeId}`), predicate) {
26
- const validDests = getValidDestinationsForSourceAndPredicate(sourceSpecNodeId, predicate);
27
- validDests.length > 0 ? suggestions.push(`Valid destinations for --[${predicate}]-->: ${validDests.join(", ")}`) : suggestions.push(`No valid destinations for ${sourceSpecNodeId} --[${predicate}]-->`);
28
- }
29
- return { valid: !1, suggestions };
30
- }
31
26
  function getRelationshipConstraints(sourceSpecNodeId, predicate, destSpecNodeId) {
32
27
  const rels = getValidRelationships(sourceSpecNodeId, predicate, destSpecNodeId);
33
28
  if (rels.length === 0)
@@ -39,6 +34,13 @@ function getRelationshipConstraints(sourceSpecNodeId, predicate, destSpecNodeId)
39
34
  };
40
35
  }
41
36
  async function addRelationshipHandler(model, source, target, predicate, properties, preResolved) {
37
+ return await addRelationship(model, source, target, predicate, properties, preResolved) !== "duplicate";
38
+ }
39
+ async function activeStagedChangeset(manager) {
40
+ const active = await manager.getActive();
41
+ return active && active.status === "staged" ? active : null;
42
+ }
43
+ async function addRelationship(model, source, target, predicate, properties, preResolved) {
42
44
  let resolvedSourceLayerName = preResolved?.source?.layerName, resolvedSourceElement = preResolved?.source?.element, resolvedTargetLayerName = preResolved?.target?.layerName, resolvedTargetElement = preResolved?.target?.element;
43
45
  if (!resolvedSourceLayerName && (resolvedSourceLayerName = await findElementLayer(model, source), !resolvedSourceLayerName))
44
46
  throw new CLIError(`Source element ${source} not found`, ErrorCategory.USER);
@@ -68,9 +70,19 @@ async function addRelationshipHandler(model, source, target, predicate, properti
68
70
  if (relType && !relType.semantics.reflexivity && source === target)
69
71
  throw new CLIError(`Invalid relationship: predicate "${predicate}" does not allow reflexive relationships (${source})`, ErrorCategory.USER);
70
72
  }
71
- if (model.relationships.find(source, target, predicate).length > 0)
72
- return !1;
73
- const stagingManager = new StagingAreaManager(model.rootPath, model), activeChangesetId = await stagingManager.getActiveId(), relData = {
73
+ const stagingManager = new StagingAreaManager(model.rootPath, model), activeChangesetId = await stagingManager.getActiveId(), activeChangeset = activeChangesetId ? await activeStagedChangeset(stagingManager) : null;
74
+ if (activeChangeset) {
75
+ const graph = await ChangesetGraph.build(model, activeChangeset), plan = planRelationshipAdd(graph, activeChangeset, { source, predicate, target });
76
+ if (plan.action === "duplicate")
77
+ return "duplicate";
78
+ if (plan.action === "restore") {
79
+ for (const key of plan.keys)
80
+ await stagingManager.unstage(activeChangeset.id, key);
81
+ return "restored";
82
+ }
83
+ } else if (model.relationships.find(source, target, predicate).length > 0)
84
+ return "duplicate";
85
+ const relData = {
74
86
  source,
75
87
  target,
76
88
  predicate,
@@ -109,15 +121,35 @@ async function addRelationshipHandler(model, source, target, predicate, properti
109
121
  });
110
122
  }
111
123
  }
112
- return !0;
124
+ return activeChangesetId ? "staged" : "added";
113
125
  }
114
- async function deleteRelationshipHandler(model, source, target, predicate, preResolved) {
126
+ async function findRelationshipsByEndpoints(model, source, target, predicate) {
127
+ const sourceIds = new Set(elementIdentifiers(model.getElementById(source) ?? {}, source)), targetIds = new Set(elementIdentifiers(model.getElementById(target) ?? {}, target));
128
+ return model.relationships.getAll().filter((rel) => sourceIds.has(rel.source) && targetIds.has(rel.target) && (!predicate || rel.predicate === predicate));
129
+ }
130
+ function noMatchingRelationshipHints(surface, source, inChangeset) {
131
+ return surface === "mcp" ? [
132
+ `Use "model_show" on ${source} to see its relationships`,
133
+ inChangeset ? 'Use "changeset_brief" to see what this changeset already stages' : 'Use "model_search" to check the source and target element IDs'
134
+ ] : [
135
+ `Use "dr relationship list ${source}" to see the relationships recorded for ${source}`,
136
+ inChangeset ? 'Use "dr changeset staged" to see what the active changeset already stages' : 'Use "dr relationship prune --dry-run" to list rows whose endpoints no longer resolve'
137
+ ];
138
+ }
139
+ async function deleteRelationshipHandler(model, source, target, predicate, preResolved, surface = "cli") {
140
+ const changesetManager = new StagingAreaManager(model.rootPath, model), changeset = await changesetManager.getActiveId() ? await activeStagedChangeset(changesetManager) : null;
141
+ if (changeset) {
142
+ const graph = await ChangesetGraph.build(model, changeset), matches = liveRelationshipsBetween(graph, source, target, predicate);
143
+ if (matches.length === 0)
144
+ throw new CLIError("No matching relationships found", ErrorCategory.USER, noMatchingRelationshipHints(surface, source, !0));
145
+ const staged = await stageRelationshipDeletes(changesetManager, graph, changeset, matches);
146
+ return { deletedCount: staged.length, staged };
147
+ }
115
148
  let resolvedSourceLayerName = preResolved?.sourceLayerName;
116
- if (!resolvedSourceLayerName && (resolvedSourceLayerName = await findElementLayer(model, source), !resolvedSourceLayerName))
117
- throw new CLIError(`Source element ${source} not found`, ErrorCategory.USER);
149
+ resolvedSourceLayerName || (resolvedSourceLayerName = await findElementLayer(model, source));
118
150
  let resolvedToDelete = preResolved?.toDelete;
119
- if (resolvedToDelete || (resolvedToDelete = model.relationships.find(source, target, predicate)), resolvedToDelete.length === 0)
120
- throw new CLIError("No matching relationships found", ErrorCategory.USER);
151
+ if (resolvedToDelete || (resolvedToDelete = await findRelationshipsByEndpoints(model, source, target, predicate)), resolvedToDelete.length === 0)
152
+ throw new CLIError("No matching relationships found", ErrorCategory.USER, noMatchingRelationshipHints(surface, source, !1));
121
153
  const stagingManager = new StagingAreaManager(model.rootPath, model), activeChangesetId = await stagingManager.getActiveId();
122
154
  if (activeChangesetId)
123
155
  for (const rel of resolvedToDelete) {
@@ -137,7 +169,9 @@ async function deleteRelationshipHandler(model, source, target, predicate, preRe
137
169
  });
138
170
  }
139
171
  else {
140
- model.relationships.delete(source, target, predicate), await model.saveRelationships(), await model.saveManifest();
172
+ for (const rel of resolvedToDelete)
173
+ model.relationships.delete(rel.source, rel.target, rel.predicate);
174
+ await model.saveRelationships(), await model.saveManifest();
141
175
  try {
142
176
  const orchestrator = new ModelReportOrchestrator(model, model.rootPath), affectedLayers = /* @__PURE__ */ new Set();
143
177
  for (const rel of resolvedToDelete) {
@@ -161,6 +195,106 @@ async function deleteRelationshipHandler(model, source, target, predicate, preRe
161
195
  }
162
196
  return { deletedCount: resolvedToDelete.length };
163
197
  }
198
+ async function pruneRelationshipsCommand(options) {
199
+ const format = options.format ?? "text";
200
+ if (format !== "text" && format !== "json")
201
+ throw new CLIError(`Invalid format: ${format}`, ErrorCategory.USER, [
202
+ "Use --format text or --format json"
203
+ ]);
204
+ const jsonOutput = format === "json" || isJson(), layers = options.layers?.length ? options.layers : void 0;
205
+ if (layers) {
206
+ const invalid = layers.filter((l) => !isValidLayerName(l));
207
+ if (invalid.length > 0)
208
+ throw new CLIError(`Invalid layer name(s): ${invalid.join(", ")}`, ErrorCategory.USER, ['Use canonical layer names, e.g. "api", "data-model", "data-store"']);
209
+ }
210
+ const model = await Model.load(options.model || process.cwd()), elements = [];
211
+ for (const layerName of model.getLayerNames()) {
212
+ const layer = await model.getLayer(layerName);
213
+ layer && elements.push(...layer.listElements());
214
+ }
215
+ const known = collectKnownIdentifiers(elements), analysis = findDanglingRelationships(model.relationships.getAll(), known, { layers }), stagingManager = new StagingAreaManager(model.rootPath, model), activeChangeset = await stagingManager.getActive(), staging = !!activeChangeset && activeChangeset.status === "staged", result = {
216
+ scanned: analysis.scanned,
217
+ dangling: analysis.dangling.length,
218
+ removed: 0,
219
+ dryRun: !!options.dryRun,
220
+ staged: !1,
221
+ ...staging ? { changeset: activeChangeset.name } : {},
222
+ ...layers ? { layers } : {},
223
+ relationships: analysis.dangling.map(({ relationship: rel, unresolved }) => ({
224
+ source: rel.source,
225
+ predicate: rel.predicate,
226
+ target: rel.target,
227
+ layer: rel.layer,
228
+ ...rel.targetLayer ? { targetLayer: rel.targetLayer } : {},
229
+ unresolved
230
+ }))
231
+ };
232
+ if (!options.dryRun && analysis.dangling.length > 0) {
233
+ if (staging) {
234
+ for (const { relationship: rel } of analysis.dangling)
235
+ await stagingManager.stage(activeChangeset.id, {
236
+ type: "relationship-delete",
237
+ elementId: `${rel.source}::${rel.predicate}::${rel.target}`,
238
+ layerName: rel.layer,
239
+ before: {
240
+ source: rel.source,
241
+ target: rel.target,
242
+ predicate: rel.predicate,
243
+ layer: rel.layer,
244
+ ...rel.targetLayer ? { targetLayer: rel.targetLayer } : {},
245
+ ...rel.properties ? { properties: rel.properties } : {}
246
+ }
247
+ });
248
+ result.staged = !0;
249
+ } else {
250
+ const danglingRows = new Set(analysis.dangling.map((d) => d.relationship));
251
+ model.relationships.retain((rel) => !danglingRows.has(rel)), await model.saveRelationships({ preserveHeader: !0 }), await model.saveManifest();
252
+ try {
253
+ const orchestrator = new ModelReportOrchestrator(model, model.rootPath), affectedLayers = /* @__PURE__ */ new Set();
254
+ for (const { relationship: rel } of analysis.dangling)
255
+ for (const name of [rel.layer, rel.targetLayer])
256
+ if (name && isValidLayerName(name))
257
+ for (const affected of orchestrator.computeAffectedLayers(name))
258
+ affectedLayers.add(affected);
259
+ await orchestrator.regenerate(affectedLayers);
260
+ } catch (error) {
261
+ emitLog(SeverityNumber.WARN, "Failed to regenerate layer reports after relationship prune", {
262
+ "relationship.pruned": analysis.dangling.length,
263
+ "error.message": getErrorMessage(error)
264
+ });
265
+ }
266
+ }
267
+ result.removed = analysis.dangling.length;
268
+ }
269
+ if (jsonOutput)
270
+ return console.log(JSON.stringify({ status: "ok", ...result }, null, 2)), result;
271
+ const scope = layers ? ` in layer(s) ${layers.join(", ")}` : "";
272
+ handleInfo(`Scanned ${result.scanned} relationship(s)${scope}: ${result.dangling} dangling`);
273
+ for (const row of result.relationships) {
274
+ const side = row.unresolved === "both" ? "source and target not found" : `${row.unresolved} not found`;
275
+ handleInfo(` - ${ansis.yellow(row.source)} --[${ansis.magenta(row.predicate)}]--> ${ansis.yellow(row.target)} ${ansis.dim(`(${side})`)}`);
276
+ }
277
+ return result.dangling === 0 ? handleSuccess("No dangling relationships found", {
278
+ scanned: result.scanned,
279
+ dangling: 0,
280
+ removed: 0
281
+ }) : result.dryRun ? handleSuccess(`Dry run: would remove ${result.dangling} dangling relationship(s)`, {
282
+ scanned: result.scanned,
283
+ dangling: result.dangling,
284
+ removed: 0,
285
+ dryRun: !0
286
+ }) : result.staged ? handleSuccess(`Staged removal of ${result.removed} dangling relationship(s) to ${ansis.bold(result.changeset)}`, {
287
+ scanned: result.scanned,
288
+ dangling: result.dangling,
289
+ removed: result.removed,
290
+ changesetStatus: "staged",
291
+ changeset: result.changeset
292
+ }) : handleSuccess(`Removed ${result.removed} dangling relationship(s)`, {
293
+ scanned: result.scanned,
294
+ dangling: result.dangling,
295
+ removed: result.removed
296
+ }), result;
297
+ }
164
298
  function relationshipCommands(program, rootProgram) {
165
299
  program.command("add <source> <target>").description("Add a relationship between elements (intra-layer or cross-layer)").option("--model <path>", "Path to model root (contains model/manifest.yaml)").requiredOption("--predicate <predicate>", "Relationship predicate (e.g., depends-on, realizes, exposes)").option("--properties <json>", "Relationship properties (JSON)").addHelpText("after", `
166
300
  Examples:
@@ -169,7 +303,7 @@ Examples:
169
303
  $ dr relationship add business.businessprocess.checkout application.applicationprocess.checkout --predicate aggregates
170
304
  $ dr relationship add element-1 element-2 --predicate realizes --properties '{"strength":"high"}'`).action(async (source, target, options) => {
171
305
  try {
172
- const model = await Model.load(options.model || process.cwd()), sourceLayerName = await findElementLayer(model, source);
306
+ const model = await Model.load(options.model), sourceLayerName = await findElementLayer(model, source);
173
307
  if (!sourceLayerName)
174
308
  throw new CLIError(`Source element ${source} not found`, ErrorCategory.USER);
175
309
  const sourceLayer = await model.getLayer(sourceLayerName);
@@ -195,17 +329,29 @@ Examples:
195
329
  } catch {
196
330
  throw new CLIError("Invalid JSON in --properties", ErrorCategory.USER);
197
331
  }
198
- const activeChangesetId = await new StagingAreaManager(model.rootPath, model).getActiveId();
199
- if (!await addRelationshipHandler(model, source, target, options.predicate, properties, {
332
+ const activeChangesetId = await new StagingAreaManager(model.rootPath, model).getActiveId(), outcome = await addRelationship(model, source, target, options.predicate, properties, {
200
333
  source: { element: sourceElement, layerName: sourceLayerName },
201
334
  target: { element: targetElement, layerName: targetLayerName }
202
- })) {
203
- handleSuccess(`Relationship ${source} --[${options.predicate}]--> ${target} already exists`, {
335
+ });
336
+ if (outcome === "duplicate") {
337
+ handleSuccess(`Relationship ${source} --[${options.predicate}]--> ${target} already exists` + (activeChangesetId ? " (in the model or the active changeset); nothing staged" : ""), {
338
+ source,
339
+ target,
340
+ predicate: options.predicate,
341
+ layer: sourceLayerName,
342
+ duplicate: !0,
343
+ ...activeChangesetId ? { changesetStatus: "unchanged" } : {}
344
+ }, { verbose: options.verbose });
345
+ return;
346
+ }
347
+ if (outcome === "restored") {
348
+ handleSuccess(`Restored relationship: ${ansis.bold(source)} ${options.predicate} ${ansis.bold(target)}` + ansis.dim(" [the active changeset's staged deletion of it was unstaged]"), {
204
349
  source,
205
350
  target,
206
351
  predicate: options.predicate,
207
352
  layer: sourceLayerName,
208
- duplicate: !0
353
+ restored: !0,
354
+ changesetStatus: "unstaged"
209
355
  }, { verbose: options.verbose });
210
356
  return;
211
357
  }
@@ -225,13 +371,13 @@ Examples:
225
371
  $ dr relationship delete element-1 element-2 --predicate depends-on
226
372
  $ dr relationship delete element-1 element-2 --force`).action(async (source, target, options) => {
227
373
  try {
228
- const model = await Model.load(options.model || process.cwd()), sourceLayerName = await findElementLayer(model, source);
229
- if (!sourceLayerName)
230
- throw new CLIError(`Source element ${source} not found`, ErrorCategory.USER);
231
- const toDelete = model.relationships.find(source, target, options.predicate);
374
+ const model = await Model.load(options.model), activeForDelete = await activeStagedChangeset(new StagingAreaManager(model.rootPath, model)), toDelete = activeForDelete ? liveRelationshipsBetween(await ChangesetGraph.build(model, activeForDelete), source, target, options.predicate) : await findRelationshipsByEndpoints(model, source, target, options.predicate);
232
375
  if (toDelete.length === 0)
233
- throw new CLIError("No matching relationships found", ErrorCategory.USER);
234
- const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
376
+ throw new CLIError("No matching relationships found", ErrorCategory.USER, [
377
+ `Use "dr relationship list ${source}" to see the relationships recorded for ${source}`,
378
+ 'Use "dr relationship prune --dry-run" to list rows whose endpoints no longer resolve'
379
+ ]);
380
+ const sourceLayerName = await findElementLayer(model, source) ?? toDelete[0].layer, isInteractive = process.stdin.isTTY && process.stdout.isTTY;
235
381
  if (!options.force && isInteractive) {
236
382
  const { confirm } = await import("@clack/prompts");
237
383
  if (!await confirm({
@@ -248,24 +394,46 @@ Examples:
248
394
  }
249
395
  const activeChangesetId = await new StagingAreaManager(model.rootPath, model).getActiveId(), result = await deleteRelationshipHandler(model, source, target, options.predicate, {
250
396
  sourceLayerName,
251
- toDelete
397
+ // In a changeset the handler re-reads the projected graph itself.
398
+ toDelete: activeForDelete ? void 0 : toDelete
252
399
  }), stagedSuffix = activeChangesetId ? " [staged]" : "", deleteDetails = {
253
400
  source,
254
401
  target,
255
402
  layer: sourceLayerName,
256
- deletedCount: result.deletedCount
403
+ deletedCount: result.deletedCount,
404
+ ...result.staged ? { relationships: result.staged } : {}
257
405
  };
258
406
  options.predicate && (deleteDetails.predicate = options.predicate), activeChangesetId && (deleteDetails.changesetStatus = "staged"), handleSuccess(`Deleted ${result.deletedCount} relationship(s) from ${ansis.bold(source)} to ${ansis.bold(target)}${stagedSuffix}`, deleteDetails, { verbose: options.verbose });
259
407
  } catch (error) {
260
408
  handleError(error);
261
409
  }
410
+ }), program.command("prune").description("Remove relationship rows whose source or target no longer resolves to an element").option("--model <path>", "Path to model root (contains model/manifest.yaml)").option("--dry-run", "List the rows that would be removed without writing").option("--layers <layers...>", "Only scan rows whose source or target layer is one of these canonical layer names").option("--format <format>", "Output format: text, json", "text").addHelpText("after", `
411
+ A row is dangling when its source or target matches neither the UUID id nor the
412
+ dotted path of any current element. Kept rows keep their order and the file's
413
+ header comment is preserved. Inside an active changeset the removals are staged
414
+ as relationship deletions instead of being written to relationships.yaml.
415
+
416
+ Examples:
417
+ $ dr relationship prune --dry-run
418
+ $ dr relationship prune
419
+ $ dr relationship prune --layers api application --format json`).action(async (options) => {
420
+ try {
421
+ await pruneRelationshipsCommand({
422
+ model: options.model,
423
+ dryRun: !!options.dryRun,
424
+ layers: options.layers,
425
+ format: options.format
426
+ });
427
+ } catch (error) {
428
+ handleError(error);
429
+ }
262
430
  }), program.command("list <id>").description("List relationships for an element").option("--model <path>", "Path to model root (contains model/manifest.yaml)").option("--direction <dir>", "Filter by direction (incoming/outgoing/all)", "all").option("--json", "Output as JSON").addHelpText("after", `
263
431
  Examples:
264
432
  $ dr relationship list api-endpoint-create-customer
265
433
  $ dr relationship list business-service-order --direction outgoing
266
434
  $ dr relationship list element-1 --json`).action(async (id, options) => {
267
435
  try {
268
- const model = await Model.load(options.model || process.cwd());
436
+ const model = await Model.load(options.model);
269
437
  if (!await findElementLayer(model, id))
270
438
  throw new CLIError(`Element ${id} not found`, ErrorCategory.USER);
271
439
  const { outgoing, incoming } = model.relationships.getForElement(id);
@@ -295,7 +463,7 @@ Examples:
295
463
  $ dr relationship show api-endpoint-create-customer business-service-customer-mgmt
296
464
  $ dr relationship show application-component-api motivation-goal-sales-efficiency`).action(async (source, target, options) => {
297
465
  try {
298
- const model = await Model.load(options.model || process.cwd());
466
+ const model = await Model.load(options.model);
299
467
  if (!await findElementLayer(model, source))
300
468
  throw new CLIError(`Source element ${source} not found`, ErrorCategory.USER);
301
469
  const relationships = model.relationships.find(source, target);
@@ -320,9 +488,12 @@ Examples:
320
488
  });
321
489
  }
322
490
  export {
491
+ addRelationship,
323
492
  addRelationshipHandler,
324
493
  deleteRelationshipHandler,
494
+ findRelationshipsByEndpoints,
325
495
  getRelationshipConstraints,
496
+ pruneRelationshipsCommand,
326
497
  relationshipCommands,
327
498
  validateRelationshipCombination
328
499
  };