# RAGRAF — мастер-план: графово-векторная память о коде для LLM-агентов

**Статус:** **v1.3 — актуализирован 2026-10-30.** Этапы 0–5 закрыты, этапы 7 и 9
закрыты 30.10.2026, публикация (этап 6) перенесена в конец решением владельца;
следующий — этап 8 (доказательность графа вызовов). Порядок и замеры — раздел
«Актуализация 2026-10-30» сразу ниже.
**Проект:** `RAGRAF`
**Дата:** 2026-10-04
**Автор:** AutoClaw

---

## Актуализация 2026-10-30: состояние и десять этапов (6–15)

> Блок «Актуализация 2026-10-29» ниже сохранён как история: он описывает состояние на ту
> дату. Здесь — замеры 30.10.2026 и план на десять этапов. Порядок этапов задан
> зависимостями, а не удобством: пока публикация не закрыта, любая правка кода уходит в
> никуда, а пока не приведено в порядок хранилище, замеры качества недостоверны.

### Б.1 Состояние по факту (замерено 30.10.2026)

| Что | Значение | Команда |
|---|---|---|
| версия | **0.11.0** (`src/ragraf/__init__.py:9`) | чтение файла |
| активная ревизия | **rev-0892**: 927 файлов / 5 796 узлов / 77 983 ребра, `capability=full` | `tools/rev_snapshot.py` |
| сверка ревизии с корпусом | 927 в записи = 927 в скане = 927 путей | `tools/check_corpus_revision.py` |
| воспроизводимость чисел | 2 прогона — 76 238 / 76 238; бюджет 120 и 600 с — одинаково | `tools/check_edges_repeatable.py` |
| документный слой | **264** документа, **15 431** чанк, p95 164 токена | `query doc_status` |
| хранилища | граф **912 МБ**, лексика+векторы **6 545 МБ**, управление **5,4 МБ** | размеры файлов |
| ревизии в истории | **777**, ограничения хранения нет | `control.rev_history` |
| реестр | **42** разработки, **34** проиндексированы | `catalog`, `registry_line.py` |
| проверки ядра | **54 файла / 1 477 проверок / 0 провалов** (1 568,5 с; два прогона подряд) | `check_all.py --group core` |
| чекер конвенций | **42** проверки / 0 ошибок / 0 предупреждений | `Invoke-ProjectDoctrine.ps1` |
| ссылки `файл:строка` | **215** ссылок в 21 документе, 0 неверных; 111 пар «утверждение → объект», 0 провалов | `check_line_refs.py` |
| переводы | 149 объектов, 0 нарушений | `check_translations.py` |
| классы инструментов | рабочих **29** (названы прямо 27), разовых **336** (объявлено 336) | `check_tools_classes.py` |
| гейт поставки | пройден: 5 порч из 5, 2 приманки из 2, дерево запускается | `release/gate_release.py` |
| поверхности | **62** операции фасада, **69** инструментов MCP, 23 подкоманды CLI, 29 языков деревом / 40 текстом, 84 формата | `tools/surface_counts.py` |
| временные песочницы | **3 854** каталога, **17,70 ГБ**; старше суток 3 264 / 6,15 ГБ | инвентарь (только чтение) |
| настройки | 4 ключа владельца, умолчания не записаны | `catalog/settings.json` |
| `.git` в рабочем каталоге | отсутствует | `Test-Path` |

### Б.2 Что изменилось с 29.10.2026

Закрыты дефекты, найденные замерами, — каждый с проверкой, которая ловит возврат:

1. **Число файлов ревизии.** Считалось длиной списка строк, а ключ таблицы `file` —
   `(rev_id, path)`: `.html` в режиме `document` давал две строки на файл, и ревизия
   называла 912 и 914 файлов при фактических 908/910. Теперь `COUNT(DISTINCT path)`
   (`src/ragraf/revisions.py`, `count_files`), проверка — `tools/check_corpus_revision.py`
   и правило чекера `Test-CorpusRevisionAgreement`.
2. **Активная ревизия.** Выбор был размазан по четырём читателям и не учитывал состояние:
   запись `building`/`failed` с бо́льшим номером называлась активной и показывала нули.
   Теперь один выбор — `store.active_revision` (`src/ragraf/store.py:174`), пропущенное
   называется в ответе (`skipped_unsealed`), проверка — `tests/test_active_revision.py`
   (22 свойства, подсадка ловится шестью провалами).
3. **Следы прогонов.** Проверяльщик, проверка манифеста и гейт поставки оставляли свои
   песочницы (2 305 + 437 + 58 каталогов), раннер не убирал следы наборов, а тест архивов
   писал настройки в живой файл и не мог убрать каталог из-за открытых SQLite-соединений.
   Теперь уборка в `finally`/`atexit`, раннер убирает появившееся за прогон
   (`--keep-sandboxes` оставляет), `_drop_tmp` закрывает соединения и называет остаток.
   Проверки: 0 новых каталогов после прогона, `ragraf-gate-*` не растёт (58).

### Б.3 Десять этапов (6–15): предмет, основание, критерий

| № | Этап | Зачем (измеренное основание) | Критерий приёмки |
|---|---|---|---|
| **7** ✅ закрыт 30.10.2026 | **Прогон не меняет живое состояние** | путь `catalog/` собирался в семи модулях от каталога установки, поэтому прогон писал в живой `catalog/`; плавающий провал `test_ensure` (3/17) не был воспроизведён | путь состояния выбирает `paths` (`src/ragraf/paths.py:106`); живой `catalog/*` не меняется; прогон ядра два раза подряд; раннер называет набор, изменивший состояние — см. Б.7 |
| 8 | **Доказательность графа вызовов** | из 76 238 рёбер **57 269** — `calls_heuristic` (75 %), подтверждённых `calls` — 11 364; агент не может отличить факт от догадки по числу | доля `calls_heuristic` названа в ответе операции и в отчёте; бюджет ложных связей задан настройкой; на контрольном наборе вопросов точность `impact` измерена до/после |
| 9 | **Хранилище: объём, ротация, восстановление** | лексика+векторы **6,5 ГБ** при 912 файлах, граф 912 МБ, 777 ревизий, ограничения хранения нет; рост за сутки виден | задана политика хранения (сколько ревизий, что компактится); размер после компакции измерен; восстановление из копии проверено на живом корпусе |
| 10 | **Документный слой 2.0** | 264 документа, 15 431 чанк; OCR 74,3 % при разбросе 21–95 %; каталог `PAULMANN.RU` — **10 188 PDF** без замера масштаба; дедупликация документов выключена по умолчанию | замер 10 188 PDF (время, объём, доля читаемых); OCR ≥ 90 % на контрольной странице; решение по дедупликации с числами |
| 11 | **Полнота разбора языков** | 29 языков деревом, но PowerShell: 53 файла из 96 разбираются целиком, 43 — частично; SQL/batch/INI сознательно текстом | доля полного разбора по каждому языку измерена и названа; для PowerShell — причина каждого частичного разбора и план |
| 12 | **Скорость индексации** | полный прогон 912 файлов — **133 с**; GPU-порог 1451 текст не достигается; стадии не профилированы по стоимости | профиль стадий с числами; быстрая переиндексация после правки одного файла ≤ 30 с; решение по GPU обосновано замером, а не ожиданием |
| 13 | **Польза для агента: доверие и метрики** | контракт (`index_rev`, `capability`, `drift`, `provenance`, `stale_warnings`) есть, но полезность не измерена на задачах; `usage`/`task_stats` собирают числа, а не эффект | на 10 реальных вопросах по коду: попадание в файл с первого запроса, доля фактов со ссылкой, число прочитанных файлов до и после |
| 14 | **Безопасность и границы** | секреты вынесены в `catalog/` (вне корпуса), включён захват встреченного (`secrets_capture_enabled`), но аудит выдачи и права MCP не проверены на живом | проверка: ни один секрет не попадает в ответ операции; `secrets audit` показывает каждую выдачу; права MCP ограничены и проверены отказом |
| 15 | **Издания и установка с нуля** | 15 возможностей изданий, три издания, лицензии; установка на чистой машине не проверялась, обновление — тоже | установка на чистой машине по документу: 0 ручных шагов; обновление с 0.5.0 на 0.6.0 без потери индекса; лицензионный гейт отказывает Community на Pro-операции |
| **6** — **последним** | **Публикация и проверка из клона** | `.git` в рабочем каталоге нет; тег и CI зависят от коммита владельца, поэтому публикация перенесена в конец (решение владельца 30.10.2026) | тег на коммите владельца; из клона `--version` совпадает с кодом, `check_release_runs.py` 14/0, CI зелёный на windows и ubuntu |

### Б.4 Порядок и зависимости

```
7 живое состояние ──► 9 хранилище ──► 8 граф вызовов
                                  ├──► 10 документы
                                  └──► 11 языки
8, 10, 11 ──► 12 скорость ──► 13 польза ──► 14 безопасность ──► 15 издания ──► 6 публикация
```

Почему так: **6** закрывает разрыв «здесь проверено — в клоне нет» и делает остальное
публикуемым; **7** убирает искажения, из-за которых замеры 8–12 могут врать (прогон,
меняющий настройки, меняет и состав документного слоя); **9** идёт раньше качества, потому
что 6,5 ГБ лексики при 912 файлах — это предел, за которым замеры становятся дорогими;
**8, 10, 11** — три независимые ветки качества, каждая со своим замером; **12** имеет смысл
после них, иначе оптимизируется неизвестно что; **13** измеряет пользу на живых вопросах;
**14** и **15** — перед публикацией следующего выпуска.

### Б.5 Что остаётся решением владельца

| Вопрос | Состояние |
|---|---|
| Публикация 0.5.0: коммит и тег в клоне | не сделано: `.git` в этом каталоге нет |
| Уборка 3 854 песочниц (17,70 ГБ; старше суток 3 264 / 6,15 ГБ) | не выполнена: удаление необратимо, согласие не получено |
| Запись реестра `sagaai-platform` без каталога (чужой проект) | не тронута: состояние владельца, не мусор прогона |
| 10 188 PDF каталога `PAULMANN.RU` | замер масштаба не делался (этап 10) |
| `connectors`, `fleet` в реестре изданий | сняты 26.10.2026; возврат только с контрактом запуска |
| Плавающий провал `test_ensure.py` | причина не установлена; раннер теперь печатает хвост вывода провалившегося набора |

### Б.6 Порядок после решения владельца (30.10.2026)

**Решение владельца:** публикация (этап 6) уезжает на **последнее** место; работа идёт
по очереди 7 → 9 → 8 → 10 → 11 → 12 → 13 → 14 → 15 → 6. После закрытия каждого этапа
выдаётся проработанный промт на следующий — чтобы контекст не терялся между сессиями.

Почему публикация последней не ломает остальное: этапы 7–15 меняют код, а публикация —
это коммит и тег уже готового дерева. Пока публикация не сделана, каждая правка кода
остаётся локальной, и это дешевле, чем публиковать восемь раз подряд.

### Б.7 Этап 7 — закрыт 30.10.2026

**Что было.** Путь к `catalog/` собирался независимо в семи модулях от каталога установки.
Поэтому прогон проверок писал в живой `catalog/settings.json` и в живой реестр
разработок — то есть менял состояние, на котором сам же и проверяет. Плавающий провал
`test_ensure` (3 из 17 при одиночном 17/0) месяц числился «причина не установлена».

**Причина провала — воспроизведена, а не предположена.** Проверка переиспользовала общий
каталог `~/_ragraf_ensure_probe`: остаток прошлого прогона даёт ответ `actual` вместо
`created`, ветка `created` не исполняется, и **12 из 17 проверок не запускаются**
(красная проба 30.10.2026: `пройдено: 3, провалено: 2`). После правки та же проба с тем
же остатком даёт `20 из 20`.

**Что сделано.**

1. `src/ragraf/paths.py` — единственный выбор пути состояния: `RAGRAF_CATALOG_DIR`
   отводит весь каталог, `RAGRAF_SETTINGS_PATH` — только файл настроек; без переменных
   поведение прежнее. Им пользуются `settings.py`, `catalog.py`, `sources.py` — те
   модули, которые прогон действительно пишет.
2. `tests/test_ensure.py` — состояние прогона в своём каталоге, каталог пробы уникален
   (`_ragraf_ensure_probe_<pid>`), остатки прошлых прогонов снимаются и называются в
   выводе, добавлены три проверки «живой `catalog/*` не изменён».
3. `tests/test_doc_layer_archive.py` — вместо записи в живой файл и возврата байтов
   набор работает в своём каталоге, а живой файл только сверяется.
4. `tools/check_all.py` — снимок живого `catalog/*` до и после КАЖДОГО набора; набор,
   изменивший состояние, называется по имени, прогон завершается ненулевым кодом.
5. `tests/test_state_isolation.py` — 22 проверки: приоритет переменных, запись в
   отведённое место, неизменность живого состояния (в том числе после дочернего
   процесса), два прогона `test_ensure` подряд, отсутствие зашитого пути в трёх
   пишущих модулях.

**Замеры.**

| Что | Значение | Команда |
|---|---|---|
| `tests/test_state_isolation.py` | **22 проверки / 0 провалов** (87 с) | прямой запуск |
| `tests/test_ensure.py` | **20 проверок / 0 провалов** (было 17) | прямой запуск |
| `tests/test_doc_layer_archive.py` | **38 проверок / 0 провалов** | прямой запуск |
| красная проба «остаток прошлого прогона» | до правки `3 из 17`, после — `20 из 20` | `.staging/i33-stage7` |
| состав группы `core` | **54** набора (добавлен `test_state_isolation.py`) | `check_all.py --list` |
| прогон ядра №1 (диагностика) | 54 наборов, 1477 проверок, провалов 1; живое состояние изменили 11 наборов | `.staging/i33-stage7/run1.log` |
| прогон ядра №2 | 54 наборов, провалов 1; живое состояние изменили 5 наборов | `run2.log` |
| прогон ядра №3 | 54 набора, 1477 проверок, провалов 0; живое состояние не изменил ни один набор: да | `run3.log` |
| прогон ядра №4 (приёмка: второй подряд) | 54 набора, 1477 проверок, провалов 0; живое состояние не изменил ни один набор: да; 1 568,5 с | `run4.log` |

**Границы.** Снимок раннера видит ИТОГОВОЕ состояние, а не процесс: набор, записавший
живой файл и вернувший байты, снимком не ловится. Четыре модуля по-прежнему выводят путь
от каталога установки (`access.py`, `db_access.py`, `sso.py`, `vault.py`) — обычный прогон
их не пишет, а вынос хранилища секретов на `paths` относится к этапу 14. Разбор —
`docs/03_implementation/STATE-ISOLATION.md`.

**Следующий этап — 9 (хранилище).** Промт на него выдан в отчёте о закрытии этапа 7.

### Б.9 Этап 9 — закрыт 30.10.2026

**Что было.** `.ragraf` занимал **26 829 МБ** при **8 808 МБ** собственно индекса:
`cpg/` — 17 831 МБ, `index/` — 8 954 МБ. В лексике лежали **7 729 196 строк**
документного слоя от 265 ревизий, которых в истории нет; в каталоге `cpg` —
473 файла CPG (5,65 ГБ) и 452 распакованных каталога Joern (11,5 ГБ) от 473
ревизий, данных которых в хранилищах уже нет. Ни `index_status`, ни `catalog` не
называли ни объёма `cpg`, ни хвоста ротации: объём был виден только обходом диска.

