@orkestrel/qualifier 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,25 +1,25 @@
1
- import { Check } from '@orkestrel/reason';
2
- import { CheckResult } from '@orkestrel/reason';
3
- import { Comparison } from '@orkestrel/reason';
4
- import { EmitterErrorHandler } from '@orkestrel/emitter';
5
- import { EmitterHooks } from '@orkestrel/emitter';
6
- import { EmitterInterface } from '@orkestrel/emitter';
7
- import { EvaluatorInterface } from '@orkestrel/reason';
8
- import { FieldPath } from '@orkestrel/contract';
1
+ import type { Check } from '@orkestrel/reason';
2
+ import type { CheckResult } from '@orkestrel/reason';
3
+ import type { Comparison } from '@orkestrel/reason';
4
+ import type { EmitterErrorHandler } from '@orkestrel/emitter';
5
+ import type { EmitterHooks } from '@orkestrel/emitter';
6
+ import type { EmitterInterface } from '@orkestrel/emitter';
7
+ import type { EvaluatorInterface } from '@orkestrel/reason';
8
+ import type { FieldPath } from '@orkestrel/contract';
9
9
  import { Guard } from '@orkestrel/contract';
10
- import { JSONValue } from '@orkestrel/contract';
11
- import { LogicalDefinition } from '@orkestrel/reason';
12
- import { LogicalResult } from '@orkestrel/reason';
13
- import { QuantitativeDefinition } from '@orkestrel/reason';
14
- import { QuantitativeResult } from '@orkestrel/reason';
15
- import { ReasonInterface } from '@orkestrel/reason';
16
- import { ReasonResult } from '@orkestrel/reason';
17
- import { ReasonValidationResult } from '@orkestrel/reason';
18
- import { Rule } from '@orkestrel/reason';
19
- import { Subject } from '@orkestrel/reason';
10
+ import type { JSONValue } from '@orkestrel/contract';
11
+ import type { LogicalDefinition } from '@orkestrel/reason';
12
+ import type { LogicalResult } from '@orkestrel/reason';
13
+ import type { QuantitativeDefinition } from '@orkestrel/reason';
14
+ import type { QuantitativeResult } from '@orkestrel/reason';
15
+ import type { ReasonInterface } from '@orkestrel/reason';
16
+ import type { ReasonResult } from '@orkestrel/reason';
17
+ import type { ReasonValidationResult } from '@orkestrel/reason';
18
+ import type { Rule } from '@orkestrel/reason';
19
+ import type { Subject } from '@orkestrel/reason';
20
20
 
21
21
  /**
22
- * Assert a value is a valid qualification {@link Subject}, narrowing it in place.
22
+ * Asserts a value is a valid qualification {@link Subject}, narrowing it in place.
23
23
  *
24
24
  * @param value - The candidate subject to validate
25
25
  * @throws {@link QualifierError} `'MISMATCH'` when the value is not a record, or
@@ -35,7 +35,28 @@ import { Subject } from '@orkestrel/reason';
35
35
  export declare function assertSubject(value: unknown): asserts value is Subject;
36
36
 
37
37
  /**
38
- * Return the most severe {@link Eligibility} in a list.
38
+ * Builds a {@link Premise} from an authored {@link Check} and its evaluated
39
+ * `CheckResult`.
40
+ *
41
+ * @param check - The authored check
42
+ * @param result - The evaluated check result carrying the resolved `actual` and `met`
43
+ * @param labels - Optional field-to-label overrides, keyed by dot-joined field
44
+ * @returns A fresh premise
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { checkToPremise } from '@orkestrel/qualifier'
49
+ * import { createCheck } from '@orkestrel/reason'
50
+ *
51
+ * const authored = createCheck('age', 'above', 18)
52
+ * checkToPremise(authored, { field: 'age', actual: 25, met: true })
53
+ * // { field: 'age', comparison: 'above', expected: 18, actual: 25, met: true }
54
+ * ```
55
+ */
56
+ export declare function checkToPremise(check: Check, result: CheckResult, labels?: Readonly<Record<string, string>>): Premise;
57
+
58
+ /**
59
+ * Returns the most severe {@link Eligibility} in a list.
39
60
  *
40
61
  * @param eligibilities - The eligibilities to combine
41
62
  * @returns The most severe eligibility, or `'eligible'` for an empty list
@@ -47,34 +68,106 @@ import { Subject } from '@orkestrel/reason';
47
68
  * combineEligibilities(['eligible', 'referral']) // 'referral'
48
69
  * ```
49
70
  */
50
- export declare function combineEligibilities(eligibilities: Eligibility[]): Eligibility;
71
+ export declare function combineEligibilities(eligibilities: readonly Eligibility[]): Eligibility;
51
72
 
52
73
  /**
53
- * Create one qualifier over a reason engine.
74
+ * Creates a fresh {@link QualificationDefinition}, omitting every absent optional key.
54
75
  *
55
76
  * @remarks
56
- * A standalone qualifier creates and OWNS one shared quantitative-plus-logical
77
+ * `passes` and `rulings` arrays are copied, and record `metadata` is
78
+ * shallow-copied so nested values are not deep-cloned.
79
+ *
80
+ * @param id - The definition id
81
+ * @param name - The display name
82
+ * @param passes - The ordered qualification passes
83
+ * @param input - Optional description, rulings, and metadata
84
+ * @returns A fresh qualification definition
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * import { createQualificationDefinition } from '@orkestrel/qualifier'
89
+ *
90
+ * createQualificationDefinition('standard', 'Standard', [gates], { rulings: [ruling] })
91
+ * ```
92
+ */
93
+ export declare function createQualificationDefinition(id: string, name: string, passes: readonly QualificationPass[], input?: QualificationInput): QualificationDefinition;
94
+
95
+ /**
96
+ * Creates one {@link QualifierInterface} over a reason engine.
97
+ *
98
+ * @remarks
99
+ * A standalone qualifier creates and owns one shared quantitative-plus-logical
57
100
  * reason engine, destroying it on `destroy()`. An injected `options.engine`
58
- * remains caller-owned and is never destroyed. Validation is on by default.
101
+ * remains caller-owned and is never destroyed. `validate` runs semantic
102
+ * validation before every qualification. Default: `true`. `labels` overrides the
103
+ * display name of a premise field, keyed by the dot-joined field path
104
+ * `formatField` produces, and takes precedence over a premise's own `label`.
59
105
  *
60
106
  * @param options - Optional injected engine, validation, labels, and emitter hooks
61
107
  * @returns A {@link QualifierInterface}
62
108
  *
63
- * @example
109
+ * @example Create a qualifier
64
110
  * ```ts
65
- * import { createQualifier } from '@orkestrel/qualifier'
111
+ * import { createQualificationDefinition, createQualifier, createRuling } from '@orkestrel/qualifier'
112
+ * import { createAtom, createLogicalDefinition, createRule } from '@orkestrel/reason'
113
+ *
114
+ * const gates = createLogicalDefinition('gates', 'Eligibility gates', [
115
+ * createRule(
116
+ * 'licensed',
117
+ * [createAtom('licensed', 'equals', false)],
118
+ * createAtom('blocked', 'equals', true),
119
+ * ),
120
+ * ])
121
+ *
122
+ * const bare = createRuling('license', 'gates', 'licensed', 'restriction')
123
+ * const messaged = createRuling('license', 'gates', 'licensed', 'restriction', {
124
+ * message: 'A license is required',
125
+ * })
126
+ *
127
+ * 'message' in bare // false — an absent optional key is omitted, never written as undefined
128
+ * messaged.message // 'A license is required'
129
+ *
130
+ * const passes = [gates]
131
+ * const definition = createQualificationDefinition('standard', 'Standard eligibility', passes, {
132
+ * rulings: [messaged],
133
+ * })
134
+ *
135
+ * 'description' in definition // false
136
+ * definition.passes === passes // false — the factory copies what it is handed
66
137
  *
67
138
  * const qualifier = createQualifier()
68
- * qualifier.qualify({ id: 'risk-1' }, definition)
139
+ * qualifier.qualify({ id: 'risk-1', licensed: false }, definition)
69
140
  * qualifier.destroy()
70
141
  * ```
71
142
  */
72
143
  export declare function createQualifier(options?: QualifierOptions): QualifierInterface;
73
144
 
74
- /** Default definition validation policy for `createQualifier` / `Qualifier.qualify`. */
145
+ /**
146
+ * Creates a fresh {@link Ruling} from the rule it reacts to and the effect it applies.
147
+ *
148
+ * @param id - The ruling id
149
+ * @param pass - The logical pass id the rule lives in
150
+ * @param rule - The rule id whose firing this ruling reacts to
151
+ * @param effect - The eligibility impact when the rule fires
152
+ * @param input - Optional selection scope and message template
153
+ * @returns A fresh ruling
154
+ *
155
+ * @example
156
+ * ```ts
157
+ * import { createRuling } from '@orkestrel/qualifier'
158
+ *
159
+ * createRuling('license', 'gates', 'licensed', 'restriction', { message: 'A license is required' })
160
+ * ```
161
+ */
162
+ export declare function createRuling(id: string, pass: string, rule: string, effect: QualificationEffect, input?: RulingInput): Ruling;
163
+
164
+ /**
165
+ * Holds the default definition validation policy for `createQualifier` and
166
+ * `Qualifier.qualify`, `true`.
167
+ */
75
168
  export declare const DEFAULT_QUALIFIER_VALIDATE = true;
