# Отчёт внешней команды: BASHNYA, движок 0.1.34

> Текст отчёта приведён **как получен** (транскрипт сообщения команды переноса
> первого уровня BASHNYA, `p2game/bashnya`). Решения по пунктам — в конце файла,
> в разделе «Решения». Ничего из отчёта не выброшено: спорные и уже закрытые
> пункты тоже на месте, чтобы история не подчищалась задним числом.

Составлено командой переноса первого уровня BASHNYA (`p2game/bashnya`) при работе
на **готовом дистрибутиве без правок ядра**. Все пункты воспроизводились на живом
бинарнике; ничего не выдумано из документации. Чекбоксы — чтобы отмечать
«принято / исправлено / не будем».

**Окружение всех проверок**

| Параметр | Значение |
|---|---|
| Версия | `Russiano2D 0.1.34` |
| Платформа | macOS 27.0.1, Apple Silicon (arm64), GPU-бэкенд `metal` |
| Бинарник | `russiano2d`, sha256 `e67b8d73f9c4e73f92b288e960a2ccf51d43bc971555162a3d7a42800cace878` (совпадает с `SHA256SUMS.txt`) |
| Как проверяли | обычный запуск, `--agent --headless --fixed-dt 0.0166666667`, `--stats`, скриншоты, ~500 прогонов |
| Объём работы | перенесён уровень 20×150 клеток: игрок, камера, генератор, враги, ловушки, бой, HUD; 17 автопроверок + 239 физических проверок переходов уровня |

Дополнительные материалы (полные журналы и контекст): `bashnya/MIGRATION_BLOCKERS.md`
(формальные блокеры с repro), `bashnya/tools/verification/README.md` (§7 — грабли,
на которые мы наступили), `bashnya/tools/notes/api_*.md` (справки по API, снятые
с бинарника).

---

## A. Дефекты поведения

### A1. У обёртки узла нет читаемых `.x`/`.y`, а присваивание молча создаёт JS-свойство

**Что ожидали:** `node.x` / `node.y` — координаты узла (так написано в
`HIGH_LEVEL_API.md` §2 и так **использует пример движка** `game/level.js:264,298`).

**Что на самом деле:** чтение даёт `undefined`, запись молча создаёт обычное
JS-свойство и узел не двигает. Ошибка не появляется ни в журнале, ни в оверлее —
`undefined` уезжает в арифметику и превращает координаты в `NaN`.

```js
$('<rect>', { id: 'r' }).at(100, 200).size(32, 32).appendTo($.world);
$.log(typeof $('#r').x);       // undefined   (ожидали number)
$('#r').x = 500;               // молча создаёт JS-свойство
$.log($('#r').pos().x);        // 100 — узел не двинулся
```

**Почему это важно:** мы потеряли на этом несколько часов (меч игрока «исчез»,
потому что позиция стала `NaN`; проверки выхода/падения не срабатывали). Грабли
тиражируются примером самого движка.

**Предложение:** сделать `.x`/`.y` настоящими аксессорами обёртки (как
`.pos()`/`.at()`), либо в строгом режиме бросать ошибку на присваивание
неизвестного свойства, либо убрать `node.x = …` из `game/level.js`.

### A2. Импорт модулей не выходит за каталог игры; `..` в спецификаторе не нормализуется

**Что ожидали:** относительный импорт резолвится относительно файла-источника,
`..` нормализуется как в браузере/Node.

**Что на самом деле:** движок склеивает каталог игры с текстом спецификатора и
**выбрасывает сегменты `..`**. Абсолютный путь просто приклеивается к каталогу игры.

```bash
# из tools/devgames/probe/main.js:
import '../../game/src/config.js'
# → JS: не удалось прочитать модуль
#   '…/tools/devgames/probe/game/src/config.js'
import '/abs/path/game/src/config.js'
# → JS: не удалось прочитать модуль
#   '…/tools/devgames/probe/abs/path/game/src/config.js'
```

Внутри каталога игры `..` работает (`game/src/enemies/walker.js` → `../config.js` ✓).

