Files
summercms/.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-VERIFICATION.md
2026-10-03 12:04:54 +02:00

252 lines
27 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 13-p-ytarium-api-wishlist-notifications-csv-credentials-public
verified: 2026-10-03T10:02:27Z
status: human_needed
score: 5/5 roadmap success criteria verified; plan truths 87/89 verified, 1 deviates from its literal text (D-03 queue names, routed to human for an override decision), 1 backstop truth holds by control flow with no dedicated race test (insufficient_spec, routed to human)
covered_files:
- ".planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-01-PLAN.md"
- ".planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-01-SUMMARY.md"
- ".planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-02-PLAN.md"
- ".planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-02-SUMMARY.md"
- ".planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-03-PLAN.md"
- ".planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-03-SUMMARY.md"
- ".planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-04-PLAN.md"
- ".planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-04-SUMMARY.md"
- ".planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-05-PLAN.md"
- ".planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-05-SUMMARY.md"
- ".planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-06-PLAN.md"
- ".planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-06-SUMMARY.md"
- "modules/conga/conga.go"
- "modules/lagoon/validate_rules.go"
- "modules/surf/overlap.go"
- "modules/surf/router.go"
- "modules/tide/centrifugo_golden.go"
- "modules/tide/diff.go"
- "scripts/check-phase13.sh"
covered_digest: "v2:sha256:78681e983350c9a3f226b9d36a65ce24aa1fc92a9387d72798ffc30d5aee688c"
covered_files_note: "verification.fingerprint refuses paths outside the summercms.go root, so the fonoteka.go and sm-user-plugin implementation files (the bulk of this phase) are not in the digest; their state at verification is fonoteka.go 40c575e (clean tree) and sm-user-plugin 8a65890 (clean tree)."
behavior_unverified: 0
overrides_applied: 0
mvp_mode_note: "ROADMAP marks Phase 13 mode: mvp, but the goal is not a User Story (user-story.validate: valid=false) and no 13-*-PLAN.md carries one. Following the Phase 1/3/5/8-12 precedent, the five ROADMAP success criteria are the contract, User Flow Coverage is derived from them, and plan must_haves are supporting evidence."
deferred:
- truth: "Wishlist match/apply-release routes, ai-credential/test and discogs-credential/test pass the parity diff"
addressed_in: "Phase 14"
evidence: "Phase 14 SC4: 'the wishlist wishlist/albums/{id}/match and apply-release routes, discogs-credential/test and the CSV row-edit Discogs pick (selected_discogs_id) pass the parity diff'; SC5: 'ai-credential/test passes the parity diff, and the AI resolver's admin tier uses the backend global vision model'"
- truth: "CSV import/match jobs and the wishlist digest job are worked (bodies, digest mail, queue-row delete)"
addressed_in: "Phase 14"
evidence: "Phase 14 SC1: 'The CSV import write job and the self-redispatching Discogs match job ... both complete correctly'; SC2: 'The wishlist digest job coalesces a 30-minute window and deletes its queue row on completion'"
- truth: "Notification pruning (fonoteka:prune-notifications)"
addressed_in: "Phase 14"
evidence: "Phase 14 SC6: 'the ported oauth-client/prune-notifications/reindex commands all run correctly'"
human_verification:
- test: "Decide on the D-03 queue-name deviation: the 13-01 truth pins queues fonoteka.csv.import, fonoteka.csv.match, fonoteka.wishlist.digest; the code (classes/job_contract.go:33-46) uses fonoteka_csv_import, fonoteka_csv_match, fonoteka_wishlist_digest because River rejects dotted queue names. STATE.md line 486 still records this as 'pending user confirmation'."
expected: "Accept an override (kinds, labels, args and delay are unchanged and are what Phase 14 workers and summer_jobs parity consume; the dotted names survive as summer_jobs labels), or ask for a different spelling before any production row carries these queues. D-03 is marked costly to reverse."
why_human: "D-03 is a signed-off, costly-to-reverse contract; only the user can accept a change to it."
- test: "Decide on the 13-01 backstop truth 'a Dispatch of an unregistered kind racing a worker start never routes through the worker client'"
expected: "Accept on the control-flow evidence: modules/conga/conga.go clientFor returns insertOnlyLocked() for every kind not in m.jobs, with no path to m.worker; TestUnregisteredKindWithWorker/WithoutWorker cover both states but no test races a worker start against a Dispatch. Or ask for a race test."
why_human: "The truth is tagged verification: backstop; code reading is not the explicit test evidence a backstop truth requires."
- test: "Read code-review warning WR-04 and decide whether 13-SECURITY-REVIEW.md T-13-03 and T-13-06 should record the residual risk"
expected: "A user who subscribed via wishlist/token/{token}/subscribe keeps peer-album read, reserve and notifications after the owner disables or regenerates the share (classes/wishlist_resolver.go ReservableWishlistIDs). This matches PHP AlbumReservationService::reservableWishlistIds, so parity holds; the security review currently says 'None known'. Either amend the residual-risk cells or open a product decision for revocation (a PHP behaviour change)."
why_human: "Product/security judgment; changing behaviour would break the parity rule and needs a decision note."
- test: "Triage code-review warnings WR-01, WR-02, WR-03 (13-REVIEW-DISPOSITION.md: all still 'open')"
expected: "Each is fixed, deferred with a reason, or skipped. None blocks a Phase 13 success criterion: WR-01 is latent (no catch-all under the family prefix today; the app boots and routes dispatch), WR-02 is a mapping/row/cancel-vs-commit race PHP shares (commit-vs-commit CAS holds, TestCsvCommitCAS), WR-03 is a double-click purchase double-notify. WR-02 matters before Phase 14 starts CSV workers."
why_human: "Disposition of review findings is a human decision; the review disposition file records none."
- test: "Push sm-user-plugin master (5 commits ahead of origin/master, including d80d799 RegisterEvent.Payload and 8a65890) to its origin"
expected: "origin/master contains 8a65890, so a fresh clone of fonoteka.go can check out its submodule pointer"
why_human: "Pushing a core-plugin repo is a user action; executors were told not to push."
---
# Phase 13: Płytarium API — wishlist, notifications, CSV, credentials, public routes Verification Report
**Phase Goal:** The remaining core API surface — wishlist, notifications, CSV import/export, per-user/org credentials, and onboarding/public/invitation routes — is ported with byte-compatible shapes and their own public rate-limit buckets.
**Verified:** 2026-10-03T10:02:27Z
**Status:** human_needed
**Re-verification:** No — initial verification
**MVP note:** ROADMAP marks this phase `mode: mvp`, but the goal is not a User Story and no plan carries one. As in Phases 1, 3, 5 and 8 to 12, the five ROADMAP success criteria are the contract.
## User Flow Coverage
| Step (from SC) | Expected | Evidence in codebase | Status |
| --- | --- | --- | --- |
| Nuxt user curates, shares, subscribes to, reserves, reveals and purchases wishlist items | PHP bodies, bell rows, digest-queue rows, publications | 30 wishlist routes (fonoteka.go `routes.go` 137-179, 283-287); `TestFonotekaNuxtFlows/nuxt-wishlist` and `/mcp-wishlist` PASS (verifier run); `TestBroadcastGoldens/{wishlist-item-added,reservation-revealed,wishlist-purchased}` in the gate | VERIFIED |
| User reads the bell list, unread count, marks one or all read | PHP bodies; Winter 404 page for foreign ids | `routes.go` 123-126; `notifications_controller.go`; recorded 404 fixtures are Winter HTML; `TestNotificationsRoutes` PASS | VERIFIED |
| User uploads a CSV, polls, remaps, edits rows, commits, cancels; exports on both groups | PHP bodies, queued job rows equal to PHP's | `routes.go` 54-67, 294; `TestFonotekaNuxtFlows/nuxt-csv` (job rows diffed against `nuxt-csv.rows.json`) PASS | VERIFIED |
| User stores personal/org AI and Discogs credentials | Encrypted at rest, PHP bodies, resolver order | `routes.go` 198-204; `lagoon.Encrypted` model fields with `json:"-"`; `TestCredentialsCRUD`, `TestCredentialSecretsNeverSerialized`, `TestResolveAIConfigPrecedence`, `TestDiscogsSharedMirror` PASS | VERIFIED |
| Anonymous visitor opens onboarding, an invitation, a shared collection or wishlist | No auth, PHP headers, own throttles and pubfail lockout | `routes.go` 306-334; `TestFonotekaNuxtFlows/{onboarding,public-anonymous,public-pubfail}` PASS; `TestPublicBucketsPerRoute` PASS | VERIFIED |
## Goal Achievement
### Roadmap Success Criteria (the contract)
| # | Success criterion | Status | Evidence |
| --- | --- | --- | --- |
| SC1 | Wishlist items, subscriptions, `public-wishlist/{token}` views, reservations (reserve/reveal) and purchase/digest triggers pass the parity diff; match/apply-release are Phase 14; the digest trigger queues its job here | ✓ VERIFIED | All 28 JWT and 4 token-group wishlist routes are mounted (`routes.go` 137-179, 283-287) and `status: ported` with recorded cases (reserve 5 cases incl. 422/409/409/404; reveal 4; purchase 3; token subscribe GET 5). The 3 `public-wishlist` routes are ported (`routes.go` 332-334). Purchase enqueues `WishlistPurchasedMailArgs` in the transaction (`TestPurchaseMailAfterCommit`, `TestPurchaseSideEffects` PASS). Item-added runs from `albumAddedCallback` and upserts `wishlist_digest_queue` with `ON CONFLICT`, dispatching `WishlistDigestArgs` (1800 s) only on first insert (`TestDigestCoalescing` PASS). The verifier ran the `nuxt-wishlist` and `mcp-wishlist` flows: PASS. `wishlist/albums/{id}/match` and `apply-release` are `pending` in the manifest and absent from the router (`TestRouteTablePhase13/phase14-routes-absent`). |
| SC2 | Notifications list, unread count and mark-read (one and all) pass the parity diff; pruning is a Phase 14 command | ✓ VERIFIED | 4 routes (`routes.go` 123-126), all ported, with recorded cases (`{id}/read` 200, 200, 404, 404; the 404s are the Winter production HTML page). `TestNotificationsRoutes` PASS (50-row cap, empty list, foreign id). No prune route exists. |
| SC3 | CSV import as a multi-step session (store, show/poll, mapping patch, per-row edit, commit, cancel) and CSV export on both groups pass the parity diff; commit and mapping enqueue jobs whose bodies are Phase 14 | ✓ VERIFIED | 6 import routes plus export on JWT and token groups (`routes.go` 54-67, 294), all ported (for example store 9 cases, show 11, rows 11, commit 11). `csv_import_service.go` dispatches `CsvMatchArgs` (line 571) and `CsvImportArgs` (line 860) through conga with their labels. Commit is an `UPDATE ... WHERE status='preview'` compare-and-swap (`TestCsvCommitCAS` PASS). `jobs.go` registers only the invitation and purchase-mail workers, so the CSV jobs stay queued (`TestCsvJobRows` PASS). The `selected_discogs_id` branch answers `discogs_unavailable` through the seam (`TestCsvRowPickSeam` PASS). The export fixture body (BOM, HEADERS, formula guard, fputcsv quoting) replays green. The verifier ran `nuxt-csv` (job rows diffed against PHP's `golem15_apparatus_jobs` dump): PASS. |
| SC4 | Per-user and per-org Discogs/AI credentials CRUD store encrypted values, honor the org-lock flag, and resolve correctly; the `/test` routes are Phase 14 | ✓ VERIFIED | The 7 CRUD routes (`routes.go` 198-204) are ported (POST ai-credential 7 cases, POST discogs-credential 10, POST org-ai-credential 6). Secrets are `lagoon.Encrypted` with `json:"-"` (`models/*_credential.go`), written only via `lagoon.NewEncrypted` (`credential_write_service.go` 98, 130, 225). `ResolveAIConfig` matches PHP `AiConfigResolver::resolve` tier by tier: admin, then org-lock for org members, then user, then org, else `ErrNoAICredential`. `ResolveDiscogsConfig` (`gates.go` 136) follows admin/env, then user, then org. `AIAllowed` applies the org-lock on `me/context`. `TestResolveAIConfigPrecedence`, `TestCredentialsCRUD`, `TestCredentialSecretsNeverSerialized` and `TestDiscogsSharedMirror` PASS. The `/test` routes are pending and absent from the router. Info: `ResolveAIConfig` has no route caller yet. In PHP its only caller is `RecognizeApiController` (Phase 14), so this matches. |
| SC5 | Onboarding, public and invitation-inspection routes, including `public/{token}`, `.../albums`, `.../albums/{id}`, are reachable without auth and enforce their own public rate-limit buckets | ✓ VERIFIED | The onboarding group (`routes.go` 308) and invitation inspection (315) carry only `throttle:10,1`, with no auth. The public group (325-334) carries `public.share-headers` and nothing else. The resolve routes carry `throttle:10,1`; the four albums routes carry `throttle:fonoteka-public-token` (60/min, `pubtok:<token>`) and then `throttle:fonoteka-public-ip` (120/min, client IP), exactly as in routes.php 399-428 and Plugin.php `RateLimiter::for`. The middleware sets `X-Robots-Tag` and `Cache-Control: no-store, private` and rewrites 429 bodies to JSON, as the PHP `PublicShareHeaders` does. The `pubfail:<ip>` counter is shared by all 6 routes. `TestPublicBucketsPerRoute`, `TestPublicResolve`, `TestPubfailCounter` (including goroutine concurrency), `TestPublicAlbumFieldSet` and the flows `public-anonymous`, `public-pubfail` (10x 404 then 429) and `onboarding` PASS in the verifier run. |
**Score:** 5/5 roadmap success criteria verified (0 present, behavior-unverified).
### Plan must-haves (supporting)
89 plan truths across 13-01 to 13-06. The gate's `--named` stage ran each by exact name with skips refused, and `-race` was on for the fonoteka package. The verifier re-ran 22 of the load-bearing named tests plus the parity corpus and every Nuxt flow:
| Group | Status | Notes |
| --- | --- | --- |
| 13-01 framework (surf overlap, conga workerless kinds, lagoon `prohibited`, tide masks, parity scaffolding, planning rewording) | 15/17 VERIFIED, 1 deviation, 1 insufficient_spec | **Deviation:** the job-contract truth names dotted queues, but the code uses underscores because River refuses dots. Kinds, labels, args and delay match. Routed to human with an override suggestion. **insufficient_spec:** the backstop race truth holds by control flow (`clientFor` has no worker path for unregistered kinds), but no race test exists. Routed to human. |
| 13-02 notifications, credentials, onboarding, invitation inspect, sm-user-plugin Payload and exports | 18/18 VERIFIED | `TestBootstrapConcurrent`, `TestRegisterInvitationListener` and `TestRegisterEventPayload` pass (gate). The sm-user-plugin diff `c258e9f..HEAD` is additive: `events.go` gains only `Payload`, no exported func is removed, and `/register` replays in `TestUserAPINuxtFlows`. The backstop truth on org-less provisioning is covered by `credentials_smoke_test.go:186-198`. |
| 13-03 wishlist | 18/18 VERIFIED | The backstop truth on concurrent first-read provisioning is covered by `wishlist_smoke_test.go` subtest `concurrent-first-reads`. |
| 13-04 CSV | 15/15 VERIFIED | The backstop truth on export ordering and header columns is proven by the parity replay of the PHP-recorded export bodies (`GET___fonoteka_api_v1_export_csv_jwt*.yaml`). |
| 13-05 public | 11/11 VERIFIED | The backstop truth on concurrent pubfail is covered by the goroutine block in `TestPubfailCounter`. |
| 13-06 tests, gate, sign-off | 10/10 VERIFIED | `FuzzWriteEndpoints` and `TestPhase13Threats` pass under `-race` in the gate. Coverage floors are met (gate log). The `--removal` mutation evidence (RC rows) is the 13-06 executor's run; it is not part of `--all`, and the verifier did not re-run it because it edits tracked source. |
### Prohibitions (all `verification: test`)
Each of the 17 prohibitions has a wired, passing test, so none are flagged:
| Prohibition | Enforcing evidence |
| --- | --- |
| No Phase 13 job worked or faked (D-04), for CSV and digest | `TestJobContractDispatchWhileWorkerRuns`, `TestCsvJobRows`, `TestDigestCoalescing`; `jobs.go` registers no CSV or digest worker |
| Overlap dispatcher keeps every route in the table | `TestOverlappingConstrainedRoutes`, `TestRouteTablePhase13` |
| Framework code does not name the application | No new mentions in surf/conga/lagoon/tide or the 4 docs pages. The existing mentions in `lagoon/*_test.go` and `tide/capture_test.go` come from Phase 10.2. `TestDocsTree` PASS |
| Payload carries no password | `events.go` doc and `TestRegisterEventPayload` |
| sm-user-plugin additive only | diff review above, plus `TestUserAPINuxtFlows` |
| No secret in a response, log, job arg or fixture | `TestCredentialSecretsNeverSerialized`; `check_corpus --check-secrets` (gate) |
| `/test`, match and apply-release routes not mounted | `TestRouteTablePhase13/phase14-routes-absent`; manifest `pending` |
| Owner cannot learn the reserver before reveal | `TestReservationMask`, `TestPhase13ReservationMask` |
| No mail from a rolled-back purchase | `TestPurchaseMailAfterCommit` |
| `selected_discogs_id` never fabricates | `TestCsvRowPickSeam` |
| Uploaded CSV is not publicly served | The bucket is `file://./storage/app` under the `fonoteka-csv/` prefix, and the static uploads root is `storage/app/uploads/public` (`config/storage.yaml`). Also covered by `TestPhase13Threats` T-13-1x |
| Public view exposes no private fields | `TestPublicAlbumFieldSet` (12-key allow-list) |
| Disabled, regenerated or wrong-kind token does not resolve | `TestPublicResolve`, `TestPhase13Threats/T-13-03`, the `public-anonymous` regenerate step |
| No threat marked mitigated without a removal-checked test; pending routes never count as passing | gate `--evidence`, `corpus_coverage` (passing 157 + pending 14 = 171) |
### Deferred Items
| # | Item | Addressed In | Evidence |
| --- | --- | --- | --- |
| 1 | Wishlist match/apply-release, `ai-credential/test`, `discogs-credential/test`, CSV `selected_discogs_id` success | Phase 14 | SC4, SC5 |
| 2 | CSV import/match and wishlist digest job bodies | Phase 14 | SC1, SC2 (JOBS-02, JOBS-03) |
| 3 | `fonoteka:prune-notifications` | Phase 14 | SC6 |
### Required Artifacts
| Artifact | Status | Details |
| --- | --- | --- |
| `summercms.go/modules/surf/overlap.go` | ✓ VERIFIED | Contains `SetPathValue`, wired from `router.go` compile, and the fonoteka overlap pairs dispatch (`TestWishlistOverlapRoutesAssembled`) |
| `summercms.go/modules/conga/conga.go` | ✓ VERIFIED | `ErrUnregisteredKindQueue`, `clientFor`/`knownQueues` |
| `fonoteka.go/.../classes/job_contract.go` | ✓ VERIFIED (queue spelling deviation) | Kinds, labels and args are pinned by `TestJobContract`; queues use underscores |
| `fonoteka.go/.../controllers/api/{notifications,credentials,wishlist_*,csv_*,public_share,onboarding}_controller.go` | ✓ VERIFIED | Substantive (75 to 395 lines each), routed in `routes.go`, and exercised by the parity replay |
| `fonoteka.go/.../classes/{ai_config_resolver,wishlist_resolver,reservations,wishlist_notifications,csv_import_service,public_share}.go`, `classes/csv/*` | ✓ VERIFIED | Wired from the handlers. `ResolveAIConfig` has no Phase 13 caller; it matches PHP, where the only caller is recognize (Phase 14) |
| `fonoteka.go/plugins/golem15/user/classes/events.go` | ✓ VERIFIED | `Payload map[string]any`; listener wired in `plugin.go` |
| `fonoteka.go/parity/fixtures/nuxt/{nuxt-wishlist,nuxt-csv,onboarding,public-anonymous,public-pubfail}.yaml`, `mcp/mcp-wishlist.yaml`, `*.rows.json` | ✓ VERIFIED | Present, replayed green. Fixtures carry Winter HTML 404 pages and Laravel header shapes, which points to PHP recording |
| `summercms.go/scripts/check-phase13.sh` | ✓ VERIFIED | `EXPECTED_PORTED=157`, fail-closed detectors; the orchestrator's `--all` run exited 0 |
### Key Link Verification
| From | To | Via | Status |
| --- | --- | --- | --- |
| `routes.go` | surf overlap families | 4 PHP wishlist overlap pairs on one router | WIRED (`TestWishlistOverlapRoutesAssembled` PASS) |
| `notification_service.go` `albumAddedCallback` | `wishlist_notifications.go` | `NotifyWishlistItemAdded` on the insert tx | WIRED (`TestWishlistItemAddedOncePerPath` in gate) |
| `wishlist_notifications.go` / `csv_import_service.go` | `job_contract.go` | conga `Dispatch` with labels and queues | WIRED |
| `plugin.go` | sm-user-plugin `RegisterEvent` | `HandleRegisterEvent` listener | WIRED (`TestRegisterInvitationListener`, onboarding flow) |
| `public_share_controller.go` | `share_service.go` `ResolvePublic` | after the pubfail check | WIRED |
| `routes.go` public albums routes | `plugin.go` `Buckets()` | `throttle:fonoteka-public-token`, `throttle:fonoteka-public-ip` | WIRED (`TestPublicBucketsPerRoute`) |
| credential write service | `lagoon.NewEncrypted` | api_key and token | WIRED |
### Behavioral Spot-Checks (verifier's own runs; TMPDIR/GOTMPDIR on the home disk, FORCE_COLOR unset)
| Behavior | Command | Result | Status |
| --- | --- | --- | --- |
| Parity corpus replays against Go | `go test -count=1 ./parity -run '^(TestFonotekaNuxtFlows\|TestParityCorpus)$'` | `recorded 171/171 passing 157 failing 0 unrecorded 0 pending 14`; ok 44.4s | ✓ PASS |
| All 8 Nuxt flows | same run, `-v` | nuxt-collections, nuxt-albums, onboarding, nuxt-wishlist, mcp-wishlist, nuxt-csv, public-anonymous, public-pubfail all PASS | ✓ PASS |
| 22 load-bearing named tests | `go test -count=1 ./plugins/golem15/fonoteka -run '^(TestRouteTablePhase13\|...\|TestPhase13Threats)$'` | all 22 PASS, ok 20.1s | ✓ PASS |
| Pending routes are only Phase 14 or out of scope | manifest parse | 14 pending: 4 Phase 13 to 14 (D-01, D-02) plus 10 album/oauth/recognize routes outside this phase | ✓ PASS |
| Full gate | orchestrator `scripts/check-phase13.sh --all` | exit 0 (`gate-all.log`) | ✓ PASS (orchestrator evidence) |
### Probe Execution
Step 7c: no `scripts/*/tests/probe-*.sh` is declared by the phase. The phase gate `scripts/check-phase13.sh --all` is the runnable check (orchestrator run, exit 0). `--removal` was not re-run because it mutates tracked source.
### Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
| --- | --- | --- | --- | --- |
| API-03 | 13-01, 13-03, 13-05, 13-06 | Wishlist items, subscriptions, public-wishlist, reservations, purchase and digest triggers | ✓ SATISFIED | SC1 |
| API-04 | 13-01, 13-02, 13-06 | Notifications list, unread, mark read | ✓ SATISFIED | SC2 |
| API-05 | 13-01, 13-04, 13-06 | CSV session and export on both groups | ✓ SATISFIED | SC3 |
| API-06 | 13-01, 13-02, 13-06 | Credentials CRUD, encryption, org-lock, resolution | ✓ SATISFIED | SC4 |
| API-07 | 13-01, 13-02, 13-05, 13-06 | Onboarding, public, invitation inspection with public buckets | ✓ SATISFIED | SC5 |
All 5 IDs appear in plan frontmatter and are marked Complete in REQUIREMENTS.md (lines 78-82, 217-221). No orphaned requirement maps to Phase 13.
### Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
| --- | --- | --- | --- | --- |
| `fonoteka.go/.../classes/csv_import_service.go` | 519-587, 639-705, 889-901 | Status check before an unlocked write (WR-02) | ⚠️ Warning | A mapping, row or cancel write can race a commit and leave a second import job queued. PHP has the same race. It matters once Phase 14 starts the workers |
| `fonoteka.go/.../controllers/api/wishlist_albums_controller.go` | 366-383 | Purchase re-check outside the tx (WR-03) | ⚠️ Warning | A double submit sends duplicate bell rows and mail jobs |
| `summercms.go/modules/surf/overlap.go` | 224-288 | Method-less family pattern (WR-01) | ⚠️ Warning | Latent boot failure when a family sits beside a method-specific catch-all; does not affect fonoteka today |
| `13-SECURITY-REVIEW.md` | T-13-03, T-13-06 rows | Residual risk "None known" (WR-04) | ⚠️ Warning | Under-states the PHP-parity subscriber-retains-access behaviour |
| `classes/job_contract.go` | 33-46 | D-03 queue spelling differs from the signed-off text | ℹ️ Info | Forced by River; pending user confirmation per STATE.md:486 |
| debt markers | none | No TBD, FIXME or XXX in Phase 13 files | none | none |
### Human Verification Required
#### 1. D-03 queue-name deviation
**Test:** Compare `classes/job_contract.go` queues (`fonoteka_csv_import`, `fonoteka_csv_match`, `fonoteka_wishlist_digest`) with D-03's dotted names.
**Expected:** Accept an override, or choose another River-valid spelling, before production rows carry them.
**Why human:** D-03 is a costly, signed-off contract, and STATE.md still lists it as pending user confirmation.
Suggested override if accepted:
```yaml
overrides:
- must_have: "the job contract lives in one file, classes/job_contract.go, and TestJobContract pins every string: ... queues fonoteka.csv.import, fonoteka.csv.match, fonoteka.wishlist.digest, mail ..."
reason: "River rejects dotted queue names; the dotted names remain the summer_jobs labels (PHP parity), kinds/args/delay unchanged; queues spelled with underscores"
accepted_by: "<name>"
accepted_at: "<ISO timestamp>"
```
#### 2. Backstop truth: unregistered-kind dispatch racing a worker start
**Test:** Read `modules/conga/conga.go` `clientFor`.
**Expected:** Every unregistered kind reaches `insertOnlyLocked()`, and no branch returns `m.worker`. Accept, or ask for a race test.
**Why human:** A backstop truth requires explicit test evidence; this one only has control-flow evidence.
#### 3. WR-04 residual risk
**Test:** Read 13-REVIEW.md WR-04 next to 13-SECURITY-REVIEW.md T-13-03 and T-13-06.
**Expected:** Amend the residual-risk cells, or record a product decision on revoking token subscribers.
**Why human:** This is a security and product judgment, and any change departs from PHP parity.
#### 4. Review findings disposition
**Test:** Set a disposition for WR-01, WR-02 and WR-03 (and the IN items) in 13-REVIEW-DISPOSITION.md.
**Expected:** Each is fixed, deferred with a reason, or skipped. WR-02 should be resolved before the Phase 14 CSV workers.
**Why human:** Triage decision.
#### 5. Push sm-user-plugin
**Test:** `git -C fonoteka.go/plugins/golem15/user push` (5 commits ahead of origin/master).
**Expected:** origin contains 8a65890.
**Why human:** Pushing a core-plugin repo is a user action.
### Gaps Summary
No blocking gaps. All five roadmap success criteria hold in the code and in the verifier's own runs:
- 58 Phase 13 routes are mounted and ported.
- The parity corpus passes 157 of 171 recorded routes with 0 failing; the 14 pending are the 4 deliberate Phase 14 routes plus 10 routes outside this phase.
- Every Phase 13 Nuxt and MCP flow replays green.
- Credentials are encrypted, and the resolvers follow PHP tier by tier.
- The public surface carries PHP's exact throttle buckets, headers and pubfail lockout.
The four review warnings do not defeat a success criterion:
- WR-01 is latent.
- WR-02 and WR-03 are concurrency edges outside the recorded contract; WR-02 is shared with PHP.
- WR-04 matches PHP, so the issue is a documentation gap.
Status is `human_needed` for five reasons: the signed-off D-03 queue spelling still needs the user's confirmation, one backstop truth has only control-flow evidence, the WR-04 residual risk needs a decision, the review findings have no disposition, and the sm-user-plugin commits are unpushed.
---
_Verified: 2026-10-03T10:02:27Z_
_Verifier: Claude (gsd-verifier)_