docs(13-03): complete wishlist plan

This commit is contained in:
Jakub Zych
2026-10-03 08:24:27 +02:00
parent 7ccabf5394
commit d1d5ee2012
2 changed files with 404 additions and 0 deletions

View File

@@ -0,0 +1,398 @@
---
phase: 13-p-ytarium-api-wishlist-notifications-csv-credentials-public
plan: 03
subsystem: api
tags: [wishlist, reservations, subscriptions, notifications, digest, conga, river, postcard, parity, tide, broadcasts]
requires:
- phase: 13-01
provides: surf overlap families, conga workerless kinds, lagoon prohibited, tide notification publication masks, job contract, php_parity.sh rows and QUEUE_CONNECTION, share:wishlist capture
- phase: 13-02
provides: WriteNotification usage, notification registry routes, seed-state pattern (fonotekaRouteExtras), {{secret:...}} vars
- phase: 12
provides: album write path (CreateAlbum/UpdateAlbum, covers, broadcast suppression), share service, invitation mail job pattern, Winter error pages
provides:
- "Wishlist resolver and scopes: ActiveWishlist/ProvisionWishlist (users-row lock), WishlistsVisibleTo, HouseholdPeerIDs, ReservableWishlistIDs"
- "Optional reservation key on AlbumDTO (WithReservation, WithoutArtists serializer options) with stateForViewer's three-way mask"
- "30 wishlist routes ported (26 JWT, 4 personal-token): own list/show/store/update/destroy/similar, share, settings, household, subscriptions by token and by collection, subscribers, reservations, reveal, purchase, peer albums"
- "Item-added branch of albumAddedCallback: bell rows plus an atomic digest upsert that dispatches one delayed wishlist_digest job per window (no worker until Phase 14)"
- "SetJobDispatcher/JobDispatcher/ErrNoJobDispatcher wired at boot; jobDB fresh-statement handle for job queues inside GORM callbacks"
- "ResolvePublic(ctx, db, token, kind) in share_service.go for 13-05"
- "Purchase: MoveToCollection in one transaction (updated album event, release, bells, one wishlist_purchased_mail job per email subscriber) and the mail worker with the pl/en templates"
- "Parity: wishlist seed state on both sides, 30 routes re-recorded (143 ported), nuxt-wishlist (+ rows golden), mcp-wishlist, three publication goldens"
affects: [13-04, 13-05, 13-06, 14]
actuals:
tokens: 106635
tasks: 4
commits: 4
tech-stack:
added: []
patterns:
- "Serializer options: SerializeAlbums(..., WithReservation(rc), WithoutArtists()) per PHP call site; no option keeps every existing body byte-identical"
- "Job queues called from inside a GORM callback get jobDB(tx, ctx) (a chained fresh statement), never a bare cleanSession handle"
- "Multi-publication broadcast goldens are compared order-insensitively (Go sends each publication as its own River job) on a database of their own"
- "Parity cases that need two states use PARITY_CASE=albums,wishlist on both sides via fonotekaCaseExtras"
key-files:
created:
- fonoteka.go/plugins/golem15/fonoteka/classes/wishlist_resolver.go
- fonoteka.go/plugins/golem15/fonoteka/classes/reservations.go
- fonoteka.go/plugins/golem15/fonoteka/classes/wishlist_subscriptions.go
- fonoteka.go/plugins/golem15/fonoteka/classes/wishlist_notifications.go
- fonoteka.go/plugins/golem15/fonoteka/classes/similarity_finder.go
- fonoteka.go/plugins/golem15/fonoteka/controllers/api/wishlist_albums_controller.go
- fonoteka.go/plugins/golem15/fonoteka/controllers/api/wishlist_share_settings_controller.go
- fonoteka.go/plugins/golem15/fonoteka/controllers/api/wishlist_subscriptions_controller.go
- fonoteka.go/plugins/golem15/fonoteka/controllers/api/wishlist_reservations_controller.go
- fonoteka.go/plugins/golem15/fonoteka/controllers/api/wishlist_peer_controller.go
- fonoteka.go/plugins/golem15/fonoteka/views/mail/wishlist_item_purchased.htm
- fonoteka.go/plugins/golem15/fonoteka/views/mail/wishlist_item_purchased-en.htm
- fonoteka.go/plugins/golem15/fonoteka/wishlist_smoke_test.go
- fonoteka.go/plugins/golem15/fonoteka/wishlist_notifications_test.go
- fonoteka.go/plugins/golem15/fonoteka/wishlist_reservations_test.go
- fonoteka.go/parity/fixtures/nuxt/nuxt-wishlist.yaml
- fonoteka.go/parity/fixtures/nuxt/nuxt-wishlist.rows.json
- fonoteka.go/parity/fixtures/mcp/mcp-wishlist.yaml
- fonoteka.go/parity/fixtures/broadcasts/flows/wishlist.yaml
- fonoteka.go/parity/fixtures/broadcasts/wishlist-item-added.yaml
- fonoteka.go/parity/fixtures/broadcasts/reservation-revealed.yaml
- fonoteka.go/parity/fixtures/broadcasts/wishlist-purchased.yaml
modified:
- fonoteka.go/plugins/golem15/fonoteka/classes/serialize_album.go
- fonoteka.go/plugins/golem15/fonoteka/classes/notification_service.go
- fonoteka.go/plugins/golem15/fonoteka/classes/share_service.go
- fonoteka.go/plugins/golem15/fonoteka/classes/album_write_service.go
- fonoteka.go/plugins/golem15/fonoteka/controllers/api/albums_controller.go
- fonoteka.go/plugins/golem15/fonoteka/routes.go
- fonoteka.go/plugins/golem15/fonoteka/realtime.go
- fonoteka.go/plugins/golem15/fonoteka/jobs.go
- fonoteka.go/plugins/golem15/fonoteka/mail.go
- fonoteka.go/plugins/golem15/fonoteka/phase08_coverage_test.go
- fonoteka.go/parity/manifest.yaml
- fonoteka.go/parity/fixtures/routes/ (30 routes, 108 route fixtures)
- fonoteka.go/parity/fonoteka_seed_test.go
- fonoteka.go/parity/fonoteka_reset.php
- fonoteka.go/parity/fonoteka_flows_test.go
- fonoteka.go/parity/broadcast_goldens_test.go
- fonoteka.go/parity/parity_test.go
- fonoteka.go/parity/parity_contract_test.go
- fonoteka.go/parity/README.md
key-decisions:
- "The item-added discretion is resolved by extending albumAddedCallback (RESEARCH recommendation): the hook loads the collection kind once and calls NotifyAlbumAdded or NotifyWishlistItemAdded, so every insert path (JWT, token group, direct CreateAlbum, later CSV) notifies exactly once on the insert's transaction."
- "The digest upsert is one INSERT ... ON CONFLICT ... RETURNING (xmax = 0); only the row-creating insert dispatches WishlistDigestArgs on fonoteka_wishlist_digest with label wishlist_digest and the 1800 s delay. A missing dispatcher fails the write with ErrNoJobDispatcher."
- "A freshly provisioned wishlist is returned with ReservationsAllowed false in memory (the row holds the default true), because PHP answers that request from the unsaved model; GET wishlist/settings for a new user reports false once, as recorded."
- "Reserve and reveal bodies serialize artists as [] and rating as null: PHP loads only reservation.user there and serializeAlbum reads artists only when eager-loaded. Expressed as the WithoutArtists serializer option."
- "MoveToCollection saves the album under broadcast suppression and emits its one updated.fonoteka.album event before releasing the reservation and writing the bells, matching PHP's save-time broadcast order; it takes the upload bucket for the payload."
- "Purchase mail is queued only for subscribers whose user row still exists (PHP returns early for a missing user); the worker also skips a subscriber deleted before it runs, logging the id only."
- "Wishlist routes guard path ids above the Postgres integer range with int4PathID (PHP's 404); the general pathID gap stays logged."
patterns-established:
- "A route family whose cases need wishlist data maps to the wishlist seed state in fonotekaRouteExtras; PHP and Go apply it identically, including the notifications sequence lift on the Go side"
- "Flows recorded with QUEUE_CONNECTION=database dump their row goldens with php_parity.sh rows using an id-free SELECT the Go replay runs verbatim on Postgres"
requirements-completed: [API-03]
coverage:
- id: D1
description: "Own wishlist list (both groups) and show (JWT): provisioned once under a users-row lock, PHP's pagination envelope, owner reservation mask without reserved_by before reveal, 404 JSON for foreign/missing/out-of-range ids; existing album bodies carry no reservation key"
requirement: API-03
verification:
- kind: unit
ref: "fonoteka.go/plugins/golem15/fonoteka/wishlist_smoke_test.go#TestWishlistOwnListAndShow"
status: pass
- kind: integration
ref: "fonoteka.go/parity/parity_test.go#TestParityCorpus (GET wishlist/albums jwt + personal_token, GET wishlist/albums/{id})"
status: pass
human_judgment: false
- id: D2
description: "Wishlist item store/update/destroy on both groups through the album write path (condition/shelf prohibited, 422 JSON with validation.prohibited, duplicate check against the real collection) and similar (ILIKE, literal %, _ and \\)"
requirement: API-03
verification:
- kind: unit
ref: "fonoteka.go/plugins/golem15/fonoteka/wishlist_smoke_test.go#TestWishlistShareSettingsHousehold/similar"
status: pass
- kind: integration
ref: "fonoteka.go/parity/parity_test.go#TestParityCorpus (POST/PUT/DELETE wishlist/albums on both groups, GET wishlist/albums/similar)"
status: pass
human_judgment: false
- id: D3
description: "Item-added side effects: one bell row per ws-enabled non-actor subscriber (household peers included) and one digest upsert per email subscriber on every insert path; one dispatched digest job per window (1800 s, unserved queue, label wishlist_digest); rollback leaves nothing; matching notification publications"
requirement: API-03
verification:
- kind: integration
ref: "fonoteka.go/plugins/golem15/fonoteka/wishlist_notifications_test.go#TestWishlistItemAddedOncePerPath"
status: pass
- kind: integration
ref: "fonoteka.go/plugins/golem15/fonoteka/wishlist_notifications_test.go#TestDigestCoalescing"
status: pass
- kind: integration
ref: "fonoteka.go/parity/broadcast_goldens_test.go#TestBroadcastGoldens/wishlist-item-added"
status: pass
human_judgment: false
- id: D4
description: "Wishlist share (/w/ paths, regenerate under the row lock, throttle:10,1), settings (reservations_allowed required|boolean, 422 JSON, follower count) and household (8 previews by name, household vs subscribed), JWT only"
requirement: API-03
verification:
- kind: unit
ref: "fonoteka.go/plugins/golem15/fonoteka/wishlist_smoke_test.go#TestWishlistShareSettingsHousehold"
status: pass
- kind: integration
ref: "fonoteka.go/parity/parity_test.go#TestParityCorpus (wishlist share, share/regenerate, settings, household)"
status: pass
human_judgment: false
- id: D5
description: "Subscriptions by token (ResolvePublic: shape, LOWER() lookup, constant-time compare) and by visible collection, roster with synthetic household peers, subscriptions list in PHP order, Winter 404 pages"
requirement: API-03
verification:
- kind: unit
ref: "fonoteka.go/plugins/golem15/fonoteka/wishlist_reservations_test.go#TestWishlistSubscriptions"
status: pass
- kind: integration
ref: "fonoteka.go/parity/parity_test.go#TestParityCorpus (subscribers, subscriptions, token and collection subscribe routes)"
status: pass
human_judgment: false
- id: D6
description: "Reservations: first wins under the album row lock (the loser gets the 409 page), 422 own, 409 disabled, reserver-only cancel, owner-only idempotent reveal writing one bell row, owner never learns the reserver before reveal"
requirement: API-03
verification:
- kind: integration
ref: "fonoteka.go/plugins/golem15/fonoteka/wishlist_reservations_test.go#TestReserveConcurrent"
status: pass
- kind: integration
ref: "fonoteka.go/plugins/golem15/fonoteka/wishlist_reservations_test.go#TestRevealIdempotent"
status: pass
- kind: integration
ref: "fonoteka.go/plugins/golem15/fonoteka/wishlist_reservations_test.go#TestReservationMask"
status: pass
- kind: integration
ref: "fonoteka.go/parity/broadcast_goldens_test.go#TestBroadcastGoldens/reservation-revealed"
status: pass
human_judgment: false
- id: D7
description: "Purchase (D-07): move to the real collection, release, bells for every ws subscriber (actor included) and the reserver once, one purchase mail job per email subscriber in the transaction; the worker sends the pl/en template with PHP's subject; rollback leaves no move, job, mail or row"
requirement: API-03
verification:
- kind: integration
ref: "fonoteka.go/plugins/golem15/fonoteka/wishlist_notifications_test.go#TestPurchaseSideEffects"
status: pass
- kind: integration
ref: "fonoteka.go/plugins/golem15/fonoteka/wishlist_notifications_test.go#TestPurchaseMailAfterCommit"
status: pass
- kind: integration
ref: "fonoteka.go/parity/broadcast_goldens_test.go#TestBroadcastGoldens/wishlist-purchased"
status: pass
human_judgment: false
- id: D8
description: "Peer wishlist albums index/show through reservableWishlistIds with the viewer's mask, and the four routes.php overlap pairs dispatching to the real handlers through the assembled router; nothing social on the token group, match/apply-release unmounted"
requirement: API-03
verification:
- kind: integration
ref: "fonoteka.go/plugins/golem15/fonoteka/wishlist_reservations_test.go#TestWishlistOverlapRoutesAssembled"
status: pass
- kind: unit
ref: "fonoteka.go/plugins/golem15/fonoteka/phase08_coverage_test.go#TestOAuthTokenSurfaceIsolationCoverage"
status: pass
- kind: integration
ref: "fonoteka.go/parity/parity_test.go#TestParityCorpus/coverage (143 ported, 143 passing)"
status: pass
human_judgment: false
- id: D9
description: "Recorded client journeys: nuxt-wishlist (database queue) with its digest-rows golden and one digest job per window, and mcp-wishlist with the pinned token; corpus secrets check green"
requirement: API-03
verification:
- kind: e2e
ref: "fonoteka.go/parity/fonoteka_flows_test.go#TestFonotekaNuxtFlows/nuxt-wishlist"
status: pass
- kind: e2e
ref: "fonoteka.go/parity/fonoteka_flows_test.go#TestFonotekaNuxtFlows/mcp-wishlist"
status: pass
- kind: other
ref: "go run ./parity/check_corpus.go --manifest parity/manifest.yaml --routes .../routes.php --require-recorded --check-secrets"
status: pass
human_judgment: false
duration: 51min
completed: 2026-10-03
status: complete
plan_head_before: ec054a2c8ee2d887dd4899bb07fb01cda720ce3d
plan_head_after: 37405f014878e249bd501ec93b1b7ffaa632772b
---
# Phase 13 Plan 03: Wishlist Summary
**All 30 Phase 13 wishlist routes now run in Go with PHP's bodies, error pages and reservation mask. That covers the own list, item CRUD, similar, share, settings, household, subscriptions, secret reservations, reveal, purchase and peer views. Item-added bells and a coalesced digest job hang off the album insert hook, and purchase mail goes through a River job that is sent only after commit. The corpus is at 143 ported, and both real clients' journeys and three publication goldens replay green.**
## Performance
- **Duration:** 51 min
- **Started:** 2026-10-03T05:31:59Z
- **Completed:** 2026-10-03T06:23:00Z
- **Tasks:** 4 of 4
- **Files modified:** 132 in fonoteka.go: 25 source/test files, 108 route fixtures and 7 flow, rows and golden files
## Accomplishments
- **Resolver and scopes.**
- `ActiveWishlist`/`ProvisionWishlist` lock the users row FOR UPDATE, reuse the own wishlist or create `Moja lista życzeń`. Concurrent first reads create one row.
- `WishlistsVisibleTo` and `HouseholdPeerIDs` derive peers as PHP does: owners and live editors of every collection the user belongs to.
- `ReservableWishlistIDs` adds the subscribed wishlists, re-checked as live wishlists.
- **Serializer.** `AlbumDTO.Reservation` uses `json:"reservation,omitempty"`. `SerializeAlbums` takes options:
- `WithReservation(rc)`: `stateForViewer`'s mask. The owner before reveal gets `reserved`, `is_mine` and `revealed` only.
- `WithoutArtists()`: the artists are `[]`, as in PHP's reserve and reveal bodies.
With no option, every existing album body is unchanged; the album broadcast goldens and corpus confirm it.
- **Item writes.** Store, update and destroy work on both groups through the Phase 12 write path:
- `condition` and `shelf` are `prohibited`. A2 is settled by recording: the message is `validation.prohibited`.
- The duplicate check runs against the real collection.
- `similar` uses `ILIKE ... ESCAPE '\'` with `%`, `_` and `\` escaped.
- **Item-added (D-08).** `albumAddedCallback` branches on the collection kind, and `NotifyWishlistItemAdded` uses `SubscribersOf`:
- persisted rows come first, then synthetic household peers with both channels on;
- the actor is skipped;
- ws subscribers get `wishlist_item_added` with `{album_id, album_name, collection_id, owner_name}`;
- email subscribers go through `EnqueueWishlistDigest` (the atomic upsert, with the dispatch on the first insert only).
- **Share, settings, household.**
- Share mirrors `collection/share` on the own wishlist (`/w/` paths).
- Settings validate before resolving, write the flag directly and report `followers_count`.
- Household groups `subscriptionsFor` with album counts and eight name-ordered previews.
- **Subscriptions.** `Subscribe` is an upsert that stamps `subscribed_at`. `Unsubscribe` is also ported. `SubscriptionStateFor` has `forced` for peers. Token routes use the new `ResolvePublic`; collection routes use `WishlistsVisibleTo`. Every miss is the Winter 404 page.
- **Reservations.**
- `ReserveAlbum` checks 422 own and 409 disabled, then locks the album row; an existing reservation gives the Winter 409 page.
- `CancelReservation` works for the reserver only.
- `RevealReservation` is one-way and writes a bell row only.
- `ReleaseForAlbum` is also ported.
- **Purchase (D-07).** `MoveToCollection` runs in one transaction:
1. the save under suppression, then one `updated` album event on the new collection channel;
2. the reservation release;
3. bells for every ws subscriber (the actor too) and for the reserver once;
4. `WishlistPurchasedMailArgs` enqueued on `mail` per email subscriber.
The worker sends `wishlist_item_purchased(-en)` by `preferred_locale` with `albumName` and `wishlistName`.
- **Peer views.** Index and show go through `ReservableWishlistIDs` with the viewer's mask, `is_owner` and `owner`. The four overlap pairs dispatch correctly through the assembled router.
- **Parity.**
- The `wishlist` seed state exists on both sides.
- 30 routes are re-recorded (108 fixtures), so 143 routes are ported.
- `nuxt-wishlist` was recorded with `QUEUE_CONNECTION=database`. Its rows golden is bob's digest at `item_count` 2, with one digest job.
- `mcp-wishlist` replays.
- The `wishlist-item-added`, `reservation-revealed` and `wishlist-purchased` goldens replay.
- The README documents the recipes.
## Task Commits
fonoteka.go (master, not pushed):
1. **Task 1: own wishlist read path and reservation mask** - `338f518` (feat)
2. **Task 2: item writes, item-added bells and digest, share/settings/household** - `ff38bad` (feat)
3. **Task 3: subscriptions, reservations, peer views, purchase and its mail** - `791daad` (feat)
4. **Task 4: nuxt-wishlist and mcp-wishlist flows, rows golden, publication goldens** - `37405f0` (test)
## Decisions Made
See `key-decisions` above. The user may want to confirm one of them: the purchase emits the album `updated` event before the bells. That matches the order PHP records under `sync`. In production the PHP broadcast is queued and would arrive after the bells. No client depends on the cross-channel order.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] Job dispatch inside the album insert hook inserted extra album rows**
- **Found during:** Task 2 (TestWishlistItemAddedOncePerPath)
- **Issue:** `cleanSession` (`Session{NewDB: true}`) keeps the callback's album statement until the first chained call. `conga.Dispatch` called `WithContext`, which clones that statement, and its `Create(&record)` re-ran the album insert. Each digest dispatch added two album rows.
- **Fix:** job queues get `jobDB(tx, ctx)` (`cleanSession(...).Scopes()`, a fresh statement). The latent hazard is logged in deferred-items.md.
- **Files modified:** classes/wishlist_notifications.go
- **Commit:** ff38bad
**2. [Rule 1 - Parity] A provisioned wishlist's settings answer `reservations_allowed: false`**
- **Found during:** Task 2 (GET wishlist/settings `provision` case)
- **Issue:** PHP serializes the unsaved model, whose flag was never set, while the row holds the default true.
- **Fix:** `ProvisionWishlist` returns the in-memory flag as false; later reads see true.
- **Commit:** ff38bad
**3. [Rule 1 - Test seed] New notifications sorted below the seeded one in Go**
- **Issue:** a Postgres sequence does not advance past an explicit id the way SQLite's AUTOINCREMENT does.
- **Fix:** `seedParityWishlist` lifts the notifications sequence.
- **Commit:** ff38bad
**4. [Rule 3 - Blocking] Phase 8 isolation subtests asserted the whole wishlist family absent**
- **Fix:** they now assert the real invariant through `assertWishlistSurfaces`:
- token-group wishlist routes are exactly the four CRUD routes, each with its one scope;
- every other wishlist route is JWT only;
- match and apply-release are absent;
- the share subtests allow `/wishlist/share`.
- **Commits:** 338f518, ff38bad
**5. [Rule 1 - Ordering] Purchase publications**
- **Found during:** Task 4 (the wishlist-purchased golden)
- **Issue:** PHP publishes the moved album's `updated` event inside the save, before the bells.
- **Fix:** `MoveToCollection` now emits it right after the save under suppression; it gained a bucket parameter, and the handler no longer emits separately.
- **Commit:** 37405f0
**6. [Rule 3 - Test harness] Broadcast goldens with several publications**
- **Issue 1:** small Go ids collided in the normaliser (a user id equal to a collection id).
- **Issue 2:** River delivers publications in no fixed order.
- **Issue 3:** seeding the shared database broke `TestOAuthFlows`, which runs later.
- **Fix:** the wishlist subtests run on a database of their own with lifted collection ids, and compare order-insensitively (`assertMatchesGoldenUnordered`).
- **Commit:** 37405f0
**7. [Rule 2 - Correctness] Path ids above the int4 range**
- **Fix:** the wishlist routes use `int4PathID`, so ids above the int4 range get PHP's 404 (an `out-of-range` case is recorded).
- **Commit:** 338f518
**8. [Scope] Small refactor outside the plan's file list**
- `albums_controller.go`'s `createAlbumWithCovers` now delegates to `createAlbumIn(collectionID)`, so the wishlist store reuses the exact write path.
- **Commit:** ff38bad
**9. [Recording] The wishlist share 422 case was not recorded**
- **Issue:** the `share:wishlist` capture rule on `PUT wishlist/share` needs a token in the response, so a 422 cannot be recorded.
- **Coverage instead:** the 422 path shares `collectionShareRules` with the collection share and is asserted in `TestWishlistShareSettingsHousehold/share`.
---
**Total deviations:** 9: 4 bugs or parity fixes, 2 blocking test inventories or harness issues, 1 correctness hardening, 1 refactor, 1 recording limitation. **Impact:** every response body and side effect matches PHP. The interface changes from the plan are:
- `MoveToCollection(ctx, tx, svc, bucket, album, target)`;
- serializer options rather than a `SerializeAlbum` parameter;
- `ReserveAlbum` takes the wishlist so it can perform the controller's 422/409 checks.
## Issues Encountered
- `TestPhase09SecurityRoutes` (pre-existing, Phase 12.2 cabana routes) still fails in the fonoteka plugin package; it is logged in deferred-items.md. Every other suite is green:
- the root module;
- parity (including `TestParityCorpus` 143/143, `TestFonotekaNuxtFlows`, `TestBroadcastGoldens`, `TestOAuthFlows`);
- the plugin's subpackages.
- The `FORCE_COLOR` shell variable was unset for test runs, as in 13-01/13-02.
- The isolated PHP server was started for recording (sync, then `QUEUE_CONNECTION=database`) and stopped before returning.
## Known Stubs
None. The digest job kind has no worker by design (D-04/D-08, Phase 14 JOBS-03). The match and apply-release routes stay pending (D-01).
## Threat Flags
None beyond the plan's register:
- **T-13-04, T-13-05:** TestReservationMask, TestReserveConcurrent and TestRevealIdempotent; no bell or publication names the reserver to the owner.
- **T-13-06:** scoped lookups with identical 404 pages.
- **T-13-07:** `assertWishlistSurfaces` plus the acceptance greps.
- **T-13-28:** the rollback cases in TestDigestCoalescing and TestPurchaseMailAfterCommit.
- **T-13-29:** ResolvePublic with disabled, regenerated, case-folded and malformed tokens tested.
- **T-13-30:** the purchase mail args carry an id and two names only, and the worker logs the subscriber id only.
## User Setup Required
None.
## Next Phase Readiness
- 13-05 can reuse `classes.ResolvePublic(ctx, db, token, "wishlist")` for `public-wishlist/{token}`. The `wishlist` seed state already holds a shared wishlist (`share:wishlist`) with three albums.
- 13-06 should add the wishlist write routes to `write_endpoints_fuzz_test.go` and the wishlist routes to the Phase 13 route-table test.
- Phase 14 consumes the queued `golem15.fonoteka.wishlist_digest` jobs (`fonoteka_wishlist_digest`) and the digest-queue rows.
## Self-Check: PASSED
- Created files exist: all 22 `key-files.created` paths checked with `test -f`.
- Commits exist in fonoteka.go: 338f518, ff38bad, 791daad, 37405f0.
- Plan verification:
- fonoteka.go `go vet ./...` (root, plugin, parity) is clean.
- `go test` is green for the root, parity and the plugin's subpackages; the plugin package is green except the pre-existing TestPhase09SecurityRoutes.
- The corpus has 143 routes ported and passing.
- `nuxt-wishlist`, `mcp-wishlist` and the three new broadcast goldens pass.
- `check_corpus --require-recorded --check-secrets` is green.

View File

@@ -13,3 +13,9 @@ Out-of-scope findings logged by plan executors. Not fixed by the plan that found
- **Path ids between 2^31 and 2^32 bind-fail into an opaque 500 on other routes.** `pathID` (`fonoteka.go/plugins/golem15/fonoteka/controllers/api/collections_controller.go:264`) parses up to uint32, but the id columns are Postgres `integer`, so for example `DELETE /household/invitations/3000000000` fails to encode the argument and answers the opaque 500 where PHP answers its 404. 13-02 guards only `MarkNotificationRead` (`id > math.MaxInt32` is not found). Fix: have `pathID` treat values above `math.MaxInt32` as 0 (no row), or guard each caller. - **Path ids between 2^31 and 2^32 bind-fail into an opaque 500 on other routes.** `pathID` (`fonoteka.go/plugins/golem15/fonoteka/controllers/api/collections_controller.go:264`) parses up to uint32, but the id columns are Postgres `integer`, so for example `DELETE /household/invitations/3000000000` fails to encode the argument and answers the opaque 500 where PHP answers its 404. 13-02 guards only `MarkNotificationRead` (`id > math.MaxInt32` is not found). Fix: have `pathID` treat values above `math.MaxInt32` as 0 (no row), or guard each caller.
- **A Phase 2 fixture of a still-pending route carries a masked provider key text.** `parity/fixtures/routes/POST___fonoteka_api_v1_ai-credential_test_jwt.yaml:21` holds OpenAI's error text with `sk-parit******real` (a fake, masked value). The route stays pending until Phase 14 (D-02), which re-records it. - **A Phase 2 fixture of a still-pending route carries a masked provider key text.** `parity/fixtures/routes/POST___fonoteka_api_v1_ai-credential_test_jwt.yaml:21` holds OpenAI's error text with `sk-parit******real` (a fake, masked value). The route stays pending until Phase 14 (D-02), which re-records it.
- **`TestPhase09SecurityRoutes` and the `household_smoke_test.go` gofmt drift** from 13-01's list are still open; 13-02 did not touch them. - **`TestPhase09SecurityRoutes` and the `household_smoke_test.go` gofmt drift** from 13-01's list are still open; 13-02 did not touch them.
## From 13-03
- **The general `pathID` range gap is still open.** The 30 wishlist routes use `int4PathID` (`controllers/api/wishlist_albums_controller.go`), which treats ids above `math.MaxInt32` as no row, so they answer PHP's 404. Other `pathID` routes are unchanged (see the 13-02 entry).
- **`cleanSession` handles carry the triggering write's statement until their first chained call.** `db.Session(&gorm.Session{NewDB: true})` keeps the callback's statement on the handle itself; only a chained call (`Where`, `Raw`, `Scopes`, ...) starts a fresh one, while `WithContext` clones the old one. 13-03 hit it when `conga.Dispatch` (which calls `WithContext` and then `Create`) received such a handle inside the album insert hook: every digest dispatch inserted two extra album rows. The wishlist code now hands job queues `jobDB` (`cleanSession(...).Scopes()`). `WriteNotification` passes the same kind of handle to `lighthouse.Service.Emit`; it works today because `Emit` does not create through it, but any future callee that does `WithContext(...).Create(...)` on a `cleanSession` handle has the same bug. Fix: make `cleanSession` return a chained (fresh-statement) handle.
- **TestPhase09SecurityRoutes** (12.2 cabana routes) and the `household_smoke_test.go` gofmt drift are still open.