From c3e9eae0833074142857633d66b38bdcb3dc4ad0 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Fri, 18 Sep 2026 15:23:21 +0200 Subject: [PATCH] docs(05): capture phase context --- .../05-data-layer-full-fidelity/05-CONTEXT.md | 159 ++++++++++++++ .../05-DISCUSSION-LOG.md | 194 ++++++++++++++++++ 2 files changed, 353 insertions(+) create mode 100644 .planning/phases/05-data-layer-full-fidelity/05-CONTEXT.md create mode 100644 .planning/phases/05-data-layer-full-fidelity/05-DISCUSSION-LOG.md diff --git a/.planning/phases/05-data-layer-full-fidelity/05-CONTEXT.md b/.planning/phases/05-data-layer-full-fidelity/05-CONTEXT.md new file mode 100644 index 0000000..805d91b --- /dev/null +++ b/.planning/phases/05-data-layer-full-fidelity/05-CONTEXT.md @@ -0,0 +1,159 @@ +# Phase 5: Data layer full fidelity - Context + +**Gathered:** 2026-09-18 +**Status:** Ready for planning + + +## Phase Boundary + +Phase 5 ports the whole Płytarium data layer: all 25 models of `golem15.fonoteka` (plus the `golem15.user` tables they depend on) with matching table names, columns, indexes, defaults, relations, casts and lifecycle hooks, and a Go migration set whose final schema equals PHP's. Requirements DATA-03 to DATA-11 and CLI-03. Roadmap mode: mvp. The phase is security-load-bearing: mass-assignment boundaries and encrypted-at-rest credentials are named invariants in the PHP source, so the security-review agent runs on it and the DTO-vs-model convention applies from the first model. + +Two repos. `summercms.go` grows `lagoon` with what the models pull in: lifecycle hooks and transactional soft-delete cascade, relation helpers with ordered results and pivot models, `Fill` with the fillable allow-list, the hidden discipline, rule-string validation with the Laravel-shaped 422 map, casts (jsonable, money as fixed 4-decimal string, `lagoon.Encrypted`), the `system_files` attachment model with blob storage and thumbnails, the pagination envelope, the GORM callback registry for cross-plugin extension, and `migrate:rollback --plugin` hardening (CLI-03). It still knows nothing about Płytarium. `fonoteka.go` restructures its two flat Phase 3 plugins into the Winter directory layout and receives the models, the migration set, the write services' fill boundary and the tests. + +Not in this phase: any HTTP route, including the photo-upload endpoint (Phase 12, API-02), the guarded outbound fetch for cover URLs (HTTP-07), Discogs cover import (Phase 14), River jobs, Centrifugo broadcasting and Typesense indexing behind `Album`'s PHP traits (Phase 11), the user plugin's auth flows (Phase 7), the OAuth provider storage implementation (Phase 8; only the OAuth *models and tables* land here), admin form schemas (Phase 9), and the production data import (Phase 15). + +Known drift to correct in planning: the roadmap says "27 migrations"; `updates/version.yaml` now lists 38 PHP migration files across 26 versions. The count is not an acceptance number (see D-01). + + + + +## Implementation Decisions + +### Migration granularity +- **D-01:** Go migrations stay squashed per final-state table (P3 D-16 stands). One create migration per table, or per small tightly-coupled table group (e.g. OAuth tables, CSV import tables, the two Discogs credential tables), in FK order, each carrying a comment naming the PHP update files it folds. The PHP rename/flatten history (areas, items, locations, tags, item_categories) is not replayed. Success criterion 1 is read as: every Go migration runs up and down individually, and the final schema matches PHP's. The roadmap's "27" is not a target; the planner should fix the wording in ROADMAP.md/REQUIREMENTS.md DATA-09 when the phase is planned. +- **D-02:** Schema equality is proven by an automated diff, not by reading. A normalized snapshot of the PHP app's final schema on Postgres (tables, columns, types, nullability, defaults, indexes, unique constraints, FKs) is produced once and committed in `fonoteka.go`; a testcontainers test migrates the Go sets up and diffs `information_schema`/`pg_catalog` against it. Intended differences live in a small, commented allow-list in the test. How the snapshot is produced (running the PHP migrations against a scratch Postgres vs. another route) is research; if the PHP migrations do not run cleanly on Postgres, research reports that before planning. +- **D-03:** The Phase 3 minimal tables are widened by one appended ALTER migration per table (`widen_users`, `widen_collections`, `widen_albums`) placed right after the two shipped Phase 3 migrations, followed by the new create-table migrations. Shipped migrations are never edited (P3 D-17). Each widen migration has a real Rollback. +- **D-04:** Reference data in the PHP history (the various-artist taxonomy seed, any settings defaults) is ported as idempotent data migrations inside the plugin's set, keyed by natural key, with a Rollback that removes them, same as the genres seed (P3 D-18). No seeder command. + +### Model DX: fillable, hidden, rules +- **D-05:** Each model declares `Fillable() []string`, a mechanical copy of PHP `$fillable`. `lagoon.Fill` copies onto the model only names that are both requested by the caller and in the model's fillable list. Write services pass their own narrower list exactly as PHP does (`AlbumWriteService::FILL_FIELDS` is a strict subset of `Album::$fillable`: no `collection_id`, no `market_price_source`). The model list is the backstop, the service list is the real boundary. `json.Unmarshal` into a GORM model is never used on a write path (PITFALLS Pitfall 3). +- **D-06:** Non-fillable and unknown keys are silently ignored, as Eloquent does; they are never persisted and never produce a 422. In non-production environments the dropped keys are logged once per call site. +- **D-07:** Criterion 3 is proven at service level in this phase: the fill boundary of the PHP write services is ported now (at minimum Album, Collection and the four credential models) and fuzzed against real Postgres with random extra and server-owned keys, asserting nothing outside the list is persisted. `lagoon.Fill` has its own framework-level fuzz test on a fixture model. The HTTP-level fuzz over request DTOs is Phase 12's job; criterion 3's "every write endpoint" clause moves there and the planner notes it in the roadmap. +- **D-08:** Hidden columns carry `json:"-"` on the GORM model so an accidental marshal cannot leak them, and models also declare `Hidden() []string` (for the Phase 9 admin schema and for a lagoon test that marshals every registered model and asserts hidden names are absent). API payloads are not produced by marshaling models: they come from ported `Serialize*` functions, as PHP's `SerializesFonoteka` does. The "explicit per-call override" is a named, greppable function (`Reveal`-style), not a serializer option. +- **D-09:** Models declare `Rules() map[string]string` with the PHP rule strings copied verbatim. `lagoon` translates the Laravel rule grammar Płytarium uses (required, nullable, integer, numeric, between, min, max, in, unique:table, plus whatever research finds in the 25 models) onto go-playground/validator, runs it in a before-save callback, and returns the Laravel-shaped 422 error map with messages translated through `phrasebook`. `unique:table` is a DB check in the same step and must respect soft deletes the way Winter's does. An untranslatable rule fails loudly at boot or test time, never silently passes. + +### Encrypted cast +- **D-10:** The Go cast reads and writes only its own AES-256-GCM format. Laravel CBC payloads are never on the live read path. Phase 5 ships a small, tested helper that decrypts a Laravel `encrypted` payload (base64 JSON with iv/value/mac, AES-256-CBC + HMAC under `APP_KEY`) for the Phase 15 cutover import to re-encrypt; it is not wired into the cast. +- **D-11:** The key is `app.key` in config (`SUMMER_APP__KEY`), 32 bytes base64. Missing, short or undecodable key fails boot with no default, same rule as the JWT secret (P3 D-11). The column-encryption key is derived from it with HKDF and a fixed label so `app.key` can serve other purposes later without key reuse. A `summer key:generate` command prints a fresh key. +- **D-12:** Ciphertext is versioned: a format/key-id prefix, nonce, ciphertext+tag. Config accepts `app.previous_keys` as decrypt-only keys (Laravel's `APP_PREVIOUS_KEYS` equivalent). No re-encrypt command in this phase. +- **D-13:** Encrypted columns are typed `lagoon.Encrypted` on the model: Scanner/Valuer decrypt on read and encrypt on write; `MarshalJSON`, `String()` and `GoString()` always emit a redaction; plaintext is only reachable through an explicit `.Reveal()`. Applies to `api_key` on `UserAiCredential` and `OrgAiCredential` and to the secret columns of `UserDiscogsCredential` and `OrgDiscogsCredential`; research lists the exact columns from the PHP `$casts`. + +### Attachments (DATA-08) +- **D-14:** Attachments use Winter's `system_files` table shape and name (`disk_name`, `file_name`, `file_size`, `content_type`, `title`, `description`, `field`, `attachment_id`, `attachment_type`, `is_public`, `sort_order`, timestamps). `attachment_type` keeps the PHP class string (`Golem15\Fonoteka\Models\Album`) through a per-model morph name, so cutover copies rows and files verbatim and later Winter ports reuse it. The table and `File` model are framework-owned (`lagoon`) with their own migration set that runs before plugin sets. +- **D-15:** Full storage scope lands in this phase (user chose the largest option over the recommended table-only cut): `gocloud.dev/blob` is wired now, files are stored and deleted through it, and thumbnails are generated. Only the HTTP upload endpoint stays in Phase 12. Planner should size the phase accordingly. +- **D-16:** v1 bucket is `fileblob` rooted at the same `storage/app/uploads` tree PHP uses, with bucket URL and public path prefix from a `storage.uploads.*` config section mirroring `cms.php`'s `storage.uploads`. Winter's `disk_name` partition path (`public/xxx/yyy/zzz/`) and the relative public URL shape (`relativeMediaUrl`) are reproduced exactly. The framework serves public files with a static handler at the configured path; S3/GCS is a config change later. `memblob` in unit tests. +- **D-17:** `Thumb(w, h, mode)` reproduces Winter's thumb filename and location (next to the original, `thumb____0_0_.`; research confirms the exact rule from Winter Storm's `File` model) and generates lazily on first call through blob when the thumb is missing. Thumbs copied at cutover are reused, and `thumb_url` stays byte-identical for the parity diff. Pixel equality with PHP GD is not a goal. +- **D-18:** The resize library is research's pick, recorded in RESEARCH.md as the phase decision naming the dependency: pure Go, maintained, no cgo/libvips. stdlib `image/*` plus `golang.org/x/image/draw` is the baseline to beat. +- **D-19:** Deletion follows Winter: soft-deleting an owner keeps `system_files` rows and blobs; force delete removes rows inside the delete transaction and removes originals and thumbs from blob after commit. This runs through the same lifecycle-callback mechanism as the other cascades (DATA-03), not a special case. + +### Claude's Discretion +- Lifecycle hook naming and wiring (Winter names `BeforeValidate`/`BeforeCreate`/`BeforeSave`/`BeforeDelete`/`AfterDelete` as optional interfaces vs. GORM's own hook names), the base model embed, and how soft-delete cascade runs in one transaction. +- Pivot model shape for `album_artists.sort_order` and `CollectionEditor` (GORM `SetupJoinTable` vs explicit has-many-through), as long as the 3+ artist ordering round-trip and the business columns hold. +- Money cast type and API: a named string-backed type in `models/` (or `lagoon` if generic) that reproduces `MarketPriceCast::get` (null for null/blank/non-numeric, else 4-decimal string) and leaves normalisation to the `Album` before-save hook, as PHP does. Never `float64` in the JSON path. +- Jsonable cast shape, including `[]` vs `null` behavior per column (Pitfall 4). +- Pagination helper API; the envelope is fixed: `{data, meta{current_page,last_page,per_page,total}}`, no `links`. +- Callback-registry API surface (ARCHITECTURE.md Pattern 3b) and which plugin/model pair demonstrates criterion 5's cross-plugin extension plus companion migration; a fixture plugin in tests is acceptable if no real Płytarium case fits. +- Soft-delete + unique strategy (partial unique indexes vs. matching PHP's actual constraints); D-02's schema diff decides what "matching" means, the delete-then-recreate test must pass for every soft-deletable uniquely-keyed table. +- Which `Serialize*` functions are ported now (only those needed by this phase's tests) vs. Phase 12. +- `Album`'s service-calling hook (`ArtistResolver` in `beforeSave`) becomes a callback registered from `classes/`, per the layout note; Scout/Broadcastable traits are left as seams for Phase 11. +- CLI-03 details beyond what Phase 3 shipped. +- Plan count and split, subject to the plan-count checkpoint and "unit tests are the last plan" rules. Given D-15, expect more than the usual number of plans. + +### Folded Todos +- **Verify the models-leaf layout rule against keios.eu and a jz/pxpx plugin before Phase 5** (`.planning/todos/pending/verify-models-leaf-rule.md`). Runs as the first task of the phase, before 24 models are laid out under the rule: repeat the `models/ → classes/` (and any sibling) `use`-edge probe on one keios.eu stack plugin with models and services (`/media/nvme/dev/golem15/keios.eu/plugins/golem15/user` or `paymentgateway`), one jz plugin and one pxpx plugin; classify each edge as cast / service-calling hook / other. Any "other" edge means `.planning/notes/plugin-layout-winter-directories.md` is amended before the bulk port proceeds. + + + + +## Canonical References + +**Downstream agents MUST read these before planning or implementing.** + +### The PHP data layer being ported +- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/*.php` — the 25 models: `$table`, `$fillable`, `$guarded`, `$hidden`, `$casts`, `$jsonable`, `$dates`, `$rules`, relations, hooks. `Album.php` is the densest (constants, money cast, jsonable, soft delete, belongsToMany artists, attachMany photos, `beforeSave` with `ArtistResolver`). +- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/updates/version.yaml` and `updates/v*/` — 38 migration files across 26 versions; the history D-01 squashes and the seeds D-04 ports. +- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/classes/casts/MarketPriceCast.php` — money cast semantics and the docblock explaining the blank-string case. +- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/classes/AlbumWriteService.php` — `FILL_FIELDS` and the fill/fillEmpty/overwrite boundary that D-05 and D-07 port and fuzz. +- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/UserAiCredential.php`, `OrgAiCredential.php`, `UserDiscogsCredential.php`, `OrgDiscogsCredential.php` — `encrypted` casts and `$hidden` (D-10 to D-13). +- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/traits/SerializesFonoteka.php` — payload builders; lines ~190–215 for the photo payload, `getThumb(200, 200, ['mode' => 'crop'])` and `relativeMediaUrl` (D-08, D-16, D-17). +- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/Collection.php` lines ~91–107 — `attachMany photos` (ordered) and public `attachOne image`. +- `/media/nvme/dev/golem15/fonoteka/config/cms.php` lines ~300–335 — `storage.uploads` / `media` / `resized` disks and paths (D-16). +- Winter Storm `File` model and `Winter\Storm\Database\Attach\File` in `/media/nvme/dev/golem15/fonoteka/vendor/winter/storm` — `disk_name` partitioning, `getPath`, `getThumb` naming, delete behavior (D-14, D-17, D-19). +- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/user/models/` and its `updates/` — the user-plugin tables that `widen_users` must match. + +### Go code this phase builds on +- `lagoon/connection.go`, `lagoon/migrations.go`, `lagoon/commands.go`, `lagoon/order.go` — shared `*sql.DB`, per-plugin gormigrate runner, `migrate:*` commands, Polish order helper. +- `pact/capabilities.go` — `HasModels`/`HasMigrations` and friends. +- `../fonoteka.go/plugins/golem15/fonoteka/migrations.go` and `../fonoteka.go/plugins/golem15/user/migrations.go` — the shipped, never-edited Phase 3 migrations (`202609170001_create_schema`, `202609170002_seed_genres`) that D-03 appends to. +- `../fonoteka.go/plugins/golem15/fonoteka/*.go` and `../fonoteka.go/plugins/golem15/user/*.go` — the flat Phase 3 plugins to restructure into the Winter layout. +- `internal/build/` — the `models/`-is-a-leaf enforcement shipped in Phase 4 (P4 D-11) and the `make:model`/`make:migration` stubs that should match what this phase hand-writes. + +### Layout and architecture decisions +- `.planning/notes/plugin-layout-winter-directories.md` — directory mapping and the models-leaf rule (casts into `models/`, service-calling hooks out to `classes/`). +- `.planning/todos/pending/verify-models-leaf-rule.md` — the folded todo with its probe commands. +- `.planning/research/ARCHITECTURE.md` §Pattern 3 (3a events, 3b GORM callback registry, 3c companion structs over shared tables), §Migration Flow, §Anti-Pattern 4. Text that says goose is superseded by gormigrate. +- `.planning/research/PITFALLS.md` §Pitfall 3 (DTO vs model), §Pitfall 4 (nil vs `[]`), §Pitfall 5 (float vs fixed-decimal string), §Pitfall 7 (tri-state bool), §Pitfall 11 (soft delete + unique), §Pitfall 12 (pivot columns), §Pitfall 13 (no AutoMigrate), §Security Mistakes, §Integration Gotchas row on gocloud.dev/blob URL shape. +- `.planning/research/STACK.md` §GORM: plain structs, §Migration tooling, §Version Compatibility (gorm v1.31.2, validator v10.30.4, gocloud.dev v0.46.0, testcontainers v0.44.0). +- `.planning/research/FEATURES.md` — model-feature inventory from the PHP source. + +### Prior phase decisions that bind this phase +- `.planning/phases/03-first-vertical-slice-genres-end-to-end/03-CONTEXT.md` D-02, D-06, D-11, D-16, D-17, D-18, D-19. +- `.planning/phases/04-cli-scaffolding-i18n-and-mail/04-CONTEXT.md` D-10, D-11, D-12, D-13. +- `.planning/phases/01-framework-kernel-foundation/01-CONTEXT.md` D-06, D-08, D-10 (config namespaces, env mapping, fail-fast boot). +- `.planning/PROJECT.md` §Constraints, §Key Decisions; `.planning/REQUIREMENTS.md` DATA-03 to DATA-11, CLI-03; `.planning/ROADMAP.md` Phase 5 section. +- `CLAUDE.md` (repo root) §GSD workflow rules. + + + + +## Existing Code Insights + +### Reusable Assets +- `lagoon`: connection on the shared pgx-stdlib `*sql.DB`, per-plugin gormigrate runner with per-plugin history tables, `migrate`, `migrate:rollback --plugin`, `migrate:status`, the `pl-PL` ICU order helper and its testcontainers Postgres test harness. +- `phrasebook` (Phase 4) for translated validation messages; `compass` for `app.key`, `app.previous_keys` and `storage.uploads.*`; `backpack` to publish the blob bucket and file service; `bonfire` for `key:generate`. +- `internal/build` leaf-import check and `make:model`/`make:migration` stubs. +- `fonoteka.go/parity` testcontainers `TestMain`: the place the schema-diff test and the service-level fuzz tests can share a Postgres with. + +### Established Patterns +- No AutoMigrate anywhere, tests included; shipped migrations are never edited; each plugin's set rolls back without touching another's history. +- Fail boot loudly on misconfiguration (now also: bad `app.key`, untranslatable rule string, unconfigured uploads bucket). +- Response DTOs and explicit serializers, never the GORM model; `[]` not `null`; no generic envelope. +- Framework never imports the app; anything Płytarium-specific (money cast semantics, write services, morph names) lives in `fonoteka.go`. +- New dependencies need naming: `gocloud.dev` is already decided; the image resize library is named by this phase's research (D-18). Nothing else without a decision note. + +### Integration Points +- The Phase 3 plugins in `fonoteka.go` are flat single packages; this phase moves them to `models/`, `classes/`, `controllers/`, `middleware/`, `updates/` with `plugin.go`/`routes.go` at the root, keeping the genres route and its parity fixture green throughout. +- The framework's `system_files` migration set must order before every plugin set. +- `Album`'s Scout and Broadcastable behavior are seams only; Phase 11 attaches them through the callback registry built here. +- OAuth models and tables land here; the zitadel/oidc storage that uses them is Phase 8. + + + + +## Specific Ideas + +- PHP's real mass-assignment boundary is two-layered: `$fillable` as backstop, a narrower per-service list as the working boundary. The Go port keeps both layers rather than collapsing them into DTO shape alone, so an agent can diff `Fillable()` against the PHP array line by line. +- Rule strings, fillable lists and hidden lists are copied verbatim from PHP so conversion stays mechanical; translation to Go idiom happens once, inside `lagoon`. +- Secrets should be unable to leak by accident: `json:"-"`, a redacting `lagoon.Encrypted`, and explicit `Reveal` calls the security review can grep. +- `system_files` and the uploads tree stay Winter-shaped so cutover is a copy, and so the next Winter project port gets attachments for free. +- The user chose to take blob storage and thumbnails in this phase despite the smaller recommended cut; treat it as a deliberate choice, not scope creep, and plan capacity for it. + + + + +## Deferred Ideas + +- HTTP-level fuzz of request DTOs on every write endpoint — Phase 12/13 when the routes exist (second half of criterion 3). +- Photo upload endpoint, manual cover URL and Discogs cover import — Phase 12 (API-02), HTTP-07, Phase 14. +- Re-encrypt command for key rotation — after v1; the versioned format (D-12) keeps it possible. +- Cutover import that decrypts Laravel payloads and re-encrypts, and copies `system_files` rows and the uploads tree — Phase 15, using the helper from D-10. +- S3/GCS bucket for uploads — config change after cutover. +- Typesense indexing and Centrifugo broadcasting hooks on `Album` — Phase 11. +- Correcting the "27 migrations" wording in ROADMAP.md and REQUIREMENTS.md DATA-09, and moving criterion 3's endpoint clause — do at plan time for this phase. + + + +--- + +*Phase: 5-Data layer full fidelity* +*Context gathered: 2026-09-18* diff --git a/.planning/phases/05-data-layer-full-fidelity/05-DISCUSSION-LOG.md b/.planning/phases/05-data-layer-full-fidelity/05-DISCUSSION-LOG.md new file mode 100644 index 0000000..ce02123 --- /dev/null +++ b/.planning/phases/05-data-layer-full-fidelity/05-DISCUSSION-LOG.md @@ -0,0 +1,194 @@ +# Phase 5: Data layer full fidelity - Discussion Log + +> **Audit trail only.** Do not use as input to planning, research, or execution agents. +> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered. + +**Date:** 2026-09-18 +**Phase:** 5-Data layer full fidelity +**Areas discussed:** Todo folding, Migration granularity, Model DX (fillable, hidden, rules), Encrypted cast compatibility, Attachments depth (DATA-08) + +--- + +## Todo folding + +| Option | Description | Selected | +|--------|-------------|----------| +| Fold in as first task | Run the models-leaf probe on keios.eu, jz and pxpx plugins at the start of Phase 5; amend the note if an "other" edge appears | ✓ | +| Keep it separate | Leave as a pending todo; apply the rule on Fonoteka evidence only | | + +**User's choice:** Fold in as first task + +--- + +## Migration granularity + +Context raised: P3 D-16 (squash per table) conflicts with roadmap criterion 1 ("all 27 migrations run up and down individually"); `version.yaml` actually lists 38 files across 26 versions. + +### Relation to PHP history + +| Option | Description | Selected | +|--------|-------------|----------| +| Squash per table | Keep P3 D-16; one create migration per final-state table, commented with folded PHP files; reword criterion 1 | ✓ | +| Mirror PHP one-to-one | One Go migration per PHP file including renames and the items-to-albums flatten | | +| Squash per PHP version | One Go migration per version.yaml entry with a final-state effect | | + +### Proving schema equality + +| Option | Description | Selected | +|--------|-------------|----------| +| Schema-diff test vs PHP dump | Committed normalized snapshot of PHP's final Postgres schema; testcontainers test diffs information_schema with a documented allow-list | ✓ | +| Hand-audited checklist | Reviewer reads migration comments against PHP | | +| You decide | Researcher picks after checking PHP migrations on Postgres | | + +### Widening the Phase 3 tables + +| Option | Description | Selected | +|--------|-------------|----------| +| One widen migration per table | widen_albums, widen_collections, widen_users after the Phase 3 pair | ✓ | +| Single "phase 5 widen" migration | One ALTER migration for all three | | +| You decide | Planner orders by FK needs | | + +### Data seeds + +| Option | Description | Selected | +|--------|-------------|----------| +| Idempotent data migrations | Same as genres seed (P3 D-18) | ✓ | +| Separate seed command | Schema-only migrations plus `fonoteka:seed` | | + +--- + +## Model DX: fillable, hidden, rules + +Context raised: PHP API never serializes models via `toArray()` (`SerializesFonoteka` builds payloads); `AlbumWriteService::FILL_FIELDS` is a strict subset of `Album::$fillable`. + +### Declaring fillable + +| Option | Description | Selected | +|--------|-------------|----------| +| Declared list + lagoon.Fill | `Fillable() []string` copied from PHP; `lagoon.Fill` intersects requested and fillable names; services pass narrower lists | ✓ | +| Struct tags | `summer:"fillable"` tags read by reflection | | +| Hand-written DTO copy only | No framework concept; DTO shape is the only boundary | | + +### Unknown / non-fillable keys + +| Option | Description | Selected | +|--------|-------------|----------| +| Silently ignored, as PHP | Dropped, never persisted; logged once in non-production | ✓ | +| Reject with 422 | Stricter than PHP; changes the contract | | + +### Proving criterion 3 without write routes + +| Option | Description | Selected | +|--------|-------------|----------| +| Service-level fuzz now | Port write services' fill boundary and fuzz against Postgres; HTTP fuzz in Phase 12 | ✓ | +| One real write route now | Pull a write route forward as the DTO reference | | +| Framework-only test | Fuzz lagoon.Fill on a fixture model only | | + +### Hidden + +| Option | Description | Selected | +|--------|-------------|----------| +| json:"-" + explicit serializers | `json:"-"` on secrets, `Hidden()` list, marshal test, named Reveal override | ✓ | +| Generic lagoon.Serialize(model, opts) | Reflection serializer with WithVisible option | | + +### Rules + +| Option | Description | Selected | +|--------|-------------|----------| +| Rules() map on model, validated on save | PHP strings verbatim; lagoon translates to validator; Laravel-shaped 422 | ✓ | +| validate struct tags | Hand-translated go-playground tags per field | | +| You decide | Researcher evaluates grammar coverage first | | + +--- + +## Encrypted cast compatibility + +### Existing Laravel ciphertext + +| Option | Description | Selected | +|--------|-------------|----------| +| GCM only; re-encrypt at import | Cast handles only AES-GCM; ship a Laravel decrypt helper for the Phase 15 import | ✓ | +| Cast reads both, writes GCM | CBC decrypt on the live read path, lazy re-encrypt | | +| No migration of secrets | Users re-enter keys after cutover | | + +### Key source + +| Option | Description | Selected | +|--------|-------------|----------| +| app.key, fail boot if missing | 32-byte base64, HKDF-derived column key, `summer key:generate` | ✓ | +| Dedicated database.encryption_key | Separate secret for column encryption | | + +### Rotation + +| Option | Description | Selected | +|--------|-------------|----------| +| Versioned format + previous keys | Key-id prefix; `app.previous_keys` decrypt-only | ✓ | +| Single key, no version prefix | Simplest; rotation needs a format change later | | + +### Go type + +| Option | Description | Selected | +|--------|-------------|----------| +| lagoon.Encrypted type | Scanner/Valuer; redacting MarshalJSON/String; explicit Reveal() | ✓ | +| Plain string + serializer tag | `gorm:"serializer:encrypted"` on a string | | + +--- + +## Attachments depth (DATA-08) + +### Table + +| Option | Description | Selected | +|--------|-------------|----------| +| Winter's system_files shape | Same table and columns, PHP class strings as morph names, framework-owned | ✓ | +| New clean table | Go-native `summer_files` | | + +### Storage depth + +| Option | Description | Selected | +|--------|-------------|----------| +| Table, relations and path/URL math only | Recommended cut; blob and thumbs later | | +| Full blob storage now | gocloud.dev/blob in this phase, thumbs later | | +| Everything incl. thumbnails | Blob storage plus 200x200 crop thumbs | ✓ | + +**Notes:** User picked the largest scope over the recommendation. HTTP upload endpoint remains Phase 12. + +### Thumb shape + +| Option | Description | Selected | +|--------|-------------|----------| +| Same name and path, lazy on first call | Winter's thumb filename next to the original, generated when missing | ✓ | +| Same name, eager at attach time | Generate declared sizes on attach | | +| You decide | Researcher confirms Winter's rules first | | + +### Image library + +| Option | Description | Selected | +|--------|-------------|----------| +| stdlib image + x/image/draw | No cgo, quasi-stdlib | | +| disintegration/imaging | Convenient, unmaintained since 2020 | | +| Researcher picks | Maintained pure-Go option recorded in RESEARCH.md; no cgo | ✓ | + +### Bucket and serving + +| Option | Description | Selected | +|--------|-------------|----------| +| fileblob at storage/app, config-driven URL, Go static handler | Mirrors cms.php storage.uploads; memblob in tests | ✓ | +| Same, no Go static handler | Reverse proxy serves files | | + +### Delete behavior + +| Option | Description | Selected | +|--------|-------------|----------| +| As Winter | Soft delete keeps files; force delete removes rows in-tx and blobs after commit | ✓ | +| Never delete blobs automatically | Prune command later | | + +--- + +## Claude's Discretion + +Hook naming and base model embed; pivot model shape; money cast type; jsonable cast shape; pagination helper API; callback-registry API and the cross-plugin extension demo pair; soft-delete + unique index strategy; which Serialize* functions are ported now; CLI-03 details; plan count and split. + +## Deferred Ideas + +HTTP-level DTO fuzz (Phase 12/13); upload endpoint, manual cover URL, Discogs cover import (Phase 12/14); re-encrypt command; cutover import of secrets and files (Phase 15); S3/GCS bucket; Typesense and Centrifugo hooks on Album (Phase 11); roadmap wording fixes ("27 migrations", criterion 3 endpoint clause) at plan time.