229 lines
24 KiB
Markdown
229 lines
24 KiB
Markdown
---
|
|
phase: 05-data-layer-full-fidelity
|
|
plan: 05
|
|
type: execute
|
|
wave: 4
|
|
depends_on: ["05-02", "05-03", "05-04"]
|
|
files_modified:
|
|
- fonoteka.go/plugins/golem15/fonoteka/updates/20_remaining.go
|
|
- fonoteka.go/plugins/golem15/fonoteka/models/collection_invitation.go
|
|
- fonoteka.go/plugins/golem15/fonoteka/models/pending_invitation_registration.go
|
|
- fonoteka.go/plugins/golem15/fonoteka/models/csv_import.go
|
|
- fonoteka.go/plugins/golem15/fonoteka/models/csv_import_row.go
|
|
- fonoteka.go/plugins/golem15/fonoteka/models/notification.go
|
|
- fonoteka.go/plugins/golem15/fonoteka/models/wishlist_subscription.go
|
|
- fonoteka.go/plugins/golem15/fonoteka/models/wishlist_digest_queue.go
|
|
- fonoteka.go/plugins/golem15/fonoteka/models/settings.go
|
|
- summercms.go/lagoon/validate.go
|
|
- fonoteka.go/parity/fixtureplugin/plugin.go
|
|
- fonoteka.go/parity/fixtureplugin/migrations.go
|
|
- fonoteka.go/parity/cross_plugin_callback_test.go
|
|
- fonoteka.go/parity/testdata/php_schema_snapshot.sql
|
|
- fonoteka.go/parity/schema_diff_test.go
|
|
- fonoteka.go/parity/rollback_isolation_full_test.go
|
|
autonomous: true
|
|
requirements: [DATA-09, DATA-11, CLI-03]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "The remaining 8 models (CollectionInvitation, PendingInvitationRegistration, CsvImport, CsvImportRow, Notification, WishlistSubscription, WishlistDigestQueue, Settings) and their migrations exist, completing the 25-model port (DATA-09)"
|
|
- "notifications, wishlist_subscriptions and wishlist_digest_queue have no FK constraints on user_id/collection_id, matching PHP exactly — not fixed as an oversight (RESEARCH.md Open Question 3)"
|
|
- "Settings is a dedicated golem15_fonoteka_settings singleton-row table with a typed search_use_typesense BOOLEAN column, not Winter's generic system_settings mechanism (RESEARCH.md Open Question 2, resolved by the user)"
|
|
- "A committed, live-verified PHP schema snapshot exists in fonoteka.go, and a testcontainers test migrates every Go migration set and diffs information_schema/pg_catalog against it with a small commented allow-list — the D-02 proof (DATA-09)"
|
|
- "A test-only fixture plugin (not loaded by production app/app.go) extends golem15.fonoteka's Album lifecycle through its own Boot() GORM callback and adds a companion column via its own gormigrate set, without editing models/album.go and without registering that migration in fonoteka updates.All() (DATA-11, ARCHITECTURE.md Pattern 3b/3c, CONTEXT.md discretion)"
|
|
- "summer migrate:rollback --plugin=fonoteka rolls back only Fonoteka's own last migration, leaving golem15.user's history and earlier Fonoteka migrations untouched, now that the full 20+ migration schema exists (CLI-03)"
|
|
artifacts:
|
|
- path: fonoteka.go/plugins/golem15/fonoteka/updates/20_remaining.go
|
|
provides: "7-8 migrations completing the 25-model schema"
|
|
- path: fonoteka.go/parity/testdata/php_schema_snapshot.sql
|
|
provides: "Committed golden schema snapshot for the D-02 diff test"
|
|
- path: fonoteka.go/parity/schema_diff_test.go
|
|
provides: "TestSchemaMatchesPHPSnapshot"
|
|
- path: fonoteka.go/parity/fixtureplugin/plugin.go
|
|
provides: "Test-only plugin Boot() registering a GORM callback on Album create/save"
|
|
- path: fonoteka.go/parity/fixtureplugin/migrations.go
|
|
provides: "Fixture plugin's own gormigrate set adding demo_extension_note on the test DB only"
|
|
key_links:
|
|
- from: fonoteka.go/parity/fixtureplugin/plugin.go
|
|
to: fonoteka.go/plugins/golem15/fonoteka/models/album.go
|
|
via: "Boot() registers a GORM callback on Album without editing models/album.go; TestCrossPluginCallback is the only activator"
|
|
pattern: "Callback\\(\\)\\.Create\\(\\)"
|
|
- from: fonoteka.go/parity/schema_diff_test.go
|
|
to: fonoteka.go/parity/testdata/php_schema_snapshot.sql
|
|
via: "diff information_schema against the committed snapshot"
|
|
pattern: "php_schema_snapshot\\.sql"
|
|
---
|
|
|
|
<objective>
|
|
Finish the 25-model port (the remaining 8 models with no dense casts/relations), prove D-02's schema-equality claim with an automated diff against a committed live-verified PHP snapshot, demonstrate DATA-11's cross-plugin lifecycle-and-schema extension with a test-only fixture plugin, and re-verify CLI-03's rollback isolation now that the full migration set exists.
|
|
|
|
Purpose: this plan is the phase's proof point — every earlier plan built pieces, this one proves the whole schema equals PHP's and that the extension mechanisms (callback registry + companion migration) actually work end to end, not just in isolation.
|
|
Output: 8 remaining models + their migrations; a committed PHP schema snapshot + diff test; a test-only fixture plugin for DATA-11; a rollback-isolation test against the full production schema.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/phases/05-data-layer-full-fidelity/05-CONTEXT.md
|
|
@.planning/phases/05-data-layer-full-fidelity/05-RESEARCH.md
|
|
@.planning/phases/05-data-layer-full-fidelity/05-01-SUMMARY.md
|
|
@.planning/phases/05-data-layer-full-fidelity/05-02-SUMMARY.md
|
|
</context>
|
|
|
|
<interfaces>
|
|
<!-- Contracts this plan's tasks consume -->
|
|
|
|
From fonoteka.go's models/updates/classes registries (Plan 05-01, both plugins):
|
|
```go
|
|
func Register(models ...any); func All() []any
|
|
func Register(ms ...*gormigrate.Migration); func All() []*gormigrate.Migration
|
|
func RegisterHook(fn func(*gorm.DB) error); func RegisterHooks(gdb *gorm.DB) error
|
|
```
|
|
|
|
From fonoteka.go/parity/migrate_test.go (the existing isolation-test shape this plan's Task 3 re-runs against the full schema):
|
|
```go
|
|
func parityDB(t *testing.T) *sql.DB
|
|
func gormOnSharedPool(t *testing.T, db *sql.DB) *gorm.DB
|
|
func activateAppPlugins(t *testing.T) (*backpack.App, []party.Plugin)
|
|
```
|
|
|
|
From summercms.go/lagoon (unchanged public API this plan's tests call):
|
|
```go
|
|
func Migrate(gdb *gorm.DB, plugins []party.Plugin) error
|
|
func RollbackLast(gdb *gorm.DB, plugins []party.Plugin, pluginID string) error
|
|
func Status(gdb *gorm.DB, plugins []party.Plugin) ([]StatusRow, error)
|
|
```
|
|
</interfaces>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1 (fonoteka.go): Remaining 8 models and their migrations — invitations, CSV import, notifications, wishlist, settings</name>
|
|
<files>fonoteka.go/plugins/golem15/fonoteka/updates/20_remaining.go, fonoteka.go/plugins/golem15/fonoteka/models/{collection_invitation,pending_invitation_registration,csv_import,csv_import_row,notification,wishlist_subscription,wishlist_digest_queue,settings}.go, summercms.go/lagoon/validate.go</files>
|
|
<read_first>
|
|
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/updates/v1.0.8/create_collection_invitations_table.php
|
|
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/updates/v1.0.8/create_pending_invitation_registrations_table.php
|
|
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/updates/v1.1.2/create_csv_import_tables.php
|
|
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/updates/v1.1.6/add_csv_import_mode.php
|
|
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/updates/v1.2.1/create_notifications_table.php
|
|
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/updates/v1.2.2/create_wishlist_subscriptions_table.php
|
|
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/updates/v1.2.3/create_wishlist_digest_queue_table.php
|
|
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/CollectionInvitation.php (computed status accessor)
|
|
.planning/phases/05-data-layer-full-fidelity/05-RESEARCH.md (Full Per-Model Inventory rows 7, 9, 10, 12, 18, 19, 24, 25; Open Questions 2 and 3; Squashed Migration List rows 9-17)
|
|
</read_first>
|
|
<action>
|
|
Create `updates/20_remaining.go` (package `updates`) with migrations for: `create_collection_invitations` (`golem15_fonoteka_collection_invitations`: `collection_id`, `email`, `token_hash UNIQUE`, `invited_by`, `expires_at`, `accepted_at`, `accepted_by`, `revoked_at`, timestamps; FK `collection_id`->collections, `invited_by`/`accepted_by`->users), `create_pending_invitation_registrations` (`user_id UNIQUE`, `invitation_id`, timestamps), `create_csv_import_tables` (`golem15_fonoteka_csv_imports`: `user_id`, `collection_id`, `match_job_id`, `import_job_id`, `status`, `import_mode`, `column_map` text (jsonable), `original_filename`, `storage_path`, `row_count`, `error_message`, timestamps; `golem15_fonoteka_csv_import_rows`: `csv_import_id`, `row_index`, `status`, `raw_json`, `artist`, `title`, `candidates_json`, `selected_discogs_id`, `matched_album_id`, `draft_json`, `error_code`, `error_message`, timestamps, `UNIQUE(csv_import_id, row_index)`), `create_notifications` (`golem15_fonoteka_notifications`: `user_id`, `type`, `payload` text, `read_at`, timestamps — read the PHP migration and confirm it has no `->foreign()` call on `user_id`; add none), `create_wishlist_subscriptions` (`user_id`, `collection_id`, `ws_enabled`, `email_enabled`, `subscribed_at`, timestamps, `UNIQUE(user_id, collection_id)` — no FK on either column), `create_wishlist_digest_queue` (`user_id`, `collection_id`, `item_count`, timestamps, `UNIQUE(user_id, collection_id)` — no FK on either column). Verify every column type/default against the listed PHP files directly rather than guessing. End with a package `init()` registering the whole slice with `updates.Register(...)`.
|
|
|
|
Add `create_fonoteka_settings` to the same file (RESEARCH.md Open Question 2, resolved: dedicated typed table): `golem15_fonoteka_settings (id SERIAL PRIMARY KEY, search_use_typesense BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW())`, seeded with one singleton row (`id = 1`) via an idempotent existence-guarded insert, same style as the earlier genre/artist seed migrations.
|
|
|
|
Create `models/settings.go`: `Settings{ID uint; SearchUseTypesense bool; CreatedAt, UpdatedAt time.Time}`, `TableName() "golem15_fonoteka_settings"`, `Fillable() []string{"search_use_typesense"}`, `Rules() map[string]string{"search_use_typesense": "boolean"}`. Add one small case to `lagoon/validate.go`'s rule-translation table for the bare `boolean` token (treat it as a type-check no-op, since Go's `bool` field type already enforces this) — this is the only touch of that file in this plan.
|
|
|
|
Create the 7 remaining model files: `CollectionInvitation` (row 7, plus a `Status() string` method porting the PHP accessor's accepted/revoked/expired/pending precedence verbatim), `PendingInvitationRegistration` (row 18), `CsvImport`/`CsvImportRow` (rows 9/10, jsonable-typed columns via Plan 05-02's `lagoon.Jsonable[T]`), `Notification` (row 12, `Payload lagoon.Jsonable[map[string]any]`, `UserID uint` with no FK-generating GORM tag), `WishlistSubscription` (row 25, no FK), `WishlistDigestQueue` (row 24, no FK). Every file registers itself via `init()`.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/fonoteka.go && go build ./... && go vet ./...</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- A new migration test migrates the full `updates.All()` slice for `golem15.fonoteka` and asserts all 8 remaining tables exist, then rolls each back individually.
|
|
- `grep -n "REFERENCES" fonoteka.go/plugins/golem15/fonoteka/updates/20_remaining.go` shows no `REFERENCES` clause on the `notifications`, `wishlist_subscriptions`, or `wishlist_digest_queue` table definitions.
|
|
- Exactly one row exists in `golem15_fonoteka_settings` after migrate, and a second `Migrate()` call does not insert a duplicate.
|
|
</acceptance_criteria>
|
|
<done>All 8 remaining models and migrations exist; the FK-omission and Settings-storage decisions match RESEARCH.md's resolved open questions exactly.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2 (fonoteka.go): D-02 schema-diff proof — committed PHP snapshot + TestSchemaMatchesPHPSnapshot</name>
|
|
<files>fonoteka.go/parity/testdata/php_schema_snapshot.sql, fonoteka.go/parity/schema_diff_test.go</files>
|
|
<read_first>
|
|
fonoteka.go/parity/migrate_test.go (parityDB/gormOnSharedPool/activateAppPlugins/dsnWithDB helpers, dedicated-database-per-test shape lines 95-121)
|
|
.planning/phases/05-data-layer-full-fidelity/05-RESEARCH.md ("D-02 Verified: PHP Migrations Run Cleanly on Postgres" — the exact reproducible docker+winter:up+pg_dump method; "Squashed Migration List" for the full expected table set)
|
|
</read_first>
|
|
<action>
|
|
Reproduce the exact method RESEARCH.md documents as already verified this research session: run a scratch `postgres:16-alpine` container, run the real PHP app's `php artisan winter:up` against it (from the sibling PHP repo at `/media/nvme/dev/golem15/fonoteka`), then `pg_dump --schema-only` filtered to `golem15_fonoteka_*`, `system_files`, `users`, `golem15_user_organisations` tables, normalizing owner/privilege noise (`--no-owner --no-privileges`). Commit the resulting SQL as `fonoteka.go/parity/testdata/php_schema_snapshot.sql` — this is a one-time, offline generation step; CI/tests never need PHP installed.
|
|
|
|
Create `parity/schema_diff_test.go`'s `TestSchemaMatchesPHPSnapshot`: spin up a dedicated ICU pl-PL database (same pattern as `migrate_test.go`'s `TestRollbackLastIsolatesFonoteka`), run `lagoon.Migrate(gdb, plugins)` for both `golem15.user` and `golem15.fonoteka` plus the framework's `system_files` set, then load the committed snapshot into a second dedicated database (`psql < php_schema_snapshot.sql` via `exec.Command` or by parsing the SQL directly), and diff both databases' `information_schema.columns`/`information_schema.table_constraints`/`pg_indexes` for the shared table set, failing on any unexplained difference (missing table, missing column, type mismatch, missing/extra unique or FK constraint). Keep a small, explicitly commented allow-list `var allowedDiffs = map[string]string{...}` for genuinely intended differences. Required entries (each with an inline comment naming the decision):
|
|
1. `golem15_fonoteka_settings` existing in the Go schema with no PHP equivalent, since Settings uses a dedicated table per the resolved open question rather than Winter's generic `system_settings`.
|
|
2. `users` table column-count gap: the PHP snapshot dumps the full 52-column Winter `users` table; Go keeps the Phase-3 stub (no `organisation_id`/`organisation_role`/membership columns). This is intended. Cite the user-confirmed no-`widen_users` decision (D-03 deviation, deferred to Phase 7 AUTH-02). Do NOT add a `widen_users` migration to close this diff — that would contradict the confirmed decision.
|
|
Document exactly why each allow-listed entry exists, per the scope_reduction/gap-handling discipline: this is a deliberate decision, not a silently-tolerated gap. Run `lagoon.Migrate` against the production plugins only (`golem15.user` + `golem15.fonoteka` + framework `system_files`). Do not activate the DATA-11 fixture plugin in this test. Do not add `demo_extension_note` (or any fixture-only column) to `allowedDiffs` — that column must not exist on the production schema (D-02).
|
|
</action>
|
|
<verify>
|
|
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/fonoteka.go && go vet ./parity/... && go test ./parity/... -run TestSchemaMatchesPHPSnapshot</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `fonoteka.go/parity/testdata/php_schema_snapshot.sql` is committed and non-empty, covering all 25 Fonoteka tables plus `system_files`/`users`/`golem15_user_organisations`.
|
|
- `TestSchemaMatchesPHPSnapshot` fails if a column is deliberately dropped from a migration in a scratch branch (spot-check this manually during development, then restore before finishing).
|
|
- Every entry in `allowedDiffs` has an inline comment naming the specific decision that justifies it: `golem15_fonoteka_settings` cites RESEARCH Open Question 2; the `users` table column-count gap cites the user-confirmed no-`widen_users` decision (D-03 deviation, deferred to Phase 7 AUTH-02). Do not close the `users` gap with a `widen_users` migration.
|
|
- `grep -n "demo_extension_note" fonoteka.go/parity/schema_diff_test.go fonoteka.go/plugins/golem15/fonoteka/updates` returns no matches (production schema and its allow-list never mention the fixture column).
|
|
</acceptance_criteria>
|
|
<done>The committed PHP schema snapshot exists and `TestSchemaMatchesPHPSnapshot` proves the full Go schema matches it modulo a small, justified, commented allow-list.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3 (fonoteka.go): DATA-11 test-only fixture plugin; CLI-03 rollback-isolation re-verification against the full production schema</name>
|
|
<files>fonoteka.go/parity/fixtureplugin/plugin.go, fonoteka.go/parity/fixtureplugin/migrations.go, fonoteka.go/parity/cross_plugin_callback_test.go, fonoteka.go/parity/rollback_isolation_full_test.go</files>
|
|
<read_first>
|
|
.planning/research/ARCHITECTURE.md (Pattern 3b GORM callback registry, Pattern 3c companion struct + own migration)
|
|
.planning/phases/05-data-layer-full-fidelity/05-RESEARCH.md ("Pattern: Cross-plugin lifecycle extension without editing the owning model" code example)
|
|
fonoteka.go/parity/migrate_test.go (TestRollbackLastIsolatesFonoteka — the exact shape to re-run against the now-full migration set; activateAppPlugins/parityDB/gormOnSharedPool helpers)
|
|
.planning/phases/05-data-layer-full-fidelity/05-CONTEXT.md (Claude's Discretion: a fixture plugin in tests is acceptable if no real Płytarium case fits)
|
|
fonoteka.go/app/app.go (production PluginIDs — must not gain the fixture)
|
|
fonoteka.go/plugins.gen.go (generated blank-imports — must not gain the fixture)
|
|
summercms.go/party/registry.go (Register is process-wide; the fixture must not call it from init())
|
|
summercms.go/pact/capabilities.go (HasMigrations)
|
|
</read_first>
|
|
<action>
|
|
Demonstrate criterion 5's two halves with a test-only fixture plugin, not an intra-plugin demo inside golem15.fonoteka (CONTEXT.md discretion: a fixture plugin in tests is acceptable if no real Płytarium case fits). Create package `fixtureplugin` under `fonoteka.go/parity/fixtureplugin/` (same app module, not under `plugins/golem15/` so `summer.yaml`/`plugins.gen.go` never pick it up). `plugin.go` implements `party.Plugin` and `pact.HasMigrations`: `ID()` returns `"parity.fixtureplugin"`, `Requires()` returns `[]string{"golem15.fonoteka"}`. Do NOT call `party.Register` from `init()` and do NOT blank-import this package from `app/app.go` or `plugins.gen.go` — a process-wide Register would leak into other tests. `Boot(app *backpack.App) error` looks up `*gorm.DB`, then registers a GORM callback `gdb.Callback().Create().After("gorm:create").Register("fixtureplugin:demo_album_created", ...)` (and the matching Update hook if save also needs it) that type-asserts `tx.Statement.Schema.ModelType == reflect.TypeOf(models.Album{})` before incrementing an exported atomic counter; it must not edit `models/album.go`. `migrations.go` returns this plugin's own `[]*gormigrate.Migration` with `demo_add_album_notes_extension_column` running `ALTER TABLE golem15_fonoteka_albums ADD COLUMN demo_extension_note TEXT` and a Rollback that drops it. That slice is returned from `Migrations()` only — it is never passed to fonoteka's `updates.Register` / `updates.All()`. Query the companion column through a fixture-local struct embedding `models.Album` (ARCHITECTURE.md Pattern 3c), never by adding the field to `models/album.go`.
|
|
|
|
Create `parity/cross_plugin_callback_test.go`'s `TestCrossPluginCallback`: use the existing `parityDB`/`gormOnSharedPool`/`activateAppPlugins` helpers to migrate the production plugins, then construct `&fixtureplugin.Plugin{}` in-process (no `party.Register`), call `Boot` against the app whose `*gorm.DB` is published, and `lagoon.Migrate(gdb, []party.Plugin{fix})` so the companion column exists on this test DB only. Insert an Album, assert the callback fired exactly once, also create a Genre and assert the counter does not increment, and assert `demo_extension_note` exists on this database via `information_schema`. A production-only migrate (Task 2's schema-diff test) must not see that column.
|
|
|
|
Add `parity/rollback_isolation_full_test.go` re-running `TestRollbackLastIsolatesFonoteka`'s exact shape (dedicated ICU pl-PL database, migrate both production plugins' full sets, call `lagoon.RollbackLast(gdb, plugins, "golem15.fonoteka")`) against the now-complete Fonoteka `updates.All()` only — the fixture plugin's companion migration is not in that set. Assert only the single last-registered Fonoteka migration is rolled back, `golem15.user`'s history table is completely untouched, and every earlier Fonoteka migration/table survives.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/fonoteka.go && go vet ./... && go test ./parity/... -run TestCrossPluginCallback && go test ./parity/... -run TestRollbackIsolatesFonotekaFullSchema</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- The fixture callback fires exactly once per `Album` create and zero times for any other model's create (assert both, e.g. by also creating a `Genre` and confirming the counter does not increment).
|
|
- `grep -n "demo_extension_note" fonoteka.go/plugins/golem15/fonoteka/models/album.go` returns no match; `grep -n "demo_extension_note" fonoteka.go/plugins/golem15/fonoteka/updates` returns no match (the companion column is not in fonoteka's production migration set).
|
|
- `grep -n "fixtureplugin" fonoteka.go/app/app.go fonoteka.go/plugins.gen.go` returns no match; `grep -n "party.Register" fonoteka.go/parity/fixtureplugin` returns no match.
|
|
- The rollback-isolation test against the full production schema passes: `golem15.user`'s migration count and table set are byte-identical before and after the call.
|
|
</acceptance_criteria>
|
|
<done>A test-only fixture plugin extends Album's lifecycle and schema without editing fonoteka's own model file or production migration set; CLI-03's isolation guarantee is re-proven against the complete Phase 5 production schema.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|--------------|
|
|
| schema-diff test -> committed snapshot | the snapshot is the ground truth for "matches PHP" — it must be regenerated only via the documented reproducible method, never hand-edited to make a failing diff pass |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|-------------|-----------------|
|
|
| T-05-17 | Tampering (test integrity) | `allowedDiffs` allow-list in `TestSchemaMatchesPHPSnapshot` | mitigate | Every entry requires an inline comment naming the specific decision that justifies it; Plan 05-06's review checklist re-checks this list is not being used to paper over an unintended gap |
|
|
| T-05-18 | Repudiation / Tampering | `notifications`/`wishlist_subscriptions`/`wishlist_digest_queue` lacking FK constraints | accept | Deliberate parity match with PHP (RESEARCH.md Open Question 3); documented in the migration's own comment so it is never "fixed" by a later, uninformed change |
|
|
| T-05-19 | Elevation of Privilege | fixture-plugin cross-plugin callback registration | accept | Test-only fixture plugin, not loaded by production `app/app.go`; gated behind `models.Album` type-assertion so it cannot fire for any other model; not wired into any HTTP path this phase |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
`cd fonoteka.go && go vet ./... && go test ./...` (testcontainers Postgres) covering the remaining 8 models, the schema-diff test, the test-only fixture plugin, and the full-schema rollback-isolation test.
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- All 25 Płytarium models and their squashed migration sets exist and are covered by `updates.All()`.
|
|
- `TestSchemaMatchesPHPSnapshot` is green against a committed, reproducibly-generated snapshot with a small, justified allow-list and no fixture-only columns.
|
|
- A test-only fixture plugin's companion migration and Boot() callback demonstrate DATA-11's two extension halves without editing the owning model's file or fonoteka `updates.All()`.
|
|
- `migrate:rollback --plugin=fonoteka` isolation holds against the complete production schema.
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/05-data-layer-full-fidelity/05-05-SUMMARY.md` when done
|
|
</output>
|