Каждый осознанный компромисс из дорожной карты «прототип → продукт» фиксируется здесь: что упростили → почему → влияние → как чинить после MVP. Перед любым расширением просматривайте этот файл: закрытые пункты помечайте ✅ и переносите в CHANGELOG.
Формат: ID | Блок | Компромисс | Почему | Влияние | План исправления
| ID | Компромисс | Почему | Влияние | План после MVP |
|---|---|---|---|---|
| A-C1 | Circuit breaker и token bucket живут в памяти процесса | Нет Redis-зависимости на старте; один asyncio-loop | Multi-worker деплой за балансировщиком видит разные стейты breaker'ов; квота делится неточно | Перенести счётчики в Redis (точка замены — core.llm_gateway.get_breaker/get_bucket); либо sticky routing |
| A-C2 | SQLite вместо Postgres | Нулевая инфраструктура для MVP | Ограничение по конкурентной записи и горизонтальному масштабу | SQLAlchemy async + Alembic + Postgres (docker-compose уже имеет профиль postgres) |
| A-C3 | /health?deep=true пингует провайдеров только вручную |
Дешёвые k8s-пробы не должны генерить трафик к внешним API | Деградация LLM не видна в базовой пробе | Синтетический мониторинг снаружи (self-check по расписанию) + алерт |
| A-C4 | ✅ (2026-08-29) SLO-таблица в README без фактических цифр | Нужен прогон locust на целевом железе | Цели заявлены, но не подтверждены измерениями | Закрыто с честной оговоркой: README ранее вообще не содержал числовых SLO (только упоминание каталога в дереве проекта) — добавлена реальная измеренная таблица p50/p95/p99 (docs/LOAD_TEST_RESULTS.md), явно помеченная «локальный dev-ноутбук, не прод-железо». Добавлен MockProvider (PROVIDER=mock) — бесплатный, без сети, для прогона locust без платных вызовов LLM. Второй сценарий подтверждает circuit breaker/retry-queue под гарантированно падающим провайдером. Nightly CI (nightly-loadtest.yml) прогоняет happy-path сценарий на каждый релизный тег. Прогон на реальном прод-железе остаётся будущим шагом |
| A-C5 | Retry-воркер внутри процесса FastAPI | Без Celery/Redis для MVP | При остановке приложения очередь ждёт следующего старта; нет горизонтального масштабирования воркеров | Вынести в Celery/RQ worker при росте объёмов |
| ID | Компромисс | Почему | Влияние | План после MVP |
|---|---|---|---|---|
| B-C1 | Поиск похожих pHash — линейный скан Python-кандидатов | SQLite не умеет popcount; объёмы MVP малы | O(N) на запрос; при сотнях тысяч хэшей замедлится | pgvector/Faiss по эмбеддингам (CLIP) — заодно закроет reverse search по смыслу |
| B-C2 | ELA-калибровка эвристическая (RMS×6, порог 25) | Нет размеченной выборки реальных карточек | Возможны ложные флаги на «шумных» фото | Калибровка на golden dataset из реальных фото; ROC-подбор порога |
| B-C3 | Консенсус — ровно второй провайдер (не ансамбль N) | Стоимость/латентность | Спорные случаи разрешаются двумя мнениями, а не тремя+ | Расширить asyncio.gather до N моделей; агрегация большинством |
| B-C4 | Форензика модулирует confidence максимум ±15 | Предсказуемость и аудит важнее агрессивной коррекции | Ярлык вердикта всегда за LLM | После набора статистики — пересмотр весов формулы final_score |
| ID | Компромисс | Почему | Влияние | План после MVP |
|---|---|---|---|---|
| C-C1 | Один tick-job планировщика, расписание в БД | Проще и персистентно без SQLAlchemy job-store | Менее гибко, чем per-watch jobs; джиттер до минуты | APScheduler job-store в БД при необходимости секундной точности |
| C-C2 | Эталонные фото watch'ей — base64 в SQLite | Нет файлового/S3-хранилища в MVP | Раздувает БД; лимит размера картинки | Файловое хранилище / S3-совместимое (MinIO) |
| C-C3 | Ozon/Yandex discovery — HTML link-extraction (только ссылки) | JS-rendering полный парсинг дорог; названия/цены добираются при фетче карточки | Метаданные листинга беднее, чем у WB | Полноценные парсеры выдачи через браузерную ферму / официальные API партнёров |
| C-C4 | ✅ (2026-08-29) Email-дайджест — заготовка (лог) | Не выбран SMTP-провайдер | Уведомления только в Telegram | Закрыто: app/email_alerts.py (SMTP-агностично, через SMTP_* env, без привязки к конкретному провайдеру), HTML-шаблон app/templates/emails/digest.html.j2, отправка встроена в существующий тик discovery-планировщика (maybe_send_digest). Новое поле digest_email у brand watch; создание watch с digest_email при не настроенном SMTP — явная ошибка 400 (не тихое молчание) |
| C-C5 | Сканирование выдачи может триггерить антибот | Используются публичные интерфейсы площадок | Возможны пропуски/блокировки IP | Прокси-пул + ротация UA; мониторинг успешности сканов как метрика |
| ID | Компромисс | Почему | Влияние | План после MVP |
|---|---|---|---|---|
| E-C1 | Time-to-detection считается только для Discovery-находок | Дата публикации карточки конкурентом неизвестна для ручных проверок | Метрика TTD отражает только автоматический мониторинг | Парсинг даты публикации из карточки/первого появления в выдаче (историческая серия сканов) |
| E-C2 | PPTX-экспорт — компактная текстовая дека (без графиков-картинок) | Рендер Chart.js в изображения требует headless-браузера | Презентация без визуальных графиков | Снимок canvas через JS → загрузка картинки → вставка в слайд, либо серверный рендер |
| E-C3 | ✅ частично (2026-08-29) Chart.js с CDN на фронтенде | Не bundled-ассет в монолитном index.html | Дашборд не работает офлайн / при блокировке CDN | Проверено: frontend/ (React, Recharts) не имеет ни одной CDN-зависимости (JS, шрифты) — уже bundled, уже под строгим default-src 'self' CSP в frontend/nginx.conf. Осталось открытым только для legacy/index.html (Chart.js + Google Fonts с CDN, до сих пор отдаётся FastAPI на /) — решение сознательно отложено: legacy/ заморожен, полноценное закрытие требует отдельного продуктового решения о его судьбе (переезд на React-фронтенд vs локальный бандл в статическом файле без сборки) |
| E-C4 | ✅ (2026-08-29, disclaimer) «Защищённая выручка» — грубая оценка (fakes × avg price) | Нет данных о конверсии и каннибализации спроса | Цифра для отчётности, не для финансового учёта | Disclaimer-часть закрыта: React-дашборд показывает реальный disclaimer бэкенда рядом с цифрой (был баг несовпадения полей protected_revenue/methodology vs фактических protected_revenue_estimate/disclaimer — из-за него цифра/пояснение вообще не отображались верно, исправлено), заголовок переименован в «Оценка защищённой выручки». PPTX-экспорт теперь тоже несёт disclaimer рядом с цифрой (раньше — голая цифра в булите). Legacy-дашборд уже был корректен. Методологическая часть (коэффициент конверсии, каннибализация, калибровка по возвратам) остаётся будущим шагом |
| ID | Компромисс | Почему | Влияние | План после MVP |
|---|---|---|---|---|
| F-C1 | ✅ (2026-08-29) Изоляция тенантов — фильтрация tenant_id в SQL-запросах, без Row-Level Security |
SQLite не поддерживает RLS; фильтры централизованы в db-функциях | Один пропущенный WHERE = утечка между тенантами | Закрыто частично: добавлен app-level defense-in-depth tenancy.ensure_owned() (второй, независимый от SQL, гейт для всех get-by-id роутов) + систематический регресс-сьют tests/test_tenant_isolation.py + чеклист в docs/ARCHITECTURE.md. Postgres RLS остаётся будущим шагом после миграции (A-C2) — SQL-уровень по-прежнему не имеет защиты на уровне СУБД |
| F-C2 | Биллинг — нормализованный контракт вебхуков, а не нативные события Stripe/Yookassa | Разные форматы событий провайдеров; для MVP достаточно активации/отмены | Реальные payload'ы требуют адаптеров на стороне интеграции | Тонкие адаптеры под каждый провайдер + обработка invoice/payment_intent |
| F-C3 | Роли хранятся на ключе, а не на пользователе (user == ключ) | Нет полноценной системы пользователей/сессий в MVP | «Users» в лимитах тарифа = число активных ключей | Таблица users + сессии/JWT; ключи становятся сервисными токенами |
| F-C4 | Партнёрский rate limit в памяти процесса | Единый механизм с token bucket блока A | На multi-worker деплое лимит умножается на число воркеров | Общий bucket в Redis вместе с F-C1 |
| F-C5 | ✅ (2026-08-29) Open mode (без API_SECRET_KEY) остаётся доступным | Обратная совместимость фронтенда и локальной разработки | В продакшене забыть задать секрет = незакрытый API | Закрыто: startup эмитит logger.warning(...) при open mode; STRICT_AUTH=1 (app/core/config.py) обрывает старт (RuntimeError) без API_SECRET_KEY; docker-compose.yml/docs/DEPLOY.md рекомендуют STRICT_AUTH=1 для клиентских деплоев (публичное демо намеренно остаётся open mode) |
| F-C6 | pHash-кэш вердиктов глобальный, а не per-tenant | Экономия LLM-вызовов: одно и то же фото контрафакта на разных карточках не анализируется дважды | Tenant B может получить мгновенный вердикт, найденный для tenant A (только факт классификации фото, без пользовательских данных) | Флаг PHASH_CACHE_SCOPE=global|tenant в конфиге; по умолчанию оставить global |
| ID | Компромисс | Почему | Влияние | План после MVP |
|---|---|---|---|---|
| D-C1 | ✅ (2026-08-29) Скриншот карточки — best-effort: при недоступном браузере делается при генерации PDF, а не строго «на момент проверки» | Полноценный браузер на каждую проверку дорог; MVP-баланс | Скриншот может отражать состояние страницы на момент генерации пакета — ослабляет chain of custody для этого файла | Закрыто: screenshot_queue + screenshot_retry_worker.py — захват ставится в очередь немедленно при анализе (requested_at = момент анализа), с backoff-ретраями. PDF-эндпоинт больше не делает live-захват «на лету»; вместо этого evidence_store.get_screenshot_status() честно репортит captured/captured_late/pending/unavailable, PDF показывает «Дата анализа» и «Дата захвата скриншота» отдельными полями |
| D-C2 | Артефакты доказательств на локальной ФС (EVIDENCE_DIR/) |
Нет S3 в MVP | Не переживают пересоздание контейнера; нет репликации | S3/MinIO + версионирование объектов |
| D-C3 | PDF на reportlab с базовой вёрсткой | Быстро, без headless-Chromium печати HTML | Ограниченная типографика; нет подписи/печати | WeasyPrint из HTML-шаблона + электронная подпись (КЭП) при юридических требованиях |
| D-C4 | SLA-часы зашиты в константу DEFAULT_SLA_HOURS |
Конфигурируемость не была критична для первого workflow | Смена SLA требует правки кода | Перенести в таблицу/настройки tenant'а (естественно ляжет в Block F) |
| D-C5 | Один кейс = одна проверка (нет объединения карточек одного продавца в групповое дело) | Проще аудит и переходы | Продавец с 20 карточками = 20 кейсов | Группировка по seller_id + parent-case |
- Любое решение «сделаем проще сейчас» попадает сюда в момент принятия, а не потом.
- Перед началом нового блока — просмотреть открытые пункты затрагиваемых подсистем.
- После стабилизации MVP пункты закрываются по приоритету влияния; факт закрытия — в CHANGELOG.