Commit Graph

2742 Commits

Author SHA1 Message Date
Дмитрий 2b18922db6 docs(автоподбор): песочница шага 2 — все 5 конкурентов на единой новой модели
Перегнал было/станет для 385 Автозайм124, 412 Драйв займ, 414 Банк ДОМ.РФ,
416 Ваш Инвестор, 420 КрасЛомбард на одном движке шага 2: 2ГИС ищется
поиском по имени фирмы (карточка находится, даже если её не было в выдаче),
Яндекс.Карты вскрываются Playwright, склейка кир/лат доменов, подменные
коллтрекинг-номера помечены отдельно от настоящих.

Артефакты — только dev, боевой шаг 2 не изменён.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:51:43 +03:00
Дмитрий ec4a2b178c feat(автоподбор): песочница «глобального шага 2» + артефакты было/станет
Прототип нового шага 2 «Собрать источники» — отдельный скрипт scripts/sandbox-step2.php,
боевой шаг 2 НЕ тронут. Отпечаток фирмы (домен+имя+тип+город) → поиск по отпечатку через EXA
(гео-скоуп по типу фед/рег) → фильтр «точно она» по якорному телефону → глубокое вскрытие
сайтов (код/скрытые/подменный коллтрекинг/Контакты) + 2ГИС(поиск по имени, JSON) + Яндекс(Playwright)
→ все номера с пометками. Генерит HTML «было/станет» на конкурента (было из БД, станет из прогона),
ссылки кликабельные. scripts/_shot.cjs — скриншот HTML в PNG.

Артефакты bylo-stanet/ на 5 конкурентов с «Предложениями» (tenant 36). Свежие со всеми фиксами —
КрасЛомбард и Банк ДОМ.РФ; остальным нужен финальный перегон. Детали, дизайн и 4 фикса
(схема сайта, Яндекс→Playwright, 2ГИС по имени/JSON/ретрай, близнецы кир/лат) — в файле находок.

НЕ в портале, НЕ на проде. Прототип для оценки.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:22:13 +03:00
Дмитрий df83fa836c feat(автоподбор): Конкурентное поле — финальный проход склейки, вскрыть всё, группы дублей, телефоны, отсев рекламных номеров 2ГИС
Шаг 1 «Конкурентное поле» — чекпоинт:
- Финальный проход «Найти и объединить дубли» на поле и предложениях; клиент решает по каждой группе, ничего не склеиваем молча.
- Тихая склейка только по сайту и коду справочника; телефон и людный номер у более чем 4 фирм — на решение клиента.
- Колонка phones jsonb; телефон-дубли видны уже на «Предложениях».
- Sonar даёт только имя и тип, сайт всегда через EXA; вскрываем все карточки без гейта «есть сайт».
- Яндекс-карточки через локальный Playwright параллельно; рубрика из заголовка идёт в описание.
- Фикс рекламных номеров: 2ГИС-парсер разбирает телефоны пообъектно и выкидывает рекламные кнопки с platforms/caption чужих фирм.

Тесты: автоподбор 224/224 unit, 105/105 feature; фронт-спеки зелёные. Времянки app/scripts/diag-*/probe-* в гит не входят.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:34:03 +03:00
Дмитрий 65770f0e70 docs: снимок 02.07 — launch-gate ЯДРО на проде + автоподбор в воркстри + промт продолжения
Консолидированное состояние двух треков (launch-gate выкачен на liderra.ru core-only;
автоподбор работает только в воркстри) + квирки окружения/тестов + промт для
восстановления контекста после компакта.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:22:53 +03:00
Дмитрий 4c05af7fda docs(биллинг): статус фичи launch-gate — реализовано, на gitea, follow-up
Спека помечена РЕАЛИЗОВАНО (коммиты f78ab4af/9ff7bc8f/8a8783df), ревью opus без
критичных находок. Зафиксирован follow-up: прод-флаг BILLING_LAUNCH_REQUIRES_ACTIVE_TIERS,
DRY-сведение оставшихся копий правила, пред-существующая партиция-хрупкость тест-инфры.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 12:26:41 +03:00
Дмитрий 8a8783df4a fix(биллинг): повышение лимита проекта — evaluate+запись под lockForUpdate(Tenant)
По итогам финального ревью: путь повышения дневного лимита в ProjectController@update
прогонял балансовый гейт и писал новый лимит БЕЗ замка на тенанта (в отличие от
create/setActive). Две параллельные правки лимита одного тенанта могли обе пройти
гейт и суммарно превысить ёмкость баланса (TOCTOU). Обёрнуто в DB::transaction +
Tenant::lockForUpdate() при повышении лимита — evaluate и запись под одним локом.

