archtracker-mcp 0.4.3 → 0.5.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 +1504 -112
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +94 -4
- package/dist/index.js +579 -19
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.js +671 -61
- package/dist/mcp/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/arch-analyze/SKILL.md +6 -2
- package/skills/arch-check/SKILL.md +11 -8
- package/skills/arch-context/SKILL.md +8 -6
- package/skills/arch-search/SKILL.md +7 -7
- package/skills/arch-serve/SKILL.md +27 -0
- package/skills/arch-snapshot/SKILL.md +9 -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,13 +1843,396 @@ function formatAnalysisReport(graph, options = {}) {
|
|
|
1761
1843
|
return lines.join("\n");
|
|
1762
1844
|
}
|
|
1763
1845
|
|
|
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
|
+
|
|
1764
2229
|
// src/storage/snapshot.ts
|
|
1765
2230
|
import { mkdir, writeFile, readFile as readFile2, access } from "fs/promises";
|
|
1766
|
-
import { join as
|
|
2231
|
+
import { join as join5 } from "path";
|
|
1767
2232
|
import { z } from "zod";
|
|
1768
2233
|
|
|
1769
2234
|
// src/types/schema.ts
|
|
1770
|
-
var SCHEMA_VERSION = "1.
|
|
2235
|
+
var SCHEMA_VERSION = "1.1";
|
|
1771
2236
|
|
|
1772
2237
|
// src/storage/snapshot.ts
|
|
1773
2238
|
var ARCHTRACKER_DIR = ".archtracker";
|
|
@@ -1791,26 +2256,27 @@ var DependencyGraphSchema = z.object({
|
|
|
1791
2256
|
totalEdges: z.number()
|
|
1792
2257
|
});
|
|
1793
2258
|
var SnapshotSchema = z.object({
|
|
1794
|
-
version: z.
|
|
2259
|
+
version: z.enum([SCHEMA_VERSION, "1.0"]),
|
|
1795
2260
|
timestamp: z.string(),
|
|
1796
2261
|
rootDir: z.string(),
|
|
1797
2262
|
graph: DependencyGraphSchema
|
|
1798
2263
|
});
|
|
1799
|
-
async function saveSnapshot(projectRoot, graph) {
|
|
1800
|
-
const dirPath =
|
|
1801
|
-
const filePath =
|
|
2264
|
+
async function saveSnapshot(projectRoot, graph, multiLayer) {
|
|
2265
|
+
const dirPath = join5(projectRoot, ARCHTRACKER_DIR);
|
|
2266
|
+
const filePath = join5(dirPath, SNAPSHOT_FILE);
|
|
1802
2267
|
const snapshot = {
|
|
1803
2268
|
version: SCHEMA_VERSION,
|
|
1804
2269
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1805
2270
|
rootDir: graph.rootDir,
|
|
1806
|
-
graph
|
|
2271
|
+
graph,
|
|
2272
|
+
...multiLayer ? { multiLayer } : {}
|
|
1807
2273
|
};
|
|
1808
2274
|
await mkdir(dirPath, { recursive: true });
|
|
1809
2275
|
await writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
1810
2276
|
return snapshot;
|
|
1811
2277
|
}
|
|
1812
2278
|
async function loadSnapshot(projectRoot) {
|
|
1813
|
-
const filePath =
|
|
2279
|
+
const filePath = join5(projectRoot, ARCHTRACKER_DIR, SNAPSHOT_FILE);
|
|
1814
2280
|
let raw;
|
|
1815
2281
|
try {
|
|
1816
2282
|
raw = await readFile2(filePath, "utf-8");
|
|
@@ -1947,11 +2413,76 @@ function arraysEqual(a, b) {
|
|
|
1947
2413
|
return true;
|
|
1948
2414
|
}
|
|
1949
2415
|
|
|
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
|
+
|
|
1950
2481
|
// src/utils/path-guard.ts
|
|
1951
|
-
import { resolve as
|
|
2482
|
+
import { resolve as resolve5 } from "path";
|
|
1952
2483
|
function validatePath(inputPath, boundary) {
|
|
1953
|
-
const resolved =
|
|
1954
|
-
const root = boundary ?
|
|
2484
|
+
const resolved = resolve5(inputPath);
|
|
2485
|
+
const root = boundary ? resolve5(boundary) : process.cwd();
|
|
1955
2486
|
if (!resolved.startsWith(root)) {
|
|
1956
2487
|
throw new PathTraversalError(
|
|
1957
2488
|
t("pathGuard.traversal", { input: inputPath, resolved, boundary: root })
|
|
@@ -1967,14 +2498,14 @@ var PathTraversalError = class extends Error {
|
|
|
1967
2498
|
};
|
|
1968
2499
|
|
|
1969
2500
|
// src/utils/version.ts
|
|
1970
|
-
import { readFileSync as
|
|
1971
|
-
import { join as
|
|
2501
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2502
|
+
import { join as join7, dirname as dirname2 } from "path";
|
|
1972
2503
|
import { fileURLToPath } from "url";
|
|
1973
2504
|
function loadVersion() {
|
|
1974
2505
|
let dir = dirname2(fileURLToPath(import.meta.url));
|
|
1975
2506
|
for (let i = 0; i < 5; i++) {
|
|
1976
2507
|
try {
|
|
1977
|
-
const pkg = JSON.parse(
|
|
2508
|
+
const pkg = JSON.parse(readFileSync3(join7(dir, "package.json"), "utf-8"));
|
|
1978
2509
|
return pkg.version;
|
|
1979
2510
|
} catch {
|
|
1980
2511
|
dir = dirname2(dir);
|
|
@@ -1989,34 +2520,76 @@ var server = new McpServer({
|
|
|
1989
2520
|
name: "archtracker",
|
|
1990
2521
|
version: VERSION
|
|
1991
2522
|
});
|
|
1992
|
-
var languageEnum =
|
|
2523
|
+
var languageEnum = z3.enum(LANGUAGE_IDS);
|
|
1993
2524
|
var LANG_DISPLAY = {
|
|
1994
2525
|
javascript: "JS/TS",
|
|
1995
2526
|
"c-cpp": "C/C++",
|
|
1996
2527
|
"c-sharp": "C#"
|
|
1997
2528
|
};
|
|
1998
2529
|
var languageList = LANGUAGE_IDS.map((id) => LANG_DISPLAY[id] ?? id.charAt(0).toUpperCase() + id.slice(1)).join(", ");
|
|
2530
|
+
async function resolveGraphForMcp(opts) {
|
|
2531
|
+
if (opts.targetDir === "src") {
|
|
2532
|
+
const layerConfig = await loadLayerConfig(opts.projectRoot);
|
|
2533
|
+
if (layerConfig) {
|
|
2534
|
+
const multi = await analyzeMultiLayer(opts.projectRoot, layerConfig.layers);
|
|
2535
|
+
const autoConnections = detectCrossLayerConnections(multi.layers, layerConfig.layers);
|
|
2536
|
+
const manualConnections = layerConfig.connections ?? [];
|
|
2537
|
+
const manualKeys = new Set(manualConnections.map(
|
|
2538
|
+
(c) => `${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`
|
|
2539
|
+
));
|
|
2540
|
+
const allConnections = [
|
|
2541
|
+
...manualConnections,
|
|
2542
|
+
...autoConnections.filter(
|
|
2543
|
+
(c) => !manualKeys.has(`${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`)
|
|
2544
|
+
)
|
|
2545
|
+
];
|
|
2546
|
+
return { graph: multi.merged, multiLayer: multi, layerMetadata: multi.layerMetadata, crossEdges: allConnections };
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
const graph = await analyzeProject(opts.targetDir, {
|
|
2550
|
+
exclude: opts.exclude,
|
|
2551
|
+
language: opts.language
|
|
2552
|
+
});
|
|
2553
|
+
return { graph };
|
|
2554
|
+
}
|
|
2555
|
+
function formatLayerSummary(metadata) {
|
|
2556
|
+
return metadata.map(
|
|
2557
|
+
(m) => ` [${m.name}] ${m.fileCount} files, ${m.edgeCount} edges (${m.language})`
|
|
2558
|
+
).join("\n");
|
|
2559
|
+
}
|
|
1999
2560
|
server.tool(
|
|
2000
2561
|
"generate_map",
|
|
2001
|
-
`Analyze dependency graph
|
|
2562
|
+
`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
2563
|
{
|
|
2003
|
-
targetDir:
|
|
2004
|
-
|
|
2005
|
-
|
|
2564
|
+
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
|
+
projectRoot: z3.string().default(".").describe("Project root (where .archtracker/ is located)"),
|
|
2566
|
+
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)"),
|
|
2006
2568
|
language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
|
|
2007
2569
|
},
|
|
2008
|
-
async ({ targetDir, exclude, maxDepth, language }) => {
|
|
2570
|
+
async ({ targetDir, projectRoot, exclude, maxDepth, language }) => {
|
|
2009
2571
|
try {
|
|
2010
2572
|
validatePath(targetDir);
|
|
2011
|
-
|
|
2573
|
+
validatePath(projectRoot);
|
|
2574
|
+
const { graph, layerMetadata, crossEdges } = await resolveGraphForMcp({
|
|
2575
|
+
targetDir,
|
|
2576
|
+
projectRoot,
|
|
2577
|
+
exclude,
|
|
2578
|
+
language
|
|
2579
|
+
});
|
|
2012
2580
|
const summary = [
|
|
2013
2581
|
t("mcp.analyzeComplete", { files: graph.totalFiles, edges: graph.totalEdges }),
|
|
2014
|
-
graph.circularDependencies.length > 0 ? t("mcp.circularFound", { count: graph.circularDependencies.length }) : t("mcp.circularNone")
|
|
2582
|
+
graph.circularDependencies.length > 0 ? t("mcp.circularFound", { count: graph.circularDependencies.length }) : t("mcp.circularNone"),
|
|
2583
|
+
...layerMetadata ? ["\nLayers:\n" + formatLayerSummary(layerMetadata)] : [],
|
|
2584
|
+
...crossEdges?.length ? [`
|
|
2585
|
+
Cross-layer connections: ${crossEdges.length}`] : []
|
|
2015
2586
|
].join("\n");
|
|
2587
|
+
const result = { ...graph };
|
|
2588
|
+
if (crossEdges?.length) result.crossLayerConnections = crossEdges;
|
|
2016
2589
|
return {
|
|
2017
2590
|
content: [
|
|
2018
2591
|
{ type: "text", text: summary },
|
|
2019
|
-
{ type: "text", text: JSON.stringify(
|
|
2592
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
2020
2593
|
]
|
|
2021
2594
|
};
|
|
2022
2595
|
} catch (error) {
|
|
@@ -2028,24 +2601,40 @@ server.tool(
|
|
|
2028
2601
|
"analyze_existing_architecture",
|
|
2029
2602
|
`Comprehensive architecture analysis for existing projects. Shows critical components, circular dependencies, orphan files, coupling hotspots, and directory breakdown. Supports ${LANGUAGE_IDS.length} languages.`,
|
|
2030
2603
|
{
|
|
2031
|
-
targetDir:
|
|
2032
|
-
exclude:
|
|
2033
|
-
topN:
|
|
2034
|
-
saveSnapshot:
|
|
2035
|
-
projectRoot:
|
|
2604
|
+
targetDir: z3.string().default("src").describe("Target directory path (default: src)"),
|
|
2605
|
+
exclude: z3.array(z3.string()).optional().describe("Array of regex patterns to exclude"),
|
|
2606
|
+
topN: z3.number().int().min(1).max(50).optional().describe("Number of top items to show in each section (default: 10)"),
|
|
2607
|
+
saveSnapshot: z3.boolean().optional().describe("Also save a snapshot after analysis (default: false)"),
|
|
2608
|
+
projectRoot: z3.string().default(".").describe("Project root (needed only when saveSnapshot is true)"),
|
|
2036
2609
|
language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
|
|
2037
2610
|
},
|
|
2038
2611
|
async ({ targetDir, exclude, topN, saveSnapshot: doSave, projectRoot, language }) => {
|
|
2039
2612
|
try {
|
|
2040
2613
|
validatePath(targetDir);
|
|
2041
|
-
const graph = await
|
|
2614
|
+
const { graph, multiLayer, layerMetadata, crossEdges } = await resolveGraphForMcp({
|
|
2615
|
+
targetDir,
|
|
2616
|
+
projectRoot,
|
|
2617
|
+
exclude,
|
|
2618
|
+
language
|
|
2619
|
+
});
|
|
2042
2620
|
const report = formatAnalysisReport(graph, { topN: topN ?? 10 });
|
|
2043
2621
|
const content = [
|
|
2044
2622
|
{ type: "text", text: report }
|
|
2045
2623
|
];
|
|
2624
|
+
if (layerMetadata) {
|
|
2625
|
+
content.push({ type: "text", text: "\nLayers:\n" + formatLayerSummary(layerMetadata) });
|
|
2626
|
+
}
|
|
2627
|
+
if (crossEdges?.length) {
|
|
2628
|
+
const crossSummary = crossEdges.map(
|
|
2629
|
+
(c) => ` ${c.fromLayer}/${c.fromFile} \u2192 ${c.toLayer}/${c.toFile} [${c.type}] ${c.label ?? ""}`
|
|
2630
|
+
).join("\n");
|
|
2631
|
+
content.push({ type: "text", text: `
|
|
2632
|
+
Cross-layer connections (${crossEdges.length}):
|
|
2633
|
+
${crossSummary}` });
|
|
2634
|
+
}
|
|
2046
2635
|
if (doSave) {
|
|
2047
2636
|
validatePath(projectRoot);
|
|
2048
|
-
await saveSnapshot(projectRoot, graph);
|
|
2637
|
+
await saveSnapshot(projectRoot, graph, multiLayer);
|
|
2049
2638
|
content.push({ type: "text", text: t("analyze.snapshotSaved") });
|
|
2050
2639
|
}
|
|
2051
2640
|
return { content };
|
|
@@ -2058,22 +2647,27 @@ server.tool(
|
|
|
2058
2647
|
"save_architecture_snapshot",
|
|
2059
2648
|
"Save the current dependency graph as a snapshot to .archtracker/snapshot.json",
|
|
2060
2649
|
{
|
|
2061
|
-
targetDir:
|
|
2062
|
-
projectRoot:
|
|
2650
|
+
targetDir: z3.string().default("src").describe("Target directory path"),
|
|
2651
|
+
projectRoot: z3.string().default(".").describe("Project root (where .archtracker is placed)"),
|
|
2063
2652
|
language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
|
|
2064
2653
|
},
|
|
2065
2654
|
async ({ targetDir, projectRoot, language }) => {
|
|
2066
2655
|
try {
|
|
2067
2656
|
validatePath(targetDir);
|
|
2068
2657
|
validatePath(projectRoot);
|
|
2069
|
-
const graph = await
|
|
2070
|
-
|
|
2658
|
+
const { graph, multiLayer, layerMetadata } = await resolveGraphForMcp({
|
|
2659
|
+
targetDir,
|
|
2660
|
+
projectRoot,
|
|
2661
|
+
language
|
|
2662
|
+
});
|
|
2663
|
+
const snapshot = await saveSnapshot(projectRoot, graph, multiLayer);
|
|
2071
2664
|
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
2665
|
const report = [
|
|
2073
2666
|
t("mcp.snapshotSaved"),
|
|
2074
2667
|
t("cli.timestamp", { ts: snapshot.timestamp }),
|
|
2075
2668
|
t("cli.fileCount", { count: graph.totalFiles }),
|
|
2076
2669
|
t("cli.edgeCount", { count: graph.totalEdges }),
|
|
2670
|
+
...layerMetadata ? ["", "Layers:", formatLayerSummary(layerMetadata)] : [],
|
|
2077
2671
|
"",
|
|
2078
2672
|
t("cli.keyComponents"),
|
|
2079
2673
|
...keyComponents
|
|
@@ -2088,8 +2682,8 @@ server.tool(
|
|
|
2088
2682
|
"check_architecture_diff",
|
|
2089
2683
|
"Compare saved snapshot with current code dependencies and warn about files that may need updates",
|
|
2090
2684
|
{
|
|
2091
|
-
targetDir:
|
|
2092
|
-
projectRoot:
|
|
2685
|
+
targetDir: z3.string().default("src").describe("Target directory path"),
|
|
2686
|
+
projectRoot: z3.string().default(".").describe("Project root (where .archtracker is placed)"),
|
|
2093
2687
|
language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
|
|
2094
2688
|
},
|
|
2095
2689
|
async ({ targetDir, projectRoot, language }) => {
|
|
@@ -2098,8 +2692,12 @@ server.tool(
|
|
|
2098
2692
|
validatePath(projectRoot);
|
|
2099
2693
|
const existingSnapshot = await loadSnapshot(projectRoot);
|
|
2100
2694
|
if (!existingSnapshot) {
|
|
2101
|
-
const graph = await
|
|
2102
|
-
|
|
2695
|
+
const { graph, multiLayer } = await resolveGraphForMcp({
|
|
2696
|
+
targetDir,
|
|
2697
|
+
projectRoot,
|
|
2698
|
+
language
|
|
2699
|
+
});
|
|
2700
|
+
await saveSnapshot(projectRoot, graph, multiLayer);
|
|
2103
2701
|
return {
|
|
2104
2702
|
content: [
|
|
2105
2703
|
{
|
|
@@ -2113,7 +2711,11 @@ server.tool(
|
|
|
2113
2711
|
]
|
|
2114
2712
|
};
|
|
2115
2713
|
}
|
|
2116
|
-
const currentGraph = await
|
|
2714
|
+
const { graph: currentGraph } = await resolveGraphForMcp({
|
|
2715
|
+
targetDir,
|
|
2716
|
+
projectRoot,
|
|
2717
|
+
language
|
|
2718
|
+
});
|
|
2117
2719
|
const diff = computeDiff(existingSnapshot.graph, currentGraph);
|
|
2118
2720
|
const report = formatDiffReport(diff);
|
|
2119
2721
|
return { content: [{ type: "text", text: report }] };
|
|
@@ -2126,16 +2728,20 @@ server.tool(
|
|
|
2126
2728
|
"get_current_context",
|
|
2127
2729
|
"Get current valid file paths and architecture summary for AI session initialization",
|
|
2128
2730
|
{
|
|
2129
|
-
targetDir:
|
|
2130
|
-
projectRoot:
|
|
2731
|
+
targetDir: z3.string().default("src").describe("Target directory path"),
|
|
2732
|
+
projectRoot: z3.string().default(".").describe("Project root"),
|
|
2131
2733
|
language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
|
|
2132
2734
|
},
|
|
2133
2735
|
async ({ targetDir, projectRoot, language }) => {
|
|
2134
2736
|
try {
|
|
2135
2737
|
let snapshot = await loadSnapshot(projectRoot);
|
|
2136
2738
|
if (!snapshot) {
|
|
2137
|
-
const graph2 = await
|
|
2138
|
-
|
|
2739
|
+
const { graph: graph2, multiLayer } = await resolveGraphForMcp({
|
|
2740
|
+
targetDir,
|
|
2741
|
+
projectRoot,
|
|
2742
|
+
language
|
|
2743
|
+
});
|
|
2744
|
+
snapshot = await saveSnapshot(projectRoot, graph2, multiLayer);
|
|
2139
2745
|
}
|
|
2140
2746
|
const graph = snapshot.graph;
|
|
2141
2747
|
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 +2787,13 @@ server.tool(
|
|
|
2181
2787
|
"search_architecture",
|
|
2182
2788
|
"Search architecture: file path search, impact analysis, critical component detection, orphan file detection",
|
|
2183
2789
|
{
|
|
2184
|
-
query:
|
|
2185
|
-
mode:
|
|
2790
|
+
query: z3.string().optional().describe("Search query (required for path/affected modes, not needed for critical/orphans)"),
|
|
2791
|
+
mode: z3.enum(["path", "affected", "critical", "orphans"]).default("path").describe(
|
|
2186
2792
|
"Search mode: path=search by path, affected=change impact, critical=key files, orphans=isolated files"
|
|
2187
2793
|
),
|
|
2188
|
-
targetDir:
|
|
2189
|
-
projectRoot:
|
|
2190
|
-
limit:
|
|
2794
|
+
targetDir: z3.string().default("src").describe("Target directory path"),
|
|
2795
|
+
projectRoot: z3.string().default(".").describe("Project root"),
|
|
2796
|
+
limit: z3.number().int().min(1).max(50).optional().describe("Max results (default: 10)"),
|
|
2191
2797
|
language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
|
|
2192
2798
|
},
|
|
2193
2799
|
async ({ query, mode, targetDir, projectRoot, limit, language }) => {
|
|
@@ -2196,8 +2802,12 @@ server.tool(
|
|
|
2196
2802
|
validatePath(projectRoot);
|
|
2197
2803
|
let snapshot = await loadSnapshot(projectRoot);
|
|
2198
2804
|
if (!snapshot) {
|
|
2199
|
-
const graph2 = await
|
|
2200
|
-
|
|
2805
|
+
const { graph: graph2, multiLayer } = await resolveGraphForMcp({
|
|
2806
|
+
targetDir,
|
|
2807
|
+
projectRoot,
|
|
2808
|
+
language
|
|
2809
|
+
});
|
|
2810
|
+
snapshot = await saveSnapshot(projectRoot, graph2, multiLayer);
|
|
2201
2811
|
}
|
|
2202
2812
|
const graph = snapshot.graph;
|
|
2203
2813
|
const maxResults = limit ?? 10;
|