# OMX Conformance Tests

Each test has a check you run and an observable pass condition, so this list doubles as a test
plan. Run the group for each stage your product runs, and skip the others:

| Group                                   | Run it if your product                                       |
| --------------------------------------- | ------------------------------------------------------------ |
| A. Registration                         | Serves a registration document                               |
| B. Registration that names destinations | Also puts a `publish` array in that document                 |
| C. Publisher                            | Accepts results documents                                    |
| D. Meet Operations                      | Reads registration documents and hands off results documents |

OMX-C107, in group A, applies to every stage. A test marked **must** is required for conformance,
one marked _should_ is a strong recommendation, and one marked _may_ applies only if you build the
optional feature it tests. A test's id never changes, and an id the list skips is never reused.

Three tools do much of the work for you:

- The strict schemas, the registration check
  [`registration-document-v1`](/omx/schemas/registration-document-v1.schema.json) and the results
  check [`results-document-v1`](/omx/schemas/results-document-v1.schema.json), fail a value this
  version does not define, a date-time without seconds, the wrong `omx.kind`, and a field your stage
  does not write. No schema fails a field the format does not define (OMX-R129), so a misspelt key
  passes them: check your field names against the [field reference](/omx/reference).
- [`omx-check.mjs`](/omx/tools/omx-check.mjs) checks what no schema can, and names the test each
  problem fails. It reads the document's `omx.kind`; `--kind registration` or `--kind results`
  checks a document as that kind whatever it says.
- Each test that needs a crafted document names a **fixture**. The
  [fixture index](/omx/fixtures/index.json) lists them all with the outcome a conforming product
  produces (`accept`, `reject`, `refuse-version`, `refuse-kind`, `refuse-results`, `older`, `equal`,
  or `operator`), and the [test page](/omx/test) runs the Publisher tests against your endpoint.
  `accept` is what a reader does; the checker, which judges a document as a writer would send it,
  may still report problems in an `accept` fixture that a reader is meant to tolerate, such as
  `null` values or a later version's values.

**Fixture order.** Each fixture's `updated_at` is later than every example's and than the fixture
before it in the index, so the Publisher fixtures applied to one meet in the index's order each
apply. `older` and `equal` fixtures are sent right after `c30-first`, to a meet holding nothing
later. A `partial` fixture is sent after the `full` document its test names.

You run every test yourself; none needs sign-off from 2Timer. If a test is ambiguous, that is a bug
in the format: report it to [jason@2timer.app](mailto:jason@2timer.app?subject=OMX%20conformance).

---

## A. Registration

Required of anyone serving a registration document, whether or not you want results back.

**OMX-C1 — The document validates** (must)

- **Check:** validate your document against
  [`registration-document-v1`](/omx/schemas/registration-document-v1.schema.json) with any
  draft-07 validator, with no add-ons or options, and run `node omx-check.mjs` on it.
- **Pass:** zero errors from both, including for a meet with relays, scratches, and at least one
  unattached athlete. The lenient [reader schema](/omx/schemas/meet-document-v1.schema.json)
  accepts values a later minor version may define, so passing it alone does not pass this test.

**OMX-C107 — Every document says its kind** (must)

- **Check:** fetch your document and read `omx.kind`. If you also read documents, offer your
  product the fixtures [`c107-missing-kind.omx.json`](/omx/fixtures/c107-missing-kind.omx.json)
  and [`c107-unknown-kind.omx.json`](/omx/fixtures/c107-unknown-kind.omx.json).
- **Pass:** your document says `"kind": "registration"`, and a results document you write says
  `"results"`. Meet Operations refuses a document with no kind, or a kind it does not know, before
  creating anything, and names what it found. A Publisher answers both `422` with `errors` naming
  `/omx/kind`, and answers a registration document `422` too: it reads results documents only. No
  product works out a document's kind from its fields (OMX-R135).
- **Fixtures:** `c107-missing-kind`, `c107-unknown-kind`

**OMX-C2 — Ids are stable across pulls** (must)

- **Check:** fetch the document, change something unrelated in your database, fetch again. Diff the
  `id` of every team, athlete, entry, event, and division.
- **Pass:** no id changed. Regenerated ids break the results join and are the single most common
  implementation error.

**OMX-C3 — Ids are unique within their list** (must)

- **Check:** for each of `teams`, `athletes`, `entries`, `events`, `divisions`,
  `groups`, `waves`, and `publish`, count distinct ids against list length. Count distinct team
  `code`s against the number of teams.
- **Pass:** equal. `omx-check.mjs` checks this.
- **Fixture:** [`c3-duplicate-ids.omx.json`](/omx/fixtures/c3-duplicate-ids.omx.json)

**OMX-C4 — References resolve** (must)

- **Check:** for every `team_id`, `athlete_id`, `event_id`, `division_id`,
  `group_id`, `wave_id`, and relay leg, look up the id in its list. In a results document, add
  `multi_event_id`, each entry's `round` in its event's `rounds`, `group_places[].group_id`, every
  split's `marker_id` in its event's `markers`, and every `team_scores` team, scope, and counted
  entry. A `partial` results document's team scores may name teams and entries it does not carry.
- **Pass:** every one is found. The schema cannot check this, so a validator run alone does not prove
  it; `omx-check.mjs` does.
- **Fixture:** [`c4-unresolved-reference.omx.json`](/omx/fixtures/c4-unresolved-reference.omx.json)

**OMX-C108 — Entries can be joined** (must)

- **Check:** fetch a document with individual entries and relays, including two relays from one
  team in one event.
- **Pass:** athlete + event names one entry: no athlete is entered twice in one event. Every relay
  entry carries `relay.letter`, and team + letter + event names one relay entry. A Publisher joins a
  result back to your entry through these, because no entry carries an `external_ids` (OMX-R136). A
  results document keeps the same rule within each round, and every relay in it carries its letter.
- **Fixture:** [`c108-entered-twice.omx.json`](/omx/fixtures/c108-entered-twice.omx.json)

**OMX-C5 — HTTPS only** (must)

