@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,518 @@
1
+ /**
2
+ * KGEN Filter System - Deterministic template filters
3
+ *
4
+ * Implements all v1 Lock specification filters:
5
+ * - Text: upper, lower, trim, replace, split, join, slice
6
+ * - Data: default, unique, sort, groupby, map, sum, count
7
+ * - Format: json, md, csv
8
+ * - RDF: prefix, expand, sparql
9
+ * - Validation: shaclReport
10
+ * - CAS: casDigest, attestRef
11
+ */
12
+
13
+ import crypto from 'crypto';
14
+
15
+ export class KGenFilters {
16
+ constructor(options = {}) {
17
+ this.options = {
18
+ deterministicMode: options.deterministicMode !== false,
19
+ strictMode: options.strictMode !== false,
20
+ ...options
21
+ };
22
+
23
+ this.filters = new Map();
24
+ this.registerCoreFilters();
25
+ }
26
+
27
+ /**
28
+ * Register a filter function
29
+ */
30
+ register(name, filterFunction, options = {}) {
31
+ if (typeof filterFunction !== 'function') {
32
+ throw new Error(`Filter '${name}' must be a function`);
33
+ }
34
+
35
+ this.filters.set(name, {
36
+ function: filterFunction,
37
+ deterministic: options.deterministic !== false,
38
+ description: options.description || '',
39
+ category: options.category || 'custom'
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Apply filter to value
45
+ */
46
+ apply(filterName, value, ...args) {
47
+ const filter = this.filters.get(filterName);
48
+
49
+ if (!filter) {
50
+ if (this.options.strictMode) {
51
+ throw new Error(`Unknown filter: ${filterName}`);
52
+ }
53
+ return value; // Return original value if filter not found
54
+ }
55
+
56
+ // Check deterministic mode compliance
57
+ if (this.options.deterministicMode && !filter.deterministic) {
58
+ throw new Error(`Filter '${filterName}' is not deterministic and cannot be used in deterministic mode`);
59
+ }
60
+
61
+ try {
62
+ return filter.function(value, ...args);
63
+ } catch (error) {
64
+ if (this.options.strictMode) {
65
+ throw new Error(`Filter '${filterName}' failed: ${error.message}`);
66
+ }
67
+ return value; // Return original value on error
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Check if filter exists
73
+ */
74
+ has(filterName) {
75
+ return this.filters.has(filterName);
76
+ }
77
+
78
+ /**
79
+ * Get filter count
80
+ */
81
+ getFilterCount() {
82
+ return this.filters.size;
83
+ }
84
+
85
+ /**
86
+ * Register all core filters
87
+ */
88
+ registerCoreFilters() {
89
+ this.registerTextFilters();
90
+ this.registerDataFilters();
91
+ this.registerFormatFilters();
92
+ this.registerRDFFilters();
93
+ this.registerValidationFilters();
94
+ this.registerCASFilters();
95
+ this.registerUtilityFilters();
96
+ }
97
+
98
+ /**
99
+ * Text processing filters
100
+ */
101
+ registerTextFilters() {
102
+ this.register('upper', (str) => String(str || '').toUpperCase(), {
103
+ category: 'text',
104
+ description: 'Convert string to uppercase'
105
+ });
106
+
107
+ this.register('lower', (str) => String(str || '').toLowerCase(), {
108
+ category: 'text',
109
+ description: 'Convert string to lowercase'
110
+ });
111
+
112
+ this.register('trim', (str) => String(str || '').trim(), {
113
+ category: 'text',
114
+ description: 'Remove leading and trailing whitespace'
115
+ });
116
+
117
+ this.register('replace', (str, search, replace = '') => {
118
+ const searchRegex = typeof search === 'string' ?
119
+ new RegExp(search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g') :
120
+ search;
121
+ return String(str || '').replace(searchRegex, replace);
122
+ }, {
123
+ category: 'text',
124
+ description: 'Replace occurrences of search string'
125
+ });
126
+
127
+ this.register('split', (str, separator = '') => {
128
+ return String(str || '').split(separator);
129
+ }, {
130
+ category: 'text',
131
+ description: 'Split string into array'
132
+ });
133
+
134
+ this.register('join', (arr, separator = '') => {
135
+ if (!Array.isArray(arr)) return String(arr || '');
136
+ return arr.join(separator);
137
+ }, {
138
+ category: 'text',
139
+ description: 'Join array elements into string'
140
+ });
141
+
142
+ this.register('slice', (str, start = 0, end) => {
143
+ const s = String(str || '');
144
+ return end !== undefined ? s.slice(start, end) : s.slice(start);
145
+ }, {
146
+ category: 'text',
147
+ description: 'Extract substring by position'
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Data processing filters
153
+ */
154
+ registerDataFilters() {
155
+ this.register('default', (value, defaultValue = '') => {
156
+ return (value === null || value === undefined || value === '') ? defaultValue : value;
157
+ }, {
158
+ category: 'data',
159
+ description: 'Provide default value if original is empty'
160
+ });
161
+
162
+ this.register('unique', (arr) => {
163
+ if (!Array.isArray(arr)) return arr;
164
+ return [...new Set(arr)];
165
+ }, {
166
+ category: 'data',
167
+ description: 'Remove duplicate values from array'
168
+ });
169
+
170
+ this.register('sort', (arr, key) => {
171
+ if (!Array.isArray(arr)) return arr;
172
+
173
+ return [...arr].sort((a, b) => {
174
+ let aVal, bVal;
175
+
176
+ if (key) {
177
+ aVal = typeof a === 'object' ? a[key] : a;
178
+ bVal = typeof b === 'object' ? b[key] : b;
179
+ } else {
180
+ aVal = a;
181
+ bVal = b;
182
+ }
183
+
184
+ // Handle null/undefined consistently
185
+ if (aVal == null && bVal == null) return 0;
186
+ if (aVal == null) return -1;
187
+ if (bVal == null) return 1;
188
+
189
+ // Stable sort for determinism
190
+ return aVal > bVal ? 1 : aVal < bVal ? -1 : 0;
191
+ });
192
+ }, {
193
+ category: 'data',
194
+ description: 'Sort array by value or key'
195
+ });
196
+
197
+ this.register('groupby', (arr, key) => {
198
+ if (!Array.isArray(arr)) return {};
199
+
200
+ const groups = {};
201
+ arr.forEach(item => {
202
+ const groupKey = typeof item === 'object' && item !== null ?
203
+ item[key] : String(item);
204
+
205
+ if (!groups[groupKey]) {
206
+ groups[groupKey] = [];
207
+ }
208
+ groups[groupKey].push(item);
209
+ });
210
+
211
+ return groups;
212
+ }, {
213
+ category: 'data',
214
+ description: 'Group array elements by key'
215
+ });
216
+
217
+ this.register('map', (arr, key) => {
218
+ if (!Array.isArray(arr)) return arr;
219
+
220
+ return arr.map(item => {
221
+ if (typeof item === 'object' && item !== null) {
222
+ return item[key];
223
+ }
224
+ return item;
225
+ });
226
+ }, {
227
+ category: 'data',
228
+ description: 'Extract values by key from array of objects'
229
+ });
230
+
231
+ this.register('sum', (arr, key) => {
232
+ if (!Array.isArray(arr)) return 0;
233
+
234
+ return arr.reduce((sum, item) => {
235
+ let val;
236
+ if (key && typeof item === 'object' && item !== null) {
237
+ val = item[key];
238
+ } else {
239
+ val = item;
240
+ }
241
+
242
+ const num = Number(val);
243
+ return sum + (isNaN(num) ? 0 : num);
244
+ }, 0);
245
+ }, {
246
+ category: 'data',
247
+ description: 'Sum numeric values in array'
248
+ });
249
+
250
+ this.register('count', (arr) => {
251
+ if (Array.isArray(arr)) return arr.length;
252
+ if (typeof arr === 'object' && arr !== null) return Object.keys(arr).length;
253
+ return 0;
254
+ }, {
255
+ category: 'data',
256
+ description: 'Count elements in array or object'
257
+ });
258
+ }
259
+
260
+ /**
261
+ * Format output filters
262
+ */
263
+ registerFormatFilters() {
264
+ this.register('json', (obj, indent) => {
265
+ try {
266
+ const indentValue = indent ? (typeof indent === 'number' ? indent : 2) : 0;
267
+ return JSON.stringify(obj, null, indentValue);
268
+ } catch (error) {
269
+ return '{}';
270
+ }
271
+ }, {
272
+ category: 'format',
273
+ description: 'Convert object to JSON string'
274
+ });
275
+
276
+ this.register('md', (str) => {
277
+ // Basic markdown escaping for safety
278
+ return String(str || '').replace(/([*_`\\])/g, '\\$1');
279
+ }, {
280
+ category: 'format',
281
+ description: 'Escape markdown special characters'
282
+ });
283
+
284
+ this.register('csv', (arr, delimiter = ',') => {
285
+ if (!Array.isArray(arr)) return '';
286
+
287
+ return arr.map(item => {
288
+ if (typeof item === 'object' && item !== null) {
289
+ return JSON.stringify(item).replace(/"/g, '""');
290
+ }
291
+ const str = String(item);
292
+ return str.includes(delimiter) ? `"${str.replace(/"/g, '""')}"` : str;
293
+ }).join(delimiter);
294
+ }, {
295
+ category: 'format',
296
+ description: 'Convert array to CSV format'
297
+ });
298
+ }
299
+
300
+ /**
301
+ * RDF and semantic web filters
302
+ */
303
+ registerRDFFilters() {
304
+ this.register('prefix', (uri, prefixes = {}) => {
305
+ if (!uri || typeof uri !== 'string') return uri;
306
+
307
+ // Find matching prefix
308
+ for (const [prefix, namespace] of Object.entries(prefixes)) {
309
+ if (uri.startsWith(namespace)) {
310
+ return uri.replace(namespace, `${prefix}:`);
311
+ }
312
+ }
313
+
314
+ return uri;
315
+ }, {
316
+ category: 'rdf',
317
+ description: 'Convert full URI to prefixed form'
318
+ });
319
+
320
+ this.register('expand', (prefixedUri, prefixes = {}) => {
321
+ if (!prefixedUri || typeof prefixedUri !== 'string') return prefixedUri;
322
+
323
+ const colonIndex = prefixedUri.indexOf(':');
324
+ if (colonIndex === -1) return prefixedUri;
325
+
326
+ const prefix = prefixedUri.substring(0, colonIndex);
327
+ const suffix = prefixedUri.substring(colonIndex + 1);
328
+
329
+ if (prefixes[prefix]) {
330
+ return prefixes[prefix] + suffix;
331
+ }
332
+
333
+ return prefixedUri;
334
+ }, {
335
+ category: 'rdf',
336
+ description: 'Expand prefixed URI to full form'
337
+ });
338
+
339
+ this.register('sparql', (query, params = {}) => {
340
+ if (!query || typeof query !== 'string') return '';
341
+
342
+ let processedQuery = query;
343
+
344
+ // Simple parameter substitution for deterministic queries
345
+ Object.entries(params).forEach(([key, value]) => {
346
+ const placeholder = new RegExp(`\\$\\{${key}\\}`, 'g');
347
+ processedQuery = processedQuery.replace(placeholder, String(value));
348
+ });
349
+
350
+ return processedQuery;
351
+ }, {
352
+ category: 'rdf',
353
+ description: 'Process SPARQL query with parameters'
354
+ });
355
+ }
356
+
357
+ /**
358
+ * Validation filters
359
+ */
360
+ registerValidationFilters() {
361
+ this.register('shaclReport', (data, shaclShapes = {}) => {
362
+ // Simplified SHACL validation for deterministic behavior
363
+ const report = {
364
+ conforms: true,
365
+ results: [],
366
+ timestamp: this.options.deterministicMode ? '2024-01-01T00:00:00.000Z' : new Date().toISOString()
367
+ };
368
+
369
+ if (!data || typeof data !== 'object') {
370
+ report.conforms = false;
371
+ report.results.push({
372
+ severity: 'Violation',
373
+ message: 'Invalid data format for SHACL validation'
374
+ });
375
+ }
376
+
377
+ // TODO: Implement full SHACL validation logic
378
+ // For now, return basic conformance report
379
+
380
+ return report;
381
+ }, {
382
+ category: 'validation',
383
+ description: 'Generate SHACL validation report'
384
+ });
385
+ }
386
+
387
+ /**
388
+ * Content Addressable Storage (CAS) filters
389
+ */
390
+ registerCASFilters() {
391
+ this.register('casDigest', (content, algorithm = 'sha256') => {
392
+ try {
393
+ const hash = crypto.createHash(algorithm);
394
+ hash.update(String(content || ''), 'utf8');
395
+ return hash.digest('hex');
396
+ } catch (error) {
397
+ if (this.options.strictMode) {
398
+ throw new Error(`CAS digest failed: ${error.message}`);
399
+ }
400
+ return '';
401
+ }
402
+ }, {
403
+ category: 'cas',
404
+ description: 'Generate content-addressable digest'
405
+ });
406
+
407
+ this.register('attestRef', (content, options = {}) => {
408
+ const digest = this.apply('casDigest', content, options.algorithm || 'sha256');
409
+ const timestamp = this.options.deterministicMode ?
410
+ '2024-01-01T00:00:00.000Z' :
411
+ new Date().toISOString();
412
+
413
+ return {
414
+ digest,
415
+ algorithm: options.algorithm || 'sha256',
416
+ timestamp,
417
+ attestor: options.attestor || 'kgen-templates',
418
+ version: '1.0.0'
419
+ };
420
+ }, {
421
+ category: 'cas',
422
+ description: 'Generate attestation reference for content'
423
+ });
424
+ }
425
+
426
+ /**
427
+ * Utility filters
428
+ */
429
+ registerUtilityFilters() {
430
+ // Non-deterministic filters that throw in deterministic mode
431
+ this.register('now', () => {
432
+ if (this.options.deterministicMode) {
433
+ throw new Error('Filter "now" is not allowed in deterministic mode');
434
+ }
435
+ return new Date().toISOString();
436
+ }, {
437
+ category: 'utility',
438
+ deterministic: false,
439
+ description: 'Get current timestamp (non-deterministic)'
440
+ });
441
+
442
+ this.register('random', () => {
443
+ if (this.options.deterministicMode) {
444
+ throw new Error('Filter "random" is not allowed in deterministic mode');
445
+ }
446
+ return Math.random();
447
+ }, {
448
+ category: 'utility',
449
+ deterministic: false,
450
+ description: 'Generate random number (non-deterministic)'
451
+ });
452
+
453
+ this.register('uuid', () => {
454
+ if (this.options.deterministicMode) {
455
+ throw new Error('Filter "uuid" is not allowed in deterministic mode');
456
+ }
457
+ return crypto.randomUUID();
458
+ }, {
459
+ category: 'utility',
460
+ deterministic: false,
461
+ description: 'Generate UUID (non-deterministic)'
462
+ });
463
+ }
464
+
465
+ /**
466
+ * Get all filters by category
467
+ */
468
+ getFiltersByCategory(category) {
469
+ const result = {};
470
+
471
+ for (const [name, filter] of this.filters) {
472
+ if (filter.category === category) {
473
+ result[name] = {
474
+ description: filter.description,
475
+ deterministic: filter.deterministic
476
+ };
477
+ }
478
+ }
479
+
480
+ return result;
481
+ }
482
+
483
+ /**
484
+ * List all filter names
485
+ */
486
+ listFilters() {
487
+ return Array.from(this.filters.keys()).sort();
488
+ }
489
+
490
+ /**
491
+ * Get filter statistics
492
+ */
493
+ getStats() {
494
+ const categories = {};
495
+ let deterministicCount = 0;
496
+
497
+ for (const [name, filter] of this.filters) {
498
+ if (!categories[filter.category]) {
499
+ categories[filter.category] = 0;
500
+ }
501
+ categories[filter.category]++;
502
+
503
+ if (filter.deterministic) {
504
+ deterministicCount++;
505
+ }
506
+ }
507
+
508
+ return {
509
+ totalFilters: this.filters.size,
510
+ deterministicFilters: deterministicCount,
511
+ nonDeterministicFilters: this.filters.size - deterministicCount,
512
+ categories,
513
+ deterministicMode: this.options.deterministicMode
514
+ };
515
+ }
516
+ }
517
+
518
+ export default KGenFilters;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * KGEN Core Template Engine - Native implementation without nunjucks
3
+ *
4
+ * Pipeline: plan → render → post → attest
5
+ * Features: Deterministic rendering, custom filters, attestation support
6
+ */
7
+
8
+ export { KGenTemplateEngine } from './kgen-engine.js';
9
+ export { KGenParser } from './parser.js';
10
+ export { KGenFilters } from './filters.js';
11
+ export { KGenRenderer } from './renderer.js';
12
+ export { KGenPostProcessor } from './post-processor.js';
13
+ export { KGenAttestor } from './attestor.js';
14
+
15
+ // Convenience factory function
16
+ export function createKGenEngine(options = {}) {
17
+ return new KGenTemplateEngine(options);
18
+ }
19
+
20
+ // Default export
21
+ export { KGenTemplateEngine as default };