
В погоне за нейрослопным кодом. Поймал не то, за чем гнался
Искал, чем машинный код отличается от человеческого: 33 репозитория, 7 языков, 587 тысяч функций, 13.5 млн строк. Детектора не нашёл, зато нашёл величину, которая не сдвинулась за 25 лет. Попутно опроверг три своих вывода из четырёх.
Как отличить слоп от кода
В прошлый раз я мерил текст: комментарии, коммиты, прозу. Вопрос напрашивался сам - а код? Если у машинной прозы есть измеримые приметы, должны быть и у машинного кода.
Примет я не нашёл. Нашёл другое, про что и пост.

Повод дала статья Measuring the sloppiness of code. Там предлагают две метрики из SlopCodeBench, обе с красивым разделением:
Verbosity = |строки ast-grep ∪ строки клонов| / LOC
mass(f) = CC(f) * SLOC(f)
Erosion = Σ mass(f), где CC(f) > 10 / Σ mass(f)Erosion - это доля массы кода, сидящей в тяжёлых функциях. У живых репозиториев обещано 0.31 ± 0.17, у кода агентов 0.68 ± 0.20. Вдвое. Выглядит как готовый детектор.
Я написал считалку на go/ast и прогнал на Go stdlib.
erosion:
go/src/go 0.908
encoding 0.899
net/http 0.779Стандартная библиотека Go, которую полируют с 2009 года, - выше «агентского» уровня. Не на процент, а сильно.
Подумал, что дело в моём SLOC: я считал физические строки, а надо значащие. Пересчитал. Сдвинулось в третьем знаке: 0.775 против 0.769.
Дело не в моём коде. Порог CC > 10 абсолютный, а масса CC * SLOC усиливает большие функции квадратично. В stdlib кодеки и парсеры - это гигантские switch, и они правы. Позже я добавил coreutils (0.913) и sqlite (0.843) - код, у которого копирайты начинаются с восьмидесятых и с двухтысячных.
Восемь корпусов, шесть из них - эталонный человеческий код. Все выше порога, за которым положено начинаться слопу.
Тогда что вообще устойчиво
Раз готовая метрика не подошла, я стал искать величины, которые держатся сами - через языки, через десятилетия, через смену авторов. Взял четыре кандидата и начал добавлять корпуса.
К концу получилось 33 корпуса, 7 языков, 587 тысяч функций и тринадцать с половиной миллионов строк кода.
| корпус | язык | строк | функций | история |
|---|---|---|---|---|
linux 5.10.269, fs kernel mm | C | 2 439 210 | 41 638 | |
postgres src/backend | C | 1 394 960 | 16 429 | 65 380 коммитов, 60 авторов, с 1996 |
| sqlite 3.46.0 | C | 371 628 | 3 187 | |
coreutils 9.4 src | C | 125 134 | 1 349 | |
| eclipse.platform.ui | Java | 1 190 812 | 71 300 | 44 467 коммитов, 717 авторов, с 2001 |
| eclipse.jdt.core | Java | 1 144 512 | 86 102 | 27 900 коммитов, 248 авторов, с 2001 |
| eclipse.jdt.ui | Java | 1 141 626 | 67 651 | 31 210 коммитов, 262 автора, с 2001 |
| eclipse.platform | Java | 933 766 | 51 956 | 45 381 коммит, 423 автора, с 2001 |
| eclipse.platform.swt | Java | 655 094 | 33 223 | 32 106 коммитов, 282 автора, с 2001 |
| org.eclipse.rcptt | Java | 781 582 | 42 476 | 2 218 коммитов, 40 авторов, с 2014 |
| dltk.core | Java | 566 830 | 27 905 | 6 512 коммитов, 37 авторов, с 2007 |
| dltk.tcl | Java | 139 750 | 7 429 | 2 856 коммитов, 13 авторов, с 2007 |
| rcpml | Java | 17 062 | 1 082 | CVS, ревизии 2005-2008 |
cpython Lib | Python | 428 048 | 61 210 | |
| tokio 1.53.1 | Rust | 121 636 | 7 157 | |
| ripgrep 15.2.0 | Rust | 68 862 | 2 953 | |
Go stdlib 1.27, go net/http encoding | Go | 165 398 | 4 539 | |
| Tenniarb | Swift | 45 972 | 1 550 | 374 коммита, с 2017 |
| hum1izer | Go | 22 544 | 328 |
Плюс четыре проекта с заявленным машинным происхождением и четыре моих рабочих - о них ниже. Полная таблица с версиями и источниками лежит в репозитории.
Вот итог по четырём кандидатам.
| кандидат | что вышло |
|---|---|
| глубина вложенности | устоял |
| плотность функций | опровергнут |
| доля тривиальных функций | опровергнут |
| медиана цикломатики | держится, но бесполезна |
Три из четырёх я опроверг сам, пока добавлял корпуса. По порядку.
Опровержение первое: подпись, которой нет
У меня в ~/.claude/CLAUDE.md лежат правила для агента: не плоди абстракции, не делай хелпер для одноразовой операции, три похожих строки лучше преждевременного обобщения.
Сравнил два своих проекта, написанных под этими правилами, со stdlib:
| корпус | функций до 3 строк | медиана SLOC | медиана CC |
|---|---|---|---|
| go/src/go | 32.3% | 7 | 2 |
| net/http | 24.2% | 8 | 2 |
| hum1izer | 7.9% | 13 | 4 |
Картина читалась сама: правила убрали однострочные обёртки, функции стали плотнее. Отпечаток инструкций в цифрах. Я это записал и почти поверил.
Потом добавил ещё два своих проекта - того же автора, того же происхождения, под тем же глобальным файлом.
| проект | функций до 3 строк | медиана SLOC | CC>10 |
|---|---|---|---|
| веб-бэкенд на Go + Svelte | 26.1% | 7 | 3.8% |
| веб-сервис, Go + TS | 10.8% | 13 | 6.5% |
| CLI с TUI | 18.3% | 12 | 12.2% |
| hum1izer: парсер и TUI | 7.9% | 13 | 16.5% |
Разброс внутри группы - втрое по обёрткам и вчетверо по CC>10. Он полностью перекрывает диапазон stdlib: веб-бэкенд вышел мелкогранулярнее net/http, а hum1izer плотнее всех корпусов замера.
Зато видно, что форму задаёт на самом деле. Слоистый бэкенд - это тонкие методы репозитория и хендлеры по три строки. Парсер с экраном разбора - это алгоритмы, которые не режутся на обёртки. Одни и те же правила, один и тот же автор, разница втрое - и объясняется она задачей, а не инструкциями агенту.
Добил это мой собственный код двадцатилетней давности. RCPML, Java, ревизии с 2005 по 2008, лежит в CVS-архиве. Писался за тринадцать лет до первого копилота. 57% функций короче трёх строк.
Мой вывод строился на том, что у hum1izer их 7.9%, а у stdlib 21-32%. Реальный диапазон по всем корпусам - от 4.4% до 57%, и решает язык со своими идиомами: в Java половина функций это геттеры и делегаты. Тот же я, тот же подход, разница втрое - просто язык другой.
На двух проектах видно ровно то, что ищешь.
Опровержение второе: плотность функций
Считал функции на тысячу значащих строк. По восьми Go-корпусам получалось 38-56, и это выглядело законом: независимо от возраста, автора и задачи.
Потом пришла Java: 57.3, 60.7, 63.4, 69.3, 102.5.
Два почти непересекающихся диапазона, и причина та же - геттеры. Величина описывает язык, а не дисциплину.
Опровержение третье: медиана цикломатики
«От 2 до 4 во всех корпусах без исключения» - записал я, имея восемь корпусов. На тринадцатом один из проектов дал 1, потом оба Java-корпуса тоже дали 1.
Величина и правда устойчивая, диапазон 1-4. Только порога из неё не сделать: разница между «медиана 1» и «медиана 4» - это разница между Java с геттерами и Go с обработкой ошибок, а не между хорошим и плохим кодом.
Что устояло
Глубина вложенности. Максимум уровней if/for/while/switch внутри одной функции.
Медиана равна 1 во всех тридцати трёх корпусах. Во всех семи языках. В коде, которому и год, и тридцать лет. p95 равен 2 или 3 в двадцати семи корпусах из тридцати двух, где я мерил разбором дерева.
Доля функций глубже четырёх уровней:
| корпус | язык | depth>4 |
|---|---|---|
| tokio | Rust | 0.1% |
| ripgrep | Rust | 0.4% |
| linux/kernel | C | 0.4% |
| cpython/Lib | Python | 0.5% |
| eclipse.platform.ui | Java | 0.7% |
| org.eclipse.rcptt | Java | 0.7% |
| eclipse.platform | Java | 1.1% |
| eclipse.jdt.ui | Java | 1.2% |
| dltk.core | Java | 1.5% |
| eclipse.jdt.core | Java | 1.6% |
| tenniarb | Swift | 2.1% |
| go/src/go | Go | 2.3% |
| postgres | C | 2.8% |
| coreutils | C | 7.3% |
| sqlite | C | 7.3% |
Разброс - в семьдесят раз между краями, но двадцать девять корпусов из тридцати двух сидят ниже трёх процентов. Выбиваются sqlite, coreutils и один маленький проект на TypeScript.
Для сравнения, обычный порог CC > 10 по тем же корпусам гуляет от 0.6% до 21.5%. И упирается он не в неряшливость: go/src/go и encoding дают по 11.9%, PostgreSQL 13.0%. Критерий, который срабатывает на каждой девятой функции стандартной библиотеки, агент научится игнорировать первым же.
Шесть корпусов из этих тридцати трёх писал я сам
Вышло случайно: под руку попались проекты, в которых я участвовал, причём в разные годы.
| проект | мои коммиты | доля | годы |
|---|---|---|---|
| rcpml | 1067 ревизий CVS | 85% | 2005-2008 |
| dltk.tcl | 1203 | 42% | 2007-2013 |
| dltk.core | 1527 | 23% | 2007-2016 |
| org.eclipse.rcptt | 181 | 8% | 2015-2022 |
| Tenniarb | 373 | 99.7% | 2017-2026 |
| Intabia Platform | 2741 | 28% | 2021-2026 |
Это 110 тысяч функций из 587, пятая часть замера. Двадцать лет, пять языков. Глубина у всех шести в норме: rcpml 0.9%, rcptt 0.7%, dltk.core 1.5%, платформа 1.9%, Tenniarb 2.1%.
Против гипотезы про «подпись правил в коде» это работает лучше остального. Правилам для агента у меня меньше года, а глубина в моём коде была такой же в 2005, когда я писал Java руками в Eclipse 3.1.
И она не двигается со временем
У части репозиториев есть история на двадцать пять лет, и грех было не посмотреть. Я снял состояние на конец каждого года через git archive и померил ряды.
eclipse.platform, 25 лет, 423 автора, 45 тысяч коммитов:
| год | функций | depth>4 | CC>10 | SLOC>60 |
|---|---|---|---|---|
| 2001 | 1537 | 1.11% | 0.91% | 0.26% |
| 2005 | 12818 | 1.93% | 2.71% | 0.98% |
| 2013 | 8358 | 1.56% | 2.09% | 0.74% |
| 2021 | 13469 | 1.46% | 1.69% | 0.99% |
| 2026 | 51956 | 1.09% | 1.80% | 1.04% |
Кодовая база выросла в тридцать четыре раза и вернулась ровно туда, откуда начала. При этом цикломатика за те же годы удвоилась, а доля длинных функций выросла вчетверо.
PostgreSQL, 30 лет: depth p95 равен 4 с 1997 года. Двадцать девять лет подряд одно и то же число. Всё остальное выросло вдвое за первое десятилетие и встало на плато.
DLTK за 18 лет: depth>4 1.69% → 1.61%. RCPTT за 11 лет: 0.75% → 0.78%.
Похоже, это потолок, через который просто не лезут. Код стареет, обрастает, цикломатика и длина функций ползут вверх - а вложенность остаётся где была.

