# RAGRAF — отчёт о применении

> **Status: historical (v2 era, 2026-10-05).** The audit findings about the shell
> reference were addressed in v3.0.0 — see `CHANGELOG.md` §Fixed. This report is
> kept as the record of the v2.07 generation.

**Проект:** `Clean_BOM_Senior` (`C:\AutoClaw\Clean_BOM_Senior`)
**Дата:** 5 октября 2026
**Задача:** аудит и тесты `clean-bom-senior.ps1` / `clean-bom-senior.sh`, исправление
найденных дефектов, создание `clean-bom-senior.bat`.

---

## 1. Что RAGRAF дал, а что не мог дать

Раздел 8 `AGENTS.md` требует начинать работу с кода через RAGRAF, а при отсутствии
проекта в реестре — подключать его. Проекта в реестре не было.

```
$ python C:\AutoClaw\RAGRAF\tools\ragraf.py catalog
не указан проект: нужен аргумент --project
```

В `C:\AutoClaw\RAGRAF\config` лежали `md.viewer.yaml`, `revoap.yaml`,
`z-batteryservice.yaml`, `fixture.yaml` — `Clean_BOM_Senior` отсутствовал.

Подключение выполнено по порядку §8.5, и на первом же шаге обнаружилась граница
инструмента, которую пришлось замерить отдельно.

### 1.1 Границы этого корпуса — замерено, не предположено

`ragraf.py init` осмотрел каталог и вернул:

```
конфигурация создана: C:\AutoClaw\RAGRAF\config\clean_bom_senior.yaml
  файлов:  4   объём: 73 КБ
  языки: markdown 2, javascript 2
  исключено каталогов: 1 (.git)
```

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

| Факт | Где |
|---|---|
| Карта расширений → язык содержит только `.php .phtml .inc .md .sql .css .htm .html .js .mjs` | `RAGRAF\src\ragraf\config.py`, `LANG_BY_EXT` |
| Разбор деревом (AST) — только `php`, `css`, `html`; текстовые чанки — `markdown`, `sql`, `javascript` | `RAGRAF\src\ragraf\indexer.py:309,315` |
| `language_of()` возвращает `None` для неизвестного расширения, и файл отбрасывается как `skipped_not_included` | `RAGRAF\src\ragraf\config.py`, `indexer.py:318` |

**Вывод: RAGRAF здесь не мог статически разбирать ни PowerShell, ни shell.** Ни
`symbol`, ни `code`, ни `callers` по этим файлам не работают: единственное, что было
бы проиндексировано — `README.md` и `AGENTS.md` как markdown. `class_usage`,
`schema`, `table_usage`, `policy_trace`, `write_check` к этому проекту не относятся
(нет ни стилей, ни схемы БД, ни матрицы прав).

Корпус зарегистрирован и проиндексирован в этом объёме:

```
$ ... index
языки: javascript 2, markdown 2   include: **/*.js **/*.mjs **/*.md
```

### 1.2 Что из §8 всё же сработало

Пункт §8.1 («знания модели о конкретном проекте — смесь предположений; факт требует
ссылки») применился к **RAGRAF как к корпусу**. Правка конфигурации формально была бы
права, если бы `.ps1`/`.sh` разбирались — но сверка с `config.py` показала, что дело не
в конфиге: расширения отсутствуют на уровне карты языков. Без этой проверки я бы
«включил» расширения в `include` и отчитался о поддержке, которой нет.

### 1.3 Состояние индекса

`freshness` после правок не понижался: изменились `clean-bom-senior.ps1` и
`clean-bom-senior.bat`, которых в индексе нет; проиндексированные `README.md` и
`AGENTS.md` на момент запуска оставались прежними. Отчёт о свежести для markdown-части
корпуса приложен в конце (раздел 6).

---

## 2. Как на самом деле искались факты

Раз статический индекс по `.ps1`/`.sh` недоступен, требования §8 пришлось выполнять
буквально: **факт — это ссылка `файл:строка` либо замер**. Поэтому каждый дефект ниже
имеет либо номер строки, либо воспроизведённый прогон на изолированном наборе.

