# Отчёт о реализации: M1–M2 (каркас и граф PHP)

**Дата:** 2026-10-04 · **Пилот:** `rev oap.com` (392 файла, 323 PHP, 126 386 строк корпуса)
**Ревизия индекса:** `rev-0012` · **Тесты:** сквозной сценарий пройден целиком

---

## 1. Что реализовано

### 1.1 Каркас (M1)

| Компонент | Файл | Что делает |
|---|---|---|
| Конфигурация корпуса | `src/ragraf/config.py` | Валидация, пути, хэш конфигурации, политика ревизий |
| Три хранилища | `src/ragraf/store.py` | Схема SQLite: граф (`graph.sqlite`), лексика+вектор (`lexvec.sqlite`), контроль (`control.sqlite`) |
| Скан корпуса | `src/ragraf/corpus.py` | Отбор файлов, исключения, `sha256`, счёт строк, хэш корпуса |
| Ревизии | `src/ragraf/revisions.py` | Создание, запечатывание, ротация (история не удаляется), запись графа |
| Стадии конвейера | `src/ragraf/indexer.py` | scan → ast → load → resolve → lexvec → seal |
| Версии инструментов | `src/ragraf/pipeline.py` | Каждая ревизия хранит версии всех экстракторов (ФТ-10) |
| CLI | `src/ragraf/cli.py` | `index`, `reindex`, `drift`, `query`, `status`, `drop`, `selftest` |
| Обёртка PowerShell 7.6 | `scripts/ragraf.ps1` | `#Requires`, utf8NoBOM, без WinPS-compat |

### 1.2 Граф PHP (M2)

Разбор через `tree-sitter`: узлы 8 видов, 12 видов рёбер, провенанс у каждого ребра.

| Что извлекается | Как | Проверено |
|---|---|---|
| file / namespace / class / interface / trait / enum | объявления tree-sitter | 3 861 узел на пилоте |
| function / method / property / constant | объявления с `body_sha` | методы привязаны к своим классам (0 ошибок) |
| `contains`, `defines`, `imports`, `extends`, `instantiates` | синтаксис | 1 280 `imports`, 1 678 `instantiates` |
| `calls` (доказанные) | вывод типов свойств + алиасы `use` + разрешение отложенных имён | 13 890 |
| `calls_builtin` | встроенные функции PHP отделены от «потерянных» связей | 13 094 |
| `calls_heuristic` (эвристика) | динамические вызовы, `call_user_func`, методы без вывода типа | 4 413 |
| `queries` → таблицы из SQL-литералов | разбор строк, дедупликация | 212 |
| `reads_superglobal` | `$_GET/$_POST/...` | 433 |
| Текстовый слой (`text_chunk`) | комментарии и докблоки — **отдельно** от структурных фактов | 55 000+ чанков |

### 1.3 Фасад (M5)

17 операций с единым конвертом (`index_rev`, `capability`, `drift`, `provenance`, `limits`, `stale_warnings`). Реализованы: `search`, `symbol`, `references`, `callers`, `callees`, `impact`, `slice`, `code`, `repo_map`, `schema`, `table_usage`, `routes`, `index_status`, `drift_report`, `drift_ack`, `live_read`, `calibrate`.

`slice` и `flow` в режиме `degraded` **честно отказывают** с указанием причины, а не возвращают пустоту (ФТ-12).

### 1.4 Лексико-векторный поиск (M4)

| Слой | Технология | Состояние |
|---|---|---|
| Лексика | SQLite FTS5, BM25, токенизация с разбором camelCase | 3 120 чанков |
| Вектор | `sqlite-vec` `vec0`, 384 измерения | 3 072 вектора |
| Модель | `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` (ONNX, без torch) | скачана, 0,22 ГБ |
| Слияние | RRF: лексика + вектор | работает |
| Переиспользование | по хэшу текста чанка | **2 965 из 3 000** |

### 1.5 Журнал расхождений (M5)

Реализован целиком и проверен сквозным тестом: обнаружение дельт (по `sha256` файла и `body_sha` символа), дедупликация, пометка `stale`, запись `slice_boundary` при выносе кода в метод, `reindex --commit` с архивацией и человекочитаемым отчётом, `drift_ack` без удаления записей.

---

## 2. Замеры на пилоте

### 2.1 M7 — стоимость индексации и обновления

