41 034 метода, 2 170 файлов и один Миша: как я перестал быть единственным источником знаний о коде

12 093 метода из 19 880 в моём проекте — ни одного комментария. Покрытие документацией — 39%.

Это не чужой легаси-код. Это мой собственный проект, который я пишу прямо сейчас. Я направил свой инструмент на самого себя и увидел то, от чего у любого техлида дёргается глаз: кодовая база растёт быстрее, чем документация. 1183 файла на Python — без учета тестов. 480 коммитов за полгода. 21 аналитический сценарий, которые я наваял за это время. А документация покрывает меньше половины.

Новый разработчик приходит в проект, открывает src/workflow/scenarios/audit_composite.py — а там 6318 строк, 114 вызовов в другие модули, цикломатическая сложность 80. И задаёт первый вопрос: «Кто мне объяснит, как это работает?»

Раньше ответ был: «Спроси Мишу». Теперь — запрос к графу свойств кода. И я наконец-то могу заниматься своей работой, а не быть живой документацией.


Но для начала давайте представлюсь. Привет! Меня зовут Михаил, и я здесь, чтобы рассказать о своём проекте — я делаю платформу для ИИ-дополненного анализа исходного кода. Поскольку это не статья для блога «Я пиарюсь», рассказывать буду не про то, как всё замечательно работает, а про реальный опыт и эксперимент.

Мотивация родилась летом 2025 года. В какой-то момент я осознал: SOTA-модели уже пишут вполне осмысленный код даже на таких экзотических языках, как Erlang или Elixir. И тут встал вопрос — куда направить всю эту мощь? Что можно попробовать сделать из того, что раньше казалось неразрешимой задачей?

Мне всегда нравился код. Написать собственный язык программирования — детская нереализованная мечта. А что, если попробовать объединить технологии продвинутого статического анализа (вроде joern) с возможностями ИИ? Позволит ли это ИИ понимать и поддерживать системы такой сложности, как исходный код PostgreSQL? И второй вызов: можно ли в одиночку создать систему корпоративного уровня? То, на что раньше уходили человеко-годы сильнейших команд.

С этими вопросами я и взялся за pet-проект.


Почему документация не спасает

Симптомы, которые я увидел на себе

Когда я запустил самоаудит (41 034 метода, 2 170 файлов, общая оценка качества 4.7 из 10), картина вырисовалась до боли знакомая:

Проблема

Мои данные

Что это значит на деле

Покрытие документацией

39% (7 787 из 19 880 методов)

12 093 метода — и ни одного комментария. Вообще.

Нерешённые задачи в коде

268 комментариев TODO/FIXME

Документация обещает одно, код делает другое, а TODO висят годами.

Сложность без тестов

753 метода со сложностью > 10 без тестов

Самые опасные места — они же самые непонятные. И никто не рефакторит, потому что страшно.

Файлы-монолиты

8 файлов длиннее 1200 строк

audit_composite.py — 6318 строк в одном файле. Как вам такое?

Сильная связанность

модуль workflow — 274 вызова в другие модули

Тронешь одну строчку — и по цепочке посыплется полсотни файлов.

Корневая причина

У меня есть CLAUDE.md на 600+ строк, есть README, есть архитектурные заметки в вики. Но даже это покрывает от силы процентов двадцать вопросов, которые реально задаёт новый разработчик.

Потому что большинство вопросов — не про «зачем», а про «как»:

  • Кто вызывает execute_agent_safely? (Спойлер: никто — она мёртвая, я её удалил, когда проверял инструмент на себе.)

  • Что произойдёт, если я изменю CPGQueryService? (Ответ: затронет 11 модулей-примесей, из которых он собран, и все 21 сценарий.)

  • Как данные текут от API-запроса до DuckDB? (Через 4 слоя: маршрутизатор, сервис, оркестратор, хранилище.)

Эти ответы живут не в документации, а в структуре кода. Инструмент, который видит эту структуру — граф свойств кода — отвечает на них за миллисекунды.

