Files
summercms/.planning/REQUIREMENTS.md

242 lines
18 KiB
Markdown

# Requirements: SummerCMS (Go)
**Defined:** 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.
## v1 Requirements
Requirements for v1 (the Płytarium port). Each maps to roadmap phases. "User" below is the plugin author or operator unless a requirement names the Nuxt app, the MCP server or an admin.
### Kernel and plugins (KERN)
- [x] **KERN-01**: Application boots from layered YAML config via koanf (base files, env overlay directory, per-plugin namespace, environment variables) with dot-path access and typed section loading
- [x] **KERN-02**: A plugin implements one required interface (ID, Requires, Register, Boot); Register runs for all plugins before any Boot, and order follows a topological sort of Requires
- [x] **KERN-03**: Plugins opt into capabilities through small optional interfaces (models, migrations, routes, middleware, commands, jobs, listeners, admin controllers, navigation, permissions, schedule, mail templates, config, lang) discovered by type assertion
- [x] **KERN-04**: Plugins are Go modules in a go.work workspace that self-register in init(); `summer build` regenerates the blank-import list and rebuilds the binary; `summer plugin:add` wires a new module in
- [x] **KERN-05**: A plugin can check at boot whether an optional plugin is registered and skip its integration without a hard import (replaces PHP class_exists guards)
- [x] **KERN-06**: A typed event bus offers three primitives: fire-and-forget, fire-and-collect (listener return values merged into one payload), fire-until-handled; plugins listen on other plugins' events
- [x] **KERN-07**: Per-request state (user, organization, active collection, locale) travels in context.Context; no package-level globals hold request state
- [x] **KERN-08**: A service registry (backpack) lets a plugin publish a service (job manager, authorizer registry, credential resolver) that other plugins resolve by interface
- [x] **KERN-09**: A dev watch loop rebuilds and restarts the binary on source change
### Console and scaffolding (CLI)
- [x] **CLI-01**: The `summer` binary (cobra) discovers commands registered by plugins and offers rich output (spinner, progress bar, table, prompts) with non-TTY degradation, following the summer-bonfire design
- [x] **CLI-02**: Scaffolding commands generate a plugin, model, migration, command, job and admin controller with stubs that compile
- [x] **CLI-03**: Migration commands run up, status, and roll back the last migration of a named plugin
- [ ] **CLI-04**: Plugins register recurring commands (daily or interval) that a scheduler runs in-process or via `summer schedule:run`
- [ ] **CLI-05**: Płytarium commands are ported: oauth-client (create, list without reprinting secrets, add redirect URIs, set scope ceiling), prune-notifications, reindex with drop-old-index flag
- [ ] **CLI-06**: Queue worker runs in-process or via `summer queue:work`
### Internationalization and mail (I18N)
- [x] **I18N-01**: Translation keys use `vendor.plugin::group.key`, load from per-plugin per-locale YAML files, support parameters and CLDR plurals (go-i18n) for pl and en
- [ ] **I18N-02**: Locale is resolved per request from the user's persisted preferred_locale with header fallback, and the locale endpoints stay reachable while the must-change-password lock is active
- [x] **I18N-03**: Plugins register mail templates and layouts by dotted name with the per-locale suffix convention, rendered with html/template and sent through a driver interface (SMTP via go-mail)
### Data layer (DATA)
- [x] **DATA-01**: GORM on Postgres through one shared *sql.DB (pgx stdlib) with a separate small pgx pool reserved for River's listener
- [x] **DATA-02**: Each plugin ships a gormigrate migration set with up and down; sets run in plugin dependency order with per-plugin version tracking; AutoMigrate is never the schema source
- [x] **DATA-03**: Models get timestamps, soft delete, and lifecycle hooks (beforeValidate, beforeCreate, beforeSave, beforeDelete, afterDelete) that can cascade soft deletes inside a transaction
- [x] **DATA-04**: Relations cover belongsTo, hasOne, hasMany and belongsToMany with ordered results and dedicated pivot models carrying business columns (CollectionEditor role/granted_at/granted_by, album_artists sort_order)
- [x] **DATA-05**: Model rule strings (required, between, unique:table, nullable, integer, in) are validated on save via go-playground/validator with translated messages and a 422 error map shaped like Laravel's
- [x] **DATA-06**: Mass assignment goes through per-endpoint request DTOs honoring each model's fillable allow-list; serialization honors a hidden deny-list with an explicit per-call override
- [x] **DATA-07**: Custom casts exist for jsonable columns, money as a fixed four-decimal string, and encrypted-at-rest secrets (AES-GCM, app-key derived) that are also hidden from serialization
- [x] **DATA-08**: A polymorphic file attachment table (owner type, owner id, field, disk path, sort order, public/private) backs attachOne and attachMany, stored via gocloud.dev/blob with the same public URL shape
- [x] **DATA-09**: All 25 Płytarium models and their squashed migration set are ported with matching table names, columns, indexes and defaults (migration count is not itself an acceptance number — squashed per plan-time decision D-01 in 05-CONTEXT.md)
- [x] **DATA-10**: Paginated responses use the exact `{data, meta{current_page, last_page, per_page, total}}` envelope without a links key
- [x] **DATA-11**: Other plugins can hook a model's lifecycle through the GORM callback registry and extend its schema with a companion migration
### HTTP and routing (HTTP)
- [x] **HTTP-01**: Plugins register route groups on net/http ServeMux with typed params and regex constraints; unknown and malformed ids both return 404 on ownership-scoped resources
- [x] **HTTP-02**: Plugins register named middleware that other plugins reference by name; the pipeline order is recover, CORS, locale, auth group, must-change-password, org context, rate limit, handler
- [x] **HTTP-03**: Three mutually exclusive auth groups share the same handlers with different route subsets: JWT under /_fonoteka/api/v1, personal scoped token under /api/v1/fonoteka, and public groups (onboarding, public/{token}, public-wishlist/{token}, invitation inspection)
- [x] **HTTP-04**: A rate limiter supports named buckets keyed by a resolver (token id, IP, route param), stacking two limiters on one route, and ports Płytarium's five named buckets and inline throttles 1:1
- [x] **HTTP-05**: An auth guard registry lets plugins add guards (JWT, personal token, OAuth bearer) that all resolve to the same current-user accessor
- [x] **HTTP-06**: Response conventions are preserved: empty arrays serialize as [], timestamps as +00:00, tri-state booleans keep null, conditional keys are omitted not nulled, and no blanket envelope or error middleware wraps OAuth routes
- [x] **HTTP-07**: A guarded outbound fetch helper enforces host allow-lists, byte caps and timeouts for user-supplied URLs (manual cover URL, Discogs cover)
- [x] **HTTP-08**: OpenAPI is generated from swaggo/swag annotations on handlers and openapi-typescript produces the admin SPA's types
- [x] **HTTP-09**: CORS and JSON body size limits match the PHP deployment
### Authentication and users (AUTH)
- [ ] **AUTH-01**: User plugin port: registration, login, logout, password reset, email verification, and JWT issue/refresh (golang-jwt) with the same claims and cookie behavior the Nuxt app expects
- [ ] **AUTH-02**: Organizations with roles; organization fields appear on the user payload through a fire-and-collect event so the fonoteka plugin extends the user plugin without editing it
- [ ] **AUTH-03**: Personal API tokens with a read|write|ai scope ceiling, token CRUD endpoints, and a scope-checking middleware
- [ ] **AUTH-04**: The must-change-password flag locks the authenticated surface with 423 except the locale and password-change routes
- [ ] **AUTH-05**: OAuth2.1 authorization server on zitadel/oidc: RFC 8414 metadata, authorize with PKCE and consent screen, token endpoint for authorization_code and refresh_token, RFC 7591 dynamic client registration, RFC 8707 resource parameter tolerance, exact WWW-Authenticate and protected-resource-metadata headers
- [ ] **AUTH-06**: OAuth routes are form-urlencoded, CSRF-free, rate limited, and return unwrapped RFC 6749 bodies with the PHP cache headers
- [ ] **AUTH-07**: Connected apps can be listed and revoked; OAuthClient, OAuthAuthCode and OAuthRefreshToken models are ported; fonoteka-mcp completes its install and auth flow unchanged
- [ ] **AUTH-08**: Backend admin users with roles and a permissions registry are separate from frontend users, and gate both navigation and admin controller access
### Płytarium API (API)
- [ ] **API-01**: Collections: CRUD, active-context switch (me/context returns an opaque channel name), editor invitations and acceptance, share link regeneration, public token views
- [ ] **API-02**: Albums: CRUD, ratings, reservations, photo upload and manual cover URL, Discogs cover price, artists/genres/styles lookups, search that treats Typesense as a pre-filter re-gated in SQL
- [ ] **API-03**: Wishlist: items, subscriptions, public-wishlist/{token} views, purchase and digest triggers
- [ ] **API-04**: Notifications: list, mark read, prune; realtime token endpoint owned by the websockets plugin
- [ ] **API-05**: CSV import as a multi-step session (store, show/poll, mapping patch, per-row edit, commit, cancel) and CSV export on both authenticated groups
- [ ] **API-06**: Per-user and per-org Discogs and AI credentials CRUD with encrypted storage, org-lock flag, and env-to-org-to-user resolution
- [ ] **API-07**: Onboarding, public and invitation inspection routes with their public rate-limit buckets
- [ ] **API-08**: Feedback submissions and sitemap output from the stack plugins
- [ ] **API-09**: All 154 routes are registered on the correct groups with identical paths, methods, status codes and bodies
### Background jobs (JOBS)
- [ ] **JOBS-01**: River runs on the shared *sql.DB with riverdatabasesql for transactional enqueue and riverpgxv5 for LISTEN/NOTIFY; job outcomes (complete, fail, skip) are recorded and queryable through a job manager service
- [ ] **JOBS-02**: CSV import write job and Discogs match job (240 s timeout) are ported; the match job re-enqueues itself with a delay on a Discogs rate-limit error instead of failing the batch
- [ ] **JOBS-03**: Wishlist digest coalesces notifications in a 30-minute window and deletes its queue row on completion
### Realtime (RT)
- [ ] **RT-01**: The websockets plugin publishes to the existing Centrifugo server (gocent) and issues connection and subscription JWTs with the same secret and claims, served at GET /api/realtime/token
- [ ] **RT-02**: A channel-namespace authorizer registry lets plugins register authorizers (collection, wishlist); the server re-validates on every subscribe and channel names never expose raw ids
- [ ] **RT-03**: A broadcastable model interface emits live patches with bulk-write suppression, separate from durable notifications
### Search (SRCH)
- [ ] **SRCH-01**: Album documents sync to Typesense scoped by collection_id, behind a settings kill-switch, degrading gracefully when the DB or config is absent
- [ ] **SRCH-02**: The reindex command asserts zero documents with collection_id 0 before and after, and can drop the legacy index
### Integrations (INTG)
- [ ] **INTG-01**: Discogs client with proactive rate threshold, bounded in-request wait budget, retry-after fallback, and host-locked cover fetch
- [ ] **INTG-02**: AI cover recognition through provider adapters (Anthropic Go SDK, OpenAI-compatible) with per-credential model and base URL overrides
### Admin (ADMIN)
- [ ] **ADMIN-01**: fields.yaml is parsed (goccy/go-yaml) into a JSON form schema with text, textarea, checkbox, switch, dropdown (model-method options), relation (nameFrom, emptyOption), plus span, tabs, context and attributes
- [ ] **ADMIN-02**: columns.yaml is parsed into a JSON list schema with searchable, sortable, relation columns and datetime/switch renderers
- [ ] **ADMIN-03**: A relation-manager schema (search, link, unlink, manage/view lists) replaces the one `partial` field in Collections' editors tab
- [ ] **ADMIN-04**: Admin CRUD endpoints per controller expose extension hooks (listExtendQuery, formExtendQuery, formBeforeCreate, formBeforeUpdate, relationExtendManageQuery), and bulk delete runs each record's lifecycle hooks
- [ ] **ADMIN-05**: A settings model binds to a settings screen through the same schema pipeline (search_use_typesense)
- [ ] **ADMIN-06**: A minimal Vue 3 + TypeScript SPA renders login, permission-gated navigation, lists, forms and the relation manager for Albums, Artists, Collections, Genres and Styles using generated types
### Quality and cutover (QA)
- [x] **QA-01**: A fixture recorder captures requests and responses from the running PHP backend for every route plus the Nuxt app's and MCP server's real flows
- [x] **QA-02**: A replay-and-diff harness runs fixtures against the Go backend with a normalizer for nondeterministic fields and assertions for the parity classes (nil vs [], date formats, tri-state booleans, envelopes, conditional keys)
- [x] **QA-03**: go vet and go test ./... are green at every commit; each phase ends with a unit-test plan; integration tests use testcontainers Postgres
- [x] **QA-04**: The first vertical slice (GET /_fonoteka/api/v1/genres) passes the parity diff end to end before further kernel abstraction
- [ ] **QA-05**: Cutover: the parity harness is green on all 154 routes and vue-fonoteka-app and fonoteka-mcp run unchanged against the Go backend
## v2 Requirements
Deferred to a later milestone. Tracked but not in the current roadmap.
### Framework
- **FW-01**: Generalized three-tier credential resolver (env, org, user with org lock) as one framework piece instead of two copies
- **FW-02**: Policy-based row and field-level admin permissions
- **FW-03**: Server-rendered theme engine with components in templates (keios.eu port)
- **FW-04**: Payments plugins (keios.eu port)
- **FW-05**: Sortable, NestedTree, Revisionable, Sluggable behaviors, built on demand
- **FW-06**: WASM sandboxed extension API (see seed)
## Out of Scope
| Feature | Reason |
|---------|--------|
| Runtime plugin loading (stdlib plugin, yaegi, RPC) | Rejected in why-go-not-scala.md; compiled plugins only |
| MySQL or SQLite support | Postgres only; River and one migration target |
| Backend AJAX partial-refresh framework | Admin is a JSON-API SPA; bulk actions become REST endpoints |
| Generic response envelope or blanket error middleware | Breaks the three envelope families the PHP API has, including unwrapped OAuth responses |
| Improving API response shapes during the port | Parity is the acceptance test |
| Native websocket server replacing Centrifugo | Nuxt client connects to Centrifugo directly and must stay unchanged |
| Chat, forum, video | wavepath.org territory |
| Porting Scala module code | Designs carry over, code does not |
## Traceability
Which phases cover which requirements. Updated during roadmap creation.
| Requirement | Phase | Status |
|-------------|-------|--------|
| KERN-01 | Phase 1 | Complete |
| KERN-02 | Phase 1 | Complete |
| KERN-03 | Phase 1 | Complete |
| KERN-04 | Phase 1 | Complete |
| KERN-05 | Phase 1 | Complete |
| KERN-06 | Phase 1 | Complete |
| KERN-07 | Phase 1 | Complete |
| KERN-08 | Phase 1 | Complete |
| KERN-09 | Phase 1 | Complete |
| CLI-01 | Phase 1 | Complete |
| CLI-02 | Phase 4 | Complete |
| CLI-03 | Phase 5 | Complete |
| CLI-04 | Phase 11 | Pending |
| CLI-05 | Phase 14 | Pending |
| CLI-06 | Phase 11 | Pending |
| I18N-01 | Phase 4 | Complete |
| I18N-02 | Phase 7 | Pending |
| I18N-03 | Phase 4 | Complete |
| DATA-01 | Phase 3 | Complete |
| DATA-02 | Phase 3 | Complete |
| DATA-03 | Phase 5 | Complete |
| DATA-04 | Phase 5 | Complete |
| DATA-05 | Phase 5 | Complete |
| DATA-06 | Phase 5 | Complete |
| DATA-07 | Phase 5 | Complete |
| DATA-08 | Phase 5 | Complete |
| DATA-09 | Phase 5 | Complete |
| DATA-10 | Phase 5 | Complete |
| DATA-11 | Phase 5 | Complete |
| HTTP-01 | Phase 3 | Complete |
| HTTP-02 | Phase 3 | Complete |
| HTTP-03 | Phase 6 | Complete |
| HTTP-04 | Phase 6 | Complete |
| HTTP-05 | Phase 6 | Complete |
| HTTP-06 | Phase 6 | Complete |
| HTTP-07 | Phase 6 | Complete |
| HTTP-08 | Phase 6 | Complete |
| HTTP-09 | Phase 6 | Complete |
| 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 | Complete |
| QA-02 | Phase 2 | Complete |
| QA-03 | Phase 2 | Complete |
| QA-04 | Phase 3 | Complete |
| QA-05 | Phase 15 | Pending |
**Coverage:**
- v1 requirements: 76 total
- Mapped to phases: 76
- Unmapped: 0 ✓
---
*Requirements defined: 2026-09-16*
*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)*