Инженерная практика
Увеличивает ли AI дублирование кода: проверяем гипотезу на истории своего репозитория
Рост скорости разработки с AI может скрывать увеличение копирования, снижение рефакторинга и накопление стоимости сопровождения. Но отдельные неудачные примеры ничего не доказывают. Нужен воспроизводимый анализ истории репозитория.
Что именно мы проверяем
Дублирование кода — это одинаковые или очень похожие фрагменты, существующие в нескольких местах. Само по себе совпадение не всегда плохо: сгенерированные файлы, миграции и независимые адаптеры могут повторяться намеренно. Риск возникает, когда одна бизнес-логика размножается и её приходится синхронно исправлять в нескольких модулях.
Рабочая гипотеза звучит так:
После систематического внедрения AI доля дублированного кода в новых изменениях выросла, а разработчики стали реже переиспользовать и изменять существующие модули.
Для проверки недостаточно сравнить общий процент дубликатов в двух снимках. Репозиторий растёт, код перемещается, меняются языки и состав команды. Поэтому сравним четыре сигнала:
- Доля дубликатов в снимках кода и, отдельно, среди добавленных строк.
- Перемещённый код: сколько изменений Git распознаёт как переименование или перенос.
- Связность новых функций: используют ли они существующие модули или создают параллельные реализации.
- Изменения старых модулей: затрагивает ли новая работа устоявшиеся файлы либо почти всегда добавляет новые.
До начала: зафиксируйте дизайн сравнения
Сначала определите дату, после которой AI стал регулярной частью разработки. Это должна быть известная внутренняя веха: например, начало командного пилота. Не выбирайте границу после просмотра графика — иначе легко подогнать период под желаемый вывод.
Возьмите два сопоставимых окна. Например:
до: 2025-01-01 — 2025-03-31
после: 2025-04-01 — 2025-06-30
Это только пример дат, а не факт о каком-либо проекте. Желательно, чтобы окна:
- имели одинаковую продолжительность;
- не включали массовую миграцию только с одной стороны;
- относились к одной основной ветке;
- имели сопоставимые языки, объём работы и правила ревью.
Заранее запишите исключения: каталоги зависимостей, сборочные артефакты, сгенерированный код, fixtures, snapshots, vendored-код и минифицированные файлы. Одни и те же правила должны действовать в обоих периодах.
Безопасная подготовка
Анализ лучше выполнять в отдельном клоне или дополнительном рабочем дереве. Все команды ниже читают историю и создают отчёты в каталоге .analysis; они не переписывают коммиты и не отправляют данные наружу.
git status --short
git branch --show-current
git log -1 --format='%H %cI'
mkdir -p .analysis/before .analysis/after
Если рабочее дерево содержит незакоммиченные изменения, не удаляйте и не прячьте их автоматически. Для снимков используйте git archive: он извлекает только содержимое выбранного коммита и не переключает текущую ветку.
Определите последние коммиты каждого окна:
BEFORE_END='2025-03-31T23:59:59'
AFTER_END='2025-06-30T23:59:59'
git rev-list -1 --before="$BEFORE_END" HEAD
git rev-list -1 --before="$AFTER_END" HEAD
Скопируйте полученные хеши вручную и проверьте даты:
BEFORE_COMMIT='вставьте_проверенный_хеш'
AFTER_COMMIT='вставьте_проверенный_хеш'
git show -s --format='%H %cI %s' "$BEFORE_COMMIT"
git show -s --format='%H %cI %s' "$AFTER_COMMIT"
Не запускайте дальнейшие команды, если хеш пуст, относится не к той ветке или его дата выходит за выбранное окно.
Шаг 1. Сравните дубликаты в двух снимках
Создайте каталоги со снимками:
mkdir -p .analysis/before/tree .analysis/after/tree
git archive "$BEFORE_COMMIT" | tar -x -C .analysis/before/tree
git archive "$AFTER_COMMIT" | tar -x -C .analysis/after/tree
Для поиска клонов нужен локально одобренный анализатор, поддерживающий языки проекта и машиночитаемый отчёт. Конкретный инструмент не принципиален; важнее одинаковые версия и конфигурация. Не отправляйте закрытый код во внешний сервис без разрешения владельца репозитория.
Ниже — пример абстрактной конфигурации. Имена параметров уточните в документации выбранного анализатора:
{
"minimumTokens": 50,
"minimumLines": 5,
"format": "json",
"exclude": [
"**/vendor/**",
"**/node_modules/**",
"**/dist/**",
"**/build/**",
"**/generated/**",
"**/*.min.js",
"**/__snapshots__/**"
]
}
Пример запуска условной команды clone-detector:
clone-detector \
--config duplication.json \
--root .analysis/before/tree \
--output .analysis/before/duplicates.json
clone-detector \
--config duplication.json \
--root .analysis/after/tree \
--output .analysis/after/duplicates.json
clone-detector здесь — обозначение выбранного вами инструмента, а не гарантированно установленная программа.
Для каждого снимка сохраните:
- число анализируемых строк;
- число строк, входящих хотя бы в один клон;
- долю дублированных строк;
- число групп клонов;
- медианный размер группы;
- дубликаты внутри одного модуля и между модулями.
Основная формула:
доля дубликатов =
уникальное число строк, входящих в группы клонов
------------------------------------------------ × 100%
все анализируемые строки
Не суммируйте длины пересекающихся совпадений: одна строка должна учитываться один раз.
Шаг 2. Отделите новый долг от старого
Сравнение снимков показывает накопленный итог, но крупная старая копия может скрыть ухудшение новых изменений. Поэтому отдельно измерьте добавленные строки за каждый период.
BEFORE_FROM='2025-01-01'
BEFORE_TO='2025-03-31 23:59:59'
AFTER_FROM='2025-04-01'
AFTER_TO='2025-06-30 23:59:59'
git log --since="$BEFORE_FROM" --until="$BEFORE_TO" \
--no-merges --numstat --format='commit %H' \
> .analysis/before/numstat.txt
git log --since="$AFTER_FROM" --until="$AFTER_TO" \
--no-merges --numstat --format='commit %H' \
> .analysis/after/numstat.txt
--numstat даёт объём добавлений и удалений, но не определяет, какие строки дублированы. Для точной оценки новых дубликатов нужен анализ патчей: сопоставьте добавленные строки с группами клонов в конечном снимке. Практически полезны две метрики:
доля новых строк в клонах =
добавленные в период строки, которые входят в клоны конечного снимка
------------------------------------------------------------------- × 100%
все добавленные в период анализируемые строки
доля новых межмодульных клонов =
добавленные строки в клонах, пересекающих границы модулей
---------------------------------------------------------- × 100%
все добавленные в период анализируемые строки
Если автоматического сопоставления нет, возьмите одинаковую случайную или систематическую выборку групп из обоих периодов и проведите слепую ручную классификацию. Размер выборки и правило отбора зафиксируйте до просмотра содержимого.
Шаг 3. Измерьте перенос и переименование
Git не хранит перенос файла как отдельное событие: он вычисляет сходство содержимого. Поэтому результат зависит от порога. Используйте один порог в обоих окнах и сохраните его в отчёте.
git log --since="$BEFORE_FROM" --until="$BEFORE_TO" \
--no-merges --find-renames=50% --find-copies=50% \
--name-status --format='commit %H' \
> .analysis/before/moves.txt
git log --since="$AFTER_FROM" --until="$AFTER_TO" \
--no-merges --find-renames=50% --find-copies=50% \
--name-status --format='commit %H' \
> .analysis/after/moves.txt
Статусы R означают распознанные переименования, C — копии. Считайте их раздельно. Рост C при стабильном или падающем R может согласовываться с гипотезой о копировании, но не доказывает её: Git способен принять шаблонные или сгенерированные файлы за копии.
Для нормализации используйте число событий на 100 изменённых файлов:
копии на 100 файлов = C / число изменённых файлов × 100
переносы на 100 файлов = R / число изменённых файлов × 100
Проведите проверку чувствительности с порогами 40%, 60% и 80%. Если вывод меняется вместе с порогом, признак слишком нестабилен для сильного заключения.
Шаг 4. Оцените связность новых функций
Под связностью здесь понимается не математическая метрика графа, а наблюдаемое включение новой функции в существующую архитектуру. Новая возможность обычно считается лучше связанной, если она:
- вызывает существующие доменные сервисы вместо повторения их логики;
- расширяет текущие интерфейсы и типы;
- добавляет обработчик рядом с аналогичными обработчиками;
- сопровождается изменениями общих тестов и документации;
- не создаёт параллельный набор утилит с похожими именами.
Автоматически определить «новую функцию» по Git сложно. Надёжнее использовать единицу работы, уже принятую в проекте: merge-коммит, pull request или задачу. Если доступны только коммиты, заранее установите правило группировки.
Для каждой выбранной единицы работы заполните карточку:
| Поле | Допустимые значения |
|---|---|
| Период | до / после |
| Новая пользовательская возможность | да / нет / неясно |
| Изменён существующий доменный модуль | да / нет |
| Использован существующий публичный интерфейс | да / нет / неприменимо |
| Создана параллельная реализация | да / нет / неясно |
| Есть межмодульный клон бизнес-логики | да / нет / неясно |
Итоговую долю считайте только по однозначным карточкам. Значения «неясно» показывайте отдельно, а не превращайте в «нет».
Шаг 5. Проверьте, меняются ли старые модули
Если после внедрения AI разработчики чаще добавляют автономные файлы и реже интегрируют изменения в существующую архитектуру, доля изменений старых модулей может снизиться.
Зафиксируйте порог возраста. Например, «старым» считается файл, существовавший не менее 180 дней к началу периода. Это пример операционного определения; для короткоживущего проекта разумнее выбрать меньший порог.
Получить дату первого появления файла можно так:
git log --follow --diff-filter=A \
--format='%aI' -- path/to/file | tail -n 1
Команда безопасна, но запускать её отдельно для тысяч файлов медленно. Для большого репозитория подготовьте кэш соответствия «путь — дата появления» один раз и сохраните параметры анализа.
Сравните:
доля работ со старым модулем =
единицы работы, изменившие хотя бы один старый доменный файл
----------------------------------------------------------- × 100%
все выбранные единицы работы
доля новых файлов =
впервые добавленные анализируемые файлы
--------------------------------------- × 100%
все изменённые анализируемые файлы
Исключите документацию, конфигурацию, миграции и тестовые данные либо покажите их отдельными категориями. Иначе изменение структуры инфраструктуры исказит картину доменного кода.
Сведите результаты в одну таблицу
Не объединяйте показатели в непрозрачный «индекс качества». Покажите исходные числители, знаменатели и нормализованные доли:
| Показатель | До AI | После AI | Изменение |
|---|---|---|---|
| Дублированные строки / анализируемые строки | заполнить | заполнить | п.п. и % |
| Новые строки в межмодульных клонах | заполнить | заполнить | п.п. и % |
| Копии Git на 100 изменённых файлов | заполнить | заполнить | абсолютное |
| Переносы Git на 100 изменённых файлов | заполнить | заполнить | абсолютное |
| Новые функции, меняющие старый доменный модуль | заполнить | заполнить | п.п. |
| Новые функции с параллельной реализацией | заполнить | заполнить | п.п. |
Процентные пункты и относительный рост — разные величины. Переход с 4% до 6% равен росту на 2 процентных пункта, или на 50% относительно исходного значения.
Как проверить результат
- Повторите запуск. Одинаковые коммиты, конфигурация и версия анализатора должны дать те же отчёты.
- Проверьте выборку вручную. Просмотрите несколько крупнейших и несколько случайных групп клонов из каждого периода.
- Удалите известный шум. Повторите расчёт без миграций, generated-кода, snapshots и тестовых fixtures.
- Измените пороги. Сравните результаты при разных минимальных размерах клона и порогах сходства Git.
- Разделите языки. Не смешивайте показатели для SQL, TypeScript и шаблонов, если анализатор обрабатывает их по-разному.
- Нормализуйте по объёму. Покажите значения на тысячу добавленных строк, на 100 файлов или на единицу работы.
- Сравните соседние окна. Если ухудшение началось до AI, вероятнее другая причина: сроки, реорганизация или смена архитектуры.
Гипотеза получает поддержку, если несколько устойчивых сигналов движутся согласованно: растут новые межмодульные клоны и Git-копии, падает доля изменений старых модулей, а ручная проверка обнаруживает параллельную бизнес-логику. Один выросший процент дубликатов недостаточен.
Типовые ошибки
Сравнивать только текущий снимок с очень старым
Так измеряется рост репозитория, а не влияние AI. Нужны одинаковые окна и анализ новых строк.
Считать любое совпадение дефектом
Импорты, декларативные схемы, миграции и тестовые таблицы могут повторяться обоснованно. Классифицируйте типы клонов.
Менять конфигурацию между периодами
Разные исключения или пороги делают сравнение недействительным. Версионируйте конфигурацию анализа отдельно от исследуемых снимков.
Использовать только число групп
Десять крошечных групп и десять копий по 300 строк — разные риски. Сохраняйте объём, размер и расположение клонов.
Игнорировать переименования
Перенесённый файл может выглядеть как удаление и повторное добавление. Используйте одинаковое обнаружение переименований и проверяйте несколько порогов.
Выдавать корреляцию за причинность
Одновременно с AI могли измениться сроки, команда, архитектура, доля начинающих разработчиков или правила ревью. Формулируйте вывод как наблюдаемую связь.
Публиковать чувствительные фрагменты
Отчёты детекторов могут содержать исходный код и пути. Перед передачей отчёта проверьте политику доступа и удалите содержимое, не нужное для агрегированной статистики.
Ограничения метода
- Дата внедрения AI редко означает одновременное и одинаковое использование всеми разработчиками.
- Git фиксирует итоговые изменения, но не показывает отклонённые подсказки и промежуточные варианты.
- Детекторы находят синтаксическое сходство лучше, чем смысловое дублирование.
- Низкая доля клонов не гарантирует хорошую архитектуру: избыточные абстракции тоже увеличивают стоимость сопровождения.
- Изменение старых модулей может быть как полезной интеграцией, так и признаком высокой связанности.
- Небольшие периоды и малое число функций дают нестабильные доли.
- Результаты одного репозитория нельзя автоматически переносить на другие команды и стеки.
Что делать, если рост подтвердился
Не запрещайте AI на основании одной метрики. Сначала найдите повторяющиеся сценарии: новые интеграции, обработчики, валидаторы, преобразования DTO или инфраструктурные адаптеры. Затем измените процесс там, где возникает проблема.
- Добавьте в запрос к AI указание сначала найти существующие реализации и точки расширения.
- Требуйте от автора перечислить переиспользованные модули и объяснить появление новой абстракции.
- Проверяйте дубликаты только в изменённых строках, чтобы не блокировать работу из-за старого долга.
- Установите порог предупреждения, а не автоматический запрет, пока команда не проверит качество сигнала.
- Выделяйте рефакторинг в ту же единицу работы, когда новая функция создаёт второй экземпляр бизнес-правила.
- Повторяйте измерение через равные интервалы с той же конфигурацией.
Больше практик по безопасной оценке агентной разработки собрано в гайдах Agent Lab Journal. Определения используемых терминов доступны в глоссарии.
Критерий завершения
Исследование можно считать воспроизводимым, если другой инженер способен по зафиксированным датам, хешам коммитов, версии инструмента, конфигурации исключений и правилам классификации получить те же агрегированные значения.
Хороший итоговый вывод звучит осторожно и проверяемо:
В выбранном репозитории после начала регулярного использования AI выросла доля новых межмодульных клонов, одновременно снизилась доля функций, изменяющих существующие доменные модули. Эффект сохраняется после исключения generated-кода и при нескольких порогах сходства. Это поддерживает гипотезу о росте копирования, но не доказывает, что причиной был AI.
Если сигналы расходятся, честный результат — гипотеза не подтверждена. Ценность анализа не в поиске обвиняемого, а в обнаружении места, где ускорение разработки начинает превращаться в будущую стоимость сопровождения.