@datasynx/agentic-ai-cartography 0.3.3 → 0.5.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/dist/index.js CHANGED
@@ -27,9 +27,21 @@ CREATE TABLE IF NOT EXISTS nodes (
27
27
  confidence REAL DEFAULT 0.5,
28
28
  metadata TEXT NOT NULL DEFAULT '{}',
29
29
  tags TEXT NOT NULL DEFAULT '[]',
30
+ domain TEXT,
31
+ sub_domain TEXT,
32
+ quality_score REAL,
30
33
  PRIMARY KEY (id, session_id)
31
34
  );
32
35
 
36
+ CREATE TABLE IF NOT EXISTS connections (
37
+ id TEXT PRIMARY KEY,
38
+ session_id TEXT NOT NULL REFERENCES sessions(id),
39
+ source_asset_id TEXT NOT NULL,
40
+ target_asset_id TEXT NOT NULL,
41
+ type TEXT,
42
+ created_at TEXT NOT NULL
43
+ );
44
+
33
45
  CREATE TABLE IF NOT EXISTS edges (
34
46
  id TEXT PRIMARY KEY,
35
47
  session_id TEXT NOT NULL REFERENCES sessions(id),
@@ -104,6 +116,7 @@ CREATE INDEX IF NOT EXISTS idx_edges_session ON edges(session_id);
104
116
  CREATE INDEX IF NOT EXISTS idx_events_session ON activity_events(session_id);
105
117
  CREATE INDEX IF NOT EXISTS idx_events_task ON activity_events(task_id);
106
118
  CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_id);
119
+ CREATE INDEX IF NOT EXISTS idx_connections_session ON connections(session_id);
107
120
  `;
108
121
  var CartographyDB = class {
109
122
  db;
@@ -119,7 +132,24 @@ var CartographyDB = class {
119
132
  const version = this.db.pragma("user_version", { simple: true });
120
133
  if (version === 0) {
121
134
  this.db.exec(SCHEMA);
122
- this.db.pragma("user_version = 1");
135
+ this.db.pragma("user_version = 2");
136
+ } else if (version === 1) {
137
+ const cols = this.db.prepare("PRAGMA table_info(nodes)").all().map((c) => c.name);
138
+ if (!cols.includes("domain")) this.db.exec("ALTER TABLE nodes ADD COLUMN domain TEXT");
139
+ if (!cols.includes("sub_domain")) this.db.exec("ALTER TABLE nodes ADD COLUMN sub_domain TEXT");
140
+ if (!cols.includes("quality_score")) this.db.exec("ALTER TABLE nodes ADD COLUMN quality_score REAL");
141
+ this.db.exec(`
142
+ CREATE TABLE IF NOT EXISTS connections (
143
+ id TEXT PRIMARY KEY,
144
+ session_id TEXT NOT NULL REFERENCES sessions(id),
145
+ source_asset_id TEXT NOT NULL,
146
+ target_asset_id TEXT NOT NULL,
147
+ type TEXT,
148
+ created_at TEXT NOT NULL
149
+ );
150
+ CREATE INDEX IF NOT EXISTS idx_connections_session ON connections(session_id);
151
+ `);
152
+ this.db.pragma("user_version = 2");
123
153
  }
124
154
  }
125
155
  close() {
@@ -162,8 +192,9 @@ var CartographyDB = class {
162
192
  upsertNode(sessionId, node, depth = 0) {
163
193
  this.db.prepare(`
164
194
  INSERT OR REPLACE INTO nodes
165
- (id, session_id, type, name, discovered_via, discovered_at, depth, confidence, metadata, tags)
166
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
195
+ (id, session_id, type, name, discovered_via, discovered_at, depth, confidence, metadata, tags,
196
+ domain, sub_domain, quality_score)
197
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
167
198
  `).run(
168
199
  node.id,
169
200
  sessionId,
@@ -174,12 +205,18 @@ var CartographyDB = class {
174
205
  depth,
175
206
  node.confidence,
176
207
  JSON.stringify(node.metadata ?? {}),
177
- JSON.stringify(node.tags ?? [])
208
+ JSON.stringify(node.tags ?? []),
209
+ node.domain ?? null,
210
+ node.subDomain ?? null,
211
+ node.qualityScore ?? null
178
212
  );
179
213
  }
180
214
  getNodes(sessionId) {
181
215
  const rows = this.db.prepare("SELECT * FROM nodes WHERE session_id = ?").all(sessionId);
182
- return rows.map((r) => ({
216
+ return rows.map((r) => this.mapNode(r));
217
+ }
218
+ mapNode(r) {
219
+ return {
183
220
  id: r["id"],
184
221
  sessionId: r["session_id"],
185
222
  type: r["type"],
@@ -190,8 +227,11 @@ var CartographyDB = class {
190
227
  confidence: r["confidence"],
191
228
  metadata: JSON.parse(r["metadata"]),
192
229
  tags: JSON.parse(r["tags"]),
193
- pathId: r["path_id"]
194
- }));
230
+ pathId: r["path_id"],
231
+ domain: r["domain"] ?? void 0,
232
+ subDomain: r["sub_domain"] ?? void 0,
233
+ qualityScore: r["quality_score"] ?? void 0
234
+ };
195
235
  }
196
236
  deleteNode(sessionId, nodeId) {
197
237
  this.db.prepare("DELETE FROM nodes WHERE session_id = ? AND id = ?").run(sessionId, nodeId);
@@ -403,6 +443,33 @@ var CartographyDB = class {
403
443
  generatedAt: r["generated_at"]
404
444
  }));
405
445
  }
446
+ // ── Connections (user-created hex map links) ─────────────────────────────
447
+ upsertConnection(sessionId, conn) {
448
+ const existing = this.db.prepare(
449
+ "SELECT id FROM connections WHERE session_id = ? AND source_asset_id = ? AND target_asset_id = ?"
450
+ ).get(sessionId, conn.sourceAssetId, conn.targetAssetId);
451
+ if (existing) return existing.id;
452
+ const id = crypto.randomUUID();
453
+ this.db.prepare(`
454
+ INSERT INTO connections (id, session_id, source_asset_id, target_asset_id, type, created_at)
455
+ VALUES (?, ?, ?, ?, ?, ?)
456
+ `).run(id, sessionId, conn.sourceAssetId, conn.targetAssetId, conn.type ?? null, (/* @__PURE__ */ new Date()).toISOString());
457
+ return id;
458
+ }
459
+ getConnections(sessionId) {
460
+ const rows = this.db.prepare("SELECT * FROM connections WHERE session_id = ?").all(sessionId);
461
+ return rows.map((r) => ({
462
+ id: r["id"],
463
+ sessionId: r["session_id"],
464
+ sourceAssetId: r["source_asset_id"],
465
+ targetAssetId: r["target_asset_id"],
466
+ type: r["type"] ?? void 0,
467
+ createdAt: r["created_at"]
468
+ }));
469
+ }
470
+ deleteConnection(sessionId, connectionId) {
471
+ this.db.prepare("DELETE FROM connections WHERE session_id = ? AND id = ?").run(sessionId, connectionId);
472
+ }
406
473
  // ── Approvals ───────────────────────────
407
474
  setApproval(pattern, action) {
408
475
  this.db.prepare(`
@@ -461,7 +528,10 @@ var NodeSchema = z.object({
461
528
  discoveredVia: z.string(),
462
529
  confidence: z.number().min(0).max(1).default(0.5),
463
530
  metadata: z.record(z.unknown()).default({}),
464
- tags: z.array(z.string()).default([])
531
+ tags: z.array(z.string()).default([]),
532
+ domain: z.string().optional().describe('Business domain, e.g. "Marketing", "Finance"'),
533
+ subDomain: z.string().optional().describe('Sub-domain, e.g. "Forecast client orders"'),
534
+ qualityScore: z.number().min(0).max(100).optional().describe("Data quality score 0\u2013100")
465
535
  });
