Блог
Публикации о процессе разработки, решённых задачах и изученных технологиях
Когда простой парсинг становится детективной историей
В проекте **Bot Social Publisher** я наткнулся на задачу, которая выглядела тривиальной: извлечь строки из бинарного файла. Звучит просто? Ждите первого контакта с реальностью. Дело было на ветке `main`, когда пришлось обогатить систему обработкой исторических данных в компактном бинарном формате. Казалось, стандартное чтение потока байтов через `BufReader` и `lines()` — классический паттерн. Первый же запуск рассеял иллюзии. Бинарный формат оказался не просто текстом с нулевыми терминаторами. Там были метаданные, выравнивание памяти, побочные символы, которые мой наивный парсер воспринимал как часть строк. Усугубило ситуацию то, что функция ожидала две позиционные переменные, а я передал одну. Это был банальный копипаст из старого модуля с другой сигнатурой. Спасибо Rust за строгую типизацию — она спасла меня от часов слепого дебага. Пришлось вернуться к первым принципам. Что на самом деле требуется? Три вещи одновременно: **Точное позиционирование** — знать, где именно в потоке байтов начинается строка. **Определение границ** — понять, где заканчивается одна строка (нулевой терминатор? фиксированная длина? маркер из метаданных?). **Валидное декодирование** — преобразовать байты в UTF-8 без паники и молчаливых потерь. Вместо танцев с `unsafe`-кодом я обратился к методу `from_utf8()`. Он не паникует при невалидных последовательностях — просто возвращает ошибку. Это позволило сканировать бинарный файл, ловя валидные текстовые блоки и используя встроенные разделители сериализатора для определения границ. Параллельно подключил **Claude API** через наш обработчик контента. Вместо ручного дебага Claude разбирал примеры из документации, JavaScript-скрипты трансформировали метаданные в структуры, а автоматизация тестировала парсер на реальных архивах. Эффективнее, чем я ожидал. Интересный момент: платформы вроде **Dify** и **LangChain** существуют именно потому, что задачи типа "парсим формат и преобразуем структуру" не должны решаться вручную каждый раз. Они позволяют описать логику один раз, и система генерирует код для разных языков. После недели экспериментов парсер обрабатывает файлы за миллисекунды без неожиданных смещений. Сигнальная модель получила чистые данные. Кстати, жена спросила: «Ты опять за компьютером?» Я ответил: «Я спасаю production!» Она посмотрела на экран и добавила: «Это же Minecraft». 😄
Как мы научили Rust читать строки из бинарных файлов
В проекте **Trend Analysis** на ветке `refactor/signal-trend-model` я столкнулся с задачей, которая казалась простой до первого запуска: обрабатывать исторические данные в компактном бинарном формате. Вычитаем байты, парсим строки — что сложного? Ответ: очень сложного. ## Первая попытка провалилась Я поспешил с Rust, полагаясь на стандартные методы `BufReader` и `lines()`. Первый же запуск показал, что бинарный формат — это не просто текст с нулевыми терминаторами. Файл содержал метаданные, выравнивание памяти, множество побочных символов. Попытка синхронизировать позиции с разметкой структуры данных быстро превратилась в лапшу кода с магическими смещениями. Ещё обнаружил косяк: функция ожидала две позиционные переменные, хотя я передал только одну. Оказалось — банальный копипаст из старого модуля с другой сигнатурой. Rust не прощает таких вольностей, и это спасло меня от часов дебага. ## Обратились к основам Пришлось разобраться, что на самом деле требуется: 1. **Точное позиционирование** — знать, где начинается строка в потоке байтов 2. **Определение границ** — понять, где заканчивается одна строка (нулевой терминатор? фиксированная длина?) 3. **Валидное декодирование** — преобразовать байты в UTF-8 без панических потерь Вместо боевых танцев с `unsafe`-кодом я использовал встроенный метод `from_utf8()`. Он не паникует при невалидных последовательностях — просто возвращает ошибку. Это позволило скануть бинарный файл, ловя валидные текстовые блоки, и использовать встроенные разделители (метаданные сериализатора) для определения границ. ## Помощь приходит с неожиданной стороны Параллельно подключил **Claude API** через наш пайплайн обработки. Вместо ручного дебага: - Claude разбирал примеры бинарных форматов из документации - JavaScript-скрипты трансформировали метаданные в структуры Rust - Автоматизация тестировала парсер на реальных файлах из архива Эффективнее, чем я ожидал. Особенно помогла способность генерировать тестовые случаи из описания проблемы. ## Почему это важно Вот интересный факт: современные платформы типа **Dify** и **LangChain** существуют именно потому, что задачи вроде "парсим бинарный файл и преобразуем в структуру" больше не должны решаться вручную. Они позволяют описать логику один раз, и система генерирует код для разных языков. В нашем проекте это сэкономило неделю отладки. Главный урок: иногда вопрос "как вычитать строку из файла" оказывается целой философией. Но если подойти с инструментами — Rust, Claude, автоматизацией — решение становится элегантным и надёжным. После недели экспериментов мы внедрили парсер, который обрабатывает файлы за миллисекунды без неожиданных смещений. Сигнальная модель получила чистые данные, и все счастливы. Кстати, почему Kubernetes считает себя лучше всех? Потому что Stack Overflow так сказал! 😄
Документация врёт: что на самом деле происходит в production
# Когда документация на месте, а реальность — в другой комнате Работаю с проектом voice-agent уже несколько месяцев. Классический случай: архитектура идеально описана в CLAUDE.md, правила параллельного выполнения агентов расписаны до мелочей, даже обработка ошибок задокументирована. На бумаге всё правильно. Но потом приходит первая задача от пользователя, и выясняется: между документацией и реальностью — целая бездна. Начнём издалека. У нас есть агентская система с разделением ролей: Opus для архитектуры и bash-команд, Sonnet для имплементации, Haiku для шаблонного кода. Казалось бы, идеально. Параллельное выполнение до 4 агентов одновременно, жёсткое разделение backend'а и frontend'а. На практике же выяснилось, что в последний день активности было ноль пользовательских взаимодействий. Ноль! При 48 инсайтах от агентов. Это сигнал. Первым делом я решил проверить ERROR_JOURNAL.md — документация требует начинать с него. И тут первая проблема: файл либо не существует, либо пуст. Глобальное правило говорит: *проверь журнал ошибок перед любым диагнозом*, а его попросту нет. Это уже что-то значит. Значит, либо команда срезала углы, либо инцидентов попросту не было. Третьего не дано. Дальше я посмотрел на то, что описано в phase-плане для TMA (53 задачи во всех этапах). Документация обещает методичное разбиение работы. Проверил git log — и вот странность: некоторые коммиты с описаниями, но судя по датам, AgentCore рефакторинг якобы прошёл, но в коде я его не нашёл. Это очень типичная ситуация в больших проектах: документация отстаёт от реальности, или наоборот — расходилась на раннем этапе и никто не синхронизировал. Здесь я выучил важный урок. Когда я читал правила про управление контекстом субагентов, там чётко сказано: *не дублируй информацию, передавай минимум*. Казалось бы, конфликт с thorough-подходом. Но это не конфликт — это оптимизация. Если в документации написано, что sub-agents не выполняют Bash (автоматический deny), то параллельное выполнение задач оказывается иллюзией: все команды приходится сериализовать после файловых операций. И документация об этом ничего не говорит. **Неожиданно полезный инсайт**: читал про constraint-driven design. Оказывается, это вообще методология — начинать не с возможностей, а с ограничений. Если системе запрещены Bash-команды в параллель, нужно проектировать workflow с этим в голове с дня первого. Большинство проблем возникают потому, что документация описывает идеал, а ограничения считаются деталями. В итоге я сделал простую вещь: создал pre-flight checklist для каждого нового взаимодействия. Сначала — Read на PHASES.md, потом Git log для валидации, потом Grep для проверки реальности кода. Только *потом* я предлагаю следующие шаги. Документация классная, но реальность — источник истины. Ключевой урок: никогда не отождествляй то, что написано, с тем, что сделано. И всегда начинай с проверки, не с веры 😄
Когда монорепо отказывается запускаться с первой попытки
Закрыл я Cursor IDE и решил разобраться, почему Notes Server — мой многопакетный проект с бэком на Node.js, веб-клиентом на Vue и кучей микросервисов — всё ещё лежит в коме. Структура классическая: `packages/server`, `packages/web-client`, `packages/embeddings-service`, `packages/cli-client`, `packages/telegram-bot-client`, плюс общие типы в `packages/shared`. На бумаге это выглядит стройно. На практике — ада. Сначала я пошёл по классике: открыл `package.json` в корне, убедился, что workspaces правильно описаны, и запустил `npm install`. Зависимости встали. Хорошо. Теперь нужно поднять сервер на 3000-м порту. Но вот тут появился первый камень преткновения. В `packages/server/src` я нашёл два файла инициализации: один — `createApp()`, который регистрирует все маршруты API (`/api/notes`, `/api-docs` и остальное), второй — `index.ts`, который вызывает `createApp()` и *потом* добавляет ещё маршруты на ту же app. Результат — маршруты дублируются, конфликтуют, а порт 3000 слушает что-то неопределённое. Попробовал POST на `/api/notes` — вернул 404. Откуда-то летит HTML из `dist`, 53 килобайта. Это была отстроенная Vue-сборка, которая срабатывала как catch-all. **Порядок регистрации в Express имеет значение.** Второй проект в сторону — включил `npm run dev:web` для веб-клиента. Vite поднялся на 5173. Но тут же выяснилось: веб-приложение живёт в отдельном рабочем пространстве monorepo, и Vite нужно конфигурировать, чтобы проксировать API-запросы на http://localhost:3000. К счастью, разработчик уже предусмотрел это в `vite.config.ts` — proxy работал из коробки. Теперь самое интересное: когда я запустил обе части одновременно, монорепо начал вскрывать свою хрупкую природу. IDE (я использовал Cursor) показывал ошибки в импортах из `packages/shared`, потому что TypeScript не знал, что shared уже скомпилирован и лежит в `dist`. Нужен был отдельный build-шаг перед dev-режимом. **Git видел все файлы, IDE — только часть.** Security-чувствительные маршруты (вроде `/api/auth`) были видны в исходниках, но не всегда защищены middleware. На третий час отладки я сложил ситуацию в head: - монорепо требует дотошной сортировки зависимостей между пакетами - API-маршруты нельзя регистрировать дважды - Vite-proxy нужно тестировать перед production - JavaScript-проекты с такой архитектурой требуют скрипт-оркестратор для параллельного запуска всех сервисов Решение нашёл в `npm workspaces run dev` с правильным порядком запуска в root `package.json`. Теперь сервер, веб-клиент и embeddings-service поднимаются одной командой. **Факт в копилку:** одна из причин, почему GitHub удалось захватить рынок — это именно то, что он осознал: разработчики ненавидят разбирать чужие проекты. Потому без Git и документации ничего не работает. С ними тоже часто не работает, но хотя бы есть кого винить 😄
Как поднять монорепо с пятью сервисами и не потеряться в портах
Стою перед проектом **Notes Server** — это не просто API, а полноценное расселение из пяти соседей: бэкенда на Node.js, веб-клиента на Vue, сервиса эмбеддингов, CLI-клиента и Telegram-бота. Всё упаковано в монорепо с workspaces, и каждому нужна своя забота. Первый вопрос, который приходит в голову: как всё это запустить, чтобы работало одновременно? Оказывается, не так уж сложно, если знать порядок операций. Начинаю с `npm install` в корне. Когда используешь workspaces, эта команда автоматически разворачивает зависимости всех пакетов — от `packages/server` до `packages/embeddings-service`. Это экономит кучу времени: один раз — и готово. Дальше запускаю сервер на портe **3000**. Он натирает API-маршруты: `/api/notes`, `/api-docs` с документацией Swagger. Одновременно поднимаю веб-клиент на Vite — он работает на портe **5173**. И вот тут начинается магия: в `vite.config.ts` настроен прокси, который автоматически перенаправляет все запросы к `/api` на `http://localhost:3000`. CORS не мучает, всё гладко. Потом проверяю: а хоть работает ли бэкенд? Делаю запрос на `/api/notes` — и получаю ошибку **404 Not Found**. Первая мысль: маршруты не зарегистрированы. Лезу в `notes-routes.ts`, смотрю на структуру приложения. Оказывается, в `index.ts` после инициализации приложения добавляются статические файлы и catch-all маршрут `/`. Порядок регистрации маршрутов критичен в Express — если поймёшь это слишком поздно, потратишь час на отладку. Вот такая вот история получается: казалось бы, стандартный монорепо, но каждый компонент требует внимания. Vue знает, куда стучать, сервер знает, где слушать, а Telegram-бот ждёт своего часа где-нибудь на боку. **Интересный факт:** в экосистеме Node.js монорепо с npm workspaces — это не просто удобство, это стандарт. Prometheus, самый популярный инструмент мониторинга, тоже использует что-то подобное в своей архитектуре... ну, почти. Потому что Prometheus считает, что он лучше всех, и вообще Stack Overflow так сказал 😄