@actuarial-ts/core 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +65 -215
  2. package/dist/casualtyDiagnostics.d.ts +19 -49
  3. package/dist/casualtyDiagnostics.d.ts.map +1 -1
  4. package/dist/casualtyDiagnostics.js +84 -114
  5. package/dist/casualtyDiagnostics.js.map +1 -1
  6. package/dist/diagnosticAggregation.d.ts +29 -0
  7. package/dist/diagnosticAggregation.d.ts.map +1 -0
  8. package/dist/diagnosticAggregation.js +50 -0
  9. package/dist/diagnosticAggregation.js.map +1 -0
  10. package/dist/diagnosticDefinitions.d.ts +286 -0
  11. package/dist/diagnosticDefinitions.d.ts.map +1 -0
  12. package/dist/diagnosticDefinitions.js +1026 -0
  13. package/dist/diagnosticDefinitions.js.map +1 -0
  14. package/dist/diagnosticDerivations.d.ts +12 -0
  15. package/dist/diagnosticDerivations.d.ts.map +1 -0
  16. package/dist/diagnosticDerivations.js +80 -0
  17. package/dist/diagnosticDerivations.js.map +1 -0
  18. package/dist/diagnosticExposure.d.ts +51 -0
  19. package/dist/diagnosticExposure.d.ts.map +1 -0
  20. package/dist/diagnosticExposure.js +65 -0
  21. package/dist/diagnosticExposure.js.map +1 -0
  22. package/dist/diagnosticExpressions.d.ts +56 -0
  23. package/dist/diagnosticExpressions.d.ts.map +1 -0
  24. package/dist/diagnosticExpressions.js +104 -0
  25. package/dist/diagnosticExpressions.js.map +1 -0
  26. package/dist/diagnosticFormulas.d.ts +195 -0
  27. package/dist/diagnosticFormulas.d.ts.map +1 -0
  28. package/dist/diagnosticFormulas.js +85 -0
  29. package/dist/diagnosticFormulas.js.map +1 -0
  30. package/dist/diagnosticIdentity.d.ts +237 -0
  31. package/dist/diagnosticIdentity.d.ts.map +1 -0
  32. package/dist/diagnosticIdentity.js +454 -0
  33. package/dist/diagnosticIdentity.js.map +1 -0
  34. package/dist/diagnosticPeriods.d.ts +16 -0
  35. package/dist/diagnosticPeriods.d.ts.map +1 -0
  36. package/dist/diagnosticPeriods.js +98 -0
  37. package/dist/diagnosticPeriods.js.map +1 -0
  38. package/dist/diagnosticPreparation.d.ts +98 -0
  39. package/dist/diagnosticPreparation.d.ts.map +1 -0
  40. package/dist/diagnosticPreparation.js +169 -0
  41. package/dist/diagnosticPreparation.js.map +1 -0
  42. package/dist/diagnosticReview.d.ts +44 -0
  43. package/dist/diagnosticReview.d.ts.map +1 -0
  44. package/dist/diagnosticReview.js +84 -0
  45. package/dist/diagnosticReview.js.map +1 -0
  46. package/dist/diagnosticRules.d.ts +32 -0
  47. package/dist/diagnosticRules.d.ts.map +1 -0
  48. package/dist/diagnosticRules.js +32 -0
  49. package/dist/diagnosticRules.js.map +1 -0
  50. package/dist/diagnosticRunner.d.ts +85 -0
  51. package/dist/diagnosticRunner.d.ts.map +1 -0
  52. package/dist/diagnosticRunner.js +117 -0
  53. package/dist/diagnosticRunner.js.map +1 -0
  54. package/dist/index.d.ts +24 -1
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +12 -1
  57. package/dist/index.js.map +1 -1
  58. package/dist/types.d.ts +17 -0
  59. package/dist/types.d.ts.map +1 -1
  60. package/dist/types.js +114 -0
  61. package/dist/types.js.map +1 -1
  62. package/dist/version.d.ts +3 -0
  63. package/dist/version.d.ts.map +1 -0
  64. package/dist/version.js +3 -0
  65. package/dist/version.js.map +1 -0
  66. package/package.json +2 -2
  67. package/src/casualtyDiagnostics.ts +79 -187
  68. package/src/diagnosticAggregation.ts +79 -0
  69. package/src/diagnosticDefinitions.ts +1375 -0
  70. package/src/diagnosticDerivations.ts +89 -0
  71. package/src/diagnosticExposure.ts +91 -0
  72. package/src/diagnosticExpressions.ts +173 -0
  73. package/src/diagnosticFormulas.ts +150 -0
  74. package/src/diagnosticIdentity.ts +767 -0
  75. package/src/diagnosticPeriods.ts +117 -0
  76. package/src/diagnosticPreparation.ts +233 -0
  77. package/src/diagnosticReview.ts +91 -0
  78. package/src/diagnosticRules.ts +69 -0
  79. package/src/diagnosticRunner.ts +110 -0
  80. package/src/index.ts +156 -1
  81. package/src/types.ts +161 -0
  82. package/src/version.ts +2 -0
  83. package/dist/metricDiagnostics.d.ts +0 -212
  84. package/dist/metricDiagnostics.d.ts.map +0 -1
  85. package/dist/metricDiagnostics.js +0 -627
  86. package/dist/metricDiagnostics.js.map +0 -1
  87. package/src/metricDiagnostics.ts +0 -876
package/README.md CHANGED
@@ -1,244 +1,94 @@
1
1
  # @actuarial-ts/core
2
2
 
3
- A pure, zero-dependency P&C loss reserving engine for TypeScript. The
4
- deterministic shelf: triangles, development factors, chain ladder,
5
- Bornhuetter-Ferguson, Benktander, Cape Cod (with the Gluck decay
6
- generalization), Expected Claims, frequency-severity, Berquist-Sherman,
7
- Munich chain ladder, case-outstanding development, Fisher-Lange,
8
- salvage/subrogation, ULAE, tail fitting, large-loss capping and ILF
9
- restoration, trend and premium on-leveling, discounting (built to the June
10
- 2026 ASOP No. 20), and assumption diagnostics. The stochastic layer, fully
11
- seeded and reproducible: Mack standard errors, the ODP bootstrap,
12
- Merz-Wuthrich one-year risk, and Clark growth-curve MLE — every method
13
- validated against published actuarial literature where published values
14
- exist.
15
-
16
- `@actuarial-ts/core` is the numeric kernel of the actuarial-ts SDK. It is
17
- **designed to support the actuary's compliance with the Actuarial Standards of
18
- Practice** (ASOP Nos. 43, 23, 41, 56, 20, and 21) by making
19
- methods, assumptions, and their diagnostics explicit and reportable.
20
- Responsibility for compliance remains with the credentialed actuary; no
21
- software can be "ASOP-approved" and this one does not claim to be.
3
+ Pure, framework-free P&C actuarial math for TypeScript. It includes triangles, deterministic and stochastic reserving, trends/on-leveling, limits and ILFs, discounting, and generalized definition-driven casualty diagnostics.
4
+
5
+ The package is designed to support an actuary’s work under applicable ASOPs; it does not make a work product compliant and is not “ASOP-approved.” The credentialed actuary remains responsible for data, assumptions, selections, review, and communication.
22
6
 
