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

36
.env.example Normal file
View file

@ -0,0 +1,36 @@
# Cards-Greenfield — Local Dev Defaults
# Copy to .env (git-ignored) and adjust as needed.
# === API + DB ===
NODE_ENV=development
DATABASE_URL=postgresql://cards:cards@localhost:5435/cards
CARDS_API_PORT=3081
CARDS_WEB_PORT=3082
# === Plattform-Endpunkte ===
# Lokal mit Mana-Plattform-Stack auf Default-Ports laufend
MANA_AUTH_URL=http://localhost:3001
MANA_CREDITS_URL=http://localhost:3061
MANA_MEDIA_URL=http://localhost:3065
MANA_SHARE_URL=http://localhost:3072
MANA_LINKS_URL=http://localhost:3073
MANA_EVENTS_URL=http://localhost:3074
MANA_MCP_URL=http://localhost:3069
MANA_SEARCH_URL=http://localhost:3076
# === App-Identität (von mana-auth ausgegeben) ===
# Cards-Service-Key für interne Cross-Service-Calls.
# Production: aus mana-auth.apps-Tabelle bei App-Onboarding.
CARDS_APP_SERVICE_KEY=msk_dev_change_me
# === JWT-Validation ===
# JWKS-URL für User-JWT-Verification (5-Min-Cache).
MANA_AUTH_JWKS_URL=http://localhost:3001/.well-known/jwks.json
# === Verdaccio (für pnpm install) ===
# Lokal: npm login --registry=https://pkg.mana.how
# Production: GitHub Secret
NPM_AUTH_TOKEN=
# === MinIO (über mana-media) ===
CARDS_MEDIA_BUCKET=cards-storage

46
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,46 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.9
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Setup Bun (für apps/api)
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
env:
NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
- name: Type-check
run: pnpm run type-check
- name: Test
run: pnpm run test
- name: Format check
run: pnpm run format:check
- name: Validate manifest
run: pnpm run validate:manifest

65
.gitignore vendored Normal file
View file

@ -0,0 +1,65 @@
# Dependencies
node_modules/
.pnpm-store/
# Build outputs
dist/
build/
.turbo/
.svelte-kit/
# Environment files
.env
.env.local
.env.*.local
.env.secrets
!.env.development
!.env.example
!.env.secrets.example
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
logs/
*.log
npm-debug.log*
pnpm-debug.log*
# Testing
coverage/
.nyc_output/
playwright-report/
test-results/
# TypeScript
*.tsbuildinfo
# Drizzle compiled config
drizzle.config.js
drizzle.config.d.ts
# Cache
.cache/
.eslintcache
.prettiercache
# Package manager locks (keep only pnpm)
package-lock.json
yarn.lock
# Mac Mini deploy
.env.macmini
ssh-key-command.txt
# Volumes for local docker-compose
.volumes/
.local/

7
.npmrc Normal file
View file

@ -0,0 +1,7 @@
# Cards consumes @mana/* shared packages from the Verein's private
# Verdaccio registry (pkg.mana.how on Mac Mini).
# Local dev: run `npm login --registry=https://pkg.mana.how` once;
# the resulting ~/.npmrc token is read via $NPM_AUTH_TOKEN substitution.
# Production CI: set NPM_AUTH_TOKEN in workflow secrets.
@mana:registry=https://pkg.mana.how/
//pkg.mana.how/:_authToken=${NPM_AUTH_TOKEN}

15
.prettierrc.json Normal file
View file

@ -0,0 +1,15 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

133
CLAUDE.md Normal file
View file

