@orkestrel/qualifier 0.0.13 → 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.
package/README.md CHANGED
@@ -1,13 +1,18 @@
1
1
  # @orkestrel/qualifier
2
2
 
3
- A typed **eligibility engine** over [`@orkestrel/reason`](https://github.com/orkestrel/reason):
4
- authored **passes** — quantitative derivations and logical rule gates — run in order
5
- against a **subject** (a plain data record) to produce **findings** (evidence-rich
6
- ruling outcomes), **derivations** (quantitative audit trails), and **eligibility**
7
- (global plus per-scope). The caller supplies the definition; `Qualifier` only
8
- evaluates what it is given. Qualification never mutates its inputs — every result is
9
- a fresh object. Environment-agnostic — no I/O, no browser or server assumptions.
10
- Part of the `@orkestrel` line.
3
+ > A synchronous, deterministic eligibility engine that runs a pure,
4
+ > JSON-serializable `QualificationDefinition`'s ordered `passes` against one
5
+ > subject through one `@orkestrel/reason` engine and returns a fresh
6
+ > `QualificationResult` carrying global and scoped eligibility, evidence-rich
7
+ > `findings`, and quantitative `derivations`.
8
+
9
+ Author the passes — quantitative derivations and logical rule gates — hand a subject
10
+ (a plain data record) to `qualify`, and read what comes back. The caller supplies the
11
+ definition; `Qualifier` only evaluates what it is given. Inject a
12
+ [`@orkestrel/reason`](https://github.com/orkestrel/reason) `ReasonInterface` where
13
+ qualification shares an engine with the rest of your reasoning, and call `destroy()`
14
+ when the qualifier's work is done. Environment-agnostic — no I/O, no browser or
15
+ server assumptions. Part of the `@orkestrel` line.
11
16
 
12
17
  ## Install
13
18
 
@@ -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
- /** Holds the 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
- /** Names 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
- /** Lists the eligibility severities in 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
- /** Maps each {@link QualificationEffect} to its eligibility impact; `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,7 +31,8 @@ var EFFECT_ELIGIBILITIES = Object.freeze({
22
31
  //#endregion
23
32
  //#region src/core/errors.ts
24
33
  /**
25
- * Represents 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
38
  * `DEFINITION` — a qualification definition failed semantic validation; when
@@ -60,6 +70,10 @@ function isQualifierError(value) {
60
70
  /**
61
71
  * Determines whether a value is an {@link Eligibility} literal.
62
72
  *
73
+ * @remarks
74
+ * The guard is total and leaves nothing unchecked: a scalar union has no open or exact
75
+ * axis.
76
+ *
63
77
  * @param value - The value to test
64
78
  * @returns True if `value` is one of the eligibility literals; false otherwise
65
79
  */
@@ -67,6 +81,10 @@ var isEligibility = (0, _orkestrel_contract.literalOf)("eligible", "ineligible",
67
81
  /**
68
82
  * Determines whether a value is a {@link QualificationEffect} literal.
69
83
  *
84
+ * @remarks
85
+ * The guard is total and leaves nothing unchecked: a scalar union has no open or exact
86
+ * axis.
87
+ *
70
88
  * @param value - The value to test
71
89
  * @returns True if `value` is one of the effect literals; false otherwise
72
90
  */
@@ -75,8 +93,9 @@ var isQualificationEffect = (0, _orkestrel_contract.literalOf)("restriction", "r
75
93
  * Determines whether a value is an open string-keyed record of {@link Eligibility} values.
76
94
  *
77
95
  * @remarks
78
- * Every own string-named property is checked, including non-enumerable properties. Inherited and
79
- * 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.
80
99
  *
81
100
  * @param value - The value to test
82
101
  * @returns True if `value` is a non-array object whose own string-named values are
@@ -89,9 +108,11 @@ function isEligibilityRecord(value) {
89
108
  * Determines whether a value is an open result-side {@link Premise}.
90
109
  *
91
110
  * @remarks
92
- * `expected` is unchecked and may be absent or contain any value because its published type is
93
- * `unknown`. `actual` is unchecked and may be absent or contain any value for the same reason.
94
- * 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.
95
116
  *
96
117
  * @param value - The value to test
97
118
  * @returns True if every checked premise member follows its published type; false otherwise
@@ -115,8 +136,10 @@ function isPremise(value) {
115
136
  * Determines whether a value is an open result-side {@link Finding}.
116
137
  *
117
138
  * @remarks
118
- * Unknown members are admitted. Optional `scope` and `message` members may be absent or
119
- * `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.
120
143
  *
121
144
  * @param value - The value to test
122
145
  * @returns True if every published finding member is valid; false otherwise
@@ -137,8 +160,10 @@ function isFinding(value) {
137
160
  * Determines whether a value is an open result-side {@link Derivation}.
138
161
  *
139
162
  * @remarks
140
- * Unknown members are admitted. `value` follows the published `number` type, including `NaN` and
141
- * 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.
142
167
  *
143
168
  * @param value - The value to test
144
169
  * @returns True if every published derivation member is valid; false otherwise
@@ -157,7 +182,10 @@ function isDerivation(value) {
157
182
  *
158
183
  * @remarks
159
184
  * This guard is result-postured for values returned through a borrowed qualifier interface. It
160
- * 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.
161
189
  *
162
190
  * @param value - The value to test
163
191
  * @returns True if every published qualification-result member is valid; false otherwise
@@ -178,6 +206,11 @@ function isQualificationResult(value) {
178
206
  /**
179
207
  * Determines whether a value is a {@link QualificationPass} (a quantitative or logical definition).
180
208
  *
209
+ * @remarks
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.
213
+ *
181
214
  * @param value - The value to test
182
215
  * @returns True if `value` is a complete reason quantitative or logical definition; false otherwise
183
216
  */
@@ -187,6 +220,10 @@ function isQualificationPass(value) {
187
220
  /**
188
221
  * Determines whether a value is an exact {@link Ruling} record.
189
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
+ *
190
227
  * @param value - The value to test
191
228
  * @returns True if `value` carries every ruling member and no unknown key; false otherwise
192
229
  */
@@ -203,6 +240,11 @@ function isRuling(value) {
203
240
  /**
204
241
  * Determines whether a value is an exact {@link QualificationDefinition} record.
205
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
+ *
206
248
  * @param value - The value to test
207
249
  * @returns True if `value` carries every definition member and no unknown key; false otherwise
208
250
  */
@@ -227,10 +269,10 @@ function isQualificationDefinition(value) {
227
269
  *
228
270
  * @remarks
229
271
  * Each token is split on `.` into a `FieldPath` array and resolved with
230
- * `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
231
273
  * here is the token-to-path bridge). A finite number renders with `en-US`
232
274
  * thousand grouping (`5010` → `5,010`); any other resolved value String-coerces.
233
- * An UNRESOLVED path renders as the empty string.
275
+ * An unresolved path renders as the empty string.
234
276
  *
235
277
  * @param template - The message template carrying `{{dotted.path}}` tokens
236
278
  * @param subject - The record tokens resolve against
@@ -287,7 +329,7 @@ function renderComparison(comparison) {
287
329
  * A reason `Bounds`-shaped record (the `between` / `outside` expected value)
288
330
  * renders its present sides joined with the word `and`; an absent side is
289
331
  * omitted. A finite number and everything else fall back to plain `String()` —
290
- * 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
291
333
  * message-token-only behavior).
292
334
  *
293
335
  * @param value - The value to render
@@ -316,7 +358,7 @@ function renderValue(value) {
316
358
  * Renders one {@link Premise} into a display-neutral sentence.
317
359
  *
318
360
  * @remarks
319
- * The checked form renders only when `field` and `comparison` are BOTH
361
+ * The checked form renders only when `field` and `comparison` are both
320
362
  * present, and `description` then goes unused; a premise missing either half
321
363
  * of the checked pair renders as described instead.
322
364
  *
@@ -381,7 +423,7 @@ function checkToPremise(check, result, labels) {
381
423
  * A reason rule result carries only booleans, so this is the qualifier's own
382
424
  * premise-audit projection: each authored premise expression is flattened to its
383
425
  * atom leaves through `extractAtoms`, and each leaf's `Check` is re-evaluated through
384
- * the injected `evaluator`. A membership check (`any` / `none`) over an EMPTY
426
+ * the injected `evaluator`. A membership check (`any` / `none`) over an empty
385
427
  * array value is content-free and is skipped.
386
428
  *
387
429
  * @param rule - The authored rule
@@ -550,7 +592,7 @@ function mergeQualificationContext(context, id, projection) {
550
592
  *
551
593
  * @remarks
552
594
  * `applied` reflects whether the ruling's rule fired. Premises are re-evaluated
553
- * 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
554
596
  * derived can never satisfy its own reported premise. The message interpolates
555
597
  * against that snapshot.
556
598
  *
@@ -863,8 +905,9 @@ function describeUnreadDerivations(definition) {
863
905
  * Runs ordered passes over one reason engine and returns eligibility.
864
906
  *
865
907
  * @remarks
866
- * The engine is OWNED when self-created (destroyed on `destroy()`) and borrowed
867
- * 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
868
911
  * copy-on-write overlays only — the caller's `subject` is never mutated, and the
869
912
  * qualifier's internal projection is discarded after each call.
870
913
  */
@@ -1092,10 +1135,10 @@ var Qualifier = class {
1092
1135
  //#endregion
1093
1136
  //#region src/core/factories.ts
1094
1137
  /**
1095
- * Creates one qualifier over a reason engine.
1138
+ * Creates one {@link QualifierInterface} over a reason engine.
1096
1139
  *
1097
1140
  * @remarks
1098
- * A standalone qualifier creates and OWNS one shared quantitative-plus-logical
1141
+ * A standalone qualifier creates and owns one shared quantitative-plus-logical
1099
1142
  * reason engine, destroying it on `destroy()`. An injected `options.engine`
1100
1143
  * remains caller-owned and is never destroyed. `validate` runs semantic
1101
1144
  * validation before every qualification. Default: `true`. `labels` overrides the
@@ -1105,12 +1148,37 @@ var Qualifier = class {
1105
1148
  * @param options - Optional injected engine, validation, labels, and emitter hooks
1106
1149
  * @returns A {@link QualifierInterface}
1107
1150
  *
1108
- * @example
1151
+ * @example Create a qualifier
1109
1152
  * ```ts
1110
- * 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
1111
1179
  *
1112
1180
  * const qualifier = createQualifier()
1113
- * qualifier.qualify({ id: 'risk-1' }, definition)
1181
+ * qualifier.qualify({ id: 'risk-1', licensed: false }, definition)
1114
1182
  * qualifier.destroy()
1115
1183
  * ```
1116
1184
  */
@@ -1118,10 +1186,9 @@ function createQualifier(options) {
1118
1186
  return new Qualifier(options);
1119
1187
  }
1120
1188
  /**
1121
- * Creates a {@link QualificationDefinition}.
1189
+ * Creates a fresh {@link QualificationDefinition}, omitting every absent optional key.
1122
1190
  *
1123
1191
  * @remarks
1124
- * Returns a fresh top-level definition, omitting absent optional keys;
1125
1192
  * `passes` and `rulings` arrays are copied, and record `metadata` is
1126
1193
  * shallow-copied so nested values are not deep-cloned.
1127
1194
  *
@@ -1149,7 +1216,7 @@ function createQualificationDefinition(id, name, passes, input) {
1149
1216
  };
1150
1217
  }
1151
1218
  /**
1152
- * Creates a {@link Ruling} — one authored consequence for one rule in one pass.
1219
+ * Creates a fresh {@link Ruling} from the rule it reacts to and the effect it applies.
1153
1220
  *
1154
1221
  * @param id - The ruling id
1155
1222
  * @param pass - The logical pass id the rule lives in