|
166 | 166 | "resultHash", |
167 | 167 | }) |
168 | 168 | _V5_TRANSFER_CHECK_KEYS = frozenset({"invariant", "passed", "evidence"}) |
| 169 | +_V5_TRANSFER_POLICIES: dict[str, dict[str, Any]] = { |
| 170 | + "entity_leakage": { |
| 171 | + "taskId": "forecasting-future-leakage-01", |
| 172 | + "evaluatorVersion": "counterlab-transfer-v1", |
| 173 | + "selectedStrategy": "time_ordered_holdout", |
| 174 | + "identifiedRisks": ["centered_window_reads_future"], |
| 175 | + "requiredEvidence": frozenset( |
| 176 | + {"center_true_uses_later_targets", "random_split_mixes_dates"} |
| 177 | + ), |
| 178 | + "allowedEvidence": frozenset( |
| 179 | + { |
| 180 | + "center_true_uses_later_targets", |
| 181 | + "random_split_mixes_dates", |
| 182 | + "metric_is_mae", |
| 183 | + } |
| 184 | + ), |
| 185 | + "checks": [ |
| 186 | + { |
| 187 | + "invariant": "TIME_AWARE_EVALUATION", |
| 188 | + "passed": True, |
| 189 | + "evidence": ( |
| 190 | + "A deployment-aligned forecast trains on earlier dates and " |
| 191 | + "evaluates later dates." |
| 192 | + ), |
| 193 | + }, |
| 194 | + { |
| 195 | + "invariant": "FUTURE_INFORMATION_RISK", |
| 196 | + "passed": True, |
| 197 | + "evidence": ( |
| 198 | + "A centered target window reads observations that do not " |
| 199 | + "exist at prediction time." |
| 200 | + ), |
| 201 | + }, |
| 202 | + { |
| 203 | + "invariant": "EVIDENCE_GROUNDED", |
| 204 | + "passed": True, |
| 205 | + "evidence": ( |
| 206 | + "Both center=True and the shuffled date split must be " |
| 207 | + "selected as code evidence." |
| 208 | + ), |
| 209 | + }, |
| 210 | + ], |
| 211 | + }, |
| 212 | + "class_imbalance": { |
| 213 | + "taskId": "manufacturing-defect-transfer-01", |
| 214 | + "evaluatorVersion": "counterlab-imbalance-transfer-v1", |
| 215 | + "selectedStrategy": "reject_accuracy_only", |
| 216 | + "identifiedRisks": ["recall_and_pr_auc"], |
| 217 | + "requiredEvidence": frozenset( |
| 218 | + {"zero_true_positives", "rare_base_rate"} |
| 219 | + ), |
| 220 | + "allowedEvidence": frozenset( |
| 221 | + {"zero_true_positives", "rare_base_rate", "many_true_negatives"} |
| 222 | + ), |
| 223 | + "checks": [ |
| 224 | + { |
| 225 | + "invariant": "ACCURACY_CLAIM_REJECTED", |
| 226 | + "passed": True, |
| 227 | + "evidence": ( |
| 228 | + "Overall accuracy cannot support deployment when the " |
| 229 | + "classifier misses every rare defective part." |
| 230 | + ), |
| 231 | + }, |
| 232 | + { |
| 233 | + "invariant": "MINORITY_METRICS_SELECTED", |
| 234 | + "passed": True, |
| 235 | + "evidence": ( |
| 236 | + "Defect recall and PR-AUC expose performance on the rare " |
| 237 | + "class and relative to its base rate." |
| 238 | + ), |
| 239 | + }, |
| 240 | + { |
| 241 | + "invariant": "EVIDENCE_GROUNDED", |
| 242 | + "passed": True, |
| 243 | + "evidence": ( |
| 244 | + "Use both the zero true-positive count and the 1% base rate " |
| 245 | + "as evidence." |
| 246 | + ), |
| 247 | + }, |
| 248 | + ], |
| 249 | + }, |
| 250 | +} |
169 | 251 |
|
170 | 252 |
|
171 | 253 | class HostedPatchError(ValueError): |
@@ -200,6 +282,56 @@ def _strict_fields( |
200 | 282 | ) |
201 | 283 |
|
202 | 284 |
|
| 285 | +def _validate_v5_transfer_semantics( |
| 286 | + concept: str, transfer: Mapping[str, Any] |
| 287 | +) -> None: |
| 288 | + policy = _V5_TRANSFER_POLICIES.get(concept) |
| 289 | + if policy is None: |
| 290 | + raise HostedPatchError("v5 patch transfer Subject Pack is not registered") |
| 291 | + evidence = _sequence( |
| 292 | + transfer.get("evidenceChoices"), "transferResult.evidenceChoices" |
| 293 | + ) |
| 294 | + evidence_set = set(evidence) |
| 295 | + required_evidence = policy["requiredEvidence"] |
| 296 | + allowed_evidence = policy["allowedEvidence"] |
| 297 | + checks = [ |
| 298 | + dict(_mapping(check, "transfer check")) |
| 299 | + for check in _sequence(transfer.get("checks"), "transferResult.checks") |
| 300 | + ] |
| 301 | + if ( |
| 302 | + transfer.get("taskId") != policy["taskId"] |
| 303 | + or transfer.get("evaluatorVersion") != policy["evaluatorVersion"] |
| 304 | + or transfer.get("selectedStrategy") != policy["selectedStrategy"] |
| 305 | + or transfer.get("identifiedRisks") != policy["identifiedRisks"] |
| 306 | + or len(evidence) != len(evidence_set) |
| 307 | + or evidence != sorted(evidence) |
| 308 | + or not required_evidence.issubset(evidence_set) |
| 309 | + or not evidence_set.issubset(allowed_evidence) |
| 310 | + or any( |
| 311 | + not isinstance(check.get("evidence"), str) |
| 312 | + or not check["evidence"].strip() |
| 313 | + for check in checks |
| 314 | + ) |
| 315 | + or [ |
| 316 | + { |
| 317 | + "invariant": check.get("invariant"), |
| 318 | + "passed": check.get("passed"), |
| 319 | + } |
| 320 | + for check in checks |
| 321 | + ] |
| 322 | + != [ |
| 323 | + { |
| 324 | + "invariant": check["invariant"], |
| 325 | + "passed": check["passed"], |
| 326 | + } |
| 327 | + for check in policy["checks"] |
| 328 | + ] |
| 329 | + ): |
| 330 | + raise HostedPatchError( |
| 331 | + "v5 patch transfer does not match the fixed evaluator semantics" |
| 332 | + ) |
| 333 | + |
| 334 | + |
203 | 335 | def _validate_v5_patch_bundle(bundle: Mapping[str, Any]) -> None: |
204 | 336 | _strict_fields(bundle, _V5_PATCH_BUNDLE_KEYS, "v5 patch bundle") |
205 | 337 | manifest = _mapping(bundle.get("artifactManifest"), "artifactManifest") |
@@ -267,6 +399,7 @@ def _validate_v5_patch_bundle(bundle: Mapping[str, Any]) -> None: |
267 | 399 | concept = belief.get("concept") |
268 | 400 | if concept not in _PATCH_OPERATIONS: |
269 | 401 | raise HostedPatchError("v5 patch Subject Pack is not registered") |
| 402 | + _validate_v5_transfer_semantics(str(concept), transfer) |
270 | 403 | if ( |
271 | 404 | manifest.get("support", {}).get("status") != "SUPPORTED" |
272 | 405 | or belief.get("supportState") != "SUPPORTED" |
|
0 commit comments