76
169
 
77
- /** One quantitative pass's audit result. */
170
+ /** Represents one quantitative pass's audit result. */
78
171
  export declare interface Derivation {
79
172
  readonly id: string;
80
173
  readonly value: number;
@@ -84,7 +177,7 @@ import { Subject } from '@orkestrel/reason';
84
177
  }
85
178
 
86
179
  /**
87
- * Derive global eligibility from applied, unscoped findings.
180
+ * Derives global eligibility from applied, unscoped findings.
88
181
  *
89
182
  * @remarks
90
183
  * Only findings that both applied and carry no `scope` shape global eligibility.
@@ -92,7 +185,8 @@ import { Subject } from '@orkestrel/reason';
92
185
  * `failed`.
93
186
  *
94
187
  * @param findings - The resolved findings
95
- * @param failed - Whether an operational pass failure occurred
188
+ * @param failed - If `true`, adds a synthetic `referral` impact so an operational failure
189
+ * fails closed; if `false`, derives eligibility from the findings alone. Default: `false`
96
190
  * @returns The most severe global eligibility
97
191
  *
98
192
  * @example
@@ -103,10 +197,10 @@ import { Subject } from '@orkestrel/reason';
103
197
  * deriveFindingEligibility([finding]) // 'ineligible'
104
198
  * ```
105
199
  */
106
- export declare function deriveFindingEligibility(findings: Finding[], failed?: boolean): Eligibility;
200
+ export declare function deriveFindingEligibility(findings: readonly Finding[], failed?: boolean): Eligibility;
107
201
 
108
202
  /**
109
- * Derive one eligibility per finding scope.
203
+ * Derives one eligibility per finding scope.
110
204
  *
111
205
  * @remarks
112
206
  * Only applied, scoped findings contribute. Each named scope combines its own
@@ -124,109 +218,100 @@ import { Subject } from '@orkestrel/reason';
124
218
  * deriveScopeEligibilities([finding]) // { wind: 'ineligible' }
125
219
  * ```
126
220
  */
127
- export declare function deriveScopeEligibilities(findings: Finding[]): Readonly<Record<string, Eligibility>>;
221
+ export declare function deriveScopeEligibilities(findings: readonly Finding[]): Readonly<Record<string, Eligibility>>;
128
222
 
129
223
  /**
130
- * Describe a {@link Premise} comparison as a display-neutral verb phrase.
131
- *
132
- * @param comparison - The comparison to describe
133
- * @returns A display-neutral phrase
134
- *
135
- * @example
136
- * ```ts
137
- * import { describeComparison } from '@orkestrel/qualifier'
138
- *
139
- * describeComparison('above') // 'is more than'
140
- * ```
141
- */
142
- export declare function describeComparison(comparison: NonNullable<Premise['comparison']>): string;
143
-
144
- /**
145
- * Render one {@link Premise} into a display-neutral sentence.
224
+ * Describes each logical pass carrying no rulings.
146
225
  *
147
226
  * @remarks
148
- * The checked form renders only when `field` and `comparison` are BOTH
149
- * present, and `description` then goes unused; a premise missing either half
150
- * of the checked pair renders as described instead.
227
+ * A logical pass with no matching ruling can never contribute a finding, so its
228
+ * rules only ever influence later passes' reads — this is very likely an authoring
229
+ * oversight.
151
230
  *
152
- * @param entry - The premise to render
153
- * @param labels - Optional field-to-label overrides, keyed by dot-joined field
154
- * @returns A display-neutral sentence
231
+ * @param definition - The qualification definition to check
232
+ * @returns A fresh list of warning messages
155
233
  *
156
234
  * @example
157
235
  * ```ts
158
- * import { describePremise } from '@orkestrel/qualifier'
236
+ * import { createQualificationDefinition, describeEmptyLogicalPasses } from '@orkestrel/qualifier'
237
+ * import { createAtom, createLogicalDefinition, createRule } from '@orkestrel/reason'
159
238
  *
160
- * describePremise({ field: 'age', comparison: 'above', expected: 18, actual: 25, met: true })
161
- * // 'age is more than 18 → met'
239
+ * const gates = createLogicalDefinition('gates', 'Gates', [
240
+ * createRule('licensed', [createAtom('licensed', 'equals', false)], createAtom('blocked', 'equals', true)),
241
+ * ])
242
+ * const definition = createQualificationDefinition('standard', 'Standard', [gates])
162
243
  *
163
- * describePremise({ description: 'Applicant is enrolled', met: true })
164
- * // 'Applicant is enrolled → met'
244
+ * describeEmptyLogicalPasses(definition) // ["Logical pass 'gates' has no rulings"]
165
245
  * ```
166
246
  */
167
- export declare function describePremise(entry: Premise, labels?: Readonly<Record<string, string>>): string;
247
+ export declare function describeEmptyLogicalPasses(definition: QualificationDefinition): readonly string[];
168
248
 
169
249
  /**
170
- * Render a structured or scalar expected value display-neutrally.
250
+ * Describes each ruling whose pass or rule does not exist or whose pass is not
251
+ * logical, and each pass id shadowing the reserved {@link QUALIFICATION_KEY}.
171
252
  *
172
- * @remarks
173
- * An array renders as its elements, each `String`-coerced, joined with `', '`.
174
- * A reason `Bounds`-shaped record (the `between` / `outside` expected value)
175
- * renders its present sides joined with the word `and`; an absent side is
176
- * omitted. A finite number and everything else fall back to plain `String()` —
177
- * a finite number is NOT grouped here (grouping is {@link interpolateMessage}'s
178
- * message-token-only behavior).
179
- *
180
- * @param value - The value to render
181
- * @returns A display-neutral rendering
253
+ * @param definition - The qualification definition to check
254
+ * @returns A fresh list of reference-error messages
182
255
  *
183
256
  * @example
184
257
  * ```ts
185
- * import { describeValue } from '@orkestrel/qualifier'
258
+ * import { createQualificationDefinition, createRuling, describeMissingReferences } from '@orkestrel/qualifier'
259
+ * import { createAtom, createLogicalDefinition, createRule } from '@orkestrel/reason'
260
+ *
261
+ * const gates = createLogicalDefinition('gates', 'Gates', [
262
+ * createRule('licensed', [createAtom('licensed', 'equals', false)], createAtom('blocked', 'equals', true)),
263
+ * ])
264
+ * const definition = createQualificationDefinition('standard', 'Standard', [gates], {
265
+ * rulings: [createRuling('license', 'gates', 'absent', 'restriction')],
266
+ * })
186
267
  *
187
- * describeValue([18, 25, 40]) // '18, 25, 40'
188
- * describeValue({ minimum: 18, maximum: 65 }) // '18 and 65'
189
- * describeValue(42) // '42'
268
+ * describeMissingReferences(definition)
269
+ * // ["Ruling 'license' references missing rule 'absent' in pass 'gates'"]
190
270
  * ```
191
271
  */
192
- export declare function describeValue(value: unknown): string;
193
-
194
- /** The eligibility impact of each {@link QualificationEffect}; `condition` remains eligible. */
195
- export declare const EFFECT_ELIGIBILITIES: Readonly<Record<QualificationEffect, Eligibility>>;
196
-
197
- /** The eligibility outcome axis. */
198
- export declare type Eligibility = 'eligible' | 'ineligible' | 'referral';
199
-
200
- /** Eligibility severity order — most to least severe. */
201
- export declare const ELIGIBILITY_PRECEDENCE: readonly Eligibility[];
272
+ export declare function describeMissingReferences(definition: QualificationDefinition): readonly string[];
202
273
 
203
274
  /**
204
- * Find logical passes carrying no rulings.
275
+ * Describes each quantitative pass never read by a later pass.
205
276
  *
206
277
  * @remarks
207
- * A logical pass with no matching ruling can never contribute a finding, so its
208
- * rules only ever influence later passes' reads — this is very likely an authoring
209
- * oversight.
278
+ * A quantitative pass's projected value lives under `qualification.<id>` — if no
279
+ * later logical premise or quantitative factor field reads that key, the
280
+ * derivation is dead weight. Reads are collected per later pass: a logical pass's
281
+ * premise atoms (conclusion atoms are writes, not reads), and a quantitative
282
+ * pass's field/lookup/range factor sources plus factor checks.
210
283
  *
211
284
  * @param definition - The qualification definition to check
212
285
  * @returns A fresh list of warning messages
213
286
  *
214
287
  * @example
215
288
  * ```ts
216
- * import { findEmptyLogicalPasses, qualificationDefinition } from '@orkestrel/qualifier'
217
- * import { atom, logicalDefinition, rule } from '@orkestrel/reason'
289
+ * import { createQualificationDefinition, describeUnreadDerivations } from '@orkestrel/qualifier'
290
+ * import { createFactorGroup, createQuantitativeDefinition, createStaticFactor } from '@orkestrel/reason'
218
291
  *
219
- * const gates = logicalDefinition('gates', 'Gates', [
220
- * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),
292
+ * const cap = createQuantitativeDefinition('cap', 'TIV cap', [
293
+ * createFactorGroup('limit', 'sum', [createStaticFactor('base', 500_000)]),
221
294
  * ])
222
- * const definition = qualificationDefinition('standard', 'Standard', [gates])
295
+ * const definition = createQualificationDefinition('standard', 'Standard', [cap])
223
296
  *
224
- * findEmptyLogicalPasses(definition) // ["Logical pass 'gates' has no rulings"]
297
+ * describeUnreadDerivations(definition) // ["Quantitative pass 'cap' is never read by a later pass"]
225
298
  * ```
226
299
  */
