@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.
@@ -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
- /** Default definition validation policy for `createProgram` / `ProgramManager.add`. */
8
+ /**
9
+ * Names the default definition validation policy, `true`, for `createProgram` /
10
+ * `ProgramManager.add`.
11
+ */
9
12
  var DEFAULT_PROGRAM_VALIDATE = true;
10
- /** Status tally precedence order — least to most resolved. */
11
- var STATUS_PRECEDENCE = Object.freeze([
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
- /** The deterministic authority decision for each global eligibility. */
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
- /** The reserved working-subject key a batch's aggregate projection is written under. */
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
- /** The reserved working-subject key the authority's outcome projection is written under. */
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
- * A coded programmer error thrown by the program layer.
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
- * authored notice or qualification ruling scope names no rating line.
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
- constructor(code, message, context) {
46
- super(message);
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
- /** Narrow a caught value to a {@link ProgramError}. */
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
- * Determine whether a value is a {@link Decision} literal.
101
+ * Determines whether a value is a {@link Decision} literal.
60
102
  *
61
103
  * @param value - The candidate value
62
- * @returns `true` when `value` is a {@link Decision}
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
- * Determine whether a value is a {@link Status} literal.
115
+ * Determines whether a value is a {@link Status} literal.
74
116
  *
75
117
  * @param value - The candidate value
76
- * @returns `true` when `value` is a {@link Status}
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)("ineligible", "referral", "conditional", "unrated", "eligible");
127
+ var isStatus = (0, _orkestrel_contract.literalOf)(STATUSES);
86
128
  /**
87
- * Determine whether a value is a {@link ProgramEffect} literal.
129
+ * Determines whether a value is a {@link ProgramEffect} literal.
88
130
  *
89
131
  * @param value - The candidate value
90
- * @returns `true` when `value` is a {@link ProgramEffect}
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
- * Determine whether a value is an exact {@link Notice} record.
143
+ * Determines whether a value is an exact {@link Notice} record.
102
144
  *
103
145
  * @param value - The candidate value
104
- * @returns `true` when `value` is a {@link Notice}
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
- * Determine whether a value is an exact {@link AggregateDefinition} record.
163
+ * Determines whether a value is an exact {@link AggregateDefinition} record.
122
164
  *
123
165
  * @param value - The candidate value
124
- * @returns `true` when `value` is an {@link AggregateDefinition}
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
- by: _orkestrel_reason.isFieldPath,
178
+ partition: _orkestrel_reason.isFieldPath,
137
179
  gates: _orkestrel_reason.isLogicalDefinition
138
- }, ["by", "gates"])(value);
180
+ }, ["partition", "gates"])(value);
139
181
  }
140
182
  /**
141
- * Determine whether a value is an exact {@link ProgramDefinition} record.
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 `true` when `value` is a {@link ProgramDefinition}
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
- * Determine whether a value is an open program sums record.
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 `true` when every own string-named value is a number
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
- * Determine whether a value is an open result-side {@link Determination}.
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 `true` when every published determination member conforms
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
- * Determine whether a value is an open result-side {@link AggregateGroup}.
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 `true` when every published aggregate-group member conforms
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
- * Determine whether a value is an open result-side {@link Tally}.
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 `true` when every published tally member conforms
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
- * Determine whether a value is a total open status-tally record.
308
+ * Determines whether a value is a total open status-tally record.
267
309
  *
268
310
  * @remarks
269
- * Every {@link Status} in {@link STATUS_PRECEDENCE} is required and checked.
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 `true` when every required status member is a {@link Tally}
315
+ * @returns True if every required status member is a {@link Tally}; false otherwise
274
316
  *
275
317
  * @example
276
318
  * ```ts
277
- * import { emptyTallies, isTallies } from '@orkestrel/program'
319
+ * import { buildEmptyTallies, isTallies } from '@orkestrel/program'
278
320
  *
279
- * isTallies(emptyTallies([])) // true
321
+ * isTallies(buildEmptyTallies([])) // true
280
322
  * ```
281
323
  */
282
324
  function isTallies(value) {
283
- return (0, _orkestrel_contract.whereOf)((0, _orkestrel_contract.objectOf)({}), (record) => STATUS_PRECEDENCE.every((status) => isTally(Reflect.get(record, status))))(value);
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
- * Determine whether a value is an open {@link ProgramResult}.
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 `true` when every published program-result member conforms
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
- * Determine whether a value is an open {@link AggregateResult}.
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 `true` when every published aggregate-result member conforms
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
- * Determine whether a value is an open {@link ProgramValidationResult}.
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 three
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 `true` when every published program-validation member conforms
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
- * Return a fresh JSON value tree that does not alias the input.
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 `true` when the subject owns `aggregate` or `outcome`
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
- * Assert a value is a valid program {@link Subject}, narrowing it in place.
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} `'MISMATCH'` when the value is not a record, or
434
- * `'RESERVED'` when it already carries the `aggregate` or `outcome` key
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
- * Select the rating lines a subject may be rated on from scoped eligibility.
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 BEFORE the rater is invoked — the excluded line is never
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
- * Derive the final program {@link Status} from a definition's rating policy and
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 (AGENTS §10): global
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 OMITS
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 NEVER `unrated`. Otherwise a subject with no successful
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
- * Map a global {@link Eligibility} to its deterministic authority {@link Decision}.
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
- * Resolve authored {@link Notice}s into unconditionally-applied `notice`
530
- * {@link Determination}s.
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 { buildNotices } from '@orkestrel/program'
555
+ * import { buildNoticeDeterminations } from '@orkestrel/program'
544
556
  *
545
- * buildNotices([{ id: 'min', message: 'Minimum applies' }], { id: 'r1' })
557
+ * buildNoticeDeterminations([{ id: 'min', message: 'Minimum applies' }], { id: 'r1' })
546
558
  * ```
547
559
  */
548
- function buildNotices(notices, subject) {
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
- * Convert a logical result's applied rules into `limit` {@link Determination}s.
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}s with no program-authored ruling map, so a
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 logicalPremises}. A rule that never
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 { buildLimits } from '@orkestrel/program'
591
+ * import { buildLimitDeterminations } from '@orkestrel/program'
580
592
  *
581
- * buildLimits(authority, resolved, outcome, evaluator)
593
+ * buildLimitDeterminations(authority, resolved, outcome, evaluator)
582
594
  * ```
583
595
  */
584
- function buildLimits(definition, result, working, evaluator, labels) {
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.logicalPremises)(rule, working, evaluator, labels)
607
+ premises: (0, _orkestrel_qualifier.ruleToPremises)(rule, working, evaluator, labels)
596
608
  });
