Files
summercms/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-VERIFICATION.md
2026-09-28 00:03:21 +02:00

31 KiB
Raw Blame History

phase, verified, status, score, covered_files, covered_digest, covered_files_note, behavior_unverified, overrides_applied, mvp_mode_note, human_verification
phase verified status score covered_files covered_digest covered_files_note behavior_unverified overrides_applied mvp_mode_note human_verification
09-backend-admin-authentication-and-schema-pipeline 2026-09-28T00:10:00Z 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
bouncer/audience_test.go
bouncer/backend_guard_test.go
bouncer/context.go
bouncer/jwt.go
bouncer/mint.go
bouncer/refresh.go
cabana/admin_openapi.go
cabana/auth.go
cabana/auth_test.go
cabana/bulk_test.go
cabana/commands.go
cabana/commands_test.go
cabana/contracts.go
cabana/crud.go
cabana/crud_lifecycle_test.go
cabana/crud_test.go
cabana/filter_schema.go
cabana/form_schema.go
cabana/form_schema_test.go
cabana/http.go
cabana/list_schema.go
cabana/list_schema_test.go
cabana/metadata_settings_test.go
cabana/navigation.go
cabana/phase09_contract_test.go
cabana/query.go
cabana/query_test.go
cabana/registry.go
cabana/relation.go
cabana/relation_test.go
cabana/schema.go
cabana/schema_types.go
cabana/security_coverage_test.go
cabana/security_test.go
cabana/settings.go
internal/build/artifact.go
internal/build/build.go
internal/build/build_test.go
internal/build/stubs/artifacts.tmpl
lagoon/backend_admin_migrations.go
lagoon/backend_admin_migrations_test.go
lagoon/fill.go
lagoon/migrations.go
pact/capabilities.go
phrasebook/translator.go
scripts/check-phase9.sh
surf/router.go
v2:sha256:510b91f54dea0918569d267a7aba22fcf879ad69899aa6e2033677a68a84de41 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 21c0f12 (clean working tree). summercms.go was checked at HEAD 2ad19bd. 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 (user-story.validate: true), used for User Flow Coverage. As in the Phase 1/3/5/8/10 reports, the five ROADMAP success criteria are the contract.
test expected why_human
Decide CR-01: generic admin CRUD cannot save a writable `type: number` field (json.Number is not convertible by lagoon.Fill), and type mismatches return 500 instead of 422 Either fix it before closing Phase 9 (normalize json.Number before Fill or teach lagoon.convertValue about it, map conversion failures to a per-field 422, add a CRUD test that writes a non-protected number field bound to an int column), or record an explicit deferral or override with a reason Reproduced by the verifier, but it defeats no ROADMAP success criterion: none of the five Płytarium forms or the settings form writes a numeric column. It does defeat the D-06 in-scope `number` field type on writes, which is a scope decision the verifier cannot make.
test expected why_human
Triage the 19 open code-review warnings, in particular the four that touch AUTH-08 semantics: WR-01 (wildcard requirements never match, ALL instead of Winter's ANY), WR-02 (denied navigation parent emitted with its albums target), WR-17 (user-level backend_users.permissions ignored, so Winter denies are lost at cutover), WR-14 (logout cannot revoke an expired-but-refreshable token) Each warning set to fixed, deferred (with reason) or skipped in 09-REVIEW-DISPOSITION.md. All 32 findings are currently `open`. None defeats a success criterion for the current Płytarium configuration (controllers declare single exact codes; the navigation leak matches Winter's own wildcard behavior), but D-01/D-03 promise Winter permission semantics at cutover, and that is a policy call.
test expected why_human
Review the 24 judgment-tier prohibitions (verdicts in the Prohibitions table) Accept or reject the verifier's non-authoritative verdicts. Two are qualified: 09-11 #1 (navigation must not reveal the target of an inaccessible entry; WR-02 shows the parent's albums target to a genres-only admin) and 09-12 #1 (zero-test detection is per invocation, not per package; WR-18). 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-09-28T00:10:00Z (summercms.go HEAD 2ad19bd, fonoteka.go HEAD 21c0f12) Status: human_needed Re-verification: No. This is the first verification of Phase 9. Phase 10 was already built and verified on top of it, so the checks below run against HEAD.

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.

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 cabana/commands.go, RuntimeCommands in generated main.go; TestAdminCreateCommand, TestAdminResetPasswordCommand (full suite PASS) VERIFIED
Log in separately Backend login by login or email returns a backend-audience JWT signed with admin.jwt.secret; frontend credentials fail cabana/auth.go, bouncer.NewBackendJWTGuard; TestAdminAuthLifecycle, TestAdminTracerGenreList (a frontend user with the same email is rejected), TestPhase09GuardIsolation (re-run: PASS) VERIFIED
See permitted navigation /navigation lists only permitted items cabana/navigation.go Metadata; TestAdminMetadataFiltering (developer sees fonoteka, publisher sees []), re-run: PASS VERIFIED (WR-02 caveat)
Open a controller 403 without the controller permission, before any schema or SQL work cabana/http.go protect; TestPhase09PermissionMatrix, TestAdminTracerPermissionBoundary, TestPhase09SecurityRoutes (re-run: PASS) VERIFIED
Work with schema-driven lists and forms Form/list/relation schemas compiled from Winter YAML; CRUD, bulk delete, relation manager, settings Sections below; TestPhase09AssembledAcceptance (re-run: PASS, real PostgreSQL) VERIFIED
Outcome: permission-gated, reusable, decoupled Framework has no app names; admin identity never touches frontend users grep -i "fonoteka|collection_editors|granted_by|golem15_" on non-test cabana/*.go: no hits. BackendUsers.FindByID reads only backend_users VERIFIED

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 tables (lagoon/backend_admin_migrations.go, developer/publisher seeded as system roles). Separate backend guard with its own secret and a required backend audience. Cross-audience tokens fail in both directions (TestPhase09GuardIsolation, TestAdminTracerAuthBoundary). Grants come from the role JSON plus HasPermissions role assignments (cabana/auth.go:36-61). Every controller, relation and CRUD route goes through protect: guard, then controller lookup, then Allows(principal, RequiredPermissions()), then work (cabana/http.go:701-719). Settings go through protectSetting. /navigation and /settings filter entries by the same Allows. The five Fonoteka controllers each declare one exact code (access_albums etc.). Tests re-run and passing: TestPhase09PermissionMatrix, TestPhase09SecurityMatrix, TestAdminMetadataFiltering, TestAdminMetadataRejectsFrontendPrincipal. Caveats (warnings, not failures): WR-01 (a wildcard requirement never matches a specific grant, and multi-code requirements use ALL where Winter uses ANY; no Fonoteka controller or setting uses either), WR-02 (a genres-only admin gets the fonoteka parent entry with controller: golem15.fonoteka.albums, which then answers 403; Winter's hasAnyAccess wildcard shows the same parent), WR-17 (user-level backend_users.permissions is ignored).
2 fields.yaml for a real controller parses (goccy/go-yaml) into a JSON form schema covering text, textarea, checkbox, switch, dropdown (model-method options) and relation (nameFrom, emptyOption), with span/tabs/context/attributes layout hints. ✓ VERIFIED cabana/form_schema.go decodes with github.com/goccy/go-yaml and yaml.DisallowUnknownField() (line 279), walks the AST to keep order, and rejects unknown keys and types and type: partial (line 344). The real Fonoteka YAML covers every listed item: text (all models), textarea (genre, collection), checkbox (artist is_various), switch (settings), dropdown options: getFormatOptions (album, served by Album.DropdownOptions), relation with nameFrom and emptyOption (album genre, collection owner), span (all), tab (collection editors), context (style slug, collection editors), attributes (style slug readonly). Tests re-run: TestFormSchemaCompile, TestFormSchemaRejects, TestAlbumsAdminForm, TestAlbumsAdminDropdowns, TestStylesAdminForm, TestCollectionsAdminForm: PASS.
3 columns.yaml parses into a JSON list schema with searchable/sortable/relation columns and datetime/switch renderers. ✓ VERIFIED cabana/list_schema.go column types text, datetime, switch (line 23); ListColumn carries searchable, sortable, relation, select (cabana/schema_types.go:20-24). Real YAML: searchable (all models), sortable (album format, collection created_at), relation + select (album genre.name, collection owner.username, mapped to email by ListRelationColumnMapper), type: datetime (collection created_at), type: switch (artist is_various). The list query resolves search/sort/filter only through compiled allowlists, with a primary-key tie-break. Tests re-run: TestListSchemaCompile, TestAlbumsAdminList, TestArtistsAdminList, TestCollectionsAdminListCRUD, TestGenresAdminEdges: PASS. Caveat: WR-08 (a non-text searchable column gives a PostgreSQL error and a 500; no Fonoteka searchable column is non-text, but the scaffold emits id: searchable: true).
4 The relation-manager schema supports search/link/unlink/manage-or-view lists for Collections' editors tab, replacing the partial field entirely. ✓ VERIFIED models/collection/fields.yaml editors: type: relation-manager, relation: editors, tab, span: full, context: update. There is no partial anywhere in the Fonoteka fields YAML, and the compiler rejects it. controllers/collections/config_relation.yaml has view.list.columns, manage.list.columns, toolbarButtons: link|unlink, showSearch. cabana/relation.go serves schema, linked, candidates (with RelationExtendManageQuery applied at line 493), link and unlink (RelationBeforeLink at line 683), with explicit pivot writes and no hardcoded pivot names. Tests re-run on PostgreSQL: TestCollectionsAdminRelationSchema, Link, Unlink, Idempotent, ForgedPivot, CrossScope, Concurrent, RelationEdges, RejectsPartial, TestRelationCandidateExclusions: PASS. Caveats: WR-05 (link/unlink do not check toolbarButtons), WR-07 (column order depends on 16-space indentation; the tracked file uses it), WR-15 (granted_by stores a backend admin id in a frontend-user column).
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 Hooks are optional interfaces in pact/capabilities.go:193-254, type-asserted in cabana/query.go:109, cabana/crud.go:439,528,575,591, cabana/relation.go:493. Albums implements the list/form/before-create/before-update hooks (D-14 collection scoping), and Collections implements RelationExtendManageQuery and RelationBeforeLink. Bulk delete (CRUDService.BulkDelete, cabana/crud.go:187) normalizes and sorts ids, locks the scoped rows through ListExtendQuery, and deletes each row separately with tx.Delete(model), so GORM model hooks and Form*Delete hooks fire per record. It never issues a single DELETE ... IN. TestBulkDeleteDuplicates, TestBulkDeleteIdempotent, TestBulkDeleteRollback and TestCRUDHooks (re-run: PASS) assert per-record hook ids in ascending order. The Fonoteka setting is registered through HasSettings with a compiled models/settings/fields.yaml. Schema, GET and PUT use the same FormSchema.Localize, writable projection, lagoon.Fill and lagoon.Validate (cabana/settings.go). TestAdminSettings* (8 tests, re-run on PostgreSQL: PASS). CR-01 (confirmed, escalated): see Human Verification item 1. It does not affect this truth for the delivered app, because search_use_typesense is a bool switch and no Fonoteka form writes a numeric column.

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 (TestAlbumsAdmin*, TestArtistsAdmin*, TestGenresAdmin*, TestStylesAdmin*, TestCollectionsAdmin*, TestAdminMetadata*, TestAdminSettings*, TestAdminTracer*, TestAdminAuthLifecycleAssembled, TestPhase09*) with -v: 79 PASS, 0 SKIP, 0 FAIL. The framework suites (bouncer, cabana, lagoon, internal/build) passed in the full go test ./... run.

Truth 09-12 #4 names ../fonoteka.go/docs/openapi.json as the admin OpenAPI home. Phase 10 D-15 intentionally moved the admin paths to the framework-owned summercms.go/admin/openapi/admin.json, and fonoteka.go/docs/openapi.json is again the parity document. The intent still holds. admin.json lists all 21 D-09 paths, scripts/check-admin-openapi.sh --check exits 0, and scripts/check-phase9.sh --openapi exits 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 (login by life and by life@example.test against Life@Example.Test), TestAdminInactive/TestAdminDeleted (assertSameOpaque), TestAdminTracerGenreList (inserts a frontend user with the admin's email and asserts the frontend password is rejected). All re-run: PASS VERIFIED (WR-11 caveat: login = ? OR lower(email) = ? takes the first match on a cross-field collision)
09-11: missing settings GET is side-effect-free with exists: false; first PUT creates; identical PUT is idempotent TestAdminSettingsMissingRead, TestAdminSettingsCreate, TestAdminSettingsIdempotentUpdate (re-run: PASS) VERIFIED
09-12: AUTH-08 is the separate D-01/D-02 principal only; no shared role table or dual-purpose token Separate tables and migration, Principal.Backend provenance flag, audience checks; TestPhase09GuardIsolation, TestAdminMetadataRejectsFrontendPrincipal (PASS) VERIFIED

Prohibitions (judgment tier, non-authoritative verdicts)

Plan Prohibition Verdict
01 Backend identities must not share frontend user rows, the jwt guard, or a signing secret not violated
01 Hidden navigation must not replace server-side authorization not violated (protect/protectSetting on every route)
01 Tracer must not be mock-only not violated (testcontainers PostgreSQL, assembled router)
02 No boot, web-wizard or env-seeded first admin not violated (migration seeds roles only; admin:create only)
02 No cookie session store and no merge with the frontend user model not violated (Phase 10 carries the same JWT in a cookie; there is no server-side session store)
02 Auth logs carry no password, JWT, secret or hash not violated (TestAdminAuthLogging, TestPhase09SecurityCoverage)
03 Form compiler must not accept type: partial not violated (form_schema.go:344)
03 Labels not deferred to the client or cached in the first request's locale not violated (per-request Localize; T-09-06)
03 Unknown keys, types or providers not silently ignored not violated (DisallowUnknownField, formFieldKeys)
04 YAML must not inject SQL or name an arbitrary method not violated (conditions: rejected, finite FilterScopes)
04 Equal sort values must not make rows jump across pages not violated (PK tie-break; adjacent-page tests)
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 (the album form has no user choice field)
07 Artist parity not reached by silently omitting Winter keys not violated (Phase 10 TestPhase10Controllers asserts fields and columns equal the tracked YAML)
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 (grep of non-test cabana/*.go: no hits)
10 Owner, cross-collection or already-linked candidates not linkable by forgery not violated (ForgedPivot, CrossScope, Idempotent)
11 Navigation/settings metadata must not reveal existence, label or target of inaccessible entries qualified: a genres-only admin receives the parent fonoteka entry with controller: golem15.fonoteka.albums (WR-02). Only the parent's default target leaks, and Winter behaves the same way.
11 Reading a missing singleton must not create a row not violated (TestAdminSettingsMissingRead)
12 Acceptance must not depend on skipped PostgreSQL tests or zero-test matches qualified: check-phase9.sh --self-test refuses skips and zero-test runs (re-run: PASS), but detection is per invocation, not per package (WR-18).
12 High threats marked mitigated only with a named failing-when-broken test not violated (09-SECURITY-REVIEW.md names a command per threat; six rows are "owned by 09-0x; not re-run in 09-12", and all of those passed in this session)

Required Artifacts

Artifact Expected Status Details
lagoon/backend_admin_migrations.go backend_users/roles, system-role seed ✓ VERIFIED Explicit SQL up/down, no AutoMigrate; TestBackendAdmin* in lagoon suite PASS
cabana/auth.go backend provider, login/refresh/logout/me ✓ VERIFIED Wired from Activate; BackendUsers reads only backend_users
cabana/commands.go admin:create, admin:reset-password ✓ VERIFIED RuntimeCommands appended in the generated main
cabana/http.go route mounting, protect ✓ VERIFIED Mounted from surf.BuildRouter via cabana.Activate
cabana/form_schema.go, schema_types.go strict typed form compiler ✓ VERIFIED goccy/go-yaml strict decode
cabana/list_schema.go, filter_schema.go, query.go list compiler and allowlisted query ✓ VERIFIED
cabana/crud.go CRUD, projection, hooks, bulk delete ✓ VERIFIED (CR-01) Numeric writes fail inside lagoon.Fill
cabana/relation.go relation schema and link/unlink ✓ VERIFIED
cabana/navigation.go, settings.go filtered metadata, singleton settings ✓ VERIFIED (WR-02)
scripts/check-phase9.sh fail-closed gate ✓ VERIFIED --self-test, --openapi re-run: exit 0
fonoteka.go/.../controllers/{albums,artists,collections,genres,styles}_admin_controller.go five controllers with permissions ✓ VERIFIED Registered through HasAdminControllers
fonoteka.go/.../models/*/fields.yaml, columns.yaml, controllers/*/config_*.yaml Winter-shaped YAML ✓ VERIFIED Embedded; partial replaced by relation-manager
fonoteka.go/.../admin_permissions.go, admin_navigation.go, admin_settings.go registerPermissions/Navigation/Settings ports ✓ VERIFIED
fonoteka.go/.../classes/backend_album_collection.go D-14 email-to-collection resolver ✓ VERIFIED TestAlbumsAdminCollectionMatch etc. PASS
fonoteka.go/.../admin_phase09_e2e_test.go assembled acceptance ✓ VERIFIED TestPhase09AssembledAcceptance PASS
From To Via Status
surf/router.go cabana/http.go cabana.Activate in BuildRouter WIRED
cabana/http.go guard bouncer/jwt.go NewBackendJWTGuard(secret, users, bl, ...) with backend audience WIRED
cabana/http.go handlers compiled schemas s.reg.Get(id) then cc.List/cc.Form/cc.Relations WIRED
cabana/crud.go lagoon.Fill/lagoon.Validate save transaction WIRED (CR-01 breaks numeric values)
cabana/crud.go bulk model lifecycle hooks per-row tx.Delete(model) WIRED
cabana/settings.go form pipeline setting.Form.Localize, Fill, 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 principal grants from backend_user_roles Yes ✓ FLOWING
Settings GET data golem15_fonoteka_settings row or compiled defaults Yes ✓ FLOWING
Relation linked/candidates rows pivot-joined user query with the owner and linked users excluded Yes ✓ FLOWING

Behavioral Spot-Checks

Behavior Command Result Status
Framework regression go vet ./... && go test ./... -count=1 (summercms.go) exit 0, 23 packages ok ✓ PASS
App regression go vet + go test $(go list -f '{{.Dir}}/...' -m) -count=1 (fonoteka.go) exit 0, 13 packages ok ✓ PASS
Assembled Phase 9 acceptance `go test ./plugins/golem15/fonoteka -run '^(TestPhase09.* TestAdminSettings.* TestAdminMetadata.*
Controllers, auth and tracer `go test ./plugins/golem15/fonoteka -run '^(TestAlbumsAdmin.* TestGenresAdmin.* TestArtistsAdmin.*
Bulk hooks, CRUD hooks, schema compile, permission matrix `go test ./cabana -run '^(TestBulkDelete(Duplicates Idempotent Rollback)
Guard isolation go test ./bouncer -run '^TestPhase09GuardIsolation$' PASS ✓ PASS
CR-01 reproduction scratch module calling lagoon.Fill(m, {"year"}, {"year": json.Number("1990")}) on an int field lagoon: fill year: cannot assign json.Number to int ✗ confirms CR-01

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 "phase9 self-test passed", exit 0 PASS
scripts/check-phase9.sh --openapi "phase9 openapi passed", exit 0 PASS
scripts/check-admin-openapi.sh --check exit 0 PASS

Requirements Coverage

Requirement Source Plans Description Status Evidence
AUTH-08 09-01, 09-02, 09-11, 09-12 Backend admins with roles and a permission registry, separate from frontend users, gating navigation and controllers ✓ SATISFIED Truth 1 (warnings WR-01, WR-02, WR-14, WR-17)
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 (CR-01 escalated) 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. Info: REQUIREMENTS.md still shows all six as [ ] / Pending, and the traceability update is left to phase completion.

Anti-Patterns Found

File Line Pattern Severity Impact
cabana/crud.go + lagoon/fill.go 625 / 155-166 UseNumber values passed to Fill, which cannot convert json.Number 🛑 (review CR-01; escalated, not a roadmap gap) Writable number fields 500; type mismatches 500 instead of 422
cabana/contracts.go 105-137 wildcard requirement unmatched; ALL not ANY ⚠️ Warning (WR-01) Latent for future Winter ports
cabana/navigation.go 42-54 denied parent emitted with its target ⚠️ Warning (WR-02) Minor metadata disclosure
cabana/auth.go 36-76 user-level permissions ignored ⚠️ Warning (WR-17) Winter denies lost at cutover
cabana/query.go, internal/build/stubs/artifacts.tmpl 294, 139-142 LOWER(col) on non-text columns; scaffold makes id searchable ⚠️ Warning (WR-08) Scaffolded controllers 500 on search
internal/build/stubs/artifacts.tmpl 92-106 scaffold has no permissions or record source ⚠️ Warning (WR-09) Open-to-all admins once completed naively
internal/build/artifact.go 179 "TODO: describe " string in generated command stub ℹ️ Info Generated text, not a debt marker in phase code

The other open review warnings (WR-03 to WR-07, WR-10 to WR-16, WR-18, WR-19) and the 12 info findings are recorded in 09-REVIEW.md. None of them defeats a ROADMAP success criterion for the current configuration. No unreferenced TBD/FIXME/XXX was found in phase files.

Human Verification Required

1. Decide CR-01 (numeric fields cannot be saved)

Test: Choose one: fix before closing the phase, defer with a tracked follow-up, or accept with an override. Expected: If fixed: normalize json.Number before lagoon.Fill or in lagoon.convertValue, map conversion failures to a per-field 422 instead of a CapabilityError 500, and add a CRUD test that writes a non-protected type: number field bound to an int column (plus the settings equivalent). Why human: The verifier reproduced it. It breaks the D-06 in-scope number type on writes, which Phase 10's NumberField.vue renders, but no ROADMAP criterion and no Płytarium form depends on it. No later phase in the roadmap covers it.

2. Triage the open review warnings, starting with the AUTH-08 ones

Test: Set dispositions in 09-REVIEW-DISPOSITION.md, where all 32 findings are open. Expected: WR-01, WR-02, WR-14 and WR-17 explicitly fixed or deferred with a reason. D-01/D-03 promise Winter permission semantics and a straight backend_users copy at cutover (Phase 15). Why human: This is a policy and scope decision.

3. Review the 24 judgment-tier prohibitions

Test: Accept or reject the verdicts in the Prohibitions table. Expected: Pay particular attention to the two qualified verdicts (09-11 navigation target, 09-12 per-invocation zero-test detection). 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 partial, the CRUD hooks, per-record bulk delete and the settings binding all exist, are wired, and pass their tests on real PostgreSQL at HEAD in both repositories.

The phase is still not passed, for three reasons. The critical review finding CR-01 is real and reproduced: the framework CRUD and settings engine cannot persist numbers, even though number is a D-06 in-scope field type. All 19 review warnings are untriaged, and four of them bear on the Winter permission semantics that AUTH-08 and D-03 promise. And 24 judgment-tier prohibitions need human sign-off. None of these blocks Płytarium today, but they are decisions for the developer, not for the verifier.


Verified: 2026-09-28T00:10:00Z Verifier: Claude (gsd-verifier)