Текст я писал с языковой моделью: она собирала черновик и вычитывала формулировки. Замеры, код и всё, что здесь названо фактом, — мои. Каждая цифра ниже получена обходом дисков, а не по памяти; выгрузка с хешами приложена.
Это третий мой текст здесь. Первый — про ограничение диагностического адаптера ELM327, второй — про восстановление программы ПЛК по бумажной документации. В обоих читатели нашли расхождения между текстом и кодом; в обоих есть раздел «Обновления», где эти правки перечислены с датами и никами. Единственный опубликованный код из перечисленного ниже — github.com/gdm0991/vagdiag.
Сразу оговорюсь: цифра в заголовке — плохая метрика. Строки кода не измеряют ни пользу, ни качество, и я привожу их только как масштаб, а не как достижение. Гораздо интереснее другое: что происходит с инженером, когда у него впервые в жизни появляется исполнитель, который пишет код быстрее, чем инженер успевает продумывать требования. И где в этой новой скорости проходит граница между «работает» и «выглядит как работает».
Это не восторженный пост. Половина текста — про то, где я ошибся, что не доделал и какие собственные результаты потом сам же признал недействительными.
И сразу про то, за что мне уже прилетело. К прошлой статье первым же комментарием было: «иишность текста просто не даёт продвинуться дальше нескольких абзацев». Претензия по делу — тот текст я собирал с моделью и получил на выходе её интонацию вместе с её структурой. Этот делал иначе: черновик руками, модель на вычитку и на сверку цифр с файлами. Стало ли лучше — судить не мне. Если опять читается как машина, скажите прямо, я приму.
Кто я и почему это не очередной пост про нейросети
Двенадцать лет я занимаюсь промышленной автоматизацией. Испытательные стенды газотурбинных двигателей: измерительные каналы, исполнительные механизмы, ПЛК, SCADA, конструкторская документация по ЕСКД. Это область, где ошибка стоит не «отката релиза», а испорченного объекта испытаний, сорванной программы работ и, в худшем случае, здоровья людей рядом со стендом.
Я писал программы для ПЛК. Но разработчиком ПО в общепринятом смысле я не был. Я не проектировал распределённые системы, не собирал мобильные приложения, не занимался криптографией, не поднимал CI. У меня была другая профессия: понимать объект, формулировать требования, задавать методику проверки и отвечать за то, что измеренное значение — действительно измеренное.
15 мая 2026 года я начал работать с LLM-ассистентом как с основным рабочим инструментом. Не как со справочником и не как с генератором сниппетов — как с исполнителем, которому ставится задача и от которого принимается работа. К 21 августа, когда я сел считать, прошло 98 дней. За них получилось 320 348 строк кода и разметки в актуальных версиях и ещё 84 294 в предыдущих версиях тех же продуктов.
Посчитайте сразу, чтобы не считать в комментариях: 320 348 / 98 — это 3 269 строк в день, включая выходные и те дни, когда я был на основной работе. Руками столько не пишут, и я не писал. Их писал ассистент. Я ставил задачу, задавал методику проверки и принимал работу. Если такая цифра кажется вам сомнительной как показатель качества — вы правы, и половина статьи ниже ровно об этом.
Цифру в заголовке принято округлять, я этого делать не буду — сейчас объясню, откуда она взялась, потому что без этого объяснения любое число здесь стоит ровно ноль.
Как я считал
Обход обоих дисков 21 августа 2026 года. Инструмента cloc на машине не оказалось, поэтому обход написан на Python: файлы отбираются по расширению, у каждого берётся SHA-1 содержимого, дубликаты схлопываются по хешу.
Прочитано 7 359 файлов, уникальных по содержимому — 3 123.
Из этих 3 123 в главную цифру попали 2 944. Остальные 179 — предыдущие версии и сгенерированные данные, они вынесены отдельными строками воронки ниже. В таблице по продуктам дальше стоит 1 258 файлов: там я считал только те, что смог уверенно отнести к конкретному продукту, а не всё подряд. Расхождение между 2 944 и 1 258 — это мера того, насколько на дисках бардак, и я не собираюсь делать вид, что его нет.
Дедупликация — не формальность. Одна папка на рабочем столе вложена сама в себя, и без схлопывания главный проект считался бы дважды: одного только кода задвоилось 172 075 строк. Если сложить всё подряд, как это сделает find, получится 1 801 600 строк — цифра, которая не значит ничего.
Шаг |
Строк |
Файлов |
|---|---|---|
Сложить всё подряд с рабочего стола |
1 801 600 |
|
− дубликаты по SHA-1 содержимого |
639 202 |
|
− конфиги, скрипты, документация |
334 696 |
|
− массивы шрифтов и картинок в заголовочных файлах прошивки, xref-отчёты трассировки плат |
318 893 |
|
− предыдущие версии тех же продуктов |
234 599 |
2 298 |
+ то же самое со второго диска |
320 348 |
2 944 |
Последняя строка — не приписка задним числом, а отдельный обход: второй диск подключался позже и дал ещё 85 749 строк в 646 файлах. В черновике этой статьи её не было, и воронка из-за этого не сходилась: 318 893 минус 84 294 даёт 234 599, а никак не 320 348. Вычитание проверяется в столбик за минуту, и кто-нибудь это сделал бы.
Что исключено: node_modules, venv, site-packages, vendor, сборочные каталоги, встроенный переносимый Python, platform-tools, минифицированные файлы, package-lock.json, всё крупнее 3 МБ. Отдельно проверил на заимствования — поиск по именам двадцати известных библиотек дал 48 совпадений, все ложные: это папки lib/ во Flutter-проекте. Чужого кода в счёте нет.
Что не исключено и о чём надо сказать прямо: 265 222 строки конфигурации в цифру не входят, и правильно — три четверти этого объёма машинная выгрузка проекта из среды разработки в XML, а не написанный текст. Не входит и документация: 63 301 строка в markdown.
Из чего складываются 320 348
Продукт |
Строк |
Файлов |
|---|---|---|
Duga — мессенджер и две прошивки LoRa |
141 515 |
463 |
РУД-РОД — стенд, программа ПЛК и интерфейс оператора |
113 344 |
443 |
Сайты, SEO-переделки, веб-резюме |
16 128 |
18 |
StudyAI — обучающая платформа |
13 487 |
192 |
ТАРО и AstroClaude |
10 729 |
36 |
Переделка ПО испытательного стенда — о ней отдельная статья |
9 612 |
61 |
«Маска», VPN, Tab Watcher |
4 516 |
23 |
Игры и мелочи |
2 330 |
7 |
ГОСТ-Контроль — нормоконтроль КД |
1 931 |
3 |
VagDiag — автодиагностика |
376 |
1 |
прочее, не отнесённое к продукту |
6 380 |
11 |
Итого |
320 348 |
1 258 |
У VagDiag в таблице 376 строк — это не описка: сама программа живёт в архиве и в репозитории на GitHub, а на диске остались только Android-обвязка и передаточные файлы. Считается то, что лежит на дисках.
Предыдущие версии — те самые 84 294 строки — распределены так: Duga 67 294, РУД-РОД 16 533, остальное 467. Это написанный руками текст, но это тот же продукт на более ранней итерации. Поэтому в главную цифру он не входит, и когда я говорю «320 тысяч», я имею в виду актуальные версии, а не сумму всех черновиков.
Цифра — нижняя граница. Часть файлов на втором диске я не успел перенести для подсчёта, это снова версии РУД-РОД. И в счёт не входит то, что развёрнуто на трёх серверах.
И вот главный тезис, ради которого я это пишу. Ассистент закрыл вопрос «как написать». Он не закрыл — и, по-моему, в обозримом будущем не закроет — два других вопроса: что должно получиться и как понять, что получилось правильно. Именно эти два вопроса и есть содержание инженерной профессии. Промышленная культура проверки, которую я двенадцать лет впитывал на стендах, оказалась переносимой в разработку ПО практически один в один. Это оказалось для меня более сильным открытием, чем сама скорость.
Что сделано — коротко и цифрами
Перечисляю по убыванию значимости, потому что дальше буду ссылаться на конкретные проекты.
1. Система управления исполнительными механизмами испытательного стенда
Новое изделие полного цикла за восемь календарных дней. Что в него вошло:
программа ПЛК на ST для CODESYS: 22 файла, 3 449 строк — 12 функциональных блоков, 3 программы, 17 функций, 6 списков глобальных переменных;
экспорт проекта в PLCopen XML собственным генератором на Python, потому что руками собирать XML-дерево на такую структуру — это отдельная работа с высоким шансом опечатки;
интерфейс оператора на .NET 8 + Avalonia: 22 экрана, 13 280 строк, локализация на 1 254 ключа;
второй фронтенд — веб-интерфейс через встроенный прямо в приложение HTTP+WebSocket-сервер, со снимком состояния каждые 250 мс;
поставка одним файлом: single-file .exe на 53 МБ, без установщика и без зависимостей на машине оператора;
руководство по эксплуатации на 24 раздела, сгенерированное программно из тех же исходников, что и сама программа.
Техническая суть задачи была не в объёме кода, а в требованиях. Нужна была точность позиционирования и перекладка «край-край» меньше секунды — без датчиков угла. Только импульсы энкодеров, калибровочная таблица, компенсация люфта и программный обход механической «мёртвой точки». Плюс требования по безопасности — и здесь надо сказать точно, потому что это как раз то место, где легко приукрасить.
Целевым уровнем при проектировании я закладывал PL c по EN ISO 13849-1. Расчёта MTTFd, DC и CCF я не делал, валидацию не проводил, Safety-реле в схеме нет. Значит, заявлять достигнутый уровень производительности я не имею права: это проектное намерение, а не подтверждённый результат. Что реально сделано — двухканальный ввод аварийной кнопки и двусторонний watchdog «ПК↔ПЛК», который детектирует самое неприятное состояние: «TCP-соединение живо, а контроллер завис».
В первой редакции этого текста стояло «категория 1 / PL c». Это ошибка внутри одного предложения: двухканальная архитектура — признак категории 3, а категория 1 одноканальна. Держать в одной строке и то и другое нельзя, и хорошо, что я это перечитал раньше, чем читатель.
И статус, чтобы «сделано» не читалось как «лежит в папке»: система прошла пусконаладку на реальном стенде. Не демонстрация и не симулятор. Собственный симулятор ПЛК при этом был написан заранее и ровно затем, чтобы на объект приехала программа, которую уже гоняли, а не текст, который там увидят впервые.
Вот на этом месте видно разделение труда. Требование «перекладка меньше секунды без датчика угла, с компенсацией люфта и обходом мёртвой точки» не могло прийти от ассистента. Оно приходит от того, кто видел механизм, знает, как он изнашивается, и понимает, почему датчик угла в этой конструкции поставить нельзя. Ассистент отлично написал под это код. Придумать это он не мог, потому что для этого нужно знать объект.
2. Децентрализованный мессенджер
141 515 строк в актуальной версии по замеру, плюс 67 294 в предыдущих. Состав:
Python-сервер, включая собственный WebSocket-шлюз на 3 000 строк со 143 типами кадров протокола;
веб-клиент на чистом JS — 10 900 строк, без фреймворка;
Android-клиент на Kotlin: два flavour из одной кодовой базы, 21 подписанная сборка;
две прошивки LoRa с нуля на C++ для двух семейств плат, суммарно около 31 000 строк, со своим OLED-рендерером и кириллическим шрифтом;
криптография: ECDH P-256 + AES-GCM с interop-тестом Python↔WebCrypto, постквантовый слой X25519 + ML-KEM-768, офлайн-верифицируемый ECDSA-сертификат подписки — платная функция продолжает работать при полностью упавшем сервере. Forward secrecy при этом нет: компрометация долговременного ключа раскрывает всю прошлую переписку. Пишу это прямо здесь, в перечне возможностей, а не в разделе ограничений внизу — иначе получится реклама с примечанием мелким шрифтом;
инфраструктура: 3 VPS в трёх странах, автопиринг, ежесуточные шифрованные копии, запасной вход с подъёмом за минуту. 9 $/мес против 43 $/мес за один сервер у первого рассмотренного провайдера.
3. EdTech-платформа
Flutter-клиент и шесть сервисов: пять на FastAPI, платёжный на Node.js. Алгоритм интервальных повторений реализован с нуля, а не взят библиотекой. 179 автотестов, OpenAPI-контракт, Helm-чарт, два pipeline CI.
4. Диагностическая программа для автомобильной электроники
Собственный минимальный клиент UDS поверх дешёвого Wi-Fi-адаптера: сборка кадров ISO-TP, работа по ISO 15765-4, разбор диагностических кодов по ISO 14229-1 — с типом отказа и состоянием ошибки, а не просто «код такой-то».
Это, кстати, характерный пример. Стандарты читал я — по-другому не выйдет, там нужно понимать, что вообще происходит на шине. Кодировал их ассистент. Проверял снова я.
Метод: четыре вещи, без которых ничего бы не собралось
Самая ценная часть этой истории — не список продуктов, а рабочий процесс, который под них пришлось выстроить. Ни одна из этих практик не была придумана заранее. Все четыре родились из конкретной боли.
HANDOFF-документы
Столкнулся: контекст модели конечен, а проект живёт месяцами. Через два дня работы ассистент не помнит, почему три недели назад был выбран один вариант протокола, а не другой, и с полной уверенностью предлагает вернуться к тому, что уже отвергли.
Поэтому: в конце каждой рабочей сессии я стал писать документ передачи контекста. Не «отчёт о проделанной работе» — именно передачу смены, как в оперативной службе. Формат стабилизировался быстро:
# HANDOFF 2026-07-14-02 — узел ретрансляции LoRa ## Состояние Ветка/файл: relay_core_v37.cpp (предыдущая — в архиве) Собирается: да. Прошито на плату: нет (нет физического доступа) ## Что сделано в эту сессию - Переписана очередь исходящих кадров: было кольцо на 8, стало 24 - Добавлен счётчик отброшенных кадров по причине "занятость эфира" ## Почему именно так - Кольцо на 8 переполнялось при трёх соседях в зоне слышимости. 24 — из расчёта: 3 соседа × 8 кадров окна. Не «на глаз». - Отдельный счётчик нужен, чтобы отличать потерю в эфире от потери в собственной очереди. Без него диагностика вслепую. ## Что проверено и чем - Эмулятор радиоканала, 500 прогонов: потерь в очереди 0 - Реальная плата: НЕ ПРОВЕРЕНО ## Что не сделано - Приоритет служебных кадров над пользовательскими — не начато ## Открытые вопросы - При четырёх соседях расчёт 24 не проходит. Нужно решение: увеличивать буфер или вводить приоритет. Не решено. ## Отвергнуто (и почему) - Динамическое выделение очереди — отказ, фрагментация кучи на длинных прогонах; на этой плате памяти нет запаса
За один проект накопилось 202 таких документа — это не «больше двухсот» на глаз, а результат подсчёта по маске имени.
Получилось — и с неожиданным побочным эффектом. Я вводил HANDOFF как костыль против забывания. А получил полную историю решений, включая отвергнутые варианты и причины отказа. Через два месяца, когда возник вопрос «а почему у нас тут не динамическая очередь», ответ нашёлся за минуту вместе с обоснованием. Ни один git log так не умеет: коммиты рассказывают, что изменилось, а не почему выбрано это и что было рассмотрено и выброшено. Раздел «Отвергнуто» оказался ценнее раздела «Сделано».
Версионирование файлами вместо git — и почему это плохо
Скажу прямо: я начал не с git. Новая версия — новый файл, предыдущая — в архив. relay_core_v36.cpp, relay_core_v37.cpp, и так далее.
Причина честная и неприятная: на старте я не понимал git. Я двенадцать лет работал с проектами ПЛК, где «версия» — это файл проекта с датой в имени и подпись в листе регистрации изменений. Мне казалось, что я просто переношу привычную схему.
Чем обернулось: сотни архивных файлов. Дубли, которые начинают жить своей жизнью: правишь v41, а сборка тянет v39, потому что в трёх местах путь прописан по-разному. Невозможность работать в команде вообще — двое человек с такой схемой пересекутся в первый же день. Невозможность нормально ответить на вопрос «что именно поменялось между этими двумя версиями», кроме как diff’ом руками.
Правильный вывод простой и без оправданий: git заводится в первый день проекта, до первой строки кода. Не когда проект «дорастёт». Мой личный опыт показывает, что переход на git задним числом, когда в архиве уже несколько сотен файлов, — это отдельная неприятная работа, которую проще было не создавать.
Одну вещь тут надо развести, иначе получится противоречие с тем, что написано выше про CI, Helm-чарт и подписанные сборки. Так было не везде и не всегда. Прошивки и ранние версии стендовой программы жили файлами до самого конца. Платформа с автотестами и двумя pipeline поднималась уже на git — иначе её просто не собрать. Точную дату перелома назвать не могу: я её не записал, а надо было — это ровно то событие, которое потом хочется найти в истории и не находишь.
Единственное, что я в этой схеме оставлю осознанно, — сами HANDOFF-документы. Но они и должны лежать в репозитории, а не рядом с ним.
Оркестрация подзадач
Столкнулся: крупная задача упирается не в способности ассистента, а в её собственный размер. «Сделай клиент UDS» — это не задача, это тема.
Поэтому: декомпозиция и раздача независимых кусков параллельным агентам, с последующим сведением результатов. Сборка кадров ISO-TP отдельно, разбор кодов ошибок отдельно, транспортный слой отдельно.
Получилось: работает, и хорошо, ровно до тех пор, пока подзадачи действительно независимы. Ломается моментально, когда между ними есть скрытая связь — не та, которую ты видел при декомпозиции, а та, которую не видел. Классика: два агента параллельно и совершенно корректно определили одну и ту же структуру данных, но с разным порядком полей. Каждый кусок по отдельности проходит тесты. Вместе — не работает, и причина не видна ни в одном из двух кусков.
Вывод, который я для себя сделал: качество оркестрации определяется не количеством агентов, а качеством границ между подзадачами. Границу проводит человек, знающий предметную область. Это опять не делегируется.
Имена версий как журнал отладки
Отдельная история — прошивка. 88 версий за пять суток, и названия читаются как протокол сужения гипотез: «почему не разобралось», «какое поле», «что в отвергнутом».
Причина такого темпа неприятная: физического доступа к устройствам не было. Отладка велась по одному изменению за прогон — иначе, получив результат, невозможно сказать, какое из трёх изменений сработало. Это ровно тот же принцип, по которому на стенде не крутят два параметра сразу. Медленно, зато каждый прогон даёт однозначно интерпретируемый результат.
Побочный эффект: список имён версий сам стал документом. По нему видно, как гипотеза «проблема в разборе» сменилась на «проблема в конкретном поле», а потом на «проблема в том, что мы делаем с отвергнутым кадром».
Отладка железа: четыре случая, которые стоили дороже всего
Прошивки — единственная часть этой истории, где ассистент помогал слабее всего. Не потому, что плохо писал код. Потому что почти всё время уходило не на код.
Пересобрал десять раз, а плата молчала. Прошивка при старте сверяет свой хеш с тем, что записан в EEPROM. Не сошлось — радио не поднимается, а в интерфейсе видно безобидное «параметры приняты, состояние не изменилось». Десять пересборок подряд, и ни разу я не переназначил хеш: это отдельная команда, про которую надо помнить. Лечение в итоге легло не в инструкцию, а в скрипт — теперь переназначение делает он сам. Правило, которое надо помнить, — плохое правило.
Прошилась не та плата. Утилита предлагает выбрать плату номером в списке. После заливки порт меняется, список перестраивается, и следующая заливка уходит соседке. Выбирать надо по порту, а не по номеру строки. Дошло не с первого раза.
Потеряно три сборки на «две правки за прогон». Ровно та ошибка, за которую на стенде я выгнал бы сам себя: поменял два параметра, получил результат — и результат не значит ничего.
Полдня на «регистратор доменов не принимает Яндекс». Теория была стройная: западный сервис режет российскую почту, надо искать обход. Потом я прочитал поле буквально. В адресе стояла запятая вместо точки. Один символ.
Последний случай я держу в голове как образец. Гипотеза сформулировалась мгновенно, объясняла всё наблюдаемое и была полностью выдуманной. Быстрый исполнитель эту болезнь усиливает: теперь и проверить красивую теорию можно за десять минут, и потратить на неё полдня — тоже.
Честность: «измерено» против «работает»
Это центральный раздел, и он самый длинный не случайно.
Чем быстрее появляется код, тем дешевле становится произвести то, что выглядит работающим. Это главный эффект, который я наблюдал все девяносто восемь дней подряд. Раньше между «идея» и «выглядит работающим» лежали недели труда, и этот труд сам по себе был фильтром: за неделю возни с кодом успеваешь понять, что затея нежизнеспособна. Теперь этого фильтра нет. Экран рисуется, кнопки нажимаются, лог бежит — а работает ли оно, из этого не следует вообще ничего.
Единственная защита, которую я знаю, — та же, что на стенде: измерять, а не верить.
Голос по радиоканалу: не «получилось», а число
Была задача — голосовые сообщения через LoRa. Первая реализация «заработала»: сообщение уходило, приходило, воспроизводилось. По меркам демо — успех.
Вместо того чтобы это записать в успех, я снял числа:
доставка: 50 из 50 пакетов;
10 секунд речи — 9,8 секунды передачи в быстром режиме;
те же 10 секунд речи — 23,9 секунды в обычном режиме.
Первая цифра говорит, что канал надёжен. Вторая и третья — что в обычном режиме голосовое сообщение идёт в 2,4 раза дольше, чем звучит. Это не «медленно, но работает». Это функция, которая на реальном рабочем профиле создаст очередь, забьёт эфир и уронит текстовые сообщения, ради которых всё и строилось.
Поэтому запрет голосового режима в определённых условиях зашит в приложение по измеренному числу, а не по экспертной оценке «ну, наверное, будет медленно». Порог — это конкретное значение, полученное конкретным замером, и в коде рядом стоит комментарий, откуда оно взялось.
Дальше — важнее. Бюджет канала я посчитал из физики: фактор расширения, полоса, кодовая скорость, нормативный предел занятости эфира в 1 %. Эти четыре числа дают предельную пропускную способность, и она не обсуждается: она следует из регламента и модуляции.
Посчитаю вслух, потому что иначе это посчитают за меня. Один процент занятости — это 36 секунд эфира в час на устройство. Голосовое на 10 секунд речи в разрешённом режиме (SF7, полоса 250 кГц) занимает 9,8 с — 27 % часового бюджета. Потолок, стало быть, три-четыре голосовых в час, и ни одним больше. В запрещённом режиме (SF8, 125 кГц) одно сообщение съедает 23,9 с, две трети часа. Поэтому он и запрещён.
Отсюда видно, что «голос разрешён» и «голосом можно пользоваться как чатом» — совершенно разные утверждения. Бюджет считает сама прошивка, и режимов у неё три: строгий — при исчерпании часового лимита передача блокируется; предупреждающий — пропускает, но сообщает, что вы вышли за регламент; экстренный — когда связь нужна любой ценой, без предупреждения и без блокировки.
Третий режим оставлен сознательно и сознательно выходит за норматив. Это решение человека у аппарата, а не программы, и оно должно быть решением человека: в ситуации, ради которой всё это радио и затевалось, занятость эфира не самая крупная из проблем. Но написать об этом надо здесь, а не спрятать в настройках.
Чего я не знаю: сколько голосовых в час люди шлют на самом деле. Телеметрии по режимам у меня нет, и собирать её с чужих устройств я не собираюсь — это мессенджер, у которого приватность заявлена главной чертой. Так что реальный профиль нагрузки для меня остаётся тёмным пятном.
На этой арифметике были закрыты две функции, которые очень хотелось сделать: голосовой кодек с высоким битрейтом и звонки в реальном времени. Не после месяца попыток. До первой строки кода. Расчёт показал, что требуемый поток превышает физический предел канала кратно, и никакая оптимизация этот разрыв не покроет.
Это, по-моему, и есть главное практическое применение инженерной подготовки в эпоху быстрого кода. Ассистент напишет тебе кодек. Он с готовностью напишет тебе и звонки. Он не скажет: «то, что ты просишь, не влезает в эфир по регламенту». Это должен сказать ты. И сказать не мнением, а расчётом.
Собственные аудиты, которые находят собственные ошибки
Я завёл практику регулярных аудитов собственной системы по конкретным осям: приватность, устойчивость к обрывам, безопасность памяти. Не «посмотри, всё ли хорошо», а узкая проверка с заранее сформулированным вопросом.
Что они нашли:
Голосовые сообщения и файлы месяц хранились на сервере открытым текстом. Шифрование было. Проверка приватности — тоже была. Но проверка смотрела только на поле text. Всё, что шло другими полями — вложения, голос, — проходило мимо неё. Формально система «имела сквозное шифрование». Фактически месяц не имела для целого класса данных.
Это ровно тот случай, ради которого нужен отрицательный контроль. На стенде это азбука: если ты не проверил, что твой метод даёт отрицательный результат там, где его быть не должно, ты не знаешь, что он вообще что-то меряет. Проверка приватности, которая ни разу не была прогнана на заведомо незашифрованных данных, — это не проверка, это ритуал.
Звонки утекали на сторонний публичный медиасервер. Библиотека имела дефолтную конфигурацию, дефолт указывал наружу, и всё работало. Именно что работало — соединение устанавливалось, звук шёл. Аудит спрашивал не «работает ли», а «куда именно идёт трафик».
26 мест молча теряли команды при обрыве WebSocket. Не падали, не логировали ошибку — тихо теряли. Пользователь нажимает, интерфейс подтверждает, ничего не происходит. Худший вид отказа: система сообщает об успехе, которого не было.
Двадцать шесть — это сколько нашёл аудит на путях, которые я сам и перечислил в задании. Сколько таких мест в системе на самом деле, я не знаю. Проверка искала там, куда её послали, и её улов — нижняя граница, а не итог.
Целый класс переполнений буфера в прошивке. Классическая C-шная работа со строками, десятки мест. Закрыт одним C+±шаблоном: 80 из 94 вызовов защищены без правок в самих вызовах — размер приёмника выводится компилятором из типа, а не передаётся руками.
// Было: размер приёмника передаётся вручную. // Ошибка в одном из 94 вызовов — и это переполнение. snprintf(buf, sizeof(buf), fmt, ...); // Стало: размер выводится из типа массива, передать неправильный нельзя. template <size_t N, typename... Args> int fmt_to(char (&dst)[N], const char* fmt, Args... args) { int n = snprintf(dst, N, fmt, args...); return (n < 0) ? 0 : (n >= (int)N ? (int)N - 1 : n); }
Оставшиеся 14 вызовов работали с указателями, а не с массивами, — там вывод размера невозможен, и они переписывались руками поштучно. Я пишу это, потому что «закрыл класс ошибок одним шаблоном» без уточнения «80 из 94» было бы приукрашиванием.
Стенды-подделки: как проверять то, к чему нет доступа
Часть инструментов управления устройствами писалась вообще без физического доступа к этим устройствам. Ситуация абсолютно рядовая и в промышленности: оборудование на объекте, ты не на объекте, программа нужна.
Поэтому под них делались эмуляторы внешних интерфейсов: поддельный adb, поддельный сервер, синтетический пакет приложения. Мотив у меня формулировался буквально так: отдать инструмент, не запустив его ни разу, — значит отдать текст, а не работу.
И обязательный раздел в описании каждого такого стенда — «что это доказывает и чего это не доказывает». Дословный пример:
Доказывает: логика разбора ответа корректна на всех 6 формах ответа, включая усечённый и с мусорным префиксом. Порядок вызовов соответствует протоколу. Таймауты отрабатывают.
Не доказывает: ничего о поведении на реальном устройстве. Не проверены: реальные задержки, поведение при отключении кабеля в середине операции, версии прошивок, отличные от смоделированной.
Этот раздел — самая полезная часть стенда. Без него эмулятор превращается в машину по производству ложной уверенности: все тесты зелёные, значит всё хорошо. Нет. Зелёные тесты на эмуляторе означают ровно то, что написано в первом абзаце, и ни буквой больше.
На стенде ГТД это называется границами применимости методики, и без них результат измерения не принимают. В разработке ПО это работает точно так же — просто никто не заставляет писать.
Недоверие к чужому «успеху»
Отдельный принцип, который я вынес с производства: отчёт об успехе — не доказательство успеха. Внешняя система, сообщающая «готово», сообщает только то, что она так считает.
Конкретный случай: результат установки приложения на устройство перепроверяется отдельным опросом устройства — «а есть ли там теперь этот пакет?». Причина простая: команда установки умеет напечатать Success и не установить ничего. Такое бывает при рассинхронизации состояния, при нехватке места, при частичном откате. Строка «Success» при этом честно печатается.
res = run_install(pkg_path) # напечатал "Success" if not device_has_package(pkg_name): # но проверяем сами raise InstallFailed( "инструмент отчитался об успехе, пакет на устройстве отсутствует" )
Это три лишние строки. Они закрывают целый класс ситуаций, в которых ты уверен, что всё установилось, и полдня ищешь ошибку не там.
Тот же принцип — на watchdog «ПК↔ПЛК» в стендовой системе. Живой TCP-сокет доказывает, что жив стек. Он ничего не доказывает про то, что прикладная задача в контроллере крутится. Поэтому детект «TCP жив, контроллер завис» — это отдельная проверка со встречным счётчиком, а не вывод из факта соединения.
Отзыв собственных выводов
Самый неприятный эпизод за всё это время. Серия правок была проверена и признана рабочей. Потом выяснилось, что стенд, на котором шла проверка, сам был неисправен.
Правильное действие тут ровно одно и оно болезненное: все результаты, полученные на неисправном стенде, признаются недействительными. Не «скорее всего, там всё нормально было». Не «ну часть-то точно работала». Недействительны все, целиком, и переставляются на повторную проверку — по одному изменению за прогон.
Я потерял на этом несколько суток. Но альтернатива — оставить в системе набор правок с неизвестным статусом и потом ловить последствия месяцами — хуже на порядок. В метрологии это не обсуждается: измерения, выполненные прибором, у которого не подтверждена исправность, не имеют силы. Не «сомнительны» — не имеют силы.
Способность отозвать собственный вывод — это, по-моему, вообще ключевой навык в работе с быстрым кодом. Потому что теперь выводы производятся быстро, и неверных среди них больше в абсолютном выражении.
Аудит функциональности с публичным счётом
Ближе к концу я провёл сплошную проверку заявленных функций мессенджера. Результат: из 36 проверенных функций 35 работают, 1 — честная заглушка, которая обозначена как заглушка и в коде, и в интерфейсе.
И сразу о цене этой цифры. Список из 36 функций составил я. Критерий «работает» сформулировал я. Проверку провёл тоже я. Внешней приёмки не было ни на одном шаге — значит, 35 из 36 весит ровно столько, сколько весит самооценка, и предъявлять её как аттестат я не могу. Предъявляю как то, что можно оспорить: если кто-то захочет проверить, список функций и критерии выложу целиком.
Но полезнее второй список — того, чего нет:
обмен между телефонами напрямую остался заглушкой. Заявлять «децентрализованный» и не иметь прямого обмена — натяжка, и я её проговариваю;
forward secrecy нет. Есть ECDH и постквантовый слой, но компрометация долговременного ключа раскрывает прошлую переписку. Это существенное ограничение, и писать «безопасный мессенджер» без этой оговорки было бы нечестно;
платёжный провайдер не подключён;
переводы интерфейса сделаны на треть: 412 строк из 1 254.
Я специально держу этот список рядом со списком достижений. Продукт, у которого опубликован только первый список, — это маркетинг. Инженерная сдача — это когда оба списка в одном документе.
Про деньги — честно
Механика монетизации написана и проверена: сертификаты подписки, офлайн-верификация, ограничения по тарифу. Работает. Но платить внутри пока некому — сервер всегда отвечает «песочница».
Своя формулировка, к которой я пришёл: не хватает не кода, а людей.
Это, кажется, и есть главный сдвиг. Раньше узким местом был код: идея есть, реализовать некому. Теперь узкое место переехало. Реализация перестала быть дефицитом. Дефицитом стало всё остальное: пользователи, доверие, поддержка, продажи, сообщество. Написать платёжный контур оказалось несопоставимо проще, чем найти первого человека, готового заплатить.
Первое внешнее ревью пришло из комментариев
Пока я готовил этот текст, вышла вторая статья — про восстановление программы ПЛК по бумажной документации. И там впервые за всё это время мои архитектурные решения разобрал человек со стороны.
@optemist, АСУТПшник с пятнадцатью годами опыта, написал три вещи.
Первая: перекрёстные ссылки в проекте потеряны из-за непрямой адресации. Это верно, и это прямое следствие того самого data-driven, которым я в той статье хвалился. Индексы вычисляются в рантайме, разрешать среде нечего, и наладчик на объекте остаётся без основного инструмента поиска. Я показал выигрыш и не показал счёт.
Вторая: циклы индексации стоило вынести в прерывание с постоянной цикличностью. Не сделано, всё крутится в OB1, замера худшего времени цикла у меня нет — ни разу не снимал.
Третья: «надеюсь, в DB с поинтерами не используются temp-переменные, они могут просачиваться между циклами». Вот это я проверил, и оно не подтвердилось: прогнал все семь блоков файла статическим проходом, ни одна VAR_TEMP не читается раньше, чем в неё записали.
Но проверка по следу замечания нашла другое. В блоке-секвенсоре циклограммы на ветке раннего выхода сбрасывались два выхода из трёх, а третий — номер текущего шага — сохранял значение прошлого вызова. Не мусор: правдоподобное число, которому верхний уровень верит. Одна строка, исправлено.
Замечание не подтвердилось, а проверка по нему нашла ошибку. Это стоит записать отдельно: проверять надо и те претензии, которые кажутся мимо.
@toolowinmydev добавил то, чего я по себе не вижу вообще: «там, где нужно было использовать ИИ — вы думали сами, а там где надо было делать самому — делегировали агенту». Проверить это утверждение я не могу — у меня нет доступа к тому, как моя работа выглядит снаружи. Записываю дословно, потому что оно про меня, а не про код.
Оба независимо друг от друга сказали одно и то же: получился монстр, поддерживать будет тяжело. Тут спорить нечем. Меньше кода и проще в сопровождении — разные вещи, а я их склеил.
А ещё через сутки @optemist ответил на мой разбор, и ответ оказался полезнее исходного замечания.
Первое: анализ времени цикла встроен в саму TIA Portal. То есть замера у меня нет не потому, что его тяжело сделать, а потому, что я не знал про инструмент, лежащий внутри среды, в которой я работал. Это неприятнее, чем просто «не измерил».
Второе — готовая альтернатива моей архитектуре: массивы в ПЛК держать только под хранение данных, а каждому устройству, крану или мотору, давать свой экземплярный DB и вызывать отдельно. Тогда перекрёстные ссылки остаются целыми. SCL — только под манипуляции с данными, остальное на LAD или FBD.
Третье, про энкодеры на скоростных входах и с постоянным периодом опроса, я к тому моменту уже знал — но узнал дорогим способом: на другом стенде программный счётчик на десятимиллисекундном цикле терял импульсы, и лечилось это аппаратным счётчиком, а не аккуратным кодом. Он это назвал сразу, не наступая.
Прав ли он полностью, я не знаю. У меня нет опыта сопровождения таких проектов через пять лет после сдачи, а у него пятнадцать лет такого опыта есть. Но это первая за все 98 дней конкретная архитектурная альтернатива, которую предложил мне человек, а не я сам себе.
К чему это здесь. В черновике этой статьи, в разделе про bus-factor, было написано, что за все эти месяцы ни один равный по компетенции человек не оспорил ни одного моего архитектурного решения. Когда я это писал, так и было. Первый технический разбор пришёл через два часа пятьдесят три минуты после публикации — я сверил по отметкам времени, потому что сам не поверил. Столько стоило внешнее ревью, которое я за 98 дней не смог организовать себе сам. И оно полезнее моего собственного по простой причине: он смотрел не туда, куда смотрю я.
Где я ошибся
Отдельный раздел, потому что это самая полезная часть для тех, кто пойдёт тем же путём.
1. Не завёл git с первого дня. Разобрано выше. Сотни архивных файлов, дубли, сборка тянет не ту версию, невозможность командной работы. Единственная причина — я не понимал инструмент и заменил его привычной с производства схемой «файл с датой». Это было ошибкой, и она стоила больше, чем неделя на изучение git.
2. Слишком долго доверял тому, что «выглядит работающим». Месяц незашифрованных вложений — прямое следствие. Проверка была, но она никогда не запускалась на данных, где обязана была дать отрицательный результат. Если бы я с самого начала применял отрицательный контроль так же дисциплинированно, как на стенде, этого месяца не было бы.
3. Переоценил параллельную оркестрацию. Пару раз я раздавал в параллель задачи, между которыми была связь, — и потом сводил результаты дольше, чем занял бы последовательный проход. Экономии не случилось, зато случились расхождения в структурах данных, которые ловились потом.
4. Bus-factor единица. Это главный риск, и я называю его прямо. Десять продуктов, 320 тысяч строк, вся история решений — в моей голове и в 202 HANDOFF-документах, написанных мной, в моих терминах, под мой способ мышления. Если завтра я выйду из проекта, войти в него сможет очень квалифицированный человек и очень нескоро.
Частично HANDOFF-и это лечат — они и писались как передача смены, и человек по ним восстановит логику решений лучше, чем по коду. Но полноценно не лечат: нет второго носителя контекста, нет постоянного ревью со стороны. Почти все критические замечания за эти 98 дней я сформулировал себе сам, а два внешних, разобранных выше, пришли уже после того, как работа была сделана и опубликована. Это лучше, чем ничего, и я не буду делать вид, что это то же самое, что второй инженер рядом.
5. Документация не заменяет второго инженера. Я генерировал руководства программно, и это правильно — они не расходятся с кодом. Но руководство по эксплуатации объясняет, как пользоваться. Оно не объясняет, почему система устроена именно так и что сломается, если поменять вот эту константу.
Что я вынес
Скорость выросла кратно, требовательность к проверке — тоже. «На порядок» написать не могу: до мая я никогда не измерял собственную выработку, и сравнивать сегодняшние 3 269 строк в день попросту не с чем. Кратно — это всё, за что я готов отвечать. И это не «зато». Это прямое следствие: чем дешевле производится код, тем дешевле производится код, который выглядит работающим. Раньше трудоёмкость реализации была естественным фильтром для плохих идей — на реализацию плохой идеи уходили недели, и по дороге ты успевал понять, что она плохая. Фильтр исчез. Заменить его можно только измерением: не «работает?», а «сколько», «на каком объёме», «при каком отказе», «что это доказывает и чего не доказывает».
Предметная область стала главным активом. Все ключевые решения в этих проектах — не про код. Отказаться от датчиков угла и вытянуть точность на калибровочной таблице с компенсацией люфта. Посчитать бюджет канала и закрыть две функции арифметикой до начала работы. Понять, что живой TCP не означает живой контроллер. Ассистент закрывает «как написать». Он не закрывает «что должно получиться» и «как понять, что получилось правильно». Инженер, который знает объект, теперь стоит дороже, а не дешевле, — потому что его дефицитный ресурс перестал быть связан узким местом реализации.
Инженерная культура из промышленности переносится в разработку напрямую. Мне не пришлось изобретать метод. Отрицательный контроль, нормирование погрешности, границы применимости методики, «проверка, которая в принципе не может ничего опровергнуть, — не проверка», жёсткое разделение «измерено / оценено / заявлено», отзыв результатов, полученных на неисправном оборудовании, отладка по одному изменению за прогон — всё это я двенадцать лет применял на стендах. В разработке ПО оно работает так же. Разница только в том, что на стенде эту дисциплину обеспечивают внешние требования, а в разработке её приходится обеспечивать себе самому.
И честный итог по масштабу. Девяносто восемь дней, десять продуктов и работ, 320 348 строк по замеру — это не история про то, как один человек заменил отдел. Это история про то, что один человек с сильной предметной подготовкой и дисциплиной проверки может довести до работающего состояния то, до чего раньше не доходили руки. С понятными ограничениями: bus-factor единица, часть функций честно не сделана, платить пока некому.
Об авторе
Груздев Дмитрий Михайлович, инженер АСУТП, 12 лет в промышленной автоматизации — испытательные стенды газотурбинных двигателей, ПЛК, SCADA, конструкторская документация по ЕСКД.
Соавтор изобретения по патенту РФ № 2 837 827 «Система для управления подачей топлива в двигатель на испытательном стенде»: заявка 31.01.2024, публикация 07.04.2025. Формулирую по буквам, потому что здесь легко сказать лишнего: я один из авторов изобретения, а патентообладатель — работодатель. Права на патент мне не принадлежат. Проверяется по номеру в реестре Роспатента за минуту, и правильно, что проверяется.
Упоминаю один раз и только затем, чтобы было понятно: всё написанное выше — взгляд человека из практической инженерии, а не из блогинга про нейросети.
Вопрос к читателям
Мне действительно интересно обсудить одну вещь.
Мой основной инструмент контроля — перенесённая с производства культура измерения: отрицательный контроль, границы применимости, разделение «измерено / оценено / заявлено». Она работает, но она полностью держится на одном человеке, который сам ставит себе проверки и сам их принимает.
Как вы у себя решаете задачу проверки кода, который появляется быстрее, чем его успевают читать? Что у вас реально ловит не «упало», а «выглядит работающим, но по факту не работает» — и делает это без ручного ревью каждой строки? И отдельно: у кого получилось выстроить такой контроль не в одиночку, и как выглядит разделение ролей, когда исполнителя перестало не хватать?
Обновления
Раздел заведён заранее и пуст. Найдёте расхождение между тем, что здесь написано, и тем, что следует из цифр, — напишите в комментариях: правка появится здесь с датой и вашим ником. В двух прошлых текстах такой раздел собрал десять пунктов, и оба стали от этого лучше.
Fedyaration
Ключевое тут не строки в день, а методика проверки.
n0isy
Дожили: x не y, а z - уже в комментариях. Получается нас нейрослоп разговаривать научил.
Автору, чтоб два раза не вставать: поставил минус сразу за весь сгенерированный текст, который он не удосужился прочитать.
Оох и махровый же нейрослопище!