@chappibunny/repolens 1.6.0 → 1.7.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/CHANGELOG.md +20 -0
- package/README.md +1 -1
- package/package.json +5 -4
- package/src/ai/generate-sections.js +63 -36
- package/src/analyzers/context-builder.js +224 -26
- package/src/analyzers/domain-inference.js +56 -1
- package/src/analyzers/flow-inference.js +117 -10
- package/src/analyzers/graphql-analyzer.js +7 -2
- package/src/core/scan.js +70 -33
- package/src/docs/generate-doc-set.js +14 -9
- package/src/renderers/render.js +104 -54
- package/src/renderers/renderDiff.js +23 -3
- package/src/renderers/renderMap.js +93 -12
|
@@ -1,27 +1,134 @@
|
|
|
1
|
-
// Infer data flows through the application using heuristics
|
|
1
|
+
// Infer data flows through the application using import graph analysis + heuristics
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Primary flow inference: uses dep graph when available for real import-chain flows,
|
|
5
|
+
* falls back to keyword heuristics for repos without dep graph data.
|
|
6
|
+
*/
|
|
7
|
+
export function inferDataFlows(scanResult, config, depGraph = null) {
|
|
4
8
|
const flows = [];
|
|
5
9
|
|
|
6
|
-
//
|
|
10
|
+
// If we have dep graph data, build real import-chain flows from entry points
|
|
11
|
+
if (depGraph?.nodes && depGraph.nodes.length > 0) {
|
|
12
|
+
const entryFlows = inferFlowsFromEntryPoints(scanResult, depGraph);
|
|
13
|
+
flows.push(...entryFlows);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Keyword-based heuristic flows (only add if not already covered by import-chain analysis)
|
|
17
|
+
const existingNames = new Set(flows.map(f => f.name.toLowerCase()));
|
|
18
|
+
|
|
7
19
|
const marketFlow = inferMarketDataFlow(scanResult);
|
|
8
|
-
if (marketFlow) flows.push(marketFlow);
|
|
20
|
+
if (marketFlow && !existingNames.has(marketFlow.name.toLowerCase())) flows.push(marketFlow);
|
|
9
21
|
|
|
10
|
-
// Authentication Flow
|
|
11
22
|
const authFlow = inferAuthFlow(scanResult);
|
|
12
|
-
if (authFlow) flows.push(authFlow);
|
|
23
|
+
if (authFlow && !existingNames.has(authFlow.name.toLowerCase())) flows.push(authFlow);
|
|
13
24
|
|
|
14
|
-
// Content/Article Flow
|
|
15
25
|
const contentFlow = inferContentFlow(scanResult);
|
|
16
|
-
if (contentFlow) flows.push(contentFlow);
|
|
26
|
+
if (contentFlow && !existingNames.has(contentFlow.name.toLowerCase())) flows.push(contentFlow);
|
|
17
27
|
|
|
18
|
-
// API Integration Flow
|
|
19
28
|
const apiFlow = inferApiIntegrationFlow(scanResult);
|
|
20
|
-
if (apiFlow) flows.push(apiFlow);
|
|
29
|
+
if (apiFlow && !existingNames.has(apiFlow.name.toLowerCase())) flows.push(apiFlow);
|
|
21
30
|
|
|
22
31
|
return flows;
|
|
23
32
|
}
|
|
24
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Trace real import chains from entry points through the dep graph.
|
|
36
|
+
* Entry points: CLI/bin files, page components, API route handlers, index files.
|
|
37
|
+
*/
|
|
38
|
+
function inferFlowsFromEntryPoints(scanResult, depGraph) {
|
|
39
|
+
const flows = [];
|
|
40
|
+
if (!depGraph?.nodes) return flows;
|
|
41
|
+
|
|
42
|
+
// Find entry points (files with high fan-out and low/zero fan-in)
|
|
43
|
+
const entryPoints = depGraph.nodes.filter(n => {
|
|
44
|
+
const key = n.key.toLowerCase();
|
|
45
|
+
const isEntry = (
|
|
46
|
+
key.includes("bin/") ||
|
|
47
|
+
key.includes("cli") ||
|
|
48
|
+
key.endsWith("/index") ||
|
|
49
|
+
key.includes("pages/") ||
|
|
50
|
+
key.includes("app/") ||
|
|
51
|
+
(n.importedBy.length === 0 && n.imports.length >= 2) // True entry: nothing imports it
|
|
52
|
+
);
|
|
53
|
+
return isEntry && n.imports.length >= 2;
|
|
54
|
+
}).sort((a, b) => {
|
|
55
|
+
// Prioritize src/ entry points over tests/
|
|
56
|
+
const aIsTest = a.key.toLowerCase().includes("test");
|
|
57
|
+
const bIsTest = b.key.toLowerCase().includes("test");
|
|
58
|
+
if (aIsTest !== bIsTest) return aIsTest ? 1 : -1;
|
|
59
|
+
return b.imports.length - a.imports.length;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Limit: all src entries + max 2 test entries
|
|
63
|
+
const srcEntries = entryPoints.filter(n => !n.key.toLowerCase().includes("test"));
|
|
64
|
+
const testEntries = entryPoints.filter(n => n.key.toLowerCase().includes("test")).slice(0, 2);
|
|
65
|
+
const selectedEntries = [...srcEntries, ...testEntries].slice(0, 5);
|
|
66
|
+
|
|
67
|
+
for (const entry of selectedEntries) {
|
|
68
|
+
const chain = traceImportChain(entry.key, depGraph, 6);
|
|
69
|
+
if (chain.length < 2) continue;
|
|
70
|
+
|
|
71
|
+
const shortName = entry.key.split("/").pop();
|
|
72
|
+
const isCliEntry = entry.key.toLowerCase().includes("cli") || entry.key.toLowerCase().includes("bin/");
|
|
73
|
+
const flowType = isCliEntry ? "Command" : "Request";
|
|
74
|
+
|
|
75
|
+
// Classify each step in the chain
|
|
76
|
+
const steps = chain.map((nodeKey, i) => {
|
|
77
|
+
const node = depGraph.nodes.find(n => n.key === nodeKey);
|
|
78
|
+
const name = nodeKey.split("/").pop();
|
|
79
|
+
const fanIn = node?.importedBy?.length || 0;
|
|
80
|
+
const fanOut = node?.imports?.length || 0;
|
|
81
|
+
|
|
82
|
+
if (i === 0) return `\`${name}\` (entry point, imports ${fanOut} modules)`;
|
|
83
|
+
if (fanIn >= 5) return `\`${name}\` (shared infrastructure, used by ${fanIn} files)`;
|
|
84
|
+
if (fanOut === 0) return `\`${name}\` (leaf — no further dependencies)`;
|
|
85
|
+
return `\`${name}\` → imports ${fanOut} module${fanOut === 1 ? "" : "s"}`;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
flows.push({
|
|
89
|
+
name: `${shortName} ${flowType} Flow`,
|
|
90
|
+
description: `Import chain starting from \`${entry.key}\` — traces how this entry point depends on ${chain.length - 1} downstream module${chain.length - 1 === 1 ? "" : "s"}`,
|
|
91
|
+
steps,
|
|
92
|
+
modules: chain.slice(0, 8),
|
|
93
|
+
critical: entry.imports.length >= 5,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return flows;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Trace from an entry node through the dep graph via BFS, following highest-fan-out paths.
|
|
102
|
+
* Returns an ordered chain of module keys representing the primary dependency path.
|
|
103
|
+
*/
|
|
104
|
+
function traceImportChain(startKey, depGraph, maxDepth = 6) {
|
|
105
|
+
const chain = [startKey];
|
|
106
|
+
const visited = new Set([startKey]);
|
|
107
|
+
let current = startKey;
|
|
108
|
+
|
|
109
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
110
|
+
const node = depGraph.nodes.find(n => n.key === current);
|
|
111
|
+
if (!node || node.imports.length === 0) break;
|
|
112
|
+
|
|
113
|
+
// Follow the most-imported (highest fan-in) dependency first — it's the most interesting path
|
|
114
|
+
const candidates = node.imports
|
|
115
|
+
.filter(imp => !visited.has(imp))
|
|
116
|
+
.map(imp => {
|
|
117
|
+
const target = depGraph.nodes.find(n => n.key === imp);
|
|
118
|
+
return { key: imp, fanIn: target?.importedBy?.length || 0 };
|
|
119
|
+
})
|
|
120
|
+
.sort((a, b) => b.fanIn - a.fanIn);
|
|
121
|
+
|
|
122
|
+
if (candidates.length === 0) break;
|
|
123
|
+
|
|
124
|
+
current = candidates[0].key;
|
|
125
|
+
visited.add(current);
|
|
126
|
+
chain.push(current);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return chain;
|
|
130
|
+
}
|
|
131
|
+
|
|
25
132
|
function inferMarketDataFlow(scanResult) {
|
|
26
133
|
const { modules, pages, api } = scanResult;
|
|
27
134
|
|
|
@@ -31,7 +31,7 @@ const SCHEMA_LIBRARY_PATTERNS = [
|
|
|
31
31
|
{ name: "Apollo Server", pattern: /ApolloServer|@apollo\/server|apollo-server/ },
|
|
32
32
|
{ name: "GraphQL Yoga", pattern: /graphql-yoga|createYoga/ },
|
|
33
33
|
{ name: "Mercurius", pattern: /mercurius/ },
|
|
34
|
-
{ name: "graphql-js", pattern: /graphql\b
|
|
34
|
+
{ name: "graphql-js", pattern: /graphql\b.*\bbuildSchema\b|\bGraphQLSchema\b|\bGraphQLObjectType\b/ },
|
|
35
35
|
{ name: "type-graphql", pattern: /type-graphql|@Resolver|@Query|@Mutation/ },
|
|
36
36
|
{ name: "Nexus", pattern: /nexus|makeSchema|objectType\(/ },
|
|
37
37
|
{ name: "Pothos", pattern: /pothos|SchemaBuilder/ },
|
|
@@ -84,8 +84,13 @@ export async function analyzeGraphQL(files, repoRoot) {
|
|
|
84
84
|
const schemaFiles = files.filter(f => GRAPHQL_EXTENSIONS.some(ext => f.endsWith(ext)));
|
|
85
85
|
|
|
86
86
|
// Phase 2: Find JS/TS files with inline schema or resolvers
|
|
87
|
+
// Exclude test files and our own analysis/rendering files (they reference library names as strings, not usage)
|
|
88
|
+
const testPattern = /(?:^|\/)(?:tests?|__tests?__|spec|__spec__)\/|\.(test|spec)\.[jt]sx?$/i;
|
|
89
|
+
const selfPatterns = ["graphql-analyzer.js", "renderAnalysis.js"];
|
|
87
90
|
const codeFiles = files.filter(f =>
|
|
88
|
-
f.endsWith(".js") || f.endsWith(".ts") || f.endsWith(".jsx") || f.endsWith(".tsx")
|
|
91
|
+
(f.endsWith(".js") || f.endsWith(".ts") || f.endsWith(".jsx") || f.endsWith(".tsx")) &&
|
|
92
|
+
!testPattern.test(f) &&
|
|
93
|
+
!selfPatterns.some(s => f.endsWith(s))
|
|
89
94
|
);
|
|
90
95
|
|
|
91
96
|
// Parse .graphql/.gql files
|
package/src/core/scan.js
CHANGED
|
@@ -29,13 +29,22 @@ function isExpressRoute(content) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function isReactRouterFile(content) {
|
|
32
|
-
// Detect React Router patterns
|
|
33
|
-
|
|
32
|
+
// Detect React Router patterns — require import evidence, not just string mentions
|
|
33
|
+
const hasImport = /import\s+.*?from\s+['"]react-router/.test(content)
|
|
34
|
+
|| /require\s*\(\s*['"]react-router/.test(content);
|
|
35
|
+
const hasJSX = /<Route\s/.test(content);
|
|
36
|
+
const hasFactory = /createBrowserRouter\s*\(/.test(content)
|
|
37
|
+
|| /createRoutesFromElements\s*\(/.test(content);
|
|
38
|
+
return hasImport || hasJSX || hasFactory;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
function isVueRouterFile(content) {
|
|
37
|
-
// Detect Vue Router patterns
|
|
38
|
-
|
|
42
|
+
// Detect Vue Router patterns — require import evidence, not just string mentions
|
|
43
|
+
const hasImport = /import\s+.*?from\s+['"]vue-router/.test(content)
|
|
44
|
+
|| /require\s*\(\s*['"]vue-router/.test(content);
|
|
45
|
+
const hasConstructor = /new\s+VueRouter\s*\(/.test(content);
|
|
46
|
+
const hasFactory = /createRouter\s*\(/.test(content) && hasImport;
|
|
47
|
+
return hasImport || hasConstructor || hasFactory;
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
function isNextPage(file) {
|
|
@@ -159,6 +168,10 @@ async function extractRepoMetadata(repoRoot) {
|
|
|
159
168
|
if (allDeps["nestjs"] || allDeps["@nestjs/core"]) metadata.frameworks.push("NestJS");
|
|
160
169
|
if (allDeps["svelte"]) metadata.frameworks.push("Svelte");
|
|
161
170
|
if (allDeps["solid-js"]) metadata.frameworks.push("Solid");
|
|
171
|
+
if (allDeps["hono"]) metadata.frameworks.push("Hono");
|
|
172
|
+
if (allDeps["koa"]) metadata.frameworks.push("Koa");
|
|
173
|
+
if (allDeps["hapi"] || allDeps["@hapi/hapi"]) metadata.frameworks.push("Hapi");
|
|
174
|
+
if (allDeps["electron"]) metadata.frameworks.push("Electron");
|
|
162
175
|
|
|
163
176
|
// Detect test frameworks
|
|
164
177
|
if (allDeps["vitest"]) metadata.testFrameworks.push("Vitest");
|
|
@@ -166,6 +179,7 @@ async function extractRepoMetadata(repoRoot) {
|
|
|
166
179
|
if (allDeps["mocha"]) metadata.testFrameworks.push("Mocha");
|
|
167
180
|
if (allDeps["playwright"]) metadata.testFrameworks.push("Playwright");
|
|
168
181
|
if (allDeps["cypress"]) metadata.testFrameworks.push("Cypress");
|
|
182
|
+
if (allDeps["ava"]) metadata.testFrameworks.push("Ava");
|
|
169
183
|
|
|
170
184
|
// Detect build tools
|
|
171
185
|
if (allDeps["vite"]) metadata.buildTools.push("Vite");
|
|
@@ -173,9 +187,25 @@ async function extractRepoMetadata(repoRoot) {
|
|
|
173
187
|
if (allDeps["rollup"]) metadata.buildTools.push("Rollup");
|
|
174
188
|
if (allDeps["esbuild"]) metadata.buildTools.push("esbuild");
|
|
175
189
|
if (allDeps["turbo"]) metadata.buildTools.push("Turborepo");
|
|
190
|
+
if (allDeps["tsup"]) metadata.buildTools.push("tsup");
|
|
191
|
+
if (allDeps["swc"] || allDeps["@swc/core"]) metadata.buildTools.push("SWC");
|
|
192
|
+
if (allDeps["parcel"]) metadata.buildTools.push("Parcel");
|
|
176
193
|
|
|
177
|
-
// Detect
|
|
194
|
+
// Detect languages
|
|
178
195
|
if (allDeps["typescript"]) metadata.languages.add("TypeScript");
|
|
196
|
+
|
|
197
|
+
// Infer JavaScript if package.json exists (any npm project uses JS/Node)
|
|
198
|
+
metadata.languages.add("JavaScript");
|
|
199
|
+
|
|
200
|
+
// Detect Node.js runtime indicators
|
|
201
|
+
const hasNodeEngines = pkg.engines && pkg.engines.node;
|
|
202
|
+
const hasBin = pkg.bin != null;
|
|
203
|
+
const hasNodeDeps = allDeps["node-fetch"] || allDeps["fs-extra"] || allDeps["dotenv"]
|
|
204
|
+
|| allDeps["commander"] || allDeps["yargs"] || allDeps["chalk"]
|
|
205
|
+
|| allDeps["inquirer"] || allDeps["ora"] || allDeps["execa"];
|
|
206
|
+
if (hasNodeEngines || hasBin || hasNodeDeps || pkg.type === "module") {
|
|
207
|
+
metadata.languages.add("Node.js");
|
|
208
|
+
}
|
|
179
209
|
} catch {
|
|
180
210
|
// No package.json or invalid JSON
|
|
181
211
|
}
|
|
@@ -241,30 +271,29 @@ function extractExpressRoutes(content) {
|
|
|
241
271
|
|
|
242
272
|
function extractReactRoutes(content, file) {
|
|
243
273
|
const routes = [];
|
|
274
|
+
const lines = content.split("\n");
|
|
244
275
|
|
|
245
276
|
// Match <Route path="..." />
|
|
246
277
|
const routePattern = /<Route\s+[^>]*path\s*=\s*['"`]([^'"`]+)['"`][^>]*\/?>/gi;
|
|
247
278
|
let match;
|
|
248
279
|
|
|
249
280
|
while ((match = routePattern.exec(content)) !== null) {
|
|
250
|
-
const [,
|
|
251
|
-
|
|
252
|
-
file,
|
|
253
|
-
|
|
254
|
-
framework: "React Router"
|
|
255
|
-
});
|
|
281
|
+
const [, routePath] = match;
|
|
282
|
+
if (isValidRoutePath(routePath)) {
|
|
283
|
+
routes.push({ file, path: routePath, framework: "React Router" });
|
|
284
|
+
}
|
|
256
285
|
}
|
|
257
286
|
|
|
258
|
-
// Match path: "..." in route objects
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
framework: "React Router"
|
|
267
|
-
}
|
|
287
|
+
// Match path: "..." in route objects (skip comment lines)
|
|
288
|
+
for (const line of lines) {
|
|
289
|
+
const trimmed = line.trim();
|
|
290
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) continue;
|
|
291
|
+
const objectMatch = /path\s*:\s*['"`]([^'"`]+)['"`]/i.exec(trimmed);
|
|
292
|
+
if (objectMatch) {
|
|
293
|
+
const routePath = objectMatch[1];
|
|
294
|
+
if (isValidRoutePath(routePath) && !routes.some(r => r.path === routePath)) {
|
|
295
|
+
routes.push({ file, path: routePath, framework: "React Router" });
|
|
296
|
+
}
|
|
268
297
|
}
|
|
269
298
|
}
|
|
270
299
|
|
|
@@ -273,23 +302,31 @@ function extractReactRoutes(content, file) {
|
|
|
273
302
|
|
|
274
303
|
function extractVueRoutes(content, file) {
|
|
275
304
|
const routes = [];
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
path
|
|
286
|
-
|
|
287
|
-
|
|
305
|
+
const lines = content.split("\n");
|
|
306
|
+
|
|
307
|
+
// Match path: '...' or path: "..." in Vue router definitions (skip comment lines)
|
|
308
|
+
for (const line of lines) {
|
|
309
|
+
const trimmed = line.trim();
|
|
310
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) continue;
|
|
311
|
+
const match = /path\s*:\s*['"`]([^'"`]+)['"`]/i.exec(trimmed);
|
|
312
|
+
if (match) {
|
|
313
|
+
const routePath = match[1];
|
|
314
|
+
if (isValidRoutePath(routePath) && !routes.some(r => r.path === routePath)) {
|
|
315
|
+
routes.push({ file, path: routePath, framework: "Vue Router" });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
288
318
|
}
|
|
289
319
|
|
|
290
320
|
return routes;
|
|
291
321
|
}
|
|
292
322
|
|
|
323
|
+
function isValidRoutePath(p) {
|
|
324
|
+
// Filter out placeholder/documentation strings and non-path values
|
|
325
|
+
if (!p || p === "..." || p === "*" || p.length > 200) return false;
|
|
326
|
+
// Must look like a URL path (starts with / or is a relative segment)
|
|
327
|
+
return p.startsWith("/") || /^[a-zA-Z0-9]/.test(p);
|
|
328
|
+
}
|
|
329
|
+
|
|
293
330
|
export async function scanRepo(cfg) {
|
|
294
331
|
const repoRoot = cfg.__repoRoot;
|
|
295
332
|
|
|
@@ -33,10 +33,10 @@ import path from "node:path";
|
|
|
33
33
|
export async function generateDocumentSet(scanResult, config, diffData = null, pluginManager = null) {
|
|
34
34
|
info("Building structured context for AI...");
|
|
35
35
|
|
|
36
|
-
// Build AI context from scan results
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
// Build AI context from scan results (dep graph computed later, patched in after)
|
|
37
|
+
let aiContext = buildAIContext(scanResult, config);
|
|
38
|
+
let moduleContext = buildModuleContext(scanResult.modules, config);
|
|
39
|
+
let flows = inferDataFlows(scanResult, config);
|
|
40
40
|
|
|
41
41
|
// Run extended analysis (v0.8.0)
|
|
42
42
|
const repoRoot = config.__repoRoot || process.cwd();
|
|
@@ -63,6 +63,11 @@ export async function generateDocumentSet(scanResult, config, diffData = null, p
|
|
|
63
63
|
await saveBaseline(snapshot, outputDir);
|
|
64
64
|
} catch (e) { warn(`Drift detection failed: ${e.message}`); }
|
|
65
65
|
|
|
66
|
+
// Rebuild AI context with dep graph for enriched module roles and pattern verification
|
|
67
|
+
aiContext = buildAIContext(scanResult, config, depGraph);
|
|
68
|
+
moduleContext = buildModuleContext(scanResult.modules, config, depGraph);
|
|
69
|
+
flows = inferDataFlows(scanResult, config, depGraph);
|
|
70
|
+
|
|
66
71
|
// CODEOWNERS integration
|
|
67
72
|
const codeowners = await parseCodeowners(repoRoot);
|
|
68
73
|
const ownershipMap = codeowners.found
|
|
@@ -196,19 +201,19 @@ async function generateDocument(docPlan, context) {
|
|
|
196
201
|
return await generateArchitectureOverview(aiContext, { depGraph, driftResult });
|
|
197
202
|
|
|
198
203
|
case "module_catalog":
|
|
199
|
-
// Hybrid: deterministic skeleton + ownership info
|
|
200
|
-
return renderModuleCatalogOriginal(config, scanResult, ownershipMap);
|
|
204
|
+
// Hybrid: deterministic skeleton + ownership info + dep-graph roles
|
|
205
|
+
return renderModuleCatalogOriginal(config, scanResult, ownershipMap, depGraph);
|
|
201
206
|
|
|
202
207
|
case "route_map":
|
|
203
|
-
// Hybrid: deterministic skeleton +
|
|
204
|
-
return renderRouteMapOriginal(config, scanResult);
|
|
208
|
+
// Hybrid: deterministic skeleton + context-aware messaging
|
|
209
|
+
return renderRouteMapOriginal(config, scanResult, aiContext);
|
|
205
210
|
|
|
206
211
|
case "api_surface":
|
|
207
212
|
// Hybrid: deterministic skeleton + AI enhancement (for now, just deterministic)
|
|
208
213
|
return renderApiSurfaceOriginal(config, scanResult);
|
|
209
214
|
|
|
210
215
|
case "data_flows":
|
|
211
|
-
return await generateDataFlows(flows, aiContext, { depGraph, scanResult
|
|
216
|
+
return await generateDataFlows(flows, aiContext, { depGraph, scanResult, moduleContext });
|
|
212
217
|
|
|
213
218
|
case "arch_diff":
|
|
214
219
|
if (!diffData) {
|
package/src/renderers/render.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { computeModuleLevelMetrics, describeModuleDepRole } from "../analyzers/context-builder.js";
|
|
2
|
+
|
|
3
|
+
export function renderSystemOverview(cfg, scan, depGraph = null) {
|
|
2
4
|
const projectName = cfg.project?.name || "Project";
|
|
3
5
|
const date = new Date().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" });
|
|
4
6
|
|
|
@@ -17,8 +19,8 @@ export function renderSystemOverview(cfg, scan) {
|
|
|
17
19
|
`|--------|-------|`,
|
|
18
20
|
`| Files scanned | ${scan.filesCount} |`,
|
|
19
21
|
`| Modules detected | ${scan.modules.length} |`,
|
|
20
|
-
`| Application pages | ${scan.pages
|
|
21
|
-
`| API endpoints | ${scan.api.length}
|
|
22
|
+
...(scan.pages?.length ? [`| Application pages | ${scan.pages.length} |`] : []),
|
|
23
|
+
...(scan.api.length ? [`| API endpoints | ${scan.api.length} |`] : []),
|
|
22
24
|
``
|
|
23
25
|
];
|
|
24
26
|
|
|
@@ -35,9 +37,9 @@ export function renderSystemOverview(cfg, scan) {
|
|
|
35
37
|
`| Category | Technologies |`,
|
|
36
38
|
`|----------|-------------|`
|
|
37
39
|
);
|
|
38
|
-
|
|
40
|
+
lines.push(`| Frameworks | ${frameworks.join(", ") || 'N/A'} |`);
|
|
39
41
|
if (languages.length) lines.push(`| Languages | ${languages.join(", ")} |`);
|
|
40
|
-
|
|
42
|
+
lines.push(`| Build Tools | ${buildTools.join(", ") || 'N/A'} |`);
|
|
41
43
|
if (testFrameworks.length) lines.push(`| Testing | ${testFrameworks.join(", ")} |`);
|
|
42
44
|
lines.push(``);
|
|
43
45
|
}
|
|
@@ -53,8 +55,8 @@ export function renderSystemOverview(cfg, scan) {
|
|
|
53
55
|
`## Architecture Summary`,
|
|
54
56
|
``,
|
|
55
57
|
`The repository is organized as ${sizeDesc} with **${scan.modules.length} modules** spanning **${scan.filesCount} files**. `
|
|
56
|
-
+ (scan.api.length > 0 ? `It exposes **${scan.api.length} API endpoint${scan.api.length === 1 ? "" : "s"}
|
|
57
|
-
+ (scan.pages?.length > 0 ? `
|
|
58
|
+
+ (scan.api.length > 0 ? `It exposes **${scan.api.length} API endpoint${scan.api.length === 1 ? "" : "s"}**. ` : "")
|
|
59
|
+
+ (scan.pages?.length > 0 ? `It serves **${scan.pages.length} application page${scan.pages.length === 1 ? "" : "s"}**. ` : "")
|
|
58
60
|
+ `The largest modules are listed below, ranked by file count.`,
|
|
59
61
|
``
|
|
60
62
|
);
|
|
@@ -62,6 +64,7 @@ export function renderSystemOverview(cfg, scan) {
|
|
|
62
64
|
|
|
63
65
|
// Largest modules as a table instead of bullets
|
|
64
66
|
const topModules = scan.modules.slice(0, 10);
|
|
67
|
+
const moduleMetrics = computeModuleLevelMetrics(depGraph, scan.modules);
|
|
65
68
|
if (topModules.length > 0) {
|
|
66
69
|
lines.push(
|
|
67
70
|
`## Largest Modules`,
|
|
@@ -70,7 +73,8 @@ export function renderSystemOverview(cfg, scan) {
|
|
|
70
73
|
`|--------|-------|-------------|`
|
|
71
74
|
);
|
|
72
75
|
for (const m of topModules) {
|
|
73
|
-
const
|
|
76
|
+
const depRole = describeModuleDepRole(m.key, moduleMetrics);
|
|
77
|
+
const desc = describeModule(m.key, depRole);
|
|
74
78
|
lines.push(`| \`${m.key}\` | ${m.fileCount} | ${desc} |`);
|
|
75
79
|
}
|
|
76
80
|
lines.push(``);
|
|
@@ -105,36 +109,50 @@ export function renderSystemOverview(cfg, scan) {
|
|
|
105
109
|
return lines.join("\n");
|
|
106
110
|
}
|
|
107
111
|
|
|
108
|
-
function describeModule(key) {
|
|
112
|
+
function describeModule(key, depRole = null) {
|
|
109
113
|
const normalized = key.toLowerCase();
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (normalized.includes("
|
|
113
|
-
if (normalized.includes("
|
|
114
|
-
if (normalized.includes("
|
|
115
|
-
if (normalized.includes("
|
|
116
|
-
if (normalized.includes("
|
|
117
|
-
if (normalized.includes("
|
|
118
|
-
if (normalized.includes("
|
|
119
|
-
if (normalized.includes("
|
|
120
|
-
if (normalized.includes("
|
|
121
|
-
if (normalized.includes("
|
|
122
|
-
if (normalized.includes("
|
|
123
|
-
if (normalized.includes("
|
|
124
|
-
if (normalized.includes("
|
|
125
|
-
if (normalized.includes("
|
|
126
|
-
if (normalized.includes("
|
|
127
|
-
if (normalized.includes("
|
|
128
|
-
if (normalized.includes("
|
|
129
|
-
if (normalized.includes("
|
|
130
|
-
if (normalized.includes("
|
|
131
|
-
if (normalized.includes("
|
|
132
|
-
if (normalized.includes("
|
|
133
|
-
|
|
114
|
+
// Base description from path keywords
|
|
115
|
+
let base;
|
|
116
|
+
if (normalized.includes("core")) base = "Core business logic and shared foundations";
|
|
117
|
+
else if (normalized.includes("util")) base = "Shared utilities and helper functions";
|
|
118
|
+
else if (normalized.includes("api")) base = "API route handlers and endpoint definitions";
|
|
119
|
+
else if (normalized.includes("component")) base = "Reusable UI components";
|
|
120
|
+
else if (normalized.includes("hook")) base = "Custom React hooks";
|
|
121
|
+
else if (normalized.includes("page")) base = "Application page components";
|
|
122
|
+
else if (normalized.includes("lib")) base = "Library code and third-party integrations";
|
|
123
|
+
else if (normalized.includes("service")) base = "Service layer and external integrations";
|
|
124
|
+
else if (normalized.includes("model")) base = "Data models and schema definitions";
|
|
125
|
+
else if (normalized.includes("store") || normalized.includes("state")) base = "State management";
|
|
126
|
+
else if (normalized.includes("config")) base = "Configuration and settings";
|
|
127
|
+
else if (normalized.includes("test")) base = "Test suites and fixtures";
|
|
128
|
+
else if (normalized.includes("style") || normalized.includes("css")) base = "Styling and design tokens";
|
|
129
|
+
else if (normalized.includes("type")) base = "Type definitions and interfaces";
|
|
130
|
+
else if (normalized.includes("middleware")) base = "Request middleware and interceptors";
|
|
131
|
+
else if (normalized.includes("auth")) base = "Authentication and authorization";
|
|
132
|
+
else if (normalized.includes("render")) base = "Rendering logic and output formatters";
|
|
133
|
+
else if (normalized.includes("publish")) base = "Publishing and delivery integrations";
|
|
134
|
+
else if (normalized.includes("analyz")) base = "Code analysis and intelligence";
|
|
135
|
+
else if (normalized.includes("delivery")) base = "Content delivery and distribution";
|
|
136
|
+
else if (normalized.includes("integrat")) base = "Third-party service integrations";
|
|
137
|
+
else if (normalized.includes("doc")) base = "Documentation generation";
|
|
138
|
+
else if (normalized.includes("bin") || normalized.includes("cli")) base = "CLI entry point and commands";
|
|
139
|
+
else if (normalized.includes("plugin") || normalized.includes("extension")) base = "Plugin system and extensions";
|
|
140
|
+
else if (normalized.includes("prompt")) base = "Prompt engineering and templates";
|
|
141
|
+
else if (normalized.includes("provider")) base = "Service provider adapters";
|
|
142
|
+
else if (normalized.includes("generate") || normalized.includes("section")) base = "Content generation pipeline";
|
|
143
|
+
else if (normalized.includes("ai") || normalized.includes("ml")) base = "AI and machine learning integration";
|
|
144
|
+
else base = "Application module";
|
|
145
|
+
|
|
146
|
+
// Enrich with dependency role if available
|
|
147
|
+
if (depRole) {
|
|
148
|
+
return `${base} · ${depRole}`;
|
|
149
|
+
}
|
|
150
|
+
return base;
|
|
134
151
|
}
|
|
135
152
|
|
|
136
|
-
export function renderModuleCatalog(cfg, scan, ownershipMap = {}) {
|
|
153
|
+
export function renderModuleCatalog(cfg, scan, ownershipMap = {}, depGraph = null) {
|
|
137
154
|
const hasOwnership = Object.keys(ownershipMap).length > 0;
|
|
155
|
+
const moduleMetrics = computeModuleLevelMetrics(depGraph, scan.modules);
|
|
138
156
|
const lines = [
|
|
139
157
|
`# Module Catalog`,
|
|
140
158
|
``,
|
|
@@ -172,7 +190,8 @@ export function renderModuleCatalog(cfg, scan, ownershipMap = {}) {
|
|
|
172
190
|
}
|
|
173
191
|
|
|
174
192
|
for (const module of scan.modules.slice(0, 100)) {
|
|
175
|
-
const
|
|
193
|
+
const depRole = describeModuleDepRole(module.key, moduleMetrics);
|
|
194
|
+
const desc = describeModule(module.key, depRole);
|
|
176
195
|
const owners = ownershipMap[module.key];
|
|
177
196
|
if (hasOwnership) {
|
|
178
197
|
lines.push(`| \`${module.key}\` | ${module.fileCount} | ${desc} | ${owners ? owners.join(", ") : "—"} |`);
|
|
@@ -277,7 +296,7 @@ export function renderApiSurface(cfg, scan) {
|
|
|
277
296
|
return lines.join("\n");
|
|
278
297
|
}
|
|
279
298
|
|
|
280
|
-
export function renderRouteMap(cfg, scan) {
|
|
299
|
+
export function renderRouteMap(cfg, scan, aiContext = null) {
|
|
281
300
|
const lines = [
|
|
282
301
|
`# Route Map`,
|
|
283
302
|
``,
|
|
@@ -330,30 +349,61 @@ export function renderRouteMap(cfg, scan) {
|
|
|
330
349
|
}
|
|
331
350
|
|
|
332
351
|
if (!scan.pages?.length && !scan.api?.length) {
|
|
352
|
+
// Determine project type for context-aware messaging
|
|
353
|
+
const patterns = aiContext?.patterns || [];
|
|
354
|
+
const isCLI = patterns.some(p => p.toLowerCase().includes("cli"));
|
|
355
|
+
const isLibrary = patterns.some(p => p.toLowerCase().includes("library") || p.toLowerCase().includes("shared"));
|
|
356
|
+
const techStack = aiContext?.techStack || {};
|
|
357
|
+
const hasWebFramework = (techStack.frameworks || []).some(f =>
|
|
358
|
+
/next|react|vue|angular|express|fastify|hono|koa|django|flask|rails|spring/i.test(f)
|
|
359
|
+
);
|
|
360
|
+
|
|
333
361
|
lines.push(
|
|
334
362
|
`## Route Detection`,
|
|
335
|
-
``,
|
|
336
|
-
`No routes were auto-detected in this scan. RepoLens currently supports:`,
|
|
337
|
-
``,
|
|
338
|
-
`| Framework | Pattern | Status |`,
|
|
339
|
-
`|-----------|---------|--------|`,
|
|
340
|
-
`| Next.js | \`pages/\` and \`app/\` directories | Supported |`,
|
|
341
|
-
`| Next.js API | \`pages/api/\` and App Router | Supported |`,
|
|
342
|
-
`| Express.js | \`app.get\`, \`router.post\`, etc. | Supported |`,
|
|
343
|
-
`| React Router | \`<Route>\` components | Supported |`,
|
|
344
|
-
`| Vue Router | \`routes\` array definitions | Supported |`,
|
|
345
|
-
``,
|
|
346
|
-
`If your project uses a different routing framework, open an issue at [github.com/CHAPIBUNNY/repolens](https://github.com/CHAPIBUNNY/repolens/issues) to request support.`,
|
|
347
363
|
``
|
|
348
364
|
);
|
|
365
|
+
|
|
366
|
+
if (isCLI) {
|
|
367
|
+
lines.push(
|
|
368
|
+
`This project is a **CLI tool** — it does not expose HTTP routes or serve web pages. This is expected behavior, not a detection failure.`,
|
|
369
|
+
``,
|
|
370
|
+
`CLI tools interact through terminal commands rather than URLs. See the **System Overview** or **Developer Onboarding** documents for command documentation.`,
|
|
371
|
+
``
|
|
372
|
+
);
|
|
373
|
+
} else if (isLibrary && !hasWebFramework) {
|
|
374
|
+
lines.push(
|
|
375
|
+
`This project is a **library/package** — it does not define its own routes or pages. Libraries are consumed by other applications that define their own routing.`,
|
|
376
|
+
``
|
|
377
|
+
);
|
|
378
|
+
} else {
|
|
379
|
+
lines.push(
|
|
380
|
+
`No routes were auto-detected in this scan. RepoLens currently supports:`,
|
|
381
|
+
``,
|
|
382
|
+
`| Framework | Pattern | Status |`,
|
|
383
|
+
`|-----------|---------|--------|`,
|
|
384
|
+
`| Next.js | \`pages/\` and \`app/\` directories | Supported |`,
|
|
385
|
+
`| Next.js API | \`pages/api/\` and App Router | Supported |`,
|
|
386
|
+
`| Express.js | \`app.get\`, \`router.post\`, etc. | Supported |`,
|
|
387
|
+
`| Fastify | \`fastify.get\`, \`fastify.post\`, etc. | Supported |`,
|
|
388
|
+
`| Hono | \`app.get\`, \`app.post\`, etc. | Supported |`,
|
|
389
|
+
`| React Router | \`<Route>\` components | Supported |`,
|
|
390
|
+
`| Vue Router | \`routes\` array definitions | Supported |`,
|
|
391
|
+
``,
|
|
392
|
+
`If your project uses a different routing framework, open an issue at [github.com/CHAPIBUNNY/repolens](https://github.com/CHAPIBUNNY/repolens/issues) to request support.`,
|
|
393
|
+
``
|
|
394
|
+
);
|
|
395
|
+
}
|
|
349
396
|
}
|
|
350
397
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
398
|
+
// Only add the route hint footer for projects that actually have routes
|
|
399
|
+
if (scan.pages?.length || scan.api?.length) {
|
|
400
|
+
lines.push(
|
|
401
|
+
`---`,
|
|
402
|
+
``,
|
|
403
|
+
`*Paths starting with \`/api/\` are backend endpoints; all others are user-facing pages.*`,
|
|
404
|
+
``
|
|
405
|
+
);
|
|
406
|
+
}
|
|
357
407
|
|
|
358
408
|
return lines.join("\n");
|
|
359
409
|
}
|