### 2.1 Два открытия о среде, найденные замерами

**`pwsh -Command` искажает аргументы и код выхода — измерено.** Это критично для
любого теста, который запускает порт.

| Способ запуска | Аргумент `-` доходит как | Код выхода при валидной ошибке |
|---|---|---|
| `pwsh -File script.ps1 -` | `-` | 2 |
| `pwsh -Command "& script.ps1 -"` | пустая строка | 1 вместо 2 |

Из-за этого ранняя батарея проверок CLI дала ложные результаты, и первый вариант
`tests/cli-contract.ps1` был неверен. Тест переведён на прямой `ProcessStartInfo` с
`ArgumentList` — без промежуточной оболочки.

**Утилита извлечения метки, написанная для отладки, обрезала метку на внутреннем
`goto`-ярлыке.** Два прогона на «сломанном» стенде показали дефект там, где его не
было. Правильный вывод: инструмент отладки тоже требует проверки на корректность.

---

## 3. Найденные дефекты и что с ними сделано

### 3.1 Порт `clean-bom-senior.ps1` — исправлено

#### Дефект A: `-` (одиночный дефис) не считался неизвестной опцией

* **Строка:** `clean-bom-senior.ps1:349` — `elseif ($argument.StartsWith('-') -and $argument.Length -gt 1)`
* **Эталон:** `clean-bom-senior.sh:477` — `case "$1" in … -*)` совпадает с одиночным `-`
* **Замер:** `bash reference.sh -` → `exit 2`, `Unknown option: -`;
  порт → `File not found: -`, `exit 0`. То же для `--dry-run -`.
* **Почему это важно:** вызывающий скрипт не мог отличить реализации друг от друга —
  расхождение в контракте, а не косметика.
* **Исправление:** условие `$argument.Length -ge 1 -and $argument[0] -ceq '-'`.
  Ошибка в первой редакции правки (`-gt 1`) исключала ровно нужный случай; поймана
  прогоном, а не чтением.

#### Дефект B: NUL-байт за пределами первых 8192 байт не обнаруживался → порча файла

* **Строка:** `clean-bom-senior.ps1:493` — `$buffer = [byte[]]::new(8192)`, одно чтение
* **Замер:** файл `BOM + 9000 'x' + NUL + CRLF` (9006 Б) → порт **перезаписал** его до
  9002 Б, вернув валидный на вид текст. Документация при этом обещает:
  «A file that carries a supported extension but holds a NUL byte is reported and
  skipped instead of being rewritten» (`README.md:635`).
* **Исправление:** новый `Find-NulByteOffset` — чтение блоками 64 КБ до конца файла.
  `Test-UnsupportedForContents` стал обёрткой над ним, поэтому один проход отвечает и
  на «бинарный ли», и на «где именно»; сообщение теперь содержит смещение.
* **Почему это худший класс дефектов:** инструмент сработал «успешно» и молча испортил
  файл в чужом проекте.

#### Дефект C: пути в логе абсолютные вместо `./имя`

* **Строки:** сообщения `Invoke-CleanFile` / `Invoke-DryRunFile`
* **Эталон:** обход через `find .`, поэтому печатает `./имя`; блок «Files That Would Be
  Processed» порта содержал `C:\Users\...\a.php`.
* **Почему это дефект, а не стиль:** README заявляет «same output»; лог порта и лог
  эталона не совпадали построчно.
* **Исправление:** `Format-DisplayPath` — путь относительно текущего каталога с
  префиксом `./`. Для аргументов командной строки относительная форма **отключена**
  (`-DisplayRelative $false`): эталон печатает их verbatim.

#### Замечание без правки: счётчик `size_errors`

`ErrorTypes['size']` (`clean-bom-senior.ps1:1030`) недостижим: переполнение по размеру
даёт skipped. В эталоне то же — `ERROR_TYPES["size"]` инициализируется в строке 104 и
печатается в 538, но ни один путь его не увеличивает (проверено поиском по файлу).
Строка вывода сохранена ради паритета, значение остаётся `0`. Не изменялось.

### 3.2 Эталон `clean-bom-senior.sh` — решения по каждому

