From ce941c6e1bbbed2521c626c534e3527b4f9d6aed Mon Sep 17 00:00:00 2001 From: Lovebug MCP Date: Fri, 24 Jul 2026 00:44:43 +0000 Subject: [PATCH] =?UTF-8?q?mcp:=20project-plan=20=E2=80=94=20PBS=20Members?= =?UTF-8?q?hip=20LOE=201=20Phase=204=20=E2=80=94=20Collections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pbs-membership-loe1-phase4-collections.md | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 Sources/Dev/pbs-membership-loe1-phase4-collections.md diff --git a/Sources/Dev/pbs-membership-loe1-phase4-collections.md b/Sources/Dev/pbs-membership-loe1-phase4-collections.md new file mode 100644 index 0000000..fe2e409 --- /dev/null +++ b/Sources/Dev/pbs-membership-loe1-phase4-collections.md @@ -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. \ No newline at end of file