UpdateLimitGateTest 3/3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 12:21:17 +03:00
Дмитрий 9ff7bc8fa0 feat(биллинг-фронт): баланс на запуске — сообщения в рублях, метка «не запущен», группы
Фронт для правила «баланс блокирует запуск, а не создание»:
- ProjectLimitOverloadDialog переформулирован в «Проект не запущен — пополните
  ~X ₽ или уменьшите объём» (сумма в рублях topup_rub).
- NewProjectDialog: создание всегда успешно; при launch.deferred показывает
  сообщение (не блок). Update-лимита 409 → тот же диалог.
- Возобновление (toggle-active) ловит 409 balance_insufficient (стор не
  переключает is_active), показывает диалог; постоянная метка «Не запущен —
  не хватает баланса» на карточке/в панели по preflight_blocked_at/balance_blocked.
- BulkActionsBar: сводка «Запущено N, отложено M — не хватает баланса» на resume.
- Автоподбор: CreateScreen показывает сводку запуска (рубли); FieldCompetitor
  массовые pause/resume через общий POST /api/projects/bulk (наследует BULK_MAX,
  слепок-защиту, балансовый гейт).
- ProjectResource отдаёт preflight_blocked_at.

Новые/обновлённые фронт-тесты зелёные. Пред-существующий красный baseline
фронт-набора (ErrorView/Settings/Legal/… + ProjectsView v-show) не трогался.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 12:04:16 +03:00
Дмитрий f78ab4af3d feat(биллинг): баланс блокирует запуск проекта, а не создание — единый гейт
Создавать проекты можно всегда; баланс проверяется в момент ЗАПУСКА
(создать-и-запустить / запустить / возобновить одиночно и пачкой / автоподбор)
под замком на клиента (без гонок). Не хватает — проект остаётся на паузе с
меткой preflight_blocked_at, клиенту сообщение в рублях (сколько пополнить).
Групповой запуск «сколько влезло». Нет активного тарифа на дату → запуск
запрещён (fail-closed). Гейт реквизитов первого проекта добавлен и в автоподбор.

- LaunchBalanceGate — единый гейт вместо 3 копий preflight (ProjectController
  store/update, AutopodborController), под DB::transaction + lockForUpdate(Tenant).
- ProjectService::create($launch) + новый setActive(); bulk resume «сколько влезло».
- AutopodborProjectCreator: пачка в транзакции через общий ProjectService::create.
- Идемпотентность box/phone_type миграций автоподбора (Schema::hasColumn guard).
- Тест-инфра: afterRefreshingDatabase восстанавливает месячные партиции.

Тесты фичи 40/40 зелёные. Спека и план — docs/superpowers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:07:22 +03:00
Дмитрий a88c4034b7 docs(автоподбор): снимок 02.07 — живой движок работает end-to-end + ускорение 2ГИС + промт продолжения
Зафиксировано состояние: фича Stage 1+2 собрана, живой сбор проверен (прогон #12 ~8 мин, 139 фирм 2ГИС,
14 сохранено, 300₽), коммит b3f5158e, квирки (лимит xfetch, EXA OS-env, очередь-локально, node-путь) и
промт для восстановления контекста. Детали движка — в памяти project-autopodbor-engine-live-working-2026-07-02.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 06:30:03 +03:00
Дмитрий b3f5158e48 perf/fix автоподбор: параллельный сбор 2ГИС по 3 + большие таймауты + безопасное письмо + node из config
Ускорение шага 1: CategoryScraper грузит страницы категории 2ГИС не по одной, а пачками поперёк рубрик
через BatchPageFetcher. На остывшем xfetch 2ГИС упал с ~23 мин до ~5 мин и собрал 139 фирм вместо 12.
Общий лимит одновременных xfetch снижен 4→3 (безопаснее под оконный rate-limit сервиса).

Таймауты ИИ/EXA подняты с запасом (латентность моделей плавает, сбор асинхронный, клиент ждёт):
sonar 120→300с, отсев агрегаторов 90→240с, общий AITUNNEL 30→120с, EXA 30→90с. Джоба: один прогон
без авто-ретраев (tries 3→1, чтобы таймаут не перезапускал платный движок трижды) и общий таймаут 900→1800с.

Фиксы, найденные на живом прогоне:
- Письмо «готово» больше не роняет прогон при недоступной почте (тесты на сбой SMTP при status done/empty).
- Яндекс-загрузчик берёт путь к node из config (autopodbor.node_bin, дефолт node) — из-под PHP node
  теперь находится; на живом прогоне Яндекс дал 78 фирм вместо 0.

