Files
summercms/.planning/phases/14.1-oauth-identities-and-fonoteka-me-routes/14.1-VERIFICATION.md
Jakub Zych eb84825724 docs(14.1): record code review and verification
WR-01 is fixed; WR-02 and the three info findings stay open. The phase
goal is 8/8 with corpus 175/175/0.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-05 21:48:08 +02:00

18 KiB
Raw Blame History

phase, verified, status, score, covered_files, covered_digest, covered_files_note, behavior_unverified, overrides_applied, mvp_mode_note, decision_coverage, gaps, deferred
phase verified status score covered_files covered_digest covered_files_note behavior_unverified overrides_applied mvp_mode_note decision_coverage gaps deferred
14.1-oauth-identities-and-fonoteka-me-routes 2026-10-05T20:00:00Z passed 8/8 must-haves verified
.planning/phases/14.1-oauth-identities-and-fonoteka-me-routes/14.1-01-PLAN.md
.planning/phases/14.1-oauth-identities-and-fonoteka-me-routes/14.1-01-SUMMARY.md
.planning/phases/14.1-oauth-identities-and-fonoteka-me-routes/14.1-02-PLAN.md
.planning/phases/14.1-oauth-identities-and-fonoteka-me-routes/14.1-02-SUMMARY.md
v2:sha256:a8355b54246ac18cf06c363cbfe9fe2c15ab07074939d3b90042cc2534099f73 verification.fingerprint covers only paths under the summercms.go root. Sibling implementation at re-verification: fonoteka.go 7fc790d (master ahead 10 of origin, clean tree), sm-user-plugin cf5c6b9 (master ahead 27 of origin, clean tree). Framework tree has no 14.1 module edits. 0 0 ROADMAP marks Phase 14.1 mode: mvp, but the ROADMAP goal is not a User Story. Plan 01/02 objectives carry the story. As in Phases 1, 3, 5 and 8 to 14, ROADMAP success criteria are the contract; User Flow Coverage is derived from them plus the plan story.
honored total not_honored
13 13
truth addressed_in evidence
Live Nuxt Settings → Connected accounts and fonoteka-mcp me() against the Go backend Phase 15 Phase 15 success criteria 3 and 4: vue-fonoteka-app runs unchanged for a full manual session; fonoteka-mcp completes install/auth and representative tool calls. Plan 02 human-check is that cutover UAT, not a 14.1 code gap.

Phase 14.1: OAuth identities and fonoteka me routes Verification Report

Phase Goal: The three manifest routes no phase owned (14-CONTEXT D-09) are ported and pass the parity diff, so that no route is left pending before cutover. They are GET /_fonoteka/api/v1/oauth-identities, DELETE /_fonoteka/api/v1/oauth-identities/{provider} and GET /api/v1/fonoteka/me (the full contract, not only the minimal Phase 8 D-20 version). Verified: 2026-10-05T20:00:00Z Status: passed Re-verification: Yes — closed the DATA-07 Hidden marshal gap after WR-01 (json:"-" on ProfileData, expectedUserModels = 8, Jsonable sentinel skip). TestHiddenNeverMarshals and TestSchemaMatchesPHPSnapshot PASS on fonoteka.go 7fc790d / sm-user-plugin cf5c6b9.

MVP note: ROADMAP marks this phase mode: mvp, but the goal is not a User Story. Plan objectives carry As a signed-in Nuxt user…. ROADMAP success criteria remain the contract.

User Flow Coverage

User story (from plan objectives): As a signed-in Nuxt user (and as a fonoteka-mcp token caller), I want to list and unlink connected OAuth identities and receive the full personal-token /me body, so that Settings → Connected accounts and MCP bootstrap work against Go with zero pending routes.