**Почему это важно:** нельзя держать рядом с игрой инструменты, примеры и
отладочные игры, которые импортируют код игры; приходится складывать всё внутрь
каталога игры и включать флагом.

**Предложение:** нормализовать путь; при выходе за корень игры — явная ошибка
(«импорт вне каталога игры»), а не молчаливая склейка.

### A3. `areaOverlap()` фильтрует пересечения в ДВЕ стороны

**Что ожидали:** `areaOverlap(name)` возвращает всё, что геометрически
пересекается с областью; маска области — «кого я вижу» (как у `$.world.raycast`).

**Что на самом деле:** тело попадает в результат, только если
`area.mask & other.layer != 0` **И** `other.mask & area.layer != 0`. Маска цели,
которой владеет другой модуль, решает, увидит ли её моя область.

Получено `["w"]` — стена (layer 1, mask «всё») видна, а лежащая в области цель
`t` (layer 2, mask `0x1|0x4`) нет, потому что `area.mask(1) & t.layer(2) == 0`.
С `{layer:0x4, mask:0xFFFFFFFF}` цель появляется, с `{layer:0x8, …}` — снова
исчезает (вторая сторона фильтра); `$('#t').mask(0xFFFFFFFF)` возвращает её.

**Почему это важно:** named-area нельзя использовать как «кто меня касается»,
если слоями владеет чужой код — а именно так устроены хитбоксы в игре.

**Предложение:** односторонний фильтр по маске области (как в `$.world.raycast`)
либо явное описание двусторонности в `HIGH_LEVEL_API.md`.

### A4. `.frame(i)` не обновляет `node.frame_index`

**Что ожидали:** после `.stopAnim(); .frame(0)` узел показывает кадр 0 и
`frame_index === 0` (или `frame_index` — не публичное поле).

**Что на самом деле:** `.frame()` пишет только `node.sprite`; `frame_index`
двигают исключительно дорожки `.animate()`, поэтому он остаётся равным
последнему кадру анимации. Отображается правильный кадр, а поле врёт.

Получено `{frame_index: 3, sprite_index: 0}` — то есть кадр 0, а поле говорит 3.

**Предложение:** `def('frame')` пишет и `node.frame_index = index`, либо убрать
`frame_index` из публичных описаний узла.

### A5. Запись файлов проваливается МОЛЧА

**Что ожидали:** если файл нельзя записать, в журнале есть причина (путь, errno).

**Что на самом деле:** `$.fs.write`, `$.fs.writeJSON`, `$.store.save()`,
`$.store.file(f).save()` возвращают `false` и не пишут в журнал **ничего**.
Легко не заметить: игра «работает», а прогресс не сохраняется.

```bash
# базовый каталог = каталог бинарника (read-only в поставке)
cd /tmp && /path/to/russiano2d --game /tmp/mygame --seconds 2
# в игре: $.fs.basePath() → каталог движка
#         $.fs.write('x.json','{}') → false        (без сообщений)
#         $.store.save()            → false        (без сообщений)
```

**Предложение:** писать в журнал одну строку с путём и причиной отказа
(`read-only`, `ENOENT`, `EACCES`), как это делается для текстур и звуков.

### A6. Снимок узла без компонента здоровья показывает `alive:false, hp:0`

**Что ожидаемо:** узел без `.health()` не должен выглядеть мёртвым.

**Что на самом деле:** в `state.entities` / `query` / `inspect` такой узел
описывается как `{hp: 0, max_hp: 0, alive: false}` — автоматическая проверка
«враг жив?» даёт ложный отрицательный результат.

**Предложение:** `alive: null` (или не отдавать поле), когда у узла нет
компонента здоровья.

### A7. Предупреждение на каждый узел с незнакомым тегом

**Что наблюдаем:** `$('<walker>')` → `$: неизвестный тег <walker> — создаю пустой
узел` на КАЖДЫЙ узел. На нашем уровне (около 50 врагов и 70 ловушек) это давало
**от 106 до 125 строк** шума на каждую загрузку — пока мы не перешли на встроенные
теги `<enemy>`/`<sprite>` ради этого предупреждения. При этом узел с незнакомым
тегом создаётся и работает: класс, тело, коллизия, селектор `.walker` — всё ок.

