typevet 0.1.0.dev1__py3-none-any.whl
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.
- typevet/__init__.py +67 -0
- typevet/_version.py +43 -0
- typevet/adapters/__init__.py +16 -0
- typevet/adapters/diagnostics/__init__.py +59 -0
- typevet/adapters/diagnostics/fields.py +108 -0
- typevet/adapters/diagnostics/generation_events.py +75 -0
- typevet/adapters/diagnostics/http_events.py +91 -0
- typevet/adapters/diagnostics/logs.py +116 -0
- typevet/adapters/diagnostics/redaction.py +94 -0
- typevet/adapters/diagnostics/settings.py +77 -0
- typevet/adapters/inbound/__init__.py +65 -0
- typevet/adapters/inbound/api.py +61 -0
- typevet/adapters/inbound/backend_settings.py +608 -0
- typevet/adapters/inbound/cord_semantic_acceptance_cli.py +135 -0
- typevet/adapters/inbound/helpers.py +67 -0
- typevet/adapters/inbound/settings.py +154 -0
- typevet/adapters/outbound/__init__.py +62 -0
- typevet/adapters/outbound/async_fake.py +123 -0
- typevet/adapters/outbound/async_llama_cpp.py +168 -0
- typevet/adapters/outbound/chat_completion.py +148 -0
- typevet/adapters/outbound/fake.py +119 -0
- typevet/adapters/outbound/gemma/__init__.py +84 -0
- typevet/adapters/outbound/gemma/answer_binding.py +290 -0
- typevet/adapters/outbound/gemma/scoring_prefix.py +102 -0
- typevet/adapters/outbound/gemma/served_template.py +131 -0
- typevet/adapters/outbound/gemma_native_vision_factory.py +286 -0
- typevet/adapters/outbound/generation_finite.py +50 -0
- typevet/adapters/outbound/http_errors.py +38 -0
- typevet/adapters/outbound/judgment_scoring.py +471 -0
- typevet/adapters/outbound/llama_cpp.py +187 -0
- typevet/adapters/outbound/llama_cpp_http.py +103 -0
- typevet/adapters/outbound/llama_cpp_multimodal.py +145 -0
- typevet/adapters/outbound/llama_cpp_scoring.py +333 -0
- typevet/adapters/outbound/vllm_content.py +63 -0
- typevet/adapters/outbound/vllm_generation.py +163 -0
- typevet/adapters/outbound/vllm_generation_async.py +257 -0
- typevet/adapters/outbound/vllm_http.py +121 -0
- typevet/adapters/outbound/vllm_judgment_factory.py +210 -0
- typevet/adapters/outbound/vllm_scoring.py +372 -0
- typevet/domain/__init__.py +193 -0
- typevet/domain/candidate_scoring_request.py +134 -0
- typevet/domain/candidate_scoring_response.py +96 -0
- typevet/domain/candidate_scoring_validate.py +197 -0
- typevet/domain/decision_compile.py +368 -0
- typevet/domain/decision_execute.py +229 -0
- typevet/domain/decisions.py +129 -0
- typevet/domain/errors.py +235 -0
- typevet/domain/field_instructions.py +134 -0
- typevet/domain/judgment_answers.py +198 -0
- typevet/domain/judgment_normalize.py +243 -0
- typevet/domain/judgment_questions.py +187 -0
- typevet/domain/judgment_response.py +102 -0
- typevet/domain/media.py +101 -0
- typevet/domain/models.py +115 -0
- typevet/domain/question_schema.py +254 -0
- typevet/domain/scoring_stage.py +34 -0
- typevet/evaluation/__init__.py +19 -0
- typevet/evaluation/consumer_http_accounting.py +135 -0
- typevet/evaluation/cord_expense_call_accounting.py +108 -0
- typevet/evaluation/cord_expense_live_harness.py +75 -0
- typevet/evaluation/cord_expense_receipt_requirement.py +212 -0
- typevet/evaluation/cord_expense_smoke.py +328 -0
- typevet/evaluation/cord_semantic_acceptance.py +463 -0
- typevet/evaluation/cord_semantic_acceptance_report.py +90 -0
- typevet/evaluation/cord_semantic_metrics.py +57 -0
- typevet/evaluation/datasets/__init__.py +30 -0
- typevet/evaluation/datasets/banking77.py +270 -0
- typevet/evaluation/datasets/boolq.py +550 -0
- typevet/evaluation/datasets/boolq_download.py +132 -0
- typevet/evaluation/datasets/civil_comments.py +450 -0
- typevet/evaluation/datasets/clinc.py +125 -0
- typevet/evaluation/datasets/clinc_domains.json +172 -0
- typevet/evaluation/datasets/clinc_download.py +103 -0
- typevet/evaluation/datasets/clinc_plus_intent_names.json +153 -0
- typevet/evaluation/datasets/clinc_rows.py +306 -0
- typevet/evaluation/datasets/clinc_shard.py +156 -0
- typevet/evaluation/datasets/cord_expense.py +381 -0
- typevet/evaluation/datasets/difraud.py +256 -0
- typevet/evaluation/datasets/go_emotions.py +368 -0
- typevet/evaluation/datasets/go_emotions_download.py +154 -0
- typevet/evaluation/datasets/hyperpartisan.py +562 -0
- typevet/evaluation/datasets/partner_guard.py +170 -0
- typevet/evaluation/datasets/psai.py +415 -0
- typevet/evaluation/datasets/psai_download.py +167 -0
- typevet/evaluation/datasets/psai_evidence_pilot.py +570 -0
- typevet/evaluation/datasets/psai_schema.py +157 -0
- typevet/evaluation/datasets/psai_stream.py +166 -0
- typevet/evaluation/datasets/psai_vision.py +534 -0
- typevet/evaluation/datasets/psai_vision_controls.py +502 -0
- typevet/evaluation/datasets/pubmedqa.py +420 -0
- typevet/evaluation/experiment_identity.py +677 -0
- typevet/evaluation/psai_vision_consumer_accounting.py +190 -0
- typevet/evaluation/psai_vision_consumer_dispatch.py +216 -0
- typevet/evaluation/psai_vision_consumer_harness.py +342 -0
- typevet/evaluation/psai_vision_consumer_live.py +332 -0
- typevet/evaluation/psai_vision_consumer_live_identity.py +156 -0
- typevet/evaluation/psai_vision_consumer_live_receipt.py +132 -0
- typevet/evaluation/psai_vision_consumer_live_router.py +162 -0
- typevet/evaluation/psai_vision_consumer_offline.py +418 -0
- typevet/evaluation/psai_vision_consumer_outcomes.py +224 -0
- typevet/evaluation/psai_vision_consumer_protocol.py +69 -0
- typevet/evaluation/psai_vision_consumer_receipt.py +161 -0
- typevet/evaluation/psai_vision_consumer_receipt_structure.py +263 -0
- typevet/evaluation/psai_vision_probability_evidence.py +336 -0
- typevet/evaluation/runner/__init__.py +56 -0
- typevet/evaluation/runner/core.py +104 -0
- typevet/evaluation/runner/datasets.py +186 -0
- typevet/evaluation/runner/live_gate.py +149 -0
- typevet/evaluation/runner/report.py +116 -0
- typevet/evaluation/tpjep/__init__.py +81 -0
- typevet/evaluation/tpjep/loader.py +237 -0
- typevet/evaluation/tpjep/outcome.py +101 -0
- typevet/evaluation/tpjep/records.py +428 -0
- typevet/evaluation/tpjep/runner.py +325 -0
- typevet/ports/__init__.py +39 -0
- typevet/ports/async_generation.py +57 -0
- typevet/ports/framing.py +59 -0
- typevet/ports/generation.py +58 -0
- typevet/ports/judgment.py +71 -0
- typevet/ports/scoring.py +64 -0
- typevet/py.typed +0 -0
- typevet/runtime/__init__.py +50 -0
- typevet/runtime/categorical.py +196 -0
- typevet/runtime/judgment.py +17 -0
- typevet/runtime/llama_cpp_gemma_vision.py +28 -0
- typevet/runtime/scoring_prefix.py +14 -0
- typevet/runtime/vllm_judgment.py +28 -0
- typevet/testing/__init__.py +22 -0
- typevet/testing/fakes.py +152 -0
- typevet-0.1.0.dev1.dist-info/METADATA +125 -0
- typevet-0.1.0.dev1.dist-info/RECORD +133 -0
- typevet-0.1.0.dev1.dist-info/WHEEL +4 -0
- typevet-0.1.0.dev1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Receipt serialization and acceptance for the PSAI consumer harness ([#177][i177]).
|
|
2
|
+
|
|
3
|
+
Examples:
|
|
4
|
+
```python
|
|
5
|
+
from typevet.evaluation.psai_vision_consumer_receipt import (
|
|
6
|
+
evaluate_consumer_receipt_acceptance,
|
|
7
|
+
serialize_answer,
|
|
8
|
+
)
|
|
9
|
+
from typevet.domain.judgment_answers import NoulAnswer
|
|
10
|
+
|
|
11
|
+
payload = serialize_answer(NoulAnswer(noul=0.5))
|
|
12
|
+
assert payload["kind"] == "Noul"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
See Also:
|
|
16
|
+
- [typevet.evaluation.psai_vision_consumer_harness][]: orchestration
|
|
17
|
+
- [typevet.evaluation.psai_vision_consumer_receipt_structure][]: structural checks
|
|
18
|
+
|
|
19
|
+
Acceptance runs structural fail-closed checks first, then semantic gold
|
|
20
|
+
replay when fixture identity pins on the receipt are valid.
|
|
21
|
+
|
|
22
|
+
[i177]: https://github.com/Alberto-Codes/typevet/issues/177
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import re
|
|
29
|
+
from collections.abc import Mapping
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
from typevet.domain.judgment_answers import (
|
|
34
|
+
Answer,
|
|
35
|
+
ChoiceAnswer,
|
|
36
|
+
NoulAnswer,
|
|
37
|
+
ScoreAnswer,
|
|
38
|
+
)
|
|
39
|
+
from typevet.evaluation.psai_vision_consumer_outcomes import expected_outcome_failures
|
|
40
|
+
from typevet.evaluation.psai_vision_consumer_receipt_structure import (
|
|
41
|
+
protocol_structural_failures,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def serialize_answer(answer: Answer) -> dict[str, Any]:
|
|
46
|
+
"""Map a public ``Answer`` to a JSON-serializable dict.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
answer: Typed judgment answer.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Dict with ``kind`` and type-specific fields.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
TypeError: When ``answer`` is not a known ``Answer`` subtype.
|
|
56
|
+
"""
|
|
57
|
+
if isinstance(answer, NoulAnswer):
|
|
58
|
+
return {"kind": "Noul", "noul": answer.noul}
|
|
59
|
+
if isinstance(answer, ChoiceAnswer):
|
|
60
|
+
return {
|
|
61
|
+
"kind": "Choice",
|
|
62
|
+
"choice": answer.choice,
|
|
63
|
+
"confidence": answer.confidence,
|
|
64
|
+
"probabilities": dict(answer.probabilities),
|
|
65
|
+
}
|
|
66
|
+
if isinstance(answer, ScoreAnswer):
|
|
67
|
+
return {
|
|
68
|
+
"kind": "Score",
|
|
69
|
+
"score": answer.score,
|
|
70
|
+
"confidence": answer.confidence,
|
|
71
|
+
"legend": {str(k): v for k, v in answer.legend.items()},
|
|
72
|
+
"probabilities": {str(k): v for k, v in answer.probabilities.items()},
|
|
73
|
+
}
|
|
74
|
+
msg = f"unsupported answer type: {type(answer)!r}"
|
|
75
|
+
raise TypeError(msg)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def consumer_receipt_basename(
|
|
79
|
+
*,
|
|
80
|
+
protocol_revision: int,
|
|
81
|
+
wheel_sha256: str | None,
|
|
82
|
+
model_id: str,
|
|
83
|
+
) -> str:
|
|
84
|
+
"""Return a stable receipt filename for one proof configuration.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
protocol_revision: Frozen protocol revision integer.
|
|
88
|
+
wheel_sha256: Optional wheel digest from the environment.
|
|
89
|
+
model_id: Model id under test.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Filename including ``.json`` suffix.
|
|
93
|
+
"""
|
|
94
|
+
wheel_part = (wheel_sha256 or "unknown-wheel")[:16]
|
|
95
|
+
model_slug = re.sub(r"[^a-zA-Z0-9._-]+", "_", model_id)[:48]
|
|
96
|
+
return f"consumer-receipt-p{protocol_revision}-{wheel_part}-{model_slug}.json"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def resolve_receipt_write_path(
|
|
100
|
+
out_dir: Path,
|
|
101
|
+
*,
|
|
102
|
+
protocol_revision: int,
|
|
103
|
+
wheel_sha256: str | None,
|
|
104
|
+
model_id: str,
|
|
105
|
+
) -> Path:
|
|
106
|
+
"""Pick a non-colliding receipt path under ``out_dir``.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
out_dir: Directory for receipts.
|
|
110
|
+
protocol_revision: Frozen protocol revision.
|
|
111
|
+
wheel_sha256: Optional wheel digest.
|
|
112
|
+
model_id: Model id under test.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Path that does not yet exist (appends ``-attempt-N`` when needed).
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
OSError: When no free attempt suffix is available.
|
|
119
|
+
"""
|
|
120
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
base = out_dir / consumer_receipt_basename(
|
|
122
|
+
protocol_revision=protocol_revision,
|
|
123
|
+
wheel_sha256=wheel_sha256,
|
|
124
|
+
model_id=model_id,
|
|
125
|
+
)
|
|
126
|
+
if not base.exists():
|
|
127
|
+
return base
|
|
128
|
+
for attempt in range(1, 1000):
|
|
129
|
+
candidate = base.with_name(base.stem + f"-attempt-{attempt}" + base.suffix)
|
|
130
|
+
if not candidate.exists():
|
|
131
|
+
return candidate
|
|
132
|
+
msg = f"could not allocate receipt path under {out_dir}"
|
|
133
|
+
raise OSError(msg)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def evaluate_consumer_receipt_acceptance(
|
|
137
|
+
receipt: Mapping[str, Any],
|
|
138
|
+
) -> tuple[bool, list[str]]:
|
|
139
|
+
"""Check frozen acceptance conditions on a saved receipt.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
receipt: Consumer proof receipt dict.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
``(accepted, failure_messages)``. Structural violations (empty body,
|
|
146
|
+
stripped matrix, bad counts) reject before gold outcome comparison.
|
|
147
|
+
|
|
148
|
+
"""
|
|
149
|
+
failures = protocol_structural_failures(receipt)
|
|
150
|
+
failures.extend(expected_outcome_failures(receipt))
|
|
151
|
+
return (not failures, failures)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def write_receipt_json(path: Path, receipt: Mapping[str, Any]) -> None:
|
|
155
|
+
"""Write ``receipt`` as indented JSON to ``path``.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
path: Destination file.
|
|
159
|
+
receipt: Serializable receipt mapping.
|
|
160
|
+
"""
|
|
161
|
+
path.write_text(json.dumps(receipt, indent=2), encoding="utf-8")
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Structural fail-closed checks for consumer receipts ([#177][i177]).
|
|
2
|
+
|
|
3
|
+
Examples:
|
|
4
|
+
```python
|
|
5
|
+
from typevet.evaluation.psai_vision_consumer_receipt_structure import (
|
|
6
|
+
protocol_structural_failures,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
assert protocol_structural_failures({}) == ["receipt is empty"]
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
See Also:
|
|
13
|
+
- [typevet.evaluation.psai_vision_consumer_protocol][]: frozen pins
|
|
14
|
+
|
|
15
|
+
[i177]: https://github.com/Alberto-Codes/typevet/issues/177
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from collections.abc import Mapping
|
|
21
|
+
from math import isfinite
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from typevet.evaluation.psai_vision_consumer_accounting import (
|
|
25
|
+
FROZEN_CONSUMER_CASE_UIDS,
|
|
26
|
+
TEXT_ANNOTATION_JUDGE_UIDS,
|
|
27
|
+
)
|
|
28
|
+
from typevet.evaluation.psai_vision_consumer_protocol import (
|
|
29
|
+
FROZEN_JUDGMENT_CALLS,
|
|
30
|
+
FROZEN_PROTOCOL_REVISION,
|
|
31
|
+
FROZEN_SCORING_REQUESTS,
|
|
32
|
+
expected_matrix_row_keys,
|
|
33
|
+
expected_paired_ordering_uids,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _finite_prob(value: object) -> bool:
|
|
38
|
+
return isinstance(value, (int, float)) and isfinite(float(value))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _noul_answer_failures(name: str, payload: Mapping[str, Any]) -> list[str]:
|
|
42
|
+
prob = payload.get("noul")
|
|
43
|
+
if not isinstance(prob, (int, float)) or not isfinite(float(prob)):
|
|
44
|
+
return [f"answer {name!r} has non-finite noul"]
|
|
45
|
+
prob_float = float(prob)
|
|
46
|
+
if not 0.0 <= prob_float <= 1.0:
|
|
47
|
+
return [f"answer {name!r} noul out of [0,1]"]
|
|
48
|
+
return []
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _mapping_prob_failures(
|
|
52
|
+
name: str,
|
|
53
|
+
probs: object,
|
|
54
|
+
*,
|
|
55
|
+
label: str,
|
|
56
|
+
) -> list[str]:
|
|
57
|
+
if not isinstance(probs, Mapping):
|
|
58
|
+
return []
|
|
59
|
+
failures: list[str] = []
|
|
60
|
+
for key, value in probs.items():
|
|
61
|
+
if not _finite_prob(value):
|
|
62
|
+
failures.append(f"answer {name!r} {label} prob {key!r} non-finite")
|
|
63
|
+
return failures
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _answer_probability_failures(answers: Mapping[str, Any]) -> list[str]:
|
|
67
|
+
failures: list[str] = []
|
|
68
|
+
for name, payload in answers.items():
|
|
69
|
+
if not isinstance(payload, Mapping):
|
|
70
|
+
failures.append(f"answer {name!r} is not an object")
|
|
71
|
+
continue
|
|
72
|
+
kind = payload.get("kind")
|
|
73
|
+
if kind == "Noul":
|
|
74
|
+
failures.extend(_noul_answer_failures(name, payload))
|
|
75
|
+
elif kind == "Choice":
|
|
76
|
+
failures.extend(
|
|
77
|
+
_mapping_prob_failures(
|
|
78
|
+
name, payload.get("probabilities"), label="choice"
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
elif kind == "Score":
|
|
82
|
+
failures.extend(
|
|
83
|
+
_mapping_prob_failures(
|
|
84
|
+
name, payload.get("probabilities"), label="score"
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
return failures
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _revision_and_count_failures(receipt: Mapping[str, Any]) -> list[str]:
|
|
91
|
+
failures: list[str] = []
|
|
92
|
+
revision = receipt.get("consumer_live_protocol_revision")
|
|
93
|
+
if revision != FROZEN_PROTOCOL_REVISION:
|
|
94
|
+
failures.append(
|
|
95
|
+
f"consumer_live_protocol_revision {revision!r} != {FROZEN_PROTOCOL_REVISION}"
|
|
96
|
+
)
|
|
97
|
+
judgment = receipt.get("judgment_call_count")
|
|
98
|
+
scoring = receipt.get("scoring_request_count")
|
|
99
|
+
if not isinstance(judgment, int) or judgment <= 0:
|
|
100
|
+
failures.append("judgment_call_count missing or zero")
|
|
101
|
+
if not isinstance(scoring, int) or scoring <= 0:
|
|
102
|
+
failures.append("scoring_request_count missing or zero")
|
|
103
|
+
if judgment != FROZEN_JUDGMENT_CALLS:
|
|
104
|
+
failures.append(
|
|
105
|
+
f"judgment_call_count {judgment!r} != frozen {FROZEN_JUDGMENT_CALLS}"
|
|
106
|
+
)
|
|
107
|
+
if scoring != FROZEN_SCORING_REQUESTS:
|
|
108
|
+
failures.append(
|
|
109
|
+
f"scoring_request_count {scoring!r} != frozen {FROZEN_SCORING_REQUESTS}"
|
|
110
|
+
)
|
|
111
|
+
require_live = receipt.get("require_live") is True
|
|
112
|
+
aux = receipt.get("auxiliary_http_count")
|
|
113
|
+
if require_live and (not isinstance(aux, int) or aux <= 0):
|
|
114
|
+
failures.append("auxiliary_http_count missing or zero for live receipt")
|
|
115
|
+
return failures
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _capability_failures(receipt: Mapping[str, Any]) -> list[str]:
|
|
119
|
+
failures: list[str] = []
|
|
120
|
+
require_live = receipt.get("require_live") is True
|
|
121
|
+
capability = receipt.get("capability")
|
|
122
|
+
if not isinstance(capability, Mapping):
|
|
123
|
+
return ["missing capability block"]
|
|
124
|
+
if require_live and capability.get("vision") is not True:
|
|
125
|
+
failures.append("capability.vision is not true for live receipt")
|
|
126
|
+
elif capability.get("vision") is False:
|
|
127
|
+
failures.append("capability.vision is false")
|
|
128
|
+
negative = receipt.get("unsupported_capability_negative")
|
|
129
|
+
if not isinstance(negative, Mapping):
|
|
130
|
+
failures.append("missing unsupported_capability_negative block")
|
|
131
|
+
elif negative.get("ok") is not True:
|
|
132
|
+
failures.append("unsupported_capability_negative did not pass")
|
|
133
|
+
return failures
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _matrix_row_failures(matrix: list[Any]) -> list[str]:
|
|
137
|
+
failures: list[str] = []
|
|
138
|
+
expected_keys = expected_matrix_row_keys()
|
|
139
|
+
seen_keys: set[tuple[str, str, str]] = set()
|
|
140
|
+
for row in matrix:
|
|
141
|
+
if not isinstance(row, Mapping):
|
|
142
|
+
return ["matrix row is not an object"]
|
|
143
|
+
uid = row.get("unique_data_id")
|
|
144
|
+
condition = row.get("condition")
|
|
145
|
+
leg = row.get("leg")
|
|
146
|
+
if not isinstance(uid, str) or not isinstance(condition, str):
|
|
147
|
+
return ["matrix row missing unique_data_id or condition"]
|
|
148
|
+
if not isinstance(leg, str):
|
|
149
|
+
return [f"matrix row {uid!r} missing leg"]
|
|
150
|
+
key = (uid, condition, leg)
|
|
151
|
+
if (
|
|
152
|
+
uid not in FROZEN_CONSUMER_CASE_UIDS
|
|
153
|
+
and uid not in TEXT_ANNOTATION_JUDGE_UIDS
|
|
154
|
+
):
|
|
155
|
+
failures.append(f"matrix row unknown unique_data_id {uid!r}")
|
|
156
|
+
if key not in expected_keys:
|
|
157
|
+
failures.append(f"matrix row unexpected key {key!r}")
|
|
158
|
+
if key in seen_keys:
|
|
159
|
+
failures.append(f"duplicate matrix row {key!r}")
|
|
160
|
+
seen_keys.add(key)
|
|
161
|
+
answers = row.get("answers")
|
|
162
|
+
if not isinstance(answers, Mapping) or not answers:
|
|
163
|
+
failures.append(f"matrix row {key!r} missing answers map")
|
|
164
|
+
else:
|
|
165
|
+
failures.extend(_answer_probability_failures(answers))
|
|
166
|
+
missing = expected_keys - seen_keys
|
|
167
|
+
if missing:
|
|
168
|
+
failures.append(f"missing matrix rows for keys {sorted(missing)!r}")
|
|
169
|
+
return failures
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _matrix_failures(receipt: Mapping[str, Any]) -> list[str]:
|
|
173
|
+
matrix = receipt.get("matrix_rows")
|
|
174
|
+
if not isinstance(matrix, list) or not matrix:
|
|
175
|
+
return ["matrix_rows missing or empty"]
|
|
176
|
+
failures = _matrix_row_failures(matrix)
|
|
177
|
+
judgment = receipt.get("judgment_call_count")
|
|
178
|
+
if isinstance(judgment, int) and len(matrix) != judgment:
|
|
179
|
+
failures.append(f"matrix_rows {len(matrix)} != judgment_call_count {judgment}")
|
|
180
|
+
return failures
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _paired_ordering_failures(receipt: Mapping[str, Any]) -> list[str]:
|
|
184
|
+
pairs = receipt.get("paired_ordering")
|
|
185
|
+
if not isinstance(pairs, list) or not pairs:
|
|
186
|
+
return ["paired_ordering missing or empty"]
|
|
187
|
+
failures: list[str] = []
|
|
188
|
+
pair_uids: set[str] = set()
|
|
189
|
+
for pair in pairs:
|
|
190
|
+
if not isinstance(pair, Mapping):
|
|
191
|
+
return ["paired_ordering entry is not an object"]
|
|
192
|
+
uid = pair.get("unique_data_id")
|
|
193
|
+
if not isinstance(uid, str):
|
|
194
|
+
return ["paired_ordering entry missing unique_data_id"]
|
|
195
|
+
if uid not in expected_paired_ordering_uids():
|
|
196
|
+
failures.append(f"paired_ordering unknown unique_data_id {uid!r}")
|
|
197
|
+
if uid in pair_uids:
|
|
198
|
+
failures.append(f"duplicate paired_ordering uid {uid!r}")
|
|
199
|
+
pair_uids.add(uid)
|
|
200
|
+
if not pair.get("ordered"):
|
|
201
|
+
failures.append(f"paired_ordering failed for row {uid!r}")
|
|
202
|
+
missing_pairs = expected_paired_ordering_uids() - pair_uids
|
|
203
|
+
if missing_pairs:
|
|
204
|
+
failures.append(f"missing paired_ordering for {sorted(missing_pairs)!r}")
|
|
205
|
+
return failures
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _observed_scoring_failures(receipt: Mapping[str, Any]) -> list[str]:
|
|
209
|
+
failures: list[str] = []
|
|
210
|
+
scoring = receipt.get("scoring_request_count")
|
|
211
|
+
observed = receipt.get("scoring_requests_observed")
|
|
212
|
+
if isinstance(scoring, int) and isinstance(observed, int) and observed != scoring:
|
|
213
|
+
failures.append(
|
|
214
|
+
f"scoring_requests_observed {observed} != scoring_request_count {scoring}"
|
|
215
|
+
)
|
|
216
|
+
matrix = receipt.get("matrix_rows")
|
|
217
|
+
if not isinstance(matrix, list) or not isinstance(observed, int):
|
|
218
|
+
return failures
|
|
219
|
+
answer_total = sum(
|
|
220
|
+
len(row["answers"])
|
|
221
|
+
for row in matrix
|
|
222
|
+
if isinstance(row, Mapping) and isinstance(row.get("answers"), Mapping)
|
|
223
|
+
)
|
|
224
|
+
if answer_total != observed:
|
|
225
|
+
failures.append(
|
|
226
|
+
f"scoring_requests_observed {observed} != "
|
|
227
|
+
f"serialized answer count {answer_total}"
|
|
228
|
+
)
|
|
229
|
+
return failures
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _pin_failures(receipt: Mapping[str, Any]) -> list[str]:
|
|
233
|
+
failures: list[str] = []
|
|
234
|
+
manifest_pin = receipt.get("manifest_sha256")
|
|
235
|
+
if not isinstance(manifest_pin, str) or not manifest_pin.strip():
|
|
236
|
+
failures.append("manifest_sha256 pin missing")
|
|
237
|
+
image_pins = receipt.get("frozen_case_image_digests")
|
|
238
|
+
if not isinstance(image_pins, list) or len(image_pins) != len(
|
|
239
|
+
FROZEN_CONSUMER_CASE_UIDS
|
|
240
|
+
):
|
|
241
|
+
failures.append("frozen_case_image_digests pin missing or incomplete")
|
|
242
|
+
return failures
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def protocol_structural_failures(receipt: Mapping[str, Any]) -> list[str]:
|
|
246
|
+
"""Fail-closed structural checks independent of machine-local paths.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
receipt: Consumer proof receipt dict.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Human-readable failure messages (empty when structure matches rev 2).
|
|
253
|
+
"""
|
|
254
|
+
if not receipt:
|
|
255
|
+
return ["receipt is empty"]
|
|
256
|
+
failures: list[str] = []
|
|
257
|
+
failures.extend(_revision_and_count_failures(receipt))
|
|
258
|
+
failures.extend(_capability_failures(receipt))
|
|
259
|
+
failures.extend(_matrix_failures(receipt))
|
|
260
|
+
failures.extend(_paired_ordering_failures(receipt))
|
|
261
|
+
failures.extend(_observed_scoring_failures(receipt))
|
|
262
|
+
failures.extend(_pin_failures(receipt))
|
|
263
|
+
return failures
|