--- phase: 09-backend-admin-authentication-and-schema-pipeline plan: 04 subsystem: admin tags: [cabana, list-schema, gorm, pagination, filters, phrasebook] requires: - phase: 09-backend-admin-authentication-and-schema-pipeline provides: cabana list compiler, form localizer, and backend guard provides: - Ordered Winter list schema with columns, actions, default sort, and page sizes - Switch, date-range, and registered model-scope filters without raw SQL - Allowlisted list query execution with D-11 envelopes and primary-key tie-break affects: [09-backend-admin-authentication-and-schema-pipeline, admin-api, phase-10-spa] actuals: tokens: 22349 tasks: 3 commits: 7 tech-stack: added: [] patterns: - "List IR caches phrase keys; Localize copies labels per request" - "Query identifiers resolve only through compiled columns, filters, and FilterScopes" - "lagoon.Paginate supplies last-page math; the admin envelope keeps D-11's page key" key-files: created: - cabana/list_schema.go - cabana/filter_schema.go - cabana/query.go - cabana/testdata/list/all_columns.yaml - cabana/testdata/list/all_filters.yaml modified: - cabana/schema.go - cabana/schema_types.go - cabana/http.go - cabana/contracts.go - pact/capabilities.go key-decisions: - "Omitted sortable defaults to true, except relation columns, which stay unsortable unless columns.yaml sets sortable" - "perPageOptions keep YAML order and must include recordsPerPage" - "list_toolbar, recordUrl, and showCheckboxes compile to create, update, and delete" - "A conditions key is a boot error; a model scope must be listed by FilterScopes()" - "Admin list meta uses D-11 page, while lagoon.Paginate still computes last_page" patterns-established: - "Pattern: list compilation walks declaration order and rejects unknown keys before routes are served" - "Pattern: search, sort, and filter values are bound; identifiers come only from the compiled schema" requirements-completed: [ADMIN-02] coverage: - id: D1 description: Every locked list column, action, default sort, search term, page size, and showSetup switch compiles to stable Winter JSON. requirement: ADMIN-02 verification: - kind: unit ref: cabana/list_schema_test.go#TestListSchemaCompile status: pass human_judgment: false - id: D2 description: Empty and single list declarations stay arrays, and column plus perPageOptions order is stable across compiles. requirement: ADMIN-02 verification: - kind: unit ref: cabana/list_schema_test.go#TestListSchemaEmpty status: pass - kind: unit ref: cabana/list_schema_test.go#TestListSchemaSingle status: pass - kind: unit ref: cabana/list_schema_test.go#TestListSchemaOrdering status: pass human_judgment: false - id: D3 description: Duplicate columns, unknown keys, bad defaults, path escape, a mismatched modelClass, and unsupported actions fail with plugin, controller, and file context. requirement: ADMIN-02 verification: - kind: unit ref: cabana/list_schema_test.go#TestListSchemaRejects status: pass human_judgment: false - id: D4 description: Switch, date-range, and model-scope filters compile in source order with typed switch values and no condition string. requirement: ADMIN-02 verification: - kind: unit ref: cabana/list_schema_test.go#TestListSchemaFilter status: pass human_judgment: false - id: D5 description: Model scopes resolve only through FilterScopes, and raw conditions, unknown columns, and arbitrary method names fail activation. requirement: ADMIN-02 verification: - kind: unit ref: cabana/list_schema_test.go#TestListSchemaScope status: pass - kind: unit ref: cabana/list_schema_test.go#TestListSchemaRejectsRawCondition status: pass human_judgment: false - id: D6 description: Filter and option labels localize per request while column, scope, and option values stay unchanged on the cached schema. requirement: ADMIN-02 verification: - kind: unit ref: cabana/list_schema_test.go#TestListSchemaFilter/labels_localize status: pass human_judgment: false - id: D7 description: Search, sort, relation search, empty, single, and adjacent pages return the D-11 envelope with a primary-key tie-break. requirement: ADMIN-02 verification: - kind: integration ref: cabana/query_test.go#TestListQueryContract status: pass - kind: integration ref: cabana/query_test.go#TestListQueryEmpty status: pass - kind: integration ref: cabana/query_test.go#TestListQuerySingle status: pass - kind: integration ref: cabana/query_test.go#TestListQueryAdjacent status: pass human_judgment: false - id: D8 description: Switch, date-range, and model-scope filters narrow rows through bound values and the registered scope name. requirement: ADMIN-02 verification: - kind: integration ref: cabana/query_test.go#TestListQueryFilters status: pass human_judgment: false - id: D9 description: Unknown or case-changed identifiers, oversized pages, and injected search text fail closed, and permission denial still runs before the query. requirement: ADMIN-02 verification: - kind: integration ref: cabana/query_test.go#TestListQueryRejectsInjection status: pass human_judgment: false duration: 36min completed: 2026-09-24 status: complete plan_head_before: db3d7222e9cf733b9926e29921c5e74d6abab579 plan_head_after: 3efdcc1c59a94533f28427495bfe0a646aa3fd4e --- # Phase 9 Plan 04: Deterministic admin list queries Summary **Winter list YAML now compiles to ordered columns and typed filters, and the admin index runs that schema through bound, tie-broken queries.** ## Performance - **Duration:** 36 min - **Started:** 2026-09-24T16:29:24Z - **Completed:** 2026-09-24T17:05:17Z - **Tasks:** 3 - **Files modified:** 13 ## Accomplishments - `config_list.yaml` and `columns.yaml` compile to ordered columns, default sort, `searchTerm: "search"`, page-size choices, toolbar create, row update, and bulk delete. Empty collections marshal as `[]`. - `config_filter.yaml` accepts only switch, daterange, and a model scope named by `FilterScopes()`. A `conditions` key fails activation. Option values keep their JSON types. - `GET /_admin/api/v1/{vendor}/{plugin}/{controller}` searches, sorts, filters, and pages through those selectors. Equal sort values break ties on the primary key. Unknown identifiers return `validation_failed` after the permission check. - The 09-01 genre list still compiles, including a controller with no `config_form.yaml`. ## Task Commits Each task was committed atomically. `commits: 7` is `git rev-list --count` from the plan ledger in summercms.go. The tracer expectation lives in fonoteka.go. 1. **Task 1: Ordered list columns and actions (RED)** - `fd591ed` (test) 2. **Task 1: Ordered list columns and actions (GREEN)** - `aab4398` (feat) 3. **Task 2: Typed filters (RED)** - `cb832eb` (test) 4. **Task 2: Typed filters (GREEN)** - `d3a9307` (feat) 5. **Task 3: Deterministic list queries (RED)** - `7f451c3` (test) 6. **Task 3: Deterministic list queries (GREEN)** - `6a57f63` (feat) 7. **Relation sort default (fix)** - `3efdcc1` (fix) **App tracer:** `d8fb9ac` in fonoteka.go (test) **Plan metadata:** included in the docs commit for this summary ## Files Created/Modified - `cabana/list_schema.go` - strict list compiler, model column check, and request localizer - `cabana/filter_schema.go` - switch, daterange, and scope compilation - `cabana/query.go` - allowlisted search, sort, filter, scope, and pagination - `cabana/schema_types.go` - list, filter, and action JSON types - `cabana/schema.go` - shared YAML helpers; list compilation moved out - `cabana/http.go` - schema response and index handler use the compiled list - `cabana/contracts.go` - D-10 error details for validation - `pact/capabilities.go` - `FilterScopes()` catalog on `FilterScope` - `cabana/testdata/list/all_columns.yaml` - column fixture - `cabana/testdata/list/all_filters.yaml` - filter fixture - `fonoteka.go/plugins/golem15/fonoteka/admin_tracer_test.go` - unknown sort expects 422 ## Decisions Made - Omitted `sortable` means true, matching Winter, except a relation column, which is not a local column and stays unsortable unless YAML sets `sortable`. An explicit true orders the joined `select` column, then the primary key. - `perPageOptions` keep source order. `recordsPerPage` must be one of them. The query rejects any other `per_page`. - `toolbar.buttons: list_toolbar` is the create action. `recordUrl` is the update row action. `showCheckboxes` is bulk delete. Other button names fail activation. - The search query parameter is always `search`. YAML cannot rename it. - `conditions` is rejected with the column-or-scope rule. Scope names must appear in `FilterScope.FilterScopes()` exactly. - D-11 meta is `page`, `per_page`, `total`, `last_page`. `lagoon.Paginate` still computes `last_page`; its `current_page` is not the admin key. - `ADMIN-02` stays shared with later plans in this phase, so REQUIREMENTS.md is not marked complete by this plan alone. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 2 - Missing Critical] Served the full list schema and validation details** - **Found during:** Task 1 and Task 3 - **Issue:** The schema handler copied a subset of `ListSchema`, so new slices would marshal as null, and `WriteError` could not attach field messages. - **Fix:** The schema handler returns the localized schema. `WriteErrorDetails` writes D-10 `validation_failed` details. - **Files modified:** `cabana/http.go`, `cabana/contracts.go` - **Verification:** `TestListSchemaEmpty` and `TestListQueryRejectsInjection` passed - **Committed in:** `aab4398` and `6a57f63` **2. [Rule 2 - Missing Critical] Added FilterScopes() to the model capability** - **Found during:** Task 2 - **Issue:** `FilterScope` could execute a name but could not declare the finite set, so an arbitrary method could not be rejected without reflection. - **Fix:** `FilterScopes() []string` is now part of `pact.FilterScope`. Compilation accepts only those names. - **Files modified:** `pact/capabilities.go`, `cabana/filter_schema.go` - **Verification:** `TestListSchemaScope` and `TestListSchemaRejectsRawCondition` passed - **Committed in:** `d3a9307` **3. [Rule 1 - Bug] Stopped treating relation columns as sortable by default** - **Found during:** Task 3 - **Issue:** Omitted `sortable` became true for relation columns, while a sort on that key is not a local column. - **Fix:** Relation columns default to not sortable. Explicit `sortable: true` orders the joined select column and then the primary key. - **Files modified:** `cabana/list_schema.go`, `cabana/query.go` - **Verification:** `go test ./cabana -run '^(TestListSchema|TestListQuery)' -count=1` passed - **Committed in:** `3efdcc1` **4. [Rule 1 - Bug] Updated the genre tracer for unknown sorts** - **Found during:** Task 3 - **Issue:** `TestAdminTracerGenreList` expected `sort=users.email` to return 200. The new contract rejects that identifier. - **Fix:** The persisted genre is still read without a caller sort. The unknown sort expects `validation_failed` and the body does not echo the identifier. - **Files modified:** `fonoteka.go/plugins/golem15/fonoteka/admin_tracer_test.go` - **Verification:** `go test ./plugins/golem15/fonoteka -run '^TestAdminTracerGenreList$' -count=1` passed - **Committed in:** `d8fb9ac` (fonoteka.go) --- **Total deviations:** 4 auto-fixed (2 missing critical, 2 bug) **Impact on plan:** The extra capability and response fields are the locked list contract. No new dependency and no change to the frontend API. ## TDD Gate Compliance | Gate | Commit | Result | |------|--------|--------| | RED Task 1 | `fd591ed` | `TestListSchemaCompile` failed because `perPageOptions` was unknown. `TestListSchemaEmpty` compared the old four-key JSON. | | GREEN Task 1 | `aab4398` | `TestListSchema(Compile\|Empty\|Single\|Ordering\|Rejects)` passed | | RED Task 2 | `cb832eb` | `TestListSchemaFilter` failed on unknown field `filter` | | GREEN Task 2 | `d3a9307` | `TestListSchema(Filter\|Scope\|RejectsRawCondition)` passed | | RED Task 3 | `7f451c3` | `TestListQueryContract` returned every row for `search=Other` | | GREEN Task 3 | `6a57f63` | `TestListQuery(Contract\|Empty\|Single\|Adjacent\|Filters\|RejectsInjection)` passed | ## Issues Encountered None. ## User Setup Required None - no external service configuration required. ## Next Phase Readiness Ready for 09-05. List compilation and the index query are reusable by the later controller plans. `ADMIN-02` remains pending until every plan that declares it has a summary. ## Self-Check: PASSED - FOUND: cabana/list_schema.go - FOUND: cabana/filter_schema.go - FOUND: cabana/query.go - FOUND: cabana/testdata/list/all_columns.yaml - FOUND: cabana/testdata/list/all_filters.yaml - FOUND: fd591ed - FOUND: aab4398 - FOUND: cb832eb - FOUND: d3a9307 - FOUND: 7f451c3 - FOUND: 6a57f63 - FOUND: 3efdcc1 - FOUND: d8fb9ac RED evidence for `TestListSchemaCompile`, `TestListSchemaFilter`, and `TestListQueryContract` was checked with `gsd_run check tdd-red-evidence` and returned `RED_EVIDENCE_OK` before each implementation commit. --- *Phase: 09-backend-admin-authentication-and-schema-pipeline* *Completed: 2026-09-24*