diff --git a/docs/project/backlog/README.md b/docs/project/backlog/README.md index e72f6c8..883ff30 100644 --- a/docs/project/backlog/README.md +++ b/docs/project/backlog/README.md @@ -73,6 +73,7 @@ for its existing violations, so every WP ends green. | [WP-26](WP-26-org-template-editor.md) | Admin org-template editor | 6 · Brief v2 | done | | [WP-27](WP-27-brief-ux-layer.md) | Brief UX layer (undo/redo, standaardbrief, diff) | 6 · Brief v2 | todo | | [WP-28](WP-28-brief-v2-demo-polish.md) | Brief v2 demo polish (scenarios, e2e, docs) | 6 · Brief v2 | todo | +| [WP-29](WP-29-stamdata-beheer-editor.md) | Stamdata beheer editor (low-code, PR-emitting) | follow-on · ADR-0004 | done | Sequencing dependencies (stated in the WPs too): 01 before 10–15 (axe covers story churn); 03/04 before 05–09 (boundaries stop new violations during refactors); 06 before 07 (typed diff --git a/docs/project/backlog/WP-29-stamdata-beheer-editor.md b/docs/project/backlog/WP-29-stamdata-beheer-editor.md new file mode 100644 index 0000000..fc204d9 --- /dev/null +++ b/docs/project/backlog/WP-29-stamdata-beheer-editor.md @@ -0,0 +1,80 @@ +# WP-29 — Stamdata beheer editor (low-code, PR-emitting) + +Status: done (0e77faf) +Phase: follow-on — ADR-0004 realization (not part of the 2026-07-02 showcase audit) + +## Why + +ADR-0004 (stamdata-as-code) named a **future low-code editor that commits a PR** as its +mitigation for "a non-developer may need dev assistance to edit C#", and floated a +**data-file format** as the alternative to typed-C# constants when hand-editing ergonomics +outweigh maximal compile-time safety. This WP realizes both: an admin-only maintenance editor +that reads the stamdata catalog, edits rows in a grid, and produces the edited JSON data-file +the admin drops into the repo — the existing CI build + `StamdataValidationTests` stay the +authority. No production database, no runtime write path (ADR-0004 unchanged). + +## Read first + +- ADR-0004 (`docs/reference/architecture/0004-stamdata-as-code.md`) — the model this obeys. +- ADR-0001 (BFF-lite + decision DTOs) — the endpoints are screen-shaped, admin-gated reads. +- `src/app/brief/**` (WP-23/26) — the org-template admin editor is the closest prior art + (root store + machine + capability guard + admin role). + +## Decisions (pre-made, don't relitigate) + +- **Read-only endpoints only.** `GET /stamdata` (catalog) and `GET /stamdata/{table}?peildatum=` + (schema + rows). No POST/PUT/DELETE — the edit lands as a reviewed PR, not a write. The + `stamdata:edit` capability / `CanEditStamdata` gate the *reads* (naming is the enforce-twin + of a future edit capability; deliberate). +- **Generic, schema-driven.** One endpoint pair + one grid editor serve every table. Columns + are reflected from the typed record (`StamdataTable.Of`); the FE renders inputs by column + type (`date`/`number`/`enum`/`text`). A new table is one line in `StamdataCatalog` — no new + endpoint, UI, or test. (Catalog of one today; this is the shape ADR-0004 prescribed.) +- **Data-file format for `professions`.** `professions.json` (embedded resource) replaces the + hardcoded C# dictionary, deserialized into `ProfessionMapping`. This trades compile-time + *value* checking (gate #1) for editor ergonomics — the value gate becomes + `StamdataValidationTests` (gate #2), exactly the trade-off ADR-0004's consequences listed. +- **Valid-time.** `geldigVan`/`geldigTot` (half-open `[van, tot)`); a table is temporal iff it + has both columns. `peildatum` previews "which rows applied on date X". `Professions.ByProgram` + preserves pre-valid-time behaviour by filtering to rows active today. +- **Apply path = download → PR.** The editor's `download()` serializes the draft to + `{table}.json`; the admin commits it. `mutation-command` is intentionally not used. +- Admin-only, resource-independent authz — same shape as org-template management (role IS the + decision), denials audited (no PII). + +## Files + +- Backend: `backend/src/BigRegister.Api/Stamdata/{StamdataCatalog,StamdataTable,StamdataFile,ProfessionMapping}.cs` (new), `Professions.cs` (now loads the data-file), `professions.json` (new), `BigRegister.Api.csproj` (embed `Stamdata\*.json`); `Program.cs` (two GET endpoints + `StamdataAdmin` gate), `Contracts/Dtos.cs` (3 DTOs), `Domain/Authorization/Authz.cs` (`stamdata:edit` + `CanEditStamdata`); tests `StamdataEndpointTests.cs` (new), `StamdataValidationTests.cs` (generic `Every_catalog_table_is_valid`). +- Frontend: `src/app/beheer/**` (contracts / domain + specs / infrastructure + spec / application / ui + organism story); `app.routes.ts` (guarded lazy route), `shared/domain/capability.ts` + `shared/infrastructure/me.adapter.ts` (`stamdata:edit`), `eslint.config.mjs` (`beheer` boundary rules), `tsconfig.json` (`@beheer/*` alias); regenerated `backend/swagger.json` + `src/app/shared/infrastructure/api-client.ts`. + +## Acceptance criteria + +- [x] `GET /stamdata` and `GET /stamdata/{table}` return admin-only (403 + audit otherwise). +- [x] `professions` served from `professions.json`; `ProfessionMapping` typed; behaviour of + `Professions.ByProgram` unchanged for all-current rows. +- [x] Generic build gate: `Every_catalog_table_is_valid` covers every catalog table (keys + non-blank, no overlapping validity, well-formed windows). +- [x] `beheer/stamdata` route capability-guarded; page shows denial for non-admin; grid + renders from reflected schema; edits update dirty/change-count; `download()` yields a + valid `{table}.json`; `peildatum` before 2000-01-01 → zero rows. +- [x] Full gate GREEN both sides; `npm run gen:api` leaves no drift; new stamdata story passes axe. + +## Verification + +`cd backend && dotnet test && dotnet format --verify-no-changes`; `npm run lint && npm run +check:tokens && npm test && npm run build && npm run build-storybook && npm run test-storybook:ci`; +`npm run gen:api && git diff --exit-code -- backend/swagger.json src/app/shared/infrastructure/api-client.ts`. +Live: `/beheer/stamdata?role=admin` renders + edits + downloads; without `?role=admin` denies. + +## Out of scope + +- Runtime persistence of edits (would contradict ADR-0004) — the download-to-PR path is the design. +- Migrating `PolicyQuestions` / document-categories into the catalog (each is a later one-liner). +- A write-back "commit a PR on the admin's behalf" integration (the `download()` seam is where it slots in). + +## Risks + +- Data-file weakens compile-time value safety for `professions` — mitigated by + `StamdataValidationTests` running in CI (a bad value fails the build, never prod). +- Generic reflection assumes the record's first property is the key and camelCase JSON names — + documented conventions in `StamdataTable`; covered by the endpoint schema test. diff --git a/docs/reference/architecture/0004-stamdata-as-code.md b/docs/reference/architecture/0004-stamdata-as-code.md index 7dde07c..f432799 100644 --- a/docs/reference/architecture/0004-stamdata-as-code.md +++ b/docs/reference/architecture/0004-stamdata-as-code.md @@ -35,10 +35,17 @@ production database, never runtime-editable. 1. **One home, typed.** Business-editable reference data lives in the `BigRegister.Stamdata` namespace (`backend/src/BigRegister.Api/Stamdata/`), one file per - concern, as plain typed C# data (records / dictionaries). Separate the **data** (what the - business tunes) from the **rules** (dev-owned logic that consumes it): the profession - *table* is `Stamdata.Professions`; the *rule* "an English diploma needs a B2 question" - stays in `DiplomaRules`. + concern. A table lives **either** as plain typed C# data (records / dictionaries) **or** as + a typed JSON data-file deserialized into a record (`professions.json` → `ProfessionMapping`, + loaded via `StamdataFile`). Both are checked-in config-as-code, gated the same way; the + data-file trades the compiler's *value* check (gate #1 sees only the shape, not a wrong + `beroep`) for hand-editing ergonomics and the low-code editor below — the value gate becomes + `StamdataValidationTests`. Separate the **data** (what the business tunes) from the **rules** + (dev-owned logic that consumes it): the profession *table* is `Stamdata.Professions`; the + *rule* "an English diploma needs a B2 question" stays in `DiplomaRules`. Tables may carry + **valid-time** (`geldigVan`/`geldigTot`, half-open `[van, tot)`); `StamdataCatalog` + + `StamdataTable.Of` describe every table generically (columns reflected from the record) + so one endpoint pair and one grid editor serve all of them. 2. **Served unchanged.** The existing BFF-lite endpoints keep serving this data (`/duo/diplomas`, `/intake/policy`, `/uploads/categories`, …). No frontend change — the FE still renders decisions. @@ -54,7 +61,7 @@ production database, never runtime-editable. | Kind | Home | Gate | | --- | --- | --- | -| Reference tables + tunable numbers (professions↔diplomas, thresholds, policy questions, document categories) | `Stamdata/` typed C# | compiler + `StamdataValidationTests` | +| Reference tables + tunable numbers (professions↔diplomas, thresholds, policy questions, document categories) | `Stamdata/` typed C# **or** typed JSON data-file (`professions.json`), optionally valid-timed | compiler (shape; + values when C#) + `StamdataValidationTests` (values, references, validity windows) | | User-facing UI copy | `$localize` → `src/locale/*.xlf` | build (`i18nMissingTranslation: error`) | | Letter / brief passage content | config-as-code in the backend (seed content), **not** the DB | compiler + endpoint tests | @@ -80,3 +87,9 @@ everyone. Stamdata (the rules and reference tables the whole register runs on) s which now consumes both (behaviour unchanged), guarded by `StamdataValidationTests`. Document-category definitions follow the same pattern as the obvious next step; not moved yet. +- **Shipped as a follow-on (WP-29):** the "future low-code editor" and "data-file format" this + ADR floated are now real. `professions` moved to `professions.json` (typed, valid-timed) and + the generic `StamdataCatalog`/`StamdataTable`/`StamdataFile` model plus read-only, admin-gated + `GET /stamdata` endpoints back an Angular `beheer/stamdata` editor. It is **not** a runtime + write path: the admin edits a grid and downloads the edited JSON to commit as a reviewed PR — + the compile/validation gate stays the authority, so this ADR's core decision is unchanged.