# P1: ускорение поиска и обязательное использование RAGRAF — отчёт о выполнении

Дата: 2026-10-14. Исполнитель: HARNESS (агент). Основание — ТЗ «ускорение поиска
по коду через RAGRAF и обязательное его использование из HARNESS», разделы 2
(P1-1…P1-4) и 4 (порядок работ). P0 выполнен ранее и здесь не переписывается;
в конце перечислены закрытые остатки P0 и посторонние находки.

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

---

## 1. Что сделано по пунктам ТЗ

| Пункт | Что требуется | Что сделано | Где в коде |
|---|---|---|---|
| P1-1 | `search_multi`: несколько запросов одним вызовом, кодировки, группировка по копиям, пометка активного файла, ≤ 2 с | Literal-поиск по тексту чанков (FTS-отбор + проверка подстрокой), определение кодировки UTF-8/CP1251/UTF-16, группировка по семействам, роль файла (точка входа / активный / копия) с доказательством `path:line` | `lexvec.literal_hits`, `textdec.py`, `analysis.py`, `facade.op_search_multi` |
| P1-2 | `duplicates`, `dead_code`, `entrypoints`; активные/мёртвые файлы определять по фактам | Модуль структурного анализа: семейства по нормализованному имени, главный файл по детерминированному правилу, достижимость от точек входа, мёртвый код только при совпадении двух независимых проверок | `analysis.py`, `facade.op_duplicates/op_dead_code/op_entrypoints` |
| P1-3 | Полиглот: Perl, HTML-шаблоны, ключи переводов, SQL вне строк PHP | Пакетные переменные Perl с приведением знака, плейсхолдеры `%{Key}`/`@{Key}`/`{{Key}}` и связь с `Translate(...)`, HEREDOC/NOWDOC, SQL в Perl (`$dbh->do`, `<<'SQL'`), наполнение `web_route`, ссылки на файлы строковыми литералами | `extract/generic.py`, `extract/php_ast.py`, `extract/css_html.py`, `indexer.py` (стадия load), `revisions.add_routes` |
| P1-4 | Пометки колонок, сверка миграций с живой базой, «кто пишет» | Единое правило признаков (`db_marks`), разбор UNIQUE/PK/AUTO_INCREMENT/GENERATED в миграциях, пометки в `schema` и `db_schema`, `migrations_vs_live` в `db_drift`, текст запроса и способ вызова (`prepared`/прямой) в `table_usage` | `db_marks.py`, `schema_db.py`, `db_ops.py`, `facade.op_schema/op_db_schema/op_write_check/op_db_drift/op_table_usage` |

Поверхности обновлены: **32 операции фасада** (было 28) и **35 инструментов MCP**
(было 31), у каждой новой операции есть ветви в CLI (`search_multi`, `duplicates`,
`entrypoints`, `dead_code`) и в реестре `web_route`.

---

## 2. Приёмка: что измерено

### P1-1 на корпусе BWO (тот самый корпус из ТЗ)

Запрос из ТЗ: `["Allowed formats", "fileExtensions", "'MCH', 'MOL'"]`.

| Замер | Значение |
|---|---|
| Время вызова `search_multi` на rev-0008 (102 401 чанк, 4 639 файлов) | **1,31–1,41 с** (требование ≤ 2 с); повторные вызовы в одном процессе 1,31 и 1,34 с |
| Способ отбора по каждому запросу | `fts` (3 из 3) |
| Найдено | 60 совпадений в 25 файлах, 19 семейств |
| Кодировка в ответе | поле `encoding` у каждого хита |

Честно про разброс: один замер, сделанный сразу после двух подпроцессов `ensure`
(то есть на загруженной машине и с холодным первым вызовом), дал 2,76 с — выше
предела. В том же процессе второй вызов снова уложился в 1,34 с, а отдельный
замер в чистом процессе — 1,35 с три раза подряд. Причина — холодный старт и
соперничество за диск, а не алгоритм; но предел 2 с держится НЕ с запасом
двойного размера, и это стоит знать до включения операции в горячий путь.

Требование «указать, что активен `php/Wizard/Files7.php`» выполняется механизмом
ссылок, собранных при индексации: `templates/Files.html` содержит
`var BWO_FILES_ENDPOINT = "/php/Wizard/Files7.php";` (строка 100), и этот путь
становится ребром графа. На BWO ответ по семейству `php/Wizard/Files.php`:

