docs: complete project research

This commit is contained in:
2026-03-16 11:39:16 +01:00
parent 1324018989
commit 79457053b3
5 changed files with 1273 additions and 889 deletions

View File

@@ -1,243 +1,208 @@
# Project Research Summary
**Project:** GearBox
**Domain:** Single-user gear management and purchase planning web app
**Researched:** 2026-03-14
**Project:** GearBox v1.2 -- Collection Power-Ups
**Domain:** Gear management (bikepacking, sim racing, etc.) -- feature enhancement milestone
**Researched:** 2026-03-16
**Confidence:** HIGH
## Executive Summary
GearBox is a single-user personal gear management app with a critical differentiator: purchase planning threads. Every competitor (LighterPack, GearGrams, Packstack, Hikt) is a post-purchase inventory tool — they help you track what you own. GearBox closes the loop by adding a structured pre-purchase research workflow where users compare candidates, track research status, and resolve threads by promoting winners into their collection. This is the entire reason to build the product; the collection management side is table stakes, and the purchase planning threads are the moat. Research strongly recommends building both together in the v1 scope, not sequencing them separately, because the thread resolution workflow only becomes compelling once a real collection exists to reference.
GearBox v1.2 adds six features to the existing gear management app: item search/filter, weight classification (base/worn/consumable), weight distribution charts, candidate status tracking, weight unit selection, and a planning category filter upgrade. Research confirms that four of six features require zero new dependencies -- they are pure application logic built on the existing stack (Drizzle ORM, React Query, Zod, Tailwind). The sole new dependency is `react-minimal-pie-chart` (~2kB gzipped) for donut chart visualization. The codebase is well-positioned for these additions: the settings table already supports key-value preferences, the `setup_items` join table is the correct place for weight classification, and the client-side data model is small enough for in-memory filtering.
The recommended architecture is a single-process Bun fullstack monolith: Hono for the API layer, React 19 + Vite 8 for the frontend, Drizzle ORM + bun:sqlite for the database, TanStack Router + TanStack Query for client navigation and server state, and Tailwind CSS v4 for styling. This stack is purpose-built for the constraints: Bun is a project requirement, SQLite is optimal for single-user, and every tool in the list has zero or near-zero runtime overhead. Zustand handles the small amount of client-only UI state. The entire stack is type-safe end-to-end through Zod schemas shared between client and server.
The recommended approach is to build weight unit selection first because it refactors the `formatWeight` function that every subsequent feature depends on for display. Search/filter and candidate status tracking are independent and low-risk. Weight classification is the most architecturally significant change -- it adds a column to the `setup_items` join table and changes the sync API shape from `{ itemIds: number[] }` to `{ items: Array<{ itemId, weightClass }> }`. Weight distribution charts come last because they depend on both the unit formatter and the classification data. The two schema changes (columns on `setup_items` and `thread_candidates`) should be batched into a single Drizzle migration.
The biggest risks are front-loaded in Phase 1: unit handling (weights must be canonicalized to grams from day one), currency precision (prices must be stored as integer cents), category flexibility (must use user-defined tags, not a hardcoded hierarchy), and image storage strategy (relative paths to a local directory, never BLOBs for full-size, never absolute paths). Getting these wrong requires painful data migrations later. The second major risk is the thread state machine in Phase 2 — the combination of candidate status, thread lifecycle, and "move winner to collection" creates a stateful flow that must be modeled as an explicit state machine with transactional resolution, not assembled incrementally.
The primary risks are: (1) weight unit conversion rounding drift from bidirectional conversion in edit forms, (2) accidentally placing weight classification on the `items` table instead of the `setup_items` join table, and (3) chart data diverging from displayed totals due to separate computation paths. All three are preventable with clear architectural rules established in the first phase: store grams canonically, convert only at the display boundary, and use a single source of truth for weight computations.
## Key Findings
### Recommended Stack
The stack is a tightly integrated Bun-native toolchain with no redundant tools. Bun serves as runtime, package manager, test runner, and provides built-in SQLite — eliminating entire categories of infrastructure. Vite 8 (Rolldown-based, 5-30x faster than Vite 7) handles the dev server and production frontend builds. The client-server boundary is clean: Hono serves the API, React handles the UI, and Zod schemas in a `shared/` directory provide a single source of truth for data shapes on both sides.
The existing stack (React 19, Hono, Drizzle ORM, SQLite, Bun) handles all v1.2 features without modification. One small library addition is needed.
The architecture note in STACK.md suggests Bun's fullstack HTML-based routing (not Vite's dev server proxy pattern). This differs slightly from the standard Vite proxy setup: each page is a separate HTML entrypoint imported into `Bun.serve()`, and TanStack Router handles in-page client-side navigation only. This simplifies the development setup to a single `bun run` command with no proxy configuration.
**Core technologies (all existing, no changes):**
- **Drizzle ORM `like()`, `eq()`, `and()`**: Available for server-side filtering if needed in the future, but client-side filtering is preferred at this scale
- **Zod `z.enum()`**: Validates weight classification (`"base" | "worn" | "consumable"`) and candidate status (`"researching" | "ordered" | "arrived"`) with compile-time type safety
- **React Query `useSetting()`**: Reactive settings caching ensures unit preference changes propagate to all weight displays without page refresh
- **Existing `settings` table**: Key-value store supports weight unit preference with no schema change
**Core technologies:**
- Bun 1.3.x: Runtime, package manager, test runner, bundler — eliminates Node.js and npm
- React 19.2.x + Vite 8.x: SPA framework + dev server — stable, large ecosystem, HMR out of the box
- Hono 4.12.x: API layer — Web Standards based, first-class Bun support, ~12kB, faster than Express on Bun
- SQLite (bun:sqlite) + Drizzle ORM 0.45.x: Database — zero-dependency, built into Bun, type-safe queries and migrations
- TanStack Router 1.167.x + TanStack Query 5.93.x: Routing + server state — full type-safe routing, automatic cache invalidation
- Tailwind CSS 4.2.x: Styling — CSS-native config, no JS file, microsecond incremental builds
- Zustand 5.x: Client UI state — minimal boilerplate for filter state, modals, theme
- Zod 4.3.x: Schema validation — shared between client and server as single source of truth for types
- Biome: Linting + formatting — replaces ESLint + Prettier, Rust-based, near-zero config
**New dependency:**
- **react-minimal-pie-chart ^9.1.2**: Donut/pie charts at ~2kB gzipped. React 19 compatible (explicit in peerDeps). Zero external dependencies. TypeScript native. Chosen over Recharts (~97kB, React 19 rendering issues reported) and Chart.js (~60kB, canvas-based, harder to style with Tailwind).
**Version flag:** Verify that `@hono/zod-validator` supports Zod 4.x before starting. If not, pin Zod 3.23.x until the validator is updated.
**What NOT to add:**
- Recharts, Chart.js, or visx (massive overkill for one chart type)
- Fuse.js or FTS5 (overkill for name search on sub-1000 item collections)
- XState (candidate status is a simple enum, not a complex state machine)
- i18n library for unit conversion (four constants and a formatter function)
### Expected Features
The feature research distinguishes cleanly between what every gear app does (table stakes) and what GearBox uniquely does (purchase planning threads). No competitor has threads, candidate comparison, or thread resolution. This is the entire competitive surface. Everything else is hygiene.
**Must have (table stakes):**
- **Search items by name** -- every competitor with an inventory has search; LighterPack notably lacks it and users complain
- **Filter items by category** -- partially exists in planning view, missing from collection view
- **Weight unit selection (g/oz/lb/kg)** -- universal across all competitors; gear specs come in mixed units
- **Weight classification (base/worn/consumable)** -- pioneered by LighterPack, now industry standard; "base weight" is the core metric of the ultralight community
**Must have (table stakes) — v1 launch:**
- Item CRUD with weight, price, category, notes, product URL — minimum unit of value
- User-defined categories/tags — must be flexible, not a hardcoded hierarchy
- Weight unit support (g, oz, lb, kg) — gear community requires this; store canonical grams internally
- Automatic weight/cost totals by category and setup — the reason to use an app over a text file
- Named setups composed from collection items — compose loadouts, get aggregate totals
- Planning threads with candidate items — the core differentiator
- Side-by-side candidate comparison with deltas (not just raw values) — the payoff of threads
- Thread resolution: pick winner, move to collection — closes the purchase research loop
- Search and filter on collection — essential at 30+ items
- Dashboard home page — clean entry point per project constraints
**Should have (differentiators):**
- **Weight distribution donut chart** -- LighterPack's pie chart is cited as its best feature; GearBox can uniquely combine category and classification breakdown
- **Candidate status tracking (researching/ordered/arrived)** -- entirely unique to GearBox's planning thread concept; no competitor has purchase lifecycle tracking
- **Per-setup classification** -- architecturally superior to competitors; the same item can be classified differently across setups
**Should have (competitive) — v1.x after validation:**
- Impact preview: how a thread candidate changes a specific setup's weight and cost
- Status tracking on thread items (researching / ordered / arrived)
- Priority/ranking within threads
- Photos per item (one photo per item initially)
- CSV import/export — migration path from spreadsheets, data portability
- Weight distribution visualization (pie/bar chart by category)
**Defer — v2+:**
- Multi-photo gallery per item
- Shareable read-only links for setups
- Drag-and-drop reordering
- Bulk operations (multi-select, bulk delete)
- Dark mode
- Item history/changelog
**Defer (v2+):**
- Per-item weight input in multiple units (parsing complexity)
- Interactive chart drill-down (click to zoom into categories)
- Weight goals/targets (opinionated norms conflict with hobby-agnostic design)
- Custom weight classification labels beyond base/worn/consumable
- Server-side full-text search (premature for single-user scale)
- Status change timestamps on candidates (useful but not essential now)
### Architecture Approach
The architecture is a monolithic Bun process with a clear 4-layer structure: API routes (HTTP concerns), service layer (business logic and calculations), Drizzle ORM (type-safe data access), and bun:sqlite (embedded storage). There are no microservices, no Docker, no external database server. The client is a React SPA served as static files by the same Bun process. Internal communication is REST + JSON; no WebSockets needed. The data model has three primary entities — items, threads (with candidates), and setups — connected by explicit foreign keys and a junction table for the many-to-many setup-to-items relationship.
All v1.2 features integrate into the existing three-layer architecture (client/server/database) with minimal structural changes. The client layer gains 5 new files (SearchBar, WeightChart, UnitSelector components; useFormatWeight hook; migration SQL) and modifies 15 existing files. The server layer changes are limited to the setup service (weight classification PATCH endpoint, updated sync function) and thread service (candidate status field passthrough). No new route registrations are needed in `src/server/index.ts`. The API layer (`lib/api.ts`) and UI state store (`uiStore.ts`) require no changes.
**Major components:**
1. Collection (items): Core entity. Source of truth for owned gear. Every other feature references items.
2. Planning Threads (threads + candidates): Pre-purchase research. Thread lifecycle is a state machine; resolution is transactional.
3. Setups: Named loadouts composed from collection items. Totals are always computed live from item data, never cached.
4. Service Layer: Business logic isolated from HTTP concerns. Enables testing without HTTP mocking. Key: `calculateSetupTotals()`, `computeCandidateImpact()`.
5. Dashboard: Read-only aggregation. Built last since it reads from all other entities.
6. Image Storage: Filesystem (`./uploads/` or `data/images/{item-id}/`) with relative paths in DB. Thumbnails on upload.
**Build order from ARCHITECTURE.md (follow this):**
1. Database schema (Drizzle) — everything depends on this
2. Items API (CRUD) — the core entity
3. Collection UI — first visible feature, validates end-to-end
4. Threads + candidates API and UI — depends on items for resolution
5. Setups API and UI — depends on items for composition
6. Dashboard — aggregates from all entities, build last
7. Polish: image upload, impact calculations, status tracking
1. **`useFormatWeight` hook** -- single source of truth for unit-aware weight formatting; wraps `useSetting("weightUnit")` and `formatWeight(grams, unit)` so all weight displays stay consistent
2. **`WeightChart` component** -- reusable donut chart wrapper; used in collection page (weight by category) and setup detail page (weight by classification)
3. **`SearchBar` component** -- reusable search input with clear button; collection page filters via `useMemo` over the cached `useItems()` data
4. **Updated `syncSetupItems`** -- breaking API change from `{ itemIds: number[] }` to `{ items: Array<{ itemId, weightClass }> }`; single call site (ItemPicker.tsx) makes this safe
5. **`PATCH /api/setups/:id/items/:itemId`** -- new endpoint for updating weight classification without triggering full sync (which would destroy classification data)
### Critical Pitfalls
1. **Unit handling treated as display-only** — Store all weights as canonical grams at write time. Accept any unit as input, convert on save. Build a `weightToGrams(value, unit)` utility on day one. A bare number field with no unit tracking will silently corrupt all aggregates when users paste specs in mixed units.
1. **Weight unit conversion rounding drift** -- bidirectional conversion in edit forms causes grams to drift over multiple edit cycles. Always load stored grams from the API, convert for display, and convert user input back to grams once on save. Never re-convert from a previously displayed value.
2. **Rigid category hierarchy** — Use user-defined flat tags, not a hardcoded category tree. A `categories` table with `parent_id` foreign keys will fail the moment a user tries to track sim racing gear or photography equipment. Tags allow many-to-many, support any hobby, and do not require schema changes to add a new domain.
2. **Weight classification at the wrong level** -- placing `classification` on the `items` table instead of `setup_items` prevents per-setup classification. A rain jacket is "worn" in summer but "base weight" in winter. This is the single most important schema decision in v1.2 and is costly to reverse.
3. **Thread state machine complexity** — Model the thread lifecycle as an explicit state machine before writing any code. Document valid transitions. The "resolve thread" action must be a single atomic transaction: validate winner exists, create collection item, mark thread resolved, update candidate statuses. Without this, impossible states (resolved thread with active candidates, ghost items in collection) accumulate silently.
3. **Chart data diverging from displayed totals** -- the codebase already has two computation paths (SQL aggregates in `totals.service.ts` vs. JavaScript reduce in `$setupId.tsx`). Adding charts creates a third. Use a shared utility for weight summation and convert units only at the final display step.
4. **Setup totals cached in the database** — Never store `totalWeight` or `totalCost` on a setup record. Always compute from live item data via `SUM()`. Cached totals go stale the moment any member item is edited, and the bugs are subtle (the UI shows a total that doesn't match the items).
4. **Server-side search for client-side data** -- adding search API parameters creates React Query cache fragmentation and unnecessary latency. Keep filtering client-side with `useMemo` over the cached items array.
5. **Comparison view that displays data but doesn't aid decisions** — The comparison view must show deltas between candidates and against the item being replaced from the collection, not just raw values side by side. Color-code lighter/heavier, cheaper/more expensive. A comparison table with no computed differences is worse than a spreadsheet.
**Additional high-priority pitfalls to address per phase:**
- Currency stored as floats (use integer cents always)
- Image paths stored as absolute paths or as BLOBs for full-size images
- Thread resolution is destructive (archive threads, don't delete them — users need to reference why they chose X over Y)
- Item deletion without setup impact warning
5. **Test helper desync with schema** -- the manual `createTestDb()` in `tests/helpers/db.ts` duplicates schema in raw SQL. Every column addition must be mirrored there or tests pass against the wrong schema.
## Implications for Roadmap
Based on the combined research, a 5-phase structure is recommended. Phases 1-3 deliver the v1 MVP; Phases 4-5 deliver the v1.x feature set.
Based on combined research, a 5-phase structure is recommended:
### Phase 1: Foundation — Data Model, Infrastructure, Core Item CRUD
### Phase 1: Weight Unit Selection
**Rationale:** Everything depends on getting the data model right. Unit handling, currency precision, category flexibility, image storage strategy, and the items schema are all Phase 1 decisions. Getting these wrong requires expensive data migrations. The architecture research explicitly states: "Database schema + Drizzle setup — Everything depends on the data model." The pitfalls research agrees: 6 of 9 pitfalls have "Phase 1" as their prevention phase.
**Rationale:** Foundational infrastructure. The `formatWeight` refactor touches every component that displays weight (~8 call sites). All subsequent features depend on this formatter working correctly with unit awareness. Building this first means classification totals, chart labels, and setup breakdowns automatically display in the user's preferred unit.
**Delivers:** Working gear catalog — users can add, edit, delete, and browse their collection. Item CRUD with all core fields. Weight unit conversion. User-defined categories. Image upload with thumbnail generation and cleanup on delete. SQLite database with WAL mode enabled, automatic backup mechanism, and all schemas finalized.
**Delivers:** Global weight unit preference (g/oz/lb/kg) stored in settings, `useFormatWeight` hook, updated `formatWeight` function, UnitSelector component in TotalsBar, correct unit display across all existing weight surfaces (ItemCard, CandidateCard, CategoryHeader, TotalsBar, setup detail), correct unit handling in ItemForm and CandidateForm weight inputs.
**Features from FEATURES.md:** Item CRUD with core fields, user-defined categories, weight unit support (g/oz/lb/kg), notes and product URL fields, search and filter.
**Addresses:** Weight unit selection (table stakes from FEATURES.md)
**Pitfalls to prevent:** Unit handling (canonical grams), currency precision (integer cents), category flexibility (user-defined tags, no hierarchy), image storage (relative paths, thumbnails), data loss prevention (WAL mode, auto-backup mechanism).
**Avoids:** Rounding drift (Pitfall 1), inconsistent unit application (Pitfall 7), flash of unconverted weights on load
**Research flag:** Standard patterns. Schema design for inventory apps is well-documented. No research phase needed.
**Schema changes:** None (uses existing settings table key-value store)
---
### Phase 2: Search, Filter, and Planning Category Filter
### Phase 2: Planning Threads — The Core Differentiator
**Rationale:** Pure client-side addition with no schema changes, no API changes, and no dependencies on other v1.2 features. Immediately useful as collections grow. The planning category filter upgrade fits naturally here since both involve filter UX and the icon-aware dropdown is a shared component.
**Rationale:** Threads are why GearBox exists. The feature dependency graph in FEATURES.md shows threads require items to exist (to resolve candidates into the collection), which is why Phase 1 must complete first. The thread state machine is the most complex feature in the product and gets its own phase to ensure the state transitions are modeled correctly before any UI is built.
**Delivers:** Search input in collection view, icon-aware category filter dropdown (reused in gear and planning tabs), filtered item display with count ("showing 12 of 47 items"), URL search param persistence, empty state for no results, result count display.
**Delivers:** Complete purchase planning workflow — create threads, add candidates with weight/price/notes, compare candidates side-by-side with weight/cost deltas (not just raw values), resolve threads by selecting a winner and moving it to the collection, archive resolved threads.
**Addresses:** Search items by name (table stakes), filter by category (table stakes), planning category filter upgrade (differentiator)
**Features from FEATURES.md:** Planning threads, side-by-side candidate comparison (with deltas), thread resolution workflow. Does not include status tracking (researching/ordered/arrived) or priority/ranking — those are v1.x.
**Avoids:** Server-side search anti-pattern (Pitfall 3), search state lost on tab switch (UX pitfall), category groups disappearing incorrectly during filtering
**Pitfalls to prevent:** Thread state machine complexity (model transitions explicitly, transactional resolution), comparison usefulness (show deltas and impact, not just raw data), thread archiving (never destructive resolution).
**Schema changes:** None
**Research flag:** Needs careful design work before coding. The state machine for thread lifecycle (open -> in-progress -> resolved/cancelled) combined with candidate status (researching / ordered / arrived) and the resolution side-effect (create collection item) has no off-the-shelf reference implementation. Design the state diagram first.
### Phase 3: Candidate Status Tracking
---
**Rationale:** Simple schema change on `thread_candidates` with minimal integration surface. Independent of other features. Low complexity but requires awareness of the existing thread resolution flow. Schema change should be batched with Phase 4 into one Drizzle migration.
### Phase 3: Setups — Named Loadouts and Composition
**Delivers:** Status column on candidates (researching/ordered/arrived), status badge on CandidateCard with click-to-cycle, status field in CandidateForm, Zod enum validation, status transition validation in service layer (researching -> ordered -> arrived, no backward transitions).
**Rationale:** Setups require items to exist (Phase 1) and benefit from threads being stable (Phase 2) because thread resolution can affect setup membership (the replaced item should be updatable in setups). The many-to-many setup-items relationship and the setup integrity pitfall require careful foreign key design.
**Addresses:** Candidate status tracking (differentiator -- unique to GearBox)
**Delivers:** Named setups composed from collection items. Weight and cost totals computed live (never cached). Base/worn/consumable weight classification per item per setup. Category weight breakdown. Item deletion warns about setup membership. Visual indicator when a setup item is no longer in the collection.
**Avoids:** Status without transition validation (Pitfall 4), test helper desync (Pitfall 6), not handling candidate status during thread resolution
**Features from FEATURES.md:** Named setups with item selection and totals, setup weight/cost breakdown by category, automatic totals.
**Schema changes:** Add `status TEXT NOT NULL DEFAULT 'researching'` to `thread_candidates`
**Pitfalls to prevent:** Setup totals cached in DB (always compute live), setup composition breaks on collection changes (explicit `ON DELETE` behavior, visual indicators for missing items, no silent CASCADE).
### Phase 4: Weight Classification
**Research flag:** Standard patterns for junction table composition. No research phase needed for the setup-items relationship. The weight classification (base/worn/consumable) per setup entry is worth a design session — this is per-setup metadata on the junction, not a property of the item itself.
**Rationale:** Most architecturally significant change in v1.2. Changes the sync API shape (breaking change, single call site). Requires Phase 1 to be complete so classification totals display in the correct unit. Schema migration should be batched with Phase 3.
---
**Delivers:** `weightClass` column on `setup_items`, updated sync endpoint accepting `{ items: Array<{ itemId, weightClass }> }`, new `PATCH /api/setups/:id/items/:itemId` endpoint, three-segment classification toggle per item in setup detail view, base/worn/consumable weight subtotals.
### Phase 4: Dashboard and Polish
**Addresses:** Weight classification base/worn/consumable (table stakes), per-setup classification (differentiator)
**Rationale:** The architecture research explicitly states "Dashboard — aggregates stats from all other entities. Build last since it reads from everything." Dashboard requires all prior phases to be stable since it reads from items, threads, and setups simultaneously. This phase also adds the weight visualization chart that requires a full dataset to be meaningful.
**Avoids:** Classification on items table (Pitfall 2), test helper desync (Pitfall 6), losing classification data on sync
**Delivers:** Dashboard home page with summary cards (item count, active threads, setup count, collection value). Weight distribution visualization (pie/bar chart by category). Dashboard stats endpoint (`/api/stats`) as a read-only aggregation. General UI polish for the "light, airy, minimalist" aesthetic.
**Schema changes:** Add `weight_class TEXT NOT NULL DEFAULT 'base'` to `setup_items`
**Features from FEATURES.md:** Dashboard home page, weight distribution visualization.
### Phase 5: Weight Distribution Charts
**Research flag:** Standard patterns. Dashboard aggregation is a straightforward read-only endpoint. Charting is well-documented. No research phase needed.
**Rationale:** Depends on Phase 1 (unit-aware labels) and Phase 4 (classification data for setup breakdown). Only phase requiring a new npm dependency. Highest UI complexity but lowest architectural risk -- read-only visualization of existing data.
---
**Delivers:** `react-minimal-pie-chart` integration, `WeightChart` component, collection-level donut chart (weight by category from `useTotals()`), setup-level donut chart (weight by classification), chart legend with consistent colors, hover tooltips with formatted weights.
### Phase 5: v1.x Enhancements
**Addresses:** Weight distribution visualization (differentiator)
**Rationale:** These features add significant value but depend on the core (Phases 1-3) being proven out. Impact preview requires both stable setups and stable threads. CSV import/export validates the data model is clean (if import is buggy, the model has problems). Photos add storage complexity that is easier to handle once the core CRUD flow is solid.
**Avoids:** Chart/totals divergence (Pitfall 5), chart crashing on null-weight items, unnecessary chart re-renders on unrelated state changes
**Delivers:** Impact preview (how a thread candidate changes a specific setup's weight/cost). Thread item status tracking (researching / ordered / arrived). Priority/ranking within threads. Photos per item (upload, display, cleanup). CSV import/export with unit detection.
**Features from FEATURES.md:** Impact preview, status tracking, priority/ranking, photos per item, CSV import/export.
**Pitfalls to prevent:** CSV import missing unit conversion (must detect and convert oz/lb/kg to grams on import). Image uploads without size/type validation. Product URLs not sanitized (validate http/https protocol, render with `rel="noopener noreferrer"`).
**Research flag:** CSV import with unit detection may need a design pass — handling "5 oz", "142g", "0.3 lb" in the same weight column requires a parsing strategy. Worth a short research spike before implementation.
---
**Schema changes:** None (npm dependency: `bun add react-minimal-pie-chart`)
### Phase Ordering Rationale
- **Data model first:** Six of nine pitfalls identified are Phase 1 prevention items. The schema is the hardest thing to change later and the most consequential.
- **Threads before setups:** Thread resolution creates collection items; setup composition consumes them. But more importantly, threads are the differentiating feature — proving the thread workflow works is more valuable than setups.
- **Dashboard last:** Explicitly recommended by architecture research. Aggregating from incomplete entities produces misleading data and masks bugs.
- **Impact preview in Phase 5:** This feature requires both stable setups (Phase 3) and stable threads (Phase 2). Building it before both are solid means rebuilding it when either changes.
- **Photos deferred to Phase 5:** The core value proposition is weight/cost tracking and purchase planning, not a photo gallery. Adding photo infrastructure in Phase 1 increases scope without validating the core concept.
- **Phase 1 first** because `formatWeight` is called by every weight-displaying component. Refactoring it after other features are built means touching the same files twice.
- **Phase 2 is independent** and could be built in any order, but sequencing it second allows the team to ship a quick win while Phase 3/4 schema changes are designed.
- **Batch Phase 3 + Phase 4 schema migrations** into one `bun run db:generate` run. Both add columns to existing tables; a single migration simplifies deployment.
- **Phase 4 after Phase 1** because classification totals need the unit-aware formatter.
- **Phase 5 last** because it is pure visualization depending on data from Phases 1 and 4, and introduces the only external dependency.
### Research Flags
**Needs design/research before coding:**
- **Phase 2 (Thread State Machine):** Design the state diagram for thread lifecycle x candidate status before writing any code. Define all valid transitions and invalid states explicitly. This is the most stateful feature in the product and has no off-the-shelf pattern to follow.
- **Phase 5 (CSV Import):** Design the column-mapping and unit-detection strategy before implementation. The spreadsheet-to-app migration workflow is critical for the target audience (users migrating from gear spreadsheets).
Phases likely needing deeper research during planning:
- **Phase 4 (Weight Classification):** The sync API shape change is breaking. The existing delete-all/re-insert pattern destroys classification data. Needs careful design of the PATCH endpoint and how ItemPicker interacts with classification preservation during item add/remove. Worth a `/gsd:research-phase`.
- **Phase 5 (Weight Distribution Charts):** react-minimal-pie-chart API specifics (label rendering, responsive sizing, animation control) should be validated with a quick prototype. Consider a short research spike.
**Standard patterns — no research phase needed:**
- **Phase 1 (Data model + CRUD):** Schema design for inventory apps is well-documented. Drizzle + bun:sqlite patterns are covered in official docs.
- **Phase 3 (Setups):** Junction table composition is a standard relational pattern. Foreign key behavior for integrity is documented.
- **Phase 4 (Dashboard):** Aggregation endpoints and charting are standard. No novel patterns.
Phases with standard patterns (skip research-phase):
- **Phase 1 (Weight Unit Selection):** Well-documented pattern. Extend `formatWeight`, add a `useSetting` wrapper, propagate through components. No unknowns.
- **Phase 2 (Search/Filter):** Textbook client-side filtering with `useMemo`. No API changes. Standard React pattern.
- **Phase 3 (Candidate Status):** Simple column addition with Zod enum validation. Existing `useUpdateCandidate` mutation already handles partial updates.
## Confidence Assessment
| Area | Confidence | Notes |
|------|------------|-------|
| Stack | HIGH | All technologies verified against official docs. Version compatibility confirmed. One flag: verify `@hono/zod-validator` supports Zod 4.x before starting. |
| Features | HIGH | Competitor analysis is thorough (LighterPack, GearGrams, Packstack, Hikt all compared). Feature gaps and differentiators are clearly identified. |
| Architecture | HIGH | Bun fullstack monolith pattern is official and well-documented. Service layer and data flow patterns are standard. |
| Pitfalls | HIGH | Pitfalls are domain-specific and well-sourced. SQLite BLOB guidance from official SQLite docs. Comparison UX from NN/g. Unit conversion antipatterns documented. |
| Stack | HIGH | Only one new dependency (react-minimal-pie-chart). React 19 compatibility verified via package.json peerDeps. All other features use existing stack with no changes. |
| Features | HIGH | Feature set derived from analysis of 8+ competing tools (LighterPack, Hikt, PackLight, Packstack, HikeLite, Packrat, OutPack, BPL Calculator). Clear consensus on table stakes vs. differentiators. |
| Architecture | HIGH | Based on direct codebase analysis with integration points mapped to specific files. The 5 new / 15 modified file inventory is concrete and verified against the existing codebase. |
| Pitfalls | HIGH | Derived from codebase-specific patterns (test helper duplication, dual computation paths) combined with domain risks (unit conversion rounding, classification scope). Not generic warnings. |
**Overall confidence: HIGH**
**Overall confidence:** HIGH
### Gaps to Address
- **Zod 4 / @hono/zod-validator compatibility:** STACK.md flags this explicitly. Verify before starting. If incompatible, pin Zod 3.23.x. This is a quick check, not a blocker.
- **Bun fullstack vs. Vite proxy setup:** STACK.md describes the Vite dev server proxy pattern (standard approach), while ARCHITECTURE.md describes Bun's HTML-based routing with `Bun.serve()` (newer approach). These are two valid patterns. The architecture file's approach (Bun fullstack) is simpler for production deployment. Confirm which pattern to follow before project setup — they require different `vite.config.ts` and entry point structures.
- **Weight classification (base/worn/consumable) data model:** Where does this live? On the `setup_items` junction table (per-setup classification, same item can be "base" in one setup and "worn" in another) or on the item itself (one classification for all setups)? The per-setup model is more flexible but more complex. Decide in Phase 1 schema design, not Phase 3 when setups are built.
- **Tag vs. single-category field:** PITFALLS.md recommends a flat tag system. FEATURES.md implies a single "category" field. The right answer is probably a single optional category field (for broad grouping, e.g., "clothing") plus user-defined tags for fine-grained organization. Confirm the data model in Phase 1.
- **`lb` display format:** FEATURES.md suggests "2 lb 3 oz" (pounds + remainder ounces) while STACK.md suggests simpler decimal format. The traditional "lb + oz" format is more useful to American users but adds formatting complexity. Decide during Phase 1 implementation.
- **Status change timestamps:** PITFALLS.md recommends storing `statusChangedAt` alongside `status` for staleness detection ("ordered 30 days ago -- still waiting?"). Low effort to add during the schema migration. Decide during Phase 3 planning.
- **Sync API backward compatibility:** The sync endpoint shape changes from `{ itemIds: number[] }` to `{ items: [...] }`. Single call site (ItemPicker.tsx), but verify no external consumers exist before shipping.
- **react-minimal-pie-chart responsive behavior:** SVG-based and should handle responsive sizing, but exact approach (CSS width vs. explicit size prop) should be validated in Phase 5. Not a risk, just a detail to confirm.
## Sources
### Primary (HIGH confidence)
- [Bun official docs](https://bun.com/docs) — bun:sqlite, fullstack dev server, Bun.serve() routing
- [Hono official docs](https://hono.dev/docs) — Bun integration, middleware patterns
- [Drizzle ORM docs - Bun SQLite](https://orm.drizzle.team/docs/connect-bun-sqlite) — driver support, schema patterns
- [Vite releases](https://vite.dev/releases) — v8.0 with Rolldown confirmed
- [Tailwind CSS v4.2 blog](https://tailwindcss.com/blog/tailwindcss-v4) — CSS-native config, Vite plugin
- [TanStack Router docs](https://tanstack.com/router/latest) — file-based routing, typed params
- [TanStack Query docs](https://tanstack.com/query/latest) — cache invalidation, mutations
- [SQLite Internal vs External BLOBs](https://sqlite.org/intern-v-extern-blob.html) — image storage guidance
- [Comparison Tables — NN/g](https://www.nngroup.com/articles/comparison-tables/) — comparison UX best practices
- [Drizzle ORM Filter Operators](https://orm.drizzle.team/docs/operators) -- like, eq, and operators for search/filter
- [Drizzle ORM Conditional Filters Guide](https://orm.drizzle.team/docs/guides/conditional-filters-in-query) -- dynamic filter composition
- [react-minimal-pie-chart GitHub](https://github.com/toomuchdesign/react-minimal-pie-chart) -- v9.1.2, React 19 peerDeps verified in package.json
- [LighterPack](https://lighterpack.com/) -- base/worn/consumable classification standard, pie chart visualization pattern
- [99Boulders LighterPack Tutorial](https://www.99boulders.com/lighterpack-tutorial) -- classification definitions and feature walkthrough
- [BackpackPeek Pack Weight Calculator Guide](https://backpackpeek.com/blog/pack-weight-calculator-base-weight-guide) -- weight classification methodology
- Direct codebase analysis of GearBox v1.1 -- schema.ts, services, hooks, routes, test helpers
### Secondary (MEDIUM confidence)
- [Hikt Blog: Best Backpacking Gear Apps 2026](https://hikt.app/blog/best-backpacking-gear-apps-2026/) — competitor feature analysis
- [Building Full-Stack App with Bun.js, React and Drizzle ORM](https://awplife.com/building-full-stack-app-with-bun-js-react-drizzle/) — project structure reference
- [Designing better file organization around tags, not hierarchies](https://www.nayuki.io/page/designing-better-file-organization-around-tags-not-hierarchies) — tags vs hierarchy rationale
- [Hikt](https://hikt.app/) -- searchable gear closet, base vs worn weight display
- [PackLight (iOS)](https://apps.apple.com/us/app/packlight-for-backpacking/id1054845207) -- search, categories, bar graph visualization
- [Packstack](https://www.packstack.io/) -- base/worn/consumable weight separation
- [Packrat](https://www.packrat.app/) -- flexible weight unit input and display conversion
- [Recharts React 19 issue #6857](https://github.com/recharts/recharts/issues/6857) -- rendering issues with React 19.2.3
- [TanStack Query filtering discussions](https://github.com/TanStack/query/discussions/1113) -- client-side vs server-side filtering patterns
- [LogRocket Best React Chart Libraries 2025](https://blog.logrocket.com/best-react-chart-libraries-2025/) -- chart library comparison
### Tertiary (LOW confidence / needs validation)
- [Zod v4 release notes](https://zod.dev/v4) — @hono/zod-validator compatibility with Zod 4 unconfirmed, verify before use
### Tertiary (LOW confidence)
- [SQLite LIKE case sensitivity](https://github.com/drizzle-team/drizzle-orm-docs/issues/239) -- LIKE is case-insensitive in SQLite (relevant only if search moves server-side)
- [Drizzle ORM SQLite migration pitfalls #1313](https://github.com/drizzle-team/drizzle-orm/issues/1313) -- data loss bug with push + add column (monitor during migration)
---
*Research completed: 2026-03-14*
*Research completed: 2026-03-16*
*Ready for roadmap: yes*