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.
@@ -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('./argob-schema.js');
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: { type: 'object' },
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 ArgoBument fallback by default.
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
- for (const mutation of mutations) {
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
- requireId(mutation.element.id, 'mutation.element.id');
1413
- const existingElement = findById(nextDocument.elements, mutation.element.id);
1414
- let targetElementId = mutation.element.id;
1415
- let reusedElement = false;
1416
- if (!existingElement) {
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
- reusedElement = true;
1886
+ elementReused = true;
1426
1887
  } else {
1427
- nextDocument.elements.push(clone(mutation.element));
1428
- syncViewsToElementSubdiagramViews(nextDocument, findById(nextDocument.elements, mutation.element.id));
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: !existingElement && !reusedElement,
1442
- ...(reusedElement ? { reused: true, reusedId: targetElementId, requestedId: mutation.element.id } : {}),
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
- requireId(mutation.relationship.id, 'mutation.relationship.id');
1542
- const existingRelationship = findById(nextDocument.relationships, mutation.relationship.id);
1543
- let targetRelationshipId = mutation.relationship.id;
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 reusedRelationship = false;
1547
- if (!existingRelationship) {
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
- reusedRelationship = true;
2050
+ relationshipReused = true;
1559
2051
  } else {
1560
- nextDocument.relationships.push(clone(mutation.relationship));
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: !existingRelationship && !reusedRelationship,
1577
- ...(reusedRelationship ? { reused: true, reusedId: targetRelationshipId, requestedId: mutation.relationship.id } : {}),
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
- if (findView(nextDocument.views, mutation.view.view_id)) {
1662
- throw new Error(`View '${mutation.view.view_id}' already exists`);
1663
- }
1664
- const candidates = findDuplicateViews(nextDocument.views, mutation.view);
1665
- const resolution = resolveDuplicateConflict({
1666
- onConflict: mutation.onConflict,
1667
- justification: mutation.justification,
1668
- label: `view (name '${mutation.view.view_name}')`,
1669
- }, candidates);
1670
- if (resolution.action === 'reuse') {
1671
- mutationSummaries.push({
1672
- type: mutation.type,
1673
- id: resolution.existing.view_id,
1674
- created: false,
1675
- reused: true,
1676
- reusedId: resolution.existing.view_id,
1677
- requestedId: mutation.view.view_id,
1678
- });
1679
- } else {
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
- if (Array.isArray(newView.included_relationships)) {
1685
- newView.included_relationships = addUnique([], newView.included_relationships);
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
- function getSystemArchitectureResult(payload) {
2703
- const failed = payload.status === 'failed';
2704
- return toolResult(payload, {
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,