@unrdf/kgn 5.0.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/LICENSE +21 -0
- package/README.md +210 -0
- package/package.json +90 -0
- package/src/MIGRATION_COMPLETE.md +186 -0
- package/src/PORT-MAP.md +302 -0
- package/src/base/filter-templates.js +479 -0
- package/src/base/index.js +92 -0
- package/src/base/injection-targets.js +583 -0
- package/src/base/macro-templates.js +298 -0
- package/src/base/macro-templates.js.bak +461 -0
- package/src/base/shacl-templates.js +617 -0
- package/src/base/template-base.js +388 -0
- package/src/core/attestor.js +381 -0
- package/src/core/filters.js +518 -0
- package/src/core/index.js +21 -0
- package/src/core/kgen-engine.js +372 -0
- package/src/core/parser.js +447 -0
- package/src/core/post-processor.js +313 -0
- package/src/core/renderer.js +469 -0
- package/src/doc-generator/cli.mjs +122 -0
- package/src/doc-generator/index.mjs +28 -0
- package/src/doc-generator/mdx-generator.mjs +71 -0
- package/src/doc-generator/nav-generator.mjs +136 -0
- package/src/doc-generator/parser.mjs +291 -0
- package/src/doc-generator/rdf-builder.mjs +306 -0
- package/src/doc-generator/scanner.mjs +189 -0
- package/src/engine/index.js +42 -0
- package/src/engine/pipeline.js +448 -0
- package/src/engine/renderer.js +604 -0
- package/src/engine/template-engine.js +566 -0
- package/src/filters/array.js +436 -0
- package/src/filters/data.js +479 -0
- package/src/filters/index.js +270 -0
- package/src/filters/rdf.js +264 -0
- package/src/filters/text.js +369 -0
- package/src/index.js +109 -0
- package/src/inheritance/index.js +40 -0
- package/src/injection/api.js +260 -0
- package/src/injection/atomic-writer.js +327 -0
- package/src/injection/constants.js +136 -0
- package/src/injection/idempotency-manager.js +295 -0
- package/src/injection/index.js +28 -0
- package/src/injection/injection-engine.js +378 -0
- package/src/injection/integration.js +339 -0
- package/src/injection/modes/index.js +341 -0
- package/src/injection/rollback-manager.js +373 -0
- package/src/injection/target-resolver.js +323 -0
- package/src/injection/tests/atomic-writer.test.js +382 -0
- package/src/injection/tests/injection-engine.test.js +611 -0
- package/src/injection/tests/integration.test.js +392 -0
- package/src/injection/tests/run-tests.js +283 -0
- package/src/injection/validation-engine.js +547 -0
- package/src/linter/determinism-linter.js +473 -0
- package/src/linter/determinism.js +410 -0
- package/src/linter/index.js +6 -0
- package/src/linter/test-doubles.js +475 -0
- package/src/parser/frontmatter.js +228 -0
- package/src/parser/variables.js +344 -0
- package/src/renderer/deterministic.js +245 -0
- package/src/renderer/index.js +6 -0
- package/src/templates/latex/academic-paper.njk +186 -0
- package/src/templates/latex/index.js +104 -0
- package/src/templates/nextjs/app-page.njk +66 -0
- package/src/templates/nextjs/index.js +80 -0
- package/src/templates/office/docx/document.njk +368 -0
- package/src/templates/office/index.js +79 -0
- package/src/templates/office/word-report.njk +129 -0
- package/src/utils/template-utils.js +426 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KGEN Validation Engine
|
|
3
|
+
*
|
|
4
|
+
* Provides comprehensive validation for injection targets and content
|
|
5
|
+
* including syntax, semantics, encoding, and security checks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { promises as fs } from 'fs';
|
|
9
|
+
import { basename, extname } from 'path';
|
|
10
|
+
import { createHash } from 'crypto';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
ERROR_CODES,
|
|
14
|
+
VALIDATION_RULES,
|
|
15
|
+
BINARY_PATTERNS,
|
|
16
|
+
CONTENT_PATTERNS,
|
|
17
|
+
ENCODINGS,
|
|
18
|
+
CHECKSUM_ALGORITHMS
|
|
19
|
+
} from './constants.js';
|
|
20
|
+
|
|
21
|
+
export class ValidationEngine {
|
|
22
|
+
constructor(config = {}) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.validationCache = new Map();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validate injection target
|
|
29
|
+
*/
|
|
30
|
+
async validateTarget(target) {
|
|
31
|
+
const cacheKey = this._getCacheKey(target);
|
|
32
|
+
|
|
33
|
+
if (this.validationCache.has(cacheKey)) {
|
|
34
|
+
return this.validationCache.get(cacheKey);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = {
|
|
38
|
+
valid: true,
|
|
39
|
+
errors: [],
|
|
40
|
+
warnings: [],
|
|
41
|
+
checks: {
|
|
42
|
+
existence: null,
|
|
43
|
+
permissions: null,
|
|
44
|
+
encoding: null,
|
|
45
|
+
size: null,
|
|
46
|
+
binary: null,
|
|
47
|
+
syntax: null
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// Check file existence
|
|
53
|
+
result.checks.existence = await this._validateExistence(target);
|
|
54
|
+
|
|
55
|
+
// If file doesn't exist and creation is not allowed, stop here
|
|
56
|
+
if (!result.checks.existence.exists && !target.createIfMissing) {
|
|
57
|
+
if (target.mode !== 'create') {
|
|
58
|
+
result.valid = false;
|
|
59
|
+
result.errors.push(`Target file does not exist: ${target.resolvedPath}`);
|
|
60
|
+
this.validationCache.set(cacheKey, result);
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Skip further checks if file doesn't exist
|
|
66
|
+
if (!result.checks.existence.exists) {
|
|
67
|
+
result.valid = true;
|
|
68
|
+
this.validationCache.set(cacheKey, result);
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Validate permissions
|
|
73
|
+
result.checks.permissions = await this._validatePermissions(target);
|
|
74
|
+
if (!result.checks.permissions.writable) {
|
|
75
|
+
result.valid = false;
|
|
76
|
+
result.errors.push(`Target file is not writable: ${target.resolvedPath}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Validate file is not binary
|
|
80
|
+
result.checks.binary = await this._validateNotBinary(target);
|
|
81
|
+
if (result.checks.binary.isBinary) {
|
|
82
|
+
result.valid = false;
|
|
83
|
+
result.errors.push(`Target file is binary - text injection not supported: ${target.resolvedPath}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Validate file size
|
|
87
|
+
result.checks.size = await this._validateSize(target);
|
|
88
|
+
if (!result.checks.size.withinLimits) {
|
|
89
|
+
result.valid = false;
|
|
90
|
+
result.errors.push(`Target file exceeds size limit: ${result.checks.size.actualSize} > ${result.checks.size.maxSize}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Validate encoding
|
|
94
|
+
if (this.config.validateEncoding) {
|
|
95
|
+
result.checks.encoding = await this._validateEncoding(target);
|
|
96
|
+
if (!result.checks.encoding.valid) {
|
|
97
|
+
result.warnings.push(`Encoding issue detected: ${result.checks.encoding.issue}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Validate syntax if applicable
|
|
102
|
+
if (target.validateSyntax) {
|
|
103
|
+
result.checks.syntax = await this._validateSyntax(target);
|
|
104
|
+
if (!result.checks.syntax.valid) {
|
|
105
|
+
result.errors.push(`Syntax validation failed: ${result.checks.syntax.error}`);
|
|
106
|
+
result.valid = false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
} catch (error) {
|
|
111
|
+
result.valid = false;
|
|
112
|
+
result.errors.push(`Validation error: ${error.message}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.validationCache.set(cacheKey, result);
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Validate injection content
|
|
121
|
+
*/
|
|
122
|
+
async validateContent(content, target) {
|
|
123
|
+
const result = {
|
|
124
|
+
valid: true,
|
|
125
|
+
errors: [],
|
|
126
|
+
warnings: []
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Validate content is not empty if required
|
|
130
|
+
if (target.requireNonEmpty && (!content || content.trim() === '')) {
|
|
131
|
+
result.valid = false;
|
|
132
|
+
result.errors.push('Content cannot be empty');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Validate content encoding
|
|
136
|
+
if (this.config.validateEncoding) {
|
|
137
|
+
const encodingValid = this._validateContentEncoding(content);
|
|
138
|
+
if (!encodingValid.valid) {
|
|
139
|
+
result.valid = false;
|
|
140
|
+
result.errors.push(`Content encoding invalid: ${encodingValid.error}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Validate content doesn't contain binary data
|
|
145
|
+
if (this._containsBinaryData(content)) {
|
|
146
|
+
result.valid = false;
|
|
147
|
+
result.errors.push('Content contains binary data');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Validate syntax if target file type supports it
|
|
151
|
+
if (target.validateSyntax) {
|
|
152
|
+
const syntaxResult = await this._validateContentSyntax(content, target);
|
|
153
|
+
if (!syntaxResult.valid) {
|
|
154
|
+
result.valid = false;
|
|
155
|
+
result.errors.push(`Content syntax invalid: ${syntaxResult.error}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Validate content size
|
|
160
|
+
const contentSize = Buffer.byteLength(content, 'utf8');
|
|
161
|
+
if (contentSize > this.config.maxContentSize) {
|
|
162
|
+
result.valid = false;
|
|
163
|
+
result.errors.push(`Content too large: ${contentSize} > ${this.config.maxContentSize} bytes`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Validate line endings consistency
|
|
167
|
+
if (this.config.consistentLineEndings) {
|
|
168
|
+
const lineEndingResult = this._validateLineEndings(content);
|
|
169
|
+
if (!lineEndingResult.consistent) {
|
|
170
|
+
result.warnings.push(`Inconsistent line endings detected: ${lineEndingResult.types.join(', ')}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Custom content validation
|
|
175
|
+
if (target.customValidation && typeof target.customValidation === 'function') {
|
|
176
|
+
try {
|
|
177
|
+
const customResult = await target.customValidation(content, target);
|
|
178
|
+
if (!customResult.valid) {
|
|
179
|
+
result.valid = false;
|
|
180
|
+
result.errors.push(customResult.error || 'Custom validation failed');
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
result.valid = false;
|
|
184
|
+
result.errors.push(`Custom validation error: ${error.message}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!result.valid) {
|
|
189
|
+
throw new ValidationError('Content validation failed', result.errors);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Validate merged content (after injection)
|
|
197
|
+
*/
|
|
198
|
+
async validateMergedContent(mergedContent, target) {
|
|
199
|
+
// First run standard content validation
|
|
200
|
+
await this.validateContent(mergedContent, target);
|
|
201
|
+
|
|
202
|
+
// Additional validations specific to merged content
|
|
203
|
+
const result = {
|
|
204
|
+
valid: true,
|
|
205
|
+
errors: [],
|
|
206
|
+
warnings: []
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Check for duplicate imports/exports
|
|
210
|
+
if (this._looksLikeCode(target.resolvedPath)) {
|
|
211
|
+
const duplicates = this._findDuplicateStatements(mergedContent);
|
|
212
|
+
if (duplicates.length > 0) {
|
|
213
|
+
result.warnings.push(`Duplicate statements detected: ${duplicates.join(', ')}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Validate structural integrity (balanced brackets, etc.)
|
|
218
|
+
const structureResult = this._validateStructure(mergedContent, target);
|
|
219
|
+
if (!structureResult.valid) {
|
|
220
|
+
result.valid = false;
|
|
221
|
+
result.errors.push(`Structural validation failed: ${structureResult.error}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check for naming conflicts
|
|
225
|
+
if (target.checkConflicts) {
|
|
226
|
+
const conflicts = await this._checkNamingConflicts(mergedContent, target);
|
|
227
|
+
if (conflicts.length > 0) {
|
|
228
|
+
result.warnings.push(`Potential naming conflicts: ${conflicts.join(', ')}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!result.valid) {
|
|
233
|
+
throw new ValidationError('Merged content validation failed', result.errors);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Private validation methods
|
|
241
|
+
*/
|
|
242
|
+
|
|
243
|
+
async _validateExistence(target) {
|
|
244
|
+
try {
|
|
245
|
+
const stats = await fs.stat(target.resolvedPath);
|
|
246
|
+
return {
|
|
247
|
+
exists: true,
|
|
248
|
+
isFile: stats.isFile(),
|
|
249
|
+
isDirectory: stats.isDirectory(),
|
|
250
|
+
size: stats.size,
|
|
251
|
+
modified: stats.mtime
|
|
252
|
+
};
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (error.code === 'ENOENT') {
|
|
255
|
+
return { exists: false };
|
|
256
|
+
}
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async _validatePermissions(target) {
|
|
262
|
+
try {
|
|
263
|
+
const stats = await fs.stat(target.resolvedPath);
|
|
264
|
+
const mode = stats.mode;
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
readable: !!(mode & 0o444),
|
|
268
|
+
writable: !!(mode & 0o222),
|
|
269
|
+
executable: !!(mode & 0o111),
|
|
270
|
+
mode: mode.toString(8)
|
|
271
|
+
};
|
|
272
|
+
} catch (error) {
|
|
273
|
+
throw new Error(`Permission check failed: ${error.message}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async _validateNotBinary(target) {
|
|
278
|
+
try {
|
|
279
|
+
// Read first 1KB to check for binary content
|
|
280
|
+
const buffer = await fs.readFile(target.resolvedPath, { encoding: null, flag: 'r' });
|
|
281
|
+
const sample = buffer.slice(0, 1024);
|
|
282
|
+
|
|
283
|
+
const isBinary = BINARY_PATTERNS.some(pattern => pattern.test(sample.toString('utf8', 0, Math.min(512, sample.length))));
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
isBinary,
|
|
287
|
+
confidence: isBinary ? 'high' : 'low'
|
|
288
|
+
};
|
|
289
|
+
} catch (error) {
|
|
290
|
+
throw new Error(`Binary check failed: ${error.message}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async _validateSize(target) {
|
|
295
|
+
try {
|
|
296
|
+
const stats = await fs.stat(target.resolvedPath);
|
|
297
|
+
const maxSize = this.config.maxFileSize;
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
withinLimits: stats.size <= maxSize,
|
|
301
|
+
actualSize: stats.size,
|
|
302
|
+
maxSize
|
|
303
|
+
};
|
|
304
|
+
} catch (error) {
|
|
305
|
+
throw new Error(`Size check failed: ${error.message}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async _validateEncoding(target) {
|
|
310
|
+
try {
|
|
311
|
+
const content = await fs.readFile(target.resolvedPath, 'utf8');
|
|
312
|
+
|
|
313
|
+
// Check for encoding issues
|
|
314
|
+
const hasReplacementChar = content.includes('\uFFFD');
|
|
315
|
+
const validUtf8 = this._isValidUtf8(content);
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
valid: !hasReplacementChar && validUtf8,
|
|
319
|
+
encoding: 'utf8',
|
|
320
|
+
issue: hasReplacementChar ? 'Contains replacement characters' :
|
|
321
|
+
!validUtf8 ? 'Invalid UTF-8 sequences' : null
|
|
322
|
+
};
|
|
323
|
+
} catch (error) {
|
|
324
|
+
return {
|
|
325
|
+
valid: false,
|
|
326
|
+
issue: `Encoding validation failed: ${error.message}`
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async _validateSyntax(target) {
|
|
332
|
+
const ext = extname(target.resolvedPath).toLowerCase();
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
const content = await fs.readFile(target.resolvedPath, 'utf8');
|
|
336
|
+
|
|
337
|
+
switch (ext) {
|
|
338
|
+
case '.js':
|
|
339
|
+
case '.mjs':
|
|
340
|
+
return this._validateJavaScript(content);
|
|
341
|
+
case '.ts':
|
|
342
|
+
return this._validateTypeScript(content);
|
|
343
|
+
case '.json':
|
|
344
|
+
return this._validateJSON(content);
|
|
345
|
+
default:
|
|
346
|
+
return { valid: true, message: 'No syntax validation available' };
|
|
347
|
+
}
|
|
348
|
+
} catch (error) {
|
|
349
|
+
return {
|
|
350
|
+
valid: false,
|
|
351
|
+
error: error.message
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
_validateJavaScript(content) {
|
|
357
|
+
try {
|
|
358
|
+
// Basic syntax check - validate without executing
|
|
359
|
+
const wrappedContent = `(function() { ${content} })`;
|
|
360
|
+
new Function(wrappedContent);
|
|
361
|
+
return { valid: true };
|
|
362
|
+
} catch (error) {
|
|
363
|
+
return {
|
|
364
|
+
valid: false,
|
|
365
|
+
error: error.message
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
_validateTypeScript(content) {
|
|
371
|
+
// TypeScript validation would require the TS compiler
|
|
372
|
+
// For now, just check basic syntax patterns
|
|
373
|
+
const hasBasicSyntaxErrors = /\b(interface|type|class)\s*\{/.test(content) &&
|
|
374
|
+
!content.includes('}');
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
valid: !hasBasicSyntaxErrors,
|
|
378
|
+
error: hasBasicSyntaxErrors ? 'Unbalanced braces detected' : null
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
_validateJSON(content) {
|
|
383
|
+
try {
|
|
384
|
+
JSON.parse(content);
|
|
385
|
+
return { valid: true };
|
|
386
|
+
} catch (error) {
|
|
387
|
+
return {
|
|
388
|
+
valid: false,
|
|
389
|
+
error: error.message
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async _validateContentSyntax(content, target) {
|
|
395
|
+
const ext = extname(target.resolvedPath).toLowerCase();
|
|
396
|
+
|
|
397
|
+
switch (ext) {
|
|
398
|
+
case '.js':
|
|
399
|
+
case '.mjs':
|
|
400
|
+
return this._validateJavaScript(content);
|
|
401
|
+
case '.ts':
|
|
402
|
+
return this._validateTypeScript(content);
|
|
403
|
+
case '.json':
|
|
404
|
+
return this._validateJSON(content);
|
|
405
|
+
default:
|
|
406
|
+
return { valid: true };
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
_validateContentEncoding(content) {
|
|
411
|
+
try {
|
|
412
|
+
// Check if content can be encoded as UTF-8
|
|
413
|
+
Buffer.from(content, 'utf8');
|
|
414
|
+
return { valid: true };
|
|
415
|
+
} catch (error) {
|
|
416
|
+
return {
|
|
417
|
+
valid: false,
|
|
418
|
+
error: error.message
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
_containsBinaryData(content) {
|
|
424
|
+
return BINARY_PATTERNS.some(pattern => pattern.test(content));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
_validateLineEndings(content) {
|
|
428
|
+
const types = [];
|
|
429
|
+
|
|
430
|
+
if (content.includes('\r\n')) types.push('CRLF');
|
|
431
|
+
if (content.includes('\n') && !content.includes('\r\n')) types.push('LF');
|
|
432
|
+
if (content.includes('\r') && !content.includes('\r\n')) types.push('CR');
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
consistent: types.length <= 1,
|
|
436
|
+
types
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
_isValidUtf8(str) {
|
|
441
|
+
try {
|
|
442
|
+
return str === Buffer.from(str, 'utf8').toString('utf8');
|
|
443
|
+
} catch {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
_looksLikeCode(filePath) {
|
|
449
|
+
const ext = extname(filePath).toLowerCase();
|
|
450
|
+
return ['.js', '.ts', '.jsx', '.tsx', '.mjs'].includes(ext);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
_findDuplicateStatements(content) {
|
|
454
|
+
const duplicates = [];
|
|
455
|
+
const statements = new Set();
|
|
456
|
+
|
|
457
|
+
// Check for duplicate imports
|
|
458
|
+
const imports = content.match(CONTENT_PATTERNS.IMPORT_STATEMENT) || [];
|
|
459
|
+
for (const imp of imports) {
|
|
460
|
+
if (statements.has(imp)) {
|
|
461
|
+
duplicates.push(imp.trim());
|
|
462
|
+
}
|
|
463
|
+
statements.add(imp);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return duplicates;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
_validateStructure(content, target) {
|
|
470
|
+
// Basic structural validation
|
|
471
|
+
const brackets = { '(': 0, '[': 0, '{': 0 };
|
|
472
|
+
const closers = { ')': '(', ']': '[', '}': '{' };
|
|
473
|
+
|
|
474
|
+
for (const char of content) {
|
|
475
|
+
if (brackets.hasOwnProperty(char)) {
|
|
476
|
+
brackets[char]++;
|
|
477
|
+
} else if (closers.hasOwnProperty(char)) {
|
|
478
|
+
const opener = closers[char];
|
|
479
|
+
if (brackets[opener] > 0) {
|
|
480
|
+
brackets[opener]--;
|
|
481
|
+
} else {
|
|
482
|
+
return {
|
|
483
|
+
valid: false,
|
|
484
|
+
error: `Unmatched closing ${char}`
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const unmatched = Object.entries(brackets).filter(([, count]) => count > 0);
|
|
491
|
+
if (unmatched.length > 0) {
|
|
492
|
+
return {
|
|
493
|
+
valid: false,
|
|
494
|
+
error: `Unmatched opening brackets: ${unmatched.map(([b]) => b).join(', ')}`
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return { valid: true };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async _checkNamingConflicts(content, target) {
|
|
502
|
+
// Simple naming conflict detection
|
|
503
|
+
const conflicts = [];
|
|
504
|
+
|
|
505
|
+
// Extract function names
|
|
506
|
+
const functions = content.match(/function\s+(\w+)/g) || [];
|
|
507
|
+
const functionNames = functions.map(f => f.replace('function ', ''));
|
|
508
|
+
|
|
509
|
+
// Check for duplicates
|
|
510
|
+
const seen = new Set();
|
|
511
|
+
for (const name of functionNames) {
|
|
512
|
+
if (seen.has(name)) {
|
|
513
|
+
conflicts.push(`function ${name}`);
|
|
514
|
+
}
|
|
515
|
+
seen.add(name);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return conflicts;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
_getCacheKey(target) {
|
|
522
|
+
const hash = createHash(CHECKSUM_ALGORITHMS.SHA256);
|
|
523
|
+
hash.update(target.resolvedPath);
|
|
524
|
+
hash.update(target.mode);
|
|
525
|
+
hash.update(JSON.stringify(target.skipIf || {}));
|
|
526
|
+
return hash.digest('hex').substring(0, 16);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Clear validation cache
|
|
531
|
+
*/
|
|
532
|
+
clearCache() {
|
|
533
|
+
this.validationCache.clear();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Custom validation error
|
|
539
|
+
*/
|
|
540
|
+
export class ValidationError extends Error {
|
|
541
|
+
constructor(message, errors = []) {
|
|
542
|
+
super(message);
|
|
543
|
+
this.name = 'ValidationError';
|
|
544
|
+
this.errors = errors;
|
|
545
|
+
this.code = ERROR_CODES.VALIDATION_FAILED;
|
|
546
|
+
}
|
|
547
|
+
}
|