В репозитории PrincetonUniversity/SPECFEMPP обнаружился редкий, но показательный случай: пул-реквест, который должен был устранить циклические зависимости в C++-проекте, вместо этого породил новый цикл из семи файлов. Обычный code review его не заметил. Заметил автоматический инструмент архитектурного анализа.
Как чистка зависимостей добавила новый цикл
PR под номером 943 назывался Single include header for assembly module и выглядел разумно: убрать узкий фасадный заголовок, заменить прямые include на forward declarations, разгрузить properties_container.hpp. По названию и описанию - правильное архитектурное решение.
Проблема пришла с заменой одного include на другой. Там, где раньше стоял маленький специализированный заголовок assembly_mesh.hpp, подтягивавший три конкретных файла, появился широкий umbrella-header assembly.hpp. Внутри него уже существовал обратный путь в source-заголовки. Цикл замкнулся.
Итог: семь файлов оказались намертво связаны в одну сильносвязную компоненту - core/specfem/assembly.hpp, core/specfem/assembly/assembly.hpp, include/source/interface.hpp и ещё четыре source-заголовка. Их теперь нельзя собирать, тестировать или переиспользовать по отдельности.
Почему это не видно глазами
Include-цикл - это не подозрительная строка и не забытая переменная. Это путь в графе. Человек, читающий diff, видит замену одного include на другой - и это выглядит как упрощение. Чтобы понять, что цикл появился, нужно держать в голове весь граф зависимостей проекта одновременно. Это невозможно.
C++ Core Guidelines прямо предписывают избегать таких ситуаций: правило SF.9 запрещает циклические зависимости между исходными файлами. John Lakos в своей классической работе по проектированию больших C++-систем объяснял это ещё детальнее: как только include graph становится цикличным, физические компоненты перестают быть независимыми. Любое изменение в одном месте тянет лишнюю пересборку по всей цепочке. Роберт Мартин называл это нарушением Acyclic Dependencies Principle.
Что делает archcheck и зачем он нужен
Именно для таких случаев был написан инструмент archcheck. Его главный вопрос - что этот PR сделал с архитектурой проекта? Не с отдельными строками, а со структурой целиком.
- обнаруживает новые и выросшие циклы в include-графе
- находит god-заголовки с аномально большим числом входящих зависимостей
- проверяет длину include-цепочек
- ловит using namespace в заголовочных файлах
- сравнивает граф с базовой версией и показывает, что именно изменилось
Инструмент работает без compile_commands.json - сканирует .h и .cpp напрямую. В режиме --diff он фиксирует структурный дельта между ветками.
Агентский код и новая роль инженера
За этим кейсом стоит более широкий сдвиг. С распространением агентской разработки PR стал почти бесплатным для исполнителя. Код генерируется пачками, объём изменений растёт быстрее, чем человек способен их осмыслить. Ручное code review в старом формате не масштабируется. Инженер устаёт, выгорает, начинает пропускать именно структурные проблемы - те, что не видны в одном экране diff.
Выход - сместить роль человека на этап до генерации: задать контекст, ограничения, архитектурные правила. А всё, что всё равно пролезло, ловить на выходе детерминированными проверками в CI. Не памятью автора, а инструментом, который повторяет одни и те же проверки на каждом PR без усталости и без исключений.