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