# Log System Redesign Plan

**Status:** DRAFT — not started
**Branch:** `feat-turn-writes-logs`
**Goal:** Lean mutation deltas. Kill bloated payload/undo duplication.

## Problem

Current events = 10-16KB each. `add_participant` row = 16KB to say "Fighter added, init 22." Root cause: full participant roster stored 3x (payload + undo_payload.updates + undo_payload.redo).

DB: 2486 docs, most bloated. Replay trace 1.6-5.5MB for ~250 steps.

Old log (pre-refactor) = message string + small undo object. Proven worked for undo/redo + viewer. Just wanted MORE STRUCTURE, not MORE DATA. Overbuilt instead.

## Intention

Each event = mutation delta + lean snapshot. Structured for filter/group/analyzer. No roster arrays.

### Canonical lean event shape

```js
{
  id, ts, type,                    // identity + filter key
  message,                         // human text (viewer displays)
  encounterId, encounterName,      // which encounter
  participantId, participantName,  // who (nullable: pause/nextTurn have none)
  // type-specific scalar delta (one of these clusters per type):
  //   add:        { init, maxHp, type: 'character'|'monster' }
  //   remove:     { dead: bool }
  //   damage/heal:{ amount }
  //   condition:  { condition }
  //   toggleActive:{ revive: bool }
  //   reorder:    { draggedId, targetId }
  //   update:     { changedFields: ['notes', ...] }
  //   nextTurn:   (none — snapshot carries new round/pointer)
  //   pause/resume/start/end: (none)
  snapshot: {                      // lean, ALREADY GOOD — keep as-is
    round, currentTurnParticipantId, turnOrderIds, activeIds
  },
  undo: <type-specific inverse, id-based>  // minimal
}
```

### Undo inverses (id-based, minimal)

| type | undo |
|------|------|
| add_participant | `{ participantId }` → removeParticipant by id |
| remove_participant | full participant obj (must restore — only place full obj needed) |
| damage | `{ amount: -amount }` or heal back |
| heal | `{ amount: -amount }` or damage back |
| condition | `{ condition }` (toggle is its own inverse) |
| toggleActive | `{ revive: !revive }` |
| reorder | `{ draggedId, targetId }` (swap back) |
| update | `{ changedFields: [...], oldValues: {...} }` |
| nextTurn | `{ currentTurnParticipantId, round }` from snapshot delta |
| pause/resume | toggle |
| start/end | full prior scalar state |

**No `redo` field.** Redo = re-apply forward delta from event itself.

**No `undo_payload` wrapper.** Just `undo` object.

## 4 Consumers

1. **Viewer** — shows `message` + `type` + `participantName` + `undone` flag. Legacy logs (no type) display message only.
2. **Undo/redo** — apply `undo` inverse by id. Id-based, minimal.
3. **Replay** — turn.js funcs write own lean events. Trust func return. NO read-back. JSONL trace = lean snapshots + types.
4. **Analyzer** — reads `snapshot` sequence + `type`. Reconstructs roster from event stream (add/remove by id). NO payload arrays needed.

## Legacy Support

- **Viewer:** yes — show old message-only logs, no type parse
- **Undo:** no — old logs unundoable (acceptable, sit inert)
- **Analyzer:** best-effort — snapshot may be absent on legacy → skip invariant check for that run

## Why Lean

- `add_participant` 16KB → ~150 bytes
- Replay trace 5.5MB → ~250KB
- Undo still works (proven old log did same)
- Analyzer unaffected (uses snapshot+type, not payload)

## Migration Plan

### Phase 1: Redesign event writers (shared/turn.js)

- [ ] Rewrite `buildEntry()` to produce lean shape (delta fields per type)
- [ ] Rewrite `commit()` — no patch storage, write lean event
- [ ] Per-func: compute minimal delta + inverse from enc before/after
- [ ] Drop `payload`, `undo_payload`, `undo_payload.redo`

### Phase 2: Update undo executor

- [ ] Find undo consumer (App.js `applyUndo` or similar)
- [ ] Switch to id-based inverse application
- [ ] Handle each type's undo shape
- [ ] Drop `transactionalUndo` full-patch path if exists

### Phase 3: Update replay trace

- [ ] `replay-combat.js` callStep: trust return, snapshot only (no read-back)
- [ ] Trace = `{step, ts, type, pre:leanSnap, post:leanSnap}` — no participant arrays

### Phase 4: Update analyzer

- [ ] `analyze-turns.js` already uses snapshot+type only — verify no payload dep
- [ ] Reconstruct roster from event stream for skip/double-act checks (id-level)

### Phase 5: Legacy compat

- [ ] `logEvent.js normalizeEvent` — lift legacy into lean shape (type='legacy', message only)
- [ ] Viewer handles legacy gracefully
- [ ] Download = lean events only (legacy excluded or message-only)

### Phase 6: Cleanup

- [ ] Delete old bloated docs from DB? Or leave (inert, viewer reads message)?
- [ ] Verify build CI=true passes
- [ ] Verify analyzer clean on fresh replay
- [ ] Verify undo works on new events

## NOT in scope

- Fixing 5 pre-existing initiative bugs (logged in TODO.md, separate)
- Changing snapshot shape (already lean)
- Changing viewer UI (just data source)

## Open Questions

- Undo for `remove_participant`: must restore full obj (only place full obj needed). Acceptable?
- Should legacy logs be deleted from DB or left inert?
- Download format: lean JSON array (no legacy) — confirm
