@documentation-robotics/cli 0.1.13 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -2
- package/dist/analysis/relationship-classifier.js +2 -9
- package/dist/analysis/relationship-classifier.js.map +2 -2
- package/dist/analyzers/call-graph-utils.js +2 -2
- package/dist/analyzers/call-graph-utils.js.map +2 -2
- package/dist/analyzers/cards/build.js +171 -0
- package/dist/analyzers/cards/build.js.map +7 -0
- package/dist/analyzers/cards/classify.js +42 -0
- package/dist/analyzers/cards/classify.js.map +7 -0
- package/dist/analyzers/cards/edge-index.js +44 -0
- package/dist/analyzers/cards/edge-index.js.map +7 -0
- package/dist/analyzers/cards/facts.js +46 -0
- package/dist/analyzers/cards/facts.js.map +7 -0
- package/dist/analyzers/cards/graph.js +42 -0
- package/dist/analyzers/cards/graph.js.map +7 -0
- package/dist/analyzers/cards/index.js +8 -0
- package/dist/analyzers/cards/index.js.map +7 -0
- package/dist/analyzers/cards/tier0.js +18 -0
- package/dist/analyzers/cards/tier0.js.map +7 -0
- package/dist/analyzers/cards/types.js +1 -0
- package/dist/analyzers/cards/types.js.map +7 -0
- package/dist/analyzers/cbm-analyzer.js +195 -171
- package/dist/analyzers/cbm-analyzer.js.map +2 -2
- package/dist/analyzers/cbm-responses.js +336 -0
- package/dist/analyzers/cbm-responses.js.map +7 -0
- package/dist/analyzers/edge-relationships.js +45 -0
- package/dist/analyzers/edge-relationships.js.map +7 -0
- package/dist/analyzers/mapping-loader.js +9 -0
- package/dist/analyzers/mapping-loader.js.map +2 -2
- package/dist/analyzers/tier0-rules.js +153 -0
- package/dist/analyzers/tier0-rules.js.map +7 -0
- package/dist/analyzers/types.js.map +1 -1
- package/dist/analyzers/verify-engine.js +22 -19
- package/dist/analyzers/verify-engine.js.map +2 -2
- 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 +112 -28
- package/dist/cli.js.map +3 -3
- package/dist/coding-agents/agents/claude-code.js +3 -1
- package/dist/coding-agents/agents/claude-code.js.map +2 -2
- package/dist/coding-agents/claude-code-client.js +2 -1
- package/dist/coding-agents/claude-code-client.js.map +2 -2
- package/dist/coding-agents/copilot-client.js +17 -12
- package/dist/coding-agents/copilot-client.js.map +2 -2
- package/dist/coding-agents/default-permissions.js +152 -0
- package/dist/coding-agents/default-permissions.js.map +7 -0
- package/dist/coding-agents/drbot-mcp.js +167 -0
- package/dist/coding-agents/drbot-mcp.js.map +7 -0
- package/dist/commands/add.js +12 -7
- package/dist/commands/add.js.map +2 -2
- package/dist/commands/analyzer.js +134 -23
- package/dist/commands/analyzer.js.map +2 -2
- package/dist/commands/audit.js +6 -6
- package/dist/commands/audit.js.map +2 -2
- package/dist/commands/changeset-verify.js +55 -0
- package/dist/commands/changeset-verify.js.map +7 -0
- package/dist/commands/changeset.js +261 -147
- package/dist/commands/changeset.js.map +2 -2
- package/dist/commands/delete.js +52 -19
- package/dist/commands/delete.js.map +3 -3
- package/dist/commands/element.js +3 -2
- package/dist/commands/element.js.map +2 -2
- package/dist/commands/farm.js +773 -0
- package/dist/commands/farm.js.map +7 -0
- package/dist/commands/init.js +11 -1
- package/dist/commands/init.js.map +2 -2
- package/dist/commands/mcp.js +122 -0
- package/dist/commands/mcp.js.map +7 -0
- package/dist/commands/relationship.js +251 -47
- package/dist/commands/relationship.js.map +2 -2
- package/dist/commands/reports.js +1 -1
- package/dist/commands/reports.js.map +2 -2
- package/dist/commands/update.js +56 -24
- package/dist/commands/update.js.map +2 -2
- package/dist/commands/upgrade.js +99 -11
- package/dist/commands/upgrade.js.map +3 -3
- package/dist/commands/validate.js +41 -3
- package/dist/commands/validate.js.map +2 -2
- package/dist/commands/version.js +2 -2
- package/dist/commands/version.js.map +2 -2
- package/dist/commands/visualize.js +4 -3
- package/dist/commands/visualize.js.map +2 -2
- package/dist/config.js +83 -0
- package/dist/config.js.map +7 -0
- package/dist/core/annotation-store.js +138 -0
- package/dist/core/annotation-store.js.map +7 -0
- package/dist/core/catalog-migration.js +33 -0
- package/dist/core/catalog-migration.js.map +7 -0
- package/dist/core/changeset-analysis.js +421 -0
- package/dist/core/changeset-analysis.js.map +7 -0
- package/dist/core/changeset-base-index.js +153 -0
- package/dist/core/changeset-base-index.js.map +7 -0
- package/dist/core/changeset-brief-delta.js +102 -0
- package/dist/core/changeset-brief-delta.js.map +7 -0
- package/dist/core/changeset-graph.js +281 -0
- package/dist/core/changeset-graph.js.map +7 -0
- package/dist/core/changeset-only-guard.js +32 -0
- package/dist/core/changeset-only-guard.js.map +7 -0
- package/dist/core/changeset-summary.js +68 -0
- package/dist/core/changeset-summary.js.map +7 -0
- package/dist/core/changeset-validator.js +66 -20
- package/dist/core/changeset-validator.js.map +2 -2
- package/dist/core/changeset-verify.js +518 -0
- package/dist/core/changeset-verify.js.map +7 -0
- package/dist/core/changeset.js +56 -9
- package/dist/core/changeset.js.map +2 -2
- package/dist/core/element-type-migration.js +145 -0
- package/dist/core/element-type-migration.js.map +7 -0
- package/dist/core/farm-manifest.js +140 -0
- package/dist/core/farm-manifest.js.map +7 -0
- package/dist/core/farm-sync-engine.js +346 -0
- package/dist/core/farm-sync-engine.js.map +7 -0
- package/dist/core/farm-sync-state.js +167 -0
- package/dist/core/farm-sync-state.js.map +7 -0
- package/dist/core/manifest.js +6 -3
- package/dist/core/manifest.js.map +2 -2
- package/dist/core/migration-registry.js +84 -1
- 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 +257 -44
- package/dist/core/model.js.map +3 -3
- package/dist/core/mutation-handler.js +71 -6
- package/dist/core/mutation-handler.js.map +2 -2
- package/dist/core/relationship-migration-table.js +6639 -0
- package/dist/core/relationship-migration-table.js.map +7 -0
- package/dist/core/relationship-migration.js +272 -0
- package/dist/core/relationship-migration.js.map +7 -0
- package/dist/core/relationship-prune.js +49 -0
- package/dist/core/relationship-prune.js.map +7 -0
- package/dist/core/relationship-staging.js +68 -0
- package/dist/core/relationship-staging.js.map +7 -0
- package/dist/core/relationships.js +26 -4
- package/dist/core/relationships.js.map +2 -2
- package/dist/core/report-data-model.js +7 -13
- package/dist/core/report-data-model.js.map +2 -2
- package/dist/core/staged-changeset-storage.js +64 -17
- package/dist/core/staged-changeset-storage.js.map +2 -2
- package/dist/core/staged-source-reference.js +28 -0
- package/dist/core/staged-source-reference.js.map +7 -0
- package/dist/core/staging-area.js +83 -18
- package/dist/core/staging-area.js.map +2 -2
- package/dist/core/virtual-projection.js +14 -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 +2209 -7727
- package/dist/generated/relationship-index.js.map +2 -2
- package/dist/integrations/claude_code/agents/dr-architect.md +16 -3
- package/dist/integrations/claude_code/agents/dr-extractor.md +1 -1
- package/dist/integrations/claude_code/commands/dr-analyzer.md +565 -0
- package/dist/integrations/claude_code/commands/dr-map.md +13 -0
- package/dist/integrations/claude_code/commands/dr-plan.md +40 -0
- package/dist/integrations/claude_code/commands/dr-relate.md +106 -96
- package/dist/integrations/claude_code/commands/dr-verify.md +45 -2
- package/dist/integrations/claude_code/docs/END_TO_END_WALKTHROUGH.md +14 -0
- package/dist/integrations/claude_code/docs/MCP_SERVER.md +79 -0
- package/dist/integrations/claude_code/docs/QUICK_START.md +37 -8
- package/dist/integrations/claude_code/docs/README.md +52 -1
- package/dist/integrations/claude_code/skills/dr_01_motivation_layer/SKILL.md +93 -72
- package/dist/integrations/claude_code/skills/dr_02_business_layer/SKILL.md +163 -126
- package/dist/integrations/claude_code/skills/dr_03_product_layer/SKILL.md +161 -126
- package/dist/integrations/claude_code/skills/dr_04_security_layer/SKILL.md +153 -133
- package/dist/integrations/claude_code/skills/dr_05_application_layer/SKILL.md +129 -100
- package/dist/integrations/claude_code/skills/dr_06_technology_layer/SKILL.md +111 -94
- package/dist/integrations/claude_code/skills/dr_07_api_layer/SKILL.md +143 -157
- package/dist/integrations/claude_code/skills/dr_08_data_model_layer/SKILL.md +117 -33
- package/dist/integrations/claude_code/skills/dr_09_dataset_layer/SKILL.md +74 -25
- package/dist/integrations/claude_code/skills/dr_10_ux_layer/SKILL.md +88 -12
- package/dist/integrations/claude_code/skills/dr_11_navigation_layer/SKILL.md +83 -24
- package/dist/integrations/claude_code/skills/dr_12_apm_layer/SKILL.md +57 -12
- package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +92 -15
- package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +119 -0
- package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +3 -0
- package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +1 -1
- package/dist/integrations/github_copilot/agents/dr-architect.md +1 -1
- package/dist/integrations/github_copilot/skills/dr_01_motivation_layer/SKILL.md +182 -102
- package/dist/integrations/github_copilot/skills/dr_02_business_layer/SKILL.md +266 -148
- package/dist/integrations/github_copilot/skills/dr_03_product_layer/SKILL.md +547 -86
- package/dist/integrations/github_copilot/skills/dr_04_security_layer/SKILL.md +164 -169
- package/dist/integrations/github_copilot/skills/dr_05_application_layer/SKILL.md +420 -150
- package/dist/integrations/github_copilot/skills/dr_06_technology_layer/SKILL.md +331 -138
- package/dist/integrations/github_copilot/skills/dr_07_api_layer/SKILL.md +265 -201
- package/dist/integrations/github_copilot/skills/dr_08_data_model_layer/SKILL.md +181 -58
- package/dist/integrations/github_copilot/skills/dr_09_dataset_layer/SKILL.md +209 -57
- package/dist/integrations/github_copilot/skills/dr_10_ux_layer/SKILL.md +277 -112
- package/dist/integrations/github_copilot/skills/dr_11_navigation_layer/SKILL.md +219 -70
- package/dist/integrations/github_copilot/skills/dr_12_apm_layer/SKILL.md +189 -105
- package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +222 -102
- package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +119 -0
- package/dist/integrations/github_copilot/skills/dr_changeset_reviewer/SKILL.md +3 -0
- package/dist/integrations/hash-utils.js.map +2 -2
- package/dist/mcp/api-key-manager.js +111 -0
- package/dist/mcp/api-key-manager.js.map +7 -0
- package/dist/mcp/changeset-only.js +34 -0
- package/dist/mcp/changeset-only.js.map +7 -0
- package/dist/mcp/http-transport.js +176 -0
- package/dist/mcp/http-transport.js.map +7 -0
- package/dist/mcp/resource-registry.js +97 -0
- package/dist/mcp/resource-registry.js.map +7 -0
- package/dist/mcp/tool-registry.js +60 -0
- package/dist/mcp/tool-registry.js.map +7 -0
- package/dist/mcp/tools/annotation-create.js +39 -0
- package/dist/mcp/tools/annotation-create.js.map +7 -0
- package/dist/mcp/tools/annotation-delete.js +29 -0
- package/dist/mcp/tools/annotation-delete.js.map +7 -0
- package/dist/mcp/tools/annotation-get.js +29 -0
- package/dist/mcp/tools/annotation-get.js.map +7 -0
- package/dist/mcp/tools/annotation-list.js +24 -0
- package/dist/mcp/tools/annotation-list.js.map +7 -0
- package/dist/mcp/tools/annotation-reply.js +34 -0
- package/dist/mcp/tools/annotation-reply.js.map +7 -0
- package/dist/mcp/tools/annotation-shared.js +10 -0
- package/dist/mcp/tools/annotation-shared.js.map +7 -0
- package/dist/mcp/tools/annotation-update.js +39 -0
- package/dist/mcp/tools/annotation-update.js.map +7 -0
- package/dist/mcp/tools/changeset-analysis-tools.js +78 -0
- package/dist/mcp/tools/changeset-analysis-tools.js.map +7 -0
- package/dist/mcp/tools/changeset-editing.js +184 -0
- package/dist/mcp/tools/changeset-editing.js.map +7 -0
- package/dist/mcp/tools/changeset-lifecycle.js +83 -0
- package/dist/mcp/tools/changeset-lifecycle.js.map +7 -0
- package/dist/mcp/tools/changeset-list.js +33 -0
- package/dist/mcp/tools/changeset-list.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 +55 -0
- package/dist/mcp/tools/changeset-show.js.map +7 -0
- package/dist/mcp/tools/changeset-verify.js +43 -0
- package/dist/mcp/tools/changeset-verify.js.map +7 -0
- package/dist/mcp/tools/chat-launch.js +50 -0
- package/dist/mcp/tools/chat-launch.js.map +7 -0
- package/dist/mcp/tools/chat-status.js +26 -0
- package/dist/mcp/tools/chat-status.js.map +7 -0
- package/dist/mcp/tools/index.js +65 -0
- package/dist/mcp/tools/index.js.map +7 -0
- package/dist/mcp/tools/model-add.js +107 -0
- package/dist/mcp/tools/model-add.js.map +7 -0
- package/dist/mcp/tools/model-delete.js +99 -0
- package/dist/mcp/tools/model-delete.js.map +7 -0
- package/dist/mcp/tools/model-export.js +72 -0
- package/dist/mcp/tools/model-export.js.map +7 -0
- package/dist/mcp/tools/model-info.js +51 -0
- package/dist/mcp/tools/model-info.js.map +7 -0
- package/dist/mcp/tools/model-list.js +53 -0
- package/dist/mcp/tools/model-list.js.map +7 -0
- package/dist/mcp/tools/model-reload.js +34 -0
- package/dist/mcp/tools/model-reload.js.map +7 -0
- package/dist/mcp/tools/model-search.js +102 -0
- package/dist/mcp/tools/model-search.js.map +7 -0
- package/dist/mcp/tools/model-show.js +68 -0
- package/dist/mcp/tools/model-show.js.map +7 -0
- package/dist/mcp/tools/model-stats.js +22 -0
- package/dist/mcp/tools/model-stats.js.map +7 -0
- package/dist/mcp/tools/model-trace.js +118 -0
- package/dist/mcp/tools/model-trace.js.map +7 -0
- package/dist/mcp/tools/model-update.js +121 -0
- package/dist/mcp/tools/model-update.js.map +7 -0
- package/dist/mcp/tools/model-validate.js +78 -0
- package/dist/mcp/tools/model-validate.js.map +7 -0
- package/dist/mcp/tools/relationship-tools.js +98 -0
- package/dist/mcp/tools/relationship-tools.js.map +7 -0
- package/dist/mcp/tools/shared.js +126 -0
- package/dist/mcp/tools/shared.js.map +7 -0
- package/dist/mcp/tools/spec-type.js +90 -0
- package/dist/mcp/tools/spec-type.js.map +7 -0
- package/dist/reports/model-layer-report-generator.js +5 -3
- package/dist/reports/model-layer-report-generator.js.map +2 -2
- package/dist/reports/model-readme-generator.js +116 -0
- package/dist/reports/model-readme-generator.js.map +7 -0
- package/dist/reports/model-report-data.js +37 -1
- package/dist/reports/model-report-data.js.map +2 -2
- package/dist/reports/model-report-orchestrator.js +29 -6
- package/dist/reports/model-report-orchestrator.js.map +2 -2
- package/dist/reports/model-report-utils.js +9 -0
- package/dist/reports/model-report-utils.js.map +7 -0
- package/dist/schemas/bundled/analyzers/cbm.json +1186 -68
- package/dist/schemas/bundled/analyzers/manifest.json +2 -2
- package/dist/schemas/bundled/api.json +1029 -1658
- package/dist/schemas/bundled/apm.json +499 -734
- package/dist/schemas/bundled/application.json +745 -370
- package/dist/schemas/bundled/base.json +3 -220
- package/dist/schemas/bundled/business.json +452 -366
- package/dist/schemas/bundled/data-model.json +403 -383
- package/dist/schemas/bundled/data-store.json +334 -558
- package/dist/schemas/bundled/manifest.json +18 -18
- package/dist/schemas/bundled/motivation.json +349 -368
- package/dist/schemas/bundled/navigation.json +415 -731
- package/dist/schemas/bundled/product.json +325 -159
- package/dist/schemas/bundled/security.json +864 -1232
- package/dist/schemas/bundled/technology.json +574 -703
- package/dist/schemas/bundled/testing.json +718 -1643
- package/dist/schemas/bundled/ux.json +739 -1352
- package/dist/server/schemas.js +8 -0
- package/dist/server/schemas.js.map +2 -2
- package/dist/server/server.js +234 -31
- package/dist/server/server.js.map +2 -2
- package/dist/server-entry.js +2 -1
- package/dist/server-entry.js.map +2 -2
- package/dist/telemetry/config.js +2 -40
- package/dist/telemetry/config.js.map +2 -2
- package/dist/telemetry/index.js +1 -1
- package/dist/types/index.js.map +2 -2
- package/dist/utils/ansi-suppressor.js +23 -0
- package/dist/utils/ansi-suppressor.js.map +7 -0
- package/dist/utils/attribute-patch.js +41 -0
- package/dist/utils/attribute-patch.js.map +7 -0
- package/dist/utils/color-utils.js +34 -0
- package/dist/utils/color-utils.js.map +7 -0
- package/dist/utils/element-reference-shape.js +15 -1
- package/dist/utils/element-reference-shape.js.map +3 -3
- package/dist/utils/error-hints.js +29 -0
- package/dist/utils/error-hints.js.map +7 -0
- package/dist/utils/errors.js +75 -10
- package/dist/utils/errors.js.map +2 -2
- package/dist/utils/globals.js +17 -1
- package/dist/utils/globals.js.map +2 -2
- package/dist/utils/project-paths.js +50 -3
- package/dist/utils/project-paths.js.map +2 -2
- package/dist/utils/reference-path-parser.js +49 -0
- package/dist/utils/reference-path-parser.js.map +7 -0
- package/dist/utils/source-reference.js +17 -10
- package/dist/utils/source-reference.js.map +2 -2
- package/dist/utils/spec-version.js +10 -2
- package/dist/utils/spec-version.js.map +2 -2
- package/dist/validators/composed-reference-validator.js +240 -0
- package/dist/validators/composed-reference-validator.js.map +7 -0
- package/dist/validators/reference-validator.js +57 -13
- package/dist/validators/reference-validator.js.map +3 -3
- package/dist/validators/relationship-schema-validator.js +33 -5
- package/dist/validators/relationship-schema-validator.js.map +3 -3
- package/dist/validators/schema-validator.js +27 -9
- package/dist/validators/schema-validator.js.map +2 -2
- package/dist/validators/types.js.map +1 -1
- package/dist/viewer/README.md +6 -6
- package/dist/viewer/assets/main-CxzcuU_o.js +84 -0
- package/dist/viewer/assets/main-j-7ULYpm.css +1 -0
- package/dist/viewer/index.html +2 -2
- package/dist/viewer/manifest.json +10 -10
- package/dist/viewer/version.json +1 -1
- package/package.json +7 -3
- package/dist/viewer/assets/main-B7AOQXXX.js +0 -81
- package/dist/viewer/assets/main-vdrFrG7q.css +0 -1
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { ValidationResult } from "./types.js";
|
|
2
|
+
import { Validator } from "./validator.js";
|
|
3
|
+
import { parseReferencePath, ReferencePathParseError } from "../utils/reference-path-parser.js";
|
|
4
|
+
import { getErrorMessage } from "../utils/errors.js";
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { parse as parseYAML } from "yaml";
|
|
8
|
+
import { FarmManifest } from "../core/farm-manifest.js";
|
|
9
|
+
import { resolveModelDirectory } from "../utils/project-paths.js";
|
|
10
|
+
class ComposedReferenceValidator {
|
|
11
|
+
modelPathOverrides;
|
|
12
|
+
resolvedModels = /* @__PURE__ */ new Map();
|
|
13
|
+
referencedModels = /* @__PURE__ */ new Set();
|
|
14
|
+
resolutionDiagnostics = new ValidationResult();
|
|
15
|
+
constructor(modelPathOverrides = {}) {
|
|
16
|
+
this.modelPathOverrides = modelPathOverrides;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create a ComposedReferenceValidator from a farm root
|
|
20
|
+
* Automatically builds model-path overrides from the farm's project entries
|
|
21
|
+
* @param farmRoot - Path to the farm root directory
|
|
22
|
+
* @returns ComposedReferenceValidator instance configured for the farm
|
|
23
|
+
*/
|
|
24
|
+
static async fromFarm(farmRoot) {
|
|
25
|
+
const farmYamlPath = path.join(farmRoot, "farm.yaml"), manifest = await FarmManifest.load(farmYamlPath), modelPathOverrides = {};
|
|
26
|
+
for (const project of manifest.getAllProjects()) {
|
|
27
|
+
const modelFullPath = path.join(farmRoot, project.model);
|
|
28
|
+
modelPathOverrides[project.name] = modelFullPath;
|
|
29
|
+
}
|
|
30
|
+
return new ComposedReferenceValidator(modelPathOverrides);
|
|
31
|
+
}
|
|
32
|
+
async validateModel(model) {
|
|
33
|
+
const result = await new Validator().validateModel(model), declaredModels = /* @__PURE__ */ new Map();
|
|
34
|
+
for (const modelName of Object.keys(model.manifest.models || {}))
|
|
35
|
+
declaredModels.set(modelName.toLowerCase(), modelName);
|
|
36
|
+
this.referencedModels.clear(), this.resolvedModels.clear(), this.resolutionDiagnostics = new ValidationResult(), this.collectReferencedModels(model);
|
|
37
|
+
for (const declaredName of declaredModels.values())
|
|
38
|
+
await this.resolveExternalModel(declaredName);
|
|
39
|
+
result.merge(this.resolutionDiagnostics);
|
|
40
|
+
for (const [lowerModelName, declaredName] of declaredModels) {
|
|
41
|
+
const isReferenced = this.referencedModels.has(lowerModelName);
|
|
42
|
+
if (!this.resolvedModels.has(declaredName))
|
|
43
|
+
isReferenced ? result.addError({
|
|
44
|
+
layer: "manifest",
|
|
45
|
+
message: `External model '${declaredName}' is referenced in qualified paths but could not be resolved from the configured paths`,
|
|
46
|
+
category: "reference",
|
|
47
|
+
fixSuggestion: `Ensure the model exists on disk, or use '--model-path ${declaredName}=/path/to/project-root' to specify its location`
|
|
48
|
+
}) : result.addWarning({
|
|
49
|
+
layer: "manifest",
|
|
50
|
+
message: `External model '${declaredName}' is declared but not referenced in any qualified paths and could not be found on disk`,
|
|
51
|
+
category: "reference",
|
|
52
|
+
fixSuggestion: `Remove the declaration if not needed, or use '--model-path ${declaredName}=/path/to/project-root' to specify its location`
|
|
53
|
+
});
|
|
54
|
+
else if (isReferenced) {
|
|
55
|
+
const validationResult = this.validateQualifiedReferencesAgainstModel(model, declaredName);
|
|
56
|
+
result.merge(validationResult, `[Composed/${declaredName}]`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
collectReferencedModels(model) {
|
|
62
|
+
for (const [, layer] of model.layers)
|
|
63
|
+
for (const element of layer.listElements())
|
|
64
|
+
for (const ref of element.references || [])
|
|
65
|
+
if (ref.target.startsWith("@"))
|
|
66
|
+
try {
|
|
67
|
+
const parsed = parseReferencePath(ref.target);
|
|
68
|
+
parsed.modelName && this.referencedModels.add(parsed.modelName.toLowerCase());
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error instanceof ReferencePathParseError)
|
|
71
|
+
continue;
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async resolveExternalModel(modelName) {
|
|
76
|
+
let modelPath = this.modelPathOverrides[modelName];
|
|
77
|
+
if (!modelPath) {
|
|
78
|
+
const lowerModelName = modelName.toLowerCase();
|
|
79
|
+
for (const [key, overridePath] of Object.entries(this.modelPathOverrides))
|
|
80
|
+
if (key.toLowerCase() === lowerModelName) {
|
|
81
|
+
modelPath = overridePath;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (modelPath)
|
|
86
|
+
try {
|
|
87
|
+
let rootStats;
|
|
88
|
+
try {
|
|
89
|
+
rootStats = await fs.stat(modelPath);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (err?.code !== "ENOENT")
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
if (rootStats && !rootStats.isDirectory()) {
|
|
95
|
+
this.resolutionDiagnostics.addWarning({
|
|
96
|
+
layer: "manifest",
|
|
97
|
+
message: `External model '${modelName}' path is not a directory: ${modelPath}`,
|
|
98
|
+
category: "reference",
|
|
99
|
+
fixSuggestion: `Ensure '--model-path ${modelName}=${modelPath}' points to a project root containing documentation-robotics/model/`
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const resolved = await resolveModelDirectory(modelPath);
|
|
104
|
+
if (!resolved) {
|
|
105
|
+
this.addResolutionDiagnostic(modelName, new Error("Model directory not found"), modelPath, !0);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const modelDir = resolved.modelDir;
|
|
109
|
+
if (!resolved.isDirectory) {
|
|
110
|
+
this.resolutionDiagnostics.addWarning({
|
|
111
|
+
layer: "manifest",
|
|
112
|
+
message: `External model '${modelName}' path is not a directory: ${modelDir}`,
|
|
113
|
+
category: "reference",
|
|
114
|
+
fixSuggestion: `Ensure '--model-path ${modelName}=${modelPath}' points to a project root or model directory`
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const elementIds = /* @__PURE__ */ new Set();
|
|
119
|
+
let entries = [];
|
|
120
|
+
try {
|
|
121
|
+
entries = await fs.readdir(modelDir, { withFileTypes: !0 });
|
|
122
|
+
} catch (error) {
|
|
123
|
+
this.addResolutionDiagnostic(modelName, error, modelPath);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
for (const entry of entries)
|
|
127
|
+
if (entry.isDirectory() && /^\d+_/.test(entry.name)) {
|
|
128
|
+
const layerPath = path.join(modelDir, entry.name);
|
|
129
|
+
await this.collectElementsFromLayer(layerPath, elementIds);
|
|
130
|
+
}
|
|
131
|
+
this.resolvedModels.set(modelName, {
|
|
132
|
+
modelName,
|
|
133
|
+
elementIds
|
|
134
|
+
});
|
|
135
|
+
} catch (error) {
|
|
136
|
+
this.addResolutionDiagnostic(modelName, error, modelPath);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async collectElementsFromLayer(layerPath, elementIds) {
|
|
140
|
+
try {
|
|
141
|
+
const entries = await fs.readdir(layerPath, { withFileTypes: !0 });
|
|
142
|
+
for (const entry of entries)
|
|
143
|
+
if (entry.isFile() && (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml")))
|
|
144
|
+
try {
|
|
145
|
+
const content = await fs.readFile(path.join(layerPath, entry.name), "utf-8");
|
|
146
|
+
this.extractElementPathsFromYAML(content, elementIds, layerPath, entry.name);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
const filePath = path.join(layerPath, entry.name);
|
|
149
|
+
this.resolutionDiagnostics.addWarning({
|
|
150
|
+
layer: "manifest",
|
|
151
|
+
message: `Failed to read YAML file at ${filePath}: ${getErrorMessage(error)}`,
|
|
152
|
+
category: "reference"
|
|
153
|
+
});
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
this.resolutionDiagnostics.addWarning({
|
|
158
|
+
layer: "manifest",
|
|
159
|
+
message: `Failed to read layer directory at ${layerPath}: ${getErrorMessage(error)}`,
|
|
160
|
+
category: "reference"
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
extractElementPathsFromYAML(content, elementIds, layerPath, fileName) {
|
|
165
|
+
try {
|
|
166
|
+
const parsed = parseYAML(content);
|
|
167
|
+
if (!parsed) {
|
|
168
|
+
this.resolutionDiagnostics.addWarning({
|
|
169
|
+
layer: "manifest",
|
|
170
|
+
message: `YAML file at ${path.join(layerPath, fileName)} parsed to empty content`,
|
|
171
|
+
category: "reference"
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const items = [];
|
|
176
|
+
if (parsed && typeof parsed == "object" && "elements" in parsed) {
|
|
177
|
+
const elements = parsed.elements;
|
|
178
|
+
Array.isArray(elements) && items.push(...elements);
|
|
179
|
+
} else if (Array.isArray(parsed))
|
|
180
|
+
items.push(...parsed);
|
|
181
|
+
else if (parsed && typeof parsed == "object" && !Array.isArray(parsed)) {
|
|
182
|
+
const values = Object.values(parsed);
|
|
183
|
+
values.length > 0 && values[0] && typeof values[0] == "object" && ("path" in values[0] || "id" in values[0]) ? items.push(...values) : items.push(parsed);
|
|
184
|
+
}
|
|
185
|
+
for (const item of items)
|
|
186
|
+
if (item && typeof item == "object") {
|
|
187
|
+
const elementPath = item.path || item.id;
|
|
188
|
+
elementPath && typeof elementPath == "string" && elementIds.add(elementPath.trim());
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
this.resolutionDiagnostics.addWarning({
|
|
192
|
+
layer: "manifest",
|
|
193
|
+
message: `Failed to parse YAML file at ${path.join(layerPath, fileName)}: ${getErrorMessage(error)}`,
|
|
194
|
+
category: "reference"
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
validateQualifiedReferencesAgainstModel(model, modelName) {
|
|
199
|
+
const result = new ValidationResult(), resolvedModel = this.resolvedModels.get(modelName);
|
|
200
|
+
if (!resolvedModel)
|
|
201
|
+
return result;
|
|
202
|
+
for (const [sourceLayerName, layer] of model.layers)
|
|
203
|
+
for (const element of layer.listElements()) {
|
|
204
|
+
const elementId = element.path || element.id;
|
|
205
|
+
for (const ref of element.references || [])
|
|
206
|
+
if (ref.target.startsWith("@"))
|
|
207
|
+
try {
|
|
208
|
+
const parsed = parseReferencePath(ref.target);
|
|
209
|
+
if (parsed.modelName?.toLowerCase() !== modelName.toLowerCase())
|
|
210
|
+
continue;
|
|
211
|
+
resolvedModel.elementIds.has(parsed.segment) || result.addError({
|
|
212
|
+
layer: sourceLayerName,
|
|
213
|
+
elementId,
|
|
214
|
+
message: `Broken qualified reference: target '${parsed.segment}' does not exist in external model '${modelName}'`,
|
|
215
|
+
category: "reference",
|
|
216
|
+
fixSuggestion: `Create element '${parsed.segment}' in model '${modelName}' or remove this reference`
|
|
217
|
+
});
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (error instanceof ReferencePathParseError)
|
|
220
|
+
continue;
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
addResolutionDiagnostic(modelName, error, modelPath, isPathError = !1) {
|
|
227
|
+
const errorMsg = getErrorMessage(error), code = error?.code;
|
|
228
|
+
let message = "", fixSuggestion = "";
|
|
229
|
+
code === "EACCES" ? (message = `Permission denied accessing external model '${modelName}' at ${modelPath}. Check file permissions.`, fixSuggestion = `Ensure the model path is readable, or use '--model-path ${modelName}=/path/to/alternative-location' to specify another location`) : code === "EIO" ? (message = `I/O error reading external model '${modelName}' at ${modelPath}. There may be a filesystem or device issue.`, fixSuggestion = `Check the filesystem for errors, or provide an alternative model path via '--model-path ${modelName}=/path/to/alternative-location'`) : code === "ENOENT" || isPathError ? (message = `Could not find external model '${modelName}' \u2014 expected at ${modelPath}/documentation-robotics/model or ${modelPath}/model`, fixSuggestion = `Ensure the model exists at the specified path, or use '--model-path ${modelName}=/path/to/project-root' to specify its location`) : (message = `Failed to resolve external model '${modelName}' at ${modelPath}: ${errorMsg}`, fixSuggestion = `Check the path and filesystem permissions, or use '--model-path ${modelName}=/path/to/project-root' to specify a different location`), this.resolutionDiagnostics.addWarning({
|
|
230
|
+
layer: "manifest",
|
|
231
|
+
message,
|
|
232
|
+
category: "reference",
|
|
233
|
+
fixSuggestion
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
export {
|
|
238
|
+
ComposedReferenceValidator
|
|
239
|
+
};
|
|
240
|
+
//# sourceMappingURL=composed-reference-validator.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 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\";\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
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ValidationResult } from "./types.js";
|
|
2
2
|
import { getLayerByNumber, LAYER_HIERARCHY, getAllLayerIds } from "../generated/layer-registry.js";
|
|
3
|
+
import { parseReferencePath, ReferencePathParseError } from "../utils/reference-path-parser.js";
|
|
3
4
|
class ReferenceValidator {
|
|
4
5
|
// Known layer names from generated registry (including hyphenated ones)
|
|
5
6
|
KNOWN_LAYERS = getAllLayerIds();
|
|
@@ -20,7 +21,7 @@ class ReferenceValidator {
|
|
|
20
21
|
const result = new ValidationResult(), allElementIds = this.collectAllElementIds(model);
|
|
21
22
|
for (const [layerName, layer] of model.layers)
|
|
22
23
|
for (const element of layer.listElements())
|
|
23
|
-
this.validateReferences(element.path || element.id, element.references || [], layerName, allElementIds, result);
|
|
24
|
+
this.validateReferences(element.path || element.id, element.references || [], layerName, allElementIds, model, result);
|
|
24
25
|
return result;
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
@@ -36,24 +37,60 @@ class ReferenceValidator {
|
|
|
36
37
|
/**
|
|
37
38
|
* Validate references for an element
|
|
38
39
|
*/
|
|
39
|
-
validateReferences(elementId, references, sourceLayerName, validIds, result) {
|
|
40
|
+
validateReferences(elementId, references, sourceLayerName, validIds, model, result) {
|
|
40
41
|
for (const ref of references) {
|
|
41
|
-
|
|
42
|
+
let parsedModel, targetSegment = ref.target;
|
|
43
|
+
if (ref.target.startsWith("@")) {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = parseReferencePath(ref.target);
|
|
46
|
+
parsedModel = parsed.modelName, targetSegment = parsed.segment;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error instanceof ReferencePathParseError)
|
|
49
|
+
result.addError({
|
|
50
|
+
layer: sourceLayerName,
|
|
51
|
+
elementId,
|
|
52
|
+
message: `Malformed qualified reference path: ${error.message}`,
|
|
53
|
+
fixSuggestion: "Use format '@{model-name}/{layer}.{type}.{name}' for qualified references or '{layer}.{type}.{name}' for unqualified references",
|
|
54
|
+
category: "reference"
|
|
55
|
+
});
|
|
56
|
+
else
|
|
57
|
+
throw error;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const modelFound = parsedModel && model.manifest.models && this.findManifestModel(model.manifest.models, parsedModel);
|
|
61
|
+
if (parsedModel && !modelFound) {
|
|
62
|
+
result.addError({
|
|
63
|
+
layer: sourceLayerName,
|
|
64
|
+
elementId,
|
|
65
|
+
message: `Unknown external model reference: model '${parsedModel}' is not declared in manifest`,
|
|
66
|
+
fixSuggestion: `Add model '${parsedModel}' to the 'models' section of manifest.yaml`,
|
|
67
|
+
category: "reference"
|
|
68
|
+
});
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
} else if (!validIds.has(ref.target)) {
|
|
72
|
+
if (model.loadedLayerFilter && model.loadedLayerFilter.length > 0) {
|
|
73
|
+
const targetLayerName2 = this.extractLayerFromId(ref.target);
|
|
74
|
+
if (!model.loadedLayerFilter.includes(targetLayerName2))
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
42
77
|
result.addError({
|
|
43
78
|
layer: sourceLayerName,
|
|
44
79
|
elementId,
|
|
45
80
|
message: `Broken reference: target '${ref.target}' does not exist`,
|
|
46
|
-
fixSuggestion: `Remove reference or create element '${ref.target}'
|
|
81
|
+
fixSuggestion: `Remove reference or create element '${ref.target}'`,
|
|
82
|
+
category: "reference"
|
|
47
83
|
});
|
|
48
84
|
continue;
|
|
49
85
|
}
|
|
50
|
-
const targetLayerName = this.extractLayerFromId(
|
|
86
|
+
const targetLayerName = this.extractLayerFromId(targetSegment), sourceLevel = this.LAYER_HIERARCHY[sourceLayerName], targetLevel = this.LAYER_HIERARCHY[targetLayerName];
|
|
51
87
|
if (sourceLevel === void 0) {
|
|
52
88
|
result.addError({
|
|
53
89
|
layer: sourceLayerName,
|
|
54
90
|
elementId,
|
|
55
91
|
message: `Unknown source layer: ${sourceLayerName}`,
|
|
56
|
-
fixSuggestion: `Use one of the valid layers: ${this.KNOWN_LAYERS.join(", ")}
|
|
92
|
+
fixSuggestion: `Use one of the valid layers: ${this.KNOWN_LAYERS.join(", ")}`,
|
|
93
|
+
category: "reference"
|
|
57
94
|
});
|
|
58
95
|
continue;
|
|
59
96
|
}
|
|
@@ -62,16 +99,11 @@ class ReferenceValidator {
|
|
|
62
99
|
layer: sourceLayerName,
|
|
63
100
|
elementId,
|
|
64
101
|
message: `Target element '${ref.target}' has unknown layer: ${targetLayerName}`,
|
|
65
|
-
fixSuggestion: `Check that element ID '${ref.target}' uses a valid layer prefix (e.g., motivation-, business-, api-)
|
|
102
|
+
fixSuggestion: `Check that element ID '${ref.target}' uses a valid layer prefix (e.g., motivation-, business-, api-)`,
|
|
103
|
+
category: "reference"
|
|
66
104
|
});
|
|
67
105
|
continue;
|
|
68
106
|
}
|
|
69
|
-
sourceLevel > targetLevel && result.addError({
|
|
70
|
-
layer: sourceLayerName,
|
|
71
|
-
elementId,
|
|
72
|
-
message: `Invalid reference direction: ${sourceLayerName} (level ${sourceLevel}) cannot reference ${targetLayerName} (level ${targetLevel})`,
|
|
73
|
-
fixSuggestion: "References must go from higher layers to lower layers (motivation \u2192 testing)"
|
|
74
|
-
});
|
|
75
107
|
}
|
|
76
108
|
}
|
|
77
109
|
/**
|
|
@@ -84,6 +116,18 @@ class ReferenceValidator {
|
|
|
84
116
|
return layer;
|
|
85
117
|
return elementId.split(separator)[0] || "";
|
|
86
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Find a model in the manifest using case-insensitive lookup
|
|
121
|
+
* Handles the case where parseReferencePath normalizes model names to lowercase,
|
|
122
|
+
* but the manifest may have different casing for the declared model keys.
|
|
123
|
+
*/
|
|
124
|
+
findManifestModel(models, modelName) {
|
|
125
|
+
const lowerModelName = modelName.toLowerCase();
|
|
126
|
+
for (const key of Object.keys(models))
|
|
127
|
+
if (key.toLowerCase() === lowerModelName)
|
|
128
|
+
return !0;
|
|
129
|
+
return !1;
|
|
130
|
+
}
|
|
87
131
|
}
|
|
88
132
|
export {
|
|
89
133
|
ReferenceValidator
|
|
@@ -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\";\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 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 result: ValidationResult\n ): void {\n for (const ref of references) {\n // Check target
|
|
5
|
-
"mappings": "AAIA,SAAS,wBAAwB;AAEjC,SAAS,kBAAkB,iBAAiB,sBAAsB;
|
|
6
|
-
"names": []
|
|
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
|
+
"names": ["targetLayerName"]
|
|
7
7
|
}
|
|
@@ -88,8 +88,32 @@ class RelationshipValidator {
|
|
|
88
88
|
* Validate a single relationship
|
|
89
89
|
*/
|
|
90
90
|
async validateRelationship(relationship, model, relationshipsBySource, relationshipsByTarget) {
|
|
91
|
-
const errors = []
|
|
92
|
-
|
|
91
|
+
const errors = [];
|
|
92
|
+
let sourceElement = null, targetElement = null, bothInFilterAndFound = !1;
|
|
93
|
+
if (model.loadedLayerFilter && model.loadedLayerFilter.length > 0) {
|
|
94
|
+
const sourceLayer2 = this.extractLayerFromElementId(relationship.source), targetLayer2 = this.extractLayerFromElementId(relationship.target);
|
|
95
|
+
if (sourceLayer2 && targetLayer2) {
|
|
96
|
+
const sourceInFilter = model.loadedLayerFilter.includes(sourceLayer2), targetInFilter = model.loadedLayerFilter.includes(targetLayer2);
|
|
97
|
+
if (!sourceInFilter && !targetInFilter)
|
|
98
|
+
return errors;
|
|
99
|
+
if (sourceInFilter && (sourceElement = this.findElementInModel(model, relationship.source), !sourceElement))
|
|
100
|
+
return errors.push({
|
|
101
|
+
layer: relationship.layer,
|
|
102
|
+
elementId: relationship.source,
|
|
103
|
+
message: `Relationship source element '${relationship.source}' not found`
|
|
104
|
+
}), errors;
|
|
105
|
+
if (targetInFilter && (targetElement = this.findElementInModel(model, relationship.target), !targetElement))
|
|
106
|
+
return errors.push({
|
|
107
|
+
layer: relationship.layer,
|
|
108
|
+
elementId: relationship.target,
|
|
109
|
+
message: `Relationship target element '${relationship.target}' not found`
|
|
110
|
+
}), errors;
|
|
111
|
+
if (!sourceInFilter || !targetInFilter)
|
|
112
|
+
return errors;
|
|
113
|
+
bothInFilterAndFound = !0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (bothInFilterAndFound || (sourceElement = this.findElementInModel(model, relationship.source), targetElement = this.findElementInModel(model, relationship.target)), !sourceElement)
|
|
93
117
|
return errors.push({
|
|
94
118
|
layer: relationship.layer,
|
|
95
119
|
elementId: relationship.source,
|
|
@@ -107,7 +131,7 @@ class RelationshipValidator {
|
|
|
107
131
|
return errors.push({
|
|
108
132
|
layer: relationship.layer,
|
|
109
133
|
elementId: relationship.source,
|
|
110
|
-
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})`
|
|
111
135
|
}), errors;
|
|
112
136
|
}
|
|
113
137
|
const schema = this.relationshipSchemas.get(schemaKey);
|
|
@@ -115,7 +139,7 @@ class RelationshipValidator {
|
|
|
115
139
|
return errors.push({
|
|
116
140
|
layer: relationship.layer,
|
|
117
141
|
elementId: relationship.source,
|
|
118
|
-
message: `Failed to load schema for relationship: ${schemaKey}`
|
|
142
|
+
message: `Failed to load schema for relationship: ${relationship.source} --[${relationship.predicate}]--> ${relationship.target} (${schemaKey})`
|
|
119
143
|
}), errors;
|
|
120
144
|
const cardinalityErrors = this.validateCardinality(relationship, schema, relationshipsBySource, relationshipsByTarget);
|
|
121
145
|
if (errors.push(...cardinalityErrors), relationship.properties && schema.attributes) {
|
|
@@ -183,7 +207,7 @@ class RelationshipValidator {
|
|
|
183
207
|
key in schema.attributes || errors.push({
|
|
184
208
|
layer: relationship.layer,
|
|
185
209
|
elementId: relationship.source,
|
|
186
|
-
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}'`
|
|
187
211
|
});
|
|
188
212
|
return errors;
|
|
189
213
|
}
|
|
@@ -236,6 +260,10 @@ class RelationshipValidator {
|
|
|
236
260
|
}
|
|
237
261
|
return null;
|
|
238
262
|
}
|
|
263
|
+
extractLayerFromElementId(elementId) {
|
|
264
|
+
const parts = elementId.split(".");
|
|
265
|
+
return parts.length < 2 ? null : parts[0];
|
|
266
|
+
}
|
|
239
267
|
}
|
|
240
268
|
export {
|
|
241
269
|
RelationshipValidator
|