Files
summercms/.planning/phases/03-first-vertical-slice-genres-end-to-end/03-03-PLAN.md
2026-09-17 18:40:13 +02:00

11 KiB

phase: 03-first-vertical-slice-genres-end-to-end plan: 03 type: execute wave: 3 depends_on: ["03-02"] files_modified: - surf/params.go - lagoon/migrations.go - lagoon/commands.go - examples/hello/main.go - examples/hello/plugins/greeter/plugin.go - ../fonoteka.go/parity/parity_test.go - ../fonoteka.go/parity/genres_seed_test.go - ../fonoteka.go/parity/manifest.yaml - ../fonoteka.go/README.md autonomous: true requirements: [DATA-02, HTTP-01, HTTP-02, QA-04] must_haves: truths: - "D-15: surf parses integer path params, applies regex/enum constraints, and gives the same 404 for malformed and unknown ownership-scoped IDs; examples/hello exercises the API." - "D-19: migrate:rollback --plugin=golem15.fonoteka only reverses that plugin's latest migration and migrate:status reports each plugin's applied IDs." - "D-20: the app-owned genres seed hook supplies Alice, active collection, a test-only JWT and colliding IDs; the unmodified PHP fixture passes through the real app handler." - "The corpus records exactly one ported/passing Go route and 153 pending routes; pending never counts as passing." artifacts: - path: surf/params.go provides: Reusable typed and constrained ServeMux path parameters - path: lagoon/commands.go provides: Reversible per-plugin migration CLI - path: ../fonoteka.go/parity/genres_seed_test.go provides: Temporary trusted Go-only genre fixture seed hook - path: ../fonoteka.go/parity/parity_test.go provides: Real app handler replay and honest corpus counts - path: ../fonoteka.go/parity/manifest.yaml provides: One declared ported route and temporary hook key_links: - from: ../fonoteka.go/parity/parity_test.go to: ../fonoteka.go/app/app.go via: newTarget boots the real app over the TestMain SQL pool - from: ../fonoteka.go/parity/manifest.yaml to: ../fonoteka.go/parity/genres_seed_test.go via: seed_hook: genres allow-list dispatch - from: ../fonoteka.go/parity/parity_test.go to: tide/replay.go via: unchanged tide.ReplayFlow against recorded fixture **As a** port developer, **I want to** roll back one plugin, parse future typed routes, and replay the PHP genres fixture against the real app, **so that** the first route is demonstrably compatible and the platform is usable for later routes.

Purpose: Complete the phase's reusable CLI/route seams and turn exactly one recorded route from pending into an honest Go pass. Output: Typed params, migration rollback/status, trusted parity seed hook, real replay target, and coverage accounting.

<execution_context> @/home/jin/.codex/get-shit-done/workflows/execute-plan.md @/home/jin/.codex/get-shit-done/templates/summary.md </execution_context>