227
- export declare function findEmptyLogicalPasses(definition: QualificationDefinition): readonly string[];
300
+ export declare function describeUnreadDerivations(definition: QualificationDefinition): readonly string[];
228
301
 
229
- /** One resolved ruling. */
302
+ /**
303
+ * Maps each {@link QualificationEffect} to its eligibility impact — `restriction` to
304
+ * `ineligible`, `referral` to `referral`, and `condition` to `eligible`.
305
+ */
306
+ export declare const EFFECT_ELIGIBILITIES: Readonly<Record<QualificationEffect, Eligibility>>;
307
+
308
+ /** Represents the eligibility outcome axis. */
309
+ export declare type Eligibility = 'eligible' | 'ineligible' | 'referral';
310
+
311
+ /** Lists the eligibility severities most to least severe: `ineligible`, `referral`, `eligible`. */
312
+ export declare const ELIGIBILITY_PRECEDENCE: readonly Eligibility[];
313
+
314
+ /** Represents one resolved ruling. */
230
315
  export declare interface Finding {
231
316
  readonly id: string;
232
317
  readonly pass: string;
@@ -239,32 +324,7 @@ import { Subject } from '@orkestrel/reason';
239
324
  }
240
325
 
241
326
  /**
242
- * Find rulings whose pass or rule does not exist, whose pass is not logical, or
243
- * a pass id shadowing the reserved {@link QUALIFICATION_KEY}.
244
- *
245
- * @param definition - The qualification definition to check
246
- * @returns A fresh list of reference-error messages
247
- *
248
- * @example
249
- * ```ts
250
- * import { findMissingReferences, qualificationDefinition, rulingDefinition } from '@orkestrel/qualifier'
251
- * import { atom, logicalDefinition, rule } from '@orkestrel/reason'
252
- *
253
- * const gates = logicalDefinition('gates', 'Gates', [
254
- * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),
255
- * ])
256
- * const definition = qualificationDefinition('standard', 'Standard', [gates], {
257
- * rulings: [rulingDefinition('license', 'gates', 'absent', 'restriction')],
258
- * })
259
- *
260
- * findMissingReferences(definition)
261
- * // ["Ruling 'license' references missing rule 'absent' in pass 'gates'"]
262
- * ```
263
- */
264
- export declare function findMissingReferences(definition: QualificationDefinition): readonly string[];
265
-
266
- /**
267
- * Locate an authored {@link Rule} by id.
327
+ * Locates an authored {@link Rule} by id.
268
328
  *
269
329
  * @param definition - The logical definition to search
270
330
  * @param id - The rule id
@@ -273,10 +333,10 @@ import { Subject } from '@orkestrel/reason';
273
333
  * @example
274
334
  * ```ts
275
335
  * import { findRule } from '@orkestrel/qualifier'
276
- * import { atom, logicalDefinition, rule } from '@orkestrel/reason'
336
+ * import { createAtom, createLogicalDefinition, createRule } from '@orkestrel/reason'
277
337
  *
278
- * const gates = logicalDefinition('gates', 'Gates', [
279
- * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),
338
+ * const gates = createLogicalDefinition('gates', 'Gates', [
339
+ * createRule('licensed', [createAtom('licensed', 'equals', false)], createAtom('blocked', 'equals', true)),
280
340
  * ])
281
341
  *
282
342
  * findRule(gates, 'licensed')?.id // 'licensed'
@@ -285,38 +345,10 @@ import { Subject } from '@orkestrel/reason';
285
345
  export declare function findRule(definition: LogicalDefinition, id: string): Rule | undefined;
286
346
 
287
347
  /**
288
- * Find quantitative passes never read by a later pass.
289
- *
290
- * @remarks
291
- * A quantitative pass's projected value lives under `qualification.<id>` — if no
292
- * later logical premise or quantitative factor field reads that key, the
293
- * derivation is dead weight. Reads are collected per later pass: a logical pass's
294
- * premise atoms (conclusion atoms are writes, not reads), and a quantitative
295
- * pass's field/lookup/range factor sources plus factor checks.
296
- *
297
- * @param definition - The qualification definition to check
298
- * @returns A fresh list of warning messages
299
- *
300
- * @example
301
- * ```ts
302
- * import { findUnreadDerivations, qualificationDefinition } from '@orkestrel/qualifier'
303
- * import { factorGroup, quantitativeDefinition, staticFactor } from '@orkestrel/reason'
304
- *
305
- * const cap = quantitativeDefinition('cap', 'TIV cap', [
306
- * factorGroup('limit', 'sum', [staticFactor('base', 500_000)]),
307
- * ])
308
- * const definition = qualificationDefinition('standard', 'Standard', [cap])
309
- *
310
- * findUnreadDerivations(definition) // ["Quantitative pass 'cap' is never read by a later pass"]
311
- * ```
312
- */
313
- export declare function findUnreadDerivations(definition: QualificationDefinition): readonly string[];
314
-
315
- /**
316
- * Determine whether a subject already owns the reserved {@link QUALIFICATION_KEY}.
348
+ * Determines whether a subject already owns the reserved {@link QUALIFICATION_KEY}.
317
349
  *
318
350
  * @param subject - The subject to check
319
- * @returns `true` when the subject owns the reserved key
351
+ * @returns True if the subject owns the reserved key; false otherwise
320
352
  *
321
353
  * @example
322
354
  * ```ts
@@ -328,14 +360,14 @@ import { Subject } from '@orkestrel/reason';
328
360
  export declare function hasReservedKey(subject: Subject): boolean;
329
361
 
330
362
  /**
331
- * Interpolate `{{dotted.path}}` tokens in a message template against a subject.
363
+ * Interpolates `{{dotted.path}}` tokens in a message template against a subject.
332
364
  *
333
365
  * @remarks
334
366
  * Each token is split on `.` into a `FieldPath` array and resolved with
335
- * `resolveField` (a plain string field is ONE key, never dot-split — the split
367
+ * `resolveField` (a plain string field is one key, never dot-split — the split
336
368
  * here is the token-to-path bridge). A finite number renders with `en-US`
337
369
  * thousand grouping (`5010` → `5,010`); any other resolved value String-coerces.
338
- * An UNRESOLVED path renders as the empty string.
370
+ * An unresolved path renders as the empty string.
339
371
  *
340
372
  * @param template - The message template carrying `{{dotted.path}}` tokens
341
373
  * @param subject - The record tokens resolve against
@@ -352,138 +384,154 @@ import { Subject } from '@orkestrel/reason';
352
384
  export declare function interpolateMessage(template: string, subject: Readonly<Record<string, unknown>>): string;
353
385
 
354
386
  /**
355
- * Determine whether a value is an open result-side {@link Derivation}.
387
+ * Determines whether a value is an open result-side {@link Derivation}.
356
388
  *
357
389
  * @remarks
358
- * Unknown members are admitted. `value` follows the published `number` type, including `NaN` and
359
- * infinities.
390
+ * Every published member is checked: `value` follows the published `number` type, including
391
+ * `NaN` and the infinities, and `trace` and `errors` hold strings. Unknown members are
392
+ * admitted, because a foreign result implementation may add them without changing the
393
+ * published contract.
360
394
  *
361
395
  * @param value - The value to test
362
- * @returns `true` when every published derivation member is valid
396
+ * @returns True if every published derivation member is valid; false otherwise
363
397
  */
364
398
  export declare function isDerivation(value: unknown): value is Derivation;
365
399
 
366
- /** Determine whether a value is an {@link Eligibility} literal. */
400
+ /**
401
+ * Determines whether a value is an {@link Eligibility} literal.
402
+ *
403
+ * @remarks
404
+ * The guard is total and leaves nothing unchecked: a scalar union has no open or exact
405
+ * axis.
406
+ *
407
+ * @param value - The value to test
408
+ * @returns True if `value` is one of the eligibility literals; false otherwise
409
+ */
367
410
  export declare const isEligibility: Guard<"eligible" | "ineligible" | "referral">;
368
411
 
369
412
  /**
370
- * Determine whether a value is an open string-keyed record of {@link Eligibility} values.
413
+ * Determines whether a value is an open string-keyed record of {@link Eligibility} values.
371
414
  *
372
415
  * @remarks
373
- * Every own string-named property is checked, including non-enumerable properties. Inherited and
374
- * symbol-named members are left unchecked because they are outside the record this guard certifies.
416
+ * Every own string-named value is checked through {@link isEligibility}, including
417
+ * non-enumerable values. Inherited and symbol-named members are left unchecked because they
418
+ * are outside the own string-keyed record this guard certifies.
375
419
  *
376
420
  * @param value - The value to test
377
- * @returns `true` when `value` is a non-array object whose own string-named values are eligibilities
421
+ * @returns True if `value` is a non-array object whose own string-named values are
422
+ * eligibilities; false otherwise
378
423
  */
