1c9b531996
Complete the server-backed storage layer for PubScript so the app can run against a real Fastify + SQLite backend instead of only local IndexedDB. Server (@pubscript/server): - db.ts: better-sqlite3 schema + migrations (users, shares, projects, books, chapters, entities, boards, ai_sessions, audit); WAL mode; idempotent admin bootstrap via PUBSCRIPT_ADMIN_EMAIL/PASSWORD. - auth.ts: argon2 hashing, JWT-ish session tokens, guards, auth routes. - routes.ts: REST CRUD for projects/books/chapters/entities/boards/ai_sessions with multi-tenant isolation and optimistic concurrency (baseRev). - ws.ts: Fastify websocket hub for autosave + presence events. - mapping.ts: row <-> core type conversion. Core (@pubscript/core): - Zod domain schemas (projects, books, chapters, entities, boards, ai_sessions) exported from index. - storage-local.ts (IndexedDB/Dexie) and storage-server.ts (REST) adapters implementing the StorageAdapter contract. Tests & tooling: - storage.contract.test.ts: parameterized over both adapters; wired as `pnpm contract` and added to CI. - seed.ts (`pnpm --filter server seed`): creates demo project "The Glass Harbor" — 3 chapters of prose, 4 linked characters, 1 world location, 6-node board. - Dockerfile + compose wiring for the server runtime. Acceptance: seed project opens with 3 chapters, 4 linked characters, 1 board.
2.8 KiB
2.8 KiB
PubScript Conventions
Guidelines every chunk follows so a human and a strictly-instructed coding agent stay in lockstep.
Language & tooling
- TypeScript
stricteverywhere, withnoUncheckedIndexedAccessandexactOptionalPropertyTypeson. Noanyin new code. - pnpm workspaces with a committed lockfile. Never run
pnpm installwithout--frozen-lockfilein CI. - ESM throughout (
"type": "module"). Import sibling modules with a.jsextension even though the source is.ts(bundler module resolution). - Prettier (100-col, double quotes) + ESLint flat config (
typescript-eslintrecommended,no-explicit-anyoff, unused-vars on).
File layout
PubScript/
├── PLAN.md ← single source of truth for *what* and *why*
├── CHUNK#n.md ← executable work orders (this chunk's steps)
├── apps/web/ ← React 19 + Vite app (runs in browser AND Tauri)
├── packages/
│ ├── core/ ← domain types, IDs, time, token budget, StorageAdapter contract
│ ├── ui/ ← design tokens, theme engine, component kit
│ ├── editor/ boards/ exporters/ ai/ ← later chunks
├── server/ ← Fastify + SQLite: auth, multi-tenant, WS, admin, AI proxy
├── desktop/ ← Tauri 2 shell (later chunk)
├── docker/ ← Dockerfile + compose
└── docs/ ← ADRs, data model, role prompts, parity matrix
The golden rule: code against packages/core, never against a backend
Every feature codes against the StorageAdapter contract in packages/core/src/storage.ts, never against ServerAdapter or LocalAdapter directly. This is what makes web, desktop, and local modes interchangeable and lets a single contract-test suite enforce parity.
Domain conventions
- IDs are ULIDs (sortable, collision-free). See
packages/core/src/ids.ts. - Timestamps are UTC ISO-8601 strings. See
packages/core/src/time.ts. - All domain types live in
packages/core/src/types.tsand are shared by app and server. Do not redefine them locally.
Testing
- Vitest for unit tests colocated as
src/**/*.test.ts(x). - Contract suite (
packages/corecontractscript) runs the same assertions against everyStorageAdapterimplementation. - Acceptance criteria in each
CHUNK#n.mdare the gate to the next chunk.
Commits
Conventional Commits (feat:, fix:, chore:, docs:, refactor:, test:, build:). One logical change per commit; reference the chunk in the body when relevant.
Risks & fallbacks
- Tailwind v4 + React 19 friction → pin exact versions and record them here.
better-sqlite3native build in Docker →node:22-alpinewithpython3 make g++in the build stage; fall back tonode:22-slimif needed (record in an ADR).