readme-gen-analyzer 1.0.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.
Files changed (92) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/README.md +77 -0
  3. package/dist/analyzers/ast-feature.detector.d.ts +10 -0
  4. package/dist/analyzers/ast-feature.detector.js +151 -0
  5. package/dist/analyzers/definition.extractor.d.ts +9 -0
  6. package/dist/analyzers/definition.extractor.js +141 -0
  7. package/dist/analyzers/dependency.analyzer.d.ts +14 -0
  8. package/dist/analyzers/dependency.analyzer.js +30 -0
  9. package/dist/analyzers/devops.analyzer.d.ts +3 -0
  10. package/dist/analyzers/devops.analyzer.js +42 -0
  11. package/dist/analyzers/env.extractor.d.ts +7 -0
  12. package/dist/analyzers/env.extractor.js +46 -0
  13. package/dist/analyzers/example.analyzer.d.ts +6 -0
  14. package/dist/analyzers/example.analyzer.js +84 -0
  15. package/dist/analyzers/feature.detector.d.ts +4 -0
  16. package/dist/analyzers/feature.detector.js +68 -0
  17. package/dist/analyzers/package.parser.d.ts +28 -0
  18. package/dist/analyzers/package.parser.js +341 -0
  19. package/dist/analyzers/polyglot.extractors.d.ts +18 -0
  20. package/dist/analyzers/polyglot.extractors.js +153 -0
  21. package/dist/analyzers/route.extractor.d.ts +10 -0
  22. package/dist/analyzers/route.extractor.js +41 -0
  23. package/dist/analyzers/schema.analyzer.d.ts +3 -0
  24. package/dist/analyzers/schema.analyzer.js +48 -0
  25. package/dist/analyzers/semantic.refiner.d.ts +16 -0
  26. package/dist/analyzers/semantic.refiner.js +154 -0
  27. package/dist/analyzers/structure.analyzer.d.ts +18 -0
  28. package/dist/analyzers/structure.analyzer.js +150 -0
  29. package/dist/analyzers/trace.analyzer.d.ts +10 -0
  30. package/dist/analyzers/trace.analyzer.js +75 -0
  31. package/dist/index.d.ts +27 -0
  32. package/dist/index.js +44 -0
  33. package/dist/internal/analysis/chunker.d.ts +25 -0
  34. package/dist/internal/analysis/chunker.js +78 -0
  35. package/dist/internal/analysis/evidence.d.ts +17 -0
  36. package/dist/internal/analysis/evidence.js +130 -0
  37. package/dist/internal/analysis/techStack.d.ts +6 -0
  38. package/dist/internal/analysis/techStack.js +67 -0
  39. package/dist/internal/llm/llmClient.d.ts +69 -0
  40. package/dist/internal/llm/llmClient.js +204 -0
  41. package/dist/internal/pipeline/merge.d.ts +14 -0
  42. package/dist/internal/pipeline/merge.js +53 -0
  43. package/dist/internal/pipeline/persona.d.ts +7 -0
  44. package/dist/internal/pipeline/persona.js +28 -0
  45. package/dist/internal/pipeline/quality.d.ts +9 -0
  46. package/dist/internal/pipeline/quality.js +52 -0
  47. package/dist/internal/pipeline/readme.d.ts +3 -0
  48. package/dist/internal/pipeline/readme.js +80 -0
  49. package/dist/internal/pipeline/runPipeline.d.ts +57 -0
  50. package/dist/internal/pipeline/runPipeline.js +101 -0
  51. package/dist/internal/pipeline/stages.d.ts +5 -0
  52. package/dist/internal/pipeline/stages.js +85 -0
  53. package/dist/internal/pipeline/types.d.ts +98 -0
  54. package/dist/internal/pipeline/types.js +2 -0
  55. package/dist/types.d.ts +85 -0
  56. package/dist/types.js +2 -0
  57. package/dist/utils/scanner.d.ts +14 -0
  58. package/dist/utils/scanner.js +81 -0
  59. package/dist/utils/scriptsMarkdown.d.ts +9 -0
  60. package/dist/utils/scriptsMarkdown.js +131 -0
  61. package/package.json +19 -0
  62. package/src/analyzers/ast-feature.detector.ts +173 -0
  63. package/src/analyzers/definition.extractor.ts +156 -0
  64. package/src/analyzers/dependency.analyzer.ts +32 -0
  65. package/src/analyzers/devops.analyzer.ts +44 -0
  66. package/src/analyzers/env.extractor.ts +58 -0
  67. package/src/analyzers/example.analyzer.ts +96 -0
  68. package/src/analyzers/feature.detector.ts +65 -0
  69. package/src/analyzers/package.parser.ts +364 -0
  70. package/src/analyzers/polyglot.extractors.ts +169 -0
  71. package/src/analyzers/route.extractor.ts +54 -0
  72. package/src/analyzers/schema.analyzer.ts +50 -0
  73. package/src/analyzers/semantic.refiner.ts +163 -0
  74. package/src/analyzers/structure.analyzer.ts +156 -0
  75. package/src/analyzers/trace.analyzer.ts +75 -0
  76. package/src/index.ts +29 -0
  77. package/src/internal/analysis/chunker.ts +103 -0
  78. package/src/internal/analysis/evidence.ts +152 -0
  79. package/src/internal/analysis/techStack.ts +71 -0
  80. package/src/internal/llm/llmClient.ts +261 -0
  81. package/src/internal/pipeline/merge.ts +63 -0
  82. package/src/internal/pipeline/persona.ts +27 -0
  83. package/src/internal/pipeline/quality.ts +47 -0
  84. package/src/internal/pipeline/readme.ts +98 -0
  85. package/src/internal/pipeline/runPipeline.ts +153 -0
  86. package/src/internal/pipeline/stages.ts +89 -0
  87. package/src/internal/pipeline/types.ts +102 -0
  88. package/src/types.ts +100 -0
  89. package/src/utils/scanner.ts +48 -0
  90. package/src/utils/scriptsMarkdown.ts +140 -0
  91. package/test-local.ts +16 -0
  92. package/tsconfig.json +16 -0
