diff --git a/.planning/phases/02-api-parity-harness-bootstrap/02-CONTEXT.md b/.planning/phases/02-api-parity-harness-bootstrap/02-CONTEXT.md new file mode 100644 index 0000000..42ef929 --- /dev/null +++ b/.planning/phases/02-api-parity-harness-bootstrap/02-CONTEXT.md @@ -0,0 +1,123 @@ +# Phase 2: API parity harness bootstrap - Context + +**Gathered:** 2026-09-16 +**Status:** Ready for planning + + +## Phase Boundary + +Phase 2 delivers the acceptance mechanism for every later port phase: a fixture recorder that captures request/response pairs from the running PHP Płytarium backend (every route plus the real Nuxt and MCP flows) and a replay-and-diff harness that runs those fixtures against any HTTP backend with a normalizer for nondeterministic fields and explicit assertions for the parity classes named in research (nil vs `[]`, `+00:00` dates and null-vs-absent keys, tri-state booleans, envelope and conditional keys, string-vs-number money). Requirements QA-01, QA-02, QA-03. + +Two repos: `summercms.go` gets the generic harness library, the fixture schema, the `summer parity:*` commands and a small synthetic corpus for its own tests; it knows nothing about Płytarium. `fonoteka.go` gets the Płytarium route manifest, the seed flow, the recorded corpus and the replay test that boots the app in-process. + +Not in this phase: any ported endpoint (Phase 3 ports the first one), the OpenAPI document, rate-limit or auth-group implementation, and the cutover run across all 154 routes (Phase 15). The harness only needs a running PHP backend to record against. + +**Dependency change:** the user chose to build the recorder as `summer parity:*` commands on the Phase 1 `bonfire` command kernel rather than as a standalone binary. Phase 2 therefore depends on Phase 1 (at least its `bonfire` and `cmd/summer` plans) and is no longer a parallel day-one workstream. ROADMAP.md still says "Depends on: Nothing (parallel with Phase 1)" and must be updated. + + + + +## Implementation Decisions + +### Recording strategy +- **D-01:** Two capture paths, both producing the same fixture format. A recording reverse proxy (`summer parity:proxy`) sits in front of the PHP backend on `:8422`; the Nuxt dev app (`NUXT_DEV_BACKEND_ORIGIN`) and the MCP server (`FONOTEKA_API_URL`) are pointed at it and real sessions are recorded as ordered flows. A scripted runner (`summer parity:record`) drives a route manifest to guarantee coverage of all 154 routes, including error cases (404, 422, 401, 423, throttled). +- **D-02:** Recordings are made against a local PHP backend (`php artisan serve --port=8422`) on a fresh database holding a small deterministic parity dataset. Never against the developer's ad-hoc dev database and never against production. +- **D-03:** The parity dataset is created through the PHP API itself in a recorded seed flow (onboarding bootstrap, register, login, create collection, albums, artists, genres, styles, invitations, personal tokens, and so on). Only what the API cannot create (backend admin user, OAuth client via the existing `oauth-client` artisan command) uses artisan. No seeder code is added to the PHP repo; the PHP originals stay untouched. +- **D-04:** The recorder and replayer are `summer parity:proxy`, `summer parity:record` and `summer parity:replay` commands implemented on the Phase 1 `bonfire` command interface, not a standalone binary and not `go test` env-var switches. Phase 2 waits for Phase 1's command kernel. + +### Fixture format and layout +- **D-05:** One fixture file per flow. A flow is an ordered list of steps; each step carries the request (method, path, query, kept request headers, body) and the recorded response (status, kept response headers, body). Proxy-recorded sessions become multi-step flows (`nuxt/…`, `mcp/…`); the manifest-driven runner emits one-step flows per route case (`routes/…`). +- **D-06:** Fixtures are YAML parsed with goccy/go-yaml, with a small schema owned by the framework library: flow name, description, steps, per-step `capture`, `normalize` and `headers` overrides, and an optional `seed_hook` name. JSON bodies are stored verbatim as block scalars so the recorded bytes stay inspectable; the diff parses them (see D-13). +- **D-07:** Credentials are scrubbed to named placeholders at record time. The seed flow's credential map (identity name to JWT, `inv_` personal token, OAuth client secret, authorization code, `auth_token` cookie) drives replacement, so fixtures contain `{{jwt:alice}}`, `{{token:mcp-read}}` and similar instead of secrets, and the corpus is committed. Replay resolves placeholders from the variable store populated by capture rules (D-11). +- **D-08:** Corpus location is a top-level `parity/` directory in `fonoteka.go`: `parity/manifest.yaml` (the 154 routes with auth group, identities, expected cases and header rules), `parity/fixtures/{seed,nuxt,mcp,routes}/`, and `parity/parity_test.go` that replays everything. The framework repo keeps only the library, the schema and a synthetic corpus under its own testdata. + +### Replay auth and data state +- **D-09:** Before route fixtures replay, the recorded seed flow replays against the Go backend. Both backends reach identical state through identical requests, and the seed flow itself is parity-tested. This follows the roadmap's plugin-by-plugin order: a read fixture passes once the writes it depends on are ported. +- **D-10:** Escape hatch for routes whose write endpoint is not yet ported (Phase 3's `GET genres` before `POST genres`): a flow may name a seed hook, and the app test registers a Go function for that name that inserts rows through GORM or SQL. Hooks are declared in the manifest as temporary with the route that will replace them, and are deleted when that write route lands. +- **D-11:** Steps declare capture rules (a JSON path in the response mapped to a variable name). Login and token-mint steps capture credentials; create steps capture ids, share tokens and OAuth codes. Later steps and flows reference variables in paths, headers and bodies. Recording and replay use the same mechanism on both backends. +- **D-12:** Two replay hosts. In `fonoteka.go`'s `go test`, the app handler runs in-process on `httptest.Server` with a testcontainers Postgres, so parity runs hermetically in CI. `summer parity:replay --target ` runs the same corpus against any running server, including the PHP backend itself as a self-check that the corpus and normalizer are sound. + +### Diff strictness and reporting +- **D-13:** JSON bodies compare structurally with token-type checks: key sets, values and JSON token types (`"1.5000"` string vs `1.5` number, `null` vs `[]` vs absent key, `true`/`false`/`null`). Key order is ignored. Non-JSON bodies (CSV export, images, OAuth form responses) compare by bytes after header-declared content type. +- **D-14:** Headers compare against a global allow-list (status, `Content-Type`, and the pagination and CORS headers the clients read) plus per-route additions declared in the manifest: `Cache-Control` and `Pragma` on OAuth routes, `WWW-Authenticate` and protected-resource-metadata on 401s, `Content-Disposition` on CSV export. `Date`, `Server`, request ids and framework-specific headers are never compared. +- **D-15:** Normalization is rule-based. The manifest declares global normalizers keyed by JSON path pattern and value shape: any `*_at` field must match the Carbon `+00:00` ISO-8601 shape but its value is masked; ids are asserted integer and masked; captured variables compare by reference (the same variable must resolve to the same value across steps); slugs derived from seeded names compare exactly. A step can add or disable rules. Format assertions run on masked fields, so a `Z`-suffixed date or a `null` where `[]` was recorded still fails. +- **D-16:** A run executes every flow to the end, skipping the rest of a flow only when a capture fails. Each failing step prints a path-level diff (per JSON path, header and status). The run ends with a coverage table over the 154 manifest routes: recorded, passing, failing, unrecorded. In `go test`, each flow is a subtest so `-run` selects one; the CLI prints the same report and exits non-zero on any failure. + +### Claude's Discretion +- Package name for the harness library in `summercms.go`: none of the IDEA_LIB_NAMES entries cover testing; pick a summer-themed name in the same spirit (for example `tide`: it comes in and goes out, record and replay) and record it in the plan. +- Expected-failure handling during the port: how routes not yet ported are marked in the manifest so CI stays green on ported routes and red only on regressions (a per-route `status: pending|ported` field is the obvious shape). Must exist in Phase 2 because Phase 3 will have one green route out of 154. +- Whether each flow gets a fresh database or flows share one seeded database with per-flow ordering. Default to one seeded database per `go test` run with flows ordered by the manifest, since the seed flow is the expensive part. +- How proxy-recorded OAuth PKCE flows from the MCP server are captured (the browser leg of authorize and consent goes through the proxy too) and how the authorization code and PKCE verifier are captured into variables. +- Binary response storage in YAML (base64 block or sidecar file next to the fixture, chosen by size). +- Proxy transport details: plain HTTP on localhost, no TLS, cookie passthrough, streaming bodies buffered for recording. +- Whether `summer parity:record` re-records in place (an `--update` mode) or always writes to a new directory for review. Either is acceptable; document it. + + + + +## Canonical References + +**Downstream agents MUST read these before planning or implementing.** + +### Parity classes and harness design +- `.planning/research/PITFALLS.md` §Pitfall 4 (nil slice vs `[]`), §Pitfall 5 (money as fixed-decimal string), §Pitfall 6 (ISO-8601 `+00:00` and null-vs-omitted dates), §Pitfall 7 (tri-state booleans), §Pitfall 8 (envelopes and conditional keys), §Pitfall 9 (OAuth routes free of blanket middleware), §Rate-limit bucket parity, §Pitfall-to-Phase Mapping — the assertion categories the harness must implement as explicit checks, not just deep-equal. +- `.planning/research/SUMMARY.md` §Phase 3: API parity harness — what the harness delivers and why it must diff full bodies field by field. +- `.planning/research/ARCHITECTURE.md` §Recommended Project Structure (`test/parity/` entry) and §Integration Points (harness has near-zero dependency on framework internals). Note the structure sketch is superseded by D-08 (corpus lives in `fonoteka.go/parity/`). +- `.planning/research/STACK.md` §Testing stack (testcontainers-go, testify, golden-file pattern) and §Supporting Libraries (goccy/go-yaml). The golden-file sketch there is superseded by the flow-based YAML format in D-05 and D-06. + +### The PHP contract being recorded +- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/routes.php` — the 154 routes and their six route groups: JWT `/_fonoteka/api/v1` (with and without the must-change-password gate), the throttled public onboarding group, the public share group with `PublicShareHeaders`, the personal-token group `/api/v1/fonoteka` with `inv.scope:*` middleware, and the unprefixed OAuth group (`/.well-known/oauth-authorization-server` and friends). The manifest in D-08 is derived from this file. +- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/traits/SerializesFonoteka.php` — the serializer whose field-by-field behavior (conditional keys, tri-state booleans, `toIso8601String`, empty-array defaults) defines the normalizer rules and format assertions in D-15. +- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/AccessTestCase.php` and `FonotekaTokenTestCase.php` — `makeUser`, `makeCollectionFor`, `mintToken`, `authHeaders`, `jwtHeaders`: the identities and credential shapes the seed flow must reproduce through the API. +- `/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/nuxt.config.ts` (runtime config: `fonotekaApiBase`, `userApiBase`, `journalApiBase`, `feedbackApiBase`, `NUXT_DEV_BACKEND_ORIGIN`) and `app/composables/useFonoteka.ts` (bearer JWT read from the `auth_token` cookie) — how the Nuxt app reaches the backend and what the proxy must sit in front of. +- `/media/nvme/dev/golem15/fonoteka/fonoteka-mcp/src/config.ts` and `src/client.ts` — `FONOTEKA_API_URL` and the `inv_` bearer the MCP server forwards unchanged. +- `/media/nvme/dev/golem15/fonoteka/README.md` — local run instructions (backend `:8422`, SPA `:3119`). + +### Project-level decisions +- `.planning/PROJECT.md` §Constraints (two repos, stdlib first, parity is the acceptance test) and §Key Decisions (parity harness as a day-one workstream: now qualified by D-04). +- `.planning/REQUIREMENTS.md` QA-01, QA-02, QA-03. +- `.planning/phases/01-framework-kernel-foundation/01-CONTEXT.md` D-01, D-02, D-15, D-17 — the `summer` tool, the `bonfire` command interface and colon-style command names that `summer parity:*` builds on. +- `CLAUDE.md` (repo root) §GSD workflow rules — lean planning, plan-count checkpoint, unit tests as the last plan, `go vet` and `go test ./...` green at every commit. + + + + +## Existing Code Insights + +### Reusable Assets +- No Go code exists yet in either repo. `fonoteka.go` is an empty shell (`go.mod` module `git.golem15.com/golem15/fonoteka`, README, CLAUDE.md) created 2026-09-16. Phase 1 plans are written (4 plans) but not executed; the `bonfire` command interface Phase 2 depends on will come from them. +- The PHP test suite (143 files, `getJson`/`postJson`/`putJson`/`deleteJson` style) is a complete map of request shapes and expected statuses per route; the manifest's cases can be derived from it without re-deriving the contract. + +### Established Patterns +- Stdlib first. New dependencies for Phase 2 are limited to goccy/go-yaml (already chosen), testify, testcontainers-go with its postgres module, and whatever Phase 1 introduced (cobra, koanf). A JSON diff or JSONPath library is not pre-approved; a small hand-rolled path matcher is expected. +- Framework repo never imports the app. The harness library exposes interfaces (fixture loader, target, normalizer, reporter) and the app test wires them. + +### Integration Points +- Phase 3 is the first consumer: `parity/fixtures/routes/genres-list.yaml` plus a temporary `genres` seed hook (D-10) is what makes its success criterion 4 measurable. +- Phase 15 runs the full corpus against the Go binary from the CLI (D-12) as the cutover gate. +- `summer parity:*` commands register through the same `bonfire.Command` interface as every other plugin command (Phase 1 D-17). + + + + +## Specific Ideas + +- The user rejected the standalone-binary shortcut in favor of building the recorder on the framework's own command kernel, accepting that Phase 2 now follows Phase 1 instead of running in parallel. +- Replaying the corpus against the PHP backend itself must pass cleanly; it is the proof that the normalizer masks exactly the nondeterministic fields and nothing else. +- Seeding through the API doubles as parity coverage for the write endpoints, so the seed flow should touch as many create endpoints as the dataset reasonably needs. + + + + +## Deferred Ideas + +- ROADMAP.md dependency update for Phase 2 ("Depends on: Phase 1", drop the "parallel with Phase 1" note in Execution Order and STATE.md's current focus line) — do via `/gsd-phase edit 2` before planning, not by hand. +- Recording against production `plytarium.com` for read-only routes as a late sanity check before cutover — Phase 15 may consider it; not part of the corpus. +- Promoting the harness to verify a second app (keios.eu) — only when that port starts. + + + +--- + +*Phase: 02-api-parity-harness-bootstrap* +*Context gathered: 2026-09-16* diff --git a/.planning/phases/02-api-parity-harness-bootstrap/02-DISCUSSION-LOG.md b/.planning/phases/02-api-parity-harness-bootstrap/02-DISCUSSION-LOG.md new file mode 100644 index 0000000..1c1fec9 --- /dev/null +++ b/.planning/phases/02-api-parity-harness-bootstrap/02-DISCUSSION-LOG.md @@ -0,0 +1,203 @@ +# Phase 2: API parity harness bootstrap - 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-16 +**Phase:** 02-api-parity-harness-bootstrap +**Areas discussed:** Recording strategy, Fixture format and layout, Replay auth and data state, Diff strictness and reporting + +--- + +## Recording strategy + +### How should fixtures be captured from the PHP backend? + +| Option | Description | Selected | +|--------|-------------|----------| +| Both: proxy + scripted | Recording reverse proxy in front of :8422 captures real Nuxt and MCP sessions; a manifest-driven runner fills coverage for all 154 routes | ✓ | +| Recording proxy only | Point Nuxt and MCP at the proxy and record whatever they send | | +| Scripted runner only | A Go program issues each request from a manifest with seeded auth | | + +**User's choice:** Both: proxy + scripted + +### Which PHP instance and dataset should recordings come from? + +| Option | Description | Selected | +|--------|-------------|----------| +| Local :8422 with a seeded parity dataset | Deterministic dataset built on a fresh DB before recording | ✓ | +| Local :8422 with existing dev database | Record against whatever is in the dev DB | | +| Production plytarium.com (read-only routes) | GET routes from the live site, writes locally | | + +**User's choice:** Local :8422 with a seeded parity dataset + +### How should the seeded parity dataset be built on the PHP side? + +| Option | Description | Selected | +|--------|-------------|----------| +| Seed through the API itself | Recorded setup flow calls the PHP API; artisan only for what the API cannot create | ✓ | +| A PHP seeder command in the fonoteka plugin | `fonoteka:parity-seed` artisan command with fixed ids | | +| A checked-in SQL dump | Restore a hand-prepared dump before recording | | + +**User's choice:** Seed through the API itself + +### What shape should the recorder tool take, given Phase 1's CLI kernel may not exist yet? + +| Option | Description | Selected | +|--------|-------------|----------| +| Standalone `parity` binary now, wrapped later | `cmd/parity` on plain flag/cobra, exposed as `summer parity:*` after Phase 1 | | +| go test driven only | Recording and replay as `go test` runs with env-var switches | | +| Wait for Phase 1 and build `summer parity:*` directly | Serialize Phase 2 after Phase 1; recorder is a bonfire command from day one | ✓ | + +**User's choice:** Wait for Phase 1 and build `summer parity:*` directly +**Notes:** This makes Phase 2 depend on Phase 1 and drops the roadmap's "parallel with Phase 1" note; ROADMAP.md needs updating. + +--- + +## Fixture format and layout + +### How should recorded fixtures be organized on disk? + +| Option | Description | Selected | +|--------|-------------|----------| +| One file per flow, steps inside | Ordered steps per flow; manifest runner emits one-step flows per route | ✓ | +| One file per request/response pair | Every exchange its own file | | +| One file per route with all cases | 154 files each holding every case | | + +**User's choice:** One file per flow, steps inside + +### What file format should a flow fixture use? + +| Option | Description | Selected | +|--------|-------------|----------| +| Custom YAML via goccy/go-yaml | Small schema, human-editable, same YAML library as config and admin schemas | ✓ | +| Custom JSON | Same schema as JSON | | +| HAR files | Browser-standard archive with a sidecar for rules | | + +**User's choice:** Custom YAML via goccy/go-yaml + +### How should credentials and volatile headers be handled in stored fixtures? + +| Option | Description | Selected | +|--------|-------------|----------| +| Scrub to named placeholders at record time | `{{jwt:alice}}`-style placeholders from a credential map; replay resolves them | ✓ | +| Store real values, gitignore the fixture directory | Never commit fixtures | | +| Strip auth entirely, re-add at replay from the manifest | Manifest says which identity each step runs as | | + +**User's choice:** Scrub to named placeholders at record time + +### Where should the fixture corpus and the Płytarium manifest live? + +| Option | Description | Selected | +|--------|-------------|----------| +| fonoteka.go/parity/ with fixtures beside the manifest | manifest.yaml, fixtures/{nuxt,mcp,routes}, replay test; framework ships library + synthetic corpus | ✓ | +| fonoteka.go/plugins/fonoteka/testdata/parity/ | Inside the fonoteka plugin module | | +| A third repo for the corpus | Shared by PHP and Go sides | | + +**User's choice:** fonoteka.go/parity/ with fixtures beside the manifest + +--- + +## Replay auth and data state + +### How should the Go backend's database reach the same state the recording was made against? + +| Option | Description | Selected | +|--------|-------------|----------| +| Replay the recorded seed flow against Go first | Same API-driven seed flow on both sides | ✓ | +| Direct Postgres seed from the fixture corpus | Insert rows from a seed file | | +| Hybrid: SQL seed now, API seed once writes exist | Two mechanisms during the port | | + +**User's choice:** Replay the recorded seed flow against Go first + +### When a replay needs state that no ported write endpoint can create yet, what fills the gap? + +| Option | Description | Selected | +|--------|-------------|----------| +| Per-flow Go seed hook, deleted once the write route lands | Named hook registered by the app test, listed as temporary in the manifest | ✓ | +| Port the matching write endpoint in the same phase | Phase 3 would grow beyond one route | | +| Permanent SQL seed files per flow | Second source of truth for state | | + +**User's choice:** Per-flow Go seed hook, deleted once the write route lands + +### How does a replay obtain credentials to substitute for placeholders? + +| Option | Description | Selected | +|--------|-------------|----------| +| Capture rules on seed-flow steps | JSON path from response into a variable store; same for ids, share tokens, OAuth codes | ✓ | +| Harness-level identity registry | Harness logs identities in outside any flow | | +| Mint tokens directly against the Go DB | Sign JWTs and insert tokens in Postgres | | + +**User's choice:** Capture rules on seed-flow steps + +### How is the Go backend hosted while fixtures replay against it? + +| Option | Description | Selected | +|--------|-------------|----------| +| Both: in-process httptest.Server in tests, any URL from the CLI | Hermetic go test with testcontainers Postgres; `summer parity:replay --target` for any server including PHP as self-check | ✓ | +| In-process httptest.Server only | Replay exists only as a Go test | | +| External URL only | Always target a running server | | + +**User's choice:** Both: in-process httptest.Server in tests, any URL from the CLI + +--- + +## Diff strictness and reporting + +### What counts as a body mismatch for JSON responses? + +| Option | Description | Selected | +|--------|-------------|----------| +| Structural equality plus token-type checks | Key sets, values and JSON token types; key order ignored | ✓ | +| Raw byte equality after canonicalization | Sorted keys, fixed whitespace, compare bytes | | +| Strict byte equality, key order included | PHP key order becomes part of the contract | | + +**User's choice:** Structural equality plus token-type checks + +### Which response headers should the diff compare? + +| Option | Description | Selected | +|--------|-------------|----------| +| Global allow-list plus per-route additions | Status, Content-Type, pagination/CORS always; OAuth cache headers, WWW-Authenticate, Content-Disposition per route | ✓ | +| Status and Content-Type only | Ignore all other headers | | +| All headers except a deny-list | Compare everything not on a volatile list | | + +**User's choice:** Global allow-list plus per-route additions + +### How should nondeterministic values be normalized before the diff? + +| Option | Description | Selected | +|--------|-------------|----------| +| Global rules by field pattern plus per-step overrides | Pattern-keyed normalizers with format assertions on masked fields; captured variables compare by reference | ✓ | +| Per-step ignore lists only | Each step lists JSON paths to skip | | +| Mask by value type at record time | Recorder rewrites dates/ids/tokens into placeholders | | + +**User's choice:** Global rules by field pattern plus per-step overrides + +### How should a parity run report its results? + +| Option | Description | Selected | +|--------|-------------|----------| +| Run everything, report per step, summarize per route | Path-level diff per failing step; coverage table over 154 routes; flows as go test subtests | ✓ | +| Fail fast on first mismatch | Stop at the first failing step | | +| Report only, never fail | JSON report, exit zero until cutover | | + +**User's choice:** Run everything, report per step, summarize per route + +--- + +## Claude's Discretion + +- Harness library package name (summer-themed; `tide` suggested). +- Expected-failure marking for unported routes so CI stays green on ported routes. +- Fresh DB per flow vs one seeded DB per run (default: one per run, manifest-ordered flows). +- Capturing the MCP OAuth PKCE flow through the proxy and into variables. +- Binary response storage (base64 block vs sidecar file). +- Proxy transport details (plain HTTP on localhost, cookie passthrough, buffered bodies). +- Whether `summer parity:record` has an `--update` mode or writes to a new directory. + +## Deferred Ideas + +- ROADMAP.md dependency update for Phase 2 (now depends on Phase 1) via `/gsd-phase edit 2`. +- Recording read-only routes against production as a late sanity check before cutover (Phase 15 may consider). +- Reusing the harness for the keios.eu port when it starts.