@aiready/context-analyzer 0.21.15 → 0.21.17

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.
@@ -0,0 +1,208 @@
1
+ import { analyzeContext } from '../analyzer';
2
+ import { generateSummary } from '../summary';
3
+
4
+ /**
5
+ * Generate HTML report
6
+ */
7
+ export function generateHTMLReport(
8
+ summary: ReturnType<typeof generateSummary>,
9
+ results: Awaited<ReturnType<typeof analyzeContext>>
10
+ ): string {
11
+ const totalIssues =
12
+ summary.criticalIssues + summary.majorIssues + summary.minorIssues;
13
+
14
+ // 'results' may be used in templates later; reference to avoid lint warnings
15
+ void results;
16
+
17
+ return `<!DOCTYPE html>
18
+ <html lang="en">
19
+ <head>
20
+ <meta charset="UTF-8">
21
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
22
+ <title>aiready Context Analysis Report</title>
23
+ <style>
24
+ body {
25
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
26
+ line-height: 1.6;
27
+ color: #333;
28
+ max-width: 1200px;
29
+ margin: 0 auto;
30
+ padding: 20px;
31
+ background-color: #f5f5f5;
32
+ }
33
+ h1, h2, h3 { color: #2c3e50; }
34
+ .header {
35
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
36
+ color: white;
37
+ padding: 30px;
38
+ border-radius: 8px;
39
+ margin-bottom: 30px;
40
+ }
41
+ .summary {
42
+ display: grid;
43
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
44
+ gap: 20px;
45
+ margin-bottom: 30px;
46
+ }
47
+ .card {
48
+ background: white;
49
+ padding: 20px;
50
+ border-radius: 8px;
51
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
52
+ }
53
+ .metric {
54
+ font-size: 2em;
55
+ font-weight: bold;
56
+ color: #667eea;
57
+ }
58
+ .label {
59
+ color: #666;
60
+ font-size: 0.9em;
61
+ margin-top: 5px;
62
+ }
63
+ .issue-critical { color: #e74c3c; }
64
+ .issue-major { color: #f39c12; }
65
+ .issue-minor { color: #3498db; }
66
+ table {
67
+ width: 100%;
68
+ border-collapse: collapse;
69
+ background: white;
70
+ border-radius: 8px;
71
+ overflow: hidden;
72
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
73
+ }
74
+ th, td {
75
+ padding: 12px;
76
+ text-align: left;
77
+ border-bottom: 1px solid #eee;
78
+ }
79
+ th {
80
+ background-color: #667eea;
81
+ color: white;
82
+ font-weight: 600;
83
+ }
84
+ tr:hover { background-color: #f8f9fa; }
85
+ .footer {
86
+ text-align: center;
87
+ margin-top: 40px;
88
+ padding: 20px;
89
+ color: #666;
90
+ font-size: 0.9em;
91
+ }
92
+ </style>
93
+ </head>
94
+ <body>
95
+ <div class="header">
96
+ <h1>🔍 AIReady Context Analysis Report</h1>
97
+ <p>Generated on ${new Date().toLocaleString()}</p>
98
+ </div>
99
+
100
+ <div class="summary">
101
+ <div class="card">
102
+ <div class="metric">${summary.totalFiles}</div>
103
+ <div class="label">Files Analyzed</div>
104
+ </div>
105
+ <div class="card">
106
+ <div class="metric">${summary.totalTokens.toLocaleString()}</div>
107
+ <div class="label">Total Tokens</div>
108
+ </div>
109
+ <div class="card">
110
+ <div class="metric">${summary.avgContextBudget.toFixed(0)}</div>
111
+ <div class="label">Avg Context Budget</div>
112
+ </div>
113
+ <div class="card">
114
+ <div class="metric ${totalIssues > 0 ? 'issue-major' : ''}">${totalIssues}</div>
115
+ <div class="label">Total Issues</div>
116
+ </div>
117
+ </div>
118
+
119
+ ${
120
+ totalIssues > 0
121
+ ? `
122
+ <div class="card" style="margin-bottom: 30px;">
123
+ <h2>⚠️ Issues Summary</h2>
124
+ <p>
125
+ <span class="issue-critical">🔴 Critical: ${summary.criticalIssues}</span> &nbsp;
126
+ <span class="issue-major">🟡 Major: ${summary.majorIssues}</span> &nbsp;
127
+ <span class="issue-minor">🔵 Minor: ${summary.minorIssues}</span>
128
+ </p>
129
+ <p><strong>Potential Savings:</strong> ${summary.totalPotentialSavings.toLocaleString()} tokens</p>
130
+ </div>
131
+ `
132
+ : ''
133
+ }
134
+
135
+ ${
136
+ summary.fragmentedModules.length > 0
137
+ ? `
138
+ <div class="card" style="margin-bottom: 30px;">
139
+ <h2>🧩 Fragmented Modules</h2>
140
+ <table>
141
+ <thead>
142
+ <tr>
143
+ <th>Domain</th>
144
+ <th>Files</th>
145
+ <th>Fragmentation</th>
146
+ <th>Token Cost</th>
147
+ </tr>
148
+ </thead>
149
+ <tbody>
150
+ ${summary.fragmentedModules
151
+ .map(
152
+ (m) => `
153
+ <tr>
154
+ <td>${m.domain}</td>
155
+ <td>${m.files.length}</td>
156
+ <td>${(m.fragmentationScore * 100).toFixed(0)}%</td>
157
+ <td>${m.totalTokens.toLocaleString()}</td>
158
+ </tr>
159
+ `
160
+ )
161
+ .join('')}
162
+ </tbody>
163
+ </table>
164
+ </div>
165
+ `
166
+ : ''
167
+ }
168
+
169
+ ${
170
+ summary.topExpensiveFiles.length > 0
171
+ ? `
172
+ <div class="card" style="margin-bottom: 30px;">
173
+ <h2>💸 Most Expensive Files</h2>
174
+ <table>
175
+ <thead>
176
+ <tr>
177
+ <th>File</th>
178
+ <th>Context Budget</th>
179
+ <th>Severity</th>
180
+ </tr>
181
+ </thead>
182
+ <tbody>
183
+ ${summary.topExpensiveFiles
184
+ .map(
185
+ (f) => `
186
+ <tr>
187
+ <td>${f.file}</td>
188
+ <td>${f.contextBudget.toLocaleString()} tokens</td>
189
+ <td class="issue-${f.severity}">${f.severity.toUpperCase()}</td>
190
+ </tr>
191
+ `
192
+ )
193
+ .join('')}
194
+ </tbody>
195
+ </table>
196
+ </div>
197
+ `
198
+ : ''
199
+ }
200
+
201
+ <div class="footer">
202
+ <p>Generated by <strong>@aiready/context-analyzer</strong></p>
203
+ <p>Like AIReady? <a href="https://github.com/caopengau/aiready-context-analyzer">Star us on GitHub</a></p>
204
+ <p>Found a bug? <a href="https://github.com/caopengau/aiready-context-analyzer/issues">Report it here</a></p>
205
+ </div>
206
+ </body>
207
+ </html>`;
208
+ }
@@ -0,0 +1,96 @@
1
+ import chalk from 'chalk';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import prompts from 'prompts';
5
+
6
+ /**
7
+ * Interactive setup: detect common frameworks and suggest excludes & focus areas
8
+ */
9
+ export async function runInteractiveSetup(
10
+ directory: string,
11
+ current: any
12
+ ): Promise<any> {
13
+ console.log(chalk.yellow("🧭 Interactive mode: let's tailor the analysis."));
14
+
15
+ const pkgPath = join(directory, 'package.json');
16
+ let deps: Record<string, string> = {};
17
+ if (existsSync(pkgPath)) {
18
+ try {
19
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
20
+ deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
21
+ } catch (e) {
22
+ void e;
23
+ // Ignore parse errors, use empty deps
24
+ }
25
+ }
26
+
27
+ const hasNextJs = existsSync(join(directory, '.next')) || !!deps['next'];
28
+ const hasCDK =
29
+ existsSync(join(directory, 'cdk.out')) ||
30
+ !!deps['aws-cdk-lib'] ||
31
+ Object.keys(deps).some((d) => d.startsWith('@aws-cdk/'));
32
+
33
+ const recommendedExcludes = new Set<string>(current.exclude || []);
34
+ if (
35
+ hasNextJs &&
36
+ !Array.from(recommendedExcludes).some((p) => p.includes('.next'))
37
+ ) {
38
+ recommendedExcludes.add('**/.next/**');
39
+ }
40
+ if (
41
+ hasCDK &&
42
+ !Array.from(recommendedExcludes).some((p) => p.includes('cdk.out'))
43
+ ) {
44
+ recommendedExcludes.add('**/cdk.out/**');
45
+ }
46
+
47
+ const { applyExcludes } = await prompts({
48
+ type: 'toggle',
49
+ name: 'applyExcludes',
50
+ message: `Detected ${hasNextJs ? 'Next.js ' : ''}${hasCDK ? 'AWS CDK ' : ''}frameworks. Apply recommended excludes?`,
51
+ initial: true,
52
+ active: 'yes',
53
+ inactive: 'no',
54
+ });
55
+
56
+ const nextOptions = { ...current };
57
+ if (applyExcludes) {
58
+ nextOptions.exclude = Array.from(recommendedExcludes);
59
+ }
60
+
61
+ const { focusArea } = await prompts({
62
+ type: 'select',
63
+ name: 'focusArea',
64
+ message: 'Which areas to focus?',
65
+ choices: [
66
+ { title: 'Frontend (web app)', value: 'frontend' },
67
+ { title: 'Backend (API/infra)', value: 'backend' },
68
+ { title: 'Both', value: 'both' },
69
+ ],
70
+ initial: 2,
71
+ });
72
+
73
+ if (focusArea === 'frontend') {
74
+ nextOptions.include = ['**/*.{ts,tsx,js,jsx}'];
75
+ nextOptions.exclude = Array.from(
76
+ new Set([
77
+ ...(nextOptions.exclude || []),
78
+ '**/cdk.out/**',
79
+ '**/infra/**',
80
+ '**/server/**',
81
+ '**/backend/**',
82
+ ])
83
+ );
84
+ } else if (focusArea === 'backend') {
85
+ nextOptions.include = [
86
+ '**/api/**',
87
+ '**/server/**',
88
+ '**/backend/**',
89
+ '**/infra/**',
90
+ '**/*.{ts,js,py,java}',
91
+ ];
92
+ }
93
+
94
+ console.log(chalk.green('✓ Interactive configuration applied.'));
95
+ return nextOptions;
96
+ }
@@ -0,0 +1,85 @@
1
+ import type { DependencyGraph, CoUsageData } from '../types';
2
+
3
+ /**
4
+ * Build co-usage matrix: track which files are imported together frequently.
5
+ *
6
+ * @param graph - The dependency graph to analyze.
7
+ * @returns Map of file path to nested map of related files and their co-occurrence counts.
8
+ */
9
+ export function buildCoUsageMatrix(
10
+ graph: DependencyGraph
11
+ ): Map<string, Map<string, number>> {
12
+ const coUsageMatrix = new Map<string, Map<string, number>>();
13
+
14
+ for (const [, node] of graph.nodes) {
15
+ const imports = node.imports;
16
+
17
+ for (let i = 0; i < imports.length; i++) {
18
+ const fileA = imports[i];
19
+ if (!coUsageMatrix.has(fileA)) coUsageMatrix.set(fileA, new Map());
20
+
21
+ for (let j = i + 1; j < imports.length; j++) {
22
+ const fileB = imports[j];
23
+ const fileAUsage = coUsageMatrix.get(fileA)!;
24
+ fileAUsage.set(fileB, (fileAUsage.get(fileB) || 0) + 1);
25
+
26
+ if (!coUsageMatrix.has(fileB)) coUsageMatrix.set(fileB, new Map());
27
+ const fileBUsage = coUsageMatrix.get(fileB)!;
28
+ fileBUsage.set(fileA, (fileBUsage.get(fileA) || 0) + 1);
29
+ }
30
+ }
31
+ }
32
+
33
+ return coUsageMatrix;
34
+ }
35
+
36
+ /**
37
+ * Find semantic clusters using frequently occurring co-usage patterns.
38
+ *
39
+ * @param coUsageMatrix - The co-usage matrix from buildCoUsageMatrix.
40
+ * @param minCoUsage - Minimum co-usage count to consider a strong relationship (default: 3).
41
+ * @returns Map of cluster representative files to their associated cluster members.
42
+ */
43
+ export function findSemanticClusters(
44
+ coUsageMatrix: Map<string, Map<string, number>>,
45
+ minCoUsage: number = 3
46
+ ): Map<string, string[]> {
47
+ const clusters = new Map<string, string[]>();
48
+ const visited = new Set<string>();
49
+
50
+ for (const [file, coUsages] of coUsageMatrix) {
51
+ if (visited.has(file)) continue;
52
+
53
+ const cluster: string[] = [file];
54
+ visited.add(file);
55
+
56
+ for (const [relatedFile, count] of coUsages) {
57
+ if (count >= minCoUsage && !visited.has(relatedFile)) {
58
+ cluster.push(relatedFile);
59
+ visited.add(relatedFile);
60
+ }
61
+ }
62
+
63
+ if (cluster.length > 1) clusters.set(file, cluster);
64
+ }
65
+
66
+ return clusters;
67
+ }
68
+
69
+ /**
70
+ * Retrieve co-usage data for a specific file.
71
+ *
72
+ * @param file - The file path to look up.
73
+ * @param coUsageMatrix - The global co-usage matrix.
74
+ * @returns Formatted co-usage data object.
75
+ */
76
+ export function getCoUsageData(
77
+ file: string,
78
+ coUsageMatrix: Map<string, Map<string, number>>
79
+ ): CoUsageData {
80
+ return {
81
+ file,
82
+ coImportedWith: coUsageMatrix.get(file) || new Map(),
83
+ sharedImporters: [],
84
+ };
85
+ }
@@ -0,0 +1,46 @@
1
+ import type { DependencyGraph } from '../types';
2
+
3
+ /**
4
+ * Identify candidates for module consolidation based on high co-usage or shared types.
5
+ *
6
+ * @param graph - The dependency graph.
7
+ * @param coUsageMatrix - Matrix of frequently paired files.
8
+ * @param typeGraph - Map of shared type references.
9
+ * @param minCoUsage - Minimum co-usage count threshold.
10
+ * @param minSharedTypes - Minimum shared types threshold.
11
+ * @returns Array of consolidation candidates sorted by strength.
12
+ */
13
+ export function findConsolidationCandidates(
14
+ graph: DependencyGraph,
15
+ coUsageMatrix: Map<string, Map<string, number>>,
16
+ typeGraph: Map<string, Set<string>>,
17
+ minCoUsage: number = 5,
18
+ minSharedTypes: number = 2
19
+ ): { files: string[]; reason: string; strength: number }[] {
20
+ const candidates: { files: string[]; reason: string; strength: number }[] =
21
+ [];
22
+ for (const [fileA, coUsages] of coUsageMatrix) {
23
+ const nodeA = graph.nodes.get(fileA);
24
+ if (!nodeA) continue;
25
+ for (const [fileB, count] of coUsages) {
26
+ if (fileB <= fileA || count < minCoUsage) continue;
27
+ const nodeB = graph.nodes.get(fileB);
28
+ if (!nodeB) continue;
29
+ const typesA = new Set(
30
+ nodeA.exports.flatMap((e) => e.typeReferences || [])
31
+ );
32
+ const typesB = new Set(
33
+ nodeB.exports.flatMap((e) => e.typeReferences || [])
34
+ );
35
+ const sharedTypes = Array.from(typesA).filter((t) => typesB.has(t));
36
+ if (sharedTypes.length >= minSharedTypes || count >= minCoUsage * 2) {
37
+ candidates.push({
38
+ files: [fileA, fileB],
39
+ reason: `High co-usage (${count}x)`,
40
+ strength: count / 10,
41
+ });
42
+ }
43
+ }
44
+ }
45
+ return candidates.sort((a, b) => b.strength - a.strength);
46
+ }
@@ -1,101 +1,32 @@
1
1
  import type {
2
2
  DependencyGraph,
3
- CoUsageData,
4
3
  DomainAssignment,
5
4
  DomainSignals,
6
5
  ExportInfo,
7
- } from './types';
8
- import { singularize } from './utils/string-utils';
6
+ } from '../types';
7
+ import { singularize } from '../utils/string-utils';
9
8
 
