@aiready/core 0.9.20 → 0.9.23

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/dist/index.d.ts CHANGED
@@ -1,274 +1,5 @@
1
- interface AnalysisResult {
2
- fileName: string;
3
- issues: Issue[];
4
- metrics: Metrics;
5
- }
6
- interface Issue {
7
- type: IssueType;
8
- severity: 'critical' | 'major' | 'minor' | 'info';
9
- message: string;
10
- location: Location;
11
- suggestion?: string;
12
- }
13
- type IssueType = 'duplicate-pattern' | 'context-fragmentation' | 'doc-drift' | 'naming-inconsistency' | 'naming-quality' | 'pattern-inconsistency' | 'architecture-inconsistency' | 'dead-code' | 'circular-dependency' | 'missing-types';
14
- interface Location {
15
- file: string;
16
- line: number;
17
- column?: number;
18
- endLine?: number;
19
- endColumn?: number;
20
- }
21
- interface Metrics {
22
- tokenCost?: number;
23
- complexityScore?: number;
24
- consistencyScore?: number;
25
- docFreshnessScore?: number;
26
- }
27
- interface ScanOptions {
28
- rootDir: string;
29
- include?: string[];
30
- exclude?: string[];
31
- maxDepth?: number;
32
- }
33
- interface AIReadyConfig {
34
- scan?: {
35
- include?: string[];
36
- exclude?: string[];
37
- tools?: string[];
38
- };
39
- tools?: {
40
- 'pattern-detect'?: {
41
- enabled?: boolean;
42
- scoreWeight?: number;
43
- minSimilarity?: number;
44
- minLines?: number;
45
- batchSize?: number;
46
- approx?: boolean;
47
- minSharedTokens?: number;
48
- maxCandidatesPerBlock?: number;
49
- streamResults?: boolean;
50
- maxResults?: number;
51
- };
52
- 'context-analyzer'?: {
53
- enabled?: boolean;
54
- scoreWeight?: number;
55
- maxDepth?: number;
56
- maxContextBudget?: number;
57
- minCohesion?: number;
58
- maxFragmentation?: number;
59
- focus?: 'fragmentation' | 'cohesion' | 'depth' | 'all';
60
- includeNodeModules?: boolean;
61
- maxResults?: number;
62
- domainKeywords?: string[];
63
- domainPatterns?: string[];
64
- pathDomainMap?: Record<string, string>;
65
- };
66
- 'consistency'?: {
67
- enabled?: boolean;
68
- scoreWeight?: number;
69
- acceptedAbbreviations?: string[];
70
- shortWords?: string[];
71
- disableChecks?: ('single-letter' | 'abbreviation' | 'convention-mix' | 'unclear' | 'poor-naming')[];
72
- };
73
- [toolName: string]: {
74
- enabled?: boolean;
75
- scoreWeight?: number;
76
- [key: string]: any;
77
- } | undefined;
78
- };
79
- scoring?: {
80
- threshold?: number;
81
- showBreakdown?: boolean;
82
- compareBaseline?: string;
83
- saveTo?: string;
84
- };
85
- output?: {
86
- format?: 'console' | 'json' | 'html';
87
- file?: string;
88
- };
89
- visualizer?: {
90
- groupingDirs?: string[];
91
- graph?: {
92
- maxNodes?: number;
93
- maxEdges?: number;
94
- };
95
- };
96
- }
97
- interface Report {
98
- summary: {
99
- totalFiles: number;
100
- totalIssues: number;
101
- criticalIssues: number;
102
- majorIssues: number;
103
- };
104
- results: AnalysisResult[];
105
- metrics: {
106
- overallScore: number;
107
- tokenCostTotal: number;
108
- avgConsistency: number;
109
- };
110
- }
111
-
112
- /**
113
- * Language-agnostic AST and parser interfaces for multi-language support
114
- *
115
- * This module provides abstractions for parsing different programming languages
116
- * while maintaining a consistent interface for analysis tools.
117
- */
118
- /**
119
- * Supported programming languages
120
- */
121
- declare enum Language {
122
- TypeScript = "typescript",
123
- JavaScript = "javascript",
124
- Python = "python",
125
- Java = "java",
126
- Go = "go",
127
- Rust = "rust",
128
- CSharp = "csharp"
129
- }
130
- /**
131
- * File extensions mapped to languages
132
- */
133
- declare const LANGUAGE_EXTENSIONS: Record<string, Language>;
134
- /**
135
- * Location information in source code
136
- */
137
- interface SourceLocation {
138
- line: number;
139
- column: number;
140
- }
141
- interface SourceRange {
142
- start: SourceLocation;
143
- end: SourceLocation;
144
- }
145
- /**
146
- * Common AST node type (language-agnostic)
147
- */
148
- interface CommonASTNode {
149
- type: string;
150
- loc?: SourceRange;
151
- raw?: any;
152
- }
153
- /**
154
- * Export information (function, class, variable, etc.)
155
- */
156
- interface ExportInfo {
157
- name: string;
158
- type: 'function' | 'class' | 'const' | 'type' | 'interface' | 'default' | 'variable';
159
- loc?: SourceRange;
160
- /** Imports used within this export */
161
- imports?: string[];
162
- /** Dependencies on other exports in same file */
163
- dependencies?: string[];
164
- /** TypeScript types referenced */
165
- typeReferences?: string[];
166
- /** For methods: parent class name */
167
- parentClass?: string;
168
- /** For functions/methods: parameters */
169
- parameters?: string[];
170
- /** Visibility (public, private, protected) */
171
- visibility?: 'public' | 'private' | 'protected';
172
- }
173
- /**
174
- * Import information
175
- */
176
- interface ImportInfo {
177
- /** Module being imported from */
178
- source: string;
179
- /** What's being imported */
180
- specifiers: string[];
181
- /** Is this a type-only import (TypeScript) */
182
- isTypeOnly?: boolean;
183
- /** Location in source */
184
- loc?: SourceRange;
185
- }
186
- /**
187
- * Parse result containing exports and imports
188
- */
189
- interface ParseResult {
190
- exports: ExportInfo[];
191
- imports: ImportInfo[];
192
- /** Language of the parsed file */
193
- language: Language;
194
- /** Any parse warnings (non-fatal) */
195
- warnings?: string[];
196
- }
197
- /**
198
- * Naming convention rules per language
199
- */
200
- interface NamingConvention {
201
- /** Allowed variable naming patterns */
202
- variablePattern: RegExp;
203
- /** Allowed function naming patterns */
204
- functionPattern: RegExp;
205
- /** Allowed class naming patterns */
206
- classPattern: RegExp;
207
- /** Allowed constant naming patterns */
208
- constantPattern: RegExp;
209
- /** Language-specific exceptions (e.g., __init__ in Python) */
210
- exceptions?: string[];
211
- }
212
- /**
213
- * Language-specific configuration
214
- */
215
- interface LanguageConfig {
216
- language: Language;
217
- /** File extensions for this language */
218
- extensions: string[];
219
- /** Naming conventions */
220
- namingConventions: NamingConvention;
221
- /** Common abbreviations allowed */
222
- allowedAbbreviations?: string[];
223
- /** Language-specific keywords to ignore */
224
- keywords?: string[];
225
- }
226
- /**
227
- * Abstract interface for language parsers
228
- * Each language implementation should implement this interface
229
- */
230
- interface LanguageParser {
231
- /** Language this parser handles */
232
- readonly language: Language;
233
- /** File extensions this parser supports */
234
- readonly extensions: string[];
235
- /**
236
- * Parse source code and extract structure
237
- * @param code - Source code to parse
238
- * @param filePath - Path to the file (for context)
239
- * @returns Parse result with exports and imports
240
- * @throws ParseError if code has syntax errors
241
- */
242
- parse(code: string, filePath: string): ParseResult;
243
- /**
244
- * Get naming conventions for this language
245
- */
246
- getNamingConventions(): NamingConvention;
247
- /**
248
- * Check if this parser can handle a file
249
- * @param filePath - File path to check
250
- */
251
- canHandle(filePath: string): boolean;
252
- }
253
- /**
254
- * Parser error with location information
255
- */
256
- declare class ParseError extends Error {
257
- readonly filePath: string;
258
- readonly loc?: SourceLocation | undefined;
259
- constructor(message: string, filePath: string, loc?: SourceLocation | undefined);
260
- }
261
- /**
262
- * Statistics about parsed code
263
- */
264
- interface ParseStatistics {
265
- language: Language;
266
- filesAnalyzed: number;
267
- totalExports: number;
268
- totalImports: number;
269
- parseErrors: number;
270
- warnings: number;
271
- }
1
+ import { ScanOptions, AIReadyConfig, LanguageParser, Language, ParseResult, NamingConvention } from './client.js';
2
+ export { AnalysisResult, CommonASTNode, DEFAULT_TOOL_WEIGHTS, ExportInfo, GraphData, GraphEdge, GraphIssueSeverity, GraphMetadata, GraphNode, ImportInfo, Issue, IssueType, LANGUAGE_EXTENSIONS, LanguageConfig, Location, Metrics, ParseError, ParseStatistics, Report, ScoringConfig, ScoringResult, SourceLocation, SourceRange, TOOL_NAME_MAP, ToolScoringOutput, calculateOverallScore, formatScore, formatToolScore, generateHTML, getRating, getRatingDisplay, getToolWeight, normalizeToolName, parseWeightString } from './client.js';
272
3
 
