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>
This commit is contained in:
Till 2026-05-08 14:08:41 +02:00
commit 8605b1b517
37 changed files with 1197 additions and 0 deletions

View file

@ -0,0 +1,31 @@
{
"name": "@cards/domain",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Cards-Domain — Pure-TS-Modell: Card-Types, FSRS-Adapter, Cloze-Parser, Anki-Import-Helpers, zod-Schemas. Keine DB-, Framework- oder Hono/SvelteKit-Abhängigkeiten.",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./types": "./src/types.ts",
"./fsrs": "./src/fsrs.ts",
"./cloze": "./src/cloze.ts",
"./schemas": "./src/schemas/index.ts"
},
"scripts": {
"build": "tsc -p tsconfig.json --noEmit",
"type-check": "tsc -p tsconfig.json --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"lint": "echo 'lint configured later'",
"clean": "rm -rf dist .turbo coverage"
},
"dependencies": {
"ts-fsrs": "^5.3.2",
"zod": "3"
},
"devDependencies": {
"vitest": "^2.1.0"
}
}

View file

@ -0,0 +1,8 @@
// @cards/domain — public API
//
// Pure-TS, keine DB/Framework-Abhängigkeiten. Wird vom apps/api
// (Drizzle-Schemas) und apps/web (UI) gleichermaßen konsumiert.
export * from './types.ts';
// export * from './fsrs.ts'; // Phase 3: FSRS-Adapter (ts-fsrs)
// export * from './cloze.ts'; // Phase 8 oder später: Cloze-Parser

View file

@ -0,0 +1,7 @@
// Phase-3-Aufgabe: zod-Schemas für API-Inputs/Outputs.
//
// Konvention: ein zod-Schema pro Domain-Type (DeckSchema, CardSchema,
// ReviewSchema). API-Routen rufen `Schema.parse()` für Input-Validation,
// und `zod-to-json-schema` generiert die JSON-Schemas für mana-mcp/-share.
export {};

View file

@ -0,0 +1,87 @@
// Domain-Typen für Cards.
//
// Modellierung folgt den Lessons aus mana-monorepo
// (siehe docs/LESSONS_FROM_MANA_MONOREPO.md):
//
// - CardType ist eine discriminated union.
// - Card hat `fields: Record<string, string>` als generischen Slot.
// - Pro Karte gibt es N Reviews mit `subIndex` (basic = 1, basic-reverse = 2,
// cloze = 1 pro Cluster).
// - cardReviews bleiben PLAINTEXT, weil der Scheduler täglich auf `due`
// filtert.
/** Phase-1-MVP-Set; Cloze + Image-Occlusion in Phase 8+. */
export type CardType = 'basic' | 'basic-reverse';
/** Voll geplantes Set (für Schemas vorbereitet, MVP nicht alle implementiert). */
export type CardTypeFuture =
| 'basic'
| 'basic-reverse'
| 'cloze'
| 'type-in'
| 'image-occlusion'
| 'audio'
| 'multiple-choice';
export type CardFields = Record<string, string>;
export type Deck = {
id: string;
user_id: string;
name: string;
description?: string;
color?: string;
visibility: 'private' | 'space' | 'public';
fsrs_settings: FsrsSettings;
created_at: string;
updated_at: string;
};
export type Card = {
id: string;
deck_id: string;
user_id: string;
type: CardType;
fields: CardFields;
tags: string[];
media_refs: string[];
created_at: string;
updated_at: string;
};
/**
* Pro `(card_id, sub_index)` ein Review-Eintrag. Der Scheduler quert auf
* `due <= now` täglich das Feld bleibt deshalb plaintext und ist indiziert.
*/
export type Review = {
card_id: string;
sub_index: number;
user_id: string;
due: string;
stability: number;
difficulty: number;
elapsed_days: number;
scheduled_days: number;
reps: number;
lapses: number;
state: 'new' | 'learning' | 'review' | 'relearning';
last_review: string | null;
};
export type StudySession = {
id: string;
user_id: string;
deck_id: string;
started_at: string;
finished_at: string | null;
cards_reviewed: number;
cards_correct: number;
};
/** Default-Konstanten aus ts-fsrs; per-Deck-Overrides möglich. */
export type FsrsSettings = {
requestRetention?: number;
maximumInterval?: number;
w?: number[];
enableFuzz?: boolean;
};

View file

@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*", "tests/**/*"],
"exclude": ["node_modules", "dist", ".turbo"]
}