Files
summercms/.planning/phases/12-p-ytarium-api-collections-and-albums/12-VERIFICATION.md

267 lines
24 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: 12-p-ytarium-api-collections-and-albums
verified: 2026-10-05T20:22:38Z
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:ad68223b4f2d8a3edc2a45610b63a44a2be73001d1ed8d509f5b280ac2d2e4ed"
covered_files_note: "Fingerprint refreshed 2026-10-05 at summercms.go HEAD e18885f. Content-hashed SPA bundles now modules/boardwalk/dist/assets/index-CqzF_Nki.css and index-z_LzMU8D.js. Later phases edited already-covered modules/ files (same digest-drift as Phase 9 Layer 2). Paths remain modules/. Status stays human_needed — this only clears the stale overlay so execute-phase is not re-invoked."
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:<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:
```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: "<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)_