@mistralys/persona-builder 0.2.0

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/dist/index.cjs ADDED
@@ -0,0 +1,437 @@
1
+ 'use strict';
2
+
3
+ var module$1 = require('module');
4
+ var promises = require('fs/promises');
5
+ var path4 = require('path');
6
+ var yaml2 = require('js-yaml');
7
+ var fs = require('fs');
8
+
9
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
10
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
+
12
+ var path4__default = /*#__PURE__*/_interopDefault(path4);
13
+ var yaml2__default = /*#__PURE__*/_interopDefault(yaml2);
14
+
15
+ // src/index.ts
16
+
17
+ // src/engine/partials.ts
18
+ function resolvePartials(text, partialsMap, depth = 0) {
19
+ if (depth >= 2) return text;
20
+ return text.replace(/\{\{> ([\w-]+)\}\}/g, (match, name) => {
21
+ if (!(name in partialsMap)) {
22
+ console.warn(`[WARN] Partial not found: ${match}`);
23
+ return match;
24
+ }
25
+ return resolvePartials(partialsMap[name], partialsMap, depth + 1).trimEnd();
26
+ });
27
+ }
28
+
29
+ // src/engine/conditionals.ts
30
+ function resolveConditionals(text, context) {
31
+ return text.replace(
32
+ /\n*\{\{#if (\w+)\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if\}\}\n*/g,
33
+ (_match, flag, inner, elseInner) => {
34
+ if (context[flag]) {
35
+ return "\n" + inner.replace(/^\n+/, "").replace(/\n+$/, "") + "\n";
36
+ }
37
+ if (elseInner !== void 0) {
38
+ return "\n" + elseInner.replace(/^\n+/, "").replace(/\n+$/, "") + "\n";
39
+ }
40
+ return "\n";
41
+ }
42
+ );
43
+ }
44
+
45
+ // src/engine/variables.ts
46
+ function resolveVariables(text, context, filename) {
47
+ return text.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
48
+ if (varName in context && context[varName] !== void 0) {
49
+ return String(context[varName]);
50
+ }
51
+ console.warn(`[WARN] Unresolved variable: ${match} in ${filename}`);
52
+ return match;
53
+ });
54
+ }
55
+
56
+ // src/engine/postProcessor.ts
57
+ function collapseBlankLines(text) {
58
+ return text.replace(/\n{4,}/g, "\n\n\n");
59
+ }
60
+ function ensureBlankLineBeforeHeadings(text) {
61
+ let result = text.replace(/([^\n])\n(#{1,6} )/g, "$1\n\n$2");
62
+ result = result.replace(/([^\n])\n(---)\n/g, "$1\n\n$2\n");
63
+ result = result.replace(/\n(---)\n([^\n])/g, "\n$1\n\n$2");
64
+ return result;
65
+ }
66
+ function normalizeNewlines(text) {
67
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
68
+ }
69
+
70
+ // src/engine/serializer.ts
71
+ function serializeTools(tools) {
72
+ return "[" + tools.map((t) => `'${t}'`).join(", ") + "]";
73
+ }
74
+ function serializeToolsList(tools) {
75
+ return tools.map((t) => `'${t}'`).join(", ");
76
+ }
77
+ async function loadPartials(dir) {
78
+ const entries = await promises.readdir(dir, { withFileTypes: true });
79
+ const mdFiles = entries.filter(
80
+ (entry) => entry.isFile() && entry.name.endsWith(".md")
81
+ );
82
+ const pairs = await Promise.all(
83
+ mdFiles.map(async (entry) => {
84
+ const stem = entry.name.slice(0, -".md".length);
85
+ const filePath = path4__default.default.join(dir, entry.name);
86
+ const content = await promises.readFile(filePath, "utf8");
87
+ return [stem, content];
88
+ })
89
+ );
90
+ return Object.fromEntries(pairs);
91
+ }
92
+ async function discoverPersonaYamls(root) {
93
+ const absRoot = path4__default.default.resolve(root);
94
+ const allEntries = await promises.readdir(absRoot, { recursive: true, withFileTypes: false });
95
+ const yamlPaths = allEntries.filter((entry) => entry.endsWith(".yaml")).map((entry) => path4__default.default.join(absRoot, entry)).sort();
96
+ return yamlPaths;
97
+ }
98
+ async function loadMetadata(yamlPath) {
99
+ const raw = await promises.readFile(yamlPath, "utf8");
100
+ const parsed = yaml2__default.default.load(raw);
101
+ if (parsed === null || parsed === void 0 || typeof parsed !== "object" || Array.isArray(parsed)) {
102
+ throw new Error(
103
+ `loadMetadata: expected a YAML object in "${yamlPath}", got ${Array.isArray(parsed) ? "array" : String(parsed)}`
104
+ );
105
+ }
106
+ const record = parsed;
107
+ if (typeof record["name"] !== "string" || record["name"].trim() === "") {
108
+ throw new Error(
109
+ `loadMetadata: YAML file "${yamlPath}" is missing a required string field "name"`
110
+ );
111
+ }
112
+ return record;
113
+ }
114
+ async function loadContent(mdPath) {
115
+ const absPath = path4__default.default.resolve(mdPath);
116
+ return promises.readFile(absPath, "utf8");
117
+ }
118
+
119
+ // src/plugins/runner.ts
120
+ function runSuiteInit(plugins, suite, sharedMeta) {
121
+ for (const plugin of plugins) {
122
+ if (typeof plugin.onSuiteInit === "function") {
123
+ plugin.onSuiteInit(suite, sharedMeta);
124
+ }
125
+ }
126
+ }
127
+ function runBuildContext(plugins, ctx, persona, suite) {
128
+ let accumulated = ctx;
129
+ for (const plugin of plugins) {
130
+ if (typeof plugin.onBuildContext === "function") {
131
+ accumulated = plugin.onBuildContext(accumulated, persona, suite);
132
+ }
133
+ }
134
+ return accumulated;
135
+ }
136
+ function runPostRender(plugins, rendered, persona, target) {
137
+ let output = rendered;
138
+ for (const plugin of plugins) {
139
+ if (typeof plugin.onPostRender === "function") {
140
+ output = plugin.onPostRender(output, persona, target);
141
+ }
142
+ }
143
+ return output;
144
+ }
145
+ function runValidate(plugins, persona, suite) {
146
+ const results = [];
147
+ for (const plugin of plugins) {
148
+ if (typeof plugin.onValidate === "function") {
149
+ const pluginResults = plugin.onValidate(persona, suite);
150
+ results.push(...pluginResults);
151
+ }
152
+ }
153
+ return results;
154
+ }
155
+
156
+ // src/builders/frontmatter.ts
157
+ var DEFAULT_FRONTMATTER_VSCODE = `---
158
+ name: '{{name}} v{{version}}'
159
+ description: '{{description}}'
160
+ tools: [{{tools_list}}]
161
+ ---`;
162
+ var DEFAULT_FRONTMATTER_CLAUDE_CODE = `---
163
+ name: {{cc_file_name_stem}}
164
+ permissionMode: {{cc_permission_mode}}
165
+ model: {{cc_model}}
166
+ memory: {{cc_memory}}
167
+ allowedTools: [{{cc_tools_list}}]
168
+ ---`;
169
+ function resolveFrontmatterTemplate(target, plugins, configTemplates) {
170
+ for (const plugin of plugins) {
171
+ if (plugin.frontmatterTemplates && target in plugin.frontmatterTemplates) {
172
+ const tpl = plugin.frontmatterTemplates[target];
173
+ if (tpl !== void 0) return tpl;
174
+ }
175
+ }
176
+ if (configTemplates && target in configTemplates) {
177
+ const tpl = configTemplates[target];
178
+ if (tpl !== void 0) return tpl;
179
+ }
180
+ return target === "vscode" ? DEFAULT_FRONTMATTER_VSCODE : DEFAULT_FRONTMATTER_CLAUDE_CODE;
181
+ }
182
+ function renderFrontmatter(template, context, filename) {
183
+ let rendered = resolveConditionals(template, context);
184
+ rendered = resolveVariables(rendered, context, filename);
185
+ return rendered;
186
+ }
187
+ async function discoverSuitePersonaYamls(suiteConfig) {
188
+ const metaSubdir = suiteConfig.metaSubdir ?? "meta";
189
+ const metaDir = path4__default.default.join(suiteConfig.srcDir, metaSubdir);
190
+ const entries = await promises.readdir(metaDir, { withFileTypes: true });
191
+ return entries.filter((e) => e.isFile() && e.name.endsWith(".yaml") && !e.name.startsWith("_")).map((e) => path4__default.default.join(metaDir, e.name)).sort();
192
+ }
193
+ async function loadRawYaml(filePath) {
194
+ if (!fs.existsSync(filePath)) return {};
195
+ const raw = await promises.readFile(filePath, "utf8");
196
+ const parsed = yaml2__default.default.load(raw);
197
+ if (parsed === null || parsed === void 0) return {};
198
+ if (typeof parsed !== "object" || Array.isArray(parsed)) return {};
199
+ return parsed;
200
+ }
201
+ async function loadPersonaYaml(yamlPath) {
202
+ const raw = await promises.readFile(yamlPath, "utf8");
203
+ const parsed = yaml2__default.default.load(raw);
204
+ if (parsed === null || parsed === void 0 || typeof parsed !== "object" || Array.isArray(parsed)) {
205
+ throw new Error(`buildPersona: expected a YAML object in "${yamlPath}"`);
206
+ }
207
+ const record = parsed;
208
+ if (!record["name"]) {
209
+ record["name"] = path4__default.default.basename(yamlPath, ".yaml");
210
+ }
211
+ return record;
212
+ }
213
+ function buildContext(personaMeta, sharedMeta) {
214
+ const version = typeof personaMeta["version"] === "string" ? personaMeta["version"] : typeof sharedMeta["default_version"] === "string" ? sharedMeta["default_version"] : "0.0.0";
215
+ const merged = {
216
+ ...sharedMeta,
217
+ ...personaMeta,
218
+ version
219
+ };
220
+ const tools = Array.isArray(merged["tools"]) ? merged["tools"] : [];
221
+ if (!("tools_list" in merged)) {
222
+ merged["tools_list"] = serializeToolsList(tools);
223
+ }
224
+ if (!("tools_json" in merged)) {
225
+ merged["tools_json"] = serializeTools(tools);
226
+ }
227
+ const ccTools = Array.isArray(merged["cc_tools"]) ? merged["cc_tools"] : tools;
228
+ if (!("cc_tools_list" in merged)) {
229
+ merged["cc_tools_list"] = serializeToolsList(ccTools);
230
+ }
231
+ if (!("cc_tools_json" in merged)) {
232
+ merged["cc_tools_json"] = serializeTools(ccTools);
233
+ }
234
+ if (!("cc_file_name_stem" in merged) && typeof merged["cc_file_name"] === "string") {
235
+ const ccFileName = merged["cc_file_name"];
236
+ merged["cc_file_name_stem"] = ccFileName.replace(/\.md$/, "");
237
+ }
238
+ return merged;
239
+ }
240
+ async function buildPersona(personaYamlPath, suiteName, suiteConfig, sharedMeta, partialsMap, config, plugins, target) {
241
+ const personaMeta = await loadPersonaYaml(personaYamlPath);
242
+ let context = buildContext(personaMeta, sharedMeta);
243
+ const personaMetaTyped = personaMeta;
244
+ context = runBuildContext(plugins, context, personaMetaTyped, suiteConfig);
245
+ const fmTemplate = resolveFrontmatterTemplate(target, plugins, config.frontmatter);
246
+ const contentBasename = path4__default.default.basename(personaYamlPath, ".yaml") + ".md";
247
+ const frontmatter = renderFrontmatter(fmTemplate, context, contentBasename);
248
+ const contentSubdir = suiteConfig.contentSubdir ?? "content";
249
+ const contentPath = path4__default.default.join(suiteConfig.srcDir, contentSubdir, contentBasename);
250
+ const bodyTemplate = normalizeNewlines(await promises.readFile(contentPath, "utf8"));
251
+ let body = resolvePartials(bodyTemplate, partialsMap);
252
+ body = resolveConditionals(body, context);
253
+ body = resolveVariables(body, context, contentBasename);
254
+ body = collapseBlankLines(body);
255
+ body = ensureBlankLineBeforeHeadings(body);
256
+ body = body.trimEnd();
257
+ let output = normalizeNewlines(`${frontmatter}
258
+
259
+ ${body}
260
+ `);
261
+ output = runPostRender(plugins, output, personaMetaTyped, target);
262
+ const validationResults = runValidate(plugins, personaMetaTyped, suiteConfig);
263
+ const outputDir = target === "vscode" ? suiteConfig.outVscode : suiteConfig.outClaudeCode;
264
+ let outputBasename;
265
+ if (target === "vscode" && typeof context["vs_file_name"] === "string") {
266
+ outputBasename = context["vs_file_name"];
267
+ } else if (target === "claude-code" && typeof context["cc_file_name"] === "string") {
268
+ outputBasename = context["cc_file_name"];
269
+ } else {
270
+ outputBasename = contentBasename;
271
+ }
272
+ const outputPath = path4__default.default.join(outputDir, outputBasename);
273
+ const check = config.check ?? false;
274
+ let written = false;
275
+ if (!check) {
276
+ await promises.mkdir(outputDir, { recursive: true });
277
+ await promises.writeFile(outputPath, output, "utf8");
278
+ written = true;
279
+ }
280
+ return {
281
+ suite: suiteName,
282
+ target,
283
+ personaYamlPath,
284
+ outputPath,
285
+ content: output,
286
+ validationResults,
287
+ written
288
+ };
289
+ }
290
+ async function buildSuite(suiteName, suiteConfig, config, plugins, target) {
291
+ const metaSubdir = suiteConfig.metaSubdir ?? "meta";
292
+ const sharedYamlPath = path4__default.default.join(suiteConfig.srcDir, metaSubdir, "_shared.yaml");
293
+ const sharedMeta = await loadRawYaml(sharedYamlPath);
294
+ let partialsMap = {};
295
+ if (config.sharedPartialsDir && fs.existsSync(config.sharedPartialsDir)) {
296
+ partialsMap = { ...partialsMap, ...await loadPartials(config.sharedPartialsDir) };
297
+ }
298
+ const partialsSubdir = suiteConfig.partialsSubdir ?? "partials";
299
+ const suitePartialsDir = path4__default.default.join(suiteConfig.srcDir, partialsSubdir);
300
+ if (fs.existsSync(suitePartialsDir)) {
301
+ partialsMap = { ...partialsMap, ...await loadPartials(suitePartialsDir) };
302
+ }
303
+ runSuiteInit(plugins, suiteConfig, sharedMeta);
304
+ const personaYamlPaths = await discoverSuitePersonaYamls(suiteConfig);
305
+ const results = [];
306
+ for (const yamlPath of personaYamlPaths) {
307
+ const result = await buildPersona(
308
+ yamlPath,
309
+ suiteName,
310
+ suiteConfig,
311
+ sharedMeta,
312
+ partialsMap,
313
+ config,
314
+ plugins,
315
+ target
316
+ );
317
+ results.push(result);
318
+ }
319
+ return results;
320
+ }
321
+ async function build(config) {
322
+ const plugins = config.plugins ?? [];
323
+ const targets = config.targets ?? ["vscode", "claude-code"];
324
+ const allResults = [];
325
+ for (const [suiteName, suiteConfig] of Object.entries(config.suites)) {
326
+ for (const target of targets) {
327
+ const suiteResults = await buildSuite(suiteName, suiteConfig, config, plugins, target);
328
+ allResults.push(...suiteResults);
329
+ }
330
+ }
331
+ const strictFailures = config.strict ? allResults.flatMap(
332
+ (r) => r.validationResults.filter(
333
+ (v) => v.severity === "error" || v.severity === "warning"
334
+ )
335
+ ) : [];
336
+ const success = !config.strict || strictFailures.length === 0;
337
+ const summary = {
338
+ success,
339
+ results: allResults,
340
+ strictFailures,
341
+ totalBuilt: allResults.length,
342
+ totalWritten: allResults.filter((r) => r.written).length
343
+ };
344
+ if (config.strict && !success) {
345
+ const messages = strictFailures.map((f) => `[${f.severity}] ${f.message}`).join("\n");
346
+ throw new Error(
347
+ `Build failed in strict mode \u2014 ${strictFailures.length} validation issue(s):
348
+ ${messages}`
349
+ );
350
+ }
351
+ return summary;
352
+ }
353
+
354
+ // src/validators/filename-validator.ts
355
+ var FILENAME_RULES = [
356
+ {
357
+ description: "no uppercase letters",
358
+ violated: (name) => /[A-Z]/.test(name),
359
+ message: (name) => `Filename "${name}" contains uppercase letters. Use lowercase kebab-case (e.g. "my-persona.md").`
360
+ },
361
+ {
362
+ description: "no spaces",
363
+ violated: (name) => /\s/.test(name),
364
+ message: (name) => `Filename "${name}" contains spaces. Use hyphens to separate words (e.g. "my-persona.md").`
365
+ },
366
+ {
367
+ description: "kebab-case characters only",
368
+ violated: (name) => {
369
+ const segments = name.split(".");
370
+ if (segments.length === 1) {
371
+ return !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
372
+ }
373
+ return !segments.every((seg) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(seg));
374
+ },
375
+ message: (name) => `Filename "${name}" does not conform to kebab-case naming. Use lowercase letters, digits, and hyphens only (e.g. "my-persona.md").`
376
+ }
377
+ ];
378
+ function validateFileName(filePath) {
379
+ const basename = filePath.includes("/") ? filePath.split("/").pop() ?? filePath : filePath.includes("\\") ? filePath.split("\\").pop() ?? filePath : filePath;
380
+ const errors = [];
381
+ for (const rule of FILENAME_RULES) {
382
+ if (rule.violated(basename)) {
383
+ errors.push({
384
+ severity: "error",
385
+ message: rule.message(basename)
386
+ });
387
+ }
388
+ }
389
+ return errors;
390
+ }
391
+
392
+ // src/validators/strict-validator.ts
393
+ function validateStrictMarkers(renderedContent, requiredMarkers) {
394
+ const errors = [];
395
+ for (const marker of requiredMarkers) {
396
+ if (!renderedContent.includes(marker)) {
397
+ errors.push({
398
+ severity: "error",
399
+ message: `Required marker "${marker}" is missing from the rendered output.`
400
+ });
401
+ }
402
+ }
403
+ return errors;
404
+ }
405
+
406
+ // src/index.ts
407
+ var _pkgRequire = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
408
+ var VERSION = _pkgRequire("../package.json").version;
409
+
410
+ exports.DEFAULT_FRONTMATTER_CLAUDE_CODE = DEFAULT_FRONTMATTER_CLAUDE_CODE;
411
+ exports.DEFAULT_FRONTMATTER_VSCODE = DEFAULT_FRONTMATTER_VSCODE;
412
+ exports.VERSION = VERSION;
413
+ exports.build = build;
414
+ exports.buildPersona = buildPersona;
415
+ exports.buildSuite = buildSuite;
416
+ exports.collapseBlankLines = collapseBlankLines;
417
+ exports.discoverPersonaYamls = discoverPersonaYamls;
418
+ exports.ensureBlankLineBeforeHeadings = ensureBlankLineBeforeHeadings;
419
+ exports.loadContent = loadContent;
420
+ exports.loadMetadata = loadMetadata;
421
+ exports.loadPartials = loadPartials;
422
+ exports.normalizeNewlines = normalizeNewlines;
423
+ exports.renderFrontmatter = renderFrontmatter;
424
+ exports.resolveConditionals = resolveConditionals;
425
+ exports.resolveFrontmatterTemplate = resolveFrontmatterTemplate;
426
+ exports.resolvePartials = resolvePartials;
427
+ exports.resolveVariables = resolveVariables;
428
+ exports.runBuildContext = runBuildContext;
429
+ exports.runPostRender = runPostRender;
430
+ exports.runSuiteInit = runSuiteInit;
431
+ exports.runValidate = runValidate;
432
+ exports.serializeTools = serializeTools;
433
+ exports.serializeToolsList = serializeToolsList;
434
+ exports.validateFileName = validateFileName;
435
+ exports.validateStrictMarkers = validateStrictMarkers;
436
+ //# sourceMappingURL=index.cjs.map
437
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/engine/partials.ts","../src/engine/conditionals.ts","../src/engine/variables.ts","../src/engine/postProcessor.ts","../src/engine/serializer.ts","../src/loaders/partials-loader.ts","../src/loaders/metadata-loader.ts","../src/loaders/content-loader.ts","../src/plugins/runner.ts","../src/builders/frontmatter.ts","../src/builders/persona-builder.ts","../src/validators/filename-validator.ts","../src/validators/strict-validator.ts","../src/index.ts"],"names":["readdir","path","readFile","yaml","existsSync","mkdir","writeFile","createRequire"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\r\n * partials.ts\r\n *\r\n * Pure template-engine function for resolving partial inclusions.\r\n * Supports {{> name}} syntax with up to depth-2 recursion to handle\r\n * partials-within-partials. No file-system I/O.\r\n */\r\n\r\n/**\r\n * Resolve partial inclusions in a template string.\r\n *\r\n * Replaces `{{> name}}` markers with the content from `partialsMap`.\r\n * Recursion is capped at depth 2 so that:\r\n * - depth 0 → 1: outer partials are expanded\r\n * - depth 1 → 2: one level of nested partials are expanded\r\n * - depth 2: recursion stops, marker is left as-is\r\n *\r\n * Each resolved partial is `trimEnd()`-ed to prevent trailing blank lines\r\n * from causing double-blank-line artefacts during concatenation.\r\n *\r\n * If a partial name is not found in `partialsMap`, the original marker is\r\n * preserved and a warning is emitted via `console.warn`.\r\n *\r\n * @param text - Template string potentially containing {{> name}} markers\r\n * @param partialsMap - Map of partial name → partial content\r\n * @param depth - Current recursion depth (callers should omit; defaults to 0)\r\n * @returns The template string with partial markers replaced\r\n */\r\nexport function resolvePartials(\r\n text: string,\r\n partialsMap: Record<string, string>,\r\n depth = 0,\r\n): string {\r\n if (depth >= 2) return text;\r\n return text.replace(/\\{\\{> ([\\w-]+)\\}\\}/g, (match, name: string) => {\r\n if (!(name in partialsMap)) {\r\n console.warn(`[WARN] Partial not found: ${match}`);\r\n return match;\r\n }\r\n // Recursively resolve nested partials (depth + 1).\r\n // trimEnd() strips trailing whitespace to avoid extra blank lines.\r\n return resolvePartials(partialsMap[name], partialsMap, depth + 1).trimEnd();\r\n });\r\n}\r\n","/**\r\n * conditionals.ts\r\n *\r\n * Pure template-engine function for resolving conditional blocks.\r\n * Handles {{#if flag}}…{{/if}} and {{#if flag}}…{{else}}…{{/if}} syntax.\r\n * No file-system I/O.\r\n */\r\n\r\n/**\r\n * Resolve conditional blocks in a template string.\r\n *\r\n * Syntax:\r\n * `{{#if flag}}content{{/if}}`\r\n * `{{#if flag}}truthy-content{{else}}falsy-content{{/if}}`\r\n *\r\n * Behaviour:\r\n * - When `context[flag]` is truthy: the delimiters are stripped and the\r\n * content before `{{else}}` (or the entire inner block if no `{{else}}`)\r\n * is kept, surrounded by single `\\n` delimiters.\r\n * - When `context[flag]` is falsy and a `{{else}}` branch exists: the\r\n * content after `{{else}}` is kept, surrounded by single `\\n` delimiters.\r\n * - When `context[flag]` is falsy and no `{{else}}` branch exists: the\r\n * entire block (including surrounding newlines) is removed, leaving a\r\n * single `\\n`.\r\n * - Unknown flags (absent from context) are treated as falsy.\r\n *\r\n * Leading and trailing newlines within the kept content are trimmed so the\r\n * output does not accumulate extra blank lines.\r\n *\r\n * @param text - Template string potentially containing {{#if}} blocks\r\n * @param context - Key-value map used to evaluate flag truthiness\r\n * @returns The template string with conditional blocks resolved\r\n */\r\nexport function resolveConditionals(\r\n text: string,\r\n context: Record<string, unknown>,\r\n): string {\r\n return text.replace(\r\n /\\n*\\{\\{#if (\\w+)\\}\\}([\\s\\S]*?)(?:\\{\\{else\\}\\}([\\s\\S]*?))?\\{\\{\\/if\\}\\}\\n*/g,\r\n (\r\n _match: string,\r\n flag: string,\r\n inner: string,\r\n elseInner: string | undefined,\r\n ) => {\r\n if (context[flag]) {\r\n // Truthy: keep content before {{else}} (or entire inner if no {{else}})\r\n return '\\n' + inner.replace(/^\\n+/, '').replace(/\\n+$/, '') + '\\n';\r\n }\r\n if (elseInner !== undefined) {\r\n // Falsy with {{else}}: keep content after {{else}}\r\n return '\\n' + elseInner.replace(/^\\n+/, '').replace(/\\n+$/, '') + '\\n';\r\n }\r\n // Falsy without {{else}}: remove entire block\r\n return '\\n';\r\n },\r\n );\r\n}\r\n","/**\r\n * variables.ts\r\n *\r\n * Pure template-engine function for resolving variable substitutions.\r\n * Handles {{varName}} syntax. No file-system I/O.\r\n */\r\n\r\n/**\r\n * Resolve variable substitutions in a template string.\r\n *\r\n * Replaces `{{varName}}` markers with `String(context[varName])`.\r\n * If a variable is not found in `context` (or its value is `undefined`),\r\n * the original marker is preserved and a warning is emitted via\r\n * `console.warn`, identifying the file by `filename` for easier debugging.\r\n *\r\n * Note: this step must run AFTER `resolvePartials` and `resolveConditionals`\r\n * so that only plain variable markers remain.\r\n *\r\n * @param text - Template string potentially containing {{varName}} markers\r\n * @param context - Key-value map of variable name → value\r\n * @param filename - Identifier used in warning messages (e.g. persona file path)\r\n * @returns The template string with variable markers substituted\r\n */\r\nexport function resolveVariables(\r\n text: string,\r\n context: Record<string, unknown>,\r\n filename: string,\r\n): string {\r\n return text.replace(/\\{\\{(\\w+)\\}\\}/g, (match, varName: string) => {\r\n if (varName in context && context[varName] !== undefined) {\r\n return String(context[varName]);\r\n }\r\n console.warn(`[WARN] Unresolved variable: ${match} in ${filename}`);\r\n return match;\r\n });\r\n}\r\n","/**\r\n * postProcessor.ts\r\n *\r\n * Pure post-processing functions for cleaning up rendered persona output.\r\n * All functions are side-effect-free and operate only on strings.\r\n * No file-system I/O.\r\n */\r\n\r\n/**\r\n * Collapse 3 or more consecutive blank lines into 2 blank lines.\r\n *\r\n * Specifically converts 4 or more consecutive `\\n` characters into `\\n\\n\\n`\r\n * (which equals 2 blank lines between paragraphs).\r\n *\r\n * @param text - Rendered output string\r\n * @returns String with excessive blank lines collapsed\r\n */\r\nexport function collapseBlankLines(text: string): string {\r\n return text.replace(/\\n{4,}/g, '\\n\\n\\n');\r\n}\r\n\r\n/**\r\n * Ensure every Markdown heading has a blank line immediately before it.\r\n *\r\n * Also ensures horizontal rules (`---`) have a blank line before and after\r\n * them. This corrects spacing gaps caused by partial concatenation where\r\n * `trimEnd()` strips trailing newlines and conditionals add only a single\r\n * `\\n` delimiter.\r\n *\r\n * @param text - Rendered output string\r\n * @returns String with blank lines inserted before headings and rules\r\n */\r\nexport function ensureBlankLineBeforeHeadings(text: string): string {\r\n // Blank line before headings\r\n let result = text.replace(/([^\\n])\\n(#{1,6} )/g, '$1\\n\\n$2');\r\n // Blank line before horizontal rules (---)\r\n result = result.replace(/([^\\n])\\n(---)\\n/g, '$1\\n\\n$2\\n');\r\n // Blank line after horizontal rules (---)\r\n result = result.replace(/\\n(---)\\n([^\\n])/g, '\\n$1\\n\\n$2');\r\n return result;\r\n}\r\n\r\n/**\r\n * Normalize line endings to LF (`\\n`) for OS-agnostic output.\r\n *\r\n * Converts CRLF (`\\r\\n`) first, then strips any remaining stray CR (`\\r`).\r\n *\r\n * @param text - String potentially containing CRLF or CR line endings\r\n * @returns String with all line endings normalized to LF\r\n */\r\nexport function normalizeNewlines(text: string): string {\r\n return text.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\r\n}\r\n","/**\r\n * serializer.ts\r\n *\r\n * Pure serializer functions for converting tool lists to YAML-compatible\r\n * string representations. No file-system I/O.\r\n */\r\n\r\n/**\r\n * Serialize a tools array in YAML single-quote flow format WITH outer brackets.\r\n *\r\n * Output format: `['tool1', 'tool2', 'tool3']`\r\n * Used by the ledger suite to preserve byte-identical frontmatter output.\r\n *\r\n * @param tools - Array of tool name strings\r\n * @returns YAML flow-sequence string including outer brackets\r\n *\r\n * @example\r\n * serializeTools(['Bash', 'Read']) // => \"['Bash', 'Read']\"\r\n * serializeTools([]) // => \"[]\"\r\n */\r\nexport function serializeTools(tools: string[]): string {\r\n return '[' + tools.map((t) => `'${t}'`).join(', ') + ']';\r\n}\r\n\r\n/**\r\n * Serialize a tools array in YAML single-quote flow format WITHOUT outer brackets.\r\n *\r\n * Output format: `'tool1', 'tool2', 'tool3'`\r\n * Used inside standalone frontmatter templates which supply the surrounding `[ ]`.\r\n *\r\n * @param tools - Array of tool name strings\r\n * @returns Comma-separated quoted tool names (no outer brackets)\r\n *\r\n * @example\r\n * serializeToolsList(['Bash', 'Read']) // => \"'Bash', 'Read'\"\r\n * serializeToolsList([]) // => \"\"\r\n */\r\nexport function serializeToolsList(tools: string[]): string {\r\n return tools.map((t) => `'${t}'`).join(', ');\r\n}\r\n","/**\r\n * src/loaders/partials-loader.ts\r\n *\r\n * File-system loader for Handlebars-style partial snippets.\r\n *\r\n * Reads every `.md` file in `dir`, keys each entry by the filename stem\r\n * (i.e. the portion before the final `.md` extension), and returns the\r\n * map. Callers that need a two-layer (shared → suite-local override)\r\n * setup should call `loadPartials` twice and merge the results themselves,\r\n * with the suite-local result spreading last.\r\n *\r\n * All file reads are performed asynchronously. Path construction uses\r\n * `path.join` and `path.posix`-compatible operations so no path-separator\r\n * assumptions are baked in.\r\n */\r\n\r\nimport { readdir, readFile } from 'node:fs/promises';\r\nimport path from 'node:path';\r\n\r\n/**\r\n * Load all `.md` files in `dir` and return them as a `Record<string, string>`\r\n * keyed by filename stem.\r\n *\r\n * Files whose names do not end in `.md` are silently ignored.\r\n * The directory must exist; a missing directory throws an `ENOENT` error from\r\n * the underlying `readdir` call (let callers decide how to handle absence).\r\n *\r\n * @param dir Absolute (or relative) path to the directory to scan.\r\n * @returns A map from filename stem → file content string.\r\n *\r\n * @example\r\n * const partials = await loadPartials('/project/partials');\r\n * // { greeting: 'Hello, {{name}}!', footer: '---\\nEnd of file' }\r\n */\r\nexport async function loadPartials(dir: string): Promise<Record<string, string>> {\r\n const entries = await readdir(dir, { withFileTypes: true });\r\n\r\n const mdFiles = entries.filter(\r\n (entry) => entry.isFile() && entry.name.endsWith('.md'),\r\n );\r\n\r\n const pairs = await Promise.all(\r\n mdFiles.map(async (entry) => {\r\n const stem = entry.name.slice(0, -'.md'.length); // strip trailing \".md\"\r\n const filePath = path.join(dir, entry.name);\r\n const content = await readFile(filePath, 'utf8');\r\n return [stem, content] as [string, string];\r\n }),\r\n );\r\n\r\n return Object.fromEntries(pairs);\r\n}\r\n","/**\r\n * src/loaders/metadata-loader.ts\r\n *\r\n * File-system loader for persona YAML metadata files.\r\n *\r\n * Provides two exports:\r\n *\r\n * 1. `discoverPersonaYamls(root)` — recursively walks `root` and returns\r\n * absolute paths for every `*.yaml` file found, regardless of nesting\r\n * depth. Uses Node's built-in `fs.readdir` with `recursive: true`\r\n * (available since Node 18.17). No glob library is required.\r\n *\r\n * 2. `loadMetadata(yamlPath)` — reads a single YAML file and parses it\r\n * with `js-yaml` into a fully typed `PersonaMetadata` object.\r\n *\r\n * Path construction relies exclusively on `node:path` so the output is\r\n * correct on both POSIX and Windows.\r\n */\r\n\r\nimport { readdir, readFile } from 'node:fs/promises';\r\nimport path from 'node:path';\r\nimport yaml from 'js-yaml';\r\nimport type { PersonaMetadata } from '../plugins/types.js';\r\n\r\n// Re-export the type so consumers can import it directly from this module\r\nexport type { PersonaMetadata };\r\n\r\n// ---------------------------------------------------------------------------\r\n// YAML discovery\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Recursively discover all `*.yaml` files under `root` and return their\r\n * absolute paths sorted lexicographically.\r\n *\r\n * Uses `readdir` with `{ recursive: true }` (Node ≥ 18.17). Each returned\r\n * path is normalised through `path.resolve` so callers always receive\r\n * absolute, platform-consistent paths.\r\n *\r\n * @param root The directory to search (absolute or resolvable relative path).\r\n * @returns Sorted array of absolute paths to every `*.yaml` file found.\r\n *\r\n * @example\r\n * const yamls = await discoverPersonaYamls('/project/personas/ledger/src/meta');\r\n * // ['/project/personas/ledger/src/meta/alpha.yaml', ...]\r\n */\r\nexport async function discoverPersonaYamls(root: string): Promise<string[]> {\r\n const absRoot = path.resolve(root);\r\n\r\n // Node ≥ 18.17: readdir with recursive returns relative paths from root\r\n const allEntries = await readdir(absRoot, { recursive: true, withFileTypes: false });\r\n\r\n const yamlPaths = (allEntries as string[])\r\n .filter((entry) => entry.endsWith('.yaml'))\r\n .map((entry) => path.join(absRoot, entry))\r\n .sort();\r\n\r\n return yamlPaths;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// YAML parsing\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Load and parse a single persona YAML file into a typed `PersonaMetadata`\r\n * object.\r\n *\r\n * The YAML is parsed using `js-yaml`'s safe `load` function. The result\r\n * is validated to be a non-null object; if the YAML is empty or does not\r\n * parse to an object, an `Error` is thrown.\r\n *\r\n * `PersonaMetadata` requires a `name` field. If the YAML does not contain\r\n * a `name` key the function throws an `Error` with a descriptive message.\r\n *\r\n * @param yamlPath Absolute path to the YAML file.\r\n * @returns Parsed and validated `PersonaMetadata` object.\r\n * @throws `Error` when the file is unparseable, not an object, or\r\n * is missing the required `name` field.\r\n *\r\n * @example\r\n * const meta = await loadMetadata('/project/meta/my-persona.yaml');\r\n * // { name: 'my-persona', description: '...', tools: [...] }\r\n */\r\nexport async function loadMetadata(yamlPath: string): Promise<PersonaMetadata> {\r\n const raw = await readFile(yamlPath, 'utf8');\r\n\r\n const parsed: unknown = yaml.load(raw);\r\n\r\n if (parsed === null || parsed === undefined || typeof parsed !== 'object' || Array.isArray(parsed)) {\r\n throw new Error(\r\n `loadMetadata: expected a YAML object in \"${yamlPath}\", got ${\r\n Array.isArray(parsed) ? 'array' : String(parsed)\r\n }`,\r\n );\r\n }\r\n\r\n const record = parsed as Record<string, unknown>;\r\n\r\n if (typeof record['name'] !== 'string' || record['name'].trim() === '') {\r\n throw new Error(\r\n `loadMetadata: YAML file \"${yamlPath}\" is missing a required string field \"name\"`,\r\n );\r\n }\r\n\r\n return record as PersonaMetadata;\r\n}\r\n","/**\r\n * src/loaders/content-loader.ts\r\n *\r\n * File-system loader for persona Markdown content templates.\r\n *\r\n * Provides a single `loadContent` function that reads the raw string content\r\n * of a persona Markdown file from disk. The content is returned exactly as\r\n * stored — no template substitution, no post-processing. Those concerns\r\n * belong to the engine layer.\r\n *\r\n * All I/O is asynchronous. Path construction uses `node:path` so the\r\n * implementation is path-separator–agnostic.\r\n */\r\n\r\nimport { readFile } from 'node:fs/promises';\r\nimport path from 'node:path';\r\n\r\n/**\r\n * Read a persona Markdown content file and return its raw string content.\r\n *\r\n * The file is read with UTF-8 encoding. No parsing, template resolution,\r\n * or post-processing is applied — that is the engine layer's responsibility.\r\n *\r\n * @param mdPath Absolute (or resolvable relative) path to the `.md` file.\r\n * @returns Raw UTF-8 string content of the file.\r\n * @throws An `ENOENT` error (from `fs/promises`) if the file does not\r\n * exist, or any other I/O error the OS reports.\r\n *\r\n * @example\r\n * const body = await loadContent('/project/content/my-persona.md');\r\n * // '{{> greeting}}\\n\\n## About\\n\\nThis is {{name}}...'\r\n */\r\nexport async function loadContent(mdPath: string): Promise<string> {\r\n const absPath = path.resolve(mdPath);\r\n return readFile(absPath, 'utf8');\r\n}\r\n","/**\r\n * src/plugins/runner.ts\r\n *\r\n * Plugin runner — responsible for invoking plugin hooks in registration order.\r\n *\r\n * Each exported function corresponds to one lifecycle hook defined in\r\n * PersonaBuildPlugin. The runner:\r\n * - Skips plugins that do not implement the requested hook (hook is optional)\r\n * - Invokes hooks in the order plugins are registered (first-in first-called)\r\n * - For accumulating hooks (onBuildContext, onPostRender), each plugin\r\n * receives the output of the previous plugin as its first argument\r\n * - For collecting hooks (onValidate), results are concatenated into a\r\n * flat array\r\n *\r\n * No file-system I/O. No async operations.\r\n */\r\n\r\nimport type {\r\n PersonaBuildPlugin,\r\n PersonaMetadata,\r\n SuiteConfig,\r\n TargetType,\r\n ValidationResult,\r\n} from './types.js';\r\n\r\n// ---------------------------------------------------------------------------\r\n// Suite-level hook\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Invoke the `onSuiteInit` hook on every registered plugin.\r\n *\r\n * Each plugin may optionally implement this hook. Plugins are called in\r\n * registration order. The hook receives the suite config and a mutable\r\n * `sharedMeta` object — plugins may mutate `sharedMeta` in place; the\r\n * same reference is passed to every subsequent plugin.\r\n *\r\n * @param plugins Ordered list of registered plugins\r\n * @param suite The suite configuration object\r\n * @param sharedMeta Mutable shared metadata object (mutated in place by plugins)\r\n */\r\nexport function runSuiteInit(\r\n plugins: PersonaBuildPlugin[],\r\n suite: SuiteConfig,\r\n sharedMeta: Record<string, unknown>,\r\n): void {\r\n for (const plugin of plugins) {\r\n if (typeof plugin.onSuiteInit === 'function') {\r\n plugin.onSuiteInit(suite, sharedMeta);\r\n }\r\n }\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Per-persona context accumulation\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Invoke the `onBuildContext` hook on every registered plugin, accumulating\r\n * context mutations sequentially.\r\n *\r\n * Each plugin receives the context returned by the previous plugin. If a\r\n * plugin does not implement `onBuildContext`, the context passes through\r\n * unchanged. The final accumulated context is returned.\r\n *\r\n * @param plugins Ordered list of registered plugins\r\n * @param ctx Initial rendering context for this persona\r\n * @param persona Typed metadata for the persona being built\r\n * @param suite The suite configuration object\r\n * @returns Accumulated rendering context after all plugins have run\r\n */\r\nexport function runBuildContext(\r\n plugins: PersonaBuildPlugin[],\r\n ctx: Record<string, unknown>,\r\n persona: PersonaMetadata,\r\n suite: SuiteConfig,\r\n): Record<string, unknown> {\r\n let accumulated = ctx;\r\n for (const plugin of plugins) {\r\n if (typeof plugin.onBuildContext === 'function') {\r\n accumulated = plugin.onBuildContext(accumulated, persona, suite);\r\n }\r\n }\r\n return accumulated;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Per-persona post-render chain\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Invoke the `onPostRender` hook on every registered plugin, chaining the\r\n * output string sequentially.\r\n *\r\n * Each plugin receives the string returned by the previous plugin. If a\r\n * plugin does not implement `onPostRender`, the string passes through\r\n * unchanged. The final string is returned.\r\n *\r\n * @param plugins Ordered list of registered plugins\r\n * @param rendered Initial rendered output string\r\n * @param persona Typed metadata for the persona being built\r\n * @param target The current build target\r\n * @returns Final output string after all plugins have run\r\n */\r\nexport function runPostRender(\r\n plugins: PersonaBuildPlugin[],\r\n rendered: string,\r\n persona: PersonaMetadata,\r\n target: TargetType,\r\n): string {\r\n let output = rendered;\r\n for (const plugin of plugins) {\r\n if (typeof plugin.onPostRender === 'function') {\r\n output = plugin.onPostRender(output, persona, target);\r\n }\r\n }\r\n return output;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Per-persona validation collection\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Invoke the `onValidate` hook on every registered plugin and collect all\r\n * returned ValidationResult objects into a single flat array.\r\n *\r\n * Plugins that do not implement `onValidate` contribute nothing to the result.\r\n * The return value is always an array (never null/undefined).\r\n *\r\n * @param plugins Ordered list of registered plugins\r\n * @param persona Typed metadata for the persona being built\r\n * @param suite The suite configuration object\r\n * @returns Flat array of all ValidationResult objects from all plugins\r\n */\r\nexport function runValidate(\r\n plugins: PersonaBuildPlugin[],\r\n persona: PersonaMetadata,\r\n suite: SuiteConfig,\r\n): ValidationResult[] {\r\n const results: ValidationResult[] = [];\r\n for (const plugin of plugins) {\r\n if (typeof plugin.onValidate === 'function') {\r\n const pluginResults = plugin.onValidate(persona, suite);\r\n results.push(...pluginResults);\r\n }\r\n }\r\n return results;\r\n}\r\n","/**\r\n * src/builders/frontmatter.ts\r\n *\r\n * Frontmatter template registry for @smor/persona-build.\r\n *\r\n * Ships two minimal default templates — one per target — that work for the\r\n * \"standalone\" persona mode (simple personas without numbered workflows or\r\n * MCP server blocks). Projects needing richer frontmatter register custom\r\n * templates via the `PersonaBuildPlugin.frontmatterTemplates` property.\r\n *\r\n * Template rendering follows the same two-step sequence as body rendering:\r\n * 1. resolveConditionals() — resolve {{#if flag}} blocks\r\n * 2. resolveVariables() — substitute {{varName}} markers\r\n *\r\n * No partials in frontmatter — frontmatter is kept deliberately simple.\r\n */\r\n\r\nimport { resolveConditionals } from '../engine/conditionals.js';\r\nimport { resolveVariables } from '../engine/variables.js';\r\nimport type { PersonaBuildPlugin } from '../plugins/types.js';\r\n\r\n// ---------------------------------------------------------------------------\r\n// Built-in default templates\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Default VS Code frontmatter template.\r\n *\r\n * Minimal fields that work for standalone personas. Projects using numbered\r\n * workflows (e.g. ledger) should inject a richer template via a plugin.\r\n */\r\nexport const DEFAULT_FRONTMATTER_VSCODE = `---\r\nname: '{{name}} v{{version}}'\r\ndescription: '{{description}}'\r\ntools: [{{tools_list}}]\r\n---`;\r\n\r\n/**\r\n * Default Claude Code frontmatter template.\r\n *\r\n * Minimal fields that work for standalone personas. Projects using numbered\r\n * workflows should inject a richer template via a plugin.\r\n */\r\nexport const DEFAULT_FRONTMATTER_CLAUDE_CODE = `---\r\nname: {{cc_file_name_stem}}\r\npermissionMode: {{cc_permission_mode}}\r\nmodel: {{cc_model}}\r\nmemory: {{cc_memory}}\r\nallowedTools: [{{cc_tools_list}}]\r\n---`;\r\n\r\n// ---------------------------------------------------------------------------\r\n// Template resolution\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Resolve frontmatter template precedence.\r\n *\r\n * Precedence order (highest wins):\r\n * 1. Plugin `frontmatterTemplates` — the last plugin with a matching key\r\n * wins (plugins are applied in reverse-registration order so the\r\n * *first* registered plugin with a template takes precedence over later\r\n * ones, matching the general plugin-chain contract).\r\n * 2. `configTemplates` — templates passed via `BuildConfig.frontmatter`\r\n * 3. Library defaults (`DEFAULT_FRONTMATTER_VSCODE` / `DEFAULT_FRONTMATTER_CLAUDE_CODE`)\r\n *\r\n * @param target The build target ('vscode' | 'claude-code')\r\n * @param plugins Registered plugins (searched in order; first match wins)\r\n * @param configTemplates Optional caller-supplied overrides from BuildConfig\r\n * @returns The resolved template string\r\n */\r\nexport function resolveFrontmatterTemplate(\r\n target: 'vscode' | 'claude-code',\r\n plugins: PersonaBuildPlugin[],\r\n configTemplates?: Partial<Record<'vscode' | 'claude-code', string>>,\r\n): string {\r\n // Check plugins in registration order — first plugin with a matching\r\n // frontmatterTemplates entry wins.\r\n for (const plugin of plugins) {\r\n if (plugin.frontmatterTemplates && target in plugin.frontmatterTemplates) {\r\n const tpl = plugin.frontmatterTemplates[target];\r\n if (tpl !== undefined) return tpl;\r\n }\r\n }\r\n\r\n // Caller-supplied config templates\r\n if (configTemplates && target in configTemplates) {\r\n const tpl = configTemplates[target];\r\n if (tpl !== undefined) return tpl;\r\n }\r\n\r\n // Library defaults\r\n return target === 'vscode' ? DEFAULT_FRONTMATTER_VSCODE : DEFAULT_FRONTMATTER_CLAUDE_CODE;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Frontmatter rendering\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Render a frontmatter template string against the given context.\r\n *\r\n * Applies the standard two-step template resolution:\r\n * 1. `resolveConditionals` — `{{#if flag}}` blocks\r\n * 2. `resolveVariables` — `{{varName}}` substitution\r\n *\r\n * @param template The raw frontmatter template string (may contain markers)\r\n * @param context Key-value context for variable substitution\r\n * @param filename Source filename used in warning messages\r\n * @returns Rendered frontmatter string (ready to prepend to body)\r\n */\r\nexport function renderFrontmatter(\r\n template: string,\r\n context: Record<string, unknown>,\r\n filename: string,\r\n): string {\r\n let rendered = resolveConditionals(template, context);\r\n rendered = resolveVariables(rendered, context, filename);\r\n return rendered;\r\n}\r\n","/**\r\n * src/builders/persona-builder.ts\r\n *\r\n * Core build orchestrator for @smor/persona-build.\r\n *\r\n * Exports three public functions:\r\n *\r\n * 1. buildPersona(personaYamlPath, suiteName, suiteConfig, sharedMeta,\r\n * partialsMap, config, plugins)\r\n * — Builds a single persona for a single target. Returns a BuildResult.\r\n *\r\n * 2. buildSuite(suiteName, suiteConfig, config, plugins)\r\n * — Discovers all persona YAMLs for a suite, fires onSuiteInit, maps\r\n * buildPersona() over each, and returns BuildResult[].\r\n *\r\n * 3. build(config)\r\n * — Top-level entry point. Iterates all suites × targets, calls\r\n * buildSuite() for each combination, and returns a BuildSummary.\r\n * Respects --check (no writes) and --strict (fail on warnings/errors).\r\n */\r\n\r\nimport { readdir, readFile, mkdir, writeFile } from 'node:fs/promises';\r\nimport { existsSync } from 'node:fs';\r\nimport path from 'node:path';\r\nimport yaml from 'js-yaml';\r\n\r\nimport { resolvePartials } from '../engine/partials.js';\r\nimport { resolveConditionals } from '../engine/conditionals.js';\r\nimport { resolveVariables } from '../engine/variables.js';\r\nimport {\r\n collapseBlankLines,\r\n ensureBlankLineBeforeHeadings,\r\n normalizeNewlines,\r\n} from '../engine/postProcessor.js';\r\nimport { serializeTools, serializeToolsList } from '../engine/serializer.js';\r\nimport { loadPartials } from '../loaders/partials-loader.js';\r\nimport {\r\n runSuiteInit,\r\n runBuildContext,\r\n runPostRender,\r\n runValidate,\r\n} from '../plugins/runner.js';\r\n\r\nimport { resolveFrontmatterTemplate, renderFrontmatter } from './frontmatter.js';\r\nimport type { BuildConfig, BuildResult, BuildSummary } from './types.js';\r\nimport type { PersonaBuildPlugin, PersonaMetadata, SuiteConfig, ValidationResult } from '../plugins/types.js';\r\n\r\n// ---------------------------------------------------------------------------\r\n// Internal helpers\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Discover all persona YAML files in the `meta/` subdirectory of a suite.\r\n *\r\n * Excludes files whose names start with `_` (shared metadata files such as\r\n * `_shared.yaml`). Results are sorted lexicographically.\r\n *\r\n * @param suiteConfig Suite configuration (used to locate `metaSubdir`)\r\n * @returns Absolute paths to each persona YAML file, sorted.\r\n */\r\nasync function discoverSuitePersonaYamls(suiteConfig: SuiteConfig): Promise<string[]> {\r\n const metaSubdir = suiteConfig.metaSubdir ?? 'meta';\r\n const metaDir = path.join(suiteConfig.srcDir, metaSubdir);\r\n\r\n const entries = await readdir(metaDir, { withFileTypes: true });\r\n\r\n return entries\r\n .filter((e) => e.isFile() && e.name.endsWith('.yaml') && !e.name.startsWith('_'))\r\n .map((e) => path.join(metaDir, e.name))\r\n .sort();\r\n}\r\n\r\n/**\r\n * Load and parse a raw YAML file into a plain object.\r\n * Used for `_shared.yaml` which does not conform to PersonaMetadata's\r\n * `name` requirement.\r\n *\r\n * @param filePath Absolute path to the YAML file\r\n * @returns Parsed object, or {} when the file is empty/absent\r\n */\r\nasync function loadRawYaml(filePath: string): Promise<Record<string, unknown>> {\r\n if (!existsSync(filePath)) return {};\r\n const raw = await readFile(filePath, 'utf8');\r\n const parsed: unknown = yaml.load(raw);\r\n if (parsed === null || parsed === undefined) return {};\r\n if (typeof parsed !== 'object' || Array.isArray(parsed)) return {};\r\n return parsed as Record<string, unknown>;\r\n}\r\n\r\n/**\r\n * Load a persona YAML file and return it as a plain metadata record.\r\n * The `name` field is derived from the filename stem when absent.\r\n *\r\n * @param yamlPath Absolute path to the persona YAML file\r\n * @returns Merged metadata record ready for context building\r\n */\r\nasync function loadPersonaYaml(yamlPath: string): Promise<Record<string, unknown>> {\r\n const raw = await readFile(yamlPath, 'utf8');\r\n const parsed: unknown = yaml.load(raw);\r\n\r\n if (parsed === null || parsed === undefined || typeof parsed !== 'object' || Array.isArray(parsed)) {\r\n throw new Error(`buildPersona: expected a YAML object in \"${yamlPath}\"`);\r\n }\r\n\r\n const record = parsed as Record<string, unknown>;\r\n\r\n // Derive name from filename stem if not present in YAML\r\n if (!record['name']) {\r\n record['name'] = path.basename(yamlPath, '.yaml');\r\n }\r\n\r\n return record;\r\n}\r\n\r\n/**\r\n * Build the merged template context for a single persona.\r\n *\r\n * Merge order (later values win):\r\n * 1. sharedMeta (suite-level defaults)\r\n * 2. per-persona YAML fields\r\n * 3. derived/computed fields (version fallback, etc.)\r\n *\r\n * @param personaMeta Per-persona YAML as a plain record\r\n * @param sharedMeta Parsed `_shared.yaml` fields\r\n * @returns Merged rendering context\r\n */\r\nfunction buildContext(\r\n personaMeta: Record<string, unknown>,\r\n sharedMeta: Record<string, unknown>,\r\n): Record<string, unknown> {\r\n const version =\r\n typeof personaMeta['version'] === 'string'\r\n ? personaMeta['version']\r\n : typeof sharedMeta['default_version'] === 'string'\r\n ? sharedMeta['default_version']\r\n : '0.0.0';\r\n\r\n // Merge base: shared first, persona overrides\r\n const merged: Record<string, unknown> = {\r\n ...sharedMeta,\r\n ...personaMeta,\r\n version,\r\n };\r\n\r\n // ── Derived convenience fields (only set when not already provided) ───────\r\n // tools_list / tools_json — serialized from the `tools` array if present\r\n const tools = Array.isArray(merged['tools']) ? (merged['tools'] as string[]) : [];\r\n if (!('tools_list' in merged)) {\r\n merged['tools_list'] = serializeToolsList(tools);\r\n }\r\n if (!('tools_json' in merged)) {\r\n merged['tools_json'] = serializeTools(tools);\r\n }\r\n\r\n // cc_tools_list / cc_tools_json — from `cc_tools` or fall back to `tools`\r\n const ccTools = Array.isArray(merged['cc_tools']) ? (merged['cc_tools'] as string[]) : tools;\r\n if (!('cc_tools_list' in merged)) {\r\n merged['cc_tools_list'] = serializeToolsList(ccTools);\r\n }\r\n if (!('cc_tools_json' in merged)) {\r\n merged['cc_tools_json'] = serializeTools(ccTools);\r\n }\r\n\r\n // cc_file_name_stem — stem of cc_file_name (for default CC frontmatter template)\r\n if (!('cc_file_name_stem' in merged) && typeof merged['cc_file_name'] === 'string') {\r\n const ccFileName = merged['cc_file_name'] as string;\r\n merged['cc_file_name_stem'] = ccFileName.replace(/\\.md$/, '');\r\n }\r\n\r\n return merged;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// buildPersona — single persona × single target\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Build a single persona for a single output target.\r\n *\r\n * Pipeline:\r\n * 1. Load sharedMeta + personaMeta (callers supply pre-loaded values)\r\n * 2. Build merged context\r\n * 3. Run onBuildContext plugin hooks (context accumulation)\r\n * 4. Resolve frontmatter template → render frontmatter\r\n * 5. Load content template\r\n * 6. Render body: partials → conditionals → variables → post-process\r\n * 7. Assemble final output (frontmatter + body)\r\n * 8. Run onPostRender plugin hooks (output chain)\r\n * 9. Run onValidate plugin hooks (validation collection)\r\n * 10. Determine output file path\r\n * 11. Write output file (unless check mode)\r\n * 12. Return BuildResult\r\n *\r\n * @param personaYamlPath Absolute path to the persona YAML source file\r\n * @param suiteName Identifier for the suite this persona belongs to\r\n * @param suiteConfig Suite configuration object\r\n * @param sharedMeta Pre-loaded `_shared.yaml` contents\r\n * @param partialsMap Pre-loaded partials map (shared + suite-local merged)\r\n * @param config Top-level BuildConfig\r\n * @param plugins Registered plugins\r\n * @param target Target output format\r\n * @returns BuildResult for this persona × target combination\r\n */\r\nexport async function buildPersona(\r\n personaYamlPath: string,\r\n suiteName: string,\r\n suiteConfig: SuiteConfig,\r\n sharedMeta: Record<string, unknown>,\r\n partialsMap: Record<string, string>,\r\n config: BuildConfig,\r\n plugins: PersonaBuildPlugin[],\r\n target: 'vscode' | 'claude-code',\r\n): Promise<BuildResult> {\r\n // ── 1. Load persona metadata ──────────────────────────────────────────────\r\n const personaMeta = await loadPersonaYaml(personaYamlPath);\r\n\r\n // ── 2. Build merged context ───────────────────────────────────────────────\r\n let context = buildContext(personaMeta, sharedMeta);\r\n\r\n // ── 3. Plugin onBuildContext ──────────────────────────────────────────────\r\n // Cast context to PersonaMetadata for the plugin runner (it requires a\r\n // name field which is guaranteed by loadPersonaYaml above).\r\n const personaMetaTyped = personaMeta as PersonaMetadata;\r\n context = runBuildContext(plugins, context, personaMetaTyped, suiteConfig);\r\n\r\n // ── 4. Render frontmatter ─────────────────────────────────────────────────\r\n const fmTemplate = resolveFrontmatterTemplate(target, plugins, config.frontmatter);\r\n const contentBasename = path.basename(personaYamlPath, '.yaml') + '.md';\r\n const frontmatter = renderFrontmatter(fmTemplate, context, contentBasename);\r\n\r\n // ── 5. Load content template ──────────────────────────────────────────────\r\n const contentSubdir = suiteConfig.contentSubdir ?? 'content';\r\n const contentPath = path.join(suiteConfig.srcDir, contentSubdir, contentBasename);\r\n const bodyTemplate = normalizeNewlines(await readFile(contentPath, 'utf8'));\r\n\r\n // ── 6. Render body ────────────────────────────────────────────────────────\r\n let body = resolvePartials(bodyTemplate, partialsMap);\r\n body = resolveConditionals(body, context);\r\n body = resolveVariables(body, context, contentBasename);\r\n body = collapseBlankLines(body);\r\n body = ensureBlankLineBeforeHeadings(body);\r\n body = body.trimEnd();\r\n\r\n // ── 7. Assemble output ────────────────────────────────────────────────────\r\n let output = normalizeNewlines(`${frontmatter}\\n\\n${body}\\n`);\r\n\r\n // ── 8. Plugin onPostRender ────────────────────────────────────────────────\r\n output = runPostRender(plugins, output, personaMetaTyped, target);\r\n\r\n // ── 9. Plugin onValidate ──────────────────────────────────────────────────\r\n const validationResults: ValidationResult[] = runValidate(plugins, personaMetaTyped, suiteConfig);\r\n\r\n // ── 10. Determine output file path ────────────────────────────────────────\r\n const outputDir = target === 'vscode' ? suiteConfig.outVscode : suiteConfig.outClaudeCode;\r\n // Use declared output filename fields when present (vs_file_name / cc_file_name),\r\n // falling back to the content basename.\r\n let outputBasename: string;\r\n if (target === 'vscode' && typeof context['vs_file_name'] === 'string') {\r\n outputBasename = context['vs_file_name'];\r\n } else if (target === 'claude-code' && typeof context['cc_file_name'] === 'string') {\r\n outputBasename = context['cc_file_name'];\r\n } else {\r\n outputBasename = contentBasename;\r\n }\r\n const outputPath = path.join(outputDir, outputBasename);\r\n\r\n // ── 11. Write (unless check mode) ─────────────────────────────────────────\r\n const check = config.check ?? false;\r\n let written = false;\r\n\r\n if (!check) {\r\n await mkdir(outputDir, { recursive: true });\r\n await writeFile(outputPath, output, 'utf8');\r\n written = true;\r\n }\r\n\r\n return {\r\n suite: suiteName,\r\n target,\r\n personaYamlPath,\r\n outputPath,\r\n content: output,\r\n validationResults,\r\n written,\r\n };\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// buildSuite — all personas in one suite × one target\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Build all personas in a suite for a single output target.\r\n *\r\n * Pipeline:\r\n * 1. Load `_shared.yaml` for the suite\r\n * 2. Load merged partials (shared → suite-local)\r\n * 3. Run `onSuiteInit` on all plugins\r\n * 4. Discover all persona YAML files\r\n * 5. Call `buildPersona()` for each\r\n *\r\n * @param suiteName Identifier for this suite\r\n * @param suiteConfig Suite configuration\r\n * @param config Top-level BuildConfig\r\n * @param plugins Registered plugins\r\n * @param target Target output format\r\n * @returns Array of BuildResult objects, one per persona\r\n */\r\nexport async function buildSuite(\r\n suiteName: string,\r\n suiteConfig: SuiteConfig,\r\n config: BuildConfig,\r\n plugins: PersonaBuildPlugin[],\r\n target: 'vscode' | 'claude-code',\r\n): Promise<BuildResult[]> {\r\n // ── 1. Load shared metadata ───────────────────────────────────────────────\r\n const metaSubdir = suiteConfig.metaSubdir ?? 'meta';\r\n const sharedYamlPath = path.join(suiteConfig.srcDir, metaSubdir, '_shared.yaml');\r\n const sharedMeta = await loadRawYaml(sharedYamlPath);\r\n\r\n // ── 2. Load partials (two-layer: shared base → suite-local override) ──────\r\n let partialsMap: Record<string, string> = {};\r\n\r\n if (config.sharedPartialsDir && existsSync(config.sharedPartialsDir)) {\r\n partialsMap = { ...partialsMap, ...(await loadPartials(config.sharedPartialsDir)) };\r\n }\r\n\r\n const partialsSubdir = suiteConfig.partialsSubdir ?? 'partials';\r\n const suitePartialsDir = path.join(suiteConfig.srcDir, partialsSubdir);\r\n if (existsSync(suitePartialsDir)) {\r\n partialsMap = { ...partialsMap, ...(await loadPartials(suitePartialsDir)) };\r\n }\r\n\r\n // ── 3. Plugin onSuiteInit ─────────────────────────────────────────────────\r\n runSuiteInit(plugins, suiteConfig, sharedMeta);\r\n\r\n // ── 4. Discover persona YAML files ────────────────────────────────────────\r\n const personaYamlPaths = await discoverSuitePersonaYamls(suiteConfig);\r\n\r\n // ── 5. Build each persona ─────────────────────────────────────────────────\r\n const results: BuildResult[] = [];\r\n for (const yamlPath of personaYamlPaths) {\r\n const result = await buildPersona(\r\n yamlPath,\r\n suiteName,\r\n suiteConfig,\r\n sharedMeta,\r\n partialsMap,\r\n config,\r\n plugins,\r\n target,\r\n );\r\n results.push(result);\r\n }\r\n\r\n return results;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// build — top-level entry point\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Top-level build orchestrator.\r\n *\r\n * Iterates all `config.suites × config.targets` combinations, calls\r\n * `buildSuite()` for each, and aggregates the results into a `BuildSummary`.\r\n *\r\n * Modes:\r\n * - Normal: renders and writes all personas.\r\n * - `check: true`: renders without writing; useful for CI staleness checks.\r\n * - `strict: true`: throws when any ValidationResult has severity `'error'`\r\n * or `'warning'`. All suites are processed before the throw, so output\r\n * files **will** be written to disk even when the build ultimately fails.\r\n * **For CI usage, combine `strict: true` with `check: true`** to avoid\r\n * leaving partial artefacts on disk when validation fails.\r\n *\r\n * @param config Typed build configuration\r\n * @returns Aggregated BuildSummary\r\n * @throws `Error` when `strict: true` and validation failures exist\r\n */\r\nexport async function build(config: BuildConfig): Promise<BuildSummary> {\r\n const plugins = config.plugins ?? [];\r\n const targets = config.targets ?? ['vscode', 'claude-code'];\r\n const allResults: BuildResult[] = [];\r\n\r\n for (const [suiteName, suiteConfig] of Object.entries(config.suites)) {\r\n for (const target of targets) {\r\n const suiteResults = await buildSuite(suiteName, suiteConfig, config, plugins, target);\r\n allResults.push(...suiteResults);\r\n }\r\n }\r\n\r\n // Collect strict failures (error + warning severity)\r\n const strictFailures: ValidationResult[] = config.strict\r\n ? allResults.flatMap((r) =>\r\n r.validationResults.filter(\r\n (v) => v.severity === 'error' || v.severity === 'warning',\r\n ),\r\n )\r\n : [];\r\n\r\n const success = !config.strict || strictFailures.length === 0;\r\n\r\n const summary: BuildSummary = {\r\n success,\r\n results: allResults,\r\n strictFailures,\r\n totalBuilt: allResults.length,\r\n totalWritten: allResults.filter((r) => r.written).length,\r\n };\r\n\r\n if (config.strict && !success) {\r\n const messages = strictFailures.map((f) => `[${f.severity}] ${f.message}`).join('\\n');\r\n throw new Error(\r\n `Build failed in strict mode — ${strictFailures.length} validation issue(s):\\n${messages}`,\r\n );\r\n }\r\n\r\n return summary;\r\n}\r\n","/**\r\n * src/validators/filename-validator.ts\r\n *\r\n * Validates persona output filenames against the project naming convention.\r\n *\r\n * Convention: kebab-case only — lowercase letters, digits, and hyphens.\r\n * No spaces, no uppercase letters, no special characters other than hyphens\r\n * and dots (for the file extension).\r\n *\r\n * This is a pure function: no file I/O, no process.exit, no side effects.\r\n * It depends only on `ValidationResult` from `src/plugins/types.ts`.\r\n */\r\n\r\nimport type { ValidationResult } from '../plugins/types.js';\r\n\r\n// ---------------------------------------------------------------------------\r\n// Validation rule definitions\r\n// ---------------------------------------------------------------------------\r\n\r\ninterface FilenameRule {\r\n /** Human-readable description of the rule (used in error messages) */\r\n description: string;\r\n /** Returns true when the filename is *invalid* (i.e. the rule is violated) */\r\n violated: (basename: string) => boolean;\r\n /** Message factory — receives the offending basename */\r\n message: (basename: string) => string;\r\n}\r\n\r\nconst FILENAME_RULES: FilenameRule[] = [\r\n {\r\n description: 'no uppercase letters',\r\n violated: (name) => /[A-Z]/.test(name),\r\n message: (name) =>\r\n `Filename \"${name}\" contains uppercase letters. Use lowercase kebab-case (e.g. \"my-persona.md\").`,\r\n },\r\n {\r\n description: 'no spaces',\r\n violated: (name) => /\\s/.test(name),\r\n message: (name) =>\r\n `Filename \"${name}\" contains spaces. Use hyphens to separate words (e.g. \"my-persona.md\").`,\r\n },\r\n {\r\n description: 'kebab-case characters only',\r\n violated: (name) => {\r\n // A valid filename consists of one or more dot-separated segments.\r\n // Each segment must be a non-empty kebab-case token:\r\n // - starts and ends with a lowercase letter or digit\r\n // - may contain hyphens, but not consecutive hyphens\r\n // Examples of valid names: \"my-persona.md\", \"1-developer.agent.md\"\r\n // Examples of invalid names: \"My_Persona.md\", \"--bad.md\", \"foo..bar.md\"\r\n const segments = name.split('.');\r\n if (segments.length === 1) {\r\n // No extension — treat the whole name as a kebab stem\r\n return !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);\r\n }\r\n // All segments (stem + extension parts) must be valid kebab tokens\r\n return !segments.every((seg) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(seg));\r\n },\r\n message: (name) =>\r\n `Filename \"${name}\" does not conform to kebab-case naming. ` +\r\n `Use lowercase letters, digits, and hyphens only (e.g. \"my-persona.md\").`,\r\n },\r\n];\r\n\r\n// ---------------------------------------------------------------------------\r\n// Public API\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Validate a persona filename against the project naming convention.\r\n *\r\n * Accepts either a bare filename (`my-persona.md`) or a full/relative path\r\n * — only the basename (last path segment) is evaluated.\r\n *\r\n * @param filePath Filename or path to validate (only the basename is checked)\r\n * @returns Empty array when the filename conforms; one ValidationResult\r\n * per violated rule otherwise. Each result has severity \"error\".\r\n *\r\n * @example\r\n * validateFileName('my-persona.md'); // []\r\n * validateFileName('My Persona.md'); // [{severity:'error', message:'...'}]\r\n * validateFileName('/abs/path/my-persona.md');// []\r\n */\r\nexport function validateFileName(filePath: string): ValidationResult[] {\r\n const basename = filePath.includes('/')\r\n ? filePath.split('/').pop() ?? filePath\r\n : filePath.includes('\\\\')\r\n ? filePath.split('\\\\').pop() ?? filePath\r\n : filePath;\r\n\r\n const errors: ValidationResult[] = [];\r\n\r\n for (const rule of FILENAME_RULES) {\r\n if (rule.violated(basename)) {\r\n errors.push({\r\n severity: 'error',\r\n message: rule.message(basename),\r\n });\r\n }\r\n }\r\n\r\n return errors;\r\n}\r\n","/**\r\n * src/validators/strict-validator.ts\r\n *\r\n * Validates that a set of required marker strings are present in a rendered\r\n * persona output string.\r\n *\r\n * \"Strict\" mode in the build pipeline guards against incomplete renders —\r\n * e.g. a required section marker (e.g. \"{{ROLE}}\") that was never resolved.\r\n * This validator generalises that concept: callers supply the list of marker\r\n * strings that *must* appear in the final rendered content.\r\n *\r\n * This is a pure function: no file I/O, no side effects.\r\n * It depends only on `ValidationResult` from `src/plugins/types.ts`.\r\n */\r\n\r\nimport type { ValidationResult } from '../plugins/types.js';\r\n\r\n// ---------------------------------------------------------------------------\r\n// Public API\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Validate that every required marker string is present in the rendered output.\r\n *\r\n * Each absent marker produces one `ValidationResult` entry with severity\r\n * `\"error\"` and a descriptive message identifying the missing marker.\r\n *\r\n * @param renderedContent The final rendered output string to inspect\r\n * @param requiredMarkers Array of marker strings that must appear verbatim in\r\n * `renderedContent`. An empty array always returns `[]`.\r\n * @returns Empty array when all markers are found; one entry per\r\n * absent marker otherwise. Each entry has severity \"error\".\r\n *\r\n * @example\r\n * validateStrictMarkers('Hello world', ['Hello', 'world']); // []\r\n * validateStrictMarkers('Hello world', ['{{MISSING}}']);\r\n * // [{severity:'error', message:'Required marker \"{{MISSING}}\" is missing from the rendered output.'}]\r\n */\r\nexport function validateStrictMarkers(\r\n renderedContent: string,\r\n requiredMarkers: string[],\r\n): ValidationResult[] {\r\n const errors: ValidationResult[] = [];\r\n\r\n for (const marker of requiredMarkers) {\r\n if (!renderedContent.includes(marker)) {\r\n errors.push({\r\n severity: 'error',\r\n message: `Required marker \"${marker}\" is missing from the rendered output.`,\r\n });\r\n }\r\n }\r\n\r\n return errors;\r\n}\r\n","/**\r\n * @smor/persona-build\r\n *\r\n * Public API barrel export.\r\n * Feature modules will be exported from here as they are implemented in subsequent WPs.\r\n */\r\n\r\nimport { createRequire } from 'node:module';\r\n\r\n// Engine exports (WP-002)\r\nexport * from './engine/index.js';\r\n\r\n// Loader exports (WP-003)\r\nexport * from './loaders/index.js';\r\n\r\n// Plugin exports (WP-003/WP-004)\r\nexport * from './plugins/index.js';\r\n\r\n// Builder exports (WP-006)\r\nexport * from './builders/index.js';\r\n\r\n// Validator exports (WP-005)\r\nexport * from './validators/index.js';\r\n\r\n/** Package version — sourced from package.json (single source of truth). */\r\nconst _pkgRequire = createRequire(import.meta.url);\r\nexport const VERSION = (_pkgRequire('../package.json') as { version: string }).version;\r\n"]}