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.
@@ -0,0 +1,574 @@
1
+ 'use strict';
2
+
3
+ // ARGO schema-bundle resolution and ontology construction.
4
+ //
5
+ // The toolchain is no longer hard-wired to a single modeling language. A
6
+ // "schema bundle" is a directory that carries the graph contract:
7
+ //
8
+ // <bundle>/SystemArchitecture.schema.json (required) JSON Schema of the graph
9
+ // <bundle>/argob.config.json (optional) bundle descriptor:
10
+ // language, enum locations, guide,
11
+ // rules file, invariant switches
12
+ // <bundle>/argob-rules.json (optional) ontology rules data
13
+ // (type metadata, relationship
14
+ // categories, endpoint matrix)
15
+ // <bundle>/ARGOB.md (optional) human-readable guide
16
+ //
17
+ // Resolution precedence (first bundle that has SystemArchitecture.schema.json):
18
+ // 1. ARGO_SCHEMA_DIR — explicit override (tests / hosts)
19
+ // 2. <workspaceRoot>/.argo/schema — the repository's own schema
20
+ // 3. <argoRoot>/schema — the default ArgoBument schema
21
+ //
22
+ // The default bundle keeps the historical ArchiMate 3.2 + ARGO behaviour. A
23
+ // custom bundle may define its own element/relationship types and, optionally,
24
+ // its own endpoint rules and invariants.
25
+
26
+ const fs = require('node:fs');
27
+ const path = require('node:path');
28
+
29
+ const { getArgoRoot } = require('./argo-paths.js');
30
+
31
+ const SCHEMA_BASENAME = 'SystemArchitecture.schema.json';
32
+ const CONFIG_BASENAME = 'argob.config.json';
33
+ const RULES_BASENAME = 'argob-rules.json';
34
+ const GUIDE_BASENAME = 'ARGOB.md';
35
+ const DEFAULT_GUIDE_BASENAME = 'archimate3.2.md';
36
+ const DEFAULT_LANGUAGE = 'ArchiMate 3.2';
37
+ const DEFAULT_ROOT_VIEW_NAME = 'SystemArchitecture';
38
+ const DEFAULT_MAX_ELEMENTS_PER_VIEW = 15;
39
+ const DEFAULT_ACTOR_ELEMENT_TYPE = 'Business Actor';
40
+
41
+ // Which relationship types express a delivery dependency and in which direction.
42
+ // Declared per bundle via argob.config.json "deliveryDependencies"; the default
43
+ // ArgoBument bundle keeps the ArchiMate mapping below (previous behaviour).
44
+ const ARCHIMATE_DELIVERY_DEPENDENCIES = Object.freeze({
45
+ sourceDependsOnTarget: Object.freeze(['Access', 'Assignment', 'Specialization', 'Composition', 'Aggregation']),
46
+ targetDependsOnSource: Object.freeze(['Serving', 'Realization', 'Flow', 'Triggering', 'Influence']),
47
+ });
48
+
49
+ function resolveDeliveryDependencies(config, dialect) {
50
+ const raw = config && typeof config.deliveryDependencies === 'object' && config.deliveryDependencies !== null
51
+ ? config.deliveryDependencies
52
+ : null;
53
+ if (raw) {
54
+ return {
55
+ sourceDependsOnTarget: Array.isArray(raw.sourceDependsOnTarget) ? raw.sourceDependsOnTarget.slice() : [],
56
+ targetDependsOnSource: Array.isArray(raw.targetDependsOnSource) ? raw.targetDependsOnSource.slice() : [],
57
+ };
58
+ }
59
+ if (dialect === 'archimate-class-matrix') {
60
+ return {
61
+ sourceDependsOnTarget: ARCHIMATE_DELIVERY_DEPENDENCIES.sourceDependsOnTarget.slice(),
62
+ targetDependsOnSource: ARCHIMATE_DELIVERY_DEPENDENCIES.targetDependsOnSource.slice(),
63
+ };
64
+ }
65
+ // Custom schema with no declared dependency semantics: no delivery ordering
66
+ // (tests still run, in declaration order).
67
+ return { sourceDependsOnTarget: [], targetDependsOnSource: [] };
68
+ }
69
+ const ELEMENT_ENUM_KEYS = ['archimateElementType', 'elementType', 'elementTypes'];
70
+ const RELATIONSHIP_ENUM_KEYS = ['archimateRelationshipType', 'relationshipType', 'relationshipTypes'];
71
+
72
+ function toPosix(value) {
73
+ return String(value == null ? '' : value).replace(/\\/g, '/');
74
+ }
75
+
76
+ function readJsonFile(absolutePath) {
77
+ return JSON.parse(fs.readFileSync(absolutePath, 'utf8'));
78
+ }
79
+
80
+ function isFile(p) {
81
+ try {
82
+ return fs.statSync(p).isFile();
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ function relativeLabel(workspaceRoot, absolutePath) {
89
+ const workspaceRelative = path.relative(workspaceRoot, absolutePath);
90
+ if (workspaceRelative && !workspaceRelative.startsWith('..') && !path.isAbsolute(workspaceRelative)) {
91
+ return toPosix(workspaceRelative);
92
+ }
93
+ const argoRelative = path.relative(getArgoRoot(), absolutePath);
94
+ if (argoRelative && !argoRelative.startsWith('..') && !path.isAbsolute(argoRelative)) {
95
+ return `<argo>/${toPosix(argoRelative)}`;
96
+ }
97
+ return toPosix(absolutePath);
98
+ }
99
+
100
+ function buildBundle(kind, dir, workspaceRoot) {
101
+ const schemaFile = path.join(dir, SCHEMA_BASENAME);
102
+ const schema = readJsonFile(schemaFile);
103
+
104
+ const configFile = path.join(dir, CONFIG_BASENAME);
105
+ const fileConfig = isFile(configFile) ? readJsonFile(configFile) : {};
106
+ const inlineConfig = schema && typeof schema['x-argob'] === 'object' && schema['x-argob'] !== null
107
+ ? schema['x-argob']
108
+ : {};
109
+ const config = { ...inlineConfig, ...fileConfig };
110
+
111
+ const rulesFile = typeof config.rules === 'string' && config.rules.trim() !== ''
112
+ ? path.resolve(dir, config.rules)
113
+ : path.join(dir, RULES_BASENAME);
114
+ const rules = isFile(rulesFile) ? readJsonFile(rulesFile) : null;
115
+
116
+ let guidePath = null;
117
+ if (typeof config.guide === 'string' && config.guide.trim() !== '') {
118
+ const candidate = path.resolve(dir, config.guide);
119
+ if (isFile(candidate)) {
120
+ guidePath = candidate;
121
+ }
122
+ } else if (kind === 'default') {
123
+ const candidate = path.join(dir, DEFAULT_GUIDE_BASENAME);
124
+ if (isFile(candidate)) {
125
+ guidePath = candidate;
126
+ }
127
+ } else {
128
+ const candidate = path.join(dir, GUIDE_BASENAME);
129
+ if (isFile(candidate)) {
130
+ guidePath = candidate;
131
+ }
132
+ }
133
+
134
+ return {
135
+ kind,
136
+ dir,
137
+ relativeDir: relativeLabel(workspaceRoot, dir),
138
+ schema: {
139
+ absolutePath: schemaFile,
140
+ relativePath: relativeLabel(workspaceRoot, schemaFile),
141
+ },
142
+ schemaDocument: schema,
143
+ config: {
144
+ filePath: isFile(configFile) ? { absolutePath: configFile, relativePath: relativeLabel(workspaceRoot, configFile) } : null,
145
+ ...config,
146
+ },
147
+ rulesPath: rules
148
+ ? { absolutePath: rulesFile, relativePath: relativeLabel(workspaceRoot, rulesFile) }
149
+ : null,
150
+ rules,
151
+ guidePath: guidePath
152
+ ? { absolutePath: guidePath, relativePath: relativeLabel(workspaceRoot, guidePath) }
153
+ : null,
154
+ };
155
+ }
156
+
157
+ function resolveSchemaBundle(workspaceRoot, options = {}) {
158
+ const root = path.resolve(workspaceRoot || process.cwd());
159
+ const candidates = [];
160
+
161
+ const envDir = options.schemaDir || process.env.ARGO_SCHEMA_DIR;
162
+ if (typeof envDir === 'string' && envDir.trim() !== '') {
163
+ candidates.push({ kind: 'override', dir: path.resolve(root, envDir.trim()) });
164
+ }
165
+ candidates.push({ kind: 'workspace', dir: path.join(root, '.argo', 'schema') });
166
+ candidates.push({ kind: 'default', dir: path.join(getArgoRoot(), 'schema') });
167
+
168
+ const seen = new Set();
169
+ for (const candidate of candidates) {
170
+ const key = path.resolve(candidate.dir).toLowerCase();
171
+ if (seen.has(key)) {
172
+ continue;
173
+ }
174
+ seen.add(key);
175
+ if (isFile(path.join(candidate.dir, SCHEMA_BASENAME))) {
176
+ return buildBundle(candidate.kind, candidate.dir, root);
177
+ }
178
+ }
179
+
180
+ throw new Error(
181
+ `Unable to locate '${SCHEMA_BASENAME}'. Checked: ${candidates.map(c => c.dir).join(', ')}`,
182
+ );
183
+ }
184
+
185
+ function resolveEnumFromSchema(schema, keys, configPathKey) {
186
+ if (configPathKey && Array.isArray(configPathKey)) {
187
+ let current = schema;
188
+ for (const segment of configPathKey) {
189
+ if (!current || typeof current !== 'object' || !(segment in current)) {
190
+ current = undefined;
191
+ break;
192
+ }
193
+ current = current[segment];
194
+ }
195
+ if (Array.isArray(current)) {
196
+ return current.slice();
197
+ }
198
+ }
199
+ const defs = schema && typeof schema.$defs === 'object' && schema.$defs !== null ? schema.$defs : {};
200
+ for (const key of keys) {
201
+ const node = defs[key];
202
+ if (node && Array.isArray(node.enum)) {
203
+ return node.enum.slice();
204
+ }
205
+ }
206
+ return [];
207
+ }
208
+
209
+ function resolveTypeEnums(bundle) {
210
+ const config = bundle.config || {};
211
+ const schema = bundle.schemaDocument || {};
212
+
213
+ let elementTypes = Array.isArray(config.elementTypes) ? config.elementTypes.slice() : [];
214
+ if (elementTypes.length === 0) {
215
+ elementTypes = resolveEnumFromSchema(schema, ELEMENT_ENUM_KEYS, config.elementTypeEnumPath);
216
+ }
217
+
218
+ let relationshipTypes = Array.isArray(config.relationshipTypes) ? config.relationshipTypes.slice() : [];
219
+ if (relationshipTypes.length === 0) {
220
+ relationshipTypes = resolveEnumFromSchema(schema, RELATIONSHIP_ENUM_KEYS, config.relationshipTypeEnumPath);
221
+ }
222
+
223
+ return { elementTypes, relationshipTypes };
224
+ }
225
+
226
+ function resolveInvariants(config, defaults) {
227
+ const raw = config && typeof config.invariants === 'object' && config.invariants !== null
228
+ ? config.invariants
229
+ : {};
230
+ const invariants = { ...defaults };
231
+ if (typeof raw.statementGrammar === 'boolean') {
232
+ invariants.statementGrammar = raw.statementGrammar;
233
+ }
234
+ if (typeof raw.endpointMatrix === 'boolean') {
235
+ invariants.endpointMatrix = raw.endpointMatrix;
236
+ }
237
+ if (raw.rootViewName === null) {
238
+ invariants.rootViewName = null;
239
+ } else if (typeof raw.rootViewName === 'string' && raw.rootViewName.trim() !== '') {
240
+ invariants.rootViewName = raw.rootViewName.trim();
241
+ }
242
+ if (raw.maxElementsPerView === null) {
243
+ invariants.maxElementsPerView = null;
244
+ } else if (Number.isInteger(raw.maxElementsPerView) && raw.maxElementsPerView >= 0) {
245
+ invariants.maxElementsPerView = raw.maxElementsPerView;
246
+ }
247
+ return invariants;
248
+ }
249
+
250
+ function resolveActorElementType(config) {
251
+ // The Actor element type is part of the bundle contract: the ARGO workflow
252
+ // identifies the agent through an Actor element (wakeup gate). A bundle may
253
+ // rename it, or set it to null to declare that the schema has no actor concept
254
+ // (actor identification is then skipped). Absent => the default 'Business Actor'.
255
+ if (config && Object.prototype.hasOwnProperty.call(config, 'actorElementType')) {
256
+ return config.actorElementType;
257
+ }
258
+ return DEFAULT_ACTOR_ELEMENT_TYPE;
259
+ }
260
+
261
+ function validateBundle({ language, dialect, elementTypes, relationshipTypes, actorElementType, matrix, deliveryDependencies }) {
262
+ const errors = [];
263
+ const elementTypeList = Array.isArray(elementTypes) ? elementTypes : [];
264
+ const relationshipTypeList = Array.isArray(relationshipTypes) ? relationshipTypes : [];
265
+
266
+ if (elementTypeList.length === 0) {
267
+ errors.push(`schema bundle '${language}' defines no element types`);
268
+ }
269
+ if (relationshipTypeList.length === 0) {
270
+ errors.push(`schema bundle '${language}' defines no relationship types`);
271
+ }
272
+
273
+ if (actorElementType === null) {
274
+ // Explicit opt-out: the schema has no actor/agent identity concept.
275
+ } else if (typeof actorElementType === 'string' && actorElementType.trim() !== '') {
276
+ if (!elementTypeList.includes(actorElementType)) {
277
+ errors.push(
278
+ `schema bundle '${language}' declares actorElementType '${actorElementType}' which is not one of its element types; ` +
279
+ `set a valid actorElementType in argob.config.json (one of: ${elementTypeList.join(', ') || '(none)'}) ` +
280
+ `or set "actorElementType": null if the schema has no actor concept`,
281
+ );
282
+ }
283
+ } else {
284
+ errors.push(`schema bundle '${language}' actorElementType must be a non-empty string or null; got ${JSON.stringify(actorElementType)}`);
285
+ }
286
+
287
+ // The endpoint matrix (type-keyed bundles only) must only reference declared types.
288
+ if (dialect !== 'archimate-class-matrix' && matrix && typeof matrix === 'object') {
289
+ for (const [relationshipType, targetsBySource] of Object.entries(matrix)) {
290
+ if (!relationshipTypeList.includes(relationshipType)) {
291
+ errors.push(`relationshipTargetMatrix references unknown relationship type '${relationshipType}'`);
292
+ }
293
+ for (const [sourceType, targetList] of Object.entries(targetsBySource || {})) {
294
+ if (!elementTypeList.includes(sourceType)) {
295
+ errors.push(`relationshipTargetMatrix['${relationshipType}'] references unknown element type '${sourceType}'`);
296
+ }
297
+ for (const targetType of Array.isArray(targetList) ? targetList : []) {
298
+ if (targetType !== '*' && !elementTypeList.includes(targetType)) {
299
+ errors.push(`relationshipTargetMatrix['${relationshipType}']['${sourceType}'] references unknown element type '${targetType}'`);
300
+ }
301
+ }
302
+ }
303
+ }
304
+ }
305
+
306
+ errors.push(...validateDeliveryDependencies(language, relationshipTypeList, deliveryDependencies));
307
+
308
+ return { status: errors.length === 0 ? 'passed' : 'failed', errors };
309
+ }
310
+
311
+ function validateDeliveryDependencies(language, relationshipTypeList, deliveryDependencies) {
312
+ const errors = [];
313
+ if (!deliveryDependencies || typeof deliveryDependencies !== 'object') {
314
+ return errors;
315
+ }
316
+ const relSet = new Set(relationshipTypeList);
317
+ for (const key of ['sourceDependsOnTarget', 'targetDependsOnSource']) {
318
+ for (const type of Array.isArray(deliveryDependencies[key]) ? deliveryDependencies[key] : []) {
319
+ if (!relSet.has(type)) {
320
+ errors.push(`schema bundle '${language}' deliveryDependencies.${key} references unknown relationship type '${type}'`);
321
+ }
322
+ }
323
+ }
324
+ return errors;
325
+ }
326
+
327
+ function buildClassMatrixOntology(bundle) {
328
+ const language = typeof bundle.config.language === 'string' && bundle.config.language.trim() !== ''
329
+ ? bundle.config.language.trim()
330
+ : DEFAULT_LANGUAGE;
331
+ const rules = bundle.rules;
332
+ let elementTypeMetadata;
333
+ let relationshipCategoryByType;
334
+ let isSupportedElementType;
335
+ let isSupportedRelationshipType;
336
+ let getMetadata;
337
+ let getArchiMateClass;
338
+ let validateRelationshipEndpointTypes;
339
+
340
+ if (rules) {
341
+ // Data-driven default: the bundle ships its own rule data (argob-rules.json),
342
+ // so the DEFAULT schema is replaceable file-for-file exactly like a custom one.
343
+ const classByType = rules.archimateClassByElementType || {};
344
+ const classMatrix = rules.relationshipTargetMatrix || {};
345
+ elementTypeMetadata = new Map(Object.entries(rules.elementTypeMetadata || {}));
346
+ relationshipCategoryByType = new Map(Object.entries(rules.relationshipCategoryByType || {}));
347
+ isSupportedElementType = (type) => elementTypeMetadata.has(type);
348
+ isSupportedRelationshipType = (type) => relationshipCategoryByType.has(type);
349
+ getMetadata = (element) => elementTypeMetadata.get(element && element.type) || {};
350
+ getArchiMateClass = (elementOrType) => {
351
+ const type = typeof elementOrType === 'string' ? elementOrType : elementOrType && elementOrType.type;
352
+ return classByType[type];
353
+ };
354
+ validateRelationshipEndpointTypes = (relationship, source, target) => {
355
+ if (!relationship || !source || !target) {
356
+ return [];
357
+ }
358
+ const type = relationship.type;
359
+ if (!relationshipCategoryByType.has(type)) {
360
+ return ['relationships \'' + relationship.id + '\' uses unsupported ArchiMate relationship type \'' + type + '\''];
361
+ }
362
+ const sourceClass = getArchiMateClass(source);
363
+ const targetClass = getArchiMateClass(target);
364
+ if (!sourceClass || !targetClass) {
365
+ return [];
366
+ }
367
+ const allowedTargets = classMatrix[type] && classMatrix[type][sourceClass];
368
+ if (!allowedTargets || !allowedTargets.some((allowed) => allowed === targetClass || allowed === 'ModelConcept')) {
369
+ return ['relationships \'' + relationship.id + '\' violates ArchiMate 3.2 relationship matrix: ' + source.type + ' \'' + source.name + '\' cannot ' + type + ' ' + target.type + ' \'' + target.name + '\''];
370
+ }
371
+ return [];
372
+ };
373
+ } else {
374
+ // Legacy fallback: an installation without argob-rules.json uses the bundled module.
375
+ const mod = require('./archimate32-rules.js');
376
+ elementTypeMetadata = mod.elementTypeMetadata;
377
+ relationshipCategoryByType = mod.relationshipCategoryByType;
378
+ isSupportedElementType = mod.isSupportedElementType;
379
+ isSupportedRelationshipType = mod.isSupportedRelationshipType;
380
+ getMetadata = mod.getMetadata;
381
+ getArchiMateClass = mod.getArchiMateClass;
382
+ validateRelationshipEndpointTypes = mod.validateRelationshipEndpointTypes;
383
+ }
384
+
385
+ const elementTypes = Array.from(elementTypeMetadata.keys());
386
+ const relationshipTypes = Array.from(relationshipCategoryByType.keys());
387
+ const actorElementType = resolveActorElementType(bundle.config);
388
+ const deliveryDependencies = resolveDeliveryDependencies(bundle.config, 'archimate-class-matrix');
389
+ return finalizeOntology({
390
+ kind: bundle.kind,
391
+ dialect: 'archimate-class-matrix',
392
+ language,
393
+ elementTypeErrorLabel: 'ArchiMate',
394
+ relationshipTypeErrorLabel: 'ArchiMate',
395
+ matrixErrorLabel: 'ArchiMate 3.2 relationship matrix',
396
+ elementTypes,
397
+ relationshipTypes,
398
+ actorElementType,
399
+ deliveryDependencies,
400
+ bundleValidation: validateBundle({ language, dialect: 'archimate-class-matrix', elementTypes, relationshipTypes, actorElementType, matrix: null, deliveryDependencies }),
401
+ elementTypeMetadata,
402
+ relationshipCategoryByType,
403
+ isSupportedElementType,
404
+ isSupportedRelationshipType,
405
+ getMetadata,
406
+ getArchiMateClass,
407
+ validateRelationshipEndpointTypes,
408
+ invariants: resolveInvariants(bundle.config, {
409
+ statementGrammar: true,
410
+ endpointMatrix: true,
411
+ rootViewName: DEFAULT_ROOT_VIEW_NAME,
412
+ maxElementsPerView: DEFAULT_MAX_ELEMENTS_PER_VIEW,
413
+ }),
414
+ });
415
+ }
416
+
417
+ function buildTypeMatrixOntology(bundle) {
418
+ const config = bundle.config || {};
419
+ const language = typeof config.language === 'string' && config.language.trim() !== ''
420
+ ? config.language.trim()
421
+ : 'Custom Ontology';
422
+ const enums = resolveTypeEnums(bundle);
423
+ const rules = bundle.rules || {};
424
+ const rawMetadata = rules.elementTypeMetadata && typeof rules.elementTypeMetadata === 'object'
425
+ ? rules.elementTypeMetadata
426
+ : {};
427
+ const rawCategories = rules.relationshipCategoryByType && typeof rules.relationshipCategoryByType === 'object'
428
+ ? rules.relationshipCategoryByType
429
+ : {};
430
+ const matrix = rules.relationshipTargetMatrix && typeof rules.relationshipTargetMatrix === 'object'
431
+ ? rules.relationshipTargetMatrix
432
+ : null;
433
+
434
+ const elementTypeMetadata = new Map();
435
+ for (const type of enums.elementTypes) {
436
+ elementTypeMetadata.set(type, rawMetadata[type] || { layer: null, aspect: null });
437
+ }
438
+ for (const [type, meta] of Object.entries(rawMetadata)) {
439
+ if (!elementTypeMetadata.has(type)) {
440
+ elementTypeMetadata.set(type, meta);
441
+ }
442
+ }
443
+
444
+ const relationshipCategoryByType = new Map();
445
+ for (const type of enums.relationshipTypes) {
446
+ relationshipCategoryByType.set(type, rawCategories[type] || 'Custom');
447
+ }
448
+ for (const [type, category] of Object.entries(rawCategories)) {
449
+ if (!relationshipCategoryByType.has(type)) {
450
+ relationshipCategoryByType.set(type, category);
451
+ }
452
+ }
453
+
454
+ const matrixErrorLabel = `${language} relationship matrix`;
455
+ const actorElementType = resolveActorElementType(config);
456
+ const elementTypes = Array.from(elementTypeMetadata.keys());
457
+ const relationshipTypes = Array.from(relationshipCategoryByType.keys());
458
+ const deliveryDependencies = resolveDeliveryDependencies(config, 'type-matrix');
459
+ const getArchiMateClass = (elementOrType) => {
460
+ const type = typeof elementOrType === 'string' ? elementOrType : elementOrType && elementOrType.type;
461
+ return type;
462
+ };
463
+
464
+ function validateRelationshipEndpointTypes(relationship, source, target) {
465
+ if (!relationship || !source || !target) {
466
+ return [];
467
+ }
468
+ const type = relationship.type;
469
+ if (!relationshipCategoryByType.has(type)) {
470
+ return [`relationships '${relationship.id}' uses unsupported ${language} relationship type '${type}'`];
471
+ }
472
+ if (!matrix) {
473
+ return [];
474
+ }
475
+ const allowedTargets = matrix[type] && matrix[type][source.type];
476
+ if (!allowedTargets) {
477
+ return [];
478
+ }
479
+ const ok = allowedTargets.some((allowed) => allowed === target.type || allowed === '*');
480
+ if (!ok) {
481
+ return [`relationships '${relationship.id}' violates ${matrixErrorLabel}: ${source.type} '${source.name}' cannot ${type} ${target.type} '${target.name}'`];
482
+ }
483
+ return [];
484
+ }
485
+
486
+ return finalizeOntology({
487
+ kind: bundle.kind,
488
+ dialect: 'type-matrix',
489
+ language,
490
+ elementTypeErrorLabel: language,
491
+ relationshipTypeErrorLabel: language,
492
+ matrixErrorLabel,
493
+ elementTypes,
494
+ relationshipTypes,
495
+ actorElementType,
496
+ deliveryDependencies,
497
+ bundleValidation: validateBundle({ language, dialect: 'type-matrix', elementTypes, relationshipTypes, actorElementType, matrix, deliveryDependencies }),
498
+ elementTypeMetadata,
499
+ relationshipCategoryByType,
500
+ isSupportedElementType: (type) => elementTypeMetadata.has(type),
501
+ isSupportedRelationshipType: (type) => relationshipCategoryByType.has(type),
502
+ getMetadata: (element) => elementTypeMetadata.get(element && element.type) || {},
503
+ getArchiMateClass,
504
+ validateRelationshipEndpointTypes,
505
+ invariants: resolveInvariants(config, {
506
+ statementGrammar: true,
507
+ endpointMatrix: Boolean(matrix),
508
+ rootViewName: DEFAULT_ROOT_VIEW_NAME,
509
+ maxElementsPerView: DEFAULT_MAX_ELEMENTS_PER_VIEW,
510
+ }),
511
+ });
512
+ }
513
+
514
+ function finalizeOntology(ontology) {
515
+ const elementById = new Map();
516
+ function auditRelationshipEndpointTypes(document, relationshipIds) {
517
+ const errors = [];
518
+ const idSet = Array.isArray(relationshipIds) && relationshipIds.length > 0
519
+ ? new Set(relationshipIds)
520
+ : undefined;
521
+ for (const element of (document && document.elements) || []) {
522
+ elementById.set(element.id, element);
523
+ }
524
+ for (const relationship of (document && document.relationships) || []) {
525
+ if (idSet && !idSet.has(relationship.id)) {
526
+ continue;
527
+ }
528
+ const source = elementById.get(relationship.source_id);
529
+ const target = elementById.get(relationship.target_id);
530
+ errors.push(...ontology.validateRelationshipEndpointTypes(relationship, source, target));
531
+ }
532
+ return errors;
533
+ }
534
+ return {
535
+ ...ontology,
536
+ auditRelationshipEndpointTypes,
537
+ };
538
+ }
539
+
540
+ const ontologyCache = new Map();
541
+
542
+ function buildOntology(bundle) {
543
+ const rules = bundle.rules;
544
+ if (rules && rules.dialect === 'archimate-class-matrix') {
545
+ return buildClassMatrixOntology(bundle);
546
+ }
547
+ if (rules) {
548
+ return buildTypeMatrixOntology(bundle);
549
+ }
550
+ // No rules file: the default bundle falls back to the bundled module; a custom
551
+ // bundle without rules is validated permissively (types from schema enums).
552
+ return bundle.kind === 'default' ? buildClassMatrixOntology(bundle) : buildTypeMatrixOntology(bundle);
553
+ }
554
+
555
+ function loadSchemaBundleAndOntology(workspaceRoot, options = {}) {
556
+ const bundle = resolveSchemaBundle(workspaceRoot, options);
557
+ const cacheKey = `${bundle.kind}:${path.resolve(bundle.dir).toLowerCase()}`;
558
+ if (ontologyCache.has(cacheKey)) {
559
+ return { bundle, ontology: ontologyCache.get(cacheKey) };
560
+ }
561
+ const ontology = buildOntology(bundle);
562
+ ontologyCache.set(cacheKey, ontology);
563
+ return { bundle, ontology };
564
+ }
565
+
566
+ module.exports = {
567
+ SCHEMA_BASENAME,
568
+ CONFIG_BASENAME,
569
+ RULES_BASENAME,
570
+ resolveSchemaBundle,
571
+ resolveTypeEnums,
572
+ buildOntology,
573
+ loadSchemaBundleAndOntology,
574
+ };
@@ -56,6 +56,20 @@ async function runHarnessReport({ checkOnly, workspaceRoot, includeBootstrap })
56
56
 
57
57
  try {
58
58
  report.harnessEnvironment = loadRepositoryArgoEnvironment(workspaceRoot);
59
+ try {
60
+ const { bundle, ontology } = require('./argob-schema.js').loadSchemaBundleAndOntology(workspaceRoot);
61
+ report.schemaBundle = {
62
+ kind: bundle.kind,
63
+ language: ontology.language,
64
+ dialect: ontology.dialect,
65
+ dir: bundle.relativeDir,
66
+ guide: bundle.guidePath ? bundle.guidePath.relativePath : null,
67
+ actorElementType: ontology.actorElementType,
68
+ bundleValidation: ontology.bundleValidation ? ontology.bundleValidation.status : null,
69
+ };
70
+ } catch (schemaError) {
71
+ report.schemaBundle = { kind: 'unknown', error: String(schemaError && schemaError.message ? schemaError.message : schemaError) };
72
+ }
59
73
  if (includeBootstrap) {
60
74
  report.workspaceBootstrap = await ensureWorkspaceBootstrap({ checkOnly, workspaceRoot });
61
75
  }