From 5d79f7d0bbe7174cff49c2f013de53a771cc783b Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Thu, 1 Oct 2026 21:50:43 +0200 Subject: [PATCH] docs(09): re-verify phase after code review fixes --- .../09-VERIFICATION.md | 194 ++++++++++-------- 1 file changed, 106 insertions(+), 88 deletions(-) diff --git a/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-VERIFICATION.md b/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-VERIFICATION.md index b9172ef..2615927 100644 --- a/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-VERIFICATION.md +++ b/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-VERIFICATION.md @@ -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 `` 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 `` 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)_