* активен `php/Wizard/Files.php` (5 ссылок, первая — `db/Files.html:17`) — самый
  подключённый из файлов, где нашлась искомая строка;
* подключены также `Files6.php` (5), `Files2.php` (1), `Files5.php` (1);
* отдельным полем `referenced_outside_hits` назван `php/Wizard/Files7.php` —
  7 ссылок, в том числе `templates/Files.html:100`, — и это честно помечено как
  «семейство шире найденного»: в самом `Files7.php` искомой строки нет.

Иначе говоря, ответ не выдумывает единственного «рабочего файла» там, где
подключены несколько копий: он называет, сколько ссылок у каждой и откуда они.

Проверено также на синтетическом корпусе, повторяющем BWO
(`tests/test_structure_p1.py`, 24 проверки, в том числе литерал с ведущим слэшем):
семейство `php/Wizard/Files.php` → активен `Files7.php`, остальные названы
копиями, доказательство — `templates/Files.html:6`.

### P1-2 на синтетическом корпусе BWO

`tests/test_structure_p1.py`: группы копий `Files.php`…`Files7.php` (8 файлов,
сходство выше порога), главный файл `Files7.php`, `dead_code("php/Wizard")`
называет неподключённые копии и НЕ называет мёртвым рабочий файл,
`dead_code("cgi-bin")` называет копию скрипта без ссылок с причиной
«исполняемый скрипт, но ни одной ссылки нет».

### P1-3

`tests/test_polyglot_p13.py`, 17 проверок:

| Свойство | Результат |
|---|---|
| Perl: `%hFORMAT` и `%dFORMAT` | объявление найдено как символ, использования — через `references("perl:%hFORMAT")` |
| SQL в heredoc/nowdoc PHP | `table_usage("StringData", mode=write)` находит `php/BCTC_Import.php` и `php/BWO_Import.php` |
| SQL в Perl (`<<'SQL'`, `$dbh->do`) | `table_usage("audit_log")` и `table_usage("orders")` находят `cgi-bin/import.pl` |
| Шаблон ↔ ключ перевода | `%{reg.months}` в шаблоне и `Translate('reg.months')` в PHP — ОДИН узел, `references("tpl:reg.months")` отдаёт оба файла |
| Реестр точек входа | `Route::post('/api/save')` → вид `route`; `action=` и `fetch(...)` → вид `endpoint` |

На корпусе BWO (rev-0007) подтверждено: `table_usage("StringData", mode=write)` —
27 обращений, среди них `via=prepared`; `table_usage("cellvoltagedata", mode=write)` —
13 обращений в `cgi-bin/*/import.pl` и `import_stdin.pl`.

### P1-4

`tests/test_db_marks.py`, 42 проверки, включая живую базу этой машины:

| Свойство | Результат |
|---|---|
| `VIRTUAL/STORED GENERATED` против `DEFAULT_GENERATED` | генерируемыми названы только первые; `DEFAULT_GENERATED` — «значение по умолчанию — выражение», запись разрешена |
| Живая схема (269 таблиц, 3 069 колонок) | помечено **79** генерируемых колонок; ложных срабатываний на `DEFAULT_GENERATED` — **0** (до правки подстрочный поиск давал 171 совпадение, из них 92 ложных) |
| Миграции | `UNIQUE KEY`, составной `UNIQUE`, `PRIMARY KEY (…)`, `AUTO_INCREMENT`, `GENERATED ALWAYS AS (…) STORED/VIRTUAL`, `DEFAULT` — разобраны; имена колонок с пробелами и дефисами (`String Nominal Voltage`, `Watt-hours`) больше не теряются |
| `ALTER TABLE … ADD COLUMN` | признаки (NOT NULL, DEFAULT) читаются из строки ALTER, а не теряются |
| `db_drift` | добавлен раздел `migrations_vs_live`: чего нет в живой базе из миграций и наоборот; при пустой одной из сторон `plausible=false` и причина — пустой список расхождений ничего не доказывает |
| `write_check` | генерируемые колонки собираются из ДВУХ источников (живая база и миграции), `columns_marks` даёт `pk/unique/auto_increment/generated/has_default`, `AUTO_INCREMENT` и генерируемые исключены из «обязательных» |

