Eval-claim specification

August 26, 2026 · View on GitHub

An eval receipt is a regular proofbundle/v0.1 bundle (see SPEC.md) whose payload is a canonical eval claim. This document specifies the claim.

1. What a receipt proves (and does not)

A receipt is tamper-evident signed evidence of exactly one thing: on suite S, metric comparator threshold held, passed=… — signed by a stated issuer and anchored in a tamper-evident Merkle tree. It carries only salted commitments to the model and dataset identifiers, never the weights, the raw data, or the plaintext names.

It does not prove that the evaluation was well designed, that the suite measures what it claims, or that the reported score is itself correct. Those are human judgements. What it removes is the need to simply trust the number.

1a. Score evidence classes

The exact score is used only to compute passed and is then discarded, so a receipt carries a threshold verdict, not an exact score. show-eval declares this explicitly, and the machine-readable classifier proofbundle.evalclaim.eval_evidence_class returns one of:

  • THRESHOLD_VERDICT_VERIFIED — the signed claim proves passed for the stated comparator/threshold. This is the ONLY class the frozen v0.1 schema produces.
  • EXACT_SCORE_VERIFIED — reachable only through the optional, additive exact-score profile (a signed decimal-string score whose recomputed passed agrees). Not part of the frozen 3.x core; EXPERIMENTAL.
  • SCORE_COMMITMENT_PRESENT — a signed score commitment is a binding, not a range proof: it does not prove the hidden score crossed the threshold.
  • SCORE_WITHHELD — the exact score is deliberately withheld; only the threshold verdict is signed.
  • METHODOLOGY_NOT_EVALUATED — always present: a receipt never judges whether the suite is well designed.

2. Data minimization

The payload contains only salted commitments, the threshold, and passed — never weights, raw data, or plaintext model/dataset names. Each identifier appears as a salted commitment sha256:hex(salt ‖ utf8(identifier)) with a ≥16-byte high-entropy salt that stays with the issuer and is never in the payload. Without the salt the identifier cannot be recovered from the commitment — not even via a rainbow table over known model names such as gpt-4o. The issuer may later disclose identifier + salt; that is the path for selective disclosure in v0.5.

3. Fields

fieldrequiredtypenotes
schemayesstringconst proofbundle/eval-claim/v0.1
suiteyesstringeval suite name
suite_versionyesstring
metricyesstringe.g. accuracy, refusal_rate
comparatoryesstringone of >= > <= <
thresholdyesstringa decimal string (e.g. "0.80"), never a JSON float
passedyesbooleancomputed by the emitter from comparator+threshold, not trusted from the caller
nyesintegersample size, 0 ≤ n ≤ $2^{53}$-1
model_id_commityesstringsha256:<hex> salted commitment to the model identifier
dataset_id_commityesstringsha256:<hex> salted commitment to the dataset identifier
commit_algyesstringconst sha256-salted-v1
issueryesstringed25519:<base64 of the 32-byte public key> — part of the SIGNED payload; binds the receipt to the issuer
timestampyesstringRFC 3339
assurance_levelyesenumself_attested (default) · third_party · reproduced · enclave_attested — how much a PASS is worth; SIGNED (issuer-declared), always shown by show-eval. See THREAT_MODEL.md. enclave_attested is a STRING claim by itself — proofbundle.evalclaim.enclave_assurance_proven(claim, bundle, eat_jws=…, verifier_pubkey=…) optionally corroborates it against a real TEE Attestation Result (show-eval --eat/--verifier-key; EXPERIMENTAL v2.0, docs/EXPERIMENTAL_ENCLAVE.md) — additive, never force-promotes the signed field itself
context_bindingnostringhash of an external context (e.g. a request id), against reuse in a foreign context
ci95noarrayexactly two decimal strings
multiple_testingnostringe.g. holm
prereg_sha256nostringsha256 (hex) over the RAW bytes of the eval protocol file, committed BEFORE the run (proofbundle prereg); a verifier re-hashes the disclosed protocol and checks it
evaluation_card_sha256nostringsha256 (hex) over the RAW bytes of an external, human-readable Eval Card document (Hugging Face EvalEval Coalition "Evaluation Cards", arXiv:2606.09809 — see src/proofbundle/evalcard.py); mechanically identical to prereg_sha256 (proofbundle evalcard / evalcard.verify_evaluation_card). Added in this revision: because the schema is additionalProperties: false, a receipt carrying this field is a one-way compatibility step — an older proofbundle build rejects it as an unknown field (mirrors anchors[], SPEC.md §7i) rather than silently ignoring it
provenancenoobjecttraceability metadata (not a security commitment): harness, git_hash, harness_version, run_id, run_timestamp (log-native), config_hash (<alg>:<hex> over canonical config JSON), plus adapter-specific keys (e.g. task_hash, stderr) and the additive benchmark-hacking VISIBILITY keys run_attempts/aborted_runs (non-negative integers) and methodology_sha256/benchjack_audit_report_sha256 (plain sha256 references; see THREAT_MODEL.md — visibility only, never a guarantee against a gamed benchmark, BenchJack arXiv:2605.12673)