По TDD. Бэкенд автоподбора 262/262, Pint чисто. Живой прогон end-to-end: ~8 мин, 14 новых конкурентов,
списание 300 ₽ корректно.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 05:43:34 +03:00
Дмитрий 76c67e1fc5 feat автоподбор параллельная загрузка карточек справочника пачками по 4
Замер боевого лимита xf4.ru показал безопасный потолок 4 одновременных запроса — выше идут 429 Too Many Requests, плюс есть лимит за окно времени. Сбор до 25 карточек филиалов в XfetchDirectoryFetcher грузился по одной секунды на каждую — узкое место шага 2.

Новый интерфейс BatchPageFetcher + XfetchClient::htmlBatch грузит страницы порциями по services.xfetch.concurrency дефолт 4, параллелит внутри порции через Http::pool, ретраит пустые и 429. XfetchDirectoryFetcher берёт карточки одной пачкой если загрузчик batch-совместимый, иначе по одной. Два справочника шага 2 оставлены последовательно намеренно — иначе 2x4=8 за лимитом. Число выносится в XFETCH_CONCURRENCY без деплоя.

По TDD. Бэкенд автоподбора 258/258, Pint чисто.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 18:52:18 +03:00
Дмитрий 9a9164fee2 feat автоподбор Stage 2 актуализация — новый сайт фирмы поля через Добавить/Заменить в форму проекта
Секция «На актуализацию»: дельта-карточка показывает текущие сайты и карточки фирмы поля + новый сайт кликабельными ссылками, кнопки Добавить и Заменить ведут через подтверждение «Создать проект?» в существующую форму CreateScreen с подставленным новым источником. Имя фирмы берётся из поля, не перезатирается.

Бэкенд: endpoint POST /competitors/{id}/actualize action=add|replace new_site. add заводит сайт-источник рядом со старым, replace архивирует старые сайт-источники и заводит новый, при активном проекте по старому сайту возвращает 409 manage_via_project. /proposals обогащён полем matched с текущими сайтами и карточками совпавшей фирмы.

По TDD. Бэкенд автоподбора 254/254, фронт автоподбора 64/64, ESLint чисто, Pint чисто.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 18:27:00 +03:00
Дмитрий 82123057f5 docs(автоподбор): спека + план фичи сверки находок + файл состояния Stage 1 и промт продолжения
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 15:47:12 +03:00
Дмитрий 5a861e0e85 style(автоподбор): pint-форматирование — скобки конструкторов, порядок импортов, выравнивание; логика без изменений
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 15:45:37 +03:00
Дмитрий 6d48503a0e feat(автоподбор): сверка находок со состоянием клиента — 3 группы предложений + скрытые дубли, мягкое удаление в архив, убран вычет себя
Stage 1: опознавалка фирмы по сайту/карточкам, ProposalClassifier new/actualize/archived/hidden, box=archived, bulk удалить всех ранее удалённых. Backend 249/249, front autopodbor 61/61.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 15:45:11 +03:00
Дмитрий fc4e4bacbf feat(автоподбор): центр похожести из описания ниши, а не только доменов-примеров
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 15:44:43 +03:00
Дмитрий 68341e5576 fix(автоподбор): дедуп ДО отсева агрегаторов + файл состояния/промт продолжения
Отсев агрегаторов шёл по СЫРЬЮ до склейки — gpt-4o-mini залетало 227 позиций
с дублями (Ломбардико ×6). Порядок в FindCompetitorsAssembler: сначала
склейка/дедуп, потом отсев — классифицируем ~90 уникальных, а не 227.

Плюс файл состояния сессии (что сделано/осталось) + промт восстановления
контекста для компакта. TDD, 234/234 зелёные. НЕ прод.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:27:26 +03:00
Дмитрий f1ab608444 feat(автоподбор): масштаб и чистота шага 1 — пул EXA, очередь-throttle, батч агрегатора, узкие рубрики, дедуп по словарю
Масштаб (лимиты внешних сервисов — на КЛЮЧ, общий на всех клиентов):
- EXA: параллельный пул findSites (concurrency=5) вместо 40 запросов по одному.
- Агрегатор: батч вместо ~90 запросов по одному + чанкинг по 40 и длинный
  таймаут 90с (большой список gpt-4o-mini не успевал в 30с).
- Очередь autopodbor + WithoutOverlapping на всех 3 джобах — глобальный
  потолок = число воркеров; assertNoInFlight держит один подбор на клиента.

