@orkestrel/template 0.0.5 → 0.0.7

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.
@@ -1,665 +1,841 @@
1
- import { ContractShape } from '@orkestrel/contract';
2
- import { EmitterErrorHandler } from '@orkestrel/emitter';
3
- import { EmitterHooks } from '@orkestrel/emitter';
4
- import { EmitterInterface } from '@orkestrel/emitter';
5
- import { FieldPath } from '@orkestrel/contract';
1
+ import type { ContractShape } from '@orkestrel/contract';
2
+ import type { EmitterErrorHandler } from '@orkestrel/emitter';
3
+ import type { EmitterHooks } from '@orkestrel/emitter';
4
+ import type { EmitterInterface } from '@orkestrel/emitter';
5
+ import type { FieldPath } from '@orkestrel/contract';
6
6
 
7
7
  /**
8
- * Create a template.
8
+ * Creates a working {@link TemplateInterface} from a {@link TemplateOptions}
9
+ * bag, backed by the `Template` class.
9
10
  *
10
11
  * @param options - The template's `name` / `content`, an optional `id`
11
12
  * (defaults to a generated UUID), `placeholders`, catalog metadata, and
12
13
  * `missing` / `locale` fill defaults
13
14
  * @returns A working {@link TemplateInterface}
14
- *
15
- * @example
16
- * ```ts
17
- * import { createTemplate } from '@src/core'
18
- *
19
- * const greeting = createTemplate({ name: 'greeting', content: 'Hi {{name}}' })
20
- * greeting.fill({ name: 'Ada' }) // 'Hi Ada'
21
- * ```
22
- */
23
- export declare function createTemplate(options: TemplateOptions): TemplateInterface;
24
-
25
- /**
26
- * Create a template registry.
27
- *
28
- * @param options - Optional initial `templates` seed collection and
29
- * manager-wide `missing` / `locale` fill defaults, emitter `on` hooks, and
30
- * an `error` handler
31
- * @returns A working {@link TemplateManagerInterface}
32
- *
33
- * @example
34
- * ```ts
35
- * import { createTemplateManager } from '@src/core'
36
- *
37
- * const templates = createTemplateManager({
38
- * templates: [{ id: 'greeting', name: 'greeting', content: 'Hi {{name}}' }],
39
- * })
40
- * templates.fill('greeting', { name: 'Ada' }) // 'Hi Ada'
41
- * ```
42
- */
43
- export declare function createTemplateManager(options?: TemplateManagerOptions): TemplateManagerInterface;
44
-
45
- /** Default `locale` for `Template#fill` / `TemplateManager#fill` when unspecified. */
46
- export declare const DEFAULT_LOCALE = "en-US";
47
-
48
- /** Default `missing` policy for `Template#fill` / `TemplateManager#fill` when unspecified. */
49
- export declare const DEFAULT_MISSING_POLICY: MissingPolicy;
50
-
51
- /**
52
- * The single-pass `{{name}}` substitution pattern shared by `Template#fill`
53
- * and `Template#validate`.
54
- *
55
- * @remarks
56
- * Global-flagged, two-alternative pattern: a match of the FIRST alternative
57
- * (`\{{` — a literal backslash followed by `{{`) means "emit a literal
58
- * `{{`" — the escape hatch for content that must show `{{` without
59
- * triggering substitution. A match that instead populates capture group 1
60
- * (`\{{([^{}]+?)\}\}`) means "substitute the named token" — group 1 is the
61
- * RAW (untrimmed) token text between the braces; every call site trims it
62
- * (`token.trim()`) before using it as a lookup name, so `'{{ name }}'` still
63
- * resolves `'name'`. The pattern intentionally does NOT wrap the token in
64
- * `\s*` — an unclosed `'{{' + ' '.repeat(n)` with no closing `}}` would
65
- * otherwise force the regex engine into catastrophic backtracking over the
66
- * whitespace run (O(n^2)); trimming after the match keeps the same
67
- * whitespace tolerance without the backtracking hazard. Every call site
68
- * builds a fresh `RegExp` from `.source` / `.flags` rather than sharing this
69
- * instance's mutable `lastIndex` across scans.
70
- */
71
- export declare const FILL_PATTERN: RegExp;
72
-
73
- /**
74
- * Substitute every `{{name}}` token in `content` in a single pass.
75
- *
76
- * @remarks
77
- * Uses a fresh `RegExp` clone of `FILL_PATTERN` per call (never sharing its
78
- * `lastIndex`) and a single `String#replace` scan — substituted output is
79
- * never re-scanned. For each token: the matching declared
80
- * {@link TemplatePlaceholder} (exact `name`) supplies its `path` (falling
81
- * back to the token split on `.`); ANY path segment in `UNSAFE_FIELD_SEGMENTS`
82
- * makes the token unresolved without ever calling `resolveField` (a
83
- * prototype-pollution guard). A resolved value formats via `formatValue`; an
84
- * unresolved value falls back to the placeholder's `fallback` when declared;
85
- * otherwise `options.missing` governs — `'literal'` re-emits the original
86
- * `{{name}}` text, `'empty'` emits `''`, and `'error'` emits `''` for every
87
- * token but collects EVERY unresolved required token (an undeclared token, or
88
- * a declared token with `required !== false`) and throws one
89
- * {@link TemplateError} coded `MISSING` listing them all, in first-appearance
90
- * order, once the scan completes. An escaped `\{{` emits a literal `{{`.
91
- *
92
- * PARITY: called with no declared `placeholders` and `{ missing: 'empty' }`,
93
- * this reproduces `interpolateMessage` (`@src/core` sibling
94
- * `interpret`) vector-for-vector. KNOWN DIVERGENCE: `FILL_PATTERN`'s token
95
- * class (`[^{}]`) excludes `{`, where `interpolateMessage`'s (`[^}]`) allows
96
- * it — a token containing `{` therefore behaves differently here.
97
- *
98
- * @param content - The template content carrying `{{name}}` tokens
99
- * @param values - The values tokens resolve against
100
- * @param options - `missing` (default `'error'`), `locale` (default `'en-US'`), and the declared `placeholders` (default none) tokens resolve against
101
- * @returns The substituted content
102
- *
103
- * @example
104
- * ```ts
105
- * import { fillTemplate } from '@src/core'
106
- *
107
- * fillTemplate('Hi {{name}}', { name: 'Ada' }) // 'Hi Ada'
108
- * fillTemplate('Limit {{limit}}', { limit: 5010 }, { missing: 'empty' }) // 'Limit 5,010'
109
- * ```
110
- */
111
- export declare function fillTemplate(content: string, values?: TemplateFillValues, options?: TemplateFillOptions & {
112
- readonly placeholders?: readonly TemplatePlaceholder[];
113
- }): string;
114
-
115
- /**
116
- * Format a resolved fill value for substitution into a template's `content`.
117
- *
118
- * @remarks
119
- * A finite number renders with the given locale's thousand grouping (via
120
- * `toLocaleString`); every other value — including `null` — String-coerces.
121
- * `null` therefore renders as the literal string `'null'`, intentionally
122
- * mirroring `interpolateMessage`'s coercion parity (see `fillTemplate`). An
123
- * invalid BCP-47 `locale` tag throws a `RangeError` from the underlying
124
- * `toLocaleString` call when `value` is a finite number — this is a caller
125
- * error (an invalid locale argument), by design, and is not caught here.
126
- *
127
- * @param value - The resolved value to format
128
- * @param locale - The locale used for finite-number formatting
129
- * @returns The formatted string
130
- *
131
- * @example
132
- * ```ts
133
- * import { formatValue } from '@src/core'
134
- *
135
- * formatValue(5010, 'en-US') // '5,010'
136
- * formatValue(null, 'en-US') // 'null'
137
- * ```
138
- */
139
- export declare function formatValue(value: unknown, locale: string): string;
140
-
141
- /**
142
- * Narrow an unknown caught value to a {@link TemplateError}.
143
- *
144
- * @param value - The value to test (typically a `catch` binding)
145
- * @returns `true` when `value` is a {@link TemplateError}
146
- *
147
- * @example
148
- * ```ts
149
- * import { isTemplateError } from '@src/core'
150
- *
151
- * try {
152
- * manager.template('missing')
153
- * } catch (error) {
154
- * if (isTemplateError(error) && error.code === 'NOTFOUND') return
155
- * }
156
- * ```
157
- */
158
- export declare function isTemplateError(value: unknown): value is TemplateError;
159
-
160
- /**
161
- * How {@link TemplateInterface#fill} handles an unresolved required
162
- * placeholder.
163
- *
164
- * @remarks
165
- * `error` — throws a {@link TemplateError} coded `MISSING`. `empty` —
166
- * substitutes an empty string. `literal` — substitutes the placeholder's own
167
- * `{{name}}` token back into the output, unchanged.
168
- */
169
- export declare type MissingPolicy = 'error' | 'empty' | 'literal';
170
-
171
- /**
172
- * Build the `@orkestrel/contract` object shape describing a template's
173
- * declared placeholders.
174
- *
175
- * @remarks
176
- * Each placeholder becomes a `stringShape` carrying its `description`;
177
- * `required === false` wraps it in `optionalShape`. Used by `Template` to
178
- * compile its `parameters()` contract once per instance.
179
- *
180
- * @param placeholders - The declared placeholders to shape
181
- * @returns The contract shape for `createContract`
182
- *
183
- * @example
184
- * ```ts
185
- * import { placeholderShape } from '@src/core'
186
- * import { createContract } from '@orkestrel/contract'
187
- *
188
- * const contract = createContract(placeholderShape([{ name: 'city' }]))
189
- * ```
190
- */
191
- export declare function placeholderShape(placeholders: readonly TemplatePlaceholder[]): ContractShape;
192
-
193
- /**
194
- * Resolve a field path against a fill-values record, refusing any path that
195
- * touches a prototype-pollution-unsafe segment.
196
- *
197
- * @remarks
198
- * A prototype-pollution guard shared by `fillTemplate` and `Template#validate`
199
- * so the two stay in lockstep: `path` normalizes to a segment array (a bare
200
- * string `path` becomes a single-segment array); if ANY segment appears in
201
- * `UNSAFE_FIELD_SEGMENTS` (`'__proto__'`, `'constructor'`, `'prototype'`), the
202
- * lookup is refused and `undefined` is returned WITHOUT ever calling
203
- * `resolveField` — a path like `['__proto__', 'polluted']` can never reach
204
- * the record's actual prototype chain through this function. Every other
205
- * path resolves through `@orkestrel/contract`'s `resolveField`.
206
- *
207
- * @param record - The fill-values record to resolve against
208
- * @param path - The field path — a single segment or a segment array
209
- * @returns The resolved value, or `undefined` when unresolved or the path is unsafe
210
- *
211
- * @example
212
- * ```ts
213
- * import { resolveSafeField } from '@src/core'
214
- *
215
- * resolveSafeField({ a: { b: 1 } }, ['a', 'b']) // 1
216
- * resolveSafeField({}, ['__proto__', 'polluted']) // undefined
217
- * ```
218
- */
219
- export declare function resolveSafeField(record: TemplateFillValues, path: FieldPath): unknown;
220
-
221
- /**
222
- * A named, versionable template — `{{name}}` tokens in `content`, filled
223
- * against a values record.
224
- *
225
- * @remarks
226
- * `missing` / `locale` seed this instance's default {@link TemplateFillOptions},
227
- * overridable per `fill` call. Its `parameters()` contract (built from
228
- * `placeholders` via `placeholderShape`) compiles once, in the constructor.
229
- *
230
- * @example
231
- * ```ts
232
- * const greeting = new Template({ name: 'greeting', content: 'Hi {{name}}' })
233
- * greeting.fill({ name: 'Ada' }) // 'Hi Ada'
234
- * ```
235
- */
236
- export declare class Template implements TemplateInterface {
237
- #private;
238
- readonly id: string;
239
- readonly name: string;
240
- readonly content: string;
241
- readonly placeholders: readonly TemplatePlaceholder[];
242
- readonly summary?: string;
243
- readonly description?: string;
244
- readonly category?: string;
245
- readonly tags?: readonly string[];
246
- constructor(options: TemplateOptions);
247
- /**
248
- * The plain, JSON-serializable data this template carries.
249
- *
250
- * @returns The {@link TemplateDefinition} record
251
- *
252
- * @example
253
- * ```ts
254
- * const instance = new Template({ name: 'greeting', content: 'Hi {{name}}' })
255
- * instance.definition().name // 'greeting'
256
- * ```
257
- */
258
- definition(): TemplateDefinition;
259
- /**
260
- * Substitute every `{{name}}` token in `content` against `values`.
261
- *
262
- * @param values - The values tokens resolve against
263
- * @param options - Per-call overrides for this instance's `missing` / `locale` defaults
264
- * @returns The substituted content
265
- *
266
- * @example
267
- * ```ts
268
- * const instance = new Template({ name: 'greeting', content: 'Hi {{name}}' })
269
- * instance.fill({ name: 'Ada' }) // 'Hi Ada'
270
- * ```
271
- */
272
- fill(values?: TemplateFillValues, options?: TemplateFillOptions): string;
273
- /**
274
- * Report which required placeholders would stay unresolved, and which
275
- * `values` keys go unused, without producing output.
276
- *
277
- * @remarks
278
- * Content-token driven: scans `this.content` for every `{{name}}` token
279
- * (skipping escaped `\{{` matches) the same way `fill` does, so `validate`
280
- * predicts `fill`'s `'error'`-{@link MissingPolicy} outcome exactly — a
281
- * token reported here as missing is precisely a token that would throw
282
- * under `fill(values, { missing: 'error' })`. For each distinct token
283
- * (first-appearance order, trimmed): a declared {@link TemplatePlaceholder}
284
- * sharing its `name` supplies `path` (falling back to the token split on
285
- * `.`); the value resolves via `resolveSafeField`. The token is `missing`
286
- * only when the value is unresolved AND no `fallback` is declared AND the
287
- * placeholder is required (`required !== false`, including undeclared
288
- * tokens). `extra` lists every `values` key with no declared placeholder.
15
+ * @throws {@link TemplateError} Thrown when `options.placeholders` declares a duplicate `name` or an empty `path` (coded `INVALID`)
289
16
  *
290
- * @param values - The values to check
291
- * @returns The {@link TemplateValidationResult}
292
- *
293
- * @example
17
+ * @example Create a template and a registry
294
18
  * ```ts
295
- * const instance = new Template({
296
- * name: 'greeting',
297
- * content: 'Hi {{name}}',
298
- * placeholders: [{ name: 'name' }],
299
- * })
300
- * instance.validate({}).missing // ['name']
301
- * ```
302
- */
303
- validate(values?: TemplateFillValues): TemplateValidationResult;
304
- /**
305
- * Project this template's placeholders to the open tool-parameters record
306
- * shape.
19
+ * import { createTemplate, createTemplateManager } from '@orkestrel/template'
307
20
  *
308
- * @returns The compiled parameters record, or `undefined` when `schemaToParameters` yields none
21
+ * const greeting = createTemplate({ name: 'greeting', content: 'Hi {{name}}' })
22
+ * greeting.fill({ name: 'Ada' }) // 'Hi Ada'
309
23
  *
310
- * @example
311
- * ```ts
312
- * const instance = new Template({
313
- * name: 'greeting',
314
- * content: 'Hi {{name}}',
315
- * placeholders: [{ name: 'name' }],
24
+ * const templates = createTemplateManager({
25
+ * templates: [
26
+ * { id: 'greeting', name: 'greeting', content: 'Hi {{name}}', category: 'mail' },
27
+ * { id: 'farewell', name: 'farewell', content: 'Bye {{name}}', category: 'mail' },
28
+ * { id: 'alert', name: 'alert', content: 'Alert: {{reason}}', category: 'ops' },
29
+ * ],
316
30
  * })
317
- * instance.parameters()
31
+ * templates.fill('greeting', { name: 'Ada' }) // 'Hi Ada'
32
+ * templates.find({ category: 'mail' }).map((one) => one.id) // ['greeting', 'farewell']
33
+ * templates.has('alert') // true
34
+ * templates.has('missing') // false
318
35
  * ```
319
36
  */
320
- parameters(): Readonly<Record<string, unknown>> | undefined;
321
- }
37
+ export declare function createTemplate(options: TemplateOptions): TemplateInterface;
322
38
 
