Files
PubScript/docs/CONVENTIONS.md
T
Schwitzz 1c9b531996 Implement CHUNK #2: server layer, core schemas/adapters, contract tests, seed
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.
2026-10-24 18:31:20 -05:00

2.8 KiB

PubScript Conventions

Guidelines every chunk follows so a human and a strictly-instructed coding agent stay in lockstep.

Language & tooling

  • TypeScript strict everywhere, with noUncheckedIndexedAccess and exactOptionalPropertyTypes on. No any in new code.
  • pnpm workspaces with a committed lockfile. Never run pnpm install without --frozen-lockfile in CI.
  • ESM throughout ("type": "module"). Import sibling modules with a .js extension even though the source is .ts (bundler module resolution).
  • Prettier (100-col, double quotes) + ESLint flat config (typescript-eslint recommended, no-explicit-any off, 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.ts and 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/core contract script) runs the same assertions against every StorageAdapter implementation.
  • Acceptance criteria in each CHUNK#n.md are 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-sqlite3 native build in Docker → node:22-alpine with python3 make g++ in the build stage; fall back to node:22-slim if needed (record in an ADR).