@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/cli.js CHANGED
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ DOMAIN_COLORS,
4
+ DOMAIN_PALETTE,
3
5
  EDGE_RELATIONSHIPS,
4
6
  NODE_TYPES,
5
7
  SOPStepSchema,
6
8
  defaultConfig
7
- } from "./chunk-FFNOC6HF.js";
9
+ } from "./chunk-A7FTULDM.js";
8
10
  import {
9
11
  scanAllBookmarks,
10
12
  scanAllHistory
@@ -79,9 +81,21 @@ CREATE TABLE IF NOT EXISTS nodes (
79
81
  confidence REAL DEFAULT 0.5,
80
82
  metadata TEXT NOT NULL DEFAULT '{}',
81
83
  tags TEXT NOT NULL DEFAULT '[]',
84
+ domain TEXT,
85
+ sub_domain TEXT,
86
+ quality_score REAL,
82
87
  PRIMARY KEY (id, session_id)
83
88
  );
84
89
 
90
+ CREATE TABLE IF NOT EXISTS connections (
91
+ id TEXT PRIMARY KEY,
92
+ session_id TEXT NOT NULL REFERENCES sessions(id),
93
+ source_asset_id TEXT NOT NULL,
94
+ target_asset_id TEXT NOT NULL,
95
+ type TEXT,
96
+ created_at TEXT NOT NULL
97
+ );
98
+
85
99
  CREATE TABLE IF NOT EXISTS edges (
86
100
  id TEXT PRIMARY KEY,
87
101
  session_id TEXT NOT NULL REFERENCES sessions(id),
@@ -156,6 +170,7 @@ CREATE INDEX IF NOT EXISTS idx_edges_session ON edges(session_id);
156
170
  CREATE INDEX IF NOT EXISTS idx_events_session ON activity_events(session_id);
157
171
  CREATE INDEX IF NOT EXISTS idx_events_task ON activity_events(task_id);
158
172
  CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_id);
173
+ CREATE INDEX IF NOT EXISTS idx_connections_session ON connections(session_id);
159
174
  `;
160
175
  var CartographyDB = class {
161
176
  db;
@@ -171,7 +186,24 @@ var CartographyDB = class {
171
186
  const version = this.db.pragma("user_version", { simple: true });
172
187
  if (version === 0) {
173
188
  this.db.exec(SCHEMA);
174
- this.db.pragma("user_version = 1");
189
+ this.db.pragma("user_version = 2");
190
+ } else if (version === 1) {
191
+ const cols = this.db.prepare("PRAGMA table_info(nodes)").all().map((c) => c.name);
192
+ if (!cols.includes("domain")) this.db.exec("ALTER TABLE nodes ADD COLUMN domain TEXT");
193
+ if (!cols.includes("sub_domain")) this.db.exec("ALTER TABLE nodes ADD COLUMN sub_domain TEXT");
194
+ if (!cols.includes("quality_score")) this.db.exec("ALTER TABLE nodes ADD COLUMN quality_score REAL");
195
+ this.db.exec(`
196
+ CREATE TABLE IF NOT EXISTS connections (
197
+ id TEXT PRIMARY KEY,
198
+ session_id TEXT NOT NULL REFERENCES sessions(id),
199
+ source_asset_id TEXT NOT NULL,
200
+ target_asset_id TEXT NOT NULL,
201
+ type TEXT,
202
+ created_at TEXT NOT NULL
203
+ );
204
+ CREATE INDEX IF NOT EXISTS idx_connections_session ON connections(session_id);
205
+ `);
206
+ this.db.pragma("user_version = 2");
175
207
  }
176
208
  }
177
209
  close() {
@@ -214,8 +246,9 @@ var CartographyDB = class {
214
246
  upsertNode(sessionId, node, depth = 0) {
215
247
  this.db.prepare(`
216
248
  INSERT OR REPLACE INTO nodes
217
- (id, session_id, type, name, discovered_via, discovered_at, depth, confidence, metadata, tags)
218
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
249
+ (id, session_id, type, name, discovered_via, discovered_at, depth, confidence, metadata, tags,
250
+ domain, sub_domain, quality_score)
251
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
219
252
  `).run(
220
253
  node.id,
221
254
  sessionId,
@@ -226,12 +259,18 @@ var CartographyDB = class {
226
259
  depth,
227
260
  node.confidence,
228
261
  JSON.stringify(node.metadata ?? {}),
229
- JSON.stringify(node.tags ?? [])
262
+ JSON.stringify(node.tags ?? []),
263
+ node.domain ?? null,
264
+ node.subDomain ?? null,
265
+ node.qualityScore ?? null
230
266
  );
231
267
  }
232
268
  getNodes(sessionId) {
233
269
  const rows = this.db.prepare("SELECT * FROM nodes WHERE session_id = ?").all(sessionId);
234
- return rows.map((r) => ({
270
+ return rows.map((r) => this.mapNode(r));
271
+ }
272
+ mapNode(r) {
273
+ return {
235
274
  id: r["id"],
236
275
  sessionId: r["session_id"],
237
276
  type: r["type"],
@@ -242,8 +281,11 @@ var CartographyDB = class {
242
281
  confidence: r["confidence"],
243
282
  metadata: JSON.parse(r["metadata"]),
244
283
  tags: JSON.parse(r["tags"]),
245
- pathId: r["path_id"]
246
- }));
284
+ pathId: r["path_id"],
285
+ domain: r["domain"] ?? void 0,
286
+ subDomain: r["sub_domain"] ?? void 0,
287
+ qualityScore: r["quality_score"] ?? void 0
288
+ };
247
289
  }
248
290
  deleteNode(sessionId, nodeId) {
249
291
  this.db.prepare("DELETE FROM nodes WHERE session_id = ? AND id = ?").run(sessionId, nodeId);
@@ -455,6 +497,33 @@ var CartographyDB = class {
455
497
  generatedAt: r["generated_at"]
456
498
  }));
457
499
  }
500
+ // ── Connections (user-created hex map links) ─────────────────────────────
501
+ upsertConnection(sessionId, conn) {
502
+ const existing = this.db.prepare(
503
+ "SELECT id FROM connections WHERE session_id = ? AND source_asset_id = ? AND target_asset_id = ?"
504
+ ).get(sessionId, conn.sourceAssetId, conn.targetAssetId);
505
+ if (existing) return existing.id;
506
+ const id = crypto.randomUUID();
507
+ this.db.prepare(`
508
+ INSERT INTO connections (id, session_id, source_asset_id, target_asset_id, type, created_at)
509
+ VALUES (?, ?, ?, ?, ?, ?)
510
+ `).run(id, sessionId, conn.sourceAssetId, conn.targetAssetId, conn.type ?? null, (/* @__PURE__ */ new Date()).toISOString());
511
+ return id;
512
+ }
513
+ getConnections(sessionId) {
514
+ const rows = this.db.prepare("SELECT * FROM connections WHERE session_id = ?").all(sessionId);
515
+ return rows.map((r) => ({
516
+ id: r["id"],
517
+ sessionId: r["session_id"],
518
+ sourceAssetId: r["source_asset_id"],
519
+ targetAssetId: r["target_asset_id"],
520
+ type: r["type"] ?? void 0,
521
+ createdAt: r["created_at"]
522
+ }));
523
+ }
524
+ deleteConnection(sessionId, connectionId) {
525
+ this.db.prepare("DELETE FROM connections WHERE session_id = ? AND id = ?").run(sessionId, connectionId);
526
+ }
458
527
  // ── Approvals ───────────────────────────
459
528
  setApproval(pattern, action) {
460
529
  this.db.prepare(`
@@ -496,7 +565,10 @@ async function createCartographyTools(db, sessionId, opts = {}) {
496
565
  discoveredVia: z.string(),
497
566
  confidence: z.number().min(0).max(1),
498
567
  metadata: z.record(z.unknown()).optional(),
499
- tags: z.array(z.string()).optional()
568
+ tags: z.array(z.string()).optional(),
569
+ domain: z.string().optional().describe('Business domain, e.g. "Marketing", "Finance"'),
570
+ subDomain: z.string().optional().describe('Sub-domain, e.g. "Forecast client orders"'),
571
+ qualityScore: z.number().min(0).max(100).optional().describe("Data quality score 0\u2013100")
500
572
  }, async (args) => {
501
573
  const node = {
502
574
  id: stripSensitive(args["id"]),
@@ -505,7 +577,10 @@ async function createCartographyTools(db, sessionId, opts = {}) {
505
577
  discoveredVia: args["discoveredVia"],
506
578
  confidence: args["confidence"],
507
579
  metadata: args["metadata"] ?? {},
508
- tags: args["tags"] ?? []
580
+ tags: args["tags"] ?? [],
581
+ domain: args["domain"],
582
+ subDomain: args["subDomain"],
583
+ qualityScore: args["qualityScore"]
509
584
  };
510
585
  db.upsertNode(sessionId, node);
511
586
  return { content: [{ type: "text", text: `\u2713 Node: ${node.id}` }] };
@@ -600,14 +675,14 @@ async function createCartographyTools(db, sessionId, opts = {}) {
600
675
  tool("scan_local_databases", "Scan for local database files and running DB servers \u2014 PostgreSQL databases, MySQL, SQLite files from installed apps", {
601
676
  deep: z.boolean().default(false).optional().describe("Also search home directory recursively for SQLite/DB files (slower)")
602
677
  }, async (args) => {
603
- const { execSync: execSync2 } = await import("child_process");
678
+ const { execSync: execSync3 } = await import("child_process");
604
679
  const { homedir } = await import("os");
605
680
  const { existsSync: existsSync3 } = await import("fs");
606
681
  const deep = args["deep"] ?? false;
607
682
  const HOME = homedir();
608
683
  const run = (cmd) => {
609
684
  try {
610
- return execSync2(cmd, { stdio: "pipe", timeout: 1e4, shell: "/bin/sh" }).toString().trim();
685
+ return execSync3(cmd, { stdio: "pipe", timeout: 1e4, shell: "/bin/sh" }).toString().trim();
611
686
  } catch {
612
687
  return "";
613
688
  }
@@ -634,12 +709,12 @@ ${v}`).join("\n\n");
634
709
  tool("scan_k8s_resources", "Scan Kubernetes cluster via kubectl \u2014 100% readonly (get, describe)", {
635
710
  namespace: z.string().optional().describe("Filter by namespace \u2014 empty = all namespaces")
636
711
  }, async (args) => {
637
- const { execSync: execSync2 } = await import("child_process");
712
+ const { execSync: execSync3 } = await import("child_process");
638
713
  const ns = args["namespace"];
639
714
  const nsFlag = ns ? `-n ${ns}` : "--all-namespaces";
640
715
  const run = (cmd) => {
641
716
  try {
642
- return execSync2(cmd, { stdio: "pipe", timeout: 15e3, shell: "/bin/sh" }).toString().trim();
717
+ return execSync3(cmd, { stdio: "pipe", timeout: 15e3, shell: "/bin/sh" }).toString().trim();
643
718
  } catch (e) {
644
719
  return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
645
720
  }
@@ -663,7 +738,7 @@ ${run(c)}`).join("\n\n");
663
738
  region: z.string().optional().describe("AWS Region \u2014 default: AWS_DEFAULT_REGION or profile"),
664
739
  profile: z.string().optional().describe("AWS CLI profile")
665
740
  }, async (args) => {
666
- const { execSync: execSync2 } = await import("child_process");
741
+ const { execSync: execSync3 } = await import("child_process");
667
742
  const region = args["region"];
668
743
  const profile = args["profile"];
669
744
  const env = { ...process.env };
@@ -671,7 +746,7 @@ ${run(c)}`).join("\n\n");
671
746
  const pf = profile ? `--profile ${profile}` : "";
672
747
  const run = (cmd) => {
673
748
  try {
674
- return execSync2(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh", env }).toString().trim();
749
+ return execSync3(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh", env }).toString().trim();
675
750
  } catch (e) {
676
751
  return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
677
752
  }
@@ -693,12 +768,12 @@ ${run(c)}`).join("\n\n");
693
768
  tool("scan_gcp_resources", "Scan Google Cloud Platform via gcloud CLI \u2014 100% readonly (list, describe)", {
694
769
  project: z.string().optional().describe("GCP Project ID \u2014 default: current gcloud project")
695
770
  }, async (args) => {
696
- const { execSync: execSync2 } = await import("child_process");
771
+ const { execSync: execSync3 } = await import("child_process");
697
772
  const project = args["project"];
698
773
  const pf = project ? `--project ${project}` : "";
699
774
  const run = (cmd) => {
700
775
  try {
701
- return execSync2(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
776
+ return execSync3(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
702
777
  } catch (e) {
703
778
  return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
704
779
  }
@@ -722,14 +797,14 @@ ${run(c)}`).join("\n\n");
722
797
  subscription: z.string().optional().describe("Azure Subscription ID"),
723
798
  resourceGroup: z.string().optional().describe("Filter by resource group")
724
799
  }, async (args) => {
725
- const { execSync: execSync2 } = await import("child_process");
800
+ const { execSync: execSync3 } = await import("child_process");
726
801
  const sub = args["subscription"];
727
802
  const rg = args["resourceGroup"];
728
803
  const sf = sub ? `--subscription ${sub}` : "";
729
804
  const rf = rg ? `--resource-group ${rg}` : "";
730
805
  const run = (cmd) => {
731
806
  try {
732
- return execSync2(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
807
+ return execSync3(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
733
808
  } catch (e) {
734
809
  return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
735
810
  }
@@ -752,11 +827,11 @@ ${run(c)}`).join("\n\n");
752
827
  tool("scan_installed_apps", "Scan all installed apps and tools \u2014 IDEs, office, dev tools, business apps, databases", {
753
828
  searchHint: z.string().optional().describe('Optional search term to find specific tools (e.g. "hubspot windsurf cursor")')
754
829
  }, async (args) => {
755
- const { execSync: execSync2 } = await import("child_process");
830
+ const { execSync: execSync3 } = await import("child_process");
756
831
  const hint = args["searchHint"];
757
832
  const run = (cmd) => {
758
833
  try {
759
- return execSync2(cmd, { stdio: "pipe", timeout: 15e3, shell: "/bin/sh" }).toString().trim();
834
+ return execSync3(cmd, { stdio: "pipe", timeout: 15e3, shell: "/bin/sh" }).toString().trim();
760
835
  } catch {
761
836
  return "";
762
837
  }
@@ -1137,6 +1212,221 @@ Use ask_user when you need context from the user.`;
1137
1212
  // src/exporter.ts
1138
1213
  import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
1139
1214
  import { join as join2 } from "path";
1215
+
1216
+ // src/hex.ts
1217
+ function hexToPixel(q, r, size) {
1218
+ const x = size * (3 / 2 * q);
1219
+ const y = size * (Math.sqrt(3) / 2 * q + Math.sqrt(3) * r);
1220
+ return { x, y };
1221
+ }
1222
+ var HEX_DIRECTIONS = [
1223
+ { q: 1, r: 0 },
1224
+ { q: 1, r: -1 },
1225
+ { q: 0, r: -1 },
1226
+ { q: -1, r: 0 },
1227
+ { q: -1, r: 1 },
1228
+ { q: 0, r: 1 }
1229
+ ];
1230
+ function hexDistance(a, b) {
1231
+ return (Math.abs(a.q - b.q) + Math.abs(a.q + a.r - b.q - b.r) + Math.abs(a.r - b.r)) / 2;
1232
+ }
1233
+ function hexRing(center, radius) {
1234
+ if (radius === 0) return [{ q: center.q, r: center.r }];
1235
+ const results = [];
1236
+ let hex = {
1237
+ q: center.q + HEX_DIRECTIONS[4].q * radius,
1238
+ r: center.r + HEX_DIRECTIONS[4].r * radius
1239
+ };
1240
+ for (let side = 0; side < 6; side++) {
1241
+ for (let step = 0; step < radius; step++) {
1242
+ results.push({ q: hex.q, r: hex.r });
1243
+ hex = {
1244
+ q: hex.q + HEX_DIRECTIONS[side].q,
1245
+ r: hex.r + HEX_DIRECTIONS[side].r
1246
+ };
1247
+ }
1248
+ }
1249
+ return results;
1250
+ }
1251
+ function hexSpiral(center, count) {
1252
+ const positions = [];
1253
+ let ring = 0;
1254
+ while (positions.length < count) {
1255
+ const ringPositions = hexRing(center, ring);
1256
+ for (const pos of ringPositions) {
1257
+ if (positions.length >= count) break;
1258
+ positions.push(pos);
1259
+ }
1260
+ ring++;
1261
+ }
1262
+ return positions;
1263
+ }
1264
+
1265
+ // src/cluster.ts
1266
+ function assignColor(domain, allDomains) {
1267
+ if (DOMAIN_COLORS[domain]) return DOMAIN_COLORS[domain];
1268
+ const idx = allDomains.indexOf(domain);
1269
+ return DOMAIN_PALETTE[idx % DOMAIN_PALETTE.length];
1270
+ }
1271
+ function assignColors(domains) {
1272
+ const result = {};
1273
+ for (const d of domains) {
1274
+ result[d] = assignColor(d, domains);
1275
+ }
1276
+ return result;
1277
+ }
1278
+ function groupByDomain(assets) {
1279
+ const map = /* @__PURE__ */ new Map();
1280
+ for (const a of assets) {
1281
+ const d = a.domain || "Other";
1282
+ if (!map.has(d)) map.set(d, []);
1283
+ map.get(d).push(a);
1284
+ }
1285
+ return map;
1286
+ }
1287
+ var CLUSTER_GAP = 3;
1288
+ function layoutClusters(groups, hexSize) {
1289
+ const allDomains = Array.from(groups.keys());
1290
+ const colors = assignColors(allDomains);
1291
+ const sorted = Array.from(groups.entries()).sort((a, b) => b[1].length - a[1].length);
1292
+ const occupied = /* @__PURE__ */ new Set();
1293
+ const key = (q, r) => `${q},${r}`;
1294
+ const clusters = [];
1295
+ const allAssets = [];
1296
+ for (const [domain, domainAssets] of sorted) {
1297
+ const origin = clusters.length === 0 ? { q: 0, r: 0 } : findFreeOrigin(occupied, domainAssets.length, CLUSTER_GAP);
1298
+ const positions = hexSpiral(origin, domainAssets.length);
1299
+ const assetIds = [];
1300
+ for (let i = 0; i < domainAssets.length; i++) {
1301
+ const asset = domainAssets[i];
1302
+ asset.position = positions[i];
1303
+ assetIds.push(asset.id);
1304
+ occupied.add(key(positions[i].q, positions[i].r));
1305
+ allAssets.push(asset);
1306
+ }
1307
+ const centroid = computeCentroid(positions, hexSize);
1308
+ clusters.push({
1309
+ id: `cluster:${domain}`,
1310
+ label: domain,
1311
+ domain,
1312
+ color: colors[domain],
1313
+ assetIds,
1314
+ centroid
1315
+ });
1316
+ }
1317
+ return { clusters, assets: allAssets };
1318
+ }
1319
+ function findFreeOrigin(occupied, count, gap) {
1320
+ const key = (q, r) => `${q},${r}`;
1321
+ for (let searchRadius = 1; searchRadius < 100; searchRadius++) {
1322
+ const candidates = hexSpiral({ q: 0, r: 0 }, 1 + 6 * searchRadius * (searchRadius + 1) / 2);
1323
+ for (const candidate of candidates) {
1324
+ const testPositions = hexSpiral(candidate, count);
1325
+ let fits = true;
1326
+ for (const tp of testPositions) {
1327
+ if (occupied.has(key(tp.q, tp.r))) {
1328
+ fits = false;
1329
+ break;
1330
+ }
1331
+ for (const oKey of occupied) {
1332
+ const [oq, or] = oKey.split(",").map(Number);
1333
+ if (hexDistance(tp, { q: oq, r: or }) < gap) {
1334
+ fits = false;
1335
+ break;
1336
+ }
1337
+ }
1338
+ if (!fits) break;
1339
+ }
1340
+ if (fits) return candidate;
1341
+ }
1342
+ }
1343
+ return { q: occupied.size * 5, r: 0 };
1344
+ }
1345
+ function computeCentroid(positions, hexSize) {
1346
+ if (positions.length === 0) return { x: 0, y: 0 };
1347
+ let sx = 0, sy = 0;
1348
+ for (const { q, r } of positions) {
1349
+ const { x, y } = hexToPixel(q, r, hexSize);
1350
+ sx += x;
1351
+ sy += y;
1352
+ }
1353
+ return { x: sx / positions.length, y: sy / positions.length };
1354
+ }
1355
+
1356
+ // src/mapper.ts
1357
+ var TYPE_TO_DOMAIN = {
1358
+ database_server: "Data Layer",
1359
+ database: "Data Layer",
1360
+ table: "Data Layer",
1361
+ cache_server: "Data Layer",
1362
+ web_service: "Web / API",
1363
+ api_endpoint: "Web / API",
1364
+ message_broker: "Messaging",
1365
+ queue: "Messaging",
1366
+ topic: "Messaging",
1367
+ host: "Infrastructure",
1368
+ container: "Infrastructure",
1369
+ pod: "Infrastructure",
1370
+ k8s_cluster: "Infrastructure",
1371
+ config_file: "Infrastructure",
1372
+ saas_tool: "SaaS Tools"
1373
+ };
1374
+ function resolveDomain(node) {
1375
+ if (node.domain) return node.domain;
1376
+ const meta = node.metadata;
1377
+ if (typeof meta["category"] === "string" && meta["category"].length > 0) {
1378
+ return meta["category"];
1379
+ }
1380
+ for (const tag of node.tags ?? []) {
1381
+ if (tag.length > 2 && tag[0] === tag[0].toUpperCase()) {
1382
+ return tag;
1383
+ }
1384
+ }
1385
+ return TYPE_TO_DOMAIN[node.type] ?? "Other";
1386
+ }
1387
+ function nodesToAssets(nodes) {
1388
+ return nodes.map((n) => ({
1389
+ id: n.id,
1390
+ name: n.name,
1391
+ domain: resolveDomain(n),
1392
+ subDomain: n.subDomain,
1393
+ qualityScore: n.qualityScore ?? Math.round(n.confidence * 100),
1394
+ metadata: n.metadata ?? {},
1395
+ position: { q: 0, r: 0 }
1396
+ // will be assigned by layoutClusters
1397
+ }));
1398
+ }
1399
+ function edgesToConnections(edges) {
1400
+ return edges.map((e) => ({
1401
+ id: e.id,
1402
+ sourceAssetId: e.sourceId,
1403
+ targetAssetId: e.targetId,
1404
+ type: e.relationship
1405
+ }));
1406
+ }
1407
+ var HEX_SIZE = 24;
1408
+ function buildMapData(nodes, edges, options) {
1409
+ const rawAssets = nodesToAssets(nodes);
1410
+ const connections = edgesToConnections(edges);
1411
+ if (rawAssets.length === 0) {
1412
+ return {
1413
+ assets: [],
1414
+ clusters: [],
1415
+ connections,
1416
+ meta: { exportedAt: (/* @__PURE__ */ new Date()).toISOString(), theme: options?.theme ?? "light" }
1417
+ };
1418
+ }
1419
+ const groups = groupByDomain(rawAssets);
1420
+ const { clusters, assets } = layoutClusters(groups, HEX_SIZE);
1421
+ return {
1422
+ assets,
1423
+ clusters,
1424
+ connections,
1425
+ meta: { exportedAt: (/* @__PURE__ */ new Date()).toISOString(), theme: options?.theme ?? "light" }
1426
+ };
1427
+ }
1428
+
1429
+ // src/exporter.ts
1140
1430
  function nodeLayer(type) {
1141
1431
  if (type === "saas_tool") return "saas";
1142
1432
  if (["web_service", "api_endpoint"].includes(type)) return "web";
@@ -1880,7 +2170,647 @@ function exportSOPMarkdown(sop) {
1880
2170
  }
1881
2171
  return lines.join("\n");
1882
2172
  }
1883
- function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml", "html", "sops"]) {
2173
+ function exportCartographyMap(nodes, edges, options) {
2174
+ const mapData = buildMapData(nodes, edges, options);
2175
+ const { assets, clusters, connections, meta } = mapData;
2176
+ const isEmpty = assets.length === 0;
2177
+ const HEX_SIZE2 = 24;
2178
+ const dataJson = JSON.stringify({
2179
+ assets: assets.map((a) => ({
2180
+ id: a.id,
2181
+ name: a.name,
2182
+ domain: a.domain,
2183
+ subDomain: a.subDomain ?? null,
2184
+ qualityScore: a.qualityScore ?? null,
2185
+ metadata: a.metadata,
2186
+ q: a.position.q,
2187
+ r: a.position.r
2188
+ })),
2189
+ clusters: clusters.map((c) => ({
2190
+ id: c.id,
2191
+ label: c.label,
2192
+ domain: c.domain,
2193
+ color: c.color,
2194
+ assetIds: c.assetIds,
2195
+ centroid: c.centroid
2196
+ })),
2197
+ connections: connections.map((c) => ({
2198
+ id: c.id,
2199
+ sourceAssetId: c.sourceAssetId,
2200
+ targetAssetId: c.targetAssetId,
2201
+ type: c.type ?? "connection"
2202
+ }))
2203
+ });
2204
+ return `<!DOCTYPE html>
2205
+ <html lang="en">
2206
+ <head>
2207
+ <meta charset="UTF-8"/>
2208
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
2209
+ <title>Data Cartography Map</title>
2210
+ <style>
2211
+ *{box-sizing:border-box;margin:0;padding:0}
2212
+ html,body{width:100%;height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
2213
+ body{display:flex;flex-direction:column;background:${meta.theme === "dark" ? "#0f172a" : "#f8fafc"};color:${meta.theme === "dark" ? "#e2e8f0" : "#1e293b"}}
2214
+ #topbar{
2215
+ height:48px;display:flex;align-items:center;gap:16px;padding:0 20px;
2216
+ background:${meta.theme === "dark" ? "#1e293b" : "#fff"};border-bottom:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};z-index:10;flex-shrink:0;
2217
+ }
2218
+ #topbar h1{font-size:15px;font-weight:600;letter-spacing:-0.01em}
2219
+ #search-box{
2220
+ display:flex;align-items:center;gap:8px;background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"};
2221
+ border-radius:8px;padding:5px 10px;margin-left:auto;
2222
+ }
2223
+ #search-box input{
2224
+ border:none;background:transparent;font-size:13px;outline:none;width:180px;color:inherit;
2225
+ }
2226
+ #search-box input::placeholder{color:#94a3b8}
2227
+ #main{flex:1;display:flex;overflow:hidden;position:relative}
2228
+ #canvas-wrap{flex:1;position:relative;overflow:hidden;cursor:grab}
2229
+ #canvas-wrap.dragging{cursor:grabbing}
2230
+ #canvas-wrap.connecting{cursor:crosshair}
2231
+ canvas{display:block;width:100%;height:100%}
2232
+ /* Detail panel */
2233
+ #detail-panel{
2234
+ width:280px;background:${meta.theme === "dark" ? "#1e293b" : "#fff"};border-left:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
2235
+ display:flex;flex-direction:column;transform:translateX(100%);
2236
+ transition:transform .2s ease;z-index:5;flex-shrink:0;overflow-y:auto;
2237
+ }
2238
+ #detail-panel.open{transform:translateX(0)}
2239
+ #detail-panel .panel-header{
2240
+ padding:16px;border-bottom:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};display:flex;align-items:center;gap:10px;
2241
+ }
2242
+ #detail-panel .panel-header h3{font-size:14px;font-weight:600;flex:1;word-break:break-word}
2243
+ #detail-panel .close-btn{
2244
+ width:24px;height:24px;border:none;background:transparent;cursor:pointer;
2245
+ color:#94a3b8;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:16px;
2246
+ }
2247
+ #detail-panel .close-btn:hover{background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"}}
2248
+ #detail-panel .panel-body{padding:12px 16px;display:flex;flex-direction:column;gap:12px}
2249
+ #detail-panel .meta-row{display:flex;flex-direction:column;gap:3px}
2250
+ #detail-panel .meta-label{font-size:11px;font-weight:500;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em}
2251
+ #detail-panel .meta-value{font-size:13px;word-break:break-all}
2252
+ #detail-panel .quality-bar{height:6px;border-radius:3px;background:${meta.theme === "dark" ? "#334155" : "#e2e8f0"};margin-top:4px}
2253
+ #detail-panel .quality-fill{height:6px;border-radius:3px;transition:width .3s}
2254
+ /* Bottom-left toolbar */
2255
+ #toolbar-left{
2256
+ position:absolute;bottom:20px;left:20px;display:flex;gap:8px;z-index:10;
2257
+ }
2258
+ .tb-btn{
2259
+ width:40px;height:40px;border-radius:10px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
2260
+ background:${meta.theme === "dark" ? "#1e293b" : "#fff"};box-shadow:0 1px 4px rgba(0,0,0,.08);cursor:pointer;
2261
+ display:flex;align-items:center;justify-content:center;font-size:18px;
2262
+ transition:all .15s;color:inherit;
2263
+ }
2264
+ .tb-btn:hover{border-color:#94a3b8}
2265
+ .tb-btn.active{background:${meta.theme === "dark" ? "#1e3a5f" : "#eff6ff"};border-color:#3b82f6}
2266
+ /* Bottom-right toolbar */
2267
+ #toolbar-right{
2268
+ position:absolute;bottom:20px;right:20px;display:flex;flex-direction:column;
2269
+ align-items:flex-end;gap:8px;z-index:10;
2270
+ }
2271
+ #zoom-controls{display:flex;align-items:center;gap:6px}
2272
+ .zoom-btn{
2273
+ width:34px;height:34px;border-radius:8px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
2274
+ background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
2275
+ font-size:18px;color:inherit;display:flex;align-items:center;justify-content:center;
2276
+ }
2277
+ .zoom-btn:hover{background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"}}
2278
+ #zoom-pct{font-size:12px;font-weight:500;color:#64748b;min-width:38px;text-align:center}
2279
+ #detail-selector{display:flex;flex-direction:column;gap:4px}
2280
+ .detail-btn{
2281
+ width:34px;height:34px;border-radius:8px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
2282
+ background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
2283
+ font-size:12px;font-weight:600;color:#64748b;display:flex;align-items:center;justify-content:center;
2284
+ }
2285
+ .detail-btn:hover{background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"}}
2286
+ .detail-btn.active{background:${meta.theme === "dark" ? "#1e3a5f" : "#eff6ff"};border-color:#3b82f6;color:#2563eb}
2287
+ #connect-btn{
2288
+ width:40px;height:40px;border-radius:10px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
2289
+ background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
2290
+ font-size:18px;display:flex;align-items:center;justify-content:center;color:inherit;
2291
+ }
2292
+ #connect-btn.active{background:#fef3c7;border-color:#f59e0b}
2293
+ /* Tooltip */
2294
+ #tooltip{
2295
+ position:fixed;background:#1e293b;color:#fff;border-radius:8px;
2296
+ padding:8px 12px;font-size:12px;pointer-events:none;z-index:100;
2297
+ display:none;max-width:220px;box-shadow:0 4px 12px rgba(0,0,0,.15);
2298
+ }
2299
+ #tooltip .tt-name{font-weight:600;margin-bottom:2px}
2300
+ #tooltip .tt-domain{color:#94a3b8;font-size:11px}
2301
+ #tooltip .tt-quality{font-size:11px;margin-top:2px}
2302
+ /* Empty state */
2303
+ #empty-state{
2304
+ position:absolute;inset:0;display:flex;flex-direction:column;
2305
+ align-items:center;justify-content:center;gap:12px;color:#94a3b8;
2306
+ }
2307
+ #empty-state p{font-size:14px}
2308
+ /* Theme toggle */
2309
+ #theme-btn{
2310
+ width:40px;height:40px;border-radius:10px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
2311
+ background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
2312
+ font-size:18px;display:flex;align-items:center;justify-content:center;color:inherit;
2313
+ }
2314
+ /* Dark mode overrides (toggled via JS) */
2315
+ body.dark{background:#0f172a;color:#e2e8f0}
2316
+ body.dark #topbar{background:#1e293b;border-color:#334155}
2317
+ body.dark #search-box{background:#334155}
2318
+ body.dark #detail-panel{background:#1e293b;border-color:#334155}
2319
+ body.dark .tb-btn,body.dark .zoom-btn,body.dark .detail-btn,body.dark #connect-btn,body.dark #theme-btn{
2320
+ background:#1e293b;border-color:#334155;color:#e2e8f0;
2321
+ }
2322
+ /* Light mode overrides */
2323
+ body.light{background:#f8fafc;color:#1e293b}
2324
+ body.light #topbar{background:#fff;border-color:#e2e8f0}
2325
+ /* Connection hint */
2326
+ #connect-hint{
2327
+ position:absolute;top:12px;left:50%;transform:translateX(-50%);
2328
+ background:#fef3c7;border:1px solid #f59e0b;color:#92400e;
2329
+ padding:6px 14px;border-radius:20px;font-size:12px;font-weight:500;
2330
+ display:none;z-index:20;pointer-events:none;
2331
+ }
2332
+ /* Screen reader only */
2333
+ .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}
2334
+ </style>
2335
+ </head>
2336
+ <body class="${meta.theme}">
2337
+ <!-- Top bar -->
2338
+ <div id="topbar">
2339
+ <h1>Data Cartography Map</h1>
2340
+ <div id="search-box">
2341
+ <span style="color:#94a3b8;font-size:14px">&#8981;</span>
2342
+ <input id="search-input" type="text" placeholder="Search assets..." aria-label="Search data assets"/>
2343
+ </div>
2344
+ <button id="theme-btn" title="Toggle dark/light mode" aria-label="Toggle theme">${meta.theme === "dark" ? "&#9788;" : "&#9790;"}</button>
2345
+ </div>
2346
+ <!-- SR summary -->
2347
+ <div class="sr-only" role="status" aria-live="polite" id="sr-summary">
2348
+ Data cartography map with ${assets.length} assets in ${clusters.length} clusters.
2349
+ </div>
2350
+ <!-- Main area -->
2351
+ <div id="main">
2352
+ <div id="canvas-wrap" role="application" aria-label="Data cartography hex map" tabindex="0">
2353
+ <canvas id="hexmap" aria-hidden="true"></canvas>
2354
+ ${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>' : ""}
2355
+ </div>
2356
+ <div id="detail-panel" role="complementary" aria-label="Asset details">
2357
+ <div class="panel-header">
2358
+ <h3 id="dp-name">&mdash;</h3>
2359
+ <button class="close-btn" id="dp-close" aria-label="Close panel">&#10005;</button>
2360
+ </div>
2361
+ <div class="panel-body" id="dp-body"></div>
2362
+ </div>
2363
+ </div>
2364
+ <!-- Bottom-left toolbar -->
2365
+ <div id="toolbar-left">
2366
+ <button class="tb-btn active" id="btn-labels" title="Show labels" aria-pressed="true" aria-label="Toggle labels">&#127991;</button>
2367
+ <button class="tb-btn" id="btn-quality" title="Quality layer" aria-pressed="false" aria-label="Toggle quality layer">&#128065;</button>
2368
+ </div>
2369
+ <!-- Bottom-right toolbar -->
2370
+ <div id="toolbar-right">
2371
+ <div id="zoom-controls">
2372
+ <button class="zoom-btn" id="zoom-out" aria-label="Zoom out">&minus;</button>
2373
+ <span id="zoom-pct">100%</span>
2374
+ <button class="zoom-btn" id="zoom-in" aria-label="Zoom in">+</button>
2375
+ </div>
2376
+ <div id="detail-selector">
2377
+ <button class="detail-btn" id="dl-1" aria-label="Detail level 1">1</button>
2378
+ <button class="detail-btn active" id="dl-2" aria-label="Detail level 2">2</button>
2379
+ <button class="detail-btn" id="dl-3" aria-label="Detail level 3">3</button>
2380
+ <button class="detail-btn" id="dl-4" aria-label="Detail level 4">4</button>
2381
+ </div>
2382
+ <button id="connect-btn" title="Connection tool" aria-label="Toggle connection tool">&#128279;</button>
2383
+ </div>
2384
+ <!-- Connection hint -->
2385
+ <div id="connect-hint">Click two assets to create a connection</div>
2386
+ <!-- Tooltip -->
2387
+ <div id="tooltip" role="tooltip">
2388
+ <div class="tt-name" id="tt-name"></div>
2389
+ <div class="tt-domain" id="tt-domain"></div>
2390
+ <div class="tt-quality" id="tt-quality"></div>
2391
+ </div>
2392
+
2393
+ <script>
2394
+ (function() {
2395
+ 'use strict';
2396
+
2397
+ // \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
2398
+ const MAP = ${dataJson};
2399
+ const HEX_SIZE = ${HEX_SIZE2};
2400
+ const IS_EMPTY = ${isEmpty};
2401
+
2402
+ // Build asset index
2403
+ const assetIndex = new Map();
2404
+ const clusterByAsset = new Map();
2405
+ for (const c of MAP.clusters) {
2406
+ for (const aid of c.assetIds) {
2407
+ clusterByAsset.set(aid, c);
2408
+ }
2409
+ }
2410
+ for (const a of MAP.assets) {
2411
+ assetIndex.set(a.id, a);
2412
+ }
2413
+
2414
+ // \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
2415
+ const canvas = document.getElementById('hexmap');
2416
+ const ctx = canvas.getContext('2d');
2417
+ const wrap = document.getElementById('canvas-wrap');
2418
+ let W = 0, H = 0;
2419
+
2420
+ function resize() {
2421
+ const dpr = window.devicePixelRatio || 1;
2422
+ W = wrap.clientWidth; H = wrap.clientHeight;
2423
+ canvas.width = W * dpr; canvas.height = H * dpr;
2424
+ canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
2425
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
2426
+ draw();
2427
+ }
2428
+ window.addEventListener('resize', resize);
2429
+
2430
+ // \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
2431
+ let vx = 0, vy = 0, scale = 1;
2432
+ let detailLevel = 2, showLabels = true, showQuality = false;
2433
+ let isDark = document.body.classList.contains('dark');
2434
+ let connectMode = false, connectFirst = null;
2435
+ let hoveredAssetId = null, selectedAssetId = null;
2436
+ let searchQuery = '';
2437
+ let localConnections = [...MAP.connections];
2438
+
2439
+ // Flat-top hex math
2440
+ function htp_x(q, r) { return HEX_SIZE * (3/2 * q); }
2441
+ function htp_y(q, r) { return HEX_SIZE * (Math.sqrt(3)/2 * q + Math.sqrt(3) * r); }
2442
+ function w2s(wx, wy) { return { x: wx*scale+vx, y: wy*scale+vy }; }
2443
+ function s2w(sx, sy) { return { x: (sx-vx)/scale, y: (sy-vy)/scale }; }
2444
+
2445
+ function fitToView() {
2446
+ if (IS_EMPTY || MAP.assets.length === 0) { vx = 0; vy = 0; scale = 1; return; }
2447
+ let mnx=Infinity,mny=Infinity,mxx=-Infinity,mxy=-Infinity;
2448
+ for (const a of MAP.assets) {
2449
+ const px=htp_x(a.q,a.r), py=htp_y(a.q,a.r);
2450
+ if(px<mnx)mnx=px;if(py<mny)mny=py;if(px>mxx)mxx=px;if(py>mxy)mxy=py;
2451
+ }
2452
+ const pw=mxx-mnx+HEX_SIZE*4, ph=mxy-mny+HEX_SIZE*4;
2453
+ scale = Math.min(W/pw, H/ph, 2) * 0.85;
2454
+ vx = W/2 - ((mnx+mxx)/2)*scale;
2455
+ vy = H/2 - ((mny+mxy)/2)*scale;
2456
+ }
2457
+
2458
+ // \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
2459
+ function hexPath(cx, cy, r) {
2460
+ ctx.beginPath();
2461
+ for (let i=0;i<6;i++) {
2462
+ const angle = Math.PI/180*(60*i);
2463
+ const x=cx+r*Math.cos(angle), y=cy+r*Math.sin(angle);
2464
+ i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);
2465
+ }
2466
+ ctx.closePath();
2467
+ }
2468
+
2469
+ function shadeV(hex, amt) {
2470
+ if(!hex||hex.length<7)return hex;
2471
+ const n=parseInt(hex.replace('#',''),16);
2472
+ const r=Math.min(255,(n>>16)+amt), g=Math.min(255,((n>>8)&0xff)+amt), b=Math.min(255,(n&0xff)+amt);
2473
+ return '#'+r.toString(16).padStart(2,'0')+g.toString(16).padStart(2,'0')+b.toString(16).padStart(2,'0');
2474
+ }
2475
+
2476
+ function draw() {
2477
+ ctx.clearRect(0,0,W,H);
2478
+ ctx.fillStyle = isDark ? '#0f172a' : '#f8fafc';
2479
+ ctx.fillRect(0,0,W,H);
2480
+ if (IS_EMPTY) return;
2481
+
2482
+ const size = HEX_SIZE * scale;
2483
+ const matchedIds = getSearchMatches();
2484
+ const hasSearch = searchQuery.length > 0;
2485
+
2486
+ // Draw connections
2487
+ ctx.save();
2488
+ ctx.strokeStyle = isDark ? 'rgba(148,163,184,0.35)' : 'rgba(100,116,139,0.25)';
2489
+ ctx.lineWidth = 1.5;
2490
+ ctx.setLineDash([4,4]);
2491
+ for (const conn of localConnections) {
2492
+ const src = assetIndex.get(conn.sourceAssetId);
2493
+ const tgt = assetIndex.get(conn.targetAssetId);
2494
+ if (!src||!tgt) continue;
2495
+ const sp=w2s(htp_x(src.q,src.r),htp_y(src.q,src.r));
2496
+ const tp=w2s(htp_x(tgt.q,tgt.r),htp_y(tgt.q,tgt.r));
2497
+ ctx.beginPath();ctx.moveTo(sp.x,sp.y);ctx.lineTo(tp.x,tp.y);ctx.stroke();
2498
+ }
2499
+ ctx.setLineDash([]);
2500
+ ctx.restore();
2501
+
2502
+ // Draw hexagons per cluster
2503
+ for (const cluster of MAP.clusters) {
2504
+ const baseColor = cluster.color;
2505
+ const clusterAssets = cluster.assetIds.map(id=>assetIndex.get(id)).filter(Boolean);
2506
+ const isClusterMatch = !hasSearch || clusterAssets.some(a => matchedIds.has(a.id));
2507
+ const clusterDim = hasSearch && !isClusterMatch;
2508
+
2509
+ for (let ai=0; ai<clusterAssets.length; ai++) {
2510
+ const asset = clusterAssets[ai];
2511
+ const wx=htp_x(asset.q,asset.r), wy=htp_y(asset.q,asset.r);
2512
+ const s=w2s(wx,wy);
2513
+ const cx=s.x, cy=s.y;
2514
+
2515
+ // Frustum cull
2516
+ if(cx+size<0||cx-size>W||cy+size<0||cy-size>H) continue;
2517
+
2518
+ // Shade variation
2519
+ const shade = ai%3===0?18:ai%3===1?8:0;
2520
+ let fillColor = shadeV(baseColor, shade);
2521
+
2522
+ // Quality overlay
2523
+ if (showQuality && asset.qualityScore !== null && asset.qualityScore !== undefined) {
2524
+ const q = asset.qualityScore;
2525
+ if (q < 40) fillColor = '#ef4444';
2526
+ else if (q < 70) fillColor = '#f97316';
2527
+ }
2528
+
2529
+ const alpha = clusterDim ? 0.18 : 1;
2530
+ const isHovered = asset.id === hoveredAssetId;
2531
+ const isSelected = asset.id === selectedAssetId;
2532
+ const isConnectFirst = asset.id === connectFirst;
2533
+
2534
+ ctx.save();
2535
+ ctx.globalAlpha = alpha;
2536
+ hexPath(cx, cy, size*0.92);
2537
+
2538
+ if (isDark && (isHovered||isSelected||isConnectFirst)) {
2539
+ ctx.shadowColor = fillColor;
2540
+ ctx.shadowBlur = isSelected ? 16 : 8;
2541
+ }
2542
+
2543
+ ctx.fillStyle = fillColor;
2544
+ ctx.fill();
2545
+
2546
+ if (isSelected||isConnectFirst) {
2547
+ ctx.strokeStyle = isConnectFirst ? '#f59e0b' : '#fff';
2548
+ ctx.lineWidth = 2.5;
2549
+ ctx.stroke();
2550
+ } else if (isHovered) {
2551
+ ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.2)';
2552
+ ctx.lineWidth = 1.5;
2553
+ ctx.stroke();
2554
+ } else {
2555
+ ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.4)';
2556
+ ctx.lineWidth = 1;
2557
+ ctx.stroke();
2558
+ }
2559
+ ctx.restore();
2560
+
2561
+ // Quality dot
2562
+ if (showQuality && asset.qualityScore!==null && asset.qualityScore!==undefined && size>8) {
2563
+ const q = asset.qualityScore;
2564
+ if (q < 70) {
2565
+ ctx.beginPath();
2566
+ ctx.arc(cx+size*0.4, cy-size*0.4, Math.max(3,size*0.14), 0, Math.PI*2);
2567
+ ctx.fillStyle = q<40?'#ef4444':'#f97316';
2568
+ ctx.fill();
2569
+ }
2570
+ }
2571
+
2572
+ // Asset labels (detail 4, or 3 at high zoom)
2573
+ const showAssetLabel = showLabels && !clusterDim &&
2574
+ ((detailLevel>=4)||(detailLevel===3 && scale>=0.8));
2575
+ if (showAssetLabel && size>14) {
2576
+ const label = asset.name.length>12 ? asset.name.substring(0,11)+'...' : asset.name;
2577
+ ctx.save();
2578
+ ctx.font = Math.max(8,Math.min(11,size*0.38))+'px -apple-system,sans-serif';
2579
+ ctx.fillStyle = isDark ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.9)';
2580
+ ctx.textAlign='center';ctx.textBaseline='middle';
2581
+ ctx.fillText(label, cx, cy);
2582
+ ctx.restore();
2583
+ }
2584
+ }
2585
+ }
2586
+
2587
+ // Cluster labels (pill badges)
2588
+ if (showLabels && detailLevel>=1) {
2589
+ for (const cluster of MAP.clusters) {
2590
+ if (cluster.assetIds.length===0) continue;
2591
+ if (hasSearch && !cluster.assetIds.some(id=>matchedIds.has(id))) continue;
2592
+ const s=w2s(cluster.centroid.x, cluster.centroid.y);
2593
+ drawPill(s.x, s.y-size*1.2, cluster.label, cluster.color, 14);
2594
+ }
2595
+ }
2596
+
2597
+ // Sub-domain labels (detail 2+)
2598
+ if (showLabels && detailLevel>=2) {
2599
+ const subGroups = new Map();
2600
+ for (const a of MAP.assets) {
2601
+ if (!a.subDomain) continue;
2602
+ const key = a.domain+'|'+a.subDomain;
2603
+ if (!subGroups.has(key)) subGroups.set(key, []);
2604
+ subGroups.get(key).push(a);
2605
+ }
2606
+ for (const [, group] of subGroups) {
2607
+ let sx=0,sy=0;
2608
+ for (const a of group) { sx+=htp_x(a.q,a.r); sy+=htp_y(a.q,a.r); }
2609
+ const cx=sx/group.length, cy=sy/group.length;
2610
+ const s = w2s(cx, cy);
2611
+ drawPill(s.x, s.y+size*1.5, group[0].subDomain, '#64748b', 11);
2612
+ }
2613
+ }
2614
+ }
2615
+
2616
+ function drawPill(x, y, text, color, fontSize) {
2617
+ if(!text) return;
2618
+ ctx.save();
2619
+ ctx.font = '600 '+fontSize+'px -apple-system,sans-serif';
2620
+ const tw=ctx.measureText(text).width;
2621
+ const ph=fontSize+8, pw=tw+20;
2622
+ ctx.beginPath();
2623
+ if (ctx.roundRect) ctx.roundRect(x-pw/2, y-ph/2, pw, ph, ph/2);
2624
+ else { ctx.rect(x-pw/2, y-ph/2, pw, ph); }
2625
+ ctx.fillStyle = isDark ? 'rgba(30,41,59,0.9)' : 'rgba(255,255,255,0.92)';
2626
+ ctx.shadowColor='rgba(0,0,0,0.15)'; ctx.shadowBlur=6;
2627
+ ctx.fill(); ctx.shadowBlur=0;
2628
+ ctx.fillStyle = isDark ? '#e2e8f0' : '#0f172a';
2629
+ ctx.textAlign='center'; ctx.textBaseline='middle';
2630
+ ctx.fillText(text, x, y);
2631
+ ctx.restore();
2632
+ }
2633
+
2634
+ // \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
2635
+ function getAssetAt(sx, sy) {
2636
+ const w=s2w(sx,sy);
2637
+ for (const a of MAP.assets) {
2638
+ const wx=htp_x(a.q,a.r), wy=htp_y(a.q,a.r);
2639
+ const dx=Math.abs(w.x-wx), dy=Math.abs(w.y-wy);
2640
+ if (dx>HEX_SIZE||dy>HEX_SIZE) continue;
2641
+ if (dx*dx+dy*dy < HEX_SIZE*HEX_SIZE) return a;
2642
+ }
2643
+ return null;
2644
+ }
2645
+
2646
+ // \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
2647
+ function getSearchMatches() {
2648
+ if(!searchQuery) return new Set();
2649
+ const q=searchQuery.toLowerCase();
2650
+ const m=new Set();
2651
+ for(const a of MAP.assets){
2652
+ if(a.name.toLowerCase().includes(q)||(a.domain&&a.domain.toLowerCase().includes(q))||
2653
+ (a.subDomain&&a.subDomain.toLowerCase().includes(q))) m.add(a.id);
2654
+ }
2655
+ return m;
2656
+ }
2657
+
2658
+ // \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
2659
+ let dragging=false, lastMX=0, lastMY=0;
2660
+
2661
+ wrap.addEventListener('mousedown', e=>{
2662
+ if(e.button!==0)return;
2663
+ dragging=true; lastMX=e.clientX; lastMY=e.clientY;
2664
+ wrap.classList.add('dragging');
2665
+ });
2666
+ window.addEventListener('mouseup', ()=>{dragging=false;wrap.classList.remove('dragging');});
2667
+ window.addEventListener('mousemove', e=>{
2668
+ if(dragging){
2669
+ vx+=e.clientX-lastMX; vy+=e.clientY-lastMY;
2670
+ lastMX=e.clientX; lastMY=e.clientY; draw(); return;
2671
+ }
2672
+ const rect=wrap.getBoundingClientRect();
2673
+ const sx=e.clientX-rect.left, sy=e.clientY-rect.top;
2674
+ const asset=getAssetAt(sx,sy);
2675
+ const newId=asset?asset.id:null;
2676
+ if(newId!==hoveredAssetId){hoveredAssetId=newId;draw();}
2677
+ const tt=document.getElementById('tooltip');
2678
+ if(asset){
2679
+ document.getElementById('tt-name').textContent=asset.name;
2680
+ document.getElementById('tt-domain').textContent=asset.domain+(asset.subDomain?' > '+asset.subDomain:'');
2681
+ document.getElementById('tt-quality').textContent=asset.qualityScore!==null?'Quality: '+asset.qualityScore+'/100':'';
2682
+ tt.style.display='block';tt.style.left=(e.clientX+12)+'px';tt.style.top=(e.clientY-8)+'px';
2683
+ } else { tt.style.display='none'; }
2684
+ });
2685
+
2686
+ wrap.addEventListener('click', e=>{
2687
+ const rect=wrap.getBoundingClientRect();
2688
+ const sx=e.clientX-rect.left, sy=e.clientY-rect.top;
2689
+ const asset=getAssetAt(sx,sy);
2690
+ if(connectMode){
2691
+ if(!asset) return;
2692
+ if(!connectFirst){connectFirst=asset.id;draw();}
2693
+ else if(connectFirst!==asset.id){
2694
+ localConnections.push({id:crypto.randomUUID(),sourceAssetId:connectFirst,targetAssetId:asset.id,type:'connection'});
2695
+ connectFirst=null;draw();
2696
+ }
2697
+ return;
2698
+ }
2699
+ if(asset){selectedAssetId=asset.id;showDetailPanel(asset);}
2700
+ else{selectedAssetId=null;document.getElementById('detail-panel').classList.remove('open');}
2701
+ draw();
2702
+ });
2703
+
2704
+ // Touch
2705
+ let lastTouches=[];
2706
+ wrap.addEventListener('touchstart',e=>{lastTouches=[...e.touches];},{passive:true});
2707
+ wrap.addEventListener('touchmove',e=>{
2708
+ if(e.touches.length===1){
2709
+ vx+=e.touches[0].clientX-lastTouches[0].clientX;
2710
+ vy+=e.touches[0].clientY-lastTouches[0].clientY;draw();
2711
+ } else if(e.touches.length===2){
2712
+ const d0=Math.hypot(lastTouches[0].clientX-lastTouches[1].clientX,lastTouches[0].clientY-lastTouches[1].clientY);
2713
+ const d1=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY);
2714
+ const mx=(e.touches[0].clientX+e.touches[1].clientX)/2;
2715
+ const my=(e.touches[0].clientY+e.touches[1].clientY)/2;
2716
+ applyZoom(d1/d0,mx,my);
2717
+ }
2718
+ lastTouches=[...e.touches];
2719
+ },{passive:true});
2720
+
2721
+ wrap.addEventListener('wheel',e=>{
2722
+ e.preventDefault();
2723
+ const rect=wrap.getBoundingClientRect();
2724
+ applyZoom(e.deltaY<0?1.12:1/1.12,e.clientX-rect.left,e.clientY-rect.top);
2725
+ },{passive:false});
2726
+
2727
+ function applyZoom(factor,sx,sy){
2728
+ const ns=Math.max(0.05,Math.min(8,scale*factor));
2729
+ const wx=(sx-vx)/scale,wy=(sy-vy)/scale;
2730
+ scale=ns;vx=sx-wx*scale;vy=sy-wy*scale;
2731
+ document.getElementById('zoom-pct').textContent=Math.round(scale*100)+'%';draw();
2732
+ }
2733
+ document.getElementById('zoom-in').addEventListener('click',()=>applyZoom(1.25,W/2,H/2));
2734
+ document.getElementById('zoom-out').addEventListener('click',()=>applyZoom(1/1.25,W/2,H/2));
2735
+
2736
+ // Keyboard
2737
+ wrap.addEventListener('keydown',e=>{
2738
+ const step=40;
2739
+ if(e.key==='ArrowLeft'){vx+=step;draw();}
2740
+ else if(e.key==='ArrowRight'){vx-=step;draw();}
2741
+ else if(e.key==='ArrowUp'){vy+=step;draw();}
2742
+ else if(e.key==='ArrowDown'){vy-=step;draw();}
2743
+ else if(e.key==='+'||e.key==='=')applyZoom(1.2,W/2,H/2);
2744
+ else if(e.key==='-')applyZoom(1/1.2,W/2,H/2);
2745
+ else if(e.key==='Escape'){
2746
+ selectedAssetId=null;document.getElementById('detail-panel').classList.remove('open');
2747
+ if(connectMode)toggleConnect();draw();
2748
+ }
2749
+ });
2750
+
2751
+ // \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
2752
+ function showDetailPanel(asset) {
2753
+ document.getElementById('dp-name').textContent=asset.name;
2754
+ const body=document.getElementById('dp-body');
2755
+ const rows=[['Domain',asset.domain],['Sub-domain',asset.subDomain],
2756
+ ['Quality Score',asset.qualityScore!==null?renderQuality(asset.qualityScore):null],
2757
+ ...Object.entries(asset.metadata||{}).slice(0,8).map(([k,v])=>[k,String(v)])
2758
+ ].filter(([,v])=>v!==null&&v!==undefined&&v!=='');
2759
+ 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('');
2760
+ const related=localConnections.filter(c=>c.sourceAssetId===asset.id||c.targetAssetId===asset.id);
2761
+ if(related.length>0){
2762
+ body.innerHTML+='<div class="meta-row"><div class="meta-label">Connections ('+related.length+')</div><div>'+
2763
+ related.map(c=>{const oid=c.sourceAssetId===asset.id?c.targetAssetId:c.sourceAssetId;
2764
+ 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>';
2765
+ }
2766
+ document.getElementById('detail-panel').classList.add('open');
2767
+ }
2768
+ function renderQuality(s){
2769
+ const c=s>=70?'#22c55e':s>=40?'#f97316':'#ef4444';
2770
+ return s+'/100 <div class="quality-bar"><div class="quality-fill" style="width:'+s+'%;background:'+c+'"></div></div>';
2771
+ }
2772
+ function esc(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
2773
+ document.getElementById('dp-close').addEventListener('click',()=>{
2774
+ document.getElementById('detail-panel').classList.remove('open');selectedAssetId=null;draw();
2775
+ });
2776
+
2777
+ // \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
2778
+ [1,2,3,4].forEach(n=>{
2779
+ document.getElementById('dl-'+n).addEventListener('click',()=>{
2780
+ detailLevel=n;document.querySelectorAll('.detail-btn').forEach(b=>b.classList.remove('active'));
2781
+ document.getElementById('dl-'+n).classList.add('active');draw();
2782
+ });
2783
+ });
2784
+ document.getElementById('btn-labels').addEventListener('click',()=>{
2785
+ showLabels=!showLabels;document.getElementById('btn-labels').classList.toggle('active',showLabels);draw();
2786
+ });
2787
+ document.getElementById('btn-quality').addEventListener('click',()=>{
2788
+ showQuality=!showQuality;document.getElementById('btn-quality').classList.toggle('active',showQuality);draw();
2789
+ });
2790
+ function toggleConnect(){
2791
+ connectMode=!connectMode;connectFirst=null;
2792
+ document.getElementById('connect-btn').classList.toggle('active',connectMode);
2793
+ wrap.classList.toggle('connecting',connectMode);
2794
+ document.getElementById('connect-hint').style.display=connectMode?'block':'none';draw();
2795
+ }
2796
+ document.getElementById('connect-btn').addEventListener('click',toggleConnect);
2797
+ document.getElementById('theme-btn').addEventListener('click',()=>{
2798
+ isDark=!isDark;
2799
+ document.body.classList.toggle('dark',isDark);document.body.classList.toggle('light',!isDark);
2800
+ document.getElementById('theme-btn').innerHTML=isDark?'&#9788;':'&#9790;';draw();
2801
+ });
2802
+ document.getElementById('search-input').addEventListener('input',e=>{searchQuery=e.target.value.trim();draw();});
2803
+
2804
+ // \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
2805
+ resize(); fitToView();
2806
+ document.getElementById('zoom-pct').textContent=Math.round(scale*100)+'%';
2807
+ draw();
2808
+ })();
2809
+ </script>
2810
+ </body>
2811
+ </html>`;
2812
+ }
2813
+ function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml", "html", "map", "sops"]) {
1884
2814
  mkdirSync2(outputDir, { recursive: true });
1885
2815
  mkdirSync2(join2(outputDir, "sops"), { recursive: true });
1886
2816
  mkdirSync2(join2(outputDir, "workflows"), { recursive: true });
@@ -1903,6 +2833,10 @@ function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml
1903
2833
  writeFileSync(join2(outputDir, "topology.html"), exportHTML(nodes, edges));
1904
2834
  process.stderr.write("\u2713 topology.html\n");
1905
2835
  }
2836
+ if (formats.includes("map")) {
2837
+ writeFileSync(join2(outputDir, "cartography-map.html"), exportCartographyMap(nodes, edges));
2838
+ process.stderr.write("\u2713 cartography-map.html\n");
2839
+ }
1906
2840
  if (formats.includes("sops")) {
1907
2841
  const sops = db.getSOPs(sessionId);
1908
2842
  for (const sop of sops) {
@@ -1919,9 +2853,10 @@ function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml
1919
2853
  }
1920
2854
 
1921
2855
  // src/cli.ts
1922
- import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
2856
+ import { readFileSync as readFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
1923
2857
  import { resolve } from "path";
1924
2858
  import { createInterface } from "readline";
2859
+ import { execSync as execSync2 } from "child_process";
1925
2860
  var bold = (s) => `\x1B[1m${s}\x1B[0m`;
1926
2861
  var dim = (s) => `\x1B[2m${s}\x1B[0m`;
1927
2862
  var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
@@ -1934,7 +2869,7 @@ function main() {
1934
2869
  const program = new Command();
1935
2870
  const CMD = "datasynx-cartography";
1936
2871
  const VERSION = "0.2.3";
1937
- program.name(CMD).description("AI-powered Infrastructure Cartography").version(VERSION);
2872
+ program.name(CMD).description("AI-powered Infrastructure Cartography & SOP Generation").version(VERSION);
1938
2873
  program.command("discover").description("Scan and map your infrastructure").option("--entry <hosts...>", "Entry points", ["localhost"]).option("--depth <n>", "Max crawl depth", "8").option("--max-turns <n>", "Max agent turns", "50").option("--model <m>", "Agent model", "claude-sonnet-4-5-20250929").option("--org <name>", "Organization name (for Backstage)").option("-o, --output <dir>", "Output directory", "./datasynx-output").option("--db <path>", "DB path").option("-v, --verbose", "Show agent reasoning", false).action(async (opts) => {
1939
2874
  checkPrerequisites();
1940
2875
  const config = defaultConfig({
@@ -2146,23 +3081,22 @@ function main() {
2146
3081
  exportAll(db, sessionId, config.outputDir);
2147
3082
  const osc8 = (url, label) => `\x1B]8;;${url}\x1B\\${label}\x1B]8;;\x1B\\`;
2148
3083
  const htmlPath = resolve(config.outputDir, "topology.html");
3084
+ const mapPath = resolve(config.outputDir, "cartography-map.html");
2149
3085
  const topoPath = resolve(config.outputDir, "topology.mermaid");
2150
3086
  w("\n");
2151
- if (existsSync2(htmlPath)) {
2152
- const fileUrl = `file://${htmlPath}`;
2153
- w(` ${green("\u2192")} ${osc8(fileUrl, bold("Open topology.html"))}
3087
+ if (existsSync2(mapPath)) {
3088
+ w(` ${green("\u2192")} ${osc8(`file://${mapPath}`, bold("Open cartography-map.html"))} ${dim("\u2190 Hex Map")}
2154
3089
  `);
2155
- w(` ${dim(fileUrl)}
3090
+ }
3091
+ if (existsSync2(htmlPath)) {
3092
+ w(` ${green("\u2192")} ${osc8(`file://${htmlPath}`, bold("Open topology.html"))}
2156
3093
  `);
2157
3094
  }
2158
3095
  if (existsSync2(topoPath)) {
2159
3096
  try {
2160
3097
  const code = readFileSync2(topoPath, "utf8");
2161
- const b64 = Buffer.from(JSON.stringify({ code, mermaid: { theme: "dark" } })).toString("base64url");
2162
- const liveUrl = `https://mermaid.live/view#base64:${b64}`;
2163
- w(` ${cyan("\u2192")} ${osc8(liveUrl, bold("Open in mermaid.live"))}
2164
- `);
2165
- w(` ${dim(liveUrl)}
3098
+ const b64 = Buffer.from(JSON.stringify({ code, mermaid: { theme: "dark" } })).toString("base64");
3099
+ w(` ${cyan("\u2192")} ${osc8(`https://mermaid.live/view#base64:${b64}`, bold("Open in mermaid.live"))}
2166
3100
  `);
2167
3101
  } catch {
2168
3102
  }
@@ -2217,7 +3151,7 @@ function main() {
2217
3151
  }
2218
3152
  db.close();
2219
3153
  });
2220
- program.command("export [session-id]").description("Generate all output files").option("-o, --output <dir>", "Output directory", "./datasynx-output").option("--format <fmt...>", "Formats: mermaid,json,yaml,html,sops").action((sessionId, opts) => {
3154
+ program.command("export [session-id]").description("Generate all output files").option("-o, --output <dir>", "Output directory", "./datasynx-output").option("--format <fmt...>", "Formats: mermaid,json,yaml,html,map,sops").action((sessionId, opts) => {
2221
3155
  const config = defaultConfig({ outputDir: opts.output });
2222
3156
  const db = new CartographyDB(config.dbPath);
2223
3157
  const session = sessionId ? db.getSession(sessionId) : db.getLatestSession();
@@ -2227,12 +3161,43 @@ function main() {
2227
3161
  process.exitCode = 1;
2228
3162
  return;
2229
3163
  }
2230
- const formats = opts.format ?? ["mermaid", "json", "yaml", "html", "sops"];
3164
+ const formats = opts.format ?? ["mermaid", "json", "yaml", "html", "map", "sops"];
2231
3165
  exportAll(db, session.id, opts.output, formats);
2232
3166
  process.stderr.write(`\u2713 Exported to: ${opts.output}
2233
3167
  `);
2234
3168
  db.close();
2235
3169
  });
3170
+ program.command("map [session-id]").description("Open the interactive Data Cartography hex map in your browser").option("-o, --output <dir>", "Output directory", "./datasynx-output").option("--theme <theme>", "Theme: light or dark", "light").action((sessionId, opts) => {
3171
+ const config = defaultConfig({ outputDir: opts.output });
3172
+ const db = new CartographyDB(config.dbPath);
3173
+ const session = sessionId ? db.getSession(sessionId) : db.getLatestSession();
3174
+ if (!session) {
3175
+ process.stderr.write("No session found. Run discover first.\n");
3176
+ db.close();
3177
+ process.exitCode = 1;
3178
+ return;
3179
+ }
3180
+ const nodes = db.getNodes(session.id);
3181
+ const edges = db.getEdges(session.id);
3182
+ const outDir = resolve(opts.output);
3183
+ mkdirSync3(outDir, { recursive: true });
3184
+ const outPath = resolve(outDir, "cartography-map.html");
3185
+ writeFileSync2(outPath, exportCartographyMap(nodes, edges, { theme: opts.theme }));
3186
+ db.close();
3187
+ const osc8 = (url, label) => `\x1B]8;;${url}\x1B\\${label}\x1B]8;;\x1B\\`;
3188
+ const fileUrl = `file://${outPath}`;
3189
+ process.stderr.write(`
3190
+ ${green("OK")} ${osc8(fileUrl, bold("Open cartography-map.html"))}
3191
+ `);
3192
+ process.stderr.write(` ${dim(fileUrl)}
3193
+
3194
+ `);
3195
+ try {
3196
+ const cmd = process.platform === "darwin" ? `open "${outPath}"` : process.platform === "win32" ? `start "" "${outPath}"` : `xdg-open "${outPath}"`;
3197
+ execSync2(cmd, { stdio: "ignore" });
3198
+ } catch {
3199
+ }
3200
+ });
2236
3201
  program.command("show [session-id]").description("Show session details").action((sessionId) => {
2237
3202
  const config = defaultConfig();
2238
3203
  const db = new CartographyDB(config.dbPath);
@@ -2476,6 +3441,7 @@ ${infraSummary.substring(0, 12e3)}`;
2476
3441
  out(dim(" topology.mermaid Infrastructure topology (graph TB)\n"));
2477
3442
  out(dim(" dependencies.mermaid Service dependencies (graph LR)\n"));
2478
3443
  out(dim(" topology.html Interactive D3.js force graph\n"));
3444
+ out(dim(" cartography-map.html Hex grid data cartography map\n"));
2479
3445
  out(dim(" sops/ Generated SOPs as Markdown\n"));
2480
3446
  out(dim(" workflows/ Workflow flowcharts as Mermaid\n"));
2481
3447
  out("\n");
@@ -2617,7 +3583,7 @@ ${infraSummary.substring(0, 12e3)}`;
2617
3583
  `);
2618
3584
  return;
2619
3585
  }
2620
- const { NODE_TYPES: NODE_TYPES2 } = await import("./types-ROE3Z6HY.js");
3586
+ const { NODE_TYPES: NODE_TYPES2 } = await import("./types-ZD6G5JKR.js");
2621
3587
  if (!process.stdin.isTTY) {
2622
3588
  w(red("\n \u2717 Interactive mode requires a terminal (use --file for non-interactive)\n\n"));
2623
3589
  process.exitCode = 1;
@@ -2692,7 +3658,7 @@ ${infraSummary.substring(0, 12e3)}`;
2692
3658
  `);
2693
3659
  });
2694
3660
  program.command("doctor").description("Check all requirements and cloud CLIs").action(async () => {
2695
- const { execSync: execSync2 } = await import("child_process");
3661
+ const { execSync: execSync3 } = await import("child_process");
2696
3662
  const { existsSync: existsSync3, readFileSync: readFileSync3 } = await import("fs");
2697
3663
  const { join: join3 } = await import("path");
2698
3664
  const out = (s) => process.stdout.write(s);
@@ -2715,7 +3681,7 @@ ${infraSummary.substring(0, 12e3)}`;
2715
3681
  allGood = false;
2716
3682
  }
2717
3683
  try {
2718
- const v = execSync2("claude --version", { stdio: "pipe" }).toString().trim();
3684
+ const v = execSync3("claude --version", { stdio: "pipe" }).toString().trim();
2719
3685
  ok(`Claude CLI ${dim2(v)}`);
2720
3686
  } catch {
2721
3687
  err("Claude CLI not found \u2014 npm i -g @anthropic-ai/claude-code");
@@ -2739,7 +3705,7 @@ ${infraSummary.substring(0, 12e3)}`;
2739
3705
  allGood = false;
2740
3706
  }
2741
3707
  try {
2742
- const v = execSync2("kubectl version --client --short 2>/dev/null || kubectl version --client", { stdio: "pipe" }).toString().split("\n")[0]?.trim() ?? "";
3708
+ const v = execSync3("kubectl version --client --short 2>/dev/null || kubectl version --client", { stdio: "pipe" }).toString().split("\n")[0]?.trim() ?? "";
2743
3709
  ok(`kubectl ${dim2(v || "(client OK)")}`);
2744
3710
  } catch {
2745
3711
  warn(`kubectl not found ${dim2("\u2014 install: https://kubernetes.io/docs/tasks/tools/")}`);
@@ -2751,7 +3717,7 @@ ${infraSummary.substring(0, 12e3)}`;
2751
3717
  ];
2752
3718
  for (const [name, cmd, hint] of cloudClis) {
2753
3719
  try {
2754
- execSync2(cmd, { stdio: "pipe" });
3720
+ execSync3(cmd, { stdio: "pipe" });
2755
3721
  ok(`${name} ${dim2("(cloud scanning available)")}`);
2756
3722
  } catch {
2757
3723
  warn(`${name} not found ${dim2("\u2014 cloud scan skipped | " + hint)}`);
@@ -2763,7 +3729,7 @@ ${infraSummary.substring(0, 12e3)}`;
2763
3729
  ];
2764
3730
  for (const [name, cmd] of localTools) {
2765
3731
  try {
2766
- execSync2(cmd, { stdio: "pipe" });
3732
+ execSync3(cmd, { stdio: "pipe" });
2767
3733
  ok(`${name} ${dim2("(discovery tool)")}`);
2768
3734
  } catch {
2769
3735
  warn(`${name} not found ${dim2("\u2014 discovery without " + name + " will be limited")}`);