@ -0,0 +1,133 @@
# CLAUDE.md — cards repo
Guidance for Claude Code when working in this repository.
## Was dieses Repo ist
**Cards** — eigenständige föderierte Spaced-Repetition-App des
Vereins **mana e.V.** Greenfield-Build (Strategie B, beschlossen
2026-05-08). Cards ist eine **Peer-App** im mana-Ökosystem: redet über
mana-share/-mcp/-search/-events/-credits mit Memoro, Who und der
Mana-Successor-App, hat aber eigene DB, eigenes Frontend, eigene
Deployment-Pipeline.
```
HTTP/JWT ┌──────────────┐
mana e.V. ◄─────────────── │ cards/ │ Postgres `cards`
Plattform │ (this repo) │ cardecky.mana.how
(auth/credits/share/etc.) └──────────────┘
```
## Status
**Phase 0 — Repo-Skeleton.** Vollständiger Plan in
`mana/docs/playbooks/CARDS_GREENFIELD.md` (im Plattform-Repo).
Nächste konkrete Schritte: Read-Day, dann Phase 1 (Auth-Föderation).
## Konventionen
- **pnpm 9.15.x**, Node 20+, Turborepo 2.x
- **Tabs** für Indent, **single quotes**, 100-col Prettier
(`.prettierrc.json` ist die Source of Truth)
- **Stack:** SvelteKit 2 + Svelte 5 (runes-only) für Web,
Hono + Bun + Drizzle für API, Postgres mit `pgSchema('cards')`
- **Drizzle 0.38 / drizzle-kit 0.30 / zod 3** — bewusst gleich wie
mana-Plattform; Migration auf 0.45/0.31/zod-4 läuft mit Plattform-
Migrations-Pfad mit (`mana/docs/MIGRATION_DRIZZLE_ZOD.md`)
- **JWT-Validation** lokal via JWKS-Cache (5 min), niemals Live-Call
zu mana-auth pro Request
- **Service-to-Service**-Calls via `X-Service-Key` (`CARDS_APP_SERVICE_KEY`)
oder mana-auth-issued Bearer-Tokens
- **Tests:** Vitest für Unit/Integration, Playwright für e2e
- **Formatierung:** `pnpm run format` vor jedem Commit
## Architektonische Invarianten
Diese sind beschlossen. Nicht ohne explizite Diskussion antasten:
1. **Server-authoritative MVP.** Keine Dexie, keine IndexedDB, keine
eigene Sync-Engine. Frontend = HTTP-Client zu cards-api. Local-First
später via mana-sync-Federation, nicht durch eigenen Stack.
2. **Kein Code aus mana-monorepo kopiert.** Code dort wird gelesen
(Lessons-Doc), nicht übernommen. Sauber neu ab Tag 0.
3. **Eigene Postgres-DB `cards`** im geteilten Mana-Cluster, Schema-
Isolation via `pgSchema('cards')`.
4. **Föderation über `@mana/shared-app-tpl`.** Pflicht-Endpoints
(`/.well-known/mana-app.json`, `/healthz`, `/api/v1/share/receive`,
`/api/v1/tools/:name`, `/api/v1/search`, `/api/v1/dsgvo/...`)
kommen aus dem geteilten Boilerplate.
5. **Keine externe SaaS, wo vermeidbar.** Erlaubte Ausnahmen kommen
über mana-Plattform: Stripe (mana-credits), Anthropic/OpenAI
(mana-llm), Cloudflare-Tunnel (Edge-Routing).
6. **Encryption initial AUS.** Nachrüstbar, wenn sensibles Feld auftaucht.
## Repo-Struktur
```
cards/
├── apps/
│ ├── web/ SvelteKit-Frontend (cardecky.mana.how)
│ └── api/ Hono+Bun-Backend
├── packages/
│ └── cards-domain/ Pure-TS: FSRS, Card-Types, Schemas, Anki-Parser
├── infrastructure/ docker-compose, tunnel-config
├── docs/ DOMAIN.md, ANKI_IMPORT.md, LESSONS_FROM_MANA_MONOREPO.md, …
├── app-manifest.json Source of Truth für Föderation
└── .github/workflows/ ci.yml, deploy.yml
```
## @mana/* Konsumation
Cards zieht alle Shared-Pakete aus Verdaccio (`pkg.mana.how`):
- `@mana/shared-share-protocol` — Manifest, Envelope-Schemas
- `@mana/shared-app-tpl` — Pflicht-Endpoint-Boilerplate
- `@mana/shared-hono` — Auth-Middleware, Health-Routes, Error-Handler
- `@mana/shared-auth` — Client-SDK für Login-Flow
- `@mana/shared-types` — Common Types
- `@mana/shared-icons`, `@mana/shared-i18n`, `@mana/shared-theme`,
`@mana/shared-tailwind` — UI-Building-Blocks (kein Code-Duplikat
aus mana-monorepo)
Alle als reguläre `dependencies`. Versions-Disziplin ist Klasse-A
(siehe `mana/docs/SHARED_PACKAGES.md`).
## Wichtige Cross-Repo-Doks
- `mana/docs/playbooks/CARDS_GREENFIELD.md` — der vollständige Plan
- `mana/docs/FEDERATION.md` — Architektur-Grundlagen
- `mana/docs/SHARE_PROTOCOL.md` — Manifest- und Envelope-Schemas
- `mana/docs/MANA_AUTH_FEDERATION.md` — App-Identitäts-Modell
- `mana/docs/SHARED_PACKAGES.md` — Versionierungs-Disziplin
- `mana/docs/PORTS.md` — Port-Allokation (cards-api: TBD, cards-web: TBD)
## Wenn ein neuer Pflicht-Endpoint dazukommt
1. `app-manifest.json` aktualisieren (Endpoint-Pfad, Auth-Mode)
2. Handler in `apps/api/src/routes/` schreiben
3. Tests in `apps/api/tests/` (Vitest)
4. `pnpm run validate:manifest` lokal grün
5. CI-Workflow zieht Manifest-Validation automatisch
## Wenn ein neues Tool/Share/Accept dazukommt
1. Schema in `packages/cards-domain/src/schemas/` definieren (zod)
2. JSON-Schema-Export für mana-mcp/mana-share generieren
3. Handler im Manifest registrieren (`tools[].name` oder `accepts[].handler`)
4. Implementierung in `apps/api/src/routes/tools.ts` bzw.
`apps/api/src/share-handlers/`
5. Test-Coverage: zod-parse + Handler-Logic getrennt
## Lokal entwickeln
```bash
pnpm install # @mana/* aus pkg.mana.how holen
pnpm docker:up # Lokales Postgres
pnpm db:push # Drizzle-Schema aktualisieren
pnpm dev # api + web parallel
```
Voraussetzungen:
- mana-Plattform-Stack lokal laufend (mana-auth, optional die
Föderations-Services für E2E-Tests)
- Verdaccio-Token in `~/.npmrc` (`npm login --registry=https://pkg.mana.how`)