| Показатель | Значение | Порог НФТ | Вердикт |
|---|---|---|---|
| Первая индексация (с построением 3 000 эмбеддингов) | **75,0 с** | ≤ 15 мин | ✅ **в 12 раз быстрее порога** |
| Индексация без векторов | **8,8 с** | — | — |
| Повторная индексация (векторы переиспользованы) | **8,8 с** | ≤ 3 мин | ✅ **в 20 раз быстрее** |
| Стадии без векторов | scan 0,51 · ast 1,89 · load 0,62 · resolve 1,02 · lexvec 3,2 | — | — |
| Всего рёбер | 39 135 (из них 15 419 доказанных) | — | — |
| Файлов корпуса | 392 (323 PHP разобраны, 0 ошибок разбора, 0 частичных) | — | — |

**Вывод:** самая дорогая часть — эмбеддинги (69 с из 75). Всё остальное — секунды. Граф PHP строится за 2 с на 126 тыс. строк.

### 2.2 M2 — связность соизменённых файлов

**Методика (важная оговорка).** Задача строго «предсказать impact до правки» требует построения индекса на состоянии перед каждым из 103 коммитов. На первом проходе измеряется более слабое, но честно называемое свойство: **для коммита, изменившего несколько PHP-файлов, проверяется, какие из них достижимы от главного изменённого символа в графе** (обход в оба направления, глубина 3).

Артефакт: `bench/results/M2_rev-0012.json` · скрипт: `tools/bench_m2.py`

| Показатель | Значение |
|---|---|
| Проанализировано коммитов | 20 |
| **Recall (среднее)** | **0,771** |
| **Recall (медиана)** | **1,000** |
| Precision (среднее) | 0,071 |
| Baseline «затронут только сам файл» | 0,000 |
| Коммитов с полной связностью (**все** соизменённые файлы достижимы) | **11 из 20** |
| Коммитов с нулевой связностью | 1 из 20 |

**Как читать цифры честно:**

- **Recall 0,771 при медиане 1,000** — в половине коммитов и более все соизменённые файлы связаны в графе структурно; среднее занижают отдельные случаи. Baseline 0,000 означает: наивный подход «затронут только тот файл, где символ» не находит **ни одного** из остальных файлов. То есть структурные связи дают заметную добавку.
- **Precision 0,071 намеренно не показателен** для этой метрики: при обходе в оба направления на глубину 3 достижимо много файлов, и «доля достижимых, которые реально менялись» мало что значит. Ключевая цифра здесь — recall. Для настоящей precision нужен расчёт по каждому коммиту с отдельным индексом (следующий шаг).
- **Худшие случаи — конструкторы.** Из 9 коммитов с неполной связностью в 4 главным символом был `__construct`. Через внедрение зависимостей изменения конструктора расходятся по всей цепочке инициализации, а граф видит только прямые связи. Это конкретное, воспроизводимое узкое место, а не общая «неточность».
- **Не связанными чаще всего оказываются** точки входа (`index.php`), конфигурационные файлы (`config.php`) и файлы, подключённые через динамические механизмы.

**Ориентир из литературы:** измеренный потолок для предсказания затронутых артефактов — precision 69% / recall 79% / F1 55% (LCIP, `arXiv:1512.07435`, Java, симуляция мутациями). Наш recall 0,771 близок к этому потолку, но это **разные задачи** и разные языки: сравнение допустимо только как порядок величины, что и указано в артефакте.

### 2.3 M4 — сжатие контекста (частично)

| Показатель | Значение |
|---|---|
| Чанков по символам | 3 120 |
| Символов-гигантов, нарезанных окнами | есть (файл на 9 222 строки, метод на 1 217 строк) |
| `repo_map` в бюджете 2 000 токенов | укладывается (проверено) |
| Замер сжатия среза | **не выполнен** — требует CPG (`slice`), которая недоступна |

### 2.4 M5 — защита от выводов по устаревшим данным

Сквозной тест `tests/test_drift_e2e.py` — **все 20 проверок пройдены**:

| Проверка | Результат |
|---|---|
| Индексация на изолированном корпусе | ✅ |
| Секретоносный файл не попал в индекс (ФТ-39) | ✅ 0 совпадений значения |
| 25 операций чтения не изменили хранилища (ФТ-24) | ✅ хэши совпали |
| Навигация: символ, callers, callees, impact, таблица, `repo_map` | ✅ |
| Обнаружение правки (ФТ-25) | ✅ 1 запись |
| Дедупликация: повторный скан не задваивает журнал (ФТ-27) | ✅ 0 новых |
| **`slice_boundary` при выносе кода в метод (ФТ-29)** | ✅ с примечанием, какой метод создан |
| Пометка `stale` для изменённого файла (ФТ-28) | ✅ |
| `live_read` видит отличие от индекса (ФТ-31) | ✅ |
| `reindex --commit`: новая ревизия, архив с 3 разделами (ФТ-32/33) | ✅ |
| `drift_ack` не удаляет запись (ФТ-36) | ✅ |
| **Детерминированность `edge_uid` при повторной сборке (ФТ-13)** | ✅ множества совпали |

