- GET schema/form localizes the compiled form after the permission check
- relation: genre resolves to the exported Go field without changing the YAML key
- Reject an empty selection and dedupe ids before locking rows in pk order
- Return deleted 0 when every requested row is already gone, without hooks
- Roll back mixed, hook, and cancelled batches so no partial delete commits
- Empty selections are 422 and duplicates run once in primary-key order
- A completed retry and an all-absent selection delete nothing and skip hooks
- Mixed, hook, cancel, and concurrent requests keep the batch atomic
- Mount show, create, update, and delete behind the backend permission check
- Run controller and model hooks once per operation and roll back on failure
- Treat missing and out-of-scope records the same, including idempotent delete
- Record routes must enforce permission before ids or bodies and return D-10 envelopes
- Create, update, and delete run Before and After hooks once inside the transaction
- Out-of-scope and missing records are indistinguishable, and hook failure rolls back
- Bind schema fields to model columns at activation and drop protected keys
- Create and update Fill, run BeforeValidate, then Validate before persistence
- Missing Fill or Validate capability and provider errors fail closed
- Create and update must Fill then Validate and return D-10 422 field errors
- Schema bindings exclude protected, cased, nested, and unknown keys
- Missing Fill or Validate capability fails closed with controller context
- Search, sort, filters, and pagination use compiled selectors and bound values
- Equal sort keys break ties on the primary key so adjacent pages do not overlap
- Unknown identifiers return validation_failed before SQL
- Search, sort, filters, and adjacent pages must return the D-11 envelope
- Empty and single results keep an array and the requested page size
- Unknown identifiers and injected values fail closed before unsafe SQL
- Switch, date-range, and model-scope filters must keep typed values
- Option labels localize without changing identifiers or cached keys
- Raw conditions, unknown scopes, and arbitrary methods fail activation
- Typed columns, actions, default sort, search term, and page sizes
- Omitted sortable defaults to true and empty collections marshal as arrays
- Unknown keys, bad defaults, and path escape fail before routes are served
- Columns, actions, default sort, and page sizes must compile to typed JSON
- Empty and single declarations stay arrays and keep source order
- Unsupported keys, actions, defaults, and path escape fail activation
- config_form.yaml and config_list.yaml point at models/<name>/fields.yaml and columns.yaml
- Duplicate model or controller assets fail before any new file is written
- make:admin-controller must emit config_form and config_list beside model fields and columns
- A pre-existing model asset must fail before any controller file is written
- Cached schemas stay source-key IR and each response carries its own meta.locale
- YAML option maps keep declaration order and scalar type; method options call DropdownOptions and fail boot without a provider
- The same cached schema must localize pl and en independently, including Accept-Language parent fallback and raw keys
- YAML option maps keep order and scalar type, and a method provider is required at boot
- Strict config_form and fields documents keep source order and JSON scalar types
- Unknown keys, partials, path escape, and a mismatched modelClass fail activation with plugin context
- List-only controllers still activate when config_form.yaml is absent
- All locked field kinds, empty and single documents, and source order fail closed
- Unknown keys, types, duplicates, path escape, modelClass, partials, and missing assets must name the plugin, controller, and file
- Refresh, logout, and me use a separate PostgreSQL jti blacklist and safe profile
- Login stamps last_login only after a successful check and throttles repeated attempts
- Add the admin jti table, reset cutoff, and Winter indexes without AutoMigrate
- Reapply the developer and publisher seed idempotently and allow repeated role codes
- Fresh migrate is missing tokens_valid_after and the admin blacklist table
- Reapplying the seed is not idempotent and role codes reject Winter duplicates
- Same-secret backend token is still accepted by the frontend guard
- Permission denial must not invoke the schema or database callback
- Admin error bodies must not echo secrets or raw tokens
- Audience-aware mint, verify, refresh, and backend guard keep frontend tokens compatible
- Cabana mounts raw admin login, list schema, and record list behind admin.jwt.secret
- Framework migration seeds Winter backend users and developer/publisher roles
Documents the 103-method audit closure, the self-performed 11/11-closed
security review (with disclosure), the real defects check-phase8.sh's first
end-to-end run found and fixed, and the checkpoint decision to close Phase 8
with the Playwright UI matrix gap carried forward.
scripts/check-phase8.sh's final gate ran once with every stage green except
stage_ui_harness's Playwright browser matrix, a deliberate fatal() never
authored by 08-05. The user approved closing Phase 8 with this gap carried
forward; 08-VALIDATION.md flips 08-W0-07 green, marks 08-W0-08 partially
verified, and sets nyquist_compliant: false honestly. deferred-items.md
records what the follow-up spec needs to do.
Two real defects surfaced by the gate's first live run against the real
fonoteka-mcp SDK:
- stage_revoke looked up the connected app by a.name; ConnectedAppsIndex
actually serializes client_name (confirmed against
controllers/api/connected_app_controller.go serializeConnectedApp).
- Even with that fixed, stage_revoke ran after stage_replay, by which
point RevokeLineage's forward walk (presenting the pre-refresh spent
secret) had already cascade-revoked the live post-refresh access token
too -- correct, intentional T-08-REFRESH-REPLAY behavior, and the exact
same effect 08-09-PLAN.md's own mcp-lifecycle fixture ordering already
documented ('connected-apps would already be empty if list ran after
replay'). stage_refresh now captures the connected-app id while the
session is still live; stage_revoke DELETEs that id directly instead of
re-listing (ConnectedAppsDestroy has no revoked_at filter on its own
lookup, so this still exercises the real endpoint, idempotently, against
the id the real MCP-driven session actually owned).
08-10 Task 3 is the first time this gate has actually been executed
against real Docker/Postgres/the real fonoteka CLI/the real fonoteka-mcp
process. Four independent, previously-undetected defects surfaced:
- stage_postgres never set POSTGRES_INITDB_ARGS for the ICU pl-PL locale
lagoon.Use requires (every other Postgres testcontainer in this project
already does); the app failed to boot at all.
- stage_app_boot's seed step POSTed to
/_fonoteka/api/v1/onboarding/bootstrap, a route routes.go never mounts
(its own comment marks that group deliberately empty, pending a later
phase). The gate's test user/collection are now seeded directly with
SQL, matching every app-level OAuth test's own real-Postgres seeding.
- phase8_workdir() assigned PHASE8_WORKDIR from inside a function body
that is always invoked via command substitution (a subshell): the
assignment never escaped back to the calling shell, so every separate
caller (stage_postgres, stage_app_boot, each phase8_mcp_stage call, ...)
minted its own fresh mktemp directory. This silently fragmented one
run's state (app.log, the MCP client's gate-state.json) across dozens
of directories that never saw each other's writes -- the MCP client's
dcr stage could never see discovery's saved metadata. PHASE8_WORKDIR is
now set once, directly, in run_full_gate before any stage runs.
- gate-state.json (the MCP client's shared cross-invocation state) holds
raw live secrets by design and is never redacted; stage_secret_scan
correctly flagged it. It is now deleted once the MCP lifecycle stages
are done with it, before the scan runs -- the scan itself stays exactly
as strict as it already was.
stage_security_review also now refuses a nonzero threats_open count or a
missing required T-08-* row, not just a missing/unverified file, and gains
--security-review-only, a focused mode for Task 2's own verify command.