45
README.md Normal file
View file

@ -0,0 +1,45 @@
# Cards
**Eigenständige Spaced-Repetition-App des Vereins mana e.V.**
Cards ist eine föderierte Peer-App im mana-Ökosystem. Sie verwaltet
Karteikarten, plant Wiederholungen mit dem FSRS-Algorithmus und
empfängt Inhalte aus anderen Verein-Apps (z.B. Zitate aus Memoro,
Notizen aus Mana, Web-Schnipsel aus dem Browser-Plugin).
→ Live (geplant): <https://cardecky.mana.how>
## Stack
- **Frontend:** SvelteKit 2 + Svelte 5 (runes-only)
- **Backend:** Hono + Bun + Drizzle ORM
- **Datenbank:** Postgres mit Schema-Isolation (`pgSchema('cards')`)
- **Auth:** föderiert über mana-auth (EdDSA JWT, JWKS-Cache)
- **Subscriptions:** mana-credits (zentral pro Verein-Account)
- **AI-Tools:** über mana-mcp Claude Desktop / persona-runner verfügbar
- **i18n:** DE / EN / FR / ES / IT
- **Build:** Turborepo + pnpm 9
## Status
Phase 0 (Repo-Skeleton) — siehe `mana/docs/playbooks/CARDS_GREENFIELD.md`
für den vollständigen Plan.
## Lokal entwickeln
```bash
pnpm install
pnpm docker:up # Postgres in Docker
pnpm db:push # Drizzle-Schema
pnpm dev # api + web parallel
```
→ API auf `http://localhost:3081`, Web auf `http://localhost:3082` (oder Vite-Dev-Default `5173`).
Voraussetzung: Mana-Plattform-Stack (mana-auth, evtl. Föderations-Services)
muss lokal laufen, sonst greift Auth-Login nicht.
## Lizenz
Mana-Verein-intern, MIT (siehe `mana/docs/COMPLIANCE.md` für Details
zur Verein-Lizenzpolitik).

65
app-manifest.json Normal file
View file

