@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,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variable Extractor - Extract and validate template variables
|
|
3
|
+
* Migrated from ~/unjucks with enhanced pattern matching
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class VariableExtractor {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.includeFilters = options.includeFilters !== false;
|
|
9
|
+
this.includeFunctions = options.includeFunctions !== false;
|
|
10
|
+
this.strict = options.strict !== false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract all variables from template content
|
|
15
|
+
*/
|
|
16
|
+
extract(content) {
|
|
17
|
+
const variables = new Set();
|
|
18
|
+
const filters = new Set();
|
|
19
|
+
const functions = new Set();
|
|
20
|
+
|
|
21
|
+
// Extract {{ variable }} patterns
|
|
22
|
+
this.extractOutputVariables(content, variables, filters);
|
|
23
|
+
|
|
24
|
+
// Extract {% for variable in ... %} patterns
|
|
25
|
+
this.extractLoopVariables(content, variables);
|
|
26
|
+
|
|
27
|
+
// Extract {% if variable %} patterns
|
|
28
|
+
this.extractConditionalVariables(content, variables);
|
|
29
|
+
|
|
30
|
+
// Extract {% set variable = ... %} patterns
|
|
31
|
+
this.extractAssignmentVariables(content, variables);
|
|
32
|
+
|
|
33
|
+
// Extract function calls
|
|
34
|
+
if (this.includeFunctions) {
|
|
35
|
+
this.extractFunctionCalls(content, functions);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
variables: Array.from(variables).sort(),
|
|
40
|
+
filters: this.includeFilters ? Array.from(filters).sort() : [],
|
|
41
|
+
functions: this.includeFunctions ? Array.from(functions).sort() : [],
|
|
42
|
+
totalVariables: variables.size,
|
|
43
|
+
complexity: this.calculateComplexity(content)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract variables from output expressions {{ ... }}
|
|
49
|
+
*/
|
|
50
|
+
extractOutputVariables(content, variables, filters) {
|
|
51
|
+
// Match {{ variable | filter1 | filter2 }} patterns
|
|
52
|
+
const outputPattern = /\{\{\s*([^}]+?)\s*\}\}/g;
|
|
53
|
+
let match;
|
|
54
|
+
|
|
55
|
+
while ((match = outputPattern.exec(content)) !== null) {
|
|
56
|
+
const expression = match[1].trim();
|
|
57
|
+
this.parseExpression(expression, variables, filters);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Extract variables from loop constructs
|
|
63
|
+
*/
|
|
64
|
+
extractLoopVariables(content, variables) {
|
|
65
|
+
// Match {% for item in collection %} patterns
|
|
66
|
+
const forPattern = /\{\%\s*for\s+(\w+)(?:\s*,\s*(\w+))?\s+in\s+([^%]+?)\s*\%\}/g;
|
|
67
|
+
let match;
|
|
68
|
+
|
|
69
|
+
while ((match = forPattern.exec(content)) !== null) {
|
|
70
|
+
const [, itemVar, indexVar, collection] = match;
|
|
71
|
+
|
|
72
|
+
// Collection is a variable we need
|
|
73
|
+
const collectionVar = collection.trim().split('.')[0].split('|')[0].trim();
|
|
74
|
+
if (collectionVar && this.isValidVariable(collectionVar)) {
|
|
75
|
+
variables.add(collectionVar);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Note: itemVar and indexVar are loop-local, not template variables
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Extract variables from conditional statements
|
|
84
|
+
*/
|
|
85
|
+
extractConditionalVariables(content, variables) {
|
|
86
|
+
// Match {% if condition %} patterns
|
|
87
|
+
const ifPattern = /\{\%\s*if\s+([^%]+?)\s*\%\}/g;
|
|
88
|
+
const elifPattern = /\{\%\s*elif\s+([^%]+?)\s*\%\}/g;
|
|
89
|
+
|
|
90
|
+
let match;
|
|
91
|
+
|
|
92
|
+
// Process if statements
|
|
93
|
+
while ((match = ifPattern.exec(content)) !== null) {
|
|
94
|
+
const condition = match[1].trim();
|
|
95
|
+
this.parseCondition(condition, variables);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Process elif statements
|
|
99
|
+
while ((match = elifPattern.exec(content)) !== null) {
|
|
100
|
+
const condition = match[1].trim();
|
|
101
|
+
this.parseCondition(condition, variables);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Extract variables from assignment statements
|
|
107
|
+
*/
|
|
108
|
+
extractAssignmentVariables(content, variables) {
|
|
109
|
+
// Match {% set variable = expression %} patterns
|
|
110
|
+
const setPattern = /\{\%\s*set\s+\w+\s*=\s*([^%]+?)\s*\%\}/g;
|
|
111
|
+
let match;
|
|
112
|
+
|
|
113
|
+
while ((match = setPattern.exec(content)) !== null) {
|
|
114
|
+
const expression = match[1].trim();
|
|
115
|
+
this.parseExpression(expression, variables, new Set());
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Extract function calls from template
|
|
121
|
+
*/
|
|
122
|
+
extractFunctionCalls(content, functions) {
|
|
123
|
+
// Match function calls like func(args)
|
|
124
|
+
const functionPattern = /(\w+)\s*\(/g;
|
|
125
|
+
let match;
|
|
126
|
+
|
|
127
|
+
while ((match = functionPattern.exec(content)) !== null) {
|
|
128
|
+
const funcName = match[1];
|
|
129
|
+
if (!this.isBuiltinFunction(funcName)) {
|
|
130
|
+
functions.add(funcName);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Parse expression to extract variables and filters
|
|
137
|
+
*/
|
|
138
|
+
parseExpression(expression, variables, filters) {
|
|
139
|
+
// Handle complex expressions with operators
|
|
140
|
+
const parts = expression.split(/[+\-*\/\(\)]/);
|
|
141
|
+
|
|
142
|
+
for (let part of parts) {
|
|
143
|
+
part = part.trim();
|
|
144
|
+
|
|
145
|
+
// Skip empty parts and literals
|
|
146
|
+
if (!part || this.isLiteral(part)) continue;
|
|
147
|
+
|
|
148
|
+
// Check for filter chain: variable | filter1 | filter2
|
|
149
|
+
if (part.includes('|')) {
|
|
150
|
+
const [varPart, ...filterParts] = part.split('|').map(p => p.trim());
|
|
151
|
+
|
|
152
|
+
// Extract variable
|
|
153
|
+
const rootVar = this.extractRootVariable(varPart);
|
|
154
|
+
if (rootVar && this.isValidVariable(rootVar)) {
|
|
155
|
+
variables.add(rootVar);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Extract filters
|
|
159
|
+
filterParts.forEach(filter => {
|
|
160
|
+
const filterName = filter.split('(')[0].trim();
|
|
161
|
+
if (filterName && this.includeFilters) {
|
|
162
|
+
filters.add(filterName);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
} else {
|
|
166
|
+
// Simple variable reference
|
|
167
|
+
const rootVar = this.extractRootVariable(part);
|
|
168
|
+
if (rootVar && this.isValidVariable(rootVar)) {
|
|
169
|
+
variables.add(rootVar);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Parse conditional expression to extract variables
|
|
177
|
+
*/
|
|
178
|
+
parseCondition(condition, variables) {
|
|
179
|
+
// Handle operators: and, or, not, ==, !=, <, >, <=, >=, in, is
|
|
180
|
+
const conditionParts = condition.split(/\s+(?:and|or|not|==|!=|<=|>=|<|>|in|is)\s+/);
|
|
181
|
+
|
|
182
|
+
conditionParts.forEach(part => {
|
|
183
|
+
part = part.trim();
|
|
184
|
+
if (part && !this.isLiteral(part)) {
|
|
185
|
+
const rootVar = this.extractRootVariable(part);
|
|
186
|
+
if (rootVar && this.isValidVariable(rootVar)) {
|
|
187
|
+
variables.add(rootVar);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Extract root variable from complex expression
|
|
195
|
+
*/
|
|
196
|
+
extractRootVariable(expression) {
|
|
197
|
+
// Handle dot notation: user.name -> user
|
|
198
|
+
// Handle array access: users[0] -> users
|
|
199
|
+
// Handle function calls: len(users) -> users
|
|
200
|
+
|
|
201
|
+
let cleaned = expression.trim();
|
|
202
|
+
|
|
203
|
+
// Remove function calls but keep arguments
|
|
204
|
+
cleaned = cleaned.replace(/\w+\(([^)]+)\)/g, '$1');
|
|
205
|
+
|
|
206
|
+
// Extract variable before dot or bracket
|
|
207
|
+
const match = cleaned.match(/^([a-zA-Z_][a-zA-Z0-9_]*)/);
|
|
208
|
+
return match ? match[1] : null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Check if value is a literal (string, number, boolean)
|
|
213
|
+
*/
|
|
214
|
+
isLiteral(value) {
|
|
215
|
+
// String literals
|
|
216
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
217
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Number literals
|
|
222
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Boolean literals
|
|
227
|
+
if (['true', 'false', 'True', 'False'].includes(value)) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// None/null literals
|
|
232
|
+
if (['none', 'null', 'None', 'Null'].includes(value)) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Check if variable name is valid
|
|
241
|
+
*/
|
|
242
|
+
isValidVariable(name) {
|
|
243
|
+
// Valid variable names: letters, numbers, underscore
|
|
244
|
+
// Must start with letter or underscore
|
|
245
|
+
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name) &&
|
|
246
|
+
!this.isBuiltinVariable(name);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Check if variable is a builtin
|
|
251
|
+
*/
|
|
252
|
+
isBuiltinVariable(name) {
|
|
253
|
+
const builtins = [
|
|
254
|
+
'loop', 'super', 'self', 'varargs', 'kwargs',
|
|
255
|
+
'joiner', 'cycler', 'range', 'lipsum'
|
|
256
|
+
];
|
|
257
|
+
return builtins.includes(name);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Check if function is a builtin
|
|
262
|
+
*/
|
|
263
|
+
isBuiltinFunction(name) {
|
|
264
|
+
const builtins = [
|
|
265
|
+
'range', 'lipsum', 'dict', 'list', 'tuple', 'set',
|
|
266
|
+
'len', 'str', 'int', 'float', 'bool'
|
|
267
|
+
];
|
|
268
|
+
return builtins.includes(name);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Calculate template complexity
|
|
273
|
+
*/
|
|
274
|
+
calculateComplexity(content) {
|
|
275
|
+
const patterns = {
|
|
276
|
+
variables: /\{\{\s*[^}]+?\s*\}\}/g,
|
|
277
|
+
conditions: /\{\%\s*if\s+[^%]+?\s*\%\}/g,
|
|
278
|
+
loops: /\{\%\s*for\s+[^%]+?\s*\%\}/g,
|
|
279
|
+
includes: /\{\%\s*include\s+[^%]+?\s*\%\}/g,
|
|
280
|
+
macros: /\{\%\s*macro\s+[^%]+?\s*\%\}/g,
|
|
281
|
+
blocks: /\{\%\s*block\s+[^%]+?\s*\%\}/g
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
let complexity = 0;
|
|
285
|
+
Object.values(patterns).forEach(pattern => {
|
|
286
|
+
const matches = content.match(pattern);
|
|
287
|
+
complexity += matches ? matches.length : 0;
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return complexity;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Validate extracted variables against provided context
|
|
295
|
+
*/
|
|
296
|
+
validateContext(extractedVars, context, options = {}) {
|
|
297
|
+
const errors = [];
|
|
298
|
+
const warnings = [];
|
|
299
|
+
|
|
300
|
+
const availableVars = new Set([
|
|
301
|
+
...Object.keys(context),
|
|
302
|
+
'__meta', // Always available
|
|
303
|
+
...options.additionalVars || []
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
extractedVars.forEach(varName => {
|
|
307
|
+
if (!availableVars.has(varName)) {
|
|
308
|
+
if (options.strictMode) {
|
|
309
|
+
errors.push(`Required variable '${varName}' not found in context`);
|
|
310
|
+
} else {
|
|
311
|
+
warnings.push(`Variable '${varName}' not found in context`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Check for unused context variables
|
|
317
|
+
if (options.warnUnused) {
|
|
318
|
+
Object.keys(context).forEach(contextVar => {
|
|
319
|
+
if (!extractedVars.includes(contextVar) && !contextVar.startsWith('_')) {
|
|
320
|
+
warnings.push(`Context variable '${contextVar}' is unused in template`);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
valid: errors.length === 0,
|
|
327
|
+
errors,
|
|
328
|
+
warnings
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get extractor statistics
|
|
334
|
+
*/
|
|
335
|
+
getStats() {
|
|
336
|
+
return {
|
|
337
|
+
includeFilters: this.includeFilters,
|
|
338
|
+
includeFunctions: this.includeFunctions,
|
|
339
|
+
strict: this.strict
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export default VariableExtractor;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic Renderer - Ensures identical output across template runs
|
|
3
|
+
* Migrated from ~/unjucks with enhanced deterministic guarantees
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
|
|
8
|
+
export class DeterministicRenderer {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.staticBuildTime = options.staticBuildTime || '2024-01-01T00:00:00.000Z';
|
|
11
|
+
this.blockNonDeterministic = options.blockNonDeterministic !== false;
|
|
12
|
+
this.strictMode = options.strictMode !== false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Render template with deterministic context
|
|
17
|
+
*/
|
|
18
|
+
async render(nunjucksEnv, templateContent, context) {
|
|
19
|
+
try {
|
|
20
|
+
// Create deterministic context
|
|
21
|
+
const deterministicContext = this.createDeterministicContext(context);
|
|
22
|
+
|
|
23
|
+
// Block non-deterministic operations if enabled
|
|
24
|
+
if (this.blockNonDeterministic) {
|
|
25
|
+
this.validateDeterministicContext(deterministicContext);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Render with enhanced error handling
|
|
29
|
+
const rendered = nunjucksEnv.renderString(templateContent, deterministicContext);
|
|
30
|
+
|
|
31
|
+
return this.postProcessOutput(rendered);
|
|
32
|
+
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (this.strictMode) {
|
|
35
|
+
throw new Error(`Deterministic rendering failed: ${error.message}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Fallback to non-deterministic rendering
|
|
39
|
+
console.warn('[DeterministicRenderer] Fallback to non-deterministic rendering:', error.message);
|
|
40
|
+
return nunjucksEnv.renderString(templateContent, context);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Render string template with deterministic context
|
|
46
|
+
*/
|
|
47
|
+
renderString(nunjucksEnv, templateContent, context) {
|
|
48
|
+
const deterministicContext = this.createDeterministicContext(context);
|
|
49
|
+
const rendered = nunjucksEnv.renderString(templateContent, deterministicContext);
|
|
50
|
+
return this.postProcessOutput(rendered);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create deterministic context by replacing non-deterministic values
|
|
55
|
+
*/
|
|
56
|
+
createDeterministicContext(context) {
|
|
57
|
+
const deterministicContext = { ...context };
|
|
58
|
+
|
|
59
|
+
// Add deterministic metadata
|
|
60
|
+
deterministicContext.__deterministic = {
|
|
61
|
+
buildTime: this.staticBuildTime,
|
|
62
|
+
renderTime: this.staticBuildTime, // Use same time for consistency
|
|
63
|
+
hash: this.hashContent(JSON.stringify(context)),
|
|
64
|
+
mode: 'deterministic'
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Override potentially non-deterministic built-ins
|
|
68
|
+
this.overrideNonDeterministicValues(deterministicContext);
|
|
69
|
+
|
|
70
|
+
return deterministicContext;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Override non-deterministic values in context
|
|
75
|
+
*/
|
|
76
|
+
overrideNonDeterministicValues(context) {
|
|
77
|
+
// Replace any Date objects with static date
|
|
78
|
+
this.replaceInObject(context, (key, value) => {
|
|
79
|
+
if (value instanceof Date) {
|
|
80
|
+
return new Date(this.staticBuildTime);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Replace current timestamp references
|
|
84
|
+
if (typeof value === 'string' && this.looksLikeTimestamp(value)) {
|
|
85
|
+
return this.staticBuildTime;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return value;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Recursively replace values in object
|
|
94
|
+
*/
|
|
95
|
+
replaceInObject(obj, replacer) {
|
|
96
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
97
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
98
|
+
this.replaceInObject(value, replacer);
|
|
99
|
+
} else if (Array.isArray(value)) {
|
|
100
|
+
for (let i = 0; i < value.length; i++) {
|
|
101
|
+
if (typeof value[i] === 'object' && value[i] !== null) {
|
|
102
|
+
this.replaceInObject(value[i], replacer);
|
|
103
|
+
} else {
|
|
104
|
+
value[i] = replacer(i.toString(), value[i]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
obj[key] = replacer(key, value);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if string looks like a timestamp
|
|
115
|
+
*/
|
|
116
|
+
looksLikeTimestamp(value) {
|
|
117
|
+
// ISO 8601 format
|
|
118
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/.test(value)) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Unix timestamp (seconds or milliseconds)
|
|
123
|
+
if (/^\d{10,13}$/.test(value)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validate context doesn't contain non-deterministic operations
|
|
132
|
+
*/
|
|
133
|
+
validateDeterministicContext(context) {
|
|
134
|
+
const nonDeterministicPatterns = [
|
|
135
|
+
/Math\.random/,
|
|
136
|
+
/Date\.now/,
|
|
137
|
+
/new Date\(\)/,
|
|
138
|
+
/crypto\.randomUUID/,
|
|
139
|
+
/Math\.floor\(Math\.random/
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
const contextStr = JSON.stringify(context);
|
|
143
|
+
|
|
144
|
+
nonDeterministicPatterns.forEach(pattern => {
|
|
145
|
+
if (pattern.test(contextStr)) {
|
|
146
|
+
throw new Error(`Non-deterministic operation detected: ${pattern.source}`);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Post-process rendered output for consistency
|
|
153
|
+
*/
|
|
154
|
+
postProcessOutput(output) {
|
|
155
|
+
// Normalize line endings
|
|
156
|
+
let processed = output.replace(/\r\n|\r/g, '\n');
|
|
157
|
+
|
|
158
|
+
// Remove trailing whitespace from lines (but keep empty lines)
|
|
159
|
+
processed = processed.replace(/[^\S\n]+$/gm, '');
|
|
160
|
+
|
|
161
|
+
// Ensure consistent final newline
|
|
162
|
+
if (processed.length > 0 && !processed.endsWith('\n')) {
|
|
163
|
+
processed += '\n';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return processed;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get deterministic time string
|
|
171
|
+
*/
|
|
172
|
+
getDeterministicTime() {
|
|
173
|
+
return this.staticBuildTime;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Generate deterministic hash
|
|
178
|
+
*/
|
|
179
|
+
hashContent(content) {
|
|
180
|
+
return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Verify template produces identical output across runs
|
|
185
|
+
*/
|
|
186
|
+
async verifyDeterminism(nunjucksEnv, templateContent, context, iterations = 3) {
|
|
187
|
+
const outputs = [];
|
|
188
|
+
const hashes = new Set();
|
|
189
|
+
|
|
190
|
+
for (let i = 0; i < iterations; i++) {
|
|
191
|
+
const output = await this.render(nunjucksEnv, templateContent, context);
|
|
192
|
+
const hash = this.hashContent(output);
|
|
193
|
+
|
|
194
|
+
outputs.push({ iteration: i + 1, output, hash });
|
|
195
|
+
hashes.add(hash);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const isDeterministic = hashes.size === 1;
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
isDeterministic,
|
|
202
|
+
iterations,
|
|
203
|
+
uniqueOutputs: hashes.size,
|
|
204
|
+
outputs: outputs.slice(0, 2), // Include first 2 for comparison
|
|
205
|
+
firstHash: outputs[0]?.hash,
|
|
206
|
+
allHashesSame: isDeterministic
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Create reproducibility report
|
|
212
|
+
*/
|
|
213
|
+
async createReproducibilityReport(nunjucksEnv, templateContent, context) {
|
|
214
|
+
const verification = await this.verifyDeterminism(nunjucksEnv, templateContent, context, 5);
|
|
215
|
+
|
|
216
|
+
const report = {
|
|
217
|
+
timestamp: this.getDeterministicTime(),
|
|
218
|
+
templateHash: this.hashContent(templateContent),
|
|
219
|
+
contextHash: this.hashContent(JSON.stringify(context)),
|
|
220
|
+
deterministicSettings: {
|
|
221
|
+
staticBuildTime: this.staticBuildTime,
|
|
222
|
+
blockNonDeterministic: this.blockNonDeterministic,
|
|
223
|
+
strictMode: this.strictMode
|
|
224
|
+
},
|
|
225
|
+
verification,
|
|
226
|
+
reproducible: verification.isDeterministic,
|
|
227
|
+
confidence: verification.isDeterministic ? 'HIGH' : 'LOW'
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return report;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get renderer statistics
|
|
235
|
+
*/
|
|
236
|
+
getStats() {
|
|
237
|
+
return {
|
|
238
|
+
staticBuildTime: this.staticBuildTime,
|
|
239
|
+
blockNonDeterministic: this.blockNonDeterministic,
|
|
240
|
+
strictMode: this.strictMode
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export default DeterministicRenderer;
|