Commit Graph
25 Commits
Author SHA1 Message Date
ehoandClaude Sonnet 5 e7156c5132 feat(WP-67): merge behandelportal into this repo as a monorepo
Restructures into apps/ssp + apps/behandelportal (two Angular projects)
plus libs/shared + libs/beheer (cross-app libraries), replacing WP-61's
separate sibling repo. That split had already produced real drift: a
hand-vendored copy of the backend's OpenAPI doc, a shared/ui+layout tree
forked and silently diverging (7 files), and beheer + the styles.scss
token bridge duplicated byte-for-byte across both repos.

- git mv the SSP's src/app/* into apps/ssp/; fold shared/, beheer/,
  environments/, the Storybook docs/*.mdx, and styles.scss into
  libs/shared + libs/beheer (all confirmed identical between the two
  repos before merging). auth stays deliberately duplicated per
  ADR-0002 (actor-specific, expected to diverge) - amended there.
- One generated API client (libs/shared), no more vendored swagger.json.
- .dependency-cruiser split into a base factory + one config per app,
  and Storybook into .storybook-ssp/.storybook-behandelportal - both
  forced by the @auth/* alias resolving to different directories per app.
- SiteHeaderComponent/ShellComponent gained HEADER_NAV_ITEMS/
  HEADER_ADMIN_LINKS/DEBUG_PANEL injection tokens so each app supplies
  its own nav/admin-links/dev-panel instead of one being hardcoded.
- CLAUDE.md, ARCHITECTURE.md, dependencies.md, and ADR-0002 updated;
  WP-67 backlog entry documents the full decision trail.

npm run ci green (lint, dep:check x2, 360 tests across ssp/
behandelportal/shared/beheer, both localized builds, backend tests,
snippet + api-client drift); both dev servers, both Storybook
instances, and docker compose verified working.

The old sibling repo (/home/eho/repos/behandelportal) is left
untouched, not deleted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 21:01:57 +02:00
ehoandClaude Sonnet 5 e7db69d8a9 perf(ci): path-filter jobs by which side changed (WP-30 #3)
New `changes` job (dorny/paths-filter, pinned to SHA) computes frontend/
backend outputs; every downstream job gates its real steps on the relevant
output(s) instead of being skipped as a whole job. Conservative "skip steps,
not jobs" variant: every job still runs and reports a status (checkout always
executes) even when its side is untouched, so a required-status-check never
waits on a job that never started — the tradeoff the WP itself flagged as the
open risk of this item. e2e/semgrep/api-client-drift gate on either side
(they exercise both). `.github/workflows/**` counts as both sides, so a CI
change always gets a full run. Validated with `actionlint` (0 issues) and a
local YAML parse; the actual skip behavior can only be confirmed on a real
Gitea PR run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 09:28:55 +02:00
ehoandClaude Sonnet 5 e02e8ce058 perf(ci): split lint into its own fast-fail job (WP-30 #4)
New `lint` job (lint + format:check + check:tokens) runs in parallel with
`frontend`, reporting in ~5 min instead of waiting on the full
test:coverage/ng build --localize/npm audit chain. Depends on the item-1
node_modules cache (otherwise this would duplicate a full npm ci for no
speed benefit, per the WP's own note).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 09:26:41 +02:00
ehoandClaude Sonnet 5 e46b87b26d perf(ci): cache node_modules, skip npm ci on a hit (WP-30 #1)
actions/cache on node_modules keyed by package-lock.json hash, across all 4
npm-based jobs (frontend, storybook-a11y, e2e, api-client-drift). The if:
guard is what makes this a real speedup: npm ci deletes-then-reinstalls
unconditionally, so caching node_modules alone does nothing unless the
install step is skipped outright on a cache hit.

Unverifiable from this environment — CI timing needs a real Gitea run to
confirm; shipped as its own commit so a red run is easy to bisect/revert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 09:26:05 +02:00
ehoandClaude Sonnet 5 526da76617 fix(ci): triage semgrep findings, make the gate blocking (WP-30 #6)
Local semgrep run found 25 findings (not the WP's remembered 27 — already-stale
by the time this ran): dependabot cooldown, npm min-release-age, every GitHub
Action pinned to a full commit SHA (dependabot's existing github-actions
ecosystem entry keeps these current), and 2 detect-non-literal-regexp findings
in e2e/create-ssp.mjs suppressed as false positives (non-attacker-controlled
input: a test's own captured version number, a local generator's CLI arg).
`semgrep scan` now runs with `--error`, a real blocking gate instead of
report-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 09:25:21 +02:00
ehoandClaude Sonnet 5 5cb3e1a9f0 feat(zgw): docker OpenZaak integration-test harness (WP-54)
Opt-in docker-compose (postgres+redis+OpenZaak, no celery/nginx) +
bootstrap-catalogus.sh seed a real OpenZaak instance; OpenZaakIntegrationTests
(Category=Integration, excluded from default dotnet test/CI) proves the ZGW
seam against it for the first time. That live run caught a real bug:
ZgwHttpClient never sent Content-Crs/Accept-Crs headers, so every write would
412 against a spec-compliant OpenZaak — fixed alongside the harness.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 09:08:35 +02:00
ehoandClaude Sonnet 5 f356dc7329 fix(storybook): silence NODE_ENV DefinePlugin warning; revert CI memory cap to 4g
CI / frontend (push) Successful in 2m22s
CI / backend (push) Successful in 1m46s
CI / storybook-a11y (push) Successful in 7m39s
CI / e2e (push) Successful in 4m1s
CI / semgrep (push) Successful in 1m2s
CI / api-client-drift (push) Successful in 1m58s
.storybook/main.ts: webpack core's auto-DefinePlugin (from optimization.nodeEnv)
and Storybook's own preset both define process.env.NODE_ENV on the same
compile, triggering a "Conflicting values" warning even though both resolve
to "development" locally. Disable the redundant one via webpackFinal.

ci.yml: revert the storybook-a11y container's memory cap 6g -> 4g. The 6g
bump was based on an unconstrained local RSS measurement (~5.8GB) that
doesn't reflect real behavior under a cgroup cap. Verified directly: running
this job's exact steps (npm ci, playwright install, build-storybook,
test-storybook:ci) in `docker run --cpus=2 --memory=4g --memory-swap=4g
node:24-bookworm` completes clean, no OOM, 62/62 suites passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 07:47:25 +02:00
ehoandClaude Sonnet 5 c4dd846fbb fix(ci): unbreak backend format, storybook-a11y, and e2e jobs
CI / frontend (push) Successful in 2m12s
CI / backend (push) Successful in 1m37s
CI / e2e (push) Successful in 3m33s
CI / storybook-a11y (push) Successful in 8m23s
CI / semgrep (push) Successful in 1m4s
CI / api-client-drift (push) Successful in 1m52s
- backend: dotnet format the WP-51 migration (2-space indent, no BOM)
  to match .editorconfig — dotnet format --verify-no-changes was failing.
- storybook: stub FeatureFlagStore (WP-47) in shell/site-header stories
  alongside AccessStore, fixing NG0201 no-provider errors; bump the
  storybook-a11y container's memory cap 4g→6g (build-storybook +
  compodoc measured ~5.8GB peak RSS, leaving too little headroom).
- backend: fix a startup-breaking bug in the new (WP-52) POST
  /zgw/notificaties handler — it took ZgwOptions as a minimal-API
  parameter, which isn't registered in DI, so ASP.NET's endpoint-table
  build threw on every request once the route was registered (incl.
  /swagger, which is why Playwright's webServer health check timed
  out). Close over the existing `zgw` local instead.
- e2e: brief-v2.spec.ts's "Voorbeeld" button locator was ambiguous
  once a second "Voorbeeld met testwaarden" button existed (Playwright
  name matching is substring-based) — added `exact: true`. Also fixed
  the sent-letter preview flow to match app-letter-composer's actual
  behavior (single click → fetch, no in-page dialog, unlike
  app-behandel-scherm's), and fixed a watermark assertion that checked
  for the always-present `.preview-watermark` CSS class name instead
  of the conditionally-rendered "VOORBEELD" marker text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 07:18:07 +02:00
ehoandClaude Opus 4.8 a37dfd47a4 ci: cap storybook-a11y resources + document the ACL learnings
CI / frontend (push) Successful in 3m22s
CI / backend (push) Canceled after 0s
CI / e2e (push) Canceled after 0s
CI / semgrep (push) Canceled after 0s
CI / api-client-drift (push) Canceled after 0s
CI / storybook-a11y (push) Canceled after 24s
- test-storybook:ci gets --maxWorkers=2 so the Jest runner stops spawning one
  headless Chromium per core and OOM-ing the Gitea runner host (the root cause).
- storybook-a11y job gains a container resource ceiling (--cpus=2 --memory=4g) as
  a belt-and-suspenders guardrail; noted it needs a docker-mode act_runner.
- openzaak-integration.md: add "Anti-corruption layer — two nested boundaries"
  teaching section (BFF ACL vs upstreams + FE ACL vs BFF, and the principles).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 15:21:28 +02:00
ehoandClaude Opus 4.8 19f2e9b734 feat(showcase): WP-39 — linked code snippets + teaching animations
CI / frontend (push) Successful in 2m5s
CI / storybook-a11y (push) Successful in 5m56s
CI / backend (push) Successful in 1m30s
CI / e2e (push) Successful in 2m51s
CI / semgrep (push) Successful in 1m1s
CI / api-client-drift (push) Successful in 2m6s
Anti-drift snippets: `?raw` isn't supported by Angular's esbuild build, so real code
is exposed via // #region showcase:<name> markers in source (registration/remote-data/
postcode/change-request.machine/intake.machine) → scripts/gen-snippets.mjs → committed
snippets.generated.ts, with a CI drift gate so the shown code is the shipped code. The
/concepts page renders the 5 real snippets (union/fold/parse/machine/steps) with a source
caption + a tiny dependency-free highlightTs (+spec); deliberately-wrong illustrations stay
authored. Teaching motion: active state node pops on transition + parse result animates in
(existing reduced-motion-safe toolkit). Generated file prettier-ignored (like api-client).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 14:49:52 +02:00
ehoandClaude Opus 4.8 f8cd77a48a feat(test): WP-46 — Vitest coverage (report-only)
Enable coverage via the @angular/build:unit-test builder's first-class options: a
`coverage` configuration on the test target (v8 provider, text-summary/html/lcov,
excludes for specs/stories/contracts/generated client) + `npm run test:coverage`.
Report-only (no thresholds) — visibility first. CI's test step now runs test:coverage
(strict superset: fails on a failing test, never on coverage). Baseline ~71% stmts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 14:23:55 +02:00
ehoandClaude Opus 4.8 7d2a36ff22 feat(arch): WP-38 — dependency graph + declarative boundaries (dependency-cruiser)
CI / frontend (push) Successful in 2m11s
CI / storybook-a11y (push) Successful in 5m46s
CI / backend (push) Successful in 1m29s
CI / e2e (push) Successful in 2m55s
CI / semgrep (push) Successful in 1m1s
CI / api-client-drift (push) Successful in 2m5s
Adopt dependency-cruiser as the single declarative source for bounded-context +
atomic-layer boundaries, replacing the per-context no-restricted-imports blocks that
had to be hand-copied (and had left herregistratie uncovered). `.dependency-cruiser.js`
encodes context direction (everyone→shared, herregistratie→registratie, showcase→*),
domain-purity, contracts-import-nothing, ui↛infrastructure, ApiClient confinement, and
no-circular. `npm run dep:check` enforces (wired into ci-local.sh + the frontend CI job);
`npm run dep:graph` emits a committed mermaid context×layer graph. ESLint slimmed to
no-explicit-any + template a11y. Docs + new-context skill updated to the single source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:51:04 +02:00
ehoandClaude Opus 4.8 5b6023045e ci(semgrep): install with --ignore-installed (apt-managed PyJWT can't be uninstalled)
The runner's semgrep install failed trying to replace Debian's apt-managed PyJWT
("Cannot uninstall PyJWT ... RECORD file not found"). --ignore-installed installs
semgrep's deps fresh without uninstalling the apt copies, staying within the
runner's constraints (no setup-python, no container job).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 12:51:00 +02:00
ehoandClaude Opus 4.8 9b2a878933 ci(semgrep): install via runner python3/pip, drop container + setup-python
CI / api-client-drift (push) Failing after 30s
CI / frontend (push) Successful in 1m51s
CI / storybook-a11y (push) Successful in 4m50s
CI / backend (push) Successful in 1m29s
CI / e2e (push) Successful in 3m0s
CI / semgrep (push) Failing after 29s
The container: approach failed — this act_runner times out pulling its base runner
image (docker.gitea.com/runner-images:ubuntu-latest, IPv6) for container jobs. And
the earlier setup-python step failed downloading Python. Both avoided: run on the
plain ubuntu-latest runner and install semgrep with the preinstalled python3/pip
(`python3 -m pip install --break-system-packages semgrep`; --break-system-packages
survives PEP-668, pip puts semgrep on PATH). Verified in a clean python:3.12
container that pip install lands `semgrep` on PATH and the scan runs. Still
report-only; WP-30 tracks the flip to --error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 10:10:57 +02:00
ehoandClaude Opus 4.8 b34cc2ba3b ci(semgrep): run in the official Semgrep container (setup-python failed on the runner)
CI / frontend (push) Failing after 31s
CI / storybook-a11y (push) Failing after 31s
CI / backend (push) Failing after 31s
CI / e2e (push) Failing after 30s
CI / semgrep (push) Failing after 25s
CI / api-client-drift (push) Failing after 30s
The setup-python + `pip install semgrep` step errored on the Gitea runner. Switch to
`container: docker.io/semgrep/semgrep` (semgrep preinstalled) — the documented way to
run Semgrep in CI, and the exact execution verified locally (306 rules / 450 files,
27 findings, exit 0). Fully-qualified image name so short-name resolution works under
Docker or podman. Still report-only (no --error); WP-30 tracks flipping to blocking.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:46:48 +02:00
ehoandClaude Opus 4.8 c404995980 ci: replace CodeQL with Semgrep (Gitea-compatible SAST)
CI / frontend (push) Failing after 31s
CI / storybook-a11y (push) Failing after 30s
CI / backend (push) Failing after 30s
CI / e2e (push) Failing after 30s
CI / semgrep (push) Failing after 30s
CI / api-client-drift (push) Failing after 30s
CodeQL is GitHub-only — its analyze step uploads SARIF to GitHub's code-scanning
API and assumes a GitHub Security tab; this CI runs on Gitea only, so the job could
never go green (it had been red since it was added). Replace it with Semgrep OSS, a
plain CLI SAST with no account/platform API, which runs fine on Gitea.

- Remove the codeql job (+ its security-events permission) and the schedule trigger
  (it existed only for codeql; semgrep runs on push + PR).
- Add a semgrep job: setup-python + `pip install semgrep` +
  `semgrep scan --config p/default --config p/csharp --metrics=off`. pip-on-runner
  (not container:) mirrors the other jobs' model; anonymous registry, telemetry off.
- Report-only for now (no --error → job stays green): a local dry-run found 27
  findings, mostly CI/config policy (unpinned actions, .npmrc), not app-code vulns.
  WP-30 tracks triaging them + flipping to --error (a blocking gate).

Verified locally: `semgrep scan` runs clean (exit 0 without --error, 306 rules /
450 files). CI behaviour confirmable only on the Gitea runner — watch the run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:34:13 +02:00
ehoandClaude Opus 4.8 708d4c2308 ci: speed up pipeline + shrink demo web image
CI / frontend (push) Successful in 1m46s
CI / backend (push) Has been cancelled
CI / e2e (push) Has been cancelled
CI / codeql (csharp) (push) Has been cancelled
CI / codeql (javascript-typescript) (push) Has been cancelled
CI / api-client-drift (push) Has been cancelled
CI / storybook-a11y (push) Has been cancelled
CI (Gitea Actions, .github/workflows/ci.yml):
- CodeQL off the PR critical path: runs on push-to-main + a weekly cron only
  (`if: github.event_name != 'pull_request'`, `schedule: Mondays 03:00 UTC`).
  The 2-language 20-min matrix was the slowest thing on every PR; code is still
  scanned on main + weekly.
- Cache Playwright browsers (~/.cache/ms-playwright) in the storybook-a11y + e2e
  jobs — skips the chromium download on a hit; `install --with-deps` then only does
  the fast apt deps check.
- Cache NuGet (~/.nuget/packages, keyed on **/*.csproj — no packages.lock.json) in
  the backend / e2e / api-client-drift / codeql-csharp jobs.
