archgraph-argo 0.26.2 → 0.28.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 +127 -57
- package/argo/.env.example +181 -163
- package/argo/plugins/argo-wakeup.js +29 -27
- package/argo/rules/archgraph.instructions.md +189 -187
- package/argo/schema/schema-bundle.config.json +22 -0
- package/argo/schema/schema-bundle.rules.json +12229 -0
- package/argo/scripts/agentSearchDiagnose.js +410 -407
- package/argo/scripts/argo-mcp-server.js +122 -9
- package/argo/scripts/ensureArgoHarnessEnvironment.js +14 -0
- package/argo/scripts/external-graph-query.js +162 -0
- package/argo/scripts/graph-rag/defaultSemanticRetrieval.js +0 -13
- package/argo/scripts/graph-rag/liveEmbeddingProviderConfig.js +9 -61
- package/argo/scripts/graph-semantics.js +363 -227
- package/argo/scripts/runArchitectureTests.js +591 -583
- package/argo/scripts/schema-bundle.js +932 -0
- package/argo/scripts/systemarchitecture-mcp-server.js +758 -110
- package/argo/scripts/validateSystemArchitecture.js +262 -253
- package/argo/scripts/validator-mcp-server.js +1 -1
- package/argo/scripts/workspace-write-guard.js +144 -0
- package/argo/skills/argo-init/SKILL.md +7 -9
- package/argo/skills/ea-human-reconcile/SKILL.md +40 -39
- package/cordis.patch.yml +15 -6
- package/dsh-argo-wakeup/index.js +17 -17
- package/dsh-argo-workspace/index.js +227 -141
- package/install-argo.ps1 +336 -206
- package/package.json +57 -53
|
@@ -0,0 +1,932 @@
|
|
|
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>/schema-bundle.config.json (optional) bundle descriptor:
|
|
10
|
+
// language, enum locations, guide,
|
|
11
|
+
// rules file, invariant switches
|
|
12
|
+
// <bundle>/schema-bundle.rules.json (optional) ontology rules data
|
|
13
|
+
// (type metadata, relationship
|
|
14
|
+
// categories, endpoint matrix)
|
|
15
|
+
// <bundle>/GUIDE.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 ArchiMate 3.2 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 = 'schema-bundle.config.json';
|
|
33
|
+
const RULES_BASENAME = 'schema-bundle.rules.json';
|
|
34
|
+
const GUIDE_BASENAME = 'GUIDE.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 schema-bundle.config.json "deliveryDependencies"; the default
|
|
43
|
+
// ArchiMate 3.2 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 schemaExists = isFile(schemaFile);
|
|
103
|
+
const schema = schemaExists ? readJsonFile(schemaFile) : null;
|
|
104
|
+
|
|
105
|
+
const configFile = path.join(dir, CONFIG_BASENAME);
|
|
106
|
+
const fileConfig = isFile(configFile) ? readJsonFile(configFile) : {};
|
|
107
|
+
const inlineConfig = schema && typeof schema['x-schema-bundle'] === 'object' && schema['x-schema-bundle'] !== null
|
|
108
|
+
? schema['x-schema-bundle']
|
|
109
|
+
: {};
|
|
110
|
+
const config = { ...inlineConfig, ...fileConfig };
|
|
111
|
+
|
|
112
|
+
const rulesFile = typeof config.rules === 'string' && config.rules.trim() !== ''
|
|
113
|
+
? path.resolve(dir, config.rules)
|
|
114
|
+
: path.join(dir, RULES_BASENAME);
|
|
115
|
+
const rules = isFile(rulesFile) ? readJsonFile(rulesFile) : null;
|
|
116
|
+
|
|
117
|
+
let guidePath = null;
|
|
118
|
+
if (typeof config.guide === 'string' && config.guide.trim() !== '') {
|
|
119
|
+
const candidate = path.resolve(dir, config.guide);
|
|
120
|
+
if (isFile(candidate)) {
|
|
121
|
+
guidePath = candidate;
|
|
122
|
+
}
|
|
123
|
+
} else if (kind === 'default') {
|
|
124
|
+
const candidate = path.join(dir, DEFAULT_GUIDE_BASENAME);
|
|
125
|
+
if (isFile(candidate)) {
|
|
126
|
+
guidePath = candidate;
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
const candidate = path.join(dir, GUIDE_BASENAME);
|
|
130
|
+
if (isFile(candidate)) {
|
|
131
|
+
guidePath = candidate;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
kind,
|
|
137
|
+
dir,
|
|
138
|
+
relativeDir: relativeLabel(workspaceRoot, dir),
|
|
139
|
+
schema: schemaExists
|
|
140
|
+
? { absolutePath: schemaFile, relativePath: relativeLabel(workspaceRoot, schemaFile) }
|
|
141
|
+
: null,
|
|
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
|
+
// --- Bundle inheritance (issue #4) -----------------------------------------
|
|
158
|
+
// A bundle may declare `extends` to inherit a base bundle (the built-in default,
|
|
159
|
+
// or another bundle directory) and add/override on top of it instead of forking
|
|
160
|
+
// the whole rules file. `addElementTypes` / `addRelationships` / `overrideMatrix`
|
|
161
|
+
// are the delta; the effective element universe becomes base ∪ add (single
|
|
162
|
+
// source of truth), matrix/metadata merge by key.
|
|
163
|
+
|
|
164
|
+
function bundleConfig(dir) {
|
|
165
|
+
const configFile = path.join(dir, CONFIG_BASENAME);
|
|
166
|
+
if (!isFile(configFile)) {
|
|
167
|
+
return {};
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
return readJsonFile(configFile);
|
|
171
|
+
} catch {
|
|
172
|
+
return {};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function hasExtendsConfig(dir) {
|
|
177
|
+
const config = bundleConfig(dir);
|
|
178
|
+
return typeof config.extends === 'string' && config.extends.trim() !== '';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Symbolic names a bundle declares about itself (`id` + `aliases`) in its config.
|
|
182
|
+
// The framework resolves an `extends` value against these DECLARED names — never a
|
|
183
|
+
// name hardcoded in framework logic — so a bundle's identity/aliases are data.
|
|
184
|
+
function bundleSymbolicNames(dir) {
|
|
185
|
+
const config = bundleConfig(dir);
|
|
186
|
+
const names = new Set();
|
|
187
|
+
if (typeof config.id === 'string' && config.id.trim() !== '') {
|
|
188
|
+
names.add(config.id.trim());
|
|
189
|
+
}
|
|
190
|
+
if (Array.isArray(config.aliases)) {
|
|
191
|
+
for (const alias of config.aliases) {
|
|
192
|
+
if (typeof alias === 'string' && alias.trim() !== '') {
|
|
193
|
+
names.add(alias.trim());
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return names;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isPathLikeExt(value) {
|
|
201
|
+
return value.includes('/') || value.includes('\\') || value.startsWith('.') || value.startsWith('~')
|
|
202
|
+
|| /^[a-zA-Z]:/.test(value);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Bounded, general candidate list for symbolic bundle resolution: the built-in
|
|
206
|
+
// default bundle, any directory a bundle lists in its own `basePaths`, the bundle's
|
|
207
|
+
// own directory, and its immediate sub-directories. The framework matches a name
|
|
208
|
+
// against each candidate's SELF-DECLARED id/aliases (data) — it hardcodes no
|
|
209
|
+
// modeling-language name. A non-matching symbolic name falls back to a path, so
|
|
210
|
+
// bare relative directory references keep working.
|
|
211
|
+
function candidateBaseDirs(childDir, config) {
|
|
212
|
+
const dirs = [];
|
|
213
|
+
const seen = new Set();
|
|
214
|
+
const push = (dir) => {
|
|
215
|
+
if (!dir) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const resolved = path.resolve(dir);
|
|
219
|
+
const key = resolved.toLowerCase();
|
|
220
|
+
if (!seen.has(key)) {
|
|
221
|
+
seen.add(key);
|
|
222
|
+
dirs.push(resolved);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
push(path.join(getArgoRoot(), 'schema'));
|
|
226
|
+
const basePaths = config && Array.isArray(config.basePaths) ? config.basePaths : [];
|
|
227
|
+
for (const basePath of basePaths) {
|
|
228
|
+
if (typeof basePath === 'string' && basePath.trim() !== '') {
|
|
229
|
+
push(path.resolve(childDir, basePath.trim()));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
push(childDir);
|
|
233
|
+
let entries = [];
|
|
234
|
+
try {
|
|
235
|
+
entries = fs.readdirSync(childDir, { withFileTypes: true });
|
|
236
|
+
} catch {
|
|
237
|
+
entries = [];
|
|
238
|
+
}
|
|
239
|
+
for (const entry of entries) {
|
|
240
|
+
if (entry.isDirectory()) {
|
|
241
|
+
push(path.join(childDir, entry.name));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return dirs;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function resolveBaseBundleDir(ext, childDir, config) {
|
|
248
|
+
const value = String(ext).trim();
|
|
249
|
+
// `default` is the reserved, language-neutral name of the built-in bundle.
|
|
250
|
+
if (value === 'default') {
|
|
251
|
+
return path.join(getArgoRoot(), 'schema');
|
|
252
|
+
}
|
|
253
|
+
if (!isPathLikeExt(value)) {
|
|
254
|
+
for (const dir of candidateBaseDirs(childDir, config)) {
|
|
255
|
+
if (bundleSymbolicNames(dir).has(value)) {
|
|
256
|
+
return dir;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Not a symbolic hit: treat as a path relative to the bundle directory.
|
|
261
|
+
return path.resolve(childDir, value);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function deepCloneJson(value) {
|
|
265
|
+
return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function mergeMatrixInto(target, source) {
|
|
269
|
+
if (!source || typeof source !== 'object') {
|
|
270
|
+
return target;
|
|
271
|
+
}
|
|
272
|
+
for (const [relationshipType, bySource] of Object.entries(source)) {
|
|
273
|
+
if (!bySource || typeof bySource !== 'object') {
|
|
274
|
+
target[relationshipType] = deepCloneJson(bySource);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (!target[relationshipType] || typeof target[relationshipType] !== 'object') {
|
|
278
|
+
target[relationshipType] = {};
|
|
279
|
+
}
|
|
280
|
+
for (const [sourceType, targets] of Object.entries(bySource)) {
|
|
281
|
+
target[relationshipType][sourceType] = deepCloneJson(targets);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return target;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Append added types to a JSON-Schema enum def (the element/relationship type
|
|
288
|
+
// universe), so a schema-less inheriting Profile's own added types are structurally
|
|
289
|
+
// valid — not just present at the ontology level.
|
|
290
|
+
function extendSchemaEnum(schemaDocument, enumKeys, additions) {
|
|
291
|
+
const defs = schemaDocument && typeof schemaDocument.$defs === 'object' ? schemaDocument.$defs : null;
|
|
292
|
+
if (!defs) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
for (const key of enumKeys) {
|
|
296
|
+
const node = defs[key];
|
|
297
|
+
if (node && Array.isArray(node.enum)) {
|
|
298
|
+
for (const value of additions) {
|
|
299
|
+
if (typeof value === 'string' && value !== '' && !node.enum.includes(value)) {
|
|
300
|
+
node.enum.push(value);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function mergeExtendsBundle(base, child) {
|
|
309
|
+
const baseRules = base.rules || {};
|
|
310
|
+
const childRules = child.rules || {};
|
|
311
|
+
const config = { ...(base.config || {}), ...(child.config || {}) };
|
|
312
|
+
|
|
313
|
+
const elementTypeMetadata = { ...(baseRules.elementTypeMetadata || {}), ...(childRules.elementTypeMetadata || {}) };
|
|
314
|
+
const archimateClassByElementType = { ...(baseRules.archimateClassByElementType || {}), ...(childRules.archimateClassByElementType || {}) };
|
|
315
|
+
const relationshipCategoryByType = { ...(baseRules.relationshipCategoryByType || {}), ...(childRules.relationshipCategoryByType || {}) };
|
|
316
|
+
|
|
317
|
+
const addElementTypes = config.addElementTypes && typeof config.addElementTypes === 'object' ? config.addElementTypes : {};
|
|
318
|
+
for (const [type, meta] of Object.entries(addElementTypes)) {
|
|
319
|
+
if (!elementTypeMetadata[type]) {
|
|
320
|
+
elementTypeMetadata[type] = { layer: (meta && meta.layer) || null, aspect: (meta && meta.aspect) || null };
|
|
321
|
+
}
|
|
322
|
+
if (meta && typeof meta.class === 'string' && meta.class !== '') {
|
|
323
|
+
archimateClassByElementType[type] = meta.class;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const addRelationships = config.addRelationships && typeof config.addRelationships === 'object' ? config.addRelationships : {};
|
|
327
|
+
for (const [type, category] of Object.entries(addRelationships)) {
|
|
328
|
+
relationshipCategoryByType[type] = category;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Guarantee base ∪ add for the element/relationship universe regardless of the
|
|
332
|
+
// base dialect (class-matrix derives types from rules metadata, type-matrix from
|
|
333
|
+
// schema enums) — an inheriting Profile must never silently drop a base type.
|
|
334
|
+
const baseOntology = buildOntology(base);
|
|
335
|
+
for (const type of baseOntology.elementTypes) {
|
|
336
|
+
if (!elementTypeMetadata[type]) {
|
|
337
|
+
elementTypeMetadata[type] = { layer: null, aspect: null };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
for (const type of baseOntology.relationshipTypes) {
|
|
341
|
+
if (!relationshipCategoryByType[type]) {
|
|
342
|
+
relationshipCategoryByType[type] = 'Custom';
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const relationshipTargetMatrix = deepCloneJson(baseRules.relationshipTargetMatrix || {}) || {};
|
|
347
|
+
mergeMatrixInto(relationshipTargetMatrix, childRules.relationshipTargetMatrix || {});
|
|
348
|
+
mergeMatrixInto(relationshipTargetMatrix, config.overrideMatrix || {});
|
|
349
|
+
|
|
350
|
+
// Inherit the base dialect as-is; never default to a specific dialect NAME here
|
|
351
|
+
// (that would couple the generic merge to one modeling language). When both are
|
|
352
|
+
// absent the ontology builder decides from the bundle's declared rules.
|
|
353
|
+
const dialect = childRules.dialect || baseRules.dialect;
|
|
354
|
+
const rules = {
|
|
355
|
+
...baseRules,
|
|
356
|
+
...childRules,
|
|
357
|
+
dialect,
|
|
358
|
+
elementTypeMetadata,
|
|
359
|
+
archimateClassByElementType,
|
|
360
|
+
relationshipCategoryByType,
|
|
361
|
+
relationshipTargetMatrix,
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// Structural schema: when the child has none, it inherits the base schema — but
|
|
365
|
+
// its added types must be valid against the base's $defs enum too, otherwise
|
|
366
|
+
// whole-graph validation (validateAgainstSchema) rejects elements of the added
|
|
367
|
+
// types. Extend the inherited schema enum in place (clone, never mutate base).
|
|
368
|
+
let schemaDocument = child.schemaDocument || base.schemaDocument;
|
|
369
|
+
if (!child.schemaDocument && base.schemaDocument) {
|
|
370
|
+
const extraElementTypes = Object.keys(addElementTypes).filter(Boolean);
|
|
371
|
+
const extraRelationshipTypes = Object.keys(addRelationships).filter(Boolean);
|
|
372
|
+
const extraFromConfigElements = Array.isArray(config.elementTypes) ? config.elementTypes : [];
|
|
373
|
+
const extraFromConfigRelationships = Array.isArray(config.relationshipTypes) ? config.relationshipTypes : [];
|
|
374
|
+
if (extraElementTypes.length || extraRelationshipTypes.length || extraFromConfigElements.length || extraFromConfigRelationships.length) {
|
|
375
|
+
schemaDocument = deepCloneJson(base.schemaDocument);
|
|
376
|
+
extendSchemaEnum(schemaDocument, ELEMENT_ENUM_KEYS, extraElementTypes.concat(extraFromConfigElements));
|
|
377
|
+
extendSchemaEnum(schemaDocument, RELATIONSHIP_ENUM_KEYS, extraRelationshipTypes.concat(extraFromConfigRelationships));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
...child,
|
|
383
|
+
schema: child.schema || base.schema,
|
|
384
|
+
schemaDocument,
|
|
385
|
+
rules,
|
|
386
|
+
config,
|
|
387
|
+
inheritedFrom: base.dir,
|
|
388
|
+
chainDirs: Array.from(new Set([
|
|
389
|
+
...(Array.isArray(base.chainDirs) && base.chainDirs.length > 0 ? base.chainDirs : [path.resolve(base.dir).toLowerCase()]),
|
|
390
|
+
path.resolve(child.dir).toLowerCase(),
|
|
391
|
+
])),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function applyExtends(bundle, workspaceRoot, seen = new Set()) {
|
|
396
|
+
const ext = bundle && bundle.config && typeof bundle.config.extends === 'string' ? bundle.config.extends.trim() : '';
|
|
397
|
+
if (!ext) {
|
|
398
|
+
return bundle;
|
|
399
|
+
}
|
|
400
|
+
seen.add(path.resolve(bundle.dir).toLowerCase());
|
|
401
|
+
const baseDir = resolveBaseBundleDir(ext, bundle.dir, bundle.config);
|
|
402
|
+
const baseKey = path.resolve(baseDir).toLowerCase();
|
|
403
|
+
if (seen.has(baseKey)) {
|
|
404
|
+
throw new Error(`schema bundle extends cycle detected at '${baseDir}'`);
|
|
405
|
+
}
|
|
406
|
+
if (!isFile(path.join(baseDir, SCHEMA_BASENAME)) && !hasExtendsConfig(baseDir)) {
|
|
407
|
+
throw new Error(`schema bundle extends '${ext}' could not be resolved: no schema bundle at '${baseDir}'`);
|
|
408
|
+
}
|
|
409
|
+
const baseBundle = applyExtends(buildBundle('extends', baseDir, workspaceRoot), workspaceRoot, seen);
|
|
410
|
+
return mergeExtendsBundle(baseBundle, bundle);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function resolveSchemaBundle(workspaceRoot, options = {}) {
|
|
414
|
+
const root = path.resolve(workspaceRoot || process.cwd());
|
|
415
|
+
const candidates = [];
|
|
416
|
+
|
|
417
|
+
const envDir = options.schemaDir || process.env.ARGO_SCHEMA_DIR;
|
|
418
|
+
if (typeof envDir === 'string' && envDir.trim() !== '') {
|
|
419
|
+
candidates.push({ kind: 'override', dir: path.resolve(root, envDir.trim()) });
|
|
420
|
+
}
|
|
421
|
+
candidates.push({ kind: 'workspace', dir: path.join(root, '.argo', 'schema') });
|
|
422
|
+
candidates.push({ kind: 'default', dir: path.join(getArgoRoot(), 'schema') });
|
|
423
|
+
|
|
424
|
+
const seen = new Set();
|
|
425
|
+
for (const candidate of candidates) {
|
|
426
|
+
const key = path.resolve(candidate.dir).toLowerCase();
|
|
427
|
+
if (seen.has(key)) {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
seen.add(key);
|
|
431
|
+
if (isFile(path.join(candidate.dir, SCHEMA_BASENAME)) || hasExtendsConfig(candidate.dir)) {
|
|
432
|
+
return applyExtends(buildBundle(candidate.kind, candidate.dir, root), root);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
throw new Error(
|
|
437
|
+
`Unable to locate '${SCHEMA_BASENAME}'. Checked: ${candidates.map(c => c.dir).join(', ')}`,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function resolveEnumFromSchema(schema, keys, configPathKey) {
|
|
442
|
+
if (configPathKey && Array.isArray(configPathKey)) {
|
|
443
|
+
let current = schema;
|
|
444
|
+
for (const segment of configPathKey) {
|
|
445
|
+
if (!current || typeof current !== 'object' || !(segment in current)) {
|
|
446
|
+
current = undefined;
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
current = current[segment];
|
|
450
|
+
}
|
|
451
|
+
if (Array.isArray(current)) {
|
|
452
|
+
return current.slice();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const defs = schema && typeof schema.$defs === 'object' && schema.$defs !== null ? schema.$defs : {};
|
|
456
|
+
for (const key of keys) {
|
|
457
|
+
const node = defs[key];
|
|
458
|
+
if (node && Array.isArray(node.enum)) {
|
|
459
|
+
return node.enum.slice();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return [];
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function resolveTypeEnums(bundle) {
|
|
466
|
+
const config = bundle.config || {};
|
|
467
|
+
const schema = bundle.schemaDocument || {};
|
|
468
|
+
|
|
469
|
+
let elementTypes = Array.isArray(config.elementTypes) ? config.elementTypes.slice() : [];
|
|
470
|
+
if (elementTypes.length === 0) {
|
|
471
|
+
elementTypes = resolveEnumFromSchema(schema, ELEMENT_ENUM_KEYS, config.elementTypeEnumPath);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
let relationshipTypes = Array.isArray(config.relationshipTypes) ? config.relationshipTypes.slice() : [];
|
|
475
|
+
if (relationshipTypes.length === 0) {
|
|
476
|
+
relationshipTypes = resolveEnumFromSchema(schema, RELATIONSHIP_ENUM_KEYS, config.relationshipTypeEnumPath);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return { elementTypes, relationshipTypes };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function resolveInvariants(config, defaults) {
|
|
483
|
+
const raw = config && typeof config.invariants === 'object' && config.invariants !== null
|
|
484
|
+
? config.invariants
|
|
485
|
+
: {};
|
|
486
|
+
const invariants = { ...defaults };
|
|
487
|
+
if (typeof raw.statementGrammar === 'boolean') {
|
|
488
|
+
invariants.statementGrammar = raw.statementGrammar;
|
|
489
|
+
}
|
|
490
|
+
if (typeof raw.endpointMatrix === 'boolean') {
|
|
491
|
+
invariants.endpointMatrix = raw.endpointMatrix;
|
|
492
|
+
}
|
|
493
|
+
if (raw.rootViewName === null) {
|
|
494
|
+
invariants.rootViewName = null;
|
|
495
|
+
} else if (typeof raw.rootViewName === 'string' && raw.rootViewName.trim() !== '') {
|
|
496
|
+
invariants.rootViewName = raw.rootViewName.trim();
|
|
497
|
+
}
|
|
498
|
+
if (raw.maxElementsPerView === null) {
|
|
499
|
+
invariants.maxElementsPerView = null;
|
|
500
|
+
} else if (Number.isInteger(raw.maxElementsPerView) && raw.maxElementsPerView >= 0) {
|
|
501
|
+
invariants.maxElementsPerView = raw.maxElementsPerView;
|
|
502
|
+
}
|
|
503
|
+
return invariants;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function resolveActorElementType(config) {
|
|
507
|
+
// The Actor element type is part of the bundle contract: the ARGO workflow
|
|
508
|
+
// identifies the agent through an Actor element (wakeup gate). A bundle may
|
|
509
|
+
// rename it, or set it to null to declare that the schema has no actor concept
|
|
510
|
+
// (actor identification is then skipped). Absent => the default 'Business Actor'.
|
|
511
|
+
if (config && Object.prototype.hasOwnProperty.call(config, 'actorElementType')) {
|
|
512
|
+
return config.actorElementType;
|
|
513
|
+
}
|
|
514
|
+
return DEFAULT_ACTOR_ELEMENT_TYPE;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Per-element-type attribute contract (issue #3): declare that an element type
|
|
518
|
+
// must carry certain attributes, that some attribute values are a controlled
|
|
519
|
+
// vocabulary, and that some attribute values are unique within the type.
|
|
520
|
+
// { "Rule": { "required": ["ruleId","normativity"], "unique": ["ruleId"],
|
|
521
|
+
// "enumByAttr": { "normativity": ["MUST","SHOULD","MAY","MUST_NOT"] } } }
|
|
522
|
+
// Absent => no per-type attribute contract (backward compatible).
|
|
523
|
+
function resolveAttributeContracts(config) {
|
|
524
|
+
const raw = config && typeof config.attributesByElementType === 'object' && config.attributesByElementType !== null
|
|
525
|
+
? config.attributesByElementType
|
|
526
|
+
: null;
|
|
527
|
+
if (!raw) {
|
|
528
|
+
return undefined;
|
|
529
|
+
}
|
|
530
|
+
const contracts = {};
|
|
531
|
+
for (const [type, contract] of Object.entries(raw)) {
|
|
532
|
+
if (!contract || typeof contract !== 'object') {
|
|
533
|
+
contracts[type] = {};
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
contracts[type] = {
|
|
537
|
+
required: Array.isArray(contract.required) ? contract.required.slice() : undefined,
|
|
538
|
+
unique: Array.isArray(contract.unique) ? contract.unique.slice() : undefined,
|
|
539
|
+
enumByAttr: contract.enumByAttr && typeof contract.enumByAttr === 'object' ? { ...contract.enumByAttr } : undefined,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
return contracts;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function validateAttributeContracts(language, elementTypeList, contracts) {
|
|
546
|
+
const errors = [];
|
|
547
|
+
if (!contracts || typeof contracts !== 'object') {
|
|
548
|
+
return errors;
|
|
549
|
+
}
|
|
550
|
+
for (const [type, contract] of Object.entries(contracts)) {
|
|
551
|
+
if (!elementTypeList.includes(type)) {
|
|
552
|
+
errors.push(`schema bundle '${language}' attributesByElementType references unknown element type '${type}'`);
|
|
553
|
+
}
|
|
554
|
+
if (!contract || typeof contract !== 'object') {
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
for (const key of ['required', 'unique']) {
|
|
558
|
+
const list = contract[key];
|
|
559
|
+
if (list === undefined) {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
if (!Array.isArray(list)) {
|
|
563
|
+
errors.push(`schema bundle '${language}' attributesByElementType['${type}'].${key} must be an array`);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
for (const name of list) {
|
|
567
|
+
if (typeof name !== 'string' || name === '') {
|
|
568
|
+
errors.push(`schema bundle '${language}' attributesByElementType['${type}'].${key} entries must be non-empty strings`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (contract.enumByAttr !== undefined) {
|
|
573
|
+
if (!contract.enumByAttr || typeof contract.enumByAttr !== 'object') {
|
|
574
|
+
errors.push(`schema bundle '${language}' attributesByElementType['${type}'].enumByAttr must be an object`);
|
|
575
|
+
} else {
|
|
576
|
+
for (const [attr, allowed] of Object.entries(contract.enumByAttr)) {
|
|
577
|
+
if (!Array.isArray(allowed) || allowed.length === 0) {
|
|
578
|
+
errors.push(`schema bundle '${language}' attributesByElementType['${type}'].enumByAttr['${attr}'] must be a non-empty array`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return errors;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function validateBundle({ language, dialect, elementTypes, relationshipTypes, actorElementType, matrix, deliveryDependencies, attributesByElementType }) {
|
|
588
|
+
const errors = [];
|
|
589
|
+
const elementTypeList = Array.isArray(elementTypes) ? elementTypes : [];
|
|
590
|
+
const relationshipTypeList = Array.isArray(relationshipTypes) ? relationshipTypes : [];
|
|
591
|
+
|
|
592
|
+
if (elementTypeList.length === 0) {
|
|
593
|
+
errors.push(`schema bundle '${language}' defines no element types`);
|
|
594
|
+
}
|
|
595
|
+
if (relationshipTypeList.length === 0) {
|
|
596
|
+
errors.push(`schema bundle '${language}' defines no relationship types`);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (actorElementType === null) {
|
|
600
|
+
// Explicit opt-out: the schema has no actor/agent identity concept.
|
|
601
|
+
} else if (typeof actorElementType === 'string' && actorElementType.trim() !== '') {
|
|
602
|
+
if (!elementTypeList.includes(actorElementType)) {
|
|
603
|
+
errors.push(
|
|
604
|
+
`schema bundle '${language}' declares actorElementType '${actorElementType}' which is not one of its element types; ` +
|
|
605
|
+
`set a valid actorElementType in schema-bundle.config.json (one of: ${elementTypeList.join(', ') || '(none)'}) ` +
|
|
606
|
+
`or set "actorElementType": null if the schema has no actor concept`,
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
} else {
|
|
610
|
+
errors.push(`schema bundle '${language}' actorElementType must be a non-empty string or null; got ${JSON.stringify(actorElementType)}`);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// The endpoint matrix (type-keyed bundles only) must only reference declared types.
|
|
614
|
+
if (dialect !== 'archimate-class-matrix' && matrix && typeof matrix === 'object') {
|
|
615
|
+
for (const [relationshipType, targetsBySource] of Object.entries(matrix)) {
|
|
616
|
+
if (!relationshipTypeList.includes(relationshipType)) {
|
|
617
|
+
errors.push(`relationshipTargetMatrix references unknown relationship type '${relationshipType}'`);
|
|
618
|
+
}
|
|
619
|
+
for (const [sourceType, targetList] of Object.entries(targetsBySource || {})) {
|
|
620
|
+
if (!elementTypeList.includes(sourceType)) {
|
|
621
|
+
errors.push(`relationshipTargetMatrix['${relationshipType}'] references unknown element type '${sourceType}'`);
|
|
622
|
+
}
|
|
623
|
+
for (const targetType of Array.isArray(targetList) ? targetList : []) {
|
|
624
|
+
if (targetType !== '*' && !elementTypeList.includes(targetType)) {
|
|
625
|
+
errors.push(`relationshipTargetMatrix['${relationshipType}']['${sourceType}'] references unknown element type '${targetType}'`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
errors.push(...validateDeliveryDependencies(language, relationshipTypeList, deliveryDependencies));
|
|
633
|
+
errors.push(...validateAttributeContracts(language, elementTypeList, attributesByElementType));
|
|
634
|
+
|
|
635
|
+
return { status: errors.length === 0 ? 'passed' : 'failed', errors };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function validateDeliveryDependencies(language, relationshipTypeList, deliveryDependencies) {
|
|
639
|
+
const errors = [];
|
|
640
|
+
if (!deliveryDependencies || typeof deliveryDependencies !== 'object') {
|
|
641
|
+
return errors;
|
|
642
|
+
}
|
|
643
|
+
const relSet = new Set(relationshipTypeList);
|
|
644
|
+
for (const key of ['sourceDependsOnTarget', 'targetDependsOnSource']) {
|
|
645
|
+
for (const type of Array.isArray(deliveryDependencies[key]) ? deliveryDependencies[key] : []) {
|
|
646
|
+
if (!relSet.has(type)) {
|
|
647
|
+
errors.push(`schema bundle '${language}' deliveryDependencies.${key} references unknown relationship type '${type}'`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return errors;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function buildClassMatrixOntology(bundle) {
|
|
655
|
+
const language = typeof bundle.config.language === 'string' && bundle.config.language.trim() !== ''
|
|
656
|
+
? bundle.config.language.trim()
|
|
657
|
+
: DEFAULT_LANGUAGE;
|
|
658
|
+
const rules = bundle.rules;
|
|
659
|
+
let elementTypeMetadata;
|
|
660
|
+
let relationshipCategoryByType;
|
|
661
|
+
let isSupportedElementType;
|
|
662
|
+
let isSupportedRelationshipType;
|
|
663
|
+
let getMetadata;
|
|
664
|
+
let getArchiMateClass;
|
|
665
|
+
let validateRelationshipEndpointTypes;
|
|
666
|
+
|
|
667
|
+
if (rules) {
|
|
668
|
+
// Data-driven default: the bundle ships its own rule data (schema-bundle.rules.json),
|
|
669
|
+
// so the DEFAULT schema is replaceable file-for-file exactly like a custom one.
|
|
670
|
+
const classByType = rules.archimateClassByElementType || {};
|
|
671
|
+
const classMatrix = rules.relationshipTargetMatrix || {};
|
|
672
|
+
elementTypeMetadata = new Map(Object.entries(rules.elementTypeMetadata || {}));
|
|
673
|
+
relationshipCategoryByType = new Map(Object.entries(rules.relationshipCategoryByType || {}));
|
|
674
|
+
isSupportedElementType = (type) => elementTypeMetadata.has(type);
|
|
675
|
+
isSupportedRelationshipType = (type) => relationshipCategoryByType.has(type);
|
|
676
|
+
getMetadata = (element) => elementTypeMetadata.get(element && element.type) || {};
|
|
677
|
+
getArchiMateClass = (elementOrType) => {
|
|
678
|
+
const type = typeof elementOrType === 'string' ? elementOrType : elementOrType && elementOrType.type;
|
|
679
|
+
return classByType[type];
|
|
680
|
+
};
|
|
681
|
+
validateRelationshipEndpointTypes = (relationship, source, target) => {
|
|
682
|
+
if (!relationship || !source || !target) {
|
|
683
|
+
return [];
|
|
684
|
+
}
|
|
685
|
+
const type = relationship.type;
|
|
686
|
+
if (!relationshipCategoryByType.has(type)) {
|
|
687
|
+
return ['relationships \'' + relationship.id + '\' uses unsupported ArchiMate relationship type \'' + type + '\''];
|
|
688
|
+
}
|
|
689
|
+
const sourceClass = getArchiMateClass(source);
|
|
690
|
+
const targetClass = getArchiMateClass(target);
|
|
691
|
+
if (!sourceClass || !targetClass) {
|
|
692
|
+
return [];
|
|
693
|
+
}
|
|
694
|
+
const allowedTargets = classMatrix[type] && classMatrix[type][sourceClass];
|
|
695
|
+
if (!allowedTargets || !allowedTargets.some((allowed) => allowed === targetClass || allowed === 'ModelConcept')) {
|
|
696
|
+
return ['relationships \'' + relationship.id + '\' violates ArchiMate 3.2 relationship matrix: ' + source.type + ' \'' + source.name + '\' cannot ' + type + ' ' + target.type + ' \'' + target.name + '\''];
|
|
697
|
+
}
|
|
698
|
+
return [];
|
|
699
|
+
};
|
|
700
|
+
} else {
|
|
701
|
+
// Legacy fallback: an installation without schema-bundle.rules.json uses the bundled module.
|
|
702
|
+
const mod = require('./archimate32-rules.js');
|
|
703
|
+
elementTypeMetadata = mod.elementTypeMetadata;
|
|
704
|
+
relationshipCategoryByType = mod.relationshipCategoryByType;
|
|
705
|
+
isSupportedElementType = mod.isSupportedElementType;
|
|
706
|
+
isSupportedRelationshipType = mod.isSupportedRelationshipType;
|
|
707
|
+
getMetadata = mod.getMetadata;
|
|
708
|
+
getArchiMateClass = mod.getArchiMateClass;
|
|
709
|
+
validateRelationshipEndpointTypes = mod.validateRelationshipEndpointTypes;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const elementTypes = Array.from(elementTypeMetadata.keys());
|
|
713
|
+
const relationshipTypes = Array.from(relationshipCategoryByType.keys());
|
|
714
|
+
const actorElementType = resolveActorElementType(bundle.config);
|
|
715
|
+
const deliveryDependencies = resolveDeliveryDependencies(bundle.config, 'archimate-class-matrix');
|
|
716
|
+
const attributesByElementType = resolveAttributeContracts(bundle.config);
|
|
717
|
+
return finalizeOntology({
|
|
718
|
+
kind: bundle.kind,
|
|
719
|
+
dialect: 'archimate-class-matrix',
|
|
720
|
+
language,
|
|
721
|
+
elementTypeErrorLabel: 'ArchiMate',
|
|
722
|
+
relationshipTypeErrorLabel: 'ArchiMate',
|
|
723
|
+
matrixErrorLabel: 'ArchiMate 3.2 relationship matrix',
|
|
724
|
+
elementTypes,
|
|
725
|
+
relationshipTypes,
|
|
726
|
+
actorElementType,
|
|
727
|
+
deliveryDependencies,
|
|
728
|
+
attributesByElementType,
|
|
729
|
+
bundleValidation: validateBundle({ language, dialect: 'archimate-class-matrix', elementTypes, relationshipTypes, actorElementType, matrix: null, deliveryDependencies, attributesByElementType }),
|
|
730
|
+
elementTypeMetadata,
|
|
731
|
+
relationshipCategoryByType,
|
|
732
|
+
isSupportedElementType,
|
|
733
|
+
isSupportedRelationshipType,
|
|
734
|
+
getMetadata,
|
|
735
|
+
getArchiMateClass,
|
|
736
|
+
validateRelationshipEndpointTypes,
|
|
737
|
+
invariants: resolveInvariants(bundle.config, {
|
|
738
|
+
statementGrammar: true,
|
|
739
|
+
endpointMatrix: true,
|
|
740
|
+
rootViewName: DEFAULT_ROOT_VIEW_NAME,
|
|
741
|
+
maxElementsPerView: DEFAULT_MAX_ELEMENTS_PER_VIEW,
|
|
742
|
+
}),
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function buildTypeMatrixOntology(bundle) {
|
|
747
|
+
const config = bundle.config || {};
|
|
748
|
+
const language = typeof config.language === 'string' && config.language.trim() !== ''
|
|
749
|
+
? config.language.trim()
|
|
750
|
+
: 'Custom Ontology';
|
|
751
|
+
const enums = resolveTypeEnums(bundle);
|
|
752
|
+
const rules = bundle.rules || {};
|
|
753
|
+
const rawMetadata = rules.elementTypeMetadata && typeof rules.elementTypeMetadata === 'object'
|
|
754
|
+
? rules.elementTypeMetadata
|
|
755
|
+
: {};
|
|
756
|
+
const rawCategories = rules.relationshipCategoryByType && typeof rules.relationshipCategoryByType === 'object'
|
|
757
|
+
? rules.relationshipCategoryByType
|
|
758
|
+
: {};
|
|
759
|
+
const matrix = rules.relationshipTargetMatrix && typeof rules.relationshipTargetMatrix === 'object'
|
|
760
|
+
? rules.relationshipTargetMatrix
|
|
761
|
+
: null;
|
|
762
|
+
|
|
763
|
+
const elementTypeMetadata = new Map();
|
|
764
|
+
for (const type of enums.elementTypes) {
|
|
765
|
+
elementTypeMetadata.set(type, rawMetadata[type] || { layer: null, aspect: null });
|
|
766
|
+
}
|
|
767
|
+
for (const [type, meta] of Object.entries(rawMetadata)) {
|
|
768
|
+
if (!elementTypeMetadata.has(type)) {
|
|
769
|
+
elementTypeMetadata.set(type, meta);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const relationshipCategoryByType = new Map();
|
|
774
|
+
for (const type of enums.relationshipTypes) {
|
|
775
|
+
relationshipCategoryByType.set(type, rawCategories[type] || 'Custom');
|
|
776
|
+
}
|
|
777
|
+
for (const [type, category] of Object.entries(rawCategories)) {
|
|
778
|
+
if (!relationshipCategoryByType.has(type)) {
|
|
779
|
+
relationshipCategoryByType.set(type, category);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const matrixErrorLabel = `${language} relationship matrix`;
|
|
784
|
+
const actorElementType = resolveActorElementType(config);
|
|
785
|
+
const elementTypes = Array.from(elementTypeMetadata.keys());
|
|
786
|
+
const relationshipTypes = Array.from(relationshipCategoryByType.keys());
|
|
787
|
+
const deliveryDependencies = resolveDeliveryDependencies(config, 'type-matrix');
|
|
788
|
+
const attributesByElementType = resolveAttributeContracts(config);
|
|
789
|
+
const getArchiMateClass = (elementOrType) => {
|
|
790
|
+
const type = typeof elementOrType === 'string' ? elementOrType : elementOrType && elementOrType.type;
|
|
791
|
+
return type;
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
function validateRelationshipEndpointTypes(relationship, source, target) {
|
|
795
|
+
if (!relationship || !source || !target) {
|
|
796
|
+
return [];
|
|
797
|
+
}
|
|
798
|
+
const type = relationship.type;
|
|
799
|
+
if (!relationshipCategoryByType.has(type)) {
|
|
800
|
+
return [`relationships '${relationship.id}' uses unsupported ${language} relationship type '${type}'`];
|
|
801
|
+
}
|
|
802
|
+
if (!matrix) {
|
|
803
|
+
return [];
|
|
804
|
+
}
|
|
805
|
+
const allowedTargets = matrix[type] && matrix[type][source.type];
|
|
806
|
+
if (!allowedTargets) {
|
|
807
|
+
return [];
|
|
808
|
+
}
|
|
809
|
+
const ok = allowedTargets.some((allowed) => allowed === target.type || allowed === '*');
|
|
810
|
+
if (!ok) {
|
|
811
|
+
return [`relationships '${relationship.id}' violates ${matrixErrorLabel}: ${source.type} '${source.name}' cannot ${type} ${target.type} '${target.name}'`];
|
|
812
|
+
}
|
|
813
|
+
return [];
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
return finalizeOntology({
|
|
817
|
+
kind: bundle.kind,
|
|
818
|
+
dialect: 'type-matrix',
|
|
819
|
+
language,
|
|
820
|
+
elementTypeErrorLabel: language,
|
|
821
|
+
relationshipTypeErrorLabel: language,
|
|
822
|
+
matrixErrorLabel,
|
|
823
|
+
elementTypes,
|
|
824
|
+
relationshipTypes,
|
|
825
|
+
actorElementType,
|
|
826
|
+
deliveryDependencies,
|
|
827
|
+
attributesByElementType,
|
|
828
|
+
bundleValidation: validateBundle({ language, dialect: 'type-matrix', elementTypes, relationshipTypes, actorElementType, matrix, deliveryDependencies, attributesByElementType }),
|
|
829
|
+
elementTypeMetadata,
|
|
830
|
+
relationshipCategoryByType,
|
|
831
|
+
isSupportedElementType: (type) => elementTypeMetadata.has(type),
|
|
832
|
+
isSupportedRelationshipType: (type) => relationshipCategoryByType.has(type),
|
|
833
|
+
getMetadata: (element) => elementTypeMetadata.get(element && element.type) || {},
|
|
834
|
+
getArchiMateClass,
|
|
835
|
+
validateRelationshipEndpointTypes,
|
|
836
|
+
invariants: resolveInvariants(config, {
|
|
837
|
+
statementGrammar: true,
|
|
838
|
+
endpointMatrix: Boolean(matrix),
|
|
839
|
+
rootViewName: DEFAULT_ROOT_VIEW_NAME,
|
|
840
|
+
maxElementsPerView: DEFAULT_MAX_ELEMENTS_PER_VIEW,
|
|
841
|
+
}),
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function finalizeOntology(ontology) {
|
|
846
|
+
const elementById = new Map();
|
|
847
|
+
function auditRelationshipEndpointTypes(document, relationshipIds) {
|
|
848
|
+
const errors = [];
|
|
849
|
+
const idSet = Array.isArray(relationshipIds) && relationshipIds.length > 0
|
|
850
|
+
? new Set(relationshipIds)
|
|
851
|
+
: undefined;
|
|
852
|
+
for (const element of (document && document.elements) || []) {
|
|
853
|
+
elementById.set(element.id, element);
|
|
854
|
+
}
|
|
855
|
+
for (const relationship of (document && document.relationships) || []) {
|
|
856
|
+
if (idSet && !idSet.has(relationship.id)) {
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
const source = elementById.get(relationship.source_id);
|
|
860
|
+
const target = elementById.get(relationship.target_id);
|
|
861
|
+
errors.push(...ontology.validateRelationshipEndpointTypes(relationship, source, target));
|
|
862
|
+
}
|
|
863
|
+
return errors;
|
|
864
|
+
}
|
|
865
|
+
return {
|
|
866
|
+
...ontology,
|
|
867
|
+
auditRelationshipEndpointTypes,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const ontologyCache = new Map();
|
|
872
|
+
|
|
873
|
+
function buildOntology(bundle) {
|
|
874
|
+
const rules = bundle.rules;
|
|
875
|
+
if (rules && rules.dialect === 'archimate-class-matrix') {
|
|
876
|
+
return buildClassMatrixOntology(bundle);
|
|
877
|
+
}
|
|
878
|
+
if (rules) {
|
|
879
|
+
return buildTypeMatrixOntology(bundle);
|
|
880
|
+
}
|
|
881
|
+
// No rules file: the default bundle falls back to the bundled module; a custom
|
|
882
|
+
// bundle without rules is validated permissively (types from schema enums).
|
|
883
|
+
return bundle.kind === 'default' ? buildClassMatrixOntology(bundle) : buildTypeMatrixOntology(bundle);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Cheap on-disk fingerprint of a bundle (and its extends chain) so a running MCP
|
|
887
|
+
// picks up edits to the bundle files without a restart: the ontology cache key
|
|
888
|
+
// changes when any key file's mtime/size changes.
|
|
889
|
+
function bundleFingerprint(bundle) {
|
|
890
|
+
const dirs = Array.isArray(bundle.chainDirs) && bundle.chainDirs.length > 0
|
|
891
|
+
? bundle.chainDirs
|
|
892
|
+
: [path.resolve(bundle.dir).toLowerCase()];
|
|
893
|
+
const statFile = (absolutePath) => {
|
|
894
|
+
try {
|
|
895
|
+
const stat = fs.statSync(absolutePath);
|
|
896
|
+
return `${Math.round(stat.mtimeMs)}:${stat.size}`;
|
|
897
|
+
} catch {
|
|
898
|
+
return '-';
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
const parts = [];
|
|
902
|
+
for (const dir of dirs) {
|
|
903
|
+
for (const name of [SCHEMA_BASENAME, CONFIG_BASENAME, RULES_BASENAME]) {
|
|
904
|
+
parts.push(`${name}=${statFile(path.join(dir, name))}`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
if (bundle.config && typeof bundle.config.rules === 'string' && bundle.config.rules.trim() !== '') {
|
|
908
|
+
parts.push(`rules=${statFile(path.resolve(bundle.dir, bundle.config.rules))}`);
|
|
909
|
+
}
|
|
910
|
+
return parts.join('|');
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function loadSchemaBundleAndOntology(workspaceRoot, options = {}) {
|
|
914
|
+
const bundle = resolveSchemaBundle(workspaceRoot, options);
|
|
915
|
+
const cacheKey = `${bundle.kind}:${path.resolve(bundle.dir).toLowerCase()}:${bundleFingerprint(bundle)}`;
|
|
916
|
+
if (ontologyCache.has(cacheKey)) {
|
|
917
|
+
return { bundle, ontology: ontologyCache.get(cacheKey) };
|
|
918
|
+
}
|
|
919
|
+
const ontology = buildOntology(bundle);
|
|
920
|
+
ontologyCache.set(cacheKey, ontology);
|
|
921
|
+
return { bundle, ontology };
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
module.exports = {
|
|
925
|
+
SCHEMA_BASENAME,
|
|
926
|
+
CONFIG_BASENAME,
|
|
927
|
+
RULES_BASENAME,
|
|
928
|
+
resolveSchemaBundle,
|
|
929
|
+
resolveTypeEnums,
|
|
930
|
+
buildOntology,
|
|
931
|
+
loadSchemaBundleAndOntology,
|
|
932
|
+
};
|