@documentation-robotics/cli 0.2.0 → 0.3.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 +36 -2
- package/dist/analysis/relationship-classifier.js +2 -9
- package/dist/analysis/relationship-classifier.js.map +2 -2
- 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 +171 -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/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/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 +195 -171
- package/dist/analyzers/cbm-analyzer.js.map +2 -2
- package/dist/analyzers/cbm-responses.js +336 -0
- package/dist/analyzers/cbm-responses.js.map +7 -0
- package/dist/analyzers/edge-relationships.js +45 -0
- package/dist/analyzers/edge-relationships.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 +12 -2
- 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 +167 -0
- package/dist/coding-agents/drbot-mcp.js.map +7 -0
- package/dist/commands/add.js +3 -2
- package/dist/commands/add.js.map +2 -2
- package/dist/commands/analyzer.js +117 -14
- package/dist/commands/analyzer.js.map +2 -2
- package/dist/commands/changeset-verify.js +55 -0
- package/dist/commands/changeset-verify.js.map +7 -0
- package/dist/commands/changeset.js +43 -22
- 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/mcp.js +19 -4
- package/dist/commands/mcp.js.map +2 -2
- package/dist/commands/relationship.js +207 -26
- package/dist/commands/relationship.js.map +2 -2
- package/dist/commands/update.js +42 -23
- package/dist/commands/update.js.map +2 -2
- package/dist/commands/upgrade.js +99 -11
- package/dist/commands/upgrade.js.map +3 -3
- package/dist/commands/version.js +1 -1
- package/dist/commands/visualize.js +4 -3
- package/dist/commands/visualize.js.map +2 -2
- package/dist/core/catalog-migration.js +33 -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 +153 -0
- package/dist/core/changeset-base-index.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 +518 -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/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 +75 -46
- 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/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 +68 -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 +64 -17
- 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 +83 -18
- package/dist/core/staging-area.js.map +2 -2
- 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 +2236 -7790
- package/dist/generated/relationship-index.js.map +2 -2
- package/dist/integrations/claude_code/agents/dr-architect.md +16 -3
- package/dist/integrations/claude_code/agents/dr-extractor.md +1 -1
- package/dist/integrations/claude_code/commands/dr-analyzer.md +565 -0
- package/dist/integrations/claude_code/commands/dr-map.md +13 -0
- package/dist/integrations/claude_code/commands/dr-plan.md +40 -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 +26 -2
- package/dist/integrations/claude_code/docs/QUICK_START.md +37 -8
- package/dist/integrations/claude_code/docs/README.md +48 -1
- 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 +92 -15
- package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +119 -0
- package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +3 -0
- package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +1 -1
- package/dist/integrations/github_copilot/agents/dr-architect.md +1 -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 +222 -102
- package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +119 -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 +34 -0
- package/dist/mcp/changeset-only.js.map +7 -0
- package/dist/mcp/tool-registry.js +19 -3
- package/dist/mcp/tool-registry.js.map +2 -2
- package/dist/mcp/tools/changeset-analysis-tools.js +78 -0
- package/dist/mcp/tools/changeset-analysis-tools.js.map +7 -0
- package/dist/mcp/tools/changeset-editing.js +184 -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 +43 -0
- package/dist/mcp/tools/changeset-verify.js.map +7 -0
- package/dist/mcp/tools/index.js +19 -1
- package/dist/mcp/tools/index.js.map +2 -2
- package/dist/mcp/tools/model-add.js +6 -5
- 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 +35 -19
- package/dist/mcp/tools/model-update.js.map +2 -2
- package/dist/mcp/tools/relationship-tools.js +98 -0
- package/dist/mcp/tools/relationship-tools.js.map +7 -0
- package/dist/mcp/tools/shared.js +25 -2
- 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/schemas/bundled/analyzers/cbm.json +1186 -68
- 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 +718 -1643
- 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 +218 -34
- package/dist/server/server.js.map +2 -2
- 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/project-paths.js +26 -1
- package/dist/utils/project-paths.js.map +2 -2
- package/dist/utils/source-reference.js +14 -12
- 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/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-CxzcuU_o.js +84 -0
- package/dist/viewer/assets/main-j-7ULYpm.css +1 -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
|
|
@@ -327,7 +352,7 @@ your-project/
|
|
|
327
352
|
│ └── ...
|
|
328
353
|
```
|
|
329
354
|
|
|
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.
|
|
355
|
+
**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
356
|
|
|
332
357
|
### Specification Reference
|
|
333
358
|
|
|
@@ -478,10 +503,19 @@ dr relationship add business-process-order business-process-payment \
|
|
|
478
503
|
# List all relationships for an element
|
|
479
504
|
dr relationship list business-process-order
|
|
480
505
|
|
|
481
|
-
# Delete a relationship
|
|
506
|
+
# Delete a relationship (works even when an endpoint element no longer exists)
|
|
482
507
|
dr relationship delete business-process-order business-process-payment
|
|
508
|
+
|
|
509
|
+
# Remove rows whose source or target no longer resolves to an element
|
|
510
|
+
# (matches both UUID ids and dotted paths; --dry-run lists without writing)
|
|
511
|
+
dr relationship prune --dry-run
|
|
512
|
+
dr relationship prune --layers api application --format json
|
|
483
513
|
```
|
|
484
514
|
|
|
515
|
+
`dr delete` removes the relationships that name the deleted element, so `prune` is for models
|
|
516
|
+
that already carry dangling rows (earlier deletes, hand edits, imports). Inside an active changeset
|
|
517
|
+
it stages the removals instead of writing `relationships.yaml`.
|
|
518
|
+
|
|
485
519
|
### Staging Workflow
|
|
486
520
|
|
|
487
521
|
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
|
}
|
|
@@ -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,171 @@
|
|
|
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_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|java|kt|rs|cs|php|rb|swift|scala|vue|svelte)$/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$/;
|
|
16
|
+
function cardId(scope, qualifiedName) {
|
|
17
|
+
return createHash("sha1").update(`${scope}:${qualifiedName}`).digest("hex").slice(0, 12);
|
|
18
|
+
}
|
|
19
|
+
function isCandidate(node, label, exclude = VENDOR_EXCLUDE) {
|
|
20
|
+
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));
|
|
21
|
+
}
|
|
22
|
+
function lineRange(node) {
|
|
23
|
+
const s = node.start_line != null ? Number(node.start_line) : null, e = node.end_line != null ? Number(node.end_line) : null;
|
|
24
|
+
if (s != null && !Number.isNaN(s))
|
|
25
|
+
return { start: s, end: e != null && !Number.isNaN(e) ? e : null };
|
|
26
|
+
const m = /^(\d+)-(\d+)$/.exec(String(node.lines ?? ""));
|
|
27
|
+
return m ? { start: Number(m[1]), end: Number(m[2]) } : { start: null, end: null };
|
|
28
|
+
}
|
|
29
|
+
function strOrNull(v) {
|
|
30
|
+
return typeof v == "string" && v.trim() ? v : null;
|
|
31
|
+
}
|
|
32
|
+
function boolOrNull(v) {
|
|
33
|
+
return typeof v == "boolean" ? v : null;
|
|
34
|
+
}
|
|
35
|
+
function numOrNull(v) {
|
|
36
|
+
return typeof v == "number" ? v : null;
|
|
37
|
+
}
|
|
38
|
+
function strArray(v) {
|
|
39
|
+
return Array.isArray(v) ? v.filter((x) => typeof x == "string") : typeof v == "string" && v.trim() ? v.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
40
|
+
}
|
|
41
|
+
class CardBuilder {
|
|
42
|
+
graph;
|
|
43
|
+
edges;
|
|
44
|
+
options;
|
|
45
|
+
sourceLines;
|
|
46
|
+
handlerCache = /* @__PURE__ */ new Map();
|
|
47
|
+
fileLines = /* @__PURE__ */ new Map();
|
|
48
|
+
root;
|
|
49
|
+
constructor(graph, edges, options) {
|
|
50
|
+
this.graph = graph, this.edges = edges, this.options = options, this.sourceLines = options.sourceLines ?? 120, this.root = options.sourceRoot ? path.resolve(options.sourceRoot) : null;
|
|
51
|
+
}
|
|
52
|
+
/** A file's lines, read once; null when it is outside the root or unreadable. */
|
|
53
|
+
lines(file) {
|
|
54
|
+
let cached = this.fileLines.get(file);
|
|
55
|
+
if (!cached) {
|
|
56
|
+
const abs = path.resolve(this.root, file);
|
|
57
|
+
cached = abs === this.root || !abs.startsWith(this.root + path.sep) ? Promise.resolve(null) : readFile(abs, "utf-8").then((text) => text.split(`
|
|
58
|
+
`), () => null), this.fileLines.set(file, cached);
|
|
59
|
+
}
|
|
60
|
+
return cached;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A symbol's source: its line range read from disk when the builder has a root and the node
|
|
64
|
+
* has lines in a readable file, else CBM's snippet.
|
|
65
|
+
*/
|
|
66
|
+
async sourceOf(qn, node) {
|
|
67
|
+
if (this.root && node && typeof node.file_path == "string" && node.file_path) {
|
|
68
|
+
const { start, end } = lineRange(node);
|
|
69
|
+
if (start !== null && end !== null && start >= 1 && end >= start) {
|
|
70
|
+
const lines = await this.lines(node.file_path);
|
|
71
|
+
if (lines && end <= lines.length)
|
|
72
|
+
return lines.slice(start - 1, end).join(`
|
|
73
|
+
`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return (await this.graph.snippet(qn))?.source ?? null;
|
|
77
|
+
}
|
|
78
|
+
/** Strip the project prefix from a qualified name. */
|
|
79
|
+
shortQn(qn) {
|
|
80
|
+
const p = this.graph.project;
|
|
81
|
+
return qn.startsWith(p + ".") ? qn.slice(p.length + 1) : qn;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A route's handler when no HANDLES edge names one: the function, in a file with an edge into
|
|
85
|
+
* the route, whose source quotes the route's path (or calls the route's method with it).
|
|
86
|
+
*/
|
|
87
|
+
async resolveRouteHandler(routeName) {
|
|
88
|
+
const pathPart = routeName.replace(/^__route__[A-Z]+__/, "").replace(/\{\}.*$/, ""), method = (/^__route__([A-Z]+)__/.exec(routeName)?.[1] ?? "").toLowerCase(), files = /* @__PURE__ */ new Set();
|
|
89
|
+
for (const qn of this.edges.routeCallers.get(routeName) ?? []) {
|
|
90
|
+
const f = this.edges.qnFile.get(qn);
|
|
91
|
+
f && files.add(f);
|
|
92
|
+
}
|
|
93
|
+
for (const file of files) {
|
|
94
|
+
let fns = this.handlerCache.get(file);
|
|
95
|
+
if (!fns) {
|
|
96
|
+
fns = [];
|
|
97
|
+
for (const n of await this.graph.search("Function", file, ["start_line"])) {
|
|
98
|
+
const source = await this.sourceOf(n.qualified_name, n);
|
|
99
|
+
source && fns.push({ qn: n.qualified_name, source });
|
|
100
|
+
}
|
|
101
|
+
this.handlerCache.set(file, fns);
|
|
102
|
+
}
|
|
103
|
+
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));
|
|
104
|
+
if (hit)
|
|
105
|
+
return hit.qn;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
cut(source) {
|
|
110
|
+
const all = source.split(`
|
|
111
|
+
`);
|
|
112
|
+
return all.length > this.sourceLines ? { source: all.slice(0, this.sourceLines).join(`
|
|
113
|
+
`), total: all.length, truncated: !0 } : { source, total: all.length, truncated: !1 };
|
|
114
|
+
}
|
|
115
|
+
async build(node) {
|
|
116
|
+
const { edges } = this, qn = node.qualified_name, short = (s) => this.shortQn(s), { start, end } = lineRange(node);
|
|
117
|
+
let source = null, totalLines = null, truncated = !1, routeHandlerQn = null;
|
|
118
|
+
if (node.label !== "Route") {
|
|
119
|
+
const text = await this.sourceOf(qn, node);
|
|
120
|
+
text && ({ source, total: totalLines, truncated } = this.cut(text));
|
|
121
|
+
} else if (routeHandlerQn = (edges.routeHandlers.get(qn) ?? []).filter((h) => edges.qnFile.has(h))[0] ?? await this.resolveRouteHandler(qn), routeHandlerQn) {
|
|
122
|
+
const text = await this.sourceOf(routeHandlerQn, null);
|
|
123
|
+
text && ({ source, total: totalLines, truncated } = this.cut(text));
|
|
124
|
+
}
|
|
125
|
+
const callers = (edges.callers.get(qn) ?? []).map(short), callees = (edges.callees.get(qn) ?? []).map(short);
|
|
126
|
+
return {
|
|
127
|
+
// Hashed on the full qualified name, as the labelled card sets were.
|
|
128
|
+
id: cardId(this.options.scope, qn),
|
|
129
|
+
label: node.label,
|
|
130
|
+
name: node.name,
|
|
131
|
+
qualified_name: short(qn),
|
|
132
|
+
file_path: node.file_path || (routeHandlerQn ? edges.qnFile.get(routeHandlerQn) ?? null : null),
|
|
133
|
+
start_line: start,
|
|
134
|
+
end_line: end,
|
|
135
|
+
signature: strOrNull(node.signature),
|
|
136
|
+
docstring: strOrNull(node.docstring),
|
|
137
|
+
is_exported: boolOrNull(node.is_exported),
|
|
138
|
+
is_entry_point: boolOrNull(node.is_entry_point),
|
|
139
|
+
complexity: numOrNull(node.complexity),
|
|
140
|
+
parent_class: node.parent_class ? short(String(node.parent_class)) : null,
|
|
141
|
+
base_classes: strArray(node.base_classes),
|
|
142
|
+
return_type: strOrNull(node.return_type),
|
|
143
|
+
callers: (node.label === "Route" && routeHandlerQn ? [short(routeHandlerQn), ...callers] : callers).slice(0, 12),
|
|
144
|
+
callees: callees.slice(0, 12),
|
|
145
|
+
callers_total: callers.length,
|
|
146
|
+
callees_total: callees.length,
|
|
147
|
+
imports: (node.file_path ? edges.importsByFile.get(node.file_path) ?? [] : []).slice(0, 15),
|
|
148
|
+
env_vars: edges.envVars.get(qn) ?? [],
|
|
149
|
+
routes_handled: node.label === "Route" ? [node.name.replace(/^__route__[A-Z]+__/, "")] : edges.handles.get(qn) ?? [],
|
|
150
|
+
http_calls: (edges.httpCalls.get(qn) ?? []).slice(0, 8),
|
|
151
|
+
inherits: (edges.inherits.get(qn) ?? []).map(short),
|
|
152
|
+
implements: (edges.implements.get(qn) ?? []).map(short),
|
|
153
|
+
route_method: node.label === "Route" ? strOrNull(node.method) : null,
|
|
154
|
+
source,
|
|
155
|
+
source_lines_total: totalLines,
|
|
156
|
+
source_truncated: truncated
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export {
|
|
161
|
+
CARD_FIELDS,
|
|
162
|
+
CARD_LABELS,
|
|
163
|
+
CARD_VERSION,
|
|
164
|
+
CardBuilder,
|
|
165
|
+
SAMPLE_EXCLUDE,
|
|
166
|
+
SOURCE_EXT,
|
|
167
|
+
VENDOR_EXCLUDE,
|
|
168
|
+
cardId,
|
|
169
|
+
isCandidate
|
|
170
|
+
};
|
|
171
|
+
//# 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\nexport const SOURCE_EXT = /\\.(ts|tsx|js|jsx|mjs|cjs|py|go|java|kt|rs|cs|php|rb|swift|scala|vue|svelte)$/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
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/analyzers/cards/classify.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Tier-0 classification of a whole project: every candidate symbol gets a card, its facts and the\n * first matching Tier-0 rule. No network and no model calls. `dr analyzer classify --tier0-only`\n * runs this through the cbm analyzer.\n */\nimport type { Tier0Outcome, Tier0Rule } from \"../tier0-rules.js\";\nimport { CARD_FIELDS, CARD_LABELS, CARD_VERSION, CardBuilder, VENDOR_EXCLUDE, isCandidate } from \"./build.js\";\nimport { buildEdgeIndex } from \"./edge-index.js\";\nimport { buildFactsIndex, factsFor } from \"./facts.js\";\nimport { tier0ForCard } from \"./tier0.js\";\nimport type { CardFacts, CardGraph, SymbolCard } from \"./types.js\";\n\nexport interface ClassifyOptions {\n /** CBM labels to classify (default {@link CARD_LABELS} plus every label a rule names). */\n labels?: string[];\n /** Only files matching this glob (search_graph file_pattern). */\n filePattern?: string;\n /** Files to leave out, in addition to {@link VENDOR_EXCLUDE}. */\n exclude?: RegExp;\n /** Stop after this many cards, in label order. */\n limit?: number;\n /** Absolute path of the indexed codebase, so source is read from disk (see CardBuilderOptions). */\n sourceRoot?: string;\n /** Include each full card (source and neighbourhood) in the results. */\n includeCards?: boolean;\n /** Called after each card, for progress output. */\n onProgress?: (done: number, total: number) => void;\n}\n\nexport interface ClassifyResult {\n card_id: string;\n label: string;\n name: string;\n qualified_name: string;\n file_path: string | null;\n start_line: number | null;\n end_line: number | null;\n /** The first matching Tier-0 rule's action, or null when no rule covers the symbol. */\n tier0: Tier0Outcome | null;\n facts: CardFacts;\n card?: SymbolCard;\n}\n\nexport interface ClassifyReport {\n analyzer: string;\n project: string;\n mode: \"tier0\";\n /** {@link CARD_VERSION} of the cards behind these results. */\n card_version: number;\n rules: number;\n summary: {\n cards: number;\n decided: number;\n rejected: number;\n narrowed: number;\n undecided: number;\n /**\n * Non-route cards built without source (no line range or readable file, and no CBM\n * snippet), on which rules that read the declaration cannot fire.\n */\n no_source: number;\n /** Cards per rule that matched. */\n by_rule: Record<string, number>;\n /** Decided cards per `layer.type`. */\n by_target: Record<string, number>;\n };\n results: ClassifyResult[];\n}\n\n/** {@link CARD_LABELS} plus every label a Tier-0 rule names (Type, Enum, Trait, ...). */\nexport function defaultLabels(rules: Tier0Rule[]): string[] {\n return [...new Set([...CARD_LABELS, ...rules.flatMap((r) => r.labels)])];\n}\n\nexport async function classifyTier0(\n graph: CardGraph,\n rules: Tier0Rule[],\n analyzer: string,\n options: ClassifyOptions = {}\n): Promise<ClassifyReport> {\n const labels = options.labels?.length ? options.labels : defaultLabels(rules);\n const exclude = options.exclude\n ? new RegExp(`(?:${VENDOR_EXCLUDE.source})|(?:${options.exclude.source})`, options.exclude.flags.replace(\"g\", \"\"))\n : VENDOR_EXCLUDE;\n\n // One card per qualified name (Java overloads share one), deduplicated before the limit.\n const nodes = [];\n const seen = new Set<string>();\n for (const label of labels) {\n for (const node of await graph.search(label, options.filePattern, CARD_FIELDS[label] ?? [])) {\n if (!isCandidate(node, label, exclude) || seen.has(node.qualified_name)) continue;\n seen.add(node.qualified_name);\n nodes.push(node);\n }\n }\n const selected = options.limit !== undefined ? nodes.slice(0, options.limit) : nodes;\n\n const edges = await buildEdgeIndex(graph);\n const factsIndex = await buildFactsIndex(graph);\n const builder = new CardBuilder(graph, edges, { scope: graph.project, sourceRoot: options.sourceRoot });\n\n const results: ClassifyResult[] = [];\n const summary: ClassifyReport[\"summary\"] = { cards: 0, decided: 0, rejected: 0, narrowed: 0, undecided: 0, no_source: 0, by_rule: {}, by_target: {} };\n for (const node of selected) {\n const card = await builder.build(node);\n const facts = factsFor(factsIndex, node.qualified_name, card.file_path);\n const tier0 = tier0ForCard(card, facts, rules);\n\n summary.cards++;\n if (card.source === null && card.label !== \"Route\") summary.no_source++;\n if (!tier0) summary.undecided++;\n else {\n summary.by_rule[tier0.rule] = (summary.by_rule[tier0.rule] ?? 0) + 1;\n if (tier0.kind === \"decide\") {\n summary.decided++;\n const target = `${tier0.layer}.${tier0.type}`;\n summary.by_target[target] = (summary.by_target[target] ?? 0) + 1;\n } else if (tier0.kind === \"reject\") summary.rejected++;\n else summary.narrowed++;\n }\n results.push({\n card_id: card.id,\n label: card.label,\n name: card.name,\n qualified_name: card.qualified_name,\n file_path: card.file_path,\n start_line: card.start_line,\n end_line: card.end_line,\n tier0,\n facts,\n ...(options.includeCards ? { card } : {}),\n });\n options.onProgress?.(results.length, selected.length);\n }\n return { analyzer, project: graph.project, mode: \"tier0\", card_version: CARD_VERSION, rules: rules.length, summary, results };\n}\n"],
|
|
5
|
+
"mappings": "AAMA,SAAS,aAAa,aAAa,cAAc,aAAa,gBAAgB,mBAAmB;AACjG,SAAS,sBAAsB;AAC/B,SAAS,iBAAiB,gBAAgB;AAC1C,SAAS,oBAAoB;AA6DvB,SAAU,cAAc,OAAkB;AAC9C,SAAO,CAAC,GAAG,oBAAI,IAAI,CAAC,GAAG,aAAa,GAAG,MAAM,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AACzE;AAEA,eAAsB,cACpB,OACA,OACA,UACA,UAA2B,CAAA,GAAE;AAE7B,QAAM,SAAS,QAAQ,QAAQ,SAAS,QAAQ,SAAS,cAAc,KAAK,GACtE,UAAU,QAAQ,UACpB,IAAI,OAAO,MAAM,eAAe,MAAM,QAAQ,QAAQ,QAAQ,MAAM,KAAK,QAAQ,QAAQ,MAAM,QAAQ,KAAK,EAAE,CAAC,IAC/G,gBAGE,QAAQ,CAAA,GACR,OAAO,oBAAI,IAAG;AACpB,aAAW,SAAS;AAClB,eAAW,QAAQ,MAAM,MAAM,OAAO,OAAO,QAAQ,aAAa,YAAY,KAAK,KAAK,CAAA,CAAE;AACxF,MAAI,CAAC,YAAY,MAAM,OAAO,OAAO,KAAK,KAAK,IAAI,KAAK,cAAc,MACtE,KAAK,IAAI,KAAK,cAAc,GAC5B,MAAM,KAAK,IAAI;AAGnB,QAAM,WAAW,QAAQ,UAAU,SAAY,MAAM,MAAM,GAAG,QAAQ,KAAK,IAAI,OAEzE,QAAQ,MAAM,eAAe,KAAK,GAClC,aAAa,MAAM,gBAAgB,KAAK,GACxC,UAAU,IAAI,YAAY,OAAO,OAAO,EAAE,OAAO,MAAM,SAAS,YAAY,QAAQ,WAAU,CAAE,GAEhG,UAA4B,CAAA,GAC5B,UAAqC,EAAE,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,GAAG,SAAS,CAAA,GAAI,WAAW,CAAA,EAAE;AACnJ,aAAW,QAAQ,UAAU;AAC3B,UAAM,OAAO,MAAM,QAAQ,MAAM,IAAI,GAC/B,QAAQ,SAAS,YAAY,KAAK,gBAAgB,KAAK,SAAS,GAChE,QAAQ,aAAa,MAAM,OAAO,KAAK;AAI7C,QAFA,QAAQ,SACJ,KAAK,WAAW,QAAQ,KAAK,UAAU,WAAS,QAAQ,aACxD,CAAC;AAAO,cAAQ;aAElB,QAAQ,QAAQ,MAAM,IAAI,KAAK,QAAQ,QAAQ,MAAM,IAAI,KAAK,KAAK,GAC/D,MAAM,SAAS,UAAU;AAC3B,cAAQ;AACR,YAAM,SAAS,GAAG,MAAM,KAAK,IAAI,MAAM,IAAI;AAC3C,cAAQ,UAAU,MAAM,KAAK,QAAQ,UAAU,MAAM,KAAK,KAAK;IACjE,MAAO,CAAI,MAAM,SAAS,WAAU,QAAQ,aACvC,QAAQ;AAEf,YAAQ,KAAK;MACX,SAAS,KAAK;MACd,OAAO,KAAK;MACZ,MAAM,KAAK;MACX,gBAAgB,KAAK;MACrB,WAAW,KAAK;MAChB,YAAY,KAAK;MACjB,UAAU,KAAK;MACf;MACA;MACA,GAAI,QAAQ,eAAe,EAAE,KAAI,IAAK,CAAA;KACvC,GACD,QAAQ,aAAa,QAAQ,QAAQ,SAAS,MAAM;EACtD;AACA,SAAO,EAAE,UAAU,SAAS,MAAM,SAAS,MAAM,SAAS,cAAc,cAAc,OAAO,MAAM,QAAQ,SAAS,QAAO;AAC7H;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function push(m, k, v) {
|
|
2
|
+
const arr = m.get(k);
|
|
3
|
+
arr ? arr.includes(v) || arr.push(v) : m.set(k, [v]);
|
|
4
|
+
}
|
|
5
|
+
async function buildEdgeIndex(graph) {
|
|
6
|
+
const idx = {
|
|
7
|
+
callers: /* @__PURE__ */ new Map(),
|
|
8
|
+
callees: /* @__PURE__ */ new Map(),
|
|
9
|
+
importsByFile: /* @__PURE__ */ new Map(),
|
|
10
|
+
envVars: /* @__PURE__ */ new Map(),
|
|
11
|
+
handles: /* @__PURE__ */ new Map(),
|
|
12
|
+
httpCalls: /* @__PURE__ */ new Map(),
|
|
13
|
+
inherits: /* @__PURE__ */ new Map(),
|
|
14
|
+
implements: /* @__PURE__ */ new Map(),
|
|
15
|
+
routeHandlers: /* @__PURE__ */ new Map(),
|
|
16
|
+
routeCallers: /* @__PURE__ */ new Map(),
|
|
17
|
+
qnFile: /* @__PURE__ */ new Map()
|
|
18
|
+
};
|
|
19
|
+
for (const [a, b] of await graph.rows("MATCH (a)-[r:CALLS]->(b) RETURN a.qualified_name, b.qualified_name"))
|
|
20
|
+
typeof a != "string" || typeof b != "string" || (push(idx.callees, a, b), push(idx.callers, b, a));
|
|
21
|
+
for (const [file, b, bfile] of await graph.rows("MATCH (a)-[r:IMPORTS]->(b) RETURN a.file_path, b.qualified_name, b.file_path")) {
|
|
22
|
+
if (typeof file != "string")
|
|
23
|
+
continue;
|
|
24
|
+
const target = typeof bfile == "string" && bfile ? bfile : typeof b == "string" ? b : null;
|
|
25
|
+
target && push(idx.importsByFile, file, target);
|
|
26
|
+
}
|
|
27
|
+
for (const [a, key] of await graph.rows("MATCH (a)-[r:CONFIGURES]->(e:EnvVar) RETURN a.qualified_name, e.env_key"))
|
|
28
|
+
typeof a == "string" && typeof key == "string" && push(idx.envVars, a, key);
|
|
29
|
+
for (const [a, route] of await graph.rows("MATCH (a)-[r:HANDLES]->(x:Route) RETURN a.qualified_name, x.qualified_name"))
|
|
30
|
+
typeof a == "string" && typeof route == "string" && (push(idx.handles, a, route), push(idx.routeHandlers, route, a), push(idx.routeCallers, route, a));
|
|
31
|
+
for (const [a, file, route] of await graph.rows("MATCH (a)-[r]->(x:Route) RETURN a.qualified_name, a.file_path, x.qualified_name"))
|
|
32
|
+
typeof a != "string" || typeof route != "string" || (push(idx.routeCallers, route, a), typeof file == "string" && file && idx.qnFile.set(a, file));
|
|
33
|
+
for (const [a, file] of await graph.rows("MATCH (a:Function) RETURN a.qualified_name, a.file_path"))
|
|
34
|
+
typeof a == "string" && typeof file == "string" && file && idx.qnFile.set(a, file);
|
|
35
|
+
for (const [a, route, url] of await graph.rows("MATCH (a)-[r:HTTP_CALLS]->(x:Route) RETURN a.qualified_name, x.name, r.url_path"))
|
|
36
|
+
typeof a == "string" && push(idx.httpCalls, a, String(url ?? route ?? ""));
|
|
37
|
+
for (const [a, t, b] of await graph.rows("MATCH (a)-[r:INHERITS|IMPLEMENTS]->(b) RETURN a.qualified_name, type(r), b.qualified_name"))
|
|
38
|
+
typeof a != "string" || typeof b != "string" || push(t === "IMPLEMENTS" ? idx.implements : idx.inherits, a, b);
|
|
39
|
+
return idx;
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
buildEdgeIndex
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=edge-index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/analyzers/cards/edge-index.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * The graph neighbourhood every card reads, pulled once per project into adjacency maps keyed by\n * full qualified name.\n */\nimport type { CardGraph } from \"./types.js\";\n\nexport interface EdgeIndex {\n callers: Map<string, string[]>;\n callees: Map<string, string[]>;\n /** file path -> files (or symbols, when the target has no file) it imports */\n importsByFile: Map<string, string[]>;\n envVars: Map<string, string[]>;\n /** handler -> routes it handles */\n handles: Map<string, string[]>;\n httpCalls: Map<string, string[]>;\n inherits: Map<string, string[]>;\n implements: Map<string, string[]>;\n /** route qualified name -> handler qualified names */\n routeHandlers: Map<string, string[]>;\n /** route qualified name -> symbols or modules with any edge into it */\n routeCallers: Map<string, string[]>;\n /** qualified name -> file path, for every Function and route caller seen with a file */\n qnFile: Map<string, string>;\n}\n\nfunction push(m: Map<string, string[]>, k: string, v: string): void {\n const arr = m.get(k);\n if (arr) {\n if (!arr.includes(v)) arr.push(v);\n } else m.set(k, [v]);\n}\n\nexport async function buildEdgeIndex(graph: CardGraph): Promise<EdgeIndex> {\n const idx: EdgeIndex = {\n callers: new Map(),\n callees: new Map(),\n importsByFile: new Map(),\n envVars: new Map(),\n handles: new Map(),\n httpCalls: new Map(),\n inherits: new Map(),\n implements: new Map(),\n routeHandlers: new Map(),\n routeCallers: new Map(),\n qnFile: new Map(),\n };\n for (const [a, b] of await graph.rows(\"MATCH (a)-[r:CALLS]->(b) RETURN a.qualified_name, b.qualified_name\")) {\n if (typeof a !== \"string\" || typeof b !== \"string\") continue;\n push(idx.callees, a, b);\n push(idx.callers, b, a);\n }\n for (const [file, b, bfile] of await graph.rows(\"MATCH (a)-[r:IMPORTS]->(b) RETURN a.file_path, b.qualified_name, b.file_path\")) {\n if (typeof file !== \"string\") continue;\n const target = typeof bfile === \"string\" && bfile ? bfile : typeof b === \"string\" ? b : null;\n if (target) push(idx.importsByFile, file, target);\n }\n for (const [a, key] of await graph.rows(\"MATCH (a)-[r:CONFIGURES]->(e:EnvVar) RETURN a.qualified_name, e.env_key\")) {\n if (typeof a === \"string\" && typeof key === \"string\") push(idx.envVars, a, key);\n }\n for (const [a, route] of await graph.rows(\"MATCH (a)-[r:HANDLES]->(x:Route) RETURN a.qualified_name, x.qualified_name\")) {\n if (typeof a === \"string\" && typeof route === \"string\") {\n push(idx.handles, a, route);\n push(idx.routeHandlers, route, a);\n push(idx.routeCallers, route, a);\n }\n }\n for (const [a, file, route] of await graph.rows(\"MATCH (a)-[r]->(x:Route) RETURN a.qualified_name, a.file_path, x.qualified_name\")) {\n if (typeof a !== \"string\" || typeof route !== \"string\") continue;\n push(idx.routeCallers, route, a);\n if (typeof file === \"string\" && file) idx.qnFile.set(a, file);\n }\n for (const [a, file] of await graph.rows(\"MATCH (a:Function) RETURN a.qualified_name, a.file_path\")) {\n if (typeof a === \"string\" && typeof file === \"string\" && file) idx.qnFile.set(a, file);\n }\n for (const [a, route, url] of await graph.rows(\"MATCH (a)-[r:HTTP_CALLS]->(x:Route) RETURN a.qualified_name, x.name, r.url_path\")) {\n if (typeof a === \"string\") push(idx.httpCalls, a, String(url ?? route ?? \"\"));\n }\n for (const [a, t, b] of await graph.rows(\"MATCH (a)-[r:INHERITS|IMPLEMENTS]->(b) RETURN a.qualified_name, type(r), b.qualified_name\")) {\n if (typeof a !== \"string\" || typeof b !== \"string\") continue;\n push(t === \"IMPLEMENTS\" ? idx.implements : idx.inherits, a, b);\n }\n return idx;\n}\n"],
|
|
5
|
+
"mappings": "AAyBA,SAAS,KAAK,GAA0B,GAAW,GAAS;AAC1D,QAAM,MAAM,EAAE,IAAI,CAAC;AACnB,EAAI,MACG,IAAI,SAAS,CAAC,KAAG,IAAI,KAAK,CAAC,IAC3B,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC;AACrB;AAEA,eAAsB,eAAe,OAAgB;AACnD,QAAM,MAAiB;IACrB,SAAS,oBAAI,IAAG;IAChB,SAAS,oBAAI,IAAG;IAChB,eAAe,oBAAI,IAAG;IACtB,SAAS,oBAAI,IAAG;IAChB,SAAS,oBAAI,IAAG;IAChB,WAAW,oBAAI,IAAG;IAClB,UAAU,oBAAI,IAAG;IACjB,YAAY,oBAAI,IAAG;IACnB,eAAe,oBAAI,IAAG;IACtB,cAAc,oBAAI,IAAG;IACrB,QAAQ,oBAAI,IAAG;;AAEjB,aAAW,CAAC,GAAG,CAAC,KAAK,MAAM,MAAM,KAAK,oEAAoE;AACxG,IAAI,OAAO,KAAM,YAAY,OAAO,KAAM,aAC1C,KAAK,IAAI,SAAS,GAAG,CAAC,GACtB,KAAK,IAAI,SAAS,GAAG,CAAC;AAExB,aAAW,CAAC,MAAM,GAAG,KAAK,KAAK,MAAM,MAAM,KAAK,8EAA8E,GAAG;AAC/H,QAAI,OAAO,QAAS;AAAU;AAC9B,UAAM,SAAS,OAAO,SAAU,YAAY,QAAQ,QAAQ,OAAO,KAAM,WAAW,IAAI;AACxF,IAAI,UAAQ,KAAK,IAAI,eAAe,MAAM,MAAM;EAClD;AACA,aAAW,CAAC,GAAG,GAAG,KAAK,MAAM,MAAM,KAAK,yEAAyE;AAC/G,IAAI,OAAO,KAAM,YAAY,OAAO,OAAQ,YAAU,KAAK,IAAI,SAAS,GAAG,GAAG;AAEhF,aAAW,CAAC,GAAG,KAAK,KAAK,MAAM,MAAM,KAAK,4EAA4E;AACpH,IAAI,OAAO,KAAM,YAAY,OAAO,SAAU,aAC5C,KAAK,IAAI,SAAS,GAAG,KAAK,GAC1B,KAAK,IAAI,eAAe,OAAO,CAAC,GAChC,KAAK,IAAI,cAAc,OAAO,CAAC;AAGnC,aAAW,CAAC,GAAG,MAAM,KAAK,KAAK,MAAM,MAAM,KAAK,iFAAiF;AAC/H,IAAI,OAAO,KAAM,YAAY,OAAO,SAAU,aAC9C,KAAK,IAAI,cAAc,OAAO,CAAC,GAC3B,OAAO,QAAS,YAAY,QAAM,IAAI,OAAO,IAAI,GAAG,IAAI;AAE9D,aAAW,CAAC,GAAG,IAAI,KAAK,MAAM,MAAM,KAAK,yDAAyD;AAChG,IAAI,OAAO,KAAM,YAAY,OAAO,QAAS,YAAY,QAAM,IAAI,OAAO,IAAI,GAAG,IAAI;AAEvF,aAAW,CAAC,GAAG,OAAO,GAAG,KAAK,MAAM,MAAM,KAAK,iFAAiF;AAC9H,IAAI,OAAO,KAAM,YAAU,KAAK,IAAI,WAAW,GAAG,OAAO,OAAO,SAAS,EAAE,CAAC;AAE9E,aAAW,CAAC,GAAG,GAAG,CAAC,KAAK,MAAM,MAAM,KAAK,2FAA2F;AAClI,IAAI,OAAO,KAAM,YAAY,OAAO,KAAM,YAC1C,KAAK,MAAM,eAAe,IAAI,aAAa,IAAI,UAAU,GAAG,CAAC;AAE/D,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function decoratorName(text) {
|
|
2
|
+
return text.trim().replace(/^@/, "").replace(/\s*#.*$/, "").replace(/\(.*$/s, "").trim();
|
|
3
|
+
}
|
|
4
|
+
function parseList(v) {
|
|
5
|
+
if (Array.isArray(v))
|
|
6
|
+
return v.map(String);
|
|
7
|
+
if (typeof v != "string" || !v.trim())
|
|
8
|
+
return [];
|
|
9
|
+
try {
|
|
10
|
+
const p = JSON.parse(v);
|
|
11
|
+
return Array.isArray(p) ? p.map(String) : [];
|
|
12
|
+
} catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function buildFactsIndex(graph) {
|
|
17
|
+
const decorators = /* @__PURE__ */ new Map();
|
|
18
|
+
for (const [qn, decs] of await graph.rows("MATCH (b) WHERE b.decorators IS NOT NULL RETURN b.qualified_name, b.decorators"))
|
|
19
|
+
typeof qn == "string" && decorators.set(qn, parseList(decs));
|
|
20
|
+
const importers = /* @__PURE__ */ new Map();
|
|
21
|
+
for (const [afile, bfile] of await graph.rows("MATCH (a)-[r:IMPORTS]->(b) RETURN a.file_path, b.file_path"))
|
|
22
|
+
typeof afile != "string" || typeof bfile != "string" || !bfile || afile === bfile || (importers.has(bfile) || importers.set(bfile, /* @__PURE__ */ new Set()), importers.get(bfile).add(afile));
|
|
23
|
+
const methods = /* @__PURE__ */ new Map();
|
|
24
|
+
for (const [a] of await graph.rows("MATCH (a)-[r:DEFINES_METHOD]->(m) RETURN a.qualified_name, m.qualified_name"))
|
|
25
|
+
typeof a == "string" && methods.set(a, (methods.get(a) ?? 0) + 1);
|
|
26
|
+
const usage = /* @__PURE__ */ new Map();
|
|
27
|
+
for (const [afile, b, bfile] of await graph.rows("MATCH (a)-[r:USAGE]->(b) RETURN a.file_path, b.qualified_name, b.file_path"))
|
|
28
|
+
typeof afile != "string" || typeof b != "string" || afile === bfile || (usage.has(b) || usage.set(b, /* @__PURE__ */ new Set()), usage.get(b).add(afile));
|
|
29
|
+
return { decorators, importers, methods, usage };
|
|
30
|
+
}
|
|
31
|
+
function factsFor(idx, fullQualifiedName, filePath) {
|
|
32
|
+
const decorators = idx.decorators.get(fullQualifiedName) ?? [];
|
|
33
|
+
return {
|
|
34
|
+
decorators,
|
|
35
|
+
decorator_names: decorators.map(decoratorName),
|
|
36
|
+
file_importers: filePath ? idx.importers.get(filePath)?.size ?? 0 : 0,
|
|
37
|
+
methods: idx.methods.get(fullQualifiedName) ?? 0,
|
|
38
|
+
usage_files: idx.usage.get(fullQualifiedName)?.size ?? 0
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
buildFactsIndex,
|
|
43
|
+
decoratorName,
|
|
44
|
+
factsFor
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=facts.js.map
|