# ADR-041: Replacing an Engaged Official Recce Record Uses Supersede Flow, Not Update-in-Place

## Status

Accepted, 2026-06-22.

## Status History

```yaml
status_history:
  - date: 2026-06-22
    status: Proposed
    changed_by: hkl
    reason: Formalising supersede flow for client-engaged Recce records in Recce PWA
    changed_via: adr-kit (360lm)
  - date: 2026-06-22
    status: Accepted
    changed_by: hkl
    reason: Supersede flow live in Recce v16; client sees deprecated record alongside new official
    changed_via: adr-kit (360lm)
```

## Context

A Recce (store reconnaissance visit) can be marked "official" for a client — meaning the client portal shows it as the authoritative record for that counter. Two distinct scenarios require a supersede flow rather than in-place mutation:

**Scenario 1 — Corrected Recce replacing an engaged official**
A field agent submits a corrected or updated Recce to replace a prior official. If the prior official was already viewed or responded to by the client ("engaged"), silently swapping would overwrite a record the client has already acted on.

**Scenario 2 — Reassignment conflict (Multi-Recce)**
A Recce is assigned to Agent A, who submits it. Due to reassignment, the same counter+campaign Recce is also submitted by Agent B. The system now holds two competing recces for the same `(counter_id, job_id / free_text_identifier)` group. The supervisor must resolve the conflict: pick one as authoritative, or apply a supervisor override with a documented reason. The unchosen record must be traceable — not deleted.

Both scenarios use the same `superseded_by_sub_id` / `superseded_at` / `superseded_by` columns on `recce.submissions`. The differ in who initiates the supersede (system vs supervisor) and whether a written rationale is captured.

## Decision

### Scenario 1 — Corrected Recce replacing engaged official

When a new Recce is marked official for a client:

- **If previous official is NOT engaged** (client has not viewed or responded): silent swap — previous official's `is_official` is set to false, new record becomes official. Old record is not shown to client.

- **If previous official IS engaged** (client has viewed or responded): **supersede flow** — old record gains `superseded_at` timestamp and `superseded_by` FK pointing to the new official. Both records are shown to the client: the new one as current official, the old one with a "Deprecated" label. Client can see the history and understand why the record changed.

"Engagement" gate: a record is considered engaged if it has any `client_response` rows, a viewed timestamp, or an admin-approval state from the client.

### Scenario 2 — Supervisor conflict resolution (Multi-Recce reassignment)

When a `(counter_id, job_id)` group has 2+ recces (typically due to reassignment):

1. **Simple pick** — supervisor calls `counters.mark_official_recce(p_sub_id, p_picked_by)`. The DB trigger `trg_one_official_per_group` atomically demotes all sibling rows. No supersede record is written; the unchosen recce remains visible in the counter's recce history.

2. **Supervisor Override** — supervisor calls `counters.supervisor_pick_official(p_winner_sub_id, p_loser_sub_id, p_picked_by, p_note)` (added in `migrate_counters_v23.sql`). This:
   - Marks winner `is_official_for_client = TRUE` (trigger handles demotion of siblings)
   - Sets `superseded_by_sub_id = winner`, `superseded_at`, `superseded_by` on the loser — creating a formal supersede record
   - Stores the supervisor's rationale in `winner.meta.supervisor_override.note`
   - The loser gains a "↳ Superseded" badge in all supervisor views

The override path is used when the pick is non-obvious and requires a paper trail (e.g., photos from B are better but data quality from A is higher; supervisor explains the decision).

**The UI for conflict resolution** (Counter Management PWA → Multi-Recce screen):
- Supervisor taps a group → A/B tab switcher shows each recce card
- "⇆ Compare A vs B" toggles an inline field-diff table (16 fields, differing rows highlighted)
- Resolve bar: **★ A is Official** / **★ B is Official** (simple pick) / **⊕ Override** (override panel)
- Override panel: base-record radio + note textarea + "Apply Supervisor Override" button

**Decision Maker:** hkl

## Alternatives Considered

