From 4bd70cd4e5353dcb0d829eb2488972682a56b995 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sat, 14 Mar 2026 22:18:09 +0100 Subject: [PATCH] docs(01): research phase domain --- .../01-RESEARCH.md | 651 ++++++++++++++++++ 1 file changed, 651 insertions(+) create mode 100644 .planning/phases/01-foundation-and-collection/01-RESEARCH.md diff --git a/.planning/phases/01-foundation-and-collection/01-RESEARCH.md b/.planning/phases/01-foundation-and-collection/01-RESEARCH.md new file mode 100644 index 0000000..1526d05 --- /dev/null +++ b/.planning/phases/01-foundation-and-collection/01-RESEARCH.md @@ -0,0 +1,651 @@ +# Phase 1: Foundation and Collection - Research + +**Researched:** 2026-03-14 +**Domain:** Full-stack web app scaffolding, SQLite CRUD, React SPA with collection management +**Confidence:** HIGH + +## Summary + +Phase 1 is a greenfield build establishing the entire project stack: Bun runtime with Hono API server, React 19 SPA via Vite with TanStack Router, Drizzle ORM over bun:sqlite, and Tailwind v4 styling. The phase delivers complete gear collection CRUD (items and categories) with aggregate weight/cost totals, a slide-out panel for add/edit, a card grid grouped by category, and a first-run onboarding wizard. + +The critical architectural decision is using **Vite as the frontend dev server** (required by TanStack Router's file-based routing plugin) with **Hono on Bun as the backend**, connected via Vite's dev proxy. This is NOT Bun's native fullstack HTML entrypoint pattern -- TanStack Router requires the Vite plugin, which means Vite owns the frontend build pipeline. In production, Hono serves the Vite-built static assets alongside API routes from a single Bun process. + +A key blocker from STATE.md has been resolved: `@hono/zod-validator` now supports Zod 4 (merged May 2025, PR #1173). The project can use Zod 4.x without pinning to 3.x. + +**Primary recommendation:** Scaffold with Vite + TanStack Router for frontend, Hono + Drizzle on Bun for backend, with categories as a first-class table (not just a text field on items) to support emoji icons, rename, and delete-with-reassignment. + + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions +- Card grid layout, grouped by category headers +- Each card shows: item name (prominent), then tag-style chips for weight, price, and category +- Item image displayed on the card for visual identification +- Items grouped under category headers with per-category weight/cost subtotals +- Global sticky totals bar at the top showing total items, weight, and cost +- Empty categories are hidden from the collection view +- Slide-out panel from the right side for both adding and editing items +- Same panel component for add (empty) and edit (pre-filled) +- Collection remains visible behind the panel for context +- Confirmation dialog before deleting items +- Single-level categories only (no subcategories) +- Searchable category picker in the item form -- type to find existing or create new +- Categories editable from the collection overview (rename, delete, change icon) +- Each category gets an emoji/icon for visual distinction +- Deleting a category moves its items to "Uncategorized" default category +- Step-by-step onboarding wizard for first-time users (guides through: create first category, add first item) +- Cards should feel clean and minimal -- "light and airy" aesthetic +- Item info displayed as tag-style chips (compact, scannable) +- Category picker works like a combobox: type to search, select existing, or create new inline +- Photos on cards are important for visual identification even in v1 + +### Claude's Discretion +- Form layout for item add/edit panel (all fields visible vs grouped sections) +- Loading states and skeleton design +- Exact spacing, typography, and Tailwind styling choices +- Error state handling and validation feedback +- Weight unit storage (grams internally, display in user's preferred unit can be deferred to v2) + +### Deferred Ideas (OUT OF SCOPE) +- Subcategories (e.g. "Bags" -> "Handlebar Bag") +- Full photo management is v2 (basic image upload for cards IS in scope) + + + + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|-----------------| +| COLL-01 | User can add gear items with name, weight, price, category, notes, and product link | Drizzle schema for items table, Hono POST endpoint, React slide-out panel with Zod-validated form, image upload to local filesystem | +| COLL-02 | User can edit and delete gear items | Hono PUT/DELETE endpoints, same slide-out panel pre-filled for edit, confirmation dialog for delete, image cleanup on item delete | +| COLL-03 | User can organize items into user-defined categories | Separate categories table with emoji field, combobox category picker, category CRUD endpoints, "Uncategorized" default category, reassignment on category delete | +| COLL-04 | User can see automatic weight and cost totals by category and overall | SQL SUM aggregates via Drizzle, computed on read (never cached), sticky totals bar component, per-category subtotals in group headers | + + + +## Standard Stack + +### Core +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| Bun | 1.3.x | Runtime, package manager | Built-in SQLite, native TS, fast installs | +| React | 19.2.x | UI framework | Locked in CONTEXT.md | +| Vite | 8.x | Frontend dev server + production builds | Required by TanStack Router plugin for file-based routing | +| Hono | 4.12.x | Backend API framework | Web Standards, first-class Bun support, tiny footprint | +| Drizzle ORM | 0.45.x | Database ORM + migrations | Type-safe SQL, native bun:sqlite driver, built-in migration tooling | +| Tailwind CSS | 4.2.x | Styling | CSS-native config, auto content detection, microsecond incremental builds | +| TanStack Router | 1.x | Client-side routing | Type-safe routing with file-based route generation via Vite plugin | +| TanStack Query | 5.x | Server state management | Handles fetching, caching, cache invalidation on mutations | +| Zustand | 5.x | Client state management | UI state: panel open/close, active filters, onboarding step | +| Zod | 4.x | Schema validation | Shared between client forms and Hono API validation. Zod 4 confirmed compatible with @hono/zod-validator (PR #1173, May 2025) | +| TypeScript | 5.x | Type safety | Bun transpiles natively, required by Drizzle and TanStack Router | + +### Supporting +| Library | Version | Purpose | When to Use | +|---------|---------|---------|-------------| +| @tanstack/router-plugin | latest | Vite plugin for file-based routing | Required in vite.config.ts, must be listed BEFORE @vitejs/plugin-react | +| @hono/zod-validator | 0.7.6+ | Request validation middleware | Validate API request bodies/params using Zod schemas | +| drizzle-kit | latest | DB migrations CLI | `bunx drizzle-kit generate` and `bunx drizzle-kit push` for schema changes | +| clsx | 2.x | Conditional class names | Building components with variant styles | +| @vitejs/plugin-react | latest (Vite 8 compatible) | React HMR/JSX | Required in vite.config.ts for Fast Refresh | +| @tailwindcss/vite | latest | Tailwind Vite plugin | Required in vite.config.ts for Tailwind v4 | +| @biomejs/biome | latest | Linter + formatter | Single tool replacing ESLint + Prettier | + +### Alternatives Considered +| Instead of | Could Use | Tradeoff | +|------------|-----------|----------| +| Vite + Hono | Bun fullstack (HTML entrypoints) | Bun fullstack is simpler but incompatible with TanStack Router file-based routing which requires the Vite plugin | +| Zod 4.x | Zod 3.23.x | No need to pin -- @hono/zod-validator supports Zod 4 as of May 2025 | +| Separate categories table | Category as text field on items | Text field cannot store emoji/icon, cannot rename without updating all items, cannot enforce "Uncategorized" default cleanly | + +**Installation:** +```bash +# Initialize +bun init + +# Core frontend +bun add react react-dom @tanstack/react-router @tanstack/react-query zustand zod clsx + +# Core backend +bun add hono @hono/zod-validator drizzle-orm + +# Styling +bun add tailwindcss @tailwindcss/vite + +# Build tooling +bun add -d vite @vitejs/plugin-react @tanstack/router-plugin typescript @types/react @types/react-dom + +# Database tooling +bun add -d drizzle-kit + +# Linting + formatting +bun add -d @biomejs/biome + +# Dev tools +bun add -d @tanstack/react-query-devtools @tanstack/react-router-devtools +``` + +## Architecture Patterns + +### Recommended Project Structure +``` +src/ + client/ # React SPA (Vite entry point) + routes/ # TanStack Router file-based routes + __root.tsx # Root layout with sticky totals bar + index.tsx # Collection page (default route) + components/ # Shared UI components + ItemCard.tsx # Gear item card with chips + CategoryHeader.tsx # Category group header with subtotals + SlideOutPanel.tsx # Right slide-out panel for add/edit + CategoryPicker.tsx # Combobox: search, select, or create category + TotalsBar.tsx # Sticky global totals bar + OnboardingWizard.tsx # First-run step-by-step guide + ConfirmDialog.tsx # Delete confirmation + hooks/ # TanStack Query hooks + useItems.ts # CRUD operations for items + useCategories.ts # CRUD operations for categories + useTotals.ts # Aggregate totals query + stores/ # Zustand stores + uiStore.ts # Panel state, onboarding state + lib/ # Client utilities + api.ts # Fetch wrapper for API calls + formatters.ts # Weight/cost display formatting + server/ # Hono API server + index.ts # Hono app instance, route registration + routes/ # API route handlers + items.ts # /api/items CRUD + categories.ts # /api/categories CRUD + totals.ts # /api/totals aggregates + images.ts # /api/images upload + services/ # Business logic + item.service.ts # Item CRUD logic + category.service.ts # Category management with reassignment + db/ # Database layer + schema.ts # Drizzle table definitions + index.ts # Database connection singleton (WAL mode, foreign keys) + seed.ts # Seed "Uncategorized" default category + migrations/ # Drizzle Kit generated migrations + shared/ # Zod schemas shared between client and server + schemas.ts # Item, category validation schemas + types.ts # Inferred TypeScript types +public/ # Static assets +uploads/ # Gear photos (gitignored) +index.html # Vite SPA entry point +vite.config.ts # Vite + TanStack Router plugin + Tailwind plugin +drizzle.config.ts # Drizzle Kit config +``` + +### Pattern 1: Vite Frontend + Hono Backend (Dev Proxy) +**What:** Vite runs the frontend dev server with HMR. Hono runs on Bun as the API server on a separate port. Vite's `server.proxy` forwards `/api/*` to Hono. In production, Hono serves Vite's built output as static files. +**When to use:** When TanStack Router (or any Vite plugin) is required for the frontend. +**Example:** +```typescript +// vite.config.ts +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; +import { tanstackRouter } from "@tanstack/router-plugin/vite"; + +export default defineConfig({ + plugins: [ + tanstackRouter({ target: "react", autoCodeSplitting: true }), + react(), + tailwindcss(), + ], + server: { + proxy: { + "/api": "http://localhost:3000", + "/uploads": "http://localhost:3000", + }, + }, + build: { + outDir: "dist/client", + }, +}); +``` + +```typescript +// src/server/index.ts +import { Hono } from "hono"; +import { serveStatic } from "hono/bun"; +import { itemRoutes } from "./routes/items"; +import { categoryRoutes } from "./routes/categories"; + +const app = new Hono(); + +// API routes +app.route("/api/items", itemRoutes); +app.route("/api/categories", categoryRoutes); + +// Serve uploaded images +app.use("/uploads/*", serveStatic({ root: "./" })); + +// Serve Vite-built SPA in production +if (process.env.NODE_ENV === "production") { + app.use("/*", serveStatic({ root: "./dist/client" })); + app.get("*", serveStatic({ path: "./dist/client/index.html" })); +} + +export default { port: 3000, fetch: app.fetch }; +``` + +### Pattern 2: Categories as a First-Class Table +**What:** Categories are a separate table with id, name, and emoji fields. Items reference categories via foreign key. An "Uncategorized" category with a known ID (1) is seeded on DB init. +**When to use:** When categories need independent properties (emoji/icon), rename support, and delete-with-reassignment. +**Example:** +```typescript +// db/schema.ts +import { sqliteTable, text, integer, real } from "drizzle-orm/sqlite-core"; + +export const categories = sqliteTable("categories", { + id: integer("id").primaryKey({ autoIncrement: true }), + name: text("name").notNull().unique(), + emoji: text("emoji").notNull().default("📦"), + createdAt: integer("created_at", { mode: "timestamp" }).notNull() + .$defaultFn(() => new Date()), +}); + +export const items = sqliteTable("items", { + id: integer("id").primaryKey({ autoIncrement: true }), + name: text("name").notNull(), + weightGrams: real("weight_grams"), + priceCents: integer("price_cents"), + categoryId: integer("category_id").notNull().references(() => categories.id), + notes: text("notes"), + productUrl: text("product_url"), + imageFilename: text("image_filename"), + createdAt: integer("created_at", { mode: "timestamp" }).notNull() + .$defaultFn(() => new Date()), + updatedAt: integer("updated_at", { mode: "timestamp" }).notNull() + .$defaultFn(() => new Date()), +}); +``` + +### Pattern 3: Slide-Out Panel with Shared Component +**What:** A single `SlideOutPanel` component serves both add and edit flows. When adding, fields are empty. When editing, fields are pre-filled from the existing item. The panel slides in from the right, overlaying (not replacing) the collection view. +**When to use:** Per CONTEXT.md locked decision. +**State management:** +```typescript +// stores/uiStore.ts +import { create } from "zustand"; + +interface UIState { + panelMode: "closed" | "add" | "edit"; + editingItemId: number | null; + openAddPanel: () => void; + openEditPanel: (itemId: number) => void; + closePanel: () => void; +} + +export const useUIStore = create((set) => ({ + panelMode: "closed", + editingItemId: null, + openAddPanel: () => set({ panelMode: "add", editingItemId: null }), + openEditPanel: (itemId) => set({ panelMode: "edit", editingItemId: itemId }), + closePanel: () => set({ panelMode: "closed", editingItemId: null }), +})); +``` + +### Pattern 4: Computed Totals (Never Cached) +**What:** Weight and cost totals are computed on every read via SQL aggregates. Never store totals as columns. +**Why:** Avoids stale data bugs when items are added, edited, or deleted. +**Example:** +```typescript +// server/services/item.service.ts +import { db } from "../../db"; +import { items, categories } from "../../db/schema"; +import { eq, sql } from "drizzle-orm"; + +export function getCategoryTotals() { + return db + .select({ + categoryId: items.categoryId, + categoryName: categories.name, + categoryEmoji: categories.emoji, + totalWeight: sql`COALESCE(SUM(${items.weightGrams}), 0)`, + totalCost: sql`COALESCE(SUM(${items.priceCents}), 0)`, + itemCount: sql`COUNT(*)`, + }) + .from(items) + .innerJoin(categories, eq(items.categoryId, categories.id)) + .groupBy(items.categoryId) + .all(); +} + +export function getGlobalTotals() { + return db + .select({ + totalWeight: sql`COALESCE(SUM(${items.weightGrams}), 0)`, + totalCost: sql`COALESCE(SUM(${items.priceCents}), 0)`, + itemCount: sql`COUNT(*)`, + }) + .from(items) + .get(); +} +``` + +### Anti-Patterns to Avoid +- **Storing money as floats:** Use integer cents (`priceCents`). Format to dollars only in the display layer. `0.1 + 0.2 !== 0.3` in JavaScript. +- **Category as a text field on items:** Cannot store emoji, cannot rename without updating all items, cannot enforce default category on delete. +- **Caching totals in the database:** Always compute from source data. SQLite SUM() over hundreds of items is sub-millisecond. +- **Absolute paths for images:** Store relative paths only (`uploads/{filename}`). Absolute paths break on deployment or directory changes. +- **Requiring all fields to add an item:** Only require `name`. Weight, price, category, etc. should be optional. Users fill in details over time. + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Database migrations | Custom SQL scripts | Drizzle Kit (`drizzle-kit generate/push`) | Migration ordering, conflict detection, rollback support | +| Form validation | Manual if/else checks | Zod schemas shared between client and server | Single source of truth, type inference, consistent error messages | +| API data fetching/caching | useState + useEffect + fetch | TanStack Query hooks | Handles loading/error states, cache invalidation, refetching, deduplication | +| Combobox/autocomplete | Custom input with dropdown | Headless UI pattern (build from primitives with proper ARIA) or a lightweight combobox library | Keyboard navigation, screen reader support, focus management are deceptively hard | +| Slide-out panel animation | CSS transitions from scratch | Tailwind `transition-transform` + `translate-x` utilities | Consistent timing, GPU-accelerated, respects prefers-reduced-motion | +| Image resizing on upload | Custom canvas manipulation | Sharp library or accept-and-store (resize deferred to v2) | Sharp handles EXIF rotation, format conversion, memory management | + +**Key insight:** For Phase 1, defer image resizing/thumbnailing. Accept and store the uploaded image as-is. Thumbnail generation can be added in v2 without schema changes (imageFilename stays the same, just generate a thumb variant). + +## Common Pitfalls + +### Pitfall 1: Bun Fullstack vs Vite Confusion +**What goes wrong:** Attempting to use Bun's native `Bun.serve()` with HTML entrypoints AND TanStack Router, which requires Vite's build pipeline. +**Why it happens:** Bun's fullstack dev server is compelling but incompatible with TanStack Router's file-based routing Vite plugin. +**How to avoid:** Use Vite for frontend (with TanStack Router plugin). Use Hono on Bun for backend. Connect via Vite proxy in dev, static file serving in prod. +**Warning signs:** Import errors from `@tanstack/router-plugin/vite`, missing route tree generation file. + +### Pitfall 2: Category Delete Without Reassignment +**What goes wrong:** Deleting a category with foreign key constraints either fails (FK violation) or cascades (deletes all items in that category). +**Why it happens:** Using `ON DELETE CASCADE` or not handling FK constraints at all. +**How to avoid:** Before deleting a category, reassign all its items to the "Uncategorized" default category (id=1). Then delete. This is a two-step transaction. +**Warning signs:** FK constraint errors on category delete, or silent item deletion. + +### Pitfall 3: Onboarding State Persistence +**What goes wrong:** User completes onboarding, refreshes the page, and sees the wizard again. +**Why it happens:** Storing onboarding completion state only in Zustand (memory). State is lost on page refresh. +**How to avoid:** Store `onboardingComplete` as a flag in SQLite (a simple `settings` table or a dedicated endpoint). Check on app load. +**Warning signs:** Onboarding wizard appears on every fresh page load. + +### Pitfall 4: Image Upload Without Cleanup +**What goes wrong:** Deleting an item leaves its image file on disk. Over time, orphaned images accumulate. +**Why it happens:** DELETE endpoint removes the DB record but forgets to unlink the file. +**How to avoid:** In the item delete service, check `imageFilename`, unlink the file from `uploads/` before or after DB delete. Wrap in try/catch -- file missing is not an error worth failing the delete over. +**Warning signs:** `uploads/` directory grows larger than expected, files with no matching item records. + +### Pitfall 5: TanStack Router Plugin Order in Vite Config +**What goes wrong:** File-based routes are not generated, `routeTree.gen.ts` is missing or stale. +**Why it happens:** TanStack Router plugin must be listed BEFORE `@vitejs/plugin-react` in the Vite plugins array. +**How to avoid:** Always order: `tanstackRouter()`, then `react()`, then `tailwindcss()`. +**Warning signs:** Missing `routeTree.gen.ts`, type errors on route imports. + +### Pitfall 6: Forgetting PRAGMA foreign_keys = ON +**What goes wrong:** Foreign key constraints between items and categories are silently ignored. Items can reference non-existent categories. +**Why it happens:** SQLite has foreign key support but it is OFF by default. Must be enabled per connection. +**How to avoid:** Run `PRAGMA foreign_keys = ON` immediately after opening the database connection, before any queries. +**Warning signs:** Items with categoryId pointing to deleted categories, no errors on invalid inserts. + +## Code Examples + +### Database Connection Singleton +```typescript +// src/db/index.ts +import { Database } from "bun:sqlite"; +import { drizzle } from "drizzle-orm/bun-sqlite"; +import * as schema from "./schema"; + +const sqlite = new Database("gearbox.db"); +sqlite.run("PRAGMA journal_mode = WAL"); +sqlite.run("PRAGMA foreign_keys = ON"); + +export const db = drizzle(sqlite, { schema }); +``` + +### Drizzle Config +```typescript +// drizzle.config.ts +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + out: "./drizzle", + schema: "./src/db/schema.ts", + dialect: "sqlite", + dbCredentials: { + url: "gearbox.db", + }, +}); +``` + +### Shared Zod Schemas +```typescript +// src/shared/schemas.ts +import { z } from "zod"; + +export const createItemSchema = z.object({ + name: z.string().min(1, "Name is required"), + weightGrams: z.number().nonnegative().optional(), + priceCents: z.number().int().nonnegative().optional(), + categoryId: z.number().int().positive(), + notes: z.string().optional(), + productUrl: z.string().url().optional().or(z.literal("")), +}); + +export const updateItemSchema = createItemSchema.partial().extend({ + id: z.number().int().positive(), +}); + +export const createCategorySchema = z.object({ + name: z.string().min(1, "Category name is required"), + emoji: z.string().min(1).max(4).default("📦"), +}); + +export const updateCategorySchema = z.object({ + id: z.number().int().positive(), + name: z.string().min(1).optional(), + emoji: z.string().min(1).max(4).optional(), +}); + +export type CreateItem = z.infer; +export type UpdateItem = z.infer; +export type CreateCategory = z.infer; +``` + +### Hono Item Routes with Zod Validation +```typescript +// src/server/routes/items.ts +import { Hono } from "hono"; +import { zValidator } from "@hono/zod-validator"; +import { createItemSchema, updateItemSchema } from "../../shared/schemas"; +import { db } from "../../db"; +import { items } from "../../db/schema"; +import { eq } from "drizzle-orm"; + +const app = new Hono(); + +app.get("/", async (c) => { + const allItems = db.select().from(items).all(); + return c.json(allItems); +}); + +app.post("/", zValidator("json", createItemSchema), async (c) => { + const data = c.req.valid("json"); + const result = db.insert(items).values(data).returning().get(); + return c.json(result, 201); +}); + +app.put("/:id", zValidator("json", updateItemSchema), async (c) => { + const id = Number(c.req.param("id")); + const data = c.req.valid("json"); + const result = db.update(items).set({ ...data, updatedAt: new Date() }) + .where(eq(items.id, id)).returning().get(); + if (!result) return c.json({ error: "Item not found" }, 404); + return c.json(result); +}); + +app.delete("/:id", async (c) => { + const id = Number(c.req.param("id")); + // Clean up image file if exists + const item = db.select().from(items).where(eq(items.id, id)).get(); + if (!item) return c.json({ error: "Item not found" }, 404); + if (item.imageFilename) { + try { await Bun.file(`uploads/${item.imageFilename}`).exists() && + await Bun.$`rm uploads/${item.imageFilename}`; } catch {} + } + db.delete(items).where(eq(items.id, id)).run(); + return c.json({ success: true }); +}); + +export { app as itemRoutes }; +``` + +### TanStack Query Hook for Items +```typescript +// src/client/hooks/useItems.ts +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import type { CreateItem, UpdateItem } from "../../shared/schemas"; + +const API = "/api/items"; + +export function useItems() { + return useQuery({ + queryKey: ["items"], + queryFn: async () => { + const res = await fetch(API); + if (!res.ok) throw new Error("Failed to fetch items"); + return res.json(); + }, + }); +} + +export function useCreateItem() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (data: CreateItem) => { + const res = await fetch(API, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (!res.ok) throw new Error("Failed to create item"); + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["items"] }); + queryClient.invalidateQueries({ queryKey: ["totals"] }); + }, + }); +} +``` + +### Seed Default Category +```typescript +// src/db/seed.ts +import { db } from "./index"; +import { categories } from "./schema"; + +export function seedDefaults() { + const existing = db.select().from(categories).all(); + if (existing.length === 0) { + db.insert(categories).values({ + name: "Uncategorized", + emoji: "📦", + }).run(); + } +} +``` + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| Zod 3.x + @hono/zod-validator | Zod 4.x fully supported | May 2025 (PR #1173) | No need to pin Zod 3.x. Resolves STATE.md blocker. | +| Tailwind config via JS | Tailwind v4 CSS-native config | Jan 2025 | No tailwind.config.js file. Theme defined in CSS via @theme directive. | +| Vite 7 (esbuild/Rollup) | Vite 8 (Rolldown-based) | 2025 | 5-30x faster builds. Same config API. | +| React Router v6/v7 | TanStack Router v1 | 2024 | Type-safe params, file-based routes, better SPA experience | +| bun:sqlite manual SQL | Drizzle ORM 0.45.x | Ongoing | Type-safe queries, migration tooling, schema-as-code | + +**Deprecated/outdated:** +- `tailwind.config.js`: Use CSS `@theme` directive in Tailwind v4 +- `better-sqlite3`: Use `bun:sqlite` (built-in, 3-6x faster) +- Vite `server.proxy` syntax: Verify correct format for Vite 8 (string shorthand still works) + +## Open Questions + +1. **Image upload size limit and accepted formats** + - What we know: CONTEXT.md says photos on cards are important for visual identification + - What's unclear: Maximum file size, accepted formats (jpg/png/webp), whether to resize on upload or defer to v2 + - Recommendation: Accept jpg/png/webp up to 5MB. Store as-is in `uploads/`. Defer resizing/thumbnailing to v2. Use `object-fit: cover` in CSS for consistent card display. + +2. **Onboarding wizard scope** + - What we know: Step-by-step guide through "create first category, add first item" + - What's unclear: Exact number of steps, whether it is a modal overlay or a full-page takeover + - Recommendation: 2-3 step modal overlay. Step 1: Welcome + create first category (with emoji picker). Step 2: Add first item to that category. Step 3: Done, show collection. Store completion flag in a `settings` table. + +3. **Weight input UX** + - What we know: Store grams internally. Display unit deferred to v2. + - What's unclear: Should the input field accept grams only, or allow free-text with unit suffix? + - Recommendation: For v1, use a numeric input labeled "Weight (g)". Clean and simple. V2 adds unit selector. + +## Validation Architecture + +### Test Framework +| Property | Value | +|----------|-------| +| Framework | Bun test runner (built-in, Jest-compatible API) | +| Config file | None needed (Bun detects test files automatically) | +| Quick run command | `bun test --bail` | +| Full suite command | `bun test` | + +### Phase Requirements -> Test Map +| Req ID | Behavior | Test Type | Automated Command | File Exists? | +|--------|----------|-----------|-------------------|-------------| +| COLL-01 | Create item with all fields | unit | `bun test tests/services/item.service.test.ts -t "create"` | No - Wave 0 | +| COLL-01 | POST /api/items validates input | integration | `bun test tests/routes/items.test.ts -t "create"` | No - Wave 0 | +| COLL-02 | Update item fields | unit | `bun test tests/services/item.service.test.ts -t "update"` | No - Wave 0 | +| COLL-02 | Delete item cleans up image | unit | `bun test tests/services/item.service.test.ts -t "delete"` | No - Wave 0 | +| COLL-03 | Create/rename/delete category | unit | `bun test tests/services/category.service.test.ts` | No - Wave 0 | +| COLL-03 | Delete category reassigns items to Uncategorized | unit | `bun test tests/services/category.service.test.ts -t "reassign"` | No - Wave 0 | +| COLL-04 | Compute per-category totals | unit | `bun test tests/services/totals.test.ts -t "category"` | No - Wave 0 | +| COLL-04 | Compute global totals | unit | `bun test tests/services/totals.test.ts -t "global"` | No - Wave 0 | + +### Sampling Rate +- **Per task commit:** `bun test --bail` +- **Per wave merge:** `bun test` +- **Phase gate:** Full suite green before `/gsd:verify-work` + +### Wave 0 Gaps +- [ ] `tests/services/item.service.test.ts` -- covers COLL-01, COLL-02 +- [ ] `tests/services/category.service.test.ts` -- covers COLL-03 +- [ ] `tests/services/totals.test.ts` -- covers COLL-04 +- [ ] `tests/routes/items.test.ts` -- integration tests for item API endpoints +- [ ] `tests/routes/categories.test.ts` -- integration tests for category API endpoints +- [ ] `tests/helpers/db.ts` -- shared test helper: in-memory SQLite instance with migrations applied +- [ ] Biome config: `bunx @biomejs/biome init` + +## Sources + +### Primary (HIGH confidence) +- [Bun fullstack dev server docs](https://bun.com/docs/bundler/fullstack) -- HTML entrypoints, Bun.serve() route config +- [Hono + Bun getting started](https://hono.dev/docs/getting-started/bun) -- fetch handler pattern, static file serving +- [Drizzle ORM + bun:sqlite setup](https://orm.drizzle.team/docs/get-started/bun-sqlite-new) -- schema, config, migrations +- [TanStack Router + Vite installation](https://tanstack.com/router/v1/docs/framework/react/installation/with-vite) -- plugin setup, file-based routing config +- [@hono/zod-validator Zod 4 support](https://github.com/honojs/middleware/issues/1148) -- PR #1173 merged May 2025, confirmed working + +### Secondary (MEDIUM confidence) +- [Bun + React + Hono full-stack pattern](https://dev.to/falconz/serving-a-react-app-and-hono-api-together-with-bun-1gfg) -- project structure, proxy/static serving pattern +- [Tailwind CSS v4 blog](https://tailwindcss.com/blog/tailwindcss-v4) -- CSS-native config, @theme directive + +### Tertiary (LOW confidence) +- Image upload best practices for Bun -- needs validation during implementation (file size limits, multipart handling) + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH -- all libraries verified via official docs, version compatibility confirmed, Zod 4 blocker resolved +- Architecture: HIGH -- Vite + Hono pattern well-documented, TanStack Router plugin requirement verified +- Pitfalls: HIGH -- drawn from PITFALLS.md research and verified against stack specifics +- Database schema: HIGH -- Drizzle + bun:sqlite pattern verified via official docs + +**Research date:** 2026-03-14 +**Valid until:** 2026-04-14 (stable ecosystem, no fast-moving dependencies)