379
424
  export declare function isEligibilityRecord(value: unknown): value is Readonly<Record<string, Eligibility>>;
380
425
 
381
426
  /**
382
- * Determine whether a value is an open result-side {@link Finding}.
427
+ * Determines whether a value is an open result-side {@link Finding}.
383
428
  *
384
429
  * @remarks
385
- * Unknown members are admitted. Optional `scope` and `message` members may be absent or
386
- * `undefined`; a present defined value must be a string.
430
+ * Every published member is checked, including each nested {@link Premise}. Optional `scope`
431
+ * and `message` members may be absent or `undefined`; a present defined value must be a
432
+ * string. Unknown members are admitted, because a foreign result implementation may add them
433
+ * without changing the published contract.
387
434
  *
388
435
  * @param value - The value to test
389
- * @returns `true` when every published finding member is valid
436
+ * @returns True if every published finding member is valid; false otherwise
390
437
  */
391
438
  export declare function isFinding(value: unknown): value is Finding;
392
439
 
393
440
  /**
394
- * Determine whether a value is an open result-side {@link Premise}.
441
+ * Determines whether a value is an open result-side {@link Premise}.
395
442
  *
396
443
  * @remarks
397
- * `expected` is unchecked and may be absent or contain any value because its published type is
398
- * `unknown`. `actual` is unchecked and may be absent or contain any value for the same reason.
399
- * Unknown members are admitted.
444
+ * The optional `field`, `label`, `description`, `comparison`, and `met` members are checked
445
+ * when they are defined. `expected` is unchecked and may be absent or contain any value
446
+ * because its published type is `unknown`. `actual` is unchecked and may be absent or contain
447
+ * any value for the same reason. Unknown members are admitted, because an open result admits
448
+ * what the published contract does not name.
400
449
  *
401
450
  * @param value - The value to test
402
- * @returns `true` when every checked premise member follows its published type
451
+ * @returns True if every checked premise member follows its published type; false otherwise
403
452
  */
404
453
  export declare function isPremise(value: unknown): value is Premise;
405
454
 
406
- /** Determine whether a value is an exact {@link QualificationDefinition} record. */
455
+ /**
456
+ * Determines whether a value is an exact {@link QualificationDefinition} record.
457
+ *
458
+ * @remarks
459
+ * Every authored definition member, the nested pass and ruling shapes, and the complete key
460
+ * set are checked. Nothing is left unchecked, because this package owns the authored input
461
+ * record.
462
+ *
463
+ * @param value - The value to test
464
+ * @returns True if `value` carries every definition member and no unknown key; false otherwise
465
+ */
407
466
  export declare function isQualificationDefinition(value: unknown): value is QualificationDefinition;
408
467
 
409
- /** Determine whether a value is a {@link QualificationEffect} literal. */
468
+ /**
469
+ * Determines whether a value is a {@link QualificationEffect} literal.
470
+ *
471
+ * @remarks
472
+ * The guard is total and leaves nothing unchecked: a scalar union has no open or exact
473
+ * axis.
474
+ *
475
+ * @param value - The value to test
476
+ * @returns True if `value` is one of the effect literals; false otherwise
477
+ */
410
478
  export declare const isQualificationEffect: Guard<"referral" | "restriction" | "condition">;
411
479
 
412
- /** Determine whether a value is a {@link QualificationPass} (a quantitative or logical definition). */
413
- export declare function isQualificationPass(value: unknown): value is QualificationPass;
414
-
415
480
  /**
416
- * Determine whether a value is an open {@link QualificationResult} returned by a qualifier.
481
+ * Determines whether a value is a {@link QualificationPass} (a quantitative or logical definition).
417
482
  *
418
483
  * @remarks
419
- * This guard is result-postured for values returned through a borrowed qualifier interface. It
420
- * admits unknown members and class instances while checking the complete published result closure.
484
+ * The guard is exact and requires either complete reason shape, the quantitative definition or
485
+ * the logical one. Nothing is left unchecked, because the reason package owns both authored
486
+ * input shapes.
421
487
  *
422
488
  * @param value - The value to test
423
- * @returns `true` when every published qualification-result member is valid
489
+ * @returns True if `value` is a complete reason quantitative or logical definition; false otherwise
424
490
  */
425
- export declare function isQualificationResult(value: unknown): value is QualificationResult;
491
+ export declare function isQualificationPass(value: unknown): value is QualificationPass;
426
492
 
427
493
  /**
428
- * Determine whether a value is an open {@link QualificationValidationResult} returned by a qualifier.
494
+ * Determines whether a value is an open {@link QualificationResult} returned by a qualifier.
429
495
  *
430
496
  * @remarks
431
- * `QualificationValidationResult` is an alias of reasons' validation-result type, so this IS
432
- * reasons' published open result guard by delegation — the alias tracks reasons' shape
433
- * automatically, and a hand-written member list here would silently stop checking a member a
434
- * later reason release adds.
497
+ * This guard is result-postured for values returned through a borrowed qualifier interface. It
498
+ * checks the complete published result closure — eligibility, scope values, findings,
499
+ * derivations, success, and the audit trail — and admits unknown members and class instances,
500
+ * because a borrowed `QualifierInterface` implementation may add them beyond the
501
+ * published contract.
435
502
  *
436
503
  * @param value - The value to test
437
- * @returns `true` when every published qualification-validation-result member is valid
504
+ * @returns True if every published qualification-result member is valid; false otherwise
438
505
  */
439
- export declare const isQualificationValidationResult: Guard<QualificationValidationResult>;
506
+ export declare function isQualificationResult(value: unknown): value is QualificationResult;
440
507
 
441
- /** Narrow a caught value to a {@link QualifierError}. */
508
+ /**
509
+ * Narrows a caught value to a {@link QualifierError}.
510
+ *
511
+ * @param value - The caught value to test
512
+ * @returns True if `value` is a {@link QualifierError} instance; false otherwise
513
+ */
442
514
  export declare function isQualifierError(value: unknown): value is QualifierError;
443
515
 
444
- /** Determine whether a value is an exact {@link Ruling} record. */
445
- export declare function isRuling(value: unknown): value is Ruling;
446
-
447
516
  /**
448
- * Build rich premises for one fired {@link Rule} by walking its premise atoms
449
- * and re-evaluating each against the working subject.
517
+ * Determines whether a value is an exact {@link Ruling} record.
450
518
  *
451
519
  * @remarks
452
- * A reason rule result carries only booleans, so this is the qualifier's own
453
- * premise-audit projection: each authored premise expression is flattened to its
454
- * atom leaves via `extractAtoms`, and each leaf's `Check` is re-evaluated through
455
- * the injected `evaluator`. A membership check (`any` / `none`) over an EMPTY
456
- * array value is content-free and is skipped.
457
- *
458
- * @param rule - The authored rule
459
- * @param working - The working subject to evaluate against
460
- * @param evaluator - The shared reason check evaluator
461
- * @param labels - Optional field-to-label overrides, keyed by dot-joined field
462
- * @returns A fresh, ordered list of rich premises
463
- *
464
- * @example
465
- * ```ts
466
- * import { logicalPremises } from '@orkestrel/qualifier'
467
- * import { atom, createEvaluator, logicalDefinition, rule } from '@orkestrel/reason'
468
- *
469
- * const gates = logicalDefinition('gates', 'Gates', [
470
- * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),
471
- * ])
472
- * const evaluator = createEvaluator()
473
- * const premises = logicalPremises(gates.rules[0], { licensed: false }, evaluator)
520
+ * Every authored ruling member and the complete key set are checked. Nothing is left
521
+ * unchecked, because this package owns the authored input record.
474
522
  *
475
- * premises[0]?.met // true
476
- * ```
523
+ * @param value - The value to test
524
+ * @returns True if `value` carries every ruling member and no unknown key; false otherwise
477
525
  */
478
- export declare function logicalPremises(rule: Rule, working: Subject, evaluator: EvaluatorInterface, labels?: Readonly<Record<string, string>>): readonly Premise[];
526
+ export declare function isRuling(value: unknown): value is Ruling;
479
527
 
