From 79e2a43095615202cde30cee1ad8776b45c9d1fd Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Fri, 2 Oct 2026 17:08:58 +0200 Subject: [PATCH] test(12): persist human verification items as UAT --- .../12-UAT.md | 48 ++++ .../12-VERIFICATION.md | 266 ++++++++++++++++++ 2 files changed, 314 insertions(+) create mode 100644 .planning/phases/12-p-ytarium-api-collections-and-albums/12-UAT.md create mode 100644 .planning/phases/12-p-ytarium-api-collections-and-albums/12-VERIFICATION.md diff --git a/.planning/phases/12-p-ytarium-api-collections-and-albums/12-UAT.md b/.planning/phases/12-p-ytarium-api-collections-and-albums/12-UAT.md new file mode 100644 index 0000000..7fcf6cb --- /dev/null +++ b/.planning/phases/12-p-ytarium-api-collections-and-albums/12-UAT.md @@ -0,0 +1,48 @@ +--- +status: testing +phase: 12-p-ytarium-api-collections-and-albums +source: [12-VERIFICATION.md] +started: 2026-10-02T15:08:57Z +updated: 2026-10-02T15:08:57Z +--- + +## Current Test + +number: 1 +name: Decide on the 12-02 collection-name trimming override +expected: | + Accept an override: the recorded PHP fixture POST___fonoteka_api_v1_collections_jwt.yaml sends ' New Shelf ' and PHP answers 'New Shelf' (Winter trimStringAttributes); Go trims in Collection.BeforeSave and replays the fixture green. +awaiting: user response + +## Tests + +### 1. Decide on the 12-02 collection-name trimming override +expected: The plan truth "names stored byte-for-byte including spaces" was wrong about Winter; Go trims like PHP and the byte-compatible contract is met. +result: [pending] + +### 2. Read 12-SECURITY-REVIEW.md +expected: Every T-12 threat's disposition, test and residual risk reads as acceptable (threats_open: 0; T-12-13 accepted; RC-04 partial by design). Self-performed by the 12-05 executor. +result: [pending] + +### 3. Household invitation mail in pl and en in a real mail client +expected: Layout, copy and link match the PHP collection_invitation mail; footer reads '© Płytarium.' without the year (documented gap). +result: [pending] + +### 4. Read the reworded ROADMAP Phase 12/13/14 and REQUIREMENTS API-01/02/03/07, INTG-01 lines +expected: They state the Phase 12 boundary locked in 12-CONTEXT (reservations and public token views in Phase 13, cover-price in Phase 14). +result: [pending] + +### 5. Push sm-user-plugin master (c65ab3c, 8697007, c258e9f) +expected: origin/master contains c258e9f so a fresh fonoteka.go clone can check out submodule pointer f60c3af. +result: [pending] + +## Summary + +total: 5 +passed: 0 +issues: 0 +pending: 5 +skipped: 0 +blocked: 0 + +## Gaps diff --git a/.planning/phases/12-p-ytarium-api-collections-and-albums/12-VERIFICATION.md b/.planning/phases/12-p-ytarium-api-collections-and-albums/12-VERIFICATION.md new file mode 100644 index 0000000..9956e5f --- /dev/null +++ b/.planning/phases/12-p-ytarium-api-collections-and-albums/12-VERIFICATION.md @@ -0,0 +1,266 @@ +--- +phase: 12-p-ytarium-api-collections-and-albums +verified: 2026-10-02T15:06:59Z +status: human_needed +score: 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) +covered_files: + - ".planning/phases/12-p-ytarium-api-collections-and-albums/12-01-PLAN.md" + - ".planning/phases/12-p-ytarium-api-collections-and-albums/12-01-SUMMARY.md" + - ".planning/phases/12-p-ytarium-api-collections-and-albums/12-02-PLAN.md" + - ".planning/phases/12-p-ytarium-api-collections-and-albums/12-02-SUMMARY.md" + - ".planning/phases/12-p-ytarium-api-collections-and-albums/12-03-PLAN.md" + - ".planning/phases/12-p-ytarium-api-collections-and-albums/12-03-SUMMARY.md" + - ".planning/phases/12-p-ytarium-api-collections-and-albums/12-04-PLAN.md" + - ".planning/phases/12-p-ytarium-api-collections-and-albums/12-04-SUMMARY.md" + - ".planning/phases/12-p-ytarium-api-collections-and-albums/12-05-PLAN.md" + - ".planning/phases/12-p-ytarium-api-collections-and-albums/12-05-SUMMARY.md" + - "docs/database/attachments.md" + - "docs/database/casts-and-validation.md" + - "docs/services/localization.md" + - "docs/services/parity-testing.md" + - "docs/services/search.md" + - "docs/services/storage.md" + - "go.mod" + - "go.sum" + - "modules/beachcomber/README.md" + - "modules/beachcomber/engines.go" + - "modules/beachcomber/searchable.go" + - "modules/beachcomber/searchpage_test.go" + - "modules/beachcomber/typesense/engine.go" + - "modules/beachcomber/typesense/searchpage_test.go" + - "modules/lagoon/README.md" + - "modules/lagoon/attach/thumb.go" + - "modules/lagoon/attach/url_test.go" + - "modules/lagoon/validate.go" + - "modules/lagoon/validate_request.go" + - "modules/lagoon/validate_request_test.go" + - "modules/lagoon/validate_rules.go" + - "modules/lagoon/validate_rules_test.go" + - "modules/lagoon/validate_test.go" + - "modules/phrasebook/README.md" + - "modules/phrasebook/lang/en/validation.yaml" + - "modules/phrasebook/lang/pl/validation.yaml" + - "modules/tide/README.md" + - "modules/tide/centrifugo_golden.go" + - "modules/tide/diff.go" + - "modules/tide/fixture.go" + - "modules/tide/flow.go" + - "modules/tide/multipart.go" + - "modules/tide/multipart_test.go" + - "modules/tide/normalize.go" + - "modules/tide/normalize_upload_test.go" + - "modules/tide/record.go" + - "modules/tide/replay.go" + - "modules/tide/variables.go" + - "scripts/check-phase10.1.sh" + - "scripts/check-phase10.sh" + - "scripts/check-phase11.sh" + - "scripts/check-phase12.sh" +covered_digest: "v2:sha256:8d9eaccb232f77a03e44541c6a9896a3fabeab9aa468d1ec9010c4280d35ca49" +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 f60c3af and sm-user-plugin c258e9f." +behavior_unverified: 0 +overrides_applied: 0 +mvp_mode_note: "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." +human_verification: + - test: "Decide on the 12-02 backstop truth 'collection names are stored and returned byte-for-byte as sent, including leading and trailing spaces'" + expected: "Accept an override: the recorded PHP fixture POST___fonoteka_api_v1_collections_jwt.yaml sends ' New Shelf ' and PHP answers 'New Shelf' (Winter trimStringAttributes); Go trims in Collection.BeforeSave and replays the fixture green. The plan truth was wrong about Winter; the roadmap's byte-compatible contract is met." + why_human: "The truth is tagged verification: backstop and its literal text is false in the code; only a human can accept the deviation as an override." + - test: "Read 12-SECURITY-REVIEW.md" + expected: "Every T-12 threat's disposition, test and residual risk reads as acceptable (threats_open: 0; T-12-13 accepted; RC-04 is partial by design)" + why_human: "The review was self-performed by the 12-05 executor with no separate reviewer agent; 12-05-SUMMARY D7 asks for a human read." + - test: "Send a household invitation in pl and en (serve with the log or a real mailer) and open the mail in a client" + expected: "Layout, copy and link match the PHP collection_invitation mail; the footer reads '© Płytarium.' without the year (documented gap)" + why_human: "Tests assert recipient, subject and link only; visual fidelity of the HTML mail needs a human (12-03-SUMMARY D6)." + - test: "Read the reworded ROADMAP Phase 12/13/14 and REQUIREMENTS API-01/02/03/07, INTG-01 lines" + expected: "They state the Phase 12 boundary locked in 12-CONTEXT (reservations and public token views in Phase 13, cover-price in Phase 14)" + why_human: "Planning wording intent (12-01-SUMMARY D7)." + - test: "Push sm-user-plugin master (3 commits ahead: c65ab3c, 8697007, c258e9f) to its origin" + expected: "origin/master contains c258e9f, so a fresh clone of fonoteka.go can check out the submodule pointer f60c3af refers to" + why_human: "The executors were told not to push; pushing is a user action and a core-plugin repo change." +--- + +# 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:` | `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: + +```yaml +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: "" + accepted_at: "" +``` + +**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)_