**Причина, а не симптом.** Правило «удаляется вместе с ревизией» жило рукописным
списком таблиц в `Revisions._purge_rev_data` — списком, который надо было не
забыть дополнить. Документный слой появился позже списка и в него не попал.
Слой живых баз (`db_live_column` и родственные, ещё **3 594 860 строк**) не попал
туда же. Поэтому список таблиц теперь ВЫЧИСЛЯЕТСЯ по схеме (у какой таблицы есть
колонка `rev_id`), и новая таблица попадает под политику в тот день, когда
появилась.

**Что сделано.**

1. `src/ragraf/storage.py` — модуль политики: объёмы трёх хранилищ и `cpg`,
   разбор артефактов по ревизиям, план, выполнение, `VACUUM`, защита активной
   ревизии по строкам таблиц.
2. Пять настроек в `settings.DEFAULTS` с границами и объяснением каждого
   умолчания замером: `storage_keep_cpg_revs`, `storage_tmp_max_mb`,
   `storage_tmp_age_hours`, `storage_vacuum_free_percent`, `storage_auto_compact`.
3. Ротация (`revisions._purge_rev_data`) удаляет строки по вычисленному списку и
   артефакты CPG ревизии; индексатор после ротации зовёт автоочистку (по
   умолчанию выключена — см. ниже).
4. `ragraf compact` (`--plan` по умолчанию, `--apply --yes`, `--vacuum`,
   `--retain`), операция фасада `op_compact`, инструмент MCP `ragraf_compact`.
5. `index_status` показывает три хранилища (`graph`, `lexvec`, `control`),
   свободные страницы, объём `cpg`, временные каталоги, число ревизий и остаток
   хвоста — «сколько занимает» видно без обхода диска.
6. `tests/test_storage_policy.py` — 30 проверок в группе `core`: план ничего не
   пишет; удаляется только хвост; активная ревизия цела по числам и по строкам;
   повтор идемпотентен; удалённая ревизия не находится поиском; подсадки
   «удалили лишнее» и «хвост попал в хранение» ловятся.

**Замеры.**

| Что | Значение | Команда |
|---|---|---|
| замер на копии в `%TEMP%` | граф 965,2 → 842,7 МБ, лексика 7 843,3 → 860,8 МБ, `VACUUM` 21,6 с, активная ревизия цела | `tools/measure_storage_compaction.py --plan --apply-copy` |
| бэкап перед боевым прогоном | 2 699 файлов, 9 026,8 МБ, 3,1 с | `robocopy` вне дерева |
| боевой прогон | **7 761,0 МБ** освобождено: граф 965,2 → **174,9**, лексика 7 843,3 → **872,5**; 140,7 с; `VACUUM` 1,8 и 9,6 с; хвоста 0 | `ragraf compact --apply --yes` |
| активная ревизия после | `rev-0875`: 946 / 5 780 / 77 710 — до и после совпадает поштучно | `ragraf compact --plan` |
| `.ragraf` целиком | 26 829 МБ → **1 265,7 МБ** | обход каталога |
| восстановление из копии | ревизия, файлы/узлы/рёбра, счётчики лексики и 10 путей поиска совпали; копия 9 026,8 МБ за 6,8 с | `compare_stores_direct.py`, `check_corpus_revision.py` |
| проверки ядра | **55** наборов (было 54), три прогона: 1 477 проверок, провалов **0** в каждом; в одном из трёх живое состояние не изменил ни один набор, в двух раннер пометил наборы — причина внешняя (посторонний процесс менял `catalog/catalog.json` в окне простоя, что измерено) | `check_all.py --group core` |
| чекер конвенций | 45 проверок, 0 ошибок, 0 предупреждений | `tools/Invoke-ProjectDoctrine.ps1` |
| ссылки `файл:строка` | 222 ссылки, 118 пар, 0 неверных | `tools/check_line_refs.py` |

**Инцидент, который изменил умолчание.** Автоочистка была задумана включённой по
умолчанию. Правка исходников сделала корпус неактуальным, ближайший вызов фасада
запустил автоматическую переиндексацию → ротацию → автоочистку, и 473 файла CPG
(5,65 ГБ) и 452 распакованных каталога Joern (11,5 ГБ) исчезли **без плана, без
бэкапа и без решения владельца**. Строки хранилищ при этом не удалялись, активная
ревизия цела, `check_corpus_revision.py` совпадал, а все удалённые артефакты
относились к ревизиям вне истории. Вывод записан в код: `storage_auto_compact`
по умолчанию `False`, политика применяется планом и явной командой, автоматизм —
осознанный выбор владельца.

**Границы.** История ревизий (`control.sqlite`) не удаляется никогда; чужие
корпуса не трогаются; неопознанные имена в `cpg/` и пустые каталоги Joern не
удаляются; `VACUUM` в индексации не выполняется (блокирует файл на десятки
секунд). Подробности, процедура восстановления и разбор — `docs/03_implementation/STORAGE.md`.

**Следующий этап — 8 (доказательность графа вызовов).** Промт выдан в отчёте о
закрытии этапа 9.

## Актуализация 2026-10-29: где план стоит по факту

> Нумерация разделов ниже сохранена: §0–§9 — это исходный документ от 2026-10-04, и
> его ценность в том, что было предсказано, а что нашлось по дороге. Всё, что ниже
> этого блока, — состояние на 29.10.2026 с замером каждого числа.

### А.1 Этапы: что закрыто

| Этап | Предмет | Состояние | Чем подтверждено |
|---|---|---|---|
| 0 | рамка | закрыт 04.10.2026 | §0–§4 этого документа |
| 1 | исследование | закрыт | `docs/01_research/` (4 волны, `SPEC_INPUT.md`) |
| 2 | архитектура и ТЗ | закрыт | `docs/02_architecture/{TZ,ARCHITECTURE,ACCEPTANCE}.md` |
| 3 | реализация ТЗ | закрыт | **60** операций фасада, **67** инструментов MCP, **15** возможностей изданий |
| 4 | документный слой | закрыт 28–29.10.2026 | `docs/03_implementation/DOCUMENT-LAYER.md`; **261** документ, **15 238** чанков, **200** свойств в шести наборах |
| 5 | публикация 0.5.0 и защита от повторения | **не начат — следующий** | раздел А.3 |

Замеры, на которых стоит таблица (все сняты в этой сессии):

| Что | Значение | Команда |
|---|---|---|
| ревизия на момент замера | **rev-0663**: 904 файла / 5632 узла / 75 473 ребра, `capability=full`. Это снимок, а не заявление об активной ревизии: живую отдаёт `query index_status` | `tools/rev_snapshot.py` |
| документный слой | 261 документ / 15 266 чанков, p95 163, max 1200 (числа на момент замера; живые отдаёт \query doc_status\) | `query doc_status` |
| реестр | **42** разработки, 33 проиндексированы; ревизий в истории **667**, ограничения хранения нет | `catalog`, `rev_history` |
| проверки ядра | 53 файла / **1450** проверок / 0 провалов (1458,8 с) | `check_all.py --group core` |
| чекер конвенций | **41** проверка, 0 ошибок, 0 предупреждений | `Invoke-ProjectDoctrine.ps1` |
| ссылки `файл:строка` | 221 ссылка в 21 документе, **0** неверных; 113 пар «утверждение → объект», 0 провалов | `check_line_refs.py` |
| гейт поставки (числа) | OK: размеры индекса и строка реестра во всех 6 документах | `gate_release.py --check-numbers` |

### А.2 Расхождения, найденные при актуализации, и что с ними стало

| Что было объявлено | Было | Стало (29.10.2026) | Чем держится теперь |
|---|---|---|---|
| инструментов MCP: тело README и alt-текст всех шести | 61 и 66 | **67** | проверка чекера, 12 образцов (включая alt-текст и пять переводов) |
| операций фасада | 54 и 59 | **60** | там же |
| проверок чекера в документе интеграции | 35 | **41** | там же |
| «числа измерены на версии 0.4.0» | 0.4.0 | **0.5.0** | шапки шести README и `src/ragraf/__init__.py:9` |
| документного слоя нет в журнале | нет записи | запись `0.5.0` | `community/CHANGELOG.md` |
| «Отсутствует Java/JVM» (§8 этого плана) | нет JDK | OpenJDK **21.0.12.1** (Temurin), `joern_enabled: true` | `config/ragraf.yaml:250` |

**Как это ловилось.** Проверка с именем «documented operation counts match the code»
в чекере существовала, считала операции фасада и инструменты MCP — и **ничего не
сравнивала**: массив `$documents` (`tools/Invoke-ProjectDoctrine.ps1:1584`) был объявлен
и ни разу не использован. Зелёный результат означал «посчитал код», а не «сверил с
документом».

**Что сделано вместо разовой правки цифр.**

1. Сверка восстановлена: сравниваются ВСЕ совпадения образца в документе, а не первое;
   отсутствие образца в документе — находка; пять переводов и alt-текст входят в список.
2. Числа считает один инструмент — `tools/surface_counts.py`; его отказ (а не ноль) —
   тоже находка.
3. Первый прогон восстановленной проверки дал **31 расхождение в 10 файлах** — это и
   есть доказательство, что она видит: 54 и 59 операций, 61 и 66 инструментов, 35
   проверок чекера.
4. Подсадка проверена: замена 67 → 66 в README валит прогон — сообщение называет
   документ, строку, заявленное и фактическое число («называет 66 инструментов
   поверхности для агентов, а по коду их 67»); возврат значения снова даёт PASS.


### А.3 Этап 5 — публикация 0.5.0 и защита от повторения

**Закрыто 29.10.2026.** Все шесть пунктов выполнены: сверка объявленных
чисел восстановлена (31 расхождение в 10 файлах исправлено, подсадка
проверена), документы и журнал приведены, версия 0.5.0, гейт поставки
пройден (5 порч из 5, 2 приманки из 2), раздел наблюдения в документе
слоя. Добавлено сверх плана: `--dry-run` у сборщика (724 файла
предпросмотра против 724 собранных) и `tools/check_release_runs.py` —
проверка, что дерево поставки не только собрано, но и запускается.
Осталось действием владельца: коммит и тег `v0.5.0` в его клоне — в этом
каталоге `.git` нет, поэтому таблица источников честно говорит «не
совпадает» до публикации.

1. **Вернуть сравнение в чекер.** Массив `$documents`
   (`tools/Invoke-ProjectDoctrine.ps1:1584`) объявляет документы и образцы чисел —
   цикл сверки к нему утрачен. Восстановить цикл так, чтобы: число в документе
   сравнивалось с посчитанным по коду; отсутствие образца в документе было находкой
   (иначе «проверить нечем» снова пройдёт молча); исторические документы оставались
   вне списка. Проверка формулируется через свойство «документ называет число, и оно
   равно посчитанному», а не через поиск конкретной строки — иначе правка текста
   снова сделает её слепой. После включения: сначала красный прогон (документы
   врут), затем правка документов, затем зелёный.
2. **Журнал изменений.** Запись `0.5.0` по Keep a Changelog: документный слой
   (форматы, локаторы, бюджеты, валидатор), OCR как возможность Pro, девять
   дефектов фазы E и два дефекта закрытия — с числами «до/после».
3. **Версия.** `0.4.1` → `0.5.0` (новая возможность — минорная по SemVer) во всех
   местах объявления; сверка согласованности версий.
4. **Документы.** README и пять переводов: раздел про документный слой, честные
   числа, снятие «измерено на 0.4.0». `DOCUMENT-LAYER.md`: «как повторить замер».
5. **Гейт поставки.** `build_release.py` → `verify_release.py` → `gate_release.py`
   (`--check-numbers`), проверка `doc-ocr` в инвентаре Pro и отказа Community.
6. **Наблюдение.** Раздел «что проверять после следующего крупного изменения»:
   четыре команды, ожидаемые числа, признаки ложного успеха.

### А.4 Что остаётся решением владельца (не работа агента)

| Вопрос | Состояние |
|---|---|
| 10 188 PDF каталога `PAULMANN.RU`: индексировать целиком? | не решено; замер масштаба не делался |
| Песочницы замера (`%TEMP%\ragraf-phase-e2`, 261 МБ; `ragraf-phase-f`) | не удалены: удаление необратимо |
| `connectors`, `fleet` в реестре изданий | сняты 2026-10-26; возврат только вместе с контрактом запуска и живой проверкой |
| Зашифрованные PDF и PDF без `startxref` | зашифрованные читаются запасным извлекателем; испорченный (1 файл) — нет, причина названа |

---

## 0. Что требуется (формулировка заказчика)

1. Разобрать статью <https://habr.com/ru/companies/pt/articles/1084780/> и подобные работы на arXiv.
2. Спроектировать вспомогательные RAG- и графовые базы, которые **облегчают работу LLM по доработке и разработке ПО**.
3. Перед началом доработки некий софт **проводит индексацию кода** в эти базы.
4. Агенты при доработке **активно используют базы вместо изучения кода** (`rg`/чтение файлов вслепую).
5. По мере разработки — **накопление различий базы и кода** в отдельной БД или в md-файле, чтобы **не делать неправильных выводов** в процессе накопления.
6. После окончания разработки/доработки — **запуск переиндексации**, приводящей базы в соответствие с кодом.
7. Работа ведётся в три этапа: **(1) исследование → (2) архитектура и ТЗ → (3) реализация ТЗ**, с подведением итогов и запросом уточнений перед и после каждого этапа.

---

## 1. Разбор статьи-источника

### 1.1 Что утверждает статья

Статья Positive Technologies («Агент не справляется с аудитом кода? Просто добавь графы») — про **аудит безопасности**, но её механика прямо переносится на любой анализ кода агентом.