Чистота выдачи (универсально, без зашитых ниш):
- Анализатор: узкие рубрики, запрет зонтичных слов («финансовые услуги» тащила
  юрфирмы/банки).
- Дедуп: ключ имени минус слова из СЛОВАРЯ рубрик прогона («Яричъ Ломбард»→«Яричъ»);
  слово-категория из данных, не из списка. Гард: склейка только если остаётся
  ровно один слово-токен (генерики вроде «займы под залог» не трогаем).

TDD, 233/233 (unit+feature) зелёные. НЕ прод.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:18:09 +03:00
Дмитрий c259855349 feat(автоподбор): рубрика из списка Яндекса в описание + межотраслевой промпт
Playwright тянет рубрику фирмы прямо из СПИСКА Яндекса («Ломбард, автоломбард»,
«Микрофинансовая организация», «Банк») без захода в карточку — быстро и бесплатно.
Рубрика идёт в описание конкурента → в похожесть-эмбеддинги (раньше меряли по
голому имени, хлам и целевое сбивались). Промпт анализатора обезличен: примеры
из РАЗНЫХ отраслей (стоматология/автосервис/доставка) + «подбери под описание
клиента» — движок универсален, не подточен под нишу займов Омеги. TDD 216/216, НЕ прод.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 07:00:41 +03:00
Дмитрий 01a057c66c feat(автоподбор): Яндекс дедупит по slug-коду фирмы + код в склейку
Яндекс отдавал по строке на КАЖДЫЙ филиал (дедуп по id точки) — 214 строк,
из них хлам-повторы одной сети (Корунд ×15). Новый YandexListParser дедупит
по SLUG (код фирмы от источника /maps/org/<slug>/id): одна фирма = одна строка,
разные написания одной фирмы склеены, разные фирмы с разным кодом раздельны.
Код справочника (ya:slug) уходит в union-find склейку как опознавательный
признак. Живьём 214 -> 94. TDD, 215/215 (unit+feature) зелёные. НЕ прод.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 06:24:00 +03:00
Дмитрий 6e970d7231 fix(автоподбор): канал В получает чистый стоп-лист — дубли-филиалы схлопнуты
Стоп-лист известных для ИИ канала В дедуплицировался только в проверке
видели/не видели, а в промпт уходил с повторами (Корунд ×27, КрасЛомбард ×19).
Теперь чистим и сам список по ключу имени — ИИ видит каждое имя один раз:
короче, дешевле по токенам, точнее. TDD, 137/137 unit зелёные.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 05:27:45 +03:00
Дмитрий f9f4743392 docs(автоподбор): файл состояния движка + промт восстановления контекста (01.07)
Единая точка: что сделано (движок шага 1 пересобран — АНАЛИЗ→канал А 2ГИС(xfetch)+Яндекс
(Playwright)→слияние→канал В федералы(EXA)→сборка; 136 unit+74 feature зелёные, HEAD dee2ebbc),
что осталось (полный живой прогон+сверка на 72, калибровка, параллельность), ключи/окружение,
карта кода, промт для восстановления после компакта.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 04:55:51 +03:00
Дмитрий dee2ebbcf8 feat(автоподбор): второй справочник канала А — Яндекс.Карты через локальный Playwright + слияние
Firecrawl Яндекс.Карты не рендерит (0-2 орг) — по §12.2 Яндекс берём локальным Playwright.
render-yandex-list.cjs скроллит ленту результатов → 113 орг за ~18с (быстрее xfetch-2ГИС).
YandexDirectory (граница) + PlaywrightYandexDirectory (живой, Process→node). Яндекс = имя+карточка
(сайта в списке нет — только на карточке, не открываем). Оркестратор: канал А = 2ГИС(сайт)+Яндекс,
слияние (mergeCompetitors union-find) схлопывает одного конкурента из обоих справочников в одну
карточку с двумя directory_urls; сайт из 2ГИС. Провайдер подключает живой Яндекс. listingHtml →
общий хелпер tests/Pest.php. Модуль 136 unit + 74 feature зелёные. За флагом; на проде не меняется.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 04:53:58 +03:00
Дмитрий 5d40de664e feat(автоподбор): лёгкий канал А — имя+сайт+карточка из списка 2ГИС без захода в карточку
Шаг 1 = только имя+сайт+ссылка на справочник (телефоны/описание = шаг 2). Убраны медленные
per-card открытия: со страницы категории 2ГИС берём по фирме имя + ссылку карточки + САЙТ сразу
(CategoryListingParser: сайт из редиректа «Перейти на сайт», firmId в пути = id фирмы; мусор
avito/max.ru/трекеры/иностр.TLD отсеиваем). CategoryScraper отдаёт строки {name,card_url,site}
с пагинацией. LiveFindCompetitors упрощён: канал А из списка (без резолва), канал В — имена →
сайт EXA, без сайта (нет якоря) выкидываем. Живой канал А на новом ключе xfetch: 48 реальных
фирм Красноярска, 17 с сайтом, ~4.5 мин, без per-card. Филиалы схлопывает merge (union-find).
Модуль 135 unit + 74 feature зелёные. За флагом autopodbor.real_find; на проде не меняется.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 03:31:16 +03:00
Дмитрий 5a65165114 feat(автоподбор): движок шага 1 пересобран под финал v4 (каналы А+В, EXA)
Замена вырожденного «одна фраза → одна страница» на §12/§11.3 финал:
- Шаг АНАЛИЗ (ChannelA\AitunnelQueryAnalyzer): описание → запросы-рубрики (мелкая модель).
- Канал А (ChannelA\CategoryScraper): скрейп категории 2ГИС с пагинацией → резолв карточек.
- Канал В (ChannelB\*): ОДНА модель sonar-reasoning-pro × 2 прохода → ТОЛЬКО имена
  федералов; стоп-лист = имена из А + примеры; сайт федерала через EXA (ExaSiteFinder),
  т.к. у федерала нет карточки в 2ГИС/Яндексе на регион.
