@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
package/README.md CHANGED
@@ -132,6 +132,31 @@ dr validate # Validate model integrity
132
132
  dr upgrade # Check for spec upgrades
133
133
  ```
134
134
 
135
+ ### Model Farms
136
+
137
+ A farm (`farm.yaml`) groups several projects' models so they can be synced and validated
138
+ together with cross-model references.
139
+
140
+ ```bash
141
+ dr farm init # Create a farm in the current directory
142
+ dr farm add my-service # Register ./my-service; scaffolds my-service-model
143
+ dr farm add my-service --codebase ../my-service # Sibling checkout outside the farm root
144
+ dr farm add my-service --new-model # Scaffold even if the codebase has its own model
145
+ dr farm add my-service --model ../models/my-service # Register an existing model explicitly
146
+ dr farm status # Pending changes per project
147
+ dr farm validate # Validate with farm-aware references
148
+ dr farm sync [--auto-commit] # Map codebase changes into staged changesets
149
+ dr farm remove my-service [--delete-model]
150
+ ```
151
+
152
+ A codebase that already carries a model (`documentation-robotics/model/manifest.yaml`, or
153
+ `model/manifest.yaml`, or `manifest.yaml` at the codebase path) is adopted in place by
154
+ `dr farm add` rather than shadowed by an empty duplicate: `farm.yaml` points at it, even outside
155
+ the farm root, and the other farm commands read and write it there. `--delete-model` never
156
+ deletes an adopted model, and `sync --auto-commit` leaves committing a model in another git
157
+ repository to that repository (reported as `git_committed: false` with a warning). See
158
+ [docs/FARM_AUTOMATION.md](../docs/FARM_AUTOMATION.md) for flags, JSON output and CI recipes.
159
+
135
160
  ### Working with Elements
136
161
 
137
162
  ```bash
@@ -277,31 +302,27 @@ dr mcp
277
302
  dr mcp --regenerate-key
278
303
  ```
279
304
 
280
- **API key**:
281
-
282
- - On first launch, `dr mcp` generates a random API key and stores it on disk with `0600` permissions. Interactive sessions are prompted for a storage path; non-interactive launches (the normal case — an MCP client spawning `dr mcp`) default to `~/.dr-mcp-key`.
283
- - The chosen path is recorded under `mcp.api_key_path` in `~/.dr-config.yaml` (or the path from `DR_CONFIG_PATH`, if set), so subsequent launches reuse the same key without re-prompting.
284
- - Every launch prints the current key to stderr (never stdout, which carries the MCP protocol) and requires the client to supply it back via the `DR_MCP_API_KEY` environment variable. The connection is rejected if `DR_MCP_API_KEY` is missing or doesn't match.
285
- - Run `dr mcp --regenerate-key` to rotate the key (e.g. after a suspected leak). This overwrites the key at its existing configured path (or the default path, prompting once if none is configured yet), updates `~/.dr-config.yaml`, prints the new key, and exits without starting the server — no manual deletion of config or key files required.
305
+ **Which project**: the server serves the project the client has open. It asks the client for its MCP roots after connecting and moves to the first root inside a DR project (or the first root, if none has a model yet). `DR_MCP_ROOT_PATH` pins it to one model instead, and tool calls can pass `rootPath`.
286
306
 
287
- **Configuring an MCP client**:
288
-
289
- Point your MCP client at the `dr` binary and pass the key via `env`:
307
+ **Configuring an MCP client**: in Claude Code, install the `dr` plugin (see [MCP_SERVER.md](../integrations/claude_code/docs/MCP_SERVER.md)). Other clients point at the `dr` binary:
290
308
 
291
309
  ```json
