# Дейл (Deal) — Этап 0: Каркас решения Implementation Plan > Исторический документ этапа 0. Актуальное состояние — `docs/superpowers/STATUS.md` и `docs/technical/Техническая-документация-Дейл.md`. > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Создать каркас нового продукта «Дейл»: структуру `src/`, решение `core` (модульный монолит) с пустыми модулями, стандарты кода (.editorconfig + анализаторы), dev-Postgres со схемой на тенанта и tenant-контекст. **Architecture:** Модульный монолит в `src/core` (один процесс, одно sln: `Deal.Api` + `Deal.Modules.*` + `Deal.SharedKernel` + `Deal.Infrastructure` + `Deal.Contracts`). Postgres: одна БД, системные таблицы в `public`, данные тенантов в `tenant_.*`. Сервисы ml/ai/telegram — отдельные процессы со своими sln (создаются в этом этапе как пустые каталоги, наполняются позже). Фронтенд Vue переезжает как есть в `src/frontend`. **Tech Stack:** .NET 10 (C#), ASP.NET Core (Web API + minimal), EF Core, Npgsql, xUnit, docker compose. **Spec:** `docs/architecture/2026-09-05-deal-architecture-design.md` (разделы 2, 3, 4, 10, 11) **ТЗ:** `docs/spec/ТЗ-дейл-новая-архитектура.md` (разделы 3, 11) ## Global Constraints - Проект **НЕ git-репозиторий** (рабочее дерево `C:\telbase`, деплой docker compose). Вместо коммитов фиксируем затронутые файлы и результат проверок в отчёте задачи. Рабочая папка плана: `.superpowers/sdd/deal-scaffold/`. - Решение собирается на .NET 10 SDK (установлен: `10.0.400`). - Код-стайл: `C:\telbase\Стиль_кода.docx` + адаптации: 1 тип = 1 файл; комментарии на русском; XML-doc только для public-контрактов; настройки через `IOptions`; без snake_case-хелперов и регионов; public-члены — только свойства; явные модификаторы доступа. - Все имена: namespace `Deal.*`, проекты `Deal.*`, имя решения `Deal.sln`. - Каждый публичный тип — в отдельном файле, имя файла = имя типа. - Анализаторы: `Microsoft.CodeAnalysis.NetAnalyzers` включён; нарушения стиля — ошибки сборки (через `.editorconfig` severity). - Запрещено: секреты в коде/репозитории; конкатенация SQL; magic numbers. - Старый LeadRadar-код (`backend/`, `frontend/` верхнего уровня) не трогаем, кроме переноса `frontend/` → `src/frontend/`. --- ### Task 1: Структура src/ и перенос фронтенда **Files:** - Create: `src/README.md` - Create: `README.md` (корневой, краткий) **Interfaces:** - Consumes: — (старт) - Produces: структура папок `src/{core, ml-service, ai-service, telegram-service, contracts, frontend}`; фронтенд перенесён в `src/frontend/`. - [ ] **Step 1: Создать структуру каталогов** Run: ```bash mkdir -p src/core src/ml-service src/ai-service src/telegram-service src/contracts ``` - [ ] **Step 2: Перенести фронтенд** Run: ```bash mkdir -p src/frontend cp -r frontend/* src/frontend/ && rm -rf frontend ``` Expected: `src/frontend/` содержит package.json, src/, index.html и т.д.; старая папка `frontend/` удалена. - [ ] **Step 3: Создать `src/README.md`** ```markdown # Дейл (Deal) — исходники - `core/` — модульный монолит .NET (бизнес-логика, API) - `ml-service/` — ML (.NET + ONNX), отдельный процесс - `ai-service/` — LLM-фасад, отдельный процесс - `telegram-service/` — ферма сессий Telegram, отдельный процесс - `contracts/` — общие .proto (gRPC) - `frontend/` — Vue (переехал из LeadRadar как есть) Подробности: `docs/architecture/2026-09-05-deal-architecture-design.md` ``` - [ ] **Step 4: Создать корневой `README.md`** ```markdown # Дейл (Deal) SaaS-мониторинг Telegram: реальные заказы и клиенты вместо рекламы и дубликатов. - Архитектура: `docs/architecture/2026-09-05-deal-architecture-design.md` - ТЗ: `docs/spec/ТЗ-дейл-новая-архитектура.md` - Техдок: `docs/technical/Техническая-документация-Дейл.md` - Исходники: `src/` ``` - [ ] **Step 5: Проверить** Run: `ls src/` — 6 папок; `ls src/frontend/` — файлы Vue-проекта; `test -f README.md && echo ok`. Expected: все проверки успешны. - [ ] **Step 6: Зафиксировать в отчёте** `task-1-report.md` (файлы, результат проверок). --- ### Task 2: Стандарты кода — .editorconfig, Directory.Build.props **Files:** - Create: `.editorconfig` - Create: `src/core/Directory.Build.props` **Interfaces:** - Produces: единые правила для всех проектов `src/core`; нарушения — ошибки сборки. - [ ] **Step 1: Создать корневой `.editorconfig`** ```editorconfig root = true [*] charset = utf-8 end_of_line = crlf insert_final_newline = true indent_style = space indent_size = 4 trim_trailing_whitespace = true [*.{cs,vb}] indent_size = 4 # Стиль фигурных скобок — Allman (на отдельной строке) csharp_new_line_before_open_brace = all csharp_new_line_before_else = true csharp_new_line_before_catch = true csharp_new_line_before_finally = true # using — в начале файла dotnet_sort_system_directives_first = true # Модификаторы доступа — всегда явные dotnet_style_require_accessibility_modifiers = always:error # this. — не требуется dotnet_style_qualification_for_field = false:silent dotnet_style_qualification_for_property = false:silent dotnet_style_qualification_for_method = false:silent # Члены csharp_style_var_for_built_in_types = false:silent csharp_style_var_when_type_is_apparent = false:silent csharp_style_var_elsewhere = false:silent [*.cs] # Отключить лишние правила IDE, которые конфликтуют с код-стайлом проекта dotnet_diagnostic.IDE0290.severity = none ``` - [ ] **Step 2: Создать `src/core/Directory.Build.props`** ```xml net10.0 latest enable enable true latest true all runtime; build; native; contentfiles; analyzers; buildtransitive ``` - [ ] **Step 3: Зафиксировать в отчёте** (проверка сборки — после Task 3). --- ### Task 3: Решение Deal.sln и пустые проекты core **Files:** - Create: `src/core/Deal.sln` - Create: `src/core/Deal.Api/Deal.Api.csproj` + `Program.cs` - Create: `src/core/Deal.Modules.Pipeline/`, `...Kanban/`, `...Projects/`, `...Discovery/`, `...Settings/`, `...Tenants/` (csproj + класс-маркер) - Create: `src/core/Deal.SharedKernel/`, `Deal.Infrastructure/`, `Deal.Contracts/` (csproj + маркер) **Interfaces:** - Produces: собираемое решение; проекты-модули, готовые к наполнению в следующих этапах. - [ ] **Step 1: Создать решение и проекты командой** ```bash cd /c/telbase/src/core dotnet new sln -n Deal dotnet new web -n Deal.Api -o Deal.Api --no-https dotnet new classlib -n Deal.Modules.Pipeline -o Deal.Modules.Pipeline dotnet new classlib -n Deal.Modules.Kanban -o Deal.Modules.Kanban dotnet new classlib -n Deal.Modules.Projects -o Deal.Modules.Projects dotnet new classlib -n Deal.Modules.Discovery -o Deal.Modules.Discovery dotnet new classlib -n Deal.Modules.Settings -o Deal.Modules.Settings dotnet new classlib -n Deal.Modules.Tenants -o Deal.Modules.Tenants dotnet new classlib -n Deal.SharedKernel -o Deal.SharedKernel dotnet new classlib -n Deal.Infrastructure -o Deal.Infrastructure dotnet new classlib -n Deal.Contracts -o Deal.Contracts ``` - [ ] **Step 2: Добавить проекты в решение** ```bash dotnet sln Deal.sln add Deal.Api Deal.Modules.Pipeline Deal.Modules.Kanban Deal.Modules.Projects Deal.Modules.Discovery Deal.Modules.Settings Deal.Modules.Tenants Deal.SharedKernel Deal.Infrastructure Deal.Contracts ``` - [ ] **Step 3: Удалить Class1.cs и добавить маркеры модулей** Каждый модуль получает публичный маркер-класс (1 тип = 1 файл), например `Deal.Modules.Pipeline/PipelineModuleMarker.cs`: ```csharp namespace Deal.Modules.Pipeline; /// Маркер модуля Pipeline: используется для DI-сканирования и тестов. public sealed class PipelineModuleMarker { } ``` Аналогично для всех модулей и Infrastructure/SharedKernel/Contracts (маркеры: `InfrastructureMarker`, `SharedKernelMarker`, `ContractsMarker`). - [ ] **Step 4: Ссылки между проектами (минимальные, по дизайн-доку)** ```bash dotnet add Deal.Api reference Deal.SharedKernel Deal.Contracts Deal.Infrastructure dotnet add Deal.Modules.Pipeline reference Deal.SharedKernel Deal.Contracts dotnet add Deal.Modules.Kanban reference Deal.SharedKernel Deal.Contracts dotnet add Deal.Modules.Projects reference Deal.SharedKernel Deal.Contracts dotnet add Deal.Modules.Discovery reference Deal.SharedKernel Deal.Contracts dotnet add Deal.Modules.Settings reference Deal.SharedKernel Deal.Contracts dotnet add Deal.Modules.Tenants reference Deal.SharedKernel Deal.Contracts dotnet add Deal.Infrastructure reference Deal.SharedKernel Deal.Contracts ``` - [ ] **Step 5: Минимальный Program.cs в Deal.Api (health)** ```csharp var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/api/health", () => Results.Ok(new { ok = true, service = "deal" })); app.Run(); public partial class Program { } ``` - [ ] **Step 6: Собрать решение** Run: `dotnet build Deal.sln` Expected: Build succeeded, 0 warnings, 0 errors. - [ ] **Step 7: Проверить health локально** Run: `dotnet run --project Deal.Api --urls http://localhost:5080` (в фоне), затем `curl http://localhost:5080/api/health` Expected: `{"ok":true,"service":"deal"}` (процесс остановить после проверки). - [ ] **Step 8: Зафиксировать в отчёте** `task-3-report.md`. --- ### Task 4: Тесты — xUnit-каркас **Files:** - Create: `src/core/tests/Deal.Tests.Unit/Deal.Tests.Unit.csproj` - Test: `src/core/tests/Deal.Tests.Unit/MarkerTests.cs` **Interfaces:** - Consumes: маркеры модулей из Task 3. - Produces: тестовый проект, подключённый к решению. - [ ] **Step 1: Создать тестовый проект** ```bash cd /c/telbase/src/core dotnet new xunit -n Deal.Tests.Unit -o tests/Deal.Tests.Unit dotnet sln Deal.sln add tests/Deal.Tests.Unit dotnet add tests/Deal.Tests.Unit reference Deal.SharedKernel Deal.Modules.Pipeline Deal.Modules.Kanban Deal.Modules.Projects Deal.Modules.Discovery Deal.Modules.Settings Deal.Modules.Tenants ``` - [ ] **Step 2: Написать тест на маркеры модулей** `tests/Deal.Tests.Unit/MarkerTests.cs`: ```csharp using Deal.Modules.Pipeline; namespace Deal.Tests.Unit; public sealed class MarkerTests { [Fact] public void PipelineModuleMarker_IsPublicAndSealed() { Assert.True(typeof(PipelineModuleMarker).IsPublic); Assert.True(typeof(PipelineModuleMarker).IsSealed); } } ``` - [ ] **Step 3: Запустить тесты** Run: `dotnet test tests/Deal.Tests.Unit` Expected: 1 тест PASS. - [ ] **Step 4: Зафиксировать в отчёте** `task-4-report.md`. --- ### Task 5: Dev-Postgres в docker compose (схема на тенанта) **Files:** - Create: `deploy/compose.dev.yml` - Create: `deploy/.env.example` - Modify: `README.md` (инструкция запуска dev-БД) **Interfaces:** - Produces: dev-контейнер Postgres 16; БД `deal`; схема `public` готова к миграциям. - [ ] **Step 1: Создать `deploy/compose.dev.yml`** ```yaml services: postgres: image: postgres:16-alpine container_name: deal-postgres environment: POSTGRES_DB: deal POSTGRES_USER: deal POSTGRES_PASSWORD: deal_dev_password ports: - "5433:5432" # 5432 может быть занят LeadRadar-стеком volumes: - deal_pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U deal -d deal"] interval: 5s timeout: 3s retries: 10 volumes: deal_pgdata: ``` - [ ] **Step 2: Создать `deploy/.env.example`** ``` DEAL_PG_HOST=localhost DEAL_PG_PORT=5433 DEAL_PG_DB=deal DEAL_PG_USER=deal DEAL_PG_PASSWORD=deal_dev_password ``` - [ ] **Step 3: Поднять контейнер** Run: `docker compose -f deploy/compose.dev.yml up -d` Expected: `deal-postgres` running, healthy. - [ ] **Step 4: Проверить подключение** Run: ```bash docker exec deal-postgres psql -U deal -d deal -c "SELECT current_database(), current_schema();" ``` Expected: `deal | public` - [ ] **Step 5: Дополнить README.md разделом «Запуск dev-окружения»** ```markdown ## Запуск dev-окружения Postgres (схема на тенанта): `docker compose -f deploy/compose.dev.yml up -d` ``` - [ ] **Step 6: Зафиксировать в отчёте** `task-5-report.md`. --- ### Task 6: Tenant-контекст и подключение к Postgres **Files:** - Create: `src/core/Deal.SharedKernel/Tenants/TenantId.cs` - Create: `src/core/Deal.SharedKernel/Tenants/ITenantContext.cs` - Create: `src/core/Deal.Infrastructure/Data/TenantContext.cs` - Create: `src/core/Deal.Infrastructure/Data/ConnectionStringProvider.cs` - Modify: `Deal.Api/Program.cs` - Test: `tests/Deal.Tests.Unit/TenantIdTests.cs` **Interfaces:** - Produces: - `TenantId` — readonly record struct, обёртка над строкой. - `ITenantContext` — `TenantId? TenantId { get; }`, `bool HasTenant { get; }`, `string? SchemaName { get; }`. - `TenantContext` — реализация на AsyncLocal. - `ConnectionStringProvider` — строка подключения с `search_path`. - [ ] **Step 1: `TenantId.cs` (1 тип = 1 файл)** ```csharp namespace Deal.SharedKernel.Tenants; /// Идентификатор тенанта. Инвариант: непустой. public readonly record struct TenantId(string Value) { public string Value { get; } = string.IsNullOrWhiteSpace(Value) ? throw new ArgumentException("TenantId не может быть пустым", nameof(Value)) : Value; /// Имя схемы Postgres для тенанта. public string SchemaName => $"tenant_{Value}"; } ``` - [ ] **Step 2: Тест `TenantIdTests.cs`** ```csharp using Deal.SharedKernel.Tenants; namespace Deal.Tests.Unit; public sealed class TenantIdTests { [Fact] public void SchemaName_PrefixesTenant() { var id = new TenantId("abc123"); Assert.Equal("tenant_abc123", id.SchemaName); } [Fact] public void TenantId_Empty_Throws() { Assert.Throws(() => new TenantId("")); } } ``` - [ ] **Step 3: Запустить тесты** Run: `dotnet test tests/Deal.Tests.Unit` Expected: 3 теста PASS. - [ ] **Step 4: `ITenantContext.cs`** ```csharp namespace Deal.SharedKernel.Tenants; /// Контекст текущего тенанта запроса. public interface ITenantContext { TenantId? TenantId { get; } bool HasTenant { get; } /// Имя схемы текущего тенанта или null для системного контекста (public). string? SchemaName { get; } } ``` - [ ] **Step 5: `TenantContext.cs` (реализация в Infrastructure)** ```csharp using Deal.SharedKernel.Tenants; namespace Deal.Infrastructure.Data; /// Контекст тенанта на AsyncLocal: пробрасывается через весь запрос. public sealed class TenantContext : ITenantContext { private static readonly AsyncLocal Current = new(); public TenantId? TenantId => Current.Value; public bool HasTenant => Current.Value is not null; public string? SchemaName => Current.Value?.SchemaName; public void SetTenant(TenantId tenantId) => Current.Value = tenantId; } ``` - [ ] **Step 6: `ConnectionStringProvider.cs`** ```csharp using Deal.SharedKernel.Tenants; using Microsoft.Extensions.Configuration; namespace Deal.Infrastructure.Data; /// Строит строку подключения к Postgres с учётом схемы тенанта. public sealed class ConnectionStringProvider { private readonly string _baseConnectionString; public ConnectionStringProvider(IConfiguration configuration) { _baseConnectionString = configuration.GetConnectionString("DealPostgres") ?? throw new InvalidOperationException("ConnectionStrings:DealPostgres не задан"); } /// Строка подключения; при tenantId не null добавляет search_path к схеме тенанта. public string ForTenant(TenantId? tenantId) { if (tenantId is null) { return _baseConnectionString; } return $"{_baseConnectionString};Search Path={tenantId.Value.SchemaName}"; } } ``` - [ ] **Step 7: Подключить в `Program.cs` (DI)** ```csharp using Deal.Infrastructure.Data; var builder = WebApplication.CreateBuilder(args); builder.Services.AddSingleton(); builder.Services.AddSingleton(); var app = builder.Build(); ``` (недостающие `using Deal.SharedKernel.Tenants;` добавить по месту) - [ ] **Step 8: Собрать и прогнать тесты** Run: `dotnet build Deal.sln && dotnet test tests/Deal.Tests.Unit` Expected: build 0 ошибок, тесты PASS. - [ ] **Step 9: Зафиксировать в отчёте** `task-6-report.md`. --- ### Task 7: EF Core + миграции (public) **Files:** - Create: `src/core/Deal.Infrastructure/Persistence/DealDbContext.cs` - Create: `src/core/Deal.Infrastructure/Persistence/Entities/TenantEntity.cs` - Create: `src/core/Deal.Infrastructure/Persistence/DealDbDesignTimeFactory.cs` - Modify: `Deal.Api/Program.cs` (регистрация DbContext) - Test: `tests/Deal.Tests.Unit/TenantEntityTests.cs` **Interfaces:** - Produces: - `DealDbContext` — базовый DbContext; системная сущность Tenant в схеме `public`. - Миграция `InitialPublic`, применённая к `public`. - [ ] **Step 1: Добавить EF Core пакеты в Infrastructure** ```bash cd /c/telbase/src/core dotnet add Deal.Infrastructure package Microsoft.EntityFrameworkCore dotnet add Deal.Infrastructure package Npgsql.EntityFrameworkCore.PostgreSQL dotnet add Deal.Infrastructure package Microsoft.EntityFrameworkCore.Design ``` - [ ] **Step 2: `TenantEntity.cs` (в `Deal.Infrastructure/Persistence/Entities/`)** ```csharp namespace Deal.Infrastructure.Persistence.Entities; /// Тенант в системной схеме public. public sealed class TenantEntity { public Guid Id { get; set; } public string Name { get; set; } = string.Empty; public string Status { get; set; } = "active"; public DateTimeOffset CreatedAt { get; set; } } ``` - [ ] **Step 3: `DealDbContext.cs`** ```csharp using Deal.Infrastructure.Persistence.Entities; using Microsoft.EntityFrameworkCore; namespace Deal.Infrastructure.Persistence; /// Базовый DbContext. Системные сущности — в схеме public. public sealed class DealDbContext(DbContextOptions options) : DbContext(options) { public DbSet Tenants => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => { entity.ToTable("tenants", "public"); entity.HasKey(x => x.Id); entity.Property(x => x.Name).HasMaxLength(200).IsRequired(); }); } } ``` - [ ] **Step 4: `DealDbDesignTimeFactory.cs`** ```csharp using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; namespace Deal.Infrastructure.Persistence; /// Фабрика для dotnet-ef (миграции). Читает строку подключения из env. public sealed class DealDbDesignTimeFactory : IDesignTimeDbContextFactory { public DealDbContext CreateDbContext(string[] args) { var connectionString = Environment.GetEnvironmentVariable("DEAL_PG_CONNECTION") ?? "Host=localhost;Port=5433;Database=deal;Username=deal;Password=deal_dev_password"; var options = new DbContextOptionsBuilder() .UseNpgsql(connectionString) .Options; return new DealDbContext(options); } } ``` - [ ] **Step 5: Регистрация DbContext в Program.cs** ```csharp using Deal.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; var connectionString = builder.Configuration.GetConnectionString("DealPostgres") ?? "Host=localhost;Port=5433;Database=deal;Username=deal;Password=deal_dev_password"; builder.Services.AddDbContext(options => options.UseNpgsql(connectionString)); ``` (в `appsettings.Development.json` положить `ConnectionStrings:DealPostgres`; в проде — из env) - [ ] **Step 6: Создать `appsettings.Development.json` в Deal.Api** ```json { "ConnectionStrings": { "DealPostgres": "Host=localhost;Port=5433;Database=deal;Username=deal;Password=deal_dev_password" } } ``` - [ ] **Step 7: Установить dotnet-ef tool и создать миграцию** ```bash dotnet tool install --global dotnet-ef cd /c/telbase/src/core dotnet ef migrations add InitialPublic --project Deal.Infrastructure --startup-project Deal.Api ``` - [ ] **Step 8: Применить миграцию к public** ```bash dotnet ef database update --project Deal.Infrastructure --startup-project Deal.Api ``` - [ ] **Step 9: Проверить таблицу** ```bash docker exec deal-postgres psql -U deal -d deal -c "\dt public.*" ``` Expected: таблицы `tenants`, `__EFMigrationsHistory`. - [ ] **Step 10: Тест `TenantEntityTests.cs`** ```csharp using Deal.Infrastructure.Persistence.Entities; namespace Deal.Tests.Unit; public sealed class TenantEntityTests { [Fact] public void TenantEntity_Defaults_AreValid() { var entity = new TenantEntity(); Assert.Equal("active", entity.Status); Assert.NotEqual(Guid.Empty, entity.Id == Guid.Empty ? Guid.Empty : entity.Id); } } ``` (тест проверяет дефолты; при необходимости скорректировать под реальную модель) - [ ] **Step 11: Зафиксировать в отчёте** `task-7-report.md`. --- ### Task 8: Применение миграций ко всем схемам тенантов **Files:** - Create: `src/core/Deal.Infrastructure/Migrations/TenantSchemaMigrator.cs` - Test: `tests/Deal.Tests.Unit/TenantSchemaMigratorTests.cs` **Interfaces:** - Consumes: `TenantId`. - Produces: `TenantSchemaMigrator` — чистые функции формирования SQL для схем тенантов. - [ ] **Step 1: Написать тест** `tests/Deal.Tests.Unit/TenantSchemaMigratorTests.cs`: ```csharp using Deal.Infrastructure.Migrations; namespace Deal.Tests.Unit; public sealed class TenantSchemaMigratorTests { [Fact] public void CreateSchemaSql_IsEscaped() { var sql = TenantSchemaMigrator.CreateSchemaSql("tenant_abc"); Assert.Contains("CREATE SCHEMA IF NOT EXISTS \"tenant_abc\"", sql); Assert.DoesNotContain("; DROP", sql); } [Fact] public void CreateSchemaSql_EscapesQuotes() { var sql = TenantSchemaMigrator.CreateSchemaSql("tenant_a\"b"); Assert.DoesNotContain("\"b\"", sql); } } ``` - [ ] **Step 2: `TenantSchemaMigrator.cs`** ```csharp namespace Deal.Infrastructure.Migrations; /// Миграции схем тенантов. Чистые функции формирования SQL. public static class TenantSchemaMigrator { /// SQL создания схемы тенанта. Имя экранируется (не интерполируется из ввода). public static string CreateSchemaSql(string schemaName) { var escaped = schemaName.Replace("\"", "\"\""); return $"CREATE SCHEMA IF NOT EXISTS \"{escaped}\""; } /// Имена схем тенантов из БД. public static string ListTenantSchemasSql() => "SELECT schema_name FROM information_schema.schemata WHERE schema_name LIKE 'tenant\\_%' ESCAPE '\\'"; } ``` - [ ] **Step 3: Запустить тесты** Run: `dotnet test tests/Deal.Tests.Unit` Expected: PASS. - [ ] **Step 4: Зафиксировать в отчёте** `task-8-report.md`. --- ### Task 9: CI-скрипты и финальная проверка этапа **Files:** - Create: `scripts/build.sh` - Create: `scripts/test.sh` **Interfaces:** - Produces: воспроизводимая сборка и тесты одной командой. - [ ] **Step 1: `scripts/build.sh`** ```bash #!/usr/bin/env sh set -e cd "$(dirname "$0")/../src/core" dotnet build Deal.sln ``` - [ ] **Step 2: `scripts/test.sh`** ```bash #!/usr/bin/env sh set -e cd "$(dirname "$0")/../src/core" dotnet test tests/Deal.Tests.Unit ``` - [ ] **Step 3: Прогнать оба скрипта** Run: `sh scripts/build.sh && sh scripts/test.sh` Expected: build succeeded, все тесты PASS. - [ ] **Step 4: Итоговая проверка этапа** Run: - `dotnet build Deal.sln` — 0 ошибок, 0 предупреждений; - `dotnet test tests/Deal.Tests.Unit` — все PASS; - `docker ps` — `deal-postgres` healthy; - `curl http://localhost:5080/api/health` — `{"ok":true,"service":"deal"}`. - [ ] **Step 5: Зафиксировать в отчёте** `task-9-report.md` + обновить `progress.md`. --- ## Self-Review **1. Spec coverage (дизайн-док):** - §2 (стратегия/структура) → Task 1, 3. - §3 (структура src/) → Task 1, 3. - §4 (мультитенантность: схема на тенанта, search_path) → Task 5, 6, 7, 8. - §10 (деплой compose) → Task 5. - §11 (стандарты: editorconfig, анализаторы, 1 тип = 1 файл) → Task 2, все задачи. - Frontend-перенос → Task 1. - Сервисы ml/ai/telegram — пустые каталоги (Task 1); их sln создаются в следующих этапах (вне scope этапа 0). - Auth/инвайты/лимиты — следующие этапы (вне scope «каркаса»). **2. Placeholder scan:** код во всех шагах конкретный. Task 7 Step 10 — тест на дефолты TenantEntity упрощён, с пометкой скорректировать под реальную модель. **3. Type consistency:** `TenantId`, `ITenantContext`, `TenantContext`, `ConnectionStringProvider`, `TenantSchemaMigrator`, `DealDbContext`, `TenantEntity` — имена и сигнатуры согласованы между задачами 6–8. **Вне scope этапа 0:** auth/сессии, модули с бизнес-логикой, gRPC-сервисы, .proto, админка, observability, безопасность сервисов, лимиты — отдельные планы следующих этапов.