@orkestrel/rater 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,52 +1,21 @@
1
- import { arrayOf, isFiniteNumber, isJSONValue, isRecord, isString, literalOf, recordOf, resolveField, unionOf, whereOf } from "@orkestrel/contract";
2
- import { assignField, buildErrorResult, createEvaluator, createLogicalReasoner, createQuantitativeReasoner, createReason, extractAtoms, extractConclusions, formatField, isBounds, isFieldPath, isLogicalDefinition, isQuantitativeDefinition, mergeSubjects } from "@orkestrel/reason";
1
+ import { arrayOf, isArray, isJSONValue, isString, literalOf, recordOf } from "@orkestrel/contract";
2
+ import { buildErrorResult, createQuantitativeReasoner, createReason, formatField, isQuantitativeDefinition, isSubject } from "@orkestrel/reason";
3
3
  import { Emitter } from "@orkestrel/emitter";
4
- //#region src/core/constants.ts
5
- /** Default definition validation policy for `ProgramManager.add`. */
6
- var DEFAULT_RATER_VALIDATE = true;
7
- /** Eligibility severity order, most severe first — the {@link combineEligibilities} scan order. */
8
- var ELIGIBILITY_PRECEDENCE = Object.freeze([
9
- "ineligible",
10
- "referral",
11
- "eligible"
12
- ]);
13
- /** Status tally precedence order — least to most resolved. */
14
- var STATUS_PRECEDENCE = Object.freeze([
15
- "ineligible",
16
- "referral",
17
- "conditional",
18
- "unrated",
19
- "eligible"
20
- ]);
21
- /** The deterministic authority decision for each eligibility. */
22
- var ELIGIBILITY_DECISIONS = Object.freeze({
23
- eligible: "approved",
24
- ineligible: "denied",
25
- referral: "submitted"
26
- });
27
- /** The direct eligibility impact of an applied determination, by its effect. */
28
- var EFFECT_ELIGIBILITIES = Object.freeze({
29
- restriction: "ineligible",
30
- referral: "referral",
31
- condition: void 0,
32
- notice: void 0,
33
- limit: void 0
34
- });
35
- /** The reserved working-subject key a batch's aggregate projection is written under. */
36
- var AGGREGATE_KEY = "aggregate";
37
- /** The reserved working-subject key an authority's outcome projection is written under. */
38
- var OUTCOME_KEY = "outcome";
39
- //#endregion
40
4
  //#region src/core/errors.ts
41
5
  /**
42
6
  * A coded programmer error thrown by the rating layer.
43
7
  *
44
8
  * @remarks
45
- * `DUPLICATE` — a program id collision on `ProgramManager.add`. `MISSING` — an
46
- * unknown authored line reference at compile time.
47
- * `DEFINITION` — a program definition failed `isProgramDefinition`. `MISMATCH` —
48
- * a rated subject is not a record, or uses a reserved working-subject key.
9
+ * `DEFINITION` — the `rate` input failed both the array-of-lines and rating
10
+ * definition validation. `MISMATCH` — a rated subject is not a record.
49
11
  * `DESTROYED` — use of a destroyed entity.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { RaterError } from '@orkestrel/rater'
16
+ *
17
+ * throw new RaterError('MISMATCH', 'Subject must be a record')
18
+ * ```
50
19
  */
51
20
  var RaterError = class extends Error {
52
21
  code;
@@ -58,215 +27,117 @@ var RaterError = class extends Error {
58
27
  this.context = context;
59
28
  }
60
29
  };
61
- /** Narrow a caught value to a {@link RaterError}. */
62
- function isRaterError(value) {
63
- return value instanceof RaterError;
64
- }
65
- //#endregion
66
- //#region src/core/validators.ts
67
- /** Determine whether a value is an {@link Eligibility} literal. */
68
- var isEligibility = literalOf("eligible", "ineligible", "referral");
69
- /** Determine whether a value is a {@link Decision} literal. */
70
- var isDecision = literalOf("approved", "denied", "submitted");
71
- /** Determine whether a value is a {@link Status} literal. */
72
- var isStatus = literalOf("ineligible", "referral", "conditional", "unrated", "eligible");
73
- /** Determine whether a value is an {@link Effect} literal. */
74
- var isEffect = literalOf("restriction", "referral", "condition", "notice", "limit");
75
- /** Determine whether a value is a {@link Stage} literal. */
76
- var isStage = literalOf("factor", "group", "total");
77
- /** Determine whether a value is an exact {@link Ruling} record. */
78
- function isRuling(value) {
79
- return recordOf({
80
- effect: isEffect,
81
- line: isString,
82
- message: isString
83
- }, ["line", "message"])(value);
84
- }
85
- /** Determine whether a value is an exact {@link Notice} record. */
86
- function isNotice(value) {
87
- return recordOf({
88
- id: isString,
89
- message: isString,
90
- line: isString
91
- }, ["line"])(value);
92
- }
93
- /** Determine whether a value is an exact {@link PassDefinition} record. */
94
- function isPassDefinition(value) {
95
- return recordOf({
96
- line: isString,
97
- definition: unionOf(isLogicalDefinition, isQuantitativeDefinition)
98
- }, ["line"])(value);
99
- }
100
- /** Determine whether a value is an exact {@link LineDefinition} record. */
101
- function isLineDefinition(value) {
102
- return recordOf({
103
- id: isString,
104
- name: isString,
105
- description: isString,
106
- rate: isQuantitativeDefinition,
107
- metadata: isJSONValue
108
- }, ["description", "metadata"])(value);
109
- }
110
- /** Determine whether a value is an exact {@link AggregateDefinition} record. */
111
- function isAggregateDefinition(value) {
112
- return recordOf({
113
- fields: arrayOf(isFieldPath),
114
- by: isFieldPath,
115
- gates: isLogicalDefinition
116
- }, ["by", "gates"])(value);
117
- }
118
- /** Determine whether a value is a rule-id-keyed {@link Ruling} record. */
119
- var isRulings = whereOf(isRecord, (record) => Object.values(record).every(isRuling));
120
- /** Determine whether a value is an exact {@link ProgramDefinition} record. */
121
- function isProgramDefinition(value) {
122
- return recordOf({
123
- id: isString,
124
- name: isString,
125
- description: isString,
126
- passes: arrayOf(isPassDefinition),
127
- lines: arrayOf(isLineDefinition),
128
- rulings: isRulings,
129
- notices: arrayOf(isNotice),
130
- authority: isLogicalDefinition,
131
- aggregate: isAggregateDefinition,
132
- metadata: isJSONValue
133
- }, [
134
- "description",
135
- "passes",
136
- "rulings",
137
- "notices",
138
- "authority",
139
- "aggregate",
140
- "metadata"
141
- ])(value);
142
- }
143
- //#endregion
144
- //#region src/core/helpers.ts
145
30
  /**
146
- * Interpolate `{{dotted.path}}` tokens in a message template against a record.
31
+ * Narrow a caught value to a {@link RaterError}.
147
32
  *
148
- * @remarks
149
- * Each token is split on `.` into a {@link FieldPath} array and resolved with
150
- * the contracts {@link resolveField} (a plain string field is ONE key, never
151
- * dot-split — the split here is the token-to-path bridge). A finite number
152
- * renders with `en-US` thousand grouping (`5010` → `5,010`); any other resolved
153
- * value String-coerces. An UNRESOLVED path (the resolved value is `undefined`)
154
- * renders as the empty string — the deterministic "nothing to show" rule.
155
- *
156
- * @param template - The message template carrying `{{dotted.path}}` tokens
157
- * @param record - The record tokens resolve against
158
- * @returns The template with every token replaced
33
+ * @param value - The caught value to test
34
+ * @returns `true` when `value` is a `RaterError`
159
35
  *
160
36
  * @example
161
37
  * ```ts
162
- * import { interpolateMessage } from '@orkestrel/rater'
38
+ * import { isRaterError, RaterError } from '@orkestrel/rater'
163
39
  *
164
- * interpolateMessage('Limit is {{limit}}', { limit: 5010 }) // 'Limit is 5,010'
165
- * interpolateMessage('Missing {{gone}}', {}) // 'Missing '
40
+ * try {
41
+ * throw new RaterError('DESTROYED', 'Rater has been destroyed')
42
+ * } catch (error) {
43
+ * if (isRaterError(error)) error.code // 'DESTROYED'
44
+ * }
166
45
  * ```
167
46
  */
168
- function interpolateMessage(template, record) {
169
- return template.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_match, path) => {
170
- const value = resolveField(record, path.split("."));
171
- if (value === void 0) return "";
172
- if (isFiniteNumber(value)) return value.toLocaleString("en-US");
173
- return String(value);
174
- });
47
+ function isRaterError(value) {
48
+ return value instanceof RaterError;
175
49
  }
50
+ //#endregion
51
+ //#region src/core/validators.ts
176
52
  /**
177
- * Describe a {@link Premise} comparison as a display-neutral verb phrase.
53
+ * Determine whether a value is a {@link Stage} literal.
178
54
  *
179
- * @param comparison - The comparison to describe
180
- * @returns A display-neutral phrase
55
+ * @param value - The value to test
56
+ * @returns `true` when `value` is `'factor'`, `'group'`, or `'total'`
181
57
  *
182
58
  * @example
183
59
  * ```ts
184
- * import { describeComparison } from '@orkestrel/rater'
60
+ * import { isStage } from '@orkestrel/rater'
185
61
  *
186
- * describeComparison('above') // 'is more than'
62
+ * isStage('group') // true
63
+ * isStage('step') // false
187
64
  * ```
188
65
  */