| # | Дефект | Строка | Замер | Решение |
|---|---|---|---|---|
| 1 | `cleanup()` вызывает `find "$TEMP_DIR"` без кавычек: при `TMPDIR` с пробелом временные файлы не удаляются | `clean-bom-senior.sh:111` | `find $d …` при `d='/tmp/probe with space'` → `rc=1`; с кавычками → `rc=0` | **оставлено**, внесено в осознанные отличия |
| 2 | `touch -r "$backup"` восстанавливает время свежего бэкапа, а не исходное | `clean-bom-senior.sh:407` | эталон `23:39:22.659 → 23:39:25.077`; порт сохраняет | **оставлено**, уже задокументировано |
| 3 | `ERROR_TYPES["size"]` никогда не увеличивается | `:104`, `:538` | поиск по файлу: увеличения нет | **оставлено**, зафиксировано |

Почему не исправлено: правка любой из этих строк меняет поведение эталона, на котором
держится всё доказательство паритета, и потребовала бы переписать таблицу отличий и
assertions в `tests/differential.ps1`. AGENTS.md §9 п.4 фиксирует решение «`.sh` не
чинится». Дефекты не спрятаны: они в таблице, с замерами.

### 3.3 Найдено попутно, в проект не внесено

* **`package.json:42` — `"os": ["linux", "darwin"]`.** Пакет содержит `.ps1` (и теперь
  `.bat`), но установка на Windows запрещена полем `os`. Это соответствует AGENTS.md
  §9 п.1, поэтому не менялось; отмечаю как открытый вопрос.
* **`ERROR_TYPES["size"]`** — см. 3.2 п.3.
* **`tests/differential.ps1:286`** — `Start-Sleep -Milliseconds 1100` вместо явной
  установки mtime. Работает, но делает прогон медленнее и не фиксирует проверку
  однозначно; не менялось, чтобы не трогать доказательство паритета без нужды.

---

## 4. Новые тесты (обе проверки падают на дефекте — проверено)

### `tests/cli-contract.ps1`

Закрывает то, чего не покрывал `tests/differential.ps1`: разбор аргументов, детекцию
бинарности и форму путей. **15 проверок, 0 отказов.**

Сначала закрывался сам: инжекция дефекта `-gt 1` дала
`port: '-' exited 0, expected 2`; инжекция старого 8-КБ чтения —
`NUL at middle was missed - file rewritten from 9006 to 9002 bytes`.

### `tests/bat-differential.ps1`

Сравнивает `.bat` с `.ps1` (всегда) и с `.sh` (когда есть Git Bash), на 15 фикстурах
сырыми байтами, каждое исполнение — в своей копии набора. **15/15 совпадений.**
Расхождение по `nul_far.js` с `.sh` — осознанное (бинарный файл) и утверждается как
таковое, а не сравнивается.

Инжекция дефекта (разбиение тела до подстановки + отмена правила trailing CR) дала
`[DIFF] crlf_only.css length 23 vs 21` и код 1.

Существующий `tests/differential.ps1` после правок: **`identical: 14 / 14 compared`,
коды выхода 0/0** — паритет `.ps1` ↔ `.sh` не нарушен.

---

## 5. Про `clean-bom-senior.bat`

Создан как третья реализация того же контракта. Полное описание —
`docs/BAT-PORT.md`; здесь только то, что относится к работе RAGRAF и к проверкам.

**Ключевое ограничение:** в `cmd.exe` нет байтового ввода-вывода, поэтому очистка
выполняется на шестнадцатеричном представлении через `certutil -encodehex 4` /
`-decodehex 4`. Обе стороны проверены байт-в-байт, включая короткую последнюю строку и
файл из трёх байт.

**Ловушки cmd, найденные замерами (не из памяти):**