Что такое граф свойств кода (и при чём тут Roslyn)

Граф свойств кода (Code Property Graph, CPG) — это если взять синтаксическое дерево (как код написан), добавить к нему граф потока управления (в каком порядке всё выполняется), граф зависимостей по данным (как значения переменных прыгают между функциями) и граф вызовов (кто кого дёргает). И склеить это в одну большую базу данных.

Если вы работали с Roslyn в .NET, вы поймёте: там есть синтаксическое дерево и семантическая модель. CPG делает то же самое, но для 11 языков сразу, и по нему можно ходить SQL-запросами. А когда всё склеено, анализатор способен ответить на вопрос: «Есть ли путь от пользовательского ввода до SQL-запроса, который не проходит через функцию очистки?» Это taint-анализ, и синтаксическому дереву такое не под силу.

Четыре запроса, которые выкинули меня из онбординга

Запрос 1: «Кто вызывает эту функцию?»

Новичок видит aggregate_partial_results() в src/workflow/error_handling.py и хочет понять, нужна ли она вообще.

Без графа: Ctrl+Shift+F по всему проекту → 12 совпадений → половина в тестах, половина — импорты без реальных вызовов. 20 минут мучений, чтобы понять контекст.

С графом: Запрос через ACP-сервер (Agent Client Protocol — штука, которая даёт IDE прямой доступ к графу):

codegraph_find_callers("aggregate_partial_results")

→ Вызывается из:
  → security_workflow() [security/main_workflow.py:87]
  → performance_workflow() [performance.py:112]
  → architecture_workflow() [architecture.py:93]
  → и ещё из 3 композитных сценариев через invoker

Граф вызовов: Композитный оркестратор → параллельный запуск 9 подсценариев
             → aggregate_partial_results() → AggregatedResults

20 минут → 3 секунды. И новичок видит не список файлов, а контекст: функция агрегирует результаты параллельных подсценариев в композитных потоках работ.

Запрос 2: «Что затронет моё изменение?»

Новичку поручили добавить поле в AgentResult — базовую структуру для результатов агентов.

Без графа: Поиск по имени → ручной обход вызовов → пропущена сериализация в JSON → через неделю API падает с неожиданной ошибкой.

С графом:

codegraph_impact("AgentResult")

→ Прямые зависимости (7):
  → execute_agent_safely() — создаёт AgentResult
  → aggregate_partial_results() — собирает в список
  → WorkflowErrorHandler — хранит .result
  → 3 сценария потоков работ через error_handling
  → to_dict() — сериализация в JSON (обновить обязательно!)

→ Транзитивные зависимости (12):
  → security_workflow → API-обработчик → JSON-ответ клиенту
  → audit_composite → _collect_metrics → отчёт аудита

Новичок видит полный радиус поражения до того, как написал первую строку кода. И сразу замечает, что нужно обновить to_dict() — иначе новое поле просто не попадёт в API.

Запрос 3: «Как устроена подсистема аудита?»

Вместо «спроси Мишу» — запрос к графу:

codegraph_explain("audit_composite")

→ Модуль: src/workflow/scenarios/audit_composite.py
  6318 строк, сложность = 80, исходящих вызовов = 114

  Точки входа:
  → python -m src.cli audit --db PATH
  → API: POST /api/v1/audit

  Ключевые компоненты:
  → AuditRunner — оркестратор (запускает 9 подсценариев параллельно)
  → _collect_metrics() — собирает 50+ метрик из CPG
  → _build_method_filter_sql() — каскад из 5 фильтров для отсева ложных срабатываний
  → _generate_section_summary() — формирует отчёт по 12 измерениям

  Внешние зависимости:
  → CPGQueryService (собран из 11 модулей-примесей)
  → CompositionInvoker (параллельный запуск подсценариев)
  → Сценарии 02, 03, 05, 06, 07, 08, 11, 12, 16

  Метрики:
  → Цикломатическая сложность: 80 (главный метод _collect_metrics)
  → Связность: 114 исходящих вызовов (это рекорд проекта)
  → Последнее изменение: 3 дня назад

