@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,2052 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
EVALUATION_EVIDENCE_PARSER_VERSION,
|
|
5
|
+
buildEvaluationEvidenceRegistry,
|
|
6
|
+
type EvaluationEvidenceByteRange,
|
|
7
|
+
type EvaluationEvidenceUnit,
|
|
8
|
+
type EvaluationEvidenceLineRange,
|
|
9
|
+
type EvaluationEvidenceRegistry,
|
|
10
|
+
type EvaluationEvidenceUnitKind,
|
|
11
|
+
} from "./evaluation-evidence.js";
|
|
12
|
+
import {
|
|
13
|
+
EVALUATION_SOURCE_TRUST_COMPATIBILITY_V2,
|
|
14
|
+
PROMOTION_ACCEPTANCE_STRATUM_COHORT_MINIMA_V2,
|
|
15
|
+
PROMOTION_ACCEPTANCE_STRATUM_MINIMA_V2,
|
|
16
|
+
PROMOTION_COHORT_COUNT_V2,
|
|
17
|
+
PROMOTION_CRITICAL_INPUT_MINIMA_V2,
|
|
18
|
+
PROMOTION_CRITICAL_STRATUM_MINIMA_V2,
|
|
19
|
+
PROMOTION_DEVELOPMENT_QUERY_COUNT_V2,
|
|
20
|
+
PROMOTION_DUAL_ASSESSMENT_MINIMUM_V2,
|
|
21
|
+
PROMOTION_EVALUATION_QUERY_COUNT_V2,
|
|
22
|
+
PROMOTION_TEST_COHORT_COUNT_V2,
|
|
23
|
+
PROMOTION_TEST_INSUFFICIENT_COUNT_V2,
|
|
24
|
+
PROMOTION_TEST_QUERY_COUNT_V2,
|
|
25
|
+
PROMOTION_TEST_SUPPORTED_COUNT_V2,
|
|
26
|
+
PROMOTION_STRATUM_COHORT_DUAL_FRACTION_V2,
|
|
27
|
+
PROMOTION_STRATUM_COHORT_DUAL_MINIMUM_V2,
|
|
28
|
+
RETRIEVAL_EVALUATION_V2_PROTOCOL,
|
|
29
|
+
RETRIEVAL_EVALUATION_V2_SCHEMA_VERSION,
|
|
30
|
+
evaluationCandidateLockDigestV2,
|
|
31
|
+
evaluationCorpusDigestV2,
|
|
32
|
+
evaluationRetrieverDescriptorDigestV2,
|
|
33
|
+
evaluationSourceFamilyClusterIdsV2,
|
|
34
|
+
parseRetrievalEvaluationCorpusV2,
|
|
35
|
+
validatePromotionCorpusV2,
|
|
36
|
+
type EvaluationAdjudicationV2,
|
|
37
|
+
type EvaluationAssessorV2,
|
|
38
|
+
type EvaluationCohortV2,
|
|
39
|
+
type EvaluationDocumentJudgmentV2,
|
|
40
|
+
type EvaluationDocumentV2,
|
|
41
|
+
type EvaluationEvidenceUnitJudgmentV2,
|
|
42
|
+
type EvaluationEvidenceUnitV2,
|
|
43
|
+
type EvaluationExpectedSupportV2,
|
|
44
|
+
type EvaluationExperimentV2,
|
|
45
|
+
type EvaluationExternalCorpusSealV2,
|
|
46
|
+
type EvaluationInputOriginDeclarationV2,
|
|
47
|
+
type EvaluationLaneIdV2,
|
|
48
|
+
type EvaluationMeasurementProfileV2,
|
|
49
|
+
type EvaluationNegativeSubtypeV2,
|
|
50
|
+
type EvaluationQueryV2,
|
|
51
|
+
type EvaluationRawAssessorJudgmentV2,
|
|
52
|
+
type EvaluationRetrievalInputsV2,
|
|
53
|
+
type EvaluationRetrieverDescriptorV2,
|
|
54
|
+
type EvaluationSourceClassV2,
|
|
55
|
+
type EvaluationSourceFamilyV2,
|
|
56
|
+
type EvaluationSplitV2,
|
|
57
|
+
type EvaluationStratumV2,
|
|
58
|
+
type EvaluationTrustClassV2,
|
|
59
|
+
type RetrievalEvaluationCorpusV2,
|
|
60
|
+
} from "./evaluation-v2.js";
|
|
61
|
+
|
|
62
|
+
const ZERO_SHA256 = "0".repeat(64);
|
|
63
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
|
64
|
+
const DEFAULT_NGRAM_SIZE = 3;
|
|
65
|
+
const DEFAULT_CROSS_SPLIT_NGRAM_THRESHOLD = 0.8;
|
|
66
|
+
const DEFAULT_LABEL_PREDICTABILITY_CEILING = 0.65;
|
|
67
|
+
const LABEL_PREDICTABILITY_CLASSIFIER = "leave-one-out-token-jaccard-1nn-v1";
|
|
68
|
+
const MIN_SOURCE_FAMILY_RATIONALE_BYTES = 24;
|
|
69
|
+
const MAX_SOURCE_FAMILY_RATIONALE_BYTES = 2_048;
|
|
70
|
+
const MAX_SOURCE_FAMILY_REVIEWERS = 32;
|
|
71
|
+
const MAX_SOURCE_FAMILY_REVIEWER_ID_BYTES = 256;
|
|
72
|
+
|
|
73
|
+
export type PromotionCorpusMarkdownDocumentV2 = {
|
|
74
|
+
readonly documentId?: string;
|
|
75
|
+
readonly sourcePath: string;
|
|
76
|
+
readonly markdown: string;
|
|
77
|
+
/** Human-readable authoring key. The compiler replaces it with an opaque family ID. */
|
|
78
|
+
readonly sourceFamilyKey: string;
|
|
79
|
+
readonly sourceClass: EvaluationSourceClassV2;
|
|
80
|
+
readonly trustClass: EvaluationTrustClassV2;
|
|
81
|
+
/** Private review rationale. Only its digest is retained in the sealed corpus. */
|
|
82
|
+
readonly sourceFamilyRationale?: string;
|
|
83
|
+
/** Private reviewer identities. Only their digest commitment is retained. */
|
|
84
|
+
readonly sourceFamilyReviewerIds?: readonly string[];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export type AuthoredEvidenceSelectorV2 = {
|
|
88
|
+
readonly sourcePath: string;
|
|
89
|
+
/** Required with exactText; optional when an exact line range is supplied. */
|
|
90
|
+
readonly kind?: EvaluationEvidenceUnitKind;
|
|
91
|
+
/** Exact full ancestry. An empty array intentionally selects root-level evidence. */
|
|
92
|
+
readonly headingPath?: readonly string[];
|
|
93
|
+
/** Convenience guard for the final heading in headingPath. */
|
|
94
|
+
readonly heading?: string;
|
|
95
|
+
/** Exact registry unit text, including authored newline bytes. */
|
|
96
|
+
readonly exactText?: string;
|
|
97
|
+
/** Exact inclusive source line range. */
|
|
98
|
+
readonly lineRange?: EvaluationEvidenceLineRange;
|
|
99
|
+
/** Optional review-time drift guards copied from a prior registry. */
|
|
100
|
+
readonly expectedUnitId?: string;
|
|
101
|
+
readonly expectedUnitSha256?: string;
|
|
102
|
+
readonly expectedSourceSha256?: string;
|
|
103
|
+
readonly expectedByteRange?: EvaluationEvidenceByteRange;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type AuthoredDocumentJudgmentV2 = {
|
|
107
|
+
readonly sourcePath: string;
|
|
108
|
+
readonly relevance: 0 | 1 | 2 | 3;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export type AuthoredEvidenceUnitJudgmentV2 = {
|
|
112
|
+
readonly selector: AuthoredEvidenceSelectorV2;
|
|
113
|
+
readonly relevance: 0 | 1 | 2 | 3;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export type AuthoredAcceptableSupportSetV2 = {
|
|
117
|
+
readonly key: string;
|
|
118
|
+
readonly evidence: readonly AuthoredEvidenceSelectorV2[];
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export type AuthoredAtomicNuggetV2 = {
|
|
122
|
+
readonly key: string;
|
|
123
|
+
readonly text: string;
|
|
124
|
+
readonly required: boolean;
|
|
125
|
+
readonly acceptableSupportSets: readonly AuthoredAcceptableSupportSetV2[];
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export type AuthoredGoldJudgmentV2 = {
|
|
129
|
+
readonly documents: readonly AuthoredDocumentJudgmentV2[];
|
|
130
|
+
readonly evidenceUnits: readonly AuthoredEvidenceUnitJudgmentV2[];
|
|
131
|
+
readonly nuggets: readonly AuthoredAtomicNuggetV2[];
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export type AuthoredRawAssessorJudgmentV2 = {
|
|
135
|
+
readonly assessorId: string;
|
|
136
|
+
readonly expectedSupport: EvaluationExpectedSupportV2;
|
|
137
|
+
readonly documents: readonly AuthoredDocumentJudgmentV2[];
|
|
138
|
+
readonly evidenceUnits: readonly AuthoredEvidenceUnitJudgmentV2[];
|
|
139
|
+
readonly nuggets: readonly {
|
|
140
|
+
readonly nuggetKey: string;
|
|
141
|
+
readonly required?: boolean;
|
|
142
|
+
readonly acceptableSupportSetKeys: readonly string[];
|
|
143
|
+
}[];
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export type HumanAuthoredEvaluationQuestionV2 = {
|
|
147
|
+
/** Human-readable authoring key. The compiler replaces it with an opaque query ID. */
|
|
148
|
+
readonly key: string;
|
|
149
|
+
/** Preserved verbatim. The compiler never generates or rewrites question prose. */
|
|
150
|
+
readonly text: string;
|
|
151
|
+
readonly split: EvaluationSplitV2;
|
|
152
|
+
readonly cohort: EvaluationCohortV2;
|
|
153
|
+
readonly strata: readonly EvaluationStratumV2[];
|
|
154
|
+
/** One predeclared acceptance cell; it must also appear in strata. */
|
|
155
|
+
readonly primaryStratum: EvaluationStratumV2;
|
|
156
|
+
readonly expectedSupport: EvaluationExpectedSupportV2;
|
|
157
|
+
readonly primaryLane: EvaluationLaneIdV2;
|
|
158
|
+
readonly negativeSubtype?: EvaluationNegativeSubtypeV2;
|
|
159
|
+
/** Preserved executable inputs. No structured input or routing seed is inferred. */
|
|
160
|
+
readonly inputs: EvaluationRetrievalInputsV2;
|
|
161
|
+
readonly inputOrigins: readonly EvaluationInputOriginDeclarationV2[];
|
|
162
|
+
readonly gold: AuthoredGoldJudgmentV2;
|
|
163
|
+
readonly rawAssessments: readonly AuthoredRawAssessorJudgmentV2[];
|
|
164
|
+
readonly adjudication: EvaluationAdjudicationV2;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export type PromotionCorpusReviewPolicyV2 = {
|
|
168
|
+
readonly ngramSize?: number;
|
|
169
|
+
readonly crossSplitNgramThreshold?: number;
|
|
170
|
+
/** Maximum leave-one-out balanced accuracy for labels predicted from prompt text alone. */
|
|
171
|
+
readonly labelPredictabilityCeiling?: number;
|
|
172
|
+
readonly sourceFamilyAssignment?: {
|
|
173
|
+
readonly protocolId: string;
|
|
174
|
+
readonly protocolSha256: string;
|
|
175
|
+
readonly reviewerIds: readonly string[];
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export type PromotionCorpusAuthoringInputV2 = {
|
|
180
|
+
readonly id: string;
|
|
181
|
+
readonly description: string;
|
|
182
|
+
readonly sealedAt: string;
|
|
183
|
+
/** SHA-256 of the canonical immutable build.json bytes used for this compilation. */
|
|
184
|
+
readonly buildContractSha256: string;
|
|
185
|
+
readonly frozen: RetrievalEvaluationCorpusV2["frozen"];
|
|
186
|
+
readonly assessment: RetrievalEvaluationCorpusV2["assessment"];
|
|
187
|
+
readonly experiment: EvaluationExperimentV2;
|
|
188
|
+
readonly documents: readonly PromotionCorpusMarkdownDocumentV2[];
|
|
189
|
+
readonly questions: readonly HumanAuthoredEvaluationQuestionV2[];
|
|
190
|
+
readonly measurementProfiles: readonly EvaluationMeasurementProfileV2[];
|
|
191
|
+
readonly retrievers: readonly EvaluationRetrieverDescriptorV2[];
|
|
192
|
+
readonly baselineRetrieverId: string;
|
|
193
|
+
readonly evidenceParserVersion?: string;
|
|
194
|
+
readonly reviewPolicy?: PromotionCorpusReviewPolicyV2;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export type RetrievalEvaluationCorpusSealInputV2 = Omit<
|
|
198
|
+
RetrievalEvaluationCorpusV2,
|
|
199
|
+
"candidateLock" | "manifest"
|
|
200
|
+
> & {
|
|
201
|
+
readonly sealedAt: string;
|
|
202
|
+
readonly baselineRetrieverId: string;
|
|
203
|
+
readonly buildContractSha256: string;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export type RetrievalEvaluationCorpusSealResultV2 = {
|
|
207
|
+
readonly corpus: RetrievalEvaluationCorpusV2;
|
|
208
|
+
/** Persist or review this independently before using the corpus for a promotion claim. */
|
|
209
|
+
readonly externalSeal: Extract<EvaluationExternalCorpusSealV2, { readonly expectedCorpusSha256: string }>;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export type PromotionCorpusCompilationIssueV2 = {
|
|
213
|
+
readonly severity: "error" | "warning";
|
|
214
|
+
readonly code: string;
|
|
215
|
+
readonly message: string;
|
|
216
|
+
readonly queryKeys?: readonly string[];
|
|
217
|
+
readonly sourceFamilyKeys?: readonly string[];
|
|
218
|
+
readonly sourcePaths?: readonly string[];
|
|
219
|
+
readonly overlap?: number;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export type PromotionQuotaLedgerEntryV2 = {
|
|
223
|
+
readonly id: string;
|
|
224
|
+
readonly label: string;
|
|
225
|
+
readonly rule: "at-least" | "exact";
|
|
226
|
+
readonly target: number;
|
|
227
|
+
readonly actual: number;
|
|
228
|
+
readonly delta: number;
|
|
229
|
+
readonly met: boolean;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export type PromotionPowerGranularityRowV2 = {
|
|
233
|
+
readonly id: string;
|
|
234
|
+
readonly queryCount: number;
|
|
235
|
+
readonly oneOutcomeStep: {
|
|
236
|
+
readonly numerator: 1;
|
|
237
|
+
readonly denominator: number;
|
|
238
|
+
readonly percentagePoints: number;
|
|
239
|
+
};
|
|
240
|
+
readonly fivePercentagePointEventCount: number;
|
|
241
|
+
readonly nearestObservableFivePointDelta: number;
|
|
242
|
+
readonly representsFivePointsExactly: boolean;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
export type PromotionLabelPredictabilityRowV2 = {
|
|
246
|
+
readonly label: "cohort" | "expected-support" | "split";
|
|
247
|
+
readonly classes: readonly string[];
|
|
248
|
+
readonly balancedAccuracy: number;
|
|
249
|
+
readonly evaluatedQuestions: number;
|
|
250
|
+
readonly ceiling: number;
|
|
251
|
+
readonly met: boolean;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
export type PromotionPairedPowerDiagnosticsV2 = {
|
|
255
|
+
readonly eligibleCallerSeededSupportedTestPairs: number;
|
|
256
|
+
readonly independentSourceFamilyClusters: number | null;
|
|
257
|
+
readonly requiredPairs: number;
|
|
258
|
+
readonly met: boolean;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
export type PromotionCorpusDiagnosticsV2 = {
|
|
262
|
+
readonly quotaLedger: readonly PromotionQuotaLedgerEntryV2[];
|
|
263
|
+
readonly promotionLayoutReady: boolean;
|
|
264
|
+
readonly labelPredictability: {
|
|
265
|
+
readonly classifier: typeof LABEL_PREDICTABILITY_CLASSIFIER;
|
|
266
|
+
readonly rows: readonly PromotionLabelPredictabilityRowV2[];
|
|
267
|
+
readonly met: boolean;
|
|
268
|
+
};
|
|
269
|
+
readonly pairedPower: PromotionPairedPowerDiagnosticsV2;
|
|
270
|
+
readonly powerGranularity: {
|
|
271
|
+
readonly status: "descriptive-only";
|
|
272
|
+
readonly note: string;
|
|
273
|
+
readonly rows: readonly PromotionPowerGranularityRowV2[];
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
export type ResolvedAuthoredEvidenceV2 = {
|
|
278
|
+
readonly corpusEvidenceUnitId: string;
|
|
279
|
+
readonly registryUnitId: string;
|
|
280
|
+
readonly parserVersion: string;
|
|
281
|
+
readonly kind: EvaluationEvidenceUnitKind;
|
|
282
|
+
readonly documentId: string;
|
|
283
|
+
readonly sourcePath: string;
|
|
284
|
+
readonly byteRange: EvaluationEvidenceByteRange;
|
|
285
|
+
readonly lineRange: EvaluationEvidenceLineRange;
|
|
286
|
+
readonly headingPath: readonly string[];
|
|
287
|
+
readonly sourcePage?: number;
|
|
288
|
+
readonly unitSha256: string;
|
|
289
|
+
readonly sourceSha256: string;
|
|
290
|
+
readonly trustClass: string;
|
|
291
|
+
readonly exactText: string;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
type PromotionCorpusCompilationBaseV2 = {
|
|
295
|
+
readonly evidenceRegistry?: EvaluationEvidenceRegistry;
|
|
296
|
+
readonly resolvedEvidence: readonly ResolvedAuthoredEvidenceV2[];
|
|
297
|
+
readonly diagnostics: PromotionCorpusDiagnosticsV2;
|
|
298
|
+
readonly errors: readonly PromotionCorpusCompilationIssueV2[];
|
|
299
|
+
readonly warnings: readonly PromotionCorpusCompilationIssueV2[];
|
|
300
|
+
readonly reviewIssues: readonly PromotionCorpusCompilationIssueV2[];
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
export type PromotionCorpusCompilationResultV2 =
|
|
304
|
+
| PromotionCorpusCompilationBaseV2 & {
|
|
305
|
+
readonly ok: true;
|
|
306
|
+
readonly corpus: RetrievalEvaluationCorpusV2;
|
|
307
|
+
readonly externalSeal: Extract<EvaluationExternalCorpusSealV2, { readonly expectedCorpusSha256: string }>;
|
|
308
|
+
}
|
|
309
|
+
| PromotionCorpusCompilationBaseV2 & {
|
|
310
|
+
readonly ok: false;
|
|
311
|
+
readonly corpus?: never;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
type CanonicalSourceDocument = {
|
|
315
|
+
readonly documentId: string;
|
|
316
|
+
readonly sourcePath: string;
|
|
317
|
+
readonly markdown: string;
|
|
318
|
+
readonly sourceFamilyKey: string;
|
|
319
|
+
readonly sourceFamilyId: string;
|
|
320
|
+
readonly sourceClass: EvaluationSourceClassV2;
|
|
321
|
+
readonly trustClass: EvaluationTrustClassV2;
|
|
322
|
+
readonly familyAssignmentSha256?: string;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
type ResolutionContext = {
|
|
326
|
+
readonly registry: EvaluationEvidenceRegistry;
|
|
327
|
+
readonly registryDocumentsByPath: ReadonlyMap<string, EvaluationEvidenceRegistry["documents"][number]>;
|
|
328
|
+
readonly corpusDocumentsByPath: ReadonlyMap<string, CanonicalSourceDocument>;
|
|
329
|
+
readonly resolvedByRegistryId: Map<string, ResolvedAuthoredEvidenceV2>;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
class AuthoringFailure extends Error {
|
|
333
|
+
readonly code: string;
|
|
334
|
+
readonly queryKeys: readonly string[] | undefined;
|
|
335
|
+
readonly sourcePaths: readonly string[] | undefined;
|
|
336
|
+
|
|
337
|
+
constructor(
|
|
338
|
+
code: string,
|
|
339
|
+
message: string,
|
|
340
|
+
details: {
|
|
341
|
+
readonly queryKeys?: readonly string[];
|
|
342
|
+
readonly sourcePaths?: readonly string[];
|
|
343
|
+
} = {},
|
|
344
|
+
) {
|
|
345
|
+
super(message);
|
|
346
|
+
this.name = "AuthoringFailure";
|
|
347
|
+
this.code = code;
|
|
348
|
+
this.queryKeys = details.queryKeys;
|
|
349
|
+
this.sourcePaths = details.sourcePaths;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function compareText(left: string, right: string): number {
|
|
354
|
+
return left.localeCompare(right);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function uniqueSorted(values: readonly string[]): readonly string[] {
|
|
358
|
+
return Object.freeze([...new Set(values)].toSorted(compareText));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function canonicalJson(value: unknown): string {
|
|
362
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") {
|
|
363
|
+
return JSON.stringify(value);
|
|
364
|
+
}
|
|
365
|
+
if (typeof value === "number") {
|
|
366
|
+
if (!Number.isFinite(value)) throw new TypeError("Canonical input contains a non-finite number.");
|
|
367
|
+
return JSON.stringify(value);
|
|
368
|
+
}
|
|
369
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
370
|
+
if (typeof value === "object" && value !== null) {
|
|
371
|
+
const input = value as Readonly<Record<string, unknown>>;
|
|
372
|
+
return `{${Object.keys(input).toSorted(compareText).map((key) =>
|
|
373
|
+
`${JSON.stringify(key)}:${canonicalJson(input[key])}`).join(",")}}`;
|
|
374
|
+
}
|
|
375
|
+
throw new TypeError("Canonical input must be JSON-compatible.");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function framedDigest(domain: string, fields: readonly string[]): string {
|
|
379
|
+
const digest = createHash("sha256");
|
|
380
|
+
digest.update(`${domain}\0`, "utf8");
|
|
381
|
+
for (const field of fields) {
|
|
382
|
+
const bytes = Buffer.from(field, "utf8");
|
|
383
|
+
digest.update(`${bytes.byteLength}:`, "utf8");
|
|
384
|
+
digest.update(bytes);
|
|
385
|
+
digest.update("\0", "utf8");
|
|
386
|
+
}
|
|
387
|
+
return digest.digest("hex");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function opaqueId(prefix: "ng" | "q" | "sf" | "ss", fields: readonly string[]): string {
|
|
391
|
+
return `${prefix}-${framedDigest(`promotion-corpus-${prefix}-v1`, fields).slice(0, 16)}`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function authoredKey(value: unknown, label: string): string {
|
|
395
|
+
if (
|
|
396
|
+
typeof value !== "string"
|
|
397
|
+
|| value.trim() === ""
|
|
398
|
+
|| /[\0\r\n]/u.test(value)
|
|
399
|
+
|| value.normalize("NFC") !== value
|
|
400
|
+
|| Buffer.byteLength(value, "utf8") > 4_096
|
|
401
|
+
) throw new AuthoringFailure("invalid-authoring-key", `${label} must be a non-empty NFC single-line string.`);
|
|
402
|
+
return value;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
type CanonicalSourceFamilyAssignmentReview = Readonly<{
|
|
406
|
+
rationale: string;
|
|
407
|
+
reviewerIds: readonly string[];
|
|
408
|
+
}>;
|
|
409
|
+
|
|
410
|
+
function canonicalReviewerIds(value: unknown, label: string): readonly string[] {
|
|
411
|
+
if (!Array.isArray(value) || value.length < 2 || value.length > MAX_SOURCE_FAMILY_REVIEWERS) {
|
|
412
|
+
throw new AuthoringFailure(
|
|
413
|
+
"invalid-source-family-reviewers",
|
|
414
|
+
`${label} must contain from 2 through ${MAX_SOURCE_FAMILY_REVIEWERS} independent reviewer IDs.`,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
const reviewerIds = value.map((reviewerId, index) => {
|
|
418
|
+
const id = authoredKey(reviewerId, `${label}[${index}]`);
|
|
419
|
+
if (Buffer.byteLength(id, "utf8") > MAX_SOURCE_FAMILY_REVIEWER_ID_BYTES) {
|
|
420
|
+
throw new AuthoringFailure(
|
|
421
|
+
"invalid-source-family-reviewers",
|
|
422
|
+
`${label}[${index}] exceeds ${MAX_SOURCE_FAMILY_REVIEWER_ID_BYTES} UTF-8 bytes.`,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
return id;
|
|
426
|
+
});
|
|
427
|
+
const canonical = reviewerIds.toSorted(compareText);
|
|
428
|
+
if (!sameStrings(reviewerIds, canonical)) {
|
|
429
|
+
throw new AuthoringFailure(
|
|
430
|
+
"noncanonical-source-family-reviewers",
|
|
431
|
+
`${label} must be in canonical order.`,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
if (new Set(reviewerIds).size !== reviewerIds.length) {
|
|
435
|
+
throw new AuthoringFailure(
|
|
436
|
+
"duplicate-source-family-reviewers",
|
|
437
|
+
`${label} must contain distinct reviewer IDs.`,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
return Object.freeze(reviewerIds);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function canonicalSourceFamilyReview(
|
|
444
|
+
document: PromotionCorpusMarkdownDocumentV2,
|
|
445
|
+
index: number,
|
|
446
|
+
declaredReviewerIds: readonly string[] | undefined,
|
|
447
|
+
): CanonicalSourceFamilyAssignmentReview | undefined {
|
|
448
|
+
const hasRationale = document.sourceFamilyRationale !== undefined;
|
|
449
|
+
const hasReviewers = document.sourceFamilyReviewerIds !== undefined;
|
|
450
|
+
if (!hasRationale && !hasReviewers) return undefined;
|
|
451
|
+
if (!hasRationale || !hasReviewers) {
|
|
452
|
+
throw new AuthoringFailure(
|
|
453
|
+
"incomplete-source-family-review",
|
|
454
|
+
`documents[${index}] must declare both sourceFamilyRationale and sourceFamilyReviewerIds.`,
|
|
455
|
+
{ sourcePaths: [document.sourcePath] },
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
if (declaredReviewerIds === undefined) {
|
|
459
|
+
throw new AuthoringFailure(
|
|
460
|
+
"missing-source-family-review-protocol",
|
|
461
|
+
`documents[${index}] has reviewed family metadata but reviewPolicy.sourceFamilyAssignment is absent.`,
|
|
462
|
+
{ sourcePaths: [document.sourcePath] },
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
const rationale = document.sourceFamilyRationale;
|
|
466
|
+
if (
|
|
467
|
+
typeof rationale !== "string"
|
|
468
|
+
|| rationale.normalize("NFC") !== rationale
|
|
469
|
+
|| rationale.trim() !== rationale
|
|
470
|
+
|| rationale.includes("\0")
|
|
471
|
+
|| Buffer.byteLength(rationale, "utf8") < MIN_SOURCE_FAMILY_RATIONALE_BYTES
|
|
472
|
+
|| Buffer.byteLength(rationale, "utf8") > MAX_SOURCE_FAMILY_RATIONALE_BYTES
|
|
473
|
+
) {
|
|
474
|
+
throw new AuthoringFailure(
|
|
475
|
+
"invalid-source-family-rationale",
|
|
476
|
+
`documents[${index}].sourceFamilyRationale must be trimmed NFC text from ${MIN_SOURCE_FAMILY_RATIONALE_BYTES} through ${MAX_SOURCE_FAMILY_RATIONALE_BYTES} UTF-8 bytes.`,
|
|
477
|
+
{ sourcePaths: [document.sourcePath] },
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
const reviewerIds = canonicalReviewerIds(
|
|
481
|
+
document.sourceFamilyReviewerIds,
|
|
482
|
+
`documents[${index}].sourceFamilyReviewerIds`,
|
|
483
|
+
);
|
|
484
|
+
const declared = new Set(declaredReviewerIds);
|
|
485
|
+
const undeclared = reviewerIds.filter((reviewerId) => !declared.has(reviewerId));
|
|
486
|
+
if (undeclared.length > 0) {
|
|
487
|
+
throw new AuthoringFailure(
|
|
488
|
+
"undeclared-source-family-reviewers",
|
|
489
|
+
`documents[${index}] names undeclared family reviewers: ${undeclared.join(", ")}.`,
|
|
490
|
+
{ sourcePaths: [document.sourcePath] },
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
return Object.freeze({ rationale, reviewerIds });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function canonicalSha256(value: unknown, label: string): string {
|
|
497
|
+
if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
|
|
498
|
+
throw new TypeError(`${label} must be 64 lowercase hexadecimal characters.`);
|
|
499
|
+
}
|
|
500
|
+
return value;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function confinedPath(value: unknown, label: string): string {
|
|
504
|
+
if (
|
|
505
|
+
typeof value !== "string"
|
|
506
|
+
|| value === ""
|
|
507
|
+
|| value.normalize("NFC") !== value
|
|
508
|
+
|| /[\0\r\n\\]/u.test(value)
|
|
509
|
+
|| value.startsWith("/")
|
|
510
|
+
|| value.startsWith("./")
|
|
511
|
+
|| /^[a-z]:[\\/]/iu.test(value)
|
|
512
|
+
|| value.split("/").some((part) => part === "" || part === "." || part === "..")
|
|
513
|
+
) throw new AuthoringFailure("invalid-source-path", `${label} must be a canonical confined repository-relative path.`);
|
|
514
|
+
return value;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function sameStrings(left: readonly string[], right: readonly string[]): boolean {
|
|
518
|
+
return left.length === right.length && left.every((entry, index) => entry === right[index]);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function sameRange(
|
|
522
|
+
left: EvaluationEvidenceLineRange | EvaluationEvidenceByteRange,
|
|
523
|
+
right: EvaluationEvidenceLineRange | EvaluationEvidenceByteRange,
|
|
524
|
+
): boolean {
|
|
525
|
+
return left.start === right.start && left.end === right.end;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function selectorLabel(selector: AuthoredEvidenceSelectorV2): string {
|
|
529
|
+
const position = selector.lineRange === undefined
|
|
530
|
+
? `${selector.kind ?? "unit"} exact text`
|
|
531
|
+
: `lines ${selector.lineRange.start}-${selector.lineRange.end}`;
|
|
532
|
+
return `${selector.sourcePath} (${position})`;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function resolveSelector(
|
|
536
|
+
selector: AuthoredEvidenceSelectorV2,
|
|
537
|
+
context: ResolutionContext,
|
|
538
|
+
): ResolvedAuthoredEvidenceV2 {
|
|
539
|
+
const sourcePath = confinedPath(selector.sourcePath, "evidence selector sourcePath");
|
|
540
|
+
const hasLineRange = selector.lineRange !== undefined;
|
|
541
|
+
const hasExactText = selector.exactText !== undefined;
|
|
542
|
+
if (!hasLineRange && !hasExactText) {
|
|
543
|
+
throw new AuthoringFailure(
|
|
544
|
+
"selector-incomplete",
|
|
545
|
+
`Evidence selector ${sourcePath} must supply exactText or an explicit lineRange.`,
|
|
546
|
+
{ sourcePaths: [sourcePath] },
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
if (!hasLineRange && selector.kind === undefined) {
|
|
550
|
+
throw new AuthoringFailure(
|
|
551
|
+
"selector-incomplete",
|
|
552
|
+
`Exact-text selector ${sourcePath} must name its evidence-unit kind.`,
|
|
553
|
+
{ sourcePaths: [sourcePath] },
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
if (
|
|
557
|
+
selector.headingPath !== undefined
|
|
558
|
+
&& selector.heading !== undefined
|
|
559
|
+
&& selector.headingPath.at(-1) !== selector.heading
|
|
560
|
+
) {
|
|
561
|
+
throw new AuthoringFailure(
|
|
562
|
+
"selector-inconsistent-heading",
|
|
563
|
+
`Evidence selector ${sourcePath} has inconsistent heading and headingPath guards.`,
|
|
564
|
+
{ sourcePaths: [sourcePath] },
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
const document = context.registryDocumentsByPath.get(sourcePath);
|
|
568
|
+
if (document === undefined) {
|
|
569
|
+
throw new AuthoringFailure(
|
|
570
|
+
"selector-source-missing",
|
|
571
|
+
`Evidence selector names unknown frozen source ${sourcePath}.`,
|
|
572
|
+
{ sourcePaths: [sourcePath] },
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
if (
|
|
576
|
+
selector.expectedSourceSha256 !== undefined
|
|
577
|
+
&& selector.expectedSourceSha256 !== document.sourceSha256
|
|
578
|
+
) {
|
|
579
|
+
throw new AuthoringFailure(
|
|
580
|
+
"selector-source-drift",
|
|
581
|
+
`Frozen source ${sourcePath} drifted from selector source commitment ${selector.expectedSourceSha256}.`,
|
|
582
|
+
{ sourcePaths: [sourcePath] },
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
const matches = context.registry.units.filter((unit) => (
|
|
586
|
+
unit.sourcePath === sourcePath
|
|
587
|
+
&& (selector.kind === undefined || unit.kind === selector.kind)
|
|
588
|
+
&& (selector.headingPath === undefined || sameStrings(unit.headingAncestry, selector.headingPath))
|
|
589
|
+
&& (selector.heading === undefined || unit.headingAncestry.at(-1) === selector.heading)
|
|
590
|
+
&& (selector.exactText === undefined || unit.text === selector.exactText)
|
|
591
|
+
&& (selector.lineRange === undefined || sameRange(unit.lineRange, selector.lineRange))
|
|
592
|
+
));
|
|
593
|
+
if (matches.length === 0) {
|
|
594
|
+
throw new AuthoringFailure(
|
|
595
|
+
selector.expectedUnitId === undefined ? "selector-zero-or-drifted-match" : "selector-unit-drift",
|
|
596
|
+
`Evidence selector ${selectorLabel(selector)} resolved to zero registry units; the selector is stale or does not match the frozen Markdown exactly.`,
|
|
597
|
+
{ sourcePaths: [sourcePath] },
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
if (matches.length > 1) {
|
|
601
|
+
throw new AuthoringFailure(
|
|
602
|
+
"selector-ambiguous",
|
|
603
|
+
`Evidence selector ${selectorLabel(selector)} resolved to ${matches.length} registry units. Add an exact heading, kind, text, or range guard.`,
|
|
604
|
+
{ sourcePaths: [sourcePath] },
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
const unit = matches[0];
|
|
608
|
+
if (unit === undefined) throw new Error("Unique evidence selector resolution was lost.");
|
|
609
|
+
if (
|
|
610
|
+
(selector.expectedUnitId !== undefined && selector.expectedUnitId !== unit.id)
|
|
611
|
+
|| (selector.expectedUnitSha256 !== undefined && selector.expectedUnitSha256 !== unit.sha256)
|
|
612
|
+
|| (selector.expectedByteRange !== undefined && !sameRange(selector.expectedByteRange, unit.byteRange))
|
|
613
|
+
) {
|
|
614
|
+
throw new AuthoringFailure(
|
|
615
|
+
"selector-unit-drift",
|
|
616
|
+
`Evidence selector ${selectorLabel(selector)} matched a unit whose ID, bytes, or hash drifted from its review commitment.`,
|
|
617
|
+
{ sourcePaths: [sourcePath] },
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
const existing = context.resolvedByRegistryId.get(unit.id);
|
|
621
|
+
if (existing !== undefined) return existing;
|
|
622
|
+
const corpusDocument = context.corpusDocumentsByPath.get(sourcePath);
|
|
623
|
+
if (corpusDocument === undefined) throw new Error(`Resolved source ${sourcePath} lost its corpus declaration.`);
|
|
624
|
+
if (Buffer.byteLength(unit.text, "utf8") !== unit.byteRange.end - unit.byteRange.start) {
|
|
625
|
+
throw new Error(`Evidence unit ${unit.id} lost UTF-8 byte fidelity.`);
|
|
626
|
+
}
|
|
627
|
+
const resolved = Object.freeze({
|
|
628
|
+
corpusEvidenceUnitId: unit.id,
|
|
629
|
+
registryUnitId: unit.id,
|
|
630
|
+
parserVersion: unit.parserVersion,
|
|
631
|
+
kind: unit.kind,
|
|
632
|
+
documentId: corpusDocument.documentId,
|
|
633
|
+
sourcePath: unit.sourcePath,
|
|
634
|
+
byteRange: Object.freeze({ ...unit.byteRange }),
|
|
635
|
+
lineRange: Object.freeze({ ...unit.lineRange }),
|
|
636
|
+
headingPath: Object.freeze([...unit.headingAncestry]),
|
|
637
|
+
...(unit.pdfPage === undefined ? {} : { sourcePage: unit.pdfPage }),
|
|
638
|
+
unitSha256: unit.sha256,
|
|
639
|
+
sourceSha256: document.sourceSha256,
|
|
640
|
+
trustClass: unit.trustClass,
|
|
641
|
+
exactText: unit.text,
|
|
642
|
+
} satisfies ResolvedAuthoredEvidenceV2);
|
|
643
|
+
context.resolvedByRegistryId.set(unit.id, resolved);
|
|
644
|
+
return resolved;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function canonicalInputs(inputs: EvaluationRetrievalInputsV2): EvaluationRetrievalInputsV2 {
|
|
648
|
+
const filters = inputs.metadata?.filters.map((filter) => filter.kind === "exists"
|
|
649
|
+
? Object.freeze({ kind: "exists" as const, path: filter.path })
|
|
650
|
+
: Object.freeze({ kind: "equals" as const, path: filter.path, value: filter.value }));
|
|
651
|
+
const canonicalFilters = filters === undefined
|
|
652
|
+
? undefined
|
|
653
|
+
: Object.freeze([...new Map(filters.map((filter) => [canonicalJson(filter), filter])).values()]
|
|
654
|
+
.toSorted((left, right) => compareText(canonicalJson(left), canonicalJson(right))));
|
|
655
|
+
return Object.freeze({
|
|
656
|
+
text: inputs.text,
|
|
657
|
+
...(inputs.context === undefined
|
|
658
|
+
? {}
|
|
659
|
+
: { context: Object.freeze({ repositoryPath: inputs.context.repositoryPath }) }),
|
|
660
|
+
...(inputs.graph === undefined
|
|
661
|
+
? {}
|
|
662
|
+
: { graph: Object.freeze({ seeds: uniqueSorted(inputs.graph.seeds), depth: inputs.graph.depth }) }),
|
|
663
|
+
...(inputs.history === undefined
|
|
664
|
+
? {}
|
|
665
|
+
: {
|
|
666
|
+
history: Object.freeze({
|
|
667
|
+
query: inputs.history.query,
|
|
668
|
+
noteIds: uniqueSorted(inputs.history.noteIds),
|
|
669
|
+
}),
|
|
670
|
+
}),
|
|
671
|
+
...(inputs.metadata === undefined || canonicalFilters === undefined
|
|
672
|
+
? {}
|
|
673
|
+
: {
|
|
674
|
+
metadata: Object.freeze({
|
|
675
|
+
filters: canonicalFilters,
|
|
676
|
+
tags: uniqueSorted(inputs.metadata.tags),
|
|
677
|
+
}),
|
|
678
|
+
}),
|
|
679
|
+
...(inputs.noteId === undefined ? {} : { noteId: inputs.noteId }),
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function canonicalOrigins(
|
|
684
|
+
origins: readonly EvaluationInputOriginDeclarationV2[],
|
|
685
|
+
queryKey: string,
|
|
686
|
+
): readonly EvaluationInputOriginDeclarationV2[] {
|
|
687
|
+
const byLane = new Map<string, EvaluationInputOriginDeclarationV2>();
|
|
688
|
+
for (const origin of origins) {
|
|
689
|
+
const previous = byLane.get(origin.lane);
|
|
690
|
+
if (previous !== undefined && previous.origin !== origin.origin) {
|
|
691
|
+
throw new AuthoringFailure(
|
|
692
|
+
"conflicting-input-origin",
|
|
693
|
+
`Question ${queryKey} supplies conflicting origins for ${origin.lane}.`,
|
|
694
|
+
{ queryKeys: [queryKey] },
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
byLane.set(origin.lane, Object.freeze({ lane: origin.lane, origin: origin.origin }));
|
|
698
|
+
}
|
|
699
|
+
return Object.freeze([...byLane.values()].toSorted((left, right) => compareText(left.lane, right.lane)));
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function canonicalJudgments<T extends { readonly relevance: 0 | 1 | 2 | 3 }>(
|
|
703
|
+
judgments: readonly T[],
|
|
704
|
+
id: (judgment: T) => string,
|
|
705
|
+
label: string,
|
|
706
|
+
): readonly T[] {
|
|
707
|
+
const byId = new Map<string, T>();
|
|
708
|
+
for (const judgment of judgments) {
|
|
709
|
+
const key = id(judgment);
|
|
710
|
+
const previous = byId.get(key);
|
|
711
|
+
if (previous !== undefined && previous.relevance !== judgment.relevance) {
|
|
712
|
+
throw new AuthoringFailure(
|
|
713
|
+
"conflicting-relevance-judgment",
|
|
714
|
+
`${label} gives ${key} conflicting relevance grades.`,
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
byId.set(key, judgment);
|
|
718
|
+
}
|
|
719
|
+
return Object.freeze([...byId.values()].toSorted((left, right) => compareText(id(left), id(right))));
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function compileDocumentJudgments(
|
|
723
|
+
judgments: readonly AuthoredDocumentJudgmentV2[],
|
|
724
|
+
documentsByPath: ReadonlyMap<string, CanonicalSourceDocument>,
|
|
725
|
+
label: string,
|
|
726
|
+
): readonly EvaluationDocumentJudgmentV2[] {
|
|
727
|
+
const compiled = judgments.map((judgment): EvaluationDocumentJudgmentV2 => {
|
|
728
|
+
const sourcePath = confinedPath(judgment.sourcePath, `${label} sourcePath`);
|
|
729
|
+
const document = documentsByPath.get(sourcePath);
|
|
730
|
+
if (document === undefined) {
|
|
731
|
+
throw new AuthoringFailure(
|
|
732
|
+
"judgment-document-missing",
|
|
733
|
+
`${label} names unknown frozen source ${sourcePath}.`,
|
|
734
|
+
{ sourcePaths: [sourcePath] },
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
return Object.freeze({ documentId: document.documentId, relevance: judgment.relevance });
|
|
738
|
+
});
|
|
739
|
+
return canonicalJudgments(compiled, ({ documentId }) => documentId, label);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function compileEvidenceJudgments(
|
|
743
|
+
judgments: readonly AuthoredEvidenceUnitJudgmentV2[],
|
|
744
|
+
context: ResolutionContext,
|
|
745
|
+
label: string,
|
|
746
|
+
): readonly EvaluationEvidenceUnitJudgmentV2[] {
|
|
747
|
+
const compiled = judgments.map((judgment): EvaluationEvidenceUnitJudgmentV2 => Object.freeze({
|
|
748
|
+
evidenceUnitId: resolveSelector(judgment.selector, context).corpusEvidenceUnitId,
|
|
749
|
+
relevance: judgment.relevance,
|
|
750
|
+
}));
|
|
751
|
+
return canonicalJudgments(compiled, ({ evidenceUnitId }) => evidenceUnitId, label);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function compileQuestion(
|
|
755
|
+
question: HumanAuthoredEvaluationQuestionV2,
|
|
756
|
+
context: ResolutionContext,
|
|
757
|
+
): EvaluationQueryV2 {
|
|
758
|
+
const queryKey = authoredKey(question.key, "question.key");
|
|
759
|
+
const queryId = opaqueId("q", [queryKey]);
|
|
760
|
+
const nuggetKeys = new Set<string>();
|
|
761
|
+
const supportKeys = new Set<string>();
|
|
762
|
+
const supportIdsByNugget = new Map<string, ReadonlyMap<string, string>>();
|
|
763
|
+
const nuggets = question.gold.nuggets.map((nugget) => {
|
|
764
|
+
const nuggetKey = authoredKey(nugget.key, `question ${queryKey} nugget key`);
|
|
765
|
+
if (nuggetKeys.has(nuggetKey)) {
|
|
766
|
+
throw new AuthoringFailure("duplicate-nugget-key", `Question ${queryKey} repeats nugget key ${nuggetKey}.`, {
|
|
767
|
+
queryKeys: [queryKey],
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
nuggetKeys.add(nuggetKey);
|
|
771
|
+
const supportIds = new Map<string, string>();
|
|
772
|
+
const acceptableSupportSets = nugget.acceptableSupportSets.map((supportSet) => {
|
|
773
|
+
const supportKey = authoredKey(supportSet.key, `question ${queryKey} support-set key`);
|
|
774
|
+
const scopedKey = `${nuggetKey}\0${supportKey}`;
|
|
775
|
+
if (supportKeys.has(scopedKey)) {
|
|
776
|
+
throw new AuthoringFailure(
|
|
777
|
+
"duplicate-support-set-key",
|
|
778
|
+
`Question ${queryKey} nugget ${nuggetKey} repeats support-set key ${supportKey}.`,
|
|
779
|
+
{ queryKeys: [queryKey] },
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
supportKeys.add(scopedKey);
|
|
783
|
+
const supportId = opaqueId("ss", [queryKey, nuggetKey, supportKey]);
|
|
784
|
+
supportIds.set(supportKey, supportId);
|
|
785
|
+
const evidenceUnitIds = uniqueSorted(
|
|
786
|
+
supportSet.evidence.map((selector) => resolveSelector(selector, context).corpusEvidenceUnitId),
|
|
787
|
+
);
|
|
788
|
+
return Object.freeze({ id: supportId, evidenceUnitIds });
|
|
789
|
+
}).toSorted((left, right) => compareText(left.id, right.id));
|
|
790
|
+
supportIdsByNugget.set(nuggetKey, supportIds);
|
|
791
|
+
return Object.freeze({
|
|
792
|
+
id: opaqueId("ng", [queryKey, nuggetKey]),
|
|
793
|
+
text: nugget.text,
|
|
794
|
+
required: nugget.required,
|
|
795
|
+
acceptableSupportSets: Object.freeze(acceptableSupportSets),
|
|
796
|
+
});
|
|
797
|
+
}).toSorted((left, right) => compareText(left.id, right.id));
|
|
798
|
+
|
|
799
|
+
const gold = Object.freeze({
|
|
800
|
+
documents: compileDocumentJudgments(
|
|
801
|
+
question.gold.documents,
|
|
802
|
+
context.corpusDocumentsByPath,
|
|
803
|
+
`question ${queryKey} gold documents`,
|
|
804
|
+
),
|
|
805
|
+
evidenceUnits: compileEvidenceJudgments(
|
|
806
|
+
question.gold.evidenceUnits,
|
|
807
|
+
context,
|
|
808
|
+
`question ${queryKey} gold evidence`,
|
|
809
|
+
),
|
|
810
|
+
nuggets: Object.freeze(nuggets),
|
|
811
|
+
});
|
|
812
|
+
const nuggetIdByKey = new Map(question.gold.nuggets.map((nugget) => [
|
|
813
|
+
nugget.key,
|
|
814
|
+
opaqueId("ng", [queryKey, nugget.key]),
|
|
815
|
+
]));
|
|
816
|
+
const nuggetRequiredByKey = new Map(question.gold.nuggets.map((nugget) => [nugget.key, nugget.required]));
|
|
817
|
+
const assessorIds = new Set<string>();
|
|
818
|
+
const rawAssessments = question.rawAssessments.map((assessment): EvaluationRawAssessorJudgmentV2 => {
|
|
819
|
+
if (assessorIds.has(assessment.assessorId)) {
|
|
820
|
+
throw new AuthoringFailure(
|
|
821
|
+
"duplicate-assessor-decision",
|
|
822
|
+
`Question ${queryKey} repeats assessor ${assessment.assessorId}.`,
|
|
823
|
+
{ queryKeys: [queryKey] },
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
assessorIds.add(assessment.assessorId);
|
|
827
|
+
const rawNuggetKeys = new Set<string>();
|
|
828
|
+
const rawNuggets = assessment.nuggets.map((decision) => {
|
|
829
|
+
if (rawNuggetKeys.has(decision.nuggetKey)) {
|
|
830
|
+
throw new AuthoringFailure(
|
|
831
|
+
"duplicate-assessor-nugget-decision",
|
|
832
|
+
`Question ${queryKey} assessor ${assessment.assessorId} repeats nugget ${decision.nuggetKey}.`,
|
|
833
|
+
{ queryKeys: [queryKey] },
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
rawNuggetKeys.add(decision.nuggetKey);
|
|
837
|
+
const nuggetId = nuggetIdByKey.get(decision.nuggetKey);
|
|
838
|
+
const supportIds = supportIdsByNugget.get(decision.nuggetKey);
|
|
839
|
+
if (nuggetId === undefined || supportIds === undefined) {
|
|
840
|
+
throw new AuthoringFailure(
|
|
841
|
+
"assessor-nugget-missing",
|
|
842
|
+
`Question ${queryKey} assessor ${assessment.assessorId} names unknown nugget ${decision.nuggetKey}.`,
|
|
843
|
+
{ queryKeys: [queryKey] },
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
const acceptableSupportSetIds = uniqueSorted(decision.acceptableSupportSetKeys.map((key) => {
|
|
847
|
+
const id = supportIds.get(key);
|
|
848
|
+
if (id === undefined) {
|
|
849
|
+
throw new AuthoringFailure(
|
|
850
|
+
"assessor-support-set-missing",
|
|
851
|
+
`Question ${queryKey} assessor ${assessment.assessorId} names unknown support set ${key}.`,
|
|
852
|
+
{ queryKeys: [queryKey] },
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
return id;
|
|
856
|
+
}));
|
|
857
|
+
return Object.freeze({
|
|
858
|
+
nuggetId,
|
|
859
|
+
required: decision.required ?? nuggetRequiredByKey.get(decision.nuggetKey) ?? true,
|
|
860
|
+
acceptableSupportSetIds,
|
|
861
|
+
});
|
|
862
|
+
}).toSorted((left, right) => compareText(left.nuggetId, right.nuggetId));
|
|
863
|
+
return Object.freeze({
|
|
864
|
+
assessorId: assessment.assessorId,
|
|
865
|
+
expectedSupport: assessment.expectedSupport,
|
|
866
|
+
documents: compileDocumentJudgments(
|
|
867
|
+
assessment.documents,
|
|
868
|
+
context.corpusDocumentsByPath,
|
|
869
|
+
`question ${queryKey} assessor ${assessment.assessorId} documents`,
|
|
870
|
+
),
|
|
871
|
+
evidenceUnits: compileEvidenceJudgments(
|
|
872
|
+
assessment.evidenceUnits,
|
|
873
|
+
context,
|
|
874
|
+
`question ${queryKey} assessor ${assessment.assessorId} evidence`,
|
|
875
|
+
),
|
|
876
|
+
nuggets: Object.freeze(rawNuggets),
|
|
877
|
+
});
|
|
878
|
+
}).toSorted((left, right) => compareText(left.assessorId, right.assessorId));
|
|
879
|
+
|
|
880
|
+
const adjudication = question.adjudication.status === "resolved"
|
|
881
|
+
? Object.freeze({
|
|
882
|
+
status: "resolved" as const,
|
|
883
|
+
adjudicatorId: question.adjudication.adjudicatorId,
|
|
884
|
+
rationale: question.adjudication.rationale,
|
|
885
|
+
})
|
|
886
|
+
: Object.freeze({ status: question.adjudication.status });
|
|
887
|
+
return Object.freeze({
|
|
888
|
+
id: queryId,
|
|
889
|
+
text: question.text,
|
|
890
|
+
split: question.split,
|
|
891
|
+
cohort: question.cohort,
|
|
892
|
+
strata: uniqueSorted(question.strata) as readonly EvaluationStratumV2[],
|
|
893
|
+
primaryStratum: question.primaryStratum,
|
|
894
|
+
expectedSupport: question.expectedSupport,
|
|
895
|
+
primaryLane: question.primaryLane,
|
|
896
|
+
...(question.negativeSubtype === undefined ? {} : { negativeSubtype: question.negativeSubtype }),
|
|
897
|
+
inputs: canonicalInputs(question.inputs),
|
|
898
|
+
inputOrigins: canonicalOrigins(question.inputOrigins, queryKey),
|
|
899
|
+
gold,
|
|
900
|
+
rawAssessments: Object.freeze(rawAssessments),
|
|
901
|
+
adjudication,
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function cloneExperiment(experiment: EvaluationExperimentV2): EvaluationExperimentV2 {
|
|
906
|
+
const effects = new Map<string, EvaluationExperimentV2["protocol"]["minimumUsefulEffects"][number]>();
|
|
907
|
+
for (const effect of experiment.protocol.minimumUsefulEffects) {
|
|
908
|
+
const cloned = Object.freeze({
|
|
909
|
+
metric: effect.metric,
|
|
910
|
+
cohort: effect.cohort,
|
|
911
|
+
minimumAbsoluteDifference: effect.minimumAbsoluteDifference,
|
|
912
|
+
});
|
|
913
|
+
const key = `${effect.metric}:${effect.cohort}`;
|
|
914
|
+
const previous = effects.get(key);
|
|
915
|
+
if (previous !== undefined) {
|
|
916
|
+
throw new AuthoringFailure(
|
|
917
|
+
canonicalJson(previous) === canonicalJson(cloned)
|
|
918
|
+
? "duplicate-minimum-useful-effect"
|
|
919
|
+
: "conflicting-minimum-useful-effect",
|
|
920
|
+
`Experiment metric ${effect.metric} repeats a minimum useful effect for ${effect.cohort}.`,
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
effects.set(key, cloned);
|
|
924
|
+
}
|
|
925
|
+
const margins = new Map<string, EvaluationExperimentV2["protocol"]["nonInferiorityMargins"][number]>();
|
|
926
|
+
for (const margin of experiment.protocol.nonInferiorityMargins) {
|
|
927
|
+
const cloned = Object.freeze({
|
|
928
|
+
metric: margin.metric,
|
|
929
|
+
maximumAbsoluteRegression: margin.maximumAbsoluteRegression,
|
|
930
|
+
maximumRelativeRegression: margin.maximumRelativeRegression,
|
|
931
|
+
});
|
|
932
|
+
const previous = margins.get(margin.metric);
|
|
933
|
+
if (previous !== undefined && canonicalJson(previous) !== canonicalJson(cloned)) {
|
|
934
|
+
throw new AuthoringFailure(
|
|
935
|
+
"conflicting-non-inferiority-margin",
|
|
936
|
+
`Experiment metric ${margin.metric} has conflicting non-inferiority margins.`,
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
margins.set(margin.metric, cloned);
|
|
940
|
+
}
|
|
941
|
+
const localModel = experiment.environment.localModel.kind === "none"
|
|
942
|
+
? Object.freeze({ kind: "none" as const })
|
|
943
|
+
: Object.freeze({
|
|
944
|
+
kind: "model" as const,
|
|
945
|
+
id: experiment.environment.localModel.id,
|
|
946
|
+
sha256: experiment.environment.localModel.sha256,
|
|
947
|
+
});
|
|
948
|
+
return Object.freeze({
|
|
949
|
+
protocol: Object.freeze({
|
|
950
|
+
minimumUsefulEffects: Object.freeze([...effects.values()]
|
|
951
|
+
.toSorted((left, right) => compareText(
|
|
952
|
+
`${left.metric}:${left.cohort}`,
|
|
953
|
+
`${right.metric}:${right.cohort}`,
|
|
954
|
+
))),
|
|
955
|
+
nonInferiorityMargins: Object.freeze([...margins.values()]
|
|
956
|
+
.toSorted((left, right) => compareText(left.metric, right.metric))),
|
|
957
|
+
contextCeilings: Object.freeze({
|
|
958
|
+
utf8Bytes: experiment.protocol.contextCeilings.utf8Bytes,
|
|
959
|
+
readerTokens: experiment.protocol.contextCeilings.readerTokens,
|
|
960
|
+
}),
|
|
961
|
+
pairedPower: Object.freeze({
|
|
962
|
+
alpha: experiment.protocol.pairedPower.alpha,
|
|
963
|
+
targetPower: experiment.protocol.pairedPower.targetPower,
|
|
964
|
+
assumedDiscordantRate: experiment.protocol.pairedPower.assumedDiscordantRate,
|
|
965
|
+
assumedEffect: experiment.protocol.pairedPower.assumedEffect,
|
|
966
|
+
minimumUsefulEffect: experiment.protocol.pairedPower.minimumUsefulEffect,
|
|
967
|
+
requiredPairs: experiment.protocol.pairedPower.requiredPairs,
|
|
968
|
+
}),
|
|
969
|
+
}),
|
|
970
|
+
environment: Object.freeze({
|
|
971
|
+
tokenizer: Object.freeze({
|
|
972
|
+
id: experiment.environment.tokenizer.id,
|
|
973
|
+
sha256: experiment.environment.tokenizer.sha256,
|
|
974
|
+
}),
|
|
975
|
+
runtime: Object.freeze({
|
|
976
|
+
id: experiment.environment.runtime.id,
|
|
977
|
+
sha256: experiment.environment.runtime.sha256,
|
|
978
|
+
}),
|
|
979
|
+
hardware: Object.freeze({ id: experiment.environment.hardware.id }),
|
|
980
|
+
localModel,
|
|
981
|
+
cache: Object.freeze({
|
|
982
|
+
preparation: experiment.environment.cache.preparation,
|
|
983
|
+
fingerprintSha256: experiment.environment.cache.fingerprintSha256,
|
|
984
|
+
}),
|
|
985
|
+
fourReaderBatch: Object.freeze({
|
|
986
|
+
id: experiment.environment.fourReaderBatch.id,
|
|
987
|
+
sha256: experiment.environment.fourReaderBatch.sha256,
|
|
988
|
+
}),
|
|
989
|
+
incrementalMutation: Object.freeze({
|
|
990
|
+
sourcePath: experiment.environment.incrementalMutation.sourcePath,
|
|
991
|
+
appendUtf8Sha256: experiment.environment.incrementalMutation.appendUtf8Sha256,
|
|
992
|
+
expectedPostMutationSha256:
|
|
993
|
+
experiment.environment.incrementalMutation.expectedPostMutationSha256,
|
|
994
|
+
}),
|
|
995
|
+
}),
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function cloneAssessors(assessors: readonly EvaluationAssessorV2[]): readonly EvaluationAssessorV2[] {
|
|
1000
|
+
const byId = new Map<string, EvaluationAssessorV2>();
|
|
1001
|
+
for (const assessor of assessors) {
|
|
1002
|
+
const cloned = Object.freeze({
|
|
1003
|
+
id: assessor.id,
|
|
1004
|
+
...(assessor.displayName === undefined ? {} : { displayName: assessor.displayName }),
|
|
1005
|
+
...(assessor.affiliation === undefined ? {} : { affiliation: assessor.affiliation }),
|
|
1006
|
+
});
|
|
1007
|
+
const previous = byId.get(assessor.id);
|
|
1008
|
+
if (previous !== undefined && canonicalJson(previous) !== canonicalJson(cloned)) {
|
|
1009
|
+
throw new AuthoringFailure("conflicting-assessor", `Assessor ${assessor.id} has conflicting declarations.`);
|
|
1010
|
+
}
|
|
1011
|
+
byId.set(assessor.id, cloned);
|
|
1012
|
+
}
|
|
1013
|
+
return Object.freeze([...byId.values()].toSorted((left, right) => compareText(left.id, right.id)));
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function cloneMeasurementProfiles(
|
|
1017
|
+
profiles: readonly EvaluationMeasurementProfileV2[],
|
|
1018
|
+
): readonly EvaluationMeasurementProfileV2[] {
|
|
1019
|
+
const byId = new Map<string, EvaluationMeasurementProfileV2>();
|
|
1020
|
+
for (const profile of profiles) {
|
|
1021
|
+
const cloned = Object.freeze({
|
|
1022
|
+
id: profile.id,
|
|
1023
|
+
operation: profile.operation,
|
|
1024
|
+
scope: profile.scope,
|
|
1025
|
+
cacheState: profile.cacheState,
|
|
1026
|
+
concurrency: profile.concurrency,
|
|
1027
|
+
repetitions: profile.repetitions,
|
|
1028
|
+
});
|
|
1029
|
+
const previous = byId.get(profile.id);
|
|
1030
|
+
if (previous !== undefined && canonicalJson(previous) !== canonicalJson(cloned)) {
|
|
1031
|
+
throw new AuthoringFailure("conflicting-measurement-profile", `Measurement profile ${profile.id} conflicts.`);
|
|
1032
|
+
}
|
|
1033
|
+
byId.set(profile.id, cloned);
|
|
1034
|
+
}
|
|
1035
|
+
return Object.freeze([...byId.values()].toSorted((left, right) => compareText(left.id, right.id)));
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function cloneRetrievers(
|
|
1039
|
+
retrievers: readonly EvaluationRetrieverDescriptorV2[],
|
|
1040
|
+
): readonly EvaluationRetrieverDescriptorV2[] {
|
|
1041
|
+
const byId = new Map<string, EvaluationRetrieverDescriptorV2>();
|
|
1042
|
+
for (const retriever of retrievers) {
|
|
1043
|
+
const configuration: Record<string, string | number | boolean | null> = {};
|
|
1044
|
+
for (const key of Object.keys(retriever.configuration).toSorted(compareText)) {
|
|
1045
|
+
const value = retriever.configuration[key];
|
|
1046
|
+
if (value !== undefined) configuration[key] = value;
|
|
1047
|
+
}
|
|
1048
|
+
const cloned = Object.freeze({
|
|
1049
|
+
id: retriever.id,
|
|
1050
|
+
role: retriever.role,
|
|
1051
|
+
version: retriever.version,
|
|
1052
|
+
implementationSha256: retriever.implementationSha256,
|
|
1053
|
+
lanes: uniqueSorted(retriever.lanes) as readonly EvaluationLaneIdV2[],
|
|
1054
|
+
configuration: Object.freeze(configuration),
|
|
1055
|
+
});
|
|
1056
|
+
const previous = byId.get(retriever.id);
|
|
1057
|
+
if (previous !== undefined && canonicalJson(previous) !== canonicalJson(cloned)) {
|
|
1058
|
+
throw new AuthoringFailure("conflicting-retriever", `Retriever ${retriever.id} has conflicting descriptors.`);
|
|
1059
|
+
}
|
|
1060
|
+
byId.set(retriever.id, cloned);
|
|
1061
|
+
}
|
|
1062
|
+
return Object.freeze([...byId.values()].toSorted((left, right) => compareText(left.id, right.id)));
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function issueFromFailure(error: unknown): PromotionCorpusCompilationIssueV2 {
|
|
1066
|
+
if (error instanceof AuthoringFailure) {
|
|
1067
|
+
return Object.freeze({
|
|
1068
|
+
severity: "error",
|
|
1069
|
+
code: error.code,
|
|
1070
|
+
message: error.message,
|
|
1071
|
+
...(error.queryKeys === undefined ? {} : { queryKeys: Object.freeze([...error.queryKeys]) }),
|
|
1072
|
+
...(error.sourcePaths === undefined ? {} : { sourcePaths: Object.freeze([...error.sourcePaths]) }),
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
return Object.freeze({
|
|
1076
|
+
severity: "error",
|
|
1077
|
+
code: "invalid-corpus-authoring-input",
|
|
1078
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function canonicalIssues(
|
|
1083
|
+
issues: readonly PromotionCorpusCompilationIssueV2[],
|
|
1084
|
+
): readonly PromotionCorpusCompilationIssueV2[] {
|
|
1085
|
+
const byKey = new Map<string, PromotionCorpusCompilationIssueV2>();
|
|
1086
|
+
for (const issue of issues) {
|
|
1087
|
+
byKey.set(canonicalJson(issue), issue);
|
|
1088
|
+
}
|
|
1089
|
+
return Object.freeze([...byKey.values()].toSorted((left, right) => (
|
|
1090
|
+
compareText(left.code, right.code) || compareText(left.message, right.message)
|
|
1091
|
+
)));
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
type QuotaQuestion = Pick<
|
|
1095
|
+
HumanAuthoredEvaluationQuestionV2,
|
|
1096
|
+
"cohort" | "expectedSupport" | "inputs" | "primaryStratum" | "rawAssessments" | "split" | "strata"
|
|
1097
|
+
>;
|
|
1098
|
+
|
|
1099
|
+
function quotaEntry(
|
|
1100
|
+
id: string,
|
|
1101
|
+
label: string,
|
|
1102
|
+
rule: "at-least" | "exact",
|
|
1103
|
+
target: number,
|
|
1104
|
+
actual: number,
|
|
1105
|
+
): PromotionQuotaLedgerEntryV2 {
|
|
1106
|
+
return Object.freeze({
|
|
1107
|
+
id,
|
|
1108
|
+
label,
|
|
1109
|
+
rule,
|
|
1110
|
+
target,
|
|
1111
|
+
actual,
|
|
1112
|
+
delta: actual - target,
|
|
1113
|
+
met: rule === "exact" ? actual === target : actual >= target,
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/** Deterministic ledger for the reviewed 48/120, 80/40, 84/84 promotion layout. */
|
|
1118
|
+
export function promotionCorpusQuotaLedgerV2(
|
|
1119
|
+
questions: readonly QuotaQuestion[],
|
|
1120
|
+
): readonly PromotionQuotaLedgerEntryV2[] {
|
|
1121
|
+
const development = questions.filter(({ split }) => split === "development");
|
|
1122
|
+
const test = questions.filter(({ split }) => split === "test");
|
|
1123
|
+
const rows: PromotionQuotaLedgerEntryV2[] = [
|
|
1124
|
+
quotaEntry("all-caller-seeded", "All caller-seeded questions", "exact", PROMOTION_COHORT_COUNT_V2,
|
|
1125
|
+
questions.filter(({ cohort }) => cohort === "caller-seeded").length),
|
|
1126
|
+
quotaEntry("all-questions", "All questions", "exact", PROMOTION_EVALUATION_QUERY_COUNT_V2, questions.length),
|
|
1127
|
+
quotaEntry("all-text-only", "All text-only questions", "exact", PROMOTION_COHORT_COUNT_V2,
|
|
1128
|
+
questions.filter(({ cohort }) => cohort === "text-only").length),
|
|
1129
|
+
quotaEntry("development-caller-seeded", "Development caller-seeded questions", "exact", 24,
|
|
1130
|
+
development.filter(({ cohort }) => cohort === "caller-seeded").length),
|
|
1131
|
+
quotaEntry("development-questions", "Development questions", "exact", PROMOTION_DEVELOPMENT_QUERY_COUNT_V2,
|
|
1132
|
+
development.length),
|
|
1133
|
+
quotaEntry("development-text-only", "Development text-only questions", "exact", 24,
|
|
1134
|
+
development.filter(({ cohort }) => cohort === "text-only").length),
|
|
1135
|
+
quotaEntry("dual-assessment", "Independently dual-assessed questions", "at-least",
|
|
1136
|
+
PROMOTION_DUAL_ASSESSMENT_MINIMUM_V2, questions.filter(({ rawAssessments }) => rawAssessments.length >= 2).length),
|
|
1137
|
+
quotaEntry("test-caller-seeded", "Test caller-seeded questions", "exact", PROMOTION_TEST_COHORT_COUNT_V2,
|
|
1138
|
+
test.filter(({ cohort }) => cohort === "caller-seeded").length),
|
|
1139
|
+
quotaEntry("test-insufficient", "Test insufficient questions", "exact", PROMOTION_TEST_INSUFFICIENT_COUNT_V2,
|
|
1140
|
+
test.filter(({ expectedSupport }) => expectedSupport === "insufficient").length),
|
|
1141
|
+
quotaEntry("test-questions", "Test questions", "exact", PROMOTION_TEST_QUERY_COUNT_V2, test.length),
|
|
1142
|
+
quotaEntry("test-supported", "Test supported questions", "exact", PROMOTION_TEST_SUPPORTED_COUNT_V2,
|
|
1143
|
+
test.filter(({ expectedSupport }) => expectedSupport === "supported").length),
|
|
1144
|
+
quotaEntry("test-text-only", "Test text-only questions", "exact", PROMOTION_TEST_COHORT_COUNT_V2,
|
|
1145
|
+
test.filter(({ cohort }) => cohort === "text-only").length),
|
|
1146
|
+
quotaEntry("test-support-stratum-consistency", "Test support and no-answer stratum consistency", "exact",
|
|
1147
|
+
PROMOTION_TEST_QUERY_COUNT_V2, test.filter((question) =>
|
|
1148
|
+
(question.expectedSupport === "insufficient") === question.strata.includes("no-answer-near-miss")).length),
|
|
1149
|
+
];
|
|
1150
|
+
for (const cohort of ["caller-seeded", "text-only"] as const) {
|
|
1151
|
+
for (const expectedSupport of ["supported", "insufficient"] as const) {
|
|
1152
|
+
rows.push(quotaEntry(
|
|
1153
|
+
`development-${cohort}-${expectedSupport}`,
|
|
1154
|
+
`Development ${cohort} ${expectedSupport} questions`,
|
|
1155
|
+
"exact",
|
|
1156
|
+
12,
|
|
1157
|
+
development.filter((question) =>
|
|
1158
|
+
question.cohort === cohort && question.expectedSupport === expectedSupport).length,
|
|
1159
|
+
));
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
for (const cohort of ["caller-seeded", "text-only"] as const) {
|
|
1163
|
+
rows.push(quotaEntry(
|
|
1164
|
+
`test-${cohort}-supported`,
|
|
1165
|
+
`Test ${cohort} supported questions`,
|
|
1166
|
+
"exact",
|
|
1167
|
+
40,
|
|
1168
|
+
test.filter((question) => question.cohort === cohort && question.expectedSupport === "supported").length,
|
|
1169
|
+
));
|
|
1170
|
+
rows.push(quotaEntry(
|
|
1171
|
+
`test-${cohort}-insufficient`,
|
|
1172
|
+
`Test ${cohort} insufficient questions`,
|
|
1173
|
+
"exact",
|
|
1174
|
+
20,
|
|
1175
|
+
test.filter((question) => question.cohort === cohort && question.expectedSupport === "insufficient").length,
|
|
1176
|
+
));
|
|
1177
|
+
}
|
|
1178
|
+
for (const [stratum, minimum] of Object.entries(PROMOTION_CRITICAL_STRATUM_MINIMA_V2)) {
|
|
1179
|
+
rows.push(quotaEntry(
|
|
1180
|
+
`test-stratum-${stratum}`,
|
|
1181
|
+
`Test ${stratum} stratum`,
|
|
1182
|
+
"at-least",
|
|
1183
|
+
minimum,
|
|
1184
|
+
test.filter((question) => question.strata.includes(stratum as EvaluationStratumV2)).length,
|
|
1185
|
+
));
|
|
1186
|
+
rows.push(quotaEntry(
|
|
1187
|
+
`dual-stratum-${stratum}`,
|
|
1188
|
+
`Dual assessment covering ${stratum}`,
|
|
1189
|
+
"at-least",
|
|
1190
|
+
1,
|
|
1191
|
+
questions.filter((question) =>
|
|
1192
|
+
question.rawAssessments.length >= 2 && question.strata.includes(stratum as EvaluationStratumV2)).length,
|
|
1193
|
+
));
|
|
1194
|
+
}
|
|
1195
|
+
for (const [lane, minimum] of Object.entries(PROMOTION_CRITICAL_INPUT_MINIMA_V2)) {
|
|
1196
|
+
rows.push(quotaEntry(
|
|
1197
|
+
`test-input-${lane}`,
|
|
1198
|
+
`Test executable ${lane} inputs`,
|
|
1199
|
+
"at-least",
|
|
1200
|
+
minimum,
|
|
1201
|
+
test.filter((question) => question.inputs[lane as keyof EvaluationRetrievalInputsV2] !== undefined).length,
|
|
1202
|
+
));
|
|
1203
|
+
}
|
|
1204
|
+
for (const [stratum, minimum] of Object.entries(PROMOTION_ACCEPTANCE_STRATUM_MINIMA_V2)) {
|
|
1205
|
+
rows.push(quotaEntry(
|
|
1206
|
+
`test-primary-${stratum}`,
|
|
1207
|
+
`Test primary ${stratum} questions`,
|
|
1208
|
+
"at-least",
|
|
1209
|
+
minimum,
|
|
1210
|
+
test.filter((question) => question.primaryStratum === stratum).length,
|
|
1211
|
+
));
|
|
1212
|
+
for (const cohort of ["caller-seeded", "text-only"] as const) {
|
|
1213
|
+
const cohortMinimum = PROMOTION_ACCEPTANCE_STRATUM_COHORT_MINIMA_V2[
|
|
1214
|
+
stratum as keyof typeof PROMOTION_ACCEPTANCE_STRATUM_COHORT_MINIMA_V2
|
|
1215
|
+
];
|
|
1216
|
+
const cell = test.filter((question) =>
|
|
1217
|
+
question.cohort === cohort && question.primaryStratum === stratum);
|
|
1218
|
+
rows.push(quotaEntry(
|
|
1219
|
+
`test-${cohort}-primary-${stratum}`,
|
|
1220
|
+
`Test ${cohort} primary ${stratum} questions`,
|
|
1221
|
+
"at-least",
|
|
1222
|
+
cohortMinimum,
|
|
1223
|
+
cell.length,
|
|
1224
|
+
));
|
|
1225
|
+
const requiredDual = Math.max(
|
|
1226
|
+
PROMOTION_STRATUM_COHORT_DUAL_MINIMUM_V2,
|
|
1227
|
+
Math.ceil(cell.length * PROMOTION_STRATUM_COHORT_DUAL_FRACTION_V2),
|
|
1228
|
+
);
|
|
1229
|
+
rows.push(quotaEntry(
|
|
1230
|
+
`dual-test-${cohort}-${stratum}`,
|
|
1231
|
+
`Dual assessment in test ${cohort} ${stratum} cell`,
|
|
1232
|
+
"at-least",
|
|
1233
|
+
requiredDual,
|
|
1234
|
+
cell.filter(({ rawAssessments }) => rawAssessments.length >= 2).length,
|
|
1235
|
+
));
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
for (const cohort of ["caller-seeded", "text-only"] as const) {
|
|
1239
|
+
const cell = test.filter((question) =>
|
|
1240
|
+
question.cohort === cohort && question.strata.includes("no-answer-near-miss"));
|
|
1241
|
+
const requiredDual = Math.max(
|
|
1242
|
+
PROMOTION_STRATUM_COHORT_DUAL_MINIMUM_V2,
|
|
1243
|
+
Math.ceil(cell.length * PROMOTION_STRATUM_COHORT_DUAL_FRACTION_V2),
|
|
1244
|
+
);
|
|
1245
|
+
rows.push(quotaEntry(
|
|
1246
|
+
`dual-test-${cohort}-no-answer-near-miss`,
|
|
1247
|
+
`Dual assessment in test ${cohort} no-answer near-miss cell`,
|
|
1248
|
+
"at-least",
|
|
1249
|
+
requiredDual,
|
|
1250
|
+
cell.filter(({ rawAssessments }) => rawAssessments.length >= 2).length,
|
|
1251
|
+
));
|
|
1252
|
+
}
|
|
1253
|
+
return Object.freeze(rows.toSorted((left, right) => compareText(left.id, right.id)));
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function rounded(value: number): number {
|
|
1257
|
+
return Number(value.toFixed(6));
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function granularityRow(id: string, queryCount: number): PromotionPowerGranularityRowV2 {
|
|
1261
|
+
const fivePercentagePointEventCount = Math.max(1, Math.round(queryCount * 0.05));
|
|
1262
|
+
return Object.freeze({
|
|
1263
|
+
id,
|
|
1264
|
+
queryCount,
|
|
1265
|
+
oneOutcomeStep: Object.freeze({
|
|
1266
|
+
numerator: 1 as const,
|
|
1267
|
+
denominator: queryCount,
|
|
1268
|
+
percentagePoints: rounded(100 / queryCount),
|
|
1269
|
+
}),
|
|
1270
|
+
fivePercentagePointEventCount,
|
|
1271
|
+
nearestObservableFivePointDelta: rounded(100 * fivePercentagePointEventCount / queryCount),
|
|
1272
|
+
representsFivePointsExactly: queryCount % 20 === 0,
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* Reports discrete outcome resolution without pretending that sample size alone establishes power.
|
|
1278
|
+
* Paired power still requires an effect, alpha, desired power, and discordant-pair assumptions.
|
|
1279
|
+
*/
|
|
1280
|
+
export function promotionCorpusPowerGranularityV2(
|
|
1281
|
+
questions: readonly QuotaQuestion[],
|
|
1282
|
+
): PromotionCorpusDiagnosticsV2["powerGranularity"] {
|
|
1283
|
+
const test = questions.filter(({ split }) => split === "test");
|
|
1284
|
+
const groups = new Map<string, number>([
|
|
1285
|
+
["development", questions.filter(({ split }) => split === "development").length],
|
|
1286
|
+
["test", test.length],
|
|
1287
|
+
["test-caller-seeded", test.filter(({ cohort }) => cohort === "caller-seeded").length],
|
|
1288
|
+
["test-insufficient", test.filter(({ expectedSupport }) => expectedSupport === "insufficient").length],
|
|
1289
|
+
["test-supported", test.filter(({ expectedSupport }) => expectedSupport === "supported").length],
|
|
1290
|
+
["test-text-only", test.filter(({ cohort }) => cohort === "text-only").length],
|
|
1291
|
+
]);
|
|
1292
|
+
for (const stratum of Object.keys(PROMOTION_ACCEPTANCE_STRATUM_MINIMA_V2) as EvaluationStratumV2[]) {
|
|
1293
|
+
groups.set(`test-stratum-${stratum}`, test.filter((question) => question.primaryStratum === stratum).length);
|
|
1294
|
+
}
|
|
1295
|
+
groups.set("test-stratum-no-answer-near-miss",
|
|
1296
|
+
test.filter((question) => question.strata.includes("no-answer-near-miss")).length);
|
|
1297
|
+
const rows = [...groups.entries()]
|
|
1298
|
+
.filter((entry): entry is [string, number] => entry[1] > 0)
|
|
1299
|
+
.map(([id, count]) => granularityRow(id, count))
|
|
1300
|
+
.toSorted((left, right) => compareText(left.id, right.id));
|
|
1301
|
+
return Object.freeze({
|
|
1302
|
+
status: "descriptive-only",
|
|
1303
|
+
note: "Outcome granularity is descriptive only. Promotion readiness comes from the sealed prospective paired-power design.",
|
|
1304
|
+
rows: Object.freeze(rows),
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function normalizedPrompt(text: string): string {
|
|
1309
|
+
return text
|
|
1310
|
+
.normalize("NFKC")
|
|
1311
|
+
.toLowerCase()
|
|
1312
|
+
.replace(/[\p{P}\p{S}]+/gu, " ")
|
|
1313
|
+
.replace(/\s+/gu, " ")
|
|
1314
|
+
.trim();
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function ngrams(text: string, size: number): ReadonlySet<string> {
|
|
1318
|
+
const tokens = normalizedPrompt(text).split(" ").filter((token) => token !== "");
|
|
1319
|
+
if (tokens.length === 0) return new Set();
|
|
1320
|
+
if (tokens.length < size) return new Set([tokens.join(" ")]);
|
|
1321
|
+
const result = new Set<string>();
|
|
1322
|
+
for (let index = 0; index <= tokens.length - size; index += 1) {
|
|
1323
|
+
result.add(tokens.slice(index, index + size).join(" "));
|
|
1324
|
+
}
|
|
1325
|
+
return result;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function jaccard(left: ReadonlySet<string>, right: ReadonlySet<string>): number {
|
|
1329
|
+
if (left.size === 0 || right.size === 0) return 0;
|
|
1330
|
+
let intersection = 0;
|
|
1331
|
+
for (const gram of left) if (right.has(gram)) intersection += 1;
|
|
1332
|
+
return intersection / (left.size + right.size - intersection);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function promptTokens(text: string): ReadonlySet<string> {
|
|
1336
|
+
return new Set(normalizedPrompt(text).split(" ").filter((token) => token !== ""));
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function balancedAccuracy(
|
|
1340
|
+
questions: readonly HumanAuthoredEvaluationQuestionV2[],
|
|
1341
|
+
label: (question: HumanAuthoredEvaluationQuestionV2) => string,
|
|
1342
|
+
): { readonly classes: readonly string[]; readonly value: number } | undefined {
|
|
1343
|
+
const classes = uniqueSorted(questions.map(label));
|
|
1344
|
+
if (classes.length < 2 || questions.length < 3) return undefined;
|
|
1345
|
+
const tokenSets = questions.map(({ text }) => promptTokens(text));
|
|
1346
|
+
const correctByClass = new Map(classes.map((class_) => [class_, 0]));
|
|
1347
|
+
const countByClass = new Map(classes.map((class_) => [class_, 0]));
|
|
1348
|
+
for (const [index, question] of questions.entries()) {
|
|
1349
|
+
const actual = label(question);
|
|
1350
|
+
countByClass.set(actual, (countByClass.get(actual) ?? 0) + 1);
|
|
1351
|
+
let nearestIndex: number | undefined;
|
|
1352
|
+
let nearestSimilarity = -1;
|
|
1353
|
+
for (const [candidateIndex, candidate] of questions.entries()) {
|
|
1354
|
+
if (candidateIndex === index) continue;
|
|
1355
|
+
const similarity = jaccard(tokenSets[index] ?? new Set(), tokenSets[candidateIndex] ?? new Set());
|
|
1356
|
+
const nearest = nearestIndex === undefined ? undefined : questions[nearestIndex];
|
|
1357
|
+
if (
|
|
1358
|
+
similarity > nearestSimilarity
|
|
1359
|
+
|| (
|
|
1360
|
+
similarity === nearestSimilarity
|
|
1361
|
+
&& nearest !== undefined
|
|
1362
|
+
&& compareText(candidate.key, nearest.key) < 0
|
|
1363
|
+
)
|
|
1364
|
+
) {
|
|
1365
|
+
nearestIndex = candidateIndex;
|
|
1366
|
+
nearestSimilarity = similarity;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (nearestIndex !== undefined && label(questions[nearestIndex]!) === actual) {
|
|
1370
|
+
correctByClass.set(actual, (correctByClass.get(actual) ?? 0) + 1);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
const recalls = classes.map((class_) => {
|
|
1374
|
+
const count = countByClass.get(class_) ?? 0;
|
|
1375
|
+
return count === 0 ? 0 : (correctByClass.get(class_) ?? 0) / count;
|
|
1376
|
+
});
|
|
1377
|
+
return Object.freeze({ classes, value: rounded(recalls.reduce((sum, value) => sum + value, 0) / recalls.length) });
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
export function promotionCorpusLabelPredictabilityV2(
|
|
1381
|
+
questions: readonly HumanAuthoredEvaluationQuestionV2[],
|
|
1382
|
+
policy: PromotionCorpusReviewPolicyV2 | undefined,
|
|
1383
|
+
): PromotionCorpusDiagnosticsV2["labelPredictability"] {
|
|
1384
|
+
const ceiling = policy?.labelPredictabilityCeiling ?? DEFAULT_LABEL_PREDICTABILITY_CEILING;
|
|
1385
|
+
if (
|
|
1386
|
+
typeof ceiling !== "number"
|
|
1387
|
+
|| !Number.isFinite(ceiling)
|
|
1388
|
+
|| ceiling <= 0
|
|
1389
|
+
|| ceiling > DEFAULT_LABEL_PREDICTABILITY_CEILING
|
|
1390
|
+
) {
|
|
1391
|
+
throw new AuthoringFailure(
|
|
1392
|
+
"invalid-review-policy",
|
|
1393
|
+
`reviewPolicy.labelPredictabilityCeiling cannot weaken the fixed ${DEFAULT_LABEL_PREDICTABILITY_CEILING} balanced-accuracy ceiling and must be positive.`,
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
const definitions = [
|
|
1397
|
+
["cohort", (question: HumanAuthoredEvaluationQuestionV2) => question.cohort],
|
|
1398
|
+
["expected-support", (question: HumanAuthoredEvaluationQuestionV2) => question.expectedSupport],
|
|
1399
|
+
["split", (question: HumanAuthoredEvaluationQuestionV2) => question.split],
|
|
1400
|
+
] as const;
|
|
1401
|
+
const rows = definitions.flatMap(([labelName, value]) => {
|
|
1402
|
+
const result = balancedAccuracy(questions, value);
|
|
1403
|
+
if (result === undefined) return [];
|
|
1404
|
+
return [Object.freeze({
|
|
1405
|
+
label: labelName,
|
|
1406
|
+
classes: result.classes,
|
|
1407
|
+
balancedAccuracy: result.value,
|
|
1408
|
+
evaluatedQuestions: questions.length,
|
|
1409
|
+
ceiling,
|
|
1410
|
+
met: result.value <= ceiling,
|
|
1411
|
+
})];
|
|
1412
|
+
});
|
|
1413
|
+
return Object.freeze({
|
|
1414
|
+
classifier: LABEL_PREDICTABILITY_CLASSIFIER,
|
|
1415
|
+
rows: Object.freeze(rows),
|
|
1416
|
+
met: rows.every(({ met }) => met),
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
export function promotionCorpusDiagnosticsV2(
|
|
1421
|
+
questions: readonly HumanAuthoredEvaluationQuestionV2[],
|
|
1422
|
+
experiment: EvaluationExperimentV2,
|
|
1423
|
+
policy?: PromotionCorpusReviewPolicyV2,
|
|
1424
|
+
independentSourceFamilyClusters?: number,
|
|
1425
|
+
): PromotionCorpusDiagnosticsV2 {
|
|
1426
|
+
const quotaLedger = promotionCorpusQuotaLedgerV2(questions);
|
|
1427
|
+
const labelPredictability = promotionCorpusLabelPredictabilityV2(questions, policy);
|
|
1428
|
+
const eligibleCallerSeededSupportedTestPairs = questions.filter((question) =>
|
|
1429
|
+
question.split === "test"
|
|
1430
|
+
&& question.cohort === "caller-seeded"
|
|
1431
|
+
&& question.expectedSupport === "supported").length;
|
|
1432
|
+
const pairedPower = Object.freeze({
|
|
1433
|
+
eligibleCallerSeededSupportedTestPairs,
|
|
1434
|
+
independentSourceFamilyClusters: independentSourceFamilyClusters ?? null,
|
|
1435
|
+
requiredPairs: experiment.protocol.pairedPower.requiredPairs,
|
|
1436
|
+
met: eligibleCallerSeededSupportedTestPairs >= experiment.protocol.pairedPower.requiredPairs
|
|
1437
|
+
&& independentSourceFamilyClusters !== undefined
|
|
1438
|
+
&& independentSourceFamilyClusters >= experiment.protocol.pairedPower.requiredPairs,
|
|
1439
|
+
});
|
|
1440
|
+
return Object.freeze({
|
|
1441
|
+
quotaLedger,
|
|
1442
|
+
promotionLayoutReady: quotaLedger.every(({ met }) => met)
|
|
1443
|
+
&& labelPredictability.met
|
|
1444
|
+
&& pairedPower.met,
|
|
1445
|
+
labelPredictability,
|
|
1446
|
+
pairedPower,
|
|
1447
|
+
powerGranularity: promotionCorpusPowerGranularityV2(questions),
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
function promptReviewIssues(
|
|
1452
|
+
questions: readonly HumanAuthoredEvaluationQuestionV2[],
|
|
1453
|
+
policy: PromotionCorpusReviewPolicyV2 | undefined,
|
|
1454
|
+
enforceLabelPredictability: boolean,
|
|
1455
|
+
): readonly PromotionCorpusCompilationIssueV2[] {
|
|
1456
|
+
const size = policy?.ngramSize ?? DEFAULT_NGRAM_SIZE;
|
|
1457
|
+
const threshold = policy?.crossSplitNgramThreshold ?? DEFAULT_CROSS_SPLIT_NGRAM_THRESHOLD;
|
|
1458
|
+
if (size !== DEFAULT_NGRAM_SIZE) {
|
|
1459
|
+
throw new AuthoringFailure(
|
|
1460
|
+
"invalid-review-policy",
|
|
1461
|
+
`reviewPolicy.ngramSize cannot weaken the fixed ${DEFAULT_NGRAM_SIZE}-gram leakage review.`,
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
if (
|
|
1465
|
+
typeof threshold !== "number"
|
|
1466
|
+
|| !Number.isFinite(threshold)
|
|
1467
|
+
|| threshold <= 0
|
|
1468
|
+
|| threshold > DEFAULT_CROSS_SPLIT_NGRAM_THRESHOLD
|
|
1469
|
+
) {
|
|
1470
|
+
throw new AuthoringFailure(
|
|
1471
|
+
"invalid-review-policy",
|
|
1472
|
+
`reviewPolicy.crossSplitNgramThreshold must be in (0, ${DEFAULT_CROSS_SPLIT_NGRAM_THRESHOLD}].`,
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
const issues: PromotionCorpusCompilationIssueV2[] = [];
|
|
1476
|
+
const normalizedGroups = new Map<string, HumanAuthoredEvaluationQuestionV2[]>();
|
|
1477
|
+
for (const question of questions) {
|
|
1478
|
+
const normalized = normalizedPrompt(question.text);
|
|
1479
|
+
const group = normalizedGroups.get(normalized) ?? [];
|
|
1480
|
+
group.push(question);
|
|
1481
|
+
normalizedGroups.set(normalized, group);
|
|
1482
|
+
}
|
|
1483
|
+
for (const [normalized, group] of normalizedGroups) {
|
|
1484
|
+
if (normalized === "" || group.length < 2) continue;
|
|
1485
|
+
const spansEvaluationSplits = new Set(group.map(({ split }) => split)).size > 1;
|
|
1486
|
+
issues.push(Object.freeze({
|
|
1487
|
+
severity: spansEvaluationSplits ? "error" : "warning",
|
|
1488
|
+
code: spansEvaluationSplits ? "exact-cross-split-prompt-duplicate" : "duplicate-normalized-prompt",
|
|
1489
|
+
message: `Questions ${group.map(({ key }) => key).toSorted(compareText).join(", ")} share the same normalized prompt${spansEvaluationSplits ? " across evaluation splits" : ""}.`,
|
|
1490
|
+
queryKeys: uniqueSorted(group.map(({ key }) => key)),
|
|
1491
|
+
}));
|
|
1492
|
+
}
|
|
1493
|
+
const development = questions.filter(({ split }) => split === "development");
|
|
1494
|
+
const test = questions.filter(({ split }) => split === "test");
|
|
1495
|
+
const gramCache = new Map<HumanAuthoredEvaluationQuestionV2, ReadonlySet<string>>();
|
|
1496
|
+
for (const question of questions) gramCache.set(question, ngrams(question.text, size));
|
|
1497
|
+
for (const left of development) {
|
|
1498
|
+
for (const right of test) {
|
|
1499
|
+
if (normalizedPrompt(left.text) === normalizedPrompt(right.text)) continue;
|
|
1500
|
+
const overlap = jaccard(gramCache.get(left) ?? new Set(), gramCache.get(right) ?? new Set());
|
|
1501
|
+
if (overlap < threshold) continue;
|
|
1502
|
+
issues.push(Object.freeze({
|
|
1503
|
+
severity: "error",
|
|
1504
|
+
code: "high-cross-split-ngram-overlap",
|
|
1505
|
+
message: `Development question ${left.key} and test question ${right.key} have ${rounded(overlap)} ${size}-gram Jaccard overlap.`,
|
|
1506
|
+
queryKeys: uniqueSorted([left.key, right.key]),
|
|
1507
|
+
overlap: rounded(overlap),
|
|
1508
|
+
}));
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
if (enforceLabelPredictability) {
|
|
1512
|
+
for (const row of promotionCorpusLabelPredictabilityV2(questions, policy).rows) {
|
|
1513
|
+
if (row.met) continue;
|
|
1514
|
+
issues.push(Object.freeze({
|
|
1515
|
+
severity: "error",
|
|
1516
|
+
code: "prompt-label-predictability-ceiling",
|
|
1517
|
+
message: `${row.label} is predictable from prompt text at balanced accuracy ${row.balancedAccuracy}, above the sealed ceiling ${row.ceiling} under ${LABEL_PREDICTABILITY_CLASSIFIER}.`,
|
|
1518
|
+
}));
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return canonicalIssues(issues);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
function familyReview(
|
|
1525
|
+
sourceDocuments: readonly CanonicalSourceDocument[],
|
|
1526
|
+
registry: EvaluationEvidenceRegistry,
|
|
1527
|
+
queries: readonly EvaluationQueryV2[],
|
|
1528
|
+
resolvedEvidence: readonly ResolvedAuthoredEvidenceV2[],
|
|
1529
|
+
): {
|
|
1530
|
+
readonly errors: readonly PromotionCorpusCompilationIssueV2[];
|
|
1531
|
+
readonly warnings: readonly PromotionCorpusCompilationIssueV2[];
|
|
1532
|
+
} {
|
|
1533
|
+
const errors: PromotionCorpusCompilationIssueV2[] = [];
|
|
1534
|
+
const warnings: PromotionCorpusCompilationIssueV2[] = [];
|
|
1535
|
+
const sourceByDocumentId = new Map(sourceDocuments.map((document) => [document.documentId, document]));
|
|
1536
|
+
const documentByEvidenceId = new Map(resolvedEvidence.map((unit) => [
|
|
1537
|
+
unit.corpusEvidenceUnitId,
|
|
1538
|
+
unit.documentId,
|
|
1539
|
+
]));
|
|
1540
|
+
const familySplits = new Map<string, Set<EvaluationSplitV2>>();
|
|
1541
|
+
const pathSplits = new Map<string, Set<EvaluationSplitV2>>();
|
|
1542
|
+
for (const query of queries) {
|
|
1543
|
+
const documentIds = new Set([
|
|
1544
|
+
...query.gold.documents.map(({ documentId }) => documentId),
|
|
1545
|
+
...query.gold.evidenceUnits.map(({ evidenceUnitId }) => documentByEvidenceId.get(evidenceUnitId) ?? ""),
|
|
1546
|
+
]);
|
|
1547
|
+
for (const documentId of documentIds) {
|
|
1548
|
+
const source = sourceByDocumentId.get(documentId);
|
|
1549
|
+
if (source === undefined) continue;
|
|
1550
|
+
const familyClusterId = source.familyAssignmentSha256 === undefined
|
|
1551
|
+
? source.sourceFamilyId
|
|
1552
|
+
: `family-assignment:${source.familyAssignmentSha256}`;
|
|
1553
|
+
const splits = familySplits.get(familyClusterId) ?? new Set<EvaluationSplitV2>();
|
|
1554
|
+
splits.add(query.split);
|
|
1555
|
+
familySplits.set(familyClusterId, splits);
|
|
1556
|
+
const sourceSplits = pathSplits.get(source.sourcePath) ?? new Set<EvaluationSplitV2>();
|
|
1557
|
+
sourceSplits.add(query.split);
|
|
1558
|
+
pathSplits.set(source.sourcePath, sourceSplits);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
const familyKeyById = new Map(sourceDocuments.map((source) => [
|
|
1562
|
+
source.familyAssignmentSha256 === undefined
|
|
1563
|
+
? source.sourceFamilyId
|
|
1564
|
+
: `family-assignment:${source.familyAssignmentSha256}`,
|
|
1565
|
+
source.sourceFamilyKey,
|
|
1566
|
+
]));
|
|
1567
|
+
for (const [familyId, splits] of familySplits) {
|
|
1568
|
+
if (splits.size < 2) continue;
|
|
1569
|
+
const familyKey = familyKeyById.get(familyId) ?? familyId;
|
|
1570
|
+
errors.push(Object.freeze({
|
|
1571
|
+
severity: "error",
|
|
1572
|
+
code: "source-family-split-leakage",
|
|
1573
|
+
message: `Source family ${familyKey} is judged in both development and test splits.`,
|
|
1574
|
+
sourceFamilyKeys: Object.freeze([familyKey]),
|
|
1575
|
+
}));
|
|
1576
|
+
}
|
|
1577
|
+
for (const [sourcePath, splits] of pathSplits) {
|
|
1578
|
+
if (splits.size < 2) continue;
|
|
1579
|
+
errors.push(Object.freeze({
|
|
1580
|
+
severity: "error",
|
|
1581
|
+
code: "source-path-split-leakage",
|
|
1582
|
+
message: `Source path ${sourcePath} is judged in both development and test splits.`,
|
|
1583
|
+
sourcePaths: Object.freeze([sourcePath]),
|
|
1584
|
+
}));
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
const snapshotByPath = new Map(registry.documents.map((document) => [document.sourcePath, document]));
|
|
1588
|
+
const documentsByFamily = new Map<string, CanonicalSourceDocument[]>();
|
|
1589
|
+
for (const document of sourceDocuments) {
|
|
1590
|
+
const familyClusterId = document.familyAssignmentSha256 === undefined
|
|
1591
|
+
? document.sourceFamilyId
|
|
1592
|
+
: `family-assignment:${document.familyAssignmentSha256}`;
|
|
1593
|
+
const group = documentsByFamily.get(familyClusterId) ?? [];
|
|
1594
|
+
group.push(document);
|
|
1595
|
+
documentsByFamily.set(familyClusterId, group);
|
|
1596
|
+
}
|
|
1597
|
+
const familiesByFingerprint = new Map<string, string[]>();
|
|
1598
|
+
for (const [familyId, documents] of documentsByFamily) {
|
|
1599
|
+
const hashes = documents.map((document) => snapshotByPath.get(document.sourcePath)?.sourceSha256 ?? "")
|
|
1600
|
+
.toSorted(compareText);
|
|
1601
|
+
const fingerprint = framedDigest("promotion-corpus-source-family-copy-v1", hashes);
|
|
1602
|
+
const familyIds = familiesByFingerprint.get(fingerprint) ?? [];
|
|
1603
|
+
familyIds.push(familyId);
|
|
1604
|
+
familiesByFingerprint.set(fingerprint, familyIds);
|
|
1605
|
+
}
|
|
1606
|
+
for (const familyIds of familiesByFingerprint.values()) {
|
|
1607
|
+
if (familyIds.length < 2) continue;
|
|
1608
|
+
const familyKeys = uniqueSorted(familyIds.map((id) => familyKeyById.get(id) ?? id));
|
|
1609
|
+
const splits = new Set(familyIds.flatMap((id) => [...(familySplits.get(id) ?? [])]));
|
|
1610
|
+
const issue = Object.freeze({
|
|
1611
|
+
severity: splits.size > 1 ? "error" as const : "warning" as const,
|
|
1612
|
+
code: splits.size > 1 ? "copied-source-family-cross-split" : "copied-source-family",
|
|
1613
|
+
message: `Distinct source-family keys ${familyKeys.join(", ")} contain byte-identical frozen document sets${splits.size > 1 ? " across evaluation splits" : ""}.`,
|
|
1614
|
+
sourceFamilyKeys: familyKeys,
|
|
1615
|
+
});
|
|
1616
|
+
if (issue.severity === "error") errors.push(issue);
|
|
1617
|
+
else warnings.push(issue);
|
|
1618
|
+
}
|
|
1619
|
+
return Object.freeze({ errors: canonicalIssues(errors), warnings: canonicalIssues(warnings) });
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
function evidenceUnitForCorpus(
|
|
1623
|
+
unit: EvaluationEvidenceUnit,
|
|
1624
|
+
source: CanonicalSourceDocument,
|
|
1625
|
+
): EvaluationEvidenceUnitV2 {
|
|
1626
|
+
return Object.freeze({
|
|
1627
|
+
id: unit.id,
|
|
1628
|
+
documentId: unit.documentId,
|
|
1629
|
+
sourceFamilyId: source.sourceFamilyId,
|
|
1630
|
+
trustClass: source.trustClass,
|
|
1631
|
+
sourcePath: unit.sourcePath,
|
|
1632
|
+
lineRange: Object.freeze({ ...unit.lineRange }),
|
|
1633
|
+
headingPath: Object.freeze([...unit.headingAncestry]),
|
|
1634
|
+
...(unit.pdfPage === undefined ? {} : { sourcePage: unit.pdfPage }),
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
/**
|
|
1639
|
+
* Seals an already-authored corpus without consulting results or rankings. Descriptor digests,
|
|
1640
|
+
* the candidate lock, and the corpus commitment are calculated in dependency order. The corpus
|
|
1641
|
+
* digest excludes its own digest fields through evaluationCorpusDigestV2's commitment payload.
|
|
1642
|
+
*/
|
|
1643
|
+
export function sealRetrievalEvaluationCorpusV2(
|
|
1644
|
+
input: RetrievalEvaluationCorpusSealInputV2,
|
|
1645
|
+
): RetrievalEvaluationCorpusSealResultV2 {
|
|
1646
|
+
const buildContractSha256 = canonicalSha256(input.buildContractSha256, "buildContractSha256");
|
|
1647
|
+
const retrievers = [...input.retrievers].toSorted((left, right) => compareText(left.id, right.id));
|
|
1648
|
+
const candidateLock = Object.freeze({
|
|
1649
|
+
baselineRetrieverId: input.baselineRetrieverId,
|
|
1650
|
+
candidateRetrieverIds: Object.freeze(retrievers
|
|
1651
|
+
.filter(({ role }) => role === "candidate")
|
|
1652
|
+
.map(({ id }) => id)
|
|
1653
|
+
.toSorted(compareText)),
|
|
1654
|
+
descriptorDigests: Object.freeze(retrievers.map((descriptor) => Object.freeze({
|
|
1655
|
+
retrieverId: descriptor.id,
|
|
1656
|
+
sha256: evaluationRetrieverDescriptorDigestV2(descriptor),
|
|
1657
|
+
}))),
|
|
1658
|
+
});
|
|
1659
|
+
const candidateLockSha256 = evaluationCandidateLockDigestV2(candidateLock);
|
|
1660
|
+
const draft: RetrievalEvaluationCorpusV2 = {
|
|
1661
|
+
schemaVersion: input.schemaVersion,
|
|
1662
|
+
id: input.id,
|
|
1663
|
+
description: input.description,
|
|
1664
|
+
manifest: {
|
|
1665
|
+
protocol: RETRIEVAL_EVALUATION_V2_PROTOCOL,
|
|
1666
|
+
sealedAt: input.sealedAt,
|
|
1667
|
+
corpusSha256: ZERO_SHA256,
|
|
1668
|
+
candidateLockSha256,
|
|
1669
|
+
buildContractSha256,
|
|
1670
|
+
},
|
|
1671
|
+
frozen: input.frozen,
|
|
1672
|
+
assessment: input.assessment,
|
|
1673
|
+
experiment: input.experiment,
|
|
1674
|
+
sourceFamilies: input.sourceFamilies,
|
|
1675
|
+
documents: input.documents,
|
|
1676
|
+
evidenceUnits: input.evidenceUnits,
|
|
1677
|
+
measurementProfiles: input.measurementProfiles,
|
|
1678
|
+
retrievers,
|
|
1679
|
+
candidateLock,
|
|
1680
|
+
queries: input.queries,
|
|
1681
|
+
};
|
|
1682
|
+
const sealed: RetrievalEvaluationCorpusV2 = {
|
|
1683
|
+
...draft,
|
|
1684
|
+
manifest: {
|
|
1685
|
+
...draft.manifest,
|
|
1686
|
+
corpusSha256: evaluationCorpusDigestV2(draft),
|
|
1687
|
+
},
|
|
1688
|
+
};
|
|
1689
|
+
const corpus = parseRetrievalEvaluationCorpusV2(sealed, { claimPromotion: false });
|
|
1690
|
+
return Object.freeze({
|
|
1691
|
+
corpus,
|
|
1692
|
+
externalSeal: Object.freeze({ expectedCorpusSha256: corpus.manifest.corpusSha256 }),
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function canonicalSourceDocuments(
|
|
1697
|
+
documents: readonly PromotionCorpusMarkdownDocumentV2[],
|
|
1698
|
+
reviewPolicy: PromotionCorpusReviewPolicyV2 | undefined,
|
|
1699
|
+
): readonly CanonicalSourceDocument[] {
|
|
1700
|
+
const protocolInput = reviewPolicy?.sourceFamilyAssignment;
|
|
1701
|
+
const protocol = protocolInput === undefined
|
|
1702
|
+
? undefined
|
|
1703
|
+
: Object.freeze({
|
|
1704
|
+
protocolId: authoredKey(
|
|
1705
|
+
protocolInput.protocolId,
|
|
1706
|
+
"reviewPolicy.sourceFamilyAssignment.protocolId",
|
|
1707
|
+
),
|
|
1708
|
+
protocolSha256: canonicalSha256(
|
|
1709
|
+
protocolInput.protocolSha256,
|
|
1710
|
+
"reviewPolicy.sourceFamilyAssignment.protocolSha256",
|
|
1711
|
+
),
|
|
1712
|
+
reviewerIds: canonicalReviewerIds(
|
|
1713
|
+
protocolInput.reviewerIds,
|
|
1714
|
+
"reviewPolicy.sourceFamilyAssignment.reviewerIds",
|
|
1715
|
+
),
|
|
1716
|
+
});
|
|
1717
|
+
const families = new Map<string, {
|
|
1718
|
+
readonly review: CanonicalSourceFamilyAssignmentReview | undefined;
|
|
1719
|
+
readonly members: {
|
|
1720
|
+
readonly sourcePath: string;
|
|
1721
|
+
readonly sourceClass: EvaluationSourceClassV2;
|
|
1722
|
+
readonly trustClass: EvaluationTrustClassV2;
|
|
1723
|
+
}[];
|
|
1724
|
+
}>();
|
|
1725
|
+
const sourceBindings = new Map<string, { readonly documentId: string; readonly sourceFamilyKey: string }>();
|
|
1726
|
+
const documentBindings = new Map<string, string>();
|
|
1727
|
+
const result = documents.map((document, index): CanonicalSourceDocument => {
|
|
1728
|
+
const sourcePath = confinedPath(document.sourcePath, `documents[${index}].sourcePath`);
|
|
1729
|
+
const documentId = document.documentId === undefined
|
|
1730
|
+
? sourcePath
|
|
1731
|
+
: confinedPath(document.documentId, `documents[${index}].documentId`);
|
|
1732
|
+
const sourceFamilyKey = authoredKey(document.sourceFamilyKey, `documents[${index}].sourceFamilyKey`);
|
|
1733
|
+
const review = canonicalSourceFamilyReview(document, index, protocol?.reviewerIds);
|
|
1734
|
+
if (!EVALUATION_SOURCE_TRUST_COMPATIBILITY_V2[document.sourceClass]?.some(
|
|
1735
|
+
(trustClass) => trustClass === document.trustClass,
|
|
1736
|
+
)) {
|
|
1737
|
+
throw new AuthoringFailure(
|
|
1738
|
+
"incompatible-source-trust",
|
|
1739
|
+
`Source ${sourcePath} cannot combine source class ${document.sourceClass} with trust class ${document.trustClass}.`,
|
|
1740
|
+
{ sourcePaths: [sourcePath] },
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
const sourceBinding = sourceBindings.get(sourcePath);
|
|
1744
|
+
if (sourceBinding !== undefined) {
|
|
1745
|
+
throw new AuthoringFailure(
|
|
1746
|
+
"duplicate-source-path-binding",
|
|
1747
|
+
`Source path ${sourcePath} is bound more than once; every path must name exactly one document and family.`,
|
|
1748
|
+
{ sourcePaths: [sourcePath] },
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
const boundPath = documentBindings.get(documentId);
|
|
1752
|
+
if (boundPath !== undefined) {
|
|
1753
|
+
throw new AuthoringFailure(
|
|
1754
|
+
"duplicate-document-binding",
|
|
1755
|
+
`Document ${documentId} is bound to both ${boundPath} and ${sourcePath}.`,
|
|
1756
|
+
{ sourcePaths: uniqueSorted([boundPath, sourcePath]) },
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
sourceBindings.set(sourcePath, { documentId, sourceFamilyKey });
|
|
1760
|
+
documentBindings.set(documentId, sourcePath);
|
|
1761
|
+
const previous = families.get(sourceFamilyKey);
|
|
1762
|
+
if (previous !== undefined && (
|
|
1763
|
+
previous.review?.rationale !== review?.rationale
|
|
1764
|
+
|| !sameStrings(previous.review?.reviewerIds ?? [], review?.reviewerIds ?? [])
|
|
1765
|
+
)) {
|
|
1766
|
+
throw new AuthoringFailure(
|
|
1767
|
+
"conflicting-source-family-review",
|
|
1768
|
+
`Source family ${sourceFamilyKey} has inconsistent rationales or reviewer assignments.`,
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
const family = previous ?? { review, members: [] };
|
|
1772
|
+
family.members.push({
|
|
1773
|
+
sourcePath,
|
|
1774
|
+
sourceClass: document.sourceClass,
|
|
1775
|
+
trustClass: document.trustClass,
|
|
1776
|
+
});
|
|
1777
|
+
families.set(sourceFamilyKey, family);
|
|
1778
|
+
return Object.freeze({
|
|
1779
|
+
documentId,
|
|
1780
|
+
sourcePath,
|
|
1781
|
+
markdown: document.markdown,
|
|
1782
|
+
sourceFamilyKey,
|
|
1783
|
+
// The private causal-family key may legitimately span heterogeneous
|
|
1784
|
+
// provenance. Keep provenance families homogeneous while binding them to
|
|
1785
|
+
// one reviewed causal assignment below.
|
|
1786
|
+
sourceFamilyId: opaqueId("sf", [sourceFamilyKey, document.sourceClass, document.trustClass]),
|
|
1787
|
+
sourceClass: document.sourceClass,
|
|
1788
|
+
trustClass: document.trustClass,
|
|
1789
|
+
});
|
|
1790
|
+
});
|
|
1791
|
+
|
|
1792
|
+
const reviewOwnerByFingerprint = new Map<string, string>();
|
|
1793
|
+
const assignmentByFamilyKey = new Map<string, string>();
|
|
1794
|
+
for (const [sourceFamilyKey, family] of families) {
|
|
1795
|
+
if (family.review === undefined) continue;
|
|
1796
|
+
if (protocol === undefined) throw new Error("Reviewed source family lost its assignment protocol.");
|
|
1797
|
+
const reviewFingerprint = canonicalJson(family.review);
|
|
1798
|
+
const previousFamilyKey = reviewOwnerByFingerprint.get(reviewFingerprint);
|
|
1799
|
+
if (previousFamilyKey !== undefined && previousFamilyKey !== sourceFamilyKey) {
|
|
1800
|
+
throw new AuthoringFailure(
|
|
1801
|
+
"opaque-source-family-splitting",
|
|
1802
|
+
`Distinct source-family keys ${previousFamilyKey} and ${sourceFamilyKey} reuse the same review rationale and reviewers; independently justify each causal family instead of splitting by note.`,
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
reviewOwnerByFingerprint.set(reviewFingerprint, sourceFamilyKey);
|
|
1806
|
+
assignmentByFamilyKey.set(sourceFamilyKey, framedDigest(
|
|
1807
|
+
"promotion-corpus-source-family-assignment-v1",
|
|
1808
|
+
[canonicalJson({
|
|
1809
|
+
protocol,
|
|
1810
|
+
sourceFamilyKey,
|
|
1811
|
+
rationale: family.review.rationale,
|
|
1812
|
+
reviewerIds: family.review.reviewerIds,
|
|
1813
|
+
members: family.members.toSorted((left, right) => {
|
|
1814
|
+
const pathOrder = compareText(left.sourcePath, right.sourcePath);
|
|
1815
|
+
if (pathOrder !== 0) return pathOrder;
|
|
1816
|
+
const sourceClassOrder = compareText(left.sourceClass, right.sourceClass);
|
|
1817
|
+
return sourceClassOrder !== 0
|
|
1818
|
+
? sourceClassOrder
|
|
1819
|
+
: compareText(left.trustClass, right.trustClass);
|
|
1820
|
+
}),
|
|
1821
|
+
})],
|
|
1822
|
+
));
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
return Object.freeze(result
|
|
1826
|
+
.map((document) => {
|
|
1827
|
+
const familyAssignmentSha256 = assignmentByFamilyKey.get(document.sourceFamilyKey);
|
|
1828
|
+
return Object.freeze({
|
|
1829
|
+
...document,
|
|
1830
|
+
...(familyAssignmentSha256 === undefined ? {} : { familyAssignmentSha256 }),
|
|
1831
|
+
});
|
|
1832
|
+
})
|
|
1833
|
+
.toSorted((left, right) => compareText(left.documentId, right.documentId)));
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
function failedResult(
|
|
1837
|
+
diagnostics: PromotionCorpusDiagnosticsV2,
|
|
1838
|
+
errors: readonly PromotionCorpusCompilationIssueV2[],
|
|
1839
|
+
warnings: readonly PromotionCorpusCompilationIssueV2[],
|
|
1840
|
+
registry?: EvaluationEvidenceRegistry,
|
|
1841
|
+
resolvedEvidence: readonly ResolvedAuthoredEvidenceV2[] = [],
|
|
1842
|
+
): PromotionCorpusCompilationResultV2 {
|
|
1843
|
+
const canonicalErrors = canonicalIssues(errors);
|
|
1844
|
+
const canonicalWarnings = canonicalIssues(warnings);
|
|
1845
|
+
const reviewIssues = canonicalIssues([...canonicalErrors, ...canonicalWarnings].filter(({ code }) =>
|
|
1846
|
+
code.includes("prompt")
|
|
1847
|
+
|| code.includes("ngram")
|
|
1848
|
+
|| code.includes("source-family")
|
|
1849
|
+
|| code.includes("split-leakage")));
|
|
1850
|
+
return Object.freeze({
|
|
1851
|
+
ok: false,
|
|
1852
|
+
...(registry === undefined ? {} : { evidenceRegistry: registry }),
|
|
1853
|
+
resolvedEvidence: Object.freeze([...resolvedEvidence]),
|
|
1854
|
+
diagnostics,
|
|
1855
|
+
errors: canonicalErrors,
|
|
1856
|
+
warnings: canonicalWarnings,
|
|
1857
|
+
reviewIssues,
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
/** Compile a reviewable authored corpus of any size. Promotion quotas remain explicit diagnostics. */
|
|
1862
|
+
export function compileRetrievalEvaluationCorpusAuthoringV2(
|
|
1863
|
+
input: PromotionCorpusAuthoringInputV2,
|
|
1864
|
+
options: { readonly expectedPromotionSeal?: EvaluationExternalCorpusSealV2 } = {},
|
|
1865
|
+
): PromotionCorpusCompilationResultV2 {
|
|
1866
|
+
const questions = input.questions;
|
|
1867
|
+
let diagnostics = promotionCorpusDiagnosticsV2(questions, input.experiment, input.reviewPolicy);
|
|
1868
|
+
const errors: PromotionCorpusCompilationIssueV2[] = [];
|
|
1869
|
+
const warnings: PromotionCorpusCompilationIssueV2[] = [];
|
|
1870
|
+
try {
|
|
1871
|
+
for (const issue of promptReviewIssues(
|
|
1872
|
+
questions,
|
|
1873
|
+
input.reviewPolicy,
|
|
1874
|
+
options.expectedPromotionSeal !== undefined,
|
|
1875
|
+
)) {
|
|
1876
|
+
if (issue.severity === "error") errors.push(issue);
|
|
1877
|
+
else warnings.push(issue);
|
|
1878
|
+
}
|
|
1879
|
+
} catch (error) {
|
|
1880
|
+
errors.push(issueFromFailure(error));
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
let sourceDocuments: readonly CanonicalSourceDocument[];
|
|
1884
|
+
let registry: EvaluationEvidenceRegistry;
|
|
1885
|
+
try {
|
|
1886
|
+
sourceDocuments = canonicalSourceDocuments(input.documents, input.reviewPolicy);
|
|
1887
|
+
registry = buildEvaluationEvidenceRegistry({
|
|
1888
|
+
documents: sourceDocuments.map((document) => ({
|
|
1889
|
+
documentId: document.documentId,
|
|
1890
|
+
sourcePath: document.sourcePath,
|
|
1891
|
+
markdown: document.markdown,
|
|
1892
|
+
trustClass: document.trustClass,
|
|
1893
|
+
})),
|
|
1894
|
+
parserVersion: input.evidenceParserVersion ?? EVALUATION_EVIDENCE_PARSER_VERSION,
|
|
1895
|
+
});
|
|
1896
|
+
} catch (error) {
|
|
1897
|
+
errors.push(issueFromFailure(error));
|
|
1898
|
+
return failedResult(diagnostics, errors, warnings);
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
const corpusDocumentsByPath = new Map(sourceDocuments.map((document) => [document.sourcePath, document]));
|
|
1902
|
+
const context: ResolutionContext = {
|
|
1903
|
+
registry,
|
|
1904
|
+
registryDocumentsByPath: new Map(registry.documents.map((document) => [document.sourcePath, document])),
|
|
1905
|
+
corpusDocumentsByPath,
|
|
1906
|
+
resolvedByRegistryId: new Map(),
|
|
1907
|
+
};
|
|
1908
|
+
const queryKeys = new Set<string>();
|
|
1909
|
+
const queryIds = new Set<string>();
|
|
1910
|
+
const queries: EvaluationQueryV2[] = [];
|
|
1911
|
+
for (const question of questions) {
|
|
1912
|
+
let key: string;
|
|
1913
|
+
try {
|
|
1914
|
+
key = authoredKey(question.key, "question.key");
|
|
1915
|
+
if (queryKeys.has(key)) {
|
|
1916
|
+
throw new AuthoringFailure("duplicate-question-key", `Question key ${key} is duplicated.`, {
|
|
1917
|
+
queryKeys: [key],
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
queryKeys.add(key);
|
|
1921
|
+
const compiled = compileQuestion(question, context);
|
|
1922
|
+
if (queryIds.has(compiled.id)) {
|
|
1923
|
+
throw new AuthoringFailure("opaque-id-collision", `Question ${key} collides with another opaque query ID.`, {
|
|
1924
|
+
queryKeys: [key],
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
queryIds.add(compiled.id);
|
|
1928
|
+
queries.push(compiled);
|
|
1929
|
+
} catch (error) {
|
|
1930
|
+
const issue = issueFromFailure(error);
|
|
1931
|
+
errors.push(issue.queryKeys === undefined && typeof question.key === "string"
|
|
1932
|
+
? Object.freeze({ ...issue, queryKeys: Object.freeze([question.key]) })
|
|
1933
|
+
: issue);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
queries.sort((left, right) => compareText(left.id, right.id));
|
|
1937
|
+
const resolvedEvidence = [...context.resolvedByRegistryId.values()]
|
|
1938
|
+
.toSorted((left, right) => compareText(left.corpusEvidenceUnitId, right.corpusEvidenceUnitId));
|
|
1939
|
+
const familyCheck = familyReview(sourceDocuments, registry, queries, resolvedEvidence);
|
|
1940
|
+
errors.push(...familyCheck.errors);
|
|
1941
|
+
warnings.push(...familyCheck.warnings);
|
|
1942
|
+
if (errors.length > 0) {
|
|
1943
|
+
return failedResult(diagnostics, errors, warnings, registry, resolvedEvidence);
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
try {
|
|
1947
|
+
const sourceFamiliesById = new Map<string, EvaluationSourceFamilyV2>();
|
|
1948
|
+
for (const document of sourceDocuments) {
|
|
1949
|
+
sourceFamiliesById.set(document.sourceFamilyId, Object.freeze({
|
|
1950
|
+
id: document.sourceFamilyId,
|
|
1951
|
+
sourceClass: document.sourceClass,
|
|
1952
|
+
trustClass: document.trustClass,
|
|
1953
|
+
...(document.familyAssignmentSha256 === undefined
|
|
1954
|
+
? {}
|
|
1955
|
+
: { familyAssignmentSha256: document.familyAssignmentSha256 }),
|
|
1956
|
+
}));
|
|
1957
|
+
}
|
|
1958
|
+
const documents: readonly EvaluationDocumentV2[] = Object.freeze(sourceDocuments.map((document) => Object.freeze({
|
|
1959
|
+
id: document.documentId,
|
|
1960
|
+
sourceFamilyId: document.sourceFamilyId,
|
|
1961
|
+
trustClass: document.trustClass,
|
|
1962
|
+
sourcePath: document.sourcePath,
|
|
1963
|
+
})).toSorted((left, right) => compareText(left.id, right.id)));
|
|
1964
|
+
const evidenceUnits: readonly EvaluationEvidenceUnitV2[] = Object.freeze(registry.units.map((unit) => {
|
|
1965
|
+
const source = corpusDocumentsByPath.get(unit.sourcePath);
|
|
1966
|
+
if (source === undefined) throw new Error(`Registry evidence lost source ${unit.sourcePath}.`);
|
|
1967
|
+
return evidenceUnitForCorpus(unit, source);
|
|
1968
|
+
}).toSorted((left, right) => compareText(left.id, right.id)));
|
|
1969
|
+
const testQueries = queries.filter(({ split }) => split === "test");
|
|
1970
|
+
const sourceFamilies = Object.freeze([...sourceFamiliesById.values()]
|
|
1971
|
+
.toSorted((left, right) => compareText(left.id, right.id)));
|
|
1972
|
+
const clusterIdByQuery = evaluationSourceFamilyClusterIdsV2(
|
|
1973
|
+
testQueries,
|
|
1974
|
+
documents,
|
|
1975
|
+
evidenceUnits,
|
|
1976
|
+
sourceFamilies,
|
|
1977
|
+
);
|
|
1978
|
+
const independentSourceFamilyClusters = new Set(testQueries
|
|
1979
|
+
.filter((query) => query.cohort === "caller-seeded" && query.expectedSupport === "supported")
|
|
1980
|
+
.map((query) => clusterIdByQuery.get(query.id))).size;
|
|
1981
|
+
diagnostics = promotionCorpusDiagnosticsV2(
|
|
1982
|
+
questions,
|
|
1983
|
+
input.experiment,
|
|
1984
|
+
input.reviewPolicy,
|
|
1985
|
+
independentSourceFamilyClusters,
|
|
1986
|
+
);
|
|
1987
|
+
if (
|
|
1988
|
+
!diagnostics.promotionLayoutReady
|
|
1989
|
+
&& !warnings.some(({ code }) => code === "promotion-layout-not-ready")
|
|
1990
|
+
) {
|
|
1991
|
+
warnings.push(Object.freeze({
|
|
1992
|
+
severity: "warning",
|
|
1993
|
+
code: "promotion-layout-not-ready",
|
|
1994
|
+
message: "The authored questions do not yet satisfy every exact promotion quota and independent-pair minimum.",
|
|
1995
|
+
}));
|
|
1996
|
+
}
|
|
1997
|
+
const sealed = sealRetrievalEvaluationCorpusV2({
|
|
1998
|
+
schemaVersion: RETRIEVAL_EVALUATION_V2_SCHEMA_VERSION,
|
|
1999
|
+
id: input.id,
|
|
2000
|
+
description: input.description,
|
|
2001
|
+
sealedAt: input.sealedAt,
|
|
2002
|
+
baselineRetrieverId: input.baselineRetrieverId,
|
|
2003
|
+
buildContractSha256: input.buildContractSha256,
|
|
2004
|
+
frozen: Object.freeze({
|
|
2005
|
+
repositoryCommit: input.frozen.repositoryCommit,
|
|
2006
|
+
vaultTree: input.frozen.vaultTree,
|
|
2007
|
+
vaultRoot: input.frozen.vaultRoot,
|
|
2008
|
+
}),
|
|
2009
|
+
assessment: Object.freeze({
|
|
2010
|
+
rubricVersion: input.assessment.rubricVersion,
|
|
2011
|
+
assessors: cloneAssessors(input.assessment.assessors),
|
|
2012
|
+
}),
|
|
2013
|
+
experiment: cloneExperiment(input.experiment),
|
|
2014
|
+
sourceFamilies,
|
|
2015
|
+
documents,
|
|
2016
|
+
evidenceUnits,
|
|
2017
|
+
measurementProfiles: cloneMeasurementProfiles(input.measurementProfiles),
|
|
2018
|
+
retrievers: cloneRetrievers(input.retrievers),
|
|
2019
|
+
queries: Object.freeze(queries),
|
|
2020
|
+
});
|
|
2021
|
+
const finalCorpus = options.expectedPromotionSeal === undefined
|
|
2022
|
+
? sealed.corpus
|
|
2023
|
+
: validatePromotionCorpusV2(sealed.corpus, options.expectedPromotionSeal);
|
|
2024
|
+
const canonicalWarnings = canonicalIssues(warnings);
|
|
2025
|
+
return Object.freeze({
|
|
2026
|
+
ok: true,
|
|
2027
|
+
corpus: finalCorpus,
|
|
2028
|
+
externalSeal: sealed.externalSeal,
|
|
2029
|
+
evidenceRegistry: registry,
|
|
2030
|
+
resolvedEvidence: Object.freeze(resolvedEvidence),
|
|
2031
|
+
diagnostics,
|
|
2032
|
+
errors: Object.freeze([]),
|
|
2033
|
+
warnings: canonicalWarnings,
|
|
2034
|
+
reviewIssues: Object.freeze(canonicalWarnings.filter(({ code }) =>
|
|
2035
|
+
code.includes("prompt") || code.includes("ngram") || code.includes("source-family"))),
|
|
2036
|
+
});
|
|
2037
|
+
} catch (error) {
|
|
2038
|
+
errors.push(Object.freeze({
|
|
2039
|
+
...issueFromFailure(error),
|
|
2040
|
+
code: options.expectedPromotionSeal === undefined ? "invalid-compiled-corpus" : "invalid-promotion-corpus",
|
|
2041
|
+
}));
|
|
2042
|
+
return failedResult(diagnostics, errors, warnings, registry, resolvedEvidence);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
/** Compile and enforce the exact reviewed 168-question promotion design. */
|
|
2047
|
+
export function compilePromotionCorpusAuthoringV2(
|
|
2048
|
+
input: PromotionCorpusAuthoringInputV2,
|
|
2049
|
+
expectedSeal: EvaluationExternalCorpusSealV2,
|
|
2050
|
+
): PromotionCorpusCompilationResultV2 {
|
|
2051
|
+
return compileRetrievalEvaluationCorpusAuthoringV2(input, { expectedPromotionSeal: expectedSeal });
|
|
2052
|
+
}
|