597
609
  }
598
610
  return output;
599
611
  }
600
612
  /**
601
- * Build the private authority outcome projection from an assembled program result.
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
- * Assemble a {@link ProgramResult} from its qualification, rating, and
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 ONLY when
639
- * an authority ran (`options.authority`), the execution SUCCEEDED (`success`),
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
- * Add optional aggregate context to a private subject copy for qualification.
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
- * Return authored scopes (qualification ruling scopes or notice scopes) that
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
- * Assert a program definition's always-on construction invariants — missing
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} `'MISSING'` when a ruling or notice scope names
763
- * no rating line
764
- * @throws {@link ProgramError} `'DUPLICATE'` when two rating lines or two
765
- * notices share an id
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
- * Validate a program definition's shape, references, and nested definitions.
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, _orkestrel_qualifier.isQualificationValidationResult)(qualification)) {
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.by !== void 0 && (0, _orkestrel_reason.formatField)(aggregate.by).length === 0) errors.push("Aggregate partition field must be non-empty");
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
- * Coerce a subject's partition-key field to its group-key string.
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 SAME partition, and a numeric `1`
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 by - The partition key field
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, by) {
884
- return String((0, _orkestrel_contract.resolveField)(subject, by) ?? "");
895
+ function formatGroupKey(subject, partition) {
896
+ return String((0, _orkestrel_contract.resolveField)(subject, partition) ?? "");
885
897
  }
886
898
  /**
887
- * Fold one subject's finite aggregate field values into a sums record.
899
+ * Folds one subject's finite aggregate field values into a sums record.
888
900
  *
889
901
  * @remarks
890
- * Returns a FRESH record — `sums` is never mutated. Only finite numbers
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
- * Sum aggregate fields across a batch of subjects.
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 = emptySums(fields);
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
- * Partition a batch of subjects by a field, summing aggregate fields per key.
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 by - The partition key field; no partition is built when absent
951
- * @returns A fresh list of aggregate groups, or an empty list when `by` is absent
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, by) {
961
- if (by === void 0) return [];
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, by);
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
- * Build one subject's overall and optional group aggregate projection.
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
- * OWN partition, located by the same {@link formatGroupKey} key
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 by - The partition key field; no group is attached when absent
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, by) {
998
- const group = by === void 0 ? void 0 : groups.find((entry) => entry.key === formatGroupKey(subject, by));
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
- * Build the reserved-key record a batch aggregate-gate definition runs against.
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
- * Build a zero-sum record for a set of aggregate fields.
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 { emptySums } from '@orkestrel/program'
1052
+ * import { buildEmptySums } from '@orkestrel/program'
1041
1053
  *
1042
- * emptySums(['amount']) // { amount: 0 }
1054
+ * buildEmptySums(['amount']) // { amount: 0 }
1043
1055
  * ```
1044
1056
  */
