archgraph-argo 0.26.2 → 0.27.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.
@@ -6,7 +6,6 @@ const readline = require('node:readline');
6
6
  const crypto = require('node:crypto');
7
7
 
8
8
  const {
9
- getArgoRoot,
10
9
  resolveCallWorkspaceRoot,
11
10
  } = require('./argo-paths.js');
12
11
 
@@ -132,6 +131,11 @@ const {
132
131
  validateArchiMateEndpointMatrix,
133
132
  validateViewElementLimits,
134
133
  } = require('./graph-semantics.js');
134
+ const {
135
+ loadSchemaBundleAndOntology,
136
+ resolveSchemaBundle,
137
+ resolveTypeEnums,
138
+ } = require('./argob-schema.js');
135
139
  const {
136
140
  createProductionGraphRagRuntime,
137
141
  } = require('./graph-rag/productionGraphRagRuntime.js');
@@ -168,6 +172,7 @@ const {
168
172
  } = require('./neo4j-system-architecture-store.js');
169
173
 
170
174
  const losslessWriteGate = require('./lossless-write-gate.js');
175
+ const { EXTERNAL_READ_TOOLS } = require('./external-graph-query.js');
171
176
 
172
177
  const HANDLED_MUTATION_TYPES = new Set([
173
178
  'addElement',
@@ -231,7 +236,7 @@ const TOOLS = [
231
236
  },
232
237
  {
233
238
  name: 'previewSystemArchitectureMutation',
234
- description: 'Use before apply for complex or risky changes. Performs a dry-run of one or more mutations, runs schema, graph, view, and ArchiMate 3.2 validation, and does not write the graph.',
239
+ description: 'Use before apply for complex or risky changes. Performs a dry-run of one or more mutations, runs schema, graph, view, and modeling-language validation, and does not write the graph.',
235
240
  inputSchema: mutationInputSchema(),
236
241
  },
237
242
  {
@@ -380,7 +385,7 @@ const TOOLS = [
380
385
  },
381
386
  {
382
387
  name: 'queryNeo4jGraph',
383
- description: 'Run a read-only Cypher query against the Neo4j structural projection of the intent architecture, or request the projection schema so an agent can construct its own Cypher. Pass {schema: true} to return node labels, relationship types, property keys, and the legal ArchiMate element/relationship type enums. Pass {cypher: "..."} to execute a read-only query; scope it with {graphKey: $graphKey}. Write clauses (CREATE/MERGE/DELETE/SET/REMOVE/DROP/LOAD CSV/FOREACH/IN TRANSACTIONS) are rejected.',
388
+ description: 'Run a read-only Cypher query against the Neo4j structural projection of the intent architecture, or request the projection schema so an agent can construct its own Cypher. Pass {schema: true} to return node labels, relationship types, property keys, and the legal element/relationship type enums of the workspace-resolved schema bundle. Pass {cypher: "..."} to execute a read-only query; scope it with {graphKey: $graphKey}. Write clauses (CREATE/MERGE/DELETE/SET/REMOVE/DROP/LOAD CSV/FOREACH/IN TRANSACTIONS) are rejected.',
384
389
  inputSchema: {
385
390
  type: 'object',
386
391
  properties: {
@@ -415,6 +420,13 @@ const WORKSPACE_ROOT_PARAM = Object.freeze({
415
420
  description:
416
421
  'Optional absolute workspace root for this call. When provided it is used as-is; otherwise the server launch directory is used.',
417
422
  });
423
+ // Cross-project graph query: the 5 read tools accept an optional `projectId`;
424
+ // provided => routed to the federation center, omitted => the local workspace.
425
+ const PROJECT_ID_PARAM = Object.freeze({
426
+ type: 'string',
427
+ description:
428
+ 'Optional external project id. When provided, the query is routed to the federation center to read that project\'s graph (this project must be registered in .argo/federation.json); omitted = the local workspace (unchanged).',
429
+ });
418
430
  // Lossless write gate: every update/remove helper accepts an explicit loss
419
431
  // acknowledgement (see lossless-write-gate.js). add* is purely additive and is
420
432
  // left alone. Without the acknowledgement a shrinking text edit or a destructive
@@ -436,6 +448,9 @@ for (const tool of TOOLS) {
436
448
  if (!Object.prototype.hasOwnProperty.call(inputSchema.properties, 'workspaceRoot')) {
437
449
  inputSchema.properties.workspaceRoot = WORKSPACE_ROOT_PARAM;
438
450
  }
451
+ if (EXTERNAL_READ_TOOLS.has(tool.name) && !Object.prototype.hasOwnProperty.call(inputSchema.properties, 'projectId')) {
452
+ inputSchema.properties.projectId = PROJECT_ID_PARAM;
453
+ }
439
454
  if (tool.name && /^(update|remove)Architecture/.test(tool.name)) {
440
455
  for (const [key, value] of Object.entries(LOSS_ACK_INPUT_PROPS)) {
441
456
  if (!Object.prototype.hasOwnProperty.call(inputSchema.properties, key)) {
@@ -559,22 +574,7 @@ function normalizeRelativePath(value) {
559
574
  }
560
575
 
561
576
  function resolveSchemaPath(workspaceRoot) {
562
- const bundledSchemaPath = path.join(getArgoRoot(), 'schema', 'SystemArchitecture.schema.json');
563
- if (fs.existsSync(bundledSchemaPath)) {
564
- return { absolutePath: bundledSchemaPath, relativePath: SCHEMA_PATH_CANDIDATES[0] };
565
- }
566
-
567
- for (const candidate of SCHEMA_PATH_CANDIDATES) {
568
- const absolutePath = path.join(workspaceRoot, candidate);
569
- if (fs.existsSync(absolutePath)) {
570
- return { absolutePath, relativePath: candidate };
571
- }
572
- const bundledPath = path.resolve(__dirname, '..', '..', candidate);
573
- if (fs.existsSync(bundledPath)) {
574
- return { absolutePath: bundledPath, relativePath: candidate };
575
- }
576
- }
577
- throw new Error(`Unable to locate SystemArchitecture schema. Checked: ${SCHEMA_PATH_CANDIDATES.join(', ')}`);
577
+ return resolveSchemaBundle(workspaceRoot).schema;
578
578
  }
579
579
 
580
580
  function readJson(filePath, label) {
@@ -588,13 +588,17 @@ function readJson(filePath, label) {
588
588
  async function loadContext(args = {}) {
589
589
  const workspaceRoot = resolveWorkspaceRoot(args);
590
590
  const graphPath = resolveWorkspacePath(workspaceRoot, args.architecturePath || DEFAULT_GRAPH_PATH);
591
- const schemaPath = resolveSchemaPath(workspaceRoot);
591
+ // Resolve the modeling language for THIS workspace: a repository that ships
592
+ // its own bundle under .argo/schema is validated against that schema.
593
+ const { bundle, ontology } = loadSchemaBundleAndOntology(workspaceRoot);
592
594
  const context = {
593
595
  workspaceRoot,
594
596
  graphPath,
595
- schemaPath,
597
+ schemaPath: bundle.schema,
598
+ schemaBundle: bundle,
599
+ ontology,
596
600
  document: readJson(graphPath.absolutePath, graphPath.relativePath),
597
- schema: readJson(schemaPath.absolutePath, schemaPath.relativePath),
601
+ schema: bundle.schemaDocument,
598
602
  };
599
603
  context.neo4jSyncRecovery = await recoverNeo4jSyncIfNeeded({
600
604
  architecturePath: graphPath.relativePath,
@@ -605,13 +609,19 @@ async function loadContext(args = {}) {
605
609
  }
606
610
 
607
611
  function validateDocument(document, schema, options = {}) {
612
+ const ontology = options.ontology;
608
613
  const errors = [];
614
+ if (ontology && ontology.bundleValidation && ontology.bundleValidation.status === 'failed') {
615
+ errors.push(...ontology.bundleValidation.errors.map(error => `schema bundle: ${error}`));
616
+ }
609
617
  validateAgainstSchema(document, schema, '#', errors, schema);
610
- validateGraphSemantics(document, errors);
618
+ validateGraphSemantics(document, errors, ontology);
611
619
  validateArchiMateEndpointMatrix(document, errors, {
620
+ ontology,
612
621
  touchedRelationshipIds: options.touchedRelationshipIds,
613
622
  });
614
623
  validateViewElementLimits(document, errors, {
624
+ ontology,
615
625
  touchedViewIds: options.validateAllViewElementLimits
616
626
  ? (document.views || []).map(view => view && view.view_id)
617
627
  : options.touchedViewIds,
@@ -679,7 +689,7 @@ function buildIntentElementContext(context, args = {}) {
679
689
  const dependentDepth = normalizeDepth(args.dependentDepth, 1);
680
690
  const associationDepth = Math.max(1, normalizeDepth(args.associationDepth, 1));
681
691
  const associationNeighborDependencyDepth = normalizeDepth(args.associationNeighborDependencyDepth, 0);
682
- const graphIndex = buildGraphIndex(context.document);
692
+ const graphIndex = buildGraphIndex(context.document, context.ontology);
683
693
  const focusElement = focusResult.element;
684
694
  const includedElementIds = new Set([focusElement.id]);
685
695
  const includedRelationshipIds = new Set();
@@ -991,7 +1001,7 @@ function normalizeDepth(value, defaultValue) {
991
1001
  return Math.floor(numericValue);
992
1002
  }
993
1003
 
994
- function buildGraphIndex(document) {
1004
+ function buildGraphIndex(document, ontology) {
995
1005
  const relationshipById = new Map();
996
1006
  const elementById = new Map((document.elements || []).map(element => [element.id, element]));
997
1007
  const relationshipsByElementId = new Map();
@@ -1000,9 +1010,19 @@ function buildGraphIndex(document) {
1000
1010
  addIndexedRelationship(relationshipsByElementId, relationship.source_id, relationship);
1001
1011
  addIndexedRelationship(relationshipsByElementId, relationship.target_id, relationship);
1002
1012
  }
1003
- return { elementById, relationshipById, relationshipsByElementId };
1013
+ const deliveryDependencies = ontology && ontology.deliveryDependencies
1014
+ ? ontology.deliveryDependencies
1015
+ : DEFAULT_DELIVERY_DEPENDENCIES;
1016
+ return { elementById, relationshipById, relationshipsByElementId, deliveryDependencies };
1004
1017
  }
1005
1018
 
1019
+ // Dependency direction for the semantic-edge walk comes from the active schema
1020
+ // bundle's deliveryDependencies; this is the ArgoBument fallback by default.
1021
+ const DEFAULT_DELIVERY_DEPENDENCIES = Object.freeze({
1022
+ sourceDependsOnTarget: Object.freeze(['Access', 'Assignment', 'Specialization', 'Composition', 'Aggregation']),
1023
+ targetDependsOnSource: Object.freeze(['Serving', 'Realization', 'Flow', 'Triggering', 'Influence']),
1024
+ });
1025
+
1006
1026
  function addIndexedRelationship(index, elementId, relationship) {
1007
1027
  if (!index.has(elementId)) {
1008
1028
  index.set(elementId, []);
@@ -1057,8 +1077,9 @@ function resolveSemanticEdges(elementId, graphIndex) {
1057
1077
  continue;
1058
1078
  }
1059
1079
 
1060
- const sourceDependsOnTarget = ['Access', 'Assignment', 'Specialization', 'Composition', 'Aggregation'].includes(relationshipType);
1061
- const targetDependsOnSource = ['Serving', 'Realization', 'Flow', 'Triggering', 'Influence'].includes(relationshipType);
1080
+ const deliveryDependencies = graphIndex.deliveryDependencies || DEFAULT_DELIVERY_DEPENDENCIES;
1081
+ const sourceDependsOnTarget = deliveryDependencies.sourceDependsOnTarget.includes(relationshipType);
1082
+ const targetDependsOnSource = deliveryDependencies.targetDependsOnSource.includes(relationshipType);
1062
1083
  if (sourceDependsOnTarget) {
1063
1084
  edges.push({ kind: isSource ? 'dependency' : 'dependent', neighborId, relationship });
1064
1085
  continue;
@@ -1930,7 +1951,7 @@ async function buildMutationResult(context, mutations, write, dependencies, loss
1930
1951
  before: beforeSummary,
1931
1952
  after: beforeSummary,
1932
1953
  errors,
1933
- guidance: buildFailureGuidance(errors),
1954
+ guidance: buildFailureGuidance(errors, context.ontology),
1934
1955
  };
1935
1956
  if (error && Array.isArray(error.duplicateConflicts) && error.duplicateConflicts.length > 0) {
1936
1957
  failed.duplicateConflicts = error.duplicateConflicts;
@@ -1941,6 +1962,7 @@ async function buildMutationResult(context, mutations, write, dependencies, loss
1941
1962
  return failed;
1942
1963
  }
1943
1964
  const errors = validateDocument(mutationResult.document, context.schema, {
1965
+ ontology: context.ontology,
1944
1966
  touchedRelationshipIds: mutationResult.touchedRelationshipIds,
1945
1967
  touchedViewIds: mutationResult.viewLimitCheckIds,
1946
1968
  });
@@ -1960,7 +1982,7 @@ async function buildMutationResult(context, mutations, write, dependencies, loss
1960
1982
  errors,
1961
1983
  };
1962
1984
  if (errors.length > 0) {
1963
- result.guidance = buildFailureGuidance(errors);
1985
+ result.guidance = buildFailureGuidance(errors, context.ontology);
1964
1986
  }
1965
1987
 
1966
1988
  // Semantic dedup gate: a would-be element create is blocked when a same-type
@@ -2215,10 +2237,10 @@ function buildMutationEmbeddingLifecycleFailure(error, result) {
2215
2237
  });
2216
2238
  }
2217
2239
 
2218
- function buildFailureGuidance(errors) {
2240
+ function buildFailureGuidance(errors, ontology) {
2219
2241
  const guidance = [];
2220
2242
  for (const error of errors || []) {
2221
- addGuidanceForError(guidance, String(error));
2243
+ addGuidanceForError(guidance, String(error), ontology);
2222
2244
  }
2223
2245
  if (guidance.length === 0 && Array.isArray(errors) && errors.length > 0) {
2224
2246
  guidance.push('Inspect the error text, call getSystemArchitecture with an explicit semantic query to refresh relevant ids, use getIntentElementContext for focused dependency context when needed, then retry with previewSystemArchitectureMutation before writing. Use an omitted-query full snapshot only when exact complete view membership is required.');
@@ -2226,15 +2248,17 @@ function buildFailureGuidance(errors) {
2226
2248
  return guidance;
2227
2249
  }
2228
2250
 
2229
- function addGuidanceForError(guidance, error) {
2251
+ function addGuidanceForError(guidance, error, ontology) {
2252
+ const language = ontology && ontology.language ? ontology.language : 'ArchiMate 3.2';
2253
+ const matrixLabel = ontology && ontology.matrixErrorLabel ? ontology.matrixErrorLabel : 'ArchiMate 3.2 relationship matrix';
2230
2254
  if (error.includes('mutation.view_ids must contain at least one view id')) {
2231
2255
  pushUnique(guidance, 'Select the target view_ids explicitly. Prefer getSystemArchitecture with an explicit semantic query to find relevant views, then use getIntentElementContext for focused element dependencies when needed. Use a full snapshot only if exact complete view membership is required.');
2232
2256
  }
2233
- if (error.includes('violates ArchiMate 3.2 relationship matrix')) {
2234
- pushUnique(guidance, 'Check relationship.type and the source and target element types against ArchiMate 3.2. If the intended meaning is still valid, choose a compliant relationship type or change the endpoint element types by remove-and-add.');
2257
+ if (error.includes(`violates ${matrixLabel}`)) {
2258
+ pushUnique(guidance, `Check relationship.type and the source and target element types against ${language}. If the intended meaning is still valid, choose a compliant relationship type or change the endpoint element types by remove-and-add.`);
2235
2259
  }
2236
- if (error.includes('uses unsupported ArchiMate relationship type')) {
2237
- pushUnique(guidance, 'Use relationship.type for the ArchiMate relationship type and choose one of the schema-supported ArchiMate 3.2 relationship types.');
2260
+ if (error.includes('uses unsupported') && error.includes('relationship type')) {
2261
+ pushUnique(guidance, `Use relationship.type for the relationship type and choose one of the schema-supported ${language} relationship types.`);
2238
2262
  }
2239
2263
  if (error.includes('id cannot be updated') || error.includes('type cannot be updated')) {
2240
2264
  pushUnique(guidance, 'Do not patch immutable identity or type fields. To change an id or type, remove the existing element or relationship, then add the replacement with the desired id or type.');
@@ -2243,10 +2267,16 @@ function addGuidanceForError(guidance, error) {
2243
2267
  pushUnique(guidance, 'Every element and relationship must belong to at least one view. Add it with view_ids, or add the existing object to an appropriate view before validating again.');
2244
2268
  }
2245
2269
  if (error.includes('must declare parent_element_id') || error.includes('top-level view')) {
2246
- pushUnique(guidance, 'Keep exactly one top-level view named SystemArchitecture. For any sub-view, set parent_element_id to an existing element and keep parent_element_name aligned with that element name.');
2270
+ const rootViewName = ontology && ontology.invariants && ontology.invariants.rootViewName
2271
+ ? ontology.invariants.rootViewName
2272
+ : 'SystemArchitecture';
2273
+ pushUnique(guidance, `Keep exactly one top-level view${rootViewName ? ` named ${rootViewName}` : ''}. For any sub-view, set parent_element_id to an existing element and keep parent_element_name aligned with that element name.`);
2247
2274
  }
2248
- if (error.includes('must contain at most 15 elements')) {
2249
- pushUnique(guidance, 'Do not force more than 15 included_elements into one view. Pause and think about layered architecture: split the view into layered sub-views, attach each sub-view with parent_element_id, and move lower-level elements into the appropriate child view before retrying.');
2275
+ if (error.includes('must contain at most') && error.includes('elements')) {
2276
+ const maxElements = ontology && ontology.invariants && ontology.invariants.maxElementsPerView !== undefined && ontology.invariants.maxElementsPerView !== null
2277
+ ? ontology.invariants.maxElementsPerView
2278
+ : 15;
2279
+ pushUnique(guidance, `Do not force more than ${maxElements} included_elements into one view. Pause and think about layered architecture: split the view into layered sub-views, attach each sub-view with parent_element_id, and move lower-level elements into the appropriate child view before retrying.`);
2250
2280
  }
2251
2281
  if (error.includes('does not exist') || error.includes('references missing')) {
2252
2282
  pushUnique(guidance, 'Refresh current ids with getSystemArchitecture semantic query first, then call getIntentElementContext for any returned element that needs dependency context. Do not guess ids; use existing element, relationship, and view ids or create missing objects first.');
@@ -3102,13 +3132,25 @@ async function queryNeo4jGraphTool(args = {}) {
3102
3132
  function queryNeo4jGraphSchemaResult(architecturePath, workspaceRoot) {
3103
3133
  const schema = buildNeo4jGraphSchema(architecturePath);
3104
3134
  let typeEnums = {};
3135
+ let schemaBundleInfo = {};
3105
3136
  try {
3106
3137
  const resolvedRoot = workspaceRoot || resolveWorkspaceRoot({ architecturePath });
3107
- const schemaPath = resolveSchemaPath(resolvedRoot);
3108
- const jsonSchema = readJson(schemaPath.absolutePath, schemaPath.relativePath);
3138
+ const { bundle, ontology } = loadSchemaBundleAndOntology(resolvedRoot);
3139
+ const enums = resolveTypeEnums(bundle);
3109
3140
  typeEnums = {
3110
- archimateElementTypes: (jsonSchema.$defs.archimateElementType || {}).enum || [],
3111
- archimateRelationshipTypes: (jsonSchema.$defs.archimateRelationshipType || {}).enum || [],
3141
+ archimateElementTypes: enums.elementTypes,
3142
+ archimateRelationshipTypes: enums.relationshipTypes,
3143
+ schemaLanguage: (bundle.config && bundle.config.language) || null,
3144
+ schemaKind: bundle.kind,
3145
+ schemaDialect: ontology.dialect,
3146
+ actorElementType: ontology.actorElementType,
3147
+ bundleValidation: ontology.bundleValidation,
3148
+ };
3149
+ schemaBundleInfo = {
3150
+ schemaPath: bundle.schema.relativePath,
3151
+ schemaKind: bundle.kind,
3152
+ schemaDir: bundle.relativeDir,
3153
+ guidePath: bundle.guidePath ? bundle.guidePath.relativePath : null,
3112
3154
  };
3113
3155
  } catch (error) {
3114
3156
  typeEnums = {
@@ -3125,6 +3167,7 @@ function queryNeo4jGraphSchemaResult(architecturePath, workspaceRoot) {
3125
3167
  schema: {
3126
3168
  ...schema,
3127
3169
  ...typeEnums,
3170
+ ...schemaBundleInfo,
3128
3171
  },
3129
3172
  usage: {
3130
3173
  scopeGraph: 'MATCH (e:Element {graphKey: $graphKey}) ...',