273
4
  declare const DEFAULT_EXCLUDE: string[];
274
5
  /**
@@ -381,116 +112,6 @@ declare function handleCLIError(error: unknown, commandName: string): never;
381
112
  */
382
113
  declare function getElapsedTime(startTime: number): string;
383
114
 
384
- /**
385
- * AI Readiness Scoring System
386
- *
387
- * Provides dynamic, composable scoring across multiple analysis tools.
388
- * Each tool contributes a 0-100 score with configurable weights.
389
- */
390
- interface ToolScoringOutput {
391
- /** Unique tool identifier (e.g., "pattern-detect") */
392
- toolName: string;
393
- /** Normalized 0-100 score for this tool */
394
- score: number;
395
- /** Raw metrics used to calculate the score */
396
- rawMetrics: Record<string, any>;
397
- /** Factors that influenced the score */
398
- factors: Array<{
399
- name: string;
400
- impact: number;
401
- description: string;
402
- }>;
403
- /** Actionable recommendations with estimated impact */
404
- recommendations: Array<{
405
- action: string;
406
- estimatedImpact: number;
407
- priority: 'high' | 'medium' | 'low';
408
- }>;
409
- }
410
- interface ScoringResult {
411
- /** Overall AI Readiness Score (0-100) */
412
- overall: number;
413
- /** Rating category */
414
- rating: 'Excellent' | 'Good' | 'Fair' | 'Needs Work' | 'Critical';
415
- /** Timestamp of score calculation */
416
- timestamp: string;
417
- /** Tools that contributed to this score */
418
- toolsUsed: string[];
419
- /** Breakdown by tool */
420
- breakdown: ToolScoringOutput[];
421
- /** Calculation details */
422
- calculation: {
423
- formula: string;
424
- weights: Record<string, number>;
425
- normalized: string;
426
- };
427
- }
428
- interface ScoringConfig {
429
- /** Minimum passing score (exit code 1 if below) */
430
- threshold?: number;
431
- /** Show detailed breakdown in output */
432
- showBreakdown?: boolean;
433
- /** Path to baseline JSON for comparison */
434
- compareBaseline?: string;
435
- /** Auto-save score to this path */
436
- saveTo?: string;
437
- }
438
- /**
439
- * Default weights for known tools.
440
- * New tools get weight of 10 if not specified.
441
- */
442
- declare const DEFAULT_TOOL_WEIGHTS: Record<string, number>;
443
- /**
444
- * Tool name normalization map (shorthand -> full name)
445
- */
446
- declare const TOOL_NAME_MAP: Record<string, string>;
447
- /**
448
- * Normalize tool name from shorthand to full name
449
- */
450
- declare function normalizeToolName(shortName: string): string;
451
- /**
452
- * Get tool weight with fallback priority:
453
- * 1. CLI override
454
- * 2. Tool config scoreWeight
455
- * 3. Default weight
456
- * 4. 10 (for unknown tools)
457
- */
458
- declare function getToolWeight(toolName: string, toolConfig?: {
459
- scoreWeight?: number;
460
- }, cliOverride?: number): number;
461
- /**
462
- * Parse weight string from CLI (e.g., "patterns:50,context:30")
463
- */
464
- declare function parseWeightString(weightStr?: string): Map<string, number>;
465
- /**
466
- * Calculate overall AI Readiness Score from multiple tool scores.
467
- *
468
- * Formula: Σ(tool_score × tool_weight) / Σ(active_tool_weights)
469
- *
470
- * This allows dynamic composition - score adjusts automatically
471
- * based on which tools actually ran.
472
- */
473
- declare function calculateOverallScore(toolOutputs: Map<string, ToolScoringOutput>, config?: any, cliWeights?: Map<string, number>): ScoringResult;
474
- /**
475
- * Convert numeric score to rating category
476
- */
477
- declare function getRating(score: number): ScoringResult['rating'];
478
- /**
479
- * Get rating emoji and color for display
480
- */
481
- declare function getRatingDisplay(rating: ScoringResult['rating']): {
482
- emoji: string;
483
- color: string;
484
- };
485
- /**
486
- * Format score for display with rating
487
- */
488
- declare function formatScore(result: ScoringResult): string;
489
- /**
490
- * Format individual tool score for display
491
- */
492
- declare function formatToolScore(output: ToolScoringOutput): string;
493
-
494
115
  /**
495
116
  * Parser Factory - Manages language-specific parsers
496
117
  *
@@ -619,4 +240,4 @@ declare class PythonParser implements LanguageParser {
619
240
  private extractExportsRegex;
620
241
  }
621
242
 
622
- export { type AIReadyConfig, type ASTNode, type AnalysisResult, type CLIOptions, type CommonASTNode, DEFAULT_EXCLUDE, DEFAULT_TOOL_WEIGHTS, type ExportInfo, type ExportWithImports, type FileImport, type ImportInfo, type Issue, type IssueType, LANGUAGE_EXTENSIONS, Language, type LanguageConfig, type LanguageParser, type Location, type Metrics, type NamingConvention, ParseError, type ParseResult, type ParseStatistics, ParserFactory, PythonParser, type Report, type ScanOptions, type ScoringConfig, type ScoringResult, type SourceLocation, type SourceRange, TOOL_NAME_MAP, type ToolScoringOutput, TypeScriptParser, calculateImportSimilarity, calculateOverallScore, estimateTokens, extractFunctions, extractImports, formatScore, formatToolScore, getElapsedTime, getFileExtension, getParser, getRating, getRatingDisplay, getSupportedLanguages, getToolWeight, handleCLIError, handleJSONOutput, isFileSupported, isSourceFile, loadConfig, loadMergedConfig, mergeConfigWithDefaults, normalizeToolName, parseCode, parseFileExports, parseWeightString, readFileContent, resolveOutputPath, scanFiles };
243
+ export { AIReadyConfig, type ASTNode, type CLIOptions, DEFAULT_EXCLUDE, type ExportWithImports, type FileImport, Language, LanguageParser, NamingConvention, ParseResult, ParserFactory, PythonParser, ScanOptions, TypeScriptParser, calculateImportSimilarity, estimateTokens, extractFunctions, extractImports, getElapsedTime, getFileExtension, getParser, getSupportedLanguages, handleCLIError, handleJSONOutput, isFileSupported, isSourceFile, loadConfig, loadMergedConfig, mergeConfigWithDefaults, parseCode, parseFileExports, readFileContent, resolveOutputPath, scanFiles };
package/dist/index.js CHANGED
@@ -36,6 +36,7 @@ __export(index_exports, {
36
36
  extractImports: () => extractImports,
37
37
  formatScore: () => formatScore,
38
38
  formatToolScore: () => formatToolScore,
39
+ generateHTML: () => generateHTML,
39
40
  getElapsedTime: () => getElapsedTime,
40
41
  getFileExtension: () => getFileExtension,
41
42
  getParser: () => getParser,
@@ -472,6 +473,172 @@ function getElapsedTime(startTime) {
472
473
  return ((Date.now() - startTime) / 1e3).toFixed(2);
473
474
  }
474
475
 
476
+ // src/utils/visualization.ts
477
+ function generateHTML(graph) {
478
+ const payload = JSON.stringify(graph, null, 2);
479
+ return `<!doctype html>
480
+ <html>
481
+ <head>
482
+ <meta charset="utf-8" />
483
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
484
+ <title>AIReady Visualization</title>
485
+ <style>
486
+ html,body { height: 100%; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0 }
487
+ #container { display:flex; height:100vh }
488
+ #panel { width: 320px; padding: 16px; background: #071130; box-shadow: -2px 0 8px rgba(0,0,0,0.3); overflow:auto }
489
+ #canvasWrap { flex:1; display:flex; align-items:center; justify-content:center }
490
+ canvas { background: #0b1220; border-radius:8px }
491
+ .stat { margin-bottom:12px }
492
+ </style>
493
+ </head>
494
+ <body>
495
+ <div id="container">
496
+ <div id="canvasWrap"><canvas id="canvas" width="1200" height="800"></canvas></div>
497
+ <div id="panel">
498
+ <h2>AIReady Visualization</h2>
499
+ <div class="stat"><strong>Files:</strong> <span id="stat-files"></span></div>
500
+ <div class="stat"><strong>Dependencies:</strong> <span id="stat-deps"></span></div>
501
+ <div class="stat"><strong>Legend</strong></div>
502
+ <div style="font-size:13px;line-height:1.3;color:#cbd5e1;margin-top:8px">
503
+ <div style="margin-bottom:8px"><span style="display:inline-block;width:12px;height:12px;background:#ff4d4f;margin-right:8px;border:1px solid rgba(255,255,255,0.06)"></span><strong>Critical</strong>: highest severity issues.</div>
504
+ <div style="margin-bottom:8px"><span style="display:inline-block;width:12px;height:12px;background:#ff9900;margin-right:8px;border:1px solid rgba(255,255,255,0.06)"></span><strong>Major</strong>: important issues.</div>
505
+ <div style="margin-bottom:8px"><span style="display:inline-block;width:12px;height:12px;background:#ffd666;margin-right:8px;border:1px solid rgba(255,255,255,0.06)"></span><strong>Minor</strong>: low priority issues.</div>
506
+ <div style="margin-bottom:8px"><span style="display:inline-block;width:12px;height:12px;background:#91d5ff;margin-right:8px;border:1px solid rgba(255,255,255,0.06)"></span><strong>Info</strong>: informational notes.</div>
507
+ <div style="margin-top:10px;color:#94a3b8"><strong>Node size</strong>: larger = higher token cost, more issues or dependency weight.</div>
508
+ <div style="margin-top:6px;color:#94a3b8"><strong>Proximity</strong>: nodes that are spatially close are more contextually related; relatedness is represented by distance and size rather than explicit edges.</div>
509
+ <div style="margin-top:6px;color:#94a3b8"><strong>Edge colors</strong>: <span style="color:#fb7e81">Similarity</span>, <span style="color:#84c1ff">Dependency</span>, <span style="color:#ffa500">Reference</span>, default <span style="color:#334155">Other</span>.</div>
510
+ </div>
511
+ </div>
512
+ </div>
513
+
514
+ <script>
515
+ const graphData = ${payload};
516
+ document.getElementById('stat-files').textContent = graphData.metadata.totalFiles;
517
+ document.getElementById('stat-deps').textContent = graphData.metadata.totalDependencies;
518
+
519
+ const canvas = document.getElementById('canvas');
520
+ const ctx = canvas.getContext('2d');
521
+
522
+ const nodes = graphData.nodes.map((n, i) => ({
523
+ ...n,
524
+ x: canvas.width / 2 + Math.cos(i / graphData.nodes.length * Math.PI * 2) * (Math.min(canvas.width, canvas.height) / 3),
525
+ y: canvas.height / 2 + Math.sin(i / graphData.nodes.length * Math.PI * 2) * (Math.min(canvas.width, canvas.height) / 3),
526
+ }));
527
+
528
+ function draw() {
529
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
530
+
531
+ graphData.edges.forEach(edge => {
532
+ const s = nodes.find(n => n.id === edge.source);
533
+ const t = nodes.find(n => n.id === edge.target);
534
+ if (!s || !t) return;
535
+ if (edge.type === 'related') return;
536
+ if (edge.type === 'similarity') {
537
+ ctx.strokeStyle = '#fb7e81';
538
+ ctx.lineWidth = 1.2;
539
+ } else if (edge.type === 'dependency') {
540
+ ctx.strokeStyle = '#84c1ff';
541
+ ctx.lineWidth = 1.0;
542
+ } else if (edge.type === 'reference') {
543
+ ctx.strokeStyle = '#ffa500';
544
+ ctx.lineWidth = 0.9;
545
+ } else {
546
+ ctx.strokeStyle = '#334155';
547
+ ctx.lineWidth = 0.8;
548
+ }
549
+ ctx.beginPath();
550
+ ctx.moveTo(s.x, s.y);
551
+ ctx.lineTo(t.x, t.y);
552
+ ctx.stroke();
553
+ });
554
+
555
+ const groups = {};
556
+ nodes.forEach(n => {
557
+ const g = n.group || '__default';
558
+ if (!groups[g]) groups[g] = { minX: n.x, minY: n.y, maxX: n.x, maxY: n.y };
559
+ groups[g].minX = Math.min(groups[g].minX, n.x);
560
+ groups[g].minY = Math.min(groups[g].minY, n.y);
561
+ groups[g].maxX = Math.max(groups[g].maxX, n.x);
562
+ groups[g].maxY = Math.max(groups[g].maxY, n.y);
563
+ });
564
+
565
+ const groupRelations = {};
566
+ graphData.edges.forEach(edge => {
567
+ const sNode = nodes.find(n => n.id === edge.source);
568
+ const tNode = nodes.find(n => n.id === edge.target);
569
+ if (!sNode || !tNode) return;
570
+ const g1 = sNode.group || '__default';
571
+ const g2 = tNode.group || '__default';
572
+ if (g1 === g2) return;
573
+ const key = g1 < g2 ? g1 + '::' + g2 : g2 + '::' + g1;
574
+ groupRelations[key] = (groupRelations[key] || 0) + 1;
575
+ });
576
+
577
+ Object.keys(groupRelations).forEach(k => {
578
+ const count = groupRelations[k];
579
+ const [ga, gb] = k.split('::');
580
+ if (!groups[ga] || !groups[gb]) return;
581
+ const ax = (groups[ga].minX + groups[ga].maxX) / 2;
582
+ const ay = (groups[ga].minY + groups[ga].maxY) / 2;
583
+ const bx = (groups[gb].minX + groups[gb].maxX) / 2;
584
+ const by = (groups[gb].minY + groups[gb].maxY) / 2;
585
+ ctx.beginPath();
586
+ ctx.strokeStyle = 'rgba(148,163,184,0.25)';
587
+ ctx.lineWidth = Math.min(6, 0.6 + Math.sqrt(count));
588
+ ctx.moveTo(ax, ay);
589
+ ctx.lineTo(bx, by);
590
+ ctx.stroke();
591
+ });
592
+
593
+ Object.keys(groups).forEach(g => {
594
+ if (g === '__default') return;
595
+ const box = groups[g];
596
+ const pad = 16;
597
+ const x = box.minX - pad;
598
+ const y = box.minY - pad;
599
+ const w = (box.maxX - box.minX) + pad * 2;
600
+ const h = (box.maxY - box.minY) + pad * 2;
601
+ ctx.save();
602
+ ctx.fillStyle = 'rgba(30,64,175,0.04)';
603
+ ctx.strokeStyle = 'rgba(30,64,175,0.12)';
604
+ ctx.lineWidth = 1.2;
605
+ const r = 8;
606
+ ctx.beginPath();
607
+ ctx.moveTo(x + r, y);
608
+ ctx.arcTo(x + w, y, x + w, y + h, r);
609
+ ctx.arcTo(x + w, y + h, x, y + h, r);
610
+ ctx.arcTo(x, y + h, x, y, r);
611
+ ctx.arcTo(x, y, x + w, y, r);
612
+ ctx.closePath();
613
+ ctx.fill();
614
+ ctx.stroke();
615
+ ctx.restore();
616
+ ctx.fillStyle = '#94a3b8';
617
+ ctx.font = '11px sans-serif';
618
+ ctx.fillText(g, x + 8, y + 14);
619
+ });
620
+
621
+ nodes.forEach(n => {
622
+ const sizeVal = (n.size || n.value || 1);
623
+ const r = 6 + (sizeVal / 2);
624
+ ctx.beginPath();
625
+ ctx.fillStyle = n.color || '#60a5fa';
626
+ ctx.arc(n.x, n.y, r, 0, Math.PI * 2);
627
+ ctx.fill();
628
+
629
+ ctx.fillStyle = '#e2e8f0';
630
+ ctx.font = '11px sans-serif';
631
+ ctx.textAlign = 'center';
632
+ ctx.fillText(n.label || n.id.split('/').slice(-1)[0], n.x, n.y + r + 12);
633
+ });
634
+ }
635
+
636
+ draw();
637
+ </script>
638
+ </body>
639
+ </html>`;
640
+ }
641
+
475
642
  // src/scoring.ts
476
643
  var DEFAULT_TOOL_WEIGHTS = {
477
644
  "pattern-detect": 40,
@@ -1113,6 +1280,7 @@ function getSupportedLanguages() {
1113
1280
  extractImports,
1114
1281
  formatScore,
1115
1282
  formatToolScore,
1283
+ generateHTML,
1116
1284
  getElapsedTime,
1117
1285
  getFileExtension,
1118
1286
  getParser,