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