480
528
  /**
481
- * Map an engine throw caught while running one pass to a typed {@link QualifierError}.
529
+ * Maps an engine throw caught while running one pass to a typed {@link QualifierError}.
482
530
  *
483
531
  * @remarks
484
532
  * A reason `ReasonError('INVALID')` maps to `DEFINITION` (the engine rejected the
485
533
  * pass as an invalid definition); `ReasonError('DESTROYED')` maps to `DESTROYED`;
486
- * every other `ReasonError` code (e.g. `MISSING`) and every non-`ReasonError` throw
534
+ * every other `ReasonError` code (for example `MISSING`) and every non-`ReasonError` throw
487
535
  * (including a `bail: true` reasoner rethrow) maps to `ENGINE`. The original throw
488
536
  * is preserved as `context.cause`.
489
537
  *
@@ -502,7 +550,7 @@ import { Subject } from '@orkestrel/reason';
502
550
  export declare function mapEngineError(error: unknown, pass: string): QualifierError;
503
551
 
504
552
  /**
505
- * Copy-on-write merge one pass projection into the context.
553
+ * Merges one pass projection into the context, copy-on-write.
506
554
  *
507
555
  * @param context - The current projection context
508
556
  * @param id - The pass id to project under
@@ -520,10 +568,13 @@ import { Subject } from '@orkestrel/reason';
520
568
  export declare function mergeQualificationContext(context: QualificationContext, id: string, projection: QualificationProjection): QualificationContext;
521
569
 
522
570
  /**
523
- * Display-neutral evidence for one condition, in one of two authoring modes.
524
- * A CHECKED premise carries `field` and `comparison`; a DESCRIBED premise
571
+ * Represents display-neutral evidence for one condition, authored as a checked or a
572
+ * described premise.
573
+ *
574
+ * @remarks
575
+ * A checked premise carries `field` and `comparison`; a described premise
525
576
  * carries neither and renders from `description` instead. The checked form
526
- * renders only when `field` and `comparison` are BOTH present, and
577
+ * renders only when `field` and `comparison` are both present, and
527
578
  * `description` then goes unused; a premise missing either half of the
528
579
  * checked pair renders as described instead. `met` is three-state: `true`
529
580
  * (met), `false` (not met), or absent (not evaluated, rendered as unknown).
@@ -539,33 +590,15 @@ import { Subject } from '@orkestrel/reason';
539
590
  }
540
591
 
541
592
  /**
542
- * Build a {@link Premise} from an authored {@link Check} and its evaluated
543
- * `CheckResult`.
544
- *
545
- * @param check - The authored check
546
- * @param result - The evaluated check result carrying the resolved `actual` and `met`
547
- * @param labels - Optional field-to-label overrides, keyed by dot-joined field
548
- * @returns A fresh premise
549
- *
550
- * @example
551
- * ```ts
552
- * import { premiseCheck } from '@orkestrel/qualifier'
553
- * import { check } from '@orkestrel/reason'
554
- *
555
- * const authored = check('age', 'above', 18)
556
- * premiseCheck(authored, { field: 'age', actual: 25, met: true })
557
- * // { field: 'age', comparison: 'above', expected: 18, actual: 25, met: true }
558
- * ```
593
+ * Names `'qualification'`, the reserved internal projection namespace a pass's working
594
+ * projection is written under.
559
595
  */
560
- export declare function premiseCheck(check: Check, result: CheckResult, labels?: Readonly<Record<string, string>>): Premise;
561
-
562
- /** The reserved internal projection namespace a pass's working projection is written under. */
563
596
  export declare const QUALIFICATION_KEY = "qualification";
564
597
 
565
- /** The internal projection record stored under `QUALIFICATION_KEY`. */
598
+ /** Represents the internal projection record stored under `QUALIFICATION_KEY`. */
566
599
  export declare type QualificationContext = Readonly<Record<string, QualificationProjection>>;
567
600
 
568
- /** A pure authored qualification definition. */
601
+ /** Represents a pure authored qualification definition. */
569
602
  export declare interface QualificationDefinition {
570
603
  readonly id: string;
571
604
  readonly name: string;
@@ -575,46 +608,23 @@ import { Subject } from '@orkestrel/reason';
575
608
  readonly metadata?: Readonly<Record<string, JSONValue>>;
576
609
  }
577
610
 
578
- /**
579
- * Build a {@link QualificationDefinition}.
580
- *
581
- * @remarks
582
- * Returns a fresh top-level definition, omitting absent optional keys;
583
- * `passes` and `rulings` arrays are copied, and record `metadata` is
584
- * shallow-copied so nested values are not deep-cloned.
585
- *
586
- * @param id - The definition id
587
- * @param name - The display name
588
- * @param passes - The ordered qualification passes
589
- * @param input - Optional description, rulings, and metadata
590
- * @returns A fresh qualification definition
591
- *
592
- * @example
593
- * ```ts
594
- * import { qualificationDefinition } from '@orkestrel/qualifier'
595
- *
596
- * qualificationDefinition('standard', 'Standard', [gates], { rulings: [ruling] })
597
- * ```
598
- */
599
- export declare function qualificationDefinition(id: string, name: string, passes: QualificationPass[], input?: QualificationInput): QualificationDefinition;
600
-
601
- /** An authored ruling's eligibility impact. */
611
+ /** Represents an authored ruling's eligibility impact. */
602
612
  export declare type QualificationEffect = 'restriction' | 'referral' | 'condition';
603
613
 
604
- /** Optional fields accepted by `qualificationDefinition`. */
614
+ /** Carries the optional fields `createQualificationDefinition` accepts. */
605
615
  export declare interface QualificationInput {
606
616
  readonly description?: string;
607
617
  readonly rulings?: readonly Ruling[];
608
618
  readonly metadata?: Readonly<Record<string, JSONValue>>;
609
619
  }
610
620
 
611
- /** One ordered derivation or rule pass. */
621
+ /** Represents one ordered derivation or rule pass. */
612
622
  export declare type QualificationPass = QuantitativeDefinition | LogicalDefinition;
613
623
 
614
- /** One pass's internal working projection. */
624
+ /** Represents one pass's internal working projection. */
615
625
  export declare type QualificationProjection = number | boolean | Readonly<Record<string, unknown>>;
616
626
 
617
- /** One subject's complete qualification outcome. */
627
+ /** Represents one subject's complete qualification outcome. */
618
628
  export declare interface QualificationResult {
619
629
  readonly id: string;
620
630
  readonly name: string;
@@ -628,7 +638,7 @@ import { Subject } from '@orkestrel/reason';
628
638
  }
629
639
 
630
640
  /**
631
- * Wrap a {@link QualificationContext} under {@link QUALIFICATION_KEY}.
641
+ * Wraps a {@link QualificationContext} under {@link QUALIFICATION_KEY}.
632
642
  *
633
643
  * @param context - The accumulated projection context
634
644
  * @returns A record carrying the context under the reserved key
@@ -642,193 +652,469 @@ import { Subject } from '@orkestrel/reason';
642
652
  */
643
653
  export declare function qualificationToRecord(context: QualificationContext): Readonly<Record<string, QualificationContext>>;
644
654
 
645
- /** Semantic definition validation. */
646
- export declare type QualificationValidationResult = ReasonValidationResult;
647
-
648
655
  /**
649
- * A qualifier — runs ordered passes over one reason engine and returns
650
- * eligibility.
656
+ * Runs ordered passes over one reason engine and returns eligibility.
651
657
  *
652
658
  * @remarks
653
- * The engine is OWNED when self-created (destroyed on `destroy()`) and borrowed
654
- * when injected (never destroyed). `qualify` builds its working subject through
659
+ * The engine is owned when self-created (destroyed on `destroy()`) and borrowed
660
+ * when injected (never destroyed). Semantic validation runs before the first pass
661
+ * whenever the `validate` option is on. `qualify` builds its working subject through
655
662
  * copy-on-write overlays only — the caller's `subject` is never mutated, and the
656
663
  * qualifier's internal projection is discarded after each call.
657
664
  */