**Почему это важно:** при отладке журнал грепается на ошибки, и 125 строк шума
топят настоящие сообщения. Публичного реестра тегов нет (`$.kinds` — про рендер),
поэтому «правильного» способа завести свой тег тоже нет.

**Предложение:** либо дать `$.tag.define(name, defaults)`, либо писать это
предупреждение один раз на имя тега (или только при включённом оверлее).

### A8. `state.entities` раздувает агентский JSON (168–250 КБ на снимок)

При 770 узлах поле `state.entities` даёт **168 КБ** JSON на каждый `state`-снимок
(на других уровнях доходило до ~250 КБ) и заметно бьёт по скорости агентских
прогонов с частыми запросами состояния.

**Предложение:** урезать поля по умолчанию или дать `state` параметр
(`{"cmd":"state","entities":{"limit":50,"sel":".enemy","fields":["id","x","y"]}}`).

---

## B. Документация расходится с бинарником

### B1. `--seconds` / `--frames` в агентском режиме не действуют

`AGENT_API.md` §1: «`--agent` можно совмещать с `--stats` и `--seconds`».
Фактически `--stats` работает, а `--seconds`/`--frames` в `--agent` **игнорируются**
(кадры двигает только команда `step`). Прогон без внешнего клиента висит вечно.

**Предложение:** либо поддержать флаги в агентском режиме, либо поправить §1 и
вывести предупреждение при их совместном использовании.

### B2. `console` в JS-контексте не существует, а док упоминает `console.log`

`AGENT_API.md` §2: «весь прочий вывод движка (логи, предупреждения, `console.log`)
идёт в stderr». Фактически `typeof console === 'undefined'` — есть только
`$.log`/`$.logger`. Первое, что пишет человек, — `console.log(...)` — падает
в `$.ready` с `console is not defined`.

**Предложение:** либо завести минимальный `console.log/warn/error` → `$.log`,
либо убрать упоминание из дока и сказать про `$.log` в первом же абзаце.

### B3. В поставке нет `tools/agent_client.py` и `tools/run_tests.py`

`AGENT_API.md` §4 ссылается на эти файлы и на «готовый раннер тестов». В
дистрибутиве 0.1.34 их нет (есть только `sdk/tools/*`). Каждый, кто делает
автопроверки, пишет клиента заново.

**Предложение:** положить клиент в поставку или поправить ссылку.

### B4. Мелочи протокола агента

* §3.4: «циклический результат → ошибка `преобразование результата в JSON не удалось`» —
  фактически приходит `{"ok":true,"result":"<объект с циклом в ссылках>"}`.
* §3.3.1: «без `$.agent.install()` команда `query` отвечает ошибкой» — фактически
  отвечает корректно (пусто/`null`), потому что `$` поднимается всегда.
* Таблицы команд: не описаны `touch` и `pad`; `mouseMove` принимается только в camelCase
  (`mousemove` молча не работает).
* Пример `ping` в §2 показывает `frame:0`, фактически к моменту первого ответа уже прошёл кадр.

### B5. `DEVTOOLS.md` сильно отстал

Документ описывает «первый срез: PERF не подключён, таймлайна событий нет».
Фактически доступны вкладки PERF / EVENTS / PHYSICS / RENDER / AUDIO / INPUT /
ASSETS / RESOURCE / CONSOLE / ШАГ / REPORT, детектор hitch, picking и ~70 методов
`$.devtools.*`, отдающих машиночитаемый JSON (`$.devtools.physics()`,
`$.tilemap.debug()`, `$.gfx.light.debug()` и т.д.). `highlevel/devtools.md`
перечисляет 5 методов вместо ~70.

**Почему это важно:** мы почти написали свой отладочный HUD, потому что по доке
казалось, что встроенного профайлера нет.

### B6. `highlevel/store.md` документирует методы, которых нет

