Я поломался, поломался — и поломался на осколки. Признаю́: железные помощники Т9 действительно могут приносить пользу в разработке. Единственное, что мне не нравилось — то, что весь проект большой и хорошо натренированной модели не скормишь, а значит — неизбежны потери контекста, размывание смыслов и джойсовские галлюцинации.

Я уже давно понял: если мне нужно, чтобы что-то было сделано хорошо, — делегирование отпадает, придётся брать в руки молоток самому. Это касается любых жизненных аспектов: варки борща, замены сантехники, перевода Эдгара Аллана По или Антонио Мачадо на русский, или, там, программирования.

Когда БЯМ научились подключать сторонние MCP-сервера, произошел качественный скачок. Теперь не нужно файнтьюнить модель, можно файнтьюнить буковку «R» из акронима «RAG». Я-то лучше знаю, как правильно извлекать смыслы из моего личного контента. Если речь про код — лучше всего искать правду в AST.

Так и был зачат Ragex — MCP-сервер для семантического анализа кодовых баз с элементами чёрной магии. Проект, понятно, написан на Elixir, потому что ну а на чем еще?

Ragex — это (вроде, довольно успешная) попытка объединить статический анализ кода с векторными представлениями и графами знаний. В результате получается система, которая может ответить на вопросы типа «где у меня функция, которая парсит JSON?» не хуже, чем ваш коллега, который неделю назад это писал, но уже всё забыл.

Занахрена́?

Я задумывал всё это как retrieval engine для передачи в какой-нибудь клод, но, поскольку сам я ассистентами не пользуюсь, в результате получилось решение, подходящее и для подключения к БЯМ, и для помощи (если не замены) обычному LSP.

Три кита, на которых держится Ragex:

  1. Local-first: Никаких внешних API. Всё работает локально. Код не отправляется в облако на растерзание корпоративным серверам. Параноики оценят. Кроме того, использовать БЯМ для извлечения контекста из кода — глупо, когда у нас есть AST. Но простенький локальный семантический поиск я тоже, конечно, прикрутил.

  2. Гибридный поиск: Символьный анализ (AST) + семантический поиск (эмбеддинги) + графы знаний. Это как смотреть в тринокль: один окуляр видит близко, другой далеко, третий вообще смотрит в прошлое.

  3. Производительность: Запросы выполняются за разумное время, иногда — в ущерб качеству. Потому что жизнь коротка, а ждать результатов анализа кода — это издевательство и вообще прерогатива создателей IDE корпоративного масштаба.

Архитектура

Ragex состоит из нескольких слоёв, каждый из которых старается не испортить работу остальных:

┌─────────────────────────────────────┐
│   MCP Server (JSON-RPC 2.0)         │
│      stdio + Unix Socket            │
└──────────────┬──────────────────────┘
               │
       ┌───────┴────────┐
       ▼                ▼
┌─────────────┐  ┌──────────────┐
│ Анализаторы │  │ Graph Store  │
│ (AST)       │  │ (ETS)        │
└──────┬──────┘  └──────┬───────┘
       │                │
       │         ┌──────┴───────┐
       │         ▼              ▼
       │   ┌──────────┐  ┌─────────────┐
       └──►│  Vector  │  │  Bumblebee  │
           │  Store   │  │  (ML Model) │
           └──────────┘  └─────────────┘

Компоненты

1. MCP Server
Реализация Model Context Protocol — протокола, который позволяет AI-ассистентам общаться с внешними инструментами. JSON-RPC 2.0 через stdio (для интеграции) и Unix-сокет (для интерактивного использования). Люди, использующие ассистентов могут подключить этот MCP туда, я просто добавил его в свой LunarVim (см. ниже).

2. Анализаторы
Парсеры готовы для Elixir, Erlang, Python (частично), JavaScript/TypeScript (наброски), скоро добавлю раст и го, наверное. Каждый парсер извлекает AST, модули, функции, вызовы. Elixir и Erlang используют нативные парсеры, Python вызывает через порт в питоновский модуль ast, JavaScript использует регулярные выражения (потому что жизнь — боль).

3. Graph Store
Граф знаний на основе ETS (Erlang Term Storage). Узлы: модули, функции, вызовы. Рёбра: :calls, :imports, :defines. Поверх графа работают алгоритмы: PageRank, поиск путей, метрики центральности, детекция сообществ.

