diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 312ce89..a3fd931 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -375,7 +375,7 @@ Plans: 4. The relation-manager schema supports search/link/unlink/manage-or-view lists for Collections' editors tab, replacing the `partial` field entirely. 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. -**Plans**: 2/12 plans executed +**Plans**: 3/12 plans executed **Research flag:** yes Plans: @@ -387,7 +387,7 @@ Plans: - [x] 09-02-PLAN.md — Complete backend identity lifecycle and operator provisioning **Wave 3** *(blocked on Wave 2 completion)* -- [ ] 09-03-PLAN.md — Compile the typed form-schema contract and Winter scaffolding +- [x] 09-03-PLAN.md — Compile the typed form-schema contract and Winter scaffolding **Wave 4** *(blocked on Wave 3 completion)* - [ ] 09-04-PLAN.md — Compile the list contract and the allowlisted query engine @@ -529,7 +529,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → | 6. HTTP routing, auth groups and rate limiting | 14/14 | Complete | 2026-09-21 | | 7. User plugin and authentication | 8/8 | Complete | 2026-09-23 | | 8. OAuth2.1 authorization server | 10/10 | Complete | 2026-09-23 | -| 9. Backend admin authentication and schema pipeline | 2/12 | In Progress| | +| 9. Backend admin authentication and schema pipeline | 3/12 | In Progress| | | 10. Admin Vue SPA | 0/TBD | Not started | - | | 11. Jobs, realtime and search infrastructure | 0/TBD | Not started | - | | 12. Płytarium API — Collections and Albums | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 4ab3bbe..1651266 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,16 +4,16 @@ milestone: v1.0 current_phase: 09 current_phase_name: Backend admin authentication and schema pipeline status: executing -stopped_at: Completed 09-02-PLAN.md -last_updated: "2026-09-24T15:59:20.547Z" +stopped_at: Completed 09-03-PLAN.md +last_updated: "2026-09-24T16:27:09.874Z" last_activity: 2026-09-24 last_activity_desc: Phase 09 execution started -state_head: 5f218977e4cc61b103a3be4e459fe5aa6962006f +state_head: 68715fc2603f129f0e9f05d8d1db23f5d7594e12 progress: total_phases: 15 completed_phases: 8 total_plans: 67 - completed_plans: 57 + completed_plans: 58 milestone_name: milestone --- @@ -29,7 +29,7 @@ See: .planning/PROJECT.md (updated 2026-09-16) ## Current Position Phase: 09 (Backend admin authentication and schema pipeline) — EXECUTING -Plan: 3 of 12 +Plan: 4 of 12 Status: Ready to execute Last activity: 2026-09-24 — Phase 09 execution started @@ -111,6 +111,7 @@ Progress: [██████████] 100% |------|----------|-------|-------| | Phase 09 P01 | 26min | 2 tasks | 26 files | | Phase 09 P02 | 22 min | 3 tasks | 14 files | +| Phase 09 P03 | 25min | 3 tasks | 10 files | ## Accumulated Context @@ -276,6 +277,10 @@ Recent decisions affecting current work: - [Phase 09]: Admin jti rows live in backend_jwt_blacklist and cabana does not republish the frontend BlacklistStore — Refresh and logout must not revoke frontend tokens or be revoked by them. - [Phase 09]: backend_user_roles.code is indexed and not unique so Winter rows can repeat a code — admin:create rejects zero or many matches instead of a unique constraint the cutover table does not have. - [Phase 09]: tokens_valid_after is a nullable additive column used to revoke admin JWTs on password reset — The guard already honors Principal.TokensValidAfter and Winter's required columns stay unchanged. +- [Phase 09]: Cached form schemas keep source phrase keys; each response localizes a copy and records meta.locale +- [Phase 09]: Absent config_form.yaml does not fail activation, so the 09-01 Genre list controller still boots +- [Phase 09]: YAML option keys keep their JSON scalar type; method options call DropdownOptions with the exact field name +- [Phase 09]: make:admin-controller writes controllers/name config_form and config_list pointing at models/name fields and columns ### Pending Todos @@ -298,6 +303,6 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-09-24T15:59:20.134Z -Stopped at: Completed 09-02-PLAN.md +Last session: 2026-09-24T16:27:09.536Z +Stopped at: Completed 09-03-PLAN.md Resume file: None diff --git a/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-03-SUMMARY.md b/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-03-SUMMARY.md new file mode 100644 index 0000000..b5636a4 --- /dev/null +++ b/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-03-SUMMARY.md @@ -0,0 +1,244 @@ +--- +phase: 09-backend-admin-authentication-and-schema-pipeline +plan: 03 +subsystem: admin +tags: [yaml, goccy, phrasebook, cabana, admin-schema, scaffolding] + +requires: + - phase: 09-backend-admin-authentication-and-schema-pipeline + provides: cabana list compiler, admin controller registry, and phrasebook translator +provides: + - Typed Winter form schema compiled at activation with source order and JSON scalar types + - Per-request localization of labels, comments, tabs, emptyOption, and option labels + - make:admin-controller output that matches the Winter controller/model YAML split +affects: [09-backend-admin-authentication-and-schema-pipeline, admin-api, phase-10-spa] + +actuals: + tokens: 13818 + tasks: 3 + commits: 6 + +tech-stack: + added: [] + patterns: + - "Form IR caches source phrase keys; Localize copies the schema per request" + - "config_form.yaml is compiled only when the file is present, so list-only controllers still boot" + - "Dropdown method names require DropdownOptions on the controller or its NewRecord model" + +key-files: + created: + - cabana/schema_types.go + - cabana/form_schema.go + - cabana/form_schema_test.go + modified: + - cabana/contracts.go + - cabana/registry.go + - cabana/schema.go + - phrasebook/translator.go + - internal/build/artifact.go + - internal/build/stubs/artifacts.tmpl + - internal/build/build_test.go + +key-decisions: + - "Cached form schemas keep source phrase keys; each response localizes a copy and records meta.locale" + - "Absent config_form.yaml does not fail activation, so the 09-01 Genre list controller still boots" + - "YAML option keys keep their JSON scalar type; method options call DropdownOptions(field) with the exact field name" + - "make:admin-controller writes controllers//config_form.yaml and config_list.yaml pointing at models//fields.yaml and columns.yaml" + +patterns-established: + - "Pattern: boot compiler walks the YAML AST so field order and option scalar types survive DisallowUnknownField" + - "Pattern: a missing dropdown provider fails with plugin, controller, file, and the exact method name" + +requirements-completed: [ADMIN-01] + +coverage: + - id: D1 + description: Every locked form field and layout hint compiles in source order with Winter JSON keys and typed scalars. + requirement: ADMIN-01 + verification: + - kind: unit + ref: cabana/form_schema_test.go#TestFormSchemaCompile + status: pass + human_judgment: false + - id: D2 + description: Empty fields serialize as an array, a single field stays one element, and repeated compilation is byte-stable. + requirement: ADMIN-01 + verification: + - kind: unit + ref: cabana/form_schema_test.go#TestFormSchemaEmpty + status: pass + - kind: unit + ref: cabana/form_schema_test.go#TestFormSchemaSingle + status: pass + - kind: unit + ref: cabana/form_schema_test.go#TestFormSchemaOrdering + status: pass + human_judgment: false + - id: D3 + description: Unknown keys, unknown types, duplicates, path escape, a mismatched modelClass, partials, and missing assets fail with plugin, controller, and file context. + requirement: ADMIN-01 + verification: + - kind: unit + ref: cabana/form_schema_test.go#TestFormSchemaRejects + status: pass + human_judgment: false + - id: D4 + description: One cached schema localizes independently in pl and en, including Accept-Language parent fallback, app.locale, and raw-key fallback. + requirement: ADMIN-01 + verification: + - kind: unit + ref: cabana/form_schema_test.go#TestFormSchemaLocalization + status: pass + - kind: unit + ref: cabana/form_schema_test.go#TestFormSchemaLocaleIsolation + status: pass + human_judgment: false + - id: D5 + description: YAML option maps keep order and scalar type, and a string options method requires DropdownOptions at boot. + requirement: ADMIN-01 + verification: + - kind: unit + ref: cabana/form_schema_test.go#TestFormSchemaDropdownOptions + status: pass + human_judgment: false + - id: D6 + description: summer make:admin-controller emits the Winter controller/model YAML split, compiles through the strict loader, and refuses a duplicate without partial files. + requirement: ADMIN-01 + verification: + - kind: unit + ref: internal/build/build_test.go#TestMakeAdminControllerLayout + status: pass + human_judgment: false + +duration: 25min +completed: 2026-09-24 +status: complete +plan_head_before: af3312aa9260daa7106f66dff892924db965b930 +plan_head_after: 68715fc2603f129f0e9f05d8d1db23f5d7594e12 +--- + +# Phase 9 Plan 03: Typed Winter form schema Summary + +**Winter form YAML now compiles into an ordered typed JSON schema, each response localizes its own copy, and `make:admin-controller` emits that same layout.** + +## Performance + +- **Duration:** 25 min +- **Started:** 2026-09-24T16:01:20Z +- **Completed:** 2026-09-24T16:25:51Z +- **Tasks:** 3 +- **Files modified:** 10 + +## Accomplishments + +- `config_form.yaml` and `fields.yaml` compile once at cabana activation into text, textarea, number, checkbox, switch, dropdown, relation, and relation-manager fields, in source order, with Winter JSON spelling. +- Empty fields are `[]`, option values keep string, number, and boolean JSON types, and `type: partial` plus unknown keys, types, path escape, and a mismatched `modelClass` fail before serving. +- Labels, comments, tabs, emptyOption, and option labels resolve per request through phrasebook. The cache stays source keys. `meta.locale` is the selected request locale, or `app.locale` when the request has none. +- `summer make:admin-controller` writes `controllers//config_form.yaml` and `config_list.yaml` that point at `models//fields.yaml` and `columns.yaml`. A duplicate asset fails before any new file is written. + +## Task Commits + +Each task was committed atomically. `commits: 6` is `git rev-list --count` from the plan ledger. + +1. **Task 1: Strict ordered form compilation (RED)** - `ea3f070` (test) +2. **Task 1: Strict ordered form compilation (GREEN)** - `4c814e5` (feat) +3. **Task 2: Locale and dropdown options (RED)** - `ad1b760` (test) +4. **Task 2: Locale and dropdown options (GREEN)** - `da8828e` (feat) +5. **Task 3: Winter admin scaffolding (RED)** - `af8e58a` (test) +6. **Task 3: Winter admin scaffolding (GREEN)** - `68715fc` (feat) + +**Plan metadata:** pending docs commit + +## Files Created/Modified + +- `cabana/schema_types.go` - form schema, field, option, and localized view types +- `cabana/form_schema.go` - strict boot compiler and request-time localizer +- `cabana/form_schema_test.go` - golden, empty/single/order, rejection, locale, and dropdown tests +- `cabana/registry.go` - compiles a form when `config_form.yaml` is present +- `cabana/contracts.go` - compiled controllers carry the form schema +- `cabana/schema.go` - exported `CompileList` for the same strict list loader +- `phrasebook/translator.go` - `Locale()` reports the configured app locale +- `internal/build/artifact.go` - Winter directory split and duplicate checks for the full file set +- `internal/build/stubs/artifacts.tmpl` - `config_form.yaml` and `config_list.yaml` stubs +- `internal/build/build_test.go` - layout, strict compile, registry stability, and partial-write tests + +## Decisions Made + +- The cached schema stores phrase keys. `Localize` copies fields and options, so one request's locale cannot change another's result or the boot cache. +- `meta.locale` is the primary Accept-Language tag when the request has one (`pl-PL,en;q=0.5` becomes `pl-PL`). Phrasebook still parent-falls back to `pl`. With no request locale, meta uses the translator's app locale. +- A controller with only `config_list.yaml` keeps activating. The 09-01 Genre tracer has no form yet. A present but invalid form still fails activation. +- Dropdown `options:` maps keep declaration order and key scalar type. A string such as `getFormatOptions` is a method name, compared exactly, and the controller or `NewRecord()` model must implement `DropdownOptions`. Only the label is translated. +- Scaffolded `modelClass` is the controller ident (`Albums`), matching `ModelName()`, and `ConfigDir()` stays `controllers/`. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 2 - Missing Critical] Exposed the configured app locale** +- **Found during:** Task 2 (locale and dropdown options) +- **Issue:** `phrasebook.Translator` kept `app.locale` unexported, so a request with no Accept-Language could not put the real app locale in `meta.locale`. +- **Fix:** Added `Translator.Locale()`. Cabana does not read config itself. +- **Files modified:** `phrasebook/translator.go`, `cabana/form_schema.go` +- **Verification:** `TestFormSchemaLocalization/parent_and_app_locale` passed +- **Committed in:** `da8828e` + +**2. [Rule 3 - Blocking] Exported the list compiler for scaffold verification** +- **Found during:** Task 3 (Winter admin scaffolding) +- **Issue:** Generated `config_list.yaml` had to compile through the strict loader, and `compileList` was unexported to the build tests. +- **Fix:** Exported `cabana.CompileList` as a wrapper. No behavior change for activation. +- **Files modified:** `cabana/schema.go`, `internal/build/build_test.go` +- **Verification:** `TestMakeAdminControllerLayout/winter_files` passed +- **Committed in:** `af8e58a` (export) and `68715fc` (scaffold) + +--- + +**Total deviations:** 2 auto-fixed (1 missing critical, 1 blocking) +**Impact on plan:** Both keep the locked locale and loader contracts testable. No new dependency and no response-shape change outside the form schema this plan owns. + +## TDD Gate Compliance + +| Gate | Commit | Result | +|------|--------|--------| +| RED task 1 | `ea3f070` test(09-03) | `TestFormSchemaCompile` failed because compilation was not implemented | +| GREEN task 1 | `4c814e5` feat(09-03) | compile, empty, single, ordering, and reject tests passed | +| RED task 2 | `ad1b760` test(09-03) | `TestFormSchemaLocalization` failed because localization was not implemented | +| GREEN task 2 | `da8828e` feat(09-03) | localization, dropdown, and race-isolated locale tests passed | +| RED task 3 | `af8e58a` test(09-03) | `TestMakeAdminControllerLayout` failed on the old controller-local YAML paths | +| GREEN task 3 | `68715fc` feat(09-03) | Winter layout, strict compile, and duplicate-without-partial-write passed | + +`gsd_run check tdd-red-evidence` returned `RED_EVIDENCE_OK` for all three RED runs. Go's test output is not TAP, so each evidence record appends a TAP trailer naming the test the go harness failed. + +## Authentication Gates + +None. + +## Issues Encountered + +None. + +## User Setup Required + +None - no external service configuration required. + +## Next Phase Readiness + +- Ready for 09-04. Form compilation, localization, and the Winter scaffold layout are in place. +- ADMIN-01 stays pending in REQUIREMENTS.md until the later plans that also declare it have summaries. +- List-only controllers remain valid until those plans add `config_form.yaml`. + +## Self-Check: PASSED + +- FOUND: cabana/form_schema.go +- FOUND: cabana/schema_types.go +- FOUND: cabana/form_schema_test.go +- FOUND: internal/build/stubs/artifacts.tmpl +- FOUND: ea3f070 +- FOUND: 4c814e5 +- FOUND: ad1b760 +- FOUND: da8828e +- FOUND: af8e58a +- FOUND: 68715fc + +--- +*Phase: 09-backend-admin-authentication-and-schema-pipeline* +*Completed: 2026-09-24*