@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,475 @@
1
+ /**
2
+ * KGEN Deterministic Test Doubles - London BDD Style
3
+ *
4
+ * Mission: Provide deterministic alternatives for nondeterministic APIs
5
+ * Style: London School TDD - Test Doubles, Mocks, Stubs, Fakes
6
+ *
7
+ * These test doubles ensure that all tests produce consistent, deterministic results
8
+ * by replacing nondeterministic dependencies with predictable alternatives.
9
+ *
10
+ * Generated by: Determinism Sentinel Agent
11
+ * Authority: ZERO TOLERANCE for nondeterminism
12
+ */
13
+
14
+ import { createHash } from 'crypto';
15
+ import { EventEmitter } from 'events';
16
+
17
+ /**
18
+ * TEMPORAL TEST DOUBLES
19
+ * Replace all time-based nondeterministic functions
20
+ */
21
+
22
+ export class DeterministicDate extends Date {
23
+ static FIXED_TIMESTAMP = '2025-01-01T00:00:00.000Z';
24
+ static FIXED_TIME_MS = new Date(DeterministicDate.FIXED_TIMESTAMP).getTime();
25
+
26
+ constructor(...args) {
27
+ if (args.length === 0) {
28
+ // No arguments = current time -> use fixed time
29
+ super(DeterministicDate.FIXED_TIMESTAMP);
30
+ } else if (args.length === 1 && typeof args[0] === 'undefined') {
31
+ // undefined argument -> use fixed time
32
+ super(DeterministicDate.FIXED_TIMESTAMP);
33
+ } else {
34
+ // Specific time provided -> use it (deterministic)
35
+ super(...args);
36
+ }
37
+ }
38
+
39
+ static now() {
40
+ return DeterministicDate.FIXED_TIME_MS;
41
+ }
42
+
43
+ static deterministic() {
44
+ return DeterministicDate.FIXED_TIME_MS;
45
+ }
46
+ }
47
+
48
+ export class MockMath {
49
+ static seed = 0.12345; // Fixed seed for deterministic "random"
50
+
51
+ static random() {
52
+ // Linear congruential generator - deterministic pseudorandom
53
+ MockMath.seed = (MockMath.seed * 9301 + 49297) % 233280;
54
+ return MockMath.seed / 233280;
55
+ }
56
+
57
+ static resetSeed() {
58
+ MockMath.seed = 0.12345;
59
+ }
60
+
61
+ static setSeed(newSeed) {
62
+ MockMath.seed = newSeed;
63
+ }
64
+
65
+ // Proxy all other Math methods
66
+ static floor = Math.floor;
67
+ static ceil = Math.ceil;
68
+ static round = Math.round;
69
+ static abs = Math.abs;
70
+ static max = Math.max;
71
+ static min = Math.min;
72
+ }
73
+
74
+ /**
75
+ * DETERMINISTIC ID GENERATOR
76
+ * Replace UUID and random ID generation with content-based deterministic IDs
77
+ */
78
+
79
+ export class DeterministicIdGenerator {
80
+ constructor(config = {}) {
81
+ this.config = {
82
+ defaultSalt: 'kgen-deterministic',
83
+ hashAlgorithm: 'sha256',
84
+ idLength: 8,
85
+ ...config
86
+ };
87
+ this.counter = 0;
88
+ }
89
+
90
+ generateId(content, salt = null) {
91
+ const actualSalt = salt || this.config.defaultSalt;
92
+ const hash = createHash(this.config.hashAlgorithm);
93
+
94
+ // Ensure deterministic serialization of content
95
+ const serializedContent = typeof content === 'object'
96
+ ? JSON.stringify(content, Object.keys(content).sort())
97
+ : String(content);
98
+
99
+ hash.update(serializedContent);
100
+ hash.update(actualSalt);
101
+ hash.update(String(this.counter++)); // Add counter for uniqueness
102
+
103
+ return hash.digest('hex').substring(0, this.config.idLength);
104
+ }
105
+
106
+ generateUUID(content, salt = null) {
107
+ const hash = this.generateId(content, salt);
108
+ // Format as UUID v4 structure (but deterministic)
109
+ return `${hash.substring(0, 8)}-${hash.substring(8, 12)}-4${hash.substring(12, 15)}-8${hash.substring(15, 18)}-${hash.substring(18, 30)}000000`.substring(0, 36);
110
+ }
111
+
112
+ reset() {
113
+ this.counter = 0;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * ENVIRONMENT TEST DOUBLES
119
+ * Replace process.env and system information with deterministic alternatives
120
+ */
121
+
122
+ export class MockProcessEnv {
123
+ static FIXED_ENV = {
124
+ NODE_ENV: 'test',
125
+ BASE_URL: 'https://test.example.com',
126
+ API_KEY: 'test-api-key-deterministic',
127
+ DATABASE_URL: 'sqlite://test.db',
128
+ REDIS_URL: 'redis://localhost:6379/0',
129
+ PORT: '3000',
130
+ HOST: '0.0.0.0'
131
+ };
132
+
133
+ static get(key) {
134
+ return MockProcessEnv.FIXED_ENV[key];
135
+ }
136
+
137
+ static set(key, value) {
138
+ MockProcessEnv.FIXED_ENV[key] = value;
139
+ }
140
+
141
+ static reset() {
142
+ MockProcessEnv.FIXED_ENV = { ...MockProcessEnv.FIXED_ENV };
143
+ }
144
+
145
+ static toObject() {
146
+ return { ...MockProcessEnv.FIXED_ENV };
147
+ }
148
+ }
149
+
150
+ export class MockOS {
151
+ static FIXED_INFO = {
152
+ hostname: 'test-hostname',
153
+ platform: 'linux',
154
+ arch: 'x64',
155
+ version: '1.0.0-test',
156
+ tmpdir: '/tmp/test',
157
+ userInfo: {
158
+ username: 'test-user',
159
+ uid: 1000,
160
+ gid: 1000,
161
+ shell: '/bin/bash',
162
+ homedir: '/home/test-user'
163
+ }
164
+ };
165
+
166
+ static hostname() {
167
+ return MockOS.FIXED_INFO.hostname;
168
+ }
169
+
170
+ static platform() {
171
+ return MockOS.FIXED_INFO.platform;
172
+ }
173
+
174
+ static arch() {
175
+ return MockOS.FIXED_INFO.arch;
176
+ }
177
+
178
+ static version() {
179
+ return MockOS.FIXED_INFO.version;
180
+ }
181
+
182
+ static tmpdir() {
183
+ return MockOS.FIXED_INFO.tmpdir;
184
+ }
185
+
186
+ static userInfo() {
187
+ return { ...MockOS.FIXED_INFO.userInfo };
188
+ }
189
+ }
190
+
191
+ /**
192
+ * I/O TEST DOUBLES
193
+ * Replace filesystem operations with deterministic alternatives
194
+ */
195
+
196
+ export class MockFS {
197
+ static sortFiles = true;
198
+ static fixedFiles = new Map();
199
+
200
+ static async readdir(path, options = {}) {
201
+ // Simulate async operation
202
+ await new Promise(resolve => setTimeout(resolve, 0));
203
+
204
+ const files = MockFS.fixedFiles.get(path) || [
205
+ 'file-a.js',
206
+ 'file-b.js',
207
+ 'file-c.js'
208
+ ];
209
+
210
+ return MockFS.sortFiles ? files.sort() : files;
211
+ }
212
+
213
+ static async readFile(path, encoding = 'utf8') {
214
+ await new Promise(resolve => setTimeout(resolve, 0));
215
+
216
+ const content = MockFS.fixedFiles.get(`content:${path}`) ||
217
+ `// Mock file content for ${path}\nexport default 'deterministic content';`;
218
+
219
+ return content;
220
+ }
221
+
222
+ static setFixedFiles(path, files) {
223
+ MockFS.fixedFiles.set(path, files);
224
+ }
225
+
226
+ static setFixedContent(path, content) {
227
+ MockFS.fixedFiles.set(`content:${path}`, content);
228
+ }
229
+
230
+ static reset() {
231
+ MockFS.fixedFiles.clear();
232
+ MockFS.sortFiles = true;
233
+ }
234
+ }
235
+
236
+ export class MockGlob {
237
+ static patterns = new Map();
238
+
239
+ static async glob(pattern, options = {}) {
240
+ await new Promise(resolve => setTimeout(resolve, 0));
241
+
242
+ const matches = MockGlob.patterns.get(pattern) || [
243
+ `match1-${pattern}`,
244
+ `match2-${pattern}`,
245
+ `match3-${pattern}`
246
+ ];
247
+
248
+ return matches.sort(); // Always sorted for determinism
249
+ }
250
+
251
+ static setFixedMatches(pattern, matches) {
252
+ MockGlob.patterns.set(pattern, matches);
253
+ }
254
+
255
+ static reset() {
256
+ MockGlob.patterns.clear();
257
+ }
258
+ }
259
+
260
+ /**
261
+ * NETWORK TEST DOUBLES
262
+ * Replace network operations with deterministic responses
263
+ */
264
+
265
+ export class MockFetch {
266
+ static responses = new Map();
267
+ static defaultResponse = {
268
+ ok: true,
269
+ status: 200,
270
+ statusText: 'OK',
271
+ json: async () => ({ message: 'deterministic response' }),
272
+ text: async () => 'deterministic text response'
273
+ };
274
+
275
+ static async fetch(url, options = {}) {
276
+ await new Promise(resolve => setTimeout(resolve, 10)); // Simulate network delay
277
+
278
+ const key = `${options.method || 'GET'} ${url}`;
279
+ const response = MockFetch.responses.get(key) || MockFetch.defaultResponse;
280
+
281
+ return {
282
+ ...response,
283
+ url,
284
+ options: { ...options }
285
+ };
286
+ }
287
+
288
+ static setResponse(method, url, response) {
289
+ const key = `${method} ${url}`;
290
+ MockFetch.responses.set(key, response);
291
+ }
292
+
293
+ static reset() {
294
+ MockFetch.responses.clear();
295
+ }
296
+ }
297
+
298
+ /**
299
+ * COMPREHENSIVE TEST ENVIRONMENT SETUP
300
+ * Replace all nondeterministic globals with deterministic alternatives
301
+ */
302
+
303
+ export class DeterministicTestEnvironment {
304
+ constructor() {
305
+ this.originalGlobals = {};
306
+ this.idGenerator = new DeterministicIdGenerator();
307
+ this.isActive = false;
308
+ }
309
+
310
+ activate() {
311
+ if (this.isActive) return;
312
+
313
+ // Store originals
314
+ this.originalGlobals = {
315
+ Date: global.Date,
316
+ Math: global.Math,
317
+ process: global.process,
318
+ require: global.require
319
+ };
320
+
321
+ // Replace with deterministic versions
322
+ global.Date = DeterministicDate;
323
+ global.Math = new Proxy(Math, {
324
+ get: (target, prop) => {
325
+ if (prop === 'random') return MockMath.random;
326
+ return target[prop];
327
+ }
328
+ });
329
+
330
+ // Mock process.env access
331
+ if (global.process) {
332
+ const originalEnv = global.process.env;
333
+ global.process.env = new Proxy(originalEnv, {
334
+ get: (target, prop) => {
335
+ return MockProcessEnv.get(prop) || target[prop];
336
+ }
337
+ });
338
+ }
339
+
340
+ this.isActive = true;
341
+ }
342
+
343
+ deactivate() {
344
+ if (!this.isActive) return;
345
+
346
+ // Restore originals
347
+ Object.assign(global, this.originalGlobals);
348
+ this.isActive = false;
349
+ }
350
+
351
+ reset() {
352
+ MockMath.resetSeed();
353
+ MockProcessEnv.reset();
354
+ MockFS.reset();
355
+ MockGlob.reset();
356
+ MockFetch.reset();
357
+ this.idGenerator.reset();
358
+ }
359
+
360
+ generateDeterministicData(schema) {
361
+ const data = {};
362
+
363
+ for (const [key, type] of Object.entries(schema)) {
364
+ switch (type) {
365
+ case 'id':
366
+ data[key] = this.idGenerator.generateId(key);
367
+ break;
368
+ case 'uuid':
369
+ data[key] = this.idGenerator.generateUUID(key);
370
+ break;
371
+ case 'timestamp':
372
+ data[key] = DeterministicDate.FIXED_TIMESTAMP;
373
+ break;
374
+ case 'number':
375
+ data[key] = MockMath.random() * 1000;
376
+ break;
377
+ case 'string':
378
+ data[key] = `deterministic-${key}-${this.idGenerator.generateId(key)}`;
379
+ break;
380
+ default:
381
+ data[key] = `${type}-${key}`;
382
+ }
383
+ }
384
+
385
+ return data;
386
+ }
387
+ }
388
+
389
+ /**
390
+ * BDD-STYLE TEST HELPERS
391
+ * Behavior-driven development helpers for deterministic testing
392
+ */
393
+
394
+ export class BDDTestHelpers {
395
+ static createDeterministicFixture(name, schema) {
396
+ const env = new DeterministicTestEnvironment();
397
+ env.activate();
398
+
399
+ const fixture = env.generateDeterministicData(schema);
400
+
401
+ env.deactivate();
402
+
403
+ return fixture;
404
+ }
405
+
406
+ static async verifyDeterministicBehavior(testFunction, iterations = 3) {
407
+ const results = [];
408
+ const env = new DeterministicTestEnvironment();
409
+
410
+ for (let i = 0; i < iterations; i++) {
411
+ env.activate();
412
+ env.reset(); // Reset to same initial state
413
+
414
+ const result = await testFunction();
415
+ results.push(JSON.stringify(result, Object.keys(result).sort()));
416
+
417
+ env.deactivate();
418
+ }
419
+
420
+ // All results should be identical
421
+ const firstResult = results[0];
422
+ const allIdentical = results.every(result => result === firstResult);
423
+
424
+ if (!allIdentical) {
425
+ throw new Error(`Nondeterministic behavior detected. Results: ${results.join(' | ')}`);
426
+ }
427
+
428
+ return JSON.parse(firstResult);
429
+ }
430
+
431
+ static createMockContext(overrides = {}) {
432
+ return {
433
+ metadata: {
434
+ buildTime: DeterministicDate.FIXED_TIMESTAMP,
435
+ targetPlatform: 'test-platform',
436
+ ...overrides.metadata
437
+ },
438
+ config: {
439
+ environment: 'test',
440
+ targetHost: 'test-host',
441
+ baseUrl: 'https://test.example.com',
442
+ ...overrides.config
443
+ },
444
+ ...overrides
445
+ };
446
+ }
447
+ }
448
+
449
+ /**
450
+ * EXPORT ALL TEST DOUBLES
451
+ */
452
+
453
+ export const DeterministicTestDoubles = {
454
+ // Temporal
455
+ Date: DeterministicDate,
456
+ Math: MockMath,
457
+
458
+ // Environment
459
+ ProcessEnv: MockProcessEnv,
460
+ OS: MockOS,
461
+
462
+ // I/O
463
+ FS: MockFS,
464
+ Glob: MockGlob,
465
+
466
+ // Network
467
+ Fetch: MockFetch,
468
+
469
+ // Utilities
470
+ IdGenerator: DeterministicIdGenerator,
471
+ TestEnvironment: DeterministicTestEnvironment,
472
+ BDDHelpers: BDDTestHelpers
473
+ };
474
+
475
+ export default DeterministicTestDoubles;
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Frontmatter Parser - YAML frontmatter extraction and parsing
3
+ * Migrated from ~/unjucks with enhanced error handling
4
+ */
5
+
6
+ import matter from 'gray-matter';
7
+ import { parse as parseYAML, stringify as stringifyYAML } from 'yaml';
8
+
9
+ export class FrontmatterParser {
10
+ constructor(options = {}) {
11
+ this.strict = options.strict !== false;
12
+ this.allowEmpty = options.allowEmpty !== false;
13
+ }
14
+
15
+ /**
16
+ * Parse frontmatter from template content
17
+ */
18
+ parse(content) {
19
+ try {
20
+ const result = matter(content, {
21
+ engines: {
22
+ yaml: {
23
+ parse: parseYAML,
24
+ stringify: stringifyYAML
25
+ }
26
+ }
27
+ });
28
+
29
+ return {
30
+ frontmatter: result.data || {},
31
+ content: result.content || '',
32
+ isEmpty: result.isEmpty,
33
+ excerpt: result.excerpt
34
+ };
35
+
36
+ } catch (error) {
37
+ if (this.strict) {
38
+ throw new Error(`Frontmatter parsing failed: ${error.message}`);
39
+ }
40
+
41
+ // Return content as-is if parsing fails in non-strict mode
42
+ return {
43
+ frontmatter: {},
44
+ content: content,
45
+ isEmpty: true,
46
+ parseError: error.message
47
+ };
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Extract only frontmatter without parsing content
53
+ */
54
+ extractFrontmatter(content) {
55
+ try {
56
+ const result = matter(content);
57
+ return {
58
+ success: true,
59
+ frontmatter: result.data || {},
60
+ isEmpty: result.isEmpty
61
+ };
62
+ } catch (error) {
63
+ return {
64
+ success: false,
65
+ error: error.message,
66
+ frontmatter: {}
67
+ };
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Validate frontmatter structure
73
+ */
74
+ validate(frontmatter, schema = {}) {
75
+ const errors = [];
76
+ const warnings = [];
77
+
78
+ // Check required fields
79
+ if (schema.required) {
80
+ schema.required.forEach(field => {
81
+ if (!(field in frontmatter)) {
82
+ errors.push(`Missing required field: ${field}`);
83
+ }
84
+ });
85
+ }
86
+
87
+ // Check field types
88
+ if (schema.types) {
89
+ Object.entries(schema.types).forEach(([field, expectedType]) => {
90
+ if (field in frontmatter) {
91
+ const actualType = typeof frontmatter[field];
92
+ if (actualType !== expectedType) {
93
+ errors.push(`Field '${field}' should be ${expectedType}, got ${actualType}`);
94
+ }
95
+ }
96
+ });
97
+ }
98
+
99
+ // Check allowed values
100
+ if (schema.enum) {
101
+ Object.entries(schema.enum).forEach(([field, allowedValues]) => {
102
+ if (field in frontmatter) {
103
+ if (!allowedValues.includes(frontmatter[field])) {
104
+ errors.push(`Field '${field}' must be one of: ${allowedValues.join(', ')}`);
105
+ }
106
+ }
107
+ });
108
+ }
109
+
110
+ // Template-specific validations
111
+ this.validateTemplateFields(frontmatter, errors, warnings);
112
+
113
+ return {
114
+ valid: errors.length === 0,
115
+ errors,
116
+ warnings
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Validate template-specific frontmatter fields
122
+ */
123
+ validateTemplateFields(frontmatter, errors, warnings) {
124
+ // Check for common template metadata
125
+ const recommendedFields = ['name', 'description', 'version'];
126
+ recommendedFields.forEach(field => {
127
+ if (!(field in frontmatter)) {
128
+ warnings.push(`Recommended field missing: ${field}`);
129
+ }
130
+ });
131
+
132
+ // Validate variables definition
133
+ if (frontmatter.variables) {
134
+ if (typeof frontmatter.variables !== 'object') {
135
+ errors.push('variables field should be an object');
136
+ } else {
137
+ Object.entries(frontmatter.variables).forEach(([varName, varDesc]) => {
138
+ if (typeof varDesc !== 'string' && typeof varDesc !== 'object') {
139
+ warnings.push(`Variable '${varName}' should have a description`);
140
+ }
141
+ });
142
+ }
143
+ }
144
+
145
+ // Validate template type
146
+ if (frontmatter.type) {
147
+ const validTypes = ['component', 'page', 'service', 'model', 'config', 'documentation'];
148
+ if (!validTypes.includes(frontmatter.type)) {
149
+ warnings.push(`Unknown template type: ${frontmatter.type}`);
150
+ }
151
+ }
152
+
153
+ // Validate output configuration
154
+ if (frontmatter.output) {
155
+ if (typeof frontmatter.output === 'string') {
156
+ // Simple path - OK
157
+ } else if (typeof frontmatter.output === 'object') {
158
+ // Complex output config - validate structure
159
+ if (frontmatter.output.path && typeof frontmatter.output.path !== 'string') {
160
+ errors.push('output.path must be a string');
161
+ }
162
+ if (frontmatter.output.mode && !['write', 'append', 'inject'].includes(frontmatter.output.mode)) {
163
+ errors.push('output.mode must be one of: write, append, inject');
164
+ }
165
+ } else {
166
+ errors.push('output must be a string path or object configuration');
167
+ }
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Merge frontmatter with defaults
173
+ */
174
+ mergeWithDefaults(frontmatter, defaults = {}) {
175
+ return {
176
+ // Template metadata defaults
177
+ name: frontmatter.name || defaults.name || 'Untitled Template',
178
+ description: frontmatter.description || defaults.description || '',
179
+ version: frontmatter.version || defaults.version || '1.0.0',
180
+ author: frontmatter.author || defaults.author || '',
181
+
182
+ // Template configuration
183
+ type: frontmatter.type || defaults.type || 'component',
184
+ category: frontmatter.category || defaults.category || 'general',
185
+ tags: frontmatter.tags || defaults.tags || [],
186
+
187
+ // Output configuration
188
+ output: frontmatter.output || defaults.output,
189
+
190
+ // Variable definitions
191
+ variables: {
192
+ ...defaults.variables,
193
+ ...frontmatter.variables
194
+ },
195
+
196
+ // Custom fields
197
+ ...Object.fromEntries(
198
+ Object.entries(frontmatter).filter(([key]) =>
199
+ !['name', 'description', 'version', 'author', 'type', 'category', 'tags', 'output', 'variables'].includes(key)
200
+ )
201
+ )
202
+ };
203
+ }
204
+
205
+ /**
206
+ * Convert frontmatter back to YAML string
207
+ */
208
+ stringify(frontmatter, content = '') {
209
+ try {
210
+ const yamlString = stringifyYAML(frontmatter);
211
+ return `---\n${yamlString}---\n${content}`;
212
+ } catch (error) {
213
+ throw new Error(`Failed to stringify frontmatter: ${error.message}`);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Get parser statistics
219
+ */
220
+ getStats() {
221
+ return {
222
+ strict: this.strict,
223
+ allowEmpty: this.allowEmpty
224
+ };
225
+ }
226
+ }
227
+
228
+ export default FrontmatterParser;