1045
- function emptySums(fields) {
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
- * Complete a partial status tally record with zero entries for every missing
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 with all five statuses present
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
- * Build complete zero status tallies in {@link STATUS_PRECEDENCE} order.
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 { emptyTallies } from '@orkestrel/program'
1108
+ * import { buildEmptyTallies } from '@orkestrel/program'
1097
1109
  *
1098
- * emptyTallies(['amount'])
1110
+ * buildEmptyTallies(['amount'])
1099
1111
  * ```
1100
1112
  */
1101
- function emptyTallies(fields) {
1113
+ function buildEmptyTallies(fields) {
1102
1114
  const entries = {};
1103
- for (const status of STATUS_PRECEDENCE) entries[status] = {
1115
+ for (const status of STATUSES) entries[status] = {
1104
1116
  count: 0,
1105
- sums: emptySums(fields)
1117
+ sums: buildEmptySums(fields)
1106
1118
  };
1107
1119
  return completeTallies(entries);
1108
1120
  }
1109
1121
  /**
1110
- * Add one subject's aggregate contribution to a status tally record.
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 { tallyProgram } from '@orkestrel/program'
1132
+ * import { tallySubject } from '@orkestrel/program'
1121
1133
  *
1122
- * tallyProgram(tallies, result, { id: 'r1', amount: 5 }, ['amount'])
1134
+ * tallySubject(tallies, result, { id: 'r1', amount: 5 }, ['amount'])
1123
1135
  * ```
1124
1136
  */
1125
- function tallyProgram(tallies, result, subject, fields) {
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
- * Assemble one batch {@link AggregateResult} from its per-subject and aggregate
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 AND the gate evaluation to have
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
- * Build a {@link ProgramDefinition}.
1196
+ * Builds a fresh {@link ProgramDefinition}.
1185
1197
  *
1186
1198
  * @remarks
1187
- * Copies every collection and omits absent optional keys, so the returned
1188
- * definition is a fresh, JSON-serializable value that never aliases its inputs.
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 { programDefinition } from '@orkestrel/program'
1214
+ * import { buildProgramDefinition } from '@orkestrel/program'
1200
1215
  *
1201
- * programDefinition('standard', 'Standard', qualification, rating, { notices: [notice] })
1216
+ * buildProgramDefinition('standard', 'Standard', qualification, rating, { notices: [notice] })
1202
1217
  * ```
1203
1218
  */
1204
- function programDefinition(id, name, qualification, rating, input) {
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: copyJSONValue(input.metadata) }
1229
+ ...input?.metadata === void 0 ? {} : { metadata: structuredClone(input.metadata) }
1215
1230
  };
1216
1231
  }
1217
1232
  /**
1218
- * Build a {@link Notice}.
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}}`s
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 { noticeDefinition } from '@orkestrel/program'
1245
+ * import { buildNotice } from '@orkestrel/program'
1228
1246
  *
1229
- * noticeDefinition('minimum', 'Minimum earned premium applies')
1247
+ * buildNotice('minimum', 'Minimum earned premium applies')
1230
1248
  * ```
1231
1249
  */
1232
- function noticeDefinition(id, message, input) {
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
- * Build an {@link AggregateDefinition}.
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 { aggregateDefinition } from '@orkestrel/program'
1270
+ * import { buildAggregateDefinition } from '@orkestrel/program'
1249
1271
  *
1250
- * aggregateDefinition(['amount'], { by: 'location' })
1272
+ * buildAggregateDefinition(['amount'], { partition: 'location' })
1251
1273
  * ```
1252
1274
  */
1253
- function aggregateDefinition(fields, input) {
1275
+ function buildAggregateDefinition(fields, input) {
1254
1276
  return {
1255
1277
  fields: [...fields],
1256
- ...input?.by === void 0 ? {} : { by: input.by },
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
- * One compiled program — composes one qualifier and one rater over a shared
1264
- * reason engine and executes single subjects or aggregate-aware batches.
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
- * ORIGINAL subject; the qualifier's aggregate projection stays private. When no
1271
- * qualifier, rater, or engine is injected the program creates ONE shared
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 REENTRANCY-SAFE — the destroyed
1282
- * flag is set BEFORE any teardown or the `destroy` event fires, so a listener
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
- const error = new ProgramError("DEFINITION", "Program definition could not be cloned");
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
- const error = new ProgramError("DEFINITION", "Program definition could not be sealed", snapshot.id);
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 = buildNotices(this.definition.notices ?? [], subject);
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 = buildLimits(authority, resolved, outcome, this.#evaluator, this.#labels);
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?.by);
1414
- let tallies = emptyTallies(fields);
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.by);
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 = tallyProgram(tallies, result, subject, fields);
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 = buildLimits(gates, resolved, record, this.#evaluator, this.#labels);
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
- * An ordered manager over compiled {@link ProgramInterface}s (AGENTS §9), sharing
1456
- * one qualifier, rater, and reason engine across every program it compiles.
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
- * OWNS its ordered `#programs` collection and its own {@link Emitter} over
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 LAST.
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 REENTRANCY-SAFE — the destroyed flag is set BEFORE any teardown
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
- get size() {
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
- * Create one compiled program over a qualifier and rater.
1805
+ * Creates one compiled {@link ProgramInterface} over a qualifier and rater.
1585
1806
  *
1586
1807
  * @remarks
1587
- * Validates the definition at construction when `options.validate` is left at
1588
- * its {@link DEFAULT_PROGRAM_VALIDATE} default. A standalone program creates and
1589
- * OWNS one shared quantitative-plus-logical reason engine and injects it into the
1590
- * qualifier and rater it creates; injected dependencies remain caller-owned.
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, programDefinition } from '@orkestrel/program'
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
- * Create one ordered manager over compiled programs.
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.STATUS_PRECEDENCE = STATUS_PRECEDENCE;
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.buildLimits = buildLimits;
1649
- exports.buildNotices = buildNotices;
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.tallyProgram = tallyProgram;
1910
+ exports.tallySubject = tallySubject;
1684
1911
  exports.validateProgramDefinition = validateProgramDefinition;
1685
1912
 
1686
1913
  //# sourceMappingURL=index.cjs.map