@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.
@@ -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 lower = modulePath.toLowerCase();
179
-
180
- if (lower.includes("test") || lower.includes("spec") || lower.includes("__test")) return "test";
181
- if (lower.includes("api") || lower.includes("endpoint")) return "api";
182
- if (lower.includes("component") || lower.includes("widget")) return "ui";
183
- if (lower.includes("lib") || lower.includes("util") || lower.includes("helper") || lower.includes("common") || lower.includes("shared")) return "library";
184
- if (lower.includes("hook")) return "hooks";
185
- if (lower.includes("store") || lower.includes("state") || lower.includes("redux") || lower.includes("zustand")) return "state";
186
- if (lower.includes("page") || lower.includes("route") || lower.includes("view")) return "route";
187
- if (lower.includes("config") || lower.includes("setting") || lower.includes("env")) return "config";
188
- if (lower.includes("core") || lower.includes("kernel") || lower.includes("foundation")) return "core";
189
- if (lower.includes("render") || lower.includes("template") || lower.includes("format")) return "renderer";
190
- if (lower.includes("publish") || lower.includes("output") || lower.includes("export")) return "publisher";
191
- if (lower.includes("analyz") || lower.includes("inspect") || lower.includes("detect")) return "analyzer";
192
- if (lower.includes("plugin") || lower.includes("extension") || lower.includes("addon")) return "plugin";
193
- if (lower.includes("deliver") || lower.includes("dispatch") || lower.includes("send")) return "delivery";
194
- if (lower.includes("doc") || lower.includes("generate")) return "documentation";
195
- if (lower.includes("integrat") || lower.includes("connect") || lower.includes("adapter")) return "integration";
196
- if (lower.includes("cli") || lower.includes("command") || lower.includes("bin")) return "cli";
197
- if (lower.includes("ai") || lower.includes("ml") || lower.includes("model") || lower.includes("prompt")) return "ai";
198
- if (lower.includes("auth") || lower.includes("login") || lower.includes("session")) return "auth";
199
- if (lower.includes("data") || lower.includes("db") || lower.includes("model") || lower.includes("schema")) return "data";
200
- if (lower.includes("middleware")) return "middleware";
201
- if (lower.includes("service")) return "service";
202
- if (lower.includes("app")) return "app";
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(k => k.includes(keyword));
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/")) patterns.push("Next.js App Router");
235
- if (has("pages/")) patterns.push("Next.js Pages Router");
236
- if (has("component") && has("lib")) patterns.push("Layered component architecture");
237
- if (has("hook")) patterns.push("React hooks pattern");
238
- if (has("store") || has("state") || has("redux") || has("zustand")) patterns.push("Centralized state management");
239
-
240
- // General patterns — verified against dep graph when available
241
- if (has("api") || has("endpoint")) patterns.push("API route pattern");
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
- if (moduleMetrics.size > 0 && verifyHub("core")) {
245
- patterns.push("Core/kernel architecture (verified — high fan-in)");
246
- } else if (moduleMetrics.size > 0) {
247
- patterns.push("Core/kernel architecture (naming only)");
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
- if (moduleMetrics.size > 0 && (verifyHub("plugin") || verifyOrchestrator("plugin") || verifyHub("loader"))) {
255
- patterns.push("Plugin system (verified — loader/registry detected)");
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")) patterns.push("Middleware pipeline");
283
+ if (has("middleware")) cite("Middleware pipeline", ["middleware"]);
264
284
 
265
285
  if (has("render") || has("template")) {
266
- if (moduleMetrics.size > 0 && verifyOrchestrator("render")) {
267
- patterns.push("Renderer pipeline (verified — multi-output)");
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
- if (moduleMetrics.size > 0 && verifyOrchestrator("publish")) {
275
- patterns.push("Multi-output publishing (verified — multiple targets)");
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")) patterns.push("Analysis pipeline");
282
- if (has("cli") || has("command") || has("bin")) patterns.push("CLI tool architecture");
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
- if (moduleMetrics.size > 0 && verifyHub("util") || verifyHub("helper") || verifyHub("lib")) {
286
- patterns.push("Shared utility layer (verified — high fan-in)");
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")) patterns.push("Integration adapters");
293
- if (has("ai") || has("prompt") || has("provider")) patterns.push("AI/LLM integration");
294
- if (has("deliver") || has("dispatch")) patterns.push("Delivery pipeline");
295
- if (has("test") || has("spec")) patterns.push("Dedicated test infrastructure");
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
- // Parses import/require statements, builds a directed graph, detects cycles via DFS
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
- const CODE_EXTENSIONS = new Set([".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"]);
9
-
10
- // Patterns for extracting imports
11
- const IMPORT_PATTERNS = [
12
- // ES module: import ... from "..."
13
- /import\s+(?:[\w*{},\s]+\s+from\s+)?['"]([^'"]+)['"]/g,
14
- // Dynamic import: import("...")
15
- /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
16
- // CommonJS: require("...")
17
- /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
18
- // Re-export: export ... from "..."
19
- /export\s+(?:[\w*{},\s]+\s+from\s+)?['"]([^'"]+)['"]/g,
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
- export async function analyzeDependencyGraph(files, repoRoot) {
62
- const result = {
63
- nodes: [], // { key, file, imports: string[], importedBy: string[] }
64
- edges: [], // { from, to }
65
- cycles: [], // [ [a, b, c, a], ... ]
66
- externalDeps: [],// sorted unique bare-specifier imports
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
- const codeFiles = files.filter(f => {
72
- const ext = path.extname(f);
73
- return CODE_EXTENSIONS.has(ext);
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
- // Register all known file keys
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
- // Parse imports and build edges
93
- for (const file of codeFiles) {
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
- for (const pattern of IMPORT_PATTERNS) {
100
- // Reset lastIndex since we reuse regex objects
101
- const regex = new RegExp(pattern.source, pattern.flags);
102
- let match;
103
- while ((match = regex.exec(content)) !== null) {
104
- const specifier = match[1];
105
-
106
- if (isRelativeImport(specifier)) {
107
- const resolved = resolveImportPath(file, specifier);
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
- export function inferDomain(modulePath, customHints = []) {
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
- const lowerPath = modulePath.toLowerCase();
139
-
176
+
140
177
  for (const hint of hints) {
141
178
  for (const keyword of hint.match) {
142
- if (lowerPath.includes(keyword)) {
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.description,
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]) {