Как Enola строит модель архитектуры из исходного кода: не просто граф зависимостей
Разбираемся, почему инструмент Enola не просто строит граф зависимостей, а создаёт точную модель архитектурных фактов из исходного кода.
Если каждое отношение превращается в просто связь, граф становится проходимым, но уже ненадёжным для анализа архитектуры.
Когда разработчик приходит в большой или незнакомый проект, первым делом хочет понять, как всё устроено. Какие сервисы общаются между собой? Какие маршруты ведут к каким обработчикам? Какие модули зависят друг от друга? Автоматические инструменты обычно дают ответ в виде графа — узлы и рёбра, показывающие связи. Но такая картина часто бывает слишком общей и мутной, чтобы по ней можно было принимать решения. Проект Enola делает попытку это исправить: он извлекает из кода не просто связи, а осмысленные, типизированные факты об архитектуре.
Сначала синтаксис, потом — смысл
Любой парсер исходного кода может сказать нам, что в файле есть вызов функции, строковый литерал, импорт пакета или объявление метода. Это сырые данные, необходимое, но не достаточное условие для понимания архитектуры.
Возьмём пример на языке Go. Допустим, у нас есть код, который регистрирует маршруты для HTTP-сервера:
api := router.PathPrefix("/api").Subrouter()
registerCourseRoutes(api)
А в другом месте:
func registerCourseRoutes(router *mux.Router) {
router.HandleFunc("/courses", listCourses)
}
Парсер увидит и строковые литералы, и вызовы функций. Но настоящий архитектурный факт — это не сырые данные, а их смысл: «GET-запрос на /api/courses обрабатывается функцией listCourses». Чтобы прийти к такому выводу, инструмент должен понимать, как работает фреймворк (в данном случае, вероятно, gorilla/mux), как маршруты компонуются из префиксов и как значения передаются между функциями.
Та же задача стоит и при работе с аннотациями в Spring, конвенциями файлов в Next.js или системами внедрения зависимостей. Поэтому в Enola процесс разделён на два этапа: парсинг (который даёт синтаксис) и архитектурное извлечение (которое интерпретирует этот синтаксис в контексте конкретной архитектуры и фреймворка).
Репозиторий — это не вся картина
Удобно начинать анализ с отдельного Git-репозитория. Он даёт нам ревизию кода, конфигурацию, файлы и стабильные пути к исходникам. Однако граница репозитория редко совпадает с границей архитектуры.
В монорепозитории может жить несколько независимо деплоящихся сервисов. Маленький репозиторий может зависеть от схем, инфраструктуры или сгенерированных клиентов, которые живут в другом месте. Поведение приложения в рантайме может определяться конфигурацией деплоя, которой нет в репозитории с кодом.
Поэтому Enola рассматривает каждый репозиторий как самостоятельную «область извлечения». Внутри неё инструмент может установить факты: «репозиторий содержит модуль», «модуль содержит файл», «файл объявляет символ», «функция вызывает функцию», «пакет импортирует пакет», «маршрут обрабатывается символом» и так далее.
Эти факты полезны сами по себе, даже если другие репозитории не загружены. Область извлечения даёт каждой сущности локальную идентичность и происхождение. Но она не подразумевает, что вся архитектура системы находится внутри этого репозитория. Это важное различие: границы владения кодом и границы архитектуры — это чаще всего разные вещи.
Идентичность: имя — не гарантия
Для разных архитектурных понятий нужны разные правила идентификации. Нет универсального идентификатора, который подошёл бы для всего.
Для символа (например, функции или класса) может потребоваться комбинация из области извлечения, языка, полного имени, местоположения в исходном коде и его ревизии. Для HTTP-маршрута — метод (GET, POST), нормализованный путь и контекст сервиса. Для метода gRPC — пакет, сервис и имя метода. Для топика Kafka — разрешённое имя топика и контекст окружения.
Поэтому одних только имён недостаточно. Два репозитория могут оба содержать класс UserDTO, но это вовсе не обязательно один и тот же контракт. И наоборот, тип PublicUser на Go и тип ProfileResponse на Swift могут быть двумя сторонами одного и того же API. Enola разделяет такие сущности до тех пор, пока не появится доказательство их связи.
Типизированные связи: почему они важны
В обычном графе зависимостей мы видим просто «A -> B». Но для архитектурного анализа критично знать, почему эта связь существует. Сравните:
CheckoutControllerвызываетPaymentService.authorize.- Модуль
checkoutимпортирует модульpayments. - Маршрут
POST /checkoutобрабатываетсяCheckoutController.
Эти три связи отвечают на совершенно разные вопросы. Связь вызова полезна для анализа достижимости кода. Связь импорта — для поиска циклических зависимостей между пакетами. Связь маршрут-обработчик — для трассировки выполнения запроса.
Направленность тоже имеет значение. Клиент использует маршрут, а не наоборот. Поэтому в Enola связи представлены как типизированные и направленные факты, а не как просто «линии» между «точками».
Но и этого мало. Каждая связь должна сохранять доказательства своего существования: исходные локации, метод извлечения, способ разрешения, ревизию кода, конфигурацию, а также информацию о том, была ли связь извлечена напрямую или выведена. Без этих доказательств граф превращается в очередной «чёрный ящик», который даёт ответ, но не может объяснить, как к нему пришёл.
Одна модель — много проекций
Полная модель фактов содержит слишком много связей, чтобы любой анализ мог и должен был их все обрабатывать. Задача определяет, какое подмножество действительно релевантно.
Для обнаружения циклических зависимостей между пакетами нужны узлы-пакеты и рёбра-зависимости между ними. Для анализа достижимости символов нужны символы и связи вызовов/ссылок. Для трассировки выполнения запроса нужны маршруты, обработчики, сервисы и связи между ними.
Использование всех доступных рёбер одновременно может привести к путям, которые технически существуют в графе, но архитектурно бессмысленны. Например, тип принадлежит пакету, пакет зависит от другого пакета, а в том пакете есть HTTP-маршрут. Путь в графе есть. Но это вовсе не означает, что тип участвует в обработке того маршрута. Поэтому Enola строит ограниченные проекции для конкретных задач, а не считает все обходы графа равнозначными.
Конкретный пример: почему простой граф не работает
Представим монорепозиторий с фронтендом и бэкендом. Фронтенд зависит от пакета api-client, бэкенд — от фреймворка роутинга. Обычный граф зависимостей честно покажет обе связи.
Но он не сможет ответить на вопрос: «Какой метод фронтенда отправляет POST /api/orders?». Для ответа нужны дополнительные факты: метод submitOrder делает запрос на POST /api/orders, этот маршрут обрабатывается CreateOrderHandler, а тот, в свою очередь, вызывает OrderService.Create.
Полезный архитектурный путь здесь — не путь через зависимости пакетов, а проекция, объединяющая запрос, маршрут, обработчик и вызовы. Именно поэтому Enola нужна типизированная модель фактов, а не просто граф зависимостей репозиториев.
Пределы модели
Enola строит ту архитектуру, которую можно установить из загруженных исходников и конфигурации. Инструмент не претендует на то, что исходный код описывает каждый аспект продакшн-системы. Манифесты деплоя, API-шлюзы, сервисные меши, конфигурация рантайма, рефлексия, фича-флаги и инфраструктура могут менять архитектуру, видимую в работающей системе.
Поэтому модель должна различать установленные факты, выведенные связи, неразрешённые связи и информацию, находящуюся за пределами анализируемой области. Это важно и для разработчиков, и для AI-агентов: отсутствие связи в графе не всегда означает, что её нет в системе. Возможно, нужный исходник, конфигурация или «резолвер» просто не были доступны.
Что дальше
Типизированная модель фактов позволяет описать архитектуру внутри одной области извлечения. Но продакшн-системы пересекают эти границы: мобильный клиент зовёт бэкенд из другого репозитория, сервис публикует событие, которое потребляет репозиторий из соседней команды, сгенерированный клиент реализует контракт из отдельного кодового дерева.
Enola — инструмент с открытым исходным кодом, и его разработчики обещают в следующей части цикла объяснить, как их инструмент соединяет такие независимо извлечённые модели, не сливая сущности только на основе совпадения имён или сходства. Для крупных, разнородных проектов это может стать ключевым шагом к тому, чтобы архитектурный анализ наконец-то начал отражать реальность, а не её упрощённую тень.