4. Embeddings & Vector Store
Локальная ML-модель (по умолчанию — sentence-transformers/all-MiniLM-L6-v2, настраивается в конфигах) через Bumblebee. Генерирует 384-мерные векторные представления для каждой функции и модуля. Косинусная близость для семантического поиска. Всё работает без интернета.

5. Hybrid Retrieval
Reciprocal Rank Fusion (RRF) — алгоритм объединения результатов символьного и семантического поиска. Три стратегии: fusion (RRF), semantic-first, graph-first.

6. Editor System
Безопасное редактирование кода с атомарными операциями, бэкапами, валидацией синтаксиса, форматированием. Поддержка multi-file транзакций и семантического рефакторинга через AST.

Зачем это вообще нужно?

1. Семантический поиск по кодовой базе

Проблема: Вы помните, что где-то была функция для работы с HTTP-запросами, но где именно — забыли. Название тоже не помните. Grep не поможет.

Решение: Семантический поиск, да.

# Подключаемся к Ragex
alias Ragex.VectorStore

# Ищем функцию
{:ok, results} = VectorStore.search("HTTP request handler", 
  limit: 5, 
  threshold: 0.7,
  node_type: :function
)

# Результаты отсортированы по релевантности
Enum.each(results, fn {node, similarity} ->
  IO.puts("#{node.name} (#{similarity})")
  IO.puts("  File: #{node.metadata.file}")
  IO.puts("  Line: #{node.metadata.line}")
end)

2. Анализ зависимостей и вызовов

Проблема: Нужн�� понять, откуда вызывается функция process_data/2, и что она сама вызывает. Рефакторинг без такой информации — русская рулетка (которая в запутанных случаях с макросами — до сих пор лучше удается^W удавалась эвристикам из навороченных IDE, чем БЯМ).

Решение: Граф вызовов.

alias Ragex.Graph.Store
alias Ragex.Graph.Algorithms

# Найти все функции, которые вызывают process_data/2
callers = Store.get_callers({:function, "MyModule.process_data/2"})

IO.puts("Callers:")
Enum.each(callers, fn caller ->
  IO.puts("  - #{caller.id}")
end)

# Найти все функции, которые вызывает process_data/2
callees = Store.get_callees({:function, "MyModule.process_data/2"})

IO.puts("\nCallees:")
Enum.each(callees, fn callee ->
  IO.puts("  - #{callee.id}")
end)

# Найти все пути между двумя функциями (с лимитом)
paths = Algorithms.find_all_paths(
  {:function, "MyModule.start/0"},
  {:function, "MyModule.process_data/2"},
  max_depth: 10,
  max_paths: 100
)

IO.puts("\nFound #{length(paths)} paths")

Защита от плотных графов: Если у узла >10 рёбер, Ragex предупредит о потенциальных проблемах с производительностью. Параметр max_paths предотвращает зависание на экспоненциальных взрывах. Не вижу проблемы дать этот параметр на откуп разработчику.

3. Поиск бутылочных горлышек в архитектуре

Проблема: Какие функции являются критичными для всей системы? Если они упадут — всё рухнет.

Решение: Betweenness Centrality (метрика центральности по посредничеству — если кто знает, как перевести менее коряво, свистните, пожалуйста).

alias Ragex.Graph.Algorithms

# Вычислить betweenness centrality для всех функций
scores = Algorithms.betweenness_centrality(
  max_nodes: 1000,
  normalize: true
)

# Отсортировать по убыванию
top_bottlenecks = scores
  |> Enum.sort_by(fn {_node, score} -> score end, :desc)
  |> Enum.take(10)

IO.puts("Top 10 bottleneck functions:")
Enum.each(top_bottlenecks, fn {node_id, score} ->
  IO.puts("  #{node_id}: #{Float.round(score, 4)}")
end)

4. Определение (извлечение) архитектурных модулей

Проблема: Код разросся, структура неочевидна. Хотелось бы понять, какие модули логически связаны и образуют кластеры.

Решение: Community Detection (алгоритм Louvain).

alias Ragex.Graph.Algorithms

# Найти сообщества (кластеры модулей)
communities = Algorithms.detect_communities(
  algorithm: :louvain,
  hierarchical: true,
  resolution: 1.0
)

IO.puts("Found #{map_size(communities)} communities")

# Группировка по сообществам
grouped = Enum.group_by(communities, fn {_node, community} -> community end)

Enum.each(grouped, fn {community_id, members} ->
  IO.puts("\nCommunity #{community_id} (#{length(members)} nodes):")
  Enum.each(members, fn {node_id, _} ->
    IO.puts("  - #{node_id}")
  end)
end)

