ADR-0006: Auto-assign prefers latest-used only; UNFIT-latest forces manual assignment
| Status |
Accepted |
| Implementation |
Pending. Auto-assign pulls from stock only; see Amendments. |
| Date |
2026-04-24 |
| Deciders | |
| Related |
ADR-0001, ADR-0002, ADR-0005, Feature #473, US #505, Feature #403 (Signal & Sweep) |
1. Context
The RaceNumber lifecycle work (Feature #473) introduced a DETACH-and-reconcile model (ADR-0005): manual reassignment on one EP triggers a reset of the participant’s same-type future EPs, letting each event’s auto-assign reconcile later. Reconciliation depends on auto-assign making a sensible choice per EP — specifically, carrying the participant’s "same number across the series" continuity wherever that is still correct, without assuming too much when things are in flux.
Three real-world signals complicate this:
-
Participants typically expect the same physical number across a series. WS1b added a
last_usedstamp and a pick-list endpoint to support manual selection at registration with history ordered most-recent-first. Auto-assign should not undermine that by pulling random stock. -
A participant’s number history contains failed or abandoned numbers. Numbers can be
LOST(log entry only; state preserved), physically returned after breakage, or flaggedUNFIT_FOR_SERVICE. The database cannot reliably distinguish "old but fine" from "old because something went wrong". -
The most recent swap is typically the reason a new auto-assign is running. If an admin just flagged the latest number as UNFIT, auto-assign re-issuing an older number would silently undo the admin’s intent — the older number may itself be broken, lost, or missing, and nobody has signalled that.
Without a clear rule, auto-assign tends toward "pick any plausible candidate" — which is user-hostile in the failure case: ghost numbers resurface, damaged numbers get re-issued, and the human who should have made the call is not prompted.
2. Decision
Auto-assign applies the following preference, in order:
-
Latest used, if usable. If the participant has a previous number in the matching subtype pool that is either
IN_STOCKorISSUEDto this same participant, use it.last_usedresolves ties; the most recent win. -
Stock pool fallback. Otherwise, pull next-available from the subtype’s
IN_STOCKpool (ADR-0001 / existingpullNextAvailableprimitive). -
No auto-assign when latest is
UNFIT_FOR_SERVICE. If the participant’s most recent number (in the matching subtype) is flagged UNFIT at the time auto-assign is considering it, auto-assign does not run for this EP. The EP stays withnumber_id = NULLuntil an explicit assignment arrives — admin UI, CSV-suppliedNUMBER/NUMBER_IDon EP import, or result-import number-change.
The rule is deliberately narrow: auto-assign never falls through from "latest unusable" to "second-latest", "third-latest", etc. There is only one candidate from history; if it is not usable, the pool takes over — or, in the UNFIT case, a human must take over.
3. Consequences
3.1. Positive
-
Series continuity is preserved in the common case. A participant who carried number 42 through three events continues to get 42 on event four unless something deliberately changed.
-
Admin intent on UNFIT is never silently reversed. Flagging a number UNFIT is a terminal decision for auto-assign purposes — the system will not re-issue a different number from history without a human.
-
The DETACH-and-reconcile model (ADR-0005) stays honest: the reset signal really does reset things, and reconciliation uses current rules rather than inferring from deep history.
-
Ghost-number resurrection is eliminated. A number that was physically lost two events ago does not reappear on auto-assign because the rule never reaches it.
3.2. Negative
-
NULL EPs that cannot be auto-assigned (UNFIT-latest case) are visible as a validation gap at event finalisation until someone acts. The admin UI surfaces this as a validation error; operationally this is the intended escalation point.
-
Customers whose latest number is flagged UNFIT experience the "no number yet" state until manual assignment completes. Customer notification on UNFIT flagging is a separate requirement (see the communication design journal) so that the customer is informed, not surprised at registration.
-
Reassignment workflows must explicitly pick the number for participants whose latest was UNFIT-flagged. No "the system will sort it" path.
4. Alternatives Considered
4.1. Alternative A: Walk the full history, falling through to older numbers
If latest is UNFIT, try second-latest. If that was LOST, try third-latest. Continue until a usable candidate is found, else fall through to the pool. Rejected because older numbers in the person’s history may themselves be broken, lost, or retired — the system has no reliable signal and a naïve walk would resurrect ghosts. The rule would need to re-implement "is this old number actually safe to issue" logic that does not exist.
4.2. Alternative B: UNFIT-latest falls through to the pool instead of blocking
If the latest is UNFIT, quietly pick a fresh number from the pool — participant gets a new number silently. Rejected because it breaks the series-continuity expectation in a way the participant will not have been told about. An admin who flagged a number UNFIT likely needs to communicate with the customer (see the UNFIT notification requirement). A silent pool pick cuts the admin out of the loop and denies them the chance to coordinate.
4.3. Alternative C: Always pool; no history preference at all
Auto-assign ignores history entirely and always pulls next-available. Simpler but directly contradicts ADR-0002's "same number across the series" intent and defeats the WS1b last_used stamping work.
5. Amendments
5.1. 2026-10-15 — Preference one includes IN_USE; implementation status
The decision’s first preference reads "a previous number in the matching subtype pool that is either IN_STOCK or ISSUED to this same participant". IN_USE was omitted, and IN_USE is the common carry-over state: a board raced with once stays IN_USE across the series. Read literally, the rule would pull fresh stock for every returning participant. Preference one is therefore: the participant’s latest number of that subtype that is IN_STOCK, or ISSUED or IN_USE with this participant as holder. UNFIT_FOR_SERVICE still blocks, and DESTROYED and another person’s holding are never candidates.
Implementation status as of this amendment: autoAssignOneEp applies none of the preferences and pulls next-available from stock. The reconciliation sweep that is to carry the rule is designed in design-journal/2026-10/auto-number-assignment.adoc. Until it lands, series continuity comes from the carry-over procedure in the Number & Tag Runbook, which ranks a person’s boards and then leaves an entry whose latest board is UNFIT_FOR_SERVICE for manual assignment, as this decision requires, rather than carrying an older board forward.
When the rule declines an entry because the latest board is unfit, that is the moment the participant should be told a number will be assigned by hand; the possession rules in RaceNumber Lifecycle, Possession and the holder govern the notice sent at flag time, and this is its counterpart at assignment time.
6. References
-
Design journal:
design-journal/2026-03/number-tag-management.adoc(Session 9 — cascade redesigned) -
Design journal:
design-journal/2026-10/auto-number-assignment.adoc(the reactor that implements this rule) -
Design journal:
design-journal/2026-04/async-signal-sweep.adoc(future-EP auto-assign sweep — honours this ADR) -
Code:
admin-service/src/main/java/za/co/idealogic/event/admin/service/RaceNumberPoolService.java—pullNextAvailable,autoAssignNumbers,autoAssignOneEp -
ADO: Feature #473, US #505, Feature #403, and the sweep US created from that journal entry