diff --git a/CLEANUP_PLAN.md b/CLEANUP_PLAN.md new file mode 100644 index 0000000..7fd1e1b --- /dev/null +++ b/CLEANUP_PLAN.md @@ -0,0 +1,516 @@ +# Postwork Cleanup Plan — Agent Handoff + +**Generated:** 2026-07-03. The audit ran against commit `2d60cc3`; the hardening changes described in Part 0 are committed as `16d70ab`. +**How this was produced:** one interactive hardening pass over the Convex backend, then a multi-agent audit (5 parallel auditors — design tokens, components, state architecture, routes/accessibility, docs/repo hygiene — with an adversarial verifier re-checking every falsifiable claim against the actual files). **38 findings survived verification, 0 were refuted.** Every quoted "Audit finding" block below is confirmed against the code unless marked *partial*. + +## Ground rules for the executing agent + +1. **Read [CLAUDE.md](CLAUDE.md) first**, and read `convex/_generated/ai/guidelines.md` before touching anything under `convex/` — it overrides trained Convex knowledge. +2. **Setup:** `bun install`, then `bun run typecheck` (`tsc -b --noEmit`) must pass before and after every step. There is no test suite; typecheck + the per-step **Verify** commands are the safety net. `bun run dev` needs a configured Convex deployment (`npx convex dev`) — don't assume you can run the app. +3. **Work one step at a time, one commit per step.** Steps within a phase are independent unless a dependency is called out. Phases are ordered by leverage: do Phase 1 first, and do Phase 2 (primitives) **before** Phase 3 (token sweep) — the primitives absorb dozens of call sites the sweep would otherwise have to touch twice. +4. **Line numbers are a snapshot.** They were verified on 2026-07-03 but will drift as you edit. Re-locate with grep before editing; never blind-edit by line number. +5. **Do not deploy, push, or publish anything** unless the repo owner asks. Local commits only. +6. **Design authority:** [DESIGN.md](DESIGN.md) is the spec ("The Kept Record": calm, durable, single wine accent, sentence case, one primary action per view). Where DESIGN.md contradicts the shipped code, Phase 7 says which side wins — for everything else, the spec wins. +7. Tick the checkboxes in this file as you complete steps, and delete this file when everything is done. + +## Progress checklist + +- [x] 0.1 Commit the hardening changes (done — `16d70ab`) +- [ ] 1.1 Add radius tokens to the theme +- [ ] 1.2 Replace `text-red-300` with the urgent token +- [ ] 1.3 Global `:focus-visible` + select focus styles +- [ ] 1.4 Resolve tracked-uppercase labels +- [ ] 1.5 Fix "home" nav active state +- [ ] 1.6 Make the "+ new post" CTA actually start a post +- [ ] 2.1 Extract `Button` component +- [ ] 2.2 Extract `Chip` primitive (+ `PostMetaChips`) +- [ ] 2.3 Extract `Dialog` shell + `PostForm` +- [ ] 2.4 Unify popover dismiss behavior (+ UserSwitcher keyboard/direction) +- [ ] 2.5 Dedupe `OrgSquare` +- [ ] 2.6 Dedupe the wine-framed agent-panel scaffold +- [ ] 3.1 Color-token sweep: kill the 272 `[var(--color-*)]` arbitrary classes +- [ ] 3.2 Tokenize the type ramp +- [ ] 3.3 One shared avatar/identity palette +- [ ] 4.1 Shared page-header/back-link scaffold +- [ ] 4.2 Consistent loading/empty states +- [ ] 4.3 Per-route document titles +- [ ] 5.1 Restructure the overlay store (memoized context, module-level hooks) +- [ ] 5.2 Single `Priority` type, typed search params +- [ ] 5.3 Honest local-id handling +- [ ] 5.4 Flatten/document the provider pyramid +- [ ] 5.5 Fix graduated flash experiments (incl. double-composer bug) +- [ ] 5.6 Reuse thread components in `ExperimentDiscussion` +- [ ] 5.7 Derive `ExperimentSlot` from `ExperimentSlots` +- [ ] 6.1 Decide: dead Group-B backend (convex/spaces.ts + 3 tables) +- [ ] 6.2 Decide: dead `agentTasks` table +- [ ] 6.3 Backend hardening follow-ups (subject leak, vote counting, codegen) +- [ ] 7.1 Reconcile DESIGN.md with the shipped shell +- [ ] 7.2 Purge the retired monospace aesthetic from AGENTS.md / PRODUCT.md +- [ ] 7.3 Refresh README +- [ ] 7.4 Regenerate OG images +- [ ] 7.5 Archive stale planning docs +- [ ] 7.6 Pin dependencies + +--- + +## Part 0 — Already done in the previous session (do NOT redo) + +These backend-hardening changes are already committed as `16d70ab` ("Harden demo backend: …"). **Step 0.1 is done** — just read that commit (`git show 16d70ab`) so you know the current backend shape before starting. + +What was changed and why: + +| File | Change | +|---|---| +| `convex/seed.ts` | `seed:run` was a **public** mutation that wipes every table — anyone with the deployment URL could erase the demo DB. Now `internalMutation` (CLI `convex run seed:run` still works). | +| `convex/users.ts` | `setProfileBySubject` accepted `subject` and `role` from the client — unauthenticated profile takeover + self-promotion to admin. Replaced with `updateProfile`: identity derived from `ctx.auth.getUserIdentity()`, `role` not editable. `users.list` now strips the `subject` field so visitors can't enumerate real members' auth identifiers. | +| `convex/reads.ts` (deleted), `posts.create`, `posts.counts` (removed) | Dead, unauthenticated public write/scan endpoints. The client's session overlay (`src/lib/store.tsx`) replaced them long ago; nothing in `src/` called them. `posts.ts` now carries a comment documenting the intentional absence of public write paths. | +| `convex/posts.ts` `feed` | Was `.collect()` (unbounded) with a per-post, per-viewer `postReads` lookup. Now `.take(200)` on both branches. | +| `convex/schema.ts` + `convex/spaces.ts` | `feedForSpace` filtered org-visibility posts on a field (`orgId`) that didn't exist in the schema — every `"org"`-scoped post was invisible to everyone. Added `posts.orgId: v.optional(v.id("orgs"))` and made the filter honest. | +| `src/lib/store.tsx` | Session-added participants are now deduped against backend participants (was: duplicate avatars + duplicate React keys after replying to a thread you were already in). | +| `convex/_generated/api.d.ts` | Hand-edited to drop the deleted `reads` module (codegen needs a configured deployment). Regenerate properly at the first opportunity — see step 6.3. | + +--- + +## Phase 1 — Foundation quick wins (~1 hour total, all user-visible) + +### Step 1.1 — Add radius tokens to the theme + +The design system's smallest radius renders at **double the spec** because the theme never defines radius tokens. + +> **Audit finding (confirmed · impact: high · effort: small) — rounded-sm renders 4px, not the spec's 2px — radius tokens never defined in the theme** +> +> DESIGN.md specifies rounded.sm = 2px (frontmatter line 50; prose: 'tags 2px' line 145, agent badge 'rounded-sm (2px). The smallest radius in the system' line 307-308). But the @theme block in src/index.css:3-28 defines no --radius-* tokens, so Tailwind v4's defaults apply and `rounded-sm` = 0.25rem = 4px. Every 'smallest radius' element therefore renders at double the spec: src/components/AgentTag.tsx:8, src/components/UserRoleTag.tsx:16, src/components/AgentSummary.tsx:45 (ai chip), src/components/AgentTasksPanel.tsx:67, src/components/SendAgentButton.tsx:48, src/components/RichText.tsx:10 (inline code), src/routes/AgentsPage.tsx:108, src/routes/SpacePage.tsx:182. Fix: add `--radius-sm: 2px;` to the @theme block in src/index.css (optionally also pin --radius-md: 6px and --radius-lg: 8px, which currently only coincidentally match the v4 defaults of 0.375rem/0.5rem). Alternative: swap those eight usages to `rounded-xs` (0.125rem = 2px), but tokenizing keeps the DESIGN.md name-to-utility mapping intact. +> +> _Files:_ `src/index.css`, `src/components/AgentTag.tsx`, `src/components/UserRoleTag.tsx`, `src/components/AgentSummary.tsx`, `src/components/AgentTasksPanel.tsx`, `src/components/SendAgentButton.tsx`, `src/components/RichText.tsx`, `src/routes/AgentsPage.tsx`, `src/routes/SpacePage.tsx` + +**Do:** Add to the `@theme` block in `src/index.css`: `--radius-sm: 2px; --radius-md: 6px; --radius-lg: 8px;` (md/lg currently only coincidentally match Tailwind v4 defaults — pin them). +**Verify:** `bun run typecheck`; then confirm no component needed changing: the existing `rounded-sm` classes now resolve to 2px via the token. + +### Step 1.2 — Replace `text-red-300` with the urgent token + +> **Audit finding (confirmed · impact: medium · effort: small) — Urgent queue count uses Tailwind default text-red-300 instead of the --color-urgent token** +> +> src/components/AppShell.tsx:91 renders the sidebar urgent count with `text-red-300` (Tailwind's default palette red), while every other urgent-state surface uses the design token #ff6b6b: the Urgent priority chip/dot in src/lib/format.ts:29-30, the failed StatusChip in src/components/StatusChip.tsx:10, and the task error note in src/components/AgentTasksPanel.tsx:135. DESIGN.md scopes red-300 to inline error notes only (line 333) and names Urgent Coral #ff6b6b as the urgent state color (line 182). A count of urgent posts is priority state, not an error. Fix: change AppShell.tsx:91 to text-urgent (or text-[var(--color-urgent)] under the current idiom). (src/components/AgentSummary.tsx:71's red-300-on-red-500/10 error note is correct per spec and should stay.) +> +> _Files:_ `src/components/AppShell.tsx`, `src/lib/format.ts` + +**Do:** In `src/components/AppShell.tsx`, change `text-red-300` → `text-urgent` (the `--color-urgent` token generates this utility). +**Verify:** `grep -rn "red-300" src/` returns nothing. + +### Step 1.3 — Global `:focus-visible` + select focus styles + +Two confirmed findings, one fix: + +> **Audit finding (confirmed · impact: high · effort: small) — No :focus-visible treatment anywhere; selects remove outline with no replacement** +> +> grep finds zero `focus-visible` usages in src/, and src/index.css defines no focus rule. Text inputs at least pair `outline-none` with `focus:border-accent/50` (per DESIGN.md:314), but the three `s (QuickPostBar, NewPostDialog, SpacePage). +**Verify:** `grep -rn "outline-none" src/ | grep -v focus` — every remaining hit must sit next to an explicit focus treatment or the global rule must cover it. + +### Step 1.4 — Resolve tracked-uppercase labels + +> **Audit finding (confirmed · impact: medium · effort: small) — Tracked-uppercase labels beyond the one sanctioned agent badge** +> +> DESIGN.md says labels use sentence case, never tracked all-caps (lines 223-224), names the agent badge 'the one sanctioned uppercase' (line 308), and its Don't list bans 'tracked all-caps eyebrow kickers' (line 370-371). Violations in app chrome: src/components/AppShell.tsx:80 ('your queue' header: text-[11px] tracking-wide uppercase), src/components/UserSwitcher.tsx:41 (section header: tracking-wide uppercase), src/routes/FlashExperimentsPage.tsx:108 (uppercase tracking-[0.2em] accent eyebrow — the exact banned pattern, on a real route not inside src/flashExperiments/) and :214 (uppercase tracking-wider meta row), and src/components/UserRoleTag.tsx:16 (role tag rendered uppercase+tracked, though DESIGN.md line 224 specifies role badges as sentence case: 'Owner'). Fix: drop `uppercase tracking-*` from these five sites and use sentence-case label styling (text-[11px] font-medium text-muted); keep uppercase only in src/components/AgentTag.tsx. +> +> _Files:_ `src/components/AppShell.tsx`, `src/components/UserSwitcher.tsx`, `src/components/UserRoleTag.tsx`, `src/routes/FlashExperimentsPage.tsx` + +**Do:** Either lowercase the "your queue" / UserSwitcher / FlashExperimentsPage eyebrow labels (spec-compliant), **or** add a sanctioned "rail label" style to DESIGN.md §5 and keep them. Pick one and apply it everywhere — don't leave the split. If you keep uppercase anywhere, coordinate with step 7.1. +**Verify:** `grep -rn "uppercase" src/` — every hit is either the agent badge or the newly sanctioned style. + +### Step 1.5 — Fix "home" nav active state + +> **Audit finding (confirmed · impact: high · effort: small) — Sidebar 'home' nav link shows as active on every route** +> +> In /Users/m3/Developer/postwork/src/components/AppShell.tsx:21-23 and :34-42, the 'home' link's active state is computed solely from `!feedPriority` (location.search.priority), with no pathname check. On /spaces, /agents, /orgs, /posts/$postId, etc., search.priority is undefined, so 'home' receives the ACTIVE class simultaneously with the router-driven `[&.active]` highlight on the actual current page (AppShell.tsx:56) — two nav items lit at once everywhere except the priority feed. Fix: also select `s.location.pathname` in useRouterState and apply ACTIVE only when pathname === '/' (home: pathname==='/' && !priority; priority: pathname==='/' && priority==='urgent'), or use TanStack Link's activeProps with `activeOptions={{ exact: true, includeSearch: true }}` so all six nav items share one active mechanism instead of the current split (manual class for home/priority, `[&.active]` selector for ROUTE_NAV). +> +> _Files:_ `src/components/AppShell.tsx` + +**Do:** Prefer the single-mechanism fix: use TanStack `Link` `activeProps`/`activeOptions={{ exact: true, includeSearch: true }}` for all six nav links, deleting the manual `ACTIVE` constant and the `useRouterState` priority selection. +**Verify:** typecheck; read the rendered logic — exactly one nav item can be active for `/`, `/?priority=urgent`, and each ROUTE_NAV path. + +### Step 1.6 — Make the "+ new post" CTA actually start a post + +> **Audit finding (confirmed · impact: high · effort: small) — '+ new post' primary CTA is a dead link — it only navigates to '/'** +> +> The shell's single primary action (AppShell.tsx:63-68) is a `+ new post` with no onClick and no composer trigger. Clicking it from any page just navigates to the feed; the user must then separately discover the QuickPostBar peek at the bottom. Meanwhile NewPostDialog (src/components/NewPostDialog.tsx) is a fully built new-post modal that the main app never mounts — it is only imported by two flash experiments (src/flashExperiments/experiments/wide-review-shell.tsx:4, centered-rail.tsx:4). Fix: either (a) have the CTA open NewPostDialog via local state in AppShell, or (b) keep the navigation but pass intent (e.g. search param `compose: true` or shared context) that FeedPage uses to call QuickPostBar's reveal() and focus the textarea. Per DESIGN.md 'one primary action per view', the wine-filled button must actually start a post. +> +> _Files:_ `src/components/AppShell.tsx`, `src/components/NewPostDialog.tsx`, `src/components/QuickPostBar.tsx` + +**Do:** Option (a) is cleaner given `NewPostDialog` already exists and is unused by the main app: hold `open` state in `AppShell`, render `NewPostDialog` there, and make the CTA a `