Files
summercms/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-VERIFICATION.md
2026-10-01 21:50:43 +02:00

36 KiB
Raw Blame History

phase, verified, status, score, covered_files, covered_digest, covered_files_note, behavior_unverified, overrides_applied, mvp_mode_note, re_verification, human_verification
phase verified status score covered_files covered_digest covered_files_note behavior_unverified overrides_applied mvp_mode_note re_verification human_verification
09-backend-admin-authentication-and-schema-pipeline 2026-10-01T19:47:49Z human_needed 5/5 roadmap success criteria verified (plan truths 54/54 verified, including 3 backstop truths with direct test evidence)
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-01-PLAN.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-01-SUMMARY.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-02-PLAN.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-02-SUMMARY.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-03-PLAN.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-03-SUMMARY.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-04-PLAN.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-04-SUMMARY.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-05-PLAN.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-05-SUMMARY.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-06-PLAN.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-06-SUMMARY.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-07-PLAN.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-07-SUMMARY.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-08-PLAN.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-08-SUMMARY.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-09-PLAN.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-09-SUMMARY.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-10-PLAN.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-10-SUMMARY.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-11-PLAN.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-11-SUMMARY.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-12-PLAN.md
.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-12-SUMMARY.md
internal/build/artifact.go
internal/build/build.go
internal/build/build_test.go
internal/build/stubs/artifacts.tmpl
modules/bouncer/audience_test.go
modules/bouncer/backend_guard_test.go
modules/bouncer/context.go
modules/bouncer/jwt.go
modules/bouncer/mint.go
modules/bouncer/refresh.go
modules/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
modules/cabana/contracts.go
modules/cabana/crud.go
modules/cabana/crud_lifecycle_test.go
modules/cabana/crud_test.go
modules/cabana/filter_schema.go
modules/cabana/form_schema.go
modules/cabana/form_schema_test.go
modules/cabana/http.go
modules/cabana/list_schema.go
modules/cabana/list_schema_test.go
modules/cabana/metadata_settings_test.go
modules/cabana/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
modules/cabana/security_coverage_test.go
modules/cabana/security_test.go
modules/cabana/settings.go
modules/cabana/testdata/list/all_columns.yaml
modules/cabana/testdata/list/all_filters.yaml
modules/cabana/tx_context.go
modules/lagoon/backend_admin_migrations.go
modules/lagoon/backend_admin_migrations_test.go
modules/lagoon/fill.go
modules/lagoon/fill_test.go
modules/lagoon/migrations.go
modules/pact/capabilities.go
modules/phrasebook/translator.go
modules/surf/router.go
scripts/check-phase9.sh
v2:sha256:915c2ad92c7250d07cf220cb1e97ef419209530f5838654dfa0b93b361dd3599 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. 0 0 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.
previous_status previous_score reason gaps_closed gaps_remaining regressions human_items_resolved
human_needed 5/5 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).
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').
test expected why_human
Decide the three open choices recorded under 'Decisions needed' in 09-REVIEW-FIX.md (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). 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 expected why_human
Review the 24 judgment-tier prohibitions (verdicts in the Prohibitions table) 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. 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-01T19:47:49Z (summercms.go HEAD 2ecc85e, fonoteka.go HEAD e62f4fc) Status: human_needed 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 (verified in code, not taken from 09-REVIEW-FIX.md):

  • 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

User story (from every 09-*-PLAN.md): As a backend administrator, I want to authenticate separately and manage resources described by Winter-shaped schemas, so that the administration surface stays permission-gated and reusable without coupling it to frontend users.

Step Expected Evidence Status
Provision an admin summer admin:create creates a bcrypt admin with a role; no boot or web seed modules/cabana/commands.go; 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

Goal Achievement

Observable Truths (ROADMAP contract)

# Truth Status Evidence
1 A backend admin user with a role logs in separately from frontend users, and navigation/controller access is gated by the permissions registry. ✓ VERIFIED Separate Winter-shaped backend_users/backend_user_roles (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: 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.

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), 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 (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
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; 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
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 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 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; 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 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
From To Via Status
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
internal/build/build.go cabana.RuntimeCommands generated main WIRED

Data-Flow Trace (Level 4)

Artifact Data Source Real data Status
List endpoint data rows GORM query on the registered model, scoped by ListExtendQuery Yes (PostgreSQL rows in assembled tests) ✓ FLOWING
Navigation entries plugin Navigation() filtered by 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

Behavioral Spot-Checks

Behavior Command Result Status
Framework vet go vet ./... (summercms.go) exit 0, no output ✓ PASS
Framework regression (single full run) go test ./... -count=1 (summercms.go) exit 0; 35 packages ok (plus 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
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

No scripts/*/tests/probe-*.sh exists and no plan declares a probe. The phase gate ran instead:

Probe Command Result Status
scripts/check-phase9.sh --self-test 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
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
ADMIN-05 09-11, 09-12 settings model bound through the same pipeline ✓ SATISFIED Truth 5

REQUIREMENTS.md maps exactly these six IDs to Phase 9, so there are no orphaned requirements.

Anti-Patterns Found

File Line Pattern Severity Impact
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

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. Decide the three open choices from 09-REVIEW-FIX.md

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: 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.

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. CR-01 and all 19 review warnings are fixed in code, and each fix has a passing named test.

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-01T19:47:49Z Verifier: Claude (gsd-verifier)