docs(08-09): complete parity-and-real-mcp-gate plan

This commit is contained in:
Jakub Zych
2026-09-23 23:22:53 +02:00
parent e87346f9e3
commit d358bbf907
3 changed files with 270 additions and 9 deletions

View File

@@ -354,7 +354,7 @@ Plans:
**Wave 8** *(blocked on 08-07 and 08-08)*
- [ ] 08-09-PLAN.md — Replay PHP OAuth flows and assemble the self-validating final unchanged-MCP gate
- [x] 08-09-PLAN.md — Replay PHP OAuth flows and assemble the self-validating final unchanged-MCP gate
**Wave 9** *(blocked on 08-09; blocking security checkpoint)*
@@ -496,7 +496,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 →
| 5. Data layer full fidelity | 6/6 | Complete | 2026-09-18 |
| 6. HTTP routing, auth groups and rate limiting | 14/14 | Complete | 2026-09-21 |
| 7. User plugin and authentication | 8/8 | Complete | 2026-09-23 |
| 8. OAuth2.1 authorization server | 8/10 | In Progress| |
| 8. OAuth2.1 authorization server | 9/10 | In Progress| |
| 9. Backend admin authentication and schema pipeline | 0/TBD | Not started | - |
| 10. Admin Vue SPA | 0/TBD | Not started | - |
| 11. Jobs, realtime and search infrastructure | 0/TBD | Not started | - |

View File

@@ -3,14 +3,14 @@ gsd_state_version: 1.0
milestone: v1.0
milestone_name: milestone
status: executing
stopped_at: Completed 08-08-PLAN.md
last_updated: "2026-09-23T20:23:27.498Z"
stopped_at: Completed 08-09-PLAN.md
last_updated: "2026-09-23T21:22:41.461Z"
last_activity: 2026-09-23
progress:
total_phases: 15
completed_phases: 7
total_plans: 55
completed_plans: 53
completed_plans: 54
percent: 47
---
@@ -26,11 +26,11 @@ See: .planning/PROJECT.md (updated 2026-09-16)
## Current Position
Phase: 08 (oauth2-1-authorization-server) — EXECUTING
Plan: 9 of 10
Plan: 10 of 10
Status: Ready to execute
Last activity: 2026-09-23
Progress: [██████████] 96%
Progress: [██████████] 98%
## Performance Metrics
@@ -99,6 +99,7 @@ Progress: [██████████] 96%
| Phase 08 P06 | 50min | 3 tasks | 10 files |
| Phase 08 P07 | 35min | 2 tasks | 9 files |
| Phase 08 P08 | 20min | 2 tasks | 4 files |
| Phase 08 P09 | 55min | 3 tasks | 25 files |
## Accumulated Context
@@ -248,6 +249,13 @@ Recent decisions affecting current work:
- [Phase ?]: [Phase 08 P08]: me_token_controller.go was created in the Task 1 RED commit as a compiling 501 stub (Rule 3), following the 08-04/08-06/08-07 precedent -- controllers/api must compile with its new test file while the route stays unmounted
- [Phase ?]: [Phase 08 P08]: scopes/collection_ids on GET /api/v1/fonoteka/me always serialize via wire.Slice (nil -> []), a deliberate divergence from PHP's collection_ids null-means-unrestricted semantics, accepted because fonoteka-mcp's TS MeResponse type never reads collection_ids
- [Phase ?]: [Phase 08 P08]: AUTH-07 stays Pending in REQUIREMENTS.md -- this plan ships the /me prerequisite but the requirement's 'fonoteka-mcp completes its install and auth flow unchanged' clause needs 08-09's real MCP gate
- [Phase ?]: [Phase 08 P09]: Lifecycle fixture recorded via a one-time Go program calling tide.RecordFlow directly against isolated PHP (deleted after use), not via Playwright/capture_clients.mjs -- login and consent are plain JWT API calls with no browser dependency
- [Phase ?]: [Phase 08 P09]: wristband's explicit no-store Cache-Control becomes no-store, private and unheadered JSON errors default to no-cache, private -- confirmed by live PHP recording, superseding the earlier 08-CONTEXT.md assumption of bare no-store
- [Phase ?]: [Phase 08 P09]: wristband redirects now emit Symfony's exact HTML redirect body (wristband/redirect_html.go) with PHP htmlspecialchars(ENT_QUOTES) escaping -- Go's bare 302 never matched real PHP
- [Phase ?]: [Phase 08 P09]: {request_id} route constraint is now upper-bound only ([A-Za-z0-9_-]{1,128}) -- PHP applies no router-level shape constraint and the 16-char lower bound was rejecting a valid recorded 404 test case at the router
- [Phase ?]: [Phase 08 P09]: OAuthConsentController's basic-validation failure now writes Winter's generic production 500 HTML page (not a clean 422) -- PHP's bare $request->validate() on this route is never caught by a JSON exception renderer, confirmed live with APP_DEBUG=false
- [Phase ?]: [Phase 08 P09]: All nine OAuth manifest routes are status: ported with seed_hook: genres; scripts/check-phase8.sh's stage bodies are fully implemented but never executed by this plan -- 08-10 Task 3 is the sole execution site
- [Phase ?]: [Phase 08 P09]: AUTH-05/AUTH-06/AUTH-07 remain Pending in REQUIREMENTS.md -- this plan proves byte parity and builds the real-MCP gate machinery, but only 08-10's actual gate execution can prove the unchanged-fonoteka-mcp clause
### Pending Todos
@@ -269,6 +277,6 @@ Items acknowledged and carried forward from previous milestone close:
## Session Continuity
Last session: 2026-09-23T20:23:27.474Z
Stopped at: Completed 08-08-PLAN.md
Last session: 2026-09-23T21:22:41.441Z
Stopped at: Completed 08-09-PLAN.md
Resume file: None

