docs(phase-02): complete phase execution

This commit is contained in:
Jakub Zych
2026-09-17 14:23:11 +02:00
parent 8fe69ffc6b
commit b3e609ea78
2 changed files with 204 additions and 9 deletions

View File

@@ -2,9 +2,9 @@
gsd_state_version: 1.0
milestone: v1.0
milestone_name: milestone
status: verifying
stopped_at: Completed 02-05-PLAN.md
last_updated: "2026-09-17T12:16:47.266Z"
status: ready_to_plan
stopped_at: Phase 02 complete (5/5) — ready to discuss Phase 3
last_updated: 2026-09-17T12:22:49.418Z
last_activity: 2026-09-17
progress:
total_phases: 15
@@ -21,13 +21,13 @@ progress:
See: .planning/PROJECT.md (updated 2026-09-16)
**Core value:** An existing WinterCMS-shaped app can be ported plugin by plugin to a single Go binary without its frontend noticing: the PHP version's API contract is the acceptance test.
**Current focus:** Phase 02 — api-parity-harness-bootstrap
**Current focus:** Phase 3 — first vertical slice — genres end to end
## Current Position
Phase: 02 (api-parity-harness-bootstrap) — VERIFYING
Plan: 5 of 5
Status: Phase complete — ready for verification
Phase: 3
Plan: Not started
Status: Ready to plan
Last activity: 2026-09-17
Progress: [██████████] 100% of realized plans (9/9); 2/15 roadmap phases
@@ -36,7 +36,7 @@ Progress: [██████████] 100% of realized plans (9/9); 2/15 ro
**Velocity:**
- Total plans completed: 9
- Total plans completed: 14
- Average duration: 21 min
- Total execution time: 104 min
@@ -45,7 +45,7 @@ Progress: [██████████] 100% of realized plans (9/9); 2/15 ro
| Phase | Plans | Total | Avg/Plan |
|-------|-------|-------|----------|
| 01 | 4 | - | - |
| 02 | 5 | 104 min | 21 min |
| 02 | 5 | - | - |
**Recent Trend:**

View File