23
7
  ## Install
24
8
 
25
9
  ```bash
26
- npm install @actuarial-ts/core
10
+ npm install @actuarial-ts/core@0.6.0
27
11
  ```
28
12
 
29
- ESM, TypeScript-first, zero runtime dependencies, Node >= 20.
13
+ ESM, TypeScript-first, zero runtime dependencies, Node 20+.
30
14
 
31
- ## Quick start
15
+ ## Reserving quick start
32
16
 
33
17
  ```ts
34
- import {
35
- buildTriangles,
36
- computeDevelopmentFactors,
37
- runChainLadder,
38
- runMack,
39
- fitAllTails,
40
- } from "@actuarial-ts/core";
41
-
42
- // One row per claim per evaluation snapshot (the standard loss-run shape).
43
- const { paid, incurred } = buildTriangles(claimSnapshots, {
44
- cadence: "annual",
45
- asOfDate: "2025-12-31",
46
- });
18
+ import { buildTriangles, computeDevelopmentFactors, runChainLadder, runMack } from "@actuarial-ts/core";
47
19
 
48
- // The averages menu: all-year/n-year straight and volume-weighted, medial,
49
- // geometric. Selection is YOUR judgment; the engine never picks for you.
50
- const factors = computeDevelopmentFactors(paid);
51
- const selected = factors.averages.find((a) => a.spec.key === "all-wtd")!.values;
20
+ const { paid } = buildTriangles(claimSnapshots, { cadence: "annual", asOfDate: "2025-12-31" });
21
+ const selected = computeDevelopmentFactors(paid).averages.find((item) => item.spec.key === "all-wtd")!.values;
22
+ const chainLadder = runChainLadder(paid, { selected, tailFactor: 1.02 });
23
+ const mack = runMack(paid, { selected, tailFactor: 1.02 });
24
+ ```
52
25
 
53
- const tails = fitAllTails(selected);
54
- const tail = tails.exponentialDecay.valid ? tails.exponentialDecay.tailFactor : 1;
26
+ Unobservable triangle cells are `null`. Volume-weighted factors are sum/sum over rows where both cells exist. CDFs multiply right-to-left, tail last. Missing, zero, or negative divisors yield `null`, never `NaN`.
55
27
 
56
- const cl = runChainLadder(paid, { selected, tailFactor: tail });
57
- const mack = runMack(paid, { selected, tailFactor: tail });
28
+ ## Generalized diagnostics
58
29
 
59
- console.log(cl.totals.unpaid, mack.totals.standardError, cl.warnings);
60
- ```
30
+ The model deliberately separates five concerns:
61
31
 
62
- ## The contract
63
-
64
- Three rules hold everywhere:
65
-
66
- 1. **Null is a first-class citizen.** Unobservable triangle cells are `null`.
67
- Division by a missing, zero, or negative denominator yields `null` ("no
68
- factor") — never an exception, never `NaN`.
69
- 2. **Three-tier severity.** Impossible input throws `ReservingError` with a
70
- machine-readable code from the exported `RESERVING_ERROR_CODES` registry.
71
- Degraded-but-legal situations compute anyway and explain themselves in the
72
- result's `warnings: string[]`. Missing data is `null`, not an error.
73
- 3. **Judgment belongs to the caller.** The engine computes evidence (factor
74
- menus, tail fits, diagnostics) and applies *your* selections (LDFs, tails,
75
- a-prioris, trends, caps). It never silently selects.
76
-
77
- ## Method inventory
78
-
79
- | Module | Methods | Primary literature |
80
- |---|---|---|
81
- | `triangle` | `buildTriangles` (7 triangle kinds from claim-level snapshots, annual/quarterly), `triangleFromGrid` | Friedland, *Estimating Unpaid Claims Using Basic Techniques* |
82
- | `factors` | `computeDevelopmentFactors` (averages menu), `factorVolatility` | Friedland ch. 7; Mack (1993) factor conventions |
83
- | `chainladder` | `runChainLadder` | Friedland ch. 7 |
84
- | `mack` | `runMack` — distribution-free standard errors on the selected basis, with tail | Mack (1993) ASTIN 23(2); Mack (1999) ASTIN 29(2) |
85
- | `bf` | `runBornhuetterFerguson` (per-origin/global/derived a-priori) | Bornhuetter & Ferguson (1972) |
86
- | `elrMethods` | `runCapeCod`, `runExpectedClaims` | Stanard-Buhlmann; Friedland chs. 8, 10 |
87
- | `tail` | `fitTail`, `fitAllTails` (exponential decay, Sherman inverse power, validity gates) | Sherman (1984); Boor (2006) |
88
- | `berquist` | `berquistCaseAdequacy`, `berquistSettlement` | Berquist & Sherman (1977); Friedland ch. 13 |
89
- | `benktander` | `runBenktander` — the iterated BF credibility blend | Mack (2000) ASTIN 30(2); Benktander (1976) |
90
- | `freqSev` | `runFrequencySeverity`, `severityTriangle` | Friedland ch. 11 |
91
- | `munichChainLadder` | `runMunichChainLadder` — closes the paid/incurred gap | Quarg & Mack (2004), Variance 2:2 |
92
- | `caseOutstanding` | `runCaseOutstanding` | Friedland ch. 12 |
93
- | `fisherLange` | `runFisherLange` — disposal-rate frequency-severity | Fisher & Lange (1973); Friedland ch. 11 |
94
- | `salvageSubro` | `runSalvageSubro`, `netOfRecoveries` | Friedland ch. 14 |
95
- | `ulae` | `ulaeRatios`, `ulaeReserve`, `ULAE_WEIGHT_PRESETS` | Conger & Nolibos (2003); Kittel (1981) |
96
- | `discounting` | `payoutPatternFromChainLadder`, `discountUnpaid` | ASOP No. 20 (June 2026 edition) |
97
- | `stochastic` | `createRng` (seeded), `summarizeSample`, `StochasticResult` | — |
98
- | `triangleAlgebra` | `cumulativeToIncremental`, `incrementalToCumulative`, `addTriangles`, `subtractTriangles` | — |
99
- | `odpBootstrap` | `odpFit` (GLM == chain ladder identity), `runOdpBootstrap` | England & Verrall (1999/2002); Shapland, CAS Monograph 4 |
100
- | `merzWuthrich` | `runMerzWuthrich` — one-year CDR MSEP vs Mack's ultimate view | Merz & Wuthrich (2008), CAS E-Forum |
101
- | `clark` | `clarkGrowth`, `runClarkLdf`, `runClarkCapeCod` — MLE + delta-method variances | Clark (2003), CAS Forum |
102
- | `capping` | `capClaims`, `claimSizeDiagnostics` (per-occurrence caps, indexed) | standard large-loss practice |
103
- | `ilf` | censored-MLE severity fits (lognormal, Pareto), Kaplan-Meier checks, ILF table interpolation, uncap factors | Klugman et al., *Loss Models*; standard ILF practice |
104
- | `trend` | `analyzeTrend`, `trendValue` (log-linear, windowed) | Werner & Modlin, *Basic Ratemaking* ch. 6 |
105
- | `onlevel` | `parallelogramOnLevel` (exact piecewise-linear earning geometry) | Werner & Modlin ch. 5 |
106
- | `diagnostics` | `runDiagnostics` (paid/incurred drift, case adequacy, closure rates), `calendarYearTest` | Mack (1994) calendar-year rank test |
107
- | `metricDiagnostics` | generic ratio-of-sums metrics, claim-level amount layers, emergence/triangle/maturity views, optional 20-metric casualty preset | actuarial diagnostic practice |
108
- | `periods` | quarterly parse/format/compare, development age, fiscal/policy mapping, complete-quarter cutoffs | explicit SDK conventions |
109
- | `canonical` | `canonicalJson` (RFC 8785 / JCS canonical serialization), `fnv1a64` (integrity tagging aid — not a security control) | RFC 8785 |
110
-
111
- ## Quarterly metric diagnostics
112
-
113
- `runMetricDiagnostics` is the generic engine behind the optional
114
- `CASUALTY_QUARTERLY_METRICS` preset. A metric is a versioned definition with
115
- caller-selected additive component expressions. The engine sums components at
116
- the requested group/origin/valuation grain and divides once; it never averages
117
- row ratios. Each result retains the raw numerator, denominator, component
118
- values, labels, basis, scale, and structured warnings.
32
+ 1. A measure declares source, quantity kind, unit, development semantics, sum aggregation, missingness, and its population/basis.
33
+ 2. A formula template declares reusable arithmetic over typed roles.
34
+ 3. An instance binds formula roles to measure expressions.
35
+ 4. Calculation identity covers arithmetic, bindings, and all dependent measure/population/basis semantics.
36
+ 5. Presentation and review rules remain visible in full definition identity without pretending to change the arithmetic.
119
37
 
120
38
  ```ts