@@ -0,0 +1,96 @@
1
+ import { Project, SyntaxKind, CallExpression } from 'ts-morph';
2
+
3
+ export class ExampleAnalyzer {
4
+ public static analyze(files: Record<string, string>) {
5
+ const examples: any[] = [];
6
+ const project = new Project({ useInMemoryFileSystem: true });
7
+
8
+ for (const [filePath, content] of Object.entries(files)) {
9
+ if (filePath.endsWith('.py') && this.isPythonTestPath(filePath)) {
10
+ this.collectPythonTests(filePath, content, examples);
11
+ continue;
12
+ }
13
+ if (filePath.endsWith('_test.go')) {
14
+ this.collectGoTests(filePath, content, examples);
15
+ continue;
16
+ }
17
+ if (!filePath.match(/\.(test|spec)\.(ts|js|jsx|tsx)$/) && !filePath.includes('__tests__')) continue;
18
+
19
+ const sourceFile = project.createSourceFile(filePath, content);
20
+ const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
21
+
22
+ for (const call of callExpressions) {
23
+ const name = call.getExpression().getText().toLowerCase();
24
+ if (['it', 'test'].includes(name)) {
25
+ const args = call.getArguments();
26
+ if (args.length >= 2) {
27
+ const description = args[0].getText().replace(/['"`]/g, '');
28
+ const body = args[1].getText();
29
+
30
+ // Heuristic for "Good" examples: contain multiple calls or interesting snippets
31
+ const isInteresting = body.includes('await') || body.includes('expect') || body.split('\n').length > 5;
32
+
33
+ if (isInteresting && examples.length < 15) {
34
+ const cleanBody = body.replace(/^\(\) => \{|\}$|^\(async \(\) => \{|\}$/g, '').trim();
35
+ examples.push({
36
+ description,
37
+ code: cleanBody,
38
+ file: filePath
39
+ });
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+
46
+ return examples.length > 0 ? examples : undefined;
47
+ }
48
+
49
+ private static isPythonTestPath(filePath: string): boolean {
50
+ const base = filePath.split('/').pop() || '';
51
+ if (base.startsWith('test_') && base.endsWith('.py')) return true;
52
+ if (filePath.includes('/tests/') || filePath.includes('/test/')) return true;
53
+ return false;
54
+ }
55
+
56
+ private static collectPythonTests(
57
+ filePath: string,
58
+ content: string,
59
+ examples: any[],
60
+ ) {
61
+ const re = /^(async\s+)?def\s+(test_\w+)\s*\([^)]*\):/gm;
62
+ let m: RegExpExecArray | null;
63
+ while ((m = re.exec(content)) !== null) {
64
+ if (examples.length >= 15) break;
65
+ const start = m.index;
66
+ const next = content.indexOf('\ndef ', start + 5);
67
+ const block = content.slice(
68
+ start,
69
+ next === -1 ? Math.min(start + 800, content.length) : Math.min(next, start + 800),
70
+ );
71
+ examples.push({
72
+ description: m[2]!,
73
+ code: block.trim(),
74
+ file: filePath,
75
+ });
76
+ }
77
+ }
78
+
79
+ private static collectGoTests(
80
+ filePath: string,
81
+ content: string,
82
+ examples: any[],
83
+ ) {
84
+ const re = /^func\s+(Test\w+)\s*\(/gm;
85
+ let m: RegExpExecArray | null;
86
+ while ((m = re.exec(content)) !== null) {
87
+ if (examples.length >= 15) break;
88
+ const block = content.slice(m.index, m.index + 700);
89
+ examples.push({
90
+ description: m[1]!,
91
+ code: block.trim(),
92
+ file: filePath,
93
+ });
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,65 @@
1
+ export class FeatureDetector {
2
+ private static FEATURE_MAP: Record<string, string> = {
3
+ 'express': 'REST API Framework',
4
+ 'fastify': 'High-performance REST API',
5
+ 'react': 'Frontend Development',
6
+ 'next': 'Server-side Rendering',
7
+ 'mongoose': 'MongoDB Database Support',
8
+ 'prisma': 'Modern Database ORM',
9
+ 'jest': 'Automated Unit Testing',
10
+ 'cypress': 'End-to-End Testing',
11
+ 'docker': 'Containerization Support',
12
+ 'passport': 'Authentication & Authorization',
13
+ 'jsonwebtoken': 'JWT Auth Tokens',
14
+ 'axios': 'External API Integration',
15
+ 'dotenv': 'Environment Configuration',
16
+ 'typescript': 'Static Type Safety',
17
+ 'turbo': 'Monorepo Pipeline Management',
18
+ 'stripe': 'Payment Processing',
19
+ 'redis': 'Cache & Session Management',
20
+ 'socket.io': 'Real-time Communication',
21
+ 'nest': 'Enterprise-grade Architecture',
22
+ 'fastapi': 'High-performance Python API',
23
+ 'flask': 'Lightweight Python Web Services',
24
+ 'django': 'Full-stack Python Framework',
25
+ 'starlette': 'ASGI Python framework',
26
+ 'uvicorn': 'ASGI server',
27
+ 'gunicorn': 'WSGI HTTP server',
28
+ 'celery': 'Distributed task queue',
29
+ 'sqlalchemy': 'SQL toolkit / ORM',
30
+ 'pydantic': 'Data validation',
31
+ 'httpx': 'HTTP client',
32
+ 'requests': 'HTTP client',
33
+ 'boto3': 'AWS SDK',
34
+ 'pytest': 'Python testing',
35
+ 'ruff': 'Python linter/formatter',
36
+ 'black': 'Python formatter',
37
+ 'gin-gonic/gin': 'Gin HTTP framework',
38
+ 'labstack/echo': 'Echo web framework',
39
+ 'gofiber/fiber': 'Fiber web framework',
40
+ 'go-chi/chi': 'chi router',
41
+ 'gorilla/mux': 'Gorilla mux router',
42
+ 'google.golang.org/grpc': 'gRPC',
43
+ 'grpc-go': 'gRPC Go',
44
+ 'spf13/cobra': 'Cobra CLI',
45
+ 'gorm.io/gorm': 'GORM ORM',
46
+ 'testify': 'Go assertions/mocks',
47
+ };
48
+
49
+ public static detect(dependencies: string[], devDependencies: string[], hasDocker: boolean, hasRoutes: boolean): string[] {
50
+ const features = new Set<string>();
51
+ const allDeps = [...dependencies, ...devDependencies].map(d => d.toLowerCase());
52
+
53
+ for (const [dep, feature] of Object.entries(this.FEATURE_MAP)) {
54
+ if (allDeps.some(d => d.includes(dep))) {
55
+ features.add(feature);
56
+ }
57
+ }
58
+
59
+ if (hasDocker) features.add('Docker Container Environments');
60
+ if (hasRoutes) features.add('API Endpoint Management');
61
+ if (dependencies.length > 0) features.add('Dependency-driven development');
62
+
63
+ return Array.from(features);
64
+ }
65
+ }
@@ -0,0 +1,364 @@
1
+ export interface PackageMetadata {
2
+ name: string;
3
+ version: string;
4
+ description: string;
5
+ scripts: Record<string, string>;
6
+ dependencies: {
7
+ production: string[];
8
+ development: string[];
9
+ peer: string[];
10
+ };
11
+ frameworks: string[];
12
+ packageManager: string;
13
+ }
14
+
15
+ export class PackageParser {
16
+ public static async parse(files: Record<string, string>): Promise<PackageMetadata | null> {
17
+ const packageFiles = Object.keys(files).filter(f => f.endsWith('package.json'));
18
+
19
+ if (packageFiles.length > 0) {
20
+ return this.parseMultipleNode(packageFiles, files);
21
+ }
22
+
23
+ const req = files['requirements.txt'] || '';
24
+ const pyproject = files['pyproject.toml'] || '';
25
+ if (req || pyproject) {
26
+ return this.parsePython(req, pyproject);
27
+ }
28
+ if (files['go.mod']) {
29
+ return this.parseGo(files['go.mod']);
30
+ }
31
+ return null;
32
+ }
33
+
34
+ private static parseMultipleNode(packagePaths: string[], allFiles: Record<string, string>): PackageMetadata {
35
+ const mergedScripts: Record<string, string> = {};
36
+ const mergedDeps = new Set<string>();
37
+ const mergedDevDeps = new Set<string>();
38
+ const mergedPeerDeps = new Set<string>();
39
+ const mergedFrameworks = new Set<string>();
40
+
41
+ let primaryName = '';
42
+ let primaryDescription = '';
43
+ let primaryVersion = '0.0.0';
44
+
45
+ // Sort paths by depth (shallowest first) to pick primary metadata from root
46
+ const sortedPaths = [...packagePaths].sort((a, b) => a.split('/').length - b.split('/').length);
47
+
48
+ sortedPaths.forEach((path, index) => {
49
+ try {
50
+ const pkg = JSON.parse(allFiles[path]);
51
+ // Normalize path for splitting
52
+ const normalizedPath = path.replace(/\\/g, '/');
53
+ const parts = normalizedPath.split('/');
54
+
55
+ // If it's a nested package (e.g., apps/api/package.json), prefix with the directory path
56
+ let prefix = '';
57
+ if (parts.length > 1) {
58
+ const dirParts = parts.slice(0, -1);
59
+ prefix = dirParts.join('/') + ':';
60
+ }
61
+
62
+ // If no primary metadata yet, or if current one is more descriptive, pick it
63
+ if (!primaryName || (index === 0 && pkg.name)) {
64
+ primaryName = pkg.name || primaryName;
65
+ primaryDescription = pkg.description || primaryDescription;
66
+ primaryVersion = pkg.version || primaryVersion;
67
+ }
68
+
69
+ // Merge scripts with prefix if not root
70
+ if (pkg.scripts) {
71
+ Object.entries(pkg.scripts as Record<string, string>).forEach(([name, cmd]) => {
72
+ const key = prefix ? `${prefix}${name}` : name;
73
+ mergedScripts[key] = cmd;
74
+ });
75
+ }
76
+
77
+ // Merge dependencies
78
+ if (pkg.dependencies) {
79
+ Object.keys(pkg.dependencies).forEach(d => {
80
+ mergedDeps.add(d);
81
+ this.detectFramework(d, mergedFrameworks);
82
+ });
83
+ }
84
+ if (pkg.devDependencies) {
85
+ Object.keys(pkg.devDependencies).forEach(d => mergedDevDeps.add(d));
86
+ }
87
+ if (pkg.peerDependencies) {
88
+ Object.keys(pkg.peerDependencies).forEach(d => mergedPeerDeps.add(d));
89
+ }
90
+ } catch (e) {
91
+ console.warn(`Failed to parse ${path}:`, e);
92
+ }
93
+ });
94
+
95
+ let packageManager = 'npm';
96
+ if (Object.keys(allFiles).some(f => f.includes('pnpm-lock.yaml'))) packageManager = 'pnpm';
97
+ else if (Object.keys(allFiles).some(f => f.includes('yarn.lock'))) packageManager = 'yarn';
98
+ else if (Object.keys(allFiles).some(f => f.endsWith('bun.lockb'))) packageManager = 'bun';
99
+
100
+ return {
101
+ name: primaryName || 'Unknown Node Project',
102
+ version: primaryVersion,
103
+ description: primaryDescription,
104
+ scripts: mergedScripts,
105
+ dependencies: {
106
+ production: Array.from(mergedDeps),
107
+ development: Array.from(mergedDevDeps),
108
+ peer: Array.from(mergedPeerDeps),
109
+ },
110
+ frameworks: Array.from(mergedFrameworks),
111
+ packageManager,
112
+ };
113
+ }
114
+
115
+ private static detectFramework(dep: string, frameworks: Set<string>) {
116
+ const frameworkMap: Record<string, string> = {
117
+ 'express': 'Express',
118
+ 'react': 'React',
119
+ 'next': 'Next.js',
120
+ 'vue': 'Vue',
121
+ '@nestjs/core': 'NestJS',
122
+ 'koa': 'Koa',
123
+ 'fastify': 'Fastify',
124
+ 'socket.io': 'Socket.io',
125
+ 'mongoose': 'Mongoose',
126
+ 'prisma': 'Prisma',
127
+ 'tailwindcss': 'TailwindCSS',
128
+ 'vite': 'Vite'
129
+ };
130
+
131
+ if (frameworkMap[dep]) frameworks.add(frameworkMap[dep]);
132
+ }
133
+
134
+ private static parsePython(reqs: string, pyproject: string): PackageMetadata {
135
+ const py = pyproject ? this.parsePyProjectToml(pyproject) : null;
136
+ const reqDeps = this.parseRequirementsTxt(reqs);
137
+ const prod = Array.from(
138
+ new Set([...(py?.deps || []), ...reqDeps]),
139
+ );
140
+ const dev = py?.devDeps || [];
141
+
142
+ const frameworks = this.detectPythonFrameworks(prod, dev, pyproject + reqs);
143
+
144
+ const scripts: Record<string, string> = { ...py?.scripts };
145
+ if (frameworks.includes('Django') && !scripts['manage']) {
146
+ scripts['manage'] = 'python manage.py <command>';
147
+ }
148
+
149
+ const packageManager = this.detectPythonPm(pyproject);
150
+
151
+ return {
152
+ name: py?.name || 'Python Project',
153
+ version: py?.version || '1.0.0',
154
+ description: py?.description || '',
155
+ scripts,
156
+ dependencies: {
157
+ production: prod,
158
+ development: dev,
159
+ peer: [],
160
+ },
161
+ frameworks,
162
+ packageManager,
163
+ };
164
+ }
165
+
166
+ private static detectPythonPm(pyproject: string): string {
167
+ if (!pyproject) return 'pip';
168
+ if (/\[tool\.poetry\]/i.test(pyproject)) return 'poetry';
169
+ if (/\[tool\.uv\]/i.test(pyproject) || /uv\.lock/i.test(pyproject))
170
+ return 'uv';
171
+ if (/\[tool\.pdm\]/i.test(pyproject)) return 'pdm';
172
+ if (/\[tool\.hatch\]/i.test(pyproject)) return 'hatch';
173
+ return 'pip';
174
+ }
175
+
176
+ private static parseRequirementsTxt(content: string): string[] {
177
+ const names: string[] = [];
178
+ for (const line of content.split('\n')) {
179
+ const name = this.parseRequirementLine(line);
180
+ if (name) names.push(name);
181
+ }
182
+ return names;
183
+ }
184
+
185
+ /** pip / requirements.txt line → distribution name */
186
+ private static parseRequirementLine(line: string): string | null {
187
+ const t = line.trim();
188
+ if (!t || t.startsWith('#') || t.startsWith('-')) return null;
189
+ const head = t.split(/[;#]/)[0]!.trim();
190
+ if (!head) return null;
191
+ const noVersion = head.split(/\s*[<>=~!]/)[0]!.trim();
192
+ const noExtra = noVersion.split(/[\[]/)[0]!.trim();
193
+ const name = noExtra.split(/[@\s]/)[0]!.trim();
194
+ if (!name || !/^[a-zA-Z0-9._-]+$/.test(name)) return null;
195
+ return name;
196
+ }
197
+
198
+ private static parsePyProjectToml(content: string): {
199
+ name?: string;
200
+ version?: string;
201
+ description?: string;
202
+ deps: string[];
203
+ devDeps: string[];
204
+ scripts: Record<string, string>;
205
+ } {
206
+ const deps: string[] = [];
207
+ const devDeps: string[] = [];
208
+ const scripts: Record<string, string> = {};
209
+ let name: string | undefined;
210
+ let version: string | undefined;
211
+ let description: string | undefined;
212
+
213
+ let section = '';
214
+ const lines = content.split('\n');
215
+ for (const raw of lines) {
216
+ const line = raw.split('#')[0]!.trim();
217
+ const sec = line.match(/^\[([^\]]+)\]/);
218
+ if (sec) {
219
+ section = sec[1]!.toLowerCase();
220
+ continue;
221
+ }
222
+ if (section === 'project') {
223
+ const nm = line.match(/^name\s*=\s*["']([^"']+)["']/i);
224
+ const ver = line.match(/^version\s*=\s*["']([^"']+)["']/i);
225
+ const desc = line.match(/^description\s*=\s*["']([^"']*)["']/i);
226
+ if (nm) name = nm[1];
227
+ if (ver) version = ver[1];
228
+ if (desc) description = desc[1];
229
+ }
230
+ if (
231
+ section === 'tool.poetry.dependencies' ||
232
+ section === 'tool.poetry.group.dev.dependencies'
233
+ ) {
234
+ const depKey = line.match(/^([a-zA-Z0-9_.-]+)\s*=/);
235
+ if (depKey && depKey[1]!.toLowerCase() !== 'python') {
236
+ if (section.includes('dev')) devDeps.push(depKey[1]!);
237
+ else deps.push(depKey[1]!);
238
+ }
239
+ }
240
+ if (section === 'tool.poetry.scripts') {
241
+ const sc = line.match(/^([a-zA-Z0-9_.-]+)\s*=\s*["']([^"']+)["']/);
242
+ if (sc) scripts[sc[1]!] = sc[2]!;
243
+ }
244
+ }
245
+
246
+ return { name, version, description, deps, devDeps, scripts };
247
+ }
248
+
249
+ private static detectPythonFrameworks(
250
+ prod: string[],
251
+ dev: string[],
252
+ raw: string,
253
+ ): string[] {
254
+ const all = [...prod, ...dev].map((d) => d.toLowerCase());
255
+ const text = raw.toLowerCase();
256
+ const frameworks: string[] = [];
257
+ const add = (f: string) => {
258
+ if (!frameworks.includes(f)) frameworks.push(f);
259
+ };
260
+ const has = (s: string) =>
261
+ all.some((d) => d.includes(s)) || text.includes(s);
262
+ if (has('fastapi')) add('FastAPI');
263
+ if (has('flask')) add('Flask');
264
+ if (has('django')) add('Django');
265
+ if (has('starlette')) add('Starlette');
266
+ if (has('tornado')) add('Tornado');
267
+ if (has('sanic')) add('Sanic');
268
+ if (has('litestar')) add('Litestar');
269
+ if (has('sqlalchemy')) add('SQLAlchemy');
270
+ if (has('pydantic')) add('Pydantic');
271
+ if (has('celery')) add('Celery');
272
+ if (has('pytest')) add('pytest');
273
+ return frameworks;
274
+ }
275
+
276
+ private static parseGo(content: string): PackageMetadata {
277
+ const lines = content.split('\n');
278
+ const moduleLine = lines.find((l) => l.trim().startsWith('module '));
279
+ const name = moduleLine
280
+ ? moduleLine.replace(/^\s*module\s+/, '').trim().split(/\s+/)[0]!
281
+ : 'Go Project';
282
+
283
+ const modules = this.parseGoRequireBlock(content);
284
+ const frameworks = this.detectGoFrameworks(modules);
285
+
286
+ const scripts: Record<string, string> = {
287
+ build: 'go build ./...',
288
+ test: 'go test ./...',
289
+ vet: 'go vet ./...',
290
+ mod: 'go mod tidy',
291
+ };
292
+
293
+ return {
294
+ name,
295
+ version: '1.0.0',
296
+ description: '',
297
+ scripts,
298
+ dependencies: {
299
+ production: modules,
300
+ development: [],
301
+ peer: [],
302
+ },
303
+ frameworks,
304
+ packageManager: 'go mod',
305
+ };
306
+ }
307
+
308
+ private static parseGoRequireBlock(content: string): string[] {
309
+ const mods: string[] = [];
310
+ const lines = content.split('\n');
311
+ let inBlock = false;
312
+ for (const raw of lines) {
313
+ const line = raw.trim();
314
+ if (line.startsWith('require (')) {
315
+ inBlock = true;
316
+ continue;
317
+ }
318
+ if (inBlock && line === ')') {
319
+ inBlock = false;
320
+ continue;
321
+ }
322
+ if (inBlock && line && !line.startsWith('//') && !line.startsWith('replace ')) {
323
+ const modPath = line.split(/\s+/)[0]!;
324
+ if (modPath) mods.push(modPath);
325
+ continue;
326
+ }
327
+ if (line.startsWith('require ') && !line.includes('(')) {
328
+ const rest = line.replace(/^require\s+/, '').trim();
329
+ const modPath = rest.split(/\s+/)[0]!;
330
+ if (modPath && !modPath.startsWith('//')) mods.push(modPath);
331
+ }
332
+ }
333
+ return Array.from(new Set(mods));
334
+ }
335
+
336
+ private static detectGoFrameworks(modules: string[]): string[] {
337
+ const frameworks: string[] = [];
338
+ const add = (f: string) => {
339
+ if (!frameworks.includes(f)) frameworks.push(f);
340
+ };
341
+ const s = modules.join(' ').toLowerCase();
342
+ const pairs: [string, string][] = [
343
+ ['gin-gonic/gin', 'Gin'],
344
+ ['labstack/echo', 'Echo'],
345
+ ['gofiber/fiber', 'Fiber'],
346
+ ['go-chi/chi', 'chi'],
347
+ ['gorilla/mux', 'Gorilla mux'],
348
+ ['google.golang.org/grpc', 'gRPC'],
349
+ ['grpc-go', 'gRPC'],
350
+ ['grpc/grpc-go', 'gRPC'],
351
+ ['spf13/cobra', 'Cobra'],
352
+ ['spf13/viper', 'Viper'],
353
+ ['stretchr/testify', 'testify'],
354
+ ['jackc/pgx', 'pgx'],
355
+ ['go-redis/redis', 'Redis'],
356
+ ['gorm.io/gorm', 'GORM'],
357
+ ['entgo.io/ent', 'Ent'],
358
+ ];
359
+ for (const [needle, label] of pairs) {
360
+ if (s.includes(needle)) add(label);
361
+ }
362
+ return frameworks;
363
+ }
364
+ }
@@ -0,0 +1,169 @@
1
+ /** Same shape as RouteExtractor Route */
2
+ export interface PolyglotRoute {
3
+ method: string;
4
+ path: string;
5
+ file: string;
6
+ snippet?: string;
7
+ }
8
+
9
+ /** Routes & API surface for Python (FastAPI, Flask, Starlette-style) and Go (net/http, gin, echo, chi, mux, fiber). */
10
+ export class PolyglotExtractors {
11
+ public static extractRoutes(files: Record<string, string>): PolyglotRoute[] {
12
+ const routes: PolyglotRoute[] = [];
13
+ for (const [filePath, content] of Object.entries(files)) {
14
+ if (filePath.endsWith(".py")) {
15
+ routes.push(...this.pythonRoutes(filePath, content));
16
+ } else if (filePath.endsWith(".go")) {
17
+ routes.push(...this.goRoutes(filePath, content));
18
+ }
19
+ }
20
+ return routes;
21
+ }
22
+
23
+ public static extractDefinitions(
24
+ files: Record<string, string>,
25
+ ): Record<string, string[]> {
26
+ const result: Record<string, string[]> = {};
27
+ for (const [filePath, content] of Object.entries(files)) {
28
+ if (filePath.endsWith(".py")) {
29
+ const defs = this.pythonDefinitions(content);
30
+ if (defs.length) result[filePath] = defs;
31
+ } else if (filePath.endsWith(".go")) {
32
+ const defs = this.goDefinitions(content);
33
+ if (defs.length) result[filePath] = defs;
34
+ }
35
+ }
36
+ return result;
37
+ }
38
+
39
+ private static pythonRoutes(file: string, content: string): PolyglotRoute[] {
40
+ const out: PolyglotRoute[] = [];
41
+ const lines = content.split("\n");
42
+
43
+ // FastAPI / Starlette: @app.get("/path") @router.post( APIRouter
44
+ const decRe =
45
+ /@(?:app|router|api|route)\.(get|post|put|delete|patch|options|head)\s*\(\s*["'`]([^"'`]+)["'`]/gi;
46
+ let m: RegExpExecArray | null;
47
+ while ((m = decRe.exec(content)) !== null) {
48
+ out.push({
49
+ method: m[1]!.toUpperCase(),
50
+ path: m[2]!,
51
+ file,
52
+ snippet: this.snip(lines, m.index),
53
+ });
54
+ }
55
+
56
+ // Flask: @app.route("/x", methods=['GET','POST'])
57
+ const flaskRe = /@\w+\.route\s*\(\s*["'`]([^"'`]+)["'`]/gi;
58
+ while ((m = flaskRe.exec(content)) !== null) {
59
+ const methods = this.flaskMethodsNear(content, m.index);
60
+ for (const method of methods) {
61
+ out.push({
62
+ method,
63
+ path: m[1]!,
64
+ file,
65
+ snippet: this.snip(lines, m.index),
66
+ });
67
+ }
68
+ }
69
+
70
+ // Django path() re_path()
71
+ const djangoPath =
72
+ /(?:path|re_path)\s*\(\s*["']([^"']+)["']\s*,/g;
73
+ while ((m = djangoPath.exec(content)) !== null) {
74
+ out.push({
75
+ method: "GET",
76
+ path: m[1]!,
77
+ file,
78
+ snippet: this.snip(lines, m.index),
79
+ });
80
+ }
81
+
82
+ return out.slice(0, 200);
83
+ }
84
+
85
+ private static flaskMethodsNear(content: string, idx: number): string[] {
86
+ const slice = content.slice(idx, idx + 400);
87
+ const mm = slice.match(/methods\s*=\s*\[([^\]]+)\]/i);
88
+ if (!mm) return ["GET"];
89
+ const raw = mm[1]!;
90
+ const methods: string[] = [];
91
+ const part = raw.matchAll(/['"](GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD)['"]/gi);
92
+ for (const x of part) methods.push(x[1]!.toUpperCase());
93
+ return methods.length ? methods : ["GET"];
94
+ }
95
+
96
+ private static goRoutes(file: string, content: string): PolyglotRoute[] {
97
+ const out: PolyglotRoute[] = [];
98
+ const lines = content.split("\n");
99
+
100
+ // gin/echo/fiber/chi style: .Get("/path" .POST("/path"
101
+ const methodPath =
102
+ /\.(Get|Post|Put|Delete|Patch|Options|Head|Connect|Trace)\s*\(\s*["']([^"']+)["']/gi;
103
+ let m: RegExpExecArray | null;
104
+ while ((m = methodPath.exec(content)) !== null) {
105
+ out.push({
106
+ method: m[1]!.toUpperCase(),
107
+ path: m[2]!,
108
+ file,
109
+ snippet: this.snip(lines, m.index),
110
+ });
111
+ }
112
+
113
+ // stdlib
114
+ const handleFunc = /HandleFunc\s*\(\s*["']([^"']+)["']/gi;
115
+ while ((m = handleFunc.exec(content)) !== null) {
116
+ out.push({
117
+ method: "GET",
118
+ path: m[1]!,
119
+ file,
120
+ snippet: this.snip(lines, m.index),
121
+ });
122
+ }
123
+
124
+ return out.slice(0, 200);
125
+ }
126
+
127
+ private static pythonDefinitions(content: string): string[] {
128
+ const defs: string[] = [];
129
+ const lines = content.split("\n");
130
+ for (const line of lines) {
131
+ const t = line.trim();
132
+ if (/^class\s+[A-Za-z_][\w]*/.test(t)) {
133
+ defs.push(`Class: ${t.slice(0, 200)}`);
134
+ continue;
135
+ }
136
+ if (/^(async\s+)?def\s+[A-Za-z_][\w]*\s*\(/.test(t)) {
137
+ defs.push(`Function: ${t.slice(0, 200)}`);
138
+ }
139
+ }
140
+ return defs.slice(0, 80);
141
+ }
142
+
143
+ private static goDefinitions(content: string): string[] {
144
+ const defs: string[] = [];
145
+ const funcRe = /^func\s+.+$/gm;
146
+ let m: RegExpExecArray | null;
147
+ while ((m = funcRe.exec(content)) !== null) {
148
+ const line = m[0]!.trim();
149
+ if (line.length > 8) defs.push(`Func: ${line.slice(0, 220)}`);
150
+ }
151
+ const typeRe = /^type\s+\w+\s+(struct|interface)\b.*/gm;
152
+ while ((m = typeRe.exec(content)) !== null) {
153
+ defs.push(`Type: ${m[0]!.trim().slice(0, 200)}`);
154
+ }
155
+ return defs.slice(0, 80);
156
+ }
157
+
158
+ private static snip(lines: string[], charIndex: number): string | undefined {
159
+ let pos = 0;
160
+ for (let i = 0; i < lines.length; i++) {
161
+ const next = pos + lines[i]!.length + 1;
162
+ if (charIndex < next) {
163
+ return lines[i]!.trim().slice(0, 200);
164
+ }
165
+ pos = next;
166
+ }
167
+ return undefined;
168
+ }
169
+ }