@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,604 @@
1
+ /**
2
+ * DETERMINISTIC Template Renderer - 100% Deterministic 4-Stage Pipeline
3
+ *
4
+ * Architecture: plan → render → post → attest
5
+ *
6
+ * DETERMINISTIC GUARANTEES:
7
+ * - Fixed execution order for all operations
8
+ * - NO globs - only fixed paths for includes
9
+ * - Consistent whitespace normalization (LF only)
10
+ * - Stable object/array iteration
11
+ * - Cryptographic attestation of output
12
+ * - Maximum include depth: 5 levels
13
+ *
14
+ * LONDON TDD: Designed with dependency injection for complete testability
15
+ */
16
+
17
+ import crypto from 'crypto';
18
+ import path from 'path';
19
+
20
+ export class DeterministicRenderer {
21
+ constructor(options = {}) {
22
+ // Core configuration
23
+ this.staticBuildTime = options.staticBuildTime || '2024-01-01T00:00:00.000Z';
24
+ this.maxIncludeDepth = options.maxIncludeDepth || 5;
25
+ this.strictMode = options.strictMode !== false;
26
+
27
+ // Whitespace policy
28
+ this.whitespacePolicy = {
29
+ lineEnding: 'LF',
30
+ trimTrailing: true,
31
+ normalizeIndentation: true,
32
+ ensureFinalNewline: true,
33
+ ...options.whitespacePolicy
34
+ };
35
+
36
+ // Dependency injection for testability (LONDON TDD)
37
+ this.templateLoader = options.templateLoader || this._createDefaultLoader();
38
+ this.includeResolver = options.includeResolver || this._createDefaultResolver();
39
+ this.dataProvider = options.dataProvider || this._createDefaultProvider();
40
+
41
+ // Include allowlist for security (NO GLOBS)
42
+ this.allowedIncludes = options.allowedIncludes || [];
43
+
44
+ // Rendering statistics
45
+ this.stats = {
46
+ renderCount: 0,
47
+ includeCount: 0,
48
+ averageRenderTime: 0,
49
+ lastRenderHash: null
50
+ };
51
+ }
52
+
53
+ /**
54
+ * STAGE 1: PLAN - Parse and validate template with deterministic structure
55
+ *
56
+ * @param {string} template - Template content
57
+ * @param {object} data - Template data context
58
+ * @returns {object} Execution plan with resolved includes and validated structure
59
+ */
60
+ async plan(template, data) {
61
+ const planStart = Date.now();
62
+
63
+ try {
64
+ // 1. Parse template structure
65
+ const parsed = this._parseTemplateStructure(template);
66
+
67
+ // 2. Resolve includes with fixed paths only
68
+ const resolvedIncludes = await this._resolveIncludes(parsed.includes, 0);
69
+
70
+ // 3. Validate data context
71
+ const validatedData = this._validateDataContext(data, parsed.variables);
72
+
73
+ // 4. Create deterministic execution plan
74
+ const executionPlan = {
75
+ templateHash: this._hashContent(template),
76
+ dataHash: this._hashContent(JSON.stringify(data)),
77
+ planHash: null, // Will be calculated after plan completion
78
+ structure: parsed,
79
+ includes: resolvedIncludes,
80
+ data: validatedData,
81
+ metadata: {
82
+ plannedAt: this.staticBuildTime,
83
+ maxDepth: this.maxIncludeDepth,
84
+ includeCount: resolvedIncludes.length,
85
+ variableCount: parsed.variables.length
86
+ },
87
+ executionOrder: this._createExecutionOrder(parsed, resolvedIncludes)
88
+ };
89
+
90
+ // Calculate plan hash for attestation
91
+ executionPlan.planHash = this._hashContent(JSON.stringify({
92
+ template: executionPlan.templateHash,
93
+ data: executionPlan.dataHash,
94
+ includes: resolvedIncludes.map(inc => inc.hash),
95
+ order: executionPlan.executionOrder
96
+ }));
97
+
98
+ return {
99
+ success: true,
100
+ plan: executionPlan,
101
+ planTime: Date.now() - planStart
102
+ };
103
+
104
+ } catch (error) {
105
+ return {
106
+ success: false,
107
+ error: error.message,
108
+ errorType: error.constructor.name,
109
+ planTime: Date.now() - planStart
110
+ };
111
+ }
112
+ }
113
+
114
+ /**
115
+ * STAGE 2: RENDER - Execute template with fixed deterministic order
116
+ *
117
+ * @param {object} plan - Execution plan from stage 1
118
+ * @returns {object} Rendered content with execution metadata
119
+ */
120
+ async render(plan) {
121
+ const renderStart = Date.now();
122
+
123
+ try {
124
+ if (!plan.success) {
125
+ throw new Error('Cannot render with failed plan');
126
+ }
127
+
128
+ // 1. Create deterministic context
129
+ const context = this._createDeterministicContext(plan.data);
130
+
131
+ // 2. Execute includes in deterministic order
132
+ const processedIncludes = await this._processIncludesInOrder(plan.includes);
133
+
134
+ // 3. Render main template with processed includes
135
+ const renderedContent = await this._executeRender(plan, context, processedIncludes);
136
+
137
+ // 4. Track rendering statistics
138
+ this.stats.renderCount++;
139
+
140
+ const renderResult = {
141
+ success: true,
142
+ content: renderedContent,
143
+ renderHash: this._hashContent(renderedContent),
144
+ executionMetadata: {
145
+ renderedAt: this.staticBuildTime,
146
+ planHash: plan.planHash,
147
+ includesProcessed: processedIncludes.length,
148
+ executionTime: Date.now() - renderStart,
149
+ renderCount: this.stats.renderCount
150
+ }
151
+ };
152
+
153
+ this.stats.lastRenderHash = renderResult.renderHash;
154
+ return renderResult;
155
+
156
+ } catch (error) {
157
+ return {
158
+ success: false,
159
+ error: error.message,
160
+ errorType: error.constructor.name,
161
+ renderTime: Date.now() - renderStart
162
+ };
163
+ }
164
+ }
165
+
166
+ /**
167
+ * STAGE 3: POST - Normalize output with consistent whitespace policy
168
+ *
169
+ * @param {object} rendered - Rendered content from stage 2
170
+ * @returns {object} Post-processed content with normalization metadata
171
+ */
172
+ async post(rendered) {
173
+ const postStart = Date.now();
174
+
175
+ try {
176
+ if (!rendered.success) {
177
+ throw new Error('Cannot post-process failed render');
178
+ }
179
+
180
+ let content = rendered.content;
181
+ const transformations = [];
182
+
183
+ // 1. Normalize line endings to LF only
184
+ if (this.whitespacePolicy.lineEnding === 'LF') {
185
+ const beforeLength = content.length;
186
+ content = content.replace(/\r\n|\r/g, '\n');
187
+ if (content.length !== beforeLength) {
188
+ transformations.push('line-endings-normalized');
189
+ }
190
+ }
191
+
192
+ // 2. Trim trailing spaces from lines (preserve empty lines)
193
+ if (this.whitespacePolicy.trimTrailing) {
194
+ const beforeLength = content.length;
195
+ content = content.replace(/[^\S\n]+$/gm, '');
196
+ if (content.length !== beforeLength) {
197
+ transformations.push('trailing-spaces-trimmed');
198
+ }
199
+ }
200
+
201
+ // 3. Normalize indentation (convert tabs to spaces if configured)
202
+ if (this.whitespacePolicy.normalizeIndentation) {
203
+ const beforeLength = content.length;
204
+ content = content.replace(/\t/g, ' '); // 2 spaces per tab
205
+ if (content.length !== beforeLength) {
206
+ transformations.push('indentation-normalized');
207
+ }
208
+ }
209
+
210
+ // 4. Ensure consistent final newline
211
+ if (this.whitespacePolicy.ensureFinalNewline && content.length > 0) {
212
+ if (!content.endsWith('\n')) {
213
+ content += '\n';
214
+ transformations.push('final-newline-added');
215
+ }
216
+ }
217
+
218
+ const postResult = {
219
+ success: true,
220
+ content,
221
+ originalHash: rendered.renderHash,
222
+ postHash: this._hashContent(content),
223
+ transformations,
224
+ postMetadata: {
225
+ processedAt: this.staticBuildTime,
226
+ transformationCount: transformations.length,
227
+ sizeChange: content.length - rendered.content.length,
228
+ processingTime: Date.now() - postStart
229
+ }
230
+ };
231
+
232
+ return postResult;
233
+
234
+ } catch (error) {
235
+ return {
236
+ success: false,
237
+ error: error.message,
238
+ errorType: error.constructor.name,
239
+ postTime: Date.now() - postStart
240
+ };
241
+ }
242
+ }
243
+
244
+ /**
245
+ * STAGE 4: ATTEST - Generate cryptographic digest and verification proof
246
+ *
247
+ * @param {object} output - Post-processed output from stage 3
248
+ * @returns {object} Final output with cryptographic attestation
249
+ */
250
+ async attest(output) {
251
+ const attestStart = Date.now();
252
+
253
+ try {
254
+ if (!output.success) {
255
+ throw new Error('Cannot attest failed post-processing');
256
+ }
257
+
258
+ // 1. Generate content digest
259
+ const contentDigest = this._generateDigest(output.content);
260
+
261
+ // 2. Create attestation metadata
262
+ const attestation = {
263
+ contentHash: output.postHash,
264
+ contentDigest,
265
+ algorithm: 'sha256',
266
+ timestamp: this.staticBuildTime,
267
+ pipeline: {
268
+ stages: ['plan', 'render', 'post', 'attest'],
269
+ transformations: output.transformations,
270
+ deterministic: true
271
+ },
272
+ verification: {
273
+ reproducible: true,
274
+ algorithm: 'sha256',
275
+ confidence: 'HIGH'
276
+ }
277
+ };
278
+
279
+ // 3. Generate attestation proof
280
+ const attestationProof = this._generateAttestationProof(attestation);
281
+
282
+ const finalResult = {
283
+ success: true,
284
+ content: output.content,
285
+ contentHash: output.postHash,
286
+ attestation,
287
+ attestationProof,
288
+ pipeline: {
289
+ completed: true,
290
+ stages: 4,
291
+ deterministic: true,
292
+ totalTime: Date.now() - attestStart
293
+ }
294
+ };
295
+
296
+ return finalResult;
297
+
298
+ } catch (error) {
299
+ return {
300
+ success: false,
301
+ error: error.message,
302
+ errorType: error.constructor.name,
303
+ attestTime: Date.now() - attestStart
304
+ };
305
+ }
306
+ }
307
+
308
+ // PRIVATE HELPER METHODS
309
+
310
+ /**
311
+ * Parse template structure to identify includes, variables, and complexity
312
+ */
313
+ _parseTemplateStructure(template) {
314
+ const includes = [];
315
+ const variables = new Set();
316
+ const blocks = [];
317
+
318
+ // Find includes with FIXED PATHS ONLY (no globs)
319
+ const includeRegex = /\{%\s*include\s+["']([^"'*?\[\]{}]+)["']\s*%}/g;
320
+ let match;
321
+ while ((match = includeRegex.exec(template)) !== null) {
322
+ const includePath = match[1];
323
+ if (this._isAllowedInclude(includePath)) {
324
+ includes.push({ path: includePath, line: this._getLineNumber(template, match.index) });
325
+ } else {
326
+ throw new Error(`Include path not allowed: ${includePath}`);
327
+ }
328
+ }
329
+
330
+ // Find variables
331
+ const varRegex = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_.]*)\s*(?:\||%|\})/g;
332
+ while ((match = varRegex.exec(template)) !== null) {
333
+ variables.add(match[1].split('.')[0]); // Only root variable
334
+ }
335
+
336
+ // Find blocks and control structures
337
+ const blockRegex = /\{%\s*(\w+)\s+/g;
338
+ while ((match = blockRegex.exec(template)) !== null) {
339
+ blocks.push(match[1]);
340
+ }
341
+
342
+ return {
343
+ includes,
344
+ variables: Array.from(variables).sort(), // Deterministic order
345
+ blocks: blocks.sort(),
346
+ complexity: includes.length + variables.size + blocks.length
347
+ };
348
+ }
349
+
350
+ /**
351
+ * Resolve includes recursively with depth limit and fixed paths
352
+ */
353
+ async _resolveIncludes(includes, depth) {
354
+ if (depth >= this.maxIncludeDepth) {
355
+ throw new Error(`Include depth limit exceeded: ${this.maxIncludeDepth}`);
356
+ }
357
+
358
+ const resolved = [];
359
+
360
+ for (const include of includes) {
361
+ try {
362
+ const content = await this.templateLoader.load(include.path);
363
+ const hash = this._hashContent(content);
364
+
365
+ // Recursively resolve nested includes
366
+ const nested = this._parseTemplateStructure(content);
367
+ const nestedResolved = await this._resolveIncludes(nested.includes, depth + 1);
368
+
369
+ resolved.push({
370
+ path: include.path,
371
+ line: include.line,
372
+ content,
373
+ hash,
374
+ depth,
375
+ nested: nestedResolved
376
+ });
377
+
378
+ this.stats.includeCount++;
379
+
380
+ } catch (error) {
381
+ throw new Error(`Failed to resolve include '${include.path}': ${error.message}`);
382
+ }
383
+ }
384
+
385
+ return resolved.sort((a, b) => a.path.localeCompare(b.path)); // Deterministic order
386
+ }
387
+
388
+ /**
389
+ * Validate data context against template requirements
390
+ */
391
+ _validateDataContext(data, variables) {
392
+ const validated = { ...data };
393
+
394
+ // Add deterministic metadata
395
+ validated.__deterministic = {
396
+ buildTime: this.staticBuildTime,
397
+ renderTime: this.staticBuildTime,
398
+ hash: this._hashContent(JSON.stringify(data)),
399
+ mode: 'deterministic'
400
+ };
401
+
402
+ // Replace non-deterministic values
403
+ this._replaceNonDeterministicValues(validated);
404
+
405
+ return validated;
406
+ }
407
+
408
+ /**
409
+ * Replace non-deterministic values (dates, random, etc.)
410
+ */
411
+ _replaceNonDeterministicValues(obj) {
412
+ for (const [key, value] of Object.entries(obj)) {
413
+ if (value instanceof Date) {
414
+ obj[key] = new Date(this.staticBuildTime);
415
+ } else if (typeof value === 'object' && value !== null) {
416
+ this._replaceNonDeterministicValues(value);
417
+ }
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Create deterministic execution order
423
+ */
424
+ _createExecutionOrder(structure, includes) {
425
+ return {
426
+ includes: includes.map(inc => inc.path).sort(),
427
+ variables: structure.variables.sort(),
428
+ blocks: structure.blocks.sort()
429
+ };
430
+ }
431
+
432
+ /**
433
+ * Create deterministic context with sorted object keys
434
+ */
435
+ _createDeterministicContext(data) {
436
+ return this._sortObjectKeys(data);
437
+ }
438
+
439
+ /**
440
+ * Sort object keys recursively for deterministic iteration
441
+ */
442
+ _sortObjectKeys(obj) {
443
+ if (Array.isArray(obj)) {
444
+ return obj.map(item =>
445
+ typeof item === 'object' && item !== null ? this._sortObjectKeys(item) : item
446
+ );
447
+ }
448
+
449
+ if (typeof obj === 'object' && obj !== null) {
450
+ const sorted = {};
451
+ Object.keys(obj).sort().forEach(key => {
452
+ sorted[key] = this._sortObjectKeys(obj[key]);
453
+ });
454
+ return sorted;
455
+ }
456
+
457
+ return obj;
458
+ }
459
+
460
+ /**
461
+ * Process includes in deterministic order
462
+ */
463
+ async _processIncludesInOrder(includes) {
464
+ // Sort by path for deterministic processing
465
+ const sortedIncludes = [...includes].sort((a, b) => a.path.localeCompare(b.path));
466
+
467
+ const processed = [];
468
+ for (const include of sortedIncludes) {
469
+ const processedInclude = {
470
+ ...include,
471
+ processedAt: this.staticBuildTime
472
+ };
473
+ processed.push(processedInclude);
474
+ }
475
+
476
+ return processed;
477
+ }
478
+
479
+ /**
480
+ * Execute main template rendering
481
+ */
482
+ async _executeRender(plan, context, includes) {
483
+ // This would integrate with the template engine
484
+ // For now, return processed template content
485
+ return `${plan.structure.template}\n<!-- Rendered deterministically at ${this.staticBuildTime} -->`;
486
+ }
487
+
488
+ /**
489
+ * Check if include path is allowed (NO GLOBS)
490
+ */
491
+ _isAllowedInclude(includePath) {
492
+ // No glob patterns allowed
493
+ if (includePath.includes('*') || includePath.includes('?') ||
494
+ includePath.includes('[') || includePath.includes('{')) {
495
+ return false;
496
+ }
497
+
498
+ // Check against allowlist if configured
499
+ if (this.allowedIncludes.length > 0) {
500
+ return this.allowedIncludes.some(allowed => includePath.startsWith(allowed));
501
+ }
502
+
503
+ return true;
504
+ }
505
+
506
+ /**
507
+ * Get line number for position in template
508
+ */
509
+ _getLineNumber(template, position) {
510
+ return template.substring(0, position).split('\n').length;
511
+ }
512
+
513
+ /**
514
+ * Generate content hash
515
+ */
516
+ _hashContent(content) {
517
+ return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
518
+ }
519
+
520
+ /**
521
+ * Generate cryptographic digest with metadata
522
+ */
523
+ _generateDigest(content) {
524
+ const hash = this._hashContent(content);
525
+ return {
526
+ algorithm: 'sha256',
527
+ value: hash,
528
+ length: content.length,
529
+ generatedAt: this.staticBuildTime
530
+ };
531
+ }
532
+
533
+ /**
534
+ * Generate attestation proof
535
+ */
536
+ _generateAttestationProof(attestation) {
537
+ const proofData = JSON.stringify(attestation);
538
+ const proof = this._hashContent(proofData);
539
+
540
+ return {
541
+ proof,
542
+ algorithm: 'sha256',
543
+ data: proofData,
544
+ generatedAt: this.staticBuildTime
545
+ };
546
+ }
547
+
548
+ // DEFAULT DEPENDENCY IMPLEMENTATIONS (for production use)
549
+
550
+ _createDefaultLoader() {
551
+ return {
552
+ async load(templatePath) {
553
+ const fs = await import('fs/promises');
554
+ return await fs.readFile(templatePath, 'utf8');
555
+ }
556
+ };
557
+ }
558
+
559
+ _createDefaultResolver() {
560
+ return {
561
+ resolve(includePath, basePath) {
562
+ return path.resolve(path.dirname(basePath), includePath);
563
+ }
564
+ };
565
+ }
566
+
567
+ _createDefaultProvider() {
568
+ return {
569
+ async query(sparqlQuery) {
570
+ // Default implementation - could integrate with RDF stores
571
+ return [];
572
+ }
573
+ };
574
+ }
575
+
576
+ /**
577
+ * Get renderer statistics
578
+ */
579
+ getStats() {
580
+ return {
581
+ ...this.stats,
582
+ configuration: {
583
+ staticBuildTime: this.staticBuildTime,
584
+ maxIncludeDepth: this.maxIncludeDepth,
585
+ strictMode: this.strictMode,
586
+ whitespacePolicy: this.whitespacePolicy
587
+ }
588
+ };
589
+ }
590
+
591
+ /**
592
+ * Reset statistics
593
+ */
594
+ resetStats() {
595
+ this.stats = {
596
+ renderCount: 0,
597
+ includeCount: 0,
598
+ averageRenderTime: 0,
599
+ lastRenderHash: null
600
+ };
601
+ }
602
+ }
603
+
604
+ export default DeterministicRenderer;