@orkestrel/qualifier 0.0.11 → 0.0.13
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 +14 -10
- package/dist/src/core/index.cjs +315 -198
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +636 -420
- package/dist/src/core/index.d.ts +636 -420
- package/dist/src/core/index.js +307 -189
- package/dist/src/core/index.js.map +1 -1
- package/package.json +19 -16
package/dist/src/core/index.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { arrayOf, isBoolean, isFiniteNumber, isNumber, isRecord, isString, literalOf, objectOf, recordOf, resolveField, whereOf } from "@orkestrel/contract";
|
|
2
|
-
import { createEvaluator, createLogicalReasoner, createQuantitativeReasoner, createReason, extractAtoms, extractConclusions, findDuplicates, formatField, isBounds, isComparison, isFieldPath, isLogicalDefinition, isQuantitativeDefinition, isReasonError
|
|
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
|
+
/** Holds the default definition validation policy for `createQualifier` / `Qualifier.qualify`. */
|
|
6
6
|
var DEFAULT_QUALIFIER_VALIDATE = true;
|
|
7
|
-
/**
|
|
7
|
+
/** Names the reserved internal projection namespace a pass's working projection is written under. */
|
|
8
8
|
var QUALIFICATION_KEY = "qualification";
|
|
9
|
-
/**
|
|
9
|
+
/** Lists the eligibility severities in order — most to least severe. */
|
|
10
10
|
var ELIGIBILITY_PRECEDENCE = Object.freeze([
|
|
11
11
|
"ineligible",
|
|
12
12
|
"referral",
|
|
13
13
|
"eligible"
|
|
14
14
|
]);
|
|
15
|
-
/**
|
|
15
|
+
/** Maps each {@link QualificationEffect} to its eligibility impact; `condition` remains eligible. */
|
|
16
16
|
var EFFECT_ELIGIBILITIES = Object.freeze({
|
|
17
17
|
restriction: "ineligible",
|
|
18
18
|
referral: "referral",
|
|
@@ -21,14 +21,19 @@ var EFFECT_ELIGIBILITIES = Object.freeze({
|
|
|
21
21
|
//#endregion
|
|
22
22
|
//#region src/core/errors.ts
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* Represents a coded programmer error thrown by the qualifier layer.
|
|
25
25
|
*
|
|
26
26
|
* @remarks
|
|
27
|
-
* `DEFINITION` — a qualification definition failed semantic validation
|
|
28
|
-
* `
|
|
29
|
-
*
|
|
30
|
-
* `
|
|
31
|
-
*
|
|
27
|
+
* `DEFINITION` — a qualification definition failed semantic validation; when
|
|
28
|
+
* `qualify`'s own validation raises it, `context.definition` names the
|
|
29
|
+
* definition, and when an engine throw maps to it, `context.pass` names the pass
|
|
30
|
+
* and `context.cause` preserves the original throw. `MISMATCH` — a subject is
|
|
31
|
+
* not a record or already carries the reserved `qualification` key.
|
|
32
|
+
* `DESTROYED` — use of a destroyed qualifier; an engine-mapped one carries
|
|
33
|
+
* `context.pass` and `context.cause`. `ENGINE` — the underlying reason engine
|
|
34
|
+
* threw while running a pass; `context.pass` names the pass and `context.cause`
|
|
35
|
+
* preserves the original throw. `context` is absent for a throw that carries no
|
|
36
|
+
* payload.
|
|
32
37
|
*/
|
|
33
38
|
var QualifierError = class extends Error {
|
|
34
39
|
code;
|
|
@@ -40,31 +45,47 @@ var QualifierError = class extends Error {
|
|
|
40
45
|
this.context = context;
|
|
41
46
|
}
|
|
42
47
|
};
|
|
43
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Narrows a caught value to a {@link QualifierError}.
|
|
50
|
+
*
|
|
51
|
+
* @param value - The caught value to test
|
|
52
|
+
* @returns True if `value` is a {@link QualifierError} instance; false otherwise
|
|
53
|
+
*/
|
|
44
54
|
function isQualifierError(value) {
|
|
45
55
|
return value instanceof QualifierError;
|
|
46
56
|
}
|
|
47
57
|
//#endregion
|
|
48
58
|
//#region src/core/validators.ts
|
|
49
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Determines whether a value is an {@link Eligibility} literal.
|
|
61
|
+
*
|
|
62
|
+
* @param value - The value to test
|
|
63
|
+
* @returns True if `value` is one of the eligibility literals; false otherwise
|
|
64
|
+
*/
|
|
50
65
|
var isEligibility = literalOf("eligible", "ineligible", "referral");
|
|
51
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Determines whether a value is a {@link QualificationEffect} literal.
|
|
68
|
+
*
|
|
69
|
+
* @param value - The value to test
|
|
70
|
+
* @returns True if `value` is one of the effect literals; false otherwise
|
|
71
|
+
*/
|
|
52
72
|
var isQualificationEffect = literalOf("restriction", "referral", "condition");
|
|
53
73
|
/**
|
|
54
|
-
*
|
|
74
|
+
* Determines whether a value is an open string-keyed record of {@link Eligibility} values.
|
|
55
75
|
*
|
|
56
76
|
* @remarks
|
|
57
77
|
* Every own string-named property is checked, including non-enumerable properties. Inherited and
|
|
58
78
|
* symbol-named members are left unchecked because they are outside the record this guard certifies.
|
|
59
79
|
*
|
|
60
80
|
* @param value - The value to test
|
|
61
|
-
* @returns
|
|
81
|
+
* @returns True if `value` is a non-array object whose own string-named values are
|
|
82
|
+
* eligibilities; false otherwise
|
|
62
83
|
*/
|
|
63
84
|
function isEligibilityRecord(value) {
|
|
64
85
|
return whereOf(objectOf({}), (record) => Object.getOwnPropertyNames(record).every((key) => isEligibility(Reflect.get(record, key))))(value);
|
|
65
86
|
}
|
|
66
87
|
/**
|
|
67
|
-
*
|
|
88
|
+
* Determines whether a value is an open result-side {@link Premise}.
|
|
68
89
|
*
|
|
69
90
|
* @remarks
|
|
70
91
|
* `expected` is unchecked and may be absent or contain any value because its published type is
|
|
@@ -72,7 +93,7 @@ function isEligibilityRecord(value) {
|
|
|
72
93
|
* Unknown members are admitted.
|
|
73
94
|
*
|
|
74
95
|
* @param value - The value to test
|
|
75
|
-
* @returns
|
|
96
|
+
* @returns True if every checked premise member follows its published type; false otherwise
|
|
76
97
|
*/
|
|
77
98
|
function isPremise(value) {
|
|
78
99
|
return objectOf({
|
|
@@ -90,14 +111,14 @@ function isPremise(value) {
|
|
|
90
111
|
])(value);
|
|
91
112
|
}
|
|
92
113
|
/**
|
|
93
|
-
*
|
|
114
|
+
* Determines whether a value is an open result-side {@link Finding}.
|
|
94
115
|
*
|
|
95
116
|
* @remarks
|
|
96
117
|
* Unknown members are admitted. Optional `scope` and `message` members may be absent or
|
|
97
118
|
* `undefined`; a present defined value must be a string.
|
|
98
119
|
*
|
|
99
120
|
* @param value - The value to test
|
|
100
|
-
* @returns
|
|
121
|
+
* @returns True if every published finding member is valid; false otherwise
|
|
101
122
|
*/
|
|
102
123
|
function isFinding(value) {
|
|
103
124
|
return objectOf({
|
|
@@ -112,14 +133,14 @@ function isFinding(value) {
|
|
|
112
133
|
}, ["scope", "message"])(value);
|
|
113
134
|
}
|
|
114
135
|
/**
|
|
115
|
-
*
|
|
136
|
+
* Determines whether a value is an open result-side {@link Derivation}.
|
|
116
137
|
*
|
|
117
138
|
* @remarks
|
|
118
139
|
* Unknown members are admitted. `value` follows the published `number` type, including `NaN` and
|
|
119
140
|
* infinities.
|
|
120
141
|
*
|
|
121
142
|
* @param value - The value to test
|
|
122
|
-
* @returns
|
|
143
|
+
* @returns True if every published derivation member is valid; false otherwise
|
|
123
144
|
*/
|
|
124
145
|
function isDerivation(value) {
|
|
125
146
|
return objectOf({
|
|
@@ -131,14 +152,14 @@ function isDerivation(value) {
|
|
|
131
152
|
})(value);
|
|
132
153
|
}
|
|
133
154
|
/**
|
|
134
|
-
*
|
|
155
|
+
* Determines whether a value is an open {@link QualificationResult} returned by a qualifier.
|
|
135
156
|
*
|
|
136
157
|
* @remarks
|
|
137
158
|
* This guard is result-postured for values returned through a borrowed qualifier interface. It
|
|
138
159
|
* admits unknown members and class instances while checking the complete published result closure.
|
|
139
160
|
*
|
|
140
161
|
* @param value - The value to test
|
|
141
|
-
* @returns
|
|
162
|
+
* @returns True if every published qualification-result member is valid; false otherwise
|
|
142
163
|
*/
|
|
143
164
|
function isQualificationResult(value) {
|
|
144
165
|
return objectOf({
|
|
@@ -154,23 +175,20 @@ function isQualificationResult(value) {
|
|
|
154
175
|
})(value);
|
|
155
176
|
}
|
|
156
177
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* @remarks
|
|
160
|
-
* `QualificationValidationResult` is an alias of reasons' validation-result type, so this IS
|
|
161
|
-
* reasons' published open result guard by delegation — the alias tracks reasons' shape
|
|
162
|
-
* automatically, and a hand-written member list here would silently stop checking a member a
|
|
163
|
-
* later reason release adds.
|
|
178
|
+
* Determines whether a value is a {@link QualificationPass} (a quantitative or logical definition).
|
|
164
179
|
*
|
|
165
180
|
* @param value - The value to test
|
|
166
|
-
* @returns `
|
|
181
|
+
* @returns True if `value` is a complete reason quantitative or logical definition; false otherwise
|
|
167
182
|
*/
|
|
168
|
-
var isQualificationValidationResult = isReasonValidationResult;
|
|
169
|
-
/** Determine whether a value is a {@link QualificationPass} (a quantitative or logical definition). */
|
|
170
183
|
function isQualificationPass(value) {
|
|
171
184
|
return isQuantitativeDefinition(value) || isLogicalDefinition(value);
|
|
172
185
|
}
|
|
173
|
-
/**
|
|
186
|
+
/**
|
|
187
|
+
* Determines whether a value is an exact {@link Ruling} record.
|
|
188
|
+
*
|
|
189
|
+
* @param value - The value to test
|
|
190
|
+
* @returns True if `value` carries every ruling member and no unknown key; false otherwise
|
|
191
|
+
*/
|
|
174
192
|
function isRuling(value) {
|
|
175
193
|
return recordOf({
|
|
176
194
|
id: isString,
|
|
@@ -181,7 +199,12 @@ function isRuling(value) {
|
|
|
181
199
|
message: isString
|
|
182
200
|
}, ["scope", "message"])(value);
|
|
183
201
|
}
|
|
184
|
-
/**
|
|
202
|
+
/**
|
|
203
|
+
* Determines whether a value is an exact {@link QualificationDefinition} record.
|
|
204
|
+
*
|
|
205
|
+
* @param value - The value to test
|
|
206
|
+
* @returns True if `value` carries every definition member and no unknown key; false otherwise
|
|
207
|
+
*/
|
|
185
208
|
function isQualificationDefinition(value) {
|
|
186
209
|
return recordOf({
|
|
187
210
|
id: isString,
|
|
@@ -199,7 +222,7 @@ function isQualificationDefinition(value) {
|
|
|
199
222
|
//#endregion
|
|
200
223
|
//#region src/core/helpers.ts
|
|
201
224
|
/**
|
|
202
|
-
*
|
|
225
|
+
* Interpolates `{{dotted.path}}` tokens in a message template against a subject.
|
|
203
226
|
*
|
|
204
227
|
* @remarks
|
|
205
228
|
* Each token is split on `.` into a `FieldPath` array and resolved with
|
|
@@ -229,19 +252,19 @@ function interpolateMessage(template, subject) {
|
|
|
229
252
|
});
|
|
230
253
|
}
|
|
231
254
|
/**
|
|
232
|
-
*
|
|
255
|
+
* Renders a {@link Premise} comparison as a display-neutral verb phrase.
|
|
233
256
|
*
|
|
234
257
|
* @param comparison - The comparison to describe
|
|
235
258
|
* @returns A display-neutral phrase
|
|
236
259
|
*
|
|
237
260
|
* @example
|
|
238
261
|
* ```ts
|
|
239
|
-
* import {
|
|
262
|
+
* import { renderComparison } from '@orkestrel/qualifier'
|
|
240
263
|
*
|
|
241
|
-
*
|
|
264
|
+
* renderComparison('above') // 'is more than'
|
|
242
265
|
* ```
|
|
243
266
|
*/
|
|
244
|
-
function
|
|
267
|
+
function renderComparison(comparison) {
|
|
245
268
|
switch (comparison) {
|
|
246
269
|
case "equals": return "is";
|
|
247
270
|
case "not": return "is not";
|
|
@@ -256,7 +279,7 @@ function describeComparison(comparison) {
|
|
|
256
279
|
}
|
|
257
280
|
}
|
|
258
281
|
/**
|
|
259
|
-
*
|
|
282
|
+
* Renders a structured or scalar expected value display-neutrally.
|
|
260
283
|
*
|
|
261
284
|
* @remarks
|
|
262
285
|
* An array renders as its elements, each `String`-coerced, joined with `', '`.
|
|
@@ -271,14 +294,14 @@ function describeComparison(comparison) {
|
|
|
271
294
|
*
|
|
272
295
|
* @example
|
|
273
296
|
* ```ts
|
|
274
|
-
* import {
|
|
297
|
+
* import { renderValue } from '@orkestrel/qualifier'
|
|
275
298
|
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
299
|
+
* renderValue([18, 25, 40]) // '18, 25, 40'
|
|
300
|
+
* renderValue({ minimum: 18, maximum: 65 }) // '18 and 65'
|
|
301
|
+
* renderValue(42) // '42'
|
|
279
302
|
* ```
|
|
280
303
|
*/
|
|
281
|
-
function
|
|
304
|
+
function renderValue(value) {
|
|
282
305
|
if (Array.isArray(value)) return value.map((entry) => String(entry)).join(", ");
|
|
283
306
|
if (isBounds(value)) {
|
|
284
307
|
const sides = [];
|
|
@@ -289,7 +312,7 @@ function describeValue(value) {
|
|
|
289
312
|
return String(value);
|
|
290
313
|
}
|
|
291
314
|
/**
|
|
292
|
-
*
|
|
315
|
+
* Renders one {@link Premise} into a display-neutral sentence.
|
|
293
316
|
*
|
|
294
317
|
* @remarks
|
|
295
318
|
* The checked form renders only when `field` and `comparison` are BOTH
|
|
@@ -302,25 +325,25 @@ function describeValue(value) {
|
|
|
302
325
|
*
|
|
303
326
|
* @example
|
|
304
327
|
* ```ts
|
|
305
|
-
* import {
|
|
328
|
+
* import { renderPremise } from '@orkestrel/qualifier'
|
|
306
329
|
*
|
|
307
|
-
*
|
|
330
|
+
* renderPremise({ field: 'age', comparison: 'above', expected: 18, actual: 25, met: true })
|
|
308
331
|
* // 'age is more than 18 → met'
|
|
309
332
|
*
|
|
310
|
-
*
|
|
333
|
+
* renderPremise({ description: 'Applicant is enrolled', met: true })
|
|
311
334
|
* // 'Applicant is enrolled → met'
|
|
312
335
|
* ```
|
|
313
336
|
*/
|
|
314
|
-
function
|
|
337
|
+
function renderPremise(entry, labels) {
|
|
315
338
|
const status = entry.met === void 0 ? "unknown" : entry.met ? "met" : "not met";
|
|
316
339
|
if (entry.field === void 0 || entry.comparison === void 0) return `${entry.description ?? "Premise"} → ${status}`;
|
|
317
340
|
const field = formatField(entry.field);
|
|
318
341
|
const label = labels?.[field] ?? entry.label ?? field;
|
|
319
|
-
const expected = entry.expected === void 0 ? "" : ` ${
|
|
320
|
-
return `${label} ${
|
|
342
|
+
const expected = entry.expected === void 0 ? "" : ` ${renderValue(entry.expected)}`;
|
|
343
|
+
return `${label} ${renderComparison(entry.comparison)}${expected} → ${status}`;
|
|
321
344
|
}
|
|
322
345
|
/**
|
|
323
|
-
*
|
|
346
|
+
* Builds a {@link Premise} from an authored {@link Check} and its evaluated
|
|
324
347
|
* `CheckResult`.
|
|
325
348
|
*
|
|
326
349
|
* @param check - The authored check
|
|
@@ -330,15 +353,15 @@ function describePremise(entry, labels) {
|
|
|
330
353
|
*
|
|
331
354
|
* @example
|
|
332
355
|
* ```ts
|
|
333
|
-
* import {
|
|
334
|
-
* import {
|
|
356
|
+
* import { checkToPremise } from '@orkestrel/qualifier'
|
|
357
|
+
* import { createCheck } from '@orkestrel/reason'
|
|
335
358
|
*
|
|
336
|
-
* const authored =
|
|
337
|
-
*
|
|
359
|
+
* const authored = createCheck('age', 'above', 18)
|
|
360
|
+
* checkToPremise(authored, { field: 'age', actual: 25, met: true })
|
|
338
361
|
* // { field: 'age', comparison: 'above', expected: 18, actual: 25, met: true }
|
|
339
362
|
* ```
|
|
340
363
|
*/
|
|
341
|
-
function
|
|
364
|
+
function checkToPremise(check, result, labels) {
|
|
342
365
|
const field = formatField(check.field);
|
|
343
366
|
return {
|
|
344
367
|
field: check.field,
|
|
@@ -350,13 +373,13 @@ function premiseCheck(check, result, labels) {
|
|
|
350
373
|
};
|
|
351
374
|
}
|
|
352
375
|
/**
|
|
353
|
-
*
|
|
376
|
+
* Builds rich premises for one fired {@link Rule} by walking its premise atoms
|
|
354
377
|
* and re-evaluating each against the working subject.
|
|
355
378
|
*
|
|
356
379
|
* @remarks
|
|
357
380
|
* A reason rule result carries only booleans, so this is the qualifier's own
|
|
358
381
|
* premise-audit projection: each authored premise expression is flattened to its
|
|
359
|
-
* atom leaves
|
|
382
|
+
* atom leaves through `extractAtoms`, and each leaf's `Check` is re-evaluated through
|
|
360
383
|
* the injected `evaluator`. A membership check (`any` / `none`) over an EMPTY
|
|
361
384
|
* array value is content-free and is skipped.
|
|
362
385
|
*
|
|
@@ -368,29 +391,29 @@ function premiseCheck(check, result, labels) {
|
|
|
368
391
|
*
|
|
369
392
|
* @example
|
|
370
393
|
* ```ts
|
|
371
|
-
* import {
|
|
372
|
-
* import {
|
|
394
|
+
* import { ruleToPremises } from '@orkestrel/qualifier'
|
|
395
|
+
* import { createAtom, createEvaluator, createLogicalDefinition, createRule } from '@orkestrel/reason'
|
|
373
396
|
*
|
|
374
|
-
* const gates =
|
|
375
|
-
*
|
|
397
|
+
* const gates = createLogicalDefinition('gates', 'Gates', [
|
|
398
|
+
* createRule('licensed', [createAtom('licensed', 'equals', false)], createAtom('blocked', 'equals', true)),
|
|
376
399
|
* ])
|
|
377
400
|
* const evaluator = createEvaluator()
|
|
378
|
-
* const premises =
|
|
401
|
+
* const premises = ruleToPremises(gates.rules[0], { licensed: false }, evaluator)
|
|
379
402
|
*
|
|
380
403
|
* premises[0]?.met // true
|
|
381
404
|
* ```
|
|
382
405
|
*/
|
|
383
|
-
function
|
|
406
|
+
function ruleToPremises(rule, working, evaluator, labels) {
|
|
384
407
|
const output = [];
|
|
385
408
|
for (const premise of rule.premises) for (const atom of extractAtoms(premise)) {
|
|
386
409
|
const { check } = atom;
|
|
387
410
|
if ((check.operator === "any" || check.operator === "none") && Array.isArray(check.value) && check.value.length === 0) continue;
|
|
388
|
-
output.push(
|
|
411
|
+
output.push(checkToPremise(check, evaluator.evaluate(check, working), labels));
|
|
389
412
|
}
|
|
390
413
|
return output;
|
|
391
414
|
}
|
|
392
415
|
/**
|
|
393
|
-
*
|
|
416
|
+
* Locates an authored {@link Rule} by id.
|
|
394
417
|
*
|
|
395
418
|
* @param definition - The logical definition to search
|
|
396
419
|
* @param id - The rule id
|
|
@@ -399,10 +422,10 @@ function logicalPremises(rule, working, evaluator, labels) {
|
|
|
399
422
|
* @example
|
|
400
423
|
* ```ts
|
|
401
424
|
* import { findRule } from '@orkestrel/qualifier'
|
|
402
|
-
* import {
|
|
425
|
+
* import { createAtom, createLogicalDefinition, createRule } from '@orkestrel/reason'
|
|
403
426
|
*
|
|
404
|
-
* const gates =
|
|
405
|
-
*
|
|
427
|
+
* const gates = createLogicalDefinition('gates', 'Gates', [
|
|
428
|
+
* createRule('licensed', [createAtom('licensed', 'equals', false)], createAtom('blocked', 'equals', true)),
|
|
406
429
|
* ])
|
|
407
430
|
*
|
|
408
431
|
* findRule(gates, 'licensed')?.id // 'licensed'
|
|
@@ -412,7 +435,7 @@ function findRule(definition, id) {
|
|
|
412
435
|
return definition.rules.find((rule) => rule.id === id);
|
|
413
436
|
}
|
|
414
437
|
/**
|
|
415
|
-
*
|
|
438
|
+
* Projects one reason result into the internal qualification namespace.
|
|
416
439
|
*
|
|
417
440
|
* @remarks
|
|
418
441
|
* A quantitative result contributes its numeric `value`. A logical result
|
|
@@ -427,10 +450,10 @@ function findRule(definition, id) {
|
|
|
427
450
|
* @example
|
|
428
451
|
* ```ts
|
|
429
452
|
* import { reasonResultToProjection } from '@orkestrel/qualifier'
|
|
430
|
-
* import {
|
|
453
|
+
* import { createFactorGroup, createQuantitativeDefinition, createStaticFactor } from '@orkestrel/reason'
|
|
431
454
|
*
|
|
432
|
-
* const cap =
|
|
433
|
-
*
|
|
455
|
+
* const cap = createQuantitativeDefinition('cap', 'TIV cap', [
|
|
456
|
+
* createFactorGroup('limit', 'sum', [createStaticFactor('base', 500_000)]),
|
|
434
457
|
* ])
|
|
435
458
|
* const result = {
|
|
436
459
|
* reasoning: 'quantitative' as const,
|
|
@@ -458,7 +481,7 @@ function reasonResultToProjection(definition, result) {
|
|
|
458
481
|
return projection;
|
|
459
482
|
}
|
|
460
483
|
/**
|
|
461
|
-
*
|
|
484
|
+
* Projects a quantitative result into a {@link Derivation} audit record.
|
|
462
485
|
*
|
|
463
486
|
* @param id - The pass id
|
|
464
487
|
* @param result - The evaluated quantitative result
|
|
@@ -483,7 +506,7 @@ function quantitativeResultToDerivation(id, result) {
|
|
|
483
506
|
};
|
|
484
507
|
}
|
|
485
508
|
/**
|
|
486
|
-
*
|
|
509
|
+
* Wraps a {@link QualificationContext} under {@link QUALIFICATION_KEY}.
|
|
487
510
|
*
|
|
488
511
|
* @param context - The accumulated projection context
|
|
489
512
|
* @returns A record carrying the context under the reserved key
|
|
@@ -499,7 +522,7 @@ function qualificationToRecord(context) {
|
|
|
499
522
|
return { [QUALIFICATION_KEY]: context };
|
|
500
523
|
}
|
|
501
524
|
/**
|
|
502
|
-
*
|
|
525
|
+
* Merges one pass projection into the context, copy-on-write.
|
|
503
526
|
*
|
|
504
527
|
* @param context - The current projection context
|
|
505
528
|
* @param id - The pass id to project under
|
|
@@ -521,7 +544,7 @@ function mergeQualificationContext(context, id, projection) {
|
|
|
521
544
|
};
|
|
522
545
|
}
|
|
523
546
|
/**
|
|
524
|
-
*
|
|
547
|
+
* Joins a ruling, its logical rule result, the pass, the pre-projection subject,
|
|
525
548
|
* and an evaluator into a {@link Finding}.
|
|
526
549
|
*
|
|
527
550
|
* @remarks
|
|
@@ -541,22 +564,22 @@ function mergeQualificationContext(context, id, projection) {
|
|
|
541
564
|
* @example
|
|
542
565
|
* ```ts
|
|
543
566
|
* import { rulingToFinding } from '@orkestrel/qualifier'
|
|
544
|
-
* import {
|
|
567
|
+
* import { createAtom, createEvaluator, createLogicalDefinition, createRule } from '@orkestrel/reason'
|
|
545
568
|
*
|
|
546
|
-
* const gates =
|
|
547
|
-
*
|
|
569
|
+
* const gates = createLogicalDefinition('gates', 'Gates', [
|
|
570
|
+
* createRule('licensed', [createAtom('licensed', 'equals', false)], createAtom('blocked', 'equals', true)),
|
|
548
571
|
* ])
|
|
549
572
|
* const ruling = { id: 'license', pass: 'gates', rule: 'licensed', effect: 'restriction' as const }
|
|
550
|
-
* const result = { reasoning: 'logical' as const, conclusion: true, rules: [{ id: 'licensed', applied: true, premises: [true]
|
|
573
|
+
* const result = { reasoning: 'logical' as const, conclusion: true, rules: [{ id: 'licensed', applied: true, premises: [true] }], count: 1, success: true, trace: [], errors: [] }
|
|
551
574
|
* const finding = rulingToFinding(ruling, gates, result, { licensed: false }, createEvaluator())
|
|
552
575
|
*
|
|
553
576
|
* finding.applied // true
|
|
554
577
|
* ```
|
|
555
578
|
*/
|
|
556
579
|
function rulingToFinding(ruling, pass, result, subject, evaluator, labels) {
|
|
557
|
-
const applied = result.rules.find((
|
|
580
|
+
const applied = result.rules.find((resolved) => resolved.id === ruling.rule)?.applied ?? false;
|
|
558
581
|
const rule = pass.reasoning === "logical" ? findRule(pass, ruling.rule) : void 0;
|
|
559
|
-
const premises = rule === void 0 ? [] :
|
|
582
|
+
const premises = rule === void 0 ? [] : ruleToPremises(rule, subject, evaluator, labels);
|
|
560
583
|
return {
|
|
561
584
|
id: ruling.id,
|
|
562
585
|
pass: ruling.pass,
|
|
@@ -569,7 +592,7 @@ function rulingToFinding(ruling, pass, result, subject, evaluator, labels) {
|
|
|
569
592
|
};
|
|
570
593
|
}
|
|
571
594
|
/**
|
|
572
|
-
*
|
|
595
|
+
* Derives global eligibility from applied, unscoped findings.
|
|
573
596
|
*
|
|
574
597
|
* @remarks
|
|
575
598
|
* Only findings that both applied and carry no `scope` shape global eligibility.
|
|
@@ -577,7 +600,8 @@ function rulingToFinding(ruling, pass, result, subject, evaluator, labels) {
|
|
|
577
600
|
* `failed`.
|
|
578
601
|
*
|
|
579
602
|
* @param findings - The resolved findings
|
|
580
|
-
* @param failed -
|
|
603
|
+
* @param failed - If `true`, adds a synthetic `referral` impact so an operational failure
|
|
604
|
+
* fails closed; if `false`, derives eligibility from the findings alone. Default: `false`
|
|
581
605
|
* @returns The most severe global eligibility
|
|
582
606
|
*
|
|
583
607
|
* @example
|
|
@@ -594,7 +618,7 @@ function deriveFindingEligibility(findings, failed = false) {
|
|
|
594
618
|
return combineEligibilities(eligibilities);
|
|
595
619
|
}
|
|
596
620
|
/**
|
|
597
|
-
*
|
|
621
|
+
* Returns the most severe {@link Eligibility} in a list.
|
|
598
622
|
*
|
|
599
623
|
* @param eligibilities - The eligibilities to combine
|
|
600
624
|
* @returns The most severe eligibility, or `'eligible'` for an empty list
|
|
@@ -611,7 +635,7 @@ function combineEligibilities(eligibilities) {
|
|
|
611
635
|
return "eligible";
|
|
612
636
|
}
|
|
613
637
|
/**
|
|
614
|
-
*
|
|
638
|
+
* Derives one eligibility per finding scope.
|
|
615
639
|
*
|
|
616
640
|
* @remarks
|
|
617
641
|
* Only applied, scoped findings contribute. Each named scope combines its own
|
|
@@ -641,29 +665,29 @@ function deriveScopeEligibilities(findings) {
|
|
|
641
665
|
return scopes;
|
|
642
666
|
}
|
|
643
667
|
/**
|
|
644
|
-
*
|
|
645
|
-
*
|
|
668
|
+
* Describes each ruling whose pass or rule does not exist or whose pass is not
|
|
669
|
+
* logical, and each pass id shadowing the reserved {@link QUALIFICATION_KEY}.
|
|
646
670
|
*
|
|
647
671
|
* @param definition - The qualification definition to check
|
|
648
672
|
* @returns A fresh list of reference-error messages
|
|
649
673
|
*
|
|
650
674
|
* @example
|
|
651
675
|
* ```ts
|
|
652
|
-
* import {
|
|
653
|
-
* import {
|
|
676
|
+
* import { createQualificationDefinition, createRuling, describeMissingReferences } from '@orkestrel/qualifier'
|
|
677
|
+
* import { createAtom, createLogicalDefinition, createRule } from '@orkestrel/reason'
|
|
654
678
|
*
|
|
655
|
-
* const gates =
|
|
656
|
-
*
|
|
679
|
+
* const gates = createLogicalDefinition('gates', 'Gates', [
|
|
680
|
+
* createRule('licensed', [createAtom('licensed', 'equals', false)], createAtom('blocked', 'equals', true)),
|
|
657
681
|
* ])
|
|
658
|
-
* const definition =
|
|
659
|
-
* rulings: [
|
|
682
|
+
* const definition = createQualificationDefinition('standard', 'Standard', [gates], {
|
|
683
|
+
* rulings: [createRuling('license', 'gates', 'absent', 'restriction')],
|
|
660
684
|
* })
|
|
661
685
|
*
|
|
662
|
-
*
|
|
686
|
+
* describeMissingReferences(definition)
|
|
663
687
|
* // ["Ruling 'license' references missing rule 'absent' in pass 'gates'"]
|
|
664
688
|
* ```
|
|
665
689
|
*/
|
|
666
|
-
function
|
|
690
|
+
function describeMissingReferences(definition) {
|
|
667
691
|
const errors = [];
|
|
668
692
|
const passes = new Map(definition.passes.map((pass) => [pass.id, pass]));
|
|
669
693
|
for (const pass of definition.passes) if (pass.id === "qualification") errors.push(`Pass id must not equal reserved key '${QUALIFICATION_KEY}'`);
|
|
@@ -682,10 +706,10 @@ function findMissingReferences(definition) {
|
|
|
682
706
|
return errors;
|
|
683
707
|
}
|
|
684
708
|
/**
|
|
685
|
-
*
|
|
709
|
+
* Determines whether a subject already owns the reserved {@link QUALIFICATION_KEY}.
|
|
686
710
|
*
|
|
687
711
|
* @param subject - The subject to check
|
|
688
|
-
* @returns
|
|
712
|
+
* @returns True if the subject owns the reserved key; false otherwise
|
|
689
713
|
*
|
|
690
714
|
* @example
|
|
691
715
|
* ```ts
|
|
@@ -698,7 +722,7 @@ function hasReservedKey(subject) {
|
|
|
698
722
|
return Object.hasOwn(subject, QUALIFICATION_KEY);
|
|
699
723
|
}
|
|
700
724
|
/**
|
|
701
|
-
*
|
|
725
|
+
* Asserts a value is a valid qualification {@link Subject}, narrowing it in place.
|
|
702
726
|
*
|
|
703
727
|
* @param value - The candidate subject to validate
|
|
704
728
|
* @throws {@link QualifierError} `'MISMATCH'` when the value is not a record, or
|
|
@@ -716,12 +740,12 @@ function assertSubject(value) {
|
|
|
716
740
|
if (hasReservedKey(value)) throw new QualifierError("MISMATCH", `Qualification subject must not contain reserved key '${QUALIFICATION_KEY}'`);
|
|
717
741
|
}
|
|
718
742
|
/**
|
|
719
|
-
*
|
|
743
|
+
* Maps an engine throw caught while running one pass to a typed {@link QualifierError}.
|
|
720
744
|
*
|
|
721
745
|
* @remarks
|
|
722
746
|
* A reason `ReasonError('INVALID')` maps to `DEFINITION` (the engine rejected the
|
|
723
747
|
* pass as an invalid definition); `ReasonError('DESTROYED')` maps to `DESTROYED`;
|
|
724
|
-
* every other `ReasonError` code (
|
|
748
|
+
* every other `ReasonError` code (for example `MISSING`) and every non-`ReasonError` throw
|
|
725
749
|
* (including a `bail: true` reasoner rethrow) maps to `ENGINE`. The original throw
|
|
726
750
|
* is preserved as `context.cause`.
|
|
727
751
|
*
|
|
@@ -758,7 +782,7 @@ function mapEngineError(error, pass) {
|
|
|
758
782
|
});
|
|
759
783
|
}
|
|
760
784
|
/**
|
|
761
|
-
*
|
|
785
|
+
* Describes each logical pass carrying no rulings.
|
|
762
786
|
*
|
|
763
787
|
* @remarks
|
|
764
788
|
* A logical pass with no matching ruling can never contribute a finding, so its
|
|
@@ -770,18 +794,18 @@ function mapEngineError(error, pass) {
|
|
|
770
794
|
*
|
|
771
795
|
* @example
|
|
772
796
|
* ```ts
|
|
773
|
-
* import {
|
|
774
|
-
* import {
|
|
797
|
+
* import { createQualificationDefinition, describeEmptyLogicalPasses } from '@orkestrel/qualifier'
|
|
798
|
+
* import { createAtom, createLogicalDefinition, createRule } from '@orkestrel/reason'
|
|
775
799
|
*
|
|
776
|
-
* const gates =
|
|
777
|
-
*
|
|
800
|
+
* const gates = createLogicalDefinition('gates', 'Gates', [
|
|
801
|
+
* createRule('licensed', [createAtom('licensed', 'equals', false)], createAtom('blocked', 'equals', true)),
|
|
778
802
|
* ])
|
|
779
|
-
* const definition =
|
|
803
|
+
* const definition = createQualificationDefinition('standard', 'Standard', [gates])
|
|
780
804
|
*
|
|
781
|
-
*
|
|
805
|
+
* describeEmptyLogicalPasses(definition) // ["Logical pass 'gates' has no rulings"]
|
|
782
806
|
* ```
|
|
783
807
|
*/
|
|
784
|
-
function
|
|
808
|
+
function describeEmptyLogicalPasses(definition) {
|
|
785
809
|
const warnings = [];
|
|
786
810
|
for (const pass of definition.passes) {
|
|
787
811
|
if (pass.reasoning !== "logical") continue;
|
|
@@ -790,7 +814,7 @@ function findEmptyLogicalPasses(definition) {
|
|
|
790
814
|
return warnings;
|
|
791
815
|
}
|
|
792
816
|
/**
|
|
793
|
-
*
|
|
817
|
+
* Describes each quantitative pass never read by a later pass.
|
|
794
818
|
*
|
|
795
819
|
* @remarks
|
|
796
820
|
* A quantitative pass's projected value lives under `qualification.<id>` — if no
|
|
@@ -804,18 +828,18 @@ function findEmptyLogicalPasses(definition) {
|
|
|
804
828
|
*
|
|
805
829
|
* @example
|
|
806
830
|
* ```ts
|
|
807
|
-
* import {
|
|
808
|
-
* import {
|
|
831
|
+
* import { createQualificationDefinition, describeUnreadDerivations } from '@orkestrel/qualifier'
|
|
832
|
+
* import { createFactorGroup, createQuantitativeDefinition, createStaticFactor } from '@orkestrel/reason'
|
|
809
833
|
*
|
|
810
|
-
* const cap =
|
|
811
|
-
*
|
|
834
|
+
* const cap = createQuantitativeDefinition('cap', 'TIV cap', [
|
|
835
|
+
* createFactorGroup('limit', 'sum', [createStaticFactor('base', 500_000)]),
|
|
812
836
|
* ])
|
|
813
|
-
* const definition =
|
|
837
|
+
* const definition = createQualificationDefinition('standard', 'Standard', [cap])
|
|
814
838
|
*
|
|
815
|
-
*
|
|
839
|
+
* describeUnreadDerivations(definition) // ["Quantitative pass 'cap' is never read by a later pass"]
|
|
816
840
|
* ```
|
|
817
841
|
*/
|
|
818
|
-
function
|
|
842
|
+
function describeUnreadDerivations(definition) {
|
|
819
843
|
const warnings = [];
|
|
820
844
|
definition.passes.forEach((pass, index) => {
|
|
821
845
|
if (pass.reasoning !== "quantitative") return;
|
|
@@ -832,69 +856,10 @@ function findUnreadDerivations(definition) {
|
|
|
832
856
|
});
|
|
833
857
|
return warnings;
|
|
834
858
|
}
|
|
835
|
-
/**
|
|
836
|
-
* Build a {@link QualificationDefinition}.
|
|
837
|
-
*
|
|
838
|
-
* @remarks
|
|
839
|
-
* Returns a fresh top-level definition, omitting absent optional keys;
|
|
840
|
-
* `passes` and `rulings` arrays are copied, and record `metadata` is
|
|
841
|
-
* shallow-copied so nested values are not deep-cloned.
|
|
842
|
-
*
|
|
843
|
-
* @param id - The definition id
|
|
844
|
-
* @param name - The display name
|
|
845
|
-
* @param passes - The ordered qualification passes
|
|
846
|
-
* @param input - Optional description, rulings, and metadata
|
|
847
|
-
* @returns A fresh qualification definition
|
|
848
|
-
*
|
|
849
|
-
* @example
|
|
850
|
-
* ```ts
|
|
851
|
-
* import { qualificationDefinition } from '@orkestrel/qualifier'
|
|
852
|
-
*
|
|
853
|
-
* qualificationDefinition('standard', 'Standard', [gates], { rulings: [ruling] })
|
|
854
|
-
* ```
|
|
855
|
-
*/
|
|
856
|
-
function qualificationDefinition(id, name, passes, input) {
|
|
857
|
-
return {
|
|
858
|
-
id,
|
|
859
|
-
name,
|
|
860
|
-
passes: [...passes],
|
|
861
|
-
...input?.description === void 0 ? {} : { description: input.description },
|
|
862
|
-
...input?.rulings === void 0 ? {} : { rulings: [...input.rulings] },
|
|
863
|
-
...input?.metadata === void 0 ? {} : { metadata: { ...input.metadata } }
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
/**
|
|
867
|
-
* Build a {@link Ruling} — one authored consequence for one rule in one pass.
|
|
868
|
-
*
|
|
869
|
-
* @param id - The ruling id
|
|
870
|
-
* @param pass - The logical pass id the rule lives in
|
|
871
|
-
* @param rule - The rule id whose firing this ruling reacts to
|
|
872
|
-
* @param effect - The eligibility impact when the rule fires
|
|
873
|
-
* @param input - Optional selection scope and message template
|
|
874
|
-
* @returns A fresh ruling
|
|
875
|
-
*
|
|
876
|
-
* @example
|
|
877
|
-
* ```ts
|
|
878
|
-
* import { rulingDefinition } from '@orkestrel/qualifier'
|
|
879
|
-
*
|
|
880
|
-
* rulingDefinition('license', 'gates', 'licensed', 'restriction', { message: 'A license is required' })
|
|
881
|
-
* ```
|
|
882
|
-
*/
|
|
883
|
-
function rulingDefinition(id, pass, rule, effect, input) {
|
|
884
|
-
return {
|
|
885
|
-
id,
|
|
886
|
-
pass,
|
|
887
|
-
rule,
|
|
888
|
-
effect,
|
|
889
|
-
...input?.scope === void 0 ? {} : { scope: input.scope },
|
|
890
|
-
...input?.message === void 0 ? {} : { message: input.message }
|
|
891
|
-
};
|
|
892
|
-
}
|
|
893
859
|
//#endregion
|
|
894
860
|
//#region src/core/Qualifier.ts
|
|
895
861
|
/**
|
|
896
|
-
*
|
|
897
|
-
* eligibility.
|
|
862
|
+
* Runs ordered passes over one reason engine and returns eligibility.
|
|
898
863
|
*
|
|
899
864
|
* @remarks
|
|
900
865
|
* The engine is OWNED when self-created (destroyed on `destroy()`) and borrowed
|
|
@@ -924,17 +889,92 @@ var Qualifier = class {
|
|
|
924
889
|
this.#validate = options?.validate ?? true;
|
|
925
890
|
this.#labels = options?.labels;
|
|
926
891
|
}
|
|
892
|
+
/**
|
|
893
|
+
* Holds the typed observation surface carrying `derive`, `finding`, `qualify`, and
|
|
894
|
+
* `destroy`.
|
|
895
|
+
*
|
|
896
|
+
* @returns The emitter this qualifier owns
|
|
897
|
+
*
|
|
898
|
+
* @example
|
|
899
|
+
* ```ts
|
|
900
|
+
* import { Qualifier } from '@orkestrel/qualifier'
|
|
901
|
+
*
|
|
902
|
+
* const qualifier = new Qualifier()
|
|
903
|
+
* qualifier.emitter.on('qualify', (result) => console.log(result.eligibility))
|
|
904
|
+
* qualifier.destroy()
|
|
905
|
+
* ```
|
|
906
|
+
*/
|
|
927
907
|
get emitter() {
|
|
928
908
|
return this.#emitter;
|
|
929
909
|
}
|
|
910
|
+
/**
|
|
911
|
+
* Qualifies one subject against one authored definition.
|
|
912
|
+
*
|
|
913
|
+
* @remarks
|
|
914
|
+
* Semantic validation runs first when the `validate` option is on, so an invalid
|
|
915
|
+
* definition throws before any pass runs. Passes run in authored order over a
|
|
916
|
+
* copy-on-write working subject, and the caller's `subject` is never mutated. An
|
|
917
|
+
* unscoped restriction stops the remaining passes because no later finding can be
|
|
918
|
+
* more severe.
|
|
919
|
+
*
|
|
920
|
+
* @param subject - The record to qualify
|
|
921
|
+
* @param definition - The authored qualification definition
|
|
922
|
+
* @returns A fresh, frozen qualification result
|
|
923
|
+
* @throws {@link QualifierError} `'DEFINITION'` when validation is enabled and the
|
|
924
|
+
* definition fails semantic validation, or when the engine rejects a pass as an invalid
|
|
925
|
+
* definition; `'MISMATCH'` when the subject is not a record or already carries the
|
|
926
|
+
* reserved `qualification` key; `'DESTROYED'` after `destroy`, or when the engine
|
|
927
|
+
* reports itself destroyed mid-pass; `'ENGINE'` for every other engine throw
|
|
928
|
+
*
|
|
929
|
+
* @example
|
|
930
|
+
* ```ts
|
|
931
|
+
* import { createQualificationDefinition, createRuling, Qualifier } from '@orkestrel/qualifier'
|
|
932
|
+
* import { createAtom, createLogicalDefinition, createRule } from '@orkestrel/reason'
|
|
933
|
+
*
|
|
934
|
+
* const gates = createLogicalDefinition('gates', 'Eligibility gates', [
|
|
935
|
+
* createRule('licensed', [createAtom('licensed', 'equals', false)], createAtom('blocked', 'equals', true)),
|
|
936
|
+
* ])
|
|
937
|
+
* const definition = createQualificationDefinition('standard', 'Standard eligibility', [gates], {
|
|
938
|
+
* rulings: [createRuling('license', 'gates', 'licensed', 'restriction')],
|
|
939
|
+
* })
|
|
940
|
+
*
|
|
941
|
+
* const qualifier = new Qualifier()
|
|
942
|
+
* qualifier.qualify({ id: 'risk-1', licensed: false }, definition).eligibility // 'ineligible'
|
|
943
|
+
* qualifier.destroy()
|
|
944
|
+
* ```
|
|
945
|
+
*/
|
|
930
946
|
qualify(subject, definition) {
|
|
931
947
|
this.#alive();
|
|
932
948
|
if (this.#validate) {
|
|
933
949
|
const validation = this.validate(definition);
|
|
934
|
-
if (!validation.valid) throw new QualifierError("DEFINITION", `Qualification definition is invalid: ${validation.errors.join(", ")}`, definition.id);
|
|
950
|
+
if (!validation.valid) throw new QualifierError("DEFINITION", `Qualification definition is invalid: ${validation.errors.join(", ")}`, { definition: definition.id });
|
|
935
951
|
}
|
|
936
952
|
return this.#qualify(subject, definition);
|
|
937
953
|
}
|
|
954
|
+
/**
|
|
955
|
+
* Validates one authored definition semantically, without running it.
|
|
956
|
+
*
|
|
957
|
+
* @remarks
|
|
958
|
+
* Structural shape is `isQualificationDefinition`'s job. This checks the meaning:
|
|
959
|
+
* non-empty id and name, valid and uniquely identified passes and rulings, every
|
|
960
|
+
* ruling reference resolving to a logical pass and one of its rules, and no pass id
|
|
961
|
+
* shadowing `QUALIFICATION_KEY`. An empty definition, a logical pass carrying no
|
|
962
|
+
* ruling, and a derivation no later pass reads are warnings rather than errors.
|
|
963
|
+
*
|
|
964
|
+
* @param definition - The authored qualification definition
|
|
965
|
+
* @returns A fresh validation result carrying `valid`, `errors`, and `warnings`
|
|
966
|
+
* @throws {@link QualifierError} `'DESTROYED'` after `destroy`
|
|
967
|
+
*
|
|
968
|
+
* @example
|
|
969
|
+
* ```ts
|
|
970
|
+
* import { createQualificationDefinition, Qualifier } from '@orkestrel/qualifier'
|
|
971
|
+
*
|
|
972
|
+
* const qualifier = new Qualifier()
|
|
973
|
+
* qualifier.validate(createQualificationDefinition('empty', 'Empty', []))
|
|
974
|
+
* // { valid: true, errors: [], warnings: ['Definition has no passes'] }
|
|
975
|
+
* qualifier.destroy()
|
|
976
|
+
* ```
|
|
977
|
+
*/
|
|
938
978
|
validate(definition) {
|
|
939
979
|
this.#alive();
|
|
940
980
|
const errors = [];
|
|
@@ -949,16 +989,33 @@ var Qualifier = class {
|
|
|
949
989
|
});
|
|
950
990
|
for (const id of findDuplicates(definition.passes)) errors.push(`Duplicate pass id '${id}'`);
|
|
951
991
|
for (const id of findDuplicates(definition.rulings ?? [])) errors.push(`Duplicate ruling id '${id}'`);
|
|
952
|
-
for (const reference of
|
|
992
|
+
for (const reference of describeMissingReferences(definition)) errors.push(reference);
|
|
953
993
|
if (definition.passes.length === 0) warnings.push("Definition has no passes");
|
|
954
|
-
for (const warning of
|
|
955
|
-
for (const warning of
|
|
994
|
+
for (const warning of describeEmptyLogicalPasses(definition)) warnings.push(warning);
|
|
995
|
+
for (const warning of describeUnreadDerivations(definition)) warnings.push(warning);
|
|
956
996
|
return {
|
|
957
997
|
valid: errors.length === 0,
|
|
958
998
|
errors,
|
|
959
999
|
warnings
|
|
960
1000
|
};
|
|
961
1001
|
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Destroys this qualifier, idempotently.
|
|
1004
|
+
*
|
|
1005
|
+
* @remarks
|
|
1006
|
+
* An engine this qualifier created is destroyed; an injected engine stays
|
|
1007
|
+
* caller-owned and is left alone. The `destroy` event fires before the emitter
|
|
1008
|
+
* itself is destroyed, and a later call returns without re-emitting.
|
|
1009
|
+
*
|
|
1010
|
+
* @example
|
|
1011
|
+
* ```ts
|
|
1012
|
+
* import { Qualifier } from '@orkestrel/qualifier'
|
|
1013
|
+
*
|
|
1014
|
+
* const qualifier = new Qualifier()
|
|
1015
|
+
* qualifier.destroy()
|
|
1016
|
+
* qualifier.destroy() // idempotent
|
|
1017
|
+
* ```
|
|
1018
|
+
*/
|
|
962
1019
|
destroy() {
|
|
963
1020
|
if (this.#destroyed) return;
|
|
964
1021
|
if (this.#owned) this.#engine.destroy();
|
|
@@ -1034,12 +1091,15 @@ var Qualifier = class {
|
|
|
1034
1091
|
//#endregion
|
|
1035
1092
|
//#region src/core/factories.ts
|
|
1036
1093
|
/**
|
|
1037
|
-
*
|
|
1094
|
+
* Creates one qualifier over a reason engine.
|
|
1038
1095
|
*
|
|
1039
1096
|
* @remarks
|
|
1040
1097
|
* A standalone qualifier creates and OWNS one shared quantitative-plus-logical
|
|
1041
1098
|
* reason engine, destroying it on `destroy()`. An injected `options.engine`
|
|
1042
|
-
* remains caller-owned and is never destroyed.
|
|
1099
|
+
* remains caller-owned and is never destroyed. `validate` runs semantic
|
|
1100
|
+
* validation before every qualification. Default: `true`. `labels` overrides the
|
|
1101
|
+
* display name of a premise field, keyed by the dot-joined field path
|
|
1102
|
+
* `formatField` produces, and takes precedence over a premise's own `label`.
|
|
1043
1103
|
*
|
|
1044
1104
|
* @param options - Optional injected engine, validation, labels, and emitter hooks
|
|
1045
1105
|
* @returns A {@link QualifierInterface}
|
|
@@ -1056,7 +1116,65 @@ var Qualifier = class {
|
|
|
1056
1116
|
function createQualifier(options) {
|
|
1057
1117
|
return new Qualifier(options);
|
|
1058
1118
|
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Creates a {@link QualificationDefinition}.
|
|
1121
|
+
*
|
|
1122
|
+
* @remarks
|
|
1123
|
+
* Returns a fresh top-level definition, omitting absent optional keys;
|
|
1124
|
+
* `passes` and `rulings` arrays are copied, and record `metadata` is
|
|
1125
|
+
* shallow-copied so nested values are not deep-cloned.
|
|
1126
|
+
*
|
|
1127
|
+
* @param id - The definition id
|
|
1128
|
+
* @param name - The display name
|
|
1129
|
+
* @param passes - The ordered qualification passes
|
|
1130
|
+
* @param input - Optional description, rulings, and metadata
|
|
1131
|
+
* @returns A fresh qualification definition
|
|
1132
|
+
*
|
|
1133
|
+
* @example
|
|
1134
|
+
* ```ts
|
|
1135
|
+
* import { createQualificationDefinition } from '@orkestrel/qualifier'
|
|
1136
|
+
*
|
|
1137
|
+
* createQualificationDefinition('standard', 'Standard', [gates], { rulings: [ruling] })
|
|
1138
|
+
* ```
|
|
1139
|
+
*/
|
|
1140
|
+
function createQualificationDefinition(id, name, passes, input) {
|
|
1141
|
+
return {
|
|
1142
|
+
id,
|
|
1143
|
+
name,
|
|
1144
|
+
passes: [...passes],
|
|
1145
|
+
...input?.description === void 0 ? {} : { description: input.description },
|
|
1146
|
+
...input?.rulings === void 0 ? {} : { rulings: [...input.rulings] },
|
|
1147
|
+
...input?.metadata === void 0 ? {} : { metadata: { ...input.metadata } }
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Creates a {@link Ruling} — one authored consequence for one rule in one pass.
|
|
1152
|
+
*
|
|
1153
|
+
* @param id - The ruling id
|
|
1154
|
+
* @param pass - The logical pass id the rule lives in
|
|
1155
|
+
* @param rule - The rule id whose firing this ruling reacts to
|
|
1156
|
+
* @param effect - The eligibility impact when the rule fires
|
|
1157
|
+
* @param input - Optional selection scope and message template
|
|
1158
|
+
* @returns A fresh ruling
|
|
1159
|
+
*
|
|
1160
|
+
* @example
|
|
1161
|
+
* ```ts
|
|
1162
|
+
* import { createRuling } from '@orkestrel/qualifier'
|
|
1163
|
+
*
|
|
1164
|
+
* createRuling('license', 'gates', 'licensed', 'restriction', { message: 'A license is required' })
|
|
1165
|
+
* ```
|
|
1166
|
+
*/
|
|
1167
|
+
function createRuling(id, pass, rule, effect, input) {
|
|
1168
|
+
return {
|
|
1169
|
+
id,
|
|
1170
|
+
pass,
|
|
1171
|
+
rule,
|
|
1172
|
+
effect,
|
|
1173
|
+
...input?.scope === void 0 ? {} : { scope: input.scope },
|
|
1174
|
+
...input?.message === void 0 ? {} : { message: input.message }
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1059
1177
|
//#endregion
|
|
1060
|
-
export { DEFAULT_QUALIFIER_VALIDATE, EFFECT_ELIGIBILITIES, ELIGIBILITY_PRECEDENCE, QUALIFICATION_KEY, Qualifier, QualifierError, assertSubject, combineEligibilities,
|
|
1178
|
+
export { DEFAULT_QUALIFIER_VALIDATE, EFFECT_ELIGIBILITIES, ELIGIBILITY_PRECEDENCE, QUALIFICATION_KEY, Qualifier, QualifierError, assertSubject, checkToPremise, combineEligibilities, createQualificationDefinition, createQualifier, createRuling, deriveFindingEligibility, deriveScopeEligibilities, describeEmptyLogicalPasses, describeMissingReferences, describeUnreadDerivations, findRule, hasReservedKey, interpolateMessage, isDerivation, isEligibility, isEligibilityRecord, isFinding, isPremise, isQualificationDefinition, isQualificationEffect, isQualificationPass, isQualificationResult, isQualifierError, isRuling, mapEngineError, mergeQualificationContext, qualificationToRecord, quantitativeResultToDerivation, reasonResultToProjection, renderComparison, renderPremise, renderValue, ruleToPremises, rulingToFinding };
|
|
1061
1179
|
|
|
1062
1180
|
//# sourceMappingURL=index.js.map
|