658
665
  export declare class Qualifier implements QualifierInterface {
659
666
  #private;
660
667
  constructor(options?: QualifierOptions);
668
+ /**
669
+ * Holds the typed observation surface carrying `derive`, `finding`, `qualify`, and
670
+ * `destroy`.
671
+ *
672
+ * @returns The emitter this qualifier owns
673
+ *
674
+ * @example
675
+ * ```ts
676
+ * import { Qualifier } from '@orkestrel/qualifier'
677
+ *
678
+ * const qualifier = new Qualifier()
679
+ * qualifier.emitter.on('qualify', (result) => console.log(result.eligibility))
680
+ * qualifier.destroy()
681
+ * ```
682
+ */
661
683
  get emitter(): EmitterInterface<QualifierEventMap>;
662
- qualify(subject: Subject, definition: QualificationDefinition): QualificationResult;
663
- validate(definition: QualificationDefinition): QualificationValidationResult;
664
- destroy(): void;
665
- }
684
+ /**
685
+ * Qualifies one subject against one authored definition.
686
+ *
687
+ * @remarks
688
+ * Semantic validation runs first when the `validate` option is on, so an invalid
689
+ * definition throws before any pass runs. Passes run in authored order over a
690
+ * copy-on-write working subject, and the caller's `subject` is never mutated. An
691
+ * unscoped restriction stops the remaining passes because no later finding can be
692
+ * more severe.
693
+ *
694
+ * @param subject - The record to qualify
695
+ * @param definition - The authored qualification definition
696
+ * @returns A fresh, frozen qualification result
697
+ * @throws {@link QualifierError} `'DEFINITION'` when validation is enabled and the
698
+ * definition fails semantic validation, or when the engine rejects a pass as an invalid
699
+ * definition; `'MISMATCH'` when the subject is not a record or already carries the
700
+ * reserved `qualification` key; `'DESTROYED'` after `destroy`, or when the engine
701
+ * reports itself destroyed mid-pass; `'ENGINE'` for every other engine throw
702
+ *
703
+ * @example
704
+ * ```ts
705
+ * import { createQualificationDefinition, createRuling, Qualifier } from '@orkestrel/qualifier'
706
+ * import { createAtom, createLogicalDefinition, createRule } from '@orkestrel/reason'
707
+ *
708
+ * const gates = createLogicalDefinition('gates', 'Eligibility gates', [
709
+ * createRule('licensed', [createAtom('licensed', 'equals', false)], createAtom('blocked', 'equals', true)),
710
+ * ])
711
+ * const definition = createQualificationDefinition('standard', 'Standard eligibility', [gates], {
712
+ * rulings: [createRuling('license', 'gates', 'licensed', 'restriction')],
713
+ * })
714
+ *
715
+ * const qualifier = new Qualifier()
716
+ * qualifier.qualify({ id: 'risk-1', licensed: false }, definition).eligibility // 'ineligible'
717
+ * qualifier.destroy()
718
+ * ```
719
+ */
720
+ qualify(subject: Subject, definition: QualificationDefinition): QualificationResult;
721
+ /**
722
+ * Validates one authored definition semantically, without running it.
723
+ *
724
+ * @remarks
725
+ * Structural shape is `isQualificationDefinition`'s job. This checks the meaning:
726
+ * non-empty id and name, valid and uniquely identified passes and rulings, every
727
+ * ruling reference resolving to a logical pass and one of its rules, and no pass id
728
+ * shadowing `QUALIFICATION_KEY`. An empty definition, a logical pass carrying no
729
+ * ruling, and a derivation no later pass reads are warnings rather than errors.
730
+ *
731
+ * @param definition - The authored qualification definition
732
+ * @returns A fresh validation result carrying `valid`, `errors`, and `warnings`
733
+ * @throws {@link QualifierError} `'DESTROYED'` after `destroy`
734
+ *
735
+ * @example
736
+ * ```ts
737
+ * import { createQualificationDefinition, Qualifier } from '@orkestrel/qualifier'
738
+ *
739
+ * const qualifier = new Qualifier()
740
+ * qualifier.validate(createQualificationDefinition('empty', 'Empty', []))
741
+ * // { valid: true, errors: [], warnings: ['Definition has no passes'] }
742
+ * qualifier.destroy()
743
+ * ```
744
+ */
745
+ validate(definition: QualificationDefinition): ReasonValidationResult;
746
+ /**
747
+ * Destroys this qualifier, idempotently.
748
+ *
749
+ * @remarks
750
+ * An engine this qualifier created is destroyed; an injected engine stays
751
+ * caller-owned and is left alone. The `destroy` event fires before the emitter
752
+ * itself is destroyed, and a later call returns without re-emitting.
753
+ *
754
+ * @example
755
+ * ```ts
756
+ * import { Qualifier } from '@orkestrel/qualifier'
757
+ *
758
+ * const qualifier = new Qualifier()
759
+ * qualifier.destroy()
760
+ * qualifier.destroy() // idempotent
761
+ * ```
762
+ */
763
+ destroy(): void;
764
+ }
666
765
 
667
- /**
668
- * A coded programmer error thrown by the qualifier layer.
669
- *
670
- * @remarks
671
- * `DEFINITION` — a qualification definition failed semantic validation.
672
- * `MISMATCH` — a subject is not a record or already carries the reserved
673
- * `qualification` key. `DESTROYED` — use of a destroyed qualifier.
674
- * `ENGINE` — the underlying reason engine threw while running a pass (the
675
- * original throw is preserved as `context.cause`).
676
- */
677
- export declare class QualifierError extends Error {
678
- readonly code: QualifierErrorCode;
679
- readonly context: unknown;
680
- constructor(code: QualifierErrorCode, message: string, context?: unknown);
681
- }
766
+ /**
767
+ * Represents a coded programmer error thrown by the qualifier layer, carrying a
768
+ * `QualifierErrorCode` and an optional context record.
769
+ *
770
+ * @remarks
771
+ * `DEFINITION` — a qualification definition failed semantic validation; when
772
+ * `qualify`'s own validation raises it, `context.definition` names the
773
+ * definition, and when an engine throw maps to it, `context.pass` names the pass
774
+ * and `context.cause` preserves the original throw. `MISMATCH` — a subject is
775
+ * not a record or already carries the reserved `qualification` key.
776
+ * `DESTROYED` — use of a destroyed qualifier; an engine-mapped one carries
777
+ * `context.pass` and `context.cause`. `ENGINE` — the underlying reason engine
778
+ * threw while running a pass; `context.pass` names the pass and `context.cause`
779
+ * preserves the original throw. `context` is absent for a throw that carries no
780
+ * payload.
781
+ */
782
+ export declare class QualifierError extends Error {
783
+ readonly code: QualifierErrorCode;
784
+ readonly context: QualifierErrorContext | undefined;
785
+ constructor(code: QualifierErrorCode, message: string, context?: QualifierErrorContext);
786
+ }
682
787
 
683
- /** A coded {@link QualifierError} programmer-error code. */
684
- export declare type QualifierErrorCode = 'DEFINITION' | 'MISMATCH' | 'DESTROYED' | 'ENGINE';
685
-
686
- /** The push observation surface of a {@link QualifierInterface} (AGENTS §13). */
687
- export declare type QualifierEventMap = {
688
- readonly derive: readonly [derivation: Derivation];
689
- readonly finding: readonly [finding: Finding];
690
- readonly qualify: readonly [result: QualificationResult];
691
- readonly destroy: readonly [];
692
- };
693
-
694
- /** One qualifier — owns or borrows one reason engine and returns eligibility. */
695
- export declare interface QualifierInterface {
696
- readonly emitter: EmitterInterface<QualifierEventMap>;
697
- qualify(subject: Subject, definition: QualificationDefinition): QualificationResult;
698
- validate(definition: QualificationDefinition): QualificationValidationResult;
699
- destroy(): void;
700
- }
788
+ /** Represents a coded {@link QualifierError} programmer-error code. */
789
+ export declare type QualifierErrorCode = 'DEFINITION' | 'MISMATCH' | 'DESTROYED' | 'ENGINE';
701
790
 
702
- /** Options for `createQualifier` / the `Qualifier` constructor. */
703
- export declare interface QualifierOptions {
704
- readonly engine?: ReasonInterface;
705
- readonly validate?: boolean;
706
- readonly labels?: Readonly<Record<string, string>>;
707
- readonly on?: EmitterHooks<QualifierEventMap>;
708
- readonly error?: EmitterErrorHandler;
709
- }
791
+ /**
792
+ * Represents the structured payload a {@link QualifierError} carries.
793
+ *
794
+ * @remarks
795
+ * `pass` names the pass that was running when the engine threw. `definition` names the
796
+ * qualification definition that failed semantic validation. `cause` is the original throw
797
+ * an engine failure wrapped, and stays `unknown` because a thrown value can be anything.
798
+ * Every member is absent rather than `undefined` when the throw does not carry it.
799
+ */
800
+ export declare interface QualifierErrorContext {
801
+ readonly pass?: string;
802
+ readonly definition?: string;
803
+ readonly cause?: unknown;
804
+ }
710
805
 
711
- /**
712
- * Project a quantitative result into a {@link Derivation} audit record.
713
- *
714
- * @param id - The pass id
715
- * @param result - The evaluated quantitative result
716
- * @returns A fresh derivation
717
- *
718
- * @example
719
- * ```ts
720
- * import { quantitativeResultToDerivation } from '@orkestrel/qualifier'
721
- *
722
- * const result = { reasoning: 'quantitative' as const, value: 500_000, groups: [], count: 1, success: true, trace: ['cap: 500000'], errors: [] }
723
- * quantitativeResultToDerivation('cap', result)
724
- * // { id: 'cap', value: 500000, success: true, trace: ['cap: 500000'], errors: [] }
725
- * ```
726
- */
727
- export declare function quantitativeResultToDerivation(id: string, result: QuantitativeResult): Derivation;
806
+ /** Represents the push observation surface of a {@link QualifierInterface}. */
807
+ export declare type QualifierEventMap = {
808
+ readonly derive: readonly [derivation: Derivation];
809
+ readonly finding: readonly [finding: Finding];
810
+ readonly qualify: readonly [result: QualificationResult];
811
+ readonly destroy: readonly [];
812
+ };
728
813
 