Step Expected Evidence Status
List connected accounts Empty list is {"data":[]}; linked rows are facebook then google with linked_at +00:00, no secrets OAuthIdentitiesIndex; fixtures GET___fonoteka_api_v1_oauth-identities_jwt.yaml and __linked.yaml; TestOAuthIdentitiesIndexEmptyList PASS; corpus GET___fonoteka_api_v1_oauth-identities_jwt PASS ✓
Unlink one provider 204 empty when a sibling remains; Winter HTML 404 for missing/foreign/unknown; 409 localized last-method OAuthIdentitiesDestroy; DELETE fixture; D-11 tests PASS; corpus DELETE___fonoteka_api_v1_oauth-identities_{provider}_jwt PASS ✓
MCP /me bootstrap Restricted token lists collection ids; unrestricted emits "collection_ids":null; scopes stay [] MeToken; both /me fixtures; TestMeTokenHandlerNilScopesSerializeAsEmptyArrayAndCollectionIDsAsJSONNull PASS; corpus both /me routes PASS ✓
Zero pending routes Manifest 175 ported, corpus pending 0 manifest.yaml 175×ported; expectedPortedRoutes = 175; recorded 175/175 passing 175 failing 0 unrecorded 0 pending 0 ✓

Goal Achievement

Observable Truths

