docs(09): re-verify phase after code review fixes

This commit is contained in:
Jakub Zych
2026-10-01 21:50:43 +02:00
parent 2ecc85ea19
commit 5d79f7d0bb

View File

@@ -1,6 +1,6 @@
---
phase: 09-backend-admin-authentication-and-schema-pipeline
verified: 2026-10-01T18:35:29Z
verified: 2026-10-01T19:47:49Z
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:
@@ -38,9 +38,14 @@ covered_files:
- "modules/bouncer/jwt.go"
- "modules/bouncer/mint.go"
- "modules/bouncer/refresh.go"
- "modules/bouncer/refresh_test.go"
- "modules/bouncer/registry.go"
- "modules/bouncer/registry_test.go"
- "modules/cabana/admin_openapi.go"
- "modules/cabana/auth.go"
- "modules/cabana/auth_internal_test.go"
- "modules/cabana/auth_test.go"
- "modules/cabana/backend_guard_collision_test.go"
- "modules/cabana/bulk_test.go"
- "modules/cabana/commands.go"
- "modules/cabana/commands_test.go"
@@ -55,12 +60,17 @@ covered_files:
- "modules/cabana/list_schema.go"
- "modules/cabana/list_schema_test.go"
- "modules/cabana/metadata_settings_test.go"
- "modules/cabana/model_fields.go"
- "modules/cabana/model_fields_test.go"
- "modules/cabana/navigation.go"
- "modules/cabana/permissions_test.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_field.go"
- "modules/cabana/relation_field_test.go"
- "modules/cabana/relation_test.go"
- "modules/cabana/schema.go"
- "modules/cabana/schema_types.go"
@@ -69,6 +79,7 @@ covered_files:
- "modules/cabana/settings.go"
- "modules/cabana/testdata/list/all_columns.yaml"
- "modules/cabana/testdata/list/all_filters.yaml"
- "modules/cabana/tx_context.go"
- "modules/lagoon/backend_admin_migrations.go"
- "modules/lagoon/backend_admin_migrations_test.go"
- "modules/lagoon/fill.go"
@@ -78,44 +89,49 @@ covered_files:
- "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."
covered_digest: "v2:sha256:915c2ad92c7250d07cf220cb1e97ef419209530f5838654dfa0b93b361dd3599"
covered_files_note: "Current root-relative paths (framework packages live under modules/ since Phase 10.2 commit 5e50b16). The code-review fix run (1a878b3..2ecc85e) added modules/bouncer/{refresh_test,registry,registry_test}.go and modules/cabana/{auth_internal_test,backend_guard_collision_test,model_fields,model_fields_test,permissions_test,tx_context}.go as Phase 9 evidence; relation_field.go and relation_field_test.go are covered because WR-03/WR-16 changed them. 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 e62f4fc (clean working tree). summercms.go was checked at HEAD 2ecc85e."
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."
reason: "Covered files changed in the Phase 9 code-review fix run (summercms.go 1a878b3..2ecc85e: 19 fix commits plus docs 9d2128a, 2ecc85e; fonoteka.go b925dd6, c45fb99, e62f4fc)."
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."
- "Code-review triage: 09-REVIEW-DISPOSITION.md records CR-01 and WR-01..WR-19 as fixed (12 Info findings stay open, out of scope). The fixes are in the code at HEAD and each has a named test that passes (see Behavioral Spot-Checks). Three decisions from 09-REVIEW-FIX.md remain and are human item 1."
- "Prohibition 09-11 #1 (navigation must not reveal an inaccessible entry's target, WR-02): now not violated. Metadata drops a main item the principal may not open and repoints an allowed parent to its first openable child (TestNavigationDropsDeniedParentAndRepointsTarget PASS)."
- "Prohibition 09-12 #1 (acceptance must not depend on zero-test matches, WR-18): now not violated. phase9_detect judges zero tests per package; --self-test refuses a run in which one package has no passing test ('refuse: zero tests in a/cabana')."
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: "Decide the three open choices recorded under 'Decisions needed' in 09-REVIEW-FIX.md"
expected: "(a) WR-11: whether to add a unique index on lower(backend_users.email), and whether copied Winter rows are deduplicated first (login-time ambiguity is already refused and admin:create already blocks collisions). (b) WR-03: whether single-record delete stays allowed whenever a form exists (as now, matching Winter's form-screen delete button) or must also follow the list toolbar's `delete`, which needs a new form-schema field and an admin API/OpenAPI change. (c) WR-17: whether an exact-code deny in backend_users.permissions should also remove a role's wildcard grant (stricter than Winter's getMergedPermissions, which the current code follows)."
why_human: "Each is a policy or scope choice against the Winter-parity rule, not a defect the verifier can decide. None defeats a ROADMAP success criterion."
- 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)."
expected: "Accept or reject the verifier's non-authoritative verdicts. One still needs 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; the legacy path-less Winter partial is still refused. The previously qualified 09-11 #1 (WR-02) and 09-12 #1 (WR-18) are now not violated."
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)
**Verified:** 2026-10-01T19:47:49Z (summercms.go HEAD 2ecc85e, fonoteka.go HEAD e62f4fc)
**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.
**Re-verification:** Yes. The 2026-10-01T18:35Z report went stale when the Phase 9 code-review fixes changed covered files. Every truth was re-checked at HEAD. The covered-file list was rebuilt at current paths and extended with the files the fixes created.
**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:**
**Changes since the previous report that bear on Phase 9 (verified in code, not taken from 09-REVIEW-FIX.md):**
- `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.
- **Permissions (WR-01, WR-17):** `cabana.Allows` (`contracts.go:130`) now passes when ANY required code is granted, and `granted` (line 147) matches wildcard requirements (`x.*`, `*x`) as Winter's `hasPermission` does. `BackendUsers.FindByID` overlays the user's own `backend_users.permissions` on the role grants (`applyUserPermissions`, `auth.go:77`): `1` grants, `-1`/`0` remove an exact code.
- **Navigation (WR-02):** `Metadata` (`navigation.go:36`) drops a main item the principal may not open, whatever its children allow, and `openableTarget` (line 95) repoints an allowed parent to its first openable child. With Fonoteka's parent requirement `golem15.fonoteka.*`, a genres-only admin should now see the parent linked to `golem15.fonoteka.genres`, not albums (by code reading; the framework test pins this shape, no Fonoteka test asserts it).
- **Writes (WR-03, WR-04, WR-05):** `operationDeclared` (`http.go:806`) refuses create/update/delete/bulk-delete the compiled YAML does not declare (403). Form `required` applies only in contexts that can supply it. `relationMutation` (line 469) refuses link/unlink missing from `view.toolbarButtons`.
- **Auth (WR-10 to WR-14):** foreign `backend` guard fails boot (`bouncer.Registry.Owner`); ambiguous login identifiers answer the same opaque 401 (`findBackendLogin`, `auth.go:431`); dummy hash uses the configured bcrypt cost; `admin:*` read passwords from a prompt or stdin; logout runs outside the guard and revokes an expired-but-refreshable token via `bouncer.VerifyRefreshableClaimsAudience` (`refresh.go:60`).
- **Schema/query (WR-06 to WR-09, WR-16):** relation default sort is the first sortable column; relation column order comes from an ordered decode, not indentation; list search uses `LOWER(CAST(col AS TEXT))` (`query.go:296`); the scaffold declares a required permission and a record source; reflection helpers see embedded structs and explicit `column:` tags (`model_fields.go`).
- **Gate and transactions (WR-18, WR-19):** `check-phase9.sh` judges zero tests per package; hooks and scopes read the write transaction through `cabana.TxFromContext` (`tx_context.go:27`), used by Fonoteka's album and collection hooks.
- **Fonoteka (WR-15):** an admin editor link leaves `granted_by` NULL.
## User Flow Coverage
@@ -123,11 +139,11 @@ User story (from every 09-*-PLAN.md): *As a backend administrator, I want to aut
| 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 |
| Provision an admin | `summer admin:create` creates a bcrypt admin with a role; no boot or web seed | `modules/cabana/commands.go`; `TestAdminCreateCommand`, `TestAdminResetPasswordCommand`, `TestAdminPasswordWithoutFlag`, `TestAdminCreateRejectsCrossFieldCollision`: PASS | VERIFIED |
| Log in separately | Backend login by login or email returns a `backend`-audience JWT; frontend credentials fail; ambiguous identifiers fail opaquely | `modules/cabana/auth.go`, `bouncer.NewBackendJWTGuard`; `TestAdminAuthLifecycle`, `TestLoginAmbiguousIdentifier`, `TestAdminTracerGenreList`, `TestPhase09GuardIsolation`: PASS | VERIFIED |
| See permitted navigation | `/navigation` lists only permitted items and never links to a 403 target | `modules/cabana/navigation.go` `Metadata`/`openableTarget`; `TestNavigationDropsDeniedParentAndRepointsTarget`, `TestAdminMetadataFiltering`: PASS | VERIFIED |
| Open a controller | 403 without the controller permission, before any schema or SQL work | `modules/cabana/http.go` `protect` (line 780), then `operationDeclared` for writes; `TestPhase09PermissionMatrix`, `TestCRUDOperationsFollowDeclarations`, `TestAdminTracerPermissionBoundary`, `TestPhase09SecurityRoutes`: 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`: 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
@@ -136,17 +152,17 @@ User story (from every 09-*-PLAN.md): *As a backend administrator, I want to aut
| # | 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. |
| 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` (`modules/lagoon/backend_admin_migrations.go`; `TestBackendAdmin*` PASS in the full run). Separate `backend` guard with its own secret and required audience; cross-audience tokens fail both ways (`TestPhase09GuardIsolation`, `TestAdminTracerAuthBoundary`: PASS); a foreign guard fails boot (`TestActivateRefusesAForeignBackendGuard`: PASS). Grants = role JSON + `HasPermissions` role assignments + user-level overlay (`auth.go:39-77`; `TestBackendUserPermissionsOverrideRole`: PASS on PostgreSQL). Every controller, relation and CRUD route goes through `protect` (`http.go:780`), settings through `protectSetting` (line 387). Permission matching follows Winter's `hasAnyAccess` (`TestAllowsFollowsWinterHasAnyAccess`, 17 cases: PASS). Navigation and settings filter by the same `Allows`; denied parents are dropped and targets repointed (`TestNavigationDropsDeniedParentAndRepointsTarget`: PASS). Also `TestPhase09PermissionMatrix`, `TestPhase09SecurityMatrix`, `TestAdminMetadataFiltering`, `TestAdminMetadataRejectsFrontendPrincipal`: PASS. The previous WR-01/WR-02/WR-17 caveats are closed in code; WR-17's wildcard-deny nuance is a human decision. |
| 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` decodes with goccy/go-yaml and `yaml.DisallowUnknownField()` (line 286), walks the AST to keep order, rejects unknown keys and types. The real Fonoteka YAML covers every listed item. `required` now honours `context` (`TestCRUDRequiredFollowsContext`: PASS). Tests: `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` column types `text`, `datetime`, `switch`; `ListColumn` carries `searchable`, `sortable`, `relation`, `select` (`schema_types.go:20-24`). Search/sort/filter resolve only through compiled allowlists with a PK tie-break; non-text searchable columns are cast to text (`query.go:296`; `TestListSearchNonTextColumns` on PostgreSQL: PASS). Tests: `TestListSchemaCompile`, `TestAlbumsAdminList`, `TestArtistsAdminList`, `TestCollectionsAdminListCRUD`, `TestGenresAdminEdges`: PASS. WR-08 caveat closed. |
| 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` 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 473), link and unlink (`RelationBeforeLink` at line 671); link/unlink now require the panel's `toolbarButtons` (`TestRelationMutationsFollowToolbarButtons`: PASS); column order is indentation-independent (`TestRelationColumnOrderIsIndependentOfIndentation`: PASS); default sort is the first sortable column (`TestRelationDefaultSort`: PASS). Fonoteka: `TestCollectionsAdminRelation*`, `TestCollectionsAdminRejectsPartial` (legacy path-less partial fails boot), `TestRelationCandidateExclusions`: PASS. **Note:** Phase 10.1 D-09 lifted Phase 9's blanket `type: partial` boot error for a bare-name sanitized html/template partial (`compilePartialPath`, `form_schema.go:508`); the Collections editors tab is still a relation manager. WR-05, WR-07, WR-15 caveats closed. |
| 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:445,534,581,597`, `relation.go:473`. Hooks receive the write transaction through `TxFromContext` (`TestHooksReceiveTheWriteTransaction`: PASS; Fonoteka `TestAlbumsAdminHooksUseTheWriteTransaction` with a one-connection pool: PASS). `CRUDService.BulkDelete` (`crud.go:187`) locks scoped rows in one transaction and deletes per row via `deleteRecord`, so hooks fire per record; it now requires `delete` in `toolbar.buttons`. `TestBulkDeleteDuplicates`, `Idempotent`, `Rollback`, `TestCRUDHooks`: PASS. Settings use the same `Localize`, writable projection, `lagoon.Fill` (`settings.go:149`) and `lagoon.Validate`; `TestAdminSettings*` on PostgreSQL: PASS. CR-01 stays fixed (`TestCRUDFillTypeIsValidation`: 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.
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`: 79 top-level PASS, 0 SKIP, 0 FAIL. That equals the number of test functions with those prefixes in the package (79, now including `TestAlbumsAdminHooksUseTheWriteTransaction` from the WR-19 fix). The 20 tests added by the summercms.go fix commits were run by name and all pass.
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.
@@ -154,68 +170,70 @@ 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 |
| 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), `TestLoginAmbiguousIdentifier` (a cross-field collision now fails opaquely instead of taking the first match), `TestMissingUserHashUsesConfiguredCost`: PASS | VERIFIED (WR-11 caveat closed; index decision open) |
| 09-11: missing settings GET is side-effect-free with `exists: false`; first PUT creates; identical PUT is idempotent | `TestAdminSettingsMissingRead`, `TestAdminSettingsCreate`, `TestAdminSettingsIdempotentUpdate`: 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`, `TestVerifyRefreshableClaimsAudience` (refresh-window verification keeps the audience check): 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 | Backend identities must not share frontend user rows, the jwt guard, or a signing secret | not violated (foreign `backend` guard now fails boot) |
| 01 | Hidden navigation must not replace server-side authorization | not violated (`protect`/`protectSetting`/`operationDeclared` on every route; logout is public by design, CSRF-checked, and only revokes the presented token) |
| 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. |
| 02 | No cookie session store and no merge with the frontend user model | not violated |
| 02 | Auth logs carry no password, JWT, secret or hash | not violated (`TestAdminAuthLogging`, `TestPhase09SecurityCoverage`; the `--password` deprecation warning never echoes the value) |
| 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 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 | YAML must not inject SQL or name an arbitrary method | not violated (`conditions:` rejected, finite `FilterScopes`; search cast is on an allowlisted, quoted column) |
| 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) |
| 07 | Artist parity not reached by silently omitting Winter keys | not violated |
| 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. |
| 10 | No PHP partial and no hardcoded Fonoteka pivot names in the framework | not violated (non-test `modules/cabana/*.go` grep: no hits) |
| 10 | Owner, cross-collection or already-linked candidates not linkable by forgery | not violated (`ForgedPivot`, `CrossScope`, `Idempotent`; link now also requires the declared toolbar button) |
| 11 | Navigation/settings metadata must not reveal existence, label or target of inaccessible entries | **not violated** (was qualified): a denied main item is dropped; an allowed parent never links to a controller the admin cannot open (`TestNavigationDropsDeniedParentAndRepointsTarget` pins the same parent `x.*` + genres-only shape; for Fonoteka this follows from code reading, since no Fonoteka test asserts a genres-only menu) |
| 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) |
| 12 | Acceptance must not depend on skipped PostgreSQL tests or zero-test matches | **not violated** (was qualified): `phase9_detect` refuses any package without a passing test; `--self-test` re-run prints "refuse: zero tests in a/cabana" for the planted case and passes |
| 12 | High threats marked mitigated only with a named failing-when-broken test | not violated (`09-SECURITY-REVIEW.md`; `check-phase9.sh --security` re-run: "phase9 security passed") |
### 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` |
| `modules/lagoon/backend_admin_migrations.go` | backend_users/roles, system-role seed | ✓ VERIFIED | Explicit SQL up/down; no lower(email) index (WR-11 decision open) |
| `modules/cabana/auth.go` | backend provider, login/refresh/logout/me, user-level permission overlay | ✓ VERIFIED | `BackendUsers` reads only `backend_users` |
| `modules/cabana/commands.go` | admin:create, admin:reset-password | ✓ VERIFIED | prompt/stdin password, collision checks |
| `modules/cabana/http.go` | route mounting, `protect`, `operationDeclared`, `relationMutation` | ✓ VERIFIED | Mounted from `modules/surf/router.go:522` via `cabana.Activate` |
| `modules/cabana/form_schema.go`, `schema_types.go` | strict typed form compiler | ✓ VERIFIED | |
| `modules/cabana/list_schema.go`, `filter_schema.go`, `query.go`, `model_fields.go` | list compiler, allowlisted query, column resolution | ✓ VERIFIED | |
| `modules/cabana/crud.go`, `tx_context.go` | CRUD, projection, hooks, bulk delete, tx on context | ✓ VERIFIED | |
| `modules/lagoon/fill.go` | allowlisted fill used by CRUD and settings | ✓ VERIFIED | |
| `modules/cabana/relation.go`, `relation_field.go` | relation schema and link/unlink | ✓ VERIFIED | |
| `modules/cabana/navigation.go`, `settings.go` | filtered metadata, singleton settings | ✓ VERIFIED | |
| `modules/bouncer/refresh.go`, `registry.go` | refresh-window verification for logout; guard ownership | ✓ VERIFIED | |
| `scripts/check-phase9.sh` | fail-closed gate, per-package zero-test check | ✓ VERIFIED | `--self-test`, `--openapi`, `--security`: exit 0 |
| `fonoteka.go/.../controllers/{albums,artists,collections,genres,styles}_admin_controller.go`, `controllers/request_db.go` | five controllers with permissions; hooks read the write tx | ✓ VERIFIED | |
| `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 |
| `fonoteka.go/.../admin_permissions.go`, `admin_navigation.go`, `admin_settings.go` | registerPermissions/Navigation/Settings ports | ✓ VERIFIED | Parent requires `golem15.fonoteka.*` |
| `fonoteka.go/.../admin_phase09_e2e_test.go`, `admin_phase09_security_test.go` | assembled acceptance and route inventory | ✓ VERIFIED | Logout listed as public with reason |
### 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/surf/router.go` | `modules/cabana/http.go` | `cabana.Activate(app, plugins)` at line 522 | WIRED |
| `modules/cabana/http.go` guard | `modules/bouncer/jwt.go` | `NewBackendJWTGuard` with backend audience; ownership via `Registry.Owner` | WIRED |
| `modules/cabana/http.go` logout | `modules/bouncer/refresh.go` | `VerifyRefreshableClaimsAudience` then blacklist jti | WIRED |
| `modules/cabana/http.go` handlers | compiled schemas | `s.reg.Get(id)`, `operationDeclared`, then list/form/relation | WIRED |
| `modules/cabana/crud.go` | `lagoon.Fill`/`lagoon.Validate` | `save` transaction (line 290; Fill at 327, `FillTypeError` to 422) | WIRED |
| `modules/cabana/crud.go` | plugin hooks | `TxFromContext` inside `lagoon.Transaction` | WIRED |
| `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 |
@@ -226,7 +244,7 @@ Backstop (non-inferable) truths:
| 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 |
| Navigation | entries | plugin `Navigation()` filtered by role + user-level grants from `backend_user_roles`/`backend_users` | 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 |
@@ -235,12 +253,13 @@ Backstop (non-inferable) truths:
| 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 |
| Framework regression (single full run) | `go test ./... -count=1` (summercms.go) | exit 0; 35 packages ok (plus packages with no test files), 0 FAIL | ✓ PASS |
| App vet | `go vet $(go list -f '{{.Dir}}/...' -m)` (fonoteka.go, all workspace modules) | exit 0 | ✓ PASS |
| App regression | `go test $(go list -f '{{.Dir}}/...' -m) -count=1` (fonoteka.go, root + user + fonoteka plugin modules) | exit 0; 13 packages ok, 0 FAIL | ✓ PASS |
| Phase 9 framework tests, named | `go test ./modules/cabana -v -run '^(TestAllowsFollowsWinterHasAnyAccess\|TestNavigationDropsDeniedParentAndRepointsTarget\|TestRelationMutationsFollowToolbarButtons\|TestBulkDelete(Duplicates\|Idempotent\|Rollback)\|TestCRUDHooks\|TestCRUDFillTypeIsValidation\|TestFormSchema(Compile\|Rejects)\|TestListSchemaCompile\|TestRelationCandidateExclusions\|TestPhase09PermissionMatrix\|TestAdminAuthLifecycle\|TestAdminCreateCommand\|TestAdminResetPasswordCommand)$'` | 16 PASS | ✓ PASS |
| Review-fix tests, named | `go test ./modules/cabana ./modules/bouncer -v -run '^(TestActivateRefusesAForeignBackendGuard\|TestAdminCreateRejectsCrossFieldCollision\|TestAdminLogoutRevokesExpiredRefreshableToken\|TestAdminPasswordWithoutFlag\|TestBackendUserPermissionsOverrideRole\|TestCRUDOperationsFollowDeclarations\|TestCRUDRequiredFollowsContext\|TestFieldByColumnTagBeatsGoName\|TestHooksReceiveTheWriteTransaction\|TestListSearchNonTextColumns\|TestLoginAmbiguousIdentifier\|TestMissingUserHashUsesConfiguredCost\|TestModelHelpersLookThroughEmbeddedStructs\|TestRegistryOwner\|TestRelationColumnOrderIsIndependentOfIndentation\|TestRelationDefaultSort\|TestVerifyRefreshableClaimsAudience)$'` | 17 PASS (WR-09 scaffold assertions run inside `TestScaffoldAllArtifacts`, full run 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 |
| 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) | exit 0; 79 PASS, 0 SKIP, 0 FAIL | ✓ PASS |
### Probe Execution
@@ -248,19 +267,20 @@ No `scripts/*/tests/probe-*.sh` exists and no plan declares a probe. The phase g
| 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` | `--self-test` | planted cases refused ("skipped TestPhase09MigrationsFreshRollback", "zero tests", "failed TestPhase09ContractInventory", "zero tests in a/cabana"), then "phase9 self-test passed", exit 0 | PASS |
| `scripts/check-phase9.sh` | `--openapi` | "phase9 openapi passed", exit 0 | PASS |
| `scripts/check-phase9.sh` | `--security` | "phase9 security 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) |
| 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 |
| 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-04 | 09-05..09-10, 09-12 | CRUD hooks and per-record bulk delete | ✓ SATISFIED | Truth 5 |
| 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.
@@ -269,39 +289,37 @@ REQUIREMENTS.md maps exactly these six IDs to Phase 9, so there are no orphaned
| 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 |
| `internal/build/stubs/artifacts.tmpl` | 106 | `// TODO: return a pointer to the plugin's model` in the generated controller | ℹ️ Info | Deliberate scaffold placeholder (WR-09); generated text, not a debt marker in phase code. Until filled in, the scaffold answers 500 and boot refuses the undeclared permission |
| `modules/cabana/settings.go` | 149 | no test writes a numeric settings field | ℹ️ Info | The `json.Number` conversion lives in the shared `lagoon.Fill`; settings Fill failures already answer 422 |
| `.planning/.../09-UAT.md` | tests 1-3 | still pending for items this report resolves | ℹ️ Info | UAT file not regenerated by this run |
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).
No `TBD`/`FIXME`/`XXX` in any covered implementation file (grep: no hits). The 12 Info review findings (IN-01..IN-12) remain `open` in the ledger and are out of the fix run's scope; none defeats a success criterion.
### Human Verification Required
#### 1. Triage the open review findings, starting with the AUTH-08 ones
#### 1. Decide the three open choices from 09-REVIEW-FIX.md
**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.
**Test:** Read "Decisions needed" in 09-REVIEW-FIX.md and choose for each.
**Expected:**
- **WR-11:** add a unique index on `lower(backend_users.email)` or not, and whether copied Winter rows are deduplicated first. Ambiguous logins are already refused and `admin:create` already blocks collisions; the index would only add race protection.
- **WR-03:** keep single-record delete allowed whenever a form exists (current behaviour, matching Winter's form-screen delete button), or also require the list toolbar's `delete`. The second option needs a new form-schema field and an admin API and OpenAPI change.
- **WR-17:** keep Winter's exact-code merge (a `-1` on `acme.a` does not remove a role's `acme.*`), or make denies stricter than Winter.
**Why human:** These are policy and parity choices, not defects.
#### 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).
**Expected:** Confirm the superseded 09-03 `type: partial` verdict (lifted by Phase 10.1 D-09). The previously qualified 09-11 (WR-02) and 09-12 (WR-18) verdicts are now "not violated" on code and test evidence.
**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.
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. CR-01 and all 19 review warnings are fixed in code, and each fix has a passing named test.
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.
The phase is still `human_needed` for two reasons. Three policy choices from the fix run (WR-11 index, WR-03 single-delete gating, WR-17 wildcard deny) are open. And the judgment-tier prohibitions need sign-off, including one that Phase 10.1 deliberately superseded. Neither blocks Płytarium.
---
_Verified: 2026-10-01T18:35:29Z_
_Verified: 2026-10-01T19:47:49Z_
_Verifier: Claude (gsd-verifier)_