@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,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;
|