15 KiB
Phase 2: API parity harness bootstrap - Context
Gathered: 2026-09-16 Status: Ready for planning
## Phase BoundaryPhase 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.
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 --host=127.0.0.1 --port=8423) on a fresh database holding a small deterministic parity dataset. The recording proxy binds127.0.0.1:8422, and Nuxt/MCP point to that proxy. Never record against the developer's ad-hoc dev database or production. The user confirmed this port split during planning on 2026-09-16. - 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-clientartisan 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:recordandsummer parity:replaycommands implemented on the Phase 1bonfirecommand interface, not a standalone binary and notgo testenv-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,normalizeandheadersoverrides, and an optionalseed_hookname. 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_tokencookie) 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 infonoteka.go:parity/manifest.yaml(the 154 routes with auth group, identities, expected cases and header rules),parity/fixtures/{seed,nuxt,mcp,routes}/, andparity/parity_test.gothat 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 genresbeforePOST 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'sgo test, the app handler runs in-process onhttptest.Serverwith a testcontainers Postgres, so parity runs hermetically in CI.summer parity:replay --target <url>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 vs1.5number,nullvs[]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-ControlandPragmaon OAuth routes,WWW-Authenticateand protected-resource-metadata on 401s,Content-Dispositionon 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
*_atfield must match the Carbon+00:00ISO-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 aZ-suffixed date or anullwhere[]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-runselects 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 exampletide: 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|portedfield 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 testrun 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:recordre-records in place (an--updatemode) or always writes to a new directory for review. Either is acceptable; document it.
<canonical_refs>
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:00and 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 infonoteka.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 withPublicShareHeaders, the personal-token group/api/v1/fonotekawithinv.scope:*middleware, and the unprefixed OAuth group (/.well-known/oauth-authorization-serverand 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.phpandFonotekaTokenTestCase.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) andapp/composables/useFonoteka.ts(bearer JWT read from theauth_tokencookie) — 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.tsandsrc/client.ts—FONOTEKA_API_URLand theinv_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.mdQA-01, QA-02, QA-03..planning/phases/01-framework-kernel-foundation/01-CONTEXT.mdD-01, D-02, D-15, D-17 — thesummertool, thebonfirecommand interface and colon-style command names thatsummer parity:*builds on.CLAUDE.md(repo root) §GSD workflow rules — lean planning, plan-count checkpoint, unit tests as the last plan,go vetandgo test ./...green at every commit.
</canonical_refs>
<code_context>
Existing Code Insights
Reusable Assets
- No Go code exists yet in either repo.
fonoteka.gois an empty shell (go.modmodulegit.golem15.com/golem15/fonoteka, README, CLAUDE.md) created 2026-09-16. Phase 1 plans are written (4 plans) but not executed; thebonfirecommand interface Phase 2 depends on will come from them. - The PHP test suite (143 files,
getJson/postJson/putJson/deleteJsonstyle) 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.yamlplus a temporarygenresseed 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 samebonfire.Commandinterface as every other plugin command (Phase 1 D-17).
</code_context>
## 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.
- 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 2before planning, not by hand. - Recording against production
plytarium.comfor 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