189
- function describeComparison(comparison) {
190
- switch (comparison) {
191
- case "equals": return "is";
192
- case "not": return "is not";
193
- case "above": return "is more than";
194
- case "below": return "is less than";
195
- case "from": return "is at least";
196
- case "to": return "is at most";
197
- case "any": return "is any of";
198
- case "none": return "is none of";
199
- case "between": return "is between";
200
- case "outside": return "is outside";
201
- }
202
- }
66
+ var isStage = literalOf("factor", "group", "total");
203
67
  /**
204
- * Render a structured or scalar value display-neutrally.
68
+ * Determine whether a value is an exact {@link LineDefinition} record.
205
69
  *
206
70
  * @remarks
207
- * An array renders as its elements, each `String`-coerced, joined with
208
- * `', '`. A Bounds-shaped record (a reason `Bounds` — the `between`/`outside`
209
- * comparison's expected value shape) renders its present sides joined with
210
- * the word `and`; an absent side is omitted. A finite number and everything
211
- * else fall back to plain `String()` coercion — a finite number is NOT
212
- * grouped here (that grouping is {@link interpolateMessage}'s own,
213
- * message-token-only behavior).
71
+ * Total guard (AGENTS §14): adversarial input (cycles, hostile prototypes)
72
+ * returns `false`, never throws. The record shape is EXACT — an extra key fails.
214
73
  *
215
- * @param value - The value to render
216
- * @returns A display-neutral rendering
74
+ * @param value - The value to test
75
+ * @returns `true` when `value` is a `LineDefinition`
217
76
  *
218
77
  * @example
219
78
  * ```ts
220
- * import { describeValue } from '@orkestrel/rater'
79
+ * import { quantitativeDefinition } from '@orkestrel/reason'
80
+ * import { isLineDefinition } from '@orkestrel/rater'
221
81
  *
222
- * describeValue([18, 25, 40]) // '18, 25, 40'
223
- * describeValue({ minimum: 18, maximum: 65 }) // '18 and 65'
224
- * describeValue(42) // '42'
82
+ * isLineDefinition({ id: 'base', name: 'Base', rate: quantitativeDefinition('base', 'Base', []) }) // true
225
83
  * ```
226
84
  */