**Scenario 1 alternatives:**
- **Always update-in-place (overwrite the official pointer silently).** Rejected: client has already viewed and potentially acted on the previous official; silently changing what they see invalidates their prior response context; auditors and clients expect records they responded to to remain visible.
- **Always use supersede flow (even for unengaged records).** Rejected: creates deprecated ghost records for every correction, cluttering the client portal with one-line "deprecated" entries that were never seen; adds noise without audit value.
- **Delete previous official, insert new (no supersede).** Rejected: deleting a record the client responded to destroys the audit trail; cascade would also delete client responses; unrecoverable data loss.
- **Versioning (immutable records + version pointer).** Rejected: adds a version table and view complexity; supersede is sufficient — there is no requirement to traverse a version chain; the client only needs to see current + immediately-previous.

**Scenario 2 alternatives:**
- **Create a merged/composite row (INSERT new submission cherry-picking fields from A+B).** Rejected: `web_anon` has no INSERT grant on `recce.submissions`; generating a synthetic `sub_id` adds complexity; the real requirement is traceability and authority, not field-level merge — the supervisor's note in `meta` captures any field-override rationale without a new row.
- **Always require a written note (force override path, no simple pick).** Rejected: most reassignment conflicts are straightforward (one agent never actually visited; the other has complete data); forcing a note adds friction for the common case. Simple pick is available for obvious calls; override is available for contested ones.
- **Show photos from both submissions as a cherry-pick grid, write selected URLs to winner.** Partially deferred: `photo_urls` live per-submission and cannot be moved between rows without INSERT rights. The supervisor's note can reference which submission's photos are preferred; a future RPC with INSERT rights could materialise the cherry-picked set.

## Consequences

**Positive:**
- Client can always see why the official record changed (both versions visible on the portal).
- Prior client responses remain attached to the correct historical record.
- Audit trail is preserved for all engaged records.
- Simple binary logic for Scenario 1: engaged → supersede; unengaged → silent swap.
- Scenario 2 override captures supervisor's written rationale in `meta` — auditable without a separate log table.
- Both scenarios reuse the same four columns (`superseded_by_sub_id`, `superseded_at`, `superseded_by`, `is_official_for_client`) — no schema divergence between the two flows.

**Negative / Trade-offs:**
- Client portal must render both the new official and the deprecated old record when a supersede exists — UI complexity.
- `superseded_at` and `superseded_by` columns must be set atomically with `is_official` change — handled by `counters.supervisor_pick_official()` SECURITY DEFINER RPC.
- Over time, clients with many corrections accumulate deprecated records in their portal history.
- Scenario 2: the unchosen recce (loser) retains `is_official_for_client = false` and `superseded_by_sub_id = winner` — it will appear in the counter's recce history with a "↳ Superseded" badge. Supervisors must understand this is intentional (traceability), not an error.

**Risks and mitigations:**
- Engagement check misclassifies a viewed record as unengaged (e.g. client viewed but did not formally respond): mitigated by broadening the engagement definition to include any view event, not just formal responses.
- Supersede chain grows too deep (deprecated → deprecated → official): accepted — current design is one supersede level; chains beyond one are not supported; admin should archive stale deprecated records periodically.
- `trg_one_official_per_group` demotes siblings on any `is_official_for_client = TRUE` update, including `supervisor_pick_official` — this is correct and intentional; the trigger is the single point of enforcement for the one-official-per-group invariant.

## Status History (updated)

```yaml
  - date: 2026-06-26
    status: Accepted (extended)
    changed_by: hkl
    reason: >
      Added Scenario 2 — supervisor conflict resolution for multi-recce
      reassignment groups. New counters.supervisor_pick_official() RPC
      (migrate_counters_v23.sql). Multi-Recce screen redesigned with A/B
      tab switcher, field-diff table, and supervisor override panel.
    changed_via: claude-code (360lm)
```

## Related Decisions

- ADR-018 (client auth) — client portal shows both official and deprecated records to authenticated external clients.
- ADR-009 (per-PWA schema) — `recce.submissions` owns the supersede columns; no cross-schema dependency.
- ADR-062 (live AI pipeline contract) — supervisor_pick_official is a human-review gate (row-by-row card pattern) before any DB write.

## References

- `memory/dbt_recce.md` — supersede flow, engagement gate, deprecated label in client portal
- `recce/index.html` — supersede RPC and is_official flip logic
- `client/index.html` — deprecated record display in client portal view
- `counters/migrate_counters_v23.sql` — `supervisor_pick_official()` SECURITY DEFINER function
- `counters/index.html` — `renderMrgBody()`, `_recceConflictUI()`, `_recceDiffTable()`, `applyOverride()`, `renderRecceTab()` (Past Recces history browser)
