archgraph-argo 0.10.52 → 0.12.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.
- package/argo/defaults/EA-model-template.feap +0 -0
- package/argo/defaults/EA-model-template.qea +0 -0
- package/argo/scripts/argo-mcp-server.js +56 -3
- package/argo/scripts/ea-qea-sync-lib.js +688 -0
- package/argo/scripts/ea-qea-sync.js +109 -0
- package/argo/scripts/systemarchitecture-mcp-server.js +73 -0
- package/package.json +3 -2
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
const fs = require('node:fs');
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
const readline = require('node:readline');
|
|
4
|
-
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
4
|
+
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
5
|
+
const { spawnSync } = require('node:child_process');
|
|
5
6
|
|
|
6
7
|
const validatorMcp = require('./validator-mcp-server.js');
|
|
7
8
|
const systemArchitectureMcp = require('./systemarchitecture-mcp-server.js');
|
|
@@ -395,7 +396,8 @@ async function callTool(name, args = {}, progressToken = null, dependencies = un
|
|
|
395
396
|
mcp: report.mcp,
|
|
396
397
|
systemArchitecture: report.systemArchitecture,
|
|
397
398
|
subdiagramViews: report.subdiagramViews,
|
|
398
|
-
neo4j: report.neo4j,
|
|
399
|
+
neo4j: report.neo4j,
|
|
400
|
+
qeaFullProjection: workspace.qeaFullProjection,
|
|
399
401
|
semanticLifecycle: report.semanticLifecycle,
|
|
400
402
|
semanticState: report.semanticLifecycle && report.semanticLifecycle.state,
|
|
401
403
|
alignment: report.semanticLifecycle && report.semanticLifecycle.alignment,
|
|
@@ -418,6 +420,48 @@ async function withCanonicalSemanticInitTestComposition(composition, callback) {
|
|
|
418
420
|
return canonicalSemanticInitStorage.run(Object.freeze({ ...composition }), callback);
|
|
419
421
|
}
|
|
420
422
|
|
|
423
|
+
function resolveQeaProjectionTarget(workspaceRoot) {
|
|
424
|
+
try {
|
|
425
|
+
if (!workspaceRoot || !fs.existsSync(workspaceRoot)) { return null; }
|
|
426
|
+
const pick = (p) => (p && fs.existsSync(p) ? path.resolve(p) : null);
|
|
427
|
+
const qeaPath = pick(process.env.ARGO_EA_QEA);
|
|
428
|
+
if (qeaPath) { return { qeaPath, workspaceRoot }; }
|
|
429
|
+
let qeas = [];
|
|
430
|
+
try { qeas = fs.readdirSync(workspaceRoot).filter((n) => n.toLowerCase().endsWith('.qea')); } catch { /* ignore */ }
|
|
431
|
+
if (qeas.length === 1) { return { qeaPath: path.resolve(workspaceRoot, qeas[0]), workspaceRoot }; }
|
|
432
|
+
console.log('[ea-qea] init projection target: none' + (qeas.length > 1 ? ' (' + qeas.length + ' *.qea found; expected exactly one or ARGO_EA_QEA)' : '') + ' in ' + workspaceRoot);
|
|
433
|
+
return null;
|
|
434
|
+
} catch (error) {
|
|
435
|
+
console.log('[ea-qea] init projection target resolution failed: ' + String(error && error.message ? error.message : error));
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// argo init .qea FULL projection (mirrors Neo4j initial full sync): wipes the whole target
|
|
441
|
+
// .qea then rebuilds it purely from the canonical graph. Non-fatal by contract.
|
|
442
|
+
function runQeaFullProjection(workspaceRoot, graphTargetPath) {
|
|
443
|
+
const target = resolveQeaProjectionTarget(workspaceRoot);
|
|
444
|
+
const script = path.join(__dirname, 'ea-qea-sync.js');
|
|
445
|
+
if (!target || !fs.existsSync(script)) {
|
|
446
|
+
return { status: 'noop', reason: !target ? 'no .qea target (env ARGO_EA_QEA or exactly one root *.qea)' : 'argo/scripts/ea-qea-sync.js missing' };
|
|
447
|
+
}
|
|
448
|
+
const snapshotDir = path.join(workspaceRoot, '.argo', 'temp', 'qea-backups');
|
|
449
|
+
const args = [script, '--mode', 'full', '--graph', graphTargetPath, '--qea', target.qeaPath, '--snapshot-dir', snapshotDir];
|
|
450
|
+
const started = Date.now();
|
|
451
|
+
try {
|
|
452
|
+
const res = spawnSync(process.execPath, args, { cwd: workspaceRoot, encoding: 'utf8', windowsHide: true, timeout: 120000 });
|
|
453
|
+
const ok = res.status === 0;
|
|
454
|
+
return {
|
|
455
|
+
status: ok ? 'ok' : 'failed',
|
|
456
|
+
qea: target.qeaPath,
|
|
457
|
+
ms: Date.now() - started,
|
|
458
|
+
...(ok ? {} : { error: String((res.stderr || '').slice(0, 600) || ('exit ' + res.status)) }),
|
|
459
|
+
};
|
|
460
|
+
} catch (error) {
|
|
461
|
+
return { status: 'failed', qea: target.qeaPath, ms: Date.now() - started, error: String(error && error.message ? error.message : error) };
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
421
465
|
async function initializeWorkspace(workspaceRoot) {
|
|
422
466
|
const workspaceName = path.basename(workspaceRoot);
|
|
423
467
|
const createdFiles = [];
|
|
@@ -454,8 +498,17 @@ async function initializeWorkspace(workspaceRoot) {
|
|
|
454
498
|
}
|
|
455
499
|
}
|
|
456
500
|
|
|
501
|
+
// WP2791: init .qea FULL projection (parallel to Neo4j initial full sync in the harness
|
|
502
|
+
// report). Non-fatal: a qea failure must never fail workspace init.
|
|
503
|
+
let qeaFullProjection = null;
|
|
504
|
+
try {
|
|
505
|
+
qeaFullProjection = runQeaFullProjection(workspaceRoot, graphTargetPath);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
qeaFullProjection = { status: 'failed', error: String(error && error.message ? error.message : error) };
|
|
508
|
+
}
|
|
457
509
|
return {
|
|
458
|
-
workspaceRoot,
|
|
510
|
+
workspaceRoot,
|
|
511
|
+
qeaFullProjection,
|
|
459
512
|
targetFeapName,
|
|
460
513
|
createdFiles,
|
|
461
514
|
updatedFiles,
|
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// EA .qea (SQLite) direct projection library — WP2791.
|
|
4
|
+
// Zero third-party deps: node:sqlite (DatabaseSync, Node >=22/24), node:crypto, node:fs.
|
|
5
|
+
// Mirrors the ArchiMate->EA mapping conventions of eatool/EA-jsscript/import-from-kg.js
|
|
6
|
+
// (Object_Type base + Stereotype archimate name + Alias/ea_guid anchors + schema_view_id
|
|
7
|
+
// StyleEx token) while writing through SQLite directly (no EA COM required).
|
|
8
|
+
//
|
|
9
|
+
// Concurrency model:
|
|
10
|
+
// - PRAGMA busy_timeout = 15000 set on every connection; write phases wrapped in short
|
|
11
|
+
// transactions with a bounded busy retry. Keep EA's own SQLite handle open is fine —
|
|
12
|
+
// SQLite only locks during active transactions, an idle open connection does not block.
|
|
13
|
+
// - Rows are matched by Alias (schema id) / deterministic ea_guid, update-in-place only;
|
|
14
|
+
// existing t_diagramobjects/t_diagramlinks geometry is NEVER updated or deleted, only
|
|
15
|
+
// missing members are INSERTed.
|
|
16
|
+
|
|
17
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
18
|
+
const DEBUG = !!process.env.EA_QEA_DEBUG;
|
|
19
|
+
const crypto = require('node:crypto');
|
|
20
|
+
|
|
21
|
+
const SYNC_PACKAGE_NAME = 'ArchGraph Sync';
|
|
22
|
+
const DIAGRAM_TYPE = 'Logical';
|
|
23
|
+
const META_TABLE = 'kg_sync_meta'; // {kind,key,sha,payload} — Node export/reconcile store
|
|
24
|
+
const BUSY_TIMEOUT_MS = 15000;
|
|
25
|
+
const CHUNK = 200;
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// ArchiMate -> EA mapping (mirrors import-from-kg.js)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
function normalizeName(value) {
|
|
31
|
+
const text = String(value === null || value === undefined ? '' : value);
|
|
32
|
+
return text.replace(/^ArchiMate[_\s-]*/i, '');
|
|
33
|
+
}
|
|
34
|
+
function alnum(value) {
|
|
35
|
+
return String(value === null || value === undefined ? '' : value)
|
|
36
|
+
.replace(/&/g, '')
|
|
37
|
+
.replace(/[^A-Za-z0-9]/g, '');
|
|
38
|
+
}
|
|
39
|
+
function elementStereotype(type) {
|
|
40
|
+
const norm = alnum(normalizeName(type));
|
|
41
|
+
if (norm === '') { return ''; }
|
|
42
|
+
if (norm === 'SystemSoftware') { return 'ArchiMate_SystemSoftware'; }
|
|
43
|
+
if (norm === 'Constraint') { return 'ArchiMate_Constraint'; }
|
|
44
|
+
return norm;
|
|
45
|
+
}
|
|
46
|
+
const ACTIVITY_ALNUM = new Set([
|
|
47
|
+
'BusinessEvent', 'BusinessProcess', 'BusinessFunction', 'BusinessInteraction',
|
|
48
|
+
'BusinessService', 'ApplicationEvent', 'ApplicationProcess', 'ApplicationFunction',
|
|
49
|
+
'ApplicationInteraction', 'ApplicationService', 'TechnologyEvent',
|
|
50
|
+
'TechnologyProcess', 'TechnologyFunction', 'TechnologyInteraction',
|
|
51
|
+
'TechnologyService', 'ValueStream',
|
|
52
|
+
]);
|
|
53
|
+
function elementObjectType(type) {
|
|
54
|
+
const norm = alnum(normalizeName(type));
|
|
55
|
+
if (norm === 'ApplicationComponent') { return 'Component'; }
|
|
56
|
+
if (ACTIVITY_ALNUM.has(norm)) { return 'Activity'; }
|
|
57
|
+
if (norm === 'Junction' || norm === 'AndJunction' || norm === 'OrJunction') { return 'StateNode'; }
|
|
58
|
+
return 'Class';
|
|
59
|
+
}
|
|
60
|
+
function canonicalArchimateType(type) {
|
|
61
|
+
const norm = alnum(normalizeName(type));
|
|
62
|
+
return norm; // full ArchiMate name e.g. BusinessActor is carried by Stereotype column
|
|
63
|
+
}
|
|
64
|
+
function relStereotype(type) {
|
|
65
|
+
return alnum(normalizeName(type));
|
|
66
|
+
}
|
|
67
|
+
function relationshipMap(type) {
|
|
68
|
+
const norm = alnum(normalizeName(type));
|
|
69
|
+
const meta = { connectorType: 'Association', directed: false };
|
|
70
|
+
switch (norm) {
|
|
71
|
+
case 'Composition':
|
|
72
|
+
case 'Aggregation':
|
|
73
|
+
meta.connectorType = 'Association';
|
|
74
|
+
break;
|
|
75
|
+
case 'Specialization':
|
|
76
|
+
meta.connectorType = 'Generalization';
|
|
77
|
+
meta.directed = true;
|
|
78
|
+
break;
|
|
79
|
+
case 'Realization':
|
|
80
|
+
case 'Access':
|
|
81
|
+
meta.connectorType = 'Dependency';
|
|
82
|
+
meta.directed = true;
|
|
83
|
+
break;
|
|
84
|
+
case 'Serving':
|
|
85
|
+
case 'Assignment':
|
|
86
|
+
meta.connectorType = 'Association';
|
|
87
|
+
meta.directed = true;
|
|
88
|
+
break;
|
|
89
|
+
case 'Association':
|
|
90
|
+
meta.connectorType = 'Association';
|
|
91
|
+
break;
|
|
92
|
+
case 'Triggering':
|
|
93
|
+
case 'Flow':
|
|
94
|
+
case 'Influence':
|
|
95
|
+
meta.connectorType = 'ControlFlow';
|
|
96
|
+
meta.directed = true;
|
|
97
|
+
break;
|
|
98
|
+
default:
|
|
99
|
+
meta.connectorType = 'Association';
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
return meta;
|
|
103
|
+
}
|
|
104
|
+
function safeName(name, fallbackId) {
|
|
105
|
+
const n = String(name === null || name === undefined ? '' : name).trim();
|
|
106
|
+
return n !== '' ? n : String(fallbackId === null || fallbackId === undefined ? '' : fallbackId);
|
|
107
|
+
}
|
|
108
|
+
function deterministicGuid(seed) {
|
|
109
|
+
const h = crypto.createHash('sha1').update(String(seed)).digest('hex');
|
|
110
|
+
return '{' + h.slice(0, 8) + '-' + h.slice(8, 12) + '-' + h.slice(12, 16) + '-' + h.slice(16, 20) + '-' + h.slice(20, 32) + '}';
|
|
111
|
+
}
|
|
112
|
+
function sha1(text) {
|
|
113
|
+
return crypto.createHash('sha1').update(String(text)).digest('hex');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// sqlite helpers
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
function openQea(file) {
|
|
120
|
+
const db = new DatabaseSync(file);
|
|
121
|
+
db.exec('PRAGMA busy_timeout=' + BUSY_TIMEOUT_MS);
|
|
122
|
+
db.exec('PRAGMA foreign_keys=OFF');
|
|
123
|
+
return db;
|
|
124
|
+
}
|
|
125
|
+
function columnNames(db, table) {
|
|
126
|
+
const rows = db.prepare('PRAGMA table_info(' + table + ')').all();
|
|
127
|
+
return rows.map((r) => r.name);
|
|
128
|
+
}
|
|
129
|
+
function ensureMetaTable(db) {
|
|
130
|
+
db.exec('CREATE TABLE IF NOT EXISTS ' + META_TABLE + ' (kind TEXT NOT NULL, key TEXT NOT NULL, sha TEXT, payload TEXT, PRIMARY KEY(kind,key))');
|
|
131
|
+
}
|
|
132
|
+
// multi-row INSERT with positional placeholders, chunked
|
|
133
|
+
function insertMany(db, table, columns, rows) {
|
|
134
|
+
let inserted = 0;
|
|
135
|
+
if (!rows || rows.length === 0) { return 0; }
|
|
136
|
+
for (let i = 0; i < rows.length; i += CHUNK) {
|
|
137
|
+
const chunk = rows.slice(i, i + CHUNK);
|
|
138
|
+
const colSql = columns.join(', ');
|
|
139
|
+
const valueSql = chunk.map(() => '(' + columns.map(() => '?').join(', ') + ')').join(', ');
|
|
140
|
+
const sql = 'INSERT INTO ' + table + ' (' + colSql + ') VALUES ' + valueSql;
|
|
141
|
+
const params = [];
|
|
142
|
+
for (const r of chunk) { for (const c of columns) { params.push(r[c]); } }
|
|
143
|
+
db.prepare(sql).run(...params);
|
|
144
|
+
inserted += chunk.length;
|
|
145
|
+
}
|
|
146
|
+
return inserted;
|
|
147
|
+
}
|
|
148
|
+
function updateRow(db, table, setColumns, whereColumn, id, values) {
|
|
149
|
+
const setSql = setColumns.map((c) => c + '=?').join(', ');
|
|
150
|
+
db.prepare('UPDATE ' + table + ' SET ' + setSql + ' WHERE ' + whereColumn + '=?')
|
|
151
|
+
.run(...setColumns.map((c) => values[c]), id);
|
|
152
|
+
}
|
|
153
|
+
function upsertMeta(db, kind, key, node) {
|
|
154
|
+
const payload = JSON.stringify(node);
|
|
155
|
+
const sh = sha1(payload);
|
|
156
|
+
db.prepare('INSERT INTO ' + META_TABLE + ' (kind,key,sha,payload) VALUES (?,?,?,?) ON CONFLICT(kind,key) DO UPDATE SET sha=excluded.sha, payload=excluded.payload')
|
|
157
|
+
.run(kind, String(key), sh, payload);
|
|
158
|
+
return sh;
|
|
159
|
+
}
|
|
160
|
+
function readMetaKind(db, kind) {
|
|
161
|
+
const rows = db.prepare('SELECT key, payload FROM ' + META_TABLE + ' WHERE kind=? ORDER BY key').all(kind);
|
|
162
|
+
const out = [];
|
|
163
|
+
for (const r of rows) { try { out.push(JSON.parse(r.payload)); } catch { /* skip corrupt */ } }
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
// mark checkpoints
|
|
167
|
+
function nowMs() { return Date.now(); }
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Package anchoring
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
function resolveSyncPackage(db, dryRun) {
|
|
173
|
+
let roots = db.prepare('SELECT Package_ID FROM t_package WHERE Parent_ID=0 ORDER BY Package_ID').all();
|
|
174
|
+
if (roots.length === 0) { roots = db.prepare('SELECT Package_ID FROM t_package ORDER BY Package_ID LIMIT 1').all(); }
|
|
175
|
+
const rootId = Number(roots[0].Package_ID);
|
|
176
|
+
let existing = db.prepare('SELECT Package_ID, Name FROM t_package WHERE Parent_ID=? AND Name=?').get(rootId, SYNC_PACKAGE_NAME);
|
|
177
|
+
if (existing) { return { rootId, packageId: Number(existing.Package_ID), created: false }; }
|
|
178
|
+
if (dryRun) { return { rootId, packageId: 0, created: true }; }
|
|
179
|
+
const guid = deterministicGuid('pkg:' + SYNC_PACKAGE_NAME);
|
|
180
|
+
const r = db.prepare('INSERT INTO t_package (Name, Parent_ID, ea_guid, Notes) VALUES (?,?,?,?)').run(SYNC_PACKAGE_NAME, rootId, guid, '');
|
|
181
|
+
return { rootId, packageId: Number(r.lastInsertRowid), created: true };
|
|
182
|
+
}
|
|
183
|
+
function parseStyleToken(styleEx, key) {
|
|
184
|
+
const text = String(styleEx === null || styleEx === undefined ? '' : styleEx);
|
|
185
|
+
const re = new RegExp('(^|;|\\s)' + key + '=([^;]*)', 'i');
|
|
186
|
+
const m = re.exec(text);
|
|
187
|
+
return m ? m[2] : '';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Core sync
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// opts: { dryRun, allowDelete, snapshotDir }
|
|
194
|
+
function syncGraphToQea(graph, qeaPath, opts) {
|
|
195
|
+
const o = opts || {};
|
|
196
|
+
const stages = {};
|
|
197
|
+
const stats = {
|
|
198
|
+
added: { elements: 0, relationships: 0, diagrams: 0, diagramObjects: 0, diagramLinks: 0 },
|
|
199
|
+
updated: { elements: 0, relationships: 0, diagrams: 0 },
|
|
200
|
+
skipped: { elements: 0, relationships: 0, diagrams: 0 },
|
|
201
|
+
deleteCandidates: 0, deleted: 0,
|
|
202
|
+
};
|
|
203
|
+
const t0 = nowMs();
|
|
204
|
+
const db = openQea(qeaPath);
|
|
205
|
+
try {
|
|
206
|
+
ensureMetaTable(db);
|
|
207
|
+
if (!o.dryRun) { db.exec('BEGIN IMMEDIATE'); }
|
|
208
|
+
stages.package = nowMs();
|
|
209
|
+
|
|
210
|
+
const syncPkg = resolveSyncPackage(db, o.dryRun);
|
|
211
|
+
const syncId = syncPkg.packageId;
|
|
212
|
+
stats.syncPackageId = syncId;
|
|
213
|
+
|
|
214
|
+
// --- elements ---------------------------------------------------------
|
|
215
|
+
const elemById = new Map();
|
|
216
|
+
for (const e of graph.elements || []) { if (e && e.id !== undefined && e.id !== null) { elemById.set(String(e.id), e); } }
|
|
217
|
+
const existingElems = db.prepare(
|
|
218
|
+
'SELECT Object_ID, Alias, ea_guid, Object_Type, Name, Stereotype, Note, Status, Package_ID, ParentID FROM t_object WHERE Package_ID=?').all(syncId);
|
|
219
|
+
const elemByAlias = new Map();
|
|
220
|
+
const elemByGuid = new Map();
|
|
221
|
+
for (const row of existingElems) {
|
|
222
|
+
if (row.Alias) { elemByAlias.set(String(row.Alias), row); }
|
|
223
|
+
if (row.ea_guid) { elemByGuid.set(String(row.ea_guid), row); }
|
|
224
|
+
}
|
|
225
|
+
const newElems = [];
|
|
226
|
+
const parentOf = {};
|
|
227
|
+
for (const e of graph.elements || []) {
|
|
228
|
+
if (!e || e.id === undefined || e.id === null) { continue; }
|
|
229
|
+
const alias = String(e.id);
|
|
230
|
+
const guid = deterministicGuid('el:' + alias);
|
|
231
|
+
const existing = elemByAlias.get(alias) || elemByGuid.get(guid) || null;
|
|
232
|
+
const parentId = e.parent !== undefined && e.parent !== null && String(e.parent) !== '0' && elemById.has(String(e.parent)) ? parentOf[String(e.parent)] || 0 : 0;
|
|
233
|
+
const intended = {
|
|
234
|
+
Object_Type: elementObjectType(e.type),
|
|
235
|
+
Name: safeName(e.name, alias),
|
|
236
|
+
Stereotype: elementStereotype(e.type),
|
|
237
|
+
Note: String(e.description === null || e.description === undefined ? '' : e.description),
|
|
238
|
+
Status: String(e.status === null || e.status === undefined ? 'Proposed' : e.status),
|
|
239
|
+
ParentID: parentId || 0,
|
|
240
|
+
};
|
|
241
|
+
if (existing) {
|
|
242
|
+
const changed = intended.Object_Type !== existing.Object_Type || intended.Name !== existing.Name ||
|
|
243
|
+
(intended.Stereotype || '') !== (existing.Stereotype || '') || intended.Note !== (existing.Note || '') ||
|
|
244
|
+
(intended.Status || '') !== (existing.Status || '') || Number(intended.ParentID) !== Number(existing.ParentID || 0);
|
|
245
|
+
if (changed && !o.dryRun) {
|
|
246
|
+
updateRow(db, 't_object', ['Name', 'Stereotype', 'Note', 'Status', 'ParentID'], 'Object_ID', Number(existing.Object_ID), intended);
|
|
247
|
+
}
|
|
248
|
+
stats[changed ? 'updated' : 'skipped'].elements++;
|
|
249
|
+
parentOf[alias] = Number(existing.Object_ID);
|
|
250
|
+
} else {
|
|
251
|
+
newElems.push({ ...intended, Alias: alias, ea_guid: guid, Package_ID: syncId });
|
|
252
|
+
stats.added.elements++;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// batch insert new elements (ParentID refined below) then read ids
|
|
256
|
+
if (newElems.length > 0) {
|
|
257
|
+
if (!o.dryRun) {
|
|
258
|
+
insertMany(db, 't_object', ['Object_Type', 'Name', 'Stereotype', 'Note', 'Status', 'Alias', 'ea_guid', 'Package_ID', 'ParentID'], newElems);
|
|
259
|
+
}
|
|
260
|
+
for (const row of newElems) { parentOf[row.Alias] = row.Alias; } // placeholder; resolved by readback
|
|
261
|
+
}
|
|
262
|
+
// resolve real Object_ID for new elements + parents
|
|
263
|
+
const aliasToId = new Map();
|
|
264
|
+
if (!o.dryRun) {
|
|
265
|
+
const aliases = newElems.map((r) => r.Alias);
|
|
266
|
+
const readback = [];
|
|
267
|
+
for (let i = 0; i < aliases.length; i += 200) {
|
|
268
|
+
const part = aliases.slice(i, i + 200);
|
|
269
|
+
const marks = part.map(() => '?').join(',');
|
|
270
|
+
const rows = db.prepare('SELECT Object_ID, Alias FROM t_object WHERE Alias IN (' + marks + ')').all(...part);
|
|
271
|
+
readback.push(...rows);
|
|
272
|
+
}
|
|
273
|
+
for (const r of readback) { aliasToId.set(String(r.Alias), Number(r.Object_ID)); }
|
|
274
|
+
} else {
|
|
275
|
+
for (const r of newElems) { aliasToId.set(r.Alias, -1); }
|
|
276
|
+
}
|
|
277
|
+
// second pass: set ParentID where the parent is a new element
|
|
278
|
+
if (newElems.length > 0 && !o.dryRun) {
|
|
279
|
+
for (const e of graph.elements || []) {
|
|
280
|
+
if (!e || e.id === undefined || e.id === null) { continue; }
|
|
281
|
+
const alias = String(e.id);
|
|
282
|
+
const pid = aliasToId.get(alias);
|
|
283
|
+
if (pid === undefined) { continue; }
|
|
284
|
+
const parentNode = e.parent !== undefined && e.parent !== null && elemById.get(String(e.parent)) ? elemById.get(String(e.parent)) : null;
|
|
285
|
+
if (!parentNode) { continue; }
|
|
286
|
+
const parentRowId = (function () {
|
|
287
|
+
const a = String(e.parent);
|
|
288
|
+
const ex = elemByAlias.get(a) || elemByGuid.get(deterministicGuid('el:' + a));
|
|
289
|
+
if (ex) { return Number(ex.Object_ID); }
|
|
290
|
+
return aliasToId.get(a);
|
|
291
|
+
})();
|
|
292
|
+
if (parentRowId && Number(parentRowId) !== pid) {
|
|
293
|
+
db.prepare('UPDATE t_object SET ParentID=? WHERE Object_ID=?').run(Number(parentRowId), pid);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
stages.elements = nowMs();
|
|
298
|
+
|
|
299
|
+
// element anchors + meta (idempotent, fingerprint-skipped)
|
|
300
|
+
let tagStats = { propsNew: 0, propsSkip: 0 };
|
|
301
|
+
if (!o.dryRun) {
|
|
302
|
+
// minimal EA-facing anchors for compatibility with the legacy object-model exporter
|
|
303
|
+
const anchorRows = [];
|
|
304
|
+
const elemIdByAliasFinal = new Map();
|
|
305
|
+
for (const ex of existingElems) { elemIdByAliasFinal.set(String(ex.Alias), Number(ex.Object_ID)); }
|
|
306
|
+
for (const [a, id] of aliasToId) { elemIdByAliasFinal.set(a, id); }
|
|
307
|
+
const toTag = [];
|
|
308
|
+
for (const e of graph.elements || []) {
|
|
309
|
+
if (!e || e.id === undefined || e.id === null) { continue; }
|
|
310
|
+
const id = elemIdByAliasFinal.get(String(e.id));
|
|
311
|
+
if (id === undefined) { continue; }
|
|
312
|
+
toTag.push({ id: Number(id), e });
|
|
313
|
+
}
|
|
314
|
+
const metaNew = [];
|
|
315
|
+
const propsToWrite = [];
|
|
316
|
+
for (const t of toTag) {
|
|
317
|
+
const sh = upsertMeta(db, 'element', t.e.id, t.e);
|
|
318
|
+
metaNew.push(1);
|
|
319
|
+
propsToWrite.push([t.id, 'schema_id', t.e.id]);
|
|
320
|
+
propsToWrite.push([t.id, 'archimate_type', canonicalArchimateType(t.e.type)]);
|
|
321
|
+
}
|
|
322
|
+
if (propsToWrite.length > 0) {
|
|
323
|
+
const existingProps = new Set();
|
|
324
|
+
for (let i = 0; i < propsToWrite.length; i += 200) {
|
|
325
|
+
const part = propsToWrite.slice(i, i + 200);
|
|
326
|
+
const ids = new Set(part.map((p) => p[0]));
|
|
327
|
+
const marks = Array.from(ids).map(() => '?').join(',');
|
|
328
|
+
const rows = db.prepare('SELECT Object_ID, Property FROM t_objectproperties WHERE Object_ID IN (' + marks + ')').all(...Array.from(ids));
|
|
329
|
+
for (const r of rows) { existingProps.add(Number(r.Object_ID) + '|' + r.Property); }
|
|
330
|
+
}
|
|
331
|
+
const newProps = propsToWrite.filter((p) => !existingProps.has(p[0] + '|' + p[1]));
|
|
332
|
+
if (newProps.length > 0) {
|
|
333
|
+
insertMany(db, 't_objectproperties', ['Object_ID', 'Property', 'Value', 'Notes'], newProps.map((p) => ({ Object_ID: p[0], Property: p[1], Value: p[2], Notes: '' })));
|
|
334
|
+
tagStats.propsNew += newProps.length;
|
|
335
|
+
}
|
|
336
|
+
tagStats.propsSkip = propsToWrite.length - newProps.length;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
stages.elemTags = nowMs();
|
|
340
|
+
|
|
341
|
+
// --- relationships -----------------------------------------------------
|
|
342
|
+
const elemIdByAliasAll = new Map();
|
|
343
|
+
for (const ex of existingElems) { if (ex.Alias) { elemIdByAliasAll.set(String(ex.Alias), Number(ex.Object_ID)); } }
|
|
344
|
+
if (!o.dryRun) { for (const [a, id] of aliasToId) { elemIdByAliasAll.set(a, id); } }
|
|
345
|
+
const relByGuid = new Map();
|
|
346
|
+
const existingRels = db.prepare('SELECT Connector_ID, ea_guid, Name, Connector_Type, Stereotype, Notes, Direction, Start_Object_ID, End_Object_ID FROM t_connector').all();
|
|
347
|
+
for (const r of existingRels) { if (r.ea_guid) { relByGuid.set(String(r.ea_guid), r); } }
|
|
348
|
+
const newRels = [];
|
|
349
|
+
for (const rel of graph.relationships || []) {
|
|
350
|
+
if (!rel || rel.id === undefined || rel.id === null) { continue; }
|
|
351
|
+
const alias = String(rel.id);
|
|
352
|
+
const guid = deterministicGuid('rel:' + alias);
|
|
353
|
+
const start = elemIdByAliasAll.get(String(rel.source_id));
|
|
354
|
+
const end = elemIdByAliasAll.get(String(rel.target_id));
|
|
355
|
+
if (start === undefined || end === undefined) { continue; }
|
|
356
|
+
const map = relationshipMap(rel.type);
|
|
357
|
+
const direction = map.directed ? 'Source -> Destination' : '';
|
|
358
|
+
const existing = relByGuid.get(guid);
|
|
359
|
+
const intended = {
|
|
360
|
+
Name: safeName(rel.name, alias),
|
|
361
|
+
Connector_Type: map.connectorType,
|
|
362
|
+
Stereotype: relStereotype(rel.type),
|
|
363
|
+
Notes: String(rel.description === null || rel.description === undefined ? '' : rel.description),
|
|
364
|
+
Direction: direction,
|
|
365
|
+
Start_Object_ID: Number(start),
|
|
366
|
+
End_Object_ID: Number(end),
|
|
367
|
+
};
|
|
368
|
+
if (existing) {
|
|
369
|
+
const changed = intended.Name !== (existing.Name || '') || (intended.Connector_Type || '') !== (existing.Connector_Type || '') ||
|
|
370
|
+
(intended.Stereotype || '') !== (existing.Stereotype || '') || intended.Notes !== (existing.Notes || '') ||
|
|
371
|
+
intended.Direction !== (existing.Direction || '') || Number(intended.Start_Object_ID) !== Number(existing.Start_Object_ID || 0) ||
|
|
372
|
+
Number(intended.End_Object_ID) !== Number(existing.End_Object_ID || 0);
|
|
373
|
+
if (changed && !o.dryRun) {
|
|
374
|
+
updateRow(db, 't_connector', ['Name', 'Connector_Type', 'Stereotype', 'Notes', 'Direction', 'Start_Object_ID', 'End_Object_ID'], 'Connector_ID', Number(existing.Connector_ID), intended);
|
|
375
|
+
}
|
|
376
|
+
stats[changed ? 'updated' : 'skipped'].relationships++;
|
|
377
|
+
} else {
|
|
378
|
+
newRels.push({ ...intended, ea_guid: guid, _alias: alias });
|
|
379
|
+
stats.added.relationships++;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const relAliasToId = new Map();
|
|
383
|
+
if (!o.dryRun) {
|
|
384
|
+
if (newRels.length > 0) {
|
|
385
|
+
insertMany(db, 't_connector', ['Name', 'Connector_Type', 'Stereotype', 'Notes', 'Direction', 'Start_Object_ID', 'End_Object_ID', 'ea_guid'], newRels);
|
|
386
|
+
}
|
|
387
|
+
for (let i = 0; i < newRels.length; i += 200) {
|
|
388
|
+
const part = newRels.slice(i, i + 200);
|
|
389
|
+
const marks = part.map(() => '?').join(',');
|
|
390
|
+
const rows = db.prepare('SELECT Connector_ID, ea_guid FROM t_connector WHERE ea_guid IN (' + marks + ')').all(...part.map((r) => r.ea_guid));
|
|
391
|
+
for (const r of rows) { const pr = part.find((x) => x.ea_guid === r.ea_guid); if (pr) { relAliasToId.set(pr._alias, Number(r.Connector_ID)); } }
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
for (const r of newRels) { relAliasToId.set(r._alias, -1); }
|
|
395
|
+
}
|
|
396
|
+
stages.relationships = nowMs();
|
|
397
|
+
|
|
398
|
+
// connector anchors + meta
|
|
399
|
+
if (!o.dryRun) {
|
|
400
|
+
for (const rel of graph.relationships || []) {
|
|
401
|
+
if (!rel || rel.id === undefined || rel.id === null) { continue; }
|
|
402
|
+
const id = relAliasToId.get(String(rel.id));
|
|
403
|
+
if (id === undefined || id < 0) { continue; }
|
|
404
|
+
upsertMeta(db, 'relationship', rel.id, rel);
|
|
405
|
+
const existingCt = db.prepare('SELECT PropertyID FROM t_connectortag WHERE ElementID=? AND Property=?').get(Number(id), 'schema_id');
|
|
406
|
+
if (!existingCt) {
|
|
407
|
+
db.prepare('INSERT INTO t_connectortag (ElementID, Property, VALUE, NOTES) VALUES (?,?,?,?)')
|
|
408
|
+
.run(Number(id), 'schema_id', rel.id, '');
|
|
409
|
+
db.prepare('INSERT INTO t_connectortag (ElementID, Property, VALUE, NOTES) VALUES (?,?,?,?)')
|
|
410
|
+
.run(Number(id), 'archimate_relationship_type', canonicalArchimateType(rel.type), '');
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
stages.relTags = nowMs();
|
|
415
|
+
|
|
416
|
+
// --- views/diagrams ----------------------------------------------------
|
|
417
|
+
const existingDiags = db.prepare('SELECT Diagram_ID, Package_ID, Name, StyleEx, ea_guid FROM t_diagram WHERE Package_ID=?').all(syncId);
|
|
418
|
+
const diagByView = new Map();
|
|
419
|
+
for (const d of existingDiags) {
|
|
420
|
+
const v = parseStyleToken(d.StyleEx, 'schema_view_id');
|
|
421
|
+
if (v) { diagByView.set(v, d); }
|
|
422
|
+
}
|
|
423
|
+
const newDiags = [];
|
|
424
|
+
for (const view of graph.views || []) {
|
|
425
|
+
if (!view || view.view_id === undefined || view.view_id === null) { continue; }
|
|
426
|
+
const viewId = String(view.view_id);
|
|
427
|
+
const styleEx = 'schema_view_id=' + viewId + ';';
|
|
428
|
+
const parentObjectId = (function () {
|
|
429
|
+
if (view.parent_element_id !== undefined && view.parent_element_id !== null && view.parent_element_id !== '') {
|
|
430
|
+
const pid = elemIdByAliasAll.get(String(view.parent_element_id));
|
|
431
|
+
if (pid !== undefined) { return pid; }
|
|
432
|
+
}
|
|
433
|
+
return 0;
|
|
434
|
+
})();
|
|
435
|
+
const existing = diagByView.get(viewId);
|
|
436
|
+
const intended = {
|
|
437
|
+
Name: safeName(view.view_name, viewId),
|
|
438
|
+
Diagram_Type: DIAGRAM_TYPE,
|
|
439
|
+
Package_ID: syncId,
|
|
440
|
+
ParentID: parentObjectId,
|
|
441
|
+
Notes: '', // EA .qea 不保留多段 Notes;视图内容经 kg_sync_meta 保真
|
|
442
|
+
StyleEx: styleEx,
|
|
443
|
+
};
|
|
444
|
+
if (existing) {
|
|
445
|
+
const changed = intended.Name !== (existing.Name || '');
|
|
446
|
+
if (DEBUG && changed) { console.error('DEBUG diagram chg', viewId, JSON.stringify({n:[intended.Name,(existing.Name||'')], notes:[intended.Notes,(existing.Notes||'')]})); }
|
|
447
|
+
if (changed && !o.dryRun) {
|
|
448
|
+
updateRow(db, 't_diagram', ['Name'], 'Diagram_ID', Number(existing.Diagram_ID), intended);
|
|
449
|
+
}
|
|
450
|
+
stats[changed ? 'updated' : 'skipped'].diagrams++;
|
|
451
|
+
} else {
|
|
452
|
+
newDiags.push({ ...intended, ea_guid: deterministicGuid('diag:' + viewId) });
|
|
453
|
+
stats.added.diagrams++;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const diagAliasToId = new Map();
|
|
457
|
+
if (!o.dryRun) {
|
|
458
|
+
if (newDiags.length > 0) {
|
|
459
|
+
insertMany(db, 't_diagram', ['Name', 'Diagram_Type', 'Package_ID', 'ParentID', 'StyleEx', 'ea_guid'], newDiags);
|
|
460
|
+
}
|
|
461
|
+
for (let i = 0; i < newDiags.length; i += 200) {
|
|
462
|
+
const part = newDiags.slice(i, i + 200);
|
|
463
|
+
const marks = part.map(() => '?').join(',');
|
|
464
|
+
const rows = db.prepare('SELECT Diagram_ID, StyleEx FROM t_diagram WHERE StyleEx IN (' + marks + ')').all(...part.map((d) => d.StyleEx));
|
|
465
|
+
for (const r of rows) {
|
|
466
|
+
const v = parseStyleToken(r.StyleEx, 'schema_view_id');
|
|
467
|
+
if (v) { diagAliasToId.set(v, Number(r.Diagram_ID)); }
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} else {
|
|
471
|
+
for (const d of newDiags) { diagAliasToId.set(parseStyleToken(d.StyleEx, 'schema_view_id'), -1); }
|
|
472
|
+
}
|
|
473
|
+
// view meta
|
|
474
|
+
if (!o.dryRun) {
|
|
475
|
+
for (const view of graph.views || []) {
|
|
476
|
+
if (view && view.view_id !== undefined && view.view_id !== null) { upsertMeta(db, 'view', view.view_id, view); }
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// memberships: only INSERT missing; never touch existing geometry
|
|
480
|
+
for (const view of graph.views || []) {
|
|
481
|
+
if (!view || view.view_id === undefined || view.view_id === null) { continue; }
|
|
482
|
+
const viewId = String(view.view_id);
|
|
483
|
+
let dId = diagByView.get(viewId);
|
|
484
|
+
if (!dId) { dId = diagAliasToId.get(viewId); }
|
|
485
|
+
if (!dId) { continue; }
|
|
486
|
+
const diagramId = Number(dId.Diagram_ID !== undefined ? dId.Diagram_ID : dId);
|
|
487
|
+
const placedObjs = new Set();
|
|
488
|
+
const objs = db.prepare('SELECT Object_ID FROM t_diagramobjects WHERE Diagram_ID=?').all(diagramId);
|
|
489
|
+
for (const r of objs) { placedObjs.add(Number(r.Object_ID)); }
|
|
490
|
+
const nextSeq = objs.length;
|
|
491
|
+
const newObjs = [];
|
|
492
|
+
const incl = view.included_elements || [];
|
|
493
|
+
let seq = nextSeq;
|
|
494
|
+
for (const elId of incl) {
|
|
495
|
+
const oid = elemIdByAliasAll.get(String(elId));
|
|
496
|
+
if (oid === undefined) { continue; }
|
|
497
|
+
if (placedObjs.has(Number(oid))) { continue; }
|
|
498
|
+
const col = seq % 6;
|
|
499
|
+
const row = Math.floor(seq / 6);
|
|
500
|
+
newObjs.push({
|
|
501
|
+
Diagram_ID: diagramId, Object_ID: Number(oid),
|
|
502
|
+
RectLeft: 40 + col * 260, RectTop: 40 + row * 160,
|
|
503
|
+
RectRight: 40 + col * 260 + 180, RectBottom: 40 + row * 160 + 90,
|
|
504
|
+
Sequence: seq,
|
|
505
|
+
});
|
|
506
|
+
seq++;
|
|
507
|
+
}
|
|
508
|
+
if (newObjs.length > 0 && !o.dryRun) {
|
|
509
|
+
insertMany(db, 't_diagramobjects', ['Diagram_ID', 'Object_ID', 'RectLeft', 'RectTop', 'RectRight', 'RectBottom', 'Sequence'], newObjs);
|
|
510
|
+
}
|
|
511
|
+
stats.added.diagramObjects += newObjs.length;
|
|
512
|
+
|
|
513
|
+
const placedLinks = new Set();
|
|
514
|
+
const links = db.prepare('SELECT ConnectorID FROM t_diagramlinks WHERE DiagramID=?').all(diagramId);
|
|
515
|
+
for (const r of links) { placedLinks.add(Number(r.ConnectorID)); }
|
|
516
|
+
const newLinks = [];
|
|
517
|
+
for (const relId of view.included_relationships || []) {
|
|
518
|
+
const cid = relAliasToId.get(String(relId));
|
|
519
|
+
if (cid === undefined || cid < 0) { continue; }
|
|
520
|
+
if (placedLinks.has(Number(cid))) { continue; }
|
|
521
|
+
newLinks.push({ DiagramID: diagramId, ConnectorID: Number(cid), Style: '', Geometry: '' });
|
|
522
|
+
}
|
|
523
|
+
if (newLinks.length > 0 && !o.dryRun) {
|
|
524
|
+
insertMany(db, 't_diagramlinks', ['DiagramID', 'ConnectorID', 'Style', 'Geometry'], newLinks);
|
|
525
|
+
}
|
|
526
|
+
stats.added.diagramLinks += newLinks.length;
|
|
527
|
+
}
|
|
528
|
+
stages.members = nowMs();
|
|
529
|
+
|
|
530
|
+
// --- deletion reconcile (opt-in) ---------------------------------------
|
|
531
|
+
const keepAliases = new Set();
|
|
532
|
+
for (const e of graph.elements || []) { if (e && e.id !== undefined) { keepAliases.add(String(e.id)); } }
|
|
533
|
+
for (const rel of graph.relationships || []) { if (rel && rel.id !== undefined) { keepAliases.add(String(rel.id)); } }
|
|
534
|
+
const candidates = [];
|
|
535
|
+
for (const row of existingElems) {
|
|
536
|
+
if (row.Alias && !keepAliases.has(String(row.Alias))) { candidates.push({ type: 'element', id: Number(row.Object_ID), alias: row.Alias }); }
|
|
537
|
+
}
|
|
538
|
+
for (const row of existingRels) {
|
|
539
|
+
if (row.Alias && !keepAliases.has(String(row.Alias))) { candidates.push({ type: 'relationship', id: Number(row.Connector_ID), alias: row.Alias }); }
|
|
540
|
+
}
|
|
541
|
+
stats.deleteCandidates = candidates.length;
|
|
542
|
+
if (candidates.length > 0 && o.allowDelete && !o.dryRun) {
|
|
543
|
+
for (const c of candidates) {
|
|
544
|
+
if (c.type === 'relationship') {
|
|
545
|
+
db.prepare('DELETE FROM t_connector WHERE Connector_ID=?').run(c.id);
|
|
546
|
+
} else {
|
|
547
|
+
db.prepare('DELETE FROM t_diagramobjects WHERE Object_ID=?').run(c.id);
|
|
548
|
+
db.prepare('DELETE FROM t_objectproperties WHERE Object_ID=?').run(c.id);
|
|
549
|
+
db.prepare('DELETE FROM t_object WHERE Object_ID=?').run(c.id);
|
|
550
|
+
}
|
|
551
|
+
stats.deleted++;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
stages.deletes = nowMs();
|
|
555
|
+
if (!o.dryRun) { db.exec('COMMIT'); }
|
|
556
|
+
} finally {
|
|
557
|
+
try { db.close(); } catch { /* ignore */ }
|
|
558
|
+
}
|
|
559
|
+
stats.ms = {
|
|
560
|
+
total: nowMs() - t0,
|
|
561
|
+
package: stages.package - t0,
|
|
562
|
+
elements: stages.elements - stages.package,
|
|
563
|
+
elemTags: stages.elemTags - stages.elements,
|
|
564
|
+
relationships: stages.relationships - stages.elemTags,
|
|
565
|
+
relTags: stages.relTags - stages.relationships,
|
|
566
|
+
views: stages.members - stages.relTags,
|
|
567
|
+
members: stages.members - stages.relTags,
|
|
568
|
+
};
|
|
569
|
+
return { ok: true, syncPackageId: stats.syncPackageId, stats };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ---------------------------------------------------------------------------
|
|
573
|
+
// ---------------------------------------------------------------------------
|
|
574
|
+
// Full projection (whole-file rebuild, decision qea-full-wholefile-argo-scripts-no-config):
|
|
575
|
+
// wipe ALL existing content in the target .qea (every user table row, incl. kg_sync_meta),
|
|
576
|
+
// re-seed the minimal root package, then rebuild the whole .qea purely from the canonical
|
|
577
|
+
// graph. EA is treated as a projection: after full there is nothing but canonical content.
|
|
578
|
+
// ---------------------------------------------------------------------------
|
|
579
|
+
function wipeAllContent(db) {
|
|
580
|
+
const rows = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all();
|
|
581
|
+
const cleared = {};
|
|
582
|
+
for (const r of rows) {
|
|
583
|
+
try {
|
|
584
|
+
const before = db.prepare('SELECT COUNT(*) AS c FROM "' + r.name + '"').get().c;
|
|
585
|
+
db.exec('DELETE FROM "' + r.name + '"');
|
|
586
|
+
cleared[r.name] = before;
|
|
587
|
+
} catch { /* skip locked/system tables */ }
|
|
588
|
+
}
|
|
589
|
+
try { db.exec('DELETE FROM sqlite_sequence'); } catch { /* ignore */ }
|
|
590
|
+
return cleared;
|
|
591
|
+
}
|
|
592
|
+
function ensureRootPackage(db) {
|
|
593
|
+
const roots = db.prepare('SELECT Package_ID FROM t_package WHERE Parent_ID=0 ORDER BY Package_ID').all();
|
|
594
|
+
if (roots.length > 0) { return Number(roots[0].Package_ID); }
|
|
595
|
+
const r = db.prepare('INSERT INTO t_package (Name, Parent_ID, ea_guid) VALUES (?,?,?)').run('Model', 0, deterministicGuid('pkg:Model'));
|
|
596
|
+
return Number(r.lastInsertRowid);
|
|
597
|
+
}
|
|
598
|
+
function verifyQeaCanonical(graph, qeaPath) {
|
|
599
|
+
let roundtrip = null;
|
|
600
|
+
try { roundtrip = require('../../tests/_ea-roundtrip-lib.js'); } catch { roundtrip = null; }
|
|
601
|
+
const exp = exportQeaToGraph(qeaPath);
|
|
602
|
+
const counts = {
|
|
603
|
+
elements: exp.elements.length, relationships: exp.relationships.length, views: exp.views.length,
|
|
604
|
+
sourceElements: (graph.elements || []).length,
|
|
605
|
+
sourceRelationships: (graph.relationships || []).length,
|
|
606
|
+
sourceViews: (graph.views || []).length,
|
|
607
|
+
};
|
|
608
|
+
let equal = counts.elements === counts.sourceElements && counts.relationships === counts.sourceRelationships && counts.views === counts.sourceViews;
|
|
609
|
+
let diffs = null;
|
|
610
|
+
if (roundtrip && typeof roundtrip.compareRoundtrip === 'function') {
|
|
611
|
+
const rep = roundtrip.compareRoundtrip(graph, exp, {});
|
|
612
|
+
equal = rep.equal;
|
|
613
|
+
diffs = { missing: rep.missingInExport.length, extra: rep.extraInExport.length, valueDiffs: rep.valueDiffs.length };
|
|
614
|
+
}
|
|
615
|
+
return { consistent: equal, diffs, counts };
|
|
616
|
+
}
|
|
617
|
+
function fullProjection(graph, qeaPath, opts) {
|
|
618
|
+
const o = opts || {};
|
|
619
|
+
const t0 = nowMs();
|
|
620
|
+
let wiped = null;
|
|
621
|
+
let db = null;
|
|
622
|
+
try {
|
|
623
|
+
db = openQea(qeaPath);
|
|
624
|
+
ensureMetaTable(db);
|
|
625
|
+
db.exec('BEGIN IMMEDIATE');
|
|
626
|
+
wiped = wipeAllContent(db);
|
|
627
|
+
ensureRootPackage(db);
|
|
628
|
+
db.exec('COMMIT');
|
|
629
|
+
} finally {
|
|
630
|
+
if (db) { try { db.close(); } catch { /* ignore */ } }
|
|
631
|
+
}
|
|
632
|
+
const sync = syncGraphToQea(graph, qeaPath, { dryRun: o.dryRun, allowDelete: false });
|
|
633
|
+
const verification = o.verify === false ? null : verifyQeaCanonical(graph, qeaPath);
|
|
634
|
+
return { ok: true, wiped, sync: sync.stats, verification, ms: { total: nowMs() - t0 } };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ---------------------------------------------------------------------------
|
|
638
|
+
// Export
|
|
639
|
+
// ---------------------------------------------------------------------------
|
|
640
|
+
// ---------------------------------------------------------------------------
|
|
641
|
+
// Export
|
|
642
|
+
// ---------------------------------------------------------------------------
|
|
643
|
+
function exportQeaToGraph(qeaPath) {
|
|
644
|
+
const db = openQea(qeaPath);
|
|
645
|
+
try {
|
|
646
|
+
ensureMetaTable(db);
|
|
647
|
+
const elements = readMetaKind(db, 'element').filter((e) => e && e.id !== undefined);
|
|
648
|
+
const relationships = readMetaKind(db, 'relationship').filter((r) => r && r.id !== undefined);
|
|
649
|
+
const views = readMetaKind(db, 'view').filter((v) => v && v.view_id !== undefined);
|
|
650
|
+
return { name: 'ArchGraph (from archgraph.qea)', description: '', elements, relationships, views };
|
|
651
|
+
} finally {
|
|
652
|
+
db.close();
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// ---------------------------------------------------------------------------
|
|
657
|
+
// snapshot
|
|
658
|
+
// ---------------------------------------------------------------------------
|
|
659
|
+
function snapshotQea(qeaPath, snapshotDir) {
|
|
660
|
+
const fs = require('node:fs');
|
|
661
|
+
const path = require('node:path');
|
|
662
|
+
if (!fs.existsSync(qeaPath)) { return null; }
|
|
663
|
+
const dir = snapshotDir || path.dirname(qeaPath);
|
|
664
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
665
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
666
|
+
const target = path.join(dir, path.basename(qeaPath).replace(/(\.qea)$/i, '_before_sync_' + ts + '$1'));
|
|
667
|
+
fs.copyFileSync(qeaPath, target);
|
|
668
|
+
return target;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
module.exports = {
|
|
672
|
+
SYNC_PACKAGE_NAME,
|
|
673
|
+
META_TABLE,
|
|
674
|
+
openQea,
|
|
675
|
+
deterministicGuid,
|
|
676
|
+
elementObjectType,
|
|
677
|
+
elementStereotype,
|
|
678
|
+
relationshipMap,
|
|
679
|
+
canonicalArchimateType,
|
|
680
|
+
syncGraphToQea,
|
|
681
|
+
exportQeaToGraph,
|
|
682
|
+
snapshotQea,
|
|
683
|
+
readMetaKind,
|
|
684
|
+
fullProjection,
|
|
685
|
+
wipeAllContent,
|
|
686
|
+
ensureRootPackage,
|
|
687
|
+
verifyQeaCanonical,
|
|
688
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// WP2791 Node direct .qea projection CLI (no EA, no third-party deps).
|
|
5
|
+
// node argo/scripts/ea-qea-sync.js --graph <json> --qea <file.qea> --mode import|sync|full|export|watch
|
|
6
|
+
// [--delete-confirm-file <f> | -y] [--dry-run] [--snapshot-dir <dir>] [--out <file>] [--no-backup]
|
|
7
|
+
//
|
|
8
|
+
// import/sync : project design/KG graph into the .qea (update-in-place, batch INSERT,
|
|
9
|
+
// full : clear projection-owned content (kg_sync_meta + ArchGraph Sync subtree) then
|
|
10
|
+
// unchanged fingerprint skip, opt-in EA-only delete with confirmation).
|
|
11
|
+
// export : read .qea back into graph-shaped JSON (roundtrip comparable).
|
|
12
|
+
// watch : on graph JSON change, run sync (fs.watch + polling fallback).
|
|
13
|
+
// Every write first snapshots the target as <file>_before_sync_<ts> unless --no-backup.
|
|
14
|
+
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
const fs = require('node:fs');
|
|
17
|
+
const lib = require(path.join(__dirname, 'ea-qea-sync-lib.js'));
|
|
18
|
+
|
|
19
|
+
function parseArgs(argv) {
|
|
20
|
+
const args = { mode: 'sync', graph: '', qea: '', allowDelete: false, deleteConfirmFile: '', dryRun: false, snapshotDir: '', out: '', noBackup: false, intervalMs: 2000 };
|
|
21
|
+
for (let i = 0; i < argv.length; i++) {
|
|
22
|
+
const a = argv[i];
|
|
23
|
+
const next = () => (i + 1 < argv.length ? argv[++i] : '');
|
|
24
|
+
if (a === '--graph') { args.graph = next(); }
|
|
25
|
+
else if (a === '--qea') { args.qea = next(); }
|
|
26
|
+
else if (a === '--mode') { args.mode = next(); }
|
|
27
|
+
else if (a === '--delete-confirm-file') { args.deleteConfirmFile = next(); args.allowDelete = true; }
|
|
28
|
+
else if (a === '-y' || a === '--yes') { args.allowDelete = true; }
|
|
29
|
+
else if (a === '--dry-run') { args.dryRun = true; }
|
|
30
|
+
else if (a === '--snapshot-dir') { args.snapshotDir = next(); }
|
|
31
|
+
else if (a === '--out') { args.out = next(); }
|
|
32
|
+
else if (a === '--no-backup') { args.noBackup = true; }
|
|
33
|
+
else if (a === '--interval') { args.intervalMs = Number(next()) || 2000; }
|
|
34
|
+
else if (a.startsWith('-')) { /* ignore unknown */ }
|
|
35
|
+
else if (args.modeSet === undefined) { /* positional not used */ }
|
|
36
|
+
}
|
|
37
|
+
return args;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readGraph(jsonPath) {
|
|
41
|
+
const raw = fs.readFileSync(jsonPath, 'utf8').replace(/^\uFEFF/, '');
|
|
42
|
+
return JSON.parse(raw);
|
|
43
|
+
}
|
|
44
|
+
function confirmDelete(args) {
|
|
45
|
+
if (args.allowDelete) { return true; }
|
|
46
|
+
if (args.deleteConfirmFile) {
|
|
47
|
+
try {
|
|
48
|
+
const text = fs.readFileSync(args.deleteConfirmFile, 'utf8').trim().toLowerCase();
|
|
49
|
+
return text.indexOf('delete') >= 0;
|
|
50
|
+
} catch { return false; }
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function main() {
|
|
56
|
+
const args = parseArgs(process.argv.slice(2));
|
|
57
|
+
const cwd = process.cwd();
|
|
58
|
+
const graphPath = path.resolve(cwd, args.graph || 'design/KG/SystemArchitecture.json');
|
|
59
|
+
const qeaPath = path.resolve(cwd, args.qea || 'archgraph.qea');
|
|
60
|
+
if (!fs.existsSync(qeaPath)) {
|
|
61
|
+
console.error('qea not found: ' + qeaPath);
|
|
62
|
+
process.exit(2);
|
|
63
|
+
}
|
|
64
|
+
if (args.mode === 'export') {
|
|
65
|
+
const graph = lib.exportQeaToGraph(qeaPath);
|
|
66
|
+
const text = JSON.stringify(graph, null, 2);
|
|
67
|
+
if (args.out) { fs.writeFileSync(path.resolve(cwd, args.out), text, 'utf8'); console.log('export written to ' + args.out); }
|
|
68
|
+
else { console.log(text); }
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!fs.existsSync(graphPath)) {
|
|
72
|
+
console.error('graph not found: ' + graphPath);
|
|
73
|
+
process.exit(2);
|
|
74
|
+
}
|
|
75
|
+
const graph = readGraph(graphPath);
|
|
76
|
+
if (args.mode === 'watch') {
|
|
77
|
+
// eslint-disable-next-line no-constant-condition
|
|
78
|
+
while (true) {
|
|
79
|
+
runOnce(args, graphPath, qeaPath);
|
|
80
|
+
const m0 = statHash(graphPath);
|
|
81
|
+
await sleep(args.intervalMs);
|
|
82
|
+
const m1 = statHash(graphPath);
|
|
83
|
+
if (m0 !== m1) { continue; } // changed while waiting -> immediate re-sync
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
runOnce(args, graphPath, qeaPath);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function statHash(p) {
|
|
90
|
+
try { const st = fs.statSync(p); return st.size + ':' + st.mtimeMs; } catch { return 'gone'; }
|
|
91
|
+
}
|
|
92
|
+
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
|
93
|
+
function runOnce(args, graphPath, qeaPath) {
|
|
94
|
+
const graph = readGraph(graphPath);
|
|
95
|
+
let snapshot = null;
|
|
96
|
+
if (!args.dryRun && !args.noBackup) {
|
|
97
|
+
snapshot = lib.snapshotQea(qeaPath, args.snapshotDir || undefined);
|
|
98
|
+
}
|
|
99
|
+
const res = args.mode === 'full'
|
|
100
|
+
? lib.fullProjection(graph, qeaPath, { dryRun: args.dryRun })
|
|
101
|
+
: lib.syncGraphToQea(graph, qeaPath, { dryRun: args.dryRun, allowDelete: confirmDelete(args) });
|
|
102
|
+
const mode = args.dryRun ? 'dry-run' : 'sync';
|
|
103
|
+
console.log(JSON.stringify({ mode, graph: graphPath, qea: qeaPath, snapshot, result: res }, null, 2));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
main().catch((e) => {
|
|
107
|
+
console.error('ea-qea-sync failed: ' + (e && e.message));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('node:fs');
|
|
2
2
|
const path = require('node:path');
|
|
3
|
+
const { spawn } = require('node:child_process');
|
|
3
4
|
const readline = require('node:readline');
|
|
4
5
|
const crypto = require('node:crypto');
|
|
5
6
|
|
|
@@ -1615,6 +1616,25 @@ async function buildMutationResult(context, mutations, write) {
|
|
|
1615
1616
|
writeGraph(context.graphPath.absolutePath, mutationResult.document);
|
|
1616
1617
|
result.written = true;
|
|
1617
1618
|
|
|
1619
|
+
// WP2791: .qea projection parallel to the Neo4j trigger — non-fatal, best-effort.
|
|
1620
|
+
{
|
|
1621
|
+
const qeaTarget = resolveQeaProjectionTarget(context);
|
|
1622
|
+
if (qeaTarget) {
|
|
1623
|
+
try {
|
|
1624
|
+
const projection = await runQeaProjection(qeaTarget);
|
|
1625
|
+
if (projection.ok) {
|
|
1626
|
+
result.qeaProjection = { status: 'passed', qea: qeaTarget.qeaPath, ms: projection.ms };
|
|
1627
|
+
} else {
|
|
1628
|
+
result.qeaProjection = { status: 'failed', qea: qeaTarget.qeaPath, error: projection.error || ('exit ' + projection.code), ms: projection.ms };
|
|
1629
|
+
result.warnings = addUnique(result.warnings || [], ['ea-qea projection failed (non-fatal): ' + (projection.error || ('exit code ' + projection.code))]);
|
|
1630
|
+
}
|
|
1631
|
+
} catch (error) {
|
|
1632
|
+
result.qeaProjection = { status: 'failed', error: String(error && error.message ? error.message : error) };
|
|
1633
|
+
result.warnings = addUnique(result.warnings || [], ['ea-qea projection error (non-fatal): ' + String(error && error.message ? error.message : error)]);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1618
1638
|
if (shouldSyncCanonicalGraphToNeo4j(context.graphPath.relativePath)) {
|
|
1619
1639
|
try {
|
|
1620
1640
|
const syncResult = await syncArchitectureToNeo4j({
|
|
@@ -1812,6 +1832,59 @@ function summarizeDocument(document) {
|
|
|
1812
1832
|
};
|
|
1813
1833
|
}
|
|
1814
1834
|
|
|
1835
|
+
// --- WP2791: post-canonical-write .qea projection (parallel to Neo4j sync, non-fatal) ---
|
|
1836
|
+
// Target resolution (decision qea-full-wholefile-argo-scripts-no-config): env ARGO_EA_QEA >
|
|
1837
|
+
// the single *.qea at the workspace root (0/many -> no-op with an explicit log). NO config file.
|
|
1838
|
+
// Projection script runs from argo/scripts (same package as the MCP runtime), so a workspace
|
|
1839
|
+
// does not need to ship its own projection script (bundled argo/scripts module).
|
|
1840
|
+
function resolveQeaProjectionTarget(context) {
|
|
1841
|
+
try {
|
|
1842
|
+
const workspaceRoot = String(context && context.workspaceRoot ? context.workspaceRoot : '');
|
|
1843
|
+
if (!workspaceRoot || !fs.existsSync(workspaceRoot)) { return null; }
|
|
1844
|
+
const graphAbsolute = context.graphPath && context.graphPath.absolutePath ? context.graphPath.absolutePath : null;
|
|
1845
|
+
if (!graphAbsolute || !fs.existsSync(graphAbsolute)) { return null; }
|
|
1846
|
+
const pick = (p) => (p && fs.existsSync(p) ? path.resolve(p) : null);
|
|
1847
|
+
let qeaPath = pick(process.env.ARGO_EA_QEA);
|
|
1848
|
+
if (qeaPath) { return { qeaPath, graphPath: graphAbsolute, workspaceRoot }; }
|
|
1849
|
+
let qeas = [];
|
|
1850
|
+
try { qeas = fs.readdirSync(workspaceRoot).filter((n) => n.toLowerCase().endsWith('.qea')); } catch { /* ignore */ }
|
|
1851
|
+
if (qeas.length === 1) {
|
|
1852
|
+
return { qeaPath: path.resolve(workspaceRoot, qeas[0]), graphPath: graphAbsolute, workspaceRoot };
|
|
1853
|
+
}
|
|
1854
|
+
console.log('[ea-qea] projection target: none' + (qeas.length > 1 ? ' (' + qeas.length + ' *.qea found; expected exactly one or ARGO_EA_QEA)' : '') + ' in ' + workspaceRoot);
|
|
1855
|
+
return null;
|
|
1856
|
+
} catch (error) {
|
|
1857
|
+
console.log('[ea-qea] projection target resolution failed: ' + String(error && error.message ? error.message : error));
|
|
1858
|
+
return null;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
function runQeaProjection(target) {
|
|
1863
|
+
return new Promise((resolve) => {
|
|
1864
|
+
const script = path.join(__dirname, 'ea-qea-sync.js');
|
|
1865
|
+
if (!fs.existsSync(script)) {
|
|
1866
|
+
resolve({ ok: false, error: 'argo/scripts/ea-qea-sync.js missing', ms: 0 });
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
const snapshotDir = path.join(target.workspaceRoot, '.argo', 'temp', 'qea-backups');
|
|
1870
|
+
const args = [script, '--mode', 'sync', '--graph', target.graphPath, '--qea', target.qeaPath, '--snapshot-dir', snapshotDir];
|
|
1871
|
+
const started = Date.now();
|
|
1872
|
+
let stderr = '';
|
|
1873
|
+
let child;
|
|
1874
|
+
try {
|
|
1875
|
+
child = spawn(process.execPath, args, { cwd: target.workspaceRoot, windowsHide: true });
|
|
1876
|
+
} catch (error) {
|
|
1877
|
+
resolve({ ok: false, error: String(error && error.message ? error.message : error), ms: Date.now() - started });
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
child.stderr.on('data', (d) => { stderr += String(d); });
|
|
1881
|
+
child.on('error', (err) => resolve({ ok: false, error: String(err && err.message ? err.message : err), ms: Date.now() - started, stderr: stderr.slice(0, 600) }));
|
|
1882
|
+
child.on('close', (code) => {
|
|
1883
|
+
resolve({ ok: code === 0, code, ms: Date.now() - started, stderr: stderr.slice(0, 600) });
|
|
1884
|
+
});
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1815
1888
|
function writeGraph(graphPath, document) {
|
|
1816
1889
|
const tempPath = `${graphPath}.${process.pid}.${Date.now()}.tmp`;
|
|
1817
1890
|
fs.writeFileSync(tempPath, `${JSON.stringify(document, null, 2)}\n`, 'utf8');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archgraph-argo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
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": {
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"neo4j-driver": "^6.2.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@resvg/resvg-js": "^2.6.2"
|
|
41
|
+
"@resvg/resvg-js": "^2.6.2",
|
|
42
|
+
"playwright": "1.61.1"
|
|
42
43
|
},
|
|
43
44
|
"scripts": {
|
|
44
45
|
"test": "node --test \"tests/*.test.js\""
|