chore: guard backend responsibility boundaries
Backend verify / commits (pull_request) Successful in 35s
Backend verify / security (pull_request) Successful in 9s
Backend verify / verify (pull_request) Successful in 10m0s

This commit is contained in:
2026-10-01 20:53:01 +03:00
parent a13cf84ead
commit 14a204de0d
4 changed files with 88 additions and 2 deletions
+5 -2
View File
@@ -267,5 +267,8 @@ Tests сохраняют независимые literal wire assertions; обы
## Проверка ## Проверка
`npm run typecheck:app`, `npm run lint`, targeted Jest по изменённому use case. `npm run typecheck:app`, `npm run lint`, targeted Jest по изменённому use case.
Смысл boundaries проверяется review: backend не имеет отдельного автоматического ESLint проверяет прямые imports: domain не зависит от transport/persistence,
FSD/DI architecture checker. Existing tests и build не доказывают правильный owner. 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.
+4
View File
@@ -150,6 +150,10 @@ lint. Он не защищает от усложнения уже учтённо
baseline командой `npx eslint . --prune-suppressions`; не пересоздавайте его baseline командой `npx eslint . --prune-suppressions`; не пересоздавайте его
для обхода новых ошибок. Security, TypeScript и Jest в baseline не включены. для обхода новых ошибок. Security, TypeScript и Jest в baseline не включены.
Рост baseline — изменение quality gate: он требует отдельного обоснования
и review затронутого поведения. При рефакторинге сохраняйте или сокращайте
baseline; прежнее число сообщений не доказывает прежнюю сложность функции.
## SonarQube ## SonarQube
Проект `wealthledger-backend` на `https://sonarqube.personal.ksv741.ru` Проект `wealthledger-backend` на `https://sonarqube.personal.ksv741.ru`
+49
View File
@@ -15,6 +15,9 @@ import globals from 'globals';
import tseslint from 'typescript-eslint'; import tseslint from 'typescript-eslint';
const tests = ['**/*.spec.ts', 'test/**/*.ts']; const tests = ['**/*.spec.ts', 'test/**/*.ts'];
const prismaImports = [
'@prisma/*', '**/generated/prisma/**', '**/prisma.service',
];
export default defineConfig( export default defineConfig(
globalIgnores(['dist/**', 'dist-dev/**', 'build/**', 'generated/**', 'coverage/**', 'node_modules/**']), globalIgnores(['dist/**', 'dist-dev/**', 'build/**', 'generated/**', 'coverage/**', 'node_modules/**']),
@@ -119,6 +122,52 @@ export default defineConfig(
'jest/expect-expect': ['error', { assertFunctionNames: ['expect', '**.expect'] }], '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: [ files: [
'src/**/*.ts', 'src/**/*.ts',
+30
View File
@@ -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. // 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 () => { test('machine codes require enum members without banning ordinary text or Prisma selects', async () => {
const eslint = new ESLint(); const eslint = new ESLint();