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

11 KiB
Raw Permalink Blame History

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 LocalCardReviewts-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.