Files
summercms/.planning/phases/03-first-vertical-slice-genres-end-to-end/03-DISCUSSION-LOG.md
2026-09-17 17:47:07 +02:00

220 lines
9.4 KiB
Markdown

# 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)