- Оркестратор LiveFindCompetitors переписан: АНАЛИЗ→А→В→слияние→отсев→дедуп→похожесть→DTO.
- Провайдер перепрошит; config services.php +research_model/exa.
Похожесть — эмбеддер-модель (математически), резолвер/дедуп — без изменений.
Всё за тонкими границами, офлайн-тесты на фикстурах: модуль 130 unit + 74 feature зелёные.
Провайдер за флагом autopodbor.real_find; на проде не меняется.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 21:06:10 +03:00
Дмитрий d468471707 docs(автоподбор): зафиксирован последний движок §12 v4 + сверка PHP
Свод воедино: методика §12 (3 канала А/В/0 → резолвер → DaData → дедуп →
эмбеддинги → DTO) + все дословные промпты (5 моделей-исследователей,
нормализатор, фильтр агрегаторов). Честная сверка: PHP LiveFindCompetitors
собрал только нижнюю половину (резолвер/дедуп/похожесть/телефоны) и берёт
1 фразу/1 страницу → 3 находки; верхняя половина (АНАЛИЗ+мульти-запрос+
пагинация канала А, канал В 5-моделей) не перенесена. Эталон работы движка —
прогон 29.06 (72 конкурента). План переделки PHP под §12 один-в-один.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 20:03:48 +03:00
Дмитрий f940c060b8 docs(автоподбор): контекст-промпт продолжения — живой портал :8001 (белый экран) + полное состояние 2026-06-30 19:11:39 +03:00
Дмитрий 062d46e15e feat(автоподбор): письмо «подбор готов» — клиент не ждёт у экрана (фон + уведомление)
RunAutopodborSearchJob по завершении (done/empty) шлёт AutopodborReadyMail на contact_email тенанта
(с числом найденных + ссылкой на портал). Письмо не роняет подбор при недоступной почте (try/catch).
Клиент ставит задачу, работает дальше, получает «готово» письмом.

Тесты: SearchJob 4/4 (вкл. Mail::assertSent); Автоподбор unit+feature 183/183; Pint чисто.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 19:02:28 +03:00
Дмитрий 7e1ff09abe feat(автоподбор): живой ИИ через AITUNNEL — эмбеддинг-похожесть (%) + LLM-отсев агрегаторов
AitunnelEmbedder (text-embedding-3-small) → косинус-похожесть конкурента на профиль клиента (%).
AitunnelAggregatorClassifier (chat) → «поставщик или площадка?» — авто-отсев Авито/Zoon/Банки.ру.
Оба деградируют сами при пустом ключе (пустые векторы → 0%, null → не выкидываем), поэтому
подключены всегда. Ключ — только в .env (services.aitunnel.key), в гит не попадает.

