archgraph-argo 0.1.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.
Files changed (54) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +115 -0
  3. package/argo/package.json +8 -0
  4. package/argo/rules/intent-architecture-global-rule.md +45 -0
  5. package/argo/schema/ImplementationToCodingHandoff.schema.json +252 -0
  6. package/argo/schema/ImplementationToIntentTraceProposal.schema.json +180 -0
  7. package/argo/schema/IntentToImplementationHandoff.schema.json +75 -0
  8. package/argo/schema/SystemArchitecture.schema.json +378 -0
  9. package/argo/schema/archimate3.2.pdf +0 -0
  10. package/argo/scripts/ARCHITECTURE.md +57 -0
  11. package/argo/scripts/archimate32-rules.js +12301 -0
  12. package/argo/scripts/argo-mcp-server.js +629 -0
  13. package/argo/scripts/argo-paths.js +77 -0
  14. package/argo/scripts/ensureArgoHarnessEnvironment.js +340 -0
  15. package/argo/scripts/generateArchitectureDiffPlantuml.js +466 -0
  16. package/argo/scripts/graph-rag/ARCHITECTURE.md +192 -0
  17. package/argo/scripts/graph-rag/canonicalProjectionAuthority.js +45 -0
  18. package/argo/scripts/graph-rag/defaultSemanticRetrieval.js +969 -0
  19. package/argo/scripts/graph-rag/embeddingQualificationGate.js +59 -0
  20. package/argo/scripts/graph-rag/externalProductionConfig.js +74 -0
  21. package/argo/scripts/graph-rag/liveEmbeddingIndexGate.js +129 -0
  22. package/argo/scripts/graph-rag/liveEmbeddingNeo4jBoundary.js +137 -0
  23. package/argo/scripts/graph-rag/liveEmbeddingProviderClient.js +49 -0
  24. package/argo/scripts/graph-rag/liveEmbeddingProviderConfig.js +481 -0
  25. package/argo/scripts/graph-rag/mutationEmbeddingVectorLifecycle.js +1261 -0
  26. package/argo/scripts/graph-rag/neo4jNativeRetrieval.js +37 -0
  27. package/argo/scripts/graph-rag/productionGraphRagRuntime.js +1624 -0
  28. package/argo/scripts/graph-rag/semantic-persistence/ARCHITECTURE.md +51 -0
  29. package/argo/scripts/graph-rag/semantic-persistence/productionSemanticBackfill.js +241 -0
  30. package/argo/scripts/graph-rag/semantic-persistence/productionSemanticCheckpointStore.js +99 -0
  31. package/argo/scripts/graph-rag/semantic-persistence/productionSemanticNeo4jAdapter.js +149 -0
  32. package/argo/scripts/graph-rag/semantic-persistence/productionSemanticProjectionStore.js +171 -0
  33. package/argo/scripts/graph-rag/semanticOperatorError.js +38 -0
  34. package/argo/scripts/graph-rag/semanticOperatorJourney.js +459 -0
  35. package/argo/scripts/graph-rag/semanticReadinessAttestationStore.js +398 -0
  36. package/argo/scripts/graph-rag/systemMetadataCommandAdapter.js +269 -0
  37. package/argo/scripts/graph-semantics.js +220 -0
  38. package/argo/scripts/neo4j-system-architecture-store.js +777 -0
  39. package/argo/scripts/repositoryArgoEnvironment.js +101 -0
  40. package/argo/scripts/runArchitectureTests.js +583 -0
  41. package/argo/scripts/semanticOperatorJourneyCli.js +91 -0
  42. package/argo/scripts/syncSystemArchitectureToNeo4j.js +67 -0
  43. package/argo/scripts/systemarchitecture-mcp-server.js +2965 -0
  44. package/argo/scripts/test-executors/_template.js +58 -0
  45. package/argo/scripts/test-executors/default.js +199 -0
  46. package/argo/scripts/validateStageHandoff.js +459 -0
  47. package/argo/scripts/validateSystemArchitecture.js +254 -0
  48. package/argo/scripts/validateTraceProposal.js +181 -0
  49. package/argo/scripts/validator-mcp-server.js +377 -0
  50. package/argo/skills/argo-init/SKILL.md +110 -0
  51. package/bin/argo-deploy.js +12 -0
  52. package/install-argo.ps1 +112 -0
  53. package/package.json +28 -0
  54. package/vendor/neo4j-driver-6.2.0.tgz +0 -0
