@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,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;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Linter Module Exports
3
+ */
4
+
5
+ export { TemplateLinter } from './determinism.js';
6
+ export default { TemplateLinter };