10
9
  /**
11
- * Build co-usage matrix: track which files are imported together frequently.
12
- *
13
- * @param graph - The dependency graph to analyze.
14
- * @returns Map of file path to nested map of related files and their co-occurrence counts.
15
- */
16
- export function buildCoUsageMatrix(
17
- graph: DependencyGraph
18
- ): Map<string, Map<string, number>> {
19
- const coUsageMatrix = new Map<string, Map<string, number>>();
20
-
21
- for (const [, node] of graph.nodes) {
22
- const imports = node.imports;
23
-
24
- for (let i = 0; i < imports.length; i++) {
25
- const fileA = imports[i];
26
- if (!coUsageMatrix.has(fileA)) coUsageMatrix.set(fileA, new Map());
27
-
28
- for (let j = i + 1; j < imports.length; j++) {
29
- const fileB = imports[j];
30
- const fileAUsage = coUsageMatrix.get(fileA)!;
31
- fileAUsage.set(fileB, (fileAUsage.get(fileB) || 0) + 1);
32
-
33
- if (!coUsageMatrix.has(fileB)) coUsageMatrix.set(fileB, new Map());
34
- const fileBUsage = coUsageMatrix.get(fileB)!;
35
- fileBUsage.set(fileA, (fileBUsage.get(fileA) || 0) + 1);
36
- }
37
- }
38
- }
39
-
40
- return coUsageMatrix;
41
- }
42
-
43
- /**
44
- * Extract type dependencies from AST exports to build a type-based relationship graph.
45
- *
46
- * @param graph - The dependency graph to analyze.
47
- * @returns Map of type reference names to sets of files that consume or export them.
48
- */
49
- export function buildTypeGraph(
50
- graph: DependencyGraph
51
- ): Map<string, Set<string>> {
52
- const typeGraph = new Map<string, Set<string>>();
53
-
54
- for (const [file, node] of graph.nodes) {
55
- for (const exp of node.exports) {
56
- if (exp.typeReferences) {
57
- for (const typeRef of exp.typeReferences) {
58
- if (!typeGraph.has(typeRef)) typeGraph.set(typeRef, new Set());
59
- typeGraph.get(typeRef)!.add(file);
60
- }
61
- }
62
- }
63
- }
64
-
65
- return typeGraph;
66
- }
67
-
68
- /**
69
- * Find semantic clusters using frequently occurring co-usage patterns.
10
+ * Calculate confidence score for a domain assignment based on signals.
70
11
  *
71
- * @param coUsageMatrix - The co-usage matrix from buildCoUsageMatrix.
72
- * @param minCoUsage - Minimum co-usage count to consider a strong relationship (default: 3).
73
- * @returns Map of cluster representative files to their associated cluster members.
12
+ * @param signals - The set of semantic signals detected for a domain.
13
+ * @returns Numerical confidence score (0-1).
74
14
  */
