archtracker-mcp 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/README.ja.md +344 -0
- package/README.md +16 -321
- package/dist/bin.js +1 -1
- package/dist/bin.js.map +1 -1
- package/dist/cli/index.js +507 -1689
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.js +79 -17
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.js +507 -81
- package/dist/mcp/index.js.map +1 -1
- package/dist/web/viewer.js +1572 -0
- package/package.json +5 -3
package/dist/cli/index.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { watch } from "fs";
|
|
6
|
-
import { writeFile as
|
|
7
|
-
import { join as
|
|
6
|
+
import { writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
|
|
7
|
+
import { join as join10 } from "path";
|
|
8
8
|
|
|
9
9
|
// src/analyzer/analyze.ts
|
|
10
10
|
import { resolve as resolve3 } from "path";
|
|
@@ -1569,6 +1569,8 @@ var en = {
|
|
|
1569
1569
|
"diff.reasonRemoved": 'Dependency "{file}" was removed',
|
|
1570
1570
|
"diff.reasonModified": 'Dependency "{file}" had its dependencies changed',
|
|
1571
1571
|
"diff.reasonAdded": 'New dependency "{file}" was added',
|
|
1572
|
+
"diff.testSummary": " ... and {count} test/fixture file(s)",
|
|
1573
|
+
"diff.testAffectedSummary": " ... and {count} test/fixture-related review(s)",
|
|
1572
1574
|
// Search
|
|
1573
1575
|
"search.pathMatch": 'Path matches "{pattern}"',
|
|
1574
1576
|
"search.affected": 'May be affected by changes to "{file}" (via: {via})',
|
|
@@ -1630,6 +1632,12 @@ var en = {
|
|
|
1630
1632
|
"web.watching": "Watching {dir}/ for changes...",
|
|
1631
1633
|
"web.reloading": "File change detected, reloading...",
|
|
1632
1634
|
"web.reloaded": "Graph reloaded",
|
|
1635
|
+
// History
|
|
1636
|
+
"history.title": "# Snapshot History\n",
|
|
1637
|
+
"history.empty": "No snapshots found. Run `archtracker init` to create one.",
|
|
1638
|
+
"history.entry": " {ts} | {files} files, {edges} edges, {circular} circular{layers}",
|
|
1639
|
+
"history.count": "{count} snapshot(s) recorded",
|
|
1640
|
+
"history.snapshotNotFound": "Snapshot not found for timestamp: {ts}",
|
|
1633
1641
|
// Errors
|
|
1634
1642
|
"error.analyzer": "[Analysis Error] {message}",
|
|
1635
1643
|
"error.storage": "[Storage Error] {message}",
|
|
@@ -1662,6 +1670,8 @@ var ja = {
|
|
|
1662
1670
|
"diff.reasonRemoved": '\u4F9D\u5B58\u5148 "{file}" \u304C\u524A\u9664\u3055\u308C\u307E\u3057\u305F',
|
|
1663
1671
|
"diff.reasonModified": '\u4F9D\u5B58\u5148 "{file}" \u306E\u4F9D\u5B58\u95A2\u4FC2\u304C\u5909\u66F4\u3055\u308C\u307E\u3057\u305F',
|
|
1664
1672
|
"diff.reasonAdded": '\u65B0\u3057\u3044\u4F9D\u5B58\u5148 "{file}" \u304C\u8FFD\u52A0\u3055\u308C\u307E\u3057\u305F',
|
|
1673
|
+
"diff.testSummary": " ... \u4ED6 {count}\u4EF6\u306E\u30C6\u30B9\u30C8/\u30D5\u30A3\u30AF\u30B9\u30C1\u30E3\u30D5\u30A1\u30A4\u30EB",
|
|
1674
|
+
"diff.testAffectedSummary": " ... \u4ED6 {count}\u4EF6\u306E\u30C6\u30B9\u30C8/\u30D5\u30A3\u30AF\u30B9\u30C1\u30E3\u95A2\u9023\u306E\u78BA\u8A8D\u9805\u76EE",
|
|
1665
1675
|
// Search
|
|
1666
1676
|
"search.pathMatch": '\u30D1\u30B9\u304C "{pattern}" \u306B\u30DE\u30C3\u30C1',
|
|
1667
1677
|
"search.affected": '"{file}" \u306E\u5909\u66F4\u306B\u3088\u308A\u5F71\u97FF\u3092\u53D7\u3051\u308B\u53EF\u80FD\u6027\uFF08\u7D4C\u7531: {via}\uFF09',
|
|
@@ -1723,6 +1733,12 @@ var ja = {
|
|
|
1723
1733
|
"web.watching": "{dir}/ \u3092\u76E3\u8996\u4E2D...",
|
|
1724
1734
|
"web.reloading": "\u30D5\u30A1\u30A4\u30EB\u5909\u66F4\u3092\u691C\u51FA\u3001\u30EA\u30ED\u30FC\u30C9\u4E2D...",
|
|
1725
1735
|
"web.reloaded": "\u30B0\u30E9\u30D5\u3092\u66F4\u65B0\u3057\u307E\u3057\u305F",
|
|
1736
|
+
// History
|
|
1737
|
+
"history.title": "# \u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8\u5C65\u6B74\n",
|
|
1738
|
+
"history.empty": "\u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002`archtracker init` \u3067\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
1739
|
+
"history.entry": " {ts} | {files}\u30D5\u30A1\u30A4\u30EB, {edges}\u30A8\u30C3\u30B8, \u5FAA\u74B0{circular}\u4EF6{layers}",
|
|
1740
|
+
"history.count": "{count}\u4EF6\u306E\u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8\u3092\u8A18\u9332",
|
|
1741
|
+
"history.snapshotNotFound": "\u6307\u5B9A\u306E\u30BF\u30A4\u30E0\u30B9\u30BF\u30F3\u30D7\u306E\u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: {ts}",
|
|
1726
1742
|
// Errors
|
|
1727
1743
|
"error.analyzer": "[\u89E3\u6790\u30A8\u30E9\u30FC] {message}",
|
|
1728
1744
|
"error.storage": "[\u30B9\u30C8\u30EC\u30FC\u30B8\u30A8\u30E9\u30FC] {message}",
|
|
@@ -1807,14 +1823,14 @@ var LAYER_COLORS = [
|
|
|
1807
1823
|
"#ffa657",
|
|
1808
1824
|
"#7ee787"
|
|
1809
1825
|
];
|
|
1810
|
-
async function analyzeMultiLayer(projectRoot, layerDefs) {
|
|
1826
|
+
async function analyzeMultiLayer(projectRoot, layerDefs, globalExclude) {
|
|
1811
1827
|
const layers = {};
|
|
1812
1828
|
const layerMetadata = [];
|
|
1813
1829
|
for (let idx = 0; idx < layerDefs.length; idx++) {
|
|
1814
1830
|
const def = layerDefs[idx];
|
|
1815
1831
|
const targetDir = resolve4(projectRoot, def.targetDir);
|
|
1816
1832
|
const graph = await analyzeProject(targetDir, {
|
|
1817
|
-
exclude: def.exclude,
|
|
1833
|
+
exclude: def.exclude ?? globalExclude,
|
|
1818
1834
|
language: def.language
|
|
1819
1835
|
});
|
|
1820
1836
|
const language = def.language ?? await detectLanguage(targetDir) ?? "javascript";
|
|
@@ -2246,11 +2262,240 @@ function isNodeError(error) {
|
|
|
2246
2262
|
return error instanceof Error && "code" in error;
|
|
2247
2263
|
}
|
|
2248
2264
|
|
|
2265
|
+
// src/storage/graph-cache.ts
|
|
2266
|
+
import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2, stat as stat4 } from "fs/promises";
|
|
2267
|
+
import { join as join6, resolve as resolve5 } from "path";
|
|
2268
|
+
import { readdir as readdir3 } from "fs/promises";
|
|
2269
|
+
import { createHash } from "crypto";
|
|
2270
|
+
var CACHE_FILE = "graph.json";
|
|
2271
|
+
var ARCHTRACKER_DIR2 = ".archtracker";
|
|
2272
|
+
async function hashFile(filePath) {
|
|
2273
|
+
const content = await readFile3(filePath);
|
|
2274
|
+
return createHash("sha256").update(content).digest("hex");
|
|
2275
|
+
}
|
|
2276
|
+
async function collectFileFingerprints(dir, exclude = []) {
|
|
2277
|
+
const fingerprints = {};
|
|
2278
|
+
const excludeRegexes = exclude.map((p) => new RegExp(p));
|
|
2279
|
+
async function walk(currentDir) {
|
|
2280
|
+
let entries;
|
|
2281
|
+
try {
|
|
2282
|
+
entries = await readdir3(currentDir, { withFileTypes: true });
|
|
2283
|
+
} catch {
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
for (const entry of entries) {
|
|
2287
|
+
const fullPath = join6(currentDir, entry.name);
|
|
2288
|
+
const relativePath = fullPath.slice(dir.length + 1);
|
|
2289
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
2290
|
+
if (excludeRegexes.some((r) => r.test(relativePath))) continue;
|
|
2291
|
+
if (entry.isDirectory()) {
|
|
2292
|
+
await walk(fullPath);
|
|
2293
|
+
} else if (entry.isFile()) {
|
|
2294
|
+
try {
|
|
2295
|
+
const s = await stat4(fullPath);
|
|
2296
|
+
const hash = await hashFile(fullPath);
|
|
2297
|
+
fingerprints[relativePath] = { mtime: s.mtimeMs, hash };
|
|
2298
|
+
} catch {
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
await walk(dir);
|
|
2304
|
+
return fingerprints;
|
|
2305
|
+
}
|
|
2306
|
+
async function saveGraphCache(projectRoot, graph, options, extra) {
|
|
2307
|
+
const absRoot = resolve5(projectRoot);
|
|
2308
|
+
const dir = join6(absRoot, ARCHTRACKER_DIR2);
|
|
2309
|
+
await mkdir2(dir, { recursive: true });
|
|
2310
|
+
let fingerprints;
|
|
2311
|
+
if (extra?.layerDirs?.length) {
|
|
2312
|
+
fingerprints = {};
|
|
2313
|
+
for (const layerDir of extra.layerDirs) {
|
|
2314
|
+
const layerPath = resolve5(absRoot, layerDir);
|
|
2315
|
+
const layerFp = await collectFileFingerprints(layerPath, options.exclude);
|
|
2316
|
+
for (const [key, value] of Object.entries(layerFp)) {
|
|
2317
|
+
fingerprints[`${layerDir}/${key}`] = value;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
} else {
|
|
2321
|
+
fingerprints = await collectFingerprintsForGraph(absRoot, options);
|
|
2322
|
+
}
|
|
2323
|
+
let layersJsonHash;
|
|
2324
|
+
try {
|
|
2325
|
+
layersJsonHash = await hashFile(join6(dir, "layers.json"));
|
|
2326
|
+
} catch {
|
|
2327
|
+
}
|
|
2328
|
+
const mtimes = {};
|
|
2329
|
+
for (const [k, v] of Object.entries(fingerprints)) {
|
|
2330
|
+
mtimes[k] = v.mtime;
|
|
2331
|
+
}
|
|
2332
|
+
const cache = {
|
|
2333
|
+
version: "1.0",
|
|
2334
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2335
|
+
options: {
|
|
2336
|
+
targetDir: options.targetDir,
|
|
2337
|
+
projectRoot: options.projectRoot,
|
|
2338
|
+
language: options.language,
|
|
2339
|
+
exclude: options.exclude
|
|
2340
|
+
},
|
|
2341
|
+
fileMtimes: mtimes,
|
|
2342
|
+
fileHashes: Object.fromEntries(
|
|
2343
|
+
Object.entries(fingerprints).map(([k, v]) => [k, v.hash])
|
|
2344
|
+
),
|
|
2345
|
+
graph,
|
|
2346
|
+
multiLayer: extra?.multiLayer,
|
|
2347
|
+
layerMetadata: extra?.layerMetadata,
|
|
2348
|
+
layerDirs: extra?.layerDirs,
|
|
2349
|
+
layersJsonHash
|
|
2350
|
+
};
|
|
2351
|
+
await writeFile2(join6(dir, CACHE_FILE), JSON.stringify(cache), "utf-8");
|
|
2352
|
+
}
|
|
2353
|
+
async function loadGraphCache(projectRoot) {
|
|
2354
|
+
const absRoot = resolve5(projectRoot);
|
|
2355
|
+
const filePath = join6(absRoot, ARCHTRACKER_DIR2, CACHE_FILE);
|
|
2356
|
+
try {
|
|
2357
|
+
const raw = await readFile3(filePath, "utf-8");
|
|
2358
|
+
const data = JSON.parse(raw);
|
|
2359
|
+
if (data?.version !== "1.0" || !data?.graph || !data?.fileMtimes) {
|
|
2360
|
+
return null;
|
|
2361
|
+
}
|
|
2362
|
+
return data;
|
|
2363
|
+
} catch {
|
|
2364
|
+
return null;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
async function isGraphCacheValid(cache, projectRoot, options) {
|
|
2368
|
+
if (cache.options.targetDir !== options.targetDir || cache.options.projectRoot !== options.projectRoot || (cache.options.language ?? "") !== (options.language ?? "") || JSON.stringify(cache.options.exclude ?? []) !== JSON.stringify(options.exclude ?? [])) {
|
|
2369
|
+
return false;
|
|
2370
|
+
}
|
|
2371
|
+
const absRoot = resolve5(projectRoot);
|
|
2372
|
+
const targetPath = resolve5(absRoot, options.targetDir);
|
|
2373
|
+
const isMultiLayer = (cache.layerDirs?.length ?? 0) > 0;
|
|
2374
|
+
let currentLayersHash;
|
|
2375
|
+
try {
|
|
2376
|
+
currentLayersHash = await hashFile(join6(absRoot, ARCHTRACKER_DIR2, "layers.json"));
|
|
2377
|
+
} catch {
|
|
2378
|
+
}
|
|
2379
|
+
if ((cache.layersJsonHash ?? "") !== (currentLayersHash ?? "")) return false;
|
|
2380
|
+
let currentMtimes;
|
|
2381
|
+
if (isMultiLayer) {
|
|
2382
|
+
currentMtimes = {};
|
|
2383
|
+
for (const layerDir of cache.layerDirs) {
|
|
2384
|
+
const layerPath = resolve5(absRoot, layerDir);
|
|
2385
|
+
const dirMtimes = await collectFileMtimes(layerPath, options.exclude);
|
|
2386
|
+
for (const [key, value] of Object.entries(dirMtimes)) {
|
|
2387
|
+
currentMtimes[`${layerDir}/${key}`] = value;
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
} else {
|
|
2391
|
+
currentMtimes = await collectFileMtimes(targetPath, options.exclude);
|
|
2392
|
+
}
|
|
2393
|
+
const cachedKeys = new Set(Object.keys(cache.fileMtimes));
|
|
2394
|
+
const currentKeys = new Set(Object.keys(currentMtimes));
|
|
2395
|
+
if (cachedKeys.size !== currentKeys.size) return false;
|
|
2396
|
+
for (const key of cachedKeys) {
|
|
2397
|
+
if (!currentKeys.has(key)) return false;
|
|
2398
|
+
}
|
|
2399
|
+
const mtimeChanged = [];
|
|
2400
|
+
for (const key of cachedKeys) {
|
|
2401
|
+
if (Math.abs(cache.fileMtimes[key] - currentMtimes[key]) > 1) {
|
|
2402
|
+
mtimeChanged.push(key);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
if (mtimeChanged.length === 0) return true;
|
|
2406
|
+
if (!cache.fileHashes) return false;
|
|
2407
|
+
for (const key of mtimeChanged) {
|
|
2408
|
+
const cachedHash = cache.fileHashes[key];
|
|
2409
|
+
if (!cachedHash) return false;
|
|
2410
|
+
try {
|
|
2411
|
+
const fullPath = isMultiLayer ? join6(absRoot, key) : join6(targetPath, key);
|
|
2412
|
+
const currentHash = await hashFile(fullPath);
|
|
2413
|
+
if (currentHash !== cachedHash) return false;
|
|
2414
|
+
} catch {
|
|
2415
|
+
return false;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
return true;
|
|
2419
|
+
}
|
|
2420
|
+
async function collectFileMtimes(dir, exclude) {
|
|
2421
|
+
const mtimes = {};
|
|
2422
|
+
const excludeRegexes = (exclude ?? []).map((p) => new RegExp(p));
|
|
2423
|
+
async function walk(currentDir) {
|
|
2424
|
+
let entries;
|
|
2425
|
+
try {
|
|
2426
|
+
entries = await readdir3(currentDir, { withFileTypes: true });
|
|
2427
|
+
} catch {
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2430
|
+
for (const entry of entries) {
|
|
2431
|
+
const fullPath = join6(currentDir, entry.name);
|
|
2432
|
+
const relativePath = fullPath.slice(dir.length + 1);
|
|
2433
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
2434
|
+
if (excludeRegexes.some((r) => r.test(relativePath))) continue;
|
|
2435
|
+
if (entry.isDirectory()) {
|
|
2436
|
+
await walk(fullPath);
|
|
2437
|
+
} else if (entry.isFile()) {
|
|
2438
|
+
try {
|
|
2439
|
+
const s = await stat4(fullPath);
|
|
2440
|
+
mtimes[relativePath] = s.mtimeMs;
|
|
2441
|
+
} catch {
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
await walk(dir);
|
|
2447
|
+
return mtimes;
|
|
2448
|
+
}
|
|
2449
|
+
async function collectFingerprintsForGraph(absRoot, options) {
|
|
2450
|
+
const targetPath = resolve5(absRoot, options.targetDir);
|
|
2451
|
+
return collectFileFingerprints(targetPath, options.exclude);
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2249
2454
|
// src/analyzer/resolve.ts
|
|
2250
2455
|
async function resolveGraph(opts) {
|
|
2456
|
+
if (!opts.noCache) {
|
|
2457
|
+
const cache = await loadGraphCache(opts.projectRoot);
|
|
2458
|
+
if (cache) {
|
|
2459
|
+
const valid = await isGraphCacheValid(cache, opts.projectRoot, {
|
|
2460
|
+
targetDir: opts.targetDir,
|
|
2461
|
+
projectRoot: opts.projectRoot,
|
|
2462
|
+
language: opts.language,
|
|
2463
|
+
exclude: opts.exclude
|
|
2464
|
+
});
|
|
2465
|
+
if (valid) {
|
|
2466
|
+
const result2 = {
|
|
2467
|
+
graph: cache.graph,
|
|
2468
|
+
multiLayer: cache.multiLayer,
|
|
2469
|
+
layerMetadata: cache.layerMetadata,
|
|
2470
|
+
fromCache: true
|
|
2471
|
+
};
|
|
2472
|
+
if (cache.multiLayer) {
|
|
2473
|
+
const layerConfig2 = await loadLayerConfig(opts.projectRoot);
|
|
2474
|
+
if (layerConfig2) {
|
|
2475
|
+
const autoConnections = detectCrossLayerConnections(
|
|
2476
|
+
cache.multiLayer.layers,
|
|
2477
|
+
layerConfig2.layers
|
|
2478
|
+
);
|
|
2479
|
+
const manualConnections = layerConfig2.connections ?? [];
|
|
2480
|
+
const manualKeys = new Set(manualConnections.map(
|
|
2481
|
+
(c) => `${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`
|
|
2482
|
+
));
|
|
2483
|
+
result2.crossLayerEdges = [
|
|
2484
|
+
...manualConnections,
|
|
2485
|
+
...autoConnections.filter(
|
|
2486
|
+
(c) => !manualKeys.has(`${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`)
|
|
2487
|
+
)
|
|
2488
|
+
];
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
return result2;
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2251
2495
|
const layerConfig = await loadLayerConfig(opts.projectRoot);
|
|
2496
|
+
let result;
|
|
2252
2497
|
if (layerConfig) {
|
|
2253
|
-
const multi = await analyzeMultiLayer(opts.projectRoot, layerConfig.layers);
|
|
2498
|
+
const multi = await analyzeMultiLayer(opts.projectRoot, layerConfig.layers, opts.exclude);
|
|
2254
2499
|
const autoConnections = detectCrossLayerConnections(multi.layers, layerConfig.layers);
|
|
2255
2500
|
const manualConnections = layerConfig.connections ?? [];
|
|
2256
2501
|
const manualKeys = new Set(manualConnections.map(
|
|
@@ -2262,31 +2507,52 @@ async function resolveGraph(opts) {
|
|
|
2262
2507
|
(c) => !manualKeys.has(`${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`)
|
|
2263
2508
|
)
|
|
2264
2509
|
];
|
|
2265
|
-
|
|
2510
|
+
result = {
|
|
2266
2511
|
graph: multi.merged,
|
|
2267
2512
|
multiLayer: multi,
|
|
2268
2513
|
layerMetadata: multi.layerMetadata,
|
|
2269
2514
|
crossLayerEdges: merged
|
|
2270
2515
|
};
|
|
2516
|
+
} else {
|
|
2517
|
+
const graph = await analyzeProject(opts.targetDir, {
|
|
2518
|
+
exclude: opts.exclude,
|
|
2519
|
+
language: opts.language
|
|
2520
|
+
});
|
|
2521
|
+
result = { graph };
|
|
2271
2522
|
}
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2523
|
+
try {
|
|
2524
|
+
await saveGraphCache(
|
|
2525
|
+
opts.projectRoot,
|
|
2526
|
+
result.graph,
|
|
2527
|
+
{
|
|
2528
|
+
targetDir: opts.targetDir,
|
|
2529
|
+
projectRoot: opts.projectRoot,
|
|
2530
|
+
language: opts.language,
|
|
2531
|
+
exclude: opts.exclude
|
|
2532
|
+
},
|
|
2533
|
+
{
|
|
2534
|
+
multiLayer: result.multiLayer,
|
|
2535
|
+
layerMetadata: result.layerMetadata,
|
|
2536
|
+
layerDirs: layerConfig?.layers.map((l) => l.targetDir)
|
|
2537
|
+
}
|
|
2538
|
+
);
|
|
2539
|
+
} catch {
|
|
2540
|
+
}
|
|
2541
|
+
return result;
|
|
2277
2542
|
}
|
|
2278
2543
|
|
|
2279
2544
|
// src/storage/snapshot.ts
|
|
2280
|
-
import { mkdir as
|
|
2281
|
-
import { join as
|
|
2545
|
+
import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile4, readdir as readdir4, access } from "fs/promises";
|
|
2546
|
+
import { join as join7 } from "path";
|
|
2282
2547
|
import { z as z2 } from "zod";
|
|
2283
2548
|
|
|
2284
2549
|
// src/types/schema.ts
|
|
2285
2550
|
var SCHEMA_VERSION = "1.1";
|
|
2286
2551
|
|
|
2287
2552
|
// src/storage/snapshot.ts
|
|
2288
|
-
var
|
|
2553
|
+
var ARCHTRACKER_DIR3 = ".archtracker";
|
|
2289
2554
|
var SNAPSHOT_FILE = "snapshot.json";
|
|
2555
|
+
var HISTORY_DIR = "history";
|
|
2290
2556
|
var FileNodeSchema = z2.object({
|
|
2291
2557
|
path: z2.string(),
|
|
2292
2558
|
exists: z2.boolean(),
|
|
@@ -2311,25 +2577,34 @@ var SnapshotSchema = z2.object({
|
|
|
2311
2577
|
rootDir: z2.string(),
|
|
2312
2578
|
graph: DependencyGraphSchema
|
|
2313
2579
|
});
|
|
2314
|
-
async function saveSnapshot(projectRoot, graph, multiLayer) {
|
|
2315
|
-
const dirPath =
|
|
2316
|
-
const filePath =
|
|
2580
|
+
async function saveSnapshot(projectRoot, graph, multiLayer, analysisOptions) {
|
|
2581
|
+
const dirPath = join7(projectRoot, ARCHTRACKER_DIR3);
|
|
2582
|
+
const filePath = join7(dirPath, SNAPSHOT_FILE);
|
|
2317
2583
|
const snapshot = {
|
|
2318
2584
|
version: SCHEMA_VERSION,
|
|
2319
2585
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2320
2586
|
rootDir: graph.rootDir,
|
|
2321
2587
|
graph,
|
|
2322
|
-
...multiLayer ? { multiLayer } : {}
|
|
2588
|
+
...multiLayer ? { multiLayer } : {},
|
|
2589
|
+
...analysisOptions ? { analysisOptions } : {}
|
|
2323
2590
|
};
|
|
2324
|
-
await
|
|
2325
|
-
|
|
2591
|
+
await mkdir3(dirPath, { recursive: true });
|
|
2592
|
+
const json = JSON.stringify(snapshot, null, 2);
|
|
2593
|
+
await writeFile3(filePath, json, "utf-8");
|
|
2594
|
+
try {
|
|
2595
|
+
const historyPath = join7(dirPath, HISTORY_DIR);
|
|
2596
|
+
await mkdir3(historyPath, { recursive: true });
|
|
2597
|
+
const safeTs = snapshot.timestamp.replace(/[:.]/g, "-");
|
|
2598
|
+
await writeFile3(join7(historyPath, `${safeTs}.json`), json, "utf-8");
|
|
2599
|
+
} catch {
|
|
2600
|
+
}
|
|
2326
2601
|
return snapshot;
|
|
2327
2602
|
}
|
|
2328
2603
|
async function loadSnapshot(projectRoot) {
|
|
2329
|
-
const filePath =
|
|
2604
|
+
const filePath = join7(projectRoot, ARCHTRACKER_DIR3, SNAPSHOT_FILE);
|
|
2330
2605
|
let raw;
|
|
2331
2606
|
try {
|
|
2332
|
-
raw = await
|
|
2607
|
+
raw = await readFile4(filePath, "utf-8");
|
|
2333
2608
|
} catch (error) {
|
|
2334
2609
|
if (isNodeError2(error) && error.code === "ENOENT") {
|
|
2335
2610
|
return null;
|
|
@@ -2356,6 +2631,67 @@ async function loadSnapshot(projectRoot) {
|
|
|
2356
2631
|
}
|
|
2357
2632
|
return result.data;
|
|
2358
2633
|
}
|
|
2634
|
+
async function listSnapshots(projectRoot) {
|
|
2635
|
+
const historyPath = join7(projectRoot, ARCHTRACKER_DIR3, HISTORY_DIR);
|
|
2636
|
+
let entries;
|
|
2637
|
+
try {
|
|
2638
|
+
entries = await readdir4(historyPath);
|
|
2639
|
+
} catch {
|
|
2640
|
+
return [];
|
|
2641
|
+
}
|
|
2642
|
+
const jsonFiles = entries.filter((e) => e.endsWith(".json")).sort().reverse();
|
|
2643
|
+
const summaries = [];
|
|
2644
|
+
for (const file of jsonFiles) {
|
|
2645
|
+
try {
|
|
2646
|
+
const raw = await readFile4(join7(historyPath, file), "utf-8");
|
|
2647
|
+
const parsed = JSON.parse(raw);
|
|
2648
|
+
const result = SnapshotSchema.safeParse(parsed);
|
|
2649
|
+
if (result.success) {
|
|
2650
|
+
const snap = result.data;
|
|
2651
|
+
summaries.push({
|
|
2652
|
+
timestamp: snap.timestamp,
|
|
2653
|
+
totalFiles: snap.graph.totalFiles,
|
|
2654
|
+
totalEdges: snap.graph.totalEdges,
|
|
2655
|
+
circularDeps: snap.graph.circularDependencies.length,
|
|
2656
|
+
hasMultiLayer: "multiLayer" in parsed && parsed.multiLayer != null
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
} catch {
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
return summaries;
|
|
2663
|
+
}
|
|
2664
|
+
async function loadSnapshotByTimestamp(projectRoot, timestamp) {
|
|
2665
|
+
const historyPath = join7(projectRoot, ARCHTRACKER_DIR3, HISTORY_DIR);
|
|
2666
|
+
let entries;
|
|
2667
|
+
try {
|
|
2668
|
+
entries = await readdir4(historyPath);
|
|
2669
|
+
} catch {
|
|
2670
|
+
return null;
|
|
2671
|
+
}
|
|
2672
|
+
const jsonFiles = entries.filter((e) => e.endsWith(".json")).sort();
|
|
2673
|
+
const safeTs = timestamp.replace(/[:.]/g, "-");
|
|
2674
|
+
const exactMatch = jsonFiles.find((f) => f === `${safeTs}.json`);
|
|
2675
|
+
if (exactMatch) {
|
|
2676
|
+
return loadHistoryFile(join7(historyPath, exactMatch));
|
|
2677
|
+
}
|
|
2678
|
+
const prefixMatch = jsonFiles.find((f) => f.startsWith(safeTs.slice(0, 10)));
|
|
2679
|
+
if (prefixMatch) {
|
|
2680
|
+
return loadHistoryFile(join7(historyPath, prefixMatch));
|
|
2681
|
+
}
|
|
2682
|
+
return null;
|
|
2683
|
+
}
|
|
2684
|
+
async function loadHistoryFile(filePath) {
|
|
2685
|
+
try {
|
|
2686
|
+
const raw = await readFile4(filePath, "utf-8");
|
|
2687
|
+
const parsed = JSON.parse(raw);
|
|
2688
|
+
const result = SnapshotSchema.safeParse(parsed);
|
|
2689
|
+
if (result.success) return result.data;
|
|
2690
|
+
return null;
|
|
2691
|
+
} catch {
|
|
2692
|
+
return null;
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2359
2695
|
var StorageError = class extends Error {
|
|
2360
2696
|
constructor(message, options) {
|
|
2361
2697
|
super(message, options);
|
|
@@ -2417,6 +2753,17 @@ function computeDiff(oldGraph, newGraph) {
|
|
|
2417
2753
|
}
|
|
2418
2754
|
return { added, removed, modified, affectedDependents };
|
|
2419
2755
|
}
|
|
2756
|
+
function isTestOrFixture(path) {
|
|
2757
|
+
return /(__fixtures__|__tests__|__mocks__|\.test\.|\.spec\.|\.e2e\.)/.test(path);
|
|
2758
|
+
}
|
|
2759
|
+
function partition(arr, pred) {
|
|
2760
|
+
const yes = [];
|
|
2761
|
+
const no = [];
|
|
2762
|
+
for (const item of arr) {
|
|
2763
|
+
(pred(item) ? yes : no).push(item);
|
|
2764
|
+
}
|
|
2765
|
+
return [yes, no];
|
|
2766
|
+
}
|
|
2420
2767
|
function formatDiffReport(diff) {
|
|
2421
2768
|
const lines = [];
|
|
2422
2769
|
lines.push(t("diff.title"));
|
|
@@ -2425,32 +2772,51 @@ function formatDiffReport(diff) {
|
|
|
2425
2772
|
return lines.join("\n");
|
|
2426
2773
|
}
|
|
2427
2774
|
if (diff.added.length > 0) {
|
|
2775
|
+
const [testFiles, srcFiles] = partition(diff.added, isTestOrFixture);
|
|
2428
2776
|
lines.push(t("diff.added", { count: diff.added.length }));
|
|
2429
|
-
for (const f of
|
|
2777
|
+
for (const f of srcFiles) {
|
|
2430
2778
|
lines.push(` + ${f}`);
|
|
2431
2779
|
}
|
|
2780
|
+
if (testFiles.length > 0) {
|
|
2781
|
+
lines.push(t("diff.testSummary", { count: testFiles.length }));
|
|
2782
|
+
}
|
|
2432
2783
|
lines.push("");
|
|
2433
2784
|
}
|
|
2434
2785
|
if (diff.removed.length > 0) {
|
|
2786
|
+
const [testFiles, srcFiles] = partition(diff.removed, isTestOrFixture);
|
|
2435
2787
|
lines.push(t("diff.removed", { count: diff.removed.length }));
|
|
2436
|
-
for (const f of
|
|
2788
|
+
for (const f of srcFiles) {
|
|
2437
2789
|
lines.push(` - ${f}`);
|
|
2438
2790
|
}
|
|
2791
|
+
if (testFiles.length > 0) {
|
|
2792
|
+
lines.push(t("diff.testSummary", { count: testFiles.length }));
|
|
2793
|
+
}
|
|
2439
2794
|
lines.push("");
|
|
2440
2795
|
}
|
|
2441
2796
|
if (diff.modified.length > 0) {
|
|
2797
|
+
const [testFiles, srcFiles] = partition(diff.modified, isTestOrFixture);
|
|
2442
2798
|
lines.push(t("diff.modified", { count: diff.modified.length }));
|
|
2443
|
-
for (const f of
|
|
2799
|
+
for (const f of srcFiles) {
|
|
2444
2800
|
lines.push(` ~ ${f}`);
|
|
2445
2801
|
}
|
|
2802
|
+
if (testFiles.length > 0) {
|
|
2803
|
+
lines.push(t("diff.testSummary", { count: testFiles.length }));
|
|
2804
|
+
}
|
|
2446
2805
|
lines.push("");
|
|
2447
2806
|
}
|
|
2448
2807
|
if (diff.affectedDependents.length > 0) {
|
|
2808
|
+
const [testEntries, srcEntries] = partition(
|
|
2809
|
+
diff.affectedDependents,
|
|
2810
|
+
(a) => isTestOrFixture(a.file) || isTestOrFixture(a.dependsOn)
|
|
2811
|
+
);
|
|
2449
2812
|
lines.push(t("diff.affected", { count: diff.affectedDependents.length }));
|
|
2450
|
-
for (const a of
|
|
2813
|
+
for (const a of srcEntries) {
|
|
2451
2814
|
lines.push(` ! ${a.file}`);
|
|
2452
2815
|
lines.push(` ${a.reason}`);
|
|
2453
2816
|
}
|
|
2817
|
+
if (testEntries.length > 0) {
|
|
2818
|
+
lines.push(t("diff.testAffectedSummary", { count: testEntries.length }));
|
|
2819
|
+
}
|
|
2454
2820
|
lines.push("");
|
|
2455
2821
|
}
|
|
2456
2822
|
return lines.join("\n");
|
|
@@ -2466,6 +2832,11 @@ function arraysEqual(a, b) {
|
|
|
2466
2832
|
// src/web/server.ts
|
|
2467
2833
|
import { createServer } from "http";
|
|
2468
2834
|
|
|
2835
|
+
// src/web/template.ts
|
|
2836
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2837
|
+
import { join as join8, dirname as dirname2 } from "path";
|
|
2838
|
+
import { fileURLToPath } from "url";
|
|
2839
|
+
|
|
2469
2840
|
// src/web/styles.ts
|
|
2470
2841
|
function buildStyles() {
|
|
2471
2842
|
return `<style>
|
|
@@ -2798,676 +3169,33 @@ function buildViewerHtml() {
|
|
|
2798
3169
|
</div>`;
|
|
2799
3170
|
}
|
|
2800
3171
|
|
|
2801
|
-
// src/
|
|
2802
|
-
function
|
|
2803
|
-
return
|
|
2804
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2805
|
-
// HIERARCHY VIEW
|
|
2806
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2807
|
-
function buildHierarchy(){
|
|
2808
|
-
const hSvg=d3.select('#hier-svg');
|
|
2809
|
-
const hG=hSvg.append('g');
|
|
2810
|
-
const hZoom=d3.zoom().scaleExtent([0.1,4]).on('zoom',e=>hG.attr('transform',e.transform));
|
|
2811
|
-
hSvg.call(hZoom);
|
|
2812
|
-
|
|
2813
|
-
const nodeMap={}; DATA.nodes.forEach(n=>nodeMap[n.id]=n);
|
|
2814
|
-
const importsMap={}; DATA.links.forEach(l=>{const s=l.source.id??l.source,t=l.target.id??l.target;if(!importsMap[s])importsMap[s]=[];importsMap[s].push(t);});
|
|
2815
|
-
|
|
2816
|
-
const entryPoints=DATA.nodes.filter(n=>n.dependents===0).map(n=>n.id);
|
|
2817
|
-
const layers={};const visited=new Set();
|
|
2818
|
-
const queue=entryPoints.map(id=>({id,layer:0}));
|
|
2819
|
-
DATA.nodes.forEach(n=>{if(n.isOrphan)layers[n.id]=0;});
|
|
2820
|
-
|
|
2821
|
-
while(queue.length>0){
|
|
2822
|
-
const{id,layer}=queue.shift();
|
|
2823
|
-
if(visited.has(id)&&(layers[id]??-1)>=layer)continue;
|
|
2824
|
-
layers[id]=Math.max(layers[id]??0,layer);visited.add(id);
|
|
2825
|
-
(importsMap[id]||[]).forEach(t=>queue.push({id:t,layer:layer+1}));
|
|
2826
|
-
}
|
|
2827
|
-
DATA.nodes.forEach(n=>{if(!(n.id in layers))layers[n.id]=0;});
|
|
2828
|
-
|
|
2829
|
-
const maxLayer=Math.max(0,...Object.values(layers));
|
|
2830
|
-
const layerGroups={};
|
|
2831
|
-
for(let i=0;i<=maxLayer;i++)layerGroups[i]=[];
|
|
2832
|
-
Object.entries(layers).forEach(([id,l])=>layerGroups[l].push(id));
|
|
2833
|
-
Object.values(layerGroups).forEach(arr=>arr.sort((a,b)=>(nodeMap[a]?.dir||'').localeCompare(nodeMap[b]?.dir||'')||a.localeCompare(b)));
|
|
2834
|
-
|
|
2835
|
-
const boxW=200,boxH=30,gapX=24,gapY=70,padY=60,padX=40;
|
|
2836
|
-
const positions={};let maxRowWidth=0;
|
|
2837
|
-
for(let layer=0;layer<=maxLayer;layer++){const items=layerGroups[layer];maxRowWidth=Math.max(maxRowWidth,items.length*(boxW+gapX)-gapX);}
|
|
2838
|
-
for(let layer=0;layer<=maxLayer;layer++){
|
|
2839
|
-
const items=layerGroups[layer],rowWidth=items.length*(boxW+gapX)-gapX,startX=padX+(maxRowWidth-rowWidth)/2;
|
|
2840
|
-
items.forEach((id,i)=>{positions[id]={x:startX+i*(boxW+gapX),y:padY+layer*(boxH+gapY)};});
|
|
2841
|
-
}
|
|
2842
|
-
|
|
2843
|
-
const totalW=maxRowWidth+padX*2,totalH=padY*2+(maxLayer+1)*(boxH+gapY);
|
|
2844
|
-
hSvg.attr('width',Math.max(totalW,W)).attr('height',Math.max(totalH,H));
|
|
2845
|
-
|
|
2846
|
-
const linkG=hG.append('g');
|
|
2847
|
-
DATA.links.forEach(l=>{
|
|
2848
|
-
const sId=l.source.id??l.source,tId=l.target.id??l.target;
|
|
2849
|
-
const s=positions[sId],t=positions[tId]; if(!s||!t)return;
|
|
2850
|
-
const x1=s.x+boxW/2,y1=s.y+boxH,x2=t.x+boxW/2,y2=t.y,midY=(y1+y2)/2;
|
|
2851
|
-
linkG.append('path').attr('class','hier-link')
|
|
2852
|
-
.attr('d',\`M\${x1},\${y1} C\${x1},\${midY} \${x2},\${midY} \${x2},\${y2}\`)
|
|
2853
|
-
.attr('stroke',l.type==='type-only'?'#1f3d5c':'var(--border)')
|
|
2854
|
-
.attr('stroke-dasharray',l.type==='type-only'?'4,3':null)
|
|
2855
|
-
.attr('data-source',sId).attr('data-target',tId);
|
|
2856
|
-
});
|
|
2857
|
-
|
|
2858
|
-
hSvg.append('defs').append('marker').attr('id','harrow').attr('viewBox','0 -3 6 6')
|
|
2859
|
-
.attr('refX',6).attr('refY',0).attr('markerWidth',6).attr('markerHeight',6).attr('orient','auto')
|
|
2860
|
-
.append('path').attr('d','M0,-3L6,0L0,3Z').attr('fill','var(--border)');
|
|
2861
|
-
linkG.selectAll('path').attr('marker-end','url(#harrow)');
|
|
2862
|
-
|
|
2863
|
-
for(let layer=0;layer<=maxLayer;layer++){
|
|
2864
|
-
if(!layerGroups[layer].length)continue;
|
|
2865
|
-
hG.append('text').attr('class','hier-layer-label').attr('font-size',11)
|
|
2866
|
-
.attr('data-depth-idx',layer)
|
|
2867
|
-
.attr('x',12).attr('y',padY+layer*(boxH+gapY)+boxH/2+4).text('L'+layer);
|
|
2868
|
-
}
|
|
2869
|
-
|
|
2870
|
-
const nodeG=hG.append('g');
|
|
2871
|
-
DATA.nodes.forEach(n=>{
|
|
2872
|
-
const pos=positions[n.id]; if(!pos)return;
|
|
2873
|
-
const gn=nodeG.append('g').attr('class','hier-node').attr('transform',\`translate(\${pos.x},\${pos.y})\`);
|
|
2874
|
-
gn.append('rect').attr('width',boxW).attr('height',boxH)
|
|
2875
|
-
.attr('fill','var(--bg-card)').attr('stroke',nodeColor(n))
|
|
2876
|
-
.attr('stroke-width',circularSet.has(n.id)?2:1.5);
|
|
2877
|
-
gn.append('text').attr('x',8).attr('y',boxH/2+4).attr('font-size',11)
|
|
2878
|
-
.text(fileName(n.id).length>24?fileName(n.id).slice(0,22)+'\\u2026':fileName(n.id));
|
|
2879
|
-
gn.append('text').attr('x',boxW-8).attr('y',boxH/2+4)
|
|
2880
|
-
.attr('text-anchor','end').attr('font-size',10).attr('fill','var(--text-muted)')
|
|
2881
|
-
.text(n.dependents>0?'\\u2191'+n.dependents:'');
|
|
2882
|
-
gn.append('text').attr('x',8).attr('y',-4).attr('font-size',9)
|
|
2883
|
-
.attr('fill',dirColor(n.dir)).attr('opacity',0.7).text(n.dir);
|
|
2884
|
-
|
|
2885
|
-
gn.node().__data_id=n.id;
|
|
2886
|
-
gn.on('mouseover',e=>{
|
|
2887
|
-
showTooltip(e,n);
|
|
2888
|
-
if (!hierPinned) hierHighlight(n.id);
|
|
2889
|
-
})
|
|
2890
|
-
.on('mousemove',e=>positionTooltip(e))
|
|
2891
|
-
.on('mouseout',()=>{
|
|
2892
|
-
scheduleHideTooltip();
|
|
2893
|
-
if (!hierPinned) hierResetHighlight();
|
|
2894
|
-
})
|
|
2895
|
-
.on('click',(e)=>{
|
|
2896
|
-
e.stopPropagation();
|
|
2897
|
-
hierPinned=n.id;
|
|
2898
|
-
hierHighlight(n.id);
|
|
2899
|
-
showHierDetail(n);
|
|
2900
|
-
});
|
|
2901
|
-
});
|
|
2902
|
-
|
|
2903
|
-
// Hierarchy highlight helpers
|
|
2904
|
-
let hierPinned=null;
|
|
2905
|
-
function hierHighlight(nId){
|
|
2906
|
-
linkG.selectAll('path')
|
|
2907
|
-
.attr('stroke',function(){const s=this.getAttribute('data-source'),t=this.getAttribute('data-target');if(s===nId)return'#58a6ff';if(t===nId)return'#3fb950';return this.getAttribute('stroke-dasharray')?'#1f3d5c':'var(--border)';})
|
|
2908
|
-
.attr('stroke-width',function(){const s=this.getAttribute('data-source'),t=this.getAttribute('data-target');return(s===nId||t===nId)?2.5:1;})
|
|
2909
|
-
.attr('opacity',function(){const s=this.getAttribute('data-source'),t=this.getAttribute('data-target');return(s===nId||t===nId)?1:0.15;});
|
|
2910
|
-
nodeG.selectAll('.hier-node').attr('opacity',function(){
|
|
2911
|
-
const id=this.__data_id; if(id===nId)return 1;
|
|
2912
|
-
const connected=DATA.links.some(l=>{const s=l.source.id??l.source,t=l.target.id??l.target;return(s===nId&&t===id)||(t===nId&&s===id);});
|
|
2913
|
-
return connected?1:0.3;
|
|
2914
|
-
});
|
|
2915
|
-
}
|
|
2916
|
-
function hierResetHighlight(){
|
|
2917
|
-
hierPinned=null;
|
|
2918
|
-
linkG.selectAll('path')
|
|
2919
|
-
.attr('stroke',function(){return this.getAttribute('stroke-dasharray')?'#1f3d5c':'var(--border)';})
|
|
2920
|
-
.attr('stroke-width',1).attr('opacity',1);
|
|
2921
|
-
nodeG.selectAll('.hier-node').attr('opacity',1);
|
|
2922
|
-
}
|
|
2923
|
-
function showHierDetail(n){
|
|
2924
|
-
const p=document.getElementById('hier-detail');
|
|
2925
|
-
document.getElementById('hd-name').textContent=n.id;
|
|
2926
|
-
document.getElementById('hd-meta').innerHTML=i('detail.dir')+': '+esc(n.dir)+'<br>'+i('detail.dependencies')+': '+n.deps+' \\u00b7 '+i('detail.dependents')+': '+n.dependents;
|
|
2927
|
-
document.getElementById('hd-dependents').innerHTML=(n.dependentsList||[]).map(x=>'<li>\\u2190 '+esc(x)+'</li>').join('')||'<li style="color:var(--text-muted)">'+i('detail.none')+'</li>';
|
|
2928
|
-
document.getElementById('hd-deps').innerHTML=(n.dependencies||[]).map(x=>'<li>\\u2192 '+esc(x)+'</li>').join('')||'<li style="color:var(--text-muted)">'+i('detail.none')+'</li>';
|
|
2929
|
-
p.classList.add('open');
|
|
2930
|
-
}
|
|
2931
|
-
window.closeHierDetail=()=>{document.getElementById('hier-detail').classList.remove('open');hierResetHighlight();tooltip.style.display='none';tooltipLocked=false;};
|
|
2932
|
-
|
|
2933
|
-
// Click on empty space to deselect
|
|
2934
|
-
hSvg.on('click',()=>{closeHierDetail();});
|
|
2935
|
-
|
|
2936
|
-
// Hierarchy filters \u2014 layer pills or dir pills
|
|
2937
|
-
const hFilterRow=document.getElementById('hier-filter-row');
|
|
2938
|
-
const hFilterBar=document.getElementById('hier-filter-bar');
|
|
2939
|
-
if (hFilterBar) hFilterBar.style.display='';
|
|
2940
|
-
const hActiveLayers=new Set(); // empty = show all (same as graph view)
|
|
2941
|
-
|
|
2942
|
-
function hierRelayoutInner() {
|
|
2943
|
-
function isVisible(nId) {
|
|
2944
|
-
var nd = nodeMap[nId];
|
|
2945
|
-
if (!nd) return false;
|
|
2946
|
-
if (LAYERS && nd.layer && hActiveLayers.size > 0 && !hActiveLayers.has(nd.layer)) return false;
|
|
2947
|
-
return true;
|
|
2948
|
-
}
|
|
2949
|
-
|
|
2950
|
-
// Build visible layer groups and compact Y positions
|
|
2951
|
-
var visibleDepths = [];
|
|
2952
|
-
var visLayerGroups = {};
|
|
2953
|
-
for (var depth = 0; depth <= maxLayer; depth++) {
|
|
2954
|
-
var visItems = layerGroups[depth].filter(function(id) { return isVisible(id); });
|
|
2955
|
-
if (visItems.length > 0) {
|
|
2956
|
-
visLayerGroups[depth] = visItems;
|
|
2957
|
-
visibleDepths.push(depth);
|
|
2958
|
-
}
|
|
2959
|
-
}
|
|
2960
|
-
|
|
2961
|
-
// Recalculate positions for visible nodes (compacted)
|
|
2962
|
-
var newPositions = {};
|
|
2963
|
-
var newMaxRowWidth = 0;
|
|
2964
|
-
visibleDepths.forEach(function(depth) {
|
|
2965
|
-
newMaxRowWidth = Math.max(newMaxRowWidth, visLayerGroups[depth].length * (boxW + gapX) - gapX);
|
|
2966
|
-
});
|
|
2967
|
-
visibleDepths.forEach(function(depth, yIdx) {
|
|
2968
|
-
var items = visLayerGroups[depth];
|
|
2969
|
-
var rowWidth = items.length * (boxW + gapX) - gapX;
|
|
2970
|
-
var startX = padX + (newMaxRowWidth - rowWidth) / 2;
|
|
2971
|
-
items.forEach(function(id, idx) {
|
|
2972
|
-
newPositions[id] = { x: startX + idx * (boxW + gapX), y: padY + yIdx * (boxH + gapY) };
|
|
2973
|
-
});
|
|
2974
|
-
});
|
|
2975
|
-
|
|
2976
|
-
// Update SVG size
|
|
2977
|
-
var newTotalW = (newMaxRowWidth || 0) + padX * 2;
|
|
2978
|
-
var newTotalH = padY * 2 + Math.max(1, visibleDepths.length) * (boxH + gapY);
|
|
2979
|
-
hSvg.attr('width', Math.max(newTotalW, W)).attr('height', Math.max(newTotalH, H));
|
|
2980
|
-
|
|
2981
|
-
// Update nodes: hide/show + transition positions
|
|
2982
|
-
nodeG.selectAll('.hier-node').each(function() {
|
|
2983
|
-
var nId = this.__data_id;
|
|
2984
|
-
var el = d3.select(this);
|
|
2985
|
-
if (!isVisible(nId) || !newPositions[nId]) {
|
|
2986
|
-
el.attr('display', 'none');
|
|
2987
|
-
} else {
|
|
2988
|
-
el.attr('display', null)
|
|
2989
|
-
.transition().duration(300)
|
|
2990
|
-
.attr('transform', 'translate(' + newPositions[nId].x + ',' + newPositions[nId].y + ')');
|
|
2991
|
-
}
|
|
2992
|
-
});
|
|
2993
|
-
|
|
2994
|
-
// Update links: show only if both endpoints visible, recalculate bezier
|
|
2995
|
-
linkG.selectAll('path').each(function() {
|
|
2996
|
-
var sId = this.getAttribute('data-source');
|
|
2997
|
-
var tId = this.getAttribute('data-target');
|
|
2998
|
-
var el = d3.select(this);
|
|
2999
|
-
if (!isVisible(sId) || !isVisible(tId) || !newPositions[sId] || !newPositions[tId]) {
|
|
3000
|
-
el.attr('display', 'none');
|
|
3001
|
-
} else {
|
|
3002
|
-
var s = newPositions[sId], t = newPositions[tId];
|
|
3003
|
-
var x1 = s.x + boxW / 2, y1 = s.y + boxH;
|
|
3004
|
-
var x2 = t.x + boxW / 2, y2 = t.y;
|
|
3005
|
-
var midY = (y1 + y2) / 2;
|
|
3006
|
-
el.attr('display', null)
|
|
3007
|
-
.transition().duration(300)
|
|
3008
|
-
.attr('d', 'M' + x1 + ',' + y1 + ' C' + x1 + ',' + midY + ' ' + x2 + ',' + midY + ' ' + x2 + ',' + y2);
|
|
3009
|
-
}
|
|
3010
|
-
});
|
|
3011
|
-
|
|
3012
|
-
// Update depth labels: hide empty depths, reposition visible ones
|
|
3013
|
-
hG.selectAll('.hier-layer-label').each(function() {
|
|
3014
|
-
var depthIdx = +this.getAttribute('data-depth-idx');
|
|
3015
|
-
var el = d3.select(this);
|
|
3016
|
-
var yIdx = visibleDepths.indexOf(depthIdx);
|
|
3017
|
-
if (yIdx === -1) {
|
|
3018
|
-
el.attr('display', 'none');
|
|
3019
|
-
} else {
|
|
3020
|
-
el.attr('display', null)
|
|
3021
|
-
.transition().duration(300)
|
|
3022
|
-
.attr('y', padY + yIdx * (boxH + gapY) + boxH / 2 + 4);
|
|
3023
|
-
}
|
|
3024
|
-
});
|
|
3025
|
-
|
|
3026
|
-
// Close detail panel if pinned node became hidden
|
|
3027
|
-
if (hierPinned && !isVisible(hierPinned)) {
|
|
3028
|
-
closeHierDetail();
|
|
3029
|
-
}
|
|
3030
|
-
}
|
|
3031
|
-
|
|
3032
|
-
function hierSyncFromTabInner() {
|
|
3033
|
-
if (!LAYERS) return;
|
|
3034
|
-
hActiveLayers.clear();
|
|
3035
|
-
activeLayers.forEach(function(name) { hActiveLayers.add(name); });
|
|
3036
|
-
// Sync pill UI
|
|
3037
|
-
hFilterRow.querySelectorAll('.layer-pill').forEach(function(p) {
|
|
3038
|
-
var ln = p.dataset.layer;
|
|
3039
|
-
if (ln === 'all') {
|
|
3040
|
-
p.classList.toggle('active', hActiveLayers.size === 0);
|
|
3041
|
-
} else {
|
|
3042
|
-
p.classList.toggle('active', hActiveLayers.has(ln));
|
|
3043
|
-
}
|
|
3044
|
-
});
|
|
3045
|
-
}
|
|
3046
|
-
|
|
3047
|
-
if (LAYERS) {
|
|
3048
|
-
// "All" button
|
|
3049
|
-
const allPill=document.createElement('div');
|
|
3050
|
-
allPill.className='layer-pill active';
|
|
3051
|
-
allPill.style.fontWeight='400';
|
|
3052
|
-
allPill.textContent='All';
|
|
3053
|
-
allPill.dataset.layer='all';
|
|
3054
|
-
allPill.onclick=()=>{
|
|
3055
|
-
hActiveLayers.clear();
|
|
3056
|
-
hFilterRow.querySelectorAll('.layer-pill').forEach(p=>p.classList.remove('active'));
|
|
3057
|
-
allPill.classList.add('active');
|
|
3058
|
-
hierRelayoutInner();
|
|
3059
|
-
};
|
|
3060
|
-
hFilterRow.appendChild(allPill);
|
|
3061
|
-
|
|
3062
|
-
LAYERS.forEach(layer => {
|
|
3063
|
-
const pill=document.createElement('div');
|
|
3064
|
-
pill.className='layer-pill';
|
|
3065
|
-
pill.dataset.layer=layer.name;
|
|
3066
|
-
const count=DATA.nodes.filter(n=>n.layer===layer.name).length;
|
|
3067
|
-
pill.innerHTML='<div class="lp-dot" style="background:'+esc(layer.color)+'"></div>'+esc(layer.name)+' <span class="lp-count">'+count+'</span>';
|
|
3068
|
-
pill.onclick=(e)=>{
|
|
3069
|
-
if (e.shiftKey) {
|
|
3070
|
-
hActiveLayers.clear();
|
|
3071
|
-
hActiveLayers.add(layer.name);
|
|
3072
|
-
} else {
|
|
3073
|
-
if (hActiveLayers.has(layer.name)) hActiveLayers.delete(layer.name);
|
|
3074
|
-
else hActiveLayers.add(layer.name);
|
|
3075
|
-
}
|
|
3076
|
-
// Sync pill UI
|
|
3077
|
-
hFilterRow.querySelectorAll('.layer-pill').forEach(function(p) {
|
|
3078
|
-
var ln = p.dataset.layer;
|
|
3079
|
-
if (ln === 'all') p.classList.toggle('active', hActiveLayers.size === 0);
|
|
3080
|
-
else p.classList.toggle('active', hActiveLayers.has(ln));
|
|
3081
|
-
});
|
|
3082
|
-
hierRelayoutInner();
|
|
3083
|
-
};
|
|
3084
|
-
hFilterRow.appendChild(pill);
|
|
3085
|
-
});
|
|
3086
|
-
} else {
|
|
3087
|
-
const hActiveDirs=new Set(DATA.dirs);
|
|
3088
|
-
DATA.dirs.forEach(dir=>{
|
|
3089
|
-
const pill=document.createElement('div');
|
|
3090
|
-
pill.className='filter-pill active';
|
|
3091
|
-
pill.innerHTML='<div class="pill-dot" style="background:'+dirColor(dir)+'"></div>'+esc(dir||'.')+' <span class="pill-count">'+(dirCounts[dir]||0)+'</span>';
|
|
3092
|
-
pill.onclick=()=>{
|
|
3093
|
-
if(hActiveDirs.has(dir)){hActiveDirs.delete(dir);pill.classList.remove('active');}
|
|
3094
|
-
else{hActiveDirs.add(dir);pill.classList.add('active');}
|
|
3095
|
-
nodeG.selectAll('.hier-node').attr('opacity',function(){const nId=this.__data_id;return hActiveDirs.has(nodeMap[nId]?.dir)?1:0.1;});
|
|
3096
|
-
};
|
|
3097
|
-
hFilterRow.appendChild(pill);
|
|
3098
|
-
});
|
|
3099
|
-
}
|
|
3100
|
-
|
|
3101
|
-
// Assign function pointers for cross-view sync
|
|
3102
|
-
hierRelayout = hierRelayoutInner;
|
|
3103
|
-
hierSyncFromTab = hierSyncFromTabInner;
|
|
3104
|
-
|
|
3105
|
-
hSvg.call(hZoom.transform,d3.zoomIdentity.translate(
|
|
3106
|
-
Math.max(0,(W-totalW)/2),20
|
|
3107
|
-
).scale(Math.min(1,W/(totalW+40),H/(totalH+40))));
|
|
3108
|
-
|
|
3109
|
-
// If layers were already filtered in graph view, sync hierarchy on first build
|
|
3110
|
-
if (activeLayers.size > 0) {
|
|
3111
|
-
hierSyncFromTabInner();
|
|
3112
|
-
hierRelayoutInner();
|
|
3113
|
-
}
|
|
3114
|
-
}
|
|
3115
|
-
`;
|
|
3172
|
+
// src/utils/html-escape.ts
|
|
3173
|
+
function escapeHtml(s) {
|
|
3174
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3116
3175
|
}
|
|
3117
3176
|
|
|
3118
|
-
// src/web/
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
document.getElementById('diff-modified-count').textContent = modifiedSet.size;
|
|
3136
|
-
document.getElementById('diff-affected-count').textContent = affectedSet.size;
|
|
3137
|
-
|
|
3138
|
-
function isDiffNode(id) {
|
|
3139
|
-
return addedSet.has(id) || removedSet.has(id) || modifiedSet.has(id) || affectedSet.has(id);
|
|
3140
|
-
}
|
|
3141
|
-
|
|
3142
|
-
function diffStatus(id) {
|
|
3143
|
-
if (addedSet.has(id)) return 'Added';
|
|
3144
|
-
if (removedSet.has(id)) return 'Removed';
|
|
3145
|
-
if (modifiedSet.has(id)) return 'Modified';
|
|
3146
|
-
if (affectedSet.has(id)) return 'Affected';
|
|
3147
|
-
return 'Unchanged';
|
|
3148
|
-
}
|
|
3149
|
-
|
|
3150
|
-
function diffStatusColor(id) {
|
|
3151
|
-
if (addedSet.has(id)) return 'var(--green)';
|
|
3152
|
-
if (removedSet.has(id)) return 'var(--red)';
|
|
3153
|
-
if (modifiedSet.has(id)) return 'var(--yellow)';
|
|
3154
|
-
if (affectedSet.has(id)) return 'var(--accent)';
|
|
3155
|
-
return 'var(--text-muted)';
|
|
3156
|
-
}
|
|
3157
|
-
|
|
3158
|
-
// Build reverse dependency map for impact chain
|
|
3159
|
-
var diffRevMap = {};
|
|
3160
|
-
DATA.links.forEach(function(l) {
|
|
3161
|
-
var s = l.source.id ?? l.source, t = l.target.id ?? l.target;
|
|
3162
|
-
if (!diffRevMap[t]) diffRevMap[t] = [];
|
|
3163
|
-
diffRevMap[t].push(s);
|
|
3164
|
-
});
|
|
3165
|
-
|
|
3166
|
-
function getImpactChain(startId) {
|
|
3167
|
-
var result = new Set();
|
|
3168
|
-
var queue = [startId];
|
|
3169
|
-
while (queue.length) {
|
|
3170
|
-
var id = queue.shift();
|
|
3171
|
-
if (result.has(id)) continue;
|
|
3172
|
-
result.add(id);
|
|
3173
|
-
(diffRevMap[id] || []).forEach(function(x) { queue.push(x); });
|
|
3174
|
-
}
|
|
3175
|
-
return result;
|
|
3176
|
-
}
|
|
3177
|
-
|
|
3178
|
-
let diffFocusMode = false;
|
|
3179
|
-
var dNode, dLink, dSim, simNodes, simLinks;
|
|
3180
|
-
|
|
3181
|
-
window.toggleDiffFocus = function() {
|
|
3182
|
-
diffFocusMode = !diffFocusMode;
|
|
3183
|
-
var btn = document.getElementById('diff-focus-btn');
|
|
3184
|
-
btn.classList.toggle('active', diffFocusMode);
|
|
3185
|
-
btn.textContent = diffFocusMode ? i('diff.showAll') : i('diff.focusChanges');
|
|
3186
|
-
if (!diffBuilt) return;
|
|
3187
|
-
applyDiffFilter();
|
|
3188
|
-
};
|
|
3189
|
-
|
|
3190
|
-
window.closeDiffDetail = function() {
|
|
3191
|
-
document.getElementById('diff-detail').style.display = 'none';
|
|
3192
|
-
if (diffBuilt) resetDiffHighlight();
|
|
3193
|
-
};
|
|
3194
|
-
|
|
3195
|
-
function applyDiffFilter() {
|
|
3196
|
-
dNode.attr('display', function(d) {
|
|
3197
|
-
if (!diffFocusMode) return null;
|
|
3198
|
-
return isDiffNode(d.id) ? null : 'none';
|
|
3199
|
-
});
|
|
3200
|
-
dNode.select('circle')
|
|
3201
|
-
.attr('opacity', function(d) {
|
|
3202
|
-
if (diffFocusMode) return isDiffNode(d.id) ? 1 : 0;
|
|
3203
|
-
return isDiffNode(d.id) ? 1 : 0.12;
|
|
3204
|
-
});
|
|
3205
|
-
dNode.select('text')
|
|
3206
|
-
.attr('opacity', function(d) {
|
|
3207
|
-
if (diffFocusMode) return isDiffNode(d.id) ? 1 : 0;
|
|
3208
|
-
return isDiffNode(d.id) ? 1 : 0.08;
|
|
3209
|
-
});
|
|
3210
|
-
dLink.attr('display', function(l) {
|
|
3211
|
-
if (!diffFocusMode) return null;
|
|
3212
|
-
var s = l.source.id ?? l.source, t = l.target.id ?? l.target;
|
|
3213
|
-
return (isDiffNode(s) && isDiffNode(t)) ? null : 'none';
|
|
3214
|
-
});
|
|
3215
|
-
dLink.attr('opacity', function(l) {
|
|
3216
|
-
var s = l.source.id ?? l.source, t = l.target.id ?? l.target;
|
|
3217
|
-
if (isDiffNode(s) && isDiffNode(t)) return 0.6;
|
|
3218
|
-
if (isDiffNode(s) || isDiffNode(t)) return 0.15;
|
|
3219
|
-
return diffFocusMode ? 0 : 0.05;
|
|
3220
|
-
});
|
|
3221
|
-
dLink.attr('stroke', function(l) {
|
|
3222
|
-
var s = l.source.id ?? l.source, t = l.target.id ?? l.target;
|
|
3223
|
-
if (isDiffNode(s) && isDiffNode(t)) return diffStatusColor(s);
|
|
3224
|
-
return '#30363d';
|
|
3225
|
-
});
|
|
3226
|
-
dNode.select('circle')
|
|
3227
|
-
.attr('stroke-width', function(d) { return isDiffNode(d.id) ? 3 : 1; });
|
|
3228
|
-
}
|
|
3229
|
-
|
|
3230
|
-
function resetDiffHighlight() {
|
|
3231
|
-
applyDiffFilter();
|
|
3232
|
-
}
|
|
3233
|
-
|
|
3234
|
-
function highlightDiffImpact(d) {
|
|
3235
|
-
var chain = getImpactChain(d.id);
|
|
3236
|
-
dNode.select('circle').transition().duration(200)
|
|
3237
|
-
.attr('opacity', function(n) { return chain.has(n.id) ? 1 : 0.04; })
|
|
3238
|
-
.attr('stroke-width', function(n) { return chain.has(n.id) && n.id !== d.id ? 3 : isDiffNode(n.id) ? 3 : 1; })
|
|
3239
|
-
.attr('stroke', function(n) { return chain.has(n.id) && n.id !== d.id ? 'var(--red)' : diffStatusColor(n.id); });
|
|
3240
|
-
dNode.select('text').transition().duration(200)
|
|
3241
|
-
.attr('opacity', function(n) { return chain.has(n.id) ? 1 : 0.03; });
|
|
3242
|
-
dLink.transition().duration(200)
|
|
3243
|
-
.attr('opacity', function(l) {
|
|
3244
|
-
var s = l.source.id ?? l.source, t = l.target.id ?? l.target;
|
|
3245
|
-
return (chain.has(s) && chain.has(t)) ? 0.8 : 0.03;
|
|
3246
|
-
})
|
|
3247
|
-
.attr('stroke', function(l) {
|
|
3248
|
-
var s = l.source.id ?? l.source, t = l.target.id ?? l.target;
|
|
3249
|
-
return (chain.has(s) && chain.has(t)) ? 'var(--red)' : '#30363d';
|
|
3250
|
-
});
|
|
3251
|
-
return chain;
|
|
3252
|
-
}
|
|
3253
|
-
|
|
3254
|
-
function showDiffDetail(d) {
|
|
3255
|
-
var panel = document.getElementById('diff-detail');
|
|
3256
|
-
document.getElementById('dd-name').textContent = d.id;
|
|
3257
|
-
var statusEl = document.getElementById('dd-status');
|
|
3258
|
-
statusEl.textContent = diffStatus(d.id);
|
|
3259
|
-
statusEl.style.color = diffStatusColor(d.id);
|
|
3260
|
-
document.getElementById('dd-meta').innerHTML = i('detail.dir') + ': ' + esc(d.dir) + '<br>' + i('detail.dependencies') + ': ' + d.deps + ' \\u00b7 ' + i('detail.dependents') + ': ' + d.dependents;
|
|
3261
|
-
|
|
3262
|
-
// Show impact chain
|
|
3263
|
-
var chain = getImpactChain(d.id);
|
|
3264
|
-
chain.delete(d.id);
|
|
3265
|
-
var affectedList = document.getElementById('dd-affected');
|
|
3266
|
-
if (chain.size > 0) {
|
|
3267
|
-
affectedList.innerHTML = Array.from(chain).map(function(id) {
|
|
3268
|
-
return '<li style="color:' + diffStatusColor(id) + '">\\u2190 ' + esc(id) + ' <span style="font-size:10px;color:var(--text-muted)">(' + diffStatus(id) + ')</span></li>';
|
|
3269
|
-
}).join('');
|
|
3270
|
-
} else {
|
|
3271
|
-
affectedList.innerHTML = '<li style="color:var(--text-muted)">' + i('diff.noImpact') + '</li>';
|
|
3272
|
-
}
|
|
3273
|
-
|
|
3274
|
-
// Show imports
|
|
3275
|
-
var depsList = document.getElementById('dd-deps');
|
|
3276
|
-
depsList.innerHTML = (d.dependencies || []).map(function(x) {
|
|
3277
|
-
return '<li style="color:' + diffStatusColor(x) + '">\\u2192 ' + esc(x) + '</li>';
|
|
3278
|
-
}).join('') || '<li style="color:var(--text-muted)">' + i('detail.none') + '</li>';
|
|
3279
|
-
|
|
3280
|
-
panel.style.display = 'block';
|
|
3281
|
-
}
|
|
3282
|
-
|
|
3283
|
-
function buildDiffView() {
|
|
3284
|
-
const dSvg = d3.select('#diff-svg').attr('width', W).attr('height', H);
|
|
3285
|
-
const dG = dSvg.append('g');
|
|
3286
|
-
const dZoom = d3.zoom().scaleExtent([0.05,10]).on('zoom', e=>dG.attr('transform',e.transform));
|
|
3287
|
-
dSvg.call(dZoom);
|
|
3288
|
-
|
|
3289
|
-
function diffColor(d) {
|
|
3290
|
-
if (addedSet.has(d.id)) return 'var(--green)';
|
|
3291
|
-
if (removedSet.has(d.id)) return 'var(--red)';
|
|
3292
|
-
if (modifiedSet.has(d.id)) return 'var(--yellow)';
|
|
3293
|
-
if (affectedSet.has(d.id)) return 'var(--accent)';
|
|
3294
|
-
return '#30363d';
|
|
3295
|
-
}
|
|
3296
|
-
|
|
3297
|
-
const dDefs = dSvg.append('defs');
|
|
3298
|
-
dDefs.append('marker').attr('id','darrow').attr('viewBox','0 -4 8 8')
|
|
3299
|
-
.attr('refX',8).attr('refY',0).attr('markerWidth',7).attr('markerHeight',7).attr('orient','auto')
|
|
3300
|
-
.append('path').attr('d','M0,-3.5L8,0L0,3.5Z').attr('fill','#30363d');
|
|
3301
|
-
// Colored arrow markers for diff edges
|
|
3302
|
-
[['var(--green)','darrow-g'],['var(--red)','darrow-r'],['var(--yellow)','darrow-y'],['var(--accent)','darrow-a']].forEach(function(pair) {
|
|
3303
|
-
dDefs.append('marker').attr('id',pair[1]).attr('viewBox','0 -4 8 8')
|
|
3304
|
-
.attr('refX',8).attr('refY',0).attr('markerWidth',7).attr('markerHeight',7).attr('orient','auto')
|
|
3305
|
-
.append('path').attr('d','M0,-3.5L8,0L0,3.5Z').attr('fill',pair[0]);
|
|
3306
|
-
});
|
|
3307
|
-
|
|
3308
|
-
simNodes = DATA.nodes.map(d=>({...d, x:undefined, y:undefined, vx:undefined, vy:undefined}));
|
|
3309
|
-
simLinks = DATA.links.map(d=>({source:d.source.id??d.source,target:d.target.id??d.target,type:d.type}));
|
|
3310
|
-
|
|
3311
|
-
dLink = dG.append('g').selectAll('line').data(simLinks).join('line')
|
|
3312
|
-
.attr('stroke','#30363d').attr('stroke-width',1).attr('marker-end','url(#darrow)').attr('opacity',0.05);
|
|
3313
|
-
|
|
3314
|
-
dNode = dG.append('g').selectAll('g').data(simNodes).join('g').attr('cursor','pointer');
|
|
3315
|
-
dNode.append('circle')
|
|
3316
|
-
.attr('r', d=>nodeRadius(d)*nodeScale)
|
|
3317
|
-
.attr('fill', diffColor)
|
|
3318
|
-
.attr('stroke', diffColor).attr('stroke-width', d=>isDiffNode(d.id)?3:1)
|
|
3319
|
-
.attr('opacity', d=>isDiffNode(d.id)?1:0.12);
|
|
3320
|
-
dNode.append('text')
|
|
3321
|
-
.text(d=>fileName(d.id).replace(/\\.tsx?$/,''))
|
|
3322
|
-
.attr('dx', d=>nodeRadius(d)*nodeScale+4).attr('dy',3.5).attr('font-size',11)
|
|
3323
|
-
.attr('fill', d=>isDiffNode(d.id)?'var(--text)':'var(--text-muted)')
|
|
3324
|
-
.attr('opacity', d=>isDiffNode(d.id)?1:0.08)
|
|
3325
|
-
.attr('pointer-events','none');
|
|
3326
|
-
|
|
3327
|
-
dSim = d3.forceSimulation(simNodes)
|
|
3328
|
-
.force('link', d3.forceLink(simLinks).id(d=>d.id).distance(70).strength(0.25))
|
|
3329
|
-
.force('charge', d3.forceManyBody().strength(-150).distanceMax(500))
|
|
3330
|
-
.force('center', d3.forceCenter(0,0))
|
|
3331
|
-
.force('collision', d3.forceCollide().radius(d=>nodeRadius(d)*nodeScale+4));
|
|
3332
|
-
|
|
3333
|
-
// Layer-aware physics for diff view (same pattern as graph view)
|
|
3334
|
-
var dHullGroup = null;
|
|
3335
|
-
if (LAYERS && LAYERS.length > 0) {
|
|
3336
|
-
var dLayerCenters = {};
|
|
3337
|
-
var dLayerCount = LAYERS.length;
|
|
3338
|
-
var dBaseRadius = Math.max(60, Math.min(W, H) * 0.04 * Math.sqrt(dLayerCount));
|
|
3339
|
-
LAYERS.forEach(function(l, idx) {
|
|
3340
|
-
var angle = (2 * Math.PI * idx) / dLayerCount - Math.PI / 2;
|
|
3341
|
-
dLayerCenters[l.name] = { x: Math.cos(angle) * dBaseRadius, y: Math.sin(angle) * dBaseRadius };
|
|
3342
|
-
});
|
|
3343
|
-
dSim.force('center', null);
|
|
3344
|
-
dSim.force('layerX', d3.forceX(function(d) { return dLayerCenters[d.layer]?.x || 0; }).strength(function(d) { return d.layer ? 0.12 : 0.03; }));
|
|
3345
|
-
dSim.force('layerY', d3.forceY(function(d) { return dLayerCenters[d.layer]?.y || 0; }).strength(function(d) { return d.layer ? 0.12 : 0.03; }));
|
|
3346
|
-
dSim.force('link').strength(function(l) {
|
|
3347
|
-
var sL = l.source.layer ?? l.source, tL = l.target.layer ?? l.target;
|
|
3348
|
-
return sL === tL ? 0.4 : 0.1;
|
|
3349
|
-
});
|
|
3350
|
-
// Cluster force for diff view
|
|
3351
|
-
dSim.force('cluster', (function() {
|
|
3352
|
-
var ns;
|
|
3353
|
-
function f(alpha) {
|
|
3354
|
-
var centroids = {}, counts = {};
|
|
3355
|
-
ns.forEach(function(n) {
|
|
3356
|
-
if (!n.layer) return;
|
|
3357
|
-
if (!centroids[n.layer]) { centroids[n.layer] = {x:0,y:0}; counts[n.layer] = 0; }
|
|
3358
|
-
centroids[n.layer].x += n.x; centroids[n.layer].y += n.y; counts[n.layer]++;
|
|
3359
|
-
});
|
|
3360
|
-
Object.keys(centroids).forEach(function(k) { centroids[k].x /= counts[k]; centroids[k].y /= counts[k]; });
|
|
3361
|
-
ns.forEach(function(n) {
|
|
3362
|
-
if (!n.layer || !centroids[n.layer]) return;
|
|
3363
|
-
n.vx += (centroids[n.layer].x - n.x) * alpha * 0.2;
|
|
3364
|
-
n.vy += (centroids[n.layer].y - n.y) * alpha * 0.2;
|
|
3365
|
-
});
|
|
3366
|
-
}
|
|
3367
|
-
f.initialize = function(n) { ns = n; };
|
|
3368
|
-
return f;
|
|
3369
|
-
})());
|
|
3370
|
-
|
|
3371
|
-
dHullGroup = dG.insert('g', ':first-child');
|
|
3372
|
-
}
|
|
3373
|
-
|
|
3374
|
-
function updateDiffHulls() {
|
|
3375
|
-
if (!dHullGroup) return;
|
|
3376
|
-
dHullGroup.selectAll('*').remove();
|
|
3377
|
-
LAYERS.forEach(function(layer) {
|
|
3378
|
-
var layerNodes = simNodes.filter(function(n) { return n.layer === layer.name; });
|
|
3379
|
-
if (layerNodes.length === 0) return;
|
|
3380
|
-
if (diffFocusMode && !layerNodes.some(function(n) { return isDiffNode(n.id); })) return;
|
|
3381
|
-
var hasDiff = layerNodes.some(function(n) { return isDiffNode(n.id); });
|
|
3382
|
-
|
|
3383
|
-
var points = [];
|
|
3384
|
-
layerNodes.forEach(function(n) {
|
|
3385
|
-
if (n.x == null || n.y == null) return;
|
|
3386
|
-
if (diffFocusMode && !isDiffNode(n.id)) return;
|
|
3387
|
-
var r = nodeRadius(n) * nodeScale + 30;
|
|
3388
|
-
for (var a = 0; a < Math.PI * 2; a += Math.PI / 4) {
|
|
3389
|
-
points.push([n.x + Math.cos(a) * r, n.y + Math.sin(a) * r]);
|
|
3390
|
-
}
|
|
3391
|
-
});
|
|
3392
|
-
|
|
3393
|
-
var fillOp = hasDiff ? 0.15 : 0.06;
|
|
3394
|
-
var strokeOp = hasDiff ? 0.6 : 0.2;
|
|
3395
|
-
var sw = hasDiff ? 2.5 : 1;
|
|
3396
|
-
if (points.length < 6) {
|
|
3397
|
-
var cx = layerNodes.reduce(function(s, n) { return s + (n.x||0); }, 0) / layerNodes.length;
|
|
3398
|
-
var cy = layerNodes.reduce(function(s, n) { return s + (n.y||0); }, 0) / layerNodes.length;
|
|
3399
|
-
dHullGroup.append('circle').attr('cx', cx).attr('cy', cy).attr('r', 50)
|
|
3400
|
-
.attr('fill', layer.color).attr('fill-opacity', fillOp)
|
|
3401
|
-
.attr('stroke', layer.color).attr('stroke-opacity', strokeOp).attr('stroke-width', sw);
|
|
3402
|
-
} else {
|
|
3403
|
-
var hull = d3.polygonHull(points);
|
|
3404
|
-
if (hull) {
|
|
3405
|
-
dHullGroup.append('path')
|
|
3406
|
-
.attr('d', 'M' + hull.map(function(p) { return p.join(','); }).join('L') + 'Z')
|
|
3407
|
-
.attr('fill', layer.color).attr('fill-opacity', fillOp)
|
|
3408
|
-
.attr('stroke', layer.color).attr('stroke-opacity', strokeOp).attr('stroke-width', sw)
|
|
3409
|
-
.attr('stroke-dasharray', hasDiff ? null : '6,3');
|
|
3410
|
-
}
|
|
3411
|
-
}
|
|
3412
|
-
// Layer name label
|
|
3413
|
-
var visNodes = diffFocusMode ? layerNodes.filter(function(n) { return isDiffNode(n.id); }) : layerNodes;
|
|
3414
|
-
if (visNodes.length === 0) return;
|
|
3415
|
-
var lx = visNodes.reduce(function(s, n) { return s + (n.x||0); }, 0) / visNodes.length;
|
|
3416
|
-
var ly = Math.min.apply(null, visNodes.map(function(n) { return n.y||0; })) - 25;
|
|
3417
|
-
dHullGroup.append('text')
|
|
3418
|
-
.attr('x', lx).attr('y', ly).attr('text-anchor', 'middle')
|
|
3419
|
-
.attr('fill', layer.color).attr('fill-opacity', hasDiff ? 0.9 : 0.4)
|
|
3420
|
-
.attr('font-size', 12).attr('font-weight', 600).text(layer.name);
|
|
3421
|
-
});
|
|
3177
|
+
// src/web/template.ts
|
|
3178
|
+
var _viewerJs = null;
|
|
3179
|
+
function getViewerJs() {
|
|
3180
|
+
if (!_viewerJs) {
|
|
3181
|
+
const thisDir = dirname2(fileURLToPath(import.meta.url));
|
|
3182
|
+
const candidates = [
|
|
3183
|
+
join8(thisDir, "..", "web", "viewer.js"),
|
|
3184
|
+
// dist/mcp/ or dist/cli/ → dist/web/
|
|
3185
|
+
join8(thisDir, "..", "..", "dist", "web", "viewer.js")
|
|
3186
|
+
// src/web/ → dist/web/
|
|
3187
|
+
];
|
|
3188
|
+
for (const p of candidates) {
|
|
3189
|
+
try {
|
|
3190
|
+
_viewerJs = readFileSync3(p, "utf-8");
|
|
3191
|
+
break;
|
|
3192
|
+
} catch {
|
|
3193
|
+
}
|
|
3422
3194
|
}
|
|
3423
|
-
|
|
3424
|
-
var dTickCount = 0;
|
|
3425
|
-
dSim.on('tick', function() {
|
|
3426
|
-
dLink.each(function(d) {
|
|
3427
|
-
var dx=d.target.x-d.source.x, dy=d.target.y-d.source.y, dist=Math.sqrt(dx*dx+dy*dy)||1;
|
|
3428
|
-
var rT=nodeRadius(d.target)*nodeScale, rS=nodeRadius(d.source)*nodeScale;
|
|
3429
|
-
d3.select(this).attr('x1',d.source.x+(dx/dist)*rS).attr('y1',d.source.y+(dy/dist)*rS)
|
|
3430
|
-
.attr('x2',d.target.x-(dx/dist)*rT).attr('y2',d.target.y-(dy/dist)*rT);
|
|
3431
|
-
});
|
|
3432
|
-
dNode.attr('transform', function(d) { return 'translate('+d.x+','+d.y+')'; });
|
|
3433
|
-
if (++dTickCount % 5 === 0) updateDiffHulls();
|
|
3434
|
-
});
|
|
3435
|
-
|
|
3436
|
-
// Click: show impact chain + detail panel
|
|
3437
|
-
dNode.on('click', function(e, d) {
|
|
3438
|
-
e.stopPropagation();
|
|
3439
|
-
highlightDiffImpact(d);
|
|
3440
|
-
showDiffDetail(d);
|
|
3441
|
-
});
|
|
3442
|
-
|
|
3443
|
-
// Click on empty space to deselect
|
|
3444
|
-
dSvg.on('click', function() {
|
|
3445
|
-
closeDiffDetail();
|
|
3446
|
-
});
|
|
3447
|
-
|
|
3448
|
-
dNode.on('mouseover',function(e,d) { showTooltip(e,d); }).on('mousemove',function(e) { positionTooltip(e); }).on('mouseout',function() { scheduleHideTooltip(); });
|
|
3449
|
-
|
|
3450
|
-
// Apply initial filter (in case focus was toggled before build)
|
|
3451
|
-
applyDiffFilter();
|
|
3452
|
-
|
|
3453
|
-
var dAutoFitDone = false;
|
|
3454
|
-
dSim.on('end', function() {
|
|
3455
|
-
if (dAutoFitDone) return;
|
|
3456
|
-
dAutoFitDone = true;
|
|
3457
|
-
var b=dG.node().getBBox(); if(!b.width) return;
|
|
3458
|
-
var s=Math.min(W/(b.width+80),H/(b.height+80))*0.9;
|
|
3459
|
-
dSvg.call(dZoom.transform,d3.zoomIdentity.translate(W/2-(b.x+b.width/2)*s,H/2-(b.y+b.height/2)*s).scale(s));
|
|
3460
|
-
});
|
|
3195
|
+
if (!_viewerJs) throw new Error("viewer.js not found. Run 'npm run build:client' first.");
|
|
3461
3196
|
}
|
|
3462
|
-
|
|
3197
|
+
return _viewerJs;
|
|
3463
3198
|
}
|
|
3464
|
-
`;
|
|
3465
|
-
}
|
|
3466
|
-
|
|
3467
|
-
// src/utils/html-escape.ts
|
|
3468
|
-
var ESC_FUNCTION_JS = `function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }`;
|
|
3469
|
-
|
|
3470
|
-
// src/web/template.ts
|
|
3471
3199
|
function buildGraphPage(graph, options = {}) {
|
|
3472
3200
|
const locale = options.locale ?? "en";
|
|
3473
3201
|
const diff = options.diff ?? null;
|
|
@@ -3499,6 +3227,7 @@ function buildGraphPage(graph, options = {}) {
|
|
|
3499
3227
|
const layersData = layers ? JSON.stringify(layers) : "null";
|
|
3500
3228
|
const crossEdgesData = crossEdges ? JSON.stringify(crossEdges) : "null";
|
|
3501
3229
|
const graphData = JSON.stringify({ nodes, links, circularFiles: [...circularFiles], dirs, projectName });
|
|
3230
|
+
const viewerJs = getViewerJs();
|
|
3502
3231
|
return (
|
|
3503
3232
|
/* html */
|
|
3504
3233
|
`<!DOCTYPE html>
|
|
@@ -3506,986 +3235,22 @@ function buildGraphPage(graph, options = {}) {
|
|
|
3506
3235
|
<head>
|
|
3507
3236
|
<meta charset="utf-8">
|
|
3508
3237
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3509
|
-
<title>${projectName} \u2014 Architecture Viewer</title>
|
|
3238
|
+
<title>${escapeHtml(projectName)} \u2014 Architecture Viewer</title>
|
|
3510
3239
|
${buildStyles()}
|
|
3511
3240
|
</head>
|
|
3512
3241
|
<body>
|
|
3513
3242
|
${buildViewerHtml()}
|
|
3514
3243
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
3515
3244
|
<script>
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
'stats.files': 'Files', 'stats.edges': 'Edges', 'stats.circular': 'Circular',
|
|
3523
|
-
'settings.title': 'Settings', 'settings.theme': 'Theme', 'settings.fontSize': 'Font Size',
|
|
3524
|
-
'settings.nodeSize': 'Node Size', 'settings.linkOpacity': 'Link Opacity', 'settings.gravity': 'Gravity', 'settings.language': 'Language', 'settings.export': 'Export',
|
|
3525
|
-
'impact.title': 'Impact Simulation', 'impact.btn': 'Impact', 'impact.transitive': 'files affected',
|
|
3526
|
-
'search.placeholder': 'Search files...',
|
|
3527
|
-
'legend.circular': 'Circular dep', 'legend.orphan': 'Orphan', 'legend.highCoupling': 'High coupling',
|
|
3528
|
-
'legend.imports': 'imports', 'legend.importedBy': 'imported by',
|
|
3529
|
-
'detail.importedBy': 'Imported by', 'detail.imports': 'Imports',
|
|
3530
|
-
'detail.none': 'none', 'detail.dir': 'Dir', 'detail.dependencies': 'Dependencies', 'detail.dependents': 'Dependents',
|
|
3531
|
-
'tooltip.imports': 'imports', 'tooltip.importedBy': 'imported by',
|
|
3532
|
-
'help.graph': 'Scroll: zoom \xB7 Drag: pan \xB7 Click: select \xB7 / search',
|
|
3533
|
-
'help.hierarchy': 'Scroll to navigate \xB7 Click to highlight',
|
|
3534
|
-
'help.diff': 'Green=added \xB7 Red=removed \xB7 Yellow=modified \xB7 Blue=affected',
|
|
3535
|
-
'tab.diff': 'Diff',
|
|
3536
|
-
'diff.addedLabel': 'Added', 'diff.removedLabel': 'Removed', 'diff.modifiedLabel': 'Modified', 'diff.affectedLabel': 'Affected',
|
|
3537
|
-
'diff.showAll': 'Show all', 'diff.focusChanges': 'Focus changes', 'diff.noImpact': 'No downstream impact',
|
|
3538
|
-
'diff.affectedByChange': 'Affected by this change',
|
|
3539
|
-
},
|
|
3540
|
-
ja: {
|
|
3541
|
-
'tab.graph': '\u30B0\u30E9\u30D5', 'tab.hierarchy': '\u968E\u5C64\u56F3',
|
|
3542
|
-
'stats.files': '\u30D5\u30A1\u30A4\u30EB', 'stats.edges': '\u30A8\u30C3\u30B8', 'stats.circular': '\u5FAA\u74B0\u53C2\u7167',
|
|
3543
|
-
'settings.title': '\u8A2D\u5B9A', 'settings.theme': '\u30C6\u30FC\u30DE', 'settings.fontSize': '\u30D5\u30A9\u30F3\u30C8\u30B5\u30A4\u30BA',
|
|
3544
|
-
'settings.nodeSize': '\u30CE\u30FC\u30C9\u30B5\u30A4\u30BA', 'settings.linkOpacity': '\u30EA\u30F3\u30AF\u900F\u660E\u5EA6', 'settings.gravity': '\u91CD\u529B', 'settings.language': '\u8A00\u8A9E', 'settings.export': '\u30A8\u30AF\u30B9\u30DD\u30FC\u30C8',
|
|
3545
|
-
'impact.title': '\u5F71\u97FF\u7BC4\u56F2\u30B7\u30DF\u30E5\u30EC\u30FC\u30B7\u30E7\u30F3', 'impact.btn': '\u5F71\u97FF', 'impact.transitive': '\u30D5\u30A1\u30A4\u30EB\u306B\u5F71\u97FF',
|
|
3546
|
-
'search.placeholder': '\u30D5\u30A1\u30A4\u30EB\u691C\u7D22...',
|
|
3547
|
-
'legend.circular': '\u5FAA\u74B0\u53C2\u7167', 'legend.orphan': '\u5B64\u7ACB', 'legend.highCoupling': '\u9AD8\u7D50\u5408',
|
|
3548
|
-
'legend.imports': 'import\u5148', 'legend.importedBy': 'import\u5143',
|
|
3549
|
-
'detail.importedBy': 'import\u5143', 'detail.imports': 'import\u5148',
|
|
3550
|
-
'detail.none': '\u306A\u3057', 'detail.dir': '\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA', 'detail.dependencies': '\u4F9D\u5B58\u5148', 'detail.dependents': '\u88AB\u4F9D\u5B58',
|
|
3551
|
-
'tooltip.imports': 'import\u5148', 'tooltip.importedBy': 'import\u5143',
|
|
3552
|
-
'help.graph': '\u30B9\u30AF\u30ED\u30FC\u30EB: \u30BA\u30FC\u30E0 \xB7 \u30C9\u30E9\u30C3\u30B0: \u79FB\u52D5 \xB7 \u30AF\u30EA\u30C3\u30AF: \u9078\u629E \xB7 / \u691C\u7D22',
|
|
3553
|
-
'help.hierarchy': '\u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u79FB\u52D5 \xB7 \u30AF\u30EA\u30C3\u30AF\u3067\u30CF\u30A4\u30E9\u30A4\u30C8',
|
|
3554
|
-
'help.diff': '\u7DD1=\u8FFD\u52A0 \xB7 \u8D64=\u524A\u9664 \xB7 \u9EC4=\u5909\u66F4 \xB7 \u9752=\u5F71\u97FF',
|
|
3555
|
-
'tab.diff': '\u5DEE\u5206',
|
|
3556
|
-
'diff.addedLabel': '\u8FFD\u52A0', 'diff.removedLabel': '\u524A\u9664', 'diff.modifiedLabel': '\u5909\u66F4', 'diff.affectedLabel': '\u5F71\u97FF',
|
|
3557
|
-
'diff.showAll': '\u5168\u8868\u793A', 'diff.focusChanges': '\u5909\u66F4\u306E\u307F\u8868\u793A', 'diff.noImpact': '\u4E0B\u6D41\u3078\u306E\u5F71\u97FF\u306A\u3057',
|
|
3558
|
-
'diff.affectedByChange': '\u3053\u306E\u5909\u66F4\u306E\u5F71\u97FF\u7BC4\u56F2',
|
|
3559
|
-
}
|
|
3560
|
-
};
|
|
3561
|
-
let currentLang = '${locale}';
|
|
3562
|
-
function applyI18n() {
|
|
3563
|
-
const msgs = I18N[currentLang] || I18N.en;
|
|
3564
|
-
document.querySelectorAll('[data-i18n]').forEach(el => {
|
|
3565
|
-
const key = el.getAttribute('data-i18n');
|
|
3566
|
-
if (msgs[key]) el.textContent = msgs[key];
|
|
3567
|
-
});
|
|
3568
|
-
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
|
3569
|
-
const key = el.getAttribute('data-i18n-placeholder');
|
|
3570
|
-
if (msgs[key]) el.placeholder = msgs[key];
|
|
3571
|
-
});
|
|
3572
|
-
document.querySelectorAll('.lang-btn').forEach(b => b.classList.toggle('active', b.dataset.lang === currentLang));
|
|
3573
|
-
}
|
|
3574
|
-
window.setLang = (lang) => { currentLang = lang; applyI18n(); saveSettings(); };
|
|
3575
|
-
function i(key) { return (I18N[currentLang] || I18N.en)[key] || key; }
|
|
3576
|
-
${ESC_FUNCTION_JS}
|
|
3577
|
-
|
|
3578
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3579
|
-
// SETTINGS (persisted to localStorage)
|
|
3580
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3581
|
-
const STORAGE_KEY = 'archtracker-settings';
|
|
3582
|
-
function saveSettings() {
|
|
3583
|
-
const s = { theme: document.body.getAttribute('data-theme') || 'dark', fontSize: document.getElementById('font-size-val').textContent, nodeSize: document.getElementById('node-size-val').textContent, linkOpacity: document.getElementById('link-opacity-val').textContent, gravity: document.getElementById('gravity-val').textContent, layerGravity: document.getElementById('layer-gravity-val').textContent, lang: currentLang, projectTitle: document.getElementById('project-title').textContent };
|
|
3584
|
-
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(s)); } catch(e) {}
|
|
3585
|
-
}
|
|
3586
|
-
function loadSettings() {
|
|
3587
|
-
try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || null; } catch(e) { return null; }
|
|
3588
|
-
}
|
|
3589
|
-
|
|
3590
|
-
let nodeScale = 1, baseLinkOpacity = 0.4;
|
|
3591
|
-
window.toggleSettings = () => document.getElementById('settings-panel').classList.toggle('open');
|
|
3592
|
-
window.setTheme = (theme) => {
|
|
3593
|
-
document.body.setAttribute('data-theme', theme === 'light' ? 'light' : '');
|
|
3594
|
-
document.querySelectorAll('.theme-btn[data-theme-val]').forEach(b => b.classList.toggle('active', b.dataset.themeVal === theme));
|
|
3595
|
-
saveSettings();
|
|
3596
|
-
};
|
|
3597
|
-
window.setFontSize = (v) => {
|
|
3598
|
-
document.getElementById('font-size-val').textContent = v;
|
|
3599
|
-
const scale = v / 13;
|
|
3600
|
-
if (typeof node !== 'undefined') {
|
|
3601
|
-
node.select('text').attr('font-size', d => (d.dependents>=3?12:10) * scale);
|
|
3602
|
-
}
|
|
3603
|
-
saveSettings();
|
|
3604
|
-
};
|
|
3605
|
-
window.setNodeScale = (v) => {
|
|
3606
|
-
nodeScale = v / 100;
|
|
3607
|
-
document.getElementById('node-size-val').textContent = v;
|
|
3608
|
-
if (typeof node !== 'undefined') {
|
|
3609
|
-
node.select('circle').attr('r', d => nodeRadius(d) * nodeScale);
|
|
3610
|
-
node.select('text').attr('dx', d => nodeRadius(d) * nodeScale + 4);
|
|
3611
|
-
simulation.force('collision', d3.forceCollide().radius(d => nodeRadius(d) * nodeScale + 4));
|
|
3612
|
-
simulation.alpha(0.3).restart();
|
|
3613
|
-
}
|
|
3614
|
-
saveSettings();
|
|
3615
|
-
};
|
|
3616
|
-
window.setLinkOpacity = (v) => {
|
|
3617
|
-
baseLinkOpacity = v / 100;
|
|
3618
|
-
document.getElementById('link-opacity-val').textContent = v;
|
|
3619
|
-
if (typeof link !== 'undefined') link.attr('opacity', baseLinkOpacity);
|
|
3620
|
-
saveSettings();
|
|
3621
|
-
};
|
|
3622
|
-
let gravityStrength = 150;
|
|
3623
|
-
window.setGravity = (v) => {
|
|
3624
|
-
gravityStrength = +v;
|
|
3625
|
-
document.getElementById('gravity-val').textContent = v;
|
|
3626
|
-
if (typeof simulation !== 'undefined') {
|
|
3627
|
-
if (typeof updateLayerPhysics === 'function') {
|
|
3628
|
-
updateLayerPhysics();
|
|
3629
|
-
} else {
|
|
3630
|
-
simulation.force('charge', d3.forceManyBody().strength(-gravityStrength).distanceMax(500));
|
|
3631
|
-
}
|
|
3632
|
-
simulation.alpha(0.5).restart();
|
|
3633
|
-
}
|
|
3634
|
-
saveSettings();
|
|
3635
|
-
};
|
|
3636
|
-
let layerGravity = 12;
|
|
3637
|
-
window.setLayerGravity = (v) => {
|
|
3638
|
-
layerGravity = +v;
|
|
3639
|
-
document.getElementById('layer-gravity-val').textContent = v;
|
|
3640
|
-
if (typeof simulation !== 'undefined' && typeof updateLayerPhysics === 'function') {
|
|
3641
|
-
updateLayerPhysics();
|
|
3642
|
-
simulation.alpha(0.5).restart();
|
|
3643
|
-
}
|
|
3644
|
-
saveSettings();
|
|
3645
|
-
};
|
|
3646
|
-
|
|
3647
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3648
|
-
// EXPORT
|
|
3649
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3650
|
-
window.exportSVG = () => {
|
|
3651
|
-
const activeView = document.querySelector('.view.active svg');
|
|
3652
|
-
if (!activeView) return;
|
|
3653
|
-
const clone = activeView.cloneNode(true);
|
|
3654
|
-
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
3655
|
-
const blob = new Blob([clone.outerHTML], {type: 'image/svg+xml'});
|
|
3656
|
-
const a = document.createElement('a');
|
|
3657
|
-
a.href = URL.createObjectURL(blob);
|
|
3658
|
-
a.download = (document.getElementById('project-title').textContent || 'graph') + '.svg';
|
|
3659
|
-
a.click(); URL.revokeObjectURL(a.href);
|
|
3660
|
-
};
|
|
3661
|
-
window.exportPNG = () => {
|
|
3662
|
-
const activeView = document.querySelector('.view.active svg');
|
|
3663
|
-
if (!activeView) return;
|
|
3664
|
-
const clone = activeView.cloneNode(true);
|
|
3665
|
-
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
3666
|
-
const svgStr = new XMLSerializer().serializeToString(clone);
|
|
3667
|
-
const canvas = document.createElement('canvas');
|
|
3668
|
-
const bbox = activeView.getBoundingClientRect();
|
|
3669
|
-
canvas.width = bbox.width * 2; canvas.height = bbox.height * 2;
|
|
3670
|
-
const ctx = canvas.getContext('2d');
|
|
3671
|
-
ctx.scale(2, 2);
|
|
3672
|
-
const img = new Image();
|
|
3673
|
-
img.onload = () => { ctx.drawImage(img, 0, 0); const a = document.createElement('a'); a.href = canvas.toDataURL('image/png'); a.download = (document.getElementById('project-title').textContent || 'graph') + '.png'; a.click(); };
|
|
3674
|
-
img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgStr)));
|
|
3245
|
+
window.__ARCH = {
|
|
3246
|
+
data: ${graphData},
|
|
3247
|
+
layers: ${layersData},
|
|
3248
|
+
crossEdges: ${crossEdgesData},
|
|
3249
|
+
diff: ${diffData},
|
|
3250
|
+
locale: '${locale}'
|
|
3675
3251
|
};
|
|
3676
|
-
|
|
3677
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3678
|
-
// DATA
|
|
3679
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3680
|
-
const DATA = ${graphData};
|
|
3681
|
-
const LAYERS = ${layersData};
|
|
3682
|
-
const CROSS_EDGES = ${crossEdgesData};
|
|
3683
|
-
const W = window.innerWidth, H = window.innerHeight - 44;
|
|
3684
|
-
const circularSet = new Set(DATA.circularFiles);
|
|
3685
|
-
|
|
3686
|
-
// Project title (editable)
|
|
3687
|
-
const titleEl = document.getElementById('project-title');
|
|
3688
|
-
titleEl.textContent = DATA.projectName;
|
|
3689
|
-
titleEl.addEventListener('blur', () => { if (!titleEl.textContent.trim()) titleEl.textContent = DATA.projectName; document.title = titleEl.textContent + ' \u2014 Architecture Viewer'; saveSettings(); });
|
|
3690
|
-
titleEl.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); titleEl.blur(); } });
|
|
3691
|
-
|
|
3692
|
-
// Restore saved settings \u2014 phase 1: non-graph settings (before graph init)
|
|
3693
|
-
const _savedSettings = loadSettings();
|
|
3694
|
-
if (_savedSettings) {
|
|
3695
|
-
if (_savedSettings.theme) setTheme(_savedSettings.theme);
|
|
3696
|
-
if (_savedSettings.lang) { currentLang = _savedSettings.lang; applyI18n(); }
|
|
3697
|
-
if (_savedSettings.projectTitle) { titleEl.textContent = _savedSettings.projectTitle; document.title = _savedSettings.projectTitle + ' \u2014 Architecture Viewer'; }
|
|
3698
|
-
// Set slider positions (visual only \u2014 graph not built yet)
|
|
3699
|
-
if (_savedSettings.fontSize) { document.getElementById('font-size-slider').value = _savedSettings.fontSize; document.getElementById('font-size-val').textContent = _savedSettings.fontSize; }
|
|
3700
|
-
if (_savedSettings.nodeSize) { document.getElementById('node-size-slider').value = _savedSettings.nodeSize; document.getElementById('node-size-val').textContent = _savedSettings.nodeSize; nodeScale = _savedSettings.nodeSize / 100; }
|
|
3701
|
-
if (_savedSettings.linkOpacity) { document.getElementById('link-opacity-slider').value = _savedSettings.linkOpacity; document.getElementById('link-opacity-val').textContent = _savedSettings.linkOpacity; baseLinkOpacity = _savedSettings.linkOpacity / 100; }
|
|
3702
|
-
if (_savedSettings.gravity) { document.getElementById('gravity-slider').value = _savedSettings.gravity; document.getElementById('gravity-val').textContent = _savedSettings.gravity; gravityStrength = +_savedSettings.gravity; }
|
|
3703
|
-
if (_savedSettings.layerGravity) { document.getElementById('layer-gravity-slider').value = _savedSettings.layerGravity; document.getElementById('layer-gravity-val').textContent = _savedSettings.layerGravity; layerGravity = +_savedSettings.layerGravity; }
|
|
3704
|
-
}
|
|
3705
|
-
|
|
3706
|
-
document.getElementById('s-files').textContent = DATA.nodes.length;
|
|
3707
|
-
document.getElementById('s-edges').textContent = DATA.links.length;
|
|
3708
|
-
document.getElementById('s-circular').textContent = DATA.circularFiles.length;
|
|
3709
|
-
|
|
3710
|
-
const dirColor = d3.scaleOrdinal()
|
|
3711
|
-
.domain(DATA.dirs)
|
|
3712
|
-
.range(['#58a6ff','#3fb950','#d2a8ff','#f0883e','#79c0ff','#56d4dd','#db61a2','#f778ba','#ffa657','#7ee787']);
|
|
3713
|
-
|
|
3714
|
-
// Layer color map (from LAYERS metadata)
|
|
3715
|
-
const layerColorMap = {};
|
|
3716
|
-
let activeLayerFilter = null; // DEPRECATED \u2014 kept for backward compat, always null with multi-select tabs
|
|
3717
|
-
const activeLayers = new Set(); // empty = no filter (show all); non-empty = show only selected
|
|
3718
|
-
if (LAYERS) {
|
|
3719
|
-
LAYERS.forEach(l => { layerColorMap[l.name] = l.color; });
|
|
3720
|
-
document.getElementById('layer-gravity-setting').style.display = '';
|
|
3721
|
-
}
|
|
3722
|
-
|
|
3723
|
-
function nodeColor(d) {
|
|
3724
|
-
if (circularSet.has(d.id)) return '#f97583';
|
|
3725
|
-
if (d.isOrphan) return '#484f58';
|
|
3726
|
-
// Layer coloring: all-visible or multi-select \u2192 layer colors; single-select \u2192 dir colors
|
|
3727
|
-
if (LAYERS && d.layer && layerColorMap[d.layer] && activeLayers.size !== 1) return layerColorMap[d.layer];
|
|
3728
|
-
return dirColor(d.dir);
|
|
3729
|
-
}
|
|
3730
|
-
function nodeRadius(d) { return Math.max(5, Math.min(22, 4 + d.dependents * 1.8)); }
|
|
3731
|
-
function fileName(id) { return id.split('/').pop(); }
|
|
3732
|
-
|
|
3733
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3734
|
-
// TAB SWITCHING
|
|
3735
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3736
|
-
let hierBuilt = false;
|
|
3737
|
-
let diffBuilt = false;
|
|
3738
|
-
let hierRelayout = null;
|
|
3739
|
-
let hierSyncFromTab = null;
|
|
3740
|
-
document.querySelectorAll('.tab').forEach(tab => {
|
|
3741
|
-
tab.addEventListener('click', () => {
|
|
3742
|
-
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
3743
|
-
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
|
3744
|
-
tab.classList.add('active');
|
|
3745
|
-
document.getElementById(tab.dataset.view).classList.add('active');
|
|
3746
|
-
if (tab.dataset.view === 'hier-view') {
|
|
3747
|
-
if (!hierBuilt) { buildHierarchy(); hierBuilt = true; }
|
|
3748
|
-
if (hierSyncFromTab) { hierSyncFromTab(); hierRelayout(); }
|
|
3749
|
-
}
|
|
3750
|
-
if (tab.dataset.view === 'diff-view') {
|
|
3751
|
-
if (!diffBuilt) { buildDiffView(); diffBuilt = true; }
|
|
3752
|
-
}
|
|
3753
|
-
});
|
|
3754
|
-
});
|
|
3755
|
-
|
|
3756
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3757
|
-
// TOOLTIP \u2014 delayed hide + interactive
|
|
3758
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3759
|
-
const tooltip = document.getElementById('tooltip');
|
|
3760
|
-
let tooltipHideTimer = null;
|
|
3761
|
-
let tooltipLocked = false;
|
|
3762
|
-
|
|
3763
|
-
function showTooltip(e, d) {
|
|
3764
|
-
clearTimeout(tooltipHideTimer);
|
|
3765
|
-
document.getElementById('tt-name').textContent = d.id;
|
|
3766
|
-
document.getElementById('tt-dep-count').textContent = d.deps;
|
|
3767
|
-
document.getElementById('tt-dpt-count').textContent = d.dependents;
|
|
3768
|
-
const out = (d.dependencies||[]).map(x => '<div class="tt-out">\u2192 '+esc(x)+'</div>');
|
|
3769
|
-
const inc = (d.dependentsList||[]).map(x => '<div class="tt-in">\u2190 '+esc(x)+'</div>');
|
|
3770
|
-
document.getElementById('tt-details').innerHTML = [...out, ...inc].join('');
|
|
3771
|
-
tooltip.style.display = 'block';
|
|
3772
|
-
positionTooltip(e);
|
|
3773
|
-
}
|
|
3774
|
-
function positionTooltip(e) {
|
|
3775
|
-
const gap = 24;
|
|
3776
|
-
const tw = 420, th = tooltip.offsetHeight || 200;
|
|
3777
|
-
// Prefer placing to the right and above the cursor so it doesn't cover nodes below
|
|
3778
|
-
let x = e.clientX + gap;
|
|
3779
|
-
let y = e.clientY - th - 12;
|
|
3780
|
-
// If no room on the right, flip left
|
|
3781
|
-
if (x + tw > window.innerWidth) x = e.clientX - tw - gap;
|
|
3782
|
-
// If no room above, place below the cursor with gap
|
|
3783
|
-
if (y < 50) y = e.clientY + gap;
|
|
3784
|
-
// Final clamp
|
|
3785
|
-
if (y + th > window.innerHeight) y = window.innerHeight - th - 8;
|
|
3786
|
-
if (x < 8) x = 8;
|
|
3787
|
-
tooltip.style.left = x + 'px';
|
|
3788
|
-
tooltip.style.top = y + 'px';
|
|
3789
|
-
}
|
|
3790
|
-
function scheduleHideTooltip() {
|
|
3791
|
-
clearTimeout(tooltipHideTimer);
|
|
3792
|
-
tooltipHideTimer = setTimeout(() => {
|
|
3793
|
-
if (!tooltipLocked) {
|
|
3794
|
-
tooltip.style.display = 'none';
|
|
3795
|
-
if (!pinnedNode) resetGraphHighlight();
|
|
3796
|
-
}
|
|
3797
|
-
}, 250);
|
|
3798
|
-
}
|
|
3799
|
-
|
|
3800
|
-
// Keep tooltip visible when mouse enters it
|
|
3801
|
-
tooltip.addEventListener('mouseenter', () => {
|
|
3802
|
-
clearTimeout(tooltipHideTimer);
|
|
3803
|
-
tooltipLocked = true;
|
|
3804
|
-
});
|
|
3805
|
-
tooltip.addEventListener('mouseleave', () => {
|
|
3806
|
-
tooltipLocked = false;
|
|
3807
|
-
scheduleHideTooltip();
|
|
3808
|
-
});
|
|
3809
|
-
|
|
3810
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3811
|
-
// GRAPH VIEW
|
|
3812
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3813
|
-
const svg = d3.select('#graph-svg').attr('width', W).attr('height', H);
|
|
3814
|
-
const g = svg.append('g');
|
|
3815
|
-
const zoom = d3.zoom().scaleExtent([0.05, 10]).on('zoom', e => g.attr('transform', e.transform));
|
|
3816
|
-
svg.call(zoom);
|
|
3817
|
-
svg.call(zoom.transform, d3.zoomIdentity.translate(W/2, H/2).scale(0.7));
|
|
3818
|
-
|
|
3819
|
-
window.zoomIn = () => svg.transition().duration(300).call(zoom.scaleBy, 1.4);
|
|
3820
|
-
window.zoomOut = () => svg.transition().duration(300).call(zoom.scaleBy, 0.7);
|
|
3821
|
-
window.zoomFit = () => {
|
|
3822
|
-
const b = g.node().getBBox(); if (!b.width) return;
|
|
3823
|
-
const s = Math.min(W/(b.width+80), H/(b.height+80))*0.9;
|
|
3824
|
-
svg.transition().duration(500).call(zoom.transform, d3.zoomIdentity.translate(W/2-(b.x+b.width/2)*s, H/2-(b.y+b.height/2)*s).scale(s));
|
|
3825
|
-
};
|
|
3826
|
-
|
|
3827
|
-
// Defs
|
|
3828
|
-
const defs = svg.append('defs');
|
|
3829
|
-
['#30363d','#58a6ff','#3fb950'].forEach((c,i) => {
|
|
3830
|
-
defs.append('marker').attr('id','arrow-'+i).attr('viewBox','0 -4 8 8')
|
|
3831
|
-
.attr('refX',8).attr('refY',0).attr('markerWidth',7).attr('markerHeight',7).attr('orient','auto')
|
|
3832
|
-
.append('path').attr('d','M0,-3.5L8,0L0,3.5Z').attr('fill',c);
|
|
3833
|
-
});
|
|
3834
|
-
|
|
3835
|
-
// Links
|
|
3836
|
-
const link = g.append('g').selectAll('line').data(DATA.links).join('line')
|
|
3837
|
-
.attr('stroke', d => d.type==='type-only'?'#1f3d5c':'#30363d')
|
|
3838
|
-
.attr('stroke-width',1)
|
|
3839
|
-
.attr('stroke-dasharray', d => d.type==='type-only'?'4,3':d.type==='dynamic'?'6,3':null)
|
|
3840
|
-
.attr('marker-end','url(#arrow-0)')
|
|
3841
|
-
.attr('opacity', baseLinkOpacity);
|
|
3842
|
-
|
|
3843
|
-
// Cross-layer links (from layers.json connections)
|
|
3844
|
-
defs.append('marker').attr('id','arrow-cross').attr('viewBox','0 -4 8 8')
|
|
3845
|
-
.attr('refX',8).attr('refY',0).attr('markerWidth',7).attr('markerHeight',7).attr('orient','auto')
|
|
3846
|
-
.append('path').attr('d','M0,-3.5L8,0L0,3.5Z').attr('fill','#f0883e');
|
|
3847
|
-
|
|
3848
|
-
const crossLinkData = (CROSS_EDGES || []).map(e => ({
|
|
3849
|
-
source: e.fromLayer + '/' + e.fromFile,
|
|
3850
|
-
target: e.toLayer + '/' + e.toFile,
|
|
3851
|
-
sourceLayer: e.fromLayer,
|
|
3852
|
-
targetLayer: e.toLayer,
|
|
3853
|
-
type: e.type || 'api-call',
|
|
3854
|
-
label: e.label || e.type || '',
|
|
3855
|
-
})).filter(e => DATA.nodes.some(n => n.id === e.source) && DATA.nodes.some(n => n.id === e.target));
|
|
3856
|
-
|
|
3857
|
-
const crossLinkG = g.append('g');
|
|
3858
|
-
const crossLink = crossLinkG.selectAll('line').data(crossLinkData).join('line')
|
|
3859
|
-
.attr('stroke', '#f0883e')
|
|
3860
|
-
.attr('stroke-width', 2)
|
|
3861
|
-
.attr('stroke-dasharray', '8,4')
|
|
3862
|
-
.attr('marker-end', 'url(#arrow-cross)')
|
|
3863
|
-
.attr('opacity', 0.7);
|
|
3864
|
-
const crossLabel = crossLinkG.selectAll('text').data(crossLinkData).join('text')
|
|
3865
|
-
.text(d => d.label)
|
|
3866
|
-
.attr('font-size', 9)
|
|
3867
|
-
.attr('fill', '#f0883e')
|
|
3868
|
-
.attr('text-anchor', 'middle')
|
|
3869
|
-
.attr('opacity', 0.8)
|
|
3870
|
-
.attr('pointer-events', 'none');
|
|
3871
|
-
|
|
3872
|
-
// Nodes
|
|
3873
|
-
const node = g.append('g').selectAll('g').data(DATA.nodes).join('g')
|
|
3874
|
-
.attr('cursor','pointer')
|
|
3875
|
-
.call(d3.drag().on('start',dragStart).on('drag',dragging).on('end',dragEnd));
|
|
3876
|
-
|
|
3877
|
-
node.append('circle')
|
|
3878
|
-
.attr('r', d => nodeRadius(d) * nodeScale)
|
|
3879
|
-
.attr('fill', nodeColor)
|
|
3880
|
-
.attr('stroke', d => d.deps>=5?'var(--yellow)':nodeColor(d))
|
|
3881
|
-
.attr('stroke-width', d => d.deps>=5?2.5:1.5)
|
|
3882
|
-
.attr('stroke-opacity', d => d.deps>=5?0.8:0.3);
|
|
3883
|
-
|
|
3884
|
-
node.append('text')
|
|
3885
|
-
.text(d => fileName(d.id).replace(/\\.tsx?$/,''))
|
|
3886
|
-
.attr('dx', d => nodeRadius(d)*nodeScale+4)
|
|
3887
|
-
.attr('dy',3.5)
|
|
3888
|
-
.attr('font-size', d => d.dependents>=3?12:10)
|
|
3889
|
-
.attr('font-weight', d => d.dependents>=3?600:400)
|
|
3890
|
-
.attr('fill', d => d.dependents>=3?'var(--text)':'var(--text-dim)')
|
|
3891
|
-
.attr('opacity', d => d.dependents>=1||d.deps>=3?1:0.5)
|
|
3892
|
-
.attr('pointer-events','none');
|
|
3893
|
-
|
|
3894
|
-
// Simulation
|
|
3895
|
-
const simulation = d3.forceSimulation(DATA.nodes)
|
|
3896
|
-
.force('link', d3.forceLink(DATA.links).id(d=>d.id).distance(70).strength(0.25))
|
|
3897
|
-
.force('charge', d3.forceManyBody().strength(-gravityStrength).distanceMax(500))
|
|
3898
|
-
.force('center', d3.forceCenter(0,0))
|
|
3899
|
-
.force('collision', d3.forceCollide().radius(d=>nodeRadius(d)*nodeScale+4))
|
|
3900
|
-
.force('x', d3.forceX(0).strength(0.03))
|
|
3901
|
-
.force('y', d3.forceY(0).strength(0.03))
|
|
3902
|
-
.on('tick', () => {
|
|
3903
|
-
link.each(function(d) {
|
|
3904
|
-
const dx=d.target.x-d.source.x, dy=d.target.y-d.source.y;
|
|
3905
|
-
const dist=Math.sqrt(dx*dx+dy*dy)||1;
|
|
3906
|
-
const rT=nodeRadius(d.target)*nodeScale, rS=nodeRadius(d.source)*nodeScale;
|
|
3907
|
-
d3.select(this)
|
|
3908
|
-
.attr('x1',d.source.x+(dx/dist)*rS).attr('y1',d.source.y+(dy/dist)*rS)
|
|
3909
|
-
.attr('x2',d.target.x-(dx/dist)*rT).attr('y2',d.target.y-(dy/dist)*rT);
|
|
3910
|
-
});
|
|
3911
|
-
node.attr('transform', d=>\`translate(\${d.x},\${d.y})\`);
|
|
3912
|
-
});
|
|
3913
|
-
|
|
3914
|
-
// \u2500\u2500\u2500 Layer convex hulls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3915
|
-
let hullGroup = null;
|
|
3916
|
-
const activeDirs = new Set(DATA.dirs);
|
|
3917
|
-
const dirCounts = {};
|
|
3918
|
-
DATA.nodes.forEach(n => dirCounts[n.dir] = (dirCounts[n.dir] || 0) + 1);
|
|
3919
|
-
var applyLayerFilter = null; // hoisted for dir-filter integration
|
|
3920
|
-
var updateLayerPhysics = null; // hoisted \u2014 updates charge/layer forces without visibility changes
|
|
3921
|
-
|
|
3922
|
-
if (LAYERS && LAYERS.length > 0) {
|
|
3923
|
-
// \u2500\u2500\u2500 Water droplet physics: intra-layer cohesion + inter-layer separation \u2500\u2500\u2500
|
|
3924
|
-
const allLayerCount = LAYERS.length;
|
|
3925
|
-
const allBaseRadius = Math.max(60, Math.min(W, H) * 0.04 * Math.sqrt(allLayerCount));
|
|
3926
|
-
// Pre-compute full-circle positions for all layers (used when no filter)
|
|
3927
|
-
const allLayerCenters = {};
|
|
3928
|
-
LAYERS.forEach((l, idx) => {
|
|
3929
|
-
const angle = (2 * Math.PI * idx) / allLayerCount - Math.PI / 2;
|
|
3930
|
-
allLayerCenters[l.name] = { x: Math.cos(angle) * allBaseRadius, y: Math.sin(angle) * allBaseRadius };
|
|
3931
|
-
});
|
|
3932
|
-
|
|
3933
|
-
// Dynamic center calculation: compact when multi-selecting, full spread when all
|
|
3934
|
-
function getLayerCenters() {
|
|
3935
|
-
if (activeLayers.size <= 1) return allLayerCenters; // 0 = all, 1 = single (centered)
|
|
3936
|
-
// Multi-select: arrange only selected layers compactly on a smaller circle
|
|
3937
|
-
const selected = LAYERS.filter(l => activeLayers.has(l.name));
|
|
3938
|
-
const count = selected.length;
|
|
3939
|
-
const compactRadius = Math.max(40, Math.min(W, H) * 0.03 * Math.sqrt(count));
|
|
3940
|
-
const centers = {};
|
|
3941
|
-
selected.forEach((l, idx) => {
|
|
3942
|
-
const angle = (2 * Math.PI * idx) / count - Math.PI / 2;
|
|
3943
|
-
centers[l.name] = { x: Math.cos(angle) * compactRadius, y: Math.sin(angle) * compactRadius };
|
|
3944
|
-
});
|
|
3945
|
-
return centers;
|
|
3946
|
-
}
|
|
3947
|
-
|
|
3948
|
-
// Replace default centering forces with per-layer positioning
|
|
3949
|
-
const layerStrength = layerGravity / 100;
|
|
3950
|
-
simulation.force('x', null).force('y', null).force('center', null);
|
|
3951
|
-
simulation.force('layerX', d3.forceX(d => allLayerCenters[d.layer]?.x || 0).strength(d => d.layer ? layerStrength : 0.03));
|
|
3952
|
-
simulation.force('layerY', d3.forceY(d => allLayerCenters[d.layer]?.y || 0).strength(d => d.layer ? layerStrength : 0.03));
|
|
3953
|
-
|
|
3954
|
-
// Custom clustering force \u2014 surface tension pulling nodes toward their layer centroid
|
|
3955
|
-
function clusterForce() {
|
|
3956
|
-
let nodes;
|
|
3957
|
-
function force(alpha) {
|
|
3958
|
-
const centroids = {};
|
|
3959
|
-
const counts = {};
|
|
3960
|
-
nodes.forEach(n => {
|
|
3961
|
-
if (!n.layer) return;
|
|
3962
|
-
if (!centroids[n.layer]) { centroids[n.layer] = {x: 0, y: 0}; counts[n.layer] = 0; }
|
|
3963
|
-
centroids[n.layer].x += n.x;
|
|
3964
|
-
centroids[n.layer].y += n.y;
|
|
3965
|
-
counts[n.layer]++;
|
|
3966
|
-
});
|
|
3967
|
-
Object.keys(centroids).forEach(k => {
|
|
3968
|
-
centroids[k].x /= counts[k];
|
|
3969
|
-
centroids[k].y /= counts[k];
|
|
3970
|
-
});
|
|
3971
|
-
// Pull each node toward its layer centroid (surface tension)
|
|
3972
|
-
const strength = 0.2;
|
|
3973
|
-
nodes.forEach(n => {
|
|
3974
|
-
if (!n.layer || !centroids[n.layer]) return;
|
|
3975
|
-
n.vx += (centroids[n.layer].x - n.x) * alpha * strength;
|
|
3976
|
-
n.vy += (centroids[n.layer].y - n.y) * alpha * strength;
|
|
3977
|
-
});
|
|
3978
|
-
}
|
|
3979
|
-
force.initialize = (n) => { nodes = n; };
|
|
3980
|
-
return force;
|
|
3981
|
-
}
|
|
3982
|
-
simulation.force('cluster', clusterForce());
|
|
3983
|
-
|
|
3984
|
-
// Boost link strength for intra-layer edges (tighter connections within a layer)
|
|
3985
|
-
simulation.force('link').strength(l => {
|
|
3986
|
-
const sLayer = (l.source.layer ?? l.source);
|
|
3987
|
-
const tLayer = (l.target.layer ?? l.target);
|
|
3988
|
-
return sLayer === tLayer ? 0.4 : 0.1;
|
|
3989
|
-
});
|
|
3990
|
-
|
|
3991
|
-
hullGroup = g.insert('g', ':first-child');
|
|
3992
|
-
|
|
3993
|
-
function updateHulls() {
|
|
3994
|
-
if (!hullGroup) return;
|
|
3995
|
-
hullGroup.selectAll('*').remove();
|
|
3996
|
-
// Show hulls always (filter to selected layers when focused)
|
|
3997
|
-
|
|
3998
|
-
LAYERS.forEach(layer => {
|
|
3999
|
-
if (activeLayers.size > 0 && !activeLayers.has(layer.name)) return;
|
|
4000
|
-
const layerNodes = DATA.nodes.filter(n => n.layer === layer.name);
|
|
4001
|
-
if (layerNodes.length === 0) return;
|
|
4002
|
-
|
|
4003
|
-
const points = [];
|
|
4004
|
-
layerNodes.forEach(n => {
|
|
4005
|
-
if (n.x == null || n.y == null) return;
|
|
4006
|
-
const r = nodeRadius(n) * nodeScale + 30;
|
|
4007
|
-
// Add expanded points for a nicer hull shape
|
|
4008
|
-
for (let a = 0; a < Math.PI * 2; a += Math.PI / 4) {
|
|
4009
|
-
points.push([n.x + Math.cos(a) * r, n.y + Math.sin(a) * r]);
|
|
4010
|
-
}
|
|
4011
|
-
});
|
|
4012
|
-
|
|
4013
|
-
if (points.length < 3) {
|
|
4014
|
-
// Fallback: circle for 1-2 nodes
|
|
4015
|
-
const cx = layerNodes.reduce((s, n) => s + (n.x || 0), 0) / layerNodes.length;
|
|
4016
|
-
const cy = layerNodes.reduce((s, n) => s + (n.y || 0), 0) / layerNodes.length;
|
|
4017
|
-
const maxR = Math.max(60, ...layerNodes.map(n => {
|
|
4018
|
-
const dx = (n.x || 0) - cx, dy = (n.y || 0) - cy;
|
|
4019
|
-
return Math.sqrt(dx*dx + dy*dy) + nodeRadius(n) * nodeScale + 30;
|
|
4020
|
-
}));
|
|
4021
|
-
hullGroup.append('circle')
|
|
4022
|
-
.attr('cx', cx).attr('cy', cy).attr('r', maxR)
|
|
4023
|
-
.attr('class', 'layer-hull')
|
|
4024
|
-
.attr('fill', layer.color).attr('stroke', layer.color);
|
|
4025
|
-
hullGroup.append('text')
|
|
4026
|
-
.attr('class', 'layer-hull-label')
|
|
4027
|
-
.attr('x', cx).attr('y', cy - maxR - 8)
|
|
4028
|
-
.attr('text-anchor', 'middle')
|
|
4029
|
-
.attr('fill', layer.color)
|
|
4030
|
-
.text(layer.name);
|
|
4031
|
-
return;
|
|
4032
|
-
}
|
|
4033
|
-
|
|
4034
|
-
const hull = d3.polygonHull(points);
|
|
4035
|
-
if (!hull) return;
|
|
4036
|
-
|
|
4037
|
-
// Smooth the hull with a cardinal closed curve
|
|
4038
|
-
hullGroup.append('path')
|
|
4039
|
-
.attr('class', 'layer-hull')
|
|
4040
|
-
.attr('d', d3.line().curve(d3.curveCatmullRomClosed.alpha(0.5))(hull))
|
|
4041
|
-
.attr('fill', layer.color).attr('stroke', layer.color);
|
|
4042
|
-
|
|
4043
|
-
// Label at the top of the hull
|
|
4044
|
-
const topPt = hull.reduce((best, p) => p[1] < best[1] ? p : best, hull[0]);
|
|
4045
|
-
hullGroup.append('text')
|
|
4046
|
-
.attr('class', 'layer-hull-label')
|
|
4047
|
-
.attr('x', topPt[0]).attr('y', topPt[1] - 10)
|
|
4048
|
-
.attr('text-anchor', 'middle')
|
|
4049
|
-
.attr('fill', layer.color)
|
|
4050
|
-
.text(layer.name);
|
|
4051
|
-
});
|
|
4052
|
-
}
|
|
4053
|
-
|
|
4054
|
-
// Update hulls + cross-layer links on each tick
|
|
4055
|
-
simulation.on('tick', () => {
|
|
4056
|
-
// Regular links
|
|
4057
|
-
link.each(function(d) {
|
|
4058
|
-
const dx=d.target.x-d.source.x, dy=d.target.y-d.source.y;
|
|
4059
|
-
const dist=Math.sqrt(dx*dx+dy*dy)||1;
|
|
4060
|
-
const rT=nodeRadius(d.target)*nodeScale, rS=nodeRadius(d.source)*nodeScale;
|
|
4061
|
-
d3.select(this)
|
|
4062
|
-
.attr('x1',d.source.x+(dx/dist)*rS).attr('y1',d.source.y+(dy/dist)*rS)
|
|
4063
|
-
.attr('x2',d.target.x-(dx/dist)*rT).attr('y2',d.target.y-(dy/dist)*rT);
|
|
4064
|
-
});
|
|
4065
|
-
node.attr('transform', d=>\`translate(\${d.x},\${d.y})\`);
|
|
4066
|
-
// Cross-layer links \u2014 resolve node positions by ID
|
|
4067
|
-
if (crossLinkData.length > 0) {
|
|
4068
|
-
const nodeById = {};
|
|
4069
|
-
DATA.nodes.forEach(n => { nodeById[n.id] = n; });
|
|
4070
|
-
crossLink.each(function(d) {
|
|
4071
|
-
const sN = nodeById[d.source], tN = nodeById[d.target];
|
|
4072
|
-
if (!sN || !tN) return;
|
|
4073
|
-
const dx = tN.x - sN.x, dy = tN.y - sN.y;
|
|
4074
|
-
const dist = Math.sqrt(dx*dx + dy*dy) || 1;
|
|
4075
|
-
const rS = nodeRadius(sN) * nodeScale, rT = nodeRadius(tN) * nodeScale;
|
|
4076
|
-
d3.select(this)
|
|
4077
|
-
.attr('x1', sN.x + (dx/dist)*rS).attr('y1', sN.y + (dy/dist)*rS)
|
|
4078
|
-
.attr('x2', tN.x - (dx/dist)*rT).attr('y2', tN.y - (dy/dist)*rT);
|
|
4079
|
-
});
|
|
4080
|
-
crossLabel.each(function(d) {
|
|
4081
|
-
const sN = nodeById[d.source], tN = nodeById[d.target];
|
|
4082
|
-
if (!sN || !tN) return;
|
|
4083
|
-
d3.select(this).attr('x', (sN.x + tN.x) / 2).attr('y', (sN.y + tN.y) / 2 - 6);
|
|
4084
|
-
});
|
|
4085
|
-
}
|
|
4086
|
-
updateHulls();
|
|
4087
|
-
});
|
|
4088
|
-
|
|
4089
|
-
// \u2500\u2500\u2500 Layer legend \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4090
|
-
const layerLegend = document.getElementById('layer-legend');
|
|
4091
|
-
LAYERS.forEach(layer => {
|
|
4092
|
-
const item = document.createElement('div');
|
|
4093
|
-
item.className = 'legend-item';
|
|
4094
|
-
item.innerHTML = '<div class="legend-dot" style="background:' + esc(layer.color) + '"></div> ' + esc(layer.name);
|
|
4095
|
-
layerLegend.appendChild(item);
|
|
4096
|
-
});
|
|
4097
|
-
// Cross-layer edge legend
|
|
4098
|
-
if (CROSS_EDGES && CROSS_EDGES.length > 0) {
|
|
4099
|
-
const crossItem = document.createElement('div');
|
|
4100
|
-
crossItem.className = 'legend-item';
|
|
4101
|
-
crossItem.innerHTML = '<span style="color:#f0883e;font-size:11px">- - \u2192</span> Cross-layer link';
|
|
4102
|
-
layerLegend.appendChild(crossItem);
|
|
4103
|
-
}
|
|
4104
|
-
// Add separator
|
|
4105
|
-
const sep = document.createElement('hr');
|
|
4106
|
-
sep.style.cssText = 'border:none;border-top:1px solid var(--border);margin:6px 0;';
|
|
4107
|
-
layerLegend.appendChild(sep);
|
|
4108
|
-
|
|
4109
|
-
// \u2500\u2500\u2500 Layer tabs (multi-select toggles in tab bar) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4110
|
-
const layerTabsEl = document.getElementById('layer-tabs');
|
|
4111
|
-
const allTab = document.createElement('div');
|
|
4112
|
-
allTab.className = 'layer-tab active';
|
|
4113
|
-
allTab.textContent = 'All';
|
|
4114
|
-
allTab.onclick = () => {
|
|
4115
|
-
activeLayers.clear();
|
|
4116
|
-
syncLayerTabUI();
|
|
4117
|
-
applyLayerFilter();
|
|
4118
|
-
if (hierBuilt && hierSyncFromTab) { hierSyncFromTab(); hierRelayout(); }
|
|
4119
|
-
};
|
|
4120
|
-
layerTabsEl.appendChild(allTab);
|
|
4121
|
-
|
|
4122
|
-
LAYERS.forEach(layer => {
|
|
4123
|
-
const tab = document.createElement('div');
|
|
4124
|
-
tab.className = 'layer-tab';
|
|
4125
|
-
tab.dataset.layer = layer.name;
|
|
4126
|
-
tab.innerHTML = '<div class="lt-dot" style="background:' + esc(layer.color) + '"></div>' + esc(layer.name);
|
|
4127
|
-
tab.onclick = (e) => {
|
|
4128
|
-
if (e.shiftKey) {
|
|
4129
|
-
// Shift+click: solo this layer
|
|
4130
|
-
activeLayers.clear();
|
|
4131
|
-
activeLayers.add(layer.name);
|
|
4132
|
-
} else {
|
|
4133
|
-
// Toggle
|
|
4134
|
-
if (activeLayers.has(layer.name)) activeLayers.delete(layer.name);
|
|
4135
|
-
else activeLayers.add(layer.name);
|
|
4136
|
-
}
|
|
4137
|
-
syncLayerTabUI();
|
|
4138
|
-
applyLayerFilter();
|
|
4139
|
-
if (hierBuilt && hierSyncFromTab) { hierSyncFromTab(); hierRelayout(); }
|
|
4140
|
-
};
|
|
4141
|
-
layerTabsEl.appendChild(tab);
|
|
4142
|
-
});
|
|
4143
|
-
|
|
4144
|
-
function syncLayerTabUI() {
|
|
4145
|
-
allTab.classList.toggle('active', activeLayers.size === 0);
|
|
4146
|
-
layerTabsEl.querySelectorAll('.layer-tab[data-layer]').forEach(t => {
|
|
4147
|
-
t.classList.toggle('active', activeLayers.has(t.dataset.layer));
|
|
4148
|
-
});
|
|
4149
|
-
// Also sync the filter bar layer pills
|
|
4150
|
-
layerRowEl.querySelectorAll('.layer-pill[data-layer]').forEach(p => {
|
|
4151
|
-
p.classList.toggle('active', activeLayers.has(p.dataset.layer));
|
|
4152
|
-
});
|
|
4153
|
-
}
|
|
4154
|
-
|
|
4155
|
-
applyLayerFilter = function() {
|
|
4156
|
-
const isSingleLayer = activeLayers.size === 1;
|
|
4157
|
-
const hasLayerFilter = activeLayers.size > 0;
|
|
4158
|
-
node.attr('display', d => {
|
|
4159
|
-
if (!activeDirs.has(d.dir)) return 'none';
|
|
4160
|
-
if (hasLayerFilter && !activeLayers.has(d.layer)) return 'none';
|
|
4161
|
-
return null;
|
|
4162
|
-
});
|
|
4163
|
-
link.attr('display', l => {
|
|
4164
|
-
const s = l.source.id ?? l.source, t = l.target.id ?? l.target;
|
|
4165
|
-
const sN = DATA.nodes.find(n => n.id === s), tN = DATA.nodes.find(n => n.id === t);
|
|
4166
|
-
if (!sN || !tN) return 'none';
|
|
4167
|
-
if (!activeDirs.has(sN.dir) || !activeDirs.has(tN.dir)) return 'none';
|
|
4168
|
-
if (hasLayerFilter && (!activeLayers.has(sN.layer) || !activeLayers.has(tN.layer))) return 'none';
|
|
4169
|
-
return null;
|
|
4170
|
-
});
|
|
4171
|
-
// Refresh node colors: single-layer = dir-based, multi-layer = layer-based
|
|
4172
|
-
node.select('circle')
|
|
4173
|
-
.attr('fill', nodeColor)
|
|
4174
|
-
.attr('stroke', d => d.deps >= 5 ? 'var(--yellow)' : nodeColor(d));
|
|
4175
|
-
// Cross-layer links: respect user toggle + layer filter
|
|
4176
|
-
if (typeof crossLink !== 'undefined') {
|
|
4177
|
-
if (!crossLinksUserEnabled || isSingleLayer) {
|
|
4178
|
-
crossLink.attr('display', 'none');
|
|
4179
|
-
crossLabel.attr('display', 'none');
|
|
4180
|
-
} else if (hasLayerFilter) {
|
|
4181
|
-
crossLink.attr('display', d => (activeLayers.has(d.sourceLayer) && activeLayers.has(d.targetLayer)) ? null : 'none');
|
|
4182
|
-
crossLabel.attr('display', d => (activeLayers.has(d.sourceLayer) && activeLayers.has(d.targetLayer)) ? null : 'none');
|
|
4183
|
-
} else {
|
|
4184
|
-
crossLink.attr('display', null);
|
|
4185
|
-
crossLabel.attr('display', null);
|
|
4186
|
-
}
|
|
4187
|
-
}
|
|
4188
|
-
// Update stats
|
|
4189
|
-
const visibleNodes = DATA.nodes.filter(d => {
|
|
4190
|
-
if (!activeDirs.has(d.dir)) return false;
|
|
4191
|
-
if (hasLayerFilter && !activeLayers.has(d.layer)) return false;
|
|
4192
|
-
return true;
|
|
4193
|
-
});
|
|
4194
|
-
const visibleIds = new Set(visibleNodes.map(n => n.id));
|
|
4195
|
-
const visibleEdges = DATA.links.filter(l => {
|
|
4196
|
-
const s = l.source.id ?? l.source, t = l.target.id ?? l.target;
|
|
4197
|
-
return visibleIds.has(s) && visibleIds.has(t);
|
|
4198
|
-
});
|
|
4199
|
-
document.getElementById('s-files').textContent = visibleNodes.length;
|
|
4200
|
-
document.getElementById('s-edges').textContent = visibleEdges.length;
|
|
4201
|
-
const visCirc = DATA.circularFiles.filter(f => visibleIds.has(f));
|
|
4202
|
-
document.getElementById('s-circular').textContent = visCirc.length;
|
|
4203
|
-
updateHulls();
|
|
4204
|
-
// Delegate physics update and zoom to fit
|
|
4205
|
-
updateLayerPhysics();
|
|
4206
|
-
simulation.alpha(0.6).restart();
|
|
4207
|
-
setTimeout(() => zoomFit(), 600);
|
|
4208
|
-
}
|
|
4209
|
-
|
|
4210
|
-
// Separated physics update: handles charge/layer forces based on filter state.
|
|
4211
|
-
// Called by applyLayerFilter (with zoomFit), setGravity, setLayerGravity (without zoomFit).
|
|
4212
|
-
updateLayerPhysics = function() {
|
|
4213
|
-
const isSingleLayer = activeLayers.size === 1;
|
|
4214
|
-
const lStrength = layerGravity / 100;
|
|
4215
|
-
if (isSingleLayer) {
|
|
4216
|
-
simulation.force('charge', d3.forceManyBody().strength(-gravityStrength * 3).distanceMax(800));
|
|
4217
|
-
simulation.force('layerX', d3.forceX(0).strength(0.03));
|
|
4218
|
-
simulation.force('layerY', d3.forceY(0).strength(0.03));
|
|
4219
|
-
} else {
|
|
4220
|
-
const centers = getLayerCenters();
|
|
4221
|
-
simulation.force('charge', d3.forceManyBody().strength(-gravityStrength).distanceMax(500));
|
|
4222
|
-
simulation.force('layerX', d3.forceX(d => centers[d.layer]?.x || 0).strength(d => d.layer ? lStrength : 0.03));
|
|
4223
|
-
simulation.force('layerY', d3.forceY(d => centers[d.layer]?.y || 0).strength(d => d.layer ? lStrength : 0.03));
|
|
4224
|
-
}
|
|
4225
|
-
}
|
|
4226
|
-
|
|
4227
|
-
// \u2500\u2500\u2500 Layer filter pills (new grouped bar) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4228
|
-
const layerRowEl = document.getElementById('filter-layer-row');
|
|
4229
|
-
const dirPanelEl = document.getElementById('filter-dir-panel');
|
|
4230
|
-
|
|
4231
|
-
// Dir toggle button
|
|
4232
|
-
const dirToggle = document.createElement('div');
|
|
4233
|
-
dirToggle.id = 'filter-dir-toggle';
|
|
4234
|
-
dirToggle.textContent = '\u25B8 Dirs';
|
|
4235
|
-
dirToggle.onclick = () => {
|
|
4236
|
-
dirToggle.classList.toggle('open');
|
|
4237
|
-
dirPanelEl.classList.toggle('open');
|
|
4238
|
-
dirToggle.textContent = dirPanelEl.classList.contains('open') ? '\u25BE Dirs' : '\u25B8 Dirs';
|
|
4239
|
-
};
|
|
4240
|
-
layerRowEl.appendChild(dirToggle);
|
|
4241
|
-
|
|
4242
|
-
// Cross-layer link toggle (in settings sidebar)
|
|
4243
|
-
let crossLinksUserEnabled = true;
|
|
4244
|
-
if (crossLinkData.length > 0) {
|
|
4245
|
-
document.getElementById('cross-layer-setting').style.display = '';
|
|
4246
|
-
window.toggleCrossLinks = () => {
|
|
4247
|
-
crossLinksUserEnabled = !crossLinksUserEnabled;
|
|
4248
|
-
const btn = document.getElementById('cross-link-toggle');
|
|
4249
|
-
btn.textContent = crossLinksUserEnabled ? 'ON' : 'OFF';
|
|
4250
|
-
btn.classList.toggle('active', crossLinksUserEnabled);
|
|
4251
|
-
applyLayerFilter();
|
|
4252
|
-
};
|
|
4253
|
-
}
|
|
4254
|
-
|
|
4255
|
-
LAYERS.forEach(layer => {
|
|
4256
|
-
const layerNodes = DATA.nodes.filter(n => n.layer === layer.name);
|
|
4257
|
-
const pill = document.createElement('div');
|
|
4258
|
-
pill.className = 'layer-pill';
|
|
4259
|
-
pill.dataset.layer = layer.name;
|
|
4260
|
-
pill.innerHTML = '<div class="lp-dot" style="background:' + esc(layer.color) + '"></div>' + esc(layer.name) + ' <span class="lp-count">' + layerNodes.length + '</span>';
|
|
4261
|
-
pill.onclick = () => {
|
|
4262
|
-
if (activeLayers.has(layer.name)) activeLayers.delete(layer.name);
|
|
4263
|
-
else activeLayers.add(layer.name);
|
|
4264
|
-
syncLayerTabUI();
|
|
4265
|
-
applyLayerFilter();
|
|
4266
|
-
};
|
|
4267
|
-
pill.onmouseenter = () => {
|
|
4268
|
-
if (pinnedNode) return;
|
|
4269
|
-
node.select('circle').transition().duration(120).attr('opacity', d => d.layer === layer.name ? 1 : 0.1);
|
|
4270
|
-
node.select('text').transition().duration(120).attr('opacity', d => d.layer === layer.name ? 1 : 0.05);
|
|
4271
|
-
};
|
|
4272
|
-
pill.onmouseleave = () => {
|
|
4273
|
-
if (pinnedNode) return;
|
|
4274
|
-
node.select('circle').transition().duration(150).attr('opacity', 1);
|
|
4275
|
-
node.select('text').transition().duration(150).attr('opacity', d => d.dependents >= 1 || d.deps >= 3 ? 1 : 0.5);
|
|
4276
|
-
};
|
|
4277
|
-
layerRowEl.appendChild(pill);
|
|
4278
|
-
|
|
4279
|
-
// Build dir group in panel for this layer
|
|
4280
|
-
const layerDirs = [...new Set(layerNodes.map(n => n.dir))].sort();
|
|
4281
|
-
if (layerDirs.length > 0) {
|
|
4282
|
-
const group = document.createElement('div');
|
|
4283
|
-
group.className = 'dir-group';
|
|
4284
|
-
const label = document.createElement('div');
|
|
4285
|
-
label.className = 'dir-group-label';
|
|
4286
|
-
label.innerHTML = '<div class="dg-dot" style="background:' + esc(layer.color) + '"></div>' + esc(layer.name);
|
|
4287
|
-
group.appendChild(label);
|
|
4288
|
-
const pillsWrap = document.createElement('div');
|
|
4289
|
-
pillsWrap.className = 'dir-group-pills';
|
|
4290
|
-
layerDirs.forEach(dir => {
|
|
4291
|
-
const dp = document.createElement('div');
|
|
4292
|
-
dp.className = 'filter-pill active';
|
|
4293
|
-
const shortDir = dir.includes('/') ? dir.substring(dir.indexOf('/') + 1) : dir;
|
|
4294
|
-
dp.innerHTML = '<div class="pill-dot" style="background:' + dirColor(dir) + '"></div>' + esc(shortDir || '.') + ' <span class="pill-count">' + (dirCounts[dir] || 0) + '</span>';
|
|
4295
|
-
dp.onclick = () => {
|
|
4296
|
-
if (activeDirs.has(dir)) { activeDirs.delete(dir); dp.classList.remove('active'); }
|
|
4297
|
-
else { activeDirs.add(dir); dp.classList.add('active'); }
|
|
4298
|
-
applyLayerFilter();
|
|
4299
|
-
};
|
|
4300
|
-
pillsWrap.appendChild(dp);
|
|
4301
|
-
});
|
|
4302
|
-
group.appendChild(pillsWrap);
|
|
4303
|
-
dirPanelEl.appendChild(group);
|
|
4304
|
-
}
|
|
4305
|
-
});
|
|
4306
|
-
|
|
4307
|
-
// Override applyFilter to respect layers
|
|
4308
|
-
window._origApplyFilter = applyFilter;
|
|
4309
|
-
}
|
|
4310
|
-
|
|
4311
|
-
setTimeout(()=>zoomFit(), 1500);
|
|
4312
|
-
|
|
4313
|
-
// Restore saved settings \u2014 phase 2: apply to graph elements now that they exist
|
|
4314
|
-
if (_savedSettings) {
|
|
4315
|
-
if (_savedSettings.fontSize) setFontSize(_savedSettings.fontSize);
|
|
4316
|
-
}
|
|
4317
|
-
|
|
4318
|
-
// \u2500\u2500\u2500 Highlight helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4319
|
-
let pinnedNode = null;
|
|
4320
|
-
|
|
4321
|
-
function highlightNode(d) {
|
|
4322
|
-
const conn = new Set([d.id]);
|
|
4323
|
-
DATA.links.forEach(l => { const s=l.source.id??l.source,t=l.target.id??l.target; if(s===d.id)conn.add(t); if(t===d.id)conn.add(s); });
|
|
4324
|
-
node.select('circle').transition().duration(150).attr('opacity',n=>conn.has(n.id)?1:0.1);
|
|
4325
|
-
node.select('text').transition().duration(150).attr('opacity',n=>conn.has(n.id)?1:0.05);
|
|
4326
|
-
link.transition().duration(150)
|
|
4327
|
-
.attr('opacity',l=>{const s=l.source.id??l.source,t=l.target.id??l.target;return s===d.id||t===d.id?0.9:0.03;})
|
|
4328
|
-
.attr('stroke',l=>{const s=l.source.id??l.source,t=l.target.id??l.target;if(s===d.id)return'#58a6ff';if(t===d.id)return'#3fb950';return l.type==='type-only'?'#1f3d5c':'#30363d';})
|
|
4329
|
-
.attr('stroke-width',l=>{const s=l.source.id??l.source,t=l.target.id??l.target;return s===d.id||t===d.id?2:1;})
|
|
4330
|
-
.attr('marker-end',l=>{const s=l.source.id??l.source,t=l.target.id??l.target;if(s===d.id)return'url(#arrow-1)';if(t===d.id)return'url(#arrow-2)';return'url(#arrow-0)';});
|
|
4331
|
-
}
|
|
4332
|
-
|
|
4333
|
-
function resetGraphHighlight() {
|
|
4334
|
-
pinnedNode = null;
|
|
4335
|
-
node.select('circle').transition().duration(200).attr('opacity',1);
|
|
4336
|
-
node.select('text').transition().duration(200).attr('opacity',d=>d.dependents>=1||d.deps>=3?1:0.5);
|
|
4337
|
-
link.transition().duration(200)
|
|
4338
|
-
.attr('opacity',baseLinkOpacity)
|
|
4339
|
-
.attr('stroke',d=>d.type==='type-only'?'#1f3d5c':'#30363d')
|
|
4340
|
-
.attr('stroke-width',1).attr('marker-end','url(#arrow-0)');
|
|
4341
|
-
}
|
|
4342
|
-
|
|
4343
|
-
// \u2500\u2500\u2500 Hover \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4344
|
-
node.on('mouseover', (e,d) => {
|
|
4345
|
-
showTooltip(e,d);
|
|
4346
|
-
if (!pinnedNode) highlightNode(d);
|
|
4347
|
-
})
|
|
4348
|
-
.on('mousemove', e=>positionTooltip(e))
|
|
4349
|
-
.on('mouseout', () => { scheduleHideTooltip(); if (!pinnedNode) { /* highlight resets via scheduleHideTooltip */ } });
|
|
4350
|
-
|
|
4351
|
-
// \u2500\u2500\u2500 Click: pin highlight + detail panel \u2500\u2500\u2500\u2500\u2500
|
|
4352
|
-
node.on('click', (e,d) => {
|
|
4353
|
-
e.stopPropagation();
|
|
4354
|
-
pinnedNode = d;
|
|
4355
|
-
highlightNode(d);
|
|
4356
|
-
showDetail(d);
|
|
4357
|
-
});
|
|
4358
|
-
svg.on('click', () => {
|
|
4359
|
-
resetGraphHighlight();
|
|
4360
|
-
tooltip.style.display = 'none';
|
|
4361
|
-
tooltipLocked = false;
|
|
4362
|
-
closeDetail();
|
|
4363
|
-
});
|
|
4364
|
-
|
|
4365
|
-
function showDetail(d) {
|
|
4366
|
-
const p=document.getElementById('detail');
|
|
4367
|
-
document.getElementById('d-name').textContent=d.id;
|
|
4368
|
-
document.getElementById('d-meta').innerHTML=i('detail.dir')+': '+esc(d.dir)+'<br>'+i('detail.dependencies')+': '+d.deps+' \\u00b7 '+i('detail.dependents')+': '+d.dependents;
|
|
4369
|
-
const deptL=document.getElementById('d-dependents'), depsL=document.getElementById('d-deps');
|
|
4370
|
-
deptL.innerHTML=(d.dependentsList||[]).map(x=>'<li data-focus="'+esc(x)+'">\\u2190 '+esc(x)+'</li>').join('')||'<li style="color:var(--text-muted)">'+i('detail.none')+'</li>';
|
|
4371
|
-
depsL.innerHTML=(d.dependencies||[]).map(x=>'<li data-focus="'+esc(x)+'">\\u2192 '+esc(x)+'</li>').join('')||'<li style="color:var(--text-muted)">'+i('detail.none')+'</li>';
|
|
4372
|
-
p.classList.add('open');
|
|
4373
|
-
}
|
|
4374
|
-
// Event delegation for detail panel list items (avoids inline onclick)
|
|
4375
|
-
document.getElementById('d-dependents').addEventListener('click', function(e) { var li=e.target.closest('li[data-focus]'); if(li) focusNode(li.dataset.focus); });
|
|
4376
|
-
document.getElementById('d-deps').addEventListener('click', function(e) { var li=e.target.closest('li[data-focus]'); if(li) focusNode(li.dataset.focus); });
|
|
4377
|
-
window.closeDetail=()=>document.getElementById('detail').classList.remove('open');
|
|
4378
|
-
window.focusNode=(id)=>{
|
|
4379
|
-
const n=DATA.nodes.find(x=>x.id===id); if(!n)return; showDetail(n);
|
|
4380
|
-
svg.transition().duration(500).call(zoom.transform,d3.zoomIdentity.translate(W/2-n.x*1.5,H/2-n.y*1.5).scale(1.5));
|
|
4381
|
-
};
|
|
4382
|
-
|
|
4383
|
-
// Drag
|
|
4384
|
-
function dragStart(e,d){if(!e.active)simulation.alphaTarget(0.3).restart();d.fx=d.x;d.fy=d.y;}
|
|
4385
|
-
function dragging(e,d){d.fx=e.x;d.fy=e.y;}
|
|
4386
|
-
function dragEnd(e,d){if(!e.active)simulation.alphaTarget(0);}
|
|
4387
|
-
|
|
4388
|
-
// \u2500\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
|
|
4389
|
-
const searchInput=document.getElementById('search');
|
|
4390
|
-
document.addEventListener('keydown',e=>{
|
|
4391
|
-
if(e.key==='/'&&document.activeElement!==searchInput){e.preventDefault();searchInput.focus();}
|
|
4392
|
-
if(e.key==='Escape'){searchInput.value='';searchInput.blur();resetGraphHighlight();}
|
|
4393
|
-
});
|
|
4394
|
-
searchInput.addEventListener('input',e=>{
|
|
4395
|
-
const q=e.target.value.toLowerCase();
|
|
4396
|
-
if(!q){resetGraphHighlight();return;}
|
|
4397
|
-
node.select('circle').attr('opacity',d=>d.id.toLowerCase().includes(q)?1:0.06);
|
|
4398
|
-
node.select('text').attr('opacity',d=>d.id.toLowerCase().includes(q)?1:0.04);
|
|
4399
|
-
link.attr('opacity',0.03);
|
|
4400
|
-
});
|
|
4401
|
-
|
|
4402
|
-
// \u2500\u2500\u2500 Filters (click=toggle, hover=highlight nodes) \u2500\u2500
|
|
4403
|
-
if (!LAYERS) {
|
|
4404
|
-
// Non-layer mode: flat pills in filter-layer-row
|
|
4405
|
-
const filterRowEl=document.getElementById('filter-layer-row');
|
|
4406
|
-
DATA.dirs.forEach(dir=>{
|
|
4407
|
-
const pill=document.createElement('div');
|
|
4408
|
-
pill.className='filter-pill active';
|
|
4409
|
-
pill.innerHTML='<div class="pill-dot" style="background:'+dirColor(dir)+'"></div>'+esc(dir||'.')+' <span class="pill-count">'+dirCounts[dir]+'</span>';
|
|
4410
|
-
pill.onclick=()=>{
|
|
4411
|
-
if(activeDirs.has(dir)){activeDirs.delete(dir);pill.classList.remove('active');}
|
|
4412
|
-
else{activeDirs.add(dir);pill.classList.add('active');}
|
|
4413
|
-
applyFilter();
|
|
4414
|
-
};
|
|
4415
|
-
pill.onmouseenter=()=>{
|
|
4416
|
-
if(pinnedNode)return;
|
|
4417
|
-
node.select('circle').transition().duration(120).attr('opacity',d=>d.dir===dir?1:0.1);
|
|
4418
|
-
node.select('text').transition().duration(120).attr('opacity',d=>d.dir===dir?1:0.05);
|
|
4419
|
-
};
|
|
4420
|
-
pill.onmouseleave=()=>{
|
|
4421
|
-
if(pinnedNode)return;
|
|
4422
|
-
node.select('circle').transition().duration(150).attr('opacity',1);
|
|
4423
|
-
node.select('text').transition().duration(150).attr('opacity',d=>d.dependents>=1||d.deps>=3?1:0.5);
|
|
4424
|
-
};
|
|
4425
|
-
filterRowEl.appendChild(pill);
|
|
4426
|
-
});
|
|
4427
|
-
}
|
|
4428
|
-
function applyFilter(){
|
|
4429
|
-
if (LAYERS) {
|
|
4430
|
-
// Delegate to layer-aware filter
|
|
4431
|
-
if (typeof applyLayerFilter === 'function') { applyLayerFilter(); return; }
|
|
4432
|
-
}
|
|
4433
|
-
node.attr('display',d=>activeDirs.has(d.dir)?null:'none');
|
|
4434
|
-
link.attr('display',l=>{
|
|
4435
|
-
const s=l.source.id??l.source,t=l.target.id??l.target;
|
|
4436
|
-
const sD=DATA.nodes.find(n=>n.id===s)?.dir,tD=DATA.nodes.find(n=>n.id===t)?.dir;
|
|
4437
|
-
return activeDirs.has(sD)&&activeDirs.has(tD)?null:'none';
|
|
4438
|
-
});
|
|
4439
|
-
}
|
|
4440
|
-
|
|
4441
|
-
// \u2500\u2500\u2500 Impact simulation mode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4442
|
-
let impactMode=false;
|
|
4443
|
-
const impactBadge=document.getElementById('impact-badge');
|
|
4444
|
-
window.toggleImpactMode=()=>{
|
|
4445
|
-
impactMode=!impactMode;
|
|
4446
|
-
document.getElementById('impact-btn').classList.toggle('active',impactMode);
|
|
4447
|
-
if(!impactMode){impactBadge.style.display='none';resetGraphHighlight();}
|
|
4448
|
-
};
|
|
4449
|
-
function getTransitiveDependents(startId){
|
|
4450
|
-
const result=new Set();const queue=[startId];
|
|
4451
|
-
const revMap={};
|
|
4452
|
-
DATA.links.forEach(l=>{const s=l.source.id??l.source,t=l.target.id??l.target;if(!revMap[t])revMap[t]=[];revMap[t].push(s);});
|
|
4453
|
-
while(queue.length){const id=queue.shift();if(result.has(id))continue;result.add(id);(revMap[id]||[]).forEach(x=>queue.push(x));}
|
|
4454
|
-
return result;
|
|
4455
|
-
}
|
|
4456
|
-
// Override click in impact mode
|
|
4457
|
-
const origClick=node.on('click');
|
|
4458
|
-
node.on('click',(e,d)=>{
|
|
4459
|
-
if(!impactMode){e.stopPropagation();pinnedNode=d;highlightNode(d);showDetail(d);return;}
|
|
4460
|
-
e.stopPropagation();
|
|
4461
|
-
const affected=getTransitiveDependents(d.id);
|
|
4462
|
-
node.select('circle').transition().duration(200).attr('opacity',n=>affected.has(n.id)?1:0.06)
|
|
4463
|
-
.attr('stroke',n=>affected.has(n.id)&&n.id!==d.id?'var(--red)':n.deps>=5?'var(--yellow)':nodeColor(n))
|
|
4464
|
-
.attr('stroke-width',n=>affected.has(n.id)?3:1.5);
|
|
4465
|
-
node.select('text').transition().duration(200).attr('opacity',n=>affected.has(n.id)?1:0.04);
|
|
4466
|
-
link.transition().duration(200).attr('opacity',l=>{
|
|
4467
|
-
const s=l.source.id??l.source,t=l.target.id??l.target;
|
|
4468
|
-
return affected.has(s)&&affected.has(t)?0.8:0.03;
|
|
4469
|
-
}).attr('stroke',l=>{
|
|
4470
|
-
const s=l.source.id??l.source,t=l.target.id??l.target;
|
|
4471
|
-
return affected.has(s)&&affected.has(t)?'var(--red)':l.type==='type-only'?'#1f3d5c':'#30363d';
|
|
4472
|
-
});
|
|
4473
|
-
impactBadge.textContent=d.id.split('/').pop()+' \u2192 '+(affected.size-1)+' '+i('impact.transitive');
|
|
4474
|
-
impactBadge.style.display='block';
|
|
4475
|
-
});
|
|
4476
|
-
|
|
4477
|
-
window.addEventListener('resize',()=>{
|
|
4478
|
-
const w=window.innerWidth,h=window.innerHeight-44;
|
|
4479
|
-
svg.attr('width',w).attr('height',h);
|
|
4480
|
-
});
|
|
4481
|
-
|
|
4482
|
-
${buildHierarchyJs()}
|
|
4483
|
-
${buildDiffJs(diffData)}
|
|
4484
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
4485
|
-
// INIT
|
|
4486
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
4487
|
-
applyI18n();
|
|
4488
3252
|
</script>
|
|
3253
|
+
<script>${viewerJs}</script>
|
|
4489
3254
|
</body>
|
|
4490
3255
|
</html>`
|
|
4491
3256
|
);
|
|
@@ -4519,17 +3284,17 @@ function startViewer(graph, options = {}) {
|
|
|
4519
3284
|
}
|
|
4520
3285
|
|
|
4521
3286
|
// src/utils/version.ts
|
|
4522
|
-
import { readFileSync as
|
|
4523
|
-
import { join as
|
|
4524
|
-
import { fileURLToPath } from "url";
|
|
3287
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
3288
|
+
import { join as join9, dirname as dirname3 } from "path";
|
|
3289
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4525
3290
|
function loadVersion() {
|
|
4526
|
-
let dir =
|
|
3291
|
+
let dir = dirname3(fileURLToPath2(import.meta.url));
|
|
4527
3292
|
for (let i = 0; i < 5; i++) {
|
|
4528
3293
|
try {
|
|
4529
|
-
const pkg = JSON.parse(
|
|
3294
|
+
const pkg = JSON.parse(readFileSync4(join9(dir, "package.json"), "utf-8"));
|
|
4530
3295
|
return pkg.version;
|
|
4531
3296
|
} catch {
|
|
4532
|
-
dir =
|
|
3297
|
+
dir = dirname3(dir);
|
|
4533
3298
|
}
|
|
4534
3299
|
}
|
|
4535
3300
|
return "0.0.0";
|
|
@@ -4543,7 +3308,8 @@ async function resolveGraphCli(opts) {
|
|
|
4543
3308
|
targetDir: opts.target,
|
|
4544
3309
|
projectRoot: opts.root,
|
|
4545
3310
|
exclude: opts.exclude,
|
|
4546
|
-
language: opts.language
|
|
3311
|
+
language: opts.language,
|
|
3312
|
+
noCache: opts.noCache
|
|
4547
3313
|
});
|
|
4548
3314
|
}
|
|
4549
3315
|
var program = new Command();
|
|
@@ -4568,7 +3334,11 @@ program.command("init").description("Generate initial snapshot and save to .arch
|
|
|
4568
3334
|
exclude: opts.exclude,
|
|
4569
3335
|
language
|
|
4570
3336
|
});
|
|
4571
|
-
const snapshot = await saveSnapshot(opts.root, graph, multiLayer
|
|
3337
|
+
const snapshot = await saveSnapshot(opts.root, graph, multiLayer, {
|
|
3338
|
+
targetDir: opts.target,
|
|
3339
|
+
language,
|
|
3340
|
+
exclude: opts.exclude
|
|
3341
|
+
});
|
|
4572
3342
|
console.log(t("cli.snapshotSaved"));
|
|
4573
3343
|
console.log(t("cli.timestamp", { ts: snapshot.timestamp }));
|
|
4574
3344
|
console.log(t("cli.fileCount", { count: graph.totalFiles }));
|
|
@@ -4595,7 +3365,7 @@ program.command("analyze").description(
|
|
|
4595
3365
|
).option("-t, --target <dir>", "Target directory", "src").option("-r, --root <dir>", "Project root", ".").option(
|
|
4596
3366
|
"-e, --exclude <patterns...>",
|
|
4597
3367
|
"Exclude patterns (regex)"
|
|
4598
|
-
).option("-n, --top <number>", "Number of top components to show", "10").option("--save", "Also save a snapshot after analysis").option("-l, --language <lang>", `Target language (${LANGUAGE_IDS.join(", ")})`).action(async (opts) => {
|
|
3368
|
+
).option("-n, --top <number>", "Number of top components to show", "10").option("--save", "Also save a snapshot after analysis").option("--no-cache", "Force fresh analysis (ignore graph cache)").option("-l, --language <lang>", `Target language (${LANGUAGE_IDS.join(", ")})`).action(async (opts) => {
|
|
4599
3369
|
try {
|
|
4600
3370
|
const language = validateLanguage(opts.language);
|
|
4601
3371
|
console.log(t("cli.analyzing"));
|
|
@@ -4603,12 +3373,17 @@ program.command("analyze").description(
|
|
|
4603
3373
|
target: opts.target,
|
|
4604
3374
|
root: opts.root,
|
|
4605
3375
|
exclude: opts.exclude,
|
|
4606
|
-
language
|
|
3376
|
+
language,
|
|
3377
|
+
noCache: opts.noCache
|
|
4607
3378
|
});
|
|
4608
3379
|
const report = formatAnalysisReport(graph, { topN: parseInt(opts.top, 10) });
|
|
4609
3380
|
console.log(report);
|
|
4610
3381
|
if (opts.save) {
|
|
4611
|
-
await saveSnapshot(opts.root, graph, multiLayer
|
|
3382
|
+
await saveSnapshot(opts.root, graph, multiLayer, {
|
|
3383
|
+
targetDir: opts.target,
|
|
3384
|
+
language,
|
|
3385
|
+
exclude: opts.exclude
|
|
3386
|
+
});
|
|
4612
3387
|
console.log(t("analyze.snapshotSaved"));
|
|
4613
3388
|
}
|
|
4614
3389
|
} catch (error) {
|
|
@@ -4617,19 +3392,29 @@ program.command("analyze").description(
|
|
|
4617
3392
|
});
|
|
4618
3393
|
program.command("check").description(
|
|
4619
3394
|
"Compare snapshot with current code and report change impacts"
|
|
4620
|
-
).option("-t, --target <dir>", "Target directory", "src").option("-r, --root <dir>", "Project root", ".").option("--ci", "CI mode: exit code 1 if affected files exist").option("-l, --language <lang>", `Target language (${LANGUAGE_IDS.join(", ")})`).action(async (opts) => {
|
|
3395
|
+
).option("-t, --target <dir>", "Target directory", "src").option("-r, --root <dir>", "Project root", ".").option("--ci", "CI mode: exit code 1 if affected files exist").option("--from <timestamp>", "Compare from a historical snapshot (use 'archtracker history' to see timestamps)").option("--no-cache", "Force fresh analysis (ignore graph cache)").option("-l, --language <lang>", `Target language (${LANGUAGE_IDS.join(", ")})`).action(async (opts) => {
|
|
4621
3396
|
try {
|
|
4622
3397
|
const language = validateLanguage(opts.language);
|
|
4623
|
-
|
|
4624
|
-
if (
|
|
4625
|
-
|
|
4626
|
-
|
|
3398
|
+
let existingSnapshot;
|
|
3399
|
+
if (opts.from) {
|
|
3400
|
+
existingSnapshot = await loadSnapshotByTimestamp(opts.root, opts.from);
|
|
3401
|
+
if (!existingSnapshot) {
|
|
3402
|
+
console.log(t("history.snapshotNotFound", { ts: opts.from }));
|
|
3403
|
+
process.exit(1);
|
|
3404
|
+
}
|
|
3405
|
+
} else {
|
|
3406
|
+
existingSnapshot = await loadSnapshot(opts.root);
|
|
3407
|
+
if (!existingSnapshot) {
|
|
3408
|
+
console.log(t("cli.noSnapshot"));
|
|
3409
|
+
process.exit(1);
|
|
3410
|
+
}
|
|
4627
3411
|
}
|
|
4628
3412
|
console.log(t("cli.analyzing"));
|
|
4629
3413
|
const { graph: currentGraph } = await resolveGraphCli({
|
|
4630
3414
|
target: opts.target,
|
|
4631
3415
|
root: opts.root,
|
|
4632
|
-
language
|
|
3416
|
+
language,
|
|
3417
|
+
noCache: opts.noCache
|
|
4633
3418
|
});
|
|
4634
3419
|
const diff = computeDiff(existingSnapshot.graph, currentGraph);
|
|
4635
3420
|
const report = formatDiffReport(diff);
|
|
@@ -4644,7 +3429,7 @@ program.command("check").description(
|
|
|
4644
3429
|
});
|
|
4645
3430
|
program.command("context").description(
|
|
4646
3431
|
"Display current architecture context (for AI session initialization)"
|
|
4647
|
-
).option("-t, --target <dir>", "Target directory", "src").option("-r, --root <dir>", "Project root", ".").option("--json", "Output in JSON format").option("-l, --language <lang>", `Target language (${LANGUAGE_IDS.join(", ")})`).action(async (opts) => {
|
|
3432
|
+
).option("-t, --target <dir>", "Target directory", "src").option("-r, --root <dir>", "Project root", ".").option("--json", "Output in JSON format").option("--no-cache", "Force fresh analysis (ignore graph cache)").option("-l, --language <lang>", `Target language (${LANGUAGE_IDS.join(", ")})`).action(async (opts) => {
|
|
4648
3433
|
try {
|
|
4649
3434
|
const language = validateLanguage(opts.language);
|
|
4650
3435
|
let snapshot = await loadSnapshot(opts.root);
|
|
@@ -4653,9 +3438,13 @@ program.command("context").description(
|
|
|
4653
3438
|
const result = await resolveGraphCli({
|
|
4654
3439
|
target: opts.target,
|
|
4655
3440
|
root: opts.root,
|
|
3441
|
+
language,
|
|
3442
|
+
noCache: opts.noCache
|
|
3443
|
+
});
|
|
3444
|
+
snapshot = await saveSnapshot(opts.root, result.graph, result.multiLayer, {
|
|
3445
|
+
targetDir: opts.target,
|
|
4656
3446
|
language
|
|
4657
3447
|
});
|
|
4658
|
-
snapshot = await saveSnapshot(opts.root, result.graph, result.multiLayer);
|
|
4659
3448
|
}
|
|
4660
3449
|
const graph = snapshot.graph;
|
|
4661
3450
|
if (opts.json) {
|
|
@@ -4692,7 +3481,7 @@ program.command("serve").description(
|
|
|
4692
3481
|
).option("-t, --target <dir>", "Target directory", "src").option("-r, --root <dir>", "Project root", ".").option("-p, --port <number>", "Port number", "3000").option(
|
|
4693
3482
|
"-e, --exclude <patterns...>",
|
|
4694
3483
|
"Exclude patterns (regex)"
|
|
4695
|
-
).option("-w, --watch", "Watch for file changes and auto-reload").option("-l, --language <lang>", `Target language (${LANGUAGE_IDS.join(", ")})`).action(async (opts) => {
|
|
3484
|
+
).option("-w, --watch", "Watch for file changes and auto-reload").option("--no-cache", "Force fresh analysis (ignore graph cache)").option("-l, --language <lang>", `Target language (${LANGUAGE_IDS.join(", ")})`).action(async (opts) => {
|
|
4696
3485
|
try {
|
|
4697
3486
|
const language = validateLanguage(opts.language);
|
|
4698
3487
|
console.log(t("web.starting"));
|
|
@@ -4702,7 +3491,8 @@ program.command("serve").description(
|
|
|
4702
3491
|
target: opts.target,
|
|
4703
3492
|
root: opts.root,
|
|
4704
3493
|
exclude: opts.exclude,
|
|
4705
|
-
language
|
|
3494
|
+
language,
|
|
3495
|
+
noCache: opts.noCache
|
|
4706
3496
|
});
|
|
4707
3497
|
const snapshot = await loadSnapshot(opts.root);
|
|
4708
3498
|
if (snapshot) {
|
|
@@ -4729,7 +3519,9 @@ program.command("serve").description(
|
|
|
4729
3519
|
target: opts.target,
|
|
4730
3520
|
root: opts.root,
|
|
4731
3521
|
exclude: opts.exclude,
|
|
4732
|
-
language
|
|
3522
|
+
language,
|
|
3523
|
+
noCache: true
|
|
3524
|
+
// Watch mode always needs fresh analysis
|
|
4733
3525
|
});
|
|
4734
3526
|
viewer.close();
|
|
4735
3527
|
startViewer(newResult.graph, {
|
|
@@ -4768,15 +3560,41 @@ jobs:
|
|
|
4768
3560
|
- run: npx archtracker check --target ${opts.target} --ci
|
|
4769
3561
|
`;
|
|
4770
3562
|
try {
|
|
4771
|
-
const dir =
|
|
4772
|
-
await
|
|
4773
|
-
const path =
|
|
4774
|
-
await
|
|
3563
|
+
const dir = join10(".github", "workflows");
|
|
3564
|
+
await mkdir4(dir, { recursive: true });
|
|
3565
|
+
const path = join10(dir, "arch-check.yml");
|
|
3566
|
+
await writeFile4(path, workflow, "utf-8");
|
|
4775
3567
|
console.log(t("ci.generated", { path }));
|
|
4776
3568
|
} catch (error) {
|
|
4777
3569
|
handleError(error);
|
|
4778
3570
|
}
|
|
4779
3571
|
});
|
|
3572
|
+
program.command("history").description("List all saved architecture snapshots").option("-r, --root <dir>", "Project root", ".").option("-n, --limit <number>", "Max entries to show", "20").action(async (opts) => {
|
|
3573
|
+
try {
|
|
3574
|
+
const snapshots = await listSnapshots(opts.root);
|
|
3575
|
+
if (snapshots.length === 0) {
|
|
3576
|
+
console.log(t("history.empty"));
|
|
3577
|
+
return;
|
|
3578
|
+
}
|
|
3579
|
+
const limit = parseInt(opts.limit, 10);
|
|
3580
|
+
const shown = snapshots.slice(0, limit);
|
|
3581
|
+
console.log(t("history.title"));
|
|
3582
|
+
console.log(t("history.count", { count: snapshots.length }));
|
|
3583
|
+
console.log("");
|
|
3584
|
+
for (const s of shown) {
|
|
3585
|
+
const layers = s.hasMultiLayer ? " [multi-layer]" : "";
|
|
3586
|
+
console.log(t("history.entry", {
|
|
3587
|
+
ts: s.timestamp,
|
|
3588
|
+
files: s.totalFiles,
|
|
3589
|
+
edges: s.totalEdges,
|
|
3590
|
+
circular: s.circularDeps,
|
|
3591
|
+
layers
|
|
3592
|
+
}));
|
|
3593
|
+
}
|
|
3594
|
+
} catch (error) {
|
|
3595
|
+
handleError(error);
|
|
3596
|
+
}
|
|
3597
|
+
});
|
|
4780
3598
|
var layersCmd = program.command("layers").description("Manage multi-layer architecture configuration");
|
|
4781
3599
|
layersCmd.command("init").description("Create a template .archtracker/layers.json").option("-r, --root <dir>", "Project root", ".").action(async (opts) => {
|
|
4782
3600
|
try {
|