@@ -0,0 +1,195 @@
---
phase: 02-api-parity-harness-bootstrap
verified: 2026-09-17T12:21:50Z
status: passed
score: 4/4 must-haves verified
overrides_applied: 0
human_verification: []
decision_coverage:
honored: 16
total: 16
not_honored: []
---
# Phase 2: API parity harness bootstrap Verification Report
**Phase Goal:** A fixture recorder captures request/response pairs from the running PHP backend — including real Nuxt and MCP flows — and a replay-and-diff harness with a normalizer for nondeterministic fields can run against any HTTP backend. The recorder and replayer are `summer parity:*` commands on the Phase 1 command kernel (bonfire); recording itself only needs a running PHP backend, but the tooling waits for Phase 1.
**Verified:** 2026-09-17T12:21:50Z
**Status:** passed
**Re-verification:** No — initial verification
Must-haves are the four ROADMAP success criteria (they override plan-level truths). Plan `must_haves` were checked as supporting evidence. `gsd-sdk query user-story.validate` is not required: the ROADMAP goal is an infrastructure outcome, not a user story.
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | The fixture recorder captures a request/response pair from the live PHP backend for at least one route and stores it as a replayable fixture | ✓ VERIFIED | 154 committed one-step route fixtures under `../fonoteka.go/parity/fixtures/routes/` plus seed `fixtures/seed/bootstrap.yaml`. First JWT genres case `get_genres_jwt.yaml` is a live PHP `GET /_fonoteka/api/v1/genres` body (Winter catalog rows + placeholders). `go run …/check_corpus.go --require-recorded --require-clients --check-secrets` printed `recorded 154/154`. VALIDATION.md: `--fresh-php` self-replay **154/154 passing, 0 failing, 0 unrecorded**. Also real client flows `nuxt-browse.yaml` (170KB, 3k+ lines), `mcp-tools.yaml` (me + genres read + POST create), `mcp-oauth.yaml` (well-known, register, PKCE authorize/consent/token). |
| 2 | The replay-and-diff harness runs a recorded fixture against an arbitrary `httptest.Server` backend and reports a byte-level diff | ✓ VERIFIED | `tide.ReplayFlow` + `compareBodies` (JSON path diffs via `UseNumber` + exact-byte offset for non-JSON). CLI: `parity:replay --fixtures --target`. Tests: `TestParityRoundTrip` / `TestParityCommands` record then replay against httptest; changed JSON fails at `$.data`; changed bytes report offset. This session: `go test ./cmd/summer -run 'TestParityCommandContract\|TestParityCommands'` PASS. App: `TestParitySynthetic` replays SQL-backed POST/GET through `httptest.NewServer` (live this session, 13.297s). |
| 3 | The harness's normalizer and assertions explicitly catch the parity classes named in research (nil vs `[]`, date format, tri-state booleans, envelope/conditional keys) on a synthetic test case, not just status codes | ✓ VERIFIED | `tide/diff_contract_test.go` `TestDiffContract`: baseline + key-order pass; mutations fail with a named JSON path for null vs array, empty object vs array, Carbon `Z` vs `+00:00`, non-date text, present-null vs absent date, tri-state true/false/null, missing `meta` envelope, extra `links` key, conditional `reservation`, money string vs number, integer id vs string/fraction, exact slug. `normalize.go` asserts Carbon `+00:00` then masks `*_at`; integer IDs masked after type check; slugs exact. This session: `go test ./tide -run TestDiffContract` PASS. |
| 4 | `go vet` and `go test ./...` are green, and the harness's own integration tests run against testcontainers Postgres | ✓ VERIFIED | This session: root `go vet ./... && go test ./...` exit 0 (all packages ok, including `tide` and `cmd/summer`). App `go vet ./... && go test -short ./...` exit 0. Live `go test ./parity -run TestParitySynthetic -count=1` **ok 13.297s** with Docker (TestMain fails closed if Docker is missing; `-short` skips the container). VALIDATION.md: full `--fresh-php` gate 116s including race + 154-route PHP self-replay. Plan 05 is the dedicated unit/contract-test plan. |
**Score:** 4/4 truths verified
Supporting plan truths (D-01…D-16, CLI on bonfire, 154-route corpus, pending≠passing) all hold in code; they are not extra score rows.
### Required Artifacts
`gsd-sdk query verify.artifacts`: 21/24 passed. The three failures are **false negatives**: the checker treats directories as files. Manual: `fixtures/routes/` has **154** YAML files; `fixtures/nuxt/` and `fixtures/mcp/` exist with committed flows.
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `tide/flow.go` | Versioned flow/step contracts | ✓ VERIFIED | 219 lines; `Flow`/`Step`/`Request`/`Response`/`Capture`/`Normalize` |
| `tide/fixture.go` | Validated YAML load/atomic save | ✓ VERIFIED | 243 lines; goccy unknown-field reject; temp+sync+rename |
| `tide/record.go` | HTTP recording engine | ✓ VERIFIED | 147 lines; `RecordFlow`, bounded bodies, capture/scrub |
| `tide/replay.go` | Backend-independent replay | ✓ VERIFIED | 104 lines; `ReplayFlow` against injected target; continue-after-diff |
| `cmd/summer/parity.go` | Bonfire `parity:*` commands | ✓ VERIFIED | 307 lines; `parity:proxy`/`record`/`replay` |
| `tide/proxy.go` | Loopback capture proxy | ✓ VERIFIED | 380 lines; `httputil.ReverseProxy` Rewrite/SetURL; loopback-only |
| `tide/variables.go` | Capture, expand, scrub | ✓ VERIFIED | 645 lines; `{{name}}`, 0600 store, credential reject |
| `tide/normalize.go` | Shape-aware masking | ✓ VERIFIED | 149 lines; Carbon `+00:00`, integer ids, slug exact |
| `tide/manifest.go` | Generic 154-route runner | ✓ VERIFIED | 561 lines; `--next-batch` cap 15; pending/ported |
| `tide/report.go` | Coverage table | ✓ VERIFIED | `CoverageHeaders`/`SummaryLine`; used by CLI `out.Table` |
| `../fonoteka.go/parity/manifest.yaml` | Exact 154-route map | ✓ VERIFIED | 154 `- id:` rows, all `status: pending` |
| `../fonoteka.go/parity/fixtures/seed/bootstrap.yaml` | API-created seed | ✓ VERIFIED | Recorded bootstrap flow, 8468 bytes |
| `../fonoteka.go/parity/fixtures/routes/` | One fixture per route | ✓ VERIFIED | 154 YAML files (checker missed directory) |
| `../fonoteka.go/parity/fixtures/nuxt/` | Real Nuxt session | ✓ VERIFIED | `nuxt-browse.yaml` 170062 bytes |
| `../fonoteka.go/parity/fixtures/mcp/` | Real MCP sessions | ✓ VERIFIED | `mcp-tools.yaml` + `mcp-oauth.yaml` |
| `../fonoteka.go/parity/parity_test.go` | Corpus runner + Phase 3 seam | ✓ VERIFIED | `newTarget` / `seedHooks`; 154 pending subtests |
| `../fonoteka.go/parity/synthetic_test.go` | Postgres + httptest handler | ✓ VERIFIED | testcontainers `postgres:16-alpine`; POST/GET `/synthetic/items` |
| `../fonoteka.go/parity/testdata/synthetic-seed.yaml` | SQL-backed write flow | ✓ VERIFIED | Exists; replayed by `TestParitySynthetic` |
| `../fonoteka.go/parity/testdata/synthetic-read.yaml` | Read + named seed hook | ✓ VERIFIED | `synthetic-item` hook |
| `tide/diff_contract_test.go` | QA-02 negative tests | ✓ VERIFIED | Table of parity-class mutations |
| `tide/proxy_security_test.go` | Loopback/scrub regressions | ✓ VERIFIED | Non-loopback, caps, secret scan |
| `../fonoteka.go/parity/parity_contract_test.go` | Pending≠passing contracts | ✓ VERIFIED | 154 pending, zero Go passes |
| `scripts/check-phase2.sh` | Repeatable phase gate | ✓ VERIFIED | 374 lines; refuses `PHP_PARITY_TARGET`; MariaDB + PHP child |
| `.planning/phases/02-api-parity-harness-bootstrap/02-VALIDATION.md` | Measured evidence | ✓ VERIFIED | `nyquist_compliant: true`; 116s `--fresh-php` |
**Artifacts:** 24/24 verified (3 via manual directory check)
### Key Link Verification
`gsd-sdk query verify.key-links` reported 0/18 verified. Same false negative as Phase 1: the checker greps target *filenames* inside source; Go imports package paths. Manual wiring:
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `cmd/summer/main.go` | `cmd/summer/parity.go` | `toolCommands` | ✓ WIRED | `parityProxyCommand()`, `parityRecordCommand()`, `parityReplayCommand()` in `toolCommands()` |
| `cmd/summer/parity.go` | `tide/record.go` | `RecordFlow` | ✓ WIRED | `tide.RecordFlow(ctx, spec, cfg)` in `runParityRecord` |
| `cmd/summer/parity.go` | `tide/replay.go` | `ReplayFlow` | ✓ WIRED | `tide.ReplayFlow(ctx, flow, cfg)` in `runParityReplay` |
| `cmd/summer/parity.go` | `tide/proxy.go` | `parity:proxy` | ✓ WIRED | `tide.NewProxy` + `ListenAndServe`; `tide.LoadRules` |
| `tide/proxy.go` | `tide/rules.go` | per-path rules | ✓ WIRED | `p.cfg.Rules.Match` / header filter / capture copy |
| `tide/proxy.go` | `tide/variables.go` | session store | ✓ WIRED | `OpenStore`, `CaptureStep`, `ScrubStep` before flush |
| `tide/manifest.go` | `tide/record.go` | route-case record | ✓ WIRED | `RecordFlow(...)` inside `RecordManifest` |
| `tide/replay.go` | `tide/variables.go` | expand/capture | ✓ WIRED | `expandRequest` then `CaptureStep` then `expandResponse` |
| `tide/replay.go` | `tide/normalize.go` | compare-time mask | ✓ WIRED | `compareStep` → `compareBodies` → `normalizeJSON` |
| `../fonoteka.go/parity/manifest.yaml` | `fixtures/routes/` | every case fixture | ✓ WIRED | `ValidateManifest(..., ModeRequireRecorded)` + corpus `recorded 154/154` |
| `fixtures/seed/bootstrap.yaml` | route cases | captured ids | ✓ WIRED | Seed captures feed `{{jwt:…}}` / `{{id:…}}` in route YAML |
| `capture_clients.mjs` | `127.0.0.1:8422` | unchanged clients | ✓ WIRED | Spawns `parity:proxy`; `NUXT_DEV_BACKEND_ORIGIN` and `FONOTEKA_API_URL` = proxy |
| `parity_test.go` | `manifest.yaml` | pending subtests | ✓ WIRED | `tide.LoadManifest` + `t.Run` per route id |
| `parity_test.go` | `synthetic_test.go` | `newTarget` / hooks | ✓ WIRED | `newTarget` → `newSyntheticHandler`; `seedHooks["synthetic-item"]` |
| `synthetic_test.go` | `tide/replay.go` | httptest replay | ✓ WIRED | `tide.ReplayFlow(..., Target: srv.URL)` |
| `scripts/check-phase2.sh` | `parity_test.go` | `go test ./parity` | ✓ WIRED | `go test ./...` plus explicit `TestParitySynthetic` |
| `scripts/check-phase2.sh` | `check_corpus.go` | corpus audit | ✓ WIRED | `go run ./parity/check_corpus.go --require-recorded --require-clients --check-secrets` |
| `tide/diff_contract_test.go` | `tide/diff.go` | public Replay API | ✓ WIRED | Mutations go through `RecordFlow`/`ReplayFlow`, not private helpers |
**Wiring:** 18/18 connections verified (manual)
### Data-Flow Trace (Level 4)
| Artifact | Data | Source | Produces real data | Status |
|----------|------|--------|--------------------|--------|
| `parity:record` | fixture YAML body | Live HTTP response bytes | Yes — CLI tests record httptest JSON; PHP corpus bodies are Winter JSON (genres catalog, `+00:00` timestamps) | ✓ FLOWING |
| `parity:proxy` | multi-step Nuxt/MCP flows | Unchanged clients via `:8422` → PHP `:8423` | Yes — `nuxt-browse` includes onboarding, embed.js, collections/albums; `mcp-oauth` includes well-known + PKCE | ✓ FLOWING |
| `parity:replay` | path diffs | `compareBodies` / `diffJSON` / `diffBytes` | Yes — CLI mismatch prints expected/actual; contract tests assert path names | ✓ FLOWING |
| `TestParitySynthetic` | row name | POST `/synthetic/items` → Postgres → GET | Yes — live this session; mutated name fails at `$[0].name` | ✓ FLOWING |
| `check_corpus.go` | 154/154 | manifest vs snapshot vs `routes.php` | Yes — this session printed `recorded 154/154` | ✓ FLOWING |
## Requirements Coverage
PLAN frontmatter IDs: QA-01, QA-02, QA-03 (every plan). REQUIREMENTS.md maps the same three to Phase 2. No Phase 2 orphans. QA-04 is Phase 3; QA-05 is Phase 15.
| Requirement | Source Plan | Description | Status | Evidence |
|-------------|-------------|-------------|--------|----------|
| QA-01 | 01, 02, 03, 05 | Recorder captures every PHP route plus real Nuxt and MCP flows | ✓ SATISFIED | 154 route fixtures, seed, `nuxt-browse`, `mcp-tools`, `mcp-oauth`; corpus `--require-recorded --require-clients --check-secrets`; VALIDATION.md 154/154 PHP self-replay |
| QA-02 | 01, 02, 04, 05 | Replay-and-diff with normalizer and named parity-class assertions | ✓ SATISFIED | `TestDiffContract` / `TestNormalize` / `TestDiff`; synthetic httptest replay. Go backend for PHP routes is Phase 3 — ROADMAP SC2 is httptest, which is what shipped |
| QA-03 | 01–05 | `go vet` / `go test ./...` green; phase ends with unit-test plan; integration uses testcontainers Postgres | ✓ SATISFIED | Plan 05 is the dedicated test plan; this session root vet/test green; `TestParitySynthetic` starts Postgres (13.297s). Per-commit green claimed in summaries; not re-audited commit-by-commit |
**Coverage:** 3/3 requirements satisfied
### Decision Coverage
All trackable CONTEXT.md decisions are honored by shipped artifacts. `gsd-sdk query check.decision-coverage-verify`: **16/16 honored**, `not_honored: []`. D-01 proxy+manifest, D-02 loopback PHP, D-03 API seed + artisan-only admin/OAuth, D-04 `summer parity:*` on bonfire, D-05/D-06 YAML flows, D-07/D-11 placeholders, D-08 `fonoteka.go/parity/`, D-09/D-10 seed + named hooks, D-12 httptest + CLI target, D-13–D-16 diff/headers/normalize/coverage.
## Behavioral Verification
| Check | Result | Detail |
|-------|--------|--------|
| Root `go vet ./... && go test ./...` | ✓ | All packages ok (this session) |
| App `go vet ./... && go test -short ./...` | ✓ | `ok git.golem15.com/golem15/fonoteka/parity` |
| `TestDiffContract` / `TestFlowContract` / `TestManifestContract` | ✓ | `ok tide 0.032s` |
| `TestParityCommands` / `TestParityCommandContract` | ✓ | Discovers `parity:record|proxy|replay`; mismatch exits nonzero |
| `go run ./cmd/summer parity:record --help` | ✓ | Flags `--spec --target --output --manifest --fixtures` |
| `parity:replay --help` / `parity:proxy --help` | ✓ | Defaults listen `127.0.0.1:8422`, upstream `http://127.0.0.1:8423` |
| `check_corpus.go --require-recorded --require-clients --check-secrets` | ✓ | `recorded 154/154` |
| `TestParitySynthetic` (testcontainers) | ✓ | Live this session, 13.297s |
| `TestParityCorpus` `-short` | ✓ | 154 pending subtests; `ported-mismatch` skipped without Docker; coverage pending=154 passing=0 |
| Full `scripts/check-phase2.sh --fresh-php` | ℹ not re-run | VALIDATION.md 2026-09-17: 116s, 154/154 PHP self-replay, client flows match, caller `PHP_PARITY_TARGET` refused. Spot-checks above confirm the same corpus and harness still hold |
## Anti-Patterns Found
No `TODO` / `FIXME` / `XXX` / `TBD` / `HACK` in phase Go/sh/mjs. No stub `not implemented` handlers on the record/replay path. No disabled requirement tests in the framework module.
| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| `tide/variables.go` `replaceIsolated` | ~530 | Short captured numeric IDs are substituted anywhere a token boundary matches | ⚠️ Warning | Catalog/pagination/IP octets that happen to equal a captured id become `{{id:album}}` / `{{id:token}}` (e.g. Rock genre id → `{{id:token}}`, `127.0.0.{{id:album}}`, CSS `1.4` in embed.js). PHP self-replay still passed because seed IDs are stable. A later Postgres port can false-fail or false-pass if sequences differ. Compare-time `normalize.go` masking is the intended ID handling; record-time global replace of 1–2 digit ids is overly broad. |
| `../fonoteka.go/parity/synthetic_test.go` | 103 | `t.Skip` under `-short` | ℹ️ Info | Fast loop skips Postgres; TestMain still fails closed without Docker when not `-short`. Phase gate runs without `-short`. |
| `TestParityCorpus/ported-mismatch` | — | Skipped under `-short` | ℹ️ Info | Needs the synthetic handler + DB; full/non-short run covers it (`TestParityContract` too). |
**Anti-patterns:** 3 found (0 blockers, 1 warning, 2 info)
## Test Quality Audit
| Test File | Linked Req | Active | Skipped | Circular | Assertion Level | Verdict |
|-----------|-----------|--------|---------|----------|----------------|---------|
| `tide/diff_contract_test.go` | QA-02 | yes | no | no | Value (path + expected/actual) | ✓ |
| `tide/flow_contract_test.go` | QA-02, D-16 | yes | no | no | Behavioral (continuation, capture) | ✓ |
| `tide/normalize_test.go` / `diff_test.go` | QA-02 | yes | no | no | Value | ✓ |
| `cmd/summer/parity_test.go` | QA-01, QA-02 | yes | no | no | Behavioral (CLI record/replay) | ✓ |
| `tide/proxy_security_test.go` | QA-01, D-07 | yes | no | no | Behavioral (loopback, scrub) | ✓ |
| `../fonoteka.go/parity/parity_test.go` | QA-01, QA-03 | yes | `-short` on DB-backed mismatch | no | Value (154 pending, fixture load) | ✓ |
| `../fonoteka.go/parity/synthetic_test.go` | QA-02, QA-03 | yes | `-short` only | no | Behavioral (SQL write/read + path diff) | ✓ |
| `../fonoteka.go/parity/parity_contract_test.go` | QA-02, QA-03 | yes | no (uses `parityDB`) | no | Value (pending≠pass) | ✓ |
| `../fonoteka.go/parity/check_corpus.go` | QA-01 | n/a (cmd) | no | no | Value (154 ids, secret scan) | ✓ |
**Disabled tests on requirements:** 0 blockers (`-short` skips are the documented fast loop, not the sign-off path).
**Circular patterns detected:** 0. Fixtures are captured from live PHP / unchanged Nuxt+MCP, not generated from the Go SUT. Synthetic expected bodies are independently authored testdata.
**Insufficient assertions:** 0 for this phase. `TestParityCorpus` pending rows only load fixtures (honest: no Go handler yet); value-level PHP match is the `--fresh-php` gate, not `go test`.
**Provenance:** VALID — expected bodies come from the PHP backend (and real clients), which is the external oracle QA-01 names.
## Human Verification Required
N/A — Infrastructure/tooling phase with no user-facing product UI. Acceptance is CLI + corpus + tests. VALIDATION.md already automated the PHP capture/self-replay and client-flow checks that were listed as manual during planning.
## Gaps Summary
**No blocking gaps.** All four ROADMAP success criteria hold in the codebase and were exercised this session (root vet/test, app short tests, live TestParitySynthetic, corpus audit, contract tests). The 154-route PHP MariaDB gate was not re-run; VALIDATION.md plus the still-present corpus and harness are consistent with that claim.
The integer-placeholder over-scrub (warning above) does not fail a Phase 2 success criterion. Phase 3's first green genres diff should treat captured-id collisions as a fixture-hygiene check if Postgres IDs diverge from the recording DB.
## Recommended Fix Plans
None — status is `passed`.
## Verification Metadata
**Verification approach:** Goal-backward from ROADMAP success criteria
**Must-haves source:** ROADMAP.md success criteria (override PLAN.md frontmatter)
**Automated checks:** 18 passed, 0 failed (gsd-sdk filename-grep false negatives excluded)
**Human checks required:** 0
**Total verification time:** ~18 min
---
*Verified: 2026-09-17T12:21:50Z*
*Verifier: the agent (subagent)*