@chappibunny/repolens 1.15.0 → 1.15.2
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/CHANGELOG.md +56 -0
- package/README.md +84 -8
- package/llms-full.txt +84 -0
- package/llms-install.md +6 -0
- package/llms.txt +50 -0
- package/package.json +4 -2
- package/src/analyzers/complexity-analyzer.js +143 -8
- package/src/analyzers/context-builder.js +91 -77
- package/src/analyzers/dependency-graph.js +102 -48
- package/src/analyzers/domain-inference.js +45 -7
- package/src/analyzers/import-parsers.js +716 -0
- package/src/badge.js +274 -0
- package/src/cli-args.js +11 -1
- package/src/cli.js +24 -1
- package/src/core/scan.js +4 -0
- package/src/delivery/architecture-comment.js +480 -0
- package/src/delivery/comment-constants.js +15 -0
- package/src/delivery/comment.js +9 -5
- package/src/docs/generate-doc-set.js +3 -2
- package/src/docs/llms-text.js +180 -0
- package/src/help.js +79 -5
- package/src/renderers/render.js +33 -32
- package/src/renderers/renderAnalysis.js +4 -4
- package/src/watch.js +13 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Build structured AI context from deterministic scan results
|
|
2
2
|
|
|
3
|
-
import { groupModulesByDomain } from "./domain-inference.js";
|
|
3
|
+
import { groupModulesByDomain, pathHasKeyword } from "./domain-inference.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Compute per-module dependency metrics from the dep graph.
|
|
@@ -91,7 +91,7 @@ export function buildAIContext(scanResult, config, depGraph = null) {
|
|
|
91
91
|
: [];
|
|
92
92
|
|
|
93
93
|
// Group modules by business domain
|
|
94
|
-
const domainGroups = groupModulesByDomain(modules, customHints);
|
|
94
|
+
const domainGroups = groupModulesByDomain(modules, customHints, depGraph);
|
|
95
95
|
|
|
96
96
|
// Compute module-level dependency metrics
|
|
97
97
|
const moduleMetrics = computeModuleLevelMetrics(depGraph, modules);
|
|
@@ -175,32 +175,32 @@ export function buildAIContext(scanResult, config, depGraph = null) {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
function inferModuleType(modulePath) {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
if (
|
|
182
|
-
if (
|
|
183
|
-
if (
|
|
184
|
-
if (
|
|
185
|
-
if (
|
|
186
|
-
if (
|
|
187
|
-
if (
|
|
188
|
-
if (
|
|
189
|
-
if (
|
|
190
|
-
if (
|
|
191
|
-
if (
|
|
192
|
-
if (
|
|
193
|
-
if (
|
|
194
|
-
if (
|
|
195
|
-
if (
|
|
196
|
-
if (
|
|
197
|
-
if (
|
|
198
|
-
if (
|
|
199
|
-
if (
|
|
200
|
-
if (
|
|
201
|
-
if (
|
|
202
|
-
if (
|
|
203
|
-
|
|
178
|
+
const has = (keyword) => pathHasKeyword(modulePath, keyword);
|
|
179
|
+
|
|
180
|
+
if (has("test") || has("spec")) return "test";
|
|
181
|
+
if (has("api") || has("endpoint")) return "api";
|
|
182
|
+
if (has("component") || has("widget")) return "ui";
|
|
183
|
+
if (has("lib") || has("util") || has("helper") || has("common") || has("shared")) return "library";
|
|
184
|
+
if (has("hook")) return "hooks";
|
|
185
|
+
if (has("store") || has("state") || has("redux") || has("zustand")) return "state";
|
|
186
|
+
if (has("page") || has("route") || has("view")) return "route";
|
|
187
|
+
if (has("config") || has("setting") || has("env")) return "config";
|
|
188
|
+
if (has("core") || has("kernel") || has("foundation")) return "core";
|
|
189
|
+
if (has("render") || has("template") || has("format")) return "renderer";
|
|
190
|
+
if (has("publish") || has("output") || has("export")) return "publisher";
|
|
191
|
+
if (has("analyz") || has("inspect") || has("detect")) return "analyzer";
|
|
192
|
+
if (has("plugin") || has("extension") || has("addon")) return "plugin";
|
|
193
|
+
if (has("deliver") || has("dispatch") || has("send")) return "delivery";
|
|
194
|
+
if (has("doc") || has("generate")) return "documentation";
|
|
195
|
+
if (has("integrat") || has("connect") || has("adapter")) return "integration";
|
|
196
|
+
if (has("cli") || has("command") || has("bin")) return "cli";
|
|
197
|
+
if (has("ai") || has("llm") || has("openai") || has("prompt")) return "ai";
|
|
198
|
+
if (has("auth") || has("login") || has("session")) return "auth";
|
|
199
|
+
if (has("data") || has("db") || has("schema")) return "data";
|
|
200
|
+
if (has("middleware")) return "middleware";
|
|
201
|
+
if (has("service")) return "service";
|
|
202
|
+
if (has("app")) return "app";
|
|
203
|
+
|
|
204
204
|
return "other";
|
|
205
205
|
}
|
|
206
206
|
|
|
@@ -208,7 +208,34 @@ function inferArchitecturalPatterns(modules, depGraph = null) {
|
|
|
208
208
|
const patterns = [];
|
|
209
209
|
const keys = modules.map(m => m.key.toLowerCase());
|
|
210
210
|
|
|
211
|
-
const has = (keyword) => keys.some(
|
|
211
|
+
const has = (keyword) => keys.some((key) => (
|
|
212
|
+
keyword.includes("/") ? key.includes(keyword) : pathHasKeyword(key, keyword)
|
|
213
|
+
));
|
|
214
|
+
const matchingKeys = (keywords) => {
|
|
215
|
+
const found = [];
|
|
216
|
+
for (const keyword of keywords) {
|
|
217
|
+
for (const module of modules) {
|
|
218
|
+
const key = module.key;
|
|
219
|
+
const matched = keyword.includes("/")
|
|
220
|
+
? key.toLowerCase().includes(keyword)
|
|
221
|
+
: pathHasKeyword(key, keyword);
|
|
222
|
+
if (matched && !found.includes(key)) found.push(key);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return found;
|
|
226
|
+
};
|
|
227
|
+
const cite = (label, keywords, extra = "") => {
|
|
228
|
+
const found = matchingKeys(keywords);
|
|
229
|
+
if (found.length === 0 && !extra) return;
|
|
230
|
+
const sample = found.slice(0, 3).map((key) => `\`${key}\``).join(", ");
|
|
231
|
+
const more = found.length > 3 ? ` (+${found.length - 3} more)` : "";
|
|
232
|
+
const where = sample ? `${sample}${more}` : "";
|
|
233
|
+
const detail = [where, extra].filter(Boolean).join("; ");
|
|
234
|
+
patterns.push(detail ? `${label} — ${detail}` : label);
|
|
235
|
+
};
|
|
236
|
+
const externalNames = depGraph?.externalDeps || [];
|
|
237
|
+
const importsReact = externalNames.some((name) => name === "react" || name === "react-dom" || name.startsWith("react/"));
|
|
238
|
+
const importsAi = externalNames.some((name) => /(^|\/)(openai|anthropic|langchain|ollama)(\/|$)/i.test(name) || name.startsWith("@anthropic-ai/") || name.startsWith("@ai-sdk/"));
|
|
212
239
|
|
|
213
240
|
// Compute module-level metrics for verification
|
|
214
241
|
const moduleMetrics = depGraph ? computeModuleLevelMetrics(depGraph, modules) : new Map();
|
|
@@ -230,69 +257,56 @@ function inferArchitecturalPatterns(modules, depGraph = null) {
|
|
|
230
257
|
return false;
|
|
231
258
|
};
|
|
232
259
|
|
|
233
|
-
// Web framework patterns
|
|
234
|
-
if (has("app/"))
|
|
235
|
-
if (has("pages/"))
|
|
236
|
-
if (has("component") && has("lib"))
|
|
237
|
-
if (has("hook"))
|
|
238
|
-
if (has("store") || has("state") || has("redux") || has("zustand"))
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
260
|
+
// Web framework patterns. Each line names the modules (or imports) that triggered it.
|
|
261
|
+
if (has("app/")) cite("Next.js App Router", ["app/"]);
|
|
262
|
+
if (has("pages/")) cite("Next.js Pages Router", ["pages/"]);
|
|
263
|
+
if (has("component") && has("lib")) cite("Layered component architecture", ["component", "lib"]);
|
|
264
|
+
if (has("hook") && importsReact) cite("React hooks pattern", ["hook"], "imports react");
|
|
265
|
+
if (has("store") || has("state") || has("redux") || has("zustand")) {
|
|
266
|
+
cite("Centralized state management", ["store", "state", "redux", "zustand"]);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (has("api") || has("endpoint")) cite("API route pattern", ["api", "endpoint"]);
|
|
242
270
|
|
|
243
271
|
if (has("core") || has("kernel")) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
} else {
|
|
249
|
-
patterns.push("Core/kernel architecture");
|
|
250
|
-
}
|
|
272
|
+
const note = moduleMetrics.size > 0 && verifyHub("core")
|
|
273
|
+
? "high fan-in"
|
|
274
|
+
: "";
|
|
275
|
+
cite("Core/kernel architecture", ["core", "kernel"], note);
|
|
251
276
|
}
|
|
252
277
|
|
|
253
278
|
if (has("plugin") || has("extension")) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
} else if (moduleMetrics.size > 0) {
|
|
257
|
-
patterns.push("Plugin system (naming only)");
|
|
258
|
-
} else {
|
|
259
|
-
patterns.push("Plugin system");
|
|
260
|
-
}
|
|
279
|
+
const verified = moduleMetrics.size > 0 && (verifyHub("plugin") || verifyOrchestrator("plugin") || verifyHub("loader"));
|
|
280
|
+
cite("Plugin system", ["plugin", "extension"], verified ? "loader or registry fan-in" : "");
|
|
261
281
|
}
|
|
262
282
|
|
|
263
|
-
if (has("middleware"))
|
|
283
|
+
if (has("middleware")) cite("Middleware pipeline", ["middleware"]);
|
|
264
284
|
|
|
265
285
|
if (has("render") || has("template")) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
} else {
|
|
269
|
-
patterns.push("Renderer pipeline");
|
|
270
|
-
}
|
|
286
|
+
const note = moduleMetrics.size > 0 && verifyOrchestrator("render") ? "multi-output fan-out" : "";
|
|
287
|
+
cite("Renderer pipeline", ["render", "template"], note);
|
|
271
288
|
}
|
|
272
289
|
|
|
273
290
|
if (has("publish") || has("output")) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
} else {
|
|
277
|
-
patterns.push("Multi-output publishing");
|
|
278
|
-
}
|
|
291
|
+
const note = moduleMetrics.size > 0 && verifyOrchestrator("publish") ? "multiple publish targets" : "";
|
|
292
|
+
cite("Multi-output publishing", ["publish", "output"], note);
|
|
279
293
|
}
|
|
280
294
|
|
|
281
|
-
if (has("analyz") || has("detect") || has("inspect"))
|
|
282
|
-
if (has("cli") || has("command") || has("bin"))
|
|
283
|
-
|
|
295
|
+
if (has("analyz") || has("detect") || has("inspect")) cite("Analysis pipeline", ["analyz", "detect", "inspect"]);
|
|
296
|
+
if (has("cli") || has("command") || has("bin")) cite("CLI tool architecture", ["cli", "command", "bin"]);
|
|
297
|
+
|
|
284
298
|
if (has("util") || has("helper") || has("lib")) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
} else {
|
|
288
|
-
patterns.push("Shared utility layer");
|
|
289
|
-
}
|
|
299
|
+
const verified = moduleMetrics.size > 0 && verifyHub("util") || verifyHub("helper") || verifyHub("lib");
|
|
300
|
+
cite("Shared utility layer", ["util", "helper", "lib"], verified ? "high fan-in" : "");
|
|
290
301
|
}
|
|
291
|
-
|
|
292
|
-
if (has("integrat") || has("adapter") || has("connect"))
|
|
293
|
-
if (has("ai") || has("
|
|
294
|
-
|
|
295
|
-
|
|
302
|
+
|
|
303
|
+
if (has("integrat") || has("adapter") || has("connect")) cite("Integration adapters", ["integrat", "adapter", "connect"]);
|
|
304
|
+
if (has("ai") || has("llm") || has("openai") || has("anthropic") || importsAi) {
|
|
305
|
+
const aiPackages = externalNames.filter((name) => /(^|\/)(openai|anthropic|langchain|ollama)(\/|$)/i.test(name) || name.startsWith("@anthropic-ai/") || name.startsWith("@ai-sdk/"));
|
|
306
|
+
cite("AI/LLM integration", ["ai", "llm", "openai", "anthropic"], aiPackages.length ? `imports ${aiPackages.slice(0, 3).join(", ")}` : "");
|
|
307
|
+
}
|
|
308
|
+
if (has("deliver") || has("dispatch")) cite("Delivery pipeline", ["deliver", "dispatch"]);
|
|
309
|
+
if (has("test") || has("spec")) cite("Dedicated test infrastructure", ["test", "spec"]);
|
|
296
310
|
|
|
297
311
|
// Structural patterns from dep graph (not keyword-based)
|
|
298
312
|
if (depGraph?.stats) {
|
|
@@ -334,7 +348,7 @@ export function buildModuleContext(modules, config, depGraph = null) {
|
|
|
334
348
|
}))
|
|
335
349
|
: [];
|
|
336
350
|
|
|
337
|
-
const domainGroups = groupModulesByDomain(modules, customHints);
|
|
351
|
+
const domainGroups = groupModulesByDomain(modules, customHints, depGraph);
|
|
338
352
|
const moduleMetrics = computeModuleLevelMetrics(depGraph, modules);
|
|
339
353
|
|
|
340
354
|
return modules.map(module => {
|
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
// Dependency graph analysis with cycle detection
|
|
2
|
-
//
|
|
1
|
+
// Dependency graph analysis with cycle detection.
|
|
2
|
+
// JavaScript, TypeScript, Python, and Go imports become edges.
|
|
3
|
+
// Comments and strings are ignored. Type-only TypeScript imports are not edges.
|
|
3
4
|
|
|
4
5
|
import fs from "node:fs/promises";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { info, warn } from "../utils/logger.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
];
|
|
8
|
+
import {
|
|
9
|
+
extractJsImports,
|
|
10
|
+
externalPackageName,
|
|
11
|
+
extractPythonImports,
|
|
12
|
+
extractGoImports,
|
|
13
|
+
indexPythonModules,
|
|
14
|
+
resolvePythonImport,
|
|
15
|
+
goPackageDir,
|
|
16
|
+
isGoStdlib,
|
|
17
|
+
parseGoModule,
|
|
18
|
+
} from "./import-parsers.js";
|
|
19
|
+
|
|
20
|
+
const JS_EXTENSIONS = new Set([".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"]);
|
|
21
|
+
const PY_EXTENSIONS = new Set([".py", ".pyw"]);
|
|
22
|
+
const GO_EXTENSIONS = new Set([".go"]);
|
|
21
23
|
|
|
22
24
|
async function readFileSafe(filePath) {
|
|
23
25
|
try {
|
|
@@ -27,6 +29,10 @@ async function readFileSafe(filePath) {
|
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
function isGoTestFile(file) {
|
|
33
|
+
return String(file).replace(/\\/g, "/").endsWith("_test.go");
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
function isRelativeImport(specifier) {
|
|
31
37
|
return specifier.startsWith("./") || specifier.startsWith("../");
|
|
32
38
|
}
|
|
@@ -58,63 +64,110 @@ function normalizeFileKey(file) {
|
|
|
58
64
|
return key;
|
|
59
65
|
}
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
nodes: [],
|
|
64
|
-
edges: [],
|
|
65
|
-
cycles: [],
|
|
66
|
-
externalDeps: []
|
|
67
|
+
function emptyResult() {
|
|
68
|
+
return {
|
|
69
|
+
nodes: [],
|
|
70
|
+
edges: [],
|
|
71
|
+
cycles: [],
|
|
72
|
+
externalDeps: [],
|
|
67
73
|
stats: null,
|
|
68
74
|
summary: null,
|
|
69
75
|
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function loadGoModule(repoRoot) {
|
|
79
|
+
const text = await readFileSafe(path.join(repoRoot, "go.mod"));
|
|
80
|
+
return parseGoModule(text);
|
|
81
|
+
}
|
|
70
82
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
function goFilesInDir(goFiles, dir) {
|
|
84
|
+
const target = dir || "";
|
|
85
|
+
return goFiles.filter((file) => {
|
|
86
|
+
const fileDir = path.posix.dirname(file.replace(/\\/g, "/"));
|
|
87
|
+
const normalized = fileDir === "." ? "" : fileDir;
|
|
88
|
+
return normalized === target;
|
|
74
89
|
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function analyzeDependencyGraph(files, repoRoot) {
|
|
93
|
+
const result = emptyResult();
|
|
94
|
+
|
|
95
|
+
const jsFiles = files.filter((file) => JS_EXTENSIONS.has(path.extname(file)));
|
|
96
|
+
const pyFiles = files.filter((file) => PY_EXTENSIONS.has(path.extname(file)));
|
|
97
|
+
const goFiles = files.filter((file) => GO_EXTENSIONS.has(path.extname(file)) && !isGoTestFile(file));
|
|
98
|
+
const codeFiles = [...jsFiles, ...pyFiles, ...goFiles];
|
|
75
99
|
|
|
76
100
|
if (codeFiles.length === 0) return result;
|
|
77
101
|
|
|
78
|
-
// Build adjacency: fileKey → Set<fileKey>
|
|
79
102
|
const adjacency = new Map();
|
|
80
103
|
const fileKeyToFile = new Map();
|
|
81
104
|
const allFileKeys = new Set();
|
|
82
105
|
const externalSet = new Set();
|
|
106
|
+
const externalByKey = new Map();
|
|
83
107
|
|
|
84
|
-
|
|
85
|
-
for (const file of codeFiles) {
|
|
108
|
+
function register(file) {
|
|
86
109
|
const key = normalizeFileKey(file);
|
|
87
110
|
allFileKeys.add(key);
|
|
88
|
-
fileKeyToFile.set(key, file);
|
|
111
|
+
if (!fileKeyToFile.has(key)) fileKeyToFile.set(key, file);
|
|
89
112
|
if (!adjacency.has(key)) adjacency.set(key, new Set());
|
|
113
|
+
if (!externalByKey.has(key)) externalByKey.set(key, new Set());
|
|
114
|
+
return key;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function addExternal(fromKey, name) {
|
|
118
|
+
if (!name) return;
|
|
119
|
+
externalSet.add(name);
|
|
120
|
+
externalByKey.get(fromKey).add(name);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function addEdge(fromKey, toKey) {
|
|
124
|
+
if (!toKey || toKey === fromKey || !allFileKeys.has(toKey)) return;
|
|
125
|
+
adjacency.get(fromKey).add(toKey);
|
|
90
126
|
}
|
|
91
127
|
|
|
92
|
-
|
|
93
|
-
|
|
128
|
+
for (const file of codeFiles) register(file);
|
|
129
|
+
|
|
130
|
+
const pyIndex = indexPythonModules(pyFiles);
|
|
131
|
+
const goModulePath = goFiles.length > 0 ? await loadGoModule(repoRoot) : null;
|
|
132
|
+
|
|
133
|
+
for (const file of jsFiles) {
|
|
94
134
|
const content = await readFileSafe(path.join(repoRoot, file));
|
|
95
135
|
if (!content) continue;
|
|
136
|
+
const fromKey = normalizeFileKey(file);
|
|
137
|
+
for (const imported of extractJsImports(content)) {
|
|
138
|
+
if (imported.typeOnly) continue;
|
|
139
|
+
if (isRelativeImport(imported.specifier)) {
|
|
140
|
+
addEdge(fromKey, resolveImportPath(file, imported.specifier));
|
|
141
|
+
} else {
|
|
142
|
+
addExternal(fromKey, externalPackageName(imported.specifier));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
96
146
|
|
|
147
|
+
for (const file of pyFiles) {
|
|
148
|
+
const content = await readFileSafe(path.join(repoRoot, file));
|
|
149
|
+
if (!content) continue;
|
|
97
150
|
const fromKey = normalizeFileKey(file);
|
|
151
|
+
for (const imported of extractPythonImports(content)) {
|
|
152
|
+
const resolved = resolvePythonImport(file, imported, pyIndex);
|
|
153
|
+
if (resolved.kind === "internal") addEdge(fromKey, normalizeFileKey(resolved.file));
|
|
154
|
+
else if (resolved.kind === "external") addExternal(fromKey, resolved.name);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
98
157
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (resolved && allFileKeys.has(resolved)) {
|
|
109
|
-
adjacency.get(fromKey).add(resolved);
|
|
110
|
-
}
|
|
111
|
-
} else if (!specifier.startsWith("node:")) {
|
|
112
|
-
// Bare specifier → external dependency
|
|
113
|
-
// Normalize scoped packages: @scope/pkg/path → @scope/pkg
|
|
114
|
-
const parts = specifier.startsWith("@") ? specifier.split("/").slice(0, 2) : specifier.split("/").slice(0, 1);
|
|
115
|
-
externalSet.add(parts.join("/"));
|
|
158
|
+
for (const file of goFiles) {
|
|
159
|
+
const content = await readFileSafe(path.join(repoRoot, file));
|
|
160
|
+
if (!content) continue;
|
|
161
|
+
const fromKey = normalizeFileKey(file);
|
|
162
|
+
for (const importPath of extractGoImports(content)) {
|
|
163
|
+
const dir = goPackageDir(importPath, goModulePath, "");
|
|
164
|
+
if (dir !== null) {
|
|
165
|
+
for (const target of goFilesInDir(goFiles, dir)) {
|
|
166
|
+
addEdge(fromKey, normalizeFileKey(target));
|
|
116
167
|
}
|
|
168
|
+
continue;
|
|
117
169
|
}
|
|
170
|
+
if (!isGoStdlib(importPath)) addExternal(fromKey, importPath);
|
|
118
171
|
}
|
|
119
172
|
}
|
|
120
173
|
|
|
@@ -139,6 +192,7 @@ export async function analyzeDependencyGraph(files, repoRoot) {
|
|
|
139
192
|
file: fileKeyToFile.get(key) || key,
|
|
140
193
|
imports: [...(adjacency.get(key) || [])],
|
|
141
194
|
importedBy: importedByMap.get(key) || [],
|
|
195
|
+
externalImports: [...(externalByKey.get(key) || [])].sort(),
|
|
142
196
|
});
|
|
143
197
|
}
|
|
144
198
|
|
|
@@ -133,16 +133,54 @@ const DEFAULT_DOMAIN_HINTS = [
|
|
|
133
133
|
}
|
|
134
134
|
];
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
const REACT_HOOK_KEYWORDS = new Set(["hook", "hooks", "use"]);
|
|
137
|
+
|
|
138
|
+
export function pathTokens(modulePath) {
|
|
139
|
+
return String(modulePath).toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Match a path token, not a substring (`ai` does not match `_main` or `email`). */
|
|
143
|
+
export function pathHasKeyword(modulePath, keyword) {
|
|
144
|
+
const needle = String(keyword).toLowerCase();
|
|
145
|
+
if (!needle) return false;
|
|
146
|
+
return pathTokens(modulePath).some((token) => {
|
|
147
|
+
if (token === needle || token === `${needle}s`) return true;
|
|
148
|
+
if (needle.length >= 5 && token.startsWith(needle)) return true;
|
|
149
|
+
return false;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function moduleEvidence(moduleKey, depGraph) {
|
|
154
|
+
const key = String(moduleKey).replace(/\\/g, "/");
|
|
155
|
+
const nodes = (depGraph?.nodes || []).filter((node) => {
|
|
156
|
+
const file = String(node.file || node.key || "").replace(/\\/g, "/");
|
|
157
|
+
return file === key || file.startsWith(`${key}/`) || node.key === key;
|
|
158
|
+
});
|
|
159
|
+
const imports = new Set(nodes.flatMap((node) => node.externalImports || []));
|
|
160
|
+
const react = [...imports].some((name) => name === "react" || name === "react-dom" || name.startsWith("react/"));
|
|
161
|
+
const ai = [...imports].some((name) => /(^|\/)(openai|anthropic|langchain|ollama)(\/|$)/i.test(name) || name.startsWith("@anthropic-ai/") || name.startsWith("@ai-sdk/"));
|
|
162
|
+
return { react, ai };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function domainEvidenceText(hint, modulePath, keyword, evidence) {
|
|
166
|
+
const isDefault = DEFAULT_DOMAIN_HINTS.includes(hint);
|
|
167
|
+
if (!isDefault && hint.description) return hint.description;
|
|
168
|
+
const bits = [`path \`${modulePath}\` matches \`${keyword}\``];
|
|
169
|
+
if (REACT_HOOK_KEYWORDS.has(keyword) && evidence.react) bits.push("imports react");
|
|
170
|
+
if (hint.domain === "AI & Machine Learning" && evidence.ai) bits.push("imports an AI SDK");
|
|
171
|
+
return bits.join("; ");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function inferDomain(modulePath, customHints = [], evidence = {}) {
|
|
137
175
|
const hints = [...customHints, ...DEFAULT_DOMAIN_HINTS];
|
|
138
|
-
|
|
139
|
-
|
|
176
|
+
|
|
140
177
|
for (const hint of hints) {
|
|
141
178
|
for (const keyword of hint.match) {
|
|
142
|
-
if (
|
|
179
|
+
if (REACT_HOOK_KEYWORDS.has(keyword) && !evidence.react) continue;
|
|
180
|
+
if (pathHasKeyword(modulePath, keyword)) {
|
|
143
181
|
return {
|
|
144
182
|
domain: hint.domain,
|
|
145
|
-
description: hint
|
|
183
|
+
description: domainEvidenceText(hint, modulePath, keyword, evidence),
|
|
146
184
|
confidence: "pattern_match",
|
|
147
185
|
matchedKeyword: keyword
|
|
148
186
|
};
|
|
@@ -158,11 +196,11 @@ export function inferDomain(modulePath, customHints = []) {
|
|
|
158
196
|
};
|
|
159
197
|
}
|
|
160
198
|
|
|
161
|
-
export function groupModulesByDomain(modules, customHints = []) {
|
|
199
|
+
export function groupModulesByDomain(modules, customHints = [], depGraph = null) {
|
|
162
200
|
const domainGroups = {};
|
|
163
201
|
|
|
164
202
|
for (const module of modules) {
|
|
165
|
-
const inference = inferDomain(module.key, customHints);
|
|
203
|
+
const inference = inferDomain(module.key, customHints, moduleEvidence(module.key, depGraph));
|
|
166
204
|
const domainName = inference.domain;
|
|
167
205
|
|
|
168
206
|
if (!domainGroups[domainName]) {
|