Files
summercms/.planning/phases/02-api-parity-harness-bootstrap/02-CONTEXT.md
2026-09-16 16:27:50 +02:00

15 KiB

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 --host=127.0.0.1 --port=8423) on a fresh database holding a small deterministic parity dataset. The recording proxy binds 127.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-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 <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 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_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: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.

</canonical_refs>

<code_context>

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).

</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.
## 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