# RAGRAF × DeepSeek Harness: замеры, что исправлено, что осталось

**Состояние:** реализация отчёта интегратора от 13.10.2026 выполнена и проверена.
**Замерено:** 13.10.2026, Windows, DSH 0.2.0-rc.2.
**Корпус:** `RAGRAF`, активная ревизия **rev-0079**, `capability=full`,
`joern_used=true`, CPG собрана в этом прогоне (1217 рёбер), 578 файлов.
**Проверки:** чекер конвенций `checks: 19, errors: 0, warnings: 0`; `check` —
суммы совпали, реиндексация не требуется; BOM/CRLF — 38 из 38 чистых.

Этот файл — продолжение `HARNESS_INTEGRATION.md`. Там описано подключение,
здесь — что было не так, что изменено, чем это подтверждено и что осталось
открытым. Источник исходных требований: отчёт интегратора (B1–B8 + протокол),
переданный владельцем 13.10.2026.

---

## 1. Что клиент HARNESS реально поддерживает (вычитано из его кода)

Код клиента — внутри архива `app.asar` (один файл, 121 348 951 байт; `dsh/lib` —
виртуальный путь, подкаталога на диске нет):

```
dsh/node_modules/@deepseek-ai/dsh-mcp-client/lib/index.js     (837 строк)
```

| Возможность | Вердикт | Следствие для RAGRAF |
|---|---|---|
| `tools/list`, `tools/call` | поддерживается | инструменты видны как `mcp__ragraf__*` |
| `resources/list`, `templates/list`, `read` | поддерживается | три общих инструмента `list_mcp_resources` / `list_mcp_resource_templates` / `read_mcp_resource` с аргументом `server` |
| `instructions` из `initialize` | поддерживается | попадает в системный промпт как «### MCP server: ragraf», предел 32 768 байт |
| `outputSchema` + `structuredContent` | частично | валидируется и сохраняется, но **модель видит только блоки `text`** |
| `notifications/cancelled` | поддерживается | приходит при отмене и по таймауту |
| `notifications/progress` | **не запрашивается** | незапрошенное уведомление о ходе клиент считает **ошибкой** |
| `prompts/list`, `prompts/get` | **не поддерживается** | «MCP prompt templates are unsupported» |
| `logging/setLevel`, `notifications/message` | не поддерживается | диагностика обязана идти в stderr |
| `completion/complete`, `ping` | не поддерживается | keepalive на ping не рассчитан |
| `roots`, `sampling`, `elicitation` | недоступно | `capabilities: {}` — корни проекта сервер не получает |
| `title`, `annotations` инструментов | **не читаются** | это язык для других клиентов, HARNESS их игнорирует |
| таймаут `tools/call` | 300 000 мс (профиль), умолчание плагина 60 000 | прогрессом таймаут **не продлевается** |

Отсюда два проектных следствия, которые проверены кодом, а не предположением:
`structuredContent` не заменяет текстовые блоки, и `prompts/*` в HARNESS видны
не будут (другие клиенты их поддержат).

---

## 2. Что исправлено

### B1. Чужой корпус приходил молча

Конфигурация MCP-клиента задаёт рабочий каталог дочернего процесса
(`cwd: RAGRAF`), поэтому корпус определяется по каталогу **процесса
сервера**, а не по проекту задачи. Задача о другом проекте получала факты о
RAGRAF без единого признака подмены.

Сделано:

* новая операция **`ragraf_context`** — корпус, корень, ревизия, `capability`,
  свежесть, отпечаток сборки, число инструментов и **на каком основании корпус
  определён** (`arg` | `auto` | `use_project` | `none`). Работает и когда корпус
  не выбран вовсе — это первое, что вызывают в сомнении;
* в **каждом** ответе появилось поле `corpus` (`category`, `root`, `source`,
  `confirmed`, `mismatch`), а в текстовой сводке — строка `КОРПУС: …` и прямое
  предупреждение при `confirmed=false`;
* корни клиента (`roots`) читаются, если клиент их пришлёт; расхождение корня
  задачи с выбранным корпусом даёт `corpus_mismatch` и предупреждение в ответе;
* в `initialize` отдаётся `instructions` — «перед первой операцией вызовите
  `ragraf_ensure`; если ответ ссылается на чужие пути — `ragraf_context`, затем
  `ragraf_use_project`» (1198 байт);
* `--project` по-прежнему не обязателен (запрещать его нельзя: один сервер
  обслуживает несколько проектов), но при `source=auto` сервер пишет в stderr,
  что корпус не подтверждён.

Чего это НЕ даёт: сервер физически не может узнать рабочий каталог сессии —
HARNESS объявляет `capabilities: {}` и корней не передаёт. Поэтому честный
предел — видимость источника корпуса в каждом ответе плюс явный выбор
`use_project`, а не автоматическое определение проекта задачи.