### Кодировки (фундамент P1-1 и P1-3)

`src/ragraf/textdec.py` определяет UTF-8 (BOM и строгий разбор), UTF-16 (BOM) и
CP1251 (по доле кириллицы среди букв с порогом «не меньше 4 букв», чтобы
латинский текст `café naïve` не объявлялся русским). Кодировка файла пишется в
`file.encoding` и называется в ответе `search_multi`. Проверено на файле в
CP1251: русская строка находится, `sha256` остаётся хэшем исходных байт,
CP1251-файл не ломает нарезку чанков.

---

## 3. Что пришлось поправить по ходу (и почему)

1. **Индекс BWO вырос втрое на неизменном коде** — 102 433 чанка против 277 351.
   Причина: служебные узлы нового слоя (маршрут, ключ перевода, пакетная
   переменная) нарезались в чанки как тела символов. Исправлено исключением
   этих видов из нарезки; чанки вернулись к 102 401.
2. **Идентификатор узла маршрута содержал параметры адреса** (`?id=123&tab=list`),
   из-за чего один и тот же маршрут давал сотни узлов (310 946 рёбер маршрутов).
   Идентификатор считается по адресу без `?` и `#`, параметры остаются в
   свойствах.
3. **В реестр маршрутов попадали статические ссылки** (картинки, якоря):
   добавлен отбор по расширениям кода, повтор маршрута в файле даёт одну запись.
4. **Поиск ссылок на файл стоил 0,66 с на файл** (LIKE по 102 тысячам чанков) и
   5,8 с на ответ. Ссылки теперь собираются при индексации (строковые литералы
   путей), а чтение сужено до найденных файлов; время ответа 5,8 → **1,29 с**.
5. **`LIKE '@file:%'` не использовал индекс** (LIKE регистронезависим и не
   превращается в диапазон): 142 тысячи рёбер читались целиком. Заменено
   сравнением диапазона `dst >= '@file:' AND dst < '@filf'`.
6. **Проверка ссылок «по тексту» в `search_multi`** убрана: те же ссылки уже в
   графе, а поиск по тексту стоил 0,5 с на файл. У операции `duplicates` он
   остался — там он делается для одного главного файла группы.

---

## 4. Закрытые остатки P0 и находки (доделано 14.10.2026, вторая очередь)

### 4.1 Остатки P0

| Остаток | Что сделано и чем доказано |
|---|---|
| P0-4: MCP-сервер не читал `RAGRAF_TASK` | Метка читается при создании сервера и передаётся в фасад (`mcp_server.McpServer.task` → `Facade(task=...)`). В сводке `usage` появилась строка задачи: `P1-приёмка` в разделе «по задачам» |
| P0-2: бюджет проверялся только МЕЖДУ стадиями | **Закрыто.** Контрольные точки добавлены ВНУТРЬ самой долгой стадии (`_build_facts`, каждые 32 файла) и ПЕРЕД каждой тяжёлой (`load`, `resolve`, `databases`, `lexvec`) — стадия больше не начинается «в долг». Отчёт называет место: `budget_stage` и `budget_files_done`. Проверка `tests/test_budget_stage.py` (11 проверок): бюджет 1 мс → остановка на `ast` после 32 файлов, ревизия не закреплена, в примечаниях причина и подсказка; бюджет 300 с → прогон проходит и не помечается |
| P0-1: «пустая ревизия не записывается» | **Закрыто, дефект был жив.** Регистрация разработки писала в историю прогонов запись с пустым `rev_id`: в реестре оказалась разработка `AutoClaw` со «файлов 54 235, узлов 0», хотя `.ragraf` не существует вовсе (`trigger: register`, 14.10.2026 11:53). Такая строка читается как сломанный индекс. Теперь пустая ревизия в `runs` не попадает, у записи есть состояние `registered`/`indexed` (`Catalog.record_run`, поле `Entry.state`), а `catalog` печатает «заведена, индекс не строился». Заодно найдено второе: повторная регистрация **затирала ревизию построенного индекса** пустой строкой и подменяла сумму корпуса свежей — то есть объявляла устаревший индекс актуальным. Теперь регистрация заполняет только пустые поля. Проверка — `tests/test_catalog_state.py` (18 проверок). Данные приведены к правилу: из реестра убрана 21 запись `trigger=register` (по одной на каждую разработку), ни одна настоящая история прогонов не потеряна (проверено по копии) |
| P0-3: `write_check` на живой схеме, где нет миграций | **Закрыто.** Корпуса BWO и RAGRAF миграций не содержат (`db_table` пуст), а рабочие таблицы импорта есть в живой базе `wizard_online` — прежняя редакция отвечала «таблица не найдена», то есть проверка не работала ровно там, где нужна. Теперь схема берётся из собранного живого слоя, если в миграциях таблицы нет. Замер на живой базе: `TestConfig` — 55 колонок, генерируемые `String Nominal Voltage`, `Duration Seconds`, `Duration Hours`, `Duration Minutes`, `Testing Time`, `Discharge Time Format`, `String Current`; `TestData` — 30 колонок, `pk` из трёх колонок, генерируемые `Time Seconds`, `Real Time`, `Cell Voltage Middle`; `StringData` — `Power`, `Watt-hours`. Все проблемы возвращаются списком (`problems`), а не одной первой: на `TestData` одновременно срабатывают запись в генерируемую колонку и несовпадение числа значений (17 против 16). Файл `tests/test_db_marks.py` дополнен разделом PostgreSQL |
| `tests/test_exclude_defaults.py`: 4 провала | Провал давало ТОЧНОЕ равенство числа файлов ЧУЖИХ корпусов, которые выросли (z 162→222, MD.Viewer 16→180, RdpMON 1266→1274, Clean_BOM_Senior 6→18). Базовые числа стали нижней границей |