227
- function describeValue(value) {
228
- if (Array.isArray(value)) return value.map((entry) => String(entry)).join(", ");
229
- if (isBounds(value)) {
230
- const sides = [];
231
- if (value.minimum !== void 0) sides.push(String(value.minimum));
232
- if (value.maximum !== void 0) sides.push(String(value.maximum));
233
- return sides.join(" and ");
234
- }
235
- return String(value);
85
+ function isLineDefinition(value) {
86
+ return recordOf({
87
+ id: isString,
88
+ name: isString,
89
+ description: isString,
90
+ rate: isQuantitativeDefinition,
91
+ metadata: isJSONValue
92
+ }, ["description", "metadata"])(value);
236
93
  }
237
94
  /**
238
- * Render one {@link Premise} into a display-neutral sentence.
95
+ * Determine whether a value is an exact {@link RatingDefinition} record.
239
96
  *
240
- * @param entry - The premise to render
241
- * @param labels - Optional field-to-label overrides, keyed by dot-joined field
242
- * @returns A display-neutral sentence
97
+ * @remarks
98
+ * Total guard (AGENTS §14): adversarial input (cycles, hostile prototypes)
99
+ * returns `false`, never throws. The record shape is EXACT — an extra key fails.
100
+ *
101
+ * @param value - The value to test
102
+ * @returns `true` when `value` is a `RatingDefinition`
243
103
  *
244
104
  * @example
245
105
  * ```ts
246
- * import { describePremise } from '@orkestrel/rater'
106
+ * import { isRatingDefinition } from '@orkestrel/rater'
247
107
  *
248
- * describePremise({ field: 'age', comparison: 'above', expected: 18, actual: 25, met: true })
249
- * // 'age is more than 18 ? met'
108
+ * isRatingDefinition({ id: 'r1', name: 'Rating', lines: [] }) // true
250
109
  * ```
251
110
  */
252
- function describePremise(entry, labels) {
253
- const status = entry.met === void 0 ? "unknown" : entry.met ? "met" : "not met";
254
- if (entry.field === void 0 || entry.comparison === void 0) return `${entry.description ?? "Premise"} ? ${status}`;
255
- const field = formatField(entry.field);
256
- const label = labels?.[field] ?? entry.label ?? field;
257
- const expected = entry.expected === void 0 ? "" : ` ${describeValue(entry.expected)}`;
258
- return `${label} ${describeComparison(entry.comparison)}${expected} ? ${status}`;
111
+ function isRatingDefinition(value) {
112
+ return recordOf({
113
+ id: isString,
114
+ name: isString,
115
+ description: isString,
116
+ lines: arrayOf(isLineDefinition),
117
+ metadata: isJSONValue
118
+ }, ["description", "metadata"])(value);
259
119
  }
120
+ //#endregion
121
+ //#region src/core/helpers.ts
260
122
  /**
261
- * Build a {@link Premise} from an evaluated {@link Check}.
123
+ * Build an {@link Evidence} row from an evaluated {@link Check}.
262
124
  *
263
125
  * @param check - The authored check
264
126
  * @param actual - The resolved subject value
265
127
  * @param met - Whether the check was met (absent when not yet evaluated)
266
128
  * @param labels - Optional field-to-label overrides, keyed by dot-joined field
267
- * @returns A fresh premise
129
+ * @returns A fresh evidence row
130
+ *
131
+ * @example
132
+ * ```ts
133
+ * import { check } from '@orkestrel/reason'
134
+ * import { evidenceCheck } from '@orkestrel/rater'
135
+ *
136
+ * const entry = check('age', 'above', 18)
137
+ * evidenceCheck(entry, 25, true) // { field: 'age', comparison: 'above', expected: 18, actual: 25, met: true }
138
+ * ```
268
139
  */
269
- function premiseCheck(check, actual, met, labels) {
140
+ function evidenceCheck(check, actual, met, labels) {
270
141
  const field = formatField(check.field);
271
142
  return {
272
143
  field: check.field,
@@ -278,77 +149,45 @@ function premiseCheck(check, actual, met, labels) {
278
149
  };
279
150
  }
280
151
  /**
281
- * Build premises from a quantitative factor's authored checks and evaluated
282
- * check results.
152
+ * Build evidence rows from a quantitative factor's authored checks and
153
+ * evaluated check results.
283
154
  *
284
155
  * @param checks - The factor's authored checks
285
156
  * @param results - The corresponding {@link CheckResult}s, in the same order
286
157
  * @param labels - Optional field-to-label overrides, keyed by dot-joined field
287
- * @returns A fresh list of premises, one per authored check
158
+ * @returns A fresh list of evidence rows, one per authored check
159
+ *
160
+ * @example
161
+ * ```ts
162
+ * import { check } from '@orkestrel/reason'
163
+ * import { checkEvidence } from '@orkestrel/rater'
164
+ *
165
+ * const checks = [check('age', 'above', 18)]
166
+ * checkEvidence(checks, [{ field: 'age', met: true, actual: 25 }])
167
+ * ```
288
168
  */
289
- function checkPremises(checks, results, labels) {
169
+ function checkEvidence(checks, results, labels) {
290
170
  return (checks ?? []).map((check, index) => {
291
171
  const result = results?.[index];
292
- return premiseCheck(check, result?.actual, result?.met, labels);
172
+ return evidenceCheck(check, result?.actual, result?.met, labels);
293
173
  });
294
174
  }
295
175
  /**
296
- * Describe a logical {@link Expression} tree without atom-specific evidence.
297
- *
298
- * @param expression - The expression to describe
299
- * @param labels - Optional field-to-label overrides, keyed by dot-joined field
300
- * @returns A display-neutral description
301
- */
302
- function describeExpression(expression, labels) {
303
- if (expression.form === "atom") return describePremise(premiseCheck(expression.check, void 0, void 0, labels), labels);
304
- const descriptions = expression.operands.map((operand) => describeExpression(operand, labels));
305
- return `${expression.operator} (${descriptions.join(", ")})`;
306
- }
307
- /**
308
- * Build rich premises for one fired {@link Rule} by walking its premise atoms
309
- * and re-evaluating each against the working subject.
310
- *
311
- * @remarks
312
- * A reason {@link RuleResult} carries only booleans, so this is rater's own
313
- * premise-audit projection: each authored premise expression is flattened to
314
- * its atom leaves via {@link extractAtoms}, and each leaf's {@link Check} is
315
- * re-evaluated through the injected `evaluator`. A membership check (`any` /
316
- * `none`) over an EMPTY array value is content-free — a tautology or
317
- * contradiction that reads as "is none of —" in every surface — and is skipped.
318
- *
319
- * @param rule - The authored rule
320
- * @param working - The working subject to evaluate against
321
- * @param evaluator - The shared reason check evaluator
322
- * @param labels - Optional field-to-label overrides, keyed by dot-joined field
323
- * @returns A fresh, ordered list of rich premises
324
- */
325
- function logicalPremises(rule, working, evaluator, labels) {
326
- const output = [];
327
- for (const premise of rule.premises) for (const atom of extractAtoms(premise)) {
328
- const { check } = atom;
329
- if ((check.operator === "any" || check.operator === "none") && Array.isArray(check.value) && check.value.length === 0) continue;
330
- const result = evaluator.evaluate(check, working);
331
- output.push(premiseCheck(check, result.actual, result.met, labels));
332
- }
333
- return output;
334
- }
335
- /**
336
- * Locate an authored {@link Rule} by id.
337
- *
338
- * @param definition - The logical definition to search
339
- * @param id - The rule id
340
- * @returns The matching rule, or `undefined`
341
- */
342
- function findRule(definition, id) {
343
- return definition.rules.find((rule) => rule.id === id);
344
- }
345
- /**
346
176
  * Join one authored quantitative factor to its evaluated {@link FactorResult}.
347
177
  *
348
178
  * @param definition - The authored factor
349
179
  * @param results - The group's factor results
350
180
  * @param labels - Optional field-to-label overrides, keyed by dot-joined field
351
181
  * @returns A fresh worksheet factor
182
+ *
183
+ * @example
184
+ * ```ts
185
+ * import { fieldFactor } from '@orkestrel/reason'
186
+ * import { worksheetFactor } from '@orkestrel/rater'
187
+ *
188
+ * const factor = fieldFactor('seats', 'seats')
189
+ * worksheetFactor(factor, [{ id: 'seats', applied: true, value: 10 }])
190
+ * ```
352
191
  */
353
192
  function worksheetFactor(definition, results, labels) {
354
193
  const result = results.find((entry) => entry.id === definition.id);
@@ -358,7 +197,7 @@ function worksheetFactor(definition, results, labels) {
358
197
  ...definition.description === void 0 ? {} : { description: definition.description },
359
198
  applied: result?.applied ?? false,
360
199
  ...result?.value === void 0 ? {} : { value: result.value },
361
- premises: checkPremises(definition.checks, result?.checks, labels)
200
+ evidence: checkEvidence(definition.checks, result?.checks, labels)
362
201
  };
363
202
  }
364
203
  /**
@@ -368,6 +207,15 @@ function worksheetFactor(definition, results, labels) {
368
207
  * @param results - The definition's group results
369
208
  * @param labels - Optional field-to-label overrides, keyed by dot-joined field
370
209
  * @returns A fresh worksheet group
210
+ *
211
+ * @example
212
+ * ```ts
213
+ * import { factorGroup, staticFactor } from '@orkestrel/reason'
214
+ * import { worksheetGroup } from '@orkestrel/rater'
215
+ *
216
+ * const group = factorGroup('charge', 'sum', [staticFactor('base', 100)])
217
+ * worksheetGroup(group, [{ id: 'charge', applied: true, value: 100, factors: [] }])
218
+ * ```
371
219
  */
372
220
  function worksheetGroup(definition, results, labels) {
373
221
  const result = results.find((entry) => entry.id === definition.id);
@@ -389,6 +237,14 @@ function worksheetGroup(definition, results, labels) {
389
237
  * @param value - The stage's resolved value
390
238
  * @param expression - A display-neutral expression string
391
239
  * @returns A fresh step
240
+ *
241
+ * @example
242
+ * ```ts
243
+ * import { worksheetStep } from '@orkestrel/rater'
244
+ *
245
+ * worksheetStep('total', 'quote', 'Quote', 110, 'sum = 110')
246
+ * // { stage: 'total', id: 'quote', name: 'Quote', value: 110, expression: 'sum = 110' }
247
+ * ```
392
248
  */
393
249
  function worksheetStep(stage, id, name, value, expression) {
394
250
  return {
@@ -406,6 +262,18 @@ function worksheetStep(stage, id, name, value, expression) {
406
262
  * @param result - The evaluated quantitative result
407
263
  * @param groups - The definition's already-joined worksheet groups
408
264
  * @returns A fresh, ordered list of steps: applied factors, then groups, then the total
265
+ *
266
+ * @example
267
+ * ```ts
268
+ * import { createQuantitativeReasoner, createReason, factorGroup, quantitativeDefinition, staticFactor } from '@orkestrel/reason'
269
+ * import { worksheetGroup, worksheetSteps } from '@orkestrel/rater'
270
+ *
271
+ * const definition = quantitativeDefinition('base', 'Base', [factorGroup('g', 'sum', [staticFactor('flat', 100)])])
272
+ * const result = createReason({ reasoners: [createQuantitativeReasoner()] }).reason({}, definition)
273
+ * if (result.reasoning === 'quantitative') {
274
+ * worksheetSteps(definition, result, definition.groups.map((group) => worksheetGroup(group, result.groups)))
275
+ * }
276
+ * ```
409
277
  */
410
278
  function worksheetSteps(definition, result, groups) {
411
279
  const output = [];
@@ -424,6 +292,16 @@ function worksheetSteps(definition, result, groups) {
424
292
  * @param result - The evaluated quantitative result
425
293
  * @param labels - Optional field-to-label overrides, keyed by dot-joined field
426
294
  * @returns A fresh worksheet
295
+ *
296
+ * @example
297
+ * ```ts
298
+ * import { createQuantitativeReasoner, createReason, factorGroup, quantitativeDefinition, staticFactor } from '@orkestrel/reason'
299
+ * import { resultsWorksheet } from '@orkestrel/rater'
300
+ *
301
+ * const definition = quantitativeDefinition('base', 'Base', [factorGroup('g', 'sum', [staticFactor('flat', 100)])])
302
+ * const result = createReason({ reasoners: [createQuantitativeReasoner()] }).reason({}, definition)
303
+ * if (result.reasoning === 'quantitative') resultsWorksheet(definition, result) // the full audit trail
304
+ * ```
427
305
  */
428
306
  function resultsWorksheet(definition, result, labels) {
429
307
  const groups = definition.groups.map((group) => worksheetGroup(group, result.groups, labels));
@@ -441,186 +319,31 @@ function resultsWorksheet(definition, result, labels) {
441
319
  };
442
320
  }
443
321
  /**
444
- * Convert fired logical {@link RuleResult}s into line- or program-scoped
445
- * {@link Determination}s.
446
- *
447
- * @param definition - The authored logical definition
448
- * @param result - The evaluated logical result
449
- * @param rulings - Authored consequences, keyed by rule id
450
- * @param working - The working subject the rules ran against
451
- * @param line - The line id this pass is scoped to, when it is line-scoped
452
- * @param evaluator - The shared reason check evaluator
453
- * @param labels - Optional field-to-label overrides, keyed by dot-joined field
454
- * @returns A fresh list of determinations
455
- */
456
- function rulesToDeterminations(definition, result, rulings, working, line, evaluator, labels) {
457
- const output = [];
458
- for (const entry of result.rules) {
459
- const authored = findRule(definition, entry.id);
460
- if (authored === void 0) continue;
461
- const routed = rulings?.[entry.id];
462
- if (!entry.applied && routed === void 0) continue;
463
- const resolved = routed?.line ?? line;
464
- output.push({
465
- id: entry.id,
466
- effect: routed?.effect ?? "restriction",
467
- applied: entry.applied,
468
- ...resolved === void 0 ? {} : { line: resolved },
469
- ...routed?.message === void 0 ? {} : { message: interpolateMessage(routed.message, working) },
470
- premises: logicalPremises(authored, working, evaluator, labels)
471
- });
472
- }
473
- return output;
474
- }
475
- /**
476
- * Convert an authority's fired {@link RuleResult}s into `limit`
477
- * {@link Determination}s.
478
- *
479
- * @param definition - The authority's logical definition
480
- * @param result - The evaluated logical result
481
- * @param rulings - Authored consequences, keyed by rule id
482
- * @param working - The working subject the authority ran against
483
- * @param evaluator - The shared reason check evaluator
484
- * @param labels - Optional field-to-label overrides, keyed by dot-joined field
485
- * @returns A fresh list of `limit` determinations
486
- */
487
- function authorityToDeterminations(definition, result, rulings, working, evaluator, labels) {
488
- const output = [];
489
- for (const entry of result.rules) {
490
- if (!entry.applied) continue;
491
- const authored = findRule(definition, entry.id);
492
- if (authored === void 0) continue;
493
- const routed = rulings?.[entry.id];
494
- output.push({
495
- id: entry.id,
496
- effect: "limit",
497
- applied: true,
498
- ...routed?.line === void 0 ? {} : { line: routed.line },
499
- ...routed?.message === void 0 ? {} : { message: interpolateMessage(routed.message, working) },
500
- premises: logicalPremises(authored, working, evaluator, labels)
501
- });
502
- }
503
- return output;
504
- }
505
- /**
506
- * Convert authored {@link Notice}s into unconditionally-applied
507
- * `notice` {@link Determination}s.
508
- *
509
- * @param notices - The authored notices
510
- * @param working - The working subject to interpolate messages against
511
- * @returns A fresh list of notice determinations
512
- */
513
- function noticesToDeterminations(notices, working) {
514
- return (notices ?? []).map((entry) => ({
515
- id: entry.id,
516
- effect: "notice",
517
- applied: true,
518
- ...entry.line === void 0 ? {} : { line: entry.line },
519
- message: interpolateMessage(entry.message, working),
520
- premises: []
521
- }));
522
- }
523
- /**
524
- * Keep only the determinations scoped to one line.
525
- *
526
- * @param determinations - The determinations to filter
527
- * @param id - The line id to keep
528
- * @returns The determinations whose `line` matches `id`
529
- */
530
- function filterLineDeterminations(determinations, id) {
531
- return determinations.filter((entry) => entry.line === id);
532
- }
533
- /**
534
- * Keep only program-scoped (line-unscoped) determinations.
535
- *
536
- * @param determinations - The determinations to filter
537
- * @returns The determinations with no `line`
538
- */
539
- function filterProgramDeterminations(determinations) {
540
- return determinations.filter((entry) => entry.line === void 0);
541
- }
542
- /**
543
- * Derive the eligibility impact of a set of determinations.
544
- *
545
- * @param determinations - The determinations to fold
546
- * @returns The most severe eligibility any APPLIED determination's effect carries
547
- *
548
- * @example
549
- * ```ts
550
- * import { deriveDeterminationEligibility } from '@orkestrel/rater'
551
- *
552
- * deriveDeterminationEligibility([{ id: 'r1', effect: 'restriction', applied: true, premises: [] }])
553
- * // 'ineligible'
554
- * ```
555
- */
556
- function deriveDeterminationEligibility(determinations) {
557
- const impacts = ["eligible"];
558
- for (const entry of determinations) {
559
- const eligibility = entry.applied ? EFFECT_ELIGIBILITIES[entry.effect] : void 0;
560
- if (eligibility !== void 0) impacts.push(eligibility);
561
- }
562
- return combineEligibilities(impacts);
563
- }
564
- /**
565
- * Return the most severe {@link Eligibility} in a list.
566
- *
567
- * @param eligibilities - The eligibilities to combine
568
- * @returns The most severe eligibility, or `'eligible'` for an empty list
569
- *
570
- * @example
571
- * ```ts
572
- * import { combineEligibilities } from '@orkestrel/rater'
573
- *
574
- * combineEligibilities(['eligible', 'referral']) // 'referral'
575
- * ```
576
- */
577
- function combineEligibilities(eligibilities) {
578
- for (const eligibility of ELIGIBILITY_PRECEDENCE) if (eligibilities.includes(eligibility)) return eligibility;
579
- return "eligible";
580
- }
581
- /**
582
- * Convert an {@link Eligibility} to its deterministic authority {@link Decision}.
583
- *
584
- * @param eligibility - The eligibility to convert
585
- * @returns The matching decision
586
- */
587
- function decideEligibility(eligibility) {
588
- return ELIGIBILITY_DECISIONS[eligibility];
589
- }
590
- /**
591
- * Derive the final {@link Status} from eligibility, determinations, and rated
592
- * line evidence.
593
- *
594
- * @param eligibility - The program's combined eligibility
595
- * @param determinations - Every determination in scope (program and line)
596
- * @param lines - The program's rated lines
597
- * @returns The derived status
598
- */
599
- function deriveStatus(eligibility, determinations, lines) {
600
- if (eligibility === "ineligible") return "ineligible";
601
- if (eligibility === "referral") return "referral";
602
- if (determinations.some((entry) => entry.applied && entry.effect === "condition")) return "conditional";
603
- if (lines.some((entry) => !(entry.worksheet?.success ?? false) || entry.amount === void 0)) return "unrated";
604
- return "eligible";
605
- }
606
- /**
607
322
  * Build a rated {@link LineResult} from a line's evaluated
608
323
  * {@link QuantitativeResult}.
609
324
  *
610
325
  * @param definition - The authored line definition
611
326
  * @param result - The evaluated quantitative result
612
- * @param determinations - The line-scoped determinations
613
327
  * @param labels - Optional field-to-label overrides, keyed by dot-joined field
614
328
  * @returns A fresh line result
329
+ *
330
+ * @example
331
+ * ```ts
332
+ * import { createQuantitativeReasoner, createReason, quantitativeDefinition } from '@orkestrel/reason'
333
+ * import { lineDefinition, ratedLine } from '@orkestrel/rater'
334
+ *
335
+ * const line = lineDefinition('base', 'Base', quantitativeDefinition('base', 'Base', []))
336
+ * const result = createReason({ reasoners: [createQuantitativeReasoner()] }).reason({}, line.rate)
337
+ * if (result.reasoning === 'quantitative') ratedLine(line, result) // amount present only when success
338
+ * ```
615
339
  */
616
- function ratedLine(definition, result, determinations, labels) {
340
+ function ratedLine(definition, result, labels) {
617
341
  return {
618
342
  id: definition.id,
619
343
  name: definition.name,
620
- eligibility: deriveDeterminationEligibility(determinations),
621
344
  ...result.success ? { amount: result.value } : {},
622
345
  worksheet: resultsWorksheet(definition.rate, result, labels),
623
- determinations
346
+ success: result.success
624
347
  };
625
348
  }
626
349
  /**
@@ -628,6 +351,17 @@ function ratedLine(definition, result, determinations, labels) {
628
351
  *
629
352
  * @param lines - The rated lines
630
353
  * @returns The sum of every line's `amount`, or `undefined` when no line has one
354
+ *
355
+ * @example
356
+ * ```ts
357
+ * import { createQuantitativeReasoner, createReason, factorGroup, quantitativeDefinition, staticFactor } from '@orkestrel/reason'
358
+ * import { lineDefinition, ratedLine, sumAmounts } from '@orkestrel/rater'
359
+ *
360
+ * const rate = quantitativeDefinition('base', 'Base', [factorGroup('g', 'sum', [staticFactor('flat', 100)])])
361
+ * const line = lineDefinition('base', 'Base', rate)
362
+ * const result = createReason({ reasoners: [createQuantitativeReasoner()] }).reason({}, rate)
363
+ * if (result.reasoning === 'quantitative') sumAmounts([ratedLine(line, result)]) // 100
364
+ * ```
631
365
  */
632
366
  function sumAmounts(lines) {
633
367
  let total = 0;
@@ -638,703 +372,100 @@ function sumAmounts(lines) {
638
372
  }
639
373
  return count === 0 ? void 0 : total;
640
374
  }
375
+ //#endregion
376
+ //#region src/core/Rater.ts
641
377
  /**
642
- * Build an authority outcome projection from an assembled {@link ProgramResult}.
643
- *
644
- * @param result - The program result computed before authority runs
645
- * @returns A record shaped for the authority's `outcome` projection
646
- */
647
- function outcomeProjection(result) {
648
- const lines = {};
649
- for (const rated of result.lines) if (rated.amount !== void 0) lines[rated.id] = rated.amount;
650
- return {
651
- eligibility: result.eligibility,
652
- status: result.status,
653
- total: result.total,
654
- lines
655
- };
656
- }
657
- /**
658
- * Assemble a final {@link ProgramResult} from its rated parts.
659
- *
660
- * @param definition - The authored program definition
661
- * @param lines - The program's rated lines
662
- * @param determinations - The program-scoped determinations
663
- * @param derivations - The pass-level quantitative worksheets
664
- * @param total - The program's total amount, when the total handler produced one
665
- * @param decision - The authority-derived decision, when one was reached
666
- * @param trace - The accumulated reasoning trace
667
- * @param errors - The accumulated reasoning errors
668
- * @returns A fresh program result
669
- */
670
- function programResult(definition, lines, determinations, derivations, total, decision, trace, errors) {
671
- const eligibility = combineEligibilities([deriveDeterminationEligibility(determinations), ...lines.map((entry) => entry.eligibility)]);
672
- const allDeterminations = [...determinations, ...lines.flatMap((entry) => entry.determinations)];
673
- return {
674
- id: definition.id,
675
- name: definition.name,
676
- eligibility,
677
- status: deriveStatus(eligibility, allDeterminations, lines),
678
- ...decision === void 0 ? {} : { decision },
679
- lines,
680
- determinations,
681
- derivations,
682
- ...total === void 0 ? {} : { total },
683
- success: errors.length === 0 && lines.every((entry) => entry.worksheet?.success ?? true),
684
- trace,
685
- errors
686
- };
687
- }
688
- /**
689
- * Return authored line references (in passes, rulings, or notices) that name
690
- * no line on the program.
691
- *
692
- * @param definition - The program definition to check
693
- * @returns A fresh, deduped list of missing line ids
694
- */
695
- function findMissingLineReferences(definition) {
696
- const ids = new Set(definition.lines.map((entry) => entry.id));
697
- const missing = /* @__PURE__ */ new Set();
698
- for (const entry of definition.passes ?? []) if (entry.line !== void 0 && !ids.has(entry.line)) missing.add(entry.line);
699
- for (const entry of Object.values(definition.rulings ?? {})) if (entry.line !== void 0 && !ids.has(entry.line)) missing.add(entry.line);
700
- for (const entry of definition.notices ?? []) if (entry.line !== void 0 && !ids.has(entry.line)) missing.add(entry.line);
701
- return [...missing];
702
- }
703
- /** Determine whether a working record already carries a reserved rater key. */
704
- function hasReservedKey(subject) {
705
- return Object.hasOwn(subject, "aggregate") || Object.hasOwn(subject, "outcome");
706
- }
707
- /**
708
- * Locate a {@link ProgramDefinition}'s reserved-key collisions — a quantitative
709
- * pass whose id shadows {@link AGGREGATE_KEY} / {@link OUTCOME_KEY}, or a
710
- * logical pass's or the authority's fired rule conclusion whose asserted
711
- * field's FIRST path segment shadows one.
712
- *
713
- * @remarks
714
- * A quantitative pass writes its value onto the working subject under its own
715
- * definition id (`assignField`), silently clobbering the reserved aggregate or
716
- * outcome projection when the id collides. A fired rule's conclusion atoms
717
- * merge onto the working subject (`mergeSubjects`/`extractConclusions`) keyed
718
- * by `formatField(check.field)` — an atom whose field's first segment is a
719
- * reserved key is flagged here even when its FULL formatted key is a nested
720
- * form (`outcome.total`), since authoring a conclusion under a reserved
721
- * top-level segment is never intentional.
722
- *
723
- * @param definition - The program definition to scan
724
- * @returns A fresh, deduped list of offending pass or rule ids
725
- *
726
- * @example
727
- * ```ts
728
- * import { quantitativeDefinition } from '@orkestrel/reason'
729
- * import { findReservedCollisions, passDefinition, programDefinition } from '@orkestrel/rater'
730
- *
731
- * findReservedCollisions(
732
- * programDefinition('p1', 'P1', [], {
733
- * passes: [passDefinition(quantitativeDefinition('aggregate', 'Aggregate', []))],
734
- * }),
735
- * ) // ['aggregate']
736
- * ```
737
- */
738
- function findReservedCollisions(definition) {
739
- const offending = /* @__PURE__ */ new Set();
740
- const logicals = [];
741
- for (const pass of definition.passes ?? []) if (pass.definition.reasoning === "quantitative") {
742
- if (pass.definition.id === "aggregate" || pass.definition.id === "outcome") offending.add(pass.definition.id);
743
- } else if (pass.definition.reasoning === "logical") logicals.push(pass.definition);
744
- if (definition.authority !== void 0) logicals.push(definition.authority);
745
- for (const logical of logicals) for (const rule of logical.rules) for (const atom of extractAtoms(rule.conclusion)) {
746
- const head = Array.isArray(atom.check.field) ? atom.check.field[0] : atom.check.field;
747
- if (head === "aggregate" || head === "outcome") offending.add(rule.id);
748
- }
749
- return [...offending];
750
- }
751
- /**
752
- * Assert a value is a valid rater {@link Subject}, narrowing it in place.
753
- *
754
- * @param subject - The candidate subject to validate
755
- * @throws {@link RaterError} `'MISMATCH'` when the value is not a record, or
756
- * when it already carries a reserved rater working-subject key
757
- */
758
- function assertSubject(subject) {
759
- if (!isRecord(subject)) throw new RaterError("MISMATCH", "Subject must be a record");
760
- if (hasReservedKey(subject)) throw new RaterError("MISMATCH", "Subject uses a reserved rater key");
761
- }
762
- /**
763
- * Sum aggregate fields across a batch of subjects.
764
- *
765
- * @param subjects - The batch of subjects
766
- * @param fields - The fields to sum
767
- * @returns A fresh record of dot-joined field to summed finite value
768
- */
769
- function aggregateSums(subjects, fields) {
770
- const sums = {};
771
- for (const field of fields) sums[formatField(field)] = 0;
772
- for (const subject of subjects) for (const field of fields) {
773
- const key = formatField(field);
774
- const value = resolveField(subject, field);
775
- if (isFiniteNumber(value)) sums[key] = (sums[key] ?? 0) + value;
776
- }
777
- return sums;
778
- }
779
- /**
780
- * Partition a batch of subjects by a field, summing aggregate fields per
781
- * partition.
782
- *
783
- * @remarks
784
- * The partition key is the resolved `by` field, coerced with `String` —
785
- * `undefined` collapses to the empty string, so a subject missing the field
786
- * and a subject whose field is literally `''` land in the SAME partition,
787
- * and a numeric `1` collides with the string `'1'`.
788
- *
789
- * @param subjects - The batch of subjects
790
- * @param fields - The fields to sum within each partition
791
- * @param by - The partition key field; no partition is built when absent
792
- * @returns A fresh list of aggregate groups, or an empty list when `by` is absent
793
- */
794
- function aggregateGroups(subjects, fields, by) {
795
- if (by === void 0) return [];
796
- const groups = /* @__PURE__ */ new Map();
797
- for (const subject of subjects) {
798
- const key = String(resolveField(subject, by) ?? "");
799
- const entries = groups.get(key) ?? [];
800
- entries.push(subject);
801
- groups.set(key, entries);
802
- }
803
- return [...groups.entries()].map(([key, entries]) => ({
804
- key,
805
- count: entries.length,
806
- sums: aggregateSums(entries, fields)
807
- }));
808
- }
809
- /**
810
- * Collect the deduped, ordered union of every program's aggregate fields.
811
- *
812
- * @param programs - The compiled programs to scan
813
- * @returns A fresh list of aggregate field paths, in first-seen order
814
- *
815
- * @example
816
- * ```ts
817
- * import { aggregateFields } from '@orkestrel/rater'
818
- *
819
- * aggregateFields(programs) // [['income'], ['assets']]
820
- * ```
821
- */
822
- function aggregateFields(programs) {
823
- const fields = [];
824
- const keys = /* @__PURE__ */ new Set();
825
- for (const program of programs) for (const field of program.definition.aggregate?.fields ?? []) {
826
- const key = formatField(field);
827
- if (!keys.has(key)) {
828
- keys.add(key);
829
- fields.push(field);
830
- }
831
- }
832
- return fields;
833
- }
834
- /**
835
- * Locate the {@link AggregateGroup} a subject belongs to.
378
+ * The rating orchestrator — owns (or receives) the shared quantitative
379
+ * reasoning engine and projects results into the rating domain vocabulary.
836
380
  *
837
381
  * @remarks
838
- * The lookup key is the resolved `by` field, coerced with `String` —
839
- * `undefined` collapses to the empty string, matching {@link aggregateGroups}'
840
- * partition-key coercion (a missing key and an empty-string key collide, and
841
- * a numeric `1` collides with the string `'1'`).
842
- *
843
- * @param subject - The subject to key
844
- * @param groups - The candidate groups to search
845
- * @param by - The partition key field; no group is found when absent
846
- * @returns The subject's matching group, or `undefined`
382
+ * When no `engine` is injected, `Rater` builds and OWNS its own
383
+ * quantitative-only engine (`bail: false`) — Rater performs NO evaluation
384
+ * arithmetic of its own; it only orchestrates and projects. The
385
+ * array-of-lines `rate` overload is declared FIRST (AGENTS §9.2). `destroy()`
386
+ * destroys an OWNED engine, then the emitter LAST (AGENTS §13); an INJECTED
387
+ * engine is never destroyed. Afterwards every other method throws
388
+ * {@link RaterError} `'DESTROYED'`.
847
389
  *
848
390
  * @example
849
391
  * ```ts
850
- * import { groupFor } from '@orkestrel/rater'
392
+ * import { Rater } from '@orkestrel/rater'
851
393
  *
852
- * groupFor({ region: 'east' }, [{ key: 'east', count: 1, sums: {} }], ['region'])
853
- * // { key: 'east', count: 1, sums: {} }
394
+ * const rater = new Rater()
395
+ * rater.rate([], { id: 'subject-1' }) // { lines: [], success: true }
396
+ * rater.destroy()
854
397
  * ```
855
398
  */
856
- function groupFor(subject, groups, by) {
857
- if (by === void 0) return void 0;
858
- const key = String(resolveField(subject, by) ?? "");
859
- return groups.find((entry) => entry.key === key);
860
- }
861
- /**
862
- * Build the batch aggregate working projection written under
863
- * {@link AGGREGATE_KEY}'s value.
864
- *
865
- * @param count - The batch (or partition) subject count
866
- * @param sums - The summed aggregate fields
867
- * @param group - The subject's own partition, when the aggregate is partitioned
868
- * @returns A fresh aggregate projection record
869
- */
870
- function aggregateProjection(count, sums, group) {
871
- return {
872
- count,
873
- sums,
874
- ...group === void 0 ? {} : { group }
875
- };
876
- }
877
- /**
878
- * Build the reserved-key record an aggregate gate definition runs against.
879
- *
880
- * @param count - The batch (or partition) subject count
881
- * @param sums - The summed aggregate fields
882
- * @param group - The subject's own partition, when the aggregate is partitioned
883
- * @returns A fresh record carrying the aggregate projection under {@link AGGREGATE_KEY}
884
- */
885
- function aggregateRecord(count, sums, group) {
886
- return { [AGGREGATE_KEY]: aggregateProjection(count, sums, group) };
887
- }
888
- /**
889
- * Build zero sums for a set of fields.
890
- *
891
- * @param fields - The fields to zero
892
- * @returns A fresh record of dot-joined field to `0`
893
- */
894
- function emptySums(fields) {
895
- const sums = {};
896
- for (const field of fields) sums[formatField(field)] = 0;
897
- return sums;
898
- }
899
- /**
900
- * Complete a partial status tally record with zero entries for every missing
901
- * {@link Status}.
902
- *
903
- * @param entries - The partial tally entries to complete
904
- * @returns A record with all five statuses present
905
- */
906
- function completeTallies(entries) {
907
- return {
908
- ineligible: entries.ineligible ?? {
909
- count: 0,
910
- sums: {}
911
- },
912
- referral: entries.referral ?? {
913
- count: 0,
914
- sums: {}
915
- },
916
- conditional: entries.conditional ?? {
917
- count: 0,
918
- sums: {}
919
- },
920
- unrated: entries.unrated ?? {
921
- count: 0,
922
- sums: {}
923
- },
924
- eligible: entries.eligible ?? {
925
- count: 0,
926
- sums: {}
927
- }
928
- };
929
- }
930
- /**
931
- * Build empty status tallies in precedence order.
932
- *
933
- * @param fields - The fields each tally's sums are zeroed for
934
- * @returns A fresh, complete tally record
935
- */
936
- function emptyTallies(fields) {
937
- const entries = {};
938
- for (const status of STATUS_PRECEDENCE) entries[status] = {
939
- count: 0,
940
- sums: emptySums(fields)
941
- };
942
- return completeTallies(entries);
943
- }
944
- /**
945
- * Add one subject's aggregate contribution to a status tally record.
946
- *
947
- * @param tallies - The tallies to update
948
- * @param status - The subject's derived status
949
- * @param subject - The subject to fold in
950
- * @param fields - The fields to sum
951
- * @returns A fresh, complete tally record with the subject folded in
952
- */
953
- function tallySubject(tallies, status, subject, fields) {
954
- const updated = { ...tallies };
955
- const current = tallies[status];
956
- const sums = { ...current.sums };
957
- for (const field of fields) {
958
- const key = formatField(field);
959
- const value = resolveField(subject, field);
960
- if (isFiniteNumber(value)) sums[key] = (sums[key] ?? 0) + value;
961
- }
962
- updated[status] = {
963
- count: current.count + 1,
964
- sums
965
- };
966
- return completeTallies(updated);
967
- }
968
- //#endregion
969
- //#region src/core/programs/Program.ts
970
- /**
971
- * A compiled program — rates one subject at a time over an injected, shared
972
- * reasoning engine.
973
- *
974
- * @remarks
975
- * The engine is INJECTED and never owned, destroyed, or otherwise mutated by
976
- * the program: every actual evaluation (passes, lines, authority) delegates to
977
- * it. `rate` builds its working subject through copy-on-write overlays only —
978
- * the caller's `subject` is never mutated.
979
- */
980
- var Program = class {
981
- #definition;
982
- #engine;
983
- #evaluator;
984
- #total;
985
- #labels;
986
- constructor(definition, reason, options) {
987
- const missing = findMissingLineReferences(definition);
988
- if (missing.length > 0) throw new RaterError("MISSING", `Unknown line reference: ${missing.join(", ")}`, { program: definition.id });
989
- const collisions = findReservedCollisions(definition);
990
- if (collisions.length > 0) throw new RaterError("DEFINITION", `Reserved key collision: ${collisions.join(", ")}`, {
991
- program: definition.id,
992
- keys: collisions
993
- });
994
- this.#definition = definition;
995
- this.#engine = reason;
996
- this.#evaluator = createEvaluator();
997
- this.#total = options?.total;
998
- this.#labels = options?.labels;
999
- }
1000
- get id() {
1001
- return this.#definition.id;
1002
- }
1003
- get name() {
1004
- return this.#definition.name;
1005
- }
1006
- get definition() {
1007
- return this.#definition;
1008
- }
1009
- rate(subject, aggregate) {
1010
- assertSubject(subject);
1011
- let working = aggregate === void 0 ? subject : assignField(subject, AGGREGATE_KEY, aggregate);
1012
- const determinations = [];
1013
- const derivations = [];
1014
- const trace = [];
1015
- const errors = [];
1016
- for (const pass of this.#definition.passes ?? []) if (pass.definition.reasoning === "quantitative") {
1017
- const result = this.#engine.reason(working, pass.definition);
1018
- trace.push(...result.trace);
1019
- errors.push(...result.errors);
1020
- if (result.reasoning === "quantitative") {
1021
- working = assignField(working, pass.definition.id, result.value);
1022
- derivations.push(resultsWorksheet(pass.definition, result, this.#labels));
1023
- }
1024
- } else {
1025
- const result = this.#engine.reason(working, pass.definition);
1026
- trace.push(...result.trace);
1027
- errors.push(...result.errors);
1028
- if (result.reasoning === "logical") {
1029
- determinations.push(...rulesToDeterminations(pass.definition, result, this.#definition.rulings, working, pass.line, this.#evaluator, this.#labels));
1030
- for (const entry of result.rules) {
1031
- if (!entry.applied) continue;
1032
- const rule = findRule(pass.definition, entry.id);
1033
- if (rule !== void 0) working = mergeSubjects(working, extractConclusions(rule.conclusion));
1034
- }
1035
- }
1036
- }
1037
- determinations.push(...noticesToDeterminations(this.#definition.notices, working));
1038
- const scoped = filterProgramDeterminations(determinations);
1039
- const lines = this.#rateLines(working, scoped, determinations);
1040
- const total = this.#total?.(lines);
1041
- const base = programResult(this.#definition, lines, scoped, derivations, total, void 0, trace, errors);
1042
- if (this.#definition.authority === void 0) return base;
1043
- const authorityWorking = assignField(working, OUTCOME_KEY, outcomeProjection(base));
1044
- const authorityResult = this.#engine.reason(authorityWorking, this.#definition.authority);
1045
- if (authorityResult.reasoning !== "logical") return base;
1046
- const limits = authorityToDeterminations(this.#definition.authority, authorityResult, this.#definition.rulings, authorityWorking, this.#evaluator, this.#labels);
1047
- const decision = limits.length === 0 && authorityResult.errors.length === 0 ? decideEligibility(base.eligibility) : void 0;
1048
- return programResult(this.#definition, lines, [...scoped, ...limits], derivations, total, decision, [...trace, ...authorityResult.trace], [...errors, ...authorityResult.errors]);
1049
- }
1050
- #rateLines(working, context, determinations) {
1051
- const lines = [];
1052
- for (const entry of this.#definition.lines) {
1053
- const own = filterLineDeterminations(determinations, entry.id);
1054
- const line = ratedLine(entry, this.#quantitative(working, entry.rate), own, this.#labels);
1055
- lines.push({
1056
- ...line,
1057
- eligibility: deriveDeterminationEligibility([...context, ...line.determinations])
1058
- });
1059
- }
1060
- return lines;
1061
- }
1062
- #quantitative(subject, definition) {
1063
- const result = this.#engine.reason(subject, definition);
1064
- if (result.reasoning === "quantitative") return result;
1065
- const failure = buildErrorResult(definition, `Expected quantitative result, got ${result.reasoning}`);
1066
- const errors = [...result.errors, ...failure.errors];
1067
- if (failure.reasoning === "quantitative") return {
1068
- ...failure,
1069
- errors
1070
- };
1071
- return {
1072
- reasoning: "quantitative",
1073
- value: 0,
1074
- groups: [],
1075
- count: 0,
1076
- success: false,
1077
- trace: [],
1078
- errors
1079
- };
1080
- }
1081
- };
1082
- //#endregion
1083
- //#region src/core/programs/ProgramManager.ts
1084
- /**
1085
- * An ordered manager over compiled {@link ProgramInterface}s (AGENTS §9), built
1086
- * over an injected, shared reasoning engine.
1087
- *
1088
- * @remarks
1089
- * OWNS its `#programs` collection and its own {@link Emitter} over
1090
- * {@link ProgramManagerEventMap}. `destroy()` is idempotent and tears the
1091
- * emitter down LAST; every other call afterwards throws
1092
- * {@link RaterError} `'DESTROYED'`.
1093
- */
1094
- var ProgramManager = class {
1095
- #programs = /* @__PURE__ */ new Map();
1096
- #engine;
399
+ var Rater = class {
1097
400
  #emitter;
401
+ #engine;
402
+ #owned;
1098
403
  #total;
1099
404
  #labels;
1100
- #validate;
1101
405
  #destroyed = false;
1102
- constructor(engine, options) {
1103
- this.#engine = engine;
406
+ constructor(options) {
1104
407
  this.#emitter = new Emitter({
1105
408
  on: options?.on,
1106
409
  error: options?.error
1107
410
  });
411
+ if (options?.engine === void 0) {
412
+ this.#engine = createReason({
413
+ reasoners: [createQuantitativeReasoner()],
414
+ bail: false
415
+ });
416
+ this.#owned = true;
417
+ } else {
418
+ this.#engine = options.engine;
419
+ this.#owned = false;
420
+ }
1108
421
  this.#total = options?.total;
1109
422
  this.#labels = options?.labels;
1110
- this.#validate = options?.validate ?? true;
1111
423
  }
1112
424
  get emitter() {
1113
425
  return this.#emitter;
1114
426
  }
1115
- get size() {
1116
- this.#ensureAlive();
1117
- return this.#programs.size;
1118
- }
1119
- has(id) {
1120
- this.#ensureAlive();
1121
- return this.#programs.has(id);
1122
- }
1123
- program(id) {
1124
- this.#ensureAlive();
1125
- return this.#programs.get(id);
1126
- }
1127
- programs() {
427
+ rate(input, subject) {
1128
428
  this.#ensureAlive();
1129
- return [...this.#programs.values()];
1130
- }
1131
- add(definition, options) {
1132
- this.#ensureAlive();
1133
- if (this.#programs.has(definition.id)) throw new RaterError("DUPLICATE", `Program already exists: ${definition.id}`, { program: definition.id });
1134
- const id = definition.id;
1135
- if (this.#validate && !isProgramDefinition(definition)) throw new RaterError("DEFINITION", "Program definition failed validation", { program: id });
1136
- const program = new Program(definition, this.#engine, {
1137
- total: options?.total ?? this.#total,
1138
- labels: options?.labels ?? this.#labels
1139
- });
1140
- this.#programs.set(program.id, program);
1141
- this.#emitter.emit("add", program.id);
1142
- return program;
1143
- }
1144
- remove(target) {
1145
- this.#ensureAlive();
1146
- if (target === void 0) {
1147
- for (const id of this.#programs.keys()) this.#emitter.emit("remove", id);
1148
- this.#programs.clear();
1149
- return;
1150
- }
1151
- if (typeof target === "string") return this.#removeOne(target);
1152
- let removed = true;
1153
- for (const id of target) removed = this.#removeOne(id) && removed;
1154
- return removed;
1155
- }
1156
- destroy() {
1157
- if (this.#destroyed) return;
1158
- this.#programs.clear();
1159
- this.#destroyed = true;
1160
- this.#emitter.emit("destroy");
1161
- this.#emitter.destroy();
1162
- }
1163
- #removeOne(id) {
1164
- const removed = this.#programs.delete(id);
1165
- if (removed) this.#emitter.emit("remove", id);
1166
- return removed;
1167
- }
1168
- #ensureAlive() {
1169
- if (this.#destroyed) throw new RaterError("DESTROYED", "ProgramManager has been destroyed");
1170
- }
1171
- };
1172
- //#endregion
1173
- //#region src/core/Rater.ts
1174
- /**
1175
- * The rating orchestrator — owns the shared reasoning engine and an ordered
1176
- * {@link ProgramManager}, and projects results into the rating domain
1177
- * vocabulary.
1178
- *
1179
- * @remarks
1180
- * Constructs ONE shared reason engine (quantitative + logical reasoners,
1181
- * `bail: false`) and injects it into every compiled program — Rater performs
1182
- * NO evaluation arithmetic of its own; it only orchestrates and projects. The
1183
- * batch `rate` overload is declared FIRST (AGENTS §9.2) so an array subject
1184
- * resolves to the batch form. `destroy()` tears down the manager, then the
1185
- * engine, then the emitter LAST (AGENTS §13); afterwards every other method
1186
- * throws {@link RaterError} `'DESTROYED'`.
1187
- */
1188
- var Rater = class {
1189
- #emitter;
1190
- #manager;
1191
- #engine;
1192
- #evaluator;
1193
- #destroyed = false;
1194
- constructor(options) {
1195
- this.#emitter = new Emitter({
1196
- on: options?.on,
1197
- error: options?.error
1198
- });
1199
- this.#engine = createReason({
1200
- reasoners: [createQuantitativeReasoner(), createLogicalReasoner()],
1201
- bail: false
1202
- });
1203
- this.#evaluator = createEvaluator();
1204
- this.#manager = new ProgramManager(this.#engine, {
1205
- total: options?.total,
1206
- labels: options?.labels,
1207
- validate: options?.validate
1208
- });
1209
- for (const definition of options?.programs ?? []) this.#manager.add(definition);
1210
- }
1211
- get emitter() {
1212
- return this.#emitter;
1213
- }
1214
- get programs() {
1215
- return this.#manager;
1216
- }
1217
- rate(subject) {
1218
- this.#ensureAlive();
1219
- if (isRecord(subject)) return this.#rateOne(subject);
1220
- return this.#rateBatch(subject);
429
+ const lines = this.#normalize(input);
430
+ if (!isSubject(subject)) throw new RaterError("MISMATCH", "Subject must be a record");
431
+ const results = lines.map((line) => this.#rateLine(line, subject));
432
+ const total = (this.#total ?? sumAmounts)(results);
433
+ const success = results.every((entry) => entry.success);
434
+ const result = {
435
+ lines: results,
436
+ ...total === void 0 ? {} : { total },
437
+ success
438
+ };
439
+ this.#emitter.emit("rate", subject, result);
440
+ return result;
1221
441
  }
1222
442
  destroy() {
1223
443
  if (this.#destroyed) return;
1224
- this.#manager.destroy();
1225
- this.#engine.destroy();
1226
444
  this.#destroyed = true;
445
+ if (this.#owned) this.#engine.destroy();
1227
446
  this.#emitter.destroy();
1228
447
  }
1229
- #rateOne(subject) {
1230
- assertSubject(subject);
1231
- const result = this.#subject(subject);
1232
- this.#emitSubject(result);
1233
- return result;
448
+ #normalize(input) {
449
+ if (arrayOf(isLineDefinition)(input)) return input;
450
+ if (isRatingDefinition(input)) return input.lines;
451
+ throw new RaterError("DEFINITION", "Rating input must be an array of line definitions or a rating definition");
1234
452
  }
1235
- #rateBatch(subjects) {
1236
- for (const subject of subjects) assertSubject(subject);
1237
- const programs = this.#manager.programs();
1238
- const sums = aggregateSums(subjects, aggregateFields(programs));
1239
- const programSums = /* @__PURE__ */ new Map();
1240
- const programGroups = /* @__PURE__ */ new Map();
1241
- for (const program of programs) {
1242
- const aggregate = program.definition.aggregate;
1243
- if (aggregate === void 0) continue;
1244
- programSums.set(program.id, aggregateSums(subjects, aggregate.fields));
1245
- programGroups.set(program.id, aggregateGroups(subjects, aggregate.fields, aggregate.by));
1246
- }
1247
- const groups = this.#groups(programs, programGroups);
1248
- const determinations = this.#aggregateDeterminations(programs, programSums, programGroups, subjects.length);
1249
- const subjectsResult = subjects.map((subject) => this.#subject(subject, this.#projections(subject, programs, programSums, programGroups, subjects.length)));
1250
- const result = {
1251
- subjects: subjectsResult,
1252
- determinations,
1253
- groups,
1254
- tallies: this.#tallies(subjects, subjectsResult, programs),
1255
- count: subjects.length,
1256
- sums
1257
- };
1258
- for (const rated of subjectsResult) this.#emitSubject(rated);
1259
- for (const determination of determinations) if (determination.applied) this.#emitter.emit("determine", determination);
1260
- this.#emitter.emit("aggregate", result);
1261
- return result;
1262
- }
1263
- #subject(subject, projections) {
1264
- return {
1265
- subject,
1266
- programs: this.#manager.programs().map((program) => {
1267
- const projection = projections?.[program.id];
1268
- return projection === void 0 ? program.rate(subject) : program.rate(subject, projection);
1269
- })
1270
- };
453
+ #rateLine(line, subject) {
454
+ return ratedLine(line, this.#reasonQuantitative(subject, line.rate), this.#labels);
1271
455
  }
1272
- #emitSubject(result) {
1273
- for (const program of result.programs) {
1274
- const determinations = [...program.determinations, ...program.lines.flatMap((rated) => rated.determinations)];
1275
- for (const determination of determinations) if (determination.applied) this.#emitter.emit("determine", determination);
1276
- if (program.decision !== void 0) this.#emitter.emit("decide", program.decision, program);
1277
- }
1278
- this.#emitter.emit("rate", result);
1279
- }
1280
- #groups(programs, programGroups) {
1281
- const output = [];
1282
- for (const program of programs) if (program.definition.aggregate?.by !== void 0) output.push(...programGroups.get(program.id) ?? []);
1283
- return output;
1284
- }
1285
- #projections(subject, programs, programSums, programGroups, count) {
1286
- const output = {};
1287
- for (const program of programs) {
1288
- const aggregate = program.definition.aggregate;
1289
- if (aggregate === void 0) continue;
1290
- const sums = programSums.get(program.id);
1291
- if (sums === void 0) continue;
1292
- const group = groupFor(subject, programGroups.get(program.id) ?? [], aggregate.by);
1293
- output[program.id] = aggregateProjection(count, sums, group);
1294
- }
1295
- return output;
1296
- }
1297
- #aggregateDeterminations(programs, programSums, programGroups, count) {
1298
- const determinations = [];
1299
- for (const program of programs) {
1300
- const aggregate = program.definition.aggregate;
1301
- if (aggregate?.gates === void 0) continue;
1302
- const sums = programSums.get(program.id);
1303
- if (sums === void 0) continue;
1304
- determinations.push(...this.#gate(aggregate.gates, aggregateRecord(count, sums), program.definition.rulings));
1305
- for (const group of programGroups.get(program.id) ?? []) determinations.push(...this.#gate(aggregate.gates, aggregateRecord(group.count, group.sums, group), program.definition.rulings));
1306
- }
1307
- return determinations;
1308
- }
1309
- #gate(definition, record, rulings) {
1310
- return rulesToDeterminations(definition, this.#reasonLogical(record, definition), rulings, record, void 0, this.#evaluator);
1311
- }
1312
- #tallies(subjects, results, programs) {
1313
- const output = {};
1314
- for (const program of programs) {
1315
- let tallies = emptyTallies(program.definition.aggregate?.fields ?? []);
1316
- for (let index = 0; index < results.length; index += 1) {
1317
- const subject = subjects[index];
1318
- const rated = results[index]?.programs.find((entry) => entry.id === program.id);
1319
- if (subject !== void 0 && rated !== void 0) tallies = tallySubject(tallies, rated.status, subject, program.definition.aggregate?.fields ?? []);
1320
- }
1321
- output[program.id] = tallies;
1322
- }
1323
- return output;
1324
- }
1325
- #reasonLogical(subject, definition) {
456
+ #reasonQuantitative(subject, definition) {
1326
457
  const result = this.#engine.reason(subject, definition);
1327
- if (result.reasoning === "logical") return result;
1328
- const failure = buildErrorResult(definition, `Expected logical result, got ${result.reasoning}`);
1329
- const errors = [...result.errors, ...failure.errors];
1330
- if (failure.reasoning === "logical") return {
458
+ if (result.reasoning === "quantitative") return result;
459
+ const failure = buildErrorResult(definition, `Expected quantitative result, got ${result.reasoning}`);
460
+ const errors = [...isArray(result.errors) ? result.errors : [], ...failure.errors];
461
+ if (failure.reasoning === "quantitative") return {
1331
462
  ...failure,
1332
463
  errors
1333
464
  };
1334
465
  return {
1335
- reasoning: "logical",
1336
- conclusion: false,
1337
- rules: [],
466
+ reasoning: "quantitative",
467
+ value: 0,
468
+ groups: [],
1338
469
  count: 0,
1339
470
  success: false,
1340
471
  trace: [],
@@ -1350,7 +481,7 @@ var Rater = class {
1350
481
  /**
1351
482
  * Create a rating orchestrator.
1352
483
  *
1353
- * @param options - Optional total handler, seed programs, labels, validation policy, and emitter hooks
484
+ * @param options - Optional total handler, labels, injected engine, and emitter hooks
1354
485
  * @returns A {@link RaterInterface}
1355
486
  *
1356
487
  * @example
@@ -1365,75 +496,6 @@ function createRater(options) {
1365
496
  return new Rater(options);
1366
497
  }
1367
498
  /**
1368
- * Create and validate one compiled program over a shared reasoning engine.
1369
- *
1370
- * @param definition - The authored program definition
1371
- * @param reason - The shared reasoning engine, injected (never owned or destroyed by the program)
1372
- * @param options - Optional total handler and labels
1373
- * @returns A {@link ProgramInterface}
1374
- * @throws {@link RaterError} `'DEFINITION'` when the definition fails {@link isProgramDefinition}
1375
- *
1376
- * @example
1377
- * ```ts
1378
- * import { createLogicalReasoner, createQuantitativeReasoner, createReason } from '@orkestrel/reason'
1379
- * import { createProgram, programDefinition } from '@orkestrel/rater'
1380
- *
1381
- * const reason = createReason({ reasoners: [createQuantitativeReasoner(), createLogicalReasoner()], bail: false })
1382
- * const program = createProgram(programDefinition('p1', 'Program', []), reason)
1383
- * ```
1384
- */
1385
- function createProgram(definition, reason, options) {
1386
- const id = isRecord(definition) ? definition.id : void 0;
1387
- if (!isProgramDefinition(definition)) throw new RaterError("DEFINITION", "Program definition failed validation", { program: id });
1388
- return new Program(definition, reason, options);
1389
- }
1390
- /**
1391
- * Create an ordered manager over compiled programs (AGENTS §9), built over an
1392
- * injected, shared reasoning engine.
1393
- *
1394
- * @param reason - The shared reasoning engine, injected into every compiled program
1395
- * @param options - Optional total handler, labels, validation policy, and emitter hooks
1396
- * @returns A {@link ProgramManagerInterface}
1397
- *
1398
- * @example
1399
- * ```ts
1400
- * import { createLogicalReasoner, createQuantitativeReasoner, createReason } from '@orkestrel/reason'
1401
- * import { createProgramManager, programDefinition } from '@orkestrel/rater'
1402
- *
1403
- * const reason = createReason({ reasoners: [createQuantitativeReasoner(), createLogicalReasoner()], bail: false })
1404
- * const manager = createProgramManager(reason)
1405
- * manager.add(programDefinition('p1', 'Program', []))
1406
- * manager.destroy()
1407
- * ```
1408
- */
1409
- function createProgramManager(reason, options) {
1410
- return new ProgramManager(reason, options);
1411
- }
1412
- /**
1413
- * Build a {@link ProgramDefinition}.
1414
- *
1415
- * @param id - The program id
1416
- * @param name - The display name
1417
- * @param lines - The program's rateable lines
1418
- * @param overrides - Optional {@link ProgramDefinition} fields merged over the defaults
1419
- * @returns A fresh program definition
1420
- *
1421
- * @example
1422
- * ```ts
1423
- * import { programDefinition } from '@orkestrel/rater'
1424
- *
1425
- * programDefinition('p1', 'Program', [])
1426
- * ```
1427
- */
1428
- function programDefinition(id, name, lines, overrides) {
1429
- return {
1430
- id,
1431
- name,
1432
- lines,
1433
- ...overrides
1434
- };
1435
- }
1436
- /**
1437
499
  * Build a {@link LineDefinition}.
1438
500
  *
1439
501
  * @param id - The line id
@@ -1459,93 +521,33 @@ function lineDefinition(id, name, rate, overrides) {
1459
521
  };
1460
522
  }
1461
523
  /**
1462
- * Build a {@link PassDefinition}.
524
+ * Build a {@link RatingDefinition}.
1463
525
  *
1464
- * @param definition - The pass's logical or quantitative definition
1465
- * @param line - The line id this pass is scoped to, when it is line-scoped
1466
- * @returns A fresh pass definition
526
+ * @param id - The rating id
527
+ * @param name - The display name
528
+ * @param lines - The rating's ordered lines
529
+ * @param overrides - Optional {@link RatingDefinition} fields merged over the defaults
530
+ * @returns A fresh rating definition
1467
531
  *
1468
532
  * @example
1469
533
  * ```ts
534
+ * import { lineDefinition, ratingDefinition } from '@orkestrel/rater'
1470
535
  * import { quantitativeDefinition } from '@orkestrel/reason'
1471
- * import { passDefinition } from '@orkestrel/rater'
1472
536
  *
1473
- * passDefinition(quantitativeDefinition('surcharge', 'Surcharge', []))
537
+ * ratingDefinition('r1', 'Rating', [
538
+ * lineDefinition('base', 'Base Amount', quantitativeDefinition('base', 'Base', [])),
539
+ * ])
1474
540
  * ```
1475
541
  */
1476
- function passDefinition(definition, line) {
1477
- return {
1478
- definition,
1479
- ...line === void 0 ? {} : { line }
1480
- };
1481
- }
1482
- /**
1483
- * Build a {@link Ruling}.
1484
- *
1485
- * @param effect - The determination effect a fired rule routes to
1486
- * @param line - The line id this ruling is scoped to, when it is line-scoped
1487
- * @param message - An interpolated message template, when the ruling carries one
1488
- * @returns A fresh ruling
1489
- *
1490
- * @example
1491
- * ```ts
1492
- * import { rulingDefinition } from '@orkestrel/rater'
1493
- *
1494
- * rulingDefinition('restriction', 'base', 'Amount exceeds {{limit}}')
1495
- * ```
1496
- */
1497
- function rulingDefinition(effect, line, message) {
1498
- return {
1499
- effect,
1500
- ...line === void 0 ? {} : { line },
1501
- ...message === void 0 ? {} : { message }
1502
- };
1503
- }
1504
- /**
1505
- * Build a {@link Notice}.
1506
- *
1507
- * @param id - The notice id
1508
- * @param message - An interpolated message template
1509
- * @param line - The line id this notice is scoped to, when it is line-scoped
1510
- * @returns A fresh notice
1511
- *
1512
- * @example
1513
- * ```ts
1514
- * import { noticeDefinition } from '@orkestrel/rater'
1515
- *
1516
- * noticeDefinition('n1', 'Rated on {{date}}')
1517
- * ```
1518
- */
1519
- function noticeDefinition(id, message, line) {
542
+ function ratingDefinition(id, name, lines, overrides) {
1520
543
  return {
1521
544
  id,
1522
- message,
1523
- ...line === void 0 ? {} : { line }
1524
- };
1525
- }
1526
- /**
1527
- * Build an {@link AggregateDefinition}.
1528
- *
1529
- * @param fields - The batch fields to sum
1530
- * @param by - The partition key field, when the aggregate is partitioned
1531
- * @param gates - The logical definition gating aggregate determinations, when present
1532
- * @returns A fresh aggregate definition
1533
- *
1534
- * @example
1535
- * ```ts
1536
- * import { aggregateDefinition } from '@orkestrel/rater'
1537
- *
1538
- * aggregateDefinition(['amount'])
1539
- * ```
1540
- */
1541
- function aggregateDefinition(fields, by, gates) {
1542
- return {
1543
- fields,
1544
- ...by === void 0 ? {} : { by },
1545
- ...gates === void 0 ? {} : { gates }
545
+ name,
546
+ lines,
547
+ ...overrides
1546
548
  };
1547
549
  }
1548
550
  //#endregion
1549
- export { AGGREGATE_KEY, DEFAULT_RATER_VALIDATE, EFFECT_ELIGIBILITIES, ELIGIBILITY_DECISIONS, ELIGIBILITY_PRECEDENCE, OUTCOME_KEY, Program, ProgramManager, Rater, RaterError, STATUS_PRECEDENCE, aggregateDefinition, aggregateFields, aggregateGroups, aggregateProjection, aggregateRecord, aggregateSums, assertSubject, authorityToDeterminations, checkPremises, combineEligibilities, completeTallies, createProgram, createProgramManager, createRater, decideEligibility, deriveDeterminationEligibility, deriveStatus, describeComparison, describeExpression, describePremise, describeValue, emptySums, emptyTallies, filterLineDeterminations, filterProgramDeterminations, findMissingLineReferences, findReservedCollisions, findRule, groupFor, hasReservedKey, interpolateMessage, isAggregateDefinition, isDecision, isEffect, isEligibility, isLineDefinition, isNotice, isPassDefinition, isProgramDefinition, isRaterError, isRuling, isRulings, isStage, isStatus, lineDefinition, logicalPremises, noticeDefinition, noticesToDeterminations, outcomeProjection, passDefinition, premiseCheck, programDefinition, programResult, ratedLine, resultsWorksheet, rulesToDeterminations, rulingDefinition, sumAmounts, tallySubject, worksheetFactor, worksheetGroup, worksheetStep, worksheetSteps };
551
+ export { Rater, RaterError, checkEvidence, createRater, evidenceCheck, isLineDefinition, isRaterError, isRatingDefinition, isStage, lineDefinition, ratedLine, ratingDefinition, resultsWorksheet, sumAmounts, worksheetFactor, worksheetGroup, worksheetStep, worksheetSteps };
1550
552
 
1551
553
  //# sourceMappingURL=index.js.map