--- phase: 05-data-layer-full-fidelity plan: 01 subsystem: database tags: [gorm, fillable, lifecycle, pagination, winter-layout, models-leaf, gormigrate] requires: - phase: 03-first-vertical-slice-genres-end-to-end provides: lagoon shared *sql.DB, Phase-3 flat fonoteka/user plugins, genres parity fixture - phase: 04-cli-scaffolding-i18n-and-mail provides: models-leaf enforcement in summer build, Winter-shaped make:* stubs provides: - confirmed models-leaf rule against keios.eu, jz, and pxpx plugins - Winter-directory fonoteka.go plugins with self-registering models/updates/classes registries - lagoon.Fill allow-list copy with once-per-key non-production log - GORM-native Has* lifecycle interfaces and WithSoftDeleteCascade - lagoon.Paginate {data, meta} envelope and RegisterJoinTable pivot contract affects: [05-02-album-collection, 05-03-secrets, 05-04-attachments, 05-05-remaining-models, 05-06-tests] tech-stack: added: [] patterns: - self-registering init() registries so later plans add files without editing plugin.go - Fill allow-list then apply, matching OrderBy - WithSoftDeleteCascade names GORM's in-transaction BeforeDelete cascade key-files: created: - lagoon/fill.go - lagoon/fill_test.go - lagoon/lifecycle.go - lagoon/lifecycle_test.go - lagoon/paginate.go - lagoon/paginate_test.go - lagoon/relations.go - lagoon/relations_test.go - .planning/todos/done/verify-models-leaf-rule.md modified: - .planning/notes/plugin-layout-winter-directories.md - .planning/ROADMAP.md - .planning/REQUIREMENTS.md - plugins/golem15/fonoteka/plugin.go - plugins/golem15/user/plugin.go - fonoteka.go/parity/*_test.go key-decisions: - "Models-leaf rule holds on keios.eu user, jz chat, and pxpx checkout; contracts/VOs/jobs/broadcasting stay inside the cast-or-hook conversion treatments" - "plugin.go Models()/Migrations() return registry All(); Boot calls classes.RegisterHooks when *gorm.DB is published" - "lagoon.Fill matches gorm column tags against the caller allow-list and logs dropped keys once per type+key in non-production" - "Lifecycle Has* interfaces use GORM-native signatures; WithSoftDeleteCascade does not open a new transaction" - "Paginate coerces nil data to []; RegisterJoinTable fails loud on nil db" - "DATA-09 migration count is not an acceptance number (D-01); HTTP DTO fuzz moves to Phase 12 (D-07)" patterns-established: - "One Register call per model/migration file init(); plugin.go is not edited by later Phase 5 plans" - "House REST pagination is Page[T] {Data, Meta} with no links key" requirements-completed: [DATA-03, DATA-06, DATA-10] duration: 16 min completed: 2026-09-18 --- # Phase 5 Plan 01: Winter layout and lagoon primitives Summary **Models-leaf rule confirmed on three more plugins; fonoteka.go restructured into Winter directories with self-registering registries; lagoon.Fill, lifecycle cascade, Paginate, and RegisterJoinTable shipped with tests** ## Performance - **Duration:** 16 min - **Started:** 2026-09-18T16:32:17Z - **Completed:** 2026-09-18T16:48:36Z - **Tasks:** 4 - **Files modified:** 37 ## Accomplishments - Probed keios.eu `golem15/user`, jz `golem15/chat`, and pxpx `pixelpixel/checkout`; every live `models/` → sibling edge is a cast or service-calling hook, so the leaf rule holds and the folded todo is closed - Restructured both Phase-3 `fonoteka.go` plugins into `models/`, `classes/`, `updates/` (plus `controllers/` and `middleware/` on fonoteka) with `Register`/`All` collectors; `plugin.go` returns `models.All()`/`updates.All()` and never needs a later-plan edit - `lagoon.Fill` copies only allow-listed keys, logs dropped keys once in non-production, and stays silent in production; five `Has*` hook interfaces plus `WithSoftDeleteCascade` are integration-tested on testcontainers Postgres - `lagoon.Paginate` emits `{data, meta{current_page,last_page,per_page,total}}` with no `links`; `RegisterJoinTable` documents the pivot-write contract and refuses a nil db - ROADMAP.md and REQUIREMENTS.md no longer treat "27 migrations" as a target; criterion 3's HTTP DTO fuzz is Phase 12's job ## Task Commits Each task was committed atomically: 1. **Task 1: Folded-todo probe** - `a9c77fc` (docs) in summercms.go 2. **Task 2: Restructure plugins into Winter directories** - `3171a49` (feat) in fonoteka.go 3. **Task 3: Fill/lifecycle tests (RED)** - `fec23e2` (test) in summercms.go 4. **Task 3: Fill/lifecycle implementation (GREEN)** - `969a5f5` (feat) in summercms.go 5. **Task 4: Paginate and RegisterJoinTable** - `52638fb` (feat) in summercms.go 6. **Task 4: DATA-09 / criterion-3 wording** - `c85fa24` (docs) in summercms.go **Plan metadata:** pending (this file) ## Files Created/Modified - `.planning/notes/plugin-layout-winter-directories.md` — three dated Evidence sections plus widened cast/hook conversion treatments - `.planning/todos/done/verify-models-leaf-rule.md` — closed folded todo - `fonoteka.go/plugins/golem15/fonoteka/{models,classes,controllers,middleware,updates}/*` — Winter layout and registries - `fonoteka.go/plugins/golem15/fonoteka/plugin.go`, `routes.go` — `models.All()`/`updates.All()`, hook Boot, genres route - `fonoteka.go/plugins/golem15/user/{models,classes,updates}/*` and `plugin.go` — same layout for the user plugin - `fonoteka.go/parity/*_test.go` — import paths follow the new packages - `lagoon/fill.go`, `lagoon/fill_test.go` — allow-list Fill + HasFillable/HasHidden - `lagoon/lifecycle.go`, `lagoon/lifecycle_test.go` — Has* hooks and transactional cascade - `lagoon/paginate.go`, `lagoon/paginate_test.go` — DATA-10 envelope - `lagoon/relations.go`, `lagoon/relations_test.go` — SetupJoinTable wrapper and pivot-write contract - `.planning/ROADMAP.md`, `.planning/REQUIREMENTS.md` — D-01/D-07 wording ## Decisions Made - The models-leaf rule is confirmed, not excepted: PHP contracts, value objects, formatters, job dispatch, and broadcasting traits convert as casts (move into `models/`) or service-calling hooks (register from `classes/`) - Later Phase 5 plans add files under `models/`, `updates/`, and `classes/`; they do not edit `plugin.go` - `WithSoftDeleteCascade` is a named one-line wrapper around the GORM `BeforeDelete` transaction, not a second transaction - `Paginate` does not slice rows; callers pass an already-sliced DTO list and the helper only computes `LastPage` - HTTP-level fill fuzz is explicitly Phase 12; this plan's DATA-06 delivery is the `lagoon.Fill` primitive ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 2 - Missing Critical] Nil pagination data marshals as `[]`** - **Found during:** Task 4 (Paginate envelope) - **Issue:** `json.Marshal` of a nil slice is `null`, which is Pitfall 4 and would break house REST `[]` parity - **Fix:** `Paginate` replaces a nil `rows` argument with `[]T{}` - **Files modified:** `lagoon/paginate.go`, `lagoon/paginate_test.go` - **Verification:** `TestPaginateNilDataIsEmptyArray` - **Committed in:** `52638fb` (Task 4 code) **2. [Rule 3 - Blocking] Shipped migration Go identifiers needed package qualifiers after the move** - **Found during:** Task 2 (plugin restructure) - **Issue:** `updates/00_base.go` cannot stay `package fonoteka` and still live in the `updates` subpackage; `CanonicalGenres`/`Genre{}` live in `models/` - **Fix:** SQL strings unchanged (P3 D-17); package clause and `models.CanonicalGenres`/`models.Genre{}` qualifiers only - **Files modified:** `fonoteka.go/plugins/golem15/fonoteka/updates/00_base.go` - **Verification:** `go test ./... -run TestParityCorpus` passing 1/ported 1; seed rollback still uses the same table/slug SQL - **Committed in:** `3171a49` (Task 2) --- **Total deviations:** 2 auto-fixed (1 missing critical, 1 blocking) **Impact on plan:** Both required for `[]`-not-null parity and a compiling Winter-layout move. No scope creep. ## Issues Encountered None. ## Authentication Gates None. ## User Setup Required None - no external service configuration required. ## Next Phase Readiness Ready for 05-02 (Album/Collection slice). Wave-2 plans can add model, update, and class files without editing `plugin.go`. `lagoon.Fill`, lifecycle hooks, `Paginate`, and `RegisterJoinTable` are available for those models. ## Self-Check: PASSED - Key files exist on disk (lagoon primitives, plugin registries, closed todo, three Evidence sections) - Commits `a9c77fc`, `3171a49`, `fec23e2`, `969a5f5`, `52638fb`, `c85fa24` exist - `go vet ./...` and `go test ./... -short` green in summercms.go - `go vet ./...` and `go test ./... -run TestParityCorpus` green in fonoteka.go - `grep "27 migrations" .planning/ROADMAP.md .planning/REQUIREMENTS.md` has no matches --- *Phase: 05-data-layer-full-fidelity* *Completed: 2026-09-18*