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/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";
@@ -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
- if (importPath.includes("/") || importPath.endsWith(".php")) {
1126
- 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";
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
- // Bug #10 fix: import ModuleName and @testable import ModuleName
1148
- { regex: /^(?:@testable\s+)?import\s+(?:class\s+|struct\s+|enum\s+|protocol\s+|func\s+|var\s+|let\s+|typealias\s+)?(\w+)/gm }
1149
- ],
1150
- 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;
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 filePath = cleanPath.replace(/\./g, "/");
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 full = join3(rootDir, "lib", relPath);
1275
- 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;
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/storage/snapshot.ts
1690
- import { mkdir, writeFile, readFile as readFile2, access } from "fs/promises";
1691
- import { join as join4 } from "path";
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 = z.object({
1696
- path: z.string(),
1697
- exists: z.boolean(),
1698
- dependencies: z.array(z.string()),
1699
- dependents: z.array(z.string())
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 = z.object({
1702
- rootDir: z.string(),
1703
- files: z.record(z.string(), FileNodeSchema),
1704
- edges: z.array(z.object({
1705
- source: z.string(),
1706
- target: z.string(),
1707
- type: z.enum(["static", "dynamic", "type-only"])
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: z.array(z.object({ cycle: z.array(z.string()) })),
1710
- totalFiles: z.number(),
1711
- totalEdges: z.number()
2264
+ circularDependencies: z2.array(z2.object({ cycle: z2.array(z2.string()) })),
2265
+ totalFiles: z2.number(),
2266
+ totalEdges: z2.number()
1712
2267
  });
1713
- var SnapshotSchema = z.object({
1714
- version: z.literal(SCHEMA_VERSION),
1715
- timestamp: z.string(),
1716
- rootDir: z.string(),
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 = join4(projectRoot, ARCHTRACKER_DIR);
1721
- const filePath = join4(dirPath, SNAPSHOT_FILE);
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 mkdir(dirPath, { recursive: true });
1729
- await writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf-8");
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 = join4(projectRoot, ARCHTRACKER_DIR, SNAPSHOT_FILE);
2289
+ const filePath = join6(projectRoot, ARCHTRACKER_DIR2, SNAPSHOT_FILE);
1734
2290
  let raw;
1735
2291
  try {
1736
- raw = await readFile2(filePath, "utf-8");
2292
+ raw = await readFile3(filePath, "utf-8");
1737
2293
  } catch (error) {
1738
- if (isNodeError(error) && error.code === "ENOENT") {
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(join4(projectRoot, ARCHTRACKER_DIR));
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 isNodeError(error) {
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