Validator Contract

A validator is an executable - an OCI image in production, any command during development - that implements five subcommands. The service talks to it through a job directory (passed via --job) and JSON documents; nothing else crosses the boundary. Validators are stateless: every invocation is a pure function of its command line and the job directory contents.

Validators reach the server as published container images: see Challenges in the Client API for the publish, revise, and retire endpoints. There is no in-repository registry file.

This contract is a strict, closed schema. contract_version must be "3"; unknown fields are rejected. Contract v2 manifests are not accepted by this server.

Problem, task, and view

The validator author declares one or more problems P. Each problem has a human-readable title, one output requirement, and one sweep axis. The server and web UI then reason about concrete tasks P+s, where s is one element of the problem's sweep vector. A view is only an input mode for generating and scoring that same concrete task; it does not change the identity of the problem or the task.

The public structure is:

The five commands

describe

Print the validator manifest as JSON on stdout. The manifest declares the validator's name and version, its views, its problems, the output file expected for each problem, and the metrics used for ranking and display.

generate

generate --task <task_id> --view <view_id> --seed <hex> --job <dir>

Create a fresh dataset. The validator writes the participant-visible challenge into <dir>/public/ and everything needed for scoring into <dir>/private/. The service pre-creates both directories (and <dir>/submission/) before the call; the validator only writes files into them.

Two hard rules:

score

score --job <dir>

Read the task and view metadata from the files generate stored in <dir>/private/, then read <dir>/private/ and <dir>/submission/, compare them, and write the verdict to <dir>/result.json - or to <dir>/error.json when the submission is invalid.

self-test

self-test --job <dir>

Run one Validator-owned, known-valid scoring example and write the same result.json that score writes. The fixture and any answer material stay inside the Validator image; the command must not print or place them in public/. Publication requires self_test: true in the manifest and a successful self-test whose metrics and details satisfy the declared contract. This prevents a Validator that rejects every submission from passing merely because it rejects the empty conformance fixture correctly.

artifact

artifact --task <task_id> --artifact <artifact_id> --job <dir>

Materialize one public artifact declared by that task. Write the requested content to <dir>/public/artifact and a JSON provenance document to <dir>/public/provenance.json. Both must be regular files no larger than 16 MiB; all artifacts in one validator are capped at 64 MiB total. The publishing worker generates and stores them before the revision goes live, so the API never runs validator containers while serving a download.

The job directory

The service owns the job directory; the validator only ever reads and writes inside it.

PathWritten byContents
public/generateDataset files served to the client
private/generateAnswer keys and scoring metadata, kept server-side
submission/the serviceThe client upload, stored under the declared filename
result.jsonscoreMetrics for a valid submission
error.jsonscoreExplanation for an invalid submission
public/artifactartifactRequested public file or format guide
public/provenance.jsonartifactJSON describing the artifact's source or generation command

Exit codes

Exit codeMeaningService behavior
0SuccessProceed
2Invalid submissionHTTP 400; the session is kept so the client can retry
anything elseValidator faultHTTP 502; the session is released

Before finishing with exit code 2, score must write error.json explaining what was wrong with the submission.

Packaging as an image

In production the validator is an OCI image, and the image's ENTRYPOINT is the contract CLI: the service appends only the subcommand and its flags, so docker run <image> describe must print the manifest.

The hosted service at bench.qudeleap.com runs linux/amd64 images. Multi-platform images must include that platform; an arm64-only image is not sufficient. Self-hosted deployments must document their own worker platform.

Manifest format

describe prints one JSON object:

