Realizes ADR-0004's "future low-code editor that commits a PR": an
admin-only stamdata maintenance editor built on the stamdata-as-code
foundation.
Backend: `professions` moves from a hardcoded C# dictionary to an embedded
`professions.json` data-file (typed as `ProfessionMapping`) with valid-time
(geldigVan/geldigTot, half-open). A generic, reflection-driven
StamdataCatalog/StamdataTable/StamdataFile describes every table so one
endpoint pair + one grid editor serve all of them; add a table in one line.
Two read-only, admin-gated endpoints (GET /stamdata, GET /stamdata/{table}
?peildatum=) — no runtime write path. Generic build gate
`Every_catalog_table_is_valid` (keys non-blank, no overlapping validity,
well-formed windows).
Frontend: new `beheer` context (route beheer/stamdata, capabilityGuard
'stamdata:edit'). A schema-driven grid editor edits rows locally; download()
emits {table}.json for the admin to commit as a reviewed PR (no mutation
command — the CI build + StamdataValidationTests stay the authority).
Full gate GREEN both sides; gen:api leaves no drift; new stamdata story
passes axe. See WP-29 + ADR-0004.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
267 lines
8.8 KiB
JavaScript
267 lines
8.8 KiB
JavaScript
import tseslint from 'typescript-eslint';
|
|
import angular from 'angular-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' },
|
|
// This repo has no .html files — every template is inline. This processor
|
|
// extracts each component's template string into a virtual `*.html` file,
|
|
// which the template-a11y block below (`files: ['src/**/*.html']`) then lints.
|
|
processor: angular.processInlineTemplates,
|
|
},
|
|
|
|
// Template a11y (WP-17): the official angular-eslint accessibility bundle
|
|
// (alt-text, label-has-associated-control, click/mouse-events-have-key-events,
|
|
// interactive-supports-focus, valid-aria, no-autofocus, and more) linting the
|
|
// virtual `.html` files the processor above extracts from inline templates.
|
|
...angular.configs.templateAccessibility.map((c) => ({ ...c, files: ['src/**/*.html'] })),
|
|
|
|
// 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/*', '@beheer/*'],
|
|
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/*', '@beheer/*'],
|
|
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/*', '@beheer/*'],
|
|
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/*', '@beheer/*'],
|
|
message: 'brief/ may depend only on shared.',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
|
|
// beheer/ (stamdata maintenance) is an independent leaf context: it may depend only on shared.
|
|
{
|
|
files: ['src/app/beheer/**/*.ts'],
|
|
rules: {
|
|
'no-restricted-imports': [
|
|
'error',
|
|
{
|
|
patterns: [
|
|
{
|
|
group: ['@auth/*', '@registratie/*', '@herregistratie/*', '@brief/*'],
|
|
message: 'beheer/ 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/**',
|
|
'@beheer/**',
|
|
'./*',
|
|
'../*',
|
|
'./**',
|
|
'../**',
|
|
],
|
|
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/*',
|
|
'@beheer/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' },
|
|
},
|
|
];
|