| № | Тезис статьи | Что из этого следует для нас |
|---|---|---|
| 1 | Промпт-инженерия, размер модели и «залить весь репозиторий в контекст» — три тупика. Большое окно не спасает: модель тонет в шуме | Индексировать и **сжимать** контекст нужно до попадания в окно, а не надеяться на большое окно |
| 2 | Три базовых графа: **AST** (из чего состоит), **CFG** (куда пойдёт выполнение), **DFG** (откуда значение и на что влияет). **PDG** = CFG + DFG. **CPG** = наложение всех на общее множество узлов, узлы несут свойства, рёбра типизированы | Три разных вопроса агента требуют трёх разных графов; объединять их надо на общих узлах, иначе теряется связность |
| 3 | **Срез программы** (slice) по PDG резко уменьшает объём: −68…70% на функциональных наборах, **−90,93%** на проектном ReposVul. Пример: 85 строк → 18 строк с сохранением контекста | Срез — это «сжатие контекста до окна». Это главный механизм экономии, который нам нужен, а не «поиск похожего кода» |
| 4 | Полнота — за графом (формально доказанные пути, гарантированный обход), точность — за LLM (семантическая интерпретация, отсев ложных срабатываний). Даже обычную проверку кода приходится делать 5–7 раз, чтобы собрать все замечания | Агенту нельзя доверять **обход** структуры; ему нужно доверять **суждение** о найденном |
| 5 | **RepBench:** AST+PDG = 83,2% против 53,5% у сырого кода. Но «только граф» оказался лучше, чем «граф + код» — **эффект разбавления контекста**. Оговорка: 107 примеров, только C/C++ | Передавать агенту подграф/срез, а не подграф вместе с исходником. Переносить на PHP/Python/JS надо с проверкой, а не на веру |
| 6 | **LLMxCPG** (USENIX Security 2025): +15–40% F1 против VulSim/ReGVD/VulBERTa; реальные абсолютные показатели скромнее — 0,634 и 0,6 на «свежих» уязвимостях; порог γ калибруется ~20 размеченными примерами | Метрики надо мерить на **наших** данных; чужие цифры — только ориентир. Калибровка по 20 примерам — реалистичный бюджет |
| 7 | Наивное «опишу задачу словами — модель напишет Cypher/CPGQL» не работает: языки запросов низкоресурсны, модели путают `.code` и `.name`. Работают: (а) дообучение генератора с обратной связью от валидатора запросов, (б) повторное использование проверенных запросов | **Не давать агенту писать запросы к графу.** Давать фиксированные типовые операции (`callers_of`, `impact_analysis`, `slice`, `repo_map`) — параметризованные, но не свободный Cypher |
| 8 | **ALIBI** (июль 2026): враждебные комментарии скрывали уязвимость >90% случаев, защита промптами почти не работает; **Words Speak Louder Than Code**: вердикты сдвигаются от формулировки задачи (33,2%), от подброшенного «предварительного анализа» (23,5%), от репутации автора кода (18,4%), пропуск до 97% угроз. Помогают только очистка комментариев и архитектурная изоляция | Текст из корпуса (комментарии, докблоки, README, коммит-сообщения, «предыдущие выводы») — **канал влияния** на агента. Их нельзя просто подмешивать в промпт: нужен отдельный слой с явной маркировкой происхождения и режимом «только факты графа» |
| 9 | **CLEAR** (август 2026): граф может быть не графом кода, а причинно-следственным графом знаний об уязвимостях + 4 агента (Сборщик, Утверждающий, Критик, Судья) | Знания **о** коде (не только сам код) — полноправный слой. Для нас: история решений, регламенты, «почему так сделано» |
| 10 | «А почему не RAG?» Вектор ловит синтаксическое сходство, но не видит отношений «значение из A утекает в B» | RAG и граф — **не альтернативы, а два разных вопроса**: «где похожее уже делали» (вектор/лексика) и «что с чем связано» (граф) |
| 11 | Графовые БД масштабируются до миллионов узлов (Neo4j, TigerGraph); CPG реальных проектов — как раз такого порядка | Нужна настоящая графовая БД или embeddable графовый движок, а не «граф в JSON» |
| 12 | Устойчивость к обфускации: срез строится по семантическим зависимостям, поэтому переименования не ломают его; ML-детекторы по сырому коду проседают (Risse & Böhme, USENIX Security 2024). Слабое место среза — вынос кода в отдельные функции, меняющий его границы | Срез — стабильный идентификатор контекста, но вынос кода в функцию/метод ломает границы. Для нас это **норма повседневного рефакторинга** → нужна устойчивость к extract-method, иначе индекс будет постоянно «плыть» |
| 13 | **VulnGym** (август 2026): на цепочках ≥9 шагов лучший агент нашёл 22,58% уязвимостей; 56,5% фейлов — «не нашёл файл» (26,8%) или «не нашёл строку» (29,7%) | Это **навигационный** провал, а не провал рассуждения. Именно его закрывает индекс: «где файл» и «где строка» должны быть ответом за один запрос |
| 14 | Пример масштаба: GGML — 832 файла, 416 474 строки; агент с доступом к графу (MCP-сервер «codebadger») нашёл 54 точки выделения памяти и >300 опасных операций вместо линейного чтения | Порядок величин нашего корпуса ниже, но MCP как форма доставки знаний агенту — подтверждённая практика |

> **Статус проверки (обновляется по ходу Этапа 1).** Первая волна исследования подтвердила первоисточниками утверждения 5 (RepBench, `arXiv:2606.25356`), 7 (валидность CPGQL: 132/1278 и 19/1278 против 1278/1278), 8 (ALIBI, `arXiv:2607.24964`), 12 (Risse & Böhme, USENIX Security 2024) и частично 6 (LLMxCPG, USENIX Security 2025: +15–40% F1, Accuracy 0,634/F1 0,610 на ReposVul, порог γ = 0,594/0,547/0,334/0,193, калибровка на 20 точках).
> **Не подтверждено:** 22,58% и разбивка фейлов у VulnGym (README датасета прямо пишет «Baseline evaluation results — Coming soon»), агрегированные проценты сжатия 68–70% / 90,93%, «8000 токенов», числа Words Speak Louder Than Code и CLEAR.
> **Опровергнуто:** работа **LeanGuard** по этому названию не находится — ссылку статьи считать недостоверной до подтверждения.
> **Ссылка «codebadger»:** существует — <https://github.com/qcri/codebadger>, MCP-сервер над CPG Joern (GPL-3.0, Postgres/Redis, контейнер). Форма доставки графа агенту подтверждена; стек конфликтует с нашей средой (Docker + JVM).

### 1.2 Ключевой разрыв: наш кейс ≠ аудит безопасности

| Аспект | Статья (аудит безопасности) | RAGRAF (разработка и доработка) |
|---|---|---|
| Вопрос агента | «есть ли путь источник→сток?» | «где это лежит, кто это вызывает, что сломается, как сделать одинаково с существующим?» |
| Что нужно от графа | taint-путь с формальной гарантией полноты | **impact analysis**, контракты символов, связи с БД/маршрутами/формами/тестами |
| Языки | преимущественно C/C++ | PHP без фреймворка, Python, JS/TS, C#, PowerShell, SQL |
| Размер кода | сотни тысяч — миллионы строк | десятки — сотни тысяч строк на проект; несколько проектов |
| Жизненный цикл | разовый аудит зафиксированного коммита | **непрерывная разработка**; индекс живёт месяцами и неизбежно устаревает |
| Второй корпус данных | только код | код **+ схема MySQL + документация (md) + история git + конфиги** |
| Риск | пропустить уязвимость | **сломать работающее** и потратить токены на переоткрытие уже известного |

Отсюда два требования, которых в статье нет вовсе, но без которых заказ невозможен: **дрейф индекса** (§4) и **измеримость пользы на нашем корпусе** (§2.3).

### 1.3 Что в статье не рассматривается, а нам критично

1. **Дрейф индекса и различия «база vs код»** — в статье аудит одноразовый. Наша задача — живой индекс + журнал расхождений.
2. **Порядок обновления.** Инкрементальная переиндексация «по ходу» смешивает частичные выводы агента с фактами кода — это и есть механизм «неправильных выводов», которого заказчик просит избежать.
3. **Git-история как источник истины о прошлых изменениях** — даёт готовый оценочный набор (gold set) для проверки impact-анализа: по коммиту известно, какие файлы реально были затронуты.
4. **Динамические языки.** PHP без фреймворка: `include`/`require`, переменные-функции, `eval`-подобные конструкции, SQL в строках. Часть рёбер графа неизбежно эвристическая → каждое ребро обязано нести **достоверность и доказательство** (файл:строка), иначе агент будет опираться на выдуманные связи.
5. **Цена обновления.** Если переиндексация проекта занимает часы, её не будут запускать. Это нефункциональное требование первого порядка.

---

## 2. Постановка задачи RAGRAF

### 2.1 Целевой цикл (то, что заказчик описал словами)

```
┌──────────────────────────────────────────────────────────────────────┐
│ ФАЗА A — ИНДЕКСАЦИЯ (до начала доработки)                            │
│   scan → parse → extract → normalize → load → verify                 │
│   выход: INDEX_REV (снимок кода), отчёт покрытия, список пропусков   │
├──────────────────────────────────────────────────────────────────────┤
│ ФАЗА B — РАБОТА АГЕНТА С ИНДЕКСОМ                                    │
│   агент спрашивает: где лежит · кто вызывает · что сломается ·       │
│   срез функции · карта репозитория · кто читает таблицу · маршруты   │
│   НЕ сканирует репозиторий вслепую                                   │
├──────────────────────────────────────────────────────────────────────┤
│ ФАЗА C — НАКОПЛЕНИЕ РАЗЛИЧИЙ (drift ledger)                          │
│   каждая правка регистрируется как дельта: путь, хэши, символы,       │
│   затронутые рёбра, кто/когда/чем обнаружено                         │
│   ИНДЕКС ПРИ ЭТОМ НЕ МЕНЯЕТСЯ (заморожен на INDEX_REV)               │
├──────────────────────────────────────────────────────────────────────┤
│ ФАЗА D — ПЕРЕИНДЕКСАЦИЯ (после завершения доработки)                 │
│   reindex --commit → новый INDEX_REV, ledger архивируется и          │
│   превращается в человекочитаемый отчёт «что изменилось в знаниях»   │
└──────────────────────────────────────────────────────────────────────┘
```

### 2.2 Черновик требований

**Функциональные**

- **FR-1** Индексатор строит по репозиторию граф кода: узлы и типизированные рёбра с языковой разметкой.
- **FR-2** Индексатор строит векторный и лексический индекс для «поиска похожего» и навигации по тексту.
- **FR-3** В графе представлены не только символы, но и **внешние контракты**: таблицы/колонки/внешние ключи MySQL, HTTP-маршруты и их обработчики, формы и JS-обработчики, CLI/cron-точки входа, файлы документации.
- **FR-4** Фасад предоставляет фиксированный набор операций (не свободный язык запросов): `search`, `symbol`, `references`, `callers`, `callees`, `impact`, `slice`, `repo_map`, `schema`, `table_usage`, `routes`, `index_status`, `drift_report`, `drift_ack`.
- **FR-5** **Каждый факт в ответе несёт провенанс**: источник (парсер+версия), `path:line`, достоверность, `index_rev`.
- **FR-6** Журнал различий (ledger) ведётся как машиночитаемая БД **и** как человекочитаемый md-журнал; запись append-only.
- **FR-7** Любой ответ фасада при непустом ledger помечает затронутые факты как `stale` и предлагает живое чтение файла.
- **FR-8** Переиндексация запускается явной командой, синхронно или в фоне, с сохранением истории ревизий; результат — новый `INDEX_REV`.
- **FR-9** Операции чтения фасада доступны агенту как инструменты (MCP-сервер) и как CLI.
- **FR-10** Отчёт покрытия: сколько файлов разобрано, сколько не разобрано, где потеряны связи (важно для честности выводов).

**Нефункциональные**

- **NFR-1** Работает на текущей машине **без прав администратора**.
- **NFR-2** Полная переиндексация проекта до ~300 тыс. строк — не более ~15 минут на CPU; инкрементальная по дельте — секунды-минуты.
- **NFR-3** Никаких секретов в индексе: значения паролей/токенов/DSN из кода и конфигов **не индексируются**, только факт ссылки.
- **NFR-4** Индекс воспроизводим: одинаковый вход → одинаковый `INDEX_REV` (детерминированные id узлов).
- **NFR-5** Индекс можно удалить и пересобрать с нуля одной командой; артефакты не «размазаны» по репозиторию проекта.
- **NFR-6** Вся обвязка — PowerShell 7.6+/Python 3.13 по правилам `AGENTS.md` (кодировки utf8NoBOM, без CRLF/BOM в поставляемых файлах, без WinPS-compat).

### 2.3 Метрики успеха (измеримые, а не «стало лучше»)

| Метрика | Как считается | Базовая линия |
|---|---|---|
| **M1. Точность навигации** | Доля задач, где корректный файл найден с первого запроса к фасаду | Агент без индекса (`rg`/чтение) — снимается прогоном |
| **M2. Полнота impact-анализа** | Для N исторических коммитов: доля реально затронутых файлов, предсказанных `impact` **до** внесения правки | Gold set из git-истории; предсказание «только правильный файл» = 0,3–0,5 по опыту |
| **M3. Экономия контекста** | Токены и число tool-вызовов на задачу «внести правку» с индексом / без | Прямое сравнение на одинаковых задачах |
| **M4. Сжатие среза** | `тokens(срез) / tokens(подграф окружения)` по нашей кодовой базе | В статье: −68…70% и −90,93% на чужих наборах |
| **M5. Свежесть** | Доля ответов, помеченных `stale`; число инцидентов «агент опирался на устаревший факт» | Цель: инцидентов ноль, т.к. факт помечен |
| **M6. Проверяемость** | Доля утверждений агента со валидной ссылкой `path:line`, подтверждённой автоматически | Автопроверка существования файла и попадания в диапазон строк |
| **M7. Стоимость обновления** | Время полной и инкрементальной переиндексации | NFR-2 |

**Важно:** M2 измерим **задним числом** на реальной истории проектов заказчика — это отличает проверяемый результат от демонстрации. Точное значение baseline требует согласования (см. §7, вопрос 2).

---

## 3. Эскиз архитектуры (уточняется на Этапе 2)

```
L0  ИСТОЧНИКИ      код (git) · схема MySQL · документация (md) · история git · конфиги
                     │
L1  ЭКСТРАКТОРЫ    tree-sitter (синтаксис всех языков)
                   + типовые анализаторы: phpstan/psalm (PHP) · pyright/jedi (Python)
                     · TS compiler API (TS/JS) · Roslyn (C#) · AST-разбор PS (PowerShell)
                     · introspection MySQL (information_schema) · git log/diff
                     │  выход: единый поток фактов (узлы/рёбра/фрагменты текста)
L2  МОДЕЛЬ         каноническая схема: узлы, типизированные рёбра, свойства, провенанс
                     │
L3  ХРАНИЛИЩА      граф (узлы/рёбра) · векторный индекс · лексический индекс (BM25/FTS)
                   · SQLite: ревизии, покрытие, drift ledger
                     │
L4  ФАСАД          MCP-сервер (основной) + CLI (отладка и CI)
                     │
L5  ИНТЕГРАЦИЯ     AGENTS.md проекта + skill: «сначала индекс, потом чтение файлов»
```

### 3.1 Схема модели (черновик, уточняется на Этапе 2)

**Типы узлов:**

- Структура: `repo`, `package`, `module`, `file`, `namespace`, `class`, `trait`, `interface`, `enum`
- Код: `function`, `method`, `property`, `constant`, `parameter`, `sql_literal`
- База данных: `db_table`, `db_column`, `db_index`, `db_fk`, `db_view`
- Веб/точки входа: `web_route`, `http_endpoint`, `form`, `js_handler`, `cli_command`, `cron_job`
- Знания: `doc`, `doc_section`, `commit` (в т.ч. `config_key`, `env_var` — **без значений**)

**Типы рёбер:**

- Структура: `contains`, `defines`, `imports`, `uses`, `extends`, `implements`, `overrides`, `instantiates`
- Вызовы: `calls` (с числом мест вызова), `calls_heuristic` (динамический вызов, пониженная достоверность)
- Данные: `reads`, `writes`, `queries`, `returns`, `param_of`, `type_of`, `flows_to` (внутрипроцедурно)
- СУБД: `fk_to`, `references_table`
- Маршруты: `handles`, `renders`, `submits`, `posts_to`
- Проверка/знания: `tested_by`, `documented_by`, `changed_in`, `mentions`, `similar_to`

**Обязательные свойства каждого ребра:** `evidence` (`path:line`), `confidence` (1.0 статика / ~0.6 эвристика / ~0.4 вывод из истории или эмбеддинга), `extractor` (имя+версия), `index_rev`.

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

