Add PubScript master plan and 11 chunk work orders

This commit is contained in:
Schwitzz
2026-10-23 14:26:06 -05:00
commit 53f4c5b4c7
13 changed files with 1086 additions and 0 deletions
+299
View File
@@ -0,0 +1,299 @@
# PubScript — Master Plan
**A self-hostable, multi-user writing studio for novelists — one codebase that runs as a web app and as a native desktop app, with a local-first AI co-writing engine built in.**
> This file is the single source of truth for *what* we are building and *why*.
> The `CHUNK#n.md` files are the executable, step-by-step work orders for *how*.
> A human and a strictly-instructed coding agent can both follow them.
---
## 1. Vision & Goals
PubScript is a technology-forward writing application for novelists. It gives an author one coherent workspace where the act of writing, the management of the story's moving parts (characters, world, plot, timeline), and the assistance of AI all live together — and it can be self-hosted as a multi-user web app or run as a desktop app on Linux, Windows, or macOS.
The product is defined by four commitments:
1. **Parity, not approximation.** The web app and the desktop app are the *same* application. One codebase, one data model, one set of features. The desktop shell adds native capabilities (file access, print-to-PDF, local-AI plumbing) but never forks the feature set.
2. **The author's tools, integrated.** Characters, world-building, whiteboard flow, and plot structure are first-class citizens — not bolt-on notes. AI feedback is *anchored* to specific passages and offered as adoptable/rejectable revisions, never a wall of free text.
3. **Self-hostable and private.** One `docker compose up` runs the whole server (accounts, admin, projects, AI). Authors own their data. Local AI (Ollama, llama.cpp, LM Studio) is a first-class provider alongside OpenAI, Anthropic, Gemini, and OpenRouter.
4. **Fast enough to disappear.** A 100k-word novel must open in seconds, typing must never lag, and a 200-node whiteboard must stay smooth. Performance is a feature, not an afterthought.
### Non-goals (for v1)
- Real-time collaborative editing (two cursors in the same document). Multi-user means *accounts, roles, and sharing*, not live co-editing.
- Mobile-native apps. The responsive web app covers tablets/phones for reading and light edits.
- A public SaaS / billing system. Self-hosted instances are private; there is no payment layer.
---
## 2. Locked Decisions
These were confirmed with the author and are fixed for the build:
| Decision | Value |
|---|---|
| **Primary language** | TypeScript (strict) across app, packages, and server |
| **UI framework** | React 19 + Vite 6 + Tailwind v4 |
| **Desktop shell** | Tauri 2 (Rust) wrapping the *same* built web bundle |
| **Rich-text engine** | TipTap 3 (ProseMirror) |
| **Flow / whiteboard** | React Flow 12 (`@xyflow/react`) |
| **AI integration** | Vercel AI SDK v5 |
| **Server** | Fastify + SQLite (`better-sqlite3`), single-file, self-hosted |
| **Export formats** | Markdown, HTML, EPUB 3, DOCX, **ODT** (OpenDocument), PDF |
| **Multi-user** | Yes — accounts, an **admin role**, project sharing, managed from both web and desktop |
| **Platform priority** | **Linux first** (Arch, Debian, Fedora), then Windows, then macOS |
| **Self-hosting** | Docker Compose (one container serves the web app + API + data) |
### Why this stack
- **One TypeScript codebase** for app *and* server means a single language, shared types, and no context-switching — the most efficient path for both a human and an agent.
- **Tauri 2** gives a ~10 MB native shell (vs ~150 MB for Electron) with real OS integration, and because it loads the *identical* web build, parity is structural, not aspirational.
- **SQLite** keeps self-hosting trivial (one file to back up) while remaining fast enough for multi-user at the scale a writing team actually runs.
- **TipTap 3 + React Flow 12 + AI SDK v5** are the current, actively-maintained leaders in their categories and compose cleanly.
---
## 3. Architecture
### 3.1 The three runtime modes (one codebase)
```
┌─────────────────────────────────────────────┐
│ PubScript App (React 19) │
│ writing · characters · world · plot · │
│ whiteboard · AI roles · export · settings │
└───────────────┬──────────────────────────────┘
│ talks to a StorageAdapter
┌────────────────────┼───────────────────────┐
▼ ▼ ▼
┌────────────────┐ ┌──────────────────┐ ┌───────────────────┐
│ Web browser │ │ Tauri desktop │ │ Local / offline │
│ (self-hosted) │ │ shell (Linux/ │ │ mode (desktop, │
│ │ │ Win/macOS) │ │ no server) │
└───────┬────────┘ └────────┬─────────┘ └─────────┬─────────┘
│ REST + WebSocket │ same REST + WS │ local adapter
▼ ▼ ▼
┌───────────────────────────────────┐ ┌──────────────────────┐
│ PubScript Server │ │ LocalStorageAdapter │
│ Fastify + SQLite (multi-user, │ │ (Dexie / Tauri FS) │
│ accounts, admin, sharing, AI) │ │ │
└───────────────────────────────────┘ └──────────────────────┘
```
- **Web mode:** the browser talks to the self-hosted server over REST + WebSocket.
- **Desktop + server mode:** the Tauri shell runs the same app and talks to the same server (over LAN or the internet). Admin and account management work identically here.
- **Desktop local mode:** with no server configured, the app uses a local storage adapter (IndexedDB in-browser, Tauri filesystem on disk). Same UI, same features, data stays on the machine. This is the offline fallback.
The **StorageAdapter contract** (defined once in `packages/core`) is the seam that makes all three modes possible. Every feature codes against the contract, never against a specific backend.
### 3.2 Repository layout
```
PubScript/
├── PLAN.md ← this file
├── CHUNK#1.md … CHUNK#11.md ← executable work orders
├── package.json ← pnpm workspace root
├── pnpm-workspace.yaml
├── apps/
│ └── web/ ← the React 19 app (Vite). Runs in browser AND in Tauri.
├── desktop/ ← Tauri 2 shell (Rust). Loads apps/web's build.
├── packages/
│ ├── core/ ← domain types, IDs, time, token budget, StorageAdapter contract
│ ├── ui/ ← design tokens, theme engine, app shell, component kit
│ ├── editor/ ← TipTap schema, decorations, spellcheck integration
│ ├── boards/ ← React Flow whiteboard, beat board, timeline
│ ├── exporters/ ← md / html / epub / docx / odt / pdf + shared flat model
│ └── ai/ ← provider adapters, role engine, context builder, suggestion protocol
├── server/ ← Fastify + SQLite: auth, multi-tenant, WS, admin, AI proxy
├── docker/ ← Dockerfile, compose.yaml, entrypoint, backup scripts
├── .github/workflows/ ← CI (lint, typecheck, test, build, docker, desktop matrix)
└── docs/ ← ADRs, data model reference, AI role prompts, parity matrix
```
### 3.3 Data model (core domain)
All IDs are ULIDs (sortable, collision-free). All timestamps are UTC ISO-8601.
```ts
User { id, email, displayName, role: 'admin' | 'author', passKey?, createdAt }
Project { id, title, ownerId, sharedWith: {userId, perm: 'read'|'edit'}[],
settings, cover, createdAt, updatedAt }
Book { id, projectId, title, order, chapters: Chapter[] }
Chapter { id, bookId, title, doc: ProseMirrorDoc, order, meta }
Scene // v1: a scene is a `sceneBreak` node inside a document; optional Scene meta
Character { id, projectId, name, role, bio, appearance, voice,
relationships: {targetId, kind, note}[], arc, status, tags[] }
WorldItem { id, projectId, kind: 'location'|'faction'|'item'|'glossary'|'system',
name, fields: Record<string, string> }
PlotBeat { id, projectId, template, title, description,
status: 'draft'|'revised'|'locked', chapterId?, order, notes }
TimelineEvent { id, projectId, when: {era, day} | date, title, description,
chapterId?, links[] }
Board { id, projectId, nodes: FlowNode[], edges: FlowEdge[] }
Note { id, projectId, body, pinned, tags[], linkedTo }
AISession { id, projectId, role, model, settings, messages[], suggestions[] }
Suggestion { id, sessionId, target: {docId, range|nodePath}, before, after,
rationale, kind: 'replace'|'insert'|'delete'|'entity-update'|'draft',
status: 'pending'|'accepted'|'rejected'|'pinned' }
```
### 3.4 StorageAdapter contract
```ts
interface StorageAdapter {
// projects
listProjects(): Promise<ProjectMeta[]>
createProject(p: NewProject): Promise<Project>
getProject(id: string): Promise<Project>
// content (debounced autosave)
saveDocument(doc: DocumentRef): Promise<void>
saveEntity(e: Entity): Promise<void>
saveBoard(b: Board): Promise<void>
saveAISession(s: AISession): Promise<void>
// live updates
onEvent(cb: (e: StoreEvent) => void): () => void
}
```
Two implementations:
- **`LocalAdapter`** — Dexie (IndexedDB) in the browser, Tauri FS on disk. Single-user, offline.
- **`ServerAdapter`** — REST + WebSocket to the Fastify server. Multi-user, accounts, admin.
A **contract test suite** runs the *same* assertions against both, so parity is enforced by CI, not by hope.
---
## 4. Feature Specification
### 4.1 Writing surface (the heart of the app)
- TipTap 3 editor with a clean, distraction-free canvas and a focus/typewriter mode.
- Document tree: **Book → Part → Chapter**, drag-to-reorder, per-chapter word counts.
- Native formatting: headings, emphasis, blockquote, scene breaks, footnotes, inline character mentions.
- **Fonts:** a curated set of high-quality open fonts (e.g. EB Garamond, Source Serif 4, Inter, Space Grotesk, JetBrains Mono), selectable with size/leading/margins/measure controls.
- **Spell & grammar check** (see 4.5).
- Live word count, session stats, reading time.
- Markdown import/export built in.
### 4.2 Characters & world-building
- **Character sheets:** name, role, bio, voice, appearance, relationships (typed links to other characters), arc, status.
- **World items:** locations, factions, items, glossary, systems — each a flexible field set.
- Fuzzy search across all entities; backlinks; `@mention` autocomplete in the editor that links prose to entities.
- A relationship/affinity view.
### 4.3 Plot & structure
- **Plot beat board** with templates: Three-Act, Save-the-Cat (15 beats), Seven-Point, and custom. Beats carry status (draft/revised/locked), link to chapters, and show progress.
- **Timeline:** a configurable calendar (era/day or real dates), events, causal links, filtered by character/location.
- **Whiteboard flow editor:** free-form React Flow canvas — entity cards, notes, chapter nodes, images, groups; typed edges; minimap; auto-layout; 200+ nodes at 60 fps.
### 4.4 AI (see §6)
Four agentic roles — **Editor, Reader, Publisher, Co-Author** — with robust base prompts, per-role temperature/thinking/context settings, and feedback that is *anchored* to passages as adoptable/rejectable suggestions.
### 4.5 Spell & grammar
- **Spellcheck:** `hunspell` compiled to WASM (runs fully in-browser, offline). Per-language dictionaries + a user custom dictionary.
- **Grammar/style:** optional **LanguageTool** server (self-hostable, OpenAI-compatible-ish API) for grammar, style, and punctuation.
- Inline squiggle decorations + a side panel to jump/apply/ignore.
### 4.6 Export
Markdown, single-file HTML, **EPUB 3**, **DOCX**, **ODT**, and **PDF** — with title/author/cover/TOC/styling options. (Details in Chunk 6.)
### 4.7 Theming
- CSS-variable design tokens; light & dark; accent color; font/size/leading/density.
- Shipped themes: **Ink** (default), **Parchment**, **Nocturne**, **Terminal**.
- Settings persist per-user (server) or per-machine (local).
---
## 5. AI Design
### 5.1 Providers
Configured as `{ id, kind, baseUrl, apiKey, model, temperature, maxTokens, thinking, contextWindow }`.
- **OpenAI-compatible** (covers Ollama `/v1`, llama.cpp `/v1` with `--jinja`, LM Studio, vLLM) — the workhorse for local models.
- **Anthropic** (Claude), **Google** (Gemini), **OpenRouter** — via the AI SDK's native adapters.
- **Thinking / reasoning:** o-series → `reasoningEffort`; Anthropic → `thinking` budget; local (e.g. Qwen3) → configurable extra body params / `chat_template_kwargs`.
### 5.2 Token budget
`gpt-tokenizer` (o200k_base) counts tokens. A budget engine allocates: system+role ≈ 10%, context pack ≈ 40%, history ≈ 30%, output reserve ≈ 20% (all adjustable). Context is *assembled* from the author's own data — characters, world, relevant chapters — so the AI always writes with the story's canon.
### 5.3 The four roles (base prompts live in Chunk 9)
- **Editor** — a line editor: tighten prose, kill filter words, sharpen dialogue tags, fix pacing, flag weak openings/closings, check consistency against character/world data. Emits targeted, structured edits with rationale. Respects the author's voice.
- **Reader** — an honest first-time reader: what was confusing, where attention dropped, what was telegraphed too early, what paid off. Reports as a reader, not in craft jargon.
- **Publisher** — market & packaging: comps, hook, pacing vs. genre, title/cover/blurb ideas, series potential, format decisions.
- **Co-Author** — a collaborator with full project knowledge: drafts scenes/chapters in the author's established voice, proposes plot turns, writes dialogue, expands world-building — always as *suggestions* the author accepts or rejects, never silently rewriting canon.
### 5.4 Tools (AI SDK tool-calls)
`applySuggestion`, `updateEntity`, `addNote`, `draftChapter`, `queryProject` (search). These let a role act on the workspace, not just talk about it.
### 5.5 Suggestion protocol (the signature feature)
AI output is structured JSON: `{ target: {docId, range}, before, after, rationale, kind }`. The editor renders these as **inline decorations** over the exact passages. The author can **Accept** (applies the edit), **Reject**, or **Pin** (copies it into a pinned note for later). This is what makes AI feedback *integrated into the tools* rather than a chat sidebar.
---
## 6. Multi-User, Admin & Security
- **Accounts:** email + password (argon2id), session tokens (httpOnly cookie on web, bearer on desktop). Optional passkey/WebAuthn later.
- **Roles:** `admin` and `author`. Admin manages users (create, disable, change role) and sees activity — from **both** the web app and the desktop app.
- **Sharing:** project-level, invite by email, `read`/`edit` permissions, owner transfer.
- **Security:** rate limiting, audit log, multi-tenant isolation (a user only ever sees projects they own or are shared with), secrets never leave the instance.
- **Data:** one SQLite file = the whole database; trivially backed up.
---
## 7. Platforms & Packaging
| Target | Artifact | Notes |
|---|---|---|
| **Web (self-hosted)** | Docker image + `compose.yaml` | One container: serves the web app, runs the API, stores SQLite on a volume. `docker compose up` and you're in. |
| **Linux** (priority) | AppImage, `.deb`, `.rpm` | Tauri 2. First-class on Arch/Debian/Fedora. |
| **Windows** | NSIS installer + portable | Tauri. |
| **macOS** | `.dmg` (arm64 + x86_64) | Tauri; notarization is a later polish step. |
Desktop adds: native file-system access (open/save anywhere), system print-to-PDF, and direct plumbing to local AI servers (Ollama/llama.cpp on the same or another machine).
---
## 8. Quality Bar & Conventions
- **TypeScript `strict`** everywhere; no `any`; shared types in `packages/core` used by app *and* server.
- **ESLint + Prettier**; conventional commits; pnpm workspaces; pinned, lockfile-committed versions.
- **Testing:** Vitest (unit), a **storage contract suite** (same tests vs. Local & Server adapters), **golden-file** tests for exporters (epubcheck, unzip-validate), property tests for the token budget, and Playwright e2e smoke (write → autosave → reopen).
- **Performance budgets:** 100k-word project opens < 2 s; typing latency < 16 ms; 200-node board at 60 fps; entity search < 50 ms.
- **CI** (GitHub Actions): on push → install, lint, typecheck, unit tests, build web, build server, build Docker image. Desktop build matrix on demand.
---
## 9. Execution Plan — the chunks
Each chunk is a 1–3 day work order in `CHUNK#n.md`, written so a human *or* a strictly-instructed coding agent can execute it top-to-bottom. Each has prerequisites, numbered steps with exact paths/commands, and testable acceptance criteria.
| # | Chunk | ~Days | Delivers |
|---|---|---|---|
| 1 | **Scaffold & Foundations** | 1 | Monorepo, strict TS, tooling, app shell, theme system, CI, settings store |
| 2 | **Data Model, Storage & Auth** | 3 | Full domain types, StorageAdapter, Local + Server adapters, Fastify auth/multi-tenant, autosave, contract tests |
| 3 | **Writing Surface** | 3 | TipTap editor, document tree, fonts, focus mode, Markdown I/O, performance |
| 4 | **Characters & World-Building** | 2 | Entity schema, character sheets, world items, search, mentions, backlinks |
| 5 | **Plot & Whiteboard** | 3 | Beat board + templates, timeline, React Flow whiteboard (200-node perf) |
| 6 | **Export Pipeline** | 3 | MD/HTML/EPUB/DOCX/ODT + PDF, export dialog, CI validation |
| 7 | **Spell & Grammar** | 2 | hunspell-WASM spellcheck, LanguageTool grammar, decorations + panel |
| 8 | **AI Provider Layer** | 2 | Provider config + discovery, streaming, thinking params, token budget |
| 9 | **AI Roles & Suggestions** | 3 | Editor/Reader/Publisher/Co-Author, context builder, tools, accept/reject/pin |
| 10 | **Multi-User Admin & Sharing** | 2 | Admin user management, roles, project sharing, audit, security |
| 11 | **Packaging, Parity & Polish** | 3 | Docker compose, Tauri bundles (3 OS), onboarding, import/backup, perf/a11y, parity matrix |
**Total: ~27 focused working days**, sequenced so each chunk is independently shippable and testable.
---
## 10. Risks & Mitigations
| Risk | Mitigation |
|---|---|
| Tauri cross-OS packaging friction (esp. macOS notarization) | Linux-first; treat Windows as a CI matrix; macOS notarization is explicit later polish, not a v1 blocker |
| Large-document editor performance | ProseMirror is built for this; we add perf budgets + Playwright timing tests and keep decorations memoized |
| AI suggestion rendering over live-editing text (range drift) | Suggestions carry stable node anchors, not raw offsets; re-resolved on every transaction |
| Local vs. server data divergence | Contract test suite enforces identical behavior; local mode is clearly labeled in the UI |
| EPUB/ODT spec edge cases | Golden-file tests + `epubcheck` in CI; DOCX via the maintained `docx` package |
| Scope creep in AI roles | Roles are prompt + tools, not new UI surfaces; the suggestion protocol is the single integration point |
---
*Next: read `CHUNK#1.md` and start executing. Each chunk's acceptance criteria are the gate to the next.*