@hraness/kb 0.17.1
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/DISCLOSURE +43 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/agent-context.js +35 -0
- package/dist/agent-guide-audit.js +18 -0
- package/dist/attachments.js +19 -0
- package/dist/authoring.js +33 -0
- package/dist/benchmark.js +19 -0
- package/dist/capture.js +49 -0
- package/dist/cli.js +3646 -0
- package/dist/clip/acquire.js +57 -0
- package/dist/clip/args.js +21 -0
- package/dist/clip/bounded-byte-buffer.js +9 -0
- package/dist/clip/browser-profiles.js +17 -0
- package/dist/clip/bundle-reader.js +11 -0
- package/dist/clip/cli.js +27 -0
- package/dist/clip/cookies.js +21 -0
- package/dist/clip/defuddle-worker.js +84 -0
- package/dist/clip/doctor.js +32 -0
- package/dist/clip/jobs.js +784 -0
- package/dist/clip/network-proxy.js +9 -0
- package/dist/clip/network.js +28 -0
- package/dist/clip/persist.js +30 -0
- package/dist/clip/refresh.js +11 -0
- package/dist/clip/terminal.js +11 -0
- package/dist/evaluation-builder.js +12238 -0
- package/dist/evaluation-kb.js +26 -0
- package/dist/evaluation.js +35 -0
- package/dist/git.js +39 -0
- package/dist/graph.js +39 -0
- package/dist/index-01jj6rbv.js +1103 -0
- package/dist/index-06c9ctr6.js +738 -0
- package/dist/index-0kavxzqj.js +2480 -0
- package/dist/index-1gwbassd.js +743 -0
- package/dist/index-1n418kb9.js +827 -0
- package/dist/index-1vrd1rmn.js +56 -0
- package/dist/index-1xxnjn0d.js +171 -0
- package/dist/index-3rm7cz6h.js +373 -0
- package/dist/index-3v2z4f0q.js +328 -0
- package/dist/index-48pz4jpc.js +392 -0
- package/dist/index-4j3tt0c3.js +234 -0
- package/dist/index-5n05se68.js +307 -0
- package/dist/index-5vwpzb5a.js +764 -0
- package/dist/index-6g2pv9d2.js +370 -0
- package/dist/index-7fzc32gf.js +712 -0
- package/dist/index-84x0vjjp.js +432 -0
- package/dist/index-adx6khj5.js +488 -0
- package/dist/index-b88v3vtm.js +805 -0
- package/dist/index-cv6fh7z5.js +462 -0
- package/dist/index-cxfrakt7.js +1067 -0
- package/dist/index-d13v9ckt.js +150 -0
- package/dist/index-dycjbhw2.js +1943 -0
- package/dist/index-dyqwejk5.js +531 -0
- package/dist/index-e5fbsywq.js +658 -0
- package/dist/index-f984hw45.js +927 -0
- package/dist/index-gh719d91.js +63 -0
- package/dist/index-hgve9rh2.js +2046 -0
- package/dist/index-hya40gb2.js +474 -0
- package/dist/index-j4zgmzjr.js +152 -0
- package/dist/index-jsmvyyvf.js +1166 -0
- package/dist/index-kh0pwvdd.js +1974 -0
- package/dist/index-mqx4nd6v.js +119 -0
- package/dist/index-mxxxytys.js +1254 -0
- package/dist/index-n5dd7r0v.js +711 -0
- package/dist/index-npg9z1a4.js +431 -0
- package/dist/index-pj501bh1.js +140 -0
- package/dist/index-qry4vhxk.js +1041 -0
- package/dist/index-s2gw5aw9.js +199 -0
- package/dist/index-vvdj6kca.js +70 -0
- package/dist/index-w2zc0vwa.js +571 -0
- package/dist/index-x3fthpsc.js +585 -0
- package/dist/index-zr53sf63.js +44 -0
- package/dist/index-zxdy5pby.js +2728 -0
- package/dist/index-zzhgcwyt.js +966 -0
- package/dist/index.js +453 -0
- package/dist/navigation.js +13 -0
- package/dist/pdf.js +85 -0
- package/dist/percolate.js +24 -0
- package/dist/portfolio.js +77 -0
- package/dist/query.js +29 -0
- package/dist/repository-memory.js +60 -0
- package/dist/sdk.js +36 -0
- package/dist/search-rules.js +34 -0
- package/dist/search.js +22 -0
- package/dist/semantic.js +39 -0
- package/dist/source-inbox.js +17 -0
- package/dist/untrusted-content.js +32 -0
- package/dist/url-intelligence.js +74 -0
- package/dist/workflow.js +23 -0
- package/dist/workflows/decision-context.js +20 -0
- package/dist/workflows/explain-change.js +8 -0
- package/dist/workflows/index.js +28 -0
- package/dist/workflows/plan-radar.js +10 -0
- package/package.json +409 -0
- package/skills/kb/AGENTS.md +19 -0
- package/skills/kb/SKILL.md +82 -0
- package/skills/kb/agents/openai.yaml +4 -0
- package/skills/kb/references/pdf-review.md +51 -0
- package/skills/kb/references/percolate.md +144 -0
- package/skills/kb/references/plan-structure.md +105 -0
- package/skills/kb/references/plan.md +115 -0
- package/skills/kb/references/query.md +231 -0
- package/skills/kb/references/refresh.md +133 -0
- package/skills/kb/references/save-pdf.md +125 -0
- package/skills/kb/references/save-url.md +178 -0
- package/skills/kb/references/url-authentication.md +75 -0
- package/skills/kb/references/url-platforms.md +25 -0
- package/src/agent-context.ts +1241 -0
- package/src/agent-guide-audit.ts +750 -0
- package/src/attachments.ts +723 -0
- package/src/authoring.ts +1725 -0
- package/src/benchmark.ts +362 -0
- package/src/capture.ts +68 -0
- package/src/cli.ts +3660 -0
- package/src/clip/acquire.ts +1273 -0
- package/src/clip/archive-today.ts +402 -0
- package/src/clip/args.ts +423 -0
- package/src/clip/assets.ts +262 -0
- package/src/clip/bounded-byte-buffer.ts +75 -0
- package/src/clip/browser-profiles.ts +372 -0
- package/src/clip/bundle-reader.ts +608 -0
- package/src/clip/capture.ts +1054 -0
- package/src/clip/cli.ts +181 -0
- package/src/clip/cookies.ts +512 -0
- package/src/clip/doctor.ts +1110 -0
- package/src/clip/extract.ts +868 -0
- package/src/clip/jobs.ts +959 -0
- package/src/clip/lib.ts +861 -0
- package/src/clip/media.ts +1474 -0
- package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
- package/src/clip/metadata-search-tool/Cargo.toml +18 -0
- package/src/clip/metadata-search-tool/runner.ts +685 -0
- package/src/clip/metadata-search-tool/src/main.rs +389 -0
- package/src/clip/metadata-search.ts +687 -0
- package/src/clip/network-proxy.ts +627 -0
- package/src/clip/network.ts +950 -0
- package/src/clip/package-root.ts +54 -0
- package/src/clip/persist.ts +1472 -0
- package/src/clip/platforms.ts +1824 -0
- package/src/clip/refresh.ts +197 -0
- package/src/clip/structured.ts +432 -0
- package/src/clip/terminal.ts +197 -0
- package/src/clip/url-intelligence.ts +581 -0
- package/src/clip/url-metadata-backfill.ts +616 -0
- package/src/clip/url-metadata-cli.ts +251 -0
- package/src/clip/url-metadata.ts +1397 -0
- package/src/evaluation-analysis-v2.ts +1440 -0
- package/src/evaluation-builder.ts +2539 -0
- package/src/evaluation-corpus-authoring.ts +2052 -0
- package/src/evaluation-evidence.ts +1147 -0
- package/src/evaluation-execution-v2.ts +426 -0
- package/src/evaluation-implementation.ts +131 -0
- package/src/evaluation-kb-closure.ts +1430 -0
- package/src/evaluation-kb-runner-v2.ts +735 -0
- package/src/evaluation-kb-v2.ts +2567 -0
- package/src/evaluation-kb.ts +1018 -0
- package/src/evaluation-measurement-v2.ts +1462 -0
- package/src/evaluation-packing-v2.ts +452 -0
- package/src/evaluation-redaction.ts +28 -0
- package/src/evaluation-v2.ts +3857 -0
- package/src/evaluation.ts +1220 -0
- package/src/git.ts +1276 -0
- package/src/graph.ts +1487 -0
- package/src/index.ts +20 -0
- package/src/init.ts +123 -0
- package/src/navigation.ts +277 -0
- package/src/note-lock.ts +529 -0
- package/src/pdf/args.ts +202 -0
- package/src/pdf/capture.ts +298 -0
- package/src/pdf/cli.ts +159 -0
- package/src/pdf/extract.ts +303 -0
- package/src/pdf/layout.ts +618 -0
- package/src/pdf/markdown.ts +154 -0
- package/src/pdf/model.ts +234 -0
- package/src/pdf/ocr.ts +190 -0
- package/src/pdf/persist.ts +314 -0
- package/src/pdf/source.ts +135 -0
- package/src/pdf/tools.ts +146 -0
- package/src/pdf.ts +92 -0
- package/src/percolate.ts +877 -0
- package/src/portfolio-audit.ts +574 -0
- package/src/portfolio-identity.ts +162 -0
- package/src/portfolio-registry.ts +473 -0
- package/src/portfolio.ts +765 -0
- package/src/query.ts +646 -0
- package/src/repository-memory.ts +1150 -0
- package/src/sdk.ts +1452 -0
- package/src/search-rules.ts +712 -0
- package/src/search.ts +710 -0
- package/src/semantic-runtime.ts +1139 -0
- package/src/semantic.ts +2480 -0
- package/src/source-inbox.ts +232 -0
- package/src/untrusted-content.ts +401 -0
- package/src/url-intelligence.ts +104 -0
- package/src/vault.ts +636 -0
- package/src/workflow.ts +653 -0
- package/src/workflows/decision-context.ts +81 -0
- package/src/workflows/explain-change.ts +73 -0
- package/src/workflows/index.ts +3 -0
- package/src/workflows/plan-radar.ts +101 -0
|
@@ -0,0 +1,1440 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import type { PairedBootstrapInterval } from "./evaluation.js";
|
|
4
|
+
import {
|
|
5
|
+
MAX_EVALUATION_V2_QUERIES,
|
|
6
|
+
MAX_EVALUATION_V2_RESULTS_PER_LANE,
|
|
7
|
+
MAX_EVALUATION_V2_SAMPLES,
|
|
8
|
+
evaluationSourceFamilyClusterIdsV2,
|
|
9
|
+
parseRetrievalEvaluationCorpusV2,
|
|
10
|
+
parseRetrievalEvaluationReportV2,
|
|
11
|
+
type EvaluationCohortV2,
|
|
12
|
+
type EvaluationMeasurementOperationV2,
|
|
13
|
+
type EvaluationMinimumUsefulEffectMetricV2,
|
|
14
|
+
type EvaluationNonInferiorityMetricV2,
|
|
15
|
+
type EvaluationQueryV2,
|
|
16
|
+
type EvaluationRepeatedSampleV2,
|
|
17
|
+
type EvaluationStratumV2,
|
|
18
|
+
type RetrievalEvaluationCorpusV2,
|
|
19
|
+
type RetrievalEvaluationReportV2,
|
|
20
|
+
} from "./evaluation-v2.js";
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_EVALUATION_ANALYSIS_CUTOFF_V2 = 10;
|
|
23
|
+
export const DEFAULT_EVALUATION_ANALYSIS_BOOTSTRAP_RESAMPLES_V2 = 2_000;
|
|
24
|
+
export const MAX_EVALUATION_ANALYSIS_BOOTSTRAP_RESAMPLES_V2 = 10_000;
|
|
25
|
+
export const MAX_EVALUATION_ANALYSIS_BOOTSTRAP_DRAWS_V2 = 50_000_000;
|
|
26
|
+
|
|
27
|
+
export type EvaluationQualityMetricV2 =
|
|
28
|
+
| EvaluationMinimumUsefulEffectMetricV2
|
|
29
|
+
| "context-precision"
|
|
30
|
+
| "decision-accuracy"
|
|
31
|
+
| "provenance-coverage";
|
|
32
|
+
|
|
33
|
+
export type EvaluationAnalysisSliceV2 =
|
|
34
|
+
| {
|
|
35
|
+
readonly id: "overall";
|
|
36
|
+
readonly kind: "overall";
|
|
37
|
+
}
|
|
38
|
+
| {
|
|
39
|
+
readonly id: `cohort:${EvaluationCohortV2}`;
|
|
40
|
+
readonly kind: "cohort";
|
|
41
|
+
readonly cohort: EvaluationCohortV2;
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
readonly id: `primary-stratum:${EvaluationStratumV2}`;
|
|
45
|
+
readonly kind: "primary-stratum";
|
|
46
|
+
readonly primaryStratum: EvaluationStratumV2;
|
|
47
|
+
}
|
|
48
|
+
| {
|
|
49
|
+
readonly id: `cohort:${EvaluationCohortV2}:primary-stratum:${EvaluationStratumV2}`;
|
|
50
|
+
readonly kind: "cohort-primary-stratum";
|
|
51
|
+
readonly cohort: EvaluationCohortV2;
|
|
52
|
+
readonly primaryStratum: EvaluationStratumV2;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type EvaluationMetricEstimateV2 = {
|
|
56
|
+
readonly metric: EvaluationQualityMetricV2;
|
|
57
|
+
readonly eligibleQueries: number;
|
|
58
|
+
readonly observedQueries: number;
|
|
59
|
+
readonly value: number | null;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type EvaluationSliceSummaryV2 = {
|
|
63
|
+
readonly slice: EvaluationAnalysisSliceV2;
|
|
64
|
+
readonly queryCount: number;
|
|
65
|
+
readonly metrics: readonly EvaluationMetricEstimateV2[];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type EvaluationRetrieverQualitySummaryV2 = {
|
|
69
|
+
readonly retrieverId: string;
|
|
70
|
+
readonly queryCount: number;
|
|
71
|
+
readonly slices: readonly EvaluationSliceSummaryV2[];
|
|
72
|
+
readonly acceptedEvidenceUnits: number;
|
|
73
|
+
readonly provenanceCoveredEvidenceUnits: number;
|
|
74
|
+
readonly provenanceCoverage: number;
|
|
75
|
+
readonly failedSamples: number;
|
|
76
|
+
readonly unavailableWarmQuerySamples: number;
|
|
77
|
+
readonly unavailableQualitySamples: number;
|
|
78
|
+
readonly missingQualityObservations: number;
|
|
79
|
+
readonly nonzeroLlmAccountingSamples: number;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type EvaluationPairedEffectV2 = {
|
|
83
|
+
readonly baselineRetrieverId: string;
|
|
84
|
+
readonly candidateRetrieverId: string;
|
|
85
|
+
readonly metric: EvaluationQualityMetricV2;
|
|
86
|
+
readonly slice: EvaluationAnalysisSliceV2;
|
|
87
|
+
readonly direction: "higher-is-better" | "lower-is-better";
|
|
88
|
+
readonly eligibleQueries: number;
|
|
89
|
+
readonly observedPairs: number;
|
|
90
|
+
readonly sourceFamilyClusters: number;
|
|
91
|
+
readonly inferenceStatus: "estimable" | "insufficient-clusters";
|
|
92
|
+
readonly baselineMean: number;
|
|
93
|
+
readonly candidateMean: number;
|
|
94
|
+
/** Positive values always favor the candidate, including lower-is-better metrics. */
|
|
95
|
+
readonly favorableInterval: PairedBootstrapInterval;
|
|
96
|
+
/** One-sided 95% lower bound used only for the predeclared positive primary effect. */
|
|
97
|
+
readonly favorableOneSidedLower: number;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type EvaluationLatencyProfileSummaryV2 = {
|
|
101
|
+
readonly retrieverId: string;
|
|
102
|
+
readonly profileId: string;
|
|
103
|
+
readonly operation: EvaluationMeasurementOperationV2;
|
|
104
|
+
readonly cacheState: RetrievalEvaluationCorpusV2["measurementProfiles"][number]["cacheState"];
|
|
105
|
+
readonly expectedObservations: number;
|
|
106
|
+
readonly observedObservations: number;
|
|
107
|
+
readonly p95Ms: number | null;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export type EvaluationCandidateGateCheckV2 = {
|
|
111
|
+
readonly kind:
|
|
112
|
+
| "minimum-useful-effect"
|
|
113
|
+
| "noninferiority-latency"
|
|
114
|
+
| "observed-no-regression";
|
|
115
|
+
readonly metric: EvaluationMinimumUsefulEffectMetricV2 | EvaluationNonInferiorityMetricV2;
|
|
116
|
+
readonly status: "failed" | "not-evaluable" | "passed";
|
|
117
|
+
readonly sliceId?: EvaluationAnalysisSliceV2["id"];
|
|
118
|
+
readonly profileId?: string;
|
|
119
|
+
readonly observedEffect?: number;
|
|
120
|
+
readonly eligibleQueries?: number;
|
|
121
|
+
readonly observedPairs?: number;
|
|
122
|
+
readonly sourceFamilyClusters?: number;
|
|
123
|
+
readonly regressedPairs?: number;
|
|
124
|
+
readonly confidenceLower?: number;
|
|
125
|
+
readonly confidenceUpper?: number;
|
|
126
|
+
readonly requiredImprovement?: number;
|
|
127
|
+
readonly allowedRegression?: number;
|
|
128
|
+
readonly baselineObserved?: number;
|
|
129
|
+
readonly candidateObserved?: number;
|
|
130
|
+
readonly durationScope?: "context-ready-elapsed" | "query-operation";
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export type EvaluationCandidateGateReasonCodeV2 =
|
|
134
|
+
| "ambiguous-warm-query-profile"
|
|
135
|
+
| "failed-sample"
|
|
136
|
+
| "insufficient-independent-pairs"
|
|
137
|
+
| "minimum-useful-effect-not-met"
|
|
138
|
+
| "missing-quality-profile"
|
|
139
|
+
| "missing-eligible-observations"
|
|
140
|
+
| "missing-warm-query-profile"
|
|
141
|
+
| "noninferiority-margin-exceeded"
|
|
142
|
+
| "nonzero-llm-accounting"
|
|
143
|
+
| "observed-query-regression"
|
|
144
|
+
| "provenance-below-100-percent"
|
|
145
|
+
| "unavailable-quality-sample"
|
|
146
|
+
| "unavailable-warm-query-sample";
|
|
147
|
+
|
|
148
|
+
export type EvaluationVariantComplexityV2 = {
|
|
149
|
+
readonly laneCount: number;
|
|
150
|
+
readonly activeConfigurationEntries: number;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export type EvaluationVariantSelectionV2 = {
|
|
154
|
+
readonly baselineRetrieverId: string;
|
|
155
|
+
readonly orderedRetrieverIds: readonly string[];
|
|
156
|
+
readonly passingRetrieverIds: readonly string[];
|
|
157
|
+
readonly selectedRetrieverId: string | null;
|
|
158
|
+
readonly incrementalChecks: readonly EvaluationVariantIncrementalCheckV2[];
|
|
159
|
+
readonly complexity: readonly {
|
|
160
|
+
readonly retrieverId: string;
|
|
161
|
+
readonly role: "ablation" | "candidate";
|
|
162
|
+
readonly score: EvaluationVariantComplexityV2;
|
|
163
|
+
}[];
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export type EvaluationVariantIncrementalCheckV2 = {
|
|
167
|
+
readonly baselineRetrieverId: string;
|
|
168
|
+
readonly candidateRetrieverId: string;
|
|
169
|
+
readonly metric: EvaluationMinimumUsefulEffectMetricV2;
|
|
170
|
+
readonly sliceId: EvaluationAnalysisSliceV2["id"];
|
|
171
|
+
readonly status: "failed" | "not-evaluable" | "passed";
|
|
172
|
+
readonly eligibleQueries: number;
|
|
173
|
+
readonly observedPairs: number;
|
|
174
|
+
readonly sourceFamilyClusters: number;
|
|
175
|
+
readonly requiredImprovement: number;
|
|
176
|
+
readonly observedEffect?: number;
|
|
177
|
+
readonly confidenceLower?: number;
|
|
178
|
+
readonly confidenceUpper?: number;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export type EvaluationCandidateGateReasonV2 = {
|
|
182
|
+
readonly code: EvaluationCandidateGateReasonCodeV2;
|
|
183
|
+
readonly message: string;
|
|
184
|
+
readonly retrieverId?: string;
|
|
185
|
+
readonly metric?: EvaluationMinimumUsefulEffectMetricV2 | EvaluationNonInferiorityMetricV2;
|
|
186
|
+
readonly sliceId?: EvaluationAnalysisSliceV2["id"];
|
|
187
|
+
readonly profileId?: string;
|
|
188
|
+
readonly count?: number;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export type EvaluationCandidateGateV2 = {
|
|
192
|
+
readonly baselineRetrieverId: string;
|
|
193
|
+
readonly candidateRetrieverId: string;
|
|
194
|
+
readonly passed: boolean;
|
|
195
|
+
readonly checks: readonly EvaluationCandidateGateCheckV2[];
|
|
196
|
+
readonly reasons: readonly EvaluationCandidateGateReasonV2[];
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export type RetrievalEvaluationAnalysisV2 = {
|
|
200
|
+
readonly schemaVersion: 2;
|
|
201
|
+
readonly suiteSha256: string;
|
|
202
|
+
readonly candidateLockSha256: string;
|
|
203
|
+
readonly split: RetrievalEvaluationReportV2["split"];
|
|
204
|
+
readonly cutoff: number;
|
|
205
|
+
readonly bootstrap: {
|
|
206
|
+
readonly confidence: 0.95;
|
|
207
|
+
readonly resamples: number;
|
|
208
|
+
readonly draws: number;
|
|
209
|
+
};
|
|
210
|
+
readonly warmQueryProfileId: string | null;
|
|
211
|
+
readonly qualityProfileId: string | null;
|
|
212
|
+
readonly slices: readonly EvaluationAnalysisSliceV2[];
|
|
213
|
+
readonly retrievers: readonly EvaluationRetrieverQualitySummaryV2[];
|
|
214
|
+
readonly pairedEffects: readonly EvaluationPairedEffectV2[];
|
|
215
|
+
readonly latencyProfiles: readonly EvaluationLatencyProfileSummaryV2[];
|
|
216
|
+
readonly candidateGates: readonly EvaluationCandidateGateV2[];
|
|
217
|
+
/** Same sealed gate applied to every non-baseline ablation and candidate. */
|
|
218
|
+
readonly retrieverGates: readonly EvaluationCandidateGateV2[];
|
|
219
|
+
/** Select only an official locked candidate that clears its gate and every incremental check. */
|
|
220
|
+
readonly variantSelection: EvaluationVariantSelectionV2;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export type AnalyzeRetrievalEvaluationV2Options = {
|
|
224
|
+
readonly cutoff?: number;
|
|
225
|
+
readonly bootstrapResamples?: number;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
type QueryMetricValues = Readonly<Record<EvaluationQualityMetricV2, number | null>>;
|
|
229
|
+
|
|
230
|
+
type QueryScore = {
|
|
231
|
+
readonly queryId: string;
|
|
232
|
+
readonly sourceFamilyClusterId: string;
|
|
233
|
+
readonly metrics: QueryMetricValues;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
type RetrieverFaults = {
|
|
237
|
+
failedSamples: number;
|
|
238
|
+
unavailableWarmQuerySamples: number;
|
|
239
|
+
unavailableQualitySamples: number;
|
|
240
|
+
missingQualityObservations: number;
|
|
241
|
+
nonzeroLlmAccountingSamples: number;
|
|
242
|
+
acceptedEvidenceUnits: number;
|
|
243
|
+
provenanceCoveredEvidenceUnits: number;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const QUALITY_METRICS = Object.freeze([
|
|
247
|
+
"document-recall-at-k",
|
|
248
|
+
"evidence-recall-at-k",
|
|
249
|
+
"nugget-coverage",
|
|
250
|
+
"context-precision",
|
|
251
|
+
"no-answer-accuracy",
|
|
252
|
+
"false-abstention-rate",
|
|
253
|
+
"provenance-coverage",
|
|
254
|
+
"decision-accuracy",
|
|
255
|
+
] as const satisfies readonly EvaluationQualityMetricV2[]);
|
|
256
|
+
|
|
257
|
+
const COHORTS = Object.freeze(["caller-seeded", "text-only"] as const);
|
|
258
|
+
|
|
259
|
+
const ACCURACY_STRATUM_BY_METRIC = Object.freeze({
|
|
260
|
+
"active-current-state-accuracy": "active-current-state",
|
|
261
|
+
"code-path-context-accuracy": "code-path-context",
|
|
262
|
+
"conceptual-recall-accuracy": "conceptual-recall",
|
|
263
|
+
"exact-identity-accuracy": "exact-identity",
|
|
264
|
+
"local-context-accuracy": "local-context",
|
|
265
|
+
"metadata-constraint-accuracy": "metadata-constraint",
|
|
266
|
+
"multi-note-relational-accuracy": "multi-note-relational",
|
|
267
|
+
"source-provenance-accuracy": "source-provenance",
|
|
268
|
+
"temporal-stale-current-accuracy": "temporal-stale-current",
|
|
269
|
+
} as const satisfies Partial<Record<EvaluationNonInferiorityMetricV2, EvaluationStratumV2>>);
|
|
270
|
+
|
|
271
|
+
const LATENCY_OPERATION_BY_METRIC = Object.freeze({
|
|
272
|
+
"four-reader-query-p95-ms": "four-reader-query",
|
|
273
|
+
"packing-p95-ms": "packing",
|
|
274
|
+
"warm-query-p95-ms": "warm-query",
|
|
275
|
+
} as const satisfies Partial<Record<EvaluationNonInferiorityMetricV2, EvaluationMeasurementOperationV2>>);
|
|
276
|
+
|
|
277
|
+
const QUALITY_NONINFERIORITY_METRICS = new Set<EvaluationNonInferiorityMetricV2>([
|
|
278
|
+
"document-recall-at-k",
|
|
279
|
+
"evidence-recall-at-k",
|
|
280
|
+
]);
|
|
281
|
+
|
|
282
|
+
function average(values: readonly number[]): number | null {
|
|
283
|
+
if (values.length === 0) return null;
|
|
284
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function percentile(values: readonly number[], proportion: number): number | null {
|
|
288
|
+
if (values.length === 0) return null;
|
|
289
|
+
const sorted = values.toSorted((left, right) => left - right);
|
|
290
|
+
const index = Math.max(0, Math.ceil(proportion * sorted.length) - 1);
|
|
291
|
+
return sorted[index] ?? null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function meanRequired(values: readonly number[], label: string): number {
|
|
295
|
+
const result = average(values);
|
|
296
|
+
if (result === null) throw new TypeError(`${label} requires at least one finite value.`);
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function sampleGroupKey(retrieverId: string, profileId: string, queryId: string | undefined): string {
|
|
301
|
+
return `${retrieverId}\0${profileId}\0${queryId ?? ""}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function metricEligible(query: EvaluationQueryV2, metric: EvaluationQualityMetricV2): boolean {
|
|
305
|
+
if (
|
|
306
|
+
metric === "document-recall-at-k"
|
|
307
|
+
|| metric === "evidence-recall-at-k"
|
|
308
|
+
|| metric === "nugget-coverage"
|
|
309
|
+
|| metric === "false-abstention-rate"
|
|
310
|
+
) return query.expectedSupport === "supported";
|
|
311
|
+
if (metric === "no-answer-accuracy") return query.expectedSupport === "insufficient";
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function queryInSlice(query: EvaluationQueryV2, slice: EvaluationAnalysisSliceV2): boolean {
|
|
316
|
+
if (slice.kind === "overall") return true;
|
|
317
|
+
if (slice.kind === "cohort") return query.cohort === slice.cohort;
|
|
318
|
+
if (slice.kind === "primary-stratum") return query.primaryStratum === slice.primaryStratum;
|
|
319
|
+
return query.cohort === slice.cohort && query.primaryStratum === slice.primaryStratum;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function analysisSlices(queries: readonly EvaluationQueryV2[]): readonly EvaluationAnalysisSliceV2[] {
|
|
323
|
+
const primaryStrata = [...new Set(queries.map(({ primaryStratum }) => primaryStratum))].toSorted();
|
|
324
|
+
return Object.freeze([
|
|
325
|
+
Object.freeze({ id: "overall", kind: "overall" }),
|
|
326
|
+
...COHORTS.map((cohort) => Object.freeze({
|
|
327
|
+
id: `cohort:${cohort}` as const,
|
|
328
|
+
kind: "cohort" as const,
|
|
329
|
+
cohort,
|
|
330
|
+
})),
|
|
331
|
+
...primaryStrata.map((primaryStratum) => Object.freeze({
|
|
332
|
+
id: `primary-stratum:${primaryStratum}` as const,
|
|
333
|
+
kind: "primary-stratum" as const,
|
|
334
|
+
primaryStratum,
|
|
335
|
+
})),
|
|
336
|
+
...COHORTS.flatMap((cohort) => primaryStrata.map((primaryStratum) => Object.freeze({
|
|
337
|
+
id: `cohort:${cohort}:primary-stratum:${primaryStratum}` as const,
|
|
338
|
+
kind: "cohort-primary-stratum" as const,
|
|
339
|
+
cohort,
|
|
340
|
+
primaryStratum,
|
|
341
|
+
}))),
|
|
342
|
+
]);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function acceptedDecisions(sample: EvaluationRepeatedSampleV2): readonly EvaluationRepeatedSampleV2["trace"]["candidateDecisions"][number][] {
|
|
346
|
+
return sample.trace.candidateDecisions
|
|
347
|
+
.filter(({ disposition }) => disposition === "accepted")
|
|
348
|
+
.toSorted((left, right) => (left.outputRank ?? Number.MAX_SAFE_INTEGER) - (right.outputRank ?? Number.MAX_SAFE_INTEGER));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function countPackedContextProvenanceV2(
|
|
352
|
+
sample: EvaluationRepeatedSampleV2,
|
|
353
|
+
): {
|
|
354
|
+
readonly packed: number;
|
|
355
|
+
readonly covered: number;
|
|
356
|
+
} {
|
|
357
|
+
const packedEvidenceUnitIds = new Set(sample.packedContextTrace?.evidenceUnitIds ?? []);
|
|
358
|
+
const provenancedEvidenceUnitIds = new Set<string>();
|
|
359
|
+
for (const decision of sample.trace.candidateDecisions) {
|
|
360
|
+
for (const locator of decision.provenance) {
|
|
361
|
+
provenancedEvidenceUnitIds.add(locator.evidenceUnitId);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return Object.freeze({
|
|
365
|
+
packed: packedEvidenceUnitIds.size,
|
|
366
|
+
covered: [...packedEvidenceUnitIds]
|
|
367
|
+
.filter((evidenceUnitId) => provenancedEvidenceUnitIds.has(evidenceUnitId)).length,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function scoreSample(
|
|
372
|
+
query: EvaluationQueryV2,
|
|
373
|
+
sample: EvaluationRepeatedSampleV2,
|
|
374
|
+
cutoff: number,
|
|
375
|
+
evidenceById: ReadonlyMap<string, RetrievalEvaluationCorpusV2["evidenceUnits"][number]>,
|
|
376
|
+
): QueryMetricValues {
|
|
377
|
+
const packedEvidenceIds = sample.packedContextTrace?.evidenceUnitIds;
|
|
378
|
+
if (packedEvidenceIds === undefined) {
|
|
379
|
+
throw new TypeError(`Quality sample ${sample.retrieverId}/${sample.queryId ?? ""} lacks packed context.`);
|
|
380
|
+
}
|
|
381
|
+
const packedEvidence = new Set(packedEvidenceIds);
|
|
382
|
+
for (const evidenceUnitId of packedEvidence) {
|
|
383
|
+
if (!evidenceById.has(evidenceUnitId)) {
|
|
384
|
+
throw new TypeError(`Packed evidence unit ${evidenceUnitId} is missing from the catalog.`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const topKAccepted = acceptedDecisions(sample)
|
|
388
|
+
.filter(({ outputRank }) => outputRank !== undefined && outputRank <= cutoff);
|
|
389
|
+
const relevantDocuments = new Set(query.gold.documents
|
|
390
|
+
.filter(({ relevance }) => relevance > 0)
|
|
391
|
+
.map(({ documentId }) => documentId));
|
|
392
|
+
const relevantEvidence = new Set(query.gold.evidenceUnits
|
|
393
|
+
.filter(({ relevance }) => relevance > 0)
|
|
394
|
+
.map(({ evidenceUnitId }) => evidenceUnitId));
|
|
395
|
+
const topKDocuments = new Set(topKAccepted
|
|
396
|
+
.filter(({ evidenceUnitIds }) => evidenceUnitIds.some((evidenceUnitId) =>
|
|
397
|
+
packedEvidence.has(evidenceUnitId)))
|
|
398
|
+
.map(({ documentId }) => documentId));
|
|
399
|
+
const topKEvidence = new Set(sample.trace.candidateDecisions
|
|
400
|
+
.filter(({ documentId }) => topKDocuments.has(documentId))
|
|
401
|
+
.flatMap(({ evidenceUnitIds }) => evidenceUnitIds)
|
|
402
|
+
.filter((evidenceUnitId) => packedEvidence.has(evidenceUnitId)));
|
|
403
|
+
const evidenceRelevance = new Map(query.gold.evidenceUnits.map(({ evidenceUnitId, relevance }) => [
|
|
404
|
+
evidenceUnitId,
|
|
405
|
+
relevance,
|
|
406
|
+
]));
|
|
407
|
+
const requiredNuggets = query.gold.nuggets.filter(({ required }) => required);
|
|
408
|
+
if (query.expectedSupport === "supported" && requiredNuggets.length === 0) {
|
|
409
|
+
throw new TypeError(`Supported query ${query.id} has no required nugget.`);
|
|
410
|
+
}
|
|
411
|
+
const coveredRequiredNuggets = requiredNuggets.filter((nugget) =>
|
|
412
|
+
nugget.acceptableSupportSets.some((supportSet) =>
|
|
413
|
+
supportSet.evidenceUnitIds.every((evidenceUnitId) => packedEvidence.has(evidenceUnitId))));
|
|
414
|
+
const provenance = countPackedContextProvenanceV2(sample);
|
|
415
|
+
const hasUnjudgedPackedEvidence = [...packedEvidence].some(
|
|
416
|
+
(evidenceUnitId) => !evidenceRelevance.has(evidenceUnitId),
|
|
417
|
+
);
|
|
418
|
+
const contextPrecision = packedEvidence.size === 0
|
|
419
|
+
? 1
|
|
420
|
+
: hasUnjudgedPackedEvidence
|
|
421
|
+
? null
|
|
422
|
+
: [...packedEvidence].filter((evidenceUnitId) => (evidenceRelevance.get(evidenceUnitId) ?? 0) > 0).length
|
|
423
|
+
/ packedEvidence.size;
|
|
424
|
+
const nuggetCoverage = query.expectedSupport === "supported"
|
|
425
|
+
? coveredRequiredNuggets.length / requiredNuggets.length
|
|
426
|
+
: null;
|
|
427
|
+
const noAnswerAccuracy = query.expectedSupport === "insufficient"
|
|
428
|
+
? (packedEvidence.size === 0 ? 1 : 0)
|
|
429
|
+
: null;
|
|
430
|
+
const falseAbstentionRate = query.expectedSupport === "supported"
|
|
431
|
+
? (packedEvidence.size === 0 ? 1 : 0)
|
|
432
|
+
: null;
|
|
433
|
+
return Object.freeze({
|
|
434
|
+
"document-recall-at-k": query.expectedSupport === "supported" && relevantDocuments.size > 0
|
|
435
|
+
? [...relevantDocuments].filter((documentId) => topKDocuments.has(documentId)).length / relevantDocuments.size
|
|
436
|
+
: null,
|
|
437
|
+
"evidence-recall-at-k": query.expectedSupport === "supported" && relevantEvidence.size > 0
|
|
438
|
+
? [...relevantEvidence].filter((evidenceUnitId) => topKEvidence.has(evidenceUnitId)).length / relevantEvidence.size
|
|
439
|
+
: null,
|
|
440
|
+
"nugget-coverage": nuggetCoverage,
|
|
441
|
+
"context-precision": contextPrecision,
|
|
442
|
+
"no-answer-accuracy": noAnswerAccuracy,
|
|
443
|
+
"false-abstention-rate": falseAbstentionRate,
|
|
444
|
+
"provenance-coverage": provenance.packed === 0 ? 1 : provenance.covered / provenance.packed,
|
|
445
|
+
"decision-accuracy": query.expectedSupport === "supported"
|
|
446
|
+
? (nuggetCoverage === 1 ? 1 : 0)
|
|
447
|
+
: (noAnswerAccuracy ?? 0),
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function aggregateRepetitions(
|
|
452
|
+
query: EvaluationQueryV2,
|
|
453
|
+
samples: readonly EvaluationRepeatedSampleV2[],
|
|
454
|
+
cutoff: number,
|
|
455
|
+
evidenceById: ReadonlyMap<string, RetrievalEvaluationCorpusV2["evidenceUnits"][number]>,
|
|
456
|
+
sourceFamilyClusterId: string,
|
|
457
|
+
): QueryScore {
|
|
458
|
+
const perSample = samples.map((sample) => scoreSample(query, sample, cutoff, evidenceById));
|
|
459
|
+
const metrics = Object.fromEntries(QUALITY_METRICS.map((metric) => {
|
|
460
|
+
const repetitionValues = perSample.map((row) => row[metric]);
|
|
461
|
+
const values = repetitionValues.flatMap((value) => value === null ? [] : [value]);
|
|
462
|
+
return [
|
|
463
|
+
metric,
|
|
464
|
+
metric === "context-precision" && repetitionValues.some((value) => value === null)
|
|
465
|
+
? null
|
|
466
|
+
: average(values),
|
|
467
|
+
];
|
|
468
|
+
})) as Record<EvaluationQualityMetricV2, number | null>;
|
|
469
|
+
return Object.freeze({ queryId: query.id, sourceFamilyClusterId, metrics: Object.freeze(metrics) });
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function sliceSummary(
|
|
473
|
+
slice: EvaluationAnalysisSliceV2,
|
|
474
|
+
queries: readonly EvaluationQueryV2[],
|
|
475
|
+
scores: ReadonlyMap<string, QueryScore>,
|
|
476
|
+
): EvaluationSliceSummaryV2 {
|
|
477
|
+
const slicedQueries = queries.filter((query) => queryInSlice(query, slice));
|
|
478
|
+
const metrics = QUALITY_METRICS.map((metric): EvaluationMetricEstimateV2 => {
|
|
479
|
+
const eligible = slicedQueries.filter((query) => metricEligible(query, metric));
|
|
480
|
+
const values = eligible.flatMap((query) => {
|
|
481
|
+
const value = scores.get(query.id)?.metrics[metric];
|
|
482
|
+
return value === null || value === undefined ? [] : [value];
|
|
483
|
+
});
|
|
484
|
+
return Object.freeze({
|
|
485
|
+
metric,
|
|
486
|
+
eligibleQueries: eligible.length,
|
|
487
|
+
observedQueries: values.length,
|
|
488
|
+
value: average(values),
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
return Object.freeze({
|
|
492
|
+
slice,
|
|
493
|
+
queryCount: slicedQueries.length,
|
|
494
|
+
metrics: Object.freeze(metrics),
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function operationDuration(
|
|
499
|
+
sample: EvaluationRepeatedSampleV2,
|
|
500
|
+
operation: EvaluationMeasurementOperationV2,
|
|
501
|
+
): number {
|
|
502
|
+
if (operation === "cold-index") return sample.timings.indexMs;
|
|
503
|
+
if (operation === "incremental-update") return sample.timings.updateMs;
|
|
504
|
+
if (operation === "packing") return sample.timings.packingMs;
|
|
505
|
+
return sample.timings.queryMs;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function promotionOperationDuration(
|
|
509
|
+
sample: EvaluationRepeatedSampleV2,
|
|
510
|
+
operation: EvaluationMeasurementOperationV2,
|
|
511
|
+
): number {
|
|
512
|
+
if (operation === "packing") return sample.timings.elapsedMs;
|
|
513
|
+
return operationDuration(sample, operation);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function expectedProfileObservations(
|
|
517
|
+
profile: RetrievalEvaluationCorpusV2["measurementProfiles"][number],
|
|
518
|
+
queryCount: number,
|
|
519
|
+
): number {
|
|
520
|
+
return profile.repetitions * (profile.scope === "query" ? queryCount : 1);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function evaluationAnalysisBootstrapSeedV2(
|
|
524
|
+
suiteSha256: string,
|
|
525
|
+
candidateRetrieverId: string,
|
|
526
|
+
metric: EvaluationQualityMetricV2,
|
|
527
|
+
sliceId: EvaluationAnalysisSliceV2["id"] = "overall",
|
|
528
|
+
): number {
|
|
529
|
+
const digest = createHash("sha256")
|
|
530
|
+
.update(suiteSha256, "utf8")
|
|
531
|
+
.update("\0", "utf8")
|
|
532
|
+
.update(candidateRetrieverId, "utf8")
|
|
533
|
+
.update("\0", "utf8")
|
|
534
|
+
.update(metric, "utf8")
|
|
535
|
+
.update("\0", "utf8")
|
|
536
|
+
.update(sliceId, "utf8")
|
|
537
|
+
.digest();
|
|
538
|
+
return digest.readUInt32BE(0);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
type ClusteredPair = {
|
|
542
|
+
readonly baseline: number;
|
|
543
|
+
readonly candidate: number;
|
|
544
|
+
readonly clusterId: string;
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
type ObservedNoRegressionGuard = {
|
|
548
|
+
readonly eligibleQueries: number;
|
|
549
|
+
readonly observedPairs: number;
|
|
550
|
+
readonly regressedPairs: number;
|
|
551
|
+
/** Mean paired difference in the favorable direction; this is descriptive, not inferential. */
|
|
552
|
+
readonly observedEffect: number | null;
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
function favorablePairs(
|
|
556
|
+
metric: EvaluationQualityMetricV2,
|
|
557
|
+
pairs: readonly ClusteredPair[],
|
|
558
|
+
): readonly ClusteredPair[] {
|
|
559
|
+
if (metric !== "false-abstention-rate") return pairs;
|
|
560
|
+
return pairs.map(({ baseline, candidate, clusterId }) => ({
|
|
561
|
+
baseline: -baseline,
|
|
562
|
+
candidate: -candidate,
|
|
563
|
+
clusterId,
|
|
564
|
+
}));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function observedNoRegressionGuard(
|
|
568
|
+
options: Readonly<{
|
|
569
|
+
readonly metric: EvaluationQualityMetricV2;
|
|
570
|
+
readonly slice: EvaluationAnalysisSliceV2;
|
|
571
|
+
readonly queries: readonly EvaluationQueryV2[];
|
|
572
|
+
readonly baselineScores: ReadonlyMap<string, QueryScore>;
|
|
573
|
+
readonly candidateScores: ReadonlyMap<string, QueryScore>;
|
|
574
|
+
}>,
|
|
575
|
+
): ObservedNoRegressionGuard {
|
|
576
|
+
const eligibleQueries = options.queries.filter((query) =>
|
|
577
|
+
queryInSlice(query, options.slice) && metricEligible(query, options.metric));
|
|
578
|
+
const favorableDifferences = eligibleQueries.flatMap((query) => {
|
|
579
|
+
const baseline = options.baselineScores.get(query.id)?.metrics[options.metric];
|
|
580
|
+
const candidate = options.candidateScores.get(query.id)?.metrics[options.metric];
|
|
581
|
+
if (baseline === null || baseline === undefined || candidate === null || candidate === undefined) return [];
|
|
582
|
+
return [options.metric === "false-abstention-rate"
|
|
583
|
+
? baseline - candidate
|
|
584
|
+
: candidate - baseline];
|
|
585
|
+
});
|
|
586
|
+
return Object.freeze({
|
|
587
|
+
eligibleQueries: eligibleQueries.length,
|
|
588
|
+
observedPairs: favorableDifferences.length,
|
|
589
|
+
regressedPairs: favorableDifferences.filter((difference) => difference < 0).length,
|
|
590
|
+
observedEffect: average(favorableDifferences),
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function seededRandom(seed: number): () => number {
|
|
595
|
+
let state = seed >>> 0;
|
|
596
|
+
return () => {
|
|
597
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
598
|
+
let value = state;
|
|
599
|
+
value = Math.imul(value ^ (value >>> 15), value | 1);
|
|
600
|
+
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
|
|
601
|
+
return ((value ^ (value >>> 14)) >>> 0) / 4_294_967_296;
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function clusteredPairedBootstrapConfidenceInterval(
|
|
606
|
+
pairs: readonly ClusteredPair[],
|
|
607
|
+
options: Readonly<{ readonly seed: number; readonly resamples: number }>,
|
|
608
|
+
): PairedBootstrapInterval & { readonly oneSidedLower: number } {
|
|
609
|
+
if (pairs.length < 1) throw new RangeError("Clustered bootstrap requires at least one pair.");
|
|
610
|
+
const groups = new Map<string, ClusteredPair[]>();
|
|
611
|
+
for (const pair of pairs) {
|
|
612
|
+
const group = groups.get(pair.clusterId) ?? [];
|
|
613
|
+
group.push(pair);
|
|
614
|
+
groups.set(pair.clusterId, group);
|
|
615
|
+
}
|
|
616
|
+
const clusters = [...groups.keys()].toSorted();
|
|
617
|
+
const observedDifference = pairs.reduce(
|
|
618
|
+
(sum, pair) => sum + pair.candidate - pair.baseline,
|
|
619
|
+
0,
|
|
620
|
+
) / pairs.length;
|
|
621
|
+
if (clusters.length < 2) {
|
|
622
|
+
return Object.freeze({
|
|
623
|
+
pairs: pairs.length,
|
|
624
|
+
seed: options.seed,
|
|
625
|
+
resamples: options.resamples,
|
|
626
|
+
confidence: 0.95,
|
|
627
|
+
observedDifference,
|
|
628
|
+
lower: -1,
|
|
629
|
+
upper: 1,
|
|
630
|
+
oneSidedLower: -1,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
const random = seededRandom(options.seed);
|
|
634
|
+
const draws: number[] = [];
|
|
635
|
+
for (let drawIndex = 0; drawIndex < options.resamples; drawIndex += 1) {
|
|
636
|
+
let difference = 0;
|
|
637
|
+
let observations = 0;
|
|
638
|
+
for (let clusterIndex = 0; clusterIndex < clusters.length; clusterIndex += 1) {
|
|
639
|
+
const clusterId = clusters[Math.floor(random() * clusters.length)];
|
|
640
|
+
const group = clusterId === undefined ? undefined : groups.get(clusterId);
|
|
641
|
+
if (group === undefined) throw new Error("Cluster bootstrap selected a missing source family.");
|
|
642
|
+
for (const pair of group) difference += pair.candidate - pair.baseline;
|
|
643
|
+
observations += group.length;
|
|
644
|
+
}
|
|
645
|
+
draws.push(difference / observations);
|
|
646
|
+
}
|
|
647
|
+
draws.sort((left, right) => left - right);
|
|
648
|
+
return Object.freeze({
|
|
649
|
+
pairs: pairs.length,
|
|
650
|
+
seed: options.seed,
|
|
651
|
+
resamples: options.resamples,
|
|
652
|
+
confidence: 0.95,
|
|
653
|
+
observedDifference,
|
|
654
|
+
lower: draws[Math.floor(0.025 * options.resamples)] ?? -1,
|
|
655
|
+
upper: draws[Math.min(options.resamples - 1, Math.ceil(0.975 * options.resamples) - 1)] ?? 1,
|
|
656
|
+
oneSidedLower: draws[Math.floor(0.05 * options.resamples)] ?? -1,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function comparisonKey(
|
|
661
|
+
candidateRetrieverId: string,
|
|
662
|
+
metric: EvaluationQualityMetricV2,
|
|
663
|
+
sliceId: EvaluationAnalysisSliceV2["id"],
|
|
664
|
+
): string {
|
|
665
|
+
return `${candidateRetrieverId}\0${metric}\0${sliceId}`;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function pairedEffect(
|
|
669
|
+
options: Readonly<{
|
|
670
|
+
readonly baselineRetrieverId: string;
|
|
671
|
+
readonly candidateRetrieverId: string;
|
|
672
|
+
readonly metric: EvaluationQualityMetricV2;
|
|
673
|
+
readonly slice: EvaluationAnalysisSliceV2;
|
|
674
|
+
readonly queries: readonly EvaluationQueryV2[];
|
|
675
|
+
readonly baselineScores: ReadonlyMap<string, QueryScore>;
|
|
676
|
+
readonly candidateScores: ReadonlyMap<string, QueryScore>;
|
|
677
|
+
readonly suiteSha256: string;
|
|
678
|
+
readonly bootstrapResamples: number;
|
|
679
|
+
}>,
|
|
680
|
+
): EvaluationPairedEffectV2 | undefined {
|
|
681
|
+
const eligibleQueries = options.queries
|
|
682
|
+
.filter((query) => queryInSlice(query, options.slice) && metricEligible(query, options.metric));
|
|
683
|
+
const pairs = eligibleQueries.flatMap((query) => {
|
|
684
|
+
const baselineScore = options.baselineScores.get(query.id);
|
|
685
|
+
const candidateScore = options.candidateScores.get(query.id);
|
|
686
|
+
const baseline = baselineScore?.metrics[options.metric];
|
|
687
|
+
const candidate = candidateScore?.metrics[options.metric];
|
|
688
|
+
return baselineScore === undefined
|
|
689
|
+
|| baseline === null
|
|
690
|
+
|| baseline === undefined
|
|
691
|
+
|| candidate === null
|
|
692
|
+
|| candidate === undefined
|
|
693
|
+
? []
|
|
694
|
+
: [{ baseline, candidate, clusterId: baselineScore.sourceFamilyClusterId }];
|
|
695
|
+
});
|
|
696
|
+
if (pairs.length === 0) return undefined;
|
|
697
|
+
const favorable = favorablePairs(options.metric, pairs);
|
|
698
|
+
const sourceFamilyClusters = new Set(pairs.map(({ clusterId }) => clusterId)).size;
|
|
699
|
+
const interval = clusteredPairedBootstrapConfidenceInterval(favorable, {
|
|
700
|
+
seed: evaluationAnalysisBootstrapSeedV2(
|
|
701
|
+
options.suiteSha256,
|
|
702
|
+
`${options.baselineRetrieverId}->${options.candidateRetrieverId}`,
|
|
703
|
+
options.metric,
|
|
704
|
+
options.slice.id,
|
|
705
|
+
),
|
|
706
|
+
resamples: options.bootstrapResamples,
|
|
707
|
+
});
|
|
708
|
+
return Object.freeze({
|
|
709
|
+
baselineRetrieverId: options.baselineRetrieverId,
|
|
710
|
+
candidateRetrieverId: options.candidateRetrieverId,
|
|
711
|
+
metric: options.metric,
|
|
712
|
+
slice: options.slice,
|
|
713
|
+
direction: options.metric === "false-abstention-rate" ? "lower-is-better" as const : "higher-is-better" as const,
|
|
714
|
+
eligibleQueries: eligibleQueries.length,
|
|
715
|
+
observedPairs: pairs.length,
|
|
716
|
+
sourceFamilyClusters,
|
|
717
|
+
inferenceStatus: sourceFamilyClusters >= 2 ? "estimable" as const : "insufficient-clusters" as const,
|
|
718
|
+
baselineMean: meanRequired(pairs.map(({ baseline }) => baseline), "Baseline paired mean"),
|
|
719
|
+
candidateMean: meanRequired(pairs.map(({ candidate }) => candidate), "Candidate paired mean"),
|
|
720
|
+
favorableInterval: Object.freeze({
|
|
721
|
+
pairs: interval.pairs,
|
|
722
|
+
seed: interval.seed,
|
|
723
|
+
resamples: interval.resamples,
|
|
724
|
+
confidence: interval.confidence,
|
|
725
|
+
observedDifference: interval.observedDifference,
|
|
726
|
+
lower: interval.lower,
|
|
727
|
+
upper: interval.upper,
|
|
728
|
+
}),
|
|
729
|
+
favorableOneSidedLower: interval.oneSidedLower,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function reasonKey(reason: EvaluationCandidateGateReasonV2): string {
|
|
734
|
+
return [
|
|
735
|
+
reason.code,
|
|
736
|
+
reason.retrieverId ?? "",
|
|
737
|
+
reason.metric ?? "",
|
|
738
|
+
reason.sliceId ?? "",
|
|
739
|
+
reason.profileId ?? "",
|
|
740
|
+
].join("\0");
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function canonicalReasons(
|
|
744
|
+
reasons: readonly EvaluationCandidateGateReasonV2[],
|
|
745
|
+
): readonly EvaluationCandidateGateReasonV2[] {
|
|
746
|
+
const byKey = new Map<string, EvaluationCandidateGateReasonV2>();
|
|
747
|
+
for (const reason of reasons) byKey.set(reasonKey(reason), reason);
|
|
748
|
+
return Object.freeze([...byKey.values()].toSorted((left, right) => reasonKey(left).localeCompare(reasonKey(right))));
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function variantComplexity(
|
|
752
|
+
descriptor: RetrievalEvaluationCorpusV2["retrievers"][number],
|
|
753
|
+
): EvaluationVariantComplexityV2 {
|
|
754
|
+
const activeConfigurationEntries = Object.values(descriptor.configuration).filter((value) =>
|
|
755
|
+
value !== null && value !== false && value !== 0 && value !== "none").length;
|
|
756
|
+
return Object.freeze({ laneCount: descriptor.lanes.length, activeConfigurationEntries });
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function compareVariantComplexity(
|
|
760
|
+
left: { readonly retrieverId: string; readonly score: EvaluationVariantComplexityV2 },
|
|
761
|
+
right: { readonly retrieverId: string; readonly score: EvaluationVariantComplexityV2 },
|
|
762
|
+
): number {
|
|
763
|
+
return compareVariantComplexityScore(left.score, right.score)
|
|
764
|
+
|| left.retrieverId.localeCompare(right.retrieverId);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function compareVariantComplexityScore(
|
|
768
|
+
left: EvaluationVariantComplexityV2,
|
|
769
|
+
right: EvaluationVariantComplexityV2,
|
|
770
|
+
): number {
|
|
771
|
+
return left.laneCount - right.laneCount
|
|
772
|
+
|| left.activeConfigurationEntries - right.activeConfigurationEntries;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function isStrictlySimplerVariant(
|
|
776
|
+
baseline: { readonly score: EvaluationVariantComplexityV2 },
|
|
777
|
+
candidate: { readonly score: EvaluationVariantComplexityV2 },
|
|
778
|
+
): boolean {
|
|
779
|
+
return compareVariantComplexityScore(baseline.score, candidate.score) < 0;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function validateOptions(options: AnalyzeRetrievalEvaluationV2Options): {
|
|
783
|
+
readonly cutoff: number;
|
|
784
|
+
readonly bootstrapResamples: number;
|
|
785
|
+
} {
|
|
786
|
+
const cutoff = options.cutoff ?? DEFAULT_EVALUATION_ANALYSIS_CUTOFF_V2;
|
|
787
|
+
if (!Number.isSafeInteger(cutoff) || cutoff < 1 || cutoff > MAX_EVALUATION_V2_RESULTS_PER_LANE) {
|
|
788
|
+
throw new RangeError(`Analysis cutoff must be from 1 through ${MAX_EVALUATION_V2_RESULTS_PER_LANE}.`);
|
|
789
|
+
}
|
|
790
|
+
const bootstrapResamples = options.bootstrapResamples
|
|
791
|
+
?? DEFAULT_EVALUATION_ANALYSIS_BOOTSTRAP_RESAMPLES_V2;
|
|
792
|
+
if (
|
|
793
|
+
!Number.isSafeInteger(bootstrapResamples)
|
|
794
|
+
|| bootstrapResamples < 100
|
|
795
|
+
|| bootstrapResamples > MAX_EVALUATION_ANALYSIS_BOOTSTRAP_RESAMPLES_V2
|
|
796
|
+
) {
|
|
797
|
+
throw new RangeError(
|
|
798
|
+
`Analysis bootstrap resamples must be from 100 through ${MAX_EVALUATION_ANALYSIS_BOOTSTRAP_RESAMPLES_V2}.`,
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
return Object.freeze({ cutoff, bootstrapResamples });
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Analyze a strict v2 report without re-running retrieval or inspecting hidden authoring keys.
|
|
806
|
+
* Repetitions are averaged within each retriever/query cluster before any paired inference.
|
|
807
|
+
*/
|
|
808
|
+
export function analyzeRetrievalEvaluationV2(
|
|
809
|
+
corpus: RetrievalEvaluationCorpusV2,
|
|
810
|
+
report: RetrievalEvaluationReportV2,
|
|
811
|
+
options: AnalyzeRetrievalEvaluationV2Options = {},
|
|
812
|
+
): RetrievalEvaluationAnalysisV2 {
|
|
813
|
+
const parsedCorpus = parseRetrievalEvaluationCorpusV2(corpus, { claimPromotion: false });
|
|
814
|
+
const parsedReport = parseRetrievalEvaluationReportV2(report, parsedCorpus);
|
|
815
|
+
return analyzeParsedRetrievalEvaluationV2(parsedCorpus, parsedReport, options);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function analyzeParsedRetrievalEvaluationV2(
|
|
819
|
+
corpus: RetrievalEvaluationCorpusV2,
|
|
820
|
+
report: RetrievalEvaluationReportV2,
|
|
821
|
+
options: AnalyzeRetrievalEvaluationV2Options,
|
|
822
|
+
): RetrievalEvaluationAnalysisV2 {
|
|
823
|
+
const { cutoff, bootstrapResamples } = validateOptions(options);
|
|
824
|
+
if (report.schemaVersion !== 2 || corpus.schemaVersion !== 2) {
|
|
825
|
+
throw new TypeError("Analysis requires strict v2 corpus and report inputs.");
|
|
826
|
+
}
|
|
827
|
+
if (
|
|
828
|
+
report.suiteSha256 !== corpus.manifest.corpusSha256
|
|
829
|
+
|| report.candidateLockSha256 !== corpus.manifest.candidateLockSha256
|
|
830
|
+
) throw new TypeError("Analysis report commitments do not match the sealed corpus.");
|
|
831
|
+
if (corpus.queries.length > MAX_EVALUATION_V2_QUERIES || report.samples.length > MAX_EVALUATION_V2_SAMPLES) {
|
|
832
|
+
throw new RangeError("Analysis input exceeds the v2 corpus or report work bound.");
|
|
833
|
+
}
|
|
834
|
+
const queries = corpus.queries.filter((query) => report.split === "all" || query.split === report.split);
|
|
835
|
+
if (queries.length === 0) throw new TypeError("Analysis report split contains no queries.");
|
|
836
|
+
const descriptorIds = new Set(corpus.retrievers.map(({ id }) => id));
|
|
837
|
+
const profileById = new Map(corpus.measurementProfiles.map((profile) => [profile.id, profile]));
|
|
838
|
+
for (const sample of report.samples) {
|
|
839
|
+
if (!descriptorIds.has(sample.retrieverId)) {
|
|
840
|
+
throw new TypeError(`Analysis sample names unknown retriever ${sample.retrieverId}.`);
|
|
841
|
+
}
|
|
842
|
+
if (!profileById.has(sample.profileId)) {
|
|
843
|
+
throw new TypeError(`Analysis sample names unknown profile ${sample.profileId}.`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
const baselineRetrieverId = corpus.candidateLock.baselineRetrieverId;
|
|
847
|
+
if (!descriptorIds.has(baselineRetrieverId)) throw new TypeError("Sealed baseline retriever is missing.");
|
|
848
|
+
const alternativeRetrieverIds = corpus.retrievers
|
|
849
|
+
.filter(({ id }) => id !== baselineRetrieverId)
|
|
850
|
+
.map(({ id }) => id);
|
|
851
|
+
for (const candidateRetrieverId of alternativeRetrieverIds) {
|
|
852
|
+
if (!descriptorIds.has(candidateRetrieverId)) {
|
|
853
|
+
throw new TypeError(`Sealed candidate retriever ${candidateRetrieverId} is missing.`);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const warmProfiles = corpus.measurementProfiles.filter(({ operation }) => operation === "warm-query");
|
|
858
|
+
const warmProfile = warmProfiles.length === 1 ? warmProfiles[0] : undefined;
|
|
859
|
+
const qualityProfiles = corpus.measurementProfiles.filter(({ operation }) => operation === "packing");
|
|
860
|
+
const qualityProfile = qualityProfiles.length === 1 ? qualityProfiles[0] : undefined;
|
|
861
|
+
const evidenceById = new Map(corpus.evidenceUnits.map((evidence) => [evidence.id, evidence]));
|
|
862
|
+
const clusterIdByQuery = evaluationSourceFamilyClusterIdsV2(
|
|
863
|
+
queries,
|
|
864
|
+
corpus.documents,
|
|
865
|
+
corpus.evidenceUnits,
|
|
866
|
+
corpus.sourceFamilies,
|
|
867
|
+
);
|
|
868
|
+
const groupedSamples = new Map<string, EvaluationRepeatedSampleV2[]>();
|
|
869
|
+
const profileSamples = new Map<string, EvaluationRepeatedSampleV2[]>();
|
|
870
|
+
const faultsByRetriever = new Map<string, RetrieverFaults>();
|
|
871
|
+
for (const { id } of corpus.retrievers) {
|
|
872
|
+
faultsByRetriever.set(id, {
|
|
873
|
+
failedSamples: 0,
|
|
874
|
+
unavailableWarmQuerySamples: 0,
|
|
875
|
+
unavailableQualitySamples: 0,
|
|
876
|
+
missingQualityObservations: 0,
|
|
877
|
+
nonzeroLlmAccountingSamples: 0,
|
|
878
|
+
acceptedEvidenceUnits: 0,
|
|
879
|
+
provenanceCoveredEvidenceUnits: 0,
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
for (const sample of report.samples) {
|
|
883
|
+
const key = sampleGroupKey(sample.retrieverId, sample.profileId, sample.queryId);
|
|
884
|
+
const group = groupedSamples.get(key) ?? [];
|
|
885
|
+
group.push(sample);
|
|
886
|
+
groupedSamples.set(key, group);
|
|
887
|
+
const profileKey = `${sample.retrieverId}\0${sample.profileId}`;
|
|
888
|
+
const profileGroup = profileSamples.get(profileKey) ?? [];
|
|
889
|
+
profileGroup.push(sample);
|
|
890
|
+
profileSamples.set(profileKey, profileGroup);
|
|
891
|
+
const faults = faultsByRetriever.get(sample.retrieverId);
|
|
892
|
+
if (faults === undefined) continue;
|
|
893
|
+
if (sample.status === "failed") faults.failedSamples += 1;
|
|
894
|
+
if (warmProfile !== undefined && sample.profileId === warmProfile.id && sample.status === "unavailable") {
|
|
895
|
+
faults.unavailableWarmQuerySamples += 1;
|
|
896
|
+
}
|
|
897
|
+
if (qualityProfile !== undefined && sample.profileId === qualityProfile.id && sample.status === "unavailable") {
|
|
898
|
+
faults.unavailableQualitySamples += 1;
|
|
899
|
+
}
|
|
900
|
+
const llm = sample.resources.llm as { readonly calls: number; readonly inputTokens: number; readonly outputTokens: number };
|
|
901
|
+
if (llm.calls !== 0 || llm.inputTokens !== 0 || llm.outputTokens !== 0) {
|
|
902
|
+
faults.nonzeroLlmAccountingSamples += 1;
|
|
903
|
+
}
|
|
904
|
+
if (qualityProfile !== undefined && sample.profileId === qualityProfile.id) {
|
|
905
|
+
const provenance = countPackedContextProvenanceV2(sample);
|
|
906
|
+
faults.acceptedEvidenceUnits += provenance.packed;
|
|
907
|
+
faults.provenanceCoveredEvidenceUnits += provenance.covered;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
for (const group of groupedSamples.values()) group.sort((left, right) => left.repetition - right.repetition);
|
|
911
|
+
|
|
912
|
+
const scoresByRetriever = new Map<string, ReadonlyMap<string, QueryScore>>();
|
|
913
|
+
for (const { id: retrieverId } of corpus.retrievers) {
|
|
914
|
+
const scores = new Map<string, QueryScore>();
|
|
915
|
+
if (qualityProfile !== undefined) {
|
|
916
|
+
for (const query of queries) {
|
|
917
|
+
const samples = groupedSamples.get(sampleGroupKey(retrieverId, qualityProfile.id, query.id)) ?? [];
|
|
918
|
+
const completeRepetitions = samples.length === qualityProfile.repetitions
|
|
919
|
+
&& samples.every((sample, index) => sample.repetition === index + 1);
|
|
920
|
+
const faults = faultsByRetriever.get(retrieverId);
|
|
921
|
+
if (!completeRepetitions) {
|
|
922
|
+
if (faults !== undefined) faults.missingQualityObservations += 1;
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
if (samples.some(({ status }) => status === "failed" || status === "unavailable")) continue;
|
|
926
|
+
const sourceFamilyClusterId = clusterIdByQuery.get(query.id);
|
|
927
|
+
if (sourceFamilyClusterId === undefined) {
|
|
928
|
+
throw new TypeError(`Analysis lost source-family cluster for query ${query.id}.`);
|
|
929
|
+
}
|
|
930
|
+
scores.set(query.id, aggregateRepetitions(
|
|
931
|
+
query,
|
|
932
|
+
samples,
|
|
933
|
+
cutoff,
|
|
934
|
+
evidenceById,
|
|
935
|
+
sourceFamilyClusterId,
|
|
936
|
+
));
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
scoresByRetriever.set(retrieverId, scores);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const slices = analysisSlices(queries);
|
|
943
|
+
const retrievers = corpus.retrievers.map(({ id: retrieverId }): EvaluationRetrieverQualitySummaryV2 => {
|
|
944
|
+
const faults = faultsByRetriever.get(retrieverId);
|
|
945
|
+
if (faults === undefined) throw new TypeError(`Analysis lost retriever ${retrieverId}.`);
|
|
946
|
+
return Object.freeze({
|
|
947
|
+
retrieverId,
|
|
948
|
+
queryCount: queries.length,
|
|
949
|
+
slices: Object.freeze(slices.map((slice) =>
|
|
950
|
+
sliceSummary(slice, queries, scoresByRetriever.get(retrieverId) ?? new Map()))),
|
|
951
|
+
acceptedEvidenceUnits: faults.acceptedEvidenceUnits,
|
|
952
|
+
provenanceCoveredEvidenceUnits: faults.provenanceCoveredEvidenceUnits,
|
|
953
|
+
provenanceCoverage: faults.acceptedEvidenceUnits === 0
|
|
954
|
+
? 1
|
|
955
|
+
: faults.provenanceCoveredEvidenceUnits / faults.acceptedEvidenceUnits,
|
|
956
|
+
failedSamples: faults.failedSamples,
|
|
957
|
+
unavailableWarmQuerySamples: faults.unavailableWarmQuerySamples,
|
|
958
|
+
unavailableQualitySamples: faults.unavailableQualitySamples,
|
|
959
|
+
missingQualityObservations: faults.missingQualityObservations,
|
|
960
|
+
nonzeroLlmAccountingSamples: faults.nonzeroLlmAccountingSamples,
|
|
961
|
+
});
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
let bootstrapDraws = 0;
|
|
965
|
+
const pairedEffects: EvaluationPairedEffectV2[] = [];
|
|
966
|
+
const comparisonByKey = new Map<string, EvaluationPairedEffectV2>();
|
|
967
|
+
const baselineScores = scoresByRetriever.get(baselineRetrieverId) ?? new Map<string, QueryScore>();
|
|
968
|
+
const officialCandidateIds = new Set(corpus.candidateLock.candidateRetrieverIds);
|
|
969
|
+
for (const candidateRetrieverId of alternativeRetrieverIds) {
|
|
970
|
+
const candidateScores = scoresByRetriever.get(candidateRetrieverId) ?? new Map<string, QueryScore>();
|
|
971
|
+
for (const slice of slices) {
|
|
972
|
+
const slicedQueries = queries.filter((query) => queryInSlice(query, slice));
|
|
973
|
+
for (const metric of QUALITY_METRICS) {
|
|
974
|
+
const officialCandidate = officialCandidateIds.has(candidateRetrieverId);
|
|
975
|
+
const neededForAblationGate = (
|
|
976
|
+
slice.kind === "cohort"
|
|
977
|
+
&& corpus.experiment.protocol.minimumUsefulEffects.some((effect) =>
|
|
978
|
+
effect.metric === metric && effect.cohort === slice.cohort)
|
|
979
|
+
) || (
|
|
980
|
+
slice.kind === "cohort-primary-stratum"
|
|
981
|
+
&& (
|
|
982
|
+
metric === "decision-accuracy"
|
|
983
|
+
|| metric === "context-precision"
|
|
984
|
+
|| metric === "document-recall-at-k"
|
|
985
|
+
|| metric === "evidence-recall-at-k"
|
|
986
|
+
)
|
|
987
|
+
);
|
|
988
|
+
if (!officialCandidate && !neededForAblationGate) continue;
|
|
989
|
+
const comparison = pairedEffect({
|
|
990
|
+
baselineRetrieverId,
|
|
991
|
+
candidateRetrieverId,
|
|
992
|
+
metric,
|
|
993
|
+
slice,
|
|
994
|
+
queries: slicedQueries,
|
|
995
|
+
baselineScores,
|
|
996
|
+
candidateScores,
|
|
997
|
+
suiteSha256: report.suiteSha256,
|
|
998
|
+
bootstrapResamples,
|
|
999
|
+
});
|
|
1000
|
+
if (comparison === undefined) continue;
|
|
1001
|
+
bootstrapDraws += comparison.observedPairs * bootstrapResamples;
|
|
1002
|
+
if (bootstrapDraws > MAX_EVALUATION_ANALYSIS_BOOTSTRAP_DRAWS_V2) {
|
|
1003
|
+
throw new RangeError(
|
|
1004
|
+
`Analysis bootstrap would exceed ${MAX_EVALUATION_ANALYSIS_BOOTSTRAP_DRAWS_V2} paired draws.`,
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
pairedEffects.push(comparison);
|
|
1008
|
+
comparisonByKey.set(comparisonKey(candidateRetrieverId, metric, slice.id), comparison);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const latencyProfiles: EvaluationLatencyProfileSummaryV2[] = [];
|
|
1014
|
+
const promotionLatencyByKey = new Map<string, EvaluationLatencyProfileSummaryV2>();
|
|
1015
|
+
for (const { id: retrieverId } of corpus.retrievers) {
|
|
1016
|
+
for (const profile of corpus.measurementProfiles) {
|
|
1017
|
+
const samples = profileSamples.get(`${retrieverId}\0${profile.id}`) ?? [];
|
|
1018
|
+
const observedSamples = samples
|
|
1019
|
+
.filter(({ status }) => status === "ready" || status === "degraded");
|
|
1020
|
+
const values = observedSamples.map((sample) => operationDuration(sample, profile.operation));
|
|
1021
|
+
const summary = Object.freeze({
|
|
1022
|
+
retrieverId,
|
|
1023
|
+
profileId: profile.id,
|
|
1024
|
+
operation: profile.operation,
|
|
1025
|
+
cacheState: profile.cacheState,
|
|
1026
|
+
expectedObservations: expectedProfileObservations(profile, queries.length),
|
|
1027
|
+
observedObservations: values.length,
|
|
1028
|
+
p95Ms: percentile(values, 0.95),
|
|
1029
|
+
});
|
|
1030
|
+
latencyProfiles.push(summary);
|
|
1031
|
+
promotionLatencyByKey.set(`${retrieverId}\0${profile.id}`, Object.freeze({
|
|
1032
|
+
...summary,
|
|
1033
|
+
p95Ms: percentile(observedSamples.map((sample) =>
|
|
1034
|
+
promotionOperationDuration(sample, profile.operation)), 0.95),
|
|
1035
|
+
}));
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const retrieverSummaryById = new Map(retrievers.map((summary) => [summary.retrieverId, summary]));
|
|
1040
|
+
const retrieverGates = alternativeRetrieverIds.map((candidateRetrieverId): EvaluationCandidateGateV2 => {
|
|
1041
|
+
const reasons: EvaluationCandidateGateReasonV2[] = [];
|
|
1042
|
+
const checks: EvaluationCandidateGateCheckV2[] = [];
|
|
1043
|
+
const candidateScores = scoresByRetriever.get(candidateRetrieverId) ?? new Map<string, QueryScore>();
|
|
1044
|
+
if (qualityProfiles.length === 0) {
|
|
1045
|
+
reasons.push(Object.freeze({
|
|
1046
|
+
code: "missing-quality-profile",
|
|
1047
|
+
message: "The sealed corpus has no packing profile for packed-context quality inference.",
|
|
1048
|
+
}));
|
|
1049
|
+
} else if (qualityProfiles.length > 1) {
|
|
1050
|
+
reasons.push(Object.freeze({
|
|
1051
|
+
code: "missing-quality-profile",
|
|
1052
|
+
message: "The sealed corpus has more than one packing profile and analysis will not mix them.",
|
|
1053
|
+
count: qualityProfiles.length,
|
|
1054
|
+
}));
|
|
1055
|
+
}
|
|
1056
|
+
for (const retrieverId of [baselineRetrieverId, candidateRetrieverId]) {
|
|
1057
|
+
const summary = retrieverSummaryById.get(retrieverId);
|
|
1058
|
+
if (summary === undefined) continue;
|
|
1059
|
+
if (summary.failedSamples > 0) {
|
|
1060
|
+
reasons.push(Object.freeze({
|
|
1061
|
+
code: "failed-sample",
|
|
1062
|
+
message: `Retriever ${retrieverId} has failed samples.`,
|
|
1063
|
+
retrieverId,
|
|
1064
|
+
count: summary.failedSamples,
|
|
1065
|
+
}));
|
|
1066
|
+
}
|
|
1067
|
+
if (summary.unavailableWarmQuerySamples > 0) {
|
|
1068
|
+
reasons.push(Object.freeze({
|
|
1069
|
+
code: "unavailable-warm-query-sample",
|
|
1070
|
+
message: `Retriever ${retrieverId} has unavailable warm-query samples.`,
|
|
1071
|
+
retrieverId,
|
|
1072
|
+
count: summary.unavailableWarmQuerySamples,
|
|
1073
|
+
}));
|
|
1074
|
+
}
|
|
1075
|
+
if (summary.unavailableQualitySamples > 0) {
|
|
1076
|
+
reasons.push(Object.freeze({
|
|
1077
|
+
code: "unavailable-quality-sample",
|
|
1078
|
+
message: `Retriever ${retrieverId} has unavailable packed-context quality samples.`,
|
|
1079
|
+
retrieverId,
|
|
1080
|
+
count: summary.unavailableQualitySamples,
|
|
1081
|
+
}));
|
|
1082
|
+
}
|
|
1083
|
+
if (summary.missingQualityObservations > 0) {
|
|
1084
|
+
reasons.push(Object.freeze({
|
|
1085
|
+
code: "missing-eligible-observations",
|
|
1086
|
+
message: `Retriever ${retrieverId} is missing complete packing repetition clusters.`,
|
|
1087
|
+
retrieverId,
|
|
1088
|
+
count: summary.missingQualityObservations,
|
|
1089
|
+
}));
|
|
1090
|
+
}
|
|
1091
|
+
if (summary.nonzeroLlmAccountingSamples > 0) {
|
|
1092
|
+
reasons.push(Object.freeze({
|
|
1093
|
+
code: "nonzero-llm-accounting",
|
|
1094
|
+
message: `Retriever ${retrieverId} reports nonzero LLM accounting.`,
|
|
1095
|
+
retrieverId,
|
|
1096
|
+
count: summary.nonzeroLlmAccountingSamples,
|
|
1097
|
+
}));
|
|
1098
|
+
}
|
|
1099
|
+
if (summary.provenanceCoverage !== 1) {
|
|
1100
|
+
reasons.push(Object.freeze({
|
|
1101
|
+
code: "provenance-below-100-percent",
|
|
1102
|
+
message: `Retriever ${retrieverId} has provenance coverage below 100 percent.`,
|
|
1103
|
+
retrieverId,
|
|
1104
|
+
}));
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
for (const effect of corpus.experiment.protocol.minimumUsefulEffects) {
|
|
1109
|
+
const sliceId = `cohort:${effect.cohort}` as const;
|
|
1110
|
+
const comparison = comparisonByKey.get(comparisonKey(candidateRetrieverId, effect.metric, sliceId));
|
|
1111
|
+
const insufficientIndependentPairs = comparison !== undefined
|
|
1112
|
+
&& comparison.observedPairs === comparison.eligibleQueries
|
|
1113
|
+
&& comparison.sourceFamilyClusters < corpus.experiment.protocol.pairedPower.requiredPairs;
|
|
1114
|
+
if (
|
|
1115
|
+
comparison === undefined
|
|
1116
|
+
|| comparison.eligibleQueries === 0
|
|
1117
|
+
|| comparison.observedPairs !== comparison.eligibleQueries
|
|
1118
|
+
|| insufficientIndependentPairs
|
|
1119
|
+
|| comparison.inferenceStatus !== "estimable"
|
|
1120
|
+
) {
|
|
1121
|
+
checks.push(Object.freeze({
|
|
1122
|
+
kind: "minimum-useful-effect",
|
|
1123
|
+
metric: effect.metric,
|
|
1124
|
+
status: "not-evaluable",
|
|
1125
|
+
sliceId,
|
|
1126
|
+
...(comparison === undefined
|
|
1127
|
+
? {}
|
|
1128
|
+
: {
|
|
1129
|
+
eligibleQueries: comparison.eligibleQueries,
|
|
1130
|
+
observedPairs: comparison.observedPairs,
|
|
1131
|
+
sourceFamilyClusters: comparison.sourceFamilyClusters,
|
|
1132
|
+
}),
|
|
1133
|
+
requiredImprovement: effect.minimumAbsoluteDifference,
|
|
1134
|
+
}));
|
|
1135
|
+
reasons.push(insufficientIndependentPairs
|
|
1136
|
+
? Object.freeze({
|
|
1137
|
+
code: "insufficient-independent-pairs" as const,
|
|
1138
|
+
message: `Metric ${effect.metric} has ${comparison.sourceFamilyClusters} independent source-family pairs but requires ${corpus.experiment.protocol.pairedPower.requiredPairs} for ${sliceId}.`,
|
|
1139
|
+
metric: effect.metric,
|
|
1140
|
+
sliceId,
|
|
1141
|
+
count: comparison.sourceFamilyClusters,
|
|
1142
|
+
})
|
|
1143
|
+
: Object.freeze({
|
|
1144
|
+
code: "missing-eligible-observations" as const,
|
|
1145
|
+
message: `Metric ${effect.metric} lacks complete clustered pairs for ${sliceId}.`,
|
|
1146
|
+
metric: effect.metric,
|
|
1147
|
+
sliceId,
|
|
1148
|
+
}));
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
const passed = comparison.favorableOneSidedLower >= effect.minimumAbsoluteDifference;
|
|
1152
|
+
checks.push(Object.freeze({
|
|
1153
|
+
kind: "minimum-useful-effect",
|
|
1154
|
+
metric: effect.metric,
|
|
1155
|
+
status: passed ? "passed" : "failed",
|
|
1156
|
+
sliceId,
|
|
1157
|
+
eligibleQueries: comparison.eligibleQueries,
|
|
1158
|
+
observedPairs: comparison.observedPairs,
|
|
1159
|
+
sourceFamilyClusters: comparison.sourceFamilyClusters,
|
|
1160
|
+
observedEffect: comparison.favorableInterval.observedDifference,
|
|
1161
|
+
confidenceLower: comparison.favorableOneSidedLower,
|
|
1162
|
+
confidenceUpper: comparison.favorableInterval.upper,
|
|
1163
|
+
requiredImprovement: effect.minimumAbsoluteDifference,
|
|
1164
|
+
}));
|
|
1165
|
+
if (!passed) {
|
|
1166
|
+
reasons.push(Object.freeze({
|
|
1167
|
+
code: "minimum-useful-effect-not-met",
|
|
1168
|
+
message: `Metric ${effect.metric} does not clear its minimum useful effect for ${sliceId}.`,
|
|
1169
|
+
metric: effect.metric,
|
|
1170
|
+
sliceId,
|
|
1171
|
+
}));
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
for (const margin of corpus.experiment.protocol.nonInferiorityMargins) {
|
|
1176
|
+
const stratum = ACCURACY_STRATUM_BY_METRIC[margin.metric as keyof typeof ACCURACY_STRATUM_BY_METRIC];
|
|
1177
|
+
const qualityMetric = stratum === undefined && QUALITY_NONINFERIORITY_METRICS.has(margin.metric)
|
|
1178
|
+
? margin.metric as EvaluationQualityMetricV2
|
|
1179
|
+
: stratum === undefined
|
|
1180
|
+
? undefined
|
|
1181
|
+
: "decision-accuracy" as const;
|
|
1182
|
+
if (qualityMetric !== undefined) {
|
|
1183
|
+
const targetSlices: readonly Extract<
|
|
1184
|
+
EvaluationAnalysisSliceV2,
|
|
1185
|
+
{ readonly kind: "cohort-primary-stratum" }
|
|
1186
|
+
>[] = stratum === undefined
|
|
1187
|
+
? slices.filter((slice): slice is Extract<
|
|
1188
|
+
EvaluationAnalysisSliceV2,
|
|
1189
|
+
{ readonly kind: "cohort-primary-stratum" }
|
|
1190
|
+
> => slice.kind === "cohort-primary-stratum")
|
|
1191
|
+
: COHORTS.map((cohort) => Object.freeze({
|
|
1192
|
+
id: `cohort:${cohort}:primary-stratum:${stratum}` as const,
|
|
1193
|
+
kind: "cohort-primary-stratum" as const,
|
|
1194
|
+
cohort,
|
|
1195
|
+
primaryStratum: stratum,
|
|
1196
|
+
}));
|
|
1197
|
+
for (const targetSlice of targetSlices) {
|
|
1198
|
+
const sliceId = targetSlice.id;
|
|
1199
|
+
const guard = observedNoRegressionGuard({
|
|
1200
|
+
metric: qualityMetric,
|
|
1201
|
+
slice: targetSlice,
|
|
1202
|
+
queries,
|
|
1203
|
+
baselineScores,
|
|
1204
|
+
candidateScores,
|
|
1205
|
+
});
|
|
1206
|
+
if (guard.eligibleQueries === 0) continue;
|
|
1207
|
+
if (guard.observedPairs !== guard.eligibleQueries) {
|
|
1208
|
+
checks.push(Object.freeze({
|
|
1209
|
+
kind: "observed-no-regression",
|
|
1210
|
+
metric: margin.metric,
|
|
1211
|
+
status: "not-evaluable",
|
|
1212
|
+
sliceId,
|
|
1213
|
+
eligibleQueries: guard.eligibleQueries,
|
|
1214
|
+
observedPairs: guard.observedPairs,
|
|
1215
|
+
regressedPairs: guard.regressedPairs,
|
|
1216
|
+
}));
|
|
1217
|
+
reasons.push(Object.freeze({
|
|
1218
|
+
code: "missing-eligible-observations",
|
|
1219
|
+
message: `Metric ${margin.metric} lacks complete paired observations for the observed zero-regression guard in ${sliceId}.`,
|
|
1220
|
+
metric: margin.metric,
|
|
1221
|
+
sliceId,
|
|
1222
|
+
}));
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
const passed = guard.regressedPairs === 0;
|
|
1226
|
+
checks.push(Object.freeze({
|
|
1227
|
+
kind: "observed-no-regression",
|
|
1228
|
+
metric: margin.metric,
|
|
1229
|
+
status: passed ? "passed" : "failed",
|
|
1230
|
+
sliceId,
|
|
1231
|
+
...(guard.observedEffect === null ? {} : { observedEffect: guard.observedEffect }),
|
|
1232
|
+
eligibleQueries: guard.eligibleQueries,
|
|
1233
|
+
observedPairs: guard.observedPairs,
|
|
1234
|
+
regressedPairs: guard.regressedPairs,
|
|
1235
|
+
}));
|
|
1236
|
+
if (!passed) {
|
|
1237
|
+
reasons.push(Object.freeze({
|
|
1238
|
+
code: "observed-query-regression",
|
|
1239
|
+
message: `Metric ${margin.metric} has ${guard.regressedPairs} observed paired-query regressions for ${sliceId}.`,
|
|
1240
|
+
metric: margin.metric,
|
|
1241
|
+
sliceId,
|
|
1242
|
+
count: guard.regressedPairs,
|
|
1243
|
+
}));
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
const operation = LATENCY_OPERATION_BY_METRIC[margin.metric as keyof typeof LATENCY_OPERATION_BY_METRIC];
|
|
1249
|
+
if (operation === undefined) continue;
|
|
1250
|
+
const profiles = corpus.measurementProfiles.filter((profile) => profile.operation === operation);
|
|
1251
|
+
if (profiles.length === 0) {
|
|
1252
|
+
checks.push(Object.freeze({
|
|
1253
|
+
kind: "noninferiority-latency",
|
|
1254
|
+
metric: margin.metric,
|
|
1255
|
+
status: "not-evaluable",
|
|
1256
|
+
}));
|
|
1257
|
+
reasons.push(Object.freeze({
|
|
1258
|
+
code: "missing-eligible-observations",
|
|
1259
|
+
message: `Metric ${margin.metric} has no matching sealed operation profile.`,
|
|
1260
|
+
metric: margin.metric,
|
|
1261
|
+
}));
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
for (const profile of profiles) {
|
|
1265
|
+
const baseline = promotionLatencyByKey.get(`${baselineRetrieverId}\0${profile.id}`);
|
|
1266
|
+
const candidate = promotionLatencyByKey.get(`${candidateRetrieverId}\0${profile.id}`);
|
|
1267
|
+
if (
|
|
1268
|
+
baseline?.p95Ms === null
|
|
1269
|
+
|| baseline?.p95Ms === undefined
|
|
1270
|
+
|| candidate?.p95Ms === null
|
|
1271
|
+
|| candidate?.p95Ms === undefined
|
|
1272
|
+
|| baseline.observedObservations !== baseline.expectedObservations
|
|
1273
|
+
|| candidate.observedObservations !== candidate.expectedObservations
|
|
1274
|
+
) {
|
|
1275
|
+
checks.push(Object.freeze({
|
|
1276
|
+
kind: "noninferiority-latency",
|
|
1277
|
+
metric: margin.metric,
|
|
1278
|
+
status: "not-evaluable",
|
|
1279
|
+
profileId: profile.id,
|
|
1280
|
+
}));
|
|
1281
|
+
reasons.push(Object.freeze({
|
|
1282
|
+
code: "missing-eligible-observations",
|
|
1283
|
+
message: `Metric ${margin.metric} lacks complete observations for profile ${profile.id}.`,
|
|
1284
|
+
metric: margin.metric,
|
|
1285
|
+
profileId: profile.id,
|
|
1286
|
+
}));
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
const allowedRegression = Math.max(
|
|
1290
|
+
margin.maximumAbsoluteRegression,
|
|
1291
|
+
baseline.p95Ms * margin.maximumRelativeRegression,
|
|
1292
|
+
);
|
|
1293
|
+
const observedEffect = candidate.p95Ms - baseline.p95Ms;
|
|
1294
|
+
const passed = observedEffect <= allowedRegression;
|
|
1295
|
+
checks.push(Object.freeze({
|
|
1296
|
+
kind: "noninferiority-latency",
|
|
1297
|
+
metric: margin.metric,
|
|
1298
|
+
status: passed ? "passed" : "failed",
|
|
1299
|
+
profileId: profile.id,
|
|
1300
|
+
baselineObserved: baseline.p95Ms,
|
|
1301
|
+
candidateObserved: candidate.p95Ms,
|
|
1302
|
+
observedEffect,
|
|
1303
|
+
allowedRegression,
|
|
1304
|
+
durationScope: operation === "packing"
|
|
1305
|
+
? "context-ready-elapsed"
|
|
1306
|
+
: "query-operation",
|
|
1307
|
+
}));
|
|
1308
|
+
if (!passed) {
|
|
1309
|
+
reasons.push(Object.freeze({
|
|
1310
|
+
code: "noninferiority-margin-exceeded",
|
|
1311
|
+
message: `Metric ${margin.metric} exceeds its noninferiority allowance for profile ${profile.id}.`,
|
|
1312
|
+
metric: margin.metric,
|
|
1313
|
+
profileId: profile.id,
|
|
1314
|
+
}));
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
const canonical = canonicalReasons(reasons);
|
|
1319
|
+
return Object.freeze({
|
|
1320
|
+
baselineRetrieverId,
|
|
1321
|
+
candidateRetrieverId,
|
|
1322
|
+
passed: canonical.length === 0 && checks.every(({ status }) => status === "passed"),
|
|
1323
|
+
checks: Object.freeze(checks),
|
|
1324
|
+
reasons: canonical,
|
|
1325
|
+
});
|
|
1326
|
+
});
|
|
1327
|
+
const candidateGates = retrieverGates.filter(({ candidateRetrieverId }) =>
|
|
1328
|
+
officialCandidateIds.has(candidateRetrieverId));
|
|
1329
|
+
const gateByRetrieverId = new Map(retrieverGates.map((gate) => [gate.candidateRetrieverId, gate]));
|
|
1330
|
+
const complexity = corpus.retrievers
|
|
1331
|
+
.filter((descriptor): descriptor is typeof descriptor & { readonly role: "ablation" | "candidate" } =>
|
|
1332
|
+
descriptor.role === "ablation" || descriptor.role === "candidate")
|
|
1333
|
+
.map((descriptor) => Object.freeze({
|
|
1334
|
+
retrieverId: descriptor.id,
|
|
1335
|
+
role: descriptor.role,
|
|
1336
|
+
score: variantComplexity(descriptor),
|
|
1337
|
+
}))
|
|
1338
|
+
.toSorted(compareVariantComplexity);
|
|
1339
|
+
const passingRetrieverIds = complexity
|
|
1340
|
+
.filter(({ retrieverId }) => gateByRetrieverId.get(retrieverId)?.passed === true)
|
|
1341
|
+
.map(({ retrieverId }) => retrieverId);
|
|
1342
|
+
const passingComplexity = complexity.filter(({ retrieverId }) =>
|
|
1343
|
+
gateByRetrieverId.get(retrieverId)?.passed === true);
|
|
1344
|
+
const incrementalChecks: EvaluationVariantIncrementalCheckV2[] = [];
|
|
1345
|
+
let selectedRetrieverId: string | null = null;
|
|
1346
|
+
for (const candidate of passingComplexity) {
|
|
1347
|
+
if (candidate.role !== "candidate" || !officialCandidateIds.has(candidate.retrieverId)) continue;
|
|
1348
|
+
const simpler = passingComplexity.filter((baseline) =>
|
|
1349
|
+
isStrictlySimplerVariant(baseline, candidate));
|
|
1350
|
+
if (simpler.length === 0) {
|
|
1351
|
+
selectedRetrieverId = candidate.retrieverId;
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1354
|
+
let clearsEverySimplerVariant = true;
|
|
1355
|
+
for (const baseline of simpler) {
|
|
1356
|
+
for (const effect of corpus.experiment.protocol.minimumUsefulEffects) {
|
|
1357
|
+
const sliceId = `cohort:${effect.cohort}` as const;
|
|
1358
|
+
const slice = slices.find((entry) => entry.id === sliceId);
|
|
1359
|
+
if (slice === undefined) throw new Error(`Analysis lost incremental slice ${sliceId}.`);
|
|
1360
|
+
const comparison = pairedEffect({
|
|
1361
|
+
baselineRetrieverId: baseline.retrieverId,
|
|
1362
|
+
candidateRetrieverId: candidate.retrieverId,
|
|
1363
|
+
metric: effect.metric,
|
|
1364
|
+
slice,
|
|
1365
|
+
queries,
|
|
1366
|
+
baselineScores: scoresByRetriever.get(baseline.retrieverId) ?? new Map(),
|
|
1367
|
+
candidateScores: scoresByRetriever.get(candidate.retrieverId) ?? new Map(),
|
|
1368
|
+
suiteSha256: report.suiteSha256,
|
|
1369
|
+
bootstrapResamples,
|
|
1370
|
+
});
|
|
1371
|
+
if (comparison !== undefined) {
|
|
1372
|
+
bootstrapDraws += comparison.observedPairs * bootstrapResamples;
|
|
1373
|
+
if (bootstrapDraws > MAX_EVALUATION_ANALYSIS_BOOTSTRAP_DRAWS_V2) {
|
|
1374
|
+
throw new RangeError(
|
|
1375
|
+
`Analysis bootstrap would exceed ${MAX_EVALUATION_ANALYSIS_BOOTSTRAP_DRAWS_V2} paired draws.`,
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
pairedEffects.push(comparison);
|
|
1379
|
+
}
|
|
1380
|
+
const completeAndPowered = comparison !== undefined
|
|
1381
|
+
&& comparison.eligibleQueries > 0
|
|
1382
|
+
&& comparison.observedPairs === comparison.eligibleQueries
|
|
1383
|
+
&& comparison.sourceFamilyClusters >= corpus.experiment.protocol.pairedPower.requiredPairs
|
|
1384
|
+
&& comparison.inferenceStatus === "estimable";
|
|
1385
|
+
const passed = completeAndPowered
|
|
1386
|
+
&& comparison.favorableOneSidedLower >= effect.minimumAbsoluteDifference;
|
|
1387
|
+
const status = !completeAndPowered ? "not-evaluable" as const : passed ? "passed" as const : "failed" as const;
|
|
1388
|
+
incrementalChecks.push(Object.freeze({
|
|
1389
|
+
baselineRetrieverId: baseline.retrieverId,
|
|
1390
|
+
candidateRetrieverId: candidate.retrieverId,
|
|
1391
|
+
metric: effect.metric,
|
|
1392
|
+
sliceId,
|
|
1393
|
+
status,
|
|
1394
|
+
eligibleQueries: comparison?.eligibleQueries ?? queries.filter((query) =>
|
|
1395
|
+
queryInSlice(query, slice) && metricEligible(query, effect.metric)).length,
|
|
1396
|
+
observedPairs: comparison?.observedPairs ?? 0,
|
|
1397
|
+
sourceFamilyClusters: comparison?.sourceFamilyClusters ?? 0,
|
|
1398
|
+
requiredImprovement: effect.minimumAbsoluteDifference,
|
|
1399
|
+
...(comparison === undefined ? {} : {
|
|
1400
|
+
observedEffect: comparison.favorableInterval.observedDifference,
|
|
1401
|
+
confidenceLower: comparison.favorableOneSidedLower,
|
|
1402
|
+
confidenceUpper: comparison.favorableInterval.upper,
|
|
1403
|
+
}),
|
|
1404
|
+
}));
|
|
1405
|
+
if (!passed) clearsEverySimplerVariant = false;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
if (clearsEverySimplerVariant) selectedRetrieverId = candidate.retrieverId;
|
|
1409
|
+
}
|
|
1410
|
+
const variantSelection = Object.freeze({
|
|
1411
|
+
baselineRetrieverId,
|
|
1412
|
+
orderedRetrieverIds: Object.freeze(complexity.map(({ retrieverId }) => retrieverId)),
|
|
1413
|
+
passingRetrieverIds: Object.freeze(passingRetrieverIds),
|
|
1414
|
+
selectedRetrieverId,
|
|
1415
|
+
incrementalChecks: Object.freeze(incrementalChecks),
|
|
1416
|
+
complexity: Object.freeze(complexity),
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
return Object.freeze({
|
|
1420
|
+
schemaVersion: 2,
|
|
1421
|
+
suiteSha256: report.suiteSha256,
|
|
1422
|
+
candidateLockSha256: report.candidateLockSha256,
|
|
1423
|
+
split: report.split,
|
|
1424
|
+
cutoff,
|
|
1425
|
+
bootstrap: Object.freeze({
|
|
1426
|
+
confidence: 0.95 as const,
|
|
1427
|
+
resamples: bootstrapResamples,
|
|
1428
|
+
draws: bootstrapDraws,
|
|
1429
|
+
}),
|
|
1430
|
+
warmQueryProfileId: warmProfile?.id ?? null,
|
|
1431
|
+
qualityProfileId: qualityProfile?.id ?? null,
|
|
1432
|
+
slices,
|
|
1433
|
+
retrievers: Object.freeze(retrievers),
|
|
1434
|
+
pairedEffects: Object.freeze(pairedEffects),
|
|
1435
|
+
latencyProfiles: Object.freeze(latencyProfiles),
|
|
1436
|
+
candidateGates: Object.freeze(candidateGates),
|
|
1437
|
+
retrieverGates: Object.freeze(retrieverGates),
|
|
1438
|
+
variantSelection,
|
|
1439
|
+
});
|
|
1440
|
+
}
|