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/README.md +189 -14
- package/dist/bin.js +1 -1
- package/dist/bin.js.map +1 -1
- package/dist/cli/index.js +1549 -125
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +94 -4
- package/dist/index.js +624 -32
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.js +716 -74
- package/dist/mcp/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/arch-analyze/SKILL.md +6 -2
- package/skills/arch-check/SKILL.md +11 -8
- package/skills/arch-context/SKILL.md +8 -6
- package/skills/arch-search/SKILL.md +7 -7
- package/skills/arch-serve/SKILL.md +27 -0
- package/skills/arch-snapshot/SKILL.md +9 -6
package/dist/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";
|
|
@@ -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:
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1125
|
-
|
|
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
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
|
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
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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
|
|
1243
|
-
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;
|
|
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
|
|
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.
|
|
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 =
|
|
1689
|
-
const filePath =
|
|
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 =
|
|
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(
|
|
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
|