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).
16 KiB
phase, verified, status, score, re_verification, gaps, human_verification
| phase | verified | status | score | re_verification | gaps | human_verification | |||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-foundation-and-collection | 2026-03-14T22:30:00Z | gaps_found | 15/16 must-haves verified | false |
|
|
Phase 1: Foundation and Collection Verification Report
Phase Goal: Users can catalog their gear collection with full item details, organize by category, and see aggregate weight and cost totals Verified: 2026-03-14T22:30:00Z Status: gaps_found — 1 bug blocks image upload Re-verification: No — initial verification
Goal Achievement
Observable Truths
| # | Truth | Status | Evidence |
|---|---|---|---|
| 1 | Project installs, builds, and runs (bun run dev starts both servers) | VERIFIED | Build succeeds in 176ms; 30 tests pass; all route registrations in src/server/index.ts |
| 2 | Database schema exists with items/categories/settings tables and proper foreign keys | VERIFIED | src/db/schema.ts: sqliteTable for all three; items.categoryId references categories.id; src/db/index.ts: PRAGMA foreign_keys = ON |
| 3 | Shared Zod schemas validate item and category data consistently | VERIFIED | src/shared/schemas.ts exports createItemSchema, updateItemSchema, createCategorySchema, updateCategorySchema; used by both routes and client |
| 4 | Default Uncategorized category is seeded on first run | VERIFIED | src/db/seed.ts: seedDefaults() called at server startup in src/server/index.ts line 11 |
| 5 | Test infrastructure runs with in-memory SQLite | VERIFIED | tests/helpers/db.ts: createTestDb() creates :memory: DB; 30 tests pass |
| 6 | POST /api/items creates an item with all fields | VERIFIED | src/server/routes/items.ts: POST / with zValidator(createItemSchema) calls createItem service |
| 7 | PUT /api/items/:id updates any field on an existing item | VERIFIED | src/server/routes/items.ts: PUT /:id calls updateItem; updateItem sets updatedAt = new Date() |
| 8 | DELETE /api/items/:id removes an item and cleans up its image file | VERIFIED | src/server/routes/items.ts: DELETE /:id calls deleteItem, then unlink(join("uploads", imageFilename)) in try/catch |
| 9 | POST /api/categories creates a category with name and emoji | VERIFIED | src/server/routes/categories.ts: POST / with zValidator(createCategorySchema) |
| 10 | DELETE /api/categories/:id reassigns items to Uncategorized then deletes | VERIFIED | category.service.ts deleteCategory: updates items.categoryId=1, then deletes category (note: no transaction wrapper despite comment) |
| 11 | GET /api/totals returns per-category and global weight/cost/count aggregates | VERIFIED | totals.service.ts: SQL SUM/COUNT aggregates via innerJoin; route returns {categories, global} |
| 12 | User can see gear items as cards grouped by category | VERIFIED | src/client/routes/index.tsx: groups by categoryId Map, renders CategoryHeader + ItemCard grid |
| 13 | User can add/edit items via slide-out panel with all fields | VERIFIED | ItemForm.tsx: all 7 fields present (name, weight, price, category, notes, productUrl, image); wired to useCreateItem/useUpdateItem |
| 14 | User can delete an item with a confirmation dialog | VERIFIED | ConfirmDialog.tsx: reads confirmDeleteItemId from uiStore, calls useDeleteItem.mutate on confirm |
| 15 | User can see global totals in a sticky bar at the top | VERIFIED | TotalsBar.tsx: sticky top-0, uses useTotals(), displays itemCount, totalWeight, totalCost |
| 16 | User can upload an image for an item and see it on the card | FAILED | Field name mismatch: apiUpload sends formData field 'file' (api.ts:55), server reads body['image'] (images.ts:13) — upload always returns 400 "No image file provided" |
| 17 | First-time user sees onboarding wizard | VERIFIED | __root.tsx: checks useOnboardingComplete(); renders OnboardingWizard if not "true" |
| 18 | Onboarding completion persists across refresh | VERIFIED | OnboardingWizard calls useUpdateSetting({key: "onboardingComplete", value: "true"}); stored in SQLite settings table |
Score: 15/16 must-haves verified (image upload blocked by field name mismatch)
Required Artifacts
Plan 01-01 Artifacts
| Artifact | Status | Details |
|---|---|---|
src/db/schema.ts |
VERIFIED | sqliteTable present; items, categories, settings all defined; priceCents, weightGrams, categoryId all present |
src/db/index.ts |
VERIFIED | PRAGMA foreign_keys = ON; WAL mode; drizzle instance exported |
src/db/seed.ts |
VERIFIED | seedDefaults() inserts "Uncategorized" if no categories exist |
src/shared/schemas.ts |
VERIFIED | All 4 schemas exported: createItemSchema, updateItemSchema, createCategorySchema, updateCategorySchema |
src/shared/types.ts |
VERIFIED | CreateItem, UpdateItem, CreateCategory, UpdateCategory, Item, Category exported |
vite.config.ts |
VERIFIED | TanStackRouterVite plugin; proxy /api and /uploads to localhost:3000 |
tests/helpers/db.ts |
VERIFIED | createTestDb() with :memory: SQLite, schema creation, Uncategorized seed |
Plan 01-02 Artifacts
| Artifact | Status | Details |
|---|---|---|
src/server/services/item.service.ts |
VERIFIED | getAllItems, getItemById, createItem, updateItem, deleteItem exported; uses db param pattern |
src/server/services/category.service.ts |
VERIFIED | getAllCategories, createCategory, updateCategory, deleteCategory exported |
src/server/services/totals.service.ts |
VERIFIED | getCategoryTotals, getGlobalTotals with SQL aggregates |
src/server/routes/items.ts |
VERIFIED | GET/, GET/:id, POST/, PUT/:id, DELETE/:id; Zod validation; exports itemRoutes |
src/server/routes/categories.ts |
VERIFIED | All CRUD verbs; 400 for Uncategorized delete; exports categoryRoutes |
src/server/routes/totals.ts |
VERIFIED | GET/ returns {categories, global}; exports totalRoutes |
src/server/routes/images.ts |
VERIFIED (route exists) | POST/ validates type/size, generates unique filename, writes to uploads/; exports imageRoutes — but field name mismatch with client (see Gaps) |
tests/services/item.service.test.ts |
VERIFIED | 7 unit tests pass |
tests/services/category.service.test.ts |
VERIFIED | 7 unit tests pass |
tests/services/totals.test.ts |
VERIFIED | 4 unit tests pass |
tests/routes/items.test.ts |
VERIFIED | 6 integration tests pass |
tests/routes/categories.test.ts |
VERIFIED | 4 integration tests pass |
Plan 01-03 Artifacts
| Artifact | Status | Lines | Details |
|---|---|---|---|
src/client/components/ItemCard.tsx |
VERIFIED | 62 | Image, name, weight/price/category chips; calls openEditPanel on click |
src/client/components/SlideOutPanel.tsx |
VERIFIED | 76 | Fixed right panel; backdrop; Escape key; slide animation |
src/client/components/ItemForm.tsx |
VERIFIED | 283 | All 7 fields; dollar-to-cents conversion; wired to useCreateItem/useUpdateItem |
src/client/components/CategoryPicker.tsx |
VERIFIED | 200 | ARIA combobox; search filter; inline create; keyboard navigation |
src/client/components/TotalsBar.tsx |
VERIFIED | 38 | Sticky; uses useTotals; shows count/weight/cost |
src/client/components/CategoryHeader.tsx |
VERIFIED | 143 | Subtotals; edit-in-place; delete with confirm; hover-reveal buttons |
src/client/routes/index.tsx |
VERIFIED | 138 | Groups by categoryId; CategoryHeader + ItemCard grid; empty state |
Plan 01-04 Artifacts
| Artifact | Status | Lines | Details |
|---|---|---|---|
src/client/components/OnboardingWizard.tsx |
VERIFIED | 322 | 4-step modal (welcome, category, item, done); skip link; persists via useUpdateSetting |
src/client/hooks/useSettings.ts |
VERIFIED | 37 | useSetting, useUpdateSetting, useOnboardingComplete exported; fetches /api/settings/:key |
src/server/routes/settings.ts |
VERIFIED | 37 | GET/:key returns setting or 404; PUT/:key upserts via onConflictDoUpdate |
Key Link Verification
| From | To | Via | Status | Details |
|---|---|---|---|---|
| src/db/schema.ts | src/shared/schemas.ts | Shared field names (priceCents, weightGrams, categoryId) | VERIFIED | Both use same field names; Zod schema matches DB column constraints |
| vite.config.ts | src/server/index.ts | Proxy /api to localhost:3000 | VERIFIED | proxy: {"/api": "http://localhost:3000"} in vite.config.ts |
| src/server/routes/items.ts | src/server/services/item.service.ts | import item.service | VERIFIED | All 5 service functions imported and called |
| src/server/services/item.service.ts | src/db/schema.ts | db.select().from(items) | VERIFIED | getAllItems, getItemById, createItem all query items table |
| src/server/services/category.service.ts | src/db/schema.ts | update items.categoryId on delete | VERIFIED | db.update(items).set({categoryId: 1}) in deleteCategory |
| src/server/routes/items.ts | src/shared/schemas.ts | zValidator(createItemSchema) | VERIFIED | zValidator("json", createItemSchema) on POST; updateItemSchema.omit({id}) on PUT |
| src/client/hooks/useItems.ts | /api/items | TanStack Query fetch | VERIFIED | queryFn: () => apiGet("/api/items") |
| src/client/components/ItemForm.tsx | src/client/hooks/useItems.ts | useCreateItem, useUpdateItem | VERIFIED | Both mutations imported and called in handleSubmit |
| src/client/components/CategoryPicker.tsx | src/client/hooks/useCategories.ts | useCategories, useCreateCategory | VERIFIED | Both imported; useCategories for list, useCreateCategory for inline create |
| src/client/routes/index.tsx | src/client/stores/uiStore.ts | useUIStore for panel state | VERIFIED | openAddPanel from useUIStore used for FAB and empty state CTA |
| src/client/components/OnboardingWizard.tsx | src/client/hooks/useSettings.ts | onboardingComplete update | VERIFIED | useUpdateSetting called with {key: "onboardingComplete", value: "true"} |
| src/client/hooks/useSettings.ts | /api/settings | fetch /api/settings/:key | VERIFIED | apiGet("/api/settings/${key}") and apiPut("/api/settings/${key}") |
| src/client/components/OnboardingWizard.tsx | src/client/hooks/useCategories.ts | useCreateCategory in wizard | VERIFIED | createCategory.mutate called in handleCreateCategory |
| src/client/lib/api.ts (apiUpload) | src/server/routes/images.ts | FormData field name | FAILED | client: formData.append("file", file) — server: body["image"] — mismatch causes 400 |
Requirements Coverage
| Requirement | Description | Plans | Status | Evidence |
|---|---|---|---|---|
| COLL-01 | User can add gear items with name, weight, price, category, notes, and product link | 01-01, 01-02, 01-03, 01-04 | SATISFIED | createItemSchema validates all fields; POST /api/items creates; ItemForm renders all fields wired to useCreateItem |
| COLL-02 | User can edit and delete gear items | 01-02, 01-03, 01-04 | SATISFIED | PUT /api/items/:id updates; DELETE cleans up image; ItemForm edit mode pre-fills; ConfirmDialog handles delete |
| COLL-03 | User can organize items into user-defined categories | 01-01, 01-02, 01-03, 01-04 | SATISFIED | categories table with FK; category CRUD API with reassignment on delete; CategoryPicker with inline create; CategoryHeader with rename/delete |
| COLL-04 | User can see automatic weight and cost totals by category and overall | 01-02, 01-03, 01-04 | SATISFIED | getCategoryTotals/getGlobalTotals via SQL SUM/COUNT; GET /api/totals; TotalsBar and CategoryHeader display values |
All 4 requirements are satisfied at the data and API layer. COLL-01 has a partial degradation (image upload fails due to field name mismatch) but the core add-item functionality works.
Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|---|---|---|---|---|
| src/client/lib/api.ts | 55 | formData.append("file", file) — wrong field name |
Blocker | Image upload always returns 400; upload feature is non-functional |
| src/server/services/category.service.ts | 67-73 | Comment says "Use a transaction" but no transaction wrapper used | Warning | Two-statement delete without atomicity; edge-case data integrity risk if server crashes mid-delete |
Human Verification Required
1. End-to-End Collection Experience
Test: Delete gearbox.db, start both servers (bun run dev:server, bun run dev:client), visit http://localhost:5173 Expected: Onboarding wizard appears as modal overlay; step through category creation and item creation; wizard closes and collection view shows the added item as a card under the correct category; sticky totals bar reflects the item count, weight, and cost; clicking the card opens the slide-out panel pre-filled; edits save and totals update; deleting an item shows the confirm dialog and removes the card; data persists on page refresh (wizard does not reappear) Why human: Visual rendering, animation transitions, and real-time reactivity require a browser
2. Image Upload After Field Name Fix
Test: After fixing the field name mismatch, edit an item and upload an image Expected: File picker opens, image uploads successfully, thumbnail preview appears in ImageUpload component, item card displays the image with object-cover aspect-[4/3] layout Why human: File picker interaction and visual image display require browser
3. Category Delete Atomicity
Test: Delete a category that has items; verify items appear under Uncategorized Expected: Items immediately move to Uncategorized; no orphaned items with invalid categoryId Why human: The service lacks a true transaction wrapper (despite the comment); normal operation works but crash-recovery scenario requires manual inspection or a stress test
Gaps Summary
One bug blocks the image upload feature. The client-side apiUpload function in src/client/lib/api.ts appends the file under the FormData field name "file" (line 55), but the server route in src/server/routes/images.ts reads body["image"] (line 13). This mismatch means every image upload request returns HTTP 400 with "No image file provided". The fix is a one-line change to either file. All other 15 must-haves are fully verified: infrastructure builds and tests pass (30/30), all CRUD API endpoints work with correct validation, the frontend collection UI is substantively implemented and wired to the API, the onboarding wizard persists state correctly to SQLite, and all four COLL requirements are satisfied at the functional level.
A secondary warning: the category delete service claims to use a transaction (comment on line 67) but executes two separate statements. This is not a goal-blocking issue but represents a reliability gap that should be noted for hardening.
Verified: 2026-03-14T22:30:00Z Verifier: Claude (gsd-verifier)