Тесты: AITUNNEL клиенты 9/9; Автоподбор unit+feature 182/182 (флаг real_find в тестах
принудительно ВЫКЛ через phpunit.xml — иначе findCompetitors ходил бы в сеть); Pint чисто.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 18:56:05 +03:00
Дмитрий 405e4bb182 fix(автоподбор): таймаут задания поиска 900с (живой подбор идёт минутами)
RunAutopodborSearchJob.timeout=900 — живой поиск (2ГИС+Яндекс через антибот + обход карточек)
идёт ~4-5 мин; дефолтные 60с убивали задание на середине. Очередь поднимать с --timeout>=900.
2026-06-30 18:49:18 +03:00
Дмитрий adc3590ab6 fix(автоподбор): живой резолв Яндекса (город в заголовке не на фикс. месте) + ретрай флакующей выдачи
Найдено живым прогоном по нише «ломбард/Красноярск»:
1) Яндекс-заголовок «Имя, рубрика, ГОРОД, улица, дом» — город НЕ последний сегмент; брал «дом»
   как город → все карточки отбраковывались. Теперь YandexResolver: имя=первый сегмент, город —
   по НАЛИЧИЮ в заголовке (DirectoryFields::titleName/titleHasCity). 2ГИС (город последний) — без изменений.
2) ResolvingAgent (ручной resolveByName) теперь берёт ГОРОД центра субъекта (RegionCity), не имя региона.
3) Страница выдачи 2ГИС/Яндекс флакует (капча/пустой layout) → LiveFindCompetitors повторяет поиск,
   пока не появятся фирмы (searchRetries).

Тесты: модуль Автоподбора unit 99/99 + live 4/4; Pint чисто.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 18:44:27 +03:00
Дмитрий 48e65d231c feat(автоподбор): шаг1 — проводка живого findCompetitors за флагом autopodbor.real_find
LivePageFetcher (2ГИС→xfetch, Яндекс→xfetch+fallback локальный Playwright). Провайдер при
config('autopodbor.real_find')=true собирает LiveFindCompetitors (без ИИ-ключа: null-классификатор
+ нулевой эмбеддер → сырой список без отсева площадок и без %). RealCompetitorAgent.findCompetitors
использует живой движок, если подключён, иначе заглушку. Флаг по умолчанию ВЫКЛ — на проде без изменений.

Тесты: Автоподбор unit+feature 172/172 (флаг выкл — биндинг цел); Pint чисто.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 18:26:38 +03:00
Дмитрий 84e769e454 feat(автоподбор): шаг1 — живой findCompetitors (канал А: ниша → 2ГИС+Яндекс → резолв → сборка)
LiveFindCompetitors: ниша из формы → поиск 2ГИС (по слагу города) + Яндекс (по «ниша город») →
парсер выдачи → резолв каждой карточки напрямую → FindCompetitorsAssembler (фильтр/слияние/
похожесть → DTO §7.2). Добыча за PageFetcher — обход тестируется офлайн на фикстурах.
RegionCity — слаг/имя города центра субъекта (как RegionAreaCode, только уверенные).

Тесты: live 3/3 (склейка 2ГИС+Яндекс одной фирмы, вычет своего сайта, пустая выдача);
модуль Автоподбора unit 99/99; Pint чисто. Провайдер ещё не флипнут — следующий шаг.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 18:23:29 +03:00
Дмитрий 0636a3c1b4 docs(автоподбор): план шага1 — весь движок собран (A–F+C+D+H-ядро), осталось живое подключение+флип 2026-06-30 18:04:21 +03:00
Дмитрий fcff8ecd47 feat(автоподбор): шаг1 H-ядро — сборка findCompetitors (фильтр→слияние→похожесть→DTO)
FindCompetitorsAssembler: резолвленные кандидаты каналов → отсев агрегаторов (C) →
слияние+дедуп+вычет клиента (E) → отсев федералов если не нужны → похожесть-эмбеддинги (F) →
срез top-N → DTO FindCompetitorsResult (§7.2). Чистая сборка — всё ядро движка v4 складывается
вместе и протестировано офлайн на реальных сервисах + фейках границ.

НЕ флипает провайдер: добыча страниц/имён каналов (живые A-fetch/B/0) и включение боевого
движка за флагом — отдельный шаг (нужно «го» + живой прогон).

Тесты: assembler 2/2; модуль Автоподбора unit 96/96; Pint чисто.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 18:03:25 +03:00
Дмитрий 0fc589792f feat(автоподбор): шаг1 C — отсев агрегаторов LLM-классификатором (не статик-список)
AggregatorFilter + граница AggregatorClassifier (§12.6): «поставщик услуги или площадка?» —
LLM-рассуждением (за границей), а не мёртвым статик-списком. Консервативно: выкидываем только
при уверенном «агрегатор»; при неуверенности (null) конкурента не теряем.