5. Безопасный рефакторинг

Проблема: Нужно переименовать функцию old_function/2 в new_function/2 во всём проекте. Ручной рефакторинг — это ошибки и страдания.

Решение: Семантический рефакторинг через AST. Да, даже для питона.

alias Ragex.Editor.Refactor

# Переименовать функцию во всём проекте
result = Refactor.rename_function(
  :MyModule,
  :old_function,
  :new_function,
  2,  # arity
  scope: :project,
  validate: true,
  format: true
)

case result do
  {:ok, details} ->
    IO.puts("Success! Updated files:")
    Enum.each(details.edited_files, &IO.puts("  - #{&1}"))
    
  {:error, reason} ->
    IO.puts("Rollback performed. Error: #{inspect(reason)}")
end

Через граф знаний нашли все места, откуда вызывается функция, заменили AST по месту (то есть, работает даже для сгенерированного мюнхгаузен-кода), форматирует результат, валидирует синтаксис, и (!) атомарно применяет изменения (или откатывает всё при ошибке).

6. Multi-File транзакции

Проблема: Нужно одновременно изменить несколько файлов. Если хоть одно изменение невалидно — откатить всё.

Решение: Атомарные транзакции.

alias Ragex.Editor.{Transaction, Types}

# Создать транзакцию
txn = Transaction.new(validate: true, format: true)
  |> Transaction.add("lib/module_a.ex", [
      Types.replace(10, 15, "def new_version do\n  :ok\nend")
    ])
  |> Transaction.add("lib/module_b.ex", [
      Types.insert(20, "@doc \"Updated documentation\"")
    ])
  |> Transaction.add("test/module_test.exs", [
      Types.replace(5, 5, "# Updated test")
    ])

# Применить все изменения атомарно
case Transaction.commit(txn) do
  {:ok, result} ->
    IO.puts("Edited #{result.files_edited} files successfully")
    
  {:error, result} ->
    IO.puts("Transaction rolled back!")
    IO.puts("Errors: #{inspect(result.errors)}")
end

Тут важна атомарность: при любой ошибке — автоматический откат всех изменений, все бэкапы хранятся (в ~/.ragex/backups/<project_hash>/) и т. д.

7. Бонус-трек — экспорт в DOT и D3.js

Интеграция с LunarVim

LunarVim — это мой редактор кода, Neovim на стероидах. Ragex интегрируется через MCP и предоставляет команды для семантического поиска прямо из редактора.

Установка

Скопируйте конфигурационные файлы:

