@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.
- package/README.md +65 -215
- package/dist/casualtyDiagnostics.d.ts +19 -49
- package/dist/casualtyDiagnostics.d.ts.map +1 -1
- package/dist/casualtyDiagnostics.js +84 -114
- package/dist/casualtyDiagnostics.js.map +1 -1
- package/dist/diagnosticAggregation.d.ts +29 -0
- package/dist/diagnosticAggregation.d.ts.map +1 -0
- package/dist/diagnosticAggregation.js +50 -0
- package/dist/diagnosticAggregation.js.map +1 -0
- package/dist/diagnosticDefinitions.d.ts +286 -0
- package/dist/diagnosticDefinitions.d.ts.map +1 -0
- package/dist/diagnosticDefinitions.js +1026 -0
- package/dist/diagnosticDefinitions.js.map +1 -0
- package/dist/diagnosticDerivations.d.ts +12 -0
- package/dist/diagnosticDerivations.d.ts.map +1 -0
- package/dist/diagnosticDerivations.js +80 -0
- package/dist/diagnosticDerivations.js.map +1 -0
- package/dist/diagnosticExposure.d.ts +51 -0
- package/dist/diagnosticExposure.d.ts.map +1 -0
- package/dist/diagnosticExposure.js +65 -0
- package/dist/diagnosticExposure.js.map +1 -0
- package/dist/diagnosticExpressions.d.ts +56 -0
- package/dist/diagnosticExpressions.d.ts.map +1 -0
- package/dist/diagnosticExpressions.js +104 -0
- package/dist/diagnosticExpressions.js.map +1 -0
- package/dist/diagnosticFormulas.d.ts +195 -0
- package/dist/diagnosticFormulas.d.ts.map +1 -0
- package/dist/diagnosticFormulas.js +85 -0
- package/dist/diagnosticFormulas.js.map +1 -0
- package/dist/diagnosticIdentity.d.ts +237 -0
- package/dist/diagnosticIdentity.d.ts.map +1 -0
- package/dist/diagnosticIdentity.js +454 -0
- package/dist/diagnosticIdentity.js.map +1 -0
- package/dist/diagnosticPeriods.d.ts +16 -0
- package/dist/diagnosticPeriods.d.ts.map +1 -0
- package/dist/diagnosticPeriods.js +98 -0
- package/dist/diagnosticPeriods.js.map +1 -0
- package/dist/diagnosticPreparation.d.ts +98 -0
- package/dist/diagnosticPreparation.d.ts.map +1 -0
- package/dist/diagnosticPreparation.js +169 -0
- package/dist/diagnosticPreparation.js.map +1 -0
- package/dist/diagnosticReview.d.ts +44 -0
- package/dist/diagnosticReview.d.ts.map +1 -0
- package/dist/diagnosticReview.js +84 -0
- package/dist/diagnosticReview.js.map +1 -0
- package/dist/diagnosticRules.d.ts +32 -0
- package/dist/diagnosticRules.d.ts.map +1 -0
- package/dist/diagnosticRules.js +32 -0
- package/dist/diagnosticRules.js.map +1 -0
- package/dist/diagnosticRunner.d.ts +85 -0
- package/dist/diagnosticRunner.d.ts.map +1 -0
- package/dist/diagnosticRunner.js +117 -0
- package/dist/diagnosticRunner.js.map +1 -0
- package/dist/index.d.ts +24 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +114 -0
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +2 -2
- package/src/casualtyDiagnostics.ts +79 -187
- package/src/diagnosticAggregation.ts +79 -0
- package/src/diagnosticDefinitions.ts +1375 -0
- package/src/diagnosticDerivations.ts +89 -0
- package/src/diagnosticExposure.ts +91 -0
- package/src/diagnosticExpressions.ts +173 -0
- package/src/diagnosticFormulas.ts +150 -0
- package/src/diagnosticIdentity.ts +767 -0
- package/src/diagnosticPeriods.ts +117 -0
- package/src/diagnosticPreparation.ts +233 -0
- package/src/diagnosticReview.ts +91 -0
- package/src/diagnosticRules.ts +69 -0
- package/src/diagnosticRunner.ts +110 -0
- package/src/index.ts +156 -1
- package/src/types.ts +161 -0
- package/src/version.ts +2 -0
- package/dist/metricDiagnostics.d.ts +0 -212
- package/dist/metricDiagnostics.d.ts.map +0 -1
- package/dist/metricDiagnostics.js +0 -627
- package/dist/metricDiagnostics.js.map +0 -1
- package/src/metricDiagnostics.ts +0 -876
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { CompiledDiagnosticDefinition, DiagnosticPeriodAxis } from "./diagnosticDefinitions.js";
|
|
2
|
+
import { assertCompiledDiagnosticDefinition } from "./diagnosticDefinitions.js";
|
|
3
|
+
import { DiagnosticValidationError } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export interface DiagnosticNormalizedPeriod {
|
|
6
|
+
readonly label: string;
|
|
7
|
+
readonly coordinate: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type PeriodSide = "origin" | "valuation";
|
|
11
|
+
|
|
12
|
+
function calendarCoordinate(
|
|
13
|
+
label: string,
|
|
14
|
+
cadence: "month" | "quarter" | "year",
|
|
15
|
+
anchor: "start" | "end",
|
|
16
|
+
): number | null {
|
|
17
|
+
let year: number;
|
|
18
|
+
let start: number;
|
|
19
|
+
let width: number;
|
|
20
|
+
if (cadence === "year") {
|
|
21
|
+
const match = /^(\d{4})$/.exec(label);
|
|
22
|
+
if (!match) return null;
|
|
23
|
+
year = Number(match[1]);
|
|
24
|
+
start = year * 12;
|
|
25
|
+
width = 12;
|
|
26
|
+
} else if (cadence === "month") {
|
|
27
|
+
const match = /^(\d{4})-(0[1-9]|1[0-2])$/.exec(label);
|
|
28
|
+
if (!match) return null;
|
|
29
|
+
year = Number(match[1]);
|
|
30
|
+
start = year * 12 + Number(match[2]) - 1;
|
|
31
|
+
width = 1;
|
|
32
|
+
} else {
|
|
33
|
+
const compact = /^(\d{4})Q([1-4])$/.exec(label);
|
|
34
|
+
const hyphenated = /^(\d{4})-Q([1-4])$/.exec(label);
|
|
35
|
+
const first = /^Q([1-4]) (\d{4})$/.exec(label);
|
|
36
|
+
const match = compact ?? hyphenated ?? first;
|
|
37
|
+
if (!match) return null;
|
|
38
|
+
const quarterFirst = match === first;
|
|
39
|
+
year = Number(match[quarterFirst ? 2 : 1]);
|
|
40
|
+
const quarter = Number(match[quarterFirst ? 1 : 2]);
|
|
41
|
+
start = year * 12 + (quarter - 1) * 3;
|
|
42
|
+
width = 3;
|
|
43
|
+
}
|
|
44
|
+
if (year < 1 || year > 9999) return null;
|
|
45
|
+
return start + (anchor === "end" ? width : 0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeWithAxis(
|
|
49
|
+
axis: DiagnosticPeriodAxis,
|
|
50
|
+
side: PeriodSide,
|
|
51
|
+
label: string,
|
|
52
|
+
): DiagnosticNormalizedPeriod {
|
|
53
|
+
if (axis.kind === "calendar") {
|
|
54
|
+
const cadence = side === "origin" ? axis.originCadence : axis.valuationCadence;
|
|
55
|
+
const anchor = side === "origin" ? axis.originAnchor : axis.valuationAnchor;
|
|
56
|
+
const coordinate = calendarCoordinate(label, cadence, anchor);
|
|
57
|
+
if (coordinate === null) throw new DiagnosticValidationError([{
|
|
58
|
+
domain: "input",
|
|
59
|
+
code: "invalid-period",
|
|
60
|
+
path: `$.${side}`,
|
|
61
|
+
message: `Invalid ${cadence} ${side} label ${JSON.stringify(label)}`,
|
|
62
|
+
}]);
|
|
63
|
+
const canonicalLabel = cadence === "quarter"
|
|
64
|
+
? (() => {
|
|
65
|
+
const startCoordinate = calendarCoordinate(label, cadence, "start")!;
|
|
66
|
+
const year = Math.floor(startCoordinate / 12);
|
|
67
|
+
return `${String(year).padStart(4, "0")}Q${Math.floor((startCoordinate % 12) / 3) + 1}`;
|
|
68
|
+
})()
|
|
69
|
+
: label;
|
|
70
|
+
return Object.freeze({ label: canonicalLabel, coordinate });
|
|
71
|
+
}
|
|
72
|
+
const catalog = side === "origin" ? axis.origins : axis.valuations;
|
|
73
|
+
const item = catalog.find((coordinate) => coordinate.label === label || coordinate.aliases?.includes(label));
|
|
74
|
+
if (!item) throw new DiagnosticValidationError([{
|
|
75
|
+
domain: "input",
|
|
76
|
+
code: "invalid-period",
|
|
77
|
+
path: `$.${side}`,
|
|
78
|
+
message: `Unknown ordered-axis ${side} label ${JSON.stringify(label)}`,
|
|
79
|
+
}]);
|
|
80
|
+
return Object.freeze({ label: item.label, coordinate: item.coordinate });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function normalizeDiagnosticPeriod(
|
|
84
|
+
definition: CompiledDiagnosticDefinition,
|
|
85
|
+
side: PeriodSide,
|
|
86
|
+
label: string,
|
|
87
|
+
): DiagnosticNormalizedPeriod {
|
|
88
|
+
assertCompiledDiagnosticDefinition(definition);
|
|
89
|
+
return normalizeWithAxis(definition.definition.periodAxis as DiagnosticPeriodAxis, side, label);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function diagnosticDevelopmentAge(
|
|
93
|
+
definition: CompiledDiagnosticDefinition,
|
|
94
|
+
origin: string,
|
|
95
|
+
valuation: string,
|
|
96
|
+
): { readonly origin: DiagnosticNormalizedPeriod; readonly valuation: DiagnosticNormalizedPeriod; readonly developmentAge: number; readonly ageUnit: string } {
|
|
97
|
+
const normalizedOrigin = normalizeDiagnosticPeriod(definition, "origin", origin);
|
|
98
|
+
const normalizedValuation = normalizeDiagnosticPeriod(definition, "valuation", valuation);
|
|
99
|
+
const axis = definition.definition.periodAxis;
|
|
100
|
+
const developmentAge = normalizedValuation.coordinate - normalizedOrigin.coordinate + axis.ageOffset;
|
|
101
|
+
if (!Number.isSafeInteger(developmentAge) || developmentAge < 0) throw new DiagnosticValidationError([{
|
|
102
|
+
domain: "input",
|
|
103
|
+
code: "invalid-period",
|
|
104
|
+
path: "$.valuation",
|
|
105
|
+
message: "Valuation period precedes origin period or development age is unsafe",
|
|
106
|
+
}]);
|
|
107
|
+
return Object.freeze({
|
|
108
|
+
origin: normalizedOrigin,
|
|
109
|
+
valuation: normalizedValuation,
|
|
110
|
+
developmentAge,
|
|
111
|
+
ageUnit: axis.ageUnit,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function compareDiagnosticPeriods(left: DiagnosticNormalizedPeriod, right: DiagnosticNormalizedPeriod): number {
|
|
116
|
+
return left.coordinate - right.coordinate || (left.label < right.label ? -1 : left.label > right.label ? 1 : 0);
|
|
117
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { canonicalJson, fnv1a64 } from "./canonical.js";
|
|
2
|
+
import {
|
|
3
|
+
assertCompiledDiagnosticDefinition,
|
|
4
|
+
getCompiledDiagnosticDefinitionInternals,
|
|
5
|
+
type CompiledDiagnosticDefinition,
|
|
6
|
+
type DiagnosticDeepReadonly,
|
|
7
|
+
type DiagnosticSourceLocation,
|
|
8
|
+
type DiagnosticsFilter,
|
|
9
|
+
type JsonValue,
|
|
10
|
+
} from "./diagnosticDefinitions.js";
|
|
11
|
+
import { auditedDiagnosticContribution, finalizeDiagnosticContributions, type DiagnosticMeasureContribution, type DiagnosticStructuralBlocker } from "./diagnosticAggregation.js";
|
|
12
|
+
import { deriveDiagnosticClaimMeasures } from "./diagnosticDerivations.js";
|
|
13
|
+
import { diagnosticDevelopmentAge, normalizeDiagnosticPeriod } from "./diagnosticPeriods.js";
|
|
14
|
+
import { reconcileDiagnosticExposures, type DiagnosticAuditedNumericValue, type DiagnosticExposureObservation, type ReconciledDiagnosticExposure, auditDiagnosticNumber } from "./diagnosticExposure.js";
|
|
15
|
+
import type { DiagnosticMeasureStats } from "./diagnosticDefinitions.js";
|
|
16
|
+
import type { DiagnosticMetricFinding } from "./diagnosticFormulas.js";
|
|
17
|
+
import { DiagnosticValidationError, type DiagnosticValidationIssue } from "./types.js";
|
|
18
|
+
|
|
19
|
+
export interface DiagnosticLossRecordBase {
|
|
20
|
+
readonly recordId: string;
|
|
21
|
+
readonly sourceGroup: string;
|
|
22
|
+
readonly origin: string;
|
|
23
|
+
readonly valuation: string;
|
|
24
|
+
readonly complete: boolean;
|
|
25
|
+
readonly source?: DiagnosticSourceLocation;
|
|
26
|
+
readonly measures: Readonly<Record<string, number | null>>;
|
|
27
|
+
}
|
|
28
|
+
export interface DiagnosticClaimObservation extends DiagnosticLossRecordBase { readonly rowType: "claim"; readonly claimId: string }
|
|
29
|
+
export interface DiagnosticLossSnapshot extends DiagnosticLossRecordBase { readonly rowType: "aggregate" }
|
|
30
|
+
export type DiagnosticLossInput = DiagnosticClaimObservation | DiagnosticLossSnapshot;
|
|
31
|
+
|
|
32
|
+
export interface DiagnosticCompletePeriodCutoff { readonly sourceGroup: string; readonly originThrough: string | null; readonly valuationThrough: string | null }
|
|
33
|
+
export interface DiagnosticExpectedCell { readonly sourceGroup: string; readonly origin: string; readonly valuation: string; readonly source?: DiagnosticSourceLocation }
|
|
34
|
+
export interface PrepareDiagnosticDataInput {
|
|
35
|
+
readonly definition: CompiledDiagnosticDefinition;
|
|
36
|
+
readonly losses: readonly DiagnosticLossInput[];
|
|
37
|
+
readonly exposures: readonly DiagnosticExposureObservation[];
|
|
38
|
+
readonly filter?: DiagnosticsFilter;
|
|
39
|
+
readonly completePeriodCutoffs?: readonly DiagnosticCompletePeriodCutoff[];
|
|
40
|
+
readonly expectedCells?: readonly DiagnosticExpectedCell[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type DiagnosticInputDisposition = "invalid" | "complete-period-cutoff" | "filter" | "retained";
|
|
44
|
+
export type DiagnosticInputAuditRecord =
|
|
45
|
+
| { readonly kind: "loss"; readonly disposition: DiagnosticInputDisposition; readonly record: DiagnosticDeepReadonly<DiagnosticLossInput> }
|
|
46
|
+
| { readonly kind: "exposure"; readonly disposition: DiagnosticInputDisposition; readonly record: DiagnosticDeepReadonly<DiagnosticExposureObservation> }
|
|
47
|
+
| { readonly kind: "expected-cell"; readonly disposition: Exclude<DiagnosticInputDisposition, "invalid">; readonly record: DiagnosticDeepReadonly<DiagnosticExpectedCell> };
|
|
48
|
+
|
|
49
|
+
export interface PreparedDiagnosticSourceCell {
|
|
50
|
+
readonly sourceGroup: string;
|
|
51
|
+
readonly origin: string;
|
|
52
|
+
readonly valuation: string;
|
|
53
|
+
readonly developmentAge: number;
|
|
54
|
+
readonly ageUnit: string;
|
|
55
|
+
readonly lossRecordIds: readonly string[];
|
|
56
|
+
readonly contributions: Readonly<Record<string, readonly DiagnosticMeasureContribution[]>>;
|
|
57
|
+
readonly components: Readonly<Record<string, DiagnosticMeasureStats>>;
|
|
58
|
+
readonly structuralBlockers: Readonly<Record<string, readonly DiagnosticStructuralBlocker[]>>;
|
|
59
|
+
readonly findings: readonly DiagnosticMetricFinding[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
declare const preparedDiagnosticDataBrand: unique symbol;
|
|
63
|
+
export interface PreparedDiagnosticData {
|
|
64
|
+
readonly [preparedDiagnosticDataBrand]: true;
|
|
65
|
+
readonly definition: CompiledDiagnosticDefinition;
|
|
66
|
+
readonly preparationFingerprint: string;
|
|
67
|
+
readonly filter: DiagnosticDeepReadonly<DiagnosticsFilter> | null;
|
|
68
|
+
readonly completePeriodCutoffs: readonly DiagnosticCompletePeriodCutoff[];
|
|
69
|
+
readonly inputAudit: readonly DiagnosticInputAuditRecord[];
|
|
70
|
+
readonly cells: readonly PreparedDiagnosticSourceCell[];
|
|
71
|
+
readonly exposures: readonly ReconciledDiagnosticExposure[];
|
|
72
|
+
readonly expectedCellsProvided: boolean;
|
|
73
|
+
readonly expectedCells: readonly DiagnosticExpectedCell[];
|
|
74
|
+
readonly findings: readonly DiagnosticMetricFinding[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface NormalizedDiagnosticPreparationIdentity {
|
|
78
|
+
readonly definitionIntegrity: string;
|
|
79
|
+
readonly filter: DiagnosticDeepReadonly<DiagnosticsFilter> | null;
|
|
80
|
+
readonly completePeriodCutoffs: readonly DiagnosticCompletePeriodCutoff[];
|
|
81
|
+
readonly inputAudit: readonly DiagnosticInputAuditRecord[];
|
|
82
|
+
readonly cells: readonly PreparedDiagnosticSourceCell[];
|
|
83
|
+
readonly exposures: readonly ReconciledDiagnosticExposure[];
|
|
84
|
+
readonly expectedCellsProvided: boolean;
|
|
85
|
+
readonly expectedCells: readonly DiagnosticExpectedCell[];
|
|
86
|
+
readonly findings: readonly DiagnosticMetricFinding[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const authentic = new WeakSet<object>();
|
|
90
|
+
const identities = new WeakMap<object, DiagnosticDeepReadonly<NormalizedDiagnosticPreparationIdentity>>();
|
|
91
|
+
|
|
92
|
+
function deepFreeze<T>(value: T, seen = new WeakSet<object>()): DiagnosticDeepReadonly<T> {
|
|
93
|
+
if (value === null || typeof value !== "object" || seen.has(value)) return value as DiagnosticDeepReadonly<T>;
|
|
94
|
+
seen.add(value);
|
|
95
|
+
for (const child of Object.values(value as Record<string, unknown>)) deepFreeze(child, seen);
|
|
96
|
+
return Object.freeze(value) as DiagnosticDeepReadonly<T>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function codeUnit(left: string, right: string): number { return left < right ? -1 : left > right ? 1 : 0 }
|
|
100
|
+
function own<T>(record: Readonly<Record<string, T>>, key: string): T | undefined { return Object.prototype.hasOwnProperty.call(record, key) ? record[key] : undefined }
|
|
101
|
+
|
|
102
|
+
function normalizeFilter(filter: DiagnosticsFilter | undefined): DiagnosticsFilter | null {
|
|
103
|
+
if (filter === undefined) return null;
|
|
104
|
+
const set = (values: readonly string[] | undefined) => values === undefined ? undefined : [...new Set(values)].sort(codeUnit);
|
|
105
|
+
return {
|
|
106
|
+
...(filter.sourceGroups === undefined ? {} : { sourceGroups: set(filter.sourceGroups) }),
|
|
107
|
+
...(filter.outputGroups === undefined ? {} : { outputGroups: set(filter.outputGroups) }),
|
|
108
|
+
...(filter.origins === undefined ? {} : { origins: set(filter.origins) }),
|
|
109
|
+
...(filter.originFrom === undefined ? {} : { originFrom: filter.originFrom }),
|
|
110
|
+
...(filter.originThrough === undefined ? {} : { originThrough: filter.originThrough }),
|
|
111
|
+
...(filter.valuations === undefined ? {} : { valuations: set(filter.valuations) }),
|
|
112
|
+
...(filter.valuationFrom === undefined ? {} : { valuationFrom: filter.valuationFrom }),
|
|
113
|
+
...(filter.valuationThrough === undefined ? {} : { valuationThrough: filter.valuationThrough }),
|
|
114
|
+
...(filter.minDevelopmentAge === undefined ? {} : { minDevelopmentAge: filter.minDevelopmentAge }),
|
|
115
|
+
...(filter.maxDevelopmentAge === undefined ? {} : { maxDevelopmentAge: filter.maxDevelopmentAge }),
|
|
116
|
+
...(filter.instanceIds === undefined ? {} : { instanceIds: set(filter.instanceIds) }),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function selected(filter: DiagnosticsFilter | null, group: string, origin: string, valuation: string, age: number): boolean {
|
|
121
|
+
return (filter?.sourceGroups === undefined || filter.sourceGroups.includes(group)) &&
|
|
122
|
+
(filter?.origins === undefined || filter.origins.includes(origin)) &&
|
|
123
|
+
(filter?.valuations === undefined || filter.valuations.includes(valuation)) &&
|
|
124
|
+
(filter?.originFrom === undefined || origin >= filter.originFrom) &&
|
|
125
|
+
(filter?.originThrough === undefined || origin <= filter.originThrough) &&
|
|
126
|
+
(filter?.valuationFrom === undefined || valuation >= filter.valuationFrom) &&
|
|
127
|
+
(filter?.valuationThrough === undefined || valuation <= filter.valuationThrough) &&
|
|
128
|
+
(filter?.minDevelopmentAge === undefined || age >= filter.minDevelopmentAge) &&
|
|
129
|
+
(filter?.maxDevelopmentAge === undefined || age <= filter.maxDevelopmentAge);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function prepareDiagnosticData(input: PrepareDiagnosticDataInput): PreparedDiagnosticData {
|
|
133
|
+
assertCompiledDiagnosticDefinition(input.definition);
|
|
134
|
+
const definition = input.definition;
|
|
135
|
+
const internals = getCompiledDiagnosticDefinitionInternals(definition);
|
|
136
|
+
const issues: DiagnosticValidationIssue[] = [];
|
|
137
|
+
const filter = normalizeFilter(input.filter);
|
|
138
|
+
for (const bound of [filter?.minDevelopmentAge, filter?.maxDevelopmentAge]) if (bound !== undefined && (!Number.isSafeInteger(bound) || bound < 0)) issues.push({ domain: "configuration", code: "invalid-configuration", path: "$.filter", message: "Development-age bounds must be nonnegative safe integers" });
|
|
139
|
+
if (filter?.minDevelopmentAge !== undefined && filter.maxDevelopmentAge !== undefined && filter.minDevelopmentAge > filter.maxDevelopmentAge) issues.push({ domain: "configuration", code: "invalid-configuration", path: "$.filter", message: "Minimum development age exceeds maximum" });
|
|
140
|
+
input.losses.forEach((row, index) => {
|
|
141
|
+
if (row.rowType !== definition.definition.lossRowGrain) issues.push({ domain: "input", code: "invalid-input-relationship", path: `$.losses[${index}].rowType`, message: "Loss row type does not match definition grain" });
|
|
142
|
+
});
|
|
143
|
+
input.exposures.forEach((row, index) => {
|
|
144
|
+
const measure = internals.measuresById.get(row.measureId);
|
|
145
|
+
if (measure?.exposureTiming === "valuation-specific" && row.valuation === undefined) issues.push({ domain: "input", code: "missing-required", path: `$.exposures[${index}].valuation`, message: "Valuation-specific exposure requires valuation" });
|
|
146
|
+
});
|
|
147
|
+
if (issues.length > 0) throw new DiagnosticValidationError(issues);
|
|
148
|
+
|
|
149
|
+
const normalizedLosses: DiagnosticLossInput[] = [];
|
|
150
|
+
const audit: DiagnosticInputAuditRecord[] = [];
|
|
151
|
+
for (const row of input.losses) {
|
|
152
|
+
let disposition: DiagnosticInputDisposition = "retained";
|
|
153
|
+
try {
|
|
154
|
+
const period = diagnosticDevelopmentAge(definition, row.origin, row.valuation);
|
|
155
|
+
const normalized = { ...row, origin: period.origin.label, valuation: period.valuation.label } as DiagnosticLossInput;
|
|
156
|
+
if (!selected(filter, row.sourceGroup, normalized.origin, normalized.valuation, period.developmentAge)) disposition = "filter";
|
|
157
|
+
if (disposition === "retained") normalizedLosses.push(normalized);
|
|
158
|
+
audit.push({ kind: "loss", disposition, record: normalized });
|
|
159
|
+
} catch { audit.push({ kind: "loss", disposition: "invalid", record: row }); }
|
|
160
|
+
}
|
|
161
|
+
const derivedLosses = definition.definition.lossRowGrain === "claim"
|
|
162
|
+
? deriveDiagnosticClaimMeasures(normalizedLosses as DiagnosticClaimObservation[], definition) as readonly DiagnosticLossInput[]
|
|
163
|
+
: normalizedLosses;
|
|
164
|
+
const cells = new Map<string, DiagnosticLossInput[]>();
|
|
165
|
+
for (const row of derivedLosses) {
|
|
166
|
+
const key = `${row.sourceGroup}\u0000${row.origin}\u0000${row.valuation}`;
|
|
167
|
+
const list = cells.get(key) ?? [];
|
|
168
|
+
list.push(row);
|
|
169
|
+
cells.set(key, list);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const normalizedExposureInputs: DiagnosticExposureObservation[] = [];
|
|
173
|
+
for (const row of input.exposures) {
|
|
174
|
+
try {
|
|
175
|
+
const origin = normalizeDiagnosticPeriod(definition, "origin", row.origin).label;
|
|
176
|
+
const valuation = row.valuation === undefined ? undefined : normalizeDiagnosticPeriod(definition, "valuation", row.valuation).label;
|
|
177
|
+
const normalized = { ...row, origin, ...(valuation === undefined ? {} : { valuation }) };
|
|
178
|
+
const timing = internals.measuresById.get(row.measureId)?.exposureTiming;
|
|
179
|
+
const age = valuation === undefined ? 0 : diagnosticDevelopmentAge(definition, origin, valuation).developmentAge;
|
|
180
|
+
const keep = timing === "origin-static"
|
|
181
|
+
? (filter?.sourceGroups === undefined || filter.sourceGroups.includes(row.sourceGroup)) && (filter?.origins === undefined || filter.origins.includes(origin))
|
|
182
|
+
: selected(filter, row.sourceGroup, origin, valuation ?? "", age);
|
|
183
|
+
audit.push({ kind: "exposure", disposition: keep ? "retained" : "filter", record: normalized });
|
|
184
|
+
if (keep) normalizedExposureInputs.push(normalized);
|
|
185
|
+
} catch { audit.push({ kind: "exposure", disposition: "invalid", record: row }); }
|
|
186
|
+
}
|
|
187
|
+
const timingByMeasure = Object.fromEntries(definition.definition.measures.filter((measure) => measure.kind === "exposure").map((measure) => [measure.id, measure.exposureTiming!])) as Record<string, "origin-static" | "valuation-specific">;
|
|
188
|
+
const exposures = reconcileDiagnosticExposures(normalizedExposureInputs, timingByMeasure);
|
|
189
|
+
const preparedCells: PreparedDiagnosticSourceCell[] = [];
|
|
190
|
+
for (const [key, rows] of cells) {
|
|
191
|
+
const [sourceGroup, origin, valuation] = key.split("\u0000") as [string, string, string];
|
|
192
|
+
const period = diagnosticDevelopmentAge(definition, origin, valuation);
|
|
193
|
+
const contributions: Record<string, DiagnosticMeasureContribution[]> = Object.create(null);
|
|
194
|
+
const blockers: Record<string, DiagnosticStructuralBlocker[]> = Object.create(null);
|
|
195
|
+
for (const measure of definition.definition.measures) { contributions[measure.id] = []; blockers[measure.id] = []; }
|
|
196
|
+
for (const row of rows) {
|
|
197
|
+
for (const measure of definition.definition.measures.filter((item) => item.source !== "exposure")) {
|
|
198
|
+
contributions[measure.id]!.push(auditedDiagnosticContribution(row.recordId, own(row.measures, measure.id), measure.missing, row.source ? [row.source] : []));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
for (const exposure of exposures) {
|
|
202
|
+
if (exposure.status !== "valid" || exposure.sourceGroup !== sourceGroup || exposure.origin !== origin || (exposure.valuation !== undefined && exposure.valuation !== valuation)) continue;
|
|
203
|
+
contributions[exposure.measureId]!.push(auditedDiagnosticContribution(exposure.key, exposure.value, "unknown", exposure.sources, exposure.deduplicated));
|
|
204
|
+
}
|
|
205
|
+
for (const measure of definition.definition.measures.filter((item) => item.source === "exposure")) {
|
|
206
|
+
if (contributions[measure.id]!.length === 0) blockers[measure.id]!.push({ code: "loss-without-exposure", message: "Loss cell has no applicable exposure observation", sourceIds: rows.map((row) => row.recordId).sort(codeUnit), sources: [] });
|
|
207
|
+
}
|
|
208
|
+
const components = Object.fromEntries(definition.definition.measures.map((measure) => [measure.id, finalizeDiagnosticContributions(contributions[measure.id]!, measure.missing, blockers[measure.id])])) as Record<string, DiagnosticMeasureStats>;
|
|
209
|
+
preparedCells.push(deepFreeze({ sourceGroup, origin, valuation, developmentAge: period.developmentAge, ageUnit: period.ageUnit, lossRecordIds: rows.map((row) => row.recordId).sort(codeUnit), contributions, components, structuralBlockers: blockers, findings: [] }));
|
|
210
|
+
}
|
|
211
|
+
preparedCells.sort((a, b) => codeUnit(a.sourceGroup, b.sourceGroup) || codeUnit(a.origin, b.origin) || codeUnit(a.valuation, b.valuation));
|
|
212
|
+
const expectedCells = (input.expectedCells ?? []).map((item) => ({ ...item }));
|
|
213
|
+
for (const item of expectedCells) audit.push({ kind: "expected-cell", disposition: "retained", record: item });
|
|
214
|
+
const identity = deepFreeze({ definitionIntegrity: definition.definitionIntegrity, filter, completePeriodCutoffs: [...(input.completePeriodCutoffs ?? [])], inputAudit: audit, cells: preparedCells, exposures, expectedCellsProvided: input.expectedCells !== undefined, expectedCells, findings: [] });
|
|
215
|
+
const preparationFingerprint = `fnv1a64-jcs-v1:${fnv1a64(canonicalJson({ identityVersion: 1, kind: "diagnostic-preparation", preparation: identity }))}`;
|
|
216
|
+
const preparedBase = { definition, preparationFingerprint, filter, completePeriodCutoffs: input.completePeriodCutoffs ?? [], inputAudit: audit, cells: preparedCells, exposures, expectedCellsProvided: input.expectedCells !== undefined, expectedCells, findings: [] };
|
|
217
|
+
const prepared = deepFreeze(preparedBase) as unknown as PreparedDiagnosticData;
|
|
218
|
+
authentic.add(prepared);
|
|
219
|
+
identities.set(prepared, identity);
|
|
220
|
+
return prepared;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function assertPreparedDiagnosticData(value: unknown): asserts value is PreparedDiagnosticData {
|
|
224
|
+
if (value === null || typeof value !== "object" || !authentic.has(value)) throw new DiagnosticValidationError([{ domain: "input", code: "invalid-input-relationship", path: "$.prepared", message: "Value is not authentic prepared diagnostic data" }]);
|
|
225
|
+
}
|
|
226
|
+
export function getPreparedDiagnosticDataIdentity(value: PreparedDiagnosticData): DiagnosticDeepReadonly<NormalizedDiagnosticPreparationIdentity> { assertPreparedDiagnosticData(value); return identities.get(value)! }
|
|
227
|
+
export function verifyPreparedDiagnosticDataIntegrity(value: PreparedDiagnosticData): void {
|
|
228
|
+
const identity = getPreparedDiagnosticDataIdentity(value);
|
|
229
|
+
const tag = `fnv1a64-jcs-v1:${fnv1a64(canonicalJson({ identityVersion: 1, kind: "diagnostic-preparation", preparation: identity }))}`;
|
|
230
|
+
if (tag !== value.preparationFingerprint) throw new DiagnosticValidationError([{ domain: "input", code: "invalid-input-relationship", path: "$.prepared.preparationFingerprint", message: "Prepared diagnostic data integrity does not match its content" }]);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export type { JsonValue, DiagnosticAuditedNumericValue };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { getCompiledDiagnosticDefinitionInternals, type DiagnosticReviewRule, type DiagnosticSourceLocation } from "./diagnosticDefinitions.js";
|
|
2
|
+
import { evaluateDiagnosticMeasureExpression, type FinalizedDiagnosticMeasure } from "./diagnosticFormulas.js";
|
|
3
|
+
import { assertPreparedDiagnosticData, type PreparedDiagnosticData } from "./diagnosticPreparation.js";
|
|
4
|
+
import { classifyDiagnosticComparison, diagnosticPredicateMatches, type DiagnosticRuleNotEvaluatedReason } from "./diagnosticRules.js";
|
|
5
|
+
|
|
6
|
+
export interface DiagnosticReviewCoordinate { readonly sourceGroup: string; readonly origin: string; readonly valuation: string; readonly developmentAge: number; readonly ageUnit: string }
|
|
7
|
+
export type DiagnosticReviewEvaluationScope =
|
|
8
|
+
| { readonly kind: "cell"; readonly coordinate: DiagnosticReviewCoordinate; readonly sources: readonly DiagnosticSourceLocation[] }
|
|
9
|
+
| { readonly kind: "valuation-pair"; readonly previous: DiagnosticReviewCoordinate; readonly current: DiagnosticReviewCoordinate; readonly sources: readonly DiagnosticSourceLocation[] }
|
|
10
|
+
| { readonly kind: "control-total"; readonly selectedCellCount: number; readonly selectedContributionCount: number; readonly sources: readonly DiagnosticSourceLocation[] };
|
|
11
|
+
export interface DiagnosticReviewRuleEvaluation {
|
|
12
|
+
readonly ruleId: string;
|
|
13
|
+
readonly kind: DiagnosticReviewRule["kind"];
|
|
14
|
+
readonly status: "pass" | "triggered" | "not-evaluated";
|
|
15
|
+
readonly severity: "warning" | "fail";
|
|
16
|
+
readonly triggerReason: "predicate" | "missing-input" | null;
|
|
17
|
+
readonly left: number | null;
|
|
18
|
+
readonly right: number | null;
|
|
19
|
+
readonly relation: "less" | "equal" | "greater" | null;
|
|
20
|
+
readonly notEvaluatedReasons: readonly DiagnosticRuleNotEvaluatedReason[];
|
|
21
|
+
readonly expressionOverflows: readonly { readonly expressionPath: string; readonly sources: readonly DiagnosticSourceLocation[] }[];
|
|
22
|
+
readonly reviewScope: DiagnosticReviewEvaluationScope;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function states(prepared: PreparedDiagnosticData, components: typeof prepared.cells[number]["components"]): Record<string, FinalizedDiagnosticMeasure> {
|
|
26
|
+
const internals = getCompiledDiagnosticDefinitionInternals(prepared.definition);
|
|
27
|
+
return Object.fromEntries(Object.entries(components).map(([id, stats]) => {
|
|
28
|
+
const measure = internals.measuresById.get(id)!;
|
|
29
|
+
const readiness: DiagnosticRuleNotEvaluatedReason[] = [];
|
|
30
|
+
if (stats.missing) readiness.push(stats.imputedZero ? "imputed" : "missing");
|
|
31
|
+
if (stats.nonFinite) readiness.push("non-finite");
|
|
32
|
+
if (stats.structural) readiness.push("structural-ambiguity");
|
|
33
|
+
if (stats.sum === null && !stats.nonFinite && !stats.structural) readiness.push("aggregation-overflow");
|
|
34
|
+
return [id, { quantity: { kind: measure.kind, unit: measure.unit, value: stats.value }, stats, readiness }];
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function coordinate(cell: PreparedDiagnosticData["cells"][number]): DiagnosticReviewCoordinate { return { sourceGroup: cell.sourceGroup, origin: cell.origin, valuation: cell.valuation, developmentAge: cell.developmentAge, ageUnit: cell.ageUnit } }
|
|
39
|
+
|
|
40
|
+
function evaluation(rule: DiagnosticReviewRule, scope: DiagnosticReviewEvaluationScope, left: number|null, right: number|null, asserted: boolean, reasons: readonly DiagnosticRuleNotEvaluatedReason[] = []): DiagnosticReviewRuleEvaluation {
|
|
41
|
+
if (left === null || right === null || reasons.length) return { ruleId: rule.id, kind: rule.kind, status: rule.missingInput === "finding" ? "triggered" : "not-evaluated", severity: rule.severity, triggerReason: rule.missingInput === "finding" ? "missing-input" : null, left, right, relation: null, notEvaluatedReasons: reasons.length ? reasons : ["missing"], expressionOverflows: [], reviewScope: scope };
|
|
42
|
+
const compared = classifyDiagnosticComparison(left, right, rule.tolerance);
|
|
43
|
+
if (compared.status === "not-evaluated") return { ruleId: rule.id, kind: rule.kind, status: "not-evaluated", severity: rule.severity, triggerReason: null, left, right, relation: null, notEvaluatedReasons: [compared.reason], expressionOverflows: [], reviewScope: scope };
|
|
44
|
+
return { ruleId: rule.id, kind: rule.kind, status: asserted ? "pass" : "triggered", severity: rule.severity, triggerReason: asserted ? null : "predicate", left, right, relation: compared.relation, notEvaluatedReasons: [], expressionOverflows: [], reviewScope: scope };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Evaluates definition-owned semantic review rules over authenticated prepared cells. */
|
|
48
|
+
export function evaluateDiagnosticReviewRules(prepared: PreparedDiagnosticData): readonly DiagnosticReviewRuleEvaluation[] {
|
|
49
|
+
assertPreparedDiagnosticData(prepared);
|
|
50
|
+
const results: DiagnosticReviewRuleEvaluation[] = [];
|
|
51
|
+
for (const rule of prepared.definition.definition.reviewRules as readonly DiagnosticReviewRule[]) {
|
|
52
|
+
if (rule.kind === "control-total") {
|
|
53
|
+
const values = prepared.cells.map((cell) => evaluateDiagnosticMeasureExpression(rule.expression, states(prepared, cell.components), "$.reviewRules.control-total").value);
|
|
54
|
+
const left = values.some((value) => value === null) ? null : values.reduce<number>((sum, value) => sum + value!, 0);
|
|
55
|
+
const scope = { kind: "control-total" as const, selectedCellCount: prepared.cells.length, selectedContributionCount: prepared.cells.reduce((sum, cell) => sum + Object.values(cell.contributions).reduce((n, items) => n + items.length, 0), 0), sources: [] };
|
|
56
|
+
const classified = classifyDiagnosticComparison(left, rule.expected, rule.tolerance);
|
|
57
|
+
results.push(evaluation(rule, scope, left, rule.expected, classified.status === "evaluated" && classified.relation === "equal"));
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (rule.kind === "monotonic") {
|
|
61
|
+
const groups = new Map<string, typeof prepared.cells>();
|
|
62
|
+
for (const cell of prepared.cells) { const key=`${cell.sourceGroup}\0${cell.origin}`; groups.set(key,[...(groups.get(key)??[]),cell]); }
|
|
63
|
+
for (const cells of groups.values()) {
|
|
64
|
+
const sorted=[...cells].sort((a,b)=>a.developmentAge-b.developmentAge);
|
|
65
|
+
for(let index=1;index<sorted.length;index++){
|
|
66
|
+
const previous=sorted[index-1]!,current=sorted[index]!;
|
|
67
|
+
const left=evaluateDiagnosticMeasureExpression(rule.expression,states(prepared,previous.components),"$.reviewRules.monotonic").value;
|
|
68
|
+
const right=evaluateDiagnosticMeasureExpression(rule.expression,states(prepared,current.components),"$.reviewRules.monotonic").value;
|
|
69
|
+
results.push(evaluation(rule,{kind:"valuation-pair",previous:coordinate(previous),current:coordinate(current),sources:[]},left,right,rule.direction==="nondecreasing"?left!==null&&right!==null&&left<=right:left!==null&&right!==null&&left>=right));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
for (const cell of prepared.cells) {
|
|
75
|
+
const state=states(prepared,cell.components); const scope={kind:"cell" as const,coordinate:coordinate(cell),sources:[]};
|
|
76
|
+
if(rule.kind==="compare"){
|
|
77
|
+
const operand=(item:typeof rule.when.left)=>item.op==="constant"?item.value:evaluateDiagnosticMeasureExpression(item,state,"$.reviewRules.compare").value;
|
|
78
|
+
const left=operand(rule.when.left),right=operand(rule.when.right);const classified=classifyDiagnosticComparison(left,right,rule.tolerance);
|
|
79
|
+
results.push(evaluation(rule,scope,left,right,classified.status==="evaluated"&&!diagnosticPredicateMatches(rule.when.operator,classified.relation)));
|
|
80
|
+
} else if(rule.kind==="reconcile"){
|
|
81
|
+
const left=evaluateDiagnosticMeasureExpression(rule.actual,state,"$.reviewRules.reconcile.actual").value;
|
|
82
|
+
const right=rule.expected.op==="constant"?rule.expected.value:evaluateDiagnosticMeasureExpression(rule.expected,state,"$.reviewRules.reconcile.expected").value;
|
|
83
|
+
const classified=classifyDiagnosticComparison(left,right,rule.tolerance);results.push(evaluation(rule,scope,left,right,classified.status==="evaluated"&&classified.relation==="equal"));
|
|
84
|
+
} else {
|
|
85
|
+
const left=evaluateDiagnosticMeasureExpression(rule.narrower,state,"$.reviewRules.layer.narrower").value;const right=evaluateDiagnosticMeasureExpression(rule.broader,state,"$.reviewRules.layer.broader").value;
|
|
86
|
+
results.push(evaluation(rule,scope,left,right,left!==null&&right!==null&&left<=right));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return Object.freeze(results.map((item) => Object.freeze(item)));
|
|
91
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { DiagnosticComparisonPredicate, DiagnosticSourceLocation } from "./diagnosticDefinitions.js";
|
|
2
|
+
|
|
3
|
+
export type DiagnosticComparisonClassification =
|
|
4
|
+
| { readonly status: "evaluated"; readonly relation: "less" | "equal" | "greater" }
|
|
5
|
+
| { readonly status: "not-evaluated"; readonly reason: "missing" | "non-finite" | "tolerance-overflow" };
|
|
6
|
+
|
|
7
|
+
export type DiagnosticRuleNotEvaluatedReason =
|
|
8
|
+
| "missing"
|
|
9
|
+
| "imputed"
|
|
10
|
+
| "non-finite"
|
|
11
|
+
| "structural-ambiguity"
|
|
12
|
+
| "aggregation-overflow"
|
|
13
|
+
| "expression-overflow"
|
|
14
|
+
| "tolerance-overflow";
|
|
15
|
+
|
|
16
|
+
export interface DiagnosticExpressionOverflow {
|
|
17
|
+
readonly expressionPath: string;
|
|
18
|
+
readonly sources: readonly DiagnosticSourceLocation[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DiagnosticRuleEvaluation {
|
|
22
|
+
readonly ruleId: string;
|
|
23
|
+
readonly status: "pass" | "triggered" | "not-evaluated";
|
|
24
|
+
readonly severity: "warning" | "fail";
|
|
25
|
+
readonly left: number | null;
|
|
26
|
+
readonly right: number | null;
|
|
27
|
+
readonly relation: "less" | "equal" | "greater" | null;
|
|
28
|
+
readonly notEvaluatedReasons: readonly DiagnosticRuleNotEvaluatedReason[];
|
|
29
|
+
readonly expressionOverflows: readonly DiagnosticExpressionOverflow[];
|
|
30
|
+
readonly code: string | null;
|
|
31
|
+
readonly message: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Pure, overflow-safe three-way comparison shared by metric and review rules. */
|
|
35
|
+
export function classifyDiagnosticComparison(
|
|
36
|
+
left: number | null,
|
|
37
|
+
right: number | null,
|
|
38
|
+
tolerance: { readonly absolute?: number; readonly relative?: number } = {},
|
|
39
|
+
): DiagnosticComparisonClassification {
|
|
40
|
+
if (left === null || right === null) return { status: "not-evaluated", reason: "missing" };
|
|
41
|
+
if (!Number.isFinite(left) || !Number.isFinite(right)) return { status: "not-evaluated", reason: "non-finite" };
|
|
42
|
+
const absolute = tolerance.absolute ?? 0;
|
|
43
|
+
const relative = tolerance.relative ?? 0;
|
|
44
|
+
const scale = Math.max(1, Math.abs(left), Math.abs(right));
|
|
45
|
+
const relativeTolerance = relative * scale;
|
|
46
|
+
const totalTolerance = absolute + relativeTolerance;
|
|
47
|
+
if (!Number.isFinite(absolute) || absolute < 0 || !Number.isFinite(relative) || relative < 0 ||
|
|
48
|
+
!Number.isFinite(relativeTolerance) || !Number.isFinite(totalTolerance)) {
|
|
49
|
+
return { status: "not-evaluated", reason: "tolerance-overflow" };
|
|
50
|
+
}
|
|
51
|
+
if (left === right) return { status: "evaluated", relation: "equal" };
|
|
52
|
+
const distance = left > right ? left - right : right - left;
|
|
53
|
+
if (Number.isFinite(distance) && distance <= totalTolerance) {
|
|
54
|
+
return { status: "evaluated", relation: "equal" };
|
|
55
|
+
}
|
|
56
|
+
return { status: "evaluated", relation: left < right ? "less" : "greater" };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function diagnosticPredicateMatches(
|
|
60
|
+
predicate: DiagnosticComparisonPredicate["operator"],
|
|
61
|
+
relation: "less" | "equal" | "greater",
|
|
62
|
+
): boolean {
|
|
63
|
+
return predicate === "lt" ? relation === "less"
|
|
64
|
+
: predicate === "lte" ? relation !== "greater"
|
|
65
|
+
: predicate === "eq" ? relation === "equal"
|
|
66
|
+
: predicate === "neq" ? relation !== "equal"
|
|
67
|
+
: predicate === "gte" ? relation !== "less"
|
|
68
|
+
: relation === "greater";
|
|
69
|
+
}
|