Перейти к содержимому
RAGRAF

Архитектура: три файла SQLite и ноль инфраструктуры

Хранилища, конвейер индексации, конверт ответа, provenance, детерминированные идентификаторы и деградация — как устроен RAGRAF изнутри.

Модель в одном абзаце #

RAGRAF снимает снапшот корпуса в иммутабельную ревизию (rev-NNNN): граф свойств/AST, FTS5-лексика, 384-мерные векторы, схема БД из миграций и опциональные межпроцедурные вызовы из Joern — всё в трёх файлах SQLite внутри <project>/.ragraf/. Чтение идёт через фасад из 22 фиксированных операций (языка запросов нет), каждый ответ завёрнут в конверт (index_rev, capability, drift, provenance, limits, stale_warnings, freshness). Пока агент правит код, индекс не меняется: дельты копятся в append-only журнале расхождений, затронутые факты помечаются stale, а сторож контрольных сумм переиндексирует автоматически (или по reindex), архивируя журнал в отчёт «что изменилось в знаниях».

Восемь принципов #

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

#ПринципОснование
P1Индекс иммутабелен в пределах ревизииИначе факты разных моментов времени смешиваются в одном ответе — источник неверных выводов
P2Устаревание показывается, никогда не превращается в nullУстаревание, неотличимое от ошибки, хуже отсутствия индекса
P3Ребро без доказательства не живёт в графе как фактЗащита от «галлюцинаций связями» внутри самого индекса
P4Агенту не даётся язык запросов — только фиксированные операцииLLMxCPG: валидный CPGQL от базовых моделей — 19–132 из 1278
P5Граф — основной источник, вектор и лексика — дополнительныеВектор ловит поверхностное сходство и не видит межпроцедурные потоки
P6Свободный текст отделён от структурных фактовALIBI: инъекции в комментариях подавляют обнаружение >90%; формулировка сдвигает вердикты на 33,2%
P7Пилот крошечный, но сквозной (6–10 файлов)Отладка конвейера на 162 тыс. строк — потеря времени
P8Точность важнее полноты на первом этапеRepoformer: право не искать даёт до 70% ускорения

Три хранилища #

Разделение по жизненному циклу: граф и лексика большие и пересобираемые; контроль маленький и не удаляется никогда.

ФайлСодержимоеЖизненный цикл
graph.sqliterev, file (sha256, parse_status), node (kind/fqn/path/строки/props), edge (provenance, evidence_path/line, confidence, sources, edge_uid), text_chunk, cfg_edge/dfg_edge (CPG), db_table/column/fk, web_route, invalidated, coverageбольшой, пересобираемый
lexvec.sqliteft_chunk (FTS5: 4 токенизированные контент-колонки — их число является контрактом с весами bm25()), chunk_text (сырой текст — отдельная таблица намеренно), vec_chunk (vec0, FLOAT[384]), chunk_map, vec_metaбольшой, пересобираемый
control.sqliterev_history, drift_entry (append-only, dedupe_key UNIQUE), drift_archive, calibration, stale_edge, телеметрия usage_callмаленький, вечный

Почему chunk_text — отдельная таблица, а не колонка FTS: восьмая колонка молча сдвинула бы все веса bm25(), а sqlite3 исключает UNINDEXED-колонки из индекса — текст по номеру колонки не читается. Ответ должен отдавать фрагмент за один вызов (search --with-text), а адрес второго шага разрешаться без похода на диск (code --entry-ids).

Идентификаторы = содержимое #

Детерминированность — требование (НФТ-5), поэтому каждый идентификатор — хеш содержимого, никогда не время и не порядок обхода:

Text
node_id     = sha1(kind | fqn | path | start_line)      ← строковый компонент обязателен
edge_uid    = sha1(src | dst | kind | evidence_path | evidence_line)
chunk_id    = sha1(content_sha256 | chunk_kind | model) ← намеренно БЕЗ ревизии и пути
corpus_hash = sha1(отсортированный список (path, sha256))

Независимость chunk_id от ревизии — главная экономия пересборок: неизменённые чанки сохраняют эмбеддинги между ревизиями. Две сборки одной ревизии обязаны дать идентичные наборы идентификаторов (тест M1-T4). Никаких UUIDv4 и временных меток внутри идентификаторов.

Provenance: stated против derived #

Колонка provenance обязательна и принимает ровно два значения:

  • stated — ребро доказано анализатором: точная строка объявления/вызова, строка information_schema. confidence = 1.0.
  • derived — выведено эвристикой или статистикой (динамический вызов, совпадение имён, вывод из истории). confidence из калибруемой таблицы: 0.60 однозначная эвристика · 0.40 неоднозначная · 0.30 вывод из истории/эмбеддингов.

calls_heuristic — отдельный вид ребра: эвристики включаются в граф (иначе в динамическом PHP связей не найти вовсе), но никогда молча не смешиваются с доказанными — impact --mode hard возвращает только stated. Конфликты источников разрешаются приоритетом (information_schema > миграции > CPG > SCIP > tree-sitter) и записываются в coverage как conflict.* — никогда не глотаются.

Конвейер индексации #

