Archive roadmap, requirements, and phase directories to milestones/. Evolve PROJECT.md with validated requirements and key decisions. Reorganize ROADMAP.md with milestone grouping. Delete REQUIREMENTS.md (fresh for next milestone).
31 KiB
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>
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)
</user_constraints>
<phase_requirements>
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 |
</phase_requirements>
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:
# 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:
// 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",
},
});
// 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:
// 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:
// 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<UIState>((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:
// 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<number>`COALESCE(SUM(${items.weightGrams}), 0)`,
totalCost: sql<number>`COALESCE(SUM(${items.priceCents}), 0)`,
itemCount: sql<number>`COUNT(*)`,
})
.from(items)
.innerJoin(categories, eq(items.categoryId, categories.id))
.groupBy(items.categoryId)
.all();
}
export function getGlobalTotals() {
return db
.select({
totalWeight: sql<number>`COALESCE(SUM(${items.weightGrams}), 0)`,
totalCost: sql<number>`COALESCE(SUM(${items.priceCents}), 0)`,
itemCount: sql<number>`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.3in 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
// 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
// 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
// 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<typeof createItemSchema>;
export type UpdateItem = z.infer<typeof updateItemSchema>;
export type CreateCategory = z.infer<typeof createCategorySchema>;
Hono Item Routes with Zod Validation
// 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
// 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
// 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@themedirective in Tailwind v4better-sqlite3: Usebun:sqlite(built-in, 3-6x faster)- Vite
server.proxysyntax: Verify correct format for Vite 8 (string shorthand still works)
Open Questions
-
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. Useobject-fit: coverin CSS for consistent card display.
-
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
settingstable.
-
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-02tests/services/category.service.test.ts-- covers COLL-03tests/services/totals.test.ts-- covers COLL-04tests/routes/items.test.ts-- integration tests for item API endpointstests/routes/categories.test.ts-- integration tests for category API endpointstests/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 -- HTML entrypoints, Bun.serve() route config
- Hono + Bun getting started -- fetch handler pattern, static file serving
- Drizzle ORM + bun:sqlite setup -- schema, config, migrations
- TanStack Router + Vite installation -- plugin setup, file-based routing config
- @hono/zod-validator Zod 4 support -- PR #1173 merged May 2025, confirmed working
Secondary (MEDIUM confidence)
- Bun + React + Hono full-stack pattern -- project structure, proxy/static serving pattern
- Tailwind CSS v4 blog -- 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)