Тесты: aggregator 3/3; Pint чисто. (живой LLM-классификатор подключается на H/живом прогоне.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:59:47 +03:00
Дмитрий 026bc48d41 feat(автоподбор): шаг1 D — обогащение телефонов через DaData (переиспользует живой клиент)
CompetitorPhoneEnricher: телефоны карточки → тип (city/mobile/tollfree) + регион + годность (qc=0)
через существующий DaDataPhoneClient (тот же, что резолв региона лида — §12.4, не дубль).
DaData недоступна → деградация: тип по префиксу (PhoneType), регион пуст, негоден — подбор не падает.
Номера не синтезируются, только классифицируются (берём опубликованные фирмой).

Ключ DaData — в config services.dadata (env, прод). Тест офлайн через Http::fake (реальная форма ответа).

Тесты: phone 4/4; модуль Автоподбора unit 91/91; Pint чисто.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:57:22 +03:00
Дмитрий f7ec94e107 docs(автоподбор): план шага1 — прогресс A/E/F/B готовы, осталось живое C/D/G/H 2026-06-30 17:47:30 +03:00
Дмитрий 0d5e06e895 feat(автоподбор): шаг1 B (офлайн) — парсер выдачи 2ГИС/Яндекс (канал А)
SearchResultsParser: страница категория-поиска → список фирм/организаций {ссылка, имя-подсказка}.
2ГИС — путь /<город>/firm/<id> + имя из вложенного span; Яндекс — /maps/org/<seo>/<id> + aria-label.
Дедуп по ссылке. Авторитетное имя/поля даёт резолвер (под-блок A), открывая каждую карточку.
Пагинация/мульти-запрос — живая часть (блок H/живой прогон).

Фикстуры — реальные сниппеты выдачи (Красломбард, Красноярск): 2ГИС через xfetch, Яндекс
локальным Playwright.

Тесты: search 3/3; модуль Автоподбора unit 87/87; Pint чисто.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:46:44 +03:00
Дмитрий b900874a72 feat(автоподбор): шаг1 F (офлайн) — похожесть эмбеддингами (косинус), не «мнение модели»
EmbeddingRelevance: профиль клиента (примеры имя+описание) → центроид; кандидат (имя+описание)
→ косинус → relevance_pct [0..100]; сортировка по убыванию (§12.5). Векторы — за границей
Embedder (живой AITUNNEL text-embedding-3-small подключается на блоке H/живом прогоне, ключ
по §12.9). Вся логика ранжирования протестирована офлайн на детерминированном эмбеддере.

Тесты: similarity 3/3; модуль Автоподбора unit 84/84; Pint чисто.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:40:10 +03:00
Дмитрий 64d6703bb3 feat(автоподбор): шаг1 E — сильное слияние конкурентов (union-find + вычет клиента)
AutopodborDedup::mergeCompetitors — склейка одного конкурента из 3 каналов под разными
написаниями, доменом-vs-именем или общим телефоном (union-find по корню имени/домена/номеру).
Объединяет ссылки справочников и телефоны, is_federal — местная карточка перевешивает.
Вычитает самого клиента (его имя/сайт не попадают в конкурентов). Усиливает §12.11.

Размещено в существующем backend-дедупе (§7.2 «дедуп на стороне бэкенда»), а не дублем
в движке. Старый dedupCompetitors и его тесты не тронуты.

Нормалайзер: nameKey/domainRoot (alnum-ключ, ё→е) — имя «Драйв займ» сцепляется с «драйвзайм.рф».

Тесты: merge 6/6; модуль Автоподбора unit 81/81; feature dedup 3/3; Pint чисто.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:36:59 +03:00
Дмитрий 4de9474a27 docs(автоподбор): план шага1 — под-блок A (резолвер) отмечен готовым 2026-06-30 17:29:53 +03:00
Дмитрий b2c89d12bd feat(автоподбор): шаг1 A4+A5 — CompetitorResolver + resolveByName (под-блок A готов)
A4 CompetitorResolver (§12.3): имя+город → 2ГИС-в-городе (прямая ссылка канала А) →
иначе Яндекс (поиск имя+город → первый org → проверка) → иначе local=false.
is_federal ПО ФАКТУ: нет местной карточки + есть сайт = федерал; есть карточка = местный.
Транспорт за PageFetcher — вся логика офлайн на фикстурах.

A5 ResolvingAgent (§7.2 resolveByName): имя+region_code → кандидат
{name,description,site_url,directory_urls,provenance} из настоящей карточки; нет филиала
→ честный кандидат-заглушка. Свободное сравнение город↔субъект (DirectoryFields::localeMatches).

Хелперы тестов (stubPages/autopodborFixture) вынесены в tests/Pest.php.
Провайдер НЕ флипнут (findCompetitors всё ещё заглушка до под-блока H).

Тесты: Resolve 17/17; модуль Автоподбора unit 75/75; Pint чисто.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:28:14 +03:00
Дмитрий fc907c2564 feat(автоподбор): шаг1 A3 — YandexResolver + общий DirectoryFields
Разбор реальной org-карточки Яндекс.Карт (/maps/org/<seo>/<id>) в ResolvedCompetitor:
имя/город из <title>, чистый сайт из business-urls (itemprop=url, без utm),
телефоны type=phone (→7XXXXXXXXXX), описание из categories[].name. Отбраковка
чужой фирмы того же имени: имя и город обязаны совпасть с ожидаемыми, иначе null.

Рефактор: общие извлекатели имя+город и телефоны вынесены в DirectoryFields
(используют и 2ГИС, и Яндекс) — без дублирования.

Фикстура — реальная карточка КрасЛомбарда, снята локальным Playwright (render-page.cjs).

Тесты: модуль Автоподбора unit 68/68; Pint чисто.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:17:33 +03:00
Дмитрий 7746ccc54d feat(автоподбор): шаг1 A2 — TwoGisResolver, разбор карточки 2ГИС
Разбор реальной карточки филиала 2ГИС (/<город>/firm/<id>) в ResolvedCompetitor:
имя из <title>, чистый сайт из contact_groups (url, не редирект link.2gis),
телефоны type=phone (нормализованы к 7XXXXXXXXXX), город из title, описание из
rubrics. Терпим к хвосту ?stat=. Пустая оболочка 2ГИС → null.

Фикстура — реальная карточка КрасЛомбарда из живого прогона шага 2 (публичный
бизнес-телефон конкурента, не клиентские ПДн); allowlist gitleaks для app/tests/fixtures/.

Тесты: A2 3/3; модуль Автоподбора unit 64/64.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:07:33 +03:00
Дмитрий 858d8be1b2 docs(автоподбор): контекст-промпт продолжения шага 1 — за основу движок v4 (2 дня прогонов)
Резюме для подхвата после компакта: брать ПОСЛЕДНИЙ вариант (§12 v4), ранние
не воскрешать (ИНН выкинут, похожесть=эмбеддинги, агрегаторы=LLM, is_federal по факту).
Следующая задача A2 TwoGisResolver.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 16:51:40 +03:00
Дмитрий 2cf8b74763 feat(автоподбор): шаг 1 A1 — DTO ResolvedCompetitor (фундамент резолвера)
Карточка резолва имени: name/site/phones/directoryUrl/source/region/description/
isFederal + isLocal(). Без ИНН (решение владельца). Офлайн-тест 3/3.
Фундамент под-блока A плана шага 1.

НЕ на проде, воркстри avtopodbor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 16:37:05 +03:00
Дмитрий 2e37a7a380 docs(автоподбор): план шага 1 — реальный findCompetitors, фундамент-первым
Декомпозиция движка v4 (§12) на под-блоки A–H: резолвер (фундамент, офлайн-TDD) →
слияние/похожесть → каналы А/В/0 → DaData → оркестрация+флип за флагом. Живые
блоки с платными API — отдельными заходами после «го» владельца.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 16:35:48 +03:00
Дмитрий 92e2eef702 docs(автоподбор): статус — шаг 2 встроен в боевой путь и проверен живым прогоном
Обновлён GOTOVNOST: интеграция шага 2 (провод реального агента, где-нашли→БД→API,
код города по региону, ретрай 2ГИС) сделана и проверена вживую на kraslombard24.ru.
Следующий блок — шаг 1 (реальный findCompetitors).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 16:29:09 +03:00
Дмитрий bfda9f8d46 fix(автоподбор): ретрай загрузки списка филиалов 2ГИС при флаки-рендере
Живой прогон вскрыл: рендер списка 2ГИС иногда отдаёт непустую «оболочку»
без ссылок на филиалы — ретрай в XfetchClient ловит только пустой ответ,
а тут карточки терялись. XfetchDirectoryFetcher теперь повторяет загрузку
списка, пока не появятся ссылки на филиалы, до 3 раз.

Проверено вживую на kraslombard24.ru: центральный 8(391)292-00-00
подтверждён И кодом сайта, И 2ГИС с адресом «улица Ладо Кецховели, 30»;
where_found/office/confirmations доходят до БД и API. 0 фальшивок.

- тест повтора списка при флаке; бэкенд автоподбора 131/131

НЕ на проде, воркстри avtopodbor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 16:27:43 +03:00