Text
0. Проверка версий и доступности (Joern/JDK, PHP, tree-sitter) → capabilities
1. scan    — отбор файлов, исключения, sha256, языки          → corpus_hash
2. ast     — tree-sitter: границы символов, токены, вызовы, SQL-литералы
3. resolve — разрешение имён: точное → имя+файл → отказ
4. schema  — таблицы/колонки/FK из information_schema и migrations/*.sql
5. cpg     — Joern: межпроцедурные вызовы; отказ → degraded (индексация не падает)
6. lexvec  — FTS5 + эмбеддинги (переиспользование по chunk_sha256)
7. coverage, конфликты, дубли → coverage; инварианты → rev.state = 'ready'

Потоки загрузки графа и лексики независимы: граф не ждёт векторов. Важная честная деталь: в поставляемом коде AST-обход последовательный (документированный «параллельный разбор 8 ядер/16 потоков» — цель, а не факт; закрытие разрыва — в перспективах, R-PERF-1).

Конверт ответа #

Единый формат каждого ответа — требование ФТ-16: агент всегда видит, чему он доверяет.

JSON
{
  "op": "impact",
  "index_rev": "rev-0007",
  "capability": "full",
  "drift": { "files": 3, "symbols": 7, "entries": 4 },
  "provenance": "index",
  "limits": ["статический анализ; динамика не покрыта"],
  "stale_warnings": [
    { "path": "src/Pipeline/PipelineEngine.php",
      "reason": "modify после rev-0007",
      "suggest": "live_read" }
  ],
  "result": [ "…" ]
}

Значения provenance: index (только индекс) · live (только чтение файлов) · mixed (каждая запись помечена). stale_warnings заполняется автоматически для всех затронутых журналом путей; suggest обязателен. Как читать конверт — в справочнике операций.

Capability и деградация #

РежимУсловиеДоступноКонтракт
fullCPG построенавсё, включая slice—
degradedCPG отсутствует/не удаласьграф, символы, схема, маршруты, лексика, векторrev_history.joern_used=0; каждый конверт несёт capability: degraded; slice отказывает с кодом 3 и причиной

Нет модели эмбеддингов → лексический поиск работает, конверт сообщает. Нет свободного порта → сервер называет причину. Молчаливой деградации не бывает (src/ragraf/cpg.py:9-11).

Слои и процессы #

Компоненты — отдельные процессы, а не библиотеки внутри процесса агента:

СлойКомпонентыТехнологии
L5 интеграцияMCP-сервер (stdio, JSON-RPC, 23 инструмента ragraf_*), регламент AGENTS.md, skillPython
L4 фасад22 операции + конверт + drift-гейтsrc/ragraf/facade.py
L3 хранилищаgraph / lexvec / controlSQLite 3.50.4 + FTS5 + sqlite-vec
L2 загрузканормализация → схема → дедупликация → идемпотентная загрузка ревизии → инвариантыindexer.py, revisions.py
L1 экстракторыCPG (Joern+JDK) · символы (SCIP/PHPStan где доступны) · синтаксис (tree-sitter, 16 языков) · SQL/БД (миграции, information_schema) · текст (md/комментарии/docblocks)extract/, cpg.py, schema_db.py
L0 источникифайлы репозитория · миграции · документация—

Контракт производительности (НФТ-4) и реальность #

ОперацияЦельФакт
symbol / references / callers≤ 200 мс p95индексные выборки (fqn, src/dst); среднее 0,7 мс
impact (глубина 3)≤ 1 с p95рекурсивные CTE по edge
search (гибрид)≤ 1 с p95FTS5 + KNN-скан; до ~1 с — доминирует вычисление эмбеддинга запроса; кэша запросов пока нет (R-PERF-3)
slice≤ 2 с p95внутри Joern
Полная индексация пилота 162 тыс. строк≤ 15 минна 323-файловом корпусе замерено 71 с (из них CPG 64 с)
Реиндекс после правкиминутыпереиспользование эмбеддингов по chunk_sha256; CPG пересобирается целиком (инкрементального CPG в Joern не существует)

Операции схемного яруса практически бесплатны: измеренные средние schema 0,5 мс · symbol 0,7 мс · table_usage 0,9 мс · write_check 3,3 мс.

Безопасность и секреты #

  • Секреты исключаются, а не маскируются: файлы из secrets_exclude не попадают ни в text_chunk, ни в векторы, ни в логи (ФТ-39/40, тест M3-T2 по образцам).
  • db.dsn_from индексирует факт «этот символ ссылается на конфиг», без значений.
  • Телеметрия хранит sha1 аргумента + 60-символьный дайджест вместо полного текста запросов.
  • Панель администратора: только loopback, мутации POST+токен (secrets.compare_digest), записи проектных конфигов — с валидацией и .bak-копией. Известный зазор: GET /api/token выдаёт токен любому локальному клиенту — описан честно в статье о панели и SECURITY.md проекта.
  • shell=True отсутствует везде, кроме документированного запуска Joern .bat (ручное квотирование; усиление — R-SEC-4).

Откуда берутся числа #

Каждое «измерено» в документации указывает на артефакт: bench/results/*.json (опубликованы M2 rev-0012 и rev-0025), журнал обращений usage_call (по каждому корпусу), тайминги стадий в index_status. Оценки (токены ≈ символы/4, строки ≈ символы/45) помечены «оценка» везде, где появляются, — сами коэффициенты измерены на двух корпусах.


Дальше: свежесть и дрейф — механика, которая не даёт индексу врать →