- **Check:** confirm the document URL and every `publish[].url` begins `https://`.
- **Pass:** all do. Meet Operations refuses plain HTTP.

**OMX-C6 — The URL is a revocable capability** (must)

- **Check:** revoke the link in your own UI, then fetch it again.
- **Pass:** the fetch answers `410`. Then confirm the meet director can mint a new one without losing
  the meet.

**OMX-C7 — The URL secret is strong** (must)

- **Check:** read the code that mints the document URL secret.
- **Pass:** it draws at least 128 bits from a cryptographically secure generator (22 or more base62
  characters, not counting any prefix), and your logs redact it.

**OMX-C8 — Conditional requests work** (should)

- **Check:** fetch, keep the `ETag`, fetch again with `If-None-Match`.
- **Pass:** `304` with no body. Then change one entry and repeat.
- **Pass:** `200` with a new `ETag`. Meet Operations may fetch your link more than once; a `304`
  costs nothing.

**OMX-C9 — Browsers can fetch the document** (must)

- **Check:** from a web page on another origin, send `OPTIONS` and then a `GET` with
  `If-None-Match` to the document URL.
- **Pass:** both succeed. Responses carry `Access-Control-Allow-Origin: *` and expose `ETag`, the
  preflight allows `If-None-Match`, and nothing relies on cookies.

**OMX-C100 — The document URL answers without a redirect** (must)

- **Check:** from a web page on another origin, fetch the document URL exactly as you give it
  out: the form in your copy-link button, emails, and help pages.
- **Pass:** the URL itself answers `200`; nothing answers `301`, `302`, `307`, or `308`. A browser
  cannot follow a cross-origin redirect that lacks CORS headers, and Meet Operations sees that as a
  URL it cannot reach.

**OMX-C10 — A scratch is a status, not an omission** (must)

- **Check:** scratch an athlete in your product and fetch the document.
- **Pass:** the entry is still present with `status: "scr"`. An entry that vanished instead tells
  Meet Operations nothing: whether it treats the athlete as scratched is its own guess.

**OMX-C11 — Marks are in the unit you measured** (must)

- **Check:** inspect seeds for a track event, a field event measured in inches, and a multi-event.
- **Pass:** `ms`, `mm`, and `pt` values are integers; `in` has at most two decimal places; no
  imperial mark was converted to metric before sending; and each mark's unit is one its event's code
  ranks by: `ms` for a race, `mm` or `in` for a field event, `pt` for a multi-event.

**OMX-C12 — Age or birth date, never both** (must)

- **Check:** fetch a document for a meet with age groups or an age-spanned division.
- **Pass:** no athlete carries both `age` and `birth_date`. One whose age your product computes for
  this meet carries `age` (OMX-R128).

**OMX-C13 — Registration writes only what Registration writes** (must)

- **Check:** fetch your document and look for fields Meet Operations writes.
- **Pass:** no entry carries `round`, `heat`, `lane`, `result`, `net_result`, a place,
  `group_places`, `qualified`, `flags`, `wind_mps`, `reaction_time_ms`, `points`, `multi_points`,
  `splits`, `field_series`, or `official_note`, and none carries a `bib`: the bib is the athlete's.
  No event carries `rounds` or `markers`, the meet carries no `origin` or `track`, `omx` carries no
  `update` or `updated_at`, and there are no `team_scores`. Meet Operations that meets one of these
  in a registration document still reads the meet: it ignores the field and tells the timer.
- **Fixture:** [`c13-registration-writes-results.omx.json`](/omx/fixtures/c13-registration-writes-results.omx.json):
  Meet Operations reads it and reports the stray fields; `omx-check.mjs` reports them as errors.

**OMX-C14 — Every event has a code for the meet's sport** (must)

- **Check:** fetch a document with every event your product offers.
- **Pass:** every event's `code` is one the spec defines for the meet's `sport`, spelled exactly:
  `SP` and `3000S` are track codes, not `run` or `xc` codes. A race's code is its advertised
  distance, not what the course measured: the Riverbend "5K", which measures 4.92 km, is `5000`.
  Each distance has one code: a half marathon is `HMAR` and a marathon `MAR`, never `21097` or
  `13.1MILE`. A multi-event (`DEC`, `HEP`, `PENT`) may be for any gender. A registration document
  lists no component events. An event your product holds that has no code for the sport is not in
  the document.

**OMX-C15 — Multi-event entries go on the multi-event** (must)

- **Check:** enter one athlete in a decathlon and fetch the document.
- **Pass:** the athlete has one entry, on the `DEC` event, with any seed in `pt`. No entry points
  at a component event, and the document lists no component events: Meet Operations builds them.

**OMX-C88 — Only values this version defines** (must)

- **Check:** fetch a document with every sport your product offers and every value you write in an
  open list.
- **Pass:** each is a value the format defines, spelled as the format spells it. A value of your own
  fails, because a later version may define it to mean something else (OMX-R129). The strict
  schema reports it, and `omx-check.mjs` also reports an undefined sport or event code.
- **Fixture:** [`c88-bare-values.omx.json`](/omx/fixtures/c88-bare-values.omx.json)

**OMX-C103 — Registration writes only its statuses, and no origin** (must)

- **Check:** fetch a document with an entered, a scratched, and a wait-listed athlete, and one you
  hold as unconfirmed or checked in.
- **Pass:** every entry's `status` is `e`, `scr`, or `wait`, or absent, which means `e` (OMX-R186);
  an unconfirmed entry is left out or sent as `e`, since Entered means an entrant, confirmed or not; and `meet` carries no `origin`. Checked
  In and every later status are set at the meet. There is no Pending: `p` is not a status in this
  version (OMX-R151), and the reader schema refuses it.

**OMX-C106 — Your system id, and never your own id in `external_ids`** (must)

- **Check:** fetch a document whose athletes, teams, events, or meet carry ids on other systems.
- **Pass:** `omx.generator.id` is your system id, a name you picked, and it is the same in every
  document you write. No record lists that id in its `external_ids`: your own id for
  a record is its `id` (OMX-R130). Ids on other systems sit under theirs, such as an athlete's
  Athletic.net id under `anet`. No entry carries `external_ids` at all (OMX-R136).
  `omx-check.mjs` checks both, for every kind.