121
39
  import {
122
- CASUALTY_QUARTERLY_METRICS,
40
+ CASUALTY_FORMULA_TEMPLATES,
41
+ compileDiagnosticDefinition,
42
+ createCasualtyMetricInstances,
43
+ prepareDiagnosticData,
123
44
  runMetricDiagnostics,
124
45
  } from "@actuarial-ts/core";
125
46
 
126
- const result = runMetricDiagnostics({
127
- losses: [{
128
- id: "snapshot-1",
129
- group: "commercial-auto",
130
- origin: "2025Q1",
131
- valuation: "2025Q1",
132
- ageMonths: 3,
133
- policyPeriod: "PY2024",
134
- measures: {
135
- reportedCount: 80,
136
- openCount: 30,
137
- closedNoPayCount: 20,
138
- closedWithPayCount: 30,
139
- paid250: 450_000,
140
- incurred250: 700_000,
141
- paidPrimary: 600_000,
142
- incurredPrimary: 950_000,
143
- },
144
- }],
145
- exposures: [{
146
- key: "fleet-2025Q1",
147
- group: "commercial-auto",
148
- origin: "2025Q1",
149
- measures: { exposure: 1_600_000 },
150
- }],
151
- metrics: CASUALTY_QUARTERLY_METRICS,
47
+ const instances = createCasualtyMetricInstances({
48
+ counts: { reported: "reported", open: "open", closedNoPay: "closed-no-pay", closedWithPay: "closed-with-pay" },
49
+ exposure: "earned-vehicle-years",
50
+ amountBindings: [
51
+ { id: "gross", paid: "gross-paid", incurred: "gross-incurred" },
52
+ { id: "primary-250k", paid: "primary-paid", incurred: "primary-incurred" },
53
+ ],
152
54
  });
153
55
 
154
- const reported = result.emergence[0]!.metrics["reported-frequency"]!;
155
- console.log(reported.value, reported.rawNumerator, reported.rawDenominator);
156
- console.log(result.triangles[0]!.values, result.latestDiagonal);
56
+ const compiled = compileDiagnosticDefinition({
57
+ diagnosticDefinitionVersion: "1.0.0",
58
+ id: "fleet-diagnostics",
59
+ version: "1.0.0",
60
+ lossRowGrain: "aggregate",
61
+ measures,
62
+ countPopulations,
63
+ exposureBases,
64
+ amountBases,
65
+ derivedMeasures: [],
66
+ formulas: CASUALTY_FORMULA_TEMPLATES,
67
+ instances,
68
+ reviewRules,
69
+ periodAxis,
70
+ });
71
+
72
+ const prepared = prepareDiagnosticData({ definition: compiled, losses, exposures });
73
+ const result = runMetricDiagnostics({ prepared, groupMap: { fleet: "all-fleet" } });
157
74
  ```
158
75
 
159
- Missing components remain null by default. An explicit `sparsePolicy:
160
- "zero-fill"` is required to treat sparse values as zero. A missing,
161
- non-finite, zero, or negative denominator produces a null metric and an
162
- `INVALID_DENOMINATOR` warning; negative numerators remain valid.
163
- `diagnosticWarningToFinding` adapts these warning payloads to the existing core
164
- `DiagnosticFinding` severity vocabulary when a consumer wants one findings
165
- stream; ASOP-oriented `DataReviewReport` statuses remain a separate contract.
166
-
167
- Use `createCasualtyQuarterlyMetrics` to override the source component keys,
168
- exposure key, frequency scale/unit, definition version, basis labels, and
169
- display metadata. The exported `CASUALTY_QUARTERLY_METRICS` constant is the
170
- standard one-million-scale configuration. `groupMap` combines arbitrary source
171
- groups at a requested output grain by summing their components first. When
172
- exposure rows are supplied, every contributing source group/origin must have
173
- exposure; a missing contributor makes the combined exposure null with an
174
- `INCOMPLETE_EXPOSURE` warning instead of using a partial denominator. Dated
175
- exposure copies honor valuation filters, while exposure rows without a
176
- valuation remain timeless.
177
-
178
- ### Amount layers
179
-
180
- `deriveAmountLayers` evaluates a declarative layer on each claim row before
181
- aggregation. `CASUALTY_AMOUNT_LAYERS` documents two reference bases:
182
-
183
- - `$250K pre-capped total` reads already-limited paid and incurred components
184
- as additive measures. The SDK does not attempt to recreate a lost
185
- claim-level cap from an aggregate.
186
- - `Primary: $1M capped indemnity plus unlimited expense` caps paid and incurred
187
- indemnity on each claim row, then adds expense without a cap.
188
-
189
- This distinction is intentional: capping an aggregate after summation is not
190
- equivalent to a claim-level layer and is not offered by the API.
191
- `createCasualtyAmountLayers` configures every source/output key, identifier,
192
- display label, and indemnity limit; arbitrary caller layers can be authored
193
- directly as `AmountLayerDefinition` values.
194
-
195
- ### Views and periods
196
-
197
- The engine derives audited emergence points, nullable metric triangles, and a
198
- ragged latest diagonal from the same aggregated records. `sameMaturity` and
199
- `commonMaturity` select comparable points for any caller group IDs.
200
- `parseQuarterPeriod`, `compareQuarterPeriods`, `developmentAgeMonths`,
201
- `policyPeriodLabel`, `completeQuarterCutoff`, and
202
- `completeQuarterlyCutoffs` make period assumptions explicit. The default
203
- quarter-end convention starts at age 3; select the `elapsed` convention only
204
- for genuine age-zero observations.
205
-
206
- Core results deliberately contain no provenance, persistence state, or
207
- arbitrary application filter state. Use `createDiagnosticsProvenance` from
208
- `@actuarial-ts/compliance`, embed its record in
209
- `createBundle(...).parameters`, and record material judgment in the assumption
210
- ledger. Interchange consumers can carry the same record in `extensions`. No
211
- diagnostic-specific interchange schema is claimed.
212
-
213
- ## Validation against published results
214
-
215
- The test suite reproduces the following, each transcribed from the primary
216
- source into `test/fixtures/`. Five of them additionally carry a full research
217
- transcription with context and caveats under `docs/research/` (Clark,
218
- Merz-Wuthrich, England/Verrall, Munich, ULAE); for the rest the transcription
219
- notes live in the fixture file itself.
220
-
221
- - Mack (1993), ASTIN 23(2): Taylor/Ashe and mortgage-guarantee factors,
222
- reserves, sigma-squared (including the extrapolated column), standard errors.
223
- - Mack (1994), CAS Forum: the RAA triangle's calendar-year test (Appendix H)
224
- and factor-correlation test (Appendix G), every printed statistic.
225
- - Mack (1999), ASTIN 29(2): ultimates under the published 1.05 tail.
226
- - Mack (2000), ASTIN 30(2): the Benktander numerical example.
227
- - Gluck (1997), PCAS LXXXIV: Generalized Cape Cod Tables 1-4.
228
- - England (2002), IME 31: ODP bootstrap prediction errors and percentiles
229
- on Taylor/Ashe — all nine published accident years plus the Table 3
230
- predictive distribution. Asserted within the sampling error the publication
231
- itself exhibits, because a bootstrap figure is one finite sample and cannot
232
- be reproduced to the digit the way Mack's closed form can.
233
- - Merz & Wuthrich (2008): the Table 4 one-year CDR volatilities.
234
- - Clark (2003): both methods' fitted parameters, reserves, and variance
235
- decompositions (to ~1e-5).
236
- - Quarg & Mack (2004): the fire-portfolio example's every printed parameter
237
- row and projected cell.
238
-
239
- These published-value tests are the package's change contract: math changes
240
- are wrong until they pass.
76
+ The six built-in formulas are basis-independent. The factory creates ten count instances plus six per amount basis (`10 + 6 × basisCount`): one basis produces 16, two produce 22. A `$250K`, primary, gross, net, or ceded calculation is represented by caller-declared amount measures and a structured `AmountBasisDefinition`; it does not need a separate capped formula. Claim-level caps use `claim-layer` derivations before aggregation. Pre-limited external values record their source/transformation instead of implying the SDK recreated an unavailable claim-level operation.
77
+
78
+ All metrics are ratio-of-sums: measures are aggregated at source-group/origin/valuation, groups are mapped and merged, then division happens once. Measure-local `missing: "unknown" | "zero"` is explicit. Exposure timing is either `origin-static` or `valuation-specific`. Calendar and ordered axes derive normalized origins, valuations, development ages, and units; input rows cannot assert a trusted age.
79
+
80
+ Compilation validates the whole graph atomically: IDs, sources, role types, compatibility groups, development semantics, derivation acyclicity, expression limits, rule operands, basis/population references, and period coordinates. Authentic compiled/prepared objects are owner-branded and frozen. Formula, calculation, definition, preparation, and result identities are deterministic FNV-1a/JCS integrity aids—not cryptographic signatures.
81
+
82
+ See the generated [formula and instance catalog](https://github.com/yerromnitsuj/actng/blob/v0.6.0/docs/reference/diagnostic-formulas.md) and [0.6 migration guide](https://github.com/yerromnitsuj/actng/blob/v0.6.0/docs/migrations/0.6-generalized-diagnostics.md).
83
+
84
+ ## Main method families
85
+
86
+ - Reserving: chain ladder, Mack, Bornhuetter-Ferguson, Benktander, Cape Cod/Gluck, Expected Claims, frequency-severity, Fisher-Lange, Munich chain ladder, Clark, ODP bootstrap, and Merz-Wüthrich one-year risk.
87
+ - Adjustments: Berquist-Sherman, salvage/subrogation, ULAE, tails, trends, premium on-leveling, discounting, capping, severity models, and ILFs.
88
+ - Infrastructure: triangle algebra, seeded RNG, RFC 8785 canonical JSON, integrity tags, traditional triangle diagnostics, and generalized metric diagnostics.
89
+
90
+ Published-value tests are the numerical contract. A reserving math change is not acceptable until those fixtures still pass.
241
91
 
242
92
  ## License
243
93
 
244
- Apache-2.0. Copyright 2026 Justin Morrey.
94
+ Apache-2.0. See LICENSE and NOTICE.
@@ -1,53 +1,23 @@
1
- import { type AmountLayerDefinition, type MetricDefinition } from "./metricDiagnostics.js";
2
- export interface CasualtyDiagnosticComponentKeys {
3
- reported: string;
4
- open: string;
5
- closedNoPay: string;
6
- closedWithPay: string;
7
- exposure: string;
8
- paid250: string;
9
- incurred250: string;
10
- paidPrimary: string;
11
- incurredPrimary: string;
1
+ import { CASUALTY_FORMULA_TEMPLATES } from "./diagnosticFormulas.js";
2
+ import type { DiagnosticMetricInstance, DiagnosticMetricPresentation } from "./diagnosticDefinitions.js";
3
+ export { CASUALTY_FORMULA_TEMPLATES };
4
+ export interface CasualtyCountBindings {
5
+ readonly reported: string;
6
+ readonly open: string;
7
+ readonly closedNoPay: string;
8
+ readonly closedWithPay: string;
12
9
  }
13
- export declare const CASUALTY_DIAGNOSTIC_COMPONENTS: Readonly<CasualtyDiagnosticComponentKeys>;
14
- type MetricDisplayOverride = Partial<Pick<MetricDefinition, "displayName" | "description" | "unit" | "numeratorLabel" | "denominatorLabel" | "basis">>;
15
- export interface CasualtyMetricPresetOptions {
16
- /** Caller source/output measure keys, including the exposure key. */
17
- components?: Partial<CasualtyDiagnosticComponentKeys>;
18
- frequencyScale?: number;
19
- frequencyUnit?: MetricDefinition["unit"];
20
- definitionVersion?: string;
21
- basisLabels?: {
22
- limited250?: string;
23
- primary?: string;
24
- counts?: string;
25
- };
26
- /** Per-metric display/basis overrides; formulas remain the documented preset formulas. */
27
- displayOverrides?: Readonly<Record<string, MetricDisplayOverride>>;
10
+ export interface CasualtyAmountBinding {
11
+ readonly id: string;
12
+ readonly paid: string;
13
+ readonly incurred: string;
28
14
  }
29
- /** Builds the optional 20-metric reference preset from caller-selected keys and display metadata. */
30
- export declare function createCasualtyQuarterlyMetrics(options?: CasualtyMetricPresetOptions): readonly MetricDefinition[];
31
- export declare const CASUALTY_QUARTERLY_METRICS: readonly MetricDefinition[];
32
- export interface CasualtyAmountLayerOptions {
33
- components?: Partial<Pick<CasualtyDiagnosticComponentKeys, "paid250" | "incurred250" | "paidPrimary" | "incurredPrimary">>;
34
- limited250?: {
35
- id?: string;
36
- displayName?: string;
37
- paidSourceMeasure?: string;
38
- incurredSourceMeasure?: string;
39
- };
40
- primary?: {
41
- id?: string;
42
- displayName?: string;
43
- indemnityPaidMeasure?: string;
44
- indemnityIncurredMeasure?: string;
45
- expensePaidMeasure?: string;
46
- expenseIncurredMeasure?: string;
47
- indemnityLimit?: number;
48
- };
15
+ export type DiagnosticMetricPresentationOverride = Partial<DiagnosticMetricPresentation>;
16
+ export interface CreateCasualtyMetricInstancesInput {
17
+ readonly counts: CasualtyCountBindings;
18
+ readonly exposure: string;
19
+ readonly amountBindings: readonly CasualtyAmountBinding[];
20
+ readonly presentationOverrides?: Readonly<Record<string, DiagnosticMetricPresentationOverride>>;
49
21
  }
50
- export declare function createCasualtyAmountLayers(options?: CasualtyAmountLayerOptions): readonly AmountLayerDefinition[];
51
- export declare const CASUALTY_AMOUNT_LAYERS: readonly AmountLayerDefinition[];
52
- export {};
22
+ export declare function createCasualtyMetricInstances(input: CreateCasualtyMetricInstancesInput): readonly DiagnosticMetricInstance[];
53
23
  //# sourceMappingURL=casualtyDiagnostics.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"casualtyDiagnostics.d.ts","sourceRoot":"","sources":["../src/casualtyDiagnostics.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,qBAAqB,EAE1B,KAAK,gBAAgB,EAEtB,MAAM,wBAAwB,CAAC;AAahC,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,eAAO,MAAM,8BAA8B,EAAE,QAAQ,CAAC,+BAA+B,CAU3E,CAAC;AAEX,KAAK,qBAAqB,GAAG,OAAO,CAAC,IAAI,CACvC,gBAAgB,EAChB,aAAa,GAAG,aAAa,GAAG,MAAM,GAAG,gBAAgB,GAAG,kBAAkB,GAAG,OAAO,CACzF,CAAC,CAAC;AAEH,MAAM,WAAW,2BAA2B;IAC1C,qEAAqE;IACrE,UAAU,CAAC,EAAE,OAAO,CAAC,+BAA+B,CAAC,CAAC;IACtD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAC;IACzC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACzE,0FAA0F;IAC1F,gBAAgB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC,CAAC;CACpE;AA6CD,qGAAqG;AACrG,wBAAgB,8BAA8B,CAC5C,OAAO,GAAE,2BAAgC,GACxC,SAAS,gBAAgB,EAAE,CAwC7B;AAED,eAAO,MAAM,0BAA0B,6BAAmC,CAAC;AAE3E,MAAM,WAAW,0BAA0B;IACzC,UAAU,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,+BAA+B,EAAE,SAAS,GAAG,aAAa,GAAG,aAAa,GAAG,iBAAiB,CAAC,CAAC,CAAC;IAC3H,UAAU,CAAC,EAAE;QACX,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,qBAAqB,CAAC,EAAE,MAAM,CAAC;KAChC,CAAC;IACF,OAAO,CAAC,EAAE;QACR,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAC9B,wBAAwB,CAAC,EAAE,MAAM,CAAC;QAClC,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,sBAAsB,CAAC,EAAE,MAAM,CAAC;QAChC,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED,wBAAgB,0BAA0B,CACxC,OAAO,GAAE,0BAA+B,GACvC,SAAS,qBAAqB,EAAE,CA6BlC;AAED,eAAO,MAAM,sBAAsB,kCAA+B,CAAC"}
1
+ {"version":3,"file":"casualtyDiagnostics.d.ts","sourceRoot":"","sources":["../src/casualtyDiagnostics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AAErE,OAAO,KAAK,EAAE,wBAAwB,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AAEzG,OAAO,EAAE,0BAA0B,EAAE,CAAC;AAEtC,MAAM,WAAW,qBAAqB;IAAG,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;CAAE;AACzJ,MAAM,WAAW,qBAAqB;IAAG,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE;AAChH,MAAM,MAAM,oCAAoC,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAAC;AACzF,MAAM,WAAW,kCAAkC;IACjD,QAAQ,CAAC,MAAM,EAAE,qBAAqB,CAAC;IACvC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,cAAc,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC1D,QAAQ,CAAC,qBAAqB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,oCAAoC,CAAC,CAAC,CAAC;CACjG;AA+BD,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,kCAAkC,GAAG,SAAS,wBAAwB,EAAE,CAgD5H"}
@@ -1,122 +1,92 @@
1
- import { measureExpressionComponents, } from "./metricDiagnostics.js";
2
- import { ReservingError } from "./types.js";
3
- const m = (measure) => ({ op: "measure", measure });
1
+ import { CASUALTY_FORMULA_TEMPLATES } from "./diagnosticFormulas.js";
2
+ import { DiagnosticValidationError } from "./types.js";
3
+ export { CASUALTY_FORMULA_TEMPLATES };
4
+ const m = (measureId) => ({ op: "measure", measureId });
4
5
  const sub = (left, right) => ({ op: "subtract", left, right });
5
- function definedProperties(value) {
6
- if (value === undefined)
7
- return {};
8
- return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
6
+ function validToken(value) {
7
+ if (value.length === 0 || /^[\t-\r ]|[\t-\r ]$/.test(value) || value.includes("\0"))
8
+ return false;
9
+ for (let index = 0; index < value.length; index++) {
10
+ const code = value.charCodeAt(index);
11
+ if (code >= 0xd800 && code <= 0xdbff) {
12
+ const next = value.charCodeAt(++index);
13
+ if (!(next >= 0xdc00 && next <= 0xdfff))
14
+ return false;
15
+ }
16
+ else if (code >= 0xdc00 && code <= 0xdfff)
17
+ return false;
18
+ }
19
+ return true;
9
20
  }
10
- export const CASUALTY_DIAGNOSTIC_COMPONENTS = {
11
- reported: "reportedCount",
12
- open: "openCount",
13
- closedNoPay: "closedNoPayCount",
14
- closedWithPay: "closedWithPayCount",
15
- exposure: "exposure",
16
- paid250: "paid250",
17
- incurred250: "incurred250",
18
- paidPrimary: "paidPrimary",
19
- incurredPrimary: "incurredPrimary",
20
- };
21
- function metric(id, displayName, description, unit, scale, numerator, denominator, numeratorLabel, denominatorLabel, basis, version, override, warningRules) {
22
- const definition = {
23
- id,
24
- version,
25
- displayName,
26
- description,
27
- unit,
28
- scale,
29
- numerator,
30
- denominator,
31
- numeratorLabel,
32
- denominatorLabel,
33
- basis,
34
- requiredComponents: [...new Set([
35
- ...measureExpressionComponents(numerator),
36
- ...measureExpressionComponents(denominator),
37
- ])],
38
- warningRules,
39
- };
40
- return { ...definition, ...definedProperties(override) };
21
+ function encodeBindingId(value) { return encodeURIComponent(value).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`); }
22
+ function presentation(displayName, description, displayUnit, numeratorLabel, denominatorLabel) {
23
+ return { displayName, description, displayUnit, scale: 1, numeratorLabel, denominatorLabel };
41
24
  }
