@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.
- package/README.md +13 -8
- package/dist/src/core/index.cjs +102 -35
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +123 -53
- package/dist/src/core/index.d.ts +123 -53
- package/dist/src/core/index.js +103 -36
- package/dist/src/core/index.js.map +1 -1
- package/package.json +15 -16
package/dist/src/core/index.js
CHANGED
|
@@ -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
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Holds the default definition validation policy for `createQualifier` and
|
|
7
|
+
* `Qualifier.qualify`, `true`.
|
|
8
|
+
*/
|
|
6
9
|
var DEFAULT_QUALIFIER_VALIDATE = true;
|
|
7
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
78
|
-
* symbol-named members are left unchecked because they
|
|
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
|
-
* `
|
|
92
|
-
*
|
|
93
|
-
*
|
|
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
|
-
*
|
|
118
|
-
* `undefined`; a present defined value must be a
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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") &&
|
|
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
|
|
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
|
|
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
|
|
866
|
-
* when injected (never destroyed).
|
|
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
|
|
1137
|
+
* Creates one {@link QualifierInterface} over a reason engine.
|
|
1095
1138
|
*
|
|
1096
1139
|
* @remarks
|
|
1097
|
-
* A standalone qualifier creates and
|
|
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}
|
|
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
|