diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 1e53dfa..46d10ca 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -154,13 +154,88 @@ Which phases cover which requirements. Updated during roadmap creation. | Requirement | Phase | Status | |-------------|-------|--------| -| (filled by roadmap) | | | +| KERN-01 | Phase 1 | Pending | +| KERN-02 | Phase 1 | Pending | +| KERN-03 | Phase 1 | Pending | +| KERN-04 | Phase 1 | Pending | +| KERN-05 | Phase 1 | Pending | +| KERN-06 | Phase 1 | Pending | +| KERN-07 | Phase 1 | Pending | +| KERN-08 | Phase 1 | Pending | +| KERN-09 | Phase 1 | Pending | +| CLI-01 | Phase 1 | Pending | +| CLI-02 | Phase 4 | Pending | +| CLI-03 | Phase 5 | Pending | +| CLI-04 | Phase 11 | Pending | +| CLI-05 | Phase 14 | Pending | +| CLI-06 | Phase 11 | Pending | +| I18N-01 | Phase 4 | Pending | +| I18N-02 | Phase 7 | Pending | +| I18N-03 | Phase 4 | Pending | +| DATA-01 | Phase 3 | Pending | +| DATA-02 | Phase 3 | Pending | +| DATA-03 | Phase 5 | Pending | +| DATA-04 | Phase 5 | Pending | +| DATA-05 | Phase 5 | Pending | +| DATA-06 | Phase 5 | Pending | +| DATA-07 | Phase 5 | Pending | +| DATA-08 | Phase 5 | Pending | +| DATA-09 | Phase 5 | Pending | +| DATA-10 | Phase 5 | Pending | +| DATA-11 | Phase 5 | Pending | +| HTTP-01 | Phase 3 | Pending | +| HTTP-02 | Phase 3 | Pending | +| HTTP-03 | Phase 6 | Pending | +| HTTP-04 | Phase 6 | Pending | +| HTTP-05 | Phase 6 | Pending | +| HTTP-06 | Phase 6 | Pending | +| HTTP-07 | Phase 6 | Pending | +| HTTP-08 | Phase 6 | Pending | +| HTTP-09 | Phase 6 | Pending | +| AUTH-01 | Phase 7 | Pending | +| AUTH-02 | Phase 7 | Pending | +| AUTH-03 | Phase 7 | Pending | +| AUTH-04 | Phase 7 | Pending | +| AUTH-05 | Phase 8 | Pending | +| AUTH-06 | Phase 8 | Pending | +| AUTH-07 | Phase 8 | Pending | +| AUTH-08 | Phase 9 | Pending | +| API-01 | Phase 12 | Pending | +| API-02 | Phase 12 | Pending | +| API-03 | Phase 13 | Pending | +| API-04 | Phase 13 | Pending | +| API-05 | Phase 13 | Pending | +| API-06 | Phase 13 | Pending | +| API-07 | Phase 13 | Pending | +| API-08 | Phase 14 | Pending | +| API-09 | Phase 15 | Pending | +| JOBS-01 | Phase 11 | Pending | +| JOBS-02 | Phase 14 | Pending | +| JOBS-03 | Phase 14 | Pending | +| RT-01 | Phase 11 | Pending | +| RT-02 | Phase 11 | Pending | +| RT-03 | Phase 11 | Pending | +| SRCH-01 | Phase 11 | Pending | +| SRCH-02 | Phase 14 | Pending | +| INTG-01 | Phase 14 | Pending | +| INTG-02 | Phase 14 | Pending | +| ADMIN-01 | Phase 9 | Pending | +| ADMIN-02 | Phase 9 | Pending | +| ADMIN-03 | Phase 9 | Pending | +| ADMIN-04 | Phase 9 | Pending | +| ADMIN-05 | Phase 9 | Pending | +| ADMIN-06 | Phase 10 | Pending | +| QA-01 | Phase 2 | Pending | +| QA-02 | Phase 2 | Pending | +| QA-03 | Phase 2 | Pending | +| QA-04 | Phase 3 | Pending | +| QA-05 | Phase 15 | Pending | **Coverage:** - v1 requirements: 76 total -- Mapped to phases: 0 -- Unmapped: 76 ⚠️ +- Mapped to phases: 76 +- Unmapped: 0 ✓ --- *Requirements defined: 2026-09-16* -*Last updated: 2026-09-16 after initial definition* +*Last updated: 2026-09-16 after roadmap revision (15 phases, split former Phase 13 into Phase 11 jobs/realtime/search infrastructure and Phase 14 domain jobs/integrations, reordered before the API phases; 100% coverage)* diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md new file mode 100644 index 0000000..5eb56c1 --- /dev/null +++ b/.planning/ROADMAP.md @@ -0,0 +1,261 @@ +# Roadmap: SummerCMS (Go) + +## Overview + +v1 ports the Płytarium (fonoteka) headless PHP backend to a single Go binary without vue-fonoteka-app or fonoteka-mcp noticing. The journey starts with the smallest possible kernel plus a day-one parity harness, proves the whole stack on one real endpoint (`GET /_fonoteka/api/v1/genres`) before any further kernel design, then broadens outward: full data-layer fidelity, the three-auth-group HTTP layer, the user plugin, OAuth2.1 for MCP/ChatGPT, the admin schema pipeline and its Vue SPA, jobs/realtime/search infrastructure, the 154-route API surface (split into two delivery slices), domain jobs and external integrations, and finally a cutover phase where the parity harness is green on all 154 routes and both real clients run unchanged against the Go backend. + +## Phases + +**Phase Numbering:** +- Integer phases (1, 2, 3): Planned milestone work +- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED) + +Decimal phases appear between their surrounding integers in numeric order. + +- [ ] **Phase 1: Framework kernel foundation** - Config, plugin registry, event bus, container and CLI skeleton boot the `summer` binary +- [ ] **Phase 2: API parity harness bootstrap** - Fixture recorder + replay-and-diff harness against the live PHP backend, day-one workstream +- [ ] **Phase 3: First vertical slice — genres end to end** - `GET /_fonoteka/api/v1/genres` passes the parity diff through every layer +- [ ] **Phase 4: CLI scaffolding, i18n and mail** - Scaffolding commands, translated/pluralized strings, mail templates +- [ ] **Phase 5: Data layer full fidelity** - All 25 models and 27 migrations with fillable/hidden/cast/soft-delete discipline +- [ ] **Phase 6: HTTP routing, auth groups and rate limiting** - Three auth groups, named rate buckets, OAuth-safe middleware structure +- [ ] **Phase 7: User plugin and authentication** - Registration, login, JWT, organizations, personal tokens, must-change-password +- [ ] **Phase 8: OAuth2.1 authorization server** - zitadel/oidc server for fonoteka-mcp and the ChatGPT connector +- [ ] **Phase 9: Backend admin authentication and schema pipeline** - Admin roles, fields.yaml/columns.yaml, relation manager +- [ ] **Phase 10: Admin Vue SPA** - Login, navigation, lists, forms and relation manager for five controllers +- [ ] **Phase 11: Jobs, realtime and search infrastructure** - River, Centrifugo and Typesense sync brought up before the API phases that need them +- [ ] **Phase 12: Płytarium API — Collections and Albums** - Core content endpoints ported with byte-level parity +- [ ] **Phase 13: Płytarium API — wishlist, notifications, CSV, credentials, public routes** - Remaining core API surface +- [ ] **Phase 14: Domain jobs and external integrations** - CSV/Discogs jobs, wishlist digest, reindex, Discogs client, AI recognition, feedback, sitemap +- [ ] **Phase 15: Cutover** - Parity harness green on all 154 routes, both real clients run unchanged + +## Phase Details + +### Phase 1: Framework kernel foundation +**Goal**: The `summer` binary boots from layered YAML config, plugins self-register through one required interface plus optional capability interfaces, a typed event bus and service registry are available, and a CLI command framework with a dev watch loop exists — built only as far as the first vertical slice will need it, per the interleaved-not-sequential kernel approach. +**Mode:** mvp +**Depends on**: Nothing (first phase) +**Repos:** summercms.go +**Requirements**: KERN-01, KERN-02, KERN-03, KERN-04, KERN-05, KERN-06, KERN-07, KERN-08, KERN-09, CLI-01 +**Success Criteria** (what must be TRUE): + 1. `summer build` regenerates the blank-import plugin list and produces a binary that boots from a layered YAML config (base files, env overlay directory, per-plugin namespace, environment variables) with dot-path access. + 2. A throwaway plugin registered via the Plugin interface has its Register phase run for all plugins before any Boot phase runs, in `Requires()`-topological order, verified by a test with reordered input. + 3. A plugin can type-assert an optional capability interface (e.g. `HasModels`) and skip integration with another plugin that isn't registered, with no hard import. + 4. The typed event bus supports fire-and-forget, fire-and-collect, and fire-until-handled dispatch, exercised by unit tests; per-request state travels only through `context.Context`, never a package-level global. + 5. The `summer` CLI discovers a registered command and renders rich output (spinner, progress, table, prompts) with non-TTY degradation, and a dev watch loop rebuilds and restarts the binary on source change. +**Plans**: TBD + +### Phase 2: API parity harness bootstrap +**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. This is a day-one workstream with no dependency on framework progress, since it only needs a running PHP backend to record against. +**Mode:** mvp +**Depends on**: Nothing (parallel with Phase 1) +**Repos:** summercms.go, fonoteka.go +**Requirements**: QA-01, QA-02, QA-03 +**Success Criteria** (what must be TRUE): + 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. + 2. The replay-and-diff harness runs a recorded fixture against an arbitrary `httptest.Server` backend and reports a byte-level diff. + 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. + 4. `go vet` and `go test ./...` are green, and the harness's own integration tests run against testcontainers Postgres. +**Plans**: TBD + +### Phase 3: First vertical slice — genres end to end +**Goal**: `GET /_fonoteka/api/v1/genres` passes the parity diff end to end (config → DB → plugin registry → JWT-guarded routing → GORM → JSON), proving every kernel layer together before any further kernel abstraction. Touches the JWT auth guard on the request path — treat as security-load-bearing and apply the security-review agent even though the guard is a throwaway seeded-token implementation at this stage. +**Mode:** mvp +**Depends on**: Phase 1, Phase 2 +**Repos:** summercms.go, fonoteka.go +**Requirements**: DATA-01, DATA-02, HTTP-01, HTTP-02, QA-04 +**Success Criteria** (what must be TRUE): + 1. GORM connects to Postgres over one shared `*sql.DB` (pgx stdlib); a gormigrate migration set runs up and down for a single plugin with per-plugin version tracking, and `AutoMigrate` is not the schema source. + 2. A plugin registers a route group on `net/http` ServeMux with typed params, and a request is served through the full middleware pipeline (recover, CORS, locale, auth group, must-change-password, org context, rate limit, handler). + 3. A JWT-guarded `GET /_fonoteka/api/v1/genres` route returns a real `Genre` row read from Postgres. + 4. The response is diffed byte-for-byte against a fixture recorded from the live PHP backend using the Phase 2 harness, and the diff is green. +**Plans**: TBD + +### Phase 4: CLI scaffolding, i18n and mail +**Goal**: Scaffolding commands generate compiling stubs for every plugin artifact type, and plugins can register translated, CLDR-pluralized, namespaced strings and mail templates rendered through a pluggable driver interface. +**Mode:** mvp +**Depends on**: Phase 1 +**Repos:** summercms.go +**Requirements**: CLI-02, I18N-01, I18N-03 +**Success Criteria** (what must be TRUE): + 1. `summer make:plugin`, `make:model`, `make:migration`, `make:command`, `make:job` and `make:admin-controller` each generate stubs that compile and pass `go vet`. + 2. A translation key `vendor.plugin::group.key` resolves for pl and en, including a CLDR plural form, loaded from per-plugin per-locale YAML files with parameter substitution. + 3. A plugin registers a mail template and layout by dotted name with the per-locale suffix convention, and it renders via `html/template` through a driver interface (SMTP via go-mail) in a test send. +**Plans**: TBD + +### Phase 5: Data layer full fidelity +**Goal**: All 25 Płytarium models and 27 migrations are ported with matching relations, casts, hooks, and the fillable/hidden/encrypted-cast mass-assignment and serialization discipline. This is security-load-bearing — mass-assignment boundaries and encrypted-at-rest credential casts are named security invariants in the PHP source — apply the security-review agent and the DTO-vs-model convention from the first model onward. +**Mode:** mvp +**Depends on**: Phase 1, Phase 3 +**Repos:** summercms.go, fonoteka.go +**Requirements**: DATA-03, DATA-04, DATA-05, DATA-06, DATA-07, DATA-08, DATA-09, DATA-10, DATA-11, CLI-03 +**Success Criteria** (what must be TRUE): + 1. All 25 models exist with matching table names, columns, indexes and defaults; all 27 migrations run up and down individually, and `summer migrate:rollback --plugin=fonoteka` rolls back only that plugin's last migration. + 2. A many-to-many relation with pivot business columns (`album_artists.sort_order`, `CollectionEditor.role/granted_at/granted_by`) round-trips correctly for a 3+ artist album; belongsTo/hasOne/hasMany relations return ordered results. + 3. Every write endpoint uses a request DTO that enforces its model's fillable allow-list (a fuzz test posting unknown fields asserts they are rejected or ignored, never persisted), and serialization honors the hidden deny-list with an explicit per-call override. + 4. The money cast round-trips the PHP ceiling and blank-string cases as a fixed 4-decimal JSON string (never `float64`), and an encrypted-at-rest credential column is AES-GCM encrypted at rest and hidden from serialization. + 5. Paginated responses use the exact `{data, meta{current_page,last_page,per_page,total}}` envelope with no `links` key; another plugin extends a model's lifecycle through the GORM callback registry and a companion migration without editing the owning plugin's file; soft-deletable + uniquely-keyed tables pass a delete-then-recreate test. +**Plans**: TBD + +### Phase 6: HTTP routing, auth groups and rate limiting +**Goal**: The three mutually exclusive auth groups (JWT, personal token, public/onboarding) share handlers with correct route subsets, named rate-limit buckets are ported 1:1, and OAuth/RFC routes are structurally exempted from any house envelope or error middleware. Security-load-bearing — auth guard registry, rate limiting and the SSRF-guarded outbound fetch helper all live here; apply the security-review agent. +**Mode:** mvp +**Depends on**: Phase 3, Phase 5 +**Repos:** summercms.go, fonoteka.go +**Requirements**: HTTP-03, HTTP-04, HTTP-05, HTTP-06, HTTP-07, HTTP-08, HTTP-09 +**Success Criteria** (what must be TRUE): + 1. The same handler serves both a JWT-authenticated request under `/_fonoteka/api/v1` and a personal-token request under `/api/v1/fonoteka`, with public/onboarding groups reachable without auth; unknown and malformed ids on ownership-scoped resources both return 404. + 2. All seven named rate-limit buckets are enforced with the documented keys and limits, including a route stacking two limiters. + 3. Response conventions hold under test: empty arrays serialize as `[]`, timestamps carry `+00:00`, tri-state booleans keep `null`, conditional keys are omitted not nulled; an OAuth-group route carries no house envelope or error middleware, verified by route-registration inspection. + 4. The guarded outbound fetch helper rejects a non-allow-listed host and enforces a byte cap and timeout on a user-supplied cover URL fetch (manual cover URL, Discogs cover). + 5. OpenAPI is generated from swaggo/swag annotations on handlers and `openapi-typescript` produces valid TypeScript types from it; CORS and JSON body-size limits match the PHP deployment. +**Plans**: TBD + +### Phase 7: User plugin and authentication +**Goal**: The user plugin is ported with registration, login, JWT issue/refresh, organizations, personal API tokens and the must-change-password lock. Security-load-bearing — password auth, token scope ceilings and the session lock all live here; apply the security-review agent. +**Mode:** mvp +**Depends on**: Phase 6 +**Repos:** summercms.go, fonoteka.go +**Requirements**: AUTH-01, AUTH-02, AUTH-03, AUTH-04, I18N-02 +**Success Criteria** (what must be TRUE): + 1. A user can register, log in, log out, reset a forgotten password by email, verify email, and receive/refresh a JWT (golang-jwt) with the same claims and cookie behavior the Nuxt app expects. + 2. Organization fields appear on the user payload through a fire-and-collect event the fonoteka plugin populates, without the user plugin importing fonoteka. + 3. A personal API token is created with a read|write|ai scope ceiling, listed, and revoked; a scope-checking middleware rejects an out-of-scope request. + 4. The must-change-password flag returns 423 on the authenticated surface except the locale and password-change routes, and locale resolves per request from the user's persisted `preferred_locale` with header fallback even while the lock is active. +**Plans**: TBD + +### Phase 8: OAuth2.1 authorization server +**Goal**: An RFC 8414/6749/7591-compliant OAuth2.1 server on zitadel/oidc serves fonoteka-mcp and the ChatGPT connector unchanged, including exact `WWW-Authenticate` and protected-resource-metadata headers. Security-load-bearing — bearer tokens, PKCE and constant-time secret comparison all live here; apply the security-review agent. +**Mode:** mvp +**Depends on**: Phase 6, Phase 7 +**Repos:** summercms.go, fonoteka.go +**Requirements**: AUTH-05, AUTH-06, AUTH-07 +**Success Criteria** (what must be TRUE): + 1. RFC 8414 metadata is served unenveloped at its well-known path; authorize with PKCE and a consent screen works, and the token endpoint issues/refreshes authorization_code and refresh_token flows, all as unwrapped RFC 6749 bodies with the PHP cache headers (`Cache-Control: no-store`, `Pragma: no-cache`). + 2. RFC 7591 dynamic client registration and RFC 8707 resource parameter tolerance both work against a real client registration call. + 3. A 401 on a protected route carries the exact `WWW-Authenticate` and protected-resource-metadata header contract, verified against fonoteka-mcp's actual discovery flow, not just a Go unit test. + 4. Connected apps can be listed and revoked; `OAuthClient`/`OAuthAuthCode`/`OAuthRefreshToken` models persist correctly; fonoteka-mcp completes its install and auth flow unchanged; client-secret comparison uses `crypto/subtle.ConstantTimeCompare`. +**Plans**: TBD +**Research flag:** yes + +### Phase 9: Backend admin authentication and schema pipeline +**Goal**: Backend admin users with roles are separate from frontend users and gate navigation and controller access; `fields.yaml`/`columns.yaml` drive a JSON form/list schema, including a first-class relation-manager schema replacing the one `partial` field. Security-load-bearing — separate admin authentication and permissions registry live here; apply the security-review agent. This is also the least-precedented design surface in the research (one real relation-manager usage in the PHP source, no direct library equivalent). +**Mode:** mvp +**Depends on**: Phase 5, Phase 6 +**Repos:** summercms.go, fonoteka.go +**Requirements**: AUTH-08, ADMIN-01, ADMIN-02, ADMIN-03, ADMIN-04, ADMIN-05 +**Success Criteria** (what must be TRUE): + 1. A backend admin user with a role logs in separately from frontend users, and navigation/controller access is gated by the permissions registry. + 2. `fields.yaml` for a real controller parses (goccy/go-yaml) into a JSON form schema covering text, textarea, checkbox, switch, dropdown (model-method options) and relation (nameFrom, emptyOption), with span/tabs/context/attributes layout hints. + 3. `columns.yaml` parses into a JSON list schema with searchable/sortable/relation columns and datetime/switch renderers. + 4. The relation-manager schema supports search/link/unlink/manage-or-view lists for Collections' editors tab, replacing the `partial` field entirely. + 5. Admin CRUD endpoints expose `listExtendQuery`/`formExtendQuery`/`formBeforeCreate`/`formBeforeUpdate`/`relationExtendManageQuery` hooks, bulk delete runs each record's lifecycle hooks, and the Settings model binds to a settings screen through the same schema pipeline. +**Plans**: TBD +**Research flag:** yes + +### Phase 10: Admin Vue SPA +**Goal**: A minimal Vue 3 + TypeScript admin SPA renders login, permission-gated navigation, lists, forms and the relation manager for Albums, Artists, Collections, Genres and Styles, typed from the generated OpenAPI document. +**Mode:** mvp +**Depends on**: Phase 9 +**Repos:** summercms.go, fonoteka.go +**Requirements**: ADMIN-06 +**Success Criteria** (what must be TRUE): + 1. An admin logs in through the SPA and sees only the navigation items their permissions allow. + 2. Each of the five controllers (Albums, Artists, Collections, Genres, Styles) renders a working list and form generated from its JSON schema. + 3. The Collections form's relation manager lets an admin search, link and unlink an editor. + 4. API calls in the SPA use TypeScript types generated from the OpenAPI document, with no hand-maintained duplicate type. +**Plans**: TBD +**UI hint**: yes + +### Phase 11: Jobs, realtime and search infrastructure +**Goal**: River jobs run on the correct dual-driver split, Centrifugo publishing and channel authorization match the existing server, and Typesense sync stays a re-gated pre-filter — all brought up before the API phases that depend on them (album search needs Typesense sync, CSV import needs River jobs, notifications need the realtime publisher). River's dual-driver split and the Centrifugo/Typesense contracts are the least-implemented-and-verified parts of this research pass. +**Mode:** mvp +**Depends on**: Phase 5, Phase 7 +**Repos:** summercms.go, fonoteka.go +**Requirements**: JOBS-01, CLI-04, CLI-06, RT-01, RT-02, RT-03, SRCH-01 +**Success Criteria** (what must be TRUE): + 1. River runs on the shared `*sql.DB` (`riverdatabasesql`, transactional enqueue) with a separate `riverpgxv5` client for LISTEN/NOTIFY dispatch, verified by a timed test that job pickup is not poll-interval latency; job outcomes (complete, fail, skip) are queryable through a job manager service. + 2. The queue worker runs via `summer queue:work` and the scheduler runs recurring commands via `summer schedule:run`. + 3. The websockets plugin issues connection and subscription JWTs and publishes to the existing Centrifugo server with the same secret, claims and channel names, served at `GET /api/realtime/token`. + 4. A channel-namespace authorizer registry re-validates on every subscribe; a broadcastable model interface with bulk-write suppression emits exactly one summary event for a bulk operation. + 5. Typesense sync is scoped by `collection_id` behind a settings kill-switch and degrades gracefully without DB/config. +**Plans**: TBD +**Research flag:** yes + +### Phase 12: Płytarium API — Collections and Albums +**Goal**: Collections and Albums endpoints are ported with byte-compatible request/response shapes, including active-context switching, editor invitations, ratings, reservations, cover handling and search. +**Mode:** mvp +**Depends on**: Phase 5, Phase 6, Phase 7, Phase 11 +**Repos:** fonoteka.go +**Requirements**: API-01, API-02 +**Success Criteria** (what must be TRUE): + 1. Collections CRUD, the `me/context` active-collection switch (returning an opaque channel name), editor invitation/acceptance, share-link regeneration and public token views all pass the parity diff. + 2. Albums CRUD, ratings, reservations, photo upload, manual cover URL and Discogs cover price all pass the parity diff. + 3. Album search treats Typesense results as a pre-filter re-gated in SQL, verified by a security test that a stale/mis-scoped search document cannot leak an unauthorized result. + 4. Artists/genres/styles lookup endpoints used by the Albums UI pass the parity diff. +**Plans**: TBD + +### Phase 13: Płytarium API — wishlist, notifications, CSV, credentials, public routes +**Goal**: The remaining core API surface — wishlist, notifications, CSV import/export, per-user/org credentials, and onboarding/public/invitation routes — is ported with byte-compatible shapes and their own public rate-limit buckets. +**Mode:** mvp +**Depends on**: Phase 11, Phase 12 +**Repos:** fonoteka.go +**Requirements**: API-03, API-04, API-05, API-06, API-07 +**Success Criteria** (what must be TRUE): + 1. Wishlist items, subscriptions, `public-wishlist/{token}` views and purchase/digest triggers pass the parity diff. + 2. Notifications list/mark-read/prune endpoints pass the parity diff (the realtime token endpoint itself is owned by the websockets plugin, ported in Phase 11). + 3. CSV import runs as a multi-step session (store, show/poll, mapping patch, per-row edit, commit, cancel) and CSV export works on both authenticated groups, all passing the parity diff. + 4. Per-user and per-org Discogs/AI credentials CRUD store encrypted values, honor the org-lock flag, and resolve env-to-org-to-user correctly. + 5. Onboarding, public and invitation-inspection routes are reachable without auth and enforce their own public rate-limit buckets. +**Plans**: TBD + +### Phase 14: Domain jobs and external integrations +**Goal**: The domain-specific River jobs (CSV import write, Discogs match, wishlist digest), the reindex command, the Discogs client and AI cover recognition are ported on top of the Phase 11 jobs/realtime/search infrastructure and the Phase 13 API surface they serve. +**Mode:** mvp +**Depends on**: Phase 11, Phase 13 +**Repos:** fonoteka.go (plus summercms.go only if a framework helper is needed) +**Requirements**: JOBS-02, JOBS-03, SRCH-02, INTG-01, INTG-02, API-08, CLI-05 +**Success Criteria** (what must be TRUE): + 1. The CSV import write job and the self-redispatching Discogs match job (240s timeout, self-redispatching with a delay on a Discogs rate-limit error) both complete correctly. + 2. The wishlist digest job coalesces a 30-minute window and deletes its queue row on completion. + 3. The `reindex` command asserts zero `collection_id`-0 documents before and after and can drop the legacy index. + 4. The Discogs client enforces its proactive rate threshold, bounded wait budget, retry-after fallback and host-locked cover fetch. + 5. AI cover recognition works through both Anthropic and OpenAI-compatible adapters with per-credential model/base-URL overrides. + 6. Feedback submissions and sitemap output work; the ported `oauth-client`/`prune-notifications`/`reindex` commands all run correctly. +**Plans**: TBD + +### Phase 15: Cutover +**Goal**: The parity harness is green on all 154 routes and `vue-fonoteka-app` and `fonoteka-mcp` run unchanged against the Go backend in daily use — the project's definition of done. +**Mode:** mvp +**Depends on**: Phase 2, Phase 8, Phase 9, Phase 10, Phase 12, Phase 13, Phase 14 +**Repos:** fonoteka.go, summercms.go +**Requirements**: API-09, QA-05 +**Success Criteria** (what must be TRUE): + 1. All 154 routes are registered on the correct auth groups with identical paths, methods and status codes. + 2. The parity harness runs green across recorded fixtures for all 154 routes. + 3. `vue-fonoteka-app` runs unchanged against the Go backend for a full manual session (browse, edit, upload, invite, OAuth-connect an MCP client). + 4. `fonoteka-mcp` completes its install/auth flow and a representative set of tool calls unchanged against the Go backend. +**Plans**: TBD + +## Progress + +**Execution Order:** +Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9 → 10 → 11 → 12 → 13 → 14 → 15 +(Phase 2 has no dependency on Phase 1 and may be executed in parallel with it.) + +| Phase | Plans Complete | Status | Completed | +|-------|----------------|--------|-----------| +| 1. Framework kernel foundation | 0/TBD | Not started | - | +| 2. API parity harness bootstrap | 0/TBD | Not started | - | +| 3. First vertical slice — genres end to end | 0/TBD | Not started | - | +| 4. CLI scaffolding, i18n and mail | 0/TBD | Not started | - | +| 5. Data layer full fidelity | 0/TBD | Not started | - | +| 6. HTTP routing, auth groups and rate limiting | 0/TBD | Not started | - | +| 7. User plugin and authentication | 0/TBD | Not started | - | +| 8. OAuth2.1 authorization server | 0/TBD | Not started | - | +| 9. Backend admin authentication and schema pipeline | 0/TBD | Not started | - | +| 10. Admin Vue SPA | 0/TBD | Not started | - | +| 11. Jobs, realtime and search infrastructure | 0/TBD | Not started | - | +| 12. Płytarium API — Collections and Albums | 0/TBD | Not started | - | +| 13. Płytarium API — wishlist, notifications, CSV, credentials, public routes | 0/TBD | Not started | - | +| 14. Domain jobs and external integrations | 0/TBD | Not started | - | +| 15. Cutover | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md new file mode 100644 index 0000000..bccc2d8 --- /dev/null +++ b/.planning/STATE.md @@ -0,0 +1,71 @@ +# Project State + +## Project Reference + +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 1 — Framework kernel foundation (parallel: Phase 2 — API parity harness bootstrap) + +## Current Position + +Phase: 1 of 15 (Framework kernel foundation) +Plan: TBD (roadmap just created, plans not yet written) +Status: Ready to plan +Last activity: 2026-09-16 — ROADMAP.md and STATE.md created from PROJECT.md, REQUIREMENTS.md and research/{SUMMARY,ARCHITECTURE,PITFALLS}.md + +Progress: [░░░░░░░░░░] 0% + +## Performance Metrics + +**Velocity:** +- Total plans completed: 0 +- Average duration: - +- Total execution time: 0 hours + +**By Phase:** + +| Phase | Plans | Total | Avg/Plan | +|-------|-------|-------|----------| +| - | - | - | - | + +**Recent Trend:** +- Last 5 plans: - +- Trend: - + +*Updated after each plan completion* + +## Accumulated Context + +### Decisions + +Decisions are logged in PROJECT.md Key Decisions table. +Recent decisions affecting current work: + +- Roadmap: gormigrate (not goose) is the migration tool, per STACK.md's more recent reasoning — ARCHITECTURE.md/PITFALLS.md text still says goose in places; treat gormigrate as authoritative when phases 3 and 5 are planned. +- Roadmap: two repos from day one — `summercms.go` (framework, no app knowledge) and `fonoteka.go` (sibling app repo, go.work workspace of plugins). Every phase states which repo(s) it writes to. +- Roadmap: the parity harness (Phase 2) and the first vertical slice (Phase 3) are sequenced immediately after kernel foundation, ahead of any further kernel broadening, to avoid the documented Scala-era bottom-up-kernel failure mode. + +### Pending Todos + +None yet. + +### Blockers/Concerns + +- Phase 8 (OAuth2.1) needs a pre-planning check of `wavepath.org/plugins/golem15/oauthserver` to resolve whether `ClientCredentialsStorage`/`TokenExchangeStorage` are needed — flagged in research/SUMMARY.md Gaps, unresolved. +- Phase 9 (admin schema pipeline / relation manager) is the least-precedented design surface in the research — plan with `--research-phase`. +- Phase 11 (River dual-driver split) is documented but unverified against a real build — plan with `--research-phase` and budget a timed-latency test. + +## Deferred Items + +Items acknowledged and carried forward from previous milestone close: + +| Category | Item | Status | Deferred At | +|----------|------|--------|-------------| +| *(none — first milestone)* | | | | + +## Session Continuity + +Last session: 2026-09-16 +Stopped at: Roadmap and state files written; awaiting user approval of ROADMAP.md before proceeding to `/gsd:plan-phase 1`. +Resume file: None diff --git a/CLAUDE.md b/CLAUDE.md index da5a904..5a5a253 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,3 +23,198 @@ These rules apply to every GSD phase in this project and override defaults: - Never add co-author tags to commit messages. - One logical change per commit. Planning docs and code in separate commits. + + +## Project + +**SummerCMS (Go)** + +SummerCMS is a Go rewrite of the WinterCMS/OctoberCMS content management framework for the Golem15 stack. It keeps what makes WinterCMS productive for us (plugins that extend each other, YAML-driven admin forms and lists, models/controllers/components, scaffolding commands, a headless API layer) and drops the parts that do not survive a compiled language (runtime plugin autoloading, PHP-style mutable magic). It is built by Golem15 developers and AI agents, for Golem15 projects that today run on the WinterCMS starter. + +v1 is a headless backend that runs one real project, Płytarium (`fonoteka`), with its existing Nuxt 4 app and MCP server unchanged. + +**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. + +### Constraints + +- **Tech stack**: Go 1.27, standard library first (net/http ServeMux, html/template, encoding/json); a dependency is added only when the research doc or a phase decision names it — keeps the binary boring and the dependency tree auditable +- **Data**: GORM on Postgres only — chosen for Eloquent-like DX and a line-by-line model port; River needs Postgres +- **Compatibility**: `vue-fonoteka-app` and `fonoteka-mcp` must run unchanged; the Nuxt app's requests define the contract and response shapes are not improved during the port +- **Realtime**: keep the Centrifugo server and port only the publisher and token issuing — the Nuxt client connects to Centrifugo directly +- **Plugins**: compiled at build time; no runtime plugin loading without a decision note +- **Two repositories**: `summercms.go` is the framework only (the `summer` packages, the CLI, the admin SPA shell, the parity harness tooling) and knows nothing about Płytarium; `fonoteka.go`, a sibling directory in the meta repo, is the application: a go.work workspace holding the ported plugins (user, websockets, translate, feedback, sitemap, fonoteka) and the app binary, requiring the framework by module path with a local replace during development. Roadmap phases name which repo each plan writes to; planning docs stay in `summercms.go/.planning` +- **Workflow**: lean planning (few, large plans per phase), a plan-count checkpoint before PLAN.md files are written, unit tests as the last plan of every phase, `go vet` and `go test ./...` green at every commit +- **Commits**: no co-author tags; one logical change per commit; planning docs and code in separate commits +- **Core plugin contracts**: the PHP user, blog, pages and payment plugins are shared across many projects; the Go ports must preserve their contracts and the PHP originals are not changed as part of this project + + + +## Technology Stack + +## Recommended Stack +### Core Technologies +| Technology | Version | Purpose | Why Recommended | +|------------|---------|---------|-----------------| +| Go | 1.27 (Aug 2026) | Language/runtime | Already decided. Generic methods clean up repository/query-builder APIs; `encoding/json` is now json/v2-backed (stricter: rejects duplicate keys, invalid UTF-8) — matters for hand-authored `fields.yaml`/`columns.yaml` round-tripped through JSON for the admin SPA. | +| GORM | v1.31.2 (Jun 22, 2026) | ORM, Postgres only | Already decided. Current stable line; v1.31.x added generics-based `Count` etc. Confirmed via pkg.go.dev version list. HIGH confidence. | +| gorm.io/driver/postgres | v1.6.3 (Sep 14, 2026) | Postgres driver for GORM | Latest patch release, two days before this research. Its `go.mod` pins `github.com/jackc/pgx/v5 v5.10.0` — GORM's "Postgres driver" is pgx under the hood, not lib/pq. This is the crux of the River pool-sharing question below. HIGH confidence (read go.mod directly). | +| River | v0.47.0 (confirmed via multiple independent 2026 dependabot PRs bumping to this version; GitHub releases page fetch returned stale cached 2024 dates — do not trust that page directly, cross-checked against `riverdriver`/`riverpgxv5`/`riverdatabasesql` sub-package publish dates of Apr–Jul 2026) | Postgres-backed job queue | Already decided. MEDIUM-HIGH confidence on the exact patch version; HIGH confidence it's the current v0.4x line and actively maintained. | +| zitadel/oidc | v3.51.0 (Sep 14, 2026) | OAuth2/OIDC provider | Already decided, and confirmed correct: **v4 exists only as `v4.0.0-next.4` (pre-release, Jul 30, 2026)**. v3 is the maintained stable line (v3.51.0 shipped two days before this research, newer than the v4 pre-release). Stay on v3; do not chase v4 until it ships stable. Flagged explicitly per the quality gate — this does not contradict the existing decision, it confirms it. | +| golang-jwt/jwt | v5.3.1 (Jan 28, 2026), import path `github.com/golang-jwt/jwt/v5` | JWT for the SPA and for signing Centrifugo connection/subscription tokens | Already decided. One library covers both jobs — see Centrifugo section. | +| go-playground/validator | v10.30.4 (Sep 3, 2026), import path `.../validator/v10` | Struct-tag validation from YAML `rules:` | Already decided. Current. | +| cobra | v1.10.2 (Dec 3, 2025) | CLI framework | Already decided. Current stable. | +| gocloud.dev | v0.46.0 (Jun 2, 2026) | Blob storage abstraction | Already decided. Current. | +| Vue 3 + TypeScript | (frontend, out of Go versioning scope) | Minimal admin SPA | Already decided. Types generated from OpenAPI — see OpenAPI section. | +### Supporting Libraries +| Library | Version | Purpose | When to Use | +|---------|---------|---------|-------------| +| go-gormigrate/gormigrate/v2 | latest tag as of May 26, 2026 (1.2k stars, actively maintained, PostgreSQL 18 in its own CI matrix) | Migrations, up/down, per-plugin | **Recommended primary migration tool.** See "Migration tooling" below for why this beats goose and atlas for this project's shape. | +| pressly/goose | v3.27.3 (Jul 27, 2026, 11.5k stars) | Alternative migration tool | Use instead of gormigrate only if a future plugin needs pure-SQL migrations independent of `*gorm.DB`, or a non-GORM connection. Not recommended as primary — see rationale below. | +| jackc/pgx/v5 | v5.10.0 (pinned by gorm.io/driver/postgres's own go.mod) | Postgres driver, shared by GORM and River | Not a separate app-level choice — it's already there transitively through GORM's driver. River's own driver (`riverpgxv5`) also needs pgx v5. Pin the same `pgx/v5` version across `go.sum` (Go's module resolution does this automatically via MVS; just don't force a divergent replace directive). | +| koanf/v2 (`github.com/knadh/koanf/v2`) | v2.3.4 (Mar 21, 2026) | Layered config with plugin namespaces | Already decided per go-ecosystem.md. See "Config format" below for the HOCON question. | +| koanf providers: `providers/file`, `providers/env`, `providers/confmap` | ships alongside koanf/v2, versioned independently as sub-modules | Base file + env-var overlay + programmatic plugin defaults | `file` provider loads `base.yaml`/`.yaml`; `env` provider overlays `SUMMER_`-prefixed env vars with a transform func mapping `SUMMER_DB__HOST` → `db.host`; `confmap` lets each plugin register its own namespaced defaults (`plugins..*`) before the file/env layers are merged on top. | +| koanf/parsers/yaml | tracks koanf/v2 | YAML parsing for koanf | Wraps `goccy/go-yaml` as of koanf's current release — **not** `gopkg.in/yaml.v3`, which is why the direct fields/columns parsing question below matters independently. | +| goccy/go-yaml | v1.19.2 (Jan 8, 2026) | Parse `fields.yaml` / `columns.yaml` | **Use this, not `gopkg.in/yaml.v3`.** See "What NOT to Use" — yaml.v3's upstream repo (`go-yaml/yaml`) was archived by its maintainer on Apr 1, 2025 and is explicitly marked unmaintained. goccy/go-yaml passes 355/402 cases of the YAML test suite vs 295/402 for yaml.v3, has an AST/tokenizer API useful for round-tripping comments if `summer make:*` scaffolds YAML files, and is what koanf itself has moved to. HIGH confidence — this is a load-bearing finding, not a style preference. | +| swaggo/swag | v1.16.6 (Jul 29, 2026, stable; v2.0.0-rc6 exists but is not production-ready) | Generate OpenAPI from annotated net/http handlers | **Recommended primary.** See "OpenAPI generation" below. | +| openapi-typescript | current npm release (JS ecosystem, not Go-versioned) | Generate TS types for the Vue admin SPA from the OpenAPI doc swag produces | Already decided (types generated from OpenAPI). Feeds directly off swag's output JSON/YAML. | +| typesense-go | v3.2.0 (Mar 27, 2025), confirms Typesense server API v28 support | Typesense client | Already decided. This is the most recent tagged release found; no newer tag surfaced in this research pass — treat the exact patch as MEDIUM confidence (worth a re-check at implementation time since it is over a year old relative to today) but the library itself is the only real Go client and is the correct pick. | +| centrifugal/gocent/v3 | current release, import path `github.com/centrifugal/gocent/v3` | Centrifugo HTTP API client (publish/broadcast/presence) | Official client from the Centrifugo org. Small (87 stars is normal for a niche official SDK, not a red flag — same maintainers as Centrifugo itself). Given it wraps ~4 REST calls, hand-rolling a thin `net/http` client is a legitimate stdlib-first alternative if the team wants zero non-decided dependencies; gocent is the pragmatic default. | +| — (no separate library for Centrifugo tokens) | — | Sign Centrifugo connection/subscription JWTs | Centrifugo does not need its own token library — it verifies plain JWTs signed with an HMAC secret (or RSA/ECDSA). **Use the already-decided `golang-jwt/jwt/v5` to sign these tokens too**, with claims shaped per Centrifugo's connection/subscription token spec. One JWT library, two token types (SPA auth, Centrifugo realtime auth). | +| go-i18n/v2 (`github.com/nicksnyder/go-i18n/v2`) | v2.6.1 (Jan 1, 2026) | CLDR-plural i18n | Already decided. CLDR v48 as of this release (up from CLDR 44 in older v2.3.0 — make sure any tutorial/blog post referencing go-i18n is checked against the current release, plural-rule edge cases have shifted). Also replaced an unmaintained YAML dependency internally in a recent release — another confirmation that the Go YAML-library churn described above is a live, current issue, not stale training-data noise. | +| testcontainers-go + testcontainers-go/modules/postgres | v0.44.0 (Aug 7, 2026) | Spin up real Postgres in tests | For GORM model tests, migration up/down tests, and River job tests that need real Postgres behavior (JSON columns, `LISTEN`/`NOTIFY`, constraint behavior) rather than SQLite-in-CI approximations. | +| stretchr/testify | v1.12.1 (Aug 17, 2026) | Assertions in Go tests | Standard choice; use `assert`/`require` for readability in the API-parity diff tests, not as a BDD framework — keep tests as plain `func Test...(*testing.T)`. | +| air-verse/air | v1.67.4 (Aug 1, 2026), actively maintained | Dev watch-rebuild loop | This is the "watch loop that rebuilds the compiled-plugin binary on change" the plugin architecture already calls for. Config via `.air.toml`; point `cmd`/`bin` at `go build -o ./tmp/summer ./cmd/summer && ./tmp/summer serve`. | +| golangci-lint | v2.13.2 (Aug 27, 2026) | Linting | v2's config schema (`version: "2"` in `.golangci.yml`) is a breaking change from v1 — do not copy a v1 config from an older Go project without migrating it. | +### Development Tools +| Tool | Purpose | Notes | +|------|---------|-------| +| air | live rebuild in dev | Config as above; combine with the plugin system's own `plugins.go` regeneration step so editing a plugin's route/model file triggers both codegen and rebuild. | +| golangci-lint v2 | static analysis in CI and pre-commit | Enable `govet`, `staticcheck`, `errcheck`, `revive` at minimum; the stdlib-first constraint makes `depguard` worth configuring to fail CI if an undecided dependency is imported. | +| testcontainers-go | integration tests against real Postgres | Gate these behind a build tag or `-short` skip so unit tests stay fast; the phase-ending "unit tests" plan (per this repo's lean-mode rule) should still run everywhere, with testcontainers tests as a separate, slower suite. | +| golden-file API parity tests | diff Go backend responses against recorded PHP responses | Not a library — a pattern: record Płytarium's actual JSON responses per route as fixtures (`testdata/parity/.golden.json`), replay the same requests against the Go backend in `httptest.Server`, diff with `testify/assert.JSONEq` or a small custom normalizer for non-deterministic fields (timestamps, IDs). This is the literal implementation of the PROJECT.md requirement "an API parity test suite replays the Nuxt app's and MCP server's requests against both backends and diffs responses." | +## Installation +# Core (already decided, versions confirmed this session) +# Migrations +# Config +# YAML for fields.yaml / columns.yaml (NOT gopkg.in/yaml.v3) +# i18n +# OpenAPI +# Search / realtime clients +# Dev / test dependencies +# Dev tools (not go.mod dependencies) +# golangci-lint installed via its install script, pinned to v2.13.2, not go install +## Deep Dives on the Milestone's Specific Questions +### GORM: plain structs, not gorm gen or the new GORM CLI +### Migration tooling: gormigrate, not goose or atlas +- **gormigrate** (`go-gorm/gormigrate/v2`) defines migrations as `{ID, Migrate(tx *gorm.DB) error, Rollback(tx *gorm.DB) error}` structs, operates directly on the shared `*gorm.DB`, and — confirmed by reading the source (`gormigrate.go`) directly rather than trusting a README excerpt — exposes `RollbackLast()` and `RollbackTo(id)` as first-class methods, plus `MigrateTo(id)`. This is a literal implementation of "drop the last migration and fix it": run `RollbackLast()`, edit the migration's `Migrate`/`Rollback` funcs, run `Migrate()` again. No separate migration-file format, no separate driver, no SQL string embedding required (though `tx.Exec(...)` is available inside a migration when raw SQL is easier than `Migrator()` calls). +- **goose** supports Go-code migrations (not just `.sql` files) via `goose.NewGoMigration`, which is the reason it was flagged as a candidate. But it registers migrations into its own provider backed by an `embed.FS` per source tree; making per-plugin migration sets compose cleanly (each plugin contributing its own ordered slice, aggregated by the kernel at boot from the generated plugin import list) is more natural with gormigrate's plain `[]*gormigrate.Migration` slices than with goose's filesystem-and-provider model. goose is still the right tool if a future plugin needs raw-SQL migrations decoupled from `*gorm.DB` entirely (e.g. a plugin that talks to Postgres via bare `pgx` for performance reasons) — keep it as the named fallback, not the default. +- **atlas** is a declarative schema-diff tool (desired-state HCL/SQL compared against actual state, migration generated automatically). It is the better fit when GORM's own struct tags are treated as the single source of truth and hand-written data-backfill migrations are rare. This project's port needs custom, hand-authored up/down logic per PHP migration (27 of them, with real data semantics, not just DDL) — atlas's diffing model fights that instead of helping it, and it adds a second DSL and a separate binary. Not recommended for v1. +### River + GORM: share one `*sql.DB`, not one `pgxpool.Pool` +### zitadel/oidc v3 storage interface: what to implement +- **`AuthStorage`** — the core of the authorization-code + PKCE flow: `CreateAuthRequest`, `AuthRequestByID`, `AuthRequestByCode`, `SaveAuthCode`, `DeleteAuthRequest`, `CreateAccessToken`, `CreateAccessAndRefreshTokens`, `TokenRequestByRefreshToken`, `TerminateSession`, `RevokeToken`, `GetRefreshTokenInfo`, `SigningKey`, `SignatureAlgorithms`, `KeySet`. This is the interface Płytarium's `OAuthAuthCode`, `OAuthClient`, `OAuthRefreshToken` models map onto directly. +- **`OPStorage`** — `GetClientByClientID`, `AuthorizeClientIDSecret`, `SetUserinfoFromToken`, `SetIntrospectionFromToken`, `GetPrivateClaimsFromScopes`, `GetKeyByIDAndClientID`, `ValidateJWTProfileScopes`. `SetUserinfoFromScopes` exists but is explicitly documented as deprecated in favor of the optional `CanSetUserinfoFromRequest` interface — implement the newer one, leave the old one empty. +- **`ClientCredentialsStorage`** (optional) — needed only if the client-credentials grant is used (worth checking whether the MCP server or ChatGPT connector needs it; Płytarium's OAuth models list doesn't obviously call for it, flag as an open question for the phase that implements this). +- **`TokenExchangeStorage`** / **`TokenExchangeTokensVerifierStorage`** (optional) — RFC 8693 token exchange; almost certainly out of scope for a straight port unless the existing PHP OAuth server implements it (check `wavepath.org/plugins/golem15/oauthserver` for this before assuming it's unneeded). +- **Optional `Can*` interfaces** (`CanTerminateSessionFromRequest`, `CanSetUserinfoFromRequest`, and others in the same file) let the request object itself (not just IDs) reach the storage implementation — worth implementing from the start rather than the older, more limited required methods, since they carry richer context. +### Config: koanf + YAML, not a literal HOCON parser +### go-i18n + CLDR plurals +### YAML parsing: goccy/go-yaml, confirmed necessary not just preferred +### OpenAPI generation: swaggo/swag (code-first, comment-driven), not Huma, not hand-written +### Testing stack +- **testcontainers-go** (v0.44.0, Aug 7, 2026) + its `modules/postgres` for real-Postgres integration tests: GORM migrations up/down, River job processing (needs real `LISTEN`/`NOTIFY`), JSON column round-tripping. Gate behind a build tag/`-short` so the fast unit-test loop stays fast. +- **testify** (v1.12.1, Aug 17, 2026) for assertions only (`assert`/`require`) — keep tests as plain stdlib `func TestX(t *testing.T)`, no BDD DSL, consistent with stdlib-first. +- **Golden-file API parity tests**: no library needed. Record real Płytarium PHP responses as JSON fixtures per route, replay identical requests against the Go backend via `httptest.NewServer`, diff with a normalizer that ignores non-deterministic fields (timestamps, generated IDs) before comparing. This directly implements the PROJECT.md acceptance test. +### Dev loop and linting +- **air** (v1.67.4, Aug 1, 2026) for the watch-rebuild loop the compiled-plugin architecture needs to "feel like WinterCMS's drop-in loop" (go-ecosystem.md's own phrase). Point its build command at the full `go build` of the `summer` binary plus the plugin-import-list regeneration step, not just a bare `go build ./...`. +- **golangci-lint v2.13.2** (Aug 27, 2026). v2's `.golangci.yml` schema (`version: "2"`) is a breaking change from v1 configs — do not reuse a v1 config verbatim. Configure `depguard` to enforce the stdlib-first / decided-dependency-only constraint at CI time, not just by convention. +## Alternatives Considered +| Recommended | Alternative | When to Use Alternative | +|--------------|-------------|--------------------------| +| gormigrate | goose | A future plugin needs raw-SQL migrations independent of `*gorm.DB`/GORM entirely. | +| gormigrate | atlas | Schema is treated as pure declarative state (GORM struct tags as source of truth) with rare hand-authored data migrations — not this project's shape for v1. | +| plain GORM structs | go-gorm/cli or gorm.io/gen | A later, non-ported plugin wants compile-time-checked queries and is willing to add a codegen step. | +| swaggo/swag | huma + humago | A later, non-parity API surface where request/response shape isn't constrained by an existing contract, and the team accepts Huma's handler-shape convention project-wide. | +| swaggo/swag | hand-written OpenAPI + oapi-codegen | A small, from-scratch API (not a 154-route port) where authoring the spec first is cheaper than annotating existing handlers. | +| YAML (via koanf) | literal HOCON via a Go parser | Never, for this project — no maintained Go HOCON library exists at the bar this project applies elsewhere. | +| gocent/v3 | hand-rolled Centrifugo HTTP client | Team wants zero additional non-decided dependencies; Centrifugo's HTTP API is small enough (~4 calls) that this is a legitimate, if slightly more work, stdlib-first option. | +## What NOT to Use +| Avoid | Why | Use Instead | +|-------|-----|--------------| +| `gopkg.in/yaml.v3` (`go-yaml/yaml`) | Archived by its maintainer Apr 1, 2025; README explicitly says "THIS PROJECT IS UNMAINTAINED." No further fixes, security patches, or Go-compatibility work will land. | `goccy/go-yaml` v1.19.2 | +| `gorm.io/gen` or `go-gorm/cli` for v1 model code | Adds a codegen layer that works against the explicit goal of a simple, reviewable, line-by-line Eloquent-to-GORM port of 25 models. | Plain GORM structs and methods | +| `atlas` for v1 migrations | Declarative schema-diff model fights hand-authored, data-aware up/down migrations for 27 real PHP migrations with actual data semantics, and adds a second DSL/binary. | gormigrate | +| Sharing a raw `pgxpool.Pool` directly between GORM's postgres driver and River's `riverpgxv5` driver as two independent pools | Two separate pools against the same database is wasteful and defeats the point of "share one pool"; naively wiring them both to the *same* pool object isn't how either library's constructor is designed to be used. | One shared `*sql.DB` (via `pgx/v5/stdlib`) for both GORM and River's queries, plus one small separate `pgxpool.Pool` used only for River's `LISTEN`/`NOTIFY` wake-ups | +| zitadel/oidc v4 | Only exists as `v4.0.0-next.4`, a pre-release from Jul 30, 2026 — not production-ready, and older than the current v3.51.0 stable release. | zitadel/oidc v3 (`/v3`), already decided | +| Huma for the v1 parity port specifically | Requires restructuring every ported handler into Huma's typed Input/Output convention rather than ordinary `net/http` handlers, right when byte-compatible parity with an existing contract is the entire point. | swaggo/swag, annotating existing handlers | +| swaggo/swag v2 (`v2.0.0-rc6`) | Still a release candidate as of Sep 13, 2026; OpenAPI 3.1 support and dependency cleanup are still in flux. | swaggo/swag v1.16.6 | +| A literal HOCON parser in Go (`gurkankaymak/hocon`, `go-hocon/hocon`) | Both are tiny, single-or-zero-star projects with unclear or no active maintenance — far below this project's ecosystem-depth bar for every other pick. | koanf + YAML, achieving the same layering design without literal HOCON syntax | +| golangci-lint v1-style config on a v2 install | v2 changed the `.golangci.yml` schema; a copied v1 config will not behave as expected. | A `version: "2"` config written against v2's current schema | +## Stack Patterns by Variant +- Use goose's Go-code migrations for that plugin specifically +- Because gormigrate assumes `*gorm.DB`-shaped `Migrate`/`Rollback` funcs; a plugin that deliberately bypasses GORM for a hot path has no natural `*gorm.DB` to hand it +- Consider Huma instead of hand-annotated swag comments, and `go-gorm/cli` instead of plain structs +- Because the byte-compatible-parity constraint that rules both out for v1 doesn't apply to code with no existing contract to match +## Version Compatibility +| Package A | Compatible With | Notes | +|-----------|------------------|-------| +| gorm.io/gorm v1.31.2 | gorm.io/driver/postgres v1.6.3 | Driver's own go.mod targets this gorm version; keep them bumped together. | +| gorm.io/driver/postgres v1.6.3 | jackc/pgx/v5 v5.10.0 | Pinned directly in the driver's go.mod — this is the pgx version that ends up in `go.sum` for the whole binary via GORM. | +| River v0.47.0 | jackc/pgx/v5 (River's own `riverpgxv5` driver requires v5; `riverdatabasesql` driver works with any `database/sql`-registered driver, including pgx's `stdlib` registration) | Go's module resolution (MVS) will pick one pgx/v5 version for the whole build; do not force a divergent `replace` for either GORM's or River's sake — let them converge. | +| zitadel/oidc v3.51.0 | go-jose/go-jose/v4 (imported directly in `pkg/op/storage.go`) | v3's storage interfaces use `jose.SignatureAlgorithm` and `*jose.JSONWebKey` types from go-jose v4 in method signatures — a storage implementation's key-handling code will import this directly. | +| koanf/v2 v2.3.4 | koanf/parsers/yaml (wraps goccy/go-yaml) | Confirms goccy/go-yaml is already in the dependency tree via koanf; using it directly for `fields.yaml`/`columns.yaml` doesn't add a new library, just a direct dependency on one already present transitively. | +| go-i18n/v2 v2.6.1 | golang.org/x/text v0.32.0+ | go-i18n's own recent release notes cite bumping to this x/text version alongside the CLDR v48 update. | +| golangci-lint v2.13.2 | `.golangci.yml` schema `version: "2"` | Not backward compatible with v1-style config files without migration. | +## Sources +- pkg.go.dev version pages for `gorm.io/gorm`, `gorm.io/driver/postgres`, `github.com/golang-jwt/jwt/v5`, `github.com/go-playground/validator/v10`, `github.com/spf13/cobra`, `gocloud.dev`, `github.com/goccy/go-yaml`, `github.com/stretchr/testify`, `github.com/testcontainers/testcontainers-go` — versions and dates, fetched 2026-09-16. HIGH confidence. +- `raw.githubusercontent.com/go-gorm/postgres/master/go.mod` — read directly for the pgx/v5 pin. HIGH confidence. +- `riverqueue.com/docs/gorm` — official River+GORM integration guide, the source for the shared-`*sql.DB` pattern. HIGH confidence. +- `raw.githubusercontent.com/zitadel/oidc/main/pkg/op/storage.go` — read directly for interface definitions. HIGH confidence. +- github.com/zitadel/oidc releases page — v3.51.0 vs v4.0.0-next.4 dates. HIGH confidence. +- `raw.githubusercontent.com/go-gormigrate/gormigrate/master/gormigrate.go` and `README.md` — read directly for `RollbackLast`/`RollbackTo`/`MigrateTo` API and usage pattern. HIGH confidence. +- github.com/go-yaml/yaml repo page — archived status and "UNMAINTAINED" README notice, read directly. HIGH confidence. +- WebSearch results on koanf's yaml parser migrating to goccy/go-yaml — MEDIUM confidence (WebSearch-sourced, not independently re-verified against koanf's own go.mod, but corroborated by two independent search results and by go-i18n's own release notes mentioning a similar unmaintained-YAML-dependency swap). +- github.com/swaggo/swag releases page, huma.rocks docs (`bring-your-own-router`, `humago` adapter), oapi-codegen GitHub — OpenAPI approach comparison. HIGH confidence on version/maintenance facts, MEDIUM-HIGH (judgment call) on the recommendation itself. +- github.com/gurkankaymak/hocon, github.com/go-hocon/hocon — star counts and activity, read directly. HIGH confidence these are not viable options. +- github.com/typesense/typesense-go releases — v3.2.0, Mar 27 2025. MEDIUM confidence this is still the latest tag; worth a fresh check at implementation time given the gap since this research date. +- github.com/centrifugal/gocent repo page — v3 module path, star count. MEDIUM confidence (couldn't confirm an exact latest tag/date from the page content retrieved). +- github.com/air-verse/air releases — v1.67.4, Aug 1 2026. HIGH confidence. +- golangci-lint changelog/release references — v2.13.2, Aug 27 2026, v2 config schema change. HIGH confidence. + + + +## Conventions + +Conventions not yet established. Will populate as patterns emerge during development. + + + +## Architecture + +Architecture not yet mapped. Follow existing patterns found in the codebase. + + + +## Project Skills + +No project skills found. Add skills to any of: `.claude/skills/`, `.agents/skills/`, `.cursor/skills/`, `.github/skills/`, or `.codex/skills/` with a `SKILL.md` index file. + + + +## GSD Workflow Enforcement + +Before using Edit, Write, or other file-changing tools, start work through a GSD command so planning artifacts and execution context stay in sync. + +Use these entry points: +- `/gsd-quick` for small fixes, doc updates, and ad-hoc tasks +- `/gsd-debug` for investigation and bug fixing +- `/gsd-execute-phase` for planned phase work + +Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it. + + + +## Developer Profile + +> Profile not yet configured. Run `/gsd-profile-user` to generate your developer profile. +> This section is managed by `generate-claude-profile` -- do not edit manually. +