@ -0,0 +1,65 @@
{
"$schema": "https://pkg.mana.how/@mana/shared-share-protocol/0.1/manifest.json",
"schema_version": "0.1",
"id": "cards",
"name": "Cardecky",
"version": "1.0.0",
"homepage": "https://cardecky.mana.how",
"icon": "https://cardecky.mana.how/icon-512.png",
"description": "Spaced-repetition flashcards with FSRS scheduling.",
"description_de": "Lernkarten mit Spaced-Repetition (FSRS).",
"ownership": { "kind": "verein" },
"tier_required": "beta",
"endpoints": {
"base_url": "https://cardecky.mana.how",
"health": "/healthz",
"dsgvo_export": "/api/v1/dsgvo/export",
"search": "/api/v1/search",
"share_receive": "/api/v1/share/receive",
"tool_invoke": "/api/v1/tools/:name",
"deep_link_scheme": "cards://",
"link_patterns": [
{
"pattern": "card/(?<id>[a-z0-9_-]+)",
"template": "https://cardecky.mana.how/c/{id}"
},
{
"pattern": "deck/(?<id>[a-z0-9_-]+)",
"template": "https://cardecky.mana.how/d/{id}"
}
]
},
"shares": [{ "type": "mana/card", "schema_ref": "/payload-schemas/card.json" }],
"accepts": [
{ "type": "mana/quote", "handler": "create_card_from_quote" },
{ "type": "mana/url", "handler": "save_link_as_card" },
{ "type": "mana/text", "handler": "create_card_from_text" }
],
"tools": [
{
"name": "cards.create",
"description": "Erzeugt eine Lernkarte für den authentifizierten User.",
"input_schema": { "$ref": "/payload-schemas/create-card-input.json" },
"output_schema": { "$ref": "/payload-schemas/card.json" },
"auth": "user_token"
},
{
"name": "cards.search",
"description": "Sucht in den Lernkarten des authentifizierten Users.",
"input_schema": { "$ref": "/payload-schemas/search-input.json" },
"output_schema": { "$ref": "/payload-schemas/search-output.json" },
"auth": "user_token"
}
],
"search": {
"types": ["card", "deck"],
"languages": ["de", "en", "fr", "es", "it"],
"max_results": 30,
"timeout_ms": 3000
},
"data": {
"personal_data_categories": ["card_content", "review_history"],
"encrypted_at_rest": false,
"storage_location": "EU"
}
}

View file

@ -0,0 +1,13 @@
import type { Config } from 'drizzle-kit';
export default {
schema: './src/db/schema/*.ts',
out: './src/db/migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL ?? 'postgresql://cards:cards@localhost:5435/cards',
},
schemaFilter: ['cards'],
verbose: true,
strict: true,
} satisfies Config;

32
apps/api/package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "@cards/api",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Cards-API — Hono+Bun-Backend für die Greenfield-Cards-App. Spricht mana-Plattform-Services über HTTP.",
"scripts": {
"dev": "bun run --hot src/index.ts",
"start": "bun run src/index.ts",
"build": "tsc -p tsconfig.json --noEmit",
"type-check": "tsc -p tsconfig.json --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"lint": "echo 'lint configured later (eslint flat-config)'",
"clean": "rm -rf dist .turbo coverage",
"drizzle:generate": "drizzle-kit generate",
"drizzle:push": "drizzle-kit push --force",
"drizzle:studio": "drizzle-kit studio"
},
"dependencies": {
"@cards/domain": "workspace:*",
"hono": "^4.6.0",
"drizzle-orm": "0.38",
"postgres": "^3.4.0",
"zod": "3",
"zod-to-json-schema": "^3.23.0"
},
"devDependencies": {
"drizzle-kit": "0.30",
"vitest": "^2.1.0"
}
}

View file

@ -0,0 +1,13 @@
// Drizzle-Schemas für die `cards`-Datenbank.
//
// Phase-3-Aufgabe (siehe CARDS_GREENFIELD.md): hier landen
// `decks`, `cards`, `reviews`, `study_sessions`, `tags`, `media_refs`,
// `import_jobs` als pgSchema('cards').table(...) Definitionen.
//
// Schema-Skizze in mana/docs/playbooks/CARDS_GREENFIELD.md §"Drizzle-Schema-Skizze".
// Card-Type-Granularität (subIndex pro Karte) aus
// docs/LESSONS_FROM_MANA_MONOREPO.md mitnehmen.
import { pgSchema } from 'drizzle-orm/pg-core';
export const cardsSchema = pgSchema('cards');

26
apps/api/src/index.ts Normal file
View file

@ -0,0 +1,26 @@
import { Hono } from 'hono';
import { manifestRoute } from './routes/manifest.ts';
import { healthRoute } from './routes/health.ts';
const app = new Hono();
app.route('/', healthRoute);
app.route('/.well-known/mana-app.json', manifestRoute);
app.get('/', (c) =>
c.json({
app: 'cards',
version: process.env.CARDS_API_VERSION ?? '0.0.0',
see: '/.well-known/mana-app.json',
})
);
const port = Number(process.env.CARDS_API_PORT ?? 3081);
console.log(`[cards-api] listening on http://localhost:${port}`);
export default {
port,
fetch: app.fetch,
};

View file

