@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,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Linter - Enforce deterministic template patterns
|
|
3
|
+
* Migrated from ~/unjucks with comprehensive determinism checking
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class TemplateLinter {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.strict = options.strict !== false;
|
|
9
|
+
this.warnOnly = options.warnOnly === true;
|
|
10
|
+
this.customRules = options.customRules || [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Lint template for determinism issues
|
|
15
|
+
*/
|
|
16
|
+
lint(templateContent, frontmatter = {}) {
|
|
17
|
+
const issues = [];
|
|
18
|
+
const warnings = [];
|
|
19
|
+
const suggestions = [];
|
|
20
|
+
|
|
21
|
+
// Run all lint checks
|
|
22
|
+
this.checkNonDeterministicOperations(templateContent, issues);
|
|
23
|
+
this.checkDateTimeUsage(templateContent, issues, warnings);
|
|
24
|
+
this.checkRandomOperations(templateContent, issues);
|
|
25
|
+
this.checkSystemDependentOperations(templateContent, warnings);
|
|
26
|
+
this.checkVariableConsistency(templateContent, warnings);
|
|
27
|
+
this.checkFrontmatterCompliance(frontmatter, warnings, suggestions);
|
|
28
|
+
|
|
29
|
+
// Run custom rules if provided
|
|
30
|
+
this.customRules.forEach(rule => {
|
|
31
|
+
try {
|
|
32
|
+
rule(templateContent, frontmatter, issues, warnings, suggestions);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
warnings.push({
|
|
35
|
+
rule: 'custom-rule',
|
|
36
|
+
message: `Custom rule failed: ${error.message}`,
|
|
37
|
+
severity: 'warning',
|
|
38
|
+
line: 0
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Calculate determinism score
|
|
44
|
+
const score = this.calculateDeterminismScore(issues, warnings);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
deterministic: issues.length === 0,
|
|
48
|
+
score,
|
|
49
|
+
issues: issues.sort((a, b) => b.severity.localeCompare(a.severity)),
|
|
50
|
+
warnings: warnings.sort((a, b) => a.line - b.line),
|
|
51
|
+
suggestions: suggestions.sort((a, b) => a.priority - b.priority),
|
|
52
|
+
summary: {
|
|
53
|
+
totalIssues: issues.length,
|
|
54
|
+
criticalIssues: issues.filter(i => i.severity === 'error').length,
|
|
55
|
+
warnings: warnings.length,
|
|
56
|
+
suggestions: suggestions.length,
|
|
57
|
+
passesLint: issues.filter(i => i.severity === 'error').length === 0
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check for non-deterministic operations
|
|
64
|
+
*/
|
|
65
|
+
checkNonDeterministicOperations(content, issues) {
|
|
66
|
+
const nonDeterministicPatterns = [
|
|
67
|
+
{
|
|
68
|
+
pattern: /\{\{\s*([^}]*now[^}]*)\s*\}\}/gi,
|
|
69
|
+
message: 'Non-deterministic time operation: {{ now }}. Use {{ timestamp }} filter instead.',
|
|
70
|
+
rule: 'no-current-time',
|
|
71
|
+
severity: 'error',
|
|
72
|
+
fix: 'Replace with {{ timestamp }} or use deterministic date filters'
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
pattern: /\{\{\s*([^}]*random[^}]*)\s*\}\}/gi,
|
|
76
|
+
message: 'Non-deterministic random operation. Use {{ content | hash }} for consistent randomness.',
|
|
77
|
+
rule: 'no-random',
|
|
78
|
+
severity: 'error',
|
|
79
|
+
fix: 'Use hash filters for deterministic pseudo-randomness'
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
pattern: /\{\{\s*([^}]*uuid[^}]*)\s*\}\}/gi,
|
|
83
|
+
message: 'Non-deterministic UUID generation. Use {{ content | hash | shortHash }} instead.',
|
|
84
|
+
rule: 'no-uuid',
|
|
85
|
+
severity: 'error',
|
|
86
|
+
fix: 'Use hash-based ID generation for consistency'
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
pattern: /Math\.random\(\)/gi,
|
|
90
|
+
message: 'Direct Math.random() usage is non-deterministic.',
|
|
91
|
+
rule: 'no-math-random',
|
|
92
|
+
severity: 'error',
|
|
93
|
+
fix: 'Use hash-based pseudo-random generation'
|
|
94
|
+
}
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
nonDeterministicPatterns.forEach(({ pattern, message, rule, severity, fix }) => {
|
|
98
|
+
const matches = [...content.matchAll(pattern)];
|
|
99
|
+
matches.forEach(match => {
|
|
100
|
+
const line = this.getLineNumber(content, match.index);
|
|
101
|
+
issues.push({
|
|
102
|
+
rule,
|
|
103
|
+
message,
|
|
104
|
+
severity,
|
|
105
|
+
line,
|
|
106
|
+
column: match.index - this.getLineStart(content, match.index),
|
|
107
|
+
match: match[0],
|
|
108
|
+
fix
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check date/time usage patterns
|
|
116
|
+
*/
|
|
117
|
+
checkDateTimeUsage(content, issues, warnings) {
|
|
118
|
+
const dateTimePatterns = [
|
|
119
|
+
{
|
|
120
|
+
pattern: /new Date\(\)/gi,
|
|
121
|
+
message: 'new Date() without arguments is non-deterministic.',
|
|
122
|
+
type: 'error',
|
|
123
|
+
rule: 'no-current-date'
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
pattern: /Date\.now\(\)/gi,
|
|
127
|
+
message: 'Date.now() is non-deterministic.',
|
|
128
|
+
type: 'error',
|
|
129
|
+
rule: 'no-date-now'
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
pattern: /\{\{\s*([^}]*date[^}]*)\s*\}\}/gi,
|
|
133
|
+
message: 'Date usage detected. Verify it uses deterministic formatting.',
|
|
134
|
+
type: 'warning',
|
|
135
|
+
rule: 'verify-date-usage'
|
|
136
|
+
}
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
dateTimePatterns.forEach(({ pattern, message, type, rule }) => {
|
|
140
|
+
const matches = [...content.matchAll(pattern)];
|
|
141
|
+
matches.forEach(match => {
|
|
142
|
+
const line = this.getLineNumber(content, match.index);
|
|
143
|
+
const issue = {
|
|
144
|
+
rule,
|
|
145
|
+
message,
|
|
146
|
+
line,
|
|
147
|
+
column: match.index - this.getLineStart(content, match.index),
|
|
148
|
+
match: match[0]
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
if (type === 'error') {
|
|
152
|
+
issue.severity = 'error';
|
|
153
|
+
issues.push(issue);
|
|
154
|
+
} else {
|
|
155
|
+
issue.severity = 'warning';
|
|
156
|
+
warnings.push(issue);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Check for random operations
|
|
164
|
+
*/
|
|
165
|
+
checkRandomOperations(content, issues) {
|
|
166
|
+
const randomPatterns = [
|
|
167
|
+
/crypto\.randomUUID\(\)/gi,
|
|
168
|
+
/crypto\.getRandomValues\(/gi,
|
|
169
|
+
/Math\.floor\(Math\.random\(\)/gi,
|
|
170
|
+
/\|\s*random\s*[\|}]/gi
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
randomPatterns.forEach(pattern => {
|
|
174
|
+
const matches = [...content.matchAll(pattern)];
|
|
175
|
+
matches.forEach(match => {
|
|
176
|
+
const line = this.getLineNumber(content, match.index);
|
|
177
|
+
issues.push({
|
|
178
|
+
rule: 'no-random-operations',
|
|
179
|
+
message: `Random operation detected: ${match[0]}. This breaks deterministic rendering.`,
|
|
180
|
+
severity: 'error',
|
|
181
|
+
line,
|
|
182
|
+
column: match.index - this.getLineStart(content, match.index),
|
|
183
|
+
match: match[0],
|
|
184
|
+
fix: 'Use hash-based deterministic alternatives'
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Check for system-dependent operations
|
|
192
|
+
*/
|
|
193
|
+
checkSystemDependentOperations(content, warnings) {
|
|
194
|
+
const systemPatterns = [
|
|
195
|
+
{
|
|
196
|
+
pattern: /process\.env\./gi,
|
|
197
|
+
message: 'Environment variable usage may cause non-deterministic output across systems.',
|
|
198
|
+
rule: 'env-vars-warning'
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
pattern: /os\./gi,
|
|
202
|
+
message: 'OS-dependent operations may cause different output across platforms.',
|
|
203
|
+
rule: 'os-dependent-warning'
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
pattern: /require\(['"]fs['"]\)/gi,
|
|
207
|
+
message: 'File system operations may be non-deterministic.',
|
|
208
|
+
rule: 'fs-operations-warning'
|
|
209
|
+
}
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
systemPatterns.forEach(({ pattern, message, rule }) => {
|
|
213
|
+
const matches = [...content.matchAll(pattern)];
|
|
214
|
+
matches.forEach(match => {
|
|
215
|
+
const line = this.getLineNumber(content, match.index);
|
|
216
|
+
warnings.push({
|
|
217
|
+
rule,
|
|
218
|
+
message,
|
|
219
|
+
severity: 'warning',
|
|
220
|
+
line,
|
|
221
|
+
column: match.index - this.getLineStart(content, match.index),
|
|
222
|
+
match: match[0]
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Check variable naming consistency
|
|
230
|
+
*/
|
|
231
|
+
checkVariableConsistency(content, warnings) {
|
|
232
|
+
// Extract all variable references
|
|
233
|
+
const variablePattern = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_.]*)/g;
|
|
234
|
+
const variables = new Map();
|
|
235
|
+
|
|
236
|
+
let match;
|
|
237
|
+
while ((match = variablePattern.exec(content)) !== null) {
|
|
238
|
+
const varName = match[1].split('.')[0]; // Get root variable name
|
|
239
|
+
const line = this.getLineNumber(content, match.index);
|
|
240
|
+
|
|
241
|
+
if (!variables.has(varName)) {
|
|
242
|
+
variables.set(varName, []);
|
|
243
|
+
}
|
|
244
|
+
variables.get(varName).push({ line, usage: match[1] });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check for inconsistent variable usage patterns
|
|
248
|
+
variables.forEach((usages, varName) => {
|
|
249
|
+
if (usages.length === 1) {
|
|
250
|
+
warnings.push({
|
|
251
|
+
rule: 'unused-variable',
|
|
252
|
+
message: `Variable '${varName}' is only used once. Consider if it's necessary.`,
|
|
253
|
+
severity: 'info',
|
|
254
|
+
line: usages[0].line,
|
|
255
|
+
column: 0
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Check frontmatter compliance
|
|
263
|
+
*/
|
|
264
|
+
checkFrontmatterCompliance(frontmatter, warnings, suggestions) {
|
|
265
|
+
const requiredFields = ['name', 'description'];
|
|
266
|
+
const recommendedFields = ['version', 'author', 'category'];
|
|
267
|
+
|
|
268
|
+
// Check required fields
|
|
269
|
+
requiredFields.forEach(field => {
|
|
270
|
+
if (!frontmatter[field]) {
|
|
271
|
+
warnings.push({
|
|
272
|
+
rule: 'missing-required-frontmatter',
|
|
273
|
+
message: `Missing required frontmatter field: ${field}`,
|
|
274
|
+
severity: 'warning',
|
|
275
|
+
line: 0,
|
|
276
|
+
section: 'frontmatter'
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Check recommended fields
|
|
282
|
+
recommendedFields.forEach(field => {
|
|
283
|
+
if (!frontmatter[field]) {
|
|
284
|
+
suggestions.push({
|
|
285
|
+
rule: 'recommended-frontmatter',
|
|
286
|
+
message: `Consider adding frontmatter field: ${field}`,
|
|
287
|
+
priority: 2,
|
|
288
|
+
section: 'frontmatter'
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Validate variables documentation
|
|
294
|
+
if (frontmatter.variables) {
|
|
295
|
+
if (typeof frontmatter.variables !== 'object') {
|
|
296
|
+
warnings.push({
|
|
297
|
+
rule: 'invalid-variables-format',
|
|
298
|
+
message: 'Frontmatter variables should be an object with descriptions',
|
|
299
|
+
severity: 'warning',
|
|
300
|
+
line: 0,
|
|
301
|
+
section: 'frontmatter'
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
suggestions.push({
|
|
306
|
+
rule: 'document-variables',
|
|
307
|
+
message: 'Consider documenting template variables in frontmatter',
|
|
308
|
+
priority: 1,
|
|
309
|
+
section: 'frontmatter'
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Calculate determinism score (0-100)
|
|
316
|
+
*/
|
|
317
|
+
calculateDeterminismScore(issues, warnings) {
|
|
318
|
+
let score = 100;
|
|
319
|
+
|
|
320
|
+
// Subtract points for issues
|
|
321
|
+
issues.forEach(issue => {
|
|
322
|
+
switch (issue.severity) {
|
|
323
|
+
case 'error':
|
|
324
|
+
score -= 20;
|
|
325
|
+
break;
|
|
326
|
+
case 'warning':
|
|
327
|
+
score -= 5;
|
|
328
|
+
break;
|
|
329
|
+
default:
|
|
330
|
+
score -= 1;
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Subtract points for warnings
|
|
335
|
+
warnings.forEach(warning => {
|
|
336
|
+
score -= 2;
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
return Math.max(0, score);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get line number for character position
|
|
344
|
+
*/
|
|
345
|
+
getLineNumber(content, position) {
|
|
346
|
+
return content.substring(0, position).split('\n').length;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Get start position of line containing character position
|
|
351
|
+
*/
|
|
352
|
+
getLineStart(content, position) {
|
|
353
|
+
const beforePosition = content.substring(0, position);
|
|
354
|
+
const lastNewline = beforePosition.lastIndexOf('\n');
|
|
355
|
+
return lastNewline === -1 ? 0 : lastNewline + 1;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Auto-fix determinism issues where possible
|
|
360
|
+
*/
|
|
361
|
+
autoFix(templateContent) {
|
|
362
|
+
let fixed = templateContent;
|
|
363
|
+
let fixCount = 0;
|
|
364
|
+
|
|
365
|
+
const fixes = [
|
|
366
|
+
{
|
|
367
|
+
pattern: /\{\{\s*now\s*\}\}/gi,
|
|
368
|
+
replacement: '{{ timestamp }}',
|
|
369
|
+
description: 'Replace {{ now }} with {{ timestamp }}'
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
pattern: /\{\{\s*([^}]*)\s*\|\s*random\s*\}\}/gi,
|
|
373
|
+
replacement: '{{ $1 | hash | shortHash }}',
|
|
374
|
+
description: 'Replace random filter with hash-based alternative'
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
pattern: /Math\.random\(\)/gi,
|
|
378
|
+
replacement: '/* Use hash-based randomness instead */',
|
|
379
|
+
description: 'Comment out Math.random() usage'
|
|
380
|
+
}
|
|
381
|
+
];
|
|
382
|
+
|
|
383
|
+
fixes.forEach(({ pattern, replacement, description }) => {
|
|
384
|
+
const matches = fixed.match(pattern);
|
|
385
|
+
if (matches) {
|
|
386
|
+
fixed = fixed.replace(pattern, replacement);
|
|
387
|
+
fixCount += matches.length;
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
fixed,
|
|
393
|
+
fixCount,
|
|
394
|
+
modified: fixCount > 0
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Get linter statistics
|
|
400
|
+
*/
|
|
401
|
+
getStats() {
|
|
402
|
+
return {
|
|
403
|
+
strict: this.strict,
|
|
404
|
+
warnOnly: this.warnOnly,
|
|
405
|
+
customRulesCount: this.customRules.length
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export default TemplateLinter;
|