spike(todos): Candidate-Schema entwerfen + ADR (Phase 1 von #360) #361

Closed
opened 2026-05-20 13:01:10 +02:00 by admin-mrrm · 0 comments
Owner

Ziel

Phase 1 von Epic #360Candidate-Modell festlegen, bevor weitere Source-Writer entstehen, damit kein Lock-in. Output: Schema + Drizzle-Migration + ADR.

Showstopper-Risiko (warum jetzt)

  • #178 (Mail → Todo) ist als nächste Source-Implementierung geplant — wenn die im Direct-Write-Pattern (wie tracking-todo-writer) gebaut wird, zementiert sich das Anti-Pattern und mehrere Migration-Cycle-Kosten entstehen.
  • Bestehende Tracking-Source (#143/#144/#145, Followups #328/#329/#330) ist bereits im Direct-Write-Pattern — Phase 2 refactored das, Phase 1 muss das Ziel-Modell definieren.

Scope (in dieser Phase)

Schema-Entwurf (Drizzle, apps/api/src/modules/candidates/candidates.schema.ts)

Felder mindestens:

{
  id: uuid,
  ownerSub: text,                       // user-scoped
  source: enum('mail','tracking','calendar','habit','project','manual'),
  sourceRef: text,                      // idempotency key per source
  lifecycleState: enum('pending','planned','done','obsolete'),

  // Refs (was wird getan — null wenn nicht relevant)
  listId: uuid | null,
  subTodoListId: uuid | null,           // separate Todo-Liste mit Sub-Steps
  mailId: text | null,
  trackingId: uuid | null,
  calendarEventId: text | null,

  // Planungs-Constraints
  title: text,                          // Anzeige-Titel (kann aus Source abgeleitet sein, snapshot for stability)
  earliestAt: timestamp | null,
  latestAt: timestamp | null,
  estDurationMin: int | null,
  locationHints: jsonb,                 // [{name,lat,lng}] oder semantic ('on-way-home')
  dependsOn: uuid[],                    // Candidate-IDs (do-after)
  priority: enum('low','normal','high'),
  recurrence: jsonb | null,             // RRULE-ähnlich

  // Planner-Ergebnis (nullable bis geplant)
  plannedSlot: timestamp | null,
  plannedAfter: uuid | null,
  plannedBefore: uuid | null,

  // Completion-Policy
  completionPolicy: enum('manual','derived-all-items','auto-on-event','time-elapsed'),

  createdAt, updatedAt
}

Unique-Constraints

  • (ownerSub, source, sourceRef) UNIQUE → Idempotenz pro Source

ADR (docs/adr/000X-candidate-model.md)

Dokumentiert:

  1. Warum nicht Direct-Write (Source schreibt fertigen Todo) — Lock-in für Planner
  2. Cardinality: 1 Source-Item → n Candidates erlaubt (z.B. Einkaufsliste mit zwei Filialen → zwei Candidates mit Filter)
  3. Completion-Policy-Modi definiert
  4. Title-Snapshot vs. Live-Derive — Begründung warum title im Candidate liegt (Stabilität, auch wenn Source-Inhalt sich ändert)
  5. Refs vs. JSONB: Refs als typed columns (joinbar, FK-Constraints) statt JSONB — Begründung
  6. lifecycleState Enum vs. Flags — Entscheidung dokumentieren
  7. Source-Rückmeldung: wie weiß Source dass Candidate done ist? (Initialer Vorschlag: Event-Bus / Callback-Hook per Source)

Drizzle-Migration

  • apps/api/src/db/migrations/XXXX_candidates.sql erzeugen
  • Migration ist deployable, Tabelle wird aber noch nicht beschrieben (Phase 2 macht das mit Tracking)

Out-of-scope (in dieser Phase)

  • Planner-Logik (Phase 5)
  • API-Endpoints (Phase 2 fügt nur internen Service)
  • UI für Candidates (Phase 5+)
  • Migration bestehender lists/listItems Daten (nicht nötig — Candidates leben parallel)
  • Mail/Habit/Project-Sources (Phase 3/4)

Acceptance

  • candidates.schema.ts mit Drizzle-Definition committed
  • Migration generiert + auf dev-neu-DB ausführbar
  • ADR docs/adr/000X-candidate-model.md committed mit Begründungen für 1-7
  • Drizzle-Types Candidate / NewCandidate exportiert
  • Tests: nur Schema-Sanity (Insert + Unique-Constraint-Verstoß)

Bezug

  • Epic: #360
  • Blockt: #178 (Mail→Todo soll auf Candidate-Pattern bauen)
  • Folge-Phase: Tracking→Candidate-Refactor (eigenes Issue, wird nach Merge dieses Spikes angelegt)
## Ziel Phase 1 von Epic #360 — **Candidate-Modell festlegen, bevor weitere Source-Writer entstehen**, damit kein Lock-in. Output: Schema + Drizzle-Migration + ADR. ## Showstopper-Risiko (warum *jetzt*) - #178 (Mail → Todo) ist als nächste Source-Implementierung geplant — wenn die im Direct-Write-Pattern (wie `tracking-todo-writer`) gebaut wird, zementiert sich das Anti-Pattern und mehrere Migration-Cycle-Kosten entstehen. - Bestehende Tracking-Source (#143/#144/#145, Followups #328/#329/#330) ist bereits im Direct-Write-Pattern — Phase 2 refactored das, Phase 1 muss das Ziel-Modell definieren. ## Scope (in dieser Phase) ### Schema-Entwurf (Drizzle, `apps/api/src/modules/candidates/candidates.schema.ts`) Felder mindestens: ```ts { id: uuid, ownerSub: text, // user-scoped source: enum('mail','tracking','calendar','habit','project','manual'), sourceRef: text, // idempotency key per source lifecycleState: enum('pending','planned','done','obsolete'), // Refs (was wird getan — null wenn nicht relevant) listId: uuid | null, subTodoListId: uuid | null, // separate Todo-Liste mit Sub-Steps mailId: text | null, trackingId: uuid | null, calendarEventId: text | null, // Planungs-Constraints title: text, // Anzeige-Titel (kann aus Source abgeleitet sein, snapshot for stability) earliestAt: timestamp | null, latestAt: timestamp | null, estDurationMin: int | null, locationHints: jsonb, // [{name,lat,lng}] oder semantic ('on-way-home') dependsOn: uuid[], // Candidate-IDs (do-after) priority: enum('low','normal','high'), recurrence: jsonb | null, // RRULE-ähnlich // Planner-Ergebnis (nullable bis geplant) plannedSlot: timestamp | null, plannedAfter: uuid | null, plannedBefore: uuid | null, // Completion-Policy completionPolicy: enum('manual','derived-all-items','auto-on-event','time-elapsed'), createdAt, updatedAt } ``` ### Unique-Constraints - `(ownerSub, source, sourceRef)` UNIQUE → Idempotenz pro Source ### ADR (`docs/adr/000X-candidate-model.md`) Dokumentiert: 1. **Warum nicht Direct-Write** (Source schreibt fertigen Todo) — Lock-in für Planner 2. **Cardinality**: 1 Source-Item → n Candidates erlaubt (z.B. Einkaufsliste mit zwei Filialen → zwei Candidates mit Filter) 3. **Completion-Policy-Modi** definiert 4. **Title-Snapshot vs. Live-Derive** — Begründung warum `title` im Candidate liegt (Stabilität, auch wenn Source-Inhalt sich ändert) 5. **Refs vs. JSONB**: Refs als typed columns (joinbar, FK-Constraints) statt JSONB — Begründung 6. **`lifecycleState` Enum vs. Flags** — Entscheidung dokumentieren 7. **Source-Rückmeldung**: wie weiß Source dass Candidate done ist? (Initialer Vorschlag: Event-Bus / Callback-Hook per Source) ### Drizzle-Migration - `apps/api/src/db/migrations/XXXX_candidates.sql` erzeugen - **Migration ist deployable, Tabelle wird aber noch nicht beschrieben** (Phase 2 macht das mit Tracking) ## Out-of-scope (in dieser Phase) - Planner-Logik (Phase 5) - API-Endpoints (Phase 2 fügt nur internen Service) - UI für Candidates (Phase 5+) - Migration bestehender `lists`/`listItems` Daten (nicht nötig — Candidates leben parallel) - Mail/Habit/Project-Sources (Phase 3/4) ## Acceptance - [ ] `candidates.schema.ts` mit Drizzle-Definition committed - [ ] Migration generiert + auf `dev-neu`-DB ausführbar - [ ] ADR `docs/adr/000X-candidate-model.md` committed mit Begründungen für 1-7 - [ ] Drizzle-Types `Candidate` / `NewCandidate` exportiert - [ ] Tests: nur Schema-Sanity (Insert + Unique-Constraint-Verstoß) ## Bezug - Epic: #360 - Blockt: #178 (Mail→Todo soll auf Candidate-Pattern bauen) - Folge-Phase: Tracking→Candidate-Refactor (eigenes Issue, wird nach Merge dieses Spikes angelegt)
Sign in to join this conversation.
No project
No assignees
1 participant
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#361
No description provided.