feat(eslint): add boundaries/dependencies rule preventing cross-domain imports

Adds eslint-plugin-boundaries with one element type per Tier-1 domain and an
explicit allow-list encoding the architectural dependency graph:
- document may import from: shared, person, tag, ocr, activity, conversation
- geschichte may import from: shared, person, document
- ocr may import from: shared, document
- activity may import from: shared, notification
- all others (person, tag, user, notification, conversation): shared only
- routes may import from any domain

Default is 'disallow', so any unlisted cross-domain import is an error.
Two eslint-disable-next-line comments remain in shared/discussion where
person-domain helpers (getInitials, formatLifeDateRange) are needed to render
participant metadata; moving them to shared would lose the person-type context.

Closes #410
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-05 17:38:27 +02:00
committed by marcel
parent 832a8dfe2f
commit 4966855c24
3 changed files with 82 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ import { fileURLToPath } from 'node:url';
import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import boundaries from 'eslint-plugin-boundaries';
import { defineConfig } from 'eslint/config';
import globals from 'globals';
import ts from 'typescript-eslint';
@@ -61,5 +62,84 @@ export default defineConfig(
}
]
}
},
{
plugins: { boundaries },
settings: {
'import/resolver': { typescript: { project: './tsconfig.json' } },
'boundaries/elements': [
{ type: 'document', pattern: 'src/lib/document/**' },
{ type: 'person', pattern: 'src/lib/person/**' },
{ type: 'tag', pattern: 'src/lib/tag/**' },
{ type: 'user', pattern: 'src/lib/user/**' },
{ type: 'geschichte', pattern: 'src/lib/geschichte/**' },
{ type: 'notification', pattern: 'src/lib/notification/**' },
{ type: 'ocr', pattern: 'src/lib/ocr/**' },
{ type: 'activity', pattern: 'src/lib/activity/**' },
{ type: 'conversation', pattern: 'src/lib/conversation/**' },
{ type: 'shared', pattern: 'src/lib/shared/**' },
{ type: 'routes', pattern: 'src/routes/**' }
]
},
rules: {
'boundaries/dependencies': [
'error',
{
default: 'disallow',
message:
"Cross-domain import blocked. Move shared code to $lib/shared/, or expose it via the domain's index.ts.",
rules: [
// Document composes person components (D-FE-1) and tag components (D-FE-2),
// and hosts the OCR trigger in the transcription editor.
{
from: { type: 'document' },
allow: {
to: { type: ['shared', 'conversation', 'activity', 'person', 'tag', 'ocr'] }
}
},
// Geschichte editor selects persons and documents by design.
{
from: { type: 'geschichte' },
allow: { to: { type: ['shared', 'person', 'document'] } }
},
// OCR trigger embeds the document script-type selector.
{
from: { type: 'ocr' },
allow: { to: { type: ['shared', 'document'] } }
},
// Activity feed (Chronik) reads notification items for its inbox panel.
{
from: { type: 'activity' },
allow: { to: { type: ['shared', 'notification'] } }
},
{ from: { type: 'person' }, allow: { to: { type: ['shared'] } } },
{ from: { type: 'tag' }, allow: { to: { type: ['shared'] } } },
{ from: { type: 'user' }, allow: { to: { type: ['shared'] } } },
{ from: { type: 'notification' }, allow: { to: { type: ['shared'] } } },
{ from: { type: 'conversation' }, allow: { to: { type: ['shared'] } } },
{ from: { type: 'shared' }, allow: { to: { type: ['shared'] } } },
{
from: { type: 'routes' },
allow: {
to: {
type: [
'document',
'person',
'tag',
'user',
'geschichte',
'notification',
'ocr',
'activity',
'conversation',
'shared'
]
}
}
}
]
}
]
}
}
);

View File

@@ -2,6 +2,7 @@
import { m } from '$lib/paraglide/messages.js';
import type { FlatMessage } from '$lib/shared/types';
import { extractQuote } from '$lib/shared/discussion/comment';
// eslint-disable-next-line boundaries/dependencies -- discussion UI needs person initials for avatars; move to shared if getInitials becomes generic
import { getInitials } from '$lib/person/personFormat';
import { relativeTime } from '$lib/shared/utils/time';
import { renderBody } from '$lib/shared/discussion/mention';

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import type { components } from '$lib/generated/api';
// eslint-disable-next-line boundaries/dependencies -- mention dropdown needs person date formatting; extract to shared if it becomes reusable
import { formatLifeDateRange } from '$lib/person/personLifeDates';
import { m } from '$lib/paraglide/messages.js';