В таблице есть `keys() / all() / list() / count()` и `save/load/exists`.
Фактически у `$.store`: `file, set, get, has, remove, clear, keys, all, setAll,
save, load, autoSave, stopAutoSave` — **`list()`, `count()`, `exists()` отсутствуют**
(проверено `Object.keys($.store)`).

---

## C. Мелкие неудобства и лимиты (не дефекты, но стоит знать/документировать)

* Звуковых каналов **16** на всю игру; при заполнении вытесняется самый
  неважный звук, иначе `play()` возвращает `-1`. Для плотной сцены (шаги + удары +
  фон) это близко к пределу — стоило бы поднять или дать `priority` в документации
  как обязательный приём.
* Пул эмиттеров искр `$.hits` — **8 слотов**: девятый одновременный удар
  обрывает первый залп.
* `$.random.seed(n)` пересевает **все** созданные `stream(name)`; независимость
  потоков сохраняется только при отсутствии повторного `seed()`.
* Функции шума (`$.random.simplex2D` / `noise2D` / `noise`) **игнорируют `--seed`**
  (у них собственный дефолт `seed = 0`) — при разных `--seed` карта шума одинаковая.
  Легко принять за «детерминизм работает», хотя это другой детерминизм.
* `--agent` отключает автоподслеживание файлов (hot reload) — в доке это не
  сказано, а для отладочных прогонов важно.
* Однократно наблюдался краш **на выходе** (SIGABRT 134, стек
  `main → SDL_Quit → SDL_QuitFilesystem`), затем не повторялся ни разу на ~500
  прогонах. Поэтому код возврата нельзя использовать как единственный вердикт теста.
* `$.mathx` не существует (файл называется `mathx.js`, а неймспейс — `$.math`);
  `$.camera.visibleRect()`, `$.time.fixed`, `queueFree` тоже отсутствуют. В доке их
  нет — просто частые догадки, стоит упомянуть в разделе «чего нет».

---

## D. Проверено и НЕ является проблемой (чтобы не тратили время)

* `--agent --headless` работает надёжно: ~500 прогонов, stdout чистый (логи в stderr),
  детерминизм при `--fixed-dt` + `--seed` — побитово одинаковые кадры и состояние.
* Пути ассетов от базового каталога (`R2D_GAME_DIR` → cwd с `game/main.js` →
  каталог бинарника) — ведут себя ровно как описано; `$.fs.readJSON` читает
  игровые данные корректно.
* Камера на «талом» вертикальном уровне 640×4864 с `zoom(3)`, `follow`, `deadzone`
  и `limits` работает без флагов и без дрожания.
* Тайлмап 2 слоя, 22×152 клетки, `origin: [-1, 0]`, склейка тел, `solidAt`/`shapeAt`/
  `fitsAt` — согласованы; `tileAt` принимает логические координаты и учитывает `origin`.
* `$.time.onTick(fn, { node })` снимает подписку вместе с узлом — утечек нет
  (проверено: 5000 кадров с движением, прыжками и атаками, узлы 806 → 806, тела 279 → 279).
* `.friction`/`.density` в конструкторе узла читаются Box2D — предупреждение из
  `game/level.js` верное и полезное.
* `$.test.check(cond, name)` и `$.time.after(ms, fn)` — в документации порядок
  аргументов и единицы указаны правильно.

---

# Решения по пунктам

Раздел добавлен командой движка **2026-10-16** (движок 0.1.34, коммит-база —
`a229c2e`). Отчёт выше не редактировался: здесь только решения и ссылки на
факты. Статусы:

* **исправлено** — код/доки в дереве, есть тест или прогон;
* **в работе (task-NN)** — задано отдельной задаче, тест появится вместе с ней;
* **принято как ограничение** — менять не будем, но задокументировано;
* **не будем** — с причиной.

Что проверено на живом движке при разборе отчёта: полный список методов
`$.devtools` (75 имён), состав `$.store` (13 имён), протокол (`ping`/`query`/
`eval`/`touch`/`pad`/`mouseMove`), поведение `--frames`/`--seconds` в `--agent`,
шум против `--seed`, `seed()` против потоков. Числа — в таблицах ниже.

