Phase 12 R3: Marketplace γ + δ — Discovery + Engagement + Subscribe + Smart-Merge

Routes (additiv unter /api/v1/marketplace/*):

Discovery (optional-auth, anonymer Read erlaubt):
- GET /explore — featured + trending side-by-side
- GET /decks — browse mit q/tag/language/author/sort/limit/offset
  (sort: recent | popular | trending; trending = star-velocity 7d)
- GET /tags — flacher Tag-Tree

Engagement (auth pro Schreib-Route, optional-auth für GET state):
- POST/DELETE/GET /decks/:slug/star
- POST/DELETE/GET /authors/:slug/follow (cannot-follow-self → 409)

Subscribe + Version-Read + Smart-Merge-Diff:
- POST/DELETE/GET /decks/:slug/subscribe
- GET /me/subscriptions (mit update_available-Indicator)
- GET /decks/:slug/versions/:semver — voller Cards-Payload in ord-
  Reihenfolge
- GET /decks/:slug/diff?from=:semver — computeDiff (added/changed/
  removed/unchanged) basierend auf content_hash + ord-Heuristik für
  "changed an gleicher Position"

Fork + Smart-Merge-Pull (auth):
- POST /decks/:slug/fork — kopiert latest version in privaten
  cards.decks (forked_from_marketplace_* gesetzt) + cards.cards mit
  übernommenem content_hash + frische FSRS-Reviews
- POST /private/:deckId/pull-update — Smart-Merge: existing private
  hashes deduplizieren, nur added/changed cards einfügen (mit fresh
  reviews), unveränderte Karten BEHALTEN inkl. FSRS-State, removed
  cards bleiben lokal (server-authoritative User-Choice). Update der
  forked_from_marketplace_version_id auf latest.

Schema (R3a):
- cards.decks: 2 neue Columns forked_from_marketplace_deck_id +
  forked_from_marketplace_version_id (text, nullable). Drizzle-push
  grün.

Architektur-Highlights:
- @cards/domain.cardContentHash ist die single source of truth für
  Karten-Hashing; marketplace.deck_cards und cards.cards berechnen
  identisch → Smart-Merge ist hash-equality + INSERT-IGNORE statt
  Diff-Replay
- pgSchema-Trennung (marketplace.* vs. cards.*) zahlt sich aus:
  Marketplace-Read-Path (Public + Engagement) und privater Lern-Pfad
  haben separate FK-Welten und können unabhängig versioniert werden
- Hono-Middleware-Pattern: per-route authMiddleware/optionalAuth statt
  Sub-Router-Mount, weil ein Wildcard '*' auf einem Sub-Router via
  r.route('/', sub) sonst die Public-GET-Routes des Parents fängt
  (Hono-Routing-Subtilität, kostete eine Smoke-Iteration)

Verifikation:
- type-check 0 errors
- 6 neue Diff-Heuristik-Tests, 78 gesamt grün
- End-to-End-Smoke gegen lokale cards-api:
  · Cardecky-Author + Deck `r3-stoische-grundbegriffe` v1.0.0 (3 Karten)
  · Till browst (anon → 200), starred, folgt Cardecky, subscribed
  · Till forkt → privates Deck mit 3 Karten + 3 fresh FSRS-Reviews
  · SQL-Manipulation: Apatheia-Review auf state='review',
    stability=10, reps=3 (simuliert "schon gelernt")
  · Cardecky publisht v1.1.0: Apatheia + Eudaimonia unverändert,
    Logos präzisiert (changed), Tugendlehre neu (added)
  · Diff-Endpoint zeigt: unchanged=2, changed=1, added=1, removed=0
  · Till pull-update → cards_inserted=2 (changed.next + added)
  · Verifikation: card_count=5 (war 3), Apatheia-Review **identisch
    erhalten** (state=review, stability=10, reps=3, last_review IS
    NOT NULL), neue Karten state=new — FSRS-State der unveränderten
    Karte überlebt Smart-Merge unverletzt

Verbleibend: R4 ε (PRs + Card-Discussions), R5 Frontend-Routes
(/explore, /d/[slug], /u/[slug], /me/subscribed, /me/forks), R6
voller UI-E2E.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-09 15:27:39 +02:00
parent 7dbbf63523
commit d45f1c0079
10 changed files with 1170 additions and 11 deletions

View file

@ -98,7 +98,7 @@ Vollständiger Plan: [`mana/docs/playbooks/CARDS_GREENFIELD.md`](../mana/docs/pl
| 9 | Polish (DSGVO-UI, Settings, Account, Statistik, i18n, A11y, Media, Image-Occlusion) | 🟡 weit | Card-Edit + Cloze-Editor + Inbox-Banner + Account/DSGVO + Statistik + Pre-Flight-Swap + i18n DE/EN + A11y-Pass + Cloze-Hint-Anzeige + Anki-Re-Import-Dedupe + MinIO-Media-Upload + Image-Occlusion durch (9a9l). Verbleibend: type-in, audio, multiple-choice (Schema vorbereitet) |
| 10 | Production-Deploy (Mac Mini, Cloudflare-Tunnel) | ✅ live 2026-05-08 | cardecky.mana.how + cardecky-api.mana.how, alte cards.* via nginx-301-Redirect |
| 11 | Decommission Cards-Modul aus mana-monorepo | ✅ 2026-05-08 | apps/cards, services/cards-server, packages/cards-core, mana-app cards-Modul + cross-refs entfernt (4 Commits, type-check 0 errors) |
| 12 | Marketplace-Restore (R0R6) | 🟡 R0+R1+R2 durch | Plan: [`docs/playbooks/MARKETPLACE_RESTORE.md`](docs/playbooks/MARKETPLACE_RESTORE.md). R0 (Doku-Archiv + Restore-Plan + Strategie-B-Klarstellung): ✅. R1 (16 Tabellen + 5 Enums in `marketplace`-pgSchema, CHECK-Constraint verifiziert): ✅. R2 (Backend α + β): ✅ — Author-Routen (`POST/GET /authors/me`, `GET /authors/:slug`), Deck-Init (`POST /decks`), Publish-Flow (`POST /decks/:slug/publish` mit @cards/domain-Hash + per-Version-Hash + AI-Mod-Stub-Log + atomarem latest_version_id-Bump), PATCH-Metadaten, Public-Detail mit optional-auth. 16 neue Tests (72 gesamt) grün, E2E-Smoke gegen lokale cards-api durch (Cardecky-Author + Deck `r2-stoische-ethik` mit 3 Karten v1.0.0, semver-409 + paid-422-Errors verifiziert). Code-Referenz aus altem cards-server unter `docs/marketplace/archive/code/` (3331 LOC). Verbleibend: R3 γ/δ (Discovery + Subscribe + Smart-Merge), R4 ε (PRs + Discussions), R5 Frontend-Routes, R6 voller E2E-Smoke. |
| 12 | Marketplace-Restore (R0R6) | 🟡 R0+R1+R2+R3 durch | Plan: [`docs/playbooks/MARKETPLACE_RESTORE.md`](docs/playbooks/MARKETPLACE_RESTORE.md). R0 (Doku + Restore-Plan + Strategie-B-Klarstellung): ✅. R1 (16 Tabellen + 5 Enums in `marketplace`-pgSchema): ✅. R2 (Backend α + β: Author-Routen + Deck-Init + Publish-Flow): ✅. **R3 (γ + δ Discovery + Engagement + Subscribe + Smart-Merge): ✅**`GET /explore` (featured + trending), `GET /decks` (browse mit q/tag/lang/author/sort/limit/offset), `GET /tags`, `POST/DELETE/GET /decks/:slug/star`, `POST/DELETE/GET /authors/:slug/follow`, `POST/DELETE/GET /decks/:slug/subscribe`, `GET /me/subscriptions` (mit update_available-Flag), `GET /decks/:slug/versions/:semver`, `GET /decks/:slug/diff?from=:semver`, `POST /decks/:slug/fork` (private cards.decks-Kopie mit `forked_from_marketplace_*`-Pointern + frischen FSRS-Reviews), `POST /private/:deckId/pull-update` (Smart-Merge-Pull: hash-equality dedupe via `@cards/domain.cardContentHash` lässt unveränderte Karten **inkl. ihrer FSRS-Reviews komplett in Ruhe**, neue/geänderte Karten kommen als private Insert dazu). 6 neue Diff-Heuristik-Unit-Tests, 78 gesamt grün. **End-to-End-Smoke verifiziert**: Cardecky publisht v1.0.0 → Till forkt → Till studiert Apatheia (state=review, stability=10, reps=3) → Cardecky publisht v1.1.0 (Logos geändert + Tugendlehre neu) → Till pull-update → Apatheia-Review intakt, +Tugendlehre + neue Logos-Karte als zusätzliche Inserts. Verbleibend: R4 ε (PRs + Discussions), R5 Frontend-Routes, R6 voller End-to-End-Smoke gegen UI. |
Legende: ✅ erledigt + verifiziert · 🚧 blockiert · ⏸ noch nicht begonnen