- `npm ci --prefer-offline --no-audit --no-fund` in the 4 npm jobs.

Demo (docker-compose.yml, local only — NOT used by CI): web image node:24 →
node:24-slim (~1.1GB → 232MB verified). The container only runs `npm ci && ng serve`
and the native deps ship prebuilt glibc binaries, so slim needs no toolchain —
verified: npm ci clean, ng serve boots, app returns 200.

Note: CI timing/behaviour can only be confirmed on the Gitea runner (not observable
locally). Every change here is independently revertable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 20:28:19 +02:00
ehoandClaude Opus 4.8 2820cd3476 fix(ci): e2e — let Playwright manage both servers (fixes the 2-hour hang)
CI / e2e (push) Successful in 2m50s
CI / frontend (push) Successful in 1m49s
CI / storybook-a11y (push) Successful in 4m55s
CI / backend (push) Successful in 1m18s
CI / codeql (csharp) (push) Failing after 2m3s
CI / codeql (javascript-typescript) (push) Failing after 1m29s
CI / api-client-drift (push) Successful in 1m42s
The e2e job backgrounded `dotnet run &` and `ng serve &` in separate Actions
steps, then `npx wait-on` (no timeout) in a later step. A process started with
`&` in one step is killed when that step's shell exits, so wait-on waited forever
on servers that were already gone — the job hung until the runner's hard limit
(~2h; Gitea's act_runner doesn't reliably enforce timeout-minutes).