Почему нельзя брать цикломатику
Цикломатику функции понижают ровно одним способом: выделяют кусок в отдельную функцию. А у меня в правилах для агента записано «не делай хелпер для одноразовой операции».
Два требования, которые нельзя выполнить одновременно.
Глубина таким свойством не обладает. Она падает от раннего возврата и guard clause - то есть от удаления кода, а не от добавления функций. Ни с одним правилом про абстракции это не конфликтует.
Отсюда же понятно, почему depth > 4 - лучший критерий, чем CC > 10, хотя они связаны. Функций глубоких, но простых, почти нет: 0.0-0.8% в тех семи корпусах, где я считал пересечение. Плоских, но сложных - от 3.7% до 16%. Плоский switch на сорок веток читается нормально, цикломатика его валит, глубина - нет.
Где правило реально работает
Внутри C цифры разошлись на две группы.
| проект | правило записано | depth>4 |
|---|---|---|
| linux/kernel | да | 0.4% |
| linux/mm | да | 0.4% |
| linux/fs | да | 0.9% |
| PostgreSQL | нет | 2.8% |
| sqlite | нет | 7.3% |
| coreutils | нет | 7.3% |
В Linux CodingStyle написано прямым текстом: «if you need more than 3 levels of indentation, you’re screwed anyway, and should fix your program». В PostgreSQL такого нет - и там 2.8% при тридцати годах истории, 65 тысячах коммитов и шестидесяти коммиттерах. Review там жёстче, чем почти везде в моей выборке. Не помогло.
А в Java, Go, Rust и Python величина держится в 0.1-2.3% вообще без писаных правил. eclipse.platform с его 1.1% отсылает к конвенциям Oracle на 37 строк, где про вложенность ни слова.
Похоже, дело в языке. В C вложенность растёт сама: исключений нет, ранний выход идёт через goto, проверка ошибок ветвится на каждом вызове. Там, где язык не сопротивляется, хватает общей культуры. Там, где сопротивляется, работает только записанное правило.
А слоп-то она различает?
Нет.
Я нашёл на GitHub репозитории, где авторы сами пишут в описании «100% AI-generated» и «written entirely by Claude Code». Сравнивать нужно внутри языка - я уже обжёгся на том, что метрики зависят от языка сильнее, чем от автора.
Rust дал единственную пару с приличными выборками:
| корпус | кто писал | функций | SLOC p95 | SLOC>60 | depth>4 |
|---|---|---|---|---|---|
| tokio | человек | 7157 | 29 | 0.5% | 0.1% |
| ripgrep | человек | 2953 | 34 | 1.2% | 0.4% |
| slopos | агент | 17540 | 45 | 2.3% | 0.3% |
| codeprism | агент | 4376 | 50 | 3.3% | 1.4% |
Машинный код отличается длиной функций: p95 45-50 против 29-34, длинных втрое больше. А глубина у одного из них - 0.3%, ровно человеческая норма.
Go и C вообще сигнала не дали: агентский Go-проект оказался чище моего, агентский C - в десять раз чище coreutils.
Тут до меня дошло, что я мерил не то, что искал. Глубина говорит про читаемость, а не про то, кто писал. Плоский код одинаково читается независимо от автора, так что отличать машину от человека она и не обязана. Для этого работает объём, про что исходная статья и пишет.
Чем мерил и где оно врёт
Взял tree-sitter: один обход дерева, восемь грамматик, CGO меня не пугал. Сверил с эталоном: на своём проекте tree-sitter и go/ast дали одинаковые 328 функций, тот же топ, расхождение цикломатики в пределах ±3.
А потом оказалось, что на C он разваливается.
файлов с ошибкой разбора:
sqlite 75.0%
PostgreSQL 56.8%
linux/fs 54.6%
coreutils 36.2%
---
Java 0.0%
Go 0.0%Препроцессор. #if внутри тела функции ломает дерево, и функция «съедает» остаток файла. winWrite в sqlite3.c намерилась в 19703 строки при настоящих 104 - я полез проверять руками и нашёл её границы на строках 49451-49555.
Лечится отбраковкой функций с узлами ERROR внутри. Теряется 8-34% выборки, и потеря не случайная: выбывают места с самым плотным препроцессором, а они обычно и самые запутанные.
Отдельно: из шести языков, которые мне были нужны, два не завелись вовсе. Kotlin отдаёт биндинги только для c, node и rust. Swift не держит сгенерированный парсер в репозитории.
Для Swift пришлось считать глубину по отступам, без парсера. Дальше выяснилось, что парсер для неё вообще не нужен.
Парсер, которого не нужно
Цикломатику без разбора кода не посчитать. А глубину видно по отступам.
| корпус | дерево | отступы |
|---|---|---|
| go/src/go | 1/3/4/5, 2.3% | 1/3/4/6, 2.7% |
| net/http | 1/2/3/4, 0.5% | 1/3/3/4, 0.4% |
| hum1izer | 1/3/3/4, 0.3% | 2/3/3/4, 1.0% |
Медиана, p90 и p95 совпадают. Хвост завышен примерно вдвое - gofmt даёт switch два уровня отступа там, где дерево видит один. Лечится порогом 5 вместо 4.
То есть единственная метрика, пережившая весь замер, считается без CGO, без грамматик и на любом языке с отступами. AST был нужен ровно для того, от чего замер велел отказаться.
Первая версия считалки по отступам выдала на одном проекте depth>4 = 13.3%. Дерево на том же проекте показывало 0.0%, поэтому я полез смотреть, что она там насчитала.
Шаг отступа я определял как минимальную разницу между уровнями. А в коде есть перенос аргументов функции, и выравнивание по открывающей скобке даёт разницу в один-два пробела. Минимум подхватывал её и делил на неё всё остальное.
Заменил на самую частую разницу вместо самой маленькой - стало 0.0%. Если бы я не знал заранее правильного ответа, 13.3% выглядели бы вполне правдоподобно.
Что я забрал себе
Метрика, которая не воспроизвелась на вашем коде, - не ваша метрика. Erosion обещала 0.31 у живых репозиториев. Go stdlib дал 0.908, coreutils 0.913. Я перепроверил свой счётчик, пересчитал SLOC, сверился с эталонным парсером - цифры не сдвинулись. Красивая формула из статьи может просто не работать на вашем языковом миксе.
Вывод на двух проектах - это не вывод. «Подпись правил в коде» держалась ровно до третьего корпуса и рассыпалась на четвёртом. Мой же код из 2006 года добил её окончательно. Если гипотеза подтверждается на всей выборке, которая у вас есть, - значит выборка мала.
Форму кода задаёт задача, а не инструкции. Веб-бэкенд со слоями даёт 26% функций в три строки, парсер с TUI - 7.9%. Один автор, одни правила, разница втрое. Прежде чем приписывать разброс дисциплине, посмотрите, не объясняется ли он жанром.
Сравнивать метрики между языками бессмысленно. Доля коротких функций: 4.4% в ядре Linux, 57% в старой Java. Плотность функций: Go 38-56, Java 57-102. Две величины, которые я принял за свойства дисциплины, оказались счётчиками геттеров.
Порог, который нарушает каждая десятая функция хорошего кода, - плохой порог. CC > 10 в stdlib Go даёт 11.9%, в PostgreSQL 13.0%. depth > 4 там же - 2.3% и 2.8%, а в двадцати девяти корпусах из тридцати двух ниже трёх процентов. Первый агент научится игнорировать, второй заметит.
Проверяйте, не конфликтует ли новое правило со старыми. Цикломатику понижают выделением функции. У меня же записано «не плоди хелперы». Я чуть не выдал агенту два взаимоисключающих требования и не понял бы, почему он мечется.
Ищите то, что не меняется со временем. eclipse.platform вырос в 34 раза, цикломатика удвоилась, длина функций выросла вчетверо, глубина вернулась к исходной. Величина, которая двадцать пять лет стоит на месте, - готовый кандидат в пороги. Та, что ползёт, будет требовать пересмотра каждые пару лет.
Правило нужно там, где язык сопротивляется. В Java и Rust вложенность держится сама. В C - нет, и там записанная строчка в CodingStyle даёт разницу в разы, которую не даёт даже тридцатилетний review PostgreSQL.
Сверяйте новый счётчик с чужим. Ошибка в определении шага отступа дала 13.3% вместо 0.0%. Она ничего не ломала и не ругалась - просто выдавала правдоподобные проценты. Поймал я её только потому, что знал правильный ответ из другой реализации.
Инструменты замера лежат в haiodo/hum1izer под support-tools: считалка через tree-sitter на восемь языков, глубина по отступам без парсера, срез репозитория по годам, реализация метрик из исходной статьи. Методика там же: что считается, в каком порядке, какие поправки нужны для C и для смешанных репозиториев, чего этими метриками измерить нельзя. Данные - список корпусов с версиями и объёмом, ряды по годам для PostgreSQL за 30 лет и Eclipse за 25.
В сам инструмент из всего замера попала одна проверка: если комментарий длинный, а функция под ним вложена глубже пяти уровней, совет меняется с «уложись в две строки» на «чини функцию, а не текст». Потому что в такой ситуации предыдущий совет был просто неверным.
Этот пост тоже прогнан через hum1izer. Замечания были, все мои, все исправлены.