Files
PubScript/PLAN.md
T
Schwitzz bb68d9d5d2 CHUNK #3: Writing Surface — editor wiring, storage view, polish
Complete the Writing Surface chunk: wire the web writing view to the
offline-first storage adapter, finish the editor package exports, and
apply Step 4 polish (SVG monogram logo, real project in the sidebar,
font settings → CSS variables).

Editor package
- Re-export EditorContent + Editor type from @pubscript/editor so the
  web app never imports TipTap directly.

Web writing view (apps/web)
- project-store (zustand) over storageLocal: project, chapters, active
  chapter, autosave via createAutosave, idle detection via
  createIdleDetector.
- Client-side seed mirroring the server seed ("The Glass Harbor").
- WritingView component: editor + autosave label + word/scene count +
  chapter nav; wire the /write route inside AppShell.
- Import editor.css in main.tsx.

Core storage contract
- Extend StorageAdapter with createBook/listBooks/listChapters;
  implement in local + server adapters; update contract test mock.

Polish
- Sidebar: SVG monogram logo + live project title/word count instead
  of the placeholder "The Silent Cartographer".
- Fonts: applyFontSettings() in the theme engine writes --ps-font-*
  variables; .ps-content reads --ps-font-size/leading/measure; settings
  store applies fontSize on boot and change.

Docs
- PLAN.md: CHUNK #3 → in progress.
- CHUNK#3_PROGRESS.md: steps 1–3 done, step 4 polish in progress.

Verified: full workspace typecheck clean, lint clean, core 24 / ui 5 /
editor 7 tests pass, web build clean. (server vitest hits EADDRINUSE on
8080 — pre-existing Docker WordPress port conflict, unrelated.)
2026-10-25 13:57:07 -05:00

20 KiB
Raw Blame History

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.

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

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 Status Delivers
1 Scaffold & Foundations 1 ✅ Done Monorepo, strict TS, tooling, app shell, theme system, CI, settings store
2 Data Model, Storage & Auth 3 ⬜ Not started Full domain types, StorageAdapter, Local + Server adapters, Fastify auth/multi-tenant, autosave, contract tests
3 Writing Surface 3 🟡 In progress TipTap editor ✅, document tree ✅, focus mode ��…, Markdown I/O ✅, autosave ✅, fonts/logo polish 🟡
4 Characters & World-Building 2 ⬜ Not started Entity schema, character sheets, world items, search, mentions, backlinks
5 Plot & Whiteboard 3 ⬜ Not started Beat board + templates, timeline, React Flow whiteboard (200-node perf)
6 Export Pipeline 3 ⬜ Not started MD/HTML/EPUB/DOCX/ODT + PDF, export dialog, CI validation
7 Spell & Grammar 2 ⬜ Not started hunspell-WASM spellcheck, LanguageTool grammar, decorations + panel
8 AI Provider Layer 2 ⬜ Not started Provider config + discovery, streaming, thinking params, token budget
9 AI Roles & Suggestions 3 ⬜ Not started Editor/Reader/Publisher/Co-Author, context builder, tools, accept/reject/pin
10 Multi-User Admin & Sharing 2 ⬜ Not started Admin user management, roles, project sharing, audit, security
11 Packaging, Parity & Polish 3 ⬜ Not started 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.

Progress: 1 of 11 chunks complete. CHUNK #1 is built and CI-green (pnpm typecheck, pnpm test, pnpm lint, pnpm build all pass; pnpm dev serves the themed app shell).


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

CHUNK #1 is complete. Next: read CHUNK#2.md and start executing. Each chunk's acceptance criteria are the gate to the next.