Move both servers into Playwright's `webServer` (an array: backend + `npm start`),
so Playwright starts them, waits for readiness, runs the suite, and tears them
down in the one `npm run e2e` process — CI and local alike. The CI e2e job is now
just npm ci / playwright install / npm run e2e. `reuseExistingServer` is on locally
(reuses a running app, incl. the docker stack) and off in CI (fresh start).

Verified locally via `CI=1 npm run e2e` against free ports + a clean db: both
smoke tests pass in ~15s (no hang). The earlier local failure was leftover SQLite
state (WP-22) from a dirty run resuming a Concept — CI checks out fresh, so its db
is always empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 20:07:35 +02:00
ehoandClaude Sonnet 5 556f2f47bf feat(fp): WP-22 — durable persistence (SQLite/EF Core)
Applications, documents (+ audit log) and the brief move off static in-memory
Dictionaries onto a real SQLite file via EF Core, so demo data survives a
process restart or `docker compose restart api` for the first time. The three
stores (ApplicationStore/DocumentStore/BriefStore) keep their exact public
signatures and static-class shape — no DI, no async ripple into Program.cs's
minimal-API handlers — each method just opens a short-lived AppDbContext via
Db.Create() under the same lock it already had. Opaque nested shapes (a
wizard's draft snapshot, a brief's sections/placeholders/status) are stored as
JSON text columns rather than redesigned into relational tables, matching the
existing "don't interpret it" posture.

