New `brief` context — a letter-composition feature with a drafter/approver approval workflow, built as a teaching vertical slice on the repo's existing FP + Elm + atomic-design patterns (see plan in ~/.claude/plans). Domain (pure): - Rich text as a serialisable value tree (placeholders are first-class nodes), moved to @shared/kernel/rich-text.ts so the shared editor can use it. - lintPlaceholders: a pure, total content -> Diagnostic[] linter, derived never stored. - brief.machine.ts: status sum-type with guarded transitions; frozen-snapshot = deep value copy; derived diagnostics/editability. Full specs. Backend (.NET stub): - BriefStore + seed, GET/PUT /brief and submit/approve/reject/send endpoints, role via X-Role header (mirrors X-Admin), transition + approver!=drafter guards, audit logging. Regenerated typed client via gen:api. +6 backend tests. Seam: - brief.adapter.ts maps flat wire unions <-> domain discriminated unions at the parse boundary (+ spec). UI (atomic): - shared atoms: checkbox, placeholder-chip; molecule: rich-text-editor (no-dep contenteditable, DOM<->RichTextBlock round-trip tested). - brief/ui: letter-block, passage-picker, diagnostics-panel, rejection-comments, letter-section, letter-composer, letter-preview, brief.page + /brief route. - Dev-only ?role=drafter|approver toggle + roleInterceptor; dashboard nav link. Enforcement: @brief/* alias + eslint layer boundary (brief depends only on shared). Also included (same session): - Value-object specs (postcode/uren/big-nummer) — closes the "domain must have a spec" gap. - src/docs/ Storybook MDX foundation pages (atomic design, tokens, FP-in-UI). - .storybook/tsconfig.json: add @angular/localize to types (Storybook was fully broken — $localize unresolved — dev + build). Verified: 168 FE tests, 68 backend tests, lint/build/check:tokens green, Storybook boots, end-to-end HTTP smoke (self-approve 403, approver 200, full flow). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
96 lines
3.0 KiB
JavaScript
96 lines
3.0 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.' }] },
|
|
],
|
|
},
|
|
},
|
|
];
|