View File

@@ -0,0 +1,253 @@
---
phase: 08-oauth2-1-authorization-server
plan: 09
subsystem: auth
tags: [oauth2, parity, tide, wristband, mcp, playwright-free-recording, gate]
# Dependency graph
requires:
- phase: 08-oauth2-1-authorization-server
plan: 08
provides: "GET /api/v1/fonoteka/me prerequisite the real MCP gate needs, and the complete authorize/token/consent/register/revoke/refresh-rotation surface from 08-01..08-07"
provides:
- "fixtures/mcp/mcp-lifecycle.yaml: a 17-step, Go-recorded (no Playwright) lifecycle fixture covering DCR, authorize, consent, token, refresh, replay-kill, connected-apps list/revoke, post-revoke failure, deny, and a confidential client_secret_basic client exercising scope-ceiling truncation and invalid_scope"
- "TestOAuthFlows (parity/oauth_flow_test.go): replays that fixture byte-for-byte against the assembled Go app on real Postgres, plus named projections of mcp-oauth/mcp-tools that fail closed if a required step disappears"
- "Nine OAuth parity/manifest.yaml route entries (4 raw + 5 JWT-group) flipped pending -> ported, each replaying their own single-case fixture cleanly"
- "wristband byte-contract fixes: Cache-Control no-store/no-cache both gain the Laravel-session ', private' suffix live PHP actually sends; every wristband redirect now emits Symfony's exact HTML redirect body (wristband/redirect_html.go)"
- "tide.isIDKey now also masks '_ids' plural array fields (e.g. collection_ids), closing a parity-corpus normalization gap"
- "scripts/check-phase8.sh: the complete fail-closed Phase 8 final gate (disposable Postgres, assembled app, real fonoteka-mcp, full scripted SDK lifecycle via the new check-phase8-mcp-client.mjs driver, both repos' vet/test/race, parity/corpus/secret-scan, UI harness, unchanged-client diff, security-review gate) plus --contract-self-test and the permanent --red-contract self-test"
affects: [08-10-unit-tests-and-security-review]
# Tech tracking
tech-stack:
added: []
patterns:
- "Lifecycle fixtures for multi-step stateful flows (PKCE, rotation, revoke) are recorded directly via tide.RecordFlow against isolated PHP from a small one-time Go program, not via Playwright/capture_clients.mjs -- login and consent are plain JWT-authenticated JSON calls with no browser dependency, and PKCE verifier/challenge pairs are generated and pre-seeded into the vars store before recording."
- "scripts/check-phase8-mcp-client.mjs: a single stateful Node driver, invoked once per named stage with a shared JSON state file, resolves the MCP SDK's auth helpers from fonoteka-mcp's own node_modules (same resolution pattern as capture_clients.mjs) so the framework gate never adds a Node dependency of its own."
- "Winter's generic production 500 HTML page (already embedded once in the user plugin) is now also embedded directly in fonoteka's controllers/api package for the one route (OAuthConsentController::store) whose bare Laravel validate() call crashes to that page instead of a clean JSON 422 -- duplicated per plugin rather than shared cross-plugin, preserving plugin independence."
key-files:
created:
- ../fonoteka.go/parity/fixtures/mcp/mcp-lifecycle.yaml
- ../fonoteka.go/parity/oauth_flow_test.go
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/winter_error_page.html
- wristband/redirect_html.go
- scripts/check-phase8.sh
- scripts/check-phase8-mcp-client.mjs
modified:
- ../fonoteka.go/parity/manifest.yaml
- ../fonoteka.go/parity/migrate_test.go
- ../fonoteka.go/parity/parity_contract_test.go
- ../fonoteka.go/parity/parity_test.go
- ../fonoteka.go/parity/fixtures/routes/GET___fonoteka_api_v1_oauth_connected-apps_jwt.yaml
- ../fonoteka.go/parity/fixtures/routes/POST___fonoteka_api_v1_oauth_consent_jwt.yaml
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go
- ../fonoteka.go/plugins/golem15/fonoteka/routes.go
- ../fonoteka.go/plugins/golem15/fonoteka/oauth_authorize_test.go
- ../fonoteka.go/plugins/golem15/fonoteka/oauth_registration_test.go
- wristband/authorize.go
- wristband/register.go
- wristband/token.go
- wristband/authorize_test.go
- wristband/registration_test.go
- wristband/token_test.go
- tide/normalize.go
key-decisions:
- "Recorded the lifecycle fixture with a one-time Go program calling tide.RecordFlow directly (deleted after use, never committed) rather than extending capture_clients.mjs's Playwright-driven flow -- D-16's own scope only requires the Phase 2 tide tooling, and login/consent are plain JWT API calls with no UI dependency, so a Go recorder is simpler and more robust than browser automation. Documented here as a deviation from the plan's literal capture_clients.mjs file-list entry."
- "list runs before refresh/replay in the recorded lifecycle (not after, as D-16's prose ordering suggests): PHP's RevokeLineage walks forward from a replayed spent refresh row and revokes every descendant's access token too, including the then-current terminal row, so connected-apps would already be empty if list ran after replay. Revoke's own DELETE still succeeds afterward regardless (PHP's destroy() query has no revoked_at filter), so revoke and refresh-after-revoke stay after replay."
- "wristband's 'no-store' Cache-Control values become 'no-store, private', and its unheadered JSON error responses (invalid_client, invalid_grant, etc.) gain 'no-cache, private' -- both confirmed by live-recording against real isolated PHP, not assumed from source reading. The earlier 08-CONTEXT.md <specifics> wording ('Cache-Control: no-store') is superseded by this live-recorded byte contract."
- "{request_id}'s router-level [A-Za-z0-9_-]{16,128} constraint (08-UI-SPEC.md) is now upper-bound only ({1,128}): real PHP applies no router-level shape constraint at all and returns the controller's clean 404 JSON for any non-matching string, including a short/malformed one; the 16-char lower bound was silently rejecting that case at the router with a bare text/plain 404."
- "OAuthConsentController's basic-validation failure (missing/invalid request_id or scopes) now writes Winter's generic production 500 HTML page instead of a clean 422: PHP's bare $request->validate() on this specific route is never caught by a JSON exception renderer, confirmed live against isolated PHP with APP_DEBUG=false. Domain-level checks (Request not found, No grantable scopes) are unaffected and keep their existing clean JSON responses."
- "Two pre-existing Phase 2 single-case OAuth fixtures were stale, not stale-fixture-but-correct: the consent 500 page had been recorded with APP_DEBUG=true (a full debug stack trace) and the connected-apps manual_tokens_count had been over-scrubbed to a coincidental {{id:alice}} placeholder. Both were re-recorded against live PHP rather than reverse-engineered from the old bytes."
- "All nine newly-ported OAuth manifest routes gained seed_hook: genres (matching the corpus's existing convention) because none of them can fall through to the manifest's global onboarding-bootstrap seed, which no Go route currently implements."
- "tide.isIDKey now masks '_ids' plural array fields the same way it already masks singular '_id'/'id' fields -- no prior fixture in the corpus had exercised a literal, non-empty, non-placeholder array-of-ids value until this plan's connected-apps collection_ids field."
- "scripts/check-phase8.sh's stage bodies are fully implemented (not stubs) as required by Task 3's 'finish the executable final gate', but this plan never executes run_full_gate end to end -- only --contract-self-test and --red-contract, exactly as the plan's own verification section specifies. 08-10 Task 3 is the first and sole real execution."
- "AUTH-05/AUTH-06/AUTH-07 remain Pending in REQUIREMENTS.md: this plan builds and structurally self-validates the real-MCP gate machinery (D-14) but does not execute it against the real unchanged fonoteka-mcp process, which is the only thing that can actually prove those requirements' 'unchanged fonoteka-mcp completes its install/auth flow' clause. That execution is 08-10 Task 3's job."
patterns-established:
- "Go-recorded (Playwright-free) multi-step lifecycle fixtures for JWT-authenticated flows: generate PKCE pairs and any CLI-issued credentials up front, pre-seed a private tide.Store, define each step's Capture rules explicitly to avoid capture-rules.yaml route-match surprises, and call tide.RecordFlow directly against isolated PHP."
- "scripts/check-phase8-mcp-client.mjs's --stage/shared-state-file shape is the template for any future gate needing a stateful multi-step real-client script driven from a bash stage sequence."
requirements-completed: []
# Metrics
duration: ~55min
completed: 2026-09-23
---
# Phase 08 Plan 09: Parity and Real-MCP Gate Summary
**A Go-recorded (no Playwright) 17-step mcp-lifecycle fixture replays byte-for-byte against the assembled Go app, flipping all nine OAuth manifest routes to ported after fixing three genuine wristband/routing byte-contract bugs the live recording uncovered, and the complete scripted-SDK scripts/check-phase8.sh final gate is built and self-validated (never executed) for 08-10.**
## Performance
- **Duration:** ~55 min
- **Started:** ~2026-09-23T20:23:00Z (approx., following 08-08's completion)
- **Completed:** 2026-09-23T21:18:44Z
- **Tasks:** 3 completed (4 commits: RED x2 in Task 1, GREEN x1 in Task 2, GREEN x1 in Task 3)
- **Files modified:** 25 (7 created, 18 modified, across both repos)
## Accomplishments
- Recorded `fixtures/mcp/mcp-lifecycle.yaml` against real isolated PHP with a one-time Go program calling `tide.RecordFlow` directly (deleted after use): DCR -> authorize -> consent -> token -> connected-apps list (showing the live app) -> refresh -> replay of the now-spent refresh token (`invalid_grant`, lineage dead) -> revoke -> refresh-after-revoke failure -> a deny path, plus a confidential `client_secret_basic` client issued via `fonoteka:oauth-client`'s exact issuance path exercising scope-ceiling truncation (`read write ai` -> `read write`) and the `invalid_scope` redirect (`ai` alone, fully outside the ceiling).
- `TestOAuthFlows` replays that fixture byte-for-byte against the real assembled Go app on testcontainers Postgres, and re-asserts named projections of the existing `mcp-oauth`/`mcp-tools` fixtures fail closed if a required step disappears.
- The live recording surfaced three genuine, previously-undetected byte-contract gaps between wristband's assumed behavior and real PHP: every explicit `Cache-Control: no-store` PHP sets actually arrives as `no-store, private` (Laravel's session-cookie default merge), unheadered JSON error responses default to `no-cache, private` rather than nothing, and every PHP redirect (authorize success and error) carries Symfony's exact HTML redirect body with `Content-Type: text/html; charset=utf-8` that Go's bare 302 never sent. All three are now fixed (`wristband/redirect_html.go` is new) and every affected `wristband`/app-level unit test assertion was updated to the corrected expected bytes.
- Two more real bugs surfaced once the nine OAuth routes were exercised for the first time: a router-level `{request_id}` length constraint rejected a valid PHP 404 test case before it reached the controller (now upper-bound only), and `OAuthConsentController::store`'s basic validation failure needed to crash to Winter's generic production 500 HTML page (reusing the exact byte-identical page the user plugin already embeds) rather than return a clean 422, matching real PHP's uncaught-`ValidationException` behavior on this specific route.
- All nine OAuth `parity/manifest.yaml` entries (4 raw + 5 JWT-group) are `status: ported`, each gaining `seed_hook: genres`; `TestParityCorpus` reports `recorded 169/169 passing 31 failing 0 unrecorded 0 pending 138` with zero regressions across both full `go test ./...` (root + all workspace modules) and the touched `-race` packages.
- `scripts/check-phase8.sh` is the complete fail-closed final gate: every stage from `docker-preflight` through `security-review` has a real implementation (disposable Postgres, the built-and-served Go app, the real unchanged `fonoteka-mcp` process, the full scripted SDK lifecycle delegated to the new `scripts/check-phase8-mcp-client.mjs`, both repositories' `vet`/`test`/`-race`, the parity/corpus/secret-scan gate, the existing `check-phase8-ui.mjs --final-gate` UI harness, an unchanged-client git-diff check, and a `08-SECURITY-REVIEW.md status: verified` gate). `--contract-self-test` validates structure only (stage names/order, cleanup trap, loopback-only binding, the three MCP env vars, the redaction helper, no pre-final full-run flag) in ~85ms with no services booted; the permanent `--red-contract` self-test from Task 1 still passes unchanged. `run_full_gate` is never invoked by this plan.
## Task Commits
1. **Task 1: RED parity-gate anchor** (both repos)
- `d9b168f` (test, fonoteka.go): `TestPhase8RedParityGate` fails closed with `PHASE8_RED:parity-gate` while `mcp-lifecycle.yaml` doesn't exist yet; verified via `scripts/check-phase8-red.sh` go mode.
- `246a488` (test, summercms.go): `scripts/check-phase8.sh` skeleton with the ordered stage list and the permanent `--red-contract` self-test; verified via `scripts/check-phase8-red.sh` shell mode (exit 86, exact `PHASE8_STAGE:real-mcp:FAIL:PHASE8_RED:real-mcp-stage` line).
2. **Task 2: record and replay the full lifecycle** (both repos)
- `6cc07a4` (fix, summercms.go): the three wristband byte-contract fixes (`redirect_html.go`, Cache-Control corrections) plus the `tide.isIDKey` `_ids` masking fix, all confirmed by the live recording.
- `23e7885` (feat, fonoteka.go): the recorded fixture, `TestOAuthFlows`, the nine manifest flips, the two re-recorded stale fixtures, the `OAuthConsentController`/`routes.go` fixes, and the `parity_test.go`/`parity_contract_test.go` count updates.
3. **Task 3: complete the final gate** (summercms.go)
- `e87346f` (feat): all `scripts/check-phase8.sh` stage bodies plus `scripts/check-phase8-mcp-client.mjs`.
**Plan metadata:** committed as part of this summary/state-update commit.
_Note: Task 1 and Task 2 both carry `tdd="true"`; RED/GREEN pairs land as separate commits, split per repo. Task 3 (`type="auto"`, no `tdd`) is a single commit._
## Files Created/Modified
- `../fonoteka.go/parity/fixtures/mcp/mcp-lifecycle.yaml` -- the 17-step recorded lifecycle fixture
- `../fonoteka.go/parity/oauth_flow_test.go` -- `TestPhase8RedParityGate`, `TestOAuthFlows`, projection helpers, `issueConfidentialClient`, `pkcePair`, `upsertParityDefaultCollection`
- `../fonoteka.go/parity/manifest.yaml` -- nine OAuth route entries `status: ported` + `seed_hook: genres`
- `../fonoteka.go/parity/migrate_test.go` -- `testConfig` now sets `app.url` to match isolated PHP's fixed origin
- `../fonoteka.go/parity/parity_test.go` / `parity_contract_test.go` -- `expectedPortedRoutes` 22 -> 31 and the ported-route allow-list
- `../fonoteka.go/parity/fixtures/routes/GET___fonoteka_api_v1_oauth_connected-apps_jwt.yaml` / `POST___fonoteka_api_v1_oauth_consent_jwt.yaml` -- re-recorded against live PHP
- `../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go` + new `winter_error_page.html` -- basic-validation-failure now matches PHP's crash-to-500 behavior
- `../fonoteka.go/plugins/golem15/fonoteka/routes.go` -- `{request_id}` constraint is upper-bound only
- `../fonoteka.go/plugins/golem15/fonoteka/oauth_authorize_test.go` / `oauth_registration_test.go` -- updated Cache-Control expectations
- `wristband/redirect_html.go` -- Symfony-exact HTML redirect body + PHP `htmlspecialchars(ENT_QUOTES)` port
- `wristband/authorize.go` / `register.go` / `token.go` + their `_test.go` files -- corrected Cache-Control byte contract
- `tide/normalize.go` -- `isIDKey` masks `_ids` plural arrays
- `scripts/check-phase8.sh` -- the complete final gate script
- `scripts/check-phase8-mcp-client.mjs` -- the stateful scripted-SDK MCP client driver
## Decisions Made
See frontmatter `key-decisions`. Most load-bearing: the lifecycle fixture was recorded via a one-time Go program (not Playwright/`capture_clients.mjs`), and every one of the five byte-contract/routing bugs this plan fixed was confirmed against real live PHP output before being fixed in Go -- none were guessed from source reading alone.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] wristband Cache-Control values didn't match live PHP**
- **Found during:** Task 2, first `TestOAuthFlows` replay attempt
- **Issue:** `wristband` set bare `Cache-Control: no-store` and left unheadered JSON errors with no Cache-Control at all; real PHP (confirmed via the live recording and cross-checked against Phase-2-recorded single-case fixtures already in git) sends `no-store, private` and `no-cache, private` respectively.
- **Fix:** `authorize.go`, `register.go`, `token.go` updated; every affected `wristband` and app-level unit test assertion updated to match.
- **Files modified:** `wristband/authorize.go`, `wristband/register.go`, `wristband/token.go`, `wristband/authorize_test.go`, `wristband/registration_test.go`, `wristband/token_test.go`, `../fonoteka.go/plugins/golem15/fonoteka/oauth_authorize_test.go`, `../fonoteka.go/plugins/golem15/fonoteka/oauth_registration_test.go`
- **Verification:** `go test ./wristband/... -count=1`, `go test ./plugins/golem15/fonoteka -count=1`, `TestOAuthFlows`
- **Committed in:** `6cc07a4`
**2. [Rule 1 - Bug] wristband redirects never carried PHP's HTML body**
- **Found during:** Task 2, same replay
- **Issue:** PHP's `redirect()->away(...)` renders Symfony's default HTML redirect body (`Content-Type: text/html; charset=utf-8`, a fixed template with the target URL HTML-escaped four times); Go's bare 302 had no body and no Content-Type.
- **Fix:** New `wristband/redirect_html.go` ports the exact byte template and PHP's `htmlspecialchars(ENT_QUOTES)` escaping; `authorize.go`'s success and error-redirect paths now call it.
- **Files modified:** `wristband/redirect_html.go` (new), `wristband/authorize.go`
- **Verification:** `TestOAuthFlows` byte-for-byte body/header comparison
- **Committed in:** `6cc07a4`
**3. [Rule 1 - Bug] tide's id-masking missed plural `_ids` array fields**
- **Found during:** Task 2, `TestOAuthFlows` full-package run (`collection_ids[0]: expected 1 actual 8`)
- **Issue:** `isIDKey` matched `_id` and `id` but not the plural `_ids` suffix, so a literal `collection_ids` array value was compared byte-for-byte instead of being structurally masked -- no prior fixture in the corpus had exercised this with a non-empty, non-placeholder value.
- **Fix:** `isIDKey` now also matches `_ids`; each array element still reaches the existing per-element masking path.
- **Files modified:** `tide/normalize.go`
- **Verification:** `go test ./tide/... -count=1`, `TestOAuthFlows`
- **Committed in:** `6cc07a4`
**4. [Rule 1 - Bug] `{request_id}`'s router constraint rejected a valid PHP 404 test case**
- **Found during:** Task 2, `TestParityCorpus` full run
- **Issue:** The 08-UI-SPEC.md-derived `[A-Za-z0-9_-]{16,128}` constraint rejected the Phase-2-recorded 14-character `parity-missing` test value at the router (bare text/plain 404) before `OAuthConsentController::show` could return its real `{"error":"Request not found"}` 404 JSON, which is what live PHP (no router-level constraint at all) actually returns.
- **Fix:** Constraint changed to `[A-Za-z0-9_-]{1,128}` (upper bound only).
- **Files modified:** `../fonoteka.go/plugins/golem15/fonoteka/routes.go`
- **Verification:** `TestParityCorpus/GET___fonoteka_api_v1_oauth_request_{request_id}_jwt`
- **Committed in:** `23e7885`
**5. [Rule 1 - Bug] Consent's basic-validation failure returned a clean 422, not PHP's 500**
- **Found during:** Task 2, `TestParityCorpus` full run
- **Issue:** `OAuthConsentController::store`'s bare `$request->validate([...])` is never caught by a JSON exception renderer on this specific route; live PHP (confirmed via curl with `APP_DEBUG=false`, matching the isolated-PHP convention) crashes to Winter's generic production 500 HTML page. Go returned a clean `writeValidation` 422.
- **Fix:** `ConsentStore`'s basic-validation-failure branch now writes the same byte-identical Winter error page the user plugin already embeds (newly duplicated into `controllers/api/winter_error_page.html` to preserve plugin independence). Domain-level checks (`Request not found`, `No grantable scopes`) are unaffected.
- **Files modified:** `../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go`, `winter_error_page.html` (new)
- **Verification:** `TestParityCorpus/POST___fonoteka_api_v1_oauth_consent_jwt`
- **Committed in:** `23e7885`
**6. [Rule 1 - Bug] Two pre-existing Phase 2 fixtures were stale**
- **Found during:** Task 2, same `TestParityCorpus` run, after fixes 4/5 above
- **Issue:** `POST .../oauth/consent`'s case fixture had been recorded with `APP_DEBUG=true` (a full debug stack trace with incidental scrub collisions in the stack-frame numbers); `GET .../connected-apps`'s case fixture had `manual_tokens_count` over-scrubbed to a coincidental `{{id:alice}}` placeholder.
- **Fix:** Both re-recorded/hand-corrected against live isolated PHP with the current `APP_DEBUG=false` convention and the `genres` seed hook's actual manual-token count (1, not the fresh-user 0 a throwaway curl user showed).
- **Files modified:** `../fonoteka.go/parity/fixtures/routes/POST___fonoteka_api_v1_oauth_consent_jwt.yaml`, `GET___fonoteka_api_v1_oauth_connected-apps_jwt.yaml`
- **Verification:** `TestParityCorpus`
- **Committed in:** `23e7885`
**7. [Rule 3 - Blocking] `TestOAuthFlows` needed `app.url` and a matching default collection**
- **Found during:** Task 2, iterative replay debugging
- **Issue:** `testConfig(t)` left `app.url` empty (breaking every issuer/absolute-URL field) and the `genres` seed hook's hardcoded "Parity Collection" name didn't match the recorded PHP flow's actual onboarding-auto-created default collection name ("Moja kolekcja").
- **Fix:** `testConfig` now sets `app.url` to isolated PHP's fixed origin; `TestOAuthFlows` seeds its own "Moja kolekcja" default collection with no `active_collection_context` row, letting `ActiveCollectionResolver`'s own fallback resolve it exactly as the recording did.
- **Files modified:** `../fonoteka.go/parity/migrate_test.go`, `../fonoteka.go/parity/oauth_flow_test.go`
- **Verification:** `TestOAuthFlows`
- **Committed in:** `23e7885`
**8. [Rule 3 - Blocking] Shared-pool cross-test pollution between `TestOAuthFlows` and pre-existing corpus tests**
- **Found during:** Task 2, full `go test ./...` (not just `TestOAuthFlows` in isolation)
- **Issue:** `TestOAuthFlows`'s confidential-client token was left live (unrevoked) at test end, inflating `connected_apps_count` for a sibling `TestParityCorpus` case that shares the same `alice@parity.test` identity across the whole package's shared Postgres pool.
- **Fix:** `TestOAuthFlows` now revokes every `oauth_client_id`-linked token for its alice in `t.Cleanup`, regardless of pass/fail.
- **Files modified:** `../fonoteka.go/parity/oauth_flow_test.go`
- **Verification:** `go test ./parity/... -count=1` (full package, not just the single test)
- **Committed in:** `23e7885`
**9. [Rule 2 - Missing Critical] Nine new manifest routes needed `seed_hook: genres`**
- **Found during:** Task 2, first `TestParityCorpus` run after flipping the nine routes to `ported`
- **Issue:** None of the nine routes has its own `seed_hook`, so they fell through to the manifest's global onboarding-bootstrap seed, which no Go route currently implements -- every one failed with a 404 on `/_fonoteka/api/v1/onboarding/status`.
- **Fix:** Added `seed_hook: genres` to all nine, matching the corpus's existing convention for every other ported route.
- **Files modified:** `../fonoteka.go/parity/manifest.yaml`, `../fonoteka.go/parity/parity_contract_test.go` (allow-list)
- **Verification:** `TestParityCorpus`
- **Committed in:** `23e7885`
**10. [Rule 3 - Blocking] `parity_contract_test.go`'s hardcoded counts/allow-list were stale**
- **Found during:** Task 2, full `go test ./...`
- **Issue:** `TestParityContract` hardcoded `22 ported`/`147 pending` and an explicit route-id allow-list that didn't include the nine new routes.
- **Fix:** Updated to reference `expectedPortedRoutes`/`expectedPHPRoutes` and added the nine route ids to the allow-list.
- **Files modified:** `../fonoteka.go/parity/parity_contract_test.go`
- **Verification:** `go test ./... -count=1` (fonoteka.go root)
- **Committed in:** `23e7885`
---
**Total deviations:** 10 auto-fixed (6 Rule 1 bug fixes, 1 Rule 2 missing-critical addition, 3 Rule 3 blocking-issue fixes)
**Impact on plan:** All ten were necessary for the plan's own stated goal -- proving exact recorded byte parity -- to actually hold. None were scope creep; each was discovered specifically because this plan is the first to exercise these nine routes and this full lifecycle against the real Go implementation.
## Issues Encountered
None beyond the auto-fixed items above. Full `go vet`/`go test ./...` (including `-race` on touched packages) are green in `summercms.go` and across every `fonoteka.go` workspace module (root `fonoteka`/`parity`, `plugins/golem15/fonoteka` and all its subpackages, `plugins/golem15/user` and its subpackages).
## User Setup Required
None -- no external service configuration required. `scripts/check-phase8.sh`'s full gate needs Docker and the already-installed Node dependencies in `fonoteka-mcp`/`vue-fonoteka-app`, but this plan never invokes it; that's 08-10 Task 3.
## Next Phase Readiness
- `08-10` can now run `scripts/check-phase8.sh` (no flags) for the first time. The stage bodies are real, complete implementations, but none has been execution-verified end to end in this plan -- 08-10 Task 3 is the first real run and may need to iterate on the app-boot/real-mcp stage details (exact `compass` config keys, timing) once actually exercised.
- `08-SECURITY-REVIEW.md` does not exist yet; `stage_security_review` will fail closed until 08-10 creates it with `status: verified`.
- AUTH-05/AUTH-06/AUTH-07 remain Pending in REQUIREMENTS.md: this plan proves exact recorded-PHP byte parity for all nine OAuth routes and builds the complete real-MCP gate machinery, but only 08-10's actual execution of that gate can prove the "unchanged fonoteka-mcp completes its install/auth flow" clause those requirements need.
- No blockers.
## Self-Check: PASSED
- FOUND: ../fonoteka.go/parity/fixtures/mcp/mcp-lifecycle.yaml
- FOUND: ../fonoteka.go/parity/oauth_flow_test.go
- FOUND: ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/winter_error_page.html
- FOUND: wristband/redirect_html.go
- FOUND: scripts/check-phase8.sh
- FOUND: scripts/check-phase8-mcp-client.mjs
- FOUND commits (summercms.go): 246a488, 6cc07a4, e87346f
- FOUND commits (fonoteka.go): d9b168f, 23e7885
---
*Phase: 08-oauth2-1-authorization-server*
*Completed: 2026-09-23*