diff --git a/tasks/v5.0.0-plan-02-sqlite.md b/tasks/v5.0.0-plan-02-sqlite.md new file mode 100644 index 0000000..d1fe976 --- /dev/null +++ b/tasks/v5.0.0-plan-02-sqlite.md @@ -0,0 +1,416 @@ +# Plan 02: SQLite-Foundation (Pillar 3) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** SQLite-Infrastruktur ins Projekt einbauen + Migrator, der bestehende JSON-Daten aus `C:\ProgramData\Twitch_VOD_Manager\*.json` nach `app.db` ueberfuehrt. JSON-Pfade bleiben Source-of-Truth — SQLite laeuft als Shadow-Schreibziel. Cutover (SQLite wird Master) erfolgt in einem spaeteren Plan. + +**Architecture:** `better-sqlite3` (sync embedded). Schema-First via Inline-Konstante (`schema-v5.ts`) damit `tsc` keine SQL-Files kopieren muss. WAL-Mode, busy_timeout 5000ms, foreign_keys ON. Migrator ist idempotent (Marker in `migrations_applied` Tabelle). Bei Crash bleibt JSON-Pfad funktionsfaehig. + +**Tech Stack:** better-sqlite3 + @types/better-sqlite3, vitest mit echten temp-DB-Files. + +**Verifikation pro Task:** `npm run build` + `npm run test:unit` gruen. + +--- + +## Out of Scope fuer Plan 02 + +- Reads aus SQLite umstellen (Renderer/IPC bleibt auf JSON) +- electron-rebuild fuer better-sqlite3 (Workaround in Plan 02, voller Pipeline-Fix in spaeterem Plan) +- Schema-Migrationen v5 → v6 +- Sub-only-Tabellen (kommen mit OAuth in Plan 03) + +--- + +## File Structure + +**Neu:** +- `src/main/infra/schema-v5.ts` — Schema als TypeScript-String-Konstante +- `src/main/infra/db.ts` — better-sqlite3 Wrapper +- `src/main/infra/db.test.ts` +- `src/main/domain/migrator.ts` — JSON to SQLite +- `src/main/domain/migrator.test.ts` + +**Modifiziert:** +- `package.json` +- `src/main.ts` — Migrator beim App-Start (fail-soft) +- `CLAUDE.md` +- `tasks/v5.0.0-roadmap.md` + +--- + +## Tasks + +### Task 1: better-sqlite3 installieren + +- [ ] `npm install --save better-sqlite3` +- [ ] `npm install --save-dev @types/better-sqlite3` +- [ ] Verify: `node -e "const D=require('better-sqlite3'); const d=new D(':memory:'); d.prepare('SELECT 1 AS x').get(); d.close(); console.log('ok')"` → "ok" +- [ ] `npm run build` Exit 0 +- [ ] Commit: `build: add better-sqlite3 + @types` + +--- + +### Task 2: schema-v5.ts (Schema als Inline-String) + +- [ ] Datei `src/main/infra/schema-v5.ts` mit `export const SCHEMA_V5_SQL = \`...sql...\`;` +- [ ] SQL-Inhalt umfasst Tabellen: `schema_meta`, `config_kv`, `queue_items`, `downloaded_vods`, `streamers`, `archive_files`, `migrations_applied`. Plus Indices. +- [ ] Alle Tabellen `IF NOT EXISTS`. `INSERT OR IGNORE INTO schema_meta(key, value) VALUES ('schema_version', '5')`. +- [ ] Commit: `feat(db): schema v5 inline (7 tables + indices)` + +**Schema-Tabellen-Spec (Spalten):** + +``` +schema_meta(key PK, value) +config_kv(key PK, value, updated_at) +queue_items(id PK, streamer_login, vod_id, clip_id, title, output_path, + status, progress_pct, error_message, created_at, updated_at, + completed_at, payload_json) + idx_queue_status(status), idx_queue_streamer(streamer_login), + idx_queue_created(created_at) +downloaded_vods(vod_id PK, downloaded_at default now) +streamers(login PK, auto_record, auto_vod_download, added_at default now) + idx_streamers_autorec(auto_record), idx_streamers_autodl(auto_vod_download) +archive_files(path PK, streamer_login, size_bytes, duration_seconds, + created_at, verified default 0) + idx_archive_streamer(streamer_login) +migrations_applied(name PK, applied_at default now, payload) +``` + +--- + +### Task 3: db.ts Wrapper + 5 Tests + +**Public DbHandle Interface:** +- `run(sql, params?)` — single-statement execute +- `get(sql, params?)` — first row or undefined +- `all(sql, params?)` — alle Rows +- `transaction(fn)` — atomares Bracket +- `runBatch(sql)` — multi-statement (intern via `_db.exec(sql)`, fuer Schema-Bootstrap) +- `close()` +- `raw` getter + +**openDatabase(filePath: string): DbHandle** +- Erstellt File (better-sqlite3 default-Verhalten) +- `pragma journal_mode = WAL`, `pragma busy_timeout = 5000`, `pragma foreign_keys = ON` +- Fuehrt Schema-Bootstrap aus: `runBatch(SCHEMA_V5_SQL)` +- Returnt das Handle-Objekt + +**Tests** (`src/main/infra/db.test.ts`): + +```typescript +import { test, expect, describe, beforeEach, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { openDatabase, type DbHandle } from './db'; + +let tmpDir: string; +let db: DbHandle | null = null; +beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'db-test-')); +}); +afterEach(() => { + try { db?.close(); } catch { /* ignore */ } + db = null; + try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ } +}); + +describe('openDatabase', () => { + test('creates a new file', () => { + const target = path.join(tmpDir, 'a.db'); + db = openDatabase(target); + expect(fs.existsSync(target)).toBe(true); + expect(typeof db.run).toBe('function'); + expect(typeof db.get).toBe('function'); + expect(typeof db.all).toBe('function'); + expect(typeof db.close).toBe('function'); + }); + + test('schema_meta row exists with schema_version=5', () => { + db = openDatabase(path.join(tmpDir, 'b.db')); + const row = db.get<{ value: string }>('SELECT value FROM schema_meta WHERE key = ?', ['schema_version']); + expect(row?.value).toBe('5'); + }); + + test('WAL mode active', () => { + db = openDatabase(path.join(tmpDir, 'c.db')); + const row = db.get<{ journal_mode: string }>('PRAGMA journal_mode'); + expect(row?.journal_mode).toBe('wal'); + }); + + test('idempotent open: existing file keeps schema_version=5', () => { + const target = path.join(tmpDir, 'd.db'); + db = openDatabase(target); + db.close(); + db = openDatabase(target); + const row = db.get<{ value: string }>('SELECT value FROM schema_meta WHERE key = ?', ['schema_version']); + expect(row?.value).toBe('5'); + }); + + test('run + get + all roundtrip', () => { + db = openDatabase(path.join(tmpDir, 'e.db')); + db.run('INSERT INTO downloaded_vods(vod_id) VALUES (?)', ['1234']); + db.run('INSERT INTO downloaded_vods(vod_id) VALUES (?)', ['5678']); + const one = db.get<{ vod_id: string }>('SELECT vod_id FROM downloaded_vods WHERE vod_id = ?', ['1234']); + expect(one?.vod_id).toBe('1234'); + const all = db.all<{ vod_id: string }>('SELECT vod_id FROM downloaded_vods ORDER BY vod_id'); + expect(all.map(r => r.vod_id)).toEqual(['1234', '5678']); + }); +}); +``` + +- [ ] Tests schreiben (sollen fehlschlagen — Modul fehlt) +- [ ] `npm run test:unit` zeigt 5 neue failures +- [ ] db.ts implementieren (better-sqlite3 wrappen, Interface oben einhalten) +- [ ] Tests passen — `npm run test:unit` Exit 0 +- [ ] Build gruen +- [ ] Commit: `feat(db): better-sqlite3 wrapper + schema bootstrap (5 tests)` + +--- + +### Task 4: Migrator + 8 Tests + +**MigratorOptions:** +- `db: DbHandle` +- `appDataDir: string` + +**MigrationResult:** +- `alreadyApplied: boolean` +- `configMigrated: boolean` +- `queueMigrated: boolean` +- `downloadedVodsCount: number` +- `streamersCount: number` +- `errors: MigrationError[]` (`{ source, message }`) + +**migrateJsonToSqlite(opts):** +1. Check `migrations_applied` fuer `name='v4-to-v5-jsons'` → wenn vorhanden → return `alreadyApplied: true` +2. Lies `appDataDir/config.json` falls vorhanden: + - Parse, in `db.transaction(() => { ... })`: + - Whitelisted Keys (siehe Liste unten) → `INSERT OR REPLACE INTO config_kv(key, value, updated_at) VALUES (?, ?, strftime('%s','now'))` mit `value = JSON.stringify(config[key])` + - `downloaded_vod_ids` (Array of String) → `INSERT OR IGNORE INTO downloaded_vods(vod_id) VALUES (?)` + - `auto_record_streamers` → normalizeLogin + `INSERT...ON CONFLICT DO UPDATE auto_record=1` + - `auto_vod_download_streamers` → normalizeLogin + `INSERT...ON CONFLICT DO UPDATE auto_vod_download=1` + - Bei Erfolg: `.v4-backup` Copy schreiben (`fs.copyFileSync`) + - Bei Fehler: error pushen, continue +3. Lies `appDataDir/download_queue.json` falls vorhanden: + - Parse, fuer jedes Item (mit `id`-Feld): `INSERT OR REPLACE INTO queue_items(...) VALUES (...)` + - Fields: id, streamer→normalizeLogin, vod_id, clip_id, title, output_path, status (default 'pending'), progress_pct, error_message, created_at, updated_at, completed_at, payload_json=JSON.stringify(item) + - Bei Erfolg: `.v4-backup` Copy +4. Schreibe Marker: `INSERT INTO migrations_applied(name, payload) VALUES ('v4-to-v5-jsons', JSON.stringify({summary}))` + +**Whitelisted Config-Keys fuer config_kv (verbatim):** + +``` +language, performance_mode, metadata_cache_minutes, streamlink_quality, +streamlink_disable_ads, download_chat_replay, capture_live_chat, +discord_webhook_url, discord_notify_live_start, discord_notify_live_end, +discord_notify_vod_complete, discord_notify_vod_auto_queued, +auto_cleanup_enabled, auto_cleanup_days, auto_cleanup_target, +auto_cleanup_action, log_stream_events, auto_vod_download_poll_minutes, +auto_vod_max_age_hours, auto_resume_live_recording, +auto_merge_resumed_parts, delete_parts_after_merge, +auto_record_poll_seconds, filename_template_vod, filename_template_parts, +filename_template_clip, smart_queue_scheduler, prevent_duplicate_downloads, +persist_queue_on_restart, auto_resume_queue_on_startup, +notify_on_each_completion +``` + +**Tests (verbatim, src/main/domain/migrator.test.ts):** + +```typescript +import { test, expect, describe, beforeEach, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { openDatabase, type DbHandle } from '../infra/db'; +import { migrateJsonToSqlite } from './migrator'; + +let tmpDir: string; +let appDataDir: string; +let db: DbHandle; +beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'migrator-')); + appDataDir = path.join(tmpDir, 'appdata'); + fs.mkdirSync(appDataDir, { recursive: true }); + db = openDatabase(path.join(tmpDir, 'app.db')); +}); +afterEach(() => { + db.close(); + try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ } +}); + +function writeJson(name: string, payload: unknown): string { + const target = path.join(appDataDir, name); + fs.writeFileSync(target, JSON.stringify(payload, null, 2), 'utf-8'); + return target; +} + +describe('migrateJsonToSqlite', () => { + test('no JSON files: writes migrations_applied marker', () => { + const result = migrateJsonToSqlite({ db, appDataDir }); + expect(result.configMigrated).toBe(false); + expect(result.queueMigrated).toBe(false); + expect(result.downloadedVodsCount).toBe(0); + expect(result.streamersCount).toBe(0); + + const marker = db.get<{ name: string }>('SELECT name FROM migrations_applied WHERE name = ?', ['v4-to-v5-jsons']); + expect(marker?.name).toBe('v4-to-v5-jsons'); + }); + + test('migrates config.json keys into config_kv', () => { + writeJson('config.json', { + language: 'de', + performance_mode: 'speed', + metadata_cache_minutes: 30, + downloaded_vod_ids: ['1', '2', '3'], + auto_record_streamers: ['foo', 'bar'], + }); + const result = migrateJsonToSqlite({ db, appDataDir }); + expect(result.configMigrated).toBe(true); + + const lang = db.get<{ value: string }>('SELECT value FROM config_kv WHERE key = ?', ['language']); + expect(JSON.parse(lang!.value)).toBe('de'); + + const perf = db.get<{ value: string }>('SELECT value FROM config_kv WHERE key = ?', ['performance_mode']); + expect(JSON.parse(perf!.value)).toBe('speed'); + }); + + test('migrates downloaded_vod_ids', () => { + writeJson('config.json', { downloaded_vod_ids: ['100', '200', '300'] }); + const result = migrateJsonToSqlite({ db, appDataDir }); + expect(result.downloadedVodsCount).toBe(3); + const rows = db.all<{ vod_id: string }>('SELECT vod_id FROM downloaded_vods ORDER BY vod_id'); + expect(rows.map(r => r.vod_id)).toEqual(['100', '200', '300']); + }); + + test('migrates streamers from both lists', () => { + writeJson('config.json', { + auto_record_streamers: ['Alice', '@bob'], + auto_vod_download_streamers: ['bob', 'carol'], + }); + const result = migrateJsonToSqlite({ db, appDataDir }); + expect(result.streamersCount).toBeGreaterThanOrEqual(3); + + const alice = db.get<{ login: string; auto_record: number }>('SELECT login, auto_record FROM streamers WHERE login = ?', ['alice']); + expect(alice?.auto_record).toBe(1); + + const bob = db.get<{ login: string; auto_record: number; auto_vod_download: number }>('SELECT login, auto_record, auto_vod_download FROM streamers WHERE login = ?', ['bob']); + expect(bob?.auto_record).toBe(1); + expect(bob?.auto_vod_download).toBe(1); + + const carol = db.get<{ login: string; auto_vod_download: number }>('SELECT login, auto_vod_download FROM streamers WHERE login = ?', ['carol']); + expect(carol?.auto_vod_download).toBe(1); + }); + + test('migrates download_queue.json items', () => { + writeJson('download_queue.json', [ + { id: 'q1', status: 'pending', streamer: 'foo', vod_id: 'v1', created_at: 1000, updated_at: 1000 }, + { id: 'q2', status: 'completed', streamer: 'bar', vod_id: 'v2', created_at: 2000, updated_at: 3000, completed_at: 3000 }, + ]); + const result = migrateJsonToSqlite({ db, appDataDir }); + expect(result.queueMigrated).toBe(true); + + const all = db.all<{ id: string; status: string }>('SELECT id, status FROM queue_items ORDER BY id'); + expect(all).toHaveLength(2); + expect(all[0].status).toBe('pending'); + expect(all[1].status).toBe('completed'); + }); + + test('idempotent second run', () => { + writeJson('config.json', { downloaded_vod_ids: ['1', '2'] }); + migrateJsonToSqlite({ db, appDataDir }); + const result2 = migrateJsonToSqlite({ db, appDataDir }); + expect(result2.alreadyApplied).toBe(true); + const count = db.get<{ c: number }>('SELECT COUNT(*) AS c FROM downloaded_vods'); + expect(count?.c).toBe(2); + }); + + test('writes .v4-backup of source JSONs', () => { + const configPath = writeJson('config.json', { language: 'en' }); + migrateJsonToSqlite({ db, appDataDir }); + expect(fs.existsSync(configPath + '.v4-backup')).toBe(true); + expect(fs.readFileSync(configPath + '.v4-backup', 'utf-8')).toContain('"language": "en"'); + }); + + test('malformed JSON is logged + skipped', () => { + fs.writeFileSync(path.join(appDataDir, 'config.json'), '{ not valid json', 'utf-8'); + const result = migrateJsonToSqlite({ db, appDataDir }); + expect(result.configMigrated).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + expect(result.errors[0].source).toBe('config.json'); + }); +}); +``` + +- [ ] Tests schreiben (failing — Modul fehlt) +- [ ] Migrator implementieren (gemaess Interface + Step-Liste oben) +- [ ] Tests passen — `npm run test:unit` Exit 0 +- [ ] Build gruen +- [ ] Commit: `feat(db): JSON to SQLite migrator (idempotent, fail-soft, 8 tests)` + +--- + +### Task 5: Migrator-Aufruf beim App-Start + +**main.ts:** im `app.whenReady().then(() => { ... })` Block, vor dem ersten `createWindow()`-Aufruf, einen try/catch-Block mit: +- `require('./main/infra/db')` und `require('./main/domain/migrator')` (lazy, damit Native-Fehler kein App-Start verhindern) +- `openDatabase(path.join(APPDATA_DIR, 'app.db'))` +- `migrateJsonToSqlite({ db, appDataDir: APPDATA_DIR })` +- Result ins debug.log loggen (falls `debugLog` Funktion vorhanden) +- `db.close()` im `finally` +- Catch: fail-soft, nur ins debug.log oder console.error + +- [ ] Code adden +- [ ] Build gruen +- [ ] `npm run test:e2e` Exit 0 (Playwright startet App, Migrator laeuft auf leerer AppData) +- [ ] Commit: `feat(db): wire migrator into app startup (fail-soft, lazy require)` + +--- + +### Task 6: Full Verification + Version Bump 5.0.0-alpha.1 + +- [ ] `npm run test:e2e:release` Exit 0 (build + unit (mit jetzt >=104 Tests) + update-logic + 3 Playwright-Stages) +- [ ] `npm version 5.0.0-alpha.1 --no-git-tag-version` +- [ ] `npm run build` Exit 0 +- [ ] Commit: `release: 5.0.0-alpha.1 - SQLite migrator (shadow write, JSON stays master)` +- [ ] Tag: `git tag v5.0.0-alpha.1` + +--- + +### Task 7: CLAUDE.md + Roadmap Update + +**CLAUDE.md Key Patterns / Persistence:** +> JSON files in `C:\ProgramData\Twitch_VOD_Manager\` bleiben Source-of-Truth. Seit v5.0.0-alpha.1 spiegelt der Migrator (`src/main/domain/migrator.ts`) Konfiguration, Queue, downloaded_vod_ids und Streamer-Listen einmalig in `app.db` (SQLite, better-sqlite3). Schema: `src/main/infra/schema-v5.ts`. Cutover (SQLite wird Master) erfolgt in einem spaeteren Plan. + +**Roadmap:** Plan 02 Status → `DONE (v5.0.0-alpha.1)`, Plan 03 wird `NEXT`. + +- [ ] CLAUDE.md aktualisieren +- [ ] Roadmap aktualisieren +- [ ] Commit: `docs: SQLite migrator pattern + roadmap status update` + +--- + +## Self-Review + +- [ ] Tests echt (kein Skip / Stub) +- [ ] Idempotenz getestet +- [ ] Malformed-JSON getestet +- [ ] `.v4-backup` Copy getestet +- [ ] Fail-Soft Pfad in main.ts: App startet auch ohne native-build-success +- [ ] Spec-Coverage: Pillar 3 SQLite-Migrator + Schema v5 abgedeckt. Cutover bleibt fuer spaeter + +## Done-Definition + +1. `npm run test:unit` Exit 0 mit >= 13 neuen Tests (5 db + 8 migrator) +2. `npm run test:e2e:release` Exit 0 +3. SQLite `app.db` wird beim ersten Start angelegt +4. Migrator idempotent (zweimaliger Run) +5. `.v4-backup` Files existieren +6. Version 5.0.0-alpha.1 getaggt +7. CLAUDE.md + Roadmap aktualisiert + +--- + +## Execution Handoff + +Inline-Execution via `superpowers:executing-plans`. Naechster Plan (`tasks/v5.0.0-plan-03-oauth.md`) nach Done-Definition. diff --git a/tasks/v5.0.0-roadmap.md b/tasks/v5.0.0-roadmap.md index cc0a7b0..2fd3843 100644 --- a/tasks/v5.0.0-roadmap.md +++ b/tasks/v5.0.0-roadmap.md @@ -24,28 +24,35 @@ Beim Vergleich der echten Codebase (`src/main.ts` 7485 LoC mit 30+ Sektionen) ge --- -## Execution Order +## Execution Order (reordered post Plan 01) ``` -Plan 01: Foundation — Vitest + Pure-Utility-Extraction [START HIER] - └─> Plan 02: Domain Modules (twitch-api, config, queue split) - └─> Plan 03: Domain Modules (download, record, cutter, stats) - └─> Plan 04: Final main.ts Split (ipc + index.ts + delete old main.ts) - └─> Plan 05: SQLite Migration (P3) - └─> Plan 06: OAuth (P2) - ├─> Plan 07: Live-Recording Polish + Sub-only (P1) - ├─> Plan 08: Auto-Discovery Erweiterung (P7) - └─> Plan 09: UI Power-Features (P5) - └─> Plan 10: Smart-Resume Finalisierung (P6) - └─> Plan 11: v5.0.0-rc.1 → GA +Plan 01: Foundation — Vitest + Pure-Utility-Extraction [DONE — v5.0.0-alpha.0] + └─> Plan 02: SQLite-Foundation (P3) [NEXT] + └─> Plan 03: OAuth Device Code (P2) + └─> Plan 04: Live-Recording Polish + Sub-only (P1) + ├─> Plan 05: Auto-Discovery Erweiterung (P7) + └─> Plan 06: UI Power-Features (P5) + └─> Plan 07: Smart-Resume Finalisierung (P6) + └─> Plan 08: Architektur-Split Pt. 1 (state-coupled modules) + └─> Plan 09: Architektur-Split Pt. 2 (final split) + └─> Plan 10: v5.0.0-rc.1 → GA ``` -Plan 01-04 = Pillar 4 (Architektur-Split, monolithische Aufgabe in 4 Teile gesplittet). -Plan 05 = Pillar 3. -Plan 06 = Pillar 2. -Plan 07-09 = Pillars 1, 7, 5 parallelisierbar (verschiedene Subsysteme). -Plan 10 = Pillar 6. -Plan 11 = Release-Phase. +**Begruendung der Umordnung:** Plan 01 hat das Pure-Extraktion-Pattern validiert (Pillar 4 Teil-1). +Weiter splitten der state-gekoppelten Module braucht zuerst eine State-Container-Strategie — das ist +besser informiert nachdem Features (SQLite, OAuth, Live-Rec) ins System gehen und die echten +Querverbindungen sichtbar sind. Bis dahin lebt das Pattern aus Plan 01 weiter (neue Module landen +in `src/main/{infra,domain}/`), nur die alten in main.ts steckenden Stateful-Funktionen bleiben dort. + +Plan 02 = Pillar 3 (SQLite). +Plan 03 = Pillar 2 (OAuth). +Plan 04 = Pillar 1. +Plan 05 = Pillar 7. +Plan 06 = Pillar 5. +Plan 07 = Pillar 6. +Plan 08-09 = Pillar 4 Rest. +Plan 10 = Release. --- @@ -53,32 +60,31 @@ Plan 11 = Release-Phase. | # | Plan | Datei | Status | |---|---|---|---| -| 01 | Foundation: Vitest + Pure-Utility-Extraction | `tasks/v5.0.0-plan-01-foundation.md` | bereit, wird ausgefuehrt | -| 02 | Domain Split Pt. 1 | `tasks/v5.0.0-plan-02-domain-pt1.md` | erstellt nach Plan 01 | -| 03 | Domain Split Pt. 2 | `tasks/v5.0.0-plan-03-domain-pt2.md` | nach Plan 02 | -| 04 | Final Architektur-Split | `tasks/v5.0.0-plan-04-final-split.md` | nach Plan 03 | -| 05 | SQLite-Migration | `tasks/v5.0.0-plan-05-sqlite.md` | nach Plan 04 | -| 06 | OAuth | `tasks/v5.0.0-plan-06-oauth.md` | nach Plan 05 | -| 07 | Live-Rec Polish + Sub-only | `tasks/v5.0.0-plan-07-live-rec.md` | nach Plan 06 | -| 08 | Auto-Discovery Erweiterung | `tasks/v5.0.0-plan-08-auto-disc.md` | nach Plan 06 | -| 09 | UI Power | `tasks/v5.0.0-plan-09-ui-power.md` | nach Plan 06 | -| 10 | Smart-Resume Final | `tasks/v5.0.0-plan-10-smart-resume.md` | nach Plan 09 | -| 11 | Release 5.0.0 GA | `tasks/v5.0.0-plan-11-release.md` | nach Plan 10 | +| 01 | Foundation: Vitest + Pure-Utility-Extraction | `tasks/v5.0.0-plan-01-foundation.md` | **DONE** (v5.0.0-alpha.0) | +| 02 | SQLite-Foundation (Pillar 3) | `tasks/v5.0.0-plan-02-sqlite.md` | **NEXT** | +| 03 | OAuth Device Code (Pillar 2) | `tasks/v5.0.0-plan-03-oauth.md` | nach Plan 02 | +| 04 | Live-Recording Polish (Pillar 1) | `tasks/v5.0.0-plan-04-live-rec.md` | nach Plan 03 | +| 05 | Auto-Discovery Erweiterung (Pillar 7) | `tasks/v5.0.0-plan-05-auto-disc.md` | nach Plan 04 | +| 06 | UI Power (Pillar 5) | `tasks/v5.0.0-plan-06-ui-power.md` | nach Plan 04 | +| 07 | Smart-Resume Final (Pillar 6) | `tasks/v5.0.0-plan-07-smart-resume.md` | nach Plan 06 | +| 08 | Architektur-Split Pt. 1 (Pillar 4) | `tasks/v5.0.0-plan-08-split-pt1.md` | nach Plan 07 | +| 09 | Architektur-Split Pt. 2 (Pillar 4) | `tasks/v5.0.0-plan-09-split-pt2.md` | nach Plan 08 | +| 10 | Release 5.0.0 GA | `tasks/v5.0.0-plan-10-release.md` | nach Plan 09 | --- -## Versionsstrategie +## Versionsstrategie (post Reorder) | Plan abgeschlossen | Version | |---|---| -| Plan 01 | 5.0.0-alpha.0 (Foundation, opt-in via `next` channel) | -| Plan 02 | 5.0.0-alpha.1 | -| Plan 03 | 5.0.0-alpha.2 | -| Plan 04 | 5.0.0-alpha.3 | -| Plan 05 | 5.0.0-beta.0 (SQLite Migrator live, Breaking) | -| Plan 06 | 5.0.0-beta.1 (OAuth live) | -| Plan 07 + 08 + 09 | 5.0.0-beta.2, .3, .4 | -| Plan 10 | 5.0.0-rc.1 | -| Plan 11 | 5.0.0 (stable) | +| Plan 01 | **5.0.0-alpha.0** Foundation, vitest + pure modules (DONE) | +| Plan 02 | 5.0.0-alpha.1 SQLite Migrator (Breaking, opt-in next-channel) | +| Plan 03 | 5.0.0-alpha.2 OAuth | +| Plan 04 | 5.0.0-beta.0 Live-Recording Polish + Sub-only | +| Plan 05 | 5.0.0-beta.1 Auto-Discovery | +| Plan 06 | 5.0.0-beta.2 UI Power | +| Plan 07 | 5.0.0-beta.3 Smart-Resume | +| Plan 08-09 | 5.0.0-rc.0 / rc.1 Architektur-Split | +| Plan 10 | 5.0.0 stable | -Pro Plan: ein Release-Tag, Auto-Updater Channel `next`. 4.6-User bleiben auf `stable` und kriegen nur Bugfix-Patches der 4.6 Linie (sofern bestellt). +Pro Plan: ein Release-Tag, Auto-Updater Channel `next`. 4.6-User bleiben auf `stable`.