292
310
  {
293
311
  "mcpServers": {
294
- "documentation-robotics": {
312
+ "dr": {
295
313
  "command": "dr",
296
- "args": ["mcp"],
297
- "env": {
298
- "DR_MCP_API_KEY": "<key printed by dr mcp>"
299
- }
314
+ "args": ["mcp"]
300
315
  }
301
316
  }
302
317
  }
303
318
  ```
304
319
 
320
+ **API key (HTTP transport only)**: over stdio the client starts the server as your own user, so there is no key. `dr mcp --transport http` requires the key as a bearer token:
321
+
322
+ - On first launch it generates a random key and stores it with `0600` permissions. Interactive sessions are prompted for a storage path; non-interactive launches default to `~/.dr-mcp-key`. The path is recorded under `mcp.api_key_path` in `~/.dr-config.yaml` (or the path from `DR_CONFIG_PATH`).
323
+ - Every HTTP launch prints the key to stderr (`DR_MCP_QUIET_KEY=1` suppresses it).
324
+ - `dr mcp --regenerate-key` rotates it: it overwrites the key at its configured path, updates `~/.dr-config.yaml`, prints the new key and exits without starting the server.
325
+
305
326
  ## Configuration
306
327
 
307
328
  ### Model Structure
@@ -327,7 +348,7 @@ your-project/
327
348
  │ └── ...
328
349
  ```
329
350
 
330
- **Spec-version migrations**: When the model's spec version is older than the CLI's bundled spec (e.g. after a layer renumbering), run `dr upgrade` to migrate the on-disk model in place — see `MigrationRegistry` in `cli/src/core/migration-registry.ts` for the supported version paths.
351
+ **Spec-version migrations**: When the model's spec version is older than the CLI's bundled spec (e.g. after a layer renumbering), run `dr upgrade` to migrate the on-disk model in place — see `MigrationRegistry` in `cli/src/core/migration-registry.ts` for the supported version paths. `dr upgrade` also applies catalog changes (renamed element types, retired relationship types) to a model already at the current spec version, since the catalog can change between version bumps; see `cli/src/core/catalog-migration.ts`.
331
352
 
332
353
  ### Specification Reference
333
354
 
@@ -478,10 +499,19 @@ dr relationship add business-process-order business-process-payment \
478
499
  # List all relationships for an element
479
500
  dr relationship list business-process-order
480
501
 
481
- # Delete a relationship
502
+ # Delete a relationship (works even when an endpoint element no longer exists)
482
503
  dr relationship delete business-process-order business-process-payment
504
+
505
+ # Remove rows whose source or target no longer resolves to an element
506
+ # (matches both UUID ids and dotted paths; --dry-run lists without writing)
507
+ dr relationship prune --dry-run
508
+ dr relationship prune --layers api application --format json
483
509
  ```
484
510
 
511
+ `dr delete` removes the relationships that name the deleted element, so `prune` is for models
512
+ that already carry dangling rows (earlier deletes, hand edits, imports). Inside an active changeset
513
+ it stages the removals instead of writing `relationships.yaml`.
514
+
485
515
  ### Staging Workflow
486
516
 
487
517
  The staging feature allows you to safely prepare and review changes before committing them to the base model:
@@ -1,4 +1,4 @@
1
- import { getLayerOrder, isValidLayerName } from "../core/layers.js";
1
+ import { isValidLayerName } from "../core/layers.js";
2
2
  var RelationshipStrength;
3
3
  (function(RelationshipStrength2) {
4
4
  RelationshipStrength2.Weak = "weak", RelationshipStrength2.Medium = "medium", RelationshipStrength2.Strong = "strong";
@@ -108,7 +108,7 @@ class RelationshipClassifier {
108
108
  async validateSemantics(rel) {
109
109
  await this.load();
110
110
  const issues = [], warnings = [], relationshipType = this.relationshipTypeMap.get(rel.predicate);
111
- return relationshipType ? (!relationshipType.semantics.reflexivity && rel.source === rel.target && issues.push(`Predicate "${rel.predicate}" does not support reflexive relationships`), relationshipType.semantics.symmetry && rel.directionality === "unidirectional" && warnings.push(`Predicate "${rel.predicate}" is symmetric but may be unidirectional`), rel.isCrossLayer && (!isValidLayerName(rel.sourceLayer) || !isValidLayerName(rel.targetLayer) ? warnings.push(`Cross-layer relationship references unknown layer(s): ${rel.sourceLayer} \u2192 ${rel.targetLayer}`) : this.getLayerOrder(rel.sourceLayer, rel.targetLayer) < 0 && warnings.push(`Cross-layer relationship violates layer hierarchy: ${rel.sourceLayer} \u2192 ${rel.targetLayer}`))) : issues.push(`Unknown predicate: ${rel.predicate}`), {
111
+ return relationshipType ? (!relationshipType.semantics.reflexivity && rel.source === rel.target && issues.push(`Predicate "${rel.predicate}" does not support reflexive relationships`), relationshipType.semantics.symmetry && rel.directionality === "unidirectional" && warnings.push(`Predicate "${rel.predicate}" is symmetric but may be unidirectional`), rel.isCrossLayer && (!isValidLayerName(rel.sourceLayer) || !isValidLayerName(rel.targetLayer)) && warnings.push(`Cross-layer relationship references unknown layer(s): ${rel.sourceLayer} \u2192 ${rel.targetLayer}`)) : issues.push(`Unknown predicate: ${rel.predicate}`), {
112
112
  isValid: issues.length === 0,
113
113
  category: rel.category,
114
114
  predicate: rel.predicate,
@@ -166,13 +166,6 @@ class RelationshipClassifier {
166
166
  const strongPercentage = strongCount / relationships.length * 100, weakPercentage = weakCount / relationships.length * 100;
167
167
  return strongPercentage > 50 ? RelationshipStrength.Strong : weakPercentage > 50 ? RelationshipStrength.Weak : RelationshipStrength.Medium;
168
168
  }
169
- /**
170
- * Get layer ordering for compliance checking
171
- */
172
- getLayerOrder(layer1, layer2) {
173
- const order1 = getLayerOrder(layer1), order2 = getLayerOrder(layer2);
174
- return order1 - order2;
175
- }
176
169
  }
177
170
  export {
178
171
  RelationshipClassifier,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/analysis/relationship-classifier.ts"],
4
- "sourcesContent": ["/**\n * Relationship Classifier - Semantic classification of relationships\n *\n * Classifies relationships by semantic category, strength, and directionality.\n * Analyzes relationship patterns, validates against schemas, and detects issues.\n */\n\nimport { RelationshipCatalog, RelationshipType } from \"../core/relationship-catalog.js\";\nimport { ClassifiedRelationship } from \"../core/report-data-model.js\";\nimport { getLayerOrder, isValidLayerName } from \"../core/layers.js\";\nimport type { Relationship } from \"../types/index.js\";\n\n/**\n * Relationship strength assessment\n */\nexport enum RelationshipStrength {\n Weak = \"weak\",\n Medium = \"medium\",\n Strong = \"strong\",\n}\n\n/**\n * Relationship pattern analysis result\n */\nexport interface RelationshipPattern {\n predicate: string;\n category: string;\n count: number;\n percentage: number;\n averageStrength: RelationshipStrength;\n examples: string[];\n}\n\n/**\n * Transitive analysis result\n */\nexport interface TransitiveAnalysis {\n predicate: string;\n isTransitive: boolean;\n chains: TransitiveChain[];\n}\n\n/**\n * Transitive chain (A -> B -> C where A is implied to relate to C)\n */\nexport interface TransitiveChain {\n elements: string[];\n pathLength: number;\n}\n\n/**\n * Classification statistics grouped by category\n */\nexport interface CategoryStatistics {\n category: string;\n count: number;\n percentage: number;\n predicates: string[];\n avgStrength: RelationshipStrength;\n}\n\n/**\n * Validates and analyzes semantic compliance\n */\nexport interface SemanticValidationResult {\n isValid: boolean;\n category: string;\n predicate: string;\n issues: string[];\n warnings: string[];\n}\n\n/**\n * Relationship Classifier\n */\nexport class RelationshipClassifier {\n private catalog: RelationshipCatalog;\n private relationshipTypeMap: Map<string, RelationshipType> = new Map();\n private catalogLoaded = false;\n\n constructor(catalog: RelationshipCatalog) {\n this.catalog = catalog;\n }\n\n /**\n * Load and prepare the catalog\n */\n async load(): Promise<void> {\n if (this.catalogLoaded) return;\n\n await this.catalog.load();\n const types = this.catalog.getAllTypes();\n\n for (const type of types) {\n this.relationshipTypeMap.set(type.predicate, type);\n }\n\n this.catalogLoaded = true;\n }\n\n /**\n * Classify a single relationship with detailed semantic information\n */\n async classify(\n rel: Relationship,\n relationshipId?: string\n ): Promise<ClassifiedRelationship> {\n await this.load();\n\n const sourceLayer = rel.source.split(\".\")[0];\n const targetLayer = rel.target.split(\".\")[0];\n const isCrossLayer = sourceLayer !== targetLayer;\n\n const relationshipType = this.relationshipTypeMap.get(rel.predicate);\n\n return {\n id: relationshipId || `${rel.source}\u2192${rel.predicate}\u2192${rel.target}`,\n source: rel.source,\n target: rel.target,\n predicate: rel.predicate,\n category: relationshipType?.category || \"unknown\",\n archimateAlignment: relationshipType?.archimateAlignment || null,\n directionality: relationshipType?.semantics?.directionality || \"unidirectional\",\n transitivity: relationshipType?.semantics?.transitivity || false,\n symmetry: relationshipType?.semantics?.symmetry || false,\n reflexivity: relationshipType?.semantics?.reflexivity,\n sourceLayer,\n targetLayer,\n isCrossLayer,\n isSpecCompliant: relationshipType !== undefined,\n };\n }\n\n /**\n * Classify a batch of relationships\n */\n async classifyBatch(\n relationships: Relationship[]\n ): Promise<ClassifiedRelationship[]> {\n const classified: ClassifiedRelationship[] = [];\n\n for (let i = 0; i < relationships.length; i++) {\n const rel = relationships[i];\n const relationshipId = `${rel.source}\u2192${rel.predicate}\u2192${rel.target}`;\n const classifiedRel = await this.classify(rel, relationshipId);\n classified.push(classifiedRel);\n }\n\n return classified;\n }\n\n /**\n * Get relationship breakdown by category\n */\n async getCategoryBreakdown(\n relationships: ClassifiedRelationship[]\n ): Promise<CategoryStatistics[]> {\n const byCategory: Record<string, ClassifiedRelationship[]> = {};\n\n for (const rel of relationships) {\n if (!byCategory[rel.category]) {\n byCategory[rel.category] = [];\n }\n byCategory[rel.category].push(rel);\n }\n\n const breakdown: CategoryStatistics[] = [];\n\n for (const [category, rels] of Object.entries(byCategory)) {\n const predicates = Array.from(new Set(rels.map((r) => r.predicate)));\n const avgStrength = this.calculateAverageStrength(rels);\n\n breakdown.push({\n category,\n count: rels.length,\n percentage: (rels.length / relationships.length) * 100,\n predicates,\n avgStrength,\n });\n }\n\n return breakdown.sort((a, b) => b.count - a.count);\n }\n\n /**\n * Analyze transitive relationships for a specific predicate\n */\n async analyzeTransitivity(\n relationships: ClassifiedRelationship[],\n predicate: string\n ): Promise<TransitiveAnalysis> {\n const type = this.relationshipTypeMap.get(predicate);\n const isTransitive = type?.semantics.transitivity || false;\n\n const chains: TransitiveChain[] = [];\n\n if (isTransitive) {\n // Build a map of relationships for this predicate\n const relsByPredicate = relationships.filter((r) => r.predicate === predicate);\n\n // Find chains A -> B -> C\n for (const rel1 of relsByPredicate) {\n for (const rel2 of relsByPredicate) {\n if (rel1.target === rel2.source && rel1.source !== rel2.target) {\n chains.push({\n elements: [rel1.source, rel1.target, rel2.target],\n pathLength: 2,\n });\n }\n }\n }\n\n // Remove duplicate chains\n const uniqueChains = Array.from(\n new Map(chains.map((c) => [c.elements.join(\"\u2192\"), c])).values()\n );\n\n return {\n predicate,\n isTransitive: true,\n chains: uniqueChains,\n };\n }\n\n return {\n predicate,\n isTransitive: false,\n chains: [],\n };\n }\n\n /**\n * Validate a relationship against semantic rules\n */\n async validateSemantics(\n rel: ClassifiedRelationship\n ): Promise<SemanticValidationResult> {\n await this.load();\n\n const issues: string[] = [];\n const warnings: string[] = [];\n\n const relationshipType = this.relationshipTypeMap.get(rel.predicate);\n\n if (!relationshipType) {\n issues.push(`Unknown predicate: ${rel.predicate}`);\n } else {\n // Check reflexivity\n if (!relationshipType.semantics.reflexivity && rel.source === rel.target) {\n issues.push(`Predicate \"${rel.predicate}\" does not support reflexive relationships`);\n }\n\n // Check symmetry\n if (relationshipType.semantics.symmetry && rel.directionality === \"unidirectional\") {\n warnings.push(`Predicate \"${rel.predicate}\" is symmetric but may be unidirectional`);\n }\n\n // Check layer direction for cross-layer relationships\n if (rel.isCrossLayer) {\n // First check if both layers are valid\n if (!isValidLayerName(rel.sourceLayer) || !isValidLayerName(rel.targetLayer)) {\n warnings.push(\n `Cross-layer relationship references unknown layer(s): ${rel.sourceLayer} \u2192 ${rel.targetLayer}`\n );\n } else {\n // Only check hierarchy if both layers are known\n const layerOrder = this.getLayerOrder(rel.sourceLayer, rel.targetLayer);\n if (layerOrder < 0) {\n warnings.push(\n `Cross-layer relationship violates layer hierarchy: ${rel.sourceLayer} \u2192 ${rel.targetLayer}`\n );\n }\n }\n }\n }\n\n return {\n isValid: issues.length === 0,\n category: rel.category,\n predicate: rel.predicate,\n issues,\n warnings,\n };\n }\n\n /**\n * Validate a batch of relationships\n */\n async validateBatch(\n relationships: ClassifiedRelationship[]\n ): Promise<SemanticValidationResult[]> {\n const results: SemanticValidationResult[] = [];\n\n for (const rel of relationships) {\n const result = await this.validateSemantics(rel);\n results.push(result);\n }\n\n return results;\n }\n\n /**\n * Find relationships that violate semantic rules\n */\n async findSemanticViolations(\n relationships: ClassifiedRelationship[]\n ): Promise<SemanticValidationResult[]> {\n const validations = await this.validateBatch(relationships);\n return validations.filter((v) => !v.isValid || v.issues.length > 0);\n }\n\n /**\n * Analyze relationship patterns\n */\n async analyzePatterns(\n relationships: ClassifiedRelationship[]\n ): Promise<RelationshipPattern[]> {\n const byPredicate: Record<string, ClassifiedRelationship[]> = {};\n\n for (const rel of relationships) {\n if (!byPredicate[rel.predicate]) {\n byPredicate[rel.predicate] = [];\n }\n byPredicate[rel.predicate].push(rel);\n }\n\n const patterns: RelationshipPattern[] = [];\n\n for (const [predicate, rels] of Object.entries(byPredicate)) {\n const relationshipType = this.relationshipTypeMap.get(predicate);\n const examples = rels.slice(0, 3).map((r) => `${r.source} \u2192 ${r.target}`);\n const avgStrength = this.calculateAverageStrength(rels);\n\n patterns.push({\n predicate,\n category: relationshipType?.category || \"unknown\",\n count: rels.length,\n percentage: (rels.length / relationships.length) * 100,\n averageStrength: avgStrength,\n examples,\n });\n }\n\n return patterns.sort((a, b) => b.count - a.count);\n }\n\n /**\n * Calculate average relationship strength\n */\n private calculateAverageStrength(\n relationships: ClassifiedRelationship[]\n ): RelationshipStrength {\n if (relationships.length === 0) return RelationshipStrength.Medium;\n\n let strongCount = 0;\n let weakCount = 0;\n\n for (const rel of relationships) {\n // Strong: transitive, symmetric relationships\n if (rel.transitivity && rel.symmetry) {\n strongCount++;\n }\n // Weak: unidirectional, non-reflexive\n else if (rel.directionality === \"unidirectional\" && rel.reflexivity === false) {\n weakCount++;\n }\n }\n\n const strongPercentage = (strongCount / relationships.length) * 100;\n const weakPercentage = (weakCount / relationships.length) * 100;\n\n if (strongPercentage > 50) {\n return RelationshipStrength.Strong;\n } else if (weakPercentage > 50) {\n return RelationshipStrength.Weak;\n }\n\n return RelationshipStrength.Medium;\n }\n\n /**\n * Get layer ordering for compliance checking\n */\n private getLayerOrder(layer1: string, layer2: string): number {\n const order1 = getLayerOrder(layer1);\n const order2 = getLayerOrder(layer2);\n return order1 - order2;\n }\n}\n"],
5
- "mappings": "AASA,SAAS,eAAe,wBAAwB;AAMhD,IAAY;CAAZ,SAAYA,uBAAoB;AAC9B,EAAAA,sBAAA,OAAA,QACAA,sBAAA,SAAA,UACAA,sBAAA,SAAA;AACF,GAJY,yBAAA,uBAAoB,CAAA,EAAA;AA4D1B,MAAO,uBAAsB;EACzB;EACA,sBAAqD,oBAAI,IAAG;EAC5D,gBAAgB;EAExB,YAAY,SAA4B;AACtC,SAAK,UAAU;EACjB;;;;EAKA,MAAM,OAAI;AACR,QAAI,KAAK;AAAe;AAExB,UAAM,KAAK,QAAQ,KAAI;AACvB,UAAM,QAAQ,KAAK,QAAQ,YAAW;AAEtC,eAAW,QAAQ;AACjB,WAAK,oBAAoB,IAAI,KAAK,WAAW,IAAI;AAGnD,SAAK,gBAAgB;EACvB;;;;EAKA,MAAM,SACJ,KACA,gBAAuB;AAEvB,UAAM,KAAK,KAAI;AAEf,UAAM,cAAc,IAAI,OAAO,MAAM,GAAG,EAAE,CAAC,GACrC,cAAc,IAAI,OAAO,MAAM,GAAG,EAAE,CAAC,GACrC,eAAe,gBAAgB,aAE/B,mBAAmB,KAAK,oBAAoB,IAAI,IAAI,SAAS;AAEnE,WAAO;MACL,IAAI,kBAAkB,GAAG,IAAI,MAAM,SAAI,IAAI,SAAS,SAAI,IAAI,MAAM;MAClE,QAAQ,IAAI;MACZ,QAAQ,IAAI;MACZ,WAAW,IAAI;MACf,UAAU,kBAAkB,YAAY;MACxC,oBAAoB,kBAAkB,sBAAsB;MAC5D,gBAAgB,kBAAkB,WAAW,kBAAkB;MAC/D,cAAc,kBAAkB,WAAW,gBAAgB;MAC3D,UAAU,kBAAkB,WAAW,YAAY;MACnD,aAAa,kBAAkB,WAAW;MAC1C;MACA;MACA;MACA,iBAAiB,qBAAqB;;EAE1C;;;;EAKA,MAAM,cACJ,eAA6B;AAE7B,UAAM,aAAuC,CAAA;AAE7C,aAAS,IAAI,GAAG,IAAI,cAAc,QAAQ,KAAK;AAC7C,YAAM,MAAM,cAAc,CAAC,GACrB,iBAAiB,GAAG,IAAI,MAAM,SAAI,IAAI,SAAS,SAAI,IAAI,MAAM,IAC7D,gBAAgB,MAAM,KAAK,SAAS,KAAK,cAAc;AAC7D,iBAAW,KAAK,aAAa;IAC/B;AAEA,WAAO;EACT;;;;EAKA,MAAM,qBACJ,eAAuC;AAEvC,UAAM,aAAuD,CAAA;AAE7D,eAAW,OAAO;AAChB,MAAK,WAAW,IAAI,QAAQ,MAC1B,WAAW,IAAI,QAAQ,IAAI,CAAA,IAE7B,WAAW,IAAI,QAAQ,EAAE,KAAK,GAAG;AAGnC,UAAM,YAAkC,CAAA;AAExC,eAAW,CAAC,UAAU,IAAI,KAAK,OAAO,QAAQ,UAAU,GAAG;AACzD,YAAM,aAAa,MAAM,KAAK,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,GAC7D,cAAc,KAAK,yBAAyB,IAAI;AAEtD,gBAAU,KAAK;QACb;QACA,OAAO,KAAK;QACZ,YAAa,KAAK,SAAS,cAAc,SAAU;QACnD;QACA;OACD;IACH;AAEA,WAAO,UAAU,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;EACnD;;;;EAKA,MAAM,oBACJ,eACA,WAAiB;AAGjB,UAAM,eADO,KAAK,oBAAoB,IAAI,SAAS,GACxB,UAAU,gBAAgB,IAE/C,SAA4B,CAAA;AAElC,QAAI,cAAc;AAEhB,YAAM,kBAAkB,cAAc,OAAO,CAAC,MAAM,EAAE,cAAc,SAAS;AAG7E,iBAAW,QAAQ;AACjB,mBAAW,QAAQ;AACjB,UAAI,KAAK,WAAW,KAAK,UAAU,KAAK,WAAW,KAAK,UACtD,OAAO,KAAK;YACV,UAAU,CAAC,KAAK,QAAQ,KAAK,QAAQ,KAAK,MAAM;YAChD,YAAY;WACb;AAMP,YAAM,eAAe,MAAM,KACzB,IAAI,IAAI,OAAO,IAAI,CAAC,MAAM,CAAC,EAAE,SAAS,KAAK,QAAG,GAAG,CAAC,CAAC,CAAC,EAAE,OAAM,CAAE;AAGhE,aAAO;QACL;QACA,cAAc;QACd,QAAQ;;IAEZ;AAEA,WAAO;MACL;MACA,cAAc;MACd,QAAQ,CAAA;;EAEZ;;;;EAKA,MAAM,kBACJ,KAA2B;AAE3B,UAAM,KAAK,KAAI;AAEf,UAAM,SAAmB,CAAA,GACnB,WAAqB,CAAA,GAErB,mBAAmB,KAAK,oBAAoB,IAAI,IAAI,SAAS;AAEnE,WAAK,oBAIC,CAAC,iBAAiB,UAAU,eAAe,IAAI,WAAW,IAAI,UAChE,OAAO,KAAK,cAAc,IAAI,SAAS,4CAA4C,GAIjF,iBAAiB,UAAU,YAAY,IAAI,mBAAmB,oBAChE,SAAS,KAAK,cAAc,IAAI,SAAS,0CAA0C,GAIjF,IAAI,iBAEF,CAAC,iBAAiB,IAAI,WAAW,KAAK,CAAC,iBAAiB,IAAI,WAAW,IACzE,SAAS,KACP,yDAAyD,IAAI,WAAW,WAAM,IAAI,WAAW,EAAE,IAI9E,KAAK,cAAc,IAAI,aAAa,IAAI,WAAW,IACrD,KACf,SAAS,KACP,sDAAsD,IAAI,WAAW,WAAM,IAAI,WAAW,EAAE,MAxBpG,OAAO,KAAK,sBAAsB,IAAI,SAAS,EAAE,GA+B5C;MACL,SAAS,OAAO,WAAW;MAC3B,UAAU,IAAI;MACd,WAAW,IAAI;MACf;MACA;;EAEJ;;;;EAKA,MAAM,cACJ,eAAuC;AAEvC,UAAM,UAAsC,CAAA;AAE5C,eAAW,OAAO,eAAe;AAC/B,YAAM,SAAS,MAAM,KAAK,kBAAkB,GAAG;AAC/C,cAAQ,KAAK,MAAM;IACrB;AAEA,WAAO;EACT;;;;EAKA,MAAM,uBACJ,eAAuC;AAGvC,YADoB,MAAM,KAAK,cAAc,aAAa,GACvC,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,OAAO,SAAS,CAAC;EACpE;;;;EAKA,MAAM,gBACJ,eAAuC;AAEvC,UAAM,cAAwD,CAAA;AAE9D,eAAW,OAAO;AAChB,MAAK,YAAY,IAAI,SAAS,MAC5B,YAAY,IAAI,SAAS,IAAI,CAAA,IAE/B,YAAY,IAAI,SAAS,EAAE,KAAK,GAAG;AAGrC,UAAM,WAAkC,CAAA;AAExC,eAAW,CAAC,WAAW,IAAI,KAAK,OAAO,QAAQ,WAAW,GAAG;AAC3D,YAAM,mBAAmB,KAAK,oBAAoB,IAAI,SAAS,GACzD,WAAW,KAAK,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,MAAM,GAAG,EAAE,MAAM,WAAM,EAAE,MAAM,EAAE,GAClE,cAAc,KAAK,yBAAyB,IAAI;AAEtD,eAAS,KAAK;QACZ;QACA,UAAU,kBAAkB,YAAY;QACxC,OAAO,KAAK;QACZ,YAAa,KAAK,SAAS,cAAc,SAAU;QACnD,iBAAiB;QACjB;OACD;IACH;AAEA,WAAO,SAAS,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;EAClD;;;;EAKQ,yBACN,eAAuC;AAEvC,QAAI,cAAc,WAAW;AAAG,aAAO,qBAAqB;AAE5D,QAAI,cAAc,GACd,YAAY;AAEhB,eAAW,OAAO;AAEhB,MAAI,IAAI,gBAAgB,IAAI,WAC1B,gBAGO,IAAI,mBAAmB,oBAAoB,IAAI,gBAAgB,MACtE;AAIJ,UAAM,mBAAoB,cAAc,cAAc,SAAU,KAC1D,iBAAkB,YAAY,cAAc,SAAU;AAE5D,WAAI,mBAAmB,KACd,qBAAqB,SACnB,iBAAiB,KACnB,qBAAqB,OAGvB,qBAAqB;EAC9B;;;;EAKQ,cAAc,QAAgB,QAAc;AAClD,UAAM,SAAS,cAAc,MAAM,GAC7B,SAAS,cAAc,MAAM;AACnC,WAAO,SAAS;EAClB;;",
4
+ "sourcesContent": ["/**\n * Relationship Classifier - Semantic classification of relationships\n *\n * Classifies relationships by semantic category, strength, and directionality.\n * Analyzes relationship patterns, validates against schemas, and detects issues.\n */\n\nimport { RelationshipCatalog, RelationshipType } from \"../core/relationship-catalog.js\";\nimport { ClassifiedRelationship } from \"../core/report-data-model.js\";\nimport { isValidLayerName } from \"../core/layers.js\";\nimport type { Relationship } from \"../types/index.js\";\n\n/**\n * Relationship strength assessment\n */\nexport enum RelationshipStrength {\n Weak = \"weak\",\n Medium = \"medium\",\n Strong = \"strong\",\n}\n\n/**\n * Relationship pattern analysis result\n */\nexport interface RelationshipPattern {\n predicate: string;\n category: string;\n count: number;\n percentage: number;\n averageStrength: RelationshipStrength;\n examples: string[];\n}\n\n/**\n * Transitive analysis result\n */\nexport interface TransitiveAnalysis {\n predicate: string;\n isTransitive: boolean;\n chains: TransitiveChain[];\n}\n\n/**\n * Transitive chain (A -> B -> C where A is implied to relate to C)\n */\nexport interface TransitiveChain {\n elements: string[];\n pathLength: number;\n}\n\n/**\n * Classification statistics grouped by category\n */\nexport interface CategoryStatistics {\n category: string;\n count: number;\n percentage: number;\n predicates: string[];\n avgStrength: RelationshipStrength;\n}\n\n/**\n * Validates and analyzes semantic compliance\n */\nexport interface SemanticValidationResult {\n isValid: boolean;\n category: string;\n predicate: string;\n issues: string[];\n warnings: string[];\n}\n\n/**\n * Relationship Classifier\n */\nexport class RelationshipClassifier {\n private catalog: RelationshipCatalog;\n private relationshipTypeMap: Map<string, RelationshipType> = new Map();\n private catalogLoaded = false;\n\n constructor(catalog: RelationshipCatalog) {\n this.catalog = catalog;\n }\n\n /**\n * Load and prepare the catalog\n */\n async load(): Promise<void> {\n if (this.catalogLoaded) return;\n\n await this.catalog.load();\n const types = this.catalog.getAllTypes();\n\n for (const type of types) {\n this.relationshipTypeMap.set(type.predicate, type);\n }\n\n this.catalogLoaded = true;\n }\n\n /**\n * Classify a single relationship with detailed semantic information\n */\n async classify(\n rel: Relationship,\n relationshipId?: string\n ): Promise<ClassifiedRelationship> {\n await this.load();\n\n const sourceLayer = rel.source.split(\".\")[0];\n const targetLayer = rel.target.split(\".\")[0];\n const isCrossLayer = sourceLayer !== targetLayer;\n\n const relationshipType = this.relationshipTypeMap.get(rel.predicate);\n\n return {\n id: relationshipId || `${rel.source}\u2192${rel.predicate}\u2192${rel.target}`,\n source: rel.source,\n target: rel.target,\n predicate: rel.predicate,\n category: relationshipType?.category || \"unknown\",\n archimateAlignment: relationshipType?.archimateAlignment || null,\n directionality: relationshipType?.semantics?.directionality || \"unidirectional\",\n transitivity: relationshipType?.semantics?.transitivity || false,\n symmetry: relationshipType?.semantics?.symmetry || false,\n reflexivity: relationshipType?.semantics?.reflexivity,\n sourceLayer,\n targetLayer,\n isCrossLayer,\n isSpecCompliant: relationshipType !== undefined,\n };\n }\n\n /**\n * Classify a batch of relationships\n */\n async classifyBatch(\n relationships: Relationship[]\n ): Promise<ClassifiedRelationship[]> {\n const classified: ClassifiedRelationship[] = [];\n\n for (let i = 0; i < relationships.length; i++) {\n const rel = relationships[i];\n const relationshipId = `${rel.source}\u2192${rel.predicate}\u2192${rel.target}`;\n const classifiedRel = await this.classify(rel, relationshipId);\n classified.push(classifiedRel);\n }\n\n return classified;\n }\n\n /**\n * Get relationship breakdown by category\n */\n async getCategoryBreakdown(\n relationships: ClassifiedRelationship[]\n ): Promise<CategoryStatistics[]> {\n const byCategory: Record<string, ClassifiedRelationship[]> = {};\n\n for (const rel of relationships) {\n if (!byCategory[rel.category]) {\n byCategory[rel.category] = [];\n }\n byCategory[rel.category].push(rel);\n }\n\n const breakdown: CategoryStatistics[] = [];\n\n for (const [category, rels] of Object.entries(byCategory)) {\n const predicates = Array.from(new Set(rels.map((r) => r.predicate)));\n const avgStrength = this.calculateAverageStrength(rels);\n\n breakdown.push({\n category,\n count: rels.length,\n percentage: (rels.length / relationships.length) * 100,\n predicates,\n avgStrength,\n });\n }\n\n return breakdown.sort((a, b) => b.count - a.count);\n }\n\n /**\n * Analyze transitive relationships for a specific predicate\n */\n async analyzeTransitivity(\n relationships: ClassifiedRelationship[],\n predicate: string\n ): Promise<TransitiveAnalysis> {\n const type = this.relationshipTypeMap.get(predicate);\n const isTransitive = type?.semantics.transitivity || false;\n\n const chains: TransitiveChain[] = [];\n\n if (isTransitive) {\n // Build a map of relationships for this predicate\n const relsByPredicate = relationships.filter((r) => r.predicate === predicate);\n\n // Find chains A -> B -> C\n for (const rel1 of relsByPredicate) {\n for (const rel2 of relsByPredicate) {\n if (rel1.target === rel2.source && rel1.source !== rel2.target) {\n chains.push({\n elements: [rel1.source, rel1.target, rel2.target],\n pathLength: 2,\n });\n }\n }\n }\n\n // Remove duplicate chains\n const uniqueChains = Array.from(\n new Map(chains.map((c) => [c.elements.join(\"\u2192\"), c])).values()\n );\n\n return {\n predicate,\n isTransitive: true,\n chains: uniqueChains,\n };\n }\n\n return {\n predicate,\n isTransitive: false,\n chains: [],\n };\n }\n\n /**\n * Validate a relationship against semantic rules\n */\n async validateSemantics(\n rel: ClassifiedRelationship\n ): Promise<SemanticValidationResult> {\n await this.load();\n\n const issues: string[] = [];\n const warnings: string[] = [];\n\n const relationshipType = this.relationshipTypeMap.get(rel.predicate);\n\n if (!relationshipType) {\n issues.push(`Unknown predicate: ${rel.predicate}`);\n } else {\n // Check reflexivity\n if (!relationshipType.semantics.reflexivity && rel.source === rel.target) {\n issues.push(`Predicate \"${rel.predicate}\" does not support reflexive relationships`);\n }\n\n // Check symmetry\n if (relationshipType.semantics.symmetry && rel.directionality === \"unidirectional\") {\n warnings.push(`Predicate \"${rel.predicate}\" is symmetric but may be unidirectional`);\n }\n\n // Cross-layer relationships: both layers must exist. Direction is a property of the\n // predicate, not of the layers (spec/DESIGN_CHARTER.md), so no hierarchy check.\n if (rel.isCrossLayer) {\n if (!isValidLayerName(rel.sourceLayer) || !isValidLayerName(rel.targetLayer)) {\n warnings.push(\n `Cross-layer relationship references unknown layer(s): ${rel.sourceLayer} \u2192 ${rel.targetLayer}`\n );\n }\n }\n }\n\n return {\n isValid: issues.length === 0,\n category: rel.category,\n predicate: rel.predicate,\n issues,\n warnings,\n };\n }\n\n /**\n * Validate a batch of relationships\n */\n async validateBatch(\n relationships: ClassifiedRelationship[]\n ): Promise<SemanticValidationResult[]> {\n const results: SemanticValidationResult[] = [];\n\n for (const rel of relationships) {\n const result = await this.validateSemantics(rel);\n results.push(result);\n }\n\n return results;\n }\n\n /**\n * Find relationships that violate semantic rules\n */\n async findSemanticViolations(\n relationships: ClassifiedRelationship[]\n ): Promise<SemanticValidationResult[]> {\n const validations = await this.validateBatch(relationships);\n return validations.filter((v) => !v.isValid || v.issues.length > 0);\n }\n\n /**\n * Analyze relationship patterns\n */\n async analyzePatterns(\n relationships: ClassifiedRelationship[]\n ): Promise<RelationshipPattern[]> {\n const byPredicate: Record<string, ClassifiedRelationship[]> = {};\n\n for (const rel of relationships) {\n if (!byPredicate[rel.predicate]) {\n byPredicate[rel.predicate] = [];\n }\n byPredicate[rel.predicate].push(rel);\n }\n\n const patterns: RelationshipPattern[] = [];\n\n for (const [predicate, rels] of Object.entries(byPredicate)) {\n const relationshipType = this.relationshipTypeMap.get(predicate);\n const examples = rels.slice(0, 3).map((r) => `${r.source} \u2192 ${r.target}`);\n const avgStrength = this.calculateAverageStrength(rels);\n\n patterns.push({\n predicate,\n category: relationshipType?.category || \"unknown\",\n count: rels.length,\n percentage: (rels.length / relationships.length) * 100,\n averageStrength: avgStrength,\n examples,\n });\n }\n\n return patterns.sort((a, b) => b.count - a.count);\n }\n\n /**\n * Calculate average relationship strength\n */\n private calculateAverageStrength(\n relationships: ClassifiedRelationship[]\n ): RelationshipStrength {\n if (relationships.length === 0) return RelationshipStrength.Medium;\n\n let strongCount = 0;\n let weakCount = 0;\n\n for (const rel of relationships) {\n // Strong: transitive, symmetric relationships\n if (rel.transitivity && rel.symmetry) {\n strongCount++;\n }\n // Weak: unidirectional, non-reflexive\n else if (rel.directionality === \"unidirectional\" && rel.reflexivity === false) {\n weakCount++;\n }\n }\n\n const strongPercentage = (strongCount / relationships.length) * 100;\n const weakPercentage = (weakCount / relationships.length) * 100;\n\n if (strongPercentage > 50) {\n return RelationshipStrength.Strong;\n } else if (weakPercentage > 50) {\n return RelationshipStrength.Weak;\n }\n\n return RelationshipStrength.Medium;\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["RelationshipStrength"]
7
7
  }
