@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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +210 -0
  3. package/package.json +90 -0
  4. package/src/MIGRATION_COMPLETE.md +186 -0
  5. package/src/PORT-MAP.md +302 -0
  6. package/src/base/filter-templates.js +479 -0
  7. package/src/base/index.js +92 -0
  8. package/src/base/injection-targets.js +583 -0
  9. package/src/base/macro-templates.js +298 -0
  10. package/src/base/macro-templates.js.bak +461 -0
  11. package/src/base/shacl-templates.js +617 -0
  12. package/src/base/template-base.js +388 -0
  13. package/src/core/attestor.js +381 -0
  14. package/src/core/filters.js +518 -0
  15. package/src/core/index.js +21 -0
  16. package/src/core/kgen-engine.js +372 -0
  17. package/src/core/parser.js +447 -0
  18. package/src/core/post-processor.js +313 -0
  19. package/src/core/renderer.js +469 -0
  20. package/src/doc-generator/cli.mjs +122 -0
  21. package/src/doc-generator/index.mjs +28 -0
  22. package/src/doc-generator/mdx-generator.mjs +71 -0
  23. package/src/doc-generator/nav-generator.mjs +136 -0
  24. package/src/doc-generator/parser.mjs +291 -0
  25. package/src/doc-generator/rdf-builder.mjs +306 -0
  26. package/src/doc-generator/scanner.mjs +189 -0
  27. package/src/engine/index.js +42 -0
  28. package/src/engine/pipeline.js +448 -0
  29. package/src/engine/renderer.js +604 -0
  30. package/src/engine/template-engine.js +566 -0
  31. package/src/filters/array.js +436 -0
  32. package/src/filters/data.js +479 -0
  33. package/src/filters/index.js +270 -0
  34. package/src/filters/rdf.js +264 -0
  35. package/src/filters/text.js +369 -0
  36. package/src/index.js +109 -0
  37. package/src/inheritance/index.js +40 -0
  38. package/src/injection/api.js +260 -0
  39. package/src/injection/atomic-writer.js +327 -0
  40. package/src/injection/constants.js +136 -0
  41. package/src/injection/idempotency-manager.js +295 -0
  42. package/src/injection/index.js +28 -0
  43. package/src/injection/injection-engine.js +378 -0
  44. package/src/injection/integration.js +339 -0
  45. package/src/injection/modes/index.js +341 -0
  46. package/src/injection/rollback-manager.js +373 -0
  47. package/src/injection/target-resolver.js +323 -0
  48. package/src/injection/tests/atomic-writer.test.js +382 -0
  49. package/src/injection/tests/injection-engine.test.js +611 -0
  50. package/src/injection/tests/integration.test.js +392 -0
  51. package/src/injection/tests/run-tests.js +283 -0
  52. package/src/injection/validation-engine.js +547 -0
  53. package/src/linter/determinism-linter.js +473 -0
  54. package/src/linter/determinism.js +410 -0
  55. package/src/linter/index.js +6 -0
  56. package/src/linter/test-doubles.js +475 -0
  57. package/src/parser/frontmatter.js +228 -0
  58. package/src/parser/variables.js +344 -0
  59. package/src/renderer/deterministic.js +245 -0
  60. package/src/renderer/index.js +6 -0
  61. package/src/templates/latex/academic-paper.njk +186 -0
  62. package/src/templates/latex/index.js +104 -0
  63. package/src/templates/nextjs/app-page.njk +66 -0
  64. package/src/templates/nextjs/index.js +80 -0
  65. package/src/templates/office/docx/document.njk +368 -0
  66. package/src/templates/office/index.js +79 -0
  67. package/src/templates/office/word-report.njk +129 -0
  68. 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
+ }