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
|
@@ -6,7 +6,6 @@ const readline = require('node:readline');
|
|
|
6
6
|
const crypto = require('node:crypto');
|
|
7
7
|
|
|
8
8
|
const {
|
|
9
|
-
getArgoRoot,
|
|
10
9
|
resolveCallWorkspaceRoot,
|
|
11
10
|
} = require('./argo-paths.js');
|
|
12
11
|
|
|
@@ -131,7 +130,13 @@ const {
|
|
|
131
130
|
validateGraphSemantics,
|
|
132
131
|
validateArchiMateEndpointMatrix,
|
|
133
132
|
validateViewElementLimits,
|
|
133
|
+
validateAttributeContracts,
|
|
134
134
|
} = require('./graph-semantics.js');
|
|
135
|
+
const {
|
|
136
|
+
loadSchemaBundleAndOntology,
|
|
137
|
+
resolveSchemaBundle,
|
|
138
|
+
resolveTypeEnums,
|
|
139
|
+
} = require('./schema-bundle.js');
|
|
135
140
|
const {
|
|
136
141
|
createProductionGraphRagRuntime,
|
|
137
142
|
} = require('./graph-rag/productionGraphRagRuntime.js');
|
|
@@ -168,6 +173,7 @@ const {
|
|
|
168
173
|
} = require('./neo4j-system-architecture-store.js');
|
|
169
174
|
|
|
170
175
|
const losslessWriteGate = require('./lossless-write-gate.js');
|
|
176
|
+
const { EXTERNAL_READ_TOOLS } = require('./external-graph-query.js');
|
|
171
177
|
|
|
172
178
|
const HANDLED_MUTATION_TYPES = new Set([
|
|
173
179
|
'addElement',
|
|
@@ -221,17 +227,17 @@ const TOOLS = [
|
|
|
221
227
|
},
|
|
222
228
|
{
|
|
223
229
|
name: 'getIntentElementContext',
|
|
224
|
-
description: 'read-only query that returns an intent subgraph context for one element. Uses ArchiMate semantic dependency traversal with dependencyDepth and dependentDepth, preserving native subgraph elements, relationships, and views.',
|
|
230
|
+
description: 'read-only query that returns an intent subgraph context for one element. Uses ArchiMate semantic dependency traversal with dependencyDepth and dependentDepth, preserving native subgraph elements, relationships, and views. Output is bounded by maxBytes (default ARGO_CONTEXT_MAX_BYTES or 32000): beyond it non-focus members degrade to identity (id/type/name) and a complete id manifest is returned under truncation, so the host never silently cuts the payload. On a large/hub element leave includeAttributes/includeTestcases off (they are the ledger and can dominate the size) and prefer queryNeo4jGraph to locate ids, then read them narrowly.',
|
|
225
231
|
inputSchema: intentElementContextInputSchema(),
|
|
226
232
|
},
|
|
227
233
|
{
|
|
228
234
|
name: 'getArchitectureViewContext',
|
|
229
|
-
description: 'read-only query that resolves one view by view_id into its complete membership: the view object, every member element (from included_elements), every member relationship (from included_relationships), the parent element, and optionally child sub-views declared by member elements. Resolves ids into full canonical objects instead of returning raw id lists. Optional includeEaGeometry (default false) additionally returns the EA diagram geometry of the resolved view: element boxes plus each connector ROUTE (t_diagramlinks.Path as `path` + parsed `points`, with the non-route Geometry override kept separately under `geometry`).',
|
|
235
|
+
description: 'read-only query that resolves one view by view_id into its complete membership: the view object, every member element (from included_elements), every member relationship (from included_relationships), the parent element, and optionally child sub-views declared by member elements. Resolves ids into full canonical objects instead of returning raw id lists. Output is bounded by maxBytes (default ARGO_CONTEXT_MAX_BYTES or 32000): beyond it members degrade to identity (id/type/name) with a complete id manifest under truncation. Optional includeEaGeometry (default false) additionally returns the EA diagram geometry of the resolved view: element boxes plus each connector ROUTE (t_diagramlinks.Path as `path` + parsed `points`, with the non-route Geometry override kept separately under `geometry`).',
|
|
230
236
|
inputSchema: viewContextInputSchema(),
|
|
231
237
|
},
|
|
232
238
|
{
|
|
233
239
|
name: 'previewSystemArchitectureMutation',
|
|
234
|
-
description: 'Use before apply for complex or risky changes. Performs a dry-run of one or more mutations, runs schema, graph, view, and
|
|
240
|
+
description: 'Use before apply for complex or risky changes. Performs a dry-run of one or more mutations, runs schema, graph, view, and modeling-language validation, and does not write the graph.',
|
|
235
241
|
inputSchema: mutationInputSchema(),
|
|
236
242
|
},
|
|
237
243
|
{
|
|
@@ -241,12 +247,18 @@ const TOOLS = [
|
|
|
241
247
|
},
|
|
242
248
|
{
|
|
243
249
|
name: 'addArchitectureElement',
|
|
244
|
-
description: 'Use for one element. Creates a new element or adds an existing element to view_ids. view_ids is required so elements never exist outside views. Set dryRun to preview without writing.',
|
|
250
|
+
description: 'Use for one element. Creates a new element or adds an existing element to view_ids. view_ids is required so elements never exist outside views. element.id is OPTIONAL: omit it and the server auto-allocates a unique id (returned in the result); provide it to pin a specific id (must not collide with a different element). Set dryRun to preview without writing.',
|
|
245
251
|
inputSchema: {
|
|
246
252
|
type: 'object',
|
|
247
253
|
required: ['element', 'view_ids'],
|
|
248
254
|
properties: {
|
|
249
|
-
element: {
|
|
255
|
+
element: {
|
|
256
|
+
type: 'object',
|
|
257
|
+
properties: {
|
|
258
|
+
id: { type: 'string', description: 'Element id (OPTIONAL). Omit to let the server auto-allocate a unique id (a semantic slug from the name, e.g. "graph-wiki-federation-center", suffixed -002.. if taken). If provided it must not collide with a DIFFERENT element (a same (type,name) match is idempotent reuse).' },
|
|
259
|
+
},
|
|
260
|
+
description: 'The element to create/attach. `id` is optional — omit it and the server allocates one (returned in the result).',
|
|
261
|
+
},
|
|
250
262
|
view_ids: { type: 'array', minItems: 1, items: { type: 'string' } },
|
|
251
263
|
onConflict: { type: 'string', enum: ['reuse', 'allowDuplicate'], description: 'Dedup policy (default reuse). reuse: find-or-create — attach an existing exact (type, name) match; a same-type semantic near-duplicate also blocks creation. allowDuplicate: create anyway (even if a duplicate exists), requires a justification.' },
|
|
252
264
|
justification: { type: 'string', description: 'Required when onConflict is allowDuplicate.' },
|
|
@@ -288,7 +300,7 @@ const TOOLS = [
|
|
|
288
300
|
},
|
|
289
301
|
{
|
|
290
302
|
name: 'addArchitectureRelationship',
|
|
291
|
-
description: 'Use for one relationship. Creates a new relationship or adds an existing relationship to view_ids. relationship.type is the ArchiMate 3.2 relationship type and is validated against endpoint element types. Set dryRun to preview without writing.',
|
|
303
|
+
description: 'Use for one relationship. Creates a new relationship or adds an existing relationship to view_ids. relationship.type is the ArchiMate 3.2 relationship type and is validated against endpoint element types. relationship.id is OPTIONAL: omit it and the server auto-allocates a unique id (returned in the result). Set dryRun to preview without writing.',
|
|
292
304
|
inputSchema: {
|
|
293
305
|
type: 'object',
|
|
294
306
|
required: ['relationship', 'view_ids'],
|
|
@@ -335,7 +347,7 @@ const TOOLS = [
|
|
|
335
347
|
},
|
|
336
348
|
{
|
|
337
349
|
name: 'addArchitectureView',
|
|
338
|
-
description: 'Use for one view. The graph must have exactly one top-level view named SystemArchitecture; all sub-views must attach to an element with parent_element_id. Set dryRun to preview without writing.',
|
|
350
|
+
description: 'Use for one view. The graph must have exactly one top-level view named SystemArchitecture; all sub-views must attach to an element with parent_element_id. view.view_id is OPTIONAL: omit it and the server auto-allocates a unique view_id (returned in the result). Set dryRun to preview without writing.',
|
|
339
351
|
inputSchema: {
|
|
340
352
|
type: 'object',
|
|
341
353
|
required: ['view'],
|
|
@@ -380,7 +392,7 @@ const TOOLS = [
|
|
|
380
392
|
},
|
|
381
393
|
{
|
|
382
394
|
name: 'queryNeo4jGraph',
|
|
383
|
-
description: 'Run a read-only Cypher query against the Neo4j structural projection of the intent architecture, or request the projection schema so an agent can construct its own Cypher. Pass {schema: true} to return node labels, relationship types, property keys, and the legal
|
|
395
|
+
description: 'Run a read-only Cypher query against the Neo4j structural projection of the intent architecture, or request the projection schema so an agent can construct its own Cypher. Pass {schema: true} to return node labels, relationship types, property keys, and the legal element/relationship type enums of the workspace-resolved schema bundle. Pass {cypher: "..."} to execute a read-only query; scope it with {graphKey: $graphKey}. Write clauses (CREATE/MERGE/DELETE/SET/REMOVE/DROP/LOAD CSV/FOREACH/IN TRANSACTIONS) are rejected.',
|
|
384
396
|
inputSchema: {
|
|
385
397
|
type: 'object',
|
|
386
398
|
properties: {
|
|
@@ -415,6 +427,13 @@ const WORKSPACE_ROOT_PARAM = Object.freeze({
|
|
|
415
427
|
description:
|
|
416
428
|
'Optional absolute workspace root for this call. When provided it is used as-is; otherwise the server launch directory is used.',
|
|
417
429
|
});
|
|
430
|
+
// Cross-project graph query: the 5 read tools accept an optional `projectId`;
|
|
431
|
+
// provided => routed to the federation center, omitted => the local workspace.
|
|
432
|
+
const PROJECT_ID_PARAM = Object.freeze({
|
|
433
|
+
type: 'string',
|
|
434
|
+
description:
|
|
435
|
+
'Optional external project id. When provided, the query is routed to the federation center to read that project\'s graph (this project must be registered in .argo/federation.json); omitted = the local workspace (unchanged).',
|
|
436
|
+
});
|
|
418
437
|
// Lossless write gate: every update/remove helper accepts an explicit loss
|
|
419
438
|
// acknowledgement (see lossless-write-gate.js). add* is purely additive and is
|
|
420
439
|
// left alone. Without the acknowledgement a shrinking text edit or a destructive
|
|
@@ -436,6 +455,9 @@ for (const tool of TOOLS) {
|
|
|
436
455
|
if (!Object.prototype.hasOwnProperty.call(inputSchema.properties, 'workspaceRoot')) {
|
|
437
456
|
inputSchema.properties.workspaceRoot = WORKSPACE_ROOT_PARAM;
|
|
438
457
|
}
|
|
458
|
+
if (EXTERNAL_READ_TOOLS.has(tool.name) && !Object.prototype.hasOwnProperty.call(inputSchema.properties, 'projectId')) {
|
|
459
|
+
inputSchema.properties.projectId = PROJECT_ID_PARAM;
|
|
460
|
+
}
|
|
439
461
|
if (tool.name && /^(update|remove)Architecture/.test(tool.name)) {
|
|
440
462
|
for (const [key, value] of Object.entries(LOSS_ACK_INPUT_PROPS)) {
|
|
441
463
|
if (!Object.prototype.hasOwnProperty.call(inputSchema.properties, key)) {
|
|
@@ -461,8 +483,9 @@ function intentElementContextInputSchema() {
|
|
|
461
483
|
dependentDepth: { type: 'number', description: 'Default: 1. Semantic dependents that rely on the focus element.' },
|
|
462
484
|
associationDepth: { type: 'number', description: 'Default: 1. Association neighbors are expanded at least one layer.' },
|
|
463
485
|
associationNeighborDependencyDepth: { type: 'number', description: 'Default: 0. Optional dependency expansion from association neighbors.' },
|
|
464
|
-
includeAttributes: { type: 'boolean', description: 'Default: false. Include `attributes` (commit/session/release ledgers) verbatim. Omitted by default from this structural read; the focus element always keeps its own. Semantic retrieval embeds attributes, so semantic hits keep them.' },
|
|
465
|
-
includeTestcases: { type: 'boolean', description: 'Default: false. Include member `testcases` verbatim. Omitted by default from this structural read (bookkeeping); pass true when you need acceptance cases. Semantic retrieval embeds testcase descriptions, so semantic hits keep them.' },
|
|
486
|
+
includeAttributes: { type: 'boolean', description: 'Default: false. Include `attributes` (commit/session/release ledgers) verbatim. Omitted by default from this structural read; the focus element always keeps its own. Semantic retrieval embeds attributes, so semantic hits keep them. On a hub element this ledger can dominate the payload — prefer leaving it off and reading bookkeeping narrowly.' },
|
|
487
|
+
includeTestcases: { type: 'boolean', description: 'Default: false. Include member `testcases` verbatim. Omitted by default from this structural read (bookkeeping); pass true when you need acceptance cases. Semantic retrieval embeds testcase descriptions, so semantic hits keep them. On a hub element this can be large — prefer leaving it off.' },
|
|
488
|
+
maxBytes: { type: 'number', description: 'Optional output budget in UTF-8 bytes (the exact serialized response). Default: ARGO_CONTEXT_MAX_BYTES env or 32000. 0 = unlimited. When the payload exceeds the budget, non-focus members degrade to identity (id/type/name) and a complete id manifest is returned under `truncation` — the payload is never silently truncated by the host.' },
|
|
466
489
|
},
|
|
467
490
|
additionalProperties: false,
|
|
468
491
|
};
|
|
@@ -480,6 +503,7 @@ function viewContextInputSchema() {
|
|
|
480
503
|
includeAttributes: { type: 'boolean', description: 'Default: false. Include member/relationship `attributes` (commit/session/release ledgers) verbatim. Omitted by default from this structural read (bookkeeping); pass true when you need provenance.' },
|
|
481
504
|
includeTestcases: { type: 'boolean', description: 'Default: false. Include member `testcases` verbatim. Omitted by default from this structural read; pass true for acceptance-case lookups.' },
|
|
482
505
|
includeEaGeometry: { type: 'boolean', description: 'Default: false (opt-in). When true, additionally resolve the diagram GEOMETRY (element boxes + connector line routes) for this view from the workspace EA model (.qea) and return it under a `geometry` field aligned by schema id with the resolved members. Each geometry relationship carries: `path` (the EA route from t_diagramlinks.Path, "" when EA auto-routes), `points` (the parsed [{x,y}] waypoints), `edge` (the EDGE route-style token or null) and `geometry` (the raw SX/SY/EX/EY override string, which contains NO waypoints). By default the EA model is never touched and no `geometry` field is returned; a missing EA model/diagram yields geometry.present=false, never an error.' },
|
|
506
|
+
maxBytes: { type: 'number', description: 'Optional output budget in UTF-8 bytes (the exact serialized response). Default: ARGO_CONTEXT_MAX_BYTES env or 32000. 0 = unlimited. When the payload exceeds the budget, members degrade to identity (id/type/name) and a complete id manifest is returned under `truncation` — the payload is never silently truncated by the host.' },
|
|
483
507
|
},
|
|
484
508
|
additionalProperties: false,
|
|
485
509
|
};
|
|
@@ -559,22 +583,7 @@ function normalizeRelativePath(value) {
|
|
|
559
583
|
}
|
|
560
584
|
|
|
561
585
|
function resolveSchemaPath(workspaceRoot) {
|
|
562
|
-
|
|
563
|
-
if (fs.existsSync(bundledSchemaPath)) {
|
|
564
|
-
return { absolutePath: bundledSchemaPath, relativePath: SCHEMA_PATH_CANDIDATES[0] };
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
for (const candidate of SCHEMA_PATH_CANDIDATES) {
|
|
568
|
-
const absolutePath = path.join(workspaceRoot, candidate);
|
|
569
|
-
if (fs.existsSync(absolutePath)) {
|
|
570
|
-
return { absolutePath, relativePath: candidate };
|
|
571
|
-
}
|
|
572
|
-
const bundledPath = path.resolve(__dirname, '..', '..', candidate);
|
|
573
|
-
if (fs.existsSync(bundledPath)) {
|
|
574
|
-
return { absolutePath: bundledPath, relativePath: candidate };
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
throw new Error(`Unable to locate SystemArchitecture schema. Checked: ${SCHEMA_PATH_CANDIDATES.join(', ')}`);
|
|
586
|
+
return resolveSchemaBundle(workspaceRoot).schema;
|
|
578
587
|
}
|
|
579
588
|
|
|
580
589
|
function readJson(filePath, label) {
|
|
@@ -588,13 +597,17 @@ function readJson(filePath, label) {
|
|
|
588
597
|
async function loadContext(args = {}) {
|
|
589
598
|
const workspaceRoot = resolveWorkspaceRoot(args);
|
|
590
599
|
const graphPath = resolveWorkspacePath(workspaceRoot, args.architecturePath || DEFAULT_GRAPH_PATH);
|
|
591
|
-
|
|
600
|
+
// Resolve the modeling language for THIS workspace: a repository that ships
|
|
601
|
+
// its own bundle under .argo/schema is validated against that schema.
|
|
602
|
+
const { bundle, ontology } = loadSchemaBundleAndOntology(workspaceRoot);
|
|
592
603
|
const context = {
|
|
593
604
|
workspaceRoot,
|
|
594
605
|
graphPath,
|
|
595
|
-
schemaPath,
|
|
606
|
+
schemaPath: bundle.schema,
|
|
607
|
+
schemaBundle: bundle,
|
|
608
|
+
ontology,
|
|
596
609
|
document: readJson(graphPath.absolutePath, graphPath.relativePath),
|
|
597
|
-
schema:
|
|
610
|
+
schema: bundle.schemaDocument,
|
|
598
611
|
};
|
|
599
612
|
context.neo4jSyncRecovery = await recoverNeo4jSyncIfNeeded({
|
|
600
613
|
architecturePath: graphPath.relativePath,
|
|
@@ -605,13 +618,20 @@ async function loadContext(args = {}) {
|
|
|
605
618
|
}
|
|
606
619
|
|
|
607
620
|
function validateDocument(document, schema, options = {}) {
|
|
621
|
+
const ontology = options.ontology;
|
|
608
622
|
const errors = [];
|
|
623
|
+
if (ontology && ontology.bundleValidation && ontology.bundleValidation.status === 'failed') {
|
|
624
|
+
errors.push(...ontology.bundleValidation.errors.map(error => `schema bundle: ${error}`));
|
|
625
|
+
}
|
|
609
626
|
validateAgainstSchema(document, schema, '#', errors, schema);
|
|
610
|
-
validateGraphSemantics(document, errors);
|
|
627
|
+
validateGraphSemantics(document, errors, ontology);
|
|
628
|
+
validateAttributeContracts(document, errors, ontology);
|
|
611
629
|
validateArchiMateEndpointMatrix(document, errors, {
|
|
630
|
+
ontology,
|
|
612
631
|
touchedRelationshipIds: options.touchedRelationshipIds,
|
|
613
632
|
});
|
|
614
633
|
validateViewElementLimits(document, errors, {
|
|
634
|
+
ontology,
|
|
615
635
|
touchedViewIds: options.validateAllViewElementLimits
|
|
616
636
|
? (document.views || []).map(view => view && view.view_id)
|
|
617
637
|
: options.touchedViewIds,
|
|
@@ -668,6 +688,233 @@ function buildAgentProjection(omitted) {
|
|
|
668
688
|
};
|
|
669
689
|
}
|
|
670
690
|
|
|
691
|
+
// ---------------------------------------------------------------------------
|
|
692
|
+
// Context output budget (bounded, well-formed structural reads)
|
|
693
|
+
// ---------------------------------------------------------------------------
|
|
694
|
+
// A hub element/view can expand a semantic subgraph to tens of KB, which the
|
|
695
|
+
// HOST then cuts mid-JSON — the agent receives a malformed partial observation.
|
|
696
|
+
// This budget makes the ceiling the framework's, not the host's: above maxBytes
|
|
697
|
+
// the non-focus members degrade to identity (id/type/name), the focus element +
|
|
698
|
+
// boundary/explorationHints are kept, and EVERY included id is listed under
|
|
699
|
+
// `truncation` — so the payload is always valid JSON and always navigable.
|
|
700
|
+
// Default (or ARGO_CONTEXT_MAX_BYTES); 0 = unlimited. Semantic retrieval is NOT
|
|
701
|
+
// affected (only these two structural builders).
|
|
702
|
+
const DEFAULT_CONTEXT_MAX_BYTES = 32000;
|
|
703
|
+
const CONTEXT_BUDGET_NOTE =
|
|
704
|
+
'The full payload exceeded maxBytes, so non-focus members are reduced to identity '
|
|
705
|
+
+ '(id/type/name). The focus element and boundary/explorationHints are kept and every '
|
|
706
|
+
+ 'included id is listed here. Raise maxBytes (0 = unlimited) or narrow the traversal '
|
|
707
|
+
+ '(dependencyDepth/dependentDepth) to read details.';
|
|
708
|
+
|
|
709
|
+
function resolveContextMaxBytes(args = {}) {
|
|
710
|
+
const raw = args && args.maxBytes !== undefined ? args.maxBytes : process.env.ARGO_CONTEXT_MAX_BYTES;
|
|
711
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
712
|
+
return DEFAULT_CONTEXT_MAX_BYTES;
|
|
713
|
+
}
|
|
714
|
+
const numeric = Number(raw);
|
|
715
|
+
if (!Number.isFinite(numeric) || numeric < 0) {
|
|
716
|
+
return DEFAULT_CONTEXT_MAX_BYTES;
|
|
717
|
+
}
|
|
718
|
+
return Math.floor(numeric);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Budget is measured on the EXACT serialization the caller receives
|
|
722
|
+
// (`toolResult` pretty-prints with 2 spaces), in UTF-8 bytes.
|
|
723
|
+
function payloadByteLength(value) {
|
|
724
|
+
try { return Buffer.byteLength(JSON.stringify(value, null, 2)); } catch (_) { return 0; }
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function identityElementRecord(element) {
|
|
728
|
+
return { id: element.id, name: element.name, type: element.type };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function identityRelationshipRecord(relationship) {
|
|
732
|
+
return {
|
|
733
|
+
id: relationship.id,
|
|
734
|
+
name: relationship.name,
|
|
735
|
+
type: relationship.type,
|
|
736
|
+
source_id: relationship.source_id,
|
|
737
|
+
target_id: relationship.target_id,
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function identityViewRecord(view) {
|
|
742
|
+
return { view_id: view.view_id, view_name: view.view_name };
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function budgetTruncationBase(result, maxBytes, fullBytes, ids) {
|
|
746
|
+
return {
|
|
747
|
+
truncated: true,
|
|
748
|
+
reason: 'context_budget_exceeded',
|
|
749
|
+
maxBytes,
|
|
750
|
+
fullBytes,
|
|
751
|
+
includedElementIds: ids.elements,
|
|
752
|
+
includedRelationshipIds: ids.relationships,
|
|
753
|
+
includedViewIds: ids.views,
|
|
754
|
+
note: CONTEXT_BUDGET_NOTE,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Shrink the manifest id lists (last resort) so even a pathological hub stays
|
|
759
|
+
// within budget. The focus id is never dropped; `manifestTruncated` records any
|
|
760
|
+
// real loss of a listed id.
|
|
761
|
+
function trimManifestToBudget(result, maxBytes) {
|
|
762
|
+
const truncation = result.truncation;
|
|
763
|
+
const focusId = result.focusElementId;
|
|
764
|
+
// If the FOCUS element alone already exceeds the budget, trimming the (cheap)
|
|
765
|
+
// id manifest cannot get us under it — and dropping ids would only lose
|
|
766
|
+
// navigation for nothing. Keep the COMPLETE manifest and say why.
|
|
767
|
+
const focusOnly = {
|
|
768
|
+
...result,
|
|
769
|
+
truncation: {
|
|
770
|
+
...truncation,
|
|
771
|
+
includedElementIds: focusId ? [focusId] : [],
|
|
772
|
+
includedRelationshipIds: [],
|
|
773
|
+
includedViewIds: [],
|
|
774
|
+
},
|
|
775
|
+
};
|
|
776
|
+
if (focusId && payloadByteLength(focusOnly) > maxBytes) {
|
|
777
|
+
truncation.overBudgetByFocus = true;
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const arrays = ['includedElementIds', 'includedRelationshipIds', 'includedViewIds'];
|
|
781
|
+
for (let ratio = 0.9; ratio >= 0; ratio -= 0.1) {
|
|
782
|
+
for (const key of arrays) {
|
|
783
|
+
const full = truncation[key] || [];
|
|
784
|
+
const kept = full.slice(0, Math.ceil(full.length * ratio));
|
|
785
|
+
if (key === 'includedElementIds' && focusId && !kept.includes(focusId)) {
|
|
786
|
+
kept.unshift(focusId);
|
|
787
|
+
}
|
|
788
|
+
truncation[key] = kept;
|
|
789
|
+
}
|
|
790
|
+
truncation.manifestTruncated = true;
|
|
791
|
+
if (payloadByteLength(result) <= maxBytes) return;
|
|
792
|
+
}
|
|
793
|
+
truncation.manifestTruncated = true;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function applyContextBudget(result, maxBytes) {
|
|
797
|
+
if (!result || result.status !== 'passed' || !maxBytes || maxBytes <= 0) {
|
|
798
|
+
return result;
|
|
799
|
+
}
|
|
800
|
+
const fullBytes = payloadByteLength(result);
|
|
801
|
+
if (fullBytes <= maxBytes) {
|
|
802
|
+
return result;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const subgraph = result.subgraph || {};
|
|
806
|
+
const elements = Array.isArray(subgraph.elements) ? subgraph.elements : [];
|
|
807
|
+
const relationships = Array.isArray(subgraph.relationships) ? subgraph.relationships : [];
|
|
808
|
+
const views = Array.isArray(subgraph.views) ? subgraph.views : [];
|
|
809
|
+
const focusId = result.focusElementId;
|
|
810
|
+
const ids = {
|
|
811
|
+
elements: elements.map((entry) => entry.id),
|
|
812
|
+
relationships: relationships.map((entry) => entry.id),
|
|
813
|
+
views: views.map((entry) => entry.view_id),
|
|
814
|
+
};
|
|
815
|
+
const truncated = (tier) => budgetTruncationBase(result, maxBytes, fullBytes, ids);
|
|
816
|
+
const nonFocusCount = elements.filter((entry) => entry.id !== focusId).length;
|
|
817
|
+
|
|
818
|
+
const compact = {
|
|
819
|
+
...result,
|
|
820
|
+
subgraph: {
|
|
821
|
+
...subgraph,
|
|
822
|
+
elements: elements.map((entry) => (entry.id === focusId ? entry : identityElementRecord(entry))),
|
|
823
|
+
relationships: relationships.map(identityRelationshipRecord),
|
|
824
|
+
views: views.map(identityViewRecord),
|
|
825
|
+
},
|
|
826
|
+
truncation: { ...truncated('identity'), omittedElementDetails: nonFocusCount },
|
|
827
|
+
};
|
|
828
|
+
if (payloadByteLength(compact) <= maxBytes) {
|
|
829
|
+
return compact;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const idsOnly = {
|
|
833
|
+
...result,
|
|
834
|
+
subgraph: {
|
|
835
|
+
...subgraph,
|
|
836
|
+
elements: elements.map((entry) => (entry.id === focusId ? entry : { id: entry.id })),
|
|
837
|
+
relationships: relationships.map((entry) => ({ id: entry.id })),
|
|
838
|
+
views: views.map((entry) => ({ view_id: entry.view_id })),
|
|
839
|
+
},
|
|
840
|
+
truncation: truncated('ids-only'),
|
|
841
|
+
};
|
|
842
|
+
if (payloadByteLength(idsOnly) <= maxBytes) {
|
|
843
|
+
return idsOnly;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const manifestOnly = {
|
|
847
|
+
...result,
|
|
848
|
+
subgraph: {
|
|
849
|
+
elements: elements.filter((entry) => entry.id === focusId),
|
|
850
|
+
relationships: [],
|
|
851
|
+
views: [],
|
|
852
|
+
},
|
|
853
|
+
truncation: truncated('manifest-only'),
|
|
854
|
+
};
|
|
855
|
+
trimManifestToBudget(manifestOnly, maxBytes);
|
|
856
|
+
return manifestOnly;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function applyViewContextBudget(result, maxBytes) {
|
|
860
|
+
if (!result || result.status !== 'passed' || !maxBytes || maxBytes <= 0) {
|
|
861
|
+
return result;
|
|
862
|
+
}
|
|
863
|
+
const fullBytes = payloadByteLength(result);
|
|
864
|
+
if (fullBytes <= maxBytes) {
|
|
865
|
+
return result;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const elements = Array.isArray(result.elements) ? result.elements : [];
|
|
869
|
+
const relationships = Array.isArray(result.relationships) ? result.relationships : [];
|
|
870
|
+
const childViews = Array.isArray(result.childViews) ? result.childViews : [];
|
|
871
|
+
const ids = {
|
|
872
|
+
elements: elements.map((entry) => entry.id),
|
|
873
|
+
relationships: relationships.map((entry) => entry.id),
|
|
874
|
+
views: childViews.map((entry) => entry.view_id),
|
|
875
|
+
};
|
|
876
|
+
const strippedView = result.view
|
|
877
|
+
? { ...result.view, included_elements: [], included_relationships: [] }
|
|
878
|
+
: result.view;
|
|
879
|
+
const truncated = (tier) => ({ ...budgetTruncationBase(result, maxBytes, fullBytes, ids), tier });
|
|
880
|
+
|
|
881
|
+
const compact = {
|
|
882
|
+
...result,
|
|
883
|
+
elements: elements.map(identityElementRecord),
|
|
884
|
+
relationships: relationships.map(identityRelationshipRecord),
|
|
885
|
+
parentElement: result.parentElement ? identityElementRecord(result.parentElement) : result.parentElement,
|
|
886
|
+
childViews: childViews.map(identityViewRecord),
|
|
887
|
+
truncation: { ...truncated('identity'), omittedElementDetails: elements.length },
|
|
888
|
+
};
|
|
889
|
+
if (payloadByteLength(compact) <= maxBytes) {
|
|
890
|
+
return compact;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const idsOnly = {
|
|
894
|
+
...result,
|
|
895
|
+
elements: elements.map((entry) => ({ id: entry.id })),
|
|
896
|
+
relationships: relationships.map((entry) => ({ id: entry.id })),
|
|
897
|
+
parentElement: result.parentElement ? { id: result.parentElement.id } : result.parentElement,
|
|
898
|
+
childViews: childViews.map((entry) => ({ view_id: entry.view_id })),
|
|
899
|
+
truncation: truncated('ids-only'),
|
|
900
|
+
};
|
|
901
|
+
if (payloadByteLength(idsOnly) <= maxBytes) {
|
|
902
|
+
return idsOnly;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const manifestOnly = {
|
|
906
|
+
...result,
|
|
907
|
+
view: strippedView,
|
|
908
|
+
elements: [],
|
|
909
|
+
relationships: [],
|
|
910
|
+
parentElement: null,
|
|
911
|
+
childViews: [],
|
|
912
|
+
truncation: truncated('manifest-only'),
|
|
913
|
+
};
|
|
914
|
+
trimManifestToBudget(manifestOnly, maxBytes);
|
|
915
|
+
return manifestOnly;
|
|
916
|
+
}
|
|
917
|
+
|
|
671
918
|
function buildIntentElementContext(context, args = {}) {
|
|
672
919
|
const profile = args.profile || 'generic-agent';
|
|
673
920
|
const focusResult = resolveFocusElement(context.document, args);
|
|
@@ -679,7 +926,7 @@ function buildIntentElementContext(context, args = {}) {
|
|
|
679
926
|
const dependentDepth = normalizeDepth(args.dependentDepth, 1);
|
|
680
927
|
const associationDepth = Math.max(1, normalizeDepth(args.associationDepth, 1));
|
|
681
928
|
const associationNeighborDependencyDepth = normalizeDepth(args.associationNeighborDependencyDepth, 0);
|
|
682
|
-
const graphIndex = buildGraphIndex(context.document);
|
|
929
|
+
const graphIndex = buildGraphIndex(context.document, context.ontology);
|
|
683
930
|
const focusElement = focusResult.element;
|
|
684
931
|
const includedElementIds = new Set([focusElement.id]);
|
|
685
932
|
const includedRelationshipIds = new Set();
|
|
@@ -794,7 +1041,7 @@ function buildIntentElementContext(context, args = {}) {
|
|
|
794
1041
|
};
|
|
795
1042
|
const projection = buildAgentProjection(omitted);
|
|
796
1043
|
if (projection) result.projection = projection;
|
|
797
|
-
return result;
|
|
1044
|
+
return applyContextBudget(result, resolveContextMaxBytes(args));
|
|
798
1045
|
}
|
|
799
1046
|
|
|
800
1047
|
// ---------------------------------------------------------------------------
|
|
@@ -938,7 +1185,7 @@ function buildViewContext(context, args = {}) {
|
|
|
938
1185
|
}
|
|
939
1186
|
const projection = buildAgentProjection(omitted);
|
|
940
1187
|
if (projection) result.projection = projection;
|
|
941
|
-
return result;
|
|
1188
|
+
return applyViewContextBudget(result, resolveContextMaxBytes(args));
|
|
942
1189
|
}
|
|
943
1190
|
|
|
944
1191
|
function resolveFocusElement(document, args) {
|
|
@@ -991,7 +1238,7 @@ function normalizeDepth(value, defaultValue) {
|
|
|
991
1238
|
return Math.floor(numericValue);
|
|
992
1239
|
}
|
|
993
1240
|
|
|
994
|
-
function buildGraphIndex(document) {
|
|
1241
|
+
function buildGraphIndex(document, ontology) {
|
|
995
1242
|
const relationshipById = new Map();
|
|
996
1243
|
const elementById = new Map((document.elements || []).map(element => [element.id, element]));
|
|
997
1244
|
const relationshipsByElementId = new Map();
|
|
@@ -1000,9 +1247,19 @@ function buildGraphIndex(document) {
|
|
|
1000
1247
|
addIndexedRelationship(relationshipsByElementId, relationship.source_id, relationship);
|
|
1001
1248
|
addIndexedRelationship(relationshipsByElementId, relationship.target_id, relationship);
|
|
1002
1249
|
}
|
|
1003
|
-
|
|
1250
|
+
const deliveryDependencies = ontology && ontology.deliveryDependencies
|
|
1251
|
+
? ontology.deliveryDependencies
|
|
1252
|
+
: DEFAULT_DELIVERY_DEPENDENCIES;
|
|
1253
|
+
return { elementById, relationshipById, relationshipsByElementId, deliveryDependencies };
|
|
1004
1254
|
}
|
|
1005
1255
|
|
|
1256
|
+
// Dependency direction for the semantic-edge walk comes from the active schema
|
|
1257
|
+
// bundle's deliveryDependencies; this is the ArchiMate 3.2 fallback by default.
|
|
1258
|
+
const DEFAULT_DELIVERY_DEPENDENCIES = Object.freeze({
|
|
1259
|
+
sourceDependsOnTarget: Object.freeze(['Access', 'Assignment', 'Specialization', 'Composition', 'Aggregation']),
|
|
1260
|
+
targetDependsOnSource: Object.freeze(['Serving', 'Realization', 'Flow', 'Triggering', 'Influence']),
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1006
1263
|
function addIndexedRelationship(index, elementId, relationship) {
|
|
1007
1264
|
if (!index.has(elementId)) {
|
|
1008
1265
|
index.set(elementId, []);
|
|
@@ -1057,8 +1314,9 @@ function resolveSemanticEdges(elementId, graphIndex) {
|
|
|
1057
1314
|
continue;
|
|
1058
1315
|
}
|
|
1059
1316
|
|
|
1060
|
-
const
|
|
1061
|
-
const
|
|
1317
|
+
const deliveryDependencies = graphIndex.deliveryDependencies || DEFAULT_DELIVERY_DEPENDENCIES;
|
|
1318
|
+
const sourceDependsOnTarget = deliveryDependencies.sourceDependsOnTarget.includes(relationshipType);
|
|
1319
|
+
const targetDependsOnSource = deliveryDependencies.targetDependsOnSource.includes(relationshipType);
|
|
1062
1320
|
if (sourceDependsOnTarget) {
|
|
1063
1321
|
edges.push({ kind: isSource ? 'dependency' : 'dependent', neighborId, relationship });
|
|
1064
1322
|
continue;
|
|
@@ -1368,6 +1626,207 @@ function resolveDuplicateConflict(options, candidates) {
|
|
|
1368
1626
|
return { action: 'create', justification: options.justification };
|
|
1369
1627
|
}
|
|
1370
1628
|
|
|
1629
|
+
function mutationNodeId(mutation) {
|
|
1630
|
+
if (!mutation || typeof mutation !== 'object') {
|
|
1631
|
+
return '(invalid)';
|
|
1632
|
+
}
|
|
1633
|
+
if (mutation.type === 'addElement') {
|
|
1634
|
+
return (mutation.element && mutation.element.id) || '(addElement)';
|
|
1635
|
+
}
|
|
1636
|
+
if (mutation.type === 'addRelationship') {
|
|
1637
|
+
return (mutation.relationship && mutation.relationship.id) || '(addRelationship)';
|
|
1638
|
+
}
|
|
1639
|
+
if (mutation.type === 'addView') {
|
|
1640
|
+
return (mutation.view && mutation.view.view_id) || '(addView)';
|
|
1641
|
+
}
|
|
1642
|
+
if (mutation.type === 'updateView') {
|
|
1643
|
+
return mutation.view_id || mutation.id || '(updateView)';
|
|
1644
|
+
}
|
|
1645
|
+
return mutation.id || mutation.view_id || `(${mutation.type})`;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// Order a batch so it is order-independent: a mutation that references an object
|
|
1649
|
+
// created later in the SAME batch is applied after its producer. Dependencies:
|
|
1650
|
+
// addElement -> views it joins (view_ids)
|
|
1651
|
+
// addRelationship-> views it joins + its source/target elements
|
|
1652
|
+
// addView -> its parent element + included elements/relationships
|
|
1653
|
+
// update*/remove*-> the object they target (when that object is produced here)
|
|
1654
|
+
// A genuine cycle is reported with the id chain so the caller can split the batch.
|
|
1655
|
+
function orderMutationsForApplication(mutations) {
|
|
1656
|
+
const n = mutations.length;
|
|
1657
|
+
const producers = new Map();
|
|
1658
|
+
const addProducer = (key, index) => {
|
|
1659
|
+
if (!key) {
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
if (!producers.has(key)) {
|
|
1663
|
+
producers.set(key, []);
|
|
1664
|
+
}
|
|
1665
|
+
producers.get(key).push(index);
|
|
1666
|
+
};
|
|
1667
|
+
|
|
1668
|
+
mutations.forEach((mutation, index) => {
|
|
1669
|
+
if (!mutation || typeof mutation !== 'object') {
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
if (mutation.type === 'addElement' && mutation.element) {
|
|
1673
|
+
addProducer('element:' + mutation.element.id, index);
|
|
1674
|
+
}
|
|
1675
|
+
if (mutation.type === 'addRelationship' && mutation.relationship) {
|
|
1676
|
+
addProducer('relationship:' + mutation.relationship.id, index);
|
|
1677
|
+
}
|
|
1678
|
+
if (mutation.type === 'addView' && mutation.view) {
|
|
1679
|
+
addProducer('view:' + mutation.view.view_id, index);
|
|
1680
|
+
}
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
const dependencies = mutations.map(() => new Set());
|
|
1684
|
+
mutations.forEach((mutation, index) => {
|
|
1685
|
+
if (!mutation || typeof mutation !== 'object') {
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
const requireKey = (key) => {
|
|
1689
|
+
const matches = producers.get(key);
|
|
1690
|
+
if (!matches) {
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
for (const producerIndex of matches) {
|
|
1694
|
+
if (producerIndex !== index) {
|
|
1695
|
+
dependencies[index].add(producerIndex);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1699
|
+
|
|
1700
|
+
if (mutation.type === 'addElement') {
|
|
1701
|
+
for (const viewId of Array.isArray(mutation.view_ids) ? mutation.view_ids : []) {
|
|
1702
|
+
requireKey('view:' + viewId);
|
|
1703
|
+
}
|
|
1704
|
+
} else if (mutation.type === 'addRelationship') {
|
|
1705
|
+
for (const viewId of Array.isArray(mutation.view_ids) ? mutation.view_ids : []) {
|
|
1706
|
+
requireKey('view:' + viewId);
|
|
1707
|
+
}
|
|
1708
|
+
if (mutation.relationship) {
|
|
1709
|
+
requireKey('element:' + mutation.relationship.source_id);
|
|
1710
|
+
requireKey('element:' + mutation.relationship.target_id);
|
|
1711
|
+
}
|
|
1712
|
+
} else if (mutation.type === 'addView') {
|
|
1713
|
+
if (mutation.view) {
|
|
1714
|
+
requireKey('element:' + mutation.view.parent_element_id);
|
|
1715
|
+
for (const elementId of Array.isArray(mutation.view.included_elements) ? mutation.view.included_elements : []) {
|
|
1716
|
+
requireKey('element:' + elementId);
|
|
1717
|
+
}
|
|
1718
|
+
for (const relationshipId of Array.isArray(mutation.view.included_relationships) ? mutation.view.included_relationships : []) {
|
|
1719
|
+
requireKey('relationship:' + relationshipId);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
} else if (mutation.type === 'updateElement') {
|
|
1723
|
+
requireKey('element:' + mutation.id);
|
|
1724
|
+
} else if (mutation.type === 'updateRelationship') {
|
|
1725
|
+
requireKey('relationship:' + mutation.id);
|
|
1726
|
+
} else if (mutation.type === 'updateView') {
|
|
1727
|
+
requireKey('view:' + (mutation.view_id || mutation.id));
|
|
1728
|
+
} else if (mutation.type === 'removeView') {
|
|
1729
|
+
requireKey('view:' + mutation.view_id);
|
|
1730
|
+
} else if (mutation.type === 'removeElement') {
|
|
1731
|
+
requireKey('element:' + mutation.id);
|
|
1732
|
+
} else if (mutation.type === 'removeRelationship') {
|
|
1733
|
+
requireKey('relationship:' + mutation.id);
|
|
1734
|
+
}
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
const indegree = mutations.map((_, index) => dependencies[index].size);
|
|
1738
|
+
const dependents = mutations.map(() => []);
|
|
1739
|
+
dependencies.forEach((set, index) => {
|
|
1740
|
+
for (const dependency of set) {
|
|
1741
|
+
dependents[dependency].push(index);
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1745
|
+
const remaining = new Set();
|
|
1746
|
+
for (let index = 0; index < n; index += 1) {
|
|
1747
|
+
remaining.add(index);
|
|
1748
|
+
}
|
|
1749
|
+
const ready = [];
|
|
1750
|
+
for (let index = 0; index < n; index += 1) {
|
|
1751
|
+
if (indegree[index] === 0) {
|
|
1752
|
+
ready.push(index);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
ready.sort((a, b) => a - b);
|
|
1756
|
+
|
|
1757
|
+
const ordered = [];
|
|
1758
|
+
while (ready.length > 0) {
|
|
1759
|
+
const index = ready.shift();
|
|
1760
|
+
if (!remaining.has(index)) {
|
|
1761
|
+
continue;
|
|
1762
|
+
}
|
|
1763
|
+
remaining.delete(index);
|
|
1764
|
+
ordered.push(mutations[index]);
|
|
1765
|
+
for (const dependent of dependents[index]) {
|
|
1766
|
+
indegree[dependent] -= 1;
|
|
1767
|
+
if (indegree[dependent] === 0) {
|
|
1768
|
+
ready.push(dependent);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
ready.sort((a, b) => a - b);
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
if (ordered.length !== n) {
|
|
1775
|
+
const chain = findMutationCycle(mutations, dependencies, remaining);
|
|
1776
|
+
throw new Error(
|
|
1777
|
+
`Cyclic mutation dependencies in batch: ${chain.join(' -> ')}. `
|
|
1778
|
+
+ 'Order the batch so each referenced object is created first, or split the mutually-dependent objects into separate calls.',
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
return ordered;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
function findMutationCycle(mutations, dependencies, remaining) {
|
|
1786
|
+
const visited = new Set();
|
|
1787
|
+
const inStack = new Set();
|
|
1788
|
+
const stack = [];
|
|
1789
|
+
let cycle = null;
|
|
1790
|
+
|
|
1791
|
+
const visit = (index) => {
|
|
1792
|
+
if (cycle) {
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
visited.add(index);
|
|
1796
|
+
inStack.add(index);
|
|
1797
|
+
stack.push(index);
|
|
1798
|
+
for (const dependency of dependencies[index]) {
|
|
1799
|
+
if (!remaining.has(dependency)) {
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
if (inStack.has(dependency)) {
|
|
1803
|
+
cycle = stack.slice(stack.indexOf(dependency)).concat(dependency);
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
if (!visited.has(dependency)) {
|
|
1807
|
+
visit(dependency);
|
|
1808
|
+
}
|
|
1809
|
+
if (cycle) {
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
inStack.delete(index);
|
|
1814
|
+
stack.pop();
|
|
1815
|
+
};
|
|
1816
|
+
|
|
1817
|
+
for (const index of remaining) {
|
|
1818
|
+
if (!visited.has(index)) {
|
|
1819
|
+
visit(index);
|
|
1820
|
+
}
|
|
1821
|
+
if (cycle) {
|
|
1822
|
+
break;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
const chain = cycle || [...remaining];
|
|
1827
|
+
return chain.map((index) => mutationNodeId(mutations[index]));
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1371
1830
|
function applyMutations(document, mutations, options = {}) {
|
|
1372
1831
|
const nextDocument = clone(document);
|
|
1373
1832
|
const touchedElementIds = new Set();
|
|
@@ -1380,7 +1839,11 @@ function applyMutations(document, mutations, options = {}) {
|
|
|
1380
1839
|
throw new Error('mutations must contain at least one mutation');
|
|
1381
1840
|
}
|
|
1382
1841
|
|
|
1383
|
-
|
|
1842
|
+
// Apply in dependency order so a batch is order-independent (see
|
|
1843
|
+
// orderMutationsForApplication); a genuine cycle throws with the id chain.
|
|
1844
|
+
const orderedMutations = orderMutationsForApplication(mutations);
|
|
1845
|
+
|
|
1846
|
+
for (const mutation of orderedMutations) {
|
|
1384
1847
|
if (!mutation || typeof mutation !== 'object' || !HANDLED_MUTATION_TYPES.has(mutation.type)) {
|
|
1385
1848
|
throw new Error(`Unsupported mutation type: ${mutation && mutation.type}`);
|
|
1386
1849
|
}
|
|
@@ -1388,11 +1851,30 @@ function applyMutations(document, mutations, options = {}) {
|
|
|
1388
1851
|
if (mutation.type === 'addElement') {
|
|
1389
1852
|
requireObject(mutation.element, 'mutation.element');
|
|
1390
1853
|
const scopedViews = requireViewScope(nextDocument.views, mutation.view_ids, 'mutation.view_ids');
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
let
|
|
1395
|
-
|
|
1854
|
+
const requestedElementId = typeof mutation.element.id === 'string' && mutation.element.id !== ''
|
|
1855
|
+
? mutation.element.id
|
|
1856
|
+
: null;
|
|
1857
|
+
let targetElementId;
|
|
1858
|
+
let elementCreated = false;
|
|
1859
|
+
let elementReused = false;
|
|
1860
|
+
|
|
1861
|
+
const existingById = requestedElementId ? findById(nextDocument.elements, requestedElementId) : undefined;
|
|
1862
|
+
if (existingById) {
|
|
1863
|
+
// An explicit id that already exists: idempotent ONLY if it is the same
|
|
1864
|
+
// (type,name). Otherwise the caller picked a taken id — fail loudly instead
|
|
1865
|
+
// of silently attaching an unrelated element (issue #8 collision safety).
|
|
1866
|
+
const sameIdentity = existingById.type === mutation.element.type
|
|
1867
|
+
&& normalizeDedupName(existingById.name) === normalizeDedupName(mutation.element.name);
|
|
1868
|
+
if (!sameIdentity) {
|
|
1869
|
+
throw new Error(
|
|
1870
|
+
`addElement id '${requestedElementId}' is already used by a different element `
|
|
1871
|
+
+ `(type '${existingById.type}', name '${existingById.name}'). Choose another id (or omit id to auto-allocate one), `
|
|
1872
|
+
+ `attach the existing element by using its id, or update it with updateArchitectureElement.`,
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1875
|
+
targetElementId = requestedElementId;
|
|
1876
|
+
elementReused = true;
|
|
1877
|
+
} else {
|
|
1396
1878
|
const candidates = findDuplicateElements(nextDocument.elements, mutation.element);
|
|
1397
1879
|
const resolution = resolveDuplicateConflict({
|
|
1398
1880
|
onConflict: mutation.onConflict,
|
|
@@ -1401,12 +1883,19 @@ function applyMutations(document, mutations, options = {}) {
|
|
|
1401
1883
|
}, candidates);
|
|
1402
1884
|
if (resolution.action === 'reuse') {
|
|
1403
1885
|
targetElementId = resolution.existing.id;
|
|
1404
|
-
|
|
1886
|
+
elementReused = true;
|
|
1405
1887
|
} else {
|
|
1406
|
-
|
|
1407
|
-
|
|
1888
|
+
targetElementId = requestedElementId || allocateUniqueId(
|
|
1889
|
+
nextDocument.elements.map(entry => entry.id),
|
|
1890
|
+
mutation.element.name,
|
|
1891
|
+
slugifyId(mutation.element.type) || 'element',
|
|
1892
|
+
);
|
|
1893
|
+
nextDocument.elements.push({ ...clone(mutation.element), id: targetElementId });
|
|
1894
|
+
syncViewsToElementSubdiagramViews(nextDocument, findById(nextDocument.elements, targetElementId));
|
|
1895
|
+
elementCreated = true;
|
|
1408
1896
|
}
|
|
1409
1897
|
}
|
|
1898
|
+
|
|
1410
1899
|
for (const view of scopedViews) {
|
|
1411
1900
|
view.included_elements = addUnique(view.included_elements || [], [targetElementId]);
|
|
1412
1901
|
touchedViewIds.add(view.view_id);
|
|
@@ -1417,8 +1906,10 @@ function applyMutations(document, mutations, options = {}) {
|
|
|
1417
1906
|
type: mutation.type,
|
|
1418
1907
|
id: targetElementId,
|
|
1419
1908
|
view_ids: mutation.view_ids,
|
|
1420
|
-
created:
|
|
1421
|
-
...(
|
|
1909
|
+
created: elementCreated,
|
|
1910
|
+
...(elementReused ? { reused: true, reusedId: targetElementId } : {}),
|
|
1911
|
+
...(requestedElementId === null && elementCreated ? { allocatedId: targetElementId } : {}),
|
|
1912
|
+
...(elementReused && requestedElementId && requestedElementId !== targetElementId ? { requestedId: requestedElementId } : {}),
|
|
1422
1913
|
});
|
|
1423
1914
|
continue;
|
|
1424
1915
|
}
|
|
@@ -1517,13 +2008,35 @@ function applyMutations(document, mutations, options = {}) {
|
|
|
1517
2008
|
if (mutation.type === 'addRelationship') {
|
|
1518
2009
|
requireObject(mutation.relationship, 'mutation.relationship');
|
|
1519
2010
|
const scopedViews = requireViewScope(nextDocument.views, mutation.view_ids, 'mutation.view_ids');
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
2011
|
+
const requestedRelationshipId = typeof mutation.relationship.id === 'string' && mutation.relationship.id !== ''
|
|
2012
|
+
? mutation.relationship.id
|
|
2013
|
+
: null;
|
|
2014
|
+
let targetRelationshipId;
|
|
1523
2015
|
let sourceElementId = mutation.relationship.source_id;
|
|
1524
2016
|
let targetEndpointId = mutation.relationship.target_id;
|
|
1525
|
-
let
|
|
1526
|
-
|
|
2017
|
+
let relationshipCreated = false;
|
|
2018
|
+
let relationshipReused = false;
|
|
2019
|
+
|
|
2020
|
+
const existingById = requestedRelationshipId ? findById(nextDocument.relationships, requestedRelationshipId) : undefined;
|
|
2021
|
+
if (existingById) {
|
|
2022
|
+
// Explicit id that already exists: idempotent only for the same natural key;
|
|
2023
|
+
// otherwise fail loudly instead of silently attaching an unrelated edge.
|
|
2024
|
+
const sameIdentity = existingById.source_id === mutation.relationship.source_id
|
|
2025
|
+
&& existingById.type === mutation.relationship.type
|
|
2026
|
+
&& existingById.target_id === mutation.relationship.target_id
|
|
2027
|
+
&& normalizeDedupName(existingById.name) === normalizeDedupName(mutation.relationship.name);
|
|
2028
|
+
if (!sameIdentity) {
|
|
2029
|
+
throw new Error(
|
|
2030
|
+
`addRelationship id '${requestedRelationshipId}' is already used by a different relationship `
|
|
2031
|
+
+ `(source '${existingById.source_id}', type '${existingById.type}', target '${existingById.target_id}'). `
|
|
2032
|
+
+ 'Choose another id (or omit id to auto-allocate one), or update it with updateArchitectureRelationship.',
|
|
2033
|
+
);
|
|
2034
|
+
}
|
|
2035
|
+
targetRelationshipId = requestedRelationshipId;
|
|
2036
|
+
sourceElementId = existingById.source_id;
|
|
2037
|
+
targetEndpointId = existingById.target_id;
|
|
2038
|
+
relationshipReused = true;
|
|
2039
|
+
} else {
|
|
1527
2040
|
const candidates = findDuplicateRelationships(nextDocument.relationships, mutation.relationship);
|
|
1528
2041
|
const resolution = resolveDuplicateConflict({
|
|
1529
2042
|
onConflict: mutation.onConflict,
|
|
@@ -1534,9 +2047,15 @@ function applyMutations(document, mutations, options = {}) {
|
|
|
1534
2047
|
targetRelationshipId = resolution.existing.id;
|
|
1535
2048
|
sourceElementId = resolution.existing.source_id;
|
|
1536
2049
|
targetEndpointId = resolution.existing.target_id;
|
|
1537
|
-
|
|
2050
|
+
relationshipReused = true;
|
|
1538
2051
|
} else {
|
|
1539
|
-
|
|
2052
|
+
targetRelationshipId = requestedRelationshipId || allocateUniqueId(
|
|
2053
|
+
nextDocument.relationships.map(entry => entry.id),
|
|
2054
|
+
mutation.relationship.name || `${mutation.relationship.source_id}-${mutation.relationship.type}-${mutation.relationship.target_id}`,
|
|
2055
|
+
'relationship',
|
|
2056
|
+
);
|
|
2057
|
+
nextDocument.relationships.push({ ...clone(mutation.relationship), id: targetRelationshipId });
|
|
2058
|
+
relationshipCreated = true;
|
|
1540
2059
|
}
|
|
1541
2060
|
}
|
|
1542
2061
|
for (const view of scopedViews) {
|
|
@@ -1552,8 +2071,9 @@ function applyMutations(document, mutations, options = {}) {
|
|
|
1552
2071
|
type: mutation.type,
|
|
1553
2072
|
id: targetRelationshipId,
|
|
1554
2073
|
view_ids: mutation.view_ids,
|
|
1555
|
-
created:
|
|
1556
|
-
...(
|
|
2074
|
+
created: relationshipCreated,
|
|
2075
|
+
...(relationshipReused ? { reused: true, reusedId: targetRelationshipId } : {}),
|
|
2076
|
+
...(requestedRelationshipId === null && relationshipCreated ? { allocatedId: targetRelationshipId } : {}),
|
|
1557
2077
|
});
|
|
1558
2078
|
continue;
|
|
1559
2079
|
}
|
|
@@ -1637,38 +2157,65 @@ function applyMutations(document, mutations, options = {}) {
|
|
|
1637
2157
|
|
|
1638
2158
|
if (mutation.type === 'addView') {
|
|
1639
2159
|
requireObject(mutation.view, 'mutation.view');
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
const newView = clone(mutation.view);
|
|
1660
|
-
if (Array.isArray(newView.included_elements)) {
|
|
1661
|
-
newView.included_elements = addUnique([], newView.included_elements);
|
|
2160
|
+
const requestedViewId = typeof mutation.view.view_id === 'string' && mutation.view.view_id !== ''
|
|
2161
|
+
? mutation.view.view_id
|
|
2162
|
+
: null;
|
|
2163
|
+
let targetViewId;
|
|
2164
|
+
let viewCreated = false;
|
|
2165
|
+
let viewReused = false;
|
|
2166
|
+
|
|
2167
|
+
const existingById = requestedViewId ? findView(nextDocument.views, requestedViewId) : undefined;
|
|
2168
|
+
if (existingById) {
|
|
2169
|
+
// Explicit view_id that already exists: idempotent only for the same
|
|
2170
|
+
// (parent,name); otherwise fail loudly.
|
|
2171
|
+
const sameIdentity = (existingById.parent_element_id || '') === (mutation.view.parent_element_id || '')
|
|
2172
|
+
&& normalizeDedupName(existingById.view_name) === normalizeDedupName(mutation.view.view_name);
|
|
2173
|
+
if (!sameIdentity) {
|
|
2174
|
+
throw new Error(
|
|
2175
|
+
`addView view_id '${requestedViewId}' is already used by a different view `
|
|
2176
|
+
+ `(name '${existingById.view_name}', parent '${existingById.parent_element_id || ''}'). `
|
|
2177
|
+
+ 'Choose another view_id (or omit it to auto-allocate one), or update it with updateArchitectureView.',
|
|
2178
|
+
);
|
|
1662
2179
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
2180
|
+
targetViewId = requestedViewId;
|
|
2181
|
+
viewReused = true;
|
|
2182
|
+
} else {
|
|
2183
|
+
const candidates = findDuplicateViews(nextDocument.views, mutation.view);
|
|
2184
|
+
const resolution = resolveDuplicateConflict({
|
|
2185
|
+
onConflict: mutation.onConflict,
|
|
2186
|
+
justification: mutation.justification,
|
|
2187
|
+
label: `view (name '${mutation.view.view_name}')`,
|
|
2188
|
+
}, candidates);
|
|
2189
|
+
if (resolution.action === 'reuse') {
|
|
2190
|
+
targetViewId = resolution.existing.view_id;
|
|
2191
|
+
viewReused = true;
|
|
2192
|
+
} else {
|
|
2193
|
+
targetViewId = requestedViewId || allocateUniqueId(
|
|
2194
|
+
nextDocument.views.map(entry => entry.view_id),
|
|
2195
|
+
mutation.view.view_name,
|
|
2196
|
+
'view',
|
|
2197
|
+
);
|
|
2198
|
+
const newView = { ...clone(mutation.view), view_id: targetViewId };
|
|
2199
|
+
if (Array.isArray(newView.included_elements)) {
|
|
2200
|
+
newView.included_elements = addUnique([], newView.included_elements);
|
|
2201
|
+
}
|
|
2202
|
+
if (Array.isArray(newView.included_relationships)) {
|
|
2203
|
+
newView.included_relationships = addUnique([], newView.included_relationships);
|
|
2204
|
+
}
|
|
2205
|
+
nextDocument.views.push(newView);
|
|
2206
|
+
upsertSubdiagramViewIntoElement(nextDocument, newView.parent_element_id, newView);
|
|
2207
|
+
touchedViewIds.add(newView.view_id);
|
|
2208
|
+
viewLimitCheckIds.add(newView.view_id);
|
|
2209
|
+
viewCreated = true;
|
|
1665
2210
|
}
|
|
1666
|
-
nextDocument.views.push(newView);
|
|
1667
|
-
upsertSubdiagramViewIntoElement(nextDocument, newView.parent_element_id, newView);
|
|
1668
|
-
touchedViewIds.add(newView.view_id);
|
|
1669
|
-
viewLimitCheckIds.add(newView.view_id);
|
|
1670
|
-
mutationSummaries.push({ type: mutation.type, id: newView.view_id });
|
|
1671
2211
|
}
|
|
2212
|
+
mutationSummaries.push({
|
|
2213
|
+
type: mutation.type,
|
|
2214
|
+
id: targetViewId,
|
|
2215
|
+
created: viewCreated,
|
|
2216
|
+
...(viewReused ? { reused: true, reusedId: targetViewId } : {}),
|
|
2217
|
+
...(requestedViewId === null && viewCreated ? { allocatedId: targetViewId } : {}),
|
|
2218
|
+
});
|
|
1672
2219
|
continue;
|
|
1673
2220
|
}
|
|
1674
2221
|
|
|
@@ -1740,6 +2287,33 @@ function requireObject(value, label) {
|
|
|
1740
2287
|
}
|
|
1741
2288
|
}
|
|
1742
2289
|
|
|
2290
|
+
function slugifyId(seed) {
|
|
2291
|
+
return String(seed == null ? '' : seed)
|
|
2292
|
+
.toLowerCase()
|
|
2293
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
2294
|
+
.replace(/-{2,}/g, '-')
|
|
2295
|
+
.replace(/^-+|-+$/g, '');
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
// One unified id strategy for the whole framework (issue #8): a semantic slug
|
|
2299
|
+
// derived from the entity's human name, made unique within its collection by a
|
|
2300
|
+
// numeric suffix. Callers MAY omit an id; the server allocates one. There is no
|
|
2301
|
+
// per-repository id strategy — ArchGraph has a single one.
|
|
2302
|
+
function allocateUniqueId(existingIds, seed, fallbackBase) {
|
|
2303
|
+
const taken = new Set((Array.isArray(existingIds) ? existingIds : []).map(String));
|
|
2304
|
+
const base = slugifyId(seed) || fallbackBase || 'item';
|
|
2305
|
+
if (!taken.has(base)) {
|
|
2306
|
+
return base;
|
|
2307
|
+
}
|
|
2308
|
+
for (let n = 2; n < 100000; n += 1) {
|
|
2309
|
+
const candidate = `${base}-${String(n).padStart(3, '0')}`;
|
|
2310
|
+
if (!taken.has(candidate)) {
|
|
2311
|
+
return candidate;
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
return `${base}-${Date.now()}`;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
1743
2317
|
function requireId(value, label) {
|
|
1744
2318
|
if (typeof value !== 'string' || value.length === 0) {
|
|
1745
2319
|
throw new Error(`${label} must be a non-empty string`);
|
|
@@ -1930,7 +2504,7 @@ async function buildMutationResult(context, mutations, write, dependencies, loss
|
|
|
1930
2504
|
before: beforeSummary,
|
|
1931
2505
|
after: beforeSummary,
|
|
1932
2506
|
errors,
|
|
1933
|
-
guidance: buildFailureGuidance(errors),
|
|
2507
|
+
guidance: buildFailureGuidance(errors, context.ontology),
|
|
1934
2508
|
};
|
|
1935
2509
|
if (error && Array.isArray(error.duplicateConflicts) && error.duplicateConflicts.length > 0) {
|
|
1936
2510
|
failed.duplicateConflicts = error.duplicateConflicts;
|
|
@@ -1941,6 +2515,7 @@ async function buildMutationResult(context, mutations, write, dependencies, loss
|
|
|
1941
2515
|
return failed;
|
|
1942
2516
|
}
|
|
1943
2517
|
const errors = validateDocument(mutationResult.document, context.schema, {
|
|
2518
|
+
ontology: context.ontology,
|
|
1944
2519
|
touchedRelationshipIds: mutationResult.touchedRelationshipIds,
|
|
1945
2520
|
touchedViewIds: mutationResult.viewLimitCheckIds,
|
|
1946
2521
|
});
|
|
@@ -1960,7 +2535,7 @@ async function buildMutationResult(context, mutations, write, dependencies, loss
|
|
|
1960
2535
|
errors,
|
|
1961
2536
|
};
|
|
1962
2537
|
if (errors.length > 0) {
|
|
1963
|
-
result.guidance = buildFailureGuidance(errors);
|
|
2538
|
+
result.guidance = buildFailureGuidance(errors, context.ontology);
|
|
1964
2539
|
}
|
|
1965
2540
|
|
|
1966
2541
|
// Semantic dedup gate: a would-be element create is blocked when a same-type
|
|
@@ -2215,10 +2790,10 @@ function buildMutationEmbeddingLifecycleFailure(error, result) {
|
|
|
2215
2790
|
});
|
|
2216
2791
|
}
|
|
2217
2792
|
|
|
2218
|
-
function buildFailureGuidance(errors) {
|
|
2793
|
+
function buildFailureGuidance(errors, ontology) {
|
|
2219
2794
|
const guidance = [];
|
|
2220
2795
|
for (const error of errors || []) {
|
|
2221
|
-
addGuidanceForError(guidance, String(error));
|
|
2796
|
+
addGuidanceForError(guidance, String(error), ontology);
|
|
2222
2797
|
}
|
|
2223
2798
|
if (guidance.length === 0 && Array.isArray(errors) && errors.length > 0) {
|
|
2224
2799
|
guidance.push('Inspect the error text, call getSystemArchitecture with an explicit semantic query to refresh relevant ids, use getIntentElementContext for focused dependency context when needed, then retry with previewSystemArchitectureMutation before writing. Use an omitted-query full snapshot only when exact complete view membership is required.');
|
|
@@ -2226,15 +2801,17 @@ function buildFailureGuidance(errors) {
|
|
|
2226
2801
|
return guidance;
|
|
2227
2802
|
}
|
|
2228
2803
|
|
|
2229
|
-
function addGuidanceForError(guidance, error) {
|
|
2804
|
+
function addGuidanceForError(guidance, error, ontology) {
|
|
2805
|
+
const language = ontology && ontology.language ? ontology.language : 'ArchiMate 3.2';
|
|
2806
|
+
const matrixLabel = ontology && ontology.matrixErrorLabel ? ontology.matrixErrorLabel : 'ArchiMate 3.2 relationship matrix';
|
|
2230
2807
|
if (error.includes('mutation.view_ids must contain at least one view id')) {
|
|
2231
2808
|
pushUnique(guidance, 'Select the target view_ids explicitly. Prefer getSystemArchitecture with an explicit semantic query to find relevant views, then use getIntentElementContext for focused element dependencies when needed. Use a full snapshot only if exact complete view membership is required.');
|
|
2232
2809
|
}
|
|
2233
|
-
if (error.includes(
|
|
2234
|
-
pushUnique(guidance,
|
|
2810
|
+
if (error.includes(`violates ${matrixLabel}`)) {
|
|
2811
|
+
pushUnique(guidance, `Check relationship.type and the source and target element types against ${language}. If the intended meaning is still valid, choose a compliant relationship type or change the endpoint element types by remove-and-add.`);
|
|
2235
2812
|
}
|
|
2236
|
-
if (error.includes('uses unsupported
|
|
2237
|
-
pushUnique(guidance,
|
|
2813
|
+
if (error.includes('uses unsupported') && error.includes('relationship type')) {
|
|
2814
|
+
pushUnique(guidance, `Use relationship.type for the relationship type and choose one of the schema-supported ${language} relationship types.`);
|
|
2238
2815
|
}
|
|
2239
2816
|
if (error.includes('id cannot be updated') || error.includes('type cannot be updated')) {
|
|
2240
2817
|
pushUnique(guidance, 'Do not patch immutable identity or type fields. To change an id or type, remove the existing element or relationship, then add the replacement with the desired id or type.');
|
|
@@ -2243,10 +2820,16 @@ function addGuidanceForError(guidance, error) {
|
|
|
2243
2820
|
pushUnique(guidance, 'Every element and relationship must belong to at least one view. Add it with view_ids, or add the existing object to an appropriate view before validating again.');
|
|
2244
2821
|
}
|
|
2245
2822
|
if (error.includes('must declare parent_element_id') || error.includes('top-level view')) {
|
|
2246
|
-
|
|
2823
|
+
const rootViewName = ontology && ontology.invariants && ontology.invariants.rootViewName
|
|
2824
|
+
? ontology.invariants.rootViewName
|
|
2825
|
+
: 'SystemArchitecture';
|
|
2826
|
+
pushUnique(guidance, `Keep exactly one top-level view${rootViewName ? ` named ${rootViewName}` : ''}. For any sub-view, set parent_element_id to an existing element and keep parent_element_name aligned with that element name.`);
|
|
2247
2827
|
}
|
|
2248
|
-
if (error.includes('must contain at most
|
|
2249
|
-
|
|
2828
|
+
if (error.includes('must contain at most') && error.includes('elements')) {
|
|
2829
|
+
const maxElements = ontology && ontology.invariants && ontology.invariants.maxElementsPerView !== undefined && ontology.invariants.maxElementsPerView !== null
|
|
2830
|
+
? ontology.invariants.maxElementsPerView
|
|
2831
|
+
: 15;
|
|
2832
|
+
pushUnique(guidance, `Do not force more than ${maxElements} included_elements into one view. Pause and think about layered architecture: split the view into layered sub-views, attach each sub-view with parent_element_id, and move lower-level elements into the appropriate child view before retrying.`);
|
|
2250
2833
|
}
|
|
2251
2834
|
if (error.includes('does not exist') || error.includes('references missing')) {
|
|
2252
2835
|
pushUnique(guidance, 'Refresh current ids with getSystemArchitecture semantic query first, then call getIntentElementContext for any returned element that needs dependency context. Do not guess ids; use existing element, relationship, and view ids or create missing objects first.');
|
|
@@ -2625,6 +3208,18 @@ function compactMutationResponse(payload) {
|
|
|
2625
3208
|
status: payload && payload.status,
|
|
2626
3209
|
written: Boolean(payload && payload.written),
|
|
2627
3210
|
};
|
|
3211
|
+
// A successful write must tell the caller which id was used/allocated and whether
|
|
3212
|
+
// it created or reused — otherwise an omitted (auto-allocated) id is invisible
|
|
3213
|
+
// and an id collision would look like a create. Kept to a few fields (issue #8).
|
|
3214
|
+
if (Array.isArray(payload && payload.mutations) && payload.mutations.length > 0) {
|
|
3215
|
+
compact.mutations = payload.mutations.map((entry) => ({
|
|
3216
|
+
type: entry.type,
|
|
3217
|
+
id: entry.id,
|
|
3218
|
+
created: entry.created,
|
|
3219
|
+
...(entry.reused ? { reused: true, reusedId: entry.reusedId } : {}),
|
|
3220
|
+
...(entry.allocatedId ? { allocatedId: entry.allocatedId } : {}),
|
|
3221
|
+
}));
|
|
3222
|
+
}
|
|
2628
3223
|
if (payload && payload.embeddingLifecycle && payload.embeddingLifecycle.state) {
|
|
2629
3224
|
compact.embeddingLifecycle = { state: payload.embeddingLifecycle.state };
|
|
2630
3225
|
}
|
|
@@ -2669,15 +3264,51 @@ function compactMutationResponse(payload) {
|
|
|
2669
3264
|
return compact;
|
|
2670
3265
|
}
|
|
2671
3266
|
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
3267
|
+
// The typed getSystemArchitecture output contract (see
|
|
3268
|
+
// GET_SYSTEM_ARCHITECTURE_OUTPUT_SCHEMA). The error variant allows only the
|
|
3269
|
+
// listed keys, so any error (local or mirrored from a cross-project read) is
|
|
3270
|
+
// projected onto that shape with category + message guaranteed. Exposed so the
|
|
3271
|
+
// argo MCP server can build the SAME structuredContent for a cross-project read
|
|
3272
|
+
// (which bypasses this module's callTool) instead of returning a result without
|
|
3273
|
+
// structuredContent — which violates the declared outputSchema (-32600).
|
|
3274
|
+
const GET_SYSTEM_ARCHITECTURE_ERROR_KEYS = Object.freeze([
|
|
3275
|
+
'category', 'message', 'action', 'fullSnapshotFallback', 'state',
|
|
3276
|
+
'canonicalVersion', 'contentVersion', 'indexVersion',
|
|
3277
|
+
'completedChannels', 'missingChannels', 'mismatchedChannels',
|
|
3278
|
+
]);
|
|
3279
|
+
|
|
3280
|
+
function normalizeGetSystemArchitectureError(error) {
|
|
3281
|
+
const source = error && typeof error === 'object' ? error : {};
|
|
3282
|
+
const normalized = {};
|
|
3283
|
+
for (const key of GET_SYSTEM_ARCHITECTURE_ERROR_KEYS) {
|
|
3284
|
+
if (Object.prototype.hasOwnProperty.call(source, key) && source[key] !== undefined) {
|
|
3285
|
+
normalized[key] = source[key];
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
if (typeof normalized.category !== 'string' || normalized.category === '') {
|
|
3289
|
+
normalized.category = 'GET_SYSTEM_ARCHITECTURE_ERROR';
|
|
3290
|
+
}
|
|
3291
|
+
if (typeof normalized.message !== 'string' || normalized.message === '') {
|
|
3292
|
+
normalized.message = typeof source.reason === 'string' && source.reason !== ''
|
|
3293
|
+
? source.reason
|
|
3294
|
+
: 'getSystemArchitecture failed';
|
|
3295
|
+
}
|
|
3296
|
+
return normalized;
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
function buildGetSystemArchitectureStructuredContent(payload) {
|
|
3300
|
+
const failed = Boolean(payload) && payload.status === 'failed';
|
|
3301
|
+
return {
|
|
2675
3302
|
version: '1.0',
|
|
2676
3303
|
mode: failed ? 'error' : 'semantic-query',
|
|
2677
|
-
document: failed ? null : (payload.document === undefined ? null : payload.document),
|
|
2678
|
-
query: failed ? null : (payload.query || null),
|
|
2679
|
-
error: failed ? payload.error : null,
|
|
2680
|
-
}
|
|
3304
|
+
document: failed ? null : (payload && payload.document === undefined ? null : payload.document),
|
|
3305
|
+
query: failed ? null : ((payload && payload.query) || null),
|
|
3306
|
+
error: failed ? normalizeGetSystemArchitectureError(payload.error) : null,
|
|
3307
|
+
};
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
function getSystemArchitectureResult(payload) {
|
|
3311
|
+
return toolResult(payload, buildGetSystemArchitectureStructuredContent(payload));
|
|
2681
3312
|
}
|
|
2682
3313
|
|
|
2683
3314
|
async function callTool(name, args = {}, dependencies = undefined) {
|
|
@@ -3102,13 +3733,25 @@ async function queryNeo4jGraphTool(args = {}) {
|
|
|
3102
3733
|
function queryNeo4jGraphSchemaResult(architecturePath, workspaceRoot) {
|
|
3103
3734
|
const schema = buildNeo4jGraphSchema(architecturePath);
|
|
3104
3735
|
let typeEnums = {};
|
|
3736
|
+
let schemaBundleInfo = {};
|
|
3105
3737
|
try {
|
|
3106
3738
|
const resolvedRoot = workspaceRoot || resolveWorkspaceRoot({ architecturePath });
|
|
3107
|
-
const
|
|
3108
|
-
const
|
|
3739
|
+
const { bundle, ontology } = loadSchemaBundleAndOntology(resolvedRoot);
|
|
3740
|
+
const enums = resolveTypeEnums(bundle);
|
|
3109
3741
|
typeEnums = {
|
|
3110
|
-
archimateElementTypes:
|
|
3111
|
-
archimateRelationshipTypes:
|
|
3742
|
+
archimateElementTypes: enums.elementTypes,
|
|
3743
|
+
archimateRelationshipTypes: enums.relationshipTypes,
|
|
3744
|
+
schemaLanguage: (bundle.config && bundle.config.language) || null,
|
|
3745
|
+
schemaKind: bundle.kind,
|
|
3746
|
+
schemaDialect: ontology.dialect,
|
|
3747
|
+
actorElementType: ontology.actorElementType,
|
|
3748
|
+
bundleValidation: ontology.bundleValidation,
|
|
3749
|
+
};
|
|
3750
|
+
schemaBundleInfo = {
|
|
3751
|
+
schemaPath: bundle.schema.relativePath,
|
|
3752
|
+
schemaKind: bundle.kind,
|
|
3753
|
+
schemaDir: bundle.relativeDir,
|
|
3754
|
+
guidePath: bundle.guidePath ? bundle.guidePath.relativePath : null,
|
|
3112
3755
|
};
|
|
3113
3756
|
} catch (error) {
|
|
3114
3757
|
typeEnums = {
|
|
@@ -3125,6 +3768,7 @@ function queryNeo4jGraphSchemaResult(architecturePath, workspaceRoot) {
|
|
|
3125
3768
|
schema: {
|
|
3126
3769
|
...schema,
|
|
3127
3770
|
...typeEnums,
|
|
3771
|
+
...schemaBundleInfo,
|
|
3128
3772
|
},
|
|
3129
3773
|
usage: {
|
|
3130
3774
|
scopeGraph: 'MATCH (e:Element {graphKey: $graphKey}) ...',
|
|
@@ -4445,8 +5089,12 @@ if (require.main === module) {
|
|
|
4445
5089
|
|
|
4446
5090
|
module.exports = {
|
|
4447
5091
|
GET_SYSTEM_ARCHITECTURE_OUTPUT_SCHEMA,
|
|
5092
|
+
buildGetSystemArchitectureStructuredContent,
|
|
4448
5093
|
TOOLS,
|
|
4449
5094
|
applyMutations,
|
|
5095
|
+
applyContextBudget,
|
|
5096
|
+
applyViewContextBudget,
|
|
5097
|
+
resolveContextMaxBytes,
|
|
4450
5098
|
buildBusinessSemanticSummary,
|
|
4451
5099
|
buildSemanticDedupAdvisory,
|
|
4452
5100
|
selectCreatedElementAdds,
|