---

## 3. Дефекты, найденные и исправленные в ходе разработки

Все восемь обнаружены **не чтением кода, а прогоном** — это ответ на вопрос «как проверялось».

| № | Дефект | Как обнаружен | Исправление |
|---|---|---|---|
| 1 | Методы приписывались первому классу файла: стек областей видимости не закрывался | сверка числа методов класса с кодом | рекурсивный обход с явным входом/выходом |
| 2 | **Обход графа шёл в обратную сторону**: `callers` и `impact` возвращали вызываемых вместо вызывающих | сквозной тест на фикстуре | направление исправлено |
| 3 | `impact` возвращал 0 результатов | тест | то же исправление |
| 4 | **Неблочные `namespace X;` не распространялись** на следующие объявления → классы попадали в граф под короткими именами → терялись все межфайловые связи | диагностика `tools/probe_types.py` | контекст namespace в обходе |
| 5 | Динамические `$obj->method()` не связывались, хотя тип свойства известен из объявления | тест: «кто вызывает Config::get» = 0 | вывод типов свойств + алиасы `use` |
| 6 | Заглушки `@ext:` оставались после разрешения → рёбра задваивались | диагностика `tools/probe_graph.py` | замена заглушек разрешёнными |
| 7 | Изменение тела метода внутри тех же границ строк не детектировалось | тест: ожидался `slice_boundary`, получен `modify` | `body_sha` в свойствах символа |
| 8 | Отложенное разрешение вызова проверяло только первый кандидат имени | диагностика `tools/probe_slice.py` | перебор всех кандидатов |

**Отдельно — дефекты, которые сломали бы работу молча:**

| № | Дефект | Как обнаружен | Исправление |
|---|---|---|---|
| 9 | `bm25()` вызывалась с 4 весами вместо 7 → `OperationalError` → **поиск молча возвращал пусто** | сверка: в selftest FTS работал, в поиске нет | число весов приведено к числу колонок |
| 10 | Токенизация `split` рвала слова по буквам: `PipelineEngine` → `p ipeline e ngine` | диагностика поиска | `findall` + разбор camelCase |
| 11 | Расширение `vec0` не загружалось в хранилище лексики → **векторный слой молча отключался** | сверка с `selftest` | явная загрузка расширения до проверки |
| 12 | Фильтр по `rev_id` в `vec0` невозможен (колонки не хранятся) → счётчики векторов = 0 | диагностика | KNN по всем + проверка ревизии через `vec_meta` |

**Вывод по дефектам:** 4 из 12 (№ 9, 10, 11, 12) были «тихими» — код не падал, результат просто оказывался пустым. Такие дефекты ловятся только сверкой двух независимых путей (в данном случае — `selftest` против реального поиска).

---

## 4. Матрица требований ТЗ

Честная оценка: что сделано, что нет и почему.