@ -0,0 +1,27 @@
import { Hono } from 'hono';
export const healthRoute = new Hono();
healthRoute.get('/healthz', (c) => c.json({ status: 'ok' }));
healthRoute.get('/healthz/details', (c) =>
c.json({
status: 'ok',
app: 'cards',
version: process.env.CARDS_API_VERSION ?? '0.0.0',
uptime_s: Math.floor(process.uptime()),
mana_packages: {
// In Phase 5 mit @mana/shared-app-tpl ersetzen.
'@mana/shared-share-protocol': 'TBD',
'@mana/shared-app-tpl': 'TBD',
},
})
);
healthRoute.get('/version', (c) =>
c.json({
app: 'cards',
version: process.env.CARDS_API_VERSION ?? '0.0.0',
build: process.env.CARDS_BUILD_SHA ?? 'dev',
})
);

View file

@ -0,0 +1,7 @@
import { Hono } from 'hono';
import manifest from '../../../../app-manifest.json' with { type: 'json' };
export const manifestRoute = new Hono();
manifestRoute.get('/', (c) => c.json(manifest));

View file

@ -0,0 +1,24 @@
import { describe, it, expect } from 'vitest';
import { Hono } from 'hono';
import { healthRoute } from '../src/routes/health.ts';
describe('health routes', () => {
const app = new Hono();
app.route('/', healthRoute);
it('GET /healthz returns ok', async () => {
const res = await app.request('/healthz');
expect(res.status).toBe(200);
const body = (await res.json()) as { status: string };
expect(body.status).toBe('ok');
});
it('GET /version returns app + version + build', async () => {
const res = await app.request('/version');
expect(res.status).toBe(200);
const body = (await res.json()) as { app: string; version: string; build: string };
expect(body.app).toBe('cards');
expect(body.version).toBeTruthy();
expect(body.build).toBeTruthy();
});
});

14
apps/api/tsconfig.json Normal file
View file

@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"types": ["bun-types"],
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*", "tests/**/*", "drizzle.config.ts"],
"exclude": ["node_modules", "dist", ".turbo"]
}

29
apps/web/package.json Normal file
View file

@ -0,0 +1,29 @@
{
"name": "@cards/web",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Cards-Web — SvelteKit 2 + Svelte 5 Frontend für cardecky.mana.how.",
"scripts": {
"dev": "vite dev --port 3082 --host",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"test": "vitest run",
"lint": "echo 'lint configured later (eslint flat-config)'",
"clean": "rm -rf .svelte-kit build .turbo"
},
"dependencies": {
"@cards/domain": "workspace:*"
},
"devDependencies": {
"@sveltejs/adapter-node": "^5.2.0",
"@sveltejs/kit": "^2.8.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"vite": "^5.4.0",
"vitest": "^2.1.0"
}
}

13
apps/web/src/app.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

13
apps/web/src/app.html Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Cards — Karteikarten-App des Vereins mana e.V." />
<title>Cards</title>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View file

@ -0,0 +1,6 @@
// Re-exports for $lib/...
//
// Phase 4: api-clients, auth, components, stores, i18n.
// Heute: Stub.
export const APP_NAME = 'cards';

View file

@ -0,0 +1,20 @@
<script lang="ts">
let { children } = $props();
</script>
<main>
{@render children?.()}
</main>
<style>
main {
font-family:
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
max-width: 64rem;
margin: 0 auto;
padding: 2rem 1rem;
}
</style>

View file

@ -0,0 +1,16 @@
<script lang="ts">
// Phase-0-Skelett. Sobald Auth-Föderation steht (Phase 2),
// redirected diese Seite zu /(app) für eingeloggte User
// und zu auth.mana.how/login für anonyme.
</script>
<h1>Cards</h1>
<p>
Karteikarten-App des Vereins <strong>mana e.V.</strong>
— Phase 0, Repo-Skelett.
</p>
<p>
Plan: <a href="https://github.com/mana-ev/mana/blob/main/docs/playbooks/CARDS_GREENFIELD.md">
CARDS_GREENFIELD.md
</a>
</p>

View file

@ -0,0 +1,4 @@
User-agent: *
Disallow: /
# Cards ist eine private Verein-App, kein Index für Suchmaschinen.

15
apps/web/svelte.config.js Normal file
View file

@ -0,0 +1,15 @@
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
alias: {
$lib: 'src/lib',
},
},
};
export default config;

21
apps/web/tsconfig.json Normal file
View file

