8000 Core: validate a Run, refuse runs in an Eval body, check run_results, fingerprint a run, publish the run hash formulas by ynishi · Pull Request #37 · ynishi/evalhub · GitHub
[go: up one dir, main page]

Skip to content

Core: validate a Run, refuse runs in an Eval body, check run_results, fingerprint a run, publish the run hash formulas - #37

Merged
ynishi merged 1 commit into
mainfrom
feat/core-run
Sep 25, 2026
Merged

ynishi merged 1 commit into
mainfrom
feat/core-run

Conversation

@ynishi
@ynishi ynishi commented Sep 25, 2026
Copy link
Copy Markdown
Owner

Stage 2 of 9 of #24 (Runs as first-class rows), on top of stage 1 (#27, PR #36). evalhub-core only: the rules and the formulas. Store and server behaviour is unchanged and their tests are untouched; the branch is mergeable on its own. The version bump is stage 9 (#35).

What changed (all under crates/evalhub-core/)

  • validate::run(run_id, &Value), a new entry point rather than a RecordKind variant: run.json, then run_id_invalid / run_id_mismatch (at /run_id), run_status_detail (error needs error, skipped needs skip_reason, ok needs neither; reported at /error or /skip_reason, null counts as absent), attachment_ref_unknown / attachment_path_invalid for calls, artifacts[] and error.log against the run's own attachments[], metric_id_invalid on every metrics key. Records have no ext namespace rule in this crate, so runs get none; the module doc says so (see "For the owner" below).
  • Eval, two accepted ids: validate keeps its signature and picks the JSON Schema by the body's schema through RecordKind::schema_for; the compiled-validator cache is one entry per accepted id. eval::declared_schema(&Value) -> Option<EvalSchema { V1, V2 }> is public. A 2.0 body with any runs key gets the single error runs_moved, whose message names PUT /evals/{ns}/{name}/runs/{run_id} and POST /evals/{ns}/{name}/runs:batch, before the schema pass. A 1.0 body validates as before, runs[] attachment checks included (1.0 arm only). An unknown schema value keeps today's /schema error.
  • eval::split_v1(&Value) -> SplitV1 { header, runs, duplicate_run_ids }, pure, the one 1.0 → 2.0 conversion for the ingest of a 1.0 body (stage 4) and the data migration (stage 6): runs removed and schema set to 2.0 on the header; per run pass / fail → ok, error → error with kind: unrecorded, skipped → skipped with skip_reason: unrecorded, the dropped outcome kept verbatim under meta.v0.2.0_migration.outcome, the header's facets materialised onto the run, the header attachments the run names copied into its attachments[] (copied, not moved, so the header's hash equals that of the same header posted as 2.0), duplicate run_id last wins and is reported. A struct instead of the issue's tuple, so the duplicate list has a name.
  • Card run_results[]: metric_id_invalid, run_resul 8000 t_value_or_label, run_results_eval_unknown (eval compared on {ns}/{name} against the Card's core/uses_eval targets with @seq stripped; external: / hf: targets never match). run_unknown / run_not_in_used_set (store, stage 5) and too_many_run_results (server, stage 7) are in the rule table with their owner.
  • fingerprint::for_run and run::materialise(run, header) (pure, idempotent; only an absent facet is copied, whole facet; a run's explicit null is its own claim).
  • Hashes, public, formulas in the crate doc: run_content_hash(run_id, &Value) = SHA-256 of the RFC 8785 form of { …run, "run_id": run_id } (the caller passes the materialised body); runs_hash(&[(run_id, hash)]) = SHA-256 of the RFC 8785 form of [[run_id, hex], …] sorted by run_id bytes; the used-set hash is the same function. Archive and delete change none of them. Tests carry literal hex a client can reproduce (recomputed independently in review).
  • Crate doc and validate.rs module doc rewritten for the model: runs as rows, the two Eval arms, the run rule table, the formula table.
  • Fixtures (fixtures/validate/, 48 new files): valid-run, run-status-detail-{valid,ok,error,error-null,skipped-absent,skipped-null,skipped-error}, run-refs-{unknown,path-invalid}, run-id-{empty,slash,max,too-long,mismatch} (byte lengths 200 / 201), run-metric-id, eval-runs-moved, eval-run-set-v1, eval-schema-unknown, card-run-results-{valid,metric,value-or-label,eval-unknown}. eval-run-refs.json is back to its 1.0 body with run pointers (stage 1 had left it testing nothing). tests/run.rs (19 tests): materialise, split_v1 for every outcome, meta, facet and attachment copy, duplicate ids, header-hash equality, the literal hashes, for_run.

Verified

Command Outcome
cargo fmt --check clean
cargo clippy --workspace --all-targets -- -D warnings clean
cargo test -p evalhub-schema 20 passed
cargo test -p evalhub-core 62 passed (unit 13, canonical 5, fingerprint 8, id 5, run 19, validate 12)
cargo test -p evalhub-query 15 passed
cargo test -p evalhub-store (Docker) 63 passed
cargo test -p evalhub-server (Docker) 60 passed
cargo doc --no-deps -p evalhub-core 0 warnings
hash literals the five values recomputed from the canonical strings with an independent SHA-256; all match

Not run: cargo deny check (no dependency change), cargo sqlx prepare --check (no SQL change), just e2e.

Observable effect through the server (a consequence of the issue text, no server code changed)

  • a 1.0 Eval body now passes validate, so until stage 4 adds the conversion at ingest the server stores it with runs in the body;
  • a 2.0 body with runs now gets 422 runs_moved instead of schema errors.

For the owner (out of this stage's scope, not changed here)

  • crates/evalhub-schema/fixtures/card-run-results.json (stage 1) has counts attempted: 2, completed: 2, failed: 1, which fails counts_inconsistent. Core keeps a corrected copy (completed: 1). A later stage that posts the schema-crate fixture will get a 422 until it is fixed.
  • evalhub-schema's common.rs calls ext namespacing mandatory, and Core: validate a Run, refuse runs in an Eval body, check run_results, fingerprint a run, publish the run hash formulas #28 says "the ext namespace rule as for records", but no crate enforces it for records or runs. Either a rule for both (new code, is_valid_id on each key) or a softer doc; a separate change.

Refs #28

… fingerprint a run, publish the run hash formulas

Stage 2 of #24. Stage 1 gave the hub the shapes (a Run type, a
header-only Eval under evalhub.eval/2.0, Card.run_results); this stage
gives evalhub-core the rules and the formulas, and nothing else:
store and server behaviour is unchanged, and no signature they call
moves.

`validate::run(run_id, body)` is a new entry point, not a RecordKind
variant (a run is not a record, and the kind is matched exhaustively
in six places). It runs run.json and then the semantic rules:
run_id_invalid / run_id_mismatch, run_status_detail (error needs
`error`, skipped needs `skip_reason`, ok needs neither), the
attachment reference checks for calls / artifacts[] / error.log
against the run's own attachments[], and metric_id_invalid on every
metrics key. Records have no ext namespace rule in this crate, so
runs get none either; the module doc says so.

Eval now has two accepted schema ids. `validate` keeps its signature
and picks the document by the body's `schema` through
RecordKind::schema_for, with one compiled validator per accepted id.
`eval::declared_schema` tells the caller which arm applied. A 2.0
body carrying `runs` gets the single error runs_moved, naming the
run endpoints, before the schema pass; a 1.0 body validates as
before, including its runs[] attachment checks.

`eval::split_v1` is the one 1.0 -> 2.0 conversion, pure, for the
ingest of a 1.0 body (stage 4) and the data migration (stage 6):
outcome pass / fail -> ok, error -> error with kind `unrecorded`,
skipped -> skipped with skip_reason `unrecorded`, the dropped outcome
kept under meta.v0.2.0_migration, the header's facets materialised
onto each run, the header attachments a run names copied into it,
duplicate run_ids last-wins and reported. Returning a struct rather
than the tuple in the issue keeps the duplicate list named.

Card run_results[] get metric_id_invalid, run_result_value_or_label
and run_results_eval_unknown (compared on {ns}/{name}, ignoring
@seq); the rules that need the store or a count are listed in the
table with their owner.

`fingerprint::for_run`, `run::materialise` (pure, idempotent, only an
absent facet is copied) and the hashes are public with their formulas
in the crate doc: run_content_hash = SHA-256 of the RFC 8785 form of
the materialised run with its run_id; runs_hash = SHA-256 of the RFC
8785 form of [[run_id, hex], ...] sorted by run_id bytes. Archive and
delete change none of them. The hash tests carry literal hex a
client can reproduce.

Known and left for the owner: the stage-1 fixture
crates/evalhub-schema/fixtures/card-run-results.json has counts that
fail counts_inconsistent (attempted 2 < completed 2 + failed 1);
core keeps a corrected copy, the schema-crate file is out of this
stage's scope. And the schema doc calls ext namespacing mandatory
while no crate enforces it, for records or runs.

Verified: cargo fmt --check; cargo clippy --workspace --all-targets
-D warnings; cargo test -p evalhub-schema (20 passed), -p evalhub-core
(62), -p evalhub-query (15), -p evalhub-store (63, Docker),
-p evalhub-server (60, Docker); cargo doc --no-deps -p evalhub-core
(0 warnings); the five literal hash values recomputed independently
from the canonical strings and matching.

Refs #28

Signed-off-by: Yutaka Nishimura <ytk.nishimura@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0