mcp: project-plan — PBS Membership LOE 1 Phase 4 — Collections
This commit is contained in:
parent
aebbc485c8
commit
ce941c6e1b
180
Sources/Dev/pbs-membership-loe1-phase4-collections.md
Normal file
180
Sources/Dev/pbs-membership-loe1-phase4-collections.md
Normal file
@ -0,0 +1,180 @@
|
||||
---
|
||||
created: '2026-07-24'
|
||||
path: Sources/Dev
|
||||
project: pbs-membership-loe1-phase4-collections
|
||||
tags:
|
||||
- pbs
|
||||
- membership
|
||||
- loe1
|
||||
- garden
|
||||
- collections
|
||||
- flask
|
||||
- mysql
|
||||
- data-model
|
||||
type: project-plan
|
||||
---
|
||||
|
||||
# PBS Membership LOE 1 Phase 4 — Collections
|
||||
|
||||
## Constitution
|
||||
|
||||
n/a — phase within an existing LOE. Inherits the platform's locked architecture (WordPress owns identity, Garden owns features, `pbs_garden` DB, WP-cookie auth, Option D chrome).
|
||||
|
||||
---
|
||||
|
||||
## Spec
|
||||
|
||||
### Goal
|
||||
|
||||
Named, shareable groups of saved recipes. A member organizes their saves into meaningful collections; Jenny publishes collections members can subscribe to.
|
||||
|
||||
### Problem
|
||||
|
||||
3c gives members one flat, save-date-ordered list. That doesn't scale past a few dozen saves and offers no way to group by occasion, season, or theme. Jenny already publishes recipe roundups as posts — collections are the member-side equivalent of something the audience already understands.
|
||||
|
||||
### Why separate from meal planning (LOE 2)
|
||||
|
||||
Different verbs. A collection is a taxonomy — these recipes belong together. A meal plan is a schedule — this recipe on Tuesday. Collections have no time dimension; LOE 2's model is entirely time (`meal_slots`, calendars). Merging them means one inherits a model it doesn't use. They will want to talk to each other later ("add this collection to next week's plan"), which is an integration between two clean things — cheap. Untangling one merged thing is not.
|
||||
|
||||
**Design constraint:** collections must be handable to a meal planner later. Stable collection IDs, clean "list the recipes in this collection" read.
|
||||
|
||||
### Out of scope
|
||||
|
||||
- Meal-plan integration (LOE 2)
|
||||
- `editor` role (schema leaves room; only `owner` and `subscriber` built)
|
||||
- Drag-and-drop reordering — carried from the April sketch, unresolved, may not survive contact with reality
|
||||
|
||||
### User journeys
|
||||
|
||||
**Own:** Member with saves creates "Weeknight Dinners," adds recipes to it, views it, removes one, deletes it.
|
||||
|
||||
**Subscribe:** Jenny publishes "Holiday Sides." Member subscribes. It appears alongside their own collections, marked as not theirs. Jenny adds a recipe next week; the member sees it.
|
||||
|
||||
**Cascade:** Member unsaves a recipe that sits in three of their collections. They're told which three before it goes.
|
||||
|
||||
### Requirements
|
||||
|
||||
- **FR-001** — WHEN a member creates a collection, THE SYSTEM SHALL write the collection and an `owner` membership row in a single transaction.
|
||||
- **FR-002** — WHEN a member views their collections, THE SYSTEM SHALL return all collections where a membership row exists for that member, with the role available to the UI.
|
||||
- **FR-003** — WHEN a member adds a recipe to a collection they own, THE SYSTEM SHALL create a save for that member if one does not already exist.
|
||||
- **FR-004** — WHEN a member unsaves a recipe, THE SYSTEM SHALL remove it from all collections that member owns.
|
||||
- **FR-005** — WHEN an unsave would remove a recipe from one or more collections, THE SYSTEM SHALL name those collections in a confirmation before proceeding.
|
||||
- **FR-006** — WHEN a member deletes a collection, THE SYSTEM SHALL leave their saves untouched.
|
||||
- **FR-007** — THE SYSTEM SHALL permit a recipe to belong to any number of a member's collections.
|
||||
- **FR-008** — THE SYSTEM SHALL permit a collection to be empty.
|
||||
- **FR-009** — WHEN a member subscribes to a collection, THE SYSTEM SHALL write a `subscriber` membership row without copying the collection or its items.
|
||||
- **FR-010** — WHEN an owner modifies a collection, THE SYSTEM SHALL reflect that change for all subscribers.
|
||||
- **FR-011** — WHEN a member without an `owner` role views a collection, THE SYSTEM SHALL present it read-only.
|
||||
- **FR-012** — THE SYSTEM SHALL permit more than one `owner` membership row per collection.
|
||||
|
||||
### Success criteria
|
||||
|
||||
All FRs observable in-browser. A member can organize saves into overlapping collections, and a Jenny-published collection reaches subscribers with edits propagating.
|
||||
|
||||
### Edge cases
|
||||
|
||||
- Recipe already saved when added to a collection → no duplicate save
|
||||
- Recipe absent from WordPress → inherits 3c's skip-and-log
|
||||
- Last owner leaves a collection with subscribers → **unresolved** (see Unresolved questions)
|
||||
- Subscriber unsubscribes → membership row removed; collection and other members untouched
|
||||
|
||||
---
|
||||
|
||||
## Plan
|
||||
|
||||
### Decisions
|
||||
|
||||
| Decision | Chosen | Rejected | Reversibility |
|
||||
|---|---|---|---|
|
||||
| Relationship to LOE 2 | Separate feature | Collections as part of meal planning — different verbs, forces an unused time model | Two-way, but merging later is expensive |
|
||||
| Item pointer | `collection_items` → recipe post ID | Point at `user_interactions` row — collides with sharing (would reference another user's rows or manufacture phantom saves); sharing requires dereferencing to the recipe anyway | One-way once data exists |
|
||||
| Item polymorphism | `item_type` + `item_id`, only `recipe` used | Recipe-only column — cheap to leave room, no cost now | Two-way |
|
||||
| Collections per recipe | Many | One (folder model) — forces a false choice; roundups are genuinely multi-category | Two-way |
|
||||
| Control model | `collection_members` rows with `role` | Single `user_id` owner column + separate subscriptions table — can't express multiple owners; garden is intended as community-oriented | One-way in practice |
|
||||
| Sharing mechanism | Subscribe — one collection, edits propagate | Copy — cheaper and satisfies the invariant naturally, but Jenny's later edits never reach members | Two-way |
|
||||
| Save/collection coupling | Adding to an owned collection creates the save | Independent — would break the must-be-saved invariant | Two-way |
|
||||
| Unsave behavior | Cascades out of the member's owned collections | Independent — leaves collections holding unsaved recipes, contradicting the invariant | Two-way |
|
||||
| Collection deletion | Does not touch saves | Cascade to saves — destructive and surprising | Two-way |
|
||||
|
||||
### Data model
|
||||
|
||||
Two new tables in `pbs_garden`:
|
||||
|
||||
**`collections`** — id, name, timestamps. No owner column; ownership is a membership row.
|
||||
|
||||
**`collection_members`** — collection_id, user_id, role (`owner` | `subscriber`; `editor` reserved), timestamp. Unique on (collection_id, user_id).
|
||||
|
||||
**`collection_items`** — collection_id, item_type (`recipe`), item_id (WP post ID), timestamp. Unique on (collection_id, item_type, item_id).
|
||||
|
||||
No changes to `user_interactions`. The must-be-saved invariant is enforced in application logic, not schema.
|
||||
|
||||
**Known consequence:** with ownership in membership rows, nothing at the schema level guarantees a collection has an owner at all. Creation transaction handles the happy path; the owner-departure case is unresolved below.
|
||||
|
||||
### Interfaces / contracts
|
||||
|
||||
JSON endpoints under the garden blueprint, inheriting 3c's CSRF pattern for all writes — token in page, sent as header, validated server-side. Reads need no token.
|
||||
|
||||
**Writes:** create collection, rename, delete, add item, remove item, subscribe, unsubscribe.
|
||||
|
||||
**Reads:** list my collections (owned + subscribed, role included), list items in a collection.
|
||||
|
||||
### Deployment
|
||||
|
||||
Inherits 3a — existing `pbs-garden` container, Traefik routing. New tables require a migration.
|
||||
|
||||
### Tests
|
||||
|
||||
Deferred to the end-of-LOE hardening pass, consistent with 3c. Two worth writing then: the cascade (unsave removes from all owned collections) and the creation transaction (collection without owner row must not persist).
|
||||
|
||||
### NFRs
|
||||
|
||||
The "which collections is this recipe in" read on the flat saved list is an extra query per row if done naively — batch it. Otherwise n/a at this scale.
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
### Phase 4 scope
|
||||
|
||||
- Migration for three tables
|
||||
- Create/rename/delete collection + owner membership transaction (FR-001, FR-006)
|
||||
- Add/remove item with save-creation (FR-003, FR-007, FR-008)
|
||||
- Collections list read with role (FR-002, FR-011)
|
||||
- Unsave cascade + confirmation (FR-004, FR-005)
|
||||
- Subscribe/unsubscribe (FR-009, FR-010, FR-012)
|
||||
|
||||
### Iterate-shaped
|
||||
|
||||
**Collection UI and the cascade confirmation wording** — converge in-browser.
|
||||
|
||||
### Human-required handoffs
|
||||
|
||||
- Jenny's collection cards may want their own seed-packet treatment; asset decision
|
||||
- Jenny defines the first published collections
|
||||
|
||||
---
|
||||
|
||||
## Cross-cutting
|
||||
|
||||
### Codebase assumptions
|
||||
|
||||
3c shipped: the saved-recipes page, the JS-write-plus-CSRF pattern, hard delete on unsave with optimistic undo, WP featured image as canonical thumbnail with fallback, WP-absence skip-and-log.
|
||||
|
||||
### Unresolved questions
|
||||
|
||||
**Must-resolve before code:**
|
||||
|
||||
- **Does the must-be-saved invariant apply to subscribed collections?** Either propagation auto-saves into every subscriber's account when an owner adds a recipe, or the invariant is scoped to owned collections and subscribed ones are view-only lists. Travis is thinking on it. The read-only reading is consistent with FR-011. **Note: this decision impacts the relational data structure** — it is not purely an application-logic or UX choice.
|
||||
|
||||
**Defer and tag:**
|
||||
|
||||
- Last owner leaving a collection that has subscribers
|
||||
- Whether the flat saved list displays collection membership
|
||||
|
||||
**Soft — revisit in testing:**
|
||||
|
||||
- The must-be-saved invariant generally. If it proves brittle, drop it and let collections hold unsaved recipes.
|
||||
|
||||
### Pre-mortem
|
||||
|
||||
Most likely failure is the invariant. It's enforced in application logic across four separate paths — add item, unsave, subscribe, and collection delete — and a miss in any one leaves collections holding unsaved recipes. Second: the creation transaction — a collection whose owner row failed to write is invisible and unrecoverable through the UI. Third: the cascade confirmation is the first destructive multi-object action in the garden, and getting it wrong means silently destroying curation a member spent real effort on.
|
||||
Loading…
Reference in New Issue
Block a user