# SESSION_ATTENDANCE Vision

This is the product vision for the **Session Attendance** module of the infinitheism-backend product. Once a seeker is registered for a program session, the platform must answer one deceptively simple question for every registrant: *did they actually attend?* Today that answer arrives from several places at once — a click on a join link, a Zoom report, a Relationship Manager ticking a name, a coordinator's roll call — and the existing model keeps all of them in an append-only log but resolves the attendance status on a naive "first event wins" basis. This vision reframes attendance as a **single authoritative attendance status per registrant per session, resolved by a fixed trust hierarchy**, where a human authority always outranks an automated signal, any mark can be undone, and a registrant can be explicitly marked absent. It is read by the delivery team building it, the coordinators and RMs who operate it, and whoever signs off on what "done" means. The *how* will live in a downstream TRD; this doc fixes *what* and *why*. The module short code used in IDs below is **SATT**.

Related code that already exists and this module builds on: [program-attendance.entity.ts](../../../src/common/entities/program-attendance.entity.ts) (the `program_user_attendance` row and its `attendance_events` jsonb log), [attendance-source.enum.ts](../../../src/common/enum/attendance-source.enum.ts) (`JOIN_CLICK`, `ZOOM_WEBHOOK`, `MANUAL_ADMIN`, `QR_SCAN`), and [online-attendance.service.ts](../../../src/online-attendance/online-attendance.service.ts) (join capture, provider webhooks, admin marking, reports).

## Module cognition graph

The graph below answers one question: **what is the attendance thesis, what did we decide about resolving conflicting sources, and what would invalidate it?** Read it top-down: the users who produce or trust attendance, the goals they need met, the requirements that serve those goals, the five capture flows and the resolution flow in the middle, the decisions that govern them, and — around the edges — the assumptions the thesis rests on, the risks, and the questions still open.

<details><summary>Graph: What is the attendance thesis, what did we decide about conflicting sources, and what would invalidate it?</summary>

