archgraph-argo 0.14.0 → 0.15.1

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;
@@ -20,6 +22,10 @@ const crypto = require('node:crypto');
20
22
 
21
23
  const SYNC_PACKAGE_NAME = 'ArchGraph Sync';
22
24
  const DIAGRAM_TYPE = 'Logical';
25
+ // Attributes/Operations "Show Compartments" (Elements tab) default unchecked: EA stores
26
+ // these in t_diagram.PDATA as HideAtts / HideOps (hide compartment = 1). Projected
27
+ // diagrams hide both compartments by default while preserving EA's other PDATA tokens.
28
+ const DIAGRAM_DISPLAY_DEFAULT = 'HideAtts=1;HideOps=1;';
23
29
  const META_TABLE = 'kg_sync_meta'; // {kind,key,sha,payload} — Node export/reconcile store
24
30
  const BUSY_TIMEOUT_MS = 15000;
25
31
  const CHUNK = 200;
@@ -290,14 +296,21 @@ function setStyleToken(styleEx, key, value) {
290
296
  // ---------------------------------------------------------------------------
291
297
  // Core sync
292
298
  // ---------------------------------------------------------------------------
293
- // opts: { dryRun, allowDelete, snapshotDir }
299
+ // opts: { dryRun, allowDelete, snapshotDir, pruneMembership }
300
+ // pruneMembership (default true): the incremental membership reconcile is
301
+ // authoritative for schema-anchored members — a shape/connector whose canonical
302
+ // element/relationship is no longer in the view's included_elements/included_relationships
303
+ // is pruned from that diagram. Human-drawn shapes (no schema_id anchor) are never
304
+ // pruned, and still-in-view members keep their geometry untouched.
294
305
  function syncGraphToQea(graph, qeaPath, opts) {
295
306
  const o = opts || {};
307
+ const pruneMembership = o.pruneMembership !== false;
296
308
  const stages = {};
297
309
  const stats = {
298
310
  added: { elements: 0, relationships: 0, diagrams: 0, diagramObjects: 0, diagramLinks: 0 },
299
311
  updated: { elements: 0, relationships: 0, diagrams: 0 },
300
312
  skipped: { elements: 0, relationships: 0, diagrams: 0 },
313
+ removed: { diagramObjects: 0, diagramLinks: 0 },
301
314
  deleteCandidates: 0, deleted: 0,
302
315
  };
303
316
  const t0 = nowMs();
@@ -450,6 +463,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
450
463
  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
464
  for (const r of existingRels) { if (r.ea_guid) { relByGuid.set(String(r.ea_guid), r); } }
452
465
  const newRels = [];
466
+ const relAliasToId = new Map();
453
467
  for (const rel of graph.relationships || []) {
454
468
  if (!rel || rel.id === undefined || rel.id === null) { continue; }
455
469
  const alias = String(rel.id);
@@ -475,6 +489,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
475
489
  End_Object_ID: Number(end),
476
490
  };
477
491
  if (existing) {
492
+ relAliasToId.set(alias, Number(existing.Connector_ID));
478
493
  const changed = intended.Name !== (existing.Name || '') || (intended.Connector_Type || '') !== (existing.Connector_Type || '') ||
479
494
  (intended.Stereotype || '') !== (existing.Stereotype || '') || intended.Notes !== (existing.Notes || '') ||
480
495
  intended.Direction !== (existing.Direction || '') || Number(intended.Start_Object_ID) !== Number(existing.Start_Object_ID || 0) ||
@@ -488,7 +503,6 @@ function syncGraphToQea(graph, qeaPath, opts) {
488
503
  stats.added.relationships++;
489
504
  }
490
505
  }
491
- const relAliasToId = new Map();
492
506
  if (!o.dryRun) {
493
507
  if (newRels.length > 0) {
494
508
  insertMany(db, 't_connector', ['Name', 'Connector_Type', 'Stereotype', 'Notes', 'Direction', 'Start_Object_ID', 'End_Object_ID', 'ea_guid'], newRels);
@@ -528,7 +542,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
528
542
  // it touches an open project and DROPS unknown tokens like schema_view_id — if we
529
543
  // only matched by the token we would re-INSERT the same deterministic ea_guid and
530
544
  // crash on t_diagram's UNIQUE(ea_guid) (projection failure: Neo4j ok, EA stale).
531
- const existingDiags = db.prepare('SELECT Diagram_ID, Package_ID, Name, StyleEx, ea_guid FROM t_diagram WHERE Package_ID=?').all(syncId);
545
+ const existingDiags = db.prepare('SELECT Diagram_ID, Package_ID, Name, StyleEx, PDATA, ea_guid FROM t_diagram WHERE Package_ID=?').all(syncId);
532
546
  const diagByView = new Map();
533
547
  const diagByGuid = new Map();
534
548
  for (const d of existingDiags) {
@@ -557,6 +571,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
557
571
  ParentID: parentObjectId,
558
572
  Notes: '', // EA .qea 不保留多段 Notes;视图内容经 kg_sync_meta 保真
559
573
  StyleEx: styleEx,
574
+ PDATA: DIAGRAM_DISPLAY_DEFAULT,
560
575
  };
561
576
  if (existing) {
562
577
  diagViewRows.set(viewId, existing);
@@ -567,11 +582,17 @@ function syncGraphToQea(graph, qeaPath, opts) {
567
582
  setStyleToken(existing.StyleEx, 'DLKO', '1'),
568
583
  'schema_view_id=' + viewId
569
584
  );
570
- const changed = intended.Name !== (existing.Name || '') || (existing.StyleEx || '') !== anchoredStyleEx;
585
+ // Default Attributes/Operations compartments unchecked: force HideAtts=1 /
586
+ // HideOps=1 into PDATA while preserving EA's other display tokens.
587
+ const anchoredPdata = setStyleToken(
588
+ setStyleToken(existing.PDATA, 'HideAtts', '1'),
589
+ 'HideOps', '1'
590
+ );
591
+ const changed = intended.Name !== (existing.Name || '') || (existing.StyleEx || '') !== anchoredStyleEx || (existing.PDATA || '') !== anchoredPdata;
571
592
  if (DEBUG && changed) { console.error('DEBUG diagram chg', viewId, JSON.stringify({n:[intended.Name,(existing.Name||'')], style: !!parseStyleToken(existing.StyleEx,'schema_view_id')})); }
572
593
  if (changed && !o.dryRun) {
573
- db.prepare('UPDATE t_diagram SET Name=?, StyleEx=? WHERE Diagram_ID=?')
574
- .run(intended.Name, anchoredStyleEx, Number(existing.Diagram_ID));
594
+ db.prepare('UPDATE t_diagram SET Name=?, StyleEx=?, PDATA=? WHERE Diagram_ID=?')
595
+ .run(intended.Name, anchoredStyleEx, anchoredPdata, Number(existing.Diagram_ID));
575
596
  }
576
597
  stats[changed ? 'updated' : 'skipped'].diagrams++;
577
598
  } else {
@@ -582,7 +603,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
582
603
  const diagAliasToId = new Map();
583
604
  if (!o.dryRun) {
584
605
  if (newDiags.length > 0) {
585
- insertMany(db, 't_diagram', ['Name', 'Diagram_Type', 'Package_ID', 'ParentID', 'StyleEx', 'ea_guid'], newDiags);
606
+ insertMany(db, 't_diagram', ['Name', 'Diagram_Type', 'Package_ID', 'ParentID', 'StyleEx', 'PDATA', 'ea_guid'], newDiags);
586
607
  }
587
608
  for (let i = 0; i < newDiags.length; i += 200) {
588
609
  const part = newDiags.slice(i, i + 200);
@@ -608,18 +629,43 @@ function syncGraphToQea(graph, qeaPath, opts) {
608
629
  if (view && view.view_id !== undefined && view.view_id !== null) { upsertMeta(db, 'view', view.view_id, view); }
609
630
  }
610
631
  }
611
- // memberships: only INSERT missing; never touch existing geometry
632
+ // memberships: insert missing members, and prune stale schema-anchored members
633
+ // whose element/relationship is no longer in the view (authoritative canonical
634
+ // membership reconcile). Still-in-view members keep their geometry untouched;
635
+ // human-drawn shapes (no schema anchor) are never pruned.
636
+ const canonicalElemObjIds = new Set(
637
+ [...elemIdByAliasAll.values()].filter((v) => v >= 0).map((v) => Number(v))
638
+ );
639
+ const canonicalRelCids = new Set(
640
+ [...relAliasToId.values()].filter((v) => v >= 0).map((v) => Number(v))
641
+ );
612
642
  for (const view of graph.views || []) {
613
643
  if (!view || view.view_id === undefined || view.view_id === null) { continue; }
614
644
  const viewId = String(view.view_id);
615
645
  const diagramId = diagIdForView(viewId);
616
646
  if (diagramId === null) { continue; }
647
+ const incl = view.included_elements || [];
617
648
  const placedObjs = new Set();
618
649
  const objs = db.prepare('SELECT Object_ID FROM t_diagramobjects WHERE Diagram_ID=?').all(diagramId);
619
650
  for (const r of objs) { placedObjs.add(Number(r.Object_ID)); }
651
+
652
+ if (pruneMembership) {
653
+ const wantedObjs = new Set();
654
+ for (const elId of incl) {
655
+ const oid = elemIdByAliasAll.get(String(elId));
656
+ if (oid !== undefined) { wantedObjs.add(Number(oid)); }
657
+ }
658
+ const stale = [...placedObjs].filter((oid) => !wantedObjs.has(oid) && canonicalElemObjIds.has(oid));
659
+ if (stale.length > 0 && !o.dryRun) {
660
+ const del = db.prepare('DELETE FROM t_diagramobjects WHERE Diagram_ID=? AND Object_ID=?');
661
+ for (const oid of stale) { del.run(diagramId, oid); }
662
+ }
663
+ stats.removed.diagramObjects += stale.length;
664
+ for (const oid of stale) { placedObjs.delete(oid); }
665
+ }
666
+
620
667
  const nextSeq = objs.length;
621
668
  const newObjs = [];
622
- const incl = view.included_elements || [];
623
669
  let seq = nextSeq;
624
670
  for (const elId of incl) {
625
671
  const oid = elemIdByAliasAll.get(String(elId));
@@ -640,11 +686,28 @@ function syncGraphToQea(graph, qeaPath, opts) {
640
686
  }
641
687
  stats.added.diagramObjects += newObjs.length;
642
688
 
689
+ const relIncl = view.included_relationships || [];
643
690
  const placedLinks = new Set();
644
691
  const links = db.prepare('SELECT ConnectorID FROM t_diagramlinks WHERE DiagramID=?').all(diagramId);
645
692
  for (const r of links) { placedLinks.add(Number(r.ConnectorID)); }
693
+
694
+ if (pruneMembership) {
695
+ const wantedRelCids = new Set();
696
+ for (const relId of relIncl) {
697
+ const cid = relAliasToId.get(String(relId));
698
+ if (cid !== undefined && cid >= 0) { wantedRelCids.add(Number(cid)); }
699
+ }
700
+ const staleLinks = [...placedLinks].filter((cid) => !wantedRelCids.has(cid) && canonicalRelCids.has(cid));
701
+ if (staleLinks.length > 0 && !o.dryRun) {
702
+ const del = db.prepare('DELETE FROM t_diagramlinks WHERE DiagramID=? AND ConnectorID=?');
703
+ for (const cid of staleLinks) { del.run(diagramId, cid); }
704
+ }
705
+ stats.removed.diagramLinks += staleLinks.length;
706
+ for (const cid of staleLinks) { placedLinks.delete(cid); }
707
+ }
708
+
646
709
  const newLinks = [];
647
- for (const relId of view.included_relationships || []) {
710
+ for (const relId of relIncl) {
648
711
  const cid = relAliasToId.get(String(relId));
649
712
  if (cid === undefined || cid < 0) { continue; }
650
713
  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.1",
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": {