## A. Дефекты поведения

| Пункт | Решение | Что происходит / где |
|---|---|---|
| A1 `.x`/`.y` обёртки | **исправлено** (коммит `39a9af7`) | аксессоры `x/y/w/h` на обёртке: чтение числом, запись двигает узел и тело физики; мусор (NaN/строки/null) позицию не меняет и даёт одну строку с причиной; неизвестное свойство больше не создаётся молча. Тест `tests/agent/node_xy_accessors_test.py` (на старом коде 12+ провалов). Попутно в `game/level.js` починены два места с этой же граблей |
| A2 импорт вне каталога игры | **исправлено** (коммит `18dfe07`) | резолв от файла-источника, `..` нормализуется, подъём вверх разрешён, пока файл существует; явные коды `R2D_MODULE_OUTSIDE_GAME` и `R2D_MODULE_ABSOLUTE_PATH`. Старое поведение грузило **двойника** внутри каталога игры (DECOY вместо SHARED). Тест `tests/agent/module_import_paths_test.py` (8 провалов на старом коде); правила описаны в `docs/highlevel/script.md` §7 |
| A3 двусторонний фильтр `areaOverlap` | **исправлено** (коммит `8238206`) | решает только маска ЗАПРОСА (по умолчанию `0xFFFFFFFF`), как у `$.world.raycast`/`query*`/`castShape`; прежнее поведение — `{ both: true }`. Было `['wall']` (цель потеряна) → стало `['target','wall']`. Тест `tests/agent/area_overlap_filter_test.py` (5 провалов на старом коде) |
| A4 `.frame(i)` и `frame_index` | **исправлено** (коммит `8238206`) | `.frame()` пишет и `node.sprite`, и `node.frame_index`; проверено пиксельно (кадр 0 красный, кадр 2 синий совпали с полем), вне листа состояние не меняется. Тест `tests/agent/frame_index_test.py` (7 провалов на старом коде) |
| A5 молчаливый отказ записи | **исправлено** (коммит `18dfe07`) | ровно одна строка на отказ с путём и причиной (`Permission denied (errno 13)` и т.п.) плюс запись `R2D_FS_WRITE_FAILED` (`fs`, recoverable) в `$.debug.errors()`; успешная запись молчит. Все четыре точки: `$.fs.write`, `$.fs.writeJSON`, `$.store.file(f).save()`, `$.store.save()`. Тест `tests/agent/fs_write_failure_test.py` (13 провалов на старом коде); `docs/highlevel/store.md` §2 обновлён |
| A6 `alive/hp` у узла без здоровья | **исправлено** (коммит `18dfe07`) | без компонента здоровья — `alive: null`, полей `hp/max_hp` нет; у `<enemy>` и `$.health(30)` всё как было. Проверено на `query`, `inspect` и `state.entities`. Тест `tests/agent/agent_state_trim_test.py` + 12 провалов на старом коде |
| A7 предупреждение на каждый узел с незнакомым тегом | **исправлено** (коммит `39a9af7`) | гашение по имени тега на общем журнале (закрывает все четыре места: api.js, world.js, prefab.js, pool.js): на масштабе отчёта **125 строк → 3**. Тест `tests/agent/unknown_tag_notice_test.py` (7 провалов на старом коде, в т.ч. точные 125 строк) |
| A8 раздутый `state.entities` | **исправлено** (коммит `18dfe07`) | `state` принимает `entities:{limit,sel,fields}`: 774 узла — полный снимок **125.7 КБ / 3.8 мс** против урезанного **2.2 КБ / 0.5 мс** (в 57 раз меньше, в 7 раз быстрее); без параметра снимок остаётся полным, `world.count` не подменяется. Тест `tests/agent/agent_state_trim_test.py` (18 провалов на старом коде) |

## B. Документация расходится с бинарником