### 4.2 Находки, которые были исправлены

| Находка | Что сделано и чем доказано |
|---|---|
| PostgreSQL не собирал `key`/`extra` | Добавлены `attidentity`/`attgenerated` и `indisprimary`/`indisunique`: признаки выводятся единой функцией `db_index.pg_mark_fields` и разбираются тем же правилом `db_marks`, что и MySQL. Живого PostgreSQL на машине нет (`psql`, `pg_dump` отсутствуют), поэтому SQL-часть не проверена на сервере — проверено преобразование признаков: `tests/test_db_marks.py`, раздел «PostgreSQL», 6 проверок (IDENTITY, BY DEFAULT, STORED, VIRTUAL, обычная колонка, PK+identity) |
| `db_cross` брал имена таблиц регуляркой по тексту | Основной источник — рёбра `@table:`, которые ставит извлекатель языка после разбора литерала; текстовый образец остался запасным путём для корпусов без рёбер. Ответ называет оба источника (`used_sources`), а если рёбер нет — в `limits` появляется «поможет переиндексация». Замер на собственном корпусе: 1 имя из рёбер, 50 только из текста |
| `db_writer.py:18-22` расходился с кодом | Документ приведён к фактическому устройству: живой слой пишет СВОИ таблицы `db_live_*`, а `db_table`/`db_column`/`db_fk` принадлежат схеме из миграций — именно поэтому `db_drift` умеет их сравнивать |
| Дубль импорта `field, field` в `extract/generic.py` | Исправлен |
| Новая операция не попадала в метрику фактов | `summarize_result` разбирает вложенные ответы (`families → members → evidence`) с ограничением глубины и без повторного счёта. Замер на `search_multi`: было «фактов 0», стало 14 фактов, 6 со ссылкой `path:line`. Проверка — `tests/test_telemetry_facts.py` (14 проверок, включая контроль отсутствия двойного счёта) |
| `test_dedupe_prompt.py` требовал `ragraf_mode` в разделе о дублях | Перечисление канала работы (cli / mcp-stdio / mcp-http / both) описано в своём разделе `8.20` промпта, и проверка смотрит туда; настройки схлопывания проверяются в разделе о дублях. 31 проверка проходит |
| `test_auto_thresholds.py` застыл на числах дня | Проверяется СВЯЗЬ «стоимость → порог», а не абсолютные 4..8 / 15..30: корпус вырос (опорный замер 20,81 с → 63,17 с), множитель 1,74 поднимает базовые 5/20 до 9/35 — и это верный ответ. 22 проверки проходят |
| `test_audit_report.py` расходился с отчётом SagaAI | Отчёт — документ ДРУГОГО проекта, и он изменился после аудита: замер дал 118 мест `except pass` в 95 файлах против записанных 119 и 96. Таблица отчёта приведена к новому замеру (верхние строки совпали: 15/14/9/6/5/4, изменился остаток — 65 в 34 файлах), добавлена датированная пометка о пересчёте, а проверка теперь читает числа ИЗ ОТЧЁТА и сравнивает с замером — расхождение снова станет видимым, а не застынет |

