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/mcp/index.js
CHANGED
|
@@ -1562,6 +1562,8 @@ var en = {
|
|
|
1562
1562
|
"diff.reasonRemoved": 'Dependency "{file}" was removed',
|
|
1563
1563
|
"diff.reasonModified": 'Dependency "{file}" had its dependencies changed',
|
|
1564
1564
|
"diff.reasonAdded": 'New dependency "{file}" was added',
|
|
1565
|
+
"diff.testSummary": " ... and {count} test/fixture file(s)",
|
|
1566
|
+
"diff.testAffectedSummary": " ... and {count} test/fixture-related review(s)",
|
|
1565
1567
|
// Search
|
|
1566
1568
|
"search.pathMatch": 'Path matches "{pattern}"',
|
|
1567
1569
|
"search.affected": 'May be affected by changes to "{file}" (via: {via})',
|
|
@@ -1623,6 +1625,12 @@ var en = {
|
|
|
1623
1625
|
"web.watching": "Watching {dir}/ for changes...",
|
|
1624
1626
|
"web.reloading": "File change detected, reloading...",
|
|
1625
1627
|
"web.reloaded": "Graph reloaded",
|
|
1628
|
+
// History
|
|
1629
|
+
"history.title": "# Snapshot History\n",
|
|
1630
|
+
"history.empty": "No snapshots found. Run `archtracker init` to create one.",
|
|
1631
|
+
"history.entry": " {ts} | {files} files, {edges} edges, {circular} circular{layers}",
|
|
1632
|
+
"history.count": "{count} snapshot(s) recorded",
|
|
1633
|
+
"history.snapshotNotFound": "Snapshot not found for timestamp: {ts}",
|
|
1626
1634
|
// Errors
|
|
1627
1635
|
"error.analyzer": "[Analysis Error] {message}",
|
|
1628
1636
|
"error.storage": "[Storage Error] {message}",
|
|
@@ -1655,6 +1663,8 @@ var ja = {
|
|
|
1655
1663
|
"diff.reasonRemoved": '\u4F9D\u5B58\u5148 "{file}" \u304C\u524A\u9664\u3055\u308C\u307E\u3057\u305F',
|
|
1656
1664
|
"diff.reasonModified": '\u4F9D\u5B58\u5148 "{file}" \u306E\u4F9D\u5B58\u95A2\u4FC2\u304C\u5909\u66F4\u3055\u308C\u307E\u3057\u305F',
|
|
1657
1665
|
"diff.reasonAdded": '\u65B0\u3057\u3044\u4F9D\u5B58\u5148 "{file}" \u304C\u8FFD\u52A0\u3055\u308C\u307E\u3057\u305F',
|
|
1666
|
+
"diff.testSummary": " ... \u4ED6 {count}\u4EF6\u306E\u30C6\u30B9\u30C8/\u30D5\u30A3\u30AF\u30B9\u30C1\u30E3\u30D5\u30A1\u30A4\u30EB",
|
|
1667
|
+
"diff.testAffectedSummary": " ... \u4ED6 {count}\u4EF6\u306E\u30C6\u30B9\u30C8/\u30D5\u30A3\u30AF\u30B9\u30C1\u30E3\u95A2\u9023\u306E\u78BA\u8A8D\u9805\u76EE",
|
|
1658
1668
|
// Search
|
|
1659
1669
|
"search.pathMatch": '\u30D1\u30B9\u304C "{pattern}" \u306B\u30DE\u30C3\u30C1',
|
|
1660
1670
|
"search.affected": '"{file}" \u306E\u5909\u66F4\u306B\u3088\u308A\u5F71\u97FF\u3092\u53D7\u3051\u308B\u53EF\u80FD\u6027\uFF08\u7D4C\u7531: {via}\uFF09',
|
|
@@ -1716,6 +1726,12 @@ var ja = {
|
|
|
1716
1726
|
"web.watching": "{dir}/ \u3092\u76E3\u8996\u4E2D...",
|
|
1717
1727
|
"web.reloading": "\u30D5\u30A1\u30A4\u30EB\u5909\u66F4\u3092\u691C\u51FA\u3001\u30EA\u30ED\u30FC\u30C9\u4E2D...",
|
|
1718
1728
|
"web.reloaded": "\u30B0\u30E9\u30D5\u3092\u66F4\u65B0\u3057\u307E\u3057\u305F",
|
|
1729
|
+
// History
|
|
1730
|
+
"history.title": "# \u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8\u5C65\u6B74\n",
|
|
1731
|
+
"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",
|
|
1732
|
+
"history.entry": " {ts} | {files}\u30D5\u30A1\u30A4\u30EB, {edges}\u30A8\u30C3\u30B8, \u5FAA\u74B0{circular}\u4EF6{layers}",
|
|
1733
|
+
"history.count": "{count}\u4EF6\u306E\u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8\u3092\u8A18\u9332",
|
|
1734
|
+
"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}",
|
|
1719
1735
|
// Errors
|
|
1720
1736
|
"error.analyzer": "[\u89E3\u6790\u30A8\u30E9\u30FC] {message}",
|
|
1721
1737
|
"error.storage": "[\u30B9\u30C8\u30EC\u30FC\u30B8\u30A8\u30E9\u30FC] {message}",
|
|
@@ -1858,14 +1874,14 @@ var LAYER_COLORS = [
|
|
|
1858
1874
|
"#ffa657",
|
|
1859
1875
|
"#7ee787"
|
|
1860
1876
|
];
|
|
1861
|
-
async function analyzeMultiLayer(projectRoot, layerDefs) {
|
|
1877
|
+
async function analyzeMultiLayer(projectRoot, layerDefs, globalExclude) {
|
|
1862
1878
|
const layers = {};
|
|
1863
1879
|
const layerMetadata = [];
|
|
1864
1880
|
for (let idx = 0; idx < layerDefs.length; idx++) {
|
|
1865
1881
|
const def = layerDefs[idx];
|
|
1866
1882
|
const targetDir = resolve4(projectRoot, def.targetDir);
|
|
1867
1883
|
const graph = await analyzeProject(targetDir, {
|
|
1868
|
-
exclude: def.exclude,
|
|
1884
|
+
exclude: def.exclude ?? globalExclude,
|
|
1869
1885
|
language: def.language
|
|
1870
1886
|
});
|
|
1871
1887
|
const language = def.language ?? await detectLanguage(targetDir) ?? "javascript";
|
|
@@ -2291,11 +2307,240 @@ function isNodeError(error) {
|
|
|
2291
2307
|
return error instanceof Error && "code" in error;
|
|
2292
2308
|
}
|
|
2293
2309
|
|
|
2310
|
+
// src/storage/graph-cache.ts
|
|
2311
|
+
import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2, stat as stat4 } from "fs/promises";
|
|
2312
|
+
import { join as join6, resolve as resolve5 } from "path";
|
|
2313
|
+
import { readdir as readdir3 } from "fs/promises";
|
|
2314
|
+
import { createHash } from "crypto";
|
|
2315
|
+
var CACHE_FILE = "graph.json";
|
|
2316
|
+
var ARCHTRACKER_DIR2 = ".archtracker";
|
|
2317
|
+
async function hashFile(filePath) {
|
|
2318
|
+
const content = await readFile3(filePath);
|
|
2319
|
+
return createHash("sha256").update(content).digest("hex");
|
|
2320
|
+
}
|
|
2321
|
+
async function collectFileFingerprints(dir, exclude = []) {
|
|
2322
|
+
const fingerprints = {};
|
|
2323
|
+
const excludeRegexes = exclude.map((p) => new RegExp(p));
|
|
2324
|
+
async function walk(currentDir) {
|
|
2325
|
+
let entries;
|
|
2326
|
+
try {
|
|
2327
|
+
entries = await readdir3(currentDir, { withFileTypes: true });
|
|
2328
|
+
} catch {
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
for (const entry of entries) {
|
|
2332
|
+
const fullPath = join6(currentDir, entry.name);
|
|
2333
|
+
const relativePath = fullPath.slice(dir.length + 1);
|
|
2334
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
2335
|
+
if (excludeRegexes.some((r) => r.test(relativePath))) continue;
|
|
2336
|
+
if (entry.isDirectory()) {
|
|
2337
|
+
await walk(fullPath);
|
|
2338
|
+
} else if (entry.isFile()) {
|
|
2339
|
+
try {
|
|
2340
|
+
const s = await stat4(fullPath);
|
|
2341
|
+
const hash = await hashFile(fullPath);
|
|
2342
|
+
fingerprints[relativePath] = { mtime: s.mtimeMs, hash };
|
|
2343
|
+
} catch {
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
await walk(dir);
|
|
2349
|
+
return fingerprints;
|
|
2350
|
+
}
|
|
2351
|
+
async function saveGraphCache(projectRoot, graph, options, extra) {
|
|
2352
|
+
const absRoot = resolve5(projectRoot);
|
|
2353
|
+
const dir = join6(absRoot, ARCHTRACKER_DIR2);
|
|
2354
|
+
await mkdir2(dir, { recursive: true });
|
|
2355
|
+
let fingerprints;
|
|
2356
|
+
if (extra?.layerDirs?.length) {
|
|
2357
|
+
fingerprints = {};
|
|
2358
|
+
for (const layerDir of extra.layerDirs) {
|
|
2359
|
+
const layerPath = resolve5(absRoot, layerDir);
|
|
2360
|
+
const layerFp = await collectFileFingerprints(layerPath, options.exclude);
|
|
2361
|
+
for (const [key, value] of Object.entries(layerFp)) {
|
|
2362
|
+
fingerprints[`${layerDir}/${key}`] = value;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
} else {
|
|
2366
|
+
fingerprints = await collectFingerprintsForGraph(absRoot, options);
|
|
2367
|
+
}
|
|
2368
|
+
let layersJsonHash;
|
|
2369
|
+
try {
|
|
2370
|
+
layersJsonHash = await hashFile(join6(dir, "layers.json"));
|
|
2371
|
+
} catch {
|
|
2372
|
+
}
|
|
2373
|
+
const mtimes = {};
|
|
2374
|
+
for (const [k, v] of Object.entries(fingerprints)) {
|
|
2375
|
+
mtimes[k] = v.mtime;
|
|
2376
|
+
}
|
|
2377
|
+
const cache = {
|
|
2378
|
+
version: "1.0",
|
|
2379
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2380
|
+
options: {
|
|
2381
|
+
targetDir: options.targetDir,
|
|
2382
|
+
projectRoot: options.projectRoot,
|
|
2383
|
+
language: options.language,
|
|
2384
|
+
exclude: options.exclude
|
|
2385
|
+
},
|
|
2386
|
+
fileMtimes: mtimes,
|
|
2387
|
+
fileHashes: Object.fromEntries(
|
|
2388
|
+
Object.entries(fingerprints).map(([k, v]) => [k, v.hash])
|
|
2389
|
+
),
|
|
2390
|
+
graph,
|
|
2391
|
+
multiLayer: extra?.multiLayer,
|
|
2392
|
+
layerMetadata: extra?.layerMetadata,
|
|
2393
|
+
layerDirs: extra?.layerDirs,
|
|
2394
|
+
layersJsonHash
|
|
2395
|
+
};
|
|
2396
|
+
await writeFile2(join6(dir, CACHE_FILE), JSON.stringify(cache), "utf-8");
|
|
2397
|
+
}
|
|
2398
|
+
async function loadGraphCache(projectRoot) {
|
|
2399
|
+
const absRoot = resolve5(projectRoot);
|
|
2400
|
+
const filePath = join6(absRoot, ARCHTRACKER_DIR2, CACHE_FILE);
|
|
2401
|
+
try {
|
|
2402
|
+
const raw = await readFile3(filePath, "utf-8");
|
|
2403
|
+
const data = JSON.parse(raw);
|
|
2404
|
+
if (data?.version !== "1.0" || !data?.graph || !data?.fileMtimes) {
|
|
2405
|
+
return null;
|
|
2406
|
+
}
|
|
2407
|
+
return data;
|
|
2408
|
+
} catch {
|
|
2409
|
+
return null;
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
async function isGraphCacheValid(cache, projectRoot, options) {
|
|
2413
|
+
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 ?? [])) {
|
|
2414
|
+
return false;
|
|
2415
|
+
}
|
|
2416
|
+
const absRoot = resolve5(projectRoot);
|
|
2417
|
+
const targetPath = resolve5(absRoot, options.targetDir);
|
|
2418
|
+
const isMultiLayer = (cache.layerDirs?.length ?? 0) > 0;
|
|
2419
|
+
let currentLayersHash;
|
|
2420
|
+
try {
|
|
2421
|
+
currentLayersHash = await hashFile(join6(absRoot, ARCHTRACKER_DIR2, "layers.json"));
|
|
2422
|
+
} catch {
|
|
2423
|
+
}
|
|
2424
|
+
if ((cache.layersJsonHash ?? "") !== (currentLayersHash ?? "")) return false;
|
|
2425
|
+
let currentMtimes;
|
|
2426
|
+
if (isMultiLayer) {
|
|
2427
|
+
currentMtimes = {};
|
|
2428
|
+
for (const layerDir of cache.layerDirs) {
|
|
2429
|
+
const layerPath = resolve5(absRoot, layerDir);
|
|
2430
|
+
const dirMtimes = await collectFileMtimes(layerPath, options.exclude);
|
|
2431
|
+
for (const [key, value] of Object.entries(dirMtimes)) {
|
|
2432
|
+
currentMtimes[`${layerDir}/${key}`] = value;
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
} else {
|
|
2436
|
+
currentMtimes = await collectFileMtimes(targetPath, options.exclude);
|
|
2437
|
+
}
|
|
2438
|
+
const cachedKeys = new Set(Object.keys(cache.fileMtimes));
|
|
2439
|
+
const currentKeys = new Set(Object.keys(currentMtimes));
|
|
2440
|
+
if (cachedKeys.size !== currentKeys.size) return false;
|
|
2441
|
+
for (const key of cachedKeys) {
|
|
2442
|
+
if (!currentKeys.has(key)) return false;
|
|
2443
|
+
}
|
|
2444
|
+
const mtimeChanged = [];
|
|
2445
|
+
for (const key of cachedKeys) {
|
|
2446
|
+
if (Math.abs(cache.fileMtimes[key] - currentMtimes[key]) > 1) {
|
|
2447
|
+
mtimeChanged.push(key);
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
if (mtimeChanged.length === 0) return true;
|
|
2451
|
+
if (!cache.fileHashes) return false;
|
|
2452
|
+
for (const key of mtimeChanged) {
|
|
2453
|
+
const cachedHash = cache.fileHashes[key];
|
|
2454
|
+
if (!cachedHash) return false;
|
|
2455
|
+
try {
|
|
2456
|
+
const fullPath = isMultiLayer ? join6(absRoot, key) : join6(targetPath, key);
|
|
2457
|
+
const currentHash = await hashFile(fullPath);
|
|
2458
|
+
if (currentHash !== cachedHash) return false;
|
|
2459
|
+
} catch {
|
|
2460
|
+
return false;
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
return true;
|
|
2464
|
+
}
|
|
2465
|
+
async function collectFileMtimes(dir, exclude) {
|
|
2466
|
+
const mtimes = {};
|
|
2467
|
+
const excludeRegexes = (exclude ?? []).map((p) => new RegExp(p));
|
|
2468
|
+
async function walk(currentDir) {
|
|
2469
|
+
let entries;
|
|
2470
|
+
try {
|
|
2471
|
+
entries = await readdir3(currentDir, { withFileTypes: true });
|
|
2472
|
+
} catch {
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
for (const entry of entries) {
|
|
2476
|
+
const fullPath = join6(currentDir, entry.name);
|
|
2477
|
+
const relativePath = fullPath.slice(dir.length + 1);
|
|
2478
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
2479
|
+
if (excludeRegexes.some((r) => r.test(relativePath))) continue;
|
|
2480
|
+
if (entry.isDirectory()) {
|
|
2481
|
+
await walk(fullPath);
|
|
2482
|
+
} else if (entry.isFile()) {
|
|
2483
|
+
try {
|
|
2484
|
+
const s = await stat4(fullPath);
|
|
2485
|
+
mtimes[relativePath] = s.mtimeMs;
|
|
2486
|
+
} catch {
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
await walk(dir);
|
|
2492
|
+
return mtimes;
|
|
2493
|
+
}
|
|
2494
|
+
async function collectFingerprintsForGraph(absRoot, options) {
|
|
2495
|
+
const targetPath = resolve5(absRoot, options.targetDir);
|
|
2496
|
+
return collectFileFingerprints(targetPath, options.exclude);
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2294
2499
|
// src/analyzer/resolve.ts
|
|
2295
2500
|
async function resolveGraph(opts) {
|
|
2501
|
+
if (!opts.noCache) {
|
|
2502
|
+
const cache = await loadGraphCache(opts.projectRoot);
|
|
2503
|
+
if (cache) {
|
|
2504
|
+
const valid = await isGraphCacheValid(cache, opts.projectRoot, {
|
|
2505
|
+
targetDir: opts.targetDir,
|
|
2506
|
+
projectRoot: opts.projectRoot,
|
|
2507
|
+
language: opts.language,
|
|
2508
|
+
exclude: opts.exclude
|
|
2509
|
+
});
|
|
2510
|
+
if (valid) {
|
|
2511
|
+
const result2 = {
|
|
2512
|
+
graph: cache.graph,
|
|
2513
|
+
multiLayer: cache.multiLayer,
|
|
2514
|
+
layerMetadata: cache.layerMetadata,
|
|
2515
|
+
fromCache: true
|
|
2516
|
+
};
|
|
2517
|
+
if (cache.multiLayer) {
|
|
2518
|
+
const layerConfig2 = await loadLayerConfig(opts.projectRoot);
|
|
2519
|
+
if (layerConfig2) {
|
|
2520
|
+
const autoConnections = detectCrossLayerConnections(
|
|
2521
|
+
cache.multiLayer.layers,
|
|
2522
|
+
layerConfig2.layers
|
|
2523
|
+
);
|
|
2524
|
+
const manualConnections = layerConfig2.connections ?? [];
|
|
2525
|
+
const manualKeys = new Set(manualConnections.map(
|
|
2526
|
+
(c) => `${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`
|
|
2527
|
+
));
|
|
2528
|
+
result2.crossLayerEdges = [
|
|
2529
|
+
...manualConnections,
|
|
2530
|
+
...autoConnections.filter(
|
|
2531
|
+
(c) => !manualKeys.has(`${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`)
|
|
2532
|
+
)
|
|
2533
|
+
];
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
return result2;
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2296
2540
|
const layerConfig = await loadLayerConfig(opts.projectRoot);
|
|
2541
|
+
let result;
|
|
2297
2542
|
if (layerConfig) {
|
|
2298
|
-
const multi = await analyzeMultiLayer(opts.projectRoot, layerConfig.layers);
|
|
2543
|
+
const multi = await analyzeMultiLayer(opts.projectRoot, layerConfig.layers, opts.exclude);
|
|
2299
2544
|
const autoConnections = detectCrossLayerConnections(multi.layers, layerConfig.layers);
|
|
2300
2545
|
const manualConnections = layerConfig.connections ?? [];
|
|
2301
2546
|
const manualKeys = new Set(manualConnections.map(
|
|
@@ -2307,31 +2552,52 @@ async function resolveGraph(opts) {
|
|
|
2307
2552
|
(c) => !manualKeys.has(`${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`)
|
|
2308
2553
|
)
|
|
2309
2554
|
];
|
|
2310
|
-
|
|
2555
|
+
result = {
|
|
2311
2556
|
graph: multi.merged,
|
|
2312
2557
|
multiLayer: multi,
|
|
2313
2558
|
layerMetadata: multi.layerMetadata,
|
|
2314
2559
|
crossLayerEdges: merged
|
|
2315
2560
|
};
|
|
2561
|
+
} else {
|
|
2562
|
+
const graph = await analyzeProject(opts.targetDir, {
|
|
2563
|
+
exclude: opts.exclude,
|
|
2564
|
+
language: opts.language
|
|
2565
|
+
});
|
|
2566
|
+
result = { graph };
|
|
2316
2567
|
}
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2568
|
+
try {
|
|
2569
|
+
await saveGraphCache(
|
|
2570
|
+
opts.projectRoot,
|
|
2571
|
+
result.graph,
|
|
2572
|
+
{
|
|
2573
|
+
targetDir: opts.targetDir,
|
|
2574
|
+
projectRoot: opts.projectRoot,
|
|
2575
|
+
language: opts.language,
|
|
2576
|
+
exclude: opts.exclude
|
|
2577
|
+
},
|
|
2578
|
+
{
|
|
2579
|
+
multiLayer: result.multiLayer,
|
|
2580
|
+
layerMetadata: result.layerMetadata,
|
|
2581
|
+
layerDirs: layerConfig?.layers.map((l) => l.targetDir)
|
|
2582
|
+
}
|
|
2583
|
+
);
|
|
2584
|
+
} catch {
|
|
2585
|
+
}
|
|
2586
|
+
return result;
|
|
2322
2587
|
}
|
|
2323
2588
|
|
|
2324
2589
|
// src/storage/snapshot.ts
|
|
2325
|
-
import { mkdir as
|
|
2326
|
-
import { join as
|
|
2590
|
+
import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile4, readdir as readdir4, access } from "fs/promises";
|
|
2591
|
+
import { join as join7 } from "path";
|
|
2327
2592
|
import { z as z2 } from "zod";
|
|
2328
2593
|
|
|
2329
2594
|
// src/types/schema.ts
|
|
2330
2595
|
var SCHEMA_VERSION = "1.1";
|
|
2331
2596
|
|
|
2332
2597
|
// src/storage/snapshot.ts
|
|
2333
|
-
var
|
|
2598
|
+
var ARCHTRACKER_DIR3 = ".archtracker";
|
|
2334
2599
|
var SNAPSHOT_FILE = "snapshot.json";
|
|
2600
|
+
var HISTORY_DIR = "history";
|
|
2335
2601
|
var FileNodeSchema = z2.object({
|
|
2336
2602
|
path: z2.string(),
|
|
2337
2603
|
exists: z2.boolean(),
|
|
@@ -2356,25 +2622,34 @@ var SnapshotSchema = z2.object({
|
|
|
2356
2622
|
rootDir: z2.string(),
|
|
2357
2623
|
graph: DependencyGraphSchema
|
|
2358
2624
|
});
|
|
2359
|
-
async function saveSnapshot(projectRoot, graph, multiLayer) {
|
|
2360
|
-
const dirPath =
|
|
2361
|
-
const filePath =
|
|
2625
|
+
async function saveSnapshot(projectRoot, graph, multiLayer, analysisOptions) {
|
|
2626
|
+
const dirPath = join7(projectRoot, ARCHTRACKER_DIR3);
|
|
2627
|
+
const filePath = join7(dirPath, SNAPSHOT_FILE);
|
|
2362
2628
|
const snapshot = {
|
|
2363
2629
|
version: SCHEMA_VERSION,
|
|
2364
2630
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2365
2631
|
rootDir: graph.rootDir,
|
|
2366
2632
|
graph,
|
|
2367
|
-
...multiLayer ? { multiLayer } : {}
|
|
2633
|
+
...multiLayer ? { multiLayer } : {},
|
|
2634
|
+
...analysisOptions ? { analysisOptions } : {}
|
|
2368
2635
|
};
|
|
2369
|
-
await
|
|
2370
|
-
|
|
2636
|
+
await mkdir3(dirPath, { recursive: true });
|
|
2637
|
+
const json = JSON.stringify(snapshot, null, 2);
|
|
2638
|
+
await writeFile3(filePath, json, "utf-8");
|
|
2639
|
+
try {
|
|
2640
|
+
const historyPath = join7(dirPath, HISTORY_DIR);
|
|
2641
|
+
await mkdir3(historyPath, { recursive: true });
|
|
2642
|
+
const safeTs = snapshot.timestamp.replace(/[:.]/g, "-");
|
|
2643
|
+
await writeFile3(join7(historyPath, `${safeTs}.json`), json, "utf-8");
|
|
2644
|
+
} catch {
|
|
2645
|
+
}
|
|
2371
2646
|
return snapshot;
|
|
2372
2647
|
}
|
|
2373
2648
|
async function loadSnapshot(projectRoot) {
|
|
2374
|
-
const filePath =
|
|
2649
|
+
const filePath = join7(projectRoot, ARCHTRACKER_DIR3, SNAPSHOT_FILE);
|
|
2375
2650
|
let raw;
|
|
2376
2651
|
try {
|
|
2377
|
-
raw = await
|
|
2652
|
+
raw = await readFile4(filePath, "utf-8");
|
|
2378
2653
|
} catch (error) {
|
|
2379
2654
|
if (isNodeError2(error) && error.code === "ENOENT") {
|
|
2380
2655
|
return null;
|
|
@@ -2401,6 +2676,67 @@ async function loadSnapshot(projectRoot) {
|
|
|
2401
2676
|
}
|
|
2402
2677
|
return result.data;
|
|
2403
2678
|
}
|
|
2679
|
+
async function listSnapshots(projectRoot) {
|
|
2680
|
+
const historyPath = join7(projectRoot, ARCHTRACKER_DIR3, HISTORY_DIR);
|
|
2681
|
+
let entries;
|
|
2682
|
+
try {
|
|
2683
|
+
entries = await readdir4(historyPath);
|
|
2684
|
+
} catch {
|
|
2685
|
+
return [];
|
|
2686
|
+
}
|
|
2687
|
+
const jsonFiles = entries.filter((e) => e.endsWith(".json")).sort().reverse();
|
|
2688
|
+
const summaries = [];
|
|
2689
|
+
for (const file of jsonFiles) {
|
|
2690
|
+
try {
|
|
2691
|
+
const raw = await readFile4(join7(historyPath, file), "utf-8");
|
|
2692
|
+
const parsed = JSON.parse(raw);
|
|
2693
|
+
const result = SnapshotSchema.safeParse(parsed);
|
|
2694
|
+
if (result.success) {
|
|
2695
|
+
const snap = result.data;
|
|
2696
|
+
summaries.push({
|
|
2697
|
+
timestamp: snap.timestamp,
|
|
2698
|
+
totalFiles: snap.graph.totalFiles,
|
|
2699
|
+
totalEdges: snap.graph.totalEdges,
|
|
2700
|
+
circularDeps: snap.graph.circularDependencies.length,
|
|
2701
|
+
hasMultiLayer: "multiLayer" in parsed && parsed.multiLayer != null
|
|
2702
|
+
});
|
|
2703
|
+
}
|
|
2704
|
+
} catch {
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
return summaries;
|
|
2708
|
+
}
|
|
2709
|
+
async function loadSnapshotByTimestamp(projectRoot, timestamp) {
|
|
2710
|
+
const historyPath = join7(projectRoot, ARCHTRACKER_DIR3, HISTORY_DIR);
|
|
2711
|
+
let entries;
|
|
2712
|
+
try {
|
|
2713
|
+
entries = await readdir4(historyPath);
|
|
2714
|
+
} catch {
|
|
2715
|
+
return null;
|
|
2716
|
+
}
|
|
2717
|
+
const jsonFiles = entries.filter((e) => e.endsWith(".json")).sort();
|
|
2718
|
+
const safeTs = timestamp.replace(/[:.]/g, "-");
|
|
2719
|
+
const exactMatch = jsonFiles.find((f) => f === `${safeTs}.json`);
|
|
2720
|
+
if (exactMatch) {
|
|
2721
|
+
return loadHistoryFile(join7(historyPath, exactMatch));
|
|
2722
|
+
}
|
|
2723
|
+
const prefixMatch = jsonFiles.find((f) => f.startsWith(safeTs.slice(0, 10)));
|
|
2724
|
+
if (prefixMatch) {
|
|
2725
|
+
return loadHistoryFile(join7(historyPath, prefixMatch));
|
|
2726
|
+
}
|
|
2727
|
+
return null;
|
|
2728
|
+
}
|
|
2729
|
+
async function loadHistoryFile(filePath) {
|
|
2730
|
+
try {
|
|
2731
|
+
const raw = await readFile4(filePath, "utf-8");
|
|
2732
|
+
const parsed = JSON.parse(raw);
|
|
2733
|
+
const result = SnapshotSchema.safeParse(parsed);
|
|
2734
|
+
if (result.success) return result.data;
|
|
2735
|
+
return null;
|
|
2736
|
+
} catch {
|
|
2737
|
+
return null;
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2404
2740
|
var StorageError = class extends Error {
|
|
2405
2741
|
constructor(message, options) {
|
|
2406
2742
|
super(message, options);
|
|
@@ -2462,6 +2798,17 @@ function computeDiff(oldGraph, newGraph) {
|
|
|
2462
2798
|
}
|
|
2463
2799
|
return { added, removed, modified, affectedDependents };
|
|
2464
2800
|
}
|
|
2801
|
+
function isTestOrFixture(path) {
|
|
2802
|
+
return /(__fixtures__|__tests__|__mocks__|\.test\.|\.spec\.|\.e2e\.)/.test(path);
|
|
2803
|
+
}
|
|
2804
|
+
function partition(arr, pred) {
|
|
2805
|
+
const yes = [];
|
|
2806
|
+
const no = [];
|
|
2807
|
+
for (const item of arr) {
|
|
2808
|
+
(pred(item) ? yes : no).push(item);
|
|
2809
|
+
}
|
|
2810
|
+
return [yes, no];
|
|
2811
|
+
}
|
|
2465
2812
|
function formatDiffReport(diff) {
|
|
2466
2813
|
const lines = [];
|
|
2467
2814
|
lines.push(t("diff.title"));
|
|
@@ -2470,32 +2817,51 @@ function formatDiffReport(diff) {
|
|
|
2470
2817
|
return lines.join("\n");
|
|
2471
2818
|
}
|
|
2472
2819
|
if (diff.added.length > 0) {
|
|
2820
|
+
const [testFiles, srcFiles] = partition(diff.added, isTestOrFixture);
|
|
2473
2821
|
lines.push(t("diff.added", { count: diff.added.length }));
|
|
2474
|
-
for (const f of
|
|
2822
|
+
for (const f of srcFiles) {
|
|
2475
2823
|
lines.push(` + ${f}`);
|
|
2476
2824
|
}
|
|
2825
|
+
if (testFiles.length > 0) {
|
|
2826
|
+
lines.push(t("diff.testSummary", { count: testFiles.length }));
|
|
2827
|
+
}
|
|
2477
2828
|
lines.push("");
|
|
2478
2829
|
}
|
|
2479
2830
|
if (diff.removed.length > 0) {
|
|
2831
|
+
const [testFiles, srcFiles] = partition(diff.removed, isTestOrFixture);
|
|
2480
2832
|
lines.push(t("diff.removed", { count: diff.removed.length }));
|
|
2481
|
-
for (const f of
|
|
2833
|
+
for (const f of srcFiles) {
|
|
2482
2834
|
lines.push(` - ${f}`);
|
|
2483
2835
|
}
|
|
2836
|
+
if (testFiles.length > 0) {
|
|
2837
|
+
lines.push(t("diff.testSummary", { count: testFiles.length }));
|
|
2838
|
+
}
|
|
2484
2839
|
lines.push("");
|
|
2485
2840
|
}
|
|
2486
2841
|
if (diff.modified.length > 0) {
|
|
2842
|
+
const [testFiles, srcFiles] = partition(diff.modified, isTestOrFixture);
|
|
2487
2843
|
lines.push(t("diff.modified", { count: diff.modified.length }));
|
|
2488
|
-
for (const f of
|
|
2844
|
+
for (const f of srcFiles) {
|
|
2489
2845
|
lines.push(` ~ ${f}`);
|
|
2490
2846
|
}
|
|
2847
|
+
if (testFiles.length > 0) {
|
|
2848
|
+
lines.push(t("diff.testSummary", { count: testFiles.length }));
|
|
2849
|
+
}
|
|
2491
2850
|
lines.push("");
|
|
2492
2851
|
}
|
|
2493
2852
|
if (diff.affectedDependents.length > 0) {
|
|
2853
|
+
const [testEntries, srcEntries] = partition(
|
|
2854
|
+
diff.affectedDependents,
|
|
2855
|
+
(a) => isTestOrFixture(a.file) || isTestOrFixture(a.dependsOn)
|
|
2856
|
+
);
|
|
2494
2857
|
lines.push(t("diff.affected", { count: diff.affectedDependents.length }));
|
|
2495
|
-
for (const a of
|
|
2858
|
+
for (const a of srcEntries) {
|
|
2496
2859
|
lines.push(` ! ${a.file}`);
|
|
2497
2860
|
lines.push(` ${a.reason}`);
|
|
2498
2861
|
}
|
|
2862
|
+
if (testEntries.length > 0) {
|
|
2863
|
+
lines.push(t("diff.testAffectedSummary", { count: testEntries.length }));
|
|
2864
|
+
}
|
|
2499
2865
|
lines.push("");
|
|
2500
2866
|
}
|
|
2501
2867
|
return lines.join("\n");
|
|
@@ -2509,10 +2875,10 @@ function arraysEqual(a, b) {
|
|
|
2509
2875
|
}
|
|
2510
2876
|
|
|
2511
2877
|
// src/utils/path-guard.ts
|
|
2512
|
-
import { resolve as
|
|
2878
|
+
import { resolve as resolve6 } from "path";
|
|
2513
2879
|
function validatePath(inputPath, boundary) {
|
|
2514
|
-
const
|
|
2515
|
-
const
|
|
2880
|
+
const root = boundary ? resolve6(boundary) : process.cwd();
|
|
2881
|
+
const resolved = boundary ? resolve6(root, inputPath) : resolve6(inputPath);
|
|
2516
2882
|
if (!resolved.startsWith(root)) {
|
|
2517
2883
|
throw new PathTraversalError(
|
|
2518
2884
|
t("pathGuard.traversal", { input: inputPath, resolved, boundary: root })
|
|
@@ -2520,6 +2886,9 @@ function validatePath(inputPath, boundary) {
|
|
|
2520
2886
|
}
|
|
2521
2887
|
return resolved;
|
|
2522
2888
|
}
|
|
2889
|
+
function resolveProjectRoot(inputPath) {
|
|
2890
|
+
return resolve6(inputPath);
|
|
2891
|
+
}
|
|
2523
2892
|
var PathTraversalError = class extends Error {
|
|
2524
2893
|
constructor(message) {
|
|
2525
2894
|
super(message);
|
|
@@ -2529,13 +2898,13 @@ var PathTraversalError = class extends Error {
|
|
|
2529
2898
|
|
|
2530
2899
|
// src/utils/version.ts
|
|
2531
2900
|
import { readFileSync as readFileSync3 } from "fs";
|
|
2532
|
-
import { join as
|
|
2901
|
+
import { join as join8, dirname as dirname2 } from "path";
|
|
2533
2902
|
import { fileURLToPath } from "url";
|
|
2534
2903
|
function loadVersion() {
|
|
2535
2904
|
let dir = dirname2(fileURLToPath(import.meta.url));
|
|
2536
2905
|
for (let i = 0; i < 5; i++) {
|
|
2537
2906
|
try {
|
|
2538
|
-
const pkg = JSON.parse(readFileSync3(
|
|
2907
|
+
const pkg = JSON.parse(readFileSync3(join8(dir, "package.json"), "utf-8"));
|
|
2539
2908
|
return pkg.version;
|
|
2540
2909
|
} catch {
|
|
2541
2910
|
dir = dirname2(dir);
|
|
@@ -2557,13 +2926,45 @@ var LANG_DISPLAY = {
|
|
|
2557
2926
|
"c-sharp": "C#"
|
|
2558
2927
|
};
|
|
2559
2928
|
var languageList = LANGUAGE_IDS.map((id) => LANG_DISPLAY[id] ?? id.charAt(0).toUpperCase() + id.slice(1)).join(", ");
|
|
2929
|
+
var MCP_DEFAULT_EXCLUDES = [
|
|
2930
|
+
"__fixtures__",
|
|
2931
|
+
"__tests__",
|
|
2932
|
+
"__mocks__",
|
|
2933
|
+
"\\.test\\.",
|
|
2934
|
+
"\\.spec\\.",
|
|
2935
|
+
"\\.e2e\\."
|
|
2936
|
+
];
|
|
2937
|
+
var memoryCache = null;
|
|
2938
|
+
function cacheKey(opts) {
|
|
2939
|
+
return JSON.stringify([opts.targetDir, opts.projectRoot, opts.language ?? "", opts.exclude ?? []]);
|
|
2940
|
+
}
|
|
2560
2941
|
async function resolveGraphMcp(opts) {
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2942
|
+
const effectiveOpts = {
|
|
2943
|
+
...opts,
|
|
2944
|
+
exclude: opts.exclude ?? MCP_DEFAULT_EXCLUDES
|
|
2945
|
+
};
|
|
2946
|
+
const key = cacheKey(effectiveOpts);
|
|
2947
|
+
if (memoryCache && memoryCache.key === key) {
|
|
2948
|
+
const result2 = await resolveGraph({
|
|
2949
|
+
targetDir: effectiveOpts.targetDir,
|
|
2950
|
+
projectRoot: effectiveOpts.projectRoot,
|
|
2951
|
+
exclude: effectiveOpts.exclude,
|
|
2952
|
+
language: effectiveOpts.language
|
|
2953
|
+
});
|
|
2954
|
+
if (result2.fromCache) {
|
|
2955
|
+
return memoryCache.result;
|
|
2956
|
+
}
|
|
2957
|
+
memoryCache = { key, result: result2, timestamp: Date.now() };
|
|
2958
|
+
return result2;
|
|
2959
|
+
}
|
|
2960
|
+
const result = await resolveGraph({
|
|
2961
|
+
targetDir: effectiveOpts.targetDir,
|
|
2962
|
+
projectRoot: effectiveOpts.projectRoot,
|
|
2963
|
+
exclude: effectiveOpts.exclude,
|
|
2964
|
+
language: effectiveOpts.language
|
|
2566
2965
|
});
|
|
2966
|
+
memoryCache = { key, result, timestamp: Date.now() };
|
|
2967
|
+
return result;
|
|
2567
2968
|
}
|
|
2568
2969
|
function formatLayerSummary(metadata) {
|
|
2569
2970
|
return metadata.map(
|
|
@@ -2581,11 +2982,11 @@ server.tool(
|
|
|
2581
2982
|
},
|
|
2582
2983
|
async ({ targetDir, projectRoot, exclude, language }) => {
|
|
2583
2984
|
try {
|
|
2584
|
-
|
|
2585
|
-
validatePath(
|
|
2985
|
+
const root = resolveProjectRoot(projectRoot);
|
|
2986
|
+
validatePath(targetDir, root);
|
|
2586
2987
|
const { graph, layerMetadata, crossLayerEdges } = await resolveGraphMcp({
|
|
2587
2988
|
targetDir,
|
|
2588
|
-
projectRoot,
|
|
2989
|
+
projectRoot: root,
|
|
2589
2990
|
exclude,
|
|
2590
2991
|
language
|
|
2591
2992
|
});
|
|
@@ -2622,11 +3023,11 @@ server.tool(
|
|
|
2622
3023
|
},
|
|
2623
3024
|
async ({ targetDir, exclude, topN, saveSnapshot: doSave, projectRoot, language }) => {
|
|
2624
3025
|
try {
|
|
2625
|
-
|
|
2626
|
-
validatePath(
|
|
3026
|
+
const root = resolveProjectRoot(projectRoot);
|
|
3027
|
+
validatePath(targetDir, root);
|
|
2627
3028
|
const { graph, multiLayer, layerMetadata, crossLayerEdges } = await resolveGraphMcp({
|
|
2628
3029
|
targetDir,
|
|
2629
|
-
projectRoot,
|
|
3030
|
+
projectRoot: root,
|
|
2630
3031
|
exclude,
|
|
2631
3032
|
language
|
|
2632
3033
|
});
|
|
@@ -2646,7 +3047,7 @@ Cross-layer connections (${crossLayerEdges.length}):
|
|
|
2646
3047
|
${crossSummary}` });
|
|
2647
3048
|
}
|
|
2648
3049
|
if (doSave) {
|
|
2649
|
-
await saveSnapshot(
|
|
3050
|
+
await saveSnapshot(root, graph, multiLayer, { targetDir, language, exclude });
|
|
2650
3051
|
content.push({ type: "text", text: t("analyze.snapshotSaved") });
|
|
2651
3052
|
}
|
|
2652
3053
|
return { content };
|
|
@@ -2665,14 +3066,14 @@ server.tool(
|
|
|
2665
3066
|
},
|
|
2666
3067
|
async ({ targetDir, projectRoot, language }) => {
|
|
2667
3068
|
try {
|
|
2668
|
-
|
|
2669
|
-
validatePath(
|
|
3069
|
+
const root = resolveProjectRoot(projectRoot);
|
|
3070
|
+
validatePath(targetDir, root);
|
|
2670
3071
|
const { graph, multiLayer, layerMetadata } = await resolveGraphMcp({
|
|
2671
3072
|
targetDir,
|
|
2672
|
-
projectRoot,
|
|
3073
|
+
projectRoot: root,
|
|
2673
3074
|
language
|
|
2674
3075
|
});
|
|
2675
|
-
const snapshot = await saveSnapshot(
|
|
3076
|
+
const snapshot = await saveSnapshot(root, graph, multiLayer, { targetDir, language });
|
|
2676
3077
|
const keyComponents = Object.values(graph.files).sort((a, b) => b.dependents.length - a.dependents.length).slice(0, 5).map((f) => ` ${t("cli.dependedBy", { path: f.path, count: f.dependents.length })}`);
|
|
2677
3078
|
const report = [
|
|
2678
3079
|
t("mcp.snapshotSaved"),
|
|
@@ -2692,24 +3093,58 @@ server.tool(
|
|
|
2692
3093
|
);
|
|
2693
3094
|
server.tool(
|
|
2694
3095
|
"check_architecture_diff",
|
|
2695
|
-
"Compare saved snapshot with current code dependencies and warn about files that may need updates",
|
|
3096
|
+
"Compare saved snapshot with current code dependencies and warn about files that may need updates. Use fromTimestamp to diff against a historical snapshot, or listHistory=true to list all snapshots.",
|
|
2696
3097
|
{
|
|
2697
3098
|
targetDir: z3.string().default("src").describe("Target directory path"),
|
|
2698
3099
|
projectRoot: z3.string().default(".").describe("Project root (where .archtracker is placed)"),
|
|
2699
|
-
language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
|
|
3100
|
+
language: languageEnum.optional().describe("Target language (auto-detected if omitted)"),
|
|
3101
|
+
fromTimestamp: z3.string().optional().describe("Compare from a specific historical snapshot timestamp"),
|
|
3102
|
+
listHistory: z3.boolean().optional().describe("If true, list all available snapshots instead of diffing")
|
|
2700
3103
|
},
|
|
2701
|
-
async ({ targetDir, projectRoot, language }) => {
|
|
3104
|
+
async ({ targetDir, projectRoot, language, fromTimestamp, listHistory }) => {
|
|
2702
3105
|
try {
|
|
2703
|
-
|
|
2704
|
-
validatePath(
|
|
2705
|
-
|
|
3106
|
+
const root = resolveProjectRoot(projectRoot);
|
|
3107
|
+
validatePath(targetDir, root);
|
|
3108
|
+
if (listHistory) {
|
|
3109
|
+
const snapshots = await listSnapshots(root);
|
|
3110
|
+
if (snapshots.length === 0) {
|
|
3111
|
+
return { content: [{ type: "text", text: t("history.empty") }] };
|
|
3112
|
+
}
|
|
3113
|
+
const lines = [
|
|
3114
|
+
t("history.count", { count: snapshots.length }),
|
|
3115
|
+
"",
|
|
3116
|
+
...snapshots.map((s) => {
|
|
3117
|
+
const layers = s.hasMultiLayer ? " [multi-layer]" : "";
|
|
3118
|
+
return t("history.entry", {
|
|
3119
|
+
ts: s.timestamp,
|
|
3120
|
+
files: s.totalFiles,
|
|
3121
|
+
edges: s.totalEdges,
|
|
3122
|
+
circular: s.circularDeps,
|
|
3123
|
+
layers
|
|
3124
|
+
});
|
|
3125
|
+
})
|
|
3126
|
+
];
|
|
3127
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
3128
|
+
}
|
|
3129
|
+
let existingSnapshot;
|
|
3130
|
+
if (fromTimestamp) {
|
|
3131
|
+
existingSnapshot = await loadSnapshotByTimestamp(root, fromTimestamp);
|
|
3132
|
+
if (!existingSnapshot) {
|
|
3133
|
+
return {
|
|
3134
|
+
content: [{ type: "text", text: t("history.snapshotNotFound", { ts: fromTimestamp }) }],
|
|
3135
|
+
isError: true
|
|
3136
|
+
};
|
|
3137
|
+
}
|
|
3138
|
+
} else {
|
|
3139
|
+
existingSnapshot = await loadSnapshot(root);
|
|
3140
|
+
}
|
|
2706
3141
|
if (!existingSnapshot) {
|
|
2707
3142
|
const { graph, multiLayer } = await resolveGraphMcp({
|
|
2708
3143
|
targetDir,
|
|
2709
|
-
projectRoot,
|
|
3144
|
+
projectRoot: root,
|
|
2710
3145
|
language
|
|
2711
3146
|
});
|
|
2712
|
-
await saveSnapshot(
|
|
3147
|
+
await saveSnapshot(root, graph, multiLayer, { targetDir, language });
|
|
2713
3148
|
return {
|
|
2714
3149
|
content: [
|
|
2715
3150
|
{
|
|
@@ -2725,7 +3160,7 @@ server.tool(
|
|
|
2725
3160
|
}
|
|
2726
3161
|
const { graph: currentGraph } = await resolveGraphMcp({
|
|
2727
3162
|
targetDir,
|
|
2728
|
-
projectRoot,
|
|
3163
|
+
projectRoot: root,
|
|
2729
3164
|
language
|
|
2730
3165
|
});
|
|
2731
3166
|
const diff = computeDiff(existingSnapshot.graph, currentGraph);
|
|
@@ -2746,18 +3181,14 @@ server.tool(
|
|
|
2746
3181
|
},
|
|
2747
3182
|
async ({ targetDir, projectRoot, language }) => {
|
|
2748
3183
|
try {
|
|
2749
|
-
|
|
2750
|
-
validatePath(
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
});
|
|
2758
|
-
snapshot = await saveSnapshot(projectRoot, graph2, multiLayer);
|
|
2759
|
-
}
|
|
2760
|
-
const graph = snapshot.graph;
|
|
3184
|
+
const root = resolveProjectRoot(projectRoot);
|
|
3185
|
+
validatePath(targetDir, root);
|
|
3186
|
+
const { graph } = await resolveGraphMcp({
|
|
3187
|
+
targetDir,
|
|
3188
|
+
projectRoot: root,
|
|
3189
|
+
language
|
|
3190
|
+
});
|
|
3191
|
+
const existingSnapshot = await loadSnapshot(root);
|
|
2761
3192
|
const keyComponents = Object.values(graph.files).filter((f) => f.dependents.length > 0 || f.dependencies.length > 0).sort((a, b) => b.dependents.length - a.dependents.length).slice(0, 20).map((f) => ({
|
|
2762
3193
|
path: f.path,
|
|
2763
3194
|
dependentCount: f.dependents.length,
|
|
@@ -2769,7 +3200,7 @@ server.tool(
|
|
|
2769
3200
|
t("cli.fileCount", { count: graph.totalFiles }),
|
|
2770
3201
|
t("cli.edgeCount", { count: graph.totalEdges }),
|
|
2771
3202
|
t("cli.circularCount", { count: graph.circularDependencies.length }),
|
|
2772
|
-
t("cli.snapshot", { ts:
|
|
3203
|
+
...existingSnapshot ? [t("cli.snapshot", { ts: existingSnapshot.timestamp })] : [],
|
|
2773
3204
|
"",
|
|
2774
3205
|
t("cli.keyComponents"),
|
|
2775
3206
|
...keyComponents.map(
|
|
@@ -2779,8 +3210,8 @@ server.tool(
|
|
|
2779
3210
|
const context = {
|
|
2780
3211
|
validPaths,
|
|
2781
3212
|
summary,
|
|
2782
|
-
snapshotExists:
|
|
2783
|
-
snapshotTimestamp:
|
|
3213
|
+
snapshotExists: existingSnapshot !== null,
|
|
3214
|
+
snapshotTimestamp: existingSnapshot?.timestamp,
|
|
2784
3215
|
keyComponents
|
|
2785
3216
|
};
|
|
2786
3217
|
return {
|
|
@@ -2812,18 +3243,13 @@ server.tool(
|
|
|
2812
3243
|
},
|
|
2813
3244
|
async ({ query, mode, targetDir, projectRoot, limit, language }) => {
|
|
2814
3245
|
try {
|
|
2815
|
-
|
|
2816
|
-
validatePath(
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
language
|
|
2823
|
-
});
|
|
2824
|
-
snapshot = await saveSnapshot(projectRoot, graph2, multiLayer);
|
|
2825
|
-
}
|
|
2826
|
-
const graph = snapshot.graph;
|
|
3246
|
+
const root = resolveProjectRoot(projectRoot);
|
|
3247
|
+
validatePath(targetDir, root);
|
|
3248
|
+
const { graph } = await resolveGraphMcp({
|
|
3249
|
+
targetDir,
|
|
3250
|
+
projectRoot: root,
|
|
3251
|
+
language
|
|
3252
|
+
});
|
|
2827
3253
|
const maxResults = limit ?? 10;
|
|
2828
3254
|
let results;
|
|
2829
3255
|
if ((mode === "path" || mode === "affected") && !query) {
|