archgraph-argo 0.27.0 → 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 +126 -65
- package/argo/.env.example +12 -1
- package/argo/rules/archgraph.instructions.md +1 -1
- package/argo/schema/{argob.config.json → schema-bundle.config.json} +4 -2
- package/argo/schema/{argob-rules.json → schema-bundle.rules.json} +12229 -12229
- package/argo/scripts/agentSearchDiagnose.js +410 -407
- package/argo/scripts/argo-mcp-server.js +59 -12
- package/argo/scripts/ensureArgoHarnessEnvironment.js +1 -1
- package/argo/scripts/external-graph-query.js +162 -162
- package/argo/scripts/graph-rag/liveEmbeddingProviderConfig.js +3 -0
- package/argo/scripts/graph-semantics.js +100 -3
- package/argo/scripts/runArchitectureTests.js +591 -591
- package/argo/scripts/{argob-schema.js → schema-bundle.js} +386 -28
- package/argo/scripts/systemarchitecture-mcp-server.js +673 -68
- package/argo/scripts/validateSystemArchitecture.js +3 -1
- 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 +2 -2
- package/argo/skills/ea-human-reconcile/SKILL.md +40 -40
- package/cordis.patch.yml +15 -6
- package/dsh-argo-wakeup/index.js +17 -17
- package/dsh-argo-workspace/index.js +227 -142
- package/install-argo.ps1 +122 -21
- package/package.json +57 -56
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
'use strict';
|
|
1
|
+
'use strict';
|
|
2
2
|
|
|
3
3
|
// ARGO schema-bundle resolution and ontology construction.
|
|
4
4
|
//
|
|
@@ -6,18 +6,18 @@
|
|
|
6
6
|
// "schema bundle" is a directory that carries the graph contract:
|
|
7
7
|
//
|
|
8
8
|
// <bundle>/SystemArchitecture.schema.json (required) JSON Schema of the graph
|
|
9
|
-
// <bundle>/
|
|
9
|
+
// <bundle>/schema-bundle.config.json (optional) bundle descriptor:
|
|
10
10
|
// language, enum locations, guide,
|
|
11
11
|
// rules file, invariant switches
|
|
12
|
-
// <bundle>/
|
|
12
|
+
// <bundle>/schema-bundle.rules.json (optional) ontology rules data
|
|
13
13
|
// (type metadata, relationship
|
|
14
14
|
// categories, endpoint matrix)
|
|
15
|
-
// <bundle>/
|
|
15
|
+
// <bundle>/GUIDE.md (optional) human-readable guide
|
|
16
16
|
//
|
|
17
17
|
// Resolution precedence (first bundle that has SystemArchitecture.schema.json):
|
|
18
|
-
// 1. ARGO_SCHEMA_DIR
|
|
19
|
-
// 2. <workspaceRoot>/.argo/schema
|
|
20
|
-
// 3. <argoRoot>/schema
|
|
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
21
|
//
|
|
22
22
|
// The default bundle keeps the historical ArchiMate 3.2 + ARGO behaviour. A
|
|
23
23
|
// custom bundle may define its own element/relationship types and, optionally,
|
|
@@ -29,9 +29,9 @@ const path = require('node:path');
|
|
|
29
29
|
const { getArgoRoot } = require('./argo-paths.js');
|
|
30
30
|
|
|
31
31
|
const SCHEMA_BASENAME = 'SystemArchitecture.schema.json';
|
|
32
|
-
const CONFIG_BASENAME = '
|
|
33
|
-
const RULES_BASENAME = '
|
|
34
|
-
const GUIDE_BASENAME = '
|
|
32
|
+
const CONFIG_BASENAME = 'schema-bundle.config.json';
|
|
33
|
+
const RULES_BASENAME = 'schema-bundle.rules.json';
|
|
34
|
+
const GUIDE_BASENAME = 'GUIDE.md';
|
|
35
35
|
const DEFAULT_GUIDE_BASENAME = 'archimate3.2.md';
|
|
36
36
|
const DEFAULT_LANGUAGE = 'ArchiMate 3.2';
|
|
37
37
|
const DEFAULT_ROOT_VIEW_NAME = 'SystemArchitecture';
|
|
@@ -39,8 +39,8 @@ const DEFAULT_MAX_ELEMENTS_PER_VIEW = 15;
|
|
|
39
39
|
const DEFAULT_ACTOR_ELEMENT_TYPE = 'Business Actor';
|
|
40
40
|
|
|
41
41
|
// Which relationship types express a delivery dependency and in which direction.
|
|
42
|
-
// Declared per bundle via
|
|
43
|
-
//
|
|
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
44
|
const ARCHIMATE_DELIVERY_DEPENDENCIES = Object.freeze({
|
|
45
45
|
sourceDependsOnTarget: Object.freeze(['Access', 'Assignment', 'Specialization', 'Composition', 'Aggregation']),
|
|
46
46
|
targetDependsOnSource: Object.freeze(['Serving', 'Realization', 'Flow', 'Triggering', 'Influence']),
|
|
@@ -99,12 +99,13 @@ function relativeLabel(workspaceRoot, absolutePath) {
|
|
|
99
99
|
|
|
100
100
|
function buildBundle(kind, dir, workspaceRoot) {
|
|
101
101
|
const schemaFile = path.join(dir, SCHEMA_BASENAME);
|
|
102
|
-
const
|
|
102
|
+
const schemaExists = isFile(schemaFile);
|
|
103
|
+
const schema = schemaExists ? readJsonFile(schemaFile) : null;
|
|
103
104
|
|
|
104
105
|
const configFile = path.join(dir, CONFIG_BASENAME);
|
|
105
106
|
const fileConfig = isFile(configFile) ? readJsonFile(configFile) : {};
|
|
106
|
-
const inlineConfig = schema && typeof schema['x-
|
|
107
|
-
? schema['x-
|
|
107
|
+
const inlineConfig = schema && typeof schema['x-schema-bundle'] === 'object' && schema['x-schema-bundle'] !== null
|
|
108
|
+
? schema['x-schema-bundle']
|
|
108
109
|
: {};
|
|
109
110
|
const config = { ...inlineConfig, ...fileConfig };
|
|
110
111
|
|
|
@@ -135,10 +136,9 @@ function buildBundle(kind, dir, workspaceRoot) {
|
|
|
135
136
|
kind,
|
|
136
137
|
dir,
|
|
137
138
|
relativeDir: relativeLabel(workspaceRoot, dir),
|
|
138
|
-
schema:
|
|
139
|
-
absolutePath: schemaFile,
|
|
140
|
-
|
|
141
|
-
},
|
|
139
|
+
schema: schemaExists
|
|
140
|
+
? { absolutePath: schemaFile, relativePath: relativeLabel(workspaceRoot, schemaFile) }
|
|
141
|
+
: null,
|
|
142
142
|
schemaDocument: schema,
|
|
143
143
|
config: {
|
|
144
144
|
filePath: isFile(configFile) ? { absolutePath: configFile, relativePath: relativeLabel(workspaceRoot, configFile) } : null,
|
|
@@ -154,6 +154,262 @@ function buildBundle(kind, dir, workspaceRoot) {
|
|
|
154
154
|
};
|
|
155
155
|
}
|
|
156
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
|
+
|
|
157
413
|
function resolveSchemaBundle(workspaceRoot, options = {}) {
|
|
158
414
|
const root = path.resolve(workspaceRoot || process.cwd());
|
|
159
415
|
const candidates = [];
|
|
@@ -172,8 +428,8 @@ function resolveSchemaBundle(workspaceRoot, options = {}) {
|
|
|
172
428
|
continue;
|
|
173
429
|
}
|
|
174
430
|
seen.add(key);
|
|
175
|
-
if (isFile(path.join(candidate.dir, SCHEMA_BASENAME))) {
|
|
176
|
-
return buildBundle(candidate.kind, candidate.dir, root);
|
|
431
|
+
if (isFile(path.join(candidate.dir, SCHEMA_BASENAME)) || hasExtendsConfig(candidate.dir)) {
|
|
432
|
+
return applyExtends(buildBundle(candidate.kind, candidate.dir, root), root);
|
|
177
433
|
}
|
|
178
434
|
}
|
|
179
435
|
|
|
@@ -258,7 +514,77 @@ function resolveActorElementType(config) {
|
|
|
258
514
|
return DEFAULT_ACTOR_ELEMENT_TYPE;
|
|
259
515
|
}
|
|
260
516
|
|
|
261
|
-
|
|
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 }) {
|
|
262
588
|
const errors = [];
|
|
263
589
|
const elementTypeList = Array.isArray(elementTypes) ? elementTypes : [];
|
|
264
590
|
const relationshipTypeList = Array.isArray(relationshipTypes) ? relationshipTypes : [];
|
|
@@ -276,7 +602,7 @@ function validateBundle({ language, dialect, elementTypes, relationshipTypes, ac
|
|
|
276
602
|
if (!elementTypeList.includes(actorElementType)) {
|
|
277
603
|
errors.push(
|
|
278
604
|
`schema bundle '${language}' declares actorElementType '${actorElementType}' which is not one of its element types; ` +
|
|
279
|
-
`set a valid actorElementType in
|
|
605
|
+
`set a valid actorElementType in schema-bundle.config.json (one of: ${elementTypeList.join(', ') || '(none)'}) ` +
|
|
280
606
|
`or set "actorElementType": null if the schema has no actor concept`,
|
|
281
607
|
);
|
|
282
608
|
}
|
|
@@ -304,6 +630,7 @@ function validateBundle({ language, dialect, elementTypes, relationshipTypes, ac
|
|
|
304
630
|
}
|
|
305
631
|
|
|
306
632
|
errors.push(...validateDeliveryDependencies(language, relationshipTypeList, deliveryDependencies));
|
|
633
|
+
errors.push(...validateAttributeContracts(language, elementTypeList, attributesByElementType));
|
|
307
634
|
|
|
308
635
|
return { status: errors.length === 0 ? 'passed' : 'failed', errors };
|
|
309
636
|
}
|
|
@@ -338,7 +665,7 @@ function buildClassMatrixOntology(bundle) {
|
|
|
338
665
|
let validateRelationshipEndpointTypes;
|
|
339
666
|
|
|
340
667
|
if (rules) {
|
|
341
|
-
// Data-driven default: the bundle ships its own rule data (
|
|
668
|
+
// Data-driven default: the bundle ships its own rule data (schema-bundle.rules.json),
|
|
342
669
|
// so the DEFAULT schema is replaceable file-for-file exactly like a custom one.
|
|
343
670
|
const classByType = rules.archimateClassByElementType || {};
|
|
344
671
|
const classMatrix = rules.relationshipTargetMatrix || {};
|
|
@@ -371,7 +698,7 @@ function buildClassMatrixOntology(bundle) {
|
|
|
371
698
|
return [];
|
|
372
699
|
};
|
|
373
700
|
} else {
|
|
374
|
-
// Legacy fallback: an installation without
|
|
701
|
+
// Legacy fallback: an installation without schema-bundle.rules.json uses the bundled module.
|
|
375
702
|
const mod = require('./archimate32-rules.js');
|
|
376
703
|
elementTypeMetadata = mod.elementTypeMetadata;
|
|
377
704
|
relationshipCategoryByType = mod.relationshipCategoryByType;
|
|
@@ -386,6 +713,7 @@ function buildClassMatrixOntology(bundle) {
|
|
|
386
713
|
const relationshipTypes = Array.from(relationshipCategoryByType.keys());
|
|
387
714
|
const actorElementType = resolveActorElementType(bundle.config);
|
|
388
715
|
const deliveryDependencies = resolveDeliveryDependencies(bundle.config, 'archimate-class-matrix');
|
|
716
|
+
const attributesByElementType = resolveAttributeContracts(bundle.config);
|
|
389
717
|
return finalizeOntology({
|
|
390
718
|
kind: bundle.kind,
|
|
391
719
|
dialect: 'archimate-class-matrix',
|
|
@@ -397,7 +725,8 @@ function buildClassMatrixOntology(bundle) {
|
|
|
397
725
|
relationshipTypes,
|
|
398
726
|
actorElementType,
|
|
399
727
|
deliveryDependencies,
|
|
400
|
-
|
|
728
|
+
attributesByElementType,
|
|
729
|
+
bundleValidation: validateBundle({ language, dialect: 'archimate-class-matrix', elementTypes, relationshipTypes, actorElementType, matrix: null, deliveryDependencies, attributesByElementType }),
|
|
401
730
|
elementTypeMetadata,
|
|
402
731
|
relationshipCategoryByType,
|
|
403
732
|
isSupportedElementType,
|
|
@@ -456,6 +785,7 @@ function buildTypeMatrixOntology(bundle) {
|
|
|
456
785
|
const elementTypes = Array.from(elementTypeMetadata.keys());
|
|
457
786
|
const relationshipTypes = Array.from(relationshipCategoryByType.keys());
|
|
458
787
|
const deliveryDependencies = resolveDeliveryDependencies(config, 'type-matrix');
|
|
788
|
+
const attributesByElementType = resolveAttributeContracts(config);
|
|
459
789
|
const getArchiMateClass = (elementOrType) => {
|
|
460
790
|
const type = typeof elementOrType === 'string' ? elementOrType : elementOrType && elementOrType.type;
|
|
461
791
|
return type;
|
|
@@ -494,7 +824,8 @@ function buildTypeMatrixOntology(bundle) {
|
|
|
494
824
|
relationshipTypes,
|
|
495
825
|
actorElementType,
|
|
496
826
|
deliveryDependencies,
|
|
497
|
-
|
|
827
|
+
attributesByElementType,
|
|
828
|
+
bundleValidation: validateBundle({ language, dialect: 'type-matrix', elementTypes, relationshipTypes, actorElementType, matrix, deliveryDependencies, attributesByElementType }),
|
|
498
829
|
elementTypeMetadata,
|
|
499
830
|
relationshipCategoryByType,
|
|
500
831
|
isSupportedElementType: (type) => elementTypeMetadata.has(type),
|
|
@@ -552,9 +883,36 @@ function buildOntology(bundle) {
|
|
|
552
883
|
return bundle.kind === 'default' ? buildClassMatrixOntology(bundle) : buildTypeMatrixOntology(bundle);
|
|
553
884
|
}
|
|
554
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
|
+
|
|
555
913
|
function loadSchemaBundleAndOntology(workspaceRoot, options = {}) {
|
|
556
914
|
const bundle = resolveSchemaBundle(workspaceRoot, options);
|
|
557
|
-
const cacheKey = `${bundle.kind}:${path.resolve(bundle.dir).toLowerCase()}`;
|
|
915
|
+
const cacheKey = `${bundle.kind}:${path.resolve(bundle.dir).toLowerCase()}:${bundleFingerprint(bundle)}`;
|
|
558
916
|
if (ontologyCache.has(cacheKey)) {
|
|
559
917
|
return { bundle, ontology: ontologyCache.get(cacheKey) };
|
|
560
918
|
}
|