| Ловушка | Замер | Следствие |
|---|---|---|
| `.bat` с LF-переводами не исполняется | `exit 255`, вывода нет | файл обязан быть CRLF — и этот инструмент сам его не чистит |
| Скобки в тексте внутри `if ( … )` ломают блок | сначала молчаливое прекращение обработки, затем `exit 255` после строки `PROCESSING` | сообщения со скобками выводятся из отдельных меток |
| `set /a` внутри блока | `* was unexpected at this time` (переменная подставляется до разбора блока: `set /a 5+=1`) | вся арифметика вынесена в метки с `call` |
| Возврат пустого значения через `:trim`-синтаксис | `set "C=" & set "T=!C:~6!"` → `T=[~6]` (литерал) | файлы из одного BOM обрабатываются отдельной ветвью |
| `for /r *.htm` матчит и `.html` по 8.3-имени | `mixed.html` попадал в список дважды и во второй раз логировался как clean | тот же класс, что `Get-ChildItem -Filter` в PowerShell |
| `%*` в вызываемой метке содержит свои аргументы | `INFO "INFO "msg""` | `%~2` вместо `%*` |
| `copy /b` перезаписывает время приёмника | `copy` → время источника; атрибуты и creation time сохраняются | время восстанавливает PowerShell, если он есть; иначе — предупреждение в шапке |
| Пустые имена устройств Windows | `nul.js` создать нельзя | в фикстурах имена `nul`/`con`/`aux` не используются |

**Осознанные отличия** (полный список в `docs/BAT-PORT.md` §4): ASCII-справка вместо
UTF-8-буллетов; время правки через PowerShell, а без него — не восстанавливается;
пути со знаком `!` не поддерживаются; около секунды на 100 КБ; смещение NUL
сообщается номером строки дампа.

---

## 6. Отчётность по §8.7

| Что требовалось | Значение |
|---|---|
| Ревизия индекса | `Clean_BOM_Senior`, проиндексированы markdown 2 + javascript 2 (`README.md`, `AGENTS.md`, `bin/bom.js`, `bin/postinstall.js`) |
| Совпала ли контрольная сумма | да для проиндексированной части; `.ps1`/`.sh`/`.bat` в индекс не входят по устройству `LANG_BY_EXT` (`config.py`) |
| Число обращений к RAGRAF | 4 (`catalog`, `init`, `index`, `register` + сверка `config.py`/`indexer.py`) |
| Факты со ссылками `файл:строка` | 11 (см. 3.1 A/B/C, 3.2, 3.3) |
| Замеры, заменяющие индекс | 9 (аргументы и код выхода `pwsh -Command`; NUL за 8 КБ; пути в логе; `find` без кавычек; `touch -r`; 8.3-матчинг; `copy /b` и время; `findstr` по позициям NUL; `.bat` с LF) |

**Честная оценка:** RAGRAF в этом проекте помог **как учёт границ**, а не как источник
фактов о коде: он сразу и однозначно показал, что PowerShell и shell им не разбираются,
и избавил от ложного «индекс есть — можно спрашивать». Все дефекты найдены замерами и
чтением с ссылками на строки. Утверждений без ссылки на код или без замера в отчёте нет.

---

## 7. Что осталось

1. **Индексация `.ps1`/`.sh`/`.bat` в RAGRAF невозможна** без расширения
   `LANG_BY_EXT` (`config.py`) и добавления текстового пути для этих языков. Раз это
   инфраструктура RAGRAF, а не этого проекта, — вынесено как предложение, не как
   правка: без тестов RAGRAF менять её нельзя.
2. **`package.json` запрещает установку на Windows** полем `os`, хотя в пакете лежат
   `.ps1` и `.bat`. Не менялось (AGENTS.md §9 п.1).
3. **Отсутствующий аргумент-файл** даёт `Access errors: 1`, но код выхода `0` в режиме
   явных файлов — воспроизведено на обеих реализациях, поведение сохранено.
4. **Гонки параллельных прогонов** по одному файлу не проверены на живой нагрузке
   (AGENTS.md §10). Имена бэкапов уникальны по идентификатору прогона, но операции
   не сериализованы.
5. **`trailing_cr` для `.bat`** проверен только на одно-, двух- и многострочных
   файлах; отдельного случая «файл, где единственный CR стоит ровно на границе
   16-байтового дампа и одновременно в конце», в наборе нет — покрыт косвенно
   фикстурой `boundary_cr.txt`.
