27 KiB
phase, verified, status, score, covered_files, covered_digest, covered_files_note, behavior_unverified, overrides_applied, mvp_mode_note, deferred, human_verification
| phase | verified | status | score | covered_files | covered_digest | covered_files_note | behavior_unverified | overrides_applied | mvp_mode_note | deferred | human_verification | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 13-p-ytarium-api-wishlist-notifications-csv-credentials-public | 2026-10-03T10:02:27Z | human_needed | 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) |
|
v2:sha256:78681e983350c9a3f226b9d36a65ce24aa1fc92a9387d72798ffc30d5aee688c | 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). | 0 | 0 | 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. |
|
|
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:
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)