42
- const paidWarning = [{
43
- code: "PAID_EXCEEDS_INCURRED",
44
- when: "numerator-greater-than-denominator",
45
- message: "Paid exceeds incurred on the selected amount basis",
46
- tolerance: 1e-9,
47
- }];
48
- /** Builds the optional 20-metric reference preset from caller-selected keys and display metadata. */
49
- export function createCasualtyQuarterlyMetrics(options = {}) {
50
- const C = {
51
- ...CASUALTY_DIAGNOSTIC_COMPONENTS,
52
- ...definedProperties(options.components),
53
- };
54
- const frequencyScale = options.frequencyScale ?? 1_000_000;
55
- if (!Number.isFinite(frequencyScale) || frequencyScale <= 0) {
56
- throw new ReservingError("BAD_RATIO", `Casualty frequency scale must be positive; got ${frequencyScale}`);
57
- }
58
- const version = options.definitionVersion ?? "casualty-quarterly-v1";
59
- const frequencyUnit = options.frequencyUnit ?? (frequencyScale === 1_000_000
60
- ? "count-per-million"
61
- : "count-per-exposure-scale");
62
- const limitedBasis = options.basisLabels?.limited250 ?? "$250K pre-capped total";
63
- const primaryBasis = options.basisLabels?.primary ?? "$1M capped indemnity plus unlimited expense";
64
- const countBasis = options.basisLabels?.counts ?? "count";
65
- const override = (id) => options.displayOverrides?.[id];
66
- const nonClosedNoPay = sub(m(C.reported), m(C.closedNoPay));
67
- return [
68
- metric("reported-frequency", "Reported claim frequency", "Reported claims per scaled exposure units", frequencyUnit, frequencyScale, m(C.reported), m(C.exposure), "reported claims", "exposure", countBasis, version, override("reported-frequency")),
69
- metric("open-frequency", "Open claim frequency", "Open claims per scaled exposure units", frequencyUnit, frequencyScale, m(C.open), m(C.exposure), "open claims", "exposure", countBasis, version, override("open-frequency")),
70
- metric("closed-no-pay-frequency", "Closed-no-pay frequency", "Closed-no-pay claims per scaled exposure units", frequencyUnit, frequencyScale, m(C.closedNoPay), m(C.exposure), "closed-no-pay claims", "exposure", countBasis, version, override("closed-no-pay-frequency")),
71
- metric("closed-with-pay-frequency", "Closed-with-pay frequency", "Closed-with-pay claims per scaled exposure units", frequencyUnit, frequencyScale, m(C.closedWithPay), m(C.exposure), "closed-with-pay claims", "exposure", countBasis, version, override("closed-with-pay-frequency")),
72
- metric("non-closed-no-pay-frequency", "Non-closed-no-pay frequency", "Reported less closed-no-pay claims per scaled exposure units", frequencyUnit, frequencyScale, nonClosedNoPay, m(C.exposure), "reported less closed-no-pay claims", "exposure", countBasis, version, override("non-closed-no-pay-frequency")),
73
- metric("closed-no-pay-share", "Closed-no-pay share", "Closed-no-pay claims divided by reported claims", "ratio", 1, m(C.closedNoPay), m(C.reported), "closed-no-pay claims", "reported claims", countBasis, version, override("closed-no-pay-share")),
74
- metric("closed-with-pay-share", "Closed-with-pay share", "Closed-with-pay claims divided by reported claims", "ratio", 1, m(C.closedWithPay), m(C.reported), "closed-with-pay claims", "reported claims", countBasis, version, override("closed-with-pay-share")),
75
- metric("open-share", "Open share", "Open claims divided by reported claims", "ratio", 1, m(C.open), m(C.reported), "open claims", "reported claims", countBasis, version, override("open-share")),
76
- metric("paid-to-incurred-250", "Paid-to-incurred ($250K)", "Paid divided by incurred on the pre-capped $250K basis", "ratio", 1, m(C.paid250), m(C.incurred250), "$250K paid", "$250K incurred", limitedBasis, version, override("paid-to-incurred-250"), paidWarning),
77
- metric("paid-to-incurred-primary", "Paid-to-incurred (primary)", "Paid divided by incurred on the primary basis", "ratio", 1, m(C.paidPrimary), m(C.incurredPrimary), "primary paid", "primary incurred", primaryBasis, version, override("paid-to-incurred-primary"), paidWarning),
78
- metric("incurred-250-per-exposure", "Incurred per exposure ($250K)", "$250K incurred divided by exposure", "currency-per-exposure", 1, m(C.incurred250), m(C.exposure), "$250K incurred", "exposure", limitedBasis, version, override("incurred-250-per-exposure")),
79
- metric("incurred-primary-per-exposure", "Incurred per exposure (primary)", "Primary incurred divided by exposure", "currency-per-exposure", 1, m(C.incurredPrimary), m(C.exposure), "primary incurred", "exposure", primaryBasis, version, override("incurred-primary-per-exposure")),
80
- metric("incurred-250-per-non-cnp", "Incurred severity ($250K)", "$250K incurred divided by reported less closed-no-pay claims", "currency-per-claim", 1, m(C.incurred250), nonClosedNoPay, "$250K incurred", "reported less closed-no-pay claims", limitedBasis, version, override("incurred-250-per-non-cnp")),
81
- metric("incurred-primary-per-non-cnp", "Incurred severity (primary)", "Primary incurred divided by reported less closed-no-pay claims", "currency-per-claim", 1, m(C.incurredPrimary), nonClosedNoPay, "primary incurred", "reported less closed-no-pay claims", primaryBasis, version, override("incurred-primary-per-non-cnp")),
82
- metric("paid-250-per-exposure", "Paid per exposure ($250K)", "$250K paid divided by exposure", "currency-per-exposure", 1, m(C.paid250), m(C.exposure), "$250K paid", "exposure", limitedBasis, version, override("paid-250-per-exposure")),
83
- metric("paid-primary-per-exposure", "Paid per exposure (primary)", "Primary paid divided by exposure", "currency-per-exposure", 1, m(C.paidPrimary), m(C.exposure), "primary paid", "exposure", primaryBasis, version, override("paid-primary-per-exposure")),
84
- metric("paid-250-per-closed-with-pay", "Paid severity ($250K)", "$250K paid divided by closed-with-pay claims", "currency-per-claim", 1, m(C.paid250), m(C.closedWithPay), "$250K paid", "closed-with-pay claims", limitedBasis, version, override("paid-250-per-closed-with-pay")),
85
- metric("paid-primary-per-closed-with-pay", "Paid severity (primary)", "Primary paid divided by closed-with-pay claims", "currency-per-claim", 1, m(C.paidPrimary), m(C.closedWithPay), "primary paid", "closed-with-pay claims", primaryBasis, version, override("paid-primary-per-closed-with-pay")),
86
- metric("case-250-per-open", "Case reserve per open claim ($250K)", "$250K incurred less paid divided by open claims", "currency-per-claim", 1, sub(m(C.incurred250), m(C.paid250)), m(C.open), "$250K incurred less paid", "open claims", limitedBasis, version, override("case-250-per-open")),
87
- metric("case-primary-per-open", "Case reserve per open claim (primary)", "Primary incurred less paid divided by open claims", "currency-per-claim", 1, sub(m(C.incurredPrimary), m(C.paidPrimary)), m(C.open), "primary incurred less paid", "open claims", primaryBasis, version, override("case-primary-per-open")),
88
- ];
25
+ function overridePresentation(id, base, overrides) {
26
+ const override = overrides?.[id];
27
+ if (!override)
28
+ return base;
29
+ for (const [key, value] of Object.entries(override))
30
+ if (value === undefined)
31
+ throw new DiagnosticValidationError([{ domain: "configuration", code: "invalid-type", path: `$.presentationOverrides[${JSON.stringify(id)}].${key}`, message: "Presentation override cannot contain undefined" }]);
32
+ const result = { ...base, ...override };
33
+ if (!Number.isFinite(result.scale) || result.scale <= 0)
34
+ throw new DiagnosticValidationError([{ domain: "configuration", code: "invalid-number", path: `$.presentationOverrides[${JSON.stringify(id)}].scale`, message: "Presentation scale must be finite and positive" }]);
35
+ return result;
89
36
  }
90
- export const CASUALTY_QUARTERLY_METRICS = createCasualtyQuarterlyMetrics();
91
- export function createCasualtyAmountLayers(options = {}) {
92
- const C = {
93
- ...CASUALTY_DIAGNOSTIC_COMPONENTS,
94
- ...definedProperties(options.components),
95
- };
96
- const limit = options.primary?.indemnityLimit ?? 1_000_000;
97
- if (!Number.isFinite(limit) || limit <= 0) {
98
- throw new ReservingError("BAD_CAP", `Primary indemnity limit must be positive; got ${limit}`);
37
+ export function createCasualtyMetricInstances(input) {
38
+ for (const [key, value] of Object.entries({ ...input.counts, exposure: input.exposure }))
39
+ if (!validToken(value))
40
+ throw new DiagnosticValidationError([{ domain: "configuration", code: "invalid-string", path: `$.${key}`, message: "Measure binding must be a token" }]);
41
+ const ids = new Set();
42
+ for (const [index, binding] of input.amountBindings.entries()) {
43
+ if (!validToken(binding.id) || !validToken(binding.paid) || !validToken(binding.incurred))
44
+ throw new DiagnosticValidationError([{ domain: "configuration", code: "invalid-string", path: `$.amountBindings[${index}]`, message: "Amount binding fields must be tokens" }]);
45
+ if (ids.has(binding.id))
46
+ throw new DiagnosticValidationError([{ domain: "configuration", code: "duplicate-id", path: `$.amountBindings[${index}].id`, message: `Duplicate amount binding ${binding.id}` }]);
47
+ ids.add(binding.id);
99
48
  }
100
- return [
101
- {
102
- id: options.limited250?.id ?? "250k-pre-capped-total",
103
- displayName: options.limited250?.displayName ?? "$250K pre-capped total",
104
- paidMeasure: C.paid250,
105
- incurredMeasure: C.incurred250,
106
- paid: { op: "measure", measure: options.limited250?.paidSourceMeasure ?? "preCapped250Paid" },
107
- incurred: { op: "measure", measure: options.limited250?.incurredSourceMeasure ?? "preCapped250Incurred" },
108
- basis: "pre-capped-additive",
109
- },
110
- {
111
- id: options.primary?.id ?? "primary-1m-indemnity-plus-expense",
112
- displayName: options.primary?.displayName ?? "Primary: $1M capped indemnity plus unlimited expense",
113
- paidMeasure: C.paidPrimary,
114
- incurredMeasure: C.incurredPrimary,
115
- paid: { op: "add", terms: [{ op: "claim-cap", measure: options.primary?.indemnityPaidMeasure ?? "indemnityPaid", limit }, { op: "measure", measure: options.primary?.expensePaidMeasure ?? "expensePaid" }] },
116
- incurred: { op: "add", terms: [{ op: "claim-cap", measure: options.primary?.indemnityIncurredMeasure ?? "indemnityIncurred", limit }, { op: "measure", measure: options.primary?.expenseIncurredMeasure ?? "expenseIncurred" }] },
117
- basis: "claim-level-cap",
118
- },
49
+ const nonClosedNoPay = sub(m(input.counts.reported), m(input.counts.closedNoPay));
50
+ const countSpecs = [
51
+ ["reported-frequency", "frequency", m(input.counts.reported), m(input.exposure), presentation("Reported frequency", "Reported count divided by exposure", "count per exposure", "reported", "exposure")],
52
+ ["open-frequency", "frequency", m(input.counts.open), m(input.exposure), presentation("Open frequency", "Open count divided by exposure", "count per exposure", "open", "exposure")],
53
+ ["closed-no-pay-frequency", "frequency", m(input.counts.closedNoPay), m(input.exposure), presentation("Closed-no-pay frequency", "Closed-no-pay count divided by exposure", "count per exposure", "closed-no-pay", "exposure")],
54
+ ["closed-with-pay-frequency", "frequency", m(input.counts.closedWithPay), m(input.exposure), presentation("Closed-with-pay frequency", "Closed-with-pay count divided by exposure", "count per exposure", "closed-with-pay", "exposure")],
55
+ ["non-closed-no-pay-frequency", "frequency", nonClosedNoPay, m(input.exposure), presentation("Non-closed-no-pay frequency", "Reported less closed-no-pay count divided by exposure", "count per exposure", "reported less closed-no-pay", "exposure")],
56
+ ["closed-no-pay-share", "share", m(input.counts.closedNoPay), m(input.counts.reported), presentation("Closed-no-pay share", "Closed-no-pay count divided by reported count", "ratio", "closed-no-pay", "reported")],
57
+ ["closed-with-pay-share", "share", m(input.counts.closedWithPay), m(input.counts.reported), presentation("Closed-with-pay share", "Closed-with-pay count divided by reported count", "ratio", "closed-with-pay", "reported")],
58
+ ["closed-with-pay-share-of-non-closed-no-pay", "share", m(input.counts.closedWithPay), nonClosedNoPay, presentation("Closed-with-pay share of non-closed-no-pay", "Closed-with-pay count divided by reported less closed-no-pay count", "ratio", "closed-with-pay", "reported less closed-no-pay")],
59
+ ["open-share", "share", m(input.counts.open), m(input.counts.reported), presentation("Open share", "Open count divided by reported count", "ratio", "open", "reported")],
60
+ ["open-share-of-non-closed-no-pay", "share", m(input.counts.open), nonClosedNoPay, presentation("Open share of non-closed-no-pay", "Open count divided by reported less closed-no-pay count", "ratio", "open", "reported less closed-no-pay")],
119
61
  ];
62
+ const specs = [];
63
+ for (const [suffix, formulaId, numerator, denominator, display] of countSpecs) {
64
+ const id = `casualty/count/${suffix}`;
65
+ const bindings = formulaId === "frequency"
66
+ ? { claims: numerator, exposure: denominator }
67
+ : { part: numerator, whole: denominator };
68
+ specs.push({ id, formulaId, bindings, presentation: display, rules: [] });
69
+ }
70
+ for (const binding of input.amountBindings) {
71
+ const prefix = `casualty/amount/${encodeBindingId(binding.id)}`;
72
+ const amountSpecs = [
73
+ ["paid-to-incurred", "paid-to-incurred", { paid: m(binding.paid), incurred: m(binding.incurred) }, presentation("Paid to incurred", "Paid divided by incurred on the bound amount basis", "ratio", "paid", "incurred")],
74
+ ["incurred-per-exposure", "amount-per-exposure", { amount: m(binding.incurred), exposure: m(input.exposure) }, presentation("Incurred per exposure", "Incurred divided by exposure", "amount per exposure", "incurred", "exposure")],
75
+ ["incurred-per-non-closed-no-pay-claim", "amount-per-claim", { amount: m(binding.incurred), claims: nonClosedNoPay }, presentation("Incurred per non-closed-no-pay claim", "Incurred divided by reported less closed-no-pay count", "amount per claim", "incurred", "reported less closed-no-pay")],
76
+ ["paid-per-exposure", "amount-per-exposure", { amount: m(binding.paid), exposure: m(input.exposure) }, presentation("Paid per exposure", "Paid divided by exposure", "amount per exposure", "paid", "exposure")],
77
+ ["paid-per-closed-with-pay-claim", "amount-per-claim", { amount: m(binding.paid), claims: m(input.counts.closedWithPay) }, presentation("Paid per closed-with-pay claim", "Paid divided by closed-with-pay count", "amount per claim", "paid", "closed-with-pay")],
78
+ ["case-per-open-claim", "case-per-open", { incurred: m(binding.incurred), paid: m(binding.paid), open: m(input.counts.open) }, presentation("Case reserve per open claim", "Incurred less paid divided by open count", "amount per claim", "incurred less paid", "open")],
79
+ ];
80
+ for (const [suffix, formulaId, bindings, display] of amountSpecs) {
81
+ const id = `${prefix}/${suffix}`;
82
+ const rules = suffix === "paid-to-incurred" ? [{ id: `${id}/rule/paid-exceeds-incurred`, code: "paid-exceeds-incurred", message: "Paid exceeds incurred on the bound amount basis", severity: "warning", when: { left: { source: "calculation", field: "numerator" }, operator: "gt", right: { source: "calculation", field: "denominator" }, tolerance: { absolute: 0, relative: 0 } } }] : suffix === "case-per-open-claim" ? [{ id: `${id}/rule/negative-case`, code: "negative-case", message: "Incurred less paid is negative on the bound amount basis", severity: "warning", when: { left: { source: "calculation", field: "numerator" }, operator: "lt", right: { source: "constant", value: 0 }, tolerance: { absolute: 0, relative: 0 } } }] : [];
83
+ specs.push({ id, formulaId, bindings: { ...bindings }, presentation: display, rules });
84
+ }
85
+ }
86
+ const validIds = new Set(specs.map((spec) => spec.id));
87
+ for (const key of Object.keys(input.presentationOverrides ?? {}))
88
+ if (!validIds.has(key))
89
+ throw new DiagnosticValidationError([{ domain: "configuration", code: "unknown-reference", path: `$.presentationOverrides[${JSON.stringify(key)}]`, message: "Presentation override names an unknown generated instance" }]);
90
+ return Object.freeze(specs.map((spec) => Object.freeze({ id: spec.id, version: "1.0.0", formulaId: spec.formulaId, bindings: spec.bindings, presentation: overridePresentation(spec.id, spec.presentation, input.presentationOverrides), rules: spec.rules })));
120
91
  }
121
- export const CASUALTY_AMOUNT_LAYERS = createCasualtyAmountLayers();
122
92
  //# sourceMappingURL=casualtyDiagnostics.js.map