@documentation-robotics/cli 0.1.13 → 0.2.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 +38 -0
- package/dist/analyzers/verify-engine.js +22 -19
- package/dist/analyzers/verify-engine.js.map +2 -2
- package/dist/cli.js +101 -27
- 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 +122 -0
- package/dist/coding-agents/default-permissions.js.map +7 -0
- package/dist/commands/add.js +10 -6
- package/dist/commands/add.js.map +2 -2
- package/dist/commands/analyzer.js +18 -10
- 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.js +238 -145
- package/dist/commands/changeset.js.map +2 -2
- package/dist/commands/delete.js +45 -18
- 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 +657 -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 +107 -0
- package/dist/commands/mcp.js.map +7 -0
- package/dist/commands/relationship.js +53 -30
- 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 +19 -6
- package/dist/commands/update.js.map +2 -2
- 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/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/farm-manifest.js +134 -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 +159 -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 +55 -1
- package/dist/core/migration-registry.js.map +2 -2
- package/dist/core/model.js +177 -16
- package/dist/core/model.js.map +3 -3
- package/dist/core/virtual-projection.js +1 -0
- package/dist/core/virtual-projection.js.map +2 -2
- package/dist/generated/node-types.js +1 -1
- package/dist/generated/node-types.js.map +2 -2
- package/dist/generated/relationship-index.js +45 -9
- package/dist/generated/relationship-index.js.map +2 -2
- package/dist/integrations/claude_code/docs/MCP_SERVER.md +55 -0
- package/dist/integrations/claude_code/docs/README.md +4 -0
- package/dist/mcp/api-key-manager.js +98 -0
- package/dist/mcp/api-key-manager.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 +44 -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-list.js +33 -0
- package/dist/mcp/tools/changeset-list.js.map +7 -0
- package/dist/mcp/tools/changeset-show.js +39 -0
- package/dist/mcp/tools/changeset-show.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 +47 -0
- package/dist/mcp/tools/index.js.map +7 -0
- package/dist/mcp/tools/model-add.js +106 -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 +88 -0
- package/dist/mcp/tools/model-search.js.map +7 -0
- package/dist/mcp/tools/model-show.js +41 -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 +55 -0
- package/dist/mcp/tools/model-trace.js.map +7 -0
- package/dist/mcp/tools/model-update.js +105 -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/shared.js +103 -0
- package/dist/mcp/tools/shared.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 +2 -2
- package/dist/schemas/bundled/analyzers/manifest.json +2 -2
- package/dist/schemas/bundled/api.json +1 -1
- package/dist/schemas/bundled/apm.json +1 -1
- package/dist/schemas/bundled/application.json +12 -1
- package/dist/schemas/bundled/base.json +1 -1
- package/dist/schemas/bundled/business.json +1 -1
- package/dist/schemas/bundled/data-model.json +38 -2
- package/dist/schemas/bundled/data-store.json +1 -1
- package/dist/schemas/bundled/manifest.json +4 -4
- package/dist/schemas/bundled/motivation.json +1 -1
- package/dist/schemas/bundled/navigation.json +1 -1
- package/dist/schemas/bundled/product.json +1 -1
- package/dist/schemas/bundled/security.json +1 -1
- package/dist/schemas/bundled/technology.json +1 -1
- package/dist/schemas/bundled/testing.json +1 -1
- package/dist/schemas/bundled/ux.json +1 -1
- package/dist/server/server.js +22 -3
- package/dist/server/server.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 +25 -0
- package/dist/utils/ansi-suppressor.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/errors.js +69 -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 +24 -2
- 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 +5 -0
- 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 +262 -0
- package/dist/validators/composed-reference-validator.js.map +7 -0
- package/dist/validators/reference-validator.js +59 -8
- package/dist/validators/reference-validator.js.map +3 -3
- package/dist/validators/relationship-schema-validator.js +30 -2
- package/dist/validators/relationship-schema-validator.js.map +3 -3
- package/package.json +6 -3
|
@@ -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,7 +99,8 @@ 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
|
}
|
|
@@ -70,7 +108,8 @@ class ReferenceValidator {
|
|
|
70
108
|
layer: sourceLayerName,
|
|
71
109
|
elementId,
|
|
72
110
|
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)"
|
|
111
|
+
fixSuggestion: "References must go from higher layers to lower layers (motivation \u2192 testing)",
|
|
112
|
+
category: "reference"
|
|
74
113
|
});
|
|
75
114
|
}
|
|
76
115
|
}
|
|
@@ -84,6 +123,18 @@ class ReferenceValidator {
|
|
|
84
123
|
return layer;
|
|
85
124
|
return elementId.split(separator)[0] || "";
|
|
86
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Find a model in the manifest using case-insensitive lookup
|
|
128
|
+
* Handles the case where parseReferencePath normalizes model names to lowercase,
|
|
129
|
+
* but the manifest may have different casing for the declared model keys.
|
|
130
|
+
*/
|
|
131
|
+
findManifestModel(models, modelName) {
|
|
132
|
+
const lowerModelName = modelName.toLowerCase();
|
|
133
|
+
for (const key of Object.keys(models))
|
|
134
|
+
if (key.toLowerCase() === lowerModelName)
|
|
135
|
+
return !0;
|
|
136
|
+
return !1;
|
|
137
|
+
}
|
|
87
138
|
}
|
|
88
139
|
export {
|
|
89
140
|
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 directional constraint (higher \u2192 lower only)\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 // References must go from higher layers (lower numbers) to lower layers (higher numbers)\n // or within the same layer\n if (sourceLevel > targetLevel) {\n result.addError({\n layer: sourceLayerName,\n elementId,\n message: `Invalid reference direction: ${sourceLayerName} (level ${sourceLevel}) cannot reference ${targetLayerName} (level ${targetLevel})`,\n fixSuggestion:\n \"References must go from higher layers to lower layers (motivation \u2192 testing)\",\n category: \"reference\",\n });\n }\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,
|
|
@@ -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
|
|
@@ -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 // Find source and destination elements\n const sourceElement = this.findElementInModel(model, relationship.source);\n const targetElement = this.findElementInModel(model, relationship.target);\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}\n"],
|
|
5
|
-
"mappings": "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
|
|
6
|
-
"names": ["result"]
|
|
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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",
|
|
6
|
+
"names": ["result", "sourceLayer", "targetLayer"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@documentation-robotics/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CLI for Documentation Robotics",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cli.js",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"dev": "node dist/cli.js",
|
|
25
25
|
"lint": "tsc --noEmit",
|
|
26
26
|
"type-check": "tsc --noEmit",
|
|
27
|
-
"test": "bun test --timeout=30000 --concurrent ./tests/unit/core/ ./tests/unit/validators/ ./tests/unit/telemetry/ ./tests/unit/utils/ ./tests/unit/coding-agents/ ./tests/unit/integrations/ ./tests/unit/server/ ./tests/unit/audit/ ./tests/unit/export/ ./tests/compatibility/ --test-name-pattern '^(?!BaseIntegrationManager)(?!.*trace with three-element dependency chain)(?!.*trace with branching dependencies)' ; bun test --timeout=30000 ./tests/unit/analyzers/ ; bun test --timeout=30000 tests/unit/chat/*.test.ts ./tests/integration/ tests/unit/integrations/base-manager.test.ts --test-name-pattern '^(?!.*trace with three-element dependency chain)(?!.*trace with branching dependencies)'",
|
|
28
|
-
"test:unit": "bun test --timeout=30000 --concurrent ./tests/unit/core/ ./tests/unit/validators/ ./tests/unit/telemetry/ ./tests/unit/utils/ ./tests/unit/coding-agents/ ./tests/unit/integrations/ ./tests/unit/server/ ./tests/unit/scripts/ ./tests/unit/audit/ ./tests/unit/export/ --test-name-pattern '^(?!BaseIntegrationManager)(?!.*trace with three-element dependency chain)(?!.*trace with branching dependencies)' ; bun test --timeout=30000 ./tests/unit/analyzers/ ; bun test --timeout=30000 tests/unit/chat/*.test.ts tests/unit/integrations/base-manager.test.ts",
|
|
27
|
+
"test": "bun test --timeout=30000 --concurrent ./tests/unit/core/ ./tests/unit/validators/ ./tests/unit/telemetry/ ./tests/unit/utils/ ./tests/unit/coding-agents/ ./tests/unit/integrations/ ./tests/unit/server/ ./tests/unit/audit/ ./tests/unit/export/ ./tests/compatibility/ --test-name-pattern '^(?!BaseIntegrationManager)(?!.*trace with three-element dependency chain)(?!.*trace with branching dependencies)' ; bun test --timeout=30000 ./tests/unit/analyzers/ ; bun test --timeout=30000 ./tests/unit/mcp/ ; bun test --timeout=30000 tests/unit/chat/*.test.ts ./tests/integration/ tests/unit/integrations/base-manager.test.ts --test-name-pattern '^(?!.*trace with three-element dependency chain)(?!.*trace with branching dependencies)'",
|
|
28
|
+
"test:unit": "bun test --timeout=30000 --concurrent ./tests/unit/core/ ./tests/unit/validators/ ./tests/unit/telemetry/ ./tests/unit/utils/ ./tests/unit/coding-agents/ ./tests/unit/integrations/ ./tests/unit/server/ ./tests/unit/scripts/ ./tests/unit/audit/ ./tests/unit/export/ --test-name-pattern '^(?!BaseIntegrationManager)(?!.*trace with three-element dependency chain)(?!.*trace with branching dependencies)' ; bun test --timeout=30000 ./tests/unit/analyzers/ ; bun test --timeout=30000 ./tests/unit/mcp/ ; bun test --timeout=30000 tests/unit/chat/*.test.ts tests/unit/integrations/base-manager.test.ts",
|
|
29
29
|
"test:generated": "bun test --timeout=30000 tests/unit/generators/*.test.ts",
|
|
30
30
|
"test:integration": "[ -f dist/cli.js ] || npm run build && bun test --timeout=30000 ./tests/integration/ --test-name-pattern '^(?!.*trace with three-element dependency chain)(?!.*trace with branching dependencies)'",
|
|
31
31
|
"test:validators": "bun test --timeout=30000 --concurrent ./tests/unit/validators/",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"@hono/swagger-ui": "^0.5.3",
|
|
62
62
|
"@hono/zod-openapi": "^1.2.1",
|
|
63
63
|
"@hono/zod-validator": "^0.7.6",
|
|
64
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
64
65
|
"@opentelemetry/api": "^1.9.0",
|
|
65
66
|
"@opentelemetry/api-logs": "^0.57.0",
|
|
66
67
|
"@xmldom/xmldom": "^0.8.11",
|
|
@@ -69,6 +70,7 @@
|
|
|
69
70
|
"ansis": "^3.0.0",
|
|
70
71
|
"chokidar": "^4.0.3",
|
|
71
72
|
"commander": "^12.0.0",
|
|
73
|
+
"express": "^5.2.1",
|
|
72
74
|
"glob": "^11.1.0",
|
|
73
75
|
"graphology": "^0.25.4",
|
|
74
76
|
"graphology-traversal": "^0.3.1",
|
|
@@ -87,6 +89,7 @@
|
|
|
87
89
|
"@opentelemetry/sdk-node": "^0.57.0",
|
|
88
90
|
"@opentelemetry/sdk-trace-base": "^1.30.0",
|
|
89
91
|
"@types/bun": "^1.3.5",
|
|
92
|
+
"@types/express": "^5.0.0",
|
|
90
93
|
"@types/xmldom": "^0.1.34",
|
|
91
94
|
"bun": "^1.3.5",
|
|
92
95
|
"bun-types": "^1.3.5",
|