ADR-0002: Result import accepts IN_STOCK as implicit issuance
| Status |
Accepted |
| Date |
2026-04-24 |
| Deciders | |
| Related |
1. Context
The formal number lifecycle is MANUFACTURED → IN_STOCK → ISSUED → IN_USE. In the "happy path" a number is issued to a participant at registration (triggering IN_STOCK → ISSUED) and races under that person (triggering ISSUED → IN_USE on the first result-import row).
Reality is messier. The registration desk is a busy place, and the formal issuance step is not always executed:
-
Participant arrives and reports their regular board is broken, lost, or left at home.
-
Registration staff grabs a number off the stock rack and hands it to them.
-
No "lost number" workflow is run against the old number; no "issue replacement" action is recorded on the new one.
-
The event proceeds; the number races; the timing system picks up the result.
-
The CSV result import is the first time the database learns that the new number was used.
A naive implementation of recordResultImport would see a number in state IN_STOCK on a result row, classify this as an anomaly, emit a WARN, and leave the state alone. That’s wrong on two counts:
-
It treats a legitimate (if informal) operational pattern as a system error, polluting the anomaly list.
-
It leaves the race_number in
IN_STOCKwithperson_id = NULLeven though the number has clearly been used — making every downstream query wrong.
2. Decision
When recordResultImport encounters a RaceNumber in state IN_STOCK:
-
Transition the state directly
IN_STOCK → IN_USE(skippingISSUED). -
Attach
RaceNumber.personfromEventParticipant.person. -
Stamp
last_used = eventDate. -
Write a
RESULTlog entry torace_number_state_logwith the noteimplicit issuance via result import (no prior ASSIGNED log). -
Do not emit a WARN. This is a documented path, not an anomaly.
The note on the log entry is load-bearing: it preserves the provenance that no prior ASSIGNED log exists for this number, so an auditor reading the log can distinguish "this number followed the formal process" from "this number was implicitly issued".
3. Consequences
3.1. Positive
-
Ops can do their job at the registration desk without fighting the system. The software catches up to reality instead of forcing reality to catch up to the software.
-
The anomaly list stays useful — only genuinely unexpected states (MANUFACTURED with a result, DESTROYED with a result) surface there.
-
The audit trail is still complete. The "implicit issuance" note is greppable and queryable; dashboards can count implicit vs. formal issuance as a proxy for desk discipline.
-
Downstream state-based queries (
IN_USE = "currently racing",IN_STOCK = "available") give correct answers after the first result row lands.
3.2. Negative
-
Mid-event status boards (which read
RaceNumber.state) cannot distinguish "this number has been implicitly issued" from "this number is still in stock" until the first result arrives. A number handed to a participant at 08:00 still showsIN_STOCKin a stock-count query until results flow later in the day. -
Ops has no incentive to run the formal ISSUED path. Over time this might erode data quality around in-flight assignments, because "the system will sort it out from the results" becomes the default.
-
Analytics that care about issuance timing (as opposed to usage timing) need to query
race_number_state_logforASSIGNED/IMPORT_EProws; therace_numbertable alone can’t answer "when was this number issued?".
4. Alternatives Considered
4.1. Alternative A: Reject — force the formal process
Treat IN_STOCK at result-import time as an error. The admin would have to go back, run the lost/replace workflow retroactively, then re-run the import. Rejected: this pushes complexity onto the busiest people in the system. Retroactive data entry from a registration desk that has already moved on is a recipe for worse data, not better.
4.2. Alternative B: Transition via ISSUED (two-step, two log entries)
Emit two log entries — IN_STOCK → ISSUED (reason ASSIGNED) and ISSUED → IN_USE (reason RESULT) — so the log looks the same as the happy path. Rejected: it lies about the history. No operator issued the number; the system inferred it. A forensic audit should be able to tell those cases apart. The single RESULT entry with the "implicit issuance" note is the honest record.
4.3. Alternative C: Flag the number as IN_USE but leave person null
Promote state but skip the person attachment. Rejected: the result row does carry EP context, and the EP does have a person. Leaving person_id null would require special handling in every downstream query ("is the number IN_USE with a person, or without?") — the complexity compounds forever.