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