### 3.2 Что сознательно НЕ делаем (ограничение объёма)

- Не строим полноценный taint-анализ и не ищем уязвимости — это задача статьи, не наша.
- Не пишем CPGQL/DSL для агента: только фиксированные операции (тезис 7 статьи).
- Не индексируем бинарники, вендорные каталоги и сгенерированный код (список исключений — на Этапе 2).
- Не даём агенту модифицировать граф: он пишет только в ledger.

---

## 4. Ключевой механизм: накопление различий (drift ledger)

Это ядро требования заказчика «чтобы не делать неправильных выводов в процессе их накопления».

### 4.1 Три состояния и главный инвариант

| Состояние | Что это | Меняется когда |
|---|---|---|
| **Код** | фактическое содержимое репозитория | в любой момент |
| **Индекс** | снимок фактов на `INDEX_REV` | **только** командой `reindex --commit` |
| **Ledger** | append-only список дельт между индексом и кодом | каждая правка, обнаруженная хуком/сравнением |

> **Инвариант I1.** В пределах одного `INDEX_REV` индекс **иммутабелен**. Никакая частичная инкрементальная переиндексация в ходе доработки не выполняется.

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

### 4.2 Схема записи ledger

Машиночитаемая часть (SQLite, таблица `drift_entry`):

| Поле | Смысл |
|---|---|
| `id` | монотонный номер |
| `ts` | время обнаружения |
| `index_rev` | против какой ревизии зафиксирована дельта |
| `path` | файл (или `db://schema` для схемы БД) |
| `kind` | `add` / `modify` / `delete` / `rename` / `schema_change` |
| `old_hash`, `new_hash` | контентные хэши (не mtime) |
| `symbols_added`, `symbols_removed`, `symbols_changed` | символы, если удалось определить парсером на месте |
| `edges_invalidated` | какие рёбра графа заведомо устарели |
| `detected_by` | `hook:session-start` / `watcher` / `git-diff` / `manual` / `db-schema-diff` |
| `commit` | коммит, если есть |
| `ack` | признание дельты агентом: кто и когда её учёл |
| `note` | свободный комментарий (например: «вынес метод, границы среза изменились») |

Человекочитаемая часть — `drift/LEDGER.md`, дописывается теми же событиями, понятными формулировками:

```markdown
## 2026-10-04 14:32 · rev idx-0007 · detected_by git-diff
- `src/Wizard/OutgoingLetters.php` — modify
  - изменено символов: 3 (`listAction`, `$filterMap`, `applyFilters`)
  - устарели рёбра: `handles(OutgoingLetters#listAction → /wizard/outgoing/list)` — под вопросом
  - примечание: добавлен новый фильтр; контракт ответа не менялся
```

### 4.3 Жизненный цикл и защита от неверных выводов

1. **Старт доработки.** Хук сравнивает `INDEX_REV` с рабочим деревом → агент получает `drift_report`. Пустой отчёт = «можно опираться на индекс без оговорок».
2. **Ход доработки.** Каждый агентский ответ фасада содержит шапку:
   `index_rev=idx-0007 · drift=3 файла / 7 символов · provenance=index|live|mixed`.
   Факты, затронутые непустым ledger, помечаются `stale` → фасад предлагает живое чтение.
3. **Регистрация правки.** После каждой правки (или пачки) запись в ledger. Отдельно фиксируются правки, меняющие **границы срезов** (extract-method) — самое слабое место подхода по статье (тезис 12).
4. **Завершение доработки.** `reindex --commit` → новый `INDEX_REV`; ledger архивируется в `drift/archive/<rev>_<ts>.md` с ответом на вопрос «что изменилось в знаниях», и обнуляется.
5. **Дедупликация.** Ключ `(path, old_hash, new_hash)` — повторное обнаружение той же дельты не задваивает журнал.
6. **Ручные правки запрещены.** Ledger append-only; ошибка исправляется новой записью, а не перезаписью (иначе теряется история и «неправильный вывод» становится неотличим от верного).

**Определение «доработка завершена»** (выносится на уточнение, §7): явная команда `reindex` · коммит в git · завершение сессии агента · порог (N правок или M минут).

---

## 5. План по трём этапам

### Этап 1 — Исследование

**Цель:** заменить «знание из памяти модели» проверяемыми фактами: что именно работает, где измерено, что переносится на прикладной код PHP/Python/JS/C#.

**Работы (выполнено):**

1. ✅ **Волна 1.1 — верификация статьи.** 19 положений проверено: 10 подтверждено, 2 частично, 1 опровергнуто, 1 не подтверждено, 1 не найдено, 1 осталось непроверенным. Отчёт: `01_ARTICLE_COVERAGE.md`.
2. ✅ **Волна 1.2 — литература** по 6 направлениям, ~30 подтверждённых идентификаторов. Отчёт: `02_LITERATURE.md`.
3. ✅ **Волна 1.3 — инструментальный ландшафт** под среду без JVM/Docker/админа: 30 инструментов, 4 варианта стека. Отчёт: `03_TOOLS.md`.
4. ✅ **Волна 1.4 — измеримость и корпуса.** 10 проектов замерено, пилот выбран числами (`revoap.com`). Отчёт: `04_CORPUS.md`.

**Deliverable:** `docs/01_research/` — итоговый отчёт `00_RESEARCH_REPORT.md`, три предметных отчёта и **`SPEC_INPUT.md`** (20 ФТ + 7 НФТ, выведенных только из проверенных фактов).

**Критерий приёмки:** ✅ ~30 работ с реальными идентификаторами; ✅ ни одного утверждения без источника; ✅ явное разделение «первоисточник / пересказ / не найдено»; ✅ названы 4 противоречия и 6 лакун. Единственный незакрытый пункт: агрегированные проценты сжатия LLMxCPG (PDF не дочитан) — на выводы не влияет.

### Этап 2 — Архитектура и ТЗ

**Работы (выполнено):**

1. ✅ Выбор стека под ограничения среды — с учётом решения заказчика разрешить JVM: **базовый слой** (tree-sitter + SCIP/PHPStan + SQLite-граф + FTS5 + sqlite-vec + fastembed) **плюс Joern `php2cpg` с JDK 19** для CPG.
2. ✅ Финализация схемы: 5 групп узлов, 14 видов рёбер, два уровня провенанса (`stated` / `derived`), детерминированные идентификаторы.
3. ✅ Контракты **62 операций** фасада с единым конвертом ответа (`index_rev`, `capability`, `drift`, `provenance`, `limits`, `stale_warnings`).
4. ✅ Формат ledger: схема `drift_entry`/`stale_edge`/`drift_archive`, 5 способов обнаружения дельт, 5 инвариантов, дедупликация, отдельный вид записи `slice_boundary`.
5. ✅ Регламент интеграции агента: MCP + CLI + `AGENTS.md` + skill.
6. ✅ План замеров M1–M7 и 41 тест с порогами.

**Deliverable:** `docs/02_architecture/ARCHITECTURE.md` · `TZ.md` (**43 ФТ + 12 НФТ**, милстоуны M1–M8, критерии К1–К9 и пороги метрик) · `ACCEPTANCE.md` (протоколы M1–M7 на пилоте, набор из ≥ 20 задач, gold set из 20 коммитов) · `README.md`.

**Критерий приёмки:** ✅ каждая метрика из §2.3 имеет процедуру снятия и baseline; ✅ у каждого требования — проверяемый критерий и тест; ✅ явно очерчены границы (что вне объёма релиза 1) и оговорки (неподтверждённые числа не используются).

### Этап 3 — Реализация ТЗ

Порядок микрошагов (уточняется в ТЗ; здесь — для понимания формы):

- **M1** Каркас: структура каталогов, конфиг проектов-целей, версионирование `INDEX_REV`.
- **M2** Экстракторы синтаксиса первого языка (вероятно PHP — крупнейший корпус) → узлы и рёбра `contains/defines/imports/calls`.
- **M3** Графовое хранилище + загрузка + операция `index_status` и отчёт покрытия.
- **M4** Векторный и лексический индексы + гибридный `search` с провенансом.
- **M5** Внешние контракты: схема MySQL (`schema`, `table_usage`), маршруты и точки входа (`routes`).
- **M6** Операции навигации и анализа: `callers`, `callees`, `references`, `impact`, `slice`, `repo_map`.
- **M7** Drift ledger: хук старта сессии, запись дельт, пометка `stale`, архивация при `reindex --commit`.
- **M8** MCP-фасад + интеграция в регламент агента + замер M1–M7 и отчёт «индекс против baseline».

**Deliverable:** работающий индекс пилотного проекта, MCP-фасад, drift ledger, `docs/03_implementation/` с отчётами о замерах.

**Критерий приёмки:** M1/M2/M3 улучшены относительно baseline с зафиксированным протоколом; полная переиндексация укладывается в NFR-2; у каждого факта в ответе есть провенанс; после сессии доработки `reindex --commit` приводит индекс в соответствие коду, а архив содержит читаемое «что изменилось».

### Этап 4 — Документный слой (выполнен 28–29.10.2026)

Предмет: документ как первоклассный узел индекса — структура, адрес чанка,
валидатор как гейт зафиксирования ревизии. Форматы: текстовые, офисные (OOXML и
OLE), PDF, данные, почта, EPUB, HTML, архивы; OCR — отдельная возможность Pro.

Замер: 626 документов массового замера (19,9 млн символов) и 524 документа на чужом
корпусе (`z.batteryservice.ru`); чанков сверх бюджета 3188 → **0**; NUL 22 → **0**;
частичные векторы 59,7 % → **0,9 %**; попадание в раздел 8 из 10; точность OCR
**74,3 %** при разбросе 21–95 %. Подробности и способ повторить — `docs/03_implementation/DOCUMENT-LAYER.md`.

### Этап 5 — Публикация 0.5.0 и защита от повторения (следующий, не начат)

Задачи, порядок и гейты — в разделе «А.3» выше. Основание: проверка чисел в чекере
существует, но не сравнивает (мёртвое объявление `$documents`), поэтому шесть
документов называют четыре разных числа при фактических 60 операциях и 67
инструментах; документного слоя нет в журнале изменений; версия 0.4.1 при готовой
новой возможности.

---

## 6. Риски

| Риск | Проявление | Как снижаем |
|---|---|---|
| Динамика PHP | связи выдуманы или потеряны (`include`, переменные-функции) | каждое ребро несёт достоверность и `evidence`; эвристика помечена как эвристика |
| «Индекс соврал» | агент уверенно опирается на устаревшие факты | инвариант I1 + обязательная пометка `stale` + `drift_report` на старте |
| Дорогая переиндексация | индекс не обновляют → он гниёт | NFR-2 и замер M7 как часть приёмки |
| Лицензии инструментов | CodeQL/Joern ограничения | лицензии проверяются на Этапе 1 и попадают в ТЗ как ограничение |
| **JVM/Docker нет** | Joern (единственный из проверенных инструментов, дающий полный CPG) требует JDK; Neo4j требует JVM; codebadger требует Docker + JVM | `php2cpg` в Joern **есть** (проверено в официальной таблице фронтендов) — вопрос не в поддержке языка, а в JVM. Решение — за заказчиком (§7, вопрос 1) |
| Качество CPG для динамического PHP | `include`/`require`, переменные-функции, magic-методы, SQL в строках | честный отчёт покрытия + пометка эвристических рёбер |
| Правки, ломающие границы срезов | extract-method «размывает» контекст | отдельная категория записей в ledger + повторный срез после `reindex` |
| Секреты в индексе | утечка паролей в артефакты | NFR-3: значения не индексируются, только факт ссылки |
| Расхождение ожиданий по метрикам | «стало удобнее» вместо измеренного | baseline M1–M3 фиксируется до реализации, в ТЗ |

---

## 7. Открытые вопросы к заказчику

> Вопросы уточнены по итогам Этапа 1: варианты ответов и последствия взяты из замеров, а не из предположений.

> **Ответы по факту (29.10.2026).** Вопросы 1–7 и 10 закрыты реализацией, а не
> решением на бумаге: JVM разрешён и используется (OpenJDK 21, `joern_enabled: true`);
> пилот `revoap.com` отработан, в реестре 42 корпуса; эмбеддинги локальные, 384
> измерения; форма доставки — MCP + CLI + skill, плюс HTTP-канал общего индекса в Pro;
> порог переиндексации задан настройками, а не «окончанием доработки»; языков в
> разборе 16, текстом — ещё 10; секреты вынесены в отдельное хранилище вне корпуса;
> документация индексируется документным слоем, а не узлами графа. Вопрос 8 (сколько
> ревизий хранить) не пересматривался: в истории 656 ревизий, ограничения нет.
> Вопрос 9 (сверка чужих чисел) заменён собственными замерами: свои цифры оказались
> важнее чужих.

**1. Стек графа: разрешить ли JVM (Joern + JDK 19)?** Проверено: Joern — **единственный** инструмент из доступных, дающий полный CPG (AST+CFG+DFG+PDG) и срезы, и у него **есть `php2cpg`**; лицензия Apache-2.0; ставится zip-ом **без прав администратора**; ~0,5–1,5 ГБ.
- *Да* → доступны межпроцедурный data-flow и срезы по потоку данных (то, на чём построены лучшие результаты статьи).
- *Нет* → строим на `tree-sitter` + SCIP + SQLite/`real-ladybug`; ТЗ **не обещает** data-flow, только impact по структурным связям и символам.

**2. Пилотный корпус.** Замерами определён порядок: **`revoap.com`** (единственный PHP-проект с git: 103 коммита, 162 366 строк) → **`z.batteryservice.ru`** (26 195 строк, без git, но с миграциями и тестами) → **`BSG-main`** (TypeScript + 273 md). Подтверждаете такой порядок или он иной?

**3. Эмбеддинги.** Локально на CPU, без torch. Выбор: `paraphrase-multilingual-MiniLM-L12-v2` (~0,22 ГБ, 384) — русскоязычный минимум **или** `multilingual-e5-large` (2,24 ГБ, 1024) — качественнее, но медленнее. Внешний API не предлагается (передача кода вовне). Какой вариант?

**4. Форма интеграции.** Основной вариант — **MCP-сервер** + skill/A`GENTS.md` проекта (автономно от вендора модели) плюс CLI для отладки. Нужен ли дополнительно HTTP-сервис для других сред?

**5. Триггер переиндексации.** Что считать «окончанием доработки»: явную команду · git-коммит · завершение сессии агента · порог по числу правок? (Инвариант §4.1 — индекс не меняется до этого момента — сохраняется при любом ответе.)

**6. Объём первого релиза.** Один язык (PHP — покрывает пилот) или сразу все (PHP, Python, JS/TS, C#, PowerShell, SQL)?

**7. Секреты (уже фактически подтверждено).** Замером найдены секретоносные файлы в 4 из 10 корпусов. Предлагается правило: значения секретов не индексируются вообще, секретоносные пути исключаются списком (не «маскируются»). Подтверждаете?

**8. Хранение ревизий индекса.** Сколько ревизий храним и в каком виде: полные копии (проще, больше места) или дельты (компактнее, дороже в реализации)? Свободно ~1131 ГБ, так что место — не ограничение.

**9. Неподтверждённые числа.** Агрегированные проценты сжатия у LLMxCPG (68–70% / 90,93%) не подтверждены — нужно ли тратить время Этапа 2 на чтение PDF ради сверки, или достаточно эмпирического замера сжатия на нашем пилоте (M4)?

**10. Документация в графе.** Индексировать Markdown в тот же граф (связь `documented_by`) или отдельным слоем? У пилота 58 md, у `BSG-main` — 273.

---

## 8. Замеренные факты об окружении (не предположения)

| Параметр | Значение | Как проверено |
|---|---|---|
| ОС / оболочка | Windows, PowerShell **7.6.6** (Core) | `$PSVersionTable` |
| Python | **3.13.15** + pip | `python -V` |
| Node | **24.20.0** + npm | `node -v` |
| Прочие рантаймы | `rg`, `git`, PHP **8.5**, MySQL **9.7** (порт 3306, `Running`), .NET **8 runtime** (без SDK) | `Get-Command`, `Get-Service` |
| **Отсутствует** | **Java/JVM** (нет каталогов JDK/JRE) · **Docker** · **ollama** · PostgreSQL · **.NET SDK** | проверка путей и `Get-Command` |
| Диск C: | свободно **≈1131 ГБ** | `Get-PSDrive C` |
| Права | **без администратора** — установка служб/портов недоступна | правила проекта (`AGENTS.md` §7.4) + отсутствие elevated-маркера |
| Рабочая папка | `RAGRAF` — **пуста** | `ls` |
| Инструменты проекта | `Clean_BOM_Senior\clean-bom-senior.{sh,ps1}` — **есть**; линтер `Test-PwshApiVintage.ps1` — **есть**; Git Bash — **есть** | `Test-Path` |

> **Исправление 2026-10-29.** Строка «Отсутствует Java/JVM» устарела и оставлена как
> замер от 04.10.2026: сейчас в системе **OpenJDK 21.0.12.1 (Temurin)**, найден
> `C:\Android\jdk\jdk21\bin\java.exe`; Joern включён настройкой
> `joern_enabled: true` (`config/ragraf.yaml:250`). Проверка: `java -version`.
> Docker, ollama, PostgreSQL и .NET SDK по-прежнему отсутствуют.

Следствие: стек должен быть **Python/Node-центричным, без JVM и без Docker**.
**Уточнение 29.10.2026:** следствие выполнено частично и осознанно — ядро
осталось Python-центричным, Docker не понадобился, но JVM разрешён и
используется для графа свойств кода (Joern): только он даёт межпроцедурные
связи и срез. Отказ от JVM стоил бы слоя связей, а не удобства. Если для графа нужен Neo4j — это отдельное решение заказчика (вопрос 1), требующее установки JVM и, вероятно, прав администратора на службу.

---

## 9. Итоги Этапа 0 и Этапа 1

**Итог Этапа 1.** Проведено полное исследование: статья проверена по 19 положениям (10 подтверждено, 1 ссылка — LeanGuard — не существует), собрано ~30 работ с подтверждёнными идентификаторами, замерено 30 инструментов под среду без JVM/Docker/админа и 10 реальных корпусов. Пилот определён числами. Главные уточнения против первоначальных предположений: **Kuzu архивирован** (идём через `real-ladybug` или таблицы смежности в SQLite); **у Joern есть `php2cpg`**, то есть вопрос не в языке, а в JVM; **`bge-m3` и `gte-multilingual` в fastembed отсутствуют**; **Milvus Lite на Windows не работает**; **вектор + лексика + граф помещаются в один файл SQLite**. Два требования заказчика (дрейф индекса и защита от накопления неверных выводов) в литературе **не покрыты** — проектируем и измеряем сами.

**Итог Этапа 0.** Статья даёт механику (граф + срез + LLM как интерпретатор, фиксированные запросы вместо свободного Cypher, недоверие к текстовым каналам влияния), но она про разовый аудит и не отвечает на два требования заказчика: жизнь индекса во времени и измеримость пользы. Поэтому план строится вокруг **иммутабельного снимка + append-only журнала расхождений** (§4) и **метрик, снимаемых на реальной истории проектов** (§2.3).

**Следующий шаг.** Этап 2 — архитектура и ТЗ. Он начинается только после ответов заказчика на вопросы §7: они определяют и архитектуру, и объём работ (прежде всего вопрос 1 — JVM, и вопрос 2 — пилот).

---

## 10. Итоги этапов 3 и 4, следующий шаг (29.10.2026)

**Этап 3.** ТЗ реализовано: фасад (60 операций `op_*`), MCP-сервер (67 инструментов),
пять групп узлов и 14 видов рёбер, два уровня провенанса, журнал расхождений,
граф свойств кода через долгоживущий сервер Joern, сторожа свежести с
детерминированным схлопыванием дублей. Числа активной ревизии и проверок — в §А.1.

**Этап 4.** Документный слой добавлен как отдельный слой того же индекса, а не второе
хранилище: чанки идут через существующие `ft_chunk` / `chunk_text` / `chunk_map` /
`vec_meta`, `source_kind = "doc"`. Валидатор стоит между `finish_rev` и `seal_rev` и
блокирует запечатывание ревизии при нарушении — прежний индекс остаётся рабочим.
Девять дефектов, найденных только на реальных файлах, закреплены свойствами.

**Следующий шаг.** Этап 5 — публикация 0.5.0 (раздел А.3). Он начинается с проверки,
которая делает объявленные числа проверяемыми, и только потом правит документы:
иначе исправленные цифры снова разойдутся с кодом на следующей возможности.

---

## 11. Итог этапа 8 — доказательность графа вызовов (30.10.2026)

**Что было не так.** У ребра были `provenance` и `confidence`, но не было
признака СПОСОБА получения связи: `stated` означал и «CPG связал вызов с
методом», и «цель найдена по имени и файлу владельца», и «вызов встроенной
функции языка». Замер на rev-0896: из **11 621** ребра `calls` подтверждение CPG
имели **3 941**, а остальные **7 680** попадали в `callers` как связи. Вывод по
имени читался как факт — прямым нарушением принципа проекта.

**Что сделано.** Колонка `edge.evidence_kind` (`cpg_call` | `signature_match` |
`name_inference` | `structural` | `unknown`), догоняющая миграция и
`SCHEMA_VERSION = 2` (аддитивно: у старых рёбер вид восстанавливается по полям,
переиндексация не обязательна). Запись рёбер сведена к одному входу
`store.insert_edges`, классификатор один — `evidence.classify`. Ответы
`callers`/`callees`/`impact`/`slice` называют вид у каждого ребра и отдельно —
сколько рёбер выведено. Появились операция `edge_evidence`, команда
`ragraf edges --fqn … --evidence` и инструмент MCP `ragraf_edge_evidence`.
Доказательство без координаты (`path:line`, сверенная с файлом ревизии)
доказательством не считается.

**Числа до и после** (`python tools/measure_edge_evidence.py`):

| Показатель | До (rev-0896) | После (rev-0904) |
|---|---|---|
| рёбер в ревизии | 77 983 | 78 996 |
| рёбер вызова | 70 278 | 71 250 |
| подтверждено CPG (`cpg_call`) | 3 941 (5,6 %) | **4 327 (6,1 %)** |
| выведено по имени (`name_inference`) | 54 714 (77,8 %) | 55 220 (77,5 %) — **смысл уточнён этапом 11: это внешние вызовы, см. ниже** |
| по имени и месту (`signature_match`) | 11 623 (16,5 %) | 11 703 (16,4 %) |
| вид записан в хранилище | нет колонки | 78 996 из 78 996 |
| рёбра с координатой `path:line` | 77 983 (100 %) | 78 996 (100 %) |
| рёбра, ведущие в никуда | не считались | 249 (0,3 %) — названы в ответе |

**Точность `callers`** (`tools/measure_impact_precision.py --section callers`):
в ответе видно 14 рёбер (0 % выводов), в графе по тем же символам — 25 рёбер, из
них подтверждено CPG 9 (36 %), выведено 1 (4 %). Разница объясняется порогом
достоверности: раньше он молча отсекал слабые связи, теперь их число названо
(`below_confidence_floor`).

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

**Проверки.** `tests/test_edge_evidence.py` — 38 проверок в группе `core`
(56 наборов вместо 55), включая подсадку «ребро без доказательства выдано за
подтверждённое». Подробности — `docs/03_implementation/EDGE-EVIDENCE.md`.

## 12. Итог этапа 10 — честность ответа целиком (31.10.2026)

**Что было не так.** Этап 8 научил ответы называть вид доказательства, но за его
пределами остались три вещи: рёбра, ведущие в несуществующий узел (их нельзя
открыть по адресу, а ответ показывал их как связь); операции со связями вне
`callers` (`references`, `db_relations`, `dead_code`, `entrypoints`,
документные) вида доказательства не называли вовсе; порог достоверности 0,5 был
зашит, и «связей 0» нельзя было отличить от «связь отсечена правилом».
Плюс CPG-привязка «по полному имени» показывала 0 без объяснения.

**Что сделано.**

1. **Причина потери цели измерена** — `tools/measure_dangling_edges.py`:
   249 рёбер на 33 адресах = 235 «заглушка не создана» + 10 «имя неоднозначно» +
   2 «ребро выдумано» + 2 «цель была, резолвер не привязал».
2. **Потеря цели устранена правилом** «заглушка существует в ревизии ЛИБО ребра
   нет»: `src/ragraf/references.py` + стадия `refs` индексатора заводят узел
   `external` без файла для каждого адреса; производитель SQL-рёбер больше не
   читает таблицу из прозы; мёртвая ветка привязки по имени класса в
   `_resolve_external_names` оживлена.
3. **Сквозная честность**: `references` отдаёт вид у каждой связи и сводку;
   `db_relations`, `dead_code`, `entrypoints`, `doc_search`, `doc_get` прямо
   говорят, что вопрос «чем подтверждён вызов» к ним не применяется, и называют
   свою природу доказательства.
4. **Порог — параметр**: `min_confidence` у операций, `--min-confidence` в CLI,
   `min_confidence` в MCP; ответ называет значение, его источник и сколько
   связей отсечено — ВСЕГДА, а не только при пустом результате. Значение по
   умолчанию не менялось.
5. **CPG по полному имени**: `by_fullname_norm` = 5 053 ребра (было 0),
   `by_fullname` = 0 с объяснением и числом: 44 819 записей из 45 491 приходят с
   заглушкой `<unknownFullName>`.

**Числа до и после** (`tools/measure_dangling_edges.py`):

| Показатель | До (rev-0925) | После (снимок 31.10.2026) |
|---|---|---|
| рёбер в никуда | **249** (0,3 %) | **0** |
| адресов без узла | 33 | 0 |
| узлов-заглушек внешних адресов | 0 | 30 |
| выдуманных рёбер `queries` | 2 из 4 | 0 из 2 |
| `by_fullname_norm` при привязке CPG | 0 | 5 053 |
| вид доказательства в `references` | нет поля | у каждой связи + сводка |
| правило порога в ответе | только при пустом результате | в каждом ответе |

**Чего этап НЕ делает.** Он не повышает точность графа: выведенные связи
остаются выведенными, а `target_renamed`/`ambiguous` — это по-прежнему отказ
привязки, а не выдуманная связь. Заглушка означает «определения в корпусе нет»,
а не «связь доказана».

**Проверки.** `tests/test_graph_honesty.py` — 47 свойств в группе `core`,
включая подсадку «ребро в никуда» и «проза разобрана как SQL». Итог группы:
**58 наборов, 1 684 проверки, 0 провалов, код 0** — два прогона подряд
(1 696 с и 1 765 с), живое состояние после каждого не изменилось.

Сверх графа этап закрыл три дефекта самих проверок: набор менял живое
состояние (`test_mcp_http.py` оставлял строки журнала в
`catalog/access_tokens.json`), набор выполнял двадцать проверок и сообщал
«0 пройдено» (`test_mcp.py` печатал без сводки), набор был красен вне всех
групп (`test_documented_numbers.py`: неполная песочница плюс литерал
«54 operations» после этапа 10). Подробности и таблицы замеров —
`docs/03_implementation/EDGE-EVIDENCE.md`, разделы 7, 8 и 9.

## 13. Итог этапа 11 — внешние вызовы и честность проверок (31.10.2026)

**Что было не так.** Два числа врали, и оба — о связях. Первое: «77 % рёбер вызова —
вывод по имени». Замер показал, что все **55 632** ребра вида `name_inference` вели в
узлы `external` (`python:print`, `python:get`, `javascript:push`), то есть в символы,
которых в корпусе нет; привязок «по одному имени» к определениям проекта было
**ноль**. Значит фраза описывала вызовы библиотек, а не догадки про код проекта: она
занижала точность графа проекта и не называла главное — что три четверти рёбер
вызова уходят наружу корпуса. Второе: примечание `edge_evidence` называло выводом
`signature_match` и `name_inference`, а счётчик `derived` считал только
`name_inference` — доля выводов была занижена втрое (**7 % вместо 23 %**), и ответ
противоречил сам себе.

**Что сделано.**

1. Вид `external_call` («цель вне корпуса»): вычисляется при записи по таблице узлов
   ревизии и при чтении для ревизий, записанных раньше; в число выводов не входит
   намеренно — у него другая природа. Сводка печатает три величины раздельно
   (подтверждено CPG, выведено, внешних), `callers`/`callees` называют внешние связи
   отдельной строкой.
2. `signature_match` добавлен в `DERIVED_KINDS`: привязка «по имени и месту
   объявления» — вывод, а не подтверждение.
3. Три дефекта нашли не в графе, а в наборе проверок, и все три — та же тема («ответ
   не должен выдавать одно за другое»): `tests/test_mcp_http.py` удалял свои токены,
   но не строки журнала, которые сервер дописывал в `catalog/access_tokens.json`,
   поэтому раннер завершался кодом 1; `tests/test_mcp.py` печатал `[OK ]`/`[FAIL]` без
   сводки, и раннер писал «0 пройдено, 0 провалено» при двадцати выполненных
   проверках; `tests/test_documented_numbers.py` не входил ни в одну группу `check_all`
   и был красен, а его провал не останавливал ничего.

**Числа до и после** (`python tools/measure_edge_evidence.py`, живая ревизия):

| Вид | Рёбер вызова | Доля | Что означает |
|---|---|---|---|
| `external_call` | 55 861 | 77,0 % | цель вне корпуса (было записано как «вывод по имени») |
| `signature_match` | 11 634 | 16,0 % | вывод по имени и месту объявления |
| `cpg_call` | 5 085 | 7,0 % | подтверждено CPG |
| `name_inference` | **0** | 0 % | догадка по одному имени — таких среди вызовов не осталось |

| Показатель | До | После | Команда |
|---|---|---|---|
| наборов в группе `core` | 57 | **58** (добавлен `test_documented_numbers.py`) | `check_all.py --list` |
| проверок в группе `core` | 1 602 | **1 664** | `check_all.py --group core` |
| провалов, два прогона подряд | — | **0** и **0** (1 696,2 с и 1 764,9 с) | `.zwork/core-C.log`, `.zwork/core-D.log` |
| свойств в `test_documented_numbers.py` | 0 (не в группе) | **62** | прямой запуск |
| ссылок `файл:строка` | 215 | **222**, неверных 0; пар «утверждение → объект» 125, провалов 0 из 118 | `check_line_refs.py` |
| чекер конвенций | 45 проверок | **45** проверок, 0 ошибок, 0 предупреждений | `tools/Invoke-ProjectDoctrine.ps1` |

**Чего этап НЕ делает.** Он не повышает точность привязки цели: `by_fullname` = 0 при
заглушках `<unknownFullName>` и неразрешённых записях CPG — это отдельная правка
экстрактора, а не настройка. «Выведено по имени: 0» означает «среди рёбер вызова больше
нет догадок по одному имени», а не «граф стал точным».

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

**Границы, названные честно.** `external_call` не даёт «затронутых файлов» в `impact` и
не появляется у проектных символов в `callers`. Если у внешнего символа в корпусе есть
одноимённое определение, ребро привязывается к нему (`signature_match`) — признак
вычисляется по цели, а не по имени. Разбор — `docs/03_implementation/EDGE-EVIDENCE.md`,
разделы 8 и 9.

## 14. Актуализация при возобновлении (31.10.2026, 12:30)

Сеанс разработки оборвался после приёмки этапа 11: код и `EDGE-EVIDENCE.md` были
готовы, два прогона ядра зелёные, а закрытие в мастер-плане отсутствовало. Ниже —
что проверено при возобновлении и что найдено сверх плана.

**Состояние на момент возобновления** (замеры этой сессии):

| Что | Значение | Чем проверено |
|---|---|---|
| версия | 0.10.0 | `src/ragraf/__init__.py` (строка 9) |
| активная ревизия | **rev-1026**: 934 файла / 5 895 узлов / 80 363 ребра, `capability=full` | `tools/rev_snapshot.py` |
| корпус актуален | да, реиндексация не требуется | `ragraf -p config/ragraf.yaml check` |
| чекер конвенций | **45** проверок, 0 ошибок, 0 предупреждений | `tools/Invoke-ProjectDoctrine.ps1` |
| ссылки `файл:строка` | 222 ссылки, неверных 0; 125 пар, провалов 0 из 118 | `tools/check_line_refs.py` |
| группа `core` (последний прогон) | 58 наборов, 1 664 проверки, 0 провалов, 1 764,9 с | `.zwork/core-D.log` |

**Находка 1 — прогон проверок делал корпус устаревшим (исправлено).**
`tests/test_mcp_http.py` пишет журнал прогона в `tests/test_mcp_http_out.txt`, а корпус
индексирует `**/*.txt`. Замер: прогон набора переписал файл (2 193 → 1 160 байт, другой
sha256), сумма корпуса изменилась, и следующая операция заплатила переиндексацией
**47,3 с**; всего за сутки в истории оказалось **92** ревизии (30.10 — 178). Правка:
`**/*_out.txt` в исключениях корпуса (`config/ragraf.yaml`), с причиной и замером в
самом файле. Замер до/после пересбором корпуса: выпадает ровно **2** файла, файлов кода
**0**; состав **936 → 934**, узлы и рёбра не изменились. Приёмка: прогон набора
(20 проверок, 0 провалов) переписал журнал, а `ragraf check` после него ответил
«переиндексация не требуется». Свойство закреплено проверкой `tests/test_exclude_configs.py`
(раздел 5, 24 проверки вместо 21, включая отрицательный контроль «скан не пуст»);
подсадка «правило испорчено» даёт 2 провала, возврат файла побайтово — 24/0.

**Находка 1а — корпус «устаревал» от собственных проверок (исправлено).**
Причина продолжала действовать и после исключения журналов прогонов: `ensure` писал
конфигурацию нового корпуса в `config/` установленного RAGRAF, а `config/**` входит
в корпус; имя содержит pid. Замер: **две проверки подряд обе запускали
переиндексацию** (49,6 и 42,8 с). Путь вынесен в `paths` (`RAGRAF_CONFIG_DIR` +
`paths.config_dir()`), `init_corpus.create_config` берёт его оттуда, `test_ensure.py`
отводит каталог в своё состояние. Замер после правки: два прогона набора — 22/0,
`ragraf check` после них «переиндексация не требуется», скан корпуса до и после
совпал (934 файла, 0 изменённых). Проверки: `test_state_isolation.py` +3,
`test_exclude_configs.py` +1.

**Обновление 05.11.2026 (решение владельца).** `config/**` больше НЕ входит в корпус
самого RAGRAF: в `config/ragraf.yaml` добавлено правило `**/config/*.yaml` с
причиной — это настройки, а не код (тот же довод, что у `catalog/**`). Замер:
979 → **927** файлов, узлы и рёбра не изменились (6 653 / 91 876 на rev-1394), из
индекса не ушёл ни один
файл вне `config/`. Проверка — `tests/test_exclude_configs.py`, раздел 6
(29 проверок, 0 провалов); подробности — `docs/AGENTS-context.md`, раздел 11.

**Находка 1б — ответ шёл по удалённой ревизии (исправлено).**
`rev.json` называл `rev-1083` (`state=rotated`), активной была `rev-1087`; фасад верил
паспорту: «ревизия без данных: 0 файлов, 0 узлов, 0 чанков», чекер осматривал 0 рёбер
и объявлял находкой пустоту, а сторож свежести запускал переиндексацию на каждом
обращении. Исправлено сверкой с `store.active_revision` (`facade.rev_pointer_guard`),
расхождение называется. После правки: `index_status` 934 / 5 907 / 80 364, `check`
дважды «переиндексация не требуется». Свойства: `test_active_revision.py` +5.
Источник отставшего паспорта не установлен — записан находкой 8 ниже.

**Находка 2 — прерванный прогон оставляет ревизию навсегда (не исправлено).**
В истории **5** записей в состоянии `building` (`rev-0996`, `rev-0930`, `rev-0929`,
`rev-0729`, `rev-0519`), у всех поля `files`/`nodes`/`edges` пусты, ни одна не
запечатана. Видны они только через `skipped_unsealed` активной ревизии; политика
хранения их не убирает, потому что правило говорит про данные ревизии, а не про запись.
Формулировка дефекта: **оборванный прогон оставляет запись, которую никто не закрывает
и не называет**. Починка требует признака живости процесса (иначе гонка с параллельной
индексацией — второй прогон закроет чужую живую запись), поэтому она вынесена в
следующий этап, а не сделана «по пути».

**Находка 3 — разгадана: 10 секунд были спрятаны в нуле (исправлено).**
Стадия `databases` записывала в `stage_timing` литерал `0.0`, хотя работа шла раньше —
до записи стадии. Профиль быстрой переиндексации (31.10.2026) показал: `databases`
**9,827 с** факта против **0,000 с** в ответе, 224 запуска клиента СУБД
(`db_collect` 8,4 с, `db_samples` 7,7 с под профилем). Теперь время берётся вокруг
`_collect_databases`. Замер после правки: сумма стадий **43,3 с** при **49,9 с**
внутрипрогонных (было «≈31 с при 47–57 с»): остаток ≈6,6 с — ротация
(`_purge_rev_data` 4,3 с под профилем), три вызова `corpus.scan` (3,1 с) и
запечатывание ревизии. Эти три пункта — следующий шаг этапа 12, теперь с числами.

**Шаг 12.1 — снимки строк БД вынесены из быстрого прогона (сделано).**
Стадия `databases` со снимками **10,24–11,90 с**, без снимков **2,41 с** (rev-1078/1079
против rev-1077). Изолированно, с чередованием порядка вызовов: 8,43–8,65 с против
0,70–0,78 с. Введена настройка `db_samples_in_fast` (умолчание `False`), правило —
`indexer.want_db_samples`. `db_sample` больше не отвечает `found=True`, когда снимков в
ревизии нет: он называет причину. Свойства: `test_db_marks.py` +7 (подсадка ловится).
Честная граница: сравнение полного прогона по двум прогонам на плечо разницы не показало
(50,0 против 51,1 с) — разброс лексики и ротации больше эффекта; решение принято по
стадийному замеру.

**Находка 3 (исходная запись) — быстрая переиндексация дороже, чем сумма стадий.** `index_status`
называет стадии: `scan` 1,8 с, `ast` 7,4 с, `load` 3,2 с, `resolve` 1,5 с,
`schema` 0,01 с, `cpg` 1,8 с, `databases` 0 с, `lexvec` 16,7 с, `refs` 0,1 с — сумма
**≈31 с**, а фактическая быстрая переиндексация того же корпуса занимает
**47,3–56,8 с**. Расхождение **≈25 с** не объяснено: стадии не покрывают открытие и
закрытие трёх хранилищ, запись ревизии, уборку и `VACUUM`-подобные шаги. Это первый
замер этапа 12: без него критерий «≤ 30 с» проверять нечем.

**Находка 4 — baseline чекера в `AGENTS.md` устарел.** раздел 10 `AGENTS.md` называет
«checks: 41», фактический прогон даёт **45**. Исправлено там же.

**Находка 5 — набор проверок чувствителен к нагрузке.** `tests/test_mcp_http.py`,
запущенный одновременно с чекером, упал по таймауту HTTP; тот же набор в прогоне ядра
дал 20/0 и чистый повторный прогон — тоже 20/0. Это не дефект продукта, но и не
«случайность»: набор ждёт ответа сервера по фиксированному сроку, а во время чекера
диск занят гейтом поставки. Записано, чтобы такой провал не искали в коде MCP.

**Находка 6 — генератор хэшей не покрывал таблицу источников (исправлено).**
`tools/registry_line.py --write` пересчитывал хэши шести README, а сторож
`tools/check_community_wrapper.py` требует хэш для каждого файла обвязки. Правка
`community/CHANGELOG.md` оставила в таблице хэш `0787468225d6…` при фактическом
`48423ee65c07…`, и сборка поставки была забракована собственной проверкой —
штатного способа привести дерево в согласие не было. Теперь `--write`
пересчитывает все объявленные строки с существующим файлом (строка-образец
пропускается). Замер: `--verify --strict` — 8 документов, 0 ошибок,
0 предупреждений; `check_community_wrapper.py` — 230 объектов, 0 нарушений.

**Состояние на конец сессии** (замер 31.10.2026, 15:55):

| Что | Значение | Чем проверено |
|---|---|---|
| активная ревизия | **rev-1067**: 934 файла / 5 902 узла / 80 520 рёбер | `tools/rev_snapshot.py` |
| корпус актуален | да | `ragraf -p config/ragraf.yaml check` |
| чекер конвенций | **45** проверок, 0 ошибок, 0 предупреждений — PASS | `tools/Invoke-ProjectDoctrine.ps1` |
| ссылки `файл:строка` | 222 ссылки, неверных 0; 125 пар, провалов 0 из 118 | `tools/check_line_refs.py` |
| числа в README и переводах | 934 / 5 902 / 80 520 во всех шести; таблица источников сверена | `tools/registry_line.py --verify --strict` |
| журналы прогонов в корпусе | 0 путей `*_out.txt` | `tests/test_exclude_configs.py` (24 проверки) |
| незапечатанных записей | **7**, последние `rev-1039` (13:10) и `rev-1064` (15:17) созданы в ходе сессии, события в `event_log` нет | запрос к `control.rev_history` |
| группа `core` | **58 наборов, 1 684 проверки, 0 провалов** (1 712,3 с) | `.zwork/core-H.log` |

**Находка 7 — набор проверок трогает живой реестр (не исправлено).** Прогон ядра
завершился зелёным (58 наборов, 1 667 проверок, 0 провалов), но раннер назвал набор,
изменивший живое состояние: `test_mcp_compat.py` → `catalog/catalog.json` (в реестре
`updated_at` сдвинулся на 13:41:16, состав — те же 44 записи, пробных не осталось).
То есть проверка индексации пробного корпуса пишет в ЖИВОЙ реестр, а не в свой каталог:
класс тот же, что закрывал этап 7 (`RAGRAF_CATALOG_DIR`), но через обновление реестра.
Починка требует изоляции реестра для пробных корпусов — это отдельный шаг, а не «попутно».

**Находка 9 — чекер ловил собственную песочницу (исправлено).**
Уборка висела только на `atexit`; на Windows файлы держатся открытыми мгновение после
выхода дочернего процесса, и один прогон из нескольких оставлял
`ragraf-manifest-check-<случайное>` в окне измерения чекера — полный прогон давал
`44 из 45`. Уборка теперь идёт явно перед `return report()` (пять повторов с паузой),
`atexit` — запасной путь, остаток печатается как `NOTE`. Замер: 436 → 436 каталогов,
`-Only ReleaseManifest` PASS.

**Находка 9 (исходная запись) — чекер иногда ловит собственную песочницу.** Полный прогон
31.10.2026: **44 проверки из 45**, единственный провал — «проверка манифеста оставила
песочницы во временном каталоге: `ragraf-manifest-check-<случайное>` (всего 1)». Проверка
сравнивает список каталогов `%TEMP%` до и после запуска `tools/check_release_manifest.py`;
отдельный прогон того же файла руками следов **не** оставляет (замер: 436 каталогов до,
436 после). То есть это окно между выходом процесса и уборкой (`atexit`), а не мусор в
дереве. Проверка помечена как «плавающая»: её провал нельзя считать провалом поставки, но и
молча игнорировать нельзя — нужно мерить по счётчику после паузы либо убирать песочницу в
самом скрипте, а не в `atexit`. Гейт классов и сборка в этом же прогоне прошли.

**Порядок этапов на будущее.** Закрытия 10 и 11 закрывали ветку честности ответа,
поэтому пункты Б.3 «документный слой 2.0» (10) и «полнота разбора языков» (11) остаются
открытыми, а очередь после 11 — по Б.6: **12 (скорость и стабильность индексации)** →
13 (польза для агента) → 14 (безопасность) → 15 (издания) → 6 (публикация).

## 15. Этап 12 — закрыт 31.10.2026: скорость и стабильность индексации

Разбор — `docs/03_implementation/INDEX-SPEED.md`. Здесь итог и решения.

**Что было.** Быстрая переиндексация корпуса занимала **35,6 с** (замер первым
`ensure` этой сессии; в отчёте предыдущей сессии — 34,1 с), причём сумма стадий
давала 29,3 с: запечатывание, ротация и открытие хранилищ в профиль не попадали
вовсе. Стадия `lexvec` одна стоила **16,18 с**.

**Находка 1 — 17 секунд в плане запроса.** `reusable_vectors` соединял обычную
таблицу `vec_meta` с виртуальной `vec_chunk`. Ограничение по `chunk_id` в
виртуальную таблицу не передаётся, поэтому SQLite сканировал `vec_chunk` на
КАЖДУЮ строку `vec_meta` (план `SCAN v VIRTUAL TABLE INDEX 1:2!___`). Замер на
живом хранилище: соединение **16,98 с**, `EXISTS` с тем же смыслом — те же 16,98 с,
два скана и пересечение в Python — **0,17 с**. Форма заменена
(`LexVecStore.reuse_index`), возврат прежней запрещён свойством
`tests/test_vector_reuse.py`.

**Находка 2 — переписывались все векторы, не меняя ни одного.** Из 21 272 чанков
активной ревизии **все 21 272** уже имели вектор в vec0: `chunk_id` адресует
содержимое, и неизменившийся чанк получает тот же идентификатор. Прежний код
делал `DELETE` + `INSERT` на 21 тыс. строк на каждом прогоне. Теперь пишется
только отсутствующее, а для сдвинутых чанков (текст тот же, идентификатор новый)
вектор переносится запросом `INSERT … SELECT` — vec0 его поддерживает (проба на
песочнице: 5 строк из 5).

**Находка 3 — уборка векторов не работала вовсе.** `_purge_rev_data` удалял
строки vec0, но расширение на этом соединении не загружалось: `table_exists`
отвечал `True`, `DELETE` падал с `no such module: vec0`, ошибка глушилась
`break`. Замер: **19 709 строк из 41 356 (48 %)** недостижимы для поиска и никем
не убраны. Заодно исправлен смысл: удалялись векторы, нужные ОСТАЛЬНЫМ
ревизиям. Теперь убираются только те, на которые не ссылается ни одна ревизия;
после правки строк vec0 21 703, сирот 0.

**Находка 4 — текст токенизировался дважды.** 42 556 вызовов на 21 тыс. чанков,
4,68 с в профиле; половина работы была лишней (в `symbol_tokens` и `code_tokens`
попадало одно и то же значение). Теперь один вызов на чанк.

**Находка 5 — проверка модели платилась всегда.** `embedder.status()` грузит ONNX
(замер в чистом процессе: 1,57 с) и вызывался даже когда кодировать было нечего.

**Решение по ротации — пакет не экономит.** Удаление данных одной ревизии стоит
**4,45 с** (29 запросов `DELETE` по ~100 тыс. строк: 81 тыс. рёбер и 21 тыс.
чанков с FTS-строками). Пачка из трёх ревизий стоит **18,0 с**, то есть
N × 4,45 с — амортизации нет, зато появляется прогон на 34 с (замер: 33,9 с
против 26,2 с на том же корпусе). Поэтому умолчание `rotate_batch: 1` — удалять
сразу, ровный график; значение больше единицы осталось настройкой. Цена
удержания названа объёмом: **≈0,6 ГБ на ревизию** (lexvec 443 МБ + graph 170 МБ),
`retain_revs: 3` = 1,8 ГБ при пороге предупреждения 6 ГБ.

**Решение по GPU — замером.** Процессор: 1,83 мс/текст (512 текстов за 0,94 с),
карта: 0,23–0,27 мс/текст, но подготовка 17,8–29,9 с в двух прогонах. Точка
равенства — **11 113** текстов при подготовке 17,8 с и **19 185** при 29,9 с.
Быстрая переиндексация кодирует 0 текстов, полная — около 14,5 тыс., то есть
ниже порога: на этой машине карта проигрывает по времени. `gpu_min_texts` этого
корпуса поднят с 1800 до **18 000**; глобальное умолчание не тронуто — оно
относится к машинам без замера.

**Находка 6 (была находкой 2 прошлой сессии) — брошенные ревизии.**
В журнале было **9** записей `state=building` (rev-0519 от 28.10 … rev-1112) без
событий в `event_log`. Закрывать «все `building` старше суток» нельзя: рядом
может идти живой прогон, и второй закрыл бы чужую работу. Поэтому в записи
появился признак живости `heartbeat_at` (обновляется на каждой стадии), а
закрытие требует, чтобы он устарел (`index.stale_revision_seconds`, умолчание
900 с) — при этом `UPDATE` условный, по `state='building'`, поэтому одна запись
не закрывается дважды. Замер на живом журнале: 9 записей закрыты, событий
`rev_closed_stale` — 9, `building` — 0. Свойства: `tests/test_stale_revision.py`
(12 проверок, подсадка «убрать проверку простоя» даёт 5 провалов).

**Находка 7 (была находкой 7 прошлой сессии) — набор не печатал числа.**
`tests/test_drift_e2e.py` не сообщал, сколько проверок выполнил, поэтому 14
проверок и три выглядели одинаково. Теперь печатает итог (**27 проверок,
0 провалов**) и сверяет байты живого `catalog/catalog.json` до и после прогона:
изменение живого реестра — провал набора, а не наблюдение.

**Итог замера.**

| Что | До | После |
|---|---|---|
| быстрая переиндексация, всего | 35,6 с | **25,4 с** |
| стадия `lexvec` | 16,18 с | **5,84 с** |
| стадия `rotate` (видна впервые) | не измерялась | 4,68 с |
| сумма стадий | 29,3 с (без seal/rotate) | 22,6 с (все стадии) |
| пять прогонов подряд | — | 31,2 / 25,6 / 25,8 / 25,0 / 25,4 с |
| строк vec0 | 41 356, из них 19 709 сирот | 21 703, сирот 0 |

Критерий «≤ 30 с» выполнен по числу прогона (25,4 с). Настенное время
`ragraf ensure .` — 29,8–30,5 с: перед решением о переиндексации `ensure` трижды
считает сумму корпуса (≈4,4 с на 938 файлах), и эта работа в прогон не входит.
Остаток назван числом, а не спрятан.

**Границы, названные честно.** Соединение с `vec_chunk` запрещено свойством
только в `lexvec.py`: другие модули (`quality.py`, `storage.py`) читают vec0
иначе и в горячий путь индексации не входят. `rotate_batch > 1` проверен
замером, но оставлен выключенным. Порог GPU поднят только для корпуса ragraf.
Механизм закрытия брошенных ревизий опирается на время, а не на живость
процесса: прогон, который молчит дольше 900 с (например, завис в Joern), будет
закрыт — поэтому порог и вынесен в настройку.

**Следующий этап — 13 (польза для агента).**

## 16. Этап 14 — закрыт 01.11.2026: безопасность и границы

**Предмет.** Три отрицательных утверждения: ни один секрет не попадает в ответ
операции; аудит показывает каждую выдачу; права MCP ограничены и проверены
отказом. Разбор с числами и командами —
`docs/03_implementation/SECURITY-BOUNDARIES.md`.

**Измерено.** Значения 128 записей хранилища (3 короче 8 символов исключены как
ложные совпадения) искались в 20 ответах операций чтения, 65 509 текстах чанков,
88 160 именах узлов, 55 063 сигнатурах, 67 106 путях и записи карты
секретоносных мест: **совпадений 0**. Аудит: три выдачи — 450 → 453 строки с
причиной, выдача без причины — отказ `reason_required` и строка `denied`
(453 → 454).

**Найденный дефект.** Уровень выданного доступа записывался в журнал, но не
проверялся нигде: `ragraf-mcp-http.py` передавал серверу только метку
(`access_label`), поэтому доступ `{read}` мог вызвать `ragraf_secret_set` и
`ragraf_secret_delete`. Исправлено: таблица «инструмент → уровень»
(`access.py`), проверка в `mcp_server._call_tool` ДО обработчика, передача
`principal.scopes` из транспорта. Замер матрицы `read`/`write`/`admin`/`None` и
подсадка — в отчёте.

**Свойства.** `tests/test_secret_isolation.py` (30 проверок, герметичен:
хранилище и реестр в `%TEMP%`), группа `core` раннера. Подсадки: снять проверку
уровня — 4 провала; вернуть значение в `show` — 3 провала.

**Находки, не исправления.** `op_freshness` на корпусе без индекса отвечает
`OperationalError` вместо «индекса нет»; локальный CLI всегда получает права
владельца (так задумано, граница названа); проба прав оставила три заметки
`probe` в живом журнале — удалены после проверки в песочнице.

## 17. Этап 15 — закрыт 01.11.2026: издания и установка с нуля

**Предмет.** Три критерия: установка на чистой машине по документу без ручных
шагов; обновление без потери индекса; лицензионный гейт отказывает Community на
Pro-операции. Разбор с числами и протоколом —
`docs/03_implementation/INSTALL-FROM-SCRATCH.md`.

**Чистая машина** (тестовый Linux-хост, Debian 12, аккаунт без sudo, системный
Python 3.11.2, свежий venv, Python 3.13.18 из python-build-standalone). Путь
`venv + pip`, который README называл непроверенным, сначала не работал по
четырём причинам, все измерены:

1. `ragraf init` создавал конфигурацию в `venv/lib/python3.13/config/` — то есть
   реестр, настройки, секреты, лицензия и конфиги корпусов жили ВНУТРИ каталога
   интерпретатора (`paths.REPO = parents[2]`), и обновление venv уносило их;
2. `ragraf ensure` висел 10+ минут без вывода: тянул модель эмбеддингов, а флага
   «без векторов» у `ensure` не было;
3. в колесе была одна команда `ragraf`: `tools/` в пакет не попадает, MCP-сервер
   и панель на установке запускать было нечем;
4. лицензия искалась в `REPO/catalog/edition.license`, то есть в venv — платное
   издание не включилось бы никогда.

**Исправлено.** Три явных уровня пути состояния (переменная → рабочая копия →
`~/.ragraf`), тем же правилом переведены `edition.py`, `admin_server.py`,
`client_setup.py`, `attest.py`; `ensure --no-vectors` с проведением значения до
`Indexer.run` и строкой о загрузке модели; команды `ragraf-mcp` и `ragraf-admin`
в пакет.

**Замеры после правки.** `init` → `~/.ragraf/config/proj.yaml`; `ensure
--no-vectors` → корпус создан за 0,44 с, поиск по совпадающему слову находит
файл; `ragraf-mcp` из колеса отвечает на `initialize` (69 инструментов);
Community отказывает `ragraf_siem_export` строкой
`EditionRefusal … появляется в издании enterprise`, а на машине с лицензией
Enterprise та же операция разрешена. Обновление 0.5.0 → 0.5.6 (сборки 0.6.0 нет,
артефакт 0.5.0 реконструирован) сохраняет реестр, ревизию и поиск.

**Свойства.** `tests/test_install_layout.py` (15) и
`tests/test_install_from_scratch.py` (15), группа `core`. Подсадки: вернуть
`REPO/"catalog"` — 5 провалов; убрать `with_vectors` — 2; убрать `ragraf-admin`
из `[project.scripts]` — 1.

**Осталось за границей.** Предел ожидания загрузки модели не введён (нужен
watchdog над сетевой библиотекой); HTTP-транспорт MCP в колесо не входит; macOS
не замерялся; настоящего релиза 0.5.0 нет, поэтому обновление измерено на
сборке с тем же кодом и номером 0.5.0 в метаданных.

**Открытый вопрос этапа 14 закрыт (01.11.2026).** Вопрос был: почему быстрый
`ensure` иногда собирает ревизию без живого слоя баз (rev-1193, rev-1195,
rev-1204, позже rev-1221). Ответ замерен: живой доступ берётся из каталога
состояния, и если он отведён переменной `RAGRAF_CATALOG_DIR` (песочница, второй
профиль), то `db_access.json` там нет — доступ оказывается «выключен» не
решением, а окружением. Узнать это было нельзя: причина возвращалась в
`report.databases`, а стадия `databases` писалась без примечания, поэтому
`index_status` показывал `items=0` без объяснения и «слой пуст» было не отличить
от «сбор выключен». Теперь причина идёт в `stage_timing.note`, остаётся в записи
ревизии, а найденный механизм назван прямо. Свойство —
`tests/test_db_layer_visibility.py` (15 проверок, группа `core`); подсадка:
перестать передавать причину — 3 провала.

**Повторный замер приёмки (01.11.2026, тот же хост, свежие колёса).** Установка:
`init` → `…/state/config/proj.yaml`, `ensure --no-vectors` 0,30 с, поиск
находит, `ragraf-mcp` из колеса отвечает (`tools 69`). Гейт: `EditionRefusal` с
названием нужного издания. Обновление 0.5.0 → 0.5.6: та же ревизия `rev-0001`,
реестр на месте, поиск без переиндексации. Две ловушки воспроизведения
(существующий каталог проекта, маркер проекта и три файла кода) и найденный
дефект MCP — пустые `annotations` у `ragraf_compact`/`ragraf_excludes`, из-за
чего клиент не отличал чтение от записи, — описаны в
`docs/03_implementation/INSTALL-FROM-SCRATCH.md` §6.

## 18. Этап 1 плана агентских интеграций — фундамент LLM-слоя (01.11.2026)

**Что сделано.** `src/ragraf/llm.py` (единственный клиент: провайдер по настройке, ключ
только из хранилища, три состояния, отказ кодом, кэш, счётчик вызовов),
`src/ragraf/docgraph.py` (отпечаток слоя, схема шести таблиц, проверка целостности, две
команды состояния), двенадцать настроек `llm_*`/`docgraph_*`, запись отпечатка в
`rev.json` и в `meta` журнала, новый вид записи `docgraph_fingerprint` в журнале
расхождений, подкоманда `docgraph` (24-я).

**Чем проверено.** `tests/test_docgraph_foundation.py` — 70 проверок, группа `core`:
нет LLM — названная причина и код вместо пустого ответа; отпечаток устойчив и меняется
по делу; повторный вызов не идёт к провайдеру; факт без `entity_mention` отвергается, и
это ловится даже на сыром `INSERT`; ключ не попадает ни в ответ, ни в состояние, ни в
файл настроек, ни в текст ошибки. Подсадки по одному утверждению: 6, 4, 5, 4 и 2
провала — каждое утверждение ловится.

**Что осталось за границей.** Извлечение сущностей и отношений (этап 2), операции
чтения с гейтом изданий (этап 3), dual-level поиск (этап 4), темы и сводки (этап 5).
`llm.py` не вызывается индексатором: подключение — этап 2 вместе с `--doc-graph`.
Протокол с числами — `docs/03_implementation/DOCGRAPH-FOUNDATION.md`.

## 19. Этап 2 — извлечение фактов из документов (01.11.2026)

**Что сделано.** `src/ragraf/docgraph_extract.py`: промпт извлечения (строгий JSON,
запрет придумывать термины), разбор ответа, проверка адресов, запись слоя одной
транзакцией. Флаги `--doc-graph` у `index` и `reindex`, стадия `docgraph` в
индексаторе, исполняющий `ragraf docgraph build`. У писателей слоя появились
`commit` и `with_schema`: без них вызов `ensure_schema` внутри записи фиксировал
транзакцию и оставлял полузаписанный слой при отказе.

**Чем проверено.** `tests/test_docgraph_extract.py` — 31 проверка, группа `core`:
повторный прогон не делает ни одного платного обращения и оставляет слой побайтово
тем же; цитата вне чанка отвергается кодом; смена версии промпта инвалидирует кэш;
отказ провайдера на середине не оставляет полузаписанного слоя; ключ не попадает
ни в ответ, ни в слой, ни в файл настроек. Подсадки: 4, 3, 2, 2 и 2 провала.

**Что осталось за границей.** Операции чтения с гейтом изданий (этап 3), dual-level
поиск (этап 4), темы и сводки (этап 5), интеграции части A (этап 6). Проход пишет в
активную ревизию: инвариант иммутабельности касается графа кода, дополнительные
слои пересобираются повторным проходом.

## 20. Этап 3 — операции чтения документного графа и гейт изданий (01.11.2026)

**Что сделано.** `src/ragraf/docgraph_ops.py`: `entity_neighbors` (обход в ширину до
трёх шагов, направление и промежуточное звено у каждого соседа) и `doc_relations`
(пары документов через общие сущности). В фасаде — два `op_*`, в CLI — две ветви
`query`, в MCP — два инструмента с описаниями и обработчиками. Возможность
`doc-graph` (Pro) объявлена в базовой линии изданий; чтение закрыто общей обёрткой
фасада, проход — новой `edition.feature_denial()` в обеих точках запуска.

**Чем проверено.** `tests/test_docgraph_ops.py` — 63 проверки: адрес у каждого факта
и адреса с обеих сторон, `rejected_no_citation` на испорченном слое, отказ изданий
под Community и исполнение под разрешённой возможностью, пределы с кодом вызова,
совпадение `TOOLS`/`_TOOL_META`/`_handlers`, закрытый проход. Подсадки: 1, 7, 8, 1,
1 и 3 провала.

**Что осталось за границей.** Темы и сводки (этап 5), dual-level поиск (этап 4),
операции записи в слой, интеграции части A (этап 6).

## 21. Этап 3.1 — числа навыка под сверку (01.11.2026)

**Что было не так.** Навык ядра заявлял «39 операций / 41 инструмент» при 64 и 71, а
`reference/repair.md` — «33 проверки» при 45. Правило чисел в
`tools/check_skill_variants.py` применялось только к копиям файлов с режимом
`declared`; `operations.md` и `repair.md` объявлены `identical`, поэтому
сравнивались только хеши копий — совпадать они могли и в неверном числе. Вдобавок
шаблон числа не видел формы «проверок»: после 5 и больше по-русски пишут именно так,
и строка «45 проверок» считалась отсутствием заявления.

**Что сделано.** Блок `source_rules` в `VARIANTS.json` — правила для самого файла
навыка; вид правила `bold_numbers_match_code` для текста, где рядом живёт история;
шаблон числа переведён на основу слова; у проверки появился `--root`. Тексты
навыка обновлены (64 операции, 71 инструмент, 45 проверок), копии синхронизированы,
пара sha256 варианта HARNESS пересобрана, `operations.md` дополнен строками
документного слоя и документного графа.

**Чем проверено.** `tests/test_skill_numbers.py` — 19 проверок на песочнице:
порча числа в источнике, в копии, форма «проверок», история против утверждения,
`--root` на другом дереве. Подсадки: 11, 1, 3 и 2 провала.

**Что осталось.** Числа без выделения в `declared`-файлах не проверяются: историю
машина не отличает от утверждения. Это названо в контексте, а не скрыто.

## 22. Этап 4 плана «Агентские интеграции»: dual-level поиск (01.11.2026)

Закрыт этап 4 `docs/00_plan/PLAN-AGENT-INTEGRATIONS.md` (§3.4): `search` получает
`mode` = `auto` | `code` | `docs` | `themes`, уровень «тема» строится по сущностям
документного графа, деградация названа полем.

Сделано: `src/ragraf/duallevel.py` (ключи запроса промптом и кэшем, сопоставление с
сущностями, адреса упоминаний, пять кодов деградации); фасад — `_apply_levels` и ветви
режимов (`code` фильтрует материал, `docs`/`themes` включают уровни); CLI `--mode`;
MCP `ragraf_search.mode`; документ `docs/03_implementation/DOCGRAPH-SEARCH.md`;
набор `tests/test_docgraph_search.py` (81 проверки) в группе `core`.

Главное свойство, которое закреплено: **уровень «тема» только добавляет**. Лексическая
выдача, её оценки и порядок остаются прежними — иначе `search` перестал бы быть
сравним с замерами пользы, на которых держатся числа проекта.

Находка: первая редакция набора охраняла функции модуля, а не собранный ответ —
подсадка «пустой адрес» прошла с 0 провалов. Добавлен фасадный раздел с подменой
транспорта; после этого подсадка ловится (замер: 1 провал). Утверждение о шести
подсадках снято как непроверенное — см. поправку в §23.

Вторая находка — порядок проверок: `themes` упирался в проверку модели, хотя темы
читаются из слоя. Ветвь перенесена выше, свойство закреплено проверкой набора.

Числа поверхности на момент этой записи не изменились: 64 операции, 71 инструмент,
24 подкоманды. Вторая часть этапа 4 (§23) добавила операцию: 65 и 72.
## 23. Этап 4, вторая часть: два уровня одним ответом (02.11.2026)

Запись §22 описывает `mode` у `search` — «плоскую» выдачу, где уровень темы только
добавляет хиты. Отдельная задача этапа 4 — показать оба уровня **как уровни**, со
связью между ними: локальные факты (чанк с адресом `path:line`) и глобальный
уровень (сущности и связи документного графа, в которые эти чанки входят).

**Форма: отдельная операция, а не ещё один режим `search`.** У `search` контракт
плоского списка: на него опираются прежние замеры пользы, `answer=true`, пометки
устаревания и материал хитов. Двухуровневый ответ — другая форма, и внутри старого
контракта она сломала бы и сравнение, и клиентов. Поэтому `search` не менялся, а
уровни живут операцией `doc_search_levels` с тем же движком (`lexvec.search_hybrid`)
и тем же источником адресов (`entity_mention`).

**Направление уровней — отличие от `mode=docs`.** Там сопоставление шло от ключей
запроса к сущностям («что ещё похоже»). Здесь — от локальных фактов к глобальному
уровню («во что входит найденное»): вход — `chunk_id` локальных хитов, выход —
сущности и связи с адресами.

Сделано: `duallevel.facts_of_chunks` (сущности и связи по чанкам, факт без адреса
отбрасывается и считается в `rejected_no_citation`), `duallevel.describe_global_level`
(причины неполноты словами), `facade.op_doc_search_levels` (`mode` = `local` |
`global` | `both`, по умолчанию `both`), поверхности CLI (`cli.py`), MCP
(`ragraf_doc_search_levels` в `TOOLS`, `_TOOL_META`, `_handlers`), базовая линия
изданий (операция в возможности `search`, community), README и пять переводов,
`docs/AGENTS-context.md`, `docs/03_implementation/DOCGRAPH-SEARCH.md` §9.

**Правила, которые держат ответ.** Адрес у каждого факта (локальный — `locator`,
глобальный — `citations` и `via_chunk.locator`, связь — `citations`); связь уровней
названа явно (`via_entities` у хита, `via_chunk` у глобального факта, счётчики в
`global.links`); гейт стоит только на глобальном уровне (локальный работает под
любым изданием — правило 2 документа изданий); пустота названа кодом, а не пустым
списком: `layer_empty`, `no_facts_in_chunks`, `edition`, `no_local_facts`. В режиме
`global` пустой слой даёт `found=false` и причину, но **не** `error`.

**Числа поверхности: 64 → 65 операций, 71 → 72 инструмента**, подкоманд 24,
возможностей изданий 16 (новая возможность не объявлялась: глобальный уровень
закрыт существующей `doc-graph`). Свойство `tests/test_docgraph_search.py`
расширено с 42 до 81 проверки (разделы 8–11: адреса обоих уровней и явные связи;
пустой слой против «связей нет»; гейт на глобальном уровне; поверхности).

**Подсадки — четыре обязательные**, `stage-docgraph/mutations4.py` (работает на
копии дерева в `%TEMP%`, живые исходники не меняются, sha256 сверяется после
прогона): адрес глобального факта снят — 2 провала; уровни склеены без `via_chunk` —
3; пустой слой выдан за «связей нет» — 3; глобальный уровень открыт без издания — 3.

**Поправка к §22.** В записи стояло «все 6 подсадок ловятся (1, 9, 1, 2, 2, 1)», но
файла с шестью подсадками в дереве не оказалось, и замер не воспроизводился.
Исполняемый набор теперь один — `stage-docgraph/mutations4.py`, числа выше измерены
им. Утверждение о шести снято как непроверенное, а не переписано «на глаз».

**Ворота на сдаче.** Ядро дважды подряд: 72 набора, 2180 проверок, 0 провалов
(1452,2 с и 1450,3 с); чекер 45/0/0 PASS; `check_line_refs` 225 ссылок / 0 неверных
(12 адресов перенесены по замеру — вставки в `facade.py`, `cli.py`, `mcp_server.py`
сдвинули строки); `check_skill_variants` 27 объектов / 0 находок; BOM/CRLF чисто;
`test_documented_numbers` 62/0; объявления таблиц обновлены (84 строки / 199 ячеек).
`tests/test_skill_numbers.py` перестал держать числа литералом: он читает их из
текста навыка, иначе падал бы на каждой верной правке.

## 24. Этап 5 плана «Агентские интеграции»: темы и карта графа (02.11.2026)

Закрыт этап 5 `docs/00_plan/PLAN-AGENT-INTEGRATIONS.md` (§3.5, §3.6): темы
документного графа, сводки по цитатам и карта графа.

**Кластеризация детерминированная.** Граф со-встречаемости (ребро — совместное
упоминание в одном чанке, вес — число чанков), порог `min_weight` (2), компоненты
связности, разрез крупной компоненты по порядку силы. `theme_id` считается той же
функцией, что при записи, — `sha1(seed + отсортированные участники)`: одинаковый
слой даёт одинаковые темы на любой машине и в любой ревизии. Leiden не берём: он
недетерминирован, а нам нужна сравнимость между ревизиями.

**Темы бесплатны, сводка — нет.** Кластеры строятся в том же проходе, что факты
(`docgraph build`, `index --doc-graph`), и пересобираются командой
`ragraf docgraph themes` без сети. Сводка — обращение к модели на тему
(`--summaries`), поэтому она отдельная: без модели тема на месте, `summary=null`,
причина названа кодом `llm_unavailable`.

**Сводка без цитат не выдаётся.** Ответ модели разбирается строго: пустая сводка,
пустые цитаты и цитата вне списка участников — отказ разбора, запись не делается.
Второй барьер на чтении: строка с пустыми цитатами, попавшая в хранилище в обход
разбора, не показывается.

**Две операции.** `themes` — кластеры с участниками, адресами и сводкой;
`global_overview` — карта: документы, сущности, связи, темы, сводки и топ-темы с
адресами. Обе объявлены возможностью `doc-graph` (pro) и под Community отвечают
`EditionRefusal` с названием издания.

**Числа поверхности: 65 → 67 операций, 72 → 74 инструмента.** Свойство
`tests/test_docgraph_themes.py` — 40 проверок (вычислимость и стабильность
`theme_id`, адреса участников, сводки только с цитатами, различие «тем нет» и
«слой не собран», гейт и поверхности, область `scope`, команда пересборки).

**Подсадки — четыре**, `stage-docgraph/mutations5.py` (копия дерева в `%TEMP%`,
живые исходники не меняются): `theme_id` не по формуле — 1 провал; тема без
участников выдаётся — 1; сводка без цитат выдаётся из хранилища — 1; гейт снят
(возможность объявлена community) — 5.

**Ворота на сдаче.** Порядок тот же, что у предыдущих этапов: ядро дважды подряд,
чекер 45/0/0, ссылки «файл:строка» без неверных, числа поверхности сверены с кодом
(`surface_counts.py`), BOM/CRLF чисто. Числа прогона называются в отчёте этапа, а
не в записи плана: запись не должна переживать замер.

## 25. Этап 6 плана «Агентские интеграции», часть A: одиннадцать продуктов (02.11.2026)

Часть A — артефакты по одиннадцати клиентам матрицы §2.2. Задача этапа не «описать
поддержку», а сделать поддержку проверяемой: шаблон конфигурации разбирается тем же
разборщиком, что продукт, секрет в шаблоне — находка, заявленный смоук существует и
возвращает код, а таблица уровней в документе сверяется с объявлением.

**Что заведено.** `integrations/index.json` (объявление: слаг, транспорт, файл
конфигурации, шаблон, уровни, смоук либо чек-лист), одиннадцать каталогов
`integrations/<slug>/` с шаблоном, README (разделы «Подключение», «Проверка»,
«Уровни», «Границы») и либо `smoke.ps1`, либо `checklist.md`.
Матрица и уровни — `docs/03_implementation/AGENT-INTEGRATIONS.md`.

**Уровни названы честно.** L1 (шаблон), L3 (проверка в чекере), L4 (границы) есть у
всех одиннадцати. L2 (смоук, доказывающий, что клиент вызывает RAGRAF и приводит
`path:line`) — у шести: `openclaw`, `hermes`, `claude-code`, `codex-cli`, `opencode`,
`pi`. У пяти GUI-клиентов (`cline`, `deepseek-harness`, `autoclaw`, `cursor`, `manus`)
смоука нет: клиент не запускается без графического сеанса, и вместо галочки лежит
чек-лист из пяти шагов. Это L4, а не L2, и в документе так и записано.

**L2 не измерен ни у одного продукта — и это записано, а не замаскировано.**
Замер на этой машине 02.11.2026: Claude Code 2.1.316 отвечает `Not logged in ·
Please run /login`; OpenClaw 2026.10.5 — `gateway agent requires credentials before
opening a websocket`; Codex CLI 0.166.0 — `Not inside a trusted directory and
--skip-git-repo-check was not specified`; Hermes, OpenCode и Pi ждут интерактивного
ввода и в headless-прогоне не завершаются. Смоуки возвращают код 3 («клиент не
готов: нет входа, ключа или доверия к каталогу») и называют причину. Полный L2
требует авторизованного клиента на стенде 192.168.31.103 — на машине с живым
индексом клиентов «для проверки» не ставим (решение владельца).

**Проверка — новый инструмент.** `tools/check_integrations.py`: шесть проверок
(объявление разбирается и полно; каталог есть у каждого продукта и лишних нет;
README с тремя разделами; шаблон разбирается, называет `ragraf` и не содержит
секретов; у каждого продукта есть смоук либо чек-лист; таблица уровней совпадает
с объявлением). Коды возврата 0/1/2, флаги `--templates`, `--json`, `--root`.

**46-я проверка чекера.** `Test-AgentIntegrations` в `tools/Invoke-ProjectDoctrine.ps1`
вызывает инструмент с `--json` и падает, если тот не вернул JSON, вернул находки,
осмотрел меньше шести проверок или ноль (ложный PASS). Фильтр `-Only AgentIntegrations`
проверен отдельно: одна проверка, `errors: 0`.

**Свойство.** `tests/test_agent_integrations.py` — 18 проверок: живое дерево
(11 продуктов, слаги без повторов, README у каждого, уровни из закрытого перечня,
L2 без смоука невозможен, где смоука нет — чек-лист) и песочница с семью подсадками.

**Подсадки — четыре, все на инструменте** (`stage-docgraph/mutations6.py`): отключён
поиск секретов в шаблоне — 1 провал; отключена сверка таблицы уровней — 1; не
проверяется разбор шаблона — 1; снята связка «L2 → смоук» — 1. Подсадки портят сам
инструмент, и набор обязан это заметить: иначе проверка, которая всегда зелёная,
ничего не проверяет. Живые исходники после прогона сверены по sha256.

**Найденный по ходу дефект.** Функция `check_index` (правила объявления: дубли
слагов, закрытый перечень уровней, связка «L2 → смоук») была написана, но нигде не
вызывалась — то есть правила существовали как мёртвый код. Поймала это подсадка на
связку L2: набор ждал провала и не получил его. Функция подключена к проверке
объявления.

**Числа.** Операций 67, инструментов 74, подкоманд CLI 24, языков 29/40, форматов 84,
возможностей 16 — поверхность не менялась: этап добавляет артефакты и проверку, а не
операции. Чекер 45 → **46** проверок; копии навыка (`repair.md`, строка о baseline)
синхронизированы, `check_skill_variants.py` — 27 объектов, 0 находок. Набор
`tests/test_skill_numbers.py` перестал держать число проверок литералом: он читает его
из текста навыка (иначе падал на каждой верной правке — так и вышло на 46).
