@orkestrel/program 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 +40 -33
- package/dist/src/core/index.cjs +452 -225
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +1846 -1201
- package/dist/src/core/index.d.ts +1846 -1201
- package/dist/src/core/index.js +445 -217
- package/dist/src/core/index.js.map +1 -1
- package/package.json +17 -19
package/dist/src/core/index.js
CHANGED
|
@@ -1,64 +1,106 @@
|
|
|
1
1
|
import { arrayOf, isArray, isBoolean, isFiniteNumber, isJSONValue, isNumber, isRecord, isString, literalOf, objectOf, recordOf, resolveField, whereOf } from "@orkestrel/contract";
|
|
2
|
-
import { createQualifier, findRule, interpolateMessage, isEligibility, isPremise, isQualificationDefinition, isQualificationResult,
|
|
2
|
+
import { createQualifier, findRule, interpolateMessage, isEligibility, isPremise, isQualificationDefinition, isQualificationResult, ruleToPremises } from "@orkestrel/qualifier";
|
|
3
3
|
import { createRater, isRatingDefinition, isRatingResult } from "@orkestrel/rater";
|
|
4
4
|
import { createEvaluator, createLogicalReasoner, createQuantitativeReasoner, createReason, findDuplicates, formatField, isFieldPath, isLogicalDefinition, isLogicalResult, isReasonValidationResult } from "@orkestrel/reason";
|
|
5
5
|
import { Emitter } from "@orkestrel/emitter";
|
|
6
6
|
//#region src/core/constants.ts
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Names the default definition validation policy, `true`, for `createProgram` /
|
|
9
|
+
* `ProgramManager.add`.
|
|
10
|
+
*/
|
|
8
11
|
var DEFAULT_PROGRAM_VALIDATE = true;
|
|
9
|
-
/**
|
|
10
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Lists every {@link Status} literal in tally order — the source the union and its
|
|
14
|
+
* guard derive from.
|
|
15
|
+
*/
|
|
16
|
+
var STATUSES = Object.freeze([
|
|
11
17
|
"ineligible",
|
|
12
18
|
"referral",
|
|
13
19
|
"conditional",
|
|
14
20
|
"unrated",
|
|
15
21
|
"eligible"
|
|
16
22
|
]);
|
|
17
|
-
/**
|
|
23
|
+
/** Maps each global eligibility to its deterministic authority decision. */
|
|
18
24
|
var ELIGIBILITY_DECISIONS = Object.freeze({
|
|
19
25
|
eligible: "approved",
|
|
20
26
|
ineligible: "denied",
|
|
21
27
|
referral: "submitted"
|
|
22
28
|
});
|
|
23
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Names the reserved working-subject key a batch's aggregate projection is written
|
|
31
|
+
* under, `'aggregate'`.
|
|
32
|
+
*/
|
|
24
33
|
var AGGREGATE_KEY = "aggregate";
|
|
25
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Names the reserved working-subject key the authority's outcome projection is
|
|
36
|
+
* written under, `'outcome'`.
|
|
37
|
+
*/
|
|
26
38
|
var OUTCOME_KEY = "outcome";
|
|
27
39
|
//#endregion
|
|
28
40
|
//#region src/core/errors.ts
|
|
29
41
|
/**
|
|
30
|
-
*
|
|
42
|
+
* Reports a coded programmer error thrown by the program layer, carrying a
|
|
43
|
+
* machine-readable code and an optional context and cause.
|
|
31
44
|
*
|
|
32
45
|
* @remarks
|
|
33
46
|
* `DUPLICATE` — a program id collision on `ProgramManager.add`, or a duplicate
|
|
34
|
-
* authored rating-line or notice id. `MISSING` — an
|
|
35
|
-
*
|
|
47
|
+
* authored rating-line or notice id. `MISSING` — an authored notice or
|
|
48
|
+
* qualification ruling scope names no rating line.
|
|
36
49
|
* `DEFINITION` — a program, qualification, rating, authority, or aggregate
|
|
37
50
|
* policy failed validation. `MISMATCH` — an injected entity or a returned
|
|
38
51
|
* reason result has the wrong contract. `RESERVED` — a subject already
|
|
39
52
|
* carries `aggregate` or `outcome`. `DESTROYED` — use of a destroyed entity.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* import { ProgramError } from '@orkestrel/program'
|
|
57
|
+
*
|
|
58
|
+
* const error = new ProgramError('RESERVED', 'Subject carries a reserved key', 'aggregate')
|
|
59
|
+
* error.code // 'RESERVED'
|
|
60
|
+
* ```
|
|
40
61
|
*/
|
|
41
62
|
var ProgramError = class extends Error {
|
|
42
63
|
code;
|
|
43
64
|
context;
|
|
44
|
-
|
|
45
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Creates a coded program error.
|
|
67
|
+
*
|
|
68
|
+
* @param code - The machine-readable failure category
|
|
69
|
+
* @param message - The human-readable failure description
|
|
70
|
+
* @param context - Optional structured context for the failure
|
|
71
|
+
* @param cause - Optional underlying value the failure wraps
|
|
72
|
+
*/
|
|
73
|
+
constructor(code, message, context, cause) {
|
|
74
|
+
super(message, cause === void 0 ? void 0 : { cause });
|
|
46
75
|
this.name = "ProgramError";
|
|
47
76
|
this.code = code;
|
|
48
77
|
this.context = context;
|
|
49
78
|
}
|
|
50
79
|
};
|
|
51
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Determines whether a caught value is a {@link ProgramError}.
|
|
82
|
+
*
|
|
83
|
+
* @param value - The candidate value
|
|
84
|
+
* @returns True if the value is a {@link ProgramError}; false otherwise
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* import { isProgramError, ProgramError } from '@orkestrel/program'
|
|
89
|
+
*
|
|
90
|
+
* isProgramError(new ProgramError('RESERVED', 'Subject carries a reserved key')) // true
|
|
91
|
+
* isProgramError(new Error('Subject carries a reserved key')) // false
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
52
94
|
function isProgramError(value) {
|
|
53
95
|
return value instanceof ProgramError;
|
|
54
96
|
}
|
|
55
97
|
//#endregion
|
|
56
98
|
//#region src/core/validators.ts
|
|
57
99
|
/**
|
|
58
|
-
*
|
|
100
|
+
* Determines whether a value is a {@link Decision} literal.
|
|
59
101
|
*
|
|
60
102
|
* @param value - The candidate value
|
|
61
|
-
* @returns
|
|
103
|
+
* @returns True if `value` is a {@link Decision}; false otherwise
|
|
62
104
|
*
|
|
63
105
|
* @example
|
|
64
106
|
* ```ts
|
|
@@ -69,10 +111,10 @@ function isProgramError(value) {
|
|
|
69
111
|
*/
|
|
70
112
|
var isDecision = literalOf("approved", "denied", "submitted");
|
|
71
113
|
/**
|
|
72
|
-
*
|
|
114
|
+
* Determines whether a value is a {@link Status} literal.
|
|
73
115
|
*
|
|
74
116
|
* @param value - The candidate value
|
|
75
|
-
* @returns
|
|
117
|
+
* @returns True if `value` is a {@link Status}; false otherwise
|
|
76
118
|
*
|
|
77
119
|
* @example
|
|
78
120
|
* ```ts
|
|
@@ -81,12 +123,12 @@ var isDecision = literalOf("approved", "denied", "submitted");
|
|
|
81
123
|
* isStatus('eligible') // true
|
|
82
124
|
* ```
|
|
83
125
|
*/
|
|
84
|
-
var isStatus = literalOf(
|
|
126
|
+
var isStatus = literalOf(STATUSES);
|
|
85
127
|
/**
|
|
86
|
-
*
|
|
128
|
+
* Determines whether a value is a {@link ProgramEffect} literal.
|
|
87
129
|
*
|
|
88
130
|
* @param value - The candidate value
|
|
89
|
-
* @returns
|
|
131
|
+
* @returns True if `value` is a {@link ProgramEffect}; false otherwise
|
|
90
132
|
*
|
|
91
133
|
* @example
|
|
92
134
|
* ```ts
|
|
@@ -97,10 +139,10 @@ var isStatus = literalOf("ineligible", "referral", "conditional", "unrated", "el
|
|
|
97
139
|
*/
|
|
98
140
|
var isProgramEffect = literalOf("notice", "limit");
|
|
99
141
|
/**
|
|
100
|
-
*
|
|
142
|
+
* Determines whether a value is an exact {@link Notice} record.
|
|
101
143
|
*
|
|
102
144
|
* @param value - The candidate value
|
|
103
|
-
* @returns
|
|
145
|
+
* @returns True if `value` is a {@link Notice}; false otherwise
|
|
104
146
|
*
|
|
105
147
|
* @example
|
|
106
148
|
* ```ts
|
|
@@ -117,10 +159,10 @@ function isNotice(value) {
|
|
|
117
159
|
}, ["scope"])(value);
|
|
118
160
|
}
|
|
119
161
|
/**
|
|
120
|
-
*
|
|
162
|
+
* Determines whether a value is an exact {@link AggregateDefinition} record.
|
|
121
163
|
*
|
|
122
164
|
* @param value - The candidate value
|
|
123
|
-
* @returns
|
|
165
|
+
* @returns True if `value` is an {@link AggregateDefinition}; false otherwise
|
|
124
166
|
*
|
|
125
167
|
* @example
|
|
126
168
|
* ```ts
|
|
@@ -132,19 +174,19 @@ function isNotice(value) {
|
|
|
132
174
|
function isAggregateDefinition(value) {
|
|
133
175
|
return recordOf({
|
|
134
176
|
fields: arrayOf(isFieldPath),
|
|
135
|
-
|
|
177
|
+
partition: isFieldPath,
|
|
136
178
|
gates: isLogicalDefinition
|
|
137
|
-
}, ["
|
|
179
|
+
}, ["partition", "gates"])(value);
|
|
138
180
|
}
|
|
139
181
|
/**
|
|
140
|
-
*
|
|
182
|
+
* Determines whether a value is an exact {@link ProgramDefinition} record.
|
|
141
183
|
*
|
|
142
184
|
* @remarks
|
|
143
185
|
* `rating` is optional — an omitted `rating` authors an eligibility-only
|
|
144
186
|
* program (see {@link ProgramDefinition}).
|
|
145
187
|
*
|
|
146
188
|
* @param value - The candidate value
|
|
147
|
-
* @returns
|
|
189
|
+
* @returns True if `value` is a {@link ProgramDefinition}; false otherwise
|
|
148
190
|
*
|
|
149
191
|
* @example
|
|
150
192
|
* ```ts
|
|
@@ -174,7 +216,7 @@ function isProgramDefinition(value) {
|
|
|
174
216
|
])(value);
|
|
175
217
|
}
|
|
176
218
|
/**
|
|
177
|
-
*
|
|
219
|
+
* Determines whether a value is an open program sums record.
|
|
178
220
|
*
|
|
179
221
|
* @remarks
|
|
180
222
|
* Every own string-named property is checked, including non-enumerable
|
|
@@ -183,7 +225,7 @@ function isProgramDefinition(value) {
|
|
|
183
225
|
* infinities, because the published contract does not refine them.
|
|
184
226
|
*
|
|
185
227
|
* @param value - The candidate value
|
|
186
|
-
* @returns
|
|
228
|
+
* @returns True if every own string-named value is a number; false otherwise
|
|
187
229
|
*
|
|
188
230
|
* @example
|
|
189
231
|
* ```ts
|
|
@@ -196,14 +238,14 @@ function isProgramSums(value) {
|
|
|
196
238
|
return whereOf(objectOf({}), (record) => Object.getOwnPropertyNames(record).every((key) => isNumber(Reflect.get(record, key))))(value);
|
|
197
239
|
}
|
|
198
240
|
/**
|
|
199
|
-
*
|
|
241
|
+
* Determines whether a value is an open result-side {@link Determination}.
|
|
200
242
|
*
|
|
201
243
|
* @remarks
|
|
202
244
|
* Unknown members and class instances are admitted. Arrays are refused.
|
|
203
245
|
* Optional `scope` and `message` members may be absent or `undefined`.
|
|
204
246
|
*
|
|
205
247
|
* @param value - The candidate value
|
|
206
|
-
* @returns
|
|
248
|
+
* @returns True if every published determination member conforms; false otherwise
|
|
207
249
|
*
|
|
208
250
|
* @example
|
|
209
251
|
* ```ts
|
|
@@ -221,13 +263,13 @@ var isDetermination = objectOf({
|
|
|
221
263
|
premises: arrayOf(isPremise)
|
|
222
264
|
}, ["scope", "message"]);
|
|
223
265
|
/**
|
|
224
|
-
*
|
|
266
|
+
* Determines whether a value is an open result-side {@link AggregateGroup}.
|
|
225
267
|
*
|
|
226
268
|
* @remarks
|
|
227
269
|
* Unknown members and class instances are admitted. Arrays are refused.
|
|
228
270
|
*
|
|
229
271
|
* @param value - The candidate value
|
|
230
|
-
* @returns
|
|
272
|
+
* @returns True if every published aggregate-group member conforms; false otherwise
|
|
231
273
|
*
|
|
232
274
|
* @example
|
|
233
275
|
* ```ts
|
|
@@ -242,13 +284,13 @@ var isAggregateGroup = objectOf({
|
|
|
242
284
|
sums: isProgramSums
|
|
243
285
|
});
|
|
244
286
|
/**
|
|
245
|
-
*
|
|
287
|
+
* Determines whether a value is an open result-side {@link Tally}.
|
|
246
288
|
*
|
|
247
289
|
* @remarks
|
|
248
290
|
* Unknown members and class instances are admitted. Arrays are refused.
|
|
249
291
|
*
|
|
250
292
|
* @param value - The candidate value
|
|
251
|
-
* @returns
|
|
293
|
+
* @returns True if every published tally member conforms; false otherwise
|
|
252
294
|
*
|
|
253
295
|
* @example
|
|
254
296
|
* ```ts
|
|
@@ -262,27 +304,27 @@ var isTally = objectOf({
|
|
|
262
304
|
sums: isProgramSums
|
|
263
305
|
});
|
|
264
306
|
/**
|
|
265
|
-
*
|
|
307
|
+
* Determines whether a value is a total open status-tally record.
|
|
266
308
|
*
|
|
267
309
|
* @remarks
|
|
268
|
-
* Every {@link Status} in {@link
|
|
310
|
+
* Every {@link Status} in {@link STATUSES} is required and checked.
|
|
269
311
|
* Unknown members and class instances are admitted. Arrays are refused.
|
|
270
312
|
*
|
|
271
313
|
* @param value - The candidate value
|
|
272
|
-
* @returns
|
|
314
|
+
* @returns True if every required status member is a {@link Tally}; false otherwise
|
|
273
315
|
*
|
|
274
316
|
* @example
|
|
275
317
|
* ```ts
|
|
276
|
-
* import {
|
|
318
|
+
* import { buildEmptyTallies, isTallies } from '@orkestrel/program'
|
|
277
319
|
*
|
|
278
|
-
* isTallies(
|
|
320
|
+
* isTallies(buildEmptyTallies([])) // true
|
|
279
321
|
* ```
|
|
280
322
|
*/
|
|
281
323
|
function isTallies(value) {
|
|
282
|
-
return whereOf(objectOf({}), (record) =>
|
|
324
|
+
return whereOf(objectOf({}), (record) => STATUSES.every((status) => isTally(Reflect.get(record, status))))(value);
|
|
283
325
|
}
|
|
284
326
|
/**
|
|
285
|
-
*
|
|
327
|
+
* Determines whether a value is an open {@link ProgramResult}.
|
|
286
328
|
*
|
|
287
329
|
* @remarks
|
|
288
330
|
* This guard is result-postured for values returned through a borrowed
|
|
@@ -291,7 +333,7 @@ function isTallies(value) {
|
|
|
291
333
|
* over their complete nested result closures. Arrays are refused.
|
|
292
334
|
*
|
|
293
335
|
* @param value - The candidate value
|
|
294
|
-
* @returns
|
|
336
|
+
* @returns True if every published program-result member conforms; false otherwise
|
|
295
337
|
*
|
|
296
338
|
* @example
|
|
297
339
|
* ```ts
|
|
@@ -314,7 +356,7 @@ var isProgramResult = objectOf({
|
|
|
314
356
|
errors: arrayOf(isString)
|
|
315
357
|
}, ["decision", "rating"]);
|
|
316
358
|
/**
|
|
317
|
-
*
|
|
359
|
+
* Determines whether a value is an open {@link AggregateResult}.
|
|
318
360
|
*
|
|
319
361
|
* @remarks
|
|
320
362
|
* This guard is result-postured for values returned through a borrowed
|
|
@@ -323,7 +365,7 @@ var isProgramResult = objectOf({
|
|
|
323
365
|
* record, and sums record. Arrays are refused.
|
|
324
366
|
*
|
|
325
367
|
* @param value - The candidate value
|
|
326
|
-
* @returns
|
|
368
|
+
* @returns True if every published aggregate-result member conforms; false otherwise
|
|
327
369
|
*
|
|
328
370
|
* @example
|
|
329
371
|
* ```ts
|
|
@@ -346,16 +388,16 @@ var isAggregateResult = objectOf({
|
|
|
346
388
|
errors: arrayOf(isString)
|
|
347
389
|
});
|
|
348
390
|
/**
|
|
349
|
-
*
|
|
391
|
+
* Determines whether a value is an open {@link ProgramValidationResult}.
|
|
350
392
|
*
|
|
351
393
|
* @remarks
|
|
352
394
|
* `ProgramValidationResult` is this package's own declared interface, not an
|
|
353
|
-
* alias of reason's validation result. This guard therefore checks the
|
|
395
|
+
* alias of reason's validation result. This guard therefore checks the
|
|
354
396
|
* program-owned members directly so the contracts may evolve independently.
|
|
355
397
|
* Unknown members and class instances are admitted. Arrays are refused.
|
|
356
398
|
*
|
|
357
399
|
* @param value - The candidate value
|
|
358
|
-
* @returns
|
|
400
|
+
* @returns True if every published program-validation member conforms; false otherwise
|
|
359
401
|
*
|
|
360
402
|
* @example
|
|
361
403
|
* ```ts
|
|
@@ -372,38 +414,7 @@ var isProgramValidationResult = objectOf({
|
|
|
372
414
|
//#endregion
|
|
373
415
|
//#region src/core/helpers.ts
|
|
374
416
|
/**
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
* @remarks
|
|
378
|
-
* The input must be an acyclic JSON tree of bounded depth — a pathologically
|
|
379
|
-
* deep tree throws the engine's `RangeError` (stack exhaustion) rather than
|
|
380
|
-
* hanging. Each copied record uses `Object.defineProperty` for own-property
|
|
381
|
-
* definition, which defends against prototype-pollution keys (`__proto__`).
|
|
382
|
-
*
|
|
383
|
-
* @param value - The JSON value to copy
|
|
384
|
-
* @returns A fresh JSON value
|
|
385
|
-
*
|
|
386
|
-
* @example
|
|
387
|
-
* ```ts
|
|
388
|
-
* import { copyJSONValue } from '@orkestrel/program'
|
|
389
|
-
*
|
|
390
|
-
* copyJSONValue({ a: [1, 2] }) // { a: [1, 2] }, a fresh copy
|
|
391
|
-
* ```
|
|
392
|
-
*/
|
|
393
|
-
function copyJSONValue(value) {
|
|
394
|
-
if (value === null || typeof value !== "object") return value;
|
|
395
|
-
if (Array.isArray(value)) return value.map(copyJSONValue);
|
|
396
|
-
const copy = {};
|
|
397
|
-
for (const [key, entry] of Object.entries(value)) Object.defineProperty(copy, key, {
|
|
398
|
-
value: copyJSONValue(entry),
|
|
399
|
-
enumerable: true,
|
|
400
|
-
writable: true,
|
|
401
|
-
configurable: true
|
|
402
|
-
});
|
|
403
|
-
return copy;
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* Determine whether a caller subject already carries a reserved program key.
|
|
417
|
+
* Determines whether a caller subject already carries a reserved program key.
|
|
407
418
|
*
|
|
408
419
|
* @remarks
|
|
409
420
|
* `aggregate` and `outcome` are program-private working-subject namespaces — the
|
|
@@ -412,7 +423,7 @@ function copyJSONValue(value) {
|
|
|
412
423
|
* collide with a projection, so it is rejected before qualification.
|
|
413
424
|
*
|
|
414
425
|
* @param subject - The caller subject to check
|
|
415
|
-
* @returns
|
|
426
|
+
* @returns True if the subject owns `aggregate` or `outcome`; false otherwise
|
|
416
427
|
*
|
|
417
428
|
* @example
|
|
418
429
|
* ```ts
|
|
@@ -426,11 +437,12 @@ function hasReservedKey(subject) {
|
|
|
426
437
|
return Object.hasOwn(subject, "aggregate") || Object.hasOwn(subject, "outcome");
|
|
427
438
|
}
|
|
428
439
|
/**
|
|
429
|
-
*
|
|
440
|
+
* Asserts a value is a valid program {@link Subject}, narrowing it in place.
|
|
430
441
|
*
|
|
431
442
|
* @param subject - The candidate subject to validate
|
|
432
|
-
* @throws {@link ProgramError}
|
|
433
|
-
*
|
|
443
|
+
* @throws {@link ProgramError} Thrown when the value is not a record (`'MISMATCH'`).
|
|
444
|
+
* @throws {@link ProgramError} Thrown when the value already carries the `aggregate`
|
|
445
|
+
* or `outcome` key (`'RESERVED'`).
|
|
434
446
|
*
|
|
435
447
|
* @example
|
|
436
448
|
* ```ts
|
|
@@ -447,13 +459,13 @@ function assertProgramSubject(subject) {
|
|
|
447
459
|
}
|
|
448
460
|
}
|
|
449
461
|
/**
|
|
450
|
-
*
|
|
462
|
+
* Selects the rating lines a subject may be rated on from scoped eligibility.
|
|
451
463
|
*
|
|
452
464
|
* @remarks
|
|
453
465
|
* A scope names a rating-line id. A line survives when its scope is absent
|
|
454
466
|
* (eligible by default), `eligible`, or a `condition` (which is not an
|
|
455
467
|
* eligibility value and never appears here). A scoped `ineligible` or `referral`
|
|
456
|
-
* removes the line
|
|
468
|
+
* removes the line before the rater is invoked — the excluded line is never
|
|
457
469
|
* evaluated merely to discard its amount.
|
|
458
470
|
*
|
|
459
471
|
* @param lines - The program's authored rating lines
|
|
@@ -474,16 +486,16 @@ function selectProgramLines(lines, scopes) {
|
|
|
474
486
|
});
|
|
475
487
|
}
|
|
476
488
|
/**
|
|
477
|
-
*
|
|
489
|
+
* Derives the final program {@link Status} from a definition's rating policy and
|
|
478
490
|
* qualification/rating evidence.
|
|
479
491
|
*
|
|
480
492
|
* @remarks
|
|
481
|
-
* Explicit policy, not an opaque precedence reduce
|
|
493
|
+
* Explicit policy, not an opaque precedence reduce: global
|
|
482
494
|
* ineligibility or referral is terminal; a scoped referral yields `referral`;
|
|
483
495
|
* an applied `condition` or an applied scoped `restriction` (a line was
|
|
484
|
-
* removed but others rated) is `conditional`. When the definition
|
|
496
|
+
* removed but others rated) is `conditional`. When the definition omits
|
|
485
497
|
* `rating` the program is eligibility-only — status resolves to `conditional`
|
|
486
|
-
* or `eligible` and is
|
|
498
|
+
* or `eligible` and is never `unrated`. Otherwise a subject with no successful
|
|
487
499
|
* rating is `unrated`.
|
|
488
500
|
*
|
|
489
501
|
* @param definition - The authored program definition
|
|
@@ -508,7 +520,7 @@ function deriveStatus(definition, qualification, rating) {
|
|
|
508
520
|
return conditional ? "conditional" : "eligible";
|
|
509
521
|
}
|
|
510
522
|
/**
|
|
511
|
-
*
|
|
523
|
+
* Maps a global {@link Eligibility} to its deterministic authority {@link Decision}.
|
|
512
524
|
*
|
|
513
525
|
* @param eligibility - The global eligibility
|
|
514
526
|
* @returns The matching decision
|
|
@@ -525,8 +537,8 @@ function decideEligibility(eligibility) {
|
|
|
525
537
|
return ELIGIBILITY_DECISIONS[eligibility];
|
|
526
538
|
}
|
|
527
539
|
/**
|
|
528
|
-
*
|
|
529
|
-
* {@link Determination}
|
|
540
|
+
* Resolves authored {@link Notice} values into unconditionally-applied `notice`
|
|
541
|
+
* {@link Determination} values.
|
|
530
542
|
*
|
|
531
543
|
* @remarks
|
|
532
544
|
* Notices are program output only — they never affect eligibility, status, line
|
|
@@ -539,12 +551,12 @@ function decideEligibility(eligibility) {
|
|
|
539
551
|
*
|
|
540
552
|
* @example
|
|
541
553
|
* ```ts
|
|
542
|
-
* import {
|
|
554
|
+
* import { buildNoticeDeterminations } from '@orkestrel/program'
|
|
543
555
|
*
|
|
544
|
-
*
|
|
556
|
+
* buildNoticeDeterminations([{ id: 'min', message: 'Minimum applies' }], { id: 'r1' })
|
|
545
557
|
* ```
|
|
546
558
|
*/
|
|
547
|
-
function
|
|
559
|
+
function buildNoticeDeterminations(notices, subject) {
|
|
548
560
|
return notices.map((notice) => ({
|
|
549
561
|
id: notice.id,
|
|
550
562
|
effect: "notice",
|
|
@@ -555,14 +567,14 @@ function buildNotices(notices, subject) {
|
|
|
555
567
|
}));
|
|
556
568
|
}
|
|
557
569
|
/**
|
|
558
|
-
*
|
|
570
|
+
* Converts a logical result's applied rules into `limit` {@link Determination} values.
|
|
559
571
|
*
|
|
560
572
|
* @remarks
|
|
561
573
|
* Fires for both the per-subject authority and the batch aggregate gates — both
|
|
562
|
-
* are plain {@link LogicalDefinition}
|
|
574
|
+
* are plain {@link LogicalDefinition} definitions with no program-authored ruling map, so a
|
|
563
575
|
* fired rule's own `description` (from `@orkestrel/reason`) is the message
|
|
564
576
|
* template, interpolated against the working record the definition ran against.
|
|
565
|
-
* Rich premises reuse the qualifier's {@link
|
|
577
|
+
* Rich premises reuse the qualifier's {@link ruleToPremises}. A rule that never
|
|
566
578
|
* fires produces no determination — program has no authored ruling map to keep
|
|
567
579
|
* evidence for.
|
|
568
580
|
*
|
|
@@ -575,12 +587,12 @@ function buildNotices(notices, subject) {
|
|
|
575
587
|
*
|
|
576
588
|
* @example
|
|
577
589
|
* ```ts
|
|
578
|
-
* import {
|
|
590
|
+
* import { buildLimitDeterminations } from '@orkestrel/program'
|
|
579
591
|
*
|
|
580
|
-
*
|
|
592
|
+
* buildLimitDeterminations(authority, resolved, outcome, evaluator)
|
|
581
593
|
* ```
|
|
582
594
|
*/
|
|
583
|
-
function
|
|
595
|
+
function buildLimitDeterminations(definition, result, working, evaluator, labels) {
|
|
584
596
|
const output = [];
|
|
585
597
|
for (const entry of result.rules) {
|
|
586
598
|
if (!entry.applied) continue;
|
|
@@ -591,13 +603,13 @@ function buildLimits(definition, result, working, evaluator, labels) {
|
|
|
591
603
|
effect: "limit",
|
|
592
604
|
applied: true,
|
|
593
605
|
...rule.description === void 0 ? {} : { message: interpolateMessage(rule.description, working) },
|
|
594
|
-
premises:
|
|
606
|
+
premises: ruleToPremises(rule, working, evaluator, labels)
|
|
595
607
|
});
|
|
596
608
|
}
|
|
597
609
|
return output;
|
|
598
610
|
}
|
|
599
611
|
/**
|
|
600
|
-
*
|
|
612
|
+
* Builds the private authority outcome projection from an assembled program result.
|
|
601
613
|
*
|
|
602
614
|
* @remarks
|
|
603
615
|
* The authority reads this record under {@link OUTCOME_KEY}; it never receives
|
|
@@ -626,7 +638,7 @@ function buildOutcomeProjection(result) {
|
|
|
626
638
|
};
|
|
627
639
|
}
|
|
628
640
|
/**
|
|
629
|
-
*
|
|
641
|
+
* Assembles a {@link ProgramResult} from its qualification, rating, and
|
|
630
642
|
* determination parts — before or after authority.
|
|
631
643
|
*
|
|
632
644
|
* @remarks
|
|
@@ -634,8 +646,8 @@ function buildOutcomeProjection(result) {
|
|
|
634
646
|
* qualification succeeded, rating (when it ran) succeeded, and authority (when it
|
|
635
647
|
* ran) produced no errors — a valid ineligible or referral outcome still
|
|
636
648
|
* succeeds. `trace` and `errors` accumulate the qualification's, every rated
|
|
637
|
-
* line's worksheet trail, and the authority's. A `decision` is present
|
|
638
|
-
* an authority ran (`options.authority`), the execution
|
|
649
|
+
* line's worksheet trail, and the authority's. A `decision` is present only when
|
|
650
|
+
* an authority ran (`options.authority`), the execution succeeded (`success`),
|
|
639
651
|
* no `limit` determination applied, and status is not `unrated`.
|
|
640
652
|
*
|
|
641
653
|
* @param definition - The authored program definition
|
|
@@ -687,7 +699,7 @@ function buildProgramResult(definition, qualification, rating, determinations, s
|
|
|
687
699
|
};
|
|
688
700
|
}
|
|
689
701
|
/**
|
|
690
|
-
*
|
|
702
|
+
* Adds optional aggregate context to a private subject copy for qualification.
|
|
691
703
|
*
|
|
692
704
|
* @remarks
|
|
693
705
|
* The original subject is returned unchanged when no aggregate context exists.
|
|
@@ -722,7 +734,7 @@ function buildQualificationSubject(subject, aggregate) {
|
|
|
722
734
|
};
|
|
723
735
|
}
|
|
724
736
|
/**
|
|
725
|
-
*
|
|
737
|
+
* Returns authored scopes (qualification ruling scopes or notice scopes) that
|
|
726
738
|
* name no rating line on the program.
|
|
727
739
|
*
|
|
728
740
|
* @remarks
|
|
@@ -749,7 +761,7 @@ function findMissingScopes(definition) {
|
|
|
749
761
|
return [...missing];
|
|
750
762
|
}
|
|
751
763
|
/**
|
|
752
|
-
*
|
|
764
|
+
* Asserts a program definition's always-on construction invariants — missing
|
|
753
765
|
* scope references and duplicate rating-line or notice ids.
|
|
754
766
|
*
|
|
755
767
|
* @remarks
|
|
@@ -758,10 +770,10 @@ function findMissingScopes(definition) {
|
|
|
758
770
|
* an authoring mistake this severe cannot silently compile.
|
|
759
771
|
*
|
|
760
772
|
* @param definition - The program definition to assert
|
|
761
|
-
* @throws {@link ProgramError}
|
|
762
|
-
*
|
|
763
|
-
* @throws {@link ProgramError}
|
|
764
|
-
*
|
|
773
|
+
* @throws {@link ProgramError} Thrown when a ruling or notice scope names no
|
|
774
|
+
* rating line (`'MISSING'`).
|
|
775
|
+
* @throws {@link ProgramError} Thrown when two rating lines or two notices share
|
|
776
|
+
* an id (`'DUPLICATE'`).
|
|
765
777
|
*
|
|
766
778
|
* @example
|
|
767
779
|
* ```ts
|
|
@@ -779,7 +791,7 @@ function assertProgramDefinition(definition) {
|
|
|
779
791
|
if (duplicateNotices.length > 0) throw new ProgramError("DUPLICATE", `Duplicate notice id: ${duplicateNotices.join(", ")}`, definition.id);
|
|
780
792
|
}
|
|
781
793
|
/**
|
|
782
|
-
*
|
|
794
|
+
* Validates a program definition's shape, references, and nested definitions.
|
|
783
795
|
*
|
|
784
796
|
* @remarks
|
|
785
797
|
* The single semantic-validation implementation used by `Program.validate`. It
|
|
@@ -812,7 +824,7 @@ function validateProgramDefinition(definition, qualifier, engine) {
|
|
|
812
824
|
if (definition.id.length === 0) errors.push("Program id must not be empty");
|
|
813
825
|
if (definition.name.length === 0) errors.push("Program name must not be empty");
|
|
814
826
|
const qualification = qualifier.validate(definition.qualification);
|
|
815
|
-
if (
|
|
827
|
+
if (isReasonValidationResult(qualification)) {
|
|
816
828
|
errors.push(...qualification.errors.map((error) => `qualification: ${error}`));
|
|
817
829
|
warnings.push(...qualification.warnings.map((warning) => `qualification: ${warning}`));
|
|
818
830
|
} else errors.push("qualification: Qualifier returned invalid validation result");
|
|
@@ -842,7 +854,7 @@ function validateProgramDefinition(definition, qualifier, engine) {
|
|
|
842
854
|
if (fields.has(key)) errors.push(`Duplicate aggregate field "${key}"`);
|
|
843
855
|
fields.add(key);
|
|
844
856
|
}
|
|
845
|
-
if (aggregate.
|
|
857
|
+
if (aggregate.partition !== void 0 && formatField(aggregate.partition).length === 0) errors.push("Aggregate partition field must be non-empty");
|
|
846
858
|
if (aggregate.gates !== void 0) {
|
|
847
859
|
const validation = engine.validate(aggregate.gates);
|
|
848
860
|
if (isReasonValidationResult(validation)) {
|
|
@@ -860,16 +872,16 @@ function validateProgramDefinition(definition, qualifier, engine) {
|
|
|
860
872
|
};
|
|
861
873
|
}
|
|
862
874
|
/**
|
|
863
|
-
*
|
|
875
|
+
* Coerces a subject's partition-key field to its group-key string.
|
|
864
876
|
*
|
|
865
877
|
* @remarks
|
|
866
878
|
* The key is the resolved field coerced with `String` — `undefined` collapses
|
|
867
879
|
* to the empty string, so a subject missing the field and a subject whose
|
|
868
|
-
* field is literally `''` land in the
|
|
880
|
+
* field is literally `''` land in the same partition, and a numeric `1`
|
|
869
881
|
* collides with the string `'1'`.
|
|
870
882
|
*
|
|
871
883
|
* @param subject - The subject to key
|
|
872
|
-
* @param
|
|
884
|
+
* @param partition - The field the batch partitions on
|
|
873
885
|
* @returns The subject's group key
|
|
874
886
|
*
|
|
875
887
|
* @example
|
|
@@ -879,14 +891,14 @@ function validateProgramDefinition(definition, qualifier, engine) {
|
|
|
879
891
|
* formatGroupKey({ location: 'east' }, 'location') // 'east'
|
|
880
892
|
* ```
|
|
881
893
|
*/
|
|
882
|
-
function formatGroupKey(subject,
|
|
883
|
-
return String(resolveField(subject,
|
|
894
|
+
function formatGroupKey(subject, partition) {
|
|
895
|
+
return String(resolveField(subject, partition) ?? "");
|
|
884
896
|
}
|
|
885
897
|
/**
|
|
886
|
-
*
|
|
898
|
+
* Folds one subject's finite aggregate field values into a sums record.
|
|
887
899
|
*
|
|
888
900
|
* @remarks
|
|
889
|
-
* Returns a
|
|
901
|
+
* Returns a fresh record — `sums` is never mutated. Only finite numbers
|
|
890
902
|
* contribute; a non-numeric or absent value contributes zero (never a
|
|
891
903
|
* coercion). A {@link FieldPath} may be nested — `formatField` renders the
|
|
892
904
|
* dot-joined key the returned record is keyed by.
|
|
@@ -913,7 +925,7 @@ function sumFields(sums, subject, fields) {
|
|
|
913
925
|
return next;
|
|
914
926
|
}
|
|
915
927
|
/**
|
|
916
|
-
*
|
|
928
|
+
* Sums aggregate fields across a batch of subjects.
|
|
917
929
|
*
|
|
918
930
|
* @remarks
|
|
919
931
|
* A {@link FieldPath} may be nested — a nested path sums a nested subject field
|
|
@@ -933,12 +945,12 @@ function sumFields(sums, subject, fields) {
|
|
|
933
945
|
* ```
|
|
934
946
|
*/
|
|
935
947
|
function aggregateSums(subjects, fields) {
|
|
936
|
-
let sums =
|
|
948
|
+
let sums = buildEmptySums(fields);
|
|
937
949
|
for (const subject of subjects) sums = sumFields(sums, subject, fields);
|
|
938
950
|
return sums;
|
|
939
951
|
}
|
|
940
952
|
/**
|
|
941
|
-
*
|
|
953
|
+
* Partitions a batch of subjects by a field, summing aggregate fields per key.
|
|
942
954
|
*
|
|
943
955
|
* @remarks
|
|
944
956
|
* The partition key is derived by {@link formatGroupKey}. Group order follows
|
|
@@ -946,8 +958,8 @@ function aggregateSums(subjects, fields) {
|
|
|
946
958
|
*
|
|
947
959
|
* @param subjects - The batch of subjects
|
|
948
960
|
* @param fields - The fields to sum within each partition
|
|
949
|
-
* @param
|
|
950
|
-
* @returns A fresh list of aggregate groups, or an empty list when `
|
|
961
|
+
* @param partition - The field the batch partitions on; no partition is built when absent
|
|
962
|
+
* @returns A fresh list of aggregate groups, or an empty list when `partition` is absent
|
|
951
963
|
*
|
|
952
964
|
* @example
|
|
953
965
|
* ```ts
|
|
@@ -956,11 +968,11 @@ function aggregateSums(subjects, fields) {
|
|
|
956
968
|
* aggregateGroups([{ location: 'east', amount: 5 }], ['amount'], 'location')
|
|
957
969
|
* ```
|
|
958
970
|
*/
|
|
959
|
-
function aggregateGroups(subjects, fields,
|
|
960
|
-
if (
|
|
971
|
+
function aggregateGroups(subjects, fields, partition) {
|
|
972
|
+
if (partition === void 0) return [];
|
|
961
973
|
const records = /* @__PURE__ */ new Map();
|
|
962
974
|
for (const subject of subjects) {
|
|
963
|
-
const key = formatGroupKey(subject,
|
|
975
|
+
const key = formatGroupKey(subject, partition);
|
|
964
976
|
const group = records.get(key);
|
|
965
977
|
if (group === void 0) records.set(key, [subject]);
|
|
966
978
|
else group.push(subject);
|
|
@@ -972,18 +984,18 @@ function aggregateGroups(subjects, fields, by) {
|
|
|
972
984
|
}));
|
|
973
985
|
}
|
|
974
986
|
/**
|
|
975
|
-
*
|
|
987
|
+
* Builds one subject's overall and optional group aggregate projection.
|
|
976
988
|
*
|
|
977
989
|
* @remarks
|
|
978
990
|
* The projection carries the whole-batch `count` and `sums` plus the subject's
|
|
979
|
-
*
|
|
991
|
+
* own partition, located by the same {@link formatGroupKey} key
|
|
980
992
|
* {@link aggregateGroups} partitions under.
|
|
981
993
|
*
|
|
982
994
|
* @param subject - The subject to project for
|
|
983
995
|
* @param count - The whole-batch subject count
|
|
984
996
|
* @param sums - The whole-batch summed aggregate fields
|
|
985
997
|
* @param groups - The batch partitions
|
|
986
|
-
* @param
|
|
998
|
+
* @param partition - The field the batch partitions on; no group is attached when absent
|
|
987
999
|
* @returns A fresh aggregate projection
|
|
988
1000
|
*
|
|
989
1001
|
* @example
|
|
@@ -993,8 +1005,8 @@ function aggregateGroups(subjects, fields, by) {
|
|
|
993
1005
|
* buildAggregateProjection(subject, 2, { amount: 8 }, groups, 'location')
|
|
994
1006
|
* ```
|
|
995
1007
|
*/
|
|
996
|
-
function buildAggregateProjection(subject, count, sums, groups,
|
|
997
|
-
const group =
|
|
1008
|
+
function buildAggregateProjection(subject, count, sums, groups, partition) {
|
|
1009
|
+
const group = partition === void 0 ? void 0 : groups.find((entry) => entry.key === formatGroupKey(subject, partition));
|
|
998
1010
|
return {
|
|
999
1011
|
count,
|
|
1000
1012
|
sums: { ...sums },
|
|
@@ -1002,7 +1014,7 @@ function buildAggregateProjection(subject, count, sums, groups, by) {
|
|
|
1002
1014
|
};
|
|
1003
1015
|
}
|
|
1004
1016
|
/**
|
|
1005
|
-
*
|
|
1017
|
+
* Builds the reserved-key record a batch aggregate-gate definition runs against.
|
|
1006
1018
|
*
|
|
1007
1019
|
* @remarks
|
|
1008
1020
|
* Unlike a per-subject {@link buildAggregateProjection}, the batch record carries
|
|
@@ -1029,29 +1041,29 @@ function buildAggregateRecord(count, sums, groups) {
|
|
|
1029
1041
|
} };
|
|
1030
1042
|
}
|
|
1031
1043
|
/**
|
|
1032
|
-
*
|
|
1044
|
+
* Builds a zero-sum record for a set of aggregate fields.
|
|
1033
1045
|
*
|
|
1034
1046
|
* @param fields - The fields to zero
|
|
1035
1047
|
* @returns A fresh record of dot-joined field to `0`
|
|
1036
1048
|
*
|
|
1037
1049
|
* @example
|
|
1038
1050
|
* ```ts
|
|
1039
|
-
* import {
|
|
1051
|
+
* import { buildEmptySums } from '@orkestrel/program'
|
|
1040
1052
|
*
|
|
1041
|
-
*
|
|
1053
|
+
* buildEmptySums(['amount']) // { amount: 0 }
|
|
1042
1054
|
* ```
|
|
1043
1055
|
*/
|
|
1044
|
-
function
|
|
1056
|
+
function buildEmptySums(fields) {
|
|
1045
1057
|
const sums = {};
|
|
1046
1058
|
for (const field of fields) sums[formatField(field)] = 0;
|
|
1047
1059
|
return sums;
|
|
1048
1060
|
}
|
|
1049
1061
|
/**
|
|
1050
|
-
*
|
|
1062
|
+
* Completes a partial status tally record with zero entries for every missing
|
|
1051
1063
|
* {@link Status}.
|
|
1052
1064
|
*
|
|
1053
1065
|
* @param entries - The partial tally entries to complete
|
|
1054
|
-
* @returns A record
|
|
1066
|
+
* @returns A record carrying every {@link Status}
|
|
1055
1067
|
*
|
|
1056
1068
|
* @example
|
|
1057
1069
|
* ```ts
|
|
@@ -1085,28 +1097,28 @@ function completeTallies(entries) {
|
|
|
1085
1097
|
};
|
|
1086
1098
|
}
|
|
1087
1099
|
/**
|
|
1088
|
-
*
|
|
1100
|
+
* Builds complete zero status tallies in {@link STATUSES} order.
|
|
1089
1101
|
*
|
|
1090
1102
|
* @param fields - The fields each tally's sums are zeroed for
|
|
1091
1103
|
* @returns A fresh, complete tally record
|
|
1092
1104
|
*
|
|
1093
1105
|
* @example
|
|
1094
1106
|
* ```ts
|
|
1095
|
-
* import {
|
|
1107
|
+
* import { buildEmptyTallies } from '@orkestrel/program'
|
|
1096
1108
|
*
|
|
1097
|
-
*
|
|
1109
|
+
* buildEmptyTallies(['amount'])
|
|
1098
1110
|
* ```
|
|
1099
1111
|
*/
|
|
1100
|
-
function
|
|
1112
|
+
function buildEmptyTallies(fields) {
|
|
1101
1113
|
const entries = {};
|
|
1102
|
-
for (const status of
|
|
1114
|
+
for (const status of STATUSES) entries[status] = {
|
|
1103
1115
|
count: 0,
|
|
1104
|
-
sums:
|
|
1116
|
+
sums: buildEmptySums(fields)
|
|
1105
1117
|
};
|
|
1106
1118
|
return completeTallies(entries);
|
|
1107
1119
|
}
|
|
1108
1120
|
/**
|
|
1109
|
-
*
|
|
1121
|
+
* Adds one subject's aggregate contribution to a status tally record.
|
|
1110
1122
|
*
|
|
1111
1123
|
* @param tallies - The tallies to update
|
|
1112
1124
|
* @param result - The subject's program result (its `status` selects the tally)
|
|
@@ -1116,12 +1128,12 @@ function emptyTallies(fields) {
|
|
|
1116
1128
|
*
|
|
1117
1129
|
* @example
|
|
1118
1130
|
* ```ts
|
|
1119
|
-
* import {
|
|
1131
|
+
* import { tallySubject } from '@orkestrel/program'
|
|
1120
1132
|
*
|
|
1121
|
-
*
|
|
1133
|
+
* tallySubject(tallies, result, { id: 'r1', amount: 5 }, ['amount'])
|
|
1122
1134
|
* ```
|
|
1123
1135
|
*/
|
|
1124
|
-
function
|
|
1136
|
+
function tallySubject(tallies, result, subject, fields) {
|
|
1125
1137
|
const status = result.status;
|
|
1126
1138
|
const current = tallies[status];
|
|
1127
1139
|
const sums = sumFields(current.sums, subject, fields);
|
|
@@ -1134,13 +1146,13 @@ function tallyProgram(tallies, result, subject, fields) {
|
|
|
1134
1146
|
});
|
|
1135
1147
|
}
|
|
1136
1148
|
/**
|
|
1137
|
-
*
|
|
1149
|
+
* Assembles one batch {@link AggregateResult} from its per-subject and aggregate
|
|
1138
1150
|
* parts.
|
|
1139
1151
|
*
|
|
1140
1152
|
* @remarks
|
|
1141
1153
|
* `count` is the subject count, `trace` / `errors` accumulate every subject's
|
|
1142
1154
|
* plus the batch aggregate-gate evaluation's (`options.gates`), and `success`
|
|
1143
|
-
* requires every subject execution to succeed
|
|
1155
|
+
* requires every subject execution to succeed and the gate evaluation to have
|
|
1144
1156
|
* produced no errors. A fired aggregate gate contributes a `limit`
|
|
1145
1157
|
* determination, never a technical failure (a non-logical gate result is a
|
|
1146
1158
|
* caller-facing `MISMATCH` thrown by `Program` before this assembles).
|
|
@@ -1180,11 +1192,14 @@ function buildAggregateResult(definition, subjects, determinations, groups, tall
|
|
|
1180
1192
|
};
|
|
1181
1193
|
}
|
|
1182
1194
|
/**
|
|
1183
|
-
*
|
|
1195
|
+
* Builds a fresh {@link ProgramDefinition}.
|
|
1184
1196
|
*
|
|
1185
1197
|
* @remarks
|
|
1186
|
-
*
|
|
1187
|
-
*
|
|
1198
|
+
* Omits absent optional keys. `metadata` is deep-copied with `structuredClone`.
|
|
1199
|
+
* `notices` is copied as a fresh array whose elements are shared with the
|
|
1200
|
+
* input. `qualification`, `rating`, `authority`, and `aggregate` are stored
|
|
1201
|
+
* by reference. The {@link Program} constructor later snapshots and seals
|
|
1202
|
+
* the whole graph.
|
|
1188
1203
|
*
|
|
1189
1204
|
* @param id - The program id
|
|
1190
1205
|
* @param name - The display name
|
|
@@ -1195,12 +1210,12 @@ function buildAggregateResult(definition, subjects, determinations, groups, tall
|
|
|
1195
1210
|
*
|
|
1196
1211
|
* @example
|
|
1197
1212
|
* ```ts
|
|
1198
|
-
* import {
|
|
1213
|
+
* import { buildProgramDefinition } from '@orkestrel/program'
|
|
1199
1214
|
*
|
|
1200
|
-
*
|
|
1215
|
+
* buildProgramDefinition('standard', 'Standard', qualification, rating, { notices: [notice] })
|
|
1201
1216
|
* ```
|
|
1202
1217
|
*/
|
|
1203
|
-
function
|
|
1218
|
+
function buildProgramDefinition(id, name, qualification, rating, input) {
|
|
1204
1219
|
return {
|
|
1205
1220
|
id,
|
|
1206
1221
|
name,
|
|
@@ -1210,25 +1225,28 @@ function programDefinition(id, name, qualification, rating, input) {
|
|
|
1210
1225
|
...input?.notices === void 0 ? {} : { notices: [...input.notices] },
|
|
1211
1226
|
...input?.authority === void 0 ? {} : { authority: input.authority },
|
|
1212
1227
|
...input?.aggregate === void 0 ? {} : { aggregate: input.aggregate },
|
|
1213
|
-
...input?.metadata === void 0 ? {} : { metadata:
|
|
1228
|
+
...input?.metadata === void 0 ? {} : { metadata: structuredClone(input.metadata) }
|
|
1214
1229
|
};
|
|
1215
1230
|
}
|
|
1216
1231
|
/**
|
|
1217
|
-
*
|
|
1232
|
+
* Builds a fresh {@link Notice}.
|
|
1233
|
+
*
|
|
1234
|
+
* @remarks
|
|
1235
|
+
* An absent `scope` is omitted entirely rather than stored as `undefined`.
|
|
1218
1236
|
*
|
|
1219
1237
|
* @param id - The notice id
|
|
1220
|
-
* @param message - The message template, carrying optional `{{token}}`
|
|
1238
|
+
* @param message - The message template, carrying optional `{{token}}` placeholders
|
|
1221
1239
|
* @param input - Optional presentation scope
|
|
1222
1240
|
* @returns A fresh notice
|
|
1223
1241
|
*
|
|
1224
1242
|
* @example
|
|
1225
1243
|
* ```ts
|
|
1226
|
-
* import {
|
|
1244
|
+
* import { buildNotice } from '@orkestrel/program'
|
|
1227
1245
|
*
|
|
1228
|
-
*
|
|
1246
|
+
* buildNotice('minimum', 'Minimum earned premium applies')
|
|
1229
1247
|
* ```
|
|
1230
1248
|
*/
|
|
1231
|
-
function
|
|
1249
|
+
function buildNotice(id, message, input) {
|
|
1232
1250
|
return {
|
|
1233
1251
|
id,
|
|
1234
1252
|
message,
|
|
@@ -1236,7 +1254,11 @@ function noticeDefinition(id, message, input) {
|
|
|
1236
1254
|
};
|
|
1237
1255
|
}
|
|
1238
1256
|
/**
|
|
1239
|
-
*
|
|
1257
|
+
* Builds a fresh {@link AggregateDefinition}.
|
|
1258
|
+
*
|
|
1259
|
+
* @remarks
|
|
1260
|
+
* `fields` is copied into a fresh array; an absent `partition` or `gates` is
|
|
1261
|
+
* omitted entirely rather than stored as `undefined`.
|
|
1240
1262
|
*
|
|
1241
1263
|
* @param fields - The aggregate fields to sum across a batch
|
|
1242
1264
|
* @param input - Optional partition field and aggregate gates
|
|
@@ -1244,30 +1266,30 @@ function noticeDefinition(id, message, input) {
|
|
|
1244
1266
|
*
|
|
1245
1267
|
* @example
|
|
1246
1268
|
* ```ts
|
|
1247
|
-
* import {
|
|
1269
|
+
* import { buildAggregateDefinition } from '@orkestrel/program'
|
|
1248
1270
|
*
|
|
1249
|
-
*
|
|
1271
|
+
* buildAggregateDefinition(['amount'], { partition: 'location' })
|
|
1250
1272
|
* ```
|
|
1251
1273
|
*/
|
|
1252
|
-
function
|
|
1274
|
+
function buildAggregateDefinition(fields, input) {
|
|
1253
1275
|
return {
|
|
1254
1276
|
fields: [...fields],
|
|
1255
|
-
...input?.
|
|
1277
|
+
...input?.partition === void 0 ? {} : { partition: input.partition },
|
|
1256
1278
|
...input?.gates === void 0 ? {} : { gates: input.gates }
|
|
1257
1279
|
};
|
|
1258
1280
|
}
|
|
1259
1281
|
//#endregion
|
|
1260
1282
|
//#region src/core/programs/Program.ts
|
|
1261
1283
|
/**
|
|
1262
|
-
*
|
|
1263
|
-
*
|
|
1284
|
+
* Composes one qualifier and one rater over a shared reason engine, compiling one
|
|
1285
|
+
* authored definition and executing single subjects or aggregate-aware batches.
|
|
1264
1286
|
*
|
|
1265
1287
|
* @remarks
|
|
1266
1288
|
* Qualification decides whether rating happens: a globally ineligible, referred,
|
|
1267
1289
|
* or failed subject never reaches the rater, and a scoped ineligibility removes
|
|
1268
1290
|
* only its line before the first rating call. The rater always receives the
|
|
1269
|
-
*
|
|
1270
|
-
* qualifier, rater, or engine is injected the program creates
|
|
1291
|
+
* original subject; the qualifier's aggregate projection stays private. When no
|
|
1292
|
+
* qualifier, rater, or engine is injected the program creates one shared
|
|
1271
1293
|
* quantitative-plus-logical engine, injects it into the qualifier and rater it
|
|
1272
1294
|
* creates, and destroys only what it owns. A definition failure during
|
|
1273
1295
|
* construction (an invalid definition under `options.validate`) tears down
|
|
@@ -1277,8 +1299,8 @@ function aggregateDefinition(fields, input) {
|
|
|
1277
1299
|
* or `Date` reached through a reason `Check.value` is cloned but remains mutable
|
|
1278
1300
|
* because its contents live in internal slots. Uncloneable values and non-empty
|
|
1279
1301
|
* typed arrays are refused with `ProgramError('DEFINITION')` and the host error
|
|
1280
|
-
* as its cause. `destroy()` is idempotent and
|
|
1281
|
-
* flag is set
|
|
1302
|
+
* as its cause. `destroy()` is idempotent and reentrancy-safe — the destroyed
|
|
1303
|
+
* flag is set before any teardown or the `destroy` event fires, so a listener
|
|
1282
1304
|
* that re-enters `destroy()` is a no-op — and tears the emitter down last.
|
|
1283
1305
|
*/
|
|
1284
1306
|
var Program = class {
|
|
@@ -1293,21 +1315,31 @@ var Program = class {
|
|
|
1293
1315
|
#validate;
|
|
1294
1316
|
#labels;
|
|
1295
1317
|
#destroyed = false;
|
|
1318
|
+
/** Holds the authored id of the definition this program compiled. */
|
|
1296
1319
|
id;
|
|
1320
|
+
/** Holds the authored display name of the definition this program compiled. */
|
|
1297
1321
|
name;
|
|
1322
|
+
/** Holds the sealed snapshot of the authored definition this program compiled. */
|
|
1298
1323
|
definition;
|
|
1324
|
+
/**
|
|
1325
|
+
* Compiles one program from an authored definition.
|
|
1326
|
+
*
|
|
1327
|
+
* @param definition - The authored program definition
|
|
1328
|
+
* @param options - Optional injected qualifier, rater, engine, validation, labels, and emitter hooks
|
|
1329
|
+
* @throws {@link ProgramError} Thrown when the definition cannot be cloned or
|
|
1330
|
+
* sealed, or when validation is enabled and the definition fails
|
|
1331
|
+
* (`'DEFINITION'`).
|
|
1332
|
+
* @throws {@link ProgramError} Thrown when a ruling or notice scope names no
|
|
1333
|
+
* rating line (`'MISSING'`).
|
|
1334
|
+
* @throws {@link ProgramError} Thrown when the definition repeats a rating-line
|
|
1335
|
+
* or notice id (`'DUPLICATE'`).
|
|
1336
|
+
*/
|
|
1299
1337
|
constructor(definition, options) {
|
|
1300
1338
|
let snapshot;
|
|
1301
1339
|
try {
|
|
1302
1340
|
snapshot = structuredClone(definition);
|
|
1303
1341
|
} catch (cause) {
|
|
1304
|
-
|
|
1305
|
-
Object.defineProperty(error, "cause", {
|
|
1306
|
-
configurable: true,
|
|
1307
|
-
value: cause,
|
|
1308
|
-
writable: true
|
|
1309
|
-
});
|
|
1310
|
-
throw error;
|
|
1342
|
+
throw new ProgramError("DEFINITION", "Program definition could not be cloned", void 0, cause);
|
|
1311
1343
|
}
|
|
1312
1344
|
assertProgramDefinition(snapshot);
|
|
1313
1345
|
this.id = snapshot.id;
|
|
@@ -1316,13 +1348,7 @@ var Program = class {
|
|
|
1316
1348
|
try {
|
|
1317
1349
|
this.#seal();
|
|
1318
1350
|
} catch (cause) {
|
|
1319
|
-
|
|
1320
|
-
Object.defineProperty(error, "cause", {
|
|
1321
|
-
configurable: true,
|
|
1322
|
-
value: cause,
|
|
1323
|
-
writable: true
|
|
1324
|
-
});
|
|
1325
|
-
throw error;
|
|
1351
|
+
throw new ProgramError("DEFINITION", "Program definition could not be sealed", snapshot.id, cause);
|
|
1326
1352
|
}
|
|
1327
1353
|
this.#emitter = new Emitter({
|
|
1328
1354
|
...options?.on === void 0 ? {} : { on: options.on },
|
|
@@ -1348,6 +1374,21 @@ var Program = class {
|
|
|
1348
1374
|
}
|
|
1349
1375
|
}
|
|
1350
1376
|
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Holds the typed observation surface carrying `qualify`, `rate`, `determine`,
|
|
1379
|
+
* `decide`, `execute`, `aggregate`, and `destroy`.
|
|
1380
|
+
*
|
|
1381
|
+
* @returns The emitter this program owns
|
|
1382
|
+
*
|
|
1383
|
+
* @example
|
|
1384
|
+
* ```ts
|
|
1385
|
+
* import { createProgram } from '@orkestrel/program'
|
|
1386
|
+
*
|
|
1387
|
+
* const program = createProgram(definition)
|
|
1388
|
+
* program.emitter.on('execute', (result) => result.status)
|
|
1389
|
+
* program.destroy()
|
|
1390
|
+
* ```
|
|
1391
|
+
*/
|
|
1351
1392
|
get emitter() {
|
|
1352
1393
|
return this.#emitter;
|
|
1353
1394
|
}
|
|
@@ -1356,10 +1397,51 @@ var Program = class {
|
|
|
1356
1397
|
if (isArray(input)) return this.#aggregate(input);
|
|
1357
1398
|
return this.#subject(input);
|
|
1358
1399
|
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Validates this program's definition and every nested definition.
|
|
1402
|
+
*
|
|
1403
|
+
* @remarks
|
|
1404
|
+
* Exact shape is `isProgramDefinition`'s job. This checks the meaning: non-empty id
|
|
1405
|
+
* and name, every ruling and notice scope naming a rating line, unique non-empty
|
|
1406
|
+
* aggregate fields, and a non-empty partition field when present. Nested
|
|
1407
|
+
* qualification validation is delegated to the injected qualifier, and authority
|
|
1408
|
+
* and aggregate-gate validation to the shared reason engine.
|
|
1409
|
+
*
|
|
1410
|
+
* @returns A fresh validation result carrying `valid`, `errors`, and `warnings`
|
|
1411
|
+
* @throws {@link ProgramError} Thrown when the program has been destroyed
|
|
1412
|
+
* (`'DESTROYED'`).
|
|
1413
|
+
*
|
|
1414
|
+
* @example
|
|
1415
|
+
* ```ts
|
|
1416
|
+
* import { createProgram } from '@orkestrel/program'
|
|
1417
|
+
*
|
|
1418
|
+
* const program = createProgram(definition, { validate: false })
|
|
1419
|
+
* program.validate().valid // true
|
|
1420
|
+
* program.destroy()
|
|
1421
|
+
* ```
|
|
1422
|
+
*/
|
|
1359
1423
|
validate() {
|
|
1360
1424
|
this.#alive();
|
|
1361
1425
|
return validateProgramDefinition(this.definition, this.#qualifier, this.#engine);
|
|
1362
1426
|
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Destroys this program, idempotently.
|
|
1429
|
+
*
|
|
1430
|
+
* @remarks
|
|
1431
|
+
* The destroyed flag is set before any teardown or the `destroy` event, so a
|
|
1432
|
+
* listener re-entering `destroy` is a no-op. An owned qualifier, rater, and reason
|
|
1433
|
+
* engine are destroyed; an injected one stays caller-owned. The emitter is torn
|
|
1434
|
+
* down last, and stays reachable afterwards.
|
|
1435
|
+
*
|
|
1436
|
+
* @example
|
|
1437
|
+
* ```ts
|
|
1438
|
+
* import { createProgram } from '@orkestrel/program'
|
|
1439
|
+
*
|
|
1440
|
+
* const program = createProgram(definition)
|
|
1441
|
+
* program.destroy()
|
|
1442
|
+
* program.destroy() // a second call is a no-op
|
|
1443
|
+
* ```
|
|
1444
|
+
*/
|
|
1363
1445
|
destroy() {
|
|
1364
1446
|
if (this.#destroyed) return;
|
|
1365
1447
|
this.#destroyed = true;
|
|
@@ -1385,7 +1467,7 @@ var Program = class {
|
|
|
1385
1467
|
return this.#finish(subject, qualification, rating);
|
|
1386
1468
|
}
|
|
1387
1469
|
#finish(subject, qualification, rating) {
|
|
1388
|
-
const notices =
|
|
1470
|
+
const notices = buildNoticeDeterminations(this.definition.notices ?? [], subject);
|
|
1389
1471
|
for (const notice of notices) this.#emitter.emit("determine", notice);
|
|
1390
1472
|
const status = deriveStatus(this.definition, qualification, rating);
|
|
1391
1473
|
let result = buildProgramResult(this.definition, qualification, rating, notices, status);
|
|
@@ -1397,7 +1479,7 @@ var Program = class {
|
|
|
1397
1479
|
const outcome = { [OUTCOME_KEY]: buildOutcomeProjection(result) };
|
|
1398
1480
|
const resolved = this.#engine.reason(outcome, authority);
|
|
1399
1481
|
if (!isLogicalResult(resolved)) throw new ProgramError("MISMATCH", "Authority returned invalid logical result", authority.id);
|
|
1400
|
-
const limits =
|
|
1482
|
+
const limits = buildLimitDeterminations(authority, resolved, outcome, this.#evaluator, this.#labels);
|
|
1401
1483
|
for (const limit of limits) this.#emitter.emit("determine", limit);
|
|
1402
1484
|
result = buildProgramResult(this.definition, qualification, rating, [...notices, ...limits], status, { authority: resolved });
|
|
1403
1485
|
if (result.decision !== void 0) this.#emitter.emit("decide", result.decision, result);
|
|
@@ -1409,12 +1491,12 @@ var Program = class {
|
|
|
1409
1491
|
const definition = this.definition.aggregate;
|
|
1410
1492
|
const fields = [...definition?.fields ?? []];
|
|
1411
1493
|
const sums = aggregateSums(subjects, fields);
|
|
1412
|
-
const groups = aggregateGroups(subjects, fields, definition?.
|
|
1413
|
-
let tallies =
|
|
1494
|
+
const groups = aggregateGroups(subjects, fields, definition?.partition);
|
|
1495
|
+
let tallies = buildEmptyTallies(fields);
|
|
1414
1496
|
const results = subjects.map((subject) => {
|
|
1415
|
-
const projection = definition === void 0 ? void 0 : buildAggregateProjection(subject, subjects.length, sums, groups, definition.
|
|
1497
|
+
const projection = definition === void 0 ? void 0 : buildAggregateProjection(subject, subjects.length, sums, groups, definition.partition);
|
|
1416
1498
|
const result = this.#subject(subject, projection);
|
|
1417
|
-
tallies =
|
|
1499
|
+
tallies = tallySubject(tallies, result, subject, fields);
|
|
1418
1500
|
return result;
|
|
1419
1501
|
});
|
|
1420
1502
|
const gates = this.#aggregateLimits(subjects.length, sums, groups);
|
|
@@ -1428,7 +1510,7 @@ var Program = class {
|
|
|
1428
1510
|
const record = buildAggregateRecord(count, sums, groups);
|
|
1429
1511
|
const resolved = this.#engine.reason(record, gates);
|
|
1430
1512
|
if (!isLogicalResult(resolved)) throw new ProgramError("MISMATCH", "Aggregate gates returned invalid logical result", gates.id);
|
|
1431
|
-
const determinations =
|
|
1513
|
+
const determinations = buildLimitDeterminations(gates, resolved, record, this.#evaluator, this.#labels);
|
|
1432
1514
|
for (const determination of determinations) this.#emitter.emit("determine", determination);
|
|
1433
1515
|
return {
|
|
1434
1516
|
determinations,
|
|
@@ -1451,18 +1533,18 @@ var Program = class {
|
|
|
1451
1533
|
//#endregion
|
|
1452
1534
|
//#region src/core/programs/ProgramManager.ts
|
|
1453
1535
|
/**
|
|
1454
|
-
*
|
|
1455
|
-
*
|
|
1536
|
+
* Manages compiled {@link ProgramInterface} programs in order, sharing one
|
|
1537
|
+
* qualifier, rater, and reason engine across every program it compiles.
|
|
1456
1538
|
*
|
|
1457
1539
|
* @remarks
|
|
1458
|
-
*
|
|
1540
|
+
* owns its ordered `#programs` collection and its own {@link Emitter} over
|
|
1459
1541
|
* {@link ProgramManagerEventMap}. Creates or borrows one shared engine, qualifier,
|
|
1460
1542
|
* and rater and injects the same instances into every compiled program. `remove`
|
|
1461
1543
|
* destroys the programs it removes; `destroy()` removes all programs, then
|
|
1462
|
-
* destroys only the owned shared dependencies, and tears the emitter down
|
|
1544
|
+
* destroys only the owned shared dependencies, and tears the emitter down last.
|
|
1463
1545
|
* A seed-program failure during construction tears the manager down (destroying
|
|
1464
1546
|
* whatever had already been compiled) before rethrowing the original error.
|
|
1465
|
-
* `destroy()` is
|
|
1547
|
+
* `destroy()` is reentrancy-safe — the destroyed flag is set before any teardown
|
|
1466
1548
|
* or the `remove` / `destroy` events fire, so a `remove` listener that re-enters
|
|
1467
1549
|
* `destroy()` is a no-op. Every call after `destroy()` throws {@link ProgramError}
|
|
1468
1550
|
* `'DESTROYED'`.
|
|
@@ -1479,6 +1561,13 @@ var ProgramManager = class {
|
|
|
1479
1561
|
#validate;
|
|
1480
1562
|
#labels;
|
|
1481
1563
|
#destroyed = false;
|
|
1564
|
+
/**
|
|
1565
|
+
* Creates one manager and compiles every seed definition in order.
|
|
1566
|
+
*
|
|
1567
|
+
* @param options - Optional injected qualifier, rater, engine, seed programs, validation, labels, and emitter hooks
|
|
1568
|
+
* @throws {@link ProgramError} Thrown when a seed definition fails to compile,
|
|
1569
|
+
* after the manager destroys whatever it had already compiled.
|
|
1570
|
+
*/
|
|
1482
1571
|
constructor(options) {
|
|
1483
1572
|
this.#emitter = new Emitter({
|
|
1484
1573
|
...options?.on === void 0 ? {} : { on: options.on },
|
|
@@ -1502,25 +1591,138 @@ var ProgramManager = class {
|
|
|
1502
1591
|
throw error;
|
|
1503
1592
|
}
|
|
1504
1593
|
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Holds the typed observation surface carrying `add`, `remove`, and `destroy`.
|
|
1596
|
+
*
|
|
1597
|
+
* @returns The emitter this manager owns
|
|
1598
|
+
*
|
|
1599
|
+
* @example
|
|
1600
|
+
* ```ts
|
|
1601
|
+
* import { createProgramManager } from '@orkestrel/program'
|
|
1602
|
+
*
|
|
1603
|
+
* const manager = createProgramManager()
|
|
1604
|
+
* manager.emitter.on('add', (id) => id)
|
|
1605
|
+
* manager.destroy()
|
|
1606
|
+
* ```
|
|
1607
|
+
*/
|
|
1505
1608
|
get emitter() {
|
|
1506
1609
|
return this.#emitter;
|
|
1507
1610
|
}
|
|
1508
|
-
|
|
1611
|
+
/**
|
|
1612
|
+
* Holds how many programs the manager has compiled.
|
|
1613
|
+
*
|
|
1614
|
+
* @returns The number of compiled programs
|
|
1615
|
+
* @throws {@link ProgramError} Thrown when the manager has been destroyed
|
|
1616
|
+
* (`'DESTROYED'`).
|
|
1617
|
+
*
|
|
1618
|
+
* @example
|
|
1619
|
+
* ```ts
|
|
1620
|
+
* import { createProgramManager } from '@orkestrel/program'
|
|
1621
|
+
*
|
|
1622
|
+
* const manager = createProgramManager({ programs: [definition] })
|
|
1623
|
+
* manager.count // 1
|
|
1624
|
+
* manager.destroy()
|
|
1625
|
+
* ```
|
|
1626
|
+
*/
|
|
1627
|
+
get count() {
|
|
1509
1628
|
this.#alive();
|
|
1510
1629
|
return this.#programs.length;
|
|
1511
1630
|
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Reports whether an id names a compiled program.
|
|
1633
|
+
*
|
|
1634
|
+
* @param id - The program id to look for
|
|
1635
|
+
* @returns True if a compiled program carries the id; false otherwise
|
|
1636
|
+
* @throws {@link ProgramError} Thrown when the manager has been destroyed
|
|
1637
|
+
* (`'DESTROYED'`).
|
|
1638
|
+
*
|
|
1639
|
+
* @example
|
|
1640
|
+
* ```ts
|
|
1641
|
+
* import { createProgramManager } from '@orkestrel/program'
|
|
1642
|
+
*
|
|
1643
|
+
* const manager = createProgramManager({ programs: [definition] })
|
|
1644
|
+
* manager.has('standard') // true
|
|
1645
|
+
* manager.destroy()
|
|
1646
|
+
* ```
|
|
1647
|
+
*/
|
|
1512
1648
|
has(id) {
|
|
1513
1649
|
this.#alive();
|
|
1514
1650
|
return this.#programs.some((program) => program.id === id);
|
|
1515
1651
|
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Looks one compiled program up by id.
|
|
1654
|
+
*
|
|
1655
|
+
* @param id - The program id to look up
|
|
1656
|
+
* @returns The compiled program, or `undefined` when no program carries the id
|
|
1657
|
+
* @throws {@link ProgramError} Thrown when the manager has been destroyed
|
|
1658
|
+
* (`'DESTROYED'`).
|
|
1659
|
+
*
|
|
1660
|
+
* @example
|
|
1661
|
+
* ```ts
|
|
1662
|
+
* import { createProgramManager } from '@orkestrel/program'
|
|
1663
|
+
*
|
|
1664
|
+
* const manager = createProgramManager({ programs: [definition] })
|
|
1665
|
+
* manager.program('standard')?.execute({ id: 'risk-1', licensed: true })
|
|
1666
|
+
* manager.destroy()
|
|
1667
|
+
* ```
|
|
1668
|
+
*/
|
|
1516
1669
|
program(id) {
|
|
1517
1670
|
this.#alive();
|
|
1518
1671
|
return this.#programs.find((program) => program.id === id);
|
|
1519
1672
|
}
|
|
1673
|
+
/**
|
|
1674
|
+
* Returns every compiled program, in insertion order.
|
|
1675
|
+
*
|
|
1676
|
+
* @remarks
|
|
1677
|
+
* The returned array is a fresh copy, so mutating it never reaches the manager's
|
|
1678
|
+
* own collection.
|
|
1679
|
+
*
|
|
1680
|
+
* @returns A fresh array of compiled programs, in insertion order
|
|
1681
|
+
* @throws {@link ProgramError} Thrown when the manager has been destroyed
|
|
1682
|
+
* (`'DESTROYED'`).
|
|
1683
|
+
*
|
|
1684
|
+
* @example
|
|
1685
|
+
* ```ts
|
|
1686
|
+
* import { createProgramManager } from '@orkestrel/program'
|
|
1687
|
+
*
|
|
1688
|
+
* const manager = createProgramManager({ programs: [definition] })
|
|
1689
|
+
* manager.programs().map((program) => program.id) // ['standard']
|
|
1690
|
+
* manager.destroy()
|
|
1691
|
+
* ```
|
|
1692
|
+
*/
|
|
1520
1693
|
programs() {
|
|
1521
1694
|
this.#alive();
|
|
1522
1695
|
return [...this.#programs];
|
|
1523
1696
|
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Compiles one definition and appends it to the collection.
|
|
1699
|
+
*
|
|
1700
|
+
* @remarks
|
|
1701
|
+
* The compiled program borrows the manager's shared qualifier, rater, and reason
|
|
1702
|
+
* engine, and inherits the manager's `validate` and `labels` options. After
|
|
1703
|
+
* appending the program, the `add` event fires with its id.
|
|
1704
|
+
*
|
|
1705
|
+
* @param definition - The authored program definition to compile
|
|
1706
|
+
* @returns The compiled program
|
|
1707
|
+
* @throws {@link ProgramError} Thrown when the manager has been destroyed
|
|
1708
|
+
* (`'DESTROYED'`).
|
|
1709
|
+
* @throws {@link ProgramError} Thrown when the manager already carries the
|
|
1710
|
+
* definition's id, or the definition repeats a rating-line or notice id
|
|
1711
|
+
* (`'DUPLICATE'`).
|
|
1712
|
+
* @throws {@link ProgramError} Thrown when a ruling or notice scope names no
|
|
1713
|
+
* rating line (`'MISSING'`).
|
|
1714
|
+
* @throws {@link ProgramError} Thrown when validation is enabled and the
|
|
1715
|
+
* definition fails (`'DEFINITION'`).
|
|
1716
|
+
*
|
|
1717
|
+
* @example
|
|
1718
|
+
* ```ts
|
|
1719
|
+
* import { createProgramManager } from '@orkestrel/program'
|
|
1720
|
+
*
|
|
1721
|
+
* const manager = createProgramManager()
|
|
1722
|
+
* manager.add(definition).id // 'standard'
|
|
1723
|
+
* manager.destroy()
|
|
1724
|
+
* ```
|
|
1725
|
+
*/
|
|
1524
1726
|
add(definition) {
|
|
1525
1727
|
this.#alive();
|
|
1526
1728
|
if (this.has(definition.id)) throw new ProgramError("DUPLICATE", `Program "${definition.id}" already exists`, definition.id);
|
|
@@ -1548,6 +1750,25 @@ var ProgramManager = class {
|
|
|
1548
1750
|
}
|
|
1549
1751
|
if (typeof input === "string") return this.#removeOne(input);
|
|
1550
1752
|
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Destroys this manager, idempotently.
|
|
1755
|
+
*
|
|
1756
|
+
* @remarks
|
|
1757
|
+
* The destroyed flag is set before any teardown or the `remove` and `destroy`
|
|
1758
|
+
* events, so a `remove` listener re-entering `destroy` is a no-op. Compiled
|
|
1759
|
+
* programs are destroyed first, then an owned qualifier, rater, and reason engine;
|
|
1760
|
+
* an injected one stays caller-owned. The emitter is torn down last, and stays
|
|
1761
|
+
* reachable afterwards.
|
|
1762
|
+
*
|
|
1763
|
+
* @example
|
|
1764
|
+
* ```ts
|
|
1765
|
+
* import { createProgramManager } from '@orkestrel/program'
|
|
1766
|
+
*
|
|
1767
|
+
* const manager = createProgramManager({ programs: [definition] })
|
|
1768
|
+
* manager.destroy()
|
|
1769
|
+
* manager.destroy() // a second call is a no-op
|
|
1770
|
+
* ```
|
|
1771
|
+
*/
|
|
1551
1772
|
destroy() {
|
|
1552
1773
|
if (this.#destroyed) return;
|
|
1553
1774
|
this.#destroyed = true;
|
|
@@ -1580,32 +1801,39 @@ var ProgramManager = class {
|
|
|
1580
1801
|
//#endregion
|
|
1581
1802
|
//#region src/core/factories.ts
|
|
1582
1803
|
/**
|
|
1583
|
-
*
|
|
1804
|
+
* Creates one compiled {@link ProgramInterface} over a qualifier and rater.
|
|
1584
1805
|
*
|
|
1585
1806
|
* @remarks
|
|
1586
|
-
*
|
|
1587
|
-
*
|
|
1588
|
-
*
|
|
1589
|
-
*
|
|
1807
|
+
* If `options.validate` is `true`, the program validates the definition at
|
|
1808
|
+
* construction; if `false`, it compiles the definition unvalidated. Default:
|
|
1809
|
+
* {@link DEFAULT_PROGRAM_VALIDATE}. A standalone program creates and owns one
|
|
1810
|
+
* shared quantitative-plus-logical reason engine and injects it into the qualifier
|
|
1811
|
+
* and rater it creates; injected dependencies remain caller-owned.
|
|
1590
1812
|
*
|
|
1591
1813
|
* @param definition - The authored program definition
|
|
1592
1814
|
* @param options - Optional injected qualifier, rater, engine, validation, labels, and emitter hooks
|
|
1593
1815
|
* @returns A {@link ProgramInterface}
|
|
1594
1816
|
*
|
|
1595
|
-
* @example
|
|
1817
|
+
* @example Compile a program and a manager
|
|
1596
1818
|
* ```ts
|
|
1597
|
-
* import { createProgram,
|
|
1819
|
+
* import { buildProgramDefinition, createProgram, createProgramManager } from '@orkestrel/program'
|
|
1820
|
+
*
|
|
1821
|
+
* const definition = buildProgramDefinition('standard', 'Standard', qualification, rating)
|
|
1822
|
+
*
|
|
1823
|
+
* const program = createProgram(definition)
|
|
1824
|
+
* const manager = createProgramManager({ programs: [definition] })
|
|
1598
1825
|
*
|
|
1599
|
-
* const program = createProgram(programDefinition('standard', 'Standard', qualification, rating))
|
|
1600
1826
|
* program.execute({ id: 'risk-1' })
|
|
1827
|
+
*
|
|
1601
1828
|
* program.destroy()
|
|
1829
|
+
* manager.destroy()
|
|
1602
1830
|
* ```
|
|
1603
1831
|
*/
|
|
1604
1832
|
function createProgram(definition, options) {
|
|
1605
1833
|
return new Program(definition, options);
|
|
1606
1834
|
}
|
|
1607
1835
|
/**
|
|
1608
|
-
*
|
|
1836
|
+
* Creates one ordered {@link ProgramManagerInterface} over compiled programs.
|
|
1609
1837
|
*
|
|
1610
1838
|
* @remarks
|
|
1611
1839
|
* Creates or borrows one shared reason engine, qualifier, and rater and injects
|
|
@@ -1628,6 +1856,6 @@ function createProgramManager(options) {
|
|
|
1628
1856
|
return new ProgramManager(options);
|
|
1629
1857
|
}
|
|
1630
1858
|
//#endregion
|
|
1631
|
-
export { AGGREGATE_KEY, DEFAULT_PROGRAM_VALIDATE, ELIGIBILITY_DECISIONS, OUTCOME_KEY, Program, ProgramError, ProgramManager,
|
|
1859
|
+
export { AGGREGATE_KEY, DEFAULT_PROGRAM_VALIDATE, ELIGIBILITY_DECISIONS, OUTCOME_KEY, Program, ProgramError, ProgramManager, STATUSES, aggregateGroups, aggregateSums, assertProgramDefinition, assertProgramSubject, buildAggregateDefinition, buildAggregateProjection, buildAggregateRecord, buildAggregateResult, buildEmptySums, buildEmptyTallies, buildLimitDeterminations, buildNotice, buildNoticeDeterminations, buildOutcomeProjection, buildProgramDefinition, buildProgramResult, buildQualificationSubject, completeTallies, createProgram, createProgramManager, decideEligibility, deriveStatus, findMissingScopes, formatGroupKey, hasReservedKey, isAggregateDefinition, isAggregateGroup, isAggregateResult, isDecision, isDetermination, isNotice, isProgramDefinition, isProgramEffect, isProgramError, isProgramResult, isProgramSums, isProgramValidationResult, isStatus, isTallies, isTally, selectProgramLines, sumFields, tallySubject, validateProgramDefinition };
|
|
1632
1860
|
|
|
1633
1861
|
//# sourceMappingURL=index.js.map
|