{
  "contract_version": "3",
  "name": "example-validator",
  "version": "1.0.0",
  "description": "Measure answer quality on blinded tasks.",
  "self_test": true,
  "views": [
    {"view_id": "view_a", "label": "View A"},
    {"view_id": "view_b", "label": "View B"}
  ],
  "problems": [
    {
      "problem_id": "example_problem",
      "title": "Example Problem (P)",
      "display": {
        "family": "example_family",
        "setting": "blinded",
        "p": [0.001, 0.002, 0.003]
      },
      "sweep_key": "p",
      "submission": {
        "filename": "answer.bin",
        "media_type": "application/octet-stream"
      },
      "tasks": [
        {"task_id": "example_problem_p001", "benchmark_version": "1.0.0", "program_timeout_seconds": 1920, "s": 0.001, "views": ["view_a", "view_b"]},
        {"task_id": "example_problem_p002", "benchmark_version": "1.0.0", "s": 0.002, "views": ["view_a", "view_b"]}
      ]
    }
  ],
  "primary_metric": {
    "name": "score_value",
    "label": "Score value",
    "direction": "lower_is_better"
  },
  "metrics": [
    {"name": "score_ci_low", "label": "Score CI low", "direction": "lower_is_better"},
    {"name": "score_ci_high", "label": "Score CI high", "direction": "lower_is_better"}
  ],
  "leaderboard": {
    "ranking": [
      {"source": {"type": "metric", "name": "score_value"}, "direction": "ascending"},
      {"source": {"type": "platform", "field": "runtime_seconds"}, "direction": "ascending"}
    ],
    "columns": [
      {
        "column_id": "rank",
        "label": "",
        "sources": [{"type": "platform", "field": "rank"}],
        "format": {"type": "integer"}
      },
      {
        "column_id": "solver",
        "label": "Solver",
        "sources": [{"type": "platform", "field": "registered_name"}],
        "format": {"type": "text"}
      },
      {
        "column_id": "score",
        "label": "Score",
        "sources": [{"type": "metric", "name": "score_value"}],
        "format": {"type": "number", "notation": "decimal"}
      }
    ],
    "details": [
      {
        "detail_id": "username",
        "label": "Username",
        "sources": [{"type": "platform", "field": "username"}],
        "format": {"type": "text"}
      },
      {
        "detail_id": "device",
        "label": "Device",
        "sources": [{"type": "platform", "field": "device_name"}],
        "format": {"type": "text"}
      },
      {
        "detail_id": "accepted_items",
        "label": "Accepted items",
        "sources": [{"type": "detail", "name": "accepted_items"}],
        "format": {"type": "integer"}
      }
    ]
  }
}

Top-level fields:

FieldTypeRequiredMeaning
contract_versionstringyesMust be "3"
public_contract_versionstringnoSet to "1" to opt into the complete participant-facing metadata profile described below
namestringyesValidator name; the challenge name claimed at publish time, globally unique and first come
versionstringyesValidator software/image release version, reported to clients and matched by the changelog
descriptionstringyesOne sentence explaining what the benchmark evaluates
resourcesarraynoUp to 8 participant-facing HTTPS links, each with label, url, and optional description
citationsarraynoUp to 8 citations, each with title and an absolute HTTPS url
usage_termsobjectnoParticipant-facing terms with label and an absolute HTTPS url
self_testbooleanyes for publicationSet to true only when self-test --job <dir> is implemented and produces a known-valid result
viewsarrayyesNon-empty list of declared input modes
problemsarrayyesNon-empty list of problems P; each problem contains at least one task
publish_conformance_pairsarraynoNon-empty list of representative {task_id, view_id} pairs used only by server-side publish checks; every pair must reference a supported task/view, be unique, and collectively cover every declared view. Omit it to check every task/view while publishing
primary_metricobjectyesThe canonical primary metric for curves and score summaries; declares name, label, and a required direction
metricsarrayyesSecondary metric declarations (name, label, direction); use [] when there are none
leaderboardobjectyesExact ranking and display definition for leaderboard consumers

Each entry of views:

FieldTypeRequiredMeaning
view_idstringyesUnique input-mode identifier
labelstringyesHuman-readable label shown in UIs
descriptionstringnoPlain-language explanation of what this input representation contains

Each problem in problems:

FieldTypeRequiredMeaning
problem_idstringyesUnique problem identifier within the validator
titlestringyesHuman-readable problem title; unique within the validator
input_descriptionstringnoPlain-language description of the task input shown to participants
methodologyobjectnoStructured explanatory page with title, summary, 1–8 stages, optional sections, and optional HTTPS links; documentation only, never part of scoring
displayobjectyesProblem metadata shown to clients and UIs, preserved in declaration order
display_groupsarraynoOrdered groups of display fields for task-page presentation
sweep_keystringyesThe name of the display field that holds the sweep vector
submissionobjectyesDeclared once per problem; the expected output file and media type
tasksarrayyesNon-empty list of concrete tasks for this problem

Each task in tasks:

FieldTypeRequiredMeaning
task_idstringyesUnique within the validator
benchmark_versionstringnoImmutable semantic version of this concrete task definition; legacy validators that omit it fall back to the top-level version
program_timeout_secondspositive integernoMaximum participant wall time from the first dataset download request through result upload; omitted tasks use the platform default (46,800 seconds)
snumberyesOne coordinate on the problem's sweep axis
viewsarrayyesNon-empty list of allowed view_ids; every task in the same problem must use the same view set
artifactsarraynoUp to 8 public artifact declarations for this task; a validator may declare at most 128 total
preflightobjectnoReferences one sample artifact and one checker artifact by ID for local, non-submitting checks

Each task artifact declares artifact_id, label, a safe ASCII filename, media_type, and optional description, role (download, guide, sample, or checker), and absolute HTTPS source_url without embedded credentials. Artifact IDs are unique within a task. When any artifact command fails validation, the whole challenge revision is rejected before it can go live.

preflight.sample_artifact_id must reference an artifact whose role is sample; preflight.checker_artifact_id must reference one whose role is checker. The public sample must be a tar or tar+zstd dataset bundle that uses the same task-program interface as a real run. The checker receives <sample-directory> <output-file> after its own path. MirrorQ CLI downloads both files from the platform but never runs the checker unless the participant explicitly supplies a checker runtime with mirrorq preflight --checker.

Each methodology stage declares a nonblank title and description, plus an optional short notation line. A methodology section declares a title, one to six paragraphs, and up to eight optional bullet items. Reference links declare label, an absolute HTTPS url without credentials, and an optional description. The platform renders these fields as plain text and safe links; validators do not supply HTML.

Display and sweep rules:

The primary_metric object:

FieldTypeRequiredMeaning
namestringyesKey in the metrics dictionary of a score result
labelstringyesHuman-readable name for UIs
directionstringyeslower_is_better or higher_is_better; decides the ranking order
descriptionstringnoPlain-language meaning of the metric
unitstringnoDisplay unit, or unitless when the metric has no physical unit

Each entry of metrics:

FieldTypeRequiredMeaning
namestringyesKey in the metrics dictionary of a score result
labelstringyesHuman-readable name for UIs
directionstringyeslower_is_better or higher_is_better; included for every declared metric
descriptionstringnoPlain-language meaning of the metric
unitstringnoDisplay unit, or unitless when the metric has no physical unit

Complete public profile

New public Validators should set public_contract_version to "1". This activates publication-time checks for the information and fixtures a participant needs before spending server capacity. The manifest must then provide:

Existing Contract v3 Validators remain loadable when the field is omitted. They can migrate without changing their scientific definitions, then opt into the strict profile once all participant-facing declarations and public fixtures are ready.

The leaderboard object:

FieldTypeRequiredMeaning
rankingarrayyesOrdered ranking rules; 1..3 items
columnsarrayyesOrdered leaderboard columns; 1..6 items
detailsarrayyesOrdered fields in the expandable web details panel; 0..16 items

Each entry of ranking declares a source plus direction (ascending or descending). A source is either:

published_at is the time the owner most recently published the result. If a result is withdrawn and later republished, the new publication time is used.

Each column declares column_id, label, sources, and format. A source is either:

Each detail entry declares detail_id, a non-empty label, sources, and format. Its source may be:

The platform renders these fields in declaration order in a generic key/value panel. It controls the layout; validators provide data declarations, not HTML. On desktop, short values are paired two per row while long, multiline, and structured values span the full row; mobile uses one field per row. The web page shows a fixed chevron outside the 1..6 main columns only when details is non-empty, and keeps at most one score expanded. Interactive details are web-only; mirrorq leaderboard still uses the declared main columns.

format is a strict object:

When present, precision is {"mode":"decimal_places"|"significant_digits","digits":...}. number and range default notation to decimal. Only the column whose single source is platform rank may use an empty label.

For detail entries, integer may use platform attempts, numeric formats may also read a Validator result detail; range may combine 2..6 metric or result-detail sources, and fraction may combine exactly two such sources. Platform rank is never valid in the expandable panel. Missing optional platform values render as —.

Platform text fields have deliberately narrow meanings:

Validation rules enforced by the server:

Score result

A successful score writes result.json:

{
  "metrics": {"score_value": 0.0035},
  "details": {"accepted_items": 350}
}
FieldTypeRequiredMeaning
metricsobjectyesMetric name -> finite number; must include the manifest's primary metric and every metric referenced by ranking, columns, or details
detailsobjectnoFree-form extras (default {}); every detail source selected for public display must be present

Error document

An invalid submission writes error.json before exiting with code 2:

{"message": "answer.bin has 12 rows, expected 20000"}

message (string, required) is the only field; it is shown to the client in the HTTP 400 response.

Versions and provenance

The manifest's top-level version identifies the validator software/image release. Each task's benchmark_version identifies the benchmark semantics of that concrete task. A validator image may therefore be released without retiring results for unchanged tasks. Bump benchmark_version only for every task affected by a change to its generation behavior, input/output protocol, allowed view IDs, task parameters, metric names or directions, or scoring behavior. Labels, descriptions, public artifacts, preflight helpers, and leaderboard presentation may change without invalidating existing scores. A new task starts with its own version. At load time, the service hashes a canonical semantic definition containing the v3 contract version, validator name, concrete task parameters, problem protocol, allowed view IDs, and metric names and directions. It records that digest for each (benchmark, version) and refuses to load a validator that presents a previously seen version with a different definition. A publish that presents one fails at the version-hash step and tells the publisher to bump the affected task's benchmark_version. For backward compatibility, omitting benchmark_version makes that task follow the top-level version, which is the pre-task-version behavior. Explanatory methodology copy is also excluded from this digest, so documentation can improve without creating a scientifically new benchmark version.

Each validator ships a CHANGELOG.md. Its first version heading must be ## <version> (an optional v prefix is accepted) and must match the manifest. The conformance kit reports this as the changelog-current check, which runs only when --changelog <path> is given. The server's publish checks have no access to the file, so keeping the changelog current is the publisher's own discipline - run the kit with --changelog in your CI.

The service always runs a published validator by immutable OCI digest, for example repository/name@sha256:<64 hexadecimal characters>, and records that digest with every score. A tag is resolved to its digest once, when the revision is pulled. For development local_command validators it records a digest of the validator source tree instead.

Validators must remain byte-deterministic for a given task, view, and seed. The service stores the seed privately with the dataset so an authorized audit can replay the instance recipe; the seed is never returned by the participant API.

Conformance

The repository ships an executable check of the mechanical contract: manifest validity and stability, a known-valid scoring self-test, deterministic generate, seed sensitivity, private-data hygiene, and invalid-submission handling. Run it in your CI:

python tools/validator_conformance.py \
  --command "<argv>" \
  --task <task_id> \
  --view <view_id> \
  --changelog path/to/CHANGELOG.md

Pass --image <reference> instead of --command to check the container you are about to publish; the two are mutually exclusive and one is required. The checks are describe-valid, describe-stable, score-accepts-known-valid, changelog-current (only with --changelog), generate-succeeds, generate-deterministic, generate-seed-sensitive, no-private-leak, and score-rejects-empty.

no-private-leak rejects the literal seed, complete private files, and sampled 12-byte fragments from the beginning, middle, and end of private files when they appear in public/. This catches common packaging mistakes but cannot prove semantic secrecy (for example, an encoded or transformed answer leak). Validator review and Validator-owned adversarial tests remain required.

Without --task and --view the standalone kit checks every task and view the manifest declares, regardless of publish_conformance_pairs, and reports a per-pair failure as <check>: <task_id>/<view_id>: <detail>; --task and --view narrow that to the single pair you name. During the conformance step of a publish, the server uses publish_conformance_pairs when declared and otherwise checks every pair. The declaration controls publish cost only; it does not remove tasks or views from the public manifest and does not change normal benchmark execution.

Changes from v2

Contract v3 is a breaking change from v2:

  1. contract_version must be "3" and unknown fields are rejected.
  2. views and problems are top-level. Tasks live under a problem, not at the top level.
  3. display is problem metadata only; it is ordered and generic, and the server does not invent titles, curve groups, or presentation hierarchy.
  4. submission is declared once per problem.
  5. curves is gone. primary_metric and metrics are still present, but curves are now derived by the server from problems, tasks, and scores.
  6. generate now requires --view.