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