archtracker-mcp 0.4.2 → 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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/types/schema.ts
2
- var SCHEMA_VERSION = "1.0";
2
+ var SCHEMA_VERSION = "1.1";
3
3
 
4
4
  // src/analyzer/analyze.ts
5
5
  import { resolve as resolve3 } from "path";
@@ -600,7 +600,7 @@ var RegexEngine = class {
600
600
  continue;
601
601
  }
602
602
  const stripped = stripComments(content, this.config.commentStyle);
603
- const imports = this.extractImports(stripped);
603
+ const imports = this.extractImports(stripped, filePath, absRootDir, projectFileSet);
604
604
  for (const importPath of imports) {
605
605
  const resolved = this.config.resolveImport(
606
606
  importPath,
@@ -634,9 +634,9 @@ var RegexEngine = class {
634
634
  totalEdges: edges.length
635
635
  };
636
636
  }
637
- extractImports(content) {
637
+ extractImports(content, filePath, rootDir, projectFiles) {
638
638
  if (this.config.extractImports) {
639
- return this.config.extractImports(content);
639
+ return this.config.extractImports(content, filePath, rootDir, projectFiles);
640
640
  }
641
641
  const imports = [];
642
642
  for (const pattern of this.config.importPatterns) {
@@ -813,24 +813,44 @@ 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"],
819
838
  commentStyle: "python",
820
839
  importPatterns: [
821
- // from package.module import something
822
- { regex: /^from\s+(\.[\w.]*|\w[\w.]*)\s+import\b/gm }
840
+ // from package.module import something (including indented, e.g. inside try/except)
841
+ { regex: /^\s*from\s+(\.[\w.]*|\w[\w.]*)\s+import\b/gm }
823
842
  // import package.module (handled by extractImports for multi-module case)
824
843
  ],
825
844
  // Bug #1 fix: custom extractImports to handle `import a, b, c`
845
+ // Bug #12 fix: allow leading whitespace to catch try/except indented imports
826
846
  extractImports(content) {
827
847
  const imports = [];
828
- const fromRegex = /^from\s+(\.[\w.]*|\w[\w.]*)\s+import\b/gm;
848
+ const fromRegex = /^\s*from\s+(\.[\w.]*|\w[\w.]*)\s+import\b/gm;
829
849
  let match;
830
850
  while ((match = fromRegex.exec(content)) !== null) {
831
851
  imports.push(match[1]);
832
852
  }
833
- const importRegex = /^import\s+([\w.]+(?:\s*,\s*[\w.]+)*)/gm;
853
+ const importRegex = /^\s*import\s+([\w.]+(?:\s*,\s*[\w.]+)*)/gm;
834
854
  while ((match = importRegex.exec(content)) !== null) {
835
855
  const modules = match[1].split(",");
836
856
  for (const mod of modules) {
@@ -1063,6 +1083,13 @@ var java = {
1063
1083
  if (projectFiles.has(full)) return full;
1064
1084
  }
1065
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
+ }
1066
1093
  return null;
1067
1094
  },
1068
1095
  defaultExclude: ["build", "target", "\\.gradle", "\\.idea"]
@@ -1117,12 +1144,18 @@ var php = {
1117
1144
  importPatterns: [
1118
1145
  // require/include/require_once/include_once 'path'
1119
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 },
1120
1149
  // Bug #9 fix: use Namespace\Class — skip `function` and `const` keywords
1121
1150
  { regex: /^use\s+(?:function\s+|const\s+)?([\w\\]+)/gm }
1122
1151
  ],
1123
1152
  resolveImport(importPath, sourceFile, rootDir, projectFiles) {
1124
- if (importPath.includes("/") || importPath.endsWith(".php")) {
1125
- const withExt = importPath.endsWith(".php") ? importPath : importPath + ".php";
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";
1126
1159
  const fromSource = resolve2(dirname(sourceFile), withExt);
1127
1160
  if (projectFiles.has(fromSource)) return fromSource;
1128
1161
  const fromRoot2 = join3(rootDir, withExt);
@@ -1138,15 +1171,46 @@ var php = {
1138
1171
  },
1139
1172
  defaultExclude: ["vendor"]
1140
1173
  };
1174
+ var SWIFT_SKIP_FILES = /* @__PURE__ */ new Set(["Package", "main", "AppDelegate", "SceneDelegate"]);
1141
1175
  var swift = {
1142
1176
  id: "swift",
1143
1177
  extensions: [".swift"],
1144
1178
  commentStyle: "c-style",
1145
- importPatterns: [
1146
- // Bug #10 fix: import ModuleName and @testable import ModuleName
1147
- { regex: /^(?:@testable\s+)?import\s+(?:class\s+|struct\s+|enum\s+|protocol\s+|func\s+|var\s+|let\s+|typealias\s+)?(\w+)/gm }
1148
- ],
1149
- resolveImport(importPath, sourceFile, rootDir, projectFiles) {
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;
1150
1214
  const spmDir = join3(rootDir, "Sources", importPath);
1151
1215
  for (const f of projectFiles) {
1152
1216
  if (f.startsWith(spmDir + "/") && f.endsWith(".swift")) return f;
@@ -1171,7 +1235,8 @@ var kotlin = {
1171
1235
  if (cleanPath.endsWith(".")) {
1172
1236
  cleanPath = cleanPath.slice(0, -1);
1173
1237
  }
1174
- const filePath = cleanPath.replace(/\./g, "/");
1238
+ const segments = cleanPath.split(".");
1239
+ const filePath = segments.join("/");
1175
1240
  for (const ext of [".kt", ".kts"]) {
1176
1241
  for (const srcRoot of [
1177
1242
  "",
@@ -1185,21 +1250,68 @@ var kotlin = {
1185
1250
  if (projectFiles.has(full)) return full;
1186
1251
  }
1187
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
+ }
1188
1269
  return null;
1189
1270
  },
1190
1271
  defaultExclude: ["build", "\\.gradle", "\\.idea"]
1191
1272
  };
1273
+ var CS_SKIP_CLASSNAMES = /* @__PURE__ */ new Set(["AssemblyInfo", "GlobalUsings"]);
1192
1274
  var cSharp = {
1193
1275
  id: "c-sharp",
1194
1276
  extensions: [".cs"],
1195
1277
  commentStyle: "c-style",
1196
- importPatterns: [
1197
- // using Namespace; and using Namespace.SubNamespace;
1198
- // using static Namespace.Class;
1199
- // Skip: using Alias = Namespace.Class; (captured but resolved same way)
1200
- { regex: /^using\s+(?:static\s+)?([\w.]+)\s*;/gm }
1201
- ],
1278
+ importPatterns: [],
1279
+ // handled by extractImports
1280
+ extractImports(content, filePath, _rootDir, projectFiles) {
1281
+ const imports = [];
1282
+ const usingRegex = /^\s*(?:global\s+)?using\s+(?:static\s+)?([\w.]+)\s*;/gm;
1283
+ let match;
1284
+ while ((match = usingRegex.exec(content)) !== null) {
1285
+ imports.push(match[1]);
1286
+ }
1287
+ const classMap = /* @__PURE__ */ new Map();
1288
+ for (const f of projectFiles) {
1289
+ if (f === filePath) continue;
1290
+ if (!f.endsWith(".cs")) continue;
1291
+ const basename = f.split("/").pop();
1292
+ const className = basename.replace(/\.xaml\.cs$/i, "").replace(/\.cs$/i, "");
1293
+ if (!className || CS_SKIP_CLASSNAMES.has(className)) continue;
1294
+ classMap.set(className, f);
1295
+ }
1296
+ if (classMap.size > 0) {
1297
+ const escaped = [...classMap.keys()].map(
1298
+ (n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
1299
+ );
1300
+ const combined = new RegExp(`\\b(${escaped.join("|")})\\b`, "g");
1301
+ const matched = /* @__PURE__ */ new Set();
1302
+ while ((match = combined.exec(content)) !== null) {
1303
+ const className = match[1];
1304
+ const targetPath = classMap.get(className);
1305
+ if (targetPath && !matched.has(targetPath)) {
1306
+ matched.add(targetPath);
1307
+ imports.push(targetPath);
1308
+ }
1309
+ }
1310
+ }
1311
+ return imports;
1312
+ },
1202
1313
  resolveImport(importPath, _sourceFile, rootDir, projectFiles) {
1314
+ if (projectFiles.has(importPath)) return importPath;
1203
1315
  const segments = importPath.split(".");
1204
1316
  for (let i = segments.length; i > 0; i--) {
1205
1317
  const filePath = segments.slice(0, i).join("/") + ".cs";
@@ -1239,8 +1351,10 @@ var dart = {
1239
1351
  const prefix = `package:${ownPackage}/`;
1240
1352
  if (!importPath.startsWith(prefix)) return null;
1241
1353
  const relPath = importPath.slice(prefix.length);
1242
- const full = join3(rootDir, "lib", relPath);
1243
- if (projectFiles.has(full)) return full;
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;
1244
1358
  return null;
1245
1359
  }
1246
1360
  const resolved = resolve2(dirname(sourceFile), importPath);
@@ -1303,6 +1417,15 @@ var scala = {
1303
1417
  }
1304
1418
  }
1305
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
+ }
1306
1429
  return null;
1307
1430
  },
1308
1431
  defaultExclude: ["target", "\\.bsp", "\\.metals", "\\.bloop"]
@@ -1490,6 +1613,11 @@ var en = {
1490
1613
  "analyze.snapshotSaved": "\nSnapshot saved alongside analysis.",
1491
1614
  // CI
1492
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}):",
1493
1621
  // Web viewer
1494
1622
  "web.starting": "Starting architecture viewer...",
1495
1623
  "web.listening": "Architecture graph available at: http://localhost:{port}",
@@ -1578,6 +1706,11 @@ var ja = {
1578
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",
1579
1707
  // CI
1580
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):",
1581
1714
  // Web viewer
1582
1715
  "web.starting": "\u30A2\u30FC\u30AD\u30C6\u30AF\u30C1\u30E3\u30D3\u30E5\u30FC\u30A2\u30FC\u3092\u8D77\u52D5\u4E2D...",
1583
1716
  "web.listening": "\u30A2\u30FC\u30AD\u30C6\u30AF\u30C1\u30E3\u30B0\u30E9\u30D5: http://localhost:{port}",
@@ -1654,9 +1787,392 @@ function formatAnalysisReport(graph, options = {}) {
1654
1787
  return lines.join("\n");
1655
1788
  }
1656
1789
 
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
+
1657
2173
  // src/storage/snapshot.ts
1658
2174
  import { mkdir, writeFile, readFile as readFile2, access } from "fs/promises";
1659
- import { join as join4 } from "path";
2175
+ import { join as join5 } from "path";
1660
2176
  import { z } from "zod";
1661
2177
  var ARCHTRACKER_DIR = ".archtracker";
1662
2178
  var SNAPSHOT_FILE = "snapshot.json";
@@ -1679,26 +2195,27 @@ var DependencyGraphSchema = z.object({
1679
2195
  totalEdges: z.number()
1680
2196
  });
1681
2197
  var SnapshotSchema = z.object({
1682
- version: z.literal(SCHEMA_VERSION),
2198
+ version: z.enum([SCHEMA_VERSION, "1.0"]),
1683
2199
  timestamp: z.string(),
1684
2200
  rootDir: z.string(),
1685
2201
  graph: DependencyGraphSchema
1686
2202
  });
1687
- async function saveSnapshot(projectRoot, graph) {
1688
- const dirPath = join4(projectRoot, ARCHTRACKER_DIR);
1689
- const filePath = join4(dirPath, SNAPSHOT_FILE);
2203
+ async function saveSnapshot(projectRoot, graph, multiLayer) {
2204
+ const dirPath = join5(projectRoot, ARCHTRACKER_DIR);
2205
+ const filePath = join5(dirPath, SNAPSHOT_FILE);
1690
2206
  const snapshot = {
1691
2207
  version: SCHEMA_VERSION,
1692
2208
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1693
2209
  rootDir: graph.rootDir,
1694
- graph
2210
+ graph,
2211
+ ...multiLayer ? { multiLayer } : {}
1695
2212
  };
1696
2213
  await mkdir(dirPath, { recursive: true });
1697
2214
  await writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf-8");
1698
2215
  return snapshot;
1699
2216
  }
1700
2217
  async function loadSnapshot(projectRoot) {
1701
- const filePath = join4(projectRoot, ARCHTRACKER_DIR, SNAPSHOT_FILE);
2218
+ const filePath = join5(projectRoot, ARCHTRACKER_DIR, SNAPSHOT_FILE);
1702
2219
  let raw;
1703
2220
  try {
1704
2221
  raw = await readFile2(filePath, "utf-8");
@@ -1730,7 +2247,7 @@ async function loadSnapshot(projectRoot) {
1730
2247
  }
1731
2248
  async function hasArchtrackerDir(projectRoot) {
1732
2249
  try {
1733
- await access(join4(projectRoot, ARCHTRACKER_DIR));
2250
+ await access(join5(projectRoot, ARCHTRACKER_DIR));
1734
2251
  return true;
1735
2252
  } catch {
1736
2253
  return false;
@@ -1842,17 +2359,92 @@ function arraysEqual(a, b) {
1842
2359
  }
1843
2360
  return true;
1844
2361
  }
2362
+
2363
+ // src/storage/layers.ts
2364
+ import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
2365
+ import { join as join6 } from "path";
2366
+ import { z as z2 } from "zod";
2367
+ var ARCHTRACKER_DIR2 = ".archtracker";
2368
+ var LAYERS_FILE = "layers.json";
2369
+ var LayerDefinitionSchema = z2.object({
2370
+ name: z2.string().min(1).regex(
2371
+ /^[a-zA-Z0-9_-]+$/,
2372
+ "Layer name must be alphanumeric (hyphens/underscores allowed)"
2373
+ ),
2374
+ targetDir: z2.string().min(1),
2375
+ language: z2.enum(LANGUAGE_IDS).optional(),
2376
+ exclude: z2.array(z2.string()).optional(),
2377
+ color: z2.string().optional(),
2378
+ description: z2.string().optional()
2379
+ });
2380
+ var CrossLayerConnectionSchema = z2.object({
2381
+ fromLayer: z2.string(),
2382
+ fromFile: z2.string(),
2383
+ toLayer: z2.string(),
2384
+ toFile: z2.string(),
2385
+ type: z2.enum(["api-call", "event", "data-flow", "manual"]),
2386
+ label: z2.string().optional()
2387
+ });
2388
+ var LayerConfigSchema = z2.object({
2389
+ version: z2.literal("1.0"),
2390
+ layers: z2.array(LayerDefinitionSchema).min(1).refine(
2391
+ (layers) => {
2392
+ const names = layers.map((l) => l.name);
2393
+ return new Set(names).size === names.length;
2394
+ },
2395
+ { message: "Layer names must be unique" }
2396
+ ),
2397
+ connections: z2.array(CrossLayerConnectionSchema).optional()
2398
+ });
2399
+ async function loadLayerConfig(projectRoot) {
2400
+ const filePath = join6(projectRoot, ARCHTRACKER_DIR2, LAYERS_FILE);
2401
+ let raw;
2402
+ try {
2403
+ raw = await readFile3(filePath, "utf-8");
2404
+ } catch (error) {
2405
+ if (isNodeError2(error) && error.code === "ENOENT") {
2406
+ return null;
2407
+ }
2408
+ throw new Error(`Failed to read ${filePath}`);
2409
+ }
2410
+ let parsed;
2411
+ try {
2412
+ parsed = JSON.parse(raw);
2413
+ } catch {
2414
+ throw new Error(`Invalid JSON in ${filePath}`);
2415
+ }
2416
+ const result = LayerConfigSchema.safeParse(parsed);
2417
+ if (!result.success) {
2418
+ const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).slice(0, 5).join("\n");
2419
+ throw new Error(`layers.json validation failed:
2420
+ ${issues}`);
2421
+ }
2422
+ return result.data;
2423
+ }
2424
+ async function saveLayerConfig(projectRoot, config) {
2425
+ const dirPath = join6(projectRoot, ARCHTRACKER_DIR2);
2426
+ const filePath = join6(dirPath, LAYERS_FILE);
2427
+ await mkdir2(dirPath, { recursive: true });
2428
+ await writeFile2(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2429
+ }
2430
+ function isNodeError2(error) {
2431
+ return error instanceof Error && "code" in error;
2432
+ }
1845
2433
  export {
1846
2434
  AnalyzerError,
1847
2435
  SCHEMA_VERSION,
1848
2436
  StorageError,
2437
+ analyzeMultiLayer,
1849
2438
  analyzeProject,
1850
2439
  computeDiff,
2440
+ detectCrossLayerConnections,
1851
2441
  formatAnalysisReport,
1852
2442
  formatDiffReport,
1853
2443
  getLocale,
1854
2444
  hasArchtrackerDir,
2445
+ loadLayerConfig,
1855
2446
  loadSnapshot,
2447
+ saveLayerConfig,
1856
2448
  saveSnapshot,
1857
2449
  setLocale,
1858
2450
  t