@@ -0,0 +1,777 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ const {
5
+ resolveExternalProductionConfig,
6
+ } = require('./graph-rag/externalProductionConfig.js');
7
+ const {
8
+ getWorkspaceRoot,
9
+ } = require('./argo-paths.js');
10
+
11
+ const DEFAULT_GRAPH_PATH = 'design/KG/SystemArchitecture.json';
12
+ const SYNC_STATE_RELATIVE_PATH = '.argo/temp/neo4j-system-architecture-sync-state.json';
13
+ const LEGACY_NEO4J_ENV_KEYS = Object.freeze([
14
+ 'ARGO_NEO4J_URI',
15
+ 'ARGO_NEO4J_USERNAME',
16
+ 'ARGO_NEO4J_PASSWORD',
17
+ ]);
18
+ const APPROVED_NEO4J_CONFIG = Symbol('approvedNeo4jConfig');
19
+ const DISALLOWED_RUNTIME_OVERRIDE_FIELDS = Object.freeze([
20
+ 'uri',
21
+ 'username',
22
+ 'password',
23
+ 'neo4jUri',
24
+ 'neo4jUsername',
25
+ 'neo4jPassword',
26
+ 'embeddingCredential',
27
+ ]);
28
+
29
+ let neo4jDriverModule;
30
+ function requireNeo4jDriver() {
31
+ if (!neo4jDriverModule) {
32
+ neo4jDriverModule = require('neo4j-driver');
33
+ }
34
+ return neo4jDriverModule;
35
+ }
36
+
37
+ function getRepoRoot() {
38
+ return getWorkspaceRoot();
39
+ }
40
+
41
+ function resolveArchitecturePath(architecturePath = DEFAULT_GRAPH_PATH) {
42
+ return path.join(getRepoRoot(), architecturePath);
43
+ }
44
+
45
+ function resolveSyncStatePath() {
46
+ return path.join(getRepoRoot(), SYNC_STATE_RELATIVE_PATH);
47
+ }
48
+
49
+ function getDefaultNeo4jDatabaseName() {
50
+ const repoName = path.basename(getRepoRoot());
51
+ const normalized = String(repoName)
52
+ .toLowerCase()
53
+ .replace(/[^a-z0-9.-]+/g, '-')
54
+ .replace(/^-+|-+$/g, '')
55
+ .replace(/\.{2,}/g, '.')
56
+ .replace(/-{2,}/g, '-');
57
+ const safe = normalized || 'workspace';
58
+ const prefixed = /^[a-z]/.test(safe) ? safe : `db-${safe}`;
59
+ return prefixed.slice(0, 63);
60
+ }
61
+
62
+ function getNeo4jConfig(overrides = {}) {
63
+ if (overrides && overrides[APPROVED_NEO4J_CONFIG] === true) {
64
+ return overrides;
65
+ }
66
+
67
+ rejectLegacyNeo4jEnvironment();
68
+ rejectRuntimeConfigurationOverrides(overrides);
69
+ const external = resolveExternalProductionConfig({
70
+ neo4jUri: process.env.ARGO_NEO4J_DATABASE_URL,
71
+ neo4jUsername: process.env.ARGO_NEO4J_DATABASE_USERNAME,
72
+ neo4jPassword: process.env.ARGO_NEO4J_DATABASE_PASSWORD,
73
+ embeddingCredential: process.env.QWEN_KEY,
74
+ }, {
75
+ operation: 'start',
76
+ sourceKeys: new Map([
77
+ ['neo4jUri', 'ARGO_NEO4J_DATABASE_URL'],
78
+ ['neo4jUsername', 'ARGO_NEO4J_DATABASE_USERNAME'],
79
+ ['neo4jPassword', 'ARGO_NEO4J_DATABASE_PASSWORD'],
80
+ ['embeddingCredential', 'QWEN_KEY'],
81
+ ]),
82
+ });
83
+ return {
84
+ [APPROVED_NEO4J_CONFIG]: true,
85
+ uri: external.neo4jUri,
86
+ username: external.neo4jUsername,
87
+ password: external.neo4jPassword,
88
+ database: overrides.database || process.env.ARGO_NEO4J_DATABASE || getDefaultNeo4jDatabaseName(),
89
+ };
90
+ }
91
+
92
+ function rejectLegacyNeo4jEnvironment() {
93
+ const legacyKey = LEGACY_NEO4J_ENV_KEYS.find(key => {
94
+ const value = process.env[key];
95
+ return typeof value === 'string' && value.trim().length > 0;
96
+ });
97
+ if (!legacyKey) {
98
+ return;
99
+ }
100
+
101
+ const error = new Error(`${legacyKey} is not an approved Neo4j configuration source`);
102
+ error.category = 'UNSUPPORTED_LEGACY_NEO4J_ENV_ALIAS';
103
+ error.field = legacyKey;
104
+ throw error;
105
+ }
106
+
107
+ function rejectRuntimeConfigurationOverrides(overrides) {
108
+ if (!overrides || typeof overrides !== 'object') {
109
+ return;
110
+ }
111
+
112
+ const field = DISALLOWED_RUNTIME_OVERRIDE_FIELDS.find(candidate => (
113
+ Object.prototype.hasOwnProperty.call(overrides, candidate)
114
+ && overrides[candidate] !== undefined
115
+ ));
116
+ if (!field) {
117
+ return;
118
+ }
119
+
120
+ const error = new Error(`${field} is not an approved runtime configuration source`);
121
+ error.category = 'UNAPPROVED_RUNTIME_CONFIG_SOURCE';
122
+ error.field = field;
123
+ throw error;
124
+ }
125
+
126
+ function createDriver(config = {}) {
127
+ const resolved = getNeo4jConfig(config);
128
+ const neo4j = requireNeo4jDriver();
129
+ return neo4j.driver(
130
+ resolved.uri,
131
+ neo4j.auth.basic(resolved.username, resolved.password),
132
+ );
133
+ }
134
+
135
+ function readArchitectureDocument(architecturePath = DEFAULT_GRAPH_PATH) {
136
+ const absolutePath = resolveArchitecturePath(architecturePath);
137
+ if (!fs.existsSync(absolutePath)) {
138
+ throw new Error(`System architecture file is missing at ${architecturePath}`);
139
+ }
140
+
141
+ try {
142
+ return JSON.parse(fs.readFileSync(absolutePath, 'utf8'));
143
+ } catch (error) {
144
+ throw new Error(`Failed to parse ${architecturePath}: ${String(error)}`);
145
+ }
146
+ }
147
+
148
+ function sanitizeProps(properties) {
149
+ return Object.fromEntries(
150
+ Object.entries(properties).filter(([, value]) => value !== undefined),
151
+ );
152
+ }
153
+
154
+ function asJson(value) {
155
+ return value === undefined ? null : JSON.stringify(value);
156
+ }
157
+
158
+ function asArray(value) {
159
+ return Array.isArray(value) ? value : [];
160
+ }
161
+
162
+ function buildGraphKey(architecturePath = DEFAULT_GRAPH_PATH) {
163
+ return architecturePath.replace(/\\/g, '/');
164
+ }
165
+
166
+ function createEmptySyncState() {
167
+ return {
168
+ version: 1,
169
+ graphs: {},
170
+ };
171
+ }
172
+
173
+ function readNeo4jSyncState() {
174
+ const statePath = resolveSyncStatePath();
175
+ if (!fs.existsSync(statePath)) {
176
+ return createEmptySyncState();
177
+ }
178
+
179
+ try {
180
+ const parsed = JSON.parse(fs.readFileSync(statePath, 'utf8'));
181
+ if (!parsed || typeof parsed !== 'object' || typeof parsed.graphs !== 'object') {
182
+ return createEmptySyncState();
183
+ }
184
+ return parsed;
185
+ } catch {
186
+ return createEmptySyncState();
187
+ }
188
+ }
189
+
190
+ function writeNeo4jSyncState(state) {
191
+ const statePath = resolveSyncStatePath();
192
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
193
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
194
+ }
195
+
196
+ function getNeo4jGraphSyncState(architecturePath = DEFAULT_GRAPH_PATH) {
197
+ const graphKey = buildGraphKey(architecturePath);
198
+ const state = readNeo4jSyncState();
199
+ return {
200
+ graphKey,
201
+ dirty: false,
202
+ ...(state.graphs[graphKey] || {}),
203
+ };
204
+ }
205
+
206
+ function updateNeo4jGraphSyncState(architecturePath, patch) {
207
+ const graphKey = buildGraphKey(architecturePath);
208
+ const state = readNeo4jSyncState();
209
+ state.graphs[graphKey] = {
210
+ graphKey,
211
+ dirty: false,
212
+ ...(state.graphs[graphKey] || {}),
213
+ ...patch,
214
+ };
215
+ writeNeo4jSyncState(state);
216
+ return state.graphs[graphKey];
217
+ }
218
+
219
+ function markNeo4jSyncDirty(architecturePath, error) {
220
+ return updateNeo4jGraphSyncState(architecturePath, {
221
+ dirty: true,
222
+ lastError: String(error && error.message ? error.message : error),
223
+ lastFailureAt: new Date().toISOString(),
224
+ });
225
+ }
226
+
227
+ function markNeo4jSyncClean(architecturePath, verification) {
228
+ const current = getNeo4jGraphSyncState(architecturePath);
229
+ return updateNeo4jGraphSyncState(architecturePath, {
230
+ dirty: false,
231
+ lastError: undefined,
232
+ lastRecoveredAt: current.dirty ? new Date().toISOString() : current.lastRecoveredAt,
233
+ lastSuccessAt: new Date().toISOString(),
234
+ lastVerifiedCounts: verification ? verification.actual : current.lastVerifiedCounts,
235
+ });
236
+ }
237
+
238
+ function isCanonicalArchitecturePath(architecturePath = DEFAULT_GRAPH_PATH) {
239
+ return buildGraphKey(architecturePath) === buildGraphKey(DEFAULT_GRAPH_PATH);
240
+ }
241
+
242
+ function buildGraphRecord(document, graphKey) {
243
+ return sanitizeProps({
244
+ graphKey,
245
+ source_path: graphKey,
246
+ name: document.name,
247
+ description: document.description,
248
+ attributes_json: asJson(document.attributes || []),
249
+ raw_json: asJson(document),
250
+ element_count: asArray(document.elements).length,
251
+ relationship_count: asArray(document.relationships).length,
252
+ view_count: asArray(document.views).length,
253
+ });
254
+ }
255
+
256
+ function buildElementRecord(graphKey, element) {
257
+ return sanitizeProps({
258
+ graphKey,
259
+ id: element.id,
260
+ name: element.name,
261
+ type: element.type,
262
+ parent: element.parent,
263
+ alias: element.alias,
264
+ classifier: element.classifier,
265
+ description: element.description,
266
+ attributes_json: asJson(element.attributes || []),
267
+ subdiagram_views_json: asJson(element.subdiagram_views || []),
268
+ testcases_json: asJson(element.testcases || []),
269
+ raw_json: asJson(element),
270
+ });
271
+ }
272
+
273
+ function buildRelationshipRecord(graphKey, relationship) {
274
+ return sanitizeProps({
275
+ graphKey,
276
+ id: relationship.id,
277
+ name: relationship.name,
278
+ type: relationship.type,
279
+ statement: relationship.statement,
280
+ description: relationship.description,
281
+ document: relationship.document,
282
+ attributes_json: asJson(relationship.attributes || []),
283
+ source_id: relationship.source_id,
284
+ source_name: relationship.source_name,
285
+ target_id: relationship.target_id,
286
+ target_name: relationship.target_name,
287
+ raw_json: asJson(relationship),
288
+ });
289
+ }
290
+
291
+ function buildViewRecord(graphKey, view) {
292
+ return sanitizeProps({
293
+ graphKey,
294
+ view_id: view.view_id,
295
+ view_name: view.view_name,
296
+ parent_element_id: view.parent_element_id,
297
+ parent_element_name: view.parent_element_name,
298
+ description: view.description,
299
+ included_elements_json: asJson(view.included_elements || []),
300
+ included_relationships_json: asJson(view.included_relationships || []),
301
+ raw_json: asJson(view),
302
+ });
303
+ }
304
+
305
+ async function ensureConstraints(driver, database) {
306
+ const session = driver.session({ database });
307
+ try {
308
+ await session.run('CREATE CONSTRAINT argo_architecture_graph_key IF NOT EXISTS FOR (g:ArchitectureGraph) REQUIRE g.graphKey IS UNIQUE');
309
+ await session.run('CREATE CONSTRAINT argo_architecture_element_key IF NOT EXISTS FOR (e:Element) REQUIRE (e.graphKey, e.id) IS UNIQUE');
310
+ await session.run('CREATE CONSTRAINT argo_architecture_relationship_key IF NOT EXISTS FOR (r:ArchitectureRelationship) REQUIRE (r.graphKey, r.id) IS UNIQUE');
311
+ await session.run('CREATE CONSTRAINT argo_architecture_view_key IF NOT EXISTS FOR (v:View) REQUIRE (v.graphKey, v.view_id) IS UNIQUE');
312
+ } finally {
313
+ await session.close();
314
+ }
315
+ }
316
+
317
+ function escapeNeo4jIdentifier(value) {
318
+ return String(value).replace(/`/g, '``');
319
+ }
320
+
321
+ async function ensureDatabaseExists(driver, database) {
322
+ const systemSession = driver.session({ database: 'system' });
323
+ try {
324
+ const existingResult = await systemSession.run(
325
+ 'SHOW DATABASES YIELD name WHERE name = $database RETURN name',
326
+ { database },
327
+ );
328
+ if (existingResult.records.length > 0) {
329
+ return {
330
+ database,
331
+ existed: true,
332
+ created: false,
333
+ };
334
+ }
335
+
336
+ await systemSession.run(`CREATE DATABASE \`${escapeNeo4jIdentifier(database)}\` IF NOT EXISTS`);
337
+ return {
338
+ database,
339
+ existed: false,
340
+ created: true,
341
+ };
342
+ } finally {
343
+ await systemSession.close();
344
+ }
345
+ }
346
+
347
+ async function waitForDatabaseOnline(driver, database, options = {}) {
348
+ const timeoutMs = options.timeoutMs || 15000;
349
+ const pollIntervalMs = options.pollIntervalMs || 250;
350
+ const deadline = Date.now() + timeoutMs;
351
+
352
+ while (Date.now() <= deadline) {
353
+ const systemSession = driver.session({ database: 'system' });
354
+ try {
355
+ const result = await systemSession.run(
356
+ [
357
+ 'SHOW DATABASES YIELD name, currentStatus, requestedStatus',
358
+ 'WHERE name = $database',
359
+ 'RETURN currentStatus, requestedStatus',
360
+ ].join('\n'),
361
+ { database },
362
+ );
363
+ if (result.records.length > 0) {
364
+ const currentStatus = String(result.records[0].get('currentStatus') || '').toLowerCase();
365
+ const requestedStatus = String(result.records[0].get('requestedStatus') || '').toLowerCase();
366
+ if (currentStatus === 'online' && requestedStatus === 'online') {
367
+ return {
368
+ database,
369
+ currentStatus,
370
+ requestedStatus,
371
+ };
372
+ }
373
+ }
374
+ } finally {
375
+ await systemSession.close();
376
+ }
377
+
378
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
379
+ }
380
+
381
+ throw new Error(`Neo4j database '${database}' did not reach online status within ${timeoutMs}ms`);
382
+ }
383
+
384
+ async function clearGraph(tx, graphKey) {
385
+ await tx.run('MATCH (n {graphKey: $graphKey}) DETACH DELETE n', { graphKey });
386
+ }
387
+
388
+ async function writeGraphMetadata(tx, graphRecord) {
389
+ await tx.run(
390
+ [
391
+ 'MERGE (g:ArchitectureGraph {graphKey: $graph.graphKey})',
392
+ 'SET g += $graph',
393
+ 'SET g.synced_at = datetime()',
394
+ ].join('\n'),
395
+ { graph: graphRecord },
396
+ );
397
+ }
398
+
399
+ async function writeElements(tx, graphKey, elements) {
400
+ if (elements.length === 0) {
401
+ return;
402
+ }
403
+
404
+ await tx.run(
405
+ [
406
+ 'UNWIND $rows AS row',
407
+ 'MATCH (g:ArchitectureGraph {graphKey: $graphKey})',
408
+ 'CREATE (e:Element)',
409
+ 'SET e += row',
410
+ 'MERGE (g)-[:OWNS_ELEMENT]->(e)',
411
+ ].join('\n'),
412
+ { graphKey, rows: elements },
413
+ );
414
+ }
415
+
416
+ async function writeRelationships(tx, graphKey, relationships) {
417
+ if (relationships.length === 0) {
418
+ return;
419
+ }
420
+
421
+ await tx.run(
422
+ [
423
+ 'UNWIND $rows AS row',
424
+ 'MATCH (g:ArchitectureGraph {graphKey: $graphKey})',
425
+ 'MATCH (source:Element {graphKey: $graphKey, id: row.source_id})',
426
+ 'MATCH (target:Element {graphKey: $graphKey, id: row.target_id})',
427
+ 'CREATE (rel:ArchitectureRelationship)',
428
+ 'SET rel += row',
429
+ 'MERGE (g)-[:OWNS_RELATIONSHIP]->(rel)',
430
+ 'MERGE (rel)-[:RELATIONSHIP_SOURCE]->(source)',
431
+ 'MERGE (rel)-[:RELATIONSHIP_TARGET]->(target)',
432
+ 'MERGE (source)-[edge:ARCHIMATE_RELATES {graphKey: $graphKey, relationship_id: row.id}]->(target)',
433
+ 'SET edge.relationship_type = row.type,',
434
+ ' edge.name = row.name,',
435
+ ' edge.statement = row.statement,',
436
+ ' edge.source_name = row.source_name,',
437
+ ' edge.target_name = row.target_name',
438
+ ].join('\n'),
439
+ { graphKey, rows: relationships },
440
+ );
441
+ }
442
+
443
+ async function writeViews(tx, graphKey, views) {
444
+ if (views.length === 0) {
445
+ return;
446
+ }
447
+
448
+ await tx.run(
449
+ [
450
+ 'UNWIND $rows AS row',
451
+ 'MATCH (g:ArchitectureGraph {graphKey: $graphKey})',
452
+ 'CREATE (view:View)',
453
+ 'SET view += row',
454
+ 'MERGE (g)-[:OWNS_VIEW]->(view)',
455
+ 'WITH view, row',
456
+ 'OPTIONAL MATCH (parent:Element {graphKey: $graphKey, id: row.parent_element_id})',
457
+ 'FOREACH (_ IN CASE WHEN parent IS NULL THEN [] ELSE [1] END | MERGE (view)-[:VIEW_OF]->(parent))',
458
+ ].join('\n'),
459
+ { graphKey, rows: views },
460
+ );
461
+ }
462
+
463
+ async function writeViewMemberships(tx, graphKey, views) {
464
+ const elementMemberships = [];
465
+ const relationshipMemberships = [];
466
+
467
+ for (const view of views) {
468
+ for (const [index, elementId] of asArray(view.included_elements).entries()) {
469
+ elementMemberships.push({ view_id: view.view_id, element_id: elementId, order: index });
470
+ }
471
+ for (const [index, relationshipId] of asArray(view.included_relationships).entries()) {
472
+ relationshipMemberships.push({ view_id: view.view_id, relationship_id: relationshipId, order: index });
473
+ }
474
+ }
475
+
476
+ if (elementMemberships.length > 0) {
477
+ await tx.run(
478
+ [
479
+ 'UNWIND $rows AS row',
480
+ 'MATCH (view:View {graphKey: $graphKey, view_id: row.view_id})',
481
+ 'MATCH (element:Element {graphKey: $graphKey, id: row.element_id})',
482
+ 'MERGE (view)-[membership:INCLUDES_ELEMENT {order: row.order}]->(element)',
483
+ ].join('\n'),
484
+ { graphKey, rows: elementMemberships },
485
+ );
486
+ }
487
+
488
+ if (relationshipMemberships.length > 0) {
489
+ await tx.run(
490
+ [
491
+ 'UNWIND $rows AS row',
492
+ 'MATCH (view:View {graphKey: $graphKey, view_id: row.view_id})',
493
+ 'MATCH (relationship:ArchitectureRelationship {graphKey: $graphKey, id: row.relationship_id})',
494
+ 'MERGE (view)-[membership:INCLUDES_RELATIONSHIP {order: row.order}]->(relationship)',
495
+ ].join('\n'),
496
+ { graphKey, rows: relationshipMemberships },
497
+ );
498
+ }
499
+ }
500
+
501
+ async function writeSubdiagramLinks(tx, graphKey, elements) {
502
+ const rows = [];
503
+
504
+ for (const element of elements) {
505
+ for (const subdiagramView of asArray(element.subdiagram_views)) {
506
+ rows.push({ element_id: element.id, view_id: subdiagramView.view_id });
507
+ }
508
+ }
509
+
510
+ if (rows.length === 0) {
511
+ return;
512
+ }
513
+
514
+ await tx.run(
515
+ [
516
+ 'UNWIND $rows AS row',
517
+ 'MATCH (element:Element {graphKey: $graphKey, id: row.element_id})',
518
+ 'MATCH (view:View {graphKey: $graphKey, view_id: row.view_id})',
519
+ 'MERGE (element)-[:HAS_SUBDIAGRAM]->(view)',
520
+ ].join('\n'),
521
+ { graphKey, rows },
522
+ );
523
+ }
524
+
525
+ async function syncArchitectureToNeo4j(options = {}) {
526
+ const architecturePath = options.architecturePath || DEFAULT_GRAPH_PATH;
527
+ const graphKey = buildGraphKey(architecturePath);
528
+ const document = options.document || readArchitectureDocument(architecturePath);
529
+ const config = getNeo4jConfig(options);
530
+ const driver = options.driver || createDriver(config);
531
+ const ownDriver = !options.driver;
532
+
533
+ try {
534
+ await driver.verifyConnectivity();
535
+ const databaseProvision = await ensureDatabaseExists(driver, config.database);
536
+ const databaseStatus = await waitForDatabaseOnline(driver, config.database);
537
+ await ensureConstraints(driver, config.database);
538
+
539
+ const session = driver.session({ database: config.database });
540
+ try {
541
+ await session.executeWrite(async tx => {
542
+ await clearGraph(tx, graphKey);
543
+ await writeGraphMetadata(tx, buildGraphRecord(document, graphKey));
544
+ await writeElements(tx, graphKey, asArray(document.elements).map(element => buildElementRecord(graphKey, element)));
545
+ await writeRelationships(tx, graphKey, asArray(document.relationships).map(relationship => buildRelationshipRecord(graphKey, relationship)));
546
+ await writeViews(tx, graphKey, asArray(document.views).map(view => buildViewRecord(graphKey, view)));
547
+ await writeViewMemberships(tx, graphKey, asArray(document.views));
548
+ await writeSubdiagramLinks(tx, graphKey, asArray(document.elements));
549
+ });
550
+ } finally {
551
+ await session.close();
552
+ }
553
+
554
+ const verification = await verifyArchitectureSync({
555
+ architecturePath,
556
+ document,
557
+ driver,
558
+ ...config,
559
+ });
560
+
561
+ if (!verification.matches) {
562
+ throw new Error(`Neo4j sync verification mismatch for ${graphKey}`);
563
+ }
564
+
565
+ if (isCanonicalArchitecturePath(architecturePath)) {
566
+ markNeo4jSyncClean(architecturePath, verification);
567
+ }
568
+
569
+ return {
570
+ architecturePath,
571
+ graphKey,
572
+ databaseProvision: {
573
+ ...databaseProvision,
574
+ ...databaseStatus,
575
+ },
576
+ counts: verification.expected,
577
+ verification,
578
+ };
579
+ } catch (error) {
580
+ if (isCanonicalArchitecturePath(architecturePath)) {
581
+ markNeo4jSyncDirty(architecturePath, error);
582
+ }
583
+ throw error;
584
+ } finally {
585
+ if (ownDriver) {
586
+ await driver.close();
587
+ }
588
+ }
589
+ }
590
+
591
+ async function recoverNeo4jSyncIfNeeded(options = {}) {
592
+ const architecturePath = options.architecturePath || DEFAULT_GRAPH_PATH;
593
+ if (!isCanonicalArchitecturePath(architecturePath)) {
594
+ return {
595
+ attempted: false,
596
+ eligible: false,
597
+ dirty: false,
598
+ };
599
+ }
600
+
601
+ const syncState = getNeo4jGraphSyncState(architecturePath);
602
+ if (!syncState.dirty) {
603
+ return {
604
+ attempted: false,
605
+ eligible: true,
606
+ dirty: false,
607
+ };
608
+ }
609
+
610
+ try {
611
+ const result = await syncArchitectureToNeo4j(options);
612
+ return {
613
+ attempted: true,
614
+ eligible: true,
615
+ dirty: false,
616
+ status: 'passed',
617
+ graphKey: result.graphKey,
618
+ databaseProvision: result.databaseProvision,
619
+ counts: result.counts,
620
+ previousFailure: syncState.lastError,
621
+ };
622
+ } catch (error) {
623
+ return {
624
+ attempted: true,
625
+ eligible: true,
626
+ dirty: true,
627
+ status: 'failed',
628
+ graphKey: syncState.graphKey,
629
+ previousFailure: syncState.lastError,
630
+ error: String(error && error.message ? error.message : error),
631
+ };
632
+ }
633
+ }
634
+
635
+ async function verifyArchitectureSync(options = {}) {
636
+ const architecturePath = options.architecturePath || DEFAULT_GRAPH_PATH;
637
+ const graphKey = buildGraphKey(architecturePath);
638
+ const document = options.document || readArchitectureDocument(architecturePath);
639
+ const config = getNeo4jConfig(options);
640
+ const driver = options.driver || createDriver(config);
641
+ const ownDriver = !options.driver;
642
+
643
+ try {
644
+ await driver.verifyConnectivity();
645
+ const databaseProvision = await ensureDatabaseExists(driver, config.database);
646
+ const databaseStatus = await waitForDatabaseOnline(driver, config.database);
647
+ const session = driver.session({ database: config.database });
648
+ try {
649
+ const countsResult = await session.executeRead(tx => tx.run(
650
+ [
651
+ 'MATCH (g:ArchitectureGraph {graphKey: $graphKey})',
652
+ 'RETURN',
653
+ ' g.name AS name,',
654
+ ' g.description AS description,',
655
+ ' g.element_count AS declaredElementCount,',
656
+ ' g.relationship_count AS declaredRelationshipCount,',
657
+ ' g.view_count AS declaredViewCount,',
658
+ ' COUNT { (g)-[:OWNS_ELEMENT]->() } AS elementCount,',
659
+ ' COUNT { (g)-[:OWNS_RELATIONSHIP]->() } AS relationshipCount,',
660
+ ' COUNT { (g)-[:OWNS_VIEW]->() } AS viewCount',
661
+ ].join('\n'),
662
+ { graphKey },
663
+ ));
664
+
665
+ if (countsResult.records.length === 0) {
666
+ throw new Error(`No ArchitectureGraph node found for ${graphKey}`);
667
+ }
668
+
669
+ const idResult = await session.executeRead(tx => tx.run(
670
+ [
671
+ 'CALL {',
672
+ ' MATCH (e:Element {graphKey: $graphKey})',
673
+ ' RETURN collect(e.id) AS elementIds',
674
+ '}',
675
+ 'CALL {',
676
+ ' MATCH (r:ArchitectureRelationship {graphKey: $graphKey})',
677
+ ' RETURN collect(r.id) AS relationshipIds',
678
+ '}',
679
+ 'CALL {',
680
+ ' MATCH (v:View {graphKey: $graphKey})',
681
+ ' RETURN collect(v.view_id) AS viewIds',
682
+ '}',
683
+ 'RETURN elementIds, relationshipIds, viewIds',
684
+ ].join('\n'),
685
+ { graphKey },
686
+ ));
687
+
688
+ const countsRecord = countsResult.records[0];
689
+ const idsRecord = idResult.records[0];
690
+ const expected = buildExpectedCounts(document);
691
+ const actual = {
692
+ elements: toNumber(countsRecord.get('elementCount')),
693
+ relationships: toNumber(countsRecord.get('relationshipCount')),
694
+ views: toNumber(countsRecord.get('viewCount')),
695
+ };
696
+
697
+ const actualIds = {
698
+ elements: sortStrings(idsRecord.get('elementIds')),
699
+ relationships: sortStrings(idsRecord.get('relationshipIds')),
700
+ views: sortStrings(idsRecord.get('viewIds')),
701
+ };
702
+ const expectedIds = {
703
+ elements: sortStrings(asArray(document.elements).map(element => element.id)),
704
+ relationships: sortStrings(asArray(document.relationships).map(relationship => relationship.id)),
705
+ views: sortStrings(asArray(document.views).map(view => view.view_id)),
706
+ };
707
+
708
+ return {
709
+ graphKey,
710
+ databaseProvision: {
711
+ ...databaseProvision,
712
+ ...databaseStatus,
713
+ },
714
+ expected,
715
+ actual,
716
+ matches: (
717
+ expected.elements === actual.elements
718
+ && expected.relationships === actual.relationships
719
+ && expected.views === actual.views
720
+ && arraysEqual(expectedIds.elements, actualIds.elements)
721
+ && arraysEqual(expectedIds.relationships, actualIds.relationships)
722
+ && arraysEqual(expectedIds.views, actualIds.views)
723
+ ),
724
+ expectedIds,
725
+ actualIds,
726
+ };
727
+ } finally {
728
+ await session.close();
729
+ }
730
+ } finally {
731
+ if (ownDriver) {
732
+ await driver.close();
733
+ }
734
+ }
735
+ }
736
+
737
+ function buildExpectedCounts(document) {
738
+ return {
739
+ elements: asArray(document.elements).length,
740
+ relationships: asArray(document.relationships).length,
741
+ views: asArray(document.views).length,
742
+ };
743
+ }
744
+
745
+ function sortStrings(values) {
746
+ return asArray(values).map(value => String(value)).sort();
747
+ }
748
+
749
+ function arraysEqual(left, right) {
750
+ return left.length === right.length && left.every((value, index) => value === right[index]);
751
+ }
752
+
753
+ function toNumber(value) {
754
+ if (requireNeo4jDriver().isInt(value)) {
755
+ return value.toNumber();
756
+ }
757
+ return Number(value);
758
+ }
759
+
760
+ module.exports = {
761
+ DEFAULT_GRAPH_PATH,
762
+ buildGraphKey,
763
+ createDriver,
764
+ ensureDatabaseExists,
765
+ getNeo4jGraphSyncState,
766
+ getNeo4jConfig,
767
+ getDefaultNeo4jDatabaseName,
768
+ isCanonicalArchitecturePath,
769
+ markNeo4jSyncDirty,
770
+ readArchitectureDocument,
771
+ readNeo4jSyncState,
772
+ recoverNeo4jSyncIfNeeded,
773
+ resolveArchitecturePath,
774
+ syncArchitectureToNeo4j,
775
+ verifyArchitectureSync,
776
+ waitForDatabaseOnline,
777
+ };