# Phase 3: First vertical slice — genres end to end - Discussion Log > **Audit trail only.** Do not use as input to planning, research, or execution agents. > Decisions are captured in CONTEXT.md — this log preserves the alternatives considered. **Date:** 2026-09-17 **Phase:** 3-first-vertical-slice-genres-end-to-end **Areas discussed:** Handler fidelity, Throwaway JWT guard, Pipeline stage depth, Migrations and seed data --- ## Handler fidelity ### How faithful should the Go genres handler be to the PHP query? | Option | Description | Selected | |--------|-------------|----------| | Faithful query, minimal tables | Real user → active collection → accessible-album count join; only the columns the query touches; Phase 5 widens | ✓ | | Genres-only, count hardcoded 0 | Fastest green diff; handler rewritten in Phase 12; proves nothing about joins | | | Genres + albums count, no access scoping | Real COUNT join, skips user/active-collection resolution | | **User's choice:** Faithful query, minimal tables **Notes:** Scout found the fixture shows `album_count: 0` for every genre, so the diff cannot force this choice. ### How do we prove the non-zero count path? | Option | Description | Selected | |--------|-------------|----------| | Go integration test alongside parity | testcontainers test inserts albums for alice and a foreign collection; fixture untouched | ✓ | | Re-record a richer fixture from PHP | New manifest cases recorded after genre-tagged albums exist; reopens Phase 2 corpus | | | Both | Integration test plus a second recorded case | | **User's choice:** Go integration test alongside parity ### Where does active collection resolution live? | Option | Description | Selected | |--------|-------------|----------| | Minimal resolver in the fonoteka plugin | Small service porting the default path of ActiveCollectionResolver; Phase 12 extends | ✓ | | Org-context middleware sets it | Pipeline stage resolves via a plugin-supplied hook | | | You decide | Research compares against other controllers' context() use | | **User's choice:** Minimal resolver in the fonoteka plugin ### Is ?non_empty validation (422) and filtering part of this slice? | Option | Description | Selected | |--------|-------------|----------| | Port it, test it in Go only | Whole handler ported incl. 422 envelope and filter; no new fixtures | ✓ | | Skip until Phase 12 | Only the recorded 200 path, with a TODO | | | Port it and record fixtures | Add non_empty=1 and 422 cases to the manifest, re-record | | **User's choice:** Port it, test it in Go only ### PolishOrder collation on Postgres | Option | Description | Selected | |--------|-------------|----------| | Research picks, I confirm at plan review | Researcher compares query COLLATE, column collation, DB locale; lands as a lagoon helper | ✓ | | ICU collation in the query | Lock `COLLATE "pl-x-icu"` via helper now | | | Database-level Polish locale | Plain ORDER BY; deployment and testcontainers constraint | | **User's choice:** Research picks, I confirm at plan review --- ## Throwaway JWT guard ### What does the guard verify? | Option | Description | Selected | |--------|-------------|----------| | Real HS256 verify + user row lookup | Signature, pinned alg, exp, sub, user row into context; only issuing is throwaway | ✓ | | Static seeded-token table | Bearer string compared to a token list; no crypto | | | Real verify, no DB lookup | Identity from sub only | | **User's choice:** Real HS256 verify + user row lookup **Notes:** Scout: PHP `jwt.auth` is registered by the user plugin; HS256, `JWT_SECRET` from env. ### Which module owns the guard and minimal users table until Phase 7? | Option | Description | Selected | |--------|-------------|----------| | Stub user plugin in fonoteka.go | Real plugin with minimal User, migration, registers `jwt.auth`; fonoteka Requires() it | ✓ | | Framework bouncer owns verify, fonoteka plugin owns users | Less scaffolding now, a move later | | | Everything temporary inside the fonoteka plugin | One plugin, throwaway auth file | | **User's choice:** Stub user plugin in fonoteka.go ### How far does bouncer go? | Option | Description | Selected | |--------|-------------|----------| | Verifier + context accessors only | HS256 helper, user context key and accessors, UserProvider interface | ✓ | | Guard registry skeleton now | Three auth groups declared, jwt implemented | | | No bouncer yet | All auth code in the user plugin until Phase 6 | | **User's choice:** Verifier + context accessors only ### Which failure responses does the guard port now? | Option | Description | Selected | |--------|-------------|----------| | Port PHP's 401 bodies, Go-tested | Same status and JSON as JwtAuthenticate.php; no new fixtures | ✓ | | Add a recorded 401 genres case | Extend manifest, record from PHP | | | Plain 401, shape later | Exact bodies left to Phase 6/7 | | **User's choice:** Port PHP's 401 bodies, Go-tested --- ## Pipeline stage depth ### How real are the stages genres doesn't exercise? | Option | Description | Selected | |--------|-------------|----------| | Real slot, minimal behavior each | Named middleware in fixed order; CORS preflight from config, locale into context, org context pass-through, no-op limiter behind interface | ✓ | | Recover + auth real, the rest pass-through | Five named no-ops | | | All real now | Pulls most of Phase 6 forward | | **User's choice:** Real slot, minimal behavior each ### must-change-password (423) handling | Option | Description | Selected | |--------|-------------|----------| | Real check, owned by the plugin that owns it in PHP | fonoteka plugin registers `inv.must-change-password`, attached by name after `jwt.auth`; real 423 body | ✓ | | Named pass-through | Slot exists, always passes | | **User's choice:** Real check, owned by the plugin that owns it in PHP ### Route declaration API in surf | Option | Description | Selected | |--------|-------------|----------| | Laravel-like group builder over ServeMux | `r.Group(prefix, surf.Use(names...), func(g){ g.Get(...) })`; plain handlers | ✓ | | Bare ServeMux patterns + explicit wrap | Zero abstraction, repeated prefixes | | | Declarative route table | `[]surf.Route` structs, introspectable | | **User's choice:** Laravel-like group builder over ServeMux ### Typed params now? | Option | Description | Selected | |--------|-------------|----------| | Ship the helper, exercise it on a test route | Integer accessor with 404-on-malformed, tested in surf and examples/hello | ✓ | | Defer to the first route that needs it | Strictly interleaved; criterion 2 partly unmet | | **User's choice:** Ship the helper, exercise it on a test route --- ## Migrations and seed data ### Relation to PHP migration history | Option | Description | Selected | |--------|-------------|----------| | Squashed final-state migrations per table | One migration per table in final shape, comment naming folded PHP files | ✓ | | Mirror PHP one to one | One Go migration per PHP file incl. rename detour | | | Squash now, decide again in Phase 5 | Policy revisited later | | **User's choice:** Squashed final-state migrations per table ### Partial tables vs Phase 5 | Option | Description | Selected | |--------|-------------|----------| | Minimal now, Phase 5 adds ALTER migrations | Phase 3 migrations permanent, never edited | ✓ | | Rewrite in Phase 5 | Provisional migrations replaced pre-release | | | Full-shape tables now | Complete column set without Phase 5's security pass | | **User's choice:** Minimal now, Phase 5 adds ALTER migrations ### Source of the 15 seeded genres | Option | Description | Selected | |--------|-------------|----------| | A data migration, as in PHP | Idempotent by slug, PHP order, Rollback removes them | ✓ | | The parity seed hook inserts them | Test-only data | | | A db:seed style command | New framework concept | | **User's choice:** A data migration, as in PHP ### Temporary genres seed hook | Option | Description | Selected | |--------|-------------|----------| | Insert alice + collection, mint her JWT, set the colliding id vars | Fixture untouched; hook declared temporary in manifest | ✓ | | Same, but fix the scrubber collisions first | Tighten tide substitution, re-scrub 154 fixtures | | | You decide | Planner checks collision frequency | | **User's choice:** Insert alice + collection, mint her JWT, set the colliding id vars **Notes:** Scrubber fix recorded as a deferred idea. --- ## Claude's Discretion - Package internals of `surf`, `lagoon`, `bouncer`; capability interface names in `pact` - DB config keys and how the shared `*sql.DB` is published; River listener pool seam left for Phase 11 - Per-plugin gormigrate version-table naming - `serve` and `migrate:*` command set and graceful shutdown - Fail-boot behavior for unknown middleware names - Genre aggregate DTO; `[]` for empty lists - `fonoteka.go` workspace layout - JWT secret config key and fail-boot on empty secret (D-11, not asked; recorded as a security default) - Postgres Polish ordering: research proposes, user confirms at plan review ## Deferred Ideas - Tighten `tide` scrubber id-placeholder collisions and re-scrub the corpus - Recorded fixtures for non-zero counts, `non_empty=1`, 422 and 401 on genres - Active-collection resolution via the org-context stage (Phase 6/12) - Guard registry, rate-limit buckets, full CORS/locale (Phase 6) - Real token issuing in the user plugin (Phase 7) - River LISTEN/NOTIFY pgx pool (Phase 11) - Production data import (Phase 15)