archgraph-argo 0.12.5 → 0.13.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.
|
@@ -113,6 +113,84 @@ function sha1(text) {
|
|
|
113
113
|
return crypto.createHash('sha1').update(String(text)).digest('hex');
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Element child mirror (attributes -> t_attribute, testcases -> t_objecttests)
|
|
118
|
+
// Mirrors the legacy object-model import (import-from-kg.js applyElementAttributes /
|
|
119
|
+
// applyTestcases) so canonical attributes and acceptance tests are visible in EA
|
|
120
|
+
// under the element's Attributes / Testing tabs. Canonical-owned fields only:
|
|
121
|
+
// run status/results on tests are left untouched on update (EA-side state).
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
const MAX_ATTRIBUTE_DEFAULT_LENGTH = 250;
|
|
124
|
+
const TEST_CLASS_ACCEPTANCE = 4; // mirrors import-from-kg mapTestTypeToEaClass('Acceptance Test')
|
|
125
|
+
|
|
126
|
+
function attrRowGuid(elementAlias, name, occurrence) {
|
|
127
|
+
return deterministicGuid('attr:' + elementAlias + ':' + name + '#' + occurrence);
|
|
128
|
+
}
|
|
129
|
+
function attributeDefaultValue(attr) {
|
|
130
|
+
const value = attr.value === undefined || attr.value === null ? '' : String(attr.value);
|
|
131
|
+
return value.length > MAX_ATTRIBUTE_DEFAULT_LENGTH ? '' : value;
|
|
132
|
+
}
|
|
133
|
+
function attributeNoteText(attr) {
|
|
134
|
+
const parts = [];
|
|
135
|
+
const value = attr.value === undefined || attr.value === null ? '' : String(attr.value);
|
|
136
|
+
if (value.length > MAX_ATTRIBUTE_DEFAULT_LENGTH) { parts.push(value); }
|
|
137
|
+
if (attr.description !== undefined && attr.description !== null && String(attr.description) !== '') { parts.push(String(attr.description)); }
|
|
138
|
+
if (attr.content !== undefined && attr.content !== null && String(attr.content) !== '') { parts.push(String(attr.content)); }
|
|
139
|
+
return parts.join('\r\n\r\n');
|
|
140
|
+
}
|
|
141
|
+
function isPersistedAttribute(attr) {
|
|
142
|
+
return !!(attr && typeof attr === 'object' && typeof attr.name === 'string' && attr.name.trim() !== '' && attr.op !== 'remove');
|
|
143
|
+
}
|
|
144
|
+
function mirrorElementChildren(db, objectId, element, stats) {
|
|
145
|
+
const alias = String(element.id);
|
|
146
|
+
const attrs = (Array.isArray(element.attributes) ? element.attributes : []).filter(isPersistedAttribute);
|
|
147
|
+
if (attrs.length > 0) {
|
|
148
|
+
const existing = db.prepare('SELECT ID, Name, "Default", Notes, ea_guid, Pos FROM t_attribute WHERE Object_ID=?').all(objectId);
|
|
149
|
+
const available = existing.map((r) => ({ ...r }));
|
|
150
|
+
let maxPos = 0;
|
|
151
|
+
for (const r of existing) { const p = Number(r.Pos); if (!Number.isNaN(p) && p > maxPos) { maxPos = p; } }
|
|
152
|
+
const occurrence = {};
|
|
153
|
+
for (const a of attrs) {
|
|
154
|
+
occurrence[a.name] = (occurrence[a.name] || 0) + 1;
|
|
155
|
+
const guid = attrRowGuid(alias, a.name, occurrence[a.name]);
|
|
156
|
+
const notes = attributeNoteText(a);
|
|
157
|
+
const def = attributeDefaultValue(a);
|
|
158
|
+
const idx = available.findIndex((r) => String(r.ea_guid || '') === guid);
|
|
159
|
+
if (idx >= 0) {
|
|
160
|
+
db.prepare('UPDATE t_attribute SET Name=?, Type=?, "Default"=?, Notes=? WHERE ID=?')
|
|
161
|
+
.run(a.name, 'String', def, notes, Number(available[idx].ID));
|
|
162
|
+
available.splice(idx, 1);
|
|
163
|
+
stats.attributesUpdated++;
|
|
164
|
+
} else {
|
|
165
|
+
maxPos += 16;
|
|
166
|
+
db.prepare('INSERT INTO t_attribute (Object_ID, Name, Scope, Type, "Default", Notes, Pos, ea_guid) VALUES (?,?,?,?,?,?,?,?)')
|
|
167
|
+
.run(objectId, a.name, 'Public', 'String', def, notes, maxPos, guid);
|
|
168
|
+
stats.attributesAdded++;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const tests = Array.isArray(element.testcases) ? element.testcases : [];
|
|
173
|
+
for (const tc of tests) {
|
|
174
|
+
if (!tc || typeof tc.name !== 'string' || tc.name.trim() === '') { continue; }
|
|
175
|
+
const name = tc.name.trim();
|
|
176
|
+
const type = (tc.type !== undefined && tc.type !== null && String(tc.type) !== '') ? String(tc.type) : 'Acceptance Test';
|
|
177
|
+
const notes = tc.description === undefined || tc.description === null ? '' : String(tc.description);
|
|
178
|
+
const input = tc.Input === undefined || tc.Input === null ? '' : String(tc.Input);
|
|
179
|
+
const criteria = tc.acceptanceCriteria === undefined || tc.acceptanceCriteria === null ? '' : String(tc.acceptanceCriteria);
|
|
180
|
+
const existing = db.prepare('SELECT Test FROM t_objecttests WHERE Object_ID=? AND Test=? AND TestClass=?')
|
|
181
|
+
.get(objectId, name, TEST_CLASS_ACCEPTANCE);
|
|
182
|
+
if (existing) {
|
|
183
|
+
db.prepare('UPDATE t_objecttests SET TestType=?, Notes=?, InputData=?, AcceptanceCriteria=? WHERE Object_ID=? AND Test=? AND TestClass=?')
|
|
184
|
+
.run(type, notes, input, criteria, objectId, name, TEST_CLASS_ACCEPTANCE);
|
|
185
|
+
stats.testsUpdated++;
|
|
186
|
+
} else {
|
|
187
|
+
db.prepare('INSERT INTO t_objecttests (Object_ID, Test, TestClass, TestType, Notes, InputData, AcceptanceCriteria, Status, Results) VALUES (?,?,?,?,?,?,?,?,?)')
|
|
188
|
+
.run(objectId, name, TEST_CLASS_ACCEPTANCE, type, notes, input, criteria, 'Proposed', '');
|
|
189
|
+
stats.testsAdded++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
116
194
|
// ---------------------------------------------------------------------------
|
|
117
195
|
// sqlite helpers
|
|
118
196
|
// ---------------------------------------------------------------------------
|
|
@@ -186,6 +264,17 @@ function parseStyleToken(styleEx, key) {
|
|
|
186
264
|
const m = re.exec(text);
|
|
187
265
|
return m ? m[2] : '';
|
|
188
266
|
}
|
|
267
|
+
function ensureStyleToken(styleEx, keyValue) {
|
|
268
|
+
// EA rewrites StyleEx with its own tokens and drops unknown ones (e.g. schema_view_id).
|
|
269
|
+
// Re-inject our anchor while preserving EA's formatting tokens so diagram identity
|
|
270
|
+
// stays discoverable on the next sync.
|
|
271
|
+
const text = String(styleEx === null || styleEx === undefined ? '' : styleEx);
|
|
272
|
+
const key = String(keyValue).split('=')[0];
|
|
273
|
+
const existing = parseStyleToken(text, key);
|
|
274
|
+
if (existing !== '') { return text; }
|
|
275
|
+
const token = String(keyValue).indexOf('=') >= 0 ? String(keyValue) + ';' : String(keyValue) + '=;';
|
|
276
|
+
return text ? token + text : token;
|
|
277
|
+
}
|
|
189
278
|
|
|
190
279
|
// ---------------------------------------------------------------------------
|
|
191
280
|
// Core sync
|
|
@@ -335,6 +424,10 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
335
424
|
}
|
|
336
425
|
tagStats.propsSkip = propsToWrite.length - newProps.length;
|
|
337
426
|
}
|
|
427
|
+
// canonical attributes -> t_attribute, canonical testcases -> t_objecttests
|
|
428
|
+
for (const t of toTag) {
|
|
429
|
+
mirrorElementChildren(db, t.id, t.e, tagStats);
|
|
430
|
+
}
|
|
338
431
|
}
|
|
339
432
|
stages.elemTags = nowMs();
|
|
340
433
|
|
|
@@ -414,12 +507,20 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
414
507
|
stages.relTags = nowMs();
|
|
415
508
|
|
|
416
509
|
// --- views/diagrams ----------------------------------------------------
|
|
510
|
+
// Diagram identity is matched by BOTH the schema_view_id StyleEx token AND the
|
|
511
|
+
// deterministic ea_guid. EA rewrites StyleEx (its own formatting tokens) whenever
|
|
512
|
+
// it touches an open project and DROPS unknown tokens like schema_view_id — if we
|
|
513
|
+
// only matched by the token we would re-INSERT the same deterministic ea_guid and
|
|
514
|
+
// crash on t_diagram's UNIQUE(ea_guid) (projection failure: Neo4j ok, EA stale).
|
|
417
515
|
const existingDiags = db.prepare('SELECT Diagram_ID, Package_ID, Name, StyleEx, ea_guid FROM t_diagram WHERE Package_ID=?').all(syncId);
|
|
418
516
|
const diagByView = new Map();
|
|
517
|
+
const diagByGuid = new Map();
|
|
419
518
|
for (const d of existingDiags) {
|
|
519
|
+
if (d.ea_guid) { diagByGuid.set(String(d.ea_guid), d); }
|
|
420
520
|
const v = parseStyleToken(d.StyleEx, 'schema_view_id');
|
|
421
521
|
if (v) { diagByView.set(v, d); }
|
|
422
522
|
}
|
|
523
|
+
const diagViewRows = new Map(); // view_id -> matched existing row (token OR guid)
|
|
423
524
|
const newDiags = [];
|
|
424
525
|
for (const view of graph.views || []) {
|
|
425
526
|
if (!view || view.view_id === undefined || view.view_id === null) { continue; }
|
|
@@ -432,7 +533,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
432
533
|
}
|
|
433
534
|
return 0;
|
|
434
535
|
})();
|
|
435
|
-
const existing = diagByView.get(viewId);
|
|
536
|
+
const existing = diagByView.get(viewId) || diagByGuid.get(deterministicGuid('diag:' + viewId)) || null;
|
|
436
537
|
const intended = {
|
|
437
538
|
Name: safeName(view.view_name, viewId),
|
|
438
539
|
Diagram_Type: DIAGRAM_TYPE,
|
|
@@ -442,10 +543,15 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
442
543
|
StyleEx: styleEx,
|
|
443
544
|
};
|
|
444
545
|
if (existing) {
|
|
445
|
-
|
|
446
|
-
|
|
546
|
+
diagViewRows.set(viewId, existing);
|
|
547
|
+
// EA may have rewritten StyleEx and dropped the anchor — re-inject it while
|
|
548
|
+
// preserving EA's own formatting tokens so identity stays discoverable.
|
|
549
|
+
const anchoredStyleEx = ensureStyleToken(existing.StyleEx, 'schema_view_id=' + viewId);
|
|
550
|
+
const changed = intended.Name !== (existing.Name || '') || (existing.StyleEx || '') !== anchoredStyleEx;
|
|
551
|
+
if (DEBUG && changed) { console.error('DEBUG diagram chg', viewId, JSON.stringify({n:[intended.Name,(existing.Name||'')], style: !!parseStyleToken(existing.StyleEx,'schema_view_id')})); }
|
|
447
552
|
if (changed && !o.dryRun) {
|
|
448
|
-
|
|
553
|
+
db.prepare('UPDATE t_diagram SET Name=?, StyleEx=? WHERE Diagram_ID=?')
|
|
554
|
+
.run(intended.Name, anchoredStyleEx, Number(existing.Diagram_ID));
|
|
449
555
|
}
|
|
450
556
|
stats[changed ? 'updated' : 'skipped'].diagrams++;
|
|
451
557
|
} else {
|
|
@@ -470,6 +576,12 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
470
576
|
} else {
|
|
471
577
|
for (const d of newDiags) { diagAliasToId.set(parseStyleToken(d.StyleEx, 'schema_view_id'), -1); }
|
|
472
578
|
}
|
|
579
|
+
const diagIdForView = (viewId) => {
|
|
580
|
+
const row = diagViewRows.get(viewId) || diagByView.get(viewId);
|
|
581
|
+
if (row) { return Number(row.Diagram_ID !== undefined ? row.Diagram_ID : row); }
|
|
582
|
+
const planned = diagAliasToId.get(viewId);
|
|
583
|
+
return planned === undefined ? null : planned;
|
|
584
|
+
};
|
|
473
585
|
// view meta
|
|
474
586
|
if (!o.dryRun) {
|
|
475
587
|
for (const view of graph.views || []) {
|
|
@@ -480,10 +592,8 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
480
592
|
for (const view of graph.views || []) {
|
|
481
593
|
if (!view || view.view_id === undefined || view.view_id === null) { continue; }
|
|
482
594
|
const viewId = String(view.view_id);
|
|
483
|
-
|
|
484
|
-
if (
|
|
485
|
-
if (!dId) { continue; }
|
|
486
|
-
const diagramId = Number(dId.Diagram_ID !== undefined ? dId.Diagram_ID : dId);
|
|
595
|
+
const diagramId = diagIdForView(viewId);
|
|
596
|
+
if (diagramId === null) { continue; }
|
|
487
597
|
const placedObjs = new Set();
|
|
488
598
|
const objs = db.prepare('SELECT Object_ID FROM t_diagramobjects WHERE Diagram_ID=?').all(diagramId);
|
|
489
599
|
for (const r of objs) { placedObjs.add(Number(r.Object_ID)); }
|
|
@@ -637,9 +747,46 @@ function fullProjection(graph, qeaPath, opts) {
|
|
|
637
747
|
// ---------------------------------------------------------------------------
|
|
638
748
|
// Export
|
|
639
749
|
// ---------------------------------------------------------------------------
|
|
640
|
-
//
|
|
641
|
-
//
|
|
642
|
-
//
|
|
750
|
+
// Read the EA diagram GEOMETRY for one KG view from a .qea model (read-only).
|
|
751
|
+
// The view maps to the diagram anchored by the deterministic ea_guid (diag:<viewId>)
|
|
752
|
+
// or the schema_view_id StyleEx token written by the sync. Returns:
|
|
753
|
+
// - element boxes = t_diagramobjects rects joined to t_object.Alias (schema id)
|
|
754
|
+
// - connector lines = t_diagramlinks rows joined to the connector's schema_id tag
|
|
755
|
+
// Returns null when the view has no matching EA diagram. Never writes EA geometry.
|
|
756
|
+
function readViewDiagramGeometry(qeaPath, viewId) {
|
|
757
|
+
const v = String(viewId === null || viewId === undefined ? '' : viewId).trim();
|
|
758
|
+
if (v === '') { return null; }
|
|
759
|
+
const db = openQea(qeaPath);
|
|
760
|
+
try {
|
|
761
|
+
ensureMetaTable(db);
|
|
762
|
+
const guid = deterministicGuid('diag:' + v);
|
|
763
|
+
const diag = db.prepare('SELECT Diagram_ID FROM t_diagram WHERE ea_guid = ? OR StyleEx LIKE ?')
|
|
764
|
+
.get(guid, '%schema_view_id=' + v + ';%');
|
|
765
|
+
if (!diag) { return null; }
|
|
766
|
+
const diagramId = Number(diag.Diagram_ID);
|
|
767
|
+
const elements = db.prepare(
|
|
768
|
+
'SELECT o.Alias AS id, d.RectLeft AS left, d.RectTop AS top, d.RectRight AS right, d.RectBottom AS bottom ' +
|
|
769
|
+
'FROM t_diagramobjects d JOIN t_object o ON o.Object_ID = d.Object_ID ' +
|
|
770
|
+
'WHERE d.Diagram_ID = ? AND o.Alias IS NOT NULL AND o.Alias <> ? ' +
|
|
771
|
+
'ORDER BY d.Sequence'
|
|
772
|
+
).all(diagramId, '').map((r) => ({
|
|
773
|
+
id: String(r.id),
|
|
774
|
+
left: Number(r.left), top: Number(r.top), right: Number(r.right), bottom: Number(r.bottom),
|
|
775
|
+
}));
|
|
776
|
+
const relationships = db.prepare(
|
|
777
|
+
'SELECT t.VALUE AS id, dl.Geometry AS path ' +
|
|
778
|
+
'FROM t_diagramlinks dl JOIN t_connectortag t ON t.ElementID = dl.ConnectorID AND t.Property = ? ' +
|
|
779
|
+
'WHERE dl.DiagramID = ? ORDER BY dl.ConnectorID'
|
|
780
|
+
).all('schema_id', diagramId).map((r) => ({
|
|
781
|
+
id: String(r.id),
|
|
782
|
+
path: String(r.path === null || r.path === undefined ? '' : r.path),
|
|
783
|
+
}));
|
|
784
|
+
return { diagramId, elements, relationships };
|
|
785
|
+
} finally {
|
|
786
|
+
try { db.close(); } catch { /* ignore */ }
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
643
790
|
function exportQeaToGraph(qeaPath) {
|
|
644
791
|
const db = openQea(qeaPath);
|
|
645
792
|
try {
|
|
@@ -679,6 +826,7 @@ module.exports = {
|
|
|
679
826
|
canonicalArchimateType,
|
|
680
827
|
syncGraphToQea,
|
|
681
828
|
exportQeaToGraph,
|
|
829
|
+
readViewDiagramGeometry,
|
|
682
830
|
snapshotQea,
|
|
683
831
|
readMetaKind,
|
|
684
832
|
fullProjection,
|
|
@@ -219,7 +219,7 @@ const TOOLS = [
|
|
|
219
219
|
},
|
|
220
220
|
{
|
|
221
221
|
name: 'getArchitectureViewContext',
|
|
222
|
-
description: 'read-only query that resolves one view by view_id into its complete membership: the view object, every member element (from included_elements), every member relationship (from included_relationships), the parent element, and optionally child sub-views declared by member elements. Resolves ids into full canonical objects instead of returning raw id lists.',
|
|
222
|
+
description: 'read-only query that resolves one view by view_id into its complete membership: the view object, every member element (from included_elements), every member relationship (from included_relationships), the parent element, and optionally child sub-views declared by member elements. Resolves ids into full canonical objects instead of returning raw id lists. Optional includeEaGeometry (default false) additionally returns the EA diagram geometry of the resolved view.',
|
|
223
223
|
inputSchema: viewContextInputSchema(),
|
|
224
224
|
},
|
|
225
225
|
{
|
|
@@ -441,6 +441,7 @@ function viewContextInputSchema() {
|
|
|
441
441
|
view_id: { type: 'string', description: 'The id of the view to resolve.' },
|
|
442
442
|
includeParentElement: { type: 'boolean', description: 'Default: true. Resolve the parent element referenced by the view.' },
|
|
443
443
|
includeChildViews: { type: 'boolean', description: 'Default: false. Include child views declared by member elements through subdiagram_views.' },
|
|
444
|
+
includeEaGeometry: { type: 'boolean', description: 'Default: false (opt-in). When true, additionally resolve the diagram GEOMETRY (element boxes + connector line paths) for this view from the workspace EA model (.qea) and return it under a `geometry` field aligned by schema id with the resolved members. By default the EA model is never touched and no `geometry` field is returned; a missing EA model/diagram yields geometry.present=false, never an error.' },
|
|
444
445
|
},
|
|
445
446
|
additionalProperties: false,
|
|
446
447
|
};
|
|
@@ -678,6 +679,50 @@ function buildIntentElementContext(context, args = {}) {
|
|
|
678
679
|
};
|
|
679
680
|
}
|
|
680
681
|
|
|
682
|
+
// ---------------------------------------------------------------------------
|
|
683
|
+
// Optional EA diagram GEOMETRY (opt-in, default off)
|
|
684
|
+
// ---------------------------------------------------------------------------
|
|
685
|
+
// getArchitectureViewContext is a canonical-graph read; it never touches the EA
|
|
686
|
+
// model unless the caller explicitly sets includeEaGeometry=true. The workspace's
|
|
687
|
+
// EA model (.qea, the SQLite carrier this toolchain can read) may hold human-laid-out
|
|
688
|
+
// diagram geometry for a view — element boxes (t_diagramobjects rects) and connector
|
|
689
|
+
// line paths (t_diagramlinks). When present it is returned under `geometry`, aligned
|
|
690
|
+
// by schema id with the resolved members, so an image-capable LLM can redraw the view
|
|
691
|
+
// faithfully. Absent model/diagram → present:false (never an error).
|
|
692
|
+
const EA_GEOMETRY_MODEL_EXTENSIONS = new Set(['.qea']);
|
|
693
|
+
function findEaGeometryModelPath(workspaceRoot) {
|
|
694
|
+
try {
|
|
695
|
+
const entries = fs.readdirSync(workspaceRoot, { withFileTypes: true });
|
|
696
|
+
const names = entries
|
|
697
|
+
.filter((entry) => entry.isFile() && EA_GEOMETRY_MODEL_EXTENSIONS.has(path.extname(entry.name).toLowerCase()))
|
|
698
|
+
.map((entry) => entry.name)
|
|
699
|
+
.sort();
|
|
700
|
+
return names.length > 0 ? path.join(workspaceRoot, names[0]) : null;
|
|
701
|
+
} catch {
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
function readEaViewGeometry(workspaceRoot, viewId) {
|
|
706
|
+
// Lazily require the .qea projection lib: it depends on node:sqlite (Node >= 22),
|
|
707
|
+
// so it is only loaded when geometry is actually requested, never for other calls.
|
|
708
|
+
let modelRel = null;
|
|
709
|
+
try {
|
|
710
|
+
const modelPath = findEaGeometryModelPath(workspaceRoot);
|
|
711
|
+
if (!modelPath) {
|
|
712
|
+
return { source: null, present: false, elements: [], relationships: [] };
|
|
713
|
+
}
|
|
714
|
+
modelRel = normalizeRelativePath(path.relative(workspaceRoot, modelPath)) || null;
|
|
715
|
+
const eaQeaLib = require('./ea-qea-sync-lib.js');
|
|
716
|
+
const geo = eaQeaLib.readViewDiagramGeometry(modelPath, viewId);
|
|
717
|
+
if (!geo) {
|
|
718
|
+
return { source: modelRel, present: false, elements: [], relationships: [] };
|
|
719
|
+
}
|
|
720
|
+
return { source: modelRel, present: true, elements: geo.elements, relationships: geo.relationships };
|
|
721
|
+
} catch {
|
|
722
|
+
return { source: modelRel, present: false, elements: [], relationships: [] };
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
681
726
|
function buildViewContext(context, args = {}) {
|
|
682
727
|
const viewId = typeof args.view_id === 'string' ? args.view_id.trim() : '';
|
|
683
728
|
if (!viewId) {
|
|
@@ -746,7 +791,7 @@ function buildViewContext(context, args = {}) {
|
|
|
746
791
|
}
|
|
747
792
|
}
|
|
748
793
|
|
|
749
|
-
|
|
794
|
+
const result = {
|
|
750
795
|
status: 'passed',
|
|
751
796
|
graphPath: context.graphPath.relativePath,
|
|
752
797
|
view: clone(view),
|
|
@@ -757,6 +802,10 @@ function buildViewContext(context, args = {}) {
|
|
|
757
802
|
parentElement,
|
|
758
803
|
childViews,
|
|
759
804
|
};
|
|
805
|
+
if (args.includeEaGeometry === true) {
|
|
806
|
+
result.geometry = readEaViewGeometry(context.workspaceRoot, viewId);
|
|
807
|
+
}
|
|
808
|
+
return result;
|
|
760
809
|
}
|
|
761
810
|
|
|
762
811
|
function resolveFocusElement(document, args) {
|
|
@@ -1616,9 +1665,12 @@ async function buildMutationResult(context, mutations, write) {
|
|
|
1616
1665
|
writeGraph(context.graphPath.absolutePath, mutationResult.document);
|
|
1617
1666
|
result.written = true;
|
|
1618
1667
|
|
|
1619
|
-
// WP2791: .qea projection parallel to the Neo4j trigger — non-fatal, best-effort
|
|
1668
|
+
// WP2791: .qea projection parallel to the Neo4j trigger — non-fatal, best-effort,
|
|
1669
|
+
// but ALWAYS reported on the result (passed / failed / noop+reason) so a missing EA
|
|
1670
|
+
// update is never silent.
|
|
1620
1671
|
{
|
|
1621
|
-
const
|
|
1672
|
+
const resolved = resolveQeaProjectionTarget(context);
|
|
1673
|
+
const qeaTarget = resolved && resolved.target;
|
|
1622
1674
|
if (qeaTarget) {
|
|
1623
1675
|
try {
|
|
1624
1676
|
const projection = await runQeaProjection(qeaTarget);
|
|
@@ -1632,6 +1684,15 @@ async function buildMutationResult(context, mutations, write) {
|
|
|
1632
1684
|
result.qeaProjection = { status: 'failed', error: String(error && error.message ? error.message : error) };
|
|
1633
1685
|
result.warnings = addUnique(result.warnings || [], ['ea-qea projection error (non-fatal): ' + String(error && error.message ? error.message : error)]);
|
|
1634
1686
|
}
|
|
1687
|
+
} else {
|
|
1688
|
+
result.qeaProjection = {
|
|
1689
|
+
status: 'noop',
|
|
1690
|
+
reason: (resolved && resolved.reason) || 'no .qea target',
|
|
1691
|
+
workspaceRoot: context.workspaceRoot,
|
|
1692
|
+
};
|
|
1693
|
+
if (resolved && resolved.hasEaSignals) {
|
|
1694
|
+
result.warnings = addUnique(result.warnings || [], ['ea-qea projection not run: ' + ((resolved && resolved.reason) || 'no .qea target')]);
|
|
1695
|
+
}
|
|
1635
1696
|
}
|
|
1636
1697
|
}
|
|
1637
1698
|
|
|
@@ -1832,59 +1893,79 @@ function summarizeDocument(document) {
|
|
|
1832
1893
|
};
|
|
1833
1894
|
}
|
|
1834
1895
|
|
|
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
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
let
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
return;
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
}
|
|
1887
|
-
|
|
1896
|
+
// --- WP2791: post-canonical-write .qea projection (parallel to Neo4j sync, non-fatal) ---
|
|
1897
|
+
// Target resolution (decision qea-full-wholefile-argo-scripts-no-config): env ARGO_EA_QEA >
|
|
1898
|
+
// the single *.qea at the workspace root (0/many -> no-op with an explicit log). NO config file.
|
|
1899
|
+
// Projection script runs from argo/scripts (same package as the MCP runtime), so a workspace
|
|
1900
|
+
// does not need to ship its own projection script (bundled argo/scripts module).
|
|
1901
|
+
function resolveQeaProjectionTarget(context) {
|
|
1902
|
+
const none = (reason, hasEaSignals) => ({ target: null, reason, hasEaSignals: !!hasEaSignals });
|
|
1903
|
+
try {
|
|
1904
|
+
const workspaceRoot = String(context && context.workspaceRoot ? context.workspaceRoot : '');
|
|
1905
|
+
if (!workspaceRoot || !fs.existsSync(workspaceRoot)) { return none('workspace root unavailable: ' + workspaceRoot, true); }
|
|
1906
|
+
const graphAbsolute = context.graphPath && context.graphPath.absolutePath ? context.graphPath.absolutePath : null;
|
|
1907
|
+
if (!graphAbsolute || !fs.existsSync(graphAbsolute)) { return none('canonical graph not found under ' + workspaceRoot, false); }
|
|
1908
|
+
const pick = (p) => (p && fs.existsSync(p) ? path.resolve(p) : null);
|
|
1909
|
+
const envTarget = process.env.ARGO_EA_QEA;
|
|
1910
|
+
let qeaPath = pick(envTarget);
|
|
1911
|
+
if (qeaPath) { return { target: { qeaPath, graphPath: graphAbsolute, workspaceRoot } }; }
|
|
1912
|
+
let qeas = [];
|
|
1913
|
+
try { qeas = fs.readdirSync(workspaceRoot).filter((n) => n.toLowerCase().endsWith('.qea')).sort(); } catch { /* ignore */ }
|
|
1914
|
+
if (qeas.length === 1) {
|
|
1915
|
+
return { target: { qeaPath: path.resolve(workspaceRoot, qeas[0]), graphPath: graphAbsolute, workspaceRoot } };
|
|
1916
|
+
}
|
|
1917
|
+
let eaFiles = [];
|
|
1918
|
+
try { eaFiles = fs.readdirSync(workspaceRoot).filter((n) => /\.(qea|feap|eap)$/i.test(n)).sort(); } catch { /* ignore */ }
|
|
1919
|
+
if (qeas.length > 1) {
|
|
1920
|
+
const reason = `${qeas.length} *.qea files found at the workspace root (${qeas.join(', ')}); expected exactly one or set ARGO_EA_QEA`;
|
|
1921
|
+
console.log('[ea-qea] projection target: none (' + reason + ') in ' + workspaceRoot);
|
|
1922
|
+
return none(reason, true);
|
|
1923
|
+
}
|
|
1924
|
+
const legacy = eaFiles.filter((n) => /\.(feap|eap)$/i.test(n));
|
|
1925
|
+
if (legacy.length > 0) {
|
|
1926
|
+
const reason = `EA model is a legacy ${legacy.join(', ')} (Firebird/Jet); the direct .qea projection cannot write it — convert to .qea (EA 17.2+) or set ARGO_EA_QEA to a .qea target`;
|
|
1927
|
+
console.log('[ea-qea] projection target: none (' + reason + ') in ' + workspaceRoot);
|
|
1928
|
+
return none(reason, true);
|
|
1929
|
+
}
|
|
1930
|
+
if (envTarget) {
|
|
1931
|
+
const reason = 'ARGO_EA_QEA points to a missing .qea file: ' + envTarget;
|
|
1932
|
+
console.log('[ea-qea] projection target: none (' + reason + ')');
|
|
1933
|
+
return none(reason, true);
|
|
1934
|
+
}
|
|
1935
|
+
console.log('[ea-qea] projection target: none in ' + workspaceRoot);
|
|
1936
|
+
return none('no .qea target found (set ARGO_EA_QEA or place exactly one *.qea at the workspace root)', false);
|
|
1937
|
+
} catch (error) {
|
|
1938
|
+
console.log('[ea-qea] projection target resolution failed: ' + String(error && error.message ? error.message : error));
|
|
1939
|
+
return none('EA .qea target resolution failed: ' + String(error && error.message ? error.message : error), true);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
function runQeaProjection(target) {
|
|
1944
|
+
return new Promise((resolve) => {
|
|
1945
|
+
const script = path.join(__dirname, 'ea-qea-sync.js');
|
|
1946
|
+
if (!fs.existsSync(script)) {
|
|
1947
|
+
resolve({ ok: false, error: 'argo/scripts/ea-qea-sync.js missing', ms: 0 });
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
const snapshotDir = path.join(target.workspaceRoot, '.argo', 'temp', 'qea-backups');
|
|
1951
|
+
const args = [script, '--mode', 'sync', '--graph', target.graphPath, '--qea', target.qeaPath, '--snapshot-dir', snapshotDir];
|
|
1952
|
+
const started = Date.now();
|
|
1953
|
+
let stderr = '';
|
|
1954
|
+
let child;
|
|
1955
|
+
try {
|
|
1956
|
+
child = spawn(process.execPath, args, { cwd: target.workspaceRoot, windowsHide: true });
|
|
1957
|
+
} catch (error) {
|
|
1958
|
+
resolve({ ok: false, error: String(error && error.message ? error.message : error), ms: Date.now() - started });
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
child.stderr.on('data', (d) => { stderr += String(d); });
|
|
1962
|
+
child.on('error', (err) => resolve({ ok: false, error: String(err && err.message ? err.message : err), ms: Date.now() - started, stderr: stderr.slice(0, 600) }));
|
|
1963
|
+
child.on('close', (code) => {
|
|
1964
|
+
resolve({ ok: code === 0, code, ms: Date.now() - started, stderr: stderr.slice(0, 600) });
|
|
1965
|
+
});
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1888
1969
|
function writeGraph(graphPath, document) {
|
|
1889
1970
|
const tempPath = `${graphPath}.${process.pid}.${Date.now()}.tmp`;
|
|
1890
1971
|
fs.writeFileSync(tempPath, `${JSON.stringify(document, null, 2)}\n`, 'utf8');
|
|
@@ -2167,6 +2248,26 @@ function compactMutationResponse(payload) {
|
|
|
2167
2248
|
if (payload && payload.embeddingLifecycle && payload.embeddingLifecycle.state) {
|
|
2168
2249
|
compact.embeddingLifecycle = { state: payload.embeddingLifecycle.state };
|
|
2169
2250
|
}
|
|
2251
|
+
// Successful writes stay compact but downstream projection side effects (EA .qea,
|
|
2252
|
+
// Neo4j) and non-fatal warnings must stay observable — a caller deciding whether
|
|
2253
|
+
// "the EA file reflects this write" relies on qeaProjection.status (passed /
|
|
2254
|
+
// failed / noop + reason), never on silence.
|
|
2255
|
+
if (payload && payload.qeaProjection) {
|
|
2256
|
+
const q = payload.qeaProjection;
|
|
2257
|
+
if (q.status === 'passed') {
|
|
2258
|
+
compact.qeaProjection = { status: 'passed', ms: q.ms };
|
|
2259
|
+
} else if (q.status === 'noop') {
|
|
2260
|
+
compact.qeaProjection = { status: 'noop', reason: q.reason };
|
|
2261
|
+
} else {
|
|
2262
|
+
compact.qeaProjection = { status: q.status, error: q.error || q.reason };
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
if (payload && payload.neo4jSync) {
|
|
2266
|
+
compact.neo4jSync = { status: payload.neo4jSync.status };
|
|
2267
|
+
}
|
|
2268
|
+
if (Array.isArray(payload && payload.warnings) && payload.warnings.length > 0) {
|
|
2269
|
+
compact.warnings = payload.warnings;
|
|
2270
|
+
}
|
|
2170
2271
|
// Failed actual writes must retain business diagnostics (e.g. View15 maximum/observed)
|
|
2171
2272
|
// so callers can distinguish reject reasons; successful writes stay compact.
|
|
2172
2273
|
if (payload && payload.status === 'failed') {
|
package/package.json
CHANGED