| Пункт | Решение | Что изменено |
|---|---|---|
| B1 `--seconds`/`--frames` в агентском режиме | **исправлено (доком)** | факт оказался строже отчёта: в `--agent` лимиты не действуют вообще, а не только «пока не шлют `step`». Проверено: `--agent --frames 3` после семи `step` отвечает `frame: 7`; `--agent --seconds 1` жив после 200 кадров и >1 с реального времени. `docs/AGENT_API.md` §1: таблица флагов, предупреждение, пример запуска без `--seconds`; §8 — строка «`--frames`/`--seconds` не останавливают прогон». Поддержки флагов в коде нет — если нужна, это отдельная задача (см. «Что нашли мы сами») |
| B2 `console.log` в доке | **исправлено** | `docs/AGENT_API.md`: вступление прямо говорит, что в JS-контексте `console` нет (`typeof console === 'undefined'`) и вывод идёт через `$.log`/`$.logger`; упоминание `console.log` из §2 убрано, в §8 добавлен симптом `console is not defined` |
| B3 `tools/agent_client.py`/`run_tests.py` в поставке | **исправлено (доком)** | `docs/AGENT_API.md` §1, §4, §6: клиент и раннер — инструменты **репозитория**, в дистрибутив не входят (в поставке `docs/`, `game/`, `sdk/`, бинарники; `sdk/tools/*` — JS-инструменты SDK); описано, что делать, если есть только дистрибутив |
| B4 мелочи протокола | **исправлено (доком) + 1 вопрос к команде** | `docs/AGENT_API.md` §2: пример `ping` с фактическим `frame: 1` и предупреждение «не зашивайте `frame: 0`». §3.3.1: `$.agent.install()` вызывать не нужно — `$` зовёт его сам, ошибка `query` возможна только без `$`. §3.4: циклический результат — не ошибка, а строка `"<объект с циклом в ссылках>"` при `ok: true` (тип результата меняется — проверяйте `typeof`). §3.5: добавлены `touch` (`finger`, `x`, `y`, `action: down/up/clear`) и `pad` (`slot`, `button|axis`), уточнён `mouseMove` (только camelCase; `mousemove`/`mouse_move` → явная ошибка `неизвестная команда`; `x/y` — абсолютная позиция, применяется следующим кадром, ответ отдаёт позицию до применения). Вопрос: считать ли строку-заглушку вместо объекта (B4.1) дефектом — если да, заведём задачу с repro (см. ниже) |
| B5 `DEVTOOLS.md` и `highlevel/devtools.md` отстали | **исправлено** | `docs/highlevel/devtools.md` переписан: 14 вкладок (`perf`, `hitch`, `physics`, `render`, `events`, `tree`, `diag`, `audio`, `input`, `assets`, `memory`, `console`, `step`, `watch`) с указанием теста на каждую, машиночитаемый доступ (`perf/tree/hitch/physics/render/events/audio/input/assetsTab/memory/timeStep/report` и т.д.), инспектор F2, picking, оверлеи подсистем, цена обновления. `docs/DEVTOOLS.md`: статус и §2/§3/§4/§5/§8/§9 приведены к факту (панели и вкладки, что реализовано, чего ещё нет — правки значений и Export override, панели WORLD/BSP/NAV). Отдельной вкладки REPORT нет — экспорт отчёта §39 живёт внутри вкладки ДИАГ. Список методов снят с живого движка: `Object.keys($.devtools)` — **75 имён** |
| B6 `highlevel/store.md` документирует `list()/count()/exists()` | **исправлено** | таблица приведена к факту: `file, set, get, has, remove, clear, keys, all, setAll, save, load, autoSave, stopAutoSave` (**13 имён**, проверено `Object.keys($.store)`); отдельно сказано, чего нет (`list/count/exists/basePath` — они у `$.fs`) и что `save()` возвращает `false` без журнала (A5) |

## C. Мелкие неудобства и лимиты

