@actuarial-ts/core 0.6.0 → 0.7.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 +12 -3
- package/dist/canonical.d.ts +11 -0
- package/dist/canonical.d.ts.map +1 -1
- package/dist/canonical.js +136 -26
- package/dist/canonical.js.map +1 -1
- package/dist/capping.d.ts.map +1 -1
- package/dist/capping.js +39 -6
- package/dist/capping.js.map +1 -1
- package/dist/diagnosticAggregation.d.ts +3 -0
- package/dist/diagnosticAggregation.d.ts.map +1 -1
- package/dist/diagnosticAggregation.js +30 -7
- package/dist/diagnosticAggregation.js.map +1 -1
- package/dist/diagnosticDefinitions.d.ts +3 -0
- package/dist/diagnosticDefinitions.d.ts.map +1 -1
- package/dist/diagnosticDefinitions.js +844 -182
- package/dist/diagnosticDefinitions.js.map +1 -1
- package/dist/diagnosticDerivations.d.ts +23 -0
- package/dist/diagnosticDerivations.d.ts.map +1 -1
- package/dist/diagnosticDerivations.js +187 -43
- package/dist/diagnosticDerivations.js.map +1 -1
- package/dist/diagnosticEvidenceIntern.d.ts +16 -0
- package/dist/diagnosticEvidenceIntern.d.ts.map +1 -0
- package/dist/diagnosticEvidenceIntern.js +139 -0
- package/dist/diagnosticEvidenceIntern.js.map +1 -0
- package/dist/diagnosticExposure.d.ts +2 -2
- package/dist/diagnosticExposure.d.ts.map +1 -1
- package/dist/diagnosticExposure.js +319 -21
- package/dist/diagnosticExposure.js.map +1 -1
- package/dist/diagnosticExpressions.d.ts +1 -1
- package/dist/diagnosticExpressions.d.ts.map +1 -1
- package/dist/diagnosticExpressions.js +35 -15
- package/dist/diagnosticExpressions.js.map +1 -1
- package/dist/diagnosticFormulas.d.ts +3 -0
- package/dist/diagnosticFormulas.d.ts.map +1 -1
- package/dist/diagnosticFormulas.js +160 -26
- package/dist/diagnosticFormulas.js.map +1 -1
- package/dist/diagnosticIdentity.d.ts +15 -1
- package/dist/diagnosticIdentity.d.ts.map +1 -1
- package/dist/diagnosticIdentity.js +321 -51
- package/dist/diagnosticIdentity.js.map +1 -1
- package/dist/diagnosticIdentityStream.d.ts +56 -0
- package/dist/diagnosticIdentityStream.d.ts.map +1 -0
- package/dist/diagnosticIdentityStream.js +472 -0
- package/dist/diagnosticIdentityStream.js.map +1 -0
- package/dist/diagnosticOrdering.d.ts +8 -0
- package/dist/diagnosticOrdering.d.ts.map +1 -0
- package/dist/diagnosticOrdering.js +73 -0
- package/dist/diagnosticOrdering.js.map +1 -0
- package/dist/diagnosticPeriodAxis.d.ts +9 -0
- package/dist/diagnosticPeriodAxis.d.ts.map +1 -0
- package/dist/diagnosticPeriodAxis.js +69 -0
- package/dist/diagnosticPeriodAxis.js.map +1 -0
- package/dist/diagnosticPeriods.d.ts +2 -3
- package/dist/diagnosticPeriods.d.ts.map +1 -1
- package/dist/diagnosticPeriods.js +20 -68
- package/dist/diagnosticPeriods.js.map +1 -1
- package/dist/diagnosticPreparation.d.ts +75 -14
- package/dist/diagnosticPreparation.d.ts.map +1 -1
- package/dist/diagnosticPreparation.js +1606 -112
- package/dist/diagnosticPreparation.js.map +1 -1
- package/dist/diagnosticReview.d.ts +40 -15
- package/dist/diagnosticReview.d.ts.map +1 -1
- package/dist/diagnosticReview.js +600 -42
- package/dist/diagnosticReview.js.map +1 -1
- package/dist/diagnosticReviewSources.d.ts +18 -0
- package/dist/diagnosticReviewSources.d.ts.map +1 -0
- package/dist/diagnosticReviewSources.js +76 -0
- package/dist/diagnosticReviewSources.js.map +1 -0
- package/dist/diagnosticReviewStore.d.ts +88 -0
- package/dist/diagnosticReviewStore.d.ts.map +1 -0
- package/dist/diagnosticReviewStore.js +455 -0
- package/dist/diagnosticReviewStore.js.map +1 -0
- package/dist/diagnosticRunner.d.ts +27 -2
- package/dist/diagnosticRunner.d.ts.map +1 -1
- package/dist/diagnosticRunner.js +764 -70
- package/dist/diagnosticRunner.js.map +1 -1
- package/dist/diagnosticRuntime.d.ts +26 -0
- package/dist/diagnosticRuntime.d.ts.map +1 -0
- package/dist/diagnosticRuntime.js +243 -0
- package/dist/diagnosticRuntime.js.map +1 -0
- package/dist/diagnosticSourceOrdering.d.ts +6 -0
- package/dist/diagnosticSourceOrdering.d.ts.map +1 -0
- package/dist/diagnosticSourceOrdering.js +38 -0
- package/dist/diagnosticSourceOrdering.js.map +1 -0
- package/dist/fnvAccumulator.d.ts +6 -0
- package/dist/fnvAccumulator.d.ts.map +1 -0
- package/dist/fnvAccumulator.js +21 -0
- package/dist/fnvAccumulator.js.map +1 -0
- package/dist/index.d.ts +15 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/mack.d.ts +4 -0
- package/dist/mack.d.ts.map +1 -1
- package/dist/mack.js +32 -4
- package/dist/mack.js.map +1 -1
- package/dist/triangle.d.ts.map +1 -1
- package/dist/triangle.js +59 -9
- package/dist/triangle.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +22 -8
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
- package/src/canonical.ts +144 -26
- package/src/capping.ts +109 -18
- package/src/diagnosticAggregation.ts +69 -20
- package/src/diagnosticDefinitions.ts +2675 -426
- package/src/diagnosticDerivations.ts +281 -49
- package/src/diagnosticEvidenceIntern.ts +151 -0
- package/src/diagnosticExposure.ts +550 -50
- package/src/diagnosticExpressions.ts +99 -17
- package/src/diagnosticFormulas.ts +239 -39
- package/src/diagnosticIdentity.ts +700 -191
- package/src/diagnosticIdentityStream.ts +553 -0
- package/src/diagnosticOrdering.ts +106 -0
- package/src/diagnosticPeriodAxis.ts +92 -0
- package/src/diagnosticPeriods.ts +67 -82
- package/src/diagnosticPreparation.ts +2558 -132
- package/src/diagnosticReview.ts +1072 -58
- package/src/diagnosticReviewSources.ts +85 -0
- package/src/diagnosticReviewStore.ts +692 -0
- package/src/diagnosticRunner.ts +1273 -72
- package/src/diagnosticRuntime.ts +264 -0
- package/src/diagnosticSourceOrdering.ts +50 -0
- package/src/fnvAccumulator.ts +20 -0
- package/src/index.ts +69 -4
- package/src/mack.ts +72 -9
- package/src/triangle.ts +103 -13
- package/src/types.ts +55 -20
- package/src/version.ts +1 -1
package/src/diagnosticRunner.ts
CHANGED
|
@@ -1,110 +1,1311 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
DiagnosticDeepReadonly,
|
|
3
|
+
DiagnosticMeasureDefinition,
|
|
4
|
+
DiagnosticMetricPresentation,
|
|
5
|
+
DiagnosticsFilter,
|
|
6
|
+
JsonValue,
|
|
7
|
+
} from "./diagnosticDefinitions.js";
|
|
2
8
|
import { getCompiledDiagnosticDefinitionInternals } from "./diagnosticDefinitions.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
9
|
+
import { finalizeDiagnosticContributions } from "./diagnosticAggregation.js";
|
|
10
|
+
import {
|
|
11
|
+
applyDiagnosticPresentation,
|
|
12
|
+
diagnosticRawRatio,
|
|
13
|
+
evaluateDiagnosticMeasureExpression,
|
|
14
|
+
evaluateDiagnosticRoleExpression,
|
|
15
|
+
type DiagnosticMetricFinding,
|
|
16
|
+
type DiagnosticQuantity,
|
|
17
|
+
type FinalizedDiagnosticMeasure,
|
|
18
|
+
} from "./diagnosticFormulas.js";
|
|
19
|
+
import {
|
|
20
|
+
classifyDiagnosticComparison,
|
|
21
|
+
diagnosticPredicateMatches,
|
|
22
|
+
type DiagnosticRuleEvaluation,
|
|
23
|
+
} from "./diagnosticRules.js";
|
|
24
|
+
import {
|
|
25
|
+
assertCompactPreparedDiagnosticData,
|
|
26
|
+
assertPreparedDiagnosticData,
|
|
27
|
+
getCompactPreparedDiagnosticDataFingerprint,
|
|
28
|
+
type CompactPreparedDiagnosticData,
|
|
29
|
+
type PreparedDiagnosticData,
|
|
30
|
+
type PreparedDiagnosticDataContent,
|
|
31
|
+
} from "./diagnosticPreparation.js";
|
|
32
|
+
import {
|
|
33
|
+
createDiagnosticIdentityObject,
|
|
34
|
+
createDiagnosticIdentityValue,
|
|
35
|
+
type DiagnosticIdentityDocument,
|
|
36
|
+
} from "./diagnosticIdentityStream.js";
|
|
6
37
|
import type { DiagnosticMeasureStats } from "./diagnosticDefinitions.js";
|
|
7
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
DiagnosticValidationError,
|
|
40
|
+
type DiagnosticValidationIssue,
|
|
41
|
+
} from "./types.js";
|
|
42
|
+
import { canonicalJson } from "./canonical.js";
|
|
43
|
+
import {
|
|
44
|
+
projectDiagnosticIdentity,
|
|
45
|
+
type DiagnosticIdentityProjection,
|
|
46
|
+
} from "./diagnosticIdentity.js";
|
|
47
|
+
import { compareDiagnosticFindings } from "./diagnosticOrdering.js";
|
|
48
|
+
import { normalizeDiagnosticPeriod } from "./diagnosticPeriods.js";
|
|
49
|
+
import { normalizeDiagnosticSourceLocations } from "./diagnosticSourceOrdering.js";
|
|
50
|
+
import {
|
|
51
|
+
diagnosticJsonPreflight,
|
|
52
|
+
hasDiagnosticOwn,
|
|
53
|
+
isDiagnosticPlainRecord,
|
|
54
|
+
isDiagnosticToken,
|
|
55
|
+
} from "./diagnosticRuntime.js";
|
|
8
56
|
|
|
9
|
-
export interface RunMetricDiagnosticsInput {
|
|
57
|
+
export interface RunMetricDiagnosticsInput {
|
|
58
|
+
readonly prepared: PreparedDiagnosticData;
|
|
59
|
+
readonly groupMap?: Readonly<Record<string, string>>;
|
|
60
|
+
readonly groupDimensions?: Readonly<Record<string, JsonValue>>;
|
|
61
|
+
}
|
|
62
|
+
export interface RunMetricDiagnosticsCompactInput {
|
|
63
|
+
readonly prepared: CompactPreparedDiagnosticData;
|
|
64
|
+
readonly groupMap?: Readonly<Record<string, string>>;
|
|
65
|
+
readonly groupDimensions?: Readonly<Record<string, JsonValue>>;
|
|
66
|
+
}
|
|
67
|
+
type RunMetricDiagnosticsContentInput = Omit<
|
|
68
|
+
RunMetricDiagnosticsInput,
|
|
69
|
+
"prepared"
|
|
70
|
+
> & { readonly prepared: PreparedDiagnosticDataContent };
|
|
10
71
|
export interface DiagnosticMetricEvaluation {
|
|
11
|
-
readonly instanceId: string;
|
|
12
|
-
readonly
|
|
13
|
-
readonly
|
|
14
|
-
readonly
|
|
15
|
-
readonly
|
|
72
|
+
readonly instanceId: string;
|
|
73
|
+
readonly instanceVersion: string;
|
|
74
|
+
readonly formulaId: string;
|
|
75
|
+
readonly formulaVersion: string;
|
|
76
|
+
readonly semanticReferences: {
|
|
77
|
+
readonly amountBasisIds: readonly string[];
|
|
78
|
+
readonly countPopulationIds: readonly string[];
|
|
79
|
+
readonly exposureBasisIds: readonly string[];
|
|
80
|
+
};
|
|
81
|
+
readonly formulaFingerprint: string;
|
|
82
|
+
readonly calculationFingerprint: string;
|
|
83
|
+
readonly definitionIntegrity: string;
|
|
84
|
+
readonly calculation: {
|
|
85
|
+
readonly numerator: DiagnosticQuantity;
|
|
86
|
+
readonly denominator: DiagnosticQuantity;
|
|
87
|
+
readonly value: number | null;
|
|
88
|
+
};
|
|
89
|
+
readonly presentation: DiagnosticMetricPresentation & {
|
|
90
|
+
readonly value: number | null;
|
|
91
|
+
};
|
|
16
92
|
readonly components: Readonly<Record<string, DiagnosticMeasureStats>>;
|
|
17
93
|
readonly rules: readonly DiagnosticRuleEvaluation[];
|
|
18
94
|
readonly findings: readonly DiagnosticMetricFinding[];
|
|
19
95
|
}
|
|
20
|
-
export interface DiagnosticEmergencePoint {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
96
|
+
export interface DiagnosticEmergencePoint {
|
|
97
|
+
readonly group: string;
|
|
98
|
+
readonly sourceGroups: readonly string[];
|
|
99
|
+
readonly dimensions?: JsonValue;
|
|
100
|
+
readonly origin: string;
|
|
101
|
+
readonly valuation: string;
|
|
102
|
+
readonly developmentAge: number;
|
|
103
|
+
readonly ageUnit: string;
|
|
104
|
+
readonly components: Readonly<Record<string, DiagnosticMeasureStats>>;
|
|
105
|
+
readonly metrics: Readonly<Record<string, DiagnosticMetricEvaluation>>;
|
|
106
|
+
readonly findings: readonly DiagnosticMetricFinding[];
|
|
107
|
+
}
|
|
108
|
+
export interface DiagnosticMetricTriangleCell {
|
|
109
|
+
readonly origin: string;
|
|
110
|
+
readonly valuation: string;
|
|
111
|
+
readonly developmentAge: number;
|
|
112
|
+
readonly ageUnit: string;
|
|
113
|
+
readonly evaluation: DiagnosticMetricEvaluation;
|
|
114
|
+
}
|
|
115
|
+
export interface DiagnosticMetricTriangle {
|
|
116
|
+
readonly group: string;
|
|
117
|
+
readonly instanceId: string;
|
|
118
|
+
readonly origins: readonly string[];
|
|
119
|
+
readonly developmentAges: readonly number[];
|
|
120
|
+
readonly ageUnit: string;
|
|
121
|
+
readonly calculationValues: readonly (readonly (number | null)[])[];
|
|
122
|
+
readonly presentationValues: readonly (readonly (number | null)[])[];
|
|
123
|
+
readonly cells: readonly (readonly (DiagnosticMetricTriangleCell | null)[])[];
|
|
124
|
+
}
|
|
125
|
+
export interface MetricDiagnosticsResult {
|
|
126
|
+
readonly definitionIntegrity: string;
|
|
127
|
+
readonly preparationFingerprint: string;
|
|
128
|
+
readonly ageUnit: string;
|
|
129
|
+
readonly emergence: readonly DiagnosticEmergencePoint[];
|
|
130
|
+
readonly triangles: readonly DiagnosticMetricTriangle[];
|
|
131
|
+
readonly latestDiagonal: readonly DiagnosticEmergencePoint[];
|
|
132
|
+
readonly findings: readonly DiagnosticMetricFinding[];
|
|
133
|
+
}
|
|
134
|
+
export type MetricDiagnosticsResultContent = Omit<
|
|
135
|
+
MetricDiagnosticsResult,
|
|
136
|
+
"preparationFingerprint"
|
|
137
|
+
>;
|
|
138
|
+
declare const compactMetricDiagnosticsResultBrand: unique symbol;
|
|
139
|
+
/** Complete numeric views whose preparation identity is explicitly deferred. */
|
|
140
|
+
export interface CompactMetricDiagnosticsResult extends MetricDiagnosticsResultContent {
|
|
141
|
+
readonly [compactMetricDiagnosticsResultBrand]: true;
|
|
142
|
+
}
|
|
143
|
+
const compactResultPreparations = new WeakMap<
|
|
144
|
+
object,
|
|
145
|
+
CompactPreparedDiagnosticData
|
|
146
|
+
>();
|
|
147
|
+
const compactResultDocuments = new WeakMap<
|
|
148
|
+
object,
|
|
149
|
+
DiagnosticIdentityDocument
|
|
150
|
+
>();
|
|
151
|
+
export interface CommonMaturityResult {
|
|
152
|
+
readonly developmentAge: number | null;
|
|
153
|
+
readonly ageUnit: string;
|
|
154
|
+
readonly points: readonly DiagnosticDeepReadonly<DiagnosticEmergencePoint>[];
|
|
155
|
+
}
|
|
156
|
+
export type NormalizedDiagnosticResultIdentity = DiagnosticIdentityProjection<
|
|
157
|
+
DiagnosticDeepReadonly<MetricDiagnosticsResult>
|
|
158
|
+
>;
|
|
159
|
+
|
|
160
|
+
function codeUnit(a: string, b: string): number {
|
|
161
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
162
|
+
}
|
|
163
|
+
function pointer(path: string, segment: string | number): string {
|
|
164
|
+
return `${path}/${String(segment).replaceAll("~", "~0").replaceAll("/", "~1")}`;
|
|
165
|
+
}
|
|
166
|
+
function deepFreeze<T>(
|
|
167
|
+
value: T,
|
|
168
|
+
seen = new WeakSet<object>(),
|
|
169
|
+
): DiagnosticDeepReadonly<T> {
|
|
170
|
+
if (value === null || typeof value !== "object" || seen.has(value))
|
|
171
|
+
return value as DiagnosticDeepReadonly<T>;
|
|
172
|
+
seen.add(value);
|
|
173
|
+
for (const child of Object.values(value as Record<string, unknown>))
|
|
174
|
+
deepFreeze(child, seen);
|
|
175
|
+
return Object.freeze(value) as DiagnosticDeepReadonly<T>;
|
|
176
|
+
}
|
|
177
|
+
function quantity(
|
|
178
|
+
measure: DiagnosticMeasureDefinition,
|
|
179
|
+
value: number | null,
|
|
180
|
+
): DiagnosticQuantity {
|
|
181
|
+
return {
|
|
182
|
+
kind: measure.kind,
|
|
183
|
+
unit: measure.unit,
|
|
184
|
+
...(measure.basisId ? { basisId: measure.basisId } : {}),
|
|
185
|
+
...(measure.countPopulationId
|
|
186
|
+
? { countPopulationId: measure.countPopulationId }
|
|
187
|
+
: {}),
|
|
188
|
+
...(measure.exposureBasisId
|
|
189
|
+
? { exposureBasisId: measure.exposureBasisId }
|
|
190
|
+
: {}),
|
|
191
|
+
value,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const READINESS_ORDER = [
|
|
196
|
+
"missing",
|
|
197
|
+
"imputed",
|
|
198
|
+
"non-finite",
|
|
199
|
+
"structural-ambiguity",
|
|
200
|
+
"aggregation-overflow",
|
|
201
|
+
"expression-overflow",
|
|
202
|
+
"tolerance-overflow",
|
|
203
|
+
] as const;
|
|
204
|
+
function readiness(
|
|
205
|
+
values: readonly (typeof READINESS_ORDER)[number][],
|
|
206
|
+
): (typeof READINESS_ORDER)[number][] {
|
|
207
|
+
return READINESS_ORDER.filter((value) => values.includes(value));
|
|
208
|
+
}
|
|
209
|
+
function expressionMeasureIds(
|
|
210
|
+
expression: import("./diagnosticExpressions.js").DiagnosticMeasureExpression,
|
|
211
|
+
): string[] {
|
|
212
|
+
if (expression.op === "measure") return [expression.measureId];
|
|
213
|
+
return [
|
|
214
|
+
...new Set(
|
|
215
|
+
(expression.op === "add"
|
|
216
|
+
? expression.terms
|
|
217
|
+
: [expression.left, expression.right]
|
|
218
|
+
).flatMap(expressionMeasureIds),
|
|
219
|
+
),
|
|
220
|
+
].sort(codeUnit);
|
|
221
|
+
}
|
|
222
|
+
function unionSources(
|
|
223
|
+
values: readonly import("./diagnosticDefinitions.js").DiagnosticSourceLocation[],
|
|
224
|
+
): import("./diagnosticDefinitions.js").DiagnosticSourceLocation[] {
|
|
225
|
+
return normalizeDiagnosticSourceLocations(values);
|
|
226
|
+
}
|
|
26
227
|
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
228
|
+
function mergeFindings(
|
|
229
|
+
values: readonly DiagnosticMetricFinding[],
|
|
230
|
+
): DiagnosticMetricFinding[] {
|
|
231
|
+
const merged = new Map<string, DiagnosticMetricFinding>();
|
|
232
|
+
for (const value of values) {
|
|
233
|
+
const key = canonicalJson({ ...value, sources: [] });
|
|
234
|
+
const previous = merged.get(key);
|
|
235
|
+
const sources = normalizeDiagnosticSourceLocations([
|
|
236
|
+
...(previous?.sources ?? []),
|
|
237
|
+
...value.sources,
|
|
238
|
+
]);
|
|
239
|
+
merged.set(key, { ...(previous ?? value), sources });
|
|
240
|
+
}
|
|
241
|
+
return [...merged.values()].sort(compareDiagnosticFindings);
|
|
242
|
+
}
|
|
30
243
|
|
|
31
|
-
export function validateDiagnosticGroupingConfiguration(
|
|
244
|
+
export function validateDiagnosticGroupingConfiguration(
|
|
245
|
+
input: RunMetricDiagnosticsInput,
|
|
246
|
+
): void {
|
|
32
247
|
assertPreparedDiagnosticData(input.prepared);
|
|
33
|
-
|
|
248
|
+
validateGroupingContent(input);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function validateCompactDiagnosticGroupingConfiguration(
|
|
252
|
+
input: RunMetricDiagnosticsCompactInput,
|
|
253
|
+
): void {
|
|
254
|
+
assertCompactPreparedDiagnosticData(input.prepared);
|
|
255
|
+
validateGroupingContent(input);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function validateGroupingContent(
|
|
259
|
+
input: RunMetricDiagnosticsContentInput,
|
|
260
|
+
): void {
|
|
261
|
+
const boundaryIssues: DiagnosticValidationIssue[] = [];
|
|
262
|
+
for (const [value, path] of [
|
|
263
|
+
[input.groupMap, "$.groupMap"],
|
|
264
|
+
[input.groupDimensions, "$.groupDimensions"],
|
|
265
|
+
] as const) {
|
|
266
|
+
if (value === undefined) continue;
|
|
267
|
+
if (!isDiagnosticPlainRecord(value))
|
|
268
|
+
boundaryIssues.push({
|
|
269
|
+
domain: "configuration",
|
|
270
|
+
code: "invalid-type",
|
|
271
|
+
path,
|
|
272
|
+
message: "Grouping configuration must be a plain object",
|
|
273
|
+
});
|
|
274
|
+
else
|
|
275
|
+
boundaryIssues.push(
|
|
276
|
+
...diagnosticJsonPreflight(value, "configuration").map((issue) => ({
|
|
277
|
+
...issue,
|
|
278
|
+
path: issue.path === "$" ? path : `${path}${issue.path.slice(1)}`,
|
|
279
|
+
})),
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (boundaryIssues.length > 0)
|
|
283
|
+
throw new DiagnosticValidationError(boundaryIssues);
|
|
284
|
+
const auditedSourceGroups = input.prepared.inputAudit.flatMap((item) => {
|
|
285
|
+
return input.prepared.filter?.sourceGroups === undefined ||
|
|
286
|
+
input.prepared.filter.sourceGroups.includes(item.record.sourceGroup)
|
|
287
|
+
? [item.record.sourceGroup]
|
|
288
|
+
: [];
|
|
289
|
+
});
|
|
290
|
+
const sourceGroups = new Set([
|
|
291
|
+
...auditedSourceGroups,
|
|
292
|
+
...input.prepared.cells.map((cell) => cell.sourceGroup),
|
|
293
|
+
]);
|
|
34
294
|
for (const [source, target] of Object.entries(input.groupMap ?? {})) {
|
|
35
|
-
if (!sourceGroups.has(source))
|
|
36
|
-
|
|
295
|
+
if (!sourceGroups.has(source))
|
|
296
|
+
throw new DiagnosticValidationError([
|
|
297
|
+
{
|
|
298
|
+
domain: "configuration",
|
|
299
|
+
code: "invalid-configuration",
|
|
300
|
+
path: `$.groupMap[${JSON.stringify(source)}]`,
|
|
301
|
+
message: "Group map contains an unused source group",
|
|
302
|
+
},
|
|
303
|
+
]);
|
|
304
|
+
if (!isDiagnosticToken(source))
|
|
305
|
+
throw new DiagnosticValidationError([
|
|
306
|
+
{
|
|
307
|
+
domain: "configuration",
|
|
308
|
+
code: "invalid-string",
|
|
309
|
+
path: `$.groupMap[${JSON.stringify(source)}]`,
|
|
310
|
+
message: "Source group must be a nonempty token",
|
|
311
|
+
},
|
|
312
|
+
]);
|
|
313
|
+
if (!isDiagnosticToken(target))
|
|
314
|
+
throw new DiagnosticValidationError([
|
|
315
|
+
{
|
|
316
|
+
domain: "configuration",
|
|
317
|
+
code: "invalid-string",
|
|
318
|
+
path: `$.groupMap[${JSON.stringify(source)}]`,
|
|
319
|
+
message: "Output group must be a nonempty token",
|
|
320
|
+
},
|
|
321
|
+
]);
|
|
37
322
|
}
|
|
38
|
-
const outputGroups = new Set(
|
|
39
|
-
|
|
40
|
-
|
|
323
|
+
const outputGroups = new Set(
|
|
324
|
+
[...sourceGroups].map((source) =>
|
|
325
|
+
input.groupMap !== undefined && hasDiagnosticOwn(input.groupMap, source)
|
|
326
|
+
? input.groupMap[source]!
|
|
327
|
+
: source,
|
|
328
|
+
),
|
|
329
|
+
);
|
|
330
|
+
for (const group of Object.keys(input.groupDimensions ?? {}))
|
|
331
|
+
if (!outputGroups.has(group))
|
|
332
|
+
throw new DiagnosticValidationError([
|
|
333
|
+
{
|
|
334
|
+
domain: "configuration",
|
|
335
|
+
code: "invalid-configuration",
|
|
336
|
+
path: `$.groupDimensions[${JSON.stringify(group)}]`,
|
|
337
|
+
message: "Dimensions contain an unused output group",
|
|
338
|
+
},
|
|
339
|
+
]);
|
|
340
|
+
for (const group of input.prepared.filter?.outputGroups ?? [])
|
|
341
|
+
if (!outputGroups.has(group))
|
|
342
|
+
throw new DiagnosticValidationError([
|
|
343
|
+
{
|
|
344
|
+
domain: "configuration",
|
|
345
|
+
code: "unknown-reference",
|
|
346
|
+
path: "$.prepared.filter.outputGroups",
|
|
347
|
+
message: `Unknown output group ${group}`,
|
|
348
|
+
},
|
|
349
|
+
]);
|
|
41
350
|
}
|
|
42
351
|
|
|
43
|
-
function mergeStats(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
352
|
+
function mergeStats(
|
|
353
|
+
cells: readonly PreparedDiagnosticDataContent["cells"][number][],
|
|
354
|
+
measure: { readonly id: string; readonly missing: "unknown" | "zero" },
|
|
355
|
+
): DiagnosticMeasureStats {
|
|
356
|
+
const contributions = cells.flatMap(
|
|
357
|
+
(cell) => cell.contributions[measure.id] ?? [],
|
|
358
|
+
);
|
|
359
|
+
const blockers = [
|
|
360
|
+
...new Map(
|
|
361
|
+
cells
|
|
362
|
+
.flatMap((cell) => cell.structuralBlockers[measure.id] ?? [])
|
|
363
|
+
.map((blocker) => [canonicalJson(blocker), blocker]),
|
|
364
|
+
).entries(),
|
|
365
|
+
]
|
|
366
|
+
.sort(([left], [right]) => codeUnit(left, right))
|
|
367
|
+
.map(([, blocker]) => blocker);
|
|
368
|
+
return finalizeDiagnosticContributions(
|
|
369
|
+
contributions,
|
|
370
|
+
measure.missing,
|
|
371
|
+
blockers,
|
|
372
|
+
);
|
|
50
373
|
}
|
|
51
374
|
|
|
52
|
-
function inferExpressionMeasure(
|
|
53
|
-
|
|
375
|
+
function inferExpressionMeasure(
|
|
376
|
+
expression: import("./diagnosticExpressions.js").DiagnosticMeasureExpression,
|
|
377
|
+
measures: ReadonlyMap<string, DiagnosticMeasureDefinition>,
|
|
378
|
+
): DiagnosticMeasureDefinition {
|
|
379
|
+
const id =
|
|
380
|
+
expression.op === "measure"
|
|
381
|
+
? expression.measureId
|
|
382
|
+
: expression.op === "add"
|
|
383
|
+
? inferExpressionMeasure(expression.terms[0]!, measures).id
|
|
384
|
+
: inferExpressionMeasure(expression.left, measures).id;
|
|
54
385
|
return measures.get(id)!;
|
|
55
386
|
}
|
|
56
387
|
|
|
57
|
-
function
|
|
388
|
+
function inferRoleExpressionMeasure(
|
|
389
|
+
expression: import("./diagnosticExpressions.js").DiagnosticRoleExpression,
|
|
390
|
+
bindings: Readonly<
|
|
391
|
+
Record<
|
|
392
|
+
string,
|
|
393
|
+
import("./diagnosticExpressions.js").DiagnosticMeasureExpression
|
|
394
|
+
>
|
|
395
|
+
>,
|
|
396
|
+
measures: ReadonlyMap<string, DiagnosticMeasureDefinition>,
|
|
397
|
+
): DiagnosticMeasureDefinition {
|
|
398
|
+
const role =
|
|
399
|
+
expression.op === "role"
|
|
400
|
+
? expression.role
|
|
401
|
+
: inferRoleExpressionMeasure(
|
|
402
|
+
expression.op === "add" ? expression.terms[0]! : expression.left,
|
|
403
|
+
bindings,
|
|
404
|
+
measures,
|
|
405
|
+
).id;
|
|
406
|
+
if (expression.op !== "role") return measures.get(role)!;
|
|
407
|
+
return inferExpressionMeasure(bindings[role]!, measures);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function evaluatePoint(
|
|
411
|
+
prepared: PreparedDiagnosticDataContent,
|
|
412
|
+
components: Readonly<Record<string, DiagnosticMeasureStats>>,
|
|
413
|
+
instanceId: string,
|
|
414
|
+
context: {
|
|
415
|
+
readonly group: string;
|
|
416
|
+
readonly origin: string;
|
|
417
|
+
readonly valuation: string;
|
|
418
|
+
readonly developmentAge: number;
|
|
419
|
+
readonly ageUnit: string;
|
|
420
|
+
},
|
|
421
|
+
preparedFindings: readonly DiagnosticMetricFinding[],
|
|
422
|
+
blockerFindingsByMeasure: Readonly<
|
|
423
|
+
Record<string, readonly DiagnosticMetricFinding[]>
|
|
424
|
+
>,
|
|
425
|
+
sourcesByMeasure: Readonly<
|
|
426
|
+
Record<
|
|
427
|
+
string,
|
|
428
|
+
readonly import("./diagnosticDefinitions.js").DiagnosticSourceLocation[]
|
|
429
|
+
>
|
|
430
|
+
>,
|
|
431
|
+
): DiagnosticMetricEvaluation {
|
|
58
432
|
const definition = prepared.definition;
|
|
59
433
|
const internals = getCompiledDiagnosticDefinitionInternals(definition);
|
|
60
|
-
const instance = definition.definition.instances.find(
|
|
61
|
-
|
|
62
|
-
|
|
434
|
+
const instance = definition.definition.instances.find(
|
|
435
|
+
(item) => item.id === instanceId,
|
|
436
|
+
)!;
|
|
437
|
+
const formula = definition.definition.formulas.find(
|
|
438
|
+
(item) => item.id === instance.formulaId,
|
|
439
|
+
)!;
|
|
440
|
+
const instanceIndex = definition.definition.instances.findIndex(
|
|
441
|
+
(item) => item.id === instance.id,
|
|
442
|
+
);
|
|
443
|
+
const formulaIndex = definition.definition.formulas.findIndex(
|
|
444
|
+
(item) => item.id === formula.id,
|
|
445
|
+
);
|
|
446
|
+
const measureStates: Record<string, FinalizedDiagnosticMeasure> =
|
|
447
|
+
Object.create(null);
|
|
63
448
|
for (const [measureId, stats] of Object.entries(components)) {
|
|
64
449
|
const measure = internals.measuresById.get(measureId)!;
|
|
65
|
-
const readiness = [
|
|
66
|
-
|
|
450
|
+
const readiness = [
|
|
451
|
+
...(stats.missing > 0
|
|
452
|
+
? [stats.imputedZero > 0 ? ("imputed" as const) : ("missing" as const)]
|
|
453
|
+
: []),
|
|
454
|
+
...(stats.nonFinite > 0 ? ["non-finite" as const] : []),
|
|
455
|
+
...(stats.structural > 0 ? ["structural-ambiguity" as const] : []),
|
|
456
|
+
...(stats.sum === null && stats.nonFinite === 0 && stats.structural === 0
|
|
457
|
+
? ["aggregation-overflow" as const]
|
|
458
|
+
: []),
|
|
459
|
+
];
|
|
460
|
+
measureStates[measureId] = {
|
|
461
|
+
quantity: quantity(measure, stats.value),
|
|
462
|
+
stats,
|
|
463
|
+
readiness,
|
|
464
|
+
sources: sourcesByMeasure[measureId] ?? [],
|
|
465
|
+
};
|
|
67
466
|
}
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
467
|
+
const expressionSources = (
|
|
468
|
+
expression: import("./diagnosticExpressions.js").DiagnosticMeasureExpression,
|
|
469
|
+
) =>
|
|
470
|
+
unionSources(
|
|
471
|
+
expressionMeasureIds(expression).flatMap(
|
|
472
|
+
(measureId) => sourcesByMeasure[measureId] ?? [],
|
|
473
|
+
),
|
|
474
|
+
);
|
|
475
|
+
const withOverflowSources = <
|
|
476
|
+
T extends {
|
|
477
|
+
readonly value: number | null;
|
|
478
|
+
readonly reasons: readonly (typeof READINESS_ORDER)[number][];
|
|
479
|
+
readonly overflows: readonly import("./diagnosticRules.js").DiagnosticExpressionOverflow[];
|
|
480
|
+
},
|
|
481
|
+
>(
|
|
482
|
+
result: T,
|
|
483
|
+
sources: readonly import("./diagnosticDefinitions.js").DiagnosticSourceLocation[],
|
|
484
|
+
) => ({
|
|
485
|
+
...result,
|
|
486
|
+
overflows: result.overflows.map((overflow) => ({
|
|
487
|
+
...overflow,
|
|
488
|
+
sources: overflow.sources.length > 0 ? overflow.sources : sources,
|
|
489
|
+
})),
|
|
490
|
+
});
|
|
491
|
+
const bindings = Object.fromEntries(
|
|
492
|
+
Object.entries(instance.bindings).map(([role, expression]) => {
|
|
493
|
+
const sources = expressionSources(expression);
|
|
494
|
+
const path = pointer(
|
|
495
|
+
pointer(pointer(pointer("", "instances"), instanceIndex), "bindings"),
|
|
496
|
+
role,
|
|
497
|
+
);
|
|
498
|
+
return [
|
|
499
|
+
role,
|
|
500
|
+
withOverflowSources(
|
|
501
|
+
evaluateDiagnosticMeasureExpression(expression, measureStates, path),
|
|
502
|
+
sources,
|
|
503
|
+
),
|
|
504
|
+
];
|
|
505
|
+
}),
|
|
506
|
+
);
|
|
507
|
+
const calculationSources = unionSources(
|
|
508
|
+
(
|
|
509
|
+
internals.calculationDependenciesByInstanceId.get(instance.id) ?? []
|
|
510
|
+
).flatMap((measureId) => sourcesByMeasure[measureId] ?? []),
|
|
511
|
+
);
|
|
512
|
+
const roleExpressionSources = (
|
|
513
|
+
expression: import("./diagnosticExpressions.js").DiagnosticRoleExpression,
|
|
514
|
+
): import("./diagnosticDefinitions.js").DiagnosticSourceLocation[] => {
|
|
515
|
+
if (expression.op === "role")
|
|
516
|
+
return expressionSources(instance.bindings[expression.role]!);
|
|
517
|
+
return unionSources(
|
|
518
|
+
(expression.op === "add"
|
|
519
|
+
? expression.terms
|
|
520
|
+
: [expression.left, expression.right]
|
|
521
|
+
).flatMap(roleExpressionSources),
|
|
522
|
+
);
|
|
523
|
+
};
|
|
524
|
+
const numeratorSources = roleExpressionSources(formula.numerator);
|
|
525
|
+
const denominatorSources = roleExpressionSources(formula.denominator);
|
|
526
|
+
const formulaPath = pointer(pointer("", "formulas"), formulaIndex);
|
|
527
|
+
const numeratorResult = withOverflowSources(
|
|
528
|
+
evaluateDiagnosticRoleExpression(
|
|
529
|
+
formula.numerator,
|
|
530
|
+
bindings,
|
|
531
|
+
pointer(formulaPath, "numerator"),
|
|
532
|
+
),
|
|
533
|
+
numeratorSources,
|
|
534
|
+
);
|
|
535
|
+
const denominatorResult = withOverflowSources(
|
|
536
|
+
evaluateDiagnosticRoleExpression(
|
|
537
|
+
formula.denominator,
|
|
538
|
+
bindings,
|
|
539
|
+
pointer(formulaPath, "denominator"),
|
|
540
|
+
),
|
|
541
|
+
denominatorSources,
|
|
542
|
+
);
|
|
543
|
+
const raw = diagnosticRawRatio(
|
|
544
|
+
numeratorResult.value,
|
|
545
|
+
denominatorResult.value,
|
|
546
|
+
);
|
|
72
547
|
const presented = applyDiagnosticPresentation(raw, instance.presentation);
|
|
73
|
-
const numeratorMeasure =
|
|
74
|
-
|
|
548
|
+
const numeratorMeasure = inferRoleExpressionMeasure(
|
|
549
|
+
formula.numerator,
|
|
550
|
+
instance.bindings,
|
|
551
|
+
internals.measuresById,
|
|
552
|
+
);
|
|
553
|
+
const denominatorMeasure = inferRoleExpressionMeasure(
|
|
554
|
+
formula.denominator,
|
|
555
|
+
instance.bindings,
|
|
556
|
+
internals.measuresById,
|
|
557
|
+
);
|
|
75
558
|
const ruleEvaluations: DiagnosticRuleEvaluation[] = [];
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
559
|
+
const deps =
|
|
560
|
+
internals.evaluationDependenciesByInstanceId.get(instance.id) ?? [];
|
|
561
|
+
const findingContext = {
|
|
562
|
+
instanceId: instance.id,
|
|
563
|
+
...context,
|
|
564
|
+
sources: calculationSources,
|
|
565
|
+
};
|
|
566
|
+
const findings: DiagnosticMetricFinding[] = mergeFindings([
|
|
567
|
+
...preparedFindings.filter(
|
|
568
|
+
(finding) =>
|
|
569
|
+
finding.category !== "structural" &&
|
|
570
|
+
(finding.measureId === undefined || deps.includes(finding.measureId)),
|
|
571
|
+
),
|
|
572
|
+
...deps.flatMap((measureId) => blockerFindingsByMeasure[measureId] ?? []),
|
|
573
|
+
]);
|
|
574
|
+
for (const overflow of [
|
|
575
|
+
...numeratorResult.overflows,
|
|
576
|
+
...denominatorResult.overflows,
|
|
577
|
+
])
|
|
578
|
+
findings.push({
|
|
579
|
+
code: "diagnostic-expression-overflow",
|
|
580
|
+
message: "Measure expression overflowed",
|
|
581
|
+
severity: "fail",
|
|
582
|
+
category: "aggregation",
|
|
583
|
+
expressionPath: overflow.expressionPath,
|
|
584
|
+
...findingContext,
|
|
585
|
+
sources: overflow.sources,
|
|
586
|
+
});
|
|
587
|
+
if (numeratorResult.value === null)
|
|
588
|
+
findings.push({
|
|
589
|
+
code: "diagnostic-numerator-unavailable",
|
|
590
|
+
message: "Calculation numerator is unavailable",
|
|
591
|
+
severity: "warning",
|
|
592
|
+
category: "calculation",
|
|
593
|
+
...findingContext,
|
|
594
|
+
});
|
|
595
|
+
if (denominatorResult.value === null)
|
|
596
|
+
findings.push({
|
|
597
|
+
code: "diagnostic-denominator-unavailable",
|
|
598
|
+
message: "Calculation denominator is unavailable",
|
|
599
|
+
severity: "warning",
|
|
600
|
+
category: "calculation",
|
|
601
|
+
...findingContext,
|
|
602
|
+
});
|
|
603
|
+
else if (denominatorResult.value <= 0)
|
|
604
|
+
findings.push({
|
|
605
|
+
code: "diagnostic-denominator-not-positive",
|
|
606
|
+
message: "Calculation denominator is not strictly positive",
|
|
607
|
+
severity: "warning",
|
|
608
|
+
category: "calculation",
|
|
609
|
+
...findingContext,
|
|
610
|
+
});
|
|
611
|
+
else if (numeratorResult.value !== null && raw === null)
|
|
612
|
+
findings.push({
|
|
613
|
+
code: "diagnostic-calculation-overflow",
|
|
614
|
+
message: "Calculation result overflowed",
|
|
615
|
+
severity: "fail",
|
|
616
|
+
category: "calculation",
|
|
617
|
+
...findingContext,
|
|
618
|
+
});
|
|
619
|
+
if (presented.finding)
|
|
620
|
+
findings.push({ ...presented.finding, ...findingContext });
|
|
621
|
+
type Operand = {
|
|
622
|
+
readonly value: number | null;
|
|
623
|
+
readonly reasons: readonly (typeof READINESS_ORDER)[number][];
|
|
624
|
+
readonly overflows: readonly import("./diagnosticRules.js").DiagnosticExpressionOverflow[];
|
|
625
|
+
readonly sources: readonly import("./diagnosticDefinitions.js").DiagnosticSourceLocation[];
|
|
626
|
+
};
|
|
627
|
+
const operand = (
|
|
628
|
+
item: (typeof instance.rules)[number]["when"]["left"],
|
|
629
|
+
path: string,
|
|
630
|
+
): Operand => {
|
|
631
|
+
if (item.source === "constant")
|
|
632
|
+
return { value: item.value, reasons: [], overflows: [], sources: [] };
|
|
633
|
+
if (item.source === "calculation") {
|
|
634
|
+
const result =
|
|
635
|
+
item.field === "numerator" ? numeratorResult : denominatorResult;
|
|
636
|
+
return {
|
|
637
|
+
...result,
|
|
638
|
+
sources:
|
|
639
|
+
item.field === "numerator" ? numeratorSources : denominatorSources,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
const sources = expressionSources(item.expression);
|
|
643
|
+
return {
|
|
644
|
+
...withOverflowSources(
|
|
645
|
+
evaluateDiagnosticMeasureExpression(
|
|
646
|
+
item.expression,
|
|
647
|
+
measureStates,
|
|
648
|
+
pointer(path, "expression"),
|
|
649
|
+
),
|
|
650
|
+
sources,
|
|
651
|
+
),
|
|
652
|
+
sources,
|
|
653
|
+
};
|
|
654
|
+
};
|
|
655
|
+
for (const [ruleIndex, rule] of instance.rules.entries()) {
|
|
656
|
+
const rulePath = pointer(
|
|
657
|
+
pointer(
|
|
658
|
+
pointer(pointer(pointer("", "instances"), instanceIndex), "rules"),
|
|
659
|
+
ruleIndex,
|
|
660
|
+
),
|
|
661
|
+
"when",
|
|
662
|
+
);
|
|
663
|
+
const left = operand(rule.when.left, pointer(rulePath, "left"));
|
|
664
|
+
const right = operand(rule.when.right, pointer(rulePath, "right"));
|
|
665
|
+
const ruleSources = unionSources([...left.sources, ...right.sources]);
|
|
666
|
+
const expressionOverflows = [
|
|
667
|
+
...new Map(
|
|
668
|
+
[...left.overflows, ...right.overflows].map((overflow) => [
|
|
669
|
+
canonicalJson(overflow),
|
|
670
|
+
overflow,
|
|
671
|
+
]),
|
|
672
|
+
).entries(),
|
|
673
|
+
]
|
|
674
|
+
.sort(([a], [b]) => codeUnit(a, b))
|
|
675
|
+
.map(([, overflow]) => overflow);
|
|
676
|
+
const reasons = readiness([
|
|
677
|
+
...left.reasons,
|
|
678
|
+
...right.reasons,
|
|
679
|
+
...(expressionOverflows.length > 0
|
|
680
|
+
? ["expression-overflow" as const]
|
|
681
|
+
: []),
|
|
682
|
+
]);
|
|
683
|
+
const classified =
|
|
684
|
+
reasons.length > 0
|
|
685
|
+
? null
|
|
686
|
+
: classifyDiagnosticComparison(
|
|
687
|
+
left.value,
|
|
688
|
+
right.value,
|
|
689
|
+
rule.when.tolerance,
|
|
690
|
+
);
|
|
691
|
+
if (classified === null || classified.status === "not-evaluated") {
|
|
692
|
+
const notEvaluatedReasons = readiness([
|
|
693
|
+
...reasons,
|
|
694
|
+
...(classified?.status === "not-evaluated" ? [classified.reason] : []),
|
|
695
|
+
...((left.value === null || right.value === null) &&
|
|
696
|
+
reasons.length === 0
|
|
697
|
+
? ["missing" as const]
|
|
698
|
+
: []),
|
|
699
|
+
]);
|
|
700
|
+
ruleEvaluations.push({
|
|
701
|
+
ruleId: rule.id,
|
|
702
|
+
status: "not-evaluated",
|
|
703
|
+
severity: rule.severity,
|
|
704
|
+
left: left.value,
|
|
705
|
+
right: right.value,
|
|
706
|
+
relation: null,
|
|
707
|
+
notEvaluatedReasons,
|
|
708
|
+
expressionOverflows,
|
|
709
|
+
code: "diagnostic-rule-not-evaluated",
|
|
710
|
+
message: "Diagnostic metric rule was not evaluated",
|
|
711
|
+
});
|
|
712
|
+
for (const overflow of expressionOverflows)
|
|
713
|
+
findings.push({
|
|
714
|
+
code: "diagnostic-expression-overflow",
|
|
715
|
+
message: "Measure expression overflowed",
|
|
716
|
+
severity: "fail",
|
|
717
|
+
category: "aggregation",
|
|
718
|
+
ruleId: rule.id,
|
|
719
|
+
expressionPath: overflow.expressionPath,
|
|
720
|
+
...findingContext,
|
|
721
|
+
sources: overflow.sources,
|
|
722
|
+
});
|
|
723
|
+
findings.push({
|
|
724
|
+
code: "diagnostic-rule-not-evaluated",
|
|
725
|
+
message: "Diagnostic metric rule was not evaluated",
|
|
726
|
+
severity: "info",
|
|
727
|
+
category: "rule",
|
|
728
|
+
ruleId: rule.id,
|
|
729
|
+
...findingContext,
|
|
730
|
+
sources: ruleSources,
|
|
731
|
+
});
|
|
732
|
+
continue;
|
|
85
733
|
}
|
|
734
|
+
const triggered = diagnosticPredicateMatches(
|
|
735
|
+
rule.when.operator,
|
|
736
|
+
classified.relation,
|
|
737
|
+
);
|
|
738
|
+
ruleEvaluations.push({
|
|
739
|
+
ruleId: rule.id,
|
|
740
|
+
status: triggered ? "triggered" : "pass",
|
|
741
|
+
severity: rule.severity,
|
|
742
|
+
left: left.value,
|
|
743
|
+
right: right.value,
|
|
744
|
+
relation: classified.relation,
|
|
745
|
+
notEvaluatedReasons: [],
|
|
746
|
+
expressionOverflows: [],
|
|
747
|
+
code: triggered ? rule.code : null,
|
|
748
|
+
message: triggered ? rule.message : null,
|
|
749
|
+
});
|
|
750
|
+
if (triggered)
|
|
751
|
+
findings.push({
|
|
752
|
+
code: rule.code,
|
|
753
|
+
message: rule.message,
|
|
754
|
+
severity: rule.severity,
|
|
755
|
+
category: "rule",
|
|
756
|
+
ruleId: rule.id,
|
|
757
|
+
...findingContext,
|
|
758
|
+
sources: ruleSources,
|
|
759
|
+
});
|
|
86
760
|
}
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
761
|
+
const refs = {
|
|
762
|
+
amountBasisIds: [
|
|
763
|
+
...new Set(
|
|
764
|
+
deps.flatMap((id) =>
|
|
765
|
+
internals.measuresById.get(id)?.basisId
|
|
766
|
+
? [internals.measuresById.get(id)!.basisId!]
|
|
767
|
+
: [],
|
|
768
|
+
),
|
|
769
|
+
),
|
|
770
|
+
].sort(codeUnit),
|
|
771
|
+
countPopulationIds: [
|
|
772
|
+
...new Set(
|
|
773
|
+
deps.flatMap((id) =>
|
|
774
|
+
internals.measuresById.get(id)?.countPopulationId
|
|
775
|
+
? [internals.measuresById.get(id)!.countPopulationId!]
|
|
776
|
+
: [],
|
|
777
|
+
),
|
|
778
|
+
),
|
|
779
|
+
].sort(codeUnit),
|
|
780
|
+
exposureBasisIds: [
|
|
781
|
+
...new Set(
|
|
782
|
+
deps.flatMap((id) =>
|
|
783
|
+
internals.measuresById.get(id)?.exposureBasisId
|
|
784
|
+
? [internals.measuresById.get(id)!.exposureBasisId!]
|
|
785
|
+
: [],
|
|
786
|
+
),
|
|
787
|
+
),
|
|
788
|
+
].sort(codeUnit),
|
|
789
|
+
};
|
|
790
|
+
return deepFreeze({
|
|
791
|
+
instanceId: instance.id,
|
|
792
|
+
instanceVersion: instance.version,
|
|
793
|
+
formulaId: formula.id,
|
|
794
|
+
formulaVersion: formula.version,
|
|
795
|
+
semanticReferences: refs,
|
|
796
|
+
formulaFingerprint: definition.formulaFingerprints[formula.id]!,
|
|
797
|
+
calculationFingerprint: definition.calculationFingerprints[instance.id]!,
|
|
798
|
+
definitionIntegrity: definition.definitionIntegrity,
|
|
799
|
+
calculation: {
|
|
800
|
+
numerator: quantity(numeratorMeasure, numeratorResult.value),
|
|
801
|
+
denominator: quantity(denominatorMeasure, denominatorResult.value),
|
|
802
|
+
value: raw,
|
|
803
|
+
},
|
|
804
|
+
presentation: { ...instance.presentation, value: presented.value },
|
|
805
|
+
components: Object.fromEntries(deps.map((id) => [id, components[id]!])),
|
|
806
|
+
rules: ruleEvaluations,
|
|
807
|
+
findings: mergeFindings(findings),
|
|
808
|
+
});
|
|
90
809
|
}
|
|
91
810
|
|
|
92
|
-
export function runMetricDiagnostics(
|
|
811
|
+
export function runMetricDiagnostics(
|
|
812
|
+
input: RunMetricDiagnosticsInput,
|
|
813
|
+
): DiagnosticDeepReadonly<MetricDiagnosticsResult> {
|
|
93
814
|
validateDiagnosticGroupingConfiguration(input);
|
|
815
|
+
const { definitionIntegrity, ...content } = calculateMetricDiagnostics(input);
|
|
816
|
+
return Object.freeze({
|
|
817
|
+
definitionIntegrity,
|
|
818
|
+
preparationFingerprint: input.prepared.preparationFingerprint,
|
|
819
|
+
...content,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
export function runMetricDiagnosticsCompact(
|
|
824
|
+
input: RunMetricDiagnosticsCompactInput,
|
|
825
|
+
): DiagnosticDeepReadonly<CompactMetricDiagnosticsResult> {
|
|
826
|
+
validateCompactDiagnosticGroupingConfiguration(input);
|
|
827
|
+
const result = calculateMetricDiagnostics(input);
|
|
828
|
+
compactResultPreparations.set(result, input.prepared);
|
|
829
|
+
return result as DiagnosticDeepReadonly<CompactMetricDiagnosticsResult>;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
export function assertCompactMetricDiagnosticsResult(
|
|
833
|
+
value: unknown,
|
|
834
|
+
): asserts value is DiagnosticDeepReadonly<CompactMetricDiagnosticsResult> {
|
|
835
|
+
if (
|
|
836
|
+
value === null ||
|
|
837
|
+
typeof value !== "object" ||
|
|
838
|
+
!compactResultPreparations.has(value)
|
|
839
|
+
)
|
|
840
|
+
throw new DiagnosticValidationError([
|
|
841
|
+
{
|
|
842
|
+
domain: "input",
|
|
843
|
+
code: "invalid-input-relationship",
|
|
844
|
+
path: "$.result",
|
|
845
|
+
message: "Compact metric diagnostics result is not authentic",
|
|
846
|
+
},
|
|
847
|
+
]);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/** Explicit eager JSON bridge; it is not used during compact calculation. */
|
|
851
|
+
export function materializeMetricDiagnosticsResult(
|
|
852
|
+
value: DiagnosticDeepReadonly<CompactMetricDiagnosticsResult>,
|
|
853
|
+
): DiagnosticDeepReadonly<MetricDiagnosticsResult> {
|
|
854
|
+
assertCompactMetricDiagnosticsResult(value);
|
|
855
|
+
const preparationFingerprint = getCompactPreparedDiagnosticDataFingerprint(
|
|
856
|
+
compactResultPreparations.get(value)!,
|
|
857
|
+
);
|
|
858
|
+
const { definitionIntegrity, ...content } = value;
|
|
859
|
+
return Object.freeze({
|
|
860
|
+
definitionIntegrity,
|
|
861
|
+
preparationFingerprint,
|
|
862
|
+
...content,
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/** Complete core-owned result identity; no normalized result copy is retained. */
|
|
867
|
+
export function getCompactMetricDiagnosticsResultIdentityDocument(
|
|
868
|
+
value: DiagnosticDeepReadonly<CompactMetricDiagnosticsResult>,
|
|
869
|
+
): DiagnosticIdentityDocument {
|
|
870
|
+
assertCompactMetricDiagnosticsResult(value);
|
|
871
|
+
const previous = compactResultDocuments.get(value);
|
|
872
|
+
if (previous) return previous;
|
|
873
|
+
const document = createDiagnosticIdentityObject({
|
|
874
|
+
definitionIntegrity: createDiagnosticIdentityValue(
|
|
875
|
+
value.definitionIntegrity,
|
|
876
|
+
),
|
|
877
|
+
preparationFingerprint: createDiagnosticIdentityValue(
|
|
878
|
+
getCompactPreparedDiagnosticDataFingerprint(
|
|
879
|
+
compactResultPreparations.get(value)!,
|
|
880
|
+
),
|
|
881
|
+
),
|
|
882
|
+
ageUnit: createDiagnosticIdentityValue(value.ageUnit),
|
|
883
|
+
emergence: createDiagnosticIdentityValue(value.emergence),
|
|
884
|
+
triangles: createDiagnosticIdentityValue(value.triangles),
|
|
885
|
+
latestDiagonal: createDiagnosticIdentityValue(value.latestDiagonal),
|
|
886
|
+
findings: createDiagnosticIdentityValue(value.findings),
|
|
887
|
+
});
|
|
888
|
+
compactResultDocuments.set(value, document);
|
|
889
|
+
return document;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function calculateMetricDiagnostics(
|
|
893
|
+
input: RunMetricDiagnosticsContentInput,
|
|
894
|
+
): DiagnosticDeepReadonly<MetricDiagnosticsResultContent> {
|
|
94
895
|
const prepared = input.prepared;
|
|
896
|
+
// Grouping validation has already rejected non-JSON shapes. Snapshot each
|
|
897
|
+
// group's opaque metadata once; freezing output must not freeze caller JSON.
|
|
898
|
+
const dimensionSnapshots = new WeakMap<object, JsonValue>();
|
|
899
|
+
const dimensions = new Map(
|
|
900
|
+
Object.entries(input.groupDimensions ?? {}).map(([group, value]) => [
|
|
901
|
+
group,
|
|
902
|
+
snapshotOwnedDimensions(value, dimensionSnapshots),
|
|
903
|
+
]),
|
|
904
|
+
);
|
|
95
905
|
const buckets = new Map<string, typeof prepared.cells>();
|
|
96
|
-
for (const cell of prepared.cells) {
|
|
97
|
-
|
|
906
|
+
for (const cell of prepared.cells) {
|
|
907
|
+
const group =
|
|
908
|
+
input.groupMap !== undefined &&
|
|
909
|
+
hasDiagnosticOwn(input.groupMap, cell.sourceGroup)
|
|
910
|
+
? input.groupMap[cell.sourceGroup]!
|
|
911
|
+
: cell.sourceGroup;
|
|
912
|
+
if (
|
|
913
|
+
prepared.filter?.outputGroups !== undefined &&
|
|
914
|
+
!prepared.filter.outputGroups.includes(group)
|
|
915
|
+
)
|
|
916
|
+
continue;
|
|
917
|
+
const key = canonicalJson([group, cell.origin, cell.valuation]);
|
|
918
|
+
buckets.set(key, [...(buckets.get(key) ?? []), cell]);
|
|
919
|
+
}
|
|
920
|
+
const selectedInstances = prepared.definition.definition.instances.filter(
|
|
921
|
+
(instance) =>
|
|
922
|
+
prepared.filter?.instanceIds === undefined ||
|
|
923
|
+
prepared.filter.instanceIds.includes(instance.id),
|
|
924
|
+
);
|
|
98
925
|
const emergence: DiagnosticEmergencePoint[] = [];
|
|
99
|
-
for (const [key, cells] of buckets) {
|
|
100
|
-
|
|
101
|
-
|
|
926
|
+
for (const [key, cells] of buckets) {
|
|
927
|
+
const [group, origin, valuation] = JSON.parse(key) as [
|
|
928
|
+
string,
|
|
929
|
+
string,
|
|
930
|
+
string,
|
|
931
|
+
];
|
|
932
|
+
const components = Object.fromEntries(
|
|
933
|
+
prepared.definition.definition.measures.map((measure) => [
|
|
934
|
+
measure.id,
|
|
935
|
+
mergeStats(cells, measure),
|
|
936
|
+
]),
|
|
937
|
+
);
|
|
938
|
+
const sourcesByMeasure = Object.fromEntries(
|
|
939
|
+
prepared.definition.definition.measures.map((measure) => [
|
|
940
|
+
measure.id,
|
|
941
|
+
unionSources(
|
|
942
|
+
cells.flatMap((cell) => [
|
|
943
|
+
...(cell.contributions[measure.id] ?? []).flatMap(
|
|
944
|
+
(item) => item.sources,
|
|
945
|
+
),
|
|
946
|
+
...(cell.structuralBlockers[measure.id] ?? []).flatMap(
|
|
947
|
+
(item) => item.sources,
|
|
948
|
+
),
|
|
949
|
+
]),
|
|
950
|
+
),
|
|
951
|
+
]),
|
|
952
|
+
);
|
|
953
|
+
const context = {
|
|
954
|
+
group,
|
|
955
|
+
origin,
|
|
956
|
+
valuation,
|
|
957
|
+
developmentAge: cells[0]!.developmentAge,
|
|
958
|
+
ageUnit: cells[0]!.ageUnit,
|
|
959
|
+
};
|
|
960
|
+
const mappedOverflowFindings =
|
|
961
|
+
prepared.definition.definition.measures.flatMap(
|
|
962
|
+
(measure): DiagnosticMetricFinding[] => {
|
|
963
|
+
const stats = components[measure.id]!;
|
|
964
|
+
return stats.sum === null &&
|
|
965
|
+
stats.nonFinite === 0 &&
|
|
966
|
+
stats.structural === 0
|
|
967
|
+
? [
|
|
968
|
+
{
|
|
969
|
+
code: "diagnostic-measure-overflow",
|
|
970
|
+
message: "Measure aggregation overflowed",
|
|
971
|
+
severity: "fail",
|
|
972
|
+
category: "aggregation",
|
|
973
|
+
measureId: measure.id,
|
|
974
|
+
...context,
|
|
975
|
+
sources: sourcesByMeasure[measure.id] ?? [],
|
|
976
|
+
},
|
|
977
|
+
]
|
|
978
|
+
: [];
|
|
979
|
+
},
|
|
980
|
+
);
|
|
981
|
+
const mappedDeduplicationFindings =
|
|
982
|
+
prepared.definition.definition.measures.flatMap(
|
|
983
|
+
(measure): DiagnosticMetricFinding[] => {
|
|
984
|
+
const stats = components[measure.id]!;
|
|
985
|
+
return stats.deduplicated > 0
|
|
986
|
+
? [
|
|
987
|
+
{
|
|
988
|
+
code: "diagnostic-exposure-deduplicated",
|
|
989
|
+
message: `${stats.deduplicated} repeated exposure observation(s) were counted once by stable key`,
|
|
990
|
+
severity: "info",
|
|
991
|
+
category: "aggregation",
|
|
992
|
+
measureId: measure.id,
|
|
993
|
+
...context,
|
|
994
|
+
sources: sourcesByMeasure[measure.id] ?? [],
|
|
995
|
+
},
|
|
996
|
+
]
|
|
997
|
+
: [];
|
|
998
|
+
},
|
|
999
|
+
);
|
|
1000
|
+
const mappedFindings = mergeFindings([
|
|
1001
|
+
...cells.flatMap((cell) =>
|
|
1002
|
+
cell.findings.map((finding) => ({ ...finding, group })),
|
|
1003
|
+
),
|
|
1004
|
+
...mappedOverflowFindings,
|
|
1005
|
+
...mappedDeduplicationFindings,
|
|
1006
|
+
]);
|
|
1007
|
+
const blockerFindingsByMeasure = Object.fromEntries(
|
|
1008
|
+
prepared.definition.definition.measures.map((measure) => [
|
|
1009
|
+
measure.id,
|
|
1010
|
+
mergeFindings(
|
|
1011
|
+
cells.flatMap((cell) =>
|
|
1012
|
+
(cell.structuralBlockers[measure.id] ?? []).flatMap((blocker) =>
|
|
1013
|
+
blocker.finding ? [{ ...blocker.finding, group }] : [],
|
|
1014
|
+
),
|
|
1015
|
+
),
|
|
1016
|
+
),
|
|
1017
|
+
]),
|
|
1018
|
+
);
|
|
1019
|
+
const metrics = Object.fromEntries(
|
|
1020
|
+
selectedInstances.map((instance) => [
|
|
1021
|
+
instance.id,
|
|
1022
|
+
evaluatePoint(
|
|
1023
|
+
prepared,
|
|
1024
|
+
components,
|
|
1025
|
+
instance.id,
|
|
1026
|
+
context,
|
|
1027
|
+
mappedFindings,
|
|
1028
|
+
blockerFindingsByMeasure,
|
|
1029
|
+
sourcesByMeasure,
|
|
1030
|
+
),
|
|
1031
|
+
]),
|
|
1032
|
+
);
|
|
1033
|
+
emergence.push({
|
|
1034
|
+
group,
|
|
1035
|
+
sourceGroups: [...new Set(cells.map((cell) => cell.sourceGroup))].sort(
|
|
1036
|
+
codeUnit,
|
|
1037
|
+
),
|
|
1038
|
+
...(dimensions.has(group) ? { dimensions: dimensions.get(group)! } : {}),
|
|
1039
|
+
origin,
|
|
1040
|
+
valuation,
|
|
1041
|
+
developmentAge: cells[0]!.developmentAge,
|
|
1042
|
+
ageUnit: cells[0]!.ageUnit,
|
|
1043
|
+
components,
|
|
1044
|
+
metrics,
|
|
1045
|
+
findings: mergeFindings([
|
|
1046
|
+
...mappedFindings,
|
|
1047
|
+
...Object.values(metrics).flatMap((metric) => metric.findings),
|
|
1048
|
+
]),
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
const periodCoordinate = (side: "origin" | "valuation", label: string) =>
|
|
1052
|
+
normalizeDiagnosticPeriod(prepared.definition, side, label).coordinate;
|
|
1053
|
+
emergence.sort(
|
|
1054
|
+
(a, b) =>
|
|
1055
|
+
codeUnit(a.group, b.group) ||
|
|
1056
|
+
periodCoordinate("origin", a.origin) -
|
|
1057
|
+
periodCoordinate("origin", b.origin) ||
|
|
1058
|
+
periodCoordinate("valuation", a.valuation) -
|
|
1059
|
+
periodCoordinate("valuation", b.valuation) ||
|
|
1060
|
+
codeUnit(a.origin, b.origin) ||
|
|
1061
|
+
codeUnit(a.valuation, b.valuation),
|
|
1062
|
+
);
|
|
1063
|
+
const groups = [...new Set(emergence.map((point) => point.group))].sort(
|
|
1064
|
+
codeUnit,
|
|
1065
|
+
);
|
|
102
1066
|
const triangles: DiagnosticMetricTriangle[] = [];
|
|
103
|
-
for (const group of groups)
|
|
104
|
-
|
|
105
|
-
|
|
1067
|
+
for (const group of groups)
|
|
1068
|
+
for (const instance of selectedInstances) {
|
|
1069
|
+
const points = emergence.filter((point) => point.group === group);
|
|
1070
|
+
const origins = [...new Set(points.map((point) => point.origin))].sort(
|
|
1071
|
+
(a, b) =>
|
|
1072
|
+
periodCoordinate("origin", a) - periodCoordinate("origin", b) ||
|
|
1073
|
+
codeUnit(a, b),
|
|
1074
|
+
);
|
|
1075
|
+
const ages = [
|
|
1076
|
+
...new Set(points.map((point) => point.developmentAge)),
|
|
1077
|
+
].sort((a, b) => a - b);
|
|
1078
|
+
const cells = origins.map((origin) =>
|
|
1079
|
+
ages.map((age) => {
|
|
1080
|
+
const point = points.find(
|
|
1081
|
+
(item) => item.origin === origin && item.developmentAge === age,
|
|
1082
|
+
);
|
|
1083
|
+
return point
|
|
1084
|
+
? {
|
|
1085
|
+
origin: point.origin,
|
|
1086
|
+
valuation: point.valuation,
|
|
1087
|
+
developmentAge: point.developmentAge,
|
|
1088
|
+
ageUnit: point.ageUnit,
|
|
1089
|
+
evaluation: point.metrics[instance.id]!,
|
|
1090
|
+
}
|
|
1091
|
+
: null;
|
|
1092
|
+
}),
|
|
1093
|
+
);
|
|
1094
|
+
triangles.push({
|
|
1095
|
+
group,
|
|
1096
|
+
instanceId: instance.id,
|
|
1097
|
+
origins,
|
|
1098
|
+
developmentAges: ages,
|
|
1099
|
+
ageUnit: prepared.definition.definition.periodAxis.ageUnit,
|
|
1100
|
+
calculationValues: cells.map((row) =>
|
|
1101
|
+
row.map((cell) => cell?.evaluation.calculation.value ?? null),
|
|
1102
|
+
),
|
|
1103
|
+
presentationValues: cells.map((row) =>
|
|
1104
|
+
row.map((cell) => cell?.evaluation.presentation.value ?? null),
|
|
1105
|
+
),
|
|
1106
|
+
cells,
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
const latestDiagonal = groups.flatMap((group) => {
|
|
1110
|
+
const groupPoints = emergence.filter((point) => point.group === group);
|
|
1111
|
+
return [...new Set(groupPoints.map((point) => point.origin))].flatMap(
|
|
1112
|
+
(origin) => {
|
|
1113
|
+
const points = groupPoints.filter((point) => point.origin === origin);
|
|
1114
|
+
return points.length
|
|
1115
|
+
? [
|
|
1116
|
+
points.reduce((latest, point) =>
|
|
1117
|
+
point.developmentAge > latest.developmentAge ? point : latest,
|
|
1118
|
+
),
|
|
1119
|
+
]
|
|
1120
|
+
: [];
|
|
1121
|
+
},
|
|
1122
|
+
);
|
|
1123
|
+
});
|
|
1124
|
+
const attachedPreparationFindings = new Set(
|
|
1125
|
+
prepared.cells
|
|
1126
|
+
.flatMap((cell) => cell.findings)
|
|
1127
|
+
.map((finding) => canonicalJson(finding)),
|
|
1128
|
+
);
|
|
1129
|
+
const unattachedPreparationFindings = prepared.findings.filter(
|
|
1130
|
+
(finding) => !attachedPreparationFindings.has(canonicalJson(finding)),
|
|
1131
|
+
);
|
|
1132
|
+
return deepFreeze({
|
|
1133
|
+
definitionIntegrity: prepared.definition.definitionIntegrity,
|
|
1134
|
+
ageUnit: prepared.definition.definition.periodAxis.ageUnit,
|
|
1135
|
+
emergence,
|
|
1136
|
+
triangles,
|
|
1137
|
+
latestDiagonal,
|
|
1138
|
+
findings: mergeFindings([
|
|
1139
|
+
...unattachedPreparationFindings,
|
|
1140
|
+
...emergence.flatMap((point) => point.findings),
|
|
1141
|
+
]),
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/** Preserve free-JSON prototypes, own-key order, and -0 without source coercion. */
|
|
1146
|
+
function snapshotOwnedDimensions(
|
|
1147
|
+
value: JsonValue,
|
|
1148
|
+
snapshots: WeakMap<object, JsonValue>,
|
|
1149
|
+
): JsonValue {
|
|
1150
|
+
if (value === null || typeof value !== "object") return value;
|
|
1151
|
+
const previous = snapshots.get(value);
|
|
1152
|
+
if (previous !== undefined) return previous;
|
|
1153
|
+
if (Array.isArray(value)) {
|
|
1154
|
+
const result = Object.freeze(
|
|
1155
|
+
value.map((item) => snapshotOwnedDimensions(item, snapshots)),
|
|
1156
|
+
);
|
|
1157
|
+
snapshots.set(value, result);
|
|
1158
|
+
return result;
|
|
1159
|
+
}
|
|
1160
|
+
const result = Object.create(Object.getPrototypeOf(value)) as Record<
|
|
1161
|
+
string,
|
|
1162
|
+
JsonValue
|
|
1163
|
+
>;
|
|
1164
|
+
const record = value as Readonly<Record<string, JsonValue>>;
|
|
1165
|
+
for (const key of Object.keys(value))
|
|
1166
|
+
Object.defineProperty(result, key, {
|
|
1167
|
+
value: snapshotOwnedDimensions(record[key]!, snapshots),
|
|
1168
|
+
enumerable: true,
|
|
1169
|
+
configurable: true,
|
|
1170
|
+
writable: true,
|
|
1171
|
+
});
|
|
1172
|
+
const frozen = Object.freeze(result);
|
|
1173
|
+
snapshots.set(value, frozen);
|
|
1174
|
+
return frozen;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function viewGroups(
|
|
1178
|
+
result: DiagnosticDeepReadonly<MetricDiagnosticsResultContent>,
|
|
1179
|
+
outputGroups: readonly string[],
|
|
1180
|
+
path: string,
|
|
1181
|
+
): readonly string[] {
|
|
1182
|
+
if (!Array.isArray(outputGroups))
|
|
1183
|
+
throw new DiagnosticValidationError([
|
|
1184
|
+
{
|
|
1185
|
+
domain: "view",
|
|
1186
|
+
code: "invalid-type",
|
|
1187
|
+
path,
|
|
1188
|
+
message: "Output groups must be an array",
|
|
1189
|
+
},
|
|
1190
|
+
]);
|
|
1191
|
+
const produced = new Set(result.emergence.map((point) => point.group));
|
|
1192
|
+
const issues: DiagnosticValidationIssue[] = [];
|
|
1193
|
+
outputGroups.forEach((group, index) => {
|
|
1194
|
+
const itemPath = `${path}[${index}]`;
|
|
1195
|
+
if (!isDiagnosticToken(group))
|
|
1196
|
+
issues.push({
|
|
1197
|
+
domain: "view",
|
|
1198
|
+
code: "invalid-string",
|
|
1199
|
+
path: itemPath,
|
|
1200
|
+
message: "Output group must be a nonempty token",
|
|
1201
|
+
});
|
|
1202
|
+
else if (!produced.has(group))
|
|
1203
|
+
issues.push({
|
|
1204
|
+
domain: "view",
|
|
1205
|
+
code: "unknown-reference",
|
|
1206
|
+
path: itemPath,
|
|
1207
|
+
message: `Unknown output group ${group}`,
|
|
1208
|
+
});
|
|
1209
|
+
});
|
|
1210
|
+
if (issues.length > 0) throw new DiagnosticValidationError(issues);
|
|
1211
|
+
return [...new Set(outputGroups)].sort(codeUnit);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
export function sameMaturity(
|
|
1215
|
+
result: DiagnosticDeepReadonly<MetricDiagnosticsResult>,
|
|
1216
|
+
developmentAge: number,
|
|
1217
|
+
outputGroups?: readonly string[],
|
|
1218
|
+
): readonly DiagnosticDeepReadonly<DiagnosticEmergencePoint>[] {
|
|
1219
|
+
return sameMaturityContent(result, developmentAge, outputGroups);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/** Select compact numeric views without materializing preparation identity. */
|
|
1223
|
+
export function sameMaturityCompact(
|
|
1224
|
+
result: DiagnosticDeepReadonly<CompactMetricDiagnosticsResult>,
|
|
1225
|
+
developmentAge: number,
|
|
1226
|
+
outputGroups?: readonly string[],
|
|
1227
|
+
): readonly DiagnosticDeepReadonly<DiagnosticEmergencePoint>[] {
|
|
1228
|
+
return sameMaturityContent(result, developmentAge, outputGroups);
|
|
106
1229
|
}
|
|
107
1230
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
1231
|
+
function sameMaturityContent(
|
|
1232
|
+
result: DiagnosticDeepReadonly<MetricDiagnosticsResultContent>,
|
|
1233
|
+
developmentAge: number,
|
|
1234
|
+
outputGroups?: readonly string[],
|
|
1235
|
+
): readonly DiagnosticDeepReadonly<DiagnosticEmergencePoint>[] {
|
|
1236
|
+
if (!Number.isSafeInteger(developmentAge) || developmentAge < 0)
|
|
1237
|
+
throw new DiagnosticValidationError([
|
|
1238
|
+
{
|
|
1239
|
+
domain: "view",
|
|
1240
|
+
code: "invalid-number",
|
|
1241
|
+
path: "$.developmentAge",
|
|
1242
|
+
message: "Development age must be a nonnegative safe integer",
|
|
1243
|
+
},
|
|
1244
|
+
]);
|
|
1245
|
+
const groups =
|
|
1246
|
+
outputGroups === undefined
|
|
1247
|
+
? null
|
|
1248
|
+
: new Set(viewGroups(result, outputGroups, "$.outputGroups"));
|
|
1249
|
+
return result.emergence.filter(
|
|
1250
|
+
(point) =>
|
|
1251
|
+
point.developmentAge === developmentAge &&
|
|
1252
|
+
(groups === null || groups.has(point.group)),
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
export function commonMaturity(
|
|
1257
|
+
result: DiagnosticDeepReadonly<MetricDiagnosticsResult>,
|
|
1258
|
+
outputGroups: readonly string[],
|
|
1259
|
+
): DiagnosticDeepReadonly<CommonMaturityResult> {
|
|
1260
|
+
return commonMaturityContent(result, outputGroups);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/** Select compact numeric views without materializing preparation identity. */
|
|
1264
|
+
export function commonMaturityCompact(
|
|
1265
|
+
result: DiagnosticDeepReadonly<CompactMetricDiagnosticsResult>,
|
|
1266
|
+
outputGroups: readonly string[],
|
|
1267
|
+
): DiagnosticDeepReadonly<CommonMaturityResult> {
|
|
1268
|
+
return commonMaturityContent(result, outputGroups);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function commonMaturityContent(
|
|
1272
|
+
result: DiagnosticDeepReadonly<MetricDiagnosticsResultContent>,
|
|
1273
|
+
outputGroups: readonly string[],
|
|
1274
|
+
): DiagnosticDeepReadonly<CommonMaturityResult> {
|
|
1275
|
+
const groups = viewGroups(result, outputGroups, "$.outputGroups");
|
|
1276
|
+
if (groups.length === 0)
|
|
1277
|
+
return deepFreeze({
|
|
1278
|
+
developmentAge: null,
|
|
1279
|
+
ageUnit: result.ageUnit,
|
|
1280
|
+
points: [],
|
|
1281
|
+
});
|
|
1282
|
+
const common = [
|
|
1283
|
+
...new Set(
|
|
1284
|
+
result.emergence
|
|
1285
|
+
.filter((point) => point.group === groups[0])
|
|
1286
|
+
.map((point) => point.developmentAge),
|
|
1287
|
+
),
|
|
1288
|
+
]
|
|
1289
|
+
.filter((age) =>
|
|
1290
|
+
groups.every((group) =>
|
|
1291
|
+
result.emergence.some(
|
|
1292
|
+
(point) => point.group === group && point.developmentAge === age,
|
|
1293
|
+
),
|
|
1294
|
+
),
|
|
1295
|
+
)
|
|
1296
|
+
.sort((left, right) => right - left);
|
|
1297
|
+
const developmentAge = common[0] ?? null;
|
|
1298
|
+
return deepFreeze({
|
|
1299
|
+
developmentAge,
|
|
1300
|
+
ageUnit: result.ageUnit,
|
|
1301
|
+
points:
|
|
1302
|
+
developmentAge === null
|
|
1303
|
+
? []
|
|
1304
|
+
: sameMaturityContent(result, developmentAge, groups),
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
export function getMetricDiagnosticsResultIdentity(
|
|
1308
|
+
result: DiagnosticDeepReadonly<MetricDiagnosticsResult>,
|
|
1309
|
+
): NormalizedDiagnosticResultIdentity {
|
|
1310
|
+
return projectDiagnosticIdentity(result);
|
|
1311
|
+
}
|