archgraph-argo 0.10.15 → 0.10.17

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.
@@ -65,6 +65,7 @@ const SYSTEM_ARCHITECTURE_TOOL_NAMES = new Set([
65
65
  'updateArchitectureView',
66
66
  'removeArchitectureView',
67
67
  'generateArchitectureDiffPlantuml',
68
+ 'queryNeo4jGraph',
68
69
  ]);
69
70
 
70
71
  const TOOLS = [
@@ -781,9 +781,255 @@ function toNumber(value) {
781
781
  return Number(value);
782
782
  }
783
783
 
784
+ // --- Read-only Cypher query + structural projection schema -------------------
785
+
786
+ const FORBIDDEN_CYPHER_CLAUSES = Object.freeze([
787
+ 'CREATE',
788
+ 'MERGE',
789
+ 'DELETE',
790
+ 'SET',
791
+ 'REMOVE',
792
+ 'DROP',
793
+ 'LOAD CSV',
794
+ 'FOREACH',
795
+ 'IN TRANSACTIONS',
796
+ ]);
797
+
798
+ function stripCypherNoise(cypher) {
799
+ let result = String(cypher);
800
+ // Block comments.
801
+ result = result.replace(/\/\*[\s\S]*?\*\//g, ' ');
802
+ // Line comments.
803
+ result = result.replace(/\/\/[^\r\n]*/g, ' ');
804
+ // Single-quoted string literals.
805
+ result = result.replace(/'(?:[^'\\]|\\.)*'/g, ' ');
806
+ // Double-quoted identifiers/literals.
807
+ result = result.replace(/"(?:[^"\\]|\\.)*"/g, ' ');
808
+ // Backtick-escaped identifiers.
809
+ result = result.replace(/`(?:[^`\\]|\\.)*`/g, ' ');
810
+ return result;
811
+ }
812
+
813
+ function assertReadOnlyCypher(cypher) {
814
+ if (typeof cypher !== 'string' || cypher.trim().length === 0) {
815
+ const error = new Error('cypher must be a non-empty string');
816
+ error.category = 'CYPHER_QUERY_REQUIRED';
817
+ throw error;
818
+ }
819
+ if (cypher.length > 20000) {
820
+ const error = new Error('cypher exceeds the 20000 character limit');
821
+ error.category = 'CYPHER_QUERY_TOO_LONG';
822
+ throw error;
823
+ }
824
+
825
+ const normalized = stripCypherNoise(cypher).toUpperCase();
826
+ const found = FORBIDDEN_CYPHER_CLAUSES.find(clause => {
827
+ const escaped = clause.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
828
+ return new RegExp(`\\b${escaped}\\b`).test(normalized);
829
+ });
830
+ if (found) {
831
+ const error = new Error(`Cypher write clause '${found}' is not allowed; the Neo4j graph query interface is read-only`);
832
+ error.category = 'READ_ONLY_CYPHER_REQUIRED';
833
+ error.clause = found;
834
+ throw error;
835
+ }
836
+ return true;
837
+ }
838
+
839
+ function serializeNeo4jValue(value) {
840
+ if (value === null || value === undefined) {
841
+ return value === undefined ? null : value;
842
+ }
843
+ if (typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean') {
844
+ return value;
845
+ }
846
+
847
+ let neo4j = null;
848
+ try {
849
+ neo4j = requireNeo4jDriver();
850
+ } catch {
851
+ neo4j = null;
852
+ }
853
+
854
+ if (neo4j && typeof neo4j.isInt === 'function' && neo4j.isInt(value)) {
855
+ if (typeof value.inSafeRange === 'function' && value.inSafeRange()) {
856
+ return value.toNumber();
857
+ }
858
+ return value.toString();
859
+ }
860
+ if (neo4j && typeof neo4j.isNode === 'function' && neo4j.isNode(value)) {
861
+ return {
862
+ $node: {
863
+ identity: value.identity ? String(value.identity) : null,
864
+ labels: Array.isArray(value.labels) ? value.labels : [],
865
+ properties: serializeNeo4jValue(value.properties),
866
+ },
867
+ };
868
+ }
869
+ if (neo4j && typeof neo4j.isRelationship === 'function' && neo4j.isRelationship(value)) {
870
+ return {
871
+ $relationship: {
872
+ identity: value.identity ? String(value.identity) : null,
873
+ type: value.type,
874
+ start: value.start ? String(value.start) : null,
875
+ end: value.end ? String(value.end) : null,
876
+ properties: serializeNeo4jValue(value.properties),
877
+ },
878
+ };
879
+ }
880
+ if (neo4j && typeof neo4j.isPath === 'function' && neo4j.isPath(value)) {
881
+ return {
882
+ $path: {
883
+ start: serializeNeo4jValue(value.start),
884
+ end: serializeNeo4jValue(value.end),
885
+ length: serializeNeo4jValue(value.length),
886
+ segments: Array.isArray(value.segments) ? value.segments.map(serializeNeo4jValue) : [],
887
+ },
888
+ };
889
+ }
890
+ if (Array.isArray(value)) {
891
+ return value.map(serializeNeo4jValue);
892
+ }
893
+ if (typeof value === 'object') {
894
+ const result = {};
895
+ for (const [key, item] of Object.entries(value)) {
896
+ result[key] = serializeNeo4jValue(item);
897
+ }
898
+ return result;
899
+ }
900
+ return String(value);
901
+ }
902
+
903
+ function buildNeo4jGraphSchema(architecturePath = DEFAULT_GRAPH_PATH) {
904
+ return {
905
+ graphKey: buildGraphKey(architecturePath),
906
+ nodeLabels: {
907
+ ArchitectureGraph: {
908
+ description: 'One node per canonical graph document; identified by graphKey.',
909
+ properties: [
910
+ 'graphKey',
911
+ 'source_path',
912
+ 'name',
913
+ 'description',
914
+ 'attributes_json',
915
+ 'raw_json',
916
+ 'element_count',
917
+ 'relationship_count',
918
+ 'view_count',
919
+ ],
920
+ },
921
+ Element: {
922
+ description: 'Canonical architecture elements. Element.type holds the ArchiMate element type.',
923
+ properties: [
924
+ 'graphKey',
925
+ 'id',
926
+ 'name',
927
+ 'type',
928
+ 'parent',
929
+ 'alias',
930
+ 'classifier',
931
+ 'description',
932
+ 'attributes_json',
933
+ 'subdiagram_views_json',
934
+ 'testcases_json',
935
+ 'raw_json',
936
+ ],
937
+ },
938
+ ArchitectureRelationship: {
939
+ description: 'Canonical architecture relationships. type holds the ArchiMate relationship type; source_id/target_id reference Element.id.',
940
+ properties: [
941
+ 'graphKey',
942
+ 'id',
943
+ 'name',
944
+ 'type',
945
+ 'statement',
946
+ 'description',
947
+ 'document',
948
+ 'attributes_json',
949
+ 'source_id',
950
+ 'source_name',
951
+ 'target_id',
952
+ 'target_name',
953
+ 'raw_json',
954
+ ],
955
+ },
956
+ View: {
957
+ description: 'Canonical views; identified by view_id.',
958
+ properties: [
959
+ 'graphKey',
960
+ 'view_id',
961
+ 'view_name',
962
+ 'parent_element_id',
963
+ 'parent_element_name',
964
+ 'description',
965
+ 'included_elements_json',
966
+ 'included_relationships_json',
967
+ 'raw_json',
968
+ ],
969
+ },
970
+ },
971
+ relationshipTypes: {
972
+ OWNS_ELEMENT: { from: 'ArchitectureGraph', to: 'Element' },
973
+ OWNS_RELATIONSHIP: { from: 'ArchitectureGraph', to: 'ArchitectureRelationship' },
974
+ OWNS_VIEW: { from: 'ArchitectureGraph', to: 'View' },
975
+ RELATIONSHIP_SOURCE: { from: 'ArchitectureRelationship', to: 'Element', description: 'Source endpoint element of a relationship record.' },
976
+ RELATIONSHIP_TARGET: { from: 'ArchitectureRelationship', to: 'Element', description: 'Target endpoint element of a relationship record.' },
977
+ ARCHIMATE_RELATES: { from: 'Element', to: 'Element', properties: ['graphKey', 'relationship_id'], description: 'Direct ArchiMate semantic edge between two elements.' },
978
+ VIEW_OF: { from: 'View', to: 'Element', description: 'Parent element of a sub-view.' },
979
+ INCLUDES_ELEMENT: { from: 'View', to: 'Element', properties: ['order'] },
980
+ INCLUDES_RELATIONSHIP: { from: 'View', to: 'ArchitectureRelationship', properties: ['order'] },
981
+ HAS_SUBDIAGRAM: { from: 'Element', to: 'View' },
982
+ },
983
+ };
984
+ }
985
+
986
+ async function runNeo4jCypherQuery(options = {}) {
987
+ const architecturePath = options.architecturePath || DEFAULT_GRAPH_PATH;
988
+ const cypher = options.cypher;
989
+ assertReadOnlyCypher(cypher);
990
+
991
+ const graphKey = buildGraphKey(architecturePath);
992
+ const config = getNeo4jConfig(options);
993
+ const driver = options.driver || createDriver(config);
994
+ const ownDriver = !options.driver;
995
+ const session = driver.session({ database: config.database });
996
+
997
+ try {
998
+ const result = await session.executeRead(tx => tx.run(cypher, { graphKey }));
999
+ const records = result.records.map(record => {
1000
+ const entry = {};
1001
+ for (const key of record.keys) {
1002
+ entry[key] = serializeNeo4jValue(record.get(key));
1003
+ }
1004
+ return entry;
1005
+ });
1006
+
1007
+ const summary = result.summary;
1008
+ return {
1009
+ architecturePath,
1010
+ graphKey,
1011
+ records,
1012
+ summary: {
1013
+ queryType: summary ? summary.queryType : null,
1014
+ database: summary && summary.database ? summary.database.name : null,
1015
+ containsUpdates: summary && summary.counters && typeof summary.counters.containsUpdates === 'function'
1016
+ ? summary.counters.containsUpdates()
1017
+ : null,
1018
+ },
1019
+ };
1020
+ } finally {
1021
+ await session.close();
1022
+ if (ownDriver) {
1023
+ await driver.close();
1024
+ }
1025
+ }
1026
+ }
1027
+
784
1028
  module.exports = {
785
1029
  DEFAULT_GRAPH_PATH,
1030
+ assertReadOnlyCypher,
786
1031
  buildGraphKey,
1032
+ buildNeo4jGraphSchema,
787
1033
  createDriver,
788
1034
  digestCanonicalArchitecture,
789
1035
  ensureDatabaseExists,
@@ -797,6 +1043,8 @@ module.exports = {
797
1043
  readNeo4jSyncState,
798
1044
  recoverNeo4jSyncIfNeeded,
799
1045
  resolveArchitecturePath,
1046
+ runNeo4jCypherQuery,
1047
+ serializeNeo4jValue,
800
1048
  syncArchitectureToNeo4j,
801
1049
  verifyArchitectureSync,
802
1050
  waitForDatabaseOnline,
@@ -164,7 +164,10 @@ const {
164
164
  } = require('./graph-rag/mutationEmbeddingVectorLifecycle.js');
165
165
  const {
166
166
  DEFAULT_GRAPH_PATH: NEO4J_DEFAULT_GRAPH_PATH,
167
+ buildGraphKey,
168
+ buildNeo4jGraphSchema,
167
169
  recoverNeo4jSyncIfNeeded,
170
+ runNeo4jCypherQuery,
168
171
  syncArchitectureToNeo4j,
169
172
  verifyArchitectureSync,
170
173
  } = require('./neo4j-system-architecture-store.js');
@@ -379,6 +382,19 @@ const TOOLS = [
379
382
  additionalProperties: false,
380
383
  },
381
384
  },
385
+ {
386
+ name: 'queryNeo4jGraph',
387
+ 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
+ inputSchema: {
389
+ type: 'object',
390
+ properties: {
391
+ architecturePath: { type: 'string', description: `Default: ${DEFAULT_GRAPH_PATH}` },
392
+ cypher: { type: 'string', description: 'Read-only Cypher query to execute. Use $graphKey to scope to the current architecture graph, e.g. MATCH (e:Element {graphKey: $graphKey}) RETURN e.id, e.name.' },
393
+ schema: { type: 'boolean', description: 'When true, return the Neo4j projection schema instead of running a Cypher query.' },
394
+ },
395
+ additionalProperties: false,
396
+ },
397
+ },
382
398
  ];
383
399
 
384
400
  // Every tool accepts an optional per-call `workspaceRoot` (absolute path,
@@ -2154,9 +2170,80 @@ async function callTool(name, args = {}, dependencies = undefined) {
2154
2170
  return mutationToolResult(attachContextWarnings(await buildMutationResult(context, [{ type: 'removeView', view_id: args.view_id }], write), context), write);
2155
2171
  }
2156
2172
 
2173
+ if (name === 'queryNeo4jGraph') {
2174
+ return queryNeo4jGraphTool(args);
2175
+ }
2176
+
2157
2177
  throw new Error(`Unknown tool: ${name}`);
2158
2178
  }
2159
2179
 
2180
+ async function queryNeo4jGraphTool(args = {}) {
2181
+ const architecturePath = args.architecturePath || DEFAULT_GRAPH_PATH;
2182
+
2183
+ if (args.schema === true) {
2184
+ return queryNeo4jGraphSchemaResult(architecturePath);
2185
+ }
2186
+
2187
+ try {
2188
+ const result = await runNeo4jCypherQuery({
2189
+ architecturePath,
2190
+ cypher: args.cypher,
2191
+ });
2192
+ return toolResult({
2193
+ status: 'passed',
2194
+ architecturePath: result.architecturePath,
2195
+ graphKey: result.graphKey,
2196
+ records: result.records,
2197
+ summary: result.summary,
2198
+ });
2199
+ } catch (error) {
2200
+ return toolResult(queryError(
2201
+ error && error.category ? error.category : 'NEO4J_QUERY_FAILED',
2202
+ String(error && error.message ? error.message : error),
2203
+ {
2204
+ architecturePath,
2205
+ graphKey: buildGraphKey(architecturePath),
2206
+ ...(error && error.clause ? { clause: error.clause } : {}),
2207
+ },
2208
+ ));
2209
+ }
2210
+ }
2211
+
2212
+ function queryNeo4jGraphSchemaResult(architecturePath) {
2213
+ const schema = buildNeo4jGraphSchema(architecturePath);
2214
+ let typeEnums = {};
2215
+ try {
2216
+ const workspaceRoot = resolveWorkspaceRoot({ architecturePath });
2217
+ const schemaPath = resolveSchemaPath(workspaceRoot);
2218
+ const jsonSchema = readJson(schemaPath.absolutePath, schemaPath.relativePath);
2219
+ typeEnums = {
2220
+ archimateElementTypes: (jsonSchema.$defs.archimateElementType || {}).enum || [],
2221
+ archimateRelationshipTypes: (jsonSchema.$defs.archimateRelationshipType || {}).enum || [],
2222
+ };
2223
+ } catch (error) {
2224
+ typeEnums = {
2225
+ archimateElementTypes: [],
2226
+ archimateRelationshipTypes: [],
2227
+ schemaEnumError: String(error && error.message ? error.message : error),
2228
+ };
2229
+ }
2230
+
2231
+ return toolResult({
2232
+ status: 'passed',
2233
+ architecturePath,
2234
+ graphKey: buildGraphKey(architecturePath),
2235
+ schema: {
2236
+ ...schema,
2237
+ ...typeEnums,
2238
+ },
2239
+ usage: {
2240
+ scopeGraph: 'MATCH (e:Element {graphKey: $graphKey}) ...',
2241
+ exampleAllBusinessActors: "MATCH (e:Element {graphKey: $graphKey}) WHERE e.type = 'Business Actor' RETURN e.id, e.name, e.description",
2242
+ exampleAssignments: "MATCH (a:Element {graphKey: $graphKey, type: 'Business Actor'})-[r:ARCHIMATE_RELATES]->(b:Element {graphKey: $graphKey}) WHERE r.relationship_id IS NOT NULL RETURN a.name, type(r), b.name, r.relationship_id",
2243
+ },
2244
+ });
2245
+ }
2246
+
2160
2247
  async function resolveSemanticOperatorJourney(dependencies) {
2161
2248
  return dependencies && dependencies.semanticOperatorJourney
2162
2249
  ? dependencies.semanticOperatorJourney
package/install-argo.ps1 CHANGED
@@ -726,7 +726,7 @@ if ($SkipDsh) {
726
726
  Write-DshManagedBlock -Path $patchPath -Block $block -MarkerStart '# BEGIN ArchGraph ARGO deployment' -MarkerEnd '# END ArchGraph ARGO deployment'
727
727
  }
728
728
 
729
- Write-Host "[19/19] argo\agents5 -> $DshHome\.agent-presets\<id> (DeepSeek Harness agent presets)"
729
+ Write-Host "[19/19] argo\agents6 -> $DshHome\.agent-presets\<id> (DeepSeek Harness agent presets)"
730
730
  New-DshAgentPresets -DshHome $DshHome -AgentsSrc (Join-Path $argoDir 'agents')
731
731
 
732
732
  Write-Host ' Restart `dsh web` to activate the MCP bridge and the wakeup plugin;'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archgraph-argo",
3
- "version": "0.10.15",
3
+ "version": "0.10.17",
4
4
  "description": "Deploy the ArchGraph ARGO toolchain, skills, and rules (schema, scripts, argo-init skill, global rule) with one command.",
5
5
  "license": "MIT",
6
6
  "bin": {