@ -0,0 +1,21 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
},
"include": [
".svelte-kit/ambient.d.ts",
".svelte-kit/non-ambient.d.ts",
".svelte-kit/types/**/$types.d.ts",
"vite.config.ts",
"src/**/*.js",
"src/**/*.ts",
"src/**/*.svelte",
"tests/**/*"
],
"exclude": ["node_modules", ".svelte-kit/output", "build"]
}

10
apps/web/vite.config.ts Normal file
View file

@ -0,0 +1,10 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
server: {
port: Number(process.env.CARDS_WEB_PORT ?? 3082),
host: true,
},
});

View file

@ -0,0 +1,203 @@
# 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.

View file

@ -0,0 +1,28 @@
# Lokales Cards-Dev-Setup. Postgres-Container auf 5435,
# damit der mana-Plattform-Stack auf 5432 ungestört weiterläuft.
#
# Start: pnpm docker:up (vom Repo-Root)
# Logs: pnpm docker:logs
# Stop: pnpm docker:down
#
# Daten persistieren in `infrastructure/.volumes/cards-postgres`,
# git-ignored.
services:
cards-postgres:
image: postgres:16-alpine
container_name: cards-postgres
restart: unless-stopped
environment:
POSTGRES_USER: cards
POSTGRES_PASSWORD: cards
POSTGRES_DB: cards
ports:
- '5435:5432'
volumes:
- ./.volumes/cards-postgres:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U cards -d cards']
interval: 5s
timeout: 3s
retries: 10

37
package.json Normal file
View file

@ -0,0 +1,37 @@
{
"name": "cards",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Cards — eigenständige föderierte Spaced-Repetition-App des mana e.V. Greenfield-Build (Strategie B), redet über mana-share/-mcp/-search/-events mit den anderen Verein-Apps.",
"packageManager": "pnpm@9.15.9",
"engines": {
"node": ">=20",
"pnpm": "^9"
},
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"test": "turbo run test",
"lint": "turbo run lint",
"type-check": "turbo run type-check",
"clean": "turbo run clean",
"format": "prettier --config .prettierrc.json --write \"**/*.{ts,tsx,svelte,js,jsx,json,md}\"",
"format:check": "prettier --config .prettierrc.json --check \"**/*.{ts,tsx,svelte,js,jsx,json,md}\"",
"validate:manifest": "pnpm exec validate-manifest app-manifest.json",
"validate:fast": "pnpm run type-check && pnpm run validate:manifest",
"validate:all": "pnpm run type-check && pnpm run validate:manifest && pnpm run test",
"docker:up": "docker compose -f infrastructure/docker-compose.yml up -d",
"docker:down": "docker compose -f infrastructure/docker-compose.yml down",
"docker:logs": "docker compose -f infrastructure/docker-compose.yml logs -f",
"db:push": "pnpm --filter @cards/api drizzle:push"
},
"devDependencies": {
"@types/bun": "latest",
"@types/node": "^22.10.2",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.6",
"turbo": "^2.3.0",
"typescript": "^5.6.3"
}
}

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"]
}

11
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,11 @@
packages:
- 'apps/*'
- 'packages/*'
# Akzeptierte Peer-Drift mit Mana-Plattform-Stack (siehe mana/pnpm-workspace.yaml).
# Cards startet auf gleichem Stand und migriert mit der Plattform mit.
peerDependencyRules:
allowedVersions:
drizzle-orm: '0.38'
drizzle-kit: '0.30'
zod: '3'

15
tsconfig.base.json Normal file
View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ESNext"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": false
}
}

43
turbo.json Normal file
View file

@ -0,0 +1,43 @@
{
"$schema": "https://turbo.build/schema.json",
"concurrency": "5",
"globalEnv": [
"NODE_ENV",
"DATABASE_URL",
"MANA_AUTH_URL",
"MANA_SHARE_URL",
"MANA_LINKS_URL",
"MANA_MCP_URL",
"MANA_SEARCH_URL",
"MANA_EVENTS_URL",
"MANA_CREDITS_URL",
"MANA_MEDIA_URL",
"MANA_SERVICE_KEY",
"CARDS_APP_SERVICE_KEY"
],
"tasks": {
"dev": {
"cache": false,
"persistent": true
},
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", "build/**", ".svelte-kit/**"]
},
"lint": {
"dependsOn": ["^lint"],
"outputs": []
},
"type-check": {
"dependsOn": ["^type-check", "^build"],
"outputs": []
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
},
"clean": {
"cache": false
}
}
}