Found two things the WP's own text got wrong, corrected in
docs/backlog/WP-22-durable-persistence.md's Deviations section: SeedData never
seeded these three stores (only the read-only BRP/DUO-mimicking GETs, which
stay in-memory) so there's no seed step; and no new docker-compose volume is
needed since the existing bind mount already covers the SQLite file — verified
against this environment's real podman-backed compose stack, not just by
reading the file.

Also: pinned SQLitePCLRaw.bundle_e_sqlite3 to 3.0.3 (EF Core Sqlite's own
transitive default bundles a pre-3.50.2 SQLite with a known high-severity
memory-corruption advisory); found and fixed a real xUnit test race where
concurrent test-class hosts stomped a shared static connection-string field,
fixed by disabling cross-class test parallelization rather than adding DI the
stores don't otherwise need.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 10:19:23 +02:00
ehoandClaude Sonnet 5 e276629107 feat(fp): WP-20 — second locale proof (nl/en build seam)
angular.json gains an i18n block (sourceLocale nl, en translation file) and
an `en` build/serve configuration with i18nMissingTranslation: "error" so a
new $localize string without an English unit fails the build, not silently
falls back. CI now runs `ng build --localize` to build both locales every
run. Verified end-to-end, not just "the build succeeded": the nl bundle
ships "Inloggen met DigiD", the en bundle ships "Log in with DigiD".

