docs(03): capture phase context
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
# Phase 3: First vertical slice — genres end to end - Context
|
||||
|
||||
**Gathered:** 2026-09-17
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
Phase 3 makes one real route, `GET /_fonoteka/api/v1/genres`, pass the Phase 2 parity diff through every kernel layer at once: layered config, one shared `*sql.DB` (pgx stdlib) under GORM, per-plugin gormigrate sets with up and down, plugin route registration on `net/http` ServeMux, the seven-stage named middleware pipeline, a JWT guard, a GORM query and the JSON response. Requirements DATA-01, DATA-02, HTTP-01, HTTP-02, QA-04. Roadmap mode: mvp.
|
||||
|
||||
Two repos. `summercms.go` gains the framework packages the slice pulls in: `surf` (routing, named middleware, pipeline, typed params), `lagoon` (DB connection, migration runner, query helpers), `bouncer` (token verifier and auth context only), the `HasRoutes`/`HasMigrations`/`HasModels`/`HasMiddleware`-style capability interfaces in `pact`, and the runtime commands (`serve`, `migrate:*`) on the `bonfire` kernel. It still knows nothing about Płytarium. `fonoteka.go` becomes a real app: a `go.work` workspace with a stub `golem15.user` plugin, the first cut of the `golem15.fonoteka` plugin, the generated app binary, and the parity test wired to the real handler.
|
||||
|
||||
Not in this phase: any other route (including `POST genres`), register/login/refresh and token issuing (Phase 7), the guard registry and the personal-token and public auth groups, named rate-limit buckets and full CORS/locale behavior (Phase 6), the full column set, casts and mass-assignment discipline of the 25 models (Phase 5), River, Centrifugo, Typesense, OpenAPI generation. The JWT guard is on the request path, so the phase is security-load-bearing: the security-review agent runs on it even though token issuing is throwaway.
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Handler fidelity
|
||||
- **D-01:** The Go handler ports the real PHP query, not a shortcut: resolve the authenticated user and the active collection, build the accessible-albums subquery scoped to that collection, left-join the per-genre `COUNT(*)`, select `id`, `name`, `slug`, `COALESCE(album_count, 0)`, order by name with the Polish ordering, serialize as `{"data":[...]}`. The fixture shows `album_count: 0` everywhere, so the diff alone would accept a fake; the port is faithful anyway because proving GORM can express this join is the point of the slice.
|
||||
- **D-02:** Only the tables and columns that query touches exist in this phase: `golem15_fonoteka_genres` in full, and minimal `users`, collections and albums tables (ids, ownership and membership columns the access scope reads, `genre_id`, `collection_id`, the user's active-collection pointer, the must-change-password flag). Table names match PHP exactly. Phase 5 widens them (see D-16).
|
||||
- **D-03:** Active-collection resolution lives in the `golem15.fonoteka` plugin as a small `ActiveCollection` service porting only the default path of `ActiveCollectionResolver.php` (stored active collection, else the user's own). Handlers call it the way PHP controllers call `context()`. It is not a framework middleware; the pipeline's org-context stage stays a generic slot. Phase 12 extends the service with switching and editor membership.
|
||||
- **D-04:** The handler is ported whole: `?non_empty` is validated (`nullable`, in `0|1`), bad values return PHP's 422 envelope (`{"error":"Validation failed","errors":{...}}`), and `non_empty=1` filters to genres with a positive count. No new fixtures are recorded for these; they are covered by Go tests with the expected bodies read from the PHP source.
|
||||
- **D-05:** The non-zero count path and access scoping are proven by a testcontainers integration test in `fonoteka.go` alongside the parity test: insert albums for alice in her active collection, albums in a collection she cannot access, assert per-genre counts, `non_empty=1` filtering and that foreign albums are not counted. The parity fixture stays exactly as recorded; the PHP recorder is not re-run in this phase.
|
||||
- **D-06:** The Postgres equivalent of `PolishOrder` is a research item with a user confirmation at plan review. The researcher reads `PolishOrder.php` and compares an ICU `pl` collation applied in the query, a column-level collation, and a database-level locale (including what testcontainers must match). Whatever is chosen lands as a reusable `lagoon` helper called at the same call site as PHP's `PolishOrder::apply`, because every later list endpoint needs it.
|
||||
|
||||
### JWT guard
|
||||
- **D-07:** Verification is real; only issuing is throwaway. The guard validates HS256 with the algorithm pinned (no `alg` from the token header, `none` rejected), checks `exp` and `sub`, loads the user row by `sub` from the minimal `users` table, and places the user in the request context. A token for a missing user is rejected. Phase 7 replaces how tokens are minted and keeps this verifier.
|
||||
- **D-08:** A stub `golem15.user` plugin exists in `fonoteka.go` from this phase, mirroring PHP ownership: it owns the minimal `User` model and its migration and registers the named middleware `jwt.auth`. `golem15.fonoteka` declares `Requires()` on it and references `jwt.auth` by name. Phase 7 fills this plugin in rather than relocating code.
|
||||
- **D-09:** Framework `bouncer` ships only: the HS256 verification helper (golang-jwt/jwt v5), the authenticated-user context key with exported accessor functions (unexported struct key type, per Phase 1), and a small `UserProvider`-style interface the user plugin implements. No guard registry, no personal-token or public groups, no token issuing API: Phase 6 and Phase 7 own those.
|
||||
- **D-10:** Missing, malformed, expired, bad-signature and unknown-user tokens return the same status and JSON body `JwtAuthenticate.php` produces. These are asserted in Go tests against strings taken from the PHP source; no new fixtures are recorded.
|
||||
- **D-11:** (Not asked in discussion; recorded as a security default that follows from D-07 and Phase 1 D-10.) The JWT secret comes from config (a `golem15.user` namespace key, overridable by a `SUMMER_…` env var per Phase 1 D-06 and D-08). An empty or missing secret fails boot; it never falls back to a default. Tests use a fixed test-only secret.
|
||||
|
||||
### Middleware pipeline and routing API
|
||||
- **D-12:** All seven stages exist as real named middleware in the fixed order (recover, CORS, locale, auth group, must-change-password, org context, rate limit, handler), each with the smallest honest behavior: recover turns panics into a 500 without leaking internals; CORS answers preflight and sets headers from config; locale reads `Accept-Language` into context; org context is a pass-through slot; rate limit is a no-op limiter behind the interface Phase 6 will implement. Order and wiring are proven now; depth comes in Phase 6. Do not pull Phase 6 behavior forward.
|
||||
- **D-13:** The must-change-password gate is real and owned where PHP owns it: `golem15.fonoteka` registers `inv.must-change-password` (port of `RequirePasswordChange.php`) and attaches it by name after `jwt.auth`. It reads the flag on the minimal user and returns PHP's 423 body, Go-tested. This is the slice's proof of HTTP-02: one plugin referencing another plugin's middleware by name.
|
||||
- **D-14:** Routes are declared with a Laravel-like group builder that compiles to stdlib ServeMux patterns, so porting `routes.php` is line by line. Target feel: `r.Group("/_fonoteka/api/v1", surf.Use("jwt.auth", "inv.must-change-password"), func(g surf.Group) { g.Get("genres", h.Index) })`. Handlers remain plain `http.HandlerFunc`/`http.Handler` so swag annotations work later. Group-level and per-route middleware are both by name, matching PHP's `->middleware('throttle:10,1')` call sites.
|
||||
- **D-15:** The typed-param helper ships now even though genres has no path params: an integer param accessor with the rule that unknown and malformed ids both yield 404 (HTTP-01), plus the regex/enum constraint shape PHP's `->where()` uses. It is exercised by `surf`'s own tests and a route in `examples/hello`, so success criterion 2's "typed params" clause is met and Phase 12 does not invent it.
|
||||
|
||||
### Migrations and seed data
|
||||
- **D-16:** Go migrations are squashed to final state per table, not a replay of PHP's history: Postgres starts empty, so `golem15_fonoteka_genres` is created directly with no `item_categories` detour. Each migration carries a comment naming the PHP update files it folds. Production data arrives through a cutover import (Phase 15), not through migrations.
|
||||
- **D-17:** Phase 3 migrations are permanent and never edited. The minimal users, collections and albums tables are widened in Phase 5 by appended ALTER migrations. "Never edit a shipped migration" applies from the first migration.
|
||||
- **D-18:** The 15 genres are a data migration in the `golem15.fonoteka` set, as in PHP (`v1.1.0/seed_genre_and_various_artist_taxonomy.php`): idempotent by slug, inserted in the PHP order so ids come out 1–15, with a Rollback that removes them. A fresh Go database has genres without any seeder command.
|
||||
- **D-19:** Each plugin ships its own ordered `[]*gormigrate.Migration` set through a capability interface; sets run in plugin dependency order (`golem15.user` before `golem15.fonoteka`) with per-plugin version tracking, so rolling back one plugin never touches another's history (ARCHITECTURE.md Anti-Pattern 4). `AutoMigrate` is not used anywhere, tests included. Up and down are both exercised in tests for each set.
|
||||
- **D-20:** The temporary `genres` seed hook (Phase 2 D-10) in `fonoteka.go/parity` inserts alice and her collection through GORM, mints her token with the test secret into `jwt:alice`, and sets the colliding id variables (`id:genre`, `id:token`, `id:wishlist-album`) to the values the recorded fixture expects. The fixture stays byte-for-byte as recorded and `tide`'s scrubber is not changed in this phase. The hook is declared temporary in the manifest with the routes that will replace it (register/login, `POST genres`), and the genres route flips from `pending` to `ported`. `newTarget` returns the real app handler instead of the synthetic one.
|
||||
|
||||
### Claude's Discretion
|
||||
- Package internals and file layout of `surf`, `lagoon` and `bouncer`; exact capability interface names in `pact` (follow ARCHITECTURE.md Pattern 1 and the Phase 1 naming).
|
||||
- DB config section shape (`database.*` keys, pool sizes) and how the shared `*sql.DB` is published on the `backpack` container. The separate pgx pool for River's listener (DATA-01's second half) is not created until Phase 11; leave the seam, note it in the plan.
|
||||
- Version-tracking table naming for per-plugin gormigrate state.
|
||||
- The `serve` and `migrate:*` command set on the `bonfire` kernel: at minimum `serve`, `migrate`, `migrate:rollback`, `migrate:status`, colon-style per Phase 1 D-15. Graceful shutdown on signal.
|
||||
- Behavior when a route references an unregistered middleware name: fail boot with an error naming the plugin and the missing name, consistent with Phase 1 D-10.
|
||||
- The response DTO for the genre aggregate (a dedicated struct, not the GORM model, per PITFALLS.md Pitfall 3) and `[]` not `null` for an empty list (Pitfall 4).
|
||||
- How `fonoteka.go` is laid out as a workspace (`plugins/golem15/user`, `plugins/golem15/fonoteka`, generated `main.go` and `plugins.gen.go` via `summer build`, `summer.yaml` manifest order).
|
||||
- Plan count and split, subject to the repo's plan-count checkpoint and "unit tests are the last plan" rules.
|
||||
|
||||
</decisions>
|
||||
|
||||
<canonical_refs>
|
||||
## Canonical References
|
||||
|
||||
**Downstream agents MUST read these before planning or implementing.**
|
||||
|
||||
### The PHP contract for this slice
|
||||
- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/GenreApiController.php` — `index()` (validation, count subquery, `PolishOrder`, `non_empty`, `serializeGenreAggregate`) and `context()`. The handler being ported; `store()` is out of scope.
|
||||
- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/routes.php` lines 61–80 and 271 — the two JWT groups (`jwt.auth`, `bindings`; and `jwt.auth`, `inv.must-change-password`, `bindings`) and the genres route's place in the gated group. The shape D-14's builder must reproduce.
|
||||
- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/classes/ActiveCollectionResolver.php` — default-path logic to port in D-03.
|
||||
- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/classes/PolishOrder.php` — the ordering to reproduce on Postgres (D-06).
|
||||
- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/Genre.php` and the `Album::accessibleBy` scope in `models/Album.php` — columns and the access rule that define the minimal tables (D-02).
|
||||
- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/traits/SerializesFonoteka.php` — `serializeGenreAggregate` field list and types.
|
||||
- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/user/middleware/JwtAuthenticate.php` and `/media/nvme/dev/golem15/fonoteka/config/jwt.php` — claims, algorithm, and the exact 401 bodies (D-07, D-10).
|
||||
- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/middleware/RequirePasswordChange.php` and `Plugin.php` around line 214 — the 423 gate and how it is registered by name (D-13).
|
||||
- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/updates/version.yaml`, `updates/v1.0.9/rename_taxonomy_and_notes_columns.php`, `updates/v1.1.0/seed_genre_and_various_artist_taxonomy.php` — the history being squashed and the 15-genre seed with its order (D-16, D-18).
|
||||
|
||||
### Parity harness seam (Phase 2 output)
|
||||
- `../fonoteka.go/parity/fixtures/routes/get_genres_jwt.yaml` — the fixture that must go green, including the `{{id:…}}` placeholder collisions handled by D-20.
|
||||
- `../fonoteka.go/parity/manifest.yaml` around line 1513 — the route entry to flip from `pending` to `ported` and where the temporary seed hook is declared.
|
||||
- `../fonoteka.go/parity/parity_test.go` — `newTarget`, `seedHooks`, `invokeSeedHook`, `replayPortedRoute`: the app-owned seam this phase fills.
|
||||
- `../fonoteka.go/parity/capture-rules.yaml` — how `jwt:alice` is normally captured, which the hook substitutes for.
|
||||
- `.planning/phases/02-api-parity-harness-bootstrap/02-CONTEXT.md` D-10, D-12, D-13, D-15, D-16 — seed hooks, in-process replay host, diff and normalizer rules, pending-never-equals-passing.
|
||||
|
||||
### Kernel architecture and pitfalls
|
||||
- `.planning/research/ARCHITECTURE.md` §Pattern 1 (capability interfaces), §Request Flow, §Migration Flow, §Anti-Pattern 1, §Anti-Pattern 3, §Anti-Pattern 4, §Framework vs Application Repo Boundary, §First Vertical Slice. Text that says goose is superseded: gormigrate is authoritative (STATE.md, PROJECT.md Key Decisions).
|
||||
- `.planning/research/PITFALLS.md` §Pitfall 1 (bottom-up kernel: build only what genres pulls), §Pitfall 2 (context not globals), §Pitfall 3 (DTO vs model), §Pitfall 4 (nil slice vs `[]`), §Pitfall 8 (envelopes and error shapes), §Pitfall 13 (no AutoMigrate), §Pitfall 14 (shared pool), §Security Mistakes, §Pitfall-to-Phase Mapping.
|
||||
- `.planning/research/STACK.md` §River + GORM: share one `*sql.DB`, §Migration tooling, §Version Compatibility (gorm v1.31.2, driver v1.6.3, pgx v5.10.0, golang-jwt v5.3.1, validator v10.30.4, testcontainers v0.44.0).
|
||||
|
||||
### Project-level decisions
|
||||
- `.planning/PROJECT.md` §Constraints, §Key Decisions; `.planning/REQUIREMENTS.md` DATA-01, DATA-02, HTTP-01, HTTP-02, QA-04; `.planning/ROADMAP.md` Phase 3 section.
|
||||
- `.planning/phases/01-framework-kernel-foundation/01-CONTEXT.md` D-01, D-02, D-06, D-08, D-10, D-15, D-17 and §Integration Points — two-binary model, config namespaces and env mapping, fail-fast boot, command naming, and the note that `HasRoutes`/`HasModels`/`HasMigrations` are declared for Phase 3 to implement.
|
||||
- `.planning/notes/why-go-not-scala.md` — the failure mode this phase exists to avoid.
|
||||
- `CLAUDE.md` (repo root) §GSD workflow rules — lean planning, plan-count checkpoint, unit tests as the last plan, stdlib first, `go vet` and `go test ./...` green at every commit.
|
||||
|
||||
</canonical_refs>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- `summercms.go` kernel from Phase 1: `compass` (config, namespaces, env overlay), `party` (plugin registry, `Requires()` ordering, generated import list, `summer build`), `backpack` (container for Register/Boot injection), `festival` (typed events), `bonfire` (command kernel both binaries share), `pact` (currently `HasCommands`, `HasConfig`, `OptionalMessage`), `towel`, `examples/hello`.
|
||||
- `tide` (Phase 2): the parity library, manifest and fixture schema, `summer parity:*` commands, replay host and coverage report. Used as is; not modified in this phase.
|
||||
- `fonoteka.go/parity`: recorded 154-route corpus, manifest, `parity_test.go` with testcontainers Postgres already starting in `TestMain`, and `scripts/check-phase2.sh` in the framework repo.
|
||||
|
||||
### Established Patterns
|
||||
- Stdlib first. New dependencies this phase are the ones research already names: `gorm.io/gorm`, `gorm.io/driver/postgres` (pgx v5 stdlib underneath), `go-gormigrate/gormigrate/v2`, `golang-jwt/jwt/v5`, `go-playground/validator/v10`. Anything else needs a decision note. No router library: ServeMux only.
|
||||
- Request state only in `context.Context` with unexported key types and exported accessors; the container is used at Register and Boot only; app and bus are instance fields, not globals.
|
||||
- Fail boot loudly on misconfiguration (missing dependency, missing middleware name, empty JWT secret) rather than warn and continue.
|
||||
- The framework never imports the app; app-specific logic (active collection, must-change-password, genres) lives in `fonoteka.go` plugins.
|
||||
|
||||
### Integration Points
|
||||
- `pact` gains the route, middleware, migration and model capability interfaces; `party` boot wires them: migrations collected in dependency order, middleware names registered, route groups mounted on one ServeMux behind the pipeline.
|
||||
- `bonfire` kernel gains `serve` and `migrate:*`, so both `summer` and the generated `fonoteka` binary expose them.
|
||||
- `fonoteka.go/parity/parity_test.go` `newTarget` switches from the synthetic handler to the booted app handler; the `genres` seed hook is registered in `seedHooks`.
|
||||
- `fonoteka.go` has no `go.work` or plugins yet: this phase creates the workspace, the two plugins and the generated app entry point.
|
||||
|
||||
</code_context>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
- "Throwaway" applies to token issuing only. The verifier, the 401 bodies and the 423 gate are written to be kept, so the security review has something real to review.
|
||||
- The routing API should read like `routes.php`: a port of the 154 routes should be a line-by-line translation, with middleware referenced by the same names PHP uses (`jwt.auth`, `inv.must-change-password`).
|
||||
- The slice must prove cross-plugin wiring, not just one plugin: `golem15.fonoteka` requires `golem15.user`, uses its middleware by name, and migrates after it.
|
||||
- A fresh Go database should look like a fresh PHP one: genres present after `migrate`, no seeder step.
|
||||
- The recorded fixture is the oracle and is not touched; where it cannot distinguish right from wrong (zero counts), Go integration tests carry the proof.
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
- Tightening `tide`'s scrubber so unrelated ids are not replaced by captured-variable placeholders (genre ids 1, 2, 4 became `{{id:wishlist-album}}`, `{{id:token}}`, `{{id:genre}}`) and re-scrubbing the corpus — a harness fix touching all 154 fixtures; revisit when a second ported route hits the same collision, or before Phase 12.
|
||||
- Recorded fixtures for genres with non-zero counts, `?non_empty=1`, the 422 envelope and an unauthenticated 401 — candidates for the next PHP recording session; Go tests cover them until then.
|
||||
- Moving active-collection resolution into the org-context pipeline stage through a plugin-supplied resolver hook — reconsider in Phase 6 or Phase 12 when many handlers share it.
|
||||
- Guard registry with the three auth groups, named rate-limit buckets, full CORS and locale negotiation — Phase 6.
|
||||
- Register, login, refresh and real token issuing in the user plugin — Phase 7; removes the seed hook's JWT minting.
|
||||
- The separate pgx pool for River's LISTEN/NOTIFY (second half of DATA-01) — Phase 11.
|
||||
- Production data import from the PHP database — Phase 15 cutover.
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 03-first-vertical-slice-genres-end-to-end*
|
||||
*Context gathered: 2026-09-17*
|
||||
@@ -0,0 +1,219 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user