36 KiB
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) |
|
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. |
|
|
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, 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.
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 |
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 |
| 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 andadmin:createalready 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
-1onacme.adoes not remove a role'sacme.*), 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)