@orkestrel/interpret 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2705 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _orkestrel_contract = require("@orkestrel/contract");
3
+ let _orkestrel_reason = require("@orkestrel/reason");
4
+ let _orkestrel_emitter = require("@orkestrel/emitter");
5
+ //#region src/core/constants.ts
6
+ /**
7
+ * Default `similarity` for `createInterpret` / `matchAlias` — the fuzzy
8
+ * alias-match score threshold (0..1).
9
+ *
10
+ * @remarks
11
+ * Domain-qualified (not a bare `DEFAULT_SIMILARITY`) so the name stays free
12
+ * of collision on the shared `@src/core` barrel, mirroring the
13
+ * `DEFAULT_REASON_BAIL` precedent.
14
+ */
15
+ var DEFAULT_INTERPRET_SIMILARITY = .8;
16
+ /**
17
+ * Default `floor` for `createInterpret` / `matchTemplate` — the minimum
18
+ * intent confidence a template match (or the classified intent itself) must
19
+ * clear.
20
+ */
21
+ var DEFAULT_INTERPRET_FLOOR = .3;
22
+ /** Default `history` cap for an `InterpretContext`'s `previous()` ring buffer. */
23
+ var DEFAULT_INTERPRET_HISTORY = 16;
24
+ /** Default `id` for an `Interpret` orchestrator. */
25
+ var INTERPRET_ID = "interpret";
26
+ /** Confidence assigned to an exact keyword-proximity entity match. */
27
+ var CONFIDENCE_EXACT = 1;
28
+ /** Confidence assigned to an exact alias-phrase entity match. */
29
+ var CONFIDENCE_ALIAS = .9;
30
+ /** Confidence assigned when a single entity mapping collects every extracted number. */
31
+ var CONFIDENCE_COLLECT = .9;
32
+ /** Confidence assigned to a positional (order-based) entity match fallback. */
33
+ var CONFIDENCE_POSITIONAL = .7;
34
+ /** Confidence assigned to a same-domain carried-over field. */
35
+ var CONFIDENCE_CARRIED = .7;
36
+ /** Confidence assigned to a template default fill. */
37
+ var CONFIDENCE_DEFAULT = 1;
38
+ /** Confidence assigned to a successfully resolved computed field. */
39
+ var CONFIDENCE_COMPUTED = .9;
40
+ /**
41
+ * The numeric-entity extraction pattern shared by `extractNumbers` and
42
+ * `assignEntities` — an optional leading `$`, thousands-comma-grouped digits,
43
+ * an optional decimal fraction, and an optional trailing `%`.
44
+ *
45
+ * @remarks
46
+ * Carries the global flag, so every call site builds a fresh `RegExp` from
47
+ * `.source` / `.flags` (mirrors the core-root `PLACEHOLDER_PATTERN` pattern)
48
+ * rather than sharing this instance's mutable `lastIndex` across scans.
49
+ */
50
+ var NUMBER_PATTERN = /(?:\$\s*)?(\d+(?:,\d{3})*(?:\.\d+)?)\s*%?/g;
51
+ /**
52
+ * Prototype-pollution-unsafe field-path segments — `setField` refuses to
53
+ * write ANY path containing one, returning its input unchanged.
54
+ */
55
+ var UNSAFE_FIELD_SEGMENTS = Object.freeze([
56
+ "__proto__",
57
+ "prototype",
58
+ "constructor"
59
+ ]);
60
+ /**
61
+ * Neutral built-in contraction expansions for `Normalizer` — small on
62
+ * purpose; callers merge their own map over this one.
63
+ */
64
+ var DEFAULT_CONTRACTIONS = Object.freeze({
65
+ "can't": "cannot",
66
+ "won't": "will not",
67
+ "it's": "it is",
68
+ "don't": "do not"
69
+ });
70
+ /** Neutral built-in abbreviation expansions for `Normalizer` — empty by default. */
71
+ var DEFAULT_ABBREVIATIONS = Object.freeze({});
72
+ /** Neutral built-in misspelling corrections for `Normalizer` — empty by default. */
73
+ var DEFAULT_CORRECTIONS = Object.freeze({});
74
+ /** Neutral built-in action-verb vocabulary for `Extractor#extract`'s intent classification — empty by default. */
75
+ var DEFAULT_ACTIONS = Object.freeze({});
76
+ /** Neutral built-in domain-keyword vocabulary for `Extractor#extract`'s intent classification — empty by default. */
77
+ var DEFAULT_DOMAINS = Object.freeze({});
78
+ /** Neutral built-in intent-verb phrasing for `Formatter#format` — empty by default. */
79
+ var DEFAULT_VERBS = Object.freeze({});
80
+ /**
81
+ * The neutral default `Lexicon` a `Narrator` merges caller data over.
82
+ *
83
+ * @remarks
84
+ * `phrases` and `labels` are empty — there is no built-in vocabulary or
85
+ * label overrides (AGENTS §21 mechanism-never-policy). `templates` carries
86
+ * the structural, display-neutral strings the reverse helpers formerly
87
+ * hardcoded, keyed by
88
+ * `{table}.{reasoning}` for the four reasons kinds, `result.quantitative.failed`
89
+ * for the quantitative-result failure suffix, and `subject.fields` /
90
+ * `subject.empty` for `describeSubject`. Every string is a plain
91
+ * `interpolateMessage` template — `{{name}}`-style placeholders resolved
92
+ * against the caller-supplied `values` record.
93
+ */
94
+ var DEFAULT_LEXICON = Object.freeze({
95
+ phrases: Object.freeze({}),
96
+ labels: Object.freeze({}),
97
+ templates: Object.freeze({
98
+ "definition.quantitative": "{{name}}: {{count}} factor group(s)",
99
+ "definition.logical": "{{name}}: {{count}} rule(s), strategy {{strategy}}",
100
+ "definition.symbolic": "{{name}}: solve {{count}} equation(s)",
101
+ "definition.inferential": "{{name}}: {{facts}} fact(s)/{{inferences}} inference(s), {{strategy}}",
102
+ "result.quantitative": "scored {{value}} across {{count}} group(s)",
103
+ "result.quantitative.failed": "; failed: {{errors}}",
104
+ "result.logical": "{{status}}: {{count}} rule(s)",
105
+ "result.symbolic": "solved {{solved}}",
106
+ "result.inferential": "derived {{count}} fact(s)",
107
+ "subject.fields": "with {{fields}}",
108
+ "subject.empty": "with no fields"
109
+ })
110
+ });
111
+ //#endregion
112
+ //#region src/core/errors.ts
113
+ /**
114
+ * An error thrown by the interprets layer.
115
+ *
116
+ * @remarks
117
+ * Thrown for: an injected stage implementation throwing during its phase
118
+ * (`NORMALIZE_FAILED` / `EXTRACT_FAILED` / `CLARIFY_FAILED` /
119
+ * `FORMAT_FAILED` / `GENERATE_FAILED`), `createTemplate` handed data that
120
+ * fails `isTemplate` (`INVALID_TEMPLATE`), and any use of a destroyed
121
+ * `Interpret` / manager / context (`DESTROYED`). `NO_TEMPLATE` and
122
+ * `LOW_CONFIDENCE` never throw — they surface as a visible incomplete
123
+ * {@link Interpretation} instead (never an arbitrary fallback template).
124
+ * `context`, when present, carries the offending stage / template id.
125
+ */
126
+ var InterpretError = class extends Error {
127
+ code;
128
+ context;
129
+ constructor(code, message, context) {
130
+ super(message);
131
+ this.name = "InterpretError";
132
+ this.code = code;
133
+ this.context = context;
134
+ }
135
+ };
136
+ /**
137
+ * Narrow an unknown caught value to an {@link InterpretError}.
138
+ *
139
+ * @param value - The value to test (typically a `catch` binding)
140
+ * @returns `true` when `value` is an {@link InterpretError}
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * import { isInterpretError } from '@src/core'
145
+ *
146
+ * try {
147
+ * interpret.template('missing')
148
+ * } catch (error) {
149
+ * if (isInterpretError(error) && error.code === 'DESTROYED') return
150
+ * }
151
+ * ```
152
+ */
153
+ function isInterpretError(value) {
154
+ return value instanceof InterpretError;
155
+ }
156
+ //#endregion
157
+ //#region src/core/validators.ts
158
+ /**
159
+ * Determine whether a value is an {@link EntityMapping} — a literal
160
+ * alias-phrase extraction rule pointing at a subject field.
161
+ *
162
+ * @param value - The value to test
163
+ * @returns `true` when `value` is a well-formed entity mapping
164
+ *
165
+ * @example
166
+ * ```ts
167
+ * import { isEntityMapping } from '@src/core'
168
+ *
169
+ * isEntityMapping({ entity: 'age', aliases: ['years old'], field: 'age' }) // true
170
+ * isEntityMapping({ entity: 'age', aliases: [/\d+/], field: 'age' }) // false — RegExp alias
171
+ * ```
172
+ */
173
+ function isEntityMapping(value) {
174
+ return (0, _orkestrel_contract.recordOf)({
175
+ entity: _orkestrel_contract.isString,
176
+ aliases: (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString),
177
+ field: _orkestrel_reason.isFieldPath,
178
+ required: _orkestrel_contract.isBoolean
179
+ }, ["required"])(value);
180
+ }
181
+ /**
182
+ * Determine whether a value is a {@link FieldDefault} — a fallback value a
183
+ * {@link Template} fills onto an unresolved field.
184
+ *
185
+ * @remarks
186
+ * `value` is unconstrained (any value, including `null` or `undefined`) as
187
+ * long as the key is present — the trivially-true guard `notOf(unionOf())`
188
+ * mirrors the reasons `Check.value` precedent.
189
+ *
190
+ * @param value - The value to test
191
+ * @returns `true` when `value` is a well-formed field default
192
+ *
193
+ * @example
194
+ * ```ts
195
+ * import { isFieldDefault } from '@src/core'
196
+ *
197
+ * isFieldDefault({ field: 'term', value: 12 }) // true
198
+ * isFieldDefault({ field: 'term' }) // false — value missing
199
+ * ```
200
+ */
201
+ function isFieldDefault(value) {
202
+ return (0, _orkestrel_contract.recordOf)({
203
+ field: _orkestrel_reason.isFieldPath,
204
+ value: (0, _orkestrel_contract.notOf)((0, _orkestrel_contract.unionOf)())
205
+ })(value);
206
+ }
207
+ /**
208
+ * Determine whether a value is a {@link ComputedField} — a declaratively
209
+ * computed field carrying a reasons {@link SymbolicExpression} tree.
210
+ *
211
+ * @param value - The value to test
212
+ * @returns `true` when `value` is a well-formed computed field
213
+ *
214
+ * @example
215
+ * ```ts
216
+ * import { constant, operation, variable } from '@orkestrel/reason'
217
+ * import { isComputedField } from '@src/core'
218
+ *
219
+ * isComputedField({
220
+ * field: 'monthly',
221
+ * expression: operation('divide', variable('deductible'), constant(12)),
222
+ * }) // true
223
+ * isComputedField({ field: 'monthly', expression: { form: 'variable' } }) // false — name missing
224
+ * ```
225
+ */
226
+ function isComputedField(value) {
227
+ return (0, _orkestrel_contract.recordOf)({
228
+ field: _orkestrel_reason.isFieldPath,
229
+ expression: _orkestrel_reason.isSymbolicExpression
230
+ })(value);
231
+ }
232
+ /**
233
+ * Determine whether a value is a {@link Template} — a named, versionable
234
+ * interpretation template.
235
+ *
236
+ * @remarks
237
+ * `definition` is validated with reasons' `isDefinition` — a `Template`'s
238
+ * definition is already expressed in terrain reasons vocabulary, so no
239
+ * parallel interprets-owned definition guard exists.
240
+ *
241
+ * @param value - The value to test
242
+ * @returns `true` when `value` is a well-formed template
243
+ *
244
+ * @example
245
+ * ```ts
246
+ * import { factorGroup, fieldFactor, quantitativeDefinition } from '@orkestrel/reason'
247
+ * import { isTemplate } from '@src/core'
248
+ *
249
+ * isTemplate({
250
+ * id: 't1',
251
+ * name: 'Arithmetic',
252
+ * domain: 'arithmetic',
253
+ * intents: ['calculate'],
254
+ * mappings: [],
255
+ * defaults: [],
256
+ * computations: [],
257
+ * definition: quantitativeDefinition('t1', 'Arithmetic', [
258
+ * factorGroup('total', 'sum', [fieldFactor('value', 'value')]),
259
+ * ]),
260
+ * }) // true
261
+ * isTemplate({ id: 't1' }) // false — most fields missing
262
+ * ```
263
+ */
264
+ function isTemplate(value) {
265
+ return (0, _orkestrel_contract.recordOf)({
266
+ id: _orkestrel_contract.isString,
267
+ name: _orkestrel_contract.isString,
268
+ domain: _orkestrel_contract.isString,
269
+ intents: (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString),
270
+ mappings: (0, _orkestrel_contract.arrayOf)(isEntityMapping),
271
+ defaults: (0, _orkestrel_contract.arrayOf)(isFieldDefault),
272
+ computations: (0, _orkestrel_contract.arrayOf)(isComputedField),
273
+ definition: _orkestrel_reason.isDefinition
274
+ })(value);
275
+ }
276
+ //#endregion
277
+ //#region src/core/helpers.ts
278
+ /**
279
+ * Escape every regex metacharacter in `text` so it matches literally when
280
+ * compiled into a `RegExp`.
281
+ *
282
+ * @param text - The literal text to escape
283
+ * @returns `text` with every regex metacharacter backslash-escaped
284
+ *
285
+ * @example
286
+ * ```ts
287
+ * import { escapeRegExp } from '@src/core'
288
+ *
289
+ * escapeRegExp('a.b*c') // 'a\\.b\\*c'
290
+ * new RegExp(escapeRegExp('a.b*c')).test('a.b*c') // true
291
+ * ```
292
+ */
293
+ function escapeRegExp(text) {
294
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
295
+ }
296
+ /**
297
+ * Copy-on-write write a value at a (possibly nested) field path on a subject.
298
+ *
299
+ * @remarks
300
+ * Never mutates `subject` — every level a `field` array descends through is
301
+ * freshly copied, so the input and every intermediate record stay untouched
302
+ * (AGENTS §11). Prototype-pollution-safe: a `field` containing `__proto__`,
303
+ * `prototype`, or `constructor` at ANY segment (checked against
304
+ * `UNSAFE_FIELD_SEGMENTS`) is refused as a no-op, returning `subject`
305
+ * unchanged. A non-record value already sitting at an intermediate segment is
306
+ * replaced by a fresh record rather than descended into.
307
+ *
308
+ * @param subject - The subject to derive from
309
+ * @param field - The (possibly nested) field path to write
310
+ * @param value - The value to write
311
+ * @returns A fresh subject with `value` written at `field`, or `subject`
312
+ * unchanged when `field` carries an unsafe segment
313
+ *
314
+ * @example
315
+ * ```ts
316
+ * import { setField } from '@src/core'
317
+ *
318
+ * setField({ age: 25 }, 'age', 30) // { age: 30 }
319
+ * setField({}, ['address', 'city'], 'Reno') // { address: { city: 'Reno' } }
320
+ * setField({}, ['__proto__', 'polluted'], true) // {} — refused, unchanged
321
+ * ```
322
+ */
323
+ /**
324
+ * Derive the sibling field path for a computed aggregate of `field` — the
325
+ * suffix is appended to `field`'s OWN last segment, so the aggregate nests
326
+ * beside the source field rather than flattening past it.
327
+ *
328
+ * @remarks
329
+ * For an array {@link FieldPath} (e.g. `['address', 'amounts']`) with suffix
330
+ * `'Sum'` the result is `['address', 'amountsSum']` — nested beside
331
+ * `address.amounts`. For a plain string field the result stays a flat string
332
+ * (`'amounts'` → `'amountsSum'`), matching the existing single-key behavior.
333
+ *
334
+ * @param field - The source field path
335
+ * @param suffix - The aggregate suffix (`'Sum'`, `'Count'`, `'Average'`, `'Minimum'`, `'Maximum'`)
336
+ * @returns The sibling field path for the aggregate
337
+ *
338
+ * @example
339
+ * ```ts
340
+ * import { deriveAggregateField } from '@src/core'
341
+ *
342
+ * deriveAggregateField(['address', 'amounts'], 'Sum') // ['address', 'amountsSum']
343
+ * deriveAggregateField('amounts', 'Sum') // 'amountsSum'
344
+ * ```
345
+ */
346
+ function deriveAggregateField(field, suffix) {
347
+ if (Array.isArray(field)) {
348
+ const last = field[field.length - 1];
349
+ return [...field.slice(0, -1), `${last}${suffix}`];
350
+ }
351
+ return `${field}${suffix}`;
352
+ }
353
+ function setField(subject, field, value) {
354
+ const path = Array.isArray(field) ? field : [field];
355
+ if (path.length === 0) return subject;
356
+ if (path.some((segment) => UNSAFE_FIELD_SEGMENTS.includes(segment))) return subject;
357
+ const [key, ...rest] = path;
358
+ if (key === void 0) return subject;
359
+ if (rest.length === 0) return {
360
+ ...subject,
361
+ [key]: value
362
+ };
363
+ const child = subject[key];
364
+ const nested = (0, _orkestrel_contract.isRecord)(child) ? child : {};
365
+ return {
366
+ ...subject,
367
+ [key]: setField(nested, rest, value)
368
+ };
369
+ }
370
+ /**
371
+ * Interpolate `{{dotted.path}}` tokens in a message template against a record.
372
+ *
373
+ * @remarks
374
+ * Each token is split on `.` into a {@link FieldPath} array and resolved with
375
+ * the contracts `resolveField` (a plain string field is ONE key, never
376
+ * dot-split — the split here is the token-to-path bridge). A finite number
377
+ * renders with `en-US` thousand grouping (`5010` → `5,010`); any other
378
+ * resolved value String-coerces. An UNRESOLVED path (the resolved value is
379
+ * `undefined`) renders as the empty string — the deterministic "nothing to
380
+ * show" rule.
381
+ *
382
+ * @param template - The message template carrying `{{dotted.path}}` tokens
383
+ * @param record - The record tokens resolve against
384
+ * @returns The template with every token replaced
385
+ *
386
+ * @example
387
+ * ```ts
388
+ * import { interpolateMessage } from '@src/core'
389
+ *
390
+ * interpolateMessage('Limit is {{limit}}', { limit: 5010 }) // 'Limit is 5,010'
391
+ * interpolateMessage('Missing {{gone}}', {}) // 'Missing '
392
+ * ```
393
+ */
394
+ function interpolateMessage(template, record) {
395
+ return template.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_match, path) => {
396
+ const value = (0, _orkestrel_contract.resolveField)(record, path.split("."));
397
+ if (value === void 0) return "";
398
+ if ((0, _orkestrel_contract.isFiniteNumber)(value)) return value.toLocaleString("en-US");
399
+ return String(value);
400
+ });
401
+ }
402
+ /**
403
+ * Replace every whole-word occurrence of a map's keys with their values.
404
+ *
405
+ * @remarks
406
+ * Word-boundary safe (`in` never matches inside `information`) and
407
+ * case-insensitive; each key is regex-escaped via the core-root
408
+ * `escapeRegExp` before compiling, so a caller-supplied phrase containing
409
+ * regex metacharacters is matched literally. Multiple keys apply in
410
+ * `Object.entries` order — the `Normalizer` stage sequences its three maps
411
+ * (contractions, abbreviations, corrections) with three separate calls.
412
+ *
413
+ * @param text - The text to substitute within
414
+ * @param map - The `{ from: to }` substitution map
415
+ * @returns `text` with every whole-word match replaced
416
+ *
417
+ * @example
418
+ * ```ts
419
+ * import { applyReplacements } from '@src/core'
420
+ *
421
+ * applyReplacements("can't stop", { "can't": 'cannot' }) // 'cannot stop'
422
+ * applyReplacements('information', { in: 'IN' }) // 'information' — word-boundary safe
423
+ * ```
424
+ */
425
+ function applyReplacements(text, map) {
426
+ let result = text;
427
+ for (const [from, to] of Object.entries(map)) {
428
+ const pattern = new RegExp(`\\b${escapeRegExp(from)}\\b`, "gi");
429
+ result = result.replace(pattern, to);
430
+ }
431
+ return result;
432
+ }
433
+ /**
434
+ * Collapse every run of whitespace to a single space and trim the ends.
435
+ *
436
+ * @param text - The text to collapse
437
+ * @returns The collapsed text
438
+ *
439
+ * @example
440
+ * ```ts
441
+ * import { collapseWhitespace } from '@src/core'
442
+ *
443
+ * collapseWhitespace(' a b\t c ') // 'a b c'
444
+ * ```
445
+ */
446
+ function collapseWhitespace(text) {
447
+ return text.replace(/\s+/g, " ").trim();
448
+ }
449
+ /**
450
+ * Split text into lowercase tokens, stripping punctuation outside a small
451
+ * numeric/currency-safe allowlist.
452
+ *
453
+ * @remarks
454
+ * Shared by `classifyIntent` and `assignEntities` — pure ECMAScript
455
+ * (no locale-aware `Intl` segmentation), so multi-byte / astral text tokenizes
456
+ * on ASCII word boundaries only.
457
+ *
458
+ * @param text - The text to tokenize
459
+ * @returns The lowercase tokens, punctuation-stripped, empty tokens dropped
460
+ *
461
+ * @example
462
+ * ```ts
463
+ * import { tokenize } from '@src/core'
464
+ *
465
+ * tokenize('The rate is 85%.') // ['the', 'rate', 'is', '85%']
466
+ * ```
467
+ */
468
+ function tokenize(text) {
469
+ return text.toLowerCase().replace(/[^a-z0-9\s./%$'-]/g, " ").split(/\s+/).filter((token) => token.length > 0);
470
+ }
471
+ /**
472
+ * Mine every numeric literal from text — optional leading `$`, thousands
473
+ * commas, an optional decimal fraction, an optional trailing `%`.
474
+ *
475
+ * @remarks
476
+ * Numbers-only: this is the module's entire extraction contract (AGENTS
477
+ * §21 mechanism-never-policy) — no date, text-entity, or negation parsing.
478
+ *
479
+ * @param text - The text to scan
480
+ * @returns Every extracted number, in left-to-right order
481
+ *
482
+ * @example
483
+ * ```ts
484
+ * import { extractNumbers } from '@src/core'
485
+ *
486
+ * extractNumbers('income was $50,000, age 25') // [50000, 25]
487
+ * ```
488
+ */
489
+ function extractNumbers(text) {
490
+ const pattern = new RegExp(NUMBER_PATTERN.source, NUMBER_PATTERN.flags);
491
+ const numbers = [];
492
+ let match = pattern.exec(text);
493
+ while (match !== null) {
494
+ const raw = match[1];
495
+ if (raw !== void 0) {
496
+ const value = Number(raw.replace(/,/g, ""));
497
+ if (Number.isFinite(value)) numbers.push(value);
498
+ }
499
+ match = pattern.exec(text);
500
+ }
501
+ return numbers;
502
+ }
503
+ /**
504
+ * Assign already-extracted numbers to a matched template's entity mappings.
505
+ *
506
+ * @remarks
507
+ * Strategy, in order: (1) a SINGLE mapping collects every number (an array
508
+ * when more than one, a scalar otherwise) at `CONFIDENCE_COLLECT`. Otherwise,
509
+ * per mapping, the rightmost token in the text that equals the entity name
510
+ * (`CONFIDENCE_EXACT`), an alias exactly (`CONFIDENCE_ALIAS`), or an alias
511
+ * fuzzily (via `matchAlias`, confidence = its returned score) becomes a
512
+ * keyword anchor; anchors sort left-to-right and each claims its nearest
513
+ * unused number by text position. (3) Any mapping still unfilled claims the
514
+ * next unused number positionally, at `CONFIDENCE_POSITIONAL`. Every entity
515
+ * carries provenance `category: 'extracted'` with `detail` naming the
516
+ * strategy that filled it (`'collect' | 'keyword' | 'alias' | 'positional'`).
517
+ * Runs ONLY after a template has matched (an orchestrator-owned step, never
518
+ * inside `Extractor`, which stays template-agnostic).
519
+ *
520
+ * @param numbers - The numbers already extracted from `text` via `extractNumbers`
521
+ * @param mappings - The matched template's entity mappings
522
+ * @param text - The same text `numbers` was extracted from (for keyword proximity)
523
+ * @param similarity - The fuzzy alias-match score threshold (0..1)
524
+ * @returns The assigned entities, one per filled mapping
525
+ *
526
+ * @example
527
+ * ```ts
528
+ * import { assignEntities } from '@src/core'
529
+ *
530
+ * const mappings = [
531
+ * { entity: 'age', aliases: ['years old'], field: 'age' },
532
+ * { entity: 'score', aliases: ['credit score'], field: 'score' },
533
+ * ]
534
+ * assignEntities([25, 720], mappings, '25 year old with score 720', 0.8)
535
+ * // [{ name: 'age', value: 25, ... }, { name: 'score', value: 720, ... }]
536
+ * ```
537
+ */
538
+ function assignEntities(numbers, mappings, text, similarity) {
539
+ if (mappings.length === 0 || numbers.length === 0) return [];
540
+ if (mappings.length === 1) {
541
+ const mapping = mappings[0];
542
+ if (mapping === void 0) return [];
543
+ return [{
544
+ name: mapping.entity,
545
+ value: numbers.length === 1 ? numbers[0] : numbers,
546
+ provenance: {
547
+ category: "extracted",
548
+ detail: "collect"
549
+ },
550
+ confidence: CONFIDENCE_COLLECT
551
+ }];
552
+ }
553
+ const tokens = tokenize(text);
554
+ const lowerText = text.toLowerCase();
555
+ const positions = [];
556
+ const positionPattern = new RegExp(NUMBER_PATTERN.source, NUMBER_PATTERN.flags);
557
+ let positionMatch = positionPattern.exec(text);
558
+ while (positionMatch !== null) {
559
+ positions.push(positionMatch.index);
560
+ positionMatch = positionPattern.exec(text);
561
+ }
562
+ const keywordMatches = [];
563
+ for (const mapping of mappings) {
564
+ let matchedPosition = -1;
565
+ let matchedConfidence = 0;
566
+ let matchedDetail = "keyword";
567
+ const entityToken = mapping.entity.toLowerCase();
568
+ for (const token of tokens) {
569
+ const tokenPosition = lowerText.indexOf(token);
570
+ if (token === entityToken) {
571
+ if (tokenPosition > matchedPosition) {
572
+ matchedPosition = tokenPosition;
573
+ matchedConfidence = 1;
574
+ matchedDetail = "keyword";
575
+ }
576
+ continue;
577
+ }
578
+ if (mapping.aliases.some((alias) => alias.toLowerCase() === token)) {
579
+ if (tokenPosition > matchedPosition) {
580
+ matchedPosition = tokenPosition;
581
+ matchedConfidence = CONFIDENCE_ALIAS;
582
+ matchedDetail = "alias";
583
+ }
584
+ continue;
585
+ }
586
+ const fuzzy = matchAlias(token, mapping.aliases, similarity);
587
+ if (fuzzy > 0 && tokenPosition > matchedPosition) {
588
+ matchedPosition = tokenPosition;
589
+ matchedConfidence = fuzzy;
590
+ matchedDetail = "alias";
591
+ }
592
+ }
593
+ if (matchedPosition >= 0) keywordMatches.push({
594
+ mapping,
595
+ position: matchedPosition,
596
+ confidence: matchedConfidence,
597
+ detail: matchedDetail
598
+ });
599
+ }
600
+ keywordMatches.sort((a, b) => a.position - b.position);
601
+ const used = /* @__PURE__ */ new Set();
602
+ const filled = /* @__PURE__ */ new Set();
603
+ const entities = [];
604
+ for (const match of keywordMatches) {
605
+ let bestIndex = -1;
606
+ let bestDistance = Number.POSITIVE_INFINITY;
607
+ for (let index = 0; index < numbers.length; index += 1) {
608
+ if (used.has(index)) continue;
609
+ const position = positions[index];
610
+ if (position === void 0) continue;
611
+ const distance = Math.abs(position - match.position);
612
+ if (distance < bestDistance) {
613
+ bestDistance = distance;
614
+ bestIndex = index;
615
+ }
616
+ }
617
+ if (bestIndex >= 0) {
618
+ const value = numbers[bestIndex];
619
+ if (value !== void 0) {
620
+ used.add(bestIndex);
621
+ filled.add(match.mapping.entity);
622
+ entities.push({
623
+ name: match.mapping.entity,
624
+ value,
625
+ provenance: {
626
+ category: "extracted",
627
+ detail: match.detail
628
+ },
629
+ confidence: match.confidence
630
+ });
631
+ }
632
+ }
633
+ }
634
+ let numberIndex = 0;
635
+ for (const mapping of mappings) {
636
+ if (filled.has(mapping.entity)) continue;
637
+ while (numberIndex < numbers.length && used.has(numberIndex)) numberIndex += 1;
638
+ if (numberIndex >= numbers.length) break;
639
+ const value = numbers[numberIndex];
640
+ if (value !== void 0) {
641
+ used.add(numberIndex);
642
+ filled.add(mapping.entity);
643
+ entities.push({
644
+ name: mapping.entity,
645
+ value,
646
+ provenance: {
647
+ category: "extracted",
648
+ detail: "positional"
649
+ },
650
+ confidence: CONFIDENCE_POSITIONAL
651
+ });
652
+ }
653
+ numberIndex += 1;
654
+ }
655
+ return entities;
656
+ }
657
+ /**
658
+ * Classify the action + domain intent of a text against caller-supplied
659
+ * vocabularies.
660
+ *
661
+ * @remarks
662
+ * `actions` maps a token to an action name — the first matching token in
663
+ * `text` (left to right) wins, at `CONFIDENCE_EXACT`. `domains` maps a domain
664
+ * name to its keyword list — the domain with the most matching tokens wins
665
+ * (ties keep the earliest-declared domain), also at `CONFIDENCE_EXACT`.
666
+ * Combined confidence (PINNED): both fire → their average; exactly one fires
667
+ * → its value times `0.5`; neither → `0`. There is no built-in worldview and
668
+ * no auto-classification from a registered template's own `domain` name — a
669
+ * caller MUST list a template's domain among `domains` for it to classify.
670
+ * No `floor` parameter: the confidence floor gate lives at the orchestrator's
671
+ * `matchTemplate` step, never inside classification itself.
672
+ *
673
+ * @param text - The (normalized) text to classify
674
+ * @param actions - The caller's token → action-name vocabulary
675
+ * @param domains - The caller's domain-name → keyword-list vocabulary
676
+ * @returns The classified intent
677
+ *
678
+ * @example
679
+ * ```ts
680
+ * import { classifyIntent } from '@src/core'
681
+ *
682
+ * classifyIntent('calculate my rate', { calculate: 'compute' }, { rating: ['rate'] })
683
+ * // { action: 'compute', domain: 'rating', confidence: 1 }
684
+ * classifyIntent('hello', {}, {}) // { action: '', domain: '', confidence: 0 }
685
+ * ```
686
+ */
687
+ function classifyIntent(text, actions, domains) {
688
+ const tokens = tokenize(text);
689
+ let action = "";
690
+ let actionConfidence = 0;
691
+ for (const token of tokens) {
692
+ const mapped = actions[token];
693
+ if (mapped !== void 0) {
694
+ action = mapped;
695
+ actionConfidence = 1;
696
+ break;
697
+ }
698
+ }
699
+ let domain = "";
700
+ let domainConfidence = 0;
701
+ let bestMatches = 0;
702
+ for (const [name, keywords] of Object.entries(domains)) {
703
+ const lowerKeywords = keywords.map((keyword) => keyword.toLowerCase());
704
+ let matches = 0;
705
+ for (const token of tokens) if (lowerKeywords.includes(token)) matches += 1;
706
+ if (matches > bestMatches) {
707
+ bestMatches = matches;
708
+ domain = name;
709
+ domainConfidence = 1;
710
+ }
711
+ }
712
+ const confidence = actionConfidence > 0 && domainConfidence > 0 ? (actionConfidence + domainConfidence) / 2 : Math.max(actionConfidence, domainConfidence) * .5;
713
+ return {
714
+ action,
715
+ domain,
716
+ confidence
717
+ };
718
+ }
719
+ /**
720
+ * Bigram (Dice coefficient) string similarity, case-insensitive.
721
+ *
722
+ * @param a - The first string
723
+ * @param b - The second string
724
+ * @returns A score in `[0, 1]` — `1` for an exact (case-insensitive) match,
725
+ * `0` when either string is shorter than 2 characters and they are not equal
726
+ *
727
+ * @example
728
+ * ```ts
729
+ * import { scoreSimilarity } from '@src/core'
730
+ *
731
+ * scoreSimilarity('rate', 'rate') // 1
732
+ * scoreSimilarity('rate', 'value') // 0 — no shared bigrams
733
+ * ```
734
+ */
735
+ function scoreSimilarity(a, b) {
736
+ const left = a.toLowerCase();
737
+ const right = b.toLowerCase();
738
+ if (left === right) return 1;
739
+ if (left.length < 2 || right.length < 2) return 0;
740
+ const bigrams = /* @__PURE__ */ new Map();
741
+ for (let index = 0; index < left.length - 1; index += 1) {
742
+ const bigram = left.slice(index, index + 2);
743
+ bigrams.set(bigram, (bigrams.get(bigram) ?? 0) + 1);
744
+ }
745
+ let matches = 0;
746
+ for (let index = 0; index < right.length - 1; index += 1) {
747
+ const bigram = right.slice(index, index + 2);
748
+ const count = bigrams.get(bigram);
749
+ if (count !== void 0 && count > 0) {
750
+ bigrams.set(bigram, count - 1);
751
+ matches += 1;
752
+ }
753
+ }
754
+ return 2 * matches / (left.length - 1 + (right.length - 1));
755
+ }
756
+ /**
757
+ * The best `scoreSimilarity` a token achieves against a list of aliases,
758
+ * gated by a threshold.
759
+ *
760
+ * @param token - The token to score
761
+ * @param aliases - The alias phrases to score against
762
+ * @param threshold - The minimum score to report (an explicit no-match below it)
763
+ * @returns The best score when it meets `threshold`, else `0`
764
+ *
765
+ * @example
766
+ * ```ts
767
+ * import { matchAlias } from '@src/core'
768
+ *
769
+ * matchAlias('valu', ['value', 'amount'], 0.6) // ~0.86 — fuzzy hit on 'value'
770
+ * matchAlias('xyz', ['value', 'amount'], 0.6) // 0 — no alias clears the threshold
771
+ * ```
772
+ */
773
+ function matchAlias(token, aliases, threshold) {
774
+ let best = 0;
775
+ for (const alias of aliases) {
776
+ const score = scoreSimilarity(token, alias);
777
+ if (score > best) best = score;
778
+ }
779
+ return best >= threshold ? best : 0;
780
+ }
781
+ /**
782
+ * Render a value into a canonical, key-order-stable string — the pre-image
783
+ * of `digestValue`.
784
+ *
785
+ * @remarks
786
+ * Ported from the app's `raters` digest machinery (`app/core/raters/helpers.ts`)
787
+ * — record keys sort before serialization so a re-ordered object canonicalizes
788
+ * identically; arrays keep position order (position is meaningful).
789
+ * Cycle-safe and total (AGENTS §14): `visited` tracks the object ancestors
790
+ * along the CURRENT recursion path (not a global "seen" set, so the same
791
+ * object reachable twice via non-cyclic sibling branches still canonicalizes
792
+ * normally); revisiting an ancestor renders that node as the literal string
793
+ * `'[cycle]'` instead of recursing — deterministic, never throws, never
794
+ * overflows the call stack.
795
+ *
796
+ * @param value - The value to canonicalize
797
+ * @param visited - The object ancestors along the current recursion path (internal; omit at the call site)
798
+ * @returns The canonical string form
799
+ *
800
+ * @example
801
+ * ```ts
802
+ * import { canonicalize } from '@src/core'
803
+ *
804
+ * canonicalize({ b: 1, a: 2 }) === canonicalize({ a: 2, b: 1 }) // true
805
+ * ```
806
+ */
807
+ function canonicalize(value, visited = /* @__PURE__ */ new Set()) {
808
+ if (Array.isArray(value)) {
809
+ if (visited.has(value)) return JSON.stringify("[cycle]");
810
+ const nextVisited = new Set(visited);
811
+ nextVisited.add(value);
812
+ return `[${value.map((entry) => canonicalize(entry, nextVisited)).join(",")}]`;
813
+ }
814
+ if ((0, _orkestrel_contract.isRecord)(value)) {
815
+ if (visited.has(value)) return JSON.stringify("[cycle]");
816
+ const nextVisited = new Set(visited);
817
+ nextVisited.add(value);
818
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalize(value[key], nextVisited)}`).join(",")}}`;
819
+ }
820
+ return JSON.stringify(value) ?? "null";
821
+ }
822
+ /**
823
+ * Compute a canonical structural digest of a pure-JSON value — a key-order-
824
+ * stable FNV-1a hash rendered as an 8-hex-digit string.
825
+ *
826
+ * @remarks
827
+ * Pure ECMAScript, no host crypto (AGENTS §17.7) — the same algorithm as the
828
+ * app's `digest`, ported into core so `Interpretation.digest` and the
829
+ * versioned managers can hash without leaving strict core.
830
+ *
831
+ * @param value - The value to digest
832
+ * @returns The 8-character hex digest
833
+ *
834
+ * @example
835
+ * ```ts
836
+ * import { digestValue } from '@src/core'
837
+ *
838
+ * digestValue({ a: 1 }) === digestValue({ a: 1 }) // true — deterministic
839
+ * ```
840
+ */
841
+ function digestValue(value) {
842
+ const canonical = canonicalize(value);
843
+ let hash = 2166136261;
844
+ for (let index = 0; index < canonical.length; index += 1) {
845
+ hash ^= canonical.charCodeAt(index);
846
+ hash = Math.imul(hash, 16777619);
847
+ }
848
+ return (hash >>> 0).toString(16).padStart(8, "0");
849
+ }
850
+ /**
851
+ * Score how well a classified intent matches one template's domain + action.
852
+ *
853
+ * @param intent - The classified intent
854
+ * @param template - The candidate template
855
+ * @returns A score in `[0, 1]` — the mean of the domain match (`1`/`0`) and
856
+ * the action match (`1`/`0`, `template.intents` containing `intent.action`)
857
+ *
858
+ * @example
859
+ * ```ts
860
+ * import { scoreTemplate } from '@src/core'
861
+ *
862
+ * scoreTemplate(
863
+ * { action: 'compute', domain: 'rating', confidence: 1 },
864
+ * { id: 't1', name: 'T', domain: 'rating', intents: ['compute'], mappings: [], defaults: [], computations: [], definition: { reasoning: 'symbolic', id: 't1', name: 'T', equations: [], variables: {} } },
865
+ * ) // 1
866
+ * ```
867
+ */
868
+ function scoreTemplate(intent, template) {
869
+ return ((template.domain.toLowerCase() === intent.domain.toLowerCase() ? 1 : 0) + (template.intents.some((candidate) => candidate.toLowerCase() === intent.action.toLowerCase()) ? 1 : 0)) / 2;
870
+ }
871
+ /**
872
+ * Find the best-scoring registered template for a classified intent, gated
873
+ * by a confidence floor.
874
+ *
875
+ * @remarks
876
+ * Explicit no-match (AGENTS-flagged scsr defect 2 — never an arbitrary
877
+ * `templates[0]` fallback): an empty registry, or a best score strictly below
878
+ * `floor`, both return `undefined`.
879
+ *
880
+ * @param intent - The classified intent
881
+ * @param templates - The registered templates to score
882
+ * @param floor - The minimum score a match must clear
883
+ * @returns The best-scoring template, or `undefined` on no qualifying match
884
+ *
885
+ * @example
886
+ * ```ts
887
+ * import { matchTemplate } from '@src/core'
888
+ *
889
+ * matchTemplate({ action: '', domain: '', confidence: 0 }, [], 0.3) // undefined — empty registry
890
+ * ```
891
+ */
892
+ function matchTemplate(intent, templates, floor) {
893
+ let best;
894
+ let bestScore = -1;
895
+ for (const template of templates) {
896
+ const score = scoreTemplate(intent, template);
897
+ if (score > bestScore) {
898
+ bestScore = score;
899
+ best = template;
900
+ }
901
+ }
902
+ return best !== void 0 && bestScore >= floor ? best : void 0;
903
+ }
904
+ /**
905
+ * Collect every variable name referenced by a symbolic expression tree, in
906
+ * first-occurrence order.
907
+ *
908
+ * @param expression - The expression tree to scan
909
+ * @returns The referenced variable names, deduplicated
910
+ *
911
+ * @example
912
+ * ```ts
913
+ * import { constant, operation, variable } from '@orkestrel/reason'
914
+ * import { variablesOf } from '@src/core'
915
+ *
916
+ * variablesOf(operation('divide', variable('deductible'), constant(12))) // ['deductible']
917
+ * ```
918
+ */
919
+ function variablesOf(expression) {
920
+ const names = [];
921
+ const seen = /* @__PURE__ */ new Set();
922
+ function collect(node) {
923
+ if (node.form === "variable") {
924
+ if (!seen.has(node.name)) {
925
+ seen.add(node.name);
926
+ names.push(node.name);
927
+ }
928
+ return;
929
+ }
930
+ if (node.form === "operation") {
931
+ collect(node.left);
932
+ if (node.right !== void 0) collect(node.right);
933
+ }
934
+ }
935
+ collect(expression);
936
+ return names;
937
+ }
938
+ /**
939
+ * Evaluate a symbolic expression tree against resolved bindings.
940
+ *
941
+ * @remarks
942
+ * THE critical leaf (design-pinned, engine-parity semantics): an absent
943
+ * `right` operand on a binary operation defaults to `0` — matching
944
+ * `SymbolicReasoner`'s internal `#evaluate` — and is always passed as an
945
+ * EXPLICIT numeric operand, so the same tree evaluates identically here and
946
+ * inside the engine. Each arithmetic step delegates to the reasons
947
+ * `applyOperation` pure function, mapping the node's `.operator` field onto
948
+ * its `operator` parameter. An unresolved input variable, or a non-finite
949
+ * result (`NaN` from a divide-by-zero, or an overflowing `±Infinity`),
950
+ * becomes a gap — `undefined`, never landing on a subject.
951
+ *
952
+ * @param expression - The expression tree to evaluate
953
+ * @param bindings - The resolved variable bindings
954
+ * @returns The evaluated number, or `undefined` on an unresolved input or a
955
+ * non-finite result
956
+ *
957
+ * @example
958
+ * ```ts
959
+ * import { constant, operation, variable } from '@orkestrel/reason'
960
+ * import { resolveExpression } from '@src/core'
961
+ *
962
+ * resolveExpression(operation('divide', variable('deductible'), constant(12)), { deductible: 6000 }) // 500
963
+ * resolveExpression(operation('divide', constant(1), constant(0)), {}) // undefined — NaN gap
964
+ * resolveExpression(variable('missing'), {}) // undefined — unresolved input
965
+ * ```
966
+ */
967
+ function resolveExpression(expression, bindings) {
968
+ if (expression.form === "constant") return expression.value;
969
+ if (expression.form === "variable") return bindings[expression.name];
970
+ const left = resolveExpression(expression.left, bindings);
971
+ if (left === void 0) return void 0;
972
+ const right = expression.right === void 0 ? 0 : resolveExpression(expression.right, bindings);
973
+ if (right === void 0) return void 0;
974
+ const result = (0, _orkestrel_reason.applyOperation)(expression.operator, left, right);
975
+ return (0, _orkestrel_contract.isFiniteNumber)(result) ? result : void 0;
976
+ }
977
+ /**
978
+ * Render a one-line, display-neutral description of a reasons `Subject`,
979
+ * through an injected `Narrator`.
980
+ *
981
+ * @remarks
982
+ * Complements — never duplicates — the raters `describe*` family (which
983
+ * describes RATERS artifacts); this describes REASONS artifacts. Every field
984
+ * renders via `narrator.label` + `narrator.value` (looked up under the
985
+ * `'units'` phrase table, falling back to `'plain'`) — the wording is fully
986
+ * lexicon-driven (AGENTS §21 mechanism-never-policy); `Definition` /
987
+ * `ReasonResult` narration lives on `Narrator#describe` / `Narrator#narrate`
988
+ * directly.
989
+ *
990
+ * @param subject - The subject to describe
991
+ * @param narrator - The lexicon-driven renderer to render field labels/values/lines through
992
+ * @returns A one-line description, the lexicon's `'subject.empty'` line when empty
993
+ *
994
+ * @example
995
+ * ```ts
996
+ * import { createNarrator, describeSubject } from '@src/core'
997
+ *
998
+ * describeSubject({ age: 25, income: 50000 }, createNarrator()) // 'with age: 25, income: 50000'
999
+ * ```
1000
+ */
1001
+ function describeSubject(subject, narrator) {
1002
+ const keys = Object.keys(subject).sort();
1003
+ if (keys.length === 0) return narrator.line("subject.empty", {});
1004
+ const parts = keys.map((key) => {
1005
+ const unit = narrator.phrase("units", key, "plain");
1006
+ return `${narrator.label(key)}: ${narrator.value(unit, subject[key])}`;
1007
+ });
1008
+ return narrator.line("subject.fields", { fields: parts.join(", ") });
1009
+ }
1010
+ /**
1011
+ * Parse a JSON string into a `Template`, or `undefined` on invalid JSON or a
1012
+ * shape that fails `isTemplate`.
1013
+ *
1014
+ * @remarks
1015
+ * The module's sole JSON boundary (design §4) — an `Interpretation` and the
1016
+ * versioned records are produced internally, never deserialized from
1017
+ * untrusted JSON; replay re-runs `interpret`, it does not deserialize a
1018
+ * stored result.
1019
+ *
1020
+ * @param value - The JSON text to parse
1021
+ * @returns The parsed template, or `undefined`
1022
+ *
1023
+ * @example
1024
+ * ```ts
1025
+ * import { parseTemplate } from '@src/core'
1026
+ *
1027
+ * parseTemplate('not json') // undefined
1028
+ * ```
1029
+ */
1030
+ function parseTemplate(value) {
1031
+ return (0, _orkestrel_contract.parseJSONAs)(value, isTemplate);
1032
+ }
1033
+ //#endregion
1034
+ //#region src/core/managers/DefinitionManager.ts
1035
+ /**
1036
+ * The definition registry — a self-owning, versioned and content-hashed
1037
+ * record-holder for the reasons {@link Definition}s an interpretation produces.
1038
+ *
1039
+ * @remarks
1040
+ * Mirrors {@link TemplateManager}: `add` defaults each record id to the
1041
+ * definition's own `id`, derives `hash` from the definition CONTENT
1042
+ * (id-independent), and bumps `version` ONLY when that hash changes at a reused
1043
+ * id — an identical re-add keeps its version. The batch `remove(ids)` form is
1044
+ * all-or-nothing; `destroy()` is idempotent and every method afterwards throws
1045
+ * `InterpretError('DESTROYED', …)`.
1046
+ *
1047
+ * @example
1048
+ * ```ts
1049
+ * import { symbolicDefinition } from '@orkestrel/reason'
1050
+ * import { DefinitionManager } from '@src/core'
1051
+ *
1052
+ * const manager = new DefinitionManager()
1053
+ * const record = manager.add(symbolicDefinition('rate', 'Rate', []))
1054
+ * record.id // 'rate'
1055
+ * manager.add(symbolicDefinition('rate', 'Rate', [])).version // 1 — identical re-add, no bump
1056
+ * ```
1057
+ */
1058
+ var DefinitionManager = class {
1059
+ #records = /* @__PURE__ */ new Map();
1060
+ #emitter;
1061
+ #destroyed = false;
1062
+ constructor(options) {
1063
+ this.#emitter = new _orkestrel_emitter.Emitter({
1064
+ on: options?.on,
1065
+ error: options?.error
1066
+ });
1067
+ for (const definition of options?.definitions ?? []) this.add(definition);
1068
+ }
1069
+ get emitter() {
1070
+ return this.#emitter;
1071
+ }
1072
+ get size() {
1073
+ this.#ensureAlive();
1074
+ return this.#records.size;
1075
+ }
1076
+ has(id) {
1077
+ this.#ensureAlive();
1078
+ return this.#records.has(id);
1079
+ }
1080
+ definition(id) {
1081
+ this.#ensureAlive();
1082
+ return this.#records.get(id);
1083
+ }
1084
+ definitions() {
1085
+ this.#ensureAlive();
1086
+ return [...this.#records.values()];
1087
+ }
1088
+ add(definition, options) {
1089
+ this.#ensureAlive();
1090
+ const id = options?.id ?? definition.id;
1091
+ const hash = digestValue(definition);
1092
+ const existing = this.#records.get(id);
1093
+ const record = {
1094
+ id,
1095
+ definition,
1096
+ version: existing === void 0 ? 1 : existing.hash === hash ? existing.version : existing.version + 1,
1097
+ hash
1098
+ };
1099
+ this.#records.set(id, record);
1100
+ this.#emitter.emit("add", id);
1101
+ return record;
1102
+ }
1103
+ remove(target) {
1104
+ this.#ensureAlive();
1105
+ if (target === void 0) {
1106
+ for (const id of this.#records.keys()) this.#emitter.emit("remove", id);
1107
+ this.#records.clear();
1108
+ return;
1109
+ }
1110
+ if (typeof target === "string") {
1111
+ const removed = this.#records.delete(target);
1112
+ if (removed) this.#emitter.emit("remove", target);
1113
+ return removed;
1114
+ }
1115
+ for (const id of target) if (!this.#records.has(id)) return false;
1116
+ for (const id of target) {
1117
+ this.#records.delete(id);
1118
+ this.#emitter.emit("remove", id);
1119
+ }
1120
+ return true;
1121
+ }
1122
+ destroy() {
1123
+ if (this.#destroyed) return;
1124
+ this.#records.clear();
1125
+ this.#destroyed = true;
1126
+ this.#emitter.emit("destroy");
1127
+ this.#emitter.destroy();
1128
+ }
1129
+ #ensureAlive() {
1130
+ if (this.#destroyed) throw new InterpretError("DESTROYED", "Definition manager has been destroyed");
1131
+ }
1132
+ };
1133
+ //#endregion
1134
+ //#region src/core/managers/SubjectManager.ts
1135
+ /**
1136
+ * The subject registry — a self-owning, versioned and content-hashed
1137
+ * record-holder that mints its OWN record identity for every {@link Subject}
1138
+ * (a `Subject` carries no `id` field of its own).
1139
+ *
1140
+ * @remarks
1141
+ * The defect-7 fix: scsr keyed stored subjects by their definition's id, so
1142
+ * successive same-domain turns silently overwrote one shared subject. Here each
1143
+ * `add` mints a fresh `subject-{n}` id (deterministic per instance, no host
1144
+ * randomness — AGENTS §17.7) unless the caller overrides it via
1145
+ * `ManagerAddOptions.id`. `hash` is content-derived (id-independent) and
1146
+ * `version` bumps ONLY when the hash changes at a reused id. The batch
1147
+ * `remove(ids)` form is all-or-nothing; `destroy()` is idempotent and every
1148
+ * method afterwards throws `InterpretError('DESTROYED', …)`.
1149
+ *
1150
+ * @example
1151
+ * ```ts
1152
+ * import { SubjectManager } from '@src/core'
1153
+ *
1154
+ * const manager = new SubjectManager()
1155
+ * const first = manager.add({ age: 25 })
1156
+ * const second = manager.add({ age: 30 })
1157
+ * first.id !== second.id // true — each subject gets its own identity
1158
+ * ```
1159
+ */
1160
+ var SubjectManager = class {
1161
+ #records = /* @__PURE__ */ new Map();
1162
+ #emitter;
1163
+ #counter = 0;
1164
+ #destroyed = false;
1165
+ constructor(options) {
1166
+ this.#emitter = new _orkestrel_emitter.Emitter({
1167
+ on: options?.on,
1168
+ error: options?.error
1169
+ });
1170
+ for (const subject of options?.subjects ?? []) this.add(subject);
1171
+ }
1172
+ get emitter() {
1173
+ return this.#emitter;
1174
+ }
1175
+ get size() {
1176
+ this.#ensureAlive();
1177
+ return this.#records.size;
1178
+ }
1179
+ has(id) {
1180
+ this.#ensureAlive();
1181
+ return this.#records.has(id);
1182
+ }
1183
+ subject(id) {
1184
+ this.#ensureAlive();
1185
+ return this.#records.get(id);
1186
+ }
1187
+ subjects() {
1188
+ this.#ensureAlive();
1189
+ return [...this.#records.values()];
1190
+ }
1191
+ add(subject, options) {
1192
+ this.#ensureAlive();
1193
+ let id = options?.id;
1194
+ if (id === void 0) {
1195
+ id = `subject-${this.#counter}`;
1196
+ this.#counter += 1;
1197
+ }
1198
+ const hash = digestValue(subject);
1199
+ const existing = this.#records.get(id);
1200
+ const version = existing === void 0 ? 1 : existing.hash === hash ? existing.version : existing.version + 1;
1201
+ const record = {
1202
+ id,
1203
+ subject,
1204
+ version,
1205
+ hash
1206
+ };
1207
+ this.#records.set(id, record);
1208
+ this.#emitter.emit("add", id);
1209
+ return record;
1210
+ }
1211
+ remove(target) {
1212
+ this.#ensureAlive();
1213
+ if (target === void 0) {
1214
+ for (const id of this.#records.keys()) this.#emitter.emit("remove", id);
1215
+ this.#records.clear();
1216
+ return;
1217
+ }
1218
+ if (typeof target === "string") {
1219
+ const removed = this.#records.delete(target);
1220
+ if (removed) this.#emitter.emit("remove", target);
1221
+ return removed;
1222
+ }
1223
+ for (const id of target) if (!this.#records.has(id)) return false;
1224
+ for (const id of target) {
1225
+ this.#records.delete(id);
1226
+ this.#emitter.emit("remove", id);
1227
+ }
1228
+ return true;
1229
+ }
1230
+ destroy() {
1231
+ if (this.#destroyed) return;
1232
+ this.#records.clear();
1233
+ this.#destroyed = true;
1234
+ this.#emitter.emit("destroy");
1235
+ this.#emitter.destroy();
1236
+ }
1237
+ #ensureAlive() {
1238
+ if (this.#destroyed) throw new InterpretError("DESTROYED", "Subject manager has been destroyed");
1239
+ }
1240
+ };
1241
+ //#endregion
1242
+ //#region src/core/managers/InterpretContext.ts
1243
+ /**
1244
+ * Cross-turn interpretation context — a capped, replayable history of
1245
+ * completed {@link Interpretation}s plus the subject and definition registries
1246
+ * carry-over reads from.
1247
+ *
1248
+ * @remarks
1249
+ * `previous()` is a capped ring buffer (newest-last, oldest dropped once the
1250
+ * `history` cap is reached — `DEFAULT_INTERPRET_HISTORY` by default, ≥ 3
1251
+ * preserves the carry-over pin) rather than scsr's unbounded `previous` array.
1252
+ * `entities()` flattens every entity across the buffered history, most recent
1253
+ * last — the read a `Clarifier`'s same-domain carry-over consults. `add` pushes
1254
+ * one result and trims to the cap; `clear` resets the history and both
1255
+ * registries WITHOUT tearing the context down; `destroy()` is idempotent and
1256
+ * every method afterwards throws `InterpretError('DESTROYED', …)`.
1257
+ *
1258
+ * @example
1259
+ * ```ts
1260
+ * import { InterpretContext } from '@src/core'
1261
+ *
1262
+ * const context = new InterpretContext({ session: 's1', history: 8 })
1263
+ * context.session // 's1'
1264
+ * context.previous() // []
1265
+ * ```
1266
+ */
1267
+ var InterpretContext = class {
1268
+ #session;
1269
+ #subjects;
1270
+ #definitions;
1271
+ #history;
1272
+ #previous = [];
1273
+ #emitter;
1274
+ #destroyed = false;
1275
+ constructor(options) {
1276
+ this.#session = options?.session;
1277
+ this.#history = Math.max(0, options?.history ?? 16);
1278
+ this.#subjects = new SubjectManager();
1279
+ this.#definitions = new DefinitionManager();
1280
+ this.#emitter = new _orkestrel_emitter.Emitter({
1281
+ on: options?.on,
1282
+ error: options?.error
1283
+ });
1284
+ }
1285
+ get emitter() {
1286
+ return this.#emitter;
1287
+ }
1288
+ get session() {
1289
+ this.#ensureAlive();
1290
+ return this.#session;
1291
+ }
1292
+ get subjects() {
1293
+ this.#ensureAlive();
1294
+ return this.#subjects;
1295
+ }
1296
+ get definitions() {
1297
+ this.#ensureAlive();
1298
+ return this.#definitions;
1299
+ }
1300
+ previous() {
1301
+ this.#ensureAlive();
1302
+ return [...this.#previous];
1303
+ }
1304
+ entities() {
1305
+ this.#ensureAlive();
1306
+ return this.#previous.flatMap((result) => [...result.entities]);
1307
+ }
1308
+ add(result) {
1309
+ this.#ensureAlive();
1310
+ this.#previous.push(result);
1311
+ while (this.#previous.length > this.#history) this.#previous.shift();
1312
+ this.#emitter.emit("add", result.digest);
1313
+ }
1314
+ clear() {
1315
+ this.#ensureAlive();
1316
+ this.#previous.length = 0;
1317
+ this.#subjects.remove();
1318
+ this.#definitions.remove();
1319
+ this.#emitter.emit("clear");
1320
+ }
1321
+ destroy() {
1322
+ if (this.#destroyed) return;
1323
+ this.#previous.length = 0;
1324
+ this.#subjects.destroy();
1325
+ this.#definitions.destroy();
1326
+ this.#destroyed = true;
1327
+ this.#emitter.emit("destroy");
1328
+ this.#emitter.destroy();
1329
+ }
1330
+ #ensureAlive() {
1331
+ if (this.#destroyed) throw new InterpretError("DESTROYED", "Interpret context has been destroyed");
1332
+ }
1333
+ };
1334
+ //#endregion
1335
+ //#region src/core/managers/TemplateManager.ts
1336
+ /**
1337
+ * The template registry — a self-owning, versioned and content-hashed
1338
+ * record-holder for the {@link Template}s an `Interpret` orchestrator matches
1339
+ * against.
1340
+ *
1341
+ * @remarks
1342
+ * `size` (never `count` — the sole tally in scope) plus the AGENTS §9.1
1343
+ * singular/plural accessors (`template` / `templates`) and the §9.2 batch
1344
+ * `remove` overloads. `add` derives each record's `hash` from the template's
1345
+ * CONTENT (id-independent — the same template data hashes identically under
1346
+ * any record id) and bumps `version` ONLY when that hash changes: an identical
1347
+ * re-add keeps its version (unlike scsr, which bumped on every add). The
1348
+ * batch `remove(ids)` form is ALL-OR-NOTHING — any id absent from the registry
1349
+ * leaves the collection untouched and returns `false`. `destroy()` is
1350
+ * idempotent; every method afterwards throws `InterpretError('DESTROYED', …)`.
1351
+ *
1352
+ * @example
1353
+ * ```ts
1354
+ * import { factorGroup, fieldFactor, quantitativeDefinition } from '@orkestrel/reason'
1355
+ * import { TemplateManager } from '@src/core'
1356
+ *
1357
+ * const manager = new TemplateManager()
1358
+ * const record = manager.add({
1359
+ * id: 't1',
1360
+ * name: 'Arithmetic',
1361
+ * domain: 'arithmetic',
1362
+ * intents: ['calculate'],
1363
+ * mappings: [],
1364
+ * defaults: [],
1365
+ * computations: [],
1366
+ * definition: quantitativeDefinition('t1', 'Arithmetic', [
1367
+ * factorGroup('total', 'sum', [fieldFactor('value', 'value')]),
1368
+ * ]),
1369
+ * })
1370
+ * record.version // 1
1371
+ * manager.size // 1
1372
+ * ```
1373
+ */
1374
+ var TemplateManager = class {
1375
+ #records = /* @__PURE__ */ new Map();
1376
+ #emitter;
1377
+ #destroyed = false;
1378
+ constructor(options) {
1379
+ this.#emitter = new _orkestrel_emitter.Emitter({
1380
+ on: options?.on,
1381
+ error: options?.error
1382
+ });
1383
+ for (const template of options?.templates ?? []) this.add(template);
1384
+ }
1385
+ get emitter() {
1386
+ return this.#emitter;
1387
+ }
1388
+ get size() {
1389
+ this.#ensureAlive();
1390
+ return this.#records.size;
1391
+ }
1392
+ has(id) {
1393
+ this.#ensureAlive();
1394
+ return this.#records.has(id);
1395
+ }
1396
+ template(id) {
1397
+ this.#ensureAlive();
1398
+ return this.#records.get(id);
1399
+ }
1400
+ templates() {
1401
+ this.#ensureAlive();
1402
+ return [...this.#records.values()];
1403
+ }
1404
+ add(template, options) {
1405
+ this.#ensureAlive();
1406
+ const id = options?.id ?? template.id;
1407
+ const hash = digestValue(template);
1408
+ const existing = this.#records.get(id);
1409
+ const record = {
1410
+ id,
1411
+ template,
1412
+ version: existing === void 0 ? 1 : existing.hash === hash ? existing.version : existing.version + 1,
1413
+ hash
1414
+ };
1415
+ this.#records.set(id, record);
1416
+ this.#emitter.emit("add", id);
1417
+ return record;
1418
+ }
1419
+ remove(target) {
1420
+ this.#ensureAlive();
1421
+ if (target === void 0) {
1422
+ for (const id of this.#records.keys()) this.#emitter.emit("remove", id);
1423
+ this.#records.clear();
1424
+ return;
1425
+ }
1426
+ if (typeof target === "string") {
1427
+ const removed = this.#records.delete(target);
1428
+ if (removed) this.#emitter.emit("remove", target);
1429
+ return removed;
1430
+ }
1431
+ for (const id of target) if (!this.#records.has(id)) return false;
1432
+ for (const id of target) {
1433
+ this.#records.delete(id);
1434
+ this.#emitter.emit("remove", id);
1435
+ }
1436
+ return true;
1437
+ }
1438
+ destroy() {
1439
+ if (this.#destroyed) return;
1440
+ this.#records.clear();
1441
+ this.#destroyed = true;
1442
+ this.#emitter.emit("destroy");
1443
+ this.#emitter.destroy();
1444
+ }
1445
+ #ensureAlive() {
1446
+ if (this.#destroyed) throw new InterpretError("DESTROYED", "Template manager has been destroyed");
1447
+ }
1448
+ };
1449
+ //#endregion
1450
+ //#region src/core/Narrator.ts
1451
+ /**
1452
+ * A stateless, TOTAL, lexicon-driven rendering engine for the reverse
1453
+ * direction — the reverse-direction mirror of the forward `Formatter`'s
1454
+ * `verbs` seam (AGENTS §21 mechanism-never-policy).
1455
+ *
1456
+ * @remarks
1457
+ * Every wording decision is DATA — a caller-supplied `Lexicon` merged, per
1458
+ * sub-record (`phrases` / `labels` / `templates`), OVER `DEFAULT_LEXICON`.
1459
+ * Stateless and holds no resources: no emitter, no `destroy()` (deliberate —
1460
+ * there is nothing to release). Every method is total (never throws); a
1461
+ * lexicon or formatter miss degrades to its documented fallback rather than
1462
+ * a thrown error, and every lookup guards with `Object.hasOwn` so an
1463
+ * adversarial key (`toString`, `constructor`, `__proto__`) misses cleanly
1464
+ * instead of reading an inherited prototype member.
1465
+ *
1466
+ * @example
1467
+ * ```ts
1468
+ * import { Narrator } from '@src/core'
1469
+ *
1470
+ * const narrator = new Narrator({
1471
+ * lexicon: { phrases: { comparison: { equals: 'is' } } },
1472
+ * formatters: { money: (value) => `$${String(value)}` },
1473
+ * })
1474
+ * narrator.phrase('comparison', 'equals', 'equals') // 'is'
1475
+ * narrator.phrase('comparison', 'missing', 'equals') // 'equals' — fallback
1476
+ * narrator.value('money', 5) // '$5'
1477
+ * ```
1478
+ */
1479
+ var Narrator = class {
1480
+ #lexicon;
1481
+ #formatters;
1482
+ constructor(options) {
1483
+ this.#lexicon = {
1484
+ phrases: {
1485
+ ...DEFAULT_LEXICON.phrases,
1486
+ ...options?.lexicon?.phrases
1487
+ },
1488
+ labels: {
1489
+ ...DEFAULT_LEXICON.labels,
1490
+ ...options?.lexicon?.labels
1491
+ },
1492
+ templates: {
1493
+ ...DEFAULT_LEXICON.templates,
1494
+ ...options?.lexicon?.templates
1495
+ }
1496
+ };
1497
+ this.#formatters = { ...options?.formatters };
1498
+ }
1499
+ phrase(table, key, fallback) {
1500
+ if (Object.hasOwn(this.#lexicon.phrases, table)) {
1501
+ const row = this.#lexicon.phrases[table];
1502
+ if (row !== null && row !== void 0 && Object.hasOwn(row, key)) {
1503
+ const value = row[key];
1504
+ if (typeof value === "string") return value;
1505
+ }
1506
+ }
1507
+ return fallback ?? key;
1508
+ }
1509
+ label(field) {
1510
+ const key = (0, _orkestrel_reason.formatField)(field);
1511
+ if (Object.hasOwn(this.#lexicon.labels, key)) {
1512
+ const value = this.#lexicon.labels[key];
1513
+ if (typeof value === "string") return value;
1514
+ }
1515
+ return key;
1516
+ }
1517
+ line(id, values) {
1518
+ if (!Object.hasOwn(this.#lexicon.templates, id)) return "";
1519
+ const template = this.#lexicon.templates[id];
1520
+ return typeof template === "string" ? interpolateMessage(template, values) : "";
1521
+ }
1522
+ value(unit, raw) {
1523
+ if (Object.hasOwn(this.#formatters, unit)) {
1524
+ const formatter = this.#formatters[unit];
1525
+ if (typeof formatter === "function") try {
1526
+ return formatter(raw);
1527
+ } catch {
1528
+ return String(raw);
1529
+ }
1530
+ }
1531
+ return String(raw);
1532
+ }
1533
+ describe(definition) {
1534
+ switch (definition.reasoning) {
1535
+ case "quantitative": return this.line("definition.quantitative", {
1536
+ name: definition.name,
1537
+ count: definition.groups.length
1538
+ });
1539
+ case "logical": return this.line("definition.logical", {
1540
+ name: definition.name,
1541
+ count: definition.rules.length,
1542
+ strategy: definition.strategy
1543
+ });
1544
+ case "symbolic": return this.line("definition.symbolic", {
1545
+ name: definition.name,
1546
+ count: definition.equations.length
1547
+ });
1548
+ case "inferential": return this.line("definition.inferential", {
1549
+ name: definition.name,
1550
+ facts: definition.facts.length,
1551
+ inferences: definition.inferences.length,
1552
+ strategy: definition.strategy
1553
+ });
1554
+ }
1555
+ }
1556
+ narrate(result) {
1557
+ switch (result.reasoning) {
1558
+ case "quantitative": {
1559
+ const base = this.line("result.quantitative", {
1560
+ value: result.value,
1561
+ count: result.count
1562
+ });
1563
+ if (result.errors.length === 0) return base;
1564
+ return `${base}${this.line("result.quantitative.failed", { errors: result.errors.join(", ") })}`;
1565
+ }
1566
+ case "logical": return this.line("result.logical", {
1567
+ status: result.conclusion ? "met" : "unmet",
1568
+ count: result.count
1569
+ });
1570
+ case "symbolic": {
1571
+ const solved = Object.entries(result.solutions).map(([key, value]) => `${key}=${value}`).join(", ");
1572
+ return this.line("result.symbolic", { solved });
1573
+ }
1574
+ case "inferential": return this.line("result.inferential", { count: result.derived.length });
1575
+ }
1576
+ }
1577
+ };
1578
+ //#endregion
1579
+ //#region src/core/stages/Clarifier.ts
1580
+ /**
1581
+ * The `Clarifier` stage: resolves same-domain carry-over, template defaults,
1582
+ * and declaratively computed fields against an already-assigned entity set,
1583
+ * surfacing an {@link Ambiguity} for every required mapping that stays
1584
+ * unresolved.
1585
+ *
1586
+ * @remarks
1587
+ * Resolution order: fresh (already-assigned) entities always win; carry-over
1588
+ * fills a mapping only from the SAME domain's most recent prior turn (a
1589
+ * domain change drops carry-over entirely) and never overwrites a fresh
1590
+ * value; template defaults fill any field still unresolved after carry-over;
1591
+ * computed fields resolve in dependency (topological) order via
1592
+ * `resolveExpression` — an unresolved input, a non-finite result, or a
1593
+ * dependency cycle leaves the field a gap, never landing an entity. `floor`
1594
+ * (from {@link ClarifierOptions}, never hardcoded — AGENTS-flagged scsr
1595
+ * defect 4) gates whether a resolved entity's confidence counts as
1596
+ * "resolved enough" when raising ambiguities — a field with a value below
1597
+ * `floor` still raises its ambiguity.
1598
+ *
1599
+ * @example
1600
+ * ```ts
1601
+ * import { Clarifier } from '@src/core'
1602
+ *
1603
+ * const clarifier = new Clarifier({ floor: 0.3 })
1604
+ * clarifier.clarify(
1605
+ * [],
1606
+ * {
1607
+ * id: 't1',
1608
+ * name: 'Arithmetic',
1609
+ * domain: 'arithmetic',
1610
+ * intents: ['calculate'],
1611
+ * mappings: [{ entity: 'value', aliases: [], field: 'value', required: true }],
1612
+ * defaults: [],
1613
+ * computations: [],
1614
+ * definition: { reasoning: 'symbolic', id: 't1', name: 'Arithmetic', equations: [], variables: {} },
1615
+ * },
1616
+ * undefined,
1617
+ * { action: 'calculate', domain: 'arithmetic', confidence: 1 },
1618
+ * ) // { entities: [], ambiguities: [{ field: 'value', ... }], complete: false }
1619
+ * ```
1620
+ */
1621
+ var Clarifier = class {
1622
+ #floor;
1623
+ constructor(options) {
1624
+ this.#floor = options?.floor ?? .3;
1625
+ }
1626
+ clarify(entities, template, context, intent) {
1627
+ const resolved = [...entities];
1628
+ const filledEntityNames = new Set(resolved.map((entity) => entity.name));
1629
+ const filledFields = new Set(resolved.map((entity) => {
1630
+ const mapping = template.mappings.find((candidate) => candidate.entity === entity.name);
1631
+ return mapping === void 0 ? entity.name : (0, _orkestrel_reason.formatField)(mapping.field);
1632
+ }));
1633
+ this.#carryOver(template, context, intent, filledEntityNames, filledFields, resolved);
1634
+ this.#fillDefaults(template, filledFields, resolved);
1635
+ this.#resolveComputations(template, filledFields, resolved);
1636
+ const ambiguities = this.#collectAmbiguities(template, resolved);
1637
+ return {
1638
+ entities: resolved,
1639
+ ambiguities,
1640
+ complete: ambiguities.length === 0
1641
+ };
1642
+ }
1643
+ #carryOver(template, context, intent, filledEntityNames, filledFields, resolved) {
1644
+ if (context === void 0) return;
1645
+ const sameDomain = context.previous().filter((prior) => prior.intent.domain === intent.domain);
1646
+ for (const mapping of template.mappings) {
1647
+ if (filledEntityNames.has(mapping.entity)) continue;
1648
+ for (let index = sameDomain.length - 1; index >= 0; index -= 1) {
1649
+ const found = sameDomain[index]?.entities.find((entity) => entity.name === mapping.entity);
1650
+ if (found === void 0) continue;
1651
+ resolved.push({
1652
+ name: mapping.entity,
1653
+ value: found.value,
1654
+ provenance: { category: "carried" },
1655
+ confidence: CONFIDENCE_CARRIED
1656
+ });
1657
+ filledEntityNames.add(mapping.entity);
1658
+ filledFields.add((0, _orkestrel_reason.formatField)(mapping.field));
1659
+ break;
1660
+ }
1661
+ }
1662
+ }
1663
+ #fillDefaults(template, filledFields, resolved) {
1664
+ for (const fieldDefault of template.defaults) {
1665
+ const key = (0, _orkestrel_reason.formatField)(fieldDefault.field);
1666
+ if (filledFields.has(key)) continue;
1667
+ const mapping = template.mappings.find((candidate) => (0, _orkestrel_reason.formatField)(candidate.field) === key);
1668
+ resolved.push({
1669
+ name: mapping?.entity ?? key,
1670
+ value: fieldDefault.value,
1671
+ provenance: { category: "default" },
1672
+ confidence: 1
1673
+ });
1674
+ filledFields.add(key);
1675
+ }
1676
+ }
1677
+ #resolveComputations(template, filledFields, resolved) {
1678
+ const bindings = {};
1679
+ for (const entity of resolved) {
1680
+ const mapping = template.mappings.find((candidate) => candidate.entity === entity.name);
1681
+ const field = mapping === void 0 ? entity.name : (0, _orkestrel_reason.formatField)(mapping.field);
1682
+ if ((0, _orkestrel_contract.isFiniteNumber)(entity.value)) bindings[field] = entity.value;
1683
+ }
1684
+ for (const computation of this.#orderComputations(template.computations)) {
1685
+ const key = (0, _orkestrel_reason.formatField)(computation.field);
1686
+ if (filledFields.has(key)) continue;
1687
+ const value = resolveExpression(computation.expression, bindings);
1688
+ if (value === void 0) continue;
1689
+ const mapping = template.mappings.find((candidate) => (0, _orkestrel_reason.formatField)(candidate.field) === key);
1690
+ resolved.push({
1691
+ name: mapping?.entity ?? key,
1692
+ value,
1693
+ provenance: { category: "computed" },
1694
+ confidence: CONFIDENCE_COMPUTED
1695
+ });
1696
+ filledFields.add(key);
1697
+ bindings[key] = value;
1698
+ }
1699
+ }
1700
+ #orderComputations(computations) {
1701
+ const byField = /* @__PURE__ */ new Map();
1702
+ for (const computation of computations) byField.set((0, _orkestrel_reason.formatField)(computation.field), computation);
1703
+ const dependents = /* @__PURE__ */ new Map();
1704
+ const inDegree = /* @__PURE__ */ new Map();
1705
+ for (const key of byField.keys()) inDegree.set(key, 0);
1706
+ for (const [key, computation] of byField) {
1707
+ const dependencies = variablesOf(computation.expression).filter((name) => byField.has(name));
1708
+ inDegree.set(key, dependencies.length);
1709
+ for (const dependency of dependencies) {
1710
+ const list = dependents.get(dependency) ?? [];
1711
+ list.push(key);
1712
+ dependents.set(dependency, list);
1713
+ }
1714
+ }
1715
+ const queue = [];
1716
+ for (const [key, degree] of inDegree) if (degree === 0) queue.push(key);
1717
+ const ordered = [];
1718
+ let cursor = 0;
1719
+ while (cursor < queue.length) {
1720
+ const key = queue[cursor];
1721
+ cursor += 1;
1722
+ if (key === void 0) continue;
1723
+ const computation = byField.get(key);
1724
+ if (computation !== void 0) ordered.push(computation);
1725
+ for (const dependent of dependents.get(key) ?? []) {
1726
+ const next = (inDegree.get(dependent) ?? 0) - 1;
1727
+ inDegree.set(dependent, next);
1728
+ if (next === 0) queue.push(dependent);
1729
+ }
1730
+ }
1731
+ return ordered;
1732
+ }
1733
+ #collectAmbiguities(template, resolved) {
1734
+ const ambiguities = [];
1735
+ for (const mapping of template.mappings) {
1736
+ if (mapping.required !== true) continue;
1737
+ const entity = resolved.find((candidate) => candidate.name === mapping.entity);
1738
+ if (entity !== void 0 && entity.confidence >= this.#floor) continue;
1739
+ ambiguities.push({
1740
+ field: mapping.field,
1741
+ question: `What is your ${mapping.entity}?`,
1742
+ candidates: [],
1743
+ required: true
1744
+ });
1745
+ }
1746
+ return ambiguities;
1747
+ }
1748
+ };
1749
+ //#endregion
1750
+ //#region src/core/stages/Extractor.ts
1751
+ /**
1752
+ * The `Extractor` stage: template-agnostic intent classification plus raw
1753
+ * numeric-entity mining.
1754
+ *
1755
+ * @remarks
1756
+ * Deliberately never named `Parser` — the `contracts` module already owns
1757
+ * `Parser<T>`, so a class of that name would collide in type space (AGENTS
1758
+ * §21, design ledger 3). `extract` never sees a `Template`: numbers →
1759
+ * entity ASSIGNMENT is a separate orchestrator-owned step that runs only
1760
+ * after a template has matched (`assignEntities` in `helpers.ts`), not
1761
+ * inside this stage (the defect-3 fix — scsr's parser mined template-shaped
1762
+ * entities directly and only worked via an `instanceof` hack).
1763
+ *
1764
+ * @example
1765
+ * ```ts
1766
+ * import { Extractor } from '@src/core'
1767
+ *
1768
+ * const extractor = new Extractor({
1769
+ * actions: { calculate: 'compute' },
1770
+ * domains: { rating: ['rate'] },
1771
+ * })
1772
+ * extractor.extract('calculate my rate at 85')
1773
+ * // { intent: { action: 'compute', domain: 'rating', confidence: 1 }, numbers: [85], complete: true }
1774
+ * ```
1775
+ */
1776
+ var Extractor = class {
1777
+ #actions;
1778
+ #domains;
1779
+ constructor(options) {
1780
+ this.#actions = {
1781
+ ...DEFAULT_ACTIONS,
1782
+ ...options?.actions
1783
+ };
1784
+ this.#domains = {
1785
+ ...DEFAULT_DOMAINS,
1786
+ ...options?.domains
1787
+ };
1788
+ }
1789
+ extract(text) {
1790
+ const numbers = extractNumbers(text);
1791
+ const intent = classifyIntent(text, this.#actions, this.#domains);
1792
+ return {
1793
+ intent,
1794
+ numbers,
1795
+ complete: numbers.length > 0 && intent.confidence > 0
1796
+ };
1797
+ }
1798
+ };
1799
+ //#endregion
1800
+ //#region src/core/stages/Formatter.ts
1801
+ /**
1802
+ * The `Formatter` stage: renders the refined natural-language prompt for a
1803
+ * matched template.
1804
+ *
1805
+ * @remarks
1806
+ * Shape: `{verb} {template.name}` + ` with {label}: {value}, …` for every
1807
+ * non-default entity (via the core-root `formatField`) + `
1808
+ * (defaults: {label}: {value}, …)` for default-provenance entities + `
1809
+ * needed: {question} …` for every ambiguity. `verbs` maps `intent.action` to
1810
+ * its display verb; an action absent from the map falls back to the action
1811
+ * string itself. Every parameter is read — no ignored `context` argument
1812
+ * (AGENTS-flagged scsr defect 5).
1813
+ *
1814
+ * @example
1815
+ * ```ts
1816
+ * import { Formatter } from '@src/core'
1817
+ *
1818
+ * const formatter = new Formatter({ verbs: { calculate: 'Calculate' } })
1819
+ * formatter.format(
1820
+ * { action: 'calculate', domain: 'arithmetic', confidence: 1 },
1821
+ * {
1822
+ * id: 't1',
1823
+ * name: 'Arithmetic',
1824
+ * domain: 'arithmetic',
1825
+ * intents: ['calculate'],
1826
+ * mappings: [],
1827
+ * defaults: [],
1828
+ * computations: [],
1829
+ * definition: { reasoning: 'symbolic', id: 't1', name: 'Arithmetic', equations: [], variables: {} },
1830
+ * },
1831
+ * [],
1832
+ * [],
1833
+ * ) // { prompt: 'Calculate Arithmetic' }
1834
+ * ```
1835
+ */
1836
+ var Formatter = class {
1837
+ #verbs;
1838
+ constructor(options) {
1839
+ this.#verbs = {
1840
+ ...DEFAULT_VERBS,
1841
+ ...options?.verbs
1842
+ };
1843
+ }
1844
+ format(intent, template, entities, ambiguities) {
1845
+ const verb = this.#verbs[intent.action] ?? intent.action;
1846
+ const render = (list) => list.map((entity) => `${(0, _orkestrel_reason.formatField)(entity.name)}: ${String(entity.value)}`).join(", ");
1847
+ let prompt = `${verb} ${template.name}`;
1848
+ const resolved = entities.filter((entity) => entity.provenance.category !== "default");
1849
+ if (resolved.length > 0) prompt += ` with ${render(resolved)}`;
1850
+ const defaults = entities.filter((entity) => entity.provenance.category === "default");
1851
+ if (defaults.length > 0) prompt += ` (defaults: ${render(defaults)})`;
1852
+ if (ambiguities.length > 0) prompt += ` needed: ${ambiguities.map((ambiguity) => ambiguity.question).join(" ")}`;
1853
+ return { prompt };
1854
+ }
1855
+ };
1856
+ //#endregion
1857
+ //#region src/core/stages/Generator.ts
1858
+ /**
1859
+ * The `Generator` stage: builds the final `Subject` from a fully resolved
1860
+ * entity set, plus its complete field audit.
1861
+ *
1862
+ * @remarks
1863
+ * `entity → field` via `template.mappings` (an `EntityMapping.entity` name
1864
+ * lookup); an entity whose name matches no mapping lands on the field named
1865
+ * by its OWN `name` — the shape `Clarifier` uses for its synthesized
1866
+ * default/computed entities, so one lookup rule serves both extraction-
1867
+ * mapped and template-data-derived fields. A single-element array value
1868
+ * unwraps to its scalar; a multi-element, ALL-numeric array value stays an
1869
+ * array AND additionally emits five aggregate fields (`Sum` / `Count` /
1870
+ * `Average` / `Minimum` / `Maximum`, provenance `computed`), each derived as
1871
+ * a SIBLING path beside the source field via `deriveAggregateField` — for an
1872
+ * array `FieldPath` (e.g. `['address', 'amounts']`) the aggregates nest
1873
+ * (`['address', 'amountsSum']`, ...); for a plain string field they stay
1874
+ * flat (`'amountsSum'`), matching the source field's own shape.
1875
+ * `confidence` is the mean of the input entities' own confidences (`0` for
1876
+ * an empty entity set). A `FieldMapping` is emitted for EVERY field that
1877
+ * lands on the subject, including defaults, computed fields, and aggregates
1878
+ * — scsr silently omitted defaults/computed from its audit trail; this
1879
+ * closes that gap. `GeneratorOptions` is a reserved extension seam — the
1880
+ * stage has no knobs yet, so construction takes no arguments until one
1881
+ * exists.
1882
+ *
1883
+ * @example
1884
+ * ```ts
1885
+ * import { Generator } from '@src/core'
1886
+ *
1887
+ * const generator = new Generator()
1888
+ * generator.generate(
1889
+ * [
1890
+ * {
1891
+ * name: 'value',
1892
+ * value: 42,
1893
+ * provenance: { category: 'extracted', detail: 'collect' },
1894
+ * confidence: 0.9,
1895
+ * },
1896
+ * ],
1897
+ * {
1898
+ * id: 't1',
1899
+ * name: 'Arithmetic',
1900
+ * domain: 'arithmetic',
1901
+ * intents: ['calculate'],
1902
+ * mappings: [{ entity: 'value', aliases: [], field: 'value' }],
1903
+ * defaults: [],
1904
+ * computations: [],
1905
+ * definition: { reasoning: 'symbolic', id: 't1', name: 'Arithmetic', equations: [], variables: {} },
1906
+ * },
1907
+ * ) // { subject: { value: 42 }, mappings: [...], confidence: 0.9, ... }
1908
+ * ```
1909
+ */
1910
+ var Generator = class {
1911
+ generate(entities, template) {
1912
+ let subject = {};
1913
+ const mappings = [];
1914
+ for (const entity of entities) {
1915
+ const mapping = template.mappings.find((candidate) => candidate.entity === entity.name);
1916
+ const field = mapping === void 0 ? entity.name : mapping.field;
1917
+ const value = entity.value;
1918
+ if (Array.isArray(value) && value.length === 1) {
1919
+ const scalar = value[0];
1920
+ subject = setField(subject, field, scalar);
1921
+ mappings.push({
1922
+ field,
1923
+ entity: entity.name,
1924
+ value: scalar,
1925
+ provenance: entity.provenance,
1926
+ confidence: entity.confidence
1927
+ });
1928
+ continue;
1929
+ }
1930
+ if (Array.isArray(value) && value.length > 1) {
1931
+ const numeric = [];
1932
+ for (const item of value) {
1933
+ if (!(0, _orkestrel_contract.isFiniteNumber)(item)) break;
1934
+ numeric.push(item);
1935
+ }
1936
+ if (numeric.length === value.length) {
1937
+ subject = setField(subject, field, value);
1938
+ mappings.push({
1939
+ field,
1940
+ entity: entity.name,
1941
+ value,
1942
+ provenance: entity.provenance,
1943
+ confidence: entity.confidence
1944
+ });
1945
+ const sum = numeric.reduce((total, item) => total + item, 0);
1946
+ const minimum = numeric.reduce((min, item) => item < min ? item : min, numeric[0]);
1947
+ const maximum = numeric.reduce((max, item) => item > max ? item : max, numeric[0]);
1948
+ const aggregates = [
1949
+ [deriveAggregateField(field, "Sum"), sum],
1950
+ [deriveAggregateField(field, "Count"), numeric.length],
1951
+ [deriveAggregateField(field, "Average"), sum / numeric.length],
1952
+ [deriveAggregateField(field, "Minimum"), minimum],
1953
+ [deriveAggregateField(field, "Maximum"), maximum]
1954
+ ];
1955
+ for (const [aggregateField, aggregateValue] of aggregates) {
1956
+ subject = setField(subject, aggregateField, aggregateValue);
1957
+ mappings.push({
1958
+ field: aggregateField,
1959
+ value: aggregateValue,
1960
+ provenance: { category: "computed" },
1961
+ confidence: CONFIDENCE_COMPUTED
1962
+ });
1963
+ }
1964
+ continue;
1965
+ }
1966
+ }
1967
+ subject = setField(subject, field, value);
1968
+ mappings.push({
1969
+ field,
1970
+ entity: entity.name,
1971
+ value,
1972
+ provenance: entity.provenance,
1973
+ confidence: entity.confidence
1974
+ });
1975
+ }
1976
+ const confidence = entities.length === 0 ? 0 : entities.reduce((total, entity) => total + entity.confidence, 0) / entities.length;
1977
+ return {
1978
+ subject,
1979
+ definition: template.definition,
1980
+ mappings,
1981
+ confidence
1982
+ };
1983
+ }
1984
+ };
1985
+ //#endregion
1986
+ //#region src/core/stages/Normalizer.ts
1987
+ /**
1988
+ * The `Normalizer` stage: applies contraction, abbreviation, and correction
1989
+ * substitutions in order, then collapses whitespace.
1990
+ *
1991
+ * @remarks
1992
+ * Each caller map is merged OVER the neutral built-in default for its slot —
1993
+ * `text` is never mutated (AGENTS §11). Every substitution actually applied
1994
+ * (one entry per matching map KEY, not per occurrence) is recorded on the
1995
+ * result's `changes`, in `contractions → abbreviations → corrections` order,
1996
+ * so the audit trail explains every character difference between `text` and
1997
+ * `NormalizeResult.text` (the final whitespace collapse carries no entry of
1998
+ * its own — it is structural cleanup, not a substitution).
1999
+ *
2000
+ * @example
2001
+ * ```ts
2002
+ * import { Normalizer } from '@src/core'
2003
+ *
2004
+ * const normalizer = new Normalizer({ contractions: { "can't": 'cannot' } })
2005
+ * normalizer.normalize("can't stop")
2006
+ * // { text: 'cannot stop', changes: [{ from: "can't", to: 'cannot' }] }
2007
+ * ```
2008
+ */
2009
+ var Normalizer = class {
2010
+ #contractions;
2011
+ #abbreviations;
2012
+ #corrections;
2013
+ constructor(options) {
2014
+ this.#contractions = {
2015
+ ...DEFAULT_CONTRACTIONS,
2016
+ ...options?.contractions
2017
+ };
2018
+ this.#abbreviations = {
2019
+ ...DEFAULT_ABBREVIATIONS,
2020
+ ...options?.abbreviations
2021
+ };
2022
+ this.#corrections = {
2023
+ ...DEFAULT_CORRECTIONS,
2024
+ ...options?.corrections
2025
+ };
2026
+ }
2027
+ normalize(text) {
2028
+ const changes = [];
2029
+ let working = text;
2030
+ for (const map of [
2031
+ this.#contractions,
2032
+ this.#abbreviations,
2033
+ this.#corrections
2034
+ ]) {
2035
+ const applied = this.#applyStage(working, map);
2036
+ working = applied.text;
2037
+ changes.push(...applied.changes);
2038
+ }
2039
+ return {
2040
+ text: collapseWhitespace(working),
2041
+ changes
2042
+ };
2043
+ }
2044
+ #applyStage(text, map) {
2045
+ let working = text;
2046
+ const changes = [];
2047
+ for (const [from, to] of Object.entries(map)) if (new RegExp(`\\b${escapeRegExp(from)}\\b`, "i").test(working)) {
2048
+ working = applyReplacements(working, { [from]: to });
2049
+ changes.push({
2050
+ from,
2051
+ to
2052
+ });
2053
+ }
2054
+ return {
2055
+ text: working,
2056
+ changes
2057
+ };
2058
+ }
2059
+ };
2060
+ //#endregion
2061
+ //#region src/core/Interpret.ts
2062
+ /**
2063
+ * The interpretation orchestrator — the sole public entry point of the
2064
+ * `interprets` module, mirroring the reasons `Reason` orchestrator shape.
2065
+ *
2066
+ * @remarks
2067
+ * `interpret()` is genuinely SYNCHRONOUS (scsr's was fake-async with zero
2068
+ * `await`s) and runs a fixed five-stage pipeline —
2069
+ * `[normalize, extract, clarify, format, generate]` — each producing one
2070
+ * {@link StageRecord}. Between `extract` and `clarify` the orchestrator matches
2071
+ * the classified {@link Intent} against its registered {@link Template}s and,
2072
+ * on a match, assigns the extracted numbers to that template's mappings
2073
+ * (`assignEntities`) — a template-owned step, not a sixth stage. No match, or a
2074
+ * matched template whose intent confidence falls below the configured `floor`,
2075
+ * yields an explicit, auditable INCOMPLETE result (a `field: 'intent'`
2076
+ * ambiguity, absent subject/definition) rather than scsr's arbitrary
2077
+ * `templates[0]` fallback. A stage THROW is caught, marked on its record AND on
2078
+ * `failures`, emitted as `error`, and still yields a visible incomplete result
2079
+ * — never a silent fallback. Every result carries a `digest` over its original
2080
+ * text plus the matched template id/version and the built subject/definition,
2081
+ * so re-running the same text against the same template version reproduces the
2082
+ * same digest (the replay contract). `describe` / `narrate` are the reverse
2083
+ * direction (structure → prose). `destroy()` is idempotent, tears down the
2084
+ * registry and context, then destroys the emitter LAST (AGENTS §13); every
2085
+ * method afterwards except the {@link emitter} getter throws
2086
+ * `InterpretError('DESTROYED', …)`.
2087
+ *
2088
+ * @example
2089
+ * ```ts
2090
+ * import { factorGroup, fieldFactor, quantitativeDefinition } from '@orkestrel/reason'
2091
+ * import { Interpret, Extractor } from '@src/core'
2092
+ *
2093
+ * const interpret = new Interpret({
2094
+ * extractor: new Extractor({ actions: { calculate: 'calculate' }, domains: { arithmetic: ['arithmetic'] } }),
2095
+ * templates: [
2096
+ * {
2097
+ * id: 't1',
2098
+ * name: 'Arithmetic',
2099
+ * domain: 'arithmetic',
2100
+ * intents: ['calculate'],
2101
+ * mappings: [{ entity: 'value', aliases: [], field: 'value' }],
2102
+ * defaults: [],
2103
+ * computations: [],
2104
+ * definition: quantitativeDefinition('t1', 'Arithmetic', [
2105
+ * factorGroup('total', 'sum', [fieldFactor('value', 'value')]),
2106
+ * ]),
2107
+ * },
2108
+ * ],
2109
+ * })
2110
+ * interpret.interpret('calculate arithmetic 42').subject // { value: 42 }
2111
+ * ```
2112
+ */
2113
+ var Interpret = class {
2114
+ #templates;
2115
+ #context;
2116
+ #normalizer;
2117
+ #extractor;
2118
+ #clarifier;
2119
+ #formatter;
2120
+ #generator;
2121
+ #similarity;
2122
+ #floor;
2123
+ #narrator;
2124
+ #emitter;
2125
+ #destroyed = false;
2126
+ constructor(options) {
2127
+ this.#similarity = options?.similarity ?? .8;
2128
+ this.#floor = options?.floor ?? .3;
2129
+ this.#templates = new TemplateManager({ templates: options?.templates });
2130
+ this.#context = options?.context ?? new InterpretContext({ history: options?.history });
2131
+ this.#normalizer = options?.normalizer ?? new Normalizer();
2132
+ this.#extractor = options?.extractor ?? new Extractor();
2133
+ this.#clarifier = options?.clarifier ?? new Clarifier({ floor: this.#floor });
2134
+ this.#formatter = options?.formatter ?? new Formatter();
2135
+ this.#generator = options?.generator ?? new Generator();
2136
+ this.#narrator = new Narrator({
2137
+ lexicon: options?.lexicon,
2138
+ formatters: options?.formatters
2139
+ });
2140
+ this.#emitter = new _orkestrel_emitter.Emitter({
2141
+ on: options?.on,
2142
+ error: options?.error
2143
+ });
2144
+ }
2145
+ get emitter() {
2146
+ return this.#emitter;
2147
+ }
2148
+ interpret(text) {
2149
+ this.#ensureAlive();
2150
+ const stages = [];
2151
+ let normalized;
2152
+ try {
2153
+ normalized = this.#normalizer.normalize(text);
2154
+ } catch (error) {
2155
+ return this.#abort(text, text, {
2156
+ action: "",
2157
+ domain: "",
2158
+ confidence: 0
2159
+ }, [], [], stages, "normalize", "NORMALIZE_FAILED", text, error, [
2160
+ "extract",
2161
+ "clarify",
2162
+ "format",
2163
+ "generate"
2164
+ ], void 0, void 0);
2165
+ }
2166
+ stages.push({
2167
+ stage: "normalize",
2168
+ input: text,
2169
+ output: normalized,
2170
+ failed: false
2171
+ });
2172
+ let extract;
2173
+ try {
2174
+ extract = this.#extractor.extract(normalized.text);
2175
+ } catch (error) {
2176
+ return this.#abort(text, normalized.text, {
2177
+ action: "",
2178
+ domain: "",
2179
+ confidence: 0
2180
+ }, [], [], stages, "extract", "EXTRACT_FAILED", normalized.text, error, [
2181
+ "clarify",
2182
+ "format",
2183
+ "generate"
2184
+ ], void 0, void 0);
2185
+ }
2186
+ stages.push({
2187
+ stage: "extract",
2188
+ input: normalized.text,
2189
+ output: extract,
2190
+ failed: false
2191
+ });
2192
+ const intent = extract.intent;
2193
+ const matched = matchTemplate(intent, this.#templates.templates().map((record) => record.template), this.#floor);
2194
+ if (matched === void 0) return this.#gate(text, normalized.text, intent, [], stages, "NO_TEMPLATE", void 0, void 0);
2195
+ const assigned = assignEntities(extract.numbers, matched.mappings, normalized.text, this.#similarity);
2196
+ const version = this.#templates.template(matched.id)?.version;
2197
+ if (intent.confidence < this.#floor) return this.#gate(text, normalized.text, intent, assigned, stages, "LOW_CONFIDENCE", matched.id, version);
2198
+ let clarified;
2199
+ try {
2200
+ clarified = this.#clarifier.clarify(assigned, matched, this.#context, intent);
2201
+ } catch (error) {
2202
+ return this.#abort(text, normalized.text, intent, assigned, [], stages, "clarify", "CLARIFY_FAILED", assigned, error, ["format", "generate"], matched.id, version);
2203
+ }
2204
+ stages.push({
2205
+ stage: "clarify",
2206
+ input: assigned,
2207
+ output: clarified,
2208
+ failed: false
2209
+ });
2210
+ const formatInput = {
2211
+ intent,
2212
+ entities: clarified.entities,
2213
+ ambiguities: clarified.ambiguities
2214
+ };
2215
+ let formatted;
2216
+ try {
2217
+ formatted = this.#formatter.format(intent, matched, clarified.entities, clarified.ambiguities);
2218
+ } catch (error) {
2219
+ return this.#abort(text, normalized.text, intent, clarified.entities, clarified.ambiguities, stages, "format", "FORMAT_FAILED", formatInput, error, ["generate"], matched.id, version);
2220
+ }
2221
+ stages.push({
2222
+ stage: "format",
2223
+ input: formatInput,
2224
+ output: formatted,
2225
+ failed: false
2226
+ });
2227
+ let generated;
2228
+ try {
2229
+ generated = this.#generator.generate(clarified.entities, matched);
2230
+ } catch (error) {
2231
+ return this.#abort(text, normalized.text, intent, clarified.entities, clarified.ambiguities, stages, "generate", "GENERATE_FAILED", clarified.entities, error, [], matched.id, version);
2232
+ }
2233
+ stages.push({
2234
+ stage: "generate",
2235
+ input: clarified.entities,
2236
+ output: generated,
2237
+ failed: false
2238
+ });
2239
+ const digest = digestValue({
2240
+ text,
2241
+ templateId: matched.id,
2242
+ templateVersion: version,
2243
+ subject: generated.subject,
2244
+ definition: generated.definition
2245
+ });
2246
+ const result = {
2247
+ text,
2248
+ normalized: normalized.text,
2249
+ intent,
2250
+ entities: clarified.entities,
2251
+ subject: generated.subject,
2252
+ definition: generated.definition,
2253
+ mappings: generated.mappings,
2254
+ ambiguities: clarified.ambiguities,
2255
+ prompt: formatted.prompt,
2256
+ stages,
2257
+ failures: [],
2258
+ complete: clarified.complete,
2259
+ confidence: generated.confidence,
2260
+ digest
2261
+ };
2262
+ this.#context.subjects.add(generated.subject);
2263
+ this.#context.definitions.add(generated.definition, { id: generated.definition.id });
2264
+ this.#context.add(result);
2265
+ this.#emitter.emit("interpret", result);
2266
+ return result;
2267
+ }
2268
+ register(template) {
2269
+ this.#ensureAlive();
2270
+ this.#templates.add(template, { id: template.id });
2271
+ this.#emitter.emit("register", template.id);
2272
+ }
2273
+ unregister(id) {
2274
+ this.#ensureAlive();
2275
+ return this.#templates.remove(id);
2276
+ }
2277
+ template(id) {
2278
+ this.#ensureAlive();
2279
+ return this.#templates.template(id)?.template;
2280
+ }
2281
+ templates() {
2282
+ this.#ensureAlive();
2283
+ return this.#templates.templates().map((record) => record.template);
2284
+ }
2285
+ describe(definition) {
2286
+ this.#ensureAlive();
2287
+ return this.#narrator.describe(definition);
2288
+ }
2289
+ narrate(result) {
2290
+ this.#ensureAlive();
2291
+ return this.#narrator.narrate(result);
2292
+ }
2293
+ destroy() {
2294
+ if (this.#destroyed) return;
2295
+ this.#templates.destroy();
2296
+ this.#context.destroy();
2297
+ this.#destroyed = true;
2298
+ this.#emitter.emit("destroy");
2299
+ this.#emitter.destroy();
2300
+ }
2301
+ #gate(text, normalized, intent, entities, stages, code, templateId, templateVersion) {
2302
+ const domains = [...new Set(this.#templates.templates().map((record) => record.template.domain))];
2303
+ const ambiguity = {
2304
+ field: "intent",
2305
+ question: code === "NO_TEMPLATE" ? "Which domain and action did you mean?" : "Which did you mean? The intent was too weak to act on.",
2306
+ candidates: domains,
2307
+ required: true
2308
+ };
2309
+ const failure = {
2310
+ stage: "clarify",
2311
+ code,
2312
+ message: code === "NO_TEMPLATE" ? "No registered template matched the classified intent" : "Classified intent confidence fell below the configured floor"
2313
+ };
2314
+ return this.#assemble(text, normalized, intent, entities, [ambiguity], stages, [failure], [
2315
+ "clarify",
2316
+ "format",
2317
+ "generate"
2318
+ ], templateId, templateVersion);
2319
+ }
2320
+ #abort(text, normalized, intent, entities, ambiguities, stages, stage, code, input, error, remaining, templateId, templateVersion) {
2321
+ const message = error instanceof Error ? error.message : String(error);
2322
+ stages.push({
2323
+ stage,
2324
+ input,
2325
+ output: void 0,
2326
+ failed: true,
2327
+ error: message
2328
+ });
2329
+ this.#emitter.emit("error", error);
2330
+ return this.#assemble(text, normalized, intent, entities, ambiguities, stages, [{
2331
+ stage,
2332
+ code,
2333
+ message
2334
+ }], remaining, templateId, templateVersion);
2335
+ }
2336
+ #assemble(text, normalized, intent, entities, ambiguities, stages, failures, remaining, templateId, templateVersion) {
2337
+ for (const stage of remaining) stages.push({
2338
+ stage,
2339
+ input: void 0,
2340
+ output: void 0,
2341
+ failed: false
2342
+ });
2343
+ const digest = digestValue({
2344
+ text,
2345
+ templateId,
2346
+ templateVersion,
2347
+ subject: void 0,
2348
+ definition: void 0
2349
+ });
2350
+ const result = {
2351
+ text,
2352
+ normalized,
2353
+ intent,
2354
+ entities,
2355
+ mappings: [],
2356
+ ambiguities,
2357
+ prompt: "",
2358
+ stages,
2359
+ failures: [...failures],
2360
+ complete: false,
2361
+ confidence: 0,
2362
+ digest
2363
+ };
2364
+ this.#context.add(result);
2365
+ this.#emitter.emit("interpret", result);
2366
+ return result;
2367
+ }
2368
+ #ensureAlive() {
2369
+ if (this.#destroyed) throw new InterpretError("DESTROYED", "Interpret has been destroyed");
2370
+ }
2371
+ };
2372
+ //#endregion
2373
+ //#region src/core/factories.ts
2374
+ /**
2375
+ * Create an interpretation orchestrator.
2376
+ *
2377
+ * @remarks
2378
+ * `interpret()` is genuinely synchronous and runs the fixed five-stage
2379
+ * pipeline `[normalize, extract, clarify, format, generate]`. Every stage
2380
+ * slot (`normalizer` / `extractor` / `clarifier` / `formatter` / `generator`)
2381
+ * is bring-your-own — a supplied implementation is used as-is, else the
2382
+ * built-in stage is constructed from the matching per-stage options.
2383
+ *
2384
+ * @param options - Optional templates, context, stage implementations, the
2385
+ * `similarity` / `floor` axes, and emitter hooks
2386
+ * @returns A working {@link InterpretInterface}
2387
+ *
2388
+ * @example
2389
+ * ```ts
2390
+ * import { factorGroup, fieldFactor, quantitativeDefinition } from '@orkestrel/reason'
2391
+ * import { createInterpret } from '@src/core'
2392
+ *
2393
+ * const interpret = createInterpret({
2394
+ * extractor: { extract: () => ({ intent: { action: 'calculate', domain: 'arithmetic', confidence: 1 }, numbers: [42], complete: true }) },
2395
+ * templates: [
2396
+ * {
2397
+ * id: 't1',
2398
+ * name: 'Arithmetic',
2399
+ * domain: 'arithmetic',
2400
+ * intents: ['calculate'],
2401
+ * mappings: [{ entity: 'value', aliases: [], field: 'value' }],
2402
+ * defaults: [],
2403
+ * computations: [],
2404
+ * definition: quantitativeDefinition('t1', 'Arithmetic', [
2405
+ * factorGroup('total', 'sum', [fieldFactor('value', 'value')]),
2406
+ * ]),
2407
+ * },
2408
+ * ],
2409
+ * })
2410
+ * interpret.interpret('calculate arithmetic 42').subject // { value: 42 }
2411
+ * ```
2412
+ */
2413
+ function createInterpret(options) {
2414
+ return new Interpret(options);
2415
+ }
2416
+ /**
2417
+ * Create a text normalizer.
2418
+ *
2419
+ * @param options - Optional contraction / abbreviation / correction maps,
2420
+ * merged over the neutral built-in defaults
2421
+ * @returns A stateless {@link NormalizerInterface}
2422
+ *
2423
+ * @example
2424
+ * ```ts
2425
+ * import { createNormalizer } from '@src/core'
2426
+ *
2427
+ * createNormalizer().normalize("it's cold") // { text: 'it is cold', changes: [{ from: "it's", to: 'it is' }] }
2428
+ * ```
2429
+ */
2430
+ function createNormalizer(options) {
2431
+ return new Normalizer(options);
2432
+ }
2433
+ /**
2434
+ * Create a template-agnostic intent classifier and number extractor.
2435
+ *
2436
+ * @param options - Optional caller `actions` / `domains` vocabularies
2437
+ * @returns A stateless {@link ExtractorInterface}
2438
+ *
2439
+ * @example
2440
+ * ```ts
2441
+ * import { createExtractor } from '@src/core'
2442
+ *
2443
+ * const extractor = createExtractor({
2444
+ * actions: { calculate: 'calculate' },
2445
+ * domains: { arithmetic: ['arithmetic'] },
2446
+ * })
2447
+ * extractor.extract('calculate arithmetic 42').numbers // [42]
2448
+ * ```
2449
+ */
2450
+ function createExtractor(options) {
2451
+ return new Extractor(options);
2452
+ }
2453
+ /**
2454
+ * Create a clarifier — carry-over, defaults, and computed-field resolution
2455
+ * against an assigned entity set.
2456
+ *
2457
+ * @param options - Optional confidence `floor` for raised ambiguities
2458
+ * @returns A stateless {@link ClarifierInterface}
2459
+ *
2460
+ * @example
2461
+ * ```ts
2462
+ * import { createClarifier } from '@src/core'
2463
+ *
2464
+ * const clarifier = createClarifier({ floor: 0.5 })
2465
+ * ```
2466
+ */
2467
+ function createClarifier(options) {
2468
+ return new Clarifier(options);
2469
+ }
2470
+ /**
2471
+ * Create a prompt formatter.
2472
+ *
2473
+ * @param options - Optional caller intent-verb phrasing map
2474
+ * @returns A stateless {@link FormatterInterface}
2475
+ *
2476
+ * @example
2477
+ * ```ts
2478
+ * import { createFormatter } from '@src/core'
2479
+ *
2480
+ * const formatter = createFormatter({ verbs: { calculate: 'Calculate' } })
2481
+ * ```
2482
+ */
2483
+ function createFormatter(options) {
2484
+ return new Formatter(options);
2485
+ }
2486
+ /**
2487
+ * Create a subject/definition generator.
2488
+ *
2489
+ * @param options - Currently an empty extension seam
2490
+ * @returns A stateless {@link GeneratorInterface}
2491
+ *
2492
+ * @example
2493
+ * ```ts
2494
+ * import { createGenerator } from '@src/core'
2495
+ *
2496
+ * const generator = createGenerator()
2497
+ * ```
2498
+ */
2499
+ function createGenerator(_options) {
2500
+ return new Generator();
2501
+ }
2502
+ /**
2503
+ * Create a template registry.
2504
+ *
2505
+ * @param options - Optional initial seed collection
2506
+ * @returns A working {@link TemplateManagerInterface}
2507
+ *
2508
+ * @example
2509
+ * ```ts
2510
+ * import { factorGroup, fieldFactor, quantitativeDefinition } from '@orkestrel/reason'
2511
+ * import { createTemplate, createTemplateManager } from '@src/core'
2512
+ *
2513
+ * const template = createTemplate({
2514
+ * id: 't1', name: 'Arithmetic', domain: 'arithmetic', intents: ['calculate'],
2515
+ * mappings: [{ entity: 'value', aliases: [], field: 'value' }], defaults: [], computations: [],
2516
+ * definition: quantitativeDefinition('t1', 'Arithmetic', [factorGroup('total', 'sum', [fieldFactor('value', 'value')])]),
2517
+ * })
2518
+ * const templates = createTemplateManager({ templates: [template] })
2519
+ * templates.size // 1
2520
+ * ```
2521
+ */
2522
+ function createTemplateManager(options) {
2523
+ return new TemplateManager(options);
2524
+ }
2525
+ /**
2526
+ * Create a subject registry.
2527
+ *
2528
+ * @remarks
2529
+ * Mints its own record ids on `add` when none is supplied — a `Subject`
2530
+ * carries no `id` field of its own.
2531
+ *
2532
+ * @param options - Optional initial seed collection
2533
+ * @returns A working {@link SubjectManagerInterface}
2534
+ *
2535
+ * @example
2536
+ * ```ts
2537
+ * import { createSubjectManager } from '@src/core'
2538
+ *
2539
+ * const subjects = createSubjectManager({ subjects: [{ value: 1 }] })
2540
+ * subjects.size // 1
2541
+ * ```
2542
+ */
2543
+ function createSubjectManager(options) {
2544
+ return new SubjectManager(options);
2545
+ }
2546
+ /**
2547
+ * Create a definition registry.
2548
+ *
2549
+ * @param options - Optional initial seed collection
2550
+ * @returns A working {@link DefinitionManagerInterface}
2551
+ *
2552
+ * @example
2553
+ * ```ts
2554
+ * import { factorGroup, fieldFactor, quantitativeDefinition } from '@orkestrel/reason'
2555
+ * import { createDefinitionManager } from '@src/core'
2556
+ *
2557
+ * const definitions = createDefinitionManager({
2558
+ * definitions: [quantitativeDefinition('d1', 'D1', [factorGroup('total', 'sum', [fieldFactor('value', 'value')])])],
2559
+ * })
2560
+ * definitions.size // 1
2561
+ * ```
2562
+ */
2563
+ function createDefinitionManager(options) {
2564
+ return new DefinitionManager(options);
2565
+ }
2566
+ /**
2567
+ * Create a cross-turn interpretation context.
2568
+ *
2569
+ * @param options - Optional `session` label and `history` ring-buffer cap
2570
+ * @returns A working {@link InterpretContextInterface}
2571
+ *
2572
+ * @example
2573
+ * ```ts
2574
+ * import { createInterpretContext } from '@src/core'
2575
+ *
2576
+ * const context = createInterpretContext({ session: 'turn-1', history: 4 })
2577
+ * context.previous() // []
2578
+ * ```
2579
+ */
2580
+ function createInterpretContext(options) {
2581
+ return new InterpretContext(options);
2582
+ }
2583
+ /**
2584
+ * Build and validate one interpretation template from plain data.
2585
+ *
2586
+ * @remarks
2587
+ * The factory/coercer pair for template intake (AGENTS §4.6.1): this throws
2588
+ * on malformed data, while `parseTemplate` returns `undefined`. Data failing
2589
+ * {@link isTemplate} throws `InterpretError('INVALID_TEMPLATE', …)`.
2590
+ *
2591
+ * @param data - The candidate template data
2592
+ * @returns The same data, now known to satisfy {@link Template}
2593
+ * @throws {@link InterpretError} `INVALID_TEMPLATE` when `data` fails validation
2594
+ *
2595
+ * @example
2596
+ * ```ts
2597
+ * import { factorGroup, fieldFactor, quantitativeDefinition } from '@orkestrel/reason'
2598
+ * import { createTemplate } from '@src/core'
2599
+ *
2600
+ * const template = createTemplate({
2601
+ * id: 't1', name: 'Arithmetic', domain: 'arithmetic', intents: ['calculate'],
2602
+ * mappings: [{ entity: 'value', aliases: [], field: 'value' }], defaults: [], computations: [],
2603
+ * definition: quantitativeDefinition('t1', 'Arithmetic', [factorGroup('total', 'sum', [fieldFactor('value', 'value')])]),
2604
+ * })
2605
+ * template.id // 't1'
2606
+ * ```
2607
+ */
2608
+ function createTemplate(data) {
2609
+ if (!isTemplate(data)) throw new InterpretError("INVALID_TEMPLATE", "Template data failed validation");
2610
+ return data;
2611
+ }
2612
+ /**
2613
+ * Create a lexicon-driven reverse-direction rendering engine.
2614
+ *
2615
+ * @remarks
2616
+ * Stateless — `phrase` / `label` / `line` / `value` are total lookups into a
2617
+ * caller `Lexicon` merged over `DEFAULT_LEXICON`, and `describe` / `narrate`
2618
+ * compose them over a reasons `Definition` / `ReasonResult`.
2619
+ *
2620
+ * @param options - Optional `lexicon` and `formatters` map
2621
+ * @returns A stateless {@link NarratorInterface}
2622
+ *
2623
+ * @example
2624
+ * ```ts
2625
+ * import { createNarrator } from '@src/core'
2626
+ *
2627
+ * const narrator = createNarrator({ lexicon: { templates: { 'subject.empty': 'nothing here' } } })
2628
+ * narrator.line('subject.empty', {}) // 'nothing here'
2629
+ * ```
2630
+ */
2631
+ function createNarrator(options) {
2632
+ return new Narrator(options);
2633
+ }
2634
+ //#endregion
2635
+ exports.CONFIDENCE_ALIAS = CONFIDENCE_ALIAS;
2636
+ exports.CONFIDENCE_CARRIED = CONFIDENCE_CARRIED;
2637
+ exports.CONFIDENCE_COLLECT = CONFIDENCE_COLLECT;
2638
+ exports.CONFIDENCE_COMPUTED = CONFIDENCE_COMPUTED;
2639
+ exports.CONFIDENCE_DEFAULT = CONFIDENCE_DEFAULT;
2640
+ exports.CONFIDENCE_EXACT = CONFIDENCE_EXACT;
2641
+ exports.CONFIDENCE_POSITIONAL = CONFIDENCE_POSITIONAL;
2642
+ exports.Clarifier = Clarifier;
2643
+ exports.DEFAULT_ABBREVIATIONS = DEFAULT_ABBREVIATIONS;
2644
+ exports.DEFAULT_ACTIONS = DEFAULT_ACTIONS;
2645
+ exports.DEFAULT_CONTRACTIONS = DEFAULT_CONTRACTIONS;
2646
+ exports.DEFAULT_CORRECTIONS = DEFAULT_CORRECTIONS;
2647
+ exports.DEFAULT_DOMAINS = DEFAULT_DOMAINS;
2648
+ exports.DEFAULT_INTERPRET_FLOOR = DEFAULT_INTERPRET_FLOOR;
2649
+ exports.DEFAULT_INTERPRET_HISTORY = DEFAULT_INTERPRET_HISTORY;
2650
+ exports.DEFAULT_INTERPRET_SIMILARITY = DEFAULT_INTERPRET_SIMILARITY;
2651
+ exports.DEFAULT_LEXICON = DEFAULT_LEXICON;
2652
+ exports.DEFAULT_VERBS = DEFAULT_VERBS;
2653
+ exports.DefinitionManager = DefinitionManager;
2654
+ exports.Extractor = Extractor;
2655
+ exports.Formatter = Formatter;
2656
+ exports.Generator = Generator;
2657
+ exports.INTERPRET_ID = INTERPRET_ID;
2658
+ exports.Interpret = Interpret;
2659
+ exports.InterpretContext = InterpretContext;
2660
+ exports.InterpretError = InterpretError;
2661
+ exports.NUMBER_PATTERN = NUMBER_PATTERN;
2662
+ exports.Narrator = Narrator;
2663
+ exports.Normalizer = Normalizer;
2664
+ exports.SubjectManager = SubjectManager;
2665
+ exports.TemplateManager = TemplateManager;
2666
+ exports.UNSAFE_FIELD_SEGMENTS = UNSAFE_FIELD_SEGMENTS;
2667
+ exports.applyReplacements = applyReplacements;
2668
+ exports.assignEntities = assignEntities;
2669
+ exports.canonicalize = canonicalize;
2670
+ exports.classifyIntent = classifyIntent;
2671
+ exports.collapseWhitespace = collapseWhitespace;
2672
+ exports.createClarifier = createClarifier;
2673
+ exports.createDefinitionManager = createDefinitionManager;
2674
+ exports.createExtractor = createExtractor;
2675
+ exports.createFormatter = createFormatter;
2676
+ exports.createGenerator = createGenerator;
2677
+ exports.createInterpret = createInterpret;
2678
+ exports.createInterpretContext = createInterpretContext;
2679
+ exports.createNarrator = createNarrator;
2680
+ exports.createNormalizer = createNormalizer;
2681
+ exports.createSubjectManager = createSubjectManager;
2682
+ exports.createTemplate = createTemplate;
2683
+ exports.createTemplateManager = createTemplateManager;
2684
+ exports.deriveAggregateField = deriveAggregateField;
2685
+ exports.describeSubject = describeSubject;
2686
+ exports.digestValue = digestValue;
2687
+ exports.escapeRegExp = escapeRegExp;
2688
+ exports.extractNumbers = extractNumbers;
2689
+ exports.interpolateMessage = interpolateMessage;
2690
+ exports.isComputedField = isComputedField;
2691
+ exports.isEntityMapping = isEntityMapping;
2692
+ exports.isFieldDefault = isFieldDefault;
2693
+ exports.isInterpretError = isInterpretError;
2694
+ exports.isTemplate = isTemplate;
2695
+ exports.matchAlias = matchAlias;
2696
+ exports.matchTemplate = matchTemplate;
2697
+ exports.parseTemplate = parseTemplate;
2698
+ exports.resolveExpression = resolveExpression;
2699
+ exports.scoreSimilarity = scoreSimilarity;
2700
+ exports.scoreTemplate = scoreTemplate;
2701
+ exports.setField = setField;
2702
+ exports.tokenize = tokenize;
2703
+ exports.variablesOf = variablesOf;
2704
+
2705
+ //# sourceMappingURL=index.cjs.map