@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,372 @@
1
+ /**
2
+ * KGEN Native Template Engine - Deterministic template processing without nunjucks
3
+ *
4
+ * Pipeline: plan → render → post → attest
5
+ * Supports: Variables {{ var }}, conditionals {% if %}, loops {% for %}, filters {{ var | filter }}
6
+ * Deterministic: All operations produce stable, reproducible output
7
+ */
8
+
9
+ import crypto from 'crypto';
10
+ import { KGenParser } from './parser.js';
11
+ import { KGenFilters } from './filters.js';
12
+ import { KGenRenderer } from './renderer.js';
13
+ import { KGenPostProcessor } from './post-processor.js';
14
+ import { KGenAttestor } from './attestor.js';
15
+
16
+ export class KGenTemplateEngine {
17
+ constructor(options = {}) {
18
+ this.options = {
19
+ strictMode: options.strictMode !== false,
20
+ deterministicMode: options.deterministicMode !== false,
21
+ staticBuildTime: options.staticBuildTime || '2024-01-01T00:00:00.000Z',
22
+ maxDepth: options.maxDepth || 10,
23
+ enableIncludes: options.enableIncludes !== false,
24
+ enableAttestation: options.enableAttestation !== false,
25
+ ...options
26
+ };
27
+
28
+ // Initialize pipeline components
29
+ this.parser = new KGenParser(this.options);
30
+ this.filters = new KGenFilters(this.options);
31
+ this.renderer = new KGenRenderer(this.options);
32
+ this.postProcessor = new KGenPostProcessor(this.options);
33
+ this.attestor = new KGenAttestor(this.options);
34
+
35
+ // Register core filters
36
+ this.registerCoreFilters();
37
+ }
38
+
39
+ /**
40
+ * PHASE 1: PLAN - Parse and analyze template
41
+ */
42
+ async plan(template, context = {}) {
43
+ try {
44
+ const parseResult = await this.parser.parse(template);
45
+
46
+ const plan = {
47
+ success: true,
48
+ template: parseResult.template,
49
+ frontmatter: parseResult.frontmatter,
50
+ variables: parseResult.variables,
51
+ expressions: parseResult.expressions,
52
+ includes: parseResult.includes,
53
+ complexity: this.calculateComplexity(parseResult),
54
+ hash: this.hashContent(template),
55
+ contextHash: this.hashContent(JSON.stringify(context)),
56
+ timestamp: this.options.deterministicMode ? this.options.staticBuildTime : new Date().toISOString()
57
+ };
58
+
59
+ // Validate context against required variables
60
+ if (this.options.strictMode) {
61
+ this.validateContext(plan.variables, context, plan.frontmatter);
62
+ }
63
+
64
+ return plan;
65
+ } catch (error) {
66
+ return {
67
+ success: false,
68
+ error: error.message,
69
+ phase: 'plan',
70
+ timestamp: this.options.deterministicMode ? this.options.staticBuildTime : new Date().toISOString()
71
+ };
72
+ }
73
+ }
74
+
75
+ /**
76
+ * PHASE 2: RENDER - Execute template with context
77
+ */
78
+ async render(plan, context = {}) {
79
+ if (!plan.success) {
80
+ return { success: false, error: 'Invalid plan provided', phase: 'render' };
81
+ }
82
+
83
+ try {
84
+ // Merge frontmatter with context
85
+ const mergedContext = {
86
+ ...plan.frontmatter,
87
+ ...context,
88
+ __kgen: {
89
+ renderTime: this.options.deterministicMode ? this.options.staticBuildTime : new Date().toISOString(),
90
+ templateHash: plan.hash,
91
+ contextHash: plan.contextHash,
92
+ deterministicMode: this.options.deterministicMode
93
+ }
94
+ };
95
+
96
+ // Execute rendering
97
+ const renderResult = await this.renderer.render(plan.template, mergedContext, {
98
+ variables: plan.variables,
99
+ expressions: plan.expressions,
100
+ filters: this.filters
101
+ });
102
+
103
+ return {
104
+ success: true,
105
+ content: renderResult.content,
106
+ context: mergedContext,
107
+ metadata: {
108
+ ...renderResult.metadata,
109
+ phase: 'render',
110
+ renderTime: mergedContext.__kgen.renderTime
111
+ }
112
+ };
113
+ } catch (error) {
114
+ return {
115
+ success: false,
116
+ error: error.message,
117
+ phase: 'render',
118
+ context: context
119
+ };
120
+ }
121
+ }
122
+
123
+ /**
124
+ * PHASE 3: POST - Post-process rendered content
125
+ */
126
+ async post(renderResult) {
127
+ if (!renderResult.success) {
128
+ return renderResult;
129
+ }
130
+
131
+ try {
132
+ const postResult = await this.postProcessor.process(renderResult.content, {
133
+ normalizeWhitespace: true,
134
+ trimLines: true,
135
+ ensureFinalNewline: true,
136
+ deterministicMode: this.options.deterministicMode
137
+ });
138
+
139
+ return {
140
+ ...renderResult,
141
+ content: postResult.content,
142
+ metadata: {
143
+ ...renderResult.metadata,
144
+ post: postResult.metadata,
145
+ phase: 'post'
146
+ }
147
+ };
148
+ } catch (error) {
149
+ return {
150
+ success: false,
151
+ error: error.message,
152
+ phase: 'post',
153
+ originalResult: renderResult
154
+ };
155
+ }
156
+ }
157
+
158
+ /**
159
+ * PHASE 4: ATTEST - Generate attestation of deterministic output
160
+ */
161
+ async attest(postResult) {
162
+ if (!postResult.success || !this.options.enableAttestation) {
163
+ return postResult;
164
+ }
165
+
166
+ try {
167
+ const attestation = await this.attestor.attest(postResult.content, {
168
+ templateHash: postResult.metadata?.renderTime ?
169
+ this.hashContent(postResult.metadata.renderTime) : undefined,
170
+ contextHash: postResult.context ?
171
+ this.hashContent(JSON.stringify(postResult.context)) : undefined,
172
+ deterministicMode: this.options.deterministicMode
173
+ });
174
+
175
+ return {
176
+ ...postResult,
177
+ attestation,
178
+ metadata: {
179
+ ...postResult.metadata,
180
+ attestation: attestation.metadata,
181
+ phase: 'attest'
182
+ }
183
+ };
184
+ } catch (error) {
185
+ return {
186
+ success: false,
187
+ error: error.message,
188
+ phase: 'attest',
189
+ originalResult: postResult
190
+ };
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Complete pipeline: plan → render → post → attest
196
+ */
197
+ async execute(template, context = {}) {
198
+ const plan = await this.plan(template, context);
199
+ const renderResult = await this.render(plan, context);
200
+ const postResult = await this.post(renderResult);
201
+ const finalResult = await this.attest(postResult);
202
+
203
+ return finalResult;
204
+ }
205
+
206
+ /**
207
+ * Simple render method for basic use cases
208
+ */
209
+ async renderTemplate(template, context = {}) {
210
+ // Use a simplified pipeline without post-processing for simple rendering
211
+ const plan = await this.plan(template, context);
212
+ if (!plan.success) return '';
213
+
214
+ const renderResult = await this.render(plan, context);
215
+ if (!renderResult.success) return '';
216
+
217
+ // Return content without post-processing to avoid final newlines
218
+ return renderResult.content || '';
219
+ }
220
+
221
+ /**
222
+ * Register core filters
223
+ */
224
+ registerCoreFilters() {
225
+ // Text filters
226
+ this.filters.register('upper', (str) => String(str || '').toUpperCase());
227
+ this.filters.register('lower', (str) => String(str || '').toLowerCase());
228
+ this.filters.register('trim', (str) => String(str || '').trim());
229
+ this.filters.register('replace', (str, search, replace) =>
230
+ String(str || '').replace(new RegExp(search, 'g'), replace));
231
+ this.filters.register('split', (str, separator) =>
232
+ String(str || '').split(separator || ''));
233
+ this.filters.register('join', (arr, separator) =>
234
+ Array.isArray(arr) ? arr.join(separator || '') : arr);
235
+ this.filters.register('slice', (str, start, end) =>
236
+ String(str || '').slice(start, end));
237
+
238
+ // Data filters
239
+ this.filters.register('default', (value, defaultValue) =>
240
+ (value === null || value === undefined || value === '') ? defaultValue : value);
241
+ this.filters.register('unique', (arr) =>
242
+ Array.isArray(arr) ? [...new Set(arr)] : arr);
243
+ this.filters.register('sort', (arr, key) => {
244
+ if (!Array.isArray(arr)) return arr;
245
+ return [...arr].sort((a, b) => {
246
+ const aVal = key ? a[key] : a;
247
+ const bVal = key ? b[key] : b;
248
+ return aVal > bVal ? 1 : aVal < bVal ? -1 : 0;
249
+ });
250
+ });
251
+ this.filters.register('groupby', (arr, key) => {
252
+ if (!Array.isArray(arr)) return {};
253
+ return arr.reduce((groups, item) => {
254
+ const groupKey = typeof item === 'object' ? item[key] : item;
255
+ groups[groupKey] = groups[groupKey] || [];
256
+ groups[groupKey].push(item);
257
+ return groups;
258
+ }, {});
259
+ });
260
+ this.filters.register('map', (arr, key) => {
261
+ if (!Array.isArray(arr)) return arr;
262
+ return arr.map(item => typeof item === 'object' ? item[key] : item);
263
+ });
264
+ this.filters.register('sum', (arr, key) => {
265
+ if (!Array.isArray(arr)) return 0;
266
+ return arr.reduce((sum, item) => {
267
+ const val = key ? item[key] : item;
268
+ return sum + (Number(val) || 0);
269
+ }, 0);
270
+ });
271
+ this.filters.register('count', (arr) => Array.isArray(arr) ? arr.length : 0);
272
+
273
+ // Format filters
274
+ this.filters.register('json', (obj, indent) => {
275
+ try {
276
+ return JSON.stringify(obj, null, indent || 0);
277
+ } catch (e) {
278
+ return '{}';
279
+ }
280
+ });
281
+ this.filters.register('md', (str) => {
282
+ // Basic markdown escaping
283
+ return String(str || '').replace(/[*_`]/g, '\\$&');
284
+ });
285
+ this.filters.register('csv', (arr) => {
286
+ if (!Array.isArray(arr)) return '';
287
+ return arr.map(item =>
288
+ typeof item === 'object' ? JSON.stringify(item) : String(item)
289
+ ).join(',');
290
+ });
291
+ }
292
+
293
+ /**
294
+ * Calculate template complexity score
295
+ */
296
+ calculateComplexity(parseResult) {
297
+ const { variables = [], expressions = [], includes = [] } = parseResult;
298
+ return variables.length + expressions.length * 2 + includes.length * 3;
299
+ }
300
+
301
+ /**
302
+ * Validate context has required variables
303
+ */
304
+ validateContext(variables, context, frontmatter) {
305
+ const missing = [];
306
+ const available = new Set([
307
+ ...Object.keys(context || {}),
308
+ ...Object.keys(frontmatter || {}),
309
+ '__kgen'
310
+ ]);
311
+
312
+ // Common loop variables that are typically not required
313
+ const loopVars = new Set(['item', 'index', 'key', 'value', 'loop']);
314
+
315
+ variables.forEach(varName => {
316
+ if (!available.has(varName) && !loopVars.has(varName)) {
317
+ missing.push(varName);
318
+ }
319
+ });
320
+
321
+ if (missing.length > 0) {
322
+ throw new Error(`Missing required variables: ${missing.join(', ')}`);
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Generate deterministic content hash
328
+ */
329
+ hashContent(content) {
330
+ return crypto.createHash('sha256').update(String(content || ''), 'utf8').digest('hex');
331
+ }
332
+
333
+ /**
334
+ * Get engine statistics
335
+ */
336
+ getStats() {
337
+ return {
338
+ ...this.options,
339
+ filterCount: this.filters.getFilterCount(),
340
+ version: '2.0.0-kgen-native'
341
+ };
342
+ }
343
+
344
+ /**
345
+ * Verify deterministic behavior across multiple runs
346
+ */
347
+ async verifyDeterminism(template, context, iterations = 3) {
348
+ const results = [];
349
+ const hashes = new Set();
350
+
351
+ for (let i = 0; i < iterations; i++) {
352
+ const result = await this.execute(template, context);
353
+ if (result.success) {
354
+ const hash = this.hashContent(result.content);
355
+ results.push({ iteration: i + 1, hash, success: true });
356
+ hashes.add(hash);
357
+ } else {
358
+ results.push({ iteration: i + 1, success: false, error: result.error });
359
+ }
360
+ }
361
+
362
+ return {
363
+ isDeterministic: hashes.size === 1,
364
+ iterations,
365
+ successfulRuns: results.filter(r => r.success).length,
366
+ uniqueOutputs: hashes.size,
367
+ results: results.slice(0, 2) // Show first 2 for comparison
368
+ };
369
+ }
370
+ }
371
+
372
+ export default KGenTemplateEngine;