agentera 3.0.0-dev.72 → 3.0.0-dev.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -0
- package/bundle/.agentera-build-source.json +8 -0
- package/bundle/CHANGELOG.md +17 -17
- package/bundle/extract-corpus-parity.json +1 -1
- package/bundle/references/adapters/package-publication.json +49 -53
- package/bundle/references/adapters/package-registry.yaml +5 -1
- package/bundle/references/adapters/package-surface-characterization.md +6 -4
- package/bundle/references/analysis/evidence-tier-authority.yaml +45 -4
- package/bundle/references/analysis/personal-glossary-evaluation-authority.yaml +175 -0
- package/bundle/references/analysis/personal-glossary-evaluation-corpus.yaml +291 -0
- package/bundle/references/analysis/personal-glossary-holdout.yaml +192 -0
- package/bundle/references/analysis/verification-policy.yaml +8 -13
- package/bundle/references/artifacts/glossary-entry-contract.yaml +1533 -35
- package/bundle/references/cli/update-channels.yaml +2 -2
- package/bundle/references/cli/vocabulary.md +1 -1
- package/bundle/references/meta/retained-reference-authority.yaml +16 -1
- package/bundle/skills/agentera/capabilities/profile/schemas/artifacts.yaml +11 -7
- package/bundle/skills/agentera/capabilities/profile/schemas/exit.yaml +6 -3
- package/bundle/skills/agentera/capabilities/profile/schemas/validation.yaml +13 -7
- package/bundle/skills/agentera/schemas/artifacts/glossary.yaml +5 -0
- package/bundle/skills/agentera/schemas/artifacts/plan.yaml +7 -0
- package/dist/.agentera-build-source.json +8 -0
- package/dist/analytics/extractCorpus/copilotSessions.js +13 -4
- package/dist/analytics/extractCorpus/core.js +223 -3
- package/dist/analytics/extractCorpus/corpus.js +18 -1
- package/dist/analytics/extractCorpus/cursorSessions.js +22 -4
- package/dist/analytics/extractCorpus/evidenceTiers.js +123 -17
- package/dist/analytics/extractCorpus/filesystemSources.js +4 -0
- package/dist/analytics/extractCorpus/index.js +2 -2
- package/dist/analytics/extractCorpus/jsonlSessions.js +52 -3
- package/dist/analytics/extractCorpus/sqliteSessions.js +12 -3
- package/dist/analytics/personalGlossaryAdmission.js +26 -46
- package/dist/analytics/personalGlossaryCandidateProjection.js +692 -0
- package/dist/analytics/personalGlossaryCandidateProjectionExcerpts.js +144 -0
- package/dist/analytics/personalGlossaryCandidateReadView.js +23 -0
- package/dist/analytics/personalGlossaryCurrentGeneration.js +25 -0
- package/dist/analytics/personalGlossaryDecision.js +203 -0
- package/dist/analytics/personalGlossaryExplicit.js +788 -0
- package/dist/analytics/personalGlossaryExplicitMining.js +285 -0
- package/dist/analytics/personalGlossaryExplicitScope.js +41 -0
- package/dist/analytics/personalGlossaryExplicitSegments.js +418 -0
- package/dist/analytics/personalGlossaryExplicitTypes.js +26 -0
- package/dist/analytics/personalGlossaryLexicalClassifier.js +175 -0
- package/dist/analytics/personalGlossaryProfile.js +13 -3
- package/dist/analytics/personalGlossaryRecurrence.js +618 -0
- package/dist/analytics/personalGlossaryReviewRecordStorage.js +428 -0
- package/dist/analytics/personalGlossaryReviewRecords.js +332 -0
- package/dist/capabilities/index.js +5 -1
- package/dist/capabilities/profile/instructions.js +50 -15
- package/dist/cli/commands/personalGlossaryCandidateReads.js +744 -0
- package/dist/cli/commands/personalGlossaryDecision.js +349 -0
- package/dist/cli/commands/personalGlossaryPublish.js +484 -0
- package/dist/cli/commands/personalGlossaryReviewRecordReads.js +209 -0
- package/dist/cli/commands/personalGlossaryReviewRecords.js +623 -0
- package/dist/cli/commands/prime/briefOrientation.js +118 -19
- package/dist/cli/commands/report.js +18 -2
- package/dist/cli/commands/schema.js +179 -0
- package/dist/cli/commands/verify.js +51 -9
- package/dist/cli/dispatch/lifecycle.js +51 -6
- package/dist/cli/help.js +48 -2
- package/dist/cli/migrationRequired.js +4 -1
- package/dist/core/atomicWriter.js +4 -1
- package/dist/core/developmentInvocation.js +4 -1
- package/dist/core/yaml.js +53 -1
- package/dist/eval/glossaryEvaluation.js +751 -0
- package/dist/eval/glossaryEvaluationProcess.js +29 -0
- package/dist/eval/glossaryEvaluationRunner.js +571 -0
- package/dist/eval/glossaryEvaluationSuccessReport.js +297 -0
- package/dist/registries/activationTuples.js +31 -3
- package/dist/registries/evidenceTierContract.js +4 -0
- package/dist/registries/explicitSegmentGrammarContract.js +619 -0
- package/dist/registries/glossaryCandidateContractPaths.js +6 -0
- package/dist/registries/glossaryCandidateContracts.js +820 -0
- package/dist/registries/glossaryCandidateDecisionAuthority.js +119 -0
- package/dist/registries/glossaryCandidateDecisionContract.js +62 -0
- package/dist/registries/glossaryCandidateProjectionAuthority.js +197 -0
- package/dist/registries/glossaryCandidateProjectionContract.js +104 -0
- package/dist/registries/glossaryEntryContract.js +105 -103
- package/dist/registries/glossaryEntryTemporal.js +12 -0
- package/dist/registries/glossaryMiningAuthority.js +764 -0
- package/dist/registries/glossaryProfileFullContract.js +24 -0
- package/dist/registries/glossaryReviewRecordsAuthority.js +426 -0
- package/dist/registries/glossaryReviewRecordsContract.js +129 -0
- package/dist/registries/glossaryTermIdentity.js +122 -0
- package/dist/registries/packagePublication.js +3 -3
- package/dist/registries/personalGlossaryContracts.js +1 -0
- package/dist/release/releaseMetadata.js +15 -8
- package/dist/upgrade/npxPlatformStatus.js +2 -1
- package/dist/upgrade/projectIntegration.js +7 -1
- package/dist/validate/activationArtifactEvidence.js +18 -4
- package/dist/validate/activationEvidenceManifest.js +101 -22
- package/package.json +2 -4
- package/dist/cli/commands/personalGlossary.js +0 -176
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import YAML from "yaml";
|
|
5
|
+
import { resolveSourceRoot } from "../core/sourceRoot.js";
|
|
6
|
+
import { loadYamlMappingFile } from "../core/yaml.js";
|
|
7
|
+
export const GLOSSARY_EVALUATION_AUTHORITY_SCHEMA_VERSION = "agentera.personalGlossaryEvaluationAuthority.v1";
|
|
8
|
+
export const GLOSSARY_HOLDOUT_SCHEMA_VERSION = "agentera.personalGlossaryEvaluationHoldout.v1";
|
|
9
|
+
export const GLOSSARY_BEHAVIOR_FIXTURE_SCHEMA_VERSION = "agentera.personalGlossaryEvaluationBehaviorFixture.v1";
|
|
10
|
+
export const GLOSSARY_OBSERVATIONS_SCHEMA_VERSION = "agentera.personalGlossaryEvaluationObservations.v2";
|
|
11
|
+
export const GLOSSARY_EVALUATION_SCHEMA_VERSION = "agentera.personalGlossaryEvaluation.v1";
|
|
12
|
+
export const ONE_SIDED_95_WILSON_Z = 1.6448536269514722;
|
|
13
|
+
export const GLOSSARY_METRIC_IDS = [
|
|
14
|
+
"discovery_recall",
|
|
15
|
+
"scope_accuracy",
|
|
16
|
+
"inferred_review_precision",
|
|
17
|
+
"explicit_admission_precision",
|
|
18
|
+
];
|
|
19
|
+
export const GLOSSARY_EVALUATION_CASE_SET_IDS = [
|
|
20
|
+
"discovery",
|
|
21
|
+
"scope",
|
|
22
|
+
"inferred_review",
|
|
23
|
+
"explicit_admission",
|
|
24
|
+
];
|
|
25
|
+
const MAX_BEHAVIOR_FIXTURE_UTF8_BYTES = 65_536;
|
|
26
|
+
const MAX_BEHAVIOR_FIXTURE_ALIASES = 256;
|
|
27
|
+
const FORBIDDEN_PRIVATE_FIELDS = new Set([
|
|
28
|
+
"raw_text",
|
|
29
|
+
"transcript",
|
|
30
|
+
"content",
|
|
31
|
+
"profile_path",
|
|
32
|
+
"personal_history",
|
|
33
|
+
"secret",
|
|
34
|
+
"password",
|
|
35
|
+
"api_key",
|
|
36
|
+
"credential",
|
|
37
|
+
]);
|
|
38
|
+
const FORBIDDEN_LABEL_FIELDS = /^(?:expected_|observed_|label$|labels$)/u;
|
|
39
|
+
export function mapping(value) {
|
|
40
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
41
|
+
? value
|
|
42
|
+
: null;
|
|
43
|
+
}
|
|
44
|
+
export function nonEmptyString(value) {
|
|
45
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
46
|
+
}
|
|
47
|
+
function strings(value) {
|
|
48
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string")
|
|
49
|
+
? value
|
|
50
|
+
: [];
|
|
51
|
+
}
|
|
52
|
+
export function mappings(value) {
|
|
53
|
+
return Array.isArray(value)
|
|
54
|
+
? value.flatMap((item) => {
|
|
55
|
+
const record = mapping(item);
|
|
56
|
+
return record === null ? [] : [record];
|
|
57
|
+
})
|
|
58
|
+
: [];
|
|
59
|
+
}
|
|
60
|
+
function isLowerSha256(value) {
|
|
61
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
|
|
62
|
+
}
|
|
63
|
+
function isFiniteNumber(value) {
|
|
64
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
65
|
+
}
|
|
66
|
+
export function sha256(value) {
|
|
67
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
68
|
+
}
|
|
69
|
+
function canonical(value) {
|
|
70
|
+
if (Array.isArray(value))
|
|
71
|
+
return value.map(canonical);
|
|
72
|
+
if (value !== null && typeof value === "object") {
|
|
73
|
+
return Object.fromEntries(Object.entries(value)
|
|
74
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
75
|
+
.map(([key, entry]) => [key, canonical(entry)]));
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
export function canonicalDigest(value) {
|
|
80
|
+
return sha256(JSON.stringify(canonical(value)));
|
|
81
|
+
}
|
|
82
|
+
/** Count the contract-owned frozen cases in each glossary evaluation family. */
|
|
83
|
+
export function glossaryEvaluationCaseCounts(value) {
|
|
84
|
+
return Object.fromEntries(GLOSSARY_EVALUATION_CASE_SET_IDS.map((key) => [key, Array.isArray(value[key]) ? value[key].length : 0]));
|
|
85
|
+
}
|
|
86
|
+
/** Digest the frozen labels without carrying them into evaluator output. */
|
|
87
|
+
export function glossaryEvaluationLabelDigest(holdout) {
|
|
88
|
+
return canonicalDigest(Object.fromEntries([
|
|
89
|
+
["discovery", mappings(holdout.discovery).map(({ id, expected_discoverable }) => ({ id, expected_discoverable }))],
|
|
90
|
+
["scope", mappings(holdout.scope).map(({ id, expected_scope }) => ({ id, expected_scope }))],
|
|
91
|
+
["inferred_review", mappings(holdout.inferred_review).map(({ id, expected_reviewable }) => ({ id, expected_reviewable }))],
|
|
92
|
+
["explicit_admission", mappings(holdout.explicit_admission).map(({ id, expected_admissible }) => ({ id, expected_admissible }))],
|
|
93
|
+
]));
|
|
94
|
+
}
|
|
95
|
+
function sameStrings(actual, expected) {
|
|
96
|
+
return JSON.stringify(strings(actual)) === JSON.stringify(expected);
|
|
97
|
+
}
|
|
98
|
+
function exactFields(record, expected, source, errors) {
|
|
99
|
+
for (const field of Object.keys(record)) {
|
|
100
|
+
if (!expected.includes(field))
|
|
101
|
+
errors.push(`${source}.${field} is not an allowed field`);
|
|
102
|
+
}
|
|
103
|
+
for (const field of expected) {
|
|
104
|
+
if (!(field in record))
|
|
105
|
+
errors.push(`${source}.${field} is required`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function glossaryEvaluationAuthorityPath(root = resolveSourceRoot()) {
|
|
109
|
+
return path.join(root, "references", "analysis", "personal-glossary-evaluation-authority.yaml");
|
|
110
|
+
}
|
|
111
|
+
export function glossaryEvaluationHoldoutPath(root = resolveSourceRoot()) {
|
|
112
|
+
return path.join(root, "references", "analysis", "personal-glossary-holdout.yaml");
|
|
113
|
+
}
|
|
114
|
+
export function glossaryEvaluationBehaviorFixturePath(root = resolveSourceRoot()) {
|
|
115
|
+
return path.join(root, "references", "analysis", "personal-glossary-evaluation-corpus.yaml");
|
|
116
|
+
}
|
|
117
|
+
export function loadGlossaryEvaluationAuthority(root = resolveSourceRoot()) {
|
|
118
|
+
return loadYamlMappingFile(glossaryEvaluationAuthorityPath(root));
|
|
119
|
+
}
|
|
120
|
+
export function loadGlossaryEvaluationHoldout(root = resolveSourceRoot()) {
|
|
121
|
+
return loadYamlMappingFile(glossaryEvaluationHoldoutPath(root));
|
|
122
|
+
}
|
|
123
|
+
export function loadGlossaryEvaluationBehaviorFixture(root = resolveSourceRoot()) {
|
|
124
|
+
const bytes = fs.readFileSync(glossaryEvaluationBehaviorFixturePath(root));
|
|
125
|
+
return parseGlossaryEvaluationBehaviorFixture(bytes);
|
|
126
|
+
}
|
|
127
|
+
function parseGlossaryEvaluationBehaviorFixture(bytes) {
|
|
128
|
+
if (bytes.length > MAX_BEHAVIOR_FIXTURE_UTF8_BYTES) {
|
|
129
|
+
throw new Error("behavior fixture exceeds its UTF-8 bound");
|
|
130
|
+
}
|
|
131
|
+
const value = YAML.parse(bytes.toString("utf8"), { maxAliasCount: MAX_BEHAVIOR_FIXTURE_ALIASES });
|
|
132
|
+
const parsed = mapping(value);
|
|
133
|
+
if (parsed === null)
|
|
134
|
+
throw new Error("YAML root must be a mapping");
|
|
135
|
+
return parsed;
|
|
136
|
+
}
|
|
137
|
+
function metricMapping(authority, metric) {
|
|
138
|
+
return mapping(mapping(authority.metrics)?.[metric]);
|
|
139
|
+
}
|
|
140
|
+
export function metricRule(authority, metric) {
|
|
141
|
+
const raw = metricMapping(authority, metric);
|
|
142
|
+
const uncertainty = mapping(raw?.uncertainty);
|
|
143
|
+
if (!raw || !uncertainty)
|
|
144
|
+
throw new Error(`metric authority is missing ${metric}`);
|
|
145
|
+
return {
|
|
146
|
+
definition: String(raw.definition ?? ""),
|
|
147
|
+
numerator: String(raw.numerator ?? ""),
|
|
148
|
+
denominator: String(raw.denominator ?? ""),
|
|
149
|
+
denominatorZero: String(raw.denominator_zero ?? ""),
|
|
150
|
+
sampleUnit: String(raw.sample_unit ?? ""),
|
|
151
|
+
minimumSampleSize: Number(raw.minimum_sample_size),
|
|
152
|
+
pointEstimateMinimum: Number(raw.point_estimate_minimum),
|
|
153
|
+
uncertaintyMethod: String(uncertainty.method ?? ""),
|
|
154
|
+
confidence: Number(uncertainty.confidence),
|
|
155
|
+
lowerBoundMinimum: Number(uncertainty.lower_bound_minimum),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const METRIC_SHAPE = {
|
|
159
|
+
discovery_recall: {
|
|
160
|
+
numerator: "expected_discoverable_true_and_observed_discovered_true",
|
|
161
|
+
denominator: "count_expected_discoverable_true",
|
|
162
|
+
sampleUnit: "labeled_discovery_example",
|
|
163
|
+
minimumSampleSize: 20,
|
|
164
|
+
pointEstimateMinimum: 0.9,
|
|
165
|
+
lowerBoundMinimum: 0.8,
|
|
166
|
+
},
|
|
167
|
+
scope_accuracy: {
|
|
168
|
+
numerator: "expected_scope_equals_observed_scope",
|
|
169
|
+
denominator: "count_all_scope_labeled_examples",
|
|
170
|
+
sampleUnit: "labeled_scope_example",
|
|
171
|
+
minimumSampleSize: 20,
|
|
172
|
+
pointEstimateMinimum: 0.95,
|
|
173
|
+
lowerBoundMinimum: 0.8,
|
|
174
|
+
labels: ["personal", "project", "neither"],
|
|
175
|
+
},
|
|
176
|
+
inferred_review_precision: {
|
|
177
|
+
numerator: "expected_reviewable_true_and_observed_reviewed_true",
|
|
178
|
+
denominator: "count_observed_reviewed_true",
|
|
179
|
+
sampleUnit: "inferred_review_suggestion",
|
|
180
|
+
minimumSampleSize: 20,
|
|
181
|
+
pointEstimateMinimum: 0.95,
|
|
182
|
+
lowerBoundMinimum: 0.8,
|
|
183
|
+
},
|
|
184
|
+
explicit_admission_precision: {
|
|
185
|
+
numerator: "expected_admissible_true_and_observed_admitted_true",
|
|
186
|
+
denominator: "count_observed_admitted_true",
|
|
187
|
+
sampleUnit: "explicit_automatic_admission",
|
|
188
|
+
minimumSampleSize: 50,
|
|
189
|
+
pointEstimateMinimum: 0.99,
|
|
190
|
+
lowerBoundMinimum: 0.9,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
export function validateGlossaryEvaluationAuthority(authority) {
|
|
194
|
+
const errors = [];
|
|
195
|
+
if (authority.schema_version !== GLOSSARY_EVALUATION_AUTHORITY_SCHEMA_VERSION) {
|
|
196
|
+
errors.push("evaluation authority schema_version is invalid");
|
|
197
|
+
}
|
|
198
|
+
if (authority.status !== "active_authority") {
|
|
199
|
+
errors.push("evaluation authority is not active_authority");
|
|
200
|
+
}
|
|
201
|
+
const holdout = mapping(authority.holdout);
|
|
202
|
+
const holdoutLabels = mapping(holdout?.labels);
|
|
203
|
+
const provenance = mapping(holdout?.provenance);
|
|
204
|
+
const synthetic = mapping(provenance?.synthetic);
|
|
205
|
+
const consented = mapping(provenance?.consented);
|
|
206
|
+
const observations = mapping(authority.observations);
|
|
207
|
+
const behaviorFixture = mapping(observations?.behavior_fixture);
|
|
208
|
+
const results = mapping(authority.results);
|
|
209
|
+
const metricsDigest = mapping(results?.metrics_sha256);
|
|
210
|
+
if (!holdout || holdout.status !== "frozen" || holdout.identity !== "content_addressed_fixture_bytes") {
|
|
211
|
+
errors.push("evaluation authority holdout must be a frozen content-addressed fixture");
|
|
212
|
+
}
|
|
213
|
+
if (holdout?.path !== "references/analysis/personal-glossary-holdout.yaml" ||
|
|
214
|
+
!isLowerSha256(holdout?.fixture_sha256)) {
|
|
215
|
+
errors.push("evaluation authority holdout path and fixture_sha256 are required");
|
|
216
|
+
}
|
|
217
|
+
if (holdoutLabels?.source !== "evaluation_authority" ||
|
|
218
|
+
holdoutLabels?.expected_labels_immutable !== true ||
|
|
219
|
+
holdoutLabels?.relabeling !== "forbidden_after_freeze" ||
|
|
220
|
+
holdoutLabels?.changed_fixture !== "fail_before_metrics" ||
|
|
221
|
+
holdoutLabels?.failure_recording !== "append_only") {
|
|
222
|
+
errors.push("evaluation authority labels must be immutable and fail before metrics when changed");
|
|
223
|
+
}
|
|
224
|
+
if (!strings(provenance?.allowed_record_classes).includes("synthetic") ||
|
|
225
|
+
!strings(provenance?.allowed_record_classes).includes("consented") ||
|
|
226
|
+
provenance?.default_record_class !== "synthetic" ||
|
|
227
|
+
synthetic?.consent !== "not_required_synthetic" ||
|
|
228
|
+
synthetic?.raw_personal_history !== "forbidden" ||
|
|
229
|
+
synthetic?.source_rule !== "generated_or_hand_authored_synthetic_records_only" ||
|
|
230
|
+
consented?.raw_personal_history !== "allowed_only_with_fixture_record_consent" ||
|
|
231
|
+
!sameStrings(consented?.required_fields, ["consent_subject", "consent_receipt", "consented_at"]) ||
|
|
232
|
+
consented?.source_rule !== "each_record_must_bind_to_its_own_consent_provenance" ||
|
|
233
|
+
provenance?.missing_or_invalid !== "reject_before_metrics") {
|
|
234
|
+
errors.push("evaluation authority provenance must be synthetic by default and consent-bound otherwise");
|
|
235
|
+
}
|
|
236
|
+
if (observations?.schema_version !== GLOSSARY_OBSERVATIONS_SCHEMA_VERSION ||
|
|
237
|
+
observations?.source !== "current_product_behavior" ||
|
|
238
|
+
!sameStrings(observations?.required_fields, [
|
|
239
|
+
"schema_version",
|
|
240
|
+
"holdout_id",
|
|
241
|
+
"holdout_fixture_sha256",
|
|
242
|
+
"behavior_fixture_sha256",
|
|
243
|
+
"discovery",
|
|
244
|
+
"scope",
|
|
245
|
+
"inferred_review",
|
|
246
|
+
"explicit_admission",
|
|
247
|
+
]) ||
|
|
248
|
+
observations?.digest_binding !== "holdout_fixture_sha256_must_equal_frozen_fixture_digest" ||
|
|
249
|
+
observations?.coverage !== "exact_frozen_id_set_per_metric" ||
|
|
250
|
+
observations?.missing !== "not_run_non_authorizing" ||
|
|
251
|
+
observations?.invalid !== "reject_before_metrics") {
|
|
252
|
+
errors.push("evaluation authority observations must come from current product behavior");
|
|
253
|
+
}
|
|
254
|
+
if (behaviorFixture?.path !== "references/analysis/personal-glossary-evaluation-corpus.yaml" ||
|
|
255
|
+
!isLowerSha256(behaviorFixture?.fixture_sha256) ||
|
|
256
|
+
behaviorFixture?.status !== "frozen" ||
|
|
257
|
+
behaviorFixture?.identity !== "content_addressed_fixture_bytes" ||
|
|
258
|
+
behaviorFixture?.provenance !== "synthetic_only") {
|
|
259
|
+
errors.push("evaluation authority behavior fixture must be frozen, content-addressed, and synthetic-only");
|
|
260
|
+
}
|
|
261
|
+
if (metricsDigest?.field !== "metrics_sha256" ||
|
|
262
|
+
metricsDigest?.algorithm !== "sha256(canonical_json_metrics_array)") {
|
|
263
|
+
errors.push("evaluation authority results must bind the canonical metric array digest");
|
|
264
|
+
}
|
|
265
|
+
for (const metric of GLOSSARY_METRIC_IDS) {
|
|
266
|
+
const raw = metricMapping(authority, metric);
|
|
267
|
+
const uncertainty = mapping(raw?.uncertainty);
|
|
268
|
+
const expected = METRIC_SHAPE[metric];
|
|
269
|
+
if (!raw ||
|
|
270
|
+
raw.numerator !== expected.numerator ||
|
|
271
|
+
raw.denominator !== expected.denominator ||
|
|
272
|
+
raw.sample_unit !== expected.sampleUnit ||
|
|
273
|
+
raw.minimum_sample_size !== expected.minimumSampleSize ||
|
|
274
|
+
raw.point_estimate_minimum !== expected.pointEstimateMinimum ||
|
|
275
|
+
raw.denominator_zero !== "fail" ||
|
|
276
|
+
uncertainty?.method !== "wilson_lower_bound" ||
|
|
277
|
+
uncertainty?.confidence !== 0.95 ||
|
|
278
|
+
uncertainty?.lower_bound_minimum !== expected.lowerBoundMinimum ||
|
|
279
|
+
(expected.labels !== undefined && !sameStrings(raw.labels, expected.labels))) {
|
|
280
|
+
errors.push(`evaluation authority metric ${metric} does not preserve its approved denominator or threshold`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const uncertainty = mapping(authority.uncertainty);
|
|
284
|
+
const gates = mapping(authority.gates);
|
|
285
|
+
const releaseGate = mapping(gates?.release_authorizing_gate);
|
|
286
|
+
const inferredAuto = mapping(gates?.inferred_automatic_admission);
|
|
287
|
+
if (uncertainty?.method !== "one_sided_wilson_lower_bound" ||
|
|
288
|
+
uncertainty?.confidence !== 0.95 ||
|
|
289
|
+
!nonEmptyString(uncertainty?.rule) ||
|
|
290
|
+
uncertainty?.rounding !== "report_full_precision_compare_unrounded" ||
|
|
291
|
+
gates === null ||
|
|
292
|
+
mapping(gates?.exploratory_report)?.release_authorizing !== false ||
|
|
293
|
+
releaseGate?.release_authorizing !== true ||
|
|
294
|
+
!sameStrings(releaseGate?.requirements, [
|
|
295
|
+
"frozen_holdout_digest_matches_authority",
|
|
296
|
+
"observations_exactly_cover_frozen_ids",
|
|
297
|
+
"every_metric_has_a_nonzero_denominator",
|
|
298
|
+
"every_metric_meets_minimum_sample_size",
|
|
299
|
+
"every_metric_meets_point_threshold",
|
|
300
|
+
"every_metric_meets_uncertainty_threshold",
|
|
301
|
+
"inferred_automatic_admission_remains_disabled",
|
|
302
|
+
]) ||
|
|
303
|
+
inferredAuto?.status !== "disabled" ||
|
|
304
|
+
inferredAuto?.measured_result !== "cannot_enable" ||
|
|
305
|
+
!nonEmptyString(inferredAuto?.rule)) {
|
|
306
|
+
errors.push("evaluation authority must distinguish exploratory reports from the release gate");
|
|
307
|
+
}
|
|
308
|
+
const explicitGate = mapping(gates?.explicit_admission);
|
|
309
|
+
const reviewGate = mapping(gates?.inferred_review);
|
|
310
|
+
if (explicitGate?.metric !== "explicit_admission_precision" ||
|
|
311
|
+
explicitGate?.outcome !== "explicit_automatic_admission_only" ||
|
|
312
|
+
reviewGate?.metric !== "inferred_review_precision" ||
|
|
313
|
+
reviewGate?.outcome !== "review_suggestions_only" ||
|
|
314
|
+
mapping(authority.preserved_authority)?.shared_primitive !==
|
|
315
|
+
"references/artifacts/glossary-entry-contract.yaml#shared_primitive" ||
|
|
316
|
+
mapping(authority.preserved_authority)?.personal_owner !==
|
|
317
|
+
"references/artifacts/glossary-entry-contract.yaml#ownership_contracts.personal" ||
|
|
318
|
+
mapping(authority.preserved_authority)?.project_owner !==
|
|
319
|
+
"references/artifacts/glossary-entry-contract.yaml#ownership_contracts.project" ||
|
|
320
|
+
mapping(authority.preserved_authority)?.consumer_precedence !==
|
|
321
|
+
"references/artifacts/glossary-entry-contract.yaml#consumer_boundary.primary_selection" ||
|
|
322
|
+
mapping(authority.preserved_authority)?.project_publication !==
|
|
323
|
+
"references/artifacts/glossary-entry-contract.yaml#ownership_contracts.project.publication" ||
|
|
324
|
+
mapping(authority.preserved_authority)?.inferred_automatic_admission !== "disabled") {
|
|
325
|
+
errors.push("evaluation authority admission gates must preserve review-only inferred outcomes");
|
|
326
|
+
}
|
|
327
|
+
return errors;
|
|
328
|
+
}
|
|
329
|
+
function validateRecordProvenance(record, source, errors, sourceNamespace, syntheticOnly = false) {
|
|
330
|
+
const provenance = mapping(record.provenance);
|
|
331
|
+
if (!provenance) {
|
|
332
|
+
errors.push(`${source}.provenance is required`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (provenance.record_class !== "synthetic" && provenance.record_class !== "consented") {
|
|
336
|
+
errors.push(`${source}.provenance.record_class is not allowed`);
|
|
337
|
+
}
|
|
338
|
+
if (provenance.record_class === "synthetic" &&
|
|
339
|
+
(provenance.consent !== "not_required_synthetic" ||
|
|
340
|
+
provenance.contains_personal_history !== false ||
|
|
341
|
+
!nonEmptyString(provenance.source_namespace) ||
|
|
342
|
+
!provenance.source_namespace.startsWith(sourceNamespace))) {
|
|
343
|
+
errors.push(`${source}.provenance synthetic record is not privacy-safe`);
|
|
344
|
+
}
|
|
345
|
+
if (provenance.record_class === "consented" &&
|
|
346
|
+
(!nonEmptyString(provenance.consent_subject) ||
|
|
347
|
+
!nonEmptyString(provenance.consent_receipt) ||
|
|
348
|
+
!nonEmptyString(provenance.consented_at))) {
|
|
349
|
+
errors.push(`${source}.provenance consented record lacks consent provenance`);
|
|
350
|
+
}
|
|
351
|
+
if (syntheticOnly && provenance.record_class !== "synthetic") {
|
|
352
|
+
errors.push(`${source}.provenance must be synthetic`);
|
|
353
|
+
}
|
|
354
|
+
for (const key of Object.keys(record)) {
|
|
355
|
+
if (FORBIDDEN_PRIVATE_FIELDS.has(key)) {
|
|
356
|
+
errors.push(`${source} contains forbidden private content field ${key}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for (const key of Object.keys(provenance)) {
|
|
360
|
+
if (FORBIDDEN_PRIVATE_FIELDS.has(key)) {
|
|
361
|
+
errors.push(`${source}.provenance contains forbidden private content field ${key}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function validateCaseList(holdout, key, requiredFields, checkFields, seenIds, seenSourceIds, errors) {
|
|
366
|
+
const records = holdout[key];
|
|
367
|
+
if (!Array.isArray(records)) {
|
|
368
|
+
errors.push(`holdout.${key} must be a list`);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
for (const [index, value] of records.entries()) {
|
|
372
|
+
const source = `holdout.${key}[${index}]`;
|
|
373
|
+
const record = mapping(value);
|
|
374
|
+
if (!record) {
|
|
375
|
+
errors.push(`${source} must be a mapping`);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const id = record.id;
|
|
379
|
+
const sourceId = record.source_id;
|
|
380
|
+
if (!nonEmptyString(id) || seenIds.has(id))
|
|
381
|
+
errors.push(`${source}.id must be unique and non-empty`);
|
|
382
|
+
if (!nonEmptyString(sourceId) || seenSourceIds.has(sourceId)) {
|
|
383
|
+
errors.push(`${source}.source_id must be unique and non-empty`);
|
|
384
|
+
}
|
|
385
|
+
if (nonEmptyString(id))
|
|
386
|
+
seenIds.add(id);
|
|
387
|
+
if (nonEmptyString(sourceId))
|
|
388
|
+
seenSourceIds.add(sourceId);
|
|
389
|
+
exactFields(record, requiredFields, source, errors);
|
|
390
|
+
validateRecordProvenance(record, source, errors, "synthetic://personal-glossary/");
|
|
391
|
+
checkFields(record, source, errors);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
export function validateFrozenGlossaryHoldout(authority, holdout, fixtureBytes) {
|
|
395
|
+
const errors = [];
|
|
396
|
+
if (holdout.schema_version !== GLOSSARY_HOLDOUT_SCHEMA_VERSION) {
|
|
397
|
+
errors.push("holdout schema_version is invalid");
|
|
398
|
+
}
|
|
399
|
+
if (holdout.status !== "frozen" || !nonEmptyString(holdout.holdout_id)) {
|
|
400
|
+
errors.push("holdout must have a frozen holdout_id");
|
|
401
|
+
}
|
|
402
|
+
const provenance = mapping(holdout.provenance);
|
|
403
|
+
if (provenance?.record_class !== "synthetic" ||
|
|
404
|
+
provenance?.consent !== "not_required_synthetic" ||
|
|
405
|
+
provenance?.contains_personal_history !== false ||
|
|
406
|
+
!nonEmptyString(provenance?.source) ||
|
|
407
|
+
!nonEmptyString(provenance?.source_ref) ||
|
|
408
|
+
provenance?.owner !== "evaluation_authority") {
|
|
409
|
+
errors.push("holdout provenance must identify a synthetic, privacy-safe authority source");
|
|
410
|
+
}
|
|
411
|
+
const labelFreeze = mapping(holdout.label_freeze);
|
|
412
|
+
if (labelFreeze?.expected_labels_immutable !== true ||
|
|
413
|
+
labelFreeze?.relabeling !== "forbidden_after_freeze" ||
|
|
414
|
+
labelFreeze?.failure_recording !== "append_only" ||
|
|
415
|
+
labelFreeze?.changed_fixture !== "fail_before_metrics") {
|
|
416
|
+
errors.push("holdout labels must be frozen, append-only, and non-relabelable");
|
|
417
|
+
}
|
|
418
|
+
if (fixtureBytes !== undefined) {
|
|
419
|
+
const expected = mapping(authority.holdout)?.fixture_sha256;
|
|
420
|
+
if (!isLowerSha256(expected) || sha256(fixtureBytes) !== expected) {
|
|
421
|
+
errors.push("holdout fixture digest does not match evaluation authority");
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const seenIds = new Set();
|
|
425
|
+
const seenSourceIds = new Set();
|
|
426
|
+
const scopeLabels = strings(metricMapping(authority, "scope_accuracy")?.labels);
|
|
427
|
+
validateCaseList(holdout, "discovery", ["id", "source_id", "expected_discoverable", "provenance"], (record, source, caseErrors) => {
|
|
428
|
+
if (typeof record.expected_discoverable !== "boolean") {
|
|
429
|
+
caseErrors.push(`${source}.expected_discoverable must be boolean`);
|
|
430
|
+
}
|
|
431
|
+
}, seenIds, seenSourceIds, errors);
|
|
432
|
+
validateCaseList(holdout, "scope", ["id", "source_id", "expected_scope", "provenance"], (record, source, caseErrors) => {
|
|
433
|
+
if (!nonEmptyString(record.expected_scope) || !scopeLabels.includes(record.expected_scope)) {
|
|
434
|
+
caseErrors.push(`${source}.scope labels are not in the authority label set`);
|
|
435
|
+
}
|
|
436
|
+
}, seenIds, seenSourceIds, errors);
|
|
437
|
+
validateCaseList(holdout, "inferred_review", ["id", "source_id", "expected_reviewable", "provenance"], (record, source, caseErrors) => {
|
|
438
|
+
if (typeof record.expected_reviewable !== "boolean") {
|
|
439
|
+
caseErrors.push(`${source}.expected_reviewable must be boolean`);
|
|
440
|
+
}
|
|
441
|
+
}, seenIds, seenSourceIds, errors);
|
|
442
|
+
validateCaseList(holdout, "explicit_admission", ["id", "source_id", "expected_admissible", "provenance"], (record, source, caseErrors) => {
|
|
443
|
+
if (typeof record.expected_admissible !== "boolean") {
|
|
444
|
+
caseErrors.push(`${source}.expected_admissible must be boolean`);
|
|
445
|
+
}
|
|
446
|
+
}, seenIds, seenSourceIds, errors);
|
|
447
|
+
return errors;
|
|
448
|
+
}
|
|
449
|
+
function behaviorIds(value) {
|
|
450
|
+
return new Set(mappings(value).flatMap((record) => (nonEmptyString(record.id) ? [record.id] : [])));
|
|
451
|
+
}
|
|
452
|
+
function sameIdSet(left, right) {
|
|
453
|
+
return left.size === right.size && [...left].every((value) => right.has(value));
|
|
454
|
+
}
|
|
455
|
+
function validateBehaviorTextCase(record, source, errors, allowActor) {
|
|
456
|
+
const allowed = new Set(["id", "text", "provenance", ...(allowActor ? ["actor"] : [])]);
|
|
457
|
+
for (const field of Object.keys(record)) {
|
|
458
|
+
if (!allowed.has(field))
|
|
459
|
+
errors.push(`${source}.${field} is not an allowed field`);
|
|
460
|
+
}
|
|
461
|
+
for (const field of ["id", "text", "provenance"]) {
|
|
462
|
+
if (!(field in record))
|
|
463
|
+
errors.push(`${source}.${field} is required`);
|
|
464
|
+
}
|
|
465
|
+
if (!nonEmptyString(record.id))
|
|
466
|
+
errors.push(`${source}.id must be a non-empty string`);
|
|
467
|
+
if (!nonEmptyString(record.text) || Buffer.byteLength(record.text, "utf8") > 4096) {
|
|
468
|
+
errors.push(`${source}.text must be a bounded non-empty string`);
|
|
469
|
+
}
|
|
470
|
+
if (allowActor && record.actor !== undefined && record.actor !== "user" && record.actor !== "agent") {
|
|
471
|
+
errors.push(`${source}.actor must be user or agent when supplied`);
|
|
472
|
+
}
|
|
473
|
+
validateRecordProvenance(record, source, errors, "synthetic://personal-glossary-evaluation/", true);
|
|
474
|
+
}
|
|
475
|
+
function validateBehaviorInferredCase(record, source, errors) {
|
|
476
|
+
exactFields(record, ["id", "term", "sources", "provenance"], source, errors);
|
|
477
|
+
if (!nonEmptyString(record.id))
|
|
478
|
+
errors.push(`${source}.id must be a non-empty string`);
|
|
479
|
+
if (!nonEmptyString(record.term) || Buffer.byteLength(record.term, "utf8") > 256) {
|
|
480
|
+
errors.push(`${source}.term must be a bounded non-empty string`);
|
|
481
|
+
}
|
|
482
|
+
if (!Array.isArray(record.sources) || record.sources.length === 0) {
|
|
483
|
+
errors.push(`${source}.sources must be a non-empty list`);
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
for (const [index, item] of record.sources.entries()) {
|
|
487
|
+
const sourceRecord = mapping(item);
|
|
488
|
+
const itemSource = `${source}.sources[${index}]`;
|
|
489
|
+
if (!sourceRecord) {
|
|
490
|
+
errors.push(`${itemSource} must be a mapping`);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
exactFields(sourceRecord, ["source_kind", "text"], itemSource, errors);
|
|
494
|
+
if (![
|
|
495
|
+
"instruction_document",
|
|
496
|
+
"project_config_signal",
|
|
497
|
+
"conversation_turn",
|
|
498
|
+
].includes(String(sourceRecord.source_kind))) {
|
|
499
|
+
errors.push(`${itemSource}.source_kind is not supported`);
|
|
500
|
+
}
|
|
501
|
+
if (!nonEmptyString(sourceRecord.text) || Buffer.byteLength(sourceRecord.text, "utf8") > 4096) {
|
|
502
|
+
errors.push(`${itemSource}.text must be a bounded non-empty string`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
validateRecordProvenance(record, source, errors, "synthetic://personal-glossary-evaluation/", true);
|
|
507
|
+
}
|
|
508
|
+
function validateBehaviorLabelsAbsent(value, source, errors) {
|
|
509
|
+
if (Array.isArray(value)) {
|
|
510
|
+
value.forEach((item, index) => validateBehaviorLabelsAbsent(item, `${source}[${index}]`, errors));
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const record = mapping(value);
|
|
514
|
+
if (!record)
|
|
515
|
+
return;
|
|
516
|
+
for (const [key, item] of Object.entries(record)) {
|
|
517
|
+
if (FORBIDDEN_LABEL_FIELDS.test(key)) {
|
|
518
|
+
errors.push(`${source}.${key} must not supply a label or observation`);
|
|
519
|
+
}
|
|
520
|
+
validateBehaviorLabelsAbsent(item, `${source}.${key}`, errors);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function validateBehaviorList(fixture, holdout, key, validate, errors) {
|
|
524
|
+
const records = fixture[key];
|
|
525
|
+
if (!Array.isArray(records)) {
|
|
526
|
+
errors.push(`behavior_fixture.${key} must be a list`);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const seen = new Set();
|
|
530
|
+
for (const [index, item] of records.entries()) {
|
|
531
|
+
const source = `behavior_fixture.${key}[${index}]`;
|
|
532
|
+
const record = mapping(item);
|
|
533
|
+
if (!record) {
|
|
534
|
+
errors.push(`${source} must be a mapping`);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
validate(record, source, errors);
|
|
538
|
+
if (nonEmptyString(record.id)) {
|
|
539
|
+
if (seen.has(record.id))
|
|
540
|
+
errors.push(`${source}.id is duplicated`);
|
|
541
|
+
seen.add(record.id);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const holdoutIds = behaviorIds(holdout[key]);
|
|
545
|
+
if (!sameIdSet(seen, holdoutIds)) {
|
|
546
|
+
errors.push(`behavior_fixture.${key} must exactly cover frozen holdout ids`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
export function validateFrozenGlossaryBehaviorFixture(authority, holdout, fixture, fixtureBytes) {
|
|
550
|
+
const errors = [];
|
|
551
|
+
if (fixture.schema_version !== GLOSSARY_BEHAVIOR_FIXTURE_SCHEMA_VERSION) {
|
|
552
|
+
errors.push("behavior fixture schema_version is invalid");
|
|
553
|
+
}
|
|
554
|
+
if (fixture.fixture_id !== "personal-glossary-synthetic-behavior-v1" || fixture.status !== "frozen") {
|
|
555
|
+
errors.push("behavior fixture must have the frozen synthetic identity");
|
|
556
|
+
}
|
|
557
|
+
exactFields(fixture, [
|
|
558
|
+
"schema_version",
|
|
559
|
+
"fixture_id",
|
|
560
|
+
"status",
|
|
561
|
+
"provenance",
|
|
562
|
+
"synthetic_record_provenance",
|
|
563
|
+
"discovery",
|
|
564
|
+
"scope",
|
|
565
|
+
"inferred_review",
|
|
566
|
+
"explicit_admission",
|
|
567
|
+
], "behavior_fixture", errors);
|
|
568
|
+
const provenance = mapping(fixture.provenance);
|
|
569
|
+
if (provenance?.record_class !== "synthetic" ||
|
|
570
|
+
provenance?.consent !== "not_required_synthetic" ||
|
|
571
|
+
provenance?.contains_personal_history !== false ||
|
|
572
|
+
!nonEmptyString(provenance?.source) ||
|
|
573
|
+
!nonEmptyString(provenance?.source_ref) ||
|
|
574
|
+
provenance?.owner !== "evaluation_authority" ||
|
|
575
|
+
provenance?.privacy_rule !== "no_raw_personal_history_or_transcript_content") {
|
|
576
|
+
errors.push("behavior fixture provenance must identify synthetic privacy-safe product inputs");
|
|
577
|
+
}
|
|
578
|
+
if (fixtureBytes !== undefined) {
|
|
579
|
+
const expected = mapping(mapping(authority.observations)?.behavior_fixture)?.fixture_sha256;
|
|
580
|
+
if (!isLowerSha256(expected) || sha256(fixtureBytes) !== expected) {
|
|
581
|
+
errors.push("behavior fixture digest does not match evaluation authority");
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
validateBehaviorLabelsAbsent(fixture, "behavior_fixture", errors);
|
|
585
|
+
validateBehaviorList(fixture, holdout, "discovery", (record, source, caseErrors) => {
|
|
586
|
+
validateBehaviorTextCase(record, source, caseErrors, false);
|
|
587
|
+
}, errors);
|
|
588
|
+
validateBehaviorList(fixture, holdout, "scope", (record, source, caseErrors) => {
|
|
589
|
+
validateBehaviorTextCase(record, source, caseErrors, true);
|
|
590
|
+
}, errors);
|
|
591
|
+
validateBehaviorList(fixture, holdout, "inferred_review", validateBehaviorInferredCase, errors);
|
|
592
|
+
validateBehaviorList(fixture, holdout, "explicit_admission", (record, source, caseErrors) => {
|
|
593
|
+
validateBehaviorTextCase(record, source, caseErrors, false);
|
|
594
|
+
}, errors);
|
|
595
|
+
return errors;
|
|
596
|
+
}
|
|
597
|
+
const OBSERVATION_SPECS = [
|
|
598
|
+
{ key: "discovery", observedField: "observed_discovered", kind: "boolean" },
|
|
599
|
+
{ key: "scope", observedField: "observed_scope", kind: "scope" },
|
|
600
|
+
{ key: "inferred_review", observedField: "observed_reviewed", kind: "boolean" },
|
|
601
|
+
{ key: "explicit_admission", observedField: "observed_admitted", kind: "boolean" },
|
|
602
|
+
];
|
|
603
|
+
/** Validate runtime observations after current product behavior has produced them. */
|
|
604
|
+
export function validateGlossaryObservations(authority, holdout, observations) {
|
|
605
|
+
const errors = [];
|
|
606
|
+
if (observations.schema_version !== GLOSSARY_OBSERVATIONS_SCHEMA_VERSION) {
|
|
607
|
+
errors.push("observations schema_version is invalid");
|
|
608
|
+
}
|
|
609
|
+
if (observations.holdout_id !== holdout.holdout_id) {
|
|
610
|
+
errors.push("observations holdout_id does not match the frozen holdout");
|
|
611
|
+
}
|
|
612
|
+
const expectedHoldoutDigest = mapping(authority.holdout)?.fixture_sha256;
|
|
613
|
+
if (!isLowerSha256(observations.holdout_fixture_sha256) ||
|
|
614
|
+
observations.holdout_fixture_sha256 !== expectedHoldoutDigest) {
|
|
615
|
+
errors.push("observations holdout_fixture_sha256 does not match the frozen holdout digest");
|
|
616
|
+
}
|
|
617
|
+
const expectedBehaviorDigest = mapping(mapping(authority.observations)?.behavior_fixture)?.fixture_sha256;
|
|
618
|
+
if (!isLowerSha256(observations.behavior_fixture_sha256) ||
|
|
619
|
+
observations.behavior_fixture_sha256 !== expectedBehaviorDigest) {
|
|
620
|
+
errors.push("observations behavior_fixture_sha256 does not match the frozen behavior fixture digest");
|
|
621
|
+
}
|
|
622
|
+
const allowedTopLevel = new Set([
|
|
623
|
+
"schema_version",
|
|
624
|
+
"holdout_id",
|
|
625
|
+
"holdout_fixture_sha256",
|
|
626
|
+
"behavior_fixture_sha256",
|
|
627
|
+
...OBSERVATION_SPECS.map(({ key }) => key),
|
|
628
|
+
]);
|
|
629
|
+
for (const field of Object.keys(observations)) {
|
|
630
|
+
if (!allowedTopLevel.has(field))
|
|
631
|
+
errors.push(`observations.${field} is not an allowed field`);
|
|
632
|
+
}
|
|
633
|
+
const scopeLabels = strings(metricMapping(authority, "scope_accuracy")?.labels);
|
|
634
|
+
for (const { key, observedField, kind } of OBSERVATION_SPECS) {
|
|
635
|
+
const expectedIds = behaviorIds(holdout[key]);
|
|
636
|
+
const records = observations[key];
|
|
637
|
+
if (!Array.isArray(records)) {
|
|
638
|
+
errors.push(`observations.${key} must be a list`);
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
const seen = new Set();
|
|
642
|
+
for (const [index, value] of records.entries()) {
|
|
643
|
+
const source = `observations.${key}[${index}]`;
|
|
644
|
+
const record = mapping(value);
|
|
645
|
+
if (!record) {
|
|
646
|
+
errors.push(`${source} must be a mapping`);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
exactFields(record, ["id", observedField], source, errors);
|
|
650
|
+
if (!nonEmptyString(record.id) || !expectedIds.has(record.id)) {
|
|
651
|
+
errors.push(`${source}.id is unknown to the frozen holdout`);
|
|
652
|
+
}
|
|
653
|
+
else if (seen.has(record.id)) {
|
|
654
|
+
errors.push(`${source}.id is duplicated`);
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
seen.add(record.id);
|
|
658
|
+
}
|
|
659
|
+
if (kind === "boolean" && typeof record[observedField] !== "boolean") {
|
|
660
|
+
errors.push(`${source}.${observedField} must be boolean`);
|
|
661
|
+
}
|
|
662
|
+
if (kind === "scope" && !scopeLabels.includes(String(record[observedField] ?? ""))) {
|
|
663
|
+
errors.push(`${source}.${observedField} is not an allowed scope label`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
for (const expectedId of expectedIds) {
|
|
667
|
+
if (!seen.has(expectedId))
|
|
668
|
+
errors.push(`observations.${key} is missing frozen id ${expectedId}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return errors;
|
|
672
|
+
}
|
|
673
|
+
/** One-sided 95% Wilson lower bound. The authority fixes confidence at 0.95. */
|
|
674
|
+
export function wilsonLowerBound(successes, denominator, confidence = 0.95) {
|
|
675
|
+
if (!Number.isInteger(successes) ||
|
|
676
|
+
!Number.isInteger(denominator) ||
|
|
677
|
+
successes < 0 ||
|
|
678
|
+
denominator <= 0 ||
|
|
679
|
+
successes > denominator ||
|
|
680
|
+
confidence !== 0.95) {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
const z = ONE_SIDED_95_WILSON_Z;
|
|
684
|
+
const zSquared = z * z;
|
|
685
|
+
const estimate = successes / denominator;
|
|
686
|
+
const center = estimate + zSquared / (2 * denominator);
|
|
687
|
+
const margin = z * Math.sqrt(estimate * (1 - estimate) / denominator + zSquared / (4 * denominator * denominator));
|
|
688
|
+
return (center - margin) / (1 + zSquared / denominator);
|
|
689
|
+
}
|
|
690
|
+
function calculateMetric(metric, numerator, denominator, rule) {
|
|
691
|
+
const denominatorNonzero = Number.isInteger(denominator) && denominator > 0;
|
|
692
|
+
const validCounts = denominatorNonzero && Number.isInteger(numerator) && numerator >= 0 && numerator <= denominator;
|
|
693
|
+
const pointEstimate = validCounts ? numerator / denominator : null;
|
|
694
|
+
const lowerBound = validCounts ? wilsonLowerBound(numerator, denominator, rule.confidence) : null;
|
|
695
|
+
const sampleSufficient = validCounts && denominator >= rule.minimumSampleSize;
|
|
696
|
+
const pointThreshold = pointEstimate !== null && pointEstimate >= rule.pointEstimateMinimum;
|
|
697
|
+
const uncertaintyThreshold = lowerBound !== null && lowerBound >= rule.lowerBoundMinimum;
|
|
698
|
+
const failureReasons = [];
|
|
699
|
+
if (!denominatorNonzero)
|
|
700
|
+
failureReasons.push("denominator_zero");
|
|
701
|
+
if (denominatorNonzero && !validCounts)
|
|
702
|
+
failureReasons.push("invalid_counts");
|
|
703
|
+
if (validCounts && !sampleSufficient)
|
|
704
|
+
failureReasons.push("insufficient_sample");
|
|
705
|
+
if (validCounts && !pointThreshold)
|
|
706
|
+
failureReasons.push("point_threshold");
|
|
707
|
+
if (validCounts && !uncertaintyThreshold)
|
|
708
|
+
failureReasons.push("uncertainty_threshold");
|
|
709
|
+
return {
|
|
710
|
+
metric,
|
|
711
|
+
numerator: validCounts ? numerator : 0,
|
|
712
|
+
denominator: denominatorNonzero ? denominator : 0,
|
|
713
|
+
point_estimate: pointEstimate,
|
|
714
|
+
uncertainty: {
|
|
715
|
+
method: rule.uncertaintyMethod,
|
|
716
|
+
confidence: rule.confidence,
|
|
717
|
+
one_sided: true,
|
|
718
|
+
lower_bound: lowerBound,
|
|
719
|
+
},
|
|
720
|
+
thresholds: {
|
|
721
|
+
minimum_sample_size: rule.minimumSampleSize,
|
|
722
|
+
point_estimate_minimum: rule.pointEstimateMinimum,
|
|
723
|
+
lower_bound_minimum: rule.lowerBoundMinimum,
|
|
724
|
+
},
|
|
725
|
+
gates: {
|
|
726
|
+
denominator_nonzero: denominatorNonzero,
|
|
727
|
+
sample_sufficient: sampleSufficient,
|
|
728
|
+
point_threshold: pointThreshold,
|
|
729
|
+
uncertainty_threshold: uncertaintyThreshold,
|
|
730
|
+
},
|
|
731
|
+
status: denominatorNonzero && validCounts && sampleSufficient && pointThreshold && uncertaintyThreshold
|
|
732
|
+
? "pass"
|
|
733
|
+
: "fail",
|
|
734
|
+
failure_reasons: failureReasons,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
export function calculateDiscoveryRecall(examples, rule) {
|
|
738
|
+
const eligible = examples.filter((example) => example.expected);
|
|
739
|
+
return calculateMetric("discovery_recall", eligible.filter((example) => example.observed).length, eligible.length, rule);
|
|
740
|
+
}
|
|
741
|
+
export function calculateScopeAccuracy(examples, rule) {
|
|
742
|
+
return calculateMetric("scope_accuracy", examples.filter((example) => example.expected === example.observed).length, examples.length, rule);
|
|
743
|
+
}
|
|
744
|
+
export function calculateInferredReviewPrecision(examples, rule) {
|
|
745
|
+
const suggestions = examples.filter((example) => example.observed);
|
|
746
|
+
return calculateMetric("inferred_review_precision", suggestions.filter((example) => example.expected).length, suggestions.length, rule);
|
|
747
|
+
}
|
|
748
|
+
export function calculateExplicitAdmissionPrecision(examples, rule) {
|
|
749
|
+
const admissions = examples.filter((example) => example.observed);
|
|
750
|
+
return calculateMetric("explicit_admission_precision", admissions.filter((example) => example.expected).length, admissions.length, rule);
|
|
751
|
+
}
|