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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user