```items
---
id: satt-vision-cognition
title: SESSION_ATTENDANCE Vision cognition
default_open_depth: 1
default_color_by: kind
color_palette_source: .daksh/color-palette.json
width: 95vw
---
Users:
  - user-01 :: Coordinator | kind: user | role: Session Coordinator | audience: client | status: placeholder | summary: Highest-authority operator; marks, undoes, and overrides attendance for a session. | spec: [§Target users](vision.md#target-users)
  - user-02 :: Relationship Manager | kind: user | role: Relationship Manager (RM) | audience: client | status: placeholder | summary: Marks attendance for their assigned seekers; ranks below coordinator, above automated sources. | spec: [§Target users](vision.md#target-users)
  - user-03 :: Registrant | kind: user | role: Seeker / Registrant | audience: end_customer | status: placeholder | summary: The attendee whose presence is captured passively via join-click, provider, or QR. | spec: [§Target users](vision.md#target-users)
  - user-04 :: Platform Operator | kind: user | role: Platform Operator | audience: delivery | status: placeholder | summary: Owns the source registry and precedence map; accountable for the extensibility of sources. | spec: [§Target users](vision.md#target-users)
Goals:
  - goal-01 :: One authoritative attendance status | kind: goal | success_measure: Every (registrant, session) resolves to exactly one present/absent/unknown attendance status, deterministic from its recorded events. | summary: A single trustworthy attendance answer per registrant per session. | spec: [§Vision statement](vision.md#vision-statement)
  - goal-02 :: Trust hierarchy honored | kind: goal | success_measure: In every source-conflict combination, the highest-ranked source with a record decides the attendance status. | summary: Human authority always outranks an automated signal. | spec: [§Problem statements](vision.md#problem-statements)
  - goal-03 :: Extensible source model | kind: goal | success_measure: Adding or reordering a source changes only an enum and a precedence map, never resolution business logic. | summary: New attendance sources plug in without reworking the engine. | spec: [§Core capabilities](vision.md#core-capabilities)
  - goal-04 :: Reversible and auditable | kind: goal | success_measure: Any mark can be undone and any registrant explicitly marked absent, with every action retained in the event log. | summary: Attendance is correctable and fully traceable. | spec: [§Core capabilities](vision.md#core-capabilities)
Requirements:
  - req-01 :: Multi-source capture | kind: requirement | acceptance: Each of the five sources can record an attendance event against a registrant's session row. | summary: All five sources feed one shared append-only log. | spec: [§Core capabilities](vision.md#core-capabilities)
  - req-02 :: Hierarchy resolution | kind: requirement | acceptance: The attendance status equals the mark of the highest-ranked source that has any record, whether present or absent. | summary: Top-ranked source wins, present or absent. | spec: [§Problem statements](vision.md#problem-statements)
  - req-03 :: Owner-scoped edit/undo | kind: requirement | acceptance: An actor may add, edit, or undo only the record they created; altering another actor's record is blocked; the attendance status recomputes after every change. | summary: You edit only your own mark; blocking is by ownership, not by rank. | spec: [§Core capabilities](vision.md#core-capabilities)
  - req-04 :: Explicit mark-absent | kind: requirement | acceptance: A coordinator or RM can record an explicit absent event, distinct from "no record". | summary: Absence can be asserted, not just inferred. | spec: [§Core capabilities](vision.md#core-capabilities)
  - req-05 :: Append-only audit trail | kind: requirement | acceptance: No source event is ever mutated in place; undo appends a reversal rather than deleting. | summary: The full history of who marked what, when, survives. | spec: [§Core capabilities](vision.md#core-capabilities)
  - req-06 :: RM scoped to assigned registrants | kind: requirement | acceptance: An RM can mark only registrations assigned to them; a mark on an unassigned registration is rejected. | summary: Each registration has one RM, and only that RM may mark it. | spec: [§Problem statements](vision.md#problem-statements)
Capture Flows:
  - flow-01 :: Join-click capture | kind: flow | actor: Registrant | trigger: Registrant opens their personal join link within the join window | outcome: A JOIN_CLICK event appended with join-time client metadata | summary: Passive attendance from the seeker clicking to join. | spec: [§Core capabilities](vision.md#core-capabilities)
  - flow-02 :: Provider capture (Zoom) | kind: flow | actor: Online provider | trigger: Zoom webhook or reconciliation reports a participant | outcome: A ZOOM_WEBHOOK event appended for the matched registrant | summary: Attendance pulled from the online provider's own record. | spec: [§Core capabilities](vision.md#core-capabilities)
  - flow-03 :: RM manual mark | kind: flow | actor: Relationship Manager | trigger: RM marks a seeker present or absent | outcome: A manual RM-level event appended | summary: A Relationship Manager asserts attendance for their seekers. | spec: [§Core capabilities](vision.md#core-capabilities)
  - flow-04 :: Coordinator manual mark | kind: flow | actor: Coordinator | trigger: Coordinator marks a seeker present or absent | outcome: A manual coordinator-level (top authority) event appended | summary: The coordinator's roll call — the highest-trust manual signal. | spec: [§Core capabilities](vision.md#core-capabilities)
  - flow-05 :: QR-scan capture (deferred) | kind: flow | actor: Registrant | trigger: A QR scan at the session (not built in MVP) | outcome: A QR_SCAN event appended, treated as an external-provider-class source | summary: QR is modeled now like a provider source; capture is built later. | spec: [§Scope](vision.md#scope)
Resolution Flows:
  - flow-06 :: Resolve effective attendance status | kind: flow | actor: System | trigger: Any capture, undo, or mark-absent event | outcome: The single present/absent/unknown attendance status recomputed by precedence | summary: The engine that turns a log of events into one authoritative answer. | spec: [§Problem statements](vision.md#problem-statements)
  - flow-07 :: Undo a mark | kind: flow | actor: Coordinator or RM | trigger: An authorized user reverses a mark at or below their level | outcome: A reversal event appended; attendance status recomputed | summary: Correcting a wrong mark without erasing history. | spec: [§Core capabilities](vision.md#core-capabilities)
  - flow-08 :: Mark absent | kind: flow | actor: Coordinator or RM | trigger: A user asserts a seeker was absent | outcome: An explicit absent event appended; attendance status recomputed | summary: Recording a positive statement of absence. | spec: [§Core capabilities](vision.md#core-capabilities)
Data Models:
  - dm-01 :: Attendance record | kind: datamodel | shape: program_user_attendance row (registration_id, session_id, attendance_status, attendance_events[]) | summary: The per-registrant-per-session row that carries the resolved attendance status. | spec: [§Core capabilities](vision.md#core-capabilities)
  - dm-02 :: Attendance event | kind: datamodel | shape: { source, occurredAt, performedBy, present|absent, reversalOf? } appended to attendance_events | summary: One immutable entry in the append-only source log. | spec: [§Core capabilities](vision.md#core-capabilities)
  - dm-03 :: Source registry + precedence | kind: datamodel | shape: ordered map AttendanceSource -> rank (coordinator > RM > join/provider/QR) | summary: The single place that declares which sources exist and how they rank. | spec: [§Key decisions](vision.md#key-decisions)
Decisions:
  - dec-01 :: Top source wins, present or absent | kind: decision | alternatives: Present-always-wins (generous); manual-beats-automatic with coordinator==RM. | reversal_trigger: If a coordinator's hurried call proves less reliable than the provider record in practice. | summary: The highest-ranked source with any record sets the attendance status, in either direction. | spec: [§Key decisions](vision.md#key-decisions)
  - dec-02 :: Sources as enum + precedence map | kind: decision | alternatives: Config/DB-driven source table editable without deploy. | reversal_trigger: Operators need to add or reorder sources without a code release. | summary: Sources are a typed enum with a code-level precedence map; nothing per-source is hardcoded in the engine. | spec: [§Key decisions](vision.md#key-decisions)
  - dec-03 :: Edits are owner-scoped | kind: decision | alternatives: Hierarchy-gated undo (own level or below); any authorized user may undo any record. | reversal_trigger: Operators need a supervisor to correct a subordinate's record directly rather than by overriding. | summary: An actor adds/edits/undoes only the record they created; overriding a higher/lower source is done by adding your own record, not editing theirs; the attendance status recomputes. | spec: [§Key decisions](vision.md#key-decisions)
  - dec-04 :: QR modeled as a provider source now | kind: decision | alternatives: Exclude QR from the model until it is built. | reversal_trigger: QR turns out to need semantics unlike an external provider. | summary: QR is a first-class source in model and precedence, sitting with the automated sources; only its capture endpoint is deferred. | spec: [§Scope](vision.md#scope)
  - dec-05 :: Attendance status recomputed from the log | kind: decision | alternatives: Keep the existing "isAttended set on first event" summary flag. | reversal_trigger: Recompute cost at scale outweighs correctness benefit. | summary: The attendance status is a pure function of the event log by precedence, replacing first-event-wins. | spec: [§Key decisions](vision.md#key-decisions)
  - dec-06 :: Split manual into RM and coordinator | kind: decision | alternatives: Keep one MANUAL_ADMIN source for all manual marks. | reversal_trigger: The two manual roles collapse into one operationally. | summary: The single MANUAL_ADMIN source becomes two ranked sources so the hierarchy can distinguish them. | spec: [§Key decisions](vision.md#key-decisions)
  - dec-07 :: RM authorization is per-registration | kind: decision | alternatives: Any RM may mark any registrant; only coordinators are registrant-scoped. | reversal_trigger: RMs need to cover each other's registrants (shared pool). | summary: Marking rights for an RM are scoped to the registrations assigned to that RM. | spec: [§Key decisions](vision.md#key-decisions)
Assumptions:
  - asm-01 :: Human authority is most reliable | kind: assumption | validation_plan: Sample sessions where coordinator and provider disagree; check which was correct. | summary: A coordinator's manual call is genuinely more trustworthy than an automated signal. | spec: [§Leap-of-faith assumptions](vision.md#leap-of-faith-assumptions)
  - asm-02 :: Marker role is resolvable | kind: assumption | validation_plan: Confirm every manual mark can be attributed to a known RM or coordinator identity at mark time. | summary: The system can tell an RM from a coordinator when a mark is made, to rank it and gate undo. | spec: [§Leap-of-faith assumptions](vision.md#leap-of-faith-assumptions)
  - asm-03 :: Session-level granularity suffices | kind: assumption | validation_plan: Check whether any program needs per-occurrence attendance on recurring/shared sessions. | summary: One attendance status per session row is enough; no per-occurrence attendance is required. | spec: [§Leap-of-faith assumptions](vision.md#leap-of-faith-assumptions)
Risks:
  - risk-01 :: Migration from first-event model | kind: risk | likelihood: medium | impact: high | mitigation: Backfill/recompute existing rows under the new precedence; keep isAttended as a derived read. | phase: design | summary: Existing isAttended/first-event data conflicts with recompute-by-precedence. | spec: [§Problem statements](vision.md#problem-statements)
  - risk-02 :: Wrong hierarchy yields wrong attendance statuses | kind: risk | likelihood: low | impact: high | mitigation: Validate asm-01 on real conflict samples before locking precedence. | phase: design | summary: If the trust ranking is wrong, the authoritative attendance status is confidently wrong. | spec: [§Leap-of-faith assumptions](vision.md#leap-of-faith-assumptions)
  - risk-03 :: Extensibility overpromised | kind: risk | likelihood: medium | impact: medium | mitigation: Prove the add-a-source spike touches only enum + precedence map; isolate capture wiring behind a common source-adapter seam. | phase: design | summary: Even with a precedence map, each new source may still need bespoke capture wiring, weakening the "no code changes" claim. | spec: [§Key decisions](vision.md#key-decisions)
Open Questions:
  - oq-01 :: Where does join-click rank? | kind: openquestion | summary: Among the automated sources, is join-click above, below, or equal to provider and QR? | spec: [§Open questions](vision.md#open-questions)
  - oq-02 :: How is absent represented? | kind: openquestion | summary: Is "absent" an explicit event, or the absence of any present event — and how does that interact with precedence? | spec: [§Open questions](vision.md#open-questions)
  - oq-03 :: Undo fallback semantics | kind: openquestion | summary: When the top source's mark is undone, does the attendance status fall to the next-ranked source or to unknown? | spec: [§Open questions](vision.md#open-questions)
  - oq-04 :: Recurring/shared session granularity | kind: openquestion | summary: For same-link recurring sessions, is attendance per session row or per occurrence? | spec: [§Open questions](vision.md#open-questions)
  - oq-05 :: Existing-data migration path | kind: openquestion | summary: How are historical attendance rows recomputed or grandfathered under the new model? | spec: [§Open questions](vision.md#open-questions)

user-01 -> flow-04 | relation: experiences
user-01 -> flow-07 | relation: experiences
user-01 -> flow-08 | relation: experiences
user-02 -> flow-03 | relation: experiences
user-02 -> flow-07 | relation: experiences
user-02 -> flow-08 | relation: experiences
user-03 -> flow-01 | relation: experiences
user-03 -> flow-05 | relation: experiences
user-04 -> dm-03 | relation: owns
user-04 -> dec-02 | relation: owns
goal-01 -> goal-02 | relation: decomposes_into
goal-01 -> goal-04 | relation: decomposes_into
req-01 -> goal-01 | relation: serves
req-02 -> goal-02 | relation: serves
req-03 -> goal-04 | relation: serves
req-04 -> goal-04 | relation: serves
req-05 -> goal-04 | relation: serves
req-06 -> goal-01 | relation: serves
flow-01 -> dm-02 | relation: produces
flow-02 -> dm-02 | relation: produces
flow-03 -> dm-02 | relation: produces
flow-04 -> dm-02 | relation: produces
flow-05 -> dm-02 | relation: produces
flow-08 -> dm-02 | relation: produces
dm-02 -> flow-06 | relation: enables
dm-03 -> flow-06 | relation: enables
flow-07 -> flow-06 | relation: enables
flow-06 -> dm-01 | relation: produces
flow-06 -> goal-01 | relation: serves
dec-01 -> flow-06 | relation: governs
dec-05 -> flow-06 | relation: governs
dec-02 -> dm-03 | relation: governs
dec-02 -> goal-03 | relation: enables
dec-03 -> flow-07 | relation: governs
dec-04 -> flow-05 | relation: governs
dec-06 -> flow-03 | relation: governs
dec-06 -> flow-04 | relation: governs
dec-07 -> flow-03 | relation: governs
asm-01 -> dec-01 | relation: enables
asm-02 -> dec-03 | relation: enables
asm-03 -> goal-01 | relation: enables
risk-01 -> goal-01 | relation: threatens
risk-02 -> goal-02 | relation: threatens
oq-01 -> flow-06 | relation: threatens
oq-02 -> flow-06 | relation: threatens
oq-03 -> flow-07 | relation: threatens
oq-04 -> goal-01 | relation: threatens
oq-05 -> goal-01 | relation: threatens
```