## B. Registration that names destinations

Add these when your document carries a `publish` array. Skip the whole group if it does not;
Registration with no destinations is a complete implementation.

**OMX-C18 — Destination ids are stable and unique** (must)

- **Check:** fetch twice and diff `publish[].id`; count distinct ids against array length.
- **Pass:** unchanged and unique. Meet Operations reports each destination's status against its id,
  and a timer who fetches your link again should see the same destinations, not new ones.

**OMX-C19 — Every destination has a label** (should)

- **Pass:** `label` is present and reads as a name a meet director would recognise. Without it the
  timer is asked to approve a bare URL.

**OMX-C20 — Credentials are separate** (must)

- **Check:** compare each `publish[].token` against the document URL secret and against each other.
- **Pass:** all distinct. Holding the read link must not grant the right to write results, and one
  compromised destination must not implicate another.

**OMX-C21 — `deliver` and `content` were chosen, not defaulted by accident** (must)

- **Check:** for each destination, state which `deliver` (`final`, `progressive`, or `live`) and
  which `content` (`results`, and `start_lists` or not) it asks for, and why.
- **Pass:** you can answer. The default is `final` with results only, so a destination that wanted
  the meet as it unfolds, or heat sheets, and said nothing will appear broken: it sees nothing until
  every round is official, and never a heat sheet.

## C. Publisher

Required of anyone that accepts results documents.

**OMX-C22 — Accepts a document** (must)

- **Check:** `POST` a valid `full` results document with the right bearer token.
- **Pass:** `200` or `204`.

**OMX-C23 — Browsers can hand off** (must)

- **Check:** from a web page on another origin, send `OPTIONS` and then a `POST` with
  `Authorization`, `Content-Type: application/json`, and `Content-Encoding: gzip` to your endpoint.
- **Pass:** both succeed. The preflight allows `Authorization`, `Content-Type`, `Content-Encoding`,
  and `Content-Digest`; responses carry `Access-Control-Allow-Origin: *` and expose `Retry-After`.

**OMX-C101 — The push URL answers without a redirect** (must)

- **Check:** send `OPTIONS` and a `POST` to the `url` your destination names, exactly as written,
  from a web page on another origin.
- **Pass:** the URL itself answers both; neither is redirected. A browser never redirects a
  preflight, and drops `Authorization` when a redirect leaves the origin.

**OMX-C24 — Rejects a bad credential** (must)

- **Check:** `POST` with a wrong token, then with one you have retired: expired by your own rule,
  or revoked by the meet director.
- **Pass:** `401`, then `410`. You decide how long a token works, and keep it working long enough
  for corrections after the meet; once it stops, you answer `410` (OMX-R164). Anything else leaves
  the timer without a usable message.

**OMX-C125 — Answers the probe correctly, if it answers** (may)

- **Check:** send `GET` to your destination `url` with the right bearer token, then with a wrong
  one, from a web page on another origin.
- **Pass:** either you do not answer probes (`404` or `405`), or the right token gets `200` with
  `{ "accepts": [...], "generator": { "id", "name", "version"? } }`, where `accepts` lists the
  versions you read and `generator` names you as your documents do, or `503` with `Retry-After`
  while you are not ready; and the wrong token gets `401`. The preflight allows `GET`, the response
  carries `Access-Control-Allow-Origin: *`, and nothing you hold changes.

**OMX-C25 — Tokens you issue are strong** (must)

- **Check:** read the code that mints destination tokens.
- **Pass:** at least 128 bits from a cryptographically secure generator, compared in constant time,
  and redacted from logs.

**OMX-C26 — Refuses a version it does not implement** (must)

- **Check:** `POST` a document with `omx.version: "9.0"`.
- **Pass:** `422`, with a message naming the version offered and the versions you accept, not a
  schema error: read the version before validating. Nothing you hold changes.
- **Fixture:** [`c26-unknown-version.omx.json`](/omx/fixtures/c26-unknown-version.omx.json)

**OMX-C99 — Says which versions it reads** (should)

- **Check:** `POST` a document with `omx.version: "9.0"`.
- **Pass:** the `422` body carries `accepts`, listing every version you read, such as
  `["1.0"]`, so Meet Operations knows what you read.
  If your destination is named in a `publish` item or a destination card, `accepts` there lists
  the same versions.
- **Fixture:** [`c26-unknown-version.omx.json`](/omx/fixtures/c26-unknown-version.omx.json)

**OMX-C27 — Explains an invalid document** (should)

- **Check:** `POST` a document with an entry whose `result.value` is not a number.
- **Pass:** `400` or `422`, with `errors` naming the path, so the timer can pass it on. Nothing you
  hold changes.
- **Fixture:** [`c27-invalid-mark.omx.json`](/omx/fixtures/c27-invalid-mark.omx.json)

**OMX-C28 — A repeat handoff is a no-op** (must)

- **Check:** send the same document twice. Snapshot your stored state after each.
- **Pass:** identical. No duplicated entries, no doubled counts.

**OMX-C29 — A `full` document supersedes everything before it** (must)

- **Check:** send a `full` document, then a corrected `full` one with a later `updated_at`, the
  same `omx.generator.id`, and the same `meet.id`.
- **Pass:** what you hold matches the second document exactly, with nothing left over from the
  first: a round, entry, event, team, or athlete the second lacks is gone. You key a meet on
  `omx.generator.id` plus `meet.id`, whichever credential it arrives on; a document with the same
  `meet.id` under another `generator.id` is another product's meet (OMX-C91).

**OMX-C30 — An older document is refused** (must)

- **Check:** send a document with `omx.updated_at` of `2026-10-03T14:41:52.418-04:00`, then one with
  `2026-10-03T14:40:00.000-04:00`. Then send `2026-10-03T18:41:52.418Z`, the first instant written
  with a different offset.
