@aiready/context-analyzer 0.21.23 → 0.21.24

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.
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
2
2
  import { buildDependencyGraph } from '../index';
3
3
 
4
4
  describe('Auto-detection from folder structure', () => {
5
- it('should auto-detect domain keywords from folder paths', () => {
5
+ it('should auto-detect domain keywords from folder paths', async () => {
6
6
  const files = [
7
7
  {
8
8
  file: 'src/payments/process.ts',
@@ -14,7 +14,7 @@ describe('Auto-detection from folder structure', () => {
14
14
  },
15
15
  ];
16
16
 
17
- const graph = buildDependencyGraph(files);
17
+ const graph = await buildDependencyGraph(files);
18
18
  const paymentsNode = graph.nodes.get('src/payments/process.ts');
19
19
  const ordersNode = graph.nodes.get('src/orders/create.ts');
20
20
 
@@ -25,7 +25,7 @@ describe('Auto-detection from folder structure', () => {
25
25
  expect(ordersNode?.exports[0].inferredDomain).toBe('order');
26
26
  });
27
27
 
28
- it('should detect domains from nested folders', () => {
28
+ it('should detect domains from nested folders', async () => {
29
29
  const files = [
30
30
  {
31
31
  file: 'src/api/invoices/handler.ts',
@@ -33,14 +33,14 @@ describe('Auto-detection from folder structure', () => {
33
33
  },
34
34
  ];
35
35
 
36
- const graph = buildDependencyGraph(files);
36
+ const graph = await buildDependencyGraph(files);
37
37
  const node = graph.nodes.get('src/api/invoices/handler.ts');
38
38
 
39
39
  // Should detect 'invoice' from path (invoices folder)
40
40
  expect(node?.exports[0].inferredDomain).toBe('invoice');
41
41
  });
42
42
 
43
- it('should skip common infrastructure folders', () => {
43
+ it('should skip common infrastructure folders', async () => {
44
44
  const files = [
45
45
  {
46
46
  file: 'src/utils/helpers/format.ts',
@@ -48,14 +48,14 @@ describe('Auto-detection from folder structure', () => {
48
48
  },
49
49
  ];
50
50
 
51
- const graph = buildDependencyGraph(files);
51
+ const graph = await buildDependencyGraph(files);
52
52
  const node = graph.nodes.get('src/utils/helpers/format.ts');
53
53
 
54
54
  // 'utils' and 'helpers' should be skipped, no domain detected
55
55
  expect(node?.exports[0].inferredDomain).toBe('unknown');
56
56
  });
57
57
 
58
- it('should merge auto-detected with custom keywords', () => {
58
+ it('should merge auto-detected with custom keywords', async () => {
59
59
  const files = [
60
60
  {
61
61
  file: 'src/receipts/scan.ts',
@@ -63,7 +63,7 @@ describe('Auto-detection from folder structure', () => {
63
63
  },
64
64
  ];
65
65
 
66
- const graph = buildDependencyGraph(files, {
66
+ const graph = await buildDependencyGraph(files, {
67
67
  domainKeywords: ['receipt'], // Custom keyword
68
68
  });
69
69
  const node = graph.nodes.get('src/receipts/scan.ts');
@@ -74,7 +74,7 @@ describe('Auto-detection from folder structure', () => {
74
74
  });
75
75
 
76
76
  describe('Import-path domain inference', () => {
77
- it('should infer domain from import paths', () => {
77
+ it('should infer domain from import paths', async () => {
78
78
  const files = [
79
79
  {
80
80
  file: 'src/lib/session.ts',
@@ -89,14 +89,14 @@ describe('Import-path domain inference', () => {
89
89
  },
90
90
  ];
91
91
 
92
- const graph = buildDependencyGraph(files);
92
+ const graph = await buildDependencyGraph(files);
93
93
  const sessionNode = graph.nodes.get('src/lib/session.ts');
94
94
 
95
95
  // session.ts imports from '../payments/...' so should infer 'payment' domain
96
96
  expect(sessionNode?.exports[0].inferredDomain).toBe('payment');
97
97
  });
98
98
 
99
- it('should infer domain from absolute import paths', () => {
99
+ it('should infer domain from absolute import paths', async () => {
100
100
  const files = [
101
101
  {
102
102
  file: 'src/components/nav-links.ts',
@@ -111,14 +111,14 @@ describe('Import-path domain inference', () => {
111
111
  },
112
112
  ];
113
113
 
114
- const graph = buildDependencyGraph(files);
114
+ const graph = await buildDependencyGraph(files);
115
115
  const navNode = graph.nodes.get('src/components/nav-links.ts');
116
116
 
117
117
  // nav-links.ts imports from '@/orders/...' so should infer 'order' domain
118
118
  expect(navNode?.exports[0].inferredDomain).toBe('order');
119
119
  });
120
120
 
121
- it('should use identifier name first before import-path fallback', () => {
121
+ it('should use identifier name first before import-path fallback', async () => {
122
122
  const files = [
123
123
  {
124
124
  file: 'src/lib/handler.ts',
@@ -129,14 +129,14 @@ describe('Import-path domain inference', () => {
129
129
  },
130
130
  ];
131
131
 
132
- const graph = buildDependencyGraph(files);
132
+ const graph = await buildDependencyGraph(files);
133
133
  const node = graph.nodes.get('src/lib/handler.ts');
134
134
 
135
135
  // processInvoice should match 'invoice' from identifier, not 'payment' from imports
136
136
  expect(node?.exports[0].inferredDomain).toBe('invoice');
137
137
  });
138
138
 
139
- it('should fall back to import-path when identifier is generic', () => {
139
+ it('should fall back to import-path when identifier is generic', async () => {
140
140
  const files = [
141
141
  {
142
142
  file: 'src/lib/dynamodb.ts',
@@ -147,7 +147,7 @@ describe('Import-path domain inference', () => {
147
147
  },
148
148
  ];
149
149
 
150
- const graph = buildDependencyGraph(files);
150
+ const graph = await buildDependencyGraph(files);
151
151
  const node = graph.nodes.get('src/lib/dynamodb.ts');
152
152
 
153
153
  // 'connect' is generic, should infer 'customer' from import path
@@ -6,7 +6,7 @@ import {
6
6
  } from '../index';
7
7
 
8
8
  describe('fragmentation coupling discount', () => {
9
- it('does not apply discount when files have no shared imports', () => {
9
+ it('does not apply discount when files have no shared imports', async () => {
10
10
  const files = [
11
11
  {
12
12
  file: 'src/billing/a.ts',
@@ -22,7 +22,7 @@ describe('fragmentation coupling discount', () => {
22
22
  },
23
23
  ];
24
24
 
25
- const graph = buildDependencyGraph(files);
25
+ const graph = await buildDependencyGraph(files);
26
26
  const clusters = detectModuleClusters(graph);
27
27
  const cluster = clusters.find((c) => c.domain === 'billing');
28
28
  expect(cluster).toBeDefined();
@@ -38,7 +38,7 @@ describe('fragmentation coupling discount', () => {
38
38
  expect(cluster!.fragmentationScore).toBeCloseTo(expected, 6);
39
39
  });
40
40
 
41
- it('applies up-to-20% discount when files share identical imports', () => {
41
+ it('applies up-to-20% discount when files share identical imports', async () => {
42
42
  const files = [
43
43
  {
44
44
  file: 'src/billing/a.ts',
@@ -54,7 +54,7 @@ describe('fragmentation coupling discount', () => {
54
54
  },
55
55
  ];
56
56
 
57
- const graph = buildDependencyGraph(files);
57
+ const graph = await buildDependencyGraph(files);
58
58
  const clusters = detectModuleClusters(graph);
59
59
  const cluster = clusters.find((c) => c.domain === 'billing');
60
60
  expect(cluster).toBeDefined();
@@ -3,9 +3,10 @@ import * as pythonContext from '../analyzers/python-context';
3
3
 
4
4
  // Mock @aiready/core
5
5
  vi.mock('@aiready/core', () => ({
6
- getParser: vi.fn((filename: string) => {
6
+ getParser: vi.fn(async (filename: string) => {
7
7
  if (filename.endsWith('.py')) {
8
8
  return {
9
+ initialize: vi.fn().mockResolvedValue(undefined),
9
10
  parse: vi.fn(() => ({
10
11
  imports: [
11
12
  { source: 'os', specifiers: ['path'], isRelative: false },
@@ -16,6 +17,7 @@ vi.mock('@aiready/core', () => ({
16
17
  { name: 'my_function', type: 'function' },
17
18
  ],
18
19
  })),
20
+ language: 'python',
19
21
  };
20
22
  }
21
23
  return null;
@@ -39,7 +41,7 @@ describe('python-context', () => {
39
41
  describe('analyzePythonContext', () => {
40
42
  it('should return empty array when parser is not available', async () => {
41
43
  const { getParser } = await import('@aiready/core');
42
- vi.mocked(getParser).mockReturnValueOnce(null);
44
+ vi.mocked(getParser).mockResolvedValueOnce(null);
43
45
 
44
46
  const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
45
47
 
@@ -11,6 +11,17 @@ vi.mock('@aiready/core', () => ({
11
11
  `<table><thead><tr>${options.headers.map((h) => `<th>${h}</th>`).join('')}</thead><tbody>${options.rows.map((r) => `<tr>${r.map((c) => `<td>${c}</td>`).join('')}</tr>`).join('')}</tbody></table>`,
12
12
  generateReportFooter: (options: any) =>
13
13
  `<footer>${options.title} - ${options.packageUrl}</footer>`,
14
+ generateReportHero: (title: string, subtitle?: string) =>
15
+ `<div class="hero"><h1>${title}</h1><p>${subtitle}</p></div>`,
16
+ generateIssueSummary: (
17
+ critical: number,
18
+ major: number,
19
+ minor: number,
20
+ savings?: number
21
+ ) =>
22
+ `<div class="issue-summary">Issues: Critical:${critical}, Major:${major}, Minor:${minor}. Savings: ${savings}</div>`,
23
+ wrapInCard: (content: string, title?: string) =>
24
+ `<div class="card"><h2>${title}</h2>${content}</div>`,
14
25
  }));
15
26
 
16
27
  describe('generateHTMLReport', () => {
@@ -98,11 +109,11 @@ describe('generateHTMLReport', () => {
98
109
 
99
110
  const html = generateHTMLReport(summary, results);
100
111
 
101
- expect(html).toContain('Issues Summary');
112
+ expect(html).toContain('Issues:');
102
113
  expect(html).toContain('Critical:');
103
114
  expect(html).toContain('Major:');
104
115
  expect(html).toContain('Minor:');
105
- expect(html).toContain('Potential Savings');
116
+ expect(html).toContain('Savings');
106
117
  });
107
118
 
108
119
  it('should not include issues section when no issues', () => {
@@ -227,6 +238,6 @@ describe('generateHTMLReport', () => {
227
238
  const html = generateHTMLReport(summary, results);
228
239
 
229
240
  // Should show issues section
230
- expect(html).toContain('Issues Summary');
241
+ expect(html).toContain('Issues:');
231
242
  });
232
243
  });
@@ -38,7 +38,7 @@ describe('Context Scoring', () => {
38
38
  const result = calculateContextScore(summary);
39
39
 
40
40
  expect(result.score).toBeLessThan(70);
41
- expect(result.factors.some((f) => f.name === 'Context Budget')).toBe(
41
+ expect(result.factors.some((f: any) => f.name === 'Context Budget')).toBe(
42
42
  true
43
43
  );
44
44
  expect(result.recommendations.length).toBeGreaterThan(0);
@@ -60,9 +60,13 @@ describe('Context Scoring', () => {
60
60
 
61
61
  // With depth=12: depthScore=80, rawScore=100*0.35+80*0.25+100*0.25=80, no bonus (frag=0.2 not <0.2)
62
62
  expect(result.score).toBe(80);
63
- expect(result.factors.some((f) => f.name === 'Import Depth')).toBe(true);
63
+ expect(result.factors.some((f: any) => f.name === 'Import Depth')).toBe(
64
+ true
65
+ );
64
66
  expect(
65
- result.recommendations.some((r) => r.action.includes('import chains'))
67
+ result.recommendations.some((r: any) =>
68
+ r.action.includes('import chains')
69
+ )
66
70
  ).toBe(true);
67
71
  });
68
72
 
@@ -86,7 +90,9 @@ describe('Context Scoring', () => {
86
90
  // Actually frag=0.7 >= 0.2, so no bonus
87
91
  // rawScore = 80, no penalties = 80
88
92
  expect(result.score).toBeLessThan(85); // Adjusted for new calculation
89
- expect(result.factors.some((f) => f.name === 'Fragmentation')).toBe(true);
93
+ expect(result.factors.some((f: any) => f.name === 'Fragmentation')).toBe(
94
+ true
95
+ );
90
96
  });
91
97
 
92
98
  it('should apply critical issue penalties', () => {
@@ -108,9 +114,9 @@ describe('Context Scoring', () => {
108
114
  // criticalPenalty = min(20, 5*3) = min(20,15) = 15
109
115
  // finalScore = 85 - 15 = 70
110
116
  expect(result.score).toBe(70);
111
- expect(result.factors.some((f) => f.name === 'Critical Issues')).toBe(
112
- true
113
- );
117
+ expect(
118
+ result.factors.some((f: any) => f.name === 'Critical Issues')
119
+ ).toBe(true);
114
120
  });
115
121
 
116
122
  it('should handle extreme max budget penalty', () => {
@@ -128,10 +134,10 @@ describe('Context Scoring', () => {
128
134
  const result = calculateContextScore(summary);
129
135
 
130
136
  expect(
131
- result.factors.some((f) => f.name === 'Extreme File Detected')
137
+ result.factors.some((f: any) => f.name === 'Extreme File Detected')
132
138
  ).toBe(true);
133
139
  expect(
134
- result.recommendations.some((r) =>
140
+ result.recommendations.some((r: any) =>
135
141
  r.action.includes('Split large file')
136
142
  )
137
143
  ).toBe(true);
@@ -214,7 +220,7 @@ describe('Context Scoring', () => {
214
220
  // finalScore = 90
215
221
  expect(result.score).toBe(90);
216
222
  expect(
217
- result.factors.some((f) => f.name === 'Well-Organized Codebase')
223
+ result.factors.some((f: any) => f.name === 'Well-Organized Codebase')
218
224
  ).toBe(true);
219
225
  });
220
226
 
@@ -235,7 +241,7 @@ describe('Context Scoring', () => {
235
241
  // No bonus because fragmentation >= 0.2
236
242
  expect(result.score).toBe(85); // 100*0.35 + 100*0.25 + 100*0.25 = 85
237
243
  expect(
238
- result.factors.some((f) => f.name === 'Well-Organized Codebase')
244
+ result.factors.some((f: any) => f.name === 'Well-Organized Codebase')
239
245
  ).toBe(false);
240
246
  });
241
247
 
@@ -51,7 +51,7 @@ export async function analyzePythonContext(
51
51
  rootDir: string
52
52
  ): Promise<PythonContextMetrics[]> {
53
53
  const results: PythonContextMetrics[] = [];
54
- const parser = getParser('dummy.py');
54
+ const parser = await getParser('dummy.py');
55
55
 
56
56
  if (!parser) {
57
57
  console.warn('Python parser not available');
@@ -135,7 +135,7 @@ async function buildPythonDependencyGraph(
135
135
  rootDir: string
136
136
  ): Promise<Map<string, Set<string>>> {
137
137
  const graph = new Map<string, Set<string>>();
138
- const parser = getParser('dummy.py');
138
+ const parser = await getParser('dummy.py');
139
139
 
140
140
  if (!parser) return graph;
141
141
 
package/src/ast-utils.ts CHANGED
@@ -12,14 +12,14 @@ import { inferDomain, extractExports } from './semantic/domain-inference';
12
12
  * @returns Array of high-fidelity export metadata.
13
13
  * @lastUpdated 2026-03-18
14
14
  */
15
- export function extractExportsWithAST(
15
+ export async function extractExportsWithAST(
16
16
  content: string,
17
17
  filePath: string,
18
18
  domainOptions?: { domainKeywords?: string[] },
19
19
  fileImports?: string[]
20
- ): ExportInfo[] {
20
+ ): Promise<ExportInfo[]> {
21
21
  try {
22
- const { exports: astExports } = parseFileExports(content, filePath);
22
+ const { exports: astExports } = await parseFileExports(content, filePath);
23
23
 
24
24
  if (astExports.length === 0 && !isTestFile(filePath)) {
25
25
  // If AST fails to find anything, we still use regex as a last resort
@@ -128,10 +128,10 @@ export function extractDomainKeywordsFromPaths(files: FileContent[]): string[] {
128
128
  * @param options - Optional configuration for domain detection.
129
129
  * @returns Complete dependency graph with nodes, edges, and semantic matrices.
130
130
  */
131
- export function buildDependencyGraph(
131
+ export async function buildDependencyGraph(
132
132
  files: FileContent[],
133
133
  options?: { domainKeywords?: string[] }
134
- ): DependencyGraph {
134
+ ): Promise<DependencyGraph> {
135
135
  const nodes = new Map<string, DependencyNode>();
136
136
  const edges = new Map<string, Set<string>>();
137
137
 
@@ -142,7 +142,7 @@ export function buildDependencyGraph(
142
142
 
143
143
  for (const { file, content } of files) {
144
144
  // 1. Get high-fidelity AST-based imports & exports
145
- const { imports: astImports } = parseFileExports(content, file);
145
+ const { imports: astImports } = await parseFileExports(content, file);
146
146
 
147
147
  // 2. Resolve imports to absolute paths in the graph
148
148
  const resolvedImports = astImports
@@ -152,7 +152,7 @@ export function buildDependencyGraph(
152
152
  const importSources = astImports.map((i) => i.source);
153
153
 
154
154
  // 3. Wrap with platform-specific metadata (v0.11+)
155
- const exports = extractExportsWithAST(
155
+ const exports = await extractExportsWithAST(
156
156
  content,
157
157
  file,
158
158
  { domainKeywords: autoDetectedKeywords },
package/src/mapper.ts CHANGED
@@ -27,6 +27,12 @@ export interface MappingOptions {
27
27
 
28
28
  /**
29
29
  * Maps a single dependency node to a comprehensive ContextAnalysisResult.
30
+ *
31
+ * @param node - The dependency node to map
32
+ * @param graph - The full dependency graph
33
+ * @param clusters - All identified module clusters
34
+ * @param allCircularDeps - All identified circular dependencies
35
+ * @param options - Mapping options for detailed analysis
30
36
  */
31
37
  export function mapNodeToResult(
32
38
  node: DependencyNode,
@@ -69,7 +69,7 @@ export async function analyzeContext(
69
69
  }))
70
70
  );
71
71
 
72
- const graph = buildDependencyGraph(
72
+ const graph = await buildDependencyGraph(
73
73
  fileContents.filter((f) => !f.file.toLowerCase().endsWith('.py'))
74
74
  );
75
75
 
@@ -2,9 +2,12 @@ import { analyzeContext } from '../analyzer';
2
2
  import { generateSummary } from '../summary';
3
3
  import {
4
4
  generateReportHead,
5
+ generateReportHero,
5
6
  generateStatCards,
7
+ generateIssueSummary,
6
8
  generateTable,
7
9
  generateReportFooter,
10
+ wrapInCard,
8
11
  } from '@aiready/core';
9
12
 
10
13
  /**
@@ -33,22 +36,21 @@ export function generateHTMLReport(
33
36
  },
34
37
  ]);
35
38
 
36
- let body = `<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 8px; margin-bottom: 30px;">
37
- <h1 style="border: none; color: white; margin: 0;">🔍 AIReady Context Analysis Report</h1>
38
- <p style="margin: 10px 0 0 0;">Generated on ${new Date().toLocaleString()}</p>
39
- </div>
39
+ const hero = generateReportHero(
40
+ '🔍 AIReady Context Analysis Report',
41
+ `Generated on ${new Date().toLocaleString()}`
42
+ );
43
+
44
+ let body = `${hero}
40
45
  ${stats}`;
41
46
 
42
47
  if (totalIssues > 0) {
43
- body += `<div class="card" style="margin-bottom: 30px;">
44
- <h2>⚠️ Issues Summary</h2>
45
- <p>
46
- <span class="critical">🔴 Critical: ${summary.criticalIssues}</span> &nbsp;
47
- <span class="major">🟡 Major: ${summary.majorIssues}</span> &nbsp;
48
- <span class="minor">🔵 Minor: ${summary.minorIssues}</span>
49
- </p>
50
- <p><strong>Potential Savings:</strong> ${summary.totalPotentialSavings.toLocaleString()} tokens</p>
51
- </div>`;
48
+ body += generateIssueSummary(
49
+ summary.criticalIssues,
50
+ summary.majorIssues,
51
+ summary.minorIssues,
52
+ summary.totalPotentialSavings
53
+ );
52
54
  }
53
55
 
54
56
  if (summary.fragmentedModules.length > 0) {
@@ -58,10 +60,13 @@ ${stats}`;
58
60
  `${(m.fragmentationScore * 100).toFixed(0)}%`,
59
61
  m.totalTokens.toLocaleString(),
60
62
  ]);
61
- body += `<div class="card" style="margin-bottom: 30px;">
62
- <h2>🧩 Fragmented Modules</h2>
63
- ${generateTable({ headers: ['Domain', 'Files', 'Fragmentation', 'Token Cost'], rows: fragmentedRows })}
64
- </div>`;
63
+ body += wrapInCard(
64
+ generateTable({
65
+ headers: ['Domain', 'Files', 'Fragmentation', 'Token Cost'],
66
+ rows: fragmentedRows,
67
+ }),
68
+ '🧩 Fragmented Modules'
69
+ );
65
70
  }
66
71
 
67
72
  if (summary.topExpensiveFiles.length > 0) {
@@ -70,10 +75,13 @@ ${stats}`;
70
75
  `${f.contextBudget.toLocaleString()} tokens`,
71
76
  `<span class="issue-${f.severity}">${f.severity.toUpperCase()}</span>`,
72
77
  ]);
73
- body += `<div class="card" style="margin-bottom: 30px;">
74
- <h2>💸 Most Expensive Files</h2>
75
- ${generateTable({ headers: ['File', 'Context Budget', 'Severity'], rows: expensiveRows })}
76
- </div>`;
78
+ body += wrapInCard(
79
+ generateTable({
80
+ headers: ['File', 'Context Budget', 'Severity'],
81
+ rows: expensiveRows,
82
+ }),
83
+ '💸 Most Expensive Files'
84
+ );
77
85
  }
78
86
 
79
87
  const footer = generateReportFooter({