Files
summercms/.planning/phases/03-first-vertical-slice-genres-end-to-end/03-02-SUMMARY.md
Jakub Zych a9c3e7690f docs(03-02): complete tenant-scoped genre counts plan
Tasks completed: 2/2
- Count only albums visible in the active collection
- Match PHP filter, validation, ordering and JSON shape

SUMMARY: .planning/phases/03-first-vertical-slice-genres-end-to-end/03-02-SUMMARY.md
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 20:14:09 +02:00

7.0 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed duration completed
03-first-vertical-slice-genres-end-to-end 02 api
postgres
gorm
active-collection
album-count
polish-order
validation
phase provides
03-first-vertical-slice-genres-end-to-end Shared pgx-stdlib pool, JWT genre route, 15 seeded genres
JWT default active-collection resolver with grouped owner/editor access
Tenant-scoped genre album_count aggregate (owned, editor, foreign)
Allow-listed lagoon ORDER BY without COLLATE (ICU pl-PL default)
PHP-shaped non_empty validation and 422 envelope
03-03
03-04
05
12
added patterns
AccessibleByMembership grouped owner OR editor, then AND active collection_id
lagoon.OrderBy(column, dir, allow-list) at the PHP PolishOrder call site
Dedicated GenreAggregate DTO with initialized data slice
created modified
lagoon/order.go
../fonoteka.go/plugins/golem15/fonoteka/active_collection.go
../fonoteka.go/parity/genre_integration_test.go
../fonoteka.go/plugins/golem15/fonoteka/genre_handler.go
../fonoteka.go/plugins/golem15/fonoteka/genre.go
../fonoteka.go/plugins/golem15/fonoteka/plugin.go
../fonoteka.go/parity/genre_smoke_test.go
lagoon.OrderBy takes a caller allow-list so the framework never hardcodes Fonoteka table names; the handler passes PHP PolishOrder::ALLOWED_COLUMNS
Duplicate non_empty query keys last-win, matching PHP parse_str; invalid then 1 is accepted, 1 then invalid is 422
Invalid stored context is rewritten to the lowest-ID accessible kind=collection row; auto-provisioning stays out of this slice
Pattern: grouped membership OR then AND collection_id in the albums subquery
Pattern: GenreAggregate DTO with integer id/album_count, data never null, Cache-Control no-cache, private
DATA-01
HTTP-02
QA-04
5min 2026-09-17

Phase 03 Plan 02: Tenant-scoped genre album counts Summary

Active-collection album counts with grouped owner/editor access, PHP non_empty 422, and database-default Polish name order

Performance

  • Duration: 5 min
  • Started: 2026-09-17T18:08:21Z
  • Completed: 2026-09-17T18:13:14Z
  • Tasks: 2
  • Files modified: 8

Accomplishments

  • JWT default path resolves stored context if the user owns or edits it, otherwise persists the lowest-ID accessible kind=collection row; foreign albums never enter the count.
  • Genre list left-joins COUNT(*) by genre_id onto all 15 seeded rows via a dedicated GenreAggregate DTO (id/album_count JSON numbers, empty list []).
  • non_empty=1 keeps positive counts; invalid values return 422 {"error":"Validation failed","errors":{"non_empty":["Zaznaczony non empty jest nieprawidłowy."]}}; lagoon.OrderBy emits golem15_fonoteka_genres.name ASC with no COLLATE.

Task Commits

Each task was committed atomically (framework summercms.go then app fonoteka.go when both change):

  1. Task 1: Count only albums visible in the active collection
    • fc7d5810f0796bf1d1b61e85a0e98482d67c144f (feat, fonoteka.go)
  2. Task 2: Match PHP filter, validation, ordering and JSON shape
    • 56be82f3f33b160ffb114905174072430f020432 (feat, summercms.go)
    • f5adabcc6d4f9bf0557d200f885b01fd7a8b82f1 (feat, fonoteka.go)