| Пункт | Решение | Где задокументировано |
|---|---|---|
| 16 звуковых каналов, вытеснение по `priority`, `play()` → `-1` | **принято как ограничение** | уже было в [highlevel/sound.md](highlevel/sound); добавлена сводная строка в `docs/AGENT_API.md` → «Ограничения, о которых стоит знать» (с правилом вытеснения: жертва — самый неважный, иначе `-1`) |
| Пул `$.hits` — 8 слотов | **принято как ограничение** | уже было в [highlevel/hits.md](highlevel/hits) §5 «Ограничения» (пул 8 слотов, девятый удар переиспользует первый) + сводная строка в `AGENT_API.md` |
| `$.random.seed(n)` пересевает все потоки | **принято как ограничение** | уже было в [highlevel/random.md](highlevel/random) §4 («разворачивает default и все уже созданные потоки»); подтверждено прогоном: после `seed(5)` существующий поток даёт то же значение, что свежесозданный |
| Шум игнорирует `--seed` (дефолт 0) | **принято как ограничение** | добавлено в [highlevel/random.md](highlevel/random) §2.3 с числами: `noise2D(1.5, 2.5) = 0.6002521668560803` при `--seed 1` и при `--seed 999`, тогда как `$.random.next()` и потоки различаются. Совет: передавать зерно явно (`noise2D(x, y, $.random.seed())`) |
| `--agent` отключает hot reload | **принято как ограничение** | строка в «Ограничениях» `AGENT_API.md` (в коде: `hot_reload = opt_hot_reload && !opt_agent`, `engine.hotReload() === false` в агентском режиме); там же — ручной `reload` |
| Однократный SIGABRT на выходе, код возврата не вердикт | **принято как ограничение** | строка в «Ограничениях» `AGENT_API.md` + симптом в §8: у внешней команды — один раз на ~500 прогонов, у нас за сегодняшние десятки запусков не повторилось; поэтому вердикт ставят по строкам `ok`/`FAIL`, как делает `tools/run_tests.py` |
| Чего нет: `$.mathx`, `$.camera.visibleRect()`, `$.time.fixed`, `queueFree` | **принято как ограничение** | строка «Чего нет» в «Ограничениях» `AGENT_API.md`: неймспейс называется `$.math` ([highlevel/mathx.md](highlevel/mathx)), а вместо отсутствующих API указаны работающие замены (`$.camera.worldToScreen`/`isOnScreen`, `$.time.simDt`/`delta`, `node.destroy()`) |

## D. Проверено и НЕ является проблемой

Действий не требуется: пункты раздела D совпадают с нашими прогонами
(`--agent --headless` + `--fixed-dt` + `--seed` детерминированы, пути ассетов,
камера 640×4864, тайлмап с `origin: [-1, 0]`, `$.time.onTick` с узлом,
`.friction`/`.density` в конструкторе, `$.test.check`/`$.time.after`). Считаем
раздел закрытым; если что-то из него снова сломается — это будет регрессией, и
мы попросим repro.

## Что нашли мы сами, пока проверяли отчёт

1. **`--frames`/`--seconds` в `--agent` не действуют вообще** (B1): проверка
   лимита живёт в игровом цикле без агента и в браузерной сборке. Это шире
   формулировки отчёта («игнорируются, пока не шлют `step`»). Док приведён к
   факту; поддержка флагов в агентском режиме — отдельное решение (кода мы не
   трогали).
2. **Циклический результат `eval`** (B4.1): приходит
   `{"ok":true,"result":"<объект с циклом в ссылках>"}` — то есть тип результата
   молча меняется с объекта на строку. Док приведён к факту; если команда
   считает это дефектом (мы склоняемся, что `ok:true` + строка вместо ошибки
   хуже явного отказа) — нужна задача на `src/agent.c`/`src/script.c`.
3. **`mouseMove` с `x`/`y`**: ответ отдаёт позицию **до** применения (обычно
   `{"x":0,"y":0}` сразу после команды), сама позиция доезжает до игры следующим
   кадром. Док уточнён; поведение менять не обязательно.
4. **`touch`**: номер пальца — поле `finger`, а `id` — идентификатор запроса
   (эхо в ответе). В таблицу команд добавлено именно `finger`.
