Phase 3 follow-up: type-check + tests grün, ts-fsrs v5 API

- tsconfig.base.json: allowImportingTsExtensions + noEmit (.ts-Imports
  in dev, kein tsc-Output, vitest/bun/vite handhaben Build)
- ts-fsrs v5.3.2 API-Updates:
  - scheduler.next(card, now, grade) statt repeat(card, now)[rating].card
  - Grade-Type für RATING_TO_FSRS (excluded Manual)
  - learning_steps-Feld auf Review (Schema, Drizzle-Column, Adapter,
    DTO-Konverter, Tests)
- apps/web: extends .svelte-kit/tsconfig.json (SvelteKit-Empfehlung),
  test-Script mit --passWithNoTests
- apps/api: dropped types: ['bun-types'] (stale)
- pnpm-lock.yaml committed

Status:
- pnpm run type-check  4/4 packages grün (api, domain, web mit
  svelte-check 0 errors)
- pnpm run test  46 Tests grün (cards-domain: 27, apps/api: 19,
  apps/web: --passWithNoTests)
- pnpm install  136 packages, 8s

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till 2026-05-08 14:41:04 +02:00
parent 45a47e0ffd
commit 5f67bd9f3e
11 changed files with 2345 additions and 35 deletions

View file

@ -30,6 +30,7 @@ export const reviews = cardsSchema.table(
difficulty: real('difficulty').notNull(), difficulty: real('difficulty').notNull(),
elapsedDays: real('elapsed_days').notNull().default(0), elapsedDays: real('elapsed_days').notNull().default(0),
scheduledDays: real('scheduled_days').notNull().default(0), scheduledDays: real('scheduled_days').notNull().default(0),
learningSteps: integer('learning_steps').notNull().default(0),
reps: integer('reps').notNull().default(0), reps: integer('reps').notNull().default(0),
lapses: integer('lapses').notNull().default(0), lapses: integer('lapses').notNull().default(0),
state: text('state', { enum: ['new', 'learning', 'review', 'relearning'] }) state: text('state', { enum: ['new', 'learning', 'review', 'relearning'] })

View file

@ -71,6 +71,7 @@ export function cardsRouter(deps: CardsDeps = {}): Hono<{ Variables: AuthVars }>
difficulty: r.difficulty, difficulty: r.difficulty,
elapsedDays: r.elapsed_days, elapsedDays: r.elapsed_days,
scheduledDays: r.scheduled_days, scheduledDays: r.scheduled_days,
learningSteps: r.learning_steps,
reps: r.reps, reps: r.reps,
lapses: r.lapses, lapses: r.lapses,
state: r.state, state: r.state,

View file

@ -122,6 +122,7 @@ export function reviewsRouter(deps: ReviewsDeps = {}): Hono<{ Variables: AuthVar
difficulty: next.difficulty, difficulty: next.difficulty,
elapsedDays: next.elapsed_days, elapsedDays: next.elapsed_days,
scheduledDays: next.scheduled_days, scheduledDays: next.scheduled_days,
learningSteps: next.learning_steps,
reps: next.reps, reps: next.reps,
lapses: next.lapses, lapses: next.lapses,
state: next.state, state: next.state,
@ -152,6 +153,7 @@ function toReviewDto(row: typeof reviews.$inferSelect): DomainReview {
difficulty: row.difficulty, difficulty: row.difficulty,
elapsed_days: row.elapsedDays, elapsed_days: row.elapsedDays,
scheduled_days: row.scheduledDays, scheduled_days: row.scheduledDays,
learning_steps: row.learningSteps,
reps: row.reps, reps: row.reps,
lapses: row.lapses, lapses: row.lapses,
state: row.state, state: row.state,

View file

@ -2,8 +2,6 @@
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src",
"types": ["bun-types"],
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"]

View file

@ -10,7 +10,7 @@
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"test": "vitest run", "test": "vitest run --passWithNoTests",
"lint": "echo 'lint configured later (eslint flat-config)'", "lint": "echo 'lint configured later (eslint flat-config)'",
"clean": "rm -rf .svelte-kit build .turbo" "clean": "rm -rf .svelte-kit build .turbo"
}, },

View file

@ -1,21 +1,12 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"strict": true,
"allowJs": true, "allowJs": true,
"checkJs": true, "checkJs": true,
"sourceMap": true, "sourceMap": true,
"strict": true, "moduleResolution": "bundler",
"moduleResolution": "bundler" "allowImportingTsExtensions": true,
}, "noEmit": true
"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"]
} }

View file

@ -8,20 +8,18 @@
import { import {
createEmptyCard, createEmptyCard,
default_w,
FSRS, FSRS,
generatorParameters,
Rating as FsrsRating, Rating as FsrsRating,
State as FsrsState, State as FsrsState,
type Card as FsrsCard, type Card as FsrsCard,
type FSRSParameters, type Grade,
} from 'ts-fsrs'; } from 'ts-fsrs';
import type { Rating, Review, ReviewState } from './schemas/review.ts'; import type { Rating, Review, ReviewState } from './schemas/review.ts';
import type { FsrsSettings } from './schemas/fsrs-settings.ts'; import type { FsrsSettings } from './schemas/fsrs-settings.ts';
/** Public Rating ↔ ts-fsrs Rating mapping. */ /** Public Rating ↔ ts-fsrs Grade (Manual ist explizit ausgeschlossen). */
const RATING_TO_FSRS: Record<Rating, FsrsRating> = { const RATING_TO_FSRS: Record<Rating, Grade> = {
again: FsrsRating.Again, again: FsrsRating.Again,
hard: FsrsRating.Hard, hard: FsrsRating.Hard,
good: FsrsRating.Good, good: FsrsRating.Good,
@ -45,13 +43,14 @@ const STATE_TO_FSRS: Record<ReviewState, FsrsState> = {
/** Baut einen FSRS-Scheduler aus per-Deck-Settings + globalen Defaults. */ /** Baut einen FSRS-Scheduler aus per-Deck-Settings + globalen Defaults. */
export function buildScheduler(settings: FsrsSettings = {}): FSRS { export function buildScheduler(settings: FsrsSettings = {}): FSRS {
const params: FSRSParameters = generatorParameters({ return new FSRS({
...(settings.request_retention !== undefined && {
request_retention: settings.request_retention, request_retention: settings.request_retention,
maximum_interval: settings.maximum_interval, }),
w: settings.w ?? default_w, ...(settings.maximum_interval !== undefined && { maximum_interval: settings.maximum_interval }),
enable_fuzz: settings.enable_fuzz ?? true, ...(settings.w !== undefined && { w: settings.w }),
...(settings.enable_fuzz !== undefined && { enable_fuzz: settings.enable_fuzz }),
}); });
return new FSRS(params);
} }
/** Initialer Review-State für eine neue Karte (sub_index). */ /** Initialer Review-State für eine neue Karte (sub_index). */
@ -72,6 +71,7 @@ export function newReview(args: {
difficulty: fc.difficulty, difficulty: fc.difficulty,
elapsed_days: fc.elapsed_days, elapsed_days: fc.elapsed_days,
scheduled_days: fc.scheduled_days, scheduled_days: fc.scheduled_days,
learning_steps: fc.learning_steps,
reps: fc.reps, reps: fc.reps,
lapses: fc.lapses, lapses: fc.lapses,
state: STATE_FROM_FSRS[fc.state], state: STATE_FROM_FSRS[fc.state],
@ -88,10 +88,8 @@ export function gradeReview(
): Review { ): Review {
const reviewedAt = now ?? new Date(); const reviewedAt = now ?? new Date();
const scheduler = buildScheduler(settings); const scheduler = buildScheduler(settings);
const fc = toFsrsCard(current); const result = scheduler.next(toFsrsCard(current), reviewedAt, RATING_TO_FSRS[rating]);
const log = scheduler.repeat(fc, reviewedAt); return fromFsrsCard(current, result.card);
const next = log[RATING_TO_FSRS[rating]].card;
return fromFsrsCard(current, next);
} }
/** Konvertiert unseren Review-Datensatz in eine ts-fsrs Card. */ /** Konvertiert unseren Review-Datensatz in eine ts-fsrs Card. */
@ -102,6 +100,7 @@ export function toFsrsCard(r: Review): FsrsCard {
difficulty: r.difficulty, difficulty: r.difficulty,
elapsed_days: r.elapsed_days, elapsed_days: r.elapsed_days,
scheduled_days: r.scheduled_days, scheduled_days: r.scheduled_days,
learning_steps: r.learning_steps,
reps: r.reps, reps: r.reps,
lapses: r.lapses, lapses: r.lapses,
state: STATE_TO_FSRS[r.state], state: STATE_TO_FSRS[r.state],
@ -118,6 +117,7 @@ export function fromFsrsCard(prev: Review, fc: FsrsCard): Review {
difficulty: fc.difficulty, difficulty: fc.difficulty,
elapsed_days: fc.elapsed_days, elapsed_days: fc.elapsed_days,
scheduled_days: fc.scheduled_days, scheduled_days: fc.scheduled_days,
learning_steps: fc.learning_steps,
reps: fc.reps, reps: fc.reps,
lapses: fc.lapses, lapses: fc.lapses,
state: STATE_FROM_FSRS[fc.state], state: STATE_FROM_FSRS[fc.state],

View file

@ -17,6 +17,7 @@ export const ReviewSchema = z
difficulty: z.number().min(0).max(10), difficulty: z.number().min(0).max(10),
elapsed_days: z.number().nonnegative().default(0), elapsed_days: z.number().nonnegative().default(0),
scheduled_days: z.number().nonnegative().default(0), scheduled_days: z.number().nonnegative().default(0),
learning_steps: z.number().int().nonnegative().default(0),
reps: z.number().int().nonnegative().default(0), reps: z.number().int().nonnegative().default(0),
lapses: z.number().int().nonnegative().default(0), lapses: z.number().int().nonnegative().default(0),
state: ReviewStateSchema.default('new'), state: ReviewStateSchema.default('new'),

View file

@ -2,7 +2,6 @@
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src",
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"sourceMap": true "sourceMap": true

2315
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -10,6 +10,8 @@
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"verbatimModuleSyntax": false "verbatimModuleSyntax": false,
"allowImportingTsExtensions": true,
"noEmit": true
} }
} }