ADR-0006: Auto-assign prefers latest-used only; UNFIT-latest forces manual assignment

Status

Accepted

Date

2026-04-24

Deciders

[email protected]

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:

  1. Participants typically expect the same physical number across a series. WS1b added a last_used stamp 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.

  2. A participant’s number history contains failed or abandoned numbers. Numbers can be LOST (log entry only; state preserved), physically returned after breakage, or flagged UNFIT_FOR_SERVICE. The database cannot reliably distinguish "old but fine" from "old because something went wrong".

  3. 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:

  1. Latest used, if usable. If the participant has a previous number in the matching subtype pool that is either IN_STOCK or ISSUED to this same participant, use it. last_used resolves ties; the most recent win.

  2. Stock pool fallback. Otherwise, pull next-available from the subtype’s IN_STOCK pool (ADR-0001 / existing pullNextAvailable primitive).

  3. 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 with number_id = NULL until an explicit assignment arrives — admin UI, CSV-supplied NUMBER / NUMBER_ID on 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.

3.3. Neutral

  • The "same number across the series" experience is only a preference, not a guarantee. A participant whose previous number is held by someone else (theoretical — should not happen under well-behaved state rules) would get a pool number.

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. References

  • Design journal: design-journal/2026-03/number-tag-management.adoc (Session 9 — cascade redesigned)

  • 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.javapullNextAvailable, autoAssignNumbers, autoAssignOneEp

  • ADO: Feature #473, US #505, Feature #403, and the sweep US created from that journal entry