@dependabit/detector 0.1.2 → 0.1.15
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 +107 -0
- package/dist/detector.d.ts +4 -0
- package/dist/detector.d.ts.map +1 -1
- package/dist/detector.js +76 -18
- package/dist/detector.js.map +1 -1
- package/dist/llm/copilot.d.ts.map +1 -1
- package/dist/llm/copilot.js +12 -8
- package/dist/llm/copilot.js.map +1 -1
- package/dist/llm/prompts.d.ts +1 -1
- package/dist/llm/prompts.d.ts.map +1 -1
- package/dist/llm/prompts.js +10 -1
- package/dist/llm/prompts.js.map +1 -1
- package/dist/parsers/readme.d.ts.map +1 -1
- package/dist/parsers/readme.js +8 -2
- package/dist/parsers/readme.js.map +1 -1
- package/package.json +9 -4
- package/src/detector.ts +107 -19
- package/src/llm/copilot.ts +13 -10
- package/src/llm/prompts.ts +10 -1
- package/src/parsers/readme.ts +8 -2
- package/test/detector.test.ts +67 -0
- package/test/llm/copilot.test.ts +302 -23
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dependabit/detector",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/pradeepmouli/dependabit",
|
|
7
|
+
"directory": "packages/detector"
|
|
8
|
+
},
|
|
4
9
|
"description": "LLM-based dependency detection for external resources",
|
|
5
10
|
"type": "module",
|
|
6
11
|
"main": "dist/index.js",
|
|
@@ -15,8 +20,8 @@
|
|
|
15
20
|
"@actions/core": "^3.0.0",
|
|
16
21
|
"ignore": "^7.0.5",
|
|
17
22
|
"zod": "^4.3.6",
|
|
18
|
-
"@dependabit/
|
|
19
|
-
"@dependabit/
|
|
23
|
+
"@dependabit/github-client": "0.1.13",
|
|
24
|
+
"@dependabit/manifest": "0.1.13"
|
|
20
25
|
},
|
|
21
26
|
"devDependencies": {
|
|
22
27
|
"@types/node": "^25.2.3",
|
|
@@ -33,7 +38,7 @@
|
|
|
33
38
|
"license": "MIT",
|
|
34
39
|
"scripts": {
|
|
35
40
|
"build": "tsgo -p tsconfig.json",
|
|
36
|
-
"clean": "rm -rf dist",
|
|
41
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
37
42
|
"dev": "tsx watch src/index.ts",
|
|
38
43
|
"type-check": "tsgo --noEmit -p tsconfig.json",
|
|
39
44
|
"test": "vitest run",
|
package/src/detector.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { readdir, readFile } from 'node:fs/promises';
|
|
7
7
|
import { execSync } from 'node:child_process';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
|
-
import { dirname, join, relative, resolve, normalize, sep } from 'node:path';
|
|
9
|
+
import { basename, dirname, join, relative, resolve, normalize, sep } from 'node:path';
|
|
10
10
|
import { randomUUID } from 'node:crypto';
|
|
11
11
|
import ignore, { type Ignore } from 'ignore';
|
|
12
12
|
import type { LLMProvider } from './llm/client.js';
|
|
@@ -31,6 +31,8 @@ export interface DetectorOptions {
|
|
|
31
31
|
llmProvider: LLMProvider;
|
|
32
32
|
ignorePatterns?: string[];
|
|
33
33
|
useGitExcludes?: boolean;
|
|
34
|
+
repoOwner?: string;
|
|
35
|
+
repoName?: string;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export interface DetectionResult {
|
|
@@ -46,22 +48,17 @@ export interface DetectionResult {
|
|
|
46
48
|
|
|
47
49
|
const DEFAULT_IGNORE_PATTERNS = [
|
|
48
50
|
'node_modules',
|
|
49
|
-
'.git',
|
|
50
|
-
'.github',
|
|
51
|
-
'.claude',
|
|
52
|
-
'.codex',
|
|
53
51
|
'dist',
|
|
54
52
|
'build',
|
|
55
53
|
'target',
|
|
56
54
|
'vendor',
|
|
57
|
-
'.venv',
|
|
58
55
|
'venv',
|
|
59
56
|
'__pycache__',
|
|
60
|
-
'coverage'
|
|
61
|
-
'.next',
|
|
62
|
-
'.nuxt'
|
|
57
|
+
'coverage'
|
|
63
58
|
];
|
|
64
59
|
|
|
60
|
+
const ALLOWED_DOT_DIRECTORIES = new Set<string>();
|
|
61
|
+
|
|
65
62
|
/**
|
|
66
63
|
* Main detector class
|
|
67
64
|
*/
|
|
@@ -69,13 +66,38 @@ export class Detector {
|
|
|
69
66
|
private options: Required<DetectorOptions>;
|
|
70
67
|
private ignoreMatcher: Ignore | null = null;
|
|
71
68
|
private ignoreMatcherLoaded = false;
|
|
69
|
+
private skipUrlPatterns: RegExp[];
|
|
72
70
|
|
|
73
71
|
constructor(options: DetectorOptions) {
|
|
74
72
|
this.options = {
|
|
75
73
|
...options,
|
|
76
74
|
ignorePatterns: options.ignorePatterns || DEFAULT_IGNORE_PATTERNS,
|
|
77
|
-
useGitExcludes: options.useGitExcludes ?? true
|
|
75
|
+
useGitExcludes: options.useGitExcludes ?? true,
|
|
76
|
+
repoOwner: options.repoOwner || '',
|
|
77
|
+
repoName: options.repoName || ''
|
|
78
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
|
+
];
|
|
79
101
|
}
|
|
80
102
|
|
|
81
103
|
/**
|
|
@@ -128,7 +150,33 @@ export class Detector {
|
|
|
128
150
|
filesScanned++;
|
|
129
151
|
}
|
|
130
152
|
|
|
131
|
-
// 1b. Parse
|
|
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)
|
|
132
180
|
const packageFiles = await this.findPackageFiles(this.options.repoPath);
|
|
133
181
|
for (const file of packageFiles) {
|
|
134
182
|
const content = await readFile(file, 'utf-8');
|
|
@@ -154,7 +202,7 @@ export class Detector {
|
|
|
154
202
|
filesScanned++;
|
|
155
203
|
}
|
|
156
204
|
|
|
157
|
-
//
|
|
205
|
+
// 1d. Parse code comments from source files
|
|
158
206
|
const sourceFiles = await this.findSourceFiles(this.options.repoPath);
|
|
159
207
|
for (const file of sourceFiles.slice(0, 50)) {
|
|
160
208
|
// Limit to 50 files for performance
|
|
@@ -232,6 +280,8 @@ Return as JSON with "dependencies" array.`;
|
|
|
232
280
|
const now = new Date().toISOString();
|
|
233
281
|
|
|
234
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)
|
|
235
285
|
// Prepare context for potential LLM use
|
|
236
286
|
const context = data.contexts
|
|
237
287
|
.map((c) => `${c.file}${c.line ? `:${c.line}` : ''}: ${c.text}`)
|
|
@@ -269,6 +319,11 @@ Return as JSON with "dependencies" array.`;
|
|
|
269
319
|
typeConfidence = 0.3;
|
|
270
320
|
}
|
|
271
321
|
|
|
322
|
+
// Skip low-confidence entries
|
|
323
|
+
if (typeConfidence < 0.5) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
272
327
|
// Step 5: Try programmatic access method determination
|
|
273
328
|
let accessMethod: AccessMethod | null = this.determineAccessMethod(url);
|
|
274
329
|
|
|
@@ -364,6 +419,27 @@ Return as JSON: {"accessMethod": "...", "confidence": 0.0-1.0}`;
|
|
|
364
419
|
context: { file: string; line?: number; text: string },
|
|
365
420
|
detectionMethod: DetectionMethod
|
|
366
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
|
+
|
|
367
443
|
if (!map.has(url)) {
|
|
368
444
|
map.set(url, {
|
|
369
445
|
url,
|
|
@@ -374,6 +450,10 @@ Return as JSON: {"accessMethod": "...", "confidence": 0.0-1.0}`;
|
|
|
374
450
|
map.get(url)!.contexts.push(context);
|
|
375
451
|
}
|
|
376
452
|
|
|
453
|
+
private escapeRegExp(str: string): string {
|
|
454
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
455
|
+
}
|
|
456
|
+
|
|
377
457
|
/**
|
|
378
458
|
* Programmatically determine access method based on URL patterns
|
|
379
459
|
* Returns null if cannot be determined programmatically
|
|
@@ -412,6 +492,10 @@ Return as JSON: {"accessMethod": "...", "confidence": 0.0-1.0}`;
|
|
|
412
492
|
const lowerUrl = url.toLowerCase();
|
|
413
493
|
const lowerContext = context.toLowerCase();
|
|
414
494
|
|
|
495
|
+
const isGitHubRepositoryUrl = /^https?:\/\/github\.com\/[^/]+\/[^/#?]+(?:$|[/?#])/.test(
|
|
496
|
+
lowerUrl
|
|
497
|
+
);
|
|
498
|
+
|
|
415
499
|
// Research papers
|
|
416
500
|
if (
|
|
417
501
|
lowerUrl.includes('arxiv.org') ||
|
|
@@ -447,10 +531,11 @@ Return as JSON: {"accessMethod": "...", "confidence": 0.0-1.0}`;
|
|
|
447
531
|
|
|
448
532
|
// Reference implementations (GitHub repos)
|
|
449
533
|
if (
|
|
450
|
-
|
|
451
|
-
(
|
|
452
|
-
lowerContext.includes('
|
|
453
|
-
|
|
534
|
+
isGitHubRepositoryUrl ||
|
|
535
|
+
(lowerUrl.includes('github.com') &&
|
|
536
|
+
(lowerContext.includes('example') ||
|
|
537
|
+
lowerContext.includes('implementation') ||
|
|
538
|
+
lowerContext.includes('reference')))
|
|
454
539
|
) {
|
|
455
540
|
return 'reference-implementation';
|
|
456
541
|
}
|
|
@@ -526,7 +611,7 @@ Return as JSON: {"accessMethod": "...", "confidence": 0.0-1.0}`;
|
|
|
526
611
|
files.push(fullPath);
|
|
527
612
|
}
|
|
528
613
|
}
|
|
529
|
-
} catch
|
|
614
|
+
} catch {
|
|
530
615
|
// Ignore errors (permission denied, etc.)
|
|
531
616
|
}
|
|
532
617
|
|
|
@@ -560,11 +645,14 @@ Return as JSON: {"accessMethod": "...", "confidence": 0.0-1.0}`;
|
|
|
560
645
|
|
|
561
646
|
private isDotDirectoryPath(filePath: string): boolean {
|
|
562
647
|
const segments = normalize(filePath).split(sep);
|
|
563
|
-
return segments.some(
|
|
648
|
+
return segments.some(
|
|
649
|
+
(segment) =>
|
|
650
|
+
segment.startsWith('.') && segment.length > 1 && !ALLOWED_DOT_DIRECTORIES.has(segment)
|
|
651
|
+
);
|
|
564
652
|
}
|
|
565
653
|
|
|
566
654
|
private isDotDirectoryName(name: string): boolean {
|
|
567
|
-
return name.startsWith('.') && name.length > 1;
|
|
655
|
+
return name.startsWith('.') && name.length > 1 && !ALLOWED_DOT_DIRECTORIES.has(name);
|
|
568
656
|
}
|
|
569
657
|
|
|
570
658
|
private async isGitIgnored(filePath: string, isDirectory: boolean): Promise<boolean> {
|
package/src/llm/copilot.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Integrates with GitHub Copilot via CLI commands
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
7
|
import { promisify } from 'node:util';
|
|
8
8
|
import type {
|
|
9
9
|
LLMProvider,
|
|
@@ -15,7 +15,7 @@ import type {
|
|
|
15
15
|
} from './client.js';
|
|
16
16
|
import { SYSTEM_PROMPT } from './prompts.js';
|
|
17
17
|
|
|
18
|
-
const
|
|
18
|
+
const execFileAsync = promisify(execFile);
|
|
19
19
|
|
|
20
20
|
export class GitHubCopilotProvider implements LLMProvider {
|
|
21
21
|
private config: Required<LLMProviderConfig>;
|
|
@@ -44,14 +44,17 @@ export class GitHubCopilotProvider implements LLMProvider {
|
|
|
44
44
|
// Combine system prompt and user prompt for CLI
|
|
45
45
|
const fullPrompt = `${SYSTEM_PROMPT}\n\n${prompt}`;
|
|
46
46
|
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
// Use execFile to avoid shell escaping issues and command injection
|
|
48
|
+
// Pass prompt directly as an argument without manual escaping
|
|
49
|
+
const args = [
|
|
50
|
+
'copilot',
|
|
51
|
+
'-p',
|
|
52
|
+
fullPrompt,
|
|
53
|
+
'--silent',
|
|
54
|
+
'--allow-all-tools'
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const { stdout, stderr } = await execFileAsync('gh', args, {
|
|
55
58
|
maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large responses
|
|
56
59
|
timeout: 60000 // 60 second timeout
|
|
57
60
|
});
|
package/src/llm/prompts.ts
CHANGED
|
@@ -17,11 +17,20 @@ INCLUDE these types of dependencies:
|
|
|
17
17
|
|
|
18
18
|
EXCLUDE these (handled by dependabot):
|
|
19
19
|
- NPM packages in package.json
|
|
20
|
-
- Python packages in requirements.txt
|
|
20
|
+
- Python packages in requirements.txt
|
|
21
21
|
- Rust crates in Cargo.toml
|
|
22
22
|
- Docker images in Dockerfile
|
|
23
23
|
- Any declared package manager dependencies
|
|
24
24
|
|
|
25
|
+
ALSO EXCLUDE (false positives):
|
|
26
|
+
- URLs that reference the repository itself (self-references)
|
|
27
|
+
- Relative file paths (e.g., CONTRIBUTING.md, docs/guide.md, ./src/utils)
|
|
28
|
+
- Placeholder URLs used in documentation examples (example.com, example.org, localhost)
|
|
29
|
+
- Internal documentation links within the same repository
|
|
30
|
+
- URLs with template variables or placeholders (e.g., issues/[NUMBER], user/repo)
|
|
31
|
+
|
|
32
|
+
Only return dependencies with confidence >= 0.7.
|
|
33
|
+
|
|
25
34
|
For each dependency found, provide:
|
|
26
35
|
1. url: The complete URL
|
|
27
36
|
2. name: A descriptive name
|
package/src/parsers/readme.ts
CHANGED
|
@@ -10,7 +10,7 @@ export interface ExtractedReference {
|
|
|
10
10
|
type: 'markdown-link' | 'bare-url' | 'reference-link';
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
// Patterns to skip (package managers, CI badges, shields.io)
|
|
13
|
+
// Patterns to skip (package managers, CI badges, shields.io, placeholders)
|
|
14
14
|
const SKIP_PATTERNS = [
|
|
15
15
|
/npmjs\.com\/package/,
|
|
16
16
|
/pypi\.org\/project/,
|
|
@@ -21,7 +21,13 @@ const SKIP_PATTERNS = [
|
|
|
21
21
|
/badge(s)?\..*\.svg/,
|
|
22
22
|
/travis-ci\.(org|com)/,
|
|
23
23
|
/circleci\.com/,
|
|
24
|
-
/github\.com\/.*\/actions
|
|
24
|
+
/github\.com\/.*\/actions/, // GitHub Actions badges
|
|
25
|
+
/example\.com/, // placeholder domain
|
|
26
|
+
/example\.org/, // placeholder domain
|
|
27
|
+
/localhost/, // local dev URLs
|
|
28
|
+
/127\.0\.0\.1/, // loopback address
|
|
29
|
+
/\[.*\]/, // template placeholders (e.g., issues/[NUMBER])
|
|
30
|
+
/github\.com\/user\/repo/ // common placeholder in docs
|
|
25
31
|
];
|
|
26
32
|
|
|
27
33
|
/**
|
package/test/detector.test.ts
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { Detector } from '../src/detector.js';
|
|
6
|
+
|
|
7
|
+
const createDetector = (): Detector =>
|
|
8
|
+
new Detector({
|
|
9
|
+
repoPath: '.',
|
|
10
|
+
llmProvider: {
|
|
11
|
+
analyze: vi.fn(async () => ({
|
|
12
|
+
dependencies: [],
|
|
13
|
+
usage: { totalTokens: 0, latencyMs: 0 },
|
|
14
|
+
rawResponse: '{}'
|
|
15
|
+
}))
|
|
16
|
+
}
|
|
17
|
+
});
|
|
2
18
|
|
|
3
19
|
describe('Detector', () => {
|
|
4
20
|
beforeEach(() => {
|
|
@@ -49,6 +65,47 @@ describe('Detector', () => {
|
|
|
49
65
|
// Expected: Continue with parser results, log error
|
|
50
66
|
expect(true).toBe(true);
|
|
51
67
|
});
|
|
68
|
+
|
|
69
|
+
it('should keep GitHub repo links when LLM parser misses them', async () => {
|
|
70
|
+
const repoPath = await mkdtemp(join(tmpdir(), 'dependabit-detector-'));
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await mkdir(join(repoPath, 'docs'), { recursive: true });
|
|
74
|
+
await writeFile(
|
|
75
|
+
join(repoPath, 'docs', 'tooling.md'),
|
|
76
|
+
'- [spec-kit](https://github.com/github/spec-kit)\n'
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const detector = new Detector({
|
|
80
|
+
repoPath,
|
|
81
|
+
llmProvider: {
|
|
82
|
+
analyze: vi.fn(async () => ({
|
|
83
|
+
dependencies: [],
|
|
84
|
+
usage: { totalTokens: 0, latencyMs: 0 },
|
|
85
|
+
rawResponse: '{}'
|
|
86
|
+
})),
|
|
87
|
+
getSupportedModels: vi.fn(() => ['test-model']),
|
|
88
|
+
getRateLimit: vi.fn(async () => ({
|
|
89
|
+
remaining: 100,
|
|
90
|
+
limit: 100,
|
|
91
|
+
resetAt: new Date(Date.now() + 60_000)
|
|
92
|
+
})),
|
|
93
|
+
validateConfig: vi.fn(() => true)
|
|
94
|
+
},
|
|
95
|
+
repoOwner: 'pradeepmouli',
|
|
96
|
+
repoName: 'dependabit'
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const result = await detector.detectDependencies();
|
|
100
|
+
const dep = result.dependencies.find((d) => d.url === 'https://github.com/github/spec-kit');
|
|
101
|
+
|
|
102
|
+
expect(dep).toBeDefined();
|
|
103
|
+
expect(dep?.type).toBe('reference-implementation');
|
|
104
|
+
expect(dep?.accessMethod).toBe('github-api');
|
|
105
|
+
} finally {
|
|
106
|
+
await rm(repoPath, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
52
109
|
});
|
|
53
110
|
|
|
54
111
|
describe('classifyDependency', () => {
|
|
@@ -57,6 +114,16 @@ describe('Detector', () => {
|
|
|
57
114
|
expect(url).toContain('github.com');
|
|
58
115
|
});
|
|
59
116
|
|
|
117
|
+
it('should classify GitHub repository links as reference-implementation', () => {
|
|
118
|
+
const detector = createDetector();
|
|
119
|
+
const type = (detector as any).determineDependencyType(
|
|
120
|
+
'https://github.com/github/spec-kit',
|
|
121
|
+
'spec-kit'
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(type).toBe('reference-implementation');
|
|
125
|
+
});
|
|
126
|
+
|
|
60
127
|
it('should classify arXiv URLs as research-paper', () => {
|
|
61
128
|
const url = 'https://arxiv.org/abs/1706.03762';
|
|
62
129
|
expect(url).toContain('arxiv.org');
|