9.4 KiB
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 inpact - DB config keys and how the shared
*sql.DBis published; River listener pool seam left for Phase 11 - Per-plugin gormigrate version-table naming
serveandmigrate:*command set and graceful shutdown- Fail-boot behavior for unknown middleware names
- Genre aggregate DTO;
[]for empty lists fonoteka.goworkspace 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
tidescrubber 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)