### п.1. Сторож свежести на каждом вызове

Было: проверка выполнялась **один раз** при создании фасада, дальше операции
переиспользовали `self._freshness`. Долгоживущий процесс MCP не видел правку
кода до явного `ensure`.

Стало:

* `Facade._install_guard()` оборачивает **все** операции фасада проверкой
  `_ensure_fresh()` перед работой (`op_freshness` исключён — это сама проверка);
* порядок по цене: сначала **дешёвый отпечаток входа** — пути, размеры и время
  правки, **без чтения файлов** (`corpus.scan(digest=False)`,
  `corpus.stat_fingerprint`, `watch.quick_check`); полная сверка контрольных сумм
  и переиндексация — только при расхождении;
* отпечаток хранится в `.ragraf/index/watch_fingerprint.json` и обновляется на
  **каждом** пути индексации. Отдельно исправлен путь сторожа: `watch.run_reindex`
  пишет реестр сам, минуя `record_index_run`, и без явного вызова
  `write_fingerprint` отпечаток оставался от прежней ревизии — тогда дешёвая
  сверка не срабатывала бы никогда, а «проверка перед каждым вызовом»
  превратилась бы в полную сверку на каждом вызове;
* `stale` теперь отдаётся в `freshness` **всегда** (true/false), появилось поле
  `verified_by: fingerprint`.

Замеры на живом корпусе: **0,09 с** по отпечатку против **0,64 с** полной
сверки; после реальной правки — автоматическая переиндексация за **22 с** без
явного `ensure`. Кэш 15 с (тот же `FRESHNESS_TTL_SEC`) сохранён.

**Отклонение от отчёта.** Фоновая переиндексация «не блокировать ход» не сделана
осознанно: ревизия пишется в несколько таблиц (graph, coverage, rev_history,
реестр), и обрыв или наложение двух пересборок оставляют индекс, которому нельзя
верить, — это дороже ожидания. Долгая операция укладывается в клиентский таймаут
300 с (замер пересборки — 22 с), прогресс отправляется только если клиент передал
`progressToken`, а `notifications/cancelled` обрабатывается: фоновая
переиндексация не начинается, идущая не рвётся — и это названо прямо.

### B2. Проект без маркеров в корне не индексировался

`ensure(BWO)` возвращал `not_a_project`, хотя это 38 627 файлов с
маркерами в подкаталогах. Теперь маркеры ищутся на 2–3 уровня вглубь
(`_MARKER_SEARCH_MAX_DEPTH=3`, `_MARKER_SEARCH_MAX_DIRS=400`), обход использует
готовый `util.prune_dir` с `DEFAULT_EXCLUDES`, соседи сортируются (детерминизм),
а пределы при исчерпании честно называются в причине. Все прежние запреты
(домашний каталог, корень диска, служебные имена) проверяются **до** обхода.
Замер: `looks_like_project(BWO)` → `True`,
`lib/composer.json (глубина 1)`, 0,012 с; быстрые отказы сохранены (0,0005 с).

### B3. Журнал молча терял вызовы

Подтверждено живьём: при переиндексации в том же процессе запись `usage_call`
падает с `OperationalError: database is locked` — и раньше это глушилось
`except Exception: pass`. Стало:

* запись идёт через `Telemetry._write()`: до трёх попыток на блокировке, затем
  **счётчик потерь** `write_failures`, причина `write_error` и строка в stderr
  (одна на вид ошибки, чтобы не заливать диагностику);
* `usage` отдаёт `write_failures`, `write_error` и `warning` — потеря перестала
  быть невидимой;
* соединение журнала идёт через `store.connect` (WAL, `busy_timeout=10000`) —
  проверено, что все пути это соблюдают.

Замер параллельной записи: три писателя × 20 вызовов → **60 из 60**, потерь 0.
Остаточный риск назван в §5.

### B4. Одну ревизию собирали разные окружения

Разведка уточнила причину: `joern_enabled` выключается **в коде** на время
быстрой переиндексации (`watch.run_reindex`), поэтому прогон с `joern_enabled:
true` в YAML печатал «CPG отключена в конфигурации», а `capability` поднималась
до `full` переносом рёбер из прошлой ревизии — то есть тихая деградация
маскировалась. Сделано:

* `rev.json` теперь несёт `extractor_versions`, блок `cpg`
  (`mode`: built | carried | none, `used`, `calls_added`, `carried_from`,
  `downgraded`), `corpus_hash`, `config_hash`; прежние поля и их порядок
  сохранены (`seal_rev` — обратная совместимость, проверена прогоном);
