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
|
@@ -130,12 +130,13 @@ const {
|
|
|
130
130
|
validateGraphSemantics,
|
|
131
131
|
validateArchiMateEndpointMatrix,
|
|
132
132
|
validateViewElementLimits,
|
|
133
|
+
validateAttributeContracts,
|
|
133
134
|
} = require('./graph-semantics.js');
|
|
134
135
|
const {
|
|
135
136
|
loadSchemaBundleAndOntology,
|
|
136
137
|
resolveSchemaBundle,
|
|
137
138
|
resolveTypeEnums,
|
|
138
|
-
} = require('./
|
|
139
|
+
} = require('./schema-bundle.js');
|
|
139
140
|
const {
|
|
140
141
|
createProductionGraphRagRuntime,
|
|
141
142
|
} = require('./graph-rag/productionGraphRagRuntime.js');
|
|
@@ -226,12 +227,12 @@ const TOOLS = [
|
|
|
226
227
|
},
|
|
227
228
|
{
|
|
228
229
|
name: 'getIntentElementContext',
|
|
229
|
-
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.',
|
|
230
231
|
inputSchema: intentElementContextInputSchema(),
|
|
231
232
|
},
|
|
232
233
|
{
|
|
233
234
|
name: 'getArchitectureViewContext',
|
|
234
|
-
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`).',
|
|
235
236
|
inputSchema: viewContextInputSchema(),
|
|
236
237
|
},
|
|
237
238
|
{
|
|
@@ -246,12 +247,18 @@ const TOOLS = [
|
|
|
246
247
|
},
|
|
247
248
|
{
|
|
248
249
|
name: 'addArchitectureElement',
|
|
249
|
-
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.',
|
|
250
251
|
inputSchema: {
|
|
251
252
|
type: 'object',
|
|
252
253
|
required: ['element', 'view_ids'],
|
|
253
254
|
properties: {
|
|
254
|
-
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
|
+
},
|
|
255
262
|
view_ids: { type: 'array', minItems: 1, items: { type: 'string' } },
|
|
256
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.' },
|
|
257
264
|
justification: { type: 'string', description: 'Required when onConflict is allowDuplicate.' },
|
|
@@ -293,7 +300,7 @@ const TOOLS = [
|
|
|
293
300
|
},
|
|
294
301
|
{
|
|
295
302
|
name: 'addArchitectureRelationship',
|
|
296
|
-
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.',
|
|
297
304
|
inputSchema: {
|
|
298
305
|
type: 'object',
|
|
299
306
|
required: ['relationship', 'view_ids'],
|
|
@@ -340,7 +347,7 @@ const TOOLS = [
|
|
|
340
347
|
},
|
|
341
348
|
{
|
|
342
349
|
name: 'addArchitectureView',
|
|
343
|
-
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.',
|
|
344
351
|
inputSchema: {
|
|
345
352
|
type: 'object',
|
|
346
353
|
required: ['view'],
|
|
@@ -476,8 +483,9 @@ function intentElementContextInputSchema() {
|
|
|
476
483
|
dependentDepth: { type: 'number', description: 'Default: 1. Semantic dependents that rely on the focus element.' },
|
|
477
484
|
associationDepth: { type: 'number', description: 'Default: 1. Association neighbors are expanded at least one layer.' },
|
|
478
485
|
associationNeighborDependencyDepth: { type: 'number', description: 'Default: 0. Optional dependency expansion from association neighbors.' },
|
|
479
|
-
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.' },
|
|
480
|
-
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.' },
|
|
481
489
|
},
|
|
482
490
|
additionalProperties: false,
|
|
483
491
|
};
|
|
@@ -495,6 +503,7 @@ function viewContextInputSchema() {
|
|
|
495
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.' },
|
|
496
504
|
includeTestcases: { type: 'boolean', description: 'Default: false. Include member `testcases` verbatim. Omitted by default from this structural read; pass true for acceptance-case lookups.' },
|
|
497
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.' },
|
|
498
507
|
},
|
|
499
508
|
additionalProperties: false,
|
|
500
509
|
};
|
|
@@ -616,6 +625,7 @@ function validateDocument(document, schema, options = {}) {
|
|
|
616
625
|
}
|
|
617
626
|
validateAgainstSchema(document, schema, '#', errors, schema);
|
|
618
627
|
validateGraphSemantics(document, errors, ontology);
|
|
628
|
+
validateAttributeContracts(document, errors, ontology);
|
|
619
629
|
validateArchiMateEndpointMatrix(document, errors, {
|
|
620
630
|
ontology,
|
|
621
631
|
touchedRelationshipIds: options.touchedRelationshipIds,
|
|
@@ -678,6 +688,233 @@ function buildAgentProjection(omitted) {
|
|
|
678
688
|
};
|
|
679
689
|
}
|
|
680
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
|
+
|
|
681
918
|
function buildIntentElementContext(context, args = {}) {
|
|
682
919
|
const profile = args.profile || 'generic-agent';
|
|
683
920
|
const focusResult = resolveFocusElement(context.document, args);
|
|
@@ -804,7 +1041,7 @@ function buildIntentElementContext(context, args = {}) {
|
|
|
804
1041
|
};
|
|
805
1042
|
const projection = buildAgentProjection(omitted);
|
|
806
1043
|
if (projection) result.projection = projection;
|
|
807
|
-
return result;
|
|
1044
|
+
return applyContextBudget(result, resolveContextMaxBytes(args));
|
|
808
1045
|
}
|
|
809
1046
|
|
|
810
1047
|
// ---------------------------------------------------------------------------
|
|
@@ -948,7 +1185,7 @@ function buildViewContext(context, args = {}) {
|
|
|
948
1185
|
}
|
|
949
1186
|
const projection = buildAgentProjection(omitted);
|
|
950
1187
|
if (projection) result.projection = projection;
|
|
951
|
-
return result;
|
|
1188
|
+
return applyViewContextBudget(result, resolveContextMaxBytes(args));
|
|
952
1189
|
}
|
|
953
1190
|
|
|
954
1191
|
function resolveFocusElement(document, args) {
|
|
@@ -1017,7 +1254,7 @@ function buildGraphIndex(document, ontology) {
|
|
|
1017
1254
|
}
|
|
1018
1255
|
|
|
1019
1256
|
// Dependency direction for the semantic-edge walk comes from the active schema
|
|
1020
|
-
// bundle's deliveryDependencies; this is the
|
|
1257
|
+
// bundle's deliveryDependencies; this is the ArchiMate 3.2 fallback by default.
|
|
1021
1258
|
const DEFAULT_DELIVERY_DEPENDENCIES = Object.freeze({
|
|
1022
1259
|
sourceDependsOnTarget: Object.freeze(['Access', 'Assignment', 'Specialization', 'Composition', 'Aggregation']),
|
|
1023
1260
|
targetDependsOnSource: Object.freeze(['Serving', 'Realization', 'Flow', 'Triggering', 'Influence']),
|
|
@@ -1389,6 +1626,207 @@ function resolveDuplicateConflict(options, candidates) {
|
|
|
1389
1626
|
return { action: 'create', justification: options.justification };
|
|
1390
1627
|
}
|
|
1391
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
|
+
|
|
1392
1830
|
function applyMutations(document, mutations, options = {}) {
|
|
1393
1831
|
const nextDocument = clone(document);
|
|
1394
1832
|
const touchedElementIds = new Set();
|
|
@@ -1401,7 +1839,11 @@ function applyMutations(document, mutations, options = {}) {
|
|
|
1401
1839
|
throw new Error('mutations must contain at least one mutation');
|
|
1402
1840
|
}
|
|
1403
1841
|
|
|
1404
|
-
|
|
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) {
|
|
1405
1847
|
if (!mutation || typeof mutation !== 'object' || !HANDLED_MUTATION_TYPES.has(mutation.type)) {
|
|
1406
1848
|
throw new Error(`Unsupported mutation type: ${mutation && mutation.type}`);
|
|
1407
1849
|
}
|
|
@@ -1409,11 +1851,30 @@ function applyMutations(document, mutations, options = {}) {
|
|
|
1409
1851
|
if (mutation.type === 'addElement') {
|
|
1410
1852
|
requireObject(mutation.element, 'mutation.element');
|
|
1411
1853
|
const scopedViews = requireViewScope(nextDocument.views, mutation.view_ids, 'mutation.view_ids');
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
let
|
|
1416
|
-
|
|
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 {
|
|
1417
1878
|
const candidates = findDuplicateElements(nextDocument.elements, mutation.element);
|
|
1418
1879
|
const resolution = resolveDuplicateConflict({
|
|
1419
1880
|
onConflict: mutation.onConflict,
|
|
@@ -1422,12 +1883,19 @@ function applyMutations(document, mutations, options = {}) {
|
|
|
1422
1883
|
}, candidates);
|
|
1423
1884
|
if (resolution.action === 'reuse') {
|
|
1424
1885
|
targetElementId = resolution.existing.id;
|
|
1425
|
-
|
|
1886
|
+
elementReused = true;
|
|
1426
1887
|
} else {
|
|
1427
|
-
|
|
1428
|
-
|
|
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;
|
|
1429
1896
|
}
|
|
1430
1897
|
}
|
|
1898
|
+
|
|
1431
1899
|
for (const view of scopedViews) {
|
|
1432
1900
|
view.included_elements = addUnique(view.included_elements || [], [targetElementId]);
|
|
1433
1901
|
touchedViewIds.add(view.view_id);
|
|
@@ -1438,8 +1906,10 @@ function applyMutations(document, mutations, options = {}) {
|
|
|
1438
1906
|
type: mutation.type,
|
|
1439
1907
|
id: targetElementId,
|
|
1440
1908
|
view_ids: mutation.view_ids,
|
|
1441
|
-
created:
|
|
1442
|
-
...(
|
|
1909
|
+
created: elementCreated,
|
|
1910
|
+
...(elementReused ? { reused: true, reusedId: targetElementId } : {}),
|
|
1911
|
+
...(requestedElementId === null && elementCreated ? { allocatedId: targetElementId } : {}),
|
|
1912
|
+
...(elementReused && requestedElementId && requestedElementId !== targetElementId ? { requestedId: requestedElementId } : {}),
|
|
1443
1913
|
});
|
|
1444
1914
|
continue;
|
|
1445
1915
|
}
|
|
@@ -1538,13 +2008,35 @@ function applyMutations(document, mutations, options = {}) {
|
|
|
1538
2008
|
if (mutation.type === 'addRelationship') {
|
|
1539
2009
|
requireObject(mutation.relationship, 'mutation.relationship');
|
|
1540
2010
|
const scopedViews = requireViewScope(nextDocument.views, mutation.view_ids, 'mutation.view_ids');
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
2011
|
+
const requestedRelationshipId = typeof mutation.relationship.id === 'string' && mutation.relationship.id !== ''
|
|
2012
|
+
? mutation.relationship.id
|
|
2013
|
+
: null;
|
|
2014
|
+
let targetRelationshipId;
|
|
1544
2015
|
let sourceElementId = mutation.relationship.source_id;
|
|
1545
2016
|
let targetEndpointId = mutation.relationship.target_id;
|
|
1546
|
-
let
|
|
1547
|
-
|
|
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 {
|
|
1548
2040
|
const candidates = findDuplicateRelationships(nextDocument.relationships, mutation.relationship);
|
|
1549
2041
|
const resolution = resolveDuplicateConflict({
|
|
1550
2042
|
onConflict: mutation.onConflict,
|
|
@@ -1555,9 +2047,15 @@ function applyMutations(document, mutations, options = {}) {
|
|
|
1555
2047
|
targetRelationshipId = resolution.existing.id;
|
|
1556
2048
|
sourceElementId = resolution.existing.source_id;
|
|
1557
2049
|
targetEndpointId = resolution.existing.target_id;
|
|
1558
|
-
|
|
2050
|
+
relationshipReused = true;
|
|
1559
2051
|
} else {
|
|
1560
|
-
|
|
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;
|
|
1561
2059
|
}
|
|
1562
2060
|
}
|
|
1563
2061
|
for (const view of scopedViews) {
|
|
@@ -1573,8 +2071,9 @@ function applyMutations(document, mutations, options = {}) {
|
|
|
1573
2071
|
type: mutation.type,
|
|
1574
2072
|
id: targetRelationshipId,
|
|
1575
2073
|
view_ids: mutation.view_ids,
|
|
1576
|
-
created:
|
|
1577
|
-
...(
|
|
2074
|
+
created: relationshipCreated,
|
|
2075
|
+
...(relationshipReused ? { reused: true, reusedId: targetRelationshipId } : {}),
|
|
2076
|
+
...(requestedRelationshipId === null && relationshipCreated ? { allocatedId: targetRelationshipId } : {}),
|
|
1578
2077
|
});
|
|
1579
2078
|
continue;
|
|
1580
2079
|
}
|
|
@@ -1658,38 +2157,65 @@ function applyMutations(document, mutations, options = {}) {
|
|
|
1658
2157
|
|
|
1659
2158
|
if (mutation.type === 'addView') {
|
|
1660
2159
|
requireObject(mutation.view, 'mutation.view');
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
const newView = clone(mutation.view);
|
|
1681
|
-
if (Array.isArray(newView.included_elements)) {
|
|
1682
|
-
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
|
+
);
|
|
1683
2179
|
}
|
|
1684
|
-
|
|
1685
|
-
|
|
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;
|
|
1686
2210
|
}
|
|
1687
|
-
nextDocument.views.push(newView);
|
|
1688
|
-
upsertSubdiagramViewIntoElement(nextDocument, newView.parent_element_id, newView);
|
|
1689
|
-
touchedViewIds.add(newView.view_id);
|
|
1690
|
-
viewLimitCheckIds.add(newView.view_id);
|
|
1691
|
-
mutationSummaries.push({ type: mutation.type, id: newView.view_id });
|
|
1692
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
|
+
});
|
|
1693
2219
|
continue;
|
|
1694
2220
|
}
|
|
1695
2221
|
|
|
@@ -1761,6 +2287,33 @@ function requireObject(value, label) {
|
|
|
1761
2287
|
}
|
|
1762
2288
|
}
|
|
1763
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
|
+
|
|
1764
2317
|
function requireId(value, label) {
|
|
1765
2318
|
if (typeof value !== 'string' || value.length === 0) {
|
|
1766
2319
|
throw new Error(`${label} must be a non-empty string`);
|
|
@@ -2655,6 +3208,18 @@ function compactMutationResponse(payload) {
|
|
|
2655
3208
|
status: payload && payload.status,
|
|
2656
3209
|
written: Boolean(payload && payload.written),
|
|
2657
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
|
+
}
|
|
2658
3223
|
if (payload && payload.embeddingLifecycle && payload.embeddingLifecycle.state) {
|
|
2659
3224
|
compact.embeddingLifecycle = { state: payload.embeddingLifecycle.state };
|
|
2660
3225
|
}
|
|
@@ -2699,15 +3264,51 @@ function compactMutationResponse(payload) {
|
|
|
2699
3264
|
return compact;
|
|
2700
3265
|
}
|
|
2701
3266
|
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
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 {
|
|
2705
3302
|
version: '1.0',
|
|
2706
3303
|
mode: failed ? 'error' : 'semantic-query',
|
|
2707
|
-
document: failed ? null : (payload.document === undefined ? null : payload.document),
|
|
2708
|
-
query: failed ? null : (payload.query || null),
|
|
2709
|
-
error: failed ? payload.error : null,
|
|
2710
|
-
}
|
|
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));
|
|
2711
3312
|
}
|
|
2712
3313
|
|
|
2713
3314
|
async function callTool(name, args = {}, dependencies = undefined) {
|
|
@@ -4488,8 +5089,12 @@ if (require.main === module) {
|
|
|
4488
5089
|
|
|
4489
5090
|
module.exports = {
|
|
4490
5091
|
GET_SYSTEM_ARCHITECTURE_OUTPUT_SCHEMA,
|
|
5092
|
+
buildGetSystemArchitectureStructuredContent,
|
|
4491
5093
|
TOOLS,
|
|
4492
5094
|
applyMutations,
|
|
5095
|
+
applyContextBudget,
|
|
5096
|
+
applyViewContextBudget,
|
|
5097
|
+
resolveContextMaxBytes,
|
|
4493
5098
|
buildBusinessSemanticSummary,
|
|
4494
5099
|
buildSemanticDedupAdvisory,
|
|
4495
5100
|
selectCreatedElementAdds,
|