35 KiB
phase, verified, status, score, covered_files, covered_digest, covered_files_note, behavior_unverified, overrides_applied, mvp_mode_note, re_verification
| phase | verified | status | score | covered_files | covered_digest | covered_files_note | behavior_unverified | overrides_applied | mvp_mode_note | re_verification | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 09-backend-admin-authentication-and-schema-pipeline | 2026-10-01T21:17:13Z | passed | 5/5 roadmap success criteria verified (plan truths 54/54 verified, including 3 backstop truths with direct test evidence) |
|
v2:sha256:23346bc11e5eaa8e8dd1d27c7eb748e4d2628d4c1c0ae8bbe32057696d375222 | 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. modules/lagoon/backend_admin_migrations.go and its test carry the WR-11 index added in 2da8112. 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 ba1aaef (clean working tree apart from an unrelated untracked zip). |
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. |
|
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-01T21:17:13Z (summercms.go HEAD ba1aaef, fonoteka.go HEAD e62f4fc)
Status: passed
Re-verification: Yes (final). The 2026-10-01T19:47Z report went stale when the WR-11 index (2da8112) changed modules/lagoon/backend_admin_migrations.go and its test. Every truth was re-checked at HEAD, the covered-file list was rebuilt at current paths, and the human items are resolved by the user's recorded decisions (09-REVIEW-FIX.md "Decisions", 09-UAT.md complete 3/3, ba1aaef).
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, andgranted(line 147) matches wildcard requirements (x.*,*x) as Winter'shasPermissiondoes.BackendUsers.FindByIDoverlays the user's ownbackend_users.permissionson the role grants (applyUserPermissions,auth.go:77):1grants,-1/0remove an exact code. - Navigation (WR-02):
Metadata(navigation.go:36) drops a main item the principal may not open, whatever its children allow, andopenableTarget(line 95) repoints an allowed parent to its first openable child. With Fonoteka's parent requirementgolem15.fonoteka.*, a genres-only admin should now see the parent linked togolem15.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). Formrequiredapplies only in contexts that can supply it.relationMutation(line 469) refuses link/unlink missing fromview.toolbarButtons. - Auth (WR-10 to WR-14): foreign
backendguard 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 viabouncer.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 explicitcolumn:tags (model_fields.go). - Gate and transactions (WR-18, WR-19):
check-phase9.shjudges zero tests per package; hooks and scopes read the write transaction throughcabana.TxFromContext(tx_context.go:27), used by Fonoteka's album and collection hooks. - Fonoteka (WR-15): an admin editor link leaves
granted_byNULL. - WR-11 index (
2da8112, user decision): migration202610010001_backend_users_email_ci_unique(modules/lagoon/backend_admin_migrations.go) checks for emails that differ only in case, fails naming the clashing logins, otherwise createsbackend_users_email_lower_unique ON backend_users (lower(email)); rollback drops it. Registered throughBackendAdminMigrations(migrations.go:74, plugin idsummercms.cabana).
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 behaviour was kept as Winter by user 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 closed; lower(email) unique index added, TestBackendAdminEmailCaseInsensitiveUnique PASS) |
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 (accepted by the user 2026-10-01): 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). |
| 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, case-insensitive unique email | ✓ VERIFIED | Explicit SQL up/down; TestBackendAdmin* (6 incl. EmailCaseInsensitiveUnique, EmailIndexRefusesCaseDuplicates) PASS |
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 |
Key Link Verification
| 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 |
| Backend admin migrations incl. WR-11 index | go test ./modules/lagoon -count=1 -v -run '^(TestBackendAdmin.*)$' |
6 PASS (Migration, Seed, Rollback, WinterRow, EmailCaseInsensitiveUnique, EmailIndexRefusesCaseDuplicates) |
✓ 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 |
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
None open. The previous items are resolved by the user's recorded decisions on 2026-10-01 (09-REVIEW-FIX.md "Decisions", 09-UAT.md status complete, 3/3 pass, commit ba1aaef), and the code at HEAD matches each decision:
| Item | Decision | Code evidence |
|---|---|---|
WR-11 unique index on lower(email) |
add it | migration 202610010001_backend_users_email_ci_unique; TestBackendAdminEmailCaseInsensitiveUnique, TestBackendAdminEmailIndexRefusesCaseDuplicates PASS |
| WR-03 single-record delete | keep as Winter (allowed whenever a form exists) | operationDeclared in modules/cabana/http.go; TestCRUDOperationsFollowDeclarations PASS |
| WR-17 deny versus wildcard | keep as Winter (exact-code merge) | applyUserPermissions in modules/cabana/auth.go; TestBackendUserPermissionsOverrideRole PASS |
| 24 judgment-tier prohibitions | verdicts accepted; 09-03 #1 superseded by Phase 10.1 D-09 | Prohibitions table above |
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 three policy choices from the fix run are decided (WR-11 index added and tested; WR-03 and WR-17 kept as Winter), the judgment-tier prohibition verdicts are accepted, and UAT is complete. No truth failed, no human item is open, so the phase is passed.
Verified: 2026-10-01T21:17:13Z Verifier: Claude (gsd-verifier)