| Требование | Состояние | Комментарий |
|---|---|---|
| ФТ-1 индексация по команде | ✅ | `ragraf index` |
| ФТ-2 границы символов | ✅ | проверено на 100+ символах пилота |
| ФТ-3 символьный слой (SCIP) | ❌ | не подключён; символы извлекаются tree-sitter |
| ФТ-4 разделение `calls` / `calls_heuristic` | ✅ | 13 890 доказанных, 4 413 эвристических |
| ФТ-5 схема БД | ⚠️ | схема из кода (`queries`), но `db_table` не заполняется: подключение к MySQL не включено |
| ФТ-6 SQL-литералы → таблицы | ✅ | 212 рёбер, колонки при `SELECT *` не выдумываются |
| ФТ-7 точки входа | ⚠️ | `reads_superglobal` есть; таблица `web_route` пуста |
| ФТ-8 документация | ❌ | Markdown попадает в корпус, но помечается `skipped` (вне релиза 1 по языкам) |
| ФТ-9 текст отделён от структуры | ✅ | проверено: 0 утечек комментариев в имена символов |
| ФТ-10 провенанс у каждого факта | ✅ | `evidence`, `confidence`, `sources`, версии экстракторов |
| ФТ-11 отчёт покрытия | ✅ | 16 метрик + покрытие по языкам |
| ФТ-12 режим деградации | ⚠️ | работает (весь пилот в `degraded`), но искусственное отключение Joern не тестировалось — Joern не установлен |
| ФТ-13 детерминированность | ✅ | протестировано: множества `edge_uid` совпали |
| ФТ-14 17 операций | ✅ | все реализованы; `slice`/`flow` отказывают в `degraded` |
| ФТ-15 отказ от языка запросов | ✅ | CLI отвергает `cypher`/`cpgql`/`match ... return` |
| ФТ-16 полнота конверта | ✅ | проверено на всех операциях |
| ФТ-17 `path:line` в фактах | ✅ | вывод содержит и путь, и диапазон |
| ФТ-18 разделение доказанных и эвристических | ✅ | `mode=hard` отбрасывает `derived` |
| ФТ-19 `slice` без тел функций | ✅ | тела только через `code` |
| ФТ-20 `repo_map` в бюджете | ✅ | проверено при 2 000 токенов |
| ФТ-21 честное «не найдено» | ✅ | флаг `found=false` + предложение `live_read` |
| ФТ-22 MCP-инструменты | ⚠️ | CLI готов; MCP-сервер не написан (следующий шаг) |
| ФТ-23 нейтральность формулировок | ⚠️ | тексты нейтральны, формальная ревизия 50 ответов не проводилась |
| ФТ-24 индекс неизменен в ревизии | ✅ | протестировано: хэши хранилищ не меняются |
| ФТ-25 пять способов обнаружения дельт | ⚠️ | реализованы `compare`, `manual`, `git-diff`, `db-schema-diff`; хук сессии — в MCP |
| ФТ-26 журнал append-only | ✅ | нет путей удаления; `drift_ack` дописывает |
| ФТ-27 дедупликация | ✅ | протестировано |
| ФТ-28 пометка `stale` | ✅ | автоматически для затронутых путей |
| ФТ-29 `slice_boundary` | ✅ | протестировано на реальном extract-method |
| ФТ-30 `schema_change` | ⚠️ | код есть, требуется подключённая БД для проверки |
| ФТ-31 `live_read` с отличиями | ✅ | протестировано |
| ФТ-32/33 переиндексация и архив | ✅ | архив с 3 обязательными разделами |
| ФТ-34 сохранение истории | ✅ | ротация удаляет данные, история остаётся |
| ФТ-35 запись только через фасад | ✅ | записи в журнал нет в операциях чтения |
| ФТ-36 `drift_ack` не удаляет | ✅ | протестировано |
| ФТ-37 конфигурация корпуса | ✅ | валидация, влияние на отбор |
| ФТ-38 исключения | ✅ | 128 исключённых каталогов, в индексе — 0 вендорных |
| ФТ-39/40 секреты | ✅ | протестировано: 3 файла исключены, значение не найдено |
| ФТ-41/42/43 срезы и потоки (CPG) | ❌ | **требуют Joern — не установлен** |
| НФТ-1 без администратора | ✅ | всё в пользовательском каталоге |
| НФТ-2 полная индексация ≤ 15 мин | ✅ | 75 с (в 12 раз быстрее) |
| НФТ-3 инкрементальная ≤ 3 мин | ✅ | 8,8 с |
| НФТ-4 быстрые чтения | ⚠️ | не измерены систематически (p95 не снимался) |
| НФТ-5 детерминированность | ✅ | протестировано |
| НФТ-6 удаление и восстановление | ⚠️ | `drop` есть; полное сравнение после пересборки не проводилось |
| НФТ-7 фиксация версий | ✅ | `requirements.txt`, `selftest` печатает версии |
| НФТ-8 лимит памяти CPG | ❌ | неприменимо без Joern |
| НФТ-9 правила PowerShell | ✅ | линтер: 0 замечаний; BOM/CRLF: 0 из 28 файлов |
| НФТ-10 изоляция артефактов | ✅ | всё в `<проект>/.ragraf/` |
| НФТ-11 восстановление после сбоя | ⚠️ | прежняя ревизия не портится (архитектура), но прерывание не тестировалось |
| НФТ-12 воспроизводимость ответа | ⚠️ | режим только чтения обеспечен, тест не написан |

