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/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/types/schema.ts
|
|
2
|
-
var SCHEMA_VERSION = "1.
|
|
2
|
+
var SCHEMA_VERSION = "1.1";
|
|
3
3
|
|
|
4
4
|
// src/analyzer/analyze.ts
|
|
5
5
|
import { resolve as resolve3 } from "path";
|
|
@@ -813,6 +813,25 @@ async function scanExtensions(dir, counts, maxDepth, currentDepth) {
|
|
|
813
813
|
// src/analyzer/engines/languages.ts
|
|
814
814
|
import { readFileSync } from "fs";
|
|
815
815
|
import { join as join3, dirname, resolve as resolve2 } from "path";
|
|
816
|
+
|
|
817
|
+
// src/analyzer/engines/types.ts
|
|
818
|
+
var LANGUAGE_IDS = [
|
|
819
|
+
"javascript",
|
|
820
|
+
"python",
|
|
821
|
+
"rust",
|
|
822
|
+
"go",
|
|
823
|
+
"java",
|
|
824
|
+
"c-cpp",
|
|
825
|
+
"c-sharp",
|
|
826
|
+
"ruby",
|
|
827
|
+
"php",
|
|
828
|
+
"swift",
|
|
829
|
+
"kotlin",
|
|
830
|
+
"dart",
|
|
831
|
+
"scala"
|
|
832
|
+
];
|
|
833
|
+
|
|
834
|
+
// src/analyzer/engines/languages.ts
|
|
816
835
|
var python = {
|
|
817
836
|
id: "python",
|
|
818
837
|
extensions: [".py"],
|
|
@@ -1064,6 +1083,13 @@ var java = {
|
|
|
1064
1083
|
if (projectFiles.has(full)) return full;
|
|
1065
1084
|
}
|
|
1066
1085
|
}
|
|
1086
|
+
for (let i = 1; i < segments.length; i++) {
|
|
1087
|
+
const filePath = segments.slice(i).join("/") + ".java";
|
|
1088
|
+
for (const srcRoot of ["", "src/main/java/", "src/", "app/src/main/java/"]) {
|
|
1089
|
+
const full = join3(rootDir, srcRoot, filePath);
|
|
1090
|
+
if (projectFiles.has(full)) return full;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1067
1093
|
return null;
|
|
1068
1094
|
},
|
|
1069
1095
|
defaultExclude: ["build", "target", "\\.gradle", "\\.idea"]
|
|
@@ -1118,12 +1144,18 @@ var php = {
|
|
|
1118
1144
|
importPatterns: [
|
|
1119
1145
|
// require/include/require_once/include_once 'path'
|
|
1120
1146
|
{ regex: /\b(?:require|include)(?:_once)?\s+['"]([^'"]+)['"]/gm },
|
|
1147
|
+
// require_once __DIR__ . '/path' (common PHP pattern)
|
|
1148
|
+
{ regex: /\b(?:require|include)(?:_once)?\s+__DIR__\s*\.\s*['"]([^'"]+)['"]/gm },
|
|
1121
1149
|
// Bug #9 fix: use Namespace\Class — skip `function` and `const` keywords
|
|
1122
1150
|
{ regex: /^use\s+(?:function\s+|const\s+)?([\w\\]+)/gm }
|
|
1123
1151
|
],
|
|
1124
1152
|
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
1125
|
-
|
|
1126
|
-
|
|
1153
|
+
let normalizedPath = importPath;
|
|
1154
|
+
if (normalizedPath.startsWith("/")) {
|
|
1155
|
+
normalizedPath = normalizedPath.slice(1);
|
|
1156
|
+
}
|
|
1157
|
+
if (normalizedPath.includes("/") || normalizedPath.endsWith(".php")) {
|
|
1158
|
+
const withExt = normalizedPath.endsWith(".php") ? normalizedPath : normalizedPath + ".php";
|
|
1127
1159
|
const fromSource = resolve2(dirname(sourceFile), withExt);
|
|
1128
1160
|
if (projectFiles.has(fromSource)) return fromSource;
|
|
1129
1161
|
const fromRoot2 = join3(rootDir, withExt);
|
|
@@ -1139,15 +1171,46 @@ var php = {
|
|
|
1139
1171
|
},
|
|
1140
1172
|
defaultExclude: ["vendor"]
|
|
1141
1173
|
};
|
|
1174
|
+
var SWIFT_SKIP_FILES = /* @__PURE__ */ new Set(["Package", "main", "AppDelegate", "SceneDelegate"]);
|
|
1142
1175
|
var swift = {
|
|
1143
1176
|
id: "swift",
|
|
1144
1177
|
extensions: [".swift"],
|
|
1145
1178
|
commentStyle: "c-style",
|
|
1146
|
-
importPatterns: [
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1179
|
+
importPatterns: [],
|
|
1180
|
+
// handled by extractImports
|
|
1181
|
+
extractImports(content, filePath, _rootDir, projectFiles) {
|
|
1182
|
+
const imports = [];
|
|
1183
|
+
const moduleRegex = /^(?:@testable\s+)?import\s+(?:class\s+|struct\s+|enum\s+|protocol\s+|func\s+|var\s+|let\s+|typealias\s+)?(\w+)/gm;
|
|
1184
|
+
let match;
|
|
1185
|
+
while ((match = moduleRegex.exec(content)) !== null) {
|
|
1186
|
+
imports.push(match[1]);
|
|
1187
|
+
}
|
|
1188
|
+
const typeMap = /* @__PURE__ */ new Map();
|
|
1189
|
+
for (const f of projectFiles) {
|
|
1190
|
+
if (f === filePath || !f.endsWith(".swift")) continue;
|
|
1191
|
+
const basename = f.split("/").pop().replace(/\.swift$/, "");
|
|
1192
|
+
if (!basename || SWIFT_SKIP_FILES.has(basename)) continue;
|
|
1193
|
+
typeMap.set(basename, f);
|
|
1194
|
+
}
|
|
1195
|
+
if (typeMap.size > 0) {
|
|
1196
|
+
const escaped = [...typeMap.keys()].map(
|
|
1197
|
+
(n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
1198
|
+
);
|
|
1199
|
+
const combined = new RegExp(`\\b(${escaped.join("|")})\\b`, "g");
|
|
1200
|
+
const matched = /* @__PURE__ */ new Set();
|
|
1201
|
+
while ((match = combined.exec(content)) !== null) {
|
|
1202
|
+
const typeName = match[1];
|
|
1203
|
+
const targetPath = typeMap.get(typeName);
|
|
1204
|
+
if (targetPath && !matched.has(targetPath)) {
|
|
1205
|
+
matched.add(targetPath);
|
|
1206
|
+
imports.push(targetPath);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return imports;
|
|
1211
|
+
},
|
|
1212
|
+
resolveImport(importPath, _sourceFile, rootDir, projectFiles) {
|
|
1213
|
+
if (projectFiles.has(importPath)) return importPath;
|
|
1151
1214
|
const spmDir = join3(rootDir, "Sources", importPath);
|
|
1152
1215
|
for (const f of projectFiles) {
|
|
1153
1216
|
if (f.startsWith(spmDir + "/") && f.endsWith(".swift")) return f;
|
|
@@ -1172,7 +1235,8 @@ var kotlin = {
|
|
|
1172
1235
|
if (cleanPath.endsWith(".")) {
|
|
1173
1236
|
cleanPath = cleanPath.slice(0, -1);
|
|
1174
1237
|
}
|
|
1175
|
-
const
|
|
1238
|
+
const segments = cleanPath.split(".");
|
|
1239
|
+
const filePath = segments.join("/");
|
|
1176
1240
|
for (const ext of [".kt", ".kts"]) {
|
|
1177
1241
|
for (const srcRoot of [
|
|
1178
1242
|
"",
|
|
@@ -1186,6 +1250,22 @@ var kotlin = {
|
|
|
1186
1250
|
if (projectFiles.has(full)) return full;
|
|
1187
1251
|
}
|
|
1188
1252
|
}
|
|
1253
|
+
for (let i = 1; i < segments.length; i++) {
|
|
1254
|
+
const suffixPath = segments.slice(i).join("/");
|
|
1255
|
+
for (const ext of [".kt", ".kts"]) {
|
|
1256
|
+
for (const srcRoot of [
|
|
1257
|
+
"",
|
|
1258
|
+
"src/main/kotlin/",
|
|
1259
|
+
"src/main/java/",
|
|
1260
|
+
"src/",
|
|
1261
|
+
"app/src/main/kotlin/",
|
|
1262
|
+
"app/src/main/java/"
|
|
1263
|
+
]) {
|
|
1264
|
+
const full = join3(rootDir, srcRoot, suffixPath + ext);
|
|
1265
|
+
if (projectFiles.has(full)) return full;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1189
1269
|
return null;
|
|
1190
1270
|
},
|
|
1191
1271
|
defaultExclude: ["build", "\\.gradle", "\\.idea"]
|
|
@@ -1271,8 +1351,10 @@ var dart = {
|
|
|
1271
1351
|
const prefix = `package:${ownPackage}/`;
|
|
1272
1352
|
if (!importPath.startsWith(prefix)) return null;
|
|
1273
1353
|
const relPath = importPath.slice(prefix.length);
|
|
1274
|
-
const
|
|
1275
|
-
if (projectFiles.has(
|
|
1354
|
+
const libPath = join3(rootDir, "lib", relPath);
|
|
1355
|
+
if (projectFiles.has(libPath)) return libPath;
|
|
1356
|
+
const rootPath = join3(rootDir, relPath);
|
|
1357
|
+
if (projectFiles.has(rootPath)) return rootPath;
|
|
1276
1358
|
return null;
|
|
1277
1359
|
}
|
|
1278
1360
|
const resolved = resolve2(dirname(sourceFile), importPath);
|
|
@@ -1335,6 +1417,15 @@ var scala = {
|
|
|
1335
1417
|
}
|
|
1336
1418
|
}
|
|
1337
1419
|
}
|
|
1420
|
+
for (let i = 1; i < segments.length; i++) {
|
|
1421
|
+
const suffixPath = segments.slice(i).join("/");
|
|
1422
|
+
for (const ext of [".scala", ".sc"]) {
|
|
1423
|
+
for (const srcRoot of ["", "src/main/scala/", "src/", "app/"]) {
|
|
1424
|
+
const full = join3(rootDir, srcRoot, suffixPath + ext);
|
|
1425
|
+
if (projectFiles.has(full)) return full;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1338
1429
|
return null;
|
|
1339
1430
|
},
|
|
1340
1431
|
defaultExclude: ["target", "\\.bsp", "\\.metals", "\\.bloop"]
|
|
@@ -1522,6 +1613,11 @@ var en = {
|
|
|
1522
1613
|
"analyze.snapshotSaved": "\nSnapshot saved alongside analysis.",
|
|
1523
1614
|
// CI
|
|
1524
1615
|
"ci.generated": "GitHub Actions workflow generated: {path}",
|
|
1616
|
+
// Layers
|
|
1617
|
+
"layers.alreadyExists": "layers.json already exists. Edit it manually to modify.",
|
|
1618
|
+
"layers.created": "Created .archtracker/layers.json \u2014 edit it to configure your layers.",
|
|
1619
|
+
"layers.notFound": "No .archtracker/layers.json found. Run 'archtracker layers init' to create one.",
|
|
1620
|
+
"layers.header": "Configured layers ({count}):",
|
|
1525
1621
|
// Web viewer
|
|
1526
1622
|
"web.starting": "Starting architecture viewer...",
|
|
1527
1623
|
"web.listening": "Architecture graph available at: http://localhost:{port}",
|
|
@@ -1610,6 +1706,11 @@ var ja = {
|
|
|
1610
1706
|
"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",
|
|
1611
1707
|
// CI
|
|
1612
1708
|
"ci.generated": "GitHub Actions \u30EF\u30FC\u30AF\u30D5\u30ED\u30FC\u3092\u751F\u6210\u3057\u307E\u3057\u305F: {path}",
|
|
1709
|
+
// Layers
|
|
1710
|
+
"layers.alreadyExists": "layers.json \u306F\u65E2\u306B\u5B58\u5728\u3057\u307E\u3059\u3002\u76F4\u63A5\u7DE8\u96C6\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
1711
|
+
"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",
|
|
1712
|
+
"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",
|
|
1713
|
+
"layers.header": "\u8A2D\u5B9A\u6E08\u307F\u30EC\u30A4\u30E4\u30FC ({count}\u4EF6):",
|
|
1613
1714
|
// Web viewer
|
|
1614
1715
|
"web.starting": "\u30A2\u30FC\u30AD\u30C6\u30AF\u30C1\u30E3\u30D3\u30E5\u30FC\u30A2\u30FC\u3092\u8D77\u52D5\u4E2D...",
|
|
1615
1716
|
"web.listening": "\u30A2\u30FC\u30AD\u30C6\u30AF\u30C1\u30E3\u30B0\u30E9\u30D5: http://localhost:{port}",
|
|
@@ -1686,56 +1787,511 @@ function formatAnalysisReport(graph, options = {}) {
|
|
|
1686
1787
|
return lines.join("\n");
|
|
1687
1788
|
}
|
|
1688
1789
|
|
|
1689
|
-
// src/
|
|
1690
|
-
import {
|
|
1691
|
-
import {
|
|
1790
|
+
// src/analyzer/multi-layer.ts
|
|
1791
|
+
import { resolve as resolve4, join as join4 } from "path";
|
|
1792
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1793
|
+
var LAYER_COLORS = [
|
|
1794
|
+
"#58a6ff",
|
|
1795
|
+
"#3fb950",
|
|
1796
|
+
"#d2a8ff",
|
|
1797
|
+
"#f0883e",
|
|
1798
|
+
"#79c0ff",
|
|
1799
|
+
"#56d4dd",
|
|
1800
|
+
"#db61a2",
|
|
1801
|
+
"#f778ba",
|
|
1802
|
+
"#ffa657",
|
|
1803
|
+
"#7ee787"
|
|
1804
|
+
];
|
|
1805
|
+
async function analyzeMultiLayer(projectRoot, layerDefs) {
|
|
1806
|
+
const layers = {};
|
|
1807
|
+
const layerMetadata = [];
|
|
1808
|
+
for (let idx = 0; idx < layerDefs.length; idx++) {
|
|
1809
|
+
const def = layerDefs[idx];
|
|
1810
|
+
const targetDir = resolve4(projectRoot, def.targetDir);
|
|
1811
|
+
const graph = await analyzeProject(targetDir, {
|
|
1812
|
+
exclude: def.exclude,
|
|
1813
|
+
language: def.language
|
|
1814
|
+
});
|
|
1815
|
+
const language = def.language ?? await detectLanguage(targetDir) ?? "javascript";
|
|
1816
|
+
layers[def.name] = graph;
|
|
1817
|
+
layerMetadata.push({
|
|
1818
|
+
name: def.name,
|
|
1819
|
+
originalRootDir: graph.rootDir,
|
|
1820
|
+
language,
|
|
1821
|
+
color: def.color ?? LAYER_COLORS[idx % LAYER_COLORS.length],
|
|
1822
|
+
description: def.description,
|
|
1823
|
+
fileCount: graph.totalFiles,
|
|
1824
|
+
edgeCount: graph.totalEdges
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
const merged = mergeLayerGraphs(projectRoot, layers);
|
|
1828
|
+
return { layers, layerMetadata, merged };
|
|
1829
|
+
}
|
|
1830
|
+
function detectCrossLayerConnections(layers, layerDefs) {
|
|
1831
|
+
const MIN_NAME_LENGTH = 6;
|
|
1832
|
+
const MIN_SCORE_THRESHOLD = 10;
|
|
1833
|
+
const layerIdentifiers = /* @__PURE__ */ new Map();
|
|
1834
|
+
for (const [layerName, graph] of Object.entries(layers)) {
|
|
1835
|
+
const identifiers = /* @__PURE__ */ new Map();
|
|
1836
|
+
for (const filePath of Object.keys(graph.files)) {
|
|
1837
|
+
const basename = filePath.split("/").pop();
|
|
1838
|
+
const nameNoExt = basename.replace(/\.[^.]+$/, "");
|
|
1839
|
+
if (nameNoExt.length < MIN_NAME_LENGTH || GENERIC_BASENAMES.has(nameNoExt.toLowerCase())) continue;
|
|
1840
|
+
identifiers.set(nameNoExt, filePath);
|
|
1841
|
+
}
|
|
1842
|
+
layerIdentifiers.set(layerName, identifiers);
|
|
1843
|
+
}
|
|
1844
|
+
const nameLayerCount = /* @__PURE__ */ new Map();
|
|
1845
|
+
for (const [, ids] of layerIdentifiers) {
|
|
1846
|
+
for (const name of ids.keys()) {
|
|
1847
|
+
nameLayerCount.set(name, (nameLayerCount.get(name) ?? 0) + 1);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
const pairBest = /* @__PURE__ */ new Map();
|
|
1851
|
+
function tryAdd(pairKey, conn, score) {
|
|
1852
|
+
if (score < MIN_SCORE_THRESHOLD) return;
|
|
1853
|
+
const existing = pairBest.get(pairKey);
|
|
1854
|
+
if (!existing || score > existing.score) {
|
|
1855
|
+
pairBest.set(pairKey, { conn, score });
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
function isSelfDefined(content, name) {
|
|
1859
|
+
const defPatterns = [
|
|
1860
|
+
new RegExp(`\\b(?:class|struct|enum|interface|protocol|type|object)\\s+${escapeRegex(name)}\\b`),
|
|
1861
|
+
new RegExp(`\\b(?:def|func|fun|fn)\\s+${escapeRegex(name)}\\b`),
|
|
1862
|
+
new RegExp(`\\b${escapeRegex(name)}\\s*=\\s*(?:class|struct|type|interface)\\b`)
|
|
1863
|
+
];
|
|
1864
|
+
return defPatterns.some((re) => re.test(content));
|
|
1865
|
+
}
|
|
1866
|
+
function isLocalImportOnly(content, name) {
|
|
1867
|
+
const regex = new RegExp(`\\b${escapeRegex(name)}\\b`, "g");
|
|
1868
|
+
const lines = content.split("\n");
|
|
1869
|
+
let crossLayerRef = false;
|
|
1870
|
+
for (const line of lines) {
|
|
1871
|
+
if (!regex.test(line)) continue;
|
|
1872
|
+
regex.lastIndex = 0;
|
|
1873
|
+
const isLocalImport = /^\s*(?:from\s+[.'"]|import\s+[.'"]|require\s*\(\s*['"][.\/]|#include\s*")/.test(line);
|
|
1874
|
+
if (!isLocalImport) {
|
|
1875
|
+
crossLayerRef = true;
|
|
1876
|
+
break;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
return !crossLayerRef;
|
|
1880
|
+
}
|
|
1881
|
+
for (const [sourceLayer, graph] of Object.entries(layers)) {
|
|
1882
|
+
const ownNames = layerIdentifiers.get(sourceLayer) ?? /* @__PURE__ */ new Map();
|
|
1883
|
+
for (const filePath of Object.keys(graph.files)) {
|
|
1884
|
+
const absPath = join4(graph.rootDir, filePath);
|
|
1885
|
+
let content;
|
|
1886
|
+
try {
|
|
1887
|
+
content = readFileSync2(absPath, "utf-8");
|
|
1888
|
+
} catch {
|
|
1889
|
+
continue;
|
|
1890
|
+
}
|
|
1891
|
+
for (const [targetLayer, targetIds] of layerIdentifiers) {
|
|
1892
|
+
if (targetLayer === sourceLayer) continue;
|
|
1893
|
+
for (const [targetName, targetFile] of targetIds) {
|
|
1894
|
+
if (ownNames.has(targetName)) continue;
|
|
1895
|
+
if ((nameLayerCount.get(targetName) ?? 0) > 1) continue;
|
|
1896
|
+
if (!content.includes(targetName)) continue;
|
|
1897
|
+
const regex = new RegExp(`\\b${escapeRegex(targetName)}\\b`);
|
|
1898
|
+
if (!regex.test(content)) continue;
|
|
1899
|
+
if (isSelfDefined(content, targetName)) continue;
|
|
1900
|
+
if (isLocalImportOnly(content, targetName)) continue;
|
|
1901
|
+
const pairKey = `${sourceLayer}\u2192${targetLayer}`;
|
|
1902
|
+
const isPascalCase = /^[A-Z][a-z]/.test(targetName);
|
|
1903
|
+
const baseScore = targetName.length + (isPascalCase ? 5 : 0);
|
|
1904
|
+
tryAdd(pairKey, {
|
|
1905
|
+
fromLayer: sourceLayer,
|
|
1906
|
+
fromFile: filePath,
|
|
1907
|
+
toLayer: targetLayer,
|
|
1908
|
+
toFile: targetFile,
|
|
1909
|
+
type: "auto",
|
|
1910
|
+
label: targetName
|
|
1911
|
+
}, baseScore);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
for (const def of layerDefs) {
|
|
1915
|
+
if (def.name === sourceLayer) continue;
|
|
1916
|
+
const pairKey = `${sourceLayer}\u2192${def.name}`;
|
|
1917
|
+
const layerName = def.name;
|
|
1918
|
+
const suffixes = ["Client", "Service", "API", "Handler", "Provider", "Manager", "Gateway", "Proxy", "Adapter", "Connector"];
|
|
1919
|
+
const typedRe = new RegExp(`\\b${escapeRegex(layerName)}(?:${suffixes.join("|")})\\b`);
|
|
1920
|
+
if (typedRe.test(content)) {
|
|
1921
|
+
const targetGraph = layers[def.name];
|
|
1922
|
+
if (!targetGraph) continue;
|
|
1923
|
+
const entryFile = findEntryPoint(targetGraph);
|
|
1924
|
+
if (entryFile) {
|
|
1925
|
+
tryAdd(pairKey, {
|
|
1926
|
+
fromLayer: sourceLayer,
|
|
1927
|
+
fromFile: filePath,
|
|
1928
|
+
toLayer: def.name,
|
|
1929
|
+
toFile: entryFile,
|
|
1930
|
+
type: "auto",
|
|
1931
|
+
label: `${layerName}*`
|
|
1932
|
+
}, 25);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
for (const def of layerDefs) {
|
|
1937
|
+
if (def.name === sourceLayer) continue;
|
|
1938
|
+
const pairKey = `${sourceLayer}\u2192${def.name}`;
|
|
1939
|
+
const dirName = def.targetDir.split("/").pop();
|
|
1940
|
+
const isShortName = dirName.length <= 4;
|
|
1941
|
+
const patterns = [];
|
|
1942
|
+
if (!isShortName) {
|
|
1943
|
+
patterns.push({ re: new RegExp(`(?:from|require|import)\\s+['"].*\\b${escapeRegex(dirName)}\\b`, "i"), score: 15 });
|
|
1944
|
+
patterns.push({ re: new RegExp(`['"\`/]${escapeRegex(dirName)}/[\\w]`, "i"), score: 12 });
|
|
1945
|
+
} else {
|
|
1946
|
+
patterns.push({ re: new RegExp(`(?:from|require|import)\\s+['"].*/${escapeRegex(dirName)}/`, "i"), score: 13 });
|
|
1947
|
+
patterns.push({ re: new RegExp(`['"\`]\\s*(?:https?://[^'"]*)?/${escapeRegex(dirName)}/[\\w]`, "i"), score: 11 });
|
|
1948
|
+
}
|
|
1949
|
+
for (const { re, score } of patterns) {
|
|
1950
|
+
if (re.test(content)) {
|
|
1951
|
+
const targetGraph = layers[def.name];
|
|
1952
|
+
if (!targetGraph) continue;
|
|
1953
|
+
const entryFile = findEntryPoint(targetGraph);
|
|
1954
|
+
if (entryFile) {
|
|
1955
|
+
tryAdd(pairKey, {
|
|
1956
|
+
fromLayer: sourceLayer,
|
|
1957
|
+
fromFile: filePath,
|
|
1958
|
+
toLayer: def.name,
|
|
1959
|
+
toFile: entryFile,
|
|
1960
|
+
type: "auto",
|
|
1961
|
+
label: `\u2192 ${def.name}`
|
|
1962
|
+
}, score);
|
|
1963
|
+
}
|
|
1964
|
+
break;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
return [...pairBest.values()].map((v) => v.conn);
|
|
1971
|
+
}
|
|
1972
|
+
function escapeRegex(s) {
|
|
1973
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1974
|
+
}
|
|
1975
|
+
function findEntryPoint(graph) {
|
|
1976
|
+
const files = Object.values(graph.files);
|
|
1977
|
+
if (files.length === 0) return null;
|
|
1978
|
+
const sorted = files.sort((a, b) => b.dependents.length - a.dependents.length);
|
|
1979
|
+
if (sorted[0].dependents.length > 0) return sorted[0].path;
|
|
1980
|
+
const entryNames = ["main", "index", "app", "server", "lib", "mod"];
|
|
1981
|
+
for (const name of entryNames) {
|
|
1982
|
+
const entry = files.find((f) => {
|
|
1983
|
+
const basename = f.path.split("/").pop().replace(/\.[^.]+$/, "").toLowerCase();
|
|
1984
|
+
return basename === name;
|
|
1985
|
+
});
|
|
1986
|
+
if (entry) return entry.path;
|
|
1987
|
+
}
|
|
1988
|
+
return files[0].path;
|
|
1989
|
+
}
|
|
1990
|
+
var GENERIC_BASENAMES = /* @__PURE__ */ new Set([
|
|
1991
|
+
// Build / project structure
|
|
1992
|
+
"index",
|
|
1993
|
+
"main",
|
|
1994
|
+
"app",
|
|
1995
|
+
"config",
|
|
1996
|
+
"setup",
|
|
1997
|
+
"init",
|
|
1998
|
+
"mod",
|
|
1999
|
+
"package",
|
|
2000
|
+
"build",
|
|
2001
|
+
"makefile",
|
|
2002
|
+
"dockerfile",
|
|
2003
|
+
"rakefile",
|
|
2004
|
+
"gemfile",
|
|
2005
|
+
"podfile",
|
|
2006
|
+
// Common modules
|
|
2007
|
+
"utils",
|
|
2008
|
+
"helpers",
|
|
2009
|
+
"types",
|
|
2010
|
+
"models",
|
|
2011
|
+
"views",
|
|
2012
|
+
"controllers",
|
|
2013
|
+
"services",
|
|
2014
|
+
"lib",
|
|
2015
|
+
"src",
|
|
2016
|
+
"test",
|
|
2017
|
+
"spec",
|
|
2018
|
+
"tests",
|
|
2019
|
+
"bench",
|
|
2020
|
+
"example",
|
|
2021
|
+
"examples",
|
|
2022
|
+
// Infrastructure / patterns
|
|
2023
|
+
"server",
|
|
2024
|
+
"client",
|
|
2025
|
+
"routes",
|
|
2026
|
+
"middleware",
|
|
2027
|
+
"database",
|
|
2028
|
+
"engine",
|
|
2029
|
+
"error",
|
|
2030
|
+
"errors",
|
|
2031
|
+
"logger",
|
|
2032
|
+
"logging",
|
|
2033
|
+
"constants",
|
|
2034
|
+
"common",
|
|
2035
|
+
"base",
|
|
2036
|
+
"core",
|
|
2037
|
+
"data",
|
|
2038
|
+
"manager",
|
|
2039
|
+
"handler",
|
|
2040
|
+
"factory",
|
|
2041
|
+
"context",
|
|
2042
|
+
"state",
|
|
2043
|
+
"store",
|
|
2044
|
+
"cache",
|
|
2045
|
+
"queue",
|
|
2046
|
+
"task",
|
|
2047
|
+
"worker",
|
|
2048
|
+
"adapter",
|
|
2049
|
+
"bridge",
|
|
2050
|
+
// UI / presentation
|
|
2051
|
+
"event",
|
|
2052
|
+
"events",
|
|
2053
|
+
"model",
|
|
2054
|
+
"view",
|
|
2055
|
+
"home",
|
|
2056
|
+
"user",
|
|
2057
|
+
"page",
|
|
2058
|
+
"layout",
|
|
2059
|
+
"router",
|
|
2060
|
+
"provider",
|
|
2061
|
+
"component",
|
|
2062
|
+
"widget",
|
|
2063
|
+
"screen",
|
|
2064
|
+
"template",
|
|
2065
|
+
"header",
|
|
2066
|
+
"footer",
|
|
2067
|
+
"sidebar",
|
|
2068
|
+
"navbar",
|
|
2069
|
+
"dialog",
|
|
2070
|
+
"modal",
|
|
2071
|
+
"panel",
|
|
2072
|
+
// Data / IO
|
|
2073
|
+
"reader",
|
|
2074
|
+
"writer",
|
|
2075
|
+
"parser",
|
|
2076
|
+
"formatter",
|
|
2077
|
+
"serializer",
|
|
2078
|
+
"converter",
|
|
2079
|
+
"loader",
|
|
2080
|
+
"exporter",
|
|
2081
|
+
"importer",
|
|
2082
|
+
"transformer",
|
|
2083
|
+
"mapper",
|
|
2084
|
+
"reducer",
|
|
2085
|
+
"filter",
|
|
2086
|
+
"sorter",
|
|
2087
|
+
"validator",
|
|
2088
|
+
"checker",
|
|
2089
|
+
"scanner",
|
|
2090
|
+
"analyzer",
|
|
2091
|
+
// Auth / Security (generic enough to exist in many layers)
|
|
2092
|
+
"login",
|
|
2093
|
+
"register",
|
|
2094
|
+
"verify",
|
|
2095
|
+
"token",
|
|
2096
|
+
"session",
|
|
2097
|
+
"credential",
|
|
2098
|
+
"password",
|
|
2099
|
+
"permission",
|
|
2100
|
+
"profile",
|
|
2101
|
+
"account",
|
|
2102
|
+
"settings",
|
|
2103
|
+
// Network / API
|
|
2104
|
+
"request",
|
|
2105
|
+
"response",
|
|
2106
|
+
"endpoint",
|
|
2107
|
+
"controller",
|
|
2108
|
+
"service",
|
|
2109
|
+
"gateway",
|
|
2110
|
+
"proxy",
|
|
2111
|
+
"connector",
|
|
2112
|
+
"socket",
|
|
2113
|
+
"channel",
|
|
2114
|
+
"stream",
|
|
2115
|
+
"pipeline",
|
|
2116
|
+
// Storage / DB
|
|
2117
|
+
"schema",
|
|
2118
|
+
"migration",
|
|
2119
|
+
"seed",
|
|
2120
|
+
"fixture",
|
|
2121
|
+
"record",
|
|
2122
|
+
"entity",
|
|
2123
|
+
"repository",
|
|
2124
|
+
"storage",
|
|
2125
|
+
"driver",
|
|
2126
|
+
"connection",
|
|
2127
|
+
"pool",
|
|
2128
|
+
// Testing
|
|
2129
|
+
"mock",
|
|
2130
|
+
"stub",
|
|
2131
|
+
"fake",
|
|
2132
|
+
"helper",
|
|
2133
|
+
"fixture",
|
|
2134
|
+
"factory"
|
|
2135
|
+
]);
|
|
2136
|
+
function mergeLayerGraphs(projectRoot, layers) {
|
|
2137
|
+
const mergedFiles = {};
|
|
2138
|
+
const mergedEdges = [];
|
|
2139
|
+
const mergedCircular = [];
|
|
2140
|
+
for (const [layerName, graph] of Object.entries(layers)) {
|
|
2141
|
+
for (const [origPath, node] of Object.entries(graph.files)) {
|
|
2142
|
+
const prefixedPath = `${layerName}/${origPath}`;
|
|
2143
|
+
mergedFiles[prefixedPath] = {
|
|
2144
|
+
path: prefixedPath,
|
|
2145
|
+
exists: node.exists,
|
|
2146
|
+
dependencies: node.dependencies.map((d) => `${layerName}/${d}`),
|
|
2147
|
+
dependents: node.dependents.map((d) => `${layerName}/${d}`)
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
for (const edge of graph.edges) {
|
|
2151
|
+
mergedEdges.push({
|
|
2152
|
+
source: `${layerName}/${edge.source}`,
|
|
2153
|
+
target: `${layerName}/${edge.target}`,
|
|
2154
|
+
type: edge.type
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
for (const circ of graph.circularDependencies) {
|
|
2158
|
+
mergedCircular.push({
|
|
2159
|
+
cycle: circ.cycle.map((f) => `${layerName}/${f}`)
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
return {
|
|
2164
|
+
rootDir: resolve4(projectRoot),
|
|
2165
|
+
files: mergedFiles,
|
|
2166
|
+
edges: mergedEdges,
|
|
2167
|
+
circularDependencies: mergedCircular,
|
|
2168
|
+
totalFiles: Object.keys(mergedFiles).length,
|
|
2169
|
+
totalEdges: mergedEdges.length
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
// src/storage/layers.ts
|
|
2174
|
+
import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
|
|
2175
|
+
import { join as join5 } from "path";
|
|
1692
2176
|
import { z } from "zod";
|
|
1693
2177
|
var ARCHTRACKER_DIR = ".archtracker";
|
|
2178
|
+
var LAYERS_FILE = "layers.json";
|
|
2179
|
+
var LayerDefinitionSchema = z.object({
|
|
2180
|
+
name: z.string().min(1).regex(
|
|
2181
|
+
/^[a-zA-Z0-9_-]+$/,
|
|
2182
|
+
"Layer name must be alphanumeric (hyphens/underscores allowed)"
|
|
2183
|
+
),
|
|
2184
|
+
targetDir: z.string().min(1),
|
|
2185
|
+
language: z.enum(LANGUAGE_IDS).optional(),
|
|
2186
|
+
exclude: z.array(z.string()).optional(),
|
|
2187
|
+
color: z.string().optional(),
|
|
2188
|
+
description: z.string().optional()
|
|
2189
|
+
});
|
|
2190
|
+
var CrossLayerConnectionSchema = z.object({
|
|
2191
|
+
fromLayer: z.string(),
|
|
2192
|
+
fromFile: z.string(),
|
|
2193
|
+
toLayer: z.string(),
|
|
2194
|
+
toFile: z.string(),
|
|
2195
|
+
type: z.enum(["api-call", "event", "data-flow", "manual"]),
|
|
2196
|
+
label: z.string().optional()
|
|
2197
|
+
});
|
|
2198
|
+
var LayerConfigSchema = z.object({
|
|
2199
|
+
version: z.literal("1.0"),
|
|
2200
|
+
layers: z.array(LayerDefinitionSchema).min(1).refine(
|
|
2201
|
+
(layers) => {
|
|
2202
|
+
const names = layers.map((l) => l.name);
|
|
2203
|
+
return new Set(names).size === names.length;
|
|
2204
|
+
},
|
|
2205
|
+
{ message: "Layer names must be unique" }
|
|
2206
|
+
),
|
|
2207
|
+
connections: z.array(CrossLayerConnectionSchema).optional()
|
|
2208
|
+
});
|
|
2209
|
+
async function loadLayerConfig(projectRoot) {
|
|
2210
|
+
const filePath = join5(projectRoot, ARCHTRACKER_DIR, LAYERS_FILE);
|
|
2211
|
+
let raw;
|
|
2212
|
+
try {
|
|
2213
|
+
raw = await readFile2(filePath, "utf-8");
|
|
2214
|
+
} catch (error) {
|
|
2215
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
2216
|
+
return null;
|
|
2217
|
+
}
|
|
2218
|
+
throw new Error(`Failed to read ${filePath}`);
|
|
2219
|
+
}
|
|
2220
|
+
let parsed;
|
|
2221
|
+
try {
|
|
2222
|
+
parsed = JSON.parse(raw);
|
|
2223
|
+
} catch {
|
|
2224
|
+
throw new Error(`Invalid JSON in ${filePath}`);
|
|
2225
|
+
}
|
|
2226
|
+
const result = LayerConfigSchema.safeParse(parsed);
|
|
2227
|
+
if (!result.success) {
|
|
2228
|
+
const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).slice(0, 5).join("\n");
|
|
2229
|
+
throw new Error(`layers.json validation failed:
|
|
2230
|
+
${issues}`);
|
|
2231
|
+
}
|
|
2232
|
+
return result.data;
|
|
2233
|
+
}
|
|
2234
|
+
async function saveLayerConfig(projectRoot, config) {
|
|
2235
|
+
const dirPath = join5(projectRoot, ARCHTRACKER_DIR);
|
|
2236
|
+
const filePath = join5(dirPath, LAYERS_FILE);
|
|
2237
|
+
await mkdir(dirPath, { recursive: true });
|
|
2238
|
+
await writeFile(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2239
|
+
}
|
|
2240
|
+
function isNodeError(error) {
|
|
2241
|
+
return error instanceof Error && "code" in error;
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
// src/storage/snapshot.ts
|
|
2245
|
+
import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile3, access } from "fs/promises";
|
|
2246
|
+
import { join as join6 } from "path";
|
|
2247
|
+
import { z as z2 } from "zod";
|
|
2248
|
+
var ARCHTRACKER_DIR2 = ".archtracker";
|
|
1694
2249
|
var SNAPSHOT_FILE = "snapshot.json";
|
|
1695
|
-
var FileNodeSchema =
|
|
1696
|
-
path:
|
|
1697
|
-
exists:
|
|
1698
|
-
dependencies:
|
|
1699
|
-
dependents:
|
|
2250
|
+
var FileNodeSchema = z2.object({
|
|
2251
|
+
path: z2.string(),
|
|
2252
|
+
exists: z2.boolean(),
|
|
2253
|
+
dependencies: z2.array(z2.string()),
|
|
2254
|
+
dependents: z2.array(z2.string())
|
|
1700
2255
|
});
|
|
1701
|
-
var DependencyGraphSchema =
|
|
1702
|
-
rootDir:
|
|
1703
|
-
files:
|
|
1704
|
-
edges:
|
|
1705
|
-
source:
|
|
1706
|
-
target:
|
|
1707
|
-
type:
|
|
2256
|
+
var DependencyGraphSchema = z2.object({
|
|
2257
|
+
rootDir: z2.string(),
|
|
2258
|
+
files: z2.record(z2.string(), FileNodeSchema),
|
|
2259
|
+
edges: z2.array(z2.object({
|
|
2260
|
+
source: z2.string(),
|
|
2261
|
+
target: z2.string(),
|
|
2262
|
+
type: z2.enum(["static", "dynamic", "type-only"])
|
|
1708
2263
|
})),
|
|
1709
|
-
circularDependencies:
|
|
1710
|
-
totalFiles:
|
|
1711
|
-
totalEdges:
|
|
2264
|
+
circularDependencies: z2.array(z2.object({ cycle: z2.array(z2.string()) })),
|
|
2265
|
+
totalFiles: z2.number(),
|
|
2266
|
+
totalEdges: z2.number()
|
|
1712
2267
|
});
|
|
1713
|
-
var SnapshotSchema =
|
|
1714
|
-
version:
|
|
1715
|
-
timestamp:
|
|
1716
|
-
rootDir:
|
|
2268
|
+
var SnapshotSchema = z2.object({
|
|
2269
|
+
version: z2.enum([SCHEMA_VERSION, "1.0"]),
|
|
2270
|
+
timestamp: z2.string(),
|
|
2271
|
+
rootDir: z2.string(),
|
|
1717
2272
|
graph: DependencyGraphSchema
|
|
1718
2273
|
});
|
|
1719
|
-
async function saveSnapshot(projectRoot, graph) {
|
|
1720
|
-
const dirPath =
|
|
1721
|
-
const filePath =
|
|
2274
|
+
async function saveSnapshot(projectRoot, graph, multiLayer) {
|
|
2275
|
+
const dirPath = join6(projectRoot, ARCHTRACKER_DIR2);
|
|
2276
|
+
const filePath = join6(dirPath, SNAPSHOT_FILE);
|
|
1722
2277
|
const snapshot = {
|
|
1723
2278
|
version: SCHEMA_VERSION,
|
|
1724
2279
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1725
2280
|
rootDir: graph.rootDir,
|
|
1726
|
-
graph
|
|
2281
|
+
graph,
|
|
2282
|
+
...multiLayer ? { multiLayer } : {}
|
|
1727
2283
|
};
|
|
1728
|
-
await
|
|
1729
|
-
await
|
|
2284
|
+
await mkdir2(dirPath, { recursive: true });
|
|
2285
|
+
await writeFile2(filePath, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
1730
2286
|
return snapshot;
|
|
1731
2287
|
}
|
|
1732
2288
|
async function loadSnapshot(projectRoot) {
|
|
1733
|
-
const filePath =
|
|
2289
|
+
const filePath = join6(projectRoot, ARCHTRACKER_DIR2, SNAPSHOT_FILE);
|
|
1734
2290
|
let raw;
|
|
1735
2291
|
try {
|
|
1736
|
-
raw = await
|
|
2292
|
+
raw = await readFile3(filePath, "utf-8");
|
|
1737
2293
|
} catch (error) {
|
|
1738
|
-
if (
|
|
2294
|
+
if (isNodeError2(error) && error.code === "ENOENT") {
|
|
1739
2295
|
return null;
|
|
1740
2296
|
}
|
|
1741
2297
|
throw new StorageError(
|
|
@@ -1762,7 +2318,7 @@ async function loadSnapshot(projectRoot) {
|
|
|
1762
2318
|
}
|
|
1763
2319
|
async function hasArchtrackerDir(projectRoot) {
|
|
1764
2320
|
try {
|
|
1765
|
-
await access(
|
|
2321
|
+
await access(join6(projectRoot, ARCHTRACKER_DIR2));
|
|
1766
2322
|
return true;
|
|
1767
2323
|
} catch {
|
|
1768
2324
|
return false;
|
|
@@ -1774,7 +2330,7 @@ var StorageError = class extends Error {
|
|
|
1774
2330
|
this.name = "StorageError";
|
|
1775
2331
|
}
|
|
1776
2332
|
};
|
|
1777
|
-
function
|
|
2333
|
+
function isNodeError2(error) {
|
|
1778
2334
|
return error instanceof Error && "code" in error;
|
|
1779
2335
|
}
|
|
1780
2336
|
|
|
@@ -1878,13 +2434,17 @@ export {
|
|
|
1878
2434
|
AnalyzerError,
|
|
1879
2435
|
SCHEMA_VERSION,
|
|
1880
2436
|
StorageError,
|
|
2437
|
+
analyzeMultiLayer,
|
|
1881
2438
|
analyzeProject,
|
|
1882
2439
|
computeDiff,
|
|
2440
|
+
detectCrossLayerConnections,
|
|
1883
2441
|
formatAnalysisReport,
|
|
1884
2442
|
formatDiffReport,
|
|
1885
2443
|
getLocale,
|
|
1886
2444
|
hasArchtrackerDir,
|
|
2445
|
+
loadLayerConfig,
|
|
1887
2446
|
loadSnapshot,
|
|
2447
|
+
saveLayerConfig,
|
|
1888
2448
|
saveSnapshot,
|
|
1889
2449
|
setLocale,
|
|
1890
2450
|
t
|