* текст причины честный: «CPG в этом прогоне не собиралась (режим без CPG)»,
  с указанием донора при переносе рёбер; формулировка «отключена в конфигурации»
  больше не утверждается, потому что YAML-настройка и выключение кодом с этой
  стороны неразличимы;
* понижение покрытия стало громким: предупреждение в отчёте индексатора, метрики
  `cpg_downgrade` / `cpg_downgrade_note` рядом с `cpg_used`;
* исправлен путь Joern: `pipeline.py` искал `.tools/joern`, установка лежит в
  `.tools/joern-cli`, поэтому в `extractor_versions` обеих прежних ревизий
  стояло `joern: "MISSING"` при реально работавшем Joern. На rev-0079 там путь,
  а `cpg.mode = built`, `calls_added = 1217`.

`config_hash` намеренно **не** расширен на `index.joern_enabled`: этот ключ
меняется во время прогона, и включение его в хеш сделало бы сумму «дрожащей» и
зациклило сторожа.

### B5. Эвристика отдавалась как факт

Причина ложного пути `cwd` → `tools/ragraf-mcp-http.py:138` установлена точно:
заглушка внешней цели создаётся **без файла** (`stable_id("external", имя,
язык)`), а индексатор проставлял `path`/`start_line` всем символам файла — и у
общей заглушки оставался путь **последнего** файла, который её упомянул.
Замер: 735 заглушек, 420 упомянуты более чем из одного файла.

Сделано: заглушкам не пишется путь (для уже построенных индексов работает
предохранитель на отдаче: у `kind='external'` пути в ответе нет), введён порог
достоверности `MIN_EDGE_CONFIDENCE = 0.5` (по замеру: 0.4 — 28 474 ребра,
0.6 — 1 142, 0.5 и 0.7 отсутствуют вовсе), рёбра ниже порога не отдаются,
связь без доказательства помечается `ambiguous: true`, в ответе видно
`confidence_floor`. Ветка «имя без файла» в резолвере больше не называется
`name_file` с 0.7 — это `name_only` с 0.6.

Проверено: воспроизводящий вызов `callees` давал 8 связей с чужим путём,
после правки — 0; `hard_only` работает как раньше; `test_cpg_call_names.py` —
47 из 47, `test_inheritance.py` — 52 из 52.

### B6. Док-строка противоречила поведению

`tools/ragraf-mcp.py` утверждал, что запуск без `--project` отвергается. Приведено
к факту: сервер поднимается, определяет корпус по рабочему каталогу через реестр,
а если не нашёл — отвечает на `catalog`, `context`, `use_project` и честно
отказывает остальным.

### B7. `usage` не видел текущую сессию

Добавлены `scope` (`all` | `session` | `agent`) и `session_id`;
`calls_in_session` заполняется в режиме сессии. Проверено на живом сервере:
20 вызовов по stdio → `calls_in_session ≥ 15`, `session_id` назван,
`scope=session` виден в фильтре.

### B8. Размер ответов

Добавлен аргумент `brief` **всем** инструментам: он убирает служебную часть
(суммы контрольных сумм, подробности журнала расхождений, хвост предупреждений,
строки реестра сверх десяти) и **не трогает факты**; что именно выброшено —
в поле `brief_omitted`. Замер: `catalog` 11 290 → 6 287 байт.

### Протокол MCP

* `title` и `annotations` (`readOnlyHint`, `destructiveHint`, `idempotentHint`,
  `openWorldHint`) — у всех 31 инструмента, из одной таблицы `_TOOL_META`,
  полнота проверяется (`UNCLASSIFIED_TOOLS`);
* `outputSchema` (общая схема конверта) и `structuredContent` — у всех;
* `serverInfo.build` — отпечаток **содержимого** файла сервера, и
  `serverInfo.tools` — фактическое число. Это прямой ответ на расхождение
  «в исходнике 30, у клиента 24»: живой процесс был старше правки кода. Теперь
  отпечаток живого процесса сравнивается с отпечатком файла на диске;
* `tools/list` строится по фактическим обработчикам, расхождение с описаниями
  печатается в stderr;
* MCP-ресурсы: `ragraf://status`, `ragraf://catalog`,
  `ragraf://corpus/{name}/map` (+ шаблон). Чужой корпус читается **временным
  фасадом только для чтения** — ресурс не переключает состояние сервера;
* MCP-подсказки `ragraf-check`, `ragraf-repair`. **В HARNESS они не видны** —
  клиент шаблоны промптов не поддерживает; реализованы для других клиентов;
* `notifications/cancelled` обрабатывается, `notifications/progress` уходит
  **только** по запрошенному `progressToken` (иначе клиент считает это ошибкой).

---

## 3. Приёмка по §5 отчёта

