@orkestrel/scaffold 0.0.67 → 0.0.68

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.
Files changed (74) hide show
  1. package/dist/bin/main.js +67 -44
  2. package/dist/bin/main.js.map +1 -1
  3. package/dist/host/agents/templates/brief.md +9 -0
  4. package/dist/host/claude/agents/orkestrel.md +4 -4
  5. package/dist/host/claude/rules/names.md +15 -0
  6. package/dist/host/claude/rules/tests.md +33 -4
  7. package/dist/host/claude/rules/workspace.md +14 -2
  8. package/dist/host/dotfiles/prettierignore +3 -0
  9. package/dist/host/guides/README.md +65 -0
  10. package/dist/host/guides/abort.md +169 -0
  11. package/dist/host/guides/agent.md +1509 -0
  12. package/dist/host/guides/brief.md +1266 -0
  13. package/dist/host/guides/browser.md +2200 -0
  14. package/dist/host/guides/budget.md +196 -0
  15. package/dist/host/guides/codec.md +519 -0
  16. package/dist/host/guides/console.md +785 -0
  17. package/dist/host/guides/contract.md +1193 -0
  18. package/dist/host/guides/csv.md +541 -0
  19. package/dist/host/guides/database.md +2518 -0
  20. package/dist/host/guides/emitter.md +233 -0
  21. package/dist/host/guides/form.md +1791 -0
  22. package/dist/host/guides/html.md +717 -0
  23. package/dist/host/guides/indexeddb.md +505 -0
  24. package/dist/host/guides/interpret.md +1029 -0
  25. package/dist/host/guides/lsp.md +515 -0
  26. package/dist/host/guides/markdown.md +964 -0
  27. package/dist/host/guides/mcp.md +5554 -0
  28. package/dist/host/guides/middleware.md +927 -0
  29. package/dist/host/guides/msg.md +440 -0
  30. package/dist/host/guides/ndjson.md +120 -0
  31. package/dist/host/guides/ollama.md +380 -0
  32. package/dist/host/guides/pool.md +280 -0
  33. package/dist/host/guides/probe.md +1210 -0
  34. package/dist/host/guides/process.md +1620 -0
  35. package/dist/host/guides/program.md +1110 -0
  36. package/dist/host/guides/qualifier.md +854 -0
  37. package/dist/host/guides/queue.md +370 -0
  38. package/dist/host/guides/rater.md +330 -0
  39. package/dist/host/guides/reason.md +1122 -0
  40. package/dist/host/guides/relation.md +373 -0
  41. package/dist/host/guides/router.md +753 -0
  42. package/dist/host/guides/scaffold.md +192 -31
  43. package/dist/host/guides/sea.md +383 -0
  44. package/dist/host/guides/server.md +752 -0
  45. package/dist/host/guides/sqlite.md +330 -0
  46. package/dist/host/guides/sse.md +187 -0
  47. package/dist/host/guides/supervisor.md +4890 -0
  48. package/dist/host/guides/table.md +1556 -0
  49. package/dist/host/guides/template.md +280 -0
  50. package/dist/host/guides/terminal.md +1145 -0
  51. package/dist/host/guides/test.md +2969 -0
  52. package/dist/host/guides/timeout.md +252 -0
  53. package/dist/host/guides/tool.md +311 -0
  54. package/dist/host/guides/toolbox.md +1038 -0
  55. package/dist/host/guides/websocket.md +282 -0
  56. package/dist/host/guides/worker.md +615 -0
  57. package/dist/host/guides/workflow.md +1507 -0
  58. package/dist/host/guides/workspace.md +595 -0
  59. package/dist/host/manifest.json +1218 -10
  60. package/dist/host/tests/policy.test.ts +279 -2
  61. package/dist/host/tests/setupPolicy.ts +437 -6
  62. package/dist/src/core/index.cjs +38 -16
  63. package/dist/src/core/index.cjs.map +1 -1
  64. package/dist/src/core/index.d.cts +33 -9
  65. package/dist/src/core/index.d.ts +33 -9
  66. package/dist/src/core/index.js +37 -17
  67. package/dist/src/core/index.js.map +1 -1
  68. package/dist/src/server/index.cjs +1750 -1567
  69. package/dist/src/server/index.cjs.map +1 -1
  70. package/dist/src/server/index.d.cts +106 -24
  71. package/dist/src/server/index.d.ts +106 -24
  72. package/dist/src/server/index.js +1751 -1570
  73. package/dist/src/server/index.js.map +1 -1
  74. package/package.json +3 -3
@@ -0,0 +1,1110 @@
1
+ # Program
2
+
3
+ > The program composition layer: a pure, JSON-serializable `ProgramDefinition`
4
+ > that composes one qualification with an optional rating, plus notices,
5
+ > authority, and batch aggregate policy, and a `Program` that executes that
6
+ > definition in one direction — qualify, select, rate, derive status, then decide.
7
+
8
+ Qualification decides whether rating happens: a globally ineligible, referred, or
9
+ failed subject never reaches the rater, and scoped ineligibility removes only the
10
+ matching line before the first rating call. Omitting `rating` authors a
11
+ first-class eligibility-only program — the rater is never invoked, an eligible
12
+ subject resolves to `'eligible'` (or `'conditional'` under an applied condition),
13
+ and status is never `'unrated'`; an authored rating with zero lines still yields
14
+ `'unrated'`, unchanged. The rater always receives the original subject;
15
+ qualification and aggregate working projections stay private to orchestration.
16
+ `Program` executes synchronously. Its result is repeatable only while inputs and
17
+ options stay unchanged and dependency behavior remains unchanged and deterministic.
18
+
19
+ `Program` performs no reasoning arithmetic. It owns orchestration and business
20
+ outcomes — notices, authority, status, decisions, and batch aggregates — while
21
+ delegating eligibility to `Qualifier`, amounts and worksheets to `Rater`, and
22
+ logical or quantitative mechanics to the shared `@orkestrel/reason` engine behind
23
+ them. Every output is a fresh `ProgramResult` or `AggregateResult` carrying the
24
+ nested qualification and rating evidence, program determinations, trace, errors,
25
+ status, and optional decision. `Program` either receives injected qualifier,
26
+ rater, and engine instances (never destroyed by `Program`) or creates and owns
27
+ one shared quantitative-plus-logical engine (`bail: false`), destroyed in
28
+ `destroy()`. Every `execute` call fires through `Program`'s typed `emitter`.
29
+ Source: [`src/core`](../src/core). Surfaced through the `@src/core` barrel.
30
+
31
+ ## Surface
32
+
33
+ Create a program, execute one subject, and inspect the nested results:
34
+
35
+ ```ts
36
+ import { buildProgramDefinition, createProgram } from '@orkestrel/program'
37
+ import { createQualificationDefinition, createRuling } from '@orkestrel/qualifier'
38
+ import { buildLineDefinition, buildRatingDefinition } from '@orkestrel/rater'
39
+ import {
40
+ createAtom,
41
+ createFactorGroup,
42
+ createLogicalDefinition,
43
+ createQuantitativeDefinition,
44
+ createRule,
45
+ createStaticFactor,
46
+ } from '@orkestrel/reason'
47
+
48
+ const gates = createLogicalDefinition('gates', 'Eligibility gates', [
49
+ createRule(
50
+ 'licensed',
51
+ [createAtom('licensed', 'equals', false)],
52
+ createAtom('blocked', 'equals', true),
53
+ ),
54
+ ])
55
+
56
+ const qualification = createQualificationDefinition(
57
+ 'standard-qualification',
58
+ 'Standard qualification',
59
+ [gates],
60
+ {
61
+ rulings: [
62
+ createRuling('license', 'gates', 'licensed', 'restriction', {
63
+ message: 'A license is required',
64
+ }),
65
+ ],
66
+ },
67
+ )
68
+
69
+ const base = buildLineDefinition(
70
+ 'base',
71
+ 'Base premium',
72
+ createQuantitativeDefinition('base-rate', 'Base rate', [
73
+ createFactorGroup('amount', 'sum', [createStaticFactor('minimum', 100)]),
74
+ ]),
75
+ )
76
+
77
+ const rating = buildRatingDefinition('standard-rating', 'Standard rating', [base])
78
+ const definition = buildProgramDefinition('standard', 'Standard program', qualification, rating)
79
+ const program = createProgram(definition)
80
+
81
+ const eligible = program.execute({ id: 'risk-1', licensed: true })
82
+ eligible.status // 'eligible'
83
+ eligible.rating?.total // 100
84
+
85
+ const ineligible = program.execute({ id: 'risk-2', licensed: false })
86
+ ineligible.status // 'ineligible'
87
+ ineligible.rating // undefined — the rater was not called
88
+
89
+ program.destroy()
90
+ ```
91
+
92
+ The array overload is declared first and performs one aggregate-aware batch
93
+ execution:
94
+
95
+ ```ts
96
+ const result = program.execute([
97
+ { id: 'a', licensed: true, amount: 10 },
98
+ { id: 'b', licensed: false, amount: 20 },
99
+ ])
100
+
101
+ result.count // 2
102
+ result.subjects[0]?.status // 'eligible'
103
+ result.subjects[1]?.status // 'ineligible'
104
+ result.tallies.eligible.count // 1
105
+ result.tallies.ineligible.count // 1
106
+ ```
107
+
108
+ A `ProgramManager` stores compiled programs without hiding them behind a second
109
+ business facade — the manager owns collection lifecycle, each `Program` owns
110
+ execution:
111
+
112
+ ```ts
113
+ import { createProgramManager } from '@orkestrel/program'
114
+
115
+ const manager = createProgramManager()
116
+ manager.add(definition)
117
+
118
+ const standard = manager.program('standard')
119
+ standard?.execute(subject)
120
+
121
+ manager.destroy()
122
+ ```
123
+
124
+ ### Types
125
+
126
+ A `Shape` cell holds an interface's data members as bare names in braces, `?` marking an optional member and `plus` introducing its call-signature members, and a type alias's own type literal with a union's arms escaped as `\|`.
127
+
128
+ | Type | Kind | Shape | Summary |
129
+ | ------------------------- | --------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
130
+ | `Decision` | type | `'approved' \| 'denied' \| 'submitted'` | Identifies a final authority outcome, derived from global eligibility. |
131
+ | `Status` | type | `'ineligible' \| 'referral' \| 'conditional' \| 'unrated' \| 'eligible'` | Identifies the presentation and tally status derived from eligibility, conditions, and rating success. |
132
+ | `ProgramEffect` | type | `'notice' \| 'limit'` | Identifies a post-qualification program determination effect. |
133
+ | `ProgramErrorCode` | type | `'DUPLICATE' \| 'MISSING' \| 'DEFINITION' \| 'MISMATCH' \| 'RESERVED' \| 'DESTROYED'` | Identifies a coded `ProgramError` programmer-error code. |
134
+ | `ProgramInput` | interface | `{ description?, notices?, authority?, aggregate?, metadata? }` | Describes the optional fields accepted by `buildProgramDefinition`. |
135
+ | `NoticeInput` | interface | `{ scope? }` | Describes the optional fields accepted by `buildNotice`. |
136
+ | `AggregateInput` | interface | `{ partition?, gates? }` | Describes the optional fields accepted by `buildAggregateDefinition`. |
137
+ | `Notice` | interface | `{ id, message, scope? }` | Describes an authored, unconditional program notice. |
138
+ | `Determination` | interface | `{ id, effect, applied, scope?, message?, premises }` | Describes one resolved notice or authority-limit outcome. |
139
+ | `AggregateDefinition` | interface | `{ fields, partition?, gates? }` | Describes batch aggregate fields, an optional partition field, and optional gates. |
140
+ | `AggregateProjection` | interface | `{ count, sums, group? }` | Describes one subject's private aggregate working projection. |
141
+ | `AggregateGroup` | interface | `{ key, count, sums }` | Describes one batch aggregate partition. |
142
+ | `Tally` | interface | `{ count, sums }` | Describes a status tally — a count plus summed aggregate fields. |
143
+ | `ProgramDefinition` | interface | `{ id, name, description?, qualification, rating?, notices?, authority?, aggregate?, metadata? }` | Describes a pure authored program definition. |
144
+ | `ProgramResult` | interface | `{ id, name, eligibility, status, decision?, qualification, rating?, determinations, success, trace, errors }` | Describes one subject's complete program outcome. |
145
+ | `AggregateResult` | interface | `{ id, name, subjects, determinations, groups, tallies, count, sums, success, trace, errors }` | Describes a batch program outcome across every subject. |
146
+ | `ProgramValidationResult` | interface | `{ valid, errors, warnings }` | Describes semantic definition validation. |
147
+ | `ProgramEventMap` | type | `{ qualify, rate, determine, decide, execute, aggregate, destroy }` | Describes the push observation surface of a `ProgramInterface`. |
148
+ | `ProgramOptions` | interface | `{ qualifier?, rater?, engine?, validate?, labels?, on?, error? }` | Describes the options for `createProgram` / the `Program` constructor. |
149
+ | `ProgramInterface` | interface | `{ id, name, definition, emitter } plus execute, validate, destroy` | Defines one compiled program that composes one qualifier and one rater over a shared reason engine. |
150
+ | `ProgramManagerEventMap` | type | `{ add, remove, destroy }` | Describes the push observation surface of a `ProgramManagerInterface`. |
151
+ | `ProgramManagerOptions` | interface | `{ qualifier?, rater?, engine?, programs?, validate?, labels?, on?, error? }` | Describes the options for `createProgramManager` / the `ProgramManager` constructor. |
152
+ | `ProgramManagerInterface` | interface | `{ emitter, count } plus has, program, programs, add, remove, destroy` | Defines an ordered manager over compiled programs, sharing one qualifier and rater. |
153
+
154
+ Every public data member is `readonly`, every optional key is omitted rather than
155
+ `undefined`, and each name is single-word within its entity. Qualifier
156
+ supplies `Eligibility`, `Premise`, `QualificationDefinition`, and `QualificationResult`;
157
+ rater supplies `RatingDefinition` and `RatingResult`; reason supplies
158
+ `LogicalDefinition`, `Subject`, and the engine; contract supplies `FieldPath` and
159
+ `JSONValue`; the emitter supplies the observation types.
160
+
161
+ ### Constants
162
+
163
+ A `Shape` cell holds the constant's declared type.
164
+
165
+ | API | Kind | Shape | Summary |
166
+ | -------------------------- | ----- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
167
+ | `DEFAULT_PROGRAM_VALIDATE` | const | `boolean` | Names the default definition validation policy, `true`, for `createProgram` / `ProgramManager.add`. |
168
+ | `STATUSES` | const | `readonly ['ineligible', 'referral', 'conditional', 'unrated', 'eligible']` | Lists every `Status` literal in tally order — the source the union and its guard derive from. |
169
+ | `ELIGIBILITY_DECISIONS` | const | `Readonly<Record<Eligibility, Decision>>` | Maps each global eligibility to its deterministic authority decision. |
170
+ | `AGGREGATE_KEY` | const | `string` | Names the reserved working-subject key a batch's aggregate projection is written under, `'aggregate'`. |
171
+ | `OUTCOME_KEY` | const | `string` | Names the reserved working-subject key the authority's outcome projection is written under, `'outcome'`. |
172
+
173
+ `STATUSES` and `ELIGIBILITY_DECISIONS` are `Object.freeze`d; the reserved keys and
174
+ the validation default are primitives. The reserved keys exist only for composed
175
+ program execution — neither sibling package reserves these subject keys.
176
+ `completeTallies` writes every `Status` member as a literal record, and `isTallies`
177
+ checks membership through `STATUSES`.
178
+
179
+ ### Errors
180
+
181
+ | API | Kind | Summary |
182
+ | ---------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
183
+ | `ProgramError` | class | Reports a coded programmer error thrown by the program layer, carrying a machine-readable code and an optional context and cause. |
184
+ | `isProgramError` | function | Determines whether a caught value is a `ProgramError`. |
185
+
186
+ ```ts
187
+ import { isProgramError, ProgramError } from '@orkestrel/program'
188
+
189
+ try {
190
+ throw new ProgramError('RESERVED', 'Subject contains a reserved program key', 'aggregate')
191
+ } catch (error) {
192
+ if (isProgramError(error)) error.code // 'RESERVED'
193
+ }
194
+ ```
195
+
196
+ | Code | Meaning |
197
+ | ------------ | -------------------------------------------------------------------------------------------------------------- |
198
+ | `DUPLICATE` | A manager already contains the program id, or an authored definition has a duplicate rating-line or notice id. |
199
+ | `MISSING` | A notice, ruling scope, or other authored reference names no rating line. |
200
+ | `DEFINITION` | Program, qualification, rating, authority, or aggregate policy is invalid. |
201
+ | `MISMATCH` | An injected entity or returned result has the wrong contract. |
202
+ | `RESERVED` | A subject already carries `aggregate` or `outcome`. |
203
+ | `DESTROYED` | An operation was attempted after teardown. |
204
+
205
+ Eligibility and rating failures remain nested result evidence rather than throws.
206
+
207
+ ### Validators
208
+
209
+ All guards are total: adversarial input returns `false`, never throws. Authored
210
+ inputs use exact-record posture. `isNotice` stays exact because `Notice` appears
211
+ only in authored definitions; result notices are `Determination` values.
212
+
213
+ Result guards use open posture. They admit unknown members and class instances,
214
+ refuse arrays, and accept an optional member when it is absent or `undefined`.
215
+ Use `isProgramResult` and `isAggregateResult` when a result arrives through a
216
+ borrowed `ProgramInterface`. Each composite guard checks the full nested closure,
217
+ except the string-dictionary leaves (`sums`, `scopes`), which certify own members
218
+ only — a value carrying them on a prototype is admitted unchecked.
219
+ Holding `isProgramResult` therefore also holds qualifier's published
220
+ `isQualificationResult` closure and rater's published `isRatingResult` closure.
221
+ `isProgramValidationResult` checks this package's own interface directly rather
222
+ than delegating to reason's independently evolvable validation contract.
223
+
224
+ In a guard table a `Shape` cell holds the type the guard narrows to.
225
+
226
+ | API | Kind | Shape | Summary |
227
+ | --------------------------- | -------- | ---------------------------------- | -------------------------------------------------------------------- |
228
+ | `isDecision` | const | `Decision` | Determines whether a value is a `Decision` literal. |
229
+ | `isStatus` | const | `Status` | Determines whether a value is a `Status` literal. |
230
+ | `isProgramEffect` | const | `ProgramEffect` | Determines whether a value is a `ProgramEffect` literal. |
231
+ | `isNotice` | function | `Notice` | Determines whether a value is an exact `Notice` record. |
232
+ | `isAggregateDefinition` | function | `AggregateDefinition` | Determines whether a value is an exact `AggregateDefinition` record. |
233
+ | `isProgramDefinition` | function | `ProgramDefinition` | Determines whether a value is an exact `ProgramDefinition` record. |
234
+ | `isProgramSums` | function | `Readonly<Record<string, number>>` | Determines whether a value is an open program sums record. |
235
+ | `isDetermination` | const | `Determination` | Determines whether a value is an open result-side `Determination`. |
236
+ | `isAggregateGroup` | const | `AggregateGroup` | Determines whether a value is an open result-side `AggregateGroup`. |
237
+ | `isTally` | const | `Tally` | Determines whether a value is an open result-side `Tally`. |
238
+ | `isTallies` | function | `Readonly<Record<Status, Tally>>` | Determines whether a value is a total open status-tally record. |
239
+ | `isProgramResult` | const | `ProgramResult` | Determines whether a value is an open `ProgramResult`. |
240
+ | `isAggregateResult` | const | `AggregateResult` | Determines whether a value is an open `AggregateResult`. |
241
+ | `isProgramValidationResult` | const | `ProgramValidationResult` | Determines whether a value is an open `ProgramValidationResult`. |
242
+
243
+ `isProgramSums` checks every own string-named member, including non-enumerable
244
+ members, as a JavaScript `number`; it ignores inherited and symbol-named members.
245
+ `isTallies` requires every status in `STATUSES`, while admitting unknown
246
+ members because `Record<Status, Tally>` does not forbid them.
247
+
248
+ ```ts
249
+ import {
250
+ isAggregateDefinition,
251
+ isAggregateResult,
252
+ isDecision,
253
+ isDetermination,
254
+ isNotice,
255
+ isProgramDefinition,
256
+ isProgramResult,
257
+ isProgramValidationResult,
258
+ isStatus,
259
+ } from '@orkestrel/program'
260
+
261
+ isDecision('approved') // true
262
+ isStatus('conditional') // true
263
+ isNotice({ id: 'file', message: 'Subject retained for audit' }) // true
264
+ isAggregateDefinition({ fields: ['amount'], partition: 'location' }) // true
265
+ isProgramDefinition(definition) // true
266
+ isDetermination({ id: 'audit', effect: 'notice', applied: true, premises: [] }) // true
267
+ isProgramResult(program.execute(subject)) // true
268
+ isAggregateResult(program.execute(subjects)) // true
269
+ isProgramValidationResult(program.validate()) // true
270
+ ```
271
+
272
+ `isProgramDefinition` establishes exact shape only. `Program.validate` additionally
273
+ checks semantic references and delegates nested validation to qualifier and rater.
274
+
275
+ ### Helpers
276
+
277
+ The program helpers are pure orchestration leaves. They do not reproduce qualifier,
278
+ rater, or reason logic — message interpolation and rich premise construction for
279
+ authority and aggregate-gate rules reuse `@orkestrel/qualifier`'s own
280
+ `interpolateMessage`, `findRule`, and `ruleToPremises` (all public qualifier exports,
281
+ generic over any `Rule`/`Subject`/`EvaluatorInterface`) rather than re-implementing
282
+ them. `Program` owns one stateless `#evaluator` (created with `createEvaluator()`,
283
+ never destroyed — it holds no state to tear down) purely to drive that reuse.
284
+
285
+ | API | Kind | Summary |
286
+ | --------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
287
+ | `selectProgramLines` | function | Selects the rating lines a subject may be rated on from scoped eligibility. |
288
+ | `deriveStatus` | function | Derives the final program `Status` from a definition's rating policy and qualification/rating evidence. |
289
+ | `decideEligibility` | function | Maps a global `Eligibility` to its deterministic authority `Decision`. |
290
+ | `buildNoticeDeterminations` | function | Resolves authored `Notice` values into unconditionally-applied `notice` `Determination` values. |
291
+ | `buildLimitDeterminations` | function | Converts a logical result's applied rules into `limit` `Determination` values. |
292
+ | `buildProgramResult` | function | Assembles a `ProgramResult` from its qualification, rating, and determination parts — before or after authority. |
293
+ | `buildOutcomeProjection` | function | Builds the private authority outcome projection from an assembled program result. |
294
+ | `buildQualificationSubject` | function | Adds optional aggregate context to a private subject copy for qualification. |
295
+ | `findMissingScopes` | function | Returns authored scopes (qualification ruling scopes or notice scopes) that name no rating line on the program. |
296
+ | `hasReservedKey` | function | Determines whether a caller subject already carries a reserved program key. |
297
+ | `assertProgramSubject` | function | Asserts a value is a valid program `Subject`, narrowing it in place. |
298
+ | `assertProgramDefinition` | function | Asserts a program definition's always-on construction invariants — missing scope references and duplicate rating-line or notice ids. |
299
+ | `validateProgramDefinition` | function | Validates a program definition's shape, references, and nested definitions. |
300
+ | `formatGroupKey` | function | Coerces a subject's partition-key field to its group-key string. |
301
+ | `sumFields` | function | Folds one subject's finite aggregate field values into a sums record. |
302
+ | `aggregateSums` | function | Sums aggregate fields across a batch of subjects. |
303
+ | `aggregateGroups` | function | Partitions a batch of subjects by a field, summing aggregate fields per key. |
304
+ | `buildAggregateProjection` | function | Builds one subject's overall and optional group aggregate projection. |
305
+ | `buildAggregateRecord` | function | Builds the reserved-key record a batch aggregate-gate definition runs against. |
306
+ | `buildEmptySums` | function | Builds a zero-sum record for a set of aggregate fields. |
307
+ | `buildEmptyTallies` | function | Builds complete zero status tallies in `STATUSES` order. |
308
+ | `completeTallies` | function | Completes a partial status tally record with zero entries for every missing `Status`. |
309
+ | `tallySubject` | function | Adds one subject's aggregate contribution to a status tally record. |
310
+ | `buildAggregateResult` | function | Assembles one batch `AggregateResult` from its per-subject and aggregate parts. |
311
+ | `buildProgramDefinition` | function | Builds a fresh `ProgramDefinition`. |
312
+ | `buildNotice` | function | Builds a fresh `Notice`. |
313
+ | `buildAggregateDefinition` | function | Builds a fresh `AggregateDefinition`. |
314
+
315
+ The per-subject orchestration leaves guard the subject, select surviving lines, and
316
+ map eligibility to a decision:
317
+
318
+ ```ts
319
+ import {
320
+ assertProgramDefinition,
321
+ assertProgramSubject,
322
+ decideEligibility,
323
+ hasReservedKey,
324
+ selectProgramLines,
325
+ } from '@orkestrel/program'
326
+
327
+ hasReservedKey({ id: 'r1' }) // false
328
+ hasReservedKey({ id: 'r1', aggregate: {} }) // true
329
+ assertProgramSubject({ id: 'r1' }) // narrows to Subject; throws ProgramError('RESERVED') on a reserved key
330
+ assertProgramDefinition(definition) // throws ProgramError('MISSING' | 'DUPLICATE') at construction, regardless of options.validate
331
+ selectProgramLines(lines, { wind: 'ineligible' }) // every line except the 'wind' line
332
+ decideEligibility('eligible') // 'approved'
333
+ ```
334
+
335
+ The batch leaves sum configured fields, partition subjects, and seed zero records:
336
+
337
+ ```ts
338
+ import {
339
+ aggregateGroups,
340
+ aggregateSums,
341
+ buildEmptySums,
342
+ formatGroupKey,
343
+ sumFields,
344
+ } from '@orkestrel/program'
345
+
346
+ const subjects = [
347
+ { id: 'a', location: 'west', total: 100 },
348
+ { id: 'b', location: 'west', total: 200 },
349
+ { id: 'c', location: 'east', total: 50 },
350
+ ]
351
+
352
+ aggregateSums(subjects, ['total']) // { total: 350 }
353
+ aggregateGroups(subjects, ['total'], 'location') // [{ key: 'west', count: 2, sums: { total: 300 } }, { key: 'east', count: 1, sums: { total: 50 } }]
354
+ formatGroupKey({ location: 'west' }, 'location') // 'west' — String-coerced, so a missing field and '' land in the same partition
355
+ sumFields({ total: 0 }, subjects[0], ['total']) // { total: 100 } — a fresh record, only finite numbers contribute
356
+ buildEmptySums(['total']) // { total: 0 }
357
+ ```
358
+
359
+ The definition leaves build the authored program values. Each returns a fresh value,
360
+ copies collections, and omits absent optional keys entirely:
361
+
362
+ ```ts
363
+ import { buildAggregateDefinition, buildNotice, buildProgramDefinition } from '@orkestrel/program'
364
+
365
+ const aggregate = buildAggregateDefinition(['amount'], { partition: 'location' })
366
+ const notice = buildNotice('audit', 'Program {{program}} executed')
367
+
368
+ const definition = buildProgramDefinition('standard', 'Standard', qualification, rating, {
369
+ notices: [notice],
370
+ aggregate,
371
+ })
372
+ ```
373
+
374
+ ### Factories
375
+
376
+ | API | Kind | Summary |
377
+ | ---------------------- | -------- | --------------------------------------------------------------------- |
378
+ | `createProgram` | function | Creates one compiled `ProgramInterface` over a qualifier and rater. |
379
+ | `createProgramManager` | function | Creates one ordered `ProgramManagerInterface` over compiled programs. |
380
+
381
+ The factories compile entities. The authored definitions they compile are plain
382
+ values, so their builders are helper leaves rather than factories.
383
+
384
+ #### Compile a program and a manager
385
+
386
+ Compile a definition into a program and a manager, execute a subject, and tear each down:
387
+
388
+ ```ts
389
+ import { buildProgramDefinition, createProgram, createProgramManager } from '@orkestrel/program'
390
+
391
+ const definition = buildProgramDefinition('standard', 'Standard', qualification, rating)
392
+
393
+ const program = createProgram(definition)
394
+ const manager = createProgramManager({ programs: [definition] })
395
+
396
+ program.execute({ id: 'risk-1' })
397
+
398
+ program.destroy()
399
+ manager.destroy()
400
+ ```
401
+
402
+ ### Classes
403
+
404
+ | API | Kind | Summary |
405
+ | ---------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
406
+ | `Program` | class | Composes one qualifier and one rater over a shared reason engine, compiling one authored definition and executing single subjects or aggregate-aware batches. |
407
+ | `ProgramManager` | class | Manages compiled `ProgramInterface` programs in order, sharing one qualifier, rater, and reason engine across every program it compiles. |
408
+
409
+ The package has no entity named `Rater`. Rating remains a sibling concern.
410
+
411
+ ## Methods
412
+
413
+ #### `ProgramInterface`
414
+
415
+ The array overload is declared first, so the `execute` row's `Summary` carries the
416
+ batch form and the single-subject form returns one `ProgramResult` through the same
417
+ call. `execute` is the correct verb because it performs a composed workflow rather
418
+ than qualification or rating alone.
419
+
420
+ | Method | Returns | Summary |
421
+ | ---------- | ------------------------------------ | ---------------------------------------------------------------- |
422
+ | `execute` | `AggregateResult` or `ProgramResult` | Executes a subject list as one aggregate-aware batch. |
423
+ | `validate` | `ProgramValidationResult` | Validates this program's definition and every nested definition. |
424
+ | `destroy` | `void` | Destroys this program, idempotently. |
425
+
426
+ ```ts
427
+ const aggregate = program.execute(subjects)
428
+ const single = program.execute(subject)
429
+ const validation = program.validate()
430
+
431
+ program.destroy()
432
+ ```
433
+
434
+ After destroy, `execute` and `validate` throw `ProgramError('DESTROYED')`.
435
+
436
+ #### `ProgramManagerInterface`
437
+
438
+ The manager follows the singular/plural accessor and batch-removal conventions. The
439
+ id-list overload of `remove` is declared first, so that row's `Summary` carries the
440
+ list form; one id removes that program and returns a `boolean`, and no argument
441
+ removes every compiled program and returns `void`.
442
+
443
+ | Method | Returns | Summary |
444
+ | ---------- | ------------------------------- | --------------------------------------------------------- |
445
+ | `has` | `boolean` | Reports whether an id names a compiled program. |
446
+ | `program` | `ProgramInterface \| undefined` | Looks one compiled program up by id. |
447
+ | `programs` | `readonly ProgramInterface[]` | Returns every compiled program, in insertion order. |
448
+ | `add` | `ProgramInterface` | Compiles one definition and appends it to the collection. |
449
+ | `remove` | `boolean` or `void` | Removes every listed id, destroying each removed program. |
450
+ | `destroy` | `void` | Destroys this manager, idempotently. |
451
+
452
+ ```ts
453
+ const manager = createProgramManager()
454
+
455
+ manager.add(definition)
456
+ manager.has(definition.id) // true
457
+ manager.program(definition.id)?.execute(subject)
458
+ manager.programs()
459
+ manager.remove(definition.id)
460
+ manager.remove()
461
+ manager.destroy()
462
+ ```
463
+
464
+ The manager does not expose `execute`. Consumers deliberately choose a program, which
465
+ keeps execution and collection responsibilities separate.
466
+
467
+ ## Contract
468
+
469
+ ### Execution order
470
+
471
+ For one subject:
472
+
473
+ 1. assert subject shape and reserved-key safety
474
+ 2. qualify against the optional private aggregate projection
475
+ 3. emit `qualify`
476
+ 4. stop if qualification failed
477
+ 5. stop if global eligibility is `ineligible` or `referral`
478
+ 6. select rating lines from scoped eligibility
479
+ 7. skip rating when no line remains
480
+ 8. rate the selected lines against the original subject
481
+ 9. emit `rate`
482
+ 10. build notices
483
+ 11. derive status
484
+ 12. build the preliminary program result
485
+ 13. run optional authority against the private outcome projection
486
+ 14. build and emit limit determinations
487
+ 15. derive the optional decision
488
+ 16. emit `decide` when present
489
+ 17. emit `execute`
490
+
491
+ Every stop still returns a complete, successful business result when the terminal
492
+ eligibility itself was valid.
493
+
494
+ When `ProgramDefinition.rating` is omitted, line selection reads an empty line list,
495
+ rating is always skipped, and `rating` stays `undefined` for the whole execution —
496
+ the program is eligibility-only and the rater is never invoked, yet the remaining
497
+ steps (notices, status, authority, decision) still run.
498
+
499
+ ### Qualification is terminal
500
+
501
+ A globally `ineligible` or `referral` qualification — or a failed one — is terminal:
502
+ the rater is never called and `rating` stays `undefined`.
503
+
504
+ ```ts
505
+ const result = program.execute({ id: 'risk-2', licensed: false })
506
+
507
+ result.eligibility // 'ineligible'
508
+ result.rating // undefined — the rater was never called
509
+ result.success // true — a valid ineligible outcome still succeeds
510
+ ```
511
+
512
+ `ineligible` and `referral` are outcomes, not technical failures, so they do not make
513
+ `ProgramResult.success` false; only a qualification, rating, or authority error does.
514
+
515
+ ### Scoped eligibility
516
+
517
+ A scope names a rating-line id.
518
+
519
+ | Scoped result | Rating behavior | Program status |
520
+ | ------------- | --------------- | ---------------------------------------------------------- |
521
+ | absent | line selected | unchanged |
522
+ | `eligible` | line selected | unchanged |
523
+ | `ineligible` | line omitted | `conditional` when another line rates; otherwise `unrated` |
524
+ | `referral` | line omitted | `referral` |
525
+
526
+ This table applies when `rating` is authored — see Eligibility-only (Patterns) for
527
+ the omitted-rating case, where `unrated` never occurs. Line selection happens before
528
+ the first rating call — an excluded line is never evaluated merely to discard its
529
+ amount. A ruling or notice scope naming no line in `ProgramDefinition.rating?.lines`
530
+ (including every scope when `rating` is omitted entirely) is a hard authoring error —
531
+ `assertProgramDefinition` throws `ProgramError('MISSING')` at construction.
532
+
533
+ ### Conditions
534
+
535
+ An applied `condition` never removes a line — every eligible line still rates, and the
536
+ result becomes `conditional`. A scoped `restriction` behaves the same way for status:
537
+ it removes its own line but leaves the program `conditional` rather than globally
538
+ `ineligible` (an unscoped `restriction` already determines global ineligibility in
539
+ `Qualifier`).
540
+
541
+ ### Rating failures
542
+
543
+ A rating failure is not converted into ineligibility:
544
+
545
+ - qualification remains eligible
546
+ - the failed line amount remains absent
547
+ - rating evidence remains nested
548
+ - program status becomes `unrated`
549
+ - program success becomes false because execution encountered technical errors
550
+ - authority receives `rated: true` and `status: 'unrated'`
551
+ - authority still runs and may emit `limit` determinations; the `decision` is suppressed because `status` is `unrated` (a decision gate, listed later)
552
+
553
+ Scoped referral keeps global eligibility `eligible` while status remains `referral`, so a clean authority still yields an `approved` decision.
554
+
555
+ ### Notices
556
+
557
+ Notices are unconditional authored output. They:
558
+
559
+ - are emitted whether the subject is eligible or terminal
560
+ - may carry a scope for presentation
561
+ - never affect eligibility, status, line selection, or decision
562
+ - interpolate against the original subject
563
+ - are represented as `Determination` with `effect: 'notice'`
564
+
565
+ ### Authority
566
+
567
+ Authority is optional and runs last, over a private `outcome` projection of the
568
+ assembled result — its id, eligibility, status, whether it rated, total, and scoped
569
+ eligibility — never the mutable internal state of either sibling engine. Applied
570
+ authority rules become `limit` determinations. A `decision` is present only when
571
+ every gate holds:
572
+
573
+ 1. an authority definition exists on the program
574
+ 2. execution succeeded — qualification, rating (when it ran), and authority all produced no errors
575
+ 3. no `limit` determination applied
576
+ 4. status is not `unrated`
577
+
578
+ A technically-failed qualification therefore never yields a decision, even when an
579
+ authority definition exists and would otherwise fire cleanly.
580
+
581
+ The decision is deterministic in global eligibility (`eligible → approved`,
582
+ `ineligible → denied`, `referral → submitted`), preserving the distinction between
583
+ approved-with-conditions and denied — a scoped restriction can yield
584
+ `status: 'conditional'` with `decision: 'approved'`.
585
+
586
+ ### Aggregate execution
587
+
588
+ For a subject array:
589
+
590
+ 1. validate every subject and reserved key
591
+ 2. collect aggregate fields from `definition.aggregate`
592
+ 3. compute overall sums
593
+ 4. compute optional groups
594
+ 5. build each subject's aggregate projection
595
+ 6. execute subjects in input order
596
+ 7. tally every result by status
597
+ 8. run optional aggregate gates against the batch aggregate record (`count`, `sums`, `groups`)
598
+ 9. convert applied rules to batch `limit` determinations
599
+ 10. emit `aggregate`
600
+
601
+ The aggregate-gate evaluation's `trace` and `errors` fold into
602
+ `AggregateResult.trace` / `AggregateResult.errors` alongside every subject's own, and
603
+ `AggregateResult.success` additionally requires the gate evaluation to have produced
604
+ no errors — a gate evaluation failure fails the batch result even when every subject
605
+ execution succeeded.
606
+
607
+ Batch aggregation does not modify individual rating subjects.
608
+
609
+ ### Aggregate projection
610
+
611
+ The qualifier may read aggregate context through the reserved `aggregate` key on a
612
+ private subject copy:
613
+
614
+ ```ts
615
+ {
616
+ ...subject,
617
+ aggregate: { count, sums, group },
618
+ }
619
+ ```
620
+
621
+ The rater receives the original `subject`, never that private copy. This difference is
622
+ intentional and is covered by integration tests.
623
+
624
+ ### Reserved keys
625
+
626
+ Caller subjects must not contain `aggregate` or `outcome` — these keys are private
627
+ program namespaces used only for reason-engine projections. A subject that already
628
+ carries either key is rejected with `ProgramError('RESERVED')` before qualification.
629
+
630
+ ### Status
631
+
632
+ Status is explicit policy, not an opaque severity reducer. It resolves in this order:
633
+
634
+ 1. global ineligible
635
+ 2. global or scoped referral
636
+ 3. `ProgramDefinition.rating` is omitted (eligibility-only program) → `conditional` under an applied condition or scoped restriction, otherwise `eligible`, never `unrated`
637
+ 4. no successful rating, an authored rating with zero lines included → `unrated`
638
+ 5. applied condition or scoped restriction → `conditional`
639
+ 6. otherwise → `eligible`
640
+
641
+ ### Decision
642
+
643
+ `Decision` is authority output:
644
+
645
+ | Eligibility | Decision |
646
+ | ------------ | ----------- |
647
+ | `eligible` | `approved` |
648
+ | `ineligible` | `denied` |
649
+ | `referral` | `submitted` |
650
+
651
+ No decision is emitted without an authority definition.
652
+
653
+ ### Success
654
+
655
+ `ProgramResult.success` indicates execution integrity:
656
+
657
+ - valid ineligibility can succeed
658
+ - valid referral can succeed
659
+ - qualification errors fail
660
+ - rating errors fail
661
+ - authority errors fail
662
+ - notices do not fail
663
+ - a deliberately unrated result caused only by scoped exclusions can succeed
664
+
665
+ `AggregateResult.success` requires every subject execution to succeed and the batch
666
+ aggregate-gate evaluation (when configured) to have produced no errors.
667
+
668
+ ### Ownership
669
+
670
+ A standalone `Program`:
671
+
672
+ - reads the caller's definition once into an owned snapshot, runs construction
673
+ assertions against that copy, seals its plain-object graph, and exposes only the
674
+ sealed copy; a `Map`, `Set`, or `Date` reached through a reason `Check.value` is
675
+ cloned but its contents remain mutable because the seal cannot reach its internal
676
+ slots
677
+ - refuses a value that structured cloning cannot copy, or a non-empty typed array
678
+ that cannot be frozen, with `ProgramError('DEFINITION')` and the host error as its
679
+ cause
680
+ - borrows an injected reason engine or creates one shared quantitative-plus-logical engine
681
+ - injects that engine into any internally created qualifier and rater
682
+ - borrows independently injected qualifier and rater instances
683
+ - destroys only owned dependencies, its emitter last, and is idempotent — `destroy()`
684
+ sets the destroyed flag first, so a listener re-entering `destroy()` is a no-op
685
+ - when construction fails (an invalid definition under `options.validate`), tears down
686
+ everything already allocated — owned dependencies and the emitter, firing `destroy`
687
+ — before rethrowing
688
+
689
+ A `ProgramManager`:
690
+
691
+ - creates or borrows one shared quantitative-plus-logical reason engine
692
+ - injects the same qualifier, rater, and engine into every compiled program
693
+ - destroys programs first, then owned shared dependencies, then its emitter last —
694
+ reentrancy-safe the same way, the destroyed flag is set first
695
+ - when a seed program fails during construction, tears the manager down — draining
696
+ and destroying every program already compiled (each firing `remove` first), then
697
+ owned shared dependencies, then the emitter — before rethrowing the original error
698
+
699
+ ### Events
700
+
701
+ Single execution event order:
702
+
703
+ ```text
704
+ qualify
705
+ rate? only when at least one line is selected
706
+ determine* notices, then limits
707
+ decide? only when authority permits
708
+ execute
709
+ ```
710
+
711
+ Batch execution emits the per-subject events first, then aggregate determinations,
712
+ then `aggregate`. Events are synchronous, and listener failures are isolated by the
713
+ owned emitter.
714
+
715
+ ### Validation
716
+
717
+ `Program.validate` (`validateProgramDefinition`) checks:
718
+
719
+ 1. exact shape through `isProgramDefinition` — this alone establishes rating structure and authority/gates shape, so validate performs no redundant re-check of either
720
+ 2. non-empty id
721
+ 3. non-empty name
722
+ 4. nested qualification validation, delegated to the injected qualifier and prefixed `qualification:`
723
+ 5. duplicate rating-line ids (when a rating is authored)
724
+ 6. every qualification ruling scope names an existing rating line — when no rating is authored, any scope is an error, because no line exists to match
725
+ 7. duplicate notice ids
726
+ 8. every notice scope names an existing rating line — same empty-line rule when no rating is authored
727
+ 9. authority validated semantically by the shared reason engine, prefixed `authority:`
728
+ 10. aggregate fields are unique and non-empty
729
+ 11. aggregate `partition` is non-empty when present
730
+ 12. aggregate gates validated semantically by the shared reason engine, prefixed `aggregate:`
731
+
732
+ Always-on construction assertions run independently of `Program.validate` and of
733
+ `options.validate`: `assertProgramDefinition` rejects a missing scope reference
734
+ (`ProgramError('MISSING')`) and a duplicate rating-line or notice id
735
+ (`ProgramError('DUPLICATE')`) at construction, every time.
736
+
737
+ Qualification passes project their derivations under `qualification.<passId>` (the
738
+ qualifier reserves the `qualification` subject key), and authority runs against a
739
+ program-built private record whose only key is `outcome` — so an authored pass or
740
+ rule id can never collide with a caller subject key or either reserved working key;
741
+ there is no separate collision check to run.
742
+
743
+ Warnings stay conservative (validators do not attempt full logical theorem proving)
744
+ and include:
745
+
746
+ - a program whose rating defines no lines validates with the warning `Program rating has no lines`, and every eligible subject then resolves to `status: 'unrated'` because no line can rate (an omitted rating produces no such warning — it is eligibility-only by design, never `unrated`)
747
+ - aggregate gates defined without aggregate fields
748
+
749
+ ## Patterns
750
+
751
+ ### Globally ineligible
752
+
753
+ Execute a subject that qualification rejects globally:
754
+
755
+ ```ts
756
+ const result = program.execute({ id: 'risk-1', licensed: false })
757
+
758
+ result.qualification.eligibility // 'ineligible'
759
+ result.rating // undefined
760
+ result.status // 'ineligible'
761
+ ```
762
+
763
+ No quantitative reasoner call occurs.
764
+
765
+ ### Eligibility-only
766
+
767
+ Execute an eligible subject without an authored rating:
768
+
769
+ ```ts
770
+ const definition = buildProgramDefinition('gate-only', 'Gate only', qualification)
771
+ const program = createProgram(definition)
772
+
773
+ const result = program.execute({ id: 'risk-1', licensed: true })
774
+
775
+ result.rating // undefined — the rater is never invoked
776
+ result.status // 'eligible', never 'unrated'
777
+ ```
778
+
779
+ Omitting `rating` authors an eligibility-only program: qualification and its optional
780
+ authority still run in full, but rating and its rating-line references disappear from
781
+ the workflow entirely.
782
+
783
+ ### Rating-only
784
+
785
+ Execute an empty qualification before rating every authored line:
786
+
787
+ ```ts
788
+ const qualification = createQualificationDefinition('all', 'All risks', [])
789
+ const definition = buildProgramDefinition('rate-only', 'Rate only', qualification, rating)
790
+ const program = createProgram(definition)
791
+
792
+ const result = program.execute({ id: 'risk-1' })
793
+
794
+ result.qualification.eligibility // 'eligible' — an empty qualification qualifies every subject
795
+ result.rating?.lines.length // every authored line rates
796
+ ```
797
+
798
+ An empty qualification (no logical passes) qualifies every subject `eligible` with no
799
+ scoped exclusions, so every authored line rates unconditionally.
800
+
801
+ ### Scoped exclusion
802
+
803
+ Exclude the scoped rating line through a qualification restriction:
804
+
805
+ ```ts
806
+ const qualification = createQualificationDefinition(
807
+ 'property-qualification',
808
+ 'Property qualification',
809
+ [windGates],
810
+ {
811
+ rulings: [
812
+ createRuling('frame', 'wind-gates', 'frame', 'restriction', {
813
+ scope: 'wind',
814
+ message: 'Wind is unavailable for Frame construction',
815
+ }),
816
+ ],
817
+ },
818
+ )
819
+
820
+ const rating = buildRatingDefinition('property-rating', 'Property rating', [
821
+ buildLineDefinition('wind', 'Wind', windRate),
822
+ buildLineDefinition('exWind', 'Ex-Wind', exWindRate),
823
+ ])
824
+
825
+ const result = createProgram(
826
+ buildProgramDefinition('property', 'Property', qualification, rating),
827
+ ).execute({
828
+ id: 'risk-1',
829
+ construction: 'Frame',
830
+ })
831
+
832
+ result.rating?.lines.map((line) => line.id) // ['exWind']
833
+ result.status // 'conditional'
834
+ ```
835
+
836
+ The wind definition is not evaluated.
837
+
838
+ ### Scoped referral
839
+
840
+ Author a scoped referral that omits the matching rating line:
841
+
842
+ ```ts
843
+ createRuling('coastal-review', 'wind-gates', 'coastal-review', 'referral', {
844
+ scope: 'wind',
845
+ message: 'Wind requires underwriter review',
846
+ })
847
+ ```
848
+
849
+ The wind line is omitted and program status is `referral`.
850
+
851
+ ### Conditions
852
+
853
+ Author a condition that keeps every eligible rating line:
854
+
855
+ ```ts
856
+ createRuling('protective-device', 'gates', 'protective-device', 'condition', {
857
+ message: 'Install an approved protective device',
858
+ })
859
+ ```
860
+
861
+ All eligible lines rate. The result becomes `conditional`.
862
+
863
+ ### Notices
864
+
865
+ Attach an unconditional notice to a program definition:
866
+
867
+ ```ts
868
+ const notice = buildNotice('minimum', 'Minimum earned premium applies')
869
+
870
+ const definition = buildProgramDefinition('standard', 'Standard', qualification, rating, {
871
+ notices: [notice],
872
+ })
873
+ ```
874
+
875
+ ### Authority
876
+
877
+ Apply final authority to a conditional program result:
878
+
879
+ ```ts
880
+ const authority = createLogicalDefinition('authority', 'Final authority', [
881
+ createRule(
882
+ 'manual',
883
+ [createAtom(['outcome', 'status'], 'equals', 'conditional')],
884
+ createAtom('limited', 'equals', true),
885
+ {
886
+ name: 'Manual authority required',
887
+ description: 'Conditional outcomes require manual authority',
888
+ },
889
+ ),
890
+ ])
891
+
892
+ const definition = buildProgramDefinition('standard', 'Standard', qualification, rating, {
893
+ authority,
894
+ })
895
+ ```
896
+
897
+ A conditional result receives a `limit` determination and no decision.
898
+
899
+ ### Aggregate qualification
900
+
901
+ Qualify each subject against its private aggregate projection:
902
+
903
+ ```ts
904
+ const aggregate = buildAggregateDefinition(['total'], { partition: 'location' })
905
+
906
+ const qualification = createQualificationDefinition(
907
+ 'portfolio-qualification',
908
+ 'Portfolio qualification',
909
+ [
910
+ createLogicalDefinition('aggregate-gates', 'Aggregate gates', [
911
+ createRule(
912
+ 'location-cap',
913
+ [createAtom(['aggregate', 'group', 'sums', 'total'], 'above', 5_000_000)],
914
+ createAtom('blocked', 'equals', true),
915
+ ),
916
+ ]),
917
+ ],
918
+ {
919
+ rulings: [
920
+ createRuling('location-cap', 'aggregate-gates', 'location-cap', 'restriction', {
921
+ message: 'Location total exceeds the program maximum',
922
+ }),
923
+ ],
924
+ },
925
+ )
926
+ ```
927
+
928
+ Each subject qualifies against its own group projection while rating still receives
929
+ the original subject.
930
+
931
+ ### Aggregate gates
932
+
933
+ Apply the aggregate-gate logical definition to the completed batch aggregate:
934
+
935
+ ```ts
936
+ const gates = createLogicalDefinition('batch-gates', 'Batch gates', [
937
+ createRule(
938
+ 'portfolio-cap',
939
+ [createAtom(['aggregate', 'sums', 'total'], 'above', 20_000_000)],
940
+ createAtom('limited', 'equals', true),
941
+ ),
942
+ ])
943
+
944
+ const aggregate = buildAggregateDefinition(['total'], { gates })
945
+ ```
946
+
947
+ These gates create batch determinations. They do not retroactively change individual
948
+ qualification or rating results.
949
+
950
+ ### Shared dependencies
951
+
952
+ Inject caller-owned qualifier, rater, and reason instances into a manager:
953
+
954
+ ```ts
955
+ const reason = createReason({
956
+ reasoners: [createQuantitativeReasoner(), createLogicalReasoner()],
957
+ bail: false,
958
+ })
959
+ const qualifier = createQualifier({ engine: reason })
960
+ const rater = createRater({ engine: reason })
961
+
962
+ const manager = createProgramManager({ qualifier, rater, engine: reason, programs: definitions })
963
+
964
+ manager.destroy()
965
+
966
+ // Injected dependencies remain caller-owned.
967
+ qualifier.destroy()
968
+ rater.destroy()
969
+ reason.destroy()
970
+ ```
971
+
972
+ Build an injected shared engine with `bail: false`, as the preceding example shows — matching the
973
+ engine `Program` creates when none is injected. Ordinary evaluation failures (a failed
974
+ factor, an unresolvable field) surface as nested result evidence regardless of `bail`;
975
+ `bail` governs only a reasoner's own internal throw, which `bail: true` rethrows
976
+ through `program.execute` as the sibling's error instead of nesting it. An engine
977
+ missing a required reasoner always throws on dispatch, bypassing `bail` entirely —
978
+ `Program.validate` reports that misconfiguration up front.
979
+
980
+ ### Observing
981
+
982
+ Subscribe to every program event through the typed emitter hooks:
983
+
984
+ ```ts
985
+ const program = createProgram(definition, {
986
+ on: {
987
+ qualify: (result) => audit.qualification(result),
988
+ rate: (result) => audit.rating(result),
989
+ determine: (result) => audit.determination(result),
990
+ decide: (decision, result) => audit.decision(decision, result),
991
+ execute: (result) => audit.program(result),
992
+ aggregate: (result) => audit.aggregate(result),
993
+ },
994
+ error: (error, event) => audit.listenerError(error, event),
995
+ })
996
+ ```
997
+
998
+ ## Tests
999
+
1000
+ Tests mirror the source structure under `tests/src/core` —
1001
+ `validators.test.ts`, `helpers.test.ts`, and `factories.test.ts` for the centralized
1002
+ surfaces, `programs/Program.test.ts` and `programs/ProgramManager.test.ts` for the
1003
+ entities, and the reserved `integration.test.ts` for cross-entity composition — and
1004
+ use real qualifier, rater, and reason instances rather than mocks.
1005
+
1006
+ ### Program cases
1007
+
1008
+ The program suite proves the composed workflow: it qualifies before rating; never
1009
+ rates a globally ineligible, referred, or failed subject; rates only eligible scopes
1010
+ and skips rating when no scope remains; passes the original subject to the rater;
1011
+ keeps the aggregate projection private to qualification; preserves nested
1012
+ qualification findings and rating worksheets; derives conditional, referral, and
1013
+ unrated status; runs authority last and omits the decision when a limit applies,
1014
+ status is `unrated`, or execution technically failed; maps eligibility to decision;
1015
+ emits events in contract order; destroys only owned dependencies; rejects reserved
1016
+ subject keys and post-destroy calls; and never mutates definitions, subjects, or
1017
+ sibling results.
1018
+
1019
+ The hardened suite additionally proves: an aggregate-gate evaluation error fails and
1020
+ surfaces on `AggregateResult`; a technically-failed qualification never yields a
1021
+ decision even with a clean authority; eligibility-only programs resolve status and
1022
+ decisions correctly, never call the rater, and still tally correctly in a batch;
1023
+ batch execution rejects a reserved-key subject before any work runs; an all-lines
1024
+ scoped-out subject resolves `unrated`; listener throws are isolated through each
1025
+ entity's `error` handler; `destroy`/`execute` reentrancy from within a listener, and
1026
+ `ProgramManager` reentrancy from a `remove` listener, are all no-ops or safe; a
1027
+ construction failure tears down everything already allocated (firing `destroy` /
1028
+ `remove` hooks) while leaving injected dependencies untouched; duplicate rating-line
1029
+ and notice ids are rejected at construction even under `validate: false`; hostile
1030
+ subjects carrying `__proto__` / `constructor` keys never pollute a prototype;
1031
+ aggregate numeric edges (`NaN`, `Infinity`, non-numeric, and absent values all
1032
+ contribute zero; nested field paths sum correctly) plus group-key coercion
1033
+ collisions, first-seen group order, large batches, and duplicate subject ids are all
1034
+ covered; validation branch messages match exactly; notice interpolation handles a
1035
+ missing token, nested paths, and en-US thousands grouping; and a limit determination
1036
+ for a description-less rule omits `message`.
1037
+
1038
+ ### No-rate and original-subject proofs
1039
+
1040
+ A recorder-backed rater — a real `RaterInterface` that records every subject and line
1041
+ selection it receives without rating — proves the load-bearing invariants directly: a
1042
+ globally ineligible subject produces no recorded call, an aggregate execution records
1043
+ only original subjects (never the private `aggregate` projection), and a scoped
1044
+ restriction records only the surviving line ids. The recorder's leading-underscore
1045
+ callback parameters are justified callback-conformance bindings in the shared test
1046
+ collaborator, not production source.
1047
+
1048
+ ### Batch and manager cases
1049
+
1050
+ The batch suite proves overall sums, first-seen group order, per-subject group
1051
+ projections, complete zero tallies, aggregate gates running once, and an empty batch.
1052
+ The manager suite proves ordered seeding, duplicate-id rejection, defensive program
1053
+ arrays, removal of one / listed / all programs, and that one shared qualifier, rater,
1054
+ and engine back every program.
1055
+
1056
+ ### Shared-engine ownership
1057
+
1058
+ An integration test injects one shared reason engine (plus a qualifier and rater over
1059
+ it) into a manager and asserts the injected trio survives `manager.destroy()`, while a
1060
+ standalone program destroys its own engine exactly once and idempotently.
1061
+
1062
+ ### Public parity
1063
+
1064
+ `tests/src/core/integration.test.ts` asserts the barrel exports only program
1065
+ concerns — it must not re-export quantitative reason or rating implementation
1066
+ symbols such as `QuantitativeReasoner`, `Factor`, `WorksheetFactor`, or `Rater`. It
1067
+ consumes `RaterInterface` without claiming ownership of `Rater`.
1068
+
1069
+ ### Gates
1070
+
1071
+ Run scoped gates before commit:
1072
+
1073
+ ```text
1074
+ npm run format
1075
+ npm run lint
1076
+ npm run check
1077
+ npm run build
1078
+ npm run test:src:core
1079
+ ```
1080
+
1081
+ [`tests/guides.test.ts`](../tests/guides.test.ts) proves guide parity: every
1082
+ backticked export resolves, every `ProgramInterface` and `ProgramManagerInterface`
1083
+ method is documented, and the equality gate holds — every `Summary` cell against its
1084
+ declaration's description paragraph, the titled `Compile a program and a manager`
1085
+ fence against the `@example` block of that title, and the README pitch against this
1086
+ guide's tagline. It also runs the flagship fences and asserts the values their
1087
+ comments claim.
1088
+
1089
+ ## Practices
1090
+
1091
+ 1. Define `types.ts` before implementation.
1092
+ 2. Keep `execute` as the program verb.
1093
+ 3. Keep `qualify` and `rate` on their owning entities.
1094
+ 4. Qualify before selecting or rating lines.
1095
+ 5. Stop on global ineligibility, referral, or qualification failure.
1096
+ 6. Select scopes before the first rater call.
1097
+ 7. Pass the original subject to the rater.
1098
+ 8. Keep aggregate and outcome projections private.
1099
+ 9. Preserve sibling results as nested values.
1100
+ 10. Derive status explicitly, not through opaque precedence reduction.
1101
+ 11. Treat decisions as authority output.
1102
+ 12. Keep notices informational.
1103
+ 13. Share dependencies deliberately and document ownership.
1104
+ 14. Return fresh objects and defensive arrays.
1105
+ 15. Reserve throws for caller misuse and lifecycle errors.
1106
+ 16. Add no unsolicited dependency.
1107
+ 17. Keep helpers pure and self-descriptive.
1108
+ 18. Keep program management separate from execution.
1109
+ 19. Keep docs and exports bijective.
1110
+ 20. Add no compatibility re-exports for moved symbols.