archtracker-mcp 0.5.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 +863 -1809
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.js +171 -109
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.js +629 -189
- package/dist/mcp/index.js.map +1 -1
- package/dist/web/viewer.js +1572 -0
- package/package.json +5 -3
- package/skills/arch-analyze/SKILL.md +2 -0
- package/skills/arch-check/SKILL.md +1 -0
- package/skills/arch-search/SKILL.md +4 -0
- package/skills/arch-snapshot/SKILL.md +1 -0
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";
|
|
@@ -2226,62 +2242,416 @@ function mergeLayerGraphs(projectRoot, layers) {
|
|
|
2226
2242
|
};
|
|
2227
2243
|
}
|
|
2228
2244
|
|
|
2229
|
-
// src/storage/
|
|
2230
|
-
import {
|
|
2245
|
+
// src/storage/layers.ts
|
|
2246
|
+
import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
|
|
2231
2247
|
import { join as join5 } from "path";
|
|
2232
2248
|
import { z } from "zod";
|
|
2249
|
+
var ARCHTRACKER_DIR = ".archtracker";
|
|
2250
|
+
var LAYERS_FILE = "layers.json";
|
|
2251
|
+
var LayerDefinitionSchema = z.object({
|
|
2252
|
+
name: z.string().min(1).regex(
|
|
2253
|
+
/^[a-zA-Z0-9_-]+$/,
|
|
2254
|
+
"Layer name must be alphanumeric (hyphens/underscores allowed)"
|
|
2255
|
+
),
|
|
2256
|
+
targetDir: z.string().min(1),
|
|
2257
|
+
language: z.enum(LANGUAGE_IDS).optional(),
|
|
2258
|
+
exclude: z.array(z.string()).optional(),
|
|
2259
|
+
color: z.string().optional(),
|
|
2260
|
+
description: z.string().optional()
|
|
2261
|
+
});
|
|
2262
|
+
var CrossLayerConnectionSchema = z.object({
|
|
2263
|
+
fromLayer: z.string(),
|
|
2264
|
+
fromFile: z.string(),
|
|
2265
|
+
toLayer: z.string(),
|
|
2266
|
+
toFile: z.string(),
|
|
2267
|
+
type: z.enum(["api-call", "event", "data-flow", "manual"]),
|
|
2268
|
+
label: z.string().optional()
|
|
2269
|
+
});
|
|
2270
|
+
var LayerConfigSchema = z.object({
|
|
2271
|
+
version: z.literal("1.0"),
|
|
2272
|
+
layers: z.array(LayerDefinitionSchema).min(1).refine(
|
|
2273
|
+
(layers) => {
|
|
2274
|
+
const names = layers.map((l) => l.name);
|
|
2275
|
+
return new Set(names).size === names.length;
|
|
2276
|
+
},
|
|
2277
|
+
{ message: "Layer names must be unique" }
|
|
2278
|
+
),
|
|
2279
|
+
connections: z.array(CrossLayerConnectionSchema).optional()
|
|
2280
|
+
});
|
|
2281
|
+
async function loadLayerConfig(projectRoot) {
|
|
2282
|
+
const filePath = join5(projectRoot, ARCHTRACKER_DIR, LAYERS_FILE);
|
|
2283
|
+
let raw;
|
|
2284
|
+
try {
|
|
2285
|
+
raw = await readFile2(filePath, "utf-8");
|
|
2286
|
+
} catch (error) {
|
|
2287
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
2288
|
+
return null;
|
|
2289
|
+
}
|
|
2290
|
+
throw new Error(`Failed to read ${filePath}`);
|
|
2291
|
+
}
|
|
2292
|
+
let parsed;
|
|
2293
|
+
try {
|
|
2294
|
+
parsed = JSON.parse(raw);
|
|
2295
|
+
} catch {
|
|
2296
|
+
throw new Error(`Invalid JSON in ${filePath}`);
|
|
2297
|
+
}
|
|
2298
|
+
const result = LayerConfigSchema.safeParse(parsed);
|
|
2299
|
+
if (!result.success) {
|
|
2300
|
+
const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).slice(0, 5).join("\n");
|
|
2301
|
+
throw new Error(`layers.json validation failed:
|
|
2302
|
+
${issues}`);
|
|
2303
|
+
}
|
|
2304
|
+
return result.data;
|
|
2305
|
+
}
|
|
2306
|
+
function isNodeError(error) {
|
|
2307
|
+
return error instanceof Error && "code" in error;
|
|
2308
|
+
}
|
|
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
|
+
|
|
2499
|
+
// src/analyzer/resolve.ts
|
|
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
|
+
}
|
|
2540
|
+
const layerConfig = await loadLayerConfig(opts.projectRoot);
|
|
2541
|
+
let result;
|
|
2542
|
+
if (layerConfig) {
|
|
2543
|
+
const multi = await analyzeMultiLayer(opts.projectRoot, layerConfig.layers, opts.exclude);
|
|
2544
|
+
const autoConnections = detectCrossLayerConnections(multi.layers, layerConfig.layers);
|
|
2545
|
+
const manualConnections = layerConfig.connections ?? [];
|
|
2546
|
+
const manualKeys = new Set(manualConnections.map(
|
|
2547
|
+
(c) => `${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`
|
|
2548
|
+
));
|
|
2549
|
+
const merged = [
|
|
2550
|
+
...manualConnections,
|
|
2551
|
+
...autoConnections.filter(
|
|
2552
|
+
(c) => !manualKeys.has(`${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`)
|
|
2553
|
+
)
|
|
2554
|
+
];
|
|
2555
|
+
result = {
|
|
2556
|
+
graph: multi.merged,
|
|
2557
|
+
multiLayer: multi,
|
|
2558
|
+
layerMetadata: multi.layerMetadata,
|
|
2559
|
+
crossLayerEdges: merged
|
|
2560
|
+
};
|
|
2561
|
+
} else {
|
|
2562
|
+
const graph = await analyzeProject(opts.targetDir, {
|
|
2563
|
+
exclude: opts.exclude,
|
|
2564
|
+
language: opts.language
|
|
2565
|
+
});
|
|
2566
|
+
result = { graph };
|
|
2567
|
+
}
|
|
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;
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
// src/storage/snapshot.ts
|
|
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";
|
|
2592
|
+
import { z as z2 } from "zod";
|
|
2233
2593
|
|
|
2234
2594
|
// src/types/schema.ts
|
|
2235
2595
|
var SCHEMA_VERSION = "1.1";
|
|
2236
2596
|
|
|
2237
2597
|
// src/storage/snapshot.ts
|
|
2238
|
-
var
|
|
2598
|
+
var ARCHTRACKER_DIR3 = ".archtracker";
|
|
2239
2599
|
var SNAPSHOT_FILE = "snapshot.json";
|
|
2240
|
-
var
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2600
|
+
var HISTORY_DIR = "history";
|
|
2601
|
+
var FileNodeSchema = z2.object({
|
|
2602
|
+
path: z2.string(),
|
|
2603
|
+
exists: z2.boolean(),
|
|
2604
|
+
dependencies: z2.array(z2.string()),
|
|
2605
|
+
dependents: z2.array(z2.string())
|
|
2245
2606
|
});
|
|
2246
|
-
var DependencyGraphSchema =
|
|
2247
|
-
rootDir:
|
|
2248
|
-
files:
|
|
2249
|
-
edges:
|
|
2250
|
-
source:
|
|
2251
|
-
target:
|
|
2252
|
-
type:
|
|
2607
|
+
var DependencyGraphSchema = z2.object({
|
|
2608
|
+
rootDir: z2.string(),
|
|
2609
|
+
files: z2.record(z2.string(), FileNodeSchema),
|
|
2610
|
+
edges: z2.array(z2.object({
|
|
2611
|
+
source: z2.string(),
|
|
2612
|
+
target: z2.string(),
|
|
2613
|
+
type: z2.enum(["static", "dynamic", "type-only"])
|
|
2253
2614
|
})),
|
|
2254
|
-
circularDependencies:
|
|
2255
|
-
totalFiles:
|
|
2256
|
-
totalEdges:
|
|
2615
|
+
circularDependencies: z2.array(z2.object({ cycle: z2.array(z2.string()) })),
|
|
2616
|
+
totalFiles: z2.number(),
|
|
2617
|
+
totalEdges: z2.number()
|
|
2257
2618
|
});
|
|
2258
|
-
var SnapshotSchema =
|
|
2259
|
-
version:
|
|
2260
|
-
timestamp:
|
|
2261
|
-
rootDir:
|
|
2619
|
+
var SnapshotSchema = z2.object({
|
|
2620
|
+
version: z2.enum([SCHEMA_VERSION, "1.0"]),
|
|
2621
|
+
timestamp: z2.string(),
|
|
2622
|
+
rootDir: z2.string(),
|
|
2262
2623
|
graph: DependencyGraphSchema
|
|
2263
2624
|
});
|
|
2264
|
-
async function saveSnapshot(projectRoot, graph, multiLayer) {
|
|
2265
|
-
const dirPath =
|
|
2266
|
-
const filePath =
|
|
2625
|
+
async function saveSnapshot(projectRoot, graph, multiLayer, analysisOptions) {
|
|
2626
|
+
const dirPath = join7(projectRoot, ARCHTRACKER_DIR3);
|
|
2627
|
+
const filePath = join7(dirPath, SNAPSHOT_FILE);
|
|
2267
2628
|
const snapshot = {
|
|
2268
2629
|
version: SCHEMA_VERSION,
|
|
2269
2630
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2270
2631
|
rootDir: graph.rootDir,
|
|
2271
2632
|
graph,
|
|
2272
|
-
...multiLayer ? { multiLayer } : {}
|
|
2633
|
+
...multiLayer ? { multiLayer } : {},
|
|
2634
|
+
...analysisOptions ? { analysisOptions } : {}
|
|
2273
2635
|
};
|
|
2274
|
-
await
|
|
2275
|
-
|
|
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
|
+
}
|
|
2276
2646
|
return snapshot;
|
|
2277
2647
|
}
|
|
2278
2648
|
async function loadSnapshot(projectRoot) {
|
|
2279
|
-
const filePath =
|
|
2649
|
+
const filePath = join7(projectRoot, ARCHTRACKER_DIR3, SNAPSHOT_FILE);
|
|
2280
2650
|
let raw;
|
|
2281
2651
|
try {
|
|
2282
|
-
raw = await
|
|
2652
|
+
raw = await readFile4(filePath, "utf-8");
|
|
2283
2653
|
} catch (error) {
|
|
2284
|
-
if (
|
|
2654
|
+
if (isNodeError2(error) && error.code === "ENOENT") {
|
|
2285
2655
|
return null;
|
|
2286
2656
|
}
|
|
2287
2657
|
throw new StorageError(
|
|
@@ -2306,13 +2676,74 @@ async function loadSnapshot(projectRoot) {
|
|
|
2306
2676
|
}
|
|
2307
2677
|
return result.data;
|
|
2308
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
|
+
}
|
|
2309
2740
|
var StorageError = class extends Error {
|
|
2310
2741
|
constructor(message, options) {
|
|
2311
2742
|
super(message, options);
|
|
2312
2743
|
this.name = "StorageError";
|
|
2313
2744
|
}
|
|
2314
2745
|
};
|
|
2315
|
-
function
|
|
2746
|
+
function isNodeError2(error) {
|
|
2316
2747
|
return error instanceof Error && "code" in error;
|
|
2317
2748
|
}
|
|
2318
2749
|
|
|
@@ -2367,6 +2798,17 @@ function computeDiff(oldGraph, newGraph) {
|
|
|
2367
2798
|
}
|
|
2368
2799
|
return { added, removed, modified, affectedDependents };
|
|
2369
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
|
+
}
|
|
2370
2812
|
function formatDiffReport(diff) {
|
|
2371
2813
|
const lines = [];
|
|
2372
2814
|
lines.push(t("diff.title"));
|
|
@@ -2375,32 +2817,51 @@ function formatDiffReport(diff) {
|
|
|
2375
2817
|
return lines.join("\n");
|
|
2376
2818
|
}
|
|
2377
2819
|
if (diff.added.length > 0) {
|
|
2820
|
+
const [testFiles, srcFiles] = partition(diff.added, isTestOrFixture);
|
|
2378
2821
|
lines.push(t("diff.added", { count: diff.added.length }));
|
|
2379
|
-
for (const f of
|
|
2822
|
+
for (const f of srcFiles) {
|
|
2380
2823
|
lines.push(` + ${f}`);
|
|
2381
2824
|
}
|
|
2825
|
+
if (testFiles.length > 0) {
|
|
2826
|
+
lines.push(t("diff.testSummary", { count: testFiles.length }));
|
|
2827
|
+
}
|
|
2382
2828
|
lines.push("");
|
|
2383
2829
|
}
|
|
2384
2830
|
if (diff.removed.length > 0) {
|
|
2831
|
+
const [testFiles, srcFiles] = partition(diff.removed, isTestOrFixture);
|
|
2385
2832
|
lines.push(t("diff.removed", { count: diff.removed.length }));
|
|
2386
|
-
for (const f of
|
|
2833
|
+
for (const f of srcFiles) {
|
|
2387
2834
|
lines.push(` - ${f}`);
|
|
2388
2835
|
}
|
|
2836
|
+
if (testFiles.length > 0) {
|
|
2837
|
+
lines.push(t("diff.testSummary", { count: testFiles.length }));
|
|
2838
|
+
}
|
|
2389
2839
|
lines.push("");
|
|
2390
2840
|
}
|
|
2391
2841
|
if (diff.modified.length > 0) {
|
|
2842
|
+
const [testFiles, srcFiles] = partition(diff.modified, isTestOrFixture);
|
|
2392
2843
|
lines.push(t("diff.modified", { count: diff.modified.length }));
|
|
2393
|
-
for (const f of
|
|
2844
|
+
for (const f of srcFiles) {
|
|
2394
2845
|
lines.push(` ~ ${f}`);
|
|
2395
2846
|
}
|
|
2847
|
+
if (testFiles.length > 0) {
|
|
2848
|
+
lines.push(t("diff.testSummary", { count: testFiles.length }));
|
|
2849
|
+
}
|
|
2396
2850
|
lines.push("");
|
|
2397
2851
|
}
|
|
2398
2852
|
if (diff.affectedDependents.length > 0) {
|
|
2853
|
+
const [testEntries, srcEntries] = partition(
|
|
2854
|
+
diff.affectedDependents,
|
|
2855
|
+
(a) => isTestOrFixture(a.file) || isTestOrFixture(a.dependsOn)
|
|
2856
|
+
);
|
|
2399
2857
|
lines.push(t("diff.affected", { count: diff.affectedDependents.length }));
|
|
2400
|
-
for (const a of
|
|
2858
|
+
for (const a of srcEntries) {
|
|
2401
2859
|
lines.push(` ! ${a.file}`);
|
|
2402
2860
|
lines.push(` ${a.reason}`);
|
|
2403
2861
|
}
|
|
2862
|
+
if (testEntries.length > 0) {
|
|
2863
|
+
lines.push(t("diff.testAffectedSummary", { count: testEntries.length }));
|
|
2864
|
+
}
|
|
2404
2865
|
lines.push("");
|
|
2405
2866
|
}
|
|
2406
2867
|
return lines.join("\n");
|
|
@@ -2413,76 +2874,11 @@ function arraysEqual(a, b) {
|
|
|
2413
2874
|
return true;
|
|
2414
2875
|
}
|
|
2415
2876
|
|
|
2416
|
-
// src/storage/layers.ts
|
|
2417
|
-
import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
2418
|
-
import { join as join6 } from "path";
|
|
2419
|
-
import { z as z2 } from "zod";
|
|
2420
|
-
var ARCHTRACKER_DIR2 = ".archtracker";
|
|
2421
|
-
var LAYERS_FILE = "layers.json";
|
|
2422
|
-
var LayerDefinitionSchema = z2.object({
|
|
2423
|
-
name: z2.string().min(1).regex(
|
|
2424
|
-
/^[a-zA-Z0-9_-]+$/,
|
|
2425
|
-
"Layer name must be alphanumeric (hyphens/underscores allowed)"
|
|
2426
|
-
),
|
|
2427
|
-
targetDir: z2.string().min(1),
|
|
2428
|
-
language: z2.enum(LANGUAGE_IDS).optional(),
|
|
2429
|
-
exclude: z2.array(z2.string()).optional(),
|
|
2430
|
-
color: z2.string().optional(),
|
|
2431
|
-
description: z2.string().optional()
|
|
2432
|
-
});
|
|
2433
|
-
var CrossLayerConnectionSchema = z2.object({
|
|
2434
|
-
fromLayer: z2.string(),
|
|
2435
|
-
fromFile: z2.string(),
|
|
2436
|
-
toLayer: z2.string(),
|
|
2437
|
-
toFile: z2.string(),
|
|
2438
|
-
type: z2.enum(["api-call", "event", "data-flow", "manual"]),
|
|
2439
|
-
label: z2.string().optional()
|
|
2440
|
-
});
|
|
2441
|
-
var LayerConfigSchema = z2.object({
|
|
2442
|
-
version: z2.literal("1.0"),
|
|
2443
|
-
layers: z2.array(LayerDefinitionSchema).min(1).refine(
|
|
2444
|
-
(layers) => {
|
|
2445
|
-
const names = layers.map((l) => l.name);
|
|
2446
|
-
return new Set(names).size === names.length;
|
|
2447
|
-
},
|
|
2448
|
-
{ message: "Layer names must be unique" }
|
|
2449
|
-
),
|
|
2450
|
-
connections: z2.array(CrossLayerConnectionSchema).optional()
|
|
2451
|
-
});
|
|
2452
|
-
async function loadLayerConfig(projectRoot) {
|
|
2453
|
-
const filePath = join6(projectRoot, ARCHTRACKER_DIR2, LAYERS_FILE);
|
|
2454
|
-
let raw;
|
|
2455
|
-
try {
|
|
2456
|
-
raw = await readFile3(filePath, "utf-8");
|
|
2457
|
-
} catch (error) {
|
|
2458
|
-
if (isNodeError2(error) && error.code === "ENOENT") {
|
|
2459
|
-
return null;
|
|
2460
|
-
}
|
|
2461
|
-
throw new Error(`Failed to read ${filePath}`);
|
|
2462
|
-
}
|
|
2463
|
-
let parsed;
|
|
2464
|
-
try {
|
|
2465
|
-
parsed = JSON.parse(raw);
|
|
2466
|
-
} catch {
|
|
2467
|
-
throw new Error(`Invalid JSON in ${filePath}`);
|
|
2468
|
-
}
|
|
2469
|
-
const result = LayerConfigSchema.safeParse(parsed);
|
|
2470
|
-
if (!result.success) {
|
|
2471
|
-
const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).slice(0, 5).join("\n");
|
|
2472
|
-
throw new Error(`layers.json validation failed:
|
|
2473
|
-
${issues}`);
|
|
2474
|
-
}
|
|
2475
|
-
return result.data;
|
|
2476
|
-
}
|
|
2477
|
-
function isNodeError2(error) {
|
|
2478
|
-
return error instanceof Error && "code" in error;
|
|
2479
|
-
}
|
|
2480
|
-
|
|
2481
2877
|
// src/utils/path-guard.ts
|
|
2482
|
-
import { resolve as
|
|
2878
|
+
import { resolve as resolve6 } from "path";
|
|
2483
2879
|
function validatePath(inputPath, boundary) {
|
|
2484
|
-
const
|
|
2485
|
-
const
|
|
2880
|
+
const root = boundary ? resolve6(boundary) : process.cwd();
|
|
2881
|
+
const resolved = boundary ? resolve6(root, inputPath) : resolve6(inputPath);
|
|
2486
2882
|
if (!resolved.startsWith(root)) {
|
|
2487
2883
|
throw new PathTraversalError(
|
|
2488
2884
|
t("pathGuard.traversal", { input: inputPath, resolved, boundary: root })
|
|
@@ -2490,6 +2886,9 @@ function validatePath(inputPath, boundary) {
|
|
|
2490
2886
|
}
|
|
2491
2887
|
return resolved;
|
|
2492
2888
|
}
|
|
2889
|
+
function resolveProjectRoot(inputPath) {
|
|
2890
|
+
return resolve6(inputPath);
|
|
2891
|
+
}
|
|
2493
2892
|
var PathTraversalError = class extends Error {
|
|
2494
2893
|
constructor(message) {
|
|
2495
2894
|
super(message);
|
|
@@ -2499,13 +2898,13 @@ var PathTraversalError = class extends Error {
|
|
|
2499
2898
|
|
|
2500
2899
|
// src/utils/version.ts
|
|
2501
2900
|
import { readFileSync as readFileSync3 } from "fs";
|
|
2502
|
-
import { join as
|
|
2901
|
+
import { join as join8, dirname as dirname2 } from "path";
|
|
2503
2902
|
import { fileURLToPath } from "url";
|
|
2504
2903
|
function loadVersion() {
|
|
2505
2904
|
let dir = dirname2(fileURLToPath(import.meta.url));
|
|
2506
2905
|
for (let i = 0; i < 5; i++) {
|
|
2507
2906
|
try {
|
|
2508
|
-
const pkg = JSON.parse(readFileSync3(
|
|
2907
|
+
const pkg = JSON.parse(readFileSync3(join8(dir, "package.json"), "utf-8"));
|
|
2509
2908
|
return pkg.version;
|
|
2510
2909
|
} catch {
|
|
2511
2910
|
dir = dirname2(dir);
|
|
@@ -2527,30 +2926,45 @@ var LANG_DISPLAY = {
|
|
|
2527
2926
|
"c-sharp": "C#"
|
|
2528
2927
|
};
|
|
2529
2928
|
var languageList = LANGUAGE_IDS.map((id) => LANG_DISPLAY[id] ?? id.charAt(0).toUpperCase() + id.slice(1)).join(", ");
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
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
|
+
}
|
|
2941
|
+
async function resolveGraphMcp(opts) {
|
|
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;
|
|
2547
2956
|
}
|
|
2957
|
+
memoryCache = { key, result: result2, timestamp: Date.now() };
|
|
2958
|
+
return result2;
|
|
2548
2959
|
}
|
|
2549
|
-
const
|
|
2550
|
-
|
|
2551
|
-
|
|
2960
|
+
const result = await resolveGraph({
|
|
2961
|
+
targetDir: effectiveOpts.targetDir,
|
|
2962
|
+
projectRoot: effectiveOpts.projectRoot,
|
|
2963
|
+
exclude: effectiveOpts.exclude,
|
|
2964
|
+
language: effectiveOpts.language
|
|
2552
2965
|
});
|
|
2553
|
-
|
|
2966
|
+
memoryCache = { key, result, timestamp: Date.now() };
|
|
2967
|
+
return result;
|
|
2554
2968
|
}
|
|
2555
2969
|
function formatLayerSummary(metadata) {
|
|
2556
2970
|
return metadata.map(
|
|
@@ -2564,16 +2978,15 @@ server.tool(
|
|
|
2564
2978
|
targetDir: z3.string().default("src").describe("Target directory path (default: src). When layers.json exists and this is 'src', multi-layer analysis is used automatically."),
|
|
2565
2979
|
projectRoot: z3.string().default(".").describe("Project root (where .archtracker/ is located)"),
|
|
2566
2980
|
exclude: z3.array(z3.string()).optional().describe("Array of regex patterns to exclude (e.g. ['test', 'mock'])"),
|
|
2567
|
-
maxDepth: z3.number().int().min(0).optional().describe("Max analysis depth (0 = unlimited)"),
|
|
2568
2981
|
language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
|
|
2569
2982
|
},
|
|
2570
|
-
async ({ targetDir, projectRoot, exclude,
|
|
2983
|
+
async ({ targetDir, projectRoot, exclude, language }) => {
|
|
2571
2984
|
try {
|
|
2572
|
-
|
|
2573
|
-
validatePath(
|
|
2574
|
-
const { graph, layerMetadata,
|
|
2985
|
+
const root = resolveProjectRoot(projectRoot);
|
|
2986
|
+
validatePath(targetDir, root);
|
|
2987
|
+
const { graph, layerMetadata, crossLayerEdges } = await resolveGraphMcp({
|
|
2575
2988
|
targetDir,
|
|
2576
|
-
projectRoot,
|
|
2989
|
+
projectRoot: root,
|
|
2577
2990
|
exclude,
|
|
2578
2991
|
language
|
|
2579
2992
|
});
|
|
@@ -2581,11 +2994,11 @@ server.tool(
|
|
|
2581
2994
|
t("mcp.analyzeComplete", { files: graph.totalFiles, edges: graph.totalEdges }),
|
|
2582
2995
|
graph.circularDependencies.length > 0 ? t("mcp.circularFound", { count: graph.circularDependencies.length }) : t("mcp.circularNone"),
|
|
2583
2996
|
...layerMetadata ? ["\nLayers:\n" + formatLayerSummary(layerMetadata)] : [],
|
|
2584
|
-
...
|
|
2585
|
-
Cross-layer connections: ${
|
|
2997
|
+
...crossLayerEdges?.length ? [`
|
|
2998
|
+
Cross-layer connections: ${crossLayerEdges.length}`] : []
|
|
2586
2999
|
].join("\n");
|
|
2587
3000
|
const result = { ...graph };
|
|
2588
|
-
if (
|
|
3001
|
+
if (crossLayerEdges?.length) result.crossLayerConnections = crossLayerEdges;
|
|
2589
3002
|
return {
|
|
2590
3003
|
content: [
|
|
2591
3004
|
{ type: "text", text: summary },
|
|
@@ -2599,21 +3012,22 @@ Cross-layer connections: ${crossEdges.length}`] : []
|
|
|
2599
3012
|
);
|
|
2600
3013
|
server.tool(
|
|
2601
3014
|
"analyze_existing_architecture",
|
|
2602
|
-
`Comprehensive architecture analysis for existing projects. Shows critical components, circular dependencies, orphan files, coupling hotspots, and directory breakdown. Supports ${
|
|
3015
|
+
`Comprehensive architecture analysis for existing projects. Shows critical components, circular dependencies, orphan files, coupling hotspots, and directory breakdown. Supports ${languageList}.`,
|
|
2603
3016
|
{
|
|
2604
3017
|
targetDir: z3.string().default("src").describe("Target directory path (default: src)"),
|
|
2605
3018
|
exclude: z3.array(z3.string()).optional().describe("Array of regex patterns to exclude"),
|
|
2606
3019
|
topN: z3.number().int().min(1).max(50).optional().describe("Number of top items to show in each section (default: 10)"),
|
|
2607
3020
|
saveSnapshot: z3.boolean().optional().describe("Also save a snapshot after analysis (default: false)"),
|
|
2608
|
-
projectRoot: z3.string().default(".").describe("Project root (
|
|
3021
|
+
projectRoot: z3.string().default(".").describe("Project root (where .archtracker/ is located)"),
|
|
2609
3022
|
language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
|
|
2610
3023
|
},
|
|
2611
3024
|
async ({ targetDir, exclude, topN, saveSnapshot: doSave, projectRoot, language }) => {
|
|
2612
3025
|
try {
|
|
2613
|
-
|
|
2614
|
-
|
|
3026
|
+
const root = resolveProjectRoot(projectRoot);
|
|
3027
|
+
validatePath(targetDir, root);
|
|
3028
|
+
const { graph, multiLayer, layerMetadata, crossLayerEdges } = await resolveGraphMcp({
|
|
2615
3029
|
targetDir,
|
|
2616
|
-
projectRoot,
|
|
3030
|
+
projectRoot: root,
|
|
2617
3031
|
exclude,
|
|
2618
3032
|
language
|
|
2619
3033
|
});
|
|
@@ -2624,17 +3038,16 @@ server.tool(
|
|
|
2624
3038
|
if (layerMetadata) {
|
|
2625
3039
|
content.push({ type: "text", text: "\nLayers:\n" + formatLayerSummary(layerMetadata) });
|
|
2626
3040
|
}
|
|
2627
|
-
if (
|
|
2628
|
-
const crossSummary =
|
|
3041
|
+
if (crossLayerEdges?.length) {
|
|
3042
|
+
const crossSummary = crossLayerEdges.map(
|
|
2629
3043
|
(c) => ` ${c.fromLayer}/${c.fromFile} \u2192 ${c.toLayer}/${c.toFile} [${c.type}] ${c.label ?? ""}`
|
|
2630
3044
|
).join("\n");
|
|
2631
3045
|
content.push({ type: "text", text: `
|
|
2632
|
-
Cross-layer connections (${
|
|
3046
|
+
Cross-layer connections (${crossLayerEdges.length}):
|
|
2633
3047
|
${crossSummary}` });
|
|
2634
3048
|
}
|
|
2635
3049
|
if (doSave) {
|
|
2636
|
-
|
|
2637
|
-
await saveSnapshot(projectRoot, graph, multiLayer);
|
|
3050
|
+
await saveSnapshot(root, graph, multiLayer, { targetDir, language, exclude });
|
|
2638
3051
|
content.push({ type: "text", text: t("analyze.snapshotSaved") });
|
|
2639
3052
|
}
|
|
2640
3053
|
return { content };
|
|
@@ -2653,14 +3066,14 @@ server.tool(
|
|
|
2653
3066
|
},
|
|
2654
3067
|
async ({ targetDir, projectRoot, language }) => {
|
|
2655
3068
|
try {
|
|
2656
|
-
|
|
2657
|
-
validatePath(
|
|
2658
|
-
const { graph, multiLayer, layerMetadata } = await
|
|
3069
|
+
const root = resolveProjectRoot(projectRoot);
|
|
3070
|
+
validatePath(targetDir, root);
|
|
3071
|
+
const { graph, multiLayer, layerMetadata } = await resolveGraphMcp({
|
|
2659
3072
|
targetDir,
|
|
2660
|
-
projectRoot,
|
|
3073
|
+
projectRoot: root,
|
|
2661
3074
|
language
|
|
2662
3075
|
});
|
|
2663
|
-
const snapshot = await saveSnapshot(
|
|
3076
|
+
const snapshot = await saveSnapshot(root, graph, multiLayer, { targetDir, language });
|
|
2664
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 })}`);
|
|
2665
3078
|
const report = [
|
|
2666
3079
|
t("mcp.snapshotSaved"),
|
|
@@ -2680,24 +3093,58 @@ server.tool(
|
|
|
2680
3093
|
);
|
|
2681
3094
|
server.tool(
|
|
2682
3095
|
"check_architecture_diff",
|
|
2683
|
-
"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.",
|
|
2684
3097
|
{
|
|
2685
3098
|
targetDir: z3.string().default("src").describe("Target directory path"),
|
|
2686
3099
|
projectRoot: z3.string().default(".").describe("Project root (where .archtracker is placed)"),
|
|
2687
|
-
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")
|
|
2688
3103
|
},
|
|
2689
|
-
async ({ targetDir, projectRoot, language }) => {
|
|
3104
|
+
async ({ targetDir, projectRoot, language, fromTimestamp, listHistory }) => {
|
|
2690
3105
|
try {
|
|
2691
|
-
|
|
2692
|
-
validatePath(
|
|
2693
|
-
|
|
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
|
+
}
|
|
2694
3141
|
if (!existingSnapshot) {
|
|
2695
|
-
const { graph, multiLayer } = await
|
|
3142
|
+
const { graph, multiLayer } = await resolveGraphMcp({
|
|
2696
3143
|
targetDir,
|
|
2697
|
-
projectRoot,
|
|
3144
|
+
projectRoot: root,
|
|
2698
3145
|
language
|
|
2699
3146
|
});
|
|
2700
|
-
await saveSnapshot(
|
|
3147
|
+
await saveSnapshot(root, graph, multiLayer, { targetDir, language });
|
|
2701
3148
|
return {
|
|
2702
3149
|
content: [
|
|
2703
3150
|
{
|
|
@@ -2711,9 +3158,9 @@ server.tool(
|
|
|
2711
3158
|
]
|
|
2712
3159
|
};
|
|
2713
3160
|
}
|
|
2714
|
-
const { graph: currentGraph } = await
|
|
3161
|
+
const { graph: currentGraph } = await resolveGraphMcp({
|
|
2715
3162
|
targetDir,
|
|
2716
|
-
projectRoot,
|
|
3163
|
+
projectRoot: root,
|
|
2717
3164
|
language
|
|
2718
3165
|
});
|
|
2719
3166
|
const diff = computeDiff(existingSnapshot.graph, currentGraph);
|
|
@@ -2734,16 +3181,14 @@ server.tool(
|
|
|
2734
3181
|
},
|
|
2735
3182
|
async ({ targetDir, projectRoot, language }) => {
|
|
2736
3183
|
try {
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
}
|
|
2746
|
-
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);
|
|
2747
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) => ({
|
|
2748
3193
|
path: f.path,
|
|
2749
3194
|
dependentCount: f.dependents.length,
|
|
@@ -2755,7 +3200,7 @@ server.tool(
|
|
|
2755
3200
|
t("cli.fileCount", { count: graph.totalFiles }),
|
|
2756
3201
|
t("cli.edgeCount", { count: graph.totalEdges }),
|
|
2757
3202
|
t("cli.circularCount", { count: graph.circularDependencies.length }),
|
|
2758
|
-
t("cli.snapshot", { ts:
|
|
3203
|
+
...existingSnapshot ? [t("cli.snapshot", { ts: existingSnapshot.timestamp })] : [],
|
|
2759
3204
|
"",
|
|
2760
3205
|
t("cli.keyComponents"),
|
|
2761
3206
|
...keyComponents.map(
|
|
@@ -2765,8 +3210,8 @@ server.tool(
|
|
|
2765
3210
|
const context = {
|
|
2766
3211
|
validPaths,
|
|
2767
3212
|
summary,
|
|
2768
|
-
snapshotExists:
|
|
2769
|
-
snapshotTimestamp:
|
|
3213
|
+
snapshotExists: existingSnapshot !== null,
|
|
3214
|
+
snapshotTimestamp: existingSnapshot?.timestamp,
|
|
2770
3215
|
keyComponents
|
|
2771
3216
|
};
|
|
2772
3217
|
return {
|
|
@@ -2798,18 +3243,13 @@ server.tool(
|
|
|
2798
3243
|
},
|
|
2799
3244
|
async ({ query, mode, targetDir, projectRoot, limit, language }) => {
|
|
2800
3245
|
try {
|
|
2801
|
-
|
|
2802
|
-
validatePath(
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
language
|
|
2809
|
-
});
|
|
2810
|
-
snapshot = await saveSnapshot(projectRoot, graph2, multiLayer);
|
|
2811
|
-
}
|
|
2812
|
-
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
|
+
});
|
|
2813
3253
|
const maxResults = limit ?? 10;
|
|
2814
3254
|
let results;
|
|
2815
3255
|
if ((mode === "path" || mode === "affected") && !query) {
|