Архитектура: три файла 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.sqlite | rev, 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.sqlite | ft_chunk (FTS5: 4 токенизированные контент-колонки — их число является контрактом с весами bm25()), chunk_text (сырой текст — отдельная таблица намеренно), vec_chunk (vec0, FLOAT[384]), chunk_map, vec_meta | большой, пересобираемый |
control.sqlite | rev_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), поэтому каждый идентификатор — хеш содержимого, никогда не время и не порядок обхода:
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.* — никогда не глотаются.
Конвейер индексации #
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: агент всегда видит, чему он доверяет.
{
"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 и деградация #
| Режим | Условие | Доступно | Контракт |
|---|---|---|---|
full | CPG построена | всё, включая slice | — |
degraded | CPG отсутствует/не удалась | граф, символы, схема, маршруты, лексика, вектор | rev_history.joern_used=0; каждый конверт несёт capability: degraded; slice отказывает с кодом 3 и причиной |
Нет модели эмбеддингов → лексический поиск работает, конверт сообщает. Нет свободного порта → сервер называет причину. Молчаливой деградации не бывает (src/ragraf/cpg.py:9-11).
Слои и процессы #
Компоненты — отдельные процессы, а не библиотеки внутри процесса агента:
| Слой | Компоненты | Технологии |
|---|---|---|
| L5 интеграция | MCP-сервер (stdio, JSON-RPC, 23 инструмента ragraf_*), регламент AGENTS.md, skill | Python |
| L4 фасад | 22 операции + конверт + drift-гейт | src/ragraf/facade.py |
| L3 хранилища | graph / lexvec / control | SQLite 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 с p95 | FTS5 + 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) помечены «оценка» везде, где появляются, — сами коэффициенты измерены на двух корпусах.
Дальше: свежесть и дрейф — механика, которая не даёт индексу врать →