@dependabit/manifest 0.1.1
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 +7 -0
- package/LICENSE +21 -0
- package/README.md +32 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +79 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +43 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +191 -0
- package/dist/manifest.js.map +1 -0
- package/dist/schema.d.ts +488 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +213 -0
- package/dist/schema.js.map +1 -0
- package/dist/size-check.d.ts +39 -0
- package/dist/size-check.d.ts.map +1 -0
- package/dist/size-check.js +84 -0
- package/dist/size-check.js.map +1 -0
- package/dist/validator.d.ts +53 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/validator.js +110 -0
- package/dist/validator.js.map +1 -0
- package/package.json +38 -0
- package/src/config.test.ts +266 -0
- package/src/config.ts +96 -0
- package/src/index.ts +16 -0
- package/src/manifest.test.ts +400 -0
- package/src/manifest.ts +261 -0
- package/src/schema.test.ts +293 -0
- package/src/schema.ts +265 -0
- package/src/size-check.test.ts +246 -0
- package/src/size-check.ts +124 -0
- package/src/validator.test.ts +161 -0
- package/src/validator.ts +131 -0
- package/tsconfig.json +10 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdir, rm } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
readManifest,
|
|
6
|
+
writeManifest,
|
|
7
|
+
updateDependency,
|
|
8
|
+
addDependency,
|
|
9
|
+
removeDependency,
|
|
10
|
+
mergeManifests,
|
|
11
|
+
createEmptyManifest
|
|
12
|
+
} from '../src/manifest.js';
|
|
13
|
+
import type { DependencyEntry } from '../src/schema.js';
|
|
14
|
+
|
|
15
|
+
const TEST_DIR = '/tmp/dependabit-manifest-tests';
|
|
16
|
+
|
|
17
|
+
describe('Manifest Operations Tests', () => {
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
await mkdir(TEST_DIR, { recursive: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
await rm(TEST_DIR, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('readManifest and writeManifest', () => {
|
|
27
|
+
it('should write and read a manifest', async () => {
|
|
28
|
+
const manifest = createEmptyManifest({
|
|
29
|
+
owner: 'test',
|
|
30
|
+
name: 'repo',
|
|
31
|
+
branch: 'main',
|
|
32
|
+
commit: 'abc123'
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const path = join(TEST_DIR, 'manifest.json');
|
|
36
|
+
await writeManifest(path, manifest);
|
|
37
|
+
|
|
38
|
+
const read = await readManifest(path);
|
|
39
|
+
expect(read.repository.owner).toBe('test');
|
|
40
|
+
expect(read.repository.name).toBe('repo');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should create directory if not exists', async () => {
|
|
44
|
+
const manifest = createEmptyManifest({
|
|
45
|
+
owner: 'test',
|
|
46
|
+
name: 'repo',
|
|
47
|
+
branch: 'main',
|
|
48
|
+
commit: 'abc123'
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const path = join(TEST_DIR, 'nested', 'dir', 'manifest.json');
|
|
52
|
+
await writeManifest(path, manifest);
|
|
53
|
+
|
|
54
|
+
const read = await readManifest(path);
|
|
55
|
+
expect(read).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should validate manifest before writing', async () => {
|
|
59
|
+
const invalid = { version: '2.0.0' } as any;
|
|
60
|
+
const path = join(TEST_DIR, 'manifest.json');
|
|
61
|
+
|
|
62
|
+
await expect(writeManifest(path, invalid)).rejects.toThrow();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('addDependency', () => {
|
|
67
|
+
it('should add a dependency to manifest', async () => {
|
|
68
|
+
const manifest = createEmptyManifest({
|
|
69
|
+
owner: 'test',
|
|
70
|
+
name: 'repo',
|
|
71
|
+
branch: 'main',
|
|
72
|
+
commit: 'abc123'
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const path = join(TEST_DIR, 'manifest.json');
|
|
76
|
+
await writeManifest(path, manifest);
|
|
77
|
+
|
|
78
|
+
const dependency: DependencyEntry = {
|
|
79
|
+
id: '550e8400-e29b-41d4-a716-446655440000',
|
|
80
|
+
url: 'https://github.com/microsoft/TypeScript',
|
|
81
|
+
type: 'reference-implementation',
|
|
82
|
+
accessMethod: 'github-api',
|
|
83
|
+
name: 'TypeScript',
|
|
84
|
+
currentStateHash: 'sha256:abc123',
|
|
85
|
+
detectionMethod: 'manual',
|
|
86
|
+
detectionConfidence: 1.0,
|
|
87
|
+
detectedAt: new Date().toISOString(),
|
|
88
|
+
lastChecked: new Date().toISOString(),
|
|
89
|
+
auth: undefined,
|
|
90
|
+
referencedIn: []
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const updated = await addDependency(path, dependency);
|
|
94
|
+
expect(updated.dependencies).toHaveLength(1);
|
|
95
|
+
expect(updated.dependencies[0].name).toBe('TypeScript');
|
|
96
|
+
expect(updated.statistics.totalDependencies).toBe(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should reject duplicate IDs', async () => {
|
|
100
|
+
const manifest = createEmptyManifest({
|
|
101
|
+
owner: 'test',
|
|
102
|
+
name: 'repo',
|
|
103
|
+
branch: 'main',
|
|
104
|
+
commit: 'abc123'
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const path = join(TEST_DIR, 'manifest.json');
|
|
108
|
+
await writeManifest(path, manifest);
|
|
109
|
+
|
|
110
|
+
const dependency: DependencyEntry = {
|
|
111
|
+
id: '550e8400-e29b-41d4-a716-446655440000',
|
|
112
|
+
url: 'https://github.com/microsoft/TypeScript',
|
|
113
|
+
type: 'reference-implementation',
|
|
114
|
+
accessMethod: 'github-api',
|
|
115
|
+
name: 'TypeScript',
|
|
116
|
+
currentStateHash: 'sha256:abc123',
|
|
117
|
+
detectionMethod: 'manual',
|
|
118
|
+
detectionConfidence: 1.0,
|
|
119
|
+
detectedAt: new Date().toISOString(),
|
|
120
|
+
lastChecked: new Date().toISOString(),
|
|
121
|
+
auth: undefined,
|
|
122
|
+
referencedIn: []
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
await addDependency(path, dependency);
|
|
126
|
+
await expect(addDependency(path, dependency)).rejects.toThrow(/already exists/);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should reject duplicate URLs', async () => {
|
|
130
|
+
const manifest = createEmptyManifest({
|
|
131
|
+
owner: 'test',
|
|
132
|
+
name: 'repo',
|
|
133
|
+
branch: 'main',
|
|
134
|
+
commit: 'abc123'
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const path = join(TEST_DIR, 'manifest.json');
|
|
138
|
+
await writeManifest(path, manifest);
|
|
139
|
+
|
|
140
|
+
const dependency1: DependencyEntry = {
|
|
141
|
+
id: '550e8400-e29b-41d4-a716-446655440000',
|
|
142
|
+
url: 'https://github.com/microsoft/TypeScript',
|
|
143
|
+
type: 'reference-implementation',
|
|
144
|
+
accessMethod: 'github-api',
|
|
145
|
+
name: 'TypeScript',
|
|
146
|
+
currentStateHash: 'sha256:abc123',
|
|
147
|
+
detectionMethod: 'manual',
|
|
148
|
+
detectionConfidence: 1.0,
|
|
149
|
+
detectedAt: new Date().toISOString(),
|
|
150
|
+
lastChecked: new Date().toISOString(),
|
|
151
|
+
auth: undefined,
|
|
152
|
+
referencedIn: []
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const dependency2: DependencyEntry = {
|
|
156
|
+
...dependency1,
|
|
157
|
+
id: '660e8400-e29b-41d4-a716-446655440001'
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
await addDependency(path, dependency1);
|
|
161
|
+
await expect(addDependency(path, dependency2)).rejects.toThrow(/already exists/);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('updateDependency', () => {
|
|
166
|
+
it('should update a dependency', async () => {
|
|
167
|
+
const manifest = createEmptyManifest({
|
|
168
|
+
owner: 'test',
|
|
169
|
+
name: 'repo',
|
|
170
|
+
branch: 'main',
|
|
171
|
+
commit: 'abc123'
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const path = join(TEST_DIR, 'manifest.json');
|
|
175
|
+
await writeManifest(path, manifest);
|
|
176
|
+
|
|
177
|
+
const dependency: DependencyEntry = {
|
|
178
|
+
id: '550e8400-e29b-41d4-a716-446655440000',
|
|
179
|
+
url: 'https://github.com/microsoft/TypeScript',
|
|
180
|
+
type: 'reference-implementation',
|
|
181
|
+
accessMethod: 'github-api',
|
|
182
|
+
name: 'TypeScript',
|
|
183
|
+
currentStateHash: 'sha256:abc123',
|
|
184
|
+
detectionMethod: 'manual',
|
|
185
|
+
detectionConfidence: 1.0,
|
|
186
|
+
detectedAt: new Date().toISOString(),
|
|
187
|
+
lastChecked: new Date().toISOString(),
|
|
188
|
+
auth: undefined,
|
|
189
|
+
referencedIn: []
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
await addDependency(path, dependency);
|
|
193
|
+
|
|
194
|
+
const updated = await updateDependency(path, dependency.id, {
|
|
195
|
+
currentVersion: '5.9.3',
|
|
196
|
+
currentStateHash: 'sha256:newHash'
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(updated.dependencies[0].currentVersion).toBe('5.9.3');
|
|
200
|
+
expect(updated.dependencies[0].currentStateHash).toBe('sha256:newHash');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should throw error for non-existent dependency', async () => {
|
|
204
|
+
const manifest = createEmptyManifest({
|
|
205
|
+
owner: 'test',
|
|
206
|
+
name: 'repo',
|
|
207
|
+
branch: 'main',
|
|
208
|
+
commit: 'abc123'
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const path = join(TEST_DIR, 'manifest.json');
|
|
212
|
+
await writeManifest(path, manifest);
|
|
213
|
+
|
|
214
|
+
await expect(
|
|
215
|
+
updateDependency(path, '550e8400-e29b-41d4-a716-446655440000', {
|
|
216
|
+
currentVersion: '5.9.3'
|
|
217
|
+
})
|
|
218
|
+
).rejects.toThrow(/not found/);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('removeDependency', () => {
|
|
223
|
+
it('should remove a dependency', async () => {
|
|
224
|
+
const manifest = createEmptyManifest({
|
|
225
|
+
owner: 'test',
|
|
226
|
+
name: 'repo',
|
|
227
|
+
branch: 'main',
|
|
228
|
+
commit: 'abc123'
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const path = join(TEST_DIR, 'manifest.json');
|
|
232
|
+
await writeManifest(path, manifest);
|
|
233
|
+
|
|
234
|
+
const dependency: DependencyEntry = {
|
|
235
|
+
id: '550e8400-e29b-41d4-a716-446655440000',
|
|
236
|
+
url: 'https://github.com/microsoft/TypeScript',
|
|
237
|
+
type: 'reference-implementation',
|
|
238
|
+
accessMethod: 'github-api',
|
|
239
|
+
name: 'TypeScript',
|
|
240
|
+
currentStateHash: 'sha256:abc123',
|
|
241
|
+
detectionMethod: 'manual',
|
|
242
|
+
detectionConfidence: 1.0,
|
|
243
|
+
detectedAt: new Date().toISOString(),
|
|
244
|
+
lastChecked: new Date().toISOString(),
|
|
245
|
+
auth: undefined,
|
|
246
|
+
referencedIn: []
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
await addDependency(path, dependency);
|
|
250
|
+
const updated = await removeDependency(path, dependency.id);
|
|
251
|
+
|
|
252
|
+
expect(updated.dependencies).toHaveLength(0);
|
|
253
|
+
expect(updated.statistics.totalDependencies).toBe(0);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('mergeManifests', () => {
|
|
258
|
+
it('should merge two manifests preserving manual entries', () => {
|
|
259
|
+
const existing = createEmptyManifest({
|
|
260
|
+
owner: 'test',
|
|
261
|
+
name: 'repo',
|
|
262
|
+
branch: 'main',
|
|
263
|
+
commit: 'abc123'
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const manualDep: DependencyEntry = {
|
|
267
|
+
id: '550e8400-e29b-41d4-a716-446655440000',
|
|
268
|
+
url: 'https://github.com/manual/dep',
|
|
269
|
+
type: 'documentation',
|
|
270
|
+
accessMethod: 'http',
|
|
271
|
+
name: 'Manual Dependency',
|
|
272
|
+
currentStateHash: 'sha256:manual',
|
|
273
|
+
detectionMethod: 'manual',
|
|
274
|
+
detectionConfidence: 1.0,
|
|
275
|
+
detectedAt: new Date().toISOString(),
|
|
276
|
+
lastChecked: new Date().toISOString(),
|
|
277
|
+
auth: undefined,
|
|
278
|
+
referencedIn: []
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
existing.dependencies.push(manualDep);
|
|
282
|
+
|
|
283
|
+
const updated = createEmptyManifest({
|
|
284
|
+
owner: 'test',
|
|
285
|
+
name: 'repo',
|
|
286
|
+
branch: 'main',
|
|
287
|
+
commit: 'def456'
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const autoDep: DependencyEntry = {
|
|
291
|
+
id: '660e8400-e29b-41d4-a716-446655440001',
|
|
292
|
+
url: 'https://github.com/auto/dep',
|
|
293
|
+
type: 'documentation',
|
|
294
|
+
accessMethod: 'http',
|
|
295
|
+
name: 'Auto Dependency',
|
|
296
|
+
currentStateHash: 'sha256:auto',
|
|
297
|
+
detectionMethod: 'llm-analysis',
|
|
298
|
+
detectionConfidence: 0.9,
|
|
299
|
+
detectedAt: new Date().toISOString(),
|
|
300
|
+
lastChecked: new Date().toISOString(),
|
|
301
|
+
auth: undefined,
|
|
302
|
+
referencedIn: []
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
updated.dependencies.push(autoDep);
|
|
306
|
+
|
|
307
|
+
const merged = mergeManifests(existing, updated);
|
|
308
|
+
|
|
309
|
+
expect(merged.dependencies).toHaveLength(2);
|
|
310
|
+
expect(merged.dependencies.some((d) => d.name === 'Manual Dependency')).toBe(true);
|
|
311
|
+
expect(merged.dependencies.some((d) => d.name === 'Auto Dependency')).toBe(true);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should preserve change history', () => {
|
|
315
|
+
const existing = createEmptyManifest({
|
|
316
|
+
owner: 'test',
|
|
317
|
+
name: 'repo',
|
|
318
|
+
branch: 'main',
|
|
319
|
+
commit: 'abc123'
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const depWithHistory: DependencyEntry = {
|
|
323
|
+
id: '550e8400-e29b-41d4-a716-446655440000',
|
|
324
|
+
url: 'https://github.com/test/dep',
|
|
325
|
+
type: 'documentation',
|
|
326
|
+
accessMethod: 'http',
|
|
327
|
+
name: 'Test Dependency',
|
|
328
|
+
currentStateHash: 'sha256:old',
|
|
329
|
+
detectionMethod: 'llm-analysis',
|
|
330
|
+
detectionConfidence: 1.0,
|
|
331
|
+
detectedAt: new Date().toISOString(),
|
|
332
|
+
lastChecked: new Date().toISOString(),
|
|
333
|
+
auth: undefined,
|
|
334
|
+
referencedIn: [],
|
|
335
|
+
changeHistory: [
|
|
336
|
+
{
|
|
337
|
+
detectedAt: new Date().toISOString(),
|
|
338
|
+
severity: 'minor',
|
|
339
|
+
falsePositive: false
|
|
340
|
+
}
|
|
341
|
+
]
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
existing.dependencies.push(depWithHistory);
|
|
345
|
+
|
|
346
|
+
const updated = createEmptyManifest({
|
|
347
|
+
owner: 'test',
|
|
348
|
+
name: 'repo',
|
|
349
|
+
branch: 'main',
|
|
350
|
+
commit: 'def456'
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const updatedDep: DependencyEntry = {
|
|
354
|
+
...depWithHistory,
|
|
355
|
+
currentStateHash: 'sha256:new',
|
|
356
|
+
changeHistory: []
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
updated.dependencies.push(updatedDep);
|
|
360
|
+
|
|
361
|
+
const merged = mergeManifests(existing, updated);
|
|
362
|
+
|
|
363
|
+
expect(merged.dependencies[0].changeHistory).toHaveLength(1);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe('createEmptyManifest', () => {
|
|
368
|
+
it('should create a valid empty manifest', () => {
|
|
369
|
+
const manifest = createEmptyManifest({
|
|
370
|
+
owner: 'test',
|
|
371
|
+
name: 'repo',
|
|
372
|
+
branch: 'main',
|
|
373
|
+
commit: 'abc123'
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
expect(manifest.version).toBe('1.0.0');
|
|
377
|
+
expect(manifest.dependencies).toHaveLength(0);
|
|
378
|
+
expect(manifest.statistics.totalDependencies).toBe(0);
|
|
379
|
+
expect(manifest.repository.owner).toBe('test');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('should accept optional parameters', () => {
|
|
383
|
+
const manifest = createEmptyManifest({
|
|
384
|
+
owner: 'test',
|
|
385
|
+
name: 'repo',
|
|
386
|
+
branch: 'main',
|
|
387
|
+
commit: 'abc123',
|
|
388
|
+
action: 'custom-action',
|
|
389
|
+
version: '2.0.0',
|
|
390
|
+
llmProvider: 'claude',
|
|
391
|
+
llmModel: 'claude-3'
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
expect(manifest.generatedBy.action).toBe('custom-action');
|
|
395
|
+
expect(manifest.generatedBy.version).toBe('2.0.0');
|
|
396
|
+
expect(manifest.generatedBy.llmProvider).toBe('claude');
|
|
397
|
+
expect(manifest.generatedBy.llmModel).toBe('claude-3');
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
});
|
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { type DependencyManifest, type DependencyEntry } from './schema.js';
|
|
4
|
+
import { validateManifest, validateDependencyEntry } from './validator.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Read and parse a manifest file
|
|
8
|
+
*/
|
|
9
|
+
export async function readManifest(path: string): Promise<DependencyManifest> {
|
|
10
|
+
const content = await readFile(path, 'utf-8');
|
|
11
|
+
const data = JSON.parse(content);
|
|
12
|
+
return validateManifest(data);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Write a manifest to file
|
|
17
|
+
*/
|
|
18
|
+
export async function writeManifest(path: string, manifest: DependencyManifest): Promise<void> {
|
|
19
|
+
// Validate before writing
|
|
20
|
+
validateManifest(manifest);
|
|
21
|
+
|
|
22
|
+
// Ensure directory exists
|
|
23
|
+
await mkdir(dirname(path), { recursive: true });
|
|
24
|
+
|
|
25
|
+
// Write formatted JSON
|
|
26
|
+
const content = JSON.stringify(manifest, null, 2);
|
|
27
|
+
await writeFile(path, content, 'utf-8');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Update a dependency entry in the manifest
|
|
32
|
+
*/
|
|
33
|
+
export async function updateDependency(
|
|
34
|
+
path: string,
|
|
35
|
+
dependencyId: string,
|
|
36
|
+
updates: Partial<DependencyEntry>
|
|
37
|
+
): Promise<DependencyManifest> {
|
|
38
|
+
const manifest = await readManifest(path);
|
|
39
|
+
|
|
40
|
+
const dep = manifest.dependencies.find((d) => d.id === dependencyId);
|
|
41
|
+
if (!dep) {
|
|
42
|
+
throw new Error(`Dependency with id ${dependencyId} not found`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Update the dependency in place
|
|
46
|
+
Object.assign(dep, updates);
|
|
47
|
+
|
|
48
|
+
// Validate the merged dependency
|
|
49
|
+
validateDependencyEntry(dep);
|
|
50
|
+
|
|
51
|
+
// Update statistics
|
|
52
|
+
manifest.statistics = calculateStatistics(manifest.dependencies);
|
|
53
|
+
|
|
54
|
+
// Write back
|
|
55
|
+
await writeManifest(path, manifest);
|
|
56
|
+
|
|
57
|
+
return manifest;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Add a new dependency to the manifest
|
|
62
|
+
*/
|
|
63
|
+
export async function addDependency(
|
|
64
|
+
path: string,
|
|
65
|
+
dependency: DependencyEntry
|
|
66
|
+
): Promise<DependencyManifest> {
|
|
67
|
+
const manifest = await readManifest(path);
|
|
68
|
+
|
|
69
|
+
// Check for duplicates by ID or URL
|
|
70
|
+
const existingById = manifest.dependencies.find((dep) => dep.id === dependency.id);
|
|
71
|
+
const existingByUrl = manifest.dependencies.find((dep) => dep.url === dependency.url);
|
|
72
|
+
|
|
73
|
+
if (existingById) {
|
|
74
|
+
throw new Error(`Dependency with id ${dependency.id} already exists`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (existingByUrl) {
|
|
78
|
+
throw new Error(`Dependency with url ${dependency.url} already exists`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Add dependency
|
|
82
|
+
manifest.dependencies.push(dependency);
|
|
83
|
+
|
|
84
|
+
// Update statistics
|
|
85
|
+
manifest.statistics = calculateStatistics(manifest.dependencies);
|
|
86
|
+
|
|
87
|
+
// Write back
|
|
88
|
+
await writeManifest(path, manifest);
|
|
89
|
+
|
|
90
|
+
return manifest;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Remove a dependency from the manifest
|
|
95
|
+
*/
|
|
96
|
+
export async function removeDependency(
|
|
97
|
+
path: string,
|
|
98
|
+
dependencyId: string
|
|
99
|
+
): Promise<DependencyManifest> {
|
|
100
|
+
const manifest = await readManifest(path);
|
|
101
|
+
|
|
102
|
+
const index = manifest.dependencies.findIndex((dep) => dep.id === dependencyId);
|
|
103
|
+
if (index === -1) {
|
|
104
|
+
throw new Error(`Dependency with id ${dependencyId} not found`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Remove dependency
|
|
108
|
+
manifest.dependencies.splice(index, 1);
|
|
109
|
+
|
|
110
|
+
// Update statistics
|
|
111
|
+
manifest.statistics = calculateStatistics(manifest.dependencies);
|
|
112
|
+
|
|
113
|
+
// Write back
|
|
114
|
+
await writeManifest(path, manifest);
|
|
115
|
+
|
|
116
|
+
return manifest;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Merge two manifests, preserving manual entries
|
|
121
|
+
* Manual entries are those with detectionMethod === 'manual'
|
|
122
|
+
*/
|
|
123
|
+
export function mergeManifests(
|
|
124
|
+
existing: DependencyManifest,
|
|
125
|
+
updated: DependencyManifest,
|
|
126
|
+
options: {
|
|
127
|
+
preserveManual?: boolean;
|
|
128
|
+
preserveHistory?: boolean;
|
|
129
|
+
} = {}
|
|
130
|
+
): DependencyManifest {
|
|
131
|
+
const { preserveManual = true, preserveHistory = true } = options;
|
|
132
|
+
|
|
133
|
+
// Create a deep copy of the updated manifest to avoid mutations
|
|
134
|
+
const merged: DependencyManifest = {
|
|
135
|
+
...updated,
|
|
136
|
+
dependencies: updated.dependencies.map((dep) => ({
|
|
137
|
+
...dep,
|
|
138
|
+
changeHistory: dep.changeHistory ? [...dep.changeHistory] : [],
|
|
139
|
+
referencedIn: dep.referencedIn ? [...dep.referencedIn] : []
|
|
140
|
+
}))
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
if (preserveManual) {
|
|
144
|
+
// Find manual entries in existing manifest
|
|
145
|
+
const manualEntries = existing.dependencies.filter((dep) => dep.detectionMethod === 'manual');
|
|
146
|
+
|
|
147
|
+
// Add manual entries that aren't in the updated manifest
|
|
148
|
+
for (const manualEntry of manualEntries) {
|
|
149
|
+
const existsInUpdated = merged.dependencies.some(
|
|
150
|
+
(dep) => dep.id === manualEntry.id || dep.url === manualEntry.url
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
if (!existsInUpdated) {
|
|
154
|
+
merged.dependencies.push({
|
|
155
|
+
...manualEntry,
|
|
156
|
+
changeHistory: manualEntry.changeHistory ? [...manualEntry.changeHistory] : [],
|
|
157
|
+
referencedIn: manualEntry.referencedIn ? [...manualEntry.referencedIn] : []
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (preserveHistory) {
|
|
164
|
+
// Preserve change history for matching dependencies
|
|
165
|
+
merged.dependencies = merged.dependencies.map((dep) => {
|
|
166
|
+
const existingDep = existing.dependencies.find((d) => d.id === dep.id || d.url === dep.url);
|
|
167
|
+
|
|
168
|
+
if (existingDep && existingDep.changeHistory && existingDep.changeHistory.length > 0) {
|
|
169
|
+
return {
|
|
170
|
+
...dep,
|
|
171
|
+
changeHistory: [...existingDep.changeHistory, ...(dep.changeHistory || [])]
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return dep;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Recalculate statistics
|
|
180
|
+
merged.statistics = calculateStatistics(merged.dependencies);
|
|
181
|
+
|
|
182
|
+
return merged;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Calculate statistics for a list of dependencies
|
|
187
|
+
*/
|
|
188
|
+
function calculateStatistics(dependencies: DependencyEntry[]): DependencyManifest['statistics'] {
|
|
189
|
+
const byType: Record<string, number> = {};
|
|
190
|
+
const byAccessMethod: Record<string, number> = {};
|
|
191
|
+
const byDetectionMethod: Record<string, number> = {};
|
|
192
|
+
let totalConfidence = 0;
|
|
193
|
+
let falsePositiveCount = 0;
|
|
194
|
+
let totalChangeCount = 0;
|
|
195
|
+
|
|
196
|
+
for (const dep of dependencies) {
|
|
197
|
+
byType[dep.type] = (byType[dep.type] || 0) + 1;
|
|
198
|
+
byAccessMethod[dep.accessMethod] = (byAccessMethod[dep.accessMethod] || 0) + 1;
|
|
199
|
+
byDetectionMethod[dep.detectionMethod] = (byDetectionMethod[dep.detectionMethod] || 0) + 1;
|
|
200
|
+
totalConfidence += dep.detectionConfidence;
|
|
201
|
+
|
|
202
|
+
// Count false positives in change history
|
|
203
|
+
const changeHistory = dep.changeHistory || [];
|
|
204
|
+
const fpCount = changeHistory.filter((change) => change.falsePositive).length;
|
|
205
|
+
falsePositiveCount += fpCount;
|
|
206
|
+
totalChangeCount += changeHistory.length;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const averageConfidence = dependencies.length > 0 ? totalConfidence / dependencies.length : 0;
|
|
210
|
+
|
|
211
|
+
const falsePositiveRate =
|
|
212
|
+
totalChangeCount > 0 ? falsePositiveCount / totalChangeCount : undefined;
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
totalDependencies: dependencies.length,
|
|
216
|
+
byType,
|
|
217
|
+
byAccessMethod,
|
|
218
|
+
byDetectionMethod,
|
|
219
|
+
averageConfidence,
|
|
220
|
+
falsePositiveRate
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Create an empty manifest template
|
|
226
|
+
*/
|
|
227
|
+
export function createEmptyManifest(options: {
|
|
228
|
+
owner: string;
|
|
229
|
+
name: string;
|
|
230
|
+
branch: string;
|
|
231
|
+
commit: string;
|
|
232
|
+
action?: string;
|
|
233
|
+
version?: string;
|
|
234
|
+
llmProvider?: string;
|
|
235
|
+
llmModel?: string;
|
|
236
|
+
}): DependencyManifest {
|
|
237
|
+
return {
|
|
238
|
+
version: '1.0.0',
|
|
239
|
+
generatedAt: new Date().toISOString(),
|
|
240
|
+
generatedBy: {
|
|
241
|
+
action: options.action || 'dependabit',
|
|
242
|
+
version: options.version || '0.1.0',
|
|
243
|
+
llmProvider: options.llmProvider || 'github-copilot',
|
|
244
|
+
llmModel: options.llmModel
|
|
245
|
+
},
|
|
246
|
+
repository: {
|
|
247
|
+
owner: options.owner,
|
|
248
|
+
name: options.name,
|
|
249
|
+
branch: options.branch,
|
|
250
|
+
commit: options.commit
|
|
251
|
+
},
|
|
252
|
+
dependencies: [],
|
|
253
|
+
statistics: {
|
|
254
|
+
totalDependencies: 0,
|
|
255
|
+
byType: {},
|
|
256
|
+
byAccessMethod: {},
|
|
257
|
+
byDetectionMethod: {},
|
|
258
|
+
averageConfidence: 0
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
}
|