| № | Проверка | Результат |
|---|---|---|
| 1 | `tools/list` живого процесса против исходника | 31 в исходнике, 31 у сервера, 31 исполнитель; отпечаток сборки в `serverInfo` |
| 2 | Сессия с рабочим каталогом ≠ cwd сервера | корпус не подтверждён, `source=auto`, предупреждение в сводке и в stderr; `context` называет корень и основание |
| 3 | `ensure` на проекте без маркеров в корне | `BWO` → маркер найден на глубине 1 |
| 4 | 12–20 вызовов → `usage` их видит | 15 вызовов по stdio → `calls_in_session ≥ 15`, потери объявлены |
| 5 | Правка кода во время сессии | автоматическая переиндексация без `ensure`; далее 0,09 с по отпечатку |
| 6 | Три процесса на одном корпусе | 60 из 60 записей, потерь 0 (остаточный случай — в §4) |
| 7 | `callers`/`callees` без ложных путей | воспроизводящий вызов: 8 ложных связей → 0; `ambiguous` и порог на месте |
| 8 | `list_mcp_resources` / `read_mcp_resource` | три ресурса отдаются, неизвестный URI — честная ошибка |

Живая приёмка целиком: `.staging/accept_mcp_stdio.py` — **26 из 26** (сервер
запускается отдельным процессом с урезанным окружением, как это делает клиент).

Регрессия проекта: `test_mcp_compat.py` — 15 из 15, `test_dedupe.py` — 19 из 19,
`test_capability_consistency.py` — 5 из 5, `test_lang_routing.py` — 56 из 56,
`test_cpg_call_names.py` — 47 из 47, `test_inheritance.py` — 52 из 52.

---

## 4. Открытые находки

1. **`tests/test_ensure.py`, секция 4 — не объяснено до конца.** Ожидается
   `reindexed` после правки файла в каталоге-пробе, приходит `actual`.
   Что установлено: провал воспроизводится 5 раз из 5 (в том числе другим
   исполнителем) и **не зависит** от удаления оставшегося каталога-пробы;
   изолированное воспроизведение того же сценария на свежем корпусе даёт
   `reindexed` — то есть механизм сумм и переиндексации жив; на живом корпусе
   RAGRAF правка даёт `changed=true` и `choose_mode=fast`. Путь отказа
   (`ensure._ensure_existing` → `watch.check` → `choose_mode`) правкой не
   затрагивался. **Не доказано, что причина — состояние реестра от прежних
   прогонов; не доказано и обратное.** Требуется отдельная разборка.
2. **Потеря записи журнала при длительной внешней транзакции.** Три писателя
   журнала дают ноль потерь, но при одновременной переиндексации в другом
   процессе `usage_call` может не записаться (`database is locked`). Потеря
   теперь **видна** (`write_failures`), но не устранена: полное устранение —
   вынести журнал в отдельный файл, а не делить `control.sqlite` с историей
   ревизий. Это изменение схемы и миграции, отдельная работа.
3. **Порог достоверности не применён к двум исполнителям обхода.**
   `graph_algo.expand` (обход на матрице) порог не применяет, поэтому два
   исполнителя одного обхода расходятся по множеству рёбер; прямые выборки в
   `op_references`, `table_usage`, `_impact_table` порог тоже не проверяют.
   Расхождение измерено и записано комментарием, но не устранено.
4. **`docs/AGENTS-context.md`: раздел §4.1 встречается дважды** (дубликат
   нумерации существовал до этой правки, не исправлялся).
5. **`HARNESS_INTEGRATION.md` расходится сам с собой**: «Ожидается: пройдено 13»
   против «15 проверок» в таблице. Фактический прогон сегодня — 15 из 15.

---

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

```powershell
# актуализировать индекс и убедиться, что суммы совпали
& .\.venv\Scripts\python.exe tools\ragraf.py -p config/ragraf.yaml reindex
& .\.venv\Scripts\python.exe tools\ragraf.py -p config/ragraf.yaml check

# конвенции проекта (ожидается checks: 19, errors: 0)
pwsh -NoLogo -NoProfile -NonInteractive -File tools/Invoke-ProjectDoctrine.ps1

# BOM/CRLF у изменённых текстов
& .\.venv\Scripts\python.exe tools\check_bom_crlf.py

# живая приёмка по stdio: 26 проверок, урезанное окружение
& .\.venv\Scripts\python.exe .staging\accept_mcp_stdio.py

# сторож: дешёвый отпечаток против полной сверки, реакция на правку
& .\.venv\Scripts\python.exe .staging\smoke_guard.py

# параллельная запись в журнал (ожидается «нет потерь»)
& .\.venv\Scripts\python.exe .staging\stress_journal.py
```

Интерпретатор — **только** `.venv` проекта: `python` из PATH на этой машине не
имеет PyYAML, и `ensure` ответит `конфиг корпуса не читается`. Это отдельная
находка, зафиксированная в B4.