75
- export function findSemanticClusters(
76
- coUsageMatrix: Map<string, Map<string, number>>,
77
- minCoUsage: number = 3
78
- ): Map<string, string[]> {
79
- const clusters = new Map<string, string[]>();
80
- const visited = new Set<string>();
81
-
82
- for (const [file, coUsages] of coUsageMatrix) {
83
- if (visited.has(file)) continue;
84
-
85
- const cluster: string[] = [file];
86
- visited.add(file);
87
-
88
- for (const [relatedFile, count] of coUsages) {
89
- if (count >= minCoUsage && !visited.has(relatedFile)) {
90
- cluster.push(relatedFile);
91
- visited.add(relatedFile);
92
- }
93
- }
94
-
95
- if (cluster.length > 1) clusters.set(file, cluster);
96
- }
97
-
98
- return clusters;
15
+ export function calculateDomainConfidence(signals: DomainSignals): number {
16
+ const weights = {
17
+ coUsage: 0.35,
18
+ typeReference: 0.3,
19
+ exportName: 0.15,
20
+ importPath: 0.1,
21
+ folderStructure: 0.1,
22
+ };
23
+ let confidence = 0;
24
+ if (signals.coUsage) confidence += weights.coUsage;
25
+ if (signals.typeReference) confidence += weights.typeReference;
26
+ if (signals.exportName) confidence += weights.exportName;
27
+ if (signals.importPath) confidence += weights.importPath;
28
+ if (signals.folderStructure) confidence += weights.folderStructure;
29
+ return confidence;
99
30
  }
100
31
 
101
32
  /**
@@ -184,29 +115,6 @@ export function inferDomainFromSemantics(
184
115
  return assignments;
185
116
  }
186
117
 
187
- /**
188
- * Calculate confidence score for a domain assignment based on signals.
189
- *
190
- * @param signals - The set of semantic signals detected for a domain.
191
- * @returns Numerical confidence score (0-1).
192
- */
193
- export function calculateDomainConfidence(signals: DomainSignals): number {
194
- const weights = {
195
- coUsage: 0.35,
196
- typeReference: 0.3,
197
- exportName: 0.15,
198
- importPath: 0.1,
199
- folderStructure: 0.1,
200
- };
201
- let confidence = 0;
202
- if (signals.coUsage) confidence += weights.coUsage;
203
- if (signals.typeReference) confidence += weights.typeReference;
204
- if (signals.exportName) confidence += weights.exportName;
205
- if (signals.importPath) confidence += weights.importPath;
206
- if (signals.folderStructure) confidence += weights.folderStructure;
207
- return confidence;
208
- }
209
-
210
118
  /**
211
119
  * Regex-based export extraction (legacy/fallback)
212
120
  *
@@ -348,65 +256,3 @@ export function inferDomain(
348
256
 
349
257
  return 'unknown';
350
258
  }
351
-
352
- /**
353
- * Retrieve co-usage data for a specific file.
354
- *
355
- * @param file - The file path to look up.
356
- * @param coUsageMatrix - The global co-usage matrix.
357
- * @returns Formatted co-usage data object.
358
- */
359
- export function getCoUsageData(
360
- file: string,
361
- coUsageMatrix: Map<string, Map<string, number>>
362
- ): CoUsageData {
363
- return {
364
- file,
365
- coImportedWith: coUsageMatrix.get(file) || new Map(),
366
- sharedImporters: [],
367
- };
368
- }
369
-
370
- /**
371
- * Identify candidates for module consolidation based on high co-usage or shared types.
372
- *
373
- * @param graph - The dependency graph.
374
- * @param coUsageMatrix - Matrix of frequently paired files.
375
- * @param typeGraph - Map of shared type references.
376
- * @param minCoUsage - Minimum co-usage count threshold.
377
- * @param minSharedTypes - Minimum shared types threshold.
378
- * @returns Array of consolidation candidates sorted by strength.
379
- */
380
- export function findConsolidationCandidates(
381
- graph: DependencyGraph,
382
- coUsageMatrix: Map<string, Map<string, number>>,
383
- typeGraph: Map<string, Set<string>>,
384
- minCoUsage: number = 5,
385
- minSharedTypes: number = 2
386
- ): any[] {
387
- const candidates: any[] = [];
388
- for (const [fileA, coUsages] of coUsageMatrix) {
389
- const nodeA = graph.nodes.get(fileA);
390
- if (!nodeA) continue;
391
- for (const [fileB, count] of coUsages) {
392
- if (fileB <= fileA || count < minCoUsage) continue;
393
- const nodeB = graph.nodes.get(fileB);
394
- if (!nodeB) continue;
395
- const typesA = new Set(
396
- nodeA.exports.flatMap((e) => e.typeReferences || [])
397
- );
398
- const typesB = new Set(
399
- nodeB.exports.flatMap((e) => e.typeReferences || [])
400
- );
401
- const sharedTypes = Array.from(typesA).filter((t) => typesB.has(t));
402
- if (sharedTypes.length >= minSharedTypes || count >= minCoUsage * 2) {
403
- candidates.push({
404
- files: [fileA, fileB],
405
- reason: `High co-usage (${count}x)`,
406
- strength: count / 10,
407
- });
408
- }
409
- }
410
- }
411
- return candidates.sort((a, b) => b.strength - a.strength);
412
- }
@@ -0,0 +1,26 @@
1
+ import type { DependencyGraph } from '../types';
2
+
3
+ /**
4
+ * Extract type dependencies from AST exports to build a type-based relationship graph.
5
+ *
6
+ * @param graph - The dependency graph to analyze.
7
+ * @returns Map of type reference names to sets of files that consume or export them.
8
+ */
9
+ export function buildTypeGraph(
10
+ graph: DependencyGraph
11
+ ): Map<string, Set<string>> {
12
+ const typeGraph = new Map<string, Set<string>>();
13
+
14
+ for (const [file, node] of graph.nodes) {
15
+ for (const exp of node.exports) {
16
+ if (exp.typeReferences) {
17
+ for (const typeRef of exp.typeReferences) {
18
+ if (!typeGraph.has(typeRef)) typeGraph.set(typeRef, new Set());
19
+ typeGraph.get(typeRef)!.add(file);
20
+ }
21
+ }
22
+ }
23
+ }
24
+
25
+ return typeGraph;
26
+ }