Reported-version status (5.0.0, additive)

Every provenance field that carries a version as reported by the harnessharness_version, task_version, promptfoo_version — is accompanied by an explicit status:

keyvaluesmeaning
<field>_statusreported | not_reported | not_boundwhat the harness actually said
<field>_status_reasonfree textmandatory whenever the status is not reported

Why a status and not a boolean. A boolean has three states of its own — true, false and absent — and would push the ambiguity one level up instead of closing it.

What it fixes. Before 5.0.0 a version field was simply ABSENT when the harness reported none, so absence carried two different meanings at once: the harness ran and reported no version and no harness was bound at all. A verifier could not tell them apart. For a product whose whole claim is that a receipt says what it means, that is the failure class it exists against.

The version field itself is unchanged. When nothing was reported the version key stays absent — writing a value the harness never reported would put a number into evidence that nobody measured. The status makes a statement about the REPORTING, never about the version.

Never derived. The status is written only from what the harness returned. It is not inferred from neighbouring fields, and not_reported is never an all-clear: it does not fold to PASS and no gate reads it as one.

A verifier rejects a provenance block whose status is outside the three literals, whose non-reported status carries no reason, or whose status and field contradict each other in either direction (reported without the field, or the field present while the status denies it). Conformance vectors: conformance/provenance/version-status-* — one per status value plus one per rejection class.

| samples | no | object | per-sample Merkle commitment {root_b64, n, leaf_alg} — SIGNED; samples.n MUST equal n; enables the forced-random-sample audit (SPEC §7g, proofbundle audit-challenge / verify-opening) |

Machine-readable: schemas/eval_claim_v0_1.schema.json.

4. Canonicalization (RFC 8785 JCS) — emit path only

The payload bytes are the claim canonicalized with RFC 8785 JCS. This profile, enforced on the emit path:

  • object keys sorted by UTF-16 code units (not Python code points — otherwise it diverges on emoji and characters beyond the BMP);
  • duplicate keys rejected when parsing claim JSON;
  • all string values NFC-normalized (non-NFC rejected);
  • Python floats rejected (numbers with fractional parts are decimal strings);
  • integers limited to the IEEE-754 safe range (2^53-1);
  • compact separators, UTF-8.

A real RFC 8785 library is used only on the emit path. The verify path never canonicalizesdecode_eval_claim checks the exact stored bytes that verify_bundle already authenticated — so the verifier stays dependency-free (cryptography + stdlib only).

5. Issuer binding

emit_eval_receipt sets issuer to the signer's fingerprint. decode_eval_claim verifies the bundle, then checks that the bundle's signing key equals the claim's issuer field; a mismatch fails decoding. A receipt is therefore bound to the key that signed it — you cannot lift a claim under a different signature.