# SESSION_ATTENDANCE Use Cases & Worked Examples

Companion to the [Vision](vision.md). This doc pins down the *behavior* of the two independent axes — **resolution** (who wins the attendance status) and **authorization** (who may write a record) — through concrete, replayable scenarios. It is the reference the PRD's acceptance criteria formalize into a truth table and the TRD implements. Nothing here overrides the vision's decisions; it makes them observable.

## The two axes

| Axis | Governed by | Rule |
|---|---|---|
| **Who wins the attendance status** | Precedence (`dec-01`) | Coordinator > RM > Zoom > QR > Join-click. The highest **active** (non-undone) record decides, present **or** absent. |
| **Who may edit a record** | Ownership (`dec-03`) | An actor adds/edits/undoes **only the record they created**. Editing another actor's record is blocked. |
| **Which registrants an RM may mark** | Assignment (`dec-07`) | An RM may mark **only registrants assigned to them**. |

The consequence: a coordinator never *edits* an RM's mark — they **add their own**, which outranks it. Override = add + recompute. Blocking is purely "is this your record, and (for RMs) your registrant?"

## Precedence ladder

| Rank | Source | Can record | Notes |
|---|---|---|---|
| **5 — highest** | Coordinator (manual) | present **or** absent | always wins |
| **4** | RM (manual) | present **or** absent | beats all automated; scoped to assigned registrants |
| **3** | Zoom / provider | present **or** absent | absent = provider absentee list |
| **2** | QR scan | present only | deferred capture (`dec-04`) |
| **1 — lowest** | Join-click | present only | click ≠ confirmed attendance |

> ⚠️ Ordering **within** the automated tier (Zoom > QR > Join-click) is provisional — this is open question `oq-01`. Coordinator > RM > automated is locked.

## The one resolution rule

```
resolveAttendanceStatus(registration, session) =
    value of the event from the highest-rank source
    that currently has an active (non-undone) record;
    UNKNOWN if no active record exists.
```

Full enumeration is 3⁵ = 243 combinations (each source ∈ {present, absent, none}); every one reduces to the rule above. Representative rows:

| Coord(5) | RM(4) | Zoom(3) | QR(2) | Join(1) | **Attendance status** | Why |
|:---:|:---:|:---:|:---:|:---:|:---|:---|
| — | — | — | — | P | **PRESENT** | only join-click; rank 1 decides |
| — | — | P | — | P | **PRESENT** | Zoom(3) outranks join(1) |
| — | — | A | — | P | **ABSENT** | Zoom absent beats join present |
| — | A | P | P | P | **ABSENT** | RM(4) beats all automated |
| P | A | A | — | — | **PRESENT** | coordinator beats RM + Zoom |
| A | P | P | P | P | **ABSENT** | coordinator absent beats everything |
| — | — | — | — | — | **UNKNOWN** | no active record |

Key property (`dec-01`): a higher source's **absent** overrides a lower source's **present** and vice-versa — direction never matters, only rank.

## Live timeline — registrant "Ravi", session S1

Each row is one event appended to `attendance_events`; the winner after that event is **bold**.

| # | Time | Action | Active records (rank) | Winner → **Attendance status** |
|---|---|---|---|---|
| 1 | 10:00 | Ravi clicks join link | Join(1)=P | **Join** → **PRESENT** |
| 2 | 10:05 | Zoom: Ravi joined | Join(1)=P, **Zoom(3)=P** | Zoom → **PRESENT** |
| 3 | 11:00 | RM Meera (Ravi's assigned RM) marks **absent** | …, **RM(4)=A** | RM → **ABSENT** |
| 4 | 11:15 | Coordinator Arun marks **present** | …, **Coord(5)=P** | Coordinator → **PRESENT** |
| 5 | 11:20 | RM Meera tries to undo **Arun's** record | 🚫 blocked — not her record | → **PRESENT** (unchanged) |
| 6 | 11:22 | A different RM tries to mark Ravi (not assigned) | 🚫 blocked — not his registrant (`dec-07`) | → **PRESENT** (unchanged) |
| 7 | 11:25 | Coordinator Arun undoes **his own** record | Join(1)=P, Zoom(3)=P, RM(4)=A | recompute → RM → **ABSENT** |
| 8 | 11:30 | RM Meera undoes **her own** record | Join(1)=P, **Zoom(3)=P** | recompute → Zoom → **PRESENT** |
| 9 | 11:35 | Undo Zoom + join-click (each by its own source) | *(none active)* | → **UNKNOWN** |

This exercises present, absent, override, the two block cases, undo-with-fallback, and the empty state.

## Edit-rights matrix

| Actor | Own record | Another actor's record | Registrant not assigned to them |
|---|:---:|:---:|:---:|
| Coordinator | ✅ edit/undo | 🚫 blocked | ✅ (coordinators aren't registrant-scoped) |
| RM | ✅ edit/undo | 🚫 blocked | 🚫 blocked (`dec-07`) |
| System (Zoom/QR/Join) | ✅ its own event | 🚫 blocked | — (automated, not scoped) |

## Behaviors the PRD must formalize

- **Absent semantics (`oq-02`).** Is "absent" an explicit event, or the absence of a present event? These worked examples assume **explicit absent events** carried by a source, distinct from "no record" (which yields UNKNOWN).
- **Undo fallback (`oq-03`).** Undoing the current winner **recomputes over remaining active records** and falls to the next-ranked one (rows 7→8), rather than jumping to UNKNOWN.
- **Join-click ranking (`oq-01`).** The automated-tier order used above is provisional.
- **Ownership identity.** Enforcing "edit only your own record" and `dec-07` needs a stable `performedBy` (exists) and a real registration→RM **id** (today only `rmName` string — `asm-02`).
