diff --git a/.planning/phases/19-reference-item-model-tags-schema/19-CONTEXT.md b/.planning/phases/19-reference-item-model-tags-schema/19-CONTEXT.md new file mode 100644 index 0000000..30b67cf --- /dev/null +++ b/.planning/phases/19-reference-item-model-tags-schema/19-CONTEXT.md @@ -0,0 +1,137 @@ +# Phase 19: Reference Item Model & Tags Schema - Context + +**Gathered:** 2026-04-05 +**Status:** Ready for planning + + +## Phase Boundary + +Transform collection items from full data copies to reference pointers at global catalog entries. A reference item stores `globalItemId` + personal fields (categoryId, notes, purchasePriceCents, imageFilename, quantity); base data (brand, model, weight, MSRP, image, description) comes from the linked global item. Add a tag system so global items can be tagged for discovery and filtering. Update thread candidates to support `globalItemId`. Update thread resolution to create reference items with auto-link. + + + + +## Implementation Decisions + +### Reference Item Model +- **D-01:** Add `globalItemId` (nullable FK → globalItems) directly to the `items` table. When set, the item is a "reference item" — base data comes from the global item. +- **D-02:** Remove the `itemGlobalLinks` junction table entirely. It was a 1:1 relationship — a direct FK on items is simpler. Migrate existing link data first. +- **D-03:** Personal fields on items remain: `categoryId`, `notes`, `imageFilename`, `imageSourceUrl`, `quantity`. Add `purchasePriceCents` (nullable integer) for what the user actually paid. +- **D-04:** For reference items, `name`, `weightGrams`, `priceCents` on the items row are kept NULL or empty. The service layer merges global data when returning items. +- **D-05:** Standalone items (no `globalItemId`) continue to work as before — fully self-contained with all fields populated. This is the "manual entry" path. + +### Merged Data Strategy +- **D-06:** Service layer handles merging transparently. `getAllItems()` and `getItem()` join on globalItems when `globalItemId` is set, returning a unified shape. Clients see one type — no need to distinguish reference vs standalone. +- **D-07:** API response shape stays the same as current Item type. Global data fills in name, weight, price, etc. for reference items. Personal overrides (if any exist) take precedence — but initially only the fields listed in D-03 are personal. +- **D-08:** `productUrl` on reference items: comes from the global item's future `productUrl` field if added, otherwise NULL. For now, reference items may not have a product URL unless the global item has one. (Not critical — users can add to notes.) + +### Thread Candidates +- **D-09:** Add `globalItemId` (nullable FK → globalItems) to `threadCandidates` table. Candidates added from catalog have this set. +- **D-10:** Candidates with `globalItemId` display global item data (brand + model as name, weight, price, image). Personal candidate fields (notes, pros, cons, status) remain on the candidate row. +- **D-11:** Candidates without `globalItemId` work as before (fully manual data). + +### Thread Resolution +- **D-12:** When resolving a thread where the winning candidate has `globalItemId`, create a reference item: set `items.globalItemId` = candidate's `globalItemId`, copy only personal fields (categoryId, notes, imageFilename). Don't copy name/weight/price — those come from global item. +- **D-13:** When resolving a candidate WITHOUT `globalItemId`, behavior stays the same as today — full data copy. + +### Tag System +- **D-14:** New `tags` table: `id` (serial), `name` (text, unique, not null), `createdAt` (timestamp). +- **D-15:** New `globalItemTags` join table: `globalItemId` (FK), `tagId` (FK), composite primary key. Many-to-many. +- **D-16:** Tags are flat — no type column (gear-type, activity, property, etc.) for now. Keep it simple. Type categorization can be added later. +- **D-17:** Seed initial tag set via script covering common bikepacking/outdoor gear categories: handlebar-bag, framebag, saddlebag, tent, bivy, tarp, sleeping-bag, sleeping-pad, stove, cookware, headlamp, waterproof, ultralight, budget, premium, etc. +- **D-18:** Global item search extends to support tag filtering: `GET /api/global-items?q=...&tags=handlebar-bag,waterproof` returns items matching ALL specified tags. + +### Migration +- **D-19:** Migration script: for each row in `itemGlobalLinks`, set `items.globalItemId = itemGlobalLinks.globalItemId`, then drop `itemGlobalLinks` table. +- **D-20:** Existing items that had links keep their current name/weight/price data populated (not nulled out) — the service layer prefers global data for reference items, but having the old data as fallback is safe during transition. + +### Claude's Discretion +- Exact seed tag list content and count +- SQL migration ordering (add columns, migrate data, drop old table) +- Whether to update MCP tools in this phase or defer +- Test helper updates for new schema +- Whether global item search uses AND or OR for multiple tags (recommendation: AND — intersection filtering) + + + + +## Canonical References + +**Downstream agents MUST read these before planning or implementing.** + +### Design Spec +- `docs/superpowers/specs/2026-04-05-catalog-driven-gear-flow-design.md` — Full catalog-driven gear flow vision. Phase 19 implements the data model foundation described here. + +### Schema +- `src/db/schema.ts` — Current schema. Add globalItemId to items and threadCandidates, add tags + globalItemTags tables, remove itemGlobalLinks. + +### Services (must be updated for merged data) +- `src/server/services/item.service.ts` — Item CRUD — must join globalItems for reference items +- `src/server/services/global-item.service.ts` — Global item search — add tag filtering, remove linkItemToGlobal/unlinkItemFromGlobal (replaced by direct FK) +- `src/server/services/thread.service.ts` — resolveThread() at line 293 — must create reference items when candidate has globalItemId + +### Routes +- `src/server/routes/items.ts` — Item routes — link/unlink endpoints removed (replaced by globalItemId on item) +- `src/server/routes/global-items.ts` — Add tag filtering to search + +### Client (linking UI removed) +- `src/client/components/LinkToGlobalItem.tsx` — Remove or repurpose (direct linking replaced by add-from-catalog flow in Phase 21) + +### Tests +- `tests/services/global-item.service.test.ts` — Update for removed link/unlink, add tag tests +- `tests/services/thread.service.test.ts` — Update resolve tests for reference item creation +- `tests/helpers/db.ts` — Update for new schema (tags, globalItemTags, items.globalItemId) + +### Requirements +- `.planning/REQUIREMENTS.md` — CATFLOW-03, CATFLOW-04, CATFLOW-05, CATFLOW-06, TAG-01, TAG-02 + + + + +## Existing Code Insights + +### Reusable Assets +- `globalItems` table already exists with brand, model, category, weight, price, image, description +- `global-item.service.ts` has searchGlobalItems() with ILIKE — extend with tag filtering +- `thread.service.ts` resolveThread() — modify step 4 (item creation) for reference items +- `seed-global-items.ts` — extend seed script to also seed tags and assign them + +### Established Patterns +- Service DI pattern: `(db: Db, userId: number, ...)` — consistent across all services +- Drizzle ORM with pg-core — use same patterns for new tables +- Zod schemas in `src/shared/schemas.ts` — update for new fields +- Types inferred from Zod + Drizzle in `src/shared/types.ts` + +### Integration Points +- `src/db/schema.ts` — Add tables, add columns, remove itemGlobalLinks +- `src/server/index.ts` — No new route groups needed (existing global-items routes extended) +- `tests/helpers/db.ts` — Must include new tables in test migrations +- `src/db/seed-global-items.ts` — Extend to seed tags + + + + +## Specific Ideas + +- The reference model is the foundation for future crowd-sourced data: purchase prices (what users actually paid), real-world weights (vs manufacturer claims), and geographic price intelligence. The `purchasePriceCents` field is the first step. +- Catalog submission system (manual item → submit → admin review → convert to reference) is deferred but the data model should make this possible later. +- Weight override on personal items is deferred — not critical for now. + + + + +## Deferred Ideas + +- Catalog submission system — manual items submitted for admin review, approved items convert to references (saved in memory) +- Crowd-sourced purchase price intelligence — aggregate what users paid, show market prices vs MSRP (saved in memory) +- Crowd-sourced weight intelligence — user-submitted actual weights vs manufacturer claims (saved in memory) +- Admin tag management UI — manage tags via settings, not just seed script +- Tag type categorization — gear-type, activity, property, mounting as tag types +- Personal weight override field on items + + + +--- + +*Phase: 19-reference-item-model-tags-schema* +*Context gathered: 2026-04-05* diff --git a/.planning/phases/19-reference-item-model-tags-schema/19-DISCUSSION-LOG.md b/.planning/phases/19-reference-item-model-tags-schema/19-DISCUSSION-LOG.md new file mode 100644 index 0000000..762acf9 --- /dev/null +++ b/.planning/phases/19-reference-item-model-tags-schema/19-DISCUSSION-LOG.md @@ -0,0 +1,79 @@ +# Phase 19: Reference Item Model & Tags Schema - Discussion Log + +> **Audit trail only.** Do not use as input to planning, research, or execution agents. +> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered. + +**Date:** 2026-04-05 +**Phase:** 19-reference-item-model-tags-schema +**Areas discussed:** Reference item structure, Merged data strategy, Tag data model, Migration path +**Mode:** Auto (--auto flag) — all areas selected, recommended defaults chosen + +--- + +## Reference Item Structure + +| Option | Description | Selected | +|--------|-------------|----------| +| Add globalItemId to items, remove itemGlobalLinks | Direct FK on items table, drop junction table | ✓ | +| Keep itemGlobalLinks alongside globalItemId | Both mechanisms coexist | | +| Keep only itemGlobalLinks | No schema change to items | | + +**User's choice:** Add globalItemId to items, remove itemGlobalLinks +**Notes:** [auto] 1:1 relationship doesn't need a junction table. Direct FK is simpler. Migration moves existing link data first. + +--- + +## Merged Data Strategy + +| Option | Description | Selected | +|--------|-------------|----------| +| Same Item shape — services merge transparently | Client sees one type, services handle joins | ✓ | +| Separate ReferenceItem type | Different response shape for reference items | | +| Client-side merge | API returns raw, client resolves | | + +**User's choice:** Same Item shape — services merge transparently +**Notes:** [auto] No client-side changes needed for existing views. Service layer joins global data when globalItemId is set. + +--- + +## Tag Data Model + +| Option | Description | Selected | +|--------|-------------|----------| +| Separate tables, flat | tags + global_item_tags tables, no type column | ✓ | +| Separate tables, typed | tags + global_item_tags, tags have type column | | +| JSON column on globalItems | tags as JSON array, no separate tables | | + +**User's choice:** Separate tables, flat +**Notes:** [auto] Proper querying, many-to-many, type categorization can be added later. + +--- + +## Migration Path + +| Option | Description | Selected | +|--------|-------------|----------| +| Migrate links, keep old data as fallback | Set globalItemId from links, don't null out item fields | ✓ | +| Migrate links, null out duplicated fields | Clean break, reference items have no local data | | +| No migration, only new items use references | Existing items stay as copies forever | | + +**User's choice:** Migrate links, keep old data as fallback +**Notes:** [auto] Safe transition — old data remains as fallback while service layer prefers global data. + +--- + +## Claude's Discretion + +- Seed tag list content +- SQL migration ordering +- MCP tool updates timing +- Tag filtering logic (AND vs OR) + +## Deferred Ideas + +- Catalog submission system (admin review workflow) +- Price intelligence aggregation +- Weight intelligence aggregation +- Admin tag management UI +- Tag type categorization +- Personal weight override