323
- /**
324
- * A named, versionable template record pure data, no behavior.
325
- *
326
- * @remarks
327
- * `content` is the raw string carrying `{{name}}` tokens (see
328
- * `FILL_PATTERN` in `constants.ts`); `placeholders` declares every token's
329
- * lookup rule. `summary` / `description` / `category` / `tags` are optional
330
- * catalog metadata for `TemplateManagerInterface#find`.
331
- */
332
- export declare interface TemplateDefinition {
333
- readonly id: string;
334
- readonly name: string;
335
- readonly content: string;
336
- readonly placeholders: readonly TemplatePlaceholder[];
337
- readonly summary?: string;
338
- readonly description?: string;
339
- readonly category?: string;
340
- readonly tags?: readonly string[];
341
- }
39
+ /**
40
+ * Creates a working {@link TemplateManagerInterface}, optionally seeded with
41
+ * the templates the options carry, backed by the `TemplateManager` class.
42
+ *
43
+ * @param options - Optional initial `templates` seed collection and
44
+ * manager-wide `missing` / `locale` fill defaults, emitter `on` hooks, and
45
+ * an `error` handler
46
+ * @returns A working {@link TemplateManagerInterface}
47
+ * @throws {@link TemplateError} Thrown when a seeded `options.templates` bag declares a duplicate placeholder `name` or an empty `path` (coded `INVALID`)
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * import { createTemplateManager } from '@src/core'
52
+ *
53
+ * const templates = createTemplateManager({
54
+ * templates: [{ id: 'greeting', name: 'greeting', content: 'Hi {{name}}' }],
55
+ * })
56
+ * templates.fill('greeting', { name: 'Ada' }) // 'Hi Ada'
57
+ * ```
58
+ */
59
+ export declare function createTemplateManager(options?: TemplateManagerOptions): TemplateManagerInterface;
342
60
 
