@documentation-robotics/cli 0.1.0 → 0.1.7
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.
- package/LICENSE +21 -0
- package/README.md +466 -693
- package/dist/analysis/data-model-analyzer.js +189 -0
- package/dist/analysis/data-model-analyzer.js.map +7 -0
- package/dist/analysis/index.js +3 -0
- package/dist/analysis/index.js.map +7 -0
- package/dist/analysis/relationship-classifier.js +181 -0
- package/dist/analysis/relationship-classifier.js.map +7 -0
- package/dist/analyzers/base-analyzer.js +1 -0
- package/dist/analyzers/base-analyzer.js.map +7 -0
- package/dist/analyzers/call-graph-utils.js +39 -0
- package/dist/analyzers/call-graph-utils.js.map +7 -0
- package/dist/analyzers/cbm-analyzer.js +1205 -0
- package/dist/analyzers/cbm-analyzer.js.map +7 -0
- package/dist/analyzers/heuristic-utils.js +55 -0
- package/dist/analyzers/heuristic-utils.js.map +7 -0
- package/dist/analyzers/mapping-loader.js +146 -0
- package/dist/analyzers/mapping-loader.js.map +7 -0
- package/dist/analyzers/registry.js +124 -0
- package/dist/analyzers/registry.js.map +7 -0
- package/dist/analyzers/session-state.js +176 -0
- package/dist/analyzers/session-state.js.map +7 -0
- package/dist/analyzers/stdio-client.js +208 -0
- package/dist/analyzers/stdio-client.js.map +7 -0
- package/dist/analyzers/types.js +15 -0
- package/dist/analyzers/types.js.map +7 -0
- package/dist/analyzers/verify-engine.js +209 -0
- package/dist/analyzers/verify-engine.js.map +7 -0
- package/dist/analyzers/verify-ignore.js +130 -0
- package/dist/analyzers/verify-ignore.js.map +7 -0
- package/dist/analyzers/version.js +8 -0
- package/dist/analyzers/version.js.map +7 -0
- package/dist/audit/ai/runner.js +41 -0
- package/dist/audit/ai/runner.js.map +7 -0
- package/dist/audit/differential-analyzer.js +183 -0
- package/dist/audit/differential-analyzer.js.map +7 -0
- package/dist/audit/index.js +32 -0
- package/dist/audit/index.js.map +7 -0
- package/dist/audit/nodes/ai/evaluator.js +49 -0
- package/dist/audit/nodes/ai/evaluator.js.map +7 -0
- package/dist/audit/nodes/ai/parser.js +55 -0
- package/dist/audit/nodes/ai/parser.js.map +7 -0
- package/dist/audit/nodes/ai/prompts.js +103 -0
- package/dist/audit/nodes/ai/prompts.js.map +7 -0
- package/dist/audit/nodes/analysis/node-completeness-checker.js +32 -0
- package/dist/audit/nodes/analysis/node-completeness-checker.js.map +7 -0
- package/dist/audit/nodes/analysis/node-definition-analyzer.js +47 -0
- package/dist/audit/nodes/analysis/node-definition-analyzer.js.map +7 -0
- package/dist/audit/nodes/analysis/node-overlap-detector.js +79 -0
- package/dist/audit/nodes/analysis/node-overlap-detector.js.map +7 -0
- package/dist/audit/nodes/model/orchestrator.js +82 -0
- package/dist/audit/nodes/model/orchestrator.js.map +7 -0
- package/dist/audit/nodes/spec/loader.js +116 -0
- package/dist/audit/nodes/spec/loader.js.map +7 -0
- package/dist/audit/nodes/spec/orchestrator.js +68 -0
- package/dist/audit/nodes/spec/orchestrator.js.map +7 -0
- package/dist/audit/nodes/types.js +1 -0
- package/dist/audit/nodes/types.js.map +7 -0
- package/dist/audit/pipeline/index.js +5 -0
- package/dist/audit/pipeline/index.js.map +7 -0
- package/dist/audit/pipeline/pipeline-orchestrator.js +218 -0
- package/dist/audit/pipeline/pipeline-orchestrator.js.map +7 -0
- package/dist/audit/relationships/ai/evaluator.js +235 -0
- package/dist/audit/relationships/ai/evaluator.js.map +7 -0
- package/dist/audit/relationships/ai/parser.js +197 -0
- package/dist/audit/relationships/ai/parser.js.map +7 -0
- package/dist/audit/relationships/ai/prompts.js +274 -0
- package/dist/audit/relationships/ai/prompts.js.map +7 -0
- package/dist/audit/relationships/analysis/balance-assessor.js +115 -0
- package/dist/audit/relationships/analysis/balance-assessor.js.map +7 -0
- package/dist/audit/relationships/analysis/coverage-analyzer.js +99 -0
- package/dist/audit/relationships/analysis/coverage-analyzer.js.map +7 -0
- package/dist/audit/relationships/analysis/duplicate-detector.js +95 -0
- package/dist/audit/relationships/analysis/duplicate-detector.js.map +7 -0
- package/dist/audit/relationships/analysis/gap-analyzer.js +99 -0
- package/dist/audit/relationships/analysis/gap-analyzer.js.map +7 -0
- package/dist/audit/relationships/analysis/layer-templates.js +294 -0
- package/dist/audit/relationships/analysis/layer-templates.js.map +7 -0
- package/dist/audit/relationships/graph/connectivity.js +129 -0
- package/dist/audit/relationships/graph/connectivity.js.map +7 -0
- package/dist/audit/relationships/graph/relationship-graph.js +179 -0
- package/dist/audit/relationships/graph/relationship-graph.js.map +7 -0
- package/dist/audit/relationships/model/balance-assessor.js +49 -0
- package/dist/audit/relationships/model/balance-assessor.js.map +7 -0
- package/dist/audit/relationships/model/coverage-analyzer.js +34 -0
- package/dist/audit/relationships/model/coverage-analyzer.js.map +7 -0
- package/dist/audit/relationships/model/duplicate-detector.js +68 -0
- package/dist/audit/relationships/model/duplicate-detector.js.map +7 -0
- package/dist/audit/relationships/model/gap-analyzer.js +41 -0
- package/dist/audit/relationships/model/gap-analyzer.js.map +7 -0
- package/dist/audit/relationships/model/orchestrator.js +126 -0
- package/dist/audit/relationships/model/orchestrator.js.map +7 -0
- package/dist/audit/relationships/spec/orchestrator.js +157 -0
- package/dist/audit/relationships/spec/orchestrator.js.map +7 -0
- package/dist/audit/reports/index.js +5 -0
- package/dist/audit/reports/index.js.map +7 -0
- package/dist/audit/reports/report-generator.js +118 -0
- package/dist/audit/reports/report-generator.js.map +7 -0
- package/dist/audit/snapshot-storage.js +163 -0
- package/dist/audit/snapshot-storage.js.map +7 -0
- package/dist/audit/types.js +21 -0
- package/dist/audit/types.js.map +7 -0
- package/dist/cli.js +223 -92
- package/dist/cli.js.map +3 -3
- package/dist/coding-agents/agents/claude-code.js +147 -0
- package/dist/coding-agents/agents/claude-code.js.map +7 -0
- package/dist/coding-agents/agents/index.js +5 -0
- package/dist/coding-agents/agents/index.js.map +7 -0
- package/dist/coding-agents/agents/types.js +1 -0
- package/dist/coding-agents/agents/types.js.map +7 -0
- package/dist/coding-agents/base-chat-client.js +45 -0
- package/dist/coding-agents/base-chat-client.js.map +7 -0
- package/dist/coding-agents/chat-utils.js +23 -0
- package/dist/coding-agents/chat-utils.js.map +7 -0
- package/dist/coding-agents/claude-code-client.js +246 -0
- package/dist/coding-agents/claude-code-client.js.map +7 -0
- package/dist/coding-agents/copilot-client.js +209 -0
- package/dist/coding-agents/copilot-client.js.map +7 -0
- package/dist/commands/add.js +95 -33
- package/dist/commands/add.js.map +2 -2
- package/dist/commands/analyzer.js +511 -0
- package/dist/commands/analyzer.js.map +7 -0
- package/dist/commands/audit-diff.js +49 -0
- package/dist/commands/audit-diff.js.map +7 -0
- package/dist/commands/audit-snapshots.js +62 -0
- package/dist/commands/audit-snapshots.js.map +7 -0
- package/dist/commands/audit.js +81 -0
- package/dist/commands/audit.js.map +7 -0
- package/dist/commands/catalog.js +248 -0
- package/dist/commands/catalog.js.map +7 -0
- package/dist/commands/changeset.js +577 -66
- package/dist/commands/changeset.js.map +2 -2
- package/dist/commands/chat.js +106 -83
- package/dist/commands/chat.js.map +2 -2
- package/dist/commands/claude.js +74 -0
- package/dist/commands/claude.js.map +7 -0
- package/dist/commands/conformance.js +71 -68
- package/dist/commands/conformance.js.map +2 -2
- package/dist/commands/copilot.js +75 -0
- package/dist/commands/copilot.js.map +7 -0
- package/dist/commands/delete.js +125 -11
- package/dist/commands/delete.js.map +3 -3
- package/dist/commands/discover-logic.js +44 -0
- package/dist/commands/discover-logic.js.map +7 -0
- package/dist/commands/docs.js +87 -0
- package/dist/commands/docs.js.map +7 -0
- package/dist/commands/element.js +14 -20
- package/dist/commands/element.js.map +2 -2
- package/dist/commands/export.js +13 -3
- package/dist/commands/export.js.map +2 -2
- package/dist/commands/import.js +51 -0
- package/dist/commands/import.js.map +7 -0
- package/dist/commands/info.js +30 -24
- package/dist/commands/info.js.map +2 -2
- package/dist/commands/init.js +27 -11
- package/dist/commands/init.js.map +2 -2
- package/dist/commands/list.js +63 -13
- package/dist/commands/list.js.map +3 -3
- package/dist/commands/relationship.js +225 -37
- package/dist/commands/relationship.js.map +2 -2
- package/dist/commands/report.js +54 -0
- package/dist/commands/report.js.map +7 -0
- package/dist/commands/reports.js +45 -0
- package/dist/commands/reports.js.map +7 -0
- package/dist/commands/schema.js +135 -0
- package/dist/commands/schema.js.map +7 -0
- package/dist/commands/search.js +40 -12
- package/dist/commands/search.js.map +2 -2
- package/dist/commands/show.js +58 -27
- package/dist/commands/show.js.map +2 -2
- package/dist/commands/stats.js +35 -0
- package/dist/commands/stats.js.map +7 -0
- package/dist/commands/trace.js +39 -16
- package/dist/commands/trace.js.map +2 -2
- package/dist/commands/update.js +141 -14
- package/dist/commands/update.js.map +2 -2
- package/dist/commands/upgrade.js +210 -46
- package/dist/commands/upgrade.js.map +3 -3
- package/dist/commands/validate.js +142 -23
- package/dist/commands/validate.js.map +3 -3
- package/dist/commands/version.js +8 -12
- package/dist/commands/version.js.map +2 -2
- package/dist/commands/visualize.js +162 -25
- package/dist/commands/visualize.js.map +2 -2
- package/dist/core/base-snapshot-manager.js +84 -0
- package/dist/core/base-snapshot-manager.js.map +7 -0
- package/dist/core/changeset-exporter.js +305 -0
- package/dist/core/changeset-exporter.js.map +7 -0
- package/dist/core/changeset-validator.js +41 -0
- package/dist/core/changeset-validator.js.map +7 -0
- package/dist/core/changeset.js +88 -173
- package/dist/core/changeset.js.map +2 -2
- package/dist/core/dependency-tracker.js +4 -2
- package/dist/core/dependency-tracker.js.map +2 -2
- package/dist/core/element.js +98 -27
- package/dist/core/element.js.map +2 -2
- package/dist/core/golden-copy-cache.js +290 -0
- package/dist/core/golden-copy-cache.js.map +7 -0
- package/dist/core/graph-model.js +266 -0
- package/dist/core/graph-model.js.map +7 -0
- package/dist/core/layer.js +159 -16
- package/dist/core/layer.js.map +2 -2
- package/dist/core/layers.js +32 -0
- package/dist/core/layers.js.map +7 -0
- package/dist/core/manifest.js +32 -11
- package/dist/core/manifest.js.map +2 -2
- package/dist/core/migration-registry.js +56 -1
- package/dist/core/migration-registry.js.map +2 -2
- package/dist/core/model.js +424 -217
- package/dist/core/model.js.map +3 -3
- package/dist/core/mutation-handler.js +179 -0
- package/dist/core/mutation-handler.js.map +7 -0
- package/dist/core/reference-registry.js +29 -21
- package/dist/core/reference-registry.js.map +2 -2
- package/dist/core/relationship-catalog.js +189 -0
- package/dist/core/relationship-catalog.js.map +7 -0
- package/dist/core/relationship-registry.js.map +2 -2
- package/dist/core/relationships.js.map +1 -1
- package/dist/core/report-data-model.js +285 -0
- package/dist/core/report-data-model.js.map +7 -0
- package/dist/core/spec-data-service.js +185 -0
- package/dist/core/spec-data-service.js.map +7 -0
- package/dist/core/spec-loader-types.js +1 -0
- package/dist/core/spec-loader-types.js.map +7 -0
- package/dist/core/spec-loader.js +234 -0
- package/dist/core/spec-loader.js.map +7 -0
- package/dist/core/staged-changeset-storage.js +282 -0
- package/dist/core/staged-changeset-storage.js.map +7 -0
- package/dist/core/staging-area.js +1002 -0
- package/dist/core/staging-area.js.map +7 -0
- package/dist/core/stats-collector.js +169 -0
- package/dist/core/stats-collector.js.map +7 -0
- package/dist/core/virtual-projection.js +443 -0
- package/dist/core/virtual-projection.js.map +7 -0
- package/dist/export/archimate-exporter.js +99 -60
- package/dist/export/archimate-exporter.js.map +2 -2
- package/dist/export/audit-formatters.js +479 -0
- package/dist/export/audit-formatters.js.map +7 -0
- package/dist/export/diff-formatters.js +99 -0
- package/dist/export/diff-formatters.js.map +7 -0
- package/dist/export/export-manager.js.map +2 -2
- package/dist/export/graphml-exporter.js +44 -27
- package/dist/export/graphml-exporter.js.map +2 -2
- package/dist/export/index.js +4 -0
- package/dist/export/index.js.map +2 -2
- package/dist/export/json-schema-exporter.js +55 -42
- package/dist/export/json-schema-exporter.js.map +2 -2
- package/dist/export/markdown-exporter.js +92 -70
- package/dist/export/markdown-exporter.js.map +2 -2
- package/dist/export/markdown-generator.js +248 -0
- package/dist/export/markdown-generator.js.map +7 -0
- package/dist/export/markdown-utils.js +30 -0
- package/dist/export/markdown-utils.js.map +7 -0
- package/dist/export/mermaid-markdown-exporter.js +37 -0
- package/dist/export/mermaid-markdown-exporter.js.map +7 -0
- package/dist/export/openapi-exporter.js +119 -71
- package/dist/export/openapi-exporter.js.map +2 -2
- package/dist/export/plantuml-exporter.js +104 -39
- package/dist/export/plantuml-exporter.js.map +2 -2
- package/dist/export/report-formatters.js +111 -0
- package/dist/export/report-formatters.js.map +7 -0
- package/dist/export/stats-formatters.js +98 -0
- package/dist/export/stats-formatters.js.map +7 -0
- package/dist/export/types.js +2 -14
- package/dist/export/types.js.map +2 -2
- package/dist/export/verify-formatters.js +153 -0
- package/dist/export/verify-formatters.js.map +7 -0
- package/dist/generated/compiled-validators.js +17 -0
- package/dist/generated/compiled-validators.js.map +7 -0
- package/dist/generated/index.js +31 -0
- package/dist/generated/index.js.map +7 -0
- package/dist/generated/layer-registry.js +190 -0
- package/dist/generated/layer-registry.js.map +7 -0
- package/dist/generated/layer-types.js +7 -0
- package/dist/generated/layer-types.js.map +7 -0
- package/dist/generated/node-types.js +1719 -0
- package/dist/generated/node-types.js.map +7 -0
- package/dist/generated/relationship-index.js +14552 -0
- package/dist/generated/relationship-index.js.map +7 -0
- package/dist/import/archimate-importer.js +166 -0
- package/dist/import/archimate-importer.js.map +7 -0
- package/dist/import/index.js +47 -0
- package/dist/import/index.js.map +7 -0
- package/dist/import/openapi-importer.js +143 -0
- package/dist/import/openapi-importer.js.map +7 -0
- package/dist/import/types.js +1 -0
- package/dist/import/types.js.map +7 -0
- package/dist/integrations/base-manager.js +311 -0
- package/dist/integrations/base-manager.js.map +7 -0
- package/dist/integrations/claude-manager.js +360 -0
- package/dist/integrations/claude-manager.js.map +7 -0
- package/dist/integrations/claude_code/agents/dr-advisor.md +253 -0
- package/dist/integrations/claude_code/agents/dr-architect.md +278 -0
- package/dist/integrations/claude_code/agents/dr-extractor.md +286 -0
- package/dist/integrations/claude_code/commands/dr-changeset.md +657 -0
- package/dist/integrations/claude_code/commands/dr-design.md +373 -0
- package/dist/integrations/claude_code/commands/dr-info.md +132 -0
- package/dist/integrations/claude_code/commands/dr-init.md +251 -0
- package/dist/integrations/claude_code/commands/dr-map.md +936 -0
- package/dist/integrations/claude_code/commands/dr-model.md +493 -0
- package/dist/integrations/claude_code/commands/dr-relate.md +814 -0
- package/dist/integrations/claude_code/commands/dr-sync.md +279 -0
- package/dist/integrations/claude_code/commands/dr-validate.md +654 -0
- package/dist/integrations/claude_code/commands/dr-verify.md +559 -0
- package/dist/integrations/claude_code/docs/END_TO_END_WALKTHROUGH.md +903 -0
- package/dist/integrations/claude_code/docs/QUICK_START.md +271 -0
- package/dist/integrations/claude_code/docs/README.md +347 -0
- package/dist/integrations/claude_code/skills/dr_01_motivation_layer/SKILL.md +550 -0
- package/dist/integrations/claude_code/skills/dr_02_business_layer/SKILL.md +620 -0
- package/dist/integrations/claude_code/skills/dr_03_security_layer/SKILL.md +642 -0
- package/dist/integrations/claude_code/skills/dr_04_application_layer/SKILL.md +911 -0
- package/dist/integrations/claude_code/skills/dr_05_technology_layer/SKILL.md +836 -0
- package/dist/integrations/claude_code/skills/dr_06_api_layer/SKILL.md +933 -0
- package/dist/integrations/claude_code/skills/dr_07_data_model_layer/SKILL.md +210 -0
- package/dist/integrations/claude_code/skills/dr_08_dataset_layer/SKILL.md +319 -0
- package/dist/integrations/claude_code/skills/dr_09_ux_layer/SKILL.md +319 -0
- package/dist/integrations/claude_code/skills/dr_10_navigation_layer/SKILL.md +318 -0
- package/dist/integrations/claude_code/skills/dr_11_apm_layer/SKILL.md +273 -0
- package/dist/integrations/claude_code/skills/dr_12_testing_layer/SKILL.md +273 -0
- package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +187 -0
- package/dist/integrations/claude_code/skills/dr_codebase_memory/SKILL.md +590 -0
- package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +295 -0
- package/dist/integrations/copilot-manager.js +328 -0
- package/dist/integrations/copilot-manager.js.map +7 -0
- package/dist/integrations/github_copilot/agents/dr-advisor.md +243 -0
- package/dist/integrations/github_copilot/agents/dr-architect.md +1567 -0
- package/dist/integrations/github_copilot/skills/dr_01_motivation_layer/SKILL.md +491 -0
- package/dist/integrations/github_copilot/skills/dr_02_business_layer/SKILL.md +539 -0
- package/dist/integrations/github_copilot/skills/dr_03_security_layer/SKILL.md +667 -0
- package/dist/integrations/github_copilot/skills/dr_04_application_layer/SKILL.md +670 -0
- package/dist/integrations/github_copilot/skills/dr_05_technology_layer/SKILL.md +660 -0
- package/dist/integrations/github_copilot/skills/dr_06_api_layer/SKILL.md +855 -0
- package/dist/integrations/github_copilot/skills/dr_07_data_model_layer/SKILL.md +171 -0
- package/dist/integrations/github_copilot/skills/dr_08_dataset_layer/SKILL.md +216 -0
- package/dist/integrations/github_copilot/skills/dr_09_ux_layer/SKILL.md +230 -0
- package/dist/integrations/github_copilot/skills/dr_10_navigation_layer/SKILL.md +228 -0
- package/dist/integrations/github_copilot/skills/dr_11_apm_layer/SKILL.md +234 -0
- package/dist/integrations/github_copilot/skills/dr_12_testing_layer/SKILL.md +230 -0
- package/dist/integrations/github_copilot/skills/dr_changeset_reviewer/SKILL.md +187 -0
- package/dist/integrations/hash-utils.js +51 -0
- package/dist/integrations/hash-utils.js.map +7 -0
- package/dist/integrations/types.js +16 -0
- package/dist/integrations/types.js.map +7 -0
- package/dist/reports/model-layer-report-generator.js +278 -0
- package/dist/reports/model-layer-report-generator.js.map +7 -0
- package/dist/reports/model-report-data.js +57 -0
- package/dist/reports/model-report-data.js.map +7 -0
- package/dist/reports/model-report-orchestrator.js +103 -0
- package/dist/reports/model-report-orchestrator.js.map +7 -0
- package/dist/schemas/bundled/analyzers/cbm.json +595 -0
- package/dist/schemas/bundled/analyzers/manifest.json +9 -0
- package/dist/schemas/bundled/api.json +3411 -0
- package/dist/schemas/bundled/apm.json +2371 -0
- package/dist/schemas/bundled/application.json +1253 -0
- package/dist/schemas/bundled/base.json +1622 -0
- package/dist/schemas/bundled/business.json +1279 -0
- package/dist/schemas/bundled/data-model.json +1298 -0
- package/dist/schemas/bundled/data-store.json +2025 -0
- package/dist/schemas/bundled/manifest.json +93 -0
- package/dist/schemas/bundled/motivation.json +1360 -0
- package/dist/schemas/bundled/navigation.json +1874 -0
- package/dist/schemas/bundled/security.json +3488 -0
- package/dist/schemas/bundled/technology.json +2067 -0
- package/dist/schemas/bundled/testing.json +3120 -0
- package/dist/schemas/bundled/ux.json +3592 -0
- package/dist/server/schemas.js +202 -0
- package/dist/server/schemas.js.map +7 -0
- package/dist/server/server.js +1336 -252
- package/dist/server/server.js.map +3 -3
- package/dist/server/telemetry-middleware.js +22 -15
- package/dist/server/telemetry-middleware.js.map +2 -2
- package/dist/server-entry.js +65 -0
- package/dist/server-entry.js.map +7 -0
- package/dist/telemetry/config.js +89 -0
- package/dist/telemetry/config.js.map +7 -0
- package/dist/telemetry/console-interceptor.js.map +1 -1
- package/dist/telemetry/detect.js +24 -0
- package/dist/telemetry/detect.js.map +7 -0
- package/dist/telemetry/fetch-otlp-exporter.js +112 -0
- package/dist/telemetry/fetch-otlp-exporter.js.map +7 -0
- package/dist/telemetry/index.js +153 -3
- package/dist/telemetry/index.js.map +2 -2
- package/dist/telemetry/resilient-exporter.js +22 -8
- package/dist/telemetry/resilient-exporter.js.map +2 -2
- package/dist/telemetry/resilient-log-exporter.js +17 -8
- package/dist/telemetry/resilient-log-exporter.js.map +2 -2
- package/dist/telemetry/test-instrumentation.js.map +2 -2
- package/dist/types/index.js +84 -0
- package/dist/types/index.js.map +3 -3
- package/dist/types/source-reference.js +1 -0
- package/dist/types/source-reference.js.map +7 -0
- package/dist/utils/chat-logger.js +292 -0
- package/dist/utils/chat-logger.js.map +7 -0
- package/dist/utils/claude-stream.js +8 -0
- package/dist/utils/claude-stream.js.map +7 -0
- package/dist/utils/date-utils.js +7 -0
- package/dist/utils/date-utils.js.map +7 -0
- package/dist/utils/element-utils.js.map +1 -1
- package/dist/utils/errors.js +88 -15
- package/dist/utils/errors.js.map +3 -3
- package/dist/utils/export-python-annotations.js +82 -0
- package/dist/utils/export-python-annotations.js.map +7 -0
- package/dist/utils/file-io.js +7 -2
- package/dist/utils/file-io.js.map +2 -2
- package/dist/utils/file-lock.js +156 -0
- package/dist/utils/file-lock.js.map +7 -0
- package/dist/utils/id-generator.js +18 -0
- package/dist/utils/id-generator.js.map +7 -0
- package/dist/utils/layer-name-formatter.js +16 -0
- package/dist/utils/layer-name-formatter.js.map +7 -0
- package/dist/utils/markdown-anchor.js +7 -0
- package/dist/utils/markdown-anchor.js.map +7 -0
- package/dist/utils/markdown-link.js +12 -0
- package/dist/utils/markdown-link.js.map +7 -0
- package/dist/utils/markdown-table.js +17 -0
- package/dist/utils/markdown-table.js.map +7 -0
- package/dist/utils/mermaid-utils.js +11 -0
- package/dist/utils/mermaid-utils.js.map +7 -0
- package/dist/utils/project-paths.js.map +2 -2
- package/dist/utils/source-reference.js +52 -0
- package/dist/utils/source-reference.js.map +7 -0
- package/dist/utils/spec-installer.js +40 -53
- package/dist/utils/spec-installer.js.map +2 -2
- package/dist/utils/spec-version.js +5 -1
- package/dist/utils/spec-version.js.map +2 -2
- package/dist/utils/string-utils.js +12 -0
- package/dist/utils/string-utils.js.map +7 -0
- package/dist/utils/table-formatting.js +19 -0
- package/dist/utils/table-formatting.js.map +7 -0
- package/dist/validators/index.js +4 -0
- package/dist/validators/index.js.map +2 -2
- package/dist/validators/naming-validator.js +20 -23
- package/dist/validators/naming-validator.js.map +2 -2
- package/dist/validators/reference-validator.js +23 -38
- package/dist/validators/reference-validator.js.map +2 -2
- package/dist/validators/relationship-schema-validator.js +243 -0
- package/dist/validators/relationship-schema-validator.js.map +7 -0
- package/dist/validators/schema-validator.js +168 -68
- package/dist/validators/schema-validator.js.map +2 -2
- package/dist/validators/semantic-validator.js +36 -60
- package/dist/validators/semantic-validator.js.map +2 -2
- package/dist/validators/types.js.map +2 -2
- package/dist/validators/validation-formatter.js +159 -0
- package/dist/validators/validation-formatter.js.map +7 -0
- package/dist/validators/validator.js +24 -38
- package/dist/validators/validator.js.map +2 -2
- package/dist/viewer/README.md +139 -0
- package/dist/viewer/assets/main-CY0Xufuj.css +1 -0
- package/dist/viewer/assets/main-DtgErnwF.js +81 -0
- package/dist/viewer/assets/vendor-CxKvDp2x.js +17 -0
- package/dist/viewer/favicon.svg +11 -0
- package/dist/viewer/fonts/inter/Inter-Black.woff2 +0 -0
- package/dist/viewer/fonts/inter/Inter-Bold.woff2 +0 -0
- package/dist/viewer/fonts/inter/Inter-ExtraBold.woff2 +0 -0
- package/dist/viewer/fonts/inter/Inter-Light.woff2 +0 -0
- package/dist/viewer/fonts/inter/Inter-Medium.woff2 +0 -0
- package/dist/viewer/fonts/inter/Inter-Regular.woff2 +0 -0
- package/dist/viewer/fonts/inter/Inter-SemiBold.woff2 +0 -0
- package/dist/viewer/fonts/jetbrains-mono/JetBrainsMono-Medium.woff2 +0 -0
- package/dist/viewer/fonts/jetbrains-mono/JetBrainsMono-Regular.woff2 +0 -0
- package/dist/viewer/fonts/jetbrains-mono/JetBrainsMono-SemiBold.woff2 +0 -0
- package/dist/viewer/index.html +15 -0
- package/dist/viewer/manifest.json +99 -0
- package/dist/viewer/version.json +1 -0
- package/package.json +42 -16
- package/PYTHON_CLI_PARITY.md +0 -265
- package/PYTHON_CLI_STRUCTURE_UPDATE.md +0 -246
- package/biome.json +0 -42
- package/bunfig.toml +0 -3
- package/dist/ai/claude-client.js +0 -71
- package/dist/ai/claude-client.js.map +0 -7
- package/dist/ai/context-provider.js +0 -129
- package/dist/ai/context-provider.js.map +0 -7
- package/dist/ai/tools.js +0 -231
- package/dist/ai/tools.js.map +0 -7
- package/dist/commands/migrate.js +0 -36
- package/dist/commands/migrate.js.map +0 -7
- package/dist/commands/project.js +0 -74
- package/dist/commands/project.js.map +0 -7
- package/dist/core/active-changeset.js +0 -52
- package/dist/core/active-changeset.js.map +0 -7
- package/dist/core/projection-engine.js +0 -288
- package/dist/core/projection-engine.js.map +0 -7
- package/dist/schemas/bundled/01-motivation-layer.schema.json +0 -942
- package/dist/schemas/bundled/02-business-layer.schema.json +0 -1293
- package/dist/schemas/bundled/03-security-layer.schema.json +0 -2177
- package/dist/schemas/bundled/04-application-layer.schema.json +0 -1328
- package/dist/schemas/bundled/05-technology-layer.schema.json +0 -1465
- package/dist/schemas/bundled/06-api-layer.schema.json +0 -1743
- package/dist/schemas/bundled/07-data-model-layer.schema.json +0 -1253
- package/dist/schemas/bundled/08-datastore-layer.schema.json +0 -892
- package/dist/schemas/bundled/09-ux-layer.schema.json +0 -1506
- package/dist/schemas/bundled/10-navigation-layer.schema.json +0 -843
- package/dist/schemas/bundled/11-apm-observability-layer.schema.json +0 -1430
- package/dist/schemas/bundled/12-testing-layer.schema.json +0 -1091
- package/dist/schemas/bundled/common/layer-extensions.schema.json +0 -266
- package/dist/schemas/bundled/common/predicates.schema.json +0 -493
- package/dist/schemas/bundled/common/relationships.schema.json +0 -791
- package/dist/schemas/bundled/common/source-references.schema.json +0 -71
- package/dist/schemas/bundled/link-registry.json +0 -1839
- package/dist/schemas/bundled/link-registry.schema.json +0 -339
- package/dist/schemas/bundled/relationship-catalog.json +0 -1302
- package/dist/schemas/bundled/relationship-type.schema.json +0 -221
- package/dist/utils/model-path.js +0 -22
- package/dist/utils/model-path.js.map +0 -7
- package/docs/TEST-INSTRUMENTATION.md +0 -405
- package/docs/telemetry.md +0 -427
- package/test-debug.js +0 -33
- package/test-special.mjs +0 -46
- package/test_runner.ts +0 -7
|
@@ -0,0 +1,1205 @@
|
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { readFile } from "fs/promises";
|
|
4
|
+
import { CLIError, ErrorCategory, handleWarning } from "../utils/errors.js";
|
|
5
|
+
import { VALID_HTTP_METHODS } from "./types.js";
|
|
6
|
+
import { StdioClient } from "./stdio-client.js";
|
|
7
|
+
import { readIndexMeta, writeIndexMeta } from "./session-state.js";
|
|
8
|
+
import { getCliVersion } from "./version.js";
|
|
9
|
+
import { StagingAreaManager } from "../core/staging-area.js";
|
|
10
|
+
import { VerifyEngine } from "./verify-engine.js";
|
|
11
|
+
import { clampDepth, shapeCallGraphNode, determineEdgeType } from "./call-graph-utils.js";
|
|
12
|
+
import { evaluateHeuristic, capConfidence, matchPattern, getKnownHeuristicNames } from "./heuristic-utils.js";
|
|
13
|
+
import { toKebabCase } from "../utils/id-generator.js";
|
|
14
|
+
const SOURCE_CODE_EXT_RE = /\.(ts|tsx|js|jsx|mjs|cjs|vue|svelte|py|go|rb|java|rs|php|cs|cpp|c|h|kt|swift|sh|bash)$/i;
|
|
15
|
+
function isLikelyHttpRoute(qualifiedName) {
|
|
16
|
+
const httpPath = qualifiedName.replace(/^__route__[A-Z]+__/, "");
|
|
17
|
+
return !SOURCE_CODE_EXT_RE.test(httpPath) && !httpPath.startsWith("/.dr/") && !httpPath.startsWith("/schemas/");
|
|
18
|
+
}
|
|
19
|
+
function inferHttpMethodFromFunctionName(name) {
|
|
20
|
+
const lower = name.toLowerCase();
|
|
21
|
+
return /^(create|add|post|submit|send)/.test(lower) ? "POST" : /^(update|put|save|replace|set)/.test(lower) ? "PUT" : /^(patch|merge)/.test(lower) ? "PATCH" : /^(delete|remove|destroy)/.test(lower) ? "DELETE" : "GET";
|
|
22
|
+
}
|
|
23
|
+
function inferHttpPathFromFunctionName(name) {
|
|
24
|
+
const stripped = name.replace(/^(fetch|get|list|create|add|update|put|delete|remove|patch|merge|load|save|replace|post|submit|send|destroy)/i, ""), segment = toKebabCase(stripped || name);
|
|
25
|
+
return segment ? `/${segment}` : "/";
|
|
26
|
+
}
|
|
27
|
+
class CbmAnalyzer {
|
|
28
|
+
mapper;
|
|
29
|
+
testCodePatternWarned = !1;
|
|
30
|
+
warnedHeuristics = /* @__PURE__ */ new Set();
|
|
31
|
+
/**
|
|
32
|
+
* Default regex patterns for test code detection
|
|
33
|
+
*/
|
|
34
|
+
static DEFAULT_TEST_PATTERNS = [
|
|
35
|
+
/\.test\./,
|
|
36
|
+
/\.spec\./,
|
|
37
|
+
/\/tests?\//,
|
|
38
|
+
/\/__tests__\//,
|
|
39
|
+
/\/test-/,
|
|
40
|
+
/\/fixtures?\//,
|
|
41
|
+
/\/__fixtures__\//
|
|
42
|
+
];
|
|
43
|
+
constructor(mapper) {
|
|
44
|
+
this.mapper = mapper;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get the analyzer name
|
|
48
|
+
*/
|
|
49
|
+
get name() {
|
|
50
|
+
return this.mapper.getAnalyzerName();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get the analyzer display name
|
|
54
|
+
*/
|
|
55
|
+
get displayName() {
|
|
56
|
+
return this.mapper.getAnalyzerMetadata()?.display_name || this.mapper.getAnalyzerName();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Check if the MCP server is registered in .mcp.json
|
|
60
|
+
*
|
|
61
|
+
* Reads .mcp.json from the project root and checks if the analyzer's MCP server
|
|
62
|
+
* is registered there. This is extracted into a separate method for easier testing.
|
|
63
|
+
*
|
|
64
|
+
* @param projectRoot Absolute path to the project root
|
|
65
|
+
* @returns True if the MCP server is registered, false otherwise
|
|
66
|
+
* @throws CLIError if .mcp.json exists but is unreadable or malformed
|
|
67
|
+
*/
|
|
68
|
+
async checkMcpRegistration(projectRoot) {
|
|
69
|
+
try {
|
|
70
|
+
const mcpJsonPath = path.join(projectRoot, ".mcp.json"), mcpContent = await readFile(mcpJsonPath, "utf-8"), mcpConfig = JSON.parse(mcpContent), mcpServerName = this.mapper.getAnalyzerMetadata()?.mcp_server_name ?? "codebase-memory-mcp";
|
|
71
|
+
return mcpConfig.mcpServers && mcpServerName in mcpConfig.mcpServers || !1;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
const err = error;
|
|
74
|
+
if (err.code === "ENOENT")
|
|
75
|
+
return !1;
|
|
76
|
+
throw err.code === "EACCES" ? new CLIError("Permission denied reading .mcp.json", ErrorCategory.SYSTEM, [
|
|
77
|
+
`Cannot read ${path.join(projectRoot, ".mcp.json")}`,
|
|
78
|
+
"Check file permissions"
|
|
79
|
+
]) : new CLIError("Invalid .mcp.json format", ErrorCategory.VALIDATION, [
|
|
80
|
+
`Check that ${path.join(projectRoot, ".mcp.json")} contains valid JSON`,
|
|
81
|
+
`Error: ${error instanceof Error ? error.message : String(error)}`
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Detect if the analyzer is installed and functional
|
|
87
|
+
*
|
|
88
|
+
* Checks for the presence of the analyzer binary, reads .mcp.json for registration info,
|
|
89
|
+
* and validates the required tool contract by calling initialize and verifying required_tools.
|
|
90
|
+
*
|
|
91
|
+
* @param projectRoot Optional absolute path to project root (defaults to cwd)
|
|
92
|
+
* @returns Detection result with binary path, version, and MCP registration status
|
|
93
|
+
*/
|
|
94
|
+
async detect(projectRoot) {
|
|
95
|
+
const metadata = this.mapper.getAnalyzerMetadata(), binaryNames = metadata?.binary_names ?? [
|
|
96
|
+
"codebase-memory-mcp"
|
|
97
|
+
];
|
|
98
|
+
let mcpRegistered = !1;
|
|
99
|
+
try {
|
|
100
|
+
mcpRegistered = await this.checkMcpRegistration(projectRoot || process.cwd());
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error instanceof CLIError)
|
|
103
|
+
handleWarning(error.message, error.suggestions);
|
|
104
|
+
else {
|
|
105
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
106
|
+
handleWarning("Unexpected error checking MCP registration", [errorMsg]);
|
|
107
|
+
}
|
|
108
|
+
mcpRegistered = !1;
|
|
109
|
+
}
|
|
110
|
+
for (const binaryName of binaryNames) {
|
|
111
|
+
const whichResult = spawnSync("which", [binaryName], {
|
|
112
|
+
stdio: "pipe",
|
|
113
|
+
encoding: "utf-8"
|
|
114
|
+
});
|
|
115
|
+
if (whichResult.status !== 0)
|
|
116
|
+
continue;
|
|
117
|
+
const binaryPath = whichResult.stdout.trim();
|
|
118
|
+
if (!binaryPath)
|
|
119
|
+
continue;
|
|
120
|
+
const client = new StdioClient();
|
|
121
|
+
try {
|
|
122
|
+
client.spawn(binaryPath);
|
|
123
|
+
const version = await getCliVersion();
|
|
124
|
+
await client.initialize({
|
|
125
|
+
name: "dr-cli",
|
|
126
|
+
version
|
|
127
|
+
});
|
|
128
|
+
let contractOk = !0;
|
|
129
|
+
const requiredTools = metadata?.supported_tool_contract?.required_tools ?? [];
|
|
130
|
+
if (requiredTools.length > 0)
|
|
131
|
+
try {
|
|
132
|
+
const response = await client.callTool("tools/list", {}), availableTools = new Set((response.tools ?? []).map((tool) => tool.name));
|
|
133
|
+
for (const tool of requiredTools)
|
|
134
|
+
availableTools.has(tool) || (contractOk = !1, handleWarning(`Required tool '${tool}' not available in ${binaryName}`, ["Ensure the analyzer server exports all required tools"]));
|
|
135
|
+
} catch (error) {
|
|
136
|
+
contractOk = !1;
|
|
137
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
138
|
+
handleWarning(`Failed to verify required tools for ${binaryName}: ${errorMsg}`, ["Ensure all required tools are available in the analyzer server"]);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
installed: !0,
|
|
142
|
+
binary_path: binaryPath,
|
|
143
|
+
version: void 0,
|
|
144
|
+
// Version could be extracted from --version if needed
|
|
145
|
+
mcp_registered: mcpRegistered,
|
|
146
|
+
contract_ok: contractOk
|
|
147
|
+
};
|
|
148
|
+
} catch (error) {
|
|
149
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
150
|
+
handleWarning(`Binary ${binaryName} failed initialization: ${errorMessage}`, ["Check that the binary is properly installed and functional"]);
|
|
151
|
+
continue;
|
|
152
|
+
} finally {
|
|
153
|
+
try {
|
|
154
|
+
client.close();
|
|
155
|
+
} catch (error) {
|
|
156
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
157
|
+
handleWarning(`Failed to close MCP client for ${binaryName}: ${errorMessage}`, [
|
|
158
|
+
"The MCP server process may still be running",
|
|
159
|
+
"Try restarting your terminal or manually killing the process"
|
|
160
|
+
]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
installed: !1
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get the current status of the analyzer for a project
|
|
170
|
+
*
|
|
171
|
+
* Checks if the project is indexed and whether the index is fresh relative to git HEAD.
|
|
172
|
+
*
|
|
173
|
+
* @param projectRoot Absolute path to the project root
|
|
174
|
+
* @returns Current status including detection, index state, and freshness
|
|
175
|
+
*/
|
|
176
|
+
async status(projectRoot) {
|
|
177
|
+
const detected = await this.detect(projectRoot), analyzerName = this.mapper.getAnalyzerName(), indexMeta = await readIndexMeta(projectRoot, analyzerName);
|
|
178
|
+
if (!(indexMeta !== null))
|
|
179
|
+
return {
|
|
180
|
+
detected,
|
|
181
|
+
indexed: !1,
|
|
182
|
+
fresh: !1
|
|
183
|
+
};
|
|
184
|
+
let fresh = !1;
|
|
185
|
+
try {
|
|
186
|
+
const currentHeadResult = spawnSync("git", ["rev-parse", "HEAD"], {
|
|
187
|
+
cwd: projectRoot,
|
|
188
|
+
stdio: "pipe",
|
|
189
|
+
encoding: "utf-8"
|
|
190
|
+
});
|
|
191
|
+
if (currentHeadResult.status === 0)
|
|
192
|
+
fresh = currentHeadResult.stdout.trim() === indexMeta.git_head;
|
|
193
|
+
else {
|
|
194
|
+
const gitError = currentHeadResult.stderr?.trim() || "git returned non-zero status";
|
|
195
|
+
handleWarning("Failed to check git freshness", [
|
|
196
|
+
`Git error: ${gitError}`,
|
|
197
|
+
"Index freshness cannot be determined - treating as stale"
|
|
198
|
+
]), fresh = !1;
|
|
199
|
+
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
202
|
+
handleWarning("Failed to spawn git process", [
|
|
203
|
+
`Error: ${errorMsg}`,
|
|
204
|
+
"Index freshness cannot be determined - treating as stale"
|
|
205
|
+
]), fresh = !1;
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
detected,
|
|
209
|
+
indexed: !0,
|
|
210
|
+
index_meta: indexMeta,
|
|
211
|
+
fresh,
|
|
212
|
+
last_indexed: indexMeta.timestamp
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Index a project or update the index
|
|
217
|
+
*
|
|
218
|
+
* Creates a project entry in the CBM backend if it doesn't already exist,
|
|
219
|
+
* indexes the repository, and writes freshness metadata.
|
|
220
|
+
*
|
|
221
|
+
* Skips indexing if the index is fresh (git HEAD matches) unless --force is set.
|
|
222
|
+
*
|
|
223
|
+
* @param projectRoot Absolute path to the project root
|
|
224
|
+
* @param options Optional configuration
|
|
225
|
+
* @returns Index result with counts and metadata
|
|
226
|
+
*/
|
|
227
|
+
async index(projectRoot, options) {
|
|
228
|
+
const status = await this.status(projectRoot);
|
|
229
|
+
if (status.indexed && status.fresh && !options?.force) {
|
|
230
|
+
handleWarning("Index is fresh. Use --force to reindex.", [
|
|
231
|
+
`Last indexed: ${status.last_indexed}`
|
|
232
|
+
]);
|
|
233
|
+
const meta = status.index_meta;
|
|
234
|
+
return {
|
|
235
|
+
node_count: meta.node_count,
|
|
236
|
+
edge_count: meta.edge_count,
|
|
237
|
+
git_head: meta.git_head,
|
|
238
|
+
timestamp: meta.timestamp
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const detection = status.detected;
|
|
242
|
+
if (!detection.installed)
|
|
243
|
+
throw new CLIError("CBM analyzer not installed", ErrorCategory.NOT_FOUND, [
|
|
244
|
+
"Install codebase-memory-mcp: npm install -g codebase-memory-mcp",
|
|
245
|
+
"Ensure the binary is in your PATH"
|
|
246
|
+
]);
|
|
247
|
+
const client = new StdioClient();
|
|
248
|
+
try {
|
|
249
|
+
client.spawn(detection.binary_path);
|
|
250
|
+
const version = await getCliVersion();
|
|
251
|
+
await client.initialize({
|
|
252
|
+
name: "dr-cli",
|
|
253
|
+
version
|
|
254
|
+
});
|
|
255
|
+
const listProjectsResponse = await client.invokeTool("list_projects");
|
|
256
|
+
if (!listProjectsResponse || typeof listProjectsResponse != "object")
|
|
257
|
+
throw new CLIError("Invalid response from list_projects", ErrorCategory.SYSTEM, ["Ensure codebase-memory-mcp is properly installed and functional"]);
|
|
258
|
+
if ((Array.isArray(listProjectsResponse.projects) ? listProjectsResponse.projects : []).some((p) => p.root_path === projectRoot) && status.fresh && !options?.force) {
|
|
259
|
+
if (handleWarning("Project already indexed in CBM backend with fresh index.", ["Use --force to re-index the project"]), !status.index_meta)
|
|
260
|
+
throw new CLIError("Project exists in CBM backend but local metadata is missing", ErrorCategory.VALIDATION, [
|
|
261
|
+
"Use --force to re-index and regenerate metadata",
|
|
262
|
+
"Or manually delete the project from CBM backend and re-index"
|
|
263
|
+
]);
|
|
264
|
+
const meta2 = status.index_meta;
|
|
265
|
+
return {
|
|
266
|
+
node_count: meta2.node_count,
|
|
267
|
+
edge_count: meta2.edge_count,
|
|
268
|
+
git_head: meta2.git_head,
|
|
269
|
+
timestamp: meta2.timestamp
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const headResult = spawnSync("git", ["rev-parse", "HEAD"], {
|
|
273
|
+
cwd: projectRoot,
|
|
274
|
+
stdio: "pipe",
|
|
275
|
+
encoding: "utf-8"
|
|
276
|
+
});
|
|
277
|
+
if (headResult.status !== 0)
|
|
278
|
+
throw new CLIError("Failed to get git HEAD", ErrorCategory.SYSTEM, [
|
|
279
|
+
"Ensure the project is a git repository",
|
|
280
|
+
"Verify git is installed and functional",
|
|
281
|
+
`Error: ${headResult.stderr?.trim() || "unknown error"}`
|
|
282
|
+
]);
|
|
283
|
+
const gitHead = headResult.stdout.trim();
|
|
284
|
+
try {
|
|
285
|
+
const activeChangesetId = await new StagingAreaManager(projectRoot).getActiveId();
|
|
286
|
+
activeChangesetId && console.log(`Indexing with active changeset: ${activeChangesetId}`);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
289
|
+
handleWarning("Failed to read active changeset", [`Error: ${errorMsg}`, "Continuing with indexing"]);
|
|
290
|
+
}
|
|
291
|
+
const indexResponse = await client.invokeTool("index_repository", {
|
|
292
|
+
repo_path: projectRoot
|
|
293
|
+
});
|
|
294
|
+
if (!indexResponse || typeof indexResponse != "object")
|
|
295
|
+
throw new CLIError("Invalid response from index_repository", ErrorCategory.SYSTEM, ["Ensure codebase-memory-mcp is properly installed and functional"]);
|
|
296
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString(), nodeCount = typeof indexResponse.nodes == "number" ? indexResponse.nodes : 0, edgeCount = typeof indexResponse.edges == "number" ? indexResponse.edges : 0;
|
|
297
|
+
typeof indexResponse.nodes != "number" && handleWarning("Invalid node count from index_repository", [
|
|
298
|
+
`Expected number, got ${typeof indexResponse.nodes}: ${String(indexResponse.nodes)}`,
|
|
299
|
+
"Defaulting to 0 - index may be incomplete or corrupted"
|
|
300
|
+
]), typeof indexResponse.edges != "number" && handleWarning("Invalid edge count from index_repository", [
|
|
301
|
+
`Expected number, got ${typeof indexResponse.edges}: ${String(indexResponse.edges)}`,
|
|
302
|
+
"Defaulting to 0 - index may be incomplete or corrupted"
|
|
303
|
+
]);
|
|
304
|
+
const meta = {
|
|
305
|
+
git_head: gitHead,
|
|
306
|
+
timestamp,
|
|
307
|
+
node_count: nodeCount,
|
|
308
|
+
edge_count: edgeCount
|
|
309
|
+
}, analyzerName = this.mapper.getAnalyzerName();
|
|
310
|
+
return await writeIndexMeta(meta, projectRoot, analyzerName), {
|
|
311
|
+
node_count: nodeCount,
|
|
312
|
+
edge_count: edgeCount,
|
|
313
|
+
git_head: gitHead,
|
|
314
|
+
timestamp
|
|
315
|
+
};
|
|
316
|
+
} finally {
|
|
317
|
+
client.close();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Validate search_graph response structure
|
|
322
|
+
*
|
|
323
|
+
* Ensures response is an object and nodes is an array.
|
|
324
|
+
* Throws CLIError if validation fails.
|
|
325
|
+
*
|
|
326
|
+
* @private
|
|
327
|
+
* @param response Raw response from search_graph MCP tool
|
|
328
|
+
* @returns Typed nodes array (may be empty if no results found)
|
|
329
|
+
* @throws CLIError if response structure is invalid
|
|
330
|
+
*/
|
|
331
|
+
validateSearchResponse(response) {
|
|
332
|
+
if (!response || typeof response != "object")
|
|
333
|
+
throw new CLIError("Invalid response from search_graph", ErrorCategory.SYSTEM, ["Ensure codebase-memory-mcp is properly installed and functional"]);
|
|
334
|
+
const responseObj = response, nodeArray = responseObj.results ?? responseObj.nodes;
|
|
335
|
+
if (!Array.isArray(nodeArray))
|
|
336
|
+
throw new CLIError("Invalid response from search_graph: results must be an array", ErrorCategory.SYSTEM, [
|
|
337
|
+
`Expected array in 'results' or 'nodes', got ${typeof nodeArray}`,
|
|
338
|
+
"Ensure codebase-memory-mcp is properly installed and returning valid results"
|
|
339
|
+
]);
|
|
340
|
+
return nodeArray;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Resolve the internal project name used by codebase-memory-mcp for a given root path.
|
|
344
|
+
*
|
|
345
|
+
* list_projects returns { name, root_path } per entry. search_graph and other
|
|
346
|
+
* query tools require the internal `name`, not the absolute `root_path`.
|
|
347
|
+
* Falls back to the absolute path with a warning if the project is not found.
|
|
348
|
+
*/
|
|
349
|
+
async resolveProjectName(client, projectRoot) {
|
|
350
|
+
const listResponse = await client.invokeTool("list_projects"), match = (Array.isArray(listResponse?.projects) ? listResponse.projects : []).find((p) => p.root_path === projectRoot);
|
|
351
|
+
return match ? match.name : (handleWarning("Project not found in CBM backend; using absolute path as project identifier", [
|
|
352
|
+
`Project root: ${projectRoot}`,
|
|
353
|
+
"Run `dr analyzer index` to ensure the project is indexed"
|
|
354
|
+
]), projectRoot);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Query for API endpoints/routes in the indexed project
|
|
358
|
+
*
|
|
359
|
+
* Searches the indexed graph for Route nodes, applies field mappings from
|
|
360
|
+
* the analyzer's mapping, and filters based on test code exclusion rules.
|
|
361
|
+
*
|
|
362
|
+
* @param projectRoot Absolute path to the project root
|
|
363
|
+
* @returns Array of endpoint candidates with confidence and source info
|
|
364
|
+
* @throws CLIError if the project is not indexed
|
|
365
|
+
*/
|
|
366
|
+
async endpoints(projectRoot) {
|
|
367
|
+
const status = await this.status(projectRoot);
|
|
368
|
+
if (!status.indexed)
|
|
369
|
+
throw new CLIError(`Project not indexed: ${projectRoot}`, ErrorCategory.NOT_FOUND, [
|
|
370
|
+
"Run `dr analyzer index` to index the project",
|
|
371
|
+
"Ensure codebase-memory-mcp is installed and working"
|
|
372
|
+
]);
|
|
373
|
+
const detection = status.detected;
|
|
374
|
+
if (!detection.installed)
|
|
375
|
+
throw new CLIError("CBM analyzer not installed", ErrorCategory.NOT_FOUND, ["Install codebase-memory-mcp: npm install -g codebase-memory-mcp"]);
|
|
376
|
+
const routeMapping = this.mapper.getNodeMapping("Route");
|
|
377
|
+
if (!routeMapping)
|
|
378
|
+
throw new CLIError("Route mapping not found in analyzer", ErrorCategory.VALIDATION, ["Run `npm run build:spec` to recompile the analyzer artifacts"]);
|
|
379
|
+
const client = new StdioClient();
|
|
380
|
+
try {
|
|
381
|
+
client.spawn(detection.binary_path);
|
|
382
|
+
const version = await getCliVersion();
|
|
383
|
+
await client.initialize({
|
|
384
|
+
name: "dr-cli",
|
|
385
|
+
version
|
|
386
|
+
});
|
|
387
|
+
const projectName = await this.resolveProjectName(client, projectRoot), searchResponse = await client.invokeTool("search_graph", {
|
|
388
|
+
label: routeMapping.cbm_label,
|
|
389
|
+
project: projectName
|
|
390
|
+
}), nodes = this.validateSearchResponse(searchResponse).filter((n) => /^__route__[A-Z]+__\//.test(String(n.qualified_name ?? ""))).filter((n) => isLikelyHttpRoute(String(n.qualified_name ?? ""))), candidates = [];
|
|
391
|
+
for (const node of nodes) {
|
|
392
|
+
const candidate = await this.transformNodeToEndpoint(node, routeMapping, projectRoot);
|
|
393
|
+
candidate.source_file && (this.isTestCode(candidate) || candidates.push(candidate));
|
|
394
|
+
}
|
|
395
|
+
if (candidates.length === 0)
|
|
396
|
+
try {
|
|
397
|
+
const qResp = await client.invokeTool("query_graph", {
|
|
398
|
+
query: `
|
|
399
|
+
MATCH (n:Function)
|
|
400
|
+
WHERE (n.file_path CONTAINS 'client' OR n.file_path CONTAINS 'Client'
|
|
401
|
+
OR n.file_path CONTAINS 'Api' OR n.file_path CONTAINS 'Service'
|
|
402
|
+
OR n.file_path CONTAINS 'service' OR n.file_path CONTAINS 'http'
|
|
403
|
+
OR n.file_path CONTAINS 'Http')
|
|
404
|
+
AND (n.name STARTS WITH 'fetch' OR n.name STARTS WITH 'get'
|
|
405
|
+
OR n.name STARTS WITH 'post' OR n.name STARTS WITH 'put'
|
|
406
|
+
OR n.name STARTS WITH 'delete' OR n.name STARTS WITH 'patch'
|
|
407
|
+
OR n.name STARTS WITH 'create' OR n.name STARTS WITH 'update'
|
|
408
|
+
OR n.name STARTS WITH 'list')
|
|
409
|
+
RETURN n
|
|
410
|
+
`,
|
|
411
|
+
project: projectName
|
|
412
|
+
});
|
|
413
|
+
if (qResp && typeof qResp == "object") {
|
|
414
|
+
const records = qResp.records ?? [];
|
|
415
|
+
for (const record of records) {
|
|
416
|
+
const node = record.n;
|
|
417
|
+
if (!node || !node.file_path)
|
|
418
|
+
continue;
|
|
419
|
+
const candidate = this.shapeClientSideEndpoint(node, projectRoot);
|
|
420
|
+
candidate && !this.isTestCode(candidate) && candidates.push(candidate);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
425
|
+
return this.warnOnDuplicateIdFragments(candidates, (c) => c.suggested_id_fragment, (c) => c.source_symbol || c.source_file || "(unknown)"), candidates;
|
|
426
|
+
} finally {
|
|
427
|
+
client.close();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Execute a raw Cypher query against the analyzer's graph
|
|
432
|
+
*
|
|
433
|
+
* Passes the raw Cypher string directly to the query_graph MCP tool.
|
|
434
|
+
* The query_graph tool is optional (not in required_tools contract).
|
|
435
|
+
* If the tool call fails, surfaces a clear error message.
|
|
436
|
+
*
|
|
437
|
+
* @param projectRoot Absolute path to the project root
|
|
438
|
+
* @param rawQuery Raw Cypher query string
|
|
439
|
+
* @returns Raw result from query_graph tool
|
|
440
|
+
* @throws CLIError if project not indexed or query_graph tool unavailable
|
|
441
|
+
*/
|
|
442
|
+
async query(projectRoot, rawQuery) {
|
|
443
|
+
const status = await this.status(projectRoot);
|
|
444
|
+
if (!status.indexed)
|
|
445
|
+
throw new CLIError(`Project not indexed: ${projectRoot}`, ErrorCategory.NOT_FOUND, [
|
|
446
|
+
"Run `dr analyzer index` to index the project",
|
|
447
|
+
"Ensure codebase-memory-mcp is installed and working"
|
|
448
|
+
]);
|
|
449
|
+
const detection = status.detected;
|
|
450
|
+
if (!detection.installed)
|
|
451
|
+
throw new CLIError("CBM analyzer not installed", ErrorCategory.NOT_FOUND, ["Install codebase-memory-mcp: npm install -g codebase-memory-mcp"]);
|
|
452
|
+
const client = new StdioClient();
|
|
453
|
+
try {
|
|
454
|
+
client.spawn(detection.binary_path);
|
|
455
|
+
const version = await getCliVersion();
|
|
456
|
+
await client.initialize({
|
|
457
|
+
name: "dr-cli",
|
|
458
|
+
version
|
|
459
|
+
});
|
|
460
|
+
const projectName = await this.resolveProjectName(client, projectRoot);
|
|
461
|
+
try {
|
|
462
|
+
return await client.invokeTool("query_graph", {
|
|
463
|
+
query: rawQuery,
|
|
464
|
+
project: projectName
|
|
465
|
+
});
|
|
466
|
+
} catch (error) {
|
|
467
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
468
|
+
throw errorMessage.includes("query_graph") || errorMessage.includes("Unknown method") ? new CLIError("query_graph not supported by this analyzer", ErrorCategory.USER, [
|
|
469
|
+
"The query_graph tool is optional in the CBM analyzer contract",
|
|
470
|
+
"Ensure your codebase-memory-mcp version supports raw graph queries",
|
|
471
|
+
"Contact the analyzer maintainers if you need this feature"
|
|
472
|
+
]) : error;
|
|
473
|
+
}
|
|
474
|
+
} finally {
|
|
475
|
+
client.close();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
warnOnDuplicateIdFragments(items, getKey, getLabel) {
|
|
479
|
+
const seen = /* @__PURE__ */ new Map();
|
|
480
|
+
for (const item of items) {
|
|
481
|
+
const key = getKey(item);
|
|
482
|
+
if (!key)
|
|
483
|
+
continue;
|
|
484
|
+
const label = getLabel(item);
|
|
485
|
+
seen.has(key) ? handleWarning(`Duplicate suggested ID "${key}" \u2014 multiple symbols map to the same element ID`, [
|
|
486
|
+
`First: ${seen.get(key)}`,
|
|
487
|
+
`Conflict: ${label}`,
|
|
488
|
+
'Rename one or use "dr add" with a custom name'
|
|
489
|
+
]) : seen.set(key, label);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Transform a CBM graph node to an endpoint candidate
|
|
494
|
+
*
|
|
495
|
+
* Maps CBM Route node properties to DR EndpointCandidate contract:
|
|
496
|
+
* - suggested_layer: always "api" for endpoints
|
|
497
|
+
* - suggested_element_type: always "operation" for routes
|
|
498
|
+
* - suggested_name: from node properties or id (kebab-case)
|
|
499
|
+
* - http_method: from properties.method or defaults to "GET"
|
|
500
|
+
* - http_path: from properties.path or defaults to "/"
|
|
501
|
+
* - handler_qualified_name: from properties.handler_name or empty string
|
|
502
|
+
* - source_symbol: from properties.symbol or empty string
|
|
503
|
+
* - source_start_line: from properties.start_line or 0
|
|
504
|
+
* - source_end_line: from properties.end_line or 0
|
|
505
|
+
*
|
|
506
|
+
* Downgrades confidence if required fields are missing.
|
|
507
|
+
*
|
|
508
|
+
* @private
|
|
509
|
+
*/
|
|
510
|
+
async transformNodeToEndpoint(node, mapping, projectRoot) {
|
|
511
|
+
const properties = node.properties ?? {}, nodeId = String(node.name ?? node.id ?? "(unknown)");
|
|
512
|
+
let confidence = mapping.confidence;
|
|
513
|
+
const suggestedName = toKebabCase(String(properties.name ?? node.name ?? node.id ?? "")), suggestedIdFragment = suggestedName;
|
|
514
|
+
let rawMethod;
|
|
515
|
+
typeof properties.method == "string" ? rawMethod = properties.method.toUpperCase() : (rawMethod = String(node.qualified_name ?? "").match(/^__route__([A-Z]+)__/)?.[1] ?? "GET", rawMethod === "ANY" && (rawMethod = "GET"));
|
|
516
|
+
const validMethods = new Set(VALID_HTTP_METHODS), isValidMethod = validMethods.has(rawMethod), httpMethod = isValidMethod ? rawMethod : "GET";
|
|
517
|
+
isValidMethod || handleWarning(`Node ${nodeId}: Invalid HTTP method`, [
|
|
518
|
+
`Method '${rawMethod}' is not a valid HTTP method`,
|
|
519
|
+
`Valid methods: ${Array.from(validMethods).join(", ")}`,
|
|
520
|
+
"Defaulting to GET - endpoint documentation may be inaccurate"
|
|
521
|
+
]);
|
|
522
|
+
let httpPath;
|
|
523
|
+
typeof properties.path == "string" ? httpPath = properties.path : typeof node.name == "string" && node.name.startsWith("/") ? httpPath = node.name : (handleWarning(`Node ${nodeId}: Missing or invalid HTTP path`, [
|
|
524
|
+
`Expected string, got ${typeof properties.path}`,
|
|
525
|
+
"Defaulting to / - endpoint may be incomplete"
|
|
526
|
+
]), httpPath = "/");
|
|
527
|
+
const handlerQualifiedName = String(properties.handler_name ?? properties.handler_qualified_name ?? ""), sourceSymbol = String(properties.symbol ?? properties.source_symbol ?? ""), sourceStartLine = Number(properties.start_line ?? 0), sourceEndLine = Number(properties.end_line ?? 0);
|
|
528
|
+
(!handlerQualifiedName || !sourceSymbol) && (confidence = capConfidence(confidence));
|
|
529
|
+
let sourceFile = String(node.file_path ?? properties.file_path ?? properties.source_file ?? "");
|
|
530
|
+
if (sourceFile && projectRoot)
|
|
531
|
+
try {
|
|
532
|
+
sourceFile = path.relative(projectRoot, sourceFile);
|
|
533
|
+
} catch (error) {
|
|
534
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
535
|
+
handleWarning(`Failed to compute relative path for ${nodeId}`, [
|
|
536
|
+
`Error: ${errorMsg}`,
|
|
537
|
+
"Using absolute file path instead of relative"
|
|
538
|
+
]);
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
source_file: sourceFile,
|
|
542
|
+
confidence,
|
|
543
|
+
suggested_layer: "api",
|
|
544
|
+
suggested_element_type: "operation",
|
|
545
|
+
suggested_name: suggestedName,
|
|
546
|
+
suggested_id_fragment: suggestedIdFragment,
|
|
547
|
+
http_method: httpMethod,
|
|
548
|
+
http_path: httpPath,
|
|
549
|
+
handler_qualified_name: handlerQualifiedName,
|
|
550
|
+
source_symbol: sourceSymbol,
|
|
551
|
+
source_start_line: sourceStartLine,
|
|
552
|
+
source_end_line: sourceEndLine,
|
|
553
|
+
source_reference: {
|
|
554
|
+
provenance: "extracted",
|
|
555
|
+
locations: sourceFile ? [
|
|
556
|
+
{
|
|
557
|
+
file: sourceFile,
|
|
558
|
+
symbol: sourceSymbol || void 0
|
|
559
|
+
}
|
|
560
|
+
] : void 0
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Check if a candidate (endpoint, service, or datastore) is in test code
|
|
566
|
+
*
|
|
567
|
+
* Applies the test_code_exclusion filtering rule from the analyzer mapping.
|
|
568
|
+
* If the configured regex pattern is invalid, warns the user once and falls back to defaults.
|
|
569
|
+
* Uses a one-shot flag to ensure the warning is only emitted once even if called in a loop.
|
|
570
|
+
*
|
|
571
|
+
* Accepts any object with a source_file property (structural typing), allowing it to work
|
|
572
|
+
* with EndpointCandidate, ServiceCandidate, DatastoreCandidate, and temporary objects.
|
|
573
|
+
*
|
|
574
|
+
* @private
|
|
575
|
+
*/
|
|
576
|
+
/**
|
|
577
|
+
* Build a low-confidence EndpointCandidate from a client-side API function node.
|
|
578
|
+
* Used as a fallback when no server-side Route nodes are found (ISSUE-003).
|
|
579
|
+
*/
|
|
580
|
+
shapeClientSideEndpoint(node, projectRoot) {
|
|
581
|
+
const name = String(node.name ?? node.id ?? "");
|
|
582
|
+
if (!name)
|
|
583
|
+
return null;
|
|
584
|
+
let sourceFile = String(node.file_path ?? "");
|
|
585
|
+
if (sourceFile && projectRoot)
|
|
586
|
+
try {
|
|
587
|
+
sourceFile = path.relative(projectRoot, sourceFile);
|
|
588
|
+
} catch {
|
|
589
|
+
}
|
|
590
|
+
if (!sourceFile)
|
|
591
|
+
return null;
|
|
592
|
+
const httpMethod = inferHttpMethodFromFunctionName(name), httpPath = inferHttpPathFromFunctionName(name), suggestedName = toKebabCase(name);
|
|
593
|
+
return {
|
|
594
|
+
source_file: sourceFile,
|
|
595
|
+
confidence: "low",
|
|
596
|
+
suggested_layer: "api",
|
|
597
|
+
suggested_element_type: "operation",
|
|
598
|
+
suggested_name: suggestedName,
|
|
599
|
+
suggested_id_fragment: suggestedName,
|
|
600
|
+
http_method: httpMethod,
|
|
601
|
+
http_path: httpPath,
|
|
602
|
+
handler_qualified_name: name,
|
|
603
|
+
source_symbol: name,
|
|
604
|
+
source_start_line: Number(node.properties?.start_line ?? 0),
|
|
605
|
+
source_end_line: Number(node.properties?.end_line ?? 0),
|
|
606
|
+
source_reference: {
|
|
607
|
+
provenance: "inferred",
|
|
608
|
+
locations: [{ file: sourceFile, symbol: name }]
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
isTestCode(candidate) {
|
|
613
|
+
const testCodeRule = this.mapper.getFilteringRules().find((rule) => rule.name === "test_code_exclusion" && rule.enabled);
|
|
614
|
+
if (!testCodeRule || !testCodeRule.pattern)
|
|
615
|
+
return CbmAnalyzer.DEFAULT_TEST_PATTERNS.some((pattern) => pattern.test(candidate.source_file));
|
|
616
|
+
try {
|
|
617
|
+
return new RegExp(testCodeRule.pattern).test(candidate.source_file);
|
|
618
|
+
} catch (error) {
|
|
619
|
+
if (!this.testCodePatternWarned) {
|
|
620
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
621
|
+
handleWarning("Invalid test_code_exclusion regex pattern in analyzer mapping", [
|
|
622
|
+
`Pattern: ${testCodeRule.pattern}`,
|
|
623
|
+
`Error: ${errorMsg}`,
|
|
624
|
+
"Falling back to default test code patterns"
|
|
625
|
+
]), this.testCodePatternWarned = !0;
|
|
626
|
+
}
|
|
627
|
+
return CbmAnalyzer.DEFAULT_TEST_PATTERNS.some((pattern) => pattern.test(candidate.source_file));
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Query for services/components in the indexed project
|
|
632
|
+
*
|
|
633
|
+
* Searches for application-layer nodes using the mapping-loader-driven pattern.
|
|
634
|
+
* For each node, evaluates all promotion_heuristics and records which fire in
|
|
635
|
+
* qualifying_heuristics. Drops candidates with zero qualifying heuristics (unless
|
|
636
|
+
* is_entry_point heuristic fires, adding it to qualifying_heuristics). Caps confidence
|
|
637
|
+
* at "medium" and excludes test files.
|
|
638
|
+
*
|
|
639
|
+
* @param projectRoot Absolute path to the project root
|
|
640
|
+
* @returns Array of service candidates with qualifying heuristics populated
|
|
641
|
+
* @throws CLIError if the project is not indexed
|
|
642
|
+
*/
|
|
643
|
+
async services(projectRoot) {
|
|
644
|
+
const status = await this.status(projectRoot);
|
|
645
|
+
if (!status.indexed)
|
|
646
|
+
throw new CLIError(`Project not indexed: ${projectRoot}`, ErrorCategory.NOT_FOUND, [
|
|
647
|
+
"Run `dr analyzer index` to index the project",
|
|
648
|
+
"Ensure codebase-memory-mcp is installed and working"
|
|
649
|
+
]);
|
|
650
|
+
const detection = status.detected;
|
|
651
|
+
if (!detection.installed)
|
|
652
|
+
throw new CLIError("CBM analyzer not installed", ErrorCategory.NOT_FOUND, ["Install codebase-memory-mcp: npm install -g codebase-memory-mcp"]);
|
|
653
|
+
const client = new StdioClient();
|
|
654
|
+
try {
|
|
655
|
+
client.spawn(detection.binary_path);
|
|
656
|
+
const version = await getCliVersion();
|
|
657
|
+
await client.initialize({
|
|
658
|
+
name: "dr-cli",
|
|
659
|
+
version
|
|
660
|
+
});
|
|
661
|
+
const nodeLabels = this.mapper.getNodeLabels(), applicationLabels = [];
|
|
662
|
+
for (const label of nodeLabels) {
|
|
663
|
+
const mapping = this.mapper.getNodeMapping(label);
|
|
664
|
+
mapping && mapping.dr_layer === "application" && applicationLabels.push(label);
|
|
665
|
+
}
|
|
666
|
+
if (applicationLabels.length === 0)
|
|
667
|
+
return handleWarning("No application-layer labels found in analyzer mapping", [
|
|
668
|
+
"Check that the analyzer is properly configured with application-layer node mappings",
|
|
669
|
+
"Services detection requires at least one mapped application-layer node type",
|
|
670
|
+
"Returning empty services list"
|
|
671
|
+
]), [];
|
|
672
|
+
const candidates = [], projectName = await this.resolveProjectName(client, projectRoot), labelPriority = { Class: 0, Module: 1 };
|
|
673
|
+
applicationLabels.sort((a, b) => (labelPriority[a] ?? 99) - (labelPriority[b] ?? 99));
|
|
674
|
+
for (const label of applicationLabels) {
|
|
675
|
+
const searchResponse = await client.invokeTool("search_graph", {
|
|
676
|
+
label,
|
|
677
|
+
project: projectName
|
|
678
|
+
}), nodes = this.validateSearchResponse(searchResponse), nodeMapping = this.mapper.getNodeMapping(label);
|
|
679
|
+
if (!nodeMapping)
|
|
680
|
+
continue;
|
|
681
|
+
const promotionHeuristicNames = nodeMapping.promotion_heuristics || [];
|
|
682
|
+
for (const node of nodes) {
|
|
683
|
+
const candidate = await this.transformNodeToService(node, nodeMapping, projectRoot, promotionHeuristicNames);
|
|
684
|
+
candidate.qualifying_heuristics.length > 0 && !this.isTestCode(candidate) && candidates.push(candidate);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const seenFiles = /* @__PURE__ */ new Set(), deduped = candidates.filter((c) => c.source_file ? seenFiles.has(c.source_file) ? !1 : (seenFiles.add(c.source_file), !0) : !0);
|
|
688
|
+
return this.warnOnDuplicateIdFragments(deduped, (c) => c.suggested_id_fragment, (c) => c.source_symbol || c.source_file || "(unknown)"), deduped;
|
|
689
|
+
} finally {
|
|
690
|
+
client.close();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Transform a CBM graph node to a service candidate
|
|
695
|
+
*
|
|
696
|
+
* Evaluates all promotion heuristics against the node's properties and records
|
|
697
|
+
* which heuristics fire. Drops candidates where no heuristics fire (unless the
|
|
698
|
+
* is_entry_point heuristic fires, which adds it to qualifying_heuristics).
|
|
699
|
+
* Caps confidence at "medium".
|
|
700
|
+
*
|
|
701
|
+
* @private
|
|
702
|
+
*/
|
|
703
|
+
async transformNodeToService(node, mapping, projectRoot, promotionHeuristicNames) {
|
|
704
|
+
const properties = node.properties ?? {}, suggestedName = toKebabCase(String(properties.name ?? node.name ?? node.id ?? "")), suggestedIdFragment = suggestedName;
|
|
705
|
+
let sourceFile = node.file_path ?? "";
|
|
706
|
+
if (sourceFile && projectRoot)
|
|
707
|
+
try {
|
|
708
|
+
sourceFile = path.relative(projectRoot, sourceFile);
|
|
709
|
+
} catch (error) {
|
|
710
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
711
|
+
handleWarning(`Failed to compute relative path for ${String(node.name ?? node.id ?? "(unknown)")}`, [
|
|
712
|
+
`Error: ${errorMsg}`,
|
|
713
|
+
"Using absolute file path instead of relative"
|
|
714
|
+
]);
|
|
715
|
+
}
|
|
716
|
+
const qualifiedName = String(properties.qualified_name ?? node.qualified_name ?? node.name ?? node.id ?? ""), fanIn = typeof properties.fan_in == "number" ? properties.fan_in : 0, fanOut = typeof properties.fan_out == "number" ? properties.fan_out : 0, qualifyingHeuristics = [];
|
|
717
|
+
let isEntryPoint = !1;
|
|
718
|
+
for (const heuristicName of promotionHeuristicNames) {
|
|
719
|
+
const heuristic = this.mapper.getHeuristic(heuristicName);
|
|
720
|
+
if (!heuristic) {
|
|
721
|
+
this.warnedHeuristics.has(heuristicName) || (this.warnedHeuristics.add(heuristicName), handleWarning(`Promotion heuristic "${heuristicName}" not found in analyzer mapping`, [
|
|
722
|
+
"Check the mapping configuration for typos",
|
|
723
|
+
`Available heuristics: ${getKnownHeuristicNames().join(", ")}`,
|
|
724
|
+
"This heuristic will be skipped for service promotion"
|
|
725
|
+
]));
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
this.evaluateHeuristic(heuristic, node, sourceFile) && (qualifyingHeuristics.push(heuristicName), heuristicName === "is_entry_point" && (isEntryPoint = !0));
|
|
729
|
+
}
|
|
730
|
+
if (!(qualifyingHeuristics.length > 0 || isEntryPoint))
|
|
731
|
+
return {
|
|
732
|
+
suggested_layer: "application",
|
|
733
|
+
suggested_element_type: mapping.dr_element_type_promoted === "applicationcomponent" ? "applicationcomponent" : "applicationservice",
|
|
734
|
+
suggested_id_fragment: suggestedIdFragment,
|
|
735
|
+
suggested_name: suggestedName,
|
|
736
|
+
source_file: sourceFile,
|
|
737
|
+
source_symbol: String(properties.name ?? node.name ?? node.id ?? ""),
|
|
738
|
+
qualified_name: qualifiedName,
|
|
739
|
+
qualifying_heuristics: [],
|
|
740
|
+
confidence: "low",
|
|
741
|
+
fan_in: fanIn,
|
|
742
|
+
fan_out: fanOut
|
|
743
|
+
};
|
|
744
|
+
const confidence = capConfidence(mapping.confidence);
|
|
745
|
+
return {
|
|
746
|
+
suggested_layer: "application",
|
|
747
|
+
suggested_element_type: mapping.dr_element_type_promoted === "applicationcomponent" ? "applicationcomponent" : "applicationservice",
|
|
748
|
+
suggested_id_fragment: suggestedIdFragment,
|
|
749
|
+
suggested_name: suggestedName,
|
|
750
|
+
source_file: sourceFile,
|
|
751
|
+
source_symbol: String(properties.name ?? node.name ?? node.id ?? ""),
|
|
752
|
+
qualified_name: qualifiedName,
|
|
753
|
+
qualifying_heuristics: qualifyingHeuristics,
|
|
754
|
+
confidence,
|
|
755
|
+
fan_in: fanIn,
|
|
756
|
+
fan_out: fanOut
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Evaluate a single heuristic against a node
|
|
761
|
+
*
|
|
762
|
+
* Checks if the heuristic's conditions are met based on the node's properties
|
|
763
|
+
* and file path. Unknown heuristics are logged with a warning rather than
|
|
764
|
+
* silently ignored, to support dynamic heuristics from cbm.json.
|
|
765
|
+
*
|
|
766
|
+
* Warnings for unknown heuristics are deduplicated - each heuristic name is warned
|
|
767
|
+
* about only once, even if evaluated against many nodes.
|
|
768
|
+
*
|
|
769
|
+
* @private
|
|
770
|
+
*/
|
|
771
|
+
evaluateHeuristic(heuristic, node, sourceFile) {
|
|
772
|
+
const properties = node.properties ?? {}, result = evaluateHeuristic(heuristic, properties, sourceFile);
|
|
773
|
+
return heuristic.name && !getKnownHeuristicNames().includes(heuristic.name) && (this.warnedHeuristics.has(heuristic.name) || (handleWarning(`Unknown heuristic type: ${heuristic.name}`, [
|
|
774
|
+
"The heuristic is defined in cbm.json but not implemented in heuristic-utils.ts",
|
|
775
|
+
"This heuristic will be treated as not met (returning false)",
|
|
776
|
+
"Add support for this heuristic to evaluateHeuristic() if it should fire"
|
|
777
|
+
]), this.warnedHeuristics.add(heuristic.name))), result;
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Match a file path against a glob-like pattern
|
|
781
|
+
*
|
|
782
|
+
* @param filePath The file path to match
|
|
783
|
+
* @param pattern The glob pattern
|
|
784
|
+
* @returns True if the pattern matches, false otherwise
|
|
785
|
+
* @private
|
|
786
|
+
*/
|
|
787
|
+
matchPattern(filePath, pattern) {
|
|
788
|
+
return matchPattern(filePath, pattern);
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Query for datastores/databases inferred from code analysis
|
|
792
|
+
*
|
|
793
|
+
* Applies datastore_detection heuristic rules to aggregate signals by file/module.
|
|
794
|
+
* Matches import target names against import_patterns and function/symbol names against
|
|
795
|
+
* naming_indicators from the heuristic parameters. All candidates have confidence "low".
|
|
796
|
+
*
|
|
797
|
+
* @param projectRoot Absolute path to the project root
|
|
798
|
+
* @returns Array of datastore candidates aggregated by inferred datastore
|
|
799
|
+
* @throws CLIError if the project is not indexed
|
|
800
|
+
*/
|
|
801
|
+
async datastores(projectRoot) {
|
|
802
|
+
const status = await this.status(projectRoot);
|
|
803
|
+
if (!status.indexed)
|
|
804
|
+
throw new CLIError(`Project not indexed: ${projectRoot}`, ErrorCategory.NOT_FOUND, [
|
|
805
|
+
"Run `dr analyzer index` to index the project",
|
|
806
|
+
"Ensure codebase-memory-mcp is installed and working"
|
|
807
|
+
]);
|
|
808
|
+
const detection = status.detected;
|
|
809
|
+
if (!detection.installed)
|
|
810
|
+
throw new CLIError("CBM analyzer not installed", ErrorCategory.NOT_FOUND, ["Install codebase-memory-mcp: npm install -g codebase-memory-mcp"]);
|
|
811
|
+
const client = new StdioClient();
|
|
812
|
+
try {
|
|
813
|
+
client.spawn(detection.binary_path);
|
|
814
|
+
const version = await getCliVersion();
|
|
815
|
+
await client.initialize({
|
|
816
|
+
name: "dr-cli",
|
|
817
|
+
version
|
|
818
|
+
});
|
|
819
|
+
const datastoreHeuristic = this.mapper.getHeuristic("datastore_detection");
|
|
820
|
+
if (!datastoreHeuristic)
|
|
821
|
+
throw new CLIError("datastore_detection heuristic not found in analyzer", ErrorCategory.VALIDATION, [
|
|
822
|
+
"The analyzer configuration is missing the datastore_detection heuristic",
|
|
823
|
+
"Run `npm run build:spec` to recompile the analyzer artifacts"
|
|
824
|
+
]);
|
|
825
|
+
const importPatterns = datastoreHeuristic.parameters?.patterns ?? [], namingIndicators = datastoreHeuristic.parameters?.naming_indicators ?? [], datastoreSignals = /* @__PURE__ */ new Map(), datastoreFirstSource = /* @__PURE__ */ new Map(), projectName = await this.resolveProjectName(client, projectRoot);
|
|
826
|
+
let allNodes = [];
|
|
827
|
+
try {
|
|
828
|
+
const queryResponse = await client.invokeTool("query_graph", {
|
|
829
|
+
query: `
|
|
830
|
+
MATCH (n1)-[r:IMPORTS]->(n2)
|
|
831
|
+
RETURN DISTINCT n1 as node
|
|
832
|
+
UNION
|
|
833
|
+
MATCH (n1)-[r:IMPORTS]->(n2)
|
|
834
|
+
RETURN DISTINCT n2 as node
|
|
835
|
+
`,
|
|
836
|
+
project: projectName
|
|
837
|
+
});
|
|
838
|
+
queryResponse && typeof queryResponse == "object" && Array.isArray(queryResponse.records) && (allNodes = (queryResponse.records || []).map((r) => r.node).filter((n) => n && typeof n == "object"));
|
|
839
|
+
} catch (error) {
|
|
840
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
841
|
+
handleWarning("query_graph tool failed; falling back to search_graph for datastore detection", [
|
|
842
|
+
`Error: ${errorMsg}`,
|
|
843
|
+
"Using search_graph will return ALL nodes instead of only IMPORTS-connected nodes",
|
|
844
|
+
"Datastore detection results may include false positives or be less accurate",
|
|
845
|
+
"Ensure query_graph is supported by your analyzer version for best results"
|
|
846
|
+
]);
|
|
847
|
+
const nodeLabels = this.mapper.getNodeLabels();
|
|
848
|
+
for (const label of nodeLabels) {
|
|
849
|
+
const searchResponse = await client.invokeTool("search_graph", {
|
|
850
|
+
label,
|
|
851
|
+
project: projectName
|
|
852
|
+
}), nodes = this.validateSearchResponse(searchResponse);
|
|
853
|
+
allNodes.push(...nodes);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
for (const node of allNodes) {
|
|
857
|
+
const filePath = node.file_path ?? "", importTargetName = String(node.label ?? node.properties?.name ?? node.id ?? "").toLowerCase();
|
|
858
|
+
let matchedImportPattern;
|
|
859
|
+
for (const pattern of importPatterns)
|
|
860
|
+
if (this.matchPattern(importTargetName, pattern)) {
|
|
861
|
+
matchedImportPattern = pattern;
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
const nodeName = String(node.properties?.name ?? "").toLowerCase(), matchedIndicators = namingIndicators.filter((indicator) => nodeName.includes(indicator.toLowerCase()));
|
|
865
|
+
if (!matchedImportPattern && matchedIndicators.length === 0)
|
|
866
|
+
continue;
|
|
867
|
+
let datastoreName = this.inferDatastoreName(filePath, node, matchedImportPattern, importTargetName);
|
|
868
|
+
if (!datastoreName)
|
|
869
|
+
continue;
|
|
870
|
+
datastoreName = toKebabCase(datastoreName), datastoreSignals.has(datastoreName) ? datastoreFirstSource.get(datastoreName) !== importTargetName && handleWarning(`Multiple imports normalize to datastore name "${datastoreName}"`, [
|
|
871
|
+
`First: ${datastoreFirstSource.get(datastoreName)}`,
|
|
872
|
+
`Conflict: ${importTargetName}`,
|
|
873
|
+
'Use "dr add" with a custom name to distinguish them'
|
|
874
|
+
]) : (datastoreSignals.set(datastoreName, []), datastoreFirstSource.set(datastoreName, importTargetName));
|
|
875
|
+
const signals = datastoreSignals.get(datastoreName);
|
|
876
|
+
let relativeFile = filePath;
|
|
877
|
+
if (filePath && projectRoot)
|
|
878
|
+
try {
|
|
879
|
+
relativeFile = path.relative(projectRoot, filePath);
|
|
880
|
+
} catch (error) {
|
|
881
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
882
|
+
handleWarning("Failed to compute relative path in datastore detection", [
|
|
883
|
+
`File: ${filePath}`,
|
|
884
|
+
`Error: ${errorMsg}`,
|
|
885
|
+
"Using absolute file path instead of relative"
|
|
886
|
+
]);
|
|
887
|
+
}
|
|
888
|
+
signals.push({
|
|
889
|
+
sourceFile: relativeFile,
|
|
890
|
+
importPattern: matchedImportPattern,
|
|
891
|
+
functionPatterns: matchedIndicators
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
const candidates = [], entriesArray = Array.from(datastoreSignals.entries());
|
|
895
|
+
for (const [datastoreName, signals] of entriesArray) {
|
|
896
|
+
const inferredFromMap = /* @__PURE__ */ new Map();
|
|
897
|
+
for (const signal of signals) {
|
|
898
|
+
inferredFromMap.has(signal.sourceFile) || inferredFromMap.set(signal.sourceFile, {
|
|
899
|
+
importPatterns: /* @__PURE__ */ new Set(),
|
|
900
|
+
functionPatterns: /* @__PURE__ */ new Set()
|
|
901
|
+
});
|
|
902
|
+
const entry = inferredFromMap.get(signal.sourceFile);
|
|
903
|
+
signal.importPattern && entry.importPatterns.add(signal.importPattern), signal.functionPatterns.forEach((fp) => entry.functionPatterns.add(fp));
|
|
904
|
+
}
|
|
905
|
+
let inferredFromMapEntries = Array.from(inferredFromMap.entries());
|
|
906
|
+
if (inferredFromMapEntries = inferredFromMapEntries.filter(([sourceFile]) => {
|
|
907
|
+
const tempCandidate = {
|
|
908
|
+
source_file: sourceFile
|
|
909
|
+
};
|
|
910
|
+
return !this.isTestCode(tempCandidate);
|
|
911
|
+
}), inferredFromMapEntries.length === 0)
|
|
912
|
+
continue;
|
|
913
|
+
const inferredFrom = inferredFromMapEntries.map(([sourceFile, evidence]) => ({
|
|
914
|
+
source_file: sourceFile,
|
|
915
|
+
import_pattern: Array.from(evidence.importPatterns).join(", ") || datastoreName,
|
|
916
|
+
function_patterns: Array.from(evidence.functionPatterns)
|
|
917
|
+
}));
|
|
918
|
+
candidates.push({
|
|
919
|
+
suggested_layer: "data-store",
|
|
920
|
+
suggested_name: datastoreName,
|
|
921
|
+
inferred_from: inferredFrom,
|
|
922
|
+
confidence: "low",
|
|
923
|
+
notes: `Inferred from ${inferredFrom.length} source file(s) based on datastore detection heuristics`
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
const browserStorageTypes = ["local-storage", "session-storage"], browserStorageSignals = /* @__PURE__ */ new Map();
|
|
927
|
+
try {
|
|
928
|
+
const storageResp = await client.invokeTool("query_graph", {
|
|
929
|
+
query: `
|
|
930
|
+
MATCH (n:Function)
|
|
931
|
+
WHERE n.file_path CONTAINS 'Store' OR n.file_path CONTAINS 'store'
|
|
932
|
+
OR n.file_path CONTAINS 'storage' OR n.file_path CONTAINS 'Storage'
|
|
933
|
+
OR n.file_path CONTAINS 'auth' OR n.file_path CONTAINS 'Auth'
|
|
934
|
+
OR n.file_path CONTAINS 'preference' OR n.file_path CONTAINS 'Preference'
|
|
935
|
+
OR n.file_path CONTAINS 'layout' OR n.file_path CONTAINS 'Layout'
|
|
936
|
+
OR n.file_path CONTAINS 'settings' OR n.file_path CONTAINS 'Settings'
|
|
937
|
+
RETURN n
|
|
938
|
+
`,
|
|
939
|
+
project: projectName
|
|
940
|
+
});
|
|
941
|
+
if (storageResp && typeof storageResp == "object") {
|
|
942
|
+
const records = storageResp.records ?? [];
|
|
943
|
+
for (const record of records) {
|
|
944
|
+
const node = record.n;
|
|
945
|
+
if (!node?.file_path)
|
|
946
|
+
continue;
|
|
947
|
+
let relFile = node.file_path;
|
|
948
|
+
try {
|
|
949
|
+
relFile = path.relative(projectRoot, node.file_path);
|
|
950
|
+
} catch {
|
|
951
|
+
}
|
|
952
|
+
const storageKey = /session/i.test(relFile) ? "session-storage" : "local-storage";
|
|
953
|
+
browserStorageSignals.has(storageKey) || browserStorageSignals.set(storageKey, /* @__PURE__ */ new Set()), browserStorageSignals.get(storageKey).add(relFile);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
} catch {
|
|
957
|
+
}
|
|
958
|
+
for (const storageType of browserStorageTypes) {
|
|
959
|
+
const sourceFiles = browserStorageSignals.get(storageType);
|
|
960
|
+
if (!sourceFiles || sourceFiles.size === 0)
|
|
961
|
+
continue;
|
|
962
|
+
const accessPattern = storageType === "local-storage" ? "localStorage" : "sessionStorage", inferredFrom = Array.from(sourceFiles).filter((f) => !this.isTestCode({ source_file: f })).map((f) => ({
|
|
963
|
+
source_file: f,
|
|
964
|
+
import_pattern: accessPattern,
|
|
965
|
+
function_patterns: []
|
|
966
|
+
}));
|
|
967
|
+
inferredFrom.length > 0 && candidates.push({
|
|
968
|
+
suggested_layer: "data-store",
|
|
969
|
+
suggested_name: storageType,
|
|
970
|
+
inferred_from: inferredFrom,
|
|
971
|
+
confidence: "low",
|
|
972
|
+
notes: `Inferred from ${inferredFrom.length} source file(s) using browser ${accessPattern} (global API, no import required)`
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
return candidates;
|
|
976
|
+
} finally {
|
|
977
|
+
client.close();
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Infer a datastore name from import target name, file path, node properties, or import pattern
|
|
982
|
+
*
|
|
983
|
+
* @private
|
|
984
|
+
*/
|
|
985
|
+
inferDatastoreName(filePath, node, matchedPattern, importTargetName) {
|
|
986
|
+
if (importTargetName && matchedPattern && this.matchPattern(importTargetName, matchedPattern))
|
|
987
|
+
return importTargetName;
|
|
988
|
+
if (matchedPattern) {
|
|
989
|
+
const match = matchedPattern.match(/[a-z]+/i);
|
|
990
|
+
if (match)
|
|
991
|
+
return match[0];
|
|
992
|
+
}
|
|
993
|
+
const fileName = path.basename(filePath);
|
|
994
|
+
if (fileName.includes("migration") || fileName.includes("schema")) {
|
|
995
|
+
const match = fileName.match(/^([a-z0-9_-]+)/i);
|
|
996
|
+
if (match && match[1])
|
|
997
|
+
return match[1];
|
|
998
|
+
}
|
|
999
|
+
const nodeName = String(node.label ?? node.id ?? "");
|
|
1000
|
+
if (nodeName) {
|
|
1001
|
+
const nodeMapping = this.mapper.getNodeMapping(nodeName);
|
|
1002
|
+
if (nodeMapping && nodeMapping.dr_layer !== "api" && nodeMapping.dr_layer !== "application" || nodeName.toLowerCase().includes("database") || nodeName.toLowerCase().includes("repository") || nodeName.toLowerCase().includes("datastore"))
|
|
1003
|
+
return nodeName;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Query for callers of a specific function or symbol
|
|
1008
|
+
*
|
|
1009
|
+
* Delegates to trace_path MCP tool with direction="inbound" to find
|
|
1010
|
+
* all functions that call the specified qualified name. Applies depth clamping
|
|
1011
|
+
* (default 3, max 10) and transforms CBM response nodes into CallGraphNode objects
|
|
1012
|
+
* using edge-type mappings from the analyzer.
|
|
1013
|
+
*
|
|
1014
|
+
* @param projectRoot Absolute path to the project root
|
|
1015
|
+
* @param qualifiedName Fully qualified symbol name (e.g., "com.example.UserService.getUser")
|
|
1016
|
+
* @param depth Maximum traversal depth (default 3, clamped to max 10)
|
|
1017
|
+
* @returns Array of callers in the call graph
|
|
1018
|
+
* @throws CLIError if project not indexed or analyzer not installed
|
|
1019
|
+
*/
|
|
1020
|
+
async callers(projectRoot, qualifiedName, depth) {
|
|
1021
|
+
return this.traceCallPath(projectRoot, qualifiedName, depth, "inbound");
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Query for callees of a specific function or symbol
|
|
1025
|
+
*
|
|
1026
|
+
* Delegates to trace_path MCP tool with direction="outbound" to find
|
|
1027
|
+
* all functions called by the specified qualified name. Applies depth clamping
|
|
1028
|
+
* (default 3, max 10) and transforms CBM response nodes into CallGraphNode objects
|
|
1029
|
+
* using edge-type mappings from the analyzer.
|
|
1030
|
+
*
|
|
1031
|
+
* @param projectRoot Absolute path to the project root
|
|
1032
|
+
* @param qualifiedName Fully qualified symbol name (e.g., "com.example.UserService.getUser")
|
|
1033
|
+
* @param depth Maximum traversal depth (default 3, clamped to max 10)
|
|
1034
|
+
* @returns Array of callees in the call graph
|
|
1035
|
+
* @throws CLIError if project not indexed or analyzer not installed
|
|
1036
|
+
*/
|
|
1037
|
+
async callees(projectRoot, qualifiedName, depth) {
|
|
1038
|
+
return this.traceCallPath(projectRoot, qualifiedName, depth, "outbound");
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Cypher fallback for call graph traversal when trace_path is unavailable.
|
|
1042
|
+
*
|
|
1043
|
+
* Uses query_graph directly with CALLS-edge Cypher when trace_path returns a
|
|
1044
|
+
* non-array nodes field (i.e. is not implemented in the binary).
|
|
1045
|
+
*
|
|
1046
|
+
* @private
|
|
1047
|
+
*/
|
|
1048
|
+
async callGraphViaCypher(binaryPath, projectName, qualifiedName, direction, depth, projectRoot) {
|
|
1049
|
+
const safeQN = qualifiedName.replace(/'/g, "''"), limit = Math.min(depth * 20, 100), cypher = direction === "inbound" ? `MATCH (caller)-[:CALLS]->(target) WHERE target.qualified_name = '${safeQN}' OR target.name = '${safeQN}' RETURN caller.id AS id, caller.qualified_name AS qualified_name, caller.file_path AS file_path LIMIT ${limit}` : `MATCH (source)-[:CALLS]->(callee) WHERE source.qualified_name = '${safeQN}' OR source.name = '${safeQN}' RETURN callee.id AS id, callee.qualified_name AS qualified_name, callee.file_path AS file_path LIMIT ${limit}`;
|
|
1050
|
+
depth > 1 && handleWarning(`Cypher CALLS fallback: depth ${depth} requested but fallback only traverses 1 hop`, ["Multi-hop traversal requires trace_path support in the analyzer binary"]);
|
|
1051
|
+
const client = new StdioClient();
|
|
1052
|
+
try {
|
|
1053
|
+
client.spawn(binaryPath);
|
|
1054
|
+
const version = await getCliVersion();
|
|
1055
|
+
await client.initialize({ name: "dr-cli", version });
|
|
1056
|
+
const queryResponse = await client.invokeTool("query_graph", {
|
|
1057
|
+
query: cypher,
|
|
1058
|
+
project: projectName
|
|
1059
|
+
}), cols = queryResponse?.columns ?? [];
|
|
1060
|
+
return (queryResponse?.rows ?? []).map((row) => {
|
|
1061
|
+
const obj = {};
|
|
1062
|
+
return cols.forEach((col, i) => {
|
|
1063
|
+
const key = col.includes(".") ? col.slice(col.lastIndexOf(".") + 1) : col;
|
|
1064
|
+
obj[key] = row[i];
|
|
1065
|
+
}), shapeCallGraphNode(obj, projectRoot);
|
|
1066
|
+
});
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1069
|
+
return handleWarning("Cypher CALLS fallback failed \u2014 returning empty call graph", [
|
|
1070
|
+
`Error: ${errorMsg}`,
|
|
1071
|
+
"Neither trace_path nor query_graph is available for call graph traversal"
|
|
1072
|
+
]), [];
|
|
1073
|
+
} finally {
|
|
1074
|
+
client.close();
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Internal helper to trace call paths in either direction
|
|
1079
|
+
*
|
|
1080
|
+
* Handles common logic for callers() and callees(): pre-flight checks,
|
|
1081
|
+
* depth clamping, tool invocation, and response transformation.
|
|
1082
|
+
*
|
|
1083
|
+
* @private
|
|
1084
|
+
*/
|
|
1085
|
+
async traceCallPath(projectRoot, qualifiedName, depth, direction) {
|
|
1086
|
+
const status = await this.status(projectRoot);
|
|
1087
|
+
if (!status.indexed)
|
|
1088
|
+
throw new CLIError(`Project not indexed: ${projectRoot}`, ErrorCategory.NOT_FOUND, [
|
|
1089
|
+
"Run `dr analyzer index` to index the project",
|
|
1090
|
+
"Ensure codebase-memory-mcp is installed and working"
|
|
1091
|
+
]);
|
|
1092
|
+
const detection = status.detected;
|
|
1093
|
+
if (!detection.installed)
|
|
1094
|
+
throw new CLIError("CBM analyzer not installed", ErrorCategory.NOT_FOUND, ["Install codebase-memory-mcp: npm install -g codebase-memory-mcp"]);
|
|
1095
|
+
const clampedDepth = clampDepth(depth), client = new StdioClient();
|
|
1096
|
+
try {
|
|
1097
|
+
client.spawn(detection.binary_path);
|
|
1098
|
+
const version = await getCliVersion();
|
|
1099
|
+
await client.initialize({
|
|
1100
|
+
name: "dr-cli",
|
|
1101
|
+
version
|
|
1102
|
+
});
|
|
1103
|
+
const projectName = await this.resolveProjectName(client, projectRoot), traceResponse = await client.invokeTool("trace_path", {
|
|
1104
|
+
function_name: qualifiedName,
|
|
1105
|
+
direction,
|
|
1106
|
+
depth: clampedDepth,
|
|
1107
|
+
project: projectName
|
|
1108
|
+
});
|
|
1109
|
+
if (!traceResponse || typeof traceResponse != "object")
|
|
1110
|
+
throw new CLIError("Invalid response from trace_path", ErrorCategory.SYSTEM, ["Ensure codebase-memory-mcp is properly installed and functional"]);
|
|
1111
|
+
if (!Array.isArray(traceResponse.nodes))
|
|
1112
|
+
return this.callGraphViaCypher(detection.binary_path, projectName, qualifiedName, direction, clampedDepth, projectRoot);
|
|
1113
|
+
if (!Array.isArray(traceResponse.edges))
|
|
1114
|
+
return handleWarning("trace_path returned no edges array \u2014 returning empty call graph", [
|
|
1115
|
+
`Expected array, got ${typeof traceResponse.edges}`,
|
|
1116
|
+
"Ensure codebase-memory-mcp supports trace_path and the project is indexed"
|
|
1117
|
+
]), [];
|
|
1118
|
+
const nodes = traceResponse.nodes, edges = traceResponse.edges, validEdgeTypes = this.mapper.getEdgeTypes(), callGraphNodes = [];
|
|
1119
|
+
for (const node of nodes) {
|
|
1120
|
+
const nodeQualifiedName = node.qualified_name || node.id;
|
|
1121
|
+
let callGraphNode = shapeCallGraphNode(node, projectRoot);
|
|
1122
|
+
const nodeDepth = typeof node.depth == "number" ? node.depth : 0, edgeType = determineEdgeType(nodeDepth, edges, nodeQualifiedName, validEdgeTypes);
|
|
1123
|
+
callGraphNode.edge_type = edgeType, callGraphNodes.push(callGraphNode);
|
|
1124
|
+
}
|
|
1125
|
+
return callGraphNodes;
|
|
1126
|
+
} finally {
|
|
1127
|
+
client.close();
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Shape a single CBM graph node into a route object with defaults applied
|
|
1132
|
+
*
|
|
1133
|
+
* This is extracted to be testable - applies the defaulting logic used in verify()
|
|
1134
|
+
* to ensure empty/missing properties are handled consistently.
|
|
1135
|
+
*
|
|
1136
|
+
* @param node Raw node from CBM graph search
|
|
1137
|
+
* @returns Shaped route object with defaults applied
|
|
1138
|
+
* @internal
|
|
1139
|
+
*/
|
|
1140
|
+
shapeRoute(node) {
|
|
1141
|
+
const properties = node.properties ?? {}, nodeName = String(node.name ?? node.id ?? "");
|
|
1142
|
+
let httpMethod = "GET";
|
|
1143
|
+
if (typeof properties.method == "string")
|
|
1144
|
+
httpMethod = properties.method.toUpperCase();
|
|
1145
|
+
else {
|
|
1146
|
+
const extracted = String(node.qualified_name ?? "").match(/^__route__([A-Z]+)__/)?.[1] ?? "";
|
|
1147
|
+
extracted && VALID_HTTP_METHODS.includes(extracted) && (httpMethod = extracted);
|
|
1148
|
+
}
|
|
1149
|
+
const httpPath = typeof properties.path == "string" ? properties.path : typeof node.name == "string" && node.name.startsWith("/") ? node.name : "/", handler = String(properties.handler_name ?? ""), sourceFile = node.file_path ?? "", sourceSymbol = String(properties.symbol ?? properties.source_symbol ?? properties.handler_name ?? "");
|
|
1150
|
+
return {
|
|
1151
|
+
id: sourceFile && sourceSymbol ? `${sourceFile}:${sourceSymbol}` : nodeName || node.id || "(unknown)",
|
|
1152
|
+
http_method: httpMethod,
|
|
1153
|
+
http_path: httpPath,
|
|
1154
|
+
handler,
|
|
1155
|
+
source_file: sourceFile,
|
|
1156
|
+
source_symbol: sourceSymbol
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Verify that graph-discovered routes align with model endpoints
|
|
1161
|
+
*
|
|
1162
|
+
* Queries the CBM graph for Route nodes, shapes them via the Route mapping,
|
|
1163
|
+
* and delegates comparison to VerifyEngine. Entirely read-only.
|
|
1164
|
+
*
|
|
1165
|
+
* @param projectRoot Absolute path to the project root
|
|
1166
|
+
* @param options Verification options
|
|
1167
|
+
* @returns Comprehensive verification report
|
|
1168
|
+
* @throws CLIError if project not indexed or analyzer not installed
|
|
1169
|
+
*/
|
|
1170
|
+
async verify(projectRoot, options) {
|
|
1171
|
+
const status = await this.status(projectRoot);
|
|
1172
|
+
if (!status.indexed)
|
|
1173
|
+
throw new CLIError(`Project not indexed: ${projectRoot}`, ErrorCategory.NOT_FOUND, [
|
|
1174
|
+
"Run `dr analyzer index` to index the project",
|
|
1175
|
+
"Ensure codebase-memory-mcp is installed and working"
|
|
1176
|
+
]);
|
|
1177
|
+
const detection = status.detected;
|
|
1178
|
+
if (!detection.installed)
|
|
1179
|
+
throw new CLIError("CBM analyzer not installed", ErrorCategory.NOT_FOUND, ["Install codebase-memory-mcp: npm install -g codebase-memory-mcp"]);
|
|
1180
|
+
const routeMapping = this.mapper.getNodeMapping("Route");
|
|
1181
|
+
if (!routeMapping)
|
|
1182
|
+
throw new CLIError("Route mapping not found in analyzer", ErrorCategory.VALIDATION, ["Run `npm run build:spec` to recompile the analyzer artifacts"]);
|
|
1183
|
+
const client = new StdioClient();
|
|
1184
|
+
try {
|
|
1185
|
+
client.spawn(detection.binary_path);
|
|
1186
|
+
const version = await getCliVersion();
|
|
1187
|
+
await client.initialize({
|
|
1188
|
+
name: "dr-cli",
|
|
1189
|
+
version
|
|
1190
|
+
});
|
|
1191
|
+
const projectName = await this.resolveProjectName(client, projectRoot), searchResponse = await client.invokeTool("search_graph", {
|
|
1192
|
+
label: routeMapping.cbm_label,
|
|
1193
|
+
project: projectName
|
|
1194
|
+
}), routes = this.validateSearchResponse(searchResponse).filter((n) => /^__route__[A-Z]+__\//.test(String(n.qualified_name ?? ""))).filter((n) => isLikelyHttpRoute(String(n.qualified_name ?? ""))).map((node) => this.shapeRoute(node)).filter((r) => r.source_file), engine = new VerifyEngine(), indexMeta = status.index_meta, analyzerName = this.name;
|
|
1195
|
+
return await engine.computeReport(projectRoot, routes, options, analyzerName, indexMeta);
|
|
1196
|
+
} finally {
|
|
1197
|
+
client.close();
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
export {
|
|
1202
|
+
CbmAnalyzer,
|
|
1203
|
+
SOURCE_CODE_EXT_RE
|
|
1204
|
+
};
|
|
1205
|
+
//# sourceMappingURL=cbm-analyzer.js.map
|