@orkestrel/program 0.0.10 → 0.0.12

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,33 +1,33 @@
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
+ /** Names the default definition validation policy for `createProgram` / `ProgramManager.add`. */
8
8
  var DEFAULT_PROGRAM_VALIDATE = true;
9
- /** Status tally precedence orderleast to most resolved. */
10
- var STATUS_PRECEDENCE = Object.freeze([
9
+ /** Lists every {@link Status} literal the source the union and its guard derive from. */
10
+ var STATUSES = Object.freeze([
11
11
  "ineligible",
12
12
  "referral",
13
13
  "conditional",
14
14
  "unrated",
15
15
  "eligible"
16
16
  ]);
17
- /** The deterministic authority decision for each global eligibility. */
17
+ /** Maps each global eligibility to its deterministic authority decision. */
18
18
  var ELIGIBILITY_DECISIONS = Object.freeze({
19
19
  eligible: "approved",
20
20
  ineligible: "denied",
21
21
  referral: "submitted"
22
22
  });
23
- /** The reserved working-subject key a batch's aggregate projection is written under. */
23
+ /** Names the reserved working-subject key a batch's aggregate projection is written under. */
24
24
  var AGGREGATE_KEY = "aggregate";
25
- /** The reserved working-subject key the authority's outcome projection is written under. */
25
+ /** Names the reserved working-subject key the authority's outcome projection is written under. */
26
26
  var OUTCOME_KEY = "outcome";
27
27
  //#endregion
28
28
  //#region src/core/errors.ts
29
29
  /**
30
- * A coded programmer error thrown by the program layer.
30
+ * Reports a coded programmer error thrown by the program layer.
31
31
  *
32
32
  * @remarks
33
33
  * `DUPLICATE` — a program id collision on `ProgramManager.add`, or a duplicate
@@ -37,28 +37,57 @@ var OUTCOME_KEY = "outcome";
37
37
  * policy failed validation. `MISMATCH` — an injected entity or a returned
38
38
  * reason result has the wrong contract. `RESERVED` — a subject already
39
39
  * carries `aggregate` or `outcome`. `DESTROYED` — use of a destroyed entity.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * import { ProgramError } from '@orkestrel/program'
44
+ *
45
+ * const error = new ProgramError('RESERVED', 'Subject carries a reserved key', 'aggregate')
46
+ * error.code // 'RESERVED'
47
+ * ```
40
48
  */
41
49
  var ProgramError = class extends Error {
42
50
  code;
43
51
  context;
44
- constructor(code, message, context) {
45
- super(message);
52
+ /**
53
+ * Creates a coded program error.
54
+ *
55
+ * @param code - The machine-readable failure category
56
+ * @param message - The human-readable failure description
57
+ * @param context - Optional structured context for the failure
58
+ * @param cause - Optional underlying value the failure wraps
59
+ */
60
+ constructor(code, message, context, cause) {
61
+ super(message, cause === void 0 ? void 0 : { cause });
46
62
  this.name = "ProgramError";
47
63
  this.code = code;
48
64
  this.context = context;
49
65
  }
50
66
  };
51
- /** Narrow a caught value to a {@link ProgramError}. */
67
+ /**
68
+ * Determines whether a caught value is a {@link ProgramError}.
69
+ *
70
+ * @param value - The candidate value
71
+ * @returns True if the value is a {@link ProgramError}; false otherwise
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * import { isProgramError, ProgramError } from '@orkestrel/program'
76
+ *
77
+ * isProgramError(new ProgramError('RESERVED', 'Subject carries a reserved key')) // true
78
+ * isProgramError(new Error('Subject carries a reserved key')) // false
79
+ * ```
80
+ */
52
81
  function isProgramError(value) {
53
82
  return value instanceof ProgramError;
54
83
  }
55
84
  //#endregion
56
85
  //#region src/core/validators.ts
57
86
  /**
58
- * Determine whether a value is a {@link Decision} literal.
87
+ * Determines whether a value is a {@link Decision} literal.
59
88
  *
60
89
  * @param value - The candidate value
61
- * @returns `true` when `value` is a {@link Decision}
90
+ * @returns True if `value` is a {@link Decision}; false otherwise
62
91
  *
63
92
  * @example
64
93
  * ```ts
@@ -69,10 +98,10 @@ function isProgramError(value) {
69
98
  */
70
99
  var isDecision = literalOf("approved", "denied", "submitted");
71
100
  /**
72
- * Determine whether a value is a {@link Status} literal.
101
+ * Determines whether a value is a {@link Status} literal.
73
102
  *
74
103
  * @param value - The candidate value
75
- * @returns `true` when `value` is a {@link Status}
104
+ * @returns True if `value` is a {@link Status}; false otherwise
76
105
  *
77
106
  * @example
78
107
  * ```ts
@@ -81,12 +110,12 @@ var isDecision = literalOf("approved", "denied", "submitted");
81
110
  * isStatus('eligible') // true
82
111
  * ```
83
112
  */
84
- var isStatus = literalOf("ineligible", "referral", "conditional", "unrated", "eligible");
113
+ var isStatus = literalOf(STATUSES);
85
114
  /**
86
- * Determine whether a value is a {@link ProgramEffect} literal.
115
+ * Determines whether a value is a {@link ProgramEffect} literal.
87
116
  *
88
117
  * @param value - The candidate value
89
- * @returns `true` when `value` is a {@link ProgramEffect}
118
+ * @returns True if `value` is a {@link ProgramEffect}; false otherwise
90
119
  *
91
120
  * @example
92
121
  * ```ts
@@ -97,10 +126,10 @@ var isStatus = literalOf("ineligible", "referral", "conditional", "unrated", "el
97
126
  */
98
127
  var isProgramEffect = literalOf("notice", "limit");
99
128
  /**
100
- * Determine whether a value is an exact {@link Notice} record.
129
+ * Determines whether a value is an exact {@link Notice} record.
101
130
  *
102
131
  * @param value - The candidate value
103
- * @returns `true` when `value` is a {@link Notice}
132
+ * @returns True if `value` is a {@link Notice}; false otherwise
104
133
  *
105
134
  * @example
106
135
  * ```ts
@@ -117,10 +146,10 @@ function isNotice(value) {
117
146
  }, ["scope"])(value);
118
147
  }
119
148
  /**
120
- * Determine whether a value is an exact {@link AggregateDefinition} record.
149
+ * Determines whether a value is an exact {@link AggregateDefinition} record.
121
150
  *
122
151
  * @param value - The candidate value
123
- * @returns `true` when `value` is an {@link AggregateDefinition}
152
+ * @returns True if `value` is an {@link AggregateDefinition}; false otherwise
124
153
  *
125
154
  * @example
126
155
  * ```ts
@@ -132,19 +161,19 @@ function isNotice(value) {
132
161
  function isAggregateDefinition(value) {
133
162
  return recordOf({
134
163
  fields: arrayOf(isFieldPath),
135
- by: isFieldPath,
164
+ partition: isFieldPath,
136
165
  gates: isLogicalDefinition
137
- }, ["by", "gates"])(value);
166
+ }, ["partition", "gates"])(value);
138
167
  }
139
168
  /**
140
- * Determine whether a value is an exact {@link ProgramDefinition} record.
169
+ * Determines whether a value is an exact {@link ProgramDefinition} record.
141
170
  *
142
171
  * @remarks
143
172
  * `rating` is optional — an omitted `rating` authors an eligibility-only
144
173
  * program (see {@link ProgramDefinition}).
145
174
  *
146
175
  * @param value - The candidate value
147
- * @returns `true` when `value` is a {@link ProgramDefinition}
176
+ * @returns True if `value` is a {@link ProgramDefinition}; false otherwise
148
177
  *
149
178
  * @example
150
179
  * ```ts
@@ -174,7 +203,7 @@ function isProgramDefinition(value) {
174
203
  ])(value);
175
204
  }
176
205
  /**
177
- * Determine whether a value is an open program sums record.
206
+ * Determines whether a value is an open program sums record.
178
207
  *
179
208
  * @remarks
180
209
  * Every own string-named property is checked, including non-enumerable
@@ -183,7 +212,7 @@ function isProgramDefinition(value) {
183
212
  * infinities, because the published contract does not refine them.
184
213
  *
185
214
  * @param value - The candidate value
186
- * @returns `true` when every own string-named value is a number
215
+ * @returns True if every own string-named value is a number; false otherwise
187
216
  *
188
217
  * @example
189
218
  * ```ts
@@ -196,14 +225,14 @@ function isProgramSums(value) {
196
225
  return whereOf(objectOf({}), (record) => Object.getOwnPropertyNames(record).every((key) => isNumber(Reflect.get(record, key))))(value);
197
226
  }
198
227
  /**
199
- * Determine whether a value is an open result-side {@link Determination}.
228
+ * Determines whether a value is an open result-side {@link Determination}.
200
229
  *
201
230
  * @remarks
202
231
  * Unknown members and class instances are admitted. Arrays are refused.
203
232
  * Optional `scope` and `message` members may be absent or `undefined`.
204
233
  *
205
234
  * @param value - The candidate value
206
- * @returns `true` when every published determination member conforms
235
+ * @returns True if every published determination member conforms; false otherwise
207
236
  *
208
237
  * @example
209
238
  * ```ts
@@ -221,13 +250,13 @@ var isDetermination = objectOf({
221
250
  premises: arrayOf(isPremise)
222
251
  }, ["scope", "message"]);
223
252
  /**
224
- * Determine whether a value is an open result-side {@link AggregateGroup}.
253
+ * Determines whether a value is an open result-side {@link AggregateGroup}.
225
254
  *
226
255
  * @remarks
227
256
  * Unknown members and class instances are admitted. Arrays are refused.
228
257
  *
229
258
  * @param value - The candidate value
230
- * @returns `true` when every published aggregate-group member conforms
259
+ * @returns True if every published aggregate-group member conforms; false otherwise
231
260
  *
232
261
  * @example
233
262
  * ```ts
@@ -242,13 +271,13 @@ var isAggregateGroup = objectOf({
242
271
  sums: isProgramSums
243
272
  });
244
273
  /**
245
- * Determine whether a value is an open result-side {@link Tally}.
274
+ * Determines whether a value is an open result-side {@link Tally}.
246
275
  *
247
276
  * @remarks
248
277
  * Unknown members and class instances are admitted. Arrays are refused.
249
278
  *
250
279
  * @param value - The candidate value
251
- * @returns `true` when every published tally member conforms
280
+ * @returns True if every published tally member conforms; false otherwise
252
281
  *
253
282
  * @example
254
283
  * ```ts
@@ -262,27 +291,27 @@ var isTally = objectOf({
262
291
  sums: isProgramSums
263
292
  });
264
293
  /**
265
- * Determine whether a value is a total open status-tally record.
294
+ * Determines whether a value is a total open status-tally record.
266
295
  *
267
296
  * @remarks
268
- * Every {@link Status} in {@link STATUS_PRECEDENCE} is required and checked.
297
+ * Every {@link Status} in {@link STATUSES} is required and checked.
269
298
  * Unknown members and class instances are admitted. Arrays are refused.
270
299
  *
271
300
  * @param value - The candidate value
272
- * @returns `true` when every required status member is a {@link Tally}
301
+ * @returns True if every required status member is a {@link Tally}; false otherwise
273
302
  *
274
303
  * @example
275
304
  * ```ts
276
- * import { emptyTallies, isTallies } from '@orkestrel/program'
305
+ * import { buildEmptyTallies, isTallies } from '@orkestrel/program'
277
306
  *
278
- * isTallies(emptyTallies([])) // true
307
+ * isTallies(buildEmptyTallies([])) // true
279
308
  * ```
280
309
  */
281
310
  function isTallies(value) {
282
- return whereOf(objectOf({}), (record) => STATUS_PRECEDENCE.every((status) => isTally(Reflect.get(record, status))))(value);
311
+ return whereOf(objectOf({}), (record) => STATUSES.every((status) => isTally(Reflect.get(record, status))))(value);
283
312
  }
284
313
  /**
285
- * Determine whether a value is an open {@link ProgramResult}.
314
+ * Determines whether a value is an open {@link ProgramResult}.
286
315
  *
287
316
  * @remarks
288
317
  * This guard is result-postured for values returned through a borrowed
@@ -291,7 +320,7 @@ function isTallies(value) {
291
320
  * over their complete nested result closures. Arrays are refused.
292
321
  *
293
322
  * @param value - The candidate value
294
- * @returns `true` when every published program-result member conforms
323
+ * @returns True if every published program-result member conforms; false otherwise
295
324
  *
296
325
  * @example
297
326
  * ```ts
@@ -314,7 +343,7 @@ var isProgramResult = objectOf({
314
343
  errors: arrayOf(isString)
315
344
  }, ["decision", "rating"]);
316
345
  /**
317
- * Determine whether a value is an open {@link AggregateResult}.
346
+ * Determines whether a value is an open {@link AggregateResult}.
318
347
  *
319
348
  * @remarks
320
349
  * This guard is result-postured for values returned through a borrowed
@@ -323,7 +352,7 @@ var isProgramResult = objectOf({
323
352
  * record, and sums record. Arrays are refused.
324
353
  *
325
354
  * @param value - The candidate value
326
- * @returns `true` when every published aggregate-result member conforms
355
+ * @returns True if every published aggregate-result member conforms; false otherwise
327
356
  *
328
357
  * @example
329
358
  * ```ts
@@ -346,16 +375,16 @@ var isAggregateResult = objectOf({
346
375
  errors: arrayOf(isString)
347
376
  });
348
377
  /**
349
- * Determine whether a value is an open {@link ProgramValidationResult}.
378
+ * Determines whether a value is an open {@link ProgramValidationResult}.
350
379
  *
351
380
  * @remarks
352
381
  * `ProgramValidationResult` is this package's own declared interface, not an
353
- * alias of reason's validation result. This guard therefore checks the three
382
+ * alias of reason's validation result. This guard therefore checks the
354
383
  * program-owned members directly so the contracts may evolve independently.
355
384
  * Unknown members and class instances are admitted. Arrays are refused.
356
385
  *
357
386
  * @param value - The candidate value
358
- * @returns `true` when every published program-validation member conforms
387
+ * @returns True if every published program-validation member conforms; false otherwise
359
388
  *
360
389
  * @example
361
390
  * ```ts
@@ -372,38 +401,7 @@ var isProgramValidationResult = objectOf({
372
401
  //#endregion
373
402
  //#region src/core/helpers.ts
374
403
  /**
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.
404
+ * Determines whether a caller subject already carries a reserved program key.
407
405
  *
408
406
  * @remarks
409
407
  * `aggregate` and `outcome` are program-private working-subject namespaces — the
@@ -412,7 +410,7 @@ function copyJSONValue(value) {
412
410
  * collide with a projection, so it is rejected before qualification.
413
411
  *
414
412
  * @param subject - The caller subject to check
415
- * @returns `true` when the subject owns `aggregate` or `outcome`
413
+ * @returns True if the subject owns `aggregate` or `outcome`; false otherwise
416
414
  *
417
415
  * @example
418
416
  * ```ts
@@ -426,11 +424,12 @@ function hasReservedKey(subject) {
426
424
  return Object.hasOwn(subject, "aggregate") || Object.hasOwn(subject, "outcome");
427
425
  }
428
426
  /**
429
- * Assert a value is a valid program {@link Subject}, narrowing it in place.
427
+ * Asserts a value is a valid program {@link Subject}, narrowing it in place.
430
428
  *
431
429
  * @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
430
+ * @throws {@link ProgramError} Thrown when the value is not a record (`'MISMATCH'`).
431
+ * @throws {@link ProgramError} Thrown when the value already carries the `aggregate`
432
+ * or `outcome` key (`'RESERVED'`).
434
433
  *
435
434
  * @example
436
435
  * ```ts
@@ -447,7 +446,7 @@ function assertProgramSubject(subject) {
447
446
  }
448
447
  }
449
448
  /**
450
- * Select the rating lines a subject may be rated on from scoped eligibility.
449
+ * Selects the rating lines a subject may be rated on from scoped eligibility.
451
450
  *
452
451
  * @remarks
453
452
  * A scope names a rating-line id. A line survives when its scope is absent
@@ -474,11 +473,11 @@ function selectProgramLines(lines, scopes) {
474
473
  });
475
474
  }
476
475
  /**
477
- * Derive the final program {@link Status} from a definition's rating policy and
476
+ * Derives the final program {@link Status} from a definition's rating policy and
478
477
  * qualification/rating evidence.
479
478
  *
480
479
  * @remarks
481
- * Explicit policy, not an opaque precedence reduce (AGENTS §10): global
480
+ * Explicit policy, not an opaque precedence reduce: global
482
481
  * ineligibility or referral is terminal; a scoped referral yields `referral`;
483
482
  * an applied `condition` or an applied scoped `restriction` (a line was
484
483
  * removed but others rated) is `conditional`. When the definition OMITS
@@ -508,7 +507,7 @@ function deriveStatus(definition, qualification, rating) {
508
507
  return conditional ? "conditional" : "eligible";
509
508
  }
510
509
  /**
511
- * Map a global {@link Eligibility} to its deterministic authority {@link Decision}.
510
+ * Maps a global {@link Eligibility} to its deterministic authority {@link Decision}.
512
511
  *
513
512
  * @param eligibility - The global eligibility
514
513
  * @returns The matching decision
@@ -525,8 +524,8 @@ function decideEligibility(eligibility) {
525
524
  return ELIGIBILITY_DECISIONS[eligibility];
526
525
  }
527
526
  /**
528
- * Resolve authored {@link Notice}s into unconditionally-applied `notice`
529
- * {@link Determination}s.
527
+ * Resolves authored {@link Notice} values into unconditionally-applied `notice`
528
+ * {@link Determination} values.
530
529
  *
531
530
  * @remarks
532
531
  * Notices are program output only — they never affect eligibility, status, line
@@ -539,12 +538,12 @@ function decideEligibility(eligibility) {
539
538
  *
540
539
  * @example
541
540
  * ```ts
542
- * import { buildNotices } from '@orkestrel/program'
541
+ * import { buildNoticeDeterminations } from '@orkestrel/program'
543
542
  *
544
- * buildNotices([{ id: 'min', message: 'Minimum applies' }], { id: 'r1' })
543
+ * buildNoticeDeterminations([{ id: 'min', message: 'Minimum applies' }], { id: 'r1' })
545
544
  * ```
546
545
  */
547
- function buildNotices(notices, subject) {
546
+ function buildNoticeDeterminations(notices, subject) {
548
547
  return notices.map((notice) => ({
549
548
  id: notice.id,
550
549
  effect: "notice",
@@ -555,14 +554,14 @@ function buildNotices(notices, subject) {
555
554
  }));
556
555
  }
557
556
  /**
558
- * Convert a logical result's applied rules into `limit` {@link Determination}s.
557
+ * Converts a logical result's applied rules into `limit` {@link Determination} values.
559
558
  *
560
559
  * @remarks
561
560
  * 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
561
+ * are plain {@link LogicalDefinition} definitions with no program-authored ruling map, so a
563
562
  * fired rule's own `description` (from `@orkestrel/reason`) is the message
564
563
  * template, interpolated against the working record the definition ran against.
565
- * Rich premises reuse the qualifier's {@link logicalPremises}. A rule that never
564
+ * Rich premises reuse the qualifier's {@link ruleToPremises}. A rule that never
566
565
  * fires produces no determination — program has no authored ruling map to keep
567
566
  * evidence for.
568
567
  *
@@ -575,12 +574,12 @@ function buildNotices(notices, subject) {
575
574
  *
576
575
  * @example
577
576
  * ```ts
578
- * import { buildLimits } from '@orkestrel/program'
577
+ * import { buildLimitDeterminations } from '@orkestrel/program'
579
578
  *
580
- * buildLimits(authority, resolved, outcome, evaluator)
579
+ * buildLimitDeterminations(authority, resolved, outcome, evaluator)
581
580
  * ```
582
581
  */
583
- function buildLimits(definition, result, working, evaluator, labels) {
582
+ function buildLimitDeterminations(definition, result, working, evaluator, labels) {
584
583
  const output = [];
585
584
  for (const entry of result.rules) {
586
585
  if (!entry.applied) continue;
@@ -591,13 +590,13 @@ function buildLimits(definition, result, working, evaluator, labels) {
591
590
  effect: "limit",
592
591
  applied: true,
593
592
  ...rule.description === void 0 ? {} : { message: interpolateMessage(rule.description, working) },
594
- premises: logicalPremises(rule, working, evaluator, labels)
593
+ premises: ruleToPremises(rule, working, evaluator, labels)
595
594
  });
596
595
  }
597
596
  return output;
598
597
  }
599
598
  /**
600
- * Build the private authority outcome projection from an assembled program result.
599
+ * Builds the private authority outcome projection from an assembled program result.
601
600
  *
602
601
  * @remarks
603
602
  * The authority reads this record under {@link OUTCOME_KEY}; it never receives
@@ -626,7 +625,7 @@ function buildOutcomeProjection(result) {
626
625
  };
627
626
  }
628
627
  /**
629
- * Assemble a {@link ProgramResult} from its qualification, rating, and
628
+ * Assembles a {@link ProgramResult} from its qualification, rating, and
630
629
  * determination parts — before or after authority.
631
630
  *
632
631
  * @remarks
@@ -687,7 +686,7 @@ function buildProgramResult(definition, qualification, rating, determinations, s
687
686
  };
688
687
  }
689
688
  /**
690
- * Add optional aggregate context to a private subject copy for qualification.
689
+ * Adds optional aggregate context to a private subject copy for qualification.
691
690
  *
692
691
  * @remarks
693
692
  * The original subject is returned unchanged when no aggregate context exists.
@@ -722,7 +721,7 @@ function buildQualificationSubject(subject, aggregate) {
722
721
  };
723
722
  }
724
723
  /**
725
- * Return authored scopes (qualification ruling scopes or notice scopes) that
724
+ * Returns authored scopes (qualification ruling scopes or notice scopes) that
726
725
  * name no rating line on the program.
727
726
  *
728
727
  * @remarks
@@ -749,7 +748,7 @@ function findMissingScopes(definition) {
749
748
  return [...missing];
750
749
  }
751
750
  /**
752
- * Assert a program definition's always-on construction invariants — missing
751
+ * Asserts a program definition's always-on construction invariants — missing
753
752
  * scope references and duplicate rating-line or notice ids.
754
753
  *
755
754
  * @remarks
@@ -758,10 +757,10 @@ function findMissingScopes(definition) {
758
757
  * an authoring mistake this severe cannot silently compile.
759
758
  *
760
759
  * @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
760
+ * @throws {@link ProgramError} Thrown when a ruling or notice scope names no
761
+ * rating line (`'MISSING'`).
762
+ * @throws {@link ProgramError} Thrown when two rating lines or two notices share
763
+ * an id (`'DUPLICATE'`).
765
764
  *
766
765
  * @example
767
766
  * ```ts
@@ -779,7 +778,7 @@ function assertProgramDefinition(definition) {
779
778
  if (duplicateNotices.length > 0) throw new ProgramError("DUPLICATE", `Duplicate notice id: ${duplicateNotices.join(", ")}`, definition.id);
780
779
  }
781
780
  /**
782
- * Validate a program definition's shape, references, and nested definitions.
781
+ * Validates a program definition's shape, references, and nested definitions.
783
782
  *
784
783
  * @remarks
785
784
  * The single semantic-validation implementation used by `Program.validate`. It
@@ -812,7 +811,7 @@ function validateProgramDefinition(definition, qualifier, engine) {
812
811
  if (definition.id.length === 0) errors.push("Program id must not be empty");
813
812
  if (definition.name.length === 0) errors.push("Program name must not be empty");
814
813
  const qualification = qualifier.validate(definition.qualification);
815
- if (isQualificationValidationResult(qualification)) {
814
+ if (isReasonValidationResult(qualification)) {
816
815
  errors.push(...qualification.errors.map((error) => `qualification: ${error}`));
817
816
  warnings.push(...qualification.warnings.map((warning) => `qualification: ${warning}`));
818
817
  } else errors.push("qualification: Qualifier returned invalid validation result");
@@ -842,7 +841,7 @@ function validateProgramDefinition(definition, qualifier, engine) {
842
841
  if (fields.has(key)) errors.push(`Duplicate aggregate field "${key}"`);
843
842
  fields.add(key);
844
843
  }
845
- if (aggregate.by !== void 0 && formatField(aggregate.by).length === 0) errors.push("Aggregate partition field must be non-empty");
844
+ if (aggregate.partition !== void 0 && formatField(aggregate.partition).length === 0) errors.push("Aggregate partition field must be non-empty");
846
845
  if (aggregate.gates !== void 0) {
847
846
  const validation = engine.validate(aggregate.gates);
848
847
  if (isReasonValidationResult(validation)) {
@@ -860,7 +859,7 @@ function validateProgramDefinition(definition, qualifier, engine) {
860
859
  };
861
860
  }
862
861
  /**
863
- * Coerce a subject's partition-key field to its group-key string.
862
+ * Coerces a subject's partition-key field to its group-key string.
864
863
  *
865
864
  * @remarks
866
865
  * The key is the resolved field coerced with `String` — `undefined` collapses
@@ -869,7 +868,7 @@ function validateProgramDefinition(definition, qualifier, engine) {
869
868
  * collides with the string `'1'`.
870
869
  *
871
870
  * @param subject - The subject to key
872
- * @param by - The partition key field
871
+ * @param partition - The field the batch partitions on
873
872
  * @returns The subject's group key
874
873
  *
875
874
  * @example
@@ -879,11 +878,11 @@ function validateProgramDefinition(definition, qualifier, engine) {
879
878
  * formatGroupKey({ location: 'east' }, 'location') // 'east'
880
879
  * ```
881
880
  */
882
- function formatGroupKey(subject, by) {
883
- return String(resolveField(subject, by) ?? "");
881
+ function formatGroupKey(subject, partition) {
882
+ return String(resolveField(subject, partition) ?? "");
884
883
  }
885
884
  /**
886
- * Fold one subject's finite aggregate field values into a sums record.
885
+ * Folds one subject's finite aggregate field values into a sums record.
887
886
  *
888
887
  * @remarks
889
888
  * Returns a FRESH record — `sums` is never mutated. Only finite numbers
@@ -913,7 +912,7 @@ function sumFields(sums, subject, fields) {
913
912
  return next;
914
913
  }
915
914
  /**
916
- * Sum aggregate fields across a batch of subjects.
915
+ * Sums aggregate fields across a batch of subjects.
917
916
  *
918
917
  * @remarks
919
918
  * A {@link FieldPath} may be nested — a nested path sums a nested subject field
@@ -933,12 +932,12 @@ function sumFields(sums, subject, fields) {
933
932
  * ```
934
933
  */
935
934
  function aggregateSums(subjects, fields) {
936
- let sums = emptySums(fields);
935
+ let sums = buildEmptySums(fields);
937
936
  for (const subject of subjects) sums = sumFields(sums, subject, fields);
938
937
  return sums;
939
938
  }
940
939
  /**
941
- * Partition a batch of subjects by a field, summing aggregate fields per key.
940
+ * Partitions a batch of subjects by a field, summing aggregate fields per key.
942
941
  *
943
942
  * @remarks
944
943
  * The partition key is derived by {@link formatGroupKey}. Group order follows
@@ -946,8 +945,8 @@ function aggregateSums(subjects, fields) {
946
945
  *
947
946
  * @param subjects - The batch of subjects
948
947
  * @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
948
+ * @param partition - The field the batch partitions on; no partition is built when absent
949
+ * @returns A fresh list of aggregate groups, or an empty list when `partition` is absent
951
950
  *
952
951
  * @example
953
952
  * ```ts
@@ -956,11 +955,11 @@ function aggregateSums(subjects, fields) {
956
955
  * aggregateGroups([{ location: 'east', amount: 5 }], ['amount'], 'location')
957
956
  * ```
958
957
  */
959
- function aggregateGroups(subjects, fields, by) {
960
- if (by === void 0) return [];
958
+ function aggregateGroups(subjects, fields, partition) {
959
+ if (partition === void 0) return [];
961
960
  const records = /* @__PURE__ */ new Map();
962
961
  for (const subject of subjects) {
963
- const key = formatGroupKey(subject, by);
962
+ const key = formatGroupKey(subject, partition);
964
963
  const group = records.get(key);
965
964
  if (group === void 0) records.set(key, [subject]);
966
965
  else group.push(subject);
@@ -972,7 +971,7 @@ function aggregateGroups(subjects, fields, by) {
972
971
  }));
973
972
  }
974
973
  /**
975
- * Build one subject's overall and optional group aggregate projection.
974
+ * Builds one subject's overall and optional group aggregate projection.
976
975
  *
977
976
  * @remarks
978
977
  * The projection carries the whole-batch `count` and `sums` plus the subject's
@@ -983,7 +982,7 @@ function aggregateGroups(subjects, fields, by) {
983
982
  * @param count - The whole-batch subject count
984
983
  * @param sums - The whole-batch summed aggregate fields
985
984
  * @param groups - The batch partitions
986
- * @param by - The partition key field; no group is attached when absent
985
+ * @param partition - The field the batch partitions on; no group is attached when absent
987
986
  * @returns A fresh aggregate projection
988
987
  *
989
988
  * @example
@@ -993,8 +992,8 @@ function aggregateGroups(subjects, fields, by) {
993
992
  * buildAggregateProjection(subject, 2, { amount: 8 }, groups, 'location')
994
993
  * ```
995
994
  */
996
- function buildAggregateProjection(subject, count, sums, groups, by) {
997
- const group = by === void 0 ? void 0 : groups.find((entry) => entry.key === formatGroupKey(subject, by));
995
+ function buildAggregateProjection(subject, count, sums, groups, partition) {
996
+ const group = partition === void 0 ? void 0 : groups.find((entry) => entry.key === formatGroupKey(subject, partition));
998
997
  return {
999
998
  count,
1000
999
  sums: { ...sums },
@@ -1002,7 +1001,7 @@ function buildAggregateProjection(subject, count, sums, groups, by) {
1002
1001
  };
1003
1002
  }
1004
1003
  /**
1005
- * Build the reserved-key record a batch aggregate-gate definition runs against.
1004
+ * Builds the reserved-key record a batch aggregate-gate definition runs against.
1006
1005
  *
1007
1006
  * @remarks
1008
1007
  * Unlike a per-subject {@link buildAggregateProjection}, the batch record carries
@@ -1029,29 +1028,29 @@ function buildAggregateRecord(count, sums, groups) {
1029
1028
  } };
1030
1029
  }
1031
1030
  /**
1032
- * Build a zero-sum record for a set of aggregate fields.
1031
+ * Builds a zero-sum record for a set of aggregate fields.
1033
1032
  *
1034
1033
  * @param fields - The fields to zero
1035
1034
  * @returns A fresh record of dot-joined field to `0`
1036
1035
  *
1037
1036
  * @example
1038
1037
  * ```ts
1039
- * import { emptySums } from '@orkestrel/program'
1038
+ * import { buildEmptySums } from '@orkestrel/program'
1040
1039
  *
1041
- * emptySums(['amount']) // { amount: 0 }
1040
+ * buildEmptySums(['amount']) // { amount: 0 }
1042
1041
  * ```
1043
1042
  */
1044
- function emptySums(fields) {
1043
+ function buildEmptySums(fields) {
1045
1044
  const sums = {};
1046
1045
  for (const field of fields) sums[formatField(field)] = 0;
1047
1046
  return sums;
1048
1047
  }
1049
1048
  /**
1050
- * Complete a partial status tally record with zero entries for every missing
1049
+ * Completes a partial status tally record with zero entries for every missing
1051
1050
  * {@link Status}.
1052
1051
  *
1053
1052
  * @param entries - The partial tally entries to complete
1054
- * @returns A record with all five statuses present
1053
+ * @returns A record carrying every {@link Status}
1055
1054
  *
1056
1055
  * @example
1057
1056
  * ```ts
@@ -1085,28 +1084,28 @@ function completeTallies(entries) {
1085
1084
  };
1086
1085
  }
1087
1086
  /**
1088
- * Build complete zero status tallies in {@link STATUS_PRECEDENCE} order.
1087
+ * Builds complete zero status tallies in {@link STATUSES} order.
1089
1088
  *
1090
1089
  * @param fields - The fields each tally's sums are zeroed for
1091
1090
  * @returns A fresh, complete tally record
1092
1091
  *
1093
1092
  * @example
1094
1093
  * ```ts
1095
- * import { emptyTallies } from '@orkestrel/program'
1094
+ * import { buildEmptyTallies } from '@orkestrel/program'
1096
1095
  *
1097
- * emptyTallies(['amount'])
1096
+ * buildEmptyTallies(['amount'])
1098
1097
  * ```
1099
1098
  */
1100
- function emptyTallies(fields) {
1099
+ function buildEmptyTallies(fields) {
1101
1100
  const entries = {};
1102
- for (const status of STATUS_PRECEDENCE) entries[status] = {
1101
+ for (const status of STATUSES) entries[status] = {
1103
1102
  count: 0,
1104
- sums: emptySums(fields)
1103
+ sums: buildEmptySums(fields)
1105
1104
  };
1106
1105
  return completeTallies(entries);
1107
1106
  }
1108
1107
  /**
1109
- * Add one subject's aggregate contribution to a status tally record.
1108
+ * Adds one subject's aggregate contribution to a status tally record.
1110
1109
  *
1111
1110
  * @param tallies - The tallies to update
1112
1111
  * @param result - The subject's program result (its `status` selects the tally)
@@ -1116,12 +1115,12 @@ function emptyTallies(fields) {
1116
1115
  *
1117
1116
  * @example
1118
1117
  * ```ts
1119
- * import { tallyProgram } from '@orkestrel/program'
1118
+ * import { tallySubject } from '@orkestrel/program'
1120
1119
  *
1121
- * tallyProgram(tallies, result, { id: 'r1', amount: 5 }, ['amount'])
1120
+ * tallySubject(tallies, result, { id: 'r1', amount: 5 }, ['amount'])
1122
1121
  * ```
1123
1122
  */
1124
- function tallyProgram(tallies, result, subject, fields) {
1123
+ function tallySubject(tallies, result, subject, fields) {
1125
1124
  const status = result.status;
1126
1125
  const current = tallies[status];
1127
1126
  const sums = sumFields(current.sums, subject, fields);
@@ -1134,7 +1133,7 @@ function tallyProgram(tallies, result, subject, fields) {
1134
1133
  });
1135
1134
  }
1136
1135
  /**
1137
- * Assemble one batch {@link AggregateResult} from its per-subject and aggregate
1136
+ * Assembles one batch {@link AggregateResult} from its per-subject and aggregate
1138
1137
  * parts.
1139
1138
  *
1140
1139
  * @remarks
@@ -1180,11 +1179,14 @@ function buildAggregateResult(definition, subjects, determinations, groups, tall
1180
1179
  };
1181
1180
  }
1182
1181
  /**
1183
- * Build a {@link ProgramDefinition}.
1182
+ * Builds a fresh {@link ProgramDefinition}.
1184
1183
  *
1185
1184
  * @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.
1185
+ * Omits absent optional keys. `metadata` is deep-copied with `structuredClone`.
1186
+ * `notices` is copied as a fresh array whose elements are shared with the
1187
+ * input. `qualification`, `rating`, `authority`, and `aggregate` are stored
1188
+ * by reference. The {@link Program} constructor later snapshots and seals
1189
+ * the whole graph.
1188
1190
  *
1189
1191
  * @param id - The program id
1190
1192
  * @param name - The display name
@@ -1195,12 +1197,12 @@ function buildAggregateResult(definition, subjects, determinations, groups, tall
1195
1197
  *
1196
1198
  * @example
1197
1199
  * ```ts
1198
- * import { programDefinition } from '@orkestrel/program'
1200
+ * import { buildProgramDefinition } from '@orkestrel/program'
1199
1201
  *
1200
- * programDefinition('standard', 'Standard', qualification, rating, { notices: [notice] })
1202
+ * buildProgramDefinition('standard', 'Standard', qualification, rating, { notices: [notice] })
1201
1203
  * ```
1202
1204
  */
1203
- function programDefinition(id, name, qualification, rating, input) {
1205
+ function buildProgramDefinition(id, name, qualification, rating, input) {
1204
1206
  return {
1205
1207
  id,
1206
1208
  name,
@@ -1210,25 +1212,25 @@ function programDefinition(id, name, qualification, rating, input) {
1210
1212
  ...input?.notices === void 0 ? {} : { notices: [...input.notices] },
1211
1213
  ...input?.authority === void 0 ? {} : { authority: input.authority },
1212
1214
  ...input?.aggregate === void 0 ? {} : { aggregate: input.aggregate },
1213
- ...input?.metadata === void 0 ? {} : { metadata: copyJSONValue(input.metadata) }
1215
+ ...input?.metadata === void 0 ? {} : { metadata: structuredClone(input.metadata) }
1214
1216
  };
1215
1217
  }
1216
1218
  /**
1217
- * Build a {@link Notice}.
1219
+ * Builds a fresh {@link Notice}.
1218
1220
  *
1219
1221
  * @param id - The notice id
1220
- * @param message - The message template, carrying optional `{{token}}`s
1222
+ * @param message - The message template, carrying optional `{{token}}` placeholders
1221
1223
  * @param input - Optional presentation scope
1222
1224
  * @returns A fresh notice
1223
1225
  *
1224
1226
  * @example
1225
1227
  * ```ts
1226
- * import { noticeDefinition } from '@orkestrel/program'
1228
+ * import { buildNotice } from '@orkestrel/program'
1227
1229
  *
1228
- * noticeDefinition('minimum', 'Minimum earned premium applies')
1230
+ * buildNotice('minimum', 'Minimum earned premium applies')
1229
1231
  * ```
1230
1232
  */
1231
- function noticeDefinition(id, message, input) {
1233
+ function buildNotice(id, message, input) {
1232
1234
  return {
1233
1235
  id,
1234
1236
  message,
@@ -1236,7 +1238,7 @@ function noticeDefinition(id, message, input) {
1236
1238
  };
1237
1239
  }
1238
1240
  /**
1239
- * Build an {@link AggregateDefinition}.
1241
+ * Builds a fresh {@link AggregateDefinition}.
1240
1242
  *
1241
1243
  * @param fields - The aggregate fields to sum across a batch
1242
1244
  * @param input - Optional partition field and aggregate gates
@@ -1244,23 +1246,23 @@ function noticeDefinition(id, message, input) {
1244
1246
  *
1245
1247
  * @example
1246
1248
  * ```ts
1247
- * import { aggregateDefinition } from '@orkestrel/program'
1249
+ * import { buildAggregateDefinition } from '@orkestrel/program'
1248
1250
  *
1249
- * aggregateDefinition(['amount'], { by: 'location' })
1251
+ * buildAggregateDefinition(['amount'], { partition: 'location' })
1250
1252
  * ```
1251
1253
  */
1252
- function aggregateDefinition(fields, input) {
1254
+ function buildAggregateDefinition(fields, input) {
1253
1255
  return {
1254
1256
  fields: [...fields],
1255
- ...input?.by === void 0 ? {} : { by: input.by },
1257
+ ...input?.partition === void 0 ? {} : { partition: input.partition },
1256
1258
  ...input?.gates === void 0 ? {} : { gates: input.gates }
1257
1259
  };
1258
1260
  }
1259
1261
  //#endregion
1260
1262
  //#region src/core/programs/Program.ts
1261
1263
  /**
1262
- * One compiled program — composes one qualifier and one rater over a shared
1263
- * reason engine and executes single subjects or aggregate-aware batches.
1264
+ * Composes one qualifier and one rater over a shared reason engine and executes
1265
+ * single subjects or aggregate-aware batches.
1264
1266
  *
1265
1267
  * @remarks
1266
1268
  * Qualification decides whether rating happens: a globally ineligible, referred,
@@ -1293,21 +1295,31 @@ var Program = class {
1293
1295
  #validate;
1294
1296
  #labels;
1295
1297
  #destroyed = false;
1298
+ /** Holds the authored id of the definition this program compiled. */
1296
1299
  id;
1300
+ /** Holds the authored display name of the definition this program compiled. */
1297
1301
  name;
1302
+ /** Holds the sealed snapshot of the authored definition this program compiled. */
1298
1303
  definition;
1304
+ /**
1305
+ * Compiles one program from an authored definition.
1306
+ *
1307
+ * @param definition - The authored program definition
1308
+ * @param options - Optional injected qualifier, rater, engine, validation, labels, and emitter hooks
1309
+ * @throws {@link ProgramError} Thrown when the definition cannot be cloned or
1310
+ * sealed, or when validation is enabled and the definition fails
1311
+ * (`'DEFINITION'`).
1312
+ * @throws {@link ProgramError} Thrown when a ruling or notice scope names no
1313
+ * rating line (`'MISSING'`).
1314
+ * @throws {@link ProgramError} Thrown when the definition repeats a rating-line
1315
+ * or notice id (`'DUPLICATE'`).
1316
+ */
1299
1317
  constructor(definition, options) {
1300
1318
  let snapshot;
1301
1319
  try {
1302
1320
  snapshot = structuredClone(definition);
1303
1321
  } 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;
1322
+ throw new ProgramError("DEFINITION", "Program definition could not be cloned", void 0, cause);
1311
1323
  }
1312
1324
  assertProgramDefinition(snapshot);
1313
1325
  this.id = snapshot.id;
@@ -1316,13 +1328,7 @@ var Program = class {
1316
1328
  try {
1317
1329
  this.#seal();
1318
1330
  } 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;
1331
+ throw new ProgramError("DEFINITION", "Program definition could not be sealed", snapshot.id, cause);
1326
1332
  }
1327
1333
  this.#emitter = new Emitter({
1328
1334
  ...options?.on === void 0 ? {} : { on: options.on },
@@ -1348,6 +1354,21 @@ var Program = class {
1348
1354
  }
1349
1355
  }
1350
1356
  }
1357
+ /**
1358
+ * Holds the typed observation surface carrying `qualify`, `rate`, `determine`,
1359
+ * `decide`, `execute`, `aggregate`, and `destroy`.
1360
+ *
1361
+ * @returns The emitter this program owns
1362
+ *
1363
+ * @example
1364
+ * ```ts
1365
+ * import { createProgram } from '@orkestrel/program'
1366
+ *
1367
+ * const program = createProgram(definition)
1368
+ * program.emitter.on('execute', (result) => result.status)
1369
+ * program.destroy()
1370
+ * ```
1371
+ */
1351
1372
  get emitter() {
1352
1373
  return this.#emitter;
1353
1374
  }
@@ -1356,10 +1377,51 @@ var Program = class {
1356
1377
  if (isArray(input)) return this.#aggregate(input);
1357
1378
  return this.#subject(input);
1358
1379
  }
1380
+ /**
1381
+ * Validates this program's definition and every nested definition.
1382
+ *
1383
+ * @remarks
1384
+ * Exact shape is `isProgramDefinition`'s job. This checks the meaning: non-empty id
1385
+ * and name, every ruling and notice scope naming a rating line, unique non-empty
1386
+ * aggregate fields, and a non-empty partition field when present. Nested
1387
+ * qualification validation is delegated to the injected qualifier, and authority
1388
+ * and aggregate-gate validation to the shared reason engine.
1389
+ *
1390
+ * @returns A fresh validation result carrying `valid`, `errors`, and `warnings`
1391
+ * @throws {@link ProgramError} Thrown when the program has been destroyed
1392
+ * (`'DESTROYED'`).
1393
+ *
1394
+ * @example
1395
+ * ```ts
1396
+ * import { createProgram } from '@orkestrel/program'
1397
+ *
1398
+ * const program = createProgram(definition, { validate: false })
1399
+ * program.validate().valid // true
1400
+ * program.destroy()
1401
+ * ```
1402
+ */
1359
1403
  validate() {
1360
1404
  this.#alive();
1361
1405
  return validateProgramDefinition(this.definition, this.#qualifier, this.#engine);
1362
1406
  }
1407
+ /**
1408
+ * Destroys this program, idempotently.
1409
+ *
1410
+ * @remarks
1411
+ * The destroyed flag is set BEFORE any teardown or the `destroy` event, so a
1412
+ * listener re-entering `destroy` is a no-op. An owned qualifier, rater, and reason
1413
+ * engine are destroyed; an injected one stays caller-owned. The emitter is torn
1414
+ * down last, and stays reachable afterwards.
1415
+ *
1416
+ * @example
1417
+ * ```ts
1418
+ * import { createProgram } from '@orkestrel/program'
1419
+ *
1420
+ * const program = createProgram(definition)
1421
+ * program.destroy()
1422
+ * program.destroy() // a second call is a no-op
1423
+ * ```
1424
+ */
1363
1425
  destroy() {
1364
1426
  if (this.#destroyed) return;
1365
1427
  this.#destroyed = true;
@@ -1385,7 +1447,7 @@ var Program = class {
1385
1447
  return this.#finish(subject, qualification, rating);
1386
1448
  }
1387
1449
  #finish(subject, qualification, rating) {
1388
- const notices = buildNotices(this.definition.notices ?? [], subject);
1450
+ const notices = buildNoticeDeterminations(this.definition.notices ?? [], subject);
1389
1451
  for (const notice of notices) this.#emitter.emit("determine", notice);
1390
1452
  const status = deriveStatus(this.definition, qualification, rating);
1391
1453
  let result = buildProgramResult(this.definition, qualification, rating, notices, status);
@@ -1397,7 +1459,7 @@ var Program = class {
1397
1459
  const outcome = { [OUTCOME_KEY]: buildOutcomeProjection(result) };
1398
1460
  const resolved = this.#engine.reason(outcome, authority);
1399
1461
  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);
1462
+ const limits = buildLimitDeterminations(authority, resolved, outcome, this.#evaluator, this.#labels);
1401
1463
  for (const limit of limits) this.#emitter.emit("determine", limit);
1402
1464
  result = buildProgramResult(this.definition, qualification, rating, [...notices, ...limits], status, { authority: resolved });
1403
1465
  if (result.decision !== void 0) this.#emitter.emit("decide", result.decision, result);
@@ -1409,12 +1471,12 @@ var Program = class {
1409
1471
  const definition = this.definition.aggregate;
1410
1472
  const fields = [...definition?.fields ?? []];
1411
1473
  const sums = aggregateSums(subjects, fields);
1412
- const groups = aggregateGroups(subjects, fields, definition?.by);
1413
- let tallies = emptyTallies(fields);
1474
+ const groups = aggregateGroups(subjects, fields, definition?.partition);
1475
+ let tallies = buildEmptyTallies(fields);
1414
1476
  const results = subjects.map((subject) => {
1415
- const projection = definition === void 0 ? void 0 : buildAggregateProjection(subject, subjects.length, sums, groups, definition.by);
1477
+ const projection = definition === void 0 ? void 0 : buildAggregateProjection(subject, subjects.length, sums, groups, definition.partition);
1416
1478
  const result = this.#subject(subject, projection);
1417
- tallies = tallyProgram(tallies, result, subject, fields);
1479
+ tallies = tallySubject(tallies, result, subject, fields);
1418
1480
  return result;
1419
1481
  });
1420
1482
  const gates = this.#aggregateLimits(subjects.length, sums, groups);
@@ -1428,7 +1490,7 @@ var Program = class {
1428
1490
  const record = buildAggregateRecord(count, sums, groups);
1429
1491
  const resolved = this.#engine.reason(record, gates);
1430
1492
  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);
1493
+ const determinations = buildLimitDeterminations(gates, resolved, record, this.#evaluator, this.#labels);
1432
1494
  for (const determination of determinations) this.#emitter.emit("determine", determination);
1433
1495
  return {
1434
1496
  determinations,
@@ -1451,8 +1513,8 @@ var Program = class {
1451
1513
  //#endregion
1452
1514
  //#region src/core/programs/ProgramManager.ts
1453
1515
  /**
1454
- * An ordered manager over compiled {@link ProgramInterface}s (AGENTS §9), sharing
1455
- * one qualifier, rater, and reason engine across every program it compiles.
1516
+ * Manages compiled {@link ProgramInterface} programs in order, sharing one
1517
+ * qualifier, rater, and reason engine across every program it compiles.
1456
1518
  *
1457
1519
  * @remarks
1458
1520
  * OWNS its ordered `#programs` collection and its own {@link Emitter} over
@@ -1479,6 +1541,13 @@ var ProgramManager = class {
1479
1541
  #validate;
1480
1542
  #labels;
1481
1543
  #destroyed = false;
1544
+ /**
1545
+ * Creates one manager and compiles every seed definition in order.
1546
+ *
1547
+ * @param options - Optional injected qualifier, rater, engine, seed programs, validation, labels, and emitter hooks
1548
+ * @throws {@link ProgramError} Thrown when a seed definition fails to compile,
1549
+ * after the manager destroys whatever it had already compiled.
1550
+ */
1482
1551
  constructor(options) {
1483
1552
  this.#emitter = new Emitter({
1484
1553
  ...options?.on === void 0 ? {} : { on: options.on },
@@ -1502,25 +1571,138 @@ var ProgramManager = class {
1502
1571
  throw error;
1503
1572
  }
1504
1573
  }
1574
+ /**
1575
+ * Holds the typed observation surface carrying `add`, `remove`, and `destroy`.
1576
+ *
1577
+ * @returns The emitter this manager owns
1578
+ *
1579
+ * @example
1580
+ * ```ts
1581
+ * import { createProgramManager } from '@orkestrel/program'
1582
+ *
1583
+ * const manager = createProgramManager()
1584
+ * manager.emitter.on('add', (id) => id)
1585
+ * manager.destroy()
1586
+ * ```
1587
+ */
1505
1588
  get emitter() {
1506
1589
  return this.#emitter;
1507
1590
  }
1508
- get size() {
1591
+ /**
1592
+ * Holds how many programs the manager has compiled.
1593
+ *
1594
+ * @returns The number of compiled programs
1595
+ * @throws {@link ProgramError} Thrown when the manager has been destroyed
1596
+ * (`'DESTROYED'`).
1597
+ *
1598
+ * @example
1599
+ * ```ts
1600
+ * import { createProgramManager } from '@orkestrel/program'
1601
+ *
1602
+ * const manager = createProgramManager({ programs: [definition] })
1603
+ * manager.count // 1
1604
+ * manager.destroy()
1605
+ * ```
1606
+ */
1607
+ get count() {
1509
1608
  this.#alive();
1510
1609
  return this.#programs.length;
1511
1610
  }
1611
+ /**
1612
+ * Reports whether an id names a compiled program.
1613
+ *
1614
+ * @param id - The program id to look for
1615
+ * @returns True if a compiled program carries the id; false otherwise
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.has('standard') // true
1625
+ * manager.destroy()
1626
+ * ```
1627
+ */
1512
1628
  has(id) {
1513
1629
  this.#alive();
1514
1630
  return this.#programs.some((program) => program.id === id);
1515
1631
  }
1632
+ /**
1633
+ * Looks one compiled program up by id.
1634
+ *
1635
+ * @param id - The program id to look up
1636
+ * @returns The compiled program, or `undefined` when no program carries the id
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.program('standard')?.execute({ id: 'risk-1', licensed: true })
1646
+ * manager.destroy()
1647
+ * ```
1648
+ */
1516
1649
  program(id) {
1517
1650
  this.#alive();
1518
1651
  return this.#programs.find((program) => program.id === id);
1519
1652
  }
1653
+ /**
1654
+ * Returns every compiled program, in insertion order.
1655
+ *
1656
+ * @remarks
1657
+ * The returned array is a fresh copy, so mutating it never reaches the manager's
1658
+ * own collection.
1659
+ *
1660
+ * @returns A fresh array of compiled programs, in insertion order
1661
+ * @throws {@link ProgramError} Thrown when the manager has been destroyed
1662
+ * (`'DESTROYED'`).
1663
+ *
1664
+ * @example
1665
+ * ```ts
1666
+ * import { createProgramManager } from '@orkestrel/program'
1667
+ *
1668
+ * const manager = createProgramManager({ programs: [definition] })
1669
+ * manager.programs().map((program) => program.id) // ['standard']
1670
+ * manager.destroy()
1671
+ * ```
1672
+ */
1520
1673
  programs() {
1521
1674
  this.#alive();
1522
1675
  return [...this.#programs];
1523
1676
  }
1677
+ /**
1678
+ * Compiles one definition and appends it to the collection.
1679
+ *
1680
+ * @remarks
1681
+ * The compiled program borrows the manager's shared qualifier, rater, and reason
1682
+ * engine, and inherits the manager's `validate` and `labels` options. After
1683
+ * appending the program, the `add` event fires with its id.
1684
+ *
1685
+ * @param definition - The authored program definition to compile
1686
+ * @returns The compiled program
1687
+ * @throws {@link ProgramError} Thrown when the manager has been destroyed
1688
+ * (`'DESTROYED'`).
1689
+ * @throws {@link ProgramError} Thrown when the manager already carries the
1690
+ * definition's id, or the definition repeats a rating-line or notice id
1691
+ * (`'DUPLICATE'`).
1692
+ * @throws {@link ProgramError} Thrown when a ruling or notice scope names no
1693
+ * rating line (`'MISSING'`).
1694
+ * @throws {@link ProgramError} Thrown when validation is enabled and the
1695
+ * definition fails (`'DEFINITION'`).
1696
+ *
1697
+ * @example
1698
+ * ```ts
1699
+ * import { createProgramManager } from '@orkestrel/program'
1700
+ *
1701
+ * const manager = createProgramManager()
1702
+ * manager.add(definition).id // 'standard'
1703
+ * manager.destroy()
1704
+ * ```
1705
+ */
1524
1706
  add(definition) {
1525
1707
  this.#alive();
1526
1708
  if (this.has(definition.id)) throw new ProgramError("DUPLICATE", `Program "${definition.id}" already exists`, definition.id);
@@ -1548,6 +1730,25 @@ var ProgramManager = class {
1548
1730
  }
1549
1731
  if (typeof input === "string") return this.#removeOne(input);
1550
1732
  }
1733
+ /**
1734
+ * Destroys this manager, idempotently.
1735
+ *
1736
+ * @remarks
1737
+ * The destroyed flag is set BEFORE any teardown or the `remove` and `destroy`
1738
+ * events, so a `remove` listener re-entering `destroy` is a no-op. Compiled
1739
+ * programs are destroyed first, then an owned qualifier, rater, and reason engine;
1740
+ * an injected one stays caller-owned. The emitter is torn down last, and stays
1741
+ * reachable afterwards.
1742
+ *
1743
+ * @example
1744
+ * ```ts
1745
+ * import { createProgramManager } from '@orkestrel/program'
1746
+ *
1747
+ * const manager = createProgramManager({ programs: [definition] })
1748
+ * manager.destroy()
1749
+ * manager.destroy() // a second call is a no-op
1750
+ * ```
1751
+ */
1551
1752
  destroy() {
1552
1753
  if (this.#destroyed) return;
1553
1754
  this.#destroyed = true;
@@ -1580,11 +1781,12 @@ var ProgramManager = class {
1580
1781
  //#endregion
1581
1782
  //#region src/core/factories.ts
1582
1783
  /**
1583
- * Create one compiled program over a qualifier and rater.
1784
+ * Creates one compiled program over a qualifier and rater.
1584
1785
  *
1585
1786
  * @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
1787
+ * If `options.validate` is `true`, the program validates the definition at
1788
+ * construction; if `false`, it compiles the definition unvalidated. Default:
1789
+ * {@link DEFAULT_PROGRAM_VALIDATE}. A standalone program creates and
1588
1790
  * OWNS one shared quantitative-plus-logical reason engine and injects it into the
1589
1791
  * qualifier and rater it creates; injected dependencies remain caller-owned.
1590
1792
  *
@@ -1594,9 +1796,10 @@ var ProgramManager = class {
1594
1796
  *
1595
1797
  * @example
1596
1798
  * ```ts
1597
- * import { createProgram, programDefinition } from '@orkestrel/program'
1799
+ * import { buildProgramDefinition, createProgram } from '@orkestrel/program'
1598
1800
  *
1599
- * const program = createProgram(programDefinition('standard', 'Standard', qualification, rating))
1801
+ * const definition = buildProgramDefinition('standard', 'Standard', qualification, rating)
1802
+ * const program = createProgram(definition)
1600
1803
  * program.execute({ id: 'risk-1' })
1601
1804
  * program.destroy()
1602
1805
  * ```
@@ -1605,7 +1808,7 @@ function createProgram(definition, options) {
1605
1808
  return new Program(definition, options);
1606
1809
  }
1607
1810
  /**
1608
- * Create one ordered manager over compiled programs.
1811
+ * Creates one ordered manager over compiled programs.
1609
1812
  *
1610
1813
  * @remarks
1611
1814
  * Creates or borrows one shared reason engine, qualifier, and rater and injects
@@ -1628,6 +1831,6 @@ function createProgramManager(options) {
1628
1831
  return new ProgramManager(options);
1629
1832
  }
1630
1833
  //#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 };
1834
+ 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
1835
 
1633
1836
  //# sourceMappingURL=index.js.map