docs(adr): land ADR-C-001, ADR-C-003, ADR-C-007 and ADR-C-009

The architect approved the four ADR-fix tickets. All four change what the
architecture documents claim. No code changes.

ADR-0001, ADR-C-001: the worked example claimed the POC has no real backend.
It rewrites against `backend/src/BigRegister.Api`. Every path it named is
repointed. The out-of-scope list drops two discharged bullets: 33 `parse*`
boundaries exist, and `npm run gen:api` is real.

ADR-0001, ADR-C-003: a new section states that the generated client is the wire
contract. A hand-written `contracts/*.dto.ts` is the exception for two cases
only. The four survivors stay, because NSwag emits every property as optional
and flattens `RegistrationStatusDto` into five optional strings. The `parse*`
trust boundary stays mandatory, because a generated type is a compile-time
claim about the wire and not a runtime guarantee.

ADR-0003, ADR-C-007: four paths moved in WP-67 and are repointed. Point 4 kept
the principle and changed its example to `skeleton` and `spinner`. Two of its
claims were false and the amendment says so: `app-alert` wraps the vendored
`.feedback` classes, and `site-header` composes the vendored `.titlebar`.

ADR-0004, ADR-C-009: the exception section states a four-part test instead of
one named exception. `OrgTemplateStore` and `FeatureFlagStore` both pass it. RB-07
gated this ticket, because clause 4 needs an audited allow path. RB-07 landed
that, so the ADR does not ratify a control that the code lacks.

Three tickets need a matching CLAUDE.md correction in the same diff. CLAUDE.md
section 2 loses the false `alert` example. Section 4 gets the generated-client
rule and the four-part test.

Two findings were wrong. ADR-C-001 asked to keep an out-of-scope bullet that
reads "SessionStore is in-memory". The session persists to `localStorage` now,
so the bullet covers multi-tab sync only. ADR-C-007 flagged one half of point 4
and missed that the other half is equally false.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-08-27 18:29:05 +02:00
co-authored by Claude Opus 5
parent 7fbac8fca5
commit 25a5d415a5
10 changed files with 458 additions and 68 deletions
@@ -20,7 +20,7 @@ was neither isolated nor validated:
- All reference data and thresholds are **compiled-in C# constants**, served through
screen-shaped BFF-lite endpoints; the frontend renders decisions and holds no reference
data (ADR-0001).
- User-facing UI copy is already **`$localize`** (`src/locale/*.xlf`) — git-tracked, and a
- User-facing UI copy is already **`$localize`** (`apps/<app>/src/locale/*.xlf`) — git-tracked, and a
second locale is a translation file, not a code change. That is already the compile-time
model for text.
- The profession↔diploma map lived as a _private_ `Dictionary` inside `DiplomaRules`, mixed
@@ -62,17 +62,49 @@ production database, never runtime-editable.
| Kind | Home | Gate |
| ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| 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`) |
| User-facing UI copy | `$localize` → `apps/<app>/src/locale/*.xlf` | build (`i18nMissingTranslation: error`) |
| Letter / brief passage content | config-as-code in the backend (seed content), **not** the DB | compiler + endpoint tests |
### The deliberate exception: org-templates
### The deliberate exception: operational configuration
Per-organization letterhead (return address, footer, signature, margins) **is**
runtime-editable in SQLite, via the org-template admin editor (WP-23/26). That is
intentional and does not contradict this ADR: it is _operational configuration_ owned by an
admin persona, versioned with publish/rollback inside the app, and specific to one
sub-organization's identity — not the shared business rules a wrong value would break for
everyone. Stamdata (the rules and reference tables the whole register runs on) stays code.
"Never runtime-editable" above is the rule for **stamdata** — the shared reference tables
and business rules the whole register runs on. It is not a ban on all persisted
configuration. Some configuration is operational rather than business-rule, and belongs to
an admin persona at runtime.
This section states the **test** rather than a list, so the next surface can check itself
instead of arguing by analogy. Runtime-editable persistence is permitted only when all four
hold:
1. **The catalog lives in code.** What may be set — the keys, the schema, the defaults,
the descriptions — is compiled in and reviewed through git. The store holds values, never
the definition of what a value means.
2. **An unknown or unlisted key fails closed.** A row the code catalog does not know cannot
invent a setting, enable a feature, or be written. A bad row is inert, not authoritative.
3. **The value is operational.** Per-organisation identity, or an on/off rollout switch —
not a shared business rule whose wrong value breaks the register for everyone. This is the
clause that keeps stamdata out.
4. **Writes are admin-capability-gated and audited.** The write path goes through an `Authz`
capability gate, and the gate records the decision — allow as well as deny — in
`AuthzAuditStore`.
**Two surfaces pass this test today.**
| Surface | (1) catalog in code | (2) fails closed | (3) operational | (4) gated + audited |
| ----------------------------- | ----------------------------------------------- | ------------------------------------------------------------ | --------------------------------- | ------------------------------- |
| `OrgTemplateStore` (WP-23/26) | the `OrgTemplateDto` shape + `OrgTemplateRules` | unknown `subOrgId` → `null` → the endpoint 404s | one sub-organisation's letterhead | `OrgAdmin` → `orgtemplate:edit` |
| `FeatureFlagStore` (WP-47) | `Domain/Features/FeatureFlags.Catalog` | unknown key → `Set` returns false (404); `IsEnabled` → false | an on/off rollout switch | `FlagsAdmin` → `flags:manage` |
Clause (4) became true for both only with RB-07, which moved `AuditAuthz` from each gate's
deny branch into the gate itself so the allow path is recorded too. Before that, both
surfaces were gated and **not** audited, and this ADR would have ratified a control the code
did not implement.
Org-templates also carry publish/rollback versioning inside the app, which is stronger than
the test requires but not part of it.
Stamdata itself — the rules and reference tables — fails clause (3) by construction and
stays code.
## Consequences