@dependabit/detector 0.1.16 → 0.1.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.
- package/CHANGELOG.md +10 -0
- package/dist/detector.d.ts +179 -12
- package/dist/detector.d.ts.map +1 -1
- package/dist/detector.js +121 -12
- package/dist/detector.js.map +1 -1
- package/dist/diff-parser.js.map +1 -1
- package/dist/llm/client.d.ts +111 -2
- package/dist/llm/client.d.ts.map +1 -1
- package/dist/llm/client.js +33 -1
- package/dist/llm/client.js.map +1 -1
- package/dist/llm/copilot.d.ts.map +1 -1
- package/dist/llm/copilot.js +1 -7
- package/dist/llm/copilot.js.map +1 -1
- package/dist/llm/prompts.d.ts +42 -2
- package/dist/llm/prompts.d.ts.map +1 -1
- package/dist/llm/prompts.js +42 -2
- package/dist/llm/prompts.js.map +1 -1
- package/dist/parsers/code-comments.js.map +1 -1
- package/dist/parsers/package-files.js.map +1 -1
- package/dist/parsers/readme.js.map +1 -1
- package/package.json +26 -9
- package/src/detector.ts +0 -1043
- package/src/diff-parser.ts +0 -257
- package/src/index.ts +0 -43
- package/src/llm/client.ts +0 -85
- package/src/llm/copilot.ts +0 -150
- package/src/llm/prompts.ts +0 -111
- package/src/parsers/code-comments.ts +0 -178
- package/src/parsers/package-files.ts +0 -156
- package/src/parsers/readme.ts +0 -191
- package/test/detector.test.ts +0 -169
- package/test/diff-parser.test.ts +0 -187
- package/test/llm/client.test.ts +0 -31
- package/test/llm/copilot.test.ts +0 -334
- package/test/parsers/code-comments.test.ts +0 -98
- package/test/parsers/package-files.test.ts +0 -52
- package/test/parsers/readme.test.ts +0 -52
- package/tsconfig.json +0 -10
- package/tsconfig.tsbuildinfo +0 -1
package/src/detector.ts
DELETED
|
@@ -1,1043 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Detector Orchestrator
|
|
3
|
-
* Coordinates content parsers and LLM analysis to detect external dependencies
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { readdir, readFile } from 'node:fs/promises';
|
|
7
|
-
import { execSync } from 'node:child_process';
|
|
8
|
-
import { homedir } from 'node:os';
|
|
9
|
-
import { basename, dirname, join, relative, resolve, normalize, sep } from 'node:path';
|
|
10
|
-
import { randomUUID } from 'node:crypto';
|
|
11
|
-
import ignore, { type Ignore } from 'ignore';
|
|
12
|
-
import type { LLMProvider } from './llm/client.js';
|
|
13
|
-
import { createClassificationPrompt } from './llm/prompts.js';
|
|
14
|
-
import { parseReadme } from './parsers/readme.js';
|
|
15
|
-
import { parseCodeComments } from './parsers/code-comments.js';
|
|
16
|
-
import {
|
|
17
|
-
parsePackageJson,
|
|
18
|
-
parseRequirementsTxt,
|
|
19
|
-
parseCargoToml,
|
|
20
|
-
parseGoMod
|
|
21
|
-
} from './parsers/package-files.js';
|
|
22
|
-
import type {
|
|
23
|
-
DependencyEntry,
|
|
24
|
-
DependencyType,
|
|
25
|
-
AccessMethod,
|
|
26
|
-
DetectionMethod
|
|
27
|
-
} from '@dependabit/manifest';
|
|
28
|
-
|
|
29
|
-
export interface DetectorOptions {
|
|
30
|
-
repoPath: string;
|
|
31
|
-
llmProvider: LLMProvider;
|
|
32
|
-
ignorePatterns?: string[];
|
|
33
|
-
useGitExcludes?: boolean;
|
|
34
|
-
repoOwner?: string;
|
|
35
|
-
repoName?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface DetectionResult {
|
|
39
|
-
dependencies: DependencyEntry[];
|
|
40
|
-
statistics: {
|
|
41
|
-
filesScanned: number;
|
|
42
|
-
urlsFound: number;
|
|
43
|
-
llmCalls: number;
|
|
44
|
-
totalTokens: number;
|
|
45
|
-
totalLatencyMs: number;
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const DEFAULT_IGNORE_PATTERNS = [
|
|
50
|
-
'node_modules',
|
|
51
|
-
'dist',
|
|
52
|
-
'build',
|
|
53
|
-
'target',
|
|
54
|
-
'vendor',
|
|
55
|
-
'venv',
|
|
56
|
-
'__pycache__',
|
|
57
|
-
'coverage'
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
const ALLOWED_DOT_DIRECTORIES = new Set<string>();
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Main detector class
|
|
64
|
-
*/
|
|
65
|
-
export class Detector {
|
|
66
|
-
private options: Required<DetectorOptions>;
|
|
67
|
-
private ignoreMatcher: Ignore | null = null;
|
|
68
|
-
private ignoreMatcherLoaded = false;
|
|
69
|
-
private skipUrlPatterns: RegExp[];
|
|
70
|
-
|
|
71
|
-
constructor(options: DetectorOptions) {
|
|
72
|
-
this.options = {
|
|
73
|
-
...options,
|
|
74
|
-
ignorePatterns: options.ignorePatterns || DEFAULT_IGNORE_PATTERNS,
|
|
75
|
-
useGitExcludes: options.useGitExcludes ?? true,
|
|
76
|
-
repoOwner: options.repoOwner || '',
|
|
77
|
-
repoName: options.repoName || ''
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
this.skipUrlPatterns = [
|
|
81
|
-
/example\.com/,
|
|
82
|
-
/example\.org/,
|
|
83
|
-
/localhost/,
|
|
84
|
-
/127\.0\.0\.1/,
|
|
85
|
-
/\[.*\]/, // template placeholders like [NUMBER]
|
|
86
|
-
/github\.com\/user\/repo/, // common placeholder in docs
|
|
87
|
-
/your-?username/i, // template placeholder: your-username or yourusername
|
|
88
|
-
/you\/your-project/i, // template: you/your-project
|
|
89
|
-
/github\.com\/YOUR-?USERNAME/i, // GitHub template: YOUR-USERNAME
|
|
90
|
-
/github\.com\/your-?username/i, // GitHub template: your-username
|
|
91
|
-
// Self-reference: skip if URL matches current repository
|
|
92
|
-
...(this.options.repoOwner && this.options.repoName
|
|
93
|
-
? [
|
|
94
|
-
new RegExp(
|
|
95
|
-
`github\\.com\\/${this.options.repoOwner}\\/${this.options.repoName}(?:\\.git)?(?:[/?#]|$)`,
|
|
96
|
-
'i'
|
|
97
|
-
)
|
|
98
|
-
]
|
|
99
|
-
: [])
|
|
100
|
-
];
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Detect all external dependencies in the repository
|
|
105
|
-
*
|
|
106
|
-
* Implementation follows a hybrid approach:
|
|
107
|
-
* 1. Programmatic parsing of repository files (README, code comments, package files)
|
|
108
|
-
* 2. LLM analysis only for documents not fully parsed in step 1 (future enhancement)
|
|
109
|
-
* 3. Programmatic type categorization based on URL patterns and context
|
|
110
|
-
* 4. LLM fallback for uncategorized dependencies
|
|
111
|
-
* 5. Programmatic access method determination based on URL patterns
|
|
112
|
-
* 6. LLM fallback for access methods that can't be determined (future enhancement)
|
|
113
|
-
* 7. Manifest entry creation with references and versioning
|
|
114
|
-
*/
|
|
115
|
-
async detectDependencies(): Promise<DetectionResult> {
|
|
116
|
-
const allReferences: Map<
|
|
117
|
-
string,
|
|
118
|
-
{
|
|
119
|
-
url: string;
|
|
120
|
-
contexts: Array<{ file: string; line?: number; text: string }>;
|
|
121
|
-
detectionMethod: DetectionMethod;
|
|
122
|
-
}
|
|
123
|
-
> = new Map();
|
|
124
|
-
|
|
125
|
-
let filesScanned = 0;
|
|
126
|
-
let llmCalls = 0;
|
|
127
|
-
let totalTokens = 0;
|
|
128
|
-
let totalLatencyMs = 0;
|
|
129
|
-
|
|
130
|
-
// Step 1: Parse repository for dependencies (programmatic)
|
|
131
|
-
// 1a. Parse README files
|
|
132
|
-
const readmeFiles = await this.findFiles(this.options.repoPath, /^README/i);
|
|
133
|
-
for (const file of readmeFiles) {
|
|
134
|
-
const content = await readFile(file, 'utf-8');
|
|
135
|
-
const references = parseReadme(content, relative(this.options.repoPath, file));
|
|
136
|
-
|
|
137
|
-
for (const ref of references) {
|
|
138
|
-
this.addReference(
|
|
139
|
-
allReferences,
|
|
140
|
-
ref.url,
|
|
141
|
-
{
|
|
142
|
-
file: relative(this.options.repoPath, file),
|
|
143
|
-
...(ref.line !== undefined && { line: ref.line }),
|
|
144
|
-
text: ref.context
|
|
145
|
-
},
|
|
146
|
-
'llm-analysis'
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
filesScanned++;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// 1b. Parse non-README documentation files
|
|
154
|
-
const documentationFiles = await this.findFiles(this.options.repoPath, /\.(md|txt|rst|adoc)$/i);
|
|
155
|
-
for (const file of documentationFiles) {
|
|
156
|
-
if (/^README/i.test(basename(file))) {
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const content = await readFile(file, 'utf-8');
|
|
161
|
-
const references = parseReadme(content, relative(this.options.repoPath, file));
|
|
162
|
-
|
|
163
|
-
for (const ref of references) {
|
|
164
|
-
this.addReference(
|
|
165
|
-
allReferences,
|
|
166
|
-
ref.url,
|
|
167
|
-
{
|
|
168
|
-
file: relative(this.options.repoPath, file),
|
|
169
|
-
...(ref.line !== undefined && { line: ref.line }),
|
|
170
|
-
text: ref.context
|
|
171
|
-
},
|
|
172
|
-
'llm-analysis'
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
filesScanned++;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// 1c. Parse package files for metadata (NOT dependencies)
|
|
180
|
-
const packageFiles = await this.findPackageFiles(this.options.repoPath);
|
|
181
|
-
for (const file of packageFiles) {
|
|
182
|
-
const content = await readFile(file, 'utf-8');
|
|
183
|
-
const metadata = this.parsePackageFile(file, content);
|
|
184
|
-
|
|
185
|
-
for (const url of [
|
|
186
|
-
...(metadata.urls || []),
|
|
187
|
-
metadata.repository,
|
|
188
|
-
metadata.homepage,
|
|
189
|
-
metadata.documentation
|
|
190
|
-
].filter(Boolean)) {
|
|
191
|
-
this.addReference(
|
|
192
|
-
allReferences,
|
|
193
|
-
url!,
|
|
194
|
-
{
|
|
195
|
-
file: relative(this.options.repoPath, file),
|
|
196
|
-
text: 'Package metadata'
|
|
197
|
-
},
|
|
198
|
-
'package-json'
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
filesScanned++;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// 1d. Parse code comments from source files
|
|
206
|
-
const sourceFiles = await this.findSourceFiles(this.options.repoPath);
|
|
207
|
-
for (const file of sourceFiles.slice(0, 50)) {
|
|
208
|
-
// Limit to 50 files for performance
|
|
209
|
-
const content = await readFile(file, 'utf-8');
|
|
210
|
-
const references = parseCodeComments(content, relative(this.options.repoPath, file));
|
|
211
|
-
|
|
212
|
-
for (const ref of references) {
|
|
213
|
-
this.addReference(
|
|
214
|
-
allReferences,
|
|
215
|
-
ref.url,
|
|
216
|
-
{
|
|
217
|
-
file: ref.file,
|
|
218
|
-
line: ref.line,
|
|
219
|
-
text: ref.context
|
|
220
|
-
},
|
|
221
|
-
'code-comment'
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
filesScanned++;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Step 2: LLM 2nd pass for documents not fully parsed in step 1
|
|
229
|
-
// Analyze README files for dependency context that parsers might have missed
|
|
230
|
-
const llmEnhancedReferences = new Set<string>();
|
|
231
|
-
|
|
232
|
-
for (const file of readmeFiles.slice(0, 5)) {
|
|
233
|
-
// Limit to 5 READMEs for LLM analysis
|
|
234
|
-
try {
|
|
235
|
-
const content = await readFile(file, 'utf-8');
|
|
236
|
-
const relPath = relative(this.options.repoPath, file);
|
|
237
|
-
|
|
238
|
-
// Use LLM to extract additional context from README
|
|
239
|
-
const detectionPrompt = `Analyze this README file and identify any external dependencies or resources that might be referenced but not explicitly linked:
|
|
240
|
-
|
|
241
|
-
File: ${relPath}
|
|
242
|
-
Content:
|
|
243
|
-
${content.slice(0, 5000)}
|
|
244
|
-
|
|
245
|
-
Identify:
|
|
246
|
-
1. Documentation sites mentioned but not linked
|
|
247
|
-
2. Tools or libraries referenced in text
|
|
248
|
-
3. API services mentioned
|
|
249
|
-
4. Research papers or specifications cited
|
|
250
|
-
|
|
251
|
-
Return as JSON with "dependencies" array.`;
|
|
252
|
-
|
|
253
|
-
const response = await this.options.llmProvider.analyze(content, detectionPrompt);
|
|
254
|
-
llmCalls++;
|
|
255
|
-
totalTokens += response.usage.totalTokens;
|
|
256
|
-
totalLatencyMs += response.usage.latencyMs;
|
|
257
|
-
|
|
258
|
-
// Add LLM-discovered references
|
|
259
|
-
for (const dep of response.dependencies) {
|
|
260
|
-
if (dep.url && !allReferences.has(dep.url)) {
|
|
261
|
-
llmEnhancedReferences.add(dep.url);
|
|
262
|
-
this.addReference(
|
|
263
|
-
allReferences,
|
|
264
|
-
dep.url,
|
|
265
|
-
{
|
|
266
|
-
file: relPath,
|
|
267
|
-
text: dep.description || 'Discovered by LLM analysis'
|
|
268
|
-
},
|
|
269
|
-
'llm-analysis'
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
} catch (error) {
|
|
274
|
-
console.error(`LLM document analysis failed for ${file}:`, error);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Steps 3-6: Categorize dependencies (programmatic first, LLM fallback)
|
|
279
|
-
const dependencies: DependencyEntry[] = [];
|
|
280
|
-
const now = new Date().toISOString();
|
|
281
|
-
|
|
282
|
-
for (const [url, data] of allReferences) {
|
|
283
|
-
// Skip entries that will end up with low confidence
|
|
284
|
-
// (entries that can't be typed programmatically and LLM assigns low confidence)
|
|
285
|
-
// Prepare context for potential LLM use
|
|
286
|
-
const context = data.contexts
|
|
287
|
-
.map((c) => `${c.file}${c.line ? `:${c.line}` : ''}: ${c.text}`)
|
|
288
|
-
.join('\n');
|
|
289
|
-
const firstContext = data.contexts[0]?.text || '';
|
|
290
|
-
|
|
291
|
-
// Step 3: Try programmatic type categorization
|
|
292
|
-
let type: DependencyType | null = this.determineDependencyType(url, firstContext);
|
|
293
|
-
let typeConfidence = type ? 0.9 : 0.5; // High confidence for programmatic
|
|
294
|
-
|
|
295
|
-
// Step 4: If type couldn't be determined, use LLM fallback
|
|
296
|
-
if (!type) {
|
|
297
|
-
try {
|
|
298
|
-
const classificationPrompt = createClassificationPrompt(url, context);
|
|
299
|
-
const response = await this.options.llmProvider.analyze('', classificationPrompt);
|
|
300
|
-
llmCalls++;
|
|
301
|
-
totalTokens += response.usage.totalTokens;
|
|
302
|
-
totalLatencyMs += response.usage.latencyMs;
|
|
303
|
-
|
|
304
|
-
if (response.dependencies.length > 0) {
|
|
305
|
-
const dep = response.dependencies[0];
|
|
306
|
-
if (dep) {
|
|
307
|
-
type = dep.type as DependencyType;
|
|
308
|
-
typeConfidence = dep.confidence;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
} catch (error) {
|
|
312
|
-
console.error(`LLM classification failed for ${url}:`, error);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Default to 'other' if still not determined
|
|
317
|
-
if (!type) {
|
|
318
|
-
type = 'other';
|
|
319
|
-
typeConfidence = 0.3;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Skip low-confidence entries
|
|
323
|
-
if (typeConfidence < 0.5) {
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Step 5: Try programmatic access method determination
|
|
328
|
-
let accessMethod: AccessMethod | null = this.determineAccessMethod(url);
|
|
329
|
-
|
|
330
|
-
// Step 6: If access method couldn't be determined, use LLM fallback
|
|
331
|
-
if (!accessMethod) {
|
|
332
|
-
try {
|
|
333
|
-
const accessMethodPrompt = `Determine the best access method for this URL: ${url}
|
|
334
|
-
|
|
335
|
-
Context: ${firstContext}
|
|
336
|
-
|
|
337
|
-
Choose ONE of these access methods:
|
|
338
|
-
- "github-api": For GitHub repositories
|
|
339
|
-
- "arxiv": For arXiv papers
|
|
340
|
-
- "openapi": For API specifications
|
|
341
|
-
- "context7": For Context7 documentation
|
|
342
|
-
- "http": For general web resources
|
|
343
|
-
|
|
344
|
-
Return as JSON: {"accessMethod": "...", "confidence": 0.0-1.0}`;
|
|
345
|
-
|
|
346
|
-
const response = await this.options.llmProvider.analyze('', accessMethodPrompt);
|
|
347
|
-
llmCalls++;
|
|
348
|
-
totalTokens += response.usage.totalTokens;
|
|
349
|
-
totalLatencyMs += response.usage.latencyMs;
|
|
350
|
-
|
|
351
|
-
// Parse LLM response for access method
|
|
352
|
-
const content = response.rawResponse || '{}';
|
|
353
|
-
try {
|
|
354
|
-
const parsed = JSON.parse(content);
|
|
355
|
-
if (parsed.accessMethod) {
|
|
356
|
-
accessMethod = parsed.accessMethod as AccessMethod;
|
|
357
|
-
}
|
|
358
|
-
} catch {
|
|
359
|
-
// If parsing fails, fall back to http
|
|
360
|
-
accessMethod = 'http';
|
|
361
|
-
}
|
|
362
|
-
} catch (error) {
|
|
363
|
-
console.error(`LLM access method determination failed for ${url}:`, error);
|
|
364
|
-
accessMethod = 'http';
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Ensure we have a valid access method
|
|
369
|
-
if (!accessMethod) {
|
|
370
|
-
accessMethod = 'http';
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Step 7: Create manifest entry with references and versioning
|
|
374
|
-
const entry: DependencyEntry = {
|
|
375
|
-
id: randomUUID(),
|
|
376
|
-
url,
|
|
377
|
-
type,
|
|
378
|
-
accessMethod,
|
|
379
|
-
name: this.extractName(url),
|
|
380
|
-
description: firstContext,
|
|
381
|
-
currentVersion: undefined,
|
|
382
|
-
currentStateHash: '', // Will be populated by monitor
|
|
383
|
-
detectionMethod: data.detectionMethod,
|
|
384
|
-
detectionConfidence: typeConfidence,
|
|
385
|
-
detectedAt: now,
|
|
386
|
-
lastChecked: now,
|
|
387
|
-
auth: undefined,
|
|
388
|
-
monitoring: {
|
|
389
|
-
enabled: true,
|
|
390
|
-
checkFrequency: 'daily',
|
|
391
|
-
ignoreChanges: false
|
|
392
|
-
},
|
|
393
|
-
referencedIn: data.contexts.map((c) => ({
|
|
394
|
-
file: c.file,
|
|
395
|
-
line: c.line,
|
|
396
|
-
context: c.text
|
|
397
|
-
})),
|
|
398
|
-
changeHistory: []
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
dependencies.push(entry);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
return {
|
|
405
|
-
dependencies,
|
|
406
|
-
statistics: {
|
|
407
|
-
filesScanned,
|
|
408
|
-
urlsFound: allReferences.size,
|
|
409
|
-
llmCalls,
|
|
410
|
-
totalTokens,
|
|
411
|
-
totalLatencyMs
|
|
412
|
-
}
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
private addReference(
|
|
417
|
-
map: Map<string, any>,
|
|
418
|
-
url: string,
|
|
419
|
-
context: { file: string; line?: number; text: string },
|
|
420
|
-
detectionMethod: DetectionMethod
|
|
421
|
-
): void {
|
|
422
|
-
// Skip URLs that don't start with http:// or https://
|
|
423
|
-
if (!/^https?:\/\//.test(url)) {
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Skip URLs matching skip patterns (placeholders, localhost, etc.)
|
|
428
|
-
if (this.skipUrlPatterns.some((pattern) => pattern.test(url))) {
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Skip self-references (URLs pointing to the repo itself)
|
|
433
|
-
if (this.options.repoOwner && this.options.repoName) {
|
|
434
|
-
const selfPattern = new RegExp(
|
|
435
|
-
`github\\.com[/:]${this.escapeRegExp(this.options.repoOwner)}/${this.escapeRegExp(this.options.repoName)}(?:/|$|#|\\?)`,
|
|
436
|
-
'i'
|
|
437
|
-
);
|
|
438
|
-
if (selfPattern.test(url)) {
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
if (!map.has(url)) {
|
|
444
|
-
map.set(url, {
|
|
445
|
-
url,
|
|
446
|
-
contexts: [],
|
|
447
|
-
detectionMethod
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
map.get(url)!.contexts.push(context);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
private escapeRegExp(str: string): string {
|
|
454
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Programmatically determine access method based on URL patterns
|
|
459
|
-
* Returns null if cannot be determined programmatically
|
|
460
|
-
*/
|
|
461
|
-
private determineAccessMethod(url: string): AccessMethod | null {
|
|
462
|
-
// GitHub URLs
|
|
463
|
-
if (url.includes('github.com')) return 'github-api';
|
|
464
|
-
|
|
465
|
-
// arXiv papers
|
|
466
|
-
if (url.includes('arxiv.org')) return 'arxiv';
|
|
467
|
-
|
|
468
|
-
// OpenAPI/Swagger specs
|
|
469
|
-
if (
|
|
470
|
-
url.includes('openapi') ||
|
|
471
|
-
url.includes('swagger') ||
|
|
472
|
-
url.endsWith('.yaml') ||
|
|
473
|
-
url.endsWith('.json') ||
|
|
474
|
-
url.includes('/api/spec') ||
|
|
475
|
-
url.includes('/api-docs')
|
|
476
|
-
) {
|
|
477
|
-
return 'openapi';
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Context7 documentation
|
|
481
|
-
if (url.includes('context7')) return 'context7';
|
|
482
|
-
|
|
483
|
-
// Cannot determine programmatically - needs LLM
|
|
484
|
-
return null;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* Programmatically determine dependency type based on URL patterns and context
|
|
489
|
-
* Returns null if cannot be determined programmatically
|
|
490
|
-
*/
|
|
491
|
-
private determineDependencyType(url: string, context: string): DependencyType | null {
|
|
492
|
-
const lowerUrl = url.toLowerCase();
|
|
493
|
-
const lowerContext = context.toLowerCase();
|
|
494
|
-
|
|
495
|
-
const isGitHubRepositoryUrl = /^https?:\/\/github\.com\/[^/]+\/[^/#?]+(?:$|[/?#])/.test(
|
|
496
|
-
lowerUrl
|
|
497
|
-
);
|
|
498
|
-
|
|
499
|
-
// Research papers
|
|
500
|
-
if (
|
|
501
|
-
lowerUrl.includes('arxiv.org') ||
|
|
502
|
-
lowerContext.includes('paper') ||
|
|
503
|
-
lowerContext.includes('research')
|
|
504
|
-
) {
|
|
505
|
-
return 'research-paper';
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Schemas
|
|
509
|
-
if (
|
|
510
|
-
lowerUrl.includes('schema') ||
|
|
511
|
-
lowerUrl.includes('openapi') ||
|
|
512
|
-
lowerUrl.includes('swagger') ||
|
|
513
|
-
lowerUrl.includes('graphql') ||
|
|
514
|
-
lowerUrl.includes('protobuf')
|
|
515
|
-
) {
|
|
516
|
-
return 'schema';
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Documentation
|
|
520
|
-
if (
|
|
521
|
-
lowerUrl.includes('/docs') ||
|
|
522
|
-
lowerUrl.includes('/documentation') ||
|
|
523
|
-
lowerUrl.includes('/guide') ||
|
|
524
|
-
lowerUrl.includes('/tutorial') ||
|
|
525
|
-
lowerUrl.includes('/reference') ||
|
|
526
|
-
lowerContext.includes('documentation') ||
|
|
527
|
-
lowerContext.includes('docs')
|
|
528
|
-
) {
|
|
529
|
-
return 'documentation';
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Reference implementations (GitHub repos)
|
|
533
|
-
if (
|
|
534
|
-
isGitHubRepositoryUrl ||
|
|
535
|
-
(lowerUrl.includes('github.com') &&
|
|
536
|
-
(lowerContext.includes('example') ||
|
|
537
|
-
lowerContext.includes('implementation') ||
|
|
538
|
-
lowerContext.includes('reference')))
|
|
539
|
-
) {
|
|
540
|
-
return 'reference-implementation';
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// API examples
|
|
544
|
-
if (
|
|
545
|
-
lowerContext.includes('example') &&
|
|
546
|
-
(lowerContext.includes('api') || lowerContext.includes('endpoint'))
|
|
547
|
-
) {
|
|
548
|
-
return 'api-example';
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// Cannot determine programmatically - needs LLM
|
|
552
|
-
return null;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
private extractName(url: string): string {
|
|
556
|
-
// Extract a reasonable name from URL
|
|
557
|
-
try {
|
|
558
|
-
const urlObj = new URL(url);
|
|
559
|
-
const pathParts = urlObj.pathname.split('/').filter(Boolean);
|
|
560
|
-
if (pathParts.length > 0) {
|
|
561
|
-
const lastPart = pathParts[pathParts.length - 1];
|
|
562
|
-
if (lastPart) {
|
|
563
|
-
return lastPart.replace(/\.[^.]+$/, '');
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
return urlObj.hostname;
|
|
567
|
-
} catch {
|
|
568
|
-
return url;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
private parsePackageFile(
|
|
573
|
-
filePath: string,
|
|
574
|
-
content: string
|
|
575
|
-
): { urls: string[]; repository?: string; homepage?: string; documentation?: string } {
|
|
576
|
-
const fileName = filePath.split('/').pop() || '';
|
|
577
|
-
|
|
578
|
-
if (fileName === 'package.json') {
|
|
579
|
-
return parsePackageJson(content);
|
|
580
|
-
}
|
|
581
|
-
if (fileName === 'requirements.txt') {
|
|
582
|
-
return parseRequirementsTxt(content);
|
|
583
|
-
}
|
|
584
|
-
if (fileName === 'Cargo.toml') {
|
|
585
|
-
return parseCargoToml(content);
|
|
586
|
-
}
|
|
587
|
-
if (fileName === 'go.mod') {
|
|
588
|
-
return parseGoMod(content);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
return { urls: [] };
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
private async findFiles(dir: string, pattern: RegExp): Promise<string[]> {
|
|
595
|
-
const files: string[] = [];
|
|
596
|
-
|
|
597
|
-
try {
|
|
598
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
599
|
-
|
|
600
|
-
for (const entry of entries) {
|
|
601
|
-
const fullPath = join(dir, entry.name);
|
|
602
|
-
|
|
603
|
-
if (await this.shouldIgnorePath(fullPath, entry.isDirectory())) {
|
|
604
|
-
continue;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
if (entry.isDirectory()) {
|
|
608
|
-
const subFiles = await this.findFiles(fullPath, pattern);
|
|
609
|
-
files.push(...subFiles);
|
|
610
|
-
} else if (pattern.test(entry.name)) {
|
|
611
|
-
files.push(fullPath);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
} catch {
|
|
615
|
-
// Ignore errors (permission denied, etc.)
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
return files;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
private async findPackageFiles(dir: string): Promise<string[]> {
|
|
622
|
-
return this.findFiles(dir, /^(package\.json|requirements\.txt|Cargo\.toml|go\.mod)$/);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
private async findSourceFiles(dir: string): Promise<string[]> {
|
|
626
|
-
return this.findFiles(dir, /\.(ts|js|tsx|jsx|py|rs|go|java|kt|cs|rb|php)$/);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
private shouldIgnore(name: string): boolean {
|
|
630
|
-
return this.options.ignorePatterns.some((pattern) => name.includes(pattern));
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
private async shouldIgnorePath(filePath: string, isDirectory = false): Promise<boolean> {
|
|
634
|
-
if (this.isDotDirectoryPath(filePath)) {
|
|
635
|
-
return true;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
const segments = normalize(filePath).split(sep);
|
|
639
|
-
if (segments.some((segment) => this.shouldIgnore(segment))) {
|
|
640
|
-
return true;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
return await this.isGitIgnored(filePath, isDirectory);
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
private isDotDirectoryPath(filePath: string): boolean {
|
|
647
|
-
const segments = normalize(filePath).split(sep);
|
|
648
|
-
return segments.some(
|
|
649
|
-
(segment) =>
|
|
650
|
-
segment.startsWith('.') && segment.length > 1 && !ALLOWED_DOT_DIRECTORIES.has(segment)
|
|
651
|
-
);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
private isDotDirectoryName(name: string): boolean {
|
|
655
|
-
return name.startsWith('.') && name.length > 1 && !ALLOWED_DOT_DIRECTORIES.has(name);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
private async isGitIgnored(filePath: string, isDirectory: boolean): Promise<boolean> {
|
|
659
|
-
if (!this.options.useGitExcludes) {
|
|
660
|
-
return false;
|
|
661
|
-
}
|
|
662
|
-
const matcher = await this.getIgnoreMatcher();
|
|
663
|
-
if (!matcher) {
|
|
664
|
-
return false;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
const relativePath = this.getRepoRelativePath(filePath);
|
|
668
|
-
if (!relativePath) {
|
|
669
|
-
return false;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
const normalized = relativePath.split(sep).join('/');
|
|
673
|
-
const testPath = isDirectory ? `${normalized}/` : normalized;
|
|
674
|
-
return matcher.ignores(testPath);
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
private getRepoRelativePath(filePath: string): string | null {
|
|
678
|
-
const repoPath = resolve(normalize(this.options.repoPath));
|
|
679
|
-
const normalizedPath = resolve(normalize(filePath));
|
|
680
|
-
|
|
681
|
-
if (normalizedPath === repoPath) {
|
|
682
|
-
return '';
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
if (normalizedPath.startsWith(repoPath + sep)) {
|
|
686
|
-
return relative(repoPath, normalizedPath);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
return normalize(filePath);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
private async getIgnoreMatcher(): Promise<Ignore | null> {
|
|
693
|
-
if (this.ignoreMatcherLoaded) {
|
|
694
|
-
return this.ignoreMatcher;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
this.ignoreMatcherLoaded = true;
|
|
698
|
-
|
|
699
|
-
if (!this.options.useGitExcludes) {
|
|
700
|
-
this.ignoreMatcher = null;
|
|
701
|
-
return this.ignoreMatcher;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
try {
|
|
705
|
-
const matcher = ignore();
|
|
706
|
-
const gitignoreFiles = await this.collectGitignoreFiles(this.options.repoPath);
|
|
707
|
-
const extraIgnoreFiles = this.collectExtraIgnoreFiles();
|
|
708
|
-
|
|
709
|
-
for (const filePath of gitignoreFiles) {
|
|
710
|
-
const content = await readFile(filePath, 'utf-8');
|
|
711
|
-
const rules = this.prefixGitignoreRules(filePath, content);
|
|
712
|
-
if (rules.length > 0) {
|
|
713
|
-
matcher.add(rules);
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
for (const filePath of extraIgnoreFiles) {
|
|
718
|
-
try {
|
|
719
|
-
const content = await readFile(filePath, 'utf-8');
|
|
720
|
-
const rules = this.prefixGitignoreRules(filePath, content, '');
|
|
721
|
-
if (rules.length > 0) {
|
|
722
|
-
matcher.add(rules);
|
|
723
|
-
}
|
|
724
|
-
} catch {
|
|
725
|
-
// Ignore missing or unreadable global ignore files
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
this.ignoreMatcher = matcher;
|
|
730
|
-
} catch {
|
|
731
|
-
this.ignoreMatcher = null;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
return this.ignoreMatcher;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
private async collectGitignoreFiles(dir: string): Promise<string[]> {
|
|
738
|
-
const files: string[] = [];
|
|
739
|
-
|
|
740
|
-
try {
|
|
741
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
742
|
-
for (const entry of entries) {
|
|
743
|
-
if (entry.isDirectory()) {
|
|
744
|
-
if (this.isDotDirectoryName(entry.name) || this.shouldIgnore(entry.name)) {
|
|
745
|
-
continue;
|
|
746
|
-
}
|
|
747
|
-
files.push(...(await this.collectGitignoreFiles(join(dir, entry.name))));
|
|
748
|
-
} else if (entry.isFile() && entry.name === '.gitignore') {
|
|
749
|
-
files.push(join(dir, entry.name));
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
} catch {
|
|
753
|
-
// Ignore errors
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
return files;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
private prefixGitignoreRules(filePath: string, content: string, basePrefix?: string): string[] {
|
|
760
|
-
const repoPath = resolve(normalize(this.options.repoPath));
|
|
761
|
-
const ignoreDir = dirname(filePath);
|
|
762
|
-
const relativeDir = relative(repoPath, ignoreDir).split(sep).join('/');
|
|
763
|
-
const prefix = basePrefix !== undefined ? basePrefix : relativeDir ? `${relativeDir}/` : '';
|
|
764
|
-
|
|
765
|
-
return content
|
|
766
|
-
.split('\n')
|
|
767
|
-
.map((line) => line.trim())
|
|
768
|
-
.filter((line) => line && !line.startsWith('#'))
|
|
769
|
-
.map((line) => {
|
|
770
|
-
const negated = line.startsWith('!');
|
|
771
|
-
const raw = negated ? line.slice(1) : line;
|
|
772
|
-
const trimmed = raw.startsWith('/') ? raw.slice(1) : raw;
|
|
773
|
-
const scoped = `${prefix}${trimmed}`;
|
|
774
|
-
return negated ? `!${scoped}` : scoped;
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
private collectExtraIgnoreFiles(): string[] {
|
|
779
|
-
const files = new Set<string>();
|
|
780
|
-
|
|
781
|
-
files.add(join(this.options.repoPath, '.git', 'info', 'exclude'));
|
|
782
|
-
|
|
783
|
-
const globalConfig = this.getGlobalExcludeFileFromGit();
|
|
784
|
-
if (globalConfig) {
|
|
785
|
-
files.add(globalConfig);
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
for (const fallback of this.getDefaultGlobalExcludeFiles()) {
|
|
789
|
-
files.add(fallback);
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
return Array.from(files);
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
private getGlobalExcludeFileFromGit(): string | null {
|
|
796
|
-
try {
|
|
797
|
-
const output = execSync('git config --get core.excludesfile', {
|
|
798
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
|
799
|
-
})
|
|
800
|
-
.toString()
|
|
801
|
-
.trim();
|
|
802
|
-
|
|
803
|
-
if (!output) {
|
|
804
|
-
return null;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
return this.expandHomePath(output);
|
|
808
|
-
} catch {
|
|
809
|
-
return null;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
private getDefaultGlobalExcludeFiles(): string[] {
|
|
814
|
-
const home = homedir();
|
|
815
|
-
return [
|
|
816
|
-
join(home, '.config', 'git', 'ignore'),
|
|
817
|
-
join(home, '.gitignore_global'),
|
|
818
|
-
join(home, '.gitignore')
|
|
819
|
-
];
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
private expandHomePath(filePath: string): string {
|
|
823
|
-
if (filePath.startsWith('~/')) {
|
|
824
|
-
return join(homedir(), filePath.slice(2));
|
|
825
|
-
}
|
|
826
|
-
if (filePath === '~') {
|
|
827
|
-
return homedir();
|
|
828
|
-
}
|
|
829
|
-
return filePath;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
/**
|
|
833
|
-
* Analyze only specific files for dependencies (for incremental updates)
|
|
834
|
-
* This is more efficient than full repository scan when only few files changed
|
|
835
|
-
*/
|
|
836
|
-
async analyzeFiles(filePaths: string[]): Promise<DetectionResult> {
|
|
837
|
-
const allReferences: Map<
|
|
838
|
-
string,
|
|
839
|
-
{
|
|
840
|
-
url: string;
|
|
841
|
-
contexts: Array<{ file: string; line?: number; text: string }>;
|
|
842
|
-
detectionMethod: DetectionMethod;
|
|
843
|
-
}
|
|
844
|
-
> = new Map();
|
|
845
|
-
|
|
846
|
-
let filesScanned = 0;
|
|
847
|
-
let llmCalls = 0;
|
|
848
|
-
let totalTokens = 0;
|
|
849
|
-
let totalLatencyMs = 0;
|
|
850
|
-
|
|
851
|
-
for (const filePath of filePaths) {
|
|
852
|
-
if (await this.shouldIgnorePath(filePath)) {
|
|
853
|
-
continue;
|
|
854
|
-
}
|
|
855
|
-
// Validate that the file path is safe before joining
|
|
856
|
-
const normalizedRepoPath = resolve(normalize(this.options.repoPath));
|
|
857
|
-
const normalizedFilePath = normalize(filePath);
|
|
858
|
-
const fullPath = resolve(normalizedRepoPath, normalizedFilePath);
|
|
859
|
-
|
|
860
|
-
// Ensure the resolved path is within the repository boundaries
|
|
861
|
-
// Use path.sep for cross-platform compatibility
|
|
862
|
-
const repoPathWithSep = normalizedRepoPath.endsWith(sep)
|
|
863
|
-
? normalizedRepoPath
|
|
864
|
-
: normalizedRepoPath + sep;
|
|
865
|
-
|
|
866
|
-
if (!fullPath.startsWith(repoPathWithSep) && fullPath !== normalizedRepoPath) {
|
|
867
|
-
// Skip files outside the repository to prevent path traversal
|
|
868
|
-
if (process.env['DEBUG']) {
|
|
869
|
-
console.warn(`Skipping file outside repository: ${filePath}`);
|
|
870
|
-
}
|
|
871
|
-
continue;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
try {
|
|
875
|
-
const content = await readFile(fullPath, 'utf-8');
|
|
876
|
-
const relativePath = relative(this.options.repoPath, fullPath);
|
|
877
|
-
const fileName = filePath.split('/').pop() || '';
|
|
878
|
-
|
|
879
|
-
// Parse based on file type
|
|
880
|
-
if (/^README/i.test(fileName)) {
|
|
881
|
-
// README file
|
|
882
|
-
const references = parseReadme(content, relativePath);
|
|
883
|
-
for (const ref of references) {
|
|
884
|
-
this.addReference(
|
|
885
|
-
allReferences,
|
|
886
|
-
ref.url,
|
|
887
|
-
{
|
|
888
|
-
file: relativePath,
|
|
889
|
-
...(ref.line !== undefined && { line: ref.line }),
|
|
890
|
-
text: ref.context
|
|
891
|
-
},
|
|
892
|
-
'llm-analysis'
|
|
893
|
-
);
|
|
894
|
-
}
|
|
895
|
-
} else if (/^(package\.json|requirements\.txt|Cargo\.toml|go\.mod)$/i.test(fileName)) {
|
|
896
|
-
// Package file
|
|
897
|
-
const metadata = this.parsePackageFile(fullPath, content);
|
|
898
|
-
for (const url of [
|
|
899
|
-
...(metadata.urls || []),
|
|
900
|
-
metadata.repository,
|
|
901
|
-
metadata.homepage,
|
|
902
|
-
metadata.documentation
|
|
903
|
-
].filter(Boolean)) {
|
|
904
|
-
this.addReference(
|
|
905
|
-
allReferences,
|
|
906
|
-
url!,
|
|
907
|
-
{
|
|
908
|
-
file: relativePath,
|
|
909
|
-
text: 'Package metadata'
|
|
910
|
-
},
|
|
911
|
-
'package-json'
|
|
912
|
-
);
|
|
913
|
-
}
|
|
914
|
-
} else if (/\.(ts|js|tsx|jsx|py|rs|go|java|kt|cs|rb|php)$/.test(fileName)) {
|
|
915
|
-
// Source file
|
|
916
|
-
const references = parseCodeComments(content, relativePath);
|
|
917
|
-
for (const ref of references) {
|
|
918
|
-
this.addReference(
|
|
919
|
-
allReferences,
|
|
920
|
-
ref.url,
|
|
921
|
-
{
|
|
922
|
-
file: ref.file,
|
|
923
|
-
line: ref.line,
|
|
924
|
-
text: ref.context
|
|
925
|
-
},
|
|
926
|
-
'code-comment'
|
|
927
|
-
);
|
|
928
|
-
}
|
|
929
|
-
} else if (/\.(md|txt|rst|adoc)$/.test(fileName)) {
|
|
930
|
-
// Documentation file
|
|
931
|
-
const references = parseReadme(content, relativePath);
|
|
932
|
-
for (const ref of references) {
|
|
933
|
-
this.addReference(
|
|
934
|
-
allReferences,
|
|
935
|
-
ref.url,
|
|
936
|
-
{
|
|
937
|
-
file: relativePath,
|
|
938
|
-
...(ref.line !== undefined && { line: ref.line }),
|
|
939
|
-
text: ref.context
|
|
940
|
-
},
|
|
941
|
-
'llm-analysis'
|
|
942
|
-
);
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
filesScanned++;
|
|
947
|
-
} catch (error) {
|
|
948
|
-
// Skip files that can't be read - log but don't throw
|
|
949
|
-
// Using console.warn since we don't have a logger instance here
|
|
950
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
951
|
-
console.warn(`Failed to analyze ${filePath}: ${message}`);
|
|
952
|
-
if (process.env['DEBUG']) {
|
|
953
|
-
// Log full error details when DEBUG is enabled
|
|
954
|
-
console.debug('Full error while analyzing %s:', filePath, error);
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
// Create dependency entries
|
|
960
|
-
const dependencies: DependencyEntry[] = [];
|
|
961
|
-
|
|
962
|
-
for (const [url, refData] of allReferences.entries()) {
|
|
963
|
-
const contextText = refData.contexts.map((c) => c.text).join(' ');
|
|
964
|
-
|
|
965
|
-
// Step 3: Programmatic type categorization
|
|
966
|
-
let type = this.determineDependencyType(url, contextText);
|
|
967
|
-
|
|
968
|
-
// Step 4: LLM fallback for type categorization (if needed)
|
|
969
|
-
if (!type && refData.contexts.length > 0) {
|
|
970
|
-
const startTime = Date.now();
|
|
971
|
-
try {
|
|
972
|
-
const prompt = createClassificationPrompt(url, contextText);
|
|
973
|
-
const response = await this.options.llmProvider.analyze('', prompt);
|
|
974
|
-
|
|
975
|
-
llmCalls++;
|
|
976
|
-
totalTokens += response.usage?.totalTokens || 0;
|
|
977
|
-
totalLatencyMs += Date.now() - startTime;
|
|
978
|
-
|
|
979
|
-
// Use rawResponse for classification
|
|
980
|
-
const responseText = (response.rawResponse || '').toLowerCase();
|
|
981
|
-
type = (
|
|
982
|
-
responseText.includes('schema')
|
|
983
|
-
? 'schema'
|
|
984
|
-
: responseText.includes('documentation')
|
|
985
|
-
? 'documentation'
|
|
986
|
-
: responseText.includes('research') || responseText.includes('paper')
|
|
987
|
-
? 'research-paper'
|
|
988
|
-
: responseText.includes('implementation')
|
|
989
|
-
? 'reference-implementation'
|
|
990
|
-
: responseText.includes('example')
|
|
991
|
-
? 'api-example'
|
|
992
|
-
: 'other'
|
|
993
|
-
) as DependencyType;
|
|
994
|
-
} catch {
|
|
995
|
-
type = 'other';
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
if (!type) {
|
|
1000
|
-
type = 'other';
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
// Step 5: Programmatic access method determination
|
|
1004
|
-
let accessMethod = this.determineAccessMethod(url);
|
|
1005
|
-
if (!accessMethod) {
|
|
1006
|
-
accessMethod = 'http'; // Default fallback
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
const dependency: DependencyEntry = {
|
|
1010
|
-
id: randomUUID(),
|
|
1011
|
-
url,
|
|
1012
|
-
type,
|
|
1013
|
-
accessMethod,
|
|
1014
|
-
name: this.extractName(url),
|
|
1015
|
-
currentStateHash: `sha256:pending`,
|
|
1016
|
-
detectionMethod: refData.detectionMethod,
|
|
1017
|
-
detectionConfidence: refData.detectionMethod === 'manual' ? 1.0 : 0.85,
|
|
1018
|
-
detectedAt: new Date().toISOString(),
|
|
1019
|
-
lastChecked: new Date().toISOString(),
|
|
1020
|
-
auth: undefined,
|
|
1021
|
-
referencedIn: refData.contexts.map((ctx) => ({
|
|
1022
|
-
file: ctx.file,
|
|
1023
|
-
line: ctx.line,
|
|
1024
|
-
context: ctx.text
|
|
1025
|
-
})),
|
|
1026
|
-
changeHistory: []
|
|
1027
|
-
};
|
|
1028
|
-
|
|
1029
|
-
dependencies.push(dependency);
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
return {
|
|
1033
|
-
dependencies,
|
|
1034
|
-
statistics: {
|
|
1035
|
-
filesScanned,
|
|
1036
|
-
urlsFound: allReferences.size,
|
|
1037
|
-
llmCalls,
|
|
1038
|
-
totalTokens,
|
|
1039
|
-
totalLatencyMs
|
|
1040
|
-
}
|
|
1041
|
-
};
|
|
1042
|
-
}
|
|
1043
|
-
}
|