diff --git a/.dependency-cruiser.base.js b/.dependency-cruiser.base.js
index ff945cd..b6f1e0b 100644
--- a/.dependency-cruiser.base.js
+++ b/.dependency-cruiser.base.js
@@ -100,9 +100,9 @@ module.exports = function buildConfig(contextAllowed, appName, tsConfigFileName)
{
name: 'apiclient-infrastructure-only',
comment:
- 'The generated ApiClient is a value only inside infrastructure/ (+ shared/upload); elsewhere type-only.',
+ 'The generated ApiClient is a value only inside infrastructure/; elsewhere type-only.',
severity: 'error',
- from: { pathNot: '/infrastructure/|^libs/shared/src/upload/' },
+ from: { pathNot: '/infrastructure/' },
to: {
path: '^libs/shared/src/infrastructure/api-client\\.ts$',
dependencyTypesNot: ['type-only'],
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 011aab4..21fedab 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -202,6 +202,11 @@ jobs:
# run manually against backend/openzaak/ (see its README), never in CI.
- run: dotnet test backend/BigRegister.slnx --filter "Category!=Integration"
if: needs.changes.outputs.backend == 'true'
+ # RB-14/BIO-016: `npm audit --omit=dev` covers only the frontend; the .NET dependency
+ # tree was entirely unscanned. The script — not a bare `dotnet list` — is the gate,
+ # because `dotnet list package --vulnerable` exits 0 even on a High advisory.
+ - run: ./scripts/dotnet-audit.sh
+ if: needs.changes.outputs.backend == 'true'
e2e:
needs: changes
diff --git a/.gitignore b/.gitignore
index 10477d4..022d93b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,3 +58,7 @@ backend/openzaak/seeded.env
# WP-55: render-prod-secrets.sh's output — the real client secret, never committed
backend/openzaak/setup_configuration/data.prod.yaml
+
+# Agent git worktrees (Claude Code `isolation: "worktree"`) — full checkouts of
+# this repo nested inside it; never commit one.
+.claude/worktrees/
diff --git a/.prettierignore b/.prettierignore
index deed558..07cb7ce 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -4,6 +4,11 @@ storybook-static*/
coverage/
.angular/
+# Agent git worktrees — full checkouts of this repo nested inside it, so an
+# unignored `prettier --check .` walks into every one of them (and reports the
+# vendored CIBG files that the top-level ignore already excludes).
+.claude/worktrees/
+
# Lockfile
package-lock.json
@@ -21,3 +26,7 @@ plop-templates/
# Backend is formatted by `dotnet format`, not prettier
backend/
+
+# Agent prompts — their exact wording is the input, reflowing markdown edits the prompt
+docs/project/refactor-backlog-setup/agents/
+docs/project/refactor-backlog-setup/refactor-backlog/final-prompts/
diff --git a/CLAUDE.md b/CLAUDE.md
index 88fa614..3c7401a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -124,8 +124,10 @@ than hardcoding one app's content — the two apps' primary nav genuinely differ
should be **composition of existing blocks** — adding building blocks is the
exception, not the default. Atoms are thin wrappers over CIBG Huisstijl (Bootstrap 5.2)
CSS classes (`btn`, `form-control`, `card`, …); we own only a small typed `input()` API,
-the design system does the visuals. (Where CIBG lacks a class — e.g. `alert` — the atom is a
-small hand-rolled surface built from the token bridge; see ADR-0003.)
+the design system does the visuals. (Where CIBG lacks a class — e.g. `skeleton`,
+`spinner` — the atom is a small hand-rolled surface built from the token bridge and carries a
+`// CIBG-GAP EXTENSION:` marker; see ADR-0003. `alert` is **not** such a case: it wraps the
+vendored `.feedback feedback-*` classes.)
### 3. State: make illegal states unrepresentable
@@ -175,8 +177,14 @@ herregistratie eligibility) or _config value_ (server sends threshold, FE applie
for instant feedback, server re-validates as authority — e.g. scholing threshold).
FE keeps only **format** validation, never as authority.
-DTO lives in `contracts/`; a hand-written `parse*`/`toDomain` in `infrastructure/`
-validates the untrusted shape and maps DTO → domain. Wiring a real .NET backend
+The generated client
+(`libs/shared/src/infrastructure/api-client.ts`, `npm run gen:api`, drift-checked in CI) **is**
+the wire contract — consume its types directly, as 19 of the 20 adapters do. A hand-written
+`contracts/*.dto.ts` is the exception, only where codegen does not reach the endpoint or types
+it too loosely (the four survivors are all the latter — the generator emits every property as
+optional and flattens unions); such a file must still import nothing. Either way a hand-written
+`parse*`/`toDomain` in `infrastructure/` validates the untrusted shape and maps DTO → domain —
+**a generated type is a compile-time claim about the wire, not a runtime guarantee.** Wiring a real .NET backend
touches only `infrastructure/` + `contracts/` (see ARCHITECTURE §6). Server-owned
rules live **only** on the server, with no FE mirror to drift from it — the FE may
mirror a server-supplied _value_ (a threshold, a bound) for instant feedback, but
@@ -185,8 +193,12 @@ never reimplements the _algorithm_.
**Business-tunable reference data ("stamdata") is config-as-code, not a DB.** Tables the
business controls (profession↔diploma map, thresholds, policy-question text) live as typed
C# in `backend/.../Stamdata/`, validated at build by `StamdataValidationTests` (a bad edit
-fails CI, never prod) — never runtime-editable. Org-templates are the deliberate exception
-(operational per-org config in SQLite). UI copy is `$localize`. See ADR-0004.
+fails CI, never prod) — never runtime-editable. Operational configuration is the deliberate
+exception, and ADR-0004 states it as a four-part test rather than a list: the catalog lives in
+code, an unknown key fails closed, the value is operational rather than a shared business rule,
+and writes are admin-capability-gated **and** audited. Two surfaces pass it today —
+`OrgTemplateStore` (per-org letterhead) and `FeatureFlagStore` (rollout switches), both in
+SQLite. A third surface must pass the same test, not argue by analogy. UI copy is `$localize`. See ADR-0004.
### 5. Testing
@@ -214,6 +226,17 @@ regardless of which atomic layer it is (a context organism doesn't get its own
- **Naming:** shared/reusable UI is **English** (language-agnostic: `button`,
`wizard-shell`); domain contexts are **Dutch** (`registratie`, `herregistratie`,
`*.machine.ts`). Pick the language by which side of the seam the code is on.
+- **English prose uses Simplified Technical English (STE).** This covers documentation,
+ code comments, commit messages, ADRs, and the backlog notes. One idea per sentence;
+ 20 words or fewer in a procedure, 25 in a description. Active voice, present tense.
+ One word for one meaning — pick a term and repeat it, do not vary it for style. Keep
+ articles ("the test fails"). Three nouns together at most. No idioms and no humour.
+ Six sentences per paragraph at most. Write a procedure as numbered steps, one action
+ per step.
+ **STE governs form, not content.** Split a long sentence; never drop a caveat, a
+ measurement, or a precise term to make it shorter.
+ **STE does not apply to** Dutch identifiers, `$localize` copy, quoted output, or
+ existing documents you are not already editing.
- **User-facing copy = `$localize`.** Every user-visible string is wrapped in Angular's
first-party `$localize` (no third-party i18n lib), with a stable custom id
(`` $localize`:@@context.key:Tekst` ``). Source locale is `nl`; a second locale is a
diff --git a/angular.json b/angular.json
index 30dde9d..2e55d08 100644
--- a/angular.json
+++ b/angular.json
@@ -106,7 +106,7 @@
"**/*.spec.ts",
"**/*.stories.ts",
"**/contracts/**",
- "libs/shared/src/infrastructure/api-client.ts",
+ "**/infrastructure/api-client.ts",
"apps/ssp/src/main.ts",
"**/*.testing.ts",
"**/*.d.ts"
@@ -235,7 +235,7 @@
"**/*.spec.ts",
"**/*.stories.ts",
"**/contracts/**",
- "libs/shared/src/infrastructure/api-client.ts",
+ "**/infrastructure/api-client.ts",
"apps/behandelportal/src/main.ts",
"**/*.testing.ts",
"**/*.d.ts"
@@ -293,7 +293,7 @@
"**/*.spec.ts",
"**/*.stories.ts",
"**/contracts/**",
- "src/infrastructure/api-client.ts",
+ "**/infrastructure/api-client.ts",
"src/test-entry.ts",
"**/*.testing.ts",
"**/*.d.ts"
@@ -332,6 +332,7 @@
"**/*.spec.ts",
"**/*.stories.ts",
"**/contracts/**",
+ "**/infrastructure/api-client.ts",
"src/test-entry.ts",
"**/*.testing.ts",
"**/*.d.ts"
diff --git a/apps/behandelportal/src/app/auth/application/session.store.ts b/apps/behandelportal/src/app/auth/application/session.store.ts
index 47374d3..d865c3a 100644
--- a/apps/behandelportal/src/app/auth/application/session.store.ts
+++ b/apps/behandelportal/src/app/auth/application/session.store.ts
@@ -1,58 +1,50 @@
import { Injectable, computed, effect, inject, signal } from '@angular/core';
-import { Result } from '@shared/kernel/fp';
-import { Session } from '../domain/session';
-import { DigidAdapter } from '../infrastructure/digid.adapter';
+import { Principal, parseStoredPrincipal } from '../domain/principal';
+import { MedewerkerAdapter } from '../infrastructure/medewerker.adapter';
const STORAGE_KEY = 'session-v1';
-/** Restore a persisted session (best-effort; corrupt entry → logged out).
- G2: validate the shape before trusting it. G1: the BSN is never persisted
- (see the effect below), so a restored session carries an empty one — it is
- unused after login; only `naam` is shown in the chrome. */
-function restore(): Session | null {
- try {
- const raw = localStorage.getItem(STORAGE_KEY);
- if (!raw) return null;
- const parsed = JSON.parse(raw) as Partial
+ U meldt zich aan via de SSO van uw organisatie — er is geen wachtwoord nodig.
+ " : ">());
e.Property(b => b.Status).HasConversion(Json
").Append(Enc(section.Title)).Append("
");
foreach (var block in section.Blocks)
- RenderParagraphs(sb, block.Content.Paragraphs, defs);
+ RenderParagraphs(sb, block.Content.Paragraphs, defs, at);
sb.Append("
([^<]+)
").Groups[1].Value; + } + [Fact] public void Render_matches_the_golden_file() { @@ -88,6 +126,29 @@ public class LetterHtmlTests Assert.Equal(golden, html); } + [Fact] + public void Render_resolves_the_body_datum_placeholder_from_the_given_at_not_the_wall_clock() + { + const string historicalAt = "2019-03-14T08:00:00.0000000+00:00"; + + var html = LetterHtml.Render(FixtureBriefWithDatumInBody(), Template, historicalAt, watermark: false); + + Assert.Equal("14 maart 2019", ExtractBodyDatumParagraph(html)); + } + + [Fact] + public void Render_keeps_the_letterhead_date_and_the_body_datum_in_agreement_for_a_historical_at() + { + // A historical `at` (an archive re-render, a back-dated letter) is the case + // where the letterhead and the body datum placeholder could disagree within + // one document, if the body still read the wall clock (TE-007). + const string historicalAt = "2019-03-14T08:00:00.0000000+00:00"; + + var html = LetterHtml.Render(FixtureBriefWithDatumInBody(), Template, historicalAt, watermark: false); + + Assert.Equal(ExtractLetterheadDate(html), ExtractBodyDatumParagraph(html)); + } + [Fact] public void Every_letter_prefixed_class_exists_in_letter_css() { diff --git a/backend/tests/BigRegister.Tests/OrgTemplateEndpointTests.cs b/backend/tests/BigRegister.Tests/OrgTemplateEndpointTests.cs index 01784b9..dcf26b2 100644 --- a/backend/tests/BigRegister.Tests/OrgTemplateEndpointTests.cs +++ b/backend/tests/BigRegister.Tests/OrgTemplateEndpointTests.cs @@ -58,8 +58,8 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas public async Task Publish_increments_the_version() { ResetStores(); - // One unsent brief for this sub-org (GetOrCreate on first read). - await _client.GetAsync("/api/v1/brief"); + // One unsent brief for this sub-org (RB-23: GET no longer seeds — create explicitly). + await _client.PostAsync("/api/v1/brief/reset", null); var res = await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/publish", role: "admin")); res.EnsureSuccessStatusCode(); @@ -74,7 +74,7 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas public async Task Publish_appends_to_the_version_history() { ResetStores(); - await _client.GetAsync("/api/v1/brief"); + await _client.PostAsync("/api/v1/brief/reset", null); // RB-23: GET no longer seeds — create explicitly var res = await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/publish", role: "admin")); res.EnsureSuccessStatusCode(); @@ -87,8 +87,8 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas public async Task Publish_counts_the_unsent_briefs_it_affects() { ResetStores(); - // One unsent brief for this sub-org (GetOrCreate on first read). - await _client.GetAsync("/api/v1/brief"); + // One unsent brief for this sub-org (RB-23: GET no longer seeds — create explicitly). + await _client.PostAsync("/api/v1/brief/reset", null); var res = await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/publish", role: "admin")); res.EnsureSuccessStatusCode(); @@ -154,7 +154,8 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas private async Task WalkBriefToSentThenRepublish() { ResetStores(); - var brief = (await _client.GetFromJsonAsync