186 lines
29 KiB
Markdown
186 lines
29 KiB
Markdown
# Project Research Summary
|
|
|
|
**Project:** SummerCMS (Go) — v1 = Płytarium (fonoteka) headless backend port
|
|
**Domain:** Go 1.27 WinterCMS/Laravel-shaped CMF, compiled-plugin model, porting a live PHP headless API to Go with byte-level response parity for an unchanged Nuxt 4 app and MCP/OAuth client
|
|
**Researched:** 2026-09-16
|
|
**Confidence:** HIGH on stack versions, plugin-architecture mechanics, and PHP-source-grounded pitfalls; MEDIUM on architectural judgment calls (migration tool, OpenAPI approach, config format) and on features/patterns inferred from ecosystem comparison rather than direct source reading
|
|
|
|
## Executive Summary
|
|
|
|
SummerCMS v1 is not a from-scratch CMS design problem — it is a constrained port of a fully-specified, already-running PHP application (Płytarium/fonoteka: 25 models, 154 routes, 27 migrations, 3 jobs, 3 console commands) onto a compiled-plugin Go framework whose shape (Caddy/xcaddy-style build-time registration) is already decided and well-precedented. All four research passes converge on the same operating principle: **the PHP source is the spec, and the parity harness is the compiler.** Every stack pick, feature, architectural pattern, and pitfall in this research is grounded in reading the actual `fonoteka` PHP source (models, controllers, routes.php, serializer traits) rather than a generic "how do headless CMSes work" survey — this is the single most load-bearing fact for how the roadmap should be sequenced.
|
|
|
|
The recommended approach: stand up a minimal kernel (config, plugin registry, event bus, container) only far enough to support one real vertical slice — `GET /_fonoteka/api/v1/genres` through every layer (config → DB → plugin registry → routing → JWT auth → GORM → JSON response), diffed byte-for-byte against a recorded PHP fixture — before any further kernel abstraction work. This directly avoids the one documented failure mode this project has already suffered once: the Scala predecessor stalled building "Illuminate-shaped infrastructure bottom-up with no real app pulling requirements" (`why-go-not-scala.md`). In parallel, an API-parity fixture-recording harness can start at t=0 with zero Go code, since it only needs a running PHP backend to record against — this should be treated as an independent workstream, not a phase gated behind framework progress.
|
|
|
|
The key risks are not "will Go's ecosystem support this" (STACK.md and go-ecosystem.md answer that affirmatively for every named concern, OAuth2/OIDC included) but **silent parity drift**: Go's idioms actively fight several PHP behaviors this app depends on byte-for-byte — nil slices marshal to `null` where PHP guarantees `[]`, `time.Time` renders `Z` where Carbon renders `+00:00`, a plain `bool` cannot express PHP's tri-state `null`/`true`/`false` fields, GORM's default many-to-many silently drops the `sort_order` pivot column driving album-artist display order, and a "clean" unified response-envelope or error-middleware instinct will break the RFC 8414/6749 OAuth endpoints the MCP server and ChatGPT connector depend on. None of these fail loudly; all are exactly the class of bug a parity harness diffing full response bodies (not just status codes) is built to catch. The roadmap should treat the parity harness as a first-class deliverable from phase 1, not a late QA pass.
|
|
|
|
## Key Findings
|
|
|
|
### Recommended Stack
|
|
|
|
The stack is almost entirely already-decided (per PROJECT.md); this research pass confirmed versions and resolved five open integration questions. All core picks (Go 1.27, GORM v1.31.2 + `gorm.io/driver/postgres` v1.6.3, River v0.47.0, zitadel/oidc v3.51.0, golang-jwt/jwt v5.3.1, go-playground/validator v10.30.4, cobra v1.10.2, gocloud.dev v0.46.0, koanf/v2, go-i18n/v2) are current and actively maintained as of 2026-09-16 (verified via pkg.go.dev, GitHub, and official docs directly, not secondhand).
|
|
|
|
**Core technologies and resolved questions:**
|
|
- **GORM + River share one `*sql.DB`** (via `pgx/v5/stdlib`), not two independent pools — River's own docs (`riverqueue.com/docs/gorm`) document this pattern directly; a small separate `pgxpool.Pool` is used only for River's LISTEN/NOTIFY wake-ups.
|
|
- **Migrations: gormigrate, not goose.** This is a load-bearing correction to earlier documents (ARCHITECTURE.md and PITFALLS.md both still reference goose per the older `go-ecosystem.md` pick). STACK.md's deeper dive concludes gormigrate's plain `[]*gormigrate.Migration` structs (with first-class `RollbackLast()`/`RollbackTo()`) compose more naturally with per-plugin migration sets aggregated at boot than goose's filesystem-and-provider model, and matches the "drop the last migration and fix it" workflow PROJECT.md names verbatim. **This is a genuine open decision to close before the data-layer phase — see Gaps below.**
|
|
- **Config: koanf + plain YAML, not literal HOCON.** No maintained Go HOCON parser exists at this project's dependency-quality bar. PROJECT.md's "HOCON-style layered config" should be read as a layering *design* (base + env overlay + per-plugin namespace), which koanf achieves natively with YAML — not a mandate for HOCON syntax.
|
|
- **YAML parsing: goccy/go-yaml, not `gopkg.in/yaml.v3`.** `gopkg.in/yaml.v3`'s upstream was archived by its maintainer April 2025 and is explicitly marked unmaintained (confirmed by reading the repo directly) — this changes a style preference into a must, for both `fields.yaml`/`columns.yaml` parsing and (transitively, via koanf) config.
|
|
- **OpenAPI: swaggo/swag (comment-driven, code-first), not Huma.** Huma's typed-handler convention would restructure all 154 ported routes right when byte-compatible parity with an existing contract is the entire point; swag annotates ordinary `net/http` handlers with no signature changes.
|
|
- **GORM plain structs, not gorm gen/go-gorm/cli codegen**, for v1 — matches the explicit "simplest line-by-line Eloquent-to-GORM port" design goal.
|
|
|
|
### Expected Features
|
|
|
|
Płytarium's actual source (not a generic CMS feature list) defines table stakes. FEATURES.md read `Plugin.php`, `routes.php` (569 lines / ~160 routes), all 5 admin controllers' YAML configs, all 25 models, 3 jobs, and 3 console commands directly.
|
|
|
|
**Must have (table stakes, all P1):**
|
|
- Plugin descriptor with register/boot lifecycle, dependency declaration, and cross-plugin extension via events (not inheritance) — the core mechanism plugins use to extend each other (`getApiArray` payload extension, model-created notification listeners, auth guard registration)
|
|
- Full GORM relation/cast/hook fidelity: pivot models with business columns (not bare pivot arrays), polymorphic file attachments, `$jsonable`/`$fillable`/`$hidden`/`encrypted`-cast discipline, cascading soft-delete inside transactions, custom attribute casts (e.g. money as fixed-decimal string)
|
|
- **Three parallel, mutually-exclusive auth groups sharing controllers**: JWT (SPA), personal scoped token (`inv.scope:read|write|ai`, used by MCP), and public/onboarding — the single hardest routing requirement, since the same controller must serve different route subsets per group, plus 7+ named per-bucket rate limiters
|
|
- OAuth2.1 authorization server (auth code + PKCE, refresh tokens, RFC 7591 DCR, RFC 8414 discovery) for MCP/ChatGPT — already the named zitadel/oidc pick
|
|
- `fields.yaml`/`columns.yaml` → JSON schema pipeline including a **relation-manager schema type** (link/unlink UI for a belongsToMany with pivot) — used once in Płytarium (Collections' editors tab) but structurally necessary
|
|
- River-backed jobs with a debounce/coalescing pattern (wishlist digest) and a self-redispatching rate-limit-aware pattern (Discogs match job) — not just "a queue exists"
|
|
- Centrifugo publisher + pluggable per-channel-namespace authorizer registry (kept as existing infra, only publisher/token-issuing ported)
|
|
- Typesense sync that is tenant-scoped and always treated as a pre-filter re-gated in SQL, never a source of truth
|
|
- Multi-step, resumable, queued CSV import pipeline (not a single upload action)
|
|
- 3-tier BYOK-or-org-or-site-admin credential resolver pattern (used identically for Discogs and AI credentials)
|
|
|
|
**Should have / differentiators (not v1 blockers):** compile-time plugin registration removing WinterCMS's `class_exists`/`elevated` boot-order fragility; a single typed "fire and collect vs. fire and forget" event bus generalizing two hand-rolled PHP patterns into one kernel primitive; generalizing the 3-tier credential resolver into one reusable piece instead of two copies; a first-class relation-manager schema type replacing the `partial` field-type escape hatch.
|
|
|
|
**Explicitly deferred (confirmed unused by Płytarium, do not build for v1):** server-rendered theme engine, backend AJAX/partial-refresh framework, `Sluggable`/`Sortable`/`NestedTree`/`Revisionable` model behaviors (none used anywhere in the plugin — slugs are hand-rolled in lifecycle hooks), MySQL support, payments/chat/forum/video, policy-based row/field-level admin permissions (Płytarium's admin RBAC is coarse role gates only).
|
|
|
|
### Architecture Approach
|
|
|
|
Two-repo split: `summercms.go` (framework core, one Go module: `pact`, `towel`, `compass`, `festival`, `backpack`, `party`, `bonfire`, `surf`, `lagoon`, `bouncer`, `lifeguard`, `cooler`, `conga`, `postcard`, `sandcastle`, `phrasebook`, `sunset`) and `fonoteka.go` (the application, a `go.work` workspace of per-plugin Go modules requiring the framework). The framework has zero knowledge of Płytarium; plugins import the framework, never the reverse. Stack plugins (user, translate, websockets) live inside the app repo's workspace for v1 and are only extracted to their own repos when a second app needs them.
|
|
|
|
**Major components:**
|
|
1. **`party` (plugin registry)** — Caddy/xcaddy-style: each plugin self-registers via `init()`, `summer build` regenerates a blank-import list; one required `Plugin` interface (`ID/Requires/Register/Boot`) plus small optional capability interfaces (`HasModels`, `HasRoutes`, `HasJobs`, etc.) replace WinterCMS's optional-override inheritance.
|
|
2. **Three concrete plugin-extends-plugin mechanisms**, not one generic hook system: (a) typed "filter" events on `festival` for response/serialization extension, (b) GORM's own callback registry (`db.Callback()`) for model lifecycle extension, (c) migration + companion embedding struct for schema extension (one plugin adding columns to another's table). Conflating these into one abstraction is explicitly called out as the wrong move.
|
|
3. **`lagoon` (GORM/Postgres) + `conga` (River)** sharing one `*sql.DB`, with a separate pgx listener pool for LISTEN/NOTIFY — the concrete first-party-documented integration pattern from STACK.md.
|
|
4. **`surf` (routing)** implementing the three-auth-group/per-bucket-rate-limit requirement as first-class routing structure, with OAuth/RFC routes in a structurally separate group carrying no shared envelope/error middleware.
|
|
5. **First vertical slice**: `GET /_fonoteka/api/v1/genres` end-to-end (config → container → migration → plugin registry → JWT-guarded route → GORM → JSON diffed against a PHP fixture) is identified as the one phase that cannot be skipped or parallelized — every other phase's pattern is a repetition of this slice's pieces.
|
|
6. Suggested build order is a 9-layer DAG (pact/towel → compass/phrasebook/bonfire → festival → backpack → party → lagoon/surf → bouncer → [vertical-slice checkpoint] → lifeguard/cooler/conga/postcard/sandcastle → admin schema + parity harness + bulk model/route porting, parallelizable → sunset last, thin).
|
|
|
|
### Critical Pitfalls
|
|
|
|
Fifteen critical pitfalls were identified, each cited to specific PHP source lines. The five most consequential for roadmap sequencing:
|
|
|
|
1. **Bottom-up kernel over-building (recurrence risk)** — this project already failed once this way in Scala. Avoid by interleaving kernel work with the first real vertical slice; never let plugin descriptor/event-bus/config shape get a "finished" design pass before a real model/route exists end to end.
|
|
2. **Package-level global state instead of `context.Context`** for per-request org/tenant/collection resolution — PHP-FPM's process-per-request model silently protected against this; Go's shared-process concurrency does not. Must be settled as a convention before any plugin consumes it.
|
|
3. **Reusing GORM model structs as JSON request-binding structs** — loses the `$fillable`/`$guarded` mass-assignment boundary that is a named security invariant in the PHP source (e.g., `market_price_source`, `public_token` deliberately excluded). Requires per-endpoint DTOs from the first ported model onward, not retrofitted later.
|
|
4. **A "clean" generic response envelope or blanket error/auth middleware** — the PHP house style deliberately has at least three incompatible envelope families (house REST with no `links` key, Laravel-shaped validation-error maps, and unenveloped RFC 8414/6749 OAuth responses with specific cache headers). A single `APIResponse[T]` generic wrapper is flagged explicitly as a smell to catch in phase planning, not a shortcut to allow.
|
|
5. **Type-level parity gaps that don't fail loudly**: nil slice → `null` vs PHP's guaranteed `[]`; `time.Time` → `Z` vs Carbon's `+00:00`; plain `bool` unable to express tri-state null/true/false; `float64` reintroducing float-precision bugs PHP deliberately avoided for money fields via a custom cast. All of these require the parity harness to diff full response bodies field-by-field, not just check status codes or rough shape.
|
|
|
|
## Implications for Roadmap
|
|
|
|
### Phase 1: Kernel foundation + first vertical slice (interleaved, not sequential)
|
|
**Rationale:** Directly avoids Pitfall 1 (the documented Scala failure mode). The kernel must be built exactly as far as the first vertical slice needs it, no further, before any additional kernel design work.
|
|
**Delivers:** `compass` (config), `backpack` (container), `festival` (event bus), `party` (plugin registry + `summer build`), `bonfire` (CLI skeleton) — each only fleshed out to the point `GET /_fonoteka/api/v1/genres` requires. Also establishes the `context.Context`-based org/tenant-resolution convention (Pitfall 2) before any plugin uses it.
|
|
**Addresses:** Kernel/plugins table-stakes features (plugin descriptor, dependency declaration, event bus, console command registration).
|
|
**Avoids:** Pitfall 1 (bottom-up over-build), Pitfall 2 (global state vs. context).
|
|
|
|
### Phase 2: First vertical slice checkpoint — one real endpoint end to end
|
|
**Rationale:** ARCHITECTURE.md identifies this as the one phase that cannot be skipped or parallelized: every later phase depends on all its pieces (config, container, migration, plugin registry, routing, JWT auth, GORM, JSON response) existing together.
|
|
**Delivers:** `Genre` (or `Style`) model + migration (via gormigrate — see Gaps), one JWT-guarded route through `surf`, a working `party` plugin registration, and the first parity-harness fixture (recorded from the live PHP backend, diffed byte-for-byte).
|
|
**Uses:** GORM + gormigrate, `golang-jwt/jwt`, stdlib `ServeMux`.
|
|
**Implements:** The full request-flow pattern in ARCHITECTURE.md that every subsequent route repeats.
|
|
|
|
### Phase 3: API parity harness (parallel workstream, starts at t=0)
|
|
**Rationale:** Near-zero dependency on framework internals — it only needs a running PHP backend to record fixtures against. All four research passes agree this should not be gated behind kernel/framework progress.
|
|
**Delivers:** Fixture-recording tooling against the live PHP backend, an `httptest.Server`-based replay-and-diff harness with a normalizer for non-deterministic fields, and — critically — assertions covering the specific parity classes named in Pitfalls 4/6/7/8 (nil-vs-empty-array, date format/null-vs-omitted, tri-state bool, envelope/conditional-key shape), not just status-code/rough-shape checks.
|
|
**Avoids:** Pitfalls 4, 5, 6, 7, 8 (all of which "look done but aren't" without a byte-level diff).
|
|
|
|
### Phase 4: Data layer — full model/relation/cast fidelity
|
|
**Rationale:** Every endpoint touches this; the security-load-bearing `$fillable`/`$hidden`/encrypted-cast discipline and the DTO-vs-model convention (Pitfall 3) must be established here, not retrofitted after models exist.
|
|
**Delivers:** Per-plugin gormigrate migration sets (one per PHP migration, up/down tested), all 25 models with correct relation types (dedicated pivot models with business columns, not bare arrays — Pitfall 12), polymorphic file attachments, soft-delete + partial unique index audits (Pitfall 11), money-as-fixed-decimal-string and other custom casts (Pitfall 5).
|
|
**Addresses:** Data/models table-stakes row in FEATURES.md in full.
|
|
**Avoids:** Pitfalls 3, 5, 11, 12, 13 (GORM AutoMigrate misuse).
|
|
|
|
### Phase 5: HTTP/auth — three-group routing, rate limiting, personal tokens
|
|
**Rationale:** The entire 154-route API surface sits behind this; retrofitting the routing structure after routes exist is called out explicitly as expensive.
|
|
**Delivers:** JWT + personal-scoped-token (`inv.scope`) + public/onboarding route groups sharing controllers, named per-bucket rate limiters ported 1:1 (not redesigned), the must-change-password gate with its one deliberate exception route, and — critically — a structural exemption of any future OAuth/RFC route group from generic envelope/error middleware, decided now rather than after 26 models' worth of routes exist.
|
|
**Uses:** `surf`, `bouncer`, `cooler` (rate-limit buckets).
|
|
**Avoids:** Pitfall 9 (blanket middleware corrupting OAuth), "rate-limit bucket parity" moderate pitfall.
|
|
|
|
### Phase 6: OAuth2.1 server (zitadel/oidc)
|
|
**Rationale:** Depends on the routing/middleware abstraction from Phase 5 existing first (same "not-the-SPA's-JWT" pattern), but is a distinct credential type (`OAuthClient`/`OAuthAuthCode`/`OAuthRefreshToken` vs. `ApiToken`) — build the shared abstraction once, then implement both guards against it.
|
|
**Delivers:** `AuthStorage`/`OPStorage` interface implementations mapped onto the existing OAuth models, RFC 8414/7591 endpoints, and exact `WWW-Authenticate`/Protected-Resource-Metadata header contract (Pitfall 10) verified against the actual `fonoteka-mcp` client, not just unit tests.
|
|
**Avoids:** Pitfalls 9, 10; the "trusting zitadel/oidc defaults without diffing" technical-debt pattern.
|
|
**Research flag:** needs verification of whether `ClientCredentialsStorage`/`TokenExchangeStorage` are actually needed (check `wavepath.org/plugins/golem15/oauthserver` before assuming not).
|
|
|
|
### Phase 7: Admin schema pipeline (fields.yaml/columns.yaml → JSON) + minimal Vue 3 SPA
|
|
**Rationale:** Requires the data-layer serialization boundary (Phase 4) to already exist; cannot be phased independently of it per FEATURES.md's dependency notes.
|
|
**Delivers:** Form/list/relation-manager JSON schema for the 5 admin controllers, OpenAPI generation via swaggo/swag, generated TS types, and a decision on the `partial` field-type escape hatch (likely: replace with the relation-manager schema directly, per the Collections' editors-tab case).
|
|
**Research flag:** relation-manager schema design and the `partial`-type replacement are the least-precedented parts of this research pass (used once in the PHP source) — worth a `--research-phase` pass.
|
|
|
|
### Phase 8: Background jobs, realtime, search, integrations
|
|
**Rationale:** Depends on Phase 4 (models/events) and benefits from Phase 1's event bus; independently parallelizable across CSV import, Discogs/AI clients, Centrifugo, and Typesense once the data layer is stable.
|
|
**Delivers:** River with the correct dual-driver split (`riverdatabasesql` for transactional enqueue, `riverpgxv5` for LISTEN/NOTIFY work processing — Pitfall 14), the CSV import multi-step pipeline, wishlist digest debounce, Centrifugo suppress-then-emit-once broadcast pattern, Typesense sync with the SQL re-gate (never trusting search results as pre-authorized — Pitfall 15), Discogs/AI clients with the 3-tier BYOK credential resolver.
|
|
**Avoids:** Pitfalls 14, 15, and the moderate "Centrifugo broadcast-suppression" and "rate-limit bucket parity" pitfalls.
|
|
|
|
### Phase 9: Bulk remaining route/model port + full parity cutover
|
|
**Rationale:** By this point the pattern is proven and repeatable; this phase is mostly volume, parallelizable across plugins/models gated only by FK order (the same DAG the 27 PHP migrations already encode).
|
|
**Delivers:** All remaining routes/models, full parity-harness green against all 154 routes, `vue-fonoteka-app` and `fonoteka-mcp` running unchanged against the Go backend — the literal definition of done in PROJECT.md.
|
|
|
|
### Phase Ordering Rationale
|
|
|
|
- Kernel and first-slice phases are interleaved specifically to prevent the Scala-era failure mode from recurring — this is the single strongest signal across all four research documents.
|
|
- The parity harness is pulled out as an explicit parallel-from-t=0 workstream because three of the four research documents independently flag it as near-zero-dependency and because its assertions are the actual mechanism that catches most of the "critical pitfalls" — sequencing it late would defeat its purpose.
|
|
- Data layer precedes HTTP/auth precedes OAuth because each has a documented one-way dependency (auth needs models to authenticate against; OAuth reuses the auth-group routing pattern) — reversing this order was explicitly flagged as more expensive to retrofit.
|
|
- Admin schema pipeline is placed after data layer specifically because FEATURES.md's dependency notes say these "cannot be phased independently" without under-building the `$fillable`/`$hidden` security contract.
|
|
- Background/integrations is placed late because it is the most parallelizable-once-unblocked phase and has the fewest cross-dependencies on routing/OAuth decisions.
|
|
|
|
### Research Flags
|
|
|
|
Needs deeper research during planning:
|
|
- **OAuth2.1 phase** — `zitadel/oidc` storage-interface implementation against the real PHP OAuth models, and the exact `ClientCredentialsStorage`/`TokenExchangeStorage` scope question (check `wavepath.org/plugins/golem15/oauthserver` first).
|
|
- **Admin schema pipeline phase** — the relation-manager schema type and the `partial` field-type replacement are the least-precedented design surface in this research (one real usage in the PHP source, no direct library equivalent).
|
|
- **Background/jobs phase** — River's dual-driver (`riverdatabasesql` vs `riverpgxv5`) split is documented but not yet implemented in this codebase; verify the transactional-enqueue-plus-LISTEN/NOTIFY pattern against a real test before trusting the docs-only description.
|
|
|
|
Phases with standard, well-documented patterns (safe to skip `--research-phase`):
|
|
- **Kernel/plugin-registry phase** — Caddy/xcaddy's compiled-plugin model is directly precedented and already verified against real xcaddy-generated code.
|
|
- **Data layer phase** — GORM patterns (relations, casts, callbacks) are well-documented; the risk here is PHP-source-fidelity auditing (a planning/testing discipline), not unknown Go patterns.
|
|
|
|
## Confidence Assessment
|
|
|
|
| Area | Confidence | Notes |
|
|
|------|------------|-------|
|
|
| Stack | HIGH | Versions and maintenance status verified directly via pkg.go.dev/GitHub/official docs this session; MEDIUM on judgment calls (migration tool, OpenAPI approach, config format) but those calls are well-reasoned and explicit about trade-offs |
|
|
| Features | HIGH for table stakes (grounded in direct reading of the full `fonoteka` PHP source — Plugin.php, routes.php, all models/controllers/YAML configs); MEDIUM for differentiators and anti-features (informed by ecosystem comparison, not exhaustively re-audited) |
|
|
| Architecture | HIGH for plugin-registration mechanics (verified against real Caddy/xcaddy docs) and request-lifecycle mapping (grounded in the real `Plugin.php`/`routes.php`); MEDIUM for the cross-plugin schema-extension pattern and the two-repo split (design recommendation, not yet validated by an actual build) |
|
|
| Pitfalls | HIGH for anything cited to actual PHP source (routes.php, controllers/api/*.php, models/Album.php, SerializesFonoteka.php, fonoteka-mcp/src/http.ts) or official GORM/River docs; MEDIUM for Go-ecosystem-general claims and the "Laravel-shaped framework in Go" class of pitfall |
|
|
|
|
**Overall confidence:** HIGH — this research is unusually well-grounded because three of the four passes read the actual target PHP application's source directly rather than reasoning from a generic domain description.
|
|
|
|
### Gaps to Address
|
|
|
|
- **gormigrate vs. goose is not yet settled across documents.** STACK.md's deep-dive recommends gormigrate (better fit for per-plugin migration composition and the "drop the last migration and fix it" workflow); ARCHITECTURE.md and PITFALLS.md still reference goose per the earlier `go-ecosystem.md` pick and have not been updated. **Resolve this explicitly before the data-layer phase** — do not let two documents silently disagree into implementation. STACK.md's reasoning is more recent and more specific to this project's shape; the roadmap should default to gormigrate unless a phase decision overrides it.
|
|
- **PROJECT.md's field-type list needs correction.** PROJECT.md's Active requirements list `number`, `fileupload`, `repeater`, `datepicker` as WinterCMS field types Płytarium uses; FEATURES.md's direct read of all 5 controllers' `fields.yaml` files found the actual set is `text`, `textarea`, `checkbox`, `switch`, `dropdown` (model-method-fed), `relation`, and `partial` (an escape hatch, not a clean schema type) — no `number`/`fileupload`/`repeater`/`datepicker` anywhere in the plugin. Update PROJECT.md's requirement line to match, and treat `partial` as the one field type needing a bespoke design decision (see Phase 7's research flag).
|
|
- **PROJECT.md's "HOCON-style layered config" wording should be clarified as a design, not a syntax mandate** — no viable Go HOCON parser exists; koanf + YAML achieves the same layering semantics. Worth a one-line PROJECT.md update so a future reader doesn't chase a literal HOCON parser.
|
|
- **No Sluggable/Sortable/NestedTree/Revisionable behaviors are used anywhere in Płytarium** — confirmed by direct source read. These should not appear as implicit v1 scope anywhere; PROJECT.md does not currently list them, but flag this explicitly so no phase accidentally builds a packaged Sluggable behavior when the PHP source only ever hand-rolls slugs in lifecycle hooks.
|
|
- **`gopkg.in/yaml.v3` is unmaintained (archived April 2025)** — any lingering assumption (in code, docs, or a future dependency choice) that yaml.v3 is the default YAML library should be corrected to `goccy/go-yaml` before it's load-bearing anywhere.
|
|
- **typesense-go (v3.2.0, Mar 2025) and centrifugal/gocent/v3's exact latest tag** are both flagged MEDIUM confidence in STACK.md due to research-session gaps since their last verified release — re-check both at the implementation time of the search/realtime phase rather than trusting the pinned versions blindly.
|
|
- **zitadel/oidc's `ClientCredentialsStorage`/`TokenExchangeStorage` necessity** is an open question STACK.md itself flags — resolve by reading `wavepath.org/plugins/golem15/oauthserver` before the OAuth phase's interface-implementation work begins.
|
|
|
|
## Sources
|
|
|
|
### Primary (HIGH confidence)
|
|
- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/` — Plugin.php, routes.php (569 lines/~160 routes), all 5 admin controllers + config_form/config_list/config_relation.yaml, all 25 models, 3 jobs, 3 console commands, config/fonoteka.php (read directly for FEATURES.md, ARCHITECTURE.md, PITFALLS.md)
|
|
- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/traits/SerializesFonoteka.php`, `controllers/api/AlbumApiController.php`, `controllers/api/OAuthTokenController.php`, `controllers/api/OAuthMetadataController.php`, `models/Album.php` — serialization/envelope/security contract detail (PITFALLS.md)
|
|
- `/media/nvme/dev/golem15/fonoteka/fonoteka-mcp/src/http.ts`, `vue-fonoteka-app/app/composables/useCentrifugo.ts` — client-side contract confirmation
|
|
- pkg.go.dev version pages, GitHub repos/releases for every core and supporting library (GORM, River, zitadel/oidc, golang-jwt, goccy/go-yaml, gormigrate, swaggo/swag, etc.) — fetched/read directly 2026-09-16
|
|
- `riverqueue.com/docs/gorm` — official River+GORM pool-sharing integration guide
|
|
- Caddy/xcaddy official docs and generated `main.go` mechanics; PocketBase hook-system/schema-driven-admin docs
|
|
- This repo's `.planning/PROJECT.md`, `.planning/notes/why-go-not-scala.md`, `.planning/research/go-ecosystem.md`, `CLAUDE.md`
|
|
|
|
### Secondary (MEDIUM confidence)
|
|
- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/user/`, `plugins/golem15/websockets/` — skimmed, not fully read
|
|
- WebSearch: Directus/Strapi/PocketBase/Goravel 2026 comparisons (cross-checked against training knowledge, MEDIUM confidence)
|
|
- koanf's YAML parser migrating to goccy/go-yaml — WebSearch-sourced, corroborated by two independent results and go-i18n's own release notes
|
|
|
|
### Tertiary (LOW confidence, needs validation)
|
|
- Exact latest tags for typesense-go and centrifugal/gocent/v3 (both flagged as needing a fresh check at implementation time)
|
|
- otter-vs-ristretto cache performance claim carried from go-ecosystem.md (maintainer benchmarks, not independently verified)
|
|
|
|
---
|
|
*Research completed: 2026-09-16*
|
|
*Ready for roadmap: yes*
|