</details>

## Vision statement

**Every registrant of every program session resolves to one authoritative attendance status — present, absent, or unknown — computed deterministically from all recorded sources, where the highest-trust source that has a record always decides.**

## Target users

| User | Role in attendance | What changes for them |
|---|---|---|
| **Coordinator** | Highest authority. Marks present/absent for any registrant. | Their call is final for a session; nothing an automated source or an RM records outranks them at resolution. They edit only their own record — but since it ranks highest, adding it *is* the override. |
| **Relationship Manager (RM)** | Marks attendance for **only their assigned seekers**. Ranks below coordinator, above automated sources. | Their manual mark beats a Zoom/join/QR signal; a coordinator overrides not by editing the RM's mark but by adding a higher-ranked one. An RM can edit/undo only the records they created. |
| **Registrant (Seeker)** | Subject of attendance. Produces passive signals (join-click, provider, QR). | Their presence is captured automatically; a wrong automated read can be corrected by a human without disputing the log. |
| **Platform Operator** | Owns the source registry and precedence map. | Adds or reorders attendance sources through a single typed definition instead of touching resolution logic. |

## Problem statements

1. **Conflicting sources, no authority.** A registrant can have a Zoom "present", a join-click, and a coordinator's "absent" all at once. The current model resolves this as *first event wins*, which is arbitrary — it can let an automated signal outrank a human's deliberate call. **Constraint:** the resolution must be deterministic and explainable ("coordinator marked absent, so absent"), not order-of-arrival dependent. **Risk:** migrating existing `isAttended`/first-event data to a recompute-by-precedence model (see risk-01).