343
- /**
344
- * An error thrown by the template layer.
345
- *
346
- * @remarks
347
- * Thrown for: a required placeholder staying unresolved under the `error`
348
- * {@link MissingPolicy} (`MISSING`), an unknown template id
349
- * (`NOTFOUND`), `createTemplate` handed invalid data (`INVALID`), and
350
- * `TemplateManagerInterface#register` handed an id already present without
351
- * `options.replace` (`CONFLICT`). `context`, when present, carries the
352
- * offending id / name.
353
- */
354
- export declare class TemplateError extends Error {
355
- readonly code: TemplateErrorCode;
356
- readonly context?: Readonly<Record<string, unknown>>;
357
- constructor(code: TemplateErrorCode, message: string, context?: Readonly<Record<string, unknown>>);
358
- }
61
+ /**
62
+ * Holds `'en-US'`, the default `locale` for `Template#fill` /
63
+ * `TemplateManager#fill` when unspecified.
64
+ */
65
+ export declare const DEFAULT_LOCALE = "en-US";
359
66
 
360
- /**
361
- * Coded misuse / failure conditions thrown as a {@link TemplateError}.
362
- *
363
- * @remarks
364
- * `MISSING` a required placeholder stayed unresolved under the `error`
365
- * {@link MissingPolicy}. `NOTFOUND` — `TemplateManagerInterface#template`
366
- * (or `fill` / `validate` / `parameters` by id) was handed an unknown id.
367
- * `INVALID` — `createTemplate` was handed data that fails validation.
368
- * `CONFLICT` — `register` was handed an id already present without
369
- * `options.replace`.
370
- */
371
- export declare type TemplateErrorCode = 'MISSING' | 'NOTFOUND' | 'INVALID' | 'CONFLICT';
372
-
373
- /** Per-call options for `TemplateInterface#fill` / `TemplateManagerInterface#fill`. */
374
- export declare interface TemplateFillOptions {
375
- readonly missing?: MissingPolicy;
376
- readonly locale?: string;
377
- }
378
-
379
- /** The values a {@link TemplateInterface#fill} / `#validate` call resolves placeholders against. */
380
- export declare type TemplateFillValues = Readonly<Record<string, unknown>>;
67
+ /**
68
+ * Holds `'error'`, the default `missing` policy for `Template#fill` /
69
+ * `TemplateManager#fill` when unspecified.
70
+ */
71
+ export declare const DEFAULT_MISSING_POLICY: MissingPolicy;
381
72
 
382
- /**
383
- * The template contract (AGENTS §22 exact bijection with `Template`).
384
- *
385
- * @remarks
386
- * `definition` returns the plain {@link TemplateDefinition} data. `fill`
387
- * substitutes every `{{name}}` token in `content` against `values`,
388
- * honoring `options.missing` for unresolved required placeholders. `validate`
389
- * reports which required placeholders are unresolved (`missing`) and which
390
- * supplied `values` keys are unused (`extra`) without producing output.
391
- * `parameters` projects this template's placeholders to the open
392
- * tool-parameters record shape (`schemaToParameters`'s return type from
393
- * `@orkestrel/contract`).
394
- */
395
- export declare interface TemplateInterface {
396
- readonly id: string;
397
- readonly name: string;
398
- readonly content: string;
399
- readonly placeholders: readonly TemplatePlaceholder[];
400
- readonly summary?: string;
401
- readonly description?: string;
402
- readonly category?: string;
403
- readonly tags?: readonly string[];
404
- definition(): TemplateDefinition;
405
- fill(values?: TemplateFillValues, options?: TemplateFillOptions): string;
406
- validate(values?: TemplateFillValues): TemplateValidationResult;
407
- parameters(): Readonly<Record<string, unknown>> | undefined;
408
- }
73
+ /**
74
+ * Holds the single-pass `{{name}}` substitution pattern shared by
75
+ * `Template#fill` and `Template#validate`.
76
+ *
77
+ * @remarks
78
+ * Global-flagged, two-alternative pattern: a match of the first alternative
79
+ * (`\{{` a literal backslash followed by `{{`) means "emit a literal
80
+ * `{{`" the escape hatch for content that must show `{{` without
81
+ * triggering substitution. A match that instead populates capture group 1
82
+ * (`\{{([^{}]+?)\}\}`) means "substitute the named token" — group 1 is the
83
+ * untrimmed token text between the braces; every call site trims it
84
+ * (`token.trim()`) before using it as a lookup name, so `'{{ name }}'` still
85
+ * resolves `'name'`. The pattern deliberately does not wrap the token in
86
+ * `\s*` an unclosed `'{{' + ' '.repeat(n)` with no closing `}}` would
87
+ * otherwise force the regex engine into catastrophic backtracking over the
88
+ * whitespace run (O(n^2)); trimming after the match keeps the same
89
+ * whitespace tolerance without the backtracking hazard. Every call site
90
+ * builds a fresh `RegExp` from `.source` / `.flags` rather than sharing this
91
+ * instance's mutable `lastIndex` across scans.
92
+ */
93
+ export declare const FILL_PATTERN: RegExp;
409
94
 