**Итог по матрице:** ✅ 33 · ⚠️ 13 · ❌ 5 (из 51 позиции).

---

## 5. Что не сделано и почему

| Не сделано | Причина | Что нужно |
|---|---|---|
| **CPG: срезы, потоки данных, `slice`, `flow`, `flow`-замер сжатия** | Joern не установлен: прямая ссылка на артефакт релиза дала 404, GitHub API упёрся в лимит. Решение заказчика (JVM разрешена) не реализовано | найти корректное имя артефакта `joern-cli` и JDK 19, установить в `.tools/`, включить стадию CPG |
| Символьный слой SCIP (`scip-php`) | требует установки PHP-инструмента | установка и слияние определений/ссылок с tree-sitter |
| Схема БД из MySQL | подключение не включено (нужны реквизиты явно) | установить `RAGRAF_DB_*` и включить `db.enabled` |
| Маршруты и точки входа | слой не написан | эвристики роутинга + конфиги |
| Индексация Markdown | язык вне релиза 1 | включить `markdown` в языки |
| MCP-сервер | не написан | обёртка CLI над операциями фасада |
| Замеры M1, M3, M6, НФТ-4 | требуют набора задач и прогонов с baseline | задачи из `bench/tasks.yaml` и два прогона (с индексом и без) |
| Полноценный замер M2 «до правки» | требует индекса на состоянии перед каждым коммитом | 20 прогонов индексации по `git checkout` |
| Точность формулировки «precision 0,071» | метрика связности не даёт осмысленной precision | отдельный расчёт по коммитам |

---

## 6. Как повторить и проверить

```powershell
# самопроверка окружения (зависимости, парсер, vec0, модель)
python tools/ragraf.py --project config/revoap.yaml selftest

# полная индексация пилота (без векторов — 9 с; с векторами — 75 с в первый раз)
python tools/ragraf.py --project config/revoap.yaml index

# состояние: ревизия, покрытие, стадии, размер журнала
python tools/ragraf.py --project config/revoap.yaml status

# операции
python tools/ragraf.py --project config/revoap.yaml query callers --fqn "PipelineEngine::run" --hard
python tools/ragraf.py --project config/revoap.yaml query impact --target "Core\Config::get" --depth 3
python tools/ragraf.py --project config/revoap.yaml query search "настройки воркера" --limit 5
python tools/ragraf.py --project config/revoap.yaml query repo_map --budget 2000

# журнал расхождений (индекс НЕ меняется)
python tools/ragraf.py --project config/revoap.yaml drift --scan
python tools/ragraf.py --project config/revoap.yaml query drift_report

# завершение доработки: новый снимок + архив журнала
python tools/ragraf.py --project config/revoap.yaml reindex

# сквозной тест журнала на изолированном корпусе (20 проверок)
python tests/test_drift_e2e.py

# замер M2 на реальной истории пилота
python tools/bench_m2.py --commits 20 --depth 3
```

---

## 7. Вывод

**Работает сквозной цикл, ради которого делался проект:** индексация → ответы агента из базы (навигация, `callers`/`callees`, `impact`, гибридный поиск, `repo_map`) → накопление различий в журнал без изменения индекса → переиндексация с архивом «что изменилось в знаниях». Всё это проверено тестом на изолированном корпусе: **20 из 20 проверок пройдены**, включая детерминированность идентификаторов и неизменность хранилищ при чтении.

**Числа на реальном пилоте:** 392 файла и 126 386 строк проиндексированы за 8,8 с (75 с с эмбеддингами); 15 419 доказанных рёбер и 4 413 эвристических; recall связности соизменённых файлов **0,771 при медиане 1,000** против baseline 0,000.

**Главный незакрытый пункт:** CPG. Разрешение на JVM получено, но Joern не установлен (ссылка на релиз дала 404). Без него недоступны срезы, потоки данных и замер M4 — то есть именно то, ради чего JVM и разрешали.

**Что дальше, по приоритету:**
1. Установить Joern + JDK 19 и включить стадию CPG — разблокирует ФТ-41/42/43, `slice`, `flow`, замер M4 и часть M2.
2. Написать MCP-сервер и `AGENTS.md` проекта — без этого агент не начнёт пользоваться базой автоматически.
3. Собрать набор задач и снять baseline M1/M3/M6 (без baseline числа «стало лучше» недоказуемы).
4. Подключить схему MySQL и слой маршрутов.
5. Второй корпус (`z.batteryservice.ru`) для проверки переносимости.
