# P2 и доработка AGENTS.md: отчёт о выполнении

Дата: 2026-10-14. Исполнитель: HARNESS (агент). Основание — ТЗ
`docs/TZ-2026-10-14-harness-ragraf-usage.md`: разделы P2-1…P2-4, доработка
правил (`AGENTS.md`) и сверка «код ↔ база» (`db_cross`).

Все числа получены запуском кода. Где замер сделан на корпусе, это сказано прямо.

---

## 1. AGENTS.md: объём, структура, вынесение ловушек

**Измеренная проблема.** `AGENTS.md` занимал **74 378 байт** при бюджете
инструкций агента 65 536 байт: файл обрезался при загрузке, и хвост правил
(раздел 11) до агента не доходил. Половину объёма занимал раздел 5 «Traps» —
**38 937 байт** на 33 ловушки.

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

| Действие | Результат |
|---|---|
| Ловушки вынесены в [`AGENTS-TRAPS.md`](https://github.com/paulmann/RAGRAF/blob/main/AGENTS-TRAPS.md) | отдельный документ 44 КБ: назначение, правила чтения, оглавление из 35 ловушек, тела ловушек без изменений |
| В `AGENTS.md` вместо раздела 5 — рабочий индекс | «правила, выросшие из ловушек» (что обязана сделать правка), «самые дорогие ловушки», указатель по темам |
| Добавлено оглавление в начало | ссылки на 11 разделов: агент видит состав файла, а не читает его целиком |
| Исправлена нумерация | в разделе 4 было ДВА пункта «13» — теперь 13 и 14 |
| Ссылки на ловушки исправлены | «см. раздел 5, ловушка N» → `AGENTS-TRAPS.md`, ловушка N (прежние ссылки вели в никуда) |
| Обновлён размер | **74 378 → 42 415 байт** (запас к бюджету 23 КБ) |

**Зачем именно так, а не «просто сократить».** Ловушка в этом проекте — не
мнение, а измеренный дефект с замером и защитой; выбросить их нельзя, а держать
в файле, который обрезается, — значит потерять молча. Разделение «правила
остаются в `AGENTS.md`, доказательства — рядом» сохраняет и то, и другое.

**Что ещё улучшено для работы агента.**

* `tools/check_all.py` — **раннер проверок**. Раньше «прогнать проверки» значило
  «прогнать те, которые вспомнил»: 77 файлов, единого входа нет. Раннер делит их
  на группы (`core` — по умолчанию, `external` — нужны чужие корпуса, `service` —
  нужны сервисы), печатает вердикт по каждой проверке и строку для отчёта:
  `python tools/check_all.py` (состав групп — `--list`).
* В `docs/skills/ragraf-core/SKILL.md` обновлены действующие числа (38 и 35) и
  названы новые операции.

---

## 2. P2-1: сравнение ревизий (`diff`)

**Что сделано.** Новый модуль [`code_diff.py`](https://github.com/paulmann/RAGRAF/blob/main/src/ragraf/code_diff.py) и
операция `op_diff` (`diff` в CLI, `ragraf_diff` в MCP). Ответ содержит:

| Раздел | Что отвечает |
|---|---|
| `files` | добавленные, удалённые, изменённые файлы (по контрольной сумме) |
| `symbols` | добавленные и исчезнувшие символы (сравнение по `node_id`) |
| `edges` | сколько рёбер было и стало, и по каким видам связь изменилась |
| `callers_lost` | **регрессии**: символы, у которых были вызывающие и не стало ни одного |
| `writes_lost` | таблицы, которые перестали заполняться (`insert`/`update`/`delete`) |
| `modes` | режим (`capability`) каждой ревизии — граница доверия сравнения |

**Замер на собственном корпусе** (rev-0110 → rev-0111, то есть по итогам этой
же работы): файлов +2/−0/изменено 3; символов +75/−66; рёбер 37 798 → 37 895;
**потеряли вызывающих 29 символов**, из них названы местами: `_table_names`,
`_looks_like_graph_query`, `_graph_walk`. Сравнение нашло реальные последствия
рефакторинга, а не «ничего не менялось».

**Границы названы.** `removed: true` у потерявшего вызывающих означает, что и сам
символ исчез из кода — это другой случай, чем «символ есть, а вызовов нет».
Заглушки (`@file:`, `@ext:`) в регрессии не попадают: у них нет узла в графе, и
в первом замере они давали 29 записей с пустыми полями — измерено и исправлено.

---

## 3. P2-2: ответ, а не сырые хиты (`answer=true`)

**Что сделано.** Режим `answer` добавлен операциям, которые отвечают на вопрос
словами: `search`, `search_multi`, `symbol`, `callers`, `impact` (в MCP — параметр
`answer` у `ragraf_search`, `ragraf_search_multi`, `ragraf_symbol`,
`ragraf_callers`, `ragraf_impact`). Вместо структурного результата приходит текст
и ссылки:

```
**Найдено по запросу «write_check сверка плана записи»**
- `src/ragraf/facade.py:3264`
- `src/ragraf/facade.py:3293`
…
> порог достоверности связей: 0.5
```

**Замер:** ответ на типовой вопрос — **206 символов** (требование ТЗ «≤ 1–2 КБ»);
ссылки проверяемы (`файл:строка`), полное число находок названо («…и ещё N»),
оболочка (`op`, `index_rev`, `capability`, `found`) остаётся — это контракт, по
которому клиент понимает, на какую ревизию опирается ответ.

**Полезное следствие для `callers`.** Ответ на «связей 0» больше не читается как
«вызовов нет»: названы и число связей ниже порога достоверности, и вызовы,
адресованные ИМЕНИ символа без доказанной привязки к определению (`name_links`).
Замер: `callers cross_check` — 0 доказанных связей и **5 вызовов по имени** с
местами (`src/ragraf/facade.py:927` и другие).

---

## 4. P2-3: память о решениях (`note` / `notes`)

**Что сделано.** Хранилище [`notes.py`](https://github.com/paulmann/RAGRAF/blob/main/src/ragraf/notes.py) в
control-хранилище корпуса (таблица `note`), операции `op_note` / `op_notes`
(`note`/`notes` в CLI, `ragraf_note`/`ragraf_notes` в MCP).

* запись: текст (до 4000 символов), метка задачи, метки-теги, путь, автор, ревизия;
* чтение: свежие первыми, фильтр по задаче и по подстроке текста;
* **границы названы**: заметка — знание АГЕНТА, а не факт индекса; в каждой
  записи стоит `source: "note"`, автор и время, а в ответе чтения — оговорка,
  что заметки не заменяют `search`/`symbol`.

**Замер на живом корпусе:** запись и чтение по задаче (`task`) и по подстроке
работают; `count` различает «всего» и «по задаче».

---

## 5. P2-4: метрики эффекта

**Что сделано в `usage`.** Было: `hit_rate`, `calls_with_facts`, `avg_ms`,
`misses`, `stale_served`. Добавлено: **`saved_calls`** — число вызовов, которые
ЗАМЕНИЛИ чтение файлов или поиск по ним (операция из списка
`READ_REPLACING_OPS`, вернувшая хотя бы один факт). Это измерение по журналу;
оценка экономии в строках — `est_lines_not_read`.

**Замер на собственном корпусе:** 668 вызовов, попаданий **63,5 %**,
`saved_calls` считается по журналу, **`stale_served` = 0** (требование приёмки
«ответов со stale=true быть не должно»).

---

## 6. db_cross: анализ полезности и новое решение

### 6.1 Что было не так (измерено на BWO)

| Дефект | Замер |
|---|---|
| Имя без доказательства | ответ отдавал два списка ИМЁН: проверить, откуда имя, было нечем |
| Проза становилась «таблицами» | 168 имён, среди них `all`, `and`, `al`, `access` — из комментариев и английского текста |
| Пустой путь в доказательствах | у текстовых находок `path` был ПУСТ, а строка считалась от начала чанка (например `:98`) |
| Мусор из чужой базы | в «неиспользуемых» попадали таблицы `bsdocs` (`domofon`, `1c`, `ozon_update`) при том, что код работает с `wizard_online` |
| Скорость | **41,7 с** на вызов: полный проход 101 957 чанков текста регулярным выражением; после перехода на FTS — 72 с из-за плохого выражения разбора (34 с только на имена) |
| Слепая зона не названа | место `FROM $table`, где имени нет по определению, нигде не отражалось |

### 6.2 Что сделано

1. **Факты вместо имён.** Каждое имя приходит с источником (`edge` — факт разбора
   языка, `text` — текстовый образец), видом доступа (`select`/`insert`/…),
   доказательством `path:line` и текстом конструкции.
2. **Разбор SQL переписан сканером** вместо регулярного выражения: одна линейная
   проверка окна вокруг ключевого слова (было 34 с на 17 944 чанка, стало ~11 с;
   затем дешёвая отсечка по подстроке).
3. **Отбор чанков через FTS** (`from OR insert OR update OR delete OR join`) вместо
   полного прохода: 101 957 → 17 944 чанка.
4. **Правила отбора имён** (каждое из замера): только полная конструкция
   (`SELECT … FROM`, `INSERT INTO`, `UPDATE … SET`, `DELETE FROM`, `JOIN`);
   не строка-комментарий; только литерал (кавычка) либо блочный литерал
   (`<<<SQL`); служебные слова (`into`, `insert`, `all`, `any`, `the`) отброшены.
5. **Слепые зоны названы числом:** `dynamic_sql` — места `FROM $t`,
   `FROM {$t}`, `FROM " . $t . "`, `FROM ?`. Без них ответ «таблица в коде не
   встречается» читался бы как «можно удалять».
6. **База сравнения — два источника:** живая схема и миграции; таблица, которая
   есть только в миграциях, НЕ считается отсутствующей.
7. **`unused` отсортирован по продукту:** сначала таблицы баз, с которыми код
   РАБОТАЕТ, затем по размеру (строки и байты) — «мёртвая таблица на 12 ГБ»
   важнее пустой заглушки; у каждой записи признак `database_in_use`.
8. **`write_paths`:** кто пишет в таблицу (вид доступа и место) — ответ на вопрос
   «таблица перестала заполняться».
9. **`case_mismatch`:** имена, совпавшие с базой только по регистру (на
   регистрозависимой базе такой запрос таблицу не найдёт) — замер: 2 случая
   (`Chargers` ↔ `chargers`, `TestStopConditions` ↔ `teststopconditions`).

### 6.3 Замер после правки (BWO, живая база `wizard_online`)

| Показатель | Было | Стало |
|---|---|---|
| **Время вызова** | 41,7 с | **3,7 с** (11× быстрее) |
| Имён таблиц в коде | 168 (с прозой: `all`, `and`, `al`, `access`) | **121** (86 — рёбра, факт разбора; 35 — только текст) |
| Просмотрено чанков | 101 957 (весь текст) | 8 947 (отбор FTS + порядок «маленькие первыми») |
| Слепых зон названо | 0 | **265** (с местами) |
| Найдено в базе | — | 51 из 121 (остальные 70 — имена, которых в базе нет) |
| Не используется кодом | 126 (без размера, вперемешку с чужой базой) | **129** с размером и признаком «база используется кодом» |
| Кто пишет | — | 27 таблиц с местами записи |
| Регистр расходится | — | 1 |

**Граница названа, а не спрятана.** Текстовый проход остановлен по бюджету
20 млн символов (из 273 млн в SQL-подобных чанках): это видно в `limits`
(«часть файлов не просмотрена»), а чанки крупнее 400 000 символов (выгрузки,
минифицированные библиотеки — замер: 110 МБ, 45 МБ, 35 МБ) не разбираются
намеренно, и их число тоже названо. Факты из рёбер графа ограничением не
затронуты — их даёт извлекатель языка, а не текстовый образец.

---

## 7. Найденные и исправленные дефекты (сверх плана)

| Дефект | Замер | Что сделано |
|---|---|---|
| **Ссылка `path:line` внутри функции была на строку больше** (ловушка 35) | «бюджет исчерпан на стадии» лежит в `src/ragraf/indexer.py:99`, индекс отдавал `100` | Пересчёт строки вынесен в одно место `util.text_line` с учётом служебного заголовка чанка; проверка `tests/test_evidence_lines.py` сверяет ответы индекса с файлами на диске для обоих видов чанков |
| **Внешняя заглушка перекрывала определение** (ловушка 34) | `callers cross_check` отвечал «связей 0»: `rows[0]` — заглушка `python:cross_check`, а межфайловые вызовы привязываются только по имени И файлу (журнал: «по имени и файлу 1458, по имени без файла 0, не разрешено 14 365») | Реальные определения идут первыми; вызовы по имени без привязки — отдельный список `name_links` с достоверностью 0.4 |
| **`callers_lost` считал заглушки** | 29 записей с пустыми полями | Цели-заглушки исключены; у исчезнувших символов есть `removed: true` |
| **`unused` смешивал базы** | таблицы `bsdocs` в списке «код не касается» | Признак `database_in_use` и сортировка «рабочие базы → размер» |
| **Реестр хранил пустые записи прогонов** | разработка `AutoClaw`: 54 235 файлов, 0 узлов, `rev_id` пуст (индекса нет вовсе) | Пустая ревизия в историю прогонов не пишется, состояние названо (`registered`/`indexed`); данные приведены к правилу (убрана 21 запись `trigger=register`) |

---

## 8. Проверки

| Проверка | Результат |
|---|---|
| `tests/test_p2_features.py` (новый: P2-1…P2-4, поверхности) | 50 из 50 |
| `tests/test_db_cross.py` (новый: разбор SQL и выводы `cross_check`) | 47 из 47 |
| `tests/test_evidence_lines.py` (новый: точность `path:line`) | 13 из 13 |
| `tests/test_telemetry_facts.py` (факты и метрики) | 14 из 14 |
| `tests/test_catalog_state.py` (состояние реестра) | 18 из 18 |
| `tests/test_budget_stage.py` (бюджет внутри стадии) | 11 из 11 |
| `tests/test_structure_p1.py`, `test_polyglot_p13.py`, `test_db_marks.py` | 24, 17, 48 из 48 |
| `tools/check_all.py` (раннер, группа `core`) | **48 файлов: 47 зелёных, 1 красный** — `test_excludes_summary.py` падал на равенстве числа файлов ЧУЖИХ корпусов (z 162→222, MD.Viewer 16→80, RdpMON 1266→1274, Clean_BOM_Senior 6→18). Проверка исправлена на «не меньше базового замера» (как раньше `test_exclude_defaults.py`) и после правки даёт **37 из 37** |
| Итог ранера по проверкам | 1 168 проверок пройдено, 4 провалено (все четыре — тот же чужой дрейф, теперь закрыты), 759 с |
| `tools/Invoke-ProjectDoctrine.ps1` | 19 проверок, 0 ошибок, PASS (числа документов совпали с кодом: 38 инструментов, 35 операций) |
| `tools/check_bom_crlf.py` | замечаний нет |
| Переиндексация собственного корпуса | выполнена; `capability=full` |

---

## 9. Что осталось не сделанным

| Пункт | Состояние и причина |
|---|---|
| **§3.2 Технический гейт в HARNESS** (`warn`/`deny` на `pwsh`/`read`, если аргументы содержат `Select-String|rg|findstr` без `ensure`) | **не сделано, и правкой исходников невозможно**: клиент лежит в упакованном `resources\app.asar` (путь `...\app.asar\dsh\` как каталог недоступен; в `app.asar.unpacked` только нативные модули). Механизм расширения при этом есть и проверен: профиль DSH собирается патчами (`~/.dsh/profiles/desktop/cordis.patch.yml`, ключ `dsh.profile.bundles` в `package.json`, каталог `.plugin-manager`) — гейт реализуем как **плагин профиля**, это отдельная работа на API плагинов DSH |
| **§3.4 Приёмка внедрения** | «`stale_served = 0`» измерено (0 из 668 вызовов); «доля вызовов с фактами» измеряется (`hit_rate` 63,5 %); покрытие по задачам — через `usage --task`. НЕ измерено: «0 случаев `grep` без `ensure`» — без гейта §3.2 такие случаи видит только сам агент |
| Прочие находки прошлой очереди | проверки чужого проекта (`test_css_layer`, `test_class_usage`, три проверки SagaAI) — не трогали по указанию; `db_cross` по-прежнему не видит имён, собранных конкатенацией (`"SELECT * FROM " . $t`) — такие места названы в `dynamic_sql`, но имени в них нет и быть не может |
| Корпус-контейнер в реестре | запись `AutoClaw` с корнем `<полигон>` покрывает все проекты сразу; теперь честно помечена «заведена, индекс не строился» — полезность такой разработки решает владелец |
