From 14a204de0d32ad78193c614bef28513d1f97c593 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Thu, 1 Oct 2026 20:53:01 +0300 Subject: [PATCH] chore: guard backend responsibility boundaries --- .ai/architecture.md | 7 ++++-- README.md | 4 ++++ eslint.config.mjs | 49 ++++++++++++++++++++++++++++++++++++++++++ test/eslint-config.cjs | 30 ++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 2 deletions(-) diff --git a/.ai/architecture.md b/.ai/architecture.md index 2dc1b5f..8776372 100644 --- a/.ai/architecture.md +++ b/.ai/architecture.md @@ -267,5 +267,8 @@ Tests сохраняют независимые literal wire assertions; обы ## Проверка `npm run typecheck:app`, `npm run lint`, targeted Jest по изменённому use case. -Смысл boundaries проверяется review: backend не имеет отдельного автоматического -FSD/DI architecture checker. Existing tests и build не доказывают правильный owner. +ESLint проверяет прямые imports: domain не зависит от transport/persistence, +controllers делегируют application, use cases не импортируют Prisma. Чистые +protocol inputs разрешены; existing CRUD/transaction services сохраняют DB owner. +Транзитивные зависимости, DI composition и смысл ownership проверяются review: +backend не имеет полного FSD/DI graph checker. Tests/build не доказывают owner. diff --git a/README.md b/README.md index 4ab2683..7104c04 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,10 @@ lint. Он не защищает от усложнения уже учтённо baseline командой `npx eslint . --prune-suppressions`; не пересоздавайте его для обхода новых ошибок. Security, TypeScript и Jest в baseline не включены. +Рост baseline — изменение quality gate: он требует отдельного обоснования +и review затронутого поведения. При рефакторинге сохраняйте или сокращайте +baseline; прежнее число сообщений не доказывает прежнюю сложность функции. + ## SonarQube Проект `wealthledger-backend` на `https://sonarqube.personal.ksv741.ru` diff --git a/eslint.config.mjs b/eslint.config.mjs index 998a57b..9fde38f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -15,6 +15,9 @@ import globals from 'globals'; import tseslint from 'typescript-eslint'; const tests = ['**/*.spec.ts', 'test/**/*.ts']; +const prismaImports = [ + '@prisma/*', '**/generated/prisma/**', '**/prisma.service', +]; export default defineConfig( globalIgnores(['dist/**', 'dist-dev/**', 'build/**', 'generated/**', 'coverage/**', 'node_modules/**']), @@ -119,6 +122,52 @@ export default defineConfig( 'jest/expect-expect': ['error', { assertFunctionNames: ['expect', '**.expect'] }], }, }, + { + files: ['src/modules/**/domain/**/*.ts'], + ignores: tests, + rules: { + 'no-restricted-imports': ['error', { + patterns: [{ + group: [ + '@nestjs/*', ...prismaImports, '**/*.dto', '**/*.repository', + '**/*.client', '**/application/**', '**/infrastructure/**', + ], + message: 'Domain uses pure rules and value types, not transport or persistence.', + }], + }], + }, + }, + { + files: ['src/**/*.controller.ts'], + ignores: tests, + rules: { + 'no-restricted-imports': ['error', { + patterns: [ + { + group: [...prismaImports, '**/*.repository', '**/*.client'], + message: 'Controllers delegate to application services or use cases.', + }, + { + group: ['**'], + importNamePattern: '(?:Client|Repository|PrismaService)$', + message: 'Use application orchestration rather than data providers in controllers.', + }, + ], + }], + }, + }, + { + files: ['src/**/*.use-case.ts'], + ignores: tests, + rules: { + 'no-restricted-imports': ['error', { + patterns: [{ + group: prismaImports, + message: 'Use cases obtain data through concrete repositories.', + }], + }], + }, + }, { files: [ 'src/**/*.ts', diff --git a/test/eslint-config.cjs b/test/eslint-config.cjs index 592f8c0..4a8b9d1 100644 --- a/test/eslint-config.cjs +++ b/test/eslint-config.cjs @@ -91,6 +91,36 @@ test('numeric literals require names, including previously ignored numbers and b } }); +test('architecture imports enforce direct boundaries without banning DTOs or pure protocol inputs', async () => { + const domain = 'src/modules/banking/bank-cards/domain/rub-money.ts'; + const controller = 'src/modules/auth/presentation/auth.controller.ts'; + const useCase = 'src/modules/banking/bank-cards/application/get-bank-summary.use-case.ts'; + for (const [filePath, code] of [ + [domain, 'import { Injectable } from "@nestjs/common";'], + [domain, 'import { MoneyDto } from "../../../../common/money.dto";'], + [domain, 'import { PrismaService } from "../../../../prisma/prisma.service";'], + [controller, 'import { PrismaService } from "../../../prisma/prisma.service";'], + [controller, 'import { TbankClient } from "../../../integrations/tbank/tbank.api";'], + [useCase, 'import { PrismaService } from "../../../../prisma/prisma.service";'], + ]) { + const [result] = await eslint.lintText(code, { filePath }); + assert.equal(result.fatalErrorCount, 0); + assert.ok(result.messages.some(({ ruleId }) => ruleId === 'no-restricted-imports'), code); + } + for (const [filePath, code] of [ + [domain, 'import type { Money } from "../../../../common/money.types";'], + [domain, 'import { TbankOperationState } from "../../../../integrations/tbank/tbank-protocol";'], + [controller, 'import { AuthService } from "../application/auth.service";'], + [useCase, 'import { BankCardsRepository } from "../infrastructure/bank-cards.repository";'], + [useCase, 'import { BankSummaryQueryDto } from "../presentation/bank-card.dto";'], + ['src/modules/banking/bank-cards/application/bank-cards.service.ts', 'import { PrismaService } from "../../../../prisma/prisma.service";'], + ]) { + const [result] = await eslint.lintText(code, { filePath }); + assert.equal(result.fatalErrorCount, 0); + assert.ok(result.messages.every(({ ruleId }) => ruleId !== 'no-restricted-imports'), code); + } +}); + // Single-use codes must be caught too; fixture literals remain independent wire assertions. test('machine codes require enum members without banning ordinary text or Prisma selects', async () => { const eslint = new ESLint();