docs(03-03): complete typed params, rollback, and first real parity pass plan
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -119,7 +119,7 @@ Requirements for v1 (the Płytarium port). Each maps to roadmap phases. "User" b
|
|||||||
- [x] **QA-01**: A fixture recorder captures requests and responses from the running PHP backend for every route plus the Nuxt app's and MCP server's real flows
|
- [x] **QA-01**: A fixture recorder captures requests and responses from the running PHP backend for every route plus the Nuxt app's and MCP server's real flows
|
||||||
- [x] **QA-02**: A replay-and-diff harness runs fixtures against the Go backend with a normalizer for nondeterministic fields and assertions for the parity classes (nil vs [], date formats, tri-state booleans, envelopes, conditional keys)
|
- [x] **QA-02**: A replay-and-diff harness runs fixtures against the Go backend with a normalizer for nondeterministic fields and assertions for the parity classes (nil vs [], date formats, tri-state booleans, envelopes, conditional keys)
|
||||||
- [x] **QA-03**: go vet and go test ./... are green at every commit; each phase ends with a unit-test plan; integration tests use testcontainers Postgres
|
- [x] **QA-03**: go vet and go test ./... are green at every commit; each phase ends with a unit-test plan; integration tests use testcontainers Postgres
|
||||||
- [ ] **QA-04**: The first vertical slice (GET /_fonoteka/api/v1/genres) passes the parity diff end to end before further kernel abstraction
|
- [x] **QA-04**: The first vertical slice (GET /_fonoteka/api/v1/genres) passes the parity diff end to end before further kernel abstraction
|
||||||
- [ ] **QA-05**: Cutover: the parity harness is green on all 154 routes and vue-fonoteka-app and fonoteka-mcp run unchanged against the Go backend
|
- [ ] **QA-05**: Cutover: the parity harness is green on all 154 routes and vue-fonoteka-app and fonoteka-mcp run unchanged against the Go backend
|
||||||
|
|
||||||
## v2 Requirements
|
## v2 Requirements
|
||||||
@@ -228,7 +228,7 @@ Which phases cover which requirements. Updated during roadmap creation.
|
|||||||
| QA-01 | Phase 2 | Complete |
|
| QA-01 | Phase 2 | Complete |
|
||||||
| QA-02 | Phase 2 | Complete |
|
| QA-02 | Phase 2 | Complete |
|
||||||
| QA-03 | Phase 2 | Complete |
|
| QA-03 | Phase 2 | Complete |
|
||||||
| QA-04 | Phase 3 | Pending |
|
| QA-04 | Phase 3 | Complete |
|
||||||
| QA-05 | Phase 15 | Pending |
|
| QA-05 | Phase 15 | Pending |
|
||||||
|
|
||||||
**Coverage:**
|
**Coverage:**
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ Plans:
|
|||||||
|
|
||||||
**Wave 3** *(blocked on Wave 2 completion)*
|
**Wave 3** *(blocked on Wave 2 completion)*
|
||||||
|
|
||||||
- [ ] 03-03-PLAN.md — Add typed params, isolated rollback and the first real parity pass
|
- [x] 03-03-PLAN.md — Add typed params, isolated rollback and the first real parity pass
|
||||||
|
|
||||||
**Wave 4** *(blocked on Wave 3 completion)*
|
**Wave 4** *(blocked on Wave 3 completion)*
|
||||||
|
|
||||||
@@ -348,7 +348,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 →
|
|||||||
|-------|----------------|--------|-----------|
|
|-------|----------------|--------|-----------|
|
||||||
| 1. Framework kernel foundation | 4/4 | Complete | 2026-09-16 |
|
| 1. Framework kernel foundation | 4/4 | Complete | 2026-09-16 |
|
||||||
| 2. API parity harness bootstrap | 5/5 | Complete | 2026-09-17 |
|
| 2. API parity harness bootstrap | 5/5 | Complete | 2026-09-17 |
|
||||||
| 3. First vertical slice — genres end to end | 2/4 | In Progress| |
|
| 3. First vertical slice — genres end to end | 3/4 | In Progress| |
|
||||||
| 4. CLI scaffolding, i18n and mail | 0/TBD | Not started | - |
|
| 4. CLI scaffolding, i18n and mail | 0/TBD | Not started | - |
|
||||||
| 5. Data layer full fidelity | 0/TBD | Not started | - |
|
| 5. Data layer full fidelity | 0/TBD | Not started | - |
|
||||||
| 6. HTTP routing, auth groups and rate limiting | 0/TBD | Not started | - |
|
| 6. HTTP routing, auth groups and rate limiting | 0/TBD | Not started | - |
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ gsd_state_version: 1.0
|
|||||||
milestone: v1.0
|
milestone: v1.0
|
||||||
milestone_name: milestone
|
milestone_name: milestone
|
||||||
status: executing
|
status: executing
|
||||||
stopped_at: Completed 03-02-PLAN.md
|
stopped_at: Completed 03-03-PLAN.md
|
||||||
last_updated: "2026-09-17T18:14:02.630Z"
|
last_updated: "2026-09-17T18:23:37.257Z"
|
||||||
last_activity: 2026-09-17
|
last_activity: 2026-09-17
|
||||||
progress:
|
progress:
|
||||||
total_phases: 15
|
total_phases: 15
|
||||||
@@ -26,7 +26,7 @@ See: .planning/PROJECT.md (updated 2026-09-16)
|
|||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Phase: 03 (first-vertical-slice-genres-end-to-end) — EXECUTING
|
Phase: 03 (first-vertical-slice-genres-end-to-end) — EXECUTING
|
||||||
Plan: 3 of 4
|
Plan: 4 of 4
|
||||||
Status: Ready to execute
|
Status: Ready to execute
|
||||||
Last activity: 2026-09-17
|
Last activity: 2026-09-17
|
||||||
|
|
||||||
@@ -55,6 +55,7 @@ Progress: [█████████░] 85%
|
|||||||
*Updated after each plan completion*
|
*Updated after each plan completion*
|
||||||
| Phase 03 P03-01 | 17 min | 2 tasks | 52 files |
|
| Phase 03 P03-01 | 17 min | 2 tasks | 52 files |
|
||||||
| Phase 03 P03-02 | 5 min | 2 tasks | 8 files |
|
| Phase 03 P03-02 | 5 min | 2 tasks | 8 files |
|
||||||
|
| Phase 03 P03-03 | 8 min | 2 tasks | 17 files |
|
||||||
|
|
||||||
## Accumulated Context
|
## Accumulated Context
|
||||||
|
|
||||||
@@ -91,6 +92,10 @@ Recent decisions affecting current work:
|
|||||||
- [Phase 03]: lagoon.OrderBy takes a caller allow-list so the framework never hardcodes Fonoteka table names; the handler passes PHP PolishOrder::ALLOWED_COLUMNS — summercms.go must stay app-agnostic; PolishOrder columns live at the Fonoteka call site
|
- [Phase 03]: lagoon.OrderBy takes a caller allow-list so the framework never hardcodes Fonoteka table names; the handler passes PHP PolishOrder::ALLOWED_COLUMNS — summercms.go must stay app-agnostic; PolishOrder columns live at the Fonoteka call site
|
||||||
- [Phase 03]: Duplicate non_empty query keys last-win, matching PHP parse_str; invalid then 1 is accepted, 1 then invalid is 422 — PHP parse_str last-wins confirmed with php -r; Go uses vals[len(vals)-1]
|
- [Phase 03]: Duplicate non_empty query keys last-win, matching PHP parse_str; invalid then 1 is accepted, 1 then invalid is 422 — PHP parse_str last-wins confirmed with php -r; Go uses vals[len(vals)-1]
|
||||||
- [Phase 03]: Invalid stored context is rewritten to the lowest-ID accessible kind=collection row; auto-provisioning stays out of this slice — Plan 03-02 ports only the JWT default resolve path; CollectionProvisioner is Phase 12
|
- [Phase 03]: Invalid stored context is rewritten to the lowest-ID accessible kind=collection row; auto-provisioning stays out of this slice — Plan 03-02 ports only the JWT default resolve path; CollectionProvisioner is Phase 12
|
||||||
|
- [Phase 03]: Route constraints compile regex and enum allow-lists at registration; request path text is only matched — D-15 T-03-07: PHP ->where() maps onto surf.Where/WhereIn; malformed and unknown IDs share a 404
|
||||||
|
- [Phase 03]: A ported route with a trusted seed_hook skips the global PHP bootstrap replay — D-20: unported register/login and POST genres; the hook mints a test-only JWT
|
||||||
|
- [Phase 03]: Corpus passing increments only after the ported subtest succeeds; pending never counts as passing — QA-04 T-03-06: 154 recorded, 1 passing, 153 pending
|
||||||
|
- [Phase 03]: Colliding fixture IDs are derived from CanonicalGenres seed order (rock=1, electronic=2, jazz=4) — D-20: set id:token, id:wishlist-album, id:genre from PHP seed order, not user input
|
||||||
|
|
||||||
### Pending Todos
|
### Pending Todos
|
||||||
|
|
||||||
@@ -112,6 +117,6 @@ Items acknowledged and carried forward from previous milestone close:
|
|||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-09-17T18:13:51.817Z
|
Last session: 2026-09-17T18:23:37.224Z
|
||||||
Stopped at: Completed 03-02-PLAN.md
|
Stopped at: Completed 03-03-PLAN.md
|
||||||
Resume file: None
|
Resume file: None
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
---
|
||||||
|
phase: 03-first-vertical-slice-genres-end-to-end
|
||||||
|
plan: 03
|
||||||
|
subsystem: api
|
||||||
|
tags: [servemux, typed-params, gormigrate, rollback, parity, jwt, seed-hook]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: 03-first-vertical-slice-genres-end-to-end
|
||||||
|
provides: Shared pgx-stdlib pool, JWT genre route, tenant-scoped album counts
|
||||||
|
- phase: 02-api-parity-harness-bootstrap
|
||||||
|
provides: App-owned newTarget/seedHooks seam and recorded GET genres fixture
|
||||||
|
provides:
|
||||||
|
- Typed integer path params plus registration-time regex and enum constraints
|
||||||
|
- Per-plugin migrate:status IDs and migrate:rollback --plugin isolation
|
||||||
|
- Temporary trusted genres seed hook and real-app fixture replay
|
||||||
|
- Honest corpus counts: 154 recorded, 1 ported/passing, 153 pending
|
||||||
|
affects: [03-04, 05, 06, 07, 12]
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- surf.Where/WhereIn compile constraints at registration; request text never builds regex or SQL
|
||||||
|
- Ported routes with seed_hook skip global PHP bootstrap replay
|
||||||
|
- Corpus passing increments only after the ported subtest succeeds
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- surf/params.go
|
||||||
|
- ../fonoteka.go/parity/genres_seed_test.go
|
||||||
|
modified:
|
||||||
|
- surf/router.go
|
||||||
|
- pact/capabilities.go
|
||||||
|
- lagoon/migrations.go
|
||||||
|
- lagoon/commands.go
|
||||||
|
- examples/hello/plugins/greeter/plugin.go
|
||||||
|
- ../fonoteka.go/parity/parity_test.go
|
||||||
|
- ../fonoteka.go/parity/manifest.yaml
|
||||||
|
- ../fonoteka.go/README.md
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Route constraints compile regex and enum allow-lists at registration; request path text is only matched"
|
||||||
|
- "A ported route with a trusted seed_hook skips the global PHP bootstrap replay"
|
||||||
|
- "Corpus passing increments only after the ported subtest succeeds; pending never counts as passing"
|
||||||
|
- "Colliding fixture IDs are derived from CanonicalGenres seed order (rock=1, electronic=2, jazz=4)"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Pattern: pact.Router.Where/WhereIn plus surf.IntParam; malformed and unknown IDs both 404"
|
||||||
|
- "Pattern: lagoon.ErrUnknownPlugin named error; rollback of golem15.fonoteka seed leaves user history intact"
|
||||||
|
- "Pattern: seedHooks[\"genres\"] mints a test-only JWT into an in-memory tide.Store"
|
||||||
|
|
||||||
|
requirements-completed: [DATA-02, HTTP-01, HTTP-02, QA-04]
|
||||||
|
|
||||||
|
duration: 8min
|
||||||
|
completed: 2026-09-17
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 03 Plan 03: Typed params, isolated rollback, first real parity pass Summary
|
||||||
|
|
||||||
|
**Typed ServeMux params, per-plugin `migrate:rollback`, and one honest Go pass of the recorded PHP genres fixture (154 recorded, 1 passing, 153 pending)**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 8 min
|
||||||
|
- **Started:** 2026-09-17T18:14:59Z
|
||||||
|
- **Completed:** 2026-09-17T18:23:20Z
|
||||||
|
- **Tasks:** 2
|
||||||
|
- **Files modified:** 17
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- `surf.IntParam`, `Where`, and `WhereIn` compile regex/enum constraints at registration; `examples/hello` serves `/items/1`, 404s malformed and unknown IDs, and rejects enum values outside the allow-list.
|
||||||
|
- `migrate:status` lists each plugin's applied IDs; `migrate:rollback --plugin=golem15.fonoteka` rolls back only the genre seed and returns `lagoon.ErrUnknownPlugin` for a missing plugin.
|
||||||
|
- `newTarget` boots the real app; the temporary `genres` hook seeds Alice, an owned collection, a test-only JWT, and colliding IDs from CanonicalGenres order. The unmodified `get_genres_jwt.yaml` passes. Corpus: 154 recorded, 1 ported/passing, 153 pending.
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically (framework `summercms.go` then app `fonoteka.go` when both change):
|
||||||
|
|
||||||
|
1. **Task 1: Expose reversible plugin migrations and typed route parameters**
|
||||||
|
- `8e3bf266d860c7416e8988845b70b190535c4b55` (feat, summercms.go)
|
||||||
|
- `91e3df41e33c85818f4cc3470668853f04b67315` (feat, fonoteka.go)
|
||||||
|
2. **Task 2: Turn the recorded genres route into one honest Go parity pass**
|
||||||
|
- `0cc1a4da84ced8ed2451f2ea1c9a045f1c2ea682` (feat, fonoteka.go)
|
||||||
|
|
||||||
|
**Plan metadata:** (this commit)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `surf/params.go` — `IntParam`, `Regex`, `Enum`; constraints compiled at registration
|
||||||
|
- `pact/capabilities.go` — `Router.Where` / `WhereIn`
|
||||||
|
- `lagoon/migrations.go` — `ErrUnknownPlugin`, `ErrNoMigrations`, isolated `RollbackLast`
|
||||||
|
- `lagoon/commands.go` — `migrate:status` prints every applied ID
|
||||||
|
- `examples/hello/plugins/greeter/plugin.go` — `{id}` regex route and enum `/kinds/{kind}`
|
||||||
|
- `../fonoteka.go/parity/genres_seed_test.go` — trusted GORM Alice/collection hook and test-only JWT
|
||||||
|
- `../fonoteka.go/parity/parity_test.go` — real `app.Handler`, skip global bootstrap when `seed_hook` is set, passing count after successful replay, mutated-response check
|
||||||
|
- `../fonoteka.go/parity/manifest.yaml` — `GET /_fonoteka/api/v1/genres jwt` is `ported` with `seed_hook: genres`
|
||||||
|
- `../fonoteka.go/README.md` — ICU `pl-PL` create, `migrate`, `migrate:status`, `migrate:rollback --plugin=golem15.fonoteka`, `serve`
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
- Constraints are compiled when the route is registered; enum values are an allow-list, not a regex built from request text.
|
||||||
|
- A ported route with `seed_hook` does not replay unported global bootstrap endpoints.
|
||||||
|
- Passing is incremented only after the ported subtest succeeds.
|
||||||
|
- Fixture collisions `id:token=1`, `id:wishlist-album=2`, `id:genre=4` come from CanonicalGenres insertion order.
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [Rule 3 - Blocking] Synthetic proof tests kept `newSyntheticHandler` after `newTarget` became the real app**
|
||||||
|
- **Found during:** Task 2 (real handler swap)
|
||||||
|
- **Issue:** `TestParitySynthetic` and the contract `synthetic-postgres` subtest posted `/synthetic/items` through `newTarget`. The real app does not mount those routes.
|
||||||
|
- **Fix:** Call `newSyntheticHandler` for the Phase 2 synthetic proof; `newTarget` boots `app.Handler`.
|
||||||
|
- **Files modified:** `../fonoteka.go/parity/synthetic_test.go`, `../fonoteka.go/parity/parity_contract_test.go`, `../fonoteka.go/parity/parity_test.go`
|
||||||
|
- **Verification:** `go test ./parity` including `TestParitySynthetic` and `TestParityCorpus`
|
||||||
|
- **Committed in:** `0cc1a4d` (Task 2)
|
||||||
|
|
||||||
|
**2. [Rule 3 - Blocking] Rollback isolation uses a dedicated `rollback_iso` database**
|
||||||
|
- **Found during:** Task 1 (rollback test)
|
||||||
|
- **Issue:** Shared TestMain Postgres would lose the 15 genre rows (and shift serial IDs) if seed rollback ran in-place.
|
||||||
|
- **Fix:** Create ICU `pl-PL` database `rollback_iso`, migrate/rollback there, leave the parity pool untouched.
|
||||||
|
- **Files modified:** `../fonoteka.go/parity/migrate_test.go`
|
||||||
|
- **Verification:** `TestRollbackLastIsolatesFonoteka` plus later genre/parity tests still see ids 1–15
|
||||||
|
- **Committed in:** `91e3df4` (Task 1)
|
||||||
|
|
||||||
|
**3. [Discretion] `pact.Router` gained `Where`/`WhereIn`**
|
||||||
|
- **Found during:** Task 1 (typed params)
|
||||||
|
- **Issue:** Plan listed `surf/params.go` but plugins receive `pact.Router`; PHP `->where()` needs a method on that interface.
|
||||||
|
- **Fix:** Add `Where`/`WhereIn` to `pact.Router` and implement them on `*surf.Router`/`*surf.Group`.
|
||||||
|
- **Files modified:** `pact/capabilities.go`, `surf/router.go`
|
||||||
|
- **Verification:** hello typed-route tests and `go vet ./...`
|
||||||
|
- **Committed in:** `8e3bf26` (Task 1)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Total deviations:** 3 (2 blocking test-isolation, 1 interface completeness)
|
||||||
|
**Impact on plan:** No scope creep. QA-04 is green on one route; 153 remain pending.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None that blocked the slice. `go.work.sum` and `.planning/config.json` `_auto_chain_active` were left uncommitted.
|
||||||
|
|
||||||
|
## User Setup Required
|
||||||
|
|
||||||
|
None - no external service configuration required beyond the existing Postgres ICU `pl-PL` database.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
Ready for `03-04-PLAN.md` (unit, integration, and security verification). The genres corpus route is `ported`. Do not treat pending routes as passing.
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
- Key files exist on disk (`surf/params.go`, `lagoon/commands.go`, `../fonoteka.go/parity/genres_seed_test.go`, `../fonoteka.go/parity/parity_test.go`)
|
||||||
|
- `git log --grep=03-03` returns Task 1 and Task 2 commits in both repos
|
||||||
|
- Acceptance: per-plugin rollback isolation, hello typed/enum 404s, unmodified `get_genres_jwt.yaml` 200 against the real handler, corpus 154/1/153, mutated response fails, no tide or fixture edits, `go vet`/`go test ./...` green in both repos
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 03-first-vertical-slice-genres-end-to-end*
|
||||||
|
*Completed: 2026-09-17*
|
||||||
Reference in New Issue
Block a user