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