# Truth Status Evidence
1 GET oauth-identities lists the signed-in user's linked social identities exactly as PHP returns them ✓ VERIFIED Empty body {"data":[]} (TestOAuthIdentitiesIndexEmptyList). Linked extra seeds facebook+google Encrypted tokens; recorded PHP body is [{"provider":"facebook","linked_at":"2026-01-15T12:00:00+00:00"},{"provider":"google","linked_at":"2026-02-01T08:30:00+00:00"}]. Corpus subtest PASS. Index builds an explicit two-key map, ordered by provider, linked_at via wire.Time UTC.
2 DELETE oauth-identities/{provider} unlinks one identity with PHP's status codes and error shapes ✓ VERIFIED 204 empty + sibling remains (TestOAuthIdentitiesDestroyNoContentKeepsSibling). Last-method 409 EN/PL texts match lang YAML, including when has_self_set_password is true. Missing/foreign/unknown (linkedin) share byte-identical Winter HTML 404. Unauthenticated DELETE /google is 401 JSON. Recorded DELETE case is PHP Winter 404. Handler has no HasSelfSetPassword. Mux {provider} is unconstrained; allow-list lives in Destroy.
3 GET /api/v1/fonoteka/me matches the PHP response for fonoteka-mcp token callers ✓ VERIFIED Restricted fixture collection_ids:[{{id:collection}}], name parity-mcp. Unrestricted extra me-unrestricted records "collection_ids":null, name parity-unrestricted. Handler emits JSON null unless CollectionIDs.Valid && len>0; scopes stay wire.Slice. Unit test requires "collection_ids":null and fails on "scopes":null. Corpus both cases PASS.
4 All three manifest entries flip from pending to ported with recorded PHP cases, leaving zero pending routes ✓ VERIFIED Manifest: 175 routes, all status: ported, pending list empty. Three ids ported with cases empty GET, linked GET, missing DELETE, restricted /me, unrestricted /me. expectedPHPRoutes = expectedPortedRoutes = 175. assertPortedMismatch wraps a ported fixture with mutateStatus (no unported PHP route must not pass). Verifier run: recorded 175/175 passing 175 failing 0 unrecorded 0 pending 0.
5 golem15_user_oauth_identities exists via gormigrate with both unique indexes, cascade FK, jsonb profile_data, and lagoon.Encrypted token columns ✓ VERIFIED Migration ID 202610050001_create_oauth_identities, tx.Exec DDL, no AutoMigrate. TestOAuthIdentitiesMigration PASS: table, columns, jsonb udt, both unique indexes, user_id → users ON DELETE CASCADE, table gone after rollback. Model TableName, empty Fillable, Encrypted tokens json:"-".
6 Identity routes exist on the JWT group only; DELETE carries throttle:10,1; /_user and /api/v1/fonoteka never gain identity paths ✓ VERIFIED routes.go JWT group mounts Index/Destroy; WriteNotFound → WriteWinterHTTPError; DELETE "throttle:10,1"; no Where("provider". User plugin routes.go has no identity strings (still /_user/api/v1 only). Personal-token group serves GET /me only. test_oauth_identity_routes_exist_on_jwt_surface_only PASS (assertRouteSurfaces jwt-only + throttle contains-check).
7 GET list with seeded Encrypted tokens never contains plaintext, Encrypted JSON keys, or [redacted] ✓ VERIFIED TestOAuthIdentitiesIndexDoesNotLeakEncryptedTokensOrProfileData PASS. Linked PHP fixture body has only provider/linked_at despite seeded parity-oauth-*-SECRET and profile emails on both PHP reset and Go seedParityOAuthIdentities.
8 Registering OAuthIdentity keeps the DATA-07 Hidden marshal backstop green ✓ VERIFIED ProfileData is json:"-" (cf5c6b9). expectedUserModels = 8 and Jsonable sentinel skip (7fc790d). go -C fonoteka.go test ./plugins/golem15/fonoteka/classes -count=1 -run TestHiddenNeverMarshals PASS.

Score: 8/8 truths verified (0 present, behavior-unverified)

Deferred Items

# Item Addressed In Evidence
1 Live Nuxt Connected accounts list/unlink and fonoteka-mcp me() against Go Phase 15 Phase 15 SC3/SC4. 14.1's QA-05 slice is the corpus at 175/175/0.

D-08 (Winter-import mapper / Laravel decrypt) and D-13 (social-login-only lockout preflight) stay out of this phase per CONTEXT; they are Phase 15, not 14.1 gaps.

Required Artifacts

gsd-tools verify.artifacts: plan 01 6/6 passed, plan 02 4/4 passed. Manual three-level check:

Artifact Expected Status Details
plugins/golem15/user/models/oauth_identity.go OAuthIdentity, TableName, Hidden, Register ✓ VERIFIED Exists, substantive, registered. ProfileData is json:"-" with Encrypted tokens. HTTP list is FLOWING.
plugins/golem15/user/updates/202610050001_create_oauth_identities.go gormigrate ID + unique indexes ✓ VERIFIED DDL + rollback; wired via init Register.
plugins/golem15/user/controllers/oauth_identities.go Index/Destroy/Options ✓ VERIFIED Wired from fonoteka JWT group; queries OAuthIdentity by user_id.
plugins/golem15/user/lang/{en,pl}/lang.yaml last_method_blocked ✓ VERIFIED Exact PHP EN/PL strings; handler key golem15.user::lang.oauth.last_method_blocked.
plugins/golem15/fonoteka/routes.go JWT GET/DELETE mounts ✓ VERIFIED Lines 240–246.
plugins/golem15/fonoteka/controllers/api/me_token_controller.go D-09 null collection_ids ✓ VERIFIED No wire.Slice(collectionIDs).
plugins/golem15/user/oauth_identities_test.go D-11 + secret-leak ✓ VERIFIED Eight TestOAuthIdentities* funcs, all PASS this run.
plugins/golem15/user/updates/oauth_identities_test.go migration up/down ✓ VERIFIED PASS this run.
plugins/golem15/fonoteka/controllers/api/me_token_controller_test.go "collection_ids":null ✓ VERIFIED PASS this run.
plugins/golem15/fonoteka/phase08_coverage_test.go jwt-only surfaces ✓ VERIFIED Subtest PASS.
plugins/golem15/user/README.md host-mounted constructors ✓ VERIFIED OAuthIdentitiesIndex/Destroy/Options; says "the host application"; no consuming-application name.

gsd-tools verify.key-links rejected sibling ../fonoteka.go/... paths (Source path rejected — resolves outside the project directory). Manual wiring:

From To Via Status Details
fonoteka routes.go controllers/oauth_identities.go JWT OAuthIdentitiesIndex / OAuthIdentitiesDestroy; WriteNotFound → WriteWinterHTTPError WIRED Import sm-user-plugin/controllers as userctrl.
oauth_identities.go models/oauth_identity.go Index/Destroy Where("user_id = ?", user.ID) WIRED Count-then-delete is sequential (see WR-02).
me_token_controller.go models.ApiToken bouncer.Credential as *models.ApiToken, CollectionIDs WIRED FLOWING from matched credential; no re-query.
oauth_identities_test.go constructors httptest + bouncer.WithUser WIRED All D-11 tests call Index/Destroy.
parity_test.go manifest.yaml TestParityCorpus 175/175 WIRED Constants and coverage subtest.

Data-Flow Trace (Level 4)

Artifact Data Variable Source Produces Real Data Status
OAuthIdentitiesIndex data[].provider, linked_at GORM Find on golem15_user_oauth_identities for caller user_id Yes (empty slice or DB rows) ✓ FLOWING
OAuthIdentitiesDestroy 204 / 404 / 409 Count + Delete of caller rows; host Winter 404 writer Yes ✓ FLOWING
MeToken collection_ids, scopes bouncer.Credential *models.ApiToken Yes (null vs int list; Slice for scopes) ✓ FLOWING
Linked GET fixture data PHP-recorded extras; Go seedParityOAuthIdentities Yes; secrets not in body ✓ FLOWING

Behavioral Spot-Checks

Behavior Command Result Status
D-11 + secret-leak + empty list + 204 go -C $FONOTEKA test ./plugins/golem15/user ./plugins/golem15/user/updates -count=1 -run 'TestOAuthIdentities' All PASS including TestOAuthIdentitiesMigration (10.3s / 10.8s) ✓ PASS
jwt-only surfaces + MeToken null go -C $FONOTEKA test ./plugins/golem15/fonoteka ./plugins/golem15/fonoteka/controllers/api -count=1 -run 'TestOAuthTokenSurfaceIsolationCoverage|TestMeToken' test_oauth_identity_routes_exist_on_jwt_surface_only PASS; MeToken nil/restricted/no-requery PASS ✓ PASS
Corpus 175/175/0 go -C $FONOTEKA test ./parity -count=1 -v -run 'TestParityCorpus' -timeout 30m recorded 175/175 passing 175 failing 0 unrecorded 0 pending 0; oauth-identities GET/DELETE and both /me subtests PASS (47s replay) ✓ PASS
DATA-07 Hidden marshal backstop go -C $FONOTEKA test ./plugins/golem15/fonoteka/classes -count=1 -run 'TestHiddenNeverMarshals' PASS on 7fc790d (count 8, ProfileData json:"-") ✓ PASS

Probe Execution

Probe Command Result Status
— — No scripts/*/tests/probe-*.sh and no phase-declared probes SKIPPED

Requirements Coverage

REQUIREMENTS.md maps none of these IDs to Phase 14.1 (Requirements: TBD on the ROADMAP row). Plans claimed them as continuation of completed v1 rows. No orphaned IDs (nothing mapped to 14.1 that a plan omitted). Every claimed ID is accounted for:

Requirement Source Plan Description Status Evidence
API-09 01, 02 All routes on correct groups with identical paths/methods/status/bodies ✓ SATISFIED for this slice Corpus 175/175/0; three former pending ids ported
HTTP-01 01, 02 Unknown/malformed ids 404 on ownership-scoped resources ✓ SATISFIED Unconstrained {provider}; missing/foreign/unknown share Winter 404
HTTP-03 01, 02 JWT vs personal-token vs public groups ✓ SATISFIED jwt-only identity surfaces; token group has /me not identities
HTTP-04 01 Inline throttles 1:1 ✓ SATISFIED DELETE "throttle:10,1"
HTTP-06 01 [] vs null, +00:00 timestamps ✓ SATISFIED Empty list []; linked_at +00:00; /me collection_ids null exception per D-09
DATA-02 01, 02 gormigrate up/down; AutoMigrate never schema source ✓ SATISFIED Migration test PASS; no AutoMigrate
DATA-07 01, 02 Jsonable + Encrypted hidden from serialization ✓ SATISFIED HTTP list leak tests PASS; TestHiddenNeverMarshals PASS after json:"-" on ProfileData
I18N-01 01, 02 vendor.plugin::group.key YAML en/pl ✓ SATISFIED golem15.user::lang.oauth.last_method_blocked; EN/PL 409 tests PASS
QA-05 02 Parity green; consumers unchanged ✓ SATISFIED for this slice Corpus 175/175/0. Live Nuxt/MCP deferred to Phase 15

Prohibitions (test-tier, wired): DELETE allow-list in handler not mux (Winter 404 tests + no Where("provider"); two-key list map (secret-leak test); constructors not on /_user or token group (phase08); last-method count-only (409-with-password); gormigrate not AutoMigrate; 401 probe uses /google; pending never counts as passing (expectedPortedRoutes = 175).

Decision Coverage

All 13 trackable CONTEXT.md decisions (D-01..D-13) are honored by shipped artifacts (check.decision-coverage-verify: honored 13/13, blocking false). D-08 and D-13 are honored as explicit deferrals to Phase 15.

Test Quality Audit

Test File Linked Req Active Skipped Circular Assertion Level Verdict
user/oauth_identities_test.go HTTP-01, I18N-01, DATA-07 7 tests 0 No Behavioral (204/409/401/404 bytes, leak needles) OK
user/updates/oauth_identities_test.go DATA-02 1 0 (-short skip via dedicatedDB only) No Value (jsonb, index names, CASCADE) OK
me_token_controller_test.go HTTP-06, D-09 3 0 No Value ("collection_ids":null) OK
phase08_coverage_test.go HTTP-03, HTTP-04 oauth-identity subtest 0 No Behavioral (jwt-only + throttle) OK
parity/parity_test.go API-09, QA-05 TestParityCorpus -short skips replay No (PHP-recorded fixtures) Behavioral replay OK
fonoteka/classes/hidden_marshal_test.go DATA-07 1 0 No Value (model count + json tags) OK

Disabled tests on requirements: 0 Circular patterns detected: 0 (linked /me fixtures recorded from isolated PHP, not from Go) Insufficient assertions: 0 on the named 14.1 tests Provenance: VALID — PHP php_parity.sh recordings for empty GET, linked GET, DELETE 404, restricted /me, unrestricted /me

Anti-Patterns Found

File Line Pattern Severity Impact
controllers/oauth_identities.go 81–98 Count then Delete without transaction ⚠️ Warning 14.1-REVIEW WR-02: two concurrent unlinks of the last two providers can both observe n==2. Sequential PHP/Go tests cannot see it. throttle:10,1 does not serialize in-flight requests.
oauth_identities_test.go 155–180 Winter 404 asserted against a stub, not WriteWinterHTTPError ℹ️ Info IN-01. Host wiring + recorded DELETE fixture still prove production bytes.
me_token_controller_test.go nil token Valid && empty slice branch untested ℹ️ Info IN-02. Handler condition is Valid && len>0; unused branch.
Index Find 31–48 Decrypts Encrypted columns unused by the response ℹ️ Info IN-03. Not a JSON leak.

No TBD/FIXME/XXX in phase implementation files. Plan 01 SUMMARY overclaimed json:"-" on profile_data; the PLAN task text only required Encrypted tokens tagged json:"-" and Hidden() for all three.

Human Verification Required

N/A — API-parity / foundation phase. ROADMAP success criteria are programmatically checkable. Plan 02's live Nuxt/MCP check is deferred to Phase 15 (see Deferred Items).

Gaps Summary

None. The three orphan routes are ported and the parity corpus is 175/175/0. The DATA-07 marshal backstop is green after the WR-01 fix. WR-02 remains an open review warning, not a failed success criterion.


Verified: 2026-10-05T20:00:00Z Verifier: the agent (gsd-verifier)