### 4.3 Метрики эффекта (пункт 3.4 ТЗ, часть)

В журнал добавлены `stale` (ответ отдан с пометкой устаревания) и `missed`
(честное «не найдено»), в сводку — `hit_rate`, `calls_with_facts`, `avg_ms`,
`misses`, `stale_served`; CLI печатает строку «эффект». Замер на собственном
журнале: 668 вызовов, попаданий **63,5 %** (424 из 668), среднее время 947 мс,
«не найдено» 1, **устаревших ответов 0** — последнее и есть требование приёмки
`stale_served = 0`. Проверка — `tests/test_telemetry_facts.py`, раздел 6.

Триггерная таблица «перед чем → что вызвать» добавлена в двух местах: `AGENTS.md`
этого проекта (раздел 8, п. 1) и промпт агента HARNESS (раздел 8.3 — таблица
переписана с «вопрос → операция» на «шаг работы → обязательный вызов», включая
новые операции `search_multi`, `duplicates`, `entrypoints`, `dead_code`).

---

## 5. Посторонние находки (не правились)

1. **Чужой проект потерял правило оформления.** `test_css_layer.py` и
   `test_class_usage.py` падают на классе `z-agr-gantt`: он используется в
   `agreements/gantt.php`, а правила в CSS нет НИ В ОДНОМ файле проекта
   `z.batteryservice.ru` (проверено поиском по диску). Индекс прав, проверка
   сторожит возврат дефекта — обновлять её владельцу того проекта.
2. **Проверки SagaAI не запускаются в этом окружении.**
   `test_sagaai_instruction.py`, `test_sagaai_page_loads.py`, `test_sagaai_skill.py`
   требуют `sqlalchemy` и `streamlit` — их нет ни в `.venv` RAGRAF, ни в
   системном `python`. Это не дефект RAGRAF: проверки читают чужой проект.
3. **Реестр разработок допускает корпус-контейнер.** Запись `AutoClaw` с корнем
   `<полигон>` покрывает 54 235 файлов — то есть все проекты сразу. Технически
   она теперь честно помечена «заведена, индекс не строился», но полезна ли такая
   разработка в учёте — вопрос владельца: индексация контейнера смешает проекты.
4. **Текстовый образец в `db_cross` остаётся эвристикой.** Рёбра `@table:` дают
   факт, но только там, где имя таблицы — литерал. При сборке запроса
   конкатенацией (`"SELECT * FROM " . $table`) имени нет ни в рёбрах, ни в
   разборе, и текстовый образец тоже его не найдёт (он видит только готовые
   литералы) — такие места остаются невидимыми для `db_cross`, и это названо в
   ответе полем `used_sources.from_text_only`.
5. **`test_css_layer`/`test_class_usage` — единственные красные из проверок
   проекта**: обе сторожат чужой корпус и описаны в п. 1.

---

## 6. Что осталось по полному плану ТЗ

Ниже — не «пожелания», а пункты исходного ТЗ и его разделов, по которым работа
не сделана. Каждый назван так, чтобы его можно было взять в работу отдельно.

