@documentation-robotics/cli 0.1.0 → 0.1.5
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 +206 -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 +155 -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 +37 -13
- 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,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/analyzers/cbm-analyzer.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * CBM (Codebase Memory) Analyzer Backend\n *\n * Implements the AnalyzerBackend interface for the codebase-memory-mcp analyzer.\n * Handles detection, indexing, and querying of code-based metadata using the CBM MCP server.\n *\n * Key responsibilities:\n * - Detect CBM binary availability and MCP registration\n * - Manage project indexing with freshness tracking\n * - Query for endpoints and other code artifacts\n * - Transform CBM nodes to DR elements using mapping templates\n */\n\nimport { spawnSync } from \"child_process\";\nimport * as path from \"path\";\nimport { readFile } from \"fs/promises\";\nimport { CLIError, ErrorCategory, handleWarning } from \"../utils/errors.js\";\nimport type { AnalyzerBackend } from \"./base-analyzer.js\";\nimport type {\n AnalyzerStatus,\n DetectionResult,\n EndpointCandidate,\n IndexResult,\n IndexMeta,\n AnalyzerNodeMapping,\n AnalyzerHeuristic,\n HttpMethod,\n ServiceCandidate,\n DatastoreCandidate,\n CallGraphNode,\n VerifyOptions,\n VerifyReport,\n} from \"./types.js\";\nimport { VALID_HTTP_METHODS } from \"./types.js\";\nimport { StdioClient } from \"./stdio-client.js\";\nimport { readIndexMeta, writeIndexMeta } from \"./session-state.js\";\nimport type { MappingLoader } from \"./mapping-loader.js\";\nimport { getCliVersion } from \"./version.js\";\nimport { StagingAreaManager } from \"../core/staging-area.js\";\nimport { VerifyEngine } from \"./verify-engine.js\";\nimport { clampDepth, shapeCallGraphNode, determineEdgeType } from \"./call-graph-utils.js\";\nimport { evaluateHeuristic, capConfidence, matchPattern, getKnownHeuristicNames } from \"./heuristic-utils.js\";\nimport { toKebabCase } from \"../utils/id-generator.js\";\n\n/**\n * Graph node from CBM search results\n */\ninterface CbmGraphNode {\n id?: string;\n name?: string;\n qualified_name?: string;\n label: string;\n properties?: Record<string, unknown>;\n file_path?: string;\n [key: string]: unknown;\n}\n\n// Source-code file extensions that CBM sometimes mislabels as Route nodes when it\n// encounters string literals that look like paths (e.g. \"/src/app.ts\").\n// Only source-code extensions are listed \u2014 .json, .xml, .csv, .html etc. are\n// legitimate HTTP route suffixes and must NOT be filtered out.\nexport 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;\n\n/**\n * Returns true when a qualified_name looks like a real HTTP route rather than\n * a source-file path that the CBM indexer mislabelled as a Route node.\n */\nfunction isLikelyHttpRoute(qualifiedName: string): boolean {\n const httpPath = qualifiedName.replace(/^__route__[A-Z]+__/, \"\");\n // Reject source-code file paths, internal DR paths, and spec artifact paths\n // (e.g. /schemas/motivation/goal from test fixtures or openapi schema refs)\n return !SOURCE_CODE_EXT_RE.test(httpPath)\n && !httpPath.startsWith(\"/.dr/\")\n && !httpPath.startsWith(\"/schemas/\");\n}\n\n/**\n * Infer HTTP method from a function name using naming conventions.\n * Returns \"GET\" by default for ambiguous names.\n */\nfunction inferHttpMethodFromFunctionName(name: string): HttpMethod {\n const lower = name.toLowerCase();\n if (/^(create|add|post|submit|send)/.test(lower)) return \"POST\";\n if (/^(update|put|save|replace|set)/.test(lower)) return \"PUT\";\n if (/^(patch|merge)/.test(lower)) return \"PATCH\";\n if (/^(delete|remove|destroy)/.test(lower)) return \"DELETE\";\n return \"GET\";\n}\n\n/**\n * Infer an HTTP path from a function name by stripping the verb prefix and\n * converting the remainder to kebab-case.\n */\nfunction inferHttpPathFromFunctionName(name: string): string {\n const stripped = name.replace(/^(fetch|get|list|create|add|update|put|delete|remove|patch|merge|load|save|replace|post|submit|send|destroy)/i, \"\");\n const segment = toKebabCase(stripped || name);\n return segment ? `/${segment}` : \"/\";\n}\n\n/**\n * CBM Analyzer implementation\n */\nexport class CbmAnalyzer implements AnalyzerBackend {\n private mapper: MappingLoader;\n private testCodePatternWarned = false;\n private warnedHeuristics = new Set<string>();\n\n /**\n * Default regex patterns for test code detection\n */\n private static readonly DEFAULT_TEST_PATTERNS = [\n /\\.test\\./,\n /\\.spec\\./,\n /\\/tests?\\//,\n /\\/__tests__\\//,\n /\\/test-/,\n /\\/fixtures?\\//,\n /\\/__fixtures__\\//,\n ];\n\n constructor(mapper: MappingLoader) {\n this.mapper = mapper;\n }\n\n /**\n * Get the analyzer name\n */\n get name(): string {\n return this.mapper.getAnalyzerName();\n }\n\n /**\n * Get the analyzer display name\n */\n get displayName(): string {\n const metadata = this.mapper.getAnalyzerMetadata();\n return metadata?.display_name || this.mapper.getAnalyzerName();\n }\n\n /**\n * Check if the MCP server is registered in .mcp.json\n *\n * Reads .mcp.json from the project root and checks if the analyzer's MCP server\n * is registered there. This is extracted into a separate method for easier testing.\n *\n * @param projectRoot Absolute path to the project root\n * @returns True if the MCP server is registered, false otherwise\n * @throws CLIError if .mcp.json exists but is unreadable or malformed\n */\n async checkMcpRegistration(projectRoot: string): Promise<boolean> {\n try {\n const mcpJsonPath = path.join(projectRoot, \".mcp.json\");\n const mcpContent = await readFile(mcpJsonPath, \"utf-8\");\n const mcpConfig = JSON.parse(mcpContent);\n const metadata = this.mapper.getAnalyzerMetadata();\n const mcpServerName = metadata?.mcp_server_name ?? \"codebase-memory-mcp\";\n // Check if the analyzer's MCP server is registered in .mcp.json\n return (mcpConfig.mcpServers && mcpServerName in mcpConfig.mcpServers) || false;\n } catch (error) {\n // ENOENT (file not found) is expected - MCP registration is optional\n const err = error as NodeJS.ErrnoException;\n if (err.code === \"ENOENT\") {\n return false;\n }\n\n // Other errors indicate actionable problems\n if (err.code === \"EACCES\") {\n throw new CLIError(\n \"Permission denied reading .mcp.json\",\n ErrorCategory.SYSTEM,\n [\n `Cannot read ${path.join(projectRoot, \".mcp.json\")}`,\n \"Check file permissions\",\n ]\n );\n }\n\n // SyntaxError during JSON.parse or other read/parse errors\n throw new CLIError(\n \"Invalid .mcp.json format\",\n ErrorCategory.VALIDATION,\n [\n `Check that ${path.join(projectRoot, \".mcp.json\")} contains valid JSON`,\n `Error: ${error instanceof Error ? error.message : String(error)}`,\n ]\n );\n }\n }\n\n /**\n * Detect if the analyzer is installed and functional\n *\n * Checks for the presence of the analyzer binary, reads .mcp.json for registration info,\n * and validates the required tool contract by calling initialize and verifying required_tools.\n *\n * @param projectRoot Optional absolute path to project root (defaults to cwd)\n * @returns Detection result with binary path, version, and MCP registration status\n */\n async detect(projectRoot?: string): Promise<DetectionResult> {\n // Get binary names from the analyzer metadata\n const metadata = this.mapper.getAnalyzerMetadata();\n const binaryNames = (metadata?.binary_names as string[] | undefined) ?? [\n \"codebase-memory-mcp\",\n ];\n\n // Check if .mcp.json exists at project root for registration status\n // Use provided projectRoot or fallback to cwd\n let mcpRegistered = false;\n try {\n mcpRegistered = await this.checkMcpRegistration(projectRoot || process.cwd());\n } catch (error) {\n // CLIError from checkMcpRegistration indicates actionable problems (EACCES, syntax errors)\n // Surface these to the user via handleWarning(), but continue with detection\n if (error instanceof CLIError) {\n handleWarning(error.message, error.suggestions);\n } else {\n // Unexpected error - still continue with detection but warn\n const errorMsg = error instanceof Error ? error.message : String(error);\n handleWarning(\"Unexpected error checking MCP registration\", [errorMsg]);\n }\n mcpRegistered = false;\n }\n\n for (const binaryName of binaryNames) {\n // Check if binary is available using 'which'\n const whichResult = spawnSync(\"which\", [binaryName], {\n stdio: \"pipe\",\n encoding: \"utf-8\",\n });\n\n if (whichResult.status !== 0) {\n continue;\n }\n\n const binaryPath = whichResult.stdout.trim();\n\n if (!binaryPath) {\n continue;\n }\n\n // Try to initialize the analyzer to validate the tool contract\n const client = new StdioClient();\n try {\n client.spawn(binaryPath);\n\n const version = await getCliVersion();\n await client.initialize({\n name: \"dr-cli\",\n version,\n });\n\n // Verify required_tools from mapping are available on the server\n let contractOk = true;\n const requiredTools = (metadata?.supported_tool_contract as Record<string, unknown> | undefined)?.required_tools as string[] | undefined ?? [];\n\n if (requiredTools.length > 0) {\n try {\n // Verify all required tools are listed in supported tools\n // The MCP spec requires tools to be registered at initialization\n const response = await client.callTool(\"tools/list\", {}) as { tools?: Array<{ name: string }> };\n\n const availableTools = new Set(\n (response.tools ?? []).map((tool) => tool.name)\n );\n\n for (const tool of requiredTools) {\n if (!availableTools.has(tool)) {\n contractOk = false;\n handleWarning(\n `Required tool '${tool}' not available in ${binaryName}`,\n [\"Ensure the analyzer server exports all required tools\"]\n );\n }\n }\n } catch (error) {\n // If tool listing fails, treat as verification failure\n contractOk = false;\n const errorMsg = error instanceof Error ? error.message : String(error);\n handleWarning(\n `Failed to verify required tools for ${binaryName}: ${errorMsg}`,\n [\"Ensure all required tools are available in the analyzer server\"]\n );\n }\n }\n\n // Successfully initialized and verified contract\n return {\n installed: true,\n binary_path: binaryPath,\n version: undefined, // Version could be extracted from --version if needed\n mcp_registered: mcpRegistered,\n contract_ok: contractOk,\n };\n } catch (error) {\n // Initialize failed - log diagnostic info and continue to next binary\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n handleWarning(\n `Binary ${binaryName} failed initialization: ${errorMessage}`,\n [\"Check that the binary is properly installed and functional\"]\n );\n continue;\n } finally {\n // Always close the client to prevent orphan processes\n try {\n client.close();\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n handleWarning(\n `Failed to close MCP client for ${binaryName}: ${errorMessage}`,\n [\n \"The MCP server process may still be running\",\n \"Try restarting your terminal or manually killing the process\",\n ]\n );\n }\n }\n }\n\n // No binary found or functional\n return {\n installed: false,\n };\n }\n\n /**\n * Get the current status of the analyzer for a project\n *\n * Checks if the project is indexed and whether the index is fresh relative to git HEAD.\n *\n * @param projectRoot Absolute path to the project root\n * @returns Current status including detection, index state, and freshness\n */\n async status(projectRoot: string): Promise<AnalyzerStatus> {\n const detected = await this.detect(projectRoot);\n\n // Read index metadata if it exists\n const analyzerName = this.mapper.getAnalyzerName();\n const indexMeta = await readIndexMeta(projectRoot, analyzerName);\n\n // Check if indexed\n const indexed = indexMeta !== null;\n\n if (!indexed) {\n return {\n detected,\n indexed: false,\n fresh: false,\n };\n }\n\n // Check freshness: compare stored git HEAD to current HEAD\n let fresh = false;\n try {\n const currentHeadResult = spawnSync(\"git\", [\"rev-parse\", \"HEAD\"], {\n cwd: projectRoot,\n stdio: \"pipe\",\n encoding: \"utf-8\",\n });\n\n if (currentHeadResult.status === 0) {\n const currentHead = currentHeadResult.stdout.trim();\n fresh = currentHead === indexMeta.git_head;\n } else {\n // Git command failed - log diagnostic info\n const gitError = currentHeadResult.stderr?.trim() || \"git returned non-zero status\";\n handleWarning(\"Failed to check git freshness\", [\n `Git error: ${gitError}`,\n \"Index freshness cannot be determined - treating as stale\",\n ]);\n fresh = false;\n }\n } catch (error) {\n // Git spawn failed completely - log diagnostic info\n const errorMsg = error instanceof Error ? error.message : String(error);\n handleWarning(\"Failed to spawn git process\", [\n `Error: ${errorMsg}`,\n \"Index freshness cannot be determined - treating as stale\",\n ]);\n fresh = false;\n }\n\n return {\n detected,\n indexed: true,\n index_meta: indexMeta,\n fresh,\n last_indexed: indexMeta.timestamp,\n };\n }\n\n /**\n * Index a project or update the index\n *\n * Creates a project entry in the CBM backend if it doesn't already exist,\n * indexes the repository, and writes freshness metadata.\n *\n * Skips indexing if the index is fresh (git HEAD matches) unless --force is set.\n *\n * @param projectRoot Absolute path to the project root\n * @param options Optional configuration\n * @returns Index result with counts and metadata\n */\n async index(\n projectRoot: string,\n options?: { force?: boolean }\n ): Promise<IndexResult> {\n // Get current status\n const status = await this.status(projectRoot);\n\n // Skip if fresh and not forced\n if (status.indexed && status.fresh && !options?.force) {\n handleWarning(\"Index is fresh. Use --force to reindex.\", [\n `Last indexed: ${status.last_indexed}`,\n ]);\n\n // Return the existing metadata\n const meta = status.index_meta;\n return {\n node_count: meta.node_count,\n edge_count: meta.edge_count,\n git_head: meta.git_head,\n timestamp: meta.timestamp,\n };\n }\n\n // Use detection result from status() instead of re-detecting\n const detection = status.detected;\n if (!detection.installed) {\n throw new CLIError(\n \"CBM analyzer not installed\",\n ErrorCategory.NOT_FOUND,\n [\n \"Install codebase-memory-mcp: npm install -g codebase-memory-mcp\",\n \"Ensure the binary is in your PATH\",\n ]\n );\n }\n\n const client = new StdioClient();\n\n try {\n client.spawn(detection.binary_path);\n\n const version = await getCliVersion();\n await client.initialize({\n name: \"dr-cli\",\n version,\n });\n\n // Check if project is already indexed using list_projects\n // This validates the acceptance criterion and prevents duplicate project creation\n const listProjectsResponse = (await client.invokeTool(\"list_projects\")) as {\n projects?: Array<{ name: string; root_path: string; nodes?: number; edges?: number }>;\n [key: string]: unknown;\n };\n\n // Validate response structure\n if (!listProjectsResponse || typeof listProjectsResponse !== \"object\") {\n throw new CLIError(\n \"Invalid response from list_projects\",\n ErrorCategory.SYSTEM,\n [\"Ensure codebase-memory-mcp is properly installed and functional\"]\n );\n }\n\n const projects = Array.isArray(listProjectsResponse.projects)\n ? (listProjectsResponse.projects as Array<{\n name: string;\n root_path: string;\n }>)\n : [];\n const projectExists = projects.some((p) => p.root_path === projectRoot);\n\n // Prevent duplicate CBM entries if project exists and index is fresh.\n // Per FR-3.3, --force is only needed to override a fresh index, not for stale re-indexing.\n // list_projects check prevents duplicate entries; freshness is already checked at line 313.\n if (projectExists && status.fresh && !options?.force) {\n handleWarning(\n \"Project already indexed in CBM backend with fresh index.\",\n [\"Use --force to re-index the project\"]\n );\n\n // Return actual index metadata instead of fabricated zeros\n // Guard against index_meta being undefined (can happen if metadata is corrupted or deleted)\n if (!status.index_meta) {\n throw new CLIError(\n \"Project exists in CBM backend but local metadata is missing\",\n ErrorCategory.VALIDATION,\n [\n \"Use --force to re-index and regenerate metadata\",\n \"Or manually delete the project from CBM backend and re-index\"\n ]\n );\n }\n\n const meta = status.index_meta;\n return {\n node_count: meta.node_count,\n edge_count: meta.edge_count,\n git_head: meta.git_head,\n timestamp: meta.timestamp,\n };\n }\n\n // Get current git HEAD - fail if git is not available\n // This must happen before index_repository to avoid inconsistent state\n const headResult = spawnSync(\"git\", [\"rev-parse\", \"HEAD\"], {\n cwd: projectRoot,\n stdio: \"pipe\",\n encoding: \"utf-8\",\n });\n\n if (headResult.status !== 0) {\n throw new CLIError(\n \"Failed to get git HEAD\",\n ErrorCategory.SYSTEM,\n [\n \"Ensure the project is a git repository\",\n \"Verify git is installed and functional\",\n `Error: ${headResult.stderr?.trim() || \"unknown error\"}`,\n ]\n );\n }\n\n const gitHead = headResult.stdout.trim();\n\n // Read and log the active changeset for consistency tracking (v1: logging only, no branching)\n try {\n const stagingManager = new StagingAreaManager(projectRoot);\n const activeChangesetId = await stagingManager.getActiveId();\n if (activeChangesetId) {\n console.log(`Indexing with active changeset: ${activeChangesetId}`);\n }\n } catch (error) {\n // If reading active changeset fails, log a warning but continue indexing\n const errorMsg = error instanceof Error ? error.message : String(error);\n handleWarning(\n \"Failed to read active changeset\",\n [`Error: ${errorMsg}`, \"Continuing with indexing\"]\n );\n }\n\n // Index the repository\n const indexResponse = (await client.invokeTool(\"index_repository\", {\n repo_path: projectRoot,\n })) as {\n nodes?: number;\n edges?: number;\n status?: string;\n [key: string]: unknown;\n };\n\n // Validate response structure\n if (!indexResponse || typeof indexResponse !== \"object\") {\n throw new CLIError(\n \"Invalid response from index_repository\",\n ErrorCategory.SYSTEM,\n [\"Ensure codebase-memory-mcp is properly installed and functional\"]\n );\n }\n\n const timestamp = new Date().toISOString();\n const nodeCount = typeof indexResponse.nodes === \"number\" ? indexResponse.nodes : 0;\n const edgeCount = typeof indexResponse.edges === \"number\" ? indexResponse.edges : 0;\n\n // Warn if counts were not numbers and defaulted to zero\n if (typeof indexResponse.nodes !== \"number\") {\n handleWarning(\n \"Invalid node count from index_repository\",\n [\n `Expected number, got ${typeof indexResponse.nodes}: ${String(indexResponse.nodes)}`,\n \"Defaulting to 0 - index may be incomplete or corrupted\",\n ]\n );\n }\n\n if (typeof indexResponse.edges !== \"number\") {\n handleWarning(\n \"Invalid edge count from index_repository\",\n [\n `Expected number, got ${typeof indexResponse.edges}: ${String(indexResponse.edges)}`,\n \"Defaulting to 0 - index may be incomplete or corrupted\",\n ]\n );\n }\n\n // Write index metadata\n const meta: IndexMeta = {\n git_head: gitHead,\n timestamp,\n node_count: nodeCount,\n edge_count: edgeCount,\n };\n\n const analyzerName = this.mapper.getAnalyzerName();\n await writeIndexMeta(meta, projectRoot, analyzerName);\n\n return {\n node_count: nodeCount,\n edge_count: edgeCount,\n git_head: gitHead,\n timestamp,\n };\n } finally {\n client.close();\n }\n }\n\n /**\n * Validate search_graph response structure\n *\n * Ensures response is an object and nodes is an array.\n * Throws CLIError if validation fails.\n *\n * @private\n * @param response Raw response from search_graph MCP tool\n * @returns Typed nodes array (may be empty if no results found)\n * @throws CLIError if response structure is invalid\n */\n private validateSearchResponse(\n response: unknown\n ): CbmGraphNode[] {\n // Validate response structure\n if (!response || typeof response !== \"object\") {\n throw new CLIError(\n \"Invalid response from search_graph\",\n ErrorCategory.SYSTEM,\n [\"Ensure codebase-memory-mcp is properly installed and functional\"]\n );\n }\n\n // Support 'results' (codebase-memory-mcp v0.6+) and legacy 'nodes'\n const responseObj = response as { results?: unknown; nodes?: unknown; [key: string]: unknown };\n const nodeArray = responseObj.results ?? responseObj.nodes;\n if (!Array.isArray(nodeArray)) {\n throw new CLIError(\n \"Invalid response from search_graph: results must be an array\",\n ErrorCategory.SYSTEM,\n [\n `Expected array in 'results' or 'nodes', got ${typeof nodeArray}`,\n \"Ensure codebase-memory-mcp is properly installed and returning valid results\"\n ]\n );\n }\n\n return nodeArray as CbmGraphNode[];\n }\n\n /**\n * Resolve the internal project name used by codebase-memory-mcp for a given root path.\n *\n * list_projects returns { name, root_path } per entry. search_graph and other\n * query tools require the internal `name`, not the absolute `root_path`.\n * Falls back to the absolute path with a warning if the project is not found.\n */\n private async resolveProjectName(\n client: StdioClient,\n projectRoot: string\n ): Promise<string> {\n const listResponse = (await client.invokeTool(\"list_projects\")) as {\n projects?: Array<{ name: string; root_path: string }>;\n };\n const projects = Array.isArray(listResponse?.projects) ? listResponse.projects : [];\n const match = projects.find((p) => p.root_path === projectRoot);\n if (!match) {\n handleWarning(\n \"Project not found in CBM backend; using absolute path as project identifier\",\n [\n `Project root: ${projectRoot}`,\n \"Run `dr analyzer index` to ensure the project is indexed\",\n ]\n );\n return projectRoot;\n }\n return match.name;\n }\n\n /**\n * Query for API endpoints/routes in the indexed project\n *\n * Searches the indexed graph for Route nodes, applies field mappings from\n * the analyzer's mapping, and filters based on test code exclusion rules.\n *\n * @param projectRoot Absolute path to the project root\n * @returns Array of endpoint candidates with confidence and source info\n * @throws CLIError if the project is not indexed\n */\n async endpoints(projectRoot: string): Promise<EndpointCandidate[]> {\n // Check that the project is indexed\n const status = await this.status(projectRoot);\n if (!status.indexed) {\n throw new CLIError(\n `Project not indexed: ${projectRoot}`,\n ErrorCategory.NOT_FOUND,\n [\n \"Run `dr analyzer index` to index the project\",\n \"Ensure codebase-memory-mcp is installed and working\",\n ]\n );\n }\n\n // Use detection result from status() instead of re-detecting\n const detection = status.detected;\n if (!detection.installed) {\n throw new CLIError(\n \"CBM analyzer not installed\",\n ErrorCategory.NOT_FOUND,\n [\"Install codebase-memory-mcp: npm install -g codebase-memory-mcp\"]\n );\n }\n\n // Get the Route mapping BEFORE initializing client to validate it exists\n const routeMapping = this.mapper.getNodeMapping(\"Route\");\n if (!routeMapping) {\n throw new CLIError(\n \"Route mapping not found in analyzer\",\n ErrorCategory.VALIDATION,\n [\"Run `npm run build:spec` to recompile the analyzer artifacts\"]\n );\n }\n\n const client = new StdioClient();\n\n try {\n client.spawn(detection.binary_path);\n\n const version = await getCliVersion();\n await client.initialize({\n name: \"dr-cli\",\n version,\n });\n\n // Resolve internal project name for search_graph (requires name, not absolute path)\n const projectName = await this.resolveProjectName(client, projectRoot);\n\n // Search for Route nodes using the CBM label from the mapping\n const searchResponse = await client.invokeTool(\"search_graph\", {\n label: routeMapping.cbm_label,\n project: projectName,\n });\n\n const allNodes = this.validateSearchResponse(searchResponse);\n // Keep only nodes whose qualified_name follows the __route__METHOD__/path\n // convention. Nodes that don't match are graph artifacts (documentation\n // strings, base URLs, etc.) that were incidentally labelled Route by the\n // indexer \u2014 not actual HTTP routes.\n const nodes = allNodes\n .filter((n) => /^__route__[A-Z]+__\\//.test(String(n.qualified_name ?? \"\")))\n .filter((n) => isLikelyHttpRoute(String(n.qualified_name ?? \"\")));\n\n // Transform nodes to endpoint candidates\n const candidates: EndpointCandidate[] = [];\n\n for (const node of nodes) {\n const candidate = await this.transformNodeToEndpoint(\n node,\n routeMapping,\n projectRoot\n );\n\n // Skip unattributable candidates \u2014 empty source_file means the graph\n // found a path string in non-route code (test assertions, fixture data)\n if (!candidate.source_file) {\n continue;\n }\n // Apply test code exclusion filter\n if (!this.isTestCode(candidate)) {\n candidates.push(candidate);\n }\n }\n\n // ISSUE-003: Client-side API consumer detection.\n // When no server-side Route nodes are found (e.g. React/frontend apps using fetch/axios),\n // fall back to querying Function nodes in API-client files whose names follow HTTP verb\n // conventions. These are low-confidence candidates \u2014 the user confirms and adds them.\n if (candidates.length === 0) {\n try {\n const clientQuery = `\n MATCH (n:Function)\n WHERE (n.file_path CONTAINS 'client' OR n.file_path CONTAINS 'Client'\n OR n.file_path CONTAINS 'Api' OR n.file_path CONTAINS 'Service'\n OR n.file_path CONTAINS 'service' OR n.file_path CONTAINS 'http'\n OR n.file_path CONTAINS 'Http')\n AND (n.name STARTS WITH 'fetch' OR n.name STARTS WITH 'get'\n OR n.name STARTS WITH 'post' OR n.name STARTS WITH 'put'\n OR n.name STARTS WITH 'delete' OR n.name STARTS WITH 'patch'\n OR n.name STARTS WITH 'create' OR n.name STARTS WITH 'update'\n OR n.name STARTS WITH 'list')\n RETURN n\n `;\n\n const qResp = await client.invokeTool(\"query_graph\", {\n query: clientQuery,\n project: projectName,\n });\n\n if (qResp && typeof qResp === \"object\") {\n const records = (qResp as { records?: Array<{ n?: CbmGraphNode }> }).records ?? [];\n for (const record of records) {\n const node = record.n;\n if (!node || !node.file_path) continue;\n const candidate = this.shapeClientSideEndpoint(node, projectRoot);\n if (candidate && !this.isTestCode(candidate)) {\n candidates.push(candidate);\n }\n }\n }\n } catch {\n // query_graph not available \u2014 skip client-side fallback silently\n }\n }\n\n this.warnOnDuplicateIdFragments(\n candidates,\n (c) => c.suggested_id_fragment,\n (c) => c.source_symbol || c.source_file || \"(unknown)\"\n );\n return candidates;\n } finally {\n client.close();\n }\n }\n\n /**\n * Execute a raw Cypher query against the analyzer's graph\n *\n * Passes the raw Cypher string directly to the query_graph MCP tool.\n * The query_graph tool is optional (not in required_tools contract).\n * If the tool call fails, surfaces a clear error message.\n *\n * @param projectRoot Absolute path to the project root\n * @param rawQuery Raw Cypher query string\n * @returns Raw result from query_graph tool\n * @throws CLIError if project not indexed or query_graph tool unavailable\n */\n async query(projectRoot: string, rawQuery: string): Promise<unknown> {\n // Check that the project is indexed (same pre-flight checks as endpoints)\n const status = await this.status(projectRoot);\n if (!status.indexed) {\n throw new CLIError(\n `Project not indexed: ${projectRoot}`,\n ErrorCategory.NOT_FOUND,\n [\n \"Run `dr analyzer index` to index the project\",\n \"Ensure codebase-memory-mcp is installed and working\",\n ]\n );\n }\n\n // Use detection result from status() instead of re-detecting\n const detection = status.detected;\n if (!detection.installed) {\n throw new CLIError(\n \"CBM analyzer not installed\",\n ErrorCategory.NOT_FOUND,\n [\"Install codebase-memory-mcp: npm install -g codebase-memory-mcp\"]\n );\n }\n\n const client = new StdioClient();\n\n try {\n client.spawn(detection.binary_path);\n\n const version = await getCliVersion();\n await client.initialize({\n name: \"dr-cli\",\n version,\n });\n\n // Resolve internal project name \u2014 query_graph requires the CBM name, not the absolute path\n const projectName = await this.resolveProjectName(client, projectRoot);\n\n // Call query_graph tool\n try {\n const queryResponse = await client.invokeTool(\"query_graph\", {\n query: rawQuery,\n project: projectName,\n });\n\n return queryResponse;\n } catch (error) {\n // Check if the error is due to the tool not being available\n const errorMessage = error instanceof Error ? error.message : String(error);\n\n if (errorMessage.includes(\"query_graph\") || errorMessage.includes(\"Unknown method\")) {\n throw new CLIError(\n \"query_graph not supported by this analyzer\",\n ErrorCategory.USER,\n [\n \"The query_graph tool is optional in the CBM analyzer contract\",\n \"Ensure your codebase-memory-mcp version supports raw graph queries\",\n \"Contact the analyzer maintainers if you need this feature\",\n ]\n );\n }\n\n // Re-throw other errors\n throw error;\n }\n } finally {\n client.close();\n }\n }\n\n private warnOnDuplicateIdFragments<T>(\n items: T[],\n getKey: (item: T) => string | undefined,\n getLabel: (item: T) => string\n ): void {\n const seen = new Map<string, string>();\n for (const item of items) {\n const key = getKey(item);\n if (!key) continue;\n const label = getLabel(item);\n if (seen.has(key)) {\n handleWarning(\n `Duplicate suggested ID \"${key}\" \u2014 multiple symbols map to the same element ID`,\n [\n `First: ${seen.get(key)!}`,\n `Conflict: ${label}`,\n `Rename one or use \"dr add\" with a custom name`,\n ]\n );\n } else {\n seen.set(key, label);\n }\n }\n }\n\n /**\n * Transform a CBM graph node to an endpoint candidate\n *\n * Maps CBM Route node properties to DR EndpointCandidate contract:\n * - suggested_layer: always \"api\" for endpoints\n * - suggested_element_type: always \"operation\" for routes\n * - suggested_name: from node properties or id (kebab-case)\n * - http_method: from properties.method or defaults to \"GET\"\n * - http_path: from properties.path or defaults to \"/\"\n * - handler_qualified_name: from properties.handler_name or empty string\n * - source_symbol: from properties.symbol or empty string\n * - source_start_line: from properties.start_line or 0\n * - source_end_line: from properties.end_line or 0\n *\n * Downgrades confidence if required fields are missing.\n *\n * @private\n */\n private async transformNodeToEndpoint(\n node: CbmGraphNode,\n mapping: AnalyzerNodeMapping,\n projectRoot: string\n ): Promise<EndpointCandidate> {\n const properties = node.properties ?? {};\n // Node identifier for display \u2014 actual codebase-memory-mcp nodes use 'name', not 'id'\n const nodeId = String(node.name ?? node.id ?? \"(unknown)\");\n\n // Extract base fields\n let confidence = mapping.confidence as \"high\" | \"medium\" | \"low\";\n\n // Suggested name in kebab-case: properties.name \u2192 node.name \u2192 node.id\n const suggestedName = toKebabCase(String(properties.name ?? node.name ?? node.id ?? \"\"));\n\n // Suggested ID fragment (same as name for endpoints)\n const suggestedIdFragment = suggestedName;\n\n // Required fields for dr add api operation\n // Extract HTTP method: properties first, then parse qualified_name pattern __route__METHOD__/path\n let rawMethod: string;\n if (typeof properties.method === \"string\") {\n rawMethod = properties.method.toUpperCase();\n } else {\n const qn = String(node.qualified_name ?? \"\");\n const methodMatch = qn.match(/^__route__([A-Z]+)__/);\n rawMethod = methodMatch?.[1] ?? \"GET\";\n if (rawMethod === \"ANY\") rawMethod = \"GET\";\n }\n\n // Validate against valid HTTP methods using the constant\n const validMethods = new Set(VALID_HTTP_METHODS);\n const isValidMethod = validMethods.has(rawMethod as HttpMethod);\n const httpMethod: HttpMethod = isValidMethod\n ? (rawMethod as HttpMethod)\n : \"GET\";\n\n if (!isValidMethod) {\n handleWarning(\n `Node ${nodeId}: Invalid HTTP method`,\n [\n `Method '${rawMethod}' is not a valid HTTP method`,\n `Valid methods: ${Array.from(validMethods).join(\", \")}`,\n \"Defaulting to GET - endpoint documentation may be inaccurate\",\n ]\n );\n }\n\n // Extract HTTP path: properties first, then node.name (which IS the URL path for flat nodes)\n let httpPath: string;\n if (typeof properties.path === \"string\") {\n httpPath = properties.path;\n } else if (typeof node.name === \"string\" && node.name.startsWith(\"/\")) {\n httpPath = node.name;\n } else {\n handleWarning(\n `Node ${nodeId}: Missing or invalid HTTP path`,\n [\n `Expected string, got ${typeof properties.path}`,\n \"Defaulting to / - endpoint may be incomplete\",\n ]\n );\n httpPath = \"/\";\n }\n\n // Handler information \u2014 try multiple property names because CBM indexers vary.\n // Do NOT fall back to properties.qualified_name \u2014 that is the route node's own QN.\n const handlerQualifiedName = String(\n properties.handler_name ??\n properties.handler_qualified_name ??\n \"\"\n );\n const sourceSymbol = String(properties.symbol ?? properties.source_symbol ?? \"\");\n const sourceStartLine = Number(properties.start_line ?? 0);\n const sourceEndLine = Number(properties.end_line ?? 0);\n\n // Downgrade confidence if handler information is missing\n if (!handlerQualifiedName || !sourceSymbol) {\n confidence = capConfidence(confidence);\n }\n\n // Extract source file \u2014 check both node-level and properties-level fields\n let sourceFile = String(node.file_path ?? properties.file_path ?? properties.source_file ?? \"\");\n if (sourceFile && projectRoot) {\n try {\n sourceFile = path.relative(projectRoot, sourceFile);\n } catch (error) {\n // Log warning if relative path fails but continue with absolute path\n const errorMsg = error instanceof Error ? error.message : String(error);\n handleWarning(\n `Failed to compute relative path for ${nodeId}`,\n [\n `Error: ${errorMsg}`,\n \"Using absolute file path instead of relative\",\n ]\n );\n // sourceFile keeps its original absolute value\n }\n }\n\n return {\n source_file: sourceFile,\n confidence,\n suggested_layer: \"api\",\n suggested_element_type: \"operation\",\n suggested_name: suggestedName,\n suggested_id_fragment: suggestedIdFragment,\n http_method: httpMethod,\n http_path: httpPath,\n handler_qualified_name: handlerQualifiedName,\n source_symbol: sourceSymbol,\n source_start_line: sourceStartLine,\n source_end_line: sourceEndLine,\n source_reference: {\n provenance: \"extracted\",\n locations: sourceFile\n ? [\n {\n file: sourceFile,\n symbol: sourceSymbol || undefined,\n },\n ]\n : undefined,\n },\n };\n }\n\n\n /**\n * Check if a candidate (endpoint, service, or datastore) is in test code\n *\n * Applies the test_code_exclusion filtering rule from the analyzer mapping.\n * If the configured regex pattern is invalid, warns the user once and falls back to defaults.\n * Uses a one-shot flag to ensure the warning is only emitted once even if called in a loop.\n *\n * Accepts any object with a source_file property (structural typing), allowing it to work\n * with EndpointCandidate, ServiceCandidate, DatastoreCandidate, and temporary objects.\n *\n * @private\n */\n\n /**\n * Build a low-confidence EndpointCandidate from a client-side API function node.\n * Used as a fallback when no server-side Route nodes are found (ISSUE-003).\n */\n private shapeClientSideEndpoint(node: CbmGraphNode, projectRoot: string): EndpointCandidate | null {\n const name = String(node.name ?? node.id ?? \"\");\n if (!name) return null;\n\n let sourceFile = String(node.file_path ?? \"\");\n if (sourceFile && projectRoot) {\n try {\n sourceFile = path.relative(projectRoot, sourceFile);\n } catch {\n // keep absolute path on failure\n }\n }\n if (!sourceFile) return null;\n\n const httpMethod = inferHttpMethodFromFunctionName(name);\n const httpPath = inferHttpPathFromFunctionName(name);\n const suggestedName = toKebabCase(name);\n\n return {\n source_file: sourceFile,\n confidence: \"low\",\n suggested_layer: \"api\",\n suggested_element_type: \"operation\",\n suggested_name: suggestedName,\n suggested_id_fragment: suggestedName,\n http_method: httpMethod,\n http_path: httpPath,\n handler_qualified_name: name,\n source_symbol: name,\n source_start_line: Number(node.properties?.start_line ?? 0),\n source_end_line: Number(node.properties?.end_line ?? 0),\n source_reference: {\n provenance: \"inferred\",\n locations: [{ file: sourceFile, symbol: name }],\n },\n };\n }\n\n private isTestCode(candidate: { source_file: string }): boolean {\n // Get filtering rules from the analyzer mapping\n const filteringRules = this.mapper.getFilteringRules();\n\n // Find the test_code_exclusion rule\n const testCodeRule = filteringRules.find(\n (rule) => rule.name === \"test_code_exclusion\" && rule.enabled\n );\n\n // If no test_code_exclusion rule found, apply default patterns as fallback\n if (!testCodeRule || !testCodeRule.pattern) {\n return CbmAnalyzer.DEFAULT_TEST_PATTERNS.some((pattern) =>\n pattern.test(candidate.source_file)\n );\n }\n\n // Apply the pattern from the filtering rule\n try {\n const testCodePattern = new RegExp(testCodeRule.pattern);\n return testCodePattern.test(candidate.source_file);\n } catch (error) {\n // If regex is invalid, warn user once (not on every invocation in the loop)\n // and fall back to default patterns\n if (!this.testCodePatternWarned) {\n const errorMsg = error instanceof Error ? error.message : String(error);\n handleWarning(\n \"Invalid test_code_exclusion regex pattern in analyzer mapping\",\n [\n `Pattern: ${testCodeRule.pattern}`,\n `Error: ${errorMsg}`,\n \"Falling back to default test code patterns\",\n ]\n );\n this.testCodePatternWarned = true;\n }\n return CbmAnalyzer.DEFAULT_TEST_PATTERNS.some((pattern) =>\n pattern.test(candidate.source_file)\n );\n }\n }\n\n /**\n * Query for services/components in the indexed project\n *\n * Searches for application-layer nodes using the mapping-loader-driven pattern.\n * For each node, evaluates all promotion_heuristics and records which fire in\n * qualifying_heuristics. Drops candidates with zero qualifying heuristics (unless\n * is_entry_point heuristic fires, adding it to qualifying_heuristics). Caps confidence\n * at \"medium\" and excludes test files.\n *\n * @param projectRoot Absolute path to the project root\n * @returns Array of service candidates with qualifying heuristics populated\n * @throws CLIError if the project is not indexed\n */\n async services(projectRoot: string): Promise<ServiceCandidate[]> {\n // Check that the project is indexed\n const status = await this.status(projectRoot);\n if (!status.indexed) {\n throw new CLIError(\n `Project not indexed: ${projectRoot}`,\n ErrorCategory.NOT_FOUND,\n [\n \"Run `dr analyzer index` to index the project\",\n \"Ensure codebase-memory-mcp is installed and working\",\n ]\n );\n }\n\n // Use detection result from status() instead of re-detecting\n const detection = status.detected;\n if (!detection.installed) {\n throw new CLIError(\n \"CBM analyzer not installed\",\n ErrorCategory.NOT_FOUND,\n [\"Install codebase-memory-mcp: npm install -g codebase-memory-mcp\"]\n );\n }\n\n const client = new StdioClient();\n\n try {\n client.spawn(detection.binary_path);\n\n const version = await getCliVersion();\n await client.initialize({\n name: \"dr-cli\",\n version,\n });\n\n // Get application-layer node labels from the mapping (pattern from endpoints())\n const nodeLabels = this.mapper.getNodeLabels();\n const applicationLabels: string[] = [];\n\n // Filter to only application-layer labels by checking their mapping\n for (const label of nodeLabels) {\n const mapping = this.mapper.getNodeMapping(label);\n if (mapping && mapping.dr_layer === \"application\") {\n applicationLabels.push(label);\n }\n }\n\n // If no application-layer labels found, warn and return empty list\n if (applicationLabels.length === 0) {\n handleWarning(\n \"No application-layer labels found in analyzer mapping\",\n [\n \"Check that the analyzer is properly configured with application-layer node mappings\",\n \"Services detection requires at least one mapped application-layer node type\",\n \"Returning empty services list\"\n ]\n );\n return [];\n }\n\n const candidates: ServiceCandidate[] = [];\n\n // Resolve internal project name once before the label search loop\n const projectName = await this.resolveProjectName(client, projectRoot);\n\n // Process high-fidelity node types first so the source_file dedup (below) retains\n // class/module level entries over method-level entries from the same file.\n const labelPriority: Record<string, number> = { Class: 0, Module: 1 };\n applicationLabels.sort((a, b) => (labelPriority[a] ?? 99) - (labelPriority[b] ?? 99));\n\n // Search for each application-layer label\n for (const label of applicationLabels) {\n const searchResponse = await client.invokeTool(\"search_graph\", {\n label,\n project: projectName,\n });\n\n const nodes = this.validateSearchResponse(searchResponse);\n\n // Get the node mapping for this label\n const nodeMapping = this.mapper.getNodeMapping(label);\n if (!nodeMapping) {\n continue;\n }\n\n // Get promotion heuristics from the mapping\n const promotionHeuristicNames = nodeMapping.promotion_heuristics || [];\n\n // Transform nodes to service candidates\n for (const node of nodes) {\n const candidate = await this.transformNodeToService(\n node,\n nodeMapping,\n projectRoot,\n promotionHeuristicNames\n );\n\n // Filter out candidates with zero qualifying heuristics (unless is_entry_point is true)\n // and apply test code exclusion filter\n if (candidate.qualifying_heuristics.length > 0 && !this.isTestCode(candidate)) {\n candidates.push(candidate);\n }\n }\n }\n\n // Deduplicate by source_file \u2014 the model documents at class/file granularity;\n // method-level entries from the same file are noise.\n const seenFiles = new Set<string>();\n const deduped = candidates.filter((c) => {\n if (!c.source_file) return true;\n if (seenFiles.has(c.source_file)) return false;\n seenFiles.add(c.source_file);\n return true;\n });\n this.warnOnDuplicateIdFragments(\n deduped,\n (c) => c.suggested_id_fragment,\n (c) => c.source_symbol || c.source_file || \"(unknown)\"\n );\n return deduped;\n } finally {\n client.close();\n }\n }\n\n /**\n * Transform a CBM graph node to a service candidate\n *\n * Evaluates all promotion heuristics against the node's properties and records\n * which heuristics fire. Drops candidates where no heuristics fire (unless the\n * is_entry_point heuristic fires, which adds it to qualifying_heuristics).\n * Caps confidence at \"medium\".\n *\n * @private\n */\n private async transformNodeToService(\n node: CbmGraphNode,\n mapping: AnalyzerNodeMapping,\n projectRoot: string,\n promotionHeuristicNames: string[]\n ): Promise<ServiceCandidate> {\n const properties = node.properties ?? {};\n\n // Suggested name in kebab-case: properties.name \u2192 node.name \u2192 node.id\n const suggestedName = toKebabCase(String(properties.name ?? node.name ?? node.id ?? \"\"));\n\n // Suggested ID fragment (same as name for services)\n const suggestedIdFragment = suggestedName;\n\n // Extract source file (relative to project root)\n let sourceFile = node.file_path ?? \"\";\n if (sourceFile && projectRoot) {\n try {\n sourceFile = path.relative(projectRoot, sourceFile);\n } catch (error) {\n // Log warning if relative path fails but continue with absolute path\n const errorMsg = error instanceof Error ? error.message : String(error);\n handleWarning(\n `Failed to compute relative path for ${String(node.name ?? node.id ?? \"(unknown)\")}`,\n [\n `Error: ${errorMsg}`,\n \"Using absolute file path instead of relative\",\n ]\n );\n // sourceFile keeps its original absolute value\n }\n }\n\n // Extract qualified name: properties first, then node.qualified_name, then node.id\n const qualifiedName = String(properties.qualified_name ?? node.qualified_name ?? node.name ?? node.id ?? \"\");\n\n // Extract fan-in and fan-out metrics\n const fanIn = typeof properties.fan_in === \"number\" ? properties.fan_in : 0;\n const fanOut = typeof properties.fan_out === \"number\" ? properties.fan_out : 0;\n\n // Evaluate all promotion heuristics and collect which ones fire\n const qualifyingHeuristics: string[] = [];\n let isEntryPoint = false;\n\n for (const heuristicName of promotionHeuristicNames) {\n const heuristic = this.mapper.getHeuristic(heuristicName);\n if (!heuristic) {\n // Warn about missing heuristic only once per heuristic name\n if (!this.warnedHeuristics.has(heuristicName)) {\n this.warnedHeuristics.add(heuristicName);\n handleWarning(\n `Promotion heuristic \"${heuristicName}\" not found in analyzer mapping`,\n [\n \"Check the mapping configuration for typos\",\n `Available heuristics: ${getKnownHeuristicNames().join(\", \")}`,\n \"This heuristic will be skipped for service promotion\",\n ]\n );\n }\n continue;\n }\n\n // Evaluate the heuristic against the node\n const fires = this.evaluateHeuristic(heuristic, node, sourceFile);\n if (fires) {\n qualifyingHeuristics.push(heuristicName);\n // Track if is_entry_point heuristic fired\n if (heuristicName === \"is_entry_point\") {\n isEntryPoint = true;\n }\n }\n }\n\n // Drop candidates where zero heuristics fire AND is_entry_point is not true\n const shouldKeep = qualifyingHeuristics.length > 0 || isEntryPoint;\n if (!shouldKeep) {\n // Return a minimal candidate with empty qualifying_heuristics\n // The services() method will filter this out based on qualifying_heuristics.length > 0 check\n const elementType: \"applicationservice\" | \"applicationcomponent\" =\n mapping.dr_element_type_promoted === \"applicationcomponent\"\n ? \"applicationcomponent\"\n : \"applicationservice\";\n return {\n suggested_layer: \"application\",\n suggested_element_type: elementType,\n suggested_id_fragment: suggestedIdFragment,\n suggested_name: suggestedName,\n source_file: sourceFile,\n source_symbol: String(properties.name ?? node.name ?? node.id ?? \"\"),\n qualified_name: qualifiedName,\n qualifying_heuristics: [],\n confidence: \"low\",\n fan_in: fanIn,\n fan_out: fanOut,\n };\n }\n\n // Cap confidence at \"medium\" (from mapping or default to medium)\n const confidence = capConfidence(\n mapping.confidence as \"high\" | \"medium\" | \"low\"\n );\n\n // Ensure element type is one of the valid values\n const elementType: \"applicationservice\" | \"applicationcomponent\" =\n mapping.dr_element_type_promoted === \"applicationcomponent\"\n ? \"applicationcomponent\"\n : \"applicationservice\";\n\n return {\n suggested_layer: \"application\",\n suggested_element_type: elementType,\n suggested_id_fragment: suggestedIdFragment,\n suggested_name: suggestedName,\n source_file: sourceFile,\n source_symbol: String(properties.name ?? node.name ?? node.id ?? \"\"),\n qualified_name: qualifiedName,\n qualifying_heuristics: qualifyingHeuristics,\n confidence,\n fan_in: fanIn,\n fan_out: fanOut,\n };\n }\n\n /**\n * Evaluate a single heuristic against a node\n *\n * Checks if the heuristic's conditions are met based on the node's properties\n * and file path. Unknown heuristics are logged with a warning rather than\n * silently ignored, to support dynamic heuristics from cbm.json.\n *\n * Warnings for unknown heuristics are deduplicated - each heuristic name is warned\n * about only once, even if evaluated against many nodes.\n *\n * @private\n */\n private evaluateHeuristic(\n heuristic: AnalyzerHeuristic,\n node: CbmGraphNode,\n sourceFile: string\n ): boolean {\n const properties = node.properties ?? {};\n\n // Use the heuristic utility function\n const result = evaluateHeuristic(heuristic, properties, sourceFile);\n\n // For unknown heuristics, log a warning (deduplicated by heuristic name)\n if (heuristic.name && !getKnownHeuristicNames().includes(heuristic.name)) {\n // Only warn once per heuristic name\n if (!this.warnedHeuristics.has(heuristic.name)) {\n handleWarning(\n `Unknown heuristic type: ${heuristic.name}`,\n [\n \"The heuristic is defined in cbm.json but not implemented in heuristic-utils.ts\",\n \"This heuristic will be treated as not met (returning false)\",\n \"Add support for this heuristic to evaluateHeuristic() if it should fire\",\n ]\n );\n this.warnedHeuristics.add(heuristic.name);\n }\n }\n\n return result;\n }\n\n /**\n * Match a file path against a glob-like pattern\n *\n * @param filePath The file path to match\n * @param pattern The glob pattern\n * @returns True if the pattern matches, false otherwise\n * @private\n */\n private matchPattern(filePath: string, pattern: string): boolean {\n return matchPattern(filePath, pattern);\n }\n\n /**\n * Query for datastores/databases inferred from code analysis\n *\n * Applies datastore_detection heuristic rules to aggregate signals by file/module.\n * Matches import target names against import_patterns and function/symbol names against\n * naming_indicators from the heuristic parameters. All candidates have confidence \"low\".\n *\n * @param projectRoot Absolute path to the project root\n * @returns Array of datastore candidates aggregated by inferred datastore\n * @throws CLIError if the project is not indexed\n */\n async datastores(projectRoot: string): Promise<DatastoreCandidate[]> {\n // Check that the project is indexed\n const status = await this.status(projectRoot);\n if (!status.indexed) {\n throw new CLIError(\n `Project not indexed: ${projectRoot}`,\n ErrorCategory.NOT_FOUND,\n [\n \"Run `dr analyzer index` to index the project\",\n \"Ensure codebase-memory-mcp is installed and working\",\n ]\n );\n }\n\n // Use detection result from status() instead of re-detecting\n const detection = status.detected;\n if (!detection.installed) {\n throw new CLIError(\n \"CBM analyzer not installed\",\n ErrorCategory.NOT_FOUND,\n [\"Install codebase-memory-mcp: npm install -g codebase-memory-mcp\"]\n );\n }\n\n const client = new StdioClient();\n\n try {\n client.spawn(detection.binary_path);\n\n const version = await getCliVersion();\n await client.initialize({\n name: \"dr-cli\",\n version,\n });\n\n // Load the datastore_detection heuristic\n const datastoreHeuristic = this.mapper.getHeuristic(\"datastore_detection\");\n if (!datastoreHeuristic) {\n throw new CLIError(\n \"datastore_detection heuristic not found in analyzer\",\n ErrorCategory.VALIDATION,\n [\n \"The analyzer configuration is missing the datastore_detection heuristic\",\n \"Run `npm run build:spec` to recompile the analyzer artifacts\"\n ]\n );\n }\n\n const importPatterns = datastoreHeuristic.parameters?.patterns ?? [];\n const namingIndicators = datastoreHeuristic.parameters?.naming_indicators ?? [];\n\n // Map to track signals by datastore name\n const datastoreSignals = new Map<\n string,\n Array<{\n sourceFile: string;\n importPattern?: string;\n functionPatterns: string[];\n }>\n >();\n // Tracks the first import target that produced each normalized datastore name,\n // so we can warn when distinct imports collapse to the same element ID.\n const datastoreFirstSource = new Map<string, string>();\n\n // Resolve internal project name once (search_graph and query_graph require name, not path)\n const projectName = await this.resolveProjectName(client, projectRoot);\n\n // Try to use query_graph to traverse IMPORTS edges (1 call instead of many)\n // This implements the spec algorithm requirement for IMPORTS edge traversal\n let allNodes: CbmGraphNode[] = [];\n try {\n // Query for all IMPORTS edges and their connected nodes\n const importsQuery = `\n MATCH (n1)-[r:IMPORTS]->(n2)\n RETURN DISTINCT n1 as node\n UNION\n MATCH (n1)-[r:IMPORTS]->(n2)\n RETURN DISTINCT n2 as node\n `;\n\n const queryResponse = await client.invokeTool(\"query_graph\", {\n query: importsQuery,\n project: projectName,\n });\n\n // Extract nodes from query response\n if (queryResponse && typeof queryResponse === \"object\" && Array.isArray((queryResponse as { records?: unknown[] }).records)) {\n const records = (queryResponse as { records?: Array<{ node?: CbmGraphNode }> }).records || [];\n allNodes = records\n .map((r) => r.node)\n .filter((n) => n && typeof n === \"object\") as CbmGraphNode[];\n }\n } catch (error) {\n // Fallback to search_graph if query_graph is unavailable\n // Warn the user that we're using degraded search semantics\n const errorMsg = error instanceof Error ? error.message : String(error);\n handleWarning(\n \"query_graph tool failed; falling back to search_graph for datastore detection\",\n [\n `Error: ${errorMsg}`,\n \"Using search_graph will return ALL nodes instead of only IMPORTS-connected nodes\",\n \"Datastore detection results may include false positives or be less accurate\",\n \"Ensure query_graph is supported by your analyzer version for best results\"\n ]\n );\n\n // This provides backward compatibility while still attempting IMPORTS traversal\n const nodeLabels = this.mapper.getNodeLabels();\n for (const label of nodeLabels) {\n const searchResponse = await client.invokeTool(\"search_graph\", {\n label,\n project: projectName,\n });\n\n const nodes = this.validateSearchResponse(searchResponse);\n allNodes.push(...nodes);\n }\n }\n\n // Process all nodes to find datastore signals\n for (const node of allNodes) {\n const filePath = node.file_path ?? \"\";\n\n // For import target detection, check the node's import target name (from label/properties)\n // not the file path. IMPORTS edges connect to target nodes like \"mongodb\", \"pg\"\n const importTargetName = String(node.label ?? node.properties?.name ?? node.id ?? \"\").toLowerCase();\n\n // Check if import target matches datastore-related patterns\n let matchedImportPattern: string | undefined;\n for (const pattern of importPatterns) {\n if (this.matchPattern(importTargetName, pattern)) {\n matchedImportPattern = pattern;\n break;\n }\n }\n\n // Check if node name contains datastore naming indicators\n const nodeName = String(node.properties?.name ?? \"\").toLowerCase();\n const matchedIndicators = namingIndicators.filter((indicator) =>\n nodeName.includes(indicator.toLowerCase())\n );\n\n // Skip if no import pattern matched AND no naming indicators matched\n if (!matchedImportPattern && matchedIndicators.length === 0) {\n continue;\n }\n\n // Extract datastore name from import target name or node properties\n let datastoreName = this.inferDatastoreName(\n filePath,\n node,\n matchedImportPattern,\n importTargetName\n );\n if (!datastoreName) {\n continue;\n }\n\n // Normalize to lowercase and replace special chars\n datastoreName = toKebabCase(datastoreName);\n\n // Get or create entry for this datastore; warn when distinct import targets\n // normalize to the same name (merging is intentional, but the collision is surfaced)\n if (!datastoreSignals.has(datastoreName)) {\n datastoreSignals.set(datastoreName, []);\n datastoreFirstSource.set(datastoreName, importTargetName);\n } else if (datastoreFirstSource.get(datastoreName) !== importTargetName) {\n handleWarning(\n `Multiple imports normalize to datastore name \"${datastoreName}\"`,\n [\n `First: ${datastoreFirstSource.get(datastoreName)!}`,\n `Conflict: ${importTargetName}`,\n `Use \"dr add\" with a custom name to distinguish them`,\n ]\n );\n }\n\n const signals = datastoreSignals.get(datastoreName)!;\n\n // Convert file path to relative\n let relativeFile = filePath;\n if (filePath && projectRoot) {\n try {\n relativeFile = path.relative(projectRoot, filePath);\n } catch (error) {\n // Log warning if relative path fails but continue with absolute path\n const errorMsg = error instanceof Error ? error.message : String(error);\n handleWarning(\n `Failed to compute relative path in datastore detection`,\n [\n `File: ${filePath}`,\n `Error: ${errorMsg}`,\n \"Using absolute file path instead of relative\",\n ]\n );\n // relativeFile keeps its original absolute value\n }\n }\n\n // Add signal for this source file\n signals.push({\n sourceFile: relativeFile,\n importPattern: matchedImportPattern,\n functionPatterns: matchedIndicators,\n });\n }\n\n // Transform collected signals into DatastoreCandidate objects\n const candidates: DatastoreCandidate[] = [];\n\n // Convert entries to array to avoid TypeScript MapIterator issues\n const entriesArray = Array.from(datastoreSignals.entries());\n for (const [datastoreName, signals] of entriesArray) {\n // Group by source file and aggregate evidence\n const inferredFromMap = new Map<\n string,\n {\n importPatterns: Set<string>;\n functionPatterns: Set<string>;\n }\n >();\n\n for (const signal of signals) {\n if (!inferredFromMap.has(signal.sourceFile)) {\n inferredFromMap.set(signal.sourceFile, {\n importPatterns: new Set(),\n functionPatterns: new Set(),\n });\n }\n\n const entry = inferredFromMap.get(signal.sourceFile)!;\n if (signal.importPattern) {\n entry.importPatterns.add(signal.importPattern);\n }\n signal.functionPatterns.forEach((fp) => entry.functionPatterns.add(fp));\n }\n\n // Create candidate with aggregated evidence\n let inferredFromMapEntries = Array.from(inferredFromMap.entries());\n\n // Apply test code exclusion filter: remove test files from inferred_from\n inferredFromMapEntries = inferredFromMapEntries.filter(([sourceFile]) => {\n // Create a temporary object with source_file to reuse isTestCode()\n const tempCandidate = {\n source_file: sourceFile,\n };\n return !this.isTestCode(tempCandidate);\n });\n\n // Skip candidates where all sources are test code\n if (inferredFromMapEntries.length === 0) {\n continue;\n }\n\n const inferredFrom = inferredFromMapEntries.map(\n ([sourceFile, evidence]) => ({\n source_file: sourceFile,\n import_pattern: Array.from(evidence.importPatterns).join(\", \") || datastoreName,\n function_patterns: Array.from(evidence.functionPatterns),\n })\n );\n\n candidates.push({\n suggested_layer: \"data-store\",\n suggested_name: datastoreName,\n inferred_from: inferredFrom,\n confidence: \"low\",\n notes: `Inferred from ${inferredFrom.length} source file(s) based on datastore detection heuristics`,\n });\n }\n\n // ISSUE-003: Browser storage detection.\n // localStorage and sessionStorage are global browser APIs \u2014 they don't appear in the\n // IMPORTS graph. Query for Function nodes in files that are commonly associated with\n // browser-side state persistence (store files, auth files, preference files).\n const browserStorageTypes = [\"local-storage\", \"session-storage\"] as const;\n const browserStorageSignals = new Map<string, Set<string>>();\n\n try {\n const storageQuery = `\n MATCH (n:Function)\n WHERE n.file_path CONTAINS 'Store' OR n.file_path CONTAINS 'store'\n OR n.file_path CONTAINS 'storage' OR n.file_path CONTAINS 'Storage'\n OR n.file_path CONTAINS 'auth' OR n.file_path CONTAINS 'Auth'\n OR n.file_path CONTAINS 'preference' OR n.file_path CONTAINS 'Preference'\n OR n.file_path CONTAINS 'layout' OR n.file_path CONTAINS 'Layout'\n OR n.file_path CONTAINS 'settings' OR n.file_path CONTAINS 'Settings'\n RETURN n\n `;\n\n const storageResp = await client.invokeTool(\"query_graph\", {\n query: storageQuery,\n project: projectName,\n });\n\n if (storageResp && typeof storageResp === \"object\") {\n const records = (storageResp as { records?: Array<{ n?: CbmGraphNode }> }).records ?? [];\n for (const record of records) {\n const node = record.n;\n if (!node?.file_path) continue;\n\n let relFile = node.file_path;\n try { relFile = path.relative(projectRoot, node.file_path); } catch { /* keep absolute */ }\n\n // Heuristic: files with 'session' in name likely use sessionStorage;\n // otherwise assume localStorage (more common pattern).\n const storageKey = /session/i.test(relFile) ? \"session-storage\" : \"local-storage\";\n if (!browserStorageSignals.has(storageKey)) {\n browserStorageSignals.set(storageKey, new Set());\n }\n browserStorageSignals.get(storageKey)!.add(relFile);\n }\n }\n } catch {\n // query_graph not available \u2014 skip browser storage detection silently\n }\n\n for (const storageType of browserStorageTypes) {\n const sourceFiles = browserStorageSignals.get(storageType);\n if (!sourceFiles || sourceFiles.size === 0) continue;\n\n const accessPattern = storageType === \"local-storage\" ? \"localStorage\" : \"sessionStorage\";\n const inferredFrom = Array.from(sourceFiles)\n .filter((f) => !this.isTestCode({ source_file: f }))\n .map((f) => ({\n source_file: f,\n import_pattern: accessPattern,\n function_patterns: [],\n }));\n\n if (inferredFrom.length > 0) {\n candidates.push({\n suggested_layer: \"data-store\",\n suggested_name: storageType,\n inferred_from: inferredFrom,\n confidence: \"low\",\n notes: `Inferred from ${inferredFrom.length} source file(s) using browser ${accessPattern} (global API, no import required)`,\n });\n }\n }\n\n return candidates;\n } finally {\n client.close();\n }\n }\n\n /**\n * Infer a datastore name from import target name, file path, node properties, or import pattern\n *\n * @private\n */\n private inferDatastoreName(\n filePath: string,\n node: CbmGraphNode,\n matchedPattern?: string,\n importTargetName?: string\n ): string | undefined {\n // Priority 1: Use import target name directly if it matched a pattern\n // This is the most reliable signal for import-based detection (e.g., \"mongodb\", \"pg\")\n if (importTargetName && matchedPattern) {\n // If the import target matches the pattern, use it as-is\n if (this.matchPattern(importTargetName, matchedPattern)) {\n return importTargetName;\n }\n }\n\n // Priority 2: Try to extract from import pattern (e.g., \"mongodb\" from pattern)\n if (matchedPattern) {\n // Extract database name from pattern like \"mongodb\", \"pg\", etc.\n const match = matchedPattern.match(/[a-z]+/i);\n if (match) {\n return match[0];\n }\n }\n\n // Priority 3: Try to extract from file name (e.g., \"users.migration.ts\" -> \"users\")\n const fileName = path.basename(filePath);\n if (fileName.includes(\"migration\") || fileName.includes(\"schema\")) {\n const match = fileName.match(/^([a-z0-9_-]+)/i);\n if (match && match[1]) {\n return match[1];\n }\n }\n\n // Priority 4: Try to extract from node name or label\n const nodeName = String(node.label ?? node.id ?? \"\");\n\n // Check if the label is a known datastore-related type by checking mapping\n // Generic structural labels like Function, Method, Class, Module, Route should be excluded\n if (nodeName) {\n const nodeMapping = this.mapper.getNodeMapping(nodeName);\n // If the label is in the mapping and NOT a generic structural type, use it\n if (nodeMapping && nodeMapping.dr_layer !== \"api\" && nodeMapping.dr_layer !== \"application\") {\n return nodeName;\n }\n\n // Also check if it explicitly looks like a datastore name (e.g., \"Database\", \"Repository\")\n if (nodeName.toLowerCase().includes(\"database\") ||\n nodeName.toLowerCase().includes(\"repository\") ||\n nodeName.toLowerCase().includes(\"datastore\")) {\n return nodeName;\n }\n }\n\n // Fallback: use a generic name based on what was detected\n return undefined;\n }\n\n /**\n * Query for callers of a specific function or symbol\n *\n * Delegates to trace_path MCP tool with direction=\"inbound\" to find\n * all functions that call the specified qualified name. Applies depth clamping\n * (default 3, max 10) and transforms CBM response nodes into CallGraphNode objects\n * using edge-type mappings from the analyzer.\n *\n * @param projectRoot Absolute path to the project root\n * @param qualifiedName Fully qualified symbol name (e.g., \"com.example.UserService.getUser\")\n * @param depth Maximum traversal depth (default 3, clamped to max 10)\n * @returns Array of callers in the call graph\n * @throws CLIError if project not indexed or analyzer not installed\n */\n async callers(\n projectRoot: string,\n qualifiedName: string,\n depth?: number\n ): Promise<CallGraphNode[]> {\n return this.traceCallPath(projectRoot, qualifiedName, depth, \"inbound\");\n }\n\n /**\n * Query for callees of a specific function or symbol\n *\n * Delegates to trace_path MCP tool with direction=\"outbound\" to find\n * all functions called by the specified qualified name. Applies depth clamping\n * (default 3, max 10) and transforms CBM response nodes into CallGraphNode objects\n * using edge-type mappings from the analyzer.\n *\n * @param projectRoot Absolute path to the project root\n * @param qualifiedName Fully qualified symbol name (e.g., \"com.example.UserService.getUser\")\n * @param depth Maximum traversal depth (default 3, clamped to max 10)\n * @returns Array of callees in the call graph\n * @throws CLIError if project not indexed or analyzer not installed\n */\n async callees(\n projectRoot: string,\n qualifiedName: string,\n depth?: number\n ): Promise<CallGraphNode[]> {\n return this.traceCallPath(projectRoot, qualifiedName, depth, \"outbound\");\n }\n\n /**\n * Cypher fallback for call graph traversal when trace_path is unavailable.\n *\n * Uses query_graph directly with CALLS-edge Cypher when trace_path returns a\n * non-array nodes field (i.e. is not implemented in the binary).\n *\n * @private\n */\n private async callGraphViaCypher(\n binaryPath: string,\n projectName: string,\n qualifiedName: string,\n direction: \"outbound\" | \"inbound\",\n depth: number,\n projectRoot: string\n ): Promise<CallGraphNode[]> {\n // Escape single quotes for Cypher string literals (ISO SQL-style doubling)\n const safeQN = qualifiedName.replace(/'/g, \"''\");\n const limit = Math.min(depth * 20, 100);\n const cypher = direction === \"inbound\"\n ? `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}`\n : `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}`;\n\n if (depth > 1) {\n handleWarning(\n `Cypher CALLS fallback: depth ${depth} requested but fallback only traverses 1 hop`,\n [\"Multi-hop traversal requires trace_path support in the analyzer binary\"]\n );\n }\n\n // Spawn a fresh client \u2014 the binary closes the stdio pipe after an unimplemented\n // method call, so the caller's trace_path connection cannot be reused here.\n const client = new StdioClient();\n try {\n client.spawn(binaryPath);\n const version = await getCliVersion();\n await client.initialize({ name: \"dr-cli\", version });\n\n const queryResponse = await client.invokeTool(\"query_graph\", {\n query: cypher,\n project: projectName,\n }) as { columns?: string[]; rows?: unknown[][]; total?: number } | null | undefined;\n\n const cols: string[] = queryResponse?.columns ?? [];\n const tuples: unknown[][] = (queryResponse?.rows as unknown[][] | undefined) ?? [];\n return tuples.map((row) => {\n const obj: Record<string, unknown> = {};\n cols.forEach((col, i) => {\n // Strip table-alias prefix (e.g. \"caller.id\" \u2192 \"id\")\n const key = col.includes(\".\") ? col.slice(col.lastIndexOf(\".\") + 1) : col;\n obj[key] = row[i];\n });\n return shapeCallGraphNode(obj, projectRoot);\n });\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : String(error);\n handleWarning(\n \"Cypher CALLS fallback failed \u2014 returning empty call graph\",\n [\n `Error: ${errorMsg}`,\n \"Neither trace_path nor query_graph is available for call graph traversal\",\n ]\n );\n return [];\n } finally {\n client.close();\n }\n }\n\n /**\n * Internal helper to trace call paths in either direction\n *\n * Handles common logic for callers() and callees(): pre-flight checks,\n * depth clamping, tool invocation, and response transformation.\n *\n * @private\n */\n private async traceCallPath(\n projectRoot: string,\n qualifiedName: string,\n depth: number | undefined,\n direction: \"outbound\" | \"inbound\"\n ): Promise<CallGraphNode[]> {\n // Check that the project is indexed (same pre-flight checks as endpoints)\n const status = await this.status(projectRoot);\n if (!status.indexed) {\n throw new CLIError(\n `Project not indexed: ${projectRoot}`,\n ErrorCategory.NOT_FOUND,\n [\n \"Run `dr analyzer index` to index the project\",\n \"Ensure codebase-memory-mcp is installed and working\",\n ]\n );\n }\n\n // Use detection result from status() instead of re-detecting\n const detection = status.detected;\n if (!detection.installed) {\n throw new CLIError(\n \"CBM analyzer not installed\",\n ErrorCategory.NOT_FOUND,\n [\"Install codebase-memory-mcp: npm install -g codebase-memory-mcp\"]\n );\n }\n\n // Clamp depth: default 3, max 10\n const clampedDepth = clampDepth(depth);\n\n const client = new StdioClient();\n\n try {\n client.spawn(detection.binary_path);\n\n const version = await getCliVersion();\n await client.initialize({\n name: \"dr-cli\",\n version,\n });\n\n // Resolve internal project name \u2014 trace_path requires the CBM name, not the absolute path\n const projectName = await this.resolveProjectName(client, projectRoot);\n\n // Call trace_path with the clamped depth\n const traceResponse = (await client.invokeTool(\"trace_path\", {\n function_name: qualifiedName,\n direction,\n depth: clampedDepth,\n project: projectName,\n })) as {\n nodes?: Array<{\n id: string;\n qualified_name?: string;\n source_file?: string;\n source_symbol?: string;\n file_path?: string;\n depth?: number;\n [key: string]: unknown;\n }>;\n edges?: Array<{\n from_node: string;\n to_node: string;\n type?: string;\n [key: string]: unknown;\n }>;\n [key: string]: unknown;\n };\n\n // Validate response structure\n if (!traceResponse || typeof traceResponse !== \"object\") {\n throw new CLIError(\n \"Invalid response from trace_path\",\n ErrorCategory.SYSTEM,\n [\"Ensure codebase-memory-mcp is properly installed and functional\"]\n );\n }\n\n // Gracefully handle missing nodes/edges \u2014 return empty rather than crashing.\n // The CBM backend may not implement trace_path or may return an error shape.\n if (!Array.isArray(traceResponse.nodes)) {\n // trace_path not implemented \u2014 fall back to Cypher CALLS query.\n // Pass binary_path (not client): the binary closes the pipe after an unimplemented call.\n return this.callGraphViaCypher(detection.binary_path, projectName, qualifiedName, direction, clampedDepth, projectRoot);\n }\n\n if (!Array.isArray(traceResponse.edges)) {\n handleWarning(\n \"trace_path returned no edges array \u2014 returning empty call graph\",\n [\n `Expected array, got ${typeof traceResponse.edges}`,\n \"Ensure codebase-memory-mcp supports trace_path and the project is indexed\",\n ]\n );\n return [];\n }\n\n const nodes = traceResponse.nodes as Array<{\n id: string;\n qualified_name?: string;\n source_file?: string;\n source_symbol?: string;\n file_path?: string;\n depth?: number;\n [key: string]: unknown;\n }>;\n\n const edges = traceResponse.edges as Array<{\n from_node: string;\n to_node: string;\n type?: string;\n [key: string]: unknown;\n }>;\n\n // Get valid edge types from the mapping loader (don't hardcode)\n const validEdgeTypes = this.mapper.getEdgeTypes();\n\n // Transform nodes to CallGraphNode objects\n const callGraphNodes: CallGraphNode[] = [];\n\n for (const node of nodes) {\n const nodeQualifiedName = node.qualified_name || node.id;\n\n // Shape the node using utility function\n let callGraphNode = shapeCallGraphNode(node, projectRoot);\n\n // Determine edge type using utility function\n const nodeDepth = typeof node.depth === \"number\" ? node.depth : 0;\n const edgeType = determineEdgeType(\n nodeDepth,\n edges,\n nodeQualifiedName,\n validEdgeTypes\n );\n\n // Apply edge type to the shaped node\n callGraphNode.edge_type = edgeType;\n\n callGraphNodes.push(callGraphNode);\n }\n\n return callGraphNodes;\n } finally {\n client.close();\n }\n }\n\n /**\n * Shape a single CBM graph node into a route object with defaults applied\n *\n * This is extracted to be testable - applies the defaulting logic used in verify()\n * to ensure empty/missing properties are handled consistently.\n *\n * @param node Raw node from CBM graph search\n * @returns Shaped route object with defaults applied\n * @internal\n */\n shapeRoute(node: CbmGraphNode): {\n id: string;\n http_method: string;\n http_path: string;\n handler: string;\n source_file: string;\n source_symbol: string;\n } {\n const properties = node.properties ?? {};\n const nodeName = String(node.name ?? node.id ?? \"\");\n\n // Extract HTTP method: properties first, then parse from qualified_name\n // qualified_name format: __route__METHOD__/path (e.g. __route__GET__/users)\n let httpMethod = \"GET\";\n if (typeof properties.method === \"string\") {\n httpMethod = properties.method.toUpperCase();\n } else {\n const qn = String(node.qualified_name ?? \"\");\n const methodMatch = qn.match(/^__route__([A-Z]+)__/);\n const extracted = methodMatch?.[1] ?? \"\";\n if (extracted && (VALID_HTTP_METHODS as readonly string[]).includes(extracted)) {\n httpMethod = extracted;\n }\n }\n\n // Extract HTTP path: properties first, then node.name if it looks like a URL\n const httpPath = typeof properties.path === \"string\"\n ? properties.path\n : (typeof node.name === \"string\" && node.name.startsWith(\"/\") ? node.name : \"/\");\n\n const handler = String(properties.handler_name ?? \"\");\n const sourceFile = node.file_path ?? \"\";\n // Fall back to source_symbol then handler_name if symbol is absent (e.g. Python route handlers)\n const sourceSymbol = String(properties.symbol ?? properties.source_symbol ?? properties.handler_name ?? \"\");\n\n // Construct route ID (use file:symbol as primary key for matching)\n const routeId = sourceFile && sourceSymbol\n ? `${sourceFile}:${sourceSymbol}`\n : (nodeName || node.id || \"(unknown)\");\n\n return {\n id: routeId,\n http_method: httpMethod,\n http_path: httpPath,\n handler,\n source_file: sourceFile,\n source_symbol: sourceSymbol,\n };\n }\n\n /**\n * Verify that graph-discovered routes align with model endpoints\n *\n * Queries the CBM graph for Route nodes, shapes them via the Route mapping,\n * and delegates comparison to VerifyEngine. Entirely read-only.\n *\n * @param projectRoot Absolute path to the project root\n * @param options Verification options\n * @returns Comprehensive verification report\n * @throws CLIError if project not indexed or analyzer not installed\n */\n async verify(\n projectRoot: string,\n options: VerifyOptions\n ): Promise<VerifyReport> {\n // Check that the project is indexed\n const status = await this.status(projectRoot);\n if (!status.indexed) {\n throw new CLIError(\n `Project not indexed: ${projectRoot}`,\n ErrorCategory.NOT_FOUND,\n [\n \"Run `dr analyzer index` to index the project\",\n \"Ensure codebase-memory-mcp is installed and working\",\n ]\n );\n }\n\n // Use detection result from status() instead of re-detecting\n const detection = status.detected;\n if (!detection.installed) {\n throw new CLIError(\n \"CBM analyzer not installed\",\n ErrorCategory.NOT_FOUND,\n [\"Install codebase-memory-mcp: npm install -g codebase-memory-mcp\"]\n );\n }\n\n // Get the Route mapping BEFORE initializing client to validate it exists\n const routeMapping = this.mapper.getNodeMapping(\"Route\");\n if (!routeMapping) {\n throw new CLIError(\n \"Route mapping not found in analyzer\",\n ErrorCategory.VALIDATION,\n [\"Run `npm run build:spec` to recompile the analyzer artifacts\"]\n );\n }\n\n const client = new StdioClient();\n\n try {\n client.spawn(detection.binary_path);\n\n const version = await getCliVersion();\n await client.initialize({\n name: \"dr-cli\",\n version,\n });\n\n // Resolve internal project name for search_graph (requires name, not absolute path)\n const projectName = await this.resolveProjectName(client, projectRoot);\n\n // Search for Route nodes using the CBM label from the mapping\n const searchResponse = await client.invokeTool(\"search_graph\", {\n label: routeMapping.cbm_label,\n project: projectName,\n });\n\n const allNodes = this.validateSearchResponse(searchResponse);\n // Keep only nodes whose qualified_name follows the __route__METHOD__/path\n // convention. Nodes that don't match are graph artifacts (documentation\n // strings, base URLs, etc.) that were incidentally labelled Route by the\n // indexer \u2014 not actual HTTP routes.\n const nodes = allNodes\n .filter((n) => /^__route__[A-Z]+__\\//.test(String(n.qualified_name ?? \"\")))\n .filter((n) => isLikelyHttpRoute(String(n.qualified_name ?? \"\")));\n\n // Shape routes via mapping; drop unattributed nodes (same filter as endpoints())\n const routes = nodes\n .map((node) => this.shapeRoute(node))\n .filter((r) => r.source_file);\n\n // Delegate to VerifyEngine\n const engine = new VerifyEngine();\n const indexMeta = status.index_meta;\n const analyzerName = this.name;\n const report = await engine.computeReport(projectRoot, routes, options, analyzerName, indexMeta);\n\n return report;\n } finally {\n client.close();\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["meta"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { minimatch } from "minimatch";
|
|
2
|
+
function getKnownHeuristicNames() {
|
|
3
|
+
return [
|
|
4
|
+
"min_fan_in",
|
|
5
|
+
"naming_patterns",
|
|
6
|
+
"is_entry_point",
|
|
7
|
+
"directory_match",
|
|
8
|
+
"class_is_service",
|
|
9
|
+
"service_class_naming",
|
|
10
|
+
"handles_route"
|
|
11
|
+
];
|
|
12
|
+
}
|
|
13
|
+
function evaluateHeuristic(heuristic, node, sourceFile = "") {
|
|
14
|
+
switch (heuristic.name) {
|
|
15
|
+
case "min_fan_in": {
|
|
16
|
+
const threshold = heuristic.parameters?.threshold ?? 5;
|
|
17
|
+
return (typeof node.fan_in == "number" ? node.fan_in : 0) >= threshold;
|
|
18
|
+
}
|
|
19
|
+
case "naming_patterns": {
|
|
20
|
+
const suffixes = heuristic.parameters?.service_suffixes ?? [], name = String(node.name ?? "").toLowerCase();
|
|
21
|
+
return suffixes.some((suffix) => name.endsWith(suffix.toLowerCase()));
|
|
22
|
+
}
|
|
23
|
+
case "is_entry_point": {
|
|
24
|
+
const patterns = heuristic.parameters?.entry_point_patterns ?? [], name = String(node.name ?? "").toLowerCase();
|
|
25
|
+
return patterns.some((pattern) => name.includes(pattern.toLowerCase()));
|
|
26
|
+
}
|
|
27
|
+
case "directory_match":
|
|
28
|
+
return (heuristic.parameters?.patterns ?? []).some((pattern) => matchPattern(sourceFile, pattern));
|
|
29
|
+
case "class_is_service": {
|
|
30
|
+
const hasInterfaceImpl = node.implements_interface === !0, hasDependencyInjection = node.dependency_injection === !0, publicMethodsCount = Number(node.public_methods_count ?? 0), threshold = heuristic.parameters?.threshold ?? 3;
|
|
31
|
+
return hasInterfaceImpl || hasDependencyInjection || publicMethodsCount >= threshold;
|
|
32
|
+
}
|
|
33
|
+
case "service_class_naming": {
|
|
34
|
+
const prefixes = heuristic.parameters?.service_method_prefixes ?? [], name = String(node.name ?? "").toLowerCase();
|
|
35
|
+
return prefixes.some((prefix) => name.startsWith(prefix.toLowerCase()));
|
|
36
|
+
}
|
|
37
|
+
case "handles_route":
|
|
38
|
+
return node.has_route_handler === !0 || node.is_decorated === !0;
|
|
39
|
+
default:
|
|
40
|
+
return !1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function capConfidence(confidence) {
|
|
44
|
+
return confidence === "high" ? "medium" : confidence;
|
|
45
|
+
}
|
|
46
|
+
function matchPattern(filePath, pattern) {
|
|
47
|
+
return minimatch(filePath, pattern);
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
capConfidence,
|
|
51
|
+
evaluateHeuristic,
|
|
52
|
+
getKnownHeuristicNames,
|
|
53
|
+
matchPattern
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=heuristic-utils.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/analyzers/heuristic-utils.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Utilities for heuristic evaluation and service/datastore detection\n */\n\nimport { minimatch } from \"minimatch\";\nimport type { AnalyzerHeuristic } from \"./types.js\";\n\n/**\n * Get the list of known heuristic names\n * @returns Array of heuristic names that are implemented\n */\nexport function getKnownHeuristicNames(): string[] {\n return [\n \"min_fan_in\",\n \"naming_patterns\",\n \"is_entry_point\",\n \"directory_match\",\n \"class_is_service\",\n \"service_class_naming\",\n \"handles_route\",\n ];\n}\n\n/**\n * Evaluate whether a heuristic is met for a given node\n *\n * @param heuristic The heuristic to evaluate\n * @param node The node properties to evaluate against\n * @param sourceFile The source file path (for directory_match heuristics)\n * @returns True if the heuristic is met, false otherwise\n */\nexport function evaluateHeuristic(\n heuristic: AnalyzerHeuristic,\n node: Record<string, unknown>,\n sourceFile: string = \"\"\n): boolean {\n switch (heuristic.name) {\n case \"min_fan_in\": {\n const threshold = heuristic.parameters?.threshold ?? 5;\n const fanIn = typeof node.fan_in === \"number\" ? node.fan_in : 0;\n return fanIn >= threshold;\n }\n\n case \"naming_patterns\": {\n const suffixes = heuristic.parameters?.service_suffixes ?? [];\n const name = String(node.name ?? \"\").toLowerCase();\n return suffixes.some((suffix) => name.endsWith(suffix.toLowerCase()));\n }\n\n case \"is_entry_point\": {\n const patterns = heuristic.parameters?.entry_point_patterns ?? [];\n const name = String(node.name ?? \"\").toLowerCase();\n return patterns.some((pattern) => name.includes(pattern.toLowerCase()));\n }\n\n case \"directory_match\": {\n const patterns = heuristic.parameters?.patterns ?? [];\n return patterns.some((pattern) => matchPattern(sourceFile, pattern));\n }\n\n case \"class_is_service\": {\n const hasInterfaceImpl = node.implements_interface === true;\n const hasDependencyInjection = node.dependency_injection === true;\n const publicMethodsCount = Number(node.public_methods_count ?? 0);\n const threshold = heuristic.parameters?.threshold ?? 3;\n return (\n hasInterfaceImpl ||\n hasDependencyInjection ||\n publicMethodsCount >= threshold\n );\n }\n\n case \"service_class_naming\": {\n const prefixes = heuristic.parameters?.service_method_prefixes ?? [];\n const name = String(node.name ?? \"\").toLowerCase();\n return prefixes.some((prefix) => name.startsWith(prefix.toLowerCase()));\n }\n\n case \"handles_route\": {\n return node.has_route_handler === true || node.is_decorated === true;\n }\n\n default:\n return false;\n }\n}\n\n/**\n * Cap confidence at \"medium\" for service candidates\n */\nexport function capConfidence(\n confidence: \"high\" | \"medium\" | \"low\"\n): \"medium\" | \"low\" {\n if (confidence === \"high\") {\n return \"medium\";\n }\n return confidence;\n}\n\n/**\n * Match a file path against a glob pattern\n *\n * Uses minimatch for consistent glob pattern matching semantics.\n * Supports standard glob patterns like:\n * - *.ext - match files with extension\n * - src/ - match anything under src directory\n * - tests directories at any depth\n *\n * @param filePath The file path to match\n * @param pattern The glob pattern\n * @returns True if the pattern matches, false otherwise\n */\nexport function matchPattern(filePath: string, pattern: string): boolean {\n return minimatch(filePath, pattern);\n}\n"],
|
|
5
|
+
"mappings": "AAIA,SAAS,iBAAiB;AAOpB,SAAU,yBAAsB;AACpC,SAAO;IACL;IACA;IACA;IACA;IACA;IACA;IACA;;AAEJ;AAUM,SAAU,kBACd,WACA,MACA,aAAqB,IAAE;AAEvB,UAAQ,UAAU,MAAM;IACtB,KAAK,cAAc;AACjB,YAAM,YAAY,UAAU,YAAY,aAAa;AAErD,cADc,OAAO,KAAK,UAAW,WAAW,KAAK,SAAS,MAC9C;IAClB;IAEA,KAAK,mBAAmB;AACtB,YAAM,WAAW,UAAU,YAAY,oBAAoB,CAAA,GACrD,OAAO,OAAO,KAAK,QAAQ,EAAE,EAAE,YAAW;AAChD,aAAO,SAAS,KAAK,CAAC,WAAW,KAAK,SAAS,OAAO,YAAW,CAAE,CAAC;IACtE;IAEA,KAAK,kBAAkB;AACrB,YAAM,WAAW,UAAU,YAAY,wBAAwB,CAAA,GACzD,OAAO,OAAO,KAAK,QAAQ,EAAE,EAAE,YAAW;AAChD,aAAO,SAAS,KAAK,CAAC,YAAY,KAAK,SAAS,QAAQ,YAAW,CAAE,CAAC;IACxE;IAEA,KAAK;AAEH,cADiB,UAAU,YAAY,YAAY,CAAA,GACnC,KAAK,CAAC,YAAY,aAAa,YAAY,OAAO,CAAC;IAGrE,KAAK,oBAAoB;AACvB,YAAM,mBAAmB,KAAK,yBAAyB,IACjD,yBAAyB,KAAK,yBAAyB,IACvD,qBAAqB,OAAO,KAAK,wBAAwB,CAAC,GAC1D,YAAY,UAAU,YAAY,aAAa;AACrD,aACE,oBACA,0BACA,sBAAsB;IAE1B;IAEA,KAAK,wBAAwB;AAC3B,YAAM,WAAW,UAAU,YAAY,2BAA2B,CAAA,GAC5D,OAAO,OAAO,KAAK,QAAQ,EAAE,EAAE,YAAW;AAChD,aAAO,SAAS,KAAK,CAAC,WAAW,KAAK,WAAW,OAAO,YAAW,CAAE,CAAC;IACxE;IAEA,KAAK;AACH,aAAO,KAAK,sBAAsB,MAAQ,KAAK,iBAAiB;IAGlE;AACE,aAAO;EACX;AACF;AAKM,SAAU,cACd,YAAqC;AAErC,SAAI,eAAe,SACV,WAEF;AACT;AAeM,SAAU,aAAa,UAAkB,SAAe;AAC5D,SAAO,UAAU,UAAU,OAAO;AACpC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { CLIError, ErrorCategory } from "../utils/errors.js";
|
|
5
|
+
function validateAnalyzerName(analyzerName) {
|
|
6
|
+
if (!analyzerName)
|
|
7
|
+
throw new CLIError("Analyzer name cannot be empty", ErrorCategory.VALIDATION, []);
|
|
8
|
+
if (analyzerName.includes("/") || analyzerName.includes("\\"))
|
|
9
|
+
throw new CLIError("Analyzer name cannot contain path separators (/ or \\)", ErrorCategory.VALIDATION, []);
|
|
10
|
+
if (analyzerName.includes(".."))
|
|
11
|
+
throw new CLIError("Analyzer name cannot contain parent directory references (..)", ErrorCategory.VALIDATION, []);
|
|
12
|
+
}
|
|
13
|
+
class MappingLoader {
|
|
14
|
+
artifact;
|
|
15
|
+
analyzerName;
|
|
16
|
+
constructor(artifact, analyzerName) {
|
|
17
|
+
this.artifact = artifact, this.analyzerName = analyzerName;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Load a mapping artifact from the bundled schemas directory
|
|
21
|
+
*
|
|
22
|
+
* @param analyzerName Name of the analyzer (e.g., "cbm")
|
|
23
|
+
* @returns Loaded and validated MappingLoader instance
|
|
24
|
+
* @throws CLIError if artifact is missing, malformed, or analyzerName contains path traversal characters
|
|
25
|
+
*/
|
|
26
|
+
static async load(analyzerName) {
|
|
27
|
+
validateAnalyzerName(analyzerName);
|
|
28
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url)), artifactPath = path.join(currentDir, "../schemas/bundled/analyzers", `${analyzerName}.json`);
|
|
29
|
+
let artifact;
|
|
30
|
+
try {
|
|
31
|
+
const content = await fs.readFile(artifactPath, "utf-8");
|
|
32
|
+
artifact = JSON.parse(content);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
const message = error instanceof Error && "code" in error && error.code === "ENOENT" ? `Analyzer mapping not found: ${analyzerName}` : `Failed to read analyzer mapping: ${error instanceof Error ? error.message : String(error)}`;
|
|
35
|
+
throw new CLIError(message, ErrorCategory.NOT_FOUND, [
|
|
36
|
+
"Run `npm run build:spec` to compile the analyzer mapping artifacts",
|
|
37
|
+
`Check that spec/analyzers/${analyzerName}/ sources exist`
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
if (!artifact || typeof artifact != "object")
|
|
41
|
+
throw new CLIError(`Invalid analyzer mapping structure: expected object, got ${typeof artifact}`, ErrorCategory.VALIDATION, ["Run `npm run build:spec` to recompile the analyzer mapping artifacts"]);
|
|
42
|
+
const obj = artifact, missingKeys = ["nodes_by_label", "edges_by_type", "heuristics"].filter((key) => !(key in obj));
|
|
43
|
+
if (missingKeys.length > 0)
|
|
44
|
+
throw new CLIError(`Invalid analyzer mapping: missing required keys: ${missingKeys.join(", ")}`, ErrorCategory.VALIDATION, [
|
|
45
|
+
"Run `npm run build:spec` to recompile the analyzer mapping artifacts",
|
|
46
|
+
`Ensure spec/analyzers/${analyzerName}/ contains valid layer/node/edge definitions`
|
|
47
|
+
]);
|
|
48
|
+
if (typeof obj.nodes_by_label != "object" || obj.nodes_by_label === null)
|
|
49
|
+
throw new CLIError("Invalid analyzer mapping: nodes_by_label must be an object", ErrorCategory.VALIDATION, ["Run `npm run build:spec` to recompile the analyzer mapping artifacts"]);
|
|
50
|
+
if (typeof obj.edges_by_type != "object" || obj.edges_by_type === null)
|
|
51
|
+
throw new CLIError("Invalid analyzer mapping: edges_by_type must be an object", ErrorCategory.VALIDATION, ["Run `npm run build:spec` to recompile the analyzer mapping artifacts"]);
|
|
52
|
+
if (typeof obj.heuristics != "object" || obj.heuristics === null)
|
|
53
|
+
throw new CLIError("Invalid analyzer mapping: heuristics must be an object", ErrorCategory.VALIDATION, ["Run `npm run build:spec` to recompile the analyzer mapping artifacts"]);
|
|
54
|
+
return new MappingLoader(artifact, analyzerName);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get the mapping for a node label
|
|
58
|
+
*
|
|
59
|
+
* @param label CBM node label (e.g., "Route", "Function")
|
|
60
|
+
* @returns The mapping definition or undefined if not found
|
|
61
|
+
*/
|
|
62
|
+
getNodeMapping(label) {
|
|
63
|
+
return this.artifact.nodes_by_label[label];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get the mapping for an edge type
|
|
67
|
+
*
|
|
68
|
+
* @param type CBM edge type (e.g., "HTTP_CALLS", "HANDLES")
|
|
69
|
+
* @returns The mapping definition or undefined if not found
|
|
70
|
+
*/
|
|
71
|
+
getEdgeMapping(type) {
|
|
72
|
+
return this.artifact.edges_by_type[type];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get a heuristic by name
|
|
76
|
+
*
|
|
77
|
+
* @param name Heuristic name (e.g., "min_fan_in")
|
|
78
|
+
* @returns The heuristic definition or undefined if not found
|
|
79
|
+
*/
|
|
80
|
+
getHeuristic(name) {
|
|
81
|
+
const heuristics = this.artifact.heuristics.heuristics;
|
|
82
|
+
if (Array.isArray(heuristics))
|
|
83
|
+
return heuristics.find((h) => h.name === name);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get all filtering rules
|
|
87
|
+
*
|
|
88
|
+
* @returns Array of filtering rules
|
|
89
|
+
*/
|
|
90
|
+
getFilteringRules() {
|
|
91
|
+
const rules = this.artifact.heuristics.filtering_rules;
|
|
92
|
+
return Array.isArray(rules) ? rules : [];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get the confidence rubric
|
|
96
|
+
*
|
|
97
|
+
* @returns Confidence levels and their score ranges
|
|
98
|
+
*/
|
|
99
|
+
getConfidenceRubric() {
|
|
100
|
+
return this.artifact.heuristics.confidence_rubric;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Get all node labels in the mapping
|
|
104
|
+
*
|
|
105
|
+
* @returns Array of CBM label names
|
|
106
|
+
*/
|
|
107
|
+
getNodeLabels() {
|
|
108
|
+
return Object.keys(this.artifact.nodes_by_label);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get all edge types in the mapping
|
|
112
|
+
*
|
|
113
|
+
* @returns Array of CBM edge type names
|
|
114
|
+
*/
|
|
115
|
+
getEdgeTypes() {
|
|
116
|
+
return Object.keys(this.artifact.edges_by_type);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get analyzer name
|
|
120
|
+
*
|
|
121
|
+
* @returns The name of the loaded analyzer
|
|
122
|
+
*/
|
|
123
|
+
getAnalyzerName() {
|
|
124
|
+
return this.analyzerName;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Get analyzer version
|
|
128
|
+
*
|
|
129
|
+
* @returns The version of the artifact
|
|
130
|
+
*/
|
|
131
|
+
getVersion() {
|
|
132
|
+
return this.artifact.version;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get analyzer metadata (display name, description, homepage, etc.)
|
|
136
|
+
*
|
|
137
|
+
* @returns Analyzer metadata object or undefined if not available
|
|
138
|
+
*/
|
|
139
|
+
getAnalyzerMetadata() {
|
|
140
|
+
return this.artifact.analyzer;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export {
|
|
144
|
+
MappingLoader
|
|
145
|
+
};
|
|
146
|
+
//# sourceMappingURL=mapping-loader.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/analyzers/mapping-loader.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * MappingLoader - reads and validates packed analyzer artifact\n *\n * Loads the compiled CBM mapping from cli/src/schemas/bundled/analyzers/<name>.json\n * and provides typed accessors for node mappings, edge mappings, heuristics, and rules.\n *\n * The loader validates the top-level structure and throws CLIError if the artifact\n * is malformed or missing required keys, with a suggestion to run npm run build:spec.\n */\n\nimport fs from \"fs/promises\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\nimport { CLIError, ErrorCategory } from \"../utils/errors.js\";\nimport type {\n AnalyzerEdgeMapping,\n AnalyzerHeuristic,\n AnalyzerNodeMapping,\n ConfidenceRubric,\n FilteringRule,\n} from \"./types.js\";\n\n/**\n * Validate analyzer name to prevent path traversal attacks\n * Rejects names containing `/`, `\\`, or `..`\n *\n * @param analyzerName Analyzer name to validate\n * @throws CLIError if name contains path traversal characters\n */\nfunction validateAnalyzerName(analyzerName: string): void {\n if (!analyzerName) {\n throw new CLIError(\n \"Analyzer name cannot be empty\",\n ErrorCategory.VALIDATION,\n []\n );\n }\n\n if (analyzerName.includes(\"/\") || analyzerName.includes(\"\\\\\")) {\n throw new CLIError(\n \"Analyzer name cannot contain path separators (/ or \\\\)\",\n ErrorCategory.VALIDATION,\n []\n );\n }\n\n if (analyzerName.includes(\"..\")) {\n throw new CLIError(\n \"Analyzer name cannot contain parent directory references (..)\",\n ErrorCategory.VALIDATION,\n []\n );\n }\n}\n\ninterface AnalyzerManifestMetadata {\n name: string;\n display_name: string;\n description: string;\n homepage: string;\n binary_names?: string[];\n mcp_server_name?: string;\n [key: string]: unknown;\n}\n\ninterface PackedArtifact {\n name: string;\n version: string;\n analyzer?: AnalyzerManifestMetadata;\n nodes_by_label: Record<string, AnalyzerNodeMapping>;\n edges_by_type: Record<string, AnalyzerEdgeMapping>;\n heuristics: {\n confidence_rubric?: ConfidenceRubric;\n heuristics?: AnalyzerHeuristic[];\n filtering_rules?: FilteringRule[];\n [key: string]: unknown;\n };\n [key: string]: unknown;\n}\n\nexport class MappingLoader {\n private artifact: PackedArtifact;\n private analyzerName: string;\n\n private constructor(artifact: PackedArtifact, analyzerName: string) {\n this.artifact = artifact;\n this.analyzerName = analyzerName;\n }\n\n /**\n * Load a mapping artifact from the bundled schemas directory\n *\n * @param analyzerName Name of the analyzer (e.g., \"cbm\")\n * @returns Loaded and validated MappingLoader instance\n * @throws CLIError if artifact is missing, malformed, or analyzerName contains path traversal characters\n */\n static async load(analyzerName: string): Promise<MappingLoader> {\n validateAnalyzerName(analyzerName);\n\n const currentDir = path.dirname(fileURLToPath(import.meta.url));\n const artifactPath = path.join(\n currentDir,\n \"../schemas/bundled/analyzers\",\n `${analyzerName}.json`\n );\n\n let artifact: unknown;\n try {\n const content = await fs.readFile(artifactPath, \"utf-8\");\n artifact = JSON.parse(content);\n } catch (error) {\n const message =\n error instanceof Error && \"code\" in error && error.code === \"ENOENT\"\n ? `Analyzer mapping not found: ${analyzerName}`\n : `Failed to read analyzer mapping: ${error instanceof Error ? error.message : String(error)}`;\n\n throw new CLIError(\n message,\n ErrorCategory.NOT_FOUND,\n [\n \"Run `npm run build:spec` to compile the analyzer mapping artifacts\",\n `Check that spec/analyzers/${analyzerName}/ sources exist`,\n ]\n );\n }\n\n // Validate structure\n if (!artifact || typeof artifact !== \"object\") {\n throw new CLIError(\n `Invalid analyzer mapping structure: expected object, got ${typeof artifact}`,\n ErrorCategory.VALIDATION,\n [\"Run `npm run build:spec` to recompile the analyzer mapping artifacts\"]\n );\n }\n\n const obj = artifact as Record<string, unknown>;\n const requiredKeys = [\"nodes_by_label\", \"edges_by_type\", \"heuristics\"];\n const missingKeys = requiredKeys.filter((key) => !(key in obj));\n\n if (missingKeys.length > 0) {\n throw new CLIError(\n `Invalid analyzer mapping: missing required keys: ${missingKeys.join(\", \")}`,\n ErrorCategory.VALIDATION,\n [\n \"Run `npm run build:spec` to recompile the analyzer mapping artifacts\",\n `Ensure spec/analyzers/${analyzerName}/ contains valid layer/node/edge definitions`,\n ]\n );\n }\n\n // Validate types of required keys\n if (typeof obj.nodes_by_label !== \"object\" || obj.nodes_by_label === null) {\n throw new CLIError(\n `Invalid analyzer mapping: nodes_by_label must be an object`,\n ErrorCategory.VALIDATION,\n [\"Run `npm run build:spec` to recompile the analyzer mapping artifacts\"]\n );\n }\n\n if (typeof obj.edges_by_type !== \"object\" || obj.edges_by_type === null) {\n throw new CLIError(\n `Invalid analyzer mapping: edges_by_type must be an object`,\n ErrorCategory.VALIDATION,\n [\"Run `npm run build:spec` to recompile the analyzer mapping artifacts\"]\n );\n }\n\n if (typeof obj.heuristics !== \"object\" || obj.heuristics === null) {\n throw new CLIError(\n `Invalid analyzer mapping: heuristics must be an object`,\n ErrorCategory.VALIDATION,\n [\"Run `npm run build:spec` to recompile the analyzer mapping artifacts\"]\n );\n }\n\n return new MappingLoader(artifact as PackedArtifact, analyzerName);\n }\n\n /**\n * Get the mapping for a node label\n *\n * @param label CBM node label (e.g., \"Route\", \"Function\")\n * @returns The mapping definition or undefined if not found\n */\n getNodeMapping(label: string): AnalyzerNodeMapping | undefined {\n return this.artifact.nodes_by_label[label];\n }\n\n /**\n * Get the mapping for an edge type\n *\n * @param type CBM edge type (e.g., \"HTTP_CALLS\", \"HANDLES\")\n * @returns The mapping definition or undefined if not found\n */\n getEdgeMapping(type: string): AnalyzerEdgeMapping | undefined {\n return this.artifact.edges_by_type[type];\n }\n\n /**\n * Get a heuristic by name\n *\n * @param name Heuristic name (e.g., \"min_fan_in\")\n * @returns The heuristic definition or undefined if not found\n */\n getHeuristic(name: string): AnalyzerHeuristic | undefined {\n const heuristics = this.artifact.heuristics.heuristics;\n if (!Array.isArray(heuristics)) {\n return undefined;\n }\n return heuristics.find((h) => h.name === name);\n }\n\n /**\n * Get all filtering rules\n *\n * @returns Array of filtering rules\n */\n getFilteringRules(): FilteringRule[] {\n const rules = this.artifact.heuristics.filtering_rules;\n return Array.isArray(rules) ? rules : [];\n }\n\n /**\n * Get the confidence rubric\n *\n * @returns Confidence levels and their score ranges\n */\n getConfidenceRubric(): ConfidenceRubric | undefined {\n return this.artifact.heuristics.confidence_rubric;\n }\n\n /**\n * Get all node labels in the mapping\n *\n * @returns Array of CBM label names\n */\n getNodeLabels(): string[] {\n return Object.keys(this.artifact.nodes_by_label);\n }\n\n /**\n * Get all edge types in the mapping\n *\n * @returns Array of CBM edge type names\n */\n getEdgeTypes(): string[] {\n return Object.keys(this.artifact.edges_by_type);\n }\n\n /**\n * Get analyzer name\n *\n * @returns The name of the loaded analyzer\n */\n getAnalyzerName(): string {\n return this.analyzerName;\n }\n\n /**\n * Get analyzer version\n *\n * @returns The version of the artifact\n */\n getVersion(): string {\n return this.artifact.version;\n }\n\n /**\n * Get analyzer metadata (display name, description, homepage, etc.)\n *\n * @returns Analyzer metadata object or undefined if not available\n */\n getAnalyzerMetadata(): AnalyzerManifestMetadata | undefined {\n return this.artifact.analyzer;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|