@datasynx/agentic-ai-cartography 0.6.0 → 0.7.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
@@ -675,14 +675,14 @@ async function createCartographyTools(db, sessionId, opts = {}) {
675
675
  tool("scan_local_databases", "Scan for local database files and running DB servers \u2014 PostgreSQL databases, MySQL, SQLite files from installed apps", {
676
676
  deep: z.boolean().default(false).optional().describe("Also search home directory recursively for SQLite/DB files (slower)")
677
677
  }, async (args) => {
678
- const { execSync: execSync3 } = await import("child_process");
678
+ const { execSync: execSync2 } = await import("child_process");
679
679
  const { homedir } = await import("os");
680
680
  const { existsSync: existsSync3 } = await import("fs");
681
681
  const deep = args["deep"] ?? false;
682
682
  const HOME = homedir();
683
683
  const run = (cmd) => {
684
684
  try {
685
- return execSync3(cmd, { stdio: "pipe", timeout: 1e4, shell: "/bin/sh" }).toString().trim();
685
+ return execSync2(cmd, { stdio: "pipe", timeout: 1e4, shell: "/bin/sh" }).toString().trim();
686
686
  } catch {
687
687
  return "";
688
688
  }
@@ -709,12 +709,12 @@ ${v}`).join("\n\n");
709
709
  tool("scan_k8s_resources", "Scan Kubernetes cluster via kubectl \u2014 100% readonly (get, describe)", {
710
710
  namespace: z.string().optional().describe("Filter by namespace \u2014 empty = all namespaces")
711
711
  }, async (args) => {
712
- const { execSync: execSync3 } = await import("child_process");
712
+ const { execSync: execSync2 } = await import("child_process");
713
713
  const ns = args["namespace"];
714
714
  const nsFlag = ns ? `-n ${ns}` : "--all-namespaces";
715
715
  const run = (cmd) => {
716
716
  try {
717
- return execSync3(cmd, { stdio: "pipe", timeout: 15e3, shell: "/bin/sh" }).toString().trim();
717
+ return execSync2(cmd, { stdio: "pipe", timeout: 15e3, shell: "/bin/sh" }).toString().trim();
718
718
  } catch (e) {
719
719
  return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
720
720
  }
@@ -738,7 +738,7 @@ ${run(c)}`).join("\n\n");
738
738
  region: z.string().optional().describe("AWS Region \u2014 default: AWS_DEFAULT_REGION or profile"),
739
739
  profile: z.string().optional().describe("AWS CLI profile")
740
740
  }, async (args) => {
741
- const { execSync: execSync3 } = await import("child_process");
741
+ const { execSync: execSync2 } = await import("child_process");
742
742
  const region = args["region"];
743
743
  const profile = args["profile"];
744
744
  const env = { ...process.env };
@@ -746,7 +746,7 @@ ${run(c)}`).join("\n\n");
746
746
  const pf = profile ? `--profile ${profile}` : "";
747
747
  const run = (cmd) => {
748
748
  try {
749
- return execSync3(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh", env }).toString().trim();
749
+ return execSync2(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh", env }).toString().trim();
750
750
  } catch (e) {
751
751
  return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
752
752
  }
@@ -768,12 +768,12 @@ ${run(c)}`).join("\n\n");
768
768
  tool("scan_gcp_resources", "Scan Google Cloud Platform via gcloud CLI \u2014 100% readonly (list, describe)", {
769
769
  project: z.string().optional().describe("GCP Project ID \u2014 default: current gcloud project")
770
770
  }, async (args) => {
771
- const { execSync: execSync3 } = await import("child_process");
771
+ const { execSync: execSync2 } = await import("child_process");
772
772
  const project = args["project"];
773
773
  const pf = project ? `--project ${project}` : "";
774
774
  const run = (cmd) => {
775
775
  try {
776
- return execSync3(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
776
+ return execSync2(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
777
777
  } catch (e) {
778
778
  return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
779
779
  }
@@ -797,14 +797,14 @@ ${run(c)}`).join("\n\n");
797
797
  subscription: z.string().optional().describe("Azure Subscription ID"),
798
798
  resourceGroup: z.string().optional().describe("Filter by resource group")
799
799
  }, async (args) => {
800
- const { execSync: execSync3 } = await import("child_process");
800
+ const { execSync: execSync2 } = await import("child_process");
801
801
  const sub = args["subscription"];
802
802
  const rg = args["resourceGroup"];
803
803
  const sf = sub ? `--subscription ${sub}` : "";
804
804
  const rf = rg ? `--resource-group ${rg}` : "";
805
805
  const run = (cmd) => {
806
806
  try {
807
- return execSync3(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
807
+ return execSync2(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
808
808
  } catch (e) {
809
809
  return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
810
810
  }
@@ -827,11 +827,11 @@ ${run(c)}`).join("\n\n");
827
827
  tool("scan_installed_apps", "Scan all installed apps and tools \u2014 IDEs, office, dev tools, business apps, databases", {
828
828
  searchHint: z.string().optional().describe('Optional search term to find specific tools (e.g. "hubspot windsurf cursor")')
829
829
  }, async (args) => {
830
- const { execSync: execSync3 } = await import("child_process");
830
+ const { execSync: execSync2 } = await import("child_process");
831
831
  const hint = args["searchHint"];
832
832
  const run = (cmd) => {
833
833
  try {
834
- return execSync3(cmd, { stdio: "pipe", timeout: 15e3, shell: "/bin/sh" }).toString().trim();
834
+ return execSync2(cmd, { stdio: "pipe", timeout: 15e3, shell: "/bin/sh" }).toString().trim();
835
835
  } catch {
836
836
  return "";
837
837
  }
@@ -1630,525 +1630,6 @@ function exportJSON(db, sessionId) {
1630
1630
  sops
1631
1631
  }, null, 2);
1632
1632
  }
1633
- function exportHTML(nodes, edges) {
1634
- const graphData = JSON.stringify({
1635
- nodes: nodes.map((n) => ({
1636
- id: n.id,
1637
- name: n.name,
1638
- type: n.type,
1639
- layer: nodeLayer(n.type),
1640
- confidence: n.confidence,
1641
- discoveredVia: n.discoveredVia,
1642
- discoveredAt: n.discoveredAt,
1643
- tags: n.tags,
1644
- metadata: n.metadata
1645
- })),
1646
- links: edges.map((e) => ({
1647
- source: e.sourceId,
1648
- target: e.targetId,
1649
- relationship: e.relationship,
1650
- confidence: e.confidence,
1651
- evidence: e.evidence
1652
- }))
1653
- });
1654
- return `<!DOCTYPE html>
1655
- <html lang="en">
1656
- <head>
1657
- <meta charset="UTF-8">
1658
- <title>Cartography \u2014 Infrastructure Map</title>
1659
- <script src="https://d3js.org/d3.v7.min.js"></script>
1660
- <style>
1661
- * { box-sizing: border-box; margin: 0; padding: 0; }
1662
- body { background: #0a0e14; color: #e6edf3; font-family: 'SF Mono','Fira Code','Cascadia Code',monospace; display: flex; overflow: hidden; height: 100vh; }
1663
-
1664
- /* \u2500\u2500 Left node 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 */
1665
- #node-panel {
1666
- width: 220px; min-width: 220px; height: 100vh; overflow: hidden;
1667
- background: #0d1117; border-right: 1px solid #1b2028;
1668
- display: flex; flex-direction: column;
1669
- }
1670
- #node-panel-header {
1671
- padding: 10px 12px 8px; border-bottom: 1px solid #1b2028;
1672
- font-size: 11px; color: #6e7681; text-transform: uppercase; letter-spacing: 0.6px;
1673
- }
1674
- #node-search {
1675
- width: calc(100% - 16px); margin: 8px; padding: 5px 8px;
1676
- background: #161b22; border: 1px solid #30363d; border-radius: 5px;
1677
- color: #e6edf3; font-size: 11px; font-family: inherit; outline: none;
1678
- }
1679
- #node-search:focus { border-color: #58a6ff; }
1680
- #node-list { flex: 1; overflow-y: auto; padding-bottom: 8px; }
1681
- .node-list-item {
1682
- padding: 5px 12px; cursor: pointer; font-size: 11px;
1683
- display: flex; align-items: center; gap: 6px; border-left: 2px solid transparent;
1684
- }
1685
- .node-list-item:hover { background: #161b22; }
1686
- .node-list-item.active { background: #1a2436; border-left-color: #58a6ff; }
1687
- .node-list-dot { width: 7px; height: 7px; border-radius: 2px; flex-shrink: 0; }
1688
- .node-list-name { color: #c9d1d9; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
1689
- .node-list-type { color: #484f58; font-size: 9px; flex-shrink: 0; }
1690
-
1691
- /* \u2500\u2500 Center graph \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1692
- #graph { flex: 1; height: 100vh; position: relative; }
1693
- svg { width: 100%; height: 100%; }
1694
- .hull { opacity: 0.12; stroke-width: 1.5; stroke-opacity: 0.25; }
1695
- .hull-label { font-size: 13px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; fill-opacity: 0.5; pointer-events: none; }
1696
- .link { stroke-opacity: 0.4; }
1697
- .link-label { font-size: 8px; fill: #6e7681; pointer-events: none; opacity: 0; }
1698
- .node-hex { stroke-width: 1.8; cursor: pointer; transition: opacity 0.15s; }
1699
- .node-hex:hover { filter: brightness(1.3); stroke-width: 3; }
1700
- .node-hex.selected { stroke-width: 3.5; filter: brightness(1.5); }
1701
- .node-label { font-size: 10px; fill: #c9d1d9; pointer-events: none; opacity: 0; }
1702
-
1703
- /* \u2500\u2500 Right sidebar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1704
- #sidebar {
1705
- width: 300px; min-width: 300px; height: 100vh; overflow-y: auto;
1706
- background: #0d1117; border-left: 1px solid #1b2028;
1707
- padding: 16px; font-size: 12px; line-height: 1.6;
1708
- }
1709
- #sidebar h2 { margin: 0 0 8px; font-size: 14px; color: #58a6ff; }
1710
- #sidebar .meta-table { width: 100%; border-collapse: collapse; }
1711
- #sidebar .meta-table td { padding: 3px 6px; border-bottom: 1px solid #161b22; vertical-align: top; }
1712
- #sidebar .meta-table td:first-child { color: #6e7681; white-space: nowrap; width: 90px; }
1713
- #sidebar .tag { display: inline-block; background: #161b22; border-radius: 3px; padding: 1px 5px; margin: 1px; font-size: 10px; }
1714
- #sidebar .conf-bar { height: 5px; border-radius: 3px; background: #161b22; margin-top: 3px; }
1715
- #sidebar .conf-fill { height: 100%; border-radius: 3px; }
1716
- #sidebar .edges-list { margin-top: 12px; }
1717
- #sidebar .edge-item { padding: 4px 0; border-bottom: 1px solid #161b22; color: #6e7681; font-size: 11px; }
1718
- #sidebar .edge-item span { color: #c9d1d9; }
1719
- #sidebar .action-row { display: flex; gap: 6px; margin-top: 14px; }
1720
- .btn-delete {
1721
- flex: 1; padding: 6px 10px; background: transparent; border: 1px solid #6e191d;
1722
- color: #f85149; border-radius: 5px; font-size: 11px; font-family: inherit;
1723
- cursor: pointer; text-align: center;
1724
- }
1725
- .btn-delete:hover { background: #3d0c0c; }
1726
- .hint { color: #3d434b; font-size: 11px; margin-top: 8px; }
1727
-
1728
- /* \u2500\u2500 HUD \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1729
- #hud { position: absolute; top: 10px; left: 10px; background: rgba(10,14,20,0.88);
1730
- padding: 10px 14px; border-radius: 8px; font-size: 12px; border: 1px solid #1b2028; pointer-events: none; }
1731
- #hud strong { color: #58a6ff; }
1732
- #hud .stats { color: #6e7681; }
1733
- #hud .zoom-level { color: #3d434b; font-size: 10px; margin-top: 2px; }
1734
-
1735
- /* \u2500\u2500 Toolbar (filters + JGF export) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1736
- #toolbar { position: absolute; top: 10px; right: 10px; display: flex; flex-wrap: wrap; gap: 4px; pointer-events: auto; align-items: center; }
1737
- .filter-btn {
1738
- background: rgba(10,14,20,0.85); border: 1px solid #1b2028; border-radius: 6px;
1739
- color: #c9d1d9; padding: 4px 10px; font-size: 11px; cursor: pointer;
1740
- font-family: inherit; display: flex; align-items: center; gap: 5px;
1741
- }
1742
- .filter-btn:hover { border-color: #30363d; }
1743
- .filter-btn.off { opacity: 0.35; }
1744
- .filter-dot { width: 8px; height: 8px; border-radius: 2px; display: inline-block; }
1745
- .export-btn {
1746
- background: rgba(10,14,20,0.85); border: 1px solid #1b2028; border-radius: 6px;
1747
- color: #58a6ff; padding: 4px 12px; font-size: 11px; cursor: pointer;
1748
- font-family: inherit;
1749
- }
1750
- .export-btn:hover { border-color: #58a6ff; background: rgba(88,166,255,0.08); }
1751
- </style>
1752
- </head>
1753
- <body>
1754
-
1755
- <!-- Left: node list panel -->
1756
- <div id="node-panel">
1757
- <div id="node-panel-header">Nodes (${nodes.length})</div>
1758
- <input id="node-search" type="text" placeholder="Search nodes\u2026" autocomplete="off" spellcheck="false">
1759
- <div id="node-list"></div>
1760
- </div>
1761
-
1762
- <!-- Center: graph -->
1763
- <div id="graph">
1764
- <div id="hud">
1765
- <strong>Cartography</strong> &nbsp;
1766
- <span class="stats" id="hud-stats">${nodes.length} nodes \xB7 ${edges.length} edges</span><br>
1767
- <span class="zoom-level">Scroll = zoom \xB7 Drag = pan \xB7 Click = details</span>
1768
- </div>
1769
- <div id="toolbar"></div>
1770
- <svg></svg>
1771
- </div>
1772
-
1773
- <!-- Right: detail sidebar -->
1774
- <div id="sidebar">
1775
- <h2>Infrastructure Map</h2>
1776
- <p class="hint">Click a node to view details.</p>
1777
- </div>
1778
-
1779
- <script>
1780
- const data = ${graphData};
1781
-
1782
- // \u2500\u2500 Color palette per node type \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1783
- const TYPE_COLORS = {
1784
- host: '#4a9eff', database_server: '#ff6b6b', database: '#ff8c42',
1785
- web_service: '#6bcb77', api_endpoint: '#4d96ff', cache_server: '#ffd93d',
1786
- message_broker: '#c77dff', queue: '#e0aaff', topic: '#9d4edd',
1787
- container: '#48cae4', pod: '#00b4d8', k8s_cluster: '#0077b6',
1788
- config_file: '#adb5bd', saas_tool: '#c084fc', table: '#f97316', unknown: '#6c757d',
1789
- };
1790
-
1791
- const LAYER_COLORS = {
1792
- saas: '#c084fc', web: '#6bcb77', data: '#ff6b6b',
1793
- messaging: '#c77dff', infra: '#4a9eff', config: '#adb5bd', other: '#6c757d',
1794
- };
1795
- const LAYER_NAMES = {
1796
- saas: 'SaaS Tools', web: 'Web / API', data: 'Data Layer',
1797
- messaging: 'Messaging', infra: 'Infrastructure', config: 'Config', other: 'Other',
1798
- };
1799
-
1800
- // \u2500\u2500 Hexagon path \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1801
- const HEX_SIZE = { saas_tool: 16, host: 18, database_server: 18, k8s_cluster: 20, default: 14 };
1802
- function hexSize(d) { return HEX_SIZE[d.type] || HEX_SIZE.default; }
1803
- function hexPath(size) {
1804
- const pts = [];
1805
- for (let i = 0; i < 6; i++) {
1806
- const angle = (Math.PI / 3) * i - Math.PI / 6;
1807
- pts.push([size * Math.cos(angle), size * Math.sin(angle)]);
1808
- }
1809
- return 'M' + pts.map(p => p.join(',')).join('L') + 'Z';
1810
- }
1811
-
1812
- // \u2500\u2500 Left 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
1813
- const nodeListEl = document.getElementById('node-list');
1814
- const nodeSearchEl = document.getElementById('node-search');
1815
- let selectedNodeId = null;
1816
-
1817
- function buildNodeList(filter) {
1818
- const q = (filter || '').toLowerCase();
1819
- nodeListEl.innerHTML = '';
1820
- const sorted = [...data.nodes].sort((a, b) => a.name.localeCompare(b.name));
1821
- for (const d of sorted) {
1822
- if (q && !d.name.toLowerCase().includes(q) && !d.type.includes(q) && !d.id.toLowerCase().includes(q)) continue;
1823
- const item = document.createElement('div');
1824
- item.className = 'node-list-item' + (d.id === selectedNodeId ? ' active' : '');
1825
- item.dataset.id = d.id;
1826
- const color = TYPE_COLORS[d.type] || '#aaa';
1827
- item.innerHTML = \`<span class="node-list-dot" style="background:\${color}"></span>
1828
- <span class="node-list-name" title="\${d.id}">\${d.name}</span>
1829
- <span class="node-list-type">\${d.type.replace(/_/g,' ')}</span>\`;
1830
- item.onclick = () => { selectNode(d); focusNode(d); };
1831
- nodeListEl.appendChild(item);
1832
- }
1833
- }
1834
-
1835
- nodeSearchEl.addEventListener('input', e => buildNodeList(e.target.value));
1836
-
1837
- // \u2500\u2500 Sidebar detail view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1838
- const sidebar = document.getElementById('sidebar');
1839
-
1840
- function selectNode(d) {
1841
- selectedNodeId = d.id;
1842
- buildNodeList(nodeSearchEl.value);
1843
- showNode(d);
1844
- // highlight hex
1845
- d3.selectAll('.node-hex').classed('selected', nd => nd.id === d.id);
1846
- }
1847
-
1848
- function showNode(d) {
1849
- const c = TYPE_COLORS[d.type] || '#aaa';
1850
- const confPct = Math.round(d.confidence * 100);
1851
- const tags = (d.tags || []).map(t => \`<span class="tag">\${t}</span>\`).join('');
1852
- const metaRows = Object.entries(d.metadata || {})
1853
- .filter(([,v]) => v !== null && v !== undefined && String(v).length > 0)
1854
- .map(([k,v]) => \`<tr><td>\${k}</td><td>\${JSON.stringify(v)}</td></tr>\`)
1855
- .join('');
1856
- const related = data.links.filter(l =>
1857
- (l.source.id||l.source) === d.id || (l.target.id||l.target) === d.id
1858
- );
1859
- const edgeItems = related.map(l => {
1860
- const isOut = (l.source.id||l.source) === d.id;
1861
- const other = isOut ? (l.target.id||l.target) : (l.source.id||l.source);
1862
- return \`<div class="edge-item">\${isOut ? '\u2192' : '\u2190'} <span>\${other}</span> <small>[\${l.relationship}]</small></div>\`;
1863
- }).join('');
1864
-
1865
- sidebar.innerHTML = \`
1866
- <h2>\${d.name}</h2>
1867
- <table class="meta-table">
1868
- <tr><td>ID</td><td style="font-size:10px;word-break:break-all">\${d.id}</td></tr>
1869
- <tr><td>Type</td><td><span style="color:\${c}">\${d.type}</span></td></tr>
1870
- <tr><td>Layer</td><td>\${d.layer}</td></tr>
1871
- <tr><td>Confidence</td><td>
1872
- \${confPct}%
1873
- <div class="conf-bar"><div class="conf-fill" style="width:\${confPct}%;background:\${c}"></div></div>
1874
- </td></tr>
1875
- <tr><td>Discovered via</td><td>\${d.discoveredVia || '\u2014'}</td></tr>
1876
- <tr><td>Timestamp</td><td>\${d.discoveredAt ? d.discoveredAt.substring(0,19).replace('T',' ') : '\u2014'}</td></tr>
1877
- \${tags ? '<tr><td>Tags</td><td>'+tags+'</td></tr>' : ''}
1878
- \${metaRows}
1879
- </table>
1880
- \${related.length > 0 ? '<div class="edges-list"><strong>Connections (' + related.length + '):</strong>'+edgeItems+'</div>' : ''}
1881
- <div class="action-row">
1882
- <button class="btn-delete" onclick="deleteNode('\${d.id}')">\u{1F5D1} Delete node</button>
1883
- </div>
1884
- \`;
1885
- }
1886
-
1887
- // \u2500\u2500 Delete node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1888
- function deleteNode(id) {
1889
- const idx = data.nodes.findIndex(n => n.id === id);
1890
- if (idx === -1) return;
1891
- data.nodes.splice(idx, 1);
1892
- data.links = data.links.filter(l =>
1893
- (l.source.id || l.source) !== id && (l.target.id || l.target) !== id
1894
- );
1895
- selectedNodeId = null;
1896
- sidebar.innerHTML = '<h2>Infrastructure Map</h2><p class="hint">Node deleted.</p>';
1897
- document.getElementById('hud-stats').textContent =
1898
- data.nodes.length + ' nodes \xB7 ' + data.links.length + ' edges';
1899
- rebuildGraph();
1900
- buildNodeList(nodeSearchEl.value);
1901
- }
1902
-
1903
- // \u2500\u2500 SVG setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1904
- const svgEl = d3.select('svg');
1905
- const graphDiv = document.getElementById('graph');
1906
- const W = () => graphDiv.clientWidth;
1907
- const H = () => graphDiv.clientHeight;
1908
- const g = svgEl.append('g');
1909
-
1910
- svgEl.append('defs').append('marker')
1911
- .attr('id', 'arrow').attr('viewBox', '0 0 10 6')
1912
- .attr('refX', 10).attr('refY', 3)
1913
- .attr('markerWidth', 8).attr('markerHeight', 6)
1914
- .attr('orient', 'auto')
1915
- .append('path').attr('d', 'M0,0 L10,3 L0,6 Z').attr('fill', '#555');
1916
-
1917
- let currentZoom = 1;
1918
- const zoomBehavior = d3.zoom().scaleExtent([0.08, 6]).on('zoom', e => {
1919
- g.attr('transform', e.transform);
1920
- currentZoom = e.transform.k;
1921
- updateLOD(currentZoom);
1922
- });
1923
- svgEl.call(zoomBehavior);
1924
-
1925
- // \u2500\u2500 Layer filter state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1926
- const layers = [...new Set(data.nodes.map(d => d.layer))];
1927
- const layerVisible = {};
1928
- layers.forEach(l => layerVisible[l] = true);
1929
-
1930
- const toolbarEl = document.getElementById('toolbar');
1931
-
1932
- // Filter buttons
1933
- layers.forEach(layer => {
1934
- const btn = document.createElement('button');
1935
- btn.className = 'filter-btn';
1936
- btn.innerHTML = \`<span class="filter-dot" style="background:\${LAYER_COLORS[layer]||'#666'}"></span>\${LAYER_NAMES[layer]||layer}\`;
1937
- btn.onclick = () => {
1938
- layerVisible[layer] = !layerVisible[layer];
1939
- btn.classList.toggle('off', !layerVisible[layer]);
1940
- updateVisibility();
1941
- };
1942
- toolbarEl.appendChild(btn);
1943
- });
1944
-
1945
- // JGF export button
1946
- const jgfBtn = document.createElement('button');
1947
- jgfBtn.className = 'export-btn';
1948
- jgfBtn.textContent = '\u2193 JGF';
1949
- jgfBtn.title = 'Export JSON Graph Format';
1950
- jgfBtn.onclick = exportJGF;
1951
- toolbarEl.appendChild(jgfBtn);
1952
-
1953
- // \u2500\u2500 JGF export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1954
- function exportJGF() {
1955
- const jgf = {
1956
- graph: {
1957
- directed: true,
1958
- type: 'cartography',
1959
- label: 'Infrastructure Map',
1960
- metadata: { exportedAt: new Date().toISOString() },
1961
- nodes: Object.fromEntries(data.nodes.map(n => [n.id, {
1962
- label: n.name,
1963
- metadata: { type: n.type, layer: n.layer, confidence: n.confidence,
1964
- discoveredVia: n.discoveredVia, discoveredAt: n.discoveredAt,
1965
- tags: n.tags, ...n.metadata }
1966
- }])),
1967
- edges: data.links.map(l => ({
1968
- source: l.source.id || l.source,
1969
- target: l.target.id || l.target,
1970
- relation: l.relationship,
1971
- metadata: { confidence: l.confidence, evidence: l.evidence }
1972
- })),
1973
- }
1974
- };
1975
- const blob = new Blob([JSON.stringify(jgf, null, 2)], { type: 'application/json' });
1976
- const url = URL.createObjectURL(blob);
1977
- const a = document.createElement('a');
1978
- a.href = url; a.download = 'cartography-graph.jgf.json'; a.click();
1979
- URL.revokeObjectURL(url);
1980
- }
1981
-
1982
- // \u2500\u2500 Cluster force \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1983
- function clusterForce(alpha) {
1984
- const centroids = {};
1985
- const counts = {};
1986
- data.nodes.forEach(d => {
1987
- if (!centroids[d.layer]) { centroids[d.layer] = { x: 0, y: 0 }; counts[d.layer] = 0; }
1988
- centroids[d.layer].x += d.x || 0;
1989
- centroids[d.layer].y += d.y || 0;
1990
- counts[d.layer]++;
1991
- });
1992
- for (const l in centroids) { centroids[l].x /= counts[l]; centroids[l].y /= counts[l]; }
1993
- const strength = alpha * 0.15;
1994
- data.nodes.forEach(d => {
1995
- const c = centroids[d.layer];
1996
- if (c) { d.vx += (c.x - d.x) * strength; d.vy += (c.y - d.y) * strength; }
1997
- });
1998
- }
1999
-
2000
- // \u2500\u2500 Hull group \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2001
- const hullGroup = g.append('g').attr('class', 'hulls');
2002
- const hullPaths = {};
2003
- const hullLabels = {};
2004
- layers.forEach(layer => {
2005
- hullPaths[layer] = hullGroup.append('path').attr('class', 'hull')
2006
- .attr('fill', LAYER_COLORS[layer] || '#666').attr('stroke', LAYER_COLORS[layer] || '#666');
2007
- hullLabels[layer] = hullGroup.append('text').attr('class', 'hull-label')
2008
- .attr('fill', LAYER_COLORS[layer] || '#666').text(LAYER_NAMES[layer] || layer);
2009
- });
2010
-
2011
- function updateHulls() {
2012
- layers.forEach(layer => {
2013
- if (!layerVisible[layer]) { hullPaths[layer].attr('d', null); hullLabels[layer].attr('x', -9999); return; }
2014
- const pts = data.nodes.filter(d => d.layer === layer && layerVisible[d.layer]).map(d => [d.x, d.y]);
2015
- if (pts.length < 3) {
2016
- hullPaths[layer].attr('d', null);
2017
- if (pts.length > 0) hullLabels[layer].attr('x', pts[0][0]).attr('y', pts[0][1] - 30);
2018
- else hullLabels[layer].attr('x', -9999);
2019
- return;
2020
- }
2021
- const hull = d3.polygonHull(pts);
2022
- if (!hull) { hullPaths[layer].attr('d', null); return; }
2023
- const cx = d3.mean(hull, p => p[0]);
2024
- const cy = d3.mean(hull, p => p[1]);
2025
- const padded = hull.map(p => {
2026
- const dx = p[0] - cx, dy = p[1] - cy;
2027
- const len = Math.sqrt(dx*dx + dy*dy) || 1;
2028
- return [p[0] + dx/len * 40, p[1] + dy/len * 40];
2029
- });
2030
- hullPaths[layer].attr('d', 'M' + padded.join('L') + 'Z');
2031
- hullLabels[layer].attr('x', cx).attr('y', cy - d3.max(hull, p => Math.abs(p[1] - cy)) - 30);
2032
- });
2033
- }
2034
-
2035
- // \u2500\u2500 Graph rendering (rebuildable after delete) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2036
- let linkSel, linkLabelSel, nodeSel, nodeLabelSel, sim;
2037
- const linkGroup = g.append('g');
2038
- const nodeGroup = g.append('g');
2039
-
2040
- function focusNode(d) {
2041
- if (!d.x || !d.y) return;
2042
- const w = W(), h = H();
2043
- svgEl.transition().duration(500).call(
2044
- zoomBehavior.transform,
2045
- d3.zoomIdentity.translate(w / 2, h / 2).scale(Math.min(3, currentZoom < 1 ? 1.5 : currentZoom)).translate(-d.x, -d.y)
2046
- );
2047
- }
2048
-
2049
- function rebuildGraph() {
2050
- if (sim) sim.stop();
2051
-
2052
- // Links
2053
- linkSel = linkGroup.selectAll('line').data(data.links, d => \`\${d.source.id||d.source}>\${d.target.id||d.target}\`);
2054
- linkSel.exit().remove();
2055
- const linkEnter = linkSel.enter().append('line').attr('class', 'link');
2056
- linkSel = linkEnter.merge(linkSel)
2057
- .attr('stroke', d => d.confidence < 0.6 ? '#2a2e35' : '#3d434b')
2058
- .attr('stroke-dasharray', d => d.confidence < 0.6 ? '4 3' : null)
2059
- .attr('stroke-width', d => d.confidence < 0.6 ? 0.8 : 1.2)
2060
- .attr('marker-end', 'url(#arrow)');
2061
- linkSel.select('title').remove();
2062
- linkSel.append('title').text(d => \`\${d.relationship} (\${Math.round(d.confidence*100)}%)
2063
- \${d.evidence||''}\`);
2064
-
2065
- // Link labels
2066
- linkLabelSel = linkGroup.selectAll('text').data(data.links, d => \`\${d.source.id||d.source}>\${d.target.id||d.target}\`);
2067
- linkLabelSel.exit().remove();
2068
- linkLabelSel = linkLabelSel.enter().append('text').attr('class', 'link-label').merge(linkLabelSel)
2069
- .text(d => d.relationship);
2070
-
2071
- // Nodes
2072
- nodeSel = nodeGroup.selectAll('g').data(data.nodes, d => d.id);
2073
- nodeSel.exit().remove();
2074
- const nodeEnter = nodeSel.enter().append('g')
2075
- .call(d3.drag()
2076
- .on('start', (e, d) => { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
2077
- .on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
2078
- .on('end', (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; })
2079
- )
2080
- .on('click', (e, d) => { e.stopPropagation(); selectNode(d); });
2081
- nodeEnter.append('path').attr('class', 'node-hex');
2082
- nodeEnter.append('title');
2083
- nodeEnter.append('text').attr('class', 'node-label').attr('text-anchor', 'middle');
2084
-
2085
- nodeSel = nodeEnter.merge(nodeSel);
2086
- nodeSel.select('.node-hex')
2087
- .attr('d', d => hexPath(hexSize(d)))
2088
- .attr('fill', d => TYPE_COLORS[d.type] || '#aaa')
2089
- .attr('stroke', d => { const c = d3.color(TYPE_COLORS[d.type] || '#aaa'); return c ? c.brighter(0.8).formatHex() : '#ccc'; })
2090
- .attr('fill-opacity', d => 0.6 + d.confidence * 0.4)
2091
- .classed('selected', d => d.id === selectedNodeId);
2092
- nodeSel.select('title').text(d => \`\${d.name} (\${d.type})
2093
- conf: \${Math.round(d.confidence*100)}%\`);
2094
- nodeLabelSel = nodeSel.select('.node-label')
2095
- .attr('dy', d => hexSize(d) + 13)
2096
- .text(d => d.name.length > 20 ? d.name.substring(0, 18) + '\u2026' : d.name);
2097
-
2098
- // Simulation
2099
- sim = d3.forceSimulation(data.nodes)
2100
- .force('link', d3.forceLink(data.links).id(d => d.id).distance(d => d.relationship === 'contains' ? 50 : 100).strength(0.4))
2101
- .force('charge', d3.forceManyBody().strength(-280))
2102
- .force('center', d3.forceCenter(W() / 2, H() / 2))
2103
- .force('collision', d3.forceCollide().radius(d => hexSize(d) + 10))
2104
- .force('cluster', clusterForce)
2105
- .on('tick', () => {
2106
- updateHulls();
2107
- linkSel.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
2108
- .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
2109
- linkLabelSel.attr('x', d => (d.source.x + d.target.x) / 2)
2110
- .attr('y', d => (d.source.y + d.target.y) / 2 - 4);
2111
- nodeSel.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
2112
- });
2113
- }
2114
-
2115
- // \u2500\u2500 LOD & visibility \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2116
- function updateLOD(k) {
2117
- if (nodeLabelSel) nodeLabelSel.style('opacity', k > 0.5 ? Math.min(1, (k - 0.5) * 2) : 0);
2118
- if (linkLabelSel) linkLabelSel.style('opacity', k > 1.2 ? Math.min(1, (k - 1.2) * 3) : 0);
2119
- d3.selectAll('.hull-label').style('font-size', k < 0.4 ? '18px' : '13px');
2120
- }
2121
-
2122
- function updateVisibility() {
2123
- if (!nodeSel) return;
2124
- nodeSel.style('display', d => layerVisible[d.layer] ? null : 'none');
2125
- linkSel.style('display', d => {
2126
- const s = data.nodes.find(n => n.id === (d.source.id||d.source));
2127
- const t = data.nodes.find(n => n.id === (d.target.id||d.target));
2128
- return (s && layerVisible[s.layer]) && (t && layerVisible[t.layer]) ? null : 'none';
2129
- });
2130
- linkLabelSel.style('display', d => {
2131
- const s = data.nodes.find(n => n.id === (d.source.id||d.source));
2132
- const t = data.nodes.find(n => n.id === (d.target.id||d.target));
2133
- return (s && layerVisible[s.layer]) && (t && layerVisible[t.layer]) ? null : 'none';
2134
- });
2135
- }
2136
-
2137
- // \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
2138
- rebuildGraph();
2139
- buildNodeList();
2140
- updateLOD(1);
2141
-
2142
- svgEl.on('click', () => {
2143
- selectedNodeId = null;
2144
- d3.selectAll('.node-hex').classed('selected', false);
2145
- buildNodeList(nodeSearchEl.value);
2146
- sidebar.innerHTML = '<h2>Infrastructure Map</h2><p class="hint">Click a node to view details.</p>';
2147
- });
2148
- </script>
2149
- </body>
2150
- </html>`;
2151
- }
2152
1633
  function exportSOPMarkdown(sop) {
2153
1634
  const lines = [
2154
1635
  `# ${sop.title}`,
@@ -2170,646 +1651,6 @@ function exportSOPMarkdown(sop) {
2170
1651
  }
2171
1652
  return lines.join("\n");
2172
1653
  }
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
1654
  function exportDiscoveryApp(nodes, edges, options) {
2814
1655
  const theme = options?.theme ?? "dark";
2815
1656
  const graphData = JSON.stringify({
@@ -2970,8 +1811,9 @@ body{display:flex;flex-direction:column;background:var(--bg-base);color:var(--te
2970
1811
  width:40px;height:40px;border-radius:10px;border:1px solid var(--border);
2971
1812
  background:var(--bg-surface);box-shadow:0 1px 4px rgba(0,0,0,.08);cursor:pointer;
2972
1813
  display:flex;align-items:center;justify-content:center;font-size:18px;
2973
- transition:all .15s;color:var(--text);
1814
+ transition:all .15s;color:var(--text);font-family:-apple-system,sans-serif;
2974
1815
  }
1816
+ #btn-all-labels{font-size:14px;font-weight:700;letter-spacing:-.02em}
2975
1817
  .tb-tool:hover{border-color:var(--text-muted)}
2976
1818
  .tb-tool.active{background:var(--accent-dim);border-color:var(--accent)}
2977
1819
  .map-zoom{display:flex;align-items:center;gap:6px}
@@ -3143,6 +1985,7 @@ body{display:flex;flex-direction:column;background:var(--bg-base);color:var(--te
3143
1985
  </div>
3144
1986
  <div id="map-tb-left">
3145
1987
  <button class="tb-tool active" id="btn-labels" title="Toggle labels">&#127991;</button>
1988
+ <button class="tb-tool" id="btn-all-labels" title="Show all hex labels">Aa</button>
3146
1989
  <button class="tb-tool" id="btn-quality" title="Quality layer">&#128065;</button>
3147
1990
  <button class="tb-tool" id="btn-connect" title="Connection tool">&#128279;</button>
3148
1991
  </div>
@@ -3239,7 +2082,7 @@ var mapCtx = mapCanvas.getContext('2d');
3239
2082
  var mapWrap = document.getElementById('map-wrap');
3240
2083
  var mW = 0, mH = 0;
3241
2084
  var mvx = 0, mvy = 0, mScale = 1;
3242
- var mDetailLevel = 2, mShowLabels = true, mShowQuality = false;
2085
+ var mDetailLevel = 2, mShowLabels = true, mShowQuality = false, mShowAllLabels = false;
3243
2086
  var mConnectMode = false, mConnectFirst = null;
3244
2087
  var mHoveredId = null, mSelectedId = null;
3245
2088
  var mSearchQuery = '';
@@ -3384,8 +2227,8 @@ function drawMap() {
3384
2227
  mapCtx.fillStyle = asset.qualityScore < 40 ? '#ef4444' : '#f97316'; mapCtx.fill();
3385
2228
  }
3386
2229
 
3387
- var showAssetLabel = mShowLabels && !clusterDim && ((mDetailLevel >= 4) || (mDetailLevel === 3 && mScale >= 0.8));
3388
- if (showAssetLabel && size > 14) {
2230
+ var showAssetLabel = mShowLabels && !clusterDim && (mShowAllLabels || (mDetailLevel >= 4) || (mDetailLevel === 3 && mScale >= 0.8));
2231
+ if (showAssetLabel && size > 6) {
3389
2232
  var label = asset.name.length > 12 ? asset.name.substring(0, 11) + '...' : asset.name;
3390
2233
  mapCtx.save();
3391
2234
  mapCtx.font = Math.max(8, Math.min(11, size * 0.38)) + 'px -apple-system,sans-serif';
@@ -3537,6 +2380,11 @@ document.getElementById('md-close').addEventListener('click', function() {
3537
2380
  document.getElementById('btn-labels').addEventListener('click', function() {
3538
2381
  mShowLabels = !mShowLabels; this.classList.toggle('active', mShowLabels); drawMap();
3539
2382
  });
2383
+ document.getElementById('btn-all-labels').addEventListener('click', function() {
2384
+ mShowAllLabels = !mShowAllLabels; this.classList.toggle('active', mShowAllLabels);
2385
+ if (mShowAllLabels && !mShowLabels) { mShowLabels = true; document.getElementById('btn-labels').classList.add('active'); }
2386
+ drawMap();
2387
+ });
3540
2388
  document.getElementById('btn-quality').addEventListener('click', function() {
3541
2389
  mShowQuality = !mShowQuality; this.classList.toggle('active', mShowQuality); drawMap();
3542
2390
  });
@@ -3891,12 +2739,44 @@ buildTopoList();
3891
2739
  </body>
3892
2740
  </html>`;
3893
2741
  }
2742
+ function exportJGF(nodes, edges) {
2743
+ const jgf = {
2744
+ graph: {
2745
+ directed: true,
2746
+ type: "cartography",
2747
+ label: "Infrastructure Map",
2748
+ metadata: { exportedAt: (/* @__PURE__ */ new Date()).toISOString() },
2749
+ nodes: Object.fromEntries(nodes.map((n) => [n.id, {
2750
+ label: n.name,
2751
+ metadata: {
2752
+ type: n.type,
2753
+ layer: nodeLayer(n.type),
2754
+ confidence: n.confidence,
2755
+ discoveredVia: n.discoveredVia,
2756
+ discoveredAt: n.discoveredAt,
2757
+ tags: n.tags,
2758
+ metadata: n.metadata
2759
+ }
2760
+ }])),
2761
+ edges: edges.map((e) => ({
2762
+ source: e.sourceId,
2763
+ target: e.targetId,
2764
+ relation: e.relationship,
2765
+ metadata: { confidence: e.confidence, evidence: e.evidence }
2766
+ }))
2767
+ }
2768
+ };
2769
+ return JSON.stringify(jgf, null, 2);
2770
+ }
3894
2771
  function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml", "html", "map", "discovery", "sops"]) {
3895
2772
  mkdirSync2(outputDir, { recursive: true });
3896
2773
  mkdirSync2(join2(outputDir, "sops"), { recursive: true });
3897
2774
  mkdirSync2(join2(outputDir, "workflows"), { recursive: true });
3898
2775
  const nodes = db.getNodes(sessionId);
3899
2776
  const edges = db.getEdges(sessionId);
2777
+ const jgfPath = join2(outputDir, "cartography-graph.jgf.json");
2778
+ writeFileSync(jgfPath, exportJGF(nodes, edges));
2779
+ process.stderr.write("\u2713 cartography-graph.jgf.json\n");
3900
2780
  if (formats.includes("mermaid")) {
3901
2781
  writeFileSync(join2(outputDir, "topology.mermaid"), generateTopologyMermaid(nodes, edges));
3902
2782
  writeFileSync(join2(outputDir, "dependencies.mermaid"), generateDependencyMermaid(nodes, edges));
@@ -3910,15 +2790,7 @@ function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml
3910
2790
  writeFileSync(join2(outputDir, "catalog-info.yaml"), exportBackstageYAML(nodes, edges));
3911
2791
  process.stderr.write("\u2713 catalog-info.yaml\n");
3912
2792
  }
3913
- if (formats.includes("html")) {
3914
- writeFileSync(join2(outputDir, "topology.html"), exportHTML(nodes, edges));
3915
- process.stderr.write("\u2713 topology.html\n");
3916
- }
3917
- if (formats.includes("map")) {
3918
- writeFileSync(join2(outputDir, "cartography-map.html"), exportCartographyMap(nodes, edges));
3919
- process.stderr.write("\u2713 cartography-map.html\n");
3920
- }
3921
- if (formats.includes("discovery")) {
2793
+ if (formats.includes("html") || formats.includes("map") || formats.includes("discovery")) {
3922
2794
  writeFileSync(join2(outputDir, "discovery.html"), exportDiscoveryApp(nodes, edges));
3923
2795
  process.stderr.write("\u2713 discovery.html\n");
3924
2796
  }
@@ -3938,10 +2810,9 @@ function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml
3938
2810
  }
3939
2811
 
3940
2812
  // src/cli.ts
3941
- import { readFileSync as readFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
2813
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
3942
2814
  import { resolve } from "path";
3943
2815
  import { createInterface } from "readline";
3944
- import { execSync as execSync2 } from "child_process";
3945
2816
  var bold = (s) => `\x1B[1m${s}\x1B[0m`;
3946
2817
  var dim = (s) => `\x1B[2m${s}\x1B[0m`;
3947
2818
  var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
@@ -4165,32 +3036,12 @@ function main() {
4165
3036
  }
4166
3037
  exportAll(db, sessionId, config.outputDir);
4167
3038
  const osc8 = (url, label) => `\x1B]8;;${url}\x1B\\${label}\x1B]8;;\x1B\\`;
4168
- const htmlPath = resolve(config.outputDir, "topology.html");
4169
- const mapPath = resolve(config.outputDir, "cartography-map.html");
4170
3039
  const discoveryPath = resolve(config.outputDir, "discovery.html");
4171
- const topoPath = resolve(config.outputDir, "topology.mermaid");
4172
3040
  w("\n");
4173
3041
  if (existsSync2(discoveryPath)) {
4174
- w(` ${green("\u2192")} ${osc8(`file://${discoveryPath}`, bold("Open discovery.html"))} ${dim("\u2190 Enterprise Discovery Frontend")}
3042
+ w(` ${green("\u2192")} ${osc8(`file://${discoveryPath}`, bold("Open discovery.html"))} ${dim("\u2190 Map + Topology")}
4175
3043
  `);
4176
3044
  }
4177
- if (existsSync2(mapPath)) {
4178
- w(` ${green("\u2192")} ${osc8(`file://${mapPath}`, bold("Open cartography-map.html"))} ${dim("\u2190 Hex Map")}
4179
- `);
4180
- }
4181
- if (existsSync2(htmlPath)) {
4182
- w(` ${green("\u2192")} ${osc8(`file://${htmlPath}`, bold("Open topology.html"))}
4183
- `);
4184
- }
4185
- if (existsSync2(topoPath)) {
4186
- try {
4187
- const code = readFileSync2(topoPath, "utf8");
4188
- const b64 = Buffer.from(JSON.stringify({ code, mermaid: { theme: "dark" } })).toString("base64");
4189
- w(` ${cyan("\u2192")} ${osc8(`https://mermaid.live/view#base64:${b64}`, bold("Open in mermaid.live"))}
4190
- `);
4191
- } catch {
4192
- }
4193
- }
4194
3045
  w("\n");
4195
3046
  if (process.stdin.isTTY) {
4196
3047
  w(dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
@@ -4232,8 +3083,8 @@ function main() {
4232
3083
  `);
4233
3084
  w("\n");
4234
3085
  exportAll(db, sessionId, config.outputDir);
4235
- if (existsSync2(htmlPath)) {
4236
- w(` ${green("\u2192")} ${osc8(`file://${htmlPath}`, bold("topology.html updated"))}
3086
+ if (existsSync2(discoveryPath)) {
3087
+ w(` ${green("\u2192")} ${osc8(`file://${discoveryPath}`, bold("discovery.html updated"))}
4237
3088
  `);
4238
3089
  }
4239
3090
  w("\n");
@@ -4257,37 +3108,6 @@ function main() {
4257
3108
  `);
4258
3109
  db.close();
4259
3110
  });
4260
- 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) => {
4261
- const config = defaultConfig({ outputDir: opts.output });
4262
- const db = new CartographyDB(config.dbPath);
4263
- const session = sessionId ? db.getSession(sessionId) : db.getLatestSession();
4264
- if (!session) {
4265
- process.stderr.write("No session found. Run discover first.\n");
4266
- db.close();
4267
- process.exitCode = 1;
4268
- return;
4269
- }
4270
- const nodes = db.getNodes(session.id);
4271
- const edges = db.getEdges(session.id);
4272
- const outDir = resolve(opts.output);
4273
- mkdirSync3(outDir, { recursive: true });
4274
- const outPath = resolve(outDir, "cartography-map.html");
4275
- writeFileSync2(outPath, exportCartographyMap(nodes, edges, { theme: opts.theme }));
4276
- db.close();
4277
- const osc8 = (url, label) => `\x1B]8;;${url}\x1B\\${label}\x1B]8;;\x1B\\`;
4278
- const fileUrl = `file://${outPath}`;
4279
- process.stderr.write(`
4280
- ${green("OK")} ${osc8(fileUrl, bold("Open cartography-map.html"))}
4281
- `);
4282
- process.stderr.write(` ${dim(fileUrl)}
4283
-
4284
- `);
4285
- try {
4286
- const cmd = process.platform === "darwin" ? `open "${outPath}"` : process.platform === "win32" ? `start "" "${outPath}"` : `xdg-open "${outPath}"`;
4287
- execSync2(cmd, { stdio: "ignore" });
4288
- } catch {
4289
- }
4290
- });
4291
3111
  program.command("show [session-id]").description("Show session details").action((sessionId) => {
4292
3112
  const config = defaultConfig();
4293
3113
  const db = new CartographyDB(config.dbPath);
@@ -4531,8 +3351,6 @@ ${infraSummary.substring(0, 12e3)}`;
4531
3351
  out(dim(" topology.mermaid Infrastructure topology (graph TB)\n"));
4532
3352
  out(dim(" dependencies.mermaid Service dependencies (graph LR)\n"));
4533
3353
  out(dim(" discovery.html Enterprise discovery frontend (Map + Topology)\n"));
4534
- out(dim(" topology.html Interactive D3.js force graph\n"));
4535
- out(dim(" cartography-map.html Hex grid data cartography map\n"));
4536
3354
  out(dim(" sops/ Generated SOPs as Markdown\n"));
4537
3355
  out(dim(" workflows/ Workflow flowcharts as Mermaid\n"));
4538
3356
  out("\n");
@@ -4749,7 +3567,7 @@ ${infraSummary.substring(0, 12e3)}`;
4749
3567
  `);
4750
3568
  });
4751
3569
  program.command("doctor").description("Check all requirements and cloud CLIs").action(async () => {
4752
- const { execSync: execSync3 } = await import("child_process");
3570
+ const { execSync: execSync2 } = await import("child_process");
4753
3571
  const { existsSync: existsSync3, readFileSync: readFileSync3 } = await import("fs");
4754
3572
  const { join: join3 } = await import("path");
4755
3573
  const out = (s) => process.stdout.write(s);
@@ -4772,7 +3590,7 @@ ${infraSummary.substring(0, 12e3)}`;
4772
3590
  allGood = false;
4773
3591
  }
4774
3592
  try {
4775
- const v = execSync3("claude --version", { stdio: "pipe" }).toString().trim();
3593
+ const v = execSync2("claude --version", { stdio: "pipe" }).toString().trim();
4776
3594
  ok(`Claude CLI ${dim2(v)}`);
4777
3595
  } catch {
4778
3596
  err("Claude CLI not found \u2014 npm i -g @anthropic-ai/claude-code");
@@ -4796,7 +3614,7 @@ ${infraSummary.substring(0, 12e3)}`;
4796
3614
  allGood = false;
4797
3615
  }
4798
3616
  try {
4799
- const v = execSync3("kubectl version --client --short 2>/dev/null || kubectl version --client", { stdio: "pipe" }).toString().split("\n")[0]?.trim() ?? "";
3617
+ const v = execSync2("kubectl version --client --short 2>/dev/null || kubectl version --client", { stdio: "pipe" }).toString().split("\n")[0]?.trim() ?? "";
4800
3618
  ok(`kubectl ${dim2(v || "(client OK)")}`);
4801
3619
  } catch {
4802
3620
  warn(`kubectl not found ${dim2("\u2014 install: https://kubernetes.io/docs/tasks/tools/")}`);
@@ -4808,7 +3626,7 @@ ${infraSummary.substring(0, 12e3)}`;
4808
3626
  ];
4809
3627
  for (const [name, cmd, hint] of cloudClis) {
4810
3628
  try {
4811
- execSync3(cmd, { stdio: "pipe" });
3629
+ execSync2(cmd, { stdio: "pipe" });
4812
3630
  ok(`${name} ${dim2("(cloud scanning available)")}`);
4813
3631
  } catch {
4814
3632
  warn(`${name} not found ${dim2("\u2014 cloud scan skipped | " + hint)}`);
@@ -4820,7 +3638,7 @@ ${infraSummary.substring(0, 12e3)}`;
4820
3638
  ];
4821
3639
  for (const [name, cmd] of localTools) {
4822
3640
  try {
4823
- execSync3(cmd, { stdio: "pipe" });
3641
+ execSync2(cmd, { stdio: "pipe" });
4824
3642
  ok(`${name} ${dim2("(discovery tool)")}`);
4825
3643
  } catch {
4826
3644
  warn(`${name} not found ${dim2("\u2014 discovery without " + name + " will be limited")}`);