Новичок получает структурированное понимание самого сложного модуля за 30 секунд. Без меня. Без документации, которая всё равно не описывает связи между 9 подсценариями и пятиуровневый каскад фильтрации.

Запрос 4: «Покажи горячие точки — самые рискованные места»

codegraph_hotspots(metric="complexity")

→ Горячие точки (CC > 50):
  1. _security_incident_workflow  CC=133  [security/incident.py]
  2. architecture_workflow         CC=109  [architecture.py]
  3. _collect_metrics             CC=80   [audit_composite.py]
  4. performance_workflow          CC=73   [performance.py]
  5. compliance_workflow           CC=71   [compliance.py]

→ Рекомендация: 12 методов с CC > 50 — 0.06% кодовой базы,
  но 47% от общей сложности. Рефакторить в первую очередь: incident.py

Новичок сразу видит, куда лучше не соваться без подготовки — и где рефакторинг принесёт наибольшую пользу.

Зачем мне понадобилось два десятка сценариев

Когда я начинал, у меня была только базовая идея: построить граф кода и уметь по нему искать. Но чем дальше я погружался, тем больше понимал, что разные задачи требуют разных ответов.

Я начал с простого: «кто вызывает функцию» — и сделал первый сценарий для навигации. Потом добавил анализ воздействия изменений, потому что сам устал бояться рефакторинга. Потом безопасность — захотелось увидеть, откуда и куда текут данные. Потом метрики качества, горячие точки, поиск мёртвого кода.

В итоге набрался 21 сценарий. Каждый родился из конкретной боли, с которой я столкнулся в своём коде:

  • Навигация и онбординг — чтобы новые люди (и я сам через месяц) могли разобраться, что к чему.

  • Безопасность — чтобы не пропустить SQL-инъекции, когда данных много.

  • Архитектура и зависимости — чтобы распутывать циклы и следить за слоями.

  • Рефакторинг — чтобы находить мёртвый код и дубликаты.

  • Производительность — чтобы знать, где код тормозит из-за сложности.

  • Ревью кода — чтобы видеть, что сломает мой коммит.

  • Аудит — чтобы одним запуском получать общую картину здоровья проекта.

Ничего из этого я не планировал изначально как «функции продукта». Просто каждая задача возникала, и я добавлял возможность ответить на неё через граф. Иногда это был отдельный CLI-запрос, иногда — композитный сценарий, который запускал несколько проверок параллельно. Например, сценарий аудита сейчас дёргает 9 подсценариев одновременно и выдаёт отчёт по 12 метрикам за 17 минут на моей базе из 40 тысяч методов (с учетом тестов).

Как это работает

Индексация

Граф строится автоматически из исходников. Никаких аннотаций, документации или ручной настройки.

# Построить граф
cpg parse --input=/path/to/your/project 
          --output=data/projects/myproject.duckdb 
          --lang=python --include-tests

# Задать вопрос
python -m cli query "Кто вызывает функцию process_payment?"

Под капотом:

  • 11 языков: C, C++, Go, Python, JavaScript, TypeScript, Java, Kotlin, C#, PHP, 1С

  • 33 аналитических прохода (типы, поток управления, потоки данных, вызовы, метрики)

  • Инкрементальное обновление при каждом коммите — секунды, а не полная пересборка

Четыре способа задать вопрос

Я сделал несколько интерфейсов, потому что сам пользуюсь по-разному:

Способ

Для чего

Пример

CLI

Быстро в терминале

cpg query "Кто вызывает X?"

ACP-сервер

Чтобы модель в IDE знала структуру

codegraph_find_callers("X")

REST API

Для автоматизации в CI

POST /api/v1/query

TUI

Интерактивный режим

/cpg query "SELECT ..."

