arch: Candidate-Schema — vier offene Entscheidungen (Day-Planner Phase 1) #454

Closed
opened 2026-06-09 08:08:42 +02:00 by pm-bot · 1 comment
Collaborator

Kontext

Day-Planner-Epic #360 hat das Konzept eines Candidate-Layers als Sources→Planner-Zwischenstück eingeführt. Phase 1 (#453) entwirft das Schema; vier Architektur-Entscheidungen sind offen und müssen vor Schema-Commit fallen, weil sie die Migration formen.

Konkrete Entscheidung gebraucht

Vier Schema-Designs, die zusammen die Candidate-Tabelle formen — bitte einzeln entscheiden + kurz begründen:

1. Mandant: Candidate-Tabelle pro User oder global mit ownerSub?

  • Option A — global mit ownerSub: eine Tabelle, alle Candidates, Row-Filter via WHERE. Standard im Rest der API.
  • Option B — pro User: separate Schemas/Tabellen, harte physikalische Trennung.

2. Refs: separate Join-Tabelle candidate_refs oder JSONB-Feld?

  • Option A — JSONB: refs: { listId?, mailId?, trackingId?, calendarEventId?, ... } als JSONB-Spalte. Einfacher Schema-Append, weniger JOINs.
  • Option B — candidate_refs-Tabelle: (candidate_id, ref_type, ref_id). Bessere Indizierbarkeit/Foreign-Keys, klare Beziehung zu Source-Tabellen.

3. lifecycleState: Enum oder mehrere Boolean-Flags?

  • Option A — Enum: lifecycleState ∈ {pending, planned, done, obsolete, ...}. Eine Quelle der Wahrheit.
  • Option B — Booleans: isPlanned, isDone, isObsolete. Flexibler Kombinatorik, aber widersprüchliche Zustände möglich.

4. Done-Rückmeldung an die Source

Wenn ein Candidate auf done geht (z.B. Mail beantwortet, Paket abgeholt) — wie weiß die Source es, damit sie nicht erneut denselben Candidate erzeugt?

  • Option A — Source pollt Lifecycle-State vor jedem Candidate-Erzeugen (Idempotenz via sourceRef).
  • Option B — Event-Bus (candidate.done → Source-Listener), klarer Event-Flow, aber zusätzliche Infrastruktur.
  • Option C — Completion-Policy am Candidate entscheidet, wie done erkannt wird (manual, derived-all-items, auto-on-event, time-elapsed); Source muss nur lesen, nicht reagieren.