@.planning/phases/03-first-vertical-slice-genres-end-to-end/03-CONTEXT.md @.planning/phases/03-first-vertical-slice-genres-end-to-end/03-RESEARCH.md @.planning/phases/03-first-vertical-slice-genres-end-to-end/03-VALIDATION.md @.planning/phases/03-first-vertical-slice-genres-end-to-end/03-02-SUMMARY.md `tide.Route.SeedHook` is the YAML `seed_hook` field, `seedHooks` in app `parity_test.go` is a trusted Go function map, and `replayPortedRoute` currently replays the global PHP bootstrap seed before every ported case. `newTarget(t, db)` currently returns a synthetic handler. `TestParityCorpus` assumes 154 pending and zero passes; those assertions must change when the genres route becomes ported. `tide.Store.Set(name,value)` holds the test-only JWT and captured-ID substitutions. The fixture is `fixtures/routes/get_genres_jwt.yaml` and must remain byte-for-byte unmodified. Task 1: Expose reversible plugin migrations and typed route parameters surf/params.go, lagoon/migrations.go, lagoon/commands.go, examples/hello/main.go, examples/hello/plugins/greeter/plugin.go, ../fonoteka.go/README.md surf/router.go, surf/middleware.go, lagoon/migrations.go, lagoon/commands.go, bonfire/command.go, examples/hello/main.go, examples/hello/plugins/greeter/plugin.go, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/routes.php, .planning/phases/03-first-vertical-slice-genres-end-to-end/03-CONTEXT.md, .planning/phases/03-first-vertical-slice-genres-end-to-end/03-RESEARCH.md Complete `migrate:status` and `migrate:rollback --plugin=golem15.fonoteka` on the shared bonfire runtime: status lists the applied migration IDs from each plugin history table; rollback invokes only that plugin's `gormigrate.RollbackLast()` and leaves `golem15.user` history and rows intact. Keep the same capability/order path as `migrate` and make a missing/unknown plugin a nonzero named error. In `surf`, expose an integer accessor for ServeMux `Request.PathValue(name)` with positive-ID parsing and a 404 result for malformed IDs, and route constraints for allow listed regex and enum values matching PHP `->where(...)` use. Validate constraints at registration, not by constructing SQL or regex from request text. Add a small `examples/hello` route with `{id}` and an enum or regex constraint; its handler returns 404 for both malformed and unknown IDs, exercising the public API. Add app README examples for fresh ICU `pl-PL` database creation, `migrate`, `migrate:status`, `migrate:rollback --plugin=golem15.fonoteka`, and `serve`; state that rollback of the seed removes the 15 rows without undoing the user plugin. Preserve the app's generated entry point on `summer build`. go vet ./... && go test ./... && (cd ../fonoteka.go && go vet ./... && go test ./...) - `migrate:status` reports separate user and Fonoteka migration IDs; `migrate:rollback --plugin=golem15.fonoteka` changes only Fonoteka history/data. - The `examples/hello` typed route serves a known positive integer, returns 404 for malformed and unknown IDs, and rejects a route value outside its declared constraint. - Both modules' vet/test exit 0 before commit. A developer can inspect and reverse a single plugin's migration, and route authors can use typed constrained path values. Task 2: Turn the recorded genres route into one honest Go parity pass ../fonoteka.go/parity/parity_test.go, ../fonoteka.go/parity/genres_seed_test.go, ../fonoteka.go/parity/manifest.yaml ../fonoteka.go/parity/parity_test.go, ../fonoteka.go/parity/synthetic_test.go, ../fonoteka.go/parity/manifest.yaml, ../fonoteka.go/parity/fixtures/routes/get_genres_jwt.yaml, ../fonoteka.go/parity/fixtures/seed/bootstrap.yaml, ../fonoteka.go/parity/capture-rules.yaml, ../fonoteka.go/main.go, tide/manifest.go, tide/replay.go, tide/variables.go, .planning/phases/02-api-parity-harness-bootstrap/02-CONTEXT.md, .planning/phases/03-first-vertical-slice-genres-end-to-end/03-CONTEXT.md Replace `newTarget(t, db)`'s synthetic handler with the booted real `../fonoteka.go/app` handler using the TestMain `*sql.DB` and the fixed test-only secret; test code imports the compiled plugins and loads the app manifest IDs rather than duplicating their list. Apply both plugin migrations once per isolated test state. Add idempotent `seedHooks["genres"]` in app test code: insert Alice and an owned real collection through GORM, persist active context, mint only the test JWT into `jwt:alice`, and set `id:wishlist-album=2`, `id:genre=4`, and `id:token=1` so the recorded genre IDs resolve exactly; derive those three values from the PHP seed order and fixture, not user input. Change only the `GET /_fonoteka/api/v1/genres jwt` manifest entry to `status: ported` and `seed_hook: genres`, with a comment naming the temporary replacement routes (register/login and `POST /_fonoteka/api/v1/genres`). For a ported route with a trusted route seed hook, skip the global PHP bootstrap flow that calls unported Go endpoints; invoke the route hook instead, while preserving global seed behavior for routes without a hook. Keep the 154-route manifest and recorded fixture content unchanged otherwise. Update `TestParityCorpus`'s coverage assertion to require 154 recorded, exactly 1 ported/passing and 153 pending after successful replay; increment passing only after the ported subtest actually succeeds. Retain `ported-mismatch` and `ported-missing-fixture` failure checks, and add a deliberate mutated response check to prove a false pass is caught. Do not edit `tide` or its normalization/scrubber rules. go vet ./... && go test ./... && (cd ../fonoteka.go && go vet ./... && go test ./parity -run 'TestParityCorpus' -count=1 && go test ./...) - The unchanged `get_genres_jwt.yaml` replays against the real handler with status 200, expected headers, exact genre order and JSON token values under Phase 2's diff semantics. - Corpus output/assertions report 154 recorded, 1 ported/passing, 153 pending, 0 failing and 0 unrecorded; a mutated real response fails its subtest. - The app hook uses a fixed test secret and in-memory variable store, writes no live credential to git, and does not replay unported global bootstrap endpoints for this route. - Root and app vet/test exit 0; no generic `tide` file or recorded fixture changed. The first real Go route passes the recorded PHP fixture while the other 153 remain honestly pending.

<threat_model>

Trust Boundaries

Boundary Description
CLI plugin argument → migration history An arbitrary plugin name must not select or alter another plugin's migration set.
URL path value → handler Malformed or unknown identifiers must not leak ownership information.
Recorded fixture variables → test request JWT and captured IDs are substituted only from trusted test state, never committed as live secrets.

STRIDE Threat Register

Threat ID Category Severity Component Disposition Mitigation Plan
T-03-01 Tampering high migration rollback mitigate Validate plugin ID, isolate history table, test user rows and history unchanged.
T-03-07 Information disclosure medium typed route IDs mitigate Return the same 404 for malformed and unknown ownership-scoped IDs.
T-03-06 Spoofing/Tampering high parity seed and coverage mitigate Test-only secret, trusted hook map, no fixture rewrite, pass count increments only after real replay.
</threat_model>
Run both modules' vet/test, focused real corpus replay, migration rollback/status integration, and hello typed-route tests. Confirm only the genres manifest entry changes to ported.

<success_criteria> One PHP fixture passes the real Go route; per-plugin rollback and typed params work; the remaining 153 routes remain pending. </success_criteria>

Create `.planning/phases/03-first-vertical-slice-genres-end-to-end/03-03-SUMMARY.md` after completion.