docs(03): verify first vertical slice goal achievement

This commit is contained in:
Jakub Zych
2026-09-17 20:50:30 +02:00
parent eebcba5968
commit 2d511834ea

View File

@@ -0,0 +1,202 @@
---
phase: 03-first-vertical-slice-genres-end-to-end
verified: 2026-09-17T18:49:25Z
status: passed
score: 4/4 must-haves verified
overrides_applied: 0
human_verification: []
decision_coverage:
honored: 20
total: 20
not_honored: []
---
# Phase 3: First vertical slice — genres end to end Verification Report
**Phase Goal:** `GET /_fonoteka/api/v1/genres` passes the parity diff end to end (config → DB → plugin registry → JWT-guarded routing → GORM → JSON), proving every kernel layer together before any further kernel abstraction. Touches the JWT auth guard on the request path — treat as security-load-bearing and apply the security-review agent even though the guard is a throwaway seeded-token implementation at this stage.
**Verified:** 2026-09-17T18:49:25Z
**Status:** passed
**Re-verification:** No — initial verification
Must-haves are the four ROADMAP success criteria (they override plan-level truths). Plan `must_haves` were checked as supporting evidence. The ROADMAP goal is an infrastructure/API outcome, not a user-story sentence; MVP user-flow coverage is mapped from Plan 01's objective below.
`03-REVIEW.md` status is `issues` (7 warnings, 0 critical). Those findings do not break must-haves or the phase goal and are recorded as non-blocking notes. `03-SECURITY-REVIEW.md` reports `threats_open: 0`.
## User Flow Coverage
User story (Plan 01 objective): «As a Fonoteka user with a valid token, I want to request the seeded genre list from a real Postgres backed Go app, so that the first complete config → plugin → DB → auth → HTTP path exists.»
| Step | Expected | Evidence | Status |
|------|----------|----------|--------|
| Present a valid HS256 JWT for a persisted user | Guard loads the user; missing/forged tokens never reach the handler | `bouncer/jwt.go` `Verify` + `Middleware`; app `TestGenreListBehindJWT`, `TestGenreSecurityBoundaries` | ✓ |
| Request `GET /_fonoteka/api/v1/genres` | Named pipeline (`jwt.auth`, `inv.must-change-password`) on ServeMux | `plugins/golem15/fonoteka/plugin.go` `Routes`; `surf.Assemble` | ✓ |
| See the global genre list | Dedicated DTO `id`/`name`/`slug`/`album_count` under `data`, empty list `[]` | `genre_handler.go` `GenreAggregate` / `GenreList`; tenant counts from GORM | ✓ |
| Outcome | Recorded PHP fixture diffs green; 153 other routes stay pending | This session: `TestParityCorpus` `recorded 154/154 passing 1 failing 0 unrecorded 0 pending 153`; `GET /_fonoteka/api/v1/genres jwt` PASS 0.09s | ✓ |
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | GORM connects to Postgres over one shared `*sql.DB` (pgx stdlib); a gormigrate migration set runs up and down for a single plugin with per-plugin version tracking, and `AutoMigrate` is not the schema source | ✓ VERIFIED | `lagoon.Open`/`Use` open `sql.Open("pgx", dsn)` then `gorm.Open(postgres.New({Conn: sqlDB}))` and fail if `gdb.DB() != sqlDB`. `Publish` stores both handles on backpack. Per-plugin history tables `summer_migrations_<id>`. `RollbackLast` calls `gormigrate.RollbackLast()` for one plugin. This session: `go test ./lagoon -count=1` ok 7.940s (`TestSharedSQLPoolUsedByGORMAndClosed`, `TestTwoPluginMigrationSetsIsolated`, `TestNoAutoMigrate`). App: `TestMigrateSeedsCanonicalGenres`, `TestRollbackLastIsolatesFonoteka`, `TestPluginMigrationsDoNotUseAutoMigrate`. River listener `pgxpool` is a documented Phase 11 seam in `lagoon/connection.go`, not created here. |
| 2 | A plugin registers a route group on `net/http` ServeMux with typed params, and a request is served through the full middleware pipeline (recover, CORS, locale, auth group, must-change-password, org context, rate limit, handler) | ✓ VERIFIED | `surf.compile` mounts `GET `+path on `http.NewServeMux` wrapped `recoverJSON(cors(...))`. `wrap` order: locale → named (`jwt.auth`, `inv.must-change-password`) → org slot → no-op rate limit → constraints → handler. Missing names fail Assemble with plugin ID + name. Typed params: `surf.IntParam` + `Where`/`WhereIn` compiled at registration; `examples/hello` `/items/{id}` 404s malformed and unknown IDs. This session: `go test ./surf ./examples/hello -count=1` ok (`TestPipelineOrderRecoverCORSLocaleAuthPasswordOrgRateHandler`, `TestTypedIDRouteReturns404`). |
| 3 | A JWT-guarded `GET /_fonoteka/api/v1/genres` route returns a real `Genre` row read from Postgres | ✓ VERIFIED | `golem15.user` registers `jwt.auth` (`bouncer.Middleware`, HS256 pinned, `exp`+`sub`, persisted user). `golem15.fonoteka` `Requires()` `golem15.user` and mounts the route. Handler `Lookup[*gorm.DB]`, resolves active collection, left-joins tenant-scoped `COUNT(*)`. `TestGenreListBehindJWT` and `TestGenreCountsScopedToActiveCollection` hit real rows. Seed migration inserts 15 canonical genres. `TestParityCorpus/GET___fonoteka_api_v1_genres_jwt` PASS this session. |
| 4 | The response is diffed byte-for-byte against a fixture recorded from the live PHP backend using the Phase 2 harness, and the diff is green | ✓ VERIFIED | Fixture `parity/fixtures/routes/get_genres_jwt.yaml` last commits are Phase 2 (`8b5bd23`, `66ed0bb`) — unmodified in this phase. `newTarget` → `app.Handler` over TestMain SQL pool. `seedHooks["genres"]` mints test-only JWT + colliding IDs. `tide.ReplayFlow` against httptest. Coverage line this session: `recorded 154/154 passing 1 failing 0 unrecorded 0 pending 153`. `check_corpus.go --require-recorded --require-clients --check-secrets` printed `recorded 154/154`. Pending routes load fixtures only and never increment `Passing`. |
**Score:** 4/4 truths verified
Supporting plan truths (D-01…D-20, JWT matrix, seven-stage pipeline, isolated rollback, honest 1-of-154 corpus, security review `threats_open: 0`) all hold in code; they are not extra score rows.
### Required Artifacts
`gsd-sdk query verify.artifacts`: **20/20 passed** across the four plans.
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `lagoon/connection.go` | One checked pgx stdlib SQL pool passed to GORM | ✓ VERIFIED | 137 lines; `Open`/`Use`/`Publish`/`CheckLocale`; GORM identity check |
| `lagoon/migrations.go` | Per-plugin gormigrate sets and version tables | ✓ VERIFIED | `HistoryTableName`, `Migrate` in Activate order, `RollbackLast`, `Status` |
| `lagoon/order.go` | Safe database-default ordering helper | ✓ VERIFIED | Allow-listed column + asc/desc; no COLLATE |
| `lagoon/commands.go` | Reversible per-plugin migration CLI | ✓ VERIFIED | `migrate`, `migrate:rollback --plugin`, `migrate:status`; `ErrUnknownPlugin` |
| `surf/router.go` | Group declarations compiled to ServeMux | ✓ VERIFIED | `Group`/`Get`/`Assemble`; recover → CORS around mux |
| `surf/params.go` | Typed and constrained ServeMux path parameters | ✓ VERIFIED | `IntParam`, `Regex`/`Enum` compiled at registration |
| `bouncer/jwt.go` | Pinned HS256 verifier and request identity | ✓ VERIFIED | `WithValidMethods([]string{"HS256"})`, `WithExpirationRequired()`, PHP 401 bodies |
| `../fonoteka.go/plugins/golem15/fonoteka/plugin.go` | First real app route through the user plugin | ✓ VERIFIED | `Requires` `golem15.user`; `Use("jwt.auth", "inv.must-change-password")`; `GET /genres` |
| `../fonoteka.go/plugins/golem15/fonoteka/active_collection.go` | Default active collection + access predicate | ✓ VERIFIED | Grouped owner OR editor, then AND `collection_id` |
| `../fonoteka.go/plugins/golem15/fonoteka/genre_handler.go` | PHP-fidelity aggregate query and response | ✓ VERIFIED | Dedicated DTO; `lagoon.OrderBy`; `non_empty` 422 envelope |
| `../fonoteka.go/app/app.go` | Reusable app boot seam | ✓ VERIFIED | `lagoon.Use` + `Publish` then `party.Activate` then `surf.Assemble` |
| `../fonoteka.go/parity/genre_integration_test.go` | Postgres proof the zero-count fixture cannot give | ✓ VERIFIED | Owner/editor/foreign counts, `non_empty`, 422 |
| `../fonoteka.go/parity/genres_seed_test.go` | Temporary trusted Go-only genre fixture seed hook | ✓ VERIFIED | Alice, collection, test JWT, colliding IDs from CanonicalGenres |
| `../fonoteka.go/parity/parity_test.go` | Real app handler replay and honest corpus counts | ✓ VERIFIED | `newTarget` → `app.Handler`; `Passing++` only after ported replay |
| `../fonoteka.go/parity/manifest.yaml` | One declared ported route and temporary hook | ✓ VERIFIED | 1 `status: ported` + `seed_hook: genres`; 153 pending |
| `bouncer/jwt_test.go` | Adversarial token verification coverage | ✓ VERIFIED | Valid, expired, wrong alg, bad sig, malformed, missing sub/exp, alg:none, empty secret, no leak |
| `surf/middleware_test.go` | Pipeline and missing guard boot coverage | ✓ VERIFIED | Missing name fails boot; unauthenticated never hits handler; seven-stage order |
| `../fonoteka.go/parity/genre_security_test.go` | Cross-plugin auth and tenant isolation | ✓ VERIFIED | 401 variants, 423, CORS preflight, foreign counts stay 0 |
| `scripts/check-phase3.sh` | Repeatable full gate for both repositories | ✓ VERIFIED | Root+app vet/test/race, focused corpus, security/tenant, `check_corpus.go`, Phase 2 Go/CLI regression |
| `.planning/phases/03-first-vertical-slice-genres-end-to-end/03-SECURITY-REVIEW.md` | Security review evidence | ✓ VERIFIED | `threats_open: 0`; T-03-01…T-03-07 and T-03-SC closed |
**Artifacts:** 20/20 verified
### Key Link Verification
`gsd-sdk query verify.key-links` reported 0/11 verified. Same false negative as Phases 1–2: the checker greps target *filenames* inside source; Go imports package paths. Manual wiring:
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `party/registry.go` | `pact/capabilities.go` | optional capability discovery in Requires order | ✓ WIRED | `Activate` topo-sorts `Requires()` then Register/Boot. `lagoon.Migrate` type-asserts `pact.HasMigrations` in that order; `surf.Assemble` asserts `HasMiddleware`/`HasRoutes`. `party` itself asserts `HasConfig` during Activate. |
| `lagoon/connection.go` | `backpack/app.go` | app-scoped `*sql.DB` and `*gorm.DB` publication | ✓ WIRED | `Publish` → `app.Publish(sqlDB)` then `app.Publish(gdb)`. Handler `Lookup[*gorm.DB]()`. `TestSharedSQLPoolUsedByGORMAndClosed` proves identity. |
| `plugins/golem15/fonoteka/plugin.go` | `plugins/golem15/user/plugin.go` | Requires and named `jwt.auth` lookup | ✓ WIRED | `Requires() []string{"golem15.user"}`; `surf.Use("jwt.auth", "inv.must-change-password")`. User plugin registers `jwt.auth`. `TestFonotekaRequiresUserPlugin`. |
| `genre_handler.go` | `active_collection.go` | authenticated user and active collection lookup | ✓ WIRED | `bouncer.User` then `ResolveActiveCollection`; albums subquery uses `AccessibleByMembership`. |
| `genre_handler.go` | `lagoon/order.go` | allow-listed name ordering call | ✓ WIRED | `lagoon.OrderBy(q, "golem15_fonoteka_genres.name", "asc", polishOrderColumns)` |
| `genre_integration_test.go` | `genre_handler.go` | real HTTP request over testcontainers Postgres | ✓ WIRED | `app.Handler` + httptest; `TestGenreCountsScopedToActiveCollection` |
| `parity_test.go` | `app/app.go` | `newTarget` boots the real app over the TestMain SQL pool | ✓ WIRED | `app.Handler(t.Context(), testConfig(t), app.PluginIDs, db)` |
| `manifest.yaml` | `genres_seed_test.go` | `seed_hook: genres` allow-list dispatch | ✓ WIRED | YAML `seed_hook: genres`; `seedHooks["genres"] = seedGenres`; unknown names fail |
| `parity_test.go` | `tide/replay.go` | unchanged `tide.ReplayFlow` against recorded fixture | ✓ WIRED | `tide.ReplayFlow(..., Target: srv.URL, Store: store)` in `replayPortedRoute` |
| `scripts/check-phase3.sh` | `parity/parity_test.go` | focused ported-route and full corpus Go test | ✓ WIRED | `go test ./parity -count=1 -run 'TestParityCorpus$'` |
| `03-SECURITY-REVIEW.md` | `bouncer/jwt_test.go` | threat-to-test evidence | ✓ WIRED | T-03-03 cites `TestVerifyRejectsBadTokens`, `TestVerifyRejectsAlgNoneEmptySecretAndAbsentExp`, `TestVerifyAndMiddlewareOmitTokenAndSecret` (all in `jwt_test.go`) |
**Wiring:** 11/11 connections verified (manual)
### Data-Flow Trace (Level 4)
| Artifact | Data | Source | Produces real data | Status |
|----------|------|--------|--------------------|--------|
| `lagoon.Open`/`Use` | GORM queries | Shared pgx-stdlib `*sql.DB` | Yes — this session `go test ./lagoon` against testcontainers Postgres 16 ICU `pl-PL` | ✓ FLOWING |
| `golem15.fonoteka` migrations | 15 genre rows | Explicit SQL seed, not AutoMigrate | Yes — `TestMigrateSeedsCanonicalGenres`; rollback isolates Fonoteka history | ✓ FLOWING |
| `bouncer.Middleware` | authenticated principal | HS256 JWT `sub` → users table | Yes — valid token reaches handler; `alg:none`/expired/unknown-user 401 and never set `Cache-Control: no-cache, private` | ✓ FLOWING |
| `ListGenres` | `{"data":[...]}` | GORM left-join counts scoped to active collection | Yes — Alice/Bob/foreign cases; PHP 422 on bad `non_empty` | ✓ FLOWING |
| `TestParityCorpus` ported case | byte diff vs PHP fixture | `tide.ReplayFlow` + recorded `get_genres_jwt.yaml` | Yes — this session `passing 1` / genres jwt 0.09s; mutated body fails | ✓ FLOWING |
## Requirements Coverage
PLAN frontmatter IDs: DATA-01, DATA-02, HTTP-01, HTTP-02, QA-04 (union of 03-01…03-04). REQUIREMENTS.md maps the same five to Phase 3. No Phase 3 orphans. Every ID is accounted for.
| Requirement | Source Plan | Description | Status | Evidence |
|-------------|-------------|-------------|--------|----------|
| DATA-01 | 01, 02, 04 | GORM on Postgres through one shared `*sql.DB` (pgx stdlib); River listener pool reserved | ✓ SATISFIED | Shared pool proven (`TestSharedSQLPoolUsedByGORMAndClosed`). Separate `pgxpool` for River LISTEN/NOTIFY is an explicit Phase 11 seam (`lagoon/connection.go`, CONTEXT deferred list). Phase 3 success criterion does not require creating that pool. |
| DATA-02 | 01, 03, 04 | Per-plugin gormigrate up/down, dependency order, per-plugin version tracking; AutoMigrate never the schema source | ✓ SATISFIED | User then Fonoteka history tables; `RollbackLast` isolates Fonoteka; source scan + tests forbid `AutoMigrate` |
| HTTP-01 | 01, 03, 04 | Plugins register route groups on ServeMux with typed params and regex constraints; unknown and malformed ids both 404 | ✓ SATISFIED | Fonoteka group `GET /_fonoteka/api/v1/genres`. Typed/constraint API exercised by `examples/hello` and `TestTypedIDRouteReturns404` (genres has no path param) |
| HTTP-02 | 01, 02, 03, 04 | Named middleware referenced by other plugins; pipeline recover, CORS, locale, auth, must-change-password, org, rate limit, handler | ✓ VERIFIED | `jwt.auth` owned by user, `inv.must-change-password` owned by fonoteka; `TestPipelineOrderRecoverCORSLocaleAuthPasswordOrgRateHandler`; CORS preflight bypasses named auth |
| QA-04 | 01–04 | First vertical slice `GET /_fonoteka/api/v1/genres` passes the parity diff end to end before further kernel abstraction | ✓ SATISFIED | This session: `recorded 154/154 passing 1 failing 0 unrecorded 0 pending 153`. Pending never counts as passing (`TestParityContract`) |
**Coverage:** 5/5 requirements satisfied
### Decision Coverage
All trackable CONTEXT.md decisions are honored by shipped artifacts. `gsd-sdk query check.decision-coverage-verify`: **20/20 honored**, `not_honored: []`. Handler fidelity D-01–D-06, JWT D-07–D-11, pipeline/routing D-12–D-15, migrations/seed/parity D-16–D-20.
## Behavioral Verification
| Check | Result | Detail |
|-------|--------|--------|
| Root `go vet ./... && go test ./...` | ✓ | All packages ok (this session; cached packages plus forced `lagoon`/`bouncer`/`surf`/`pact`/`examples/hello`) |
| `go test ./lagoon ./bouncer ./surf ./pact ./examples/hello -count=1` | ✓ | lagoon 7.940s (testcontainers); others <3s |
| App `go vet ./... && go test ./... -count=1` | ✓ | `ok git.golem15.com/golem15/fonoteka/parity` 8.065s |
| `TestParityCorpus$` | ✓ | `recorded 154/154 passing 1 failing 0 unrecorded 0 pending 153`; `GET /_fonoteka/api/v1/genres jwt` PASS 0.09s |
| `check_corpus.go --require-recorded --require-clients --check-secrets` | ✓ | `recorded 154/154` |
| Focused genre/security/migrate tests | ✓ | Included in app `go test ./...` this session |
| Full `scripts/check-phase3.sh` (includes `-race`) | ℹ not re-run | VALIDATION.md 2026-09-17: ~21s after warmup, `phase3 check passed`. Spot-checks above confirm the same vet/test/corpus/parity path. Race was not re-executed this session. |
## Anti-Patterns Found
No `TODO` / `FIXME` / `XXX` / `TBD` / `HACK` / `coming soon` / `not implemented` in `lagoon`, `surf`, `bouncer`, fonoteka plugins, or `app`. No `AutoMigrate` in production or plugin sources.
| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| `surf/serve.go` | ~49 | `http.Server` timeouts left at zero | ⚠️ Warning | Advisory WR-01 from `03-REVIEW.md`. Slowloris hold-open on production `serve`. Does not affect httptest parity path or must-haves. Phase 6 owns HTTP hardening depth; timeouts are serve-command hardening. |
| `surf/router.go` | 290–300 | `recoverJSON` may append a 500 body after headers committed | ⚠️ Warning | Advisory WR-02. Panic-after-write is untested; genre handler does not panic after write on the passing path. |
| `cmd/summer/runtime.go` | 15–25 | `summer serve` delegate has no `--addr` flag | ⚠️ Warning | Advisory WR-03. Operators use `./bin/fonoteka serve` or default `:8080`. Not a slice blocker. |
| `../fonoteka.go/main.go` vs `app/app.go` | 31–37 / 31–43 | Production `Activate` before DB publish; tests Publish then Activate | ⚠️ Warning | Advisory WR-04. Current `Boot` only checks JWT secret, so both paths work. Later plugins that `Lookup[*gorm.DB]()` in `Boot` need this aligned (Phase 5/7). |
| `../fonoteka.go/parity/synthetic_test.go` | 110 | `t.Skip` under `-short` | ℹ️ Info | Phase 2 fast loop; TestMain still fails closed without Docker when not `-short`. Phase 3 gate runs without `-short`. |
| `lagoon/postgres_test.go` | 94 | `t.Skip` under `-short` | ℹ️ Info | Same fast-loop pattern for framework ICU Postgres. |
**Anti-patterns:** 6 found (0 blockers, 4 warnings, 2 info)
Code-review advisories WR-05…WR-07 (JWT lookup errors mapped to 401 vs PHP 500; typed-param 404 HTML page; similar) likewise do not fail the genres goal. Security review closed the JWT/tenant threats the slice owns.
## Test Quality Audit
| Test File | Linked Req | Active | Skipped | Circular | Assertion Level | Verdict |
|-----------|-----------|--------|---------|----------|----------------|---------|
| `lagoon/connection_test.go` | DATA-01 | yes | `-short` on container helper | no | Behavioral (shared `*sql.DB` identity, ICU fail) | ✓ |
| `lagoon/migrations_test.go` | DATA-02 | yes | no | no | Behavioral (two-plugin isolation, no AutoMigrate) | ✓ |
| `bouncer/jwt_test.go` | HTTP-02 | yes | no | no | Value (PHP 401 strings) + Behavioral (alg:none, leak) | ✓ |
| `surf/middleware_test.go` | HTTP-02 | yes | no | no | Behavioral (order, missing name, unauthenticated) | ✓ |
| `surf/params_test.go` / `examples/hello/hello_test.go` | HTTP-01 | yes | no | no | Behavioral (malformed+unknown 404) | ✓ |
| `../fonoteka.go/parity/genre_integration_test.go` | QA-04, D-01–D-06 | yes | no | no | Value (counts, 422 body, order) | ✓ |
| `../fonoteka.go/parity/genre_security_test.go` | HTTP-02, QA-04 | yes | no | no | Behavioral (401/423/CORS/tenant) | ✓ |
| `../fonoteka.go/parity/parity_test.go` | QA-04 | yes | no on ported path | no | Value (PHP fixture byte diff) | ✓ |
| `../fonoteka.go/parity/parity_contract_test.go` | QA-04 | yes | no | no | Value (154/1/153; pending≠pass; mutation fails) | ✓ |
| `../fonoteka.go/parity/migrate_test.go` | DATA-02 | yes | no | no | Behavioral (15 seeds, rollback isolation) | ✓ |
**Disabled tests on requirements:** 0 blockers (`-short` skips are the documented fast loop, not the sign-off path).
**Circular patterns detected:** 0. Expected genre JSON is the Phase 2 PHP recording (`get_genres_jwt.yaml`). The `genres` seed hook prepares Postgres + a test JWT so the Go handler can match that external oracle; it does not write the expected fixture from the SUT.
**Insufficient assertions:** 0. Ported replay is value-level (`tide.ReplayFlow` body/status). Pending rows only load fixtures (honest: no Go handler).
**Provenance:** VALID — expected body from the live PHP backend (Phase 2). Colliding `{{id:*}}` placeholders are restored from CanonicalGenres seed order (D-20), not from Go response capture.
## Human Verification Required
N/A — Infrastructure/API foundation phase with no user-facing product UI. Acceptance is Postgres + JWT-guarded HTTP + the Phase 2 parity harness. VALIDATION.md's production-DB locale checklist is documented in `../fonoteka.go/README.md` and enforced by `CheckLocale` in Open/Use (wrong locale fails boot); testcontainers create the same ICU `pl-PL` database.
## Gaps Summary
**No gaps found.** Phase goal achieved. All four ROADMAP success criteria hold in the codebase and were exercised this session (root/app vet/test, live `TestParityCorpus` one-of-154 pass, corpus audit). Advisory code-review warnings do not break must-haves.
DATA-01's River listener pool remains a Phase 11 seam by design (CONTEXT deferred; ROADMAP Phase 11 SC1). Not a Phase 3 gap.
## Recommended Fix Plans
None — status is `passed`.
## Verification Metadata
**Verification approach:** Goal-backward from ROADMAP success criteria
**Must-haves source:** ROADMAP.md success criteria (override PLAN.md frontmatter)
**Automated checks:** 20 artifacts passed, 11 key links passed (manual; gsd-sdk filename-grep false negatives excluded), 4/4 truths, 5/5 requirements
**Human checks required:** 0
**Total verification time:** ~12 min
---
*Verified: 2026-09-17T18:49:25Z*
*Verifier: the agent (subagent)*