archgraph-argo 0.13.9 → 0.15.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.
@@ -11,8 +11,10 @@
11
11
  // transactions with a bounded busy retry. Keep EA's own SQLite handle open is fine —
12
12
  // SQLite only locks during active transactions, an idle open connection does not block.
13
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.
14
+ // existing t_diagramobjects/t_diagramlinks geometry of STILL-VISIBLE members is never
15
+ // rewritten; the membership reconcile only INSERTs missing members and PRUNES stale
16
+ // schema-anchored members whose element/relationship is no longer in the view
17
+ // (pruneMembership, default true). Human-drawn (un-anchored) shapes are never pruned.
16
18
 
17
19
  const { DatabaseSync } = require('node:sqlite');
18
20
  const DEBUG = !!process.env.EA_QEA_DEBUG;
@@ -275,18 +277,36 @@ function ensureStyleToken(styleEx, keyValue) {
275
277
  const token = String(keyValue).indexOf('=') >= 0 ? String(keyValue) + ';' : String(keyValue) + '=;';
276
278
  return text ? token + text : token;
277
279
  }
280
+ // Force a StyleEx token to a value (replaces an existing one, else prepends).
281
+ // Used for DLKO=1 (the "Freeze Visible" connector-display flag EA persists in the
282
+ // diagram's StyleEx): every projected diagram must default to Freeze Visible ON, even
283
+ // if EA previously stored DLKO=0 / dropped it after a human unchecked the box.
284
+ function setStyleToken(styleEx, key, value) {
285
+ const text = String(styleEx === null || styleEx === undefined ? '' : styleEx);
286
+ const re = new RegExp('(^|;|\\s)' + key + '=[^;]*', 'i');
287
+ if (re.test(text)) { return text.replace(re, (m, pre) => pre + key + '=' + value); }
288
+ const token = key + '=' + value + ';';
289
+ return text ? token + text : token;
290
+ }
278
291
 
279
292
  // ---------------------------------------------------------------------------
280
293
  // Core sync
281
294
  // ---------------------------------------------------------------------------
