import type { CasualtyCountBindings, DiagnosticControlTotalProjection, DiagnosticMeasureExpression, DiagnosticReviewFilter, DiagnosticReviewRule } from "@actuarial-ts/core"; export interface CasualtyMonotonicReviewBinding { readonly id:string;readonly expression:DiagnosticMeasureExpression;readonly direction:"nondecreasing"|"nonincreasing";readonly tolerance?:{readonly absolute?:number;readonly relative?:number} } export interface CasualtyLayerOrderReviewBinding { readonly id:string;readonly narrower:DiagnosticMeasureExpression;readonly broader:DiagnosticMeasureExpression;readonly comparability:{readonly kind:"compiler-proven"}|{readonly kind:"caller-asserted";readonly rationaleArtifactId:string};readonly tolerance?:{readonly absolute?:number;readonly relative?:number} } export interface CasualtyControlTotalReviewBinding { readonly id:string;readonly expression:DiagnosticMeasureExpression;readonly expected:number;readonly filter?:DiagnosticReviewFilter;readonly projection:DiagnosticControlTotalProjection;readonly tolerance?:{readonly absolute?:number;readonly relative?:number} } export interface CreateCasualtyDiagnosticReviewRulesInput { readonly counts:CasualtyCountBindings;readonly exposure:string;readonly monotonicMeasures:readonly CasualtyMonotonicReviewBinding[];readonly layerOrders:readonly CasualtyLayerOrderReviewBinding[];readonly controlTotals:readonly CasualtyControlTotalReviewBinding[];readonly severityOverrides?:Readonly> } const m=(measureId:string)=>({op:"measure" as const,measureId});const tolerance=(value:{absolute?:number;relative?:number}|undefined)=>({absolute:value?.absolute??0,relative:value?.relative??0}); export function createCasualtyDiagnosticReviewRules(input:CreateCasualtyDiagnosticReviewRulesInput):readonly DiagnosticReviewRule[]{ const fixed:DiagnosticReviewRule[]=[ {kind:"reconcile",id:"casualty/review/count-reconciliation",code:"count-reconciliation",description:"Reported count must equal open plus closed-no-pay plus closed-with-pay count",severity:"fail",missingInput:"not-evaluated",tolerance:{absolute:0,relative:0},actual:m(input.counts.reported),expected:{op:"add",terms:[m(input.counts.open),m(input.counts.closedNoPay),m(input.counts.closedWithPay)]}}, {kind:"compare",id:"casualty/review/closed-no-pay-bound",code:"closed-no-pay-bound",description:"Closed-no-pay count must not exceed reported count",severity:"fail",missingInput:"not-evaluated",tolerance:{absolute:0,relative:0},when:{left:m(input.counts.closedNoPay),operator:"gt",right:m(input.counts.reported)}}, {kind:"compare",id:"casualty/review/positive-exposure",code:"positive-exposure",description:"Exposure must be strictly positive",severity:"fail",missingInput:"not-evaluated",tolerance:{absolute:0,relative:0},when:{left:m(input.exposure),operator:"lte",right:{op:"constant",value:0}}}, {kind:"monotonic",id:"casualty/review/closed-reopen-signal",code:"closed-reopen-signal",description:"Closed count must be nondecreasing across valuations",severity:"warning",missingInput:"not-evaluated",tolerance:{absolute:0,relative:0},expression:{op:"add",terms:[m(input.counts.closedNoPay),m(input.counts.closedWithPay)]},direction:"nondecreasing"}, ]; const dynamic:DiagnosticReviewRule[]=[...input.monotonicMeasures.map((item):DiagnosticReviewRule=>({kind:"monotonic",id:item.id,code:item.id,description:`Expression must be ${item.direction} across valuations`,severity:"warning",missingInput:"not-evaluated",tolerance:tolerance(item.tolerance),expression:item.expression,direction:item.direction})),...input.layerOrders.map((item):DiagnosticReviewRule=>({kind:"layer-order",id:item.id,code:item.id,description:"Narrower amount expression must not exceed broader amount expression",severity:"fail",missingInput:"not-evaluated",tolerance:tolerance(item.tolerance),narrower:item.narrower,broader:item.broader,comparability:item.comparability})),...input.controlTotals.map((item):DiagnosticReviewRule=>({kind:"control-total",id:item.id,code:item.id,description:"Expression must reconcile to the declared control total",severity:"fail",missingInput:"not-evaluated",tolerance:tolerance(item.tolerance),expression:item.expression,expected:item.expected,...(item.filter?{filter:item.filter}:{}),projection:item.projection}))]; const all=[...fixed,...dynamic];const ids=new Set();for(const rule of all){if(ids.has(rule.id))throw new Error(`Duplicate review rule ID ${rule.id}`);ids.add(rule.id)}for(const key of Object.keys(input.severityOverrides??{}))if(!ids.has(key))throw new Error(`Unknown severity override ${key}`);return Object.freeze(all.map((rule)=>Object.freeze({...rule,severity:input.severityOverrides?.[rule.id]??rule.severity}) as DiagnosticReviewRule)); }