308 lines
32 KiB
Markdown
308 lines
32 KiB
Markdown
---
|
||
phase: 09-backend-admin-authentication-and-schema-pipeline
|
||
verified: 2026-10-01T18:35:29Z
|
||
status: human_needed
|
||
score: 5/5 roadmap success criteria verified (plan truths 54/54 verified, including 3 backstop truths with direct test evidence)
|
||
covered_files:
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-01-PLAN.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-01-SUMMARY.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-02-PLAN.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-02-SUMMARY.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-03-PLAN.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-03-SUMMARY.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-04-PLAN.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-04-SUMMARY.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-05-PLAN.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-05-SUMMARY.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-06-PLAN.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-06-SUMMARY.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-07-PLAN.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-07-SUMMARY.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-08-PLAN.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-08-SUMMARY.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-09-PLAN.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-09-SUMMARY.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-10-PLAN.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-10-SUMMARY.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-11-PLAN.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-11-SUMMARY.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-12-PLAN.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-12-SUMMARY.md"
|
||
- "internal/build/artifact.go"
|
||
- "internal/build/build.go"
|
||
- "internal/build/build_test.go"
|
||
- "internal/build/stubs/artifacts.tmpl"
|
||
- "modules/bouncer/audience_test.go"
|
||
- "modules/bouncer/backend_guard_test.go"
|
||
- "modules/bouncer/context.go"
|
||
- "modules/bouncer/jwt.go"
|
||
- "modules/bouncer/mint.go"
|
||
- "modules/bouncer/refresh.go"
|
||
- "modules/cabana/admin_openapi.go"
|
||
- "modules/cabana/auth.go"
|
||
- "modules/cabana/auth_test.go"
|
||
- "modules/cabana/bulk_test.go"
|
||
- "modules/cabana/commands.go"
|
||
- "modules/cabana/commands_test.go"
|
||
- "modules/cabana/contracts.go"
|
||
- "modules/cabana/crud.go"
|
||
- "modules/cabana/crud_lifecycle_test.go"
|
||
- "modules/cabana/crud_test.go"
|
||
- "modules/cabana/filter_schema.go"
|
||
- "modules/cabana/form_schema.go"
|
||
- "modules/cabana/form_schema_test.go"
|
||
- "modules/cabana/http.go"
|
||
- "modules/cabana/list_schema.go"
|
||
- "modules/cabana/list_schema_test.go"
|
||
- "modules/cabana/metadata_settings_test.go"
|
||
- "modules/cabana/navigation.go"
|
||
- "modules/cabana/phase09_contract_test.go"
|
||
- "modules/cabana/query.go"
|
||
- "modules/cabana/query_test.go"
|
||
- "modules/cabana/registry.go"
|
||
- "modules/cabana/relation.go"
|
||
- "modules/cabana/relation_test.go"
|
||
- "modules/cabana/schema.go"
|
||
- "modules/cabana/schema_types.go"
|
||
- "modules/cabana/security_coverage_test.go"
|
||
- "modules/cabana/security_test.go"
|
||
- "modules/cabana/settings.go"
|
||
- "modules/cabana/testdata/list/all_columns.yaml"
|
||
- "modules/cabana/testdata/list/all_filters.yaml"
|
||
- "modules/lagoon/backend_admin_migrations.go"
|
||
- "modules/lagoon/backend_admin_migrations_test.go"
|
||
- "modules/lagoon/fill.go"
|
||
- "modules/lagoon/fill_test.go"
|
||
- "modules/lagoon/migrations.go"
|
||
- "modules/pact/capabilities.go"
|
||
- "modules/phrasebook/translator.go"
|
||
- "modules/surf/router.go"
|
||
- "scripts/check-phase9.sh"
|
||
covered_digest: "v2:sha256:642c5cedcadd932448c7a273b70474c751f6d9bf84030c331b63b101427d813d"
|
||
covered_files_note: "Paths are current root-relative paths after Phase 10.2 (commit 5e50b16) moved the framework packages under modules/. fonoteka.go files are outside the project root and cannot be fingerprinted; they are listed under Required Artifacts and were checked at fonoteka.go HEAD 2da9482 (clean working tree). summercms.go was checked at HEAD 6a2ee9d."
|
||
behavior_unverified: 0
|
||
overrides_applied: 0
|
||
mvp_mode_note: "ROADMAP marks Phase 9 mode: mvp, but the ROADMAP goal is not a User Story. Every Phase 9 PLAN carries a valid User Story, used for User Flow Coverage. As in the Phase 1/3/5/8/10 reports, the five ROADMAP success criteria are the contract."
|
||
re_verification:
|
||
previous_status: human_needed
|
||
previous_score: 5/5
|
||
reason: "Previous report (2026-09-28T00:10Z) went stale: Phase 10.2 moved 42 covered framework files under modules/, so its covered_files no longer existed."
|
||
gaps_closed: []
|
||
gaps_remaining: []
|
||
regressions: []
|
||
human_items_resolved:
|
||
- "CR-01 (numeric writes / 500 instead of 422): fixed in code after Phase 9 by Phase 10.1 commits c3efbc3 and e60e697; TestCRUDFillTypeIsValidation, TestFillJSONNumber and TestFillTypeErrorNamesTheKey pass. The ledger entry in 09-REVIEW-DISPOSITION.md is still `open` and is folded into human item 1."
|
||
human_verification:
|
||
- test: "Triage the open code-review findings in 09-REVIEW-DISPOSITION.md (all 32 still `open`). Record CR-01 as fixed (Phase 10.1, c3efbc3 + e60e697), then decide the 19 warnings, in particular the four that touch AUTH-08 semantics: WR-01 (wildcard requirements never match, ALL instead of Winter's ANY), WR-02 (denied navigation parent emitted with its albums target), WR-17 (user-level backend_users.permissions ignored, so Winter denies are lost at cutover), WR-14 (logout cannot revoke an expired-but-refreshable token)"
|
||
expected: "Each finding set to fixed, deferred (with reason) or skipped; none left `open`. The WR-01/WR-02/WR-17 code paths are unchanged at HEAD (modules/cabana/contracts.go Allows, navigation.go Metadata, auth.go FindByID)."
|
||
why_human: "None defeats a success criterion for the current Płytarium configuration (controllers declare single exact codes; the navigation leak matches Winter's own wildcard behavior), but D-01/D-03 promise Winter permission semantics at cutover, and that is a policy call."
|
||
- test: "Review the 24 judgment-tier prohibitions (verdicts in the Prohibitions table)"
|
||
expected: "Accept or reject the verifier's non-authoritative verdicts. Three need attention: 09-03 #1 (form compiler must not accept `type: partial`), which Phase 10.1 D-09 deliberately superseded with a bare-name, sanitized html/template partial; 09-11 #1 (navigation must not reveal the target of an inaccessible entry; WR-02 shows the parent's albums target to a genres-only admin); and 09-12 #1 (zero-test detection is per invocation, not per package; WR-18)."
|
||
why_human: "Judgment-tier prohibitions need human resolution"
|
||
---
|
||
|
||
# Phase 9: Backend admin authentication and schema pipeline. Verification Report
|
||
|
||
**Phase Goal:** Backend admin users with roles are separate from frontend users and gate navigation and controller access; `fields.yaml`/`columns.yaml` drive a JSON form/list schema, including a first-class relation-manager schema replacing the one `partial` field.
|
||
**Verified:** 2026-10-01T18:35:29Z (summercms.go HEAD 6a2ee9d, fonoteka.go HEAD 2da9482)
|
||
**Status:** human_needed
|
||
**Re-verification:** Yes. The 2026-09-28T00:10Z report went stale after Phase 10.2 (commit 5e50b16) moved every framework package under `modules/`. Every truth was re-checked against the code under `modules/` at HEAD, including later changes from Phases 10.1, 11 and the quick tasks. The covered-file list was rebuilt from the Phase 9 commits at current paths, not copied.
|
||
|
||
**MVP note:** ROADMAP marks this phase `mode: mvp`, but its goal is not a User Story. The PLAN files carry a valid one, which is used below. The five ROADMAP success criteria are the contract, and the plan `must_haves` are supporting evidence.
|
||
|
||
**Changes since the previous report that bear on Phase 9:**
|
||
|
||
- `5e50b16` (10.2-01): `bouncer`, `cabana`, `lagoon`, `pact`, `phrasebook`, `surf` moved to `modules/...`. Pure relocation; `57e7b56` retargeted test fixtures.
|
||
- `c3efbc3` (10.1-03) and `e60e697` (10.1 CR-01): `lagoon.Fill` converts `json.Number` into integer, unsigned and float fields and returns `*lagoon.FillTypeError`; the cabana save path maps that to a per-field 422. **This resolves the Phase 9 CR-01 defect.**
|
||
- `771d2cc`, `9df9fae` (10.1-01/02), Phase 10.1 D-09: form `type: partial` is accepted again, but only with a bare-name `path` rendered through an allowlisted html/template node renderer. The legacy Winter partial (no path) still fails boot. See truth 4 and the 09-03 prohibition.
|
||
- `f7b6b0c` (11-08): cabana writes made commit-safe (`crud.go`, `relation.go`); `037dc53`: per-query collation in lagoon. Neither changes a Phase 9 contract; all Phase 9 tests still pass.
|
||
|
||
## User Flow Coverage
|
||
|
||
User story (from every 09-*-PLAN.md): *As a backend administrator, I want to authenticate separately and manage resources described by Winter-shaped schemas, so that the administration surface stays permission-gated and reusable without coupling it to frontend users.*
|
||
|
||
| Step | Expected | Evidence | Status |
|
||
|---|---|---|---|
|
||
| Provision an admin | `summer admin:create` creates a bcrypt admin with a role; no boot or web seed | `modules/cabana/commands.go`, `RuntimeCommands` in the generated `main.go`; `TestAdminCreateCommand`, `TestAdminResetPasswordCommand` re-run: PASS | VERIFIED |
|
||
| Log in separately | Backend login by login or email returns a `backend`-audience JWT signed with `admin.jwt.secret`; frontend credentials fail | `modules/cabana/auth.go`, `bouncer.NewBackendJWTGuard`; `TestAdminAuthLifecycle`, `TestAdminTracerGenreList` (frontend user with the same email rejected), `TestPhase09GuardIsolation` re-run: PASS | VERIFIED |
|
||
| See permitted navigation | `/navigation` lists only permitted items | `modules/cabana/navigation.go` `Metadata` (line 36); `TestAdminMetadataFiltering` re-run: PASS | VERIFIED (WR-02 caveat) |
|
||
| Open a controller | 403 without the controller permission, before any schema or SQL work | `modules/cabana/http.go` `protect` (line 742): controller lookup, backend principal, `Allows`, then work; `TestPhase09PermissionMatrix`, `TestAdminTracerPermissionBoundary`, `TestPhase09SecurityRoutes` re-run: PASS | VERIFIED |
|
||
| Work with schema-driven lists and forms | Form/list/relation schemas compiled from Winter YAML; CRUD, bulk delete, relation manager, settings | Sections below; `TestPhase09AssembledAcceptance` re-run: PASS on real PostgreSQL | VERIFIED |
|
||
| Outcome: permission-gated, reusable, decoupled | Framework has no app names; admin identity never touches frontend users | `grep -niE "fonoteka|collection_editors|granted_by|golem15_"` on non-test `modules/cabana/*.go`: no hits. `BackendUsers.FindByID` reads only `backend_users` | VERIFIED |
|
||
|
||
## Goal Achievement
|
||
|
||
### Observable Truths (ROADMAP contract)
|
||
|
||
| # | Truth | Status | Evidence |
|
||
|---|---|---|---|
|
||
| 1 | A backend admin user with a role logs in separately from frontend users, and navigation/controller access is gated by the permissions registry. | ✓ VERIFIED | Separate Winter-shaped `backend_users`/`backend_user_roles` tables (`modules/lagoon/backend_admin_migrations.go`; `TestBackendAdminMigration`, `Seed`, `Rollback`, `WinterRow` re-run: PASS). Separate `backend` guard with its own secret and a required `backend` audience; cross-audience tokens fail both ways (`TestPhase09GuardIsolation`, `TestAdminTracerAuthBoundary`: PASS). Grants come from the role JSON plus `HasPermissions` role assignments (`modules/cabana/auth.go` `FindByID`/`principalFrom`). Every controller, relation and CRUD route goes through `protect` (`modules/cabana/http.go:742`); settings through `protectSetting` (line 375). `/navigation` and `/settings` filter entries with the same `Allows` (`contracts.go:127`). Tests re-run: `TestPhase09PermissionMatrix`, `TestPhase09SecurityMatrix`, `TestAdminMetadataFiltering`, `TestAdminMetadataRejectsFrontendPrincipal`: PASS. **Caveats (warnings, unchanged at HEAD):** WR-01 (`granted` matches grant wildcards only; a wildcard *requirement* never matches, and `Allows` requires ALL codes where Winter uses ANY), WR-02 (`Metadata` keeps a denied parent when a child is allowed and emits it with its own target), WR-17 (user-level `backend_users.permissions` ignored). |
|
||
| 2 | `fields.yaml` for a real controller parses (goccy/go-yaml) into a JSON form schema covering text, textarea, checkbox, switch, dropdown (model-method options) and relation (nameFrom, emptyOption), with span/tabs/context/attributes layout hints. | ✓ VERIFIED | `modules/cabana/form_schema.go` imports `github.com/goccy/go-yaml` (+ `ast`), decodes with `yaml.DisallowUnknownField()` (line 286), walks the AST to keep order, rejects unknown keys and types. The real Fonoteka YAML covers every listed item (text, textarea, checkbox `is_various`, switch in settings, dropdown `options: getFormatOptions`, relation with `nameFrom`/`emptyOption`, span, tab, context, attributes). Tests re-run: `TestFormSchemaCompile`, `TestFormSchemaRejects`, `TestAlbumsAdminForm`, `TestAlbumsAdminDropdowns`, `TestStylesAdminForm`, `TestCollectionsAdminForm`: PASS. |
|
||
| 3 | `columns.yaml` parses into a JSON list schema with searchable/sortable/relation columns and datetime/switch renderers. | ✓ VERIFIED | `modules/cabana/list_schema.go:23` column types `text`, `datetime`, `switch`; `ListColumn` carries `searchable`, `sortable`, `relation`, `select` (`schema_types.go:20-24`). Real YAML exercises each. Search/sort/filter resolve only through compiled allowlists with a primary-key tie-break. Tests re-run: `TestListSchemaCompile`, `TestAlbumsAdminList`, `TestArtistsAdminList`, `TestCollectionsAdminListCRUD`, `TestGenresAdminEdges`: PASS. Caveat: WR-08 (`LOWER(col)` at `query.go:294` on a non-text searchable column; the scaffold still emits `searchable: true` at `artifacts.tmpl:137`). |
|
||
| 4 | The relation-manager schema supports search/link/unlink/manage-or-view lists for Collections' editors tab, replacing the `partial` field entirely. | ✓ VERIFIED | `fonoteka.go/.../models/collection/fields.yaml:19` `editors: type: relation-manager`; no `partial` anywhere in the Fonoteka model YAML. `config_relation.yaml` has view/manage list columns, `toolbarButtons: link\|unlink`, `showSearch`. `modules/cabana/relation.go` serves schema, linked, candidates (`RelationExtendManageQuery` at line 494), link and unlink (`RelationBeforeLink` at line 684), with explicit pivot writes and no hardcoded pivot names. Tests re-run on PostgreSQL: `TestCollectionsAdminRelation*`, `TestCollectionsAdminRejectsPartial` (the legacy path-less Winter editors partial still fails with "type partial needs a path"), `TestRelationCandidateExclusions`: PASS. **Note:** Phase 10.1 D-09 lifted Phase 9's blanket boot error for `type: partial` in favor of a bare-name, sanitized html/template partial (`form_schema.go:504-524`). The Collections editors tab is still a relation manager, so the criterion holds; the plan-level prohibition is flagged in the Prohibitions table. Caveats: WR-05, WR-07, WR-15. |
|
||
| 5 | Admin CRUD endpoints expose listExtendQuery/formExtendQuery/formBeforeCreate/formBeforeUpdate/relationExtendManageQuery hooks, bulk delete runs each record's lifecycle hooks, and the Settings model binds to a settings screen through the same schema pipeline. | ✓ VERIFIED | Hook interfaces in `modules/pact/capabilities.go:331-394`, type-asserted in `modules/cabana/query.go:109`, `crud.go:442,531,578,594`, `relation.go:494`. `CRUDService.BulkDelete` (`crud.go:186`) locks the scoped rows in one transaction, refuses a partial selection, and calls `deleteRecord` per row, so GORM and Form*Delete hooks fire per record. `TestBulkDeleteDuplicates`, `Idempotent`, `Rollback`, `TestCRUDHooks` re-run: PASS. Settings use the same `Localize`, writable projection, `lagoon.Fill` and `lagoon.Validate` (`modules/cabana/settings.go`); `TestAdminSettings*` re-run on PostgreSQL: PASS. **CR-01 is now fixed:** `lagoon.convertValue` handles `json.Number` (`fill.go:179-221`), and `save` maps `*lagoon.FillTypeError` to a 422 on the field (`crud.go:324-333`); `TestCRUDFillTypeIsValidation` writes a `type: number` field into an `*int` column, rejects fraction/exponent/overflow/wrong-type with 422 and no row: PASS. |
|
||
|
||
**Score:** 5/5 ROADMAP truths verified (0 present-but-behavior-unverified).
|
||
|
||
### Plan must-have truths (supporting evidence)
|
||
|
||
There are 54 plan truths across 09-01 to 09-12, and each maps to a named test in its plan's `<verify>` block. I re-ran every named Fonoteka suite with `-v`: 78 top-level PASS, 0 SKIP, 0 FAIL (36 in `TestPhase09*|TestAdminSettings*|TestAdminMetadata*|TestCollectionsAdmin*`, 42 in `TestAlbumsAdmin*|TestArtistsAdmin*|TestGenresAdmin*|TestStylesAdmin*|TestAdminTracer*|TestAdminAuthLifecycleAssembled`). The 42 equals every function with those prefixes in the package, including `TestAlbumsAdminSearchUsesCommittedArtists` added in 11-08; no Phase 9 test was removed since 2026-09-27 (checked with `git log -p`). The previous report's "79" was a miscount by one.
|
||
|
||
Truth 09-12 #4 names `../fonoteka.go/docs/openapi.json` as the admin OpenAPI home. Phase 10 D-15 moved the admin paths to the framework-owned `summercms.go/admin/openapi/admin.json`. The intent still holds: `scripts/check-admin-openapi.sh --check` and `scripts/check-phase9.sh --openapi` exit 0.
|
||
|
||
Backstop (non-inferable) truths:
|
||
|
||
| Truth | Evidence | Status |
|
||
|---|---|---|
|
||
| 09-01: login accepts login or normalized email, one opaque failure, never falls back to a frontend user | `TestAdminAuthLifecycle`, `TestAdminTracerGenreList` (frontend user with the admin's email; frontend password rejected). Re-run: PASS | VERIFIED (WR-11 caveat) |
|
||
| 09-11: missing settings GET is side-effect-free with `exists: false`; first PUT creates; identical PUT is idempotent | `TestAdminSettingsMissingRead`, `TestAdminSettingsCreate`, `TestAdminSettingsIdempotentUpdate` re-run: PASS | VERIFIED |
|
||
| 09-12: AUTH-08 is the separate D-01/D-02 principal only; no shared role table or dual-purpose token | Separate tables, `Principal.Backend` flag, audience checks; `TestPhase09GuardIsolation`, `TestAdminMetadataRejectsFrontendPrincipal`: PASS | VERIFIED |
|
||
|
||
### Prohibitions (judgment tier, non-authoritative verdicts)
|
||
|
||
| Plan | Prohibition | Verdict |
|
||
|---|---|---|
|
||
| 01 | Backend identities must not share frontend user rows, the jwt guard, or a signing secret | not violated |
|
||
| 01 | Hidden navigation must not replace server-side authorization | not violated (`protect`/`protectSetting` on every route) |
|
||
| 01 | Tracer must not be mock-only | not violated (testcontainers PostgreSQL, assembled router) |
|
||
| 02 | No boot, web-wizard or env-seeded first admin | not violated (migration seeds roles only; `admin:create` only) |
|
||
| 02 | No cookie session store and no merge with the frontend user model | not violated (Phase 10 carries the same JWT in a cookie; no server-side session store) |
|
||
| 02 | Auth logs carry no password, JWT, secret or hash | not violated (`TestAdminAuthLogging`, `TestPhase09SecurityCoverage`) |
|
||
| 03 | Form compiler must not accept `type: partial` | **superseded**: Phase 10.1 D-09 lifted this for a supported partial contract (bare-name `path`, html/template, allowlisted node renderer, D-18 sanitizer). The legacy Winter path-less partial is still refused (`TestCollectionsAdminRejectsPartial`). A recorded later decision, not a regression, but the Phase 9 prohibition text no longer holds literally. |
|
||
| 03 | Labels not deferred to the client or cached in the first request's locale | not violated (per-request `Localize`) |
|
||
| 03 | Unknown keys, types or providers not silently ignored | not violated (`DisallowUnknownField`, `formFieldKeys`) |
|
||
| 04 | YAML must not inject SQL or name an arbitrary method | not violated (`conditions:` rejected, finite `FilterScopes`) |
|
||
| 04 | Equal sort values must not make rows jump across pages | not violated (PK tie-break) |
|
||
| 05 | No partially committed bulk operation | not violated (`TestBulkDeleteRollback`) |
|
||
| 05 | Replay must not rerun destructive hooks | not violated (`TestBulkDeleteIdempotent`) |
|
||
| 06 | No cross-collection or silently chosen duplicate user on albums | not violated (`TestAlbumsAdminAmbiguousEmail`, `CrossCollection`) |
|
||
| 06 | Album form choices must not expose inactive or cross-collection users | not violated |
|
||
| 07 | Artist parity not reached by silently omitting Winter keys | not violated (`TestPhase10Controllers` asserts fields and columns equal the tracked YAML) |
|
||
| 08 | No second Genre identity and no weakened permission | not violated (`TestGenresAdminTracerIdentity`, `DuplicateRegistration`) |
|
||
| 09 | Style values not coerced between scalar types | not violated (`TestStylesAdminTypedOptions`) |
|
||
| 10 | No PHP partial and no hardcoded Fonoteka pivot names in the framework | not violated (non-test `modules/cabana/*.go` grep: no hits; 10.1 partials are html/template, not PHP) |
|
||
| 10 | Owner, cross-collection or already-linked candidates not linkable by forgery | not violated (`ForgedPivot`, `CrossScope`, `Idempotent`) |
|
||
| 11 | Navigation/settings metadata must not reveal existence, label or target of inaccessible entries | **qualified**: a genres-only admin receives the parent `fonoteka` entry with its albums target (WR-02). Winter behaves the same way. |
|
||
| 11 | Reading a missing singleton must not create a row | not violated (`TestAdminSettingsMissingRead`) |
|
||
| 12 | Acceptance must not depend on skipped PostgreSQL tests or zero-test matches | **qualified**: `check-phase9.sh --self-test` refuses skips and zero-test runs (re-run: PASS), but detection is per invocation, not per package (WR-18). |
|
||
| 12 | High threats marked mitigated only with a named failing-when-broken test | not violated (`09-SECURITY-REVIEW.md` names a command per threat) |
|
||
|
||
### Required Artifacts
|
||
|
||
| Artifact | Expected | Status | Details |
|
||
|---|---|---|---|
|
||
| `modules/lagoon/backend_admin_migrations.go` | backend_users/roles, system-role seed | ✓ VERIFIED | Explicit SQL up/down; `TestBackendAdmin*` PASS |
|
||
| `modules/cabana/auth.go` | backend provider, login/refresh/logout/me | ✓ VERIFIED | Wired from `Activate`; `BackendUsers` reads only `backend_users` |
|
||
| `modules/cabana/commands.go` | admin:create, admin:reset-password | ✓ VERIFIED | `RuntimeCommands` appended in the generated main (`internal/build`) |
|
||
| `modules/cabana/http.go` | route mounting, `protect` | ✓ VERIFIED | Mounted from `modules/surf/router.go:522` via `cabana.Activate` |
|
||
| `modules/cabana/form_schema.go`, `schema_types.go` | strict typed form compiler | ✓ VERIFIED | goccy/go-yaml strict decode |
|
||
| `modules/cabana/list_schema.go`, `filter_schema.go`, `query.go` | list compiler and allowlisted query | ✓ VERIFIED | |
|
||
| `modules/cabana/crud.go` | CRUD, projection, hooks, bulk delete | ✓ VERIFIED | CR-01 fixed (Phase 10.1) |
|
||
| `modules/lagoon/fill.go` | allowlisted fill used by CRUD and settings | ✓ VERIFIED | `json.Number` conversion, `FillTypeError` |
|
||
| `modules/cabana/relation.go` | relation schema and link/unlink | ✓ VERIFIED | |
|
||
| `modules/cabana/navigation.go`, `settings.go` | filtered metadata, singleton settings | ✓ VERIFIED (WR-02) | |
|
||
| `scripts/check-phase9.sh` | fail-closed gate | ✓ VERIFIED | `--self-test`, `--openapi` re-run: exit 0 |
|
||
| `fonoteka.go/.../controllers/{albums,artists,collections,genres,styles}_admin_controller.go` | five controllers with permissions | ✓ VERIFIED | Registered through `HasAdminControllers` |
|
||
| `fonoteka.go/.../models/*/fields.yaml`, `columns.yaml`, `controllers/*/config_*.yaml` | Winter-shaped YAML | ✓ VERIFIED | `partial` replaced by `relation-manager` |
|
||
| `fonoteka.go/.../admin_permissions.go`, `admin_navigation.go`, `admin_settings.go` | registerPermissions/Navigation/Settings ports | ✓ VERIFIED | |
|
||
| `fonoteka.go/.../classes/backend_album_collection.go` | D-14 email-to-collection resolver | ✓ VERIFIED | `TestAlbumsAdminCollectionMatch` PASS |
|
||
| `fonoteka.go/.../admin_phase09_e2e_test.go` | assembled acceptance | ✓ VERIFIED | `TestPhase09AssembledAcceptance` PASS |
|
||
|
||
### Key Link Verification
|
||
|
||
| From | To | Via | Status |
|
||
|---|---|---|---|
|
||
| `modules/surf/router.go` | `modules/cabana/http.go` | `cabana.Activate(app, plugins)` at line 522, then `RegisterMiddleware("summercms.cabana", "backend", ...)` | WIRED |
|
||
| `modules/cabana/http.go` guard | `modules/bouncer/jwt.go` | `NewBackendJWTGuard` with backend audience | WIRED |
|
||
| `modules/cabana/http.go` handlers | compiled schemas | `s.reg.Get(id)` then list/form/relation | WIRED |
|
||
| `modules/cabana/crud.go` | `lagoon.Fill`/`lagoon.Validate` | `save` transaction (line 288; Fill at 324) | WIRED (CR-01 fixed) |
|
||
| `modules/cabana/crud.go` bulk | model lifecycle hooks | per-row `deleteRecord` inside one transaction | WIRED |
|
||
| `modules/cabana/settings.go` | form pipeline | `Localize`, `Fill` (line 149), `Validate` | WIRED |
|
||
| `models/collection/fields.yaml` | `config_relation.yaml` | `relation: editors` compiled at activation | WIRED |
|
||
| `internal/build/build.go` | `cabana.RuntimeCommands` | generated main | WIRED |
|
||
|
||
### Data-Flow Trace (Level 4)
|
||
|
||
| Artifact | Data | Source | Real data | Status |
|
||
|---|---|---|---|---|
|
||
| List endpoint | `data` rows | GORM query on the registered model, scoped by `ListExtendQuery` | Yes (PostgreSQL rows in assembled tests) | ✓ FLOWING |
|
||
| Navigation | entries | plugin `Navigation()` filtered by principal grants from `backend_user_roles` | Yes | ✓ FLOWING |
|
||
| Settings GET | `data` | `golem15_fonoteka_settings` row or compiled defaults | Yes | ✓ FLOWING |
|
||
| Relation linked/candidates | rows | pivot-joined user query with owner and linked users excluded | Yes | ✓ FLOWING |
|
||
|
||
### Behavioral Spot-Checks
|
||
|
||
| Behavior | Command | Result | Status |
|
||
|---|---|---|---|
|
||
| Framework vet | `go vet ./...` (summercms.go) | exit 0, no output | ✓ PASS |
|
||
| Framework regression (single full run) | `go test ./... -count=1` (summercms.go) | exit 0; 35 packages ok (plus 4 with no test files), including `modules/cabana` 28.7s, `modules/bouncer` 15.8s, `modules/lagoon` 23.5s, `modules/pact`, `modules/phrasebook`, `modules/surf`, `internal/build` 33.1s; 0 FAIL | ✓ PASS |
|
||
| Phase 9 framework tests, named | `go test ./modules/cabana -v -run '^(TestBulkDelete(Duplicates\|Idempotent\|Rollback)\|TestCRUDHooks\|TestCRUDFillTypeIsValidation\|TestFormSchema(Compile\|Rejects)\|TestListSchemaCompile\|TestRelationCandidateExclusions\|TestPhase09PermissionMatrix\|TestAdminAuthLifecycle\|TestAdminCreateCommand\|TestAdminResetPasswordCommand)$'` | 13 PASS | ✓ PASS |
|
||
| Guard isolation | `go test ./modules/bouncer -run '^TestPhase09GuardIsolation$' -v` | PASS | ✓ PASS |
|
||
| Admin tables and numeric fill | `go test ./modules/lagoon -v -run '^(TestFillJSONNumber\|TestFillTypeErrorNamesTheKey\|TestBackendAdmin.*)$'` | 6 PASS | ✓ PASS |
|
||
| App vet | `go vet $(go list -f '{{.Dir}}/...' -m)` (fonoteka.go) | exit 0 | ✓ PASS |
|
||
| Assembled Phase 9 acceptance and controllers | `go test ./plugins/golem15/fonoteka -count=1 -v -run '^(TestPhase09.*\|TestAdminSettings.*\|TestAdminMetadata.*\|TestCollectionsAdmin.*\|TestAlbumsAdmin.*\|TestGenresAdmin.*\|TestArtistsAdmin.*\|TestStylesAdmin.*\|TestAdminTracer.*\|TestAdminAuthLifecycleAssembled)$'` (fonoteka.go) | ok 17.7s; 78 PASS, 0 SKIP, 0 FAIL | ✓ PASS |
|
||
|
||
### Probe Execution
|
||
|
||
No `scripts/*/tests/probe-*.sh` exists and no plan declares a probe. The phase gate ran instead:
|
||
|
||
| Probe | Command | Result | Status |
|
||
|---|---|---|---|
|
||
| `scripts/check-phase9.sh` | `--self-test` | "refuse: zero tests", "refuse: failed TestPhase09ContractInventory", "phase9 self-test passed", exit 0 | PASS |
|
||
| `scripts/check-phase9.sh` | `--openapi` | "phase9 openapi passed", exit 0 | PASS |
|
||
| `scripts/check-admin-openapi.sh` | `--check` | exit 0 | PASS |
|
||
|
||
### Requirements Coverage
|
||
|
||
| Requirement | Source Plans | Description | Status | Evidence |
|
||
|---|---|---|---|---|
|
||
| AUTH-08 | 09-01, 09-02, 09-11, 09-12 | Backend admins with roles and a permission registry, separate from frontend users, gating navigation and controllers | ✓ SATISFIED | Truth 1 (warnings WR-01, WR-02, WR-14, WR-17) |
|
||
| ADMIN-01 | 09-03, 09-06..09-10, 09-12 | fields.yaml to JSON form schema | ✓ SATISFIED | Truth 2 |
|
||
| ADMIN-02 | 09-01, 09-04, 09-06..09-10, 09-12 | columns.yaml to JSON list schema | ✓ SATISFIED | Truth 3 |
|
||
| ADMIN-03 | 09-10, 09-12 | relation-manager replaces `partial` | ✓ SATISFIED | Truth 4 |
|
||
| ADMIN-04 | 09-05..09-10, 09-12 | CRUD hooks and per-record bulk delete | ✓ SATISFIED | Truth 5 (CR-01 fixed in Phase 10.1) |
|
||
| ADMIN-05 | 09-11, 09-12 | settings model bound through the same pipeline | ✓ SATISFIED | Truth 5 |
|
||
|
||
REQUIREMENTS.md maps exactly these six IDs to Phase 9, so there are no orphaned requirements.
|
||
|
||
### Anti-Patterns Found
|
||
|
||
| File | Line | Pattern | Severity | Impact |
|
||
|---|---|---|---|---|
|
||
| `modules/cabana/contracts.go` | 127-150 | wildcard requirement unmatched; ALL not ANY | ⚠️ Warning (WR-01) | Latent for future Winter ports |
|
||
| `modules/cabana/navigation.go` | 46-57 | denied parent emitted with its target | ⚠️ Warning (WR-02) | Minor metadata disclosure |
|
||
| `modules/cabana/auth.go` | 36-73 | user-level permissions ignored | ⚠️ Warning (WR-17) | Winter denies lost at cutover |
|
||
| `modules/cabana/query.go`, `internal/build/stubs/artifacts.tmpl` | 294, 137 | `LOWER(col)` on non-text columns; scaffold makes `id` searchable | ⚠️ Warning (WR-08) | Scaffolded controllers 500 on search |
|
||
| `internal/build/stubs/artifacts.tmpl` | scaffold controller | no permissions or record source | ⚠️ Warning (WR-09) | Open to all admins once completed naively |
|
||
| `modules/cabana/settings.go` | 149 | no test writes a numeric settings field | ℹ️ Info | CR-01's fix is in `lagoon.Fill`, shared by settings; Fill failures there already answer 422 |
|
||
| `.planning/.../09-REVIEW-DISPOSITION.md` | ledger | CR-01 still `open` although fixed | ℹ️ Info | Ledger out of date; see human item 1 |
|
||
|
||
The other open review warnings (WR-03 to WR-07, WR-10 to WR-16, WR-18, WR-19) and the 12 info findings are recorded in 09-REVIEW.md. None defeats a ROADMAP success criterion. No `TBD`/`FIXME`/`XXX` in any covered implementation file (grep: no hits).
|
||
|
||
### Human Verification Required
|
||
|
||
#### 1. Triage the open review findings, starting with the AUTH-08 ones
|
||
|
||
**Test:** Set dispositions in 09-REVIEW-DISPOSITION.md, where all 32 findings are still `open`. Record CR-01 as fixed (Phase 10.1, `c3efbc3` + `e60e697`, covered by `TestCRUDFillTypeIsValidation`).
|
||
**Expected:** WR-01, WR-02, WR-14 and WR-17 explicitly fixed or deferred with a reason. Their code is unchanged at HEAD. D-01/D-03 promise Winter permission semantics and a straight `backend_users` copy at cutover (Phase 15).
|
||
**Why human:** This is a policy and scope decision.
|
||
|
||
#### 2. Review the 24 judgment-tier prohibitions
|
||
|
||
**Test:** Accept or reject the verdicts in the Prohibitions table.
|
||
**Expected:** Pay attention to the superseded 09-03 `type: partial` prohibition (lifted by Phase 10.1 D-09) and the two qualified verdicts (09-11 navigation target, 09-12 per-invocation zero-test detection).
|
||
**Why human:** Judgment-tier prohibitions need human resolution.
|
||
|
||
The previous item "Decide CR-01" is resolved by code and test evidence and is no longer a separate item. 09-UAT.md still lists it as pending.
|
||
|
||
### Gaps Summary
|
||
|
||
No ROADMAP success criterion failed, so there are no gaps. Separate admin authentication, permission gating of controllers, settings and navigation, the strict goccy/go-yaml form and list pipeline, the relation manager that replaces the Collections editors `partial`, the CRUD hooks, per-record bulk delete and the settings binding all exist under `modules/`, are wired, and pass their tests on real PostgreSQL at HEAD in both repositories. Since the last report the critical review finding CR-01 was fixed in Phase 10.1.
|
||
|
||
The phase is still not `passed` because two human decisions are open. All review findings are untriaged in the ledger, and four warnings bear on the Winter permission semantics that AUTH-08 and D-03 promise. And 24 judgment-tier prohibitions need sign-off, one of which a later phase deliberately superseded. Neither blocks Płytarium today.
|
||
|
||
---
|
||
|
||
_Verified: 2026-10-01T18:35:29Z_
|
||
_Verifier: Claude (gsd-verifier)_
|