ACP-сервер (Agent Client Protocol) даёт языковой модели внутри IDE прямой доступ к графу. Вместо того чтобы модель гадала структуру проекта по открытым файлам, она просто спрашивает граф. У меня уже 44 таких инструмента: поиск вызывающих/вызываемых, анализ воздействия, горячие точки, объяснение модулей, поиск по шаблонам, анализ потоков данных, диаграммы зависимостей, автогенерация фиксов.

Что граф заменяет, а что — нет

Граф свойств кода не заменяет документацию. Он заменяет ту часть знаний, которая никогда в документацию не попадает:

Вопрос

Источник ответа

«Почему я использую DuckDB, а не PostgreSQL для хранения графа?»

Документация, ADR

«Кто вызывает _collect_metrics и какие данные передаёт?»

Граф

«Какова бизнес-логика расчёта скидок?»

Документация, вики

«Что затронет добавление поля в AgentResult

Граф

«Почему я выбрал LangGraph для оркестрации?»

Документация, ADR

«Какой метод — горячая точка со сложностью 133 и без тестов?»

Граф

Документация отвечает на «зачем». Граф — на «как», «что», «кто» и «сколько». И граф, в отличие от документации, всегда актуален — он строится из кода, а не из благих намерений.

Ответы на «кто вызывает?», «что затронет?», «сколько связей?» — это чистая математика: обход графа, SQL-агрегация, BFS по рёбрам вызовов. Для них не нужна языковая модель — нужны предвычисленные данные из 33 аналитических проходов.

LLM подключается на границе: когда нужно объяснить найденное человеческим языком, сравнить альтернативы или сформулировать рекомендацию.

Если вы знакомы с Roslyn: ISymbol.GetMembers() скажет, что метод существует и откуда вызывается — это структурный факт. А «зачем этот метод спроектирован именно так» — вопрос для человека или LLM.

Что я насчитал в собственном коде (немного стыдных цифр)

Всё, что я рассказал, я проверил на себе. Вот что показал аудит:

Метрика

Значение

Всего методов

41 034

Файлов

2 170

Покрытие документацией

39%

Средняя цикломатическая сложность

2.0

Максимальная сложность (CC)

133

Методов со сложностью > 10 без тестов

753

Методов длиннее 100 строк

495

Методов без единого вызова

35 847 → 0 (после того, как удалил реальный dead code и устранил FP)

Циклических зависимостей между модулями

2

Всего сценариев

21

Аналитических проходов

33

Правил структурного поиска

190

Общая оценка качества: 4.7 из 10. Не идеально, мягко говоря. Но я это знаю — потому что инструмент показал. И теперь у меня есть конкретный план: 12 методов со сложностью выше 50, 753 сложных метода без тестов, 268 нерешённых TODO.

Без графа свойств кода эти цифры были бы невидимы. С графом — это дорожная карта рефакторинга.

12 093 метода без документации в проекте из 19 880 — это не потому, что я ленивый. Это проблема масштаба: код растёт быстрее, чем я способен его документировать, даже при ИИ-разработке.

Граф свойств кода не решает проблему документации. Он решает проблему, ради которой документация и создавалась — доступ к знаниям о коде. Кто вызывает функцию? Что затронет изменение? Где горячие точки? Как устроена подсистема?

Эти вопросы новичок задаёт каждый день. Раньше ответ был — «спроси Мишу». Теперь — запрос к графу. И я наконец-то могу спокойно писать код.

Что дальше

Эта статья — вторая в серии. Первая была про то, почему CPG лучше SAST, там я разбирал безопасность и анализ потоков данных.

В следующей статье расскажу, как догфуддинг (когда я гонял инструмент на самом себе) помог отсеять 35 тысяч ложных срабатываний и превратить прототип во что-то более-менее стабильное.

Код первых экспериментов на основе которых родился проект открыт, ссылка в моём профиле на GitHub. Если хотите попробовать на своём коде или просто пообщаться на тему — пишите в комментарии или в личку. Мне правда интересно, сталкивались ли вы с похожими проблемами и как их решаете.

Автор: msvn

Источник

Оставить комментарий