cards/docs/LESSONS_FROM_MANA_MONOREPO.md
Till 8605b1b517 Phase 0+1: Repo-Skelett für Cards-Greenfield
Strategie B (beschlossen 2026-05-08): Cards wird als eigenständige
föderierte App neu gebaut, ohne Code-Übernahme aus mana-monorepo.

Skelett enthält:
- apps/api: Hono+Bun mit /healthz, /version, Manifest-Endpoint, leere
  pgSchema('cards'), Drizzle-Config, erstem Vitest
- apps/web: SvelteKit 2 + Svelte 5 (runes), Vite auf 3082
- packages/cards-domain: Pure-TS, CardType-Discriminated-Union,
  SubIndex-Granularität für Reviews, Future-CardType-Set vorbereitet
- infrastructure/docker-compose.yml: Postgres 16 auf 5435
- app-manifest.json: v1.0.0, Verein-owned, beta-tier
- .github/workflows/ci.yml
- docs/LESSONS_FROM_MANA_MONOREPO.md (Read-Day-Output, 15 Lehren)

Pre-Flight für Phase 2 (Auth-Föderation): DNS cardecky.mana.how,
GitHub-Repo mana-ev/cards, Cards-App-Registrierung in mana-auth,
NPM_AUTH_TOKEN für Verdaccio.

Plan: mana/docs/playbooks/CARDS_GREENFIELD.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 14:08:41 +02:00