729
- /**
730
- * Project one reason result into the internal qualification namespace.
731
- *
732
- * @remarks
733
- * A quantitative result contributes its numeric `value`. A logical result
734
- * contributes a record of its `conclusion` plus every applied rule's asserted
735
- * conclusion fields (`extractConclusions`). A mismatched pairing contributes
736
- * `false`. The projection is pure — it never mutates the subject or the result.
737
- *
738
- * @param definition - The pass definition that produced the result
739
- * @param result - The evaluated reason result
740
- * @returns The pass's working projection
741
- *
742
- * @example
743
- * ```ts
744
- * import { reasonResultToProjection } from '@orkestrel/qualifier'
745
- * import { factorGroup, quantitativeDefinition, staticFactor } from '@orkestrel/reason'
746
- *
747
- * const cap = quantitativeDefinition('cap', 'TIV cap', [
748
- * factorGroup('limit', 'sum', [staticFactor('base', 500_000)]),
749
- * ])
750
- * const result = {
751
- * reasoning: 'quantitative' as const,
752
- * value: 500_000,
753
- * groups: [],
754
- * count: 1,
755
- * success: true,
756
- * trace: [],
757
- * errors: [],
758
- * }
759
- * reasonResultToProjection(cap, result) // 500000
760
- * ```
761
- */
762
- export declare function reasonResultToProjection(definition: QualificationPass, result: ReasonResult): QualificationProjection;
814
+ /** Owns or borrows one reason engine and returns eligibility. */
815
+ export declare interface QualifierInterface {
816
+ /** Holds the typed observation surface carrying `derive`, `finding`, `qualify`, and `destroy`. */
817
+ readonly emitter: EmitterInterface<QualifierEventMap>;
818
+ /**
819
+ * Qualifies one subject against one authored definition.
820
+ *
821
+ * @remarks
822
+ * Semantic validation runs first when the `validate` option is on, so an invalid
823
+ * definition throws before any pass runs. Passes run in authored order and the
824
+ * caller's `subject` is never mutated: the working projection under
825
+ * `QUALIFICATION_KEY` is built copy-on-write and discarded after the call.
826
+ *
827
+ * @param subject - The record to qualify
828
+ * @param definition - The authored qualification definition
829
+ * @returns A fresh, frozen qualification result
830
+ * @throws {@link QualifierError} `'DEFINITION'` when validation is enabled and the
831
+ * definition fails semantic validation, or when the engine rejects a pass as an invalid
832
+ * definition; `'MISMATCH'` when the subject is not a record or already carries the
833
+ * reserved `qualification` key; `'DESTROYED'` after `destroy`, or when the engine
834
+ * reports itself destroyed mid-pass; `'ENGINE'` for every other engine throw
835
+ *
836
+ * @example
837
+ * ```ts
838
+ * import { createQualificationDefinition, createQualifier, createRuling } from '@orkestrel/qualifier'
839
+ * import { createAtom, createLogicalDefinition, createRule } from '@orkestrel/reason'
840
+ *
841
+ * const gates = createLogicalDefinition('gates', 'Eligibility gates', [
842
+ * createRule('licensed', [createAtom('licensed', 'equals', false)], createAtom('blocked', 'equals', true)),
843
+ * ])
844
+ * const definition = createQualificationDefinition('standard', 'Standard eligibility', [gates], {
845
+ * rulings: [createRuling('license', 'gates', 'licensed', 'restriction')],
846
+ * })
847
+ *
848
+ * const qualifier = createQualifier()
849
+ * qualifier.qualify({ id: 'risk-1', licensed: false }, definition).eligibility // 'ineligible'
850
+ * qualifier.destroy()
851
+ * ```
852
+ */
853
+ qualify(subject: Subject, definition: QualificationDefinition): QualificationResult;
854
+ /**
855
+ * Validates one authored definition semantically, without running it.
856
+ *
857
+ * @remarks
858
+ * Structural shape is `isQualificationDefinition`'s job. This checks the meaning:
859
+ * non-empty id and name, valid and uniquely identified passes and rulings, every
860
+ * ruling reference resolving to a logical pass and one of its rules, and no pass id
861
+ * shadowing `QUALIFICATION_KEY`. An empty definition, a logical pass carrying no
862
+ * ruling, and a derivation no later pass reads are warnings rather than errors.
863
+ *
864
+ * @param definition - The authored qualification definition
865
+ * @returns A fresh validation result carrying `valid`, `errors`, and `warnings`
866
+ * @throws {@link QualifierError} `'DESTROYED'` after `destroy`
867
+ *
868
+ * @example
869
+ * ```ts
870
+ * import { createQualificationDefinition, createQualifier } from '@orkestrel/qualifier'
871
+ *
872
+ * const qualifier = createQualifier()
873
+ * qualifier.validate(createQualificationDefinition('empty', 'Empty', []))
874
+ * // { valid: true, errors: [], warnings: ['Definition has no passes'] }
875
+ * qualifier.destroy()
876
+ * ```
877
+ */
878
+ validate(definition: QualificationDefinition): ReasonValidationResult;
879
+ /**
880
+ * Destroys this qualifier, idempotently.
881
+ *
882
+ * @remarks
883
+ * An owned engine is destroyed; an injected engine stays caller-owned and is left
884
+ * alone. The `destroy` event fires before the emitter itself is destroyed, and a
885
+ * later call returns without re-emitting.
886
+ *
887
+ * @example
888
+ * ```ts
889
+ * import { createQualifier } from '@orkestrel/qualifier'
890
+ *
891
+ * const qualifier = createQualifier()
892
+ * qualifier.destroy()
893
+ * qualifier.destroy() // idempotent
894
+ * ```
895
+ */
896
+ destroy(): void;
897
+ }
763
898
 
764
- /** An authored consequence for one rule in one logical pass. */
765
- export declare interface Ruling {
766
- readonly id: string;
767
- readonly pass: string;
768
- readonly rule: string;
769
- readonly effect: QualificationEffect;
770
- readonly scope?: string;
771
- readonly message?: string;
772
- }
899
+ /**
900
+ * Carries the options for `createQualifier` and the `Qualifier` constructor.
901
+ *
902
+ * @remarks
903
+ * `engine` injects a reason engine that stays caller-owned and is never destroyed; an
904
+ * absent `engine` makes the qualifier create and own one. `validate` runs semantic
905
+ * validation before every qualification. Default: `true`. `labels` overrides the display
906
+ * name of a premise field, keyed by the dot-joined field path `formatField` produces, and
907
+ * takes precedence over a premise's own `label`. `on` carries the initial emitter hooks,
908
+ * and `error` handles a listener throw.
909
+ */
910
+ export declare interface QualifierOptions {
911
+ readonly engine?: ReasonInterface;
912
+ readonly validate?: boolean;
913
+ readonly labels?: Readonly<Record<string, string>>;
914
+ readonly on?: EmitterHooks<QualifierEventMap>;
915
+ readonly error?: EmitterErrorHandler;
916
+ }
773
917
 
774
- /**
775
- * Build a {@link Ruling} — one authored consequence for one rule in one pass.
776
- *
777
- * @param id - The ruling id
778
- * @param pass - The logical pass id the rule lives in
779
- * @param rule - The rule id whose firing this ruling reacts to
780
- * @param effect - The eligibility impact when the rule fires
781
- * @param input - Optional selection scope and message template
782
- * @returns A fresh ruling
783
- *
784
- * @example
785
- * ```ts
786
- * import { rulingDefinition } from '@orkestrel/qualifier'
787
- *
788
- * rulingDefinition('license', 'gates', 'licensed', 'restriction', { message: 'A license is required' })
789
- * ```
790
- */
791
- export declare function rulingDefinition(id: string, pass: string, rule: string, effect: QualificationEffect, input?: RulingInput): Ruling;
918
+ /**
919
+ * Projects a quantitative result into a {@link Derivation} audit record.
920
+ *
921
+ * @param id - The pass id
922
+ * @param result - The evaluated quantitative result
923
+ * @returns A fresh derivation
924
+ *
925
+ * @example
926
+ * ```ts
927
+ * import { quantitativeResultToDerivation } from '@orkestrel/qualifier'
928
+ *
929
+ * const result = { reasoning: 'quantitative' as const, value: 500_000, groups: [], count: 1, success: true, trace: ['cap: 500000'], errors: [] }
930
+ * quantitativeResultToDerivation('cap', result)
931
+ * // { id: 'cap', value: 500000, success: true, trace: ['cap: 500000'], errors: [] }
932
+ * ```
933
+ */
934
+ export declare function quantitativeResultToDerivation(id: string, result: QuantitativeResult): Derivation;
792
935
 
793
- /** Optional fields accepted by `rulingDefinition`. */
794
- export declare interface RulingInput {
795
- readonly scope?: string;
796
- readonly message?: string;
797
- }
936
+ /**
937
+ * Projects one reason result into the internal qualification namespace.
938
+ *
939
+ * @remarks
940
+ * A quantitative result contributes its numeric `value`. A logical result
941
+ * contributes a record of its `conclusion` plus every applied rule's asserted
942
+ * conclusion fields (`extractConclusions`). A mismatched pairing contributes
943
+ * `false`. The projection is pure — it never mutates the subject or the result.
944
+ *
945
+ * @param definition - The pass definition that produced the result
946
+ * @param result - The evaluated reason result
947
+ * @returns The pass's working projection
948
+ *
949
+ * @example
950
+ * ```ts
951
+ * import { reasonResultToProjection } from '@orkestrel/qualifier'
952
+ * import { createFactorGroup, createQuantitativeDefinition, createStaticFactor } from '@orkestrel/reason'
953
+ *
954
+ * const cap = createQuantitativeDefinition('cap', 'TIV cap', [
955
+ * createFactorGroup('limit', 'sum', [createStaticFactor('base', 500_000)]),
956
+ * ])
957
+ * const result = {
958
+ * reasoning: 'quantitative' as const,
959
+ * value: 500_000,
960
+ * groups: [],
961
+ * count: 1,
962
+ * success: true,
963
+ * trace: [],
964
+ * errors: [],
965
+ * }
966
+ * reasonResultToProjection(cap, result) // 500000
967
+ * ```
968
+ */
969
+ export declare function reasonResultToProjection(definition: QualificationPass, result: ReasonResult): QualificationProjection;
798
970
 
799
- /**
800
- * Join a ruling, its logical rule result, the pass, the pre-projection subject,
801
- * and an evaluator into a {@link Finding}.
802
- *
803
- * @remarks
804
- * `applied` reflects whether the ruling's rule fired. Premises are re-evaluated
805
- * against the SAME subject snapshot the pass evaluated, so a conclusion the pass
806
- * derived can never satisfy its own reported premise. The message interpolates
807
- * against that snapshot.
808
- *
809
- * @param ruling - The authored ruling
810
- * @param pass - The pass the ruling's rule lives in
811
- * @param result - The pass's evaluated logical result
812
- * @param subject - The pre-projection subject snapshot the pass evaluated
813
- * @param evaluator - The shared reason check evaluator
814
- * @param labels - Optional field-to-label overrides, keyed by dot-joined field
815
- * @returns A fresh finding
816
- *
817
- * @example
818
- * ```ts
819
- * import { rulingToFinding } from '@orkestrel/qualifier'
820
- * import { atom, createEvaluator, logicalDefinition, rule } from '@orkestrel/reason'
821
- *
822
- * const gates = logicalDefinition('gates', 'Gates', [
823
- * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),
824
- * ])
825
- * const ruling = { id: 'license', pass: 'gates', rule: 'licensed', effect: 'restriction' as const }
826
- * const result = { reasoning: 'logical' as const, conclusion: true, rules: [{ id: 'licensed', applied: true, premises: [true], conclusion: true }], count: 1, success: true, trace: [], errors: [] }
827
- * const finding = rulingToFinding(ruling, gates, result, { licensed: false }, createEvaluator())
828
- *
829
- * finding.applied // true
830
- * ```
831
- */
832
- export declare function rulingToFinding(ruling: Ruling, pass: QualificationPass, result: LogicalResult, subject: Subject, evaluator: EvaluatorInterface, labels?: Readonly<Record<string, string>>): Finding;
971
+ /**
972
+ * Renders a {@link Premise} comparison as a display-neutral verb phrase.
973
+ *
974
+ * @param comparison - The comparison to describe
975
+ * @returns A display-neutral phrase
976
+ *
977
+ * @example
978
+ * ```ts
979
+ * import { renderComparison } from '@orkestrel/qualifier'
980
+ *
981
+ * renderComparison('above') // 'is more than'
982
+ * ```
983
+ */
984
+ export declare function renderComparison(comparison: NonNullable<Premise['comparison']>): string;
985
+
986
+ /**
987
+ * Renders one {@link Premise} into a display-neutral sentence.
988
+ *
989
+ * @remarks
990
+ * The checked form renders only when `field` and `comparison` are both
991
+ * present, and `description` then goes unused; a premise missing either half
992
+ * of the checked pair renders as described instead.
993
+ *
994
+ * @param entry - The premise to render
995
+ * @param labels - Optional field-to-label overrides, keyed by dot-joined field
996
+ * @returns A display-neutral sentence
997
+ *
998
+ * @example
999
+ * ```ts
1000
+ * import { renderPremise } from '@orkestrel/qualifier'
1001
+ *
1002
+ * renderPremise({ field: 'age', comparison: 'above', expected: 18, actual: 25, met: true })
1003
+ * // 'age is more than 18 → met'
1004
+ *
1005
+ * renderPremise({ description: 'Applicant is enrolled', met: true })
1006
+ * // 'Applicant is enrolled → met'
1007
+ * ```
1008
+ */
1009
+ export declare function renderPremise(entry: Premise, labels?: Readonly<Record<string, string>>): string;
1010
+
1011
+ /**
1012
+ * Renders a structured or scalar expected value display-neutrally.
1013
+ *
1014
+ * @remarks
1015
+ * An array renders as its elements, each `String`-coerced, joined with `', '`.
1016
+ * A reason `Bounds`-shaped record (the `between` / `outside` expected value)
1017
+ * renders its present sides joined with the word `and`; an absent side is
1018
+ * omitted. A finite number and everything else fall back to plain `String()` —
1019
+ * a finite number is not grouped here (grouping is {@link interpolateMessage}'s
1020
+ * message-token-only behavior).
1021
+ *
1022
+ * @param value - The value to render
1023
+ * @returns A display-neutral rendering
1024
+ *
1025
+ * @example
1026
+ * ```ts
1027
+ * import { renderValue } from '@orkestrel/qualifier'
1028
+ *
1029
+ * renderValue([18, 25, 40]) // '18, 25, 40'
1030
+ * renderValue({ minimum: 18, maximum: 65 }) // '18 and 65'
1031
+ * renderValue(42) // '42'
1032
+ * ```
1033
+ */
1034
+ export declare function renderValue(value: unknown): string;
1035
+
1036
+ /**
1037
+ * Builds rich premises for one fired {@link Rule} by walking its premise atoms
1038
+ * and re-evaluating each against the working subject.
1039
+ *
1040
+ * @remarks
1041
+ * A reason rule result carries only booleans, so this is the qualifier's own
1042
+ * premise-audit projection: each authored premise expression is flattened to its
1043
+ * atom leaves through `extractAtoms`, and each leaf's `Check` is re-evaluated through
1044
+ * the injected `evaluator`. A membership check (`any` / `none`) over an empty
1045
+ * array value is content-free and is skipped.
1046
+ *
1047
+ * @param rule - The authored rule
1048
+ * @param working - The working subject to evaluate against
1049
+ * @param evaluator - The shared reason check evaluator
1050
+ * @param labels - Optional field-to-label overrides, keyed by dot-joined field
1051
+ * @returns A fresh, ordered list of rich premises
1052
+ *
1053
+ * @example
1054
+ * ```ts
1055
+ * import { ruleToPremises } from '@orkestrel/qualifier'
1056
+ * import { createAtom, createEvaluator, createLogicalDefinition, createRule } from '@orkestrel/reason'
1057
+ *
1058
+ * const gates = createLogicalDefinition('gates', 'Gates', [
1059
+ * createRule('licensed', [createAtom('licensed', 'equals', false)], createAtom('blocked', 'equals', true)),
1060
+ * ])
1061
+ * const evaluator = createEvaluator()
1062
+ * const premises = ruleToPremises(gates.rules[0], { licensed: false }, evaluator)
1063
+ *
1064
+ * premises[0]?.met // true
1065
+ * ```
1066
+ */
1067
+ export declare function ruleToPremises(rule: Rule, working: Subject, evaluator: EvaluatorInterface, labels?: Readonly<Record<string, string>>): readonly Premise[];
1068
+
1069
+ /** Represents an authored consequence for one rule in one logical pass. */
1070
+ export declare interface Ruling {
1071
+ readonly id: string;
1072
+ readonly pass: string;
1073
+ readonly rule: string;
1074
+ readonly effect: QualificationEffect;
1075
+ readonly scope?: string;
1076
+ readonly message?: string;
1077
+ }
1078
+
1079
+ /** Carries the optional fields `createRuling` accepts. */
1080
+ export declare interface RulingInput {
1081
+ readonly scope?: string;
1082
+ readonly message?: string;
1083
+ }
1084
+
1085
+ /**
1086
+ * Joins a ruling, its logical rule result, the pass, the pre-projection subject,
1087
+ * and an evaluator into a {@link Finding}.
1088
+ *
1089
+ * @remarks
1090
+ * `applied` reflects whether the ruling's rule fired. Premises are re-evaluated
1091
+ * against the same subject snapshot the pass evaluated, so a conclusion the pass
1092
+ * derived can never satisfy its own reported premise. The message interpolates
1093
+ * against that snapshot.
1094
+ *
1095
+ * @param ruling - The authored ruling
1096
+ * @param pass - The pass the ruling's rule lives in
1097
+ * @param result - The pass's evaluated logical result
1098
+ * @param subject - The pre-projection subject snapshot the pass evaluated
1099
+ * @param evaluator - The shared reason check evaluator
1100
+ * @param labels - Optional field-to-label overrides, keyed by dot-joined field
1101
+ * @returns A fresh finding
1102
+ *
1103
+ * @example
1104
+ * ```ts
1105
+ * import { rulingToFinding } from '@orkestrel/qualifier'
1106
+ * import { createAtom, createEvaluator, createLogicalDefinition, createRule } from '@orkestrel/reason'
1107
+ *
1108
+ * const gates = createLogicalDefinition('gates', 'Gates', [
1109
+ * createRule('licensed', [createAtom('licensed', 'equals', false)], createAtom('blocked', 'equals', true)),
1110
+ * ])
1111
+ * const ruling = { id: 'license', pass: 'gates', rule: 'licensed', effect: 'restriction' as const }
1112
+ * const result = { reasoning: 'logical' as const, conclusion: true, rules: [{ id: 'licensed', applied: true, premises: [true] }], count: 1, success: true, trace: [], errors: [] }
1113
+ * const finding = rulingToFinding(ruling, gates, result, { licensed: false }, createEvaluator())
1114
+ *
1115
+ * finding.applied // true
1116
+ * ```
1117
+ */
1118
+ export declare function rulingToFinding(ruling: Ruling, pass: QualificationPass, result: LogicalResult, subject: Subject, evaluator: EvaluatorInterface, labels?: Readonly<Record<string, string>>): Finding;
833
1119
 
834
- export { }
1120
+ export { }