@orkestrel/qualifier 0.0.13 → 0.0.15

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,18 +1,27 @@
1
- import { arrayOf, isBoolean, isFiniteNumber, isNumber, isRecord, isString, literalOf, objectOf, recordOf, resolveField, whereOf } from "@orkestrel/contract";
1
+ import { arrayOf, isArray, isBoolean, isError, isFiniteNumber, isInstance, isNumber, isRecord, isString, literalOf, objectOf, recordOf, resolveField, whereOf } from "@orkestrel/contract";
2
2
  import { createEvaluator, createLogicalReasoner, createQuantitativeReasoner, createReason, extractAtoms, extractConclusions, findDuplicates, formatField, isBounds, isComparison, isFieldPath, isLogicalDefinition, isQuantitativeDefinition, isReasonError } from "@orkestrel/reason";
3
3
  import { Emitter } from "@orkestrel/emitter";
4
4
  //#region src/core/constants.ts
5
- /** Holds the default definition validation policy for `createQualifier` / `Qualifier.qualify`. */
5
+ /**
6
+ * Holds the default definition validation policy for `createQualifier` and
7
+ * `Qualifier.qualify`, `true`.
8
+ */
6
9
  var DEFAULT_QUALIFIER_VALIDATE = true;
7
- /** Names the reserved internal projection namespace a pass's working projection is written under. */
10
+ /**
11
+ * Names `'qualification'`, the reserved internal projection namespace a pass's working
12
+ * projection is written under.
13
+ */
8
14
  var QUALIFICATION_KEY = "qualification";
9
- /** Lists the eligibility severities in order — most to least severe. */
15
+ /** Lists the eligibility severities most to least severe: `ineligible`, `referral`, `eligible`. */
10
16
  var ELIGIBILITY_PRECEDENCE = Object.freeze([
11
17
  "ineligible",
12
18
  "referral",
13
19
  "eligible"
14
20
  ]);