| Пункт ТЗ | Состояние | Что именно осталось |
|---|---|---|
| P2-1 `ragref_diff(since=rev)` | **не сделано** | Операции сравнения двух ревизий кода нет. Дрейф есть только у слоя баз (`db_drift`) и у журнала расхождений. Нужны: сравнение узлов/рёбер/файлов между ревизиями, «что изменилось с прошлой задачи» — для ответа «код изменился здесь» без переиндексации |
| P2-2 Режим `answer=true` | **не сделано** | Готового текстового ответа из нескольких операций (сборка `search` + `callers` + `impact` в один абзац) нет. Сейчас каждый шаг — отдельный вызов: для агента это 3–5 обращений там, где нужно одно |
| P2-3 `ragraf_note` / `notes` | **не сделано** | Заметок между сессиями нет: знание о проекте (например «этот каталог — копия, рабочая вон та») негде сохранить, и следующая задача начинает с нуля. Нужны хранение заметок в control-хранилище и их выдача в `index_status` |
| P2-4 Метрики эффекта | **сделано частично** | Готово: `hit_rate`, `calls_with_facts`, `avg_ms`, `misses`, `stale_served`, строка «эффект» в CLI, подсчёт фактов у новых операций. Осталось: поле `saved_calls` (сколько вызовов чтения/grep заменено) объявлено, но не заполняется — для него нужен учёт на стороне клиента, а не индекса |
| §3.1 Триггерная таблица | **сделано** | `AGENTS.md` этого проекта (раздел 8, п. 1) и промпт HARNESS (раздел 8.3, таблица «шаг работы → обязательный вызов», включая `search_multi`, `duplicates`, `entrypoints`, `dead_code`) |
| §3.2 Технический гейт в клиенте (warn/deny `grep` без `ensure`) | **не сделано** | Исходники клиента лежат в упакованном `resources\app.asar`: путь `...\app.asar\dsh\` как каталог недоступен, в `app.asar.unpacked` только нативные модули (`node-pty`, `sherpa-onnx`, …). Править нечего, ПОКА гейт мыслится правкой исходников. Механизм расширения при этом есть и проверен: профиль DSH собирается патчами (`~/.dsh/profiles/desktop/cordis.patch.yml`, ключ `dsh.profile.bundles` в `package.json`, каталог `.plugin-manager`), то есть гейт реализуем как ПЛАГИН профиля — отдельная работа, требующая API плагинов DSH |
| §3.3 Дешёвый путь (`ensure` ≤ 3 с на неизменённом корпусе) | **выполнено, замер:** `ensure RAGRAF` на актуальном корпусе — status «индекс актуален», **1,54 с** (два прогона подряд 1,62 с по стенным часам) |
| §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) — причины в разделе 5.

---

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

| Проверка | Результат |
|---|---|
| `tests/test_structure_p1.py` (новый, P1-1/P1-2) | 24 из 24 |
| `tests/test_polyglot_p13.py` (новый, P1-3) | 17 из 17 |
| `tests/test_db_marks.py` (новый, P1-4, включая живую базу и раздел PostgreSQL) | 48 из 48 |
| `tests/test_budget_stage.py` (новый, P0-2: остановка внутри стадии) | 11 из 11 |
| `tests/test_telemetry_facts.py` (новый, факты и метрики эффекта P2-4) | 14 из 14 |
| `tests/test_catalog_state.py` (новый, состояние реестра) | 18 из 18 |
| `tests/test_db_index.py` (живой слой баз) | пройдена |
| `tools/Invoke-ProjectDoctrine.ps1` | 19 проверок, 0 ошибок, PASS |
| `tools/check_bom_crlf.py` | 38 текстов, требуют очистки 0 |
| `tests/test_dedupe_prompt.py` (промпт HARNESS, 31 проверка) | пройдена |
| `tests/test_auto_thresholds.py` (приведён к связи «стоимость → порог») | 22 из 22 |
| `tests/test_audit_report.py` (числа читаются из отчёта SagaAI) | 22 из 22 |
| `tests/test_lang_routing.py`, `test_generic_extract.py`, `test_inheritance.py`, `test_cpg_call_names.py`, `test_graph_algo.py`, `test_capability_consistency.py`, `test_cli_ops_dispatch.py`, `test_mcp.py`, `test_mcp_compat.py`, `test_access_live.py`, `test_mcp_http.py`, `test_text_from_index.py`, `test_dedupe.py`, `test_ensure.py`, `test_task_stats.py`, `test_exclude_defaults.py` | пройдены (26 проверок подряд, 0 провалов) |
| `tests/test_css_layer.py`, `test_class_usage.py` | **провалены** — чужой корпус, причина в разделе 5, п. 1 |
| `tests/test_sagaai_instruction.py`, `test_sagaai_page_loads.py`, `test_sagaai_skill.py` | не запускаются: нет `sqlalchemy`/`streamlit` (раздел 5, п. 2) |
| Переиндексация собственного корпуса (rev-0103, 594 файла) и корпуса BWO (rev-0008) | выполнены; `capability=full` |
