diff --git a/CHUNK#1.md b/CHUNK#1.md deleted file mode 100644 index c69f277..0000000 --- a/CHUNK#1.md +++ /dev/null @@ -1,78 +0,0 @@ -# CHUNK #1 — Scaffold & Foundations - -**Status:** ✅ **COMPLETE** — verified green (typecheck, test, lint, build, and `docker build` + runtime health check all pass). **Duration:** ~1 day · **Prereqs:** none (this chunk creates the repo) · **Follows:** — · **Unblocks:** CHUNK #2 - -**Purpose:** Stand up the entire monorepo with strict TypeScript, the app shell, the theming system, the `packages/core` contracts, a Fastify server skeleton, and a green CI pipeline. When this chunk is done, `pnpm dev` runs a themed, navigable app shell and `pnpm test` is green — everything later chunks build on. - ---- - -## Steps - -### 1. Workspace & tooling -1. `cd` to the repo root and run: - - `pnpm init` → set `"name": "pubscript"`, `"packageManager": "pnpm@latest"`. - - Create `pnpm-workspace.yaml`: - ```yaml - packages: - - "apps/*" - - "packages/*" - - "server" - ``` -2. Root `package.json` scripts: `dev`, `build`, `test`, `lint`, `typecheck`, `contract` (each fanning out to workspaces via `pnpm -r run `). -3. `tsconfig.base.json` with `"strict": true`, `"noUncheckedIndexedAccess": true`, `"exactOptionalPropertyTypes": true`, `"moduleResolution": "bundler"`, `"target": "ES2022"`. Every package extends it. -4. ESLint (flat config, `typescript-eslint` recommended + `react-hooks`) and Prettier at root; `.editorconfig`; `.gitignore` (node_modules, dist, .env, *.sqlite, desktop/target). -5. `vitest.config.ts` at root (workspace mode) so `pnpm test` runs every package. - -### 2. `apps/web` — the app -1. Scaffold: Vite 6 + React 19. Add `react-router` v7, `zustand` v5, Tailwind v4 (CSS-first config via `@tailwindcss/vite`). -2. Create the **app shell** in `apps/web/src/`: - - `AppShell.tsx` — left sidebar (Projects / Characters / World / Plot / Whiteboard / AI / Settings), top bar (project title, save status, search), main content outlet. - - `CommandPalette.tsx` — `Ctrl/Cmd+K` palette (jump to chapter, entity, setting, action). - - Routes: `/projects`, `/projects/:id/write`, `/projects/:id/characters`, `/world`, `/plot`, `/whiteboard`, `/ai`, `/settings`. -3. **Settings store** (`apps/web/src/state/settings.ts`, zustand + persist): theme id, accent, font family/size/leading/measure, density, autosave interval, per-role AI defaults. Persisted to localStorage (web) / Tauri store (desktop) behind one `SettingsBackend` interface. - -### 3. `packages/ui` — design system -1. **Theme engine** (`packages/ui/src/theme.ts`): CSS-variable token map. Implement the four themes — **Ink** (dark, default), **Parchment** (warm light), **Nocturne** (deep indigo), **Terminal** (monochrome mono) — as token objects; `applyTheme(id)` sets `document.documentElement.dataset.theme`; accent color and density are runtime overrides. -2. Component kit (typed, Tailwind-styled, no external UI framework): `Button`, `Input`, `Select`, `Modal`, `Drawer`, `Tooltip`, `Tabs`, `Badge`, `EmptyState`, `Spinner`, `Toast`. -3. `packages/ui/src/AppShell.css` — base typography, focus rings, scrollbars, reduced-motion support. - -### 4. `packages/core` — shared contracts -1. `packages/core/src/types.ts` — **every** domain type from PLAN.md §3.3 (User, Project, Book, Chapter, Character, WorldItem, PlotBeat, TimelineEvent, Board, Note, AISession, Suggestion) as strict TS interfaces + a `Role`/`Permission` union. -2. `packages/core/src/storage.ts` — the `StorageAdapter` interface exactly as in PLAN.md §3.4, plus `StoreEvent` union (`doc-changed`, `entity-changed`, `board-changed`, `ai-session-changed`, `presence`). -3. `packages/core/src/ids.ts` — ULID generator + validator. `packages/core/src/time.ts` — UTC ISO helpers. -4. `packages/core/src/tokens.ts` — `countTokens(text)` (wrap `gpt-tokenizer`, lazy-loaded) + `allocateBudget({system, context, history, output, total})` returning per-slot token caps. Unit-test both. - -### 5. `server/` — Fastify skeleton -1. `server/package.json` (ESM): `fastify`, `@fastify/static`, `@fastify/websocket`, `@fastify/cookie`, `better-sqlite3`, `argon2`, `zod`. -2. `server/src/index.ts` — boots Fastify, serves `apps/web/dist` statically (so one container serves app + API), exposes: - - `GET /api/health` → `{ ok, version, db: 'up' }` - - `GET /api/version` -3. `server/src/config.ts` — env-driven: `PUBSCRIPT_DB` (path, default `./data/pubscript.sqlite`), `PORT`, `PUBSCRIPT_ORIGIN` (CORS), `PUBSCRIPT_ADMIN_EMAIL` (bootstrap admin), `PUBSCRIPT_TOKEN_TTL`. - -### 6. CI — `.github/workflows/ci.yml` -On push/PR: `pnpm install --frozen-lockfile` → `pnpm lint` → `pnpm typecheck` → `pnpm test` → `pnpm build` (web + server) → `docker build -f docker/Dockerfile .` (smoke). Post the test summary as an artifact. - -### 7. `docker/` -- `docker/Dockerfile` — multi-stage: `node:22-alpine` build stage (pnpm, build web + server) → slim runtime stage serving the built app + running the API. Non-root user. `HEALTHCHECK` hitting `/api/health`. -- `docker/compose.yaml` — service `pubscript`, port 8080, volume `./data:/app/data`, `restart: unless-stopped`. - -### 8. Docs & conventions -- `docs/CONVENTIONS.md` — commit style, file layout rules, "code against `packages/core`, never against a backend" rule. -- `README.md` at root — 10-line quickstart: `pnpm i && pnpm dev`, `docker compose up`, where things live. - ---- - -## Acceptance criteria (all must pass) -- [ ] `pnpm i && pnpm build` succeeds with zero TS errors under `strict`. -- [ ] `pnpm dev` → app shell loads at `/`, sidebar + top bar render, all 8 routes navigate, `Ctrl+K` palette opens and jumps to a route. -- [ ] Switching theme (Ink → Parchment → Nocturne → Terminal) restyles the whole shell instantly; choice survives a reload. -- [ ] `pnpm test` green: token budget math, ULID, theme token map, settings persistence. -- [ ] `docker compose up` → `curl localhost:8080/api/health` returns `{"ok":true,...}` and the app shell loads in the browser. -- [ ] CI run is green end-to-end (lint, typecheck, test, build, docker). - -## Definition of done -Repo is a working, themed, CI-green monorepo. Any later chunk can assume: strict types exist in `packages/core`, the shell renders, settings persist, and `docker compose up` serves the app. - -## Risks & fallbacks -- **Tailwind v4 + React 19 friction** → if a component misbehaves, pin exact versions from the Vite 6 template and record them in `docs/CONVENTIONS.md`. -- **better-sqlite3 native build in Docker** → use `node:22-alpine` with `python3 make g++` in the build stage; if it still fails, fall back to `node:22-slim` (Debian) — record the choice in an ADR. diff --git a/CHUNK#10.md b/CHUNK#10.md deleted file mode 100644 index 9d8c8c8..0000000 --- a/CHUNK#10.md +++ /dev/null @@ -1,74 +0,0 @@ -# CHUNK #10 — Multi-User Admin & Sharing - -**Duration:** ~2 days · **Prereqs:** CHUNK #2 (auth skeleton, ServerAdapter) · **Follows:** CHUNK #9 · **Unblocks:** CHUNK #11 (parity: admin works in web + desktop identically) - -**Purpose:** Complete the multi-user product: full admin user management (create, disable, change role), project sharing with read/edit permissions, an audit log, and hard multi-tenant isolation — all reachable from **both** the web app and the desktop app identically. - ---- - -## Steps - -### 1. Admin API (`server/src/routes/admin.ts`) -1. `GET /api/admin/users` — list all users (name, email, role, status, lastActive). -2. `POST /api/admin/users` — create a user (email + role; server generates a one-time password or sends an invite link). -3. `PATCH /api/admin/users/:id` — change role (`admin`/`author`) or disable/enable. -4. `GET /api/admin/activity` — audit log tail (filtered by user/action/date). -5. `GET /api/admin/stats` — instance-wide: user count, project count, storage used. -6. All routes behind `requireAdmin` middleware. - -### 2. Sharing API (`server/src/routes/projects.ts` — extend Chunk 2) -1. `POST /api/projects/:id/shares` — invite by email, grant `read`/`edit`. -2. `PATCH /api/projects/:id/shares/:userId` — change permission. -3. `DELETE /api/projects/:id/shares/:userId` — revoke. -4. `POST /api/projects/:id/transfer` — transfer ownership (owner only). -5. `GET /api/projects` — return owned ∪ shared (with `perm` on each). - -### 3. Access control (`server/src/auth.ts` — extend Chunk 2) -1. `canAccessProject(projectId, perm)` — enforces owner OR shared-with (correct `perm`). Middleware on every content route. -2. **Multi-tenant isolation**: every query filters by `project_id` the user can access. A user can never read another's project — enforced at the SQL layer, not just the app layer. -3. Session revocation: disabling a user → their session token is invalidated within one event round-trip (Chunk 2 acceptance criterion). - -### 4. Audit log (`server/src/db.ts` — extend Chunk 2) -1. `audit(id, at, user_id, action, detail JSON)` — log: user create/disable, role change, share grant/revoke, ownership transfer, project delete. -2. `GET /api/admin/activity` returns the tail; retention: 1 year (v1, no purge). - -### 5. Admin UI — web (`apps/web/src/features/admin/AdminView.tsx`) -1. **Users tab**: table (name, email, role, status, lastActive); "Create user" modal; inline role toggle; disable/enable switch. -2. **Sharing tab** (per project): list shared users with permission dropdown; transfer ownership; revoke. -3. **Activity tab**: filterable audit log table. -4. **Stats tab**: instance-wide counters. - -### 6. Admin UI — desktop parity -1. The desktop app renders the **same** `AdminView` (it's part of the shared React app). No desktop-specific admin code — parity is structural. -2. Desktop uses bearer token for auth (Chunk 2); web uses cookie. Both hit the same API. - -### 7. Invite flow -1. `POST /api/admin/users` with `sendInvite: true` → generates an invite token, stores a pending invite, (v1) logs the invite link to the audit log; the invitee clicks → `/invite/:token` → sets password → becomes an author. - -### 8. Tests -1. Unit: `canAccessProject` — owner can edit; shared-read can read not edit; unrelated user gets 403. -2. Integration: admin creates user → user logs in → sees only shared projects; admin disables user → user's next API call gets 401. -3. Integration: project isolation — user A's adapter never returns user B's project (contract suite extension). -4. E2E: admin creates a user, shares a project (edit) → user sees it; revoke → it disappears. - ---- - -## Acceptance criteria (all must pass) -- [x] Admin creates/disables/changes-role of users from the web app. -- [x] Admin manages sharing (grant/revoke/transfer) from the web app. -- [x] Admin sees activity log + instance stats. -- [ ] The **same** admin UI works identically in the desktop app. -- [x] A shared-with-read user cannot edit; a shared-with-edit user can. -- [x] Multi-tenant isolation: user A can never read user B's project (enforced at SQL layer). -- [x] Disabling a user revokes their session within one event round-trip. -- [x] Audit log records all admin actions. -- [x] Contract suite still green on both adapters. -- [x] **Admin UI** (`apps/web/src/features/admin/AdminView.tsx`): the Users, Activity/Audit, and Stats views (with user creation, role updates, disable/enable, and deletion) are wired to the backend and rendered by `apps/web/src/routes/Admin.tsx`. - -## Definition of done -PubScript is a real multi-user product: admins manage users and sharing from both web and desktop, projects are shared with granular permissions, and isolation is guaranteed at the database layer. - -## Risks & fallbacks -- **Permission bugs → data leaks** → enforce isolation at the SQL layer (WHERE project_id in accessible); contract suite tests cross-user access. -- **Session revocation latency** → check token validity on every request (small cost) rather than caching; cap the auth lookup. -- **Invite email delivery** → v1 logs the invite link (no SMTP); document SMTP as a later addition. diff --git a/CHUNK#11.md b/CHUNK#11.md deleted file mode 100644 index 5e61606..0000000 --- a/CHUNK#11.md +++ /dev/null @@ -1,74 +0,0 @@ -# CHUNK #11 — Packaging, Parity & Polish - -**Duration:** ~3 days · **Prereqs:** CHUNK #10 (multi-user complete) · **Follows:** CHUNK #10 · **Unblocks:** — (final chunk) - -**Purpose:** Package PubScript for every platform — Docker Compose for self-hosting, Tauri 2 native bundles for Linux/Windows/macOS — and prove web/desktop parity. Add onboarding, import/backup, performance/a11y polish, and a parity matrix that documents every feature's cross-platform status. - ---- - -## Steps - -### 1. Docker self-hosting (`docker/`) -1. **`docker/Dockerfile`** (extend Chunk 1): multi-stage — `node:22-alpine` build (pnpm, build web + server) → slim runtime serving the built app + running the API + SQLite on a volume. Non-root user. `HEALTHCHECK` → `/api/health`. -2. **`docker/compose.yaml`** (extend Chunk 1): `pubscript` service, port 8080, `./data:/app/data` volume, `restart: unless-stopped`, env from `.env`. -3. **`docker/languagetool/compose.yaml`**: optional LanguageTool sidecar (Chunk 7) — documented, not required. -4. **Backup scripts** (`docker/backup.sh`): `pg_dump`-style SQLite snapshot (`cp` the `.sqlite` file with WAL checkpoint) → timestamped `.sqlite.bak`; restore script. -5. **`docker/README.md`**: "one `docker compose up`" quickstart; env var reference; data location; backup/restore. - -### 2. Tauri desktop bundles (`desktop/`) -1. **Tauri 2 config** (`desktop/tauri.conf.json`, extend Chunk 1): app metadata, icons, bundle targets per OS, CSP, and the capability to access the local AI endpoints + filesystem. -2. **Native capabilities**: - - File-system access (open/save anywhere) via `@tauri-apps/plugin-fs`. - - System print-to-PDF (Chunk 6 PDF backend default). - - Local AI plumbing (detect Ollama/llama.cpp on LAN). -3. **Build matrix** (`.github/workflows/desktop.yml`): - - **Linux**: AppImage + `.deb` + `.rpm`. - - **Windows**: NSIS installer + portable `.zip`. - - **macOS**: `.dmg` (arm64 + x86_64); notarization as explicit later polish. -4. **Icons**: generate per-platform icon sets (scripts in `desktop/icons/`). - -### 3. Parity proof -1. **Parity matrix** (`docs/PARITY.md`): every feature × {web, desktop-local, desktop-server} with a ✓/—/⚠. Document where desktop adds native capability (file access, print, local AI) but never forks features. -2. **Contract suite** (Chunk 2) already runs Local vs. Server adapters identically — extend it to assert the *same* feature behavior across both. CI gate. -3. **Playwright parity e2e**: run the same smoke suite against the web build AND the Tauri-built app (via `@tauri-apps/cli` test) — write → autosave → reopen → AI suggestion accept. - -### 4. Onboarding (`apps/web/src/features/onboarding/`) -1. First-run flow: create account (or local mode toggle) → create first project → seed the demo ("The Glass Harbor") OR start blank → guided tour of the 5 surfaces (write, characters, plot, whiteboard, AI). -2. **Local mode banner**: "Local mode — data stays on this device" when no server is configured. - -### 5. Import & backup (UI) -1. **Import** (`apps/web/src/features/import/`): import a `.md` file into a project (Chunk 3); import an EPUB (parse → chapters) as a bonus. -2. **Backup** (settings): "Export all data" → zip of the SQLite dump + covers; "Restore" → import the zip. - -### 6. Performance & a11y polish -1. Re-run all performance budgets (Chunk 3: 100k-doc < 2s open, < 16ms typing; Chunk 5: 200-node board 60fps) — fix regressions. -2. **a11y**: axe-core audit in CI; ensure contrast ≥ 4.5:1 in all themes, full keyboard nav, screen-reader labels on all surfaces. - -### 7. Final docs -1. `README.md` (extend Chunk 1): full quickstart for web + desktop + self-host. -2. `docs/ROLE_PROMPTS.md`, `docs/DATA_MODEL.md`, `docs/EDITOR.md`, `docs/BOARDS.md`, `docs/PARITY.md` — all complete. - -### 8. Tests -1. CI: full matrix — lint, typecheck, unit, contract, exporters golden-file, perf, a11y, web e2e, desktop e2e, docker build. -2. All performance + a11y budgets pass. - ---- - -## Acceptance criteria (all must pass) -- [x] `docker compose up` → app + API + SQLite running; `/api/health` green; app loads. -- [ ] Linux build produces AppImage + `.deb` + `.rpm`; Windows: NSIS + portable; macOS: `.dmg`. -- [ ] Desktop app: native file save/open, system print-to-PDF, local AI detection. -- [ ] Parity matrix documents every feature; contract suite + Playwright prove web/desktop parity. -- [ ] Onboarding flow guides a first-time user through all 5 surfaces. -- [ ] Import `.md`/`.epub`; backup/restore round-trips all data. -- [ ] All performance budgets pass (open, typing, board fps). -- [ ] a11y: axe-core clean, contrast ≥ 4.5:1, full keyboard nav. -- [ ] CI green end-to-end across the full matrix. - -## Definition of done -PubScript ships as a self-hostable multi-user web app AND native desktop apps on three OSes, with proven parity, polished performance and accessibility, and complete docs. The full vision from PLAN.md is realized. - -## Risks & fallbacks -- **Tauri cross-OS packaging friction** (esp. macOS notarization) → Linux-first; Windows as a CI matrix; macOS notarization is explicit later polish, not a v1 blocker. -- **Parity gaps slip through** → parity matrix + automated contract/playwright gates in CI make gaps visible. -- **Docker native-build issues** (better-sqlite3) → `node:22-alpine` with build tools; fall back to `node:22-slim` (documented in an ADR, Chunk 1). diff --git a/CHUNK#2.md b/CHUNK#2.md deleted file mode 100644 index 3e35e03..0000000 --- a/CHUNK#2.md +++ /dev/null @@ -1,93 +0,0 @@ -# CHUNK #2 — Data Model, Storage & Auth - -**Duration:** ~3 days · **Prereqs:** CHUNK #1 (green CI, app shell) · **Follows:** CHUNK #1 · **Unblocks:** everything (this is the spine) - -**Purpose:** Implement the full domain model, both `StorageAdapter` implementations (local + server), the multi-user Fastify API with auth, roles, and project sharing, the autosave engine, and — critically — the **storage contract test suite** that proves Local and Server adapters behave identically. After this chunk, the app can store and sync real content, and users can sign in, create projects, and share them. - ---- - -## Steps - -### 1. Domain model finalization (`packages/core`) -1. In `packages/core/src/types.ts`, ensure every type from PLAN.md §3.3 is complete and exported. Add: - - `ProjectMeta` (list view), `NewProject`, `DocumentRef { id, projectId, bookId?, title, doc: ProseMirrorDoc, rev: number }`. - - `ProseMirrorDoc = { type: 'doc', content: JsonNode[] }` (JSON form of the TipTap schema). - - `StoreEvent` payloads (typed per event kind), `Entity` = `Character | WorldItem | Note` (discriminated union on `kind`). - - `AISession`, `Suggestion`, `Role`, `Permission` (already in Chunk 1 — verify against PLAN.md). -2. `packages/core/src/schema.ts` — a single `ENTITY_FIELDS` registry: for each entity kind, the ordered field list (`{ key, label, type: 'text'|'textarea'|'select'|'list'|'link', options? }`). This registry drives the generic entity editor (Chunk 4) **and** the server's validation. Single source of truth. - -### 2. SQLite schema (`server/src/db.ts`) -1. Create the DB (WAL mode) with migrations in `server/src/migrations/` (plain SQL, applied in order, tracked in a `schema_migrations` table): - - `users(id, email UNIQUE, pass_hash, display_name, role, created_at)` - - `sessions(token_hash PK, user_id, expires_at, created_at)` - - `projects(id, owner_id, title, settings JSON, cover, created_at, updated_at)` - - `project_shares(project_id, user_id, perm, PRIMARY KEY(project_id,user_id))` - - `books(id, project_id, title, ord)` · `chapters(id, book_id, project_id, title, doc JSON, rev, ord, updated_at)` - - `entities(id, project_id, kind, name, fields JSON, created_at, updated_at, UNIQUE(project_id,kind,id))` - - `boards(id, project_id, nodes JSON, edges JSON, updated_at)` - - `ai_sessions(id, project_id, role, model, settings JSON, messages JSON, suggestions JSON, updated_at)` - - `audit(id, at, user_id, action, detail JSON)` -2. Indexes: `chapters(project_id)`, `entities(project_id,kind)`, `projects(owner_id)`. -3. Seed bootstrap: on first boot with `PUBSCRIPT_ADMIN_EMAIL`, create that admin user (password from `PUBSCRIPT_ADMIN_PASSWORD` or interactive prompt) — idempotent. - -### 3. Auth (`server/src/auth.ts`) -1. `POST /api/auth/register` — email+password (argon2id), creates `author`; first-ever user becomes `admin` if no users exist. -2. `POST /api/auth/login` → sets httpOnly `ps_session` cookie (web) **and** returns a bearer token (desktop uses bearer). `POST /api/auth/logout` revokes. -3. `GET /api/me` → current user + role. -4. Middleware: `requireAuth`, `requireAdmin`, `canAccessProject(projectId, perm)` — enforces owner-or-shared. Every content route uses these. -5. Rate limit `@fastify/rate-limit` on auth endpoints (10/min/IP). - -### 4. Content API (`server/src/routes/`) -Implement against the core types (zod-validated): -- `projects.ts`: `GET /api/projects` (owned ∪ shared), `POST /api/projects`, `GET/PATCH/DELETE /api/projects/:id`, `POST /api/projects/:id/shares` (invite by email, `read|edit`), `DELETE` share, `POST /api/projects/:id/transfer`. -- `books.ts` + `chapters.ts`: CRUD + `PATCH /api/chapters/:id` (body: `{ doc, baseRev }` — **optimistic concurrency**: 409 if `baseRev` stale). -- `entities.ts`: CRUD per kind; `GET /api/projects/:id/entities?kind=&q=` (fuzzy). -- `boards.ts`, `ai.ts` (session CRUD + `POST /api/ai/sessions/:id/suggestions`), `notes.ts`. -- `admin.ts` (`requireAdmin`): `GET /api/admin/users`, `POST /api/admin/users` (create), `PATCH /api/admin/users/:id` (role/disable), `GET /api/admin/activity` (audit tail). -- `ws.ts`: `GET /ws` (WebSocket, auth via token query or cookie) → fan-out `StoreEvent`s per project to subscribed clients. - -### 5. `ServerAdapter` (`packages/api-client`) -1. New package `packages/api-client` implementing `StorageAdapter`: - - REST calls with bearer (desktop) or cookies (web) — chosen by `navigator`/Tauri detection. - - `onEvent` → WebSocket subscription; reconnect with backoff; event → `StoreEvent`. - - 409 handling → emit `conflict` event (UI shows "reloaded newer version"). -2. **`LocalAdapter`** (`packages/storage-local`): Dexie (IndexedDB) tables mirroring the SQLite schema; same method signatures; `onEvent` via a local event bus. In Tauri, persist to disk via `@tauri-apps/plugin-fs` (same Dexie code, different driver). - -### 6. Autosave engine (`packages/core/src/autosave.ts`) -1. `createAutosave(adapter, { intervalMs = 800 })` — debounced `saveDocument`; tracks `dirty`, `saving`, `lastSavedAt`, exposes `onStatus` for the top-bar indicator (● unsaved / ◌ saving / ✓ saved 12:04). -2. On `conflict`: keep local, fetch remote, show a merge dialog (v1: last-write-wins with explicit user choice). -3. `beforeunload` flush. - -### 7. App wiring (`apps/web`) -1. **Auth screens:** `/login`, `/register` (use `packages/ui` components). On boot: `GET /api/me` (web) or stored token (desktop) → set current user; no server configured (local mode) → skip auth, banner "Local mode — data stays on this device". -2. **Projects page:** list (owned + shared, with share badges), new-project wizard (title, cover, first book), delete/duplicate. -3. **Project workspace:** left rail = document tree (Book → Part → Chapter, dnd reorder, per-chapter word count); center = placeholder "editor slot" (real editor lands in Chunk 3); right rail = placeholder for AI/notes. -4. **Settings → Accounts** (admin sees user management stub — full UI in Chunk 10). -5. **Seed script** `server/src/seed.ts` (`pnpm --filter server seed`): creates a demo project "The Glass Harbor" — 3 chapters of real prose, 4 characters with relationships, 1 world location, 1 board with 6 nodes — so every later chunk has something to show. - -### 8. Contract test suite (`packages/core/src/storage.contract.test.ts`) -1. One suite, parameterized over **both** adapters (Local via Dexie in jsdom/happy-dom; Server against a real Fastify instance + SQLite in `:memory:` or a temp file): - - project CRUD round-trips; document save/load byte-identical; entity CRUD per kind; board save/load; AI session + suggestions round-trip. - - multi-tenant isolation: user A can never read user B's project (server adapter only — Local adapter trivially passes). - - `onEvent`: a save in one "client" is observed by a second subscriber (WS on server; bus on local). - - optimistic concurrency: stale `baseRev` → conflict. -2. Wire as `pnpm contract` and add to CI. - ---- - -## Acceptance criteria (all must pass) -- [ ] `pnpm contract` green against **both** adapters. -- [ ] Register → login → create project → save a document → reload: content intact. -- [ ] Admin creates a second user; that user logs in, sees only their projects; admin shares a project (edit) → it appears; disable → their session is dead within one event round-trip. -- [ ] Autosave indicator cycles correctly; kill the tab mid-typing, reopen → ≤1 s of work lost. -- [ ] Two browser windows on the same project: save in one appears in the other within ~1 s (WS). -- [x] Seed project opens with 3 chapters, 4 linked characters, 1 board. -- [ ] CI green including the contract suite. - -## Definition of done -The app is a real multi-user product skeleton: accounts, roles, sharing, and a storage layer that is *provably* identical whether data lives on disk or on a server. Chunks 3–10 only add features on top of this spine. - -## Risks & fallbacks -- **Dexie in Tauri vs. browser** → abstract the driver behind one `KVBackend` interface; test both. -- **WS reconnect storms** → exponential backoff + jitter; cap at 30 s. -- **JSON doc storage size** → store ProseMirror JSON (not HTML); chapters are individually loadable, so a 100k-word novel never loads as one blob. diff --git a/CHUNK#3.md b/CHUNK#3.md deleted file mode 100644 index 6aac633..0000000 --- a/CHUNK#3.md +++ /dev/null @@ -1,62 +0,0 @@ -# CHUNK #3 — Writing Surface - -**Duration:** ~3 days · **Prereqs:** CHUNK #2 (storage spine, autosave) · **Follows:** CHUNK #2 · **Unblocks:** CHUNK #6 (export), #7 (spellcheck), #9 (AI suggestions render over this editor) - -**Purpose:** Build the actual writing experience: the TipTap 3 editor with the full node schema, the document tree, the font system, focus/typewriter modes, live stats, and Markdown import/export — performing well enough that a 100k-word project opens in under 2 seconds and typing never lags. - ---- - -## Steps - -### 1. Editor package (`packages/editor`) -1. `packages/editor/src/schema.ts` — TipTap extensions, all registered once: - - Base: `Document`, `Paragraph`, `Text`, `Heading` (1–3), `Blockquote`, `HorizontalRule`, `HardBreak`, `Bold`, `Italic`, `Strike`, `Code`, `CodeBlock`. - - **`SceneBreak`** (node) — a centered `* * *` divider, draggable, with a NodeView; splits scenes for stats and AI targeting. - - **`Footnote`** (inline + a `FootnoteArea` block at doc end) — v1 simple: number, reference, definition. - - **`CharacterMention`** (inline atom) — rendered as a styled chip linking to a character entity (inserted by autocomplete in Chunk 4; the node type lands here). - - Markdown I/O: `prosemirror-markdown` with rules for scene breaks (`***`), mentions (`@Name`), footnotes. -2. `packages/editor/src/Editor.tsx` — the headless wrapper: - - `createEditor({ doc, onUpdate, decorations })` returning `{ view, state, dispatch, applyTransaction }`. - - **Decoration pipeline:** `useDecorations(view, sources[])` — merges decorations from spellcheck (Chunk 7), AI suggestions (Chunk 9), and mentions into one `DecorationSet`, memoized, recomputed only on relevant transactions. This is the seam later chunks plug into — build it now, even with zero sources. - - `applyEdit(range, before, after)` — the transaction used by "Accept suggestion". -3. `packages/editor/src/perf.ts` — `measureTypingLatency()` (rAF-based) + a dev overlay showing ms/keystroke. - -### 2. Writing view (`apps/web/src/features/write/`) -1. `WritingView.tsx` — layout: document tree (left, from Chunk 2), editor (center), context rail (right: current chapter's linked characters/notes, collapsible). -2. **Toolbar** — floating (appears on selection) + docked (top): headings, bold/italic/strike, blockquote, scene break, footnote, mention, undo/redo. Slash menu (`/`) for block insertion. -3. **Keyboard map** (`packages/editor/src/keys.ts`): `Ctrl/Cmd+B/I/K`, `Ctrl+/` scene break, `Alt+↑/↓` chapter nav, `F2` focus mode. -4. **Focus mode** — dims everything but the current paragraph band; **Typewriter mode** — auto-scrolls the active line to a fixed vertical position. Both toggle from toolbar + settings. -5. **Stats bar** (footer): word count (chapter/book/project), session words, reading time; per-chapter counts in the tree. -6. **Font system** (`packages/ui/src/fonts.ts`): bundle via `@fontsource` — **EB Garamond**, **Source Serif 4**, **Inter**, **Space Grotesk**, **JetBrains Mono** (+ weights). Settings controls: family, size (14–22 px), leading (1.3–2.0), measure (50–90 ch), margins. Apply via CSS variables on the editor root; persist in settings store. -7. **Markdown I/O:** `exportMarkdown(doc)` / `importMarkdown(text)` in `packages/editor/src/markdown.ts`. Import dialog in the tree ("Import .md…") with a preview + conflict check; per-chapter and whole-project export (feeds Chunk 6). - -### 3. Performance work -1. **Lazy chapters:** only the open chapter's doc is in the editor; switching chapters swaps state without unmounting the view. -2. **Virtualize the tree** for books with 500+ chapters (`@tanstack/react-virtual`). -3. Add a Playwright perf test (`apps/web/e2e/perf.spec.ts`): seed a 100k-word doc (script generates 40k paragraphs), assert: open < 2 s; 20 keystrokes median latency < 16 ms (using the dev overlay's API); tree scroll at 60 fps. - -### 4. Polish -1. Placeholder states (empty book, empty chapter), save indicator already wired (Chunk 2). -2. Accessibility: visible focus rings, `aria` on toolbar/tree, full keyboard operation of the tree. -3. `docs/EDITOR.md` — node reference (for humans) + "how to add a node type" (for agents). - ---- - -## Acceptance criteria (all must Pass) -- [ ] Type a full chapter: no dropped keystrokes; latency overlay < 16 ms median on a 100k-doc. -- [ ] 100k-word seeded project opens < 2 s (Playwright perf test in CI). -- [ ] Scene breaks insert via `***` and slash menu; stats split by scene. -- [ ] Footnotes render and export to Markdown correctly. -- [ ] Font/size/leading/measure changes apply live and persist across reload. -- [ ] Focus + typewriter modes work and are keyboard-toggleable. -- [ ] Import a 50-chapter `.md` file → tree + docs correct; re-export round-trips (diff of re-imported Markdown is empty modulo whitespace). -- [ ] Decoration pipeline: register a fake decoration source → renders; (Chunk 9 will replace it with real AI suggestions.) -- [ ] A11y pass: toolbar fully keyboard-operable; contrast ≥ 4.5:1 in all four themes. - -## Definition of done -An author can sit down and *write* — fast, beautiful, with the story structure around them — and the editor exposes a stable decoration/edit API that spellcheck and AI plug into without touching core code. - -## Risks & fallbacks -- **TipTap v3 API churn** → pin the exact v3 release; wrap all v3-specific calls in `packages/editor` so the app never imports TipTap directly. -- **Markdown round-trip loss** (footnotes/mentions) → keep a canonical ProseMirror-JSON store; Markdown is an *exchange* format, not the source of truth (document this in `docs/EDITOR.md`). -- **Perf test flakiness in CI** → run perf specs on a dedicated runner with 3 runs, take the median; fail only on >2× regression. diff --git a/CHUNK#3_PROGRESS.md b/CHUNK#3_PROGRESS.md deleted file mode 100644 index 5ca98fd..0000000 --- a/CHUNK#3_PROGRESS.md +++ /dev/null @@ -1,34 +0,0 @@ -# CHUNK #3 — Editor & Writing View - -**Status:** In progress. Steps 1–3 committed. Step 4 (polish) in progress. - -## Bug fixes -- [x] **UI disappears when navigating to Write** — WritingView passed `chapter.doc.content` (a bare ProseMirror node array) as editor `content`, so `EditorState.create` built a doc node with an undefined type and threw `Cannot read properties of undefined (reading 'schema')`, unmounting the write view. Fixed by passing the full doc object (`{ type: 'doc', content }`) on init and on `setContent` sync. Verified in a headless browser: editor renders the seeded chapter ("The Glass Harbor") with no console errors. Regression tests in `packages/editor/src/use-editor-doc.test.ts`. - -## Progress -- [x] Step 1: Editor package (TipTap 3 schema, wrapper, markdown, key bindings) — committed -- [x] Step 2: Writing view in apps/web wired to storage adapter - - [x] Add @pubscript/editor to web deps - - [x] project-store (zustand) over storageLocal - - [x] client-side seed for local adapter - - [x] WritingView component (editor + autosave + word/scene count + chapter nav) - - [x] wire Write route -- [x] Step 3: Performance (autosave via createAutosave, idle detection via createIdleDetector) -- [ ] Step 4: Polish (editor.css import ✅, fonts 🟡, SVG monogram logo ✅) - - [x] editor.css import in main.tsx - - [x] SVG monogram logo in Sidebar - - [x] Sidebar reflects the real seeded project ("The Glass Harbor") + live word count - - [ ] font family/size/leading/measure controls wired to CSS variables (size only so far) - -## Key facts -- Editor package exports: usePubScriptEditor, createEditorInstance, emptyDecorationSource, throttle, createIdleDetector, serializeToMarkdown, parseMarkdown, node types + constants. -- PubScriptEditor React component re-exports EditorContent + Editor type from @pubscript/editor (web never imports TipTap directly). -- storageLocal (offline-first, IndexedDB) chosen for web wiring (server routes require auth). -- Demo prose lives in server/src/seed.ts ("The Glass Harbor", 3 chapters, 4 chars, 1 world, 1 board). -- Web app uses @pubscript/core/local (storageLocal) — no server needed to run offline. -- Server runs on PORT (default 8080; set PORT=8081 when 8080 is taken by Docker WordPress). - -## Remaining (finish Step 4) -- Wire font family/leading/measure settings to CSS variables (fontSize already wired via applyFontSettings). -- Add a settings UI control for font size (or confirm existing one applies). -- Final polish pass: focus mode visual, empty-state copy, a11y labels. diff --git a/CHUNK#4.md b/CHUNK#4.md deleted file mode 100644 index 61ece4f..0000000 --- a/CHUNK#4.md +++ /dev/null @@ -1,68 +0,0 @@ -# CHUNK #4 — Characters & World-Building - -**Duration:** ~2 days · **Prereqs:** CHUNK #2 (entity storage), CHUNK #3 (editor for mentions) · **Follows:** CHUNK #3 · **Unblocks:** CHUNK #5 (plot links entities), #9 (AI context pulls from entities) - -**Purpose:** Make the story's moving parts first-class: character sheets with typed relationships, flexible world items, fast fuzzy search, backlinks, and `@mention` links from prose to entities. After this chunk, the "tools to facilitate and integrate characters, world building" requirement is fully delivered. - -**Status (updated 2026-10-25):** 🟡 In progress. Data layer and entity UI layer complete and CI-green (`pnpm typecheck`, `pnpm test`, `pnpm lint`, `pnpm build` all pass; 65 tests green across the workspace). See "Remaining" below. - ---- - -## Steps - -### 1. Entity engine (`apps/web/src/features/entities/`) -1. **Generic field renderer** (`EntityFields.tsx`) — driven entirely by `ENTITY_FIELDS` from `packages/core/src/schema.ts` (Chunk 2): renders `text`, `textarea`, `select`, `list` (add/remove/reorder chips), `link` (entity reference) fields. Adding a field to the registry = it appears in the UI. No per-entity UI code. -2. **Entity panel** (`EntitiesView.tsx`) — tabbed: **Characters · World · Notes**. Each tab: search box (fuzzy, <50 ms at 500 entities), grouped list, create button, card grid. -3. **Entity editor** (`EntityEditor.tsx`) — right-rail drawer: fields from the registry, plus: - - **Relationships editor** (characters): add `{ target: character, kind: 'ally'|'rival'|'family'|'mentor'|'romantic'|'custom', note }`; render as a list with the linked character's chip. - - **Backlinks** tab: "Appears in" — chapters containing a mention of this entity, plus entities that reference it. - - Duplicate, delete (with confirm + backlink warning), "pin to context rail". -4. **World kinds** — `location`, `faction`, `item`, `glossary`, `system` — each with its field set from the registry (e.g. location: region, climate, key-people list; faction: goals, rivals list; item: origin, powers, holder). -5. **Relationship map** (`RelationshipMap.tsx`) — a small force/graph view (reuse React Flow with a static layout) of the selected character's web; click a node → open that character. - -### 2. Mentions & linking -1. **Autocomplete** (`packages/editor/src/mention.ts`): typing `@` opens an entity picker (fuzzy over characters + world items); Enter inserts a `CharacterMention` atom (node from Chunk 3) carrying `entityId`. -2. **Mention chip** — styled, hover → mini popover (name + one-line bio + "Open sheet"); click → opens the entity editor. -3. **Backlink computation** (`packages/core/src/backlinks.ts`): walk docs' JSON for mention nodes + entity-name matches; cache per project, invalidate on doc save. Power the "Appears in" tab and the context rail. -4. **Consistency check** (small win, big value): `findInconsistencies(project)` — e.g. a character marked `status: 'dead'` who is mentioned *after* their death chapter (by chapter order). Surface as a warnings list in the Characters tab. - -### 3. Search & navigation -1. Global search (`Ctrl/Cmd+K` palette, from Chunk 1) now indexes: chapters, entities, notes, boards. Grouped results, jump-to with context snippet. -2. Context rail (right side of WritingView) shows: this chapter's mentioned characters (auto), pinned entities, and related notes. - -### 4. Data & seed -1. Extend `server/src/seed.ts`: the demo project's 4 characters get full sheets (bio, voice, 2 relationships each); 5 world items (a city, a faction, an artifact, 2 glossary terms). -2. `docs/DATA_MODEL.md` — the entity registry table (kind → fields) as the human reference. - -### 5. Tests -1. Unit: field renderer per type; fuzzy search (500 entities < 50 ms); backlink computation (property test: insert mention → backlink appears; remove → disappears). -2. Contract: entity CRUD + `?q=` search through both adapters (add to Chunk 2's suite). -3. E2E: create character → `@mention` it in a chapter → backlinks tab lists the chapter → open sheet from the chip. - ---- - -## Acceptance criteria (all must pass) -- [~] Create a character with all field types (incl. list + link) via the generic renderer — no custom code added. Generic renderer done for `text`/`textarea`/`checkbox`/`tags` (list); `link` field type pending. -- [x] Fuzzy search over 500 seeded entities returns in < 50 ms (measured in test). -- [~] `@` autocomplete → chip → popover → open sheet, all working; works while typing fast (no stale menus). Autocomplete + chip + open sheet done; hover popover pending. -- [x] Backlinks correct and live-updating (save doc → "Appears in" updates within one event round-trip). -- [ ] Consistency check flags the seeded dead-character-after-death case. Logic in `packages/core/src/consistency.ts` done; UI wiring + seeded case pending. -- [x] Relationship map renders and navigates. -- [ ] Global palette finds a chapter, a character, and a note from one box. -- [x] Contract suite still green on both adapters. - -## Remaining work -- Wire `findInconsistencies()` into the Characters tab as a warnings list (Chunk 3.2). -- Index entities in the global `Ctrl/Cmd+K` palette (Chunk 3.1). -- Context rail (right side of WritingView): this chapter's mentioned characters, pinned entities, related notes (Chunk 3.2). -- Extend `server/src/seed.ts`: full character sheets (bio, voice, 2 relationships each) + 5 world items (Chunk 4.1). -- `docs/DATA_MODEL.md`: the entity registry table (kind → fields) as the human reference (Chunk 4.2). -- E2E: create character → `@mention` it in a chapter → backlinks tab lists the chapter → open sheet from the chip (Chunk 5.3). - -## Definition of done -The author's canon — who, what, where — is structured, searchable, linked to the prose, and checkable. This is the data the AI roles will reason from in Chunk 9. - -## Risks & fallbacks -- **Fuzzy search quality** → use a small, well-tested scorer (e.g. `fuse.js` or a hand-rolled subsequence scorer in `packages/core`); keep it dependency-light and unit-tested. -- **Backlink cost on huge docs** → compute incrementally on save (diff the doc), not full re-scan; cache in the DB (`mentions(entity_id, chapter_id)` table). -- **Mention vs. plain text ambiguity** → only explicit `@`-inserted atoms are links; name-matching is *suggested*, never auto-applied. diff --git a/CHUNK#5.md b/CHUNK#5.md deleted file mode 100644 index dcc7494..0000000 --- a/CHUNK#5.md +++ /dev/null @@ -1,60 +0,0 @@ -# CHUNK #5 — Plot & Whiteboard - -**Duration:** ~3 days · **Prereqs:** CHUNK #2 (entity storage) · **Follows:** CHUNK #4 · **Unblocks:** CHUNK #9 (AI reasons over plot structure) - -**Purpose:** Give authors three ways to see and shape their story's structure — a beat board with genre templates, a configurable timeline, and a free-form whiteboard — all backed by React Flow 12 and all linked to chapters and entities. After this chunk, plot structure is first-class and the whiteboard handles 200+ nodes at 60 fps. - ---- - -## Steps - -### 1. Beat board (`apps/web/src/features/plot/`) -1. **Beat editor** (`BeatEditor.tsx`): a beat is a `PlotBeat` with `template`, `title`, `description`, `status` (`draft` → `revised` → `locked`), optional `chapterId` link, `order`, `notes`. Edit inline in a list or in a right-rail drawer. -2. **Templates** (`apps/web/src/features/plot/templates.ts`): ship four — **Three-Act** (Setup / Confrontation / Resolution), **Save-the-Cat** (all 15 beats), **Seven-Point** (Hook, Plot Turn 1, Midpoint, Plot Turn 2, Climax), and **Custom**. Selecting a template seeds N empty beats with the template's titles; the author edits from there. -3. **Board view** (`BeatBoard.tsx`): horizontal swimlanes per act/template-section, or a single ordered list. Drag to reorder; each beat shows status color, word count of linked chapter, and a "link to chapter" picker. Progress bar = % of beats that are `revised` or `locked`. -4. **Locking**: a `locked` beat is read-only until unlocked (with reason). Prevents accidental edits during a draft pass. - -### 2. Timeline (`apps/web/src/features/timeline/`) -1. **TimelineView.tsx**: a horizontal time axis. Events (`TimelineEvent`) carry `when: {era, day} | date` (configurable: real dates OR a fictional calendar with named eras + day numbers), `title`, `description`, optional `chapterId`, and `links[]` (causal links to other events). -2. **Filters**: show/hide by character (events mentioning a character), by location, by era. Multi-select. -3. **Causal links**: draw an arrow from cause → effect; rendered as a separate layer behind the events. Click an event → open a read-only panel (full edit in the beat/entity system). - -### 3. Whiteboard flow editor (`packages/boards`) -1. **Board model** (`packages/boards/src/types.ts`): `Board { id, projectId, nodes: FlowNode[], edges: FlowEdge[] }` where a `FlowNode` is a typed card — `character`, `world`, `chapter`, `note`, `image`, or `group`. Edges are typed (`leads-to`, `contrasts`, `causes`, `custom`). -2. **Node views** (`packages/boards/src/nodes/`): each node type renders a styled card with the entity's key fields (name, role, word count, etc.) and a drag handle. Groups are translucent containers that children snap to. -3. **Canvas** (`packages/boards/src/BoardCanvas.tsx`): React Flow 12 with: - - **Minimap** (top-right), **zoom/pan** (mouse + keyboard), **auto-layout** (grid, force, or radial) via `@xyflow/layout`. - - **Multi-select** (shift-click, marquee), **group/ungroup**, **align** (distribute edges). - - **Keyboard**: `Del` delete, `Ctrl/Cmd+D` duplicate, `Ctrl/Cmd+G` group, arrow keys nudge. - - **Persistence**: every node/edge change → autosave (Chunk 2 engine) with optimistic `rev`. -4. **Performance** (`packages/boards/src/perf.ts`): virtualize off-screen nodes, memoize node views, cap visible edges. **200 nodes at 60 fps** is the hard budget — a Playwright perf test seeds 200 nodes and measures frame time. - -### 4. Cross-links -1. **Board ↔ chapters**: a `chapter` node links to a `Chapter` doc; click → opens it in the writer. -2. **Board ↔ entities**: `character`/`world` nodes link to entities; click → opens the entity editor. -3. **Import from beat board**: "Add beats to board" bulk-imports locked/revised beats as nodes. - -### 5. Tests -1. Unit: template seeding (correct beat count + titles); board save/load round-trip (both adapters). -2. E2E: create a 6-node board, drag nodes, draw edges, save → reload → identical layout. -3. Perf: 200-node board at 60 fps (Playwright). - ---- - -## Acceptance criteria (all must pass) -- [ ] Select "Save-the-Cat" → 15 beats seeded with correct titles; edit one → saves. -- [ ] Reorder beats by drag; progress bar updates (% revised/locked). -- [ ] Lock a beat → editing is blocked with a reason prompt; unlock → editable. -- [ ] Timeline renders events on a configurable calendar (real dates OR era/day); filters by character/location work. -- [ ] Whiteboard: create nodes of every type, drag, group, align, draw typed edges, save → reload identical. -- [ ] 200-node board renders at 60 fps (Playwright perf test in CI). -- [ ] A chapter node opens the writer; a character node opens the entity editor. -- [ ] Contract suite still green on both adapters. - -## Definition of done -Authors can structure a story three ways — beats, timeline, whiteboard — all linked to the prose and entities, and the whiteboard stays smooth at scale. - -## Risks & fallbacks -- **React Flow 12 API churn** → pin the exact version; wrap all calls in `packages/boards` so the app never imports `@xyflow/react` directly. -- **200-node perf** → if force-layout is too slow, default to grid layout and make force-layout opt-in; document the node cap in `docs/BOARDS.md`. -- **Drag-and-drop jank on low-end machines** → virtualize off-screen nodes; throttle autosave to 800ms. diff --git a/CHUNK#6.md b/CHUNK#6.md deleted file mode 100644 index 5daa03c..0000000 --- a/CHUNK#6.md +++ /dev/null @@ -1,77 +0,0 @@ -# CHUNK #6 — Export Pipeline - -**Status:** ✅ Complete — committed `63f34b3` (feat(export): Chunk #6 Export Pipeline). All 7 exporters, the ExportDialog, and the test suite are green. - -**Duration:** ~3 days · **Prereqs:** CHUNK #3 (editor, ProseMirror JSON) · **Follows:** CHUNK #5 · **Unblocks:** CHUNK #11 (packaging ships exports) - -**Purpose:** Ship a single export engine that turns a project's ProseMirror JSON into Markdown, single-file HTML, EPUB 3, DOCX, ODT, and PDF — all with title/author/cover/TOC/styling options. Everything shares one flat content model so formats stay consistent and tests are golden-file based. - ---- - -## Steps - -### 1. Shared flat model (`packages/exporters/src/model.ts`) -1. `flattenProject(project)` → a linear list of `Block`s: `{ type: 'heading'|'paragraph'|'sceneBreak'|'blockquote'|'codeBlock'|'list'|'footnote', text, marks[], level?, children? }`, plus frontmatter `{ title, author, language, cover, chapters: [{title, order}] }`. -2. This is the single source every exporter consumes — no exporter touches raw ProseMirror JSON directly. - -### 2. Markdown & HTML (`packages/exporters/src/md.ts`, `html.ts`) -1. **Markdown export** (`exportMarkdown(blocks)`): headings (`#`–`###`), scene breaks as `***`, blockquotes, code fences, lists, footnotes as `[1]` refs with a notes section. Round-trips with Chunk 3's importer. -2. **Single-file HTML** (`exportHtml(blocks, opts)`): one self-contained `.html` with inline CSS (theme-aware: Ink/Parchment/Nocturne/Terminal), a generated TOC (anchored headings), print styles (`@page` margins), and a cover page. No external assets. - -### 3. EPUB 3 (`packages/exporters/src/epub.ts`) -1. Build a valid EPUB 3 container **by hand** (no runtime deps): - - `mimetype` (uncompressed, `application/epub+zip`) first in the zip. - - `META-INF/container.xml` → `content.opf`. - - `content.opf`: metadata (title, author, language, UUID, cover), manifest (`chapterN.xhtml`, `toc.ncx`, `cover.svg`, `style.css`), spine (chapter order). - - `chapterN.xhtml`: one file per chapter, semantic HTML5 (`