Incidental: prettier/compodoc regen noise in docs/wcag-checklist.md,
src/docs/a11y.mdx, documentation.json from the same working session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 18:16:11 +02:00
ehoandClaude Sonnet 5 26c2c5acd0 feat(fp): WP-19 — Playwright e2e smoke against the real FE+backend
Adds a happy-path spec (login → dashboard → registratie wizard, including
a real identity-document upload → real submit) and a degraded-path spec
(?scenario=error → <app-async> error slot → retry), both driving the real
app against the real .NET backend, plus a CI job that boots both.

Writing the retry spec surfaced a real bug: AsyncComponent's retry() only
reloads a [resource]-fed instance, so every real page (all [data]-fed via
a store's RemoteData) had a silently no-op retry button. Added a
retryClicked output and wired it on the dashboard's two async blocks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 10:13:40 +02:00
ehoandClaude Opus 4.8 bf920696ac ci: harden workflow + add security scanning and format gates
CI / storybook-a11y (push) Successful in 4m3s
CI / backend (push) Successful in 1m1s
CI / codeql (csharp) (push) Failing after 39m18s
CI / codeql (javascript-typescript) (push) Failing after 1m22s
CI / frontend (push) Successful in 1m25s
CI / api-client-drift (push) Successful in 1m34s
- permissions: contents:read (least privilege), concurrency cancel,
  scope push to main+tags (was: every branch, double-running with PRs),
  per-job timeout-minutes.
- security: npm audit --omit=dev, CodeQL SAST (TS + C#), Dependabot
  (npm/nuget/actions).
- format: npm run format:check + dotnet format --verify-no-changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 13:39:31 +02:00
ehoandClaude Opus 4.8 546097434d fix(ci): point backend test at BigRegister.slnx (solution renamed from .sln)
CI / frontend (push) Successful in 1m12s
CI / storybook-a11y (push) Successful in 4m5s
CI / backend (push) Successful in 43s
CI / api-client-drift (push) Successful in 1m31s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 11:43:44 +02:00
ehoandClaude Opus 4.8 97f7de4590 feat(a11y): WP-01 — axe-on-every-story CI gate
Turn the interactive Storybook a11y addon into a build gate:
- @storybook/test-runner + axe-playwright over the static build
  (.storybook/test-runner.ts reads the a11y tags from story context)
- test-storybook / test-storybook:ci scripts; storybook-a11y CI job
- triage: escape-hatch a11y.disable on stories whose display:contents
  wrapper splits <ul>/<li> or <dl>/<dt>/<dd> (structural, deferred to
  WP-11/WP-12, each with justification + cross-ref)
- fix trivial violations: footer/wizard-shell contrast, text-input label,
  wizard stories missing provideApiClient

Verified: broken story fails the gate; 133 stories pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 19:13:18 +02:00
ehoandClaude Opus 4.8 d08f3877f7 Architect-review remediation: enforce conventions, prod-safe tooling, one form idiom, resilience seams
Acts on the showcase review. Four workstreams; all tests green
(npm run lint, 70 FE tests, ng build, 33 backend tests).

Enforcement + CI:
- eslint.config.mjs bans `any` and enforces layer/context boundaries
  (domain ≠ Angular; herregistratie → registratie → shared, auth → shared);
  `npm run lint` added; ajv 6 scoped to ESLint via nested override.
- .github/workflows/ci.yml: FE lint+check:tokens+test+build, backend dotnet test,
  and an API-client drift check.

One form idiom (the headline finding):
- change-request-form converged onto the wizard pattern — change-request.machine.ts
  (Model/Msg/reduce + value objects) + submit-change-request.ts (Result) + a real
  POST /api/v1/change-requests (server re-validates). Spec + story added; the detail
  page no longer holds an ad-hoc success signal.

Resilience/observability seam:
- api-client.provider.ts: request timeout, X-Correlation-Id, Idempotency-Key for
  writes; comments naming the retry/auth seams.
- Backend logs correlation id + a no-PII submit-audit line; /api/v1 prefix +
  backward-compat note; client regenerated.

Quick wins:
- Dev tooling excluded from prod: scenario.interceptor wired only under isDevMode()
  (?scenario= inert in prod); debug panel @if(isDev) (tree-shaken out).
- src/environments + apiBaseUrl into provideApiClient (angular.json fileReplacements).
- Backend /health + /health/ready.
- Debug view PII-minimised (redactProfile: name/address/DOB redacted, BIG masked).
- IntakePolicyAdapter (removes inline resource in the intake wizard).
- README de-staled; CLAUDE.md gains EN/NL + forms-one-idiom + lint/CI notes.
- Stories: text-input, link, data-row, site-header, site-footer, change-request-form.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 08:25:51 +02:00