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>
203 lines
11 KiB
Markdown
203 lines
11 KiB
Markdown
# 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, 1–4=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.
|