archgraph-argo 0.26.2 → 0.27.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.
@@ -1,227 +1,266 @@
1
- // Shared graph-semantics validation for SystemArchitecture.
2
- // Used by both validateSystemArchitecture.js (full validation) and
3
- // systemarchitecture-mcp-server.js (mutation-path validation).
4
- //
5
- // This module eliminates the duplicate validateGraphSemantics implementations.
6
- // All callers get identical core checks; ArchiMate endpoint matrix and view
7
- // element limits are parameterized so the full validator can check everything
8
- // while the mutation path only checks what changed.
9
-
10
- const {
11
- elementTypeMetadata,
12
- relationshipCategoryByType,
13
- validateRelationshipEndpointTypes,
14
- } = require('./archimate32-rules');
15
-
16
- /**
17
- * Core graph-semantics checks that are always identical for all callers.
18
- *
19
- * @param {object} document - parsed SystemArchitecture JSON
20
- * @param {string[]} errors - error accumulator
21
- */
22
- function validateGraphSemantics(document, errors) {
23
- if (!document || typeof document !== 'object') {
24
- return;
25
- }
26
-
27
- const elements = Array.isArray(document.elements) ? document.elements : [];
28
- const relationships = Array.isArray(document.relationships) ? document.relationships : [];
29
- const views = Array.isArray(document.views) ? document.views : [];
30
- const elementById = new Map();
31
- const relationshipById = new Map();
32
-
33
- // --- elements: identity, type, parent ---
34
- for (const element of elements) {
35
- if (!element || typeof element !== 'object') {
36
- continue;
37
- }
38
- if (elementById.has(element.id)) {
39
- errors.push(`elements contains duplicate id '${element.id}'`);
40
- continue;
41
- }
42
- elementById.set(element.id, element);
43
- if (!elementTypeMetadata.has(element.type)) {
44
- errors.push(`elements '${element.id}' uses unsupported ArchiMate element type '${element.type}'`);
45
- }
46
- }
47
-
48
- for (const element of elements) {
49
- if (!element || typeof element !== 'object' || !element.parent) {
50
- continue;
51
- }
52
- if (!elementById.has(element.parent)) {
53
- errors.push(`elements '${element.id}' references missing parent '${element.parent}'`);
54
- }
55
- }
56
-
57
- // --- relationships: identity, type, endpoints, statement ---
58
- for (const relationship of relationships) {
59
- if (!relationship || typeof relationship !== 'object') {
60
- continue;
61
- }
62
- if (relationshipById.has(relationship.id)) {
63
- errors.push(`relationships contains duplicate id '${relationship.id}'`);
64
- continue;
65
- }
66
- relationshipById.set(relationship.id, relationship);
67
- if (!relationshipCategoryByType.has(relationship.type)) {
68
- errors.push(`relationships '${relationship.id}' uses unsupported ArchiMate relationship type '${relationship.type}'`);
69
- }
70
-
71
- const source = elementById.get(relationship.source_id);
72
- if (!source) {
73
- errors.push(`relationships '${relationship.id}' references missing source_id '${relationship.source_id}'`);
74
- } else if (relationship.source_name !== source.name) {
75
- errors.push(`relationships '${relationship.id}' source_name '${relationship.source_name}' does not match element '${relationship.source_id}' name '${source.name}'`);
76
- }
77
-
78
- const target = elementById.get(relationship.target_id);
79
- if (!target) {
80
- errors.push(`relationships '${relationship.id}' references missing target_id '${relationship.target_id}'`);
81
- } else if (relationship.target_name !== target.name) {
82
- errors.push(`relationships '${relationship.id}' target_name '${relationship.target_name}' does not match element '${relationship.target_id}' name '${target.name}'`);
83
- }
84
-
85
- const expectedStatement = source && target
86
- ? `${source.name} --(${relationship.type})--> ${target.name}`
87
- : undefined;
88
- if (expectedStatement && relationship.statement !== expectedStatement) {
89
- errors.push(`relationships '${relationship.id}' statement must be '${expectedStatement}'`);
90
- }
91
- }
92
-
93
- // --- views: topology, membership, endpoint co-occurrence ---
94
- const topLevelViews = views.filter(view => view && typeof view === 'object' && !view.parent_element_id);
95
- if (topLevelViews.length !== 1) {
96
- errors.push(`views must contain exactly one top-level view named 'SystemArchitecture'; found ${topLevelViews.length}`);
97
- } else if (topLevelViews[0].view_name !== 'SystemArchitecture') {
98
- errors.push(`top-level view '${topLevelViews[0].view_id}' view_name must be 'SystemArchitecture'`);
99
- }
100
-
101
- const elementIdsIncludedInViews = new Set();
102
- const relationshipIdsIncludedInViews = new Set();
103
- for (const view of views) {
104
- if (!view || typeof view !== 'object') {
105
- continue;
106
- }
107
- if (!view.parent_element_id && view.view_name !== 'SystemArchitecture') {
108
- errors.push(`views '${view.view_id}' must declare parent_element_id unless it is the top-level SystemArchitecture view`);
109
- }
110
- if (view.parent_element_id) {
111
- const parent = elementById.get(view.parent_element_id);
112
- if (!parent) {
113
- errors.push(`views '${view.view_id}' references missing parent_element_id '${view.parent_element_id}'`);
114
- } else if (view.parent_element_name && view.parent_element_name !== parent.name) {
115
- errors.push(`views '${view.view_id}' parent_element_name '${view.parent_element_name}' does not match element '${view.parent_element_id}' name '${parent.name}'`);
116
- }
117
- }
118
- const includedElementIds = new Set(view.included_elements || []);
119
- if (includedElementIds.size !== (view.included_elements || []).length) {
120
- errors.push(`views '${view.view_id}' must not contain duplicate included_elements`);
121
- }
122
- for (const elementId of view.included_elements || []) {
123
- elementIdsIncludedInViews.add(elementId);
124
- if (!elementById.has(elementId)) {
125
- errors.push(`views '${view.view_id}' references missing included element '${elementId}'`);
126
- }
127
- }
128
- const includedRelationshipIds = new Set(view.included_relationships || []);
129
- if (includedRelationshipIds.size !== (view.included_relationships || []).length) {
130
- errors.push(`views '${view.view_id}' must not contain duplicate included_relationships`);
131
- }
132
- for (const relationshipId of view.included_relationships || []) {
133
- relationshipIdsIncludedInViews.add(relationshipId);
134
- const rel = relationshipById.get(relationshipId);
135
- if (!rel) {
136
- errors.push(`views '${view.view_id}' references missing included relationship '${relationshipId}'`);
137
- continue;
138
- }
139
- if (!includedElementIds.has(rel.source_id)) {
140
- errors.push(`views '${view.view_id}' includes relationship '${relationshipId}' but not source element '${rel.source_id}'`);
141
- }
142
- if (!includedElementIds.has(rel.target_id)) {
143
- errors.push(`views '${view.view_id}' includes relationship '${relationshipId}' but not target element '${rel.target_id}'`);
144
- }
145
- }
146
- }
147
-
148
- for (const element of elements) {
149
- if (element && typeof element === 'object' && !elementIdsIncludedInViews.has(element.id)) {
150
- errors.push(`elements '${element.id}' must be included in at least one view`);
151
- }
152
- }
153
-
154
- for (const relationship of relationships) {
155
- if (relationship && typeof relationship === 'object' && !relationshipIdsIncludedInViews.has(relationship.id)) {
156
- errors.push(`relationships '${relationship.id}' must be included in at least one view`);
157
- }
158
- }
159
- }
160
-
161
- /**
162
- * Validate ArchiMate 3.2 endpoint type matrix for relationships.
163
- *
164
- * @param {object} document - parsed SystemArchitecture JSON
165
- * @param {string[]} errors - error accumulator
166
- * @param {object} [options]
167
- * @param {string[]} [options.touchedRelationshipIds] - if provided, only these
168
- * relationships are checked; if omitted/empty, ALL relationships are checked
169
- */
170
- function validateArchiMateEndpointMatrix(document, errors, options = {}) {
171
- const elementById = new Map(
172
- (document.elements || []).map(element => [element.id, element]),
173
- );
174
- const relationshipIdSet =
175
- Array.isArray(options.touchedRelationshipIds) && options.touchedRelationshipIds.length > 0
176
- ? new Set(options.touchedRelationshipIds)
177
- : undefined;
178
-
179
- for (const relationship of document.relationships || []) {
180
- if (relationshipIdSet && !relationshipIdSet.has(relationship.id)) {
181
- continue;
182
- }
183
- const source = elementById.get(relationship.source_id);
184
- const target = elementById.get(relationship.target_id);
185
- errors.push(...validateRelationshipEndpointTypes(relationship, source, target));
186
- }
187
- }
188
-
189
- /**
190
- * Validate that each view contains at most 15 included_elements.
191
- * included_relationships do not consume this quota.
192
- *
193
- * @param {object} document - parsed SystemArchitecture JSON
194
- * @param {string[]} errors - error accumulator
195
- * @param {object} [options]
196
- * @param {string[]} [options.touchedViewIds] - if provided, only these views
197
- * are checked; if omitted/empty, ALL views are checked
198
- */
199
- function validateViewElementLimits(document, errors, options = {}) {
200
- const touchedViewIdSet =
201
- Array.isArray(options.touchedViewIds) && options.touchedViewIds.length > 0
202
- ? new Set(options.touchedViewIds)
203
- : undefined;
204
- const MAX_INCLUDED_ELEMENTS = 15;
205
-
206
- for (const view of document.views || []) {
207
- if (!view) {
208
- continue;
209
- }
210
- if (touchedViewIdSet && !touchedViewIdSet.has(view.view_id)) {
211
- continue;
212
- }
213
- const elementCount = Array.isArray(view.included_elements) ? view.included_elements.length : 0;
214
- if (elementCount > MAX_INCLUDED_ELEMENTS) {
215
- errors.push(
216
- `views '${view.view_id}' must contain at most ${MAX_INCLUDED_ELEMENTS} elements; found ${elementCount}. ` +
217
- 'Split the content into layered sub-views before adding more elements.',
218
- );
219
- }
220
- }
221
- }
222
-
223
- module.exports = {
224
- validateGraphSemantics,
225
- validateArchiMateEndpointMatrix,
226
- validateViewElementLimits,
227
- };
1
+ // Shared graph-semantics validation for SystemArchitecture.
2
+ // Used by both validateSystemArchitecture.js (full validation) and
3
+ // systemarchitecture-mcp-server.js (mutation-path validation).
4
+ //
5
+ // This module eliminates the duplicate validateGraphSemantics implementations.
6
+ // All callers get identical core checks; the modeling language (element types,
7
+ // relationship types, endpoint matrix, statement grammar, root-view name and
8
+ // view element limit) is supplied by an *ontology* so a repository that ships
9
+ // its own schema under .argo/schema is validated against its own language.
10
+ //
11
+ // The ontology argument is optional: when omitted, the default ArgoBument
12
+ // (ArchiMate 3.2 + ARGO) ontology is used, preserving historical behaviour.
13
+
14
+ const { loadSchemaBundleAndOntology } = require('./argob-schema.js');
15
+
16
+ let defaultOntology = null;
17
+
18
+ function resolveOntology(ontology) {
19
+ if (ontology) {
20
+ return ontology;
21
+ }
22
+ if (!defaultOntology) {
23
+ defaultOntology = loadSchemaBundleAndOntology(process.cwd()).ontology;
24
+ }
25
+ return defaultOntology;
26
+ }
27
+
28
+ /**
29
+ * Core graph-semantics checks that are always identical for all callers.
30
+ *
31
+ * @param {object} document - parsed SystemArchitecture JSON
32
+ * @param {string[]} errors - error accumulator
33
+ * @param {object} [ontology] - resolved modeling language (defaults to ArgoBument)
34
+ */
35
+ function validateGraphSemantics(document, errors, ontology) {
36
+ if (!document || typeof document !== 'object') {
37
+ return;
38
+ }
39
+ const language = resolveOntology(ontology);
40
+ const invariants = language.invariants || {};
41
+ const elementTypeLabel = language.elementTypeErrorLabel || language.language || 'the';
42
+ const relationshipTypeLabel = language.relationshipTypeErrorLabel || language.language || 'the';
43
+ const rootViewName = invariants.rootViewName === undefined ? 'SystemArchitecture' : invariants.rootViewName;
44
+
45
+ const elements = Array.isArray(document.elements) ? document.elements : [];
46
+ const relationships = Array.isArray(document.relationships) ? document.relationships : [];
47
+ const views = Array.isArray(document.views) ? document.views : [];
48
+ const elementById = new Map();
49
+ const relationshipById = new Map();
50
+
51
+ // --- elements: identity, type, parent ---
52
+ for (const element of elements) {
53
+ if (!element || typeof element !== 'object') {
54
+ continue;
55
+ }
56
+ if (elementById.has(element.id)) {
57
+ errors.push(`elements contains duplicate id '${element.id}'`);
58
+ continue;
59
+ }
60
+ elementById.set(element.id, element);
61
+ if (!language.isSupportedElementType(element.type)) {
62
+ errors.push(`elements '${element.id}' uses unsupported ${elementTypeLabel} element type '${element.type}'`);
63
+ }
64
+ }
65
+
66
+ for (const element of elements) {
67
+ if (!element || typeof element !== 'object' || !element.parent) {
68
+ continue;
69
+ }
70
+ if (!elementById.has(element.parent)) {
71
+ errors.push(`elements '${element.id}' references missing parent '${element.parent}'`);
72
+ }
73
+ }
74
+
75
+ // --- relationships: identity, type, endpoints, statement ---
76
+ for (const relationship of relationships) {
77
+ if (!relationship || typeof relationship !== 'object') {
78
+ continue;
79
+ }
80
+ if (relationshipById.has(relationship.id)) {
81
+ errors.push(`relationships contains duplicate id '${relationship.id}'`);
82
+ continue;
83
+ }
84
+ relationshipById.set(relationship.id, relationship);
85
+ if (!language.isSupportedRelationshipType(relationship.type)) {
86
+ errors.push(`relationships '${relationship.id}' uses unsupported ${relationshipTypeLabel} relationship type '${relationship.type}'`);
87
+ }
88
+
89
+ const source = elementById.get(relationship.source_id);
90
+ if (!source) {
91
+ errors.push(`relationships '${relationship.id}' references missing source_id '${relationship.source_id}'`);
92
+ } else if (relationship.source_name !== source.name) {
93
+ errors.push(`relationships '${relationship.id}' source_name '${relationship.source_name}' does not match element '${relationship.source_id}' name '${source.name}'`);
94
+ }
95
+
96
+ const target = elementById.get(relationship.target_id);
97
+ if (!target) {
98
+ errors.push(`relationships '${relationship.id}' references missing target_id '${relationship.target_id}'`);
99
+ } else if (relationship.target_name !== target.name) {
100
+ errors.push(`relationships '${relationship.id}' target_name '${relationship.target_name}' does not match element '${relationship.target_id}' name '${target.name}'`);
101
+ }
102
+
103
+ if (invariants.statementGrammar === false) {
104
+ continue;
105
+ }
106
+ const expectedStatement = source && target
107
+ ? `${source.name} --(${relationship.type})--> ${target.name}`
108
+ : undefined;
109
+ if (expectedStatement && relationship.statement !== expectedStatement) {
110
+ errors.push(`relationships '${relationship.id}' statement must be '${expectedStatement}'`);
111
+ }
112
+ }
113
+
114
+ // --- views: topology, membership, endpoint co-occurrence ---
115
+ const topLevelViews = views.filter(view => view && typeof view === 'object' && !view.parent_element_id);
116
+ if (topLevelViews.length !== 1) {
117
+ errors.push(
118
+ rootViewName
119
+ ? `views must contain exactly one top-level view named '${rootViewName}'; found ${topLevelViews.length}`
120
+ : `views must contain exactly one top-level view; found ${topLevelViews.length}`,
121
+ );
122
+ } else if (rootViewName && topLevelViews[0].view_name !== rootViewName) {
123
+ errors.push(`top-level view '${topLevelViews[0].view_id}' view_name must be '${rootViewName}'`);
124
+ }
125
+
126
+ const elementIdsIncludedInViews = new Set();
127
+ const relationshipIdsIncludedInViews = new Set();
128
+ for (const view of views) {
129
+ if (!view || typeof view !== 'object') {
130
+ continue;
131
+ }
132
+ if (!view.parent_element_id && rootViewName && view.view_name !== rootViewName) {
133
+ errors.push(`views '${view.view_id}' must declare parent_element_id unless it is the top-level ${rootViewName} view`);
134
+ }
135
+ if (view.parent_element_id) {
136
+ const parent = elementById.get(view.parent_element_id);
137
+ if (!parent) {
138
+ errors.push(`views '${view.view_id}' references missing parent_element_id '${view.parent_element_id}'`);
139
+ } else if (view.parent_element_name && view.parent_element_name !== parent.name) {
140
+ errors.push(`views '${view.view_id}' parent_element_name '${view.parent_element_name}' does not match element '${view.parent_element_id}' name '${parent.name}'`);
141
+ }
142
+ }
143
+ const includedElementIds = new Set(view.included_elements || []);
144
+ if (includedElementIds.size !== (view.included_elements || []).length) {
145
+ errors.push(`views '${view.view_id}' must not contain duplicate included_elements`);
146
+ }
147
+ for (const elementId of view.included_elements || []) {
148
+ elementIdsIncludedInViews.add(elementId);
149
+ if (!elementById.has(elementId)) {
150
+ errors.push(`views '${view.view_id}' references missing included element '${elementId}'`);
151
+ }
152
+ }
153
+ const includedRelationshipIds = new Set(view.included_relationships || []);
154
+ if (includedRelationshipIds.size !== (view.included_relationships || []).length) {
155
+ errors.push(`views '${view.view_id}' must not contain duplicate included_relationships`);
156
+ }
157
+ for (const relationshipId of view.included_relationships || []) {
158
+ relationshipIdsIncludedInViews.add(relationshipId);
159
+ const rel = relationshipById.get(relationshipId);
160
+ if (!rel) {
161
+ errors.push(`views '${view.view_id}' references missing included relationship '${relationshipId}'`);
162
+ continue;
163
+ }
164
+ if (!includedElementIds.has(rel.source_id)) {
165
+ errors.push(`views '${view.view_id}' includes relationship '${relationshipId}' but not source element '${rel.source_id}'`);
166
+ }
167
+ if (!includedElementIds.has(rel.target_id)) {
168
+ errors.push(`views '${view.view_id}' includes relationship '${relationshipId}' but not target element '${rel.target_id}'`);
169
+ }
170
+ }
171
+ }
172
+
173
+ for (const element of elements) {
174
+ if (element && typeof element === 'object' && !elementIdsIncludedInViews.has(element.id)) {
175
+ errors.push(`elements '${element.id}' must be included in at least one view`);
176
+ }
177
+ }
178
+
179
+ for (const relationship of relationships) {
180
+ if (relationship && typeof relationship === 'object' && !relationshipIdsIncludedInViews.has(relationship.id)) {
181
+ errors.push(`relationships '${relationship.id}' must be included in at least one view`);
182
+ }
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Validate the modeling language's endpoint-type matrix for relationships.
188
+ * No-op when the ontology disables the endpoint matrix invariant.
189
+ *
190
+ * @param {object} document - parsed SystemArchitecture JSON
191
+ * @param {string[]} errors - error accumulator
192
+ * @param {object} [options]
193
+ * @param {string[]} [options.touchedRelationshipIds] - if provided, only these
194
+ * relationships are checked; if omitted/empty, ALL relationships are checked
195
+ * @param {object} [options.ontology] - resolved modeling language
196
+ */
197
+ function validateArchiMateEndpointMatrix(document, errors, options = {}) {
198
+ const ontology = resolveOntology(options.ontology);
199
+ if (ontology.invariants && ontology.invariants.endpointMatrix === false) {
200
+ return;
201
+ }
202
+ const elementById = new Map(
203
+ (document.elements || []).map(element => [element.id, element]),
204
+ );
205
+ const relationshipIdSet =
206
+ Array.isArray(options.touchedRelationshipIds) && options.touchedRelationshipIds.length > 0
207
+ ? new Set(options.touchedRelationshipIds)
208
+ : undefined;
209
+
210
+ for (const relationship of document.relationships || []) {
211
+ if (relationshipIdSet && !relationshipIdSet.has(relationship.id)) {
212
+ continue;
213
+ }
214
+ const source = elementById.get(relationship.source_id);
215
+ const target = elementById.get(relationship.target_id);
216
+ errors.push(...ontology.validateRelationshipEndpointTypes(relationship, source, target));
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Validate the ontology's per-view element limit (default 15).
222
+ * `maxElementsPerView: null` disables the limit. included_relationships do not
223
+ * consume the quota.
224
+ *
225
+ * @param {object} document - parsed SystemArchitecture JSON
226
+ * @param {string[]} errors - error accumulator
227
+ * @param {object} [options]
228
+ * @param {string[]} [options.touchedViewIds] - if provided, only these views
229
+ * are checked; if omitted/empty, ALL views are checked
230
+ * @param {object} [options.ontology] - resolved modeling language
231
+ */
232
+ function validateViewElementLimits(document, errors, options = {}) {
233
+ const ontology = resolveOntology(options.ontology);
234
+ const maxIncludedElements = ontology.invariants && ontology.invariants.maxElementsPerView !== undefined
235
+ ? ontology.invariants.maxElementsPerView
236
+ : 15;
237
+ if (maxIncludedElements === null) {
238
+ return;
239
+ }
240
+ const touchedViewIdSet =
241
+ Array.isArray(options.touchedViewIds) && options.touchedViewIds.length > 0
242
+ ? new Set(options.touchedViewIds)
243
+ : undefined;
244
+
245
+ for (const view of document.views || []) {
246
+ if (!view) {
247
+ continue;
248
+ }
249
+ if (touchedViewIdSet && !touchedViewIdSet.has(view.view_id)) {
250
+ continue;
251
+ }
252
+ const elementCount = Array.isArray(view.included_elements) ? view.included_elements.length : 0;
253
+ if (elementCount > maxIncludedElements) {
254
+ errors.push(
255
+ `views '${view.view_id}' must contain at most ${maxIncludedElements} elements; found ${elementCount}. ` +
256
+ 'Split the content into layered sub-views before adding more elements.',
257
+ );
258
+ }
259
+ }
260
+ }
261
+
262
+ module.exports = {
263
+ validateGraphSemantics,
264
+ validateArchiMateEndpointMatrix,
265
+ validateViewElementLimits,
266
+ };
@@ -422,13 +422,21 @@ async function writeArchitectureGraph(graphPath, graph) {
422
422
  /**
423
423
  * Dependency direction for delivery:
424
424
  * For element X, its upstream dependencies = elements X needs to be delivered first.
425
- * Mirrors resolveSemanticEdges from systemarchitecture-mcp-server.js.
426
- *
427
- * - Access, Assignment, Specialization, Composition, Aggregation: source depends on target
428
- * - Serving, Realization, Flow, Triggering, Influence: target depends on source
425
+ * The mapping comes from the ACTIVE schema bundle's `deliveryDependencies`
426
+ * (argob.config.json), not hardcoded ArchiMate names, so a custom schema can
427
+ * declare its own relationship types (e.g. "Depends On"). The default ArgoBument
428
+ * bundle declares the ArchiMate mapping.
429
429
  */
430
- const DEPENDENCY_TYPES_SOURCE_DEPENDS_ON_TARGET = new Set(['Access', 'Assignment', 'Specialization', 'Composition', 'Aggregation']);
431
- const DEPENDENCY_TYPES_TARGET_DEPENDS_ON_SOURCE = new Set(['Serving', 'Realization', 'Flow', 'Triggering', 'Influence']);
430
+ const DELIVERY_DEPENDENCIES = (() => {
431
+ try {
432
+ const { loadSchemaBundleAndOntology } = require('./argob-schema.js');
433
+ return loadSchemaBundleAndOntology(repoRoot).ontology.deliveryDependencies;
434
+ } catch {
435
+ return { sourceDependsOnTarget: [], targetDependsOnSource: [] };
436
+ }
437
+ })();
438
+ const DEPENDENCY_TYPES_SOURCE_DEPENDS_ON_TARGET = new Set(DELIVERY_DEPENDENCIES.sourceDependsOnTarget);
439
+ const DEPENDENCY_TYPES_TARGET_DEPENDS_ON_SOURCE = new Set(DELIVERY_DEPENDENCIES.targetDependsOnSource);
432
440
 
433
441
  /**
434
442
  * Resolve upstream dependencies for a single element.