@@ -0,0 +1,62 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { AnalyzerRegistry } from "./registry.js";
3
+ import { MappingLoader } from "./mapping-loader.js";
4
+ import { readIndexMeta, readSession } from "./session-state.js";
5
+ import { CLIError, ErrorCategory } from "../utils/errors.js";
6
+ const NO_ANALYZER_SELECTED = "No analyzer selected. Run `dr analyzer discover` first.", VERIFIABLE_LAYERS = Object.freeze(["api"]);
7
+ async function resolveAnalyzerName(projectRoot, explicit) {
8
+ if (explicit)
9
+ return explicit;
10
+ const session = await readSession(projectRoot);
11
+ if (!session)
12
+ throw new CLIError(NO_ANALYZER_SELECTED, ErrorCategory.USER);
13
+ return session.active_analyzer;
14
+ }
15
+ async function getAnalyzerBackend(name, suggestions) {
16
+ const registry = AnalyzerRegistry.getInstance();
17
+ await registry.initialize();
18
+ const backend = await registry.getAnalyzer(name);
19
+ if (!backend)
20
+ throw new CLIError(`Analyzer not found: ${name}`, ErrorCategory.NOT_FOUND, suggestions);
21
+ return backend;
22
+ }
23
+ async function analyzerHomepage(name) {
24
+ try {
25
+ return (await MappingLoader.load(name)).getAnalyzerMetadata()?.homepage || "https://github.com/search?q=codebase-memory";
26
+ } catch {
27
+ return "https://github.com/search?q=codebase-memory";
28
+ }
29
+ }
30
+ function splitVerifyLayers(requested) {
31
+ const layers = requested && requested.length > 0 ? [...requested] : ["api"];
32
+ return {
33
+ verifiable: layers.filter((layer) => VERIFIABLE_LAYERS.includes(layer)),
34
+ unsupported: layers.filter((layer) => !VERIFIABLE_LAYERS.includes(layer))
35
+ };
36
+ }
37
+ function currentGitHead(dir) {
38
+ try {
39
+ const result = spawnSync("git", ["rev-parse", "HEAD"], { cwd: dir, stdio: "pipe", encoding: "utf-8" });
40
+ return result.status !== 0 ? null : result.stdout.trim() || null;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+ async function readIndexFreshness(projectRoot, analyzerName, codebaseRoot, gitHead = currentGitHead) {
46
+ const meta = await readIndexMeta(projectRoot, analyzerName);
47
+ if (!meta)
48
+ return { indexed: !1, fresh: !1 };
49
+ const head = gitHead(codebaseRoot);
50
+ return { indexed: !0, fresh: head !== null && head === meta.git_head, meta, currentGitHead: head };
51
+ }
52
+ export {
53
+ NO_ANALYZER_SELECTED,
54
+ VERIFIABLE_LAYERS,
55
+ analyzerHomepage,
56
+ currentGitHead,
57
+ getAnalyzerBackend,
58
+ readIndexFreshness,
59
+ resolveAnalyzerName,
60
+ splitVerifyLayers
61
+ };
62
+ //# sourceMappingURL=active-analyzer.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/analyzers/active-analyzer.ts"],
4
+ "sourcesContent": ["/**\n * Shared steps of the analyzer commands (`dr analyzer index`, `dr analyzer verify`) and the\n * codebase MCP tools (`codebase_index`, `codebase_verify`): which analyzer a project uses, its\n * backend, how to install it, which layers verify covers, and whether the index is fresh.\n */\n\nimport { spawnSync } from \"node:child_process\";\nimport { AnalyzerRegistry } from \"./registry.js\";\nimport { MappingLoader } from \"./mapping-loader.js\";\nimport { readIndexMeta, readSession } from \"./session-state.js\";\nimport type { AnalyzerBackend } from \"./base-analyzer.js\";\nimport type { IndexMeta } from \"./types.js\";\nimport { CLIError, ErrorCategory } from \"../utils/errors.js\";\n\nexport const NO_ANALYZER_SELECTED = \"No analyzer selected. Run `dr analyzer discover` first.\";\n\n/** Layers `verify` can check. Others are reported as having no verification. */\nexport const VERIFIABLE_LAYERS: readonly string[] = Object.freeze([\"api\"]);\n\n/** The analyzer to use: `explicit`, else the one selected for the project (session.json). */\nexport async function resolveAnalyzerName(projectRoot: string, explicit?: string): Promise<string> {\n if (explicit) return explicit;\n const session = await readSession(projectRoot);\n if (!session) {\n throw new CLIError(NO_ANALYZER_SELECTED, ErrorCategory.USER);\n }\n return session.active_analyzer;\n}\n\n/** The registered backend for `name`. Throws a not-found CLIError, with `suggestions`, if none. */\nexport async function getAnalyzerBackend(name: string, suggestions?: string[]): Promise<AnalyzerBackend> {\n const registry = AnalyzerRegistry.getInstance();\n await registry.initialize();\n const backend = await registry.getAnalyzer(name);\n if (!backend) {\n throw new CLIError(`Analyzer not found: ${name}`, ErrorCategory.NOT_FOUND, suggestions);\n }\n return backend;\n}\n\n/** The analyzer's homepage, from its mapping metadata. */\nexport async function analyzerHomepage(name: string): Promise<string> {\n try {\n const mapper = await MappingLoader.load(name);\n return mapper.getAnalyzerMetadata()?.homepage || \"https://github.com/search?q=codebase-memory\";\n } catch {\n return \"https://github.com/search?q=codebase-memory\";\n }\n}\n\n/** Splits requested layers into the ones verify checks and the ones it has no check for. */\nexport function splitVerifyLayers(requested: readonly string[] | undefined): {\n verifiable: string[];\n unsupported: string[];\n} {\n const layers = requested && requested.length > 0 ? [...requested] : [\"api\"];\n return {\n verifiable: layers.filter((layer) => VERIFIABLE_LAYERS.includes(layer)),\n unsupported: layers.filter((layer) => !VERIFIABLE_LAYERS.includes(layer)),\n };\n}\n\n/** The git HEAD of `dir`, or null when it isn't a git repository or git fails. */\nexport function currentGitHead(dir: string): string | null {\n try {\n const result = spawnSync(\"git\", [\"rev-parse\", \"HEAD\"], { cwd: dir, stdio: \"pipe\", encoding: \"utf-8\" });\n if (result.status !== 0) return null;\n return result.stdout.trim() || null;\n } catch {\n return null;\n }\n}\n\nexport type IndexFreshness =\n | { indexed: false; fresh: false }\n | {\n indexed: true;\n /** The index was taken at the code's current git HEAD. */\n fresh: boolean;\n meta: IndexMeta;\n /** The code's current git HEAD; null when it can't be read (the index then counts as stale). */\n currentGitHead: string | null;\n };\n\n/**\n * Whether the project has an index from `analyzerName`, and whether it was taken at the code's\n * current git HEAD. Reads `.dr/analyzers/{analyzer}/index-meta.json` and runs git; it does not\n * start the analyzer. The same test as the analyzer's own `status()`.\n */\nexport async function readIndexFreshness(\n projectRoot: string,\n analyzerName: string,\n codebaseRoot: string,\n gitHead: (dir: string) => string | null = currentGitHead\n): Promise<IndexFreshness> {\n const meta = await readIndexMeta(projectRoot, analyzerName);\n if (!meta) return { indexed: false, fresh: false };\n const head = gitHead(codebaseRoot);\n return { indexed: true, fresh: head !== null && head === meta.git_head, meta, currentGitHead: head };\n}\n"],
5
+ "mappings": "AAMA,SAAS,iBAAiB;AAC1B,SAAS,wBAAwB;AACjC,SAAS,qBAAqB;AAC9B,SAAS,eAAe,mBAAmB;AAG3C,SAAS,UAAU,qBAAqB;AAEjC,MAAM,uBAAuB,2DAGvB,oBAAuC,OAAO,OAAO,CAAC,KAAK,CAAC;AAGzE,eAAsB,oBAAoB,aAAqB,UAAiB;AAC9E,MAAI;AAAU,WAAO;AACrB,QAAM,UAAU,MAAM,YAAY,WAAW;AAC7C,MAAI,CAAC;AACH,UAAM,IAAI,SAAS,sBAAsB,cAAc,IAAI;AAE7D,SAAO,QAAQ;AACjB;AAGA,eAAsB,mBAAmB,MAAc,aAAsB;AAC3E,QAAM,WAAW,iBAAiB,YAAW;AAC7C,QAAM,SAAS,WAAU;AACzB,QAAM,UAAU,MAAM,SAAS,YAAY,IAAI;AAC/C,MAAI,CAAC;AACH,UAAM,IAAI,SAAS,uBAAuB,IAAI,IAAI,cAAc,WAAW,WAAW;AAExF,SAAO;AACT;AAGA,eAAsB,iBAAiB,MAAY;AACjD,MAAI;AAEF,YADe,MAAM,cAAc,KAAK,IAAI,GAC9B,oBAAmB,GAAI,YAAY;EACnD,QAAQ;AACN,WAAO;EACT;AACF;AAGM,SAAU,kBAAkB,WAAwC;AAIxE,QAAM,SAAS,aAAa,UAAU,SAAS,IAAI,CAAC,GAAG,SAAS,IAAI,CAAC,KAAK;AAC1E,SAAO;IACL,YAAY,OAAO,OAAO,CAAC,UAAU,kBAAkB,SAAS,KAAK,CAAC;IACtE,aAAa,OAAO,OAAO,CAAC,UAAU,CAAC,kBAAkB,SAAS,KAAK,CAAC;;AAE5E;AAGM,SAAU,eAAe,KAAW;AACxC,MAAI;AACF,UAAM,SAAS,UAAU,OAAO,CAAC,aAAa,MAAM,GAAG,EAAE,KAAK,KAAK,OAAO,QAAQ,UAAU,QAAO,CAAE;AACrG,WAAI,OAAO,WAAW,IAAU,OACzB,OAAO,OAAO,KAAI,KAAM;EACjC,QAAQ;AACN,WAAO;EACT;AACF;AAkBA,eAAsB,mBACpB,aACA,cACA,cACA,UAA0C,gBAAc;AAExD,QAAM,OAAO,MAAM,cAAc,aAAa,YAAY;AAC1D,MAAI,CAAC;AAAM,WAAO,EAAE,SAAS,IAAO,OAAO,GAAK;AAChD,QAAM,OAAO,QAAQ,YAAY;AACjC,SAAO,EAAE,SAAS,IAAM,OAAO,SAAS,QAAQ,SAAS,KAAK,UAAU,MAAM,gBAAgB,KAAI;AACpG;",
6
+ "names": []
7
+ }
@@ -1,4 +1,4 @@
1
- import { relative } from "node:path";
1
+ import { isAbsolute, relative } from "node:path";
2
2
  function isEdgeType(value) {
3
3
  return value === "CALLS" || value === "HTTP_CALLS" || value === "HANDLES";
4
4
  }
@@ -11,7 +11,7 @@ function shapeCallGraphNode(responseNode, projectRoot) {
11
11
  try {
12
12
  if (typeof sourceFile != "string" || typeof projectRoot != "string")
13
13
  throw new TypeError(`Invalid path types: sourceFile=${typeof sourceFile}, projectRoot=${typeof projectRoot}`);
14
- sourceFile = relative(projectRoot, sourceFile);
14
+ sourceFile = isAbsolute(sourceFile) ? relative(projectRoot, sourceFile) : sourceFile;
15
15
  } catch (error) {
16
16
  const errorMessage = error instanceof Error ? error.message : String(error);
17
17
  console.warn(`Warning: Failed to compute relative path for "${sourceFile}": ${errorMessage}`);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/analyzers/call-graph-utils.ts"],
4
- "sourcesContent": ["/**\n * Utilities for call graph analysis\n */\n\nimport { relative } from \"node:path\";\nimport type { CallGraphNode } from \"./types.js\";\n\n/**\n * Valid edge types for call graph edges\n */\ntype EdgeType = \"CALLS\" | \"HTTP_CALLS\" | \"HANDLES\";\n\n/**\n * Type guard to ensure a value is a valid edge type\n */\nfunction isEdgeType(value: unknown): value is EdgeType {\n return value === \"CALLS\" || value === \"HTTP_CALLS\" || value === \"HANDLES\";\n}\n\n/**\n * Clamp depth to valid range (default 3, min 0, max 10)\n */\nexport function clampDepth(depth: number | undefined): number {\n return Math.max(Math.min(depth ?? 3, 10), 0);\n}\n\n/**\n * Shape raw response node to CallGraphNode\n */\nexport function shapeCallGraphNode(\n responseNode: any,\n projectRoot?: string\n): CallGraphNode {\n // Extract source file and compute relative path if projectRoot provided\n let sourceFile = responseNode.file_path || responseNode.source_file || \"\";\n if (sourceFile && projectRoot) {\n try {\n // Validate inputs are strings before calling path.relative()\n if (typeof sourceFile !== \"string\" || typeof projectRoot !== \"string\") {\n throw new TypeError(\n `Invalid path types: sourceFile=${typeof sourceFile}, projectRoot=${typeof projectRoot}`\n );\n }\n sourceFile = relative(projectRoot, sourceFile);\n } catch (error) {\n // Log the error but keep the original path\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.warn(\n `Warning: Failed to compute relative path for \"${sourceFile}\": ${errorMessage}`\n );\n }\n }\n\n return {\n qualified_name: responseNode.qualified_name || responseNode.id,\n source_file: sourceFile,\n source_symbol: responseNode.source_symbol || responseNode.id || \"\",\n depth: typeof responseNode.depth === \"number\" ? responseNode.depth : 0,\n edge_type: \"CALLS\",\n };\n}\n\n/**\n * Determine edge type based on node depth and edges list\n */\nexport function determineEdgeType(\n nodeDepth: number,\n edges: Array<{ from_node: string; to_node: string; type?: string }>,\n targetNodeQualifiedName: string,\n validEdgeTypes: string[] = [\"CALLS\", \"HTTP_CALLS\", \"HANDLES\"]\n): EdgeType {\n const defaultEdgeType = validEdgeTypes[0] || \"CALLS\";\n\n // For root nodes (depth 0), always use default\n if (nodeDepth === 0) {\n // Validate that default is a known edge type\n if (!isEdgeType(defaultEdgeType)) {\n return \"CALLS\"; // Fallback to safe default\n }\n return defaultEdgeType;\n }\n\n // For non-root nodes, find incoming edge\n const incomingEdge = edges.find(\n (edge) => edge.to_node === targetNodeQualifiedName\n );\n if (incomingEdge && incomingEdge.type && validEdgeTypes.includes(incomingEdge.type)) {\n // Validate that the edge type is known\n if (isEdgeType(incomingEdge.type)) {\n return incomingEdge.type;\n }\n }\n\n // Return validated default\n if (!isEdgeType(defaultEdgeType)) {\n return \"CALLS\"; // Fallback to safe default\n }\n return defaultEdgeType;\n}\n"],
5
- "mappings": "AAIA,SAAS,gBAAgB;AAWzB,SAAS,WAAW,OAAc;AAChC,SAAO,UAAU,WAAW,UAAU,gBAAgB,UAAU;AAClE;AAKM,SAAU,WAAW,OAAyB;AAClD,SAAO,KAAK,IAAI,KAAK,IAAI,SAAS,GAAG,EAAE,GAAG,CAAC;AAC7C;AAKM,SAAU,mBACd,cACA,aAAoB;AAGpB,MAAI,aAAa,aAAa,aAAa,aAAa,eAAe;AACvE,MAAI,cAAc;AAChB,QAAI;AAEF,UAAI,OAAO,cAAe,YAAY,OAAO,eAAgB;AAC3D,cAAM,IAAI,UACR,kCAAkC,OAAO,UAAU,iBAAiB,OAAO,WAAW,EAAE;AAG5F,mBAAa,SAAS,aAAa,UAAU;IAC/C,SAAS,OAAO;AAEd,YAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC1E,cAAQ,KACN,iDAAiD,UAAU,MAAM,YAAY,EAAE;IAEnF;AAGF,SAAO;IACL,gBAAgB,aAAa,kBAAkB,aAAa;IAC5D,aAAa;IACb,eAAe,aAAa,iBAAiB,aAAa,MAAM;IAChE,OAAO,OAAO,aAAa,SAAU,WAAW,aAAa,QAAQ;IACrE,WAAW;;AAEf;AAKM,SAAU,kBACd,WACA,OACA,yBACA,iBAA2B,CAAC,SAAS,cAAc,SAAS,GAAC;AAE7D,QAAM,kBAAkB,eAAe,CAAC,KAAK;AAG7C,MAAI,cAAc;AAEhB,WAAK,WAAW,eAAe,IAGxB,kBAFE;AAMX,QAAM,eAAe,MAAM,KACzB,CAAC,SAAS,KAAK,YAAY,uBAAuB;AAEpD,SAAI,gBAAgB,aAAa,QAAQ,eAAe,SAAS,aAAa,IAAI,KAE5E,WAAW,aAAa,IAAI,IACvB,aAAa,OAKnB,WAAW,eAAe,IAGxB,kBAFE;AAGX;",
4
+ "sourcesContent": ["/**\n * Utilities for call graph analysis\n */\n\nimport { isAbsolute, relative } from \"node:path\";\nimport type { CallGraphNode } from \"./types.js\";\n\n/**\n * Valid edge types for call graph edges\n */\ntype EdgeType = \"CALLS\" | \"HTTP_CALLS\" | \"HANDLES\";\n\n/**\n * Type guard to ensure a value is a valid edge type\n */\nfunction isEdgeType(value: unknown): value is EdgeType {\n return value === \"CALLS\" || value === \"HTTP_CALLS\" || value === \"HANDLES\";\n}\n\n/**\n * Clamp depth to valid range (default 3, min 0, max 10)\n */\nexport function clampDepth(depth: number | undefined): number {\n return Math.max(Math.min(depth ?? 3, 10), 0);\n}\n\n/**\n * Shape raw response node to CallGraphNode\n */\nexport function shapeCallGraphNode(\n responseNode: any,\n projectRoot?: string\n): CallGraphNode {\n // Extract source file and compute relative path if projectRoot provided\n let sourceFile = responseNode.file_path || responseNode.source_file || \"\";\n if (sourceFile && projectRoot) {\n try {\n // Validate inputs are strings before calling path.relative()\n if (typeof sourceFile !== \"string\" || typeof projectRoot !== \"string\") {\n throw new TypeError(\n `Invalid path types: sourceFile=${typeof sourceFile}, projectRoot=${typeof projectRoot}`\n );\n }\n // CBM paths are repository-relative; only absolute ones need relativising\n sourceFile = isAbsolute(sourceFile) ? relative(projectRoot, sourceFile) : sourceFile;\n } catch (error) {\n // Log the error but keep the original path\n const errorMessage = error instanceof Error ? error.message : String(error);\n console.warn(\n `Warning: Failed to compute relative path for \"${sourceFile}\": ${errorMessage}`\n );\n }\n }\n\n return {\n qualified_name: responseNode.qualified_name || responseNode.id,\n source_file: sourceFile,\n source_symbol: responseNode.source_symbol || responseNode.id || \"\",\n depth: typeof responseNode.depth === \"number\" ? responseNode.depth : 0,\n edge_type: \"CALLS\",\n };\n}\n\n/**\n * Determine edge type based on node depth and edges list\n */\nexport function determineEdgeType(\n nodeDepth: number,\n edges: Array<{ from_node: string; to_node: string; type?: string }>,\n targetNodeQualifiedName: string,\n validEdgeTypes: string[] = [\"CALLS\", \"HTTP_CALLS\", \"HANDLES\"]\n): EdgeType {\n const defaultEdgeType = validEdgeTypes[0] || \"CALLS\";\n\n // For root nodes (depth 0), always use default\n if (nodeDepth === 0) {\n // Validate that default is a known edge type\n if (!isEdgeType(defaultEdgeType)) {\n return \"CALLS\"; // Fallback to safe default\n }\n return defaultEdgeType;\n }\n\n // For non-root nodes, find incoming edge\n const incomingEdge = edges.find(\n (edge) => edge.to_node === targetNodeQualifiedName\n );\n if (incomingEdge && incomingEdge.type && validEdgeTypes.includes(incomingEdge.type)) {\n // Validate that the edge type is known\n if (isEdgeType(incomingEdge.type)) {\n return incomingEdge.type;\n }\n }\n\n // Return validated default\n if (!isEdgeType(defaultEdgeType)) {\n return \"CALLS\"; // Fallback to safe default\n }\n return defaultEdgeType;\n}\n"],
5
+ "mappings": "AAIA,SAAS,YAAY,gBAAgB;AAWrC,SAAS,WAAW,OAAc;AAChC,SAAO,UAAU,WAAW,UAAU,gBAAgB,UAAU;AAClE;AAKM,SAAU,WAAW,OAAyB;AAClD,SAAO,KAAK,IAAI,KAAK,IAAI,SAAS,GAAG,EAAE,GAAG,CAAC;AAC7C;AAKM,SAAU,mBACd,cACA,aAAoB;AAGpB,MAAI,aAAa,aAAa,aAAa,aAAa,eAAe;AACvE,MAAI,cAAc;AAChB,QAAI;AAEF,UAAI,OAAO,cAAe,YAAY,OAAO,eAAgB;AAC3D,cAAM,IAAI,UACR,kCAAkC,OAAO,UAAU,iBAAiB,OAAO,WAAW,EAAE;AAI5F,mBAAa,WAAW,UAAU,IAAI,SAAS,aAAa,UAAU,IAAI;IAC5E,SAAS,OAAO;AAEd,YAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC1E,cAAQ,KACN,iDAAiD,UAAU,MAAM,YAAY,EAAE;IAEnF;AAGF,SAAO;IACL,gBAAgB,aAAa,kBAAkB,aAAa;IAC5D,aAAa;IACb,eAAe,aAAa,iBAAiB,aAAa,MAAM;IAChE,OAAO,OAAO,aAAa,SAAU,WAAW,aAAa,QAAQ;IACrE,WAAW;;AAEf;AAKM,SAAU,kBACd,WACA,OACA,yBACA,iBAA2B,CAAC,SAAS,cAAc,SAAS,GAAC;AAE7D,QAAM,kBAAkB,eAAe,CAAC,KAAK;AAG7C,MAAI,cAAc;AAEhB,WAAK,WAAW,eAAe,IAGxB,kBAFE;AAMX,QAAM,eAAe,MAAM,KACzB,CAAC,SAAS,KAAK,YAAY,uBAAuB;AAEpD,SAAI,gBAAgB,aAAa,QAAQ,eAAe,SAAS,aAAa,IAAI,KAE5E,WAAW,aAAa,IAAI,IACvB,aAAa,OAKnB,WAAW,eAAe,IAGxB,kBAFE;AAGX;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,191 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ const CARD_VERSION = 1, CARD_LABELS = Object.freeze(["Class", "Interface", "Function", "Method", "Module", "Route"]), CARD_FIELDS = Object.freeze({
5
+ Class: ["docstring", "base_classes", "is_exported", "is_entry_point", "complexity", "start_line", "end_line"],
6
+ Interface: ["docstring", "base_classes", "is_exported", "start_line", "end_line"],
7
+ Function: ["signature", "docstring", "is_exported", "is_entry_point", "complexity", "return_type", "start_line", "end_line"],
8
+ Method: ["signature", "docstring", "parent_class", "is_exported", "is_entry_point", "complexity", "return_type", "start_line", "end_line"],
9
+ Module: ["is_entry_point", "is_exported", "lines", "start_line", "end_line"],
10
+ Route: ["method", "source"],
11
+ Type: ["docstring", "is_exported", "start_line", "end_line"],
12
+ Enum: ["docstring", "is_exported", "start_line", "end_line"],
13
+ Trait: ["docstring", "is_exported", "start_line", "end_line"],
14
+ Struct: ["docstring", "is_exported", "start_line", "end_line"]
15
+ }), SOURCE_EXTENSIONS = Object.freeze([
16
+ "ts",
17
+ "tsx",
18
+ "js",
19
+ "jsx",
20
+ "mjs",
21
+ "cjs",
22
+ "py",
23
+ "go",
24
+ "java",
25
+ "kt",
26
+ "rs",
27
+ "cs",
28
+ "php",
29
+ "rb",
30
+ "swift",
31
+ "scala",
32
+ "vue",
33
+ "svelte"
34
+ ]), SOURCE_EXT = new RegExp(`\\.(${SOURCE_EXTENSIONS.join("|")})$`, "i"), VENDOR_EXCLUDE = /(^|\/)(dist|build|node_modules|vendor|\.venv|venv|__pycache__)(\/|$)/, SAMPLE_EXCLUDE = /(^|\/)(tests?|__tests__|fixtures?|__fixtures__|test-[^/]*|examples?|scripts|dist|node_modules|\.venv|venv)(\/|$)|\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)test_[^/]*\.py$|(^|\/)conftest\.py$/;
35
+ function cardId(scope, qualifiedName) {
36
+ return createHash("sha1").update(`${scope}:${qualifiedName}`).digest("hex").slice(0, 12);
37
+ }
38
+ function isCandidate(node, label, exclude = VENDOR_EXCLUDE) {
39
+ return label === "Route" ? !/^__route__[A-Z]+__\//.test(node.name) || /\.(ts|js|py|json)$/.test(node.name) ? !1 : !(node.file_path && exclude.test(node.file_path)) : !(!node.file_path || !SOURCE_EXT.test(node.file_path) || node.file_path.startsWith("<") || exclude.test(node.file_path) || label === "Method" && /^(constructor|__init__|__str__|__repr__)$/.test(node.name) || label === "Function" && /^(anonymous|arrow|lambda|<anonymous>)$/i.test(node.name));
40
+ }
41
+ function lineRange(node) {
42
+ const s = node.start_line != null ? Number(node.start_line) : null, e = node.end_line != null ? Number(node.end_line) : null;
43
+ if (s != null && !Number.isNaN(s))
44
+ return { start: s, end: e != null && !Number.isNaN(e) ? e : null };
45
+ const m = /^(\d+)-(\d+)$/.exec(String(node.lines ?? ""));
46
+ return m ? { start: Number(m[1]), end: Number(m[2]) } : { start: null, end: null };
47
+ }
48
+ function strOrNull(v) {
49
+ return typeof v == "string" && v.trim() ? v : null;
50
+ }
51
+ function boolOrNull(v) {
52
+ return typeof v == "boolean" ? v : null;
53
+ }
54
+ function numOrNull(v) {
55
+ return typeof v == "number" ? v : null;
56
+ }
57
+ function strArray(v) {
58
+ return Array.isArray(v) ? v.filter((x) => typeof x == "string") : typeof v == "string" && v.trim() ? v.split(",").map((s) => s.trim()).filter(Boolean) : [];
59
+ }
60
+ class CardBuilder {
61
+ graph;
62
+ edges;
63
+ options;
64
+ sourceLines;
65
+ handlerCache = /* @__PURE__ */ new Map();
66
+ fileLines = /* @__PURE__ */ new Map();
67
+ root;
68
+ constructor(graph, edges, options) {
69
+ this.graph = graph, this.edges = edges, this.options = options, this.sourceLines = options.sourceLines ?? 120, this.root = options.sourceRoot ? path.resolve(options.sourceRoot) : null;
70
+ }
71
+ /** A file's lines, read once; null when it is outside the root or unreadable. */
72
+ lines(file) {
73
+ let cached = this.fileLines.get(file);
74
+ if (!cached) {
75
+ const abs = path.resolve(this.root, file);
76
+ cached = abs === this.root || !abs.startsWith(this.root + path.sep) ? Promise.resolve(null) : readFile(abs, "utf-8").then((text) => text.split(`
77
+ `), () => null), this.fileLines.set(file, cached);
78
+ }
79
+ return cached;
80
+ }
81
+ /**
82
+ * A symbol's source: its line range read from disk when the builder has a root and the node
83
+ * has lines in a readable file, else CBM's snippet.
84
+ */
85
+ async sourceOf(qn, node) {
86
+ if (this.root && node && typeof node.file_path == "string" && node.file_path) {
87
+ const { start, end } = lineRange(node);
88
+ if (start !== null && end !== null && start >= 1 && end >= start) {
89
+ const lines = await this.lines(node.file_path);
90
+ if (lines && end <= lines.length)
91
+ return lines.slice(start - 1, end).join(`
92
+ `);
93
+ }
94
+ }
95
+ return (await this.graph.snippet(qn))?.source ?? null;
96
+ }
97
+ /** Strip the project prefix from a qualified name. */
98
+ shortQn(qn) {
99
+ const p = this.graph.project;
100
+ return qn.startsWith(p + ".") ? qn.slice(p.length + 1) : qn;
101
+ }
102
+ /**
103
+ * A route's handler when no HANDLES edge names one: the function, in a file with an edge into
104
+ * the route, whose source quotes the route's path (or calls the route's method with it).
105
+ */
106
+ async resolveRouteHandler(routeName) {
107
+ const pathPart = routeName.replace(/^__route__[A-Z]+__/, "").replace(/\{\}.*$/, ""), method = (/^__route__([A-Z]+)__/.exec(routeName)?.[1] ?? "").toLowerCase(), files = /* @__PURE__ */ new Set();
108
+ for (const qn of this.edges.routeCallers.get(routeName) ?? []) {
109
+ const f = this.edges.qnFile.get(qn);
110
+ f && files.add(f);
111
+ }
112
+ for (const file of files) {
113
+ let fns = this.handlerCache.get(file);
114
+ if (!fns) {
115
+ fns = [];
116
+ for (const n of await this.graph.search("Function", file, ["start_line"])) {
117
+ const source = await this.sourceOf(n.qualified_name, n);
118
+ source && fns.push({ qn: n.qualified_name, source });
119
+ }
120
+ this.handlerCache.set(file, fns);
121
+ }
122
+ const hit = fns.find((f) => f.source.includes(`"${pathPart}`) || f.source.includes(`'${pathPart}`)) ?? fns.find((f) => method && f.source.includes(`.${method}(`) && f.source.includes(pathPart));
123
+ if (hit)
124
+ return hit.qn;
125
+ }
126
+ return null;
127
+ }
128
+ cut(source) {
129
+ const all = source.split(`
130
+ `);
131
+ return all.length > this.sourceLines ? { source: all.slice(0, this.sourceLines).join(`
132
+ `), total: all.length, truncated: !0 } : { source, total: all.length, truncated: !1 };
133
+ }
134
+ async build(node) {
135
+ const { edges } = this, qn = node.qualified_name, short = (s) => this.shortQn(s), { start, end } = lineRange(node);
136
+ let source = null, totalLines = null, truncated = !1, routeHandlerQn = null;
137
+ if (node.label !== "Route") {
138
+ const text = await this.sourceOf(qn, node);
139
+ text && ({ source, total: totalLines, truncated } = this.cut(text));
140
+ } else if (routeHandlerQn = (edges.routeHandlers.get(qn) ?? []).filter((h) => edges.qnFile.has(h))[0] ?? await this.resolveRouteHandler(qn), routeHandlerQn) {
141
+ const text = await this.sourceOf(routeHandlerQn, null);
142
+ text && ({ source, total: totalLines, truncated } = this.cut(text));
143
+ }
144
+ const callers = (edges.callers.get(qn) ?? []).map(short), callees = (edges.callees.get(qn) ?? []).map(short);
145
+ return {
146
+ // Hashed on the full qualified name, as the labelled card sets were.
147
+ id: cardId(this.options.scope, qn),
148
+ label: node.label,
149
+ name: node.name,
150
+ qualified_name: short(qn),
151
+ file_path: node.file_path || (routeHandlerQn ? edges.qnFile.get(routeHandlerQn) ?? null : null),
152
+ start_line: start,
153
+ end_line: end,
154
+ signature: strOrNull(node.signature),
155
+ docstring: strOrNull(node.docstring),
156
+ is_exported: boolOrNull(node.is_exported),
157
+ is_entry_point: boolOrNull(node.is_entry_point),
158
+ complexity: numOrNull(node.complexity),
159
+ parent_class: node.parent_class ? short(String(node.parent_class)) : null,
160
+ base_classes: strArray(node.base_classes),
161
+ return_type: strOrNull(node.return_type),
162
+ callers: (node.label === "Route" && routeHandlerQn ? [short(routeHandlerQn), ...callers] : callers).slice(0, 12),
163
+ callees: callees.slice(0, 12),
164
+ callers_total: callers.length,
165
+ callees_total: callees.length,
166
+ imports: (node.file_path ? edges.importsByFile.get(node.file_path) ?? [] : []).slice(0, 15),
167
+ env_vars: edges.envVars.get(qn) ?? [],
168
+ routes_handled: node.label === "Route" ? [node.name.replace(/^__route__[A-Z]+__/, "")] : edges.handles.get(qn) ?? [],
169
+ http_calls: (edges.httpCalls.get(qn) ?? []).slice(0, 8),
170
+ inherits: (edges.inherits.get(qn) ?? []).map(short),
171
+ implements: (edges.implements.get(qn) ?? []).map(short),
172
+ route_method: node.label === "Route" ? strOrNull(node.method) : null,
173
+ source,
174
+ source_lines_total: totalLines,
175
+ source_truncated: truncated
176
+ };
177
+ }
178
+ }
179
+ export {
180
+ CARD_FIELDS,
181
+ CARD_LABELS,
182
+ CARD_VERSION,
183
+ CardBuilder,
184
+ SAMPLE_EXCLUDE,
185
+ SOURCE_EXT,
186
+ SOURCE_EXTENSIONS,
187
+ VENDOR_EXCLUDE,
188
+ cardId,
189
+ isCandidate
190
+ };
191
+ //# sourceMappingURL=build.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/analyzers/cards/build.ts"],
4
+ "sourcesContent": ["/**\n * Build symbol cards from a CBM project.\n *\n * The classify-eval harness (cli/scripts/classify-eval/) builds its labelled card sets with this\n * module, so its accuracy numbers measure the code `dr analyzer classify` runs.\n */\nimport { createHash } from \"node:crypto\";\nimport { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { EdgeIndex } from \"./edge-index.js\";\nimport type { CardGraph, GraphNode, SymbolCard } from \"./types.js\";\n\n/**\n * Version of the card's fields and how they are derived. Bump it when a change would make a card\n * built now differ from one built before for the same graph, so stored cards and their\n * classifications can be told apart.\n */\nexport const CARD_VERSION = 1;\n\n/** CBM labels that become cards in the harness's card sets (classify adds the rules' labels). */\nexport const CARD_LABELS: readonly string[] = Object.freeze([\"Class\", \"Interface\", \"Function\", \"Method\", \"Module\", \"Route\"]);\n\n/** Extra search_graph fields each label's card reads. */\nexport const CARD_FIELDS: Readonly<Record<string, string[]>> = Object.freeze({\n Class: [\"docstring\", \"base_classes\", \"is_exported\", \"is_entry_point\", \"complexity\", \"start_line\", \"end_line\"],\n Interface: [\"docstring\", \"base_classes\", \"is_exported\", \"start_line\", \"end_line\"],\n Function: [\"signature\", \"docstring\", \"is_exported\", \"is_entry_point\", \"complexity\", \"return_type\", \"start_line\", \"end_line\"],\n Method: [\"signature\", \"docstring\", \"parent_class\", \"is_exported\", \"is_entry_point\", \"complexity\", \"return_type\", \"start_line\", \"end_line\"],\n Module: [\"is_entry_point\", \"is_exported\", \"lines\", \"start_line\", \"end_line\"],\n Route: [\"method\", \"source\"],\n Type: [\"docstring\", \"is_exported\", \"start_line\", \"end_line\"],\n Enum: [\"docstring\", \"is_exported\", \"start_line\", \"end_line\"],\n Trait: [\"docstring\", \"is_exported\", \"start_line\", \"end_line\"],\n Struct: [\"docstring\", \"is_exported\", \"start_line\", \"end_line\"],\n});\n\n/** Source file extensions: files whose symbols become cards. */\nexport const SOURCE_EXTENSIONS: readonly string[] = Object.freeze([\n \"ts\", \"tsx\", \"js\", \"jsx\", \"mjs\", \"cjs\", \"py\", \"go\", \"java\", \"kt\", \"rs\", \"cs\", \"php\", \"rb\", \"swift\", \"scala\", \"vue\", \"svelte\",\n]);\n\nexport const SOURCE_EXT = new RegExp(`\\\\.(${SOURCE_EXTENSIONS.join(\"|\")})$`, \"i\");\n\n/** Build output and vendored dependencies: never cards. The default for `dr analyzer classify`. */\nexport const VENDOR_EXCLUDE = /(^|\\/)(dist|build|node_modules|vendor|\\.venv|venv|__pycache__)(\\/|$)/;\n\n/**\n * The harness's sampling exclusion: vendor paths plus tests, fixtures, examples and scripts, so\n * labelled card sets are about application code. Tests are not excluded in production, where the\n * Tier-0 test rules file them in the testing layer.\n */\nexport const SAMPLE_EXCLUDE =\n /(^|\\/)(tests?|__tests__|fixtures?|__fixtures__|test-[^/]*|examples?|scripts|dist|node_modules|\\.venv|venv)(\\/|$)|\\.(test|spec)\\.[cm]?[jt]sx?$|(^|\\/)test_[^/]*\\.py$|(^|\\/)conftest\\.py$/;\n\n/** Stable card id for a symbol (full, project-prefixed qualified name) within a scope. */\nexport function cardId(scope: string, qualifiedName: string): string {\n return createHash(\"sha1\").update(`${scope}:${qualifiedName}`).digest(\"hex\").slice(0, 12);\n}\n\n/**\n * Whether a node of this label becomes a card: a route with a path, or a symbol in a source file\n * outside `exclude`, leaving out constructors, dunder methods and anonymous functions.\n */\nexport function isCandidate(node: GraphNode, label: string, exclude: RegExp = VENDOR_EXCLUDE): boolean {\n if (label === \"Route\") {\n if (!/^__route__[A-Z]+__\\//.test(node.name) || /\\.(ts|js|py|json)$/.test(node.name)) return false;\n return !(node.file_path && exclude.test(node.file_path));\n }\n if (!node.file_path) return false;\n if (!SOURCE_EXT.test(node.file_path) || node.file_path.startsWith(\"<\")) return false;\n if (exclude.test(node.file_path)) return false;\n if (label === \"Method\" && /^(constructor|__init__|__str__|__repr__)$/.test(node.name)) return false;\n if (label === \"Function\" && /^(anonymous|arrow|lambda|<anonymous>)$/i.test(node.name)) return false;\n return true;\n}\n\nfunction lineRange(node: GraphNode): { start: number | null; end: number | null } {\n const s = node.start_line != null ? Number(node.start_line) : null;\n const e = node.end_line != null ? Number(node.end_line) : null;\n if (s != null && !Number.isNaN(s)) return { start: s, end: e != null && !Number.isNaN(e) ? e : null };\n const m = /^(\\d+)-(\\d+)$/.exec(String(node.lines ?? \"\"));\n return m ? { start: Number(m[1]), end: Number(m[2]) } : { start: null, end: null };\n}\n\nfunction strOrNull(v: unknown): string | null {\n return typeof v === \"string\" && v.trim() ? v : null;\n}\nfunction boolOrNull(v: unknown): boolean | null {\n return typeof v === \"boolean\" ? v : null;\n}\nfunction numOrNull(v: unknown): number | null {\n return typeof v === \"number\" ? v : null;\n}\nfunction strArray(v: unknown): string[] {\n if (Array.isArray(v)) return v.filter((x): x is string => typeof x === \"string\");\n if (typeof v === \"string\" && v.trim()) return v.split(\",\").map((s) => s.trim()).filter(Boolean);\n return [];\n}\n\nexport interface CardBuilderOptions {\n /** Scope for card ids: a corpus name in the harness, the CBM project in production. */\n scope: string;\n /** Source lines kept per card (default 120). */\n sourceLines?: number;\n /**\n * Absolute path of the indexed codebase. When set, a symbol's source is read from its file by\n * the node's line range, one read per file; CBM's `get_code_snippet` (full source, at most 500\n * lines) is the fallback. Without it every card costs a snippet call, about 15 ms each.\n */\n sourceRoot?: string;\n}\n\n/**\n * Builds cards for one project. Holds the per-file handler cache that route-handler resolution\n * fills, so build every card of a run with the same builder.\n */\nexport class CardBuilder {\n private readonly sourceLines: number;\n private readonly handlerCache = new Map<string, Array<{ qn: string; source: string }>>();\n private readonly fileLines = new Map<string, Promise<string[] | null>>();\n private readonly root: string | null;\n\n constructor(\n private readonly graph: CardGraph,\n private readonly edges: EdgeIndex,\n private readonly options: CardBuilderOptions\n ) {\n this.sourceLines = options.sourceLines ?? 120;\n this.root = options.sourceRoot ? path.resolve(options.sourceRoot) : null;\n }\n\n /** A file's lines, read once; null when it is outside the root or unreadable. */\n private lines(file: string): Promise<string[] | null> {\n let cached = this.fileLines.get(file);\n if (!cached) {\n const abs = path.resolve(this.root!, file);\n cached =\n abs === this.root || !abs.startsWith(this.root! + path.sep)\n ? Promise.resolve(null)\n : readFile(abs, \"utf-8\").then((text) => text.split(\"\\n\"), () => null);\n this.fileLines.set(file, cached);\n }\n return cached;\n }\n\n /**\n * A symbol's source: its line range read from disk when the builder has a root and the node\n * has lines in a readable file, else CBM's snippet.\n */\n private async sourceOf(qn: string, node: { file_path?: unknown; start_line?: unknown; end_line?: unknown; lines?: unknown } | null): Promise<string | null> {\n if (this.root && node && typeof node.file_path === \"string\" && node.file_path) {\n const { start, end } = lineRange(node as GraphNode);\n if (start !== null && end !== null && start >= 1 && end >= start) {\n const lines = await this.lines(node.file_path);\n if (lines && end <= lines.length) return lines.slice(start - 1, end).join(\"\\n\");\n }\n }\n return (await this.graph.snippet(qn))?.source ?? null;\n }\n\n /** Strip the project prefix from a qualified name. */\n shortQn(qn: string): string {\n const p = this.graph.project;\n return qn.startsWith(p + \".\") ? qn.slice(p.length + 1) : qn;\n }\n\n /**\n * A route's handler when no HANDLES edge names one: the function, in a file with an edge into\n * the route, whose source quotes the route's path (or calls the route's method with it).\n */\n private async resolveRouteHandler(routeName: string): Promise<string | null> {\n const pathPart = routeName.replace(/^__route__[A-Z]+__/, \"\").replace(/\\{\\}.*$/, \"\");\n const method = (/^__route__([A-Z]+)__/.exec(routeName)?.[1] ?? \"\").toLowerCase();\n const files = new Set<string>();\n for (const qn of this.edges.routeCallers.get(routeName) ?? []) {\n const f = this.edges.qnFile.get(qn);\n if (f) files.add(f);\n }\n for (const file of files) {\n let fns = this.handlerCache.get(file);\n if (!fns) {\n fns = [];\n for (const n of await this.graph.search(\"Function\", file, [\"start_line\"])) {\n const source = await this.sourceOf(n.qualified_name, n);\n if (source) fns.push({ qn: n.qualified_name, source });\n }\n this.handlerCache.set(file, fns);\n }\n const hit =\n fns.find((f) => f.source.includes(`\"${pathPart}`) || f.source.includes(`'${pathPart}`)) ??\n fns.find((f) => method && f.source.includes(`.${method}(`) && f.source.includes(pathPart));\n if (hit) return hit.qn;\n }\n return null;\n }\n\n private cut(source: string): { source: string; total: number; truncated: boolean } {\n const all = source.split(\"\\n\");\n return all.length > this.sourceLines\n ? { source: all.slice(0, this.sourceLines).join(\"\\n\"), total: all.length, truncated: true }\n : { source, total: all.length, truncated: false };\n }\n\n async build(node: GraphNode): Promise<SymbolCard> {\n const { edges } = this;\n const qn = node.qualified_name;\n const short = (s: string) => this.shortQn(s);\n const { start, end } = lineRange(node);\n let source: string | null = null;\n let totalLines: number | null = null;\n let truncated = false;\n let routeHandlerQn: string | null = null;\n if (node.label !== \"Route\") {\n const text = await this.sourceOf(qn, node);\n if (text) ({ source, total: totalLines, truncated } = this.cut(text));\n } else {\n // A Route borrows its handler's source: by HANDLES edge, else by resolveRouteHandler.\n const direct = (edges.routeHandlers.get(qn) ?? []).filter((h) => edges.qnFile.has(h));\n routeHandlerQn = direct[0] ?? (await this.resolveRouteHandler(qn));\n if (routeHandlerQn) {\n const text = await this.sourceOf(routeHandlerQn, null);\n if (text) ({ source, total: totalLines, truncated } = this.cut(text));\n }\n }\n const callers = (edges.callers.get(qn) ?? []).map(short);\n const callees = (edges.callees.get(qn) ?? []).map(short);\n return {\n // Hashed on the full qualified name, as the labelled card sets were.\n id: cardId(this.options.scope, qn),\n label: node.label,\n name: node.name,\n qualified_name: short(qn),\n file_path: node.file_path || (routeHandlerQn ? edges.qnFile.get(routeHandlerQn) ?? null : null),\n start_line: start,\n end_line: end,\n signature: strOrNull(node.signature),\n docstring: strOrNull(node.docstring),\n is_exported: boolOrNull(node.is_exported),\n is_entry_point: boolOrNull(node.is_entry_point),\n complexity: numOrNull(node.complexity),\n parent_class: node.parent_class ? short(String(node.parent_class)) : null,\n base_classes: strArray(node.base_classes),\n return_type: strOrNull(node.return_type),\n callers: (node.label === \"Route\" && routeHandlerQn ? [short(routeHandlerQn), ...callers] : callers).slice(0, 12),\n callees: callees.slice(0, 12),\n callers_total: callers.length,\n callees_total: callees.length,\n imports: (node.file_path ? edges.importsByFile.get(node.file_path) ?? [] : []).slice(0, 15),\n env_vars: edges.envVars.get(qn) ?? [],\n routes_handled: node.label === \"Route\" ? [node.name.replace(/^__route__[A-Z]+__/, \"\")] : edges.handles.get(qn) ?? [],\n http_calls: (edges.httpCalls.get(qn) ?? []).slice(0, 8),\n inherits: (edges.inherits.get(qn) ?? []).map(short),\n implements: (edges.implements.get(qn) ?? []).map(short),\n route_method: node.label === \"Route\" ? strOrNull(node.method) : null,\n source,\n source_lines_total: totalLines,\n source_truncated: truncated,\n };\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,42 @@
1
+ import { CARD_FIELDS, CARD_LABELS, CARD_VERSION, CardBuilder, VENDOR_EXCLUDE, isCandidate } from "./build.js";
2
+ import { buildEdgeIndex } from "./edge-index.js";
3
+ import { buildFactsIndex, factsFor } from "./facts.js";
4
+ import { tier0ForCard } from "./tier0.js";
5
+ function defaultLabels(rules) {
6
+ return [.../* @__PURE__ */ new Set([...CARD_LABELS, ...rules.flatMap((r) => r.labels)])];
7
+ }
8
+ async function classifyTier0(graph, rules, analyzer, options = {}) {
9
+ const labels = options.labels?.length ? options.labels : defaultLabels(rules), exclude = options.exclude ? new RegExp(`(?:${VENDOR_EXCLUDE.source})|(?:${options.exclude.source})`, options.exclude.flags.replace("g", "")) : VENDOR_EXCLUDE, nodes = [], seen = /* @__PURE__ */ new Set();
10
+ for (const label of labels)
11
+ for (const node of await graph.search(label, options.filePattern, CARD_FIELDS[label] ?? []))
12
+ !isCandidate(node, label, exclude) || seen.has(node.qualified_name) || (seen.add(node.qualified_name), nodes.push(node));
13
+ const selected = options.limit !== void 0 ? nodes.slice(0, options.limit) : nodes, edges = await buildEdgeIndex(graph), factsIndex = await buildFactsIndex(graph), builder = new CardBuilder(graph, edges, { scope: graph.project, sourceRoot: options.sourceRoot }), results = [], summary = { cards: 0, decided: 0, rejected: 0, narrowed: 0, undecided: 0, no_source: 0, by_rule: {}, by_target: {} };
14
+ for (const node of selected) {
15
+ const card = await builder.build(node), facts = factsFor(factsIndex, node.qualified_name, card.file_path), tier0 = tier0ForCard(card, facts, rules);
16
+ if (summary.cards++, card.source === null && card.label !== "Route" && summary.no_source++, !tier0)
17
+ summary.undecided++;
18
+ else if (summary.by_rule[tier0.rule] = (summary.by_rule[tier0.rule] ?? 0) + 1, tier0.kind === "decide") {
19
+ summary.decided++;
20
+ const target = `${tier0.layer}.${tier0.type}`;
21
+ summary.by_target[target] = (summary.by_target[target] ?? 0) + 1;
22
+ } else tier0.kind === "reject" ? summary.rejected++ : summary.narrowed++;
23
+ results.push({
24
+ card_id: card.id,
25
+ label: card.label,
26
+ name: card.name,
27
+ qualified_name: card.qualified_name,
28
+ file_path: card.file_path,
29
+ start_line: card.start_line,
30
+ end_line: card.end_line,
31
+ tier0,
32
+ facts,
33
+ ...options.includeCards ? { card } : {}
34
+ }), options.onProgress?.(results.length, selected.length);
35
+ }
36
+ return { analyzer, project: graph.project, mode: "tier0", card_version: CARD_VERSION, rules: rules.length, summary, results };
37
+ }
38
+ export {
39
+ classifyTier0,
40
+ defaultLabels
41
+ };
42
+ //# sourceMappingURL=classify.js.map