410
- /**
411
- * The template registry a self-owning, id-keyed record-holder for the
412
- * {@link TemplateInterface} instances a consumer registers, looks up, fills,
413
- * and validates by id (AGENTS §9.1 singular/plural accessors, §9.2 batch
414
- * `remove` overloads, §13 emitter ownership).
415
- *
416
- * @remarks
417
- * `register` accepts either a constructed {@link TemplateInterface} (kept
418
- * as-is, including its own `missing` / `locale` defaults) or a plain
419
- * {@link TemplateOptions} bag constructed into a `Template` with this
420
- * manager's `missing` / `locale` defaults applied wherever the bag omits
421
- * them. A duplicate `id` throws a {@link TemplateError} coded `CONFLICT`
422
- * unless `options.replace` is `true`, in which case the existing entry is
423
- * overwritten. `options.templates` SEEDS the registry at construction
424
- * WITHOUT emitting `register` only calls to `register` after construction
425
- * emit. The batch `remove(ids)` form is ALL-OR-NOTHING: any id absent from
426
- * the registry leaves the collection untouched and returns `false`.
427
- *
428
- * @example
429
- * ```ts
430
- * import { TemplateManager } from '@src/core'
431
- *
432
- * const manager = new TemplateManager()
433
- * const instance = manager.register({ name: 'greeting', content: 'Hi {{name}}' })
434
- * manager.fill(instance.id, { name: 'Ada' }) // 'Hi Ada'
435
- * ```
436
- */
437
- export declare class TemplateManager implements TemplateManagerInterface {
438
- #private;
439
- constructor(options?: TemplateManagerOptions);
440
- get emitter(): EmitterInterface<TemplateManagerEventMap>;
441
- get size(): number;
442
- /**
443
- * Register a template a constructed {@link TemplateInterface} (kept
444
- * as-is) or a plain {@link TemplateOptions} bag (constructed into a
445
- * `Template` with this manager's `missing` / `locale` defaults applied
446
- * wherever the bag omits them).
447
- *
448
- * @param template - The template instance or options to register
449
- * @param options - `replace` overwrite an existing entry sharing the same id instead of throwing
450
- * @returns The registered {@link TemplateInterface}
451
- *
452
- * @example
453
- * ```ts
454
- * const instance = manager.register({ id: 'greeting', name: 'greeting', content: 'Hi {{name}}' })
455
- * ```
456
- */
457
- register(template: TemplateInterface | TemplateOptions, options?: {
458
- readonly replace?: boolean;
459
- }): TemplateInterface;
460
- /**
461
- * Look up a registered template by id.
462
- *
463
- * @param id - The template id
464
- * @returns The registered {@link TemplateInterface}
465
- * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown
466
- */
467
- template(id: string): TemplateInterface;
468
- /**
469
- * List every registered template.
470
- *
471
- * @returns A snapshot array of every registered {@link TemplateInterface}
472
- */
473
- templates(): readonly TemplateInterface[];
474
- /**
475
- * Filter registered templates by name / category / tag — every supplied
476
- * field must match (logical AND).
477
- *
478
- * @param query - The {@link TemplateQuery} to filter by; omit for every registered template
479
- * @returns The matching templates
480
- */
481
- find(query?: TemplateQuery): readonly TemplateInterface[];
482
- /**
483
- * Test whether a template id is registered.
484
- *
485
- * @param id - The template id
486
- * @returns `true` when `id` is registered
487
- */
488
- has(id: string): boolean;
489
- /**
490
- * Remove one, several, or every registered template (AGENTS §9.2 batch
491
- * overloads) array overload declared first so a list resolves to the
492
- * batch form.
493
- *
494
- * @remarks
495
- * `remove()` removes every registered template, emitting `remove` once per
496
- * instance. `remove(id)` removes one template by id, emitting `remove` and
497
- * returning `true` when it existed, `false` otherwise. `remove(ids)` is
498
- * ALL-OR-NOTHING: if any id in the list is unregistered, the collection is
499
- * left untouched and `false` is returned; otherwise every listed template
500
- * is removed (each emitting `remove`) and `true` is returned.
501
- *
502
- * @param target - Omit to remove all, a single id, or a list of ids
503
- * @returns `boolean` for the single-id / list-of-ids forms; `void` for the remove-all form
504
- */
505
- remove(ids: readonly string[]): boolean;
506
- remove(id: string): boolean;
507
- remove(): void;
508
- /** Remove every registered template, emitting `clear`. */
509
- clear(): void;
510
- /**
511
- * Fill a registered template by id.
512
- *
513
- * @param id - The template id
514
- * @param values - The values tokens resolve against
515
- * @param options - Per-call overrides for the template's `missing` / `locale` defaults
516
- * @returns The substituted content
517
- * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown
518
- */
519
- fill(id: string, values?: TemplateFillValues, options?: TemplateFillOptions): string;
520
- /**
521
- * Validate values against a registered template by id.
95
+ /**
96
+ * Substitutes every `{{name}}` token in `content` in a single pass.
97
+ *
98
+ * @remarks
99
+ * Uses a fresh `RegExp` clone of `FILL_PATTERN` per call (never sharing its
100
+ * `lastIndex`) and a single `String#replace` scan — substituted output is
101
+ * never re-scanned. Each token resolves through `resolveToken`, the one rule
102
+ * `Template#validate` also applies: the matching declared
103
+ * {@link TemplatePlaceholder} (exact `name`) supplies its `path` (falling
104
+ * back to the token split on `.`); any path segment in `UNSAFE_FIELD_SEGMENTS`
105
+ * makes the token unresolved without ever calling `resolveField` (a
106
+ * prototype-pollution guard). A resolved value formats through `formatValue`; an
107
+ * unresolved value falls back to the placeholder's `fallback` when declared;
108
+ * otherwise `options.missing` governs `'literal'` re-emits the original
109
+ * `{{name}}` text, `'empty'` emits `''`, and `'error'` emits `''` for every
110
+ * token but collects every unresolved required token (an undeclared token, or
111
+ * a declared token with `required !== false`) and throws one
112
+ * {@link TemplateError} coded `MISSING` listing them all, in first-appearance
113
+ * order, once the scan completes. An escaped `\{{` emits a literal `{{`.
114
+ *
115
+ * Called with no declared `placeholders` and `{ missing: 'empty' }`, this is a
116
+ * bare interpolation over `content` — every token resolves by dotted path
117
+ * against the values record and every unresolved token emits `''`.
118
+ * `FILL_PATTERN`'s token class (`[^{}]`) excludes `{`, so a token containing
119
+ * `{` never matches and the surrounding `{{` stays literal.
120
+ *
121
+ * @param content - The template content carrying `{{name}}` tokens
122
+ * @param values - The values tokens resolve against
123
+ * @param options - `missing` (default `'error'`), `locale` (default `'en-US'`), and the declared `placeholders` (default none) tokens resolve against
124
+ * @returns The substituted content
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * import { fillTemplate } from '@src/core'
129
+ *
130
+ * fillTemplate('Hi {{name}}', { name: 'Ada' }) // 'Hi Ada'
131
+ * fillTemplate('Limit {{limit}}', { limit: 5010 }, { missing: 'empty' }) // 'Limit 5,010'
132
+ * ```
133
+ */
134
+ export declare function fillTemplate(content: string, values?: TemplateFillValues, options?: TemplateFillContext): string;
135
+
136
+ /**
137
+ * Formats a resolved fill value for substitution into a template's `content`.
138
+ *
139
+ * @remarks
140
+ * A finite number renders with the given locale's thousand grouping (through
141
+ * `toLocaleString`); every other value — including `null` — String-coerces.
142
+ * `null` therefore renders as the literal string `'null'`, matching
143
+ * `String(value)` exactly, so a resolved `null` is visible in the output
144
+ * rather than silently empty. An
145
+ * invalid BCP-47 `locale` tag throws a `RangeError` from the underlying
146
+ * `toLocaleString` call when `value` is a finite number this is a caller
147
+ * error (an invalid locale argument), by design, and is not caught here.
148
+ *
149
+ * @param value - The resolved value to format
150
+ * @param locale - The locale used for finite-number formatting
151
+ * @returns The formatted string
152
+ *
153
+ * @example
154
+ * ```ts
155
+ * import { formatValue } from '@src/core'
156
+ *
157
+ * formatValue(5010, 'en-US') // '5,010'
158
+ * formatValue(null, 'en-US') // 'null'
159
+ * ```
160
+ */
161
+ export declare function formatValue(value: unknown, locale: string): string;
162
+
163
+ /**
164
+ * Narrows an unknown caught value to a {@link TemplateError}.
165
+ *
166
+ * @param value - The value to test (typically a `catch` binding)
167
+ * @returns True if `value` is a {@link TemplateError}; false otherwise
168
+ *
169
+ * @example
170
+ * ```ts
171
+ * import { isTemplateError } from '@src/core'
172
+ *
173
+ * try {
174
+ * manager.fill('missing')
175
+ * } catch (error) {
176
+ * if (isTemplateError(error) && error.code === 'NOTFOUND') return
177
+ * }
178
+ * ```
179
+ */
180
+ export declare function isTemplateError(value: unknown): value is TemplateError;
181
+
182
+ /**
183
+ * Names how {@link TemplateInterface#fill} handles an unresolved required
184
+ * placeholder.
185
+ *
186
+ * @remarks
187
+ * `error` throws a {@link TemplateError} coded `MISSING`. `empty`
188
+ * substitutes an empty string. `literal` substitutes the placeholder's own
189
+ * `{{name}}` token back into the output, unchanged.
190
+ */
191
+ export declare type MissingPolicy = 'error' | 'empty' | 'literal';
192
+
193
+ /**
194
+ * Builds the `@orkestrel/contract` object shape describing a template's
195
+ * declared placeholders.
196
+ *
197
+ * @remarks
198
+ * Each placeholder becomes a `stringShape` carrying its `description`;
199
+ * `required === false` wraps it in `optionalShape`. Used by `Template` to
200
+ * compile its `parameters()` contract once per instance.
201
+ *
202
+ * @param placeholders - The declared placeholders to shape
203
+ * @returns The contract shape for `createContract`
204
+ *
205
+ * @example
206
+ * ```ts
207
+ * import { placeholderShape } from '@src/core'
208
+ * import { createContract } from '@orkestrel/contract'
209
+ *
210
+ * const contract = createContract(placeholderShape([{ name: 'city' }]))
211
+ * ```
212
+ */
213
+ export declare function placeholderShape(placeholders: readonly TemplatePlaceholder[]): ContractShape;
214
+
215
+ /**
216
+ * Resolves a field path against a fill-values record, refusing any path that
217
+ * touches a prototype-pollution-unsafe segment.
218
+ *
219
+ * @remarks
220
+ * A prototype-pollution guard shared by `fillTemplate` and `Template#validate`
221
+ * so the two stay in lockstep: `path` normalizes to a segment array (a bare
222
+ * string `path` becomes a single-segment array); if any segment appears in
223
+ * `UNSAFE_FIELD_SEGMENTS` (`'__proto__'`, `'constructor'`, `'prototype'`), the
224
+ * lookup is refused and `undefined` is returned without ever calling
225
+ * `resolveField` — a path like `['__proto__', 'polluted']` can never reach
226
+ * the record's actual prototype chain through this function. Every other
227
+ * path resolves through `@orkestrel/contract`'s `resolveField`.
228
+ *
229
+ * @param record - The fill-values record to resolve against
230
+ * @param path - The field path — a single segment or a segment array
231
+ * @returns The resolved value, or `undefined` when unresolved or the path is unsafe
232
+ *
233
+ * @example
234
+ * ```ts
235
+ * import { resolveSafeField } from '@src/core'
236
+ *
237
+ * resolveSafeField({ a: { b: 1 } }, ['a', 'b']) // 1
238
+ * resolveSafeField({}, ['__proto__', 'polluted']) // undefined
239
+ * ```
240
+ */
241
+ export declare function resolveSafeField(record: TemplateFillValues, path: FieldPath): unknown;
242
+
243
+ /**
244
+ * Resolves one `{{name}}` token against the declared placeholders and the
245
+ * fill-values record.
246
+ *
247
+ * @remarks
248
+ * The single implementation of the token rule `fillTemplate` and
249
+ * `Template#validate` both apply, so the two can never drift: the declared
250
+ * {@link TemplatePlaceholder} sharing the token's `name` (exact match)
251
+ * supplies its `path`, falling back to the token split on `.`; the value
252
+ * resolves through `resolveSafeField`, so any segment in
253
+ * `UNSAFE_FIELD_SEGMENTS` yields `undefined` without ever calling
254
+ * `resolveField`; `required` is `true` for an undeclared token and for a
255
+ * declared placeholder whose `required` is not `false`. The token is passed
256
+ * already trimmed. `fallback` is not applied here — it is read from
257
+ * `declared` by each caller, because `fill` substitutes it and `validate`
258
+ * only counts it.
259
+ *
260
+ * @param record - The fill-values record the token resolves against
261
+ * @param placeholders - The declared placeholders the token matches by name
262
+ * @param token - The trimmed token text, without its `{{` / `}}` delimiters
263
+ * @returns The {@link TemplateTokenResolution} for the token
264
+ *
265
+ * @example
266
+ * ```ts
267
+ * import { resolveToken } from '@src/core'
268
+ *
269
+ * resolveToken({ name: 'Ada' }, [], 'name').value // 'Ada'
270
+ * resolveToken({}, [{ name: 'nickname', required: false }], 'nickname').required // false
271
+ * ```
272
+ */
273
+ export declare function resolveToken(record: TemplateFillValues, placeholders: readonly TemplatePlaceholder[], token: string): TemplateTokenResolution;
274
+
275
+ /**
276
+ * Represents a named, versionable template — `{{name}}` tokens in `content`,
277
+ * filled against a values record — implementing `TemplateInterface` exactly.
278
+ *
279
+ * @remarks
280
+ * `missing` / `locale` seed this instance's default {@link TemplateFillOptions},
281
+ * overridable per `fill` call. Its `parameters()` contract (built from
282
+ * `placeholders` through `placeholderShape`) compiles once, in the constructor.
283
+ *
284
+ * @throws {@link TemplateError} Thrown when `options.placeholders` declares a duplicate `name` or an empty `path` (coded `INVALID`)
522
285
  *
523
- * @param id - The template id
524
- * @param values - The values to check
525
- * @returns The {@link TemplateValidationResult}
526
- * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown
527
- */
528
- validate(id: string, values?: TemplateFillValues): TemplateValidationResult;
286
+ * @example
287
+ * ```ts
288
+ * const greeting = new Template({ name: 'greeting', content: 'Hi {{name}}' })
289
+ * greeting.fill({ name: 'Ada' }) // 'Hi Ada'
290
+ * ```
291
+ */
292
+ export declare class Template implements TemplateInterface {
293
+ #private;
294
+ readonly id: string;
295
+ readonly name: string;
296
+ readonly content: string;
297
+ readonly placeholders: readonly TemplatePlaceholder[];
298
+ readonly summary?: string;
299
+ readonly description?: string;
300
+ readonly category?: string;
301
+ readonly tags?: readonly string[];
302
+ constructor(options: TemplateOptions);
529
303
  /**
530
- * Project a registered template's parameters by id.
304
+ * Returns the plain, JSON-serializable data this template carries.
305
+ *
306
+ * @returns The {@link TemplateDefinition} record
531
307
  *
532
- * @param id - The template id
533
- * @returns The compiled parameters record, or `undefined` when the template has none
534
- * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown
308
+ * @example
309
+ * ```ts
310
+ * const instance = new Template({ name: 'greeting', content: 'Hi {{name}}' })
311
+ * instance.definition().name // 'greeting'
312
+ * ```
313
+ */
314
+ definition(): TemplateDefinition;
315
+ /**
316
+ * Substitutes every `{{name}}` token in `content` against `values`.
317
+ *
318
+ * @param values - The values tokens resolve against
319
+ * @param options - Per-call overrides for this instance's `missing` / `locale` defaults
320
+ * @returns The substituted content
321
+ * @throws {@link TemplateError} Thrown when a required placeholder stays unresolved under the `'error'` policy (coded `MISSING`)
322
+ *
323
+ * @example
324
+ * ```ts
325
+ * const instance = new Template({ name: 'greeting', content: 'Hi {{name}}' })
326
+ * instance.fill({ name: 'Ada' }) // 'Hi Ada'
327
+ * ```
535
328
  */
536
- parameters(id: string): Readonly<Record<string, unknown>> | undefined;
329
+ fill(values?: TemplateFillValues, options?: TemplateFillOptions): string;
330
+ /**
331
+ * Reports which required placeholders would stay unresolved, and which
332
+ * `values` keys go unused, without producing output.
333
+ *
334
+ * @remarks
335
+ * Content-token driven: scans `this.content` for every `{{name}}` token
336
+ * (skipping escaped `\{{` matches) the same way `fill` does, so `validate`
337
+ * predicts `fill`'s `'error'`-{@link MissingPolicy} outcome exactly — a
338
+ * token reported here as missing is precisely a token that would throw
339
+ * under `fill(values, { missing: 'error' })`. For each distinct token
340
+ * (first-appearance order, trimmed): `resolveToken` applies the one shared
341
+ * token rule `fill` also applies — a declared {@link TemplatePlaceholder}
342
+ * sharing its `name` supplies `path` (falling back to the token split on
343
+ * `.`), and the value resolves through `resolveSafeField`. The token is `missing`
344
+ * only when the value is unresolved, no `fallback` is declared, and the
345
+ * placeholder is required (`required !== false`, including undeclared
346
+ * tokens). `extra` lists every `values` key with no declared placeholder.
347
+ *
348
+ * @param values - The values to check
349
+ * @returns The {@link TemplateValidationResult}
350
+ *
351
+ * @example
352
+ * ```ts
353
+ * const instance = new Template({
354
+ * name: 'greeting',
355
+ * content: 'Hi {{name}}',
356
+ * placeholders: [{ name: 'name' }],
357
+ * })
358
+ * instance.validate({}).missing // ['name']
359
+ * ```
360
+ */
361
+ validate(values?: TemplateFillValues): TemplateValidationResult;
362
+ /**
363
+ * Projects this template's placeholders to the open tool-parameters record
364
+ * shape.
365
+ *
366
+ * @returns The compiled parameters record, or `undefined` when `schemaToParameters` yields none
367
+ *
368
+ * @example
369
+ * ```ts
370
+ * const instance = new Template({
371
+ * name: 'greeting',
372
+ * content: 'Hi {{name}}',
373
+ * placeholders: [{ name: 'name' }],
374
+ * })
375
+ * instance.parameters()
376
+ * ```
377
+ */
378
+ parameters(): Readonly<Record<string, unknown>> | undefined;
537
379
  }
538
380
 
539
381
  /**
540
- * The push observation surface of a {@link TemplateManagerInterface} (AGENTS
541
- * §13) — an id-keyed registry, so `register` / `remove` are the events
542
- * (never ordered-list `append`/`prepend`).
382
+ * Represents a named, versionable template record pure data, no behavior.
383
+ *
384
+ * @remarks
385
+ * `content` is the raw string carrying `{{name}}` tokens (see
386
+ * `FILL_PATTERN` in `constants.ts`); `placeholders` declares every token's
387
+ * lookup rule. `category` and `tags` are the fields
388
+ * `TemplateManagerInterface#find` filters on, alongside `name`. `summary` and
389
+ * `description` are catalog metadata that `definition()` carries and no query
390
+ * reads.
543
391
  */
544
- export declare type TemplateManagerEventMap = {
545
- /** A template was registered — carries the registered template. */
546
- readonly register: readonly [template: TemplateInterface];
547
- /** A template was removed — carries the removed template. */
548
- readonly remove: readonly [template: TemplateInterface];
549
- /** The registry was cleared. */
550
- readonly clear: readonly [];
551
- };
392
+ export declare interface TemplateDefinition {
393
+ readonly id: string;
394
+ readonly name: string;
395
+ readonly content: string;
396
+ readonly placeholders: readonly TemplatePlaceholder[];
397
+ readonly summary?: string;
398
+ readonly description?: string;
399
+ readonly category?: string;
400
+ readonly tags?: readonly string[];
401
+ }
552
402
 
553
403
  /**
554
- * The template registry — a self-owning, id-keyed record-holder (AGENTS §9.1
555
- * singular/plural accessors, §9.2 batch overloads).
404
+ * Represents an error thrown by the template layer — a machine-readable
405
+ * {@link TemplateErrorCode} and an optional `context` record naming the
406
+ * offending id or placeholder name.
556
407
  *
557
408
  * @remarks
558
- * `register` accepts either a constructed {@link TemplateInterface} or a
559
- * plain {@link TemplateOptions} bag (constructed internally), and throws a
560
- * {@link TemplateError} coded `CONFLICT` when the id already exists unless
561
- * `options.replace` is `true`. `template` throws `NOTFOUND` for an unknown
562
- * id. `remove`'s batch form is all-or-nothing: any missing id in the list
563
- * leaves the collection untouched and returns `false`.
409
+ * Thrown for: a required placeholder staying unresolved under the `error`
410
+ * {@link MissingPolicy} (`MISSING`), an unknown template id
411
+ * (`NOTFOUND`), `createTemplate` handed invalid data (`INVALID`), and
412
+ * `TemplateManagerInterface#register` handed an id already present without
413
+ * `options.replace` (`CONFLICT`).
564
414
  */
565
- export declare interface TemplateManagerInterface {
566
- readonly emitter: EmitterInterface<TemplateManagerEventMap>;
567
- readonly size: number;
568
- register(template: TemplateInterface | TemplateOptions, options?: {
569
- readonly replace?: boolean;
570
- }): TemplateInterface;
571
- template(id: string): TemplateInterface;
572
- templates(): readonly TemplateInterface[];
573
- find(query?: TemplateQuery): readonly TemplateInterface[];
574
- has(id: string): boolean;
575
- remove(ids: readonly string[]): boolean;
576
- remove(id: string): boolean;
577
- remove(): void;
578
- clear(): void;
579
- fill(id: string, values?: TemplateFillValues, options?: TemplateFillOptions): string;
580
- validate(id: string, values?: TemplateFillValues): TemplateValidationResult;
581
- parameters(id: string): Readonly<Record<string, unknown>> | undefined;
415
+ export declare class TemplateError extends Error {
416
+ readonly code: TemplateErrorCode;
417
+ readonly context?: Readonly<Record<string, unknown>>;
418
+ constructor(code: TemplateErrorCode, message: string, context?: Readonly<Record<string, unknown>>);
582
419
  }
583
420
 
584
421
  /**
585
- * Options for `createTemplateManager` / the `TemplateManager` constructor.
422
+ * Names the coded misuse / failure conditions thrown as a {@link TemplateError}.
423
+ *
424
+ * @remarks
425
+ * `MISSING` — a required placeholder stayed unresolved under the `error`
426
+ * {@link MissingPolicy}. `NOTFOUND` — `TemplateManagerInterface#fill`,
427
+ * `#validate`, or `#parameters` was handed an unknown id.
428
+ * `INVALID` — `createTemplate` was handed data that fails validation.
429
+ * `CONFLICT` — `register` was handed an id already present without
430
+ * `options.replace`.
431
+ */
432
+ export declare type TemplateErrorCode = 'MISSING' | 'NOTFOUND' | 'INVALID' | 'CONFLICT';
433
+
434
+ /**
435
+ * Carries the full option bag `fillTemplate` takes — the per-call
436
+ * {@link TemplateFillOptions} plus the declared placeholders tokens resolve
437
+ * against.
586
438
  *
587
439
  * @remarks
588
- * `templates` seeds the registry either constructed {@link TemplateInterface}
589
- * instances or plain {@link TemplateOptions} bags. `missing` / `locale` are
590
- * the manager-wide default {@link TemplateFillOptions}, overridable per-call.
591
- * `on` — initial event listeners (AGENTS §8/§13). `error` — the emitter's
592
- * listener-error handler.
440
+ * `Template#fill` supplies `placeholders` from its own declaration; a direct
441
+ * `fillTemplate` caller supplies them per call, and omitting them fills
442
+ * against undeclared tokens alone.
593
443
  */
594
- export declare interface TemplateManagerOptions {
595
- readonly templates?: ReadonlyArray<TemplateInterface | TemplateOptions>;
444
+ export declare interface TemplateFillContext extends TemplateFillOptions {
445
+ readonly placeholders?: readonly TemplatePlaceholder[];
446
+ }
447
+
448
+ /** Carries the per-call options for `TemplateInterface#fill` / `TemplateManagerInterface#fill`. */
449
+ export declare interface TemplateFillOptions {
596
450
  readonly missing?: MissingPolicy;
597
451
  readonly locale?: string;
598
- readonly on?: EmitterHooks<TemplateManagerEventMap>;
599
- readonly error?: EmitterErrorHandler;
600
452
  }
601
453
 
454
+ /** Represents the values a {@link TemplateInterface#fill} / `#validate` call resolves placeholders against. */
455
+ export declare type TemplateFillValues = Readonly<Record<string, unknown>>;
456
+
602
457
  /**
603
- * Options for `createTemplate` / the `Template` constructor.
458
+ * Declares the template contract a consumer holds — the readonly template
459
+ * record and the `definition`, `fill`, `validate`, and `parameters` calls
460
+ * over it.
604
461
  *
605
462
  * @remarks
606
- * `id` defaults to a generated id when omitted. `placeholders` defaults to
607
- * an empty list. `missing` / `locale` seed the instance's default
608
- * {@link TemplateFillOptions}, overridable per-call.
463
+ * `Template` implements this contract exactly, so the class exposes these
464
+ * members and no other public behavior.
609
465
  */
610
- export declare interface TemplateOptions {
611
- readonly id?: string;
466
+ export declare interface TemplateInterface {
467
+ readonly id: string;
612
468
  readonly name: string;
613
469
  readonly content: string;
614
- readonly placeholders?: readonly TemplatePlaceholder[];
470
+ readonly placeholders: readonly TemplatePlaceholder[];
615
471
  readonly summary?: string;
616
472
  readonly description?: string;
617
473
  readonly category?: string;
618
474
  readonly tags?: readonly string[];
619
- readonly missing?: MissingPolicy;
620
- readonly locale?: string;
475
+ /** Returns the plain, JSON-serializable template data. */
476
+ definition(): TemplateDefinition;
477
+ /**
478
+ * Substitutes every `{{name}}` token in `content` against `values`.
479
+ *
480
+ * @remarks
481
+ * `options.missing` governs an unresolved required placeholder, defaulting
482
+ * to the instance's own {@link MissingPolicy}.
483
+ */
484
+ fill(values?: TemplateFillValues, options?: TemplateFillOptions): string;
485
+ /**
486
+ * Reports which required placeholders would stay unresolved, and which
487
+ * `values` keys go unused.
488
+ *
489
+ * @remarks
490
+ * The unresolved names arrive as `missing` and the unused keys as `extra`,
491
+ * and neither reading produces output.
492
+ */
493
+ validate(values?: TemplateFillValues): TemplateValidationResult;
494
+ /**
495
+ * Projects the declared placeholders to the open tool-parameters record
496
+ * shape.
497
+ *
498
+ * @remarks
499
+ * The shape is `schemaToParameters`'s return type from
500
+ * `@orkestrel/contract`.
501
+ */
502
+ parameters(): Readonly<Record<string, unknown>> | undefined;
621
503
  }
622
504
 
623
505
  /**
624
- * One placeholder a {@link TemplateDefinition}'s `content` declares its
625
- * lookup name, an optional field path into the values record, whether it is
626
- * required, and a literal fallback.
506
+ * Represents the template registry a self-owning, id-keyed record-holder for
507
+ * the {@link TemplateInterface} instances a consumer registers, looks up,
508
+ * fills, and validates by id — implementing `TemplateManagerInterface`
509
+ * exactly.
627
510
  *
628
511
  * @remarks
629
- * `name` is the `{{name}}` token as written in `content`. `path`, when
630
- * present, resolves the fill value through a (possibly nested)
631
- * {@link FieldPath} rather than a flat `name` lookup on the values record.
632
- * `required` defaults to `true` when omitted an unresolved required
633
- * placeholder is governed by the active {@link MissingPolicy}. `fallback` is
634
- * a literal substituted when the value is unresolved, regardless of
635
- * `required`.
512
+ * Singular and plural accessors, the batch `remove` overloads, and ownership
513
+ * of the emitter all sit here. `register` accepts either a constructed {@link TemplateInterface} (kept
514
+ * as-is, including its own `missing` / `locale` defaults) or a plain
515
+ * {@link TemplateOptions} bag constructed into a `Template` with this
516
+ * manager's `missing` / `locale` defaults applied wherever the bag omits
517
+ * them. A duplicate `id` throws a {@link TemplateError} coded `CONFLICT`
518
+ * unless `options.replace` is `true`, in which case the existing entry is
519
+ * overwritten. `options.templates` seeds the registry at construction without
520
+ * emitting `register` — only calls to `register` after construction emit.
521
+ * The batch `remove(ids)` form removes every present id and returns
522
+ * `true` only when every listed id was present.
523
+ *
524
+ * @example
525
+ * ```ts
526
+ * import { TemplateManager } from '@src/core'
527
+ *
528
+ * const manager = new TemplateManager()
529
+ * const instance = manager.register({ name: 'greeting', content: 'Hi {{name}}' })
530
+ * manager.fill(instance.id, { name: 'Ada' }) // 'Hi Ada'
531
+ * ```
636
532
  */
637
- export declare interface TemplatePlaceholder {
638
- readonly name: string;
639
- readonly path?: FieldPath;
640
- readonly required?: boolean;
641
- readonly fallback?: unknown;
642
- readonly description?: string;
643
- }
533
+ export declare class TemplateManager implements TemplateManagerInterface {
534
+ #private;
535
+ constructor(options?: TemplateManagerOptions);
536
+ get emitter(): EmitterInterface<TemplateManagerEventMap>;
537
+ get count(): number;
538
+ /**
539
+ * Registers a template — a constructed {@link TemplateInterface} (kept
540
+ * as-is) or a plain {@link TemplateOptions} bag (constructed into a
541
+ * `Template` with this manager's `missing` / `locale` defaults applied
542
+ * wherever the bag omits them).
543
+ *
544
+ * @param template - The template instance or options to register
545
+ * @param options - `replace` — overwrite an existing entry sharing the same id instead of throwing
546
+ * @returns The registered {@link TemplateInterface}
547
+ * @throws {@link TemplateError} Thrown when the id is already registered and `options.replace` is not `true` (coded `CONFLICT`), or when an options bag declares a duplicate placeholder `name` or an empty `path` (coded `INVALID`)
548
+ *
549
+ * @example
550
+ * ```ts
551
+ * const instance = manager.register({ id: 'greeting', name: 'greeting', content: 'Hi {{name}}' })
552
+ * ```
553
+ */
554
+ register(template: TemplateInterface | TemplateOptions, options?: TemplateRegisterOptions): TemplateInterface;
555
+ /**
556
+ * Returns one registered {@link TemplateInterface} by id.
557
+ *
558
+ * @param id - The template id
559
+ * @returns The registered {@link TemplateInterface}, or `undefined` when `id` is unregistered
560
+ */
561
+ template(id: string): TemplateInterface | undefined;
562
+ /**
563
+ * Lists every registered template.
564
+ *
565
+ * @returns A snapshot array of every registered {@link TemplateInterface}
566
+ */
567
+ templates(): readonly TemplateInterface[];
568
+ /**
569
+ * Filters registered templates by `name`, `category`, and `tag` — every
570
+ * supplied field must match.
571
+ *
572
+ * @param query - The {@link TemplateQuery} to filter by; omit for every registered template
573
+ * @returns The matching templates
574
+ */
575
+ find(query?: TemplateQuery): readonly TemplateInterface[];
576
+ /**
577
+ * Tests whether a template id is registered.
578
+ *
579
+ * @param id - The template id
580
+ * @returns True if `id` is registered; false otherwise
581
+ */
582
+ has(id: string): boolean;
583
+ /**
584
+ * Removes one, several, or every registered template.
585
+ *
586
+ * @remarks
587
+ * `remove()` removes every registered template, emitting `remove` once per
588
+ * instance. `remove(id)` removes one template by id, emitting `remove` and
589
+ * returning `true` when it existed, `false` otherwise. `remove(ids)`
590
+ * removes every listed id that is present, emitting `remove` once per
591
+ * removed instance, and returns `true` only when every listed id was
592
+ * present.
593
+ *
594
+ * @param target - Omit to remove all, a single id, or a list of ids
595
+ * @returns `boolean` for the single-id / list-of-ids forms; `void` for the remove-all form
596
+ */
597
+ remove(ids: readonly string[]): boolean;
598
+ remove(id: string): boolean;
599
+ remove(): void;
600
+ /** Removes every registered template, emitting `clear`. */
601
+ clear(): void;
602
+ /**
603
+ * Tears down the registry: drops every registered template and destroys the
604
+ * owned emitter. Idempotent.
605
+ *
606
+ * @remarks
607
+ * Teardown is not an observable registry operation and the emitter is being
608
+ * released, so this emits neither `clear` nor `remove`. The emitter is torn
609
+ * down last, after the registry is dropped.
610
+ *
611
+ * @example
612
+ * ```ts
613
+ * const manager = new TemplateManager()
614
+ * manager.destroy()
615
+ * manager.emitter.destroyed // true
616
+ * ```
617
+ */
618
+ destroy(): void;
619
+ /**
620
+ * Fills a registered template by id.
621
+ *
622
+ * @param id - The template id
623
+ * @param values - The values tokens resolve against
624
+ * @param options - Per-call overrides for the template's `missing` / `locale` defaults
625
+ * @returns The substituted content
626
+ * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown
627
+ * @throws {@link TemplateError} Thrown when a required placeholder stays unresolved under the `'error'` policy (coded `MISSING`)
628
+ */
629
+ fill(id: string, values?: TemplateFillValues, options?: TemplateFillOptions): string;
630
+ /**
631
+ * Validates values against a registered template by id.
632
+ *
633
+ * @param id - The template id
634
+ * @param values - The values to check
635
+ * @returns The {@link TemplateValidationResult}
636
+ * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown
637
+ */
638
+ validate(id: string, values?: TemplateFillValues): TemplateValidationResult;
639
+ /**
640
+ * Projects a registered template's parameters by id.
641
+ *
642
+ * @param id - The template id
643
+ * @returns The compiled parameters record, or `undefined` when the template has none
644
+ * @throws {@link TemplateError} coded `NOTFOUND` when `id` is unknown
645
+ */
646
+ parameters(id: string): Readonly<Record<string, unknown>> | undefined;
647
+ }
644
648
 
645
- /** A query for `TemplateManagerInterface#find` — every supplied field must match. */
646
- export declare interface TemplateQuery {
647
- readonly name?: string;
648
- readonly category?: string;
649
- readonly tag?: string;
650
- }
649
+ /**
650
+ * Declares the push observation surface of a {@link TemplateManagerInterface}
651
+ * an id-keyed registry, so `register` / `remove` are the events (never
652
+ * ordered-list `append`/`prepend`).
653
+ */
654
+ export declare type TemplateManagerEventMap = {
655
+ /** Fires when a template is registered — carries the registered template. */
656
+ readonly register: readonly [template: TemplateInterface];
657
+ /** Fires when a template is removed — carries the removed template. */
658
+ readonly remove: readonly [template: TemplateInterface];
659
+ /** Fires when the registry is cleared. */
660
+ readonly clear: readonly [];
661
+ };
651
662
 
652
- /** The outcome of `TemplateInterface#validate` — which required placeholders are unresolved, and which supplied values are unused. */
653
- export declare interface TemplateValidationResult {
654
- readonly valid: boolean;
655
- readonly missing: readonly string[];
656
- readonly extra: readonly string[];
657
- }
663
+ /**
664
+ * Declares the registry contract a consumer holds — a self-owning, id-keyed
665
+ * record-holder with singular and plural accessors over the templates it
666
+ * registers.
667
+ *
668
+ * @remarks
669
+ * `TemplateManager` implements this contract exactly, so the class exposes
670
+ * these members and no other public behavior. The by-id calls split on what
671
+ * an unknown id means to each: `template` reports its absence as `undefined`,
672
+ * while `fill`, `validate`, and `parameters` throw a {@link TemplateError}
673
+ * coded `NOTFOUND`, because each needs a template to proceed.
674
+ */
675
+ export declare interface TemplateManagerInterface {
676
+ readonly emitter: EmitterInterface<TemplateManagerEventMap>;
677
+ readonly count: number;
678
+ /**
679
+ * Registers one template, or overwrites the entry sharing its id when
680
+ * `options.replace` is `true`, and emits `register`.
681
+ *
682
+ * @remarks
683
+ * The template arrives as a constructed {@link TemplateInterface} or as a
684
+ * plain {@link TemplateOptions} bag, which is constructed internally. A
685
+ * duplicate id without `options.replace` throws a {@link TemplateError}
686
+ * coded `CONFLICT`.
687
+ */
688
+ register(template: TemplateInterface | TemplateOptions, options?: TemplateRegisterOptions): TemplateInterface;
689
+ /** Returns one registered template by id, or `undefined` when the id is unregistered. */
690
+ template(id: string): TemplateInterface | undefined;
691
+ /** Lists every registered template. */
692
+ templates(): readonly TemplateInterface[];
693
+ /** Filters registered templates by `name`, `category`, and `tag` — every supplied field must match. */
694
+ find(query?: TemplateQuery): readonly TemplateInterface[];
695
+ /** Reports whether a template with the given id is registered. */
696
+ has(id: string): boolean;
697
+ /**
698
+ * Removes the listed templates by id, one template by id, or every
699
+ * registered template, emitting `remove` once per removed template.
700
+ *
701
+ * @remarks
702
+ * The batch form removes every present id and reports `true` only when
703
+ * every listed id was present, so one absent id turns the answer `false`
704
+ * while the present ids still remove. The no-argument form removes every
705
+ * registered template and reports nothing.
706
+ */
707
+ remove(ids: readonly string[]): boolean;
708
+ remove(id: string): boolean;
709
+ remove(): void;
710
+ /** Removes every registered template, emitting `clear`. */
711
+ clear(): void;
712
+ /**
713
+ * Tears the registry down: drops every registered template and destroys the
714
+ * owned emitter. Idempotent.
715
+ *
716
+ * @remarks
717
+ * Teardown emits nothing, because the emitter is being released.
718
+ */
719
+ destroy(): void;
720
+ /** Fills a registered template by id. */
721
+ fill(id: string, values?: TemplateFillValues, options?: TemplateFillOptions): string;
722
+ /** Validates values against a registered template by id. */
723
+ validate(id: string, values?: TemplateFillValues): TemplateValidationResult;
724
+ /** Projects a registered template's parameters by id. */
725
+ parameters(id: string): Readonly<Record<string, unknown>> | undefined;
726
+ }
658
727
 
659
- /**
660
- * Prototype-pollution-unsafe field-path segments a fill lookup refuses to
661
- * resolve ANY path containing one, treating the placeholder as unresolved.
662
- */
663
- export declare const UNSAFE_FIELD_SEGMENTS: readonly string[];
728
+ /**
729
+ * Carries the options for `createTemplateManager` / the `TemplateManager`
730
+ * constructor.
731
+ *
732
+ * @remarks
733
+ * `templates` seeds the registry — either constructed {@link TemplateInterface}
734
+ * instances or plain {@link TemplateOptions} bags. `missing` / `locale` are
735
+ * the manager-wide default {@link TemplateFillOptions}, overridable per-call.
736
+ * `on` — initial event listeners. `error` — the emitter's
737
+ * listener-error handler.
738
+ */
739
+ export declare interface TemplateManagerOptions {
740
+ readonly templates?: ReadonlyArray<TemplateInterface | TemplateOptions>;
741
+ readonly missing?: MissingPolicy;
742
+ readonly locale?: string;
743
+ readonly on?: EmitterHooks<TemplateManagerEventMap>;
744
+ readonly error?: EmitterErrorHandler;
745
+ }
746
+
747
+ /**
748
+ * Carries the options for `createTemplate` / the `Template` constructor.
749
+ *
750
+ * @remarks
751
+ * `id` defaults to a generated id when omitted. `placeholders` defaults to
752
+ * an empty list. `missing` / `locale` seed the instance's default
753
+ * {@link TemplateFillOptions}, overridable per-call.
754
+ */
755
+ export declare interface TemplateOptions {
756
+ readonly id?: string;
757
+ readonly name: string;
758
+ readonly content: string;
759
+ readonly placeholders?: readonly TemplatePlaceholder[];
760
+ readonly summary?: string;
761
+ readonly description?: string;
762
+ readonly category?: string;
763
+ readonly tags?: readonly string[];
764
+ readonly missing?: MissingPolicy;
765
+ readonly locale?: string;
766
+ }
767
+
768
+ /**
769
+ * Represents one placeholder a {@link TemplateDefinition}'s `content`
770
+ * declares — its lookup name, an optional field path into the values record,
771
+ * whether it is required, and a literal fallback.
772
+ *
773
+ * @remarks
774
+ * `name` is the `{{name}}` token as written in `content`. `path`, when
775
+ * present, resolves the fill value through a (possibly nested)
776
+ * {@link FieldPath} rather than a flat `name` lookup on the values record.
777
+ * `required` defaults to `true` when omitted — an unresolved required
778
+ * placeholder is governed by the active {@link MissingPolicy}. `fallback` is
779
+ * a literal substituted when the value is unresolved, regardless of
780
+ * `required`.
781
+ */
782
+ export declare interface TemplatePlaceholder {
783
+ readonly name: string;
784
+ readonly path?: FieldPath;
785
+ readonly required?: boolean;
786
+ readonly fallback?: unknown;
787
+ readonly description?: string;
788
+ }
789
+
790
+ /** Represents a query for `TemplateManagerInterface#find` — every supplied field must match. */
791
+ export declare interface TemplateQuery {
792
+ readonly name?: string;
793
+ readonly category?: string;
794
+ readonly tag?: string;
795
+ }
796
+
797
+ /**
798
+ * Carries the options for `TemplateManagerInterface#register`.
799
+ *
800
+ * @remarks
801
+ * `replace` overwrites an existing entry sharing the registered id instead of
802
+ * throwing a {@link TemplateError} coded `CONFLICT`.
803
+ */
804
+ export declare interface TemplateRegisterOptions {
805
+ readonly replace?: boolean;
806
+ }
807
+
808
+ /**
809
+ * Represents one `{{name}}` token's resolution — the single token rule
810
+ * `fillTemplate` and `TemplateInterface#validate` share.
811
+ *
812
+ * @remarks
813
+ * `value` is the resolved fill value, `undefined` when the path is
814
+ * unresolved or refused by the prototype-pollution guard. `declared` is the
815
+ * matching {@link TemplatePlaceholder}, `undefined` for an undeclared token.
816
+ * `required` is `true` for an undeclared token and for a declared
817
+ * placeholder whose `required` is not `false`. A declared `fallback` is left
818
+ * on `declared` rather than applied here, because `fill` substitutes it and
819
+ * `validate` only counts it.
820
+ */
821
+ export declare interface TemplateTokenResolution {
822
+ readonly value: unknown;
823
+ readonly declared: TemplatePlaceholder | undefined;
824
+ readonly required: boolean;
825
+ }
826
+
827
+ /** Reports the outcome of `TemplateInterface#validate` — which required placeholders are unresolved, and which supplied values are unused. */
828
+ export declare interface TemplateValidationResult {
829
+ readonly valid: boolean;
830
+ readonly missing: readonly string[];
831
+ readonly extra: readonly string[];
832
+ }
833
+
834
+ /**
835
+ * Lists the prototype-pollution-unsafe field-path segments `'__proto__'`,
836
+ * `'constructor'`, and `'prototype'` — a fill lookup refuses to resolve a path
837
+ * containing one of them, treating the placeholder as unresolved.
838
+ */
839
+ export declare const UNSAFE_FIELD_SEGMENTS: readonly string[];
664
840
 
665
- export { }
841
+ export { }