466
536
  var EdgeSchema = z.object({
467
537
  sourceId: z.string(),
@@ -486,6 +556,63 @@ var SOPSchema = z.object({
486
556
  frequency: z.string(),
487
557
  confidence: z.number().min(0).max(1)
488
558
  });
559
+ var DataAssetSchema = z.object({
560
+ id: z.string(),
561
+ name: z.string(),
562
+ domain: z.string(),
563
+ subDomain: z.string().optional(),
564
+ qualityScore: z.number().min(0).max(100).optional(),
565
+ metadata: z.record(z.unknown()).default({}),
566
+ position: z.object({ q: z.number(), r: z.number() })
567
+ });
568
+ var ClusterSchema = z.object({
569
+ id: z.string(),
570
+ label: z.string(),
571
+ domain: z.string(),
572
+ color: z.string(),
573
+ assetIds: z.array(z.string()),
574
+ centroid: z.object({ x: z.number(), y: z.number() })
575
+ });
576
+ var ConnectionSchema = z.object({
577
+ id: z.string(),
578
+ sourceAssetId: z.string(),
579
+ targetAssetId: z.string(),
580
+ type: z.string().optional()
581
+ });
582
+ var DOMAIN_COLORS = {
583
+ "Quality Control": "#1a2744",
584
+ "Supply Chain": "#1e3a6e",
585
+ "Marketing": "#6a7fb5",
586
+ "Finance": "#3a8a8a",
587
+ "HR": "#2a5a9a",
588
+ "Logistics": "#0e7490",
589
+ "Sales": "#1d4ed8",
590
+ "Engineering": "#4338ca",
591
+ "Operations": "#0891b2",
592
+ "Data Layer": "#1e3352",
593
+ "Web / API": "#1a3a1a",
594
+ "Messaging": "#2a1a3a",
595
+ "Infrastructure": "#0f2a40",
596
+ "Other": "#374151"
597
+ };
598
+ var DOMAIN_PALETTE = [
599
+ "#1a2e5a",
600
+ "#1e3a8a",
601
+ "#1d4ed8",
602
+ "#2563eb",
603
+ "#3b82f6",
604
+ "#6366f1",
605
+ "#818cf8",
606
+ "#7c9fc3",
607
+ "#0e7490",
608
+ "#0891b2",
609
+ "#06b6d4",
610
+ "#22d3ee",
611
+ "#0d9488",
612
+ "#14b8a6",
613
+ "#2dd4bf",
614
+ "#5eead4"
615
+ ];
489
616
  function defaultConfig(overrides = {}) {
490
617
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
491
618
  return {
@@ -770,7 +897,10 @@ async function createCartographyTools(db, sessionId, opts = {}) {
770
897
  discoveredVia: z2.string(),
771
898
  confidence: z2.number().min(0).max(1),
772
899
  metadata: z2.record(z2.unknown()).optional(),
773
- tags: z2.array(z2.string()).optional()
900
+ tags: z2.array(z2.string()).optional(),
901
+ domain: z2.string().optional().describe('Business domain, e.g. "Marketing", "Finance"'),
902
+ subDomain: z2.string().optional().describe('Sub-domain, e.g. "Forecast client orders"'),
903
+ qualityScore: z2.number().min(0).max(100).optional().describe("Data quality score 0\u2013100")
774
904
  }, async (args) => {
775
905
  const node = {
776
906
  id: stripSensitive(args["id"]),
@@ -779,7 +909,10 @@ async function createCartographyTools(db, sessionId, opts = {}) {
779
909
  discoveredVia: args["discoveredVia"],
780
910
  confidence: args["confidence"],
781
911
  metadata: args["metadata"] ?? {},
782
- tags: args["tags"] ?? []
912
+ tags: args["tags"] ?? [],
913
+ domain: args["domain"],
914
+ subDomain: args["subDomain"],
915
+ qualityScore: args["qualityScore"]
783
916
  };
784
917
  db.upsertNode(sessionId, node);
785
918
  return { content: [{ type: "text", text: `\u2713 Node: ${node.id}` }] };
@@ -1411,6 +1544,274 @@ Use ask_user when you need context from the user.`;
1411
1544
  // src/exporter.ts
1412
1545
  import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
1413
1546
  import { join as join2 } from "path";
1547
+
1548
+ // src/hex.ts
1549
+ function hexToPixel(q, r, size) {
1550
+ const x = size * (3 / 2 * q);
1551
+ const y = size * (Math.sqrt(3) / 2 * q + Math.sqrt(3) * r);
1552
+ return { x, y };
1553
+ }
1554
+ function pixelToHex(x, y, size) {
1555
+ const q = 2 / 3 * x / size;
1556
+ const r = (-1 / 3 * x + Math.sqrt(3) / 3 * y) / size;
1557
+ return hexRound(q, r);
1558
+ }
1559
+ function hexRound(q, r) {
1560
+ const s = -q - r;
1561
+ let rq = Math.round(q);
1562
+ let rr = Math.round(r);
1563
+ const rs = Math.round(s);
1564
+ const dq = Math.abs(rq - q);
1565
+ const dr = Math.abs(rr - r);
1566
+ const ds = Math.abs(rs - s);
1567
+ if (dq > dr && dq > ds) {
1568
+ rq = -rr - rs;
1569
+ } else if (dr > ds) {
1570
+ rr = -rq - rs;
1571
+ }
1572
+ return { q: rq || 0, r: rr || 0 };
1573
+ }
1574
+ function hexCorners(cx, cy, size) {
1575
+ const corners = [];
1576
+ for (let i = 0; i < 6; i++) {
1577
+ const angle = Math.PI / 180 * (60 * i);
1578
+ corners.push({
1579
+ x: cx + size * Math.cos(angle),
1580
+ y: cy + size * Math.sin(angle)
1581
+ });
1582
+ }
1583
+ return corners;
1584
+ }
1585
+ var HEX_DIRECTIONS = [
1586
+ { q: 1, r: 0 },
1587
+ { q: 1, r: -1 },
1588
+ { q: 0, r: -1 },
1589
+ { q: -1, r: 0 },
1590
+ { q: -1, r: 1 },
1591
+ { q: 0, r: 1 }
1592
+ ];
1593
+ function hexNeighbors(q, r) {
1594
+ return HEX_DIRECTIONS.map((d) => ({ q: q + d.q, r: r + d.r }));
1595
+ }
1596
+ function hexDistance(a, b) {
1597
+ return (Math.abs(a.q - b.q) + Math.abs(a.q + a.r - b.q - b.r) + Math.abs(a.r - b.r)) / 2;
1598
+ }
1599
+ function hexRing(center, radius) {
1600
+ if (radius === 0) return [{ q: center.q, r: center.r }];
1601
+ const results = [];
1602
+ let hex = {
1603
+ q: center.q + HEX_DIRECTIONS[4].q * radius,
1604
+ r: center.r + HEX_DIRECTIONS[4].r * radius
1605
+ };
1606
+ for (let side = 0; side < 6; side++) {
1607
+ for (let step = 0; step < radius; step++) {
1608
+ results.push({ q: hex.q, r: hex.r });
1609
+ hex = {
1610
+ q: hex.q + HEX_DIRECTIONS[side].q,
1611
+ r: hex.r + HEX_DIRECTIONS[side].r
1612
+ };
1613
+ }
1614
+ }
1615
+ return results;
1616
+ }
1617
+ function hexSpiral(center, count) {
1618
+ const positions = [];
1619
+ let ring = 0;
1620
+ while (positions.length < count) {
1621
+ const ringPositions = hexRing(center, ring);
1622
+ for (const pos of ringPositions) {
1623
+ if (positions.length >= count) break;
1624
+ positions.push(pos);
1625
+ }
1626
+ ring++;
1627
+ }
1628
+ return positions;
1629
+ }
1630
+
1631
+ // src/cluster.ts
1632
+ function assignColor(domain, allDomains) {
1633
+ if (DOMAIN_COLORS[domain]) return DOMAIN_COLORS[domain];
1634
+ const idx = allDomains.indexOf(domain);
1635
+ return DOMAIN_PALETTE[idx % DOMAIN_PALETTE.length];
1636
+ }
1637
+ function assignColors(domains) {
1638
+ const result = {};
1639
+ for (const d of domains) {
1640
+ result[d] = assignColor(d, domains);
1641
+ }
1642
+ return result;
1643
+ }
1644
+ function shadeVariant(hex, amount) {
1645
+ const num = parseInt(hex.replace("#", ""), 16);
1646
+ const r = Math.min(255, (num >> 16) + amount);
1647
+ const g = Math.min(255, (num >> 8 & 255) + amount);
1648
+ const b = Math.min(255, (num & 255) + amount);
1649
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
1650
+ }
1651
+ function groupByDomain(assets) {
1652
+ const map = /* @__PURE__ */ new Map();
1653
+ for (const a of assets) {
1654
+ const d = a.domain || "Other";
1655
+ if (!map.has(d)) map.set(d, []);
1656
+ map.get(d).push(a);
1657
+ }
1658
+ return map;
1659
+ }
1660
+ var CLUSTER_GAP = 3;
1661
+ function layoutClusters(groups, hexSize) {
1662
+ const allDomains = Array.from(groups.keys());
1663
+ const colors = assignColors(allDomains);
1664
+ const sorted = Array.from(groups.entries()).sort((a, b) => b[1].length - a[1].length);
1665
+ const occupied = /* @__PURE__ */ new Set();
1666
+ const key = (q, r) => `${q},${r}`;
1667
+ const clusters = [];
1668
+ const allAssets = [];
1669
+ for (const [domain, domainAssets] of sorted) {
1670
+ const origin = clusters.length === 0 ? { q: 0, r: 0 } : findFreeOrigin(occupied, domainAssets.length, CLUSTER_GAP);
1671
+ const positions = hexSpiral(origin, domainAssets.length);
1672
+ const assetIds = [];
1673
+ for (let i = 0; i < domainAssets.length; i++) {
1674
+ const asset = domainAssets[i];
1675
+ asset.position = positions[i];
1676
+ assetIds.push(asset.id);
1677
+ occupied.add(key(positions[i].q, positions[i].r));
1678
+ allAssets.push(asset);
1679
+ }
1680
+ const centroid = computeCentroid(positions, hexSize);
1681
+ clusters.push({
1682
+ id: `cluster:${domain}`,
1683
+ label: domain,
1684
+ domain,
1685
+ color: colors[domain],
1686
+ assetIds,
1687
+ centroid
1688
+ });
1689
+ }
1690
+ return { clusters, assets: allAssets };
1691
+ }
1692
+ function findFreeOrigin(occupied, count, gap) {
1693
+ const key = (q, r) => `${q},${r}`;
1694
+ for (let searchRadius = 1; searchRadius < 100; searchRadius++) {
1695
+ const candidates = hexSpiral({ q: 0, r: 0 }, 1 + 6 * searchRadius * (searchRadius + 1) / 2);
1696
+ for (const candidate of candidates) {
1697
+ const testPositions = hexSpiral(candidate, count);
1698
+ let fits = true;
1699
+ for (const tp of testPositions) {
1700
+ if (occupied.has(key(tp.q, tp.r))) {
1701
+ fits = false;
1702
+ break;
1703
+ }
1704
+ for (const oKey of occupied) {
1705
+ const [oq, or] = oKey.split(",").map(Number);
1706
+ if (hexDistance(tp, { q: oq, r: or }) < gap) {
1707
+ fits = false;
1708
+ break;
1709
+ }
1710
+ }
1711
+ if (!fits) break;
1712
+ }
1713
+ if (fits) return candidate;
1714
+ }
1715
+ }
1716
+ return { q: occupied.size * 5, r: 0 };
1717
+ }
1718
+ function computeCentroid(positions, hexSize) {
1719
+ if (positions.length === 0) return { x: 0, y: 0 };
1720
+ let sx = 0, sy = 0;
1721
+ for (const { q, r } of positions) {
1722
+ const { x, y } = hexToPixel(q, r, hexSize);
1723
+ sx += x;
1724
+ sy += y;
1725
+ }
1726
+ return { x: sx / positions.length, y: sy / positions.length };
1727
+ }
1728
+ function computeClusterBounds(assets, hexSize) {
1729
+ if (assets.length === 0) return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
1730
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1731
+ for (const a of assets) {
1732
+ const { x, y } = hexToPixel(a.position.q, a.position.r, hexSize);
1733
+ if (x < minX) minX = x;
1734
+ if (y < minY) minY = y;
1735
+ if (x > maxX) maxX = x;
1736
+ if (y > maxY) maxY = y;
1737
+ }
1738
+ return { minX, minY, maxX, maxY };
1739
+ }
1740
+
1741
+ // src/mapper.ts
1742
+ var TYPE_TO_DOMAIN = {
1743
+ database_server: "Data Layer",
1744
+ database: "Data Layer",
1745
+ table: "Data Layer",
1746
+ cache_server: "Data Layer",
1747
+ web_service: "Web / API",
1748
+ api_endpoint: "Web / API",
1749
+ message_broker: "Messaging",
1750
+ queue: "Messaging",
1751
+ topic: "Messaging",
1752
+ host: "Infrastructure",
1753
+ container: "Infrastructure",
1754
+ pod: "Infrastructure",
1755
+ k8s_cluster: "Infrastructure",
1756
+ config_file: "Infrastructure",
1757
+ saas_tool: "SaaS Tools"
1758
+ };
1759
+ function resolveDomain(node) {
1760
+ if (node.domain) return node.domain;
1761
+ const meta = node.metadata;
1762
+ if (typeof meta["category"] === "string" && meta["category"].length > 0) {
1763
+ return meta["category"];
1764
+ }
1765
+ for (const tag of node.tags ?? []) {
1766
+ if (tag.length > 2 && tag[0] === tag[0].toUpperCase()) {
1767
+ return tag;
1768
+ }
1769
+ }
1770
+ return TYPE_TO_DOMAIN[node.type] ?? "Other";
1771
+ }
1772
+ function nodesToAssets(nodes) {
1773
+ return nodes.map((n) => ({
1774
+ id: n.id,
1775
+ name: n.name,
1776
+ domain: resolveDomain(n),
1777
+ subDomain: n.subDomain,
1778
+ qualityScore: n.qualityScore ?? Math.round(n.confidence * 100),
1779
+ metadata: n.metadata ?? {},
1780
+ position: { q: 0, r: 0 }
1781
+ // will be assigned by layoutClusters
1782
+ }));
1783
+ }
1784
+ function edgesToConnections(edges) {
1785
+ return edges.map((e) => ({
1786
+ id: e.id,
1787
+ sourceAssetId: e.sourceId,
1788
+ targetAssetId: e.targetId,
1789
+ type: e.relationship
1790
+ }));
1791
+ }
1792
+ var HEX_SIZE = 24;
1793
+ function buildMapData(nodes, edges, options) {
1794
+ const rawAssets = nodesToAssets(nodes);
1795
+ const connections = edgesToConnections(edges);
1796
+ if (rawAssets.length === 0) {
1797
+ return {
1798
+ assets: [],
1799
+ clusters: [],
1800
+ connections,
1801
+ meta: { exportedAt: (/* @__PURE__ */ new Date()).toISOString(), theme: options?.theme ?? "light" }
1802
+ };
1803
+ }
1804
+ const groups = groupByDomain(rawAssets);
1805
+ const { clusters, assets } = layoutClusters(groups, HEX_SIZE);
1806
+ return {
1807
+ assets,
1808
+ clusters,
1809
+ connections,
1810
+ meta: { exportedAt: (/* @__PURE__ */ new Date()).toISOString(), theme: options?.theme ?? "light" }
1811
+ };
1812
+ }
1813
+
1814
+ // src/exporter.ts
1414
1815
  function nodeLayer(type) {
1415
1816
  if (type === "saas_tool") return "saas";
1416
1817
  if (["web_service", "api_endpoint"].includes(type)) return "web";
@@ -2345,7 +2746,647 @@ function toggle(i) {
2345
2746
  </body>
2346
2747
  </html>`;
2347
2748
  }
2348
- function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml", "html", "sops"]) {
2749
+ function exportCartographyMap(nodes, edges, options) {
2750
+ const mapData = buildMapData(nodes, edges, options);
2751
+ const { assets, clusters, connections, meta } = mapData;
2752
+ const isEmpty = assets.length === 0;
2753
+ const HEX_SIZE2 = 24;
2754
+ const dataJson = JSON.stringify({
2755
+ assets: assets.map((a) => ({
2756
+ id: a.id,
2757
+ name: a.name,
2758
+ domain: a.domain,
2759
+ subDomain: a.subDomain ?? null,
2760
+ qualityScore: a.qualityScore ?? null,
2761
+ metadata: a.metadata,
2762
+ q: a.position.q,
2763
+ r: a.position.r
2764
+ })),
2765
+ clusters: clusters.map((c) => ({
2766
+ id: c.id,
2767
+ label: c.label,
2768
+ domain: c.domain,
2769
+ color: c.color,
2770
+ assetIds: c.assetIds,
2771
+ centroid: c.centroid
2772
+ })),
2773
+ connections: connections.map((c) => ({
2774
+ id: c.id,
2775
+ sourceAssetId: c.sourceAssetId,
2776
+ targetAssetId: c.targetAssetId,
2777
+ type: c.type ?? "connection"
2778
+ }))
2779
+ });
2780
+ return `<!DOCTYPE html>
2781
+ <html lang="en">
2782
+ <head>
2783
+ <meta charset="UTF-8"/>
2784
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
2785
+ <title>Data Cartography Map</title>
2786
+ <style>
2787
+ *{box-sizing:border-box;margin:0;padding:0}
2788
+ html,body{width:100%;height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
2789
+ body{display:flex;flex-direction:column;background:${meta.theme === "dark" ? "#0f172a" : "#f8fafc"};color:${meta.theme === "dark" ? "#e2e8f0" : "#1e293b"}}
2790
+ #topbar{
2791
+ height:48px;display:flex;align-items:center;gap:16px;padding:0 20px;
2792
+ background:${meta.theme === "dark" ? "#1e293b" : "#fff"};border-bottom:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};z-index:10;flex-shrink:0;
2793
+ }
2794
+ #topbar h1{font-size:15px;font-weight:600;letter-spacing:-0.01em}
2795
+ #search-box{
2796
+ display:flex;align-items:center;gap:8px;background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"};
2797
+ border-radius:8px;padding:5px 10px;margin-left:auto;
2798
+ }
2799
+ #search-box input{
2800
+ border:none;background:transparent;font-size:13px;outline:none;width:180px;color:inherit;
2801
+ }
2802
+ #search-box input::placeholder{color:#94a3b8}
2803
+ #main{flex:1;display:flex;overflow:hidden;position:relative}
2804
+ #canvas-wrap{flex:1;position:relative;overflow:hidden;cursor:grab}
2805
+ #canvas-wrap.dragging{cursor:grabbing}
2806
+ #canvas-wrap.connecting{cursor:crosshair}
2807
+ canvas{display:block;width:100%;height:100%}
2808
+ /* Detail panel */
2809
+ #detail-panel{
2810
+ width:280px;background:${meta.theme === "dark" ? "#1e293b" : "#fff"};border-left:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
2811
+ display:flex;flex-direction:column;transform:translateX(100%);
2812
+ transition:transform .2s ease;z-index:5;flex-shrink:0;overflow-y:auto;
2813
+ }
2814
+ #detail-panel.open{transform:translateX(0)}
2815
+ #detail-panel .panel-header{
2816
+ padding:16px;border-bottom:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};display:flex;align-items:center;gap:10px;
2817
+ }
2818
+ #detail-panel .panel-header h3{font-size:14px;font-weight:600;flex:1;word-break:break-word}
2819
+ #detail-panel .close-btn{
2820
+ width:24px;height:24px;border:none;background:transparent;cursor:pointer;
2821
+ color:#94a3b8;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:16px;
2822
+ }
2823
+ #detail-panel .close-btn:hover{background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"}}
2824
+ #detail-panel .panel-body{padding:12px 16px;display:flex;flex-direction:column;gap:12px}
2825
+ #detail-panel .meta-row{display:flex;flex-direction:column;gap:3px}
2826
+ #detail-panel .meta-label{font-size:11px;font-weight:500;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em}
2827
+ #detail-panel .meta-value{font-size:13px;word-break:break-all}
2828
+ #detail-panel .quality-bar{height:6px;border-radius:3px;background:${meta.theme === "dark" ? "#334155" : "#e2e8f0"};margin-top:4px}
2829
+ #detail-panel .quality-fill{height:6px;border-radius:3px;transition:width .3s}
2830
+ /* Bottom-left toolbar */
2831
+ #toolbar-left{
2832
+ position:absolute;bottom:20px;left:20px;display:flex;gap:8px;z-index:10;
2833
+ }
2834
+ .tb-btn{
2835
+ width:40px;height:40px;border-radius:10px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
2836
+ background:${meta.theme === "dark" ? "#1e293b" : "#fff"};box-shadow:0 1px 4px rgba(0,0,0,.08);cursor:pointer;
2837
+ display:flex;align-items:center;justify-content:center;font-size:18px;
2838
+ transition:all .15s;color:inherit;
2839
+ }
2840
+ .tb-btn:hover{border-color:#94a3b8}
2841
+ .tb-btn.active{background:${meta.theme === "dark" ? "#1e3a5f" : "#eff6ff"};border-color:#3b82f6}
2842
+ /* Bottom-right toolbar */
2843
+ #toolbar-right{
2844
+ position:absolute;bottom:20px;right:20px;display:flex;flex-direction:column;
2845
+ align-items:flex-end;gap:8px;z-index:10;
2846
+ }
2847
+ #zoom-controls{display:flex;align-items:center;gap:6px}
2848
+ .zoom-btn{
2849
+ width:34px;height:34px;border-radius:8px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
2850
+ background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
2851
+ font-size:18px;color:inherit;display:flex;align-items:center;justify-content:center;
2852
+ }
2853
+ .zoom-btn:hover{background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"}}
2854
+ #zoom-pct{font-size:12px;font-weight:500;color:#64748b;min-width:38px;text-align:center}
2855
+ #detail-selector{display:flex;flex-direction:column;gap:4px}
2856
+ .detail-btn{
2857
+ width:34px;height:34px;border-radius:8px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
2858
+ background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
2859
+ font-size:12px;font-weight:600;color:#64748b;display:flex;align-items:center;justify-content:center;
2860
+ }
2861
+ .detail-btn:hover{background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"}}
2862
+ .detail-btn.active{background:${meta.theme === "dark" ? "#1e3a5f" : "#eff6ff"};border-color:#3b82f6;color:#2563eb}
2863
+ #connect-btn{
2864
+ width:40px;height:40px;border-radius:10px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
2865
+ background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
2866
+ font-size:18px;display:flex;align-items:center;justify-content:center;color:inherit;
2867
+ }
2868
+ #connect-btn.active{background:#fef3c7;border-color:#f59e0b}
2869
+ /* Tooltip */
2870
+ #tooltip{
2871
+ position:fixed;background:#1e293b;color:#fff;border-radius:8px;
2872
+ padding:8px 12px;font-size:12px;pointer-events:none;z-index:100;
2873
+ display:none;max-width:220px;box-shadow:0 4px 12px rgba(0,0,0,.15);
2874
+ }
2875
+ #tooltip .tt-name{font-weight:600;margin-bottom:2px}
2876
+ #tooltip .tt-domain{color:#94a3b8;font-size:11px}
2877
+ #tooltip .tt-quality{font-size:11px;margin-top:2px}
2878
+ /* Empty state */
2879
+ #empty-state{
2880
+ position:absolute;inset:0;display:flex;flex-direction:column;
2881
+ align-items:center;justify-content:center;gap:12px;color:#94a3b8;
2882
+ }
2883
+ #empty-state p{font-size:14px}
2884
+ /* Theme toggle */
2885
+ #theme-btn{
2886
+ width:40px;height:40px;border-radius:10px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
2887
+ background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
2888
+ font-size:18px;display:flex;align-items:center;justify-content:center;color:inherit;
2889
+ }
2890
+ /* Dark mode overrides (toggled via JS) */
2891
+ body.dark{background:#0f172a;color:#e2e8f0}
2892
+ body.dark #topbar{background:#1e293b;border-color:#334155}
2893
+ body.dark #search-box{background:#334155}
2894
+ body.dark #detail-panel{background:#1e293b;border-color:#334155}
2895
+ body.dark .tb-btn,body.dark .zoom-btn,body.dark .detail-btn,body.dark #connect-btn,body.dark #theme-btn{
2896
+ background:#1e293b;border-color:#334155;color:#e2e8f0;
2897
+ }
2898
+ /* Light mode overrides */
2899
+ body.light{background:#f8fafc;color:#1e293b}
2900
+ body.light #topbar{background:#fff;border-color:#e2e8f0}
2901
+ /* Connection hint */
2902
+ #connect-hint{
2903
+ position:absolute;top:12px;left:50%;transform:translateX(-50%);
2904
+ background:#fef3c7;border:1px solid #f59e0b;color:#92400e;
2905
+ padding:6px 14px;border-radius:20px;font-size:12px;font-weight:500;
2906
+ display:none;z-index:20;pointer-events:none;
2907
+ }
2908
+ /* Screen reader only */
2909
+ .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}
2910
+ </style>
2911
+ </head>
2912
+ <body class="${meta.theme}">
2913
+ <!-- Top bar -->
2914
+ <div id="topbar">
2915
+ <h1>Data Cartography Map</h1>
2916
+ <div id="search-box">
2917
+ <span style="color:#94a3b8;font-size:14px">&#8981;</span>
2918
+ <input id="search-input" type="text" placeholder="Search assets..." aria-label="Search data assets"/>
2919
+ </div>
2920
+ <button id="theme-btn" title="Toggle dark/light mode" aria-label="Toggle theme">${meta.theme === "dark" ? "&#9788;" : "&#9790;"}</button>
2921
+ </div>
2922
+ <!-- SR summary -->
2923
+ <div class="sr-only" role="status" aria-live="polite" id="sr-summary">
2924
+ Data cartography map with ${assets.length} assets in ${clusters.length} clusters.
2925
+ </div>
2926
+ <!-- Main area -->
2927
+ <div id="main">
2928
+ <div id="canvas-wrap" role="application" aria-label="Data cartography hex map" tabindex="0">
2929
+ <canvas id="hexmap" aria-hidden="true"></canvas>
2930
+ ${isEmpty ? '<div id="empty-state"><p style="font-size:48px">&#128506;</p><p>No data assets available</p><p style="font-size:12px">Run <code>datasynx-cartography discover</code> to populate the map</p></div>' : ""}
2931
+ </div>
2932
+ <div id="detail-panel" role="complementary" aria-label="Asset details">
2933
+ <div class="panel-header">
2934
+ <h3 id="dp-name">&mdash;</h3>
2935
+ <button class="close-btn" id="dp-close" aria-label="Close panel">&#10005;</button>
2936
+ </div>
2937
+ <div class="panel-body" id="dp-body"></div>
2938
+ </div>
2939
+ </div>
2940
+ <!-- Bottom-left toolbar -->
2941
+ <div id="toolbar-left">
2942
+ <button class="tb-btn active" id="btn-labels" title="Show labels" aria-pressed="true" aria-label="Toggle labels">&#127991;</button>
2943
+ <button class="tb-btn" id="btn-quality" title="Quality layer" aria-pressed="false" aria-label="Toggle quality layer">&#128065;</button>
2944
+ </div>
2945
+ <!-- Bottom-right toolbar -->
2946
+ <div id="toolbar-right">
2947
+ <div id="zoom-controls">
2948
+ <button class="zoom-btn" id="zoom-out" aria-label="Zoom out">&minus;</button>
2949
+ <span id="zoom-pct">100%</span>
2950
+ <button class="zoom-btn" id="zoom-in" aria-label="Zoom in">+</button>
2951
+ </div>
2952
+ <div id="detail-selector">
2953
+ <button class="detail-btn" id="dl-1" aria-label="Detail level 1">1</button>
2954
+ <button class="detail-btn active" id="dl-2" aria-label="Detail level 2">2</button>
2955
+ <button class="detail-btn" id="dl-3" aria-label="Detail level 3">3</button>
2956
+ <button class="detail-btn" id="dl-4" aria-label="Detail level 4">4</button>
2957
+ </div>
2958
+ <button id="connect-btn" title="Connection tool" aria-label="Toggle connection tool">&#128279;</button>
2959
+ </div>
2960
+ <!-- Connection hint -->
2961
+ <div id="connect-hint">Click two assets to create a connection</div>
2962
+ <!-- Tooltip -->
2963
+ <div id="tooltip" role="tooltip">
2964
+ <div class="tt-name" id="tt-name"></div>
2965
+ <div class="tt-domain" id="tt-domain"></div>
2966
+ <div class="tt-quality" id="tt-quality"></div>
2967
+ </div>
2968
+
2969
+ <script>
2970
+ (function() {
2971
+ 'use strict';
2972
+
2973
+ // \u2500\u2500 Data \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2974
+ const MAP = ${dataJson};
2975
+ const HEX_SIZE = ${HEX_SIZE2};
2976
+ const IS_EMPTY = ${isEmpty};
2977
+
2978
+ // Build asset index
2979
+ const assetIndex = new Map();
2980
+ const clusterByAsset = new Map();
2981
+ for (const c of MAP.clusters) {
2982
+ for (const aid of c.assetIds) {
2983
+ clusterByAsset.set(aid, c);
2984
+ }
2985
+ }
2986
+ for (const a of MAP.assets) {
2987
+ assetIndex.set(a.id, a);
2988
+ }
2989
+
2990
+ // \u2500\u2500 Canvas \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2991
+ const canvas = document.getElementById('hexmap');
2992
+ const ctx = canvas.getContext('2d');
2993
+ const wrap = document.getElementById('canvas-wrap');
2994
+ let W = 0, H = 0;
2995
+
2996
+ function resize() {
2997
+ const dpr = window.devicePixelRatio || 1;
2998
+ W = wrap.clientWidth; H = wrap.clientHeight;
2999
+ canvas.width = W * dpr; canvas.height = H * dpr;
3000
+ canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
3001
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
3002
+ draw();
3003
+ }
3004
+ window.addEventListener('resize', resize);
3005
+
3006
+ // \u2500\u2500 Viewport \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3007
+ let vx = 0, vy = 0, scale = 1;
3008
+ let detailLevel = 2, showLabels = true, showQuality = false;
3009
+ let isDark = document.body.classList.contains('dark');
3010
+ let connectMode = false, connectFirst = null;
3011
+ let hoveredAssetId = null, selectedAssetId = null;
3012
+ let searchQuery = '';
3013
+ let localConnections = [...MAP.connections];
3014
+
3015
+ // Flat-top hex math
3016
+ function htp_x(q, r) { return HEX_SIZE * (3/2 * q); }
3017
+ function htp_y(q, r) { return HEX_SIZE * (Math.sqrt(3)/2 * q + Math.sqrt(3) * r); }
3018
+ function w2s(wx, wy) { return { x: wx*scale+vx, y: wy*scale+vy }; }
3019
+ function s2w(sx, sy) { return { x: (sx-vx)/scale, y: (sy-vy)/scale }; }
3020
+
3021
+ function fitToView() {
3022
+ if (IS_EMPTY || MAP.assets.length === 0) { vx = 0; vy = 0; scale = 1; return; }
3023
+ let mnx=Infinity,mny=Infinity,mxx=-Infinity,mxy=-Infinity;
3024
+ for (const a of MAP.assets) {
3025
+ const px=htp_x(a.q,a.r), py=htp_y(a.q,a.r);
3026
+ if(px<mnx)mnx=px;if(py<mny)mny=py;if(px>mxx)mxx=px;if(py>mxy)mxy=py;
3027
+ }
3028
+ const pw=mxx-mnx+HEX_SIZE*4, ph=mxy-mny+HEX_SIZE*4;
3029
+ scale = Math.min(W/pw, H/ph, 2) * 0.85;
3030
+ vx = W/2 - ((mnx+mxx)/2)*scale;
3031
+ vy = H/2 - ((mny+mxy)/2)*scale;
3032
+ }
3033
+
3034
+ // \u2500\u2500 Drawing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3035
+ function hexPath(cx, cy, r) {
3036
+ ctx.beginPath();
3037
+ for (let i=0;i<6;i++) {
3038
+ const angle = Math.PI/180*(60*i);
3039
+ const x=cx+r*Math.cos(angle), y=cy+r*Math.sin(angle);
3040
+ i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);
3041
+ }
3042
+ ctx.closePath();
3043
+ }
3044
+
3045
+ function shadeV(hex, amt) {
3046
+ if(!hex||hex.length<7)return hex;
3047
+ const n=parseInt(hex.replace('#',''),16);
3048
+ const r=Math.min(255,(n>>16)+amt), g=Math.min(255,((n>>8)&0xff)+amt), b=Math.min(255,(n&0xff)+amt);
3049
+ return '#'+r.toString(16).padStart(2,'0')+g.toString(16).padStart(2,'0')+b.toString(16).padStart(2,'0');
3050
+ }
3051
+
3052
+ function draw() {
3053
+ ctx.clearRect(0,0,W,H);
3054
+ ctx.fillStyle = isDark ? '#0f172a' : '#f8fafc';
3055
+ ctx.fillRect(0,0,W,H);
3056
+ if (IS_EMPTY) return;
3057
+
3058
+ const size = HEX_SIZE * scale;
3059
+ const matchedIds = getSearchMatches();
3060
+ const hasSearch = searchQuery.length > 0;
3061
+
3062
+ // Draw connections
3063
+ ctx.save();
3064
+ ctx.strokeStyle = isDark ? 'rgba(148,163,184,0.35)' : 'rgba(100,116,139,0.25)';
3065
+ ctx.lineWidth = 1.5;
3066
+ ctx.setLineDash([4,4]);
3067
+ for (const conn of localConnections) {
3068
+ const src = assetIndex.get(conn.sourceAssetId);
3069
+ const tgt = assetIndex.get(conn.targetAssetId);
3070
+ if (!src||!tgt) continue;
3071
+ const sp=w2s(htp_x(src.q,src.r),htp_y(src.q,src.r));
3072
+ const tp=w2s(htp_x(tgt.q,tgt.r),htp_y(tgt.q,tgt.r));
3073
+ ctx.beginPath();ctx.moveTo(sp.x,sp.y);ctx.lineTo(tp.x,tp.y);ctx.stroke();
3074
+ }
3075
+ ctx.setLineDash([]);
3076
+ ctx.restore();
3077
+
3078
+ // Draw hexagons per cluster
3079
+ for (const cluster of MAP.clusters) {
3080
+ const baseColor = cluster.color;
3081
+ const clusterAssets = cluster.assetIds.map(id=>assetIndex.get(id)).filter(Boolean);
3082
+ const isClusterMatch = !hasSearch || clusterAssets.some(a => matchedIds.has(a.id));
3083
+ const clusterDim = hasSearch && !isClusterMatch;
3084
+
3085
+ for (let ai=0; ai<clusterAssets.length; ai++) {
3086
+ const asset = clusterAssets[ai];
3087
+ const wx=htp_x(asset.q,asset.r), wy=htp_y(asset.q,asset.r);
3088
+ const s=w2s(wx,wy);
3089
+ const cx=s.x, cy=s.y;
3090
+
3091
+ // Frustum cull
3092
+ if(cx+size<0||cx-size>W||cy+size<0||cy-size>H) continue;
3093
+
3094
+ // Shade variation
3095
+ const shade = ai%3===0?18:ai%3===1?8:0;
3096
+ let fillColor = shadeV(baseColor, shade);
3097
+
3098
+ // Quality overlay
3099
+ if (showQuality && asset.qualityScore !== null && asset.qualityScore !== undefined) {
3100
+ const q = asset.qualityScore;
3101
+ if (q < 40) fillColor = '#ef4444';
3102
+ else if (q < 70) fillColor = '#f97316';
3103
+ }
3104
+
3105
+ const alpha = clusterDim ? 0.18 : 1;
3106
+ const isHovered = asset.id === hoveredAssetId;
3107
+ const isSelected = asset.id === selectedAssetId;
3108
+ const isConnectFirst = asset.id === connectFirst;
3109
+
3110
+ ctx.save();
3111
+ ctx.globalAlpha = alpha;
3112
+ hexPath(cx, cy, size*0.92);
3113
+
3114
+ if (isDark && (isHovered||isSelected||isConnectFirst)) {
3115
+ ctx.shadowColor = fillColor;
3116
+ ctx.shadowBlur = isSelected ? 16 : 8;
3117
+ }
3118
+
3119
+ ctx.fillStyle = fillColor;
3120
+ ctx.fill();
3121
+
3122
+ if (isSelected||isConnectFirst) {
3123
+ ctx.strokeStyle = isConnectFirst ? '#f59e0b' : '#fff';
3124
+ ctx.lineWidth = 2.5;
3125
+ ctx.stroke();
3126
+ } else if (isHovered) {
3127
+ ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.2)';
3128
+ ctx.lineWidth = 1.5;
3129
+ ctx.stroke();
3130
+ } else {
3131
+ ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.4)';
3132
+ ctx.lineWidth = 1;
3133
+ ctx.stroke();
3134
+ }
3135
+ ctx.restore();
3136
+
3137
+ // Quality dot
3138
+ if (showQuality && asset.qualityScore!==null && asset.qualityScore!==undefined && size>8) {
3139
+ const q = asset.qualityScore;
3140
+ if (q < 70) {
3141
+ ctx.beginPath();
3142
+ ctx.arc(cx+size*0.4, cy-size*0.4, Math.max(3,size*0.14), 0, Math.PI*2);
3143
+ ctx.fillStyle = q<40?'#ef4444':'#f97316';
3144
+ ctx.fill();
3145
+ }
3146
+ }
3147
+
3148
+ // Asset labels (detail 4, or 3 at high zoom)
3149
+ const showAssetLabel = showLabels && !clusterDim &&
3150
+ ((detailLevel>=4)||(detailLevel===3 && scale>=0.8));
3151
+ if (showAssetLabel && size>14) {
3152
+ const label = asset.name.length>12 ? asset.name.substring(0,11)+'...' : asset.name;
3153
+ ctx.save();
3154
+ ctx.font = Math.max(8,Math.min(11,size*0.38))+'px -apple-system,sans-serif';
3155
+ ctx.fillStyle = isDark ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.9)';
3156
+ ctx.textAlign='center';ctx.textBaseline='middle';
3157
+ ctx.fillText(label, cx, cy);
3158
+ ctx.restore();
3159
+ }
3160
+ }
3161
+ }
3162
+
3163
+ // Cluster labels (pill badges)
3164
+ if (showLabels && detailLevel>=1) {
3165
+ for (const cluster of MAP.clusters) {
3166
+ if (cluster.assetIds.length===0) continue;
3167
+ if (hasSearch && !cluster.assetIds.some(id=>matchedIds.has(id))) continue;
3168
+ const s=w2s(cluster.centroid.x, cluster.centroid.y);
3169
+ drawPill(s.x, s.y-size*1.2, cluster.label, cluster.color, 14);
3170
+ }
3171
+ }
3172
+
3173
+ // Sub-domain labels (detail 2+)
3174
+ if (showLabels && detailLevel>=2) {
3175
+ const subGroups = new Map();
3176
+ for (const a of MAP.assets) {
3177
+ if (!a.subDomain) continue;
3178
+ const key = a.domain+'|'+a.subDomain;
3179
+ if (!subGroups.has(key)) subGroups.set(key, []);
3180
+ subGroups.get(key).push(a);
3181
+ }
3182
+ for (const [, group] of subGroups) {
3183
+ let sx=0,sy=0;
3184
+ for (const a of group) { sx+=htp_x(a.q,a.r); sy+=htp_y(a.q,a.r); }
3185
+ const cx=sx/group.length, cy=sy/group.length;
3186
+ const s = w2s(cx, cy);
3187
+ drawPill(s.x, s.y+size*1.5, group[0].subDomain, '#64748b', 11);
3188
+ }
3189
+ }
3190
+ }
3191
+
3192
+ function drawPill(x, y, text, color, fontSize) {
3193
+ if(!text) return;
3194
+ ctx.save();
3195
+ ctx.font = '600 '+fontSize+'px -apple-system,sans-serif';
3196
+ const tw=ctx.measureText(text).width;
3197
+ const ph=fontSize+8, pw=tw+20;
3198
+ ctx.beginPath();
3199
+ if (ctx.roundRect) ctx.roundRect(x-pw/2, y-ph/2, pw, ph, ph/2);
3200
+ else { ctx.rect(x-pw/2, y-ph/2, pw, ph); }
3201
+ ctx.fillStyle = isDark ? 'rgba(30,41,59,0.9)' : 'rgba(255,255,255,0.92)';
3202
+ ctx.shadowColor='rgba(0,0,0,0.15)'; ctx.shadowBlur=6;
3203
+ ctx.fill(); ctx.shadowBlur=0;
3204
+ ctx.fillStyle = isDark ? '#e2e8f0' : '#0f172a';
3205
+ ctx.textAlign='center'; ctx.textBaseline='middle';
3206
+ ctx.fillText(text, x, y);
3207
+ ctx.restore();
3208
+ }
3209
+
3210
+ // \u2500\u2500 Hit testing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3211
+ function getAssetAt(sx, sy) {
3212
+ const w=s2w(sx,sy);
3213
+ for (const a of MAP.assets) {
3214
+ const wx=htp_x(a.q,a.r), wy=htp_y(a.q,a.r);
3215
+ const dx=Math.abs(w.x-wx), dy=Math.abs(w.y-wy);
3216
+ if (dx>HEX_SIZE||dy>HEX_SIZE) continue;
3217
+ if (dx*dx+dy*dy < HEX_SIZE*HEX_SIZE) return a;
3218
+ }
3219
+ return null;
3220
+ }
3221
+
3222
+ // \u2500\u2500 Search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3223
+ function getSearchMatches() {
3224
+ if(!searchQuery) return new Set();
3225
+ const q=searchQuery.toLowerCase();
3226
+ const m=new Set();
3227
+ for(const a of MAP.assets){
3228
+ if(a.name.toLowerCase().includes(q)||(a.domain&&a.domain.toLowerCase().includes(q))||
3229
+ (a.subDomain&&a.subDomain.toLowerCase().includes(q))) m.add(a.id);
3230
+ }
3231
+ return m;
3232
+ }
3233
+
3234
+ // \u2500\u2500 Pan & Zoom \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3235
+ let dragging=false, lastMX=0, lastMY=0;
3236
+
3237
+ wrap.addEventListener('mousedown', e=>{
3238
+ if(e.button!==0)return;
3239
+ dragging=true; lastMX=e.clientX; lastMY=e.clientY;
3240
+ wrap.classList.add('dragging');
3241
+ });
3242
+ window.addEventListener('mouseup', ()=>{dragging=false;wrap.classList.remove('dragging');});
3243
+ window.addEventListener('mousemove', e=>{
3244
+ if(dragging){
3245
+ vx+=e.clientX-lastMX; vy+=e.clientY-lastMY;
3246
+ lastMX=e.clientX; lastMY=e.clientY; draw(); return;
3247
+ }
3248
+ const rect=wrap.getBoundingClientRect();
3249
+ const sx=e.clientX-rect.left, sy=e.clientY-rect.top;
3250
+ const asset=getAssetAt(sx,sy);
3251
+ const newId=asset?asset.id:null;
3252
+ if(newId!==hoveredAssetId){hoveredAssetId=newId;draw();}
3253
+ const tt=document.getElementById('tooltip');
3254
+ if(asset){
3255
+ document.getElementById('tt-name').textContent=asset.name;
3256
+ document.getElementById('tt-domain').textContent=asset.domain+(asset.subDomain?' > '+asset.subDomain:'');
3257
+ document.getElementById('tt-quality').textContent=asset.qualityScore!==null?'Quality: '+asset.qualityScore+'/100':'';
3258
+ tt.style.display='block';tt.style.left=(e.clientX+12)+'px';tt.style.top=(e.clientY-8)+'px';
3259
+ } else { tt.style.display='none'; }
3260
+ });
3261
+
3262
+ wrap.addEventListener('click', e=>{
3263
+ const rect=wrap.getBoundingClientRect();
3264
+ const sx=e.clientX-rect.left, sy=e.clientY-rect.top;
3265
+ const asset=getAssetAt(sx,sy);
3266
+ if(connectMode){
3267
+ if(!asset) return;
3268
+ if(!connectFirst){connectFirst=asset.id;draw();}
3269
+ else if(connectFirst!==asset.id){
3270
+ localConnections.push({id:crypto.randomUUID(),sourceAssetId:connectFirst,targetAssetId:asset.id,type:'connection'});
3271
+ connectFirst=null;draw();
3272
+ }
3273
+ return;
3274
+ }
3275
+ if(asset){selectedAssetId=asset.id;showDetailPanel(asset);}
3276
+ else{selectedAssetId=null;document.getElementById('detail-panel').classList.remove('open');}
3277
+ draw();
3278
+ });
3279
+
3280
+ // Touch
3281
+ let lastTouches=[];
3282
+ wrap.addEventListener('touchstart',e=>{lastTouches=[...e.touches];},{passive:true});
3283
+ wrap.addEventListener('touchmove',e=>{
3284
+ if(e.touches.length===1){
3285
+ vx+=e.touches[0].clientX-lastTouches[0].clientX;
3286
+ vy+=e.touches[0].clientY-lastTouches[0].clientY;draw();
3287
+ } else if(e.touches.length===2){
3288
+ const d0=Math.hypot(lastTouches[0].clientX-lastTouches[1].clientX,lastTouches[0].clientY-lastTouches[1].clientY);
3289
+ const d1=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY);
3290
+ const mx=(e.touches[0].clientX+e.touches[1].clientX)/2;
3291
+ const my=(e.touches[0].clientY+e.touches[1].clientY)/2;
3292
+ applyZoom(d1/d0,mx,my);
3293
+ }
3294
+ lastTouches=[...e.touches];
3295
+ },{passive:true});
3296
+
3297
+ wrap.addEventListener('wheel',e=>{
3298
+ e.preventDefault();
3299
+ const rect=wrap.getBoundingClientRect();
3300
+ applyZoom(e.deltaY<0?1.12:1/1.12,e.clientX-rect.left,e.clientY-rect.top);
3301
+ },{passive:false});
3302
+
3303
+ function applyZoom(factor,sx,sy){
3304
+ const ns=Math.max(0.05,Math.min(8,scale*factor));
3305
+ const wx=(sx-vx)/scale,wy=(sy-vy)/scale;
3306
+ scale=ns;vx=sx-wx*scale;vy=sy-wy*scale;
3307
+ document.getElementById('zoom-pct').textContent=Math.round(scale*100)+'%';draw();
3308
+ }
3309
+ document.getElementById('zoom-in').addEventListener('click',()=>applyZoom(1.25,W/2,H/2));
3310
+ document.getElementById('zoom-out').addEventListener('click',()=>applyZoom(1/1.25,W/2,H/2));
3311
+
3312
+ // Keyboard
3313
+ wrap.addEventListener('keydown',e=>{
3314
+ const step=40;
3315
+ if(e.key==='ArrowLeft'){vx+=step;draw();}
3316
+ else if(e.key==='ArrowRight'){vx-=step;draw();}
3317
+ else if(e.key==='ArrowUp'){vy+=step;draw();}
3318
+ else if(e.key==='ArrowDown'){vy-=step;draw();}
3319
+ else if(e.key==='+'||e.key==='=')applyZoom(1.2,W/2,H/2);
3320
+ else if(e.key==='-')applyZoom(1/1.2,W/2,H/2);
3321
+ else if(e.key==='Escape'){
3322
+ selectedAssetId=null;document.getElementById('detail-panel').classList.remove('open');
3323
+ if(connectMode)toggleConnect();draw();
3324
+ }
3325
+ });
3326
+
3327
+ // \u2500\u2500 Detail Panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3328
+ function showDetailPanel(asset) {
3329
+ document.getElementById('dp-name').textContent=asset.name;
3330
+ const body=document.getElementById('dp-body');
3331
+ const rows=[['Domain',asset.domain],['Sub-domain',asset.subDomain],
3332
+ ['Quality Score',asset.qualityScore!==null?renderQuality(asset.qualityScore):null],
3333
+ ...Object.entries(asset.metadata||{}).slice(0,8).map(([k,v])=>[k,String(v)])
3334
+ ].filter(([,v])=>v!==null&&v!==undefined&&v!=='');
3335
+ body.innerHTML=rows.map(([l,v])=>'<div class="meta-row"><div class="meta-label">'+esc(String(l))+'</div><div class="meta-value">'+v+'</div></div>').join('');
3336
+ const related=localConnections.filter(c=>c.sourceAssetId===asset.id||c.targetAssetId===asset.id);
3337
+ if(related.length>0){
3338
+ body.innerHTML+='<div class="meta-row"><div class="meta-label">Connections ('+related.length+')</div><div>'+
3339
+ related.map(c=>{const oid=c.sourceAssetId===asset.id?c.targetAssetId:c.sourceAssetId;
3340
+ const o=assetIndex.get(oid);return '<div class="meta-value" style="margin-top:4px;font-size:12px">'+(o?esc(o.name):oid)+'</div>';}).join('')+'</div></div>';
3341
+ }
3342
+ document.getElementById('detail-panel').classList.add('open');
3343
+ }
3344
+ function renderQuality(s){
3345
+ const c=s>=70?'#22c55e':s>=40?'#f97316':'#ef4444';
3346
+ return s+'/100 <div class="quality-bar"><div class="quality-fill" style="width:'+s+'%;background:'+c+'"></div></div>';
3347
+ }
3348
+ function esc(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
3349
+ document.getElementById('dp-close').addEventListener('click',()=>{
3350
+ document.getElementById('detail-panel').classList.remove('open');selectedAssetId=null;draw();
3351
+ });
3352
+
3353
+ // \u2500\u2500 Toolbar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3354
+ [1,2,3,4].forEach(n=>{
3355
+ document.getElementById('dl-'+n).addEventListener('click',()=>{
3356
+ detailLevel=n;document.querySelectorAll('.detail-btn').forEach(b=>b.classList.remove('active'));
3357
+ document.getElementById('dl-'+n).classList.add('active');draw();
3358
+ });
3359
+ });
3360
+ document.getElementById('btn-labels').addEventListener('click',()=>{
3361
+ showLabels=!showLabels;document.getElementById('btn-labels').classList.toggle('active',showLabels);draw();
3362
+ });
3363
+ document.getElementById('btn-quality').addEventListener('click',()=>{
3364
+ showQuality=!showQuality;document.getElementById('btn-quality').classList.toggle('active',showQuality);draw();
3365
+ });
3366
+ function toggleConnect(){
3367
+ connectMode=!connectMode;connectFirst=null;
3368
+ document.getElementById('connect-btn').classList.toggle('active',connectMode);
3369
+ wrap.classList.toggle('connecting',connectMode);
3370
+ document.getElementById('connect-hint').style.display=connectMode?'block':'none';draw();
3371
+ }
3372
+ document.getElementById('connect-btn').addEventListener('click',toggleConnect);
3373
+ document.getElementById('theme-btn').addEventListener('click',()=>{
3374
+ isDark=!isDark;
3375
+ document.body.classList.toggle('dark',isDark);document.body.classList.toggle('light',!isDark);
3376
+ document.getElementById('theme-btn').innerHTML=isDark?'&#9788;':'&#9790;';draw();
3377
+ });
3378
+ document.getElementById('search-input').addEventListener('input',e=>{searchQuery=e.target.value.trim();draw();});
3379
+
3380
+ // \u2500\u2500 Init \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3381
+ resize(); fitToView();
3382
+ document.getElementById('zoom-pct').textContent=Math.round(scale*100)+'%';
3383
+ draw();
3384
+ })();
3385
+ </script>
3386
+ </body>
3387
+ </html>`;
3388
+ }
3389
+ function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml", "html", "map", "sops"]) {
2349
3390
  mkdirSync2(outputDir, { recursive: true });
2350
3391
  mkdirSync2(join2(outputDir, "sops"), { recursive: true });
2351
3392
  mkdirSync2(join2(outputDir, "workflows"), { recursive: true });
@@ -2368,6 +3409,10 @@ function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml
2368
3409
  writeFileSync(join2(outputDir, "topology.html"), exportHTML(nodes, edges));
2369
3410
  process.stderr.write("\u2713 topology.html\n");
2370
3411
  }
3412
+ if (formats.includes("map")) {
3413
+ writeFileSync(join2(outputDir, "cartography-map.html"), exportCartographyMap(nodes, edges));
3414
+ process.stderr.write("\u2713 cartography-map.html\n");
3415
+ }
2371
3416
  if (formats.includes("sops")) {
2372
3417
  const sops = db.getSOPs(sessionId);
2373
3418
  for (const sop of sops) {
@@ -2421,12 +3466,18 @@ function checkPrerequisites() {
2421
3466
  }
2422
3467
  export {
2423
3468
  CartographyDB,
3469
+ assignColors,
3470
+ buildMapData,
2424
3471
  checkPrerequisites,
3472
+ computeCentroid,
3473
+ computeClusterBounds,
2425
3474
  createCartographyTools,
2426
3475
  CartographyDB as default,
2427
3476
  defaultConfig,
3477
+ edgesToConnections,
2428
3478
  exportAll,
2429
3479
  exportBackstageYAML,
3480
+ exportCartographyMap,
2430
3481
  exportHTML,
2431
3482
  exportJSON,
2432
3483
  exportSOPDashboard,
@@ -2434,8 +3485,19 @@ export {
2434
3485
  generateDependencyMermaid,
2435
3486
  generateTopologyMermaid,
2436
3487
  generateWorkflowMermaid,
3488
+ groupByDomain,
3489
+ hexCorners,
3490
+ hexDistance,
3491
+ hexNeighbors,
3492
+ hexRing,
3493
+ hexSpiral,
3494
+ hexToPixel,
3495
+ layoutClusters,
3496
+ nodesToAssets,
3497
+ pixelToHex,
2437
3498
  runDiscovery,
2438
3499
  safetyHook,
3500
+ shadeVariant,
2439
3501
  stripSensitive
2440
3502
  };
2441
3503
  //# sourceMappingURL=index.js.map