From 3a3e044a70a9943e93a36d2363753ab925ab0a95 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Wed, 16 Sep 2026 03:35:10 +0200 Subject: [PATCH] docs: define v1 requirements --- .planning/REQUIREMENTS.md | 166 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 .planning/REQUIREMENTS.md diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md new file mode 100644 index 0000000..1e53dfa --- /dev/null +++ b/.planning/REQUIREMENTS.md @@ -0,0 +1,166 @@ +# 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) + +- [ ] **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 +- [ ] **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 +- [ ] **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 +- [ ] **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 +- [ ] **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) +- [ ] **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 +- [ ] **KERN-07**: Per-request state (user, organization, active collection, locale) travels in context.Context; no package-level globals hold request state +- [ ] **KERN-08**: A service registry (backpack) lets a plugin publish a service (job manager, authorizer registry, credential resolver) that other plugins resolve by interface +- [ ] **KERN-09**: A dev watch loop rebuilds and restarts the binary on source change + +### Console and scaffolding (CLI) + +- [ ] **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 +- [ ] **CLI-02**: Scaffolding commands generate a plugin, model, migration, command, job and admin controller with stubs that compile +- [ ] **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) + +- [ ] **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 +- [ ] **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) + +- [ ] **DATA-01**: GORM on Postgres through one shared *sql.DB (pgx stdlib) with a separate small pgx pool reserved for River's listener +- [ ] **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 +- [ ] **DATA-03**: Models get timestamps, soft delete, and lifecycle hooks (beforeValidate, beforeCreate, beforeSave, beforeDelete, afterDelete) that can cascade soft deletes inside a transaction +- [ ] **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) +- [ ] **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 +- [ ] **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 +- [ ] **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 +- [ ] **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 +- [ ] **DATA-09**: All 25 Płytarium models and 27 migrations are ported with matching table names, columns, indexes and defaults +- [ ] **DATA-10**: Paginated responses use the exact `{data, meta{current_page, last_page, per_page, total}}` envelope without a links key +- [ ] **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) + +- [ ] **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 +- [ ] **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 +- [ ] **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) +- [ ] **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 seven named buckets and inline throttles 1:1 +- [ ] **HTTP-05**: An auth guard registry lets plugins add guards (JWT, personal token, OAuth bearer) that all resolve to the same current-user accessor +- [ ] **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 +- [ ] **HTTP-07**: A guarded outbound fetch helper enforces host allow-lists, byte caps and timeouts for user-supplied URLs (manual cover URL, Discogs cover) +- [ ] **HTTP-08**: OpenAPI is generated from swaggo/swag annotations on handlers and openapi-typescript produces the admin SPA's types +- [ ] **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) + +- [ ] **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 +- [ ] **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) +- [ ] **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 +- [ ] **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 | +|-------------|-------|--------| +| (filled by roadmap) | | | + +**Coverage:** +- v1 requirements: 76 total +- Mapped to phases: 0 +- Unmapped: 76 ⚠️ + +--- +*Requirements defined: 2026-09-16* +*Last updated: 2026-09-16 after initial definition*