ADR-0003: Result import does not transition UNFIT_FOR_SERVICE

Status

Accepted

Date

2026-04-24

Deciders

[email protected]

Related

ADR-0001, ADR-0002, Feature #473, US #475

1. Context

A RaceNumber can be flagged UNFIT_FOR_SERVICE by an admin when physical inspection reveals damage (broken chip, delaminated tag, cracked substrate, etc.). The flag is a location-agnostic assertion about fitness — the number may still be held by a person, may still be in stock, may still be registered for a future event.

Occasionally, a result row arrives for a number that is currently flagged UNFIT. Possible real-world causes:

  1. Used despite the flag. The participant was handed the broken number at the desk and ran with it anyway (maybe nothing else was available; maybe the damage was cosmetic). The result is real.

  2. Timing picked up a damaged tag. The tag is technically working enough to get captured as the participant crosses the mat, even though the number itself is flagged for retirement. The result is real, but the fitness verdict stands.

  3. Premature flag. Someone flagged the number before the event, and it turned out to be fine. The result is real, and the flag should be cleared.

  4. Wrong number in the CSV. Timing recorded the wrong bib. The result is not for this number; it’s a data error.

The system cannot distinguish these four cases from the data. Only an operator can.

2. Decision

When recordResultImport encounters a RaceNumber in state UNFIT_FOR_SERVICE:

  1. Do not transition the state. The number stays UNFIT_FOR_SERVICE.

  2. Stamp last_used = eventDate (subject to the monotone rule in ADR-0004).

  3. Write a RESULT log entry with the note UNFIT_FOR_SERVICE at time of result import.

  4. Emit a WARN to the service log so the anomaly is visible in operational dashboards.

Admin must resolve the situation manually — by clearing the flag (if the number was fine), by running the return workflow with a disposal note (if the flag stands), or by correcting the data (if the wrong bib was recorded).

3. Consequences

3.1. Positive

  • The fitness verdict is preserved. A damaged number does not become "fit" because it crossed a finish line — a result row is evidence of activity, not of integrity.

  • Admin retains agency. The four real-world causes have four different resolutions, and the system defers to a human rather than guessing.

  • The WARN creates a visible signal that something about this number needs attention; it cannot be silently overwritten by the next import.

  • The last_used stamp keeps pick-list and timing-feed queries (WS1b) honest even in the anomaly path — the number was demonstrably used, so it should not resurface as "stale in stock".

3.2. Negative

  • A number flagged prematurely stays flagged until an admin clears it. Minor operational overhead in the "false positive flag" case.

  • The anomaly surfaces after the fact. If UNFIT numbers appear on results with regularity, it’s either a desk-discipline problem or a flagging-criteria problem, and the signal is downstream of where the decision was made.

3.3. Neutral

  • The inverse case — a number that should have been flagged but wasn’t, appearing normally in results — is invisible to this rule. That’s by design: if nobody flagged it, the system has no reason to treat it specially.

4. Alternatives Considered

4.1. Alternative A: Auto-clear UNFIT on result encounter

Treat a result as re-fitness — if it works well enough to race, it’s fit. Rejected because "used" and "fit" are different properties. A participant finishing on a partly-broken board generates a valid result but should not clear the admin’s decision to retire the number.

4.2. Alternative B: Transition to IN_USE but keep a parallel "flagged" flag

Add a separate boolean on RaceNumber (flagged_unfit) that’s orthogonal to state. Rejected because it duplicates the state machine with a shadow attribute, and every caller that cares about fitness would need to check both.

4.3. Alternative C: Block the import for UNFIT numbers

Refuse to write the result at all. Rejected: this punishes every downstream consumer (leaderboards, points, analytics) for an edge case. The right scope of the intervention is the number itself, not the result.

5. References

  • Design journal: design-journal/2026-03/number-tag-management.adoc (session 3 — UNFIT semantics)

  • Code: admin-service/src/main/java/za/co/idealogic/event/admin/service/RaceNumberAssignmentServiceEx.java — method recordResultImport, UNFIT branch

  • ADO: Feature #473, US #475