24 KiB
phase, verified, status, score, covered_files, covered_digest, covered_files_note, behavior_unverified, overrides_applied, mvp_mode_note, human_verification
| phase | verified | status | score | covered_files | covered_digest | covered_files_note | behavior_unverified | overrides_applied | mvp_mode_note | human_verification | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 12-p-ytarium-api-collections-and-albums | 2026-10-02T15:06:59Z | human_needed | 5/5 roadmap success criteria verified; plan truths 67/68 verified, 1 backstop truth contradicted by the recorded PHP contract (routed to human for an override decision) |
|
v2:sha256:8d9eaccb232f77a03e44541c6a9896a3fabeab9aa468d1ec9010c4280d35ca49 | 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 f60c3af and sm-user-plugin c258e9f. | 0 | 0 | ROADMAP marks Phase 12 mode: mvp, but the goal is not a User Story and no 12-*-PLAN.md carries one. Following the Phase 1/3/5/8/9/10/11 precedent, the five ROADMAP success criteria are the contract, User Flow Coverage is derived from them, and plan must_haves are supporting evidence. |
|
Phase 12: Płytarium API — Collections and Albums Verification Report
Phase Goal: Collections and Albums endpoints are ported with byte-compatible request/response shapes, including active-context switching, editor invitations, ratings, manual cover URL and Discogs cover import, and search. Verified: 2026-10-02T15:06:59Z 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. Following the precedent of Phases 1, 3, 5, 8, 9, 10 and 11, the five ROADMAP success criteria are the contract.
User Flow Coverage
| Step (from SC) | Expected | Evidence in codebase | Status |
|---|---|---|---|
| Nuxt user manages collections, switches context, reads me/context and its channel | PHP bodies on both auth groups | 23 collections-area routes in fonoteka.go/plugins/golem15/fonoteka/routes.go lines 50-102, 153-168; manifest cases ported; TestFonotekaNuxtFlows/nuxt-collections PASS (verifier run) |
VERIFIED |
| Owner invites, member accepts, owner manages members | PHP bodies and DB effects | 7 household routes (routes.go 105-114); classes/invitation_service.go; parity cases ported; nuxt-collections flow covers invite/accept/remove |
VERIFIED |
| User creates, edits, rates, photographs, bulk-adds, syncs albums with covers | PHP bodies and broadcasts | 36 album/lookup routes (routes.go 45-47, 71-94, 170-195); TestFonotekaNuxtFlows/nuxt-albums and TestBroadcastGoldens/{created,updated,deleted,bulk} PASS |
VERIFIED |
| User searches albums; the index cannot leak | Items and total re-gated in SQL | classes/album_search.go 202-257; TestSearchLeak PASS (verifier run, also under -race) |
VERIFIED |
Goal Achievement
Roadmap Success Criteria (the contract)
| # | Success criterion | Status | Evidence |
|---|---|---|---|
| SC1 | Collections CRUD with photos and image, switch and me/context flags, opaque channel from GET realtime/channels, editor invitation/acceptance and members, owner-only collection/share show/update/regenerate pass the parity diff |
✓ VERIFIED | Every PHP route of this surface (routes.php 77, 94, 109-128, 262-268 and token twins 458-466) is mounted in Go and status: ported in parity/manifest.yaml with recorded cases (e.g. me/context 7, POST collections 7, POST household/invitations 8, accept 5). Verifier run of go test ./parity -run TestParityCorpus: "recorded 171/171 passing 99 failing 0 unrecorded 0 pending 72". check_corpus.go --manifest parity/manifest.yaml --require-recorded --check-secrets exit 0. me_context_controller.go struct carries exactly the ten recorded flags and no id or channel. Public token views are pending and owned by Phase 13 SC5. |
| SC2 | Albums CRUD, ratings, photo upload, manual cover URL and Discogs cover import on create/bulk (cover_urls), plus sync/stats/value/missing/bulk pass the parity diff |
✓ VERIFIED | All albums routes of routes.php 228-259 except match/apply/recognize/import (Phase 14) are ported (POST albums 17 cases, PUT 12, photos 9, rating 8, search 25, sync 5, missing 7, bulk 6). classes/cover_importer.go:94 uses fetchguard AllowHostsMode; classes/manual_cover_fetcher.go:119 uses PublicOnlyMode; store (albums_controller.go:445) and bulk (albums_bulk_controller.go) import after commit. Cover-price is pending and named in Phase 14 SC4; reservations in Phase 13 SC1. |
| SC3 | Album search treats Typesense as a pre-filter re-gated in SQL; the total is the re-gated SQL count over at most 1000 engine ids, proven by a security test | ✓ VERIFIED | album_search.go: page ids re-gated through ScopedAlbums (= AlbumsAccessibleBy + active collection, album_queries.go:15), total recounted through the same scope over fetchEngineKeys capped at searchMaxTotalResults = 1000, pages of 250. TestSearchLeak (stale-moved, mis-scoped, soft-deleted, removed-editor and race, token-pin, short-page, recount, cap, engine-error; both auth groups; page ids and meta.total/last_page compared exactly; search setting on) PASS in the verifier's run, also under -race. Query shape matches PHP query_by order and weights 10,10,5,5,3,1,3,3. |
| SC4 | Artists/genres/styles lookups pass the parity diff | ✓ VERIFIED | GET artists (5 jwt, 2 token cases), GET/POST styles, POST genres ported on both groups (routes.go 44-47, 191-195), all passing in the corpus run. |
| SC5 | Request-DTO fuzz over every write endpoint asserts unknown and server-owned keys are never persisted | ✓ VERIFIED | FuzzWriteEndpoints (write_endpoints_fuzz_test.go:878) enumerates write routes from surf.BuildRouter(...).Routes() (routes_table_phase12_test.go:171-175), fails if a write route lacks a spec or a spec matches no route, requires exactly 41, sends every server-owned key with hostile values and diffs Postgres snapshots; 41 committed seeds. Verifier run PASS (plain and -race). |
Score: 5/5 roadmap success criteria; plan truths 67/68 verified (0 present-but-behavior-unverified; 1 backstop truth routed to human).
Plan must_haves (supporting evidence)
Every package that holds a named test of 12-VALIDATION.md was run in full by the verifier: summercms.go lagoon, lagoon/attach, tide, beachcomber, beachcomber/typesense, phrasebook (all ok); fonoteka.go plugins/golem15/fonoteka/... and plugins/golem15/user/... (all ok); parity TestParityCorpus, TestFonotekaNuxtFlows, TestBroadcastGoldens, TestSchemaMatchesPHPSnapshot, TestUserAPINuxtFlows (all PASS).
| Plan | Truths | Verified | Notes |
|---|---|---|---|
| 12-01 framework gaps, user groups, wording | 14 | 14 | lagoon.ValidateRequest (validate_request.go), pl/en validation.yaml (pl lacks after_or_equal), attach.PublicURL/File.URL, webp import in thumb.go, tide.MultipartBoundary/Parts, beachcomber.PageSearcher/SearchPage/QueryByWeights (typesense sends query_by_weights), sm-user-plugin user_groups migration, UserGroupCodes/HasGroupCode, Groups json:"-"; user-api fixtures replay unchanged. ROADMAP/REQUIREMENTS wording present. |
| 12-02 active context, collections, share | 16 | 15 | Resolver with FOR UPDATE, AccessibleBy pin, one inv.scope per token route (TestRouteTablePhase12), per-album delete, Winter 404 page, Winter upload layout, share tokens from crypto/rand. Backstop truth "names stored byte-for-byte including leading/trailing spaces" is contradicted: Go trims (Collection.BeforeSave) because the recorded PHP answer trims (" New Shelf " to "New Shelf"). Routed to human for an override. |
| 12-03 household and invitations | 14 | 14 | Encrypted-token River mail in the write tx, sha256 at rest, accept under FOR UPDATE, notification emit, 409 guard, nuxt-collections flow. The planned 422 envelopes are, per recorded PHP, Winter 500 pages; Go reproduces the recording. |
| 12-04 albums, search, lookups | 14 | 14 | Write path under WithoutBroadcasting, one event per write, bulk summary, covers after commit, Scout-exact recount. Deviation: engine-path items are re-gated by access plus active collection only, not "plus filters"; PHP AlbumSearchService.php:210-217 applies only the extra scope and collection_id in the Scout query() closure, so the Go code is the PHP contract. |
| 12-05 security proof, coverage, gate | 10 | 10 | Leak test, route-table test, fuzz, 26 T-12 subtests, gate script. Coverage re-measured by the verifier with scripts/check-phase12.sh --coverage (exit 0): beachcomber 84.3, typesense 95.9, lagoon 84.4, attach 87.7, tide 81.4, fonoteka classes 83.6, controllers/api 81.7, user classes 88.7, user updates 86.2. Backstop truth (no data race under -race): verifier ran FuzzWriteEndpoints seeds, TestRatingUpsertConcurrent, TestConcurrentAcceptSingleEditor, TestResolveProvisionsOnce, TestBulkSingleSummaryEvent, TestSearchLeak with -race; all PASS, no race report. |
Prohibitions (all test-tier, all with wired enforcement)
| Prohibition | Enforcing test(s) | Status |
|---|---|---|
| User-groups change alters no user-plugin response | TestUserAPINuxtFlows, user-api parity routes (16 ported) | ✓ (verifier run PASS) |
| No invented validation message | TestValidateRulesMatchLaravel (162 recorded PHP cases) | ✓ (lagoon package PASS) |
| me/context returns no id or channel | TestMeContextFlags; struct inspected | ✓ |
| owner_name never exposes a full foreign email | collections_smoke_test.go (owner_name cases) | ✓ |
| Collection delete leaves no searchable album, every member keeps a context | TestCollectionDeleteRemovesAlbumsOneByOne | ✓ |
| Raw invitation token readable only in the mail | TestPhase12Threats/T-12-07, TestInvitationMailEnqueuedInTx, check_corpus 64-hex rule | ✓ |
| Accept/removal never deletes the user's collections | TestRemoveEditorRepairsContext, TestPhase12Threats | ✓ |
| Editor or token cannot manage household | TestPhase12Threats/T-12-31, T-12-04 | ✓ |
| Search never returns or counts an inaccessible album | TestSearchLeak | ✓ |
| Failed cover never loses the album | TestCoverImportAfterCommit | ✓ |
| album_added never reaches the actor or outsiders | TestAlbumAddedNotifiesHousehold | ✓ |
| Leak test cannot pass by disabling search | Code read: setSearchSetting(t, gdb, true), ids and totals compared exactly |
✓ |
| No threat marked mitigated without a removal-proven test | scripts/check-phase12.sh --removal (25 RC rows) |
✓ by script inspection and the orchestrator-reported run; not re-run by the verifier because it mutates tracked source files while a concurrent session works in the repo |
Required Artifacts
| Artifact | Status | Details |
|---|---|---|
modules/lagoon/validate_request.go |
✓ VERIFIED | func ValidateRequest( present; used by fonoteka controllers (validateInput) |
modules/phrasebook/lang/pl/validation.yaml |
✓ VERIFIED | Winter catalog port; loaded as lagoon::validation.* |
modules/lagoon/attach/thumb.go |
✓ VERIFIED | webp import, PublicURL, File.URL, broken-image fallback |
modules/tide/multipart.go |
✓ VERIFIED | Parts and fixed boundary; used by replay of 30+ multipart fixtures |
modules/beachcomber/searchable.go |
✓ VERIFIED | PageSearcher; called by album_search.go:225,268,277 |
sm-user-plugin updates/202610020001_create_user_groups.go, classes/user_groups.go |
✓ VERIFIED | Read by classes/gates.go:31 IsSiteAdmin |
fonoteka classes/active_collection.go, access.go, share_service.go, controllers/api/http_errors.go |
✓ VERIFIED | Wired from routes.go handlers |
fonoteka classes/invitation_service.go, jobs.go, notification_service.go |
✓ VERIFIED | Wired from household routes |
fonoteka classes/album_write_service.go, album_search.go, cover_importer.go, image_guard.go |
✓ VERIFIED | Wired from album handlers |
parity/fixtures/nuxt/nuxt-collections.yaml, nuxt-albums.yaml |
✓ VERIFIED | Replayed green |
search_leak_test.go, write_endpoints_fuzz_test.go, routes_table_phase12_test.go, phase12_security_test.go, scripts/check-phase12.sh |
✓ VERIFIED | Run by the verifier (coverage stage of the gate re-run) |
Key Link Verification
| From | To | Via | Status |
|---|---|---|---|
| routes.go | collections_controller.go | one handler value on both groups | WIRED |
| collections_controller.go | classes/access.go | Scopes(classes.AccessibleBy(userID, token)) |
WIRED |
| me_context_controller / gates.go | sm-user-plugin user_groups.go | userclasses.HasGroupCode(..., "admin") |
WIRED |
| album_search.go | beachcomber.SearchPage | page and recount | WIRED |
| album_search.go | AlbumsAccessibleBy | via ScopedAlbums for items and total |
WIRED |
| cover_importer.go | fetchguard | AllowHostsMode |
WIRED |
| manual_cover_fetcher.go | fetchguard | PublicOnlyMode |
WIRED |
| albums/bulk handlers | lighthouse | WithoutBroadcasting[models.Album] then one Emit |
WIRED |
| FuzzWriteEndpoints | surf router | surf.BuildRouter(...).Routes() |
WIRED |
Data-Flow Trace (Level 4)
| Artifact | Data | Source | Real data | Status |
|---|---|---|---|---|
| albums/search response | data, meta.total |
engine ids then Postgres ScopedAlbums Find/Count |
yes | ✓ FLOWING |
| me/context | ten flags | gates over users, groups, organisations, credentials | yes | ✓ FLOWING |
| realtime/channels | collection:<id> |
Resolve |
yes | ✓ FLOWING |
Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|---|---|---|---|
| Parity corpus, flows, goldens, schema, user API | go test ./parity -run '^(TestParityCorpus|TestFonotekaNuxtFlows|TestBroadcastGoldens|TestSchemaMatchesPHPSnapshot|TestUserAPINuxtFlows)$' -count=1 -v |
ok; 171/171 recorded, 99 passing, 0 failing; both flows and four goldens PASS | ✓ PASS |
| Corpus recorded and secret-free | go run ./parity/check_corpus.go --manifest parity/manifest.yaml --require-recorded --check-secrets |
recorded 171/171, exit 0 | ✓ PASS |
| Security tests | go test ./plugins/golem15/fonoteka -run '^(TestSearchLeak|TestRouteTablePhase12|FuzzWriteEndpoints|TestPhase12Threats)$' -count=1 -v |
all PASS, no SKIP | ✓ PASS |
| Concurrency under race detector | same package, six concurrency tests and fuzz seeds, -race |
all PASS | ✓ PASS |
| Framework packages | go test ./modules/lagoon/... ./modules/tide/... ./modules/beachcomber/... ./modules/phrasebook/... -cover |
all ok | ✓ PASS |
| App plugins | go test ./plugins/golem15/fonoteka/... ./plugins/golem15/user/... -cover |
all ok | ✓ PASS |
| Vet both repos, docs tree | go vet ./... (both), go test ./cmd/summer -run TestDocsTree |
clean, ok | ✓ PASS |
| Coverage floors | scripts/check-phase12.sh --coverage |
"phase12 coverage passed" | ✓ PASS |
Probe Execution
Step 7c: no scripts/*/tests/probe-*.sh exist and no plan declares one. The phase gate scripts/check-phase12.sh is the analogue; its --coverage stage was run by the verifier (exit 0) and --all/--removal passed in the orchestrator's run after 12-05.
Requirements Coverage
| Requirement | Source plans | Description | Status | Evidence |
|---|---|---|---|---|
| API-01 | 12-01, 12-02, 12-03, 12-05 | Collections CRUD, context switch (me/context flags plus channel), invitations and acceptance, owner-only share | ✓ SATISFIED | SC1 evidence; REQUIREMENTS.md marks Complete |
| API-02 | 12-01, 12-04, 12-05 | Albums CRUD, ratings, photo upload and manual cover URL, cover_urls import, lookups, search re-gated in items and total | ✓ SATISFIED | SC2-SC5 evidence; REQUIREMENTS.md marks Complete |
No orphaned requirements: REQUIREMENTS.md maps only API-01 and API-02 to Phase 12.
Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|---|---|---|---|---|
| (none) | — | No TBD/FIXME/XXX or TODO/placeholder in Phase 12 code; the only hits are detector regexes in scripts/check-phase11.sh:714 and check-phase12.sh:508,555 |
ℹ️ Info | none |
| sm-user-plugin submodule | — | 3 commits ahead of origin/master; fonoteka.go pointer f60c3af references unpushed c258e9f (fonoteka.go itself has no remote) |
⚠️ Warning | A fresh clone cannot check out the submodule until it is pushed; listed as a human item |
fonoteka.go/plugins/golem15/fonoteka/controllers/api/albums_bulk_controller.go |
128-160 | bulkRows comment says an object of rows keeps its key order, but sortedKeys sorts the keys (numeric, then string), while PHP array_values keeps insertion order |
ℹ️ Info | Only reachable when albums is a JSON object with out-of-order keys; Nuxt sends an array |
.planning/ROADMAP.md |
616 | Phase 12 section still says "Plans: 4/5 plans executed" while the phase list and progress table say 5/5 Complete | ℹ️ Info | Planning-doc consistency only; left untouched because another session is editing ROADMAP |
| 12-SECURITY-REVIEW.md RC-04 | — | Removing AlbumsAccessibleBy from ScopedAlbums fails only the token-pin and removed-editor cases (the active-collection filter and GORM's soft-delete filter still cover the others) |
ℹ️ Info | Documented by design; defence in depth holds |
Human Verification Required
1. Override decision on the 12-02 encoding truth
Test: Compare the 12-02 backstop truth "collection names are stored and returned byte-for-byte as sent ... including leading and trailing spaces" with parity/fixtures/routes/POST___fonoteka_api_v1_collections_jwt.yaml.
Expected: PHP answers "name":"New Shelf" for " New Shelf ", and Go matches it. If you accept, add to this file's frontmatter:
overrides:
- must_have: "Edge (API-01 encoding): collection names are stored and returned byte-for-byte as sent (Winter has no TrimStrings or ConvertEmptyStringsToNull), including leading and trailing spaces."
reason: "Winter Model::setAttribute trims string attributes ($trimStringAttributes); the recorded PHP response trims, and API parity is the acceptance test (CLAUDE.md rule 6)."
accepted_by: "<name>"
accepted_at: "<ISO timestamp>"
Why human: The literal truth is false in code; only a human can accept the deviation.
2. Security review read
Test: Read 12-SECURITY-REVIEW.md.
Expected: Dispositions, tests and residual risks are acceptable.
Why human: Self-performed by the executor.
3. Invitation mail rendering
Test: Send a pl and an en invitation and open both mails in a client. Expected: Matches the PHP mail; footer without year is acceptable. Why human: Visual fidelity.
4. Planning wording
Test: Read the reworded ROADMAP Phase 12-14 criteria and REQUIREMENTS API-01/02/03/07, INTG-01. Expected: They state the boundary locked in 12-CONTEXT. Why human: Intent.
5. Push sm-user-plugin
Test: git -C ../fonoteka.go/plugins/golem15/user push origin master.
Expected: origin holds c258e9f.
Why human: User action on a core-plugin repo.
Gaps Summary
No gaps. Every roadmap success criterion is backed by running code that the verifier exercised: all Phase 12 PHP routes on both auth groups are mounted and pass the recorded parity diff (99 ported, 0 failing, 171/171 recorded), both Nuxt flows and all four broadcast goldens replay, the search leak test asserts both items and totals against a poisoned engine, and the write fuzz enumerates all 41 write routes from the assembled router. Deferred surfaces (reservations, public token views, invitation preview, cover-price, match/recognize/import) are pending in the manifest and named in Phases 13 and 14. The status is human_needed because of one backstop truth whose literal text the recorded PHP contract contradicts, plus four human-judgment items carried over from the summaries.
Verified: 2026-10-02T15:06:59Z Verifier: Claude (gsd-verifier)