203 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Lessons aus mana-monorepo für Cards-Greenfield
**Stand:** 2026-05-08, Read-Day-Output
**Quelle:** `mana-monorepo/{packages/cards-core,apps/mana/.../modules/cards,services/cards-server}`
**Verwendung:** Architektonische Lehren für den Greenfield-Build. Kein Code wird übernommen, nur Designs.
## TL;DR
- **Domain-Library separieren** (`@cards/domain`, framework-agnostic)
- **Card-Type als discriminated union** mit `fields: Record<string,string>` + `subIndex`-Granularität
- **FSRS via `ts-fsrs` v5.3.2** in dünnem Adapter, Reviews bleiben PLAINTEXT
- **Markdown-Editor**, kein Rich-Text/WYSIWYG
- **Keyboard-driven Study-View** (Space=Reveal, 14=Grade)
- **Marketplace ist separate Concern** (eigener Server in mana-monorepo, NICHT im Cards-MVP)
- **Encryption-Registry zentral planen**, MVP `enabled: false`
- **AI-Tools sind dünn** in mana-monorepo (nur Draft `create_card`); wir können sauber neu definieren
---
## 1. Domain-Library als eigener Workspace-Package
mana-monorepo hat `packages/cards-core/` mit Pure-TS:
- Card-Types, Cloze-Parser, FSRS-Wrapper, Markdown-Render-Helpers
- Keine Dexie-, Sync-, oder UI-Abhängigkeiten
- Konsumiert von App-UI-Modul UND von `services/cards-server`
**Bei uns:** `packages/cards-domain/` analog. Bewusst kein `@cards/core` (Name-Konflikt), sondern `@cards/domain`. Steht.
## 2. Card-Type-Modell
mana-monorepo unterstützt: `'basic' | 'basic-reverse' | 'cloze' | 'type-in' | 'image-occlusion' | 'audio' | 'multiple-choice'`.
Datenstruktur:
- `fields: Record<string, string>` als generischer Slot (statt `front`/`back`-Spalten)
- Pro Type unterschiedliche Field-Sets:
- `basic`/`basic-reverse`: `{ front, back }`
- `cloze`: `{ text, extra? }`
- `type-in`: `{ question, expected }`
**SubIndex-Granularität:** Pro Karte gibt es N Reviews (mit `subIndex`):
- `basic`: 1 Review (subIndex 0)
- `basic-reverse`: 2 Reviews (front→back UND back→front)
- `cloze`: 1 Review pro Cluster-Index (`{{c1::...}}`, `{{c2::...}}`)
**Bei uns:** MVP nur `basic` + `basic-reverse`. Aber `Card`-Schema gleich für die volle CardType-Future-Union vorgesehen, damit Schema-Migration klein bleibt. SubIndex-Pattern in Reviews-Tabelle übernehmen.
## 3. FSRS-Library + Adapter-Pattern
- `ts-fsrs` v5.3.2 — dependency-free, Date-basierte API
- mana-monorepo wraps in `LocalCardReview``ts-fsrs.Card` Adapter (ISO-Strings ↔ Date)
- Funktionen: `newReview()` (init), `gradeReview(review, rating)` (next state)
**Bei uns:** Genau gleicher Wrapper. Phase 3.
**Wichtige Regel:** Reviews bleiben **PLAINTEXT** in der DB, weil der Scheduler täglich auf `due <= now` quert. Encryption müsste täglich N Reviews entschlüsseln — geht nicht. mana-monorepos `crypto/registry.ts` listet `cardReviews` explizit als plaintext-allowlisted.
**Bei uns:** Schema-Doku wird das erwähnen. MVP-Encryption-OFF macht das ohnehin moot, aber Future-Proofing wichtig.
## 4. Cloze-Parser: Token-basiert, nicht Regex
mana-monorepo hat `packages/cards-core/src/cloze.ts` mit:
- Anki-kompatible Syntax: `{{c1::answer}}` oder `{{c1::answer::hint}}`
- `tokenize()``Array<{ kind: 'text' | 'cluster', ... }>`
- `clusters()` gruppiert pro Cluster-Index
- `renderCloze(source, hideIndex)``{ front, back, answer }`
**Warum Token-basiert:** Ein Cluster kann mehrfach im Text vorkommen (`{{c1::Berlin}} … {{c1::Berlin}}`). Beide müssen synchron geblankt werden. Token-Parser macht das trivial; Regex-Replace nicht.
**Bei uns:** Cloze ist Phase 8+. Wenn implementiert: Token-basiert, Anki-kompatible Syntax. Library-Wahl: gleich wie ts-fsrs eigene mini-Library schreiben (klein), oder `@anki/cloze-parser` falls existent (TBD).
## 5. Local-First-Stack ist nicht trivial
mana-monorepos Cards-Modul nutzt:
- Dexie + 5 Tabellen (`cardDecks`, `cards`, `cardReviews`, `cardStudyBlocks`, `deckTags`)
- mana-sync (Go) für Server-Sync mit Field-Level-LWW
- `__fieldMeta` pro Record für Konflikt-Detection
- `_updatedAtIndex` Shadow-Column für orderBy
- Encryption-Layer mit Master-Key aus mana-auth
**Lessons:**
- Sync-Engine ist nicht trivial (siehe `apps/mana/.../data/DATA_LAYER_AUDIT.md`)
- Quota-Recovery, Backoff, RLS, Encryption-Rollout sind ihre eigenen Features
- "Schnell mal was Local-First bauen" ist eine Illusion
**Bei uns:** **Server-authoritative MVP, Local-First erst via mana-sync-Federation** (Variante III in CARDS_GREENFIELD.md). Damit überspringen wir das gesamte Komplexitäts-Paket. Wenn später Local-First nötig: Cards bekommt `appId=cards` in mana-sync, statt eigenen Stack zu bauen.
## 6. Encryption-Registry zentral
mana-monorepo hat `apps/mana/.../data/crypto/registry.ts`:
- Cards: `{ enabled: true, fields: ['front', 'back', 'fields'] }` (ein-Tabelle)
- CardDecks: `{ enabled: true, fields: ['name', 'description'] }`
- cardReviews + cardStudyBlocks: explizit plaintext (Performance-Gründe oben)
Regel: Plaintext = IDs/Timestamps/Enums/Sortkeys/Indizes. Encrypt = User-Typed-Content.
**Bei uns:** Encryption initial **AUS** (CARDS_GREENFIELD.md). Wenn nachgerüstet: gleiche Aufteilung. Felder-Allowlist in `packages/cards-domain/src/crypto/registry.ts` zentral, von `apps/api/src/db/...` konsumiert.
## 7. Card-Editor: Markdown statt Rich-Text
mana-monorepo nutzt:
- Stateless `CardFace.svelte` (`card`, `subIndex`, `showBack`, Callbacks)
- Render-Logik:
- Basic: `renderMarkdown(card.fields.front/back)`
- Cloze: `renderCloze(card.fields.text, subIndex)` + extra-hint
- Type-In: Input-Feld + case-insensitive Vergleich gegen `expected`
**Anti-Pattern (vermieden):** Rich-Text-Editor mit Toolbar/Undo/Bold-Hotkeys.
**Bei uns:** Phase 4: Markdown-Editor. Library: vermutlich `marked` + `DOMPurify`. CodeMirror oder Monaco wäre Overkill für Karten — wir brauchen kein Syntax-Highlighting.
## 8. Study-View: Keyboard-driven
mana-monorepos `/cards/learn/[deckId]/+page.svelte`:
- **Space/Enter:** Antwort aufdecken
- **1/2/3/4:** Grade (Again/Hard/Good/Easy → ts-fsrs Rating-Enum)
- **Queue snapshot am Session-Start** — verhindert Mid-Session-Verschiebung neu fälliger Karten
- **Skip-Logic:** Wenn `INPUT` focused, ignoriere Hotkeys
**Bei uns:** Phase 4. Dasselbe Pattern. Hotkey-Handler-Helper `<KeyboardListener>`-Component oder Effect mit `addEventListener`.
## 9. Marketplace: separater Service, NICHT im MVP
mana-monorepo hat **`services/cards-server/`** (Hono+Bun, Port 3072 — Konflikt mit unserer mana-share-Plattform!) als Marketplace-Backend:
- Tabellen: `decks` (public, slug-indexed), `deck_versions` (immutable snapshots), `deck_cards` (versionId, type, fields JSON, contentHash)
- Smart-Merge via per-card SHA-256-Hashes: Subscriber pullen neue Versionen ohne FSRS-State zu verlieren
- Routes: POST `/decks`, GET `/:slug`, POST `/:slug/publish`
**Wichtig:** Das ist ein **eigenes Feature**, kein Teil des Cards-Frontend-Moduls. Cards-Web bringt Decks lokal, Marketplace ist read-only-Browse + Publish.
**Bei uns:** **Nicht im MVP.** Phase 12+. Wenn nachgerüstet: eigenes Backend (oder dedizierter Endpoint in cards-api), nicht in MVP-Schema. Smart-Merge-Pattern (content-hash) ist ein gutes Design — übernehmen, wenn es so weit ist.
**Decommission-Konsequenz:** Wenn Cards-Greenfield live ist, muss `mana-monorepo/services/cards-server/` ebenfalls weg (siehe CARDS_GREENFIELD.md → Decommission). Steht.
## 10. AI-Tools sind dünn
mana-monorepos `lib/modules/cards/tools.ts` hat:
- 1 Tool: `create_card` (name, deckId, front, back) — als Draft, NICHT in `AI_TOOL_CATALOG` registriert
- Keine `list_decks`, `get_deck_stats`, `update_card`-Tools
**Bei uns:** Phase 7 entscheidet Scope. Vermutlich `cards.create` + `cards.search` für MVP. Mehr Tools wenn Persona-Runner-Use-Cases erscheinen.
## 11. Routing-Pattern
mana-monorepos `(app)/cards/`-Routen:
- `/cards/decks` — ListView
- `/cards/decks/[id]` — DetailView (EditName, EditDescription, EditColor, VisibilityPicker)
- `/cards/learn/[deckId]` — Study-Session-View
- `/cards/explore` — Marketplace-Browse
- `/cards/progress` — Stats, Streak, Heatmap
**Bei uns:** Cards ist Standalone, nicht `(app)/cards/...`-Sub-Tree. Routing flacht aus zu `/decks`, `/decks/:id`, `/study/:deckId`. `/explore` und `/progress` sind Polish (Phase 9).
## 12. Visibility: einfacher Enum reicht
mana-monorepo nutzt `@mana/shared-privacy` mit `VisibilityLevel = 'private' | 'space' | 'public'`. Bei Änderungen wird `visibilityChangedAt` + `visibilityChangedBy` getrackt.
**Bei uns:** Gleiches Enum als TypeScript-Type in `@cards/domain`. Audit-Felder optional ab Phase 9 (DSGVO-Polish).
## 13. Tests mit fake-indexeddb
mana-monorepo nutzt `fake-indexeddb` für Unit-Tests gegen Dexie ohne echte DB.
**Bei uns:** **Nicht relevant** — wir sind server-authoritative, keine Dexie. Vitest + Hono `app.request()` (wie in mana-Plattform) für API-Tests, Playwright für e2e.
## 14. Domain-Events + Analytics früh
mana-monorepo emittiert `CardCreated` u.ä. an einen domain-event-Bus + ruft `CardsEvents.cardCreated()` für Analytics.
**Bei uns:** Domain-Events gehen über mana-events (`card.created`, `card.studied`, `deck.completed`). Analytics ggf. später. Phase 5 macht die Event-Anbindung.
## 15. Hash-Everything-Early
mana-monorepos Marketplace hat content-hash auf jeder Karte + jedem Deck-Version-Snapshot. Ermöglicht Smart-Merge ohne Diffing.
**Bei uns:** Hash-Spalte auf `cards` und `decks` schon im MVP-Schema einplanen, auch wenn Marketplace nicht da ist. Cheap to add, expensive to retrofit.
---
## Anti-Patterns aus mana-monorepo, die wir vermeiden
- **Rich-Text-Editor mit Toolbar:** Markdown reicht für Karten-Inhalte
- **Real-time-Collaboration:** Cards sind privat, async-Sync genügt
- **Geteilte Dexie für 27 Module:** entfällt, Cards hat eigene Postgres
- **`updatedAt` als synced data field:** mana-monorepo musste das nachträglich auf `__fieldMeta`-deriviert umbauen (siehe `mana-monorepo/CLAUDE.md` §"Conflict-Detection 2026-04-26"). **Wir machen es ab Tag 1 richtig:** `updatedAt` wird beim Read aus `max(__fieldMeta[*].at)` deriviert, falls wir je eine ähnliche Architektur brauchen — oder bleibt einfach eine normale Column im Server-Modell. (MVP: Column reicht.)
## 5 Kern-Entscheidungen für unser Greenfield
1. **`@cards/domain` als Pure-TS-Workspace-Package** — keine Framework-Bindings
2. **Card-Type discriminated union mit `fields`-Slot + `subIndex`-Granularität** — auch wenn MVP nur basic-Karten hat
3. **`ts-fsrs` v5.3.2 hinter dünnem Adapter** — Reviews plaintext, indiziert auf `due`
4. **Cloze als Token-Parser, wenn implementiert** — Anki-kompatible Syntax
5. **Encryption planen, aber MVP-OFF** — Felder-Allowlist von Tag 1 vorsehen
## Was nicht aus mana-monorepo kommt
- Marketplace-Backend (eigenständige Phase 12+)
- Local-First-Sync-Stack (mana-sync-Federation oder gar nicht)
- Mobile-App (PWA reicht)
- Komplexe AI-Workbench-Integration
---
**Verbindlich:** Phase 1 (Repo-Skelett) ist abgeschlossen. Phase 0 (dieser Read-Day) ist mit diesem Dokument erledigt. Phase 2 (Auth-Föderation) ist als Nächstes dran.