managarten/docs/plans/writing-module.md
Till JS 3c3b2ebbc7 feat(writing): M1+M2 — new Ghostwriter module with manual draft CRUD
M1 (skeleton):
- Module `writing` registered: 4 Dexie tables (writingDrafts,
  writingDraftVersions, writingGenerations, writingStyles) in v43,
  encrypted via typed registry entries, space-scoped via the Dexie hook.
- App entry in mana-apps.ts (sky-cyan #0ea5e9, LOCAL TIER PATCH guest),
  fountain-pen icon in app-icons.ts.
- Plan: docs/plans/writing-module.md — 12 milestones, Ghostwriter-first
  with Canvas deferred to M9, Picture-pattern analogue (Draft + Version
  + Generation), 9 preset styles, Space-Kontext-as-default.

M2 (manual CRUD):
- drafts store: createDraft (atomic draft + initial v1), updateBriefing,
  setStatus, toggleFavorite, deleteDraft (cascade soft-delete versions),
  updateVersionContent (live edit), createCheckpointVersion,
  restoreVersion (pointer flip, non-destructive), setVisibility.
- styles store: createStyle, updateStyle, upsertExtractedPrinciples,
  setSpaceDefault (exclusive flip), deleteStyle.
- queries: useAllDrafts, useDraft, useVersionsForDraft,
  useCurrentVersionForDraft (follows the pointer so restoreVersion shows
  up in the editor), useGenerationsForDraft, useAllStyles + helpers.
- UI: KindTabs (shows only kinds with drafts), StatusBadge, StatusFilter,
  DraftCard (<button> for a11y), BriefingForm (topic/kind/audience/tone/
  length/language/extra), VersionEditor (500ms debounce + onBlur flush),
  VersionHistory (restore button per version).
- Routes: /writing list + /writing/draft/[id] with {#key id} remounting.

User flow: create draft from briefing → land in detail view → type →
autosave → "Als Checkpoint speichern" for a new version → restore any
older version from the history panel. No AI yet; M3 wires mana-llm for
short-form generation and M7 switches to mana-ai missions for long-form.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:59:56 +02:00

458 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Writing — Module Plan
## Status (2026-04-24)
**Planung.** Noch nichts geshipped. Nächster Schritt: M1 (Skelett).
## Ziel
Ein Modul, mit dem der Nutzer dem AI-Agenten Brief + Stil + Referenzen gibt und **fertige Texte** produziert: Blogposts, Essays, Mails, Bewerbungen, Social Posts, Reden, Storys, Produkttexte. Kernfrage: *"Ich brauche einen Text zu X im Stil Y — schreib ihn."*
**Start-Modus: Ghostwriter.** Input → fertiger Entwurf. Nutzer bewertet ganze Versionen, verfeinert Stellen gezielt mit Selection-Tools. Ein späterer **Canvas-Modus** (freies Tippen, Inline-Autocomplete, `/`-Kommandos) ist als M9 eingeplant, aber nicht Teil des Kern-Scopes.
Nicht im Scope Phase 1:
- Freies Notizen/Journalen (→ `notes` / `journal`)
- Speichern externer Artikel (→ `articles`)
- Kollaboratives Echtzeit-Editing
- Automatische Veröffentlichung (Hand-Off zu `website` / `articles` schon, aber User löst aus)
## Abgrenzung
| Modul | Unterschied |
|---|---|
| `notes` | unstrukturierte Snippets, persönlich, ohne Zweck |
| `journal` | datierte Reflexionen, persönlich |
| `articles` | **konsumierte** Artikel (Readability-Extrakt), Highlights — hier wird gelesen, nicht produziert |
| `chat` | Gespräch, nicht produzierter Text als Artefakt |
| `presi` / `website` | Konsumenten von Text — können aus Writing-Drafts gespeist werden |
| `news-research` / `mana-research` | Recherche-Provider; Writing **konsumiert** diese Quellen als Referenz |
Writing = **intentional produzierter Prosa-Text mit Zweck und Adressat**. Existiert heute nicht.
## Getroffene Entscheidungen (vorab, 2026-04-24)
1. **Ghostwriter-Modus zuerst**, Canvas später.
2. **Styles ≠ Personas**, aber verknüpfbar. Personas (`mana-persona-runner`) bleiben für Agent-Loops; Writing hat eigene `WritingStyle`-Entität. Eine Persona kann einen `defaultWritingStyleId` referenzieren — so nutzt z.B. ein "Marketing-Agent" automatisch den "Corporate Tone"-Style.
3. **Versionierung**: Jede *volle* Generierung/Regeneration → neue `LocalDraftVersion`. Selection-basierte Refinements (Shorten/Expand/Tone) modifizieren die aktuelle Version in-place mit lokalem Undo-Stack, ohne Versions-Explosion. Erst wenn der User "Diese Änderungen übernehmen als neue Version" klickt, wird eine Version geschrieben.
4. **Kind-Liste breit von Anfang an**: `blog`, `essay`, `email`, `social`, `story`, `letter`, `speech`, `cover-letter`, `product-description`, `press-release`, `bio`, `other`. Start mit vollem Set — Templates pro Kind kommen später. Das Discriminator-Feld ist billig; nachträglich einen Kind umzubenennen ist teurer.
5. **Space-Kontext als Default-Stil**: In einem Firmen-/Team-Space wird ein `spaceDefaultStyleId` unterstützt. Der Space kann "Corporate Tone" + standardmäßig verknüpfte kontextDocs als Default-Referenzen setzen. Personal-Space → kein Default, User wählt Style pro Draft.
## Modul-Struktur
```
apps/mana/apps/web/src/lib/modules/writing/
├── types.ts # LocalDraft, Draft, Kind, Status, LocalGeneration, LocalDraftVersion, LocalWritingStyle
├── collections.ts # drafts + draftVersions + generations + writingStyles Tables + Guest-Seed
├── queries.ts # useAllDrafts, useDraftsByKind, useDraft(id), useVersions(draftId), useStyles, useStats
├── stores/
│ ├── drafts.svelte.ts # createDraft, updateBriefing, deleteDraft, setVisibility, publishVersion, restoreVersion
│ ├── generations.svelte.ts # startGeneration, cancelGeneration, applyGenerationAsVersion
│ └── styles.svelte.ts # createStyle, updateStyle, trainStyleFromSamples, deleteStyle
├── components/
│ ├── BriefingForm.svelte # topic, kind, length, tone, audience, language, style-picker, reference-picker
│ ├── DraftCard.svelte # kompakter Listeneintrag (Titel + kind-Badge + Preview + Last-Updated + Visibility-Icon)
│ ├── KindTabs.svelte # Alle | Blog | Essay | E-Mail | Social | Story | Brief | Rede | ...
│ ├── StatusBadge.svelte # entwurf | in-überarbeitung | fertig | veröffentlicht
│ ├── StylePicker.svelte # Preset-Liste + Custom-Styles + "Schreibe wie ich"-Option
│ ├── ReferencePicker.svelte # cross-modul Picker (articles, notes, library, kontext, goals, URLs)
│ ├── VersionHistory.svelte # vertikale Timeline aller Versions, Diff auf Click, Revert-Button
│ ├── DiffView.svelte # seitlicher oder Inline-Diff zwischen zwei Versionen
│ ├── SelectionToolbar.svelte # erscheint bei Text-Markierung: Kürzen / Erweitern / Ton / Umschreiben / Übersetzen
│ ├── GenerationStatus.svelte # Fortschritts-UI während Generation läuft (Streaming-Preview)
│ └── ProposalInbox.svelte # Refine-Vorschläge, die auf User-Approval warten
├── views/
│ ├── ListView.svelte # Modul-Root: KindTabs + Grid of DraftCards + "+ Neuer Draft"-FAB
│ └── DetailView.svelte # Drei-Spalten-Layout (Briefing | Text | Tools)
├── tools.ts # AI-Tools (siehe AI-Integration)
├── constants.ts # KIND_LABELS, TONE_PRESETS, LENGTH_PRESETS, STYLE_PRESETS
├── presets/
│ └── styles.ts # Preset-Styles: Akademisch, LinkedIn, Hemingway, Casual-Blog, Buzzfeed-Listicle, Nachrichten, ...
├── module.config.ts # { appId: 'writing', tables: [{ name: 'drafts' }, { name: 'draftVersions' }, { name: 'generations' }, { name: 'writingStyles' }] }
└── index.ts # Re-Exports
```
## Daten-Schema
### `LocalDraft` (Dexie)
```typescript
export type DraftKind =
| 'blog' | 'essay' | 'email' | 'social' | 'story'
| 'letter' | 'speech' | 'cover-letter'
| 'product-description' | 'press-release' | 'bio' | 'other';
export type DraftStatus = 'draft' | 'refining' | 'complete' | 'published';
export interface LocalDraft extends BaseRecord {
kind: DraftKind; // plaintext — Diskriminator
status: DraftStatus; // plaintext — filterbar
title: string; // encrypted
briefing: { // encrypted — Kern-Eingabe
topic: string;
audience?: string;
tone?: string; // z.B. "sachlich", "humorvoll", "motivierend"
language: string; // ISO-Code, default 'de'
targetLength?: { // optional — default abgeleitet von kind
type: 'words' | 'chars' | 'minutes';
value: number;
};
extraInstructions?: string;
};
styleId?: string | null; // plaintext — FK auf LocalWritingStyle, null = Ad-hoc
styleOverrides?: { // encrypted — Style-Felder, die diesen Draft übersteuern
tone?: string;
styleNotes?: string;
} | null;
references: DraftReference[]; // plaintext IDs + URLs; encrypted Notes
currentVersionId?: string | null; // plaintext — zeigt auf aktive Version
visibility: VisibilityLevel; // plaintext
visibilityChangedAt?: string | null; // plaintext
visibilityChangedBy?: string | null; // plaintext (userId)
unlistedToken?: string | null; // plaintext — minted beim Flip auf 'unlisted'
publishedTo?: DraftPublishTarget[]; // plaintext — ['website:block/abc', 'articles:xyz']
isFavorite: boolean; // plaintext
}
export interface DraftReference {
kind: 'article' | 'note' | 'library' | 'kontext' | 'goal' | 'url' | 'me-image';
targetId?: string; // plaintext, module-lokal
url?: string; // plaintext
note?: string; // encrypted — was der User an dieser Quelle relevant findet
}
export type DraftPublishTarget = {
module: 'website' | 'articles' | 'social-relay' | 'mail' | 'presi';
targetId: string;
publishedAt: string; // ISO
};
```
### `LocalDraftVersion`
```typescript
export interface LocalDraftVersion extends BaseRecord {
draftId: string; // plaintext — FK
versionNumber: number; // plaintext — 1, 2, 3...
content: string; // encrypted — der Text selbst (Markdown)
wordCount: number; // plaintext
generationId?: string | null; // plaintext — falls AI-generiert
isAiGenerated: boolean; // plaintext
parentVersionId?: string | null; // plaintext — für Branching später
summary?: string | null; // encrypted — optional Auto-Summary fürs History-Panel
}
```
Selection-basierte Refinements erzeugen **keine** neue Version; sie mutieren den `content` der aktuellen Version. Ein Undo-Stack bleibt im lokalen State (nicht synced). "Als neue Version speichern" ist ein expliziter Button.
### `LocalGeneration`
```typescript
export type GenerationStatus = 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled';
export type GenerationKind =
| 'outline' // Outline aus Briefing
| 'draft-from-brief' // Volltext aus Briefing (direkt)
| 'draft-from-outline' // Volltext aus Outline
| 'selection-rewrite' // Mark. Passage umschreiben
| 'selection-shorten' | 'selection-expand'
| 'selection-tone' | 'selection-translate'
| 'full-regenerate';
export interface LocalGeneration extends BaseRecord {
draftId: string; // plaintext
kind: GenerationKind; // plaintext
status: GenerationStatus; // plaintext
prompt: string; // encrypted — finaler zusammengebauter Prompt
provider: 'mana-ai' | 'mana-llm' | 'local-llm'; // plaintext
model?: string | null; // plaintext — z.B. "claude-opus-4-7"
params?: { // plaintext
temperature?: number;
maxTokens?: number;
} | null;
inputSelection?: { start: number; end: number } | null; // plaintext — nur bei selection-*
output?: string | null; // encrypted — was generiert wurde
outputVersionId?: string | null; // plaintext — FK falls als Version gespeichert
startedAt?: string | null; // plaintext
completedAt?: string | null; // plaintext
durationMs?: number | null; // plaintext
tokenUsage?: { input: number; output: number } | null; // plaintext
error?: string | null; // plaintext — User-lesbarer Fehler
missionId?: string | null; // plaintext — FK zu mana-ai mission, falls async
}
```
### `LocalWritingStyle`
```typescript
export type StyleSource = 'preset' | 'custom-description' | 'sample-trained' | 'self-trained';
export interface LocalWritingStyle extends BaseRecord {
name: string; // encrypted
description: string; // encrypted — Style-Beschreibung
source: StyleSource; // plaintext
presetId?: string | null; // plaintext — falls source='preset'
samples?: Array<{ // encrypted
label: string;
text: string;
sourceRef?: string; // z.B. 'journal:id', 'articles:id'
}>;
extractedPrinciples?: { // encrypted — cached Style-Extraktion
toneTraits: string[];
sentenceLengthAvg?: number;
vocabulary?: string[];
examples?: string[];
rawAnalysis?: string; // Freitext-Analyse
extractedAt: string;
} | null;
isSpaceDefault: boolean; // plaintext — für Space-Kontext-Default
isFavorite: boolean; // plaintext
}
```
**Self-Training** (source='self-trained'): Tool sammelt 1020 Snippets aus `journal` + `notes` + `articles` (Highlights) des Users, extrahiert Prinzipien einmalig, cached als `extractedPrinciples`. Explizite User-Aktion — keine Hintergrund-Analyse.
### Encryption-Registry
```typescript
// apps/mana/apps/web/src/lib/data/crypto/registry.ts
drafts: {
fields: ['title', 'briefing', 'styleOverrides', 'references'], // references: wegen .note
version: 1,
},
draftVersions: {
fields: ['content', 'summary'],
version: 1,
},
generations: {
fields: ['prompt', 'output'],
version: 1,
},
writingStyles: {
fields: ['name', 'description', 'samples', 'extractedPrinciples'],
version: 1,
},
```
Alles Nutzer-getippte: encrypted. IDs, Status, Counts, Timestamps, FK-Pointer: plaintext.
## Routing
```
apps/mana/apps/web/src/routes/(app)/writing/
├── +page.svelte # ListView: KindTabs + Grid
├── [kind]/+page.svelte # Deep-Link: /writing/blog, /writing/email ...
├── draft/[id]/+page.svelte # DetailView (drei-spaltig)
├── new/+page.svelte # Kurz-Briefing-Flow (1-Feld → Kind-Vorschlag → Briefing)
└── styles/+page.svelte # Styles-Verwaltung (Preset durchstöbern, eigene anlegen/trainieren)
```
## UI-Konzept
### ListView (`/writing`)
- **Top**: `KindTabs` (Alle | Blog | Essay | E-Mail | Social | Story | ...)
- **Sekundärleiste**: Status-Chips (Entwurf | In-Überarbeitung | Fertig | Veröffentlicht), Sort (Zuletzt bearbeitet | Titel | Wortzahl), Favoriten-Toggle
- **Grid**: `DraftCard` mit Titel + kind-Badge + 2-Zeilen-Preview (erste Zeilen der aktuellen Version) + Last-Updated + Visibility-Icon + Status-Badge
- **FAB "+"**: öffnet `/writing/new`
### `/writing/new` — Kurz-Briefing-Flow
Drei-Schritt-Wizard in einer Card:
1. "Was möchtest du schreiben?" — ein Textfeld. User tippt z.B. "LinkedIn Post zu meinem neuen Modul".
2. AI schlägt basierend auf Freitext vor: `kind='social'`, Länge=200 Wörter, Ton=professional-excited. User kann adjusten.
3. "Generate" → erstellt Draft, leitet zu DetailView weiter, startet erste Generation.
Alternativ "Ohne Vorschlag anlegen" → leeres Briefing-Form.
### DetailView (`/writing/draft/[id]`)
**Drei Spalten** (responsiv: auf Mobil als Tabs):
**Links — Briefing-Panel** (collapsible):
- `BriefingForm` mit Topic, Kind, Audience, Tone, Language, TargetLength, ExtraInstructions
- `StylePicker` — Preset, Custom, oder "Schreibe wie ich"
- `ReferencePicker` — Cross-Modul-Picker: articles, notes, library, kontext, goals, URLs
- "Generate" / "Regenerate" Button — triggert volle Generation → neue Version
- Visibility-Picker (`<VisibilityPicker>` aus shared-privacy)
**Mitte — Text**:
- Editierbarer Textbereich (Markdown, WYSIWYG-Toggle)
- Bei Selektion: `SelectionToolbar` erscheint → Kürzen / Erweitern / Ton / Umschreiben / Übersetzen
- Top-Bar: aktuelle Version, Wortzahl, Sprache, "Als neue Version speichern"-Button
- Live-Streaming während aktiver Generation (Overlay mit Streaming-Preview)
**Rechts — Tools & Context**:
- `VersionHistory` — Timeline aller Versions, Click → Diff, Revert
- Referenzen-Liste (aus Briefing) mit "Öffnen"-Link
- `ProposalInbox` — wartende Refine-Vorschläge (falls `propose`-Policy)
- "Veröffentlichen als..." → Dropdown: Website, Artikel, E-Mail, PDF-Export, Zwischenablage
### Styles-Verwaltung (`/writing/styles`)
- Grid: Preset-Styles + Custom-Styles
- Button "Eigenen Style trainieren" — öffnet Dialog:
- Option A: Style-Beschreibung eintippen ("akademisch, prägnant, aktiv formuliert")
- Option B: Textproben hochladen/aus bestehenden Drafts/Notes importieren → One-Shot-Extraction
- Option C: "Schreibe wie ich" — zieht Samples aus journal/notes/articles, extrahiert Prinzipien
- Pro Style: Preview-Box "So klingt's: [Beispiel-Absatz über Dummy-Topic]" — lazy generiert auf Klick
## Style-System — Details
### Preset-Library (`presets/styles.ts`)
Erste Tranche:
- **Akademisch** — dicht, passive Voice erlaubt, Zitate, Konjunktiv
- **Casual Blog** — du-Ansprache, kurze Absätze, rhetorische Fragen
- **LinkedIn-Post** — Hook in Zeile 1, 1-Satz-Absätze, Emoji sparsam, Call-to-action am Ende
- **Twitter/X-Thread** — nummerierte Tweets, je ≤280 Chars, Cliffhanger
- **Hemingway** — deklarativ, kurze Sätze, minimal Adjektive
- **Nachrichtlich** — inverted pyramid, nüchtern, keine Meinung
- **Buzzfeed-Listicle** — Listenformat, überspitzte Einleitungen
- **Pitch / Sales** — Problem → Agitation → Solution-Struktur
- **Memoir** — 1. Person, sensorisch, Szenen statt Zusammenfassungen
### Space-Default-Style
- Personal-Spaces: kein Default; User wählt pro Draft (oder "Schreibe wie ich" ist Default nach erstem Self-Training).
- Team/Firmen-Spaces: `spaceDefaultStyleId` im `Space`-Record (Erweiterung in `spaces-foundation`). Ein Space-Admin kann einen Style als `isSpaceDefault=true` markieren.
- Vererbung: Briefing.styleId → Space-Default-Style → Kein Style (AI wählt generisch).
### Persona-Linkage
`mana-persona-runner` Personas bekommen ein optionales `defaultWritingStyleId`. Wenn eine Persona einen Writing-Draft erzeugt (via MCP `create_draft`-Tool), wird ihr Default-Style vorausgewählt. Personas und Styles bleiben getrennte Entitäten — die Linkage ist lose.
## AI-Integration
### Tools (`tools.ts` + `@mana/shared-ai`)
| Tool | Policy | Beschreibung |
|---|---|---|
| `list_drafts` | auto | Liefert Drafts gefiltert nach `kind`/`status`, read-only |
| `get_draft` | auto | Voller Draft inkl. aktueller Version |
| `create_draft` | propose | Legt neuen Draft mit Briefing an (ohne Generation) |
| `generate_draft_content` | propose | Startet Generation auf existierendem Draft → schreibt neue Version |
| `generate_outline` | propose | Generiert Outline aus Briefing, als "Outline"-Section vor Volltext |
| `refine_selection` | propose | Mark. Passage umschreiben mit Instruction |
| `shorten_draft` | propose | Verkürzen auf Ziel-Wortzahl |
| `expand_draft` | propose | Ausweiten auf Ziel-Wortzahl |
| `change_tone` | propose | Ton wechseln |
| `translate_draft` | propose | In andere Sprache übersetzen — erstellt neuen Draft mit `language` und Link auf Original |
| `publish_draft` | propose | Nach website/articles/... veröffentlichen |
| `list_writing_styles` | auto | Alle verfügbaren Styles (Preset + Custom) |
| `train_style_from_samples` | propose | Neuen Custom-Style aus Sample-Set extrahieren |
Alle `propose`-Tools landen in `ProposalInbox` mit Preview (Diff gegen aktuellen Content bei Refine-Tools).
### Provider-Wahl (Runtime)
| Fall | Provider |
|---|---|
| Kurztext (≤300 Wörter), synchron gewünscht | `mana-llm` direkt (oder `local-llm` als Fallback) |
| Langtext (>300 Wörter) | Mission über `mana-ai` — streamt zurück, versions-fähig |
| Offline / Privacy-max | `local-llm` (Gemma 4 E2B via WebGPU) — Qualität eingeschränkt |
| Mit Recherche-Flag | Mission über `mana-ai` mit pre-planning web-research-Injection (analog zu `news-research`-Keywords) |
Die Entscheidung passiert im `generations.svelte.ts`-Store, nicht im Tool. Tools sind Provider-agnostisch.
### Mission-Flow für Langtext
1. `generate_draft_content` erstellt `LocalGeneration` mit `status='queued'`, provider=`mana-ai`
2. Store startet Mission über `mana-ai` mit Context: Briefing + Style (inkl. `extractedPrinciples`) + Referenzen (aufgelöst zu voll­text wo möglich) + Space-Kontext-Docs falls vorhanden
3. Mission-Runner kettet intern bis zu 5 Planner-Calls:
- Research (optional, falls Referenzen URLs enthalten ohne Inhalt)
- Outline (falls `generate_outline` separat gecalled oder automatisch bei langen Texten)
- Volltext-Generation
- Selbst-Review (optional — Qualitätscheck)
- Final Polish
4. Streaming-Output landet via Sync-Channel im Client-Store → UI zeigt live
5. Bei `status='succeeded'`: `applyGenerationAsVersion(generationId)` schreibt neue `LocalDraftVersion`, setzt `currentVersionId`
### Recherche-Integration
- Flag `briefing.useResearch: boolean` (im UI "Mit Web-Recherche schreiben")
- Wenn gesetzt, injectet mana-ai bei Mission-Start `mana-research` pre-planning (existing Code aus `news-research`)
- Gefundene Quellen werden automatisch als `DraftReference[]` mit `kind='url'` an den Draft gehängt
- Inline-Zitate optional als M7-Feature (Markdown-Footnotes)
## Cross-Modul Integration
### Als Konsument
| Modul | Integration |
|---|---|
| `articles` | Als Referenz pickbar; Content fließt in Prompt |
| `notes` | Als Referenz pickbar |
| `library` | Entries als Referenz ("schreibe über Film X") |
| `kontext` | Kontext-Docs als Standing-Context, Space-Default-Referenzen |
| `goals` | Als Motivation-Anker ("Ziel-Update-Post") |
| `me-images` | Für Ghost-Writer mit Foto: picture-Generation eines Headers vor-/nach-geschaltet |
| `mana-research` | Bei `useResearch=true` automatisch |
### Als Produzent
| Ziel-Modul | Publish-Hook |
|---|---|
| `website` | Draft → neuer Text-Block in ausgewählter Page |
| `articles` | Als "Eigen-Artikel" speichern (mit Autor=Self) |
| `social-relay` | Zu Social-Plattformen senden (falls Modul aktiv) |
| `mail` | Als E-Mail-Entwurf übergeben |
| `presi` | Als Präsi-Outline-Import |
| Export | Markdown-Download, PDF, Zwischenablage |
Publish-Targets werden in `draft.publishedTo[]` gespeichert → User sieht "Wurde veröffentlicht als: ..." im DetailView.
## Events (Domain-Events)
Für Workbench-Timeline + Audit:
- `WritingDraftCreated`
- `WritingDraftBriefingUpdated`
- `WritingDraftGenerationStarted` (für live-Tracking)
- `WritingDraftVersionCreated`
- `WritingDraftVersionReverted`
- `WritingDraftPublished`
- `WritingDraftVisibilityChanged`
- `WritingStyleCreated`
- `WritingStyleTrainedFromSamples`
## Registrierung (Checklist)
1. `module.config.ts` anlegen mit `tables: [drafts, draftVersions, generations, writingStyles]`
2. Config in `apps/mana/apps/web/src/lib/data/module-registry.ts` aufnehmen
3. Dexie-Schema-Migration: neue Version mit vier Tables
4. Encryption-Registry: vier Einträge
5. Routes unter `(app)/writing/` anlegen
6. App-Eintrag in `packages/shared-branding/src/mana-apps.ts`:
```typescript
{ id: 'writing', name: 'Writing', description: {...}, icon: APP_ICONS.writing, color: '#0ea5e9', status: 'development', requiredTier: 'beta' }
```
7. Icon in `packages/shared-branding/src/app-icons.ts`
8. `docs/MODULE_REGISTRY.md` ergänzen
9. Guest-Seed in `collections.ts` (1 Draft + 1 leerer Custom-Style)
10. Vitest für Mutationen + Encryption-Roundtrip + Version-Logik
## Offene Fragen
- **Outline-Mandatory?** Für Blog/Essay ist eine Outline fast immer sinnvoll; für Social/Bio/E-Mail-Kurz nicht. **Vorschlag:** `AUTO_OUTLINE_KINDS = ['blog', 'essay', 'speech', 'cover-letter', 'story']` — bei denen startet die Mission mit Outline-Schritt automatisch. User kann im Briefing überschreiben.
- **Image-Integration mit `picture`:** Soll ein Draft optional einen Header/Cover-Image haben, generiert via picture? **Vorschlag:** erst M9+. Zunächst nur `coverImageId` als optionales Feld reservieren (plaintext FK) — UI kommt später.
- **Kollaboratives Editing:** Mehrere User im gleichen Space editieren denselben Draft. Sync-Layer ist LWW → letzte Änderung gewinnt. Das reicht für den Anfang. Realtime-CRDT ist kein Phase-1-Thema.
- **Auto-Title:** Soll der Title aus dem Topic automatisch gesetzt werden oder beim ersten Generate aus dem generierten Text extrahiert? **Vorschlag:** Topic = initialer Titel; beim ersten Draft-Version-Create bietet die UI "Titel vom AI vorschlagen lassen" an.
- **Re-Generate-Semantik:** Ersetzt eine volle Re-Generation die vorherige Version oder fügt neue hinzu? Wir haben entschieden "neue Version immer" — das kann aber bei 10 Iterationen unübersichtlich werden. **Vorschlag:** History-Panel zeigt nur `isAiGenerated=true`-Versions mit Label "Generation N"; "Zwischenstände" (Selection-Apply) bleiben im lokalen Undo-Stack ohne Version-Record.
- **Token-Limits bei großen Referenzen:** Lange Artikel als Referenz → Prompt-Explosion. **Vorschlag:** Im Mission-Runner automatischen Reference-Summarizer davorschalten (schon für `articles` da? prüfen). Falls nicht, als Sub-Task in M7.
- **Veröffentlichte Drafts readonly?** Nach `publish_draft` sollte der Draft vor versehentlichem Editieren geschützt sein. **Vorschlag:** Status `published` → UI rendert text readonly mit "Editieren erlauben"-Toggle; Publish-Targets zeigen Sync-Status.
## Reihenfolge (Milestones)
1. **M1 — Skelett**: types, collections, module.config, Registrierung, Dexie-Migration (v N+1), leere Routes, leeres ListView, kein UI. *Ziel: App zeigt "Writing"-Modul-Kachel an, Route lädt leer, nichts crasht.*
2. **M2 — Draft-CRUD manuell**: `createDraft`, `BriefingForm`, `DraftCard`, `KindTabs`, `DetailView` mit manuell editierbarem Text (ohne AI). Alle 12 Kinds als Chips. *Ziel: User kann Drafts anlegen und tippen — wie ein eingebauter Texteditor.*
3. **M3 — Generation v1 (Sync-LLM)**: `generate_draft_content` über `mana-llm` direkt, ohne Mission-Runner. Schreibt neue `LocalDraftVersion`. Versions-History-Panel. *Ziel: "Generate"-Button produziert ersten Draft-Text aus Briefing für Kurztexte.*
4. **M4 — Stil-System (Presets + Custom)**: `LocalWritingStyle`-Table, 9 Presets, `/writing/styles` View, `StylePicker` in Briefing, Style fließt in Prompt ein. *Ziel: User wählt "LinkedIn-Post"-Preset und Output ändert sich sichtbar.*
5. **M4.1 — "Schreibe wie ich" (Self-Training)**: `train_style_from_samples` mit Auto-Pull aus `journal` + `notes` + `articles`. Extrahierte Prinzipien gecached. *Ziel: Ein "Self"-Style, der User's Schreibstil imitiert.*
6. **M5 — Cross-Modul-Referenzen**: `ReferencePicker`, Auflösung in Prompt-Context mit Summarizer bei Langtext. *Ziel: "Schreibe Blog über Buch X (aus library) und Artikel Y (aus articles)".*
7. **M6 — Selection-Refinement-Tools**: `SelectionToolbar`, `refine_selection` / `shorten` / `expand` / `change_tone` als Selection-Operations mit Diff-Preview. Undo-Stack lokal. *Ziel: User markiert Absatz, klickt "Kürzer" → 3 Optionen als Proposal, User picked.*
8. **M7 — Mission-Runner für Langtext + Recherche**: Flip auf `mana-ai`-Missions für lange Drafts, `useResearch`-Flag, Outline-Stage, Streaming-Preview. *Ziel: Essay >1500 Wörter mit Outline→Draft→Review in einer Mission.*
9. **M8 — AI-Tool-Katalog + MCP-Exposure**: Alle Tools in `@mana/shared-ai/src/tools/schemas.ts`, in `mana-mcp` exposed, `AiProposalInbox` im DetailView. Persona-Linkage (`defaultWritingStyleId`). *Ziel: Personas können Drafts erzeugen, Claude Desktop hat Writing-Tools.*
10. **M9 — Canvas-Modus** (optional, Phase 2): Inline-Autocomplete am Cursor, `/`-Command-Palette wie Notion AI. Gleiche Draft-Datenstruktur, alternative UX. *Ziel: User tippt im leeren Canvas, AI ergänzt kontinuierlich.*
11. **M10 — Publish-Hooks**: Integration mit `website`, `articles`, `presi`, `social-relay`. Markdown/PDF-Export. *Ziel: Ein Draft kann als Block auf Website gepublisht werden mit einem Klick.*
12. **M11 — Visibility-System adoptieren**: `<VisibilityPicker>` in DetailView, Unlisted-Share-Link, Embed-Support auf Website. *Ziel: Writing konform mit Visibility-M1+-Standard.*
M1M3 sind "Grundfunktion steht". Ab M4 wird's differenzierend. M7 macht es gegenüber ChatGPT einzigartig (Space-Kontext + Cross-Modul-Refs + Mission-Chaining). M9 ist "nice-to-have, wenn Ghostwriter-Flow sich als zu starr erweist".