archgraph-argo 0.14.0 → 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;
@@ -290,14 +292,21 @@ function setStyleToken(styleEx, key, value) {
290
292
  // ---------------------------------------------------------------------------
291
293
  // Core sync
292
294
  // ---------------------------------------------------------------------------
293
- // 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.
294
301
  function syncGraphToQea(graph, qeaPath, opts) {
295
302
  const o = opts || {};
303
+ const pruneMembership = o.pruneMembership !== false;
296
304
  const stages = {};
297
305
  const stats = {
298
306
  added: { elements: 0, relationships: 0, diagrams: 0, diagramObjects: 0, diagramLinks: 0 },
299
307
  updated: { elements: 0, relationships: 0, diagrams: 0 },
300
308
  skipped: { elements: 0, relationships: 0, diagrams: 0 },
309
+ removed: { diagramObjects: 0, diagramLinks: 0 },
301
310
  deleteCandidates: 0, deleted: 0,
302
311
  };
303
312
  const t0 = nowMs();
@@ -450,6 +459,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
450
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();
451
460
  for (const r of existingRels) { if (r.ea_guid) { relByGuid.set(String(r.ea_guid), r); } }
452
461
  const newRels = [];
462
+ const relAliasToId = new Map();
453
463
  for (const rel of graph.relationships || []) {
454
464
  if (!rel || rel.id === undefined || rel.id === null) { continue; }
455
465
  const alias = String(rel.id);
@@ -475,6 +485,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
475
485
  End_Object_ID: Number(end),
476
486
  };
477
487
  if (existing) {
488
+ relAliasToId.set(alias, Number(existing.Connector_ID));
478
489
  const changed = intended.Name !== (existing.Name || '') || (intended.Connector_Type || '') !== (existing.Connector_Type || '') ||
479
490
  (intended.Stereotype || '') !== (existing.Stereotype || '') || intended.Notes !== (existing.Notes || '') ||
480
491
  intended.Direction !== (existing.Direction || '') || Number(intended.Start_Object_ID) !== Number(existing.Start_Object_ID || 0) ||
@@ -488,7 +499,6 @@ function syncGraphToQea(graph, qeaPath, opts) {
488
499
  stats.added.relationships++;
489
500
  }
490
501
  }
491
- const relAliasToId = new Map();
492
502
  if (!o.dryRun) {
493
503
  if (newRels.length > 0) {
494
504
  insertMany(db, 't_connector', ['Name', 'Connector_Type', 'Stereotype', 'Notes', 'Direction', 'Start_Object_ID', 'End_Object_ID', 'ea_guid'], newRels);
@@ -608,18 +618,43 @@ function syncGraphToQea(graph, qeaPath, opts) {
608
618
  if (view && view.view_id !== undefined && view.view_id !== null) { upsertMeta(db, 'view', view.view_id, view); }
609
619
  }
610
620
  }
611
- // 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
+ );
612
631
  for (const view of graph.views || []) {
613
632
  if (!view || view.view_id === undefined || view.view_id === null) { continue; }
614
633
  const viewId = String(view.view_id);
615
634
  const diagramId = diagIdForView(viewId);
616
635
  if (diagramId === null) { continue; }
636
+ const incl = view.included_elements || [];
617
637
  const placedObjs = new Set();
618
638
  const objs = db.prepare('SELECT Object_ID FROM t_diagramobjects WHERE Diagram_ID=?').all(diagramId);
619
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
+
620
656
  const nextSeq = objs.length;
621
657
  const newObjs = [];
622
- const incl = view.included_elements || [];
623
658
  let seq = nextSeq;
624
659
  for (const elId of incl) {
625
660
  const oid = elemIdByAliasAll.get(String(elId));
@@ -640,11 +675,28 @@ function syncGraphToQea(graph, qeaPath, opts) {
640
675
  }
641
676
  stats.added.diagramObjects += newObjs.length;
642
677
 
678
+ const relIncl = view.included_relationships || [];
643
679
  const placedLinks = new Set();
644
680
  const links = db.prepare('SELECT ConnectorID FROM t_diagramlinks WHERE DiagramID=?').all(diagramId);
645
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
+
646
698
  const newLinks = [];
647
- for (const relId of view.included_relationships || []) {
699
+ for (const relId of relIncl) {
648
700
  const cid = relAliasToId.get(String(relId));
649
701
  if (cid === undefined || cid < 0) { continue; }
650
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.14.0",
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": {