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>
18 KiB
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 |
|
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. |
|
|
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. |
Key Link Verification
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)