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.
|
@@ -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\
|
|
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