@datasynx/agentic-ai-cartography 0.3.3 → 0.4.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/{chunk-FFNOC6HF.js → chunk-7NRS3WP6.js} +6 -2
- package/dist/chunk-7NRS3WP6.js.map +1 -0
- package/dist/cli.js +1166 -34
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +148 -1
- package/dist/index.js +1176 -11
- package/dist/index.js.map +1 -1
- package/dist/{types-ROE3Z6HY.js → types-A6K73WE4.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-FFNOC6HF.js.map +0 -1
- /package/dist/{types-ROE3Z6HY.js.map → types-A6K73WE4.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
NODE_TYPES,
|
|
5
5
|
SOPStepSchema,
|
|
6
6
|
defaultConfig
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-7NRS3WP6.js";
|
|
8
8
|
import {
|
|
9
9
|
scanAllBookmarks,
|
|
10
10
|
scanAllHistory
|
|
@@ -79,9 +79,21 @@ CREATE TABLE IF NOT EXISTS nodes (
|
|
|
79
79
|
confidence REAL DEFAULT 0.5,
|
|
80
80
|
metadata TEXT NOT NULL DEFAULT '{}',
|
|
81
81
|
tags TEXT NOT NULL DEFAULT '[]',
|
|
82
|
+
domain TEXT,
|
|
83
|
+
sub_domain TEXT,
|
|
84
|
+
quality_score REAL,
|
|
82
85
|
PRIMARY KEY (id, session_id)
|
|
83
86
|
);
|
|
84
87
|
|
|
88
|
+
CREATE TABLE IF NOT EXISTS connections (
|
|
89
|
+
id TEXT PRIMARY KEY,
|
|
90
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
91
|
+
source_asset_id TEXT NOT NULL,
|
|
92
|
+
target_asset_id TEXT NOT NULL,
|
|
93
|
+
type TEXT,
|
|
94
|
+
created_at TEXT NOT NULL
|
|
95
|
+
);
|
|
96
|
+
|
|
85
97
|
CREATE TABLE IF NOT EXISTS edges (
|
|
86
98
|
id TEXT PRIMARY KEY,
|
|
87
99
|
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
@@ -156,6 +168,7 @@ CREATE INDEX IF NOT EXISTS idx_edges_session ON edges(session_id);
|
|
|
156
168
|
CREATE INDEX IF NOT EXISTS idx_events_session ON activity_events(session_id);
|
|
157
169
|
CREATE INDEX IF NOT EXISTS idx_events_task ON activity_events(task_id);
|
|
158
170
|
CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_id);
|
|
171
|
+
CREATE INDEX IF NOT EXISTS idx_connections_session ON connections(session_id);
|
|
159
172
|
`;
|
|
160
173
|
var CartographyDB = class {
|
|
161
174
|
db;
|
|
@@ -171,7 +184,24 @@ var CartographyDB = class {
|
|
|
171
184
|
const version = this.db.pragma("user_version", { simple: true });
|
|
172
185
|
if (version === 0) {
|
|
173
186
|
this.db.exec(SCHEMA);
|
|
174
|
-
this.db.pragma("user_version =
|
|
187
|
+
this.db.pragma("user_version = 2");
|
|
188
|
+
} else if (version === 1) {
|
|
189
|
+
const cols = this.db.prepare("PRAGMA table_info(nodes)").all().map((c) => c.name);
|
|
190
|
+
if (!cols.includes("domain")) this.db.exec("ALTER TABLE nodes ADD COLUMN domain TEXT");
|
|
191
|
+
if (!cols.includes("sub_domain")) this.db.exec("ALTER TABLE nodes ADD COLUMN sub_domain TEXT");
|
|
192
|
+
if (!cols.includes("quality_score")) this.db.exec("ALTER TABLE nodes ADD COLUMN quality_score REAL");
|
|
193
|
+
this.db.exec(`
|
|
194
|
+
CREATE TABLE IF NOT EXISTS connections (
|
|
195
|
+
id TEXT PRIMARY KEY,
|
|
196
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
197
|
+
source_asset_id TEXT NOT NULL,
|
|
198
|
+
target_asset_id TEXT NOT NULL,
|
|
199
|
+
type TEXT,
|
|
200
|
+
created_at TEXT NOT NULL
|
|
201
|
+
);
|
|
202
|
+
CREATE INDEX IF NOT EXISTS idx_connections_session ON connections(session_id);
|
|
203
|
+
`);
|
|
204
|
+
this.db.pragma("user_version = 2");
|
|
175
205
|
}
|
|
176
206
|
}
|
|
177
207
|
close() {
|
|
@@ -214,8 +244,9 @@ var CartographyDB = class {
|
|
|
214
244
|
upsertNode(sessionId, node, depth = 0) {
|
|
215
245
|
this.db.prepare(`
|
|
216
246
|
INSERT OR REPLACE INTO nodes
|
|
217
|
-
(id, session_id, type, name, discovered_via, discovered_at, depth, confidence, metadata, tags
|
|
218
|
-
|
|
247
|
+
(id, session_id, type, name, discovered_via, discovered_at, depth, confidence, metadata, tags,
|
|
248
|
+
domain, sub_domain, quality_score)
|
|
249
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
219
250
|
`).run(
|
|
220
251
|
node.id,
|
|
221
252
|
sessionId,
|
|
@@ -226,12 +257,18 @@ var CartographyDB = class {
|
|
|
226
257
|
depth,
|
|
227
258
|
node.confidence,
|
|
228
259
|
JSON.stringify(node.metadata ?? {}),
|
|
229
|
-
JSON.stringify(node.tags ?? [])
|
|
260
|
+
JSON.stringify(node.tags ?? []),
|
|
261
|
+
node.domain ?? null,
|
|
262
|
+
node.subDomain ?? null,
|
|
263
|
+
node.qualityScore ?? null
|
|
230
264
|
);
|
|
231
265
|
}
|
|
232
266
|
getNodes(sessionId) {
|
|
233
267
|
const rows = this.db.prepare("SELECT * FROM nodes WHERE session_id = ?").all(sessionId);
|
|
234
|
-
return rows.map((r) => (
|
|
268
|
+
return rows.map((r) => this.mapNode(r));
|
|
269
|
+
}
|
|
270
|
+
mapNode(r) {
|
|
271
|
+
return {
|
|
235
272
|
id: r["id"],
|
|
236
273
|
sessionId: r["session_id"],
|
|
237
274
|
type: r["type"],
|
|
@@ -242,8 +279,11 @@ var CartographyDB = class {
|
|
|
242
279
|
confidence: r["confidence"],
|
|
243
280
|
metadata: JSON.parse(r["metadata"]),
|
|
244
281
|
tags: JSON.parse(r["tags"]),
|
|
245
|
-
pathId: r["path_id"]
|
|
246
|
-
|
|
282
|
+
pathId: r["path_id"],
|
|
283
|
+
domain: r["domain"] ?? void 0,
|
|
284
|
+
subDomain: r["sub_domain"] ?? void 0,
|
|
285
|
+
qualityScore: r["quality_score"] ?? void 0
|
|
286
|
+
};
|
|
247
287
|
}
|
|
248
288
|
deleteNode(sessionId, nodeId) {
|
|
249
289
|
this.db.prepare("DELETE FROM nodes WHERE session_id = ? AND id = ?").run(sessionId, nodeId);
|
|
@@ -455,6 +495,33 @@ var CartographyDB = class {
|
|
|
455
495
|
generatedAt: r["generated_at"]
|
|
456
496
|
}));
|
|
457
497
|
}
|
|
498
|
+
// ── Connections (user-created hex map links) ─────────────────────────────
|
|
499
|
+
upsertConnection(sessionId, conn) {
|
|
500
|
+
const existing = this.db.prepare(
|
|
501
|
+
"SELECT id FROM connections WHERE session_id = ? AND source_asset_id = ? AND target_asset_id = ?"
|
|
502
|
+
).get(sessionId, conn.sourceAssetId, conn.targetAssetId);
|
|
503
|
+
if (existing) return existing.id;
|
|
504
|
+
const id = crypto.randomUUID();
|
|
505
|
+
this.db.prepare(`
|
|
506
|
+
INSERT INTO connections (id, session_id, source_asset_id, target_asset_id, type, created_at)
|
|
507
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
508
|
+
`).run(id, sessionId, conn.sourceAssetId, conn.targetAssetId, conn.type ?? null, (/* @__PURE__ */ new Date()).toISOString());
|
|
509
|
+
return id;
|
|
510
|
+
}
|
|
511
|
+
getConnections(sessionId) {
|
|
512
|
+
const rows = this.db.prepare("SELECT * FROM connections WHERE session_id = ?").all(sessionId);
|
|
513
|
+
return rows.map((r) => ({
|
|
514
|
+
id: r["id"],
|
|
515
|
+
sessionId: r["session_id"],
|
|
516
|
+
sourceAssetId: r["source_asset_id"],
|
|
517
|
+
targetAssetId: r["target_asset_id"],
|
|
518
|
+
type: r["type"] ?? void 0,
|
|
519
|
+
createdAt: r["created_at"]
|
|
520
|
+
}));
|
|
521
|
+
}
|
|
522
|
+
deleteConnection(sessionId, connectionId) {
|
|
523
|
+
this.db.prepare("DELETE FROM connections WHERE session_id = ? AND id = ?").run(sessionId, connectionId);
|
|
524
|
+
}
|
|
458
525
|
// ── Approvals ───────────────────────────
|
|
459
526
|
setApproval(pattern, action) {
|
|
460
527
|
this.db.prepare(`
|
|
@@ -496,7 +563,10 @@ async function createCartographyTools(db, sessionId, opts = {}) {
|
|
|
496
563
|
discoveredVia: z.string(),
|
|
497
564
|
confidence: z.number().min(0).max(1),
|
|
498
565
|
metadata: z.record(z.unknown()).optional(),
|
|
499
|
-
tags: z.array(z.string()).optional()
|
|
566
|
+
tags: z.array(z.string()).optional(),
|
|
567
|
+
domain: z.string().optional().describe('Business domain, e.g. "Marketing", "Finance"'),
|
|
568
|
+
subDomain: z.string().optional().describe('Sub-domain, e.g. "Forecast client orders"'),
|
|
569
|
+
qualityScore: z.number().min(0).max(100).optional().describe("Data quality score 0\u2013100")
|
|
500
570
|
}, async (args) => {
|
|
501
571
|
const node = {
|
|
502
572
|
id: stripSensitive(args["id"]),
|
|
@@ -505,7 +575,10 @@ async function createCartographyTools(db, sessionId, opts = {}) {
|
|
|
505
575
|
discoveredVia: args["discoveredVia"],
|
|
506
576
|
confidence: args["confidence"],
|
|
507
577
|
metadata: args["metadata"] ?? {},
|
|
508
|
-
tags: args["tags"] ?? []
|
|
578
|
+
tags: args["tags"] ?? [],
|
|
579
|
+
domain: args["domain"],
|
|
580
|
+
subDomain: args["subDomain"],
|
|
581
|
+
qualityScore: args["qualityScore"]
|
|
509
582
|
};
|
|
510
583
|
db.upsertNode(sessionId, node);
|
|
511
584
|
return { content: [{ type: "text", text: `\u2713 Node: ${node.id}` }] };
|
|
@@ -600,14 +673,14 @@ async function createCartographyTools(db, sessionId, opts = {}) {
|
|
|
600
673
|
tool("scan_local_databases", "Scan for local database files and running DB servers \u2014 PostgreSQL databases, MySQL, SQLite files from installed apps", {
|
|
601
674
|
deep: z.boolean().default(false).optional().describe("Also search home directory recursively for SQLite/DB files (slower)")
|
|
602
675
|
}, async (args) => {
|
|
603
|
-
const { execSync:
|
|
676
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
604
677
|
const { homedir } = await import("os");
|
|
605
678
|
const { existsSync: existsSync3 } = await import("fs");
|
|
606
679
|
const deep = args["deep"] ?? false;
|
|
607
680
|
const HOME = homedir();
|
|
608
681
|
const run = (cmd) => {
|
|
609
682
|
try {
|
|
610
|
-
return
|
|
683
|
+
return execSync3(cmd, { stdio: "pipe", timeout: 1e4, shell: "/bin/sh" }).toString().trim();
|
|
611
684
|
} catch {
|
|
612
685
|
return "";
|
|
613
686
|
}
|
|
@@ -634,12 +707,12 @@ ${v}`).join("\n\n");
|
|
|
634
707
|
tool("scan_k8s_resources", "Scan Kubernetes cluster via kubectl \u2014 100% readonly (get, describe)", {
|
|
635
708
|
namespace: z.string().optional().describe("Filter by namespace \u2014 empty = all namespaces")
|
|
636
709
|
}, async (args) => {
|
|
637
|
-
const { execSync:
|
|
710
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
638
711
|
const ns = args["namespace"];
|
|
639
712
|
const nsFlag = ns ? `-n ${ns}` : "--all-namespaces";
|
|
640
713
|
const run = (cmd) => {
|
|
641
714
|
try {
|
|
642
|
-
return
|
|
715
|
+
return execSync3(cmd, { stdio: "pipe", timeout: 15e3, shell: "/bin/sh" }).toString().trim();
|
|
643
716
|
} catch (e) {
|
|
644
717
|
return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
|
|
645
718
|
}
|
|
@@ -663,7 +736,7 @@ ${run(c)}`).join("\n\n");
|
|
|
663
736
|
region: z.string().optional().describe("AWS Region \u2014 default: AWS_DEFAULT_REGION or profile"),
|
|
664
737
|
profile: z.string().optional().describe("AWS CLI profile")
|
|
665
738
|
}, async (args) => {
|
|
666
|
-
const { execSync:
|
|
739
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
667
740
|
const region = args["region"];
|
|
668
741
|
const profile = args["profile"];
|
|
669
742
|
const env = { ...process.env };
|
|
@@ -671,7 +744,7 @@ ${run(c)}`).join("\n\n");
|
|
|
671
744
|
const pf = profile ? `--profile ${profile}` : "";
|
|
672
745
|
const run = (cmd) => {
|
|
673
746
|
try {
|
|
674
|
-
return
|
|
747
|
+
return execSync3(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh", env }).toString().trim();
|
|
675
748
|
} catch (e) {
|
|
676
749
|
return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
|
|
677
750
|
}
|
|
@@ -693,12 +766,12 @@ ${run(c)}`).join("\n\n");
|
|
|
693
766
|
tool("scan_gcp_resources", "Scan Google Cloud Platform via gcloud CLI \u2014 100% readonly (list, describe)", {
|
|
694
767
|
project: z.string().optional().describe("GCP Project ID \u2014 default: current gcloud project")
|
|
695
768
|
}, async (args) => {
|
|
696
|
-
const { execSync:
|
|
769
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
697
770
|
const project = args["project"];
|
|
698
771
|
const pf = project ? `--project ${project}` : "";
|
|
699
772
|
const run = (cmd) => {
|
|
700
773
|
try {
|
|
701
|
-
return
|
|
774
|
+
return execSync3(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
|
|
702
775
|
} catch (e) {
|
|
703
776
|
return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
|
|
704
777
|
}
|
|
@@ -722,14 +795,14 @@ ${run(c)}`).join("\n\n");
|
|
|
722
795
|
subscription: z.string().optional().describe("Azure Subscription ID"),
|
|
723
796
|
resourceGroup: z.string().optional().describe("Filter by resource group")
|
|
724
797
|
}, async (args) => {
|
|
725
|
-
const { execSync:
|
|
798
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
726
799
|
const sub = args["subscription"];
|
|
727
800
|
const rg = args["resourceGroup"];
|
|
728
801
|
const sf = sub ? `--subscription ${sub}` : "";
|
|
729
802
|
const rf = rg ? `--resource-group ${rg}` : "";
|
|
730
803
|
const run = (cmd) => {
|
|
731
804
|
try {
|
|
732
|
-
return
|
|
805
|
+
return execSync3(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
|
|
733
806
|
} catch (e) {
|
|
734
807
|
return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
|
|
735
808
|
}
|
|
@@ -752,11 +825,11 @@ ${run(c)}`).join("\n\n");
|
|
|
752
825
|
tool("scan_installed_apps", "Scan all installed apps and tools \u2014 IDEs, office, dev tools, business apps, databases", {
|
|
753
826
|
searchHint: z.string().optional().describe('Optional search term to find specific tools (e.g. "hubspot windsurf cursor")')
|
|
754
827
|
}, async (args) => {
|
|
755
|
-
const { execSync:
|
|
828
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
756
829
|
const hint = args["searchHint"];
|
|
757
830
|
const run = (cmd) => {
|
|
758
831
|
try {
|
|
759
|
-
return
|
|
832
|
+
return execSync3(cmd, { stdio: "pipe", timeout: 15e3, shell: "/bin/sh" }).toString().trim();
|
|
760
833
|
} catch {
|
|
761
834
|
return "";
|
|
762
835
|
}
|
|
@@ -1137,6 +1210,227 @@ Use ask_user when you need context from the user.`;
|
|
|
1137
1210
|
// src/exporter.ts
|
|
1138
1211
|
import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
|
|
1139
1212
|
import { join as join2 } from "path";
|
|
1213
|
+
|
|
1214
|
+
// src/hex.ts
|
|
1215
|
+
function hexToPixel(q, r, size) {
|
|
1216
|
+
const x = size * (Math.sqrt(3) * q + Math.sqrt(3) / 2 * r);
|
|
1217
|
+
const y = size * (3 / 2 * r);
|
|
1218
|
+
return { x, y };
|
|
1219
|
+
}
|
|
1220
|
+
var HEX_DIRECTIONS = [
|
|
1221
|
+
{ q: 1, r: 0 },
|
|
1222
|
+
{ q: 1, r: -1 },
|
|
1223
|
+
{ q: 0, r: -1 },
|
|
1224
|
+
{ q: -1, r: 0 },
|
|
1225
|
+
{ q: -1, r: 1 },
|
|
1226
|
+
{ q: 0, r: 1 }
|
|
1227
|
+
];
|
|
1228
|
+
function hexDistance(a, b) {
|
|
1229
|
+
return (Math.abs(a.q - b.q) + Math.abs(a.q + a.r - b.q - b.r) + Math.abs(a.r - b.r)) / 2;
|
|
1230
|
+
}
|
|
1231
|
+
function hexRing(center, radius) {
|
|
1232
|
+
if (radius === 0) return [{ ...center }];
|
|
1233
|
+
const results = [];
|
|
1234
|
+
let hex = {
|
|
1235
|
+
q: center.q + HEX_DIRECTIONS[4].q * radius,
|
|
1236
|
+
r: center.r + HEX_DIRECTIONS[4].r * radius
|
|
1237
|
+
};
|
|
1238
|
+
for (let side = 0; side < 6; side++) {
|
|
1239
|
+
for (let step = 0; step < radius; step++) {
|
|
1240
|
+
results.push({ q: hex.q, r: hex.r });
|
|
1241
|
+
hex = {
|
|
1242
|
+
q: hex.q + HEX_DIRECTIONS[side].q,
|
|
1243
|
+
r: hex.r + HEX_DIRECTIONS[side].r
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
return results;
|
|
1248
|
+
}
|
|
1249
|
+
function hexSpiral(center, count) {
|
|
1250
|
+
const positions = [];
|
|
1251
|
+
let ring = 0;
|
|
1252
|
+
while (positions.length < count) {
|
|
1253
|
+
const ring_positions = hexRing(center, ring);
|
|
1254
|
+
for (const pos of ring_positions) {
|
|
1255
|
+
if (positions.length >= count) break;
|
|
1256
|
+
positions.push(pos);
|
|
1257
|
+
}
|
|
1258
|
+
ring++;
|
|
1259
|
+
}
|
|
1260
|
+
return positions;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// src/cluster.ts
|
|
1264
|
+
var DOMAIN_PALETTE = [
|
|
1265
|
+
"#1a2e5a",
|
|
1266
|
+
// 0 deep navy
|
|
1267
|
+
"#1e3a8a",
|
|
1268
|
+
// 1 dark blue
|
|
1269
|
+
"#1d4ed8",
|
|
1270
|
+
// 2 medium blue
|
|
1271
|
+
"#2563eb",
|
|
1272
|
+
// 3 blue
|
|
1273
|
+
"#3b82f6",
|
|
1274
|
+
// 4 light blue
|
|
1275
|
+
"#6366f1",
|
|
1276
|
+
// 5 indigo
|
|
1277
|
+
"#818cf8",
|
|
1278
|
+
// 6 periwinkle
|
|
1279
|
+
"#7c9fc3",
|
|
1280
|
+
// 7 slate blue
|
|
1281
|
+
"#0e7490",
|
|
1282
|
+
// 8 dark teal
|
|
1283
|
+
"#0891b2",
|
|
1284
|
+
// 9 teal
|
|
1285
|
+
"#06b6d4",
|
|
1286
|
+
// 10 cyan
|
|
1287
|
+
"#22d3ee",
|
|
1288
|
+
// 11 light cyan
|
|
1289
|
+
"#0d9488",
|
|
1290
|
+
// 12 dark teal-green
|
|
1291
|
+
"#14b8a6",
|
|
1292
|
+
// 13 teal
|
|
1293
|
+
"#2dd4bf",
|
|
1294
|
+
// 14 light teal
|
|
1295
|
+
"#5eead4"
|
|
1296
|
+
// 15 pale teal
|
|
1297
|
+
];
|
|
1298
|
+
function domainColor(domain, allDomains) {
|
|
1299
|
+
const idx = allDomains.indexOf(domain);
|
|
1300
|
+
return DOMAIN_PALETTE[idx % DOMAIN_PALETTE.length];
|
|
1301
|
+
}
|
|
1302
|
+
var HEX_SIZE = 24;
|
|
1303
|
+
var CLUSTER_GAP = 3;
|
|
1304
|
+
function layoutClusters(domainSizes) {
|
|
1305
|
+
const placements = [];
|
|
1306
|
+
const occupied = /* @__PURE__ */ new Set();
|
|
1307
|
+
const key = (q, r) => `${q},${r}`;
|
|
1308
|
+
for (const { domain, count } of domainSizes) {
|
|
1309
|
+
let origin = { q: 0, r: 0 };
|
|
1310
|
+
if (placements.length > 0) {
|
|
1311
|
+
origin = findFreeOrigin(placements, occupied, count, CLUSTER_GAP);
|
|
1312
|
+
}
|
|
1313
|
+
const positions = hexSpiral(origin, count);
|
|
1314
|
+
for (const p of positions) {
|
|
1315
|
+
occupied.add(key(p.q, p.r));
|
|
1316
|
+
}
|
|
1317
|
+
placements.push({ domain, positions, origin });
|
|
1318
|
+
}
|
|
1319
|
+
return placements;
|
|
1320
|
+
}
|
|
1321
|
+
function findFreeOrigin(existing, occupied, newCount, gap) {
|
|
1322
|
+
const radius = estimateRadius(newCount);
|
|
1323
|
+
for (let searchRing = 1; searchRing < 200; searchRing++) {
|
|
1324
|
+
const candidates = hexSpiral({ q: 0, r: 0 }, searchRing * (radius * 2 + gap + 2));
|
|
1325
|
+
for (const candidate of candidates) {
|
|
1326
|
+
const testPositions = hexSpiral(candidate, newCount);
|
|
1327
|
+
let fits = true;
|
|
1328
|
+
for (const tp of testPositions) {
|
|
1329
|
+
for (const ep of existing.flatMap((p) => p.positions)) {
|
|
1330
|
+
if (hexDistance(tp, ep) < gap) {
|
|
1331
|
+
fits = false;
|
|
1332
|
+
break;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
if (!fits) break;
|
|
1336
|
+
}
|
|
1337
|
+
if (fits) return candidate;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return { q: existing.length * 20, r: 0 };
|
|
1341
|
+
}
|
|
1342
|
+
function estimateRadius(count) {
|
|
1343
|
+
return Math.ceil(Math.sqrt(count));
|
|
1344
|
+
}
|
|
1345
|
+
function computeCentroid(positions, size) {
|
|
1346
|
+
let sx = 0, sy = 0;
|
|
1347
|
+
for (const { q, r } of positions) {
|
|
1348
|
+
const { x, y } = hexToPixel(q, r, size);
|
|
1349
|
+
sx += x;
|
|
1350
|
+
sy += y;
|
|
1351
|
+
}
|
|
1352
|
+
return { x: sx / positions.length, y: sy / positions.length };
|
|
1353
|
+
}
|
|
1354
|
+
function groupBySubDomain(assets) {
|
|
1355
|
+
const map = /* @__PURE__ */ new Map();
|
|
1356
|
+
for (const a of assets) {
|
|
1357
|
+
if (!a.subDomain) continue;
|
|
1358
|
+
if (!map.has(a.subDomain)) map.set(a.subDomain, []);
|
|
1359
|
+
map.get(a.subDomain).push(a.id);
|
|
1360
|
+
}
|
|
1361
|
+
return Array.from(map.entries()).map(([subDomain, assetIds]) => ({
|
|
1362
|
+
subDomain,
|
|
1363
|
+
assetIds,
|
|
1364
|
+
centroid: { x: 0, y: 0 }
|
|
1365
|
+
// filled in after position assignment
|
|
1366
|
+
}));
|
|
1367
|
+
}
|
|
1368
|
+
function buildClusterLayout(nodes) {
|
|
1369
|
+
const size = HEX_SIZE;
|
|
1370
|
+
const byDomain = /* @__PURE__ */ new Map();
|
|
1371
|
+
for (const n of nodes) {
|
|
1372
|
+
const d = n.domain ?? "Other";
|
|
1373
|
+
if (!byDomain.has(d)) byDomain.set(d, []);
|
|
1374
|
+
byDomain.get(d).push(n);
|
|
1375
|
+
}
|
|
1376
|
+
const domainSizes = Array.from(byDomain.entries()).map(([domain, ns]) => ({ domain, count: ns.length })).sort((a, b) => b.count - a.count);
|
|
1377
|
+
const allDomains = domainSizes.map((d) => d.domain);
|
|
1378
|
+
const placements = layoutClusters(domainSizes);
|
|
1379
|
+
const clusters = [];
|
|
1380
|
+
const subClustersMap = /* @__PURE__ */ new Map();
|
|
1381
|
+
for (const placement of placements) {
|
|
1382
|
+
const { domain, positions } = placement;
|
|
1383
|
+
const domainNodes = byDomain.get(domain) ?? [];
|
|
1384
|
+
const color = domainColor(domain, allDomains);
|
|
1385
|
+
const assets = domainNodes.map((n, i) => ({
|
|
1386
|
+
id: n.id,
|
|
1387
|
+
name: n.name,
|
|
1388
|
+
domain: n.domain ?? "Other",
|
|
1389
|
+
subDomain: n.subDomain,
|
|
1390
|
+
qualityScore: n.qualityScore,
|
|
1391
|
+
metadata: n.metadata ?? {},
|
|
1392
|
+
position: positions[i] ?? { q: 0, r: 0 }
|
|
1393
|
+
}));
|
|
1394
|
+
const centroid = computeCentroid(positions.slice(0, assets.length), size);
|
|
1395
|
+
const clusterId = `cluster:${domain}`;
|
|
1396
|
+
clusters.push({
|
|
1397
|
+
id: clusterId,
|
|
1398
|
+
label: domain,
|
|
1399
|
+
domain,
|
|
1400
|
+
color,
|
|
1401
|
+
assets,
|
|
1402
|
+
centroid
|
|
1403
|
+
});
|
|
1404
|
+
const subs = groupBySubDomain(assets);
|
|
1405
|
+
const assetById = new Map(assets.map((a) => [a.id, a]));
|
|
1406
|
+
for (const sub of subs) {
|
|
1407
|
+
const subPositions = sub.assetIds.map((id) => assetById.get(id)?.position).filter(Boolean);
|
|
1408
|
+
if (subPositions.length > 0) {
|
|
1409
|
+
sub.centroid = computeCentroid(subPositions, size);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
if (subs.length > 0) subClustersMap.set(clusterId, subs);
|
|
1413
|
+
}
|
|
1414
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1415
|
+
for (const cluster of clusters) {
|
|
1416
|
+
for (const asset of cluster.assets) {
|
|
1417
|
+
const { x, y } = hexToPixel(asset.position.q, asset.position.r, size);
|
|
1418
|
+
if (x < minX) minX = x;
|
|
1419
|
+
if (y < minY) minY = y;
|
|
1420
|
+
if (x > maxX) maxX = x;
|
|
1421
|
+
if (y > maxY) maxY = y;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
if (!isFinite(minX)) {
|
|
1425
|
+
minX = 0;
|
|
1426
|
+
minY = 0;
|
|
1427
|
+
maxX = 0;
|
|
1428
|
+
maxY = 0;
|
|
1429
|
+
}
|
|
1430
|
+
return { clusters, subClusters: subClustersMap, hexSize: size, bounds: { minX, minY, maxX, maxY } };
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// src/exporter.ts
|
|
1140
1434
|
function nodeLayer(type) {
|
|
1141
1435
|
if (type === "saas_tool") return "saas";
|
|
1142
1436
|
if (["web_service", "api_endpoint"].includes(type)) return "web";
|
|
@@ -1880,7 +2174,7 @@ function exportSOPMarkdown(sop) {
|
|
|
1880
2174
|
}
|
|
1881
2175
|
return lines.join("\n");
|
|
1882
2176
|
}
|
|
1883
|
-
function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml", "html", "sops"]) {
|
|
2177
|
+
function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml", "html", "hexmap", "sops"]) {
|
|
1884
2178
|
mkdirSync2(outputDir, { recursive: true });
|
|
1885
2179
|
mkdirSync2(join2(outputDir, "sops"), { recursive: true });
|
|
1886
2180
|
mkdirSync2(join2(outputDir, "workflows"), { recursive: true });
|
|
@@ -1916,12 +2210,795 @@ function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml
|
|
|
1916
2210
|
`);
|
|
1917
2211
|
}
|
|
1918
2212
|
}
|
|
2213
|
+
if (formats.includes("hexmap")) {
|
|
2214
|
+
const connections = db.getConnections(sessionId);
|
|
2215
|
+
writeFileSync(join2(outputDir, "hexmap.html"), exportHexMap(nodes, connections));
|
|
2216
|
+
process.stderr.write("\u2713 hexmap.html\n");
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
function exportHexMap(nodes, connections) {
|
|
2220
|
+
const layout = buildClusterLayout(nodes);
|
|
2221
|
+
const { clusters, subClusters, hexSize, bounds } = layout;
|
|
2222
|
+
const clustersJson = JSON.stringify(clusters.map((c) => ({
|
|
2223
|
+
id: c.id,
|
|
2224
|
+
label: c.label,
|
|
2225
|
+
domain: c.domain,
|
|
2226
|
+
color: c.color,
|
|
2227
|
+
centroid: c.centroid,
|
|
2228
|
+
assets: c.assets.map((a) => ({
|
|
2229
|
+
id: a.id,
|
|
2230
|
+
name: a.name,
|
|
2231
|
+
domain: a.domain,
|
|
2232
|
+
subDomain: a.subDomain ?? null,
|
|
2233
|
+
qualityScore: a.qualityScore ?? null,
|
|
2234
|
+
metadata: a.metadata,
|
|
2235
|
+
q: a.position.q,
|
|
2236
|
+
r: a.position.r
|
|
2237
|
+
}))
|
|
2238
|
+
})));
|
|
2239
|
+
const subClustersJson = JSON.stringify(
|
|
2240
|
+
Object.fromEntries(
|
|
2241
|
+
Array.from(subClusters.entries()).map(([cid, subs]) => [
|
|
2242
|
+
cid,
|
|
2243
|
+
subs.map((s) => ({ subDomain: s.subDomain, assetIds: s.assetIds, centroid: s.centroid }))
|
|
2244
|
+
])
|
|
2245
|
+
)
|
|
2246
|
+
);
|
|
2247
|
+
const connectionsJson = JSON.stringify(connections.map((c) => ({
|
|
2248
|
+
id: c.id,
|
|
2249
|
+
sourceAssetId: c.sourceAssetId,
|
|
2250
|
+
targetAssetId: c.targetAssetId,
|
|
2251
|
+
type: c.type ?? "connection"
|
|
2252
|
+
})));
|
|
2253
|
+
const hexSizeVal = hexSize;
|
|
2254
|
+
const isEmpty = nodes.length === 0;
|
|
2255
|
+
return `<!DOCTYPE html>
|
|
2256
|
+
<html lang="en">
|
|
2257
|
+
<head>
|
|
2258
|
+
<meta charset="UTF-8"/>
|
|
2259
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
2260
|
+
<title>Data Cartography Map</title>
|
|
2261
|
+
<style>
|
|
2262
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
2263
|
+
html,body{width:100%;height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
|
|
2264
|
+
body{display:flex;flex-direction:column;background:#f8fafc;color:#1e293b}
|
|
2265
|
+
#topbar{
|
|
2266
|
+
height:48px;display:flex;align-items:center;gap:16px;padding:0 20px;
|
|
2267
|
+
background:#fff;border-bottom:1px solid #e2e8f0;z-index:10;flex-shrink:0;
|
|
2268
|
+
}
|
|
2269
|
+
#topbar h1{font-size:15px;font-weight:600;color:#0f172a;letter-spacing:-0.01em}
|
|
2270
|
+
#topbar .nav-items{display:flex;gap:4px;margin-left:auto}
|
|
2271
|
+
#topbar .nav-item{
|
|
2272
|
+
padding:5px 12px;border-radius:6px;font-size:13px;cursor:pointer;
|
|
2273
|
+
color:#64748b;border:none;background:transparent;
|
|
2274
|
+
}
|
|
2275
|
+
#topbar .nav-item:hover{background:#f1f5f9;color:#0f172a}
|
|
2276
|
+
#topbar .nav-item.active{background:#eff6ff;color:#2563eb;font-weight:500}
|
|
2277
|
+
#search-box{
|
|
2278
|
+
display:flex;align-items:center;gap:8px;background:#f1f5f9;
|
|
2279
|
+
border-radius:8px;padding:5px 10px;margin-left:8px;
|
|
2280
|
+
}
|
|
2281
|
+
#search-box input{
|
|
2282
|
+
border:none;background:transparent;font-size:13px;outline:none;width:160px;color:#0f172a;
|
|
2283
|
+
}
|
|
2284
|
+
#search-box input::placeholder{color:#94a3b8}
|
|
2285
|
+
#search-icon{color:#94a3b8;font-size:14px}
|
|
2286
|
+
#main{flex:1;display:flex;overflow:hidden;position:relative}
|
|
2287
|
+
#canvas-wrap{flex:1;position:relative;overflow:hidden;cursor:grab}
|
|
2288
|
+
#canvas-wrap.dragging{cursor:grabbing}
|
|
2289
|
+
#canvas-wrap.connecting{cursor:crosshair}
|
|
2290
|
+
canvas{display:block;width:100%;height:100%}
|
|
2291
|
+
/* Detail panel */
|
|
2292
|
+
#detail-panel{
|
|
2293
|
+
width:280px;background:#fff;border-left:1px solid #e2e8f0;
|
|
2294
|
+
display:flex;flex-direction:column;transform:translateX(100%);
|
|
2295
|
+
transition:transform .2s ease;z-index:5;flex-shrink:0;overflow-y:auto;
|
|
2296
|
+
}
|
|
2297
|
+
#detail-panel.open{transform:translateX(0)}
|
|
2298
|
+
#detail-panel .panel-header{
|
|
2299
|
+
padding:16px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;gap:10px;
|
|
2300
|
+
}
|
|
2301
|
+
#detail-panel .panel-header h3{font-size:14px;font-weight:600;flex:1;word-break:break-word}
|
|
2302
|
+
#detail-panel .close-btn{
|
|
2303
|
+
width:24px;height:24px;border:none;background:transparent;cursor:pointer;
|
|
2304
|
+
color:#94a3b8;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:16px;
|
|
2305
|
+
}
|
|
2306
|
+
#detail-panel .close-btn:hover{background:#f1f5f9;color:#0f172a}
|
|
2307
|
+
#detail-panel .panel-body{padding:12px 16px;display:flex;flex-direction:column;gap:12px}
|
|
2308
|
+
#detail-panel .meta-row{display:flex;flex-direction:column;gap:3px}
|
|
2309
|
+
#detail-panel .meta-label{font-size:11px;font-weight:500;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em}
|
|
2310
|
+
#detail-panel .meta-value{font-size:13px;color:#1e293b;word-break:break-all}
|
|
2311
|
+
#detail-panel .quality-bar{height:6px;border-radius:3px;background:#e2e8f0;margin-top:4px}
|
|
2312
|
+
#detail-panel .quality-fill{height:6px;border-radius:3px;transition:width .3s}
|
|
2313
|
+
#detail-panel .badge{
|
|
2314
|
+
display:inline-flex;align-items:center;gap:4px;padding:2px 8px;
|
|
2315
|
+
border-radius:12px;font-size:11px;font-weight:500;
|
|
2316
|
+
}
|
|
2317
|
+
/* Bottom-left toolbar */
|
|
2318
|
+
#toolbar-left{
|
|
2319
|
+
position:absolute;bottom:20px;left:20px;display:flex;gap:8px;z-index:10;
|
|
2320
|
+
}
|
|
2321
|
+
.tb-btn{
|
|
2322
|
+
width:40px;height:40px;border-radius:10px;border:1px solid #e2e8f0;
|
|
2323
|
+
background:#fff;box-shadow:0 1px 4px rgba(0,0,0,.08);cursor:pointer;
|
|
2324
|
+
display:flex;align-items:center;justify-content:center;font-size:18px;
|
|
2325
|
+
transition:all .15s;position:relative;
|
|
2326
|
+
}
|
|
2327
|
+
.tb-btn:hover{border-color:#94a3b8;box-shadow:0 2px 8px rgba(0,0,0,.12)}
|
|
2328
|
+
.tb-btn.active{background:#eff6ff;border-color:#3b82f6}
|
|
2329
|
+
.tb-btn[title]:hover::after{
|
|
2330
|
+
content:attr(title);position:absolute;bottom:calc(100% + 6px);left:50%;
|
|
2331
|
+
transform:translateX(-50%);background:#1e293b;color:#fff;padding:4px 8px;
|
|
2332
|
+
border-radius:5px;font-size:11px;white-space:nowrap;pointer-events:none;
|
|
2333
|
+
}
|
|
2334
|
+
/* Bottom-right toolbar */
|
|
2335
|
+
#toolbar-right{
|
|
2336
|
+
position:absolute;bottom:20px;right:20px;display:flex;flex-direction:column;
|
|
2337
|
+
align-items:flex-end;gap:8px;z-index:10;
|
|
2338
|
+
}
|
|
2339
|
+
#zoom-controls{display:flex;align-items:center;gap:6px}
|
|
2340
|
+
.zoom-btn{
|
|
2341
|
+
width:34px;height:34px;border-radius:8px;border:1px solid #e2e8f0;
|
|
2342
|
+
background:#fff;box-shadow:0 1px 4px rgba(0,0,0,.08);cursor:pointer;
|
|
2343
|
+
font-size:18px;color:#1e293b;display:flex;align-items:center;justify-content:center;
|
|
2344
|
+
}
|
|
2345
|
+
.zoom-btn:hover{background:#f1f5f9}
|
|
2346
|
+
#zoom-pct{
|
|
2347
|
+
font-size:12px;font-weight:500;color:#64748b;min-width:38px;text-align:center;
|
|
2348
|
+
}
|
|
2349
|
+
#detail-selector{display:flex;flex-direction:column;gap:4px}
|
|
2350
|
+
.detail-btn{
|
|
2351
|
+
width:34px;height:34px;border-radius:8px;border:1px solid #e2e8f0;
|
|
2352
|
+
background:#fff;box-shadow:0 1px 4px rgba(0,0,0,.08);cursor:pointer;
|
|
2353
|
+
font-size:12px;font-weight:600;color:#64748b;display:flex;align-items:center;justify-content:center;
|
|
2354
|
+
}
|
|
2355
|
+
.detail-btn:hover{background:#f1f5f9;color:#0f172a}
|
|
2356
|
+
.detail-btn.active{background:#eff6ff;border-color:#3b82f6;color:#2563eb}
|
|
2357
|
+
#connect-btn{
|
|
2358
|
+
width:40px;height:40px;border-radius:10px;border:1px solid #e2e8f0;
|
|
2359
|
+
background:#fff;box-shadow:0 1px 4px rgba(0,0,0,.08);cursor:pointer;
|
|
2360
|
+
font-size:18px;display:flex;align-items:center;justify-content:center;
|
|
2361
|
+
}
|
|
2362
|
+
#connect-btn.active{background:#fef3c7;border-color:#f59e0b}
|
|
2363
|
+
/* Tooltip */
|
|
2364
|
+
#tooltip{
|
|
2365
|
+
position:fixed;background:#1e293b;color:#fff;border-radius:8px;
|
|
2366
|
+
padding:8px 12px;font-size:12px;pointer-events:none;z-index:100;
|
|
2367
|
+
display:none;max-width:200px;box-shadow:0 4px 12px rgba(0,0,0,.15);
|
|
2368
|
+
}
|
|
2369
|
+
#tooltip .tt-name{font-weight:600;margin-bottom:2px}
|
|
2370
|
+
#tooltip .tt-domain{color:#94a3b8;font-size:11px}
|
|
2371
|
+
/* Empty state */
|
|
2372
|
+
#empty-state{
|
|
2373
|
+
position:absolute;inset:0;display:flex;flex-direction:column;
|
|
2374
|
+
align-items:center;justify-content:center;gap:12px;color:#94a3b8;
|
|
2375
|
+
}
|
|
2376
|
+
#empty-state .es-icon{font-size:48px}
|
|
2377
|
+
#empty-state p{font-size:14px}
|
|
2378
|
+
/* Theme toggle */
|
|
2379
|
+
#theme-btn{
|
|
2380
|
+
width:40px;height:40px;border-radius:10px;border:1px solid #e2e8f0;
|
|
2381
|
+
background:#fff;box-shadow:0 1px 4px rgba(0,0,0,.08);cursor:pointer;
|
|
2382
|
+
font-size:18px;display:flex;align-items:center;justify-content:center;
|
|
2383
|
+
}
|
|
2384
|
+
/* Dark mode overrides */
|
|
2385
|
+
body.dark{background:#0f172a;color:#e2e8f0}
|
|
2386
|
+
body.dark #topbar{background:#1e293b;border-color:#334155}
|
|
2387
|
+
body.dark #topbar h1{color:#f1f5f9}
|
|
2388
|
+
body.dark #topbar .nav-item{color:#94a3b8}
|
|
2389
|
+
body.dark #topbar .nav-item:hover{background:#334155;color:#f1f5f9}
|
|
2390
|
+
body.dark #search-box{background:#334155}
|
|
2391
|
+
body.dark #search-box input{color:#f1f5f9}
|
|
2392
|
+
body.dark #detail-panel{background:#1e293b;border-color:#334155}
|
|
2393
|
+
body.dark #detail-panel .panel-header{border-color:#334155}
|
|
2394
|
+
body.dark #detail-panel .meta-value{color:#e2e8f0}
|
|
2395
|
+
body.dark .tb-btn,body.dark .zoom-btn,body.dark .detail-btn,body.dark #connect-btn,body.dark #theme-btn{
|
|
2396
|
+
background:#1e293b;border-color:#334155;color:#e2e8f0;
|
|
2397
|
+
}
|
|
2398
|
+
body.dark .tb-btn:hover,body.dark .zoom-btn:hover,body.dark .detail-btn:hover{background:#334155}
|
|
2399
|
+
body.dark #zoom-pct{color:#94a3b8}
|
|
2400
|
+
/* Connection mode indicator */
|
|
2401
|
+
#connect-hint{
|
|
2402
|
+
position:absolute;top:12px;left:50%;transform:translateX(-50%);
|
|
2403
|
+
background:#fef3c7;border:1px solid #f59e0b;color:#92400e;
|
|
2404
|
+
padding:6px 14px;border-radius:20px;font-size:12px;font-weight:500;
|
|
2405
|
+
display:none;z-index:20;pointer-events:none;
|
|
2406
|
+
}
|
|
2407
|
+
</style>
|
|
2408
|
+
</head>
|
|
2409
|
+
<body>
|
|
2410
|
+
<!-- Top bar -->
|
|
2411
|
+
<div id="topbar">
|
|
2412
|
+
<h1>\u{1F5FA} Data Cartography Map</h1>
|
|
2413
|
+
<div class="nav-items">
|
|
2414
|
+
<button class="nav-item active">Data Product Map</button>
|
|
2415
|
+
<button class="nav-item">Raw Data Map</button>
|
|
2416
|
+
<button class="nav-item">Analysis</button>
|
|
2417
|
+
</div>
|
|
2418
|
+
<div id="search-box">
|
|
2419
|
+
<span id="search-icon">\u2315</span>
|
|
2420
|
+
<input id="search-input" type="text" placeholder="Search assets\u2026" aria-label="Search data assets"/>
|
|
2421
|
+
</div>
|
|
2422
|
+
<button id="theme-btn" title="Toggle dark/light mode" aria-label="Toggle theme">\u{1F319}</button>
|
|
2423
|
+
</div>
|
|
2424
|
+
|
|
2425
|
+
<!-- Main area -->
|
|
2426
|
+
<div id="main">
|
|
2427
|
+
<div id="canvas-wrap" role="application" aria-label="Data cartography hex map" tabindex="0">
|
|
2428
|
+
<canvas id="hexmap" aria-hidden="true"></canvas>
|
|
2429
|
+
${isEmpty ? '<div id="empty-state"><div class="es-icon">\u{1F5FA}</div><p>No data assets available</p><p style="font-size:12px">Run <code>datasynx-cartography discover</code> to populate the map</p></div>' : ""}
|
|
2430
|
+
</div>
|
|
2431
|
+
<div id="detail-panel" role="complementary" aria-label="Asset details">
|
|
2432
|
+
<div class="panel-header">
|
|
2433
|
+
<h3 id="dp-name">\u2014</h3>
|
|
2434
|
+
<button class="close-btn" id="dp-close" aria-label="Close panel">\u2715</button>
|
|
2435
|
+
</div>
|
|
2436
|
+
<div class="panel-body" id="dp-body"></div>
|
|
2437
|
+
</div>
|
|
2438
|
+
</div>
|
|
2439
|
+
|
|
2440
|
+
<!-- Bottom-left toolbar -->
|
|
2441
|
+
<div id="toolbar-left">
|
|
2442
|
+
<button class="tb-btn active" id="btn-org" title="Organization view" aria-pressed="true" aria-label="Organization view">\u{1F3E2}</button>
|
|
2443
|
+
<button class="tb-btn active" id="btn-labels" title="Show labels" aria-pressed="true" aria-label="Toggle labels">\u{1F3F7}</button>
|
|
2444
|
+
<button class="tb-btn" id="btn-quality" title="Quality layer" aria-pressed="false" aria-label="Toggle quality layer">\u{1F441}</button>
|
|
2445
|
+
</div>
|
|
2446
|
+
|
|
2447
|
+
<!-- Bottom-right toolbar -->
|
|
2448
|
+
<div id="toolbar-right">
|
|
2449
|
+
<div id="zoom-controls">
|
|
2450
|
+
<button class="zoom-btn" id="zoom-out" aria-label="Zoom out">\u2212</button>
|
|
2451
|
+
<span id="zoom-pct">100%</span>
|
|
2452
|
+
<button class="zoom-btn" id="zoom-in" aria-label="Zoom in">+</button>
|
|
2453
|
+
</div>
|
|
2454
|
+
<div id="detail-selector">
|
|
2455
|
+
<button class="detail-btn" id="dl-1" aria-label="Detail level 1">1</button>
|
|
2456
|
+
<button class="detail-btn active" id="dl-2" aria-label="Detail level 2">2</button>
|
|
2457
|
+
<button class="detail-btn" id="dl-3" aria-label="Detail level 3">3</button>
|
|
2458
|
+
<button class="detail-btn" id="dl-4" aria-label="Detail level 4">4</button>
|
|
2459
|
+
</div>
|
|
2460
|
+
<button id="connect-btn" title="Connection tool" aria-label="Toggle connection tool">\u{1F517}</button>
|
|
2461
|
+
</div>
|
|
2462
|
+
|
|
2463
|
+
<!-- Connection mode hint -->
|
|
2464
|
+
<div id="connect-hint">Click two assets to create a connection</div>
|
|
2465
|
+
|
|
2466
|
+
<!-- Hover tooltip -->
|
|
2467
|
+
<div id="tooltip" role="tooltip">
|
|
2468
|
+
<div class="tt-name" id="tt-name"></div>
|
|
2469
|
+
<div class="tt-domain" id="tt-domain"></div>
|
|
2470
|
+
</div>
|
|
2471
|
+
|
|
2472
|
+
<script>
|
|
2473
|
+
(function() {
|
|
2474
|
+
'use strict';
|
|
2475
|
+
|
|
2476
|
+
// \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
|
|
2477
|
+
const CLUSTERS = ${clustersJson};
|
|
2478
|
+
const SUB_CLUSTERS = ${subClustersJson};
|
|
2479
|
+
const CONNECTIONS = ${connectionsJson};
|
|
2480
|
+
const HEX_SIZE = ${hexSizeVal};
|
|
2481
|
+
const IS_EMPTY = ${isEmpty};
|
|
2482
|
+
|
|
2483
|
+
// Build flat asset index
|
|
2484
|
+
const assetIndex = new Map();
|
|
2485
|
+
for (const c of CLUSTERS) {
|
|
2486
|
+
for (const a of c.assets) {
|
|
2487
|
+
assetIndex.set(a.id, { ...a, clusterColor: c.color, clusterId: c.id });
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
// \u2500\u2500 Canvas 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\u2500
|
|
2492
|
+
const canvas = document.getElementById('hexmap');
|
|
2493
|
+
const ctx = canvas.getContext('2d');
|
|
2494
|
+
const wrap = document.getElementById('canvas-wrap');
|
|
2495
|
+
|
|
2496
|
+
let W = 0, H = 0;
|
|
2497
|
+
function resize() {
|
|
2498
|
+
const dpr = window.devicePixelRatio || 1;
|
|
2499
|
+
W = wrap.clientWidth; H = wrap.clientHeight;
|
|
2500
|
+
canvas.width = W * dpr; canvas.height = H * dpr;
|
|
2501
|
+
canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
|
|
2502
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
2503
|
+
draw();
|
|
2504
|
+
}
|
|
2505
|
+
window.addEventListener('resize', resize);
|
|
2506
|
+
|
|
2507
|
+
// \u2500\u2500 Viewport 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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2508
|
+
let vx = 0, vy = 0, scale = 1;
|
|
2509
|
+
let detailLevel = 2;
|
|
2510
|
+
let showLabels = true;
|
|
2511
|
+
let showQuality = false;
|
|
2512
|
+
let showOrg = true;
|
|
2513
|
+
let isDark = false;
|
|
2514
|
+
let connectMode = false;
|
|
2515
|
+
let connectFirst = null;
|
|
2516
|
+
let hoveredAssetId = null;
|
|
2517
|
+
let selectedAssetId = null;
|
|
2518
|
+
let searchQuery = '';
|
|
2519
|
+
let localConnections = [...CONNECTIONS];
|
|
2520
|
+
|
|
2521
|
+
function fitToView() {
|
|
2522
|
+
if (IS_EMPTY || CLUSTERS.length === 0) { vx = 0; vy = 0; scale = 1; return; }
|
|
2523
|
+
let minX=Infinity, minY=Infinity, maxX=-Infinity, maxY=-Infinity;
|
|
2524
|
+
for (const c of CLUSTERS) for (const a of c.assets) {
|
|
2525
|
+
const px = hexToPixelX(a.q, a.r);
|
|
2526
|
+
const py = hexToPixelY(a.q, a.r);
|
|
2527
|
+
if(px<minX)minX=px; if(py<minY)minY=py;
|
|
2528
|
+
if(px>maxX)maxX=px; if(py>maxY)maxY=py;
|
|
2529
|
+
}
|
|
2530
|
+
const pw = maxX-minX+HEX_SIZE*4, ph = maxY-minY+HEX_SIZE*4;
|
|
2531
|
+
scale = Math.min(W/pw, H/ph, 1) * 0.85;
|
|
2532
|
+
vx = W/2 - ((minX+maxX)/2)*scale;
|
|
2533
|
+
vy = H/2 - ((minY+maxY)/2)*scale;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
// \u2500\u2500 Hex math (inline for self-contained HTML) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2537
|
+
function hexToPixelX(q, r) { return HEX_SIZE * (Math.sqrt(3)*q + Math.sqrt(3)/2*r); }
|
|
2538
|
+
function hexToPixelY(q, r) { return HEX_SIZE * (3/2*r); }
|
|
2539
|
+
|
|
2540
|
+
function worldToScreen(wx, wy) {
|
|
2541
|
+
return { x: wx*scale+vx, y: wy*scale+vy };
|
|
2542
|
+
}
|
|
2543
|
+
function screenToWorld(sx, sy) {
|
|
2544
|
+
return { x: (sx-vx)/scale, y: (sy-vy)/scale };
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
// \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
|
|
2548
|
+
function hexPath(cx, cy, r) {
|
|
2549
|
+
ctx.beginPath();
|
|
2550
|
+
for (let i=0;i<6;i++) {
|
|
2551
|
+
const angle = Math.PI/180*(60*i-30);
|
|
2552
|
+
const x = cx+r*Math.cos(angle), y = cy+r*Math.sin(angle);
|
|
2553
|
+
i===0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y);
|
|
2554
|
+
}
|
|
2555
|
+
ctx.closePath();
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
function draw() {
|
|
2559
|
+
ctx.clearRect(0, 0, W, H);
|
|
2560
|
+
|
|
2561
|
+
// Background
|
|
2562
|
+
ctx.fillStyle = isDark ? '#0f172a' : '#f8fafc';
|
|
2563
|
+
ctx.fillRect(0, 0, W, H);
|
|
2564
|
+
|
|
2565
|
+
if (IS_EMPTY) return;
|
|
2566
|
+
|
|
2567
|
+
const size = HEX_SIZE * scale;
|
|
2568
|
+
const matchedIds = getSearchMatches();
|
|
2569
|
+
const hasSearch = searchQuery.length > 0;
|
|
2570
|
+
|
|
2571
|
+
// \u2500\u2500 Draw connections (edges) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2572
|
+
ctx.save();
|
|
2573
|
+
ctx.strokeStyle = isDark ? 'rgba(148,163,184,0.4)' : 'rgba(100,116,139,0.3)';
|
|
2574
|
+
ctx.lineWidth = 1.5;
|
|
2575
|
+
ctx.setLineDash([4,4]);
|
|
2576
|
+
for (const conn of localConnections) {
|
|
2577
|
+
const src = assetIndex.get(conn.sourceAssetId);
|
|
2578
|
+
const tgt = assetIndex.get(conn.targetAssetId);
|
|
2579
|
+
if (!src || !tgt) continue;
|
|
2580
|
+
const sp = worldToScreen(hexToPixelX(src.q, src.r), hexToPixelY(src.q, src.r));
|
|
2581
|
+
const tp = worldToScreen(hexToPixelX(tgt.q, tgt.r), hexToPixelY(tgt.q, tgt.r));
|
|
2582
|
+
ctx.beginPath(); ctx.moveTo(sp.x, sp.y); ctx.lineTo(tp.x, tp.y); ctx.stroke();
|
|
2583
|
+
}
|
|
2584
|
+
ctx.setLineDash([]);
|
|
2585
|
+
ctx.restore();
|
|
2586
|
+
|
|
2587
|
+
// \u2500\u2500 Draw hexagons per cluster \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2588
|
+
for (const cluster of CLUSTERS) {
|
|
2589
|
+
const baseColor = cluster.color;
|
|
2590
|
+
const isClusterMatch = !hasSearch || cluster.assets.some(a =>
|
|
2591
|
+
matchedIds.has(a.id)
|
|
2592
|
+
);
|
|
2593
|
+
const clusterDim = hasSearch && !isClusterMatch;
|
|
2594
|
+
|
|
2595
|
+
for (let ai=0; ai<cluster.assets.length; ai++) {
|
|
2596
|
+
const asset = cluster.assets[ai];
|
|
2597
|
+
const wx = hexToPixelX(asset.q, asset.r);
|
|
2598
|
+
const wy = hexToPixelY(asset.q, asset.r);
|
|
2599
|
+
const s = worldToScreen(wx, wy);
|
|
2600
|
+
const cx = s.x, cy = s.y;
|
|
2601
|
+
|
|
2602
|
+
// Frustum cull
|
|
2603
|
+
if (cx+size<0 || cx-size>W || cy+size<0 || cy-size>H) continue;
|
|
2604
|
+
|
|
2605
|
+
// Shade variation: every 3rd hex slightly lighter
|
|
2606
|
+
const shade = ai%3===0 ? 18 : ai%3===1 ? 8 : 0;
|
|
2607
|
+
let fillColor = shadeVariant(baseColor, shade);
|
|
2608
|
+
|
|
2609
|
+
// Quality layer override
|
|
2610
|
+
if (showQuality && asset.qualityScore !== null) {
|
|
2611
|
+
const q = asset.qualityScore;
|
|
2612
|
+
if (q < 40) fillColor = '#ef4444';
|
|
2613
|
+
else if (q < 70) fillColor = '#f97316';
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
// Dim non-matching in search
|
|
2617
|
+
const alpha = clusterDim ? 0.18 : 1;
|
|
2618
|
+
|
|
2619
|
+
// Hover / selected highlight
|
|
2620
|
+
const isHovered = asset.id === hoveredAssetId;
|
|
2621
|
+
const isSelected = asset.id === selectedAssetId;
|
|
2622
|
+
const isConnectFirst = asset.id === connectFirst;
|
|
2623
|
+
|
|
2624
|
+
ctx.save();
|
|
2625
|
+
ctx.globalAlpha = alpha;
|
|
2626
|
+
|
|
2627
|
+
hexPath(cx, cy, size*0.92);
|
|
2628
|
+
|
|
2629
|
+
if (isDark) {
|
|
2630
|
+
// Glow effect in dark mode
|
|
2631
|
+
if (isHovered || isSelected || isConnectFirst) {
|
|
2632
|
+
ctx.shadowColor = fillColor;
|
|
2633
|
+
ctx.shadowBlur = isSelected ? 16 : 8;
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
ctx.fillStyle = fillColor;
|
|
2638
|
+
ctx.fill();
|
|
2639
|
+
|
|
2640
|
+
if (isSelected || isConnectFirst) {
|
|
2641
|
+
ctx.strokeStyle = isConnectFirst ? '#f59e0b' : '#fff';
|
|
2642
|
+
ctx.lineWidth = 2.5;
|
|
2643
|
+
ctx.stroke();
|
|
2644
|
+
} else if (isHovered) {
|
|
2645
|
+
ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.2)';
|
|
2646
|
+
ctx.lineWidth = 1.5;
|
|
2647
|
+
ctx.stroke();
|
|
2648
|
+
} else {
|
|
2649
|
+
ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.4)';
|
|
2650
|
+
ctx.lineWidth = 1;
|
|
2651
|
+
ctx.stroke();
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
ctx.restore();
|
|
2655
|
+
|
|
2656
|
+
// Quality dot indicator
|
|
2657
|
+
if (showQuality && asset.qualityScore !== null && size > 8) {
|
|
2658
|
+
const q = asset.qualityScore;
|
|
2659
|
+
if (q < 70) {
|
|
2660
|
+
ctx.beginPath();
|
|
2661
|
+
ctx.arc(cx+size*0.4, cy-size*0.4, Math.max(3, size*0.14), 0, Math.PI*2);
|
|
2662
|
+
ctx.fillStyle = q<40 ? '#ef4444' : '#f97316';
|
|
2663
|
+
ctx.fill();
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
// Asset-level labels (detail 4, or 3 at high zoom)
|
|
2668
|
+
const showAssetLabel = showLabels && !clusterDim && (
|
|
2669
|
+
(detailLevel >= 4) ||
|
|
2670
|
+
(detailLevel === 3 && scale >= 0.8)
|
|
2671
|
+
);
|
|
2672
|
+
if (showAssetLabel && size > 14) {
|
|
2673
|
+
const label = asset.name.length > 12 ? asset.name.substring(0,11)+'\u2026' : asset.name;
|
|
2674
|
+
ctx.save();
|
|
2675
|
+
ctx.font = \`\${Math.max(8, Math.min(11, size*0.38))}px -apple-system,sans-serif\`;
|
|
2676
|
+
ctx.fillStyle = isDark ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.9)';
|
|
2677
|
+
ctx.textAlign = 'center';
|
|
2678
|
+
ctx.textBaseline = 'middle';
|
|
2679
|
+
ctx.fillText(label, cx, cy);
|
|
2680
|
+
ctx.restore();
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
// \u2500\u2500 Cluster labels (pill badges) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2686
|
+
if (showLabels && detailLevel >= 1) {
|
|
2687
|
+
for (const cluster of CLUSTERS) {
|
|
2688
|
+
if (cluster.assets.length === 0) continue;
|
|
2689
|
+
const hasSearch_ = searchQuery.length > 0;
|
|
2690
|
+
const isMatch = !hasSearch_ || cluster.assets.some(a => getSearchMatches().has(a.id));
|
|
2691
|
+
if (hasSearch_ && !isMatch) continue;
|
|
2692
|
+
|
|
2693
|
+
const s = worldToScreen(cluster.centroid.x, cluster.centroid.y);
|
|
2694
|
+
drawPillLabel(s.x, s.y, cluster.label, cluster.color, 14, isDark);
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
// \u2500\u2500 Sub-cluster labels (detail 2+) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2699
|
+
if (showLabels && detailLevel >= 2) {
|
|
2700
|
+
for (const [clusterId, subs] of Object.entries(SUB_CLUSTERS)) {
|
|
2701
|
+
for (const sub of subs) {
|
|
2702
|
+
const s = worldToScreen(sub.centroid.x, sub.centroid.y);
|
|
2703
|
+
// Offset slightly below cluster centroid
|
|
2704
|
+
drawPillLabel(s.x, s.y + size*1.8, sub.subDomain, '#64748b', 11, isDark);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
function shadeVariant(hex, amount) {
|
|
2711
|
+
if (!hex || hex.length < 7) return hex;
|
|
2712
|
+
const num = parseInt(hex.replace('#',''), 16);
|
|
2713
|
+
const r = Math.min(255, (num>>16) + amount);
|
|
2714
|
+
const g = Math.min(255, ((num>>8)&0xff) + amount);
|
|
2715
|
+
const b = Math.min(255, (num&0xff) + amount);
|
|
2716
|
+
return '#' + r.toString(16).padStart(2,'0') + g.toString(16).padStart(2,'0') + b.toString(16).padStart(2,'0');
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
function drawPillLabel(x, y, text, color, fontSize, dark) {
|
|
2720
|
+
if (!text) return;
|
|
2721
|
+
ctx.save();
|
|
2722
|
+
ctx.font = \`600 \${fontSize}px -apple-system,sans-serif\`;
|
|
2723
|
+
const tw = ctx.measureText(text).width;
|
|
2724
|
+
const ph = fontSize+8, pw = tw+20;
|
|
2725
|
+
// Pill background
|
|
2726
|
+
ctx.beginPath();
|
|
2727
|
+
ctx.roundRect(x-pw/2, y-ph/2, pw, ph, ph/2);
|
|
2728
|
+
ctx.fillStyle = dark ? 'rgba(30,41,59,0.9)' : 'rgba(255,255,255,0.92)';
|
|
2729
|
+
ctx.shadowColor = 'rgba(0,0,0,0.15)';
|
|
2730
|
+
ctx.shadowBlur = 6;
|
|
2731
|
+
ctx.fill();
|
|
2732
|
+
ctx.shadowBlur = 0;
|
|
2733
|
+
// Text
|
|
2734
|
+
ctx.fillStyle = dark ? '#e2e8f0' : '#0f172a';
|
|
2735
|
+
ctx.textAlign = 'center';
|
|
2736
|
+
ctx.textBaseline = 'middle';
|
|
2737
|
+
ctx.fillText(text, x, y);
|
|
2738
|
+
ctx.restore();
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
// \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
|
|
2742
|
+
function getAssetAtScreen(sx, sy) {
|
|
2743
|
+
const w = screenToWorld(sx, sy);
|
|
2744
|
+
const size = HEX_SIZE;
|
|
2745
|
+
for (const cluster of CLUSTERS) {
|
|
2746
|
+
for (const asset of cluster.assets) {
|
|
2747
|
+
const wx = hexToPixelX(asset.q, asset.r);
|
|
2748
|
+
const wy = hexToPixelY(asset.q, asset.r);
|
|
2749
|
+
const dx = Math.abs(w.x-wx), dy = Math.abs(w.y-wy);
|
|
2750
|
+
const hw = Math.sqrt(3)/2*size;
|
|
2751
|
+
if (dx > hw || dy > size) continue;
|
|
2752
|
+
if (hw*size - size*dx - (hw/2)*dy >= 0) return asset;
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
return null;
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
// \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
|
|
2759
|
+
function getSearchMatches() {
|
|
2760
|
+
if (!searchQuery) return new Set();
|
|
2761
|
+
const q = searchQuery.toLowerCase();
|
|
2762
|
+
const matches = new Set();
|
|
2763
|
+
for (const c of CLUSTERS) {
|
|
2764
|
+
for (const a of c.assets) {
|
|
2765
|
+
if (a.name.toLowerCase().includes(q) ||
|
|
2766
|
+
(a.domain && a.domain.toLowerCase().includes(q)) ||
|
|
2767
|
+
(a.subDomain && a.subDomain.toLowerCase().includes(q))) {
|
|
2768
|
+
matches.add(a.id);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
return matches;
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
// \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
|
|
2776
|
+
let dragging = false, lastMX = 0, lastMY = 0;
|
|
2777
|
+
|
|
2778
|
+
wrap.addEventListener('mousedown', e => {
|
|
2779
|
+
if (e.button !== 0) return;
|
|
2780
|
+
dragging = true; lastMX = e.clientX; lastMY = e.clientY;
|
|
2781
|
+
wrap.classList.add('dragging');
|
|
2782
|
+
});
|
|
2783
|
+
window.addEventListener('mouseup', () => { dragging = false; wrap.classList.remove('dragging'); });
|
|
2784
|
+
window.addEventListener('mousemove', e => {
|
|
2785
|
+
if (dragging) {
|
|
2786
|
+
vx += e.clientX - lastMX; vy += e.clientY - lastMY;
|
|
2787
|
+
lastMX = e.clientX; lastMY = e.clientY;
|
|
2788
|
+
draw();
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2791
|
+
const rect = wrap.getBoundingClientRect();
|
|
2792
|
+
const sx = e.clientX - rect.left, sy = e.clientY - rect.top;
|
|
2793
|
+
const asset = getAssetAtScreen(sx, sy);
|
|
2794
|
+
const newId = asset ? asset.id : null;
|
|
2795
|
+
if (newId !== hoveredAssetId) { hoveredAssetId = newId; draw(); }
|
|
2796
|
+
if (asset) {
|
|
2797
|
+
const tooltip = document.getElementById('tooltip');
|
|
2798
|
+
document.getElementById('tt-name').textContent = asset.name;
|
|
2799
|
+
document.getElementById('tt-domain').textContent = asset.domain + (asset.subDomain ? ' \u203A '+asset.subDomain : '');
|
|
2800
|
+
tooltip.style.display = 'block';
|
|
2801
|
+
tooltip.style.left = (e.clientX+12)+'px';
|
|
2802
|
+
tooltip.style.top = (e.clientY-8)+'px';
|
|
2803
|
+
} else {
|
|
2804
|
+
document.getElementById('tooltip').style.display = 'none';
|
|
2805
|
+
}
|
|
2806
|
+
});
|
|
2807
|
+
|
|
2808
|
+
wrap.addEventListener('click', e => {
|
|
2809
|
+
const rect = wrap.getBoundingClientRect();
|
|
2810
|
+
const sx = e.clientX-rect.left, sy = e.clientY-rect.top;
|
|
2811
|
+
const asset = getAssetAtScreen(sx, sy);
|
|
2812
|
+
if (connectMode) {
|
|
2813
|
+
if (!asset) return;
|
|
2814
|
+
if (!connectFirst) {
|
|
2815
|
+
connectFirst = asset.id; draw();
|
|
2816
|
+
} else if (connectFirst !== asset.id) {
|
|
2817
|
+
// Create connection
|
|
2818
|
+
const conn = {id: crypto.randomUUID(), sourceAssetId: connectFirst, targetAssetId: asset.id, type:'connection'};
|
|
2819
|
+
localConnections.push(conn);
|
|
2820
|
+
connectFirst = null;
|
|
2821
|
+
draw();
|
|
2822
|
+
}
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2825
|
+
if (asset) {
|
|
2826
|
+
selectedAssetId = asset.id;
|
|
2827
|
+
showDetailPanel(asset);
|
|
2828
|
+
} else {
|
|
2829
|
+
selectedAssetId = null;
|
|
2830
|
+
document.getElementById('detail-panel').classList.remove('open');
|
|
2831
|
+
}
|
|
2832
|
+
draw();
|
|
2833
|
+
});
|
|
2834
|
+
|
|
2835
|
+
// Touch pan
|
|
2836
|
+
let lastTouches = [];
|
|
2837
|
+
wrap.addEventListener('touchstart', e => { lastTouches = [...e.touches]; }, { passive:true });
|
|
2838
|
+
wrap.addEventListener('touchmove', e => {
|
|
2839
|
+
if (e.touches.length === 1) {
|
|
2840
|
+
const dx = e.touches[0].clientX - lastTouches[0].clientX;
|
|
2841
|
+
const dy = e.touches[0].clientY - lastTouches[0].clientY;
|
|
2842
|
+
vx += dx; vy += dy; draw();
|
|
2843
|
+
} else if (e.touches.length === 2) {
|
|
2844
|
+
// Pinch zoom
|
|
2845
|
+
const d0 = Math.hypot(lastTouches[0].clientX-lastTouches[1].clientX, lastTouches[0].clientY-lastTouches[1].clientY);
|
|
2846
|
+
const d1 = Math.hypot(e.touches[0].clientX-e.touches[1].clientX, e.touches[0].clientY-e.touches[1].clientY);
|
|
2847
|
+
const mx = (e.touches[0].clientX+e.touches[1].clientX)/2;
|
|
2848
|
+
const my = (e.touches[0].clientY+e.touches[1].clientY)/2;
|
|
2849
|
+
applyZoom(d1/d0, mx, my);
|
|
2850
|
+
}
|
|
2851
|
+
lastTouches = [...e.touches];
|
|
2852
|
+
}, { passive:true });
|
|
2853
|
+
|
|
2854
|
+
wrap.addEventListener('wheel', e => {
|
|
2855
|
+
e.preventDefault();
|
|
2856
|
+
const rect = wrap.getBoundingClientRect();
|
|
2857
|
+
const factor = e.deltaY < 0 ? 1.12 : 1/1.12;
|
|
2858
|
+
applyZoom(factor, e.clientX-rect.left, e.clientY-rect.top);
|
|
2859
|
+
}, { passive:false });
|
|
2860
|
+
|
|
2861
|
+
function applyZoom(factor, sx, sy) {
|
|
2862
|
+
const newScale = Math.max(0.05, Math.min(8, scale*factor));
|
|
2863
|
+
const wx = (sx-vx)/scale, wy = (sy-vy)/scale;
|
|
2864
|
+
scale = newScale;
|
|
2865
|
+
vx = sx - wx*scale; vy = sy - wy*scale;
|
|
2866
|
+
document.getElementById('zoom-pct').textContent = Math.round(scale*100)+'%';
|
|
2867
|
+
draw();
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
document.getElementById('zoom-in').addEventListener('click', () => applyZoom(1.25, W/2, H/2));
|
|
2871
|
+
document.getElementById('zoom-out').addEventListener('click', () => applyZoom(1/1.25, W/2, H/2));
|
|
2872
|
+
|
|
2873
|
+
// Keyboard navigation
|
|
2874
|
+
wrap.addEventListener('keydown', e => {
|
|
2875
|
+
const step = 40;
|
|
2876
|
+
if (e.key === 'ArrowLeft') { vx += step; draw(); }
|
|
2877
|
+
else if (e.key === 'ArrowRight') { vx -= step; draw(); }
|
|
2878
|
+
else if (e.key === 'ArrowUp') { vy += step; draw(); }
|
|
2879
|
+
else if (e.key === 'ArrowDown') { vy -= step; draw(); }
|
|
2880
|
+
else if (e.key === '+' || e.key === '=') applyZoom(1.2, W/2, H/2);
|
|
2881
|
+
else if (e.key === '-') applyZoom(1/1.2, W/2, H/2);
|
|
2882
|
+
else if (e.key === 'Escape') {
|
|
2883
|
+
selectedAssetId = null;
|
|
2884
|
+
document.getElementById('detail-panel').classList.remove('open');
|
|
2885
|
+
if (connectMode) toggleConnectMode();
|
|
2886
|
+
draw();
|
|
2887
|
+
}
|
|
2888
|
+
});
|
|
2889
|
+
|
|
2890
|
+
// \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
|
|
2891
|
+
function showDetailPanel(asset) {
|
|
2892
|
+
document.getElementById('dp-name').textContent = asset.name;
|
|
2893
|
+
const body = document.getElementById('dp-body');
|
|
2894
|
+
const rows = [
|
|
2895
|
+
['Domain', asset.domain],
|
|
2896
|
+
['Sub-domain', asset.subDomain],
|
|
2897
|
+
['Quality Score', asset.qualityScore !== null ? renderQuality(asset.qualityScore) : null],
|
|
2898
|
+
...Object.entries(asset.metadata || {}).slice(0,8).map(([k,v]) => [k, String(v)]),
|
|
2899
|
+
].filter(([,v]) => v !== null && v !== undefined && v !== '');
|
|
2900
|
+
|
|
2901
|
+
body.innerHTML = rows.map(([label, value]) => \`
|
|
2902
|
+
<div class="meta-row">
|
|
2903
|
+
<div class="meta-label">\${escHtml(String(label))}</div>
|
|
2904
|
+
<div class="meta-value">\${value}</div>
|
|
2905
|
+
</div>\`).join('');
|
|
2906
|
+
|
|
2907
|
+
// Connections
|
|
2908
|
+
const related = localConnections.filter(c => c.sourceAssetId===asset.id || c.targetAssetId===asset.id);
|
|
2909
|
+
if (related.length > 0) {
|
|
2910
|
+
body.innerHTML += \`<div class="meta-row"><div class="meta-label">Connections (\${related.length})</div><div>\${
|
|
2911
|
+
related.map(c => {
|
|
2912
|
+
const otherId = c.sourceAssetId===asset.id ? c.targetAssetId : c.sourceAssetId;
|
|
2913
|
+
const other = assetIndex.get(otherId);
|
|
2914
|
+
return \`<div class="meta-value" style="margin-top:4px;font-size:12px">\${other ? escHtml(other.name) : otherId}</div>\`;
|
|
2915
|
+
}).join('')
|
|
2916
|
+
}</div></div>\`;
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
document.getElementById('detail-panel').classList.add('open');
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
function renderQuality(score) {
|
|
2923
|
+
const color = score>=70 ? '#22c55e' : score>=40 ? '#f97316' : '#ef4444';
|
|
2924
|
+
return \`\${score}/100 <div class="quality-bar"><div class="quality-fill" style="width:\${score}%;background:\${color}"></div></div>\`;
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
function escHtml(s) {
|
|
2928
|
+
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
document.getElementById('dp-close').addEventListener('click', () => {
|
|
2932
|
+
document.getElementById('detail-panel').classList.remove('open');
|
|
2933
|
+
selectedAssetId = null; draw();
|
|
2934
|
+
});
|
|
2935
|
+
|
|
2936
|
+
// \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
|
|
2937
|
+
[1,2,3,4].forEach(n => {
|
|
2938
|
+
document.getElementById('dl-'+n).addEventListener('click', () => {
|
|
2939
|
+
detailLevel = n;
|
|
2940
|
+
document.querySelectorAll('.detail-btn').forEach(b => b.classList.remove('active'));
|
|
2941
|
+
document.getElementById('dl-'+n).classList.add('active');
|
|
2942
|
+
draw();
|
|
2943
|
+
});
|
|
2944
|
+
});
|
|
2945
|
+
|
|
2946
|
+
document.getElementById('btn-org').addEventListener('click', () => {
|
|
2947
|
+
showOrg = !showOrg;
|
|
2948
|
+
document.getElementById('btn-org').classList.toggle('active', showOrg);
|
|
2949
|
+
draw();
|
|
2950
|
+
});
|
|
2951
|
+
document.getElementById('btn-labels').addEventListener('click', () => {
|
|
2952
|
+
showLabels = !showLabels;
|
|
2953
|
+
document.getElementById('btn-labels').classList.toggle('active', showLabels);
|
|
2954
|
+
draw();
|
|
2955
|
+
});
|
|
2956
|
+
document.getElementById('btn-quality').addEventListener('click', () => {
|
|
2957
|
+
showQuality = !showQuality;
|
|
2958
|
+
document.getElementById('btn-quality').classList.toggle('active', showQuality);
|
|
2959
|
+
draw();
|
|
2960
|
+
});
|
|
2961
|
+
|
|
2962
|
+
function toggleConnectMode() {
|
|
2963
|
+
connectMode = !connectMode;
|
|
2964
|
+
connectFirst = null;
|
|
2965
|
+
document.getElementById('connect-btn').classList.toggle('active', connectMode);
|
|
2966
|
+
wrap.classList.toggle('connecting', connectMode);
|
|
2967
|
+
document.getElementById('connect-hint').style.display = connectMode ? 'block' : 'none';
|
|
2968
|
+
draw();
|
|
2969
|
+
}
|
|
2970
|
+
document.getElementById('connect-btn').addEventListener('click', toggleConnectMode);
|
|
2971
|
+
|
|
2972
|
+
document.getElementById('theme-btn').addEventListener('click', () => {
|
|
2973
|
+
isDark = !isDark;
|
|
2974
|
+
document.body.classList.toggle('dark', isDark);
|
|
2975
|
+
document.getElementById('theme-btn').textContent = isDark ? '\u2600\uFE0F' : '\u{1F319}';
|
|
2976
|
+
draw();
|
|
2977
|
+
});
|
|
2978
|
+
|
|
2979
|
+
// \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
|
|
2980
|
+
document.getElementById('search-input').addEventListener('input', e => {
|
|
2981
|
+
searchQuery = e.target.value.trim();
|
|
2982
|
+
draw();
|
|
2983
|
+
});
|
|
2984
|
+
|
|
2985
|
+
// \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
|
|
2986
|
+
resize();
|
|
2987
|
+
fitToView();
|
|
2988
|
+
document.getElementById('zoom-pct').textContent = Math.round(scale*100)+'%';
|
|
2989
|
+
draw();
|
|
2990
|
+
|
|
2991
|
+
})();
|
|
2992
|
+
</script>
|
|
2993
|
+
</body>
|
|
2994
|
+
</html>`;
|
|
1919
2995
|
}
|
|
1920
2996
|
|
|
1921
2997
|
// src/cli.ts
|
|
1922
|
-
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
2998
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1923
2999
|
import { resolve } from "path";
|
|
1924
3000
|
import { createInterface } from "readline";
|
|
3001
|
+
import { execSync as execSync2 } from "child_process";
|
|
1925
3002
|
var bold = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
1926
3003
|
var dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
1927
3004
|
var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
|
|
@@ -2146,8 +3223,16 @@ function main() {
|
|
|
2146
3223
|
exportAll(db, sessionId, config.outputDir);
|
|
2147
3224
|
const osc8 = (url, label) => `\x1B]8;;${url}\x1B\\${label}\x1B]8;;\x1B\\`;
|
|
2148
3225
|
const htmlPath = resolve(config.outputDir, "topology.html");
|
|
3226
|
+
const hexmapPath = resolve(config.outputDir, "hexmap.html");
|
|
2149
3227
|
const topoPath = resolve(config.outputDir, "topology.mermaid");
|
|
2150
3228
|
w("\n");
|
|
3229
|
+
if (existsSync2(hexmapPath)) {
|
|
3230
|
+
const fileUrl = `file://${hexmapPath}`;
|
|
3231
|
+
w(` ${green("\u2192")} ${osc8(fileUrl, bold("Open hexmap.html"))} ${dim("\u2190 Data Cartography Map")}
|
|
3232
|
+
`);
|
|
3233
|
+
w(` ${dim(fileUrl)}
|
|
3234
|
+
`);
|
|
3235
|
+
}
|
|
2151
3236
|
if (existsSync2(htmlPath)) {
|
|
2152
3237
|
const fileUrl = `file://${htmlPath}`;
|
|
2153
3238
|
w(` ${green("\u2192")} ${osc8(fileUrl, bold("Open topology.html"))}
|
|
@@ -2217,7 +3302,7 @@ function main() {
|
|
|
2217
3302
|
}
|
|
2218
3303
|
db.close();
|
|
2219
3304
|
});
|
|
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) => {
|
|
3305
|
+
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,hexmap,sops").action((sessionId, opts) => {
|
|
2221
3306
|
const config = defaultConfig({ outputDir: opts.output });
|
|
2222
3307
|
const db = new CartographyDB(config.dbPath);
|
|
2223
3308
|
const session = sessionId ? db.getSession(sessionId) : db.getLatestSession();
|
|
@@ -2227,12 +3312,44 @@ function main() {
|
|
|
2227
3312
|
process.exitCode = 1;
|
|
2228
3313
|
return;
|
|
2229
3314
|
}
|
|
2230
|
-
const formats = opts.format ?? ["mermaid", "json", "yaml", "html", "sops"];
|
|
3315
|
+
const formats = opts.format ?? ["mermaid", "json", "yaml", "html", "hexmap", "sops"];
|
|
2231
3316
|
exportAll(db, session.id, opts.output, formats);
|
|
2232
3317
|
process.stderr.write(`\u2713 Exported to: ${opts.output}
|
|
2233
3318
|
`);
|
|
2234
3319
|
db.close();
|
|
2235
3320
|
});
|
|
3321
|
+
program.command("map [session-id]").description("Open the interactive Data Cartography hex map in your browser").option("-o, --output <dir>", "Output directory", "./datasynx-output").action((sessionId, opts) => {
|
|
3322
|
+
const config = defaultConfig({ outputDir: opts.output });
|
|
3323
|
+
const db = new CartographyDB(config.dbPath);
|
|
3324
|
+
const session = sessionId ? db.getSession(sessionId) : db.getLatestSession();
|
|
3325
|
+
if (!session) {
|
|
3326
|
+
process.stderr.write("\u274C No session found. Run discover first.\n");
|
|
3327
|
+
db.close();
|
|
3328
|
+
process.exitCode = 1;
|
|
3329
|
+
return;
|
|
3330
|
+
}
|
|
3331
|
+
const nodes = db.getNodes(session.id);
|
|
3332
|
+
const connections = db.getConnections(session.id);
|
|
3333
|
+
const outDir = resolve(opts.output);
|
|
3334
|
+
const outPath = resolve(outDir, "hexmap.html");
|
|
3335
|
+
import("fs").then(({ mkdirSync: mkdirSync3 }) => mkdirSync3(outDir, { recursive: true })).catch(() => {
|
|
3336
|
+
});
|
|
3337
|
+
writeFileSync2(outPath, exportHexMap(nodes, connections));
|
|
3338
|
+
db.close();
|
|
3339
|
+
const osc8 = (url, label) => `\x1B]8;;${url}\x1B\\${label}\x1B]8;;\x1B\\`;
|
|
3340
|
+
const fileUrl = `file://${outPath}`;
|
|
3341
|
+
process.stderr.write(`
|
|
3342
|
+
${green("\u2192")} ${osc8(fileUrl, bold("Open hexmap.html"))}
|
|
3343
|
+
`);
|
|
3344
|
+
process.stderr.write(` ${dim(fileUrl)}
|
|
3345
|
+
|
|
3346
|
+
`);
|
|
3347
|
+
try {
|
|
3348
|
+
const cmd = process.platform === "darwin" ? `open "${outPath}"` : process.platform === "win32" ? `start "" "${outPath}"` : `xdg-open "${outPath}"`;
|
|
3349
|
+
execSync2(cmd, { stdio: "ignore" });
|
|
3350
|
+
} catch {
|
|
3351
|
+
}
|
|
3352
|
+
});
|
|
2236
3353
|
program.command("show [session-id]").description("Show session details").action((sessionId) => {
|
|
2237
3354
|
const config = defaultConfig();
|
|
2238
3355
|
const db = new CartographyDB(config.dbPath);
|
|
@@ -2591,6 +3708,9 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
2591
3708
|
const port = entry["port"];
|
|
2592
3709
|
const tags = entry["tags"] ?? [];
|
|
2593
3710
|
const metadata = entry["metadata"] ?? {};
|
|
3711
|
+
const domain = entry["domain"];
|
|
3712
|
+
const subDomain = entry["subDomain"];
|
|
3713
|
+
const qualityScore = entry["qualityScore"];
|
|
2594
3714
|
if (!type || !name) {
|
|
2595
3715
|
w(yellow(` \u26A0 Skipped (no type/name): ${JSON.stringify(entry)}
|
|
2596
3716
|
`));
|
|
@@ -2604,7 +3724,10 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
2604
3724
|
discoveredVia: "manual",
|
|
2605
3725
|
confidence: 1,
|
|
2606
3726
|
metadata: { ...metadata, ...host ? { host } : {}, ...port ? { port } : {} },
|
|
2607
|
-
tags
|
|
3727
|
+
tags,
|
|
3728
|
+
domain,
|
|
3729
|
+
subDomain,
|
|
3730
|
+
qualityScore
|
|
2608
3731
|
});
|
|
2609
3732
|
out(` ${green("+")} ${cyan(id)} ${dim("(" + type + ")")}
|
|
2610
3733
|
`);
|
|
@@ -2617,7 +3740,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
2617
3740
|
`);
|
|
2618
3741
|
return;
|
|
2619
3742
|
}
|
|
2620
|
-
const { NODE_TYPES: NODE_TYPES2 } = await import("./types-
|
|
3743
|
+
const { NODE_TYPES: NODE_TYPES2 } = await import("./types-A6K73WE4.js");
|
|
2621
3744
|
if (!process.stdin.isTTY) {
|
|
2622
3745
|
w(red("\n \u2717 Interactive mode requires a terminal (use --file for non-interactive)\n\n"));
|
|
2623
3746
|
process.exitCode = 1;
|
|
@@ -2660,9 +3783,15 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
2660
3783
|
const hostRaw = (await ask(` ${cyan("Host / IP")} ${dim("[optional, Enter=skip]")}: `)).trim();
|
|
2661
3784
|
const portRaw = (await ask(` ${cyan("Port")} ${dim("[optional]")}: `)).trim();
|
|
2662
3785
|
const tagsRaw = (await ask(` ${cyan("Tags")} ${dim("[comma-separated, optional]")}: `)).trim();
|
|
3786
|
+
const domainRaw = (await ask(` ${cyan("Domain")} ${dim('[e.g. "Marketing", "Finance", optional]')}: `)).trim();
|
|
3787
|
+
const subDomainRaw = (await ask(` ${cyan("Sub-domain")} ${dim('[e.g. "Forecast client orders", optional]')}: `)).trim();
|
|
3788
|
+
const qualityRaw = (await ask(` ${cyan("Quality score")} ${dim("[0\u2013100, optional]")}: `)).trim();
|
|
2663
3789
|
const host = hostRaw || void 0;
|
|
2664
3790
|
const port = portRaw ? parseInt(portRaw, 10) : void 0;
|
|
2665
3791
|
const tags = tagsRaw ? tagsRaw.split(",").map((t) => t.trim()).filter(Boolean) : [];
|
|
3792
|
+
const domain = domainRaw || void 0;
|
|
3793
|
+
const subDomain = subDomainRaw || void 0;
|
|
3794
|
+
const qualityScore = qualityRaw ? Math.max(0, Math.min(100, parseFloat(qualityRaw))) : void 0;
|
|
2666
3795
|
const id = host ? `${nodeType}:${host}${port ? ":" + port : ""}` : `${nodeType}:${name.toLowerCase().replace(/\s+/g, "-")}`;
|
|
2667
3796
|
db.upsertNode(sessionId, {
|
|
2668
3797
|
id,
|
|
@@ -2671,7 +3800,10 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
2671
3800
|
discoveredVia: "manual",
|
|
2672
3801
|
confidence: 1,
|
|
2673
3802
|
metadata: { ...host ? { host } : {}, ...port ? { port } : {} },
|
|
2674
|
-
tags
|
|
3803
|
+
tags,
|
|
3804
|
+
domain,
|
|
3805
|
+
subDomain,
|
|
3806
|
+
qualityScore
|
|
2675
3807
|
});
|
|
2676
3808
|
out(` ${green("+")} ${cyan(id)}
|
|
2677
3809
|
`);
|
|
@@ -2692,7 +3824,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
2692
3824
|
`);
|
|
2693
3825
|
});
|
|
2694
3826
|
program.command("doctor").description("Check all requirements and cloud CLIs").action(async () => {
|
|
2695
|
-
const { execSync:
|
|
3827
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
2696
3828
|
const { existsSync: existsSync3, readFileSync: readFileSync3 } = await import("fs");
|
|
2697
3829
|
const { join: join3 } = await import("path");
|
|
2698
3830
|
const out = (s) => process.stdout.write(s);
|
|
@@ -2715,7 +3847,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
2715
3847
|
allGood = false;
|
|
2716
3848
|
}
|
|
2717
3849
|
try {
|
|
2718
|
-
const v =
|
|
3850
|
+
const v = execSync3("claude --version", { stdio: "pipe" }).toString().trim();
|
|
2719
3851
|
ok(`Claude CLI ${dim2(v)}`);
|
|
2720
3852
|
} catch {
|
|
2721
3853
|
err("Claude CLI not found \u2014 npm i -g @anthropic-ai/claude-code");
|
|
@@ -2739,7 +3871,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
2739
3871
|
allGood = false;
|
|
2740
3872
|
}
|
|
2741
3873
|
try {
|
|
2742
|
-
const v =
|
|
3874
|
+
const v = execSync3("kubectl version --client --short 2>/dev/null || kubectl version --client", { stdio: "pipe" }).toString().split("\n")[0]?.trim() ?? "";
|
|
2743
3875
|
ok(`kubectl ${dim2(v || "(client OK)")}`);
|
|
2744
3876
|
} catch {
|
|
2745
3877
|
warn(`kubectl not found ${dim2("\u2014 install: https://kubernetes.io/docs/tasks/tools/")}`);
|
|
@@ -2751,7 +3883,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
2751
3883
|
];
|
|
2752
3884
|
for (const [name, cmd, hint] of cloudClis) {
|
|
2753
3885
|
try {
|
|
2754
|
-
|
|
3886
|
+
execSync3(cmd, { stdio: "pipe" });
|
|
2755
3887
|
ok(`${name} ${dim2("(cloud scanning available)")}`);
|
|
2756
3888
|
} catch {
|
|
2757
3889
|
warn(`${name} not found ${dim2("\u2014 cloud scan skipped | " + hint)}`);
|
|
@@ -2763,7 +3895,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
2763
3895
|
];
|
|
2764
3896
|
for (const [name, cmd] of localTools) {
|
|
2765
3897
|
try {
|
|
2766
|
-
|
|
3898
|
+
execSync3(cmd, { stdio: "pipe" });
|
|
2767
3899
|
ok(`${name} ${dim2("(discovery tool)")}`);
|
|
2768
3900
|
} catch {
|
|
2769
3901
|
warn(`${name} not found ${dim2("\u2014 discovery without " + name + " will be limited")}`);
|