cp ragex/lvim.cfg/lua/user/*.lua ~/.config/lvim/lua/user/

Добавьте в ~/.config/lvim/config.lua:

-- Ragex integration
local ragex = require("user.ragex")
local ragex_telescope = require("user.ragex_telescope")

-- Setup
ragex.setup({
  ragex_path = vim.fn.expand("~/Proyectos/Ammotion/ragex"),
  enabled = true,
  debug = false,
})

-- Keybindings (using "r" prefix)
lvim.builtin.which_key.mappings["r"] = {
  name = "Ragex",
  s = { function() ragex_telescope.ragex_search() end, "Semantic Search" },
  w = { function() ragex_telescope.ragex_search_word() end, "Search Word" },
  f = { function() ragex_telescope.ragex_functions() end, "Find Functions" },
  m = { function() ragex_telescope.ragex_modules() end, "Find Modules" },
  a = { function() ragex.analyze_current_file() end, "Analyze File" },
  d = { function() ragex.analyze_directory(vim.fn.getcwd()) end, "Analyze Directory" },
  c = { function() ragex.show_callers() end, "Find Callers" },
  r = { function()
      vim.ui.input({ prompt = "New name: " }, function(name)
        if name then ragex.rename_function(name) end
      end)
    end, "Rename Function" },
  g = { function() ragex.graph_stats() end, "Graph Stats" },
  b = { function() ragex.show_betweenness_centrality() end, "Betweenness" },
  n = { function() ragex.show_communities("louvain") end, "Communities" },
  e = { function() ragex.export_graph("graphviz") end, "Export Graph" },
}

Использование

<leader>r
<leader>r

Пример Workflow

Типичный сценарий работы с Ragex в LunarVim:

1. Открыли проект: <leader>rd (анализировать директорию, по умолчанию — автоматом)
2. Нужно найти функцию: <leader>rs → "database connection"
3. Нашли функцию, открыли файл <Enter> в телескопе
4. Хотим узнать, кто вызывает: <leader>rc
5. Решили переименовать: <leader>rr → "connect_to_db"
6. Проверили статистику: <leader>rg
7. Экспортировали граф для визуализации: <leader>re

Настройка Auto-Analyze

Ragex может автоматически анализировать код при сохранении файлов:

ragex.setup({
  auto_analyze = true,
  auto_analyze_on_start = true,
  auto_analyze_dirs = { "/path/to/project" },
})

Инкрементальные обновления: Ragex отслеживает изменения через SHA256-хеширование и перегенерирует эмбеддинги только для изменённых файлов.

Память

На совсем жиденьких лэптопах я бы не стал пользоваться Ragex.

  • ML-модель: ~400 MB RAM

  • ETS-таблицы: линейный рост, ~400 bytes на узел

  • Эмбеддинги: ~400 bytes на вектор (384 float32)

  • Кеш-файлы: ~15 MB на 1000 сущностей

Настройка

Поддержка пользовательских Embedding-Моделей

Ragex поддерживает 4 предконфигурированных модели, но не для смены на лету:

# config/config.exs
config :ragex, :embedding_model, "sentence-transformers/all-MiniLM-L6-v2"

# Альтернативы:
# - "sentence-transformers/all-MiniLM-L12-v2" (больше точность)
# - "sentence-transformers/paraphrase-MiniLM-L3-v2" (быстрее)
# - "sentence-transformers/multi-qa-MiniLM-L6-cos-v1" (для Q&A)

Миграция моделей:

mix ragex.embeddings.migrate --from old_model --to new_model

File Watching

Автоматическое переиндексирование при изменении файлов:

# Через MCP
{"method": "tools/call", "params": {
  "name": "watch_directory",
  "arguments": {"path": "/project/lib"}
}}

# В коде
Ragex.FileWatcher.watch("/project/lib")

Экспорт Графа для Визуализации

alias Ragex.Graph.Algorithms

# Graphviz DOT format
{:ok, dot} = Algorithms.export_graphviz(
  color_by: :betweenness,
  include_communities: true
)
File.write!("graph.dot", dot)

# Рендерим через Graphviz
System.cmd("dot", ["-Tpng", "graph.dot", "-o", "graph.png"])

# D3.js JSON format (для веб-визуализации)
{:ok, json} = Algorithms.export_d3_json(include_communities: true)
File.write!("graph.json", json)

Ух, обожаю картинки и D3.js-диаграммки, которые можно пошевелить мышкой.

Ограничения и подводные камни

Потому что честность — моё третье «я»:

  1. JavaScript/TypeScript анализатор: Использует регулярные выражения. Работает для «простых» случаев. Когда-нибудь, может быть, руки дойдут.

  2. Семантический рефакторинг: Пока только для Elixir. Erlang/Python/JS в планах, но не сегодня.

  3. Плотные графы: Если у функции >100 вызовов, поиск путей может занять вечность. Используйте max_paths и max_depth.

  4. Память: 400 MB для модели — это цена локального ML. Если RAM критична, можно отключить эмбеддинги (но зачем тогда вообще Ragex?).

  5. Cold start: Первая генерация эмбеддингов занимает время. После этого — кеширование спасает.

В общем

Ragex — это попытка сделать анализ кода менее болезненным и более семантическим. Граф знаний + векторные эмбеддинги + алгоритмы на графах = инструмент, который может ответить на вопросы типа «где это используется?», «что это делает?», «почему всё сломалось?». А еще это MCP-сервер, который можно подключить к вашему ассистенту кода, чтобы тому тоже было проще ответить на эти вопросы.

MCP-протокол, потому что AI-ассистенты — будущее (или настоящее, в зависимости от того, насколько вы параноик).

Интеграция с LunarVim превращает рефакторинг в нечто почти приятное. Семантический поиск работает, граф не врёт, рефакторинг никогда не ломает код. В теории.


P. S. Если возникло желание попробовать, скачайте проект, скачайте зависимости, запустите ./start_mcp.sh и наслаждайтесь. Если что-то сломается — issue в GitHub. Если всё заработало — это, конечно, тоже можно написать в issue, но кто так делает?

P. P. S. Проект open-source, лицензия MIT. Делайте что хотите, на свой страх и риск. Автор не несёт ответственности за потерянное время, сломанный код и экзистенциальные кризисы, вызванные чтением чужих графов вызовов.

Репозиторий: https://github.com/am-kantox/ragex