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