15
- /** Maps each {@link QualificationEffect} to its eligibility impact; `condition` remains eligible. */
21
+ /**
22
+ * Maps each {@link QualificationEffect} to its eligibility impact — `restriction` to
23
+ * `ineligible`, `referral` to `referral`, and `condition` to `eligible`.
24
+ */
16
25
  var EFFECT_ELIGIBILITIES = Object.freeze({
17
26
  restriction: "ineligible",
18
27
  referral: "referral",
@@ -21,7 +30,8 @@ var EFFECT_ELIGIBILITIES = Object.freeze({
21
30
  //#endregion
22
31
  //#region src/core/errors.ts
23
32
  /**
24
- * Represents a coded programmer error thrown by the qualifier layer.
33
+ * Represents a coded programmer error thrown by the qualifier layer, carrying a
34
+ * `QualifierErrorCode` and an optional context record.
25
35
  *
26
36
  * @remarks
27
37
  * `DEFINITION` — a qualification definition failed semantic validation; when
@@ -52,13 +62,17 @@ var QualifierError = class extends Error {
52
62
  * @returns True if `value` is a {@link QualifierError} instance; false otherwise
53
63
  */
54
64
  function isQualifierError(value) {
55
- return value instanceof QualifierError;
65
+ return isInstance(value, QualifierError);
56
66
  }
57
67
  //#endregion
58
68
  //#region src/core/validators.ts
59
69
  /**
60
70
  * Determines whether a value is an {@link Eligibility} literal.
61
71
  *
72
+ * @remarks
73
+ * The guard is total and leaves nothing unchecked: a scalar union has no open or exact
74
+ * axis.
75
+ *
62
76
  * @param value - The value to test
63
77
  * @returns True if `value` is one of the eligibility literals; false otherwise
64
78
  */
@@ -66,6 +80,10 @@ var isEligibility = literalOf("eligible", "ineligible", "referral");
66
80
  /**
67
81
  * Determines whether a value is a {@link QualificationEffect} literal.
68
82
  *
83
+ * @remarks
84
+ * The guard is total and leaves nothing unchecked: a scalar union has no open or exact
85
+ * axis.
86
+ *
69
87
  * @param value - The value to test
70
88
  * @returns True if `value` is one of the effect literals; false otherwise
71
89
  */
@@ -74,8 +92,9 @@ var isQualificationEffect = literalOf("restriction", "referral", "condition");
74
92
  * Determines whether a value is an open string-keyed record of {@link Eligibility} values.
75
93
  *
76
94
  * @remarks
77
- * Every own string-named property is checked, including non-enumerable properties. Inherited and
78
- * symbol-named members are left unchecked because they are outside the record this guard certifies.
95
+ * Every own string-named value is checked through {@link isEligibility}, including
96
+ * non-enumerable values. Inherited and symbol-named members are left unchecked because they
97
+ * are outside the own string-keyed record this guard certifies.
79
98
  *
80
99
  * @param value - The value to test
81
100
  * @returns True if `value` is a non-array object whose own string-named values are
@@ -88,9 +107,11 @@ function isEligibilityRecord(value) {
88
107
  * Determines whether a value is an open result-side {@link Premise}.
89
108
  *
90
109
  * @remarks
91
- * `expected` is unchecked and may be absent or contain any value because its published type is
92
- * `unknown`. `actual` is unchecked and may be absent or contain any value for the same reason.
93
- * Unknown members are admitted.
110
+ * The optional `field`, `label`, `description`, `comparison`, and `met` members are checked
111
+ * when they are defined. `expected` is unchecked and may be absent or contain any value
112
+ * because its published type is `unknown`. `actual` is unchecked and may be absent or contain
113
+ * any value for the same reason. Unknown members are admitted, because an open result admits
114
+ * what the published contract does not name.
94
115
  *
95
116
  * @param value - The value to test
96
117
  * @returns True if every checked premise member follows its published type; false otherwise
@@ -114,8 +135,10 @@ function isPremise(value) {
114
135
  * Determines whether a value is an open result-side {@link Finding}.
115
136
  *
116
137
  * @remarks
117
- * Unknown members are admitted. Optional `scope` and `message` members may be absent or
118
- * `undefined`; a present defined value must be a string.
138
+ * Every published member is checked, including each nested {@link Premise}. Optional `scope`
139
+ * and `message` members may be absent or `undefined`; a present defined value must be a
140
+ * string. Unknown members are admitted, because a foreign result implementation may add them
141
+ * without changing the published contract.
119
142
  *
120
143
  * @param value - The value to test
121
144
  * @returns True if every published finding member is valid; false otherwise
@@ -136,8 +159,10 @@ function isFinding(value) {
136
159
  * Determines whether a value is an open result-side {@link Derivation}.
137
160
  *
138
161
  * @remarks
139
- * Unknown members are admitted. `value` follows the published `number` type, including `NaN` and
140
- * infinities.
162
+ * Every published member is checked: `value` follows the published `number` type, including
163
+ * `NaN` and the infinities, and `trace` and `errors` hold strings. Unknown members are
164
+ * admitted, because a foreign result implementation may add them without changing the
165
+ * published contract.
141
166
  *
142
167
  * @param value - The value to test
143
168
  * @returns True if every published derivation member is valid; false otherwise
@@ -156,7 +181,10 @@ function isDerivation(value) {
156
181
  *
157
182
  * @remarks
158
183
  * This guard is result-postured for values returned through a borrowed qualifier interface. It
159
- * admits unknown members and class instances while checking the complete published result closure.
184
+ * checks the complete published result closure — eligibility, scope values, findings,
185
+ * derivations, success, and the audit trail — and admits unknown members and class instances,
186
+ * because a borrowed `QualifierInterface` implementation may add them beyond the
187
+ * published contract.
160
188
  *
161
189
  * @param value - The value to test
162
190
  * @returns True if every published qualification-result member is valid; false otherwise
@@ -177,6 +205,11 @@ function isQualificationResult(value) {
177
205
  /**
178
206
  * Determines whether a value is a {@link QualificationPass} (a quantitative or logical definition).
179
207
  *
208
+ * @remarks
209
+ * The guard is exact and requires either complete reason shape, the quantitative definition or
210
+ * the logical one. Nothing is left unchecked, because the reason package owns both authored
211
+ * input shapes.
212
+ *
180
213
  * @param value - The value to test
181
214
  * @returns True if `value` is a complete reason quantitative or logical definition; false otherwise
182
215
  */
@@ -186,6 +219,10 @@ function isQualificationPass(value) {
186
219
  /**
187
220
  * Determines whether a value is an exact {@link Ruling} record.
188
221
  *
222
+ * @remarks
223
+ * Every authored ruling member and the complete key set are checked. Nothing is left
224
+ * unchecked, because this package owns the authored input record.
225
+ *
189
226
  * @param value - The value to test
190
227
  * @returns True if `value` carries every ruling member and no unknown key; false otherwise
191
228
  */
@@ -202,6 +239,11 @@ function isRuling(value) {
202
239
  /**
203
240
  * Determines whether a value is an exact {@link QualificationDefinition} record.
204
241
  *
242
+ * @remarks
243
+ * Every authored definition member, the nested pass and ruling shapes, and the complete key
244
+ * set are checked. Nothing is left unchecked, because this package owns the authored input
245
+ * record.
246
+ *
205
247
  * @param value - The value to test
206
248
  * @returns True if `value` carries every definition member and no unknown key; false otherwise
207
249
  */
@@ -226,10 +268,10 @@ function isQualificationDefinition(value) {
226
268
  *
227
269
  * @remarks
228
270
  * Each token is split on `.` into a `FieldPath` array and resolved with
229
- * `resolveField` (a plain string field is ONE key, never dot-split — the split
271
+ * `resolveField` (a plain string field is one key, never dot-split — the split
230
272
  * here is the token-to-path bridge). A finite number renders with `en-US`
231
273
  * thousand grouping (`5010` → `5,010`); any other resolved value String-coerces.
232
- * An UNRESOLVED path renders as the empty string.
274
+ * An unresolved path renders as the empty string.
233
275
  *
234
276
  * @param template - The message template carrying `{{dotted.path}}` tokens
235
277
  * @param subject - The record tokens resolve against
@@ -286,7 +328,7 @@ function renderComparison(comparison) {
286
328
  * A reason `Bounds`-shaped record (the `between` / `outside` expected value)
287
329
  * renders its present sides joined with the word `and`; an absent side is
288
330
  * omitted. A finite number and everything else fall back to plain `String()` —
289
- * a finite number is NOT grouped here (grouping is {@link interpolateMessage}'s
331
+ * a finite number is not grouped here (grouping is {@link interpolateMessage}'s
290
332
  * message-token-only behavior).
291
333
  *
292
334
  * @param value - The value to render
@@ -302,7 +344,7 @@ function renderComparison(comparison) {
302
344
  * ```
303
345
  */
304
346
  function renderValue(value) {
305
- if (Array.isArray(value)) return value.map((entry) => String(entry)).join(", ");
347
+ if (isArray(value)) return value.map((entry) => String(entry)).join(", ");
306
348
  if (isBounds(value)) {
307
349
  const sides = [];
308
350
  if (value.minimum !== void 0) sides.push(String(value.minimum));
@@ -315,7 +357,7 @@ function renderValue(value) {
315
357
  * Renders one {@link Premise} into a display-neutral sentence.
316
358
  *
317
359
  * @remarks
318
- * The checked form renders only when `field` and `comparison` are BOTH
360
+ * The checked form renders only when `field` and `comparison` are both
319
361
  * present, and `description` then goes unused; a premise missing either half
320
362
  * of the checked pair renders as described instead.
321
363
  *
@@ -380,7 +422,7 @@ function checkToPremise(check, result, labels) {
380
422
  * A reason rule result carries only booleans, so this is the qualifier's own
381
423
  * premise-audit projection: each authored premise expression is flattened to its
382
424
  * atom leaves through `extractAtoms`, and each leaf's `Check` is re-evaluated through
383
- * the injected `evaluator`. A membership check (`any` / `none`) over an EMPTY
425
+ * the injected `evaluator`. A membership check (`any` / `none`) over an empty
384
426
  * array value is content-free and is skipped.
385
427
  *
386
428
  * @param rule - The authored rule
@@ -407,7 +449,7 @@ function ruleToPremises(rule, working, evaluator, labels) {
407
449
  const output = [];
408
450
  for (const premise of rule.premises) for (const atom of extractAtoms(premise)) {
409
451
  const { check } = atom;
410
- if ((check.operator === "any" || check.operator === "none") && Array.isArray(check.value) && check.value.length === 0) continue;
452
+ if ((check.operator === "any" || check.operator === "none") && isArray(check.value) && check.value.length === 0) continue;
411
453
  output.push(checkToPremise(check, evaluator.evaluate(check, working), labels));
412
454
  }
413
455
  return output;
@@ -549,7 +591,7 @@ function mergeQualificationContext(context, id, projection) {
549
591
  *
550
592
  * @remarks
551
593
  * `applied` reflects whether the ruling's rule fired. Premises are re-evaluated
552
- * against the SAME subject snapshot the pass evaluated, so a conclusion the pass
594
+ * against the same subject snapshot the pass evaluated, so a conclusion the pass
553
595
  * derived can never satisfy its own reported premise. The message interpolates
554
596
  * against that snapshot.
555
597
  *
@@ -776,7 +818,7 @@ function mapEngineError(error, pass) {
776
818
  cause: error
777
819
  });
778
820
  }
779
- return new QualifierError("ENGINE", `Pass '${pass}' engine failure: ${error instanceof Error ? error.message : String(error)}`, {
821
+ return new QualifierError("ENGINE", `Pass '${pass}' engine failure: ${isError(error) ? error.message : String(error)}`, {
780
822
  pass,
781
823
  cause: error
782
824
  });
@@ -862,8 +904,9 @@ function describeUnreadDerivations(definition) {
862
904
  * Runs ordered passes over one reason engine and returns eligibility.
863
905
  *
864
906
  * @remarks
865
- * The engine is OWNED when self-created (destroyed on `destroy()`) and borrowed
866
- * when injected (never destroyed). `qualify` builds its working subject through
907
+ * The engine is owned when self-created (destroyed on `destroy()`) and borrowed
908
+ * when injected (never destroyed). Semantic validation runs before the first pass
909
+ * whenever the `validate` option is on. `qualify` builds its working subject through
867
910
  * copy-on-write overlays only — the caller's `subject` is never mutated, and the
868
911
  * qualifier's internal projection is discarded after each call.
869
912
  */
@@ -1091,10 +1134,10 @@ var Qualifier = class {
1091
1134
  //#endregion
1092
1135
  //#region src/core/factories.ts
1093
1136
  /**
1094
- * Creates one qualifier over a reason engine.
1137
+ * Creates one {@link QualifierInterface} over a reason engine.
1095
1138
  *
1096
1139
  * @remarks
1097
- * A standalone qualifier creates and OWNS one shared quantitative-plus-logical
1140
+ * A standalone qualifier creates and owns one shared quantitative-plus-logical
1098
1141
  * reason engine, destroying it on `destroy()`. An injected `options.engine`
1099
1142
  * remains caller-owned and is never destroyed. `validate` runs semantic
1100
1143
  * validation before every qualification. Default: `true`. `labels` overrides the
@@ -1104,12 +1147,37 @@ var Qualifier = class {
1104
1147
  * @param options - Optional injected engine, validation, labels, and emitter hooks
1105
1148
  * @returns A {@link QualifierInterface}
1106
1149
  *
1107
- * @example
1150
+ * @example Create a qualifier
1108
1151
  * ```ts
1109
- * import { createQualifier } from '@orkestrel/qualifier'
1152
+ * import { createQualificationDefinition, createQualifier, createRuling } from '@orkestrel/qualifier'
1153
+ * import { createAtom, createLogicalDefinition, createRule } from '@orkestrel/reason'
1154
+ *
1155
+ * const gates = createLogicalDefinition('gates', 'Eligibility gates', [
1156
+ * createRule(
1157
+ * 'licensed',
1158
+ * [createAtom('licensed', 'equals', false)],
1159
+ * createAtom('blocked', 'equals', true),
1160
+ * ),
1161
+ * ])
1162
+ *
1163
+ * const bare = createRuling('license', 'gates', 'licensed', 'restriction')
1164
+ * const messaged = createRuling('license', 'gates', 'licensed', 'restriction', {
1165
+ * message: 'A license is required',
1166
+ * })
1167
+ *
1168
+ * 'message' in bare // false — an absent optional key is omitted, never written as undefined
1169
+ * messaged.message // 'A license is required'
1170
+ *
1171
+ * const passes = [gates]
1172
+ * const definition = createQualificationDefinition('standard', 'Standard eligibility', passes, {
1173
+ * rulings: [messaged],
1174
+ * })
1175
+ *
1176
+ * 'description' in definition // false
1177
+ * definition.passes === passes // false — the factory copies what it is handed
1110
1178
  *
1111
1179
  * const qualifier = createQualifier()
1112
- * qualifier.qualify({ id: 'risk-1' }, definition)
1180
+ * qualifier.qualify({ id: 'risk-1', licensed: false }, definition)
1113
1181
  * qualifier.destroy()
1114
1182
  * ```
1115
1183
  */
@@ -1117,10 +1185,9 @@ function createQualifier(options) {
1117
1185
  return new Qualifier(options);
1118
1186
  }
1119
1187
  /**
1120
- * Creates a {@link QualificationDefinition}.
1188
+ * Creates a fresh {@link QualificationDefinition}, omitting every absent optional key.
1121
1189
  *
1122
1190
  * @remarks
1123
- * Returns a fresh top-level definition, omitting absent optional keys;
1124
1191
  * `passes` and `rulings` arrays are copied, and record `metadata` is
1125
1192
  * shallow-copied so nested values are not deep-cloned.
1126
1193
  *
@@ -1148,7 +1215,7 @@ function createQualificationDefinition(id, name, passes, input) {
1148
1215
  };
1149
1216
  }
1150
1217
  /**
1151
- * Creates a {@link Ruling} — one authored consequence for one rule in one pass.
1218
+ * Creates a fresh {@link Ruling} from the rule it reacts to and the effect it applies.
1152
1219
  *
1153
1220
  * @param id - The ruling id
1154
1221
  * @param pass - The logical pass id the rule lives in