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>
This commit is contained in:
2026-06-27 08:25:51 +02:00
parent cf570a8132
commit d08f3877f7
35 changed files with 1803 additions and 145 deletions

84
eslint.config.mjs Normal file
View File

@@ -0,0 +1,84 @@
import tseslint from 'typescript-eslint';
/**
* Enforces the architecture's working agreements that were previously only
* documented (CLAUDE.md): no `any`, domain/ stays framework-free, and the
* dependency direction between contexts (herregistratie → registratie → shared,
* auth → shared; shared depends on nothing). Boundary rules use path patterns on
* the import aliases, so they read as the direction statement they enforce.
*/
export default [
{
ignores: [
'dist/**',
'node_modules/**',
'storybook-static/**',
'.angular/**',
'coverage/**',
'backend/**',
// Generated client — owns its own /* eslint-disable */ header.
'src/app/shared/infrastructure/api-client.ts',
],
},
{
files: ['src/**/*.ts'],
languageOptions: {
parser: tseslint.parser,
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
},
plugins: { '@typescript-eslint': tseslint.plugin },
rules: { '@typescript-eslint/no-explicit-any': 'error' },
},
// Tests legitimately use `any` to feed invalid messages/states into reducers.
{
files: ['src/**/*.spec.ts'],
rules: { '@typescript-eslint/no-explicit-any': 'off' },
},
// domain/ = pure business rules + types. No Angular, ever.
{
files: ['src/app/**/domain/**/*.ts'],
rules: {
'no-restricted-imports': [
'error',
{ patterns: [{ group: ['@angular/*', '@angular/**'], message: 'domain/ must stay framework-free (pure TS) — no Angular imports.' }] },
],
},
},
// shared/ is the base layer: it may not depend on any feature context.
// The dev-only debug panel is the sanctioned exception (it observes every store).
{
files: ['src/app/shared/**/*.ts'],
ignores: ['src/app/shared/ui/debug-state/**'],
rules: {
'no-restricted-imports': [
'error',
{ patterns: [{ group: ['@auth/*', '@registratie/*', '@herregistratie/*'], message: 'shared/ must not depend on a feature context.' }] },
],
},
},
// auth/ may depend only on shared.
{
files: ['src/app/auth/**/*.ts'],
rules: {
'no-restricted-imports': [
'error',
{ patterns: [{ group: ['@registratie/*', '@herregistratie/*'], message: 'auth/ may depend only on shared.' }] },
],
},
},
// registratie/ may depend on shared, not on herregistratie (direction points the other way).
{
files: ['src/app/registratie/**/*.ts'],
rules: {
'no-restricted-imports': [
'error',
{ patterns: [{ group: ['@herregistratie/*'], message: 'Dependencies point herregistratie → registratie → shared, never back.' }] },
],
},
},
];