Move the two wizard lookups behind application-layer facades so ui/ no longer injects infrastructure adapters directly: - RegistratieLookupStore (BRP address + DUO diplomas): owns the resources, runs the trust-boundary parse, exposes adresStatus/prefillAdres/duoLookup. - IntakePolicyStore (scholing threshold): owns the policy resource, exposes the derived threshold. Add the lint rule ui/ + layout/ ↛ **/infrastructure/** (@typescript-eslint variant so it composes with the base direction rules; stories/specs exempted as test scaffolding). Add the documented showcase sanction (may read every context). Fix the docs' inventory: 6 contexts / 5 layers, +brief, +contracts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
187 lines
6.8 KiB
JavaScript
187 lines
6.8 KiB
JavaScript
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/*', '@brief/*'], 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/*', '@brief/*'], 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/*', '@brief/*'], message: 'Dependencies point herregistratie → registratie → shared, never back.' }] },
|
|
],
|
|
},
|
|
},
|
|
|
|
// brief/ (letter composition) is an independent leaf context: it may depend only on shared.
|
|
{
|
|
files: ['src/app/brief/**/*.ts'],
|
|
rules: {
|
|
'no-restricted-imports': [
|
|
'error',
|
|
{ patterns: [{ group: ['@auth/*', '@registratie/*', '@herregistratie/*'], message: 'brief/ may depend only on shared.' }] },
|
|
],
|
|
},
|
|
},
|
|
|
|
// contracts/ is the FE⇄BE wire seam: pure DTO shapes that must import NOTHING
|
|
// (CLAUDE.md §1, ADR-0001) — not Angular, not a context alias, not relative app
|
|
// code. Enums are inlined string-literal unions; the adapter's parse* maps them.
|
|
// (This comes after the per-context rules so it wins for contracts files.)
|
|
{
|
|
files: ['src/app/**/contracts/**/*.ts'],
|
|
rules: {
|
|
'no-restricted-imports': [
|
|
'error',
|
|
{
|
|
patterns: [
|
|
{
|
|
group: ['@angular/**', '@shared/**', '@auth/**', '@registratie/**', '@herregistratie/**', '@brief/**', './*', '../*', './**', '../**'],
|
|
message: 'contracts/ is the wire seam — it must import NOTHING (pure DTO shapes). Map wire → domain in the infrastructure adapter, not here.',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
|
|
// BFF-lite anti-corruption boundary (ADR-0001): the ApiClient (the network
|
|
// client) may be imported as a VALUE only from infrastructure-role files.
|
|
// Type-only imports of generated wire DTOs are allowed anywhere — they grant no
|
|
// network access. UI/application reach the network through an adapter or command.
|
|
{
|
|
files: ['src/app/**/*.ts'],
|
|
plugins: { '@typescript-eslint': tseslint.plugin },
|
|
rules: {
|
|
'@typescript-eslint/no-restricted-imports': [
|
|
'error',
|
|
{
|
|
patterns: [
|
|
{
|
|
group: ['@shared/infrastructure/api-client'],
|
|
allowTypeImports: true,
|
|
message: 'The ApiClient lives only in infrastructure/ adapters (ADR-0001). UI/application call an adapter or a command, not the network client. (Type-only DTO imports are fine: use `import type`.)',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
// …the infrastructure adapters ARE that boundary and own the client. shared/upload
|
|
// is a feature-scoped adapter that lives outside a /infrastructure/ folder.
|
|
{
|
|
files: ['src/app/**/infrastructure/**/*.ts', 'src/app/shared/upload/**/*.ts'],
|
|
plugins: { '@typescript-eslint': tseslint.plugin },
|
|
rules: { '@typescript-eslint/no-restricted-imports': 'off' },
|
|
},
|
|
|
|
// ui/ and layout/ are the presentation layer: dependencies point inward
|
|
// (ui → application → domain, CLAUDE.md §1), so they must NOT import
|
|
// infrastructure/ directly — they reach data through an application store or
|
|
// command. (Stories/specs are test scaffolding and may wire the real client.)
|
|
// Uses the @typescript-eslint variant so it composes with the base
|
|
// no-restricted-imports context-direction rules above (last-wins is per rule name).
|
|
{
|
|
files: ['src/app/**/ui/**/*.ts', 'src/app/**/layout/**/*.ts'],
|
|
ignores: ['**/*.stories.ts', '**/*.spec.ts'],
|
|
plugins: { '@typescript-eslint': tseslint.plugin },
|
|
rules: {
|
|
'@typescript-eslint/no-restricted-imports': [
|
|
'error',
|
|
{
|
|
patterns: [
|
|
{
|
|
group: [
|
|
'@shared/infrastructure/*',
|
|
'@auth/infrastructure/*',
|
|
'@registratie/infrastructure/*',
|
|
'@herregistratie/infrastructure/*',
|
|
'@brief/infrastructure/*',
|
|
],
|
|
allowTypeImports: true,
|
|
message: 'ui/ and layout/ must not import infrastructure/ directly (CLAUDE.md §1: ui → application → domain). Reach data through an application store or command. (Type-only DTO imports are fine: use `import type`.)',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
|
|
// Sanctioned exception: showcase/ is the teaching page whose whole point is showing
|
|
// multiple contexts side by side (ARCHITECTURE.md §6). It may read every context;
|
|
// nothing imports showcase. Same precedent as the shared/ui/debug-state exemption.
|
|
{
|
|
files: ['src/app/showcase/**/*.ts'],
|
|
rules: { 'no-restricted-imports': 'off' },
|
|
},
|
|
];
|