# CHUNK #1 — Scaffold & Foundations **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.