282
- // opts: { dryRun, allowDelete, snapshotDir }
295
+ // opts: { dryRun, allowDelete, snapshotDir, pruneMembership }
296
+ // pruneMembership (default true): the incremental membership reconcile is
297
+ // authoritative for schema-anchored members — a shape/connector whose canonical
298
+ // element/relationship is no longer in the view's included_elements/included_relationships
299
+ // is pruned from that diagram. Human-drawn shapes (no schema_id anchor) are never
300
+ // pruned, and still-in-view members keep their geometry untouched.
283
301
  function syncGraphToQea(graph, qeaPath, opts) {
284
302
  const o = opts || {};
303
+ const pruneMembership = o.pruneMembership !== false;
285
304
  const stages = {};
286
305
  const stats = {
287
306
  added: { elements: 0, relationships: 0, diagrams: 0, diagramObjects: 0, diagramLinks: 0 },
288
307
  updated: { elements: 0, relationships: 0, diagrams: 0 },
289
308
  skipped: { elements: 0, relationships: 0, diagrams: 0 },
309
+ removed: { diagramObjects: 0, diagramLinks: 0 },
290
310
  deleteCandidates: 0, deleted: 0,
291
311
  };
292
312
  const t0 = nowMs();
@@ -439,6 +459,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
439
459
  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();
440
460
  for (const r of existingRels) { if (r.ea_guid) { relByGuid.set(String(r.ea_guid), r); } }
441
461
  const newRels = [];
462
+ const relAliasToId = new Map();
442
463
  for (const rel of graph.relationships || []) {
443
464
  if (!rel || rel.id === undefined || rel.id === null) { continue; }
444
465
  const alias = String(rel.id);
@@ -464,6 +485,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
464
485
  End_Object_ID: Number(end),
465
486
  };
466
487
  if (existing) {
488
+ relAliasToId.set(alias, Number(existing.Connector_ID));
467
489
  const changed = intended.Name !== (existing.Name || '') || (intended.Connector_Type || '') !== (existing.Connector_Type || '') ||
468
490
  (intended.Stereotype || '') !== (existing.Stereotype || '') || intended.Notes !== (existing.Notes || '') ||
469
491
  intended.Direction !== (existing.Direction || '') || Number(intended.Start_Object_ID) !== Number(existing.Start_Object_ID || 0) ||
@@ -477,7 +499,6 @@ function syncGraphToQea(graph, qeaPath, opts) {
477
499
  stats.added.relationships++;
478
500
  }
479
501
  }
480
- const relAliasToId = new Map();
481
502
  if (!o.dryRun) {
482
503
  if (newRels.length > 0) {
483
504
  insertMany(db, 't_connector', ['Name', 'Connector_Type', 'Stereotype', 'Notes', 'Direction', 'Start_Object_ID', 'End_Object_ID', 'ea_guid'], newRels);
@@ -530,7 +551,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
530
551
  for (const view of graph.views || []) {
531
552
  if (!view || view.view_id === undefined || view.view_id === null) { continue; }
532
553
  const viewId = String(view.view_id);
533
- const styleEx = 'schema_view_id=' + viewId + ';';
554
+ const styleEx = 'schema_view_id=' + viewId + ';DLKO=1;';
534
555
  const parentObjectId = (function () {
535
556
  if (view.parent_element_id !== undefined && view.parent_element_id !== null && view.parent_element_id !== '') {
536
557
  const pid = elemIdByAliasAll.get(String(view.parent_element_id));
@@ -550,8 +571,12 @@ function syncGraphToQea(graph, qeaPath, opts) {
550
571
  if (existing) {
551
572
  diagViewRows.set(viewId, existing);
552
573
  // EA may have rewritten StyleEx and dropped the anchor — re-inject it while
553
- // preserving EA's own formatting tokens so identity stays discoverable.
554
- const anchoredStyleEx = ensureStyleToken(existing.StyleEx, 'schema_view_id=' + viewId);
574
+ // preserving EA's own formatting tokens so identity stays discoverable. Also
575
+ // force DLKO=1 so every diagram defaults to "Freeze Visible" checked.
576
+ const anchoredStyleEx = ensureStyleToken(
577
+ setStyleToken(existing.StyleEx, 'DLKO', '1'),
578
+ 'schema_view_id=' + viewId
579
+ );
555
580
  const changed = intended.Name !== (existing.Name || '') || (existing.StyleEx || '') !== anchoredStyleEx;
556
581
  if (DEBUG && changed) { console.error('DEBUG diagram chg', viewId, JSON.stringify({n:[intended.Name,(existing.Name||'')], style: !!parseStyleToken(existing.StyleEx,'schema_view_id')})); }
557
582
  if (changed && !o.dryRun) {
@@ -593,18 +618,43 @@ function syncGraphToQea(graph, qeaPath, opts) {
593
618
  if (view && view.view_id !== undefined && view.view_id !== null) { upsertMeta(db, 'view', view.view_id, view); }
594
619
  }
595
620
  }
596
- // memberships: only INSERT missing; never touch existing geometry
621
+ // memberships: insert missing members, and prune stale schema-anchored members
622
+ // whose element/relationship is no longer in the view (authoritative canonical
623
+ // membership reconcile). Still-in-view members keep their geometry untouched;
624
+ // human-drawn shapes (no schema anchor) are never pruned.
625
+ const canonicalElemObjIds = new Set(
626
+ [...elemIdByAliasAll.values()].filter((v) => v >= 0).map((v) => Number(v))
627
+ );
628
+ const canonicalRelCids = new Set(
629
+ [...relAliasToId.values()].filter((v) => v >= 0).map((v) => Number(v))
630
+ );
597
631
  for (const view of graph.views || []) {
598
632
  if (!view || view.view_id === undefined || view.view_id === null) { continue; }
599
633
  const viewId = String(view.view_id);
600
634
  const diagramId = diagIdForView(viewId);
601
635
  if (diagramId === null) { continue; }
636
+ const incl = view.included_elements || [];
602
637
  const placedObjs = new Set();
603
638
  const objs = db.prepare('SELECT Object_ID FROM t_diagramobjects WHERE Diagram_ID=?').all(diagramId);
604
639
  for (const r of objs) { placedObjs.add(Number(r.Object_ID)); }
640
+
641
+ if (pruneMembership) {
642
+ const wantedObjs = new Set();
643
+ for (const elId of incl) {
644
+ const oid = elemIdByAliasAll.get(String(elId));
645
+ if (oid !== undefined) { wantedObjs.add(Number(oid)); }
646
+ }
647
+ const stale = [...placedObjs].filter((oid) => !wantedObjs.has(oid) && canonicalElemObjIds.has(oid));
648
+ if (stale.length > 0 && !o.dryRun) {
649
+ const del = db.prepare('DELETE FROM t_diagramobjects WHERE Diagram_ID=? AND Object_ID=?');
650
+ for (const oid of stale) { del.run(diagramId, oid); }
651
+ }
652
+ stats.removed.diagramObjects += stale.length;
653
+ for (const oid of stale) { placedObjs.delete(oid); }
654
+ }
655
+
605
656
  const nextSeq = objs.length;
606
657
  const newObjs = [];
607
- const incl = view.included_elements || [];
608
658
  let seq = nextSeq;
609
659
  for (const elId of incl) {
610
660
  const oid = elemIdByAliasAll.get(String(elId));
@@ -625,11 +675,28 @@ function syncGraphToQea(graph, qeaPath, opts) {
625
675
  }
626
676
  stats.added.diagramObjects += newObjs.length;
627
677
 
678
+ const relIncl = view.included_relationships || [];
628
679
  const placedLinks = new Set();
629
680
  const links = db.prepare('SELECT ConnectorID FROM t_diagramlinks WHERE DiagramID=?').all(diagramId);
630
681
  for (const r of links) { placedLinks.add(Number(r.ConnectorID)); }
682
+
683
+ if (pruneMembership) {
684
+ const wantedRelCids = new Set();
685
+ for (const relId of relIncl) {
686
+ const cid = relAliasToId.get(String(relId));
687
+ if (cid !== undefined && cid >= 0) { wantedRelCids.add(Number(cid)); }
688
+ }
689
+ const staleLinks = [...placedLinks].filter((cid) => !wantedRelCids.has(cid) && canonicalRelCids.has(cid));
690
+ if (staleLinks.length > 0 && !o.dryRun) {
691
+ const del = db.prepare('DELETE FROM t_diagramlinks WHERE DiagramID=? AND ConnectorID=?');
692
+ for (const cid of staleLinks) { del.run(diagramId, cid); }
693
+ }
694
+ stats.removed.diagramLinks += staleLinks.length;
695
+ for (const cid of staleLinks) { placedLinks.delete(cid); }
696
+ }
697
+
631
698
  const newLinks = [];
632
- for (const relId of view.included_relationships || []) {
699
+ for (const relId of relIncl) {
633
700
  const cid = relAliasToId.get(String(relId));
634
701
  if (cid === undefined || cid < 0) { continue; }
635
702
  if (placedLinks.has(Number(cid))) { continue; }
@@ -17,7 +17,7 @@ const fs = require('node:fs');
17
17
  const lib = require(path.join(__dirname, 'ea-qea-sync-lib.js'));
18
18
 
19
19
  function parseArgs(argv) {
20
- const args = { mode: 'sync', graph: '', qea: '', allowDelete: false, deleteConfirmFile: '', dryRun: false, snapshotDir: '', out: '', noBackup: false, intervalMs: 2000 };
20
+ const args = { mode: 'sync', graph: '', qea: '', allowDelete: false, deleteConfirmFile: '', dryRun: false, snapshotDir: '', out: '', noBackup: false, intervalMs: 2000, pruneMembership: true };
21
21
  for (let i = 0; i < argv.length; i++) {
22
22
  const a = argv[i];
23
23
  const next = () => (i + 1 < argv.length ? argv[++i] : '');
@@ -30,6 +30,7 @@ function parseArgs(argv) {
30
30
  else if (a === '--snapshot-dir') { args.snapshotDir = next(); }
31
31
  else if (a === '--out') { args.out = next(); }
32
32
  else if (a === '--no-backup') { args.noBackup = true; }
33
+ else if (a === '--no-prune') { args.pruneMembership = false; }
33
34
  else if (a === '--interval') { args.intervalMs = Number(next()) || 2000; }
34
35
  else if (a.startsWith('-')) { /* ignore unknown */ }
35
36
  else if (args.modeSet === undefined) { /* positional not used */ }
@@ -98,7 +99,7 @@ function runOnce(args, graphPath, qeaPath) {
98
99
  }
99
100
  const res = args.mode === 'full'
100
101
  ? lib.fullProjection(graph, qeaPath, { dryRun: args.dryRun })
101
- : lib.syncGraphToQea(graph, qeaPath, { dryRun: args.dryRun, allowDelete: confirmDelete(args) });
102
+ : lib.syncGraphToQea(graph, qeaPath, { dryRun: args.dryRun, allowDelete: confirmDelete(args), pruneMembership: args.pruneMembership });
102
103
  const mode = args.dryRun ? 'dry-run' : 'sync';
103
104
  console.log(JSON.stringify({ mode, graph: graphPath, qea: qeaPath, snapshot, result: res }, null, 2));
104
105
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archgraph-argo",
3
- "version": "0.13.9",
3
+ "version": "0.15.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": {