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