# `diag.js` — структурированные ошибки и диагностика

Модуль отвечает за §17 Спецификации производственной готовности: ошибка должна
быть пригодна **машине**, а не только человеку, читающему stderr. Раньше всё
сводилось к строке в логе (`$: ошибка в …`): по ней нельзя ни посчитать
повторы, ни отфильтровать по подсистеме, ни показать в DevTools то же, что
получает автоматический тест.

Строка в лог осталась (её ищут люди и тесты) — запись идёт **рядом** с ней, в
журнал фиксированной формы.

## Публичный API

```js
$.debug.errors()            // [{ seq, code, subsystem, operation, message, … }]
$.debug.errors('R2D_ASSET_MISSING')   // фильтр по коду, подсистеме или nodeId
$.debug.report()            // сводка: по кодам, подсистемам, операциям, потерям
$.debug.reportError(where, error, { fatal: true, assetPath: 'a.png' })
$.debug.clearErrors()
$.debug.expectClean()       // бросает исключение на неустранимой ошибке
$.debug.classify(where)     // { code, subsystem } — как движок классифицирует место
```

## Форма записи

| Поле | Смысл |
|---|---|
| `seq` | порядковый номер записи за запуск |
| `code` | стабильный код (`R2D_SCRIPT_CALLBACK`, `R2D_SCENE_UPDATE`, `R2D_NODE_EVENT`, `R2D_ASSET_MISSING`, …) |
| `subsystem` | подсистема: `update`, `render`, `scene`, `events`, `audio`, `engine`… |
| `operation` | место вызова как его назвал код (`$.update`, `update сцены`, `событие collide`) |
| `message` | текст (`Error.message`, если это Error) |
| `recoverable` | можно ли продолжать: у ошибок кадра `true`, у `fatal` — `false` |
| `nodeId` | логический `nid` узла, если ошибка связана с узлом (§4.2) |
| `assetPath`, `jsonPath` | путь ресурса и путь до поля в JSON |
| `source` | `{ file, line }` из стека QuickJS, если он там есть |
| `frame` | номер кадра |
| `data` | произвольные данные вызывающего |

Операции классифицируются автоматически (`classify`): `$.ready`/`$.update`/
`$.render` → `R2D_SCRIPT_CALLBACK` с подсистемой по имени, `update сцены` →
`R2D_SCENE_UPDATE`, `событие <имя>` → `R2D_NODE_EVENT`. Явные `code` и
`subsystem` в опциях старше автоматических — так подсистема объявляет свой код
сама, не завися от текста места.

## Подавление повторов и лимит

* Одинаковые сообщения (код + место + текст) печатаются первые **3** раза,
  дальше только считаются: один сломанный кадр не должен превращать stderr в
  поток и прятать остальные ошибки.
* Счётчик подавленных виден в сводке — `summary.suppressed`. «Ограничили по
  частоте, но счётчик потерь виден» — прямое требование §17.
* Журнал — кольцо на **128** записей: диагностика не растёт вместе с
  длительностью запуска (§28). Вытесненные считает `summary.dropped`.

## Сводка

```js
{
  total, fatal, dropped, suppressed,
  by_code: { R2D_SCRIPT_CALLBACK: 12 },
  by_subsystem: { update: 12 },
  by_operation: { '$.update': 12 },
  clean: true,        // нет неустранимых ошибок
}
```

`clean` — то, по чему приёмка решает, что прогон состоялся: **устранимая**
ошибка (упал обработчик кадра, кадр продолжается) журнал не портит,
**неустранимая** (`fatal: true` — не загрузился обязательный ресурс, не
создалась обязательная коллизия) — портит. Загрузчик обязан считать такую
операцию неуспешной, а не «подготовкой завершено».

## Ограничения

* Это **не** второй лог-фреймворк: модуль не фильтрует по уровню важности, не
  пишет в файлы и не заменяет `ctx.log`.
* Полная трассировка сценария (attack → contact → damage → death → transition)
  живёт в `$.trace` (см. `docs/FRAME_PHASES.md` и §32.1 в
  [HIGH_LEVEL_API.md](HIGH_LEVEL_API)); `diag` отвечает за ошибки.
* Структурированный результат **по значению** для конкретных операций
  (например `{ ok: false, code }` от загрузчика) — отдельная работа §17; здесь
  только единый журнал ошибок.