Constraints

  • DB-Migration-Policy aus CLAUDE.md: forward-only und backward-compatible.
  • Bestehende Sources (tracking-todo-writer, #143/#144/#145) müssen schrittweise migriert werden können — kein Big-Bang.
  • Drizzle-typed; Schema endet in @mrrmlab/shared-types.
  • Skala: einzelner User pro Instanz aktuell, aber Multi-User langfristig nicht ausgeschlossen.

Dringlichkeit

Phase-1-Schema-Spike (#453) wartet auf die Entscheidungen. Folge-Phasen (Tracking-Refactor, Mail-Source #178, Habit-Source, Planner v0) blocken auf Phase 1. Keine harte Deadline, aber Schema-Drift teuer wenn später revidiert.

Bezug

  • Epic: #360
  • Phase-1-Implementierung: #453
  • Cross-Cut: #122 (KI-Foundation), #178 (Mail-Source), #328/#329/#330 (Tracking-Showstopper)
## Kontext Day-Planner-Epic #360 hat das Konzept eines `Candidate`-Layers als Sources→Planner-Zwischenstück eingeführt. Phase 1 (#453) entwirft das Schema; vier Architektur-Entscheidungen sind offen und müssen vor Schema-Commit fallen, weil sie die Migration formen. ## Konkrete Entscheidung gebraucht Vier Schema-Designs, die zusammen die Candidate-Tabelle formen — bitte einzeln entscheiden + kurz begründen: ### 1. Mandant: Candidate-Tabelle pro User oder global mit `ownerSub`? - **Option A — global mit `ownerSub`**: eine Tabelle, alle Candidates, Row-Filter via WHERE. Standard im Rest der API. - **Option B — pro User**: separate Schemas/Tabellen, harte physikalische Trennung. ### 2. Refs: separate Join-Tabelle `candidate_refs` oder JSONB-Feld? - **Option A — JSONB**: `refs: { listId?, mailId?, trackingId?, calendarEventId?, ... }` als JSONB-Spalte. Einfacher Schema-Append, weniger JOINs. - **Option B — `candidate_refs`-Tabelle**: `(candidate_id, ref_type, ref_id)`. Bessere Indizierbarkeit/Foreign-Keys, klare Beziehung zu Source-Tabellen. ### 3. `lifecycleState`: Enum oder mehrere Boolean-Flags? - **Option A — Enum**: `lifecycleState` ∈ {`pending`, `planned`, `done`, `obsolete`, ...}. Eine Quelle der Wahrheit. - **Option B — Booleans**: `isPlanned`, `isDone`, `isObsolete`. Flexibler Kombinatorik, aber widersprüchliche Zustände möglich. ### 4. Done-Rückmeldung an die Source Wenn ein Candidate auf `done` geht (z.B. Mail beantwortet, Paket abgeholt) — wie weiß die Source es, damit sie nicht erneut denselben Candidate erzeugt? - **Option A — Source pollt Lifecycle-State** vor jedem Candidate-Erzeugen (Idempotenz via `sourceRef`). - **Option B — Event-Bus** (`candidate.done` → Source-Listener), klarer Event-Flow, aber zusätzliche Infrastruktur. - **Option C — Completion-Policy am Candidate** entscheidet, *wie* done erkannt wird (`manual`, `derived-all-items`, `auto-on-event`, `time-elapsed`); Source muss nur lesen, nicht reagieren. ## Constraints - DB-Migration-Policy aus `CLAUDE.md`: forward-only und backward-compatible. - Bestehende Sources (`tracking-todo-writer`, #143/#144/#145) müssen schrittweise migriert werden können — kein Big-Bang. - Drizzle-typed; Schema endet in `@mrrmlab/shared-types`. - Skala: einzelner User pro Instanz aktuell, aber Multi-User langfristig nicht ausgeschlossen. ## Dringlichkeit Phase-1-Schema-Spike (#453) wartet auf die Entscheidungen. Folge-Phasen (Tracking-Refactor, Mail-Source #178, Habit-Source, Planner v0) blocken auf Phase 1. Keine harte Deadline, aber Schema-Drift teuer wenn später revidiert. ## Bezug - Epic: #360 - Phase-1-Implementierung: #453 - Cross-Cut: #122 (KI-Foundation), #178 (Mail-Source), #328/#329/#330 (Tracking-Showstopper)
Collaborator

Alle vier Fragen sind durch ADR 0001 — Candidate-Modell als Unit-of-Planning (docs/adr/0001-candidate-model.md, Status: Accepted, 2026-05-20) bereits entschieden. Kurzfassung:

Frage Entscheidung ADR-Sektion
Mandant Global mit ownerSub (text not null), UNIQUE (ownerSub, source, sourceRef) §5 implizit, Schema candidates.schema.ts
Refs Typed columns mit FK (listId, subTodoListId, mailId, trackingId, calendarEventId) — NICHT JSONB, NICHT join-Tabelle §5
lifecycleState Enum pending | planned | done | obsolete §6
Source-Done-Rückmeldung Event-Bus per Source-Callback-Hook (Vorschlag, Phase-2-Validierung) — Listener registrieren onCandidateDone(sourceRef), idempotent, best-effort. Candidate ist Source-of-Truth. §7

Implementierung:

  • apps/api/src/modules/candidates/candidates.schema.ts
  • Migration apps/api/drizzle/0017_slim_lady_bullseye.sql
  • Tests apps/api/test/integration/candidates-schema.int-spec.ts

Schließe das Issue als superseded by ADR 0001. Falls neue Edge-Cases auftauchen, separate arch-question mit konkretem Trigger.

Alle vier Fragen sind durch **ADR 0001 — Candidate-Modell als Unit-of-Planning** (`docs/adr/0001-candidate-model.md`, Status: Accepted, 2026-05-20) bereits entschieden. Kurzfassung: | Frage | Entscheidung | ADR-Sektion | |---|---|---| | Mandant | Global mit `ownerSub` (`text not null`), UNIQUE `(ownerSub, source, sourceRef)` | §5 implizit, Schema `candidates.schema.ts` | | Refs | **Typed columns mit FK** (`listId`, `subTodoListId`, `mailId`, `trackingId`, `calendarEventId`) — NICHT JSONB, NICHT join-Tabelle | §5 | | `lifecycleState` | **Enum** `pending \| planned \| done \| obsolete` | §6 | | Source-Done-Rückmeldung | **Event-Bus per Source-Callback-Hook** (Vorschlag, Phase-2-Validierung) — Listener registrieren `onCandidateDone(sourceRef)`, idempotent, best-effort. Candidate ist Source-of-Truth. | §7 | Implementierung: - `apps/api/src/modules/candidates/candidates.schema.ts` - Migration `apps/api/drizzle/0017_slim_lady_bullseye.sql` - Tests `apps/api/test/integration/candidates-schema.int-spec.ts` Schließe das Issue als *superseded by ADR 0001*. Falls neue Edge-Cases auftauchen, separate arch-question mit konkretem Trigger.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
admin-mrrm/mrrmlabapp#454
No description provided.