@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.
- package/dist/bin/main.js +67 -44
- package/dist/bin/main.js.map +1 -1
- package/dist/host/agents/templates/brief.md +9 -0
- package/dist/host/claude/agents/orkestrel.md +4 -4
- package/dist/host/claude/rules/names.md +15 -0
- package/dist/host/claude/rules/tests.md +33 -4
- package/dist/host/claude/rules/workspace.md +14 -2
- package/dist/host/dotfiles/prettierignore +3 -0
- package/dist/host/guides/README.md +65 -0
- package/dist/host/guides/abort.md +169 -0
- package/dist/host/guides/agent.md +1509 -0
- package/dist/host/guides/brief.md +1266 -0
- package/dist/host/guides/browser.md +2200 -0
- package/dist/host/guides/budget.md +196 -0
- package/dist/host/guides/codec.md +519 -0
- package/dist/host/guides/console.md +785 -0
- package/dist/host/guides/contract.md +1193 -0
- package/dist/host/guides/csv.md +541 -0
- package/dist/host/guides/database.md +2518 -0
- package/dist/host/guides/emitter.md +233 -0
- package/dist/host/guides/form.md +1791 -0
- package/dist/host/guides/html.md +717 -0
- package/dist/host/guides/indexeddb.md +505 -0
- package/dist/host/guides/interpret.md +1029 -0
- package/dist/host/guides/lsp.md +515 -0
- package/dist/host/guides/markdown.md +964 -0
- package/dist/host/guides/mcp.md +5554 -0
- package/dist/host/guides/middleware.md +927 -0
- package/dist/host/guides/msg.md +440 -0
- package/dist/host/guides/ndjson.md +120 -0
- package/dist/host/guides/ollama.md +380 -0
- package/dist/host/guides/pool.md +280 -0
- package/dist/host/guides/probe.md +1210 -0
- package/dist/host/guides/process.md +1620 -0
- package/dist/host/guides/program.md +1110 -0
- package/dist/host/guides/qualifier.md +854 -0
- package/dist/host/guides/queue.md +370 -0
- package/dist/host/guides/rater.md +330 -0
- package/dist/host/guides/reason.md +1122 -0
- package/dist/host/guides/relation.md +373 -0
- package/dist/host/guides/router.md +753 -0
- package/dist/host/guides/scaffold.md +192 -31
- package/dist/host/guides/sea.md +383 -0
- package/dist/host/guides/server.md +752 -0
- package/dist/host/guides/sqlite.md +330 -0
- package/dist/host/guides/sse.md +187 -0
- package/dist/host/guides/supervisor.md +4890 -0
- package/dist/host/guides/table.md +1556 -0
- package/dist/host/guides/template.md +280 -0
- package/dist/host/guides/terminal.md +1145 -0
- package/dist/host/guides/test.md +2969 -0
- package/dist/host/guides/timeout.md +252 -0
- package/dist/host/guides/tool.md +311 -0
- package/dist/host/guides/toolbox.md +1038 -0
- package/dist/host/guides/websocket.md +282 -0
- package/dist/host/guides/worker.md +615 -0
- package/dist/host/guides/workflow.md +1507 -0
- package/dist/host/guides/workspace.md +595 -0
- package/dist/host/manifest.json +1218 -10
- package/dist/host/tests/policy.test.ts +279 -2
- package/dist/host/tests/setupPolicy.ts +437 -6
- package/dist/src/core/index.cjs +38 -16
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +33 -9
- package/dist/src/core/index.d.ts +33 -9
- package/dist/src/core/index.js +37 -17
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +1750 -1567
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +106 -24
- package/dist/src/server/index.d.ts +106 -24
- package/dist/src/server/index.js +1751 -1570
- package/dist/src/server/index.js.map +1 -1
- 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.
|