2. **Manual attendance has no levels and no ownership.** The existing `AttendanceSourceEnum` has a single `MANUAL_ADMIN` source, so an RM and a coordinator are indistinguishable — the hierarchy "coordinator > RM" cannot even be represented, and there is no notion of *which* RM owns a mark. **Constraints:** a manual mark must carry its author's authority level *and* its author identity; an RM may mark only the registrants assigned to them; and each record is editable only by the actor who created it (blocking is by ownership, not rank).

3. **Sources are not extensible without risk.** QR is coming, other providers may follow. If each new source means edits inside resolution logic, every addition risks regressing the attendance status for existing sources. **Constraint:** the set of sources and their ranking must live in one declarative place.

4. **Marks are not safely reversible.** Attendance is corrected by humans who make mistakes; there must be a way to undo a mark and to assert a positive absence — without destroying the audit trail and without letting a junior role reverse a senior one's decision. **Risk:** a wrong trust ranking produces confidently-wrong attendance statuses (risk-02).

## Core capabilities

High-level only; mechanics belong in the TRD.

- **Multi-source capture into one log.** All five [attendance sources](glossary#attendance-source) — join-click, Zoom provider, RM manual, coordinator manual, QR (deferred) — append events to the same per-registrant-per-session record. Two already have working capture paths (join-click and Zoom); the module adds the RM/coordinator split and QR modeling.
- **Hierarchy resolution.** A resolution engine computes the [effective attendance status](glossary#effective-attendance-status) as the mark of the highest-ranked source that has any record, in either direction (present *or* absent). This is the module's deep core — the one capability the product is.
- **Undo and mark-absent, owner-scoped.** Every actor's record stays editable by *that actor*: they can undo it or flip it, and can assert an explicit absence. Editing another actor's record is blocked — overriding a different source is done by adding your own higher-ranked record, not by touching theirs. Every change appends an event and triggers recomputation. Marking rights for an RM are further scoped to their assigned registrants (see [source-hierarchy](glossary#source-hierarchy)).
- **Append-only audit trail.** No event is mutated or deleted; undo is a reversal event. The full "who marked what, when, from which source" history survives for dispute resolution.
- **Extensible source registry.** Sources and their precedence live in a typed enum plus a precedence map; adding or reordering a source touches only that definition, never the resolution engine.

## Key decisions

These were settled during scoping (see the graph's Decisions group for alternatives and reversal triggers):

- **dec-01 — Top source wins, present or absent.** The highest-ranked source with any record decides; a coordinator marking *absent* overrides a Zoom *present*.
- **dec-02 — Sources as an extensible enum + precedence map in code.** "Dynamic" means no per-source branching in business logic — not a runtime-editable table. Reordering or adding a source is a small, reviewed code change.
- **dec-03 — Edits are owner-scoped.** Every actor can add/edit/undo only the record they created; altering another actor's record is blocked. A coordinator overrides an RM not by editing the RM's mark but by adding a higher-ranked one; after any change the attendance status recomputes.
- **dec-07 — RM authorization is per-registration.** Each registration has one assigned RM, and only that RM may mark it.
- **dec-04 — QR is modeled now as a provider-class source; its capture endpoint is deferred.** QR behaves like Zoom in the hierarchy (an automated/external source) and is present in the enum and precedence map so it can be switched on later without a model change.
- **dec-05 — The attendance status is recomputed from the event log by precedence**, replacing the existing "isAttended set on the first event" behavior.
- **dec-06 — The single `MANUAL_ADMIN` source is split into RM-level and coordinator-level sources** so the hierarchy can rank them.

## Scope

**In scope (MVP)**
- Capture from join-click, Zoom provider, RM manual, and coordinator manual.
- Precedence-based resolution of a single attendance status per registrant per session.
- Undo (role-gated) and explicit mark-absent.
- Append-only audit trail and an extensible source registry with QR present in the model.

**Out of scope (MVP)**
- The QR-scan **capture** endpoint (QR is modeled and ranked, not captured).
- Attendance notifications or downstream reporting changes beyond exposing the resolved attendance status.
- A runtime/DB-editable source table (explicitly rejected in dec-02).
- Per-occurrence attendance for recurring/shared sessions (pending oq-04).

**Future**
- QR-scan capture activation.
- Additional online providers (Teams, Meet) as new provider-class sources.

## Leap-of-faith assumptions

These three beliefs must hold; if any is false, the module's attendance status is wrong at the root.

1. **Human authority is the most reliable signal (asm-01).** The entire hierarchy rests on the belief that a coordinator's deliberate call beats any automated read. If in practice coordinators mark hastily and the provider record is more accurate, "top source wins" produces worse attendance statuses than a simpler model. *Not yet validated* — plan: sample sessions where coordinator and provider disagree and check ground truth.
2. **The marker's role is resolvable at mark time (asm-02).** Ranking a manual mark and gating undo both require knowing whether the actor is an RM or a coordinator. The existing entity stores an RM *name string*, not a linked role — so this assumption is *not yet validated* and may need a real role source.
3. **Session-level granularity is sufficient (asm-03).** We assume one attendance status per session row is enough and no program needs per-occurrence attendance on recurring "same-link" sessions. *Not yet validated* — see oq-04.

## Success metrics

**At handoff**
- Resolution is correct in 100% of enumerated source-conflict combinations (a truth table over the ranked sources, present/absent, with and without records).
- Undo and mark-absent enforce role-gating: an RM cannot reverse a coordinator's mark in any test.
- Adding a new source in a spike touches only the enum and precedence map — zero edits to the resolution engine — proving the extensibility goal.

**At adoption**
- Coordinators and RMs successfully override incorrect automated attendance without engineering involvement.
- Every resolved attendance status is explainable from its event log (which source, which actor, when).

## Open questions

Stage 20 / the PRD must resolve these before build:

1. **Join-click ranking (oq-01).** The hierarchy given is coordinator > RM > qr/zoom. Where does *join-click* sit among the automated sources — equal to provider, above, or below?
2. **Representation of "absent" (oq-02).** Is absent an explicit event, or the absence of any present event, and how does each interact with "top source wins"?
3. **Undo fallback (oq-03).** When the top-ranked source's mark is undone, does the attendance status fall to the next-ranked source's record, or to unknown?
4. **Recurring/shared session granularity (oq-04).** Per session row, or per occurrence?
5. **Existing-data migration (oq-05).** How are historical `attendance_events` rows and `isAttended` flags recomputed or grandfathered under the precedence model?

---

Approved by:
Role:
Date:

Approved by:
Role:
Date:
