archtracker-mcp 0.4.3 → 0.6.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.md +189 -14
- package/dist/bin.js +1 -1
- package/dist/bin.js.map +1 -1
- package/dist/cli/index.js +2092 -464
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +94 -4
- package/dist/index.js +604 -44
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.js +713 -89
- package/dist/mcp/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/arch-analyze/SKILL.md +8 -2
- package/skills/arch-check/SKILL.md +12 -8
- package/skills/arch-context/SKILL.md +8 -6
- package/skills/arch-search/SKILL.md +11 -7
- package/skills/arch-serve/SKILL.md +27 -0
- package/skills/arch-snapshot/SKILL.md +10 -6
package/dist/mcp/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/mcp/index.ts
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
-
import { z as
|
|
6
|
+
import { z as z3 } from "zod";
|
|
7
7
|
|
|
8
8
|
// src/analyzer/analyze.ts
|
|
9
9
|
import { resolve as resolve3 } from "path";
|
|
@@ -1087,6 +1087,13 @@ var java = {
|
|
|
1087
1087
|
if (projectFiles.has(full)) return full;
|
|
1088
1088
|
}
|
|
1089
1089
|
}
|
|
1090
|
+
for (let i = 1; i < segments.length; i++) {
|
|
1091
|
+
const filePath = segments.slice(i).join("/") + ".java";
|
|
1092
|
+
for (const srcRoot of ["", "src/main/java/", "src/", "app/src/main/java/"]) {
|
|
1093
|
+
const full = join3(rootDir, srcRoot, filePath);
|
|
1094
|
+
if (projectFiles.has(full)) return full;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1090
1097
|
return null;
|
|
1091
1098
|
},
|
|
1092
1099
|
defaultExclude: ["build", "target", "\\.gradle", "\\.idea"]
|
|
@@ -1141,12 +1148,18 @@ var php = {
|
|
|
1141
1148
|
importPatterns: [
|
|
1142
1149
|
// require/include/require_once/include_once 'path'
|
|
1143
1150
|
{ regex: /\b(?:require|include)(?:_once)?\s+['"]([^'"]+)['"]/gm },
|
|
1151
|
+
// require_once __DIR__ . '/path' (common PHP pattern)
|
|
1152
|
+
{ regex: /\b(?:require|include)(?:_once)?\s+__DIR__\s*\.\s*['"]([^'"]+)['"]/gm },
|
|
1144
1153
|
// Bug #9 fix: use Namespace\Class — skip `function` and `const` keywords
|
|
1145
1154
|
{ regex: /^use\s+(?:function\s+|const\s+)?([\w\\]+)/gm }
|
|
1146
1155
|
],
|
|
1147
1156
|
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
1148
|
-
|
|
1149
|
-
|
|
1157
|
+
let normalizedPath = importPath;
|
|
1158
|
+
if (normalizedPath.startsWith("/")) {
|
|
1159
|
+
normalizedPath = normalizedPath.slice(1);
|
|
1160
|
+
}
|
|
1161
|
+
if (normalizedPath.includes("/") || normalizedPath.endsWith(".php")) {
|
|
1162
|
+
const withExt = normalizedPath.endsWith(".php") ? normalizedPath : normalizedPath + ".php";
|
|
1150
1163
|
const fromSource = resolve2(dirname(sourceFile), withExt);
|
|
1151
1164
|
if (projectFiles.has(fromSource)) return fromSource;
|
|
1152
1165
|
const fromRoot2 = join3(rootDir, withExt);
|
|
@@ -1162,15 +1175,46 @@ var php = {
|
|
|
1162
1175
|
},
|
|
1163
1176
|
defaultExclude: ["vendor"]
|
|
1164
1177
|
};
|
|
1178
|
+
var SWIFT_SKIP_FILES = /* @__PURE__ */ new Set(["Package", "main", "AppDelegate", "SceneDelegate"]);
|
|
1165
1179
|
var swift = {
|
|
1166
1180
|
id: "swift",
|
|
1167
1181
|
extensions: [".swift"],
|
|
1168
1182
|
commentStyle: "c-style",
|
|
1169
|
-
importPatterns: [
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1183
|
+
importPatterns: [],
|
|
1184
|
+
// handled by extractImports
|
|
1185
|
+
extractImports(content, filePath, _rootDir, projectFiles) {
|
|
1186
|
+
const imports = [];
|
|
1187
|
+
const moduleRegex = /^(?:@testable\s+)?import\s+(?:class\s+|struct\s+|enum\s+|protocol\s+|func\s+|var\s+|let\s+|typealias\s+)?(\w+)/gm;
|
|
1188
|
+
let match;
|
|
1189
|
+
while ((match = moduleRegex.exec(content)) !== null) {
|
|
1190
|
+
imports.push(match[1]);
|
|
1191
|
+
}
|
|
1192
|
+
const typeMap = /* @__PURE__ */ new Map();
|
|
1193
|
+
for (const f of projectFiles) {
|
|
1194
|
+
if (f === filePath || !f.endsWith(".swift")) continue;
|
|
1195
|
+
const basename = f.split("/").pop().replace(/\.swift$/, "");
|
|
1196
|
+
if (!basename || SWIFT_SKIP_FILES.has(basename)) continue;
|
|
1197
|
+
typeMap.set(basename, f);
|
|
1198
|
+
}
|
|
1199
|
+
if (typeMap.size > 0) {
|
|
1200
|
+
const escaped = [...typeMap.keys()].map(
|
|
1201
|
+
(n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
1202
|
+
);
|
|
1203
|
+
const combined = new RegExp(`\\b(${escaped.join("|")})\\b`, "g");
|
|
1204
|
+
const matched = /* @__PURE__ */ new Set();
|
|
1205
|
+
while ((match = combined.exec(content)) !== null) {
|
|
1206
|
+
const typeName = match[1];
|
|
1207
|
+
const targetPath = typeMap.get(typeName);
|
|
1208
|
+
if (targetPath && !matched.has(targetPath)) {
|
|
1209
|
+
matched.add(targetPath);
|
|
1210
|
+
imports.push(targetPath);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return imports;
|
|
1215
|
+
},
|
|
1216
|
+
resolveImport(importPath, _sourceFile, rootDir, projectFiles) {
|
|
1217
|
+
if (projectFiles.has(importPath)) return importPath;
|
|
1174
1218
|
const spmDir = join3(rootDir, "Sources", importPath);
|
|
1175
1219
|
for (const f of projectFiles) {
|
|
1176
1220
|
if (f.startsWith(spmDir + "/") && f.endsWith(".swift")) return f;
|
|
@@ -1195,7 +1239,8 @@ var kotlin = {
|
|
|
1195
1239
|
if (cleanPath.endsWith(".")) {
|
|
1196
1240
|
cleanPath = cleanPath.slice(0, -1);
|
|
1197
1241
|
}
|
|
1198
|
-
const
|
|
1242
|
+
const segments = cleanPath.split(".");
|
|
1243
|
+
const filePath = segments.join("/");
|
|
1199
1244
|
for (const ext of [".kt", ".kts"]) {
|
|
1200
1245
|
for (const srcRoot of [
|
|
1201
1246
|
"",
|
|
@@ -1209,6 +1254,22 @@ var kotlin = {
|
|
|
1209
1254
|
if (projectFiles.has(full)) return full;
|
|
1210
1255
|
}
|
|
1211
1256
|
}
|
|
1257
|
+
for (let i = 1; i < segments.length; i++) {
|
|
1258
|
+
const suffixPath = segments.slice(i).join("/");
|
|
1259
|
+
for (const ext of [".kt", ".kts"]) {
|
|
1260
|
+
for (const srcRoot of [
|
|
1261
|
+
"",
|
|
1262
|
+
"src/main/kotlin/",
|
|
1263
|
+
"src/main/java/",
|
|
1264
|
+
"src/",
|
|
1265
|
+
"app/src/main/kotlin/",
|
|
1266
|
+
"app/src/main/java/"
|
|
1267
|
+
]) {
|
|
1268
|
+
const full = join3(rootDir, srcRoot, suffixPath + ext);
|
|
1269
|
+
if (projectFiles.has(full)) return full;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1212
1273
|
return null;
|
|
1213
1274
|
},
|
|
1214
1275
|
defaultExclude: ["build", "\\.gradle", "\\.idea"]
|
|
@@ -1294,8 +1355,10 @@ var dart = {
|
|
|
1294
1355
|
const prefix = `package:${ownPackage}/`;
|
|
1295
1356
|
if (!importPath.startsWith(prefix)) return null;
|
|
1296
1357
|
const relPath = importPath.slice(prefix.length);
|
|
1297
|
-
const
|
|
1298
|
-
if (projectFiles.has(
|
|
1358
|
+
const libPath = join3(rootDir, "lib", relPath);
|
|
1359
|
+
if (projectFiles.has(libPath)) return libPath;
|
|
1360
|
+
const rootPath = join3(rootDir, relPath);
|
|
1361
|
+
if (projectFiles.has(rootPath)) return rootPath;
|
|
1299
1362
|
return null;
|
|
1300
1363
|
}
|
|
1301
1364
|
const resolved = resolve2(dirname(sourceFile), importPath);
|
|
@@ -1358,6 +1421,15 @@ var scala = {
|
|
|
1358
1421
|
}
|
|
1359
1422
|
}
|
|
1360
1423
|
}
|
|
1424
|
+
for (let i = 1; i < segments.length; i++) {
|
|
1425
|
+
const suffixPath = segments.slice(i).join("/");
|
|
1426
|
+
for (const ext of [".scala", ".sc"]) {
|
|
1427
|
+
for (const srcRoot of ["", "src/main/scala/", "src/", "app/"]) {
|
|
1428
|
+
const full = join3(rootDir, srcRoot, suffixPath + ext);
|
|
1429
|
+
if (projectFiles.has(full)) return full;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1361
1433
|
return null;
|
|
1362
1434
|
},
|
|
1363
1435
|
defaultExclude: ["target", "\\.bsp", "\\.metals", "\\.bloop"]
|
|
@@ -1539,6 +1611,11 @@ var en = {
|
|
|
1539
1611
|
"analyze.snapshotSaved": "\nSnapshot saved alongside analysis.",
|
|
1540
1612
|
// CI
|
|
1541
1613
|
"ci.generated": "GitHub Actions workflow generated: {path}",
|
|
1614
|
+
// Layers
|
|
1615
|
+
"layers.alreadyExists": "layers.json already exists. Edit it manually to modify.",
|
|
1616
|
+
"layers.created": "Created .archtracker/layers.json \u2014 edit it to configure your layers.",
|
|
1617
|
+
"layers.notFound": "No .archtracker/layers.json found. Run 'archtracker layers init' to create one.",
|
|
1618
|
+
"layers.header": "Configured layers ({count}):",
|
|
1542
1619
|
// Web viewer
|
|
1543
1620
|
"web.starting": "Starting architecture viewer...",
|
|
1544
1621
|
"web.listening": "Architecture graph available at: http://localhost:{port}",
|
|
@@ -1627,6 +1704,11 @@ var ja = {
|
|
|
1627
1704
|
"analyze.snapshotSaved": "\n\u5206\u6790\u3068\u540C\u6642\u306B\u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8\u3092\u4FDD\u5B58\u3057\u307E\u3057\u305F\u3002",
|
|
1628
1705
|
// CI
|
|
1629
1706
|
"ci.generated": "GitHub Actions \u30EF\u30FC\u30AF\u30D5\u30ED\u30FC\u3092\u751F\u6210\u3057\u307E\u3057\u305F: {path}",
|
|
1707
|
+
// Layers
|
|
1708
|
+
"layers.alreadyExists": "layers.json \u306F\u65E2\u306B\u5B58\u5728\u3057\u307E\u3059\u3002\u76F4\u63A5\u7DE8\u96C6\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
1709
|
+
"layers.created": ".archtracker/layers.json \u3092\u4F5C\u6210\u3057\u307E\u3057\u305F\u3002\u30EC\u30A4\u30E4\u30FC\u3092\u8A2D\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
1710
|
+
"layers.notFound": ".archtracker/layers.json \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002'archtracker layers init' \u3067\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
1711
|
+
"layers.header": "\u8A2D\u5B9A\u6E08\u307F\u30EC\u30A4\u30E4\u30FC ({count}\u4EF6):",
|
|
1630
1712
|
// Web viewer
|
|
1631
1713
|
"web.starting": "\u30A2\u30FC\u30AD\u30C6\u30AF\u30C1\u30E3\u30D3\u30E5\u30FC\u30A2\u30FC\u3092\u8D77\u52D5\u4E2D...",
|
|
1632
1714
|
"web.listening": "\u30A2\u30FC\u30AD\u30C6\u30AF\u30C1\u30E3\u30B0\u30E9\u30D5: http://localhost:{port}",
|
|
@@ -1761,61 +1843,540 @@ function formatAnalysisReport(graph, options = {}) {
|
|
|
1761
1843
|
return lines.join("\n");
|
|
1762
1844
|
}
|
|
1763
1845
|
|
|
1764
|
-
// src/
|
|
1765
|
-
import {
|
|
1766
|
-
import {
|
|
1846
|
+
// src/analyzer/multi-layer.ts
|
|
1847
|
+
import { resolve as resolve4, join as join4 } from "path";
|
|
1848
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1849
|
+
var LAYER_COLORS = [
|
|
1850
|
+
"#58a6ff",
|
|
1851
|
+
"#3fb950",
|
|
1852
|
+
"#d2a8ff",
|
|
1853
|
+
"#f0883e",
|
|
1854
|
+
"#79c0ff",
|
|
1855
|
+
"#56d4dd",
|
|
1856
|
+
"#db61a2",
|
|
1857
|
+
"#f778ba",
|
|
1858
|
+
"#ffa657",
|
|
1859
|
+
"#7ee787"
|
|
1860
|
+
];
|
|
1861
|
+
async function analyzeMultiLayer(projectRoot, layerDefs) {
|
|
1862
|
+
const layers = {};
|
|
1863
|
+
const layerMetadata = [];
|
|
1864
|
+
for (let idx = 0; idx < layerDefs.length; idx++) {
|
|
1865
|
+
const def = layerDefs[idx];
|
|
1866
|
+
const targetDir = resolve4(projectRoot, def.targetDir);
|
|
1867
|
+
const graph = await analyzeProject(targetDir, {
|
|
1868
|
+
exclude: def.exclude,
|
|
1869
|
+
language: def.language
|
|
1870
|
+
});
|
|
1871
|
+
const language = def.language ?? await detectLanguage(targetDir) ?? "javascript";
|
|
1872
|
+
layers[def.name] = graph;
|
|
1873
|
+
layerMetadata.push({
|
|
1874
|
+
name: def.name,
|
|
1875
|
+
originalRootDir: graph.rootDir,
|
|
1876
|
+
language,
|
|
1877
|
+
color: def.color ?? LAYER_COLORS[idx % LAYER_COLORS.length],
|
|
1878
|
+
description: def.description,
|
|
1879
|
+
fileCount: graph.totalFiles,
|
|
1880
|
+
edgeCount: graph.totalEdges
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
const merged = mergeLayerGraphs(projectRoot, layers);
|
|
1884
|
+
return { layers, layerMetadata, merged };
|
|
1885
|
+
}
|
|
1886
|
+
function detectCrossLayerConnections(layers, layerDefs) {
|
|
1887
|
+
const MIN_NAME_LENGTH = 6;
|
|
1888
|
+
const MIN_SCORE_THRESHOLD = 10;
|
|
1889
|
+
const layerIdentifiers = /* @__PURE__ */ new Map();
|
|
1890
|
+
for (const [layerName, graph] of Object.entries(layers)) {
|
|
1891
|
+
const identifiers = /* @__PURE__ */ new Map();
|
|
1892
|
+
for (const filePath of Object.keys(graph.files)) {
|
|
1893
|
+
const basename = filePath.split("/").pop();
|
|
1894
|
+
const nameNoExt = basename.replace(/\.[^.]+$/, "");
|
|
1895
|
+
if (nameNoExt.length < MIN_NAME_LENGTH || GENERIC_BASENAMES.has(nameNoExt.toLowerCase())) continue;
|
|
1896
|
+
identifiers.set(nameNoExt, filePath);
|
|
1897
|
+
}
|
|
1898
|
+
layerIdentifiers.set(layerName, identifiers);
|
|
1899
|
+
}
|
|
1900
|
+
const nameLayerCount = /* @__PURE__ */ new Map();
|
|
1901
|
+
for (const [, ids] of layerIdentifiers) {
|
|
1902
|
+
for (const name of ids.keys()) {
|
|
1903
|
+
nameLayerCount.set(name, (nameLayerCount.get(name) ?? 0) + 1);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
const pairBest = /* @__PURE__ */ new Map();
|
|
1907
|
+
function tryAdd(pairKey, conn, score) {
|
|
1908
|
+
if (score < MIN_SCORE_THRESHOLD) return;
|
|
1909
|
+
const existing = pairBest.get(pairKey);
|
|
1910
|
+
if (!existing || score > existing.score) {
|
|
1911
|
+
pairBest.set(pairKey, { conn, score });
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
function isSelfDefined(content, name) {
|
|
1915
|
+
const defPatterns = [
|
|
1916
|
+
new RegExp(`\\b(?:class|struct|enum|interface|protocol|type|object)\\s+${escapeRegex(name)}\\b`),
|
|
1917
|
+
new RegExp(`\\b(?:def|func|fun|fn)\\s+${escapeRegex(name)}\\b`),
|
|
1918
|
+
new RegExp(`\\b${escapeRegex(name)}\\s*=\\s*(?:class|struct|type|interface)\\b`)
|
|
1919
|
+
];
|
|
1920
|
+
return defPatterns.some((re) => re.test(content));
|
|
1921
|
+
}
|
|
1922
|
+
function isLocalImportOnly(content, name) {
|
|
1923
|
+
const regex = new RegExp(`\\b${escapeRegex(name)}\\b`, "g");
|
|
1924
|
+
const lines = content.split("\n");
|
|
1925
|
+
let crossLayerRef = false;
|
|
1926
|
+
for (const line of lines) {
|
|
1927
|
+
if (!regex.test(line)) continue;
|
|
1928
|
+
regex.lastIndex = 0;
|
|
1929
|
+
const isLocalImport = /^\s*(?:from\s+[.'"]|import\s+[.'"]|require\s*\(\s*['"][.\/]|#include\s*")/.test(line);
|
|
1930
|
+
if (!isLocalImport) {
|
|
1931
|
+
crossLayerRef = true;
|
|
1932
|
+
break;
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
return !crossLayerRef;
|
|
1936
|
+
}
|
|
1937
|
+
for (const [sourceLayer, graph] of Object.entries(layers)) {
|
|
1938
|
+
const ownNames = layerIdentifiers.get(sourceLayer) ?? /* @__PURE__ */ new Map();
|
|
1939
|
+
for (const filePath of Object.keys(graph.files)) {
|
|
1940
|
+
const absPath = join4(graph.rootDir, filePath);
|
|
1941
|
+
let content;
|
|
1942
|
+
try {
|
|
1943
|
+
content = readFileSync2(absPath, "utf-8");
|
|
1944
|
+
} catch {
|
|
1945
|
+
continue;
|
|
1946
|
+
}
|
|
1947
|
+
for (const [targetLayer, targetIds] of layerIdentifiers) {
|
|
1948
|
+
if (targetLayer === sourceLayer) continue;
|
|
1949
|
+
for (const [targetName, targetFile] of targetIds) {
|
|
1950
|
+
if (ownNames.has(targetName)) continue;
|
|
1951
|
+
if ((nameLayerCount.get(targetName) ?? 0) > 1) continue;
|
|
1952
|
+
if (!content.includes(targetName)) continue;
|
|
1953
|
+
const regex = new RegExp(`\\b${escapeRegex(targetName)}\\b`);
|
|
1954
|
+
if (!regex.test(content)) continue;
|
|
1955
|
+
if (isSelfDefined(content, targetName)) continue;
|
|
1956
|
+
if (isLocalImportOnly(content, targetName)) continue;
|
|
1957
|
+
const pairKey = `${sourceLayer}\u2192${targetLayer}`;
|
|
1958
|
+
const isPascalCase = /^[A-Z][a-z]/.test(targetName);
|
|
1959
|
+
const baseScore = targetName.length + (isPascalCase ? 5 : 0);
|
|
1960
|
+
tryAdd(pairKey, {
|
|
1961
|
+
fromLayer: sourceLayer,
|
|
1962
|
+
fromFile: filePath,
|
|
1963
|
+
toLayer: targetLayer,
|
|
1964
|
+
toFile: targetFile,
|
|
1965
|
+
type: "auto",
|
|
1966
|
+
label: targetName
|
|
1967
|
+
}, baseScore);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
for (const def of layerDefs) {
|
|
1971
|
+
if (def.name === sourceLayer) continue;
|
|
1972
|
+
const pairKey = `${sourceLayer}\u2192${def.name}`;
|
|
1973
|
+
const layerName = def.name;
|
|
1974
|
+
const suffixes = ["Client", "Service", "API", "Handler", "Provider", "Manager", "Gateway", "Proxy", "Adapter", "Connector"];
|
|
1975
|
+
const typedRe = new RegExp(`\\b${escapeRegex(layerName)}(?:${suffixes.join("|")})\\b`);
|
|
1976
|
+
if (typedRe.test(content)) {
|
|
1977
|
+
const targetGraph = layers[def.name];
|
|
1978
|
+
if (!targetGraph) continue;
|
|
1979
|
+
const entryFile = findEntryPoint(targetGraph);
|
|
1980
|
+
if (entryFile) {
|
|
1981
|
+
tryAdd(pairKey, {
|
|
1982
|
+
fromLayer: sourceLayer,
|
|
1983
|
+
fromFile: filePath,
|
|
1984
|
+
toLayer: def.name,
|
|
1985
|
+
toFile: entryFile,
|
|
1986
|
+
type: "auto",
|
|
1987
|
+
label: `${layerName}*`
|
|
1988
|
+
}, 25);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
for (const def of layerDefs) {
|
|
1993
|
+
if (def.name === sourceLayer) continue;
|
|
1994
|
+
const pairKey = `${sourceLayer}\u2192${def.name}`;
|
|
1995
|
+
const dirName = def.targetDir.split("/").pop();
|
|
1996
|
+
const isShortName = dirName.length <= 4;
|
|
1997
|
+
const patterns = [];
|
|
1998
|
+
if (!isShortName) {
|
|
1999
|
+
patterns.push({ re: new RegExp(`(?:from|require|import)\\s+['"].*\\b${escapeRegex(dirName)}\\b`, "i"), score: 15 });
|
|
2000
|
+
patterns.push({ re: new RegExp(`['"\`/]${escapeRegex(dirName)}/[\\w]`, "i"), score: 12 });
|
|
2001
|
+
} else {
|
|
2002
|
+
patterns.push({ re: new RegExp(`(?:from|require|import)\\s+['"].*/${escapeRegex(dirName)}/`, "i"), score: 13 });
|
|
2003
|
+
patterns.push({ re: new RegExp(`['"\`]\\s*(?:https?://[^'"]*)?/${escapeRegex(dirName)}/[\\w]`, "i"), score: 11 });
|
|
2004
|
+
}
|
|
2005
|
+
for (const { re, score } of patterns) {
|
|
2006
|
+
if (re.test(content)) {
|
|
2007
|
+
const targetGraph = layers[def.name];
|
|
2008
|
+
if (!targetGraph) continue;
|
|
2009
|
+
const entryFile = findEntryPoint(targetGraph);
|
|
2010
|
+
if (entryFile) {
|
|
2011
|
+
tryAdd(pairKey, {
|
|
2012
|
+
fromLayer: sourceLayer,
|
|
2013
|
+
fromFile: filePath,
|
|
2014
|
+
toLayer: def.name,
|
|
2015
|
+
toFile: entryFile,
|
|
2016
|
+
type: "auto",
|
|
2017
|
+
label: `\u2192 ${def.name}`
|
|
2018
|
+
}, score);
|
|
2019
|
+
}
|
|
2020
|
+
break;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
return [...pairBest.values()].map((v) => v.conn);
|
|
2027
|
+
}
|
|
2028
|
+
function escapeRegex(s) {
|
|
2029
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2030
|
+
}
|
|
2031
|
+
function findEntryPoint(graph) {
|
|
2032
|
+
const files = Object.values(graph.files);
|
|
2033
|
+
if (files.length === 0) return null;
|
|
2034
|
+
const sorted = files.sort((a, b) => b.dependents.length - a.dependents.length);
|
|
2035
|
+
if (sorted[0].dependents.length > 0) return sorted[0].path;
|
|
2036
|
+
const entryNames = ["main", "index", "app", "server", "lib", "mod"];
|
|
2037
|
+
for (const name of entryNames) {
|
|
2038
|
+
const entry = files.find((f) => {
|
|
2039
|
+
const basename = f.path.split("/").pop().replace(/\.[^.]+$/, "").toLowerCase();
|
|
2040
|
+
return basename === name;
|
|
2041
|
+
});
|
|
2042
|
+
if (entry) return entry.path;
|
|
2043
|
+
}
|
|
2044
|
+
return files[0].path;
|
|
2045
|
+
}
|
|
2046
|
+
var GENERIC_BASENAMES = /* @__PURE__ */ new Set([
|
|
2047
|
+
// Build / project structure
|
|
2048
|
+
"index",
|
|
2049
|
+
"main",
|
|
2050
|
+
"app",
|
|
2051
|
+
"config",
|
|
2052
|
+
"setup",
|
|
2053
|
+
"init",
|
|
2054
|
+
"mod",
|
|
2055
|
+
"package",
|
|
2056
|
+
"build",
|
|
2057
|
+
"makefile",
|
|
2058
|
+
"dockerfile",
|
|
2059
|
+
"rakefile",
|
|
2060
|
+
"gemfile",
|
|
2061
|
+
"podfile",
|
|
2062
|
+
// Common modules
|
|
2063
|
+
"utils",
|
|
2064
|
+
"helpers",
|
|
2065
|
+
"types",
|
|
2066
|
+
"models",
|
|
2067
|
+
"views",
|
|
2068
|
+
"controllers",
|
|
2069
|
+
"services",
|
|
2070
|
+
"lib",
|
|
2071
|
+
"src",
|
|
2072
|
+
"test",
|
|
2073
|
+
"spec",
|
|
2074
|
+
"tests",
|
|
2075
|
+
"bench",
|
|
2076
|
+
"example",
|
|
2077
|
+
"examples",
|
|
2078
|
+
// Infrastructure / patterns
|
|
2079
|
+
"server",
|
|
2080
|
+
"client",
|
|
2081
|
+
"routes",
|
|
2082
|
+
"middleware",
|
|
2083
|
+
"database",
|
|
2084
|
+
"engine",
|
|
2085
|
+
"error",
|
|
2086
|
+
"errors",
|
|
2087
|
+
"logger",
|
|
2088
|
+
"logging",
|
|
2089
|
+
"constants",
|
|
2090
|
+
"common",
|
|
2091
|
+
"base",
|
|
2092
|
+
"core",
|
|
2093
|
+
"data",
|
|
2094
|
+
"manager",
|
|
2095
|
+
"handler",
|
|
2096
|
+
"factory",
|
|
2097
|
+
"context",
|
|
2098
|
+
"state",
|
|
2099
|
+
"store",
|
|
2100
|
+
"cache",
|
|
2101
|
+
"queue",
|
|
2102
|
+
"task",
|
|
2103
|
+
"worker",
|
|
2104
|
+
"adapter",
|
|
2105
|
+
"bridge",
|
|
2106
|
+
// UI / presentation
|
|
2107
|
+
"event",
|
|
2108
|
+
"events",
|
|
2109
|
+
"model",
|
|
2110
|
+
"view",
|
|
2111
|
+
"home",
|
|
2112
|
+
"user",
|
|
2113
|
+
"page",
|
|
2114
|
+
"layout",
|
|
2115
|
+
"router",
|
|
2116
|
+
"provider",
|
|
2117
|
+
"component",
|
|
2118
|
+
"widget",
|
|
2119
|
+
"screen",
|
|
2120
|
+
"template",
|
|
2121
|
+
"header",
|
|
2122
|
+
"footer",
|
|
2123
|
+
"sidebar",
|
|
2124
|
+
"navbar",
|
|
2125
|
+
"dialog",
|
|
2126
|
+
"modal",
|
|
2127
|
+
"panel",
|
|
2128
|
+
// Data / IO
|
|
2129
|
+
"reader",
|
|
2130
|
+
"writer",
|
|
2131
|
+
"parser",
|
|
2132
|
+
"formatter",
|
|
2133
|
+
"serializer",
|
|
2134
|
+
"converter",
|
|
2135
|
+
"loader",
|
|
2136
|
+
"exporter",
|
|
2137
|
+
"importer",
|
|
2138
|
+
"transformer",
|
|
2139
|
+
"mapper",
|
|
2140
|
+
"reducer",
|
|
2141
|
+
"filter",
|
|
2142
|
+
"sorter",
|
|
2143
|
+
"validator",
|
|
2144
|
+
"checker",
|
|
2145
|
+
"scanner",
|
|
2146
|
+
"analyzer",
|
|
2147
|
+
// Auth / Security (generic enough to exist in many layers)
|
|
2148
|
+
"login",
|
|
2149
|
+
"register",
|
|
2150
|
+
"verify",
|
|
2151
|
+
"token",
|
|
2152
|
+
"session",
|
|
2153
|
+
"credential",
|
|
2154
|
+
"password",
|
|
2155
|
+
"permission",
|
|
2156
|
+
"profile",
|
|
2157
|
+
"account",
|
|
2158
|
+
"settings",
|
|
2159
|
+
// Network / API
|
|
2160
|
+
"request",
|
|
2161
|
+
"response",
|
|
2162
|
+
"endpoint",
|
|
2163
|
+
"controller",
|
|
2164
|
+
"service",
|
|
2165
|
+
"gateway",
|
|
2166
|
+
"proxy",
|
|
2167
|
+
"connector",
|
|
2168
|
+
"socket",
|
|
2169
|
+
"channel",
|
|
2170
|
+
"stream",
|
|
2171
|
+
"pipeline",
|
|
2172
|
+
// Storage / DB
|
|
2173
|
+
"schema",
|
|
2174
|
+
"migration",
|
|
2175
|
+
"seed",
|
|
2176
|
+
"fixture",
|
|
2177
|
+
"record",
|
|
2178
|
+
"entity",
|
|
2179
|
+
"repository",
|
|
2180
|
+
"storage",
|
|
2181
|
+
"driver",
|
|
2182
|
+
"connection",
|
|
2183
|
+
"pool",
|
|
2184
|
+
// Testing
|
|
2185
|
+
"mock",
|
|
2186
|
+
"stub",
|
|
2187
|
+
"fake",
|
|
2188
|
+
"helper",
|
|
2189
|
+
"fixture",
|
|
2190
|
+
"factory"
|
|
2191
|
+
]);
|
|
2192
|
+
function mergeLayerGraphs(projectRoot, layers) {
|
|
2193
|
+
const mergedFiles = {};
|
|
2194
|
+
const mergedEdges = [];
|
|
2195
|
+
const mergedCircular = [];
|
|
2196
|
+
for (const [layerName, graph] of Object.entries(layers)) {
|
|
2197
|
+
for (const [origPath, node] of Object.entries(graph.files)) {
|
|
2198
|
+
const prefixedPath = `${layerName}/${origPath}`;
|
|
2199
|
+
mergedFiles[prefixedPath] = {
|
|
2200
|
+
path: prefixedPath,
|
|
2201
|
+
exists: node.exists,
|
|
2202
|
+
dependencies: node.dependencies.map((d) => `${layerName}/${d}`),
|
|
2203
|
+
dependents: node.dependents.map((d) => `${layerName}/${d}`)
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
for (const edge of graph.edges) {
|
|
2207
|
+
mergedEdges.push({
|
|
2208
|
+
source: `${layerName}/${edge.source}`,
|
|
2209
|
+
target: `${layerName}/${edge.target}`,
|
|
2210
|
+
type: edge.type
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
for (const circ of graph.circularDependencies) {
|
|
2214
|
+
mergedCircular.push({
|
|
2215
|
+
cycle: circ.cycle.map((f) => `${layerName}/${f}`)
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
return {
|
|
2220
|
+
rootDir: resolve4(projectRoot),
|
|
2221
|
+
files: mergedFiles,
|
|
2222
|
+
edges: mergedEdges,
|
|
2223
|
+
circularDependencies: mergedCircular,
|
|
2224
|
+
totalFiles: Object.keys(mergedFiles).length,
|
|
2225
|
+
totalEdges: mergedEdges.length
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
// src/storage/layers.ts
|
|
2230
|
+
import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
|
|
2231
|
+
import { join as join5 } from "path";
|
|
1767
2232
|
import { z } from "zod";
|
|
2233
|
+
var ARCHTRACKER_DIR = ".archtracker";
|
|
2234
|
+
var LAYERS_FILE = "layers.json";
|
|
2235
|
+
var LayerDefinitionSchema = z.object({
|
|
2236
|
+
name: z.string().min(1).regex(
|
|
2237
|
+
/^[a-zA-Z0-9_-]+$/,
|
|
2238
|
+
"Layer name must be alphanumeric (hyphens/underscores allowed)"
|
|
2239
|
+
),
|
|
2240
|
+
targetDir: z.string().min(1),
|
|
2241
|
+
language: z.enum(LANGUAGE_IDS).optional(),
|
|
2242
|
+
exclude: z.array(z.string()).optional(),
|
|
2243
|
+
color: z.string().optional(),
|
|
2244
|
+
description: z.string().optional()
|
|
2245
|
+
});
|
|
2246
|
+
var CrossLayerConnectionSchema = z.object({
|
|
2247
|
+
fromLayer: z.string(),
|
|
2248
|
+
fromFile: z.string(),
|
|
2249
|
+
toLayer: z.string(),
|
|
2250
|
+
toFile: z.string(),
|
|
2251
|
+
type: z.enum(["api-call", "event", "data-flow", "manual"]),
|
|
2252
|
+
label: z.string().optional()
|
|
2253
|
+
});
|
|
2254
|
+
var LayerConfigSchema = z.object({
|
|
2255
|
+
version: z.literal("1.0"),
|
|
2256
|
+
layers: z.array(LayerDefinitionSchema).min(1).refine(
|
|
2257
|
+
(layers) => {
|
|
2258
|
+
const names = layers.map((l) => l.name);
|
|
2259
|
+
return new Set(names).size === names.length;
|
|
2260
|
+
},
|
|
2261
|
+
{ message: "Layer names must be unique" }
|
|
2262
|
+
),
|
|
2263
|
+
connections: z.array(CrossLayerConnectionSchema).optional()
|
|
2264
|
+
});
|
|
2265
|
+
async function loadLayerConfig(projectRoot) {
|
|
2266
|
+
const filePath = join5(projectRoot, ARCHTRACKER_DIR, LAYERS_FILE);
|
|
2267
|
+
let raw;
|
|
2268
|
+
try {
|
|
2269
|
+
raw = await readFile2(filePath, "utf-8");
|
|
2270
|
+
} catch (error) {
|
|
2271
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
2272
|
+
return null;
|
|
2273
|
+
}
|
|
2274
|
+
throw new Error(`Failed to read ${filePath}`);
|
|
2275
|
+
}
|
|
2276
|
+
let parsed;
|
|
2277
|
+
try {
|
|
2278
|
+
parsed = JSON.parse(raw);
|
|
2279
|
+
} catch {
|
|
2280
|
+
throw new Error(`Invalid JSON in ${filePath}`);
|
|
2281
|
+
}
|
|
2282
|
+
const result = LayerConfigSchema.safeParse(parsed);
|
|
2283
|
+
if (!result.success) {
|
|
2284
|
+
const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).slice(0, 5).join("\n");
|
|
2285
|
+
throw new Error(`layers.json validation failed:
|
|
2286
|
+
${issues}`);
|
|
2287
|
+
}
|
|
2288
|
+
return result.data;
|
|
2289
|
+
}
|
|
2290
|
+
function isNodeError(error) {
|
|
2291
|
+
return error instanceof Error && "code" in error;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
// src/analyzer/resolve.ts
|
|
2295
|
+
async function resolveGraph(opts) {
|
|
2296
|
+
const layerConfig = await loadLayerConfig(opts.projectRoot);
|
|
2297
|
+
if (layerConfig) {
|
|
2298
|
+
const multi = await analyzeMultiLayer(opts.projectRoot, layerConfig.layers);
|
|
2299
|
+
const autoConnections = detectCrossLayerConnections(multi.layers, layerConfig.layers);
|
|
2300
|
+
const manualConnections = layerConfig.connections ?? [];
|
|
2301
|
+
const manualKeys = new Set(manualConnections.map(
|
|
2302
|
+
(c) => `${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`
|
|
2303
|
+
));
|
|
2304
|
+
const merged = [
|
|
2305
|
+
...manualConnections,
|
|
2306
|
+
...autoConnections.filter(
|
|
2307
|
+
(c) => !manualKeys.has(`${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`)
|
|
2308
|
+
)
|
|
2309
|
+
];
|
|
2310
|
+
return {
|
|
2311
|
+
graph: multi.merged,
|
|
2312
|
+
multiLayer: multi,
|
|
2313
|
+
layerMetadata: multi.layerMetadata,
|
|
2314
|
+
crossLayerEdges: merged
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
const graph = await analyzeProject(opts.targetDir, {
|
|
2318
|
+
exclude: opts.exclude,
|
|
2319
|
+
language: opts.language
|
|
2320
|
+
});
|
|
2321
|
+
return { graph };
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
// src/storage/snapshot.ts
|
|
2325
|
+
import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile3, access } from "fs/promises";
|
|
2326
|
+
import { join as join6 } from "path";
|
|
2327
|
+
import { z as z2 } from "zod";
|
|
1768
2328
|
|
|
1769
2329
|
// src/types/schema.ts
|
|
1770
|
-
var SCHEMA_VERSION = "1.
|
|
2330
|
+
var SCHEMA_VERSION = "1.1";
|
|
1771
2331
|
|
|
1772
2332
|
// src/storage/snapshot.ts
|
|
1773
|
-
var
|
|
2333
|
+
var ARCHTRACKER_DIR2 = ".archtracker";
|
|
1774
2334
|
var SNAPSHOT_FILE = "snapshot.json";
|
|
1775
|
-
var FileNodeSchema =
|
|
1776
|
-
path:
|
|
1777
|
-
exists:
|
|
1778
|
-
dependencies:
|
|
1779
|
-
dependents:
|
|
2335
|
+
var FileNodeSchema = z2.object({
|
|
2336
|
+
path: z2.string(),
|
|
2337
|
+
exists: z2.boolean(),
|
|
2338
|
+
dependencies: z2.array(z2.string()),
|
|
2339
|
+
dependents: z2.array(z2.string())
|
|
1780
2340
|
});
|
|
1781
|
-
var DependencyGraphSchema =
|
|
1782
|
-
rootDir:
|
|
1783
|
-
files:
|
|
1784
|
-
edges:
|
|
1785
|
-
source:
|
|
1786
|
-
target:
|
|
1787
|
-
type:
|
|
2341
|
+
var DependencyGraphSchema = z2.object({
|
|
2342
|
+
rootDir: z2.string(),
|
|
2343
|
+
files: z2.record(z2.string(), FileNodeSchema),
|
|
2344
|
+
edges: z2.array(z2.object({
|
|
2345
|
+
source: z2.string(),
|
|
2346
|
+
target: z2.string(),
|
|
2347
|
+
type: z2.enum(["static", "dynamic", "type-only"])
|
|
1788
2348
|
})),
|
|
1789
|
-
circularDependencies:
|
|
1790
|
-
totalFiles:
|
|
1791
|
-
totalEdges:
|
|
2349
|
+
circularDependencies: z2.array(z2.object({ cycle: z2.array(z2.string()) })),
|
|
2350
|
+
totalFiles: z2.number(),
|
|
2351
|
+
totalEdges: z2.number()
|
|
1792
2352
|
});
|
|
1793
|
-
var SnapshotSchema =
|
|
1794
|
-
version:
|
|
1795
|
-
timestamp:
|
|
1796
|
-
rootDir:
|
|
2353
|
+
var SnapshotSchema = z2.object({
|
|
2354
|
+
version: z2.enum([SCHEMA_VERSION, "1.0"]),
|
|
2355
|
+
timestamp: z2.string(),
|
|
2356
|
+
rootDir: z2.string(),
|
|
1797
2357
|
graph: DependencyGraphSchema
|
|
1798
2358
|
});
|
|
1799
|
-
async function saveSnapshot(projectRoot, graph) {
|
|
1800
|
-
const dirPath =
|
|
1801
|
-
const filePath =
|
|
2359
|
+
async function saveSnapshot(projectRoot, graph, multiLayer) {
|
|
2360
|
+
const dirPath = join6(projectRoot, ARCHTRACKER_DIR2);
|
|
2361
|
+
const filePath = join6(dirPath, SNAPSHOT_FILE);
|
|
1802
2362
|
const snapshot = {
|
|
1803
2363
|
version: SCHEMA_VERSION,
|
|
1804
2364
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1805
2365
|
rootDir: graph.rootDir,
|
|
1806
|
-
graph
|
|
2366
|
+
graph,
|
|
2367
|
+
...multiLayer ? { multiLayer } : {}
|
|
1807
2368
|
};
|
|
1808
|
-
await
|
|
1809
|
-
await
|
|
2369
|
+
await mkdir2(dirPath, { recursive: true });
|
|
2370
|
+
await writeFile2(filePath, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
1810
2371
|
return snapshot;
|
|
1811
2372
|
}
|
|
1812
2373
|
async function loadSnapshot(projectRoot) {
|
|
1813
|
-
const filePath =
|
|
2374
|
+
const filePath = join6(projectRoot, ARCHTRACKER_DIR2, SNAPSHOT_FILE);
|
|
1814
2375
|
let raw;
|
|
1815
2376
|
try {
|
|
1816
|
-
raw = await
|
|
2377
|
+
raw = await readFile3(filePath, "utf-8");
|
|
1817
2378
|
} catch (error) {
|
|
1818
|
-
if (
|
|
2379
|
+
if (isNodeError2(error) && error.code === "ENOENT") {
|
|
1819
2380
|
return null;
|
|
1820
2381
|
}
|
|
1821
2382
|
throw new StorageError(
|
|
@@ -1846,7 +2407,7 @@ var StorageError = class extends Error {
|
|
|
1846
2407
|
this.name = "StorageError";
|
|
1847
2408
|
}
|
|
1848
2409
|
};
|
|
1849
|
-
function
|
|
2410
|
+
function isNodeError2(error) {
|
|
1850
2411
|
return error instanceof Error && "code" in error;
|
|
1851
2412
|
}
|
|
1852
2413
|
|
|
@@ -1948,10 +2509,10 @@ function arraysEqual(a, b) {
|
|
|
1948
2509
|
}
|
|
1949
2510
|
|
|
1950
2511
|
// src/utils/path-guard.ts
|
|
1951
|
-
import { resolve as
|
|
2512
|
+
import { resolve as resolve5 } from "path";
|
|
1952
2513
|
function validatePath(inputPath, boundary) {
|
|
1953
|
-
const resolved =
|
|
1954
|
-
const root = boundary ?
|
|
2514
|
+
const resolved = resolve5(inputPath);
|
|
2515
|
+
const root = boundary ? resolve5(boundary) : process.cwd();
|
|
1955
2516
|
if (!resolved.startsWith(root)) {
|
|
1956
2517
|
throw new PathTraversalError(
|
|
1957
2518
|
t("pathGuard.traversal", { input: inputPath, resolved, boundary: root })
|
|
@@ -1967,14 +2528,14 @@ var PathTraversalError = class extends Error {
|
|
|
1967
2528
|
};
|
|
1968
2529
|
|
|
1969
2530
|
// src/utils/version.ts
|
|
1970
|
-
import { readFileSync as
|
|
1971
|
-
import { join as
|
|
2531
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2532
|
+
import { join as join7, dirname as dirname2 } from "path";
|
|
1972
2533
|
import { fileURLToPath } from "url";
|
|
1973
2534
|
function loadVersion() {
|
|
1974
2535
|
let dir = dirname2(fileURLToPath(import.meta.url));
|
|
1975
2536
|
for (let i = 0; i < 5; i++) {
|
|
1976
2537
|
try {
|
|
1977
|
-
const pkg = JSON.parse(
|
|
2538
|
+
const pkg = JSON.parse(readFileSync3(join7(dir, "package.json"), "utf-8"));
|
|
1978
2539
|
return pkg.version;
|
|
1979
2540
|
} catch {
|
|
1980
2541
|
dir = dirname2(dir);
|
|
@@ -1989,34 +2550,58 @@ var server = new McpServer({
|
|
|
1989
2550
|
name: "archtracker",
|
|
1990
2551
|
version: VERSION
|
|
1991
2552
|
});
|
|
1992
|
-
var languageEnum =
|
|
2553
|
+
var languageEnum = z3.enum(LANGUAGE_IDS);
|
|
1993
2554
|
var LANG_DISPLAY = {
|
|
1994
2555
|
javascript: "JS/TS",
|
|
1995
2556
|
"c-cpp": "C/C++",
|
|
1996
2557
|
"c-sharp": "C#"
|
|
1997
2558
|
};
|
|
1998
2559
|
var languageList = LANGUAGE_IDS.map((id) => LANG_DISPLAY[id] ?? id.charAt(0).toUpperCase() + id.slice(1)).join(", ");
|
|
2560
|
+
async function resolveGraphMcp(opts) {
|
|
2561
|
+
return resolveGraph({
|
|
2562
|
+
targetDir: opts.targetDir,
|
|
2563
|
+
projectRoot: opts.projectRoot,
|
|
2564
|
+
exclude: opts.exclude,
|
|
2565
|
+
language: opts.language
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
function formatLayerSummary(metadata) {
|
|
2569
|
+
return metadata.map(
|
|
2570
|
+
(m) => ` [${m.name}] ${m.fileCount} files, ${m.edgeCount} edges (${m.language})`
|
|
2571
|
+
).join("\n");
|
|
2572
|
+
}
|
|
1999
2573
|
server.tool(
|
|
2000
2574
|
"generate_map",
|
|
2001
|
-
`Analyze dependency graph
|
|
2575
|
+
`Analyze dependency graph and return raw JSON structure for programmatic use. For human-readable reports, use analyze_existing_architecture instead. Auto-detects multi-layer projects when .archtracker/layers.json exists. Supports ${languageList}.`,
|
|
2002
2576
|
{
|
|
2003
|
-
targetDir:
|
|
2004
|
-
|
|
2005
|
-
|
|
2577
|
+
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."),
|
|
2578
|
+
projectRoot: z3.string().default(".").describe("Project root (where .archtracker/ is located)"),
|
|
2579
|
+
exclude: z3.array(z3.string()).optional().describe("Array of regex patterns to exclude (e.g. ['test', 'mock'])"),
|
|
2006
2580
|
language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
|
|
2007
2581
|
},
|
|
2008
|
-
async ({ targetDir,
|
|
2582
|
+
async ({ targetDir, projectRoot, exclude, language }) => {
|
|
2009
2583
|
try {
|
|
2010
2584
|
validatePath(targetDir);
|
|
2011
|
-
|
|
2585
|
+
validatePath(projectRoot);
|
|
2586
|
+
const { graph, layerMetadata, crossLayerEdges } = await resolveGraphMcp({
|
|
2587
|
+
targetDir,
|
|
2588
|
+
projectRoot,
|
|
2589
|
+
exclude,
|
|
2590
|
+
language
|
|
2591
|
+
});
|
|
2012
2592
|
const summary = [
|
|
2013
2593
|
t("mcp.analyzeComplete", { files: graph.totalFiles, edges: graph.totalEdges }),
|
|
2014
|
-
graph.circularDependencies.length > 0 ? t("mcp.circularFound", { count: graph.circularDependencies.length }) : t("mcp.circularNone")
|
|
2594
|
+
graph.circularDependencies.length > 0 ? t("mcp.circularFound", { count: graph.circularDependencies.length }) : t("mcp.circularNone"),
|
|
2595
|
+
...layerMetadata ? ["\nLayers:\n" + formatLayerSummary(layerMetadata)] : [],
|
|
2596
|
+
...crossLayerEdges?.length ? [`
|
|
2597
|
+
Cross-layer connections: ${crossLayerEdges.length}`] : []
|
|
2015
2598
|
].join("\n");
|
|
2599
|
+
const result = { ...graph };
|
|
2600
|
+
if (crossLayerEdges?.length) result.crossLayerConnections = crossLayerEdges;
|
|
2016
2601
|
return {
|
|
2017
2602
|
content: [
|
|
2018
2603
|
{ type: "text", text: summary },
|
|
2019
|
-
{ type: "text", text: JSON.stringify(
|
|
2604
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
2020
2605
|
]
|
|
2021
2606
|
};
|
|
2022
2607
|
} catch (error) {
|
|
@@ -2026,26 +2611,42 @@ server.tool(
|
|
|
2026
2611
|
);
|
|
2027
2612
|
server.tool(
|
|
2028
2613
|
"analyze_existing_architecture",
|
|
2029
|
-
`Comprehensive architecture analysis for existing projects. Shows critical components, circular dependencies, orphan files, coupling hotspots, and directory breakdown. Supports ${
|
|
2614
|
+
`Comprehensive architecture analysis for existing projects. Shows critical components, circular dependencies, orphan files, coupling hotspots, and directory breakdown. Supports ${languageList}.`,
|
|
2030
2615
|
{
|
|
2031
|
-
targetDir:
|
|
2032
|
-
exclude:
|
|
2033
|
-
topN:
|
|
2034
|
-
saveSnapshot:
|
|
2035
|
-
projectRoot:
|
|
2616
|
+
targetDir: z3.string().default("src").describe("Target directory path (default: src)"),
|
|
2617
|
+
exclude: z3.array(z3.string()).optional().describe("Array of regex patterns to exclude"),
|
|
2618
|
+
topN: z3.number().int().min(1).max(50).optional().describe("Number of top items to show in each section (default: 10)"),
|
|
2619
|
+
saveSnapshot: z3.boolean().optional().describe("Also save a snapshot after analysis (default: false)"),
|
|
2620
|
+
projectRoot: z3.string().default(".").describe("Project root (where .archtracker/ is located)"),
|
|
2036
2621
|
language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
|
|
2037
2622
|
},
|
|
2038
2623
|
async ({ targetDir, exclude, topN, saveSnapshot: doSave, projectRoot, language }) => {
|
|
2039
2624
|
try {
|
|
2040
2625
|
validatePath(targetDir);
|
|
2041
|
-
|
|
2626
|
+
validatePath(projectRoot);
|
|
2627
|
+
const { graph, multiLayer, layerMetadata, crossLayerEdges } = await resolveGraphMcp({
|
|
2628
|
+
targetDir,
|
|
2629
|
+
projectRoot,
|
|
2630
|
+
exclude,
|
|
2631
|
+
language
|
|
2632
|
+
});
|
|
2042
2633
|
const report = formatAnalysisReport(graph, { topN: topN ?? 10 });
|
|
2043
2634
|
const content = [
|
|
2044
2635
|
{ type: "text", text: report }
|
|
2045
2636
|
];
|
|
2637
|
+
if (layerMetadata) {
|
|
2638
|
+
content.push({ type: "text", text: "\nLayers:\n" + formatLayerSummary(layerMetadata) });
|
|
2639
|
+
}
|
|
2640
|
+
if (crossLayerEdges?.length) {
|
|
2641
|
+
const crossSummary = crossLayerEdges.map(
|
|
2642
|
+
(c) => ` ${c.fromLayer}/${c.fromFile} \u2192 ${c.toLayer}/${c.toFile} [${c.type}] ${c.label ?? ""}`
|
|
2643
|
+
).join("\n");
|
|
2644
|
+
content.push({ type: "text", text: `
|
|
2645
|
+
Cross-layer connections (${crossLayerEdges.length}):
|
|
2646
|
+
${crossSummary}` });
|
|
2647
|
+
}
|
|
2046
2648
|
if (doSave) {
|
|
2047
|
-
|
|
2048
|
-
await saveSnapshot(projectRoot, graph);
|
|
2649
|
+
await saveSnapshot(projectRoot, graph, multiLayer);
|
|
2049
2650
|
content.push({ type: "text", text: t("analyze.snapshotSaved") });
|
|
2050
2651
|
}
|
|
2051
2652
|
return { content };
|
|
@@ -2058,22 +2659,27 @@ server.tool(
|
|
|
2058
2659
|
"save_architecture_snapshot",
|
|
2059
2660
|
"Save the current dependency graph as a snapshot to .archtracker/snapshot.json",
|
|
2060
2661
|
{
|
|
2061
|
-
targetDir:
|
|
2062
|
-
projectRoot:
|
|
2662
|
+
targetDir: z3.string().default("src").describe("Target directory path"),
|
|
2663
|
+
projectRoot: z3.string().default(".").describe("Project root (where .archtracker is placed)"),
|
|
2063
2664
|
language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
|
|
2064
2665
|
},
|
|
2065
2666
|
async ({ targetDir, projectRoot, language }) => {
|
|
2066
2667
|
try {
|
|
2067
2668
|
validatePath(targetDir);
|
|
2068
2669
|
validatePath(projectRoot);
|
|
2069
|
-
const graph = await
|
|
2070
|
-
|
|
2670
|
+
const { graph, multiLayer, layerMetadata } = await resolveGraphMcp({
|
|
2671
|
+
targetDir,
|
|
2672
|
+
projectRoot,
|
|
2673
|
+
language
|
|
2674
|
+
});
|
|
2675
|
+
const snapshot = await saveSnapshot(projectRoot, graph, multiLayer);
|
|
2071
2676
|
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 })}`);
|
|
2072
2677
|
const report = [
|
|
2073
2678
|
t("mcp.snapshotSaved"),
|
|
2074
2679
|
t("cli.timestamp", { ts: snapshot.timestamp }),
|
|
2075
2680
|
t("cli.fileCount", { count: graph.totalFiles }),
|
|
2076
2681
|
t("cli.edgeCount", { count: graph.totalEdges }),
|
|
2682
|
+
...layerMetadata ? ["", "Layers:", formatLayerSummary(layerMetadata)] : [],
|
|
2077
2683
|
"",
|
|
2078
2684
|
t("cli.keyComponents"),
|
|
2079
2685
|
...keyComponents
|
|
@@ -2088,8 +2694,8 @@ server.tool(
|
|
|
2088
2694
|
"check_architecture_diff",
|
|
2089
2695
|
"Compare saved snapshot with current code dependencies and warn about files that may need updates",
|
|
2090
2696
|
{
|
|
2091
|
-
targetDir:
|
|
2092
|
-
projectRoot:
|
|
2697
|
+
targetDir: z3.string().default("src").describe("Target directory path"),
|
|
2698
|
+
projectRoot: z3.string().default(".").describe("Project root (where .archtracker is placed)"),
|
|
2093
2699
|
language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
|
|
2094
2700
|
},
|
|
2095
2701
|
async ({ targetDir, projectRoot, language }) => {
|
|
@@ -2098,8 +2704,12 @@ server.tool(
|
|
|
2098
2704
|
validatePath(projectRoot);
|
|
2099
2705
|
const existingSnapshot = await loadSnapshot(projectRoot);
|
|
2100
2706
|
if (!existingSnapshot) {
|
|
2101
|
-
const graph = await
|
|
2102
|
-
|
|
2707
|
+
const { graph, multiLayer } = await resolveGraphMcp({
|
|
2708
|
+
targetDir,
|
|
2709
|
+
projectRoot,
|
|
2710
|
+
language
|
|
2711
|
+
});
|
|
2712
|
+
await saveSnapshot(projectRoot, graph, multiLayer);
|
|
2103
2713
|
return {
|
|
2104
2714
|
content: [
|
|
2105
2715
|
{
|
|
@@ -2113,7 +2723,11 @@ server.tool(
|
|
|
2113
2723
|
]
|
|
2114
2724
|
};
|
|
2115
2725
|
}
|
|
2116
|
-
const currentGraph = await
|
|
2726
|
+
const { graph: currentGraph } = await resolveGraphMcp({
|
|
2727
|
+
targetDir,
|
|
2728
|
+
projectRoot,
|
|
2729
|
+
language
|
|
2730
|
+
});
|
|
2117
2731
|
const diff = computeDiff(existingSnapshot.graph, currentGraph);
|
|
2118
2732
|
const report = formatDiffReport(diff);
|
|
2119
2733
|
return { content: [{ type: "text", text: report }] };
|
|
@@ -2126,16 +2740,22 @@ server.tool(
|
|
|
2126
2740
|
"get_current_context",
|
|
2127
2741
|
"Get current valid file paths and architecture summary for AI session initialization",
|
|
2128
2742
|
{
|
|
2129
|
-
targetDir:
|
|
2130
|
-
projectRoot:
|
|
2743
|
+
targetDir: z3.string().default("src").describe("Target directory path"),
|
|
2744
|
+
projectRoot: z3.string().default(".").describe("Project root"),
|
|
2131
2745
|
language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
|
|
2132
2746
|
},
|
|
2133
2747
|
async ({ targetDir, projectRoot, language }) => {
|
|
2134
2748
|
try {
|
|
2749
|
+
validatePath(targetDir);
|
|
2750
|
+
validatePath(projectRoot);
|
|
2135
2751
|
let snapshot = await loadSnapshot(projectRoot);
|
|
2136
2752
|
if (!snapshot) {
|
|
2137
|
-
const graph2 = await
|
|
2138
|
-
|
|
2753
|
+
const { graph: graph2, multiLayer } = await resolveGraphMcp({
|
|
2754
|
+
targetDir,
|
|
2755
|
+
projectRoot,
|
|
2756
|
+
language
|
|
2757
|
+
});
|
|
2758
|
+
snapshot = await saveSnapshot(projectRoot, graph2, multiLayer);
|
|
2139
2759
|
}
|
|
2140
2760
|
const graph = snapshot.graph;
|
|
2141
2761
|
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) => ({
|
|
@@ -2181,13 +2801,13 @@ server.tool(
|
|
|
2181
2801
|
"search_architecture",
|
|
2182
2802
|
"Search architecture: file path search, impact analysis, critical component detection, orphan file detection",
|
|
2183
2803
|
{
|
|
2184
|
-
query:
|
|
2185
|
-
mode:
|
|
2804
|
+
query: z3.string().optional().describe("Search query (required for path/affected modes, not needed for critical/orphans)"),
|
|
2805
|
+
mode: z3.enum(["path", "affected", "critical", "orphans"]).default("path").describe(
|
|
2186
2806
|
"Search mode: path=search by path, affected=change impact, critical=key files, orphans=isolated files"
|
|
2187
2807
|
),
|
|
2188
|
-
targetDir:
|
|
2189
|
-
projectRoot:
|
|
2190
|
-
limit:
|
|
2808
|
+
targetDir: z3.string().default("src").describe("Target directory path"),
|
|
2809
|
+
projectRoot: z3.string().default(".").describe("Project root"),
|
|
2810
|
+
limit: z3.number().int().min(1).max(50).optional().describe("Max results (default: 10)"),
|
|
2191
2811
|
language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
|
|
2192
2812
|
},
|
|
2193
2813
|
async ({ query, mode, targetDir, projectRoot, limit, language }) => {
|
|
@@ -2196,8 +2816,12 @@ server.tool(
|
|
|
2196
2816
|
validatePath(projectRoot);
|
|
2197
2817
|
let snapshot = await loadSnapshot(projectRoot);
|
|
2198
2818
|
if (!snapshot) {
|
|
2199
|
-
const graph2 = await
|
|
2200
|
-
|
|
2819
|
+
const { graph: graph2, multiLayer } = await resolveGraphMcp({
|
|
2820
|
+
targetDir,
|
|
2821
|
+
projectRoot,
|
|
2822
|
+
language
|
|
2823
|
+
});
|
|
2824
|
+
snapshot = await saveSnapshot(projectRoot, graph2, multiLayer);
|
|
2201
2825
|
}
|
|
2202
2826
|
const graph = snapshot.graph;
|
|
2203
2827
|
const maxResults = limit ?? 10;
|