@orkestrel/template 0.0.4 → 0.0.6

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