Plan metadata: (this commit)

Files Created/Modified

  • lagoon/order.go — allow-listed OrderBy helper, no COLLATE
  • lagoon/order_test.go — reject unknown column/direction; clause is … ASC/DESC
  • ../fonoteka.go/plugins/golem15/fonoteka/active_collection.go — ResolveActiveCollection, AccessibleByMembership
  • ../fonoteka.go/plugins/golem15/fonoteka/genre_handler.go — aggregate query, non_empty, Polish order call site
  • ../fonoteka.go/plugins/golem15/fonoteka/genre.go — minimal Album model
  • ../fonoteka.go/plugins/golem15/fonoteka/plugin.go — HasModels includes collection/album/context
  • ../fonoteka.go/parity/genre_integration_test.go — owned/editor/foreign counts, fallback, non_empty, 15-name order, 422, empty data:[], wrong-locale startup
  • ../fonoteka.go/parity/genre_smoke_test.go — first row is Blues under ICU pl-PL

Decisions Made

  • Framework lagoon.OrderBy is generic: the Fonoteka handler supplies PHP's four-column allow-list.
  • Duplicate non_empty keys follow PHP parse_str last-wins (confirmed with php -r, isolated :8423 was not running).
  • No auto-provisioning: a user with no accessible real collection gets 15 zero-count rows (or {"data":[]} when non_empty=1).
  • Plan 01 schema already had the columns this path needs; no ALTER migration was appended.

Deviations from Plan

Auto-fixed Issues

1. [Discretion] Duplicate non_empty keys checked via parse_str, not the isolated PHP HTTP endpoint

  • Found during: Task 2
  • Issue: Plan asked to hit the isolated PHP endpoint before encoding duplicate-key behavior. :8423 was not up.
  • Fix: Confirmed PHP last-wins with parse_str("non_empty=1&non_empty=0") and encoded the same rule in Go (vals[len(vals)-1]).
  • Files modified: ../fonoteka.go/plugins/golem15/fonoteka/genre_handler.go, ../fonoteka.go/parity/genre_integration_test.go
  • Verification: non_empty=2&non_empty=1 returns the filtered list; non_empty=1&non_empty=2 returns 422
  • Committed in: f5adabc (Task 2)

2. [Rule 1 - Bug] Smoke test first-row slug after Polish order

  • Found during: Task 2
  • Issue: Plan 01 smoke asserted data[0].slug == rock under ORDER BY id. ICU name order puts Blues first.
  • Fix: Assert Blues/blues in genre_smoke_test.go.
  • Files modified: ../fonoteka.go/parity/genre_smoke_test.go
  • Verification: go test ./... -run TestGenre passes
  • Committed in: f5adabc (Task 2)

Total deviations: 2 (1 duplicate-key source discretion, 1 smoke assertion) Impact on plan: No scope creep. QA-04 corpus parity remains Plan 03-03.

Issues Encountered

None that blocked the slice. Plan 01 tables covered owner/editor/context/kind; no appended migration.

User Setup Required

None - no external service configuration required beyond the existing Postgres ICU pl-PL database.

Next Phase Readiness

Ready for 03-03-PLAN.md (typed params, isolated rollback, first real parity pass). Do not mark the genres corpus route ported until that plan. QA-04 stays pending until the recorded fixture diffs green.

Self-Check: PASSED

  • Key files exist on disk (lagoon/order.go, ../fonoteka.go/plugins/golem15/fonoteka/active_collection.go, ../fonoteka.go/parity/genre_integration_test.go)
  • git log --grep=03-02 returns Task 1 and Task 2 commits in both repos
  • Acceptance: owned/editor/foreign counts, invalid-context fallback, grouped OR SQL, non_empty=1/422/empty [], 15-name Polish order, integer JSON, ICU locale fail before query, go vet/go test ./... green in both repos

Phase: 03-first-vertical-slice-genres-end-to-end Completed: 2026-09-17