@documentation-robotics/cli 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -16
- package/dist/analysis/relationship-classifier.js +2 -9
- package/dist/analysis/relationship-classifier.js.map +2 -2
- package/dist/analyzers/active-analyzer.js +62 -0
- package/dist/analyzers/active-analyzer.js.map +7 -0
- package/dist/analyzers/call-graph-utils.js +2 -2
- package/dist/analyzers/call-graph-utils.js.map +2 -2
- package/dist/analyzers/cards/build.js +191 -0
- package/dist/analyzers/cards/build.js.map +7 -0
- package/dist/analyzers/cards/classify.js +42 -0
- package/dist/analyzers/cards/classify.js.map +7 -0
- package/dist/analyzers/cards/edge-index.js +44 -0
- package/dist/analyzers/cards/edge-index.js.map +7 -0
- package/dist/analyzers/cards/extract.js +138 -0
- package/dist/analyzers/cards/extract.js.map +7 -0
- package/dist/analyzers/cards/facts.js +46 -0
- package/dist/analyzers/cards/facts.js.map +7 -0
- package/dist/analyzers/cards/graph.js +42 -0
- package/dist/analyzers/cards/graph.js.map +7 -0
- package/dist/analyzers/cards/index.js +8 -0
- package/dist/analyzers/cards/index.js.map +7 -0
- package/dist/analyzers/cards/routes.js +96 -0
- package/dist/analyzers/cards/routes.js.map +7 -0
- package/dist/analyzers/cards/tier0.js +18 -0
- package/dist/analyzers/cards/tier0.js.map +7 -0
- package/dist/analyzers/cards/types.js +1 -0
- package/dist/analyzers/cards/types.js.map +7 -0
- package/dist/analyzers/cbm-analyzer.js +270 -174
- package/dist/analyzers/cbm-analyzer.js.map +2 -2
- package/dist/analyzers/cbm-responses.js +342 -0
- package/dist/analyzers/cbm-responses.js.map +7 -0
- package/dist/analyzers/code-changes.js +159 -0
- package/dist/analyzers/code-changes.js.map +7 -0
- package/dist/analyzers/edge-relationships.js +49 -0
- package/dist/analyzers/edge-relationships.js.map +7 -0
- package/dist/analyzers/extraction-plan.js +541 -0
- package/dist/analyzers/extraction-plan.js.map +7 -0
- package/dist/analyzers/index-job.js +229 -0
- package/dist/analyzers/index-job.js.map +7 -0
- package/dist/analyzers/mapping-loader.js +9 -0
- package/dist/analyzers/mapping-loader.js.map +2 -2
- package/dist/analyzers/tier0-rules.js +153 -0
- package/dist/analyzers/tier0-rules.js.map +7 -0
- package/dist/analyzers/types.js.map +1 -1
- package/dist/audit/relationships/ai/prompts.js +10 -10
- package/dist/audit/relationships/ai/prompts.js.map +2 -2
- package/dist/audit/relationships/analysis/layer-templates.js +52 -52
- package/dist/audit/relationships/analysis/layer-templates.js.map +1 -1
- package/dist/cli.js +23 -11
- package/dist/cli.js.map +2 -2
- package/dist/coding-agents/default-permissions.js +31 -1
- package/dist/coding-agents/default-permissions.js.map +2 -2
- package/dist/coding-agents/drbot-mcp.js +269 -0
- package/dist/coding-agents/drbot-mcp.js.map +7 -0
- package/dist/commands/add.js +7 -4
- package/dist/commands/add.js.map +2 -2
- package/dist/commands/analyzer.js +143 -40
- package/dist/commands/analyzer.js.map +2 -2
- package/dist/commands/changeset-verify.js +63 -0
- package/dist/commands/changeset-verify.js.map +7 -0
- package/dist/commands/changeset.js +80 -29
- package/dist/commands/changeset.js.map +2 -2
- package/dist/commands/delete.js +10 -4
- package/dist/commands/delete.js.map +2 -2
- package/dist/commands/farm.js +176 -60
- package/dist/commands/farm.js.map +2 -2
- package/dist/commands/init.js +9 -26
- package/dist/commands/init.js.map +2 -2
- package/dist/commands/mcp.js +40 -21
- package/dist/commands/mcp.js.map +3 -3
- package/dist/commands/relationship.js +208 -37
- package/dist/commands/relationship.js.map +2 -2
- package/dist/commands/reports.js +3 -3
- package/dist/commands/reports.js.map +2 -2
- package/dist/commands/update.js +52 -32
- package/dist/commands/update.js.map +2 -2
- package/dist/commands/upgrade.js +99 -103
- package/dist/commands/upgrade.js.map +3 -3
- package/dist/commands/validate.js +19 -3
- package/dist/commands/validate.js.map +2 -2
- package/dist/commands/version.js +1 -1
- package/dist/commands/visualize.js +34 -15
- package/dist/commands/visualize.js.map +2 -2
- package/dist/core/annotation-store.js +15 -5
- package/dist/core/annotation-store.js.map +2 -2
- package/dist/core/catalog-migration.js +34 -0
- package/dist/core/catalog-migration.js.map +7 -0
- package/dist/core/changeset-analysis.js +421 -0
- package/dist/core/changeset-analysis.js.map +7 -0
- package/dist/core/changeset-base-index.js +160 -0
- package/dist/core/changeset-base-index.js.map +7 -0
- package/dist/core/changeset-brief-compact.js +256 -0
- package/dist/core/changeset-brief-compact.js.map +7 -0
- package/dist/core/changeset-brief-delta.js +102 -0
- package/dist/core/changeset-brief-delta.js.map +7 -0
- package/dist/core/changeset-graph.js +281 -0
- package/dist/core/changeset-graph.js.map +7 -0
- package/dist/core/changeset-only-guard.js +32 -0
- package/dist/core/changeset-only-guard.js.map +7 -0
- package/dist/core/changeset-summary.js +68 -0
- package/dist/core/changeset-summary.js.map +7 -0
- package/dist/core/changeset-validator.js +66 -20
- package/dist/core/changeset-validator.js.map +2 -2
- package/dist/core/changeset-verify.js +848 -0
- package/dist/core/changeset-verify.js.map +7 -0
- package/dist/core/changeset.js +56 -9
- package/dist/core/changeset.js.map +2 -2
- package/dist/core/element-type-migration.js +145 -0
- package/dist/core/element-type-migration.js.map +7 -0
- package/dist/core/extraction-observability.js +161 -0
- package/dist/core/extraction-observability.js.map +7 -0
- package/dist/core/farm-manifest.js +12 -6
- package/dist/core/farm-manifest.js.map +2 -2
- package/dist/core/farm-sync-engine.js +1 -1
- package/dist/core/farm-sync-engine.js.map +2 -2
- package/dist/core/farm-sync-state.js +10 -2
- package/dist/core/farm-sync-state.js.map +2 -2
- package/dist/core/migration-registry.js +88 -47
- package/dist/core/migration-registry.js.map +2 -2
- package/dist/core/model-upgrade.js +102 -0
- package/dist/core/model-upgrade.js.map +7 -0
- package/dist/core/model.js +82 -30
- package/dist/core/model.js.map +2 -2
- package/dist/core/mutation-handler.js +71 -6
- package/dist/core/mutation-handler.js.map +2 -2
- package/dist/core/project-init.js +53 -0
- package/dist/core/project-init.js.map +7 -0
- package/dist/core/relationship-migration-table.js +6639 -0
- package/dist/core/relationship-migration-table.js.map +7 -0
- package/dist/core/relationship-migration.js +272 -0
- package/dist/core/relationship-migration.js.map +7 -0
- package/dist/core/relationship-prune.js +49 -0
- package/dist/core/relationship-prune.js.map +7 -0
- package/dist/core/relationship-staging.js +161 -0
- package/dist/core/relationship-staging.js.map +7 -0
- package/dist/core/relationships.js +26 -4
- package/dist/core/relationships.js.map +2 -2
- package/dist/core/report-data-model.js +7 -13
- package/dist/core/report-data-model.js.map +2 -2
- package/dist/core/staged-changeset-storage.js +145 -18
- package/dist/core/staged-changeset-storage.js.map +2 -2
- package/dist/core/staged-source-reference.js +28 -0
- package/dist/core/staged-source-reference.js.map +7 -0
- package/dist/core/staging-area.js +166 -24
- package/dist/core/staging-area.js.map +2 -2
- package/dist/core/upgrade-plan.js +117 -0
- package/dist/core/upgrade-plan.js.map +7 -0
- package/dist/core/virtual-projection.js +13 -5
- package/dist/core/virtual-projection.js.map +2 -2
- package/dist/export/archimate-exporter.js +1 -0
- package/dist/export/archimate-exporter.js.map +2 -2
- package/dist/generated/compiled-validators.js +2 -2
- package/dist/generated/compiled-validators.js.map +2 -2
- package/dist/generated/layer-registry.js +16 -16
- package/dist/generated/layer-registry.js.map +2 -2
- package/dist/generated/node-types.js +186 -168
- package/dist/generated/node-types.js.map +2 -2
- package/dist/generated/relationship-index.js +2468 -7913
- package/dist/generated/relationship-index.js.map +2 -2
- package/dist/integrations/claude_code/.claude-plugin/plugin.json +24 -0
- package/dist/integrations/claude_code/agents/dr-architect.md +30 -3
- package/dist/integrations/claude_code/agents/dr-extractor.md +8 -2
- package/dist/integrations/claude_code/commands/dr-analyzer.md +565 -0
- package/dist/integrations/claude_code/commands/dr-changeset.md +2 -1
- package/dist/integrations/claude_code/commands/dr-map.md +20 -1
- package/dist/integrations/claude_code/commands/dr-plan.md +66 -0
- package/dist/integrations/claude_code/commands/dr-relate.md +106 -96
- package/dist/integrations/claude_code/commands/dr-verify.md +45 -2
- package/dist/integrations/claude_code/docs/END_TO_END_WALKTHROUGH.md +14 -0
- package/dist/integrations/claude_code/docs/MCP_SERVER.md +47 -26
- package/dist/integrations/claude_code/docs/QUICK_START.md +37 -8
- package/dist/integrations/claude_code/docs/README.md +68 -2
- package/dist/integrations/claude_code/plugin-agents/dr-advisor.md +255 -0
- package/dist/integrations/claude_code/plugin-agents/dr-architect.md +306 -0
- package/dist/integrations/claude_code/skills/dr_01_motivation_layer/SKILL.md +93 -72
- package/dist/integrations/claude_code/skills/dr_02_business_layer/SKILL.md +163 -126
- package/dist/integrations/claude_code/skills/dr_03_product_layer/SKILL.md +161 -126
- package/dist/integrations/claude_code/skills/dr_04_security_layer/SKILL.md +153 -133
- package/dist/integrations/claude_code/skills/dr_05_application_layer/SKILL.md +129 -100
- package/dist/integrations/claude_code/skills/dr_06_technology_layer/SKILL.md +111 -94
- package/dist/integrations/claude_code/skills/dr_07_api_layer/SKILL.md +143 -157
- package/dist/integrations/claude_code/skills/dr_08_data_model_layer/SKILL.md +117 -33
- package/dist/integrations/claude_code/skills/dr_09_dataset_layer/SKILL.md +74 -25
- package/dist/integrations/claude_code/skills/dr_10_ux_layer/SKILL.md +88 -12
- package/dist/integrations/claude_code/skills/dr_11_navigation_layer/SKILL.md +83 -24
- package/dist/integrations/claude_code/skills/dr_12_apm_layer/SKILL.md +57 -12
- package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +95 -15
- package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +178 -0
- package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +8 -0
- package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +6 -1
- package/dist/integrations/github_copilot/agents/dr-architect.md +3 -1
- package/dist/integrations/github_copilot/skills/dr_01_motivation_layer/SKILL.md +182 -102
- package/dist/integrations/github_copilot/skills/dr_02_business_layer/SKILL.md +266 -148
- package/dist/integrations/github_copilot/skills/dr_03_product_layer/SKILL.md +547 -86
- package/dist/integrations/github_copilot/skills/dr_04_security_layer/SKILL.md +164 -169
- package/dist/integrations/github_copilot/skills/dr_05_application_layer/SKILL.md +420 -150
- package/dist/integrations/github_copilot/skills/dr_06_technology_layer/SKILL.md +331 -138
- package/dist/integrations/github_copilot/skills/dr_07_api_layer/SKILL.md +265 -201
- package/dist/integrations/github_copilot/skills/dr_08_data_model_layer/SKILL.md +181 -58
- package/dist/integrations/github_copilot/skills/dr_09_dataset_layer/SKILL.md +209 -57
- package/dist/integrations/github_copilot/skills/dr_10_ux_layer/SKILL.md +277 -112
- package/dist/integrations/github_copilot/skills/dr_11_navigation_layer/SKILL.md +219 -70
- package/dist/integrations/github_copilot/skills/dr_12_apm_layer/SKILL.md +189 -105
- package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +225 -102
- package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +177 -0
- package/dist/integrations/github_copilot/skills/dr_changeset_reviewer/SKILL.md +3 -0
- package/dist/integrations/hash-utils.js.map +2 -2
- package/dist/mcp/api-key-manager.js +14 -1
- package/dist/mcp/api-key-manager.js.map +2 -2
- package/dist/mcp/changeset-only.js +45 -0
- package/dist/mcp/changeset-only.js.map +7 -0
- package/dist/mcp/project-root.js +85 -0
- package/dist/mcp/project-root.js.map +7 -0
- package/dist/mcp/server-instructions.js +27 -0
- package/dist/mcp/server-instructions.js.map +7 -0
- package/dist/mcp/strict-schema.js +52 -0
- package/dist/mcp/strict-schema.js.map +7 -0
- package/dist/mcp/tool-registry.js +55 -5
- package/dist/mcp/tool-registry.js.map +2 -2
- package/dist/mcp/tools/changeset-analysis-tools.js +98 -0
- package/dist/mcp/tools/changeset-analysis-tools.js.map +7 -0
- package/dist/mcp/tools/changeset-commit.js +224 -0
- package/dist/mcp/tools/changeset-commit.js.map +7 -0
- package/dist/mcp/tools/changeset-editing.js +260 -0
- package/dist/mcp/tools/changeset-editing.js.map +7 -0
- package/dist/mcp/tools/changeset-lifecycle.js +83 -0
- package/dist/mcp/tools/changeset-lifecycle.js.map +7 -0
- package/dist/mcp/tools/changeset-shared.js +31 -0
- package/dist/mcp/tools/changeset-shared.js.map +7 -0
- package/dist/mcp/tools/changeset-show.js +17 -1
- package/dist/mcp/tools/changeset-show.js.map +2 -2
- package/dist/mcp/tools/changeset-verify.js +49 -0
- package/dist/mcp/tools/changeset-verify.js.map +7 -0
- package/dist/mcp/tools/codebase-extract.js +836 -0
- package/dist/mcp/tools/codebase-extract.js.map +7 -0
- package/dist/mcp/tools/codebase-index.js +125 -0
- package/dist/mcp/tools/codebase-index.js.map +7 -0
- package/dist/mcp/tools/codebase-shared.js +265 -0
- package/dist/mcp/tools/codebase-shared.js.map +7 -0
- package/dist/mcp/tools/codebase-verify.js +48 -0
- package/dist/mcp/tools/codebase-verify.js.map +7 -0
- package/dist/mcp/tools/index.js +34 -1
- package/dist/mcp/tools/index.js.map +2 -2
- package/dist/mcp/tools/model-add.js +191 -74
- package/dist/mcp/tools/model-add.js.map +2 -2
- package/dist/mcp/tools/model-delete.js +2 -2
- package/dist/mcp/tools/model-delete.js.map +2 -2
- package/dist/mcp/tools/model-search.js +14 -0
- package/dist/mcp/tools/model-search.js.map +2 -2
- package/dist/mcp/tools/model-show.js +29 -2
- package/dist/mcp/tools/model-show.js.map +2 -2
- package/dist/mcp/tools/model-trace.js +93 -30
- package/dist/mcp/tools/model-trace.js.map +2 -2
- package/dist/mcp/tools/model-update.js +46 -26
- package/dist/mcp/tools/model-update.js.map +2 -2
- package/dist/mcp/tools/model-validate.js +46 -4
- package/dist/mcp/tools/model-validate.js.map +2 -2
- package/dist/mcp/tools/project-status-collectors.js +381 -0
- package/dist/mcp/tools/project-status-collectors.js.map +7 -0
- package/dist/mcp/tools/project-status.js +237 -0
- package/dist/mcp/tools/project-status.js.map +7 -0
- package/dist/mcp/tools/project-tools.js +505 -0
- package/dist/mcp/tools/project-tools.js.map +7 -0
- package/dist/mcp/tools/relationship-tools.js +167 -0
- package/dist/mcp/tools/relationship-tools.js.map +7 -0
- package/dist/mcp/tools/review-open.js +210 -0
- package/dist/mcp/tools/review-open.js.map +7 -0
- package/dist/mcp/tools/shared.js +35 -4
- package/dist/mcp/tools/shared.js.map +2 -2
- package/dist/mcp/tools/spec-type.js +90 -0
- package/dist/mcp/tools/spec-type.js.map +7 -0
- package/dist/reports/model-report-orchestrator.js +31 -2
- package/dist/reports/model-report-orchestrator.js.map +2 -2
- package/dist/schemas/bundled/analyzers/cbm.json +1200 -70
- package/dist/schemas/bundled/analyzers/manifest.json +2 -2
- package/dist/schemas/bundled/api.json +1029 -1658
- package/dist/schemas/bundled/apm.json +499 -734
- package/dist/schemas/bundled/application.json +735 -371
- package/dist/schemas/bundled/base.json +3 -220
- package/dist/schemas/bundled/business.json +452 -366
- package/dist/schemas/bundled/data-model.json +395 -411
- package/dist/schemas/bundled/data-store.json +334 -558
- package/dist/schemas/bundled/manifest.json +18 -18
- package/dist/schemas/bundled/motivation.json +349 -368
- package/dist/schemas/bundled/navigation.json +415 -731
- package/dist/schemas/bundled/product.json +325 -159
- package/dist/schemas/bundled/security.json +864 -1232
- package/dist/schemas/bundled/technology.json +574 -703
- package/dist/schemas/bundled/testing.json +865 -1646
- package/dist/schemas/bundled/ux.json +739 -1352
- package/dist/server/schemas.js +8 -0
- package/dist/server/schemas.js.map +2 -2
- package/dist/server/server.js +686 -231
- package/dist/server/server.js.map +3 -3
- package/dist/server/viewer-registry.js +264 -0
- package/dist/server/viewer-registry.js.map +7 -0
- package/dist/server-entry.js +2 -1
- package/dist/server-entry.js.map +2 -2
- package/dist/telemetry/index.js +1 -1
- package/dist/utils/ansi-suppressor.js +6 -8
- package/dist/utils/ansi-suppressor.js.map +2 -2
- package/dist/utils/attribute-patch.js +41 -0
- package/dist/utils/attribute-patch.js.map +7 -0
- package/dist/utils/error-hints.js +29 -0
- package/dist/utils/error-hints.js.map +7 -0
- package/dist/utils/errors.js +9 -3
- package/dist/utils/errors.js.map +2 -2
- package/dist/utils/id-generator.js +5 -2
- package/dist/utils/id-generator.js.map +2 -2
- package/dist/utils/project-paths.js +26 -1
- package/dist/utils/project-paths.js.map +2 -2
- package/dist/utils/source-reference.js +64 -31
- package/dist/utils/source-reference.js.map +2 -2
- package/dist/utils/spec-version.js +2 -2
- package/dist/utils/spec-version.js.map +1 -1
- package/dist/validators/composed-reference-validator.js +5 -27
- package/dist/validators/composed-reference-validator.js.map +2 -2
- package/dist/validators/model-warnings.js +28 -0
- package/dist/validators/model-warnings.js.map +7 -0
- package/dist/validators/reference-validator.js +0 -7
- package/dist/validators/reference-validator.js.map +2 -2
- package/dist/validators/relationship-schema-validator.js +3 -3
- package/dist/validators/relationship-schema-validator.js.map +2 -2
- package/dist/validators/schema-validator.js +27 -9
- package/dist/validators/schema-validator.js.map +2 -2
- package/dist/validators/types.js.map +1 -1
- package/dist/viewer/README.md +6 -6
- package/dist/viewer/assets/main-CUOQGGRA.css +1 -0
- package/dist/viewer/assets/main-DvH3gYrI.js +84 -0
- package/dist/viewer/index.html +2 -2
- package/dist/viewer/manifest.json +10 -10
- package/dist/viewer/version.json +1 -1
- package/package.json +2 -1
- package/dist/viewer/assets/main-B7AOQXXX.js +0 -81
- package/dist/viewer/assets/main-vdrFrG7q.css +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/validators/composed-reference-validator.ts"],
|
|
4
|
-
"sourcesContent": ["import { ValidationResult } from \"./types.js\";\nimport type { Model } from \"../core/model.js\";\nimport { Validator } from \"./validator.js\";\nimport { parseReferencePath, ReferencePathParseError } from \"../utils/reference-path-parser.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\nimport { promises as fs, Dirent, Stats } from \"fs\";\nimport path from \"path\";\nimport { parse as parseYAML } from \"yaml\";\nimport { FarmManifest } from \"../core/farm-manifest.js\";\n\ninterface ResolvedExternalModel {\n modelName: string;\n elementIds: Set<string>;\n}\n\nexport class ComposedReferenceValidator {\n private modelPathOverrides: Record<string, string>;\n private resolvedModels: Map<string, ResolvedExternalModel> = new Map();\n private referencedModels: Set<string> = new Set();\n private resolutionDiagnostics: ValidationResult = new ValidationResult();\n\n constructor(modelPathOverrides: Record<string, string> = {}) {\n this.modelPathOverrides = modelPathOverrides;\n }\n\n /**\n * Create a ComposedReferenceValidator from a farm root\n * Automatically builds model-path overrides from the farm's project entries\n * @param farmRoot - Path to the farm root directory\n * @returns ComposedReferenceValidator instance configured for the farm\n */\n static async fromFarm(farmRoot: string): Promise<ComposedReferenceValidator> {\n const farmYamlPath = path.join(farmRoot, \"farm.yaml\");\n const manifest = await FarmManifest.load(farmYamlPath);\n\n const modelPathOverrides: Record<string, string> = {};\n\n // Build model-path overrides for each project in the farm\n for (const project of manifest.getAllProjects()) {\n // For detached model layouts, point directly to the model folder.\n // The validator will look for manifest.yaml within this folder.\n // If the model is co-located (inside codebase), it will also find it\n // via the documentation-robotics/model/ path search.\n const modelFullPath = path.join(farmRoot, project.model);\n modelPathOverrides[project.name] = modelFullPath;\n }\n\n return new ComposedReferenceValidator(modelPathOverrides);\n }\n\n async validateModel(model: Model): Promise<ValidationResult> {\n const fullValidator = new Validator();\n const result = await fullValidator.validateModel(model);\n\n const declaredModels = new Map<string, string>();\n // Map from lowercase model name to declared model name (preserving original casing)\n for (const modelName of Object.keys(model.manifest.models || {})) {\n declaredModels.set(modelName.toLowerCase(), modelName);\n }\n\n // Clear all state to ensure fresh validation on reuse\n this.referencedModels.clear();\n this.resolvedModels.clear();\n this.resolutionDiagnostics = new ValidationResult();\n this.collectReferencedModels(model);\n\n for (const declaredName of declaredModels.values()) {\n await this.resolveExternalModel(declaredName);\n }\n\n // Merge any diagnostics from resolution phase\n result.merge(this.resolutionDiagnostics);\n\n for (const [lowerModelName, declaredName] of declaredModels) {\n // Check if model is referenced using case-insensitive comparison\n const isReferenced = this.referencedModels.has(lowerModelName);\n // Check if model is resolved using the declared name\n const isResolved = this.resolvedModels.has(declaredName);\n\n if (!isResolved) {\n if (isReferenced) {\n // Declared, referenced, but unresolvable \u2192 error with guidance\n result.addError({\n layer: \"manifest\",\n message: `External model '${declaredName}' is referenced in qualified paths but could not be resolved from the configured paths`,\n category: \"reference\",\n fixSuggestion: `Ensure the model exists on disk, or use '--model-path ${declaredName}=/path/to/project-root' to specify its location`,\n });\n } else {\n // Declared but unreferenced and unresolvable \u2192 warning only\n result.addWarning({\n layer: \"manifest\",\n message: `External model '${declaredName}' is declared but not referenced in any qualified paths and could not be found on disk`,\n category: \"reference\",\n fixSuggestion: `Remove the declaration if not needed, or use '--model-path ${declaredName}=/path/to/project-root' to specify its location`,\n });\n }\n } else if (isReferenced) {\n // Declared, referenced, and resolvable \u2192 validate qualified references\n const validationResult = this.validateQualifiedReferencesAgainstModel(model, declaredName);\n result.merge(validationResult, `[Composed/${declaredName}]`);\n }\n }\n\n return result;\n }\n\n private collectReferencedModels(model: Model): void {\n for (const [, layer] of model.layers) {\n for (const element of layer.listElements()) {\n for (const ref of element.references || []) {\n if (ref.target.startsWith(\"@\")) {\n try {\n const parsed = parseReferencePath(ref.target);\n if (parsed.modelName) {\n // Store lowercase for case-insensitive comparison with declared models\n this.referencedModels.add(parsed.modelName.toLowerCase());\n }\n } catch (error) {\n if (error instanceof ReferencePathParseError) {\n // Parse errors are already handled by base validator\n continue;\n } else {\n throw error;\n }\n }\n }\n }\n }\n }\n }\n\n private async resolveExternalModel(modelName: string): Promise<void> {\n // Try exact match first, then case-insensitive match\n let modelPath = this.modelPathOverrides[modelName];\n if (!modelPath) {\n // Case-insensitive lookup in modelPathOverrides\n const lowerModelName = modelName.toLowerCase();\n for (const [key, overridePath] of Object.entries(this.modelPathOverrides)) {\n if (key.toLowerCase() === lowerModelName) {\n modelPath = overridePath;\n break;\n }\n }\n }\n\n if (!modelPath) {\n return;\n }\n\n try {\n // Verify modelPath itself is a directory\n let rootStats: Stats | undefined;\n try {\n rootStats = await fs.stat(modelPath);\n } catch (err) {\n const errorCode = (err as NodeJS.ErrnoException)?.code;\n if (errorCode !== \"ENOENT\") {\n throw err;\n }\n }\n\n if (rootStats && !rootStats.isDirectory()) {\n this.resolutionDiagnostics.addWarning({\n layer: \"manifest\",\n message: `External model '${modelName}' path is not a directory: ${modelPath}`,\n category: \"reference\",\n fixSuggestion: `Ensure '--model-path ${modelName}=${modelPath}' points to a project root containing documentation-robotics/model/`,\n });\n return;\n }\n\n // Try standard layout first: {modelPath}/documentation-robotics/model/\n let modelDir = path.join(modelPath, \"documentation-robotics\", \"model\");\n let stats: Stats | undefined;\n try {\n stats = await fs.stat(modelDir);\n } catch (err) {\n const errorCode = (err as NodeJS.ErrnoException)?.code;\n if (errorCode !== \"ENOENT\") {\n throw err;\n }\n }\n\n // Fall back to legacy layout: {modelPath}/model/\n if (!stats) {\n modelDir = path.join(modelPath, \"model\");\n try {\n stats = await fs.stat(modelDir);\n } catch (err) {\n const errorCode = (err as NodeJS.ErrnoException)?.code;\n if (errorCode !== \"ENOENT\") {\n throw err;\n }\n }\n }\n\n // Fall back to detached layout: modelPath itself is the model directory\n // (contains layer directories like 01_motivation, 07_api, etc.)\n if (!stats) {\n modelDir = modelPath;\n try {\n stats = await fs.stat(modelDir);\n } catch (err) {\n const errorCode = (err as NodeJS.ErrnoException)?.code;\n if (errorCode !== \"ENOENT\") {\n throw err;\n }\n }\n }\n\n if (!stats) {\n this.addResolutionDiagnostic(\n modelName,\n new Error(\"Model directory not found\"),\n modelPath,\n true\n );\n return;\n }\n\n if (!stats.isDirectory()) {\n this.resolutionDiagnostics.addWarning({\n layer: \"manifest\",\n message: `External model '${modelName}' path is not a directory: ${modelDir}`,\n category: \"reference\",\n fixSuggestion: `Ensure '--model-path ${modelName}=${modelPath}' points to a project root or model directory`,\n });\n return;\n }\n\n const elementIds = new Set<string>();\n\n // Try to load all layer directories\n let entries: Dirent[] = [];\n try {\n entries = await fs.readdir(modelDir, { withFileTypes: true });\n } catch (error) {\n this.addResolutionDiagnostic(modelName, error, modelPath);\n return;\n }\n\n // Load elements from each layer directory\n for (const entry of entries) {\n if (entry.isDirectory() && /^\\d+_/.test(entry.name)) {\n // This looks like a layer directory (e.g., \"01_motivation\", \"08_data-model\")\n const layerPath = path.join(modelDir, entry.name);\n await this.collectElementsFromLayer(layerPath, elementIds);\n }\n }\n\n // Successfully resolved the model (track it even if empty)\n this.resolvedModels.set(modelName, {\n modelName,\n elementIds,\n });\n } catch (error) {\n // Failed to resolve model\n this.addResolutionDiagnostic(modelName, error, modelPath);\n }\n }\n\n private async collectElementsFromLayer(layerPath: string, elementIds: Set<string>): Promise<void> {\n try {\n const entries = await fs.readdir(layerPath, { withFileTypes: true });\n\n for (const entry of entries) {\n if (entry.isFile() && (entry.name.endsWith(\".yaml\") || entry.name.endsWith(\".yml\"))) {\n // Read the YAML file and extract element paths/IDs\n try {\n const content = await fs.readFile(path.join(layerPath, entry.name), \"utf-8\");\n this.extractElementPathsFromYAML(content, elementIds, layerPath, entry.name);\n } catch (error) {\n const filePath = path.join(layerPath, entry.name);\n this.resolutionDiagnostics.addWarning({\n layer: \"manifest\",\n message: `Failed to read YAML file at ${filePath}: ${getErrorMessage(error)}`,\n category: \"reference\",\n });\n continue;\n }\n }\n }\n } catch (error) {\n this.resolutionDiagnostics.addWarning({\n layer: \"manifest\",\n message: `Failed to read layer directory at ${layerPath}: ${getErrorMessage(error)}`,\n category: \"reference\",\n });\n }\n }\n\n private extractElementPathsFromYAML(\n content: string,\n elementIds: Set<string>,\n layerPath: string,\n fileName: string\n ): void {\n try {\n const parsed = parseYAML(content);\n\n if (!parsed) {\n this.resolutionDiagnostics.addWarning({\n layer: \"manifest\",\n message: `YAML file at ${path.join(layerPath, fileName)} parsed to empty content`,\n category: \"reference\",\n });\n return;\n }\n\n // Collect items to process from various YAML formats\n const items: unknown[] = [];\n\n // Format 1: elements: [{ path: \"...\" }, ...]\n if (parsed && typeof parsed === \"object\" && \"elements\" in parsed) {\n const elements = parsed.elements;\n if (Array.isArray(elements)) {\n items.push(...elements);\n }\n }\n // Format 2: Direct array [{ path: \"...\" }, ...]\n else if (Array.isArray(parsed)) {\n items.push(...parsed);\n }\n // Format 3: Object-of-objects { \"key\": { path: \"...\" }, ... }\n else if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n // Check if this looks like an object-of-objects (values have path/id fields)\n const values = Object.values(parsed);\n if (values.length > 0 && values[0] && typeof values[0] === \"object\" && (\"path\" in values[0] || \"id\" in values[0])) {\n items.push(...values);\n } else {\n // Object without path/id fields, push as-is for type safety\n items.push(parsed);\n }\n }\n\n // Extract paths from collected items\n for (const item of items) {\n if (item && typeof item === \"object\") {\n const elementPath = (item as Record<string, unknown>).path || (item as Record<string, unknown>).id;\n if (elementPath && typeof elementPath === \"string\") {\n elementIds.add(elementPath.trim());\n }\n }\n }\n } catch (error) {\n this.resolutionDiagnostics.addWarning({\n layer: \"manifest\",\n message: `Failed to parse YAML file at ${path.join(layerPath, fileName)}: ${getErrorMessage(error)}`,\n category: \"reference\",\n });\n }\n }\n\n private validateQualifiedReferencesAgainstModel(\n model: Model,\n modelName: string\n ): ValidationResult {\n const result = new ValidationResult();\n const resolvedModel = this.resolvedModels.get(modelName);\n\n if (!resolvedModel) {\n return result;\n }\n\n for (const [sourceLayerName, layer] of model.layers) {\n for (const element of layer.listElements()) {\n const elementId = element.path || element.id;\n\n for (const ref of element.references || []) {\n if (!ref.target.startsWith(\"@\")) {\n continue;\n }\n\n try {\n const parsed = parseReferencePath(ref.target);\n // Compare model names case-insensitively\n if (parsed.modelName?.toLowerCase() !== modelName.toLowerCase()) {\n continue;\n }\n\n // Check if target element exists in the resolved external model\n if (!resolvedModel.elementIds.has(parsed.segment)) {\n result.addError({\n layer: sourceLayerName,\n elementId,\n message: `Broken qualified reference: target '${parsed.segment}' does not exist in external model '${modelName}'`,\n category: \"reference\",\n fixSuggestion: `Create element '${parsed.segment}' in model '${modelName}' or remove this reference`,\n });\n }\n } catch (error) {\n if (error instanceof ReferencePathParseError) {\n // Parse errors are already handled by base validator\n continue;\n } else {\n throw error;\n }\n }\n }\n }\n }\n\n return result;\n }\n\n private addResolutionDiagnostic(\n modelName: string,\n error: unknown,\n modelPath: string,\n isPathError: boolean = false\n ): void {\n const errorMsg = getErrorMessage(error);\n const code = (error as NodeJS.ErrnoException)?.code;\n\n let message = \"\";\n let fixSuggestion = \"\";\n\n if (code === \"EACCES\") {\n message =\n `Permission denied accessing external model '${modelName}' at ${modelPath}. ` +\n `Check file permissions.`;\n fixSuggestion = `Ensure the model path is readable, or use '--model-path ${modelName}=/path/to/alternative-location' to specify another location`;\n } else if (code === \"EIO\") {\n message =\n `I/O error reading external model '${modelName}' at ${modelPath}. ` +\n `There may be a filesystem or device issue.`;\n fixSuggestion = `Check the filesystem for errors, or provide an alternative model path via '--model-path ${modelName}=/path/to/alternative-location'`;\n } else if (code === \"ENOENT\" || isPathError) {\n message =\n `Could not find external model '${modelName}' \u2014 expected at ${modelPath}/documentation-robotics/model or ${modelPath}/model`;\n fixSuggestion = `Ensure the model exists at the specified path, or use '--model-path ${modelName}=/path/to/project-root' to specify its location`;\n } else {\n message =\n `Failed to resolve external model '${modelName}' at ${modelPath}: ${errorMsg}`;\n fixSuggestion = `Check the path and filesystem permissions, or use '--model-path ${modelName}=/path/to/project-root' to specify a different location`;\n }\n\n this.resolutionDiagnostics.addWarning({\n layer: \"manifest\",\n message,\n category: \"reference\",\n fixSuggestion,\n });\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,wBAAwB;AAEjC,SAAS,iBAAiB;AAC1B,SAAS,oBAAoB,+BAA+B;AAC5D,SAAS,uBAAuB;AAChC,SAAS,YAAY,UAAyB;AAC9C,OAAO,UAAU;AACjB,SAAS,SAAS,iBAAiB;AACnC,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["import { ValidationResult } from \"./types.js\";\nimport type { Model } from \"../core/model.js\";\nimport { Validator } from \"./validator.js\";\nimport { parseReferencePath, ReferencePathParseError } from \"../utils/reference-path-parser.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\nimport { promises as fs, Dirent, Stats } from \"fs\";\nimport path from \"path\";\nimport { parse as parseYAML } from \"yaml\";\nimport { FarmManifest } from \"../core/farm-manifest.js\";\nimport { resolveModelDirectory } from \"../utils/project-paths.js\";\n\ninterface ResolvedExternalModel {\n modelName: string;\n elementIds: Set<string>;\n}\n\nexport class ComposedReferenceValidator {\n private modelPathOverrides: Record<string, string>;\n private resolvedModels: Map<string, ResolvedExternalModel> = new Map();\n private referencedModels: Set<string> = new Set();\n private resolutionDiagnostics: ValidationResult = new ValidationResult();\n\n constructor(modelPathOverrides: Record<string, string> = {}) {\n this.modelPathOverrides = modelPathOverrides;\n }\n\n /**\n * Create a ComposedReferenceValidator from a farm root\n * Automatically builds model-path overrides from the farm's project entries\n * @param farmRoot - Path to the farm root directory\n * @returns ComposedReferenceValidator instance configured for the farm\n */\n static async fromFarm(farmRoot: string): Promise<ComposedReferenceValidator> {\n const farmYamlPath = path.join(farmRoot, \"farm.yaml\");\n const manifest = await FarmManifest.load(farmYamlPath);\n\n const modelPathOverrides: Record<string, string> = {};\n\n // Build model-path overrides for each project in the farm\n for (const project of manifest.getAllProjects()) {\n // For detached model layouts, point directly to the model folder.\n // The validator will look for manifest.yaml within this folder.\n // If the model is co-located (inside codebase), it will also find it\n // via the documentation-robotics/model/ path search.\n const modelFullPath = path.join(farmRoot, project.model);\n modelPathOverrides[project.name] = modelFullPath;\n }\n\n return new ComposedReferenceValidator(modelPathOverrides);\n }\n\n async validateModel(model: Model): Promise<ValidationResult> {\n const fullValidator = new Validator();\n const result = await fullValidator.validateModel(model);\n\n const declaredModels = new Map<string, string>();\n // Map from lowercase model name to declared model name (preserving original casing)\n for (const modelName of Object.keys(model.manifest.models || {})) {\n declaredModels.set(modelName.toLowerCase(), modelName);\n }\n\n // Clear all state to ensure fresh validation on reuse\n this.referencedModels.clear();\n this.resolvedModels.clear();\n this.resolutionDiagnostics = new ValidationResult();\n this.collectReferencedModels(model);\n\n for (const declaredName of declaredModels.values()) {\n await this.resolveExternalModel(declaredName);\n }\n\n // Merge any diagnostics from resolution phase\n result.merge(this.resolutionDiagnostics);\n\n for (const [lowerModelName, declaredName] of declaredModels) {\n // Check if model is referenced using case-insensitive comparison\n const isReferenced = this.referencedModels.has(lowerModelName);\n // Check if model is resolved using the declared name\n const isResolved = this.resolvedModels.has(declaredName);\n\n if (!isResolved) {\n if (isReferenced) {\n // Declared, referenced, but unresolvable \u2192 error with guidance\n result.addError({\n layer: \"manifest\",\n message: `External model '${declaredName}' is referenced in qualified paths but could not be resolved from the configured paths`,\n category: \"reference\",\n fixSuggestion: `Ensure the model exists on disk, or use '--model-path ${declaredName}=/path/to/project-root' to specify its location`,\n });\n } else {\n // Declared but unreferenced and unresolvable \u2192 warning only\n result.addWarning({\n layer: \"manifest\",\n message: `External model '${declaredName}' is declared but not referenced in any qualified paths and could not be found on disk`,\n category: \"reference\",\n fixSuggestion: `Remove the declaration if not needed, or use '--model-path ${declaredName}=/path/to/project-root' to specify its location`,\n });\n }\n } else if (isReferenced) {\n // Declared, referenced, and resolvable \u2192 validate qualified references\n const validationResult = this.validateQualifiedReferencesAgainstModel(model, declaredName);\n result.merge(validationResult, `[Composed/${declaredName}]`);\n }\n }\n\n return result;\n }\n\n private collectReferencedModels(model: Model): void {\n for (const [, layer] of model.layers) {\n for (const element of layer.listElements()) {\n for (const ref of element.references || []) {\n if (ref.target.startsWith(\"@\")) {\n try {\n const parsed = parseReferencePath(ref.target);\n if (parsed.modelName) {\n // Store lowercase for case-insensitive comparison with declared models\n this.referencedModels.add(parsed.modelName.toLowerCase());\n }\n } catch (error) {\n if (error instanceof ReferencePathParseError) {\n // Parse errors are already handled by base validator\n continue;\n } else {\n throw error;\n }\n }\n }\n }\n }\n }\n }\n\n private async resolveExternalModel(modelName: string): Promise<void> {\n // Try exact match first, then case-insensitive match\n let modelPath = this.modelPathOverrides[modelName];\n if (!modelPath) {\n // Case-insensitive lookup in modelPathOverrides\n const lowerModelName = modelName.toLowerCase();\n for (const [key, overridePath] of Object.entries(this.modelPathOverrides)) {\n if (key.toLowerCase() === lowerModelName) {\n modelPath = overridePath;\n break;\n }\n }\n }\n\n if (!modelPath) {\n return;\n }\n\n try {\n // Verify modelPath itself is a directory\n let rootStats: Stats | undefined;\n try {\n rootStats = await fs.stat(modelPath);\n } catch (err) {\n const errorCode = (err as NodeJS.ErrnoException)?.code;\n if (errorCode !== \"ENOENT\") {\n throw err;\n }\n }\n\n if (rootStats && !rootStats.isDirectory()) {\n this.resolutionDiagnostics.addWarning({\n layer: \"manifest\",\n message: `External model '${modelName}' path is not a directory: ${modelPath}`,\n category: \"reference\",\n fixSuggestion: `Ensure '--model-path ${modelName}=${modelPath}' points to a project root containing documentation-robotics/model/`,\n });\n return;\n }\n\n // Standard ({modelPath}/documentation-robotics/model/), legacy ({modelPath}/model/)\n // and detached (modelPath itself holds the layer directories) layouts, in that order\n const resolved = await resolveModelDirectory(modelPath);\n\n if (!resolved) {\n this.addResolutionDiagnostic(\n modelName,\n new Error(\"Model directory not found\"),\n modelPath,\n true\n );\n return;\n }\n\n const modelDir = resolved.modelDir;\n\n if (!resolved.isDirectory) {\n this.resolutionDiagnostics.addWarning({\n layer: \"manifest\",\n message: `External model '${modelName}' path is not a directory: ${modelDir}`,\n category: \"reference\",\n fixSuggestion: `Ensure '--model-path ${modelName}=${modelPath}' points to a project root or model directory`,\n });\n return;\n }\n\n const elementIds = new Set<string>();\n\n // Try to load all layer directories\n let entries: Dirent[] = [];\n try {\n entries = await fs.readdir(modelDir, { withFileTypes: true });\n } catch (error) {\n this.addResolutionDiagnostic(modelName, error, modelPath);\n return;\n }\n\n // Load elements from each layer directory\n for (const entry of entries) {\n if (entry.isDirectory() && /^\\d+_/.test(entry.name)) {\n // This looks like a layer directory (e.g., \"01_motivation\", \"08_data-model\")\n const layerPath = path.join(modelDir, entry.name);\n await this.collectElementsFromLayer(layerPath, elementIds);\n }\n }\n\n // Successfully resolved the model (track it even if empty)\n this.resolvedModels.set(modelName, {\n modelName,\n elementIds,\n });\n } catch (error) {\n // Failed to resolve model\n this.addResolutionDiagnostic(modelName, error, modelPath);\n }\n }\n\n private async collectElementsFromLayer(layerPath: string, elementIds: Set<string>): Promise<void> {\n try {\n const entries = await fs.readdir(layerPath, { withFileTypes: true });\n\n for (const entry of entries) {\n if (entry.isFile() && (entry.name.endsWith(\".yaml\") || entry.name.endsWith(\".yml\"))) {\n // Read the YAML file and extract element paths/IDs\n try {\n const content = await fs.readFile(path.join(layerPath, entry.name), \"utf-8\");\n this.extractElementPathsFromYAML(content, elementIds, layerPath, entry.name);\n } catch (error) {\n const filePath = path.join(layerPath, entry.name);\n this.resolutionDiagnostics.addWarning({\n layer: \"manifest\",\n message: `Failed to read YAML file at ${filePath}: ${getErrorMessage(error)}`,\n category: \"reference\",\n });\n continue;\n }\n }\n }\n } catch (error) {\n this.resolutionDiagnostics.addWarning({\n layer: \"manifest\",\n message: `Failed to read layer directory at ${layerPath}: ${getErrorMessage(error)}`,\n category: \"reference\",\n });\n }\n }\n\n private extractElementPathsFromYAML(\n content: string,\n elementIds: Set<string>,\n layerPath: string,\n fileName: string\n ): void {\n try {\n const parsed = parseYAML(content);\n\n if (!parsed) {\n this.resolutionDiagnostics.addWarning({\n layer: \"manifest\",\n message: `YAML file at ${path.join(layerPath, fileName)} parsed to empty content`,\n category: \"reference\",\n });\n return;\n }\n\n // Collect items to process from various YAML formats\n const items: unknown[] = [];\n\n // Format 1: elements: [{ path: \"...\" }, ...]\n if (parsed && typeof parsed === \"object\" && \"elements\" in parsed) {\n const elements = parsed.elements;\n if (Array.isArray(elements)) {\n items.push(...elements);\n }\n }\n // Format 2: Direct array [{ path: \"...\" }, ...]\n else if (Array.isArray(parsed)) {\n items.push(...parsed);\n }\n // Format 3: Object-of-objects { \"key\": { path: \"...\" }, ... }\n else if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n // Check if this looks like an object-of-objects (values have path/id fields)\n const values = Object.values(parsed);\n if (values.length > 0 && values[0] && typeof values[0] === \"object\" && (\"path\" in values[0] || \"id\" in values[0])) {\n items.push(...values);\n } else {\n // Object without path/id fields, push as-is for type safety\n items.push(parsed);\n }\n }\n\n // Extract paths from collected items\n for (const item of items) {\n if (item && typeof item === \"object\") {\n const elementPath = (item as Record<string, unknown>).path || (item as Record<string, unknown>).id;\n if (elementPath && typeof elementPath === \"string\") {\n elementIds.add(elementPath.trim());\n }\n }\n }\n } catch (error) {\n this.resolutionDiagnostics.addWarning({\n layer: \"manifest\",\n message: `Failed to parse YAML file at ${path.join(layerPath, fileName)}: ${getErrorMessage(error)}`,\n category: \"reference\",\n });\n }\n }\n\n private validateQualifiedReferencesAgainstModel(\n model: Model,\n modelName: string\n ): ValidationResult {\n const result = new ValidationResult();\n const resolvedModel = this.resolvedModels.get(modelName);\n\n if (!resolvedModel) {\n return result;\n }\n\n for (const [sourceLayerName, layer] of model.layers) {\n for (const element of layer.listElements()) {\n const elementId = element.path || element.id;\n\n for (const ref of element.references || []) {\n if (!ref.target.startsWith(\"@\")) {\n continue;\n }\n\n try {\n const parsed = parseReferencePath(ref.target);\n // Compare model names case-insensitively\n if (parsed.modelName?.toLowerCase() !== modelName.toLowerCase()) {\n continue;\n }\n\n // Check if target element exists in the resolved external model\n if (!resolvedModel.elementIds.has(parsed.segment)) {\n result.addError({\n layer: sourceLayerName,\n elementId,\n message: `Broken qualified reference: target '${parsed.segment}' does not exist in external model '${modelName}'`,\n category: \"reference\",\n fixSuggestion: `Create element '${parsed.segment}' in model '${modelName}' or remove this reference`,\n });\n }\n } catch (error) {\n if (error instanceof ReferencePathParseError) {\n // Parse errors are already handled by base validator\n continue;\n } else {\n throw error;\n }\n }\n }\n }\n }\n\n return result;\n }\n\n private addResolutionDiagnostic(\n modelName: string,\n error: unknown,\n modelPath: string,\n isPathError: boolean = false\n ): void {\n const errorMsg = getErrorMessage(error);\n const code = (error as NodeJS.ErrnoException)?.code;\n\n let message = \"\";\n let fixSuggestion = \"\";\n\n if (code === \"EACCES\") {\n message =\n `Permission denied accessing external model '${modelName}' at ${modelPath}. ` +\n `Check file permissions.`;\n fixSuggestion = `Ensure the model path is readable, or use '--model-path ${modelName}=/path/to/alternative-location' to specify another location`;\n } else if (code === \"EIO\") {\n message =\n `I/O error reading external model '${modelName}' at ${modelPath}. ` +\n `There may be a filesystem or device issue.`;\n fixSuggestion = `Check the filesystem for errors, or provide an alternative model path via '--model-path ${modelName}=/path/to/alternative-location'`;\n } else if (code === \"ENOENT\" || isPathError) {\n message =\n `Could not find external model '${modelName}' \u2014 expected at ${modelPath}/documentation-robotics/model or ${modelPath}/model`;\n fixSuggestion = `Ensure the model exists at the specified path, or use '--model-path ${modelName}=/path/to/project-root' to specify its location`;\n } else {\n message =\n `Failed to resolve external model '${modelName}' at ${modelPath}: ${errorMsg}`;\n fixSuggestion = `Check the path and filesystem permissions, or use '--model-path ${modelName}=/path/to/project-root' to specify a different location`;\n }\n\n this.resolutionDiagnostics.addWarning({\n layer: \"manifest\",\n message,\n category: \"reference\",\n fixSuggestion,\n });\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ValidationFormatter } from "./validation-formatter.js";
|
|
2
|
+
function addModelWarnings(result, model) {
|
|
3
|
+
const stats = ValidationFormatter.calculateStats(model);
|
|
4
|
+
for (const orphanId of stats.orphanedElements)
|
|
5
|
+
result.addWarning({
|
|
6
|
+
message: `Element '${orphanId}' is orphaned (no cross-layer references or intra-layer relationships)`,
|
|
7
|
+
layer: orphanId.split(".")[0] ?? "",
|
|
8
|
+
elementId: orphanId,
|
|
9
|
+
category: "orphan",
|
|
10
|
+
fixSuggestion: "Add cross-layer references or relationships to connect this element to the rest of the model"
|
|
11
|
+
});
|
|
12
|
+
for (const [layerName, layer] of model.layers)
|
|
13
|
+
for (const element of layer.listElements()) {
|
|
14
|
+
if (element.source_reference)
|
|
15
|
+
continue;
|
|
16
|
+
const elementId = element.path || element.id;
|
|
17
|
+
result.addWarning({
|
|
18
|
+
message: `Element '${elementId}' has no source reference`,
|
|
19
|
+
layer: layerName,
|
|
20
|
+
elementId,
|
|
21
|
+
fixSuggestion: "Add a source_reference with provenance to link this element to its implementation"
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
addModelWarnings
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=model-warnings.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/validators/model-warnings.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * The warnings `dr validate` adds after the validation pipeline: orphaned elements, and elements\n * with no source reference. The Validator doesn't raise them, so a caller that wants the same\n * warning count as `dr validate` adds them with this.\n */\n\nimport type { Model } from \"../core/model.js\";\nimport { ValidationFormatter } from \"./validation-formatter.js\";\nimport type { ValidationResult } from \"./types.js\";\n\nexport function addModelWarnings(result: ValidationResult, model: Model): void {\n const stats = ValidationFormatter.calculateStats(model);\n for (const orphanId of stats.orphanedElements) {\n result.addWarning({\n message: `Element '${orphanId}' is orphaned (no cross-layer references or intra-layer relationships)`,\n layer: orphanId.split(\".\")[0] ?? \"\",\n elementId: orphanId,\n category: \"orphan\",\n fixSuggestion: \"Add cross-layer references or relationships to connect this element to the rest of the model\",\n });\n }\n for (const [layerName, layer] of model.layers) {\n for (const element of layer.listElements()) {\n if (element.source_reference) continue;\n const elementId = element.path || element.id;\n result.addWarning({\n message: `Element '${elementId}' has no source reference`,\n layer: layerName,\n elementId,\n fixSuggestion: \"Add a source_reference with provenance to link this element to its implementation\",\n });\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "AAOA,SAAS,2BAA2B;AAG9B,SAAU,iBAAiB,QAA0B,OAAY;AACrE,QAAM,QAAQ,oBAAoB,eAAe,KAAK;AACtD,aAAW,YAAY,MAAM;AAC3B,WAAO,WAAW;MAChB,SAAS,YAAY,QAAQ;MAC7B,OAAO,SAAS,MAAM,GAAG,EAAE,CAAC,KAAK;MACjC,WAAW;MACX,UAAU;MACV,eAAe;KAChB;AAEH,aAAW,CAAC,WAAW,KAAK,KAAK,MAAM;AACrC,eAAW,WAAW,MAAM,aAAY,GAAI;AAC1C,UAAI,QAAQ;AAAkB;AAC9B,YAAM,YAAY,QAAQ,QAAQ,QAAQ;AAC1C,aAAO,WAAW;QAChB,SAAS,YAAY,SAAS;QAC9B,OAAO;QACP;QACA,eAAe;OAChB;IACH;AAEJ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -104,13 +104,6 @@ class ReferenceValidator {
|
|
|
104
104
|
});
|
|
105
105
|
continue;
|
|
106
106
|
}
|
|
107
|
-
sourceLevel > targetLevel && result.addError({
|
|
108
|
-
layer: sourceLayerName,
|
|
109
|
-
elementId,
|
|
110
|
-
message: `Invalid reference direction: ${sourceLayerName} (level ${sourceLevel}) cannot reference ${targetLayerName} (level ${targetLevel})`,
|
|
111
|
-
fixSuggestion: "References must go from higher layers to lower layers (motivation \u2192 testing)",
|
|
112
|
-
category: "reference"
|
|
113
|
-
});
|
|
114
107
|
}
|
|
115
108
|
}
|
|
116
109
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/validators/reference-validator.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Reference validation for cross-layer integrity\n */\n\nimport { ValidationResult } from \"./types.js\";\nimport type { Model } from \"../core/model.js\";\nimport { getLayerByNumber, LAYER_HIERARCHY, getAllLayerIds } from \"../generated/layer-registry.js\";\nimport { parseReferencePath, ReferencePathParseError } from \"../utils/reference-path-parser.js\";\n\n/**\n * Validator for cross-layer references\n */\nexport class ReferenceValidator {\n // Known layer names from generated registry (including hyphenated ones)\n private readonly KNOWN_LAYERS = getAllLayerIds();\n\n // Build layer hierarchy map at instantiation from generated registry\n private readonly LAYER_HIERARCHY: Record<string, number> = this.buildLayerHierarchy();\n\n private buildLayerHierarchy(): Record<string, number> {\n const hierarchy: Record<string, number> = {};\n for (const layerNumber of LAYER_HIERARCHY) {\n const layer = getLayerByNumber(layerNumber);\n if (layer) {\n hierarchy[layer.id] = layer.number;\n }\n }\n return hierarchy;\n }\n\n /**\n * Validate all references in a model\n */\n validateModel(model: Model): ValidationResult {\n const result = new ValidationResult();\n const allElementIds = this.collectAllElementIds(model);\n\n for (const [layerName, layer] of model.layers) {\n for (const element of layer.listElements()) {\n this.validateReferences(\n element.path || element.id,\n element.references || [],\n layerName,\n allElementIds,\n model,\n result\n );\n }\n }\n\n return result;\n }\n\n /**\n * Collect all element IDs across the model\n */\n private collectAllElementIds(model: Model): Set<string> {\n const ids = new Set<string>();\n\n for (const layer of model.layers.values()) {\n for (const element of layer.listElements()) {\n ids.add(element.path || element.id);\n // Also add raw UUID so references stored by UUID still resolve\n ids.add(element.id);\n }\n }\n\n return ids;\n }\n\n /**\n * Validate references for an element\n */\n private validateReferences(\n elementId: string,\n references: Array<{ target: string; type?: string }>,\n sourceLayerName: string,\n validIds: Set<string>,\n model: Model,\n result: ValidationResult\n ): void {\n for (const ref of references) {\n let parsedModel: string | undefined;\n let targetSegment = ref.target;\n\n // Check if this is a qualified reference (starts with @)\n if (ref.target.startsWith(\"@\")) {\n // Try to parse the qualified reference\n try {\n const parsed = parseReferencePath(ref.target);\n parsedModel = parsed.modelName;\n targetSegment = parsed.segment;\n } catch (error) {\n if (error instanceof ReferencePathParseError) {\n result.addError({\n layer: sourceLayerName,\n elementId,\n message: `Malformed qualified reference path: ${error.message}`,\n fixSuggestion: `Use format '@{model-name}/{layer}.{type}.{name}' for qualified references or '{layer}.{type}.{name}' for unqualified references`,\n category: \"reference\",\n });\n } else {\n throw error;\n }\n continue;\n }\n\n // For qualified references, check if the model is declared\n // Normalize lookup to handle case-insensitive manifest keys\n const modelFound = parsedModel && model.manifest.models && this.findManifestModel(model.manifest.models, parsedModel);\n if (parsedModel && !modelFound) {\n result.addError({\n layer: sourceLayerName,\n elementId,\n message: `Unknown external model reference: model '${parsedModel}' is not declared in manifest`,\n fixSuggestion: `Add model '${parsedModel}' to the 'models' section of manifest.yaml`,\n category: \"reference\",\n });\n continue;\n }\n } else {\n // For unqualified references, check if target exists locally\n if (!validIds.has(ref.target)) {\n // Skip validation for references to unloaded layers when filter is active\n if (model.loadedLayerFilter && model.loadedLayerFilter.length > 0) {\n const targetLayerName = this.extractLayerFromId(ref.target);\n if (!model.loadedLayerFilter.includes(targetLayerName)) {\n // Target is in an unloaded layer; skip this reference\n continue;\n }\n }\n\n result.addError({\n layer: sourceLayerName,\n elementId,\n message: `Broken reference: target '${ref.target}' does not exist`,\n fixSuggestion: `Remove reference or create element '${ref.target}'`,\n category: \"reference\",\n });\n continue;\n }\n }\n\n // Check
|
|
5
|
-
"mappings": "AAIA,SAAS,wBAAwB;AAEjC,SAAS,kBAAkB,iBAAiB,sBAAsB;AAClE,SAAS,oBAAoB,+BAA+B;AAKtD,MAAO,mBAAkB;;EAEZ,eAAe,eAAc;;EAG7B,kBAA0C,KAAK,oBAAmB;EAE3E,sBAAmB;AACzB,UAAM,YAAoC,CAAA;AAC1C,eAAW,eAAe,iBAAiB;AACzC,YAAM,QAAQ,iBAAiB,WAAW;AAC1C,MAAI,UACF,UAAU,MAAM,EAAE,IAAI,MAAM;IAEhC;AACA,WAAO;EACT;;;;EAKA,cAAc,OAAY;AACxB,UAAM,SAAS,IAAI,iBAAgB,GAC7B,gBAAgB,KAAK,qBAAqB,KAAK;AAErD,eAAW,CAAC,WAAW,KAAK,KAAK,MAAM;AACrC,iBAAW,WAAW,MAAM,aAAY;AACtC,aAAK,mBACH,QAAQ,QAAQ,QAAQ,IACxB,QAAQ,cAAc,CAAA,GACtB,WACA,eACA,OACA,MAAM;AAKZ,WAAO;EACT;;;;EAKQ,qBAAqB,OAAY;AACvC,UAAM,MAAM,oBAAI,IAAG;AAEnB,eAAW,SAAS,MAAM,OAAO,OAAM;AACrC,iBAAW,WAAW,MAAM,aAAY;AACtC,YAAI,IAAI,QAAQ,QAAQ,QAAQ,EAAE,GAElC,IAAI,IAAI,QAAQ,EAAE;AAItB,WAAO;EACT;;;;EAKQ,mBACN,WACA,YACA,iBACA,UACA,OACA,QAAwB;AAExB,eAAW,OAAO,YAAY;AAC5B,UAAI,aACA,gBAAgB,IAAI;AAGxB,UAAI,IAAI,OAAO,WAAW,GAAG,GAAG;AAE9B,YAAI;AACF,gBAAM,SAAS,mBAAmB,IAAI,MAAM;AAC5C,wBAAc,OAAO,WACrB,gBAAgB,OAAO;QACzB,SAAS,OAAO;AACd,cAAI,iBAAiB;AACnB,mBAAO,SAAS;cACd,OAAO;cACP;cACA,SAAS,uCAAuC,MAAM,OAAO;cAC7D,eAAe;cACf,UAAU;aACX;;AAED,kBAAM;AAER;QACF;AAIA,cAAM,aAAa,eAAe,MAAM,SAAS,UAAU,KAAK,kBAAkB,MAAM,SAAS,QAAQ,WAAW;AACpH,YAAI,eAAe,CAAC,YAAY;AAC9B,iBAAO,SAAS;YACd,OAAO;YACP;YACA,SAAS,4CAA4C,WAAW;YAChE,eAAe,cAAc,WAAW;YACxC,UAAU;WACX;AACD;QACF;MACF,WAEM,CAAC,SAAS,IAAI,IAAI,MAAM,GAAG;AAE7B,YAAI,MAAM,qBAAqB,MAAM,kBAAkB,SAAS,GAAG;AACjE,gBAAMA,mBAAkB,KAAK,mBAAmB,IAAI,MAAM;AAC1D,cAAI,CAAC,MAAM,kBAAkB,SAASA,gBAAe;AAEnD;QAEJ;AAEA,eAAO,SAAS;UACd,OAAO;UACP;UACA,SAAS,6BAA6B,IAAI,MAAM;UAChD,eAAe,uCAAuC,IAAI,MAAM;UAChE,UAAU;SACX;AACD;MACF;AAMF,YAAM,kBAAkB,KAAK,mBAAmB,aAAa,GACvD,cAAc,KAAK,gBAAgB,eAAe,GAClD,cAAc,KAAK,gBAAgB,eAAe;AAExD,UAAI,gBAAgB,QAAW;AAC7B,eAAO,SAAS;UACd,OAAO;UACP;UACA,SAAS,yBAAyB,eAAe;UACjD,eACE,gCAAgC,KAAK,aAAa,KAAK,IAAI,CAAC;UAC9D,UAAU;SACX;AACD;MACF;AAEA,UAAI,gBAAgB,QAAW;AAC7B,eAAO,SAAS;UACd,OAAO;UACP;UACA,SAAS,mBAAmB,IAAI,MAAM,wBAAwB,eAAe;UAC7E,eAAe,0BAA0B,IAAI,MAAM;UACnD,UAAU;SACX;AACD;MACF;
|
|
4
|
+
"sourcesContent": ["/**\n * Reference validation for cross-layer integrity\n */\n\nimport { ValidationResult } from \"./types.js\";\nimport type { Model } from \"../core/model.js\";\nimport { getLayerByNumber, LAYER_HIERARCHY, getAllLayerIds } from \"../generated/layer-registry.js\";\nimport { parseReferencePath, ReferencePathParseError } from \"../utils/reference-path-parser.js\";\n\n/**\n * Validator for cross-layer references\n */\nexport class ReferenceValidator {\n // Known layer names from generated registry (including hyphenated ones)\n private readonly KNOWN_LAYERS = getAllLayerIds();\n\n // Build layer hierarchy map at instantiation from generated registry\n private readonly LAYER_HIERARCHY: Record<string, number> = this.buildLayerHierarchy();\n\n private buildLayerHierarchy(): Record<string, number> {\n const hierarchy: Record<string, number> = {};\n for (const layerNumber of LAYER_HIERARCHY) {\n const layer = getLayerByNumber(layerNumber);\n if (layer) {\n hierarchy[layer.id] = layer.number;\n }\n }\n return hierarchy;\n }\n\n /**\n * Validate all references in a model\n */\n validateModel(model: Model): ValidationResult {\n const result = new ValidationResult();\n const allElementIds = this.collectAllElementIds(model);\n\n for (const [layerName, layer] of model.layers) {\n for (const element of layer.listElements()) {\n this.validateReferences(\n element.path || element.id,\n element.references || [],\n layerName,\n allElementIds,\n model,\n result\n );\n }\n }\n\n return result;\n }\n\n /**\n * Collect all element IDs across the model\n */\n private collectAllElementIds(model: Model): Set<string> {\n const ids = new Set<string>();\n\n for (const layer of model.layers.values()) {\n for (const element of layer.listElements()) {\n ids.add(element.path || element.id);\n // Also add raw UUID so references stored by UUID still resolve\n ids.add(element.id);\n }\n }\n\n return ids;\n }\n\n /**\n * Validate references for an element\n */\n private validateReferences(\n elementId: string,\n references: Array<{ target: string; type?: string }>,\n sourceLayerName: string,\n validIds: Set<string>,\n model: Model,\n result: ValidationResult\n ): void {\n for (const ref of references) {\n let parsedModel: string | undefined;\n let targetSegment = ref.target;\n\n // Check if this is a qualified reference (starts with @)\n if (ref.target.startsWith(\"@\")) {\n // Try to parse the qualified reference\n try {\n const parsed = parseReferencePath(ref.target);\n parsedModel = parsed.modelName;\n targetSegment = parsed.segment;\n } catch (error) {\n if (error instanceof ReferencePathParseError) {\n result.addError({\n layer: sourceLayerName,\n elementId,\n message: `Malformed qualified reference path: ${error.message}`,\n fixSuggestion: `Use format '@{model-name}/{layer}.{type}.{name}' for qualified references or '{layer}.{type}.{name}' for unqualified references`,\n category: \"reference\",\n });\n } else {\n throw error;\n }\n continue;\n }\n\n // For qualified references, check if the model is declared\n // Normalize lookup to handle case-insensitive manifest keys\n const modelFound = parsedModel && model.manifest.models && this.findManifestModel(model.manifest.models, parsedModel);\n if (parsedModel && !modelFound) {\n result.addError({\n layer: sourceLayerName,\n elementId,\n message: `Unknown external model reference: model '${parsedModel}' is not declared in manifest`,\n fixSuggestion: `Add model '${parsedModel}' to the 'models' section of manifest.yaml`,\n category: \"reference\",\n });\n continue;\n }\n } else {\n // For unqualified references, check if target exists locally\n if (!validIds.has(ref.target)) {\n // Skip validation for references to unloaded layers when filter is active\n if (model.loadedLayerFilter && model.loadedLayerFilter.length > 0) {\n const targetLayerName = this.extractLayerFromId(ref.target);\n if (!model.loadedLayerFilter.includes(targetLayerName)) {\n // Target is in an unloaded layer; skip this reference\n continue;\n }\n }\n\n result.addError({\n layer: sourceLayerName,\n elementId,\n message: `Broken reference: target '${ref.target}' does not exist`,\n fixSuggestion: `Remove reference or create element '${ref.target}'`,\n category: \"reference\",\n });\n continue;\n }\n }\n\n // Check that both layers exist\n // For qualified references, extract layer from the parsed segment\n // For unqualified references, extract layer from the target directly\n const targetLayerName = this.extractLayerFromId(targetSegment);\n const sourceLevel = this.LAYER_HIERARCHY[sourceLayerName];\n const targetLevel = this.LAYER_HIERARCHY[targetLayerName];\n\n if (sourceLevel === undefined) {\n result.addError({\n layer: sourceLayerName,\n elementId,\n message: `Unknown source layer: ${sourceLayerName}`,\n fixSuggestion:\n `Use one of the valid layers: ${this.KNOWN_LAYERS.join(\", \")}`,\n category: \"reference\",\n });\n continue;\n }\n\n if (targetLevel === undefined) {\n result.addError({\n layer: sourceLayerName,\n elementId,\n message: `Target element '${ref.target}' has unknown layer: ${targetLayerName}`,\n fixSuggestion: `Check that element ID '${ref.target}' uses a valid layer prefix (e.g., motivation-, business-, api-)`,\n category: \"reference\",\n });\n continue;\n }\n\n // No directional constraint: direction is a property of the predicate, not of the\n // layers (spec/DESIGN_CHARTER.md). Layer levels are resolved above only to reject\n // references to unknown layers.\n }\n }\n\n /**\n * Extract layer name from element ID, handling both dot-separated and hyphenated layer names\n */\n private extractLayerFromId(elementId: string): string {\n // Determine if using dot-separated format (e.g., motivation.goal.name) or hyphenated (e.g., motivation-goal-name)\n const isDotSeparated = elementId.includes(\".\");\n const separator = isDotSeparated ? \".\" : \"-\";\n\n // Try to match known layers in order of specificity (longest first)\n const sortedLayers = [...this.KNOWN_LAYERS].sort((a, b) => b.length - a.length);\n\n for (const layer of sortedLayers) {\n if (elementId.startsWith(layer + separator)) {\n return layer;\n }\n }\n\n // Fallback: return first segment (shouldn't reach here with valid element IDs)\n return elementId.split(separator)[0] || \"\";\n }\n\n /**\n * Find a model in the manifest using case-insensitive lookup\n * Handles the case where parseReferencePath normalizes model names to lowercase,\n * but the manifest may have different casing for the declared model keys.\n */\n private findManifestModel(\n models: Record<string, unknown>,\n modelName: string\n ): boolean {\n const lowerModelName = modelName.toLowerCase();\n for (const key of Object.keys(models)) {\n if (key.toLowerCase() === lowerModelName) {\n return true;\n }\n }\n return false;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["targetLayerName"]
|
|
7
7
|
}
|
|
@@ -131,7 +131,7 @@ class RelationshipValidator {
|
|
|
131
131
|
return errors.push({
|
|
132
132
|
layer: relationship.layer,
|
|
133
133
|
elementId: relationship.source,
|
|
134
|
-
message: `No schema found for relationship: ${sourceLayer}.${sourceType} --[${relationship.predicate}]--> ${targetLayer}.${targetType}`
|
|
134
|
+
message: `No schema found for relationship: ${relationship.source} --[${relationship.predicate}]--> ${relationship.target} (${sourceLayer}.${sourceType} --[${relationship.predicate}]--> ${targetLayer}.${targetType})`
|
|
135
135
|
}), errors;
|
|
136
136
|
}
|
|
137
137
|
const schema = this.relationshipSchemas.get(schemaKey);
|
|
@@ -139,7 +139,7 @@ class RelationshipValidator {
|
|
|
139
139
|
return errors.push({
|
|
140
140
|
layer: relationship.layer,
|
|
141
141
|
elementId: relationship.source,
|
|
142
|
-
message: `Failed to load schema for relationship: ${schemaKey}`
|
|
142
|
+
message: `Failed to load schema for relationship: ${relationship.source} --[${relationship.predicate}]--> ${relationship.target} (${schemaKey})`
|
|
143
143
|
}), errors;
|
|
144
144
|
const cardinalityErrors = this.validateCardinality(relationship, schema, relationshipsBySource, relationshipsByTarget);
|
|
145
145
|
if (errors.push(...cardinalityErrors), relationship.properties && schema.attributes) {
|
|
@@ -207,7 +207,7 @@ class RelationshipValidator {
|
|
|
207
207
|
key in schema.attributes || errors.push({
|
|
208
208
|
layer: relationship.layer,
|
|
209
209
|
elementId: relationship.source,
|
|
210
|
-
message: `Relationship property '${key}' not defined in schema for predicate '${relationship.predicate}'`
|
|
210
|
+
message: `Relationship property '${key}' on ${relationship.source} --[${relationship.predicate}]--> ${relationship.target} not defined in schema for predicate '${relationship.predicate}'`
|
|
211
211
|
});
|
|
212
212
|
return errors;
|
|
213
213
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/validators/relationship-schema-validator.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Relationship Schema Validation with Cardinality Enforcement\n *\n * Validates relationships against their spec relationship schemas, including:\n * - Schema compliance (source/destination node types)\n * - Cardinality constraints (one-to-one, one-to-many, many-to-one, many-to-many)\n * - Predicate validity\n * - Relationship attributes against schema\n */\n\nimport { ValidationResult } from \"./types.js\";\nimport type { Model } from \"../core/model.js\";\nimport type { Relationship } from \"../core/relationships.js\";\nimport { fileURLToPath } from \"url\";\nimport path from \"path\";\nimport { readFile } from \"../utils/file-io.js\";\nimport { existsSync } from \"fs\";\nimport { startActiveSpan } from \"../telemetry/index.js\";\n\ninterface ManifestDistFile {\n layers: Array<{ id: string }>;\n}\n\ninterface LayerDistFile {\n relationshipSchemas: Record<string, {\n id: string;\n source_spec_node_id: string;\n source_layer: string;\n destination_spec_node_id: string;\n destination_layer: string;\n predicate: string;\n cardinality: \"one-to-one\" | \"one-to-many\" | \"many-to-one\" | \"many-to-many\";\n strength: string;\n required?: boolean;\n attributes?: Record<string, unknown>;\n }>;\n}\n\ndeclare const TELEMETRY_ENABLED: boolean | undefined;\nconst isTelemetryEnabled = typeof TELEMETRY_ENABLED !== \"undefined\" ? TELEMETRY_ENABLED : false;\n\nexport interface RelationshipSchema {\n id: string;\n source_spec_node_id: string;\n source_layer: string;\n destination_spec_node_id: string;\n destination_layer: string;\n predicate: string;\n cardinality: \"one-to-one\" | \"one-to-many\" | \"many-to-one\" | \"many-to-many\";\n strength: string;\n required?: boolean;\n attributes?: Record<string, unknown>;\n}\n\n/**\n * Validator for relationship schema compliance and cardinality constraints\n *\n * Validates that relationships:\n * 1. Conform to their spec relationship schemas\n * 2. Respect cardinality constraints (e.g., one-to-one means max 1 relationship of that type)\n * 3. Reference valid predicates\n * 4. Have valid source/destination element types\n */\nexport class RelationshipValidator {\n private schemasDir: string;\n private relationshipSchemas: Map<string, RelationshipSchema> = new Map();\n\n constructor() {\n const __dirname = path.dirname(fileURLToPath(import.meta.url));\n this.schemasDir = path.join(__dirname, \"..\", \"schemas\", \"bundled\");\n }\n\n /**\n * Initialize and load relationship schemas\n */\n async initialize(): Promise<void> {\n await this.loadRelationshipSchemas();\n }\n\n /**\n * Load relationship schemas from compiled bundled spec (manifest.json + per-layer JSON files)\n */\n private async loadRelationshipSchemas(): Promise<void> {\n const manifestPath = path.join(this.schemasDir, \"manifest.json\");\n\n if (!existsSync(manifestPath)) {\n throw new Error(`Bundled manifest not found at ${manifestPath}`);\n }\n\n const manifestContent = await readFile(manifestPath);\n const manifest = JSON.parse(manifestContent) as ManifestDistFile;\n\n for (const { id: layerId } of manifest.layers) {\n const layerPath = path.join(this.schemasDir, `${layerId}.json`);\n if (!existsSync(layerPath)) {\n continue;\n }\n\n try {\n const layerContent = await readFile(layerPath);\n const layerData = JSON.parse(layerContent) as LayerDistFile;\n\n for (const [relId, relSchema] of Object.entries(layerData.relationshipSchemas || {})) {\n this.relationshipSchemas.set(relId, {\n id: relSchema.id || relId,\n source_spec_node_id: relSchema.source_spec_node_id || \"\",\n source_layer: relSchema.source_layer || \"\",\n destination_spec_node_id: relSchema.destination_spec_node_id || \"\",\n destination_layer: relSchema.destination_layer || \"\",\n predicate: relSchema.predicate || \"\",\n cardinality: relSchema.cardinality || \"many-to-many\",\n strength: relSchema.strength || \"medium\",\n required: relSchema.required || false,\n attributes: relSchema.attributes,\n });\n }\n } catch (error: any) {\n throw new Error(`Failed to load relationship schemas for layer '${layerId}': ${error.message}`);\n }\n }\n }\n\n /**\n * Validate relationships in a model with cardinality enforcement\n */\n async validateModel(model: Model): Promise<ValidationResult> {\n return startActiveSpan(\n \"relationship-schema.validate-model\",\n async (span) => {\n // Ensure schemas are loaded\n if (this.relationshipSchemas.size === 0) {\n try {\n await this.initialize();\n } catch (error: any) {\n const result = new ValidationResult();\n result.addError({\n layer: \"system\",\n message: `${error.message} Relationship validation is not available.`,\n });\n return result;\n }\n }\n\n // Verify schemas were loaded. This covers two cases:\n // 1. initialize() succeeded but the manifest has no layers with relationship schemas\n // 2. Edge case where manifest exists but is empty\n // In both cases, relationship validation cannot proceed.\n if (this.relationshipSchemas.size === 0) {\n const result = new ValidationResult();\n result.addError({\n layer: \"system\",\n message: `No relationship schemas loaded from ${path.join(this.schemasDir, \"manifest.json\")}. Relationship validation is not available.`,\n });\n return result;\n }\n\n const result = new ValidationResult();\n const allRelationships = model.relationships.getAll();\n\n if (allRelationships.length === 0) {\n return result;\n }\n\n // Group relationships for cardinality validation\n const relationshipsBySource = this.groupRelationshipsBySourceTarget(\n allRelationships,\n \"source\"\n );\n const relationshipsByTarget = this.groupRelationshipsBySourceTarget(\n allRelationships,\n \"target\"\n );\n\n // Validate each relationship\n for (const relationship of allRelationships) {\n const relErrors = await this.validateRelationship(\n relationship,\n model,\n relationshipsBySource,\n relationshipsByTarget\n );\n\n if (relErrors.length > 0) {\n for (const error of relErrors) {\n result.addError(error);\n }\n }\n }\n\n if (isTelemetryEnabled) {\n span.setAttribute(\"relationship.error_count\", result.errors.length);\n span.setAttribute(\"relationship.warning_count\", result.warnings.length);\n span.setAttribute(\"relationship.validated_count\", allRelationships.length);\n }\n\n return result;\n },\n { \"relationship.layer_count\": model.layers.size }\n );\n }\n\n /**\n * Validate a single relationship\n */\n private async validateRelationship(\n relationship: Relationship,\n model: Model,\n relationshipsBySource: Map<string, Relationship[]>,\n relationshipsByTarget: Map<string, Relationship[]>\n ): Promise<Array<{ layer: string; message: string; elementId?: string }>> {\n const errors: Array<{ layer: string; message: string; elementId?: string }> = [];\n\n // Variables to cache found elements and avoid redundant lookups\n let sourceElement: any = null;\n let targetElement: any = null;\n let bothInFilterAndFound = false;\n\n // Check if relationship crosses layer boundary when --layers filter is active\n if (model.loadedLayerFilter && model.loadedLayerFilter.length > 0) {\n const sourceLayer = this.extractLayerFromElementId(relationship.source);\n const targetLayer = this.extractLayerFromElementId(relationship.target);\n\n // If we can parse layers and both endpoints are within the filter, proceed with full validation.\n // If one or both endpoints are outside the filter, only skip if both endpoints are\n // in unloaded layers. If either endpoint is in a loaded layer, we must validate it.\n if (sourceLayer && targetLayer) {\n const sourceInFilter = model.loadedLayerFilter.includes(sourceLayer);\n const targetInFilter = model.loadedLayerFilter.includes(targetLayer);\n\n // Skip only if BOTH endpoints are in unloaded layers\n if (!sourceInFilter && !targetInFilter) {\n return errors;\n }\n\n // If at least one endpoint is in a loaded layer, check if both elements exist in loaded layers\n // If one is missing from a loaded layer, report error (genuine break)\n if (sourceInFilter) {\n sourceElement = this.findElementInModel(model, relationship.source);\n if (!sourceElement) {\n errors.push({\n layer: relationship.layer,\n elementId: relationship.source,\n message: `Relationship source element '${relationship.source}' not found`,\n });\n return errors;\n }\n }\n\n if (targetInFilter) {\n targetElement = this.findElementInModel(model, relationship.target);\n if (!targetElement) {\n errors.push({\n layer: relationship.layer,\n elementId: relationship.target,\n message: `Relationship target element '${relationship.target}' not found`,\n });\n return errors;\n }\n }\n\n // If one or both endpoints are in unloaded layers, skip full validation\n if (!sourceInFilter || !targetInFilter) {\n return errors;\n }\n\n // Both endpoints are in loaded layers and we've already found them\n bothInFilterAndFound = true;\n }\n // If we can't parse layers (unparseable element IDs), proceed to validation\n // where findElementInModel will fail with a proper error message\n }\n\n // Find source and destination elements only if we haven't already found them\n if (!bothInFilterAndFound) {\n sourceElement = this.findElementInModel(model, relationship.source);\n targetElement = this.findElementInModel(model, relationship.target);\n }\n\n if (!sourceElement) {\n errors.push({\n layer: relationship.layer,\n elementId: relationship.source,\n message: `Relationship source element '${relationship.source}' not found`,\n });\n return errors;\n }\n\n if (!targetElement) {\n errors.push({\n layer: relationship.layer,\n elementId: relationship.target,\n message: `Relationship target element '${relationship.target}' not found`,\n });\n return errors;\n }\n\n // Extract element types AND layers (CRITICAL FIX for cross-layer relationships)\n // The source and target elements may be in different layers\n const sourceLayer = sourceElement.layer_id || sourceElement.layer || relationship.layer;\n const targetLayer = targetElement.layer_id || targetElement.layer || relationship.layer;\n\n // Use spec_node_id if present, otherwise construct from layer and type\n // spec_node_id contains the canonical type (e.g., \"application.applicationservice\")\n // Guard against empty spec_node_id from older CLI versions or manually authored elements\n const sourceSpecId = sourceElement.spec_node_id\n ? sourceElement.spec_node_id\n : `${sourceLayer}.${sourceElement.type}`;\n const targetSpecId = targetElement.spec_node_id\n ? targetElement.spec_node_id\n : `${targetLayer}.${targetElement.type}`;\n\n // Find applicable relationship schema using actual source/target layers and spec node IDs\n // (not relationship.layer for both, which assumes intra-layer relationships)\n const schemaKey = this.findRelationshipSchemaKeyBySpecId(\n sourceSpecId,\n targetSpecId,\n relationship.predicate\n );\n\n if (!schemaKey) {\n // Extract type names from spec_node_id for error message (e.g., \"applicationservice\" from \"application.applicationservice\")\n const sourceType = sourceSpecId.includes(\".\") ? sourceSpecId.split(\".\")[1] : sourceElement.type;\n const targetType = targetSpecId.includes(\".\") ? targetSpecId.split(\".\")[1] : targetElement.type;\n errors.push({\n layer: relationship.layer,\n elementId: relationship.source,\n message: `No schema found for relationship: ${sourceLayer}.${sourceType} --[${relationship.predicate}]--> ${targetLayer}.${targetType}`,\n });\n return errors;\n }\n\n const schema = this.relationshipSchemas.get(schemaKey);\n if (!schema) {\n errors.push({\n layer: relationship.layer,\n elementId: relationship.source,\n message: `Failed to load schema for relationship: ${schemaKey}`,\n });\n return errors;\n }\n\n // Validate cardinality constraints\n const cardinalityErrors = this.validateCardinality(\n relationship,\n schema,\n relationshipsBySource,\n relationshipsByTarget\n );\n\n errors.push(...cardinalityErrors);\n\n // Validate relationship attributes if schema specifies them\n if (relationship.properties && schema.attributes) {\n const attrErrors = this.validateRelationshipAttributes(\n relationship,\n schema\n );\n errors.push(...attrErrors);\n }\n\n return errors;\n }\n\n /**\n * Validate cardinality constraints for a relationship\n *\n * Cardinality constraint semantics:\n * - \"one-to-one\": Each source can have at most 1 such relationship, and each target can be involved in at most 1\n * - \"one-to-many\": Each target can receive at most 1 such relationship (target is the \"one\"); source is unconstrained\n * - \"many-to-one\": Each source can be involved in at most 1 such relationship (maps to one target); multiple sources may share the same target\n * - \"many-to-many\": No cardinality constraints\n */\n private validateCardinality(\n relationship: Relationship,\n schema: RelationshipSchema,\n relationshipsBySource: Map<string, Relationship[]>,\n relationshipsByTarget: Map<string, Relationship[]>\n ): Array<{ layer: string; message: string; elementId?: string }> {\n const errors: Array<{ layer: string; message: string; elementId?: string }> = [];\n const cardinality = schema.cardinality;\n\n // Count OTHER relationships for this predicate from source and to target\n // (excluding the current relationship to get accurate violation detection)\n const sourceRelCount =\n relationshipsBySource\n .get(relationship.source)\n ?.filter(\n (r) =>\n r.predicate === relationship.predicate &&\n r.target !== relationship.target // Exclude current relationship\n ).length || 0;\n\n const targetRelCount =\n relationshipsByTarget\n .get(relationship.target)\n ?.filter(\n (r) =>\n r.predicate === relationship.predicate &&\n r.source !== relationship.source // Exclude current relationship\n ).length || 0;\n\n // Validate based on cardinality type\n switch (cardinality) {\n case \"one-to-one\":\n // Source can have at most 1 such relationship\n if (sourceRelCount > 0) {\n errors.push({\n layer: relationship.layer,\n elementId: relationship.source,\n message: `Cardinality violation: '${relationship.predicate}' has cardinality 'one-to-one', but source '${relationship.source}' already has this relationship`,\n });\n }\n // Target can be involved in at most 1 such relationship\n if (targetRelCount > 0) {\n errors.push({\n layer: relationship.layer,\n elementId: relationship.target,\n message: `Cardinality violation: '${relationship.predicate}' has cardinality 'one-to-one', but target '${relationship.target}' is already involved in this relationship`,\n });\n }\n break;\n\n case \"one-to-many\":\n // Standard ER one-to-many: the \"one\" is at the target end.\n // Each target can receive at most 1 incoming relationship of this type.\n // The source is unconstrained \u2014 it may have many outgoing of this type\n // to different targets.\n if (targetRelCount > 0) {\n errors.push({\n layer: relationship.layer,\n elementId: relationship.target,\n message: `Cardinality violation: '${relationship.predicate}' has cardinality 'one-to-many', but target '${relationship.target}' already has this relationship`,\n });\n }\n break;\n\n case \"many-to-one\":\n // Standard ER many-to-one: the \"one\" is at the source end.\n // Each source can be involved in at most 1 relationship of this type\n // (it maps to exactly one target). Multiple sources CAN share the same\n // target \u2014 fan-in is allowed and expected for shared services.\n if (sourceRelCount > 0) {\n errors.push({\n layer: relationship.layer,\n elementId: relationship.source,\n message: `Cardinality violation: '${relationship.predicate}' has cardinality 'many-to-one', but source '${relationship.source}' already has this relationship`,\n });\n }\n break;\n\n case \"many-to-many\":\n // No cardinality constraints\n break;\n }\n\n return errors;\n }\n\n /**\n * Validate relationship attributes against schema\n */\n private validateRelationshipAttributes(\n relationship: Relationship,\n schema: RelationshipSchema\n ): Array<{ layer: string; message: string; elementId?: string }> {\n const errors: Array<{ layer: string; message: string; elementId?: string }> = [];\n\n if (!schema.attributes || !relationship.properties) {\n return errors;\n }\n\n // Simple validation: check that properties match schema keys\n for (const key of Object.keys(relationship.properties)) {\n if (!(key in schema.attributes)) {\n errors.push({\n layer: relationship.layer,\n elementId: relationship.source,\n message: `Relationship property '${key}' not defined in schema for predicate '${relationship.predicate}'`,\n });\n }\n }\n\n return errors;\n }\n\n /**\n * Find relationship schema ID by source/dest spec node IDs and predicate\n *\n * Matches relationship schemas by:\n * 1. Predicate (must match exactly)\n * 2. Spec node ID pair (source and destination must match exactly)\n *\n * Spec node IDs are in the format \"layer.type\" (e.g., \"application.applicationservice\")\n * and uniquely identify an element type across the model.\n *\n * Supports both intra-layer (motivation -> motivation) and cross-layer\n * (motivation -> business) relationships.\n */\n private findRelationshipSchemaKeyBySpecId(\n sourceSpecId: string,\n destSpecId: string,\n predicate: string\n ): string | null {\n // Search for matching schema\n for (const [schemaId, schema] of this.relationshipSchemas.entries()) {\n // First filter: predicate must match exactly\n if (schema.predicate !== predicate) {\n continue;\n }\n\n // Second filter: spec node ID pair must match exactly\n // This directly compares canonical forms like \"application.applicationservice\"\n if (\n schema.source_spec_node_id === sourceSpecId &&\n schema.destination_spec_node_id === destSpecId\n ) {\n return schemaId;\n }\n }\n\n return null;\n }\n\n /**\n * Group relationships by source or target element\n */\n private groupRelationshipsBySourceTarget(\n relationships: Relationship[],\n by: \"source\" | \"target\"\n ): Map<string, Relationship[]> {\n const grouped = new Map<string, Relationship[]>();\n\n for (const rel of relationships) {\n const key = by === \"source\" ? rel.source : rel.target;\n if (!grouped.has(key)) {\n grouped.set(key, []);\n }\n grouped.get(key)!.push(rel);\n }\n\n return grouped;\n }\n\n /**\n * Find an element in the model by ID\n *\n * Performs element lookup using Layer.getElement(), which supports multiple lookup strategies:\n * - Direct lookup by graph key (path)\n * - UUID lookup\n * - Semantic ID lookup (for legacy format elements converted to UUID)\n * - Element ID lookup\n *\n * This comprehensive lookup ensures elements are found regardless of how they were stored.\n */\n private findElementInModel(model: Model, elementId: string) {\n for (const layer of model.layers.values()) {\n const element = layer.getElement(elementId);\n if (element) {\n return element;\n }\n }\n return null;\n }\n\n private extractLayerFromElementId(elementId: string): string | null {\n const parts = elementId.split(\".\");\n if (parts.length < 2) {\n return null;\n }\n return parts[0];\n }\n\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * Relationship Schema Validation with Cardinality Enforcement\n *\n * Validates relationships against their spec relationship schemas, including:\n * - Schema compliance (source/destination node types)\n * - Cardinality constraints (one-to-one, one-to-many, many-to-one, many-to-many)\n * - Predicate validity\n * - Relationship attributes against schema\n */\n\nimport { ValidationResult } from \"./types.js\";\nimport type { Model } from \"../core/model.js\";\nimport type { Relationship } from \"../core/relationships.js\";\nimport { fileURLToPath } from \"url\";\nimport path from \"path\";\nimport { readFile } from \"../utils/file-io.js\";\nimport { existsSync } from \"fs\";\nimport { startActiveSpan } from \"../telemetry/index.js\";\n\ninterface ManifestDistFile {\n layers: Array<{ id: string }>;\n}\n\ninterface LayerDistFile {\n relationshipSchemas: Record<string, {\n id: string;\n source_spec_node_id: string;\n source_layer: string;\n destination_spec_node_id: string;\n destination_layer: string;\n predicate: string;\n cardinality: \"one-to-one\" | \"one-to-many\" | \"many-to-one\" | \"many-to-many\";\n strength: string;\n required?: boolean;\n attributes?: Record<string, unknown>;\n }>;\n}\n\ndeclare const TELEMETRY_ENABLED: boolean | undefined;\nconst isTelemetryEnabled = typeof TELEMETRY_ENABLED !== \"undefined\" ? TELEMETRY_ENABLED : false;\n\nexport interface RelationshipSchema {\n id: string;\n source_spec_node_id: string;\n source_layer: string;\n destination_spec_node_id: string;\n destination_layer: string;\n predicate: string;\n cardinality: \"one-to-one\" | \"one-to-many\" | \"many-to-one\" | \"many-to-many\";\n strength: string;\n required?: boolean;\n attributes?: Record<string, unknown>;\n}\n\n/**\n * Validator for relationship schema compliance and cardinality constraints\n *\n * Validates that relationships:\n * 1. Conform to their spec relationship schemas\n * 2. Respect cardinality constraints (e.g., one-to-one means max 1 relationship of that type)\n * 3. Reference valid predicates\n * 4. Have valid source/destination element types\n */\nexport class RelationshipValidator {\n private schemasDir: string;\n private relationshipSchemas: Map<string, RelationshipSchema> = new Map();\n\n constructor() {\n const __dirname = path.dirname(fileURLToPath(import.meta.url));\n this.schemasDir = path.join(__dirname, \"..\", \"schemas\", \"bundled\");\n }\n\n /**\n * Initialize and load relationship schemas\n */\n async initialize(): Promise<void> {\n await this.loadRelationshipSchemas();\n }\n\n /**\n * Load relationship schemas from compiled bundled spec (manifest.json + per-layer JSON files)\n */\n private async loadRelationshipSchemas(): Promise<void> {\n const manifestPath = path.join(this.schemasDir, \"manifest.json\");\n\n if (!existsSync(manifestPath)) {\n throw new Error(`Bundled manifest not found at ${manifestPath}`);\n }\n\n const manifestContent = await readFile(manifestPath);\n const manifest = JSON.parse(manifestContent) as ManifestDistFile;\n\n for (const { id: layerId } of manifest.layers) {\n const layerPath = path.join(this.schemasDir, `${layerId}.json`);\n if (!existsSync(layerPath)) {\n continue;\n }\n\n try {\n const layerContent = await readFile(layerPath);\n const layerData = JSON.parse(layerContent) as LayerDistFile;\n\n for (const [relId, relSchema] of Object.entries(layerData.relationshipSchemas || {})) {\n this.relationshipSchemas.set(relId, {\n id: relSchema.id || relId,\n source_spec_node_id: relSchema.source_spec_node_id || \"\",\n source_layer: relSchema.source_layer || \"\",\n destination_spec_node_id: relSchema.destination_spec_node_id || \"\",\n destination_layer: relSchema.destination_layer || \"\",\n predicate: relSchema.predicate || \"\",\n cardinality: relSchema.cardinality || \"many-to-many\",\n strength: relSchema.strength || \"medium\",\n required: relSchema.required || false,\n attributes: relSchema.attributes,\n });\n }\n } catch (error: any) {\n throw new Error(`Failed to load relationship schemas for layer '${layerId}': ${error.message}`);\n }\n }\n }\n\n /**\n * Validate relationships in a model with cardinality enforcement\n */\n async validateModel(model: Model): Promise<ValidationResult> {\n return startActiveSpan(\n \"relationship-schema.validate-model\",\n async (span) => {\n // Ensure schemas are loaded\n if (this.relationshipSchemas.size === 0) {\n try {\n await this.initialize();\n } catch (error: any) {\n const result = new ValidationResult();\n result.addError({\n layer: \"system\",\n message: `${error.message} Relationship validation is not available.`,\n });\n return result;\n }\n }\n\n // Verify schemas were loaded. This covers two cases:\n // 1. initialize() succeeded but the manifest has no layers with relationship schemas\n // 2. Edge case where manifest exists but is empty\n // In both cases, relationship validation cannot proceed.\n if (this.relationshipSchemas.size === 0) {\n const result = new ValidationResult();\n result.addError({\n layer: \"system\",\n message: `No relationship schemas loaded from ${path.join(this.schemasDir, \"manifest.json\")}. Relationship validation is not available.`,\n });\n return result;\n }\n\n const result = new ValidationResult();\n const allRelationships = model.relationships.getAll();\n\n if (allRelationships.length === 0) {\n return result;\n }\n\n // Group relationships for cardinality validation\n const relationshipsBySource = this.groupRelationshipsBySourceTarget(\n allRelationships,\n \"source\"\n );\n const relationshipsByTarget = this.groupRelationshipsBySourceTarget(\n allRelationships,\n \"target\"\n );\n\n // Validate each relationship\n for (const relationship of allRelationships) {\n const relErrors = await this.validateRelationship(\n relationship,\n model,\n relationshipsBySource,\n relationshipsByTarget\n );\n\n if (relErrors.length > 0) {\n for (const error of relErrors) {\n result.addError(error);\n }\n }\n }\n\n if (isTelemetryEnabled) {\n span.setAttribute(\"relationship.error_count\", result.errors.length);\n span.setAttribute(\"relationship.warning_count\", result.warnings.length);\n span.setAttribute(\"relationship.validated_count\", allRelationships.length);\n }\n\n return result;\n },\n { \"relationship.layer_count\": model.layers.size }\n );\n }\n\n /**\n * Validate a single relationship\n */\n private async validateRelationship(\n relationship: Relationship,\n model: Model,\n relationshipsBySource: Map<string, Relationship[]>,\n relationshipsByTarget: Map<string, Relationship[]>\n ): Promise<Array<{ layer: string; message: string; elementId?: string }>> {\n const errors: Array<{ layer: string; message: string; elementId?: string }> = [];\n\n // Variables to cache found elements and avoid redundant lookups\n let sourceElement: any = null;\n let targetElement: any = null;\n let bothInFilterAndFound = false;\n\n // Check if relationship crosses layer boundary when --layers filter is active\n if (model.loadedLayerFilter && model.loadedLayerFilter.length > 0) {\n const sourceLayer = this.extractLayerFromElementId(relationship.source);\n const targetLayer = this.extractLayerFromElementId(relationship.target);\n\n // If we can parse layers and both endpoints are within the filter, proceed with full validation.\n // If one or both endpoints are outside the filter, only skip if both endpoints are\n // in unloaded layers. If either endpoint is in a loaded layer, we must validate it.\n if (sourceLayer && targetLayer) {\n const sourceInFilter = model.loadedLayerFilter.includes(sourceLayer);\n const targetInFilter = model.loadedLayerFilter.includes(targetLayer);\n\n // Skip only if BOTH endpoints are in unloaded layers\n if (!sourceInFilter && !targetInFilter) {\n return errors;\n }\n\n // If at least one endpoint is in a loaded layer, check if both elements exist in loaded layers\n // If one is missing from a loaded layer, report error (genuine break)\n if (sourceInFilter) {\n sourceElement = this.findElementInModel(model, relationship.source);\n if (!sourceElement) {\n errors.push({\n layer: relationship.layer,\n elementId: relationship.source,\n message: `Relationship source element '${relationship.source}' not found`,\n });\n return errors;\n }\n }\n\n if (targetInFilter) {\n targetElement = this.findElementInModel(model, relationship.target);\n if (!targetElement) {\n errors.push({\n layer: relationship.layer,\n elementId: relationship.target,\n message: `Relationship target element '${relationship.target}' not found`,\n });\n return errors;\n }\n }\n\n // If one or both endpoints are in unloaded layers, skip full validation\n if (!sourceInFilter || !targetInFilter) {\n return errors;\n }\n\n // Both endpoints are in loaded layers and we've already found them\n bothInFilterAndFound = true;\n }\n // If we can't parse layers (unparseable element IDs), proceed to validation\n // where findElementInModel will fail with a proper error message\n }\n\n // Find source and destination elements only if we haven't already found them\n if (!bothInFilterAndFound) {\n sourceElement = this.findElementInModel(model, relationship.source);\n targetElement = this.findElementInModel(model, relationship.target);\n }\n\n if (!sourceElement) {\n errors.push({\n layer: relationship.layer,\n elementId: relationship.source,\n message: `Relationship source element '${relationship.source}' not found`,\n });\n return errors;\n }\n\n if (!targetElement) {\n errors.push({\n layer: relationship.layer,\n elementId: relationship.target,\n message: `Relationship target element '${relationship.target}' not found`,\n });\n return errors;\n }\n\n // Extract element types AND layers (CRITICAL FIX for cross-layer relationships)\n // The source and target elements may be in different layers\n const sourceLayer = sourceElement.layer_id || sourceElement.layer || relationship.layer;\n const targetLayer = targetElement.layer_id || targetElement.layer || relationship.layer;\n\n // Use spec_node_id if present, otherwise construct from layer and type\n // spec_node_id contains the canonical type (e.g., \"application.applicationservice\")\n // Guard against empty spec_node_id from older CLI versions or manually authored elements\n const sourceSpecId = sourceElement.spec_node_id\n ? sourceElement.spec_node_id\n : `${sourceLayer}.${sourceElement.type}`;\n const targetSpecId = targetElement.spec_node_id\n ? targetElement.spec_node_id\n : `${targetLayer}.${targetElement.type}`;\n\n // Find applicable relationship schema using actual source/target layers and spec node IDs\n // (not relationship.layer for both, which assumes intra-layer relationships)\n const schemaKey = this.findRelationshipSchemaKeyBySpecId(\n sourceSpecId,\n targetSpecId,\n relationship.predicate\n );\n\n if (!schemaKey) {\n // Extract type names from spec_node_id for error message (e.g., \"applicationservice\" from \"application.applicationservice\")\n const sourceType = sourceSpecId.includes(\".\") ? sourceSpecId.split(\".\")[1] : sourceElement.type;\n const targetType = targetSpecId.includes(\".\") ? targetSpecId.split(\".\")[1] : targetElement.type;\n errors.push({\n layer: relationship.layer,\n elementId: relationship.source,\n message:\n `No schema found for relationship: ${relationship.source} --[${relationship.predicate}]--> ${relationship.target} ` +\n `(${sourceLayer}.${sourceType} --[${relationship.predicate}]--> ${targetLayer}.${targetType})`,\n });\n return errors;\n }\n\n const schema = this.relationshipSchemas.get(schemaKey);\n if (!schema) {\n errors.push({\n layer: relationship.layer,\n elementId: relationship.source,\n message: `Failed to load schema for relationship: ${relationship.source} --[${relationship.predicate}]--> ${relationship.target} (${schemaKey})`,\n });\n return errors;\n }\n\n // Validate cardinality constraints\n const cardinalityErrors = this.validateCardinality(\n relationship,\n schema,\n relationshipsBySource,\n relationshipsByTarget\n );\n\n errors.push(...cardinalityErrors);\n\n // Validate relationship attributes if schema specifies them\n if (relationship.properties && schema.attributes) {\n const attrErrors = this.validateRelationshipAttributes(\n relationship,\n schema\n );\n errors.push(...attrErrors);\n }\n\n return errors;\n }\n\n /**\n * Validate cardinality constraints for a relationship\n *\n * Cardinality constraint semantics:\n * - \"one-to-one\": Each source can have at most 1 such relationship, and each target can be involved in at most 1\n * - \"one-to-many\": Each target can receive at most 1 such relationship (target is the \"one\"); source is unconstrained\n * - \"many-to-one\": Each source can be involved in at most 1 such relationship (maps to one target); multiple sources may share the same target\n * - \"many-to-many\": No cardinality constraints\n */\n private validateCardinality(\n relationship: Relationship,\n schema: RelationshipSchema,\n relationshipsBySource: Map<string, Relationship[]>,\n relationshipsByTarget: Map<string, Relationship[]>\n ): Array<{ layer: string; message: string; elementId?: string }> {\n const errors: Array<{ layer: string; message: string; elementId?: string }> = [];\n const cardinality = schema.cardinality;\n\n // Count OTHER relationships for this predicate from source and to target\n // (excluding the current relationship to get accurate violation detection)\n const sourceRelCount =\n relationshipsBySource\n .get(relationship.source)\n ?.filter(\n (r) =>\n r.predicate === relationship.predicate &&\n r.target !== relationship.target // Exclude current relationship\n ).length || 0;\n\n const targetRelCount =\n relationshipsByTarget\n .get(relationship.target)\n ?.filter(\n (r) =>\n r.predicate === relationship.predicate &&\n r.source !== relationship.source // Exclude current relationship\n ).length || 0;\n\n // Validate based on cardinality type\n switch (cardinality) {\n case \"one-to-one\":\n // Source can have at most 1 such relationship\n if (sourceRelCount > 0) {\n errors.push({\n layer: relationship.layer,\n elementId: relationship.source,\n message: `Cardinality violation: '${relationship.predicate}' has cardinality 'one-to-one', but source '${relationship.source}' already has this relationship`,\n });\n }\n // Target can be involved in at most 1 such relationship\n if (targetRelCount > 0) {\n errors.push({\n layer: relationship.layer,\n elementId: relationship.target,\n message: `Cardinality violation: '${relationship.predicate}' has cardinality 'one-to-one', but target '${relationship.target}' is already involved in this relationship`,\n });\n }\n break;\n\n case \"one-to-many\":\n // Standard ER one-to-many: the \"one\" is at the target end.\n // Each target can receive at most 1 incoming relationship of this type.\n // The source is unconstrained \u2014 it may have many outgoing of this type\n // to different targets.\n if (targetRelCount > 0) {\n errors.push({\n layer: relationship.layer,\n elementId: relationship.target,\n message: `Cardinality violation: '${relationship.predicate}' has cardinality 'one-to-many', but target '${relationship.target}' already has this relationship`,\n });\n }\n break;\n\n case \"many-to-one\":\n // Standard ER many-to-one: the \"one\" is at the source end.\n // Each source can be involved in at most 1 relationship of this type\n // (it maps to exactly one target). Multiple sources CAN share the same\n // target \u2014 fan-in is allowed and expected for shared services.\n if (sourceRelCount > 0) {\n errors.push({\n layer: relationship.layer,\n elementId: relationship.source,\n message: `Cardinality violation: '${relationship.predicate}' has cardinality 'many-to-one', but source '${relationship.source}' already has this relationship`,\n });\n }\n break;\n\n case \"many-to-many\":\n // No cardinality constraints\n break;\n }\n\n return errors;\n }\n\n /**\n * Validate relationship attributes against schema\n */\n private validateRelationshipAttributes(\n relationship: Relationship,\n schema: RelationshipSchema\n ): Array<{ layer: string; message: string; elementId?: string }> {\n const errors: Array<{ layer: string; message: string; elementId?: string }> = [];\n\n if (!schema.attributes || !relationship.properties) {\n return errors;\n }\n\n // Simple validation: check that properties match schema keys\n for (const key of Object.keys(relationship.properties)) {\n if (!(key in schema.attributes)) {\n errors.push({\n layer: relationship.layer,\n elementId: relationship.source,\n message: `Relationship property '${key}' on ${relationship.source} --[${relationship.predicate}]--> ${relationship.target} not defined in schema for predicate '${relationship.predicate}'`,\n });\n }\n }\n\n return errors;\n }\n\n /**\n * Find relationship schema ID by source/dest spec node IDs and predicate\n *\n * Matches relationship schemas by:\n * 1. Predicate (must match exactly)\n * 2. Spec node ID pair (source and destination must match exactly)\n *\n * Spec node IDs are in the format \"layer.type\" (e.g., \"application.applicationservice\")\n * and uniquely identify an element type across the model.\n *\n * Supports both intra-layer (motivation -> motivation) and cross-layer\n * (motivation -> business) relationships.\n */\n private findRelationshipSchemaKeyBySpecId(\n sourceSpecId: string,\n destSpecId: string,\n predicate: string\n ): string | null {\n // Search for matching schema\n for (const [schemaId, schema] of this.relationshipSchemas.entries()) {\n // First filter: predicate must match exactly\n if (schema.predicate !== predicate) {\n continue;\n }\n\n // Second filter: spec node ID pair must match exactly\n // This directly compares canonical forms like \"application.applicationservice\"\n if (\n schema.source_spec_node_id === sourceSpecId &&\n schema.destination_spec_node_id === destSpecId\n ) {\n return schemaId;\n }\n }\n\n return null;\n }\n\n /**\n * Group relationships by source or target element\n */\n private groupRelationshipsBySourceTarget(\n relationships: Relationship[],\n by: \"source\" | \"target\"\n ): Map<string, Relationship[]> {\n const grouped = new Map<string, Relationship[]>();\n\n for (const rel of relationships) {\n const key = by === \"source\" ? rel.source : rel.target;\n if (!grouped.has(key)) {\n grouped.set(key, []);\n }\n grouped.get(key)!.push(rel);\n }\n\n return grouped;\n }\n\n /**\n * Find an element in the model by ID\n *\n * Performs element lookup using Layer.getElement(), which supports multiple lookup strategies:\n * - Direct lookup by graph key (path)\n * - UUID lookup\n * - Semantic ID lookup (for legacy format elements converted to UUID)\n * - Element ID lookup\n *\n * This comprehensive lookup ensures elements are found regardless of how they were stored.\n */\n private findElementInModel(model: Model, elementId: string) {\n for (const layer of model.layers.values()) {\n const element = layer.getElement(elementId);\n if (element) {\n return element;\n }\n }\n return null;\n }\n\n private extractLayerFromElementId(elementId: string): string | null {\n const parts = elementId.split(\".\");\n if (parts.length < 2) {\n return null;\n }\n return parts[0];\n }\n\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["result", "sourceLayer", "targetLayer"]
|
|
7
7
|
}
|
|
@@ -7,6 +7,13 @@ import { readFile } from "../utils/file-io.js";
|
|
|
7
7
|
import { startActiveSpan } from "../telemetry/index.js";
|
|
8
8
|
import { existsSync } from "fs";
|
|
9
9
|
import { validateSpecNode, validateSpecNodeRelationship, validateSourceReference, validateAttributeSpec } from "../generated/compiled-validators.js";
|
|
10
|
+
import { isValidLayer } from "../generated/layer-registry.js";
|
|
11
|
+
import { getNodeTypesForLayer, isValidNodeType } from "../generated/node-types.js";
|
|
12
|
+
import { findSimilar } from "../utils/errors.js";
|
|
13
|
+
function suggestNodeTypes(layer, type) {
|
|
14
|
+
const names = getNodeTypesForLayer(layer).map((t) => t.type), lower = type.toLowerCase(), containing = lower.length >= 4 ? names.filter((name) => name !== lower && (name.includes(lower) || lower.includes(name))) : [];
|
|
15
|
+
return Array.from(/* @__PURE__ */ new Set([...findSimilar(type, names, 3), ...containing])).slice(0, 3);
|
|
16
|
+
}
|
|
10
17
|
const isTelemetryEnabled = !1;
|
|
11
18
|
class SchemaValidator {
|
|
12
19
|
// Static shared AJV instance to avoid schema registry conflicts
|
|
@@ -19,9 +26,11 @@ class SchemaValidator {
|
|
|
19
26
|
static registeredSchemaIds = /* @__PURE__ */ new Set();
|
|
20
27
|
// Layer-level cache: layer ID → nodeSchemas map (avoids re-reading layer dist files)
|
|
21
28
|
static layerCache = /* @__PURE__ */ new Map();
|
|
29
|
+
// Compiled node schemas, keyed by layer.type. Shared like the AJV instance they were compiled
|
|
30
|
+
// in: registeredSchemaIds is static, so a per-instance cache would leave every later instance
|
|
31
|
+
// with a registered id and no validator, and it would silently fall back to the base schema.
|
|
32
|
+
static compiledSchemas = /* @__PURE__ */ new Map();
|
|
22
33
|
ajv;
|
|
23
|
-
compiledSchemas = /* @__PURE__ */ new Map();
|
|
24
|
-
// Key: layer.type
|
|
25
34
|
bundledDir;
|
|
26
35
|
constructor() {
|
|
27
36
|
SchemaValidator.sharedAjv || (SchemaValidator.sharedAjv = new Ajv({
|
|
@@ -54,7 +63,7 @@ class SchemaValidator {
|
|
|
54
63
|
async ensureBaseSchemaLoaded() {
|
|
55
64
|
if (SchemaValidator.baseSchemaLoadedInSharedInstance)
|
|
56
65
|
return;
|
|
57
|
-
|
|
66
|
+
SchemaValidator.compiledSchemas.set("spec-node", validateSpecNode), SchemaValidator.compiledSchemas.set("spec-node-relationship", validateSpecNodeRelationship), SchemaValidator.compiledSchemas.set("source-references", validateSourceReference), SchemaValidator.compiledSchemas.set("attribute-spec", validateAttributeSpec);
|
|
58
67
|
const baseJsonPath = path.join(this.bundledDir, "base.json");
|
|
59
68
|
try {
|
|
60
69
|
const baseContent = await readFile(baseJsonPath), baseData = JSON.parse(baseContent);
|
|
@@ -95,8 +104,8 @@ class SchemaValidator {
|
|
|
95
104
|
*/
|
|
96
105
|
async loadSpecNodeSchema(layer, type) {
|
|
97
106
|
const cacheKey = `${layer}.${type}`;
|
|
98
|
-
if (
|
|
99
|
-
return
|
|
107
|
+
if (SchemaValidator.compiledSchemas.has(cacheKey))
|
|
108
|
+
return SchemaValidator.compiledSchemas.get(cacheKey);
|
|
100
109
|
await this.ensureBaseSchemaLoaded();
|
|
101
110
|
try {
|
|
102
111
|
const nodeSchemas = await this.loadLayerNodeSchemas(layer);
|
|
@@ -104,10 +113,10 @@ class SchemaValidator {
|
|
|
104
113
|
return null;
|
|
105
114
|
const schema = nodeSchemas[type], schemaId = schema.$id || `urn:dr:spec:node:${layer}.${type}`;
|
|
106
115
|
if (SchemaValidator.registeredSchemaIds.has(schemaId))
|
|
107
|
-
return
|
|
116
|
+
return SchemaValidator.compiledSchemas.get(cacheKey) || null;
|
|
108
117
|
try {
|
|
109
118
|
const validate = this.ajv.compile(schema);
|
|
110
|
-
return
|
|
119
|
+
return SchemaValidator.compiledSchemas.set(cacheKey, validate), SchemaValidator.registeredSchemaIds.add(schemaId), validate;
|
|
111
120
|
} catch (registrationError) {
|
|
112
121
|
if (SchemaValidator.getErrorMessage(registrationError).includes("already exists"))
|
|
113
122
|
return SchemaValidator.registeredSchemaIds.add(schemaId), null;
|
|
@@ -162,6 +171,14 @@ class SchemaValidator {
|
|
|
162
171
|
});
|
|
163
172
|
return result;
|
|
164
173
|
}
|
|
174
|
+
if (isValidLayer(layerName) && !isValidNodeType(layerName, element.type)) {
|
|
175
|
+
const elementId = element.path || element.id, similar = suggestNodeTypes(layerName, element.type);
|
|
176
|
+
return result.validated = !0, result.hasErrors = !0, result.errors.push({
|
|
177
|
+
message: `Element '${elementId}' has type '${element.type}', which the spec does not define for the ${layerName} layer` + (similar.length > 0 ? ` (did you mean ${similar.map((s) => `'${s}'`).join(" or ")}?)` : ""),
|
|
178
|
+
location: "/type",
|
|
179
|
+
fixSuggestion: similar.length > 0 ? `Change the element's type to '${similar[0]}', or remove the element` : `Change the element's type to one the ${layerName} layer defines, or remove the element`
|
|
180
|
+
}), result;
|
|
181
|
+
}
|
|
165
182
|
const validate = await this.loadSpecNodeSchema(layerName, element.type);
|
|
166
183
|
if (!validate)
|
|
167
184
|
return result.validated = !0, result;
|
|
@@ -239,10 +256,11 @@ class SchemaValidator {
|
|
|
239
256
|
* });
|
|
240
257
|
*/
|
|
241
258
|
static reset() {
|
|
242
|
-
SchemaValidator.sharedAjv = null, SchemaValidator.baseSchemaLoadedInSharedInstance = !1, SchemaValidator.registeredSchemaIds.clear(), SchemaValidator.layerCache.clear();
|
|
259
|
+
SchemaValidator.sharedAjv = null, SchemaValidator.baseSchemaLoadedInSharedInstance = !1, SchemaValidator.registeredSchemaIds.clear(), SchemaValidator.layerCache.clear(), SchemaValidator.compiledSchemas.clear();
|
|
243
260
|
}
|
|
244
261
|
}
|
|
245
262
|
export {
|
|
246
|
-
SchemaValidator
|
|
263
|
+
SchemaValidator,
|
|
264
|
+
suggestNodeTypes
|
|
247
265
|
};
|
|
248
266
|
//# sourceMappingURL=schema-validator.js.map
|