- **Pass:** the second answers `409` and what you hold still matches the first. The third is
  treated as equal: `200`, no change. Comparing the strings as text fails this test. The rule is the
  same whatever each document's `update`.
- **Fixtures:** [`c30-first.omx.json`](/omx/fixtures/c30-first.omx.json),
  [`c30-older.omx.json`](/omx/fixtures/c30-older.omx.json), and
  [`c30-same-instant.omx.json`](/omx/fixtures/c30-same-instant.omx.json), sent in that order

**OMX-C31 — A missed handoff heals itself** (must)

- **Check:** drop a handoff entirely, then accept the next one.
- **Pass:** you are fully correct. Nothing has to detect the gap or replay a log: a `partial`
  document carries every round changed since the last document you accepted, and a `full` one
  carries the whole meet. If you lost what you accepted, you answer `412` and the next document is
  `full` (OMX-C121).

**OMX-C32 — Creates a meet it has never seen** (must)

- **Check:** with no prior knowledge and no Registration anywhere, accept a `full` document.
- **Pass:** you can create the meet (name, sport, date, time zone when given, venue, schedule,
  events, teams, athletes, entries) without asking anyone for anything.

**OMX-C33 — Reads `meet.origin`** (must)

- **Check:** accept a meet whose `origin.id` is the document's own `omx.generator.id`: the timer
  built it.
- **Pass:** you do not wait for, warn about, or error on missing Registration ids. Such a meet was
  built without Registration and never will have one.

**OMX-C34 — Joins through athletes and events** (must)

- **Check:** accept a document whose athletes, teams, and events carry your id in `external_ids`,
  under your system id, or an id on another system you know, such as `anet`.
- **Pass:** each athlete, team, and event is attached to your record by that id, never by name. An
  entry carries no `external_ids`: you find it through its athlete and its event, a relay through
  its team, letter, and event, and a multi-event component through its athlete and the component
  event, whose `multi_event_id` leads to the multi-event.

**OMX-C35 — Keeps entries with no Registration id** (must)

- **Check:** accept a document built from a registration document (its `origin.id` is
  Registration's) containing a walk-up athlete the timer created, with an entry.
- **Pass:** the entry appears, with its athlete's own name, team, and bib. Dropping it is a failure:
  a dropped entry means incomplete results and a wrong team score, published as if correct.

**OMX-C36 — Survives an athlete acquiring a Registration id** (must)

- **Check:** accept an athlete with no Registration id, then accept a later document where that
  same athlete `id` carries one in `external_ids`.
- **Pass:** one athlete, updated. Two means your storage is keyed on the external id and will
  duplicate every walk-up the timer later merges against a roster.

**OMX-C37 — Compares on `result.value`, not `display`** (must)

- **Check:** ingest two results of 10.605 and 10.614, both displaying as "10.61".
- **Pass:** you can tell them apart and order them correctly. A tie exists only where two entries
  in a round share a place, so comparing display strings invents ties that are not there.

**OMX-C38 — Handles a non-placer that carries a result** (must)

- **Check:** ingest an entry with `status: "dq"` and a populated `result`.
- **Pass:** the time or mark is retained and displayed, and no place is assigned. Whether an entry
  places is decided by `status` alone, never by whether `result` is populated.

**OMX-C40 — Invents no status codes** (must)

- **Check:** ingest an entry with `status: "nr"` in a race and one in a shot put, and a `dnf` on a
  decathlon's own entry.
- **Pass:** the `nr` entries are stored as No Result with no value, and rendered as `NT` and `NM`.
  The decathlete is shown as having abandoned, with no place in the standings. If you wrote `NT`,
  `NH`, or `NM` into a status field, or rendered `NR` on a results sheet, that is a failure: `NT`,
  `NM`, and `NH` are how `nr` prints, not statuses.

**OMX-C41 — Never recomputes** (must)

- **Check:** accept a result set whose places or team scores you would compute differently, such as a
  tie or an XC score with displacers.
- **Pass:** you display the published values unchanged. Two entries in one round with the same
  place are tied; you print them as tied without a separate flag.

**OMX-C42 — Ignores what it does not know** (must)

- **Check:** accept a document containing an unknown optional field, a camelCase key, a `$schema`
  key, and a field prefixed with another product's name (`examplereg:season_id`).
- **Pass:** accepted. Never fail an ingest over an unrecognised field.
- **Fixture:** [`c42-unknown-fields.omx.json`](/omx/fixtures/c42-unknown-fields.omx.json)

**OMX-C43 — Treats `null` as absent** (must)

- **Check:** accept a document where an entry has `"lane": null` and a finisher has
  `"result": null`.
- **Pass:** accepted. The entry has no lane, and the finisher is a finisher with no time.
- **Fixture:** [`c43-nulls.omx.json`](/omx/fixtures/c43-nulls.omx.json)

**OMX-C44 — Publishes a multi-event at both levels** (should)

- **Check:** accept a results document with a scored heptathlon.
- **Pass:** you can show the standings from the multi-event entries, each component's results from
  its own event, and one athlete's whole competition by grouping their component entries: the same
  athlete, in the events whose `multi_event_id` names the heptathlon. A component entry's
  `multi_points` is what it earned; its `points`, if any, are team points.

**OMX-C45 — Keeps personal data private** (must)

- **Check:** accept a document where an athlete has a `birth_date`, an `email`, a `phone`, and
  `notes`, as a non-conforming writer might send.
- **Pass:** nothing you publish shows any birth date, email, phone number, or note. A conforming
  results document carries none of them: it lists only the athletes its entries name, with the age
  Meet Operations placed them by.

**OMX-C46 — Rate limiting says when to come back** (should)

- **Check:** if you rate-limit, exceed the limit.
- **Pass:** `429` with a `Retry-After` header.

**OMX-C82 — Accepts a large meet** (must)

- **Check:** `POST` a valid document that is 64 MB uncompressed, once as is and once gzipped. The
  [test page](/omx/test) builds one.
- **Pass:** `200` or `204` both times. A `413` for a document below 64 MB uncompressed is a failure
  (OMX-R182).

**OMX-C109 — Accepts a gzip body** (must)

- **Check:** `POST` a valid document with `Content-Encoding: gzip` and the gzipped document as the
  body, then the same document uncompressed.
- **Pass:** both are accepted and give the same stored state. Meet Operations may compress any
  handoff, or not (OMX-R140).

**OMX-C83 — Gives the timer a destination card** (should)

- **Check:** set up a destination in your product the way a meet director would.
- **Pass:** you show a destination card with a copy button, or offer it as a file, and it validates
  against [`destination-card-v1`](/omx/schemas/destination-card-v1.schema.json), with the `deliver`
  and `content` you want. Without one, a timer has to copy a URL and a token by hand.

**OMX-C84 — Takes the event's result from the final** (must)

- **Check:** accept a document in which an event's prelims are official with places and its final,
  marked `final: true`, is `upcoming` with no entries.
- **Pass:** you show no result for the event yet, and you do not present the prelim places as the
  event's places. When the final is official, its places are the event's. You never decide the
  final from a round's number or label.
- **Fixture:** [`c84-final-not-run.omx.json`](/omx/fixtures/c84-final-not-run.omx.json)

**OMX-C110 — Shows a mark in a unit it does not know** (must)

- **Check:** accept a document with a result whose `unit` is one this version does not define, with a
  `display`.
- **Pass:** accepted. The result is shown by its `display`, and is never compared, sorted, or
  converted. The places that came with it are shown as given (OMX-R141).
- **Fixture:** [`c110-unknown-unit.omx.json`](/omx/fixtures/c110-unknown-unit.omx.json)

**OMX-C90 — Reads values from a later version safely** (must)

- **Check:** accept a document carrying a round with `status: "provisional"` and `timing: "laser"`.
- **Pass:** accepted. The round is shown as not official and the timing as unknown. An unknown value
  in an open list never fails an import, and never makes a result look official.
- **Fixture:** [`c90-later-values.omx.json`](/omx/fixtures/c90-later-values.omx.json)

**OMX-C91 — Keeps two Meet Operations products apart** (must, if you are also Registration)

- **Check:** accept the cross country results example, then a document for the same registration
  meet (the same Registration id in `meet.external_ids`) under a different `omx.generator.id` and
  `meet.id`, from another Meet Operations product.
- **Pass:** you hold both, and neither replaced the other; your operator is told the meet has a
  second Meet Operations product. Each is then replaced only by its own later documents.
- **Fixture:** [`c91-second-timing-system.omx.json`](/omx/fixtures/c91-second-timing-system.omx.json),
  sent after `xc-invitational-returned.json`

**OMX-C92 — Prints team scorers from the standing** (should)

- **Check:** accept a cross country results document whose standings list their `entries`.
- **Pass:** a team's line shows its scorers and its displacers as the standing lists them, each with
  its entry's own `points`: "45 (3-7-9-12-14) [18-22]", with no recomputation. A displacer's
  `points` is its scoring position.

**OMX-C111 — Applies a partial document** (must)

- **Check:** send [`c30-first.omx.json`](/omx/fixtures/c30-first.omx.json), a `full` document, then
  [`c111-partial.omx.json`](/omx/fixtures/c111-partial.omx.json), a later `partial` one carrying one
  event's round.
- **Pass:** that round is replaced whole: its status, its heats, and every entry in it, an entry it
  no longer lists gone. The team score blocks it carries are replaced by their `id`. The meet,
  divisions, groups, waves, events, teams, and athletes it carries are added or updated by
  `id` (OMX-R180). Every other round, entry, event, team, and
  athlete is exactly as `c30-first` left it: a `partial` document deletes nothing outside its rounds.

**OMX-C112 — Refuses an unknown `update`** (must)

- **Check:** `POST` a results document whose `omx.update` is a value this version does not define.
- **Pass:** `422` with `errors` naming `/omx/update`, and nothing you hold changes. Reading it as
  `full` would delete every round it does not carry.
- **Fixture:** [`c112-unknown-update.omx.json`](/omx/fixtures/c112-unknown-update.omx.json)

**OMX-C113 — Shows a cancelled round** (must)

- **Check:** accept a document in which one event's round is `cancelled`, with no entries.
- **Pass:** the event is shown as cancelled, not as "results to come", and no result is shown for it.
- **Fixture:** [`c113-cancelled-round.omx.json`](/omx/fixtures/c113-cancelled-round.omx.json)

**OMX-C114 — Shows athletes still competing** (should)

- **Check:** send [`road-5k-returned.json`](/omx/examples/road-5k-returned.json), a `full` document,
  then accept a later `partial` document whose round is `started`, with finishers so far carrying
  their `overall_place` and runners still on the course as `s`, some with splits.
- **Pass:** the finishers are shown with their places, marked as able to move while the round
  runs; the `s` runners are shown as still competing, with any splits and no place; and the round
  is not shown as official.
- **Fixture:** [`c114-race-in-progress.omx.json`](/omx/fixtures/c114-race-in-progress.omx.json)

**OMX-C120 — Shows an event whose code it does not know** (must)

- **Check:** accept a results document with an event whose code this version does not define, such
  as a later minor version's `600Y`, with placed entries, an `nr` entry, and a team score block.
- **Pass:** the event is shown by its `name`, with places as given, marks by `display`, the `nr`
  entry printed as `NR`, and its team scores as sent. Nothing in it is dropped or recomputed
  (OMX-R179).
- **Fixture:** [`c120-unknown-code.omx.json`](/omx/fixtures/c120-unknown-code.omx.json)

**OMX-C121 — Asks for the whole meet** (must)

- **Check:** with nothing held for a meet, accept a `partial` document for it; then accept a `full`
  one; then a `partial` again.
- **Pass:** the first `partial` gets `412`, and nothing is stored from it; the `full` gets `200` or
  `204`; the second `partial` is applied. A Publisher that restores a backup or refused a `full` with
  `413` answers `412` to every `partial` until a `full` arrives (OMX-R180).
- **Fixture:** [`c111-partial.omx.json`](/omx/fixtures/c111-partial.omx.json) and
  [`c30-first.omx.json`](/omx/fixtures/c30-first.omx.json)

## D. Meet Operations

Required of timing or meet-management software that reads registration documents and hands off
results documents.

**OMX-C50 — Builds a meet from a pasted URL** (must)

- **Check:** paste a registration document URL into an empty install.
- **Pass:** venue, schedule, divisions, events, teams, athletes, and entries exist with no wizard
  and nothing typed twice. Fetching the same link again later returns the same kind of complete
  document; how you merge it is your own decision.

**OMX-C105 — Leaves out an event whose code it does not know** (must)

- **Check:** read a registration document with an event of code `SWIM`, which this version does not
  define.
- **Pass:** the rest of the meet is read, and that event and its entries are not: the timer is told
  which event was left out and why. This is Meet Operations' rule; a Publisher shows such an event
  (OMX-C120). The event is never turned into one your product knows from its
  name. An event code is an open list, so a later minor version's event never fails a whole meet
  (OMX-R132). A writer's checker still reports the code, because a writer sends only codes this
  version defines.
- **Fixture:** [`c105-undefined-code.omx.json`](/omx/fixtures/c105-undefined-code.omx.json)

**OMX-C104 — Keeps the ids other systems gave a record** (must)

- **Check:** read a registration document whose meet carries `external_ids` with `milesplit` and
  `anet`, and whose athletes carry `anet` ids and an id under a system your product does not know,
  such as `"exampleresults": "77812"`. Then hand the meet off.
- **Pass:** the results document carries every one of those ids, unchanged, on the same records,
  beside Registration's own id under Registration's system id, whether or not your product knows
  the system (OMX-R142). A Publisher keyed on any of them can match its records. The timer may
  edit or remove one.

**OMX-C102 — Refuses a results document** (must)

- **Check:** offer a document whose `omx.kind` is `"results"`, with results and without. Then offer
  a registration document.
- **Pass:** both results documents are refused before anything is created or changed, and the timer
  is told that a meet is moved between computers with your product's own backup. The registration
  document reads as usual. Meet Operations takes only Registration's handoff; results only ever go
  forward, to a Publisher. The kind is read from `omx.kind`, never worked out from the document's
  fields (OMX-R135).
- **Fixtures:** [`c102-results-document.omx.json`](/omx/fixtures/c102-results-document.omx.json),
  refused; the [cross country registration example](/omx/examples) reads as usual

**OMX-C51 — Reads a document from a file** (should)

- **Check:** open a document saved as `meet.omx.json`, and the same document saved as `meet.json`.
- **Pass:** both build the same meet as fetching it from a URL would.

**OMX-C52 — Refuses plain HTTP** (must)

- **Pass:** an `http://` document or destination URL is rejected with a clear message.

**OMX-C53 — Refuses an unknown major, reads a later minor** (must)

- **Check:** offer a document with `omx.version: "9.0"`, one carrying a draft number such as
  `"0.9"`, and one carrying a later minor of the major you implement, such as `"1.7"`.
- **Pass:** the first two refused, naming the version offered and the versions you implement.
  Importing what you half-understand is worse than failing. The later minor is read: a minor only
  adds, so you ignore what you do not know (OMX-R175).
- **Fixtures:** [`c53-unknown-draft.omx.json`](/omx/fixtures/c53-unknown-draft.omx.json),
  [`c53-later-minor.omx.json`](/omx/fixtures/c53-later-minor.omx.json)

**OMX-C123 — One malformed value never fails a whole meet** (must)

- **Check:** read a registration document whose athlete carries `country: "JPN"`, whose team code
  is 20 characters, and whose destination's `token` is 12 characters.
- **Pass:** the meet is read, and the timer is warned about each value. The writer's own checks
  still fail the document: the strict schema keeps every format (OMX-R181).
- **Fixture:** [`c123-malformed-values.omx.json`](/omx/fixtures/c123-malformed-values.omx.json)

**OMX-C55 — Keeps the meet when its link is revoked** (must)

- **Check:** set up from a document URL, then have Registration revoke it and fetch the link again.
- **Pass:** the `410` is shown to the timer as a link that no longer works, and the meet, its
  results, and its destinations are all still there.

**OMX-C61 — A multi-event is built, never guessed** (must)

- **Check:** read a document with a `HEP` event with gender `M` in a `tf-outdoor` meet, which the
  spec's default table does not list.
- **Pass:** you build its component events by your own rules for that meet, or, if you have none,
  you report the event to the timer and leave it out. You never guess its events from its name. The
  default table is where you start, not a limit on what Registration may send (OMX-R139).
- **Fixture:** [`c61-multi-off-table.omx.json`](/omx/fixtures/c61-multi-off-table.omx.json)

**OMX-C63 — Exhibition athletes never score** (must)

- **Check:** score an event in which an `exhibition: true` athlete finishes between two scorers.
- **Pass:** the exhibition athlete holds a place and any award, scores nothing, and the next scorer
  is not pushed down a scoring slot.

**OMX-C124 — Writes team scores** (must)

- **Check:** score a cross country race with one complete team and one too small to score, and a
  track meet with an event's score and the meet's total; hand off twice.
- **Pass:** every `team_scores` block has an `id`, unique in the document and the same in both
  handoffs. A sprinter's `points` is one value, which the event's block and the meet's total both
  use. The incomplete team is listed with `complete: false` and no `place`, `points`, or
  `entries`; its runners carry no `points`, and the other team's scoring positions skip them
  (OMX-R178). `omx-check.mjs` checks the incomplete team.

**OMX-C64 — Every handoff stands on its own** (must)

- **Check:** capture what you send to a destination at three points in a meet.
- **Pass:** each `full` document is a whole meet document. Each `partial` document carries each of
  its rounds whole, with every athlete and team its entries name, so it makes sense applied to the
  last document the Publisher accepted, whichever that was. If any of them needs a document the
  Publisher may have missed, that is a failure.

**OMX-C65 — A handoff carries no secrets or private data** (must)

- **Check:** set up from a document naming two destinations, with an athlete carrying a birth date,
  an email, a phone, notes, and a chip, a meet and an event carrying notes, and an extra field of
  Registration's own on an athlete. Hand off.
- **Pass:** no handoff contains a `publish` array, a token, any athlete `email`, `phone`, `notes`,
  `birth_date`, or `chip`, the meet's or an event's `notes`, or Registration's extra field. The
  athlete carries the age you placed them by, and the document lists only the athletes and teams
  its entries name: a scratched athlete with no other entry is not in it.

**OMX-C66 — `meet.origin` is honest** (must)

- **Check:** hand off a meet built from a registration document, and one the timer built from nothing.
- **Pass:** `omx.generator.id` is your system id. The first meet's `origin` is Registration's
  `{ id, name }` and the second's is your own. The first carries your own ids in `id` and
  Registration's id unchanged in `external_ids`, under Registration's system id, on the meet and
  every event, team, and athlete that came from Registration, and on nothing else (OMX-R134,
  OMX-R136): no entry carries `external_ids`. Neither lists your own system id in `external_ids`
  (OMX-C106).

**OMX-C67 — Publishes stored values** (must)

- **Check:** hand off a race whose event displays hundredths, with two runners whose stored times
  differ only in thousandths.
- **Pass:** `result.value` is the stored thousandths value, `display` is the hundredths string, and
  the two hold different places. A tie is two entries with the same place, decided at stored value;
  there is no `tied` flag and no `precision`.

**OMX-C68 — Does not re-encode statuses** (must)

- **Check:** hand off a DQ that has a recorded time, a DNS, a thrower with three fouls, a finisher
  placed by the finish order with no time, and a decathlete who abandoned after seven events.
- **Pass:** `dq` with `result` populated and no place; `dns` and `nr` with no `result` and no
  place; the finisher with its `overall_place`, no status, and no `result` key; every entry with
  exactly one of `status` and `overall_place` (OMX-R177); the decathlete's own entry `dnf` with no place,
  and their seven component entries standing. `dnf` appears only in a race or on a multi-event's
  own entry, and `fs` only in a timed track event. No invented status codes anywhere in the
  document.

**OMX-C69 — Omits the unknown, never writes null** (must)

- **Check:** hand off a meet with an unlaned race, a finisher placed with no time, and a field event.
- **Pass:** no key in the document has the value `null`. The unlaned entries have no `lane` key,
  and the finisher carries its `overall_place`, no `status`, and no `result` key (OMX-R177).

**OMX-C70 — Gun and net times stay separate** (must)

- **Check:** hand off a road race with a start mat, placed overall by gun time and in age groups by
  net time.
- **Pass:** every finisher's `result` is the gun time and `net_result` the net time, and the round
  carries `place_by` with `overall: "gun"` and `groups: "net"`. Gender places follow the same clock
  as overall places.

**OMX-C71 — Carries what it knows about how the result came to be** (should)

- **Check:** hand off a prelims-and-finals track event, a hand-timed race, a horizontal jump, and
  a high jump.
- **Pass:** each prelim entry that advanced carries `qualified` as `Q`, `q`, `qD`, `qR`, or `qJ`, the
  letter its result prints; every round carries `timing`; each race heat's wind is on the heat as
  `wind_mps`, and a field entry's `wind_mps` is the wind of the mark that counted; each horizontal
  jump attempt in `field_series` carries `outcome` from `o`, `x`, `-`, `r`, its `mark` when valid,
  and its own `wind_mps` where one was read; each high jump attempt carries the bar height as `mark`
  whether it was cleared or missed. A product that does not record one of these omits it rather
  than guessing.

**OMX-C72 — One entry per round** (must)

- **Check:** hand off a track event with a prelim and a final.
- **Pass:** an athlete who ran both appears as two entries with two ids, `round: 1` and `round: 2`,
  and neither carries `external_ids`: a Publisher finds both through the athlete and the event. The
  prelim entry carries `qualified`; the final carries the place. No entry is ever overwritten by a
  later round.

**OMX-C73 — Multi-events come back at both levels** (must)

- **Check:** read a document that lists a heptathlon with no component events, score it, and hand
  it off in a `full` document.
- **Pass:** the document lists every component event actually contested, each with
  `multi_event_id`, built by your rules (the spec's table is the default). Each athlete's
  multi-event entry carries the points total as `result` in `pt` and `overall_place`; each of their
  component entries carries `result` and `multi_points`. The component events, which you built,
  carry no Registration id.

**OMX-C74 — Every handoff is ordered** (must)

- **Check:** hand off a meet twice with no change in between. Change one result and hand off. Delete
  one entry and hand off. Set the computer's clock back an hour, change a result, and hand off.
- **Pass:** every document carries `omx.updated_at` with milliseconds. The first two are equal and
  every later one is strictly later than the one before, including after the clock change.

**OMX-C75 — Offers a cadence, preselecting the preference** (must)

- **Check:** add destinations whose `deliver` is `final`, `progressive`, and `live`, and one whose
  `content` includes `start_lists`.
- **Pass:** the timer is offered when handoffs happen, with each destination's `deliver` and
  `content` preselected and overridable. A `final` destination receives `full` documents only. The
  format sets no cadence beyond that; you do.

**OMX-C76 — Destinations are independent** (must)

- **Check:** make one destination return `401` for a whole meet.
- **Pass:** the other keeps receiving documents, and the timer is shown per-destination status
  naming the failing one.

**OMX-C77 — A timer can add a destination** (must)

- **Check:** with no incoming document at all, add a destination by hand.
- **Pass:** it receives handoffs. Without this, Publisher-only products are unreachable.

**OMX-C79 — Retries only what can succeed** (must)

- **Check:** point a destination at a test endpoint that answers `503`, then `429` with
  `Retry-After: 30`, then `400`, `401`, `404`, `409`, `410`, `413`, and `422` in turn. While a retry
  is pending, change a result.
- **Pass:** the `503` is retried with backoff and the `429` no sooner than 30 seconds. The others
  are not retried and are shown to the timer against that destination, with any `errors` the body
  carried. The retry that finally goes out is built when it is sent, so it carries the changed
  result (OMX-C116).

**OMX-C85 — Accepts a destination card** (must)

- **Check:** paste a destination card into a meet, then open the same card as a file.
- **Pass:** each adds the destination as one the timer added by hand, approved by the act of adding
  it, with the card's `deliver` and `content` preselected, and the meet's handoffs reach it.
  Pasting the same card again changes that destination rather than adding a second. A card whose
  `url` is `http://` is refused.
- **Fixture:** [`c85-destination-card.omx-destination.json`](/omx/fixtures/c85-destination-card.omx-destination.json)

**OMX-C86 — Marks the final** (must)

- **Check:** hand off a prelims-and-finals event before the final runs, and again after it.
- **Pass:** the final round carries `final: true` both times, `upcoming` before it runs, and no other
  round does; a single-round event's round carries it too. Before the final is run, the prelim
  entries' `overall_place` is their place across the prelims.

**OMX-C87 — Every handoff passes the strict schema** (must)

- **Check:** capture a `full` and a `partial` handoff and validate each against
  [`results-document-v1`](/omx/schemas/results-document-v1.schema.json), then run
  `node omx-check.mjs` on it. Note each request's `Content-Encoding`.
- **Pass:** zero errors from both. Each body is the document itself, sent uncompressed or with
  `Content-Encoding: gzip`, and nothing else.

**OMX-C94 — Adaptive events stay separate** (must)

- **Check:** read a document with a women's shot put and a women's shot put carrying
  `adaptive: { "mode": "seated" }`.
- **Pass:** two events. If your product cannot hold an adaptive event, the operator is told and the
  seated event is not imported; it is never merged into the standard shot put.

**OMX-C96 — Splits say where they were taken** (must)

- **Check:** hand off a road race with two split mats, and a relay with leg splits.
- **Pass:** the race's event lists its `markers`, and every split names a `marker_id` from that
  list; each relay leg split names its `leg`. Every split carries one number, `time_ms`, elapsed from
  the same start as the entry's `result`: a relay leg's split is the running clock when that leg
  finished. A runner who missed a mat has no split there, and their other splits still name their
  markers.

**OMX-C97 — Writes gender as stated** (must)

- **Check:** hand off a road race open to everyone, a mixed relay, an athlete who did not state a
  gender, a non-binary athlete Registration entered in a boys' race, and a co-ed school team scored
  separately for boys and girls.
- **Pass:** the race and the relay are both `C`. The athlete with no gender carries no `gender`
  and no `gender_place`, and nothing guessed one. The non-binary athlete keeps the entry
  Registration gave them and is placed within their own gender. The boys' and girls' scores are
  separate `team_scores` blocks, each naming its `scope.gender`; standings that count every gender
  together name `C`, and no block leaves `scope.gender` out.

**OMX-C115 — A partial document carries whole rounds** (must)

- **Check:** while a meet runs, capture a `partial` handoff that carries one event's round, and one
  that carries a decathlon component's round.
- **Pass:** each round is whole: its status, timing, heats, and every entry in it. Each carries the
  round's event with only that round, every athlete, team, division, group, and wave its entries
  name, the event-scope team scores of its events, and every division- and meet-scope team score
  whole, each block with the same `id` it had in the last document. The component's
  document also carries the decathlon and the decathlon's round, because the standing changes with
  every component. No `partial` document is used to remove anything.

**OMX-C116 — One send at a time, each built when it is sent** (must)

- **Check:** for one destination, change three rounds in quick succession while the endpoint is slow,
  make the endpoint fail one send, then change a fourth round. Separately, hand off from a second
  copy of the meet (a laptop swapped mid-meet), and remove an athlete.
- **Pass:** at most one document is in flight to a destination at a time. Every send, a retry
  included, is built at the moment it goes out and carries every round changed since the
  Publisher last accepted a document, so the failed send's rounds arrive in the next. The first
  document any copy of the meet sends to a destination is `full`, and so is every document after
  anything is removed: an event, round, team, athlete, division, group, wave, or team score block.

**OMX-C122 — Sends the whole meet when asked** (must)

- **Check:** have a `progressive` destination answer the next `partial` with `412`, then answer `200`.
  Separately, have it answer a status the response table does not list, such as `418`, and then
  `599`.
- **Pass:** after the `412` the next document, sent at once, is `full`, and later ones are `partial`
  again. The `418` is not retried and the timer is told, as for `400`; the `599` is retried with
  backoff, as for `5xx` (OMX-R180).

**OMX-C117 — Entry statuses fit their round** (must)

- **Check:** hand off a meet with an `upcoming` round, a `started` one, a finished one
  (`unofficial`, `under_review`, or `official`), and a `cancelled` one.
- **Pass:** an `upcoming` round holds only `e` entries, and only for a destination that asked for
  start lists. A `started` round holds `e`, `s`, placed entries (their places so far), and `dnf`,
  `dns`, `dq`, `fs`, `nr`. A finished round holds placed entries and `dnf`, `dns`, `dq`, `fs`, `nr`
  only. No entry carries both a status and an `overall_place`, and none carries neither. A `cancelled` round holds no entries. No results entry is
  `ci`, `scr`, or `wait`: a scratched or wait-listed athlete has no results entry. `omx-check.mjs`
  checks this.

**OMX-C118 — Heat sheets only for a destination that asks** (must)

- **Check:** add two destinations, one whose `content` includes `start_lists` and one without, then
  seed a round that has not run.
- **Pass:** the first receives the round with an `e` entry for each athlete, carrying `heat`,
  `lane`, `seed`, and `wave_id` where it has them. The second receives the round, `upcoming`, with
  no entries.

**OMX-C119 — Writes a cancelled round** (must)

- **Check:** cancel an event after its heats are drawn, and hand off.
- **Pass:** the event's round is `cancelled`, with no entries, and the event stays listed: in the
  next `full` document and in a `partial` one carrying that round.

These tests are dedicated to the public domain under [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/): copy them into your own test suite.
