@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.
@@ -0,0 +1,246 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ checkManifestSize,
4
+ formatSize,
5
+ validateManifestObject,
6
+ estimateEntrySize,
7
+ canAddEntry
8
+ } from './size-check.js';
9
+
10
+ describe('checkManifestSize', () => {
11
+ it('should return ok status for small content', () => {
12
+ const content = 'small content';
13
+ const result = checkManifestSize(content);
14
+
15
+ expect(result.status).toBe('ok');
16
+ expect(result.message).toBeUndefined();
17
+ });
18
+
19
+ it('should return warning status when exceeding warn threshold', () => {
20
+ const content = Buffer.alloc(1.5 * 1024 * 1024); // 1.5 MB
21
+ const result = checkManifestSize(content);
22
+
23
+ expect(result.status).toBe('warning');
24
+ expect(result.message).toContain('approaching limit');
25
+ });
26
+
27
+ it('should return error status when exceeding error threshold', () => {
28
+ const content = Buffer.alloc(11 * 1024 * 1024); // 11 MB
29
+ const result = checkManifestSize(content);
30
+
31
+ expect(result.status).toBe('error');
32
+ expect(result.message).toContain('exceeds maximum limit');
33
+ });
34
+
35
+ it('should handle exact threshold values', () => {
36
+ const exactWarnSize = 1 * 1024 * 1024; // Exactly 1 MB
37
+ const result = checkManifestSize(Buffer.alloc(exactWarnSize));
38
+
39
+ expect(result.status).toBe('warning');
40
+ });
41
+
42
+ it('should support custom thresholds', () => {
43
+ const content = Buffer.alloc(2.5 * 1024 * 1024); // 2.5 MB
44
+ const result = checkManifestSize(content, {
45
+ warnThreshold: 2,
46
+ errorThreshold: 5
47
+ });
48
+
49
+ expect(result.status).toBe('warning');
50
+ });
51
+
52
+ it('should handle empty content', () => {
53
+ const result = checkManifestSize('');
54
+
55
+ expect(result.status).toBe('ok');
56
+ expect(result.sizeBytes).toBe(0);
57
+ expect(result.sizeMB).toBe(0);
58
+ });
59
+
60
+ it('should handle Buffer input', () => {
61
+ const buffer = Buffer.from('test content');
62
+ const result = checkManifestSize(buffer);
63
+
64
+ expect(result.sizeBytes).toBe(buffer.length);
65
+ expect(result.status).toBe('ok');
66
+ });
67
+
68
+ it('should handle string input with multi-byte characters', () => {
69
+ const content = '你好世界🌍'; // Multi-byte UTF-8 characters
70
+ const result = checkManifestSize(content);
71
+
72
+ expect(result.sizeBytes).toBe(Buffer.byteLength(content, 'utf8'));
73
+ });
74
+ });
75
+
76
+ describe('formatSize', () => {
77
+ it('should format bytes correctly', () => {
78
+ expect(formatSize(512)).toBe('512 B');
79
+ });
80
+
81
+ it('should format kilobytes correctly', () => {
82
+ expect(formatSize(1024)).toBe('1.00 KB');
83
+ expect(formatSize(5 * 1024)).toBe('5.00 KB');
84
+ });
85
+
86
+ it('should format megabytes correctly', () => {
87
+ expect(formatSize(1024 * 1024)).toBe('1.00 MB');
88
+ expect(formatSize(2.5 * 1024 * 1024)).toBe('2.50 MB');
89
+ });
90
+
91
+ it('should handle zero', () => {
92
+ expect(formatSize(0)).toBe('0 B');
93
+ });
94
+
95
+ it('should handle large values', () => {
96
+ expect(formatSize(100 * 1024 * 1024)).toBe('100.00 MB');
97
+ });
98
+ });
99
+
100
+ describe('validateManifestObject', () => {
101
+ it('should validate small manifest objects', () => {
102
+ const manifest = { dependencies: [{ id: '1', name: 'test' }] };
103
+ const result = validateManifestObject(manifest);
104
+
105
+ expect(result.status).toBe('ok');
106
+ });
107
+
108
+ it('should detect large manifest objects', () => {
109
+ const largeDeps = Array.from({ length: 10000 }, (_, i) => ({
110
+ id: `dep-${i}`,
111
+ name: `dependency-${i}`,
112
+ url: `https://github.com/org/repo-${i}`,
113
+ description: 'A'.repeat(100)
114
+ }));
115
+
116
+ const manifest = { dependencies: largeDeps };
117
+ const result = validateManifestObject(manifest);
118
+
119
+ expect(result.status).not.toBe('ok');
120
+ });
121
+
122
+ it('should handle empty objects', () => {
123
+ const result = validateManifestObject({});
124
+
125
+ expect(result.status).toBe('ok');
126
+ });
127
+
128
+ it('should support custom thresholds', () => {
129
+ const manifest = { data: 'x'.repeat(500 * 1024) }; // ~500 KB
130
+ const result = validateManifestObject(manifest, {
131
+ warnThreshold: 0.4,
132
+ errorThreshold: 1
133
+ });
134
+
135
+ expect(result.status).toBe('warning');
136
+ });
137
+ });
138
+
139
+ describe('estimateEntrySize', () => {
140
+ it('should estimate size of simple entries', () => {
141
+ const entry = { id: '1', name: 'test' };
142
+ const size = estimateEntrySize(entry);
143
+
144
+ expect(size).toBeGreaterThan(0);
145
+ expect(size).toBe(JSON.stringify(entry).length);
146
+ });
147
+
148
+ it('should estimate size of complex entries', () => {
149
+ const entry = {
150
+ id: '1',
151
+ name: 'complex-dependency',
152
+ metadata: {
153
+ version: '1.0.0',
154
+ tags: ['tag1', 'tag2', 'tag3'],
155
+ description: 'A long description with multiple words'
156
+ }
157
+ };
158
+
159
+ const size = estimateEntrySize(entry);
160
+ expect(size).toBeGreaterThan(50);
161
+ });
162
+
163
+ it('should handle null', () => {
164
+ expect(estimateEntrySize(null)).toBe(4); // "null"
165
+ });
166
+
167
+ it('should handle arrays', () => {
168
+ const entry = [1, 2, 3];
169
+ const size = estimateEntrySize(entry);
170
+ expect(size).toBe(JSON.stringify(entry).length);
171
+ });
172
+ });
173
+
174
+ describe('canAddEntry', () => {
175
+ it('should allow adding small entries to small manifests', () => {
176
+ const manifest = { dependencies: [] };
177
+ const entry = { id: '1', name: 'test' };
178
+
179
+ const result = canAddEntry(manifest, entry);
180
+
181
+ expect(result.canAdd).toBe(true);
182
+ expect(result.currentSize.status).toBe('ok');
183
+ });
184
+
185
+ it('should prevent adding entries when it would exceed limit', () => {
186
+ // Create a manifest that's already close to the limit
187
+ const largeDeps = Array.from({ length: 10000 }, (_, i) => ({
188
+ id: `dep-${i}`,
189
+ url: `https://example.com/${i}`,
190
+ description: 'A'.repeat(800) // ~800 bytes each, total ~8MB
191
+ }));
192
+
193
+ const manifest = { dependencies: largeDeps };
194
+ const newEntry = {
195
+ id: 'new-dep',
196
+ url: 'https://example.com/new',
197
+ description: 'B'.repeat(3000000) // Very large entry ~3MB
198
+ };
199
+
200
+ const result = canAddEntry(manifest, newEntry, {
201
+ errorThreshold: 10
202
+ });
203
+
204
+ expect(result.canAdd).toBe(false);
205
+ expect(result.estimatedSize.message).toContain('exceed size limit');
206
+ });
207
+
208
+ it('should show warning status when approaching limit', () => {
209
+ const deps = Array.from({ length: 1000 }, (_, i) => ({
210
+ id: `dep-${i}`,
211
+ data: 'x'.repeat(900) // Each ~900 bytes
212
+ }));
213
+
214
+ const manifest = { dependencies: deps };
215
+ const entry = { id: 'new', data: 'y'.repeat(100) };
216
+
217
+ const result = canAddEntry(manifest, entry, {
218
+ warnThreshold: 0.8,
219
+ errorThreshold: 10
220
+ });
221
+
222
+ expect(result.canAdd).toBe(true);
223
+ expect(result.estimatedSize.status).toBe('warning');
224
+ });
225
+
226
+ it('should handle adding entry to empty manifest', () => {
227
+ const manifest = {};
228
+ const entry = { id: '1', name: 'first' };
229
+
230
+ const result = canAddEntry(manifest, entry);
231
+
232
+ expect(result.canAdd).toBe(true);
233
+ });
234
+
235
+ it('should account for JSON formatting overhead', () => {
236
+ const manifest = { dependencies: [{ id: '1' }] };
237
+ const entry = { id: '2' };
238
+
239
+ const result = canAddEntry(manifest, entry);
240
+
241
+ // Estimated size should be larger than current + entry due to formatting buffer
242
+ expect(result.estimatedSize.sizeBytes).toBeGreaterThan(
243
+ result.currentSize.sizeBytes + estimateEntrySize(entry)
244
+ );
245
+ });
246
+ });
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Manifest size validation and warnings
3
+ * Checks manifest size and warns when approaching limits
4
+ */
5
+
6
+ export interface SizeCheckResult {
7
+ sizeBytes: number;
8
+ sizeMB: number;
9
+ status: 'ok' | 'warning' | 'error';
10
+ message?: string | undefined;
11
+ }
12
+
13
+ export interface SizeCheckOptions {
14
+ warnThreshold?: number; // MB (default: 1)
15
+ errorThreshold?: number; // MB (default: 10)
16
+ }
17
+
18
+ /**
19
+ * Check manifest size and return status
20
+ */
21
+ export function checkManifestSize(
22
+ content: string | Buffer,
23
+ options?: SizeCheckOptions
24
+ ): SizeCheckResult {
25
+ const warnThreshold = (options?.warnThreshold ?? 1) * 1024 * 1024; // Convert MB to bytes
26
+ const errorThreshold = (options?.errorThreshold ?? 10) * 1024 * 1024;
27
+
28
+ const sizeBytes =
29
+ typeof content === 'string' ? Buffer.byteLength(content, 'utf8') : content.length;
30
+
31
+ const sizeMB = sizeBytes / (1024 * 1024);
32
+
33
+ if (sizeBytes >= errorThreshold) {
34
+ return {
35
+ sizeBytes,
36
+ sizeMB,
37
+ status: 'error',
38
+ message: `Manifest size (${sizeMB.toFixed(2)}MB) exceeds maximum limit of ${(errorThreshold / 1024 / 1024).toFixed(0)}MB. Consider splitting or pruning data.`
39
+ };
40
+ }
41
+
42
+ if (sizeBytes >= warnThreshold) {
43
+ return {
44
+ sizeBytes,
45
+ sizeMB,
46
+ status: 'warning',
47
+ message: `Manifest size (${sizeMB.toFixed(2)}MB) is approaching limit. Warning threshold: ${(warnThreshold / 1024 / 1024).toFixed(0)}MB, Max: ${(errorThreshold / 1024 / 1024).toFixed(0)}MB.`
48
+ };
49
+ }
50
+
51
+ return {
52
+ sizeBytes,
53
+ sizeMB,
54
+ status: 'ok'
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Get formatted size string
60
+ */
61
+ export function formatSize(bytes: number): string {
62
+ if (bytes < 1024) {
63
+ return `${bytes} B`;
64
+ }
65
+ if (bytes < 1024 * 1024) {
66
+ return `${(bytes / 1024).toFixed(2)} KB`;
67
+ }
68
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
69
+ }
70
+
71
+ /**
72
+ * Validate manifest object size before serialization
73
+ */
74
+ export function validateManifestObject(
75
+ manifest: unknown,
76
+ options?: SizeCheckOptions
77
+ ): SizeCheckResult {
78
+ const serialized = JSON.stringify(manifest, null, 2);
79
+ return checkManifestSize(serialized, options);
80
+ }
81
+
82
+ /**
83
+ * Estimate manifest size impact of adding an entry
84
+ */
85
+ export function estimateEntrySize(entry: unknown): number {
86
+ const serialized = JSON.stringify(entry);
87
+ return Buffer.byteLength(serialized, 'utf8');
88
+ }
89
+
90
+ /**
91
+ * Check if adding an entry would exceed limits
92
+ */
93
+ export function canAddEntry(
94
+ currentManifest: unknown,
95
+ newEntry: unknown,
96
+ options?: SizeCheckOptions
97
+ ): {
98
+ canAdd: boolean;
99
+ currentSize: SizeCheckResult;
100
+ estimatedSize: SizeCheckResult;
101
+ } {
102
+ const currentSize = validateManifestObject(currentManifest, options);
103
+ const entrySize = estimateEntrySize(newEntry);
104
+
105
+ // Estimate new size (accounting for JSON formatting)
106
+ const estimatedBytes = currentSize.sizeBytes + entrySize + 100; // Add buffer for formatting
107
+ const estimatedSizeMB = estimatedBytes / (1024 * 1024);
108
+
109
+ const errorThreshold = (options?.errorThreshold ?? 10) * 1024 * 1024;
110
+ const canAdd = estimatedBytes < errorThreshold;
111
+
112
+ const estimatedResult = checkManifestSize(Buffer.alloc(estimatedBytes), options);
113
+
114
+ return {
115
+ canAdd,
116
+ currentSize,
117
+ estimatedSize: {
118
+ ...estimatedResult,
119
+ message: canAdd
120
+ ? estimatedResult.message
121
+ : `Adding entry would exceed size limit (estimated: ${estimatedSizeMB.toFixed(2)}MB)`
122
+ }
123
+ };
124
+ }
@@ -0,0 +1,161 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ validateManifest,
4
+ validateDependencyEntry,
5
+ validateConfig,
6
+ safeValidateManifest,
7
+ ValidationError
8
+ } from '../src/validator.js';
9
+
10
+ describe('Validator Tests', () => {
11
+ describe('validateManifest', () => {
12
+ it('should validate a valid manifest', () => {
13
+ const manifest = {
14
+ version: '1.0.0',
15
+ generatedAt: '2026-01-29T10:30:00Z',
16
+ generatedBy: {
17
+ action: 'dependabit',
18
+ version: '1.0.0',
19
+ llmProvider: 'github-copilot'
20
+ },
21
+ repository: {
22
+ owner: 'pradeepmouli',
23
+ name: 'dependabit',
24
+ branch: 'main',
25
+ commit: 'abc123'
26
+ },
27
+ dependencies: [],
28
+ statistics: {
29
+ totalDependencies: 0,
30
+ byType: {},
31
+ byAccessMethod: {},
32
+ byDetectionMethod: {},
33
+ averageConfidence: 0
34
+ }
35
+ };
36
+
37
+ const result = validateManifest(manifest);
38
+ expect(result.version).toBe('1.0.0');
39
+ });
40
+
41
+ it('should throw ValidationError for invalid manifest', () => {
42
+ const invalid = {
43
+ version: '2.0.0'
44
+ };
45
+
46
+ expect(() => validateManifest(invalid)).toThrow(ValidationError);
47
+ });
48
+
49
+ it('should provide formatted error messages', () => {
50
+ const invalid = {
51
+ version: '1.0.0'
52
+ };
53
+
54
+ try {
55
+ validateManifest(invalid);
56
+ expect.fail('Should have thrown');
57
+ } catch (error) {
58
+ expect(error).toBeInstanceOf(ValidationError);
59
+ const validationError = error as ValidationError;
60
+ const formatted = validationError.getFormattedErrors();
61
+ expect(formatted.length).toBeGreaterThan(0);
62
+ }
63
+ });
64
+ });
65
+
66
+ describe('validateDependencyEntry', () => {
67
+ it('should validate a valid dependency entry', () => {
68
+ const entry = {
69
+ id: '550e8400-e29b-41d4-a716-446655440000',
70
+ url: 'https://github.com/microsoft/TypeScript',
71
+ type: 'reference-implementation',
72
+ accessMethod: 'github-api',
73
+ name: 'TypeScript',
74
+ currentStateHash: 'sha256:abc123',
75
+ detectionMethod: 'manual',
76
+ detectionConfidence: 1.0,
77
+ detectedAt: '2026-01-29T10:30:00Z',
78
+ lastChecked: '2026-01-29T10:30:00Z',
79
+ referencedIn: []
80
+ };
81
+
82
+ const result = validateDependencyEntry(entry);
83
+ expect(result.name).toBe('TypeScript');
84
+ });
85
+
86
+ it('should throw ValidationError for invalid entry', () => {
87
+ const invalid = {
88
+ url: 'https://example.com'
89
+ };
90
+
91
+ expect(() => validateDependencyEntry(invalid)).toThrow(ValidationError);
92
+ });
93
+ });
94
+
95
+ describe('validateConfig', () => {
96
+ it('should validate a valid config', () => {
97
+ const config = {
98
+ version: '1',
99
+ schedule: {
100
+ interval: 'daily',
101
+ timezone: 'UTC'
102
+ }
103
+ };
104
+
105
+ const result = validateConfig(config);
106
+ expect(result.version).toBe('1');
107
+ });
108
+
109
+ it('should throw ValidationError for invalid config', () => {
110
+ const invalid = {
111
+ version: '2'
112
+ };
113
+
114
+ expect(() => validateConfig(invalid)).toThrow(ValidationError);
115
+ });
116
+ });
117
+
118
+ describe('safeValidateManifest', () => {
119
+ it('should return success for valid manifest', () => {
120
+ const manifest = {
121
+ version: '1.0.0',
122
+ generatedAt: '2026-01-29T10:30:00Z',
123
+ generatedBy: {
124
+ action: 'dependabit',
125
+ version: '1.0.0',
126
+ llmProvider: 'github-copilot'
127
+ },
128
+ repository: {
129
+ owner: 'pradeepmouli',
130
+ name: 'dependabit',
131
+ branch: 'main',
132
+ commit: 'abc123'
133
+ },
134
+ dependencies: [],
135
+ statistics: {
136
+ totalDependencies: 0,
137
+ byType: {},
138
+ byAccessMethod: {},
139
+ byDetectionMethod: {},
140
+ averageConfidence: 0
141
+ }
142
+ };
143
+
144
+ const result = safeValidateManifest(manifest);
145
+ expect(result.success).toBe(true);
146
+ expect(result.data).toBeDefined();
147
+ expect(result.error).toBeUndefined();
148
+ });
149
+
150
+ it('should return error for invalid manifest', () => {
151
+ const invalid = {
152
+ version: '2.0.0'
153
+ };
154
+
155
+ const result = safeValidateManifest(invalid);
156
+ expect(result.success).toBe(false);
157
+ expect(result.data).toBeUndefined();
158
+ expect(result.error).toBeInstanceOf(ValidationError);
159
+ });
160
+ });
161
+ });
@@ -0,0 +1,131 @@
1
+ import {
2
+ DependencyManifestSchema,
3
+ DependencyEntrySchema,
4
+ DependabitConfigSchema,
5
+ type DependencyManifest,
6
+ type DependencyEntry,
7
+ type DependabitConfig
8
+ } from './schema.js';
9
+ import { ZodError } from 'zod';
10
+
11
+ /**
12
+ * Validation error class with detailed error information
13
+ */
14
+ export class ValidationError extends Error {
15
+ constructor(
16
+ message: string,
17
+ public readonly errors: ZodError
18
+ ) {
19
+ super(message);
20
+ this.name = 'ValidationError';
21
+ }
22
+
23
+ /**
24
+ * Get formatted error messages
25
+ */
26
+ getFormattedErrors(): string[] {
27
+ return this.errors.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`);
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Validate a dependency manifest
33
+ * @throws {ValidationError} if validation fails
34
+ */
35
+ export function validateManifest(data: unknown): DependencyManifest {
36
+ try {
37
+ return DependencyManifestSchema.parse(data);
38
+ } catch (error) {
39
+ if (error instanceof ZodError) {
40
+ throw new ValidationError('Manifest validation failed', error);
41
+ }
42
+ throw error;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Validate a dependency entry
48
+ * @throws {ValidationError} if validation fails
49
+ */
50
+ export function validateDependencyEntry(data: unknown): DependencyEntry {
51
+ try {
52
+ return DependencyEntrySchema.parse(data);
53
+ } catch (error) {
54
+ if (error instanceof ZodError) {
55
+ throw new ValidationError('Dependency entry validation failed', error);
56
+ }
57
+ throw error;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Validate a dependabit configuration
63
+ * @throws {ValidationError} if validation fails
64
+ */
65
+ export function validateConfig(data: unknown): DependabitConfig {
66
+ try {
67
+ return DependabitConfigSchema.parse(data);
68
+ } catch (error) {
69
+ if (error instanceof ZodError) {
70
+ throw new ValidationError('Config validation failed', error);
71
+ }
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Safe validation that returns success/error result
78
+ */
79
+ export function safeValidateManifest(data: unknown): {
80
+ success: boolean;
81
+ data?: DependencyManifest;
82
+ error?: ValidationError;
83
+ } {
84
+ try {
85
+ const validated = validateManifest(data);
86
+ return { success: true, data: validated };
87
+ } catch (error) {
88
+ if (error instanceof ValidationError) {
89
+ return { success: false, error };
90
+ }
91
+ throw error;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Safe validation for dependency entry
97
+ */
98
+ export function safeValidateDependencyEntry(data: unknown): {
99
+ success: boolean;
100
+ data?: DependencyEntry;
101
+ error?: ValidationError;
102
+ } {
103
+ try {
104
+ const validated = validateDependencyEntry(data);
105
+ return { success: true, data: validated };
106
+ } catch (error) {
107
+ if (error instanceof ValidationError) {
108
+ return { success: false, error };
109
+ }
110
+ throw error;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Safe validation for config
116
+ */
117
+ export function safeValidateConfig(data: unknown): {
118
+ success: boolean;
119
+ data?: DependabitConfig;
120
+ error?: ValidationError;
121
+ } {
122
+ try {
123
+ const validated = validateConfig(data);
124
+ return { success: true, data: validated };
125
+ } catch (error) {
126
+ if (error instanceof ValidationError) {
127
+ return { success: false, error };
128
+ }
129
+ throw error;
130
+ }
131
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"],
9
+ "references": []
10
+ }