@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.
@@ -0,0 +1,829 @@
1
+ /**
2
+ * partials.ts
3
+ *
4
+ * Pure template-engine function for resolving partial inclusions.
5
+ * Supports {{> name}} syntax with up to depth-2 recursion to handle
6
+ * partials-within-partials. No file-system I/O.
7
+ */
8
+ /**
9
+ * Resolve partial inclusions in a template string.
10
+ *
11
+ * Replaces `{{> name}}` markers with the content from `partialsMap`.
12
+ * Recursion is capped at depth 2 so that:
13
+ * - depth 0 → 1: outer partials are expanded
14
+ * - depth 1 → 2: one level of nested partials are expanded
15
+ * - depth 2: recursion stops, marker is left as-is
16
+ *
17
+ * Each resolved partial is `trimEnd()`-ed to prevent trailing blank lines
18
+ * from causing double-blank-line artefacts during concatenation.
19
+ *
20
+ * If a partial name is not found in `partialsMap`, the original marker is
21
+ * preserved and a warning is emitted via `console.warn`.
22
+ *
23
+ * @param text - Template string potentially containing {{> name}} markers
24
+ * @param partialsMap - Map of partial name → partial content
25
+ * @param depth - Current recursion depth (callers should omit; defaults to 0)
26
+ * @returns The template string with partial markers replaced
27
+ */
28
+ declare function resolvePartials(text: string, partialsMap: Record<string, string>, depth?: number): string;
29
+
30
+ /**
31
+ * conditionals.ts
32
+ *
33
+ * Pure template-engine function for resolving conditional blocks.
34
+ * Handles {{#if flag}}…{{/if}} and {{#if flag}}…{{else}}…{{/if}} syntax.
35
+ * No file-system I/O.
36
+ */
37
+ /**
38
+ * Resolve conditional blocks in a template string.
39
+ *
40
+ * Syntax:
41
+ * `{{#if flag}}content{{/if}}`
42
+ * `{{#if flag}}truthy-content{{else}}falsy-content{{/if}}`
43
+ *
44
+ * Behaviour:
45
+ * - When `context[flag]` is truthy: the delimiters are stripped and the
46
+ * content before `{{else}}` (or the entire inner block if no `{{else}}`)
47
+ * is kept, surrounded by single `\n` delimiters.
48
+ * - When `context[flag]` is falsy and a `{{else}}` branch exists: the
49
+ * content after `{{else}}` is kept, surrounded by single `\n` delimiters.
50
+ * - When `context[flag]` is falsy and no `{{else}}` branch exists: the
51
+ * entire block (including surrounding newlines) is removed, leaving a
52
+ * single `\n`.
53
+ * - Unknown flags (absent from context) are treated as falsy.
54
+ *
55
+ * Leading and trailing newlines within the kept content are trimmed so the
56
+ * output does not accumulate extra blank lines.
57
+ *
58
+ * @param text - Template string potentially containing {{#if}} blocks
59
+ * @param context - Key-value map used to evaluate flag truthiness
60
+ * @returns The template string with conditional blocks resolved
61
+ */
62
+ declare function resolveConditionals(text: string, context: Record<string, unknown>): string;
63
+
64
+ /**
65
+ * variables.ts
66
+ *
67
+ * Pure template-engine function for resolving variable substitutions.
68
+ * Handles {{varName}} syntax. No file-system I/O.
69
+ */
70
+ /**
71
+ * Resolve variable substitutions in a template string.
72
+ *
73
+ * Replaces `{{varName}}` markers with `String(context[varName])`.
74
+ * If a variable is not found in `context` (or its value is `undefined`),
75
+ * the original marker is preserved and a warning is emitted via
76
+ * `console.warn`, identifying the file by `filename` for easier debugging.
77
+ *
78
+ * Note: this step must run AFTER `resolvePartials` and `resolveConditionals`
79
+ * so that only plain variable markers remain.
80
+ *
81
+ * @param text - Template string potentially containing {{varName}} markers
82
+ * @param context - Key-value map of variable name → value
83
+ * @param filename - Identifier used in warning messages (e.g. persona file path)
84
+ * @returns The template string with variable markers substituted
85
+ */
86
+ declare function resolveVariables(text: string, context: Record<string, unknown>, filename: string): string;
87
+
88
+ /**
89
+ * postProcessor.ts
90
+ *
91
+ * Pure post-processing functions for cleaning up rendered persona output.
92
+ * All functions are side-effect-free and operate only on strings.
93
+ * No file-system I/O.
94
+ */
95
+ /**
96
+ * Collapse 3 or more consecutive blank lines into 2 blank lines.
97
+ *
98
+ * Specifically converts 4 or more consecutive `\n` characters into `\n\n\n`
99
+ * (which equals 2 blank lines between paragraphs).
100
+ *
101
+ * @param text - Rendered output string
102
+ * @returns String with excessive blank lines collapsed
103
+ */
104
+ declare function collapseBlankLines(text: string): string;
105
+ /**
106
+ * Ensure every Markdown heading has a blank line immediately before it.
107
+ *
108
+ * Also ensures horizontal rules (`---`) have a blank line before and after
109
+ * them. This corrects spacing gaps caused by partial concatenation where
110
+ * `trimEnd()` strips trailing newlines and conditionals add only a single
111
+ * `\n` delimiter.
112
+ *
113
+ * @param text - Rendered output string
114
+ * @returns String with blank lines inserted before headings and rules
115
+ */
116
+ declare function ensureBlankLineBeforeHeadings(text: string): string;
117
+ /**
118
+ * Normalize line endings to LF (`\n`) for OS-agnostic output.
119
+ *
120
+ * Converts CRLF (`\r\n`) first, then strips any remaining stray CR (`\r`).
121
+ *
122
+ * @param text - String potentially containing CRLF or CR line endings
123
+ * @returns String with all line endings normalized to LF
124
+ */
125
+ declare function normalizeNewlines(text: string): string;
126
+
127
+ /**
128
+ * serializer.ts
129
+ *
130
+ * Pure serializer functions for converting tool lists to YAML-compatible
131
+ * string representations. No file-system I/O.
132
+ */
133
+ /**
134
+ * Serialize a tools array in YAML single-quote flow format WITH outer brackets.
135
+ *
136
+ * Output format: `['tool1', 'tool2', 'tool3']`
137
+ * Used by the ledger suite to preserve byte-identical frontmatter output.
138
+ *
139
+ * @param tools - Array of tool name strings
140
+ * @returns YAML flow-sequence string including outer brackets
141
+ *
142
+ * @example
143
+ * serializeTools(['Bash', 'Read']) // => "['Bash', 'Read']"
144
+ * serializeTools([]) // => "[]"
145
+ */
146
+ declare function serializeTools(tools: string[]): string;
147
+ /**
148
+ * Serialize a tools array in YAML single-quote flow format WITHOUT outer brackets.
149
+ *
150
+ * Output format: `'tool1', 'tool2', 'tool3'`
151
+ * Used inside standalone frontmatter templates which supply the surrounding `[ ]`.
152
+ *
153
+ * @param tools - Array of tool name strings
154
+ * @returns Comma-separated quoted tool names (no outer brackets)
155
+ *
156
+ * @example
157
+ * serializeToolsList(['Bash', 'Read']) // => "'Bash', 'Read'"
158
+ * serializeToolsList([]) // => ""
159
+ */
160
+ declare function serializeToolsList(tools: string[]): string;
161
+
162
+ /**
163
+ * src/loaders/partials-loader.ts
164
+ *
165
+ * File-system loader for Handlebars-style partial snippets.
166
+ *
167
+ * Reads every `.md` file in `dir`, keys each entry by the filename stem
168
+ * (i.e. the portion before the final `.md` extension), and returns the
169
+ * map. Callers that need a two-layer (shared → suite-local override)
170
+ * setup should call `loadPartials` twice and merge the results themselves,
171
+ * with the suite-local result spreading last.
172
+ *
173
+ * All file reads are performed asynchronously. Path construction uses
174
+ * `path.join` and `path.posix`-compatible operations so no path-separator
175
+ * assumptions are baked in.
176
+ */
177
+ /**
178
+ * Load all `.md` files in `dir` and return them as a `Record<string, string>`
179
+ * keyed by filename stem.
180
+ *
181
+ * Files whose names do not end in `.md` are silently ignored.
182
+ * The directory must exist; a missing directory throws an `ENOENT` error from
183
+ * the underlying `readdir` call (let callers decide how to handle absence).
184
+ *
185
+ * @param dir Absolute (or relative) path to the directory to scan.
186
+ * @returns A map from filename stem → file content string.
187
+ *
188
+ * @example
189
+ * const partials = await loadPartials('/project/partials');
190
+ * // { greeting: 'Hello, {{name}}!', footer: '---\nEnd of file' }
191
+ */
192
+ declare function loadPartials(dir: string): Promise<Record<string, string>>;
193
+
194
+ /**
195
+ * src/plugins/types.ts
196
+ *
197
+ * Core plugin system types for @smor/persona-build.
198
+ *
199
+ * Defines:
200
+ * - TargetType — union of supported output targets
201
+ * - PersonaMetadata — typed representation of a persona YAML file
202
+ * - SuiteConfig — configuration for a single persona suite
203
+ * - ValidationResult — outcome of a plugin's onValidate hook
204
+ * - PersonaBuildPlugin — interface every plugin must implement
205
+ */
206
+ /**
207
+ * The two output formats supported by the build pipeline.
208
+ * 'vscode' → VS Code `.code-workspace` instruction files
209
+ * 'claude-code' → Claude Code instruction files
210
+ */
211
+ type TargetType = 'vscode' | 'claude-code';
212
+ /**
213
+ * Typed representation of a persona YAML metadata file.
214
+ *
215
+ * Fields map directly to the keys expected in `*.yaml` persona files.
216
+ * All fields beyond `name` are optional — consumers should treat them
217
+ * as potentially absent and fall back to suite-level or shared defaults.
218
+ */
219
+ interface PersonaMetadata {
220
+ /** Unique persona identifier (matches filename stem) */
221
+ name: string;
222
+ /** Human-readable display name */
223
+ displayName?: string;
224
+ /** Short description surfaced in frontmatter */
225
+ description?: string;
226
+ /** Semantic version string (e.g. "1.2.0") */
227
+ version?: string;
228
+ /** Ordered list of tool identifiers */
229
+ tools?: string[];
230
+ /** Free-form context variables available during template rendering */
231
+ [key: string]: unknown;
232
+ }
233
+ /**
234
+ * Configuration for a single persona suite (directory of related personas).
235
+ */
236
+ interface SuiteConfig {
237
+ /** Absolute or relative path to the suite source directory */
238
+ srcDir: string;
239
+ /** Output path for VS Code formatted persona files */
240
+ outVscode: string;
241
+ /** Output path for Claude Code formatted persona files */
242
+ outClaudeCode: string;
243
+ /**
244
+ * Optional persona mode string (e.g. 'ledger').
245
+ * When present, plugins can use this to branch behaviour.
246
+ */
247
+ personaMode?: string;
248
+ /** Sub-directory within srcDir that contains partials. Default: 'partials' */
249
+ partialsSubdir?: string;
250
+ /** Sub-directory within srcDir that contains YAML metadata. Default: 'meta' */
251
+ metaSubdir?: string;
252
+ /** Sub-directory within srcDir that contains content Markdown files. Default: 'content' */
253
+ contentSubdir?: string;
254
+ }
255
+ /**
256
+ * A single validation outcome returned by a plugin's `onValidate` hook.
257
+ */
258
+ interface ValidationResult {
259
+ /** Severity level of the issue */
260
+ severity: 'error' | 'warning' | 'info';
261
+ /** Human-readable description of the issue */
262
+ message: string;
263
+ }
264
+ /**
265
+ * Interface that every persona build plugin must implement.
266
+ *
267
+ * All hooks are optional — a plugin only needs to implement the hooks it
268
+ * uses. The only required field is `name`, which is used for logging and
269
+ * identification.
270
+ *
271
+ * Hook invocation order (per persona):
272
+ * 1. onSuiteInit — once per suite, before any persona is built
273
+ * 2. onBuildContext — per persona, before template rendering
274
+ * 3. onPostRender — per persona, after body rendering
275
+ * 4. onValidate — per persona, during the validation phase
276
+ */
277
+ interface PersonaBuildPlugin {
278
+ /**
279
+ * Unique name for this plugin (used in log messages and error reporting).
280
+ */
281
+ name: string;
282
+ /**
283
+ * Called once per suite before any persona is built.
284
+ *
285
+ * Use this hook to perform suite-level setup — e.g. loading external data,
286
+ * validating the suite config, or mutating `sharedMeta` for downstream hooks.
287
+ *
288
+ * @param suite The suite configuration object
289
+ * @param sharedMeta Shared metadata merged from `_shared.yaml` (mutate in place if needed)
290
+ */
291
+ onSuiteInit?(suite: SuiteConfig, sharedMeta: Record<string, unknown>): void;
292
+ /**
293
+ * Called for each persona before template rendering.
294
+ *
295
+ * Receives the current rendering context and must return a (possibly mutated)
296
+ * context object. Plugins are chained: each plugin receives the output of the
297
+ * previous one.
298
+ *
299
+ * @param context Current rendering context (accumulates across plugins)
300
+ * @param persona Typed metadata for the persona being built
301
+ * @param suite The suite configuration object
302
+ * @returns Updated rendering context (must include all original keys)
303
+ */
304
+ onBuildContext?(context: Record<string, unknown>, persona: PersonaMetadata, suite: SuiteConfig): Record<string, unknown>;
305
+ /**
306
+ * Called for each persona after body rendering.
307
+ *
308
+ * Receives the rendered output string and can return a mutated version.
309
+ * Plugins are chained: each plugin receives the output of the previous one.
310
+ *
311
+ * @param output The rendered persona output string (accumulates across plugins)
312
+ * @param persona Typed metadata for the persona being built
313
+ * @param target The current build target
314
+ * @returns Updated output string
315
+ */
316
+ onPostRender?(output: string, persona: PersonaMetadata, target: TargetType): string;
317
+ /**
318
+ * Called during the validation phase for each persona.
319
+ *
320
+ * Return an array of ValidationResult objects (or an empty array).
321
+ * Results from all plugins are collected into a flat array by the runner.
322
+ *
323
+ * @param persona Typed metadata for the persona being built
324
+ * @param suite The suite configuration object
325
+ * @returns Array of validation results (may be empty)
326
+ */
327
+ onValidate?(persona: PersonaMetadata, suite: SuiteConfig): ValidationResult[];
328
+ /**
329
+ * Optional map of custom frontmatter templates keyed by target type.
330
+ *
331
+ * When present, the builder will use these templates in place of (or to
332
+ * augment) the library defaults for the matching target.
333
+ */
334
+ frontmatterTemplates?: Partial<Record<TargetType, string>>;
335
+ }
336
+
337
+ /**
338
+ * src/loaders/metadata-loader.ts
339
+ *
340
+ * File-system loader for persona YAML metadata files.
341
+ *
342
+ * Provides two exports:
343
+ *
344
+ * 1. `discoverPersonaYamls(root)` — recursively walks `root` and returns
345
+ * absolute paths for every `*.yaml` file found, regardless of nesting
346
+ * depth. Uses Node's built-in `fs.readdir` with `recursive: true`
347
+ * (available since Node 18.17). No glob library is required.
348
+ *
349
+ * 2. `loadMetadata(yamlPath)` — reads a single YAML file and parses it
350
+ * with `js-yaml` into a fully typed `PersonaMetadata` object.
351
+ *
352
+ * Path construction relies exclusively on `node:path` so the output is
353
+ * correct on both POSIX and Windows.
354
+ */
355
+
356
+ /**
357
+ * Recursively discover all `*.yaml` files under `root` and return their
358
+ * absolute paths sorted lexicographically.
359
+ *
360
+ * Uses `readdir` with `{ recursive: true }` (Node ≥ 18.17). Each returned
361
+ * path is normalised through `path.resolve` so callers always receive
362
+ * absolute, platform-consistent paths.
363
+ *
364
+ * @param root The directory to search (absolute or resolvable relative path).
365
+ * @returns Sorted array of absolute paths to every `*.yaml` file found.
366
+ *
367
+ * @example
368
+ * const yamls = await discoverPersonaYamls('/project/personas/ledger/src/meta');
369
+ * // ['/project/personas/ledger/src/meta/alpha.yaml', ...]
370
+ */
371
+ declare function discoverPersonaYamls(root: string): Promise<string[]>;
372
+ /**
373
+ * Load and parse a single persona YAML file into a typed `PersonaMetadata`
374
+ * object.
375
+ *
376
+ * The YAML is parsed using `js-yaml`'s safe `load` function. The result
377
+ * is validated to be a non-null object; if the YAML is empty or does not
378
+ * parse to an object, an `Error` is thrown.
379
+ *
380
+ * `PersonaMetadata` requires a `name` field. If the YAML does not contain
381
+ * a `name` key the function throws an `Error` with a descriptive message.
382
+ *
383
+ * @param yamlPath Absolute path to the YAML file.
384
+ * @returns Parsed and validated `PersonaMetadata` object.
385
+ * @throws `Error` when the file is unparseable, not an object, or
386
+ * is missing the required `name` field.
387
+ *
388
+ * @example
389
+ * const meta = await loadMetadata('/project/meta/my-persona.yaml');
390
+ * // { name: 'my-persona', description: '...', tools: [...] }
391
+ */
392
+ declare function loadMetadata(yamlPath: string): Promise<PersonaMetadata>;
393
+
394
+ /**
395
+ * src/loaders/content-loader.ts
396
+ *
397
+ * File-system loader for persona Markdown content templates.
398
+ *
399
+ * Provides a single `loadContent` function that reads the raw string content
400
+ * of a persona Markdown file from disk. The content is returned exactly as
401
+ * stored — no template substitution, no post-processing. Those concerns
402
+ * belong to the engine layer.
403
+ *
404
+ * All I/O is asynchronous. Path construction uses `node:path` so the
405
+ * implementation is path-separator–agnostic.
406
+ */
407
+ /**
408
+ * Read a persona Markdown content file and return its raw string content.
409
+ *
410
+ * The file is read with UTF-8 encoding. No parsing, template resolution,
411
+ * or post-processing is applied — that is the engine layer's responsibility.
412
+ *
413
+ * @param mdPath Absolute (or resolvable relative) path to the `.md` file.
414
+ * @returns Raw UTF-8 string content of the file.
415
+ * @throws An `ENOENT` error (from `fs/promises`) if the file does not
416
+ * exist, or any other I/O error the OS reports.
417
+ *
418
+ * @example
419
+ * const body = await loadContent('/project/content/my-persona.md');
420
+ * // '{{> greeting}}\n\n## About\n\nThis is {{name}}...'
421
+ */
422
+ declare function loadContent(mdPath: string): Promise<string>;
423
+
424
+ /**
425
+ * src/plugins/runner.ts
426
+ *
427
+ * Plugin runner — responsible for invoking plugin hooks in registration order.
428
+ *
429
+ * Each exported function corresponds to one lifecycle hook defined in
430
+ * PersonaBuildPlugin. The runner:
431
+ * - Skips plugins that do not implement the requested hook (hook is optional)
432
+ * - Invokes hooks in the order plugins are registered (first-in first-called)
433
+ * - For accumulating hooks (onBuildContext, onPostRender), each plugin
434
+ * receives the output of the previous plugin as its first argument
435
+ * - For collecting hooks (onValidate), results are concatenated into a
436
+ * flat array
437
+ *
438
+ * No file-system I/O. No async operations.
439
+ */
440
+
441
+ /**
442
+ * Invoke the `onSuiteInit` hook on every registered plugin.
443
+ *
444
+ * Each plugin may optionally implement this hook. Plugins are called in
445
+ * registration order. The hook receives the suite config and a mutable
446
+ * `sharedMeta` object — plugins may mutate `sharedMeta` in place; the
447
+ * same reference is passed to every subsequent plugin.
448
+ *
449
+ * @param plugins Ordered list of registered plugins
450
+ * @param suite The suite configuration object
451
+ * @param sharedMeta Mutable shared metadata object (mutated in place by plugins)
452
+ */
453
+ declare function runSuiteInit(plugins: PersonaBuildPlugin[], suite: SuiteConfig, sharedMeta: Record<string, unknown>): void;
454
+ /**
455
+ * Invoke the `onBuildContext` hook on every registered plugin, accumulating
456
+ * context mutations sequentially.
457
+ *
458
+ * Each plugin receives the context returned by the previous plugin. If a
459
+ * plugin does not implement `onBuildContext`, the context passes through
460
+ * unchanged. The final accumulated context is returned.
461
+ *
462
+ * @param plugins Ordered list of registered plugins
463
+ * @param ctx Initial rendering context for this persona
464
+ * @param persona Typed metadata for the persona being built
465
+ * @param suite The suite configuration object
466
+ * @returns Accumulated rendering context after all plugins have run
467
+ */
468
+ declare function runBuildContext(plugins: PersonaBuildPlugin[], ctx: Record<string, unknown>, persona: PersonaMetadata, suite: SuiteConfig): Record<string, unknown>;
469
+ /**
470
+ * Invoke the `onPostRender` hook on every registered plugin, chaining the
471
+ * output string sequentially.
472
+ *
473
+ * Each plugin receives the string returned by the previous plugin. If a
474
+ * plugin does not implement `onPostRender`, the string passes through
475
+ * unchanged. The final string is returned.
476
+ *
477
+ * @param plugins Ordered list of registered plugins
478
+ * @param rendered Initial rendered output string
479
+ * @param persona Typed metadata for the persona being built
480
+ * @param target The current build target
481
+ * @returns Final output string after all plugins have run
482
+ */
483
+ declare function runPostRender(plugins: PersonaBuildPlugin[], rendered: string, persona: PersonaMetadata, target: TargetType): string;
484
+ /**
485
+ * Invoke the `onValidate` hook on every registered plugin and collect all
486
+ * returned ValidationResult objects into a single flat array.
487
+ *
488
+ * Plugins that do not implement `onValidate` contribute nothing to the result.
489
+ * The return value is always an array (never null/undefined).
490
+ *
491
+ * @param plugins Ordered list of registered plugins
492
+ * @param persona Typed metadata for the persona being built
493
+ * @param suite The suite configuration object
494
+ * @returns Flat array of all ValidationResult objects from all plugins
495
+ */
496
+ declare function runValidate(plugins: PersonaBuildPlugin[], persona: PersonaMetadata, suite: SuiteConfig): ValidationResult[];
497
+
498
+ /**
499
+ * src/builders/types.ts
500
+ *
501
+ * Core types for the persona builder layer.
502
+ *
503
+ * Defines:
504
+ * - BuildConfig — typed configuration accepted by build()
505
+ * - BuildResult — outcome of building a single persona
506
+ * - BuildSummary — aggregated result returned by build()
507
+ *
508
+ * TargetType is re-exported from plugins/types so consumers can import
509
+ * everything builder-related from a single module.
510
+ */
511
+
512
+ /**
513
+ * Top-level configuration accepted by `build()`.
514
+ *
515
+ * At minimum, `suites` must be provided. All other fields have sensible
516
+ * defaults so a minimal configuration is:
517
+ *
518
+ * ```ts
519
+ * const summary = await build({
520
+ * suites: { my-suite: { srcDir: './src', outVscode: './out/vs', outClaudeCode: './out/cc' } },
521
+ * });
522
+ * ```
523
+ */
524
+ interface BuildConfig {
525
+ /**
526
+ * Named map of suite configurations. Each key is a suite identifier; the
527
+ * value describes source and output directories for that suite.
528
+ */
529
+ suites: Record<string, SuiteConfig>;
530
+ /**
531
+ * Absolute path to the shared partials directory. When provided, partials
532
+ * from this directory are loaded as the base layer before suite-local
533
+ * partials are overlaid. Optional.
534
+ */
535
+ sharedPartialsDir?: string;
536
+ /**
537
+ * List of registered plugins. Plugins are invoked in array order for every
538
+ * hook. Defaults to `[]`.
539
+ */
540
+ plugins?: PersonaBuildPlugin[];
541
+ /**
542
+ * Target output formats to build. Defaults to both `'vscode'` and
543
+ * `'claude-code'` when omitted.
544
+ */
545
+ targets?: Array<'vscode' | 'claude-code'>;
546
+ /**
547
+ * When `true`, no files are written to disk. The build still renders all
548
+ * personas and collects ValidationResults, but all write operations are
549
+ * skipped. Defaults to `false`.
550
+ */
551
+ check?: boolean;
552
+ /**
553
+ * When `true`, the build fails (throws or returns a failed summary) if any
554
+ * ValidationResult has severity `'error'` or `'warning'`. Defaults to
555
+ * `false`.
556
+ */
557
+ strict?: boolean;
558
+ /**
559
+ * Optional map of default frontmatter templates, keyed by target type.
560
+ * These are used as library defaults and can be overridden by plugin
561
+ * `frontmatterTemplates`. When absent, built-in defaults from
562
+ * `src/builders/frontmatter.ts` are used.
563
+ */
564
+ frontmatter?: Partial<Record<'vscode' | 'claude-code', string>>;
565
+ }
566
+ /**
567
+ * The outcome of building a single persona for a single target.
568
+ */
569
+ interface BuildResult {
570
+ /** The suite identifier this persona belongs to */
571
+ suite: string;
572
+ /** Target platform this result was generated for */
573
+ target: 'vscode' | 'claude-code';
574
+ /** Absolute path to the persona YAML source file */
575
+ personaYamlPath: string;
576
+ /** Absolute path to the output file (may not exist if check mode) */
577
+ outputPath: string;
578
+ /** The rendered persona content */
579
+ content: string;
580
+ /** Validation results collected from all plugins */
581
+ validationResults: ValidationResult[];
582
+ /** Whether the output file was written to disk (false in check mode) */
583
+ written: boolean;
584
+ }
585
+ /**
586
+ * Aggregated result returned by `build()` after processing all suites and
587
+ * targets.
588
+ */
589
+ interface BuildSummary {
590
+ /** Whether the overall build succeeded */
591
+ success: boolean;
592
+ /** Individual results for each persona × target combination */
593
+ results: BuildResult[];
594
+ /**
595
+ * When `strict` mode is enabled and a failure was detected, this holds all
596
+ * ValidationResults with severity `'error'` or `'warning'` that caused the
597
+ * failure. Empty otherwise.
598
+ */
599
+ strictFailures: ValidationResult[];
600
+ /** Total number of persona files processed */
601
+ totalBuilt: number;
602
+ /** Total number of output files written (0 in check mode) */
603
+ totalWritten: number;
604
+ }
605
+
606
+ /**
607
+ * src/builders/frontmatter.ts
608
+ *
609
+ * Frontmatter template registry for @smor/persona-build.
610
+ *
611
+ * Ships two minimal default templates — one per target — that work for the
612
+ * "standalone" persona mode (simple personas without numbered workflows or
613
+ * MCP server blocks). Projects needing richer frontmatter register custom
614
+ * templates via the `PersonaBuildPlugin.frontmatterTemplates` property.
615
+ *
616
+ * Template rendering follows the same two-step sequence as body rendering:
617
+ * 1. resolveConditionals() — resolve {{#if flag}} blocks
618
+ * 2. resolveVariables() — substitute {{varName}} markers
619
+ *
620
+ * No partials in frontmatter — frontmatter is kept deliberately simple.
621
+ */
622
+
623
+ /**
624
+ * Default VS Code frontmatter template.
625
+ *
626
+ * Minimal fields that work for standalone personas. Projects using numbered
627
+ * workflows (e.g. ledger) should inject a richer template via a plugin.
628
+ */
629
+ declare const DEFAULT_FRONTMATTER_VSCODE = "---\nname: '{{name}} v{{version}}'\ndescription: '{{description}}'\ntools: [{{tools_list}}]\n---";
630
+ /**
631
+ * Default Claude Code frontmatter template.
632
+ *
633
+ * Minimal fields that work for standalone personas. Projects using numbered
634
+ * workflows should inject a richer template via a plugin.
635
+ */
636
+ declare const DEFAULT_FRONTMATTER_CLAUDE_CODE = "---\nname: {{cc_file_name_stem}}\npermissionMode: {{cc_permission_mode}}\nmodel: {{cc_model}}\nmemory: {{cc_memory}}\nallowedTools: [{{cc_tools_list}}]\n---";
637
+ /**
638
+ * Resolve frontmatter template precedence.
639
+ *
640
+ * Precedence order (highest wins):
641
+ * 1. Plugin `frontmatterTemplates` — the last plugin with a matching key
642
+ * wins (plugins are applied in reverse-registration order so the
643
+ * *first* registered plugin with a template takes precedence over later
644
+ * ones, matching the general plugin-chain contract).
645
+ * 2. `configTemplates` — templates passed via `BuildConfig.frontmatter`
646
+ * 3. Library defaults (`DEFAULT_FRONTMATTER_VSCODE` / `DEFAULT_FRONTMATTER_CLAUDE_CODE`)
647
+ *
648
+ * @param target The build target ('vscode' | 'claude-code')
649
+ * @param plugins Registered plugins (searched in order; first match wins)
650
+ * @param configTemplates Optional caller-supplied overrides from BuildConfig
651
+ * @returns The resolved template string
652
+ */
653
+ declare function resolveFrontmatterTemplate(target: 'vscode' | 'claude-code', plugins: PersonaBuildPlugin[], configTemplates?: Partial<Record<'vscode' | 'claude-code', string>>): string;
654
+ /**
655
+ * Render a frontmatter template string against the given context.
656
+ *
657
+ * Applies the standard two-step template resolution:
658
+ * 1. `resolveConditionals` — `{{#if flag}}` blocks
659
+ * 2. `resolveVariables` — `{{varName}}` substitution
660
+ *
661
+ * @param template The raw frontmatter template string (may contain markers)
662
+ * @param context Key-value context for variable substitution
663
+ * @param filename Source filename used in warning messages
664
+ * @returns Rendered frontmatter string (ready to prepend to body)
665
+ */
666
+ declare function renderFrontmatter(template: string, context: Record<string, unknown>, filename: string): string;
667
+
668
+ /**
669
+ * src/builders/persona-builder.ts
670
+ *
671
+ * Core build orchestrator for @smor/persona-build.
672
+ *
673
+ * Exports three public functions:
674
+ *
675
+ * 1. buildPersona(personaYamlPath, suiteName, suiteConfig, sharedMeta,
676
+ * partialsMap, config, plugins)
677
+ * — Builds a single persona for a single target. Returns a BuildResult.
678
+ *
679
+ * 2. buildSuite(suiteName, suiteConfig, config, plugins)
680
+ * — Discovers all persona YAMLs for a suite, fires onSuiteInit, maps
681
+ * buildPersona() over each, and returns BuildResult[].
682
+ *
683
+ * 3. build(config)
684
+ * — Top-level entry point. Iterates all suites × targets, calls
685
+ * buildSuite() for each combination, and returns a BuildSummary.
686
+ * Respects --check (no writes) and --strict (fail on warnings/errors).
687
+ */
688
+
689
+ /**
690
+ * Build a single persona for a single output target.
691
+ *
692
+ * Pipeline:
693
+ * 1. Load sharedMeta + personaMeta (callers supply pre-loaded values)
694
+ * 2. Build merged context
695
+ * 3. Run onBuildContext plugin hooks (context accumulation)
696
+ * 4. Resolve frontmatter template → render frontmatter
697
+ * 5. Load content template
698
+ * 6. Render body: partials → conditionals → variables → post-process
699
+ * 7. Assemble final output (frontmatter + body)
700
+ * 8. Run onPostRender plugin hooks (output chain)
701
+ * 9. Run onValidate plugin hooks (validation collection)
702
+ * 10. Determine output file path
703
+ * 11. Write output file (unless check mode)
704
+ * 12. Return BuildResult
705
+ *
706
+ * @param personaYamlPath Absolute path to the persona YAML source file
707
+ * @param suiteName Identifier for the suite this persona belongs to
708
+ * @param suiteConfig Suite configuration object
709
+ * @param sharedMeta Pre-loaded `_shared.yaml` contents
710
+ * @param partialsMap Pre-loaded partials map (shared + suite-local merged)
711
+ * @param config Top-level BuildConfig
712
+ * @param plugins Registered plugins
713
+ * @param target Target output format
714
+ * @returns BuildResult for this persona × target combination
715
+ */
716
+ declare function buildPersona(personaYamlPath: string, suiteName: string, suiteConfig: SuiteConfig, sharedMeta: Record<string, unknown>, partialsMap: Record<string, string>, config: BuildConfig, plugins: PersonaBuildPlugin[], target: 'vscode' | 'claude-code'): Promise<BuildResult>;
717
+ /**
718
+ * Build all personas in a suite for a single output target.
719
+ *
720
+ * Pipeline:
721
+ * 1. Load `_shared.yaml` for the suite
722
+ * 2. Load merged partials (shared → suite-local)
723
+ * 3. Run `onSuiteInit` on all plugins
724
+ * 4. Discover all persona YAML files
725
+ * 5. Call `buildPersona()` for each
726
+ *
727
+ * @param suiteName Identifier for this suite
728
+ * @param suiteConfig Suite configuration
729
+ * @param config Top-level BuildConfig
730
+ * @param plugins Registered plugins
731
+ * @param target Target output format
732
+ * @returns Array of BuildResult objects, one per persona
733
+ */
734
+ declare function buildSuite(suiteName: string, suiteConfig: SuiteConfig, config: BuildConfig, plugins: PersonaBuildPlugin[], target: 'vscode' | 'claude-code'): Promise<BuildResult[]>;
735
+ /**
736
+ * Top-level build orchestrator.
737
+ *
738
+ * Iterates all `config.suites × config.targets` combinations, calls
739
+ * `buildSuite()` for each, and aggregates the results into a `BuildSummary`.
740
+ *
741
+ * Modes:
742
+ * - Normal: renders and writes all personas.
743
+ * - `check: true`: renders without writing; useful for CI staleness checks.
744
+ * - `strict: true`: throws when any ValidationResult has severity `'error'`
745
+ * or `'warning'`. All suites are processed before the throw, so output
746
+ * files **will** be written to disk even when the build ultimately fails.
747
+ * **For CI usage, combine `strict: true` with `check: true`** to avoid
748
+ * leaving partial artefacts on disk when validation fails.
749
+ *
750
+ * @param config Typed build configuration
751
+ * @returns Aggregated BuildSummary
752
+ * @throws `Error` when `strict: true` and validation failures exist
753
+ */
754
+ declare function build(config: BuildConfig): Promise<BuildSummary>;
755
+
756
+ /**
757
+ * src/validators/filename-validator.ts
758
+ *
759
+ * Validates persona output filenames against the project naming convention.
760
+ *
761
+ * Convention: kebab-case only — lowercase letters, digits, and hyphens.
762
+ * No spaces, no uppercase letters, no special characters other than hyphens
763
+ * and dots (for the file extension).
764
+ *
765
+ * This is a pure function: no file I/O, no process.exit, no side effects.
766
+ * It depends only on `ValidationResult` from `src/plugins/types.ts`.
767
+ */
768
+
769
+ /**
770
+ * Validate a persona filename against the project naming convention.
771
+ *
772
+ * Accepts either a bare filename (`my-persona.md`) or a full/relative path
773
+ * — only the basename (last path segment) is evaluated.
774
+ *
775
+ * @param filePath Filename or path to validate (only the basename is checked)
776
+ * @returns Empty array when the filename conforms; one ValidationResult
777
+ * per violated rule otherwise. Each result has severity "error".
778
+ *
779
+ * @example
780
+ * validateFileName('my-persona.md'); // []
781
+ * validateFileName('My Persona.md'); // [{severity:'error', message:'...'}]
782
+ * validateFileName('/abs/path/my-persona.md');// []
783
+ */
784
+ declare function validateFileName(filePath: string): ValidationResult[];
785
+
786
+ /**
787
+ * src/validators/strict-validator.ts
788
+ *
789
+ * Validates that a set of required marker strings are present in a rendered
790
+ * persona output string.
791
+ *
792
+ * "Strict" mode in the build pipeline guards against incomplete renders —
793
+ * e.g. a required section marker (e.g. "{{ROLE}}") that was never resolved.
794
+ * This validator generalises that concept: callers supply the list of marker
795
+ * strings that *must* appear in the final rendered content.
796
+ *
797
+ * This is a pure function: no file I/O, no side effects.
798
+ * It depends only on `ValidationResult` from `src/plugins/types.ts`.
799
+ */
800
+
801
+ /**
802
+ * Validate that every required marker string is present in the rendered output.
803
+ *
804
+ * Each absent marker produces one `ValidationResult` entry with severity
805
+ * `"error"` and a descriptive message identifying the missing marker.
806
+ *
807
+ * @param renderedContent The final rendered output string to inspect
808
+ * @param requiredMarkers Array of marker strings that must appear verbatim in
809
+ * `renderedContent`. An empty array always returns `[]`.
810
+ * @returns Empty array when all markers are found; one entry per
811
+ * absent marker otherwise. Each entry has severity "error".
812
+ *
813
+ * @example
814
+ * validateStrictMarkers('Hello world', ['Hello', 'world']); // []
815
+ * validateStrictMarkers('Hello world', ['{{MISSING}}']);
816
+ * // [{severity:'error', message:'Required marker "{{MISSING}}" is missing from the rendered output.'}]
817
+ */
818
+ declare function validateStrictMarkers(renderedContent: string, requiredMarkers: string[]): ValidationResult[];
819
+
820
+ /**
821
+ * @smor/persona-build
822
+ *
823
+ * Public API barrel export.
824
+ * Feature modules will be exported from here as they are implemented in subsequent WPs.
825
+ */
826
+
827
+ declare const VERSION: string;
828
+
829
+ export { type BuildConfig, type BuildResult, type BuildSummary, DEFAULT_FRONTMATTER_CLAUDE_CODE, DEFAULT_FRONTMATTER_VSCODE, type PersonaBuildPlugin, type PersonaMetadata, type SuiteConfig, type TargetType, VERSION, type ValidationResult, build, buildPersona, buildSuite, collapseBlankLines, discoverPersonaYamls, ensureBlankLineBeforeHeadings, loadContent, loadMetadata, loadPartials, normalizeNewlines, renderFrontmatter, resolveConditionals, resolveFrontmatterTemplate, resolvePartials, resolveVariables, runBuildContext, runPostRender, runSuiteInit, runValidate, serializeTools, serializeToolsList, validateFileName, validateStrictMarkers };