@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.
@@ -3,17 +3,26 @@ let _orkestrel_contract = require("@orkestrel/contract");
3
3
  let _orkestrel_reason = require("@orkestrel/reason");
4
4
  let _orkestrel_emitter = require("@orkestrel/emitter");
5
5
  //#region src/core/constants.ts
6
- /** Default definition validation policy for `createQualifier` / `Qualifier.qualify`. */
6
+ /**
7
+ * Holds the default definition validation policy for `createQualifier` and
8
+ * `Qualifier.qualify`, `true`.
9
+ */
7
10
  var DEFAULT_QUALIFIER_VALIDATE = true;
8
- /** The reserved internal projection namespace a pass's working projection is written under. */
11
+ /**
12
+ * Names `'qualification'`, the reserved internal projection namespace a pass's working
13
+ * projection is written under.
14
+ */
9
15
  var QUALIFICATION_KEY = "qualification";
10
- /** Eligibility severity order — most to least severe. */
16
+ /** Lists the eligibility severities most to least severe: `ineligible`, `referral`, `eligible`. */
11
17
  var ELIGIBILITY_PRECEDENCE = Object.freeze([
12
18
  "ineligible",
13
19
  "referral",
14
20
  "eligible"
15
21
  ]);
16
- /** The eligibility impact of each {@link QualificationEffect}; `condition` remains eligible. */
22
+ /**
23
+ * Maps each {@link QualificationEffect} to its eligibility impact — `restriction` to
24
+ * `ineligible`, `referral` to `referral`, and `condition` to `eligible`.
25
+ */
17
26
  var EFFECT_ELIGIBILITIES = Object.freeze({
18
27
  restriction: "ineligible",
19
28
  referral: "referral",
@@ -22,14 +31,20 @@ var EFFECT_ELIGIBILITIES = Object.freeze({
22
31
  //#endregion
23
32
  //#region src/core/errors.ts
24
33
  /**
25
- * A coded programmer error thrown by the qualifier layer.
34
+ * Represents a coded programmer error thrown by the qualifier layer, carrying a
35
+ * `QualifierErrorCode` and an optional context record.
26
36
  *
27
37
  * @remarks
28
- * `DEFINITION` — a qualification definition failed semantic validation.
29
- * `MISMATCH` — a subject is not a record or already carries the reserved
30
- * `qualification` key. `DESTROYED` — use of a destroyed qualifier.
31
- * `ENGINE` — the underlying reason engine threw while running a pass (the
32
- * original throw is preserved as `context.cause`).
38
+ * `DEFINITION` — a qualification definition failed semantic validation; when
39
+ * `qualify`'s own validation raises it, `context.definition` names the
40
+ * definition, and when an engine throw maps to it, `context.pass` names the pass
41
+ * and `context.cause` preserves the original throw. `MISMATCH` — a subject is
42
+ * not a record or already carries the reserved `qualification` key.
43
+ * `DESTROYED` — use of a destroyed qualifier; an engine-mapped one carries
44
+ * `context.pass` and `context.cause`. `ENGINE` — the underlying reason engine
45
+ * threw while running a pass; `context.pass` names the pass and `context.cause`
46
+ * preserves the original throw. `context` is absent for a throw that carries no
47
+ * payload.
33
48
  */
34
49
  var QualifierError = class extends Error {
35
50
  code;
@@ -41,39 +56,66 @@ var QualifierError = class extends Error {
41
56
  this.context = context;
42
57
  }
43
58
  };
44
- /** Narrow a caught value to a {@link QualifierError}. */
59
+ /**
60
+ * Narrows a caught value to a {@link QualifierError}.
61
+ *
62
+ * @param value - The caught value to test
63
+ * @returns True if `value` is a {@link QualifierError} instance; false otherwise
64
+ */
45
65
  function isQualifierError(value) {
46
66
  return value instanceof QualifierError;
47
67
  }
48
68
  //#endregion
49
69
  //#region src/core/validators.ts
50
- /** Determine whether a value is an {@link Eligibility} literal. */
70
+ /**
71
+ * Determines whether a value is an {@link Eligibility} literal.
72
+ *
73
+ * @remarks
74
+ * The guard is total and leaves nothing unchecked: a scalar union has no open or exact
75
+ * axis.
76
+ *
77
+ * @param value - The value to test
78
+ * @returns True if `value` is one of the eligibility literals; false otherwise
79
+ */
51
80
  var isEligibility = (0, _orkestrel_contract.literalOf)("eligible", "ineligible", "referral");
52
- /** Determine whether a value is a {@link QualificationEffect} literal. */
81
+ /**
82
+ * Determines whether a value is a {@link QualificationEffect} literal.
83
+ *
84
+ * @remarks
85
+ * The guard is total and leaves nothing unchecked: a scalar union has no open or exact
86
+ * axis.
87
+ *
88
+ * @param value - The value to test
89
+ * @returns True if `value` is one of the effect literals; false otherwise
90
+ */
53
91
  var isQualificationEffect = (0, _orkestrel_contract.literalOf)("restriction", "referral", "condition");
54
92
  /**
55
- * Determine whether a value is an open string-keyed record of {@link Eligibility} values.
93
+ * Determines whether a value is an open string-keyed record of {@link Eligibility} values.
56
94
  *
57
95
  * @remarks
58
- * Every own string-named property is checked, including non-enumerable properties. Inherited and
59
- * symbol-named members are left unchecked because they are outside the record this guard certifies.
96
+ * Every own string-named value is checked through {@link isEligibility}, including
97
+ * non-enumerable values. Inherited and symbol-named members are left unchecked because they
98
+ * are outside the own string-keyed record this guard certifies.
60
99
  *
61
100
  * @param value - The value to test
62
- * @returns `true` when `value` is a non-array object whose own string-named values are eligibilities
101
+ * @returns True if `value` is a non-array object whose own string-named values are
102
+ * eligibilities; false otherwise
63
103
  */
64
104
  function isEligibilityRecord(value) {
65
105
  return (0, _orkestrel_contract.whereOf)((0, _orkestrel_contract.objectOf)({}), (record) => Object.getOwnPropertyNames(record).every((key) => isEligibility(Reflect.get(record, key))))(value);
66
106
  }
67
107
  /**
68
- * Determine whether a value is an open result-side {@link Premise}.
108
+ * Determines whether a value is an open result-side {@link Premise}.
69
109
  *
70
110
  * @remarks
71
- * `expected` is unchecked and may be absent or contain any value because its published type is
72
- * `unknown`. `actual` is unchecked and may be absent or contain any value for the same reason.
73
- * Unknown members are admitted.
111
+ * The optional `field`, `label`, `description`, `comparison`, and `met` members are checked
112
+ * when they are defined. `expected` is unchecked and may be absent or contain any value
113
+ * because its published type is `unknown`. `actual` is unchecked and may be absent or contain
114
+ * any value for the same reason. Unknown members are admitted, because an open result admits
115
+ * what the published contract does not name.
74
116
  *
75
117
  * @param value - The value to test
76
- * @returns `true` when every checked premise member follows its published type
118
+ * @returns True if every checked premise member follows its published type; false otherwise
77
119
  */
78
120
  function isPremise(value) {
79
121
  return (0, _orkestrel_contract.objectOf)({
@@ -91,14 +133,16 @@ function isPremise(value) {
91
133
  ])(value);
92
134
  }
93
135
  /**
94
- * Determine whether a value is an open result-side {@link Finding}.
136
+ * Determines whether a value is an open result-side {@link Finding}.
95
137
  *
96
138
  * @remarks
97
- * Unknown members are admitted. Optional `scope` and `message` members may be absent or
98
- * `undefined`; a present defined value must be a string.
139
+ * Every published member is checked, including each nested {@link Premise}. Optional `scope`
140
+ * and `message` members may be absent or `undefined`; a present defined value must be a
141
+ * string. Unknown members are admitted, because a foreign result implementation may add them
142
+ * without changing the published contract.
99
143
  *
100
144
  * @param value - The value to test
101
- * @returns `true` when every published finding member is valid
145
+ * @returns True if every published finding member is valid; false otherwise
102
146
  */
103
147
  function isFinding(value) {
104
148
  return (0, _orkestrel_contract.objectOf)({
@@ -113,14 +157,16 @@ function isFinding(value) {
113
157
  }, ["scope", "message"])(value);
114
158
  }
115
159
  /**
116
- * Determine whether a value is an open result-side {@link Derivation}.
160
+ * Determines whether a value is an open result-side {@link Derivation}.
117
161
  *
118
162
  * @remarks
119
- * Unknown members are admitted. `value` follows the published `number` type, including `NaN` and
120
- * infinities.
163
+ * Every published member is checked: `value` follows the published `number` type, including
164
+ * `NaN` and the infinities, and `trace` and `errors` hold strings. Unknown members are
165
+ * admitted, because a foreign result implementation may add them without changing the
166
+ * published contract.
121
167
  *
122
168
  * @param value - The value to test
123
- * @returns `true` when every published derivation member is valid
169
+ * @returns True if every published derivation member is valid; false otherwise
124
170
  */
125
171
  function isDerivation(value) {
126
172
  return (0, _orkestrel_contract.objectOf)({
@@ -132,14 +178,17 @@ function isDerivation(value) {
132
178
  })(value);
133
179
  }
134
180
  /**
135
- * Determine whether a value is an open {@link QualificationResult} returned by a qualifier.
181
+ * Determines whether a value is an open {@link QualificationResult} returned by a qualifier.
136
182
  *
137
183
  * @remarks
138
184
  * This guard is result-postured for values returned through a borrowed qualifier interface. It
139
- * admits unknown members and class instances while checking the complete published result closure.
185
+ * checks the complete published result closure — eligibility, scope values, findings,
186
+ * derivations, success, and the audit trail — and admits unknown members and class instances,
187
+ * because a borrowed `QualifierInterface` implementation may add them beyond the
188
+ * published contract.
140
189
  *
141
190
  * @param value - The value to test
142
- * @returns `true` when every published qualification-result member is valid
191
+ * @returns True if every published qualification-result member is valid; false otherwise
143
192
  */
144
193
  function isQualificationResult(value) {
145
194
  return (0, _orkestrel_contract.objectOf)({
@@ -155,23 +204,29 @@ function isQualificationResult(value) {
155
204
  })(value);
156
205
  }
157
206
  /**
158
- * Determine whether a value is an open {@link QualificationValidationResult} returned by a qualifier.
207
+ * Determines whether a value is a {@link QualificationPass} (a quantitative or logical definition).
159
208
  *
160
209
  * @remarks
161
- * `QualificationValidationResult` is an alias of reasons' validation-result type, so this IS
162
- * reasons' published open result guard by delegation — the alias tracks reasons' shape
163
- * automatically, and a hand-written member list here would silently stop checking a member a
164
- * later reason release adds.
210
+ * The guard is exact and requires either complete reason shape, the quantitative definition or
211
+ * the logical one. Nothing is left unchecked, because the reason package owns both authored
212
+ * input shapes.
165
213
  *
166
214
  * @param value - The value to test
167
- * @returns `true` when every published qualification-validation-result member is valid
215
+ * @returns True if `value` is a complete reason quantitative or logical definition; false otherwise
168
216
  */
169
- var isQualificationValidationResult = _orkestrel_reason.isReasonValidationResult;
170
- /** Determine whether a value is a {@link QualificationPass} (a quantitative or logical definition). */
171
217
  function isQualificationPass(value) {
172
218
  return (0, _orkestrel_reason.isQuantitativeDefinition)(value) || (0, _orkestrel_reason.isLogicalDefinition)(value);
173
219
  }
174
- /** Determine whether a value is an exact {@link Ruling} record. */
220
+ /**
221
+ * Determines whether a value is an exact {@link Ruling} record.
222
+ *
223
+ * @remarks
224
+ * Every authored ruling member and the complete key set are checked. Nothing is left
225
+ * unchecked, because this package owns the authored input record.
226
+ *
227
+ * @param value - The value to test
228
+ * @returns True if `value` carries every ruling member and no unknown key; false otherwise
229
+ */
175
230
  function isRuling(value) {
176
231
  return (0, _orkestrel_contract.recordOf)({
177
232
  id: _orkestrel_contract.isString,
@@ -182,7 +237,17 @@ function isRuling(value) {
182
237
  message: _orkestrel_contract.isString
183
238
  }, ["scope", "message"])(value);
184
239
  }
185
- /** Determine whether a value is an exact {@link QualificationDefinition} record. */
240
+ /**
241
+ * Determines whether a value is an exact {@link QualificationDefinition} record.
242
+ *
243
+ * @remarks
244
+ * Every authored definition member, the nested pass and ruling shapes, and the complete key
245
+ * set are checked. Nothing is left unchecked, because this package owns the authored input
246
+ * record.
247
+ *
248
+ * @param value - The value to test
249
+ * @returns True if `value` carries every definition member and no unknown key; false otherwise
250
+ */
186
251
  function isQualificationDefinition(value) {
187
252
  return (0, _orkestrel_contract.recordOf)({
188
253
  id: _orkestrel_contract.isString,
@@ -200,14 +265,14 @@ function isQualificationDefinition(value) {
200
265
  //#endregion
201
266
  //#region src/core/helpers.ts
202
267
  /**
203
- * Interpolate `{{dotted.path}}` tokens in a message template against a subject.
268
+ * Interpolates `{{dotted.path}}` tokens in a message template against a subject.
204
269
  *
205
270
  * @remarks
206
271
  * Each token is split on `.` into a `FieldPath` array and resolved with
207
- * `resolveField` (a plain string field is ONE key, never dot-split — the split
272
+ * `resolveField` (a plain string field is one key, never dot-split — the split
208
273
  * here is the token-to-path bridge). A finite number renders with `en-US`
209
274
  * thousand grouping (`5010` → `5,010`); any other resolved value String-coerces.
210
- * An UNRESOLVED path renders as the empty string.
275
+ * An unresolved path renders as the empty string.
211
276
  *
212
277
  * @param template - The message template carrying `{{dotted.path}}` tokens
213
278
  * @param subject - The record tokens resolve against
@@ -230,19 +295,19 @@ function interpolateMessage(template, subject) {
230
295
  });
231
296
  }
232
297
  /**
233
- * Describe a {@link Premise} comparison as a display-neutral verb phrase.
298
+ * Renders a {@link Premise} comparison as a display-neutral verb phrase.
234
299
  *
235
300
  * @param comparison - The comparison to describe
236
301
  * @returns A display-neutral phrase
237
302
  *
238
303
  * @example
239
304
  * ```ts
240
- * import { describeComparison } from '@orkestrel/qualifier'
305
+ * import { renderComparison } from '@orkestrel/qualifier'
241
306
  *
242
- * describeComparison('above') // 'is more than'
307
+ * renderComparison('above') // 'is more than'
243
308
  * ```
244
309
  */
245
- function describeComparison(comparison) {
310
+ function renderComparison(comparison) {
246
311
  switch (comparison) {
247
312
  case "equals": return "is";
248
313
  case "not": return "is not";
@@ -257,14 +322,14 @@ function describeComparison(comparison) {
257
322
  }
258
323
  }
259
324
  /**
260
- * Render a structured or scalar expected value display-neutrally.
325
+ * Renders a structured or scalar expected value display-neutrally.
261
326
  *
262
327
  * @remarks
263
328
  * An array renders as its elements, each `String`-coerced, joined with `', '`.
264
329
  * A reason `Bounds`-shaped record (the `between` / `outside` expected value)
265
330
  * renders its present sides joined with the word `and`; an absent side is
266
331
  * omitted. A finite number and everything else fall back to plain `String()` —
267
- * a finite number is NOT grouped here (grouping is {@link interpolateMessage}'s
332
+ * a finite number is not grouped here (grouping is {@link interpolateMessage}'s
268
333
  * message-token-only behavior).
269
334
  *
270
335
  * @param value - The value to render
@@ -272,14 +337,14 @@ function describeComparison(comparison) {
272
337
  *
273
338
  * @example
274
339
  * ```ts
275
- * import { describeValue } from '@orkestrel/qualifier'
340
+ * import { renderValue } from '@orkestrel/qualifier'
276
341
  *
277
- * describeValue([18, 25, 40]) // '18, 25, 40'
278
- * describeValue({ minimum: 18, maximum: 65 }) // '18 and 65'
279
- * describeValue(42) // '42'
342
+ * renderValue([18, 25, 40]) // '18, 25, 40'
343
+ * renderValue({ minimum: 18, maximum: 65 }) // '18 and 65'
344
+ * renderValue(42) // '42'
280
345
  * ```
281
346
  */
282
- function describeValue(value) {
347
+ function renderValue(value) {
283
348
  if (Array.isArray(value)) return value.map((entry) => String(entry)).join(", ");
284
349
  if ((0, _orkestrel_reason.isBounds)(value)) {
285
350
  const sides = [];
@@ -290,10 +355,10 @@ function describeValue(value) {
290
355
  return String(value);
291
356
  }
292
357
  /**
293
- * Render one {@link Premise} into a display-neutral sentence.
358
+ * Renders one {@link Premise} into a display-neutral sentence.
294
359
  *
295
360
  * @remarks
296
- * The checked form renders only when `field` and `comparison` are BOTH
361
+ * The checked form renders only when `field` and `comparison` are both
297
362
  * present, and `description` then goes unused; a premise missing either half
298
363
  * of the checked pair renders as described instead.
299
364
  *
@@ -303,25 +368,25 @@ function describeValue(value) {
303
368
  *
304
369
  * @example
305
370
  * ```ts
306
- * import { describePremise } from '@orkestrel/qualifier'
371
+ * import { renderPremise } from '@orkestrel/qualifier'
307
372
  *
308
- * describePremise({ field: 'age', comparison: 'above', expected: 18, actual: 25, met: true })
373
+ * renderPremise({ field: 'age', comparison: 'above', expected: 18, actual: 25, met: true })
309
374
  * // 'age is more than 18 → met'
310
375
  *
311
- * describePremise({ description: 'Applicant is enrolled', met: true })
376
+ * renderPremise({ description: 'Applicant is enrolled', met: true })
312
377
  * // 'Applicant is enrolled → met'
313
378
  * ```
314
379
  */
315
- function describePremise(entry, labels) {
380
+ function renderPremise(entry, labels) {
316
381
  const status = entry.met === void 0 ? "unknown" : entry.met ? "met" : "not met";
317
382
  if (entry.field === void 0 || entry.comparison === void 0) return `${entry.description ?? "Premise"} → ${status}`;
318
383
  const field = (0, _orkestrel_reason.formatField)(entry.field);
319
384
  const label = labels?.[field] ?? entry.label ?? field;
320
- const expected = entry.expected === void 0 ? "" : ` ${describeValue(entry.expected)}`;
321
- return `${label} ${describeComparison(entry.comparison)}${expected} → ${status}`;
385
+ const expected = entry.expected === void 0 ? "" : ` ${renderValue(entry.expected)}`;
386
+ return `${label} ${renderComparison(entry.comparison)}${expected} → ${status}`;
322
387
  }
323
388
  /**
324
- * Build a {@link Premise} from an authored {@link Check} and its evaluated
389
+ * Builds a {@link Premise} from an authored {@link Check} and its evaluated
325
390
  * `CheckResult`.
326
391
  *
327
392
  * @param check - The authored check
@@ -331,15 +396,15 @@ function describePremise(entry, labels) {
331
396
  *
332
397
  * @example
333
398
  * ```ts
334
- * import { premiseCheck } from '@orkestrel/qualifier'
335
- * import { check } from '@orkestrel/reason'
399
+ * import { checkToPremise } from '@orkestrel/qualifier'
400
+ * import { createCheck } from '@orkestrel/reason'
336
401
  *
337
- * const authored = check('age', 'above', 18)
338
- * premiseCheck(authored, { field: 'age', actual: 25, met: true })
402
+ * const authored = createCheck('age', 'above', 18)
403
+ * checkToPremise(authored, { field: 'age', actual: 25, met: true })
339
404
  * // { field: 'age', comparison: 'above', expected: 18, actual: 25, met: true }
340
405
  * ```
341
406
  */
342
- function premiseCheck(check, result, labels) {
407
+ function checkToPremise(check, result, labels) {
343
408
  const field = (0, _orkestrel_reason.formatField)(check.field);
344
409
  return {
345
410
  field: check.field,
@@ -351,14 +416,14 @@ function premiseCheck(check, result, labels) {
351
416
  };
352
417
  }
353
418
  /**
354
- * Build rich premises for one fired {@link Rule} by walking its premise atoms
419
+ * Builds rich premises for one fired {@link Rule} by walking its premise atoms
355
420
  * and re-evaluating each against the working subject.
356
421
  *
357
422
  * @remarks
358
423
  * A reason rule result carries only booleans, so this is the qualifier's own
359
424
  * premise-audit projection: each authored premise expression is flattened to its
360
- * atom leaves via `extractAtoms`, and each leaf's `Check` is re-evaluated through
361
- * the injected `evaluator`. A membership check (`any` / `none`) over an EMPTY
425
+ * atom leaves through `extractAtoms`, and each leaf's `Check` is re-evaluated through
426
+ * the injected `evaluator`. A membership check (`any` / `none`) over an empty
362
427
  * array value is content-free and is skipped.
363
428
  *
364
429
  * @param rule - The authored rule
@@ -369,29 +434,29 @@ function premiseCheck(check, result, labels) {
369
434
  *
370
435
  * @example
371
436
  * ```ts
372
- * import { logicalPremises } from '@orkestrel/qualifier'
373
- * import { atom, createEvaluator, logicalDefinition, rule } from '@orkestrel/reason'
437
+ * import { ruleToPremises } from '@orkestrel/qualifier'
438
+ * import { createAtom, createEvaluator, createLogicalDefinition, createRule } from '@orkestrel/reason'
374
439
  *
375
- * const gates = logicalDefinition('gates', 'Gates', [
376
- * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),
440
+ * const gates = createLogicalDefinition('gates', 'Gates', [
441
+ * createRule('licensed', [createAtom('licensed', 'equals', false)], createAtom('blocked', 'equals', true)),
377
442
  * ])
378
443
  * const evaluator = createEvaluator()
379
- * const premises = logicalPremises(gates.rules[0], { licensed: false }, evaluator)
444
+ * const premises = ruleToPremises(gates.rules[0], { licensed: false }, evaluator)
380
445
  *
381
446
  * premises[0]?.met // true
382
447
  * ```
383
448
  */
384
- function logicalPremises(rule, working, evaluator, labels) {
449
+ function ruleToPremises(rule, working, evaluator, labels) {
385
450
  const output = [];
386
451
  for (const premise of rule.premises) for (const atom of (0, _orkestrel_reason.extractAtoms)(premise)) {
387
452
  const { check } = atom;
388
453
  if ((check.operator === "any" || check.operator === "none") && Array.isArray(check.value) && check.value.length === 0) continue;
389
- output.push(premiseCheck(check, evaluator.evaluate(check, working), labels));
454
+ output.push(checkToPremise(check, evaluator.evaluate(check, working), labels));
390
455
  }
391
456
  return output;
392
457
  }
393
458
  /**
394
- * Locate an authored {@link Rule} by id.
459
+ * Locates an authored {@link Rule} by id.
395
460
  *
396
461
  * @param definition - The logical definition to search
397
462
  * @param id - The rule id
@@ -400,10 +465,10 @@ function logicalPremises(rule, working, evaluator, labels) {
400
465
  * @example
401
466
  * ```ts
402
467
  * import { findRule } from '@orkestrel/qualifier'
403
- * import { atom, logicalDefinition, rule } from '@orkestrel/reason'
468
+ * import { createAtom, createLogicalDefinition, createRule } from '@orkestrel/reason'
404
469
  *
405
- * const gates = logicalDefinition('gates', 'Gates', [
406
- * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),
470
+ * const gates = createLogicalDefinition('gates', 'Gates', [
471
+ * createRule('licensed', [createAtom('licensed', 'equals', false)], createAtom('blocked', 'equals', true)),
407
472
  * ])
408
473
  *
409
474
  * findRule(gates, 'licensed')?.id // 'licensed'
@@ -413,7 +478,7 @@ function findRule(definition, id) {
413
478
  return definition.rules.find((rule) => rule.id === id);
414
479
  }
415
480
  /**
416
- * Project one reason result into the internal qualification namespace.
481
+ * Projects one reason result into the internal qualification namespace.
417
482
  *
418
483
  * @remarks
419
484
  * A quantitative result contributes its numeric `value`. A logical result
@@ -428,10 +493,10 @@ function findRule(definition, id) {
428
493
  * @example
429
494
  * ```ts
430
495
  * import { reasonResultToProjection } from '@orkestrel/qualifier'
431
- * import { factorGroup, quantitativeDefinition, staticFactor } from '@orkestrel/reason'
496
+ * import { createFactorGroup, createQuantitativeDefinition, createStaticFactor } from '@orkestrel/reason'
432
497
  *
433
- * const cap = quantitativeDefinition('cap', 'TIV cap', [
434
- * factorGroup('limit', 'sum', [staticFactor('base', 500_000)]),
498
+ * const cap = createQuantitativeDefinition('cap', 'TIV cap', [
499
+ * createFactorGroup('limit', 'sum', [createStaticFactor('base', 500_000)]),
435
500
  * ])
436
501
  * const result = {
437
502
  * reasoning: 'quantitative' as const,
@@ -459,7 +524,7 @@ function reasonResultToProjection(definition, result) {
459
524
  return projection;
460
525
  }
461
526
  /**
462
- * Project a quantitative result into a {@link Derivation} audit record.
527
+ * Projects a quantitative result into a {@link Derivation} audit record.
463
528
  *
464
529
  * @param id - The pass id
465
530
  * @param result - The evaluated quantitative result
@@ -484,7 +549,7 @@ function quantitativeResultToDerivation(id, result) {
484
549
  };
485
550
  }
486
551
  /**
487
- * Wrap a {@link QualificationContext} under {@link QUALIFICATION_KEY}.
552
+ * Wraps a {@link QualificationContext} under {@link QUALIFICATION_KEY}.
488
553
  *
489
554
  * @param context - The accumulated projection context
490
555
  * @returns A record carrying the context under the reserved key
@@ -500,7 +565,7 @@ function qualificationToRecord(context) {
500
565
  return { [QUALIFICATION_KEY]: context };
501
566
  }
502
567
  /**
503
- * Copy-on-write merge one pass projection into the context.
568
+ * Merges one pass projection into the context, copy-on-write.
504
569
  *
505
570
  * @param context - The current projection context
506
571
  * @param id - The pass id to project under
@@ -522,12 +587,12 @@ function mergeQualificationContext(context, id, projection) {
522
587
  };
523
588
  }
524
589
  /**
525
- * Join a ruling, its logical rule result, the pass, the pre-projection subject,
590
+ * Joins a ruling, its logical rule result, the pass, the pre-projection subject,
526
591
  * and an evaluator into a {@link Finding}.
527
592
  *
528
593
  * @remarks
529
594
  * `applied` reflects whether the ruling's rule fired. Premises are re-evaluated
530
- * against the SAME subject snapshot the pass evaluated, so a conclusion the pass
595
+ * against the same subject snapshot the pass evaluated, so a conclusion the pass
531
596
  * derived can never satisfy its own reported premise. The message interpolates
532
597
  * against that snapshot.
533
598
  *
@@ -542,22 +607,22 @@ function mergeQualificationContext(context, id, projection) {
542
607
  * @example
543
608
  * ```ts
544
609
  * import { rulingToFinding } from '@orkestrel/qualifier'
545
- * import { atom, createEvaluator, logicalDefinition, rule } from '@orkestrel/reason'
610
+ * import { createAtom, createEvaluator, createLogicalDefinition, createRule } from '@orkestrel/reason'
546
611
  *
547
- * const gates = logicalDefinition('gates', 'Gates', [
548
- * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),
612
+ * const gates = createLogicalDefinition('gates', 'Gates', [
613
+ * createRule('licensed', [createAtom('licensed', 'equals', false)], createAtom('blocked', 'equals', true)),
549
614
  * ])
550
615
  * const ruling = { id: 'license', pass: 'gates', rule: 'licensed', effect: 'restriction' as const }
551
- * const result = { reasoning: 'logical' as const, conclusion: true, rules: [{ id: 'licensed', applied: true, premises: [true], conclusion: true }], count: 1, success: true, trace: [], errors: [] }
616
+ * const result = { reasoning: 'logical' as const, conclusion: true, rules: [{ id: 'licensed', applied: true, premises: [true] }], count: 1, success: true, trace: [], errors: [] }
552
617
  * const finding = rulingToFinding(ruling, gates, result, { licensed: false }, createEvaluator())
553
618
  *
554
619
  * finding.applied // true
555
620
  * ```
556
621
  */
557
622
  function rulingToFinding(ruling, pass, result, subject, evaluator, labels) {
558
- const applied = result.rules.find((item) => item.id === ruling.rule)?.applied ?? false;
623
+ const applied = result.rules.find((resolved) => resolved.id === ruling.rule)?.applied ?? false;
559
624
  const rule = pass.reasoning === "logical" ? findRule(pass, ruling.rule) : void 0;
560
- const premises = rule === void 0 ? [] : logicalPremises(rule, subject, evaluator, labels);
625
+ const premises = rule === void 0 ? [] : ruleToPremises(rule, subject, evaluator, labels);
561
626
  return {
562
627
  id: ruling.id,
563
628
  pass: ruling.pass,
@@ -570,7 +635,7 @@ function rulingToFinding(ruling, pass, result, subject, evaluator, labels) {
570
635
  };
571
636
  }
572
637
  /**
573
- * Derive global eligibility from applied, unscoped findings.
638
+ * Derives global eligibility from applied, unscoped findings.
574
639
  *
575
640
  * @remarks
576
641
  * Only findings that both applied and carry no `scope` shape global eligibility.
@@ -578,7 +643,8 @@ function rulingToFinding(ruling, pass, result, subject, evaluator, labels) {
578
643
  * `failed`.
579
644
  *
580
645
  * @param findings - The resolved findings
581
- * @param failed - Whether an operational pass failure occurred
646
+ * @param failed - If `true`, adds a synthetic `referral` impact so an operational failure
647
+ * fails closed; if `false`, derives eligibility from the findings alone. Default: `false`
582
648
  * @returns The most severe global eligibility
583
649
  *
584
650
  * @example
@@ -595,7 +661,7 @@ function deriveFindingEligibility(findings, failed = false) {
595
661
  return combineEligibilities(eligibilities);
596
662
  }
597
663
  /**
598
- * Return the most severe {@link Eligibility} in a list.
664
+ * Returns the most severe {@link Eligibility} in a list.
599
665
  *
600
666
  * @param eligibilities - The eligibilities to combine
601
667
  * @returns The most severe eligibility, or `'eligible'` for an empty list
@@ -612,7 +678,7 @@ function combineEligibilities(eligibilities) {
612
678
  return "eligible";
613
679
  }
614
680
  /**
615
- * Derive one eligibility per finding scope.
681
+ * Derives one eligibility per finding scope.
616
682
  *
617
683
  * @remarks
618
684
  * Only applied, scoped findings contribute. Each named scope combines its own
@@ -642,29 +708,29 @@ function deriveScopeEligibilities(findings) {
642
708
  return scopes;
643
709
  }
644
710
  /**
645
- * Find rulings whose pass or rule does not exist, whose pass is not logical, or
646
- * a pass id shadowing the reserved {@link QUALIFICATION_KEY}.
711
+ * Describes each ruling whose pass or rule does not exist or whose pass is not
712
+ * logical, and each pass id shadowing the reserved {@link QUALIFICATION_KEY}.
647
713
  *
648
714
  * @param definition - The qualification definition to check
649
715
  * @returns A fresh list of reference-error messages
650
716
  *
651
717
  * @example
652
718
  * ```ts
653
- * import { findMissingReferences, qualificationDefinition, rulingDefinition } from '@orkestrel/qualifier'
654
- * import { atom, logicalDefinition, rule } from '@orkestrel/reason'
719
+ * import { createQualificationDefinition, createRuling, describeMissingReferences } from '@orkestrel/qualifier'
720
+ * import { createAtom, createLogicalDefinition, createRule } from '@orkestrel/reason'
655
721
  *
656
- * const gates = logicalDefinition('gates', 'Gates', [
657
- * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),
722
+ * const gates = createLogicalDefinition('gates', 'Gates', [
723
+ * createRule('licensed', [createAtom('licensed', 'equals', false)], createAtom('blocked', 'equals', true)),
658
724
  * ])
659
- * const definition = qualificationDefinition('standard', 'Standard', [gates], {
660
- * rulings: [rulingDefinition('license', 'gates', 'absent', 'restriction')],
725
+ * const definition = createQualificationDefinition('standard', 'Standard', [gates], {
726
+ * rulings: [createRuling('license', 'gates', 'absent', 'restriction')],
661
727
  * })
662
728
  *
663
- * findMissingReferences(definition)
729
+ * describeMissingReferences(definition)
664
730
  * // ["Ruling 'license' references missing rule 'absent' in pass 'gates'"]
665
731
  * ```
666
732
  */
667
- function findMissingReferences(definition) {
733
+ function describeMissingReferences(definition) {
668
734
  const errors = [];
669
735
  const passes = new Map(definition.passes.map((pass) => [pass.id, pass]));
670
736
  for (const pass of definition.passes) if (pass.id === "qualification") errors.push(`Pass id must not equal reserved key '${QUALIFICATION_KEY}'`);
@@ -683,10 +749,10 @@ function findMissingReferences(definition) {
683
749
  return errors;
684
750
  }
685
751
  /**
686
- * Determine whether a subject already owns the reserved {@link QUALIFICATION_KEY}.
752
+ * Determines whether a subject already owns the reserved {@link QUALIFICATION_KEY}.
687
753
  *
688
754
  * @param subject - The subject to check
689
- * @returns `true` when the subject owns the reserved key
755
+ * @returns True if the subject owns the reserved key; false otherwise
690
756
  *
691
757
  * @example
692
758
  * ```ts
@@ -699,7 +765,7 @@ function hasReservedKey(subject) {
699
765
  return Object.hasOwn(subject, QUALIFICATION_KEY);
700
766
  }
701
767
  /**
702
- * Assert a value is a valid qualification {@link Subject}, narrowing it in place.
768
+ * Asserts a value is a valid qualification {@link Subject}, narrowing it in place.
703
769
  *
704
770
  * @param value - The candidate subject to validate
705
771
  * @throws {@link QualifierError} `'MISMATCH'` when the value is not a record, or
@@ -717,12 +783,12 @@ function assertSubject(value) {
717
783
  if (hasReservedKey(value)) throw new QualifierError("MISMATCH", `Qualification subject must not contain reserved key '${QUALIFICATION_KEY}'`);
718
784
  }
719
785
  /**
720
- * Map an engine throw caught while running one pass to a typed {@link QualifierError}.
786
+ * Maps an engine throw caught while running one pass to a typed {@link QualifierError}.
721
787
  *
722
788
  * @remarks
723
789
  * A reason `ReasonError('INVALID')` maps to `DEFINITION` (the engine rejected the
724
790
  * pass as an invalid definition); `ReasonError('DESTROYED')` maps to `DESTROYED`;
725
- * every other `ReasonError` code (e.g. `MISSING`) and every non-`ReasonError` throw
791
+ * every other `ReasonError` code (for example `MISSING`) and every non-`ReasonError` throw
726
792
  * (including a `bail: true` reasoner rethrow) maps to `ENGINE`. The original throw
727
793
  * is preserved as `context.cause`.
728
794
  *
@@ -759,7 +825,7 @@ function mapEngineError(error, pass) {
759
825
  });
760
826
  }
761
827
  /**
762
- * Find logical passes carrying no rulings.
828
+ * Describes each logical pass carrying no rulings.
763
829
  *
764
830
  * @remarks
765
831
  * A logical pass with no matching ruling can never contribute a finding, so its
@@ -771,18 +837,18 @@ function mapEngineError(error, pass) {
771
837
  *
772
838
  * @example
773
839
  * ```ts
774
- * import { findEmptyLogicalPasses, qualificationDefinition } from '@orkestrel/qualifier'
775
- * import { atom, logicalDefinition, rule } from '@orkestrel/reason'
840
+ * import { createQualificationDefinition, describeEmptyLogicalPasses } from '@orkestrel/qualifier'
841
+ * import { createAtom, createLogicalDefinition, createRule } from '@orkestrel/reason'
776
842
  *
777
- * const gates = logicalDefinition('gates', 'Gates', [
778
- * rule('licensed', [atom('licensed', 'equals', false)], atom('blocked', 'equals', true)),
843
+ * const gates = createLogicalDefinition('gates', 'Gates', [
844
+ * createRule('licensed', [createAtom('licensed', 'equals', false)], createAtom('blocked', 'equals', true)),
779
845
  * ])
780
- * const definition = qualificationDefinition('standard', 'Standard', [gates])
846
+ * const definition = createQualificationDefinition('standard', 'Standard', [gates])
781
847
  *
782
- * findEmptyLogicalPasses(definition) // ["Logical pass 'gates' has no rulings"]
848
+ * describeEmptyLogicalPasses(definition) // ["Logical pass 'gates' has no rulings"]
783
849
  * ```
784
850
  */
785
- function findEmptyLogicalPasses(definition) {
851
+ function describeEmptyLogicalPasses(definition) {
786
852
  const warnings = [];
787
853
  for (const pass of definition.passes) {
788
854
  if (pass.reasoning !== "logical") continue;
@@ -791,7 +857,7 @@ function findEmptyLogicalPasses(definition) {
791
857
  return warnings;
792
858
  }
793
859
  /**
794
- * Find quantitative passes never read by a later pass.
860
+ * Describes each quantitative pass never read by a later pass.
795
861
  *
796
862
  * @remarks
797
863
  * A quantitative pass's projected value lives under `qualification.<id>` — if no
@@ -805,18 +871,18 @@ function findEmptyLogicalPasses(definition) {
805
871
  *
806
872
  * @example
807
873
  * ```ts
808
- * import { findUnreadDerivations, qualificationDefinition } from '@orkestrel/qualifier'
809
- * import { factorGroup, quantitativeDefinition, staticFactor } from '@orkestrel/reason'
874
+ * import { createQualificationDefinition, describeUnreadDerivations } from '@orkestrel/qualifier'
875
+ * import { createFactorGroup, createQuantitativeDefinition, createStaticFactor } from '@orkestrel/reason'
810
876
  *
811
- * const cap = quantitativeDefinition('cap', 'TIV cap', [
812
- * factorGroup('limit', 'sum', [staticFactor('base', 500_000)]),
877
+ * const cap = createQuantitativeDefinition('cap', 'TIV cap', [
878
+ * createFactorGroup('limit', 'sum', [createStaticFactor('base', 500_000)]),
813
879
  * ])
814
- * const definition = qualificationDefinition('standard', 'Standard', [cap])
880
+ * const definition = createQualificationDefinition('standard', 'Standard', [cap])
815
881
  *
816
- * findUnreadDerivations(definition) // ["Quantitative pass 'cap' is never read by a later pass"]
882
+ * describeUnreadDerivations(definition) // ["Quantitative pass 'cap' is never read by a later pass"]
817
883
  * ```
818
884
  */
819
- function findUnreadDerivations(definition) {
885
+ function describeUnreadDerivations(definition) {
820
886
  const warnings = [];
821
887
  definition.passes.forEach((pass, index) => {
822
888
  if (pass.reasoning !== "quantitative") return;
@@ -833,73 +899,15 @@ function findUnreadDerivations(definition) {
833
899
  });
834
900
  return warnings;
835
901
  }
836
- /**
837
- * Build a {@link QualificationDefinition}.
838
- *
839
- * @remarks
840
- * Returns a fresh top-level definition, omitting absent optional keys;
841
- * `passes` and `rulings` arrays are copied, and record `metadata` is
842
- * shallow-copied so nested values are not deep-cloned.
843
- *
844
- * @param id - The definition id
845
- * @param name - The display name
846
- * @param passes - The ordered qualification passes
847
- * @param input - Optional description, rulings, and metadata
848
- * @returns A fresh qualification definition
849
- *
850
- * @example
851
- * ```ts
852
- * import { qualificationDefinition } from '@orkestrel/qualifier'
853
- *
854
- * qualificationDefinition('standard', 'Standard', [gates], { rulings: [ruling] })
855
- * ```
856
- */
857
- function qualificationDefinition(id, name, passes, input) {
858
- return {
859
- id,
860
- name,
861
- passes: [...passes],
862
- ...input?.description === void 0 ? {} : { description: input.description },
863
- ...input?.rulings === void 0 ? {} : { rulings: [...input.rulings] },
864
- ...input?.metadata === void 0 ? {} : { metadata: { ...input.metadata } }
865
- };
866
- }
867
- /**
868
- * Build a {@link Ruling} — one authored consequence for one rule in one pass.
869
- *
870
- * @param id - The ruling id
871
- * @param pass - The logical pass id the rule lives in
872
- * @param rule - The rule id whose firing this ruling reacts to
873
- * @param effect - The eligibility impact when the rule fires
874
- * @param input - Optional selection scope and message template
875
- * @returns A fresh ruling
876
- *
877
- * @example
878
- * ```ts
879
- * import { rulingDefinition } from '@orkestrel/qualifier'
880
- *
881
- * rulingDefinition('license', 'gates', 'licensed', 'restriction', { message: 'A license is required' })
882
- * ```
883
- */
884
- function rulingDefinition(id, pass, rule, effect, input) {
885
- return {
886
- id,
887
- pass,
888
- rule,
889
- effect,
890
- ...input?.scope === void 0 ? {} : { scope: input.scope },
891
- ...input?.message === void 0 ? {} : { message: input.message }
892
- };
893
- }
894
902
  //#endregion
895
903
  //#region src/core/Qualifier.ts
896
904
  /**
897
- * A qualifier — runs ordered passes over one reason engine and returns
898
- * eligibility.
905
+ * Runs ordered passes over one reason engine and returns eligibility.
899
906
  *
900
907
  * @remarks
901
- * The engine is OWNED when self-created (destroyed on `destroy()`) and borrowed
902
- * when injected (never destroyed). `qualify` builds its working subject through
908
+ * The engine is owned when self-created (destroyed on `destroy()`) and borrowed
909
+ * when injected (never destroyed). Semantic validation runs before the first pass
910
+ * whenever the `validate` option is on. `qualify` builds its working subject through
903
911
  * copy-on-write overlays only — the caller's `subject` is never mutated, and the
904
912
  * qualifier's internal projection is discarded after each call.
905
913
  */
@@ -925,17 +933,92 @@ var Qualifier = class {
925
933
  this.#validate = options?.validate ?? true;
926
934
  this.#labels = options?.labels;
927
935
  }
936
+ /**
937
+ * Holds the typed observation surface carrying `derive`, `finding`, `qualify`, and
938
+ * `destroy`.
939
+ *
940
+ * @returns The emitter this qualifier owns
941
+ *
942
+ * @example
943
+ * ```ts
944
+ * import { Qualifier } from '@orkestrel/qualifier'
945
+ *
946
+ * const qualifier = new Qualifier()
947
+ * qualifier.emitter.on('qualify', (result) => console.log(result.eligibility))
948
+ * qualifier.destroy()
949
+ * ```
950
+ */
928
951
  get emitter() {
929
952
  return this.#emitter;
930
953
  }
954
+ /**
955
+ * Qualifies one subject against one authored definition.
956
+ *
957
+ * @remarks
958
+ * Semantic validation runs first when the `validate` option is on, so an invalid
959
+ * definition throws before any pass runs. Passes run in authored order over a
960
+ * copy-on-write working subject, and the caller's `subject` is never mutated. An
961
+ * unscoped restriction stops the remaining passes because no later finding can be
962
+ * more severe.
963
+ *
964
+ * @param subject - The record to qualify
965
+ * @param definition - The authored qualification definition
966
+ * @returns A fresh, frozen qualification result
967
+ * @throws {@link QualifierError} `'DEFINITION'` when validation is enabled and the
968
+ * definition fails semantic validation, or when the engine rejects a pass as an invalid
969
+ * definition; `'MISMATCH'` when the subject is not a record or already carries the
970
+ * reserved `qualification` key; `'DESTROYED'` after `destroy`, or when the engine
971
+ * reports itself destroyed mid-pass; `'ENGINE'` for every other engine throw
972
+ *
973
+ * @example
974
+ * ```ts
975
+ * import { createQualificationDefinition, createRuling, Qualifier } from '@orkestrel/qualifier'
976
+ * import { createAtom, createLogicalDefinition, createRule } from '@orkestrel/reason'
977
+ *
978
+ * const gates = createLogicalDefinition('gates', 'Eligibility gates', [
979
+ * createRule('licensed', [createAtom('licensed', 'equals', false)], createAtom('blocked', 'equals', true)),
980
+ * ])
981
+ * const definition = createQualificationDefinition('standard', 'Standard eligibility', [gates], {
982
+ * rulings: [createRuling('license', 'gates', 'licensed', 'restriction')],
983
+ * })
984
+ *
985
+ * const qualifier = new Qualifier()
986
+ * qualifier.qualify({ id: 'risk-1', licensed: false }, definition).eligibility // 'ineligible'
987
+ * qualifier.destroy()
988
+ * ```
989
+ */
931
990
  qualify(subject, definition) {
932
991
  this.#alive();
933
992
  if (this.#validate) {
934
993
  const validation = this.validate(definition);
935
- if (!validation.valid) throw new QualifierError("DEFINITION", `Qualification definition is invalid: ${validation.errors.join(", ")}`, definition.id);
994
+ if (!validation.valid) throw new QualifierError("DEFINITION", `Qualification definition is invalid: ${validation.errors.join(", ")}`, { definition: definition.id });
936
995
  }
937
996
  return this.#qualify(subject, definition);
938
997
  }
998
+ /**
999
+ * Validates one authored definition semantically, without running it.
1000
+ *
1001
+ * @remarks
1002
+ * Structural shape is `isQualificationDefinition`'s job. This checks the meaning:
1003
+ * non-empty id and name, valid and uniquely identified passes and rulings, every
1004
+ * ruling reference resolving to a logical pass and one of its rules, and no pass id
1005
+ * shadowing `QUALIFICATION_KEY`. An empty definition, a logical pass carrying no
1006
+ * ruling, and a derivation no later pass reads are warnings rather than errors.
1007
+ *
1008
+ * @param definition - The authored qualification definition
1009
+ * @returns A fresh validation result carrying `valid`, `errors`, and `warnings`
1010
+ * @throws {@link QualifierError} `'DESTROYED'` after `destroy`
1011
+ *
1012
+ * @example
1013
+ * ```ts
1014
+ * import { createQualificationDefinition, Qualifier } from '@orkestrel/qualifier'
1015
+ *
1016
+ * const qualifier = new Qualifier()
1017
+ * qualifier.validate(createQualificationDefinition('empty', 'Empty', []))
1018
+ * // { valid: true, errors: [], warnings: ['Definition has no passes'] }
1019
+ * qualifier.destroy()
1020
+ * ```
1021
+ */
939
1022
  validate(definition) {
940
1023
  this.#alive();
941
1024
  const errors = [];
@@ -950,16 +1033,33 @@ var Qualifier = class {
950
1033
  });
951
1034
  for (const id of (0, _orkestrel_reason.findDuplicates)(definition.passes)) errors.push(`Duplicate pass id '${id}'`);
952
1035
  for (const id of (0, _orkestrel_reason.findDuplicates)(definition.rulings ?? [])) errors.push(`Duplicate ruling id '${id}'`);
953
- for (const reference of findMissingReferences(definition)) errors.push(reference);
1036
+ for (const reference of describeMissingReferences(definition)) errors.push(reference);
954
1037
  if (definition.passes.length === 0) warnings.push("Definition has no passes");
955
- for (const warning of findEmptyLogicalPasses(definition)) warnings.push(warning);
956
- for (const warning of findUnreadDerivations(definition)) warnings.push(warning);
1038
+ for (const warning of describeEmptyLogicalPasses(definition)) warnings.push(warning);
1039
+ for (const warning of describeUnreadDerivations(definition)) warnings.push(warning);
957
1040
  return {
958
1041
  valid: errors.length === 0,
959
1042
  errors,
960
1043
  warnings
961
1044
  };
962
1045
  }
1046
+ /**
1047
+ * Destroys this qualifier, idempotently.
1048
+ *
1049
+ * @remarks
1050
+ * An engine this qualifier created is destroyed; an injected engine stays
1051
+ * caller-owned and is left alone. The `destroy` event fires before the emitter
1052
+ * itself is destroyed, and a later call returns without re-emitting.
1053
+ *
1054
+ * @example
1055
+ * ```ts
1056
+ * import { Qualifier } from '@orkestrel/qualifier'
1057
+ *
1058
+ * const qualifier = new Qualifier()
1059
+ * qualifier.destroy()
1060
+ * qualifier.destroy() // idempotent
1061
+ * ```
1062
+ */
963
1063
  destroy() {
964
1064
  if (this.#destroyed) return;
965
1065
  if (this.#owned) this.#engine.destroy();
@@ -1035,28 +1135,113 @@ var Qualifier = class {
1035
1135
  //#endregion
1036
1136
  //#region src/core/factories.ts
1037
1137
  /**
1038
- * Create one qualifier over a reason engine.
1138
+ * Creates one {@link QualifierInterface} over a reason engine.
1039
1139
  *
1040
1140
  * @remarks
1041
- * A standalone qualifier creates and OWNS one shared quantitative-plus-logical
1141
+ * A standalone qualifier creates and owns one shared quantitative-plus-logical
1042
1142
  * reason engine, destroying it on `destroy()`. An injected `options.engine`
1043
- * remains caller-owned and is never destroyed. Validation is on by default.
1143
+ * remains caller-owned and is never destroyed. `validate` runs semantic
1144
+ * validation before every qualification. Default: `true`. `labels` overrides the
1145
+ * display name of a premise field, keyed by the dot-joined field path
1146
+ * `formatField` produces, and takes precedence over a premise's own `label`.
1044
1147
  *
1045
1148
  * @param options - Optional injected engine, validation, labels, and emitter hooks
1046
1149
  * @returns A {@link QualifierInterface}
1047
1150
  *
1048
- * @example
1151
+ * @example Create a qualifier
1049
1152
  * ```ts
1050
- * import { createQualifier } from '@orkestrel/qualifier'
1153
+ * import { createQualificationDefinition, createQualifier, createRuling } from '@orkestrel/qualifier'
1154
+ * import { createAtom, createLogicalDefinition, createRule } from '@orkestrel/reason'
1155
+ *
1156
+ * const gates = createLogicalDefinition('gates', 'Eligibility gates', [
1157
+ * createRule(
1158
+ * 'licensed',
1159
+ * [createAtom('licensed', 'equals', false)],
1160
+ * createAtom('blocked', 'equals', true),
1161
+ * ),
1162
+ * ])
1163
+ *
1164
+ * const bare = createRuling('license', 'gates', 'licensed', 'restriction')
1165
+ * const messaged = createRuling('license', 'gates', 'licensed', 'restriction', {
1166
+ * message: 'A license is required',
1167
+ * })
1168
+ *
1169
+ * 'message' in bare // false — an absent optional key is omitted, never written as undefined
1170
+ * messaged.message // 'A license is required'
1171
+ *
1172
+ * const passes = [gates]
1173
+ * const definition = createQualificationDefinition('standard', 'Standard eligibility', passes, {
1174
+ * rulings: [messaged],
1175
+ * })
1176
+ *
1177
+ * 'description' in definition // false
1178
+ * definition.passes === passes // false — the factory copies what it is handed
1051
1179
  *
1052
1180
  * const qualifier = createQualifier()
1053
- * qualifier.qualify({ id: 'risk-1' }, definition)
1181
+ * qualifier.qualify({ id: 'risk-1', licensed: false }, definition)
1054
1182
  * qualifier.destroy()
1055
1183
  * ```
1056
1184
  */
1057
1185
  function createQualifier(options) {
1058
1186
  return new Qualifier(options);
1059
1187
  }
1188
+ /**
1189
+ * Creates a fresh {@link QualificationDefinition}, omitting every absent optional key.
1190
+ *
1191
+ * @remarks
1192
+ * `passes` and `rulings` arrays are copied, and record `metadata` is
1193
+ * shallow-copied so nested values are not deep-cloned.
1194
+ *
1195
+ * @param id - The definition id
1196
+ * @param name - The display name
1197
+ * @param passes - The ordered qualification passes
1198
+ * @param input - Optional description, rulings, and metadata
1199
+ * @returns A fresh qualification definition
1200
+ *
1201
+ * @example
1202
+ * ```ts
1203
+ * import { createQualificationDefinition } from '@orkestrel/qualifier'
1204
+ *
1205
+ * createQualificationDefinition('standard', 'Standard', [gates], { rulings: [ruling] })
1206
+ * ```
1207
+ */
1208
+ function createQualificationDefinition(id, name, passes, input) {
1209
+ return {
1210
+ id,
1211
+ name,
1212
+ passes: [...passes],
1213
+ ...input?.description === void 0 ? {} : { description: input.description },
1214
+ ...input?.rulings === void 0 ? {} : { rulings: [...input.rulings] },
1215
+ ...input?.metadata === void 0 ? {} : { metadata: { ...input.metadata } }
1216
+ };
1217
+ }
1218
+ /**
1219
+ * Creates a fresh {@link Ruling} from the rule it reacts to and the effect it applies.
1220
+ *
1221
+ * @param id - The ruling id
1222
+ * @param pass - The logical pass id the rule lives in
1223
+ * @param rule - The rule id whose firing this ruling reacts to
1224
+ * @param effect - The eligibility impact when the rule fires
1225
+ * @param input - Optional selection scope and message template
1226
+ * @returns A fresh ruling
1227
+ *
1228
+ * @example
1229
+ * ```ts
1230
+ * import { createRuling } from '@orkestrel/qualifier'
1231
+ *
1232
+ * createRuling('license', 'gates', 'licensed', 'restriction', { message: 'A license is required' })
1233
+ * ```
1234
+ */
1235
+ function createRuling(id, pass, rule, effect, input) {
1236
+ return {
1237
+ id,
1238
+ pass,
1239
+ rule,
1240
+ effect,
1241
+ ...input?.scope === void 0 ? {} : { scope: input.scope },
1242
+ ...input?.message === void 0 ? {} : { message: input.message }
1243
+ };
1244
+ }
1060
1245
  //#endregion
1061
1246
  exports.DEFAULT_QUALIFIER_VALIDATE = DEFAULT_QUALIFIER_VALIDATE;
1062
1247
  exports.EFFECT_ELIGIBILITIES = EFFECT_ELIGIBILITIES;
@@ -1065,17 +1250,17 @@ exports.QUALIFICATION_KEY = QUALIFICATION_KEY;
1065
1250
  exports.Qualifier = Qualifier;
1066
1251
  exports.QualifierError = QualifierError;
1067
1252
  exports.assertSubject = assertSubject;
1253
+ exports.checkToPremise = checkToPremise;
1068
1254
  exports.combineEligibilities = combineEligibilities;
1255
+ exports.createQualificationDefinition = createQualificationDefinition;
1069
1256
  exports.createQualifier = createQualifier;
1257
+ exports.createRuling = createRuling;
1070
1258
  exports.deriveFindingEligibility = deriveFindingEligibility;
1071
1259
  exports.deriveScopeEligibilities = deriveScopeEligibilities;
1072
- exports.describeComparison = describeComparison;
1073
- exports.describePremise = describePremise;
1074
- exports.describeValue = describeValue;
1075
- exports.findEmptyLogicalPasses = findEmptyLogicalPasses;
1076
- exports.findMissingReferences = findMissingReferences;
1260
+ exports.describeEmptyLogicalPasses = describeEmptyLogicalPasses;
1261
+ exports.describeMissingReferences = describeMissingReferences;
1262
+ exports.describeUnreadDerivations = describeUnreadDerivations;
1077
1263
  exports.findRule = findRule;
1078
- exports.findUnreadDerivations = findUnreadDerivations;
1079
1264
  exports.hasReservedKey = hasReservedKey;
1080
1265
  exports.interpolateMessage = interpolateMessage;
1081
1266
  exports.isDerivation = isDerivation;
@@ -1087,18 +1272,17 @@ exports.isQualificationDefinition = isQualificationDefinition;
1087
1272
  exports.isQualificationEffect = isQualificationEffect;
1088
1273
  exports.isQualificationPass = isQualificationPass;
1089
1274
  exports.isQualificationResult = isQualificationResult;
1090
- exports.isQualificationValidationResult = isQualificationValidationResult;
1091
1275
  exports.isQualifierError = isQualifierError;
1092
1276
  exports.isRuling = isRuling;
1093
- exports.logicalPremises = logicalPremises;
1094
1277
  exports.mapEngineError = mapEngineError;
1095
1278
  exports.mergeQualificationContext = mergeQualificationContext;
1096
- exports.premiseCheck = premiseCheck;
1097
- exports.qualificationDefinition = qualificationDefinition;
1098
1279
  exports.qualificationToRecord = qualificationToRecord;
1099
1280
  exports.quantitativeResultToDerivation = quantitativeResultToDerivation;
1100
1281
  exports.reasonResultToProjection = reasonResultToProjection;
1101
- exports.rulingDefinition = rulingDefinition;
1282
+ exports.renderComparison = renderComparison;
1283
+ exports.renderPremise = renderPremise;
1284
+ exports.renderValue = renderValue;
1285
+ exports.ruleToPremises = ruleToPremises;
1102
1286
  exports.rulingToFinding = rulingToFinding;
1103
1287
 
1104
1288
  //# sourceMappingURL=index.cjs.map