`, `

`, `


` for scene breaks), a `nav` TOC. - - `cover.svg`: renders the project's cover color + title/author. -2. Zip with `jszip` (already a dep). Output `.epub`. -3. **Validation**: golden-file test + `epubcheck` (run in CI via a small Node wrapper or Docker image). - -### 4. DOCX (`packages/exporters/src/docx.ts`) -1. Use the maintained **`docx`** npm package: `Document` → `Section` → `Paragraph`/`Heading`/`Quote`; add a `TableOfContents` (field `TOC`), a cover paragraph, and page breaks between chapters. -2. Styles: map themes to Word styles (font, size, heading colors). Export `.docx`. - -### 5. ODT (`packages/exporters/src/odt.ts`) -1. Build a valid OpenDocument container (parallel to EPUB): - - `mimetype` (`application/vnd.oasis.opendocument.text`) first, uncompressed. - - `content.xml`: `text:p` paragraphs, `text:h` headings, `text:section`, with `style:name` references. - - `manifest.xml`, `meta.xml` (title/author), `styles.xml` (basic paragraph styles), `thumbs/thumbnail.png` (optional). -2. Zip with `jszip`. Output `.odt`. (LibreOffice-openable; golden-file unzip-validate in CI.) - -### 6. PDF (`packages/exporters/src/pdf.ts`) -1. **Two backends** (chosen by env): - - **Server/desktop:** headless Chromium `page.pdf()` over the single-file HTML → pixel-perfect, themes intact, real TOC. This is the default. - - **Browser-only fallback:** `jspdf` + `html2canvas` (lower fidelity, no TOC automation) — used when no server is available. -2. `exportPdf(blocks, opts)` returns a `Uint8Array`. Add title page, page numbers (`- {page}` footer), and `@page` margins from the HTML path. - -### 7. Export dialog (`apps/web/src/features/export/ExportDialog.tsx`) -1. Select: format (multi-select), scope (current chapter / whole book / whole project), options (cover, TOC, theme, page size). -2. Live preview thumbnail (HTML/EPUB), progress bar, download button(s). Desktop uses Tauri FS save dialog; web uses native download. -3. **Batch export**: "Export all formats" → zips results. - -### 8. Tests -1. **Golden-file tests**: for each format, snapshot the output bytes of the seed project; assert stable across runs (whitespace-normalized for HTML/PDF). -2. **Round-trip**: MD export → import → MD export is identical. -3. **Validation**: `epubcheck` on every EPUB; unzip + XML well-formedness on ODT/EPUB. -4. **CI**: run epubcheck; unzip-validate all archives. - ---- - -## Acceptance criteria (all must pass) -- [x] Export a project to MD, HTML, EPUB, DOCX, ODT, PDF — all succeed from the dialog. -- [x] EPUB passes `epubcheck` with zero errors; opens in Calibre/Readers. -- [x] ODT opens cleanly in LibreOffice; styles preserved. -- [x] DOCX opens in Word; TOC field present (update-on-open). -- [x] PDF has title page, TOC (HTML backend), page numbers, correct margins. -- [x] Theme applied in HTML/EPUB/PDF (Ink dark / Parchment light, etc.). -- [x] Golden-file tests stable; round-trip MD identical. -- [x] CI validates EPUB + unzip-validates all archives. - -## Definition of done -An author can export a finished manuscript to every major format from one dialog, with consistent styling and validated archives. - -## Risks & fallbacks -- **EPUB/ODT spec edge cases** → golden-file tests + `epubcheck` in CI; keep the container builders tiny and unit-tested. -- **PDF fidelity without a server** → prefer headless Chromium; fall back to `jspdf` only when needed and note the limitation. -- **Cover image sizing** → normalize covers to EPUB-recommended dimensions (e.g. 1600×2560) before embedding. diff --git a/CHUNK#7.md b/CHUNK#7.md deleted file mode 100644 index 31dc8be..0000000 --- a/CHUNK#7.md +++ /dev/null @@ -1,75 +0,0 @@ -# CHUNK #7 — Spell & Grammar - -**Status:** ✅ Complete — all steps implemented, tests passing, build green. - -**Duration:** ~2 days · **Prereqs:** CHUNK #3 (editor, decoration pipeline) · **Follows:** CHUNK #6 · **Unblocks:** CHUNK #9 (AI suggestions share the decoration pipeline) - -**Purpose:** Add offline spellcheck (hunspell compiled to WASM) and optional grammar/style checking (self-hostable LanguageTool server), rendered as inline squiggles with a side panel to jump/apply/ignore. Everything runs in-browser with zero network for spellcheck; grammar is opt-in. - ---- - -## Steps - -### 1. Spellcheck engine (`packages/editor/src/spell/`) -1. **hunspell-WASM** (`packages/editor/src/spell/hunspell.ts`): lazy-load `hunspell-wasm` (codecs + dictionaries from the `hunspell-wasm/dicts` package). Initialize with a default dictionary (`en_US`) + a per-user custom dictionary. -2. **Dictionary management** (`packages/editor/src/spell/dicts.ts`): - - Load dictionaries on demand (lazy, per active language). - - **Custom dictionary**: author-approved words persisted to `localStorage` (web) / Tauri store / server profile. Words added here are never re-underlined. - - Language detection: per-document `lang` attribute (default from settings); switch mid-doc. -3. **Checker** (`packages/editor/src/spell/checker.ts`): given a ProseMirror doc, produce a list of `SpellHit { from, to, word, suggestions: string[] }` — skipping code, already-accepted mentions, and custom-dictionary words. - -### 2. Grammar engine (`packages/editor/src/grammar/`) -1. **LanguageTool adapter** (`packages/editor/src/grammar/languagetool.ts`): - - Configured via provider settings: `{ kind: 'languagetool', baseUrl }` (default `http://localhost:8080`, self-hosted Docker). - - `check(text, lang)` → POST to `/v2/check`, parse `matches[]` → `GrammarHit { from, to, message, replacements[], ruleId }`. - - Debounced (800ms) + cached per text hash to avoid spamming the server. -2. **Self-hostable server** (`docker/languagetool/`): a `docker-compose` snippet that runs the official LanguageTool image; documented in `docker/README.md`. API is OpenAI-compatible-ish (documented endpoints). - -### 3. Decoration integration (`packages/editor/src/decorations/`) -1. Plug both engines into the **decoration pipeline** built in Chunk 3 (`useDecorations`): - - Spellcheck → red squiggle (`Decoration.inline` with `class="ps-spell"`). - - Grammar → blue squiggle (`class="ps-grammar"`). -2. Memoize: decorations recomputed only on relevant transactions (doc change, accept/reject). - -### 4. Side panel (`apps/web/src/features/check/CheckPanel.tsx`) -1. Lists all hits grouped by chapter: `word/message`, suggested replacements (click to apply), "Add to dictionary", "Ignore this time", "Ignore all". -2. **Jump**: clicking a hit scrolls the editor to that range and highlights it. -3. Counts: X spelling / Y grammar issues remaining; per-chapter breakdown. - -### 5. Settings -1. Toggle spellcheck/grammar on/off, per-language dictionaries, grammar server URL, check-on-type vs. check-on-save. - -### 6. Tests -1. Unit: `checker` on a known doc → correct `SpellHit`s; custom dictionary word ignored. -2. Unit: grammar adapter parses a mock LanguageTool response correctly. -3. E2E: type a misspelled word → squiggle appears; add to dictionary → squiggle gone; reload → persists. -4. E2E (optional): with LanguageTool running, grammar hit appears + apply suggestion edits the text. - ---- - -## Acceptance criteria (all must pass) -- [x] Misspelled word → red squiggle; suggestions shown in panel. -- [x] "Add to dictionary" → word never underlined again; persists across reload. -- [x] Grammar (if LanguageTool configured) → blue squiggle; apply suggestion edits text. -- [x] Panel jumps to a hit and highlights it in the editor. -- [x] Code blocks and mentions are never spellchecked. -- [x] Toggle spellcheck off → no squiggles; setting persists. -- [x] Contract suite still green on both adapters. - -## Definition of done -Authors get inline spellcheck (fully offline) and optional grammar checking, with a clean panel to review and act — no wall of unactionable errors. - -## Completed (implementation notes) -- **Spell engine** (`packages/editor/src/spell/`): `hunspell-engine.ts` wraps `hunspell-wasm` with a lazy `load()` + `check()`/`suggest()` API; `spell.ts` provides `findSpellErrors()` (tokenizes with `WORD_RE`, dedupes, honors `minWordLength`) and `applySuggestion()`. Dictionaries are base64-decoded with `atob()` + `TextDecoder` (browser-safe, per project convention #74). -- **Grammar engine** (`packages/editor/src/grammar/`): `grammar.ts` has a regex tokenizer (`SENTENCE_RE`, `WORD_RE`) plus `parseGrammarMatches()` that maps LanguageTool `/v2/check` `matches[]` → `GrammarIssue[]` (offset/length normalization, `ruleId`/`category`/`message`/`replacements`). -- **CheckService** (`packages/editor/src/check/check-service.ts`): orchestrates both engines, exposes `checkText()`/`checkBlock()`/`addWordToDictionary()`/`ignoreWord()`/`ignoreAll()`, and emits `check:progress` events. -- **Decorations** (`packages/editor/src/decorations/`): `toCheckedIssues()` merges spell + grammar hits into `CheckedIssue[]`; `collectBlock()` assembles block text (single source of truth, per #73); `useDecorations()` renders red squiggles (`ps-spell`) and blue squiggles (`ps-grammar`) and applies accepted suggestions. -- **Web integration** (`apps/web/src/`): `useCheckService.ts` hook wires the service into `WritingView.tsx`; `CheckPanel.tsx` shows counts, per-issue message/replacements, "Add to dictionary", "Ignore"/"Ignore all", and jump-to-issue navigation; `check.css` styles squiggles + panel; `main.tsx` imports `check.css`. -- **Settings**: spellcheck toggle + grammar toggle in `Settings.tsx` (persisted via `useSettings`). -- **Docker**: `docker/languagetool/docker-compose.yml` + `README.md` for self-hosting LanguageTool (opt-in, graceful degradation if unreachable). -- **Tests**: `spell.test.ts` (9 tests) and `grammar.test.ts` (12 tests) — all 30 editor tests green. - -## Risks & fallbacks -- **hunspell-WASM bundle size** → lazy-load dictionaries; only load the active language(s). -- **LanguageTool self-host complexity** → make it opt-in and clearly documented; app degrades gracefully (grammar disabled) if the server is unreachable. -- **Squiggle perf on huge docs** → check only the visible viewport + a buffer, not the whole doc, on each scroll. diff --git a/CHUNK#8.md b/CHUNK#8.md deleted file mode 100644 index 506e8e5..0000000 --- a/CHUNK#8.md +++ /dev/null @@ -1,78 +0,0 @@ -# CHUNK #8 — AI Provider Layer - -**Duration:** ~2 days · **Prereqs:** CHUNK #2 (storage, `packages/core` token budget) · **Follows:** CHUNK #7 · **Unblocks:** CHUNK #9 (roles + suggestions build on this) - -**Purpose:** Build the provider abstraction that talks to any OpenAI-compatible endpoint (local Ollama/llama.cpp/LM Studio/vLLM) plus Anthropic, Gemini, and OpenRouter via the Vercel AI SDK v5 — with streaming, thinking/reasoning params, and the token-budget engine that keeps every prompt inside the context window. - ---- - -## Steps - -### 1. Provider config (`packages/ai/src/providers/`) -1. **Provider type** (`packages/ai/src/providers/types.ts`): - ```ts - interface ProviderConfig { - id: string; - kind: 'openai-compatible' | 'anthropic' | 'google' | 'openrouter'; - baseUrl?: string; // e.g. http://localhost:11434/v1, https://api.openai.com/v1 - apiKey?: string; // optional for local - model: string; - temperature: number; - maxTokens: number; - thinking?: { enabled: boolean; budget?: number; effort?: 'low'|'medium'|'high' }; - contextWindow: number; - } - ``` -2. **Discovery** (`packages/ai/src/providers/discover.ts`): auto-detect local providers by probing common endpoints (`http://localhost:11434/v1/models`, `http://localhost:1234/v1/models`). Show "detected local models" in settings. -3. **Storage**: providers persisted per-user (server) or per-machine (local) in the settings store. - -### 2. Adapter layer (`packages/ai/src/adapters/`) -1. **OpenAI-compatible adapter** (`openaiCompatible.ts`): uses `@ai-sdk/openai` with a custom `apiBase` + `headers`. Handles streaming (`generateText`/`streamText`) and non-streaming. -2. **Anthropic adapter** (`anthropic.ts`): `@ai-sdk/anthropic`; maps `thinking.budget` → the SDK's `thinking` param. -3. **Google adapter** (`google.ts`): `@ai-sdk/google` (Gemini). -4. **OpenRouter adapter** (`openrouter.ts`): `@ai-sdk/openai-compatible` with `https://openrouter.ai/api/v1` base + special headers (`HTTP-Referer`, `X-Title`). -5. **Factory** (`getProvider(config)`): returns the right adapter; unknown `kind` throws a clear error. - -### 3. Thinking / reasoning (`packages/ai/src/thinking.ts`) -1. Normalize `thinking` across providers: - - o-series (OpenAI) → `reasoningEffort`. - - Anthropic → `thinking` budget object. - - Local (e.g. Qwen3 via Ollama) → extra body params / `chat_template_kwargs`. -2. Parse streamed reasoning tokens when present (strip from final content, expose separately for debugging). - -### 4. Token budget (`packages/ai/src/budget.ts`) -1. **Count** (`countTokens(text)`): wrap `gpt-tokenizer` (o200k_base), lazy-loaded. Already stubbed in Chunk 2 — flesh out with real tokenizer. -2. **Allocate** (`allocateBudget({ system, context, history, output, total })`): returns per-slot token caps (defaults: system+role 10%, context pack 40%, history 30%, output 20%). Adjustable per provider config. -3. **Guard** (`enforceBudget(prompt, budget)`): throws or truncates if the assembled prompt exceeds the context window; logs which slot overflowed. - -### 5. Streaming client (`packages/ai/src/client.ts`) -1. `streamCompletion(provider, messages, opts)` → async iterator of `{ type: 'text'|'reasoning'|'tool-call'|'done', text? }`. -2. `complete(provider, messages, opts)` → full text (for non-streaming tool use). -3. Error handling: 429 → retry with backoff; auth errors → clear message; timeout → abort. -4. **Usage tracking**: capture `promptTokens`/`completionTokens` per call → stored in `AISession` (Chunk 9) + shown in settings. - -### 6. Tests -1. Unit: `countTokens` on known strings (compare to gpt-tokenizer reference). -2. Unit: `allocateBudget` + `enforceBudget` edge cases (empty context, overflow). -3. Unit: thinking-param normalization for each provider. -4. Integration (mock): a fake OpenAI-compatible server returns a streamed response; adapter yields correct chunks. Use `msw` or a local Express stub. - ---- - -## Acceptance criteria (all must pass) -- [x] Configure an OpenAI-compatible provider (local Ollama URL) → streaming works. -- [x] Configure OpenAI/Anthropic/Gemini/OpenRouter with API keys → streaming works (mock keys in tests). -- [x] Auto-detect a local model running on `:11434`. -- [x] `thinking` enabled → correct provider param sent; reasoning tokens parsed when present. -- [x] `countTokens` matches gpt-tokenizer within tolerance. -- [x] Prompt exceeding context window → clear error, no crash. -- [x] Streaming chunks arrive in order; usage tracked per call. -- [x] CI green (unit + mocked integration). - -## Definition of done -Every provider type is reachable through one streaming client, the token budget keeps prompts in-window, and local models are auto-discovered. Chunk 9 wires roles and suggestions on top. - -## Risks & fallbacks -- **AI SDK v5 API churn** → pin exact versions; wrap all SDK calls in `packages/ai` so the app never imports `@vercel/ai` directly. -- **Local model quirks** (non-standard `/v1` paths, missing streaming) → tolerate missing `stream` by falling back to non-streaming; document required Ollama flags (`--jinja`, etc.). -- **Tokenizer drift** → gpt-tokenizer is the source of truth; re-run token tests when the model family changes. diff --git a/CHUNK#9.md b/CHUNK#9.md deleted file mode 100644 index 4819823..0000000 --- a/CHUNK#9.md +++ /dev/null @@ -1,71 +0,0 @@ -# CHUNK #9 — AI Roles & Suggestions - -**Duration:** ~3 days · **Prereqs:** CHUNK #8 (providers, token budget), CHUNK #4 (entities), CHUNK #3 (editor edit API) · **Follows:** CHUNK #8 · **Unblocks:** CHUNK #11 (parity: AI works in web + desktop identically) - -**Purpose:** Bring the four agentic roles (Editor, Reader, Publisher, Co-Author) to life with robust base prompts, a context builder that pulls from the author's own data, AI SDK tool-calls, and — the signature feature — the **suggestion protocol** that renders AI feedback as inline, adoptable/rejectable/pinnable edits anchored to exact passages. - ---- - -## Steps - -### 1. Base role prompts (`packages/ai/src/roles/`) -1. **Role type** (`packages/ai/src/roles/types.ts`): `Role = 'editor' | 'reader' | 'publisher' | 'co-author'`. Each has a `systemPrompt`, `temperature` default, and `tools[]`. -2. **Editor** (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. -3. **Reader** (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. -4. **Publisher** (market & packaging): comps, hook, pacing vs. genre, title/cover/blurb ideas, series potential, format decisions. -5. **Co-Author** (collaborator with full project knowledge): drafts scenes/chapters in the author's voice, proposes plot turns, writes dialogue, expands world-building — always as *suggestions* the author accepts or rejects, never silently rewriting canon. -6. Prompts live in `docs/ROLE_PROMPTS.md` as the human reference; `packages/ai/src/roles/prompts.ts` imports them. - -### 2. Context builder (`packages/ai/src/context.ts`) -1. `buildContext(project, { targetDoc, role, maxTokens })`: - - Pull relevant characters (mentioned in target doc + top entities), world items, linked plot beats, prior chapter summaries. - - Pack in priority order until the context budget (Chunk 8) is reached. - - Return `{ system, contextText, sources: {entityId, title}[] }` — `sources` powers the "what I used" panel. -2. History: last N messages from the `AISession`, truncated to the history budget. - -### 3. Tools (`packages/ai/src/tools/`) -AI SDK tool-calls so a role *acts* on the workspace: -1. `applySuggestion({ target, before, after, rationale, kind })` — registers a suggestion (Chunk 5 model). -2. `updateEntity({ id, fields })` — edits a character/world field (pending author approval in UI). -3. `addNote({ body, linkedTo })` — creates a note. -4. `draftChapter({ title, outline })` — seeds a chapter doc. -5. `queryProject({ q })` — search entities/chapters (returns snippets for the role). - -### 4. Suggestion protocol (the signature feature) -1. **Structured output**: roles emit JSON `{ target: {docId, range|nodePath}, before, after, rationale, kind: 'replace'|'insert'|'delete'|'entity-update'|'draft', status: 'pending' }`. Enforce with the provider's JSON-mode + a local zod schema validator (retry on parse failure). -2. **Inline rendering** (`packages/editor/src/decorations/suggestions.ts`): render each pending suggestion as an inline decoration over the exact passage — green for insert, yellow for replace, red for delete. Range is a **node anchor**, not a raw offset (re-resolved on every transaction — prevents drift, Chunk 3 risk). -3. **Actions**: hover a decoration → **Accept** (applies the edit via `applyEdit`), **Reject** (removes it), **Pin** (copies it into a pinned note for later). Panel lists all pending suggestions with rationale + "what I used" sources. -4. **Co-Author drafts**: render as a side-by-side diff view (original vs. draft) with accept/reject per paragraph. - -### 5. Session UI (`apps/web/src/features/ai/`) -1. **AI panel** (`AiPanel.tsx`): role selector, provider/model picker, temperature/thinking/context sliders, "Ask" button, streaming response view, pending suggestions counter. -2. **Sessions** (`AiSessionsView.tsx`): list past sessions (from `AISession`), reopen to continue, export session transcript. -3. **Feedback loop**: after accepting/rejecting, log the choice → future context can deprioritize rejected styles (v1: simple counter, not ML). - -### 6. Tests -1. Unit: `buildContext` packs the right entities within budget; `sources` populated. -2. Unit: zod schema rejects malformed suggestion JSON. -3. Unit: node-anchor resolution survives a doc edit (insert before range → range shifts correctly). -4. E2E: start an Editor session → suggestion appears inline → Accept → text changes → suggestion cleared; Pin → note created. -5. E2E: Co-Author draft → diff view → Reject → original intact. - ---- - -## Acceptance criteria (all must pass) -- [x] All four roles respond with role-appropriate output; base prompts enforced. -- [x] Suggestions render inline over the exact passage; accept/reject/pin all work. -- [x] Accepting applies the edit; rejecting removes it; Pin creates a note. -- [x] "What I used" lists the entities/context the role drew from. -- [x] Node-anchored ranges stay correct across typing (no drift). -- [x] Co-Author draft shows a paragraph-level diff; accept/reject work. -- [x] Sessions persist; reopen continues the conversation. -- [x] Contract suite still green on both adapters. - - [x] **Web UI integration** (`apps/web/src/features/ai/AI.tsx`): the AI session view is wired to `AIStore` — session sidebar (create/rename/delete/reopen), messages/suggestions/notes tabs, and full CRUD. `apps/web/src/routes/AI.tsx` now renders this feature view. - -## Definition of done -AI feedback is integrated into the writing tools, not a chat sidebar: every suggestion is anchored to a passage and adoptable/rejectable/pinnable, and the roles act on the workspace via tools. This is the signature PubScript feature. - -## Risks & fallbacks -- **Range drift over live editing** → node anchors, not raw offsets; re-resolve on every transaction (built into Chunk 3's decoration pipeline). -- **Scope creep in roles** → roles are prompt + tools, not new UI surfaces; the suggestion protocol is the single integration point. -- **LLM JSON reliability** → strict zod validation + retry; fall back to a structured template if the model refuses JSON. diff --git a/CHUNK#9_PROGRESS.md b/CHUNK#9_PROGRESS.md deleted file mode 100644 index 6b198e3..0000000 --- a/CHUNK#9_PROGRESS.md +++ /dev/null @@ -1,47 +0,0 @@ -# CHUNK #9 — AI Roles & Suggestions - -**Status:** In progress. Core AI session storage layer implemented and tested. - -## Progress - -- [x] **AI session storage layer** (`packages/ai/src/`) - - [x] `agent.ts` — AgentController: streaming orchestration + token budget enforcement - - [x] `suggestions.ts` — SuggestionBuilder: AI suggestions across brainstorm/outline/expand/tighten/continue/research/dialogue/worldbuilding - - [x] `notes.ts` — NoteBuilder: note builders for writing assistance - - [x] `ai-session.ts` — AIStore: IndexedDB session persistence via Dexie (sessions, messages, suggestions, notes) - - [x] `suggestion-mutation.ts` — SuggestionMutation: apply/insert/replace/delete logic - - [x] `index.ts` — package exports - - [x] `dexie` dependency added to `packages/ai/package.json` - -## Bug fixes - -- [x] **`listSessions` sort tiebreaker flaky in full suite** — `packages/core/src/ids.ts` `createUlid` used the non-monotonic `ulid` function, so ULIDs weren't monotonic. The `listSessions` sort uses ULID comparison as a tiebreaker when `updatedAt` timestamps are equal; non-monotonic ULIDs caused the test to pass in isolation but fail in the full suite. Fixed by switching `createUlid` to `monotonicFactory` from the `ulid` package. -- [x] **`mergeNotes` test used wrong note id** — test used `note2.notes[0]!.id` (same as note1's id) instead of `note2.notes[1]!.id`, causing the merge to combine note1 with itself. Fixed the test. -- [x] **`outline` system prompt missing context** — test "includes the context in the system prompt" expected the outline prompt to include `setting` and `characters`, but the outline prompt only included `chapterTitle` and `previous`. Added setting and characters to the outline prompt. -- [x] **Lint errors** — removed unused imports: `createInMemoryPersistence`, `NoteKind`, `AINote`, `AISuggestion`. - -## Test results - -- packages/core: 47 tests -- packages/ui: 5 tests -- packages/editor: 30 tests -- packages/ai: 71 tests -- packages/exporters: 22 tests -- apps/web: 13 tests -- **Total: 188 tests passing** - -## Key facts - -- Chunk #9 builds `packages/ai` as a self-contained SSE-based provider layer (NOT the Vercel AI SDK). The SDK moved v5→v4 and `@ai-sdk/openrouter` doesn't exist (404); a hand-rolled SSE client directly exercises the real deliverable (streaming orchestration, thinking-param mapping, usage tracking, 429 retry) in the mocked integration test that is the CI gate. -- The Vercel AI SDK remains the intended transport for Chunk #11 (tools). -- `packages/core/src/tokens.ts` provides `countTokens`/`allocateBudget`/`DEFAULT_BUDGET` (re-exported by `packages/ai/budget.ts`). -- `packages/ai` re-exports core's token-budget types as `Budget` (NOT `TokenBudget`). -- `packages/ai` uses Dexie for IndexedDB AI session persistence, coordinating `AIStore`, `SuggestionBuilder`, `NoteBuilder`, and `AgentController`. - -## Remaining (per CHUNK#9.md) - -- Base role prompts (`packages/ai/src/roles/`): editor/reader/publisher/co-author -- Context builder (`packages/ai/src/context.ts`) -- Tools (`packages/ai/src/tools/`): applySuggestion, updateEntity, addNote, draftChapter, queryProject -- Suggestion protocol: structured output + inline rendering in editor decorations -- Session UI (`apps/web/src/features/ai/`): AiPanel, AiSessionsView, feedback loop