# История изменений

Все значимые изменения в проекте `umbot` документируются в этом файле.
Формат основан на [Keep a CHANGELOG](http://keepachangelog.com/).

## [3.1.5] - 2026-10-03

### Добавлено

- **Long polling: `bot.startPolling(options?)` и `bot.stopPolling()`.** Telegram (`getUpdates`), VK (Bots Long
  Poll) и MAX (`GET /updates`) можно запустить без вебхука: бот сам запрашивает обновления, публичный HTTPS-адрес и
  туннель не нужны. Обновление проходит тот же конвейер, что и вебхук (middleware, команды, очередь пользователя),
  кроме проверки подписи. Ошибка сети — повтор с паузой от 1 до 30 секунд; неверный токен или активный вебхук у
  Telegram останавливают polling платформы с причиной в логе. Пачка обрабатывается не больше чем по 32 обновления
  одновременно, обновления одного пользователя — по очереди в порядке пачки (у VK имена авторов пачки загружаются
  одним запросом `users.get`). `bot.close()` и SIGINT/SIGTERM останавливают polling.
  Можно совмещать с `bot.start()`: `bot.startPolling({ platforms: ['telegram'] })`.
- **Telegram (`telegram_delete_webhook`)**: опция адаптера снимает вебхук бота при первом запросе long polling (с
  предупреждением в логе). Без опции вебхук не снимается: polling останавливается с подсказкой.
- **`IPlatformAdapter.getUpdates(signal)`**: необязательный метод контракта — один запрос long polling. Свой адаптер,
  реализовавший его, тоже запускается через `bot.startPolling()`. Адаптеры без метода работают как раньше.
- **`Request.signal`**: внешний сигнал отмены запроса, действует вместе с `maxTimeQuery` и сбрасывается после
  `send()`.
- **`create from-flow`, `http_request`: переменные в URL и `{{env.NAME}}`.** `{{var}}` в пути и параметрах URL
  подставляется с `encodeURIComponent`; переменная в адресе сервера (`https://{{host}}/…`) — ошибка `validate`,
  чтобы пользователь бота не выбирал сервер, к которому ходит бот. `{{env.NAME}}` работает в URL, заголовках и теле
  и читается хелпером `env()` сгенерированного `src/utils.ts` (окружение процесса, затем `.env`); в текстах ответа
  переменные окружения не раскрываются.
- **`loadEnvFile` в `umbot/utils`**: разбор `.env` фреймворка доступен для собственных переменных проекта;
  `IEnvConfig` принимает любые имена переменных.
- **CLI `umbot doctor [--env <path>] [--offline]`**: проверяет версию Node.js, установленный umbot, `.env` и его
  место в `.gitignore`, токены платформ запросом к API (Telegram, MAX, VK, Viber, Алиса) и состояние вебхуков
  (адрес, недоставленные обновления и последняя ошибка доставки Telegram, вебхук без секрета). Токены в отчёт не
  попадают; при ошибках — код выхода 1.
- **CLI `umbot add platform|db|middleware <name> [--force]`**: каркас своего адаптера платформы
  (`src/platforms/<Name>Adapter.ts`), адаптера базы данных (`src/db/<Name>DbAdapter.ts`) или middleware-фабрики
  (`src/middleware/<name>.ts`) вместе с тестом на встроенном `node:test`. Каркас сразу компилируется и проходит тест,
  места под API платформы или драйвер базы отмечены `TODO`. Адаптер БД — рабочий, с хранением в памяти процесса, а его
  тест проверяет ответы, которых ждёт фреймворк. CLI не меняет `src/index.ts` (строки подключения печатает в консоль)
  и не перезаписывает существующие файлы без `--force`.

### Безопасность

- **`create from-flow`**: секреты `http_request` (значения `Authorization`, `Cookie`, `api_key`, `token` и т.п. в
  заголовках, параметрах URL и JSON-теле) больше не пишутся открытым текстом в `src/index.ts`. Генератор переносит их
  в `.env` как `HTTP_*` и предупреждает об этом; в коде остаётся `env('HTTP_…')`. После повторной генерации проверьте
  `.env`: значения, записанные в flow.json, теперь там.

### Исправлено

- **CLI `add db`**: каркас адаптера БД на пустой выборке возвращал `{ status: false, error: 'Записи не найдены' }`.
  По `error` ядро считает, что база не ответила, — поэтому адаптер, написанный по каркасу, не сохранял данные ни
  одного нового пользователя. Теперь «не найдено» — `{ status: false }` без `error`, тест каркаса это проверяет.
- **CLI `validate`** проверял `flow.json` как конфиг команды `create`: файл без `name` отклонялся сообщением
  «Проект не создан» без списка ошибок, а битый JSON не доходил до валидатора. Теперь `validate` сам сообщает обо
  всех ошибках файла.
- **CLI `validate`** пропускал узел, у которого `actions`, `conditions`, `buttons` или `slots` не массив, — после
  этого `create from-flow` падал с «is not iterable». Такие поля теперь ошибка валидации.
- **`create from-flow`, `http_request`**: свои `headers` у запроса с телом убирали `Content-Type: application/json`,
  и API получал тело как `text/plain`. Теперь заголовок добавляется, если в `headers` нет своего `Content-Type`.
  `headers` принимаются и объектом, а некорректные пропускаются с предупреждением (раньше молча превращались в `{}`).
- **`create from-flow` (слоты)**: слот с заглавной буквой («Погода») генерировался как есть и не срабатывал никогда —
  фреймворк сравнивает слоты с репликой в нижнем регистре. Строковые слоты команд и приветствия теперь генерируются в
  нижнем регистре; слоты-регулярки (`isPattern`) не меняются.

### Документация

- **Внешний адаптер YDB.** [umbot-ydb-adapter](https://github.com/max36895/umbot-ydb-adapter) — хранилище для
  навыков и ботов в Yandex Cloud Functions — добавлен в README («Экосистема»), `deployment.md` (serverless и
  несколько процессов) и `adapter/dbAdapter.md`. В `deployment.md` для нескольких процессов названы готовые
  `umbot-knex-adapter` и `umbot-ydb-adapter` вместо «PostgreSQL, Redis — свой адаптер».
- `adapter/external-db-adapter-spec.md`: раздел «Подводные камни» (`ensureSchema` на каждом холодном старте
  serverless, колонки ключа в данных `UPDATE`, числа из строковых условий, драйверы только в ESM, вторичные индексы)
  и таблица готовых адаптеров вместо списка «нужно написать», где PostgreSQL, MySQL и SQLite давно закрыты
  `umbot-knex-adapter`.
- JSDoc `BaseDbAdapter._select` противоречил контракту: «`status: true` даже если найдено 0 записей». Пустая
  выборка — `{ status: false }` без `error`, сбой — `status: false` с `error`, как во встроенных адаптерах. Те же
  ошибки в примерах, которые копируют в свои адаптеры: `{ status: true, data: [] }` под «записи нашлись» и
  псевдокод `_select` в `adapter/dbAdapter.md`, шаблон `_select` в `adapter/external-db-adapter-spec.md`.
- **Навигация по сайту документации.** Гайды открываются по читаемым адресам вида
  `https://www.maxim-m.ru/docs/umbot/v-3.1/guides/<раздел>` (`getting-started`, `adapter/platformAdapter`, …),
  ссылки между разделами и из README ведут на них, а старые адреса `documents/…` в тексте документации заменены.
  Справочник API сгруппирован по назначению — «Основное», «Ответ пользователю», «Хранение данных», «Подключение
  платформ», «Адаптеры платформ», «Адаптеры баз данных», «Свой адаптер», «API платформ» — вместо алфавитного списка
  классов и функций.
- Long polling, `umbot doctor`, `{{env.NAME}}` в `http_request`, `loadEnvFile` и `Request.signal` описаны в README,
  `GUIDE.md`, `getting-started.md`, `platform-integration.md`, `json-format.md`, `configuration.md`,
  `http-client.md` и `adapter/platformAdapter.md`. Убрано устаревшее «мессенджеры подключаются только через вебхук».
- `umbot add platform|db|middleware` описан в `cli/README.md`, README, `GUIDE.md`, `adapter/platformAdapter.md`,
  `adapter/dbAdapter.md` и `middleware.md`.
- **Документация на английском** — `https://www.maxim-m.ru/docs/umbot/en/`: все гайды, README и `cli/README.md`
  (машинный перевод, русская версия остаётся основной; API-справочник — с русским JSDoc). Перевод хранится по
  абзацам, и правка русского текста требует перевести только изменённые абзацы: `node scripts/i18n-docs.js`,
  порядок работы — `i18n/README.md`.
- **Порядок обработки запроса** был описан неверно: `GUIDE.md` и `api-reference.md` утверждали, что fallback
  проверяется до интентов и перехватывает запрос. На деле интенты из `setPlatformParams` ищутся раньше, а fallback
  срабатывает, только если интент не найден. Схема диспетчера исправлена и сведена к одной во всех разделах.
- **Шаги (`addStep`)**: `GUIDE.md` в одном месте разрешал колбэку шага вернуть строку, а в другом — запрещал, тип
  в `GUIDE.md` и `api-reference.md` был без `string`. Возврат строки поддерживается с 3.1.4 — описание и тип
  исправлены, JSDoc `Bot.addStep` дополнен.
- **`strict_prod`**: `.*` без якорей приводился как опасное выражение, хотя проверка ReDoS его не ищет; не было
  сказано, что команда, у которой опасны все слоты, не регистрируется (`GUIDE.md`, FAQ, `getting-started.md`).
- `json-format.md`: слоты описывались как «без учёта регистра», хотя генератор переносил их как есть (см.
  «Исправлено»); добавлено, как регистр работает для строк и регулярок.
- `api-reference.md`: `AppContext` назывался синглтоном — у каждого `Bot` свой контекст (то же в
  `adapter/readme.md`, `adapter/dbAdapter.md`); у `Preload.removeImages`/`removeSounds` не было параметра `options`
  (без `alisaSkillId` Алиса пропускается); таблицы БД создаются через `ensureSchema()` сразу после подключения, а не
  «на первом запросе»; описаны `Model.getUniqueKeys()` и `controller.platformOptions`.
- `middleware.md`: при переполнении очереди `rateLimiter` платформа получает 200, выставляется
  `platformOptions.rateLimitOverflow`, описаны `RateLimitQueueOverflowError` и `destroyRateLimiter()`; что происходит
  с исключением в своём middleware.
- `GUIDE.md`: в «Класс Bot — полный API» добавлены `startPolling()`/`stopPolling()`.
- Примеры кода: исправлены примеры, которые не компилировались при `strict` (недостающие импорты,
  `originalUserCommand` с `null`, `new MongoAdapter({})`, тип меток модели), и примеры в README/FAQ, где в
  переменную `bot` попадал `Server` из цепочки `.start()`. Примеры `isCorrectQuery` сравнивают секрет вебхука через
  `timingSafeEqual`, а пример `isPlatformOnQuery` больше не сверяет заголовок секрета с токеном бота.
- JSDoc: описаны метрики `EMetric` (`START_WEBHOOK` — метка времени, а не длительность) и защищённые поля
  `_token`/`_platformOptions` для авторов своих адаптеров.
- ТЗ демо-проекта перенесено из документации сайта в `examples/umbot-demo/SPEC.md`, рядом с самим демо.
- **`GUIDE.md` стал руководством, а не сводом всего подряд** (4360 → ~2150 строк): установка, команды и шаги,
  состояние, компоненты, обработка ошибок. Справочные главы перенесены туда, где их ищут, и в GUIDE заменены короткими
  объяснениями со ссылками: полный список импортов, `Bot`, `BotController`, `run()` из `umbot/build`, метрики и
  Preload — в `api-reference.md`; типы конфигурации и доступ к контексту — в `configuration.md`; определение
  платформы, лимиты адаптеров и сводная таблица возможностей — в `platform-integration.md`; поведение `test()` и моки
  — в `testing.md`; встроенный сервер, завершение, масштабирование и чеклист — в `deployment.md`. Шпаргалка по API
  удалена: она дублировала справочник.
- **Новый раздел «Рецепты» (`recipes.md`)**: 17 готовых решений и сценарии, требующие своей реализации (из GUIDE).
- Исправлено по ходу переноса:
    - GUIDE утверждал, что ошибки в `action()` не перехватываются: фреймворк их логирует и отвечает «Не удалось
      выполнить команду»;
    - `userData` описывался как общий для пользователя на всех платформах при совпадении `userId` — ключ записи
      `userId` + платформа;
    - у SmartApp `tts` не игнорируется (звуки и эффекты вычищаются, текст произносится);
    - лимиты кнопок: предупреждение пишут все адаптеры, а не только MAX; у MAX кнопки раскладываются `buttons.row()`;
      VK `_group` — строка или число;
    - «визуального редактора нет» — есть Umbot Flow; «inline-режим не поддерживается» — есть событие `inline`;
    - убраны примеры middleware, которые читали ответ или мерили время обработчика после `await next()` (обработчик
      в этот момент ещё не запускался), и рецепт «бэкап `userData` после `next()`» по той же причине;
    - устаревший Dockerfile в GUIDE (Node 20, без `NODE_ENV=production`); пример «Serverless: примера в репозитории
      нет» — его генерирует `create from-flow --usecloud`;
    - рецепт NLU-плагина не компилировался (класс без `destroy()`), рецепт inline-режима переписан на
      `bot.addEvent('inline')`; i18n-плагин применяется только в `BaseBotController` — это теперь сказано;
    - режим `build` в JSON-конфиге `create` генерирует запуск через `run()` из `umbot/build`, а не «только сборку»
      (`cli/README.md`);
    - FAQ: срок жизни `state` задаёт платформа (у Алисы хранилище пользователя переживает сессии), а не «только текущая
      сессия»; README: убран повтор блока «Ключевая мысль».

## [3.1.4] - 2026-09-27

### Добавлено

- **`isCorrectQuery(query, headers, parsedQuery)`**: третий необязательный параметр контракта `IPlatformAdapter` —
  тело запроса, уже разобранное фреймворком. `webhookHandle` и `webhookEvent` передают его, поэтому адаптеру с
  подписью в теле (как VK) не нужно разбирать JSON второй раз. Собственные адаптеры работают без изменений.
- **CLI `umbot webhook <telegram|max> <https-url>`**: регистрирует вебхук сразу с секретом (`setWebhook` /
  `POST /subscriptions`) и сохраняет секрет в `.env`. Секрет пишется только после успешной регистрации, поэтому
  `.env` и платформа не расходятся.
- **`TELEGRAM_WEBHOOK_SECRET` и `MAX_WEBHOOK_SECRET`**: читаются из `.env`/окружения в
  `tokens.telegram.webhookSecret` / `tokens.max_app.webhookSecret` и включают проверку подписи вебхука.
- **Дедупликация повторных доставок**: необязательный метод адаптера `getDeliveryId(query)` (реализован в Telegram,
  VK, MAX и Viber). Повтор уже принятой доставки подтверждается `200 ok` без повторного запуска логики. Повтор,
  пришедший во время обработки исходного запроса, ждёт его исхода: если исходный упал с 500, повтор обрабатывается
  заново, и апдейт не теряется.
- **`getResponseTimeout()`**: необязательный метод адаптера — сколько платформа ждёт ответа. У Алисы, Маруси и
  SmartApp — `MAX_TIME_REQUEST` (2900 мс), у мессенджеров — `null`. По нему ядро ограничивает ожидание в очереди
  запросов пользователя.
- **`isSignatureSupported()`**: необязательный метод адаптера — есть ли у платформы подпись вебхука. По нему
  `bot.start()` решает, предупреждать ли о вебхуке без проверки подписи.
- **`rateLimiter(maxQueueSize, inactivityTimeout, getKey)`**: свой ключ счётчика, например общий лимит на платформу
  (`ctx => ctx.appType ?? ''`) — на платформах без подписи `userId` задаёт отправитель и лимит на пользователя
  обходится сменой id.
- **`IRequestSend.httpStatus` / `errorBody`**: при ответе не 2xx `Request.send()` возвращает статус и тело ошибки.
- **`logWarn(msg, meta, { stderr: true })`**: опция дублирует предупреждение в stderr вне `dev` без своего логгера.
- **`ipFilter` (`rejectWithoutIp`)**: опция отклоняет запросы с неизвестным IP клиента (`bot.run()`,
  `webhookEvent()` без `clientIp`). Раньше такие запросы всегда пропускались, и забытый `clientIp` в serverless молча
  отключал `whitelist`.
- **MAX (`max_callback_edit_message`)**: опция адаптера — ответ на нажатие callback-кнопки заменяет сообщение с кнопкой
  (прежнее поведение). Нужна, если меню «перерисовывается» на месте.
- **`IQuery.uniqueKeys` и `Model.getUniqueKeys()`**: поля составного ключа записи — модель добавляет их в условие
  select/update/remove. Используется `UsersData` (`['platform']`).
- **Шаги (`addStep`)**: колбэк может вернуть строку (или `Promise<string>`) — она становится текстом ответа, как у
  `addCommand`.
- **Кнопки (`buttons.row()`)**: раскладка кнопок по рядам для Telegram, VK, MAX и Viber. `row()` завершает текущий
  ряд; без вызова каждая кнопка, как и раньше, выводится отдельной строкой. Учитываются лимиты платформ (Telegram — 8,
  VK — 5, MAX — 7 / 3, Viber — 6 кнопок в ряду): лишние кнопки переносятся на следующую строку с предупреждением.
  Собственный адаптер поддерживает ряды через хелпер `pUtils.layoutButtonRows` (`umbot/plugins`). Группу кнопки
  (`options._group`) теперь тоже учитывают Telegram, MAX и Viber (раньше — только VK): если вы задавали её там, но
  ждали кнопки столбиком, уберите группу.
- **DB-адаптеры (`ensureSchema`)**: необязательный метод адаптера, который фреймворк вызывает после каждого
  успешного подключения, до первого запроса, и передаёт описание встроенных таблиц `DB_TABLES_SCHEMA` (экспорт из
  `umbot`, типы `IDbTableSchema`, `IDbFieldSchema`). SQL-адаптер создаёт в нём таблицы, `MongoAdapter` — индексы.
  Раньше нигде не было зафиксировано, кто создаёт таблицы: SQL-адаптер падал на первом запросе.
- **MongoAdapter**: индексы `{ userId, platform }` (`UsersData`) и `{ platform, path }` (`ImageTokens`,
  `SoundTokens`) создаются автоматически — без них поиск шёл полным перебором коллекции. Без прав на создание
  индексов адаптер пишет предупреждение и продолжает работу.
- **Режим по `NODE_ENV`**: без явного `setAppMode()` режим — `strict_prod` при `NODE_ENV=production`, иначе `dev`
  (как Express и сборщики фронтенда). Docker-шаблон CLI задаёт `ENV NODE_ENV=production`. В `strict_prod` опасные
  регулярные выражения отклоняются при регистрации — после обновления проверьте лог на ошибки ReDoS; оставить
  прежний режим можно явным `setAppMode('dev')`.

### Изменено

- **Адаптеры Telegram, VK, MAX и Viber (`setQueryData`, `getContent`)** возвращают значение сразу, если ждать нечего,
  и промис — только при сетевом вызове. Контракт `IPlatformAdapter` это допускал и раньше (`boolean | Promise<boolean>`).
  Через `bot.run()`, `webhookHandle` и `webhookEvent` ничего менять не нужно. Если ваш код вызывает эти методы
  адаптера напрямую и пишет `.then()`, замените на `await` — он работает и со значением, и с промисом.
- **Производительность обработки запроса.** Замеры на одной машине до и после (AMD Ryzen 5 5600G, Node.js 24):
    - Запрос на малом боте (6 команд, VK и Telegram): **1.5–1.7 → ~0.5 мкс**, аллокации **6.6 → 2.2 КБ** на запрос.
      Убраны лишние async-уровни конвейера (`run()`, загрузка/сохранение `userData`, сборка ответа отдают промис
      только там, где есть что ждать), `.finally` и пересоздание хэш-таблицы в очереди пользователя, замыкание
      API-фасада на каждый запрос. Ключи очереди — двухуровневая карта «платформа → userId»: поиск по свежесклеенной
      строке `telegram:123456789` в V8 стоил ~0.3 мкс.
    - **Поиск команд на больших базах** больше не зависит от числа команд: частичное совпадение среди 1000 строковых
      команд **21.6 → 1.2 мкс**, fallback **33 → 1.2 мкс** (индекс подстрок, автомат Ахо–Корасик); 1000 регулярок
      **42 → 3.5 мкс** — регулярка запускается, только если в тексте есть её обязательная часть (`/заказ_\d+/` —
      «заказ_»). Выбор команды не изменился: побеждает зарегистрированная раньше, это проверено дифференциальным
      тестом против прежнего алгоритма. Индексы строятся один раз, через ~40 мс после регистрации команд; запросы,
      пришедшие раньше (serverless: первый запрос сразу после старта), ищут обычным перебором и не платят за
      сборку индексов.
    - **`webhookHandle` (VK, Telegram, MAX, Viber): ~25 → ~9 мкс** на запрос после 10 000 доставок. Дедупликация
      вытесняла старые доставки обходом `Map` с начала, а он в V8 проходит «дыры» хэш-таблицы — O(ёмкости) на каждый
      запрос; теперь кольцевой буфер. При проверенной подписи вебхука ключ доставки — её ID, тело не хэшируется;
      без подписи — одношаговый `crypto.hash`. VK с `secret_key` больше не разбирает JSON тела второй раз.
    - **Сессия в памяти (`memorySession`)**: при 10 000 активных пользователей запись стоила до ~9 мкс, теперь
      ~0.2 мкс (список по давности вместо удаления и вставки в `Map` на каждый запрос).
    - Горячие модули импортируют `Text`, `isPromise`, `keysCount` из листовых модулей, а не через баррель: цепочка
      геттеров реэкспорта стоила на прогреве и холодном старте.
- **Запросы одного пользователя выполняются по очереди** (платформа + `userId`), запросы разных пользователей —
  параллельно. Если предыдущий запрос пользователя длится дольше 10 секунд, следующий начинается, не дожидаясь его.
  На платформах со сроком ответа (Алиса, Маруся, SmartApp) запрос ждёт не дольше половины оставшегося срока, чтобы
  успеть ответить. Очередь живёт в памяти процесса.
- **Файловые логи**: `error.log` / `warn.log` больше 10 МБ переименовываются в `<имя>.1` (хранится одна копия).
- **Предупреждения при старте** (вебхук без подписи, `0.0.0.0`) вне `dev` без своего логгера дублируются в stderr —
  раньше они попадали только в `warn.log` и не были видны в `docker logs`.
- **Telegram**: на ответ 429 с `retry_after` до 5 секунд запрос повторяется один раз.
- **MAX (callback-кнопки)**: нажатие подтверждается `POST /answers` с пустым телом, ответ обработчика отправляется
  обычным сообщением — как в Telegram и VK. Раньше ответ уходил в `POST /answers` с `message`, а MAX этим заменяет
  сообщение с кнопкой. Прежнее поведение — опция `max_callback_edit_message: true`.
- **CLI `create from-flow`**: без поля `mode` генерируется `bot.setAppMode('strict_prod')`; вызов стоит сразу после
  `new Bot()`, до регистрации команд. Прежнее поведение — `"mode": "dev"` в flow.json.
- **Логирование**: в режимах `prod`/`strict_prod` без своего логгера текст ошибки (с замаскированными секретами)
  дублируется в stderr — ошибки видны в `docker logs` и журнале serverless-функции. Полные данные по-прежнему в
  `error.log`. Если stderr процесса собирается в алерты, учтите новые строки `[umbot] ...`.
- **Детектор ReDoS**: опасными считаются и вложенные квантификаторы с ограниченными границами —
  `(a{1,10}){1,10}`, `(\w{1,5}){1,20}`, `(a?){2,8}` (повтор переменной длины под переменным внешним интервалом).
  `(a{1,10}){1,10}$` на 40 символах блокировал поток на ~20 с, а в `strict_prod` регистрировался.

- **`action()` может быть асинхронным**: фреймворк дожидается промиса перед формированием ответа; ошибка в нём
  логируется, а пользователь получает текст «Не удалось выполнить команду», если `text` не заполнен.
  Раньше промис не ожидался, и код после `await` в ответ не попадал.
- **`run(config)` без `plugins`**: `MongoAdapter` подключается, только если задан адрес базы (`appConfig.db.host` или
  `DB_HOST`), иначе — `FileAdapter`. Раньше всегда подключался `MongoAdapter`, и без Mongo подключение падало,
  а `userData` не сохранялась.

### Исправлено

- **Повторная регистрация команды** (`addCommand` с уже занятым именем) оставляла старые слоты рабочими: прежняя
  фраза и прежняя регулярка (в том числе внутри группы регулярок) продолжали вызывать команду. Теперь команда
  заменяется полностью, её место в порядке регистрации (приоритет) сохраняется. Если strictMode отклонил все новые
  слоты, команда удаляется.
- **Группы регулярок после удаления или перерегистрации команды**: поиск пропускал «N позиций после хоста группы»,
  считая участников группы идущими подряд. После `removeCommand` или перерегистрации участника пропускалась чужая
  команда, и её фраза уходила в fallback. Участники группы теперь определяются по именам.
- **`removeCommand`** удалял точное совпадение фразы, даже если оно принадлежало другой команде (общий слот у двух
  команд): вторая команда переставала находиться по этой фразе целиком.
- **[Критично] MongoAdapter: сохранённые данные не находились.** `_select` возвращал сам документ вместо
  `{ status, data }`, поэтому `whereOne()` и `selectOne()` не видели существующую запись. `userData` каждого запроса
  начинались с пустого объекта, а в конец коллекции `UsersData` вставлялся новый документ; токены изображений и
  звуков не кэшировались, и медиа загружалось на платформу заново. Кроме того, каждая ненайденная запись писала в лог
  ошибку `MongoDB: undefined`. Если вы работали с `MongoAdapter`, в коллекциях `UsersData`, `ImageTokens` и
  `SoundTokens` могли накопиться дубли: после обновления читается самый ранний документ пользователя. Удалите лишние,
  оставив для каждой пары `userId` + `platform` нужную запись.
- **Ошибка чтения `userData` из БД**: сбой запроса (например, таймаут MongoDB) выглядел как «пользователь не найден».
  Запрос выполнялся с пустыми `userData`, а в конце вставлял для пользователя новую запись. Теперь сбой пишется в лог,
  запрос обрабатывается, но его `userData` не сохраняются. Собственный DB-адаптер получает это поведение, если при
  ошибке возвращает из `_select` поле `error` (см. `dbAdapter.md`).
- **Данные пользователей при параллельных запросах**: два апдейта одного пользователя (двойное нажатие кнопки,
  повторная доставка) читали одни и те же `userData`, и сохранялись изменения только последнего.
- **Повторные доставки вебхука**: платформа, не дождавшаяся ответа (загрузка медиа, очередь MAX), присылала апдейт
  повторно, и пользователь получал ответ дважды.
- **Предупреждение «БЕЗ проверки подписи»** перечисляло Алису, Марусю и SmartApp, у которых подписи вебхука нет.
- **`warn.log`**: после предупреждения без метаданных писалась строка `undefined`.
- **CLI `create` и `.env`**: проект читал `.env`, только если в JSON-конфиге был `isEnv`, поэтому секрет от
  `npx umbot webhook` и токены из `.env` не подхватывались. Теперь конфиг проекта всегда содержит `env: './.env'`, а
  `.env` создаётся с пустыми переменными токенов, секретов вебхука и MongoDB (существующий файл не трогается).
  `generateenv` и `add env` пишут тот же шаблон вместо заглушек вида `your-vk-secret-key`: с ней `VK_SECRET_KEY`
  включал проверку подписи с неверным секретом, и все запросы VK получали 401.
- **Лог старта**: адрес сервера выводился без схемы (`//0.0.0.0:3000/`).
- **Данные пользователей**: `UsersData` искалась и обновлялась только по `userId`. Пользователь Telegram 42 и
  пользователь VK 42 делили одну запись — мультиплатформенный бот с `FileAdapter`/`MongoAdapter` показывал одному
  пользователю данные другого. Теперь ключ — `userId + platform`: `FileAdapter` сам переносит записи прежнего
  формата под ключ `<platform>:<userId>`, `MongoAdapter` сам создаёт индекс. Если данные двух пользователей уже
  смешались, запись остаётся за платформой, которая её создала. Собственный DB-адаптер получает исправление, только
  если учитывает `IQuery.uniqueKeys` (см. «Добавлено»).
- **Недоступная БД**: при неудачном подключении каждый следующий запрос снова подключался к базе (у `MongoAdapter`
  ~6 с) и не укладывался в лимит платформы — сбой базы клал бота целиком. Теперь во время паузы (5 → 60 с) запросы
  сразу обрабатываются без БД.
- **`webhookEvent()` и подпись вебхука**: заголовки сравнивались с учётом регистра. Yandex Cloud Functions передаёт их
  как прислал клиент (`X-Telegram-Bot-Api-Secret-Token`), и при заданном секрете каждый настоящий запрос получал 401.
  Теперь имена заголовков приводятся к нижнему регистру.
- **`run(config, 'prod')`**: режим `strict_prod` выставлялся после регистрации параметров и команд из `config.logic`,
  поэтому опасные регулярные выражения не отклонялись. Теперь режим задаётся до регистрации. Тот же порядок исправлен
  в шаблонах CLI `create`.
- **`addForm`**: ответы формы удалялись из `userData` через `delete`. Алиса очищает поле `user_state_update` только
  значением `null`, поэтому ответы (часто личные данные) оставались в состоянии пользователя. Теперь поле получает
  `null`.
- **Шаги**: колбэк шага, вернувший строку, падал с `TypeError` вне обработки ошибок — вебхук отвечал 500, и Telegram
  повторял апдейт бесконечно. При нескольких NLU-интентах выбирался последний совпавший шаг, теперь — первый.
- **MAX (вложения)**: сообщение с только что загруженным файлом MAX отклоняет ошибкой `attachment.not.ready`, и
  терялся весь ответ, включая текст. `MaxRequest` повторяет отправку до 3 раз (0,5 / 1 / 2 с).
- **MAX и VK (`api.answerCallback`)**: если обработчик сам отвечал на нажатие через `controller.api.answerCallback()`,
  адаптер подтверждал нажатие повторно. Теперь повторного ответа нет.
- **VK**: запрос подтверждения сервера без `confirmation_token` уходил в обычную обработку (middleware, fallback,
  `messages.send` с пустым адресатом). Теперь адаптер отвечает без бизнес-логики и пишет в лог, где задать токен.
- **VK**: числовой id строкой (`'123'` — после `escapeString` DB-адаптера или из `bot.send('123', …)`) отправлялся
  как короткий адрес `domain`. Теперь — как `peer_id`.
- **SmartApp**: у кнопок-подсказок терялась ссылка — кнопка-ссылка отправляла свой заголовок как реплику. Теперь
  генерируется действие `deep_link`.
- **CLI `create from-flow --usecloud` (`deploy.js`)**: сгенерированный `scripts/deploy.js` падал с `SyntaxError` при
  запуске: escape-последовательности регулярок превращались в настоящий перевод строки. Кроме того, значения `.env`
  разбирались не так, как их читает фреймворк (инлайн-комментарий « #», кавычки), а на Windows аргументы передавались
  устаревшим для `shell: true` способом (DEP0190).
- **CLI `create from-flow --usecloud` (обработчик)**: IP клиента не передавался в `webhookEvent` — `ipFilter` не
  фильтровал; тело с `isBase64Encoded` не декодировалось. В `serverless.yml` убран блок `secrets`, которого деплой не
  использует.
- **CLI `create from-flow` (`fetchWithTimeout`)**: таймаут ограничивал только ожидание заголовков, тело ответа
  читалось без ограничения. Теперь таймаут покрывает и тело.
- **CLI (Dockerfile)**: процесс работает от пользователя `umbot`, а `/app` принадлежит root — `FileAdapter` и файловые
  логи не могли писать в `json/` и `logs/`. Каталоги создаются и отдаются пользователю в образе.
- **VK (`options._group`)**: группа приводилась к числу, и все нечисловые группы (`'nav'`, `'actions'`) сливались
  в один ряд. Теперь группы сравниваются как строки.

### Безопасность

- **CLI (`.gitignore`)**: npm не публикует файлы с именем `.gitignore`, поэтому `umbot create` и
  `create from-flow` из npm-пакета создавали пустой `.gitignore` — `.env` с токенами и `json/` с данными
  пользователей попадали в git. Шаблон переименован в `gitignore.text`; без него CLI падает с ошибкой.
- **Логи и мусорный трафик**: файлы логов больше не растут без предела — запросы на вебхук от кого угодно не могут
  заполнить диск.
- **`rateLimiter` под потоком новых `userId`**: при заполненной карте (10 000 ключей) каждый новый ключ искал
  вытесняемую запись полным обходом — 75–90 мкс на запрос. На Алисе и Марусе `userId` задаёт отправитель, так что
  поток запросов с разными id упирал ядро в ~12 тыс. запросов/с только на этом. Теперь записи связаны в список по
  давности активности: вытеснение и очистка — O(1), ключ — двухуровневая карта «платформа → ключ».
- **Пакет**: из публикации исключены `dist/.tsbuildinfo` и `cli/AGENTS.md`.

### Документация

- `BENCHMARKS.md`, `performance-and-guarantees.md`, README: результаты всех стендов перемерены на 3.1.4 (сравнение с
  конкурентами на пяти платформах — ни одного поражения, стресс под потоком, cold-start, `npm run stress`); честно
  описаны оставшиеся паритеты и где umbot уступает (память против viber-bot на 6 командах, первый запрос холодного
  старта). Описание методики стенда исправлено: паузы после регистрации у umbot нет.
- `adapter/platformAdapter.md`: `isCorrectQuery(query, headers, parsedQuery)`, ключ дедупликации при проверенной
  подписи, раздел «Синхронно, если ждать нечего» для `setQueryData`/`getContent`. GUIDE: порядок поиска команд с
  индексом подстрок и префильтром регулярок, повторная регистрация команды, O(1) у сессии в памяти.
  `middleware.md`: вытеснение и очистка `rateLimiter`.
- README и `platform-integration.md`: вместо «Полная поддержка» у всех платформ — что именно поддерживается;
  мессенджеры подключаются только через вебхук (long polling нет); Маруся — только существующие навыки; боты Viber —
  на коммерческих условиях.
- `configuration.md`, `GUIDE.md`, `getting-started.md`: секреты вебхука читаются из env, команда `umbot webhook`;
  ротация файловых логов.
- README, документация и описание пакета: второй голосовой платформой после Алисы идёт Сбер Салют, Маруся — в конце
  списков (новые навыки не создаются). Вместо «поддерживается весь функционал голосовых платформ» перечислено, что
  именно поддерживается; директивы Алисы (геолокация, аудиоплеер, аналитика) названы как ещё не абстрагированные.
- `getting-started.md`: создание проекта через `npx umbot create`; отладка с реальной платформой по шагам (туннель →
  `umbot webhook` → перезапуск) с предупреждением не отлаживаться на production-боте; в чеклисте — порты HTTPS
  Telegram и MAX; в примере `.env` — секреты вебхуков.
- `platformAdapter.md`: `getDeliveryId`, `getResponseTimeout` и `isSignatureSupported`; `middleware.md`: `getKey` у `rateLimiter`;
  `http-client.md`: `httpStatus` / `errorBody` и повтор после 429; `performance-and-guarantees.md`: очередь
  пользователя, дедупликация, объём логов.
- `middleware.md`: порядок выполнения был описан неверно (`1, 2, 3, 4`) — на деле `1, 4, 2, 3`, и обработчик
  запускается после всех middleware. Пример «логирование всех запросов» читал ответ после `await next()`, где он ещё
  не сформирован; теперь ответ логируется через `responseCb`.
- `platform-integration.md`: callback VK был описан как `show_snackbar` с текстом ответа — на деле нажатие
  подтверждается, а ответ уходит сообщением.
- FAQ: перечислены классы выражений, которые ищет проверка ReDoS; указано, что это эвристика, а надёжная защита — `re2`.
- `setAppMode`: проверка выражений выполняется при регистрации — режим нужно задавать до `addCommand`/`setPlatformParams`.

## [3.1.3] - 2026-09-19

### Добавлено

- Утилиты для логирования (getErrorMsg, getErrorToken) в адаптерах стали публичными

## [3.1.2] - 2026-09-18

### Добавлено

- **CLI `create from-flow` (кнопки с переходом)**: кнопка с `targetNodeId` теперь ведёт на блок-цель. Генерируется «именная» кнопка с payload `{ command: '[go:N]' }` и `bot.addAction('[go:N]', …)`: нажатие выполняет блок (ответ, действие, условие) либо команду/шаг, которым текст кнопки передаётся как ввод. Действия регистрируются до команд (слоты сравниваются вхождением), а ожидающий шаг пропускает такие нажатия (`return false`). Блоки, на которые ведут только кнопки, тоже генерируются. Без кнопок с переходом сгенерированный код не меняется.
- **CLI `create from-flow` (README и `.env`)**: генерируется `README.md` проекта — установка, токены, запуск, публичный HTTPS-адрес и подключение выбранных платформ; после генерации CLI печатает следующие шаги. Файл `.env` создаётся всегда (с пустыми переменными токенов выбранных платформ), при повторной генерации пустые переменные заполняются токенами из flow.json, заполненные не трогаются.
- **CLI `create from-flow` (приветствие и справка из настроек)**: без ноды welcome/help текст приветствия (с кнопками) и текст справки из настроек бота генерируются командами — раньше они не показывались никогда, так как стандартные интенты отключены (`intents: []`).
- **Telegram (`options.inline`)**: кнопка без payload/url с опцией `inline: true` показывается inline-кнопкой под сообщением; нажатие приходит боту как текст кнопки. Текст длиннее лимита `callback_data` (64 байта) передаётся токеном `#t<n>` и восстанавливается адаптером из клавиатуры сообщения. CLI выставляет опцию всем кнопкам — в сгенерированных ботах все кнопки Telegram inline.

### Изменено

- **Telegram (смешанные кнопки)**: если в сообщении есть inline-кнопки (url/payload), обычные текстовые кнопки тоже показываются inline, а не пропадают. Предупреждение о смешении клавиатур осталось только для кнопок запроса контакта/локации — inline они невозможны.

### Исправлено

- **CLI `create from-flow` (старт диалога)**: `/start` в Telegram и новая сессия Алисы/Маруси (запуск без команды) уходили в fallback — «не понял» вместо приветствия. Теперь приветствие срабатывает на `/start` (плюс стандартные или собственные слоты), а fallback вызывает его при `messageId === 0`. Шаг, ожидавший ответ в прошлой сессии, пропускает начало диалога (`return false`) и не съедает запуск.
- **CLI `create from-flow` (режим бота)**: поле `mode` игнорировалось — сгенерированный бот всегда работал в dev (подробные логи и отладочная информация доступны по URL вебхука). Теперь генерируется `bot.setAppMode(mode)`.
- **CLI `create from-flow` (`.env`)**: `env: './.env'` подключался, только если токены были во flow.json; токен, вписанный в `.env` вручную, бот не видел. Теперь `.env` подключается всегда. Без приветствия во flow.json CLI больше не подставляет своё «Привет!».
- **CLI `create from-flow` (незаданные переменные)**: `{{переменная}}`, которой ещё нет, выводилась в тексте как «undefined», а счётчик `cnt + 1` без начального значения давал `NaN`. Теперь в тексте подставляется пустая строка, в арифметике незаданная переменная считается нулём.
- **CLI `create from-flow` (условия isEmpty/isNotEmpty/contains)**: `isEmpty` генерировался как `!x`, и значение `0` считалось пустым; `contains` по незаданной переменной искал в строке «undefined». Теперь значение сравнивается как строка: пусто только отсутствующее значение или `''`.
- **CLI `create from-flow` (saveTo команды)**: ввод сохранялся после вывода текста (в ответе команды `{{переменная}}` была пустой) и в нижнем регистре. Теперь сохраняется до действий и текста, в исходном регистре (`originalUserCommand`).
- **CLI `create from-flow` (карточки)**: не генерировались заголовок и тип карточки (одна/список/галерея), `{{переменные}}` в картинке, кнопка картинки с переходом, а также карточки у шага и у блока «Действие». Теперь генерируются.
- **CLI `create from-flow` (эмоция, TTS, кнопки)**: эмоция ответа не генерировалась; `{{переменные}}` в TTS, заголовках кнопок и ссылках выводились как есть; «случайный порядок кнопок» у блока «Ответ» игнорировался. Исправлено.
- **CLI `create from-flow` (package.json)**: сгенерированный проект зависел от `umbot 3.1.0` независимо от версии CLI и не получал исправлений фреймворка, на которые опирается сгенерированный код. Теперь версия берётся из пакета, которым сгенерирован проект.
- **VK (`addAction`)**: кнопка клавиатуры с payload `{"command": "buy"}` приходит обычным сообщением, и `addAction('buy')` не срабатывал — в `userCommand` оставалась надпись кнопки. Теперь имя действия берётся из payload, как у callback-кнопок. Системная кнопка «Начать» (`{"command":"start"}`) по-прежнему приходит текстом.

- **CLI `create from-flow` (шаги)**: обработчик шага выводил свой текст до сохранения ответа пользователя и до выполнения действий шага — `{{переменная}}` в тексте подставлялась старым значением («Привет, undefined!»). Теперь шаг сохраняет ответ, выполняет действия и только потом выводит текст — так же, как показывает превью визуального редактора.
- **CLI `create from-flow` (блок «Ответ»)**: `isEnd` у standalone-блока response не генерировался — диалог не завершался, хотя завершение было задано на блоке. Теперь блок выставляет `ctrl.isEnd = true`, как и команда.
- **CLI `create from-flow` (условия eq/neq)**: сравнение генерировалось строгим `===`, поэтому ввод пользователя, сохранённый шагом строкой (`'42'`), никогда не совпадал с числом (`42` из JSON или `rand`). Теперь условия используют `isEqual` из сгенерированного `utils.ts`, который сравнивает значения как строки. Импорт добавляется только при наличии таких условий.

## [3.1.1] - 2026-09-14

### Исправлено

- **Re2 (совместимость окружения)**: у контрибьюторов с установленным `re2` падали юнит-тесты — `expect(...).toBeInstanceOf(RegExp)` не учитывал, что объект RE2 не наследует `RegExp` (проверено duck-typing-хелпером `isRegex`, как это делает сам фреймворк); тест точного совпадения флагов не учитывал, что re2 всегда дописывает `u`; тест ReDoS-логов ждал `error` там, где с re2 корректно пишется `warn` (уровень намеренно зависит от окружения); `tests/Text/re2Engine.test.ts` недетерминированно мокал `re2` поверх настоящего нативного модуля (теперь при реально установленном re2 контракт проверяется против настоящего движка, а мок — только в окружении без него).
- **Re2 (lookaround)**: `getRegExp` (склейка слотов команд, группы регулярок, `isPattern`-строки) бросал `SyntaxError` на lookaround-синтаксис (`(?<=`, `(?=`, …) при установленном `re2` — регистрация команды падала целиком. Теперь выражение, которое re2 не умеет компилировать, пересобирается штатным `RegExp` — та же политика, что у `getRegExpOrSelf` для одиночных слотов. Сообщение ReDoS-предупреждения уточнено: re2 совместим не со всеми выражениями, откатывающиеся выполняются нативно (небезопасные остаются под контролем `isDangerRegex`/strictMode).

## [3.1.0] - 2026-09-13

### Миграция с 3.0.x

- **Telegram**: автоматический `parse_mode` отключён — разметка применяется только при явном `telegram_parse_mode`. Если тексты бота полагались на автоматическую разметку (жирный, курсив), задайте опцию адаптера или экранируйте данные сами.
- **Вебхук**: нераспознанный или неразбираемый запрос платформы теперь возвращает `400` вместо `500`. На собственный код это не влияет, но мониторинг, алертящий на 5xx, больше не будет видеть эти случаи как ошибки сервера.
- **Типы VK/Viber** (влияет только на компиляцию, не на рантайм): поля `waleform` переименованы в `waveform` (`IVkAudioMessageInfo`, `IVkDocSave`); `IVkRequestObject.message` стал опциональным; `location.lat`/`lon` в `IViberParams` — `number` вместо `string`; поле `IViberUserInfo.mss` переименовано в `mcc` (реальное имя поля Viber API — Mobile Country Code; `mss` в ответе платформы всегда был undefined). Код, использовавший старые имена, нужно поправить при обновлении.

### Добавлено

- **Сессия в памяти процесса** (`MemorySessionStorage`, опция `memorySession`): при `isLocalStorage: true` на платформах без локального хранилища (Telegram, VK, MAX, Viber) и без DB-адаптера `userData` хранится в памяти процесса — шаги диалога и данные пользователя работают без БД, как `MemorySessionStorage` у grammY. Лимит — 10 000 пользователей (вытесняется дольше всех не писавший), время жизни — 24 часа с последнего запроса; настраивается `memorySession: { maxSize, ttl }`, отключается `memorySession: false`. Таймеров нет, пустой `userData` не хранится. Ограничения задокументированы и выводятся в предупреждении один раз на платформу: данные теряются при перезапуске и не разделяются между процессами, репликами и вызовами serverless-функции. Раньше в этой конфигурации `userData` между запросами не сохранялась вовсе, и сценарии `addStep` уходили в fallback.
- **Live-тест платформ** (`live-test/`, `npm run live`): консольный инструмент для проверки на настоящих платформах. Прогон сценариев отдаёт синтетический входящий запрос «от вашего тестового пользователя» в `bot.run()` — адаптер делает реальные запросы к API в ваш чат, каждый запрос печатается со статусом и ошибкой API; для голосовых платформ проверяются обязательные поля протокола. Webhook-режим поднимает демо-бота для живого диалога, есть регистрация webhook (Telegram/Viber/MAX). В npm-пакет не входит.
- **События**: новый событийный слой — обработка апдейтов по типу события для всех платформ сразу:
    - `bot.addEvent(eventType, handler)` — регистрация обработчика; выполняется до шагов и команд. Поддерживаются `async`-хендлеры, возврат строки-ответа и `false` («событие не моё» — синхронно и асинхронно одинаково событие передаётся следующему хендлеру, а после исчерпания цепочки запрос идёт по обычному конвейеру). У адаптера без поля `supportedEvents` (прямая реализация `IPlatformAdapter`) оно эквивалентно `['message']`. Управление: `bot.removeEvent(eventType)`, `bot.clearEvents()`.
    - `controller.eventType` — универсальный тип события (`TEventType`): `message`, `photo`, `voice`, `video`, `document`, `location`, `contact`, `sticker`, `callback`, `inline`, `message_edited`, `channel_post`, `start`, `subscribed`, `unsubscribed`, `auth`, `rating`. Заполняется адаптерами всех 7 платформ; хендлеры не вызываются там, где событие физически невозможно. Событие `auth` выставляется и на Марусе (`account_linking_complete_event`), симметрично Алисе.
    - Каждый адаптер объявляет поле `supportedEvents` (дефолт у `BasePlatform` — `['message']`), `bot.addEvent` предупреждает об опечатке в имени события и о событии, которого не поддерживает ни одна платформа. Кастомная платформа от `BasePlatform` объявляет собственный `supportedEvents` и участвует в валидации. Валидатор `isEventType` и перечень `ALL_EVENT_TYPES` — в `src/core/events.ts` (экспортируется из `umbot`).
- **Действия кнопок**: `bot.addAction(actionName, handler)` — обработчик нажатия callback-кнопки по payload. Адаптеры Telegram, VK и MAX нормализуют payload callback-кнопок: строка `'buy'` или JSON `{"command":"buy"}` попадает в `userCommand` как `buy` и срабатывает как обычная команда — без ручного разбора `requestObject`.
- **Match**: `controller.match` — результат совпадения команды с регулярным выражением (`RegExpExecArray`, группы в `match[1]`, `match.groups`). Вычисляется лениво — только при первом обращении обработчика; запросы, не читающие match, не платят за дополнительный прогон регулярки. Строковые `isPattern`-слоты компилируются через общий LRU-кэш `Text.getMatchRegExp` (тот же, что у поиска команд). Работает для RegExp-слотов, `isPattern`-паттернов и групп регулярок.
- **API-фасад**: `controller.api` — унифицированный доступ к возможностям активной платформы из обработчика: `sendPhoto/sendDocument/sendAudio/sendVideo(image, {caption})`, `answerCallback(text, showAlert?)`, `can(method)`. Реализация — ленивый объект-фасад (не класс), создаётся при первом обращении к `ctx.api`; на голосовых платформах — `null`. При невозможности определить `chat_id` фасад отказывается от отправки с явным warn, а не шлёт битый запрос. Telegram поддерживает весь набор; VK — sendPhoto (через штатный upload-flow)/sendDocument/answerCallback; MAX — весь набор через `POST /uploads` (вложения строятся по таблице типов `image|video|audio|file`, интерфейсы `IMaxVideo`/`IMaxFile`; `answerCallback` передаёт `dialog_id` — иначе очередь 2 callback-ответов/сек не включалась и быстрые нажатия давали 429); `caption` доходит до `messagesSend` на VK и MAX; Viber — честные `can() === false` (Bot API Viber требует URL+size). Фасад выбирается самим адаптером — новый опциональный метод `createApi(controller)` контракта `IPlatformAdapter` (базовая реализация `BasePlatform` возвращает `null`, фабрики встроенных платформ живут в `plugins/platforms/<Name>/apiFacade.ts`). Кастомная платформа подключает свой фасад переопределением одного метода — правки ядра не нужны. Диспетчер `makePlatformApi` из `umbot/plugins` сохранён для ручного использования.
- **Telegram webhook-reply**: опция адаптера `new TelegramAdapter(token, { telegram_webhook_reply: true })` — простой текстовый ответ уходит телом webhook-ответа (`{method: 'sendMessage', ...}`) без исходящего POST. Текст проходит ту же обрезку 4096 (со снятием `parse_mode` у обрезанного — единая функция `prepareTelegramMessageText`), что и `sendMessage`: webhook-ответ не получает ошибки API, и слишком длинное сообщение иначе пропадало бы молча. Opt-in, автоматически отключается для callback/inline-запросов и ответов с карточками/звуками (они уходят штатным путём). ID callback-запроса сверяется по обоим источникам (`requestData` и `platformOptions`) — спиннер кнопки подтверждается и при ответе webhook-телом.
- **Env**: новое каноническое имя переменной окружения `ALISA_TOKEN` для Алисы. Старое имя `YANDEX_TOKEN` сохраняется для обратной совместимости — при одновременном задании обеих приоритет у `ALISA_TOKEN`.
- **Env**: новая переменная окружения `SPEECH_KIT_TOKEN` — единый токен Yandex SpeechKit для синтеза речи (TTS) в чат-ботах. Значение автоматически раскладывается в `speech_kit_token` токенов Telegram, VK и Max.
- **Env**: токены из `process.env` теперь подхватываются без настройки `env`/`.env`-файла. Если `appConfig.env` не задан, фреймворк без логов заполняет отсутствующие токены из переменных окружения и не перезаписывает явно заданные (в конструкторе адаптера или конфиге). Благодаря этому `docker run -e TELEGRAM_TOKEN=...` работает без дополнительной конфигурации. Явное `env: 'local'` или путь к файлу сохраняет прежнее поведение с перезаписью.
- **CLI**: новая команда `npx umbot validate <flow.json>` для проверки корректности flow.json перед генерацией (битые ссылки в `edges`, дубликаты id, невалидные идентификаторы, невалидный JSON). Циклы в графе допустимы и ошибкой не считаются.
- **CLI**: новая команда `npx umbot stats --log <path>` для агрегации метрик из лога (счётчики строк/ошибок/ворнингов, топ команд, p50/p95/p99 latency).
- **Middleware**: добавлены стандартные middleware в `src/middleware/` и `umbot/middleware`:
    - `authGuard(check, options)` — проверка авторизации пользователя перед выполнением остальных обработчиков (кастомный `deniedText`, поддержка async check, исключения в `check` трактуются как отказ).
    - `requestId()` — проставление уникального `requestId` в `platformOptions.requestId` для сквозного трейсинга. Запасной вариант для Node < 14.17 (без `crypto.randomUUID`).
    - `maintenance(check, options)` — возврат "сервис на техобслуживании" пока `check()` возвращает true. Ошибки в `check` логируются и не ломают pipeline.
    - `ipFilter(options)` — фильтрация входящих webhook-запросов по IP клиента. Поддерживает IPv4 и CIDR, автоматически нормализует IPv6-mapped. IP берётся из сокета HTTP-запроса (заполняется в `webhookHandle` в `platformOptions.clientIp`). Fail-open вне HTTP-контекста (`bot.run()` напрямую — запрос пропускается).
- **Bot**: новый метод `bot.addForm(formName, options)` для многошаговых форм (опросников). Реализован поверх существующего `addStep` — для каждого поля регистрируется отдельный шаг `__form_<formName>_<idx>`, состояние ответа сохраняется в `userData.__formdata_<formName>`. Поддерживаются синхронные и асинхронные валидаторы (включая async `onComplete`), prompt-функция, команды отмены с кастомным текстом; ответы пользователя сохраняются в исходном регистре (`originalUserCommand`). Снять форму можно методом `bot.removeForm(formName)`.
- **Bot**: класс `Bot` теперь принимает дополнительный generic `TPlatformState`. Пример: `new Bot<MyUserData, MyState>()`. Это даёт типобезопасный доступ к `ctx.state`.
- **BotController**: аналогично `Bot` — второй generic `TPlatformState` для типизации локального хранилища платформы (state).
- **IPlatformData**: в интерфейс добавлено опциональное служебное поле `oldIntentName` для восстановления шагов диалога через локальное хранилище платформы.
- **BotTest**: новый метод `bot.simulate(query, { platform, userId, count, state })` — сам генерирует webhook payload через `getQueryExample` платформы и возвращает структурированный ответ. Упрощает написание интеграционных тестов. Работает без сети: `skipAutoReply` выставляется на время запроса (и восстанавливается после, включая ошибку), поэтому на чат-платформах ответ возвращается локально (`'ok'`), а текст читается через `bot.getBotController()`. Без явной `platform` берётся первая зарегистрированная.
- **VK**: метод `sendMessageEvent()` в `VkRequest` для корректного подтверждения callback-кнопок.
- **AppContext**: поле `eventId` в `IPlatformOptions` для хранения ID события callback-кнопки VK.
- **Маруся**: поддержка типа карточки `ImageGallery`.
- **Маруся**: обработка health-check (ping → pong) для корректного ответа на служебные запросы платформы.
- **Маруся**: исправлено формирование session-объекта в ответе — добавлен fallback для `user_id`.
- **Маруся**: расширен тип `account_linking_complete_event` с `boolean` до `object | boolean`.
- **CLI**: флаг `--force` для `create from-flow`, чтобы перезапись непустой папки была явным действием.
- **Плагины**: новый хелпер `createPlugin(fn)` (экспортируется из `umbot`) — оборачивает функцию-плагин и автоматически выставляет маркер `isPlugin = true`. Убирает частую ошибку, когда забытый флаг заставлял `bot.use()` трактовать плагин как middleware.
- **Bot**: новый метод `bot.webhookEvent(data, headers, clientIp)` — серверлесс-точка входа для обработки входящего события (Yandex Cloud Functions и т.п.) с предварительной проверкой подлинности webhook-запроса через `isCorrectQuery` платформы. В отличие от `run()`, принимает заголовки запроса и отклоняет поддельные запросы со статусом 401 до выполнения логики. Возвращает `{ statusCode, body }`, готовые для возврата из cloud-функции.
- **Документация**: в GUIDE.md добавлена секция «Высокая нагрузка: несколько процессов и горизонтальное масштабирование» (в разделе «Деплой»): масштабирование обработки webhook`ов на несколько инстансов (PM2 cluster, Docker-реплики), требование общей БД и неприменимость `FileAdapter` в multi-process конфигурациях, изоляция платформ по отдельным процессам, особенности serverless-деплоя (read-only ФС, настройка логгера).
- **Примеры**: добавлен демо-проект `examples/umbot-demo` («Кофейня-бот») по ТЗ `src/docs/demo-project-spec.md` — эталонный законченный бот в концепции «один код — 7 платформ». Вся логика построена только на командах (`addCommand`/`addStep`/`addForm`) без собственного контроллера: типизированный `userData` даёт дженерик `BotController<ICoffeeUserData>`. Показаны команды и fallback, многошаговая форма заказа с валидацией, шаг подтверждения с NLU, кнопки, карточка-галерея с ценами, извлечение чисел из фразы, middleware (`rateLimiter`, `requestId`) и `FileAdapter`. Продуктовый слой (`src/menu.ts`): цены напитков и размеров, номер заказа, итоговая сумма и программа лояльности «каждый 5-й кофе в подарок». Два режима запуска: консольный (`BotTest`) и webhook (`Bot.start`). Включает Jest-тесты через `BotTest.simulate()` для Алисы и Telegram. Временно использует локальную версию `umbot` (`file:../..`) до публикации релиза 3.1.0 в npm.
- **Производительность**: `hasAnyNluKey` (экспорт `pUtils`) — фильтр пустых NLU-объектов платформ. Алиса и Маруся присылают `request.nlu` всегда (поле в протоколе), но в большинстве запросов оно пусто; адаптеры больше не записывают пустой nlu: `setNlu({})` семантически идентичен отсутствию вызова (проверено тестами), а геттер `controller.nlu` иначе создавал объект Nlu (+Map кэша) на каждый запрос голосовой платформы. Чат-платформы (Telegram/VK/Max/Viber) пишут в nlu реальные данные (`thisUser`) и не затронуты.
- **Бенчмарки**: `npm run comparison` принимает внешнего участника — `UM_COMPARISON_ROUTER=./my-router.js npm run comparison`. Модуль экспортирует класс с контрактом `addCommand(name, slots, handler)` + `async run(appType, req)`; в таблицах появляется колонка `yours` с теми же сценариями, lockstep-порядком и метриками. Сравнение с umbot можно проверить лично, а не принимать на слово (пример контракта — в заголовке `benchmark/um.js`).
- **Бенчмарки**: новый стенд `npm run compare` (`benchmark/comparison/telegram.js`) — сравнение umbot с реальными Telegram-фреймворками grammy и telegraf. Один Telegram-апдейт на вход всем троим (umbot через TelegramAdapter, конкуренты через их штатный `handleUpdate`); семантика выровнена (подстрочный матч у всех), сеть исключена у всех (`skipAutoReply` у umbot, без `ctx.reply` у конкурентов), lockstep-раунды, p50/p95/RPS/память на запрос. 13 сценариев: 6/50/500/1000 команд × точное/частичное совпадение/регулярка + fallback; найденная команда — в середине списка. Актуальные результаты с оговорками (где umbot уступает или идёт вровень, что именно меряется, как проверить самому) — в BENCHMARKS.md. Стенд изолирован: собственный `benchmark/comparison/package.json` (зависимости ставятся `npm i --prefix benchmark/comparison`, не попадая в package.json репозитория); новые конкуренты добавляются туда же.
- **Бенчмарки**: сравнение с конкурентами расширено на остальные чат-платформы — три новых стенда в той же папке `benchmark/comparison/`, по файлу на платформу (как у Telegram-стенда): `npm run compare:vk` (`vk.js`, конкурент — vk-io + @vk-io/hear), `npm run compare:viber` (`viber.js`, конкурент — viber-bot), `npm run compare:max` (`max.js`, конкурент — официальный @maxhub/max-bot-api). Общая логика (сценарии, lockstep, метрики, печать) вынесена в `runner.js`, платформенные файлы содержат только вход и подключение конкурентов; сценарии и правила честности — те же, что у Telegram-стенда (одинаковый апдейт байт в байт, сеть исключена, подстрочный матч, найденная команда в середине списка; у VK umbot работает с `vk_load_user_info: false` — иначе адаптер по умолчанию ходит в VK API за именем пользователя). Результаты (Windows 10, Node 24): против vk-io umbot лучший в 5 из 12 (vk-io быстрее на 6–50-командных ботах — у него нет цены универсального каркаса; при 500+ командах umbot впереди, а на fallback и частичном матче — почти вдвое, и памяти меньше в 3–10 раз); против viber-bot — лучший в 7 из 12 (на 500–1000 командах быстрее в 4–7 раз, памяти в 2–8 раз меньше; оговорка: viber-bot — единственный и не обновлявшийся с 2022 г. фреймворк Viber в npm); против официального max-bot-api — лучший в 9 из 12 (на 500–1000 командах быстрее в 2–10 раз, памяти на запрос в 8–15 раз меньше). Отдельный стенд Алисы добавлен позже (`compare:alisa`, yandex-dialogs-sdk); для Марусии и SmartApp сторонних фреймворков в npm нет — сравнивать не с кем.
- **Buttons**: метод `ctx.buttons.remove()` — явная просьба убрать ранее показанную клавиатуру. Нужен там, где клавиатура «прилипает» к диалогу и живёт до явной замены: Telegram (reply-клавиатура) и ВКонтакте. У Viber, MAX, Алисы, Маруси и SmartApp клавиатура привязана к сообщению/ответу и исчезает сама, поэтому вызов там безопасен и ничего не меняет. Без этого метода снять клавиатуру было нельзя вообще: пустой список кнопок платформе не отправляется, и старая клавиатура висела у пользователя бесконечно.

### Изменено

- **CLI**: шаблоны генерируемых проектов требуют Node.js `>=22.0.0` (`engines`): совпадает с рантаймом Yandex Cloud Functions (`nodejs22`) и не отсекает Node 22 LTS. В package.json от `from-flow` `engines` теперь тоже задан.

- **Тесты (тестовые артефакты)**: временные папки сьютов переведены из `os.tmpdir()` (недра `AppData/Local/Temp` — артефакты упавшего прогона было не найти) и фиксированных путей в корне репо (`tests/Utils/__util_tmp__`, `__app_context_logs__` — гонки между параллельными прогонами Jest) в единый корень `tests/.tmp/<suite>-<rand>/` (мьюткан в `.gitignore`). Хелпер `tests/helpers/tmpDir.ts`: `createTestDir(suite)` / `await removeTestDir(dir)` (дожидается хвоста in-flight записи tmp+rename — иначе папка пересоздавалась после удаления осиротевшим `.tmp`-хвостом). Отладка упавшего прогона: `UMBOT_TEST_KEEP=1` (все сьюты) или `UMBOT_TEST_KEEP=<suite>` (выборочно) оставляет артефакты в `tests/.tmp`; зачистка — `npm run clean:tmp`. Заодно закрыты утечки таймеров в сьютах FileAdapter: `destroy()` в afterEach (init предзагружает три таблицы, тесты закрывали только одну — debounce-таймеры остальных пересоздавали папку) и `clearTimeout` перед занулением кэша в тесте destroy (таймеры продолжали жить в очереди Node после сьюта).
- **Безопасность типов**: в `tsconfig.json` включены `noUncheckedIndexedAccess` и `exactOptionalPropertyTypes` (рекомендация аудита безопасности). Индексный доступ к массивам/объектам теперь возвращает `T | undefined`, а опциональные поля не принимают явный `undefined`. Весь `src/` приведён к новым флагам: добавлены guard-проверки на lookup-доступ (`for...of` вместо индексных циклов, ранние guard-выходы вместо `arr[0].field`), опциональные поля заполняются только реальными значениями, `x.field = undefined` заменены на `delete x.field`. Поведение в рантайме не изменилось (1347 тестов зелёные), производительность ядра сохранена (~81k RPS в stress-lite).
- **Вебхук**: нераспознанный или неразбираемый запрос платформы теперь возвращает `400`, а не `500` (`webhookHandle`, `webhookEvent`). На `5xx` Telegram и VK включают повторную доставку и в итоге отключают вебхук, хотя проблема в самом запросе, а не в сервере. (См. «Миграция с 3.0.x».)
- **Telegram**: `isPlatformOnQuery` определяет платформу по наличию `update_id`, а не по списку известных полей — теперь распознаются все типы апдейтов (`my_chat_member`, `poll_answer`, `message_reaction` и др.).
- **MAX**: `isPlatformOnQuery` дополнительно принимает неизвестные типы событий по связке `update_type` + `timestamp`, поэтому новые события платформы не ломают обработку.
- **VK**: результат `users.get` кэшируется в памяти процесса (TTL 1 час, до 5000 записей) — раньше запрос уходил на каждое входящее сообщение и удваивал число обращений к API. Кэш не хранит неудачные запросы (сбой API не «замораживает» имя пользователя на час). Загрузку можно отключить опцией адаптера `vk_load_user_info: false`, сброс кэша — экспорт `clearVkUserCache()`.
- **Viber**: раскладка `rich_media` считается по сетке платформы: `Columns` — это доля от `ButtonsGroupColumns = 6`, а не количество карточек (подтверждено документацией Viber). Одиночная карточка занимает всю ширину, галерея раскладывается так, чтобы уложиться в `ButtonsGroupRows = 7`.
- **Middleware**: в JSDoc `rateLimiter` явно описано, что лимит считается на пару `{platform}:{userId}` и что попадание в очередь задерживает ответ минимум на секунду — с учётом таймаутов платформ (у Алисы ~3 с).
- **Telegram**: автоматический `parse_mode` отключён. Разметка применяется только при явном `telegram_parse_mode`; разработчик сам отвечает за экранирование динамических данных. (См. «Миграция с 3.0.x».)
- **Telegram**: устаревший режим `markdown` заменён на `MarkdownV2`.
- **Telegram**: `sendPoll` приведён к Bot API 10.2: 1–12 объектов `InputPollOption`, актуальное поле `correct_option_ids` и проверка индексов без незаметного удаления вариантов.
- **Telegram**: `sendMediaGroup` сохраняет `media` при передаче дополнительных параметров через `FormData` и отклоняет группы вне диапазона 2–10 без молчаливой потери элементов.
- **API версий**: VK API обновлён с 5.103 до 5.199; версия Viber API по умолчанию повышена с 2 до 7 (вынесено в константу `VIBER_DEFAULT_API_VERSION`).
- **Yandex SpeechKit**: исправлен некорректный код языка `en_EN` на стандартный `en-US`. Добавлены новые голоса: `kirkorov`, `lera`, `madusa`.
- **Команды**: базовые команды (`welcome`, `help`), добавленные через `addCommand` без явного указания слотов, теперь автоматически регистрируются со слотами по умолчанию.
- **MongoDB**: пакет `mongodb` переведён в опциональные peer-зависимости и подгружается лениво (динамический `import`) только при вызове `MongoAdapter.connect()`. Пользователи, которым Mongo не нужен (например, только `FileAdapter`), больше не обязаны его устанавливать. Если пакет не установлен, `connect()` возвращает `false` и пишет в лог инструкцию `npm install mongodb` вместо падения процесса.
- **Ядро (рефакторинг)**: константы `FALLBACK_COMMAND`, `WELCOME_INTENT_NAME`, `WELCOME_INTENT_SLOTS`, `HELP_INTENT_NAME`, `HELP_INTENT_SLOTS` больше не реэкспортируются из `AppContext` и `CommandReg` — все потребители и публичный баррель `core/index.ts` ссылаются напрямую на листовой модуль `core/constants.ts`. Публичный API не изменился: константы по-прежнему доступны из корневого `umbot`.
- **Ядро (рефакторинг)**: разорвано последнее runtime-ребро циклической зависимости `core/Bot → controller`. Дефолтный `BaseBotController` подгружается лениво (`require` внутри `#getBotController`) вместо статического импорта в шапке файла — к моменту вызова конструктора все модули уже инициализированы. Типы `BotController`/`IUserData`/`IPlatformData` импортируются как `import type` и стираются при сборке. Поведение и публичный API не изменились.
- **Примеры**: `examples/` переведены в единый запускаемый пакет (`npm install && npm run build && npm run <пример>`): добавлены общие `package.json` и `tsconfig.json`, скрипты запуска для каждого примера, примеры `game`/`gameInCommand` приведены к общей структуре без вложенных пакетов. Код примеров актуализирован под текущий API (`IButtonType`, `IUserData`, индексация в DB-адаптере) и исправлен перезапуск `userData` на `Object.assign` (анти-паттерн, ломающий сохранение данных). Компиляция примеров против текущего API проверяется тестом `tests/examples/compile.test.ts`.
- **BotController**: если `action()` объявлен как `async` и вернул Promise, фреймворк теперь пишет предупреждение в лог с подсказкой использовать async-колбэки `addCommand`/`addStep`/`addForm`. Раньше такой код компилировался без ошибки, но всё после первого `await` молча не попадало в ответ. Ошибки внутри такого промиса логируются вместо `unhandledRejection`.
- **Производительность**: горячий цикл поиска команд (`BotController._getCommand`) переведён с итерации `for...of` по `Map` на индексированный обход по массиву-снимку (`CommandReg.getActualCommandsList()`). Итератор `Map` аллоцирует пару-массив `[ключ, значение]` на каждый шаг (спецификация JS), что при скане давало до 7,8 КБ транзиентного мусора на запрос (1500 команд, матч в середине). Снимок — пары `[ссылка-строка, ссылка-объект]`, копий данных нет; пересобирается лениво только при изменении набора команд (addCommand/removeCommand/clearCommands инвалидируют флаг, сборка — при первом поиске). Измерения: мусор скана −67..100%, регистрация 20 000 команд осталась O(n) (~16 мс), постоянная память снимка ~82–136 Б на команду.
- **Производительность**: быстрый путь для команд с единственным RegExp-слотом без флагов `g`/`y` (поле `ICommandParam.__$singleStatelessRegExp`, вычисляется при регистрации). Такие команды проверяются прямым `re.test()`, минуя обёртку `Text.isSayText`: −37 нс (−54%) и −40 Б мусора на каждую проверенную команду скана. Условия безопасности: нет кастомного RegExp-движка (`plugins.regExp`), нет stateful-флагов — иначе прежний путь. Семантика не изменилась (сброс `lastIndex` сохранён).
- **Производительность**: `AppContext.usedMetric` кэшируется в приватное поле и пересчитывается только в `setLogger` (геттер вызывается несколько раз на каждый запрос из горячего пути). `Bot.#getAppContent` читает `isVoice` с переданного адаптера вместо повторного lookup `platforms[appType]`.
- **Производительность**: `AlisaAdapter.getContent()` не сериализует пустой state через `JSON.stringify` + `Buffer.byteLength` (пустой объект отправляется как `{}` без проверки лимита — 2 байта всегда в норме). Форма ответа не изменилась: по документации Яндекса отсутствие `session_state` сбрасывает стейт сессии, поэтому поле по-прежнему отправляется всегда.
- **Производительность**: `BasePlatform._initTTS` не сканирует текст на `#`/`<`, когда tts пустой или из 1 символа (маркер звука не короче 2 символов).
- **Производительность**: `AlisaAdapter._getResponse` переиспользует уже обрезанный text для tts в голосовом кейсе (`controller.tts === controller.text && text.length <= 1024` — в голосовом режиме Bot подставляет tts ссылочно из text). При длине ≤ 1024 обе функции (`Text.resize` и `resizeAlisaTts`) возвращают исходную строку без изменений — поведение идентично, а regex-скан и конкатенации `resizeAlisaTts` уходят из горячего пути (~0.1 мкс на каждый ответ Алисы).
- **Производительность (диагностика не в горячем пути)**: предупреждение «Платформа не поддерживает локальное хранилище» (`Bot.#runApp`) депонируется — выводится один раз на платформу за жизнь инстанса, а не с каждым запросом. Условие зависит только от конфигурации (`isLocalStorage` + поддержка платформы + DB-адаптер), но проверка стояла в горячем пути: на мультиплатформенных ботах (Алиса с localStorage + Telegram/VK/Viber/MAX без) каждый чат-запрос собирал warning-строку и прогонял её через конвейер маскирования секретов — ~4 мкс и ~1.5 КБ мусора на запрос. Эффект в сравнении с конкурентами (стенд `npm run compare*`, до/после): против vk-io umbot был лучшим в 4 из 12 сценариев — стал в 7 из 13, отставание на 6-командных ботах упало с 3.3× (7.3 против 2.2 мкс) до 1.2× (4.1 против 3.4 мкс), а сценарии с регулярками на 500–1000 команд из проигрыша (63.6 против 57.9 мкс) вышли в победу (78.3 против 93.5 мкс; vk-io в этих же прогонах деградировал до 68.6). Против viber-bot — 7 из 12 → 11 из 13; против max-bot-api — 10 из 12 → 11 из 13. Память на запрос упала с 6.2–6.6 до 4.5–5.1 КБ.
- **Производительность (чаты, ленивый NLU)**: кэш сущностей `Nlu.#cachedData` создаётся при первом обращении, а не в конструкторе. Чат-платформы пишут в NLU только `thisUser` и к сущностям не обращаются — пустой Map аллоцировался на каждый запрос впустую. Данные отправителя записываются через новый хелпер `setThisUserToNlu` (экспорт `pUtils`): если все поля пусты (анонимный канал, битый апдейт, `vk_load_user_info: false` без данных), запись пропускается целиком и объект Nlu не создаётся — зеркально `hasAnyNluKey` для голосовых платформ. Адаптеры Telegram/VK/MAX/Viber переведены на хелпер.
- **Производительность (скан команд)**: из горячего цикла `_getCommand` убран optional-chaining доступ к элементам снимка (`commandTuple?.[0]`/`?.[1]`): после полного GC (compaction двигает объекты) полиморфный доступ восстанавливался медленнее монолитного цикла конкурентов — GC-чувствительность скана была одной из причин узкого проигрыша vk-io в regex-сценариях на стендовой методике (forceGC перед каждой серией). Теперь полиморфизм по «дырявым» элементам снимка закрыт явной проверкой кортежа.
- **Производительность (аллокации)**: замыкание обработки ошибок в `BotController.#commandExecute` создаётся лениво — только когда ошибка действительно произошла (в счастливом пути не аллоцируется); `MaxAdapter.#setMessageData` не создаёт `requestData`-хранилище, когда в апдейте нет `chat_id`.
- **Производительность (группировка RegExp-слотов)**: команды, у которых ВСЕ слоты — готовые `RegExp` (без явного `isPattern`), теперь включаются в группы регулярок, как будто `isPattern = true`. Раньше счётчик группировки считал только явные isPattern-команды, поэтому 1000 готовых RegExp-слотов сканировались линейно (50 мкс/запрос на «1000 команд, регулярка»). Теперь группа комбинирует их в один combined-RegExp: 34.5 мкс против grammy 328.6, vk-io 51.5, viber-bot 141.4, max-bot-api 166.6 — из паритета/проигрыша в победу с отрывом 1.5–9.5×. Смешанные слоты (строка + RegExp) в группу НЕ попадают: их приоритет относительно строковых команд задаётся позицией в списке, а группа матчит «первый подходящий». Семантика матча не изменилась (проверено интеграционными тестами: первый/средний/последний член группы, приоритет с пост-зарегистрированной широкой командой).
- **Исправлено (группы регулярок, debounce)**: при регистрации группы из N команд промежуточные состояния могли «застыть» — `groupData.regExp` получал скомпилированный RegExp от НЕПОЛНОГО набора паттернов (1–3 из 20), и любой запрос, пришедший раньше 35-мс debounce-таймера, получал урезанную группу: часть команд группы не находилась (воспроизведено тестом: последний член группы не матчился). Теперь во время debounce `regExp` остаётся строкой, `getGroupRegExpCompiled` компилирует полную строку через кэш (запросы до таймера получают всю группу), таймер затем подменяет её прогретым объектом; добавлен прогрев JIT первой компиляции. Баг затрагивал и прежние явные isPattern-команды. Регрессионные тесты: `tests/Core/groupDebounce.test.ts`.
- **Производительность (ленивый thisUser в NLU)**: данные отправителя чат-платформ (username/first_name/last_name) записываются в приватный буфер контроллера (новый публичный метод `BotController.setThisUser`), а объект `Nlu` создаётся только при первом обращении бизнес-логики к `controller.nlu` (буфер переносится в Nlu автоматически). Раньше любой непустой `from` в апдейте создавал объект Nlu (+кэш) на каждый запрос, даже если логика NLU не читала — при 1000 RPS это ~40 КБ/с мусора впустую. `nlu.getUserName()` возвращает те же данные, что и раньше. Адаптеры Telegram/VK/MAX/Viber переведены на буфер (хелпер `setThisUserToNlu`), `clearStoreData()` сбрасывает буфер. Память на запрос: 4.6–5.1 → 3.8–4.6 КБ.
- **Производительность (мелочи горячего пути)**: `getExactMatchCommand` выходит раньше по пустой exact-карте (реестр без строковых команд не платит хэш-lookup по длинной строке — `Map.size` O(1)); пустой список интентов — замороженная константа вместо литерала `[]` на каждый fallback-запрос; инлайн бывшего `Bot.#getAppContent` в `#runApp` — минус один промис-хоп из цепочки каждого запроса; блок storage-инициализации вынесен в `#initRequestState` (читаемость горячего метода, без изменения поведения).
- **Бенчмарки (методика замера)**: стенды `npm run compare*` переведены на честную методику: разделённые фазы латентностей и памяти (GC не искажает замер), интерливинг раундов между участниками, единая пауза после регистрации команд (хвосты инициализации не попадают в замер), прогрев каждого участника до плато JIT перед раундом и порог победы «отрыв >5% и >0.3 мкс, иначе паритет». Побочно: Telegram-стенд `competitors.js` заменён на `telegram.js` в едином формате платформенных файлов, добавлен сценарий «50 команд, fallback». Актуальные результаты с оговорками (где umbot уступает или идёт вровень, что именно меряется, как проверить самому) — в BENCHMARKS.md. Подробные таблицы и история уточнений методики — в BENCHMARKS.md.
- **Бенчмарки (Alisa)**: новый стенд `npm run compare:alisa` (`benchmark/comparison/alisa.js`) — сравнение umbot с yandex-dialogs-sdk, самым скачиваемым фреймворком навыков Алисы в npm. Обработка через штатный `handleRequest` SDK (ровно путь его webhook-слоя); семантика выровнена (SDK `command(строка)` матчит только точное совпадение — проверено тестом, поэтому конкуренту слоты задаются RegExp); fallback — `any()`. Результаты (Windows 10, Node 24, методика v4): umbot лучший во всех 13 сценариях — от 1.7 против 3.1 мкс (6 команд, точное) до 35.7 против 207.7 (1000, регулярка) и 41.9 против 248.0 (1000, fallback); памяти 4.4–7.4 против 20–89 КБ на запрос. Оговорка честности: SDK не обновлялся с 2022 и не даёт аналога команд-шагов umbot из коробки (его Stage/Scene опциональны и в стенд не входят). Для Марусии и SmartApp сторонних фреймворков в npm по-прежнему нет.
- **Бенчмарки (стресс-стенд под потоком)**: новый стенд `npm run stress:compare:*` (`benchmark/comparison/stress.js` + `stress-worker.js`) — поведение под непрерывным потоком, а не латентность одного запроса: окно 200 запросов in-flight, 30 с, трафик-микс (точное/частичное/регулярка/fallback = 40/25/25/10), 1000 команд, ротация 1000 разных user_id (апдейты несут разных from_id/user_id — прогревает per-user пути и кэши), каждый участник в изолированном дочернем процессе (падение по heap limit роняет только его строку). Метрики: выполнено запросов и RPS под потоком, p50/p95/p99, доля времени в GC (perf_hooks-observer), тренд пола кучи, retained-дельта после финального GC (утечка). Результаты (Windows 10, Node 24): VK — umbot 75 420 RPS против 31 985 у vk-io (2.4×); Telegram — 78 487 против 370 у grammy и 253 у telegraf (212–310×; конкуренты при 1000 команд тонут в GC — до 66% времени: у grammy/telegraf каждый `hears` — отдельный async-слой middleware, промах проходит все 1000 слоёв); Alisa — 69 190 против 2 734 у yandex-dialogs-sdk (25×: его роутинг — Promise.all по всем командам на каждый запрос ради Levenshtein-релевантности, проверено по исходникам); Viber — 73 654 против 6 698 у viber-bot (11×); MAX — 75 290 против 742 у max-bot-api (101×). Утечек нет ни у одного участника. Метрика retained исправлена: раньше массив задержек самого воркера (8 байт на запрос) был жив в момент замера и давал ложную «утечку», пропорциональную числу запросов (у umbot 8–10 КБ/1000, у telegraf «177 КБ/1000», у max-bot-api «57 КБ/1000» — всё артефакт). Итоговая сводка по обеим методикам и всем конкурентам — в BENCHMARKS.md («Общая картина по всем платформам»), включая зоны честных уступок (vk-io на «6 командах» и fallback). Подготовка стенда вскрыла и задокументированную особенность: без fallback-команды `*` базовый контроллер на промахе отправляет ответ в API платформы — для офлайн-стенда это fetch-таймауты по 30–100 мс на каждом 10-м запросе, стенд регистрирует `*` с `skipAutoReply`.
- **Бенчмарки (cold-start и IQR)**: основной стенд дополнен двумя метриками. Cold-start — время первых 200 запросов после регистрации без прогрева (прода-метрика serverless/Cloud Functions: латентность первого запроса после подъёма инстанса; печатается отдельным блоком, числа несопоставимы со steady-state). Замер на VK: umbot на 500–1000 командах быстрее vk-io даже холодным стартом (точное совпадение O(1) не требует прогрева), на 6–50 командах легче стартует vk-io. IQR (p75−p25) — ширина ядра распределения латентности: p95 дёргается от единичных выбросов ОС, IQR устойчивее. Платформенные файлы стендов переведены на `buildBenchOptions()`-экспорт с guard `require.main` — их теперь подключают как модули stress/baseline-инструменты, не запуская стенд.
- **Бенчмарки (регрессионный baseline для CI)**: `npm run baseline:update|check` (`benchmark/comparison/baseline.js`) — night-job-контроль латентности: p50 umbot на маркерном сценарии «50 команд, точное совпадение» всех пяти стендов (медиана 3 раундов с прогревом до плато) против `baseline.json` с допуском +15% (JIT-шум между прогонами); `check` возвращает exit 1 при регрессии. Ловит «кто-то занёс тяжёлый компайл в горячий путь» автоматикой, а не ручным прогоном. Baseline создаётся на машине CI в setup-джобе, check гоняется ночным джобом на той же машине.
- **Бенчмарки**: проверка доступной памяти в `bench` и `stress` учитывает реальный потолок min(лимит heap V8, cgroup-лимит docker/k8s, физическая память с учётом сброса page cache) вместо `os.freemem()`. Раньше на unix-машинах, где память занята page cache, тест ложно отказывал в запуске даже для 1000 команд при 16 ГБ RAM.
- **Бенчмарки**: `predictMemoryUsage` в `benchmark/command.js` пересчитан по фактическим замерам (~466 Б/строковая команда, ~0.8 КБ/isPattern, ×12 прогонов на каждый count с учётом фрагментации V8 кучи). Старая формула (2 Б/команду) была занижена в сотни раз — на 200 000 команд тест падал по heap OOM с дампом вместо честного сообщения «Недостаточно памяти». Теперь тест честно отказывается от 200 000+ команд и печатает рекомендацию. В `stress-test.js` формула наоборот завышена в 14 раз — приведена к фактическим 1 КБ/команду.
- **Бенчмарки**: приведён к профессиональному виду вывод всех запускаемых бенчмарков. `command.js`: исправлен XOR-баг в `memResult` — ветка ГБ была недостижима (`(abs < 1024) ^ 2` всегда truthy), 2 ГБ печатались как «2048.00MB», единицы KB/MB/GB не соответствовали переданным килобайтам; времена ниже 1 мс печатались как «0.0002800 мс» (7 знаков) — теперь «0.28 мкс»; двойной знак «+-4.63 КБ» у отрицательных приростов убран. `stress-test.js`: «MB» в русском выводе → «МБ», «Rss» → «RSS» (аббревиатура), «p95 latency» → «p95 (время ответа)». `um.js`: исправлена опечатка «ассихронный» в названии сценария, добавлена легенда вердиктов (`++` / `== паритет` / `+-` / `--`) — раньше их приходилось расшифровывать по звёздочкам в подвале.
- **Публичный API (типы)**: класс `CommandReg` (тип публичного поля `AppContext.command`), интерфейс `IDangerRegex` (возврат `CommandReg.isDangerRegex()`) и тип `ITelegramWarnContext` (параметр `prepareTelegramMessageText()`) экспортированы из `umbot` и `umbot/plugins` — раньше они фигурировали в сигнатурах публичных членов, но не попадали в сгенерированную документацию и не были доступны потребителям для типизации. Чистое расширение, обратная совместимость не затронута. Заодно устранены все предупреждения typedoc (мёртвые entry points `cli/index.ts`/`src/docs`, битая ссылка на приватный `#getCache` в JSDoc `Nlu`).
- **Ссылки**: публичный адрес документации изменён с `https://www.maxim-m.ru/bot/ts-doc` на `https://www.maxim-m.ru/docs/umbot` — обновлены все ссылки в README, гайдах `src/docs/`, `cli/README.md`, примерах и скриптах, генерирующих ссылки (`scripts/fix-doc.js` — `baseUrl` для переписывания markdown-ссылок; `scriptDoc.js` — `DOCS_URL` для canonical/og:url/sitemap.xml/llms.txt). Файлы на сервере не переносятся: путь `/docs/umbot/*` обслуживается внутренней перезаписью Apache из прежней физической папки `bot/ts-doc`, а старые ссылки `/bot/ts-doc/*` пока получают временный (302) редирект — после стабилизации его стоит поменять на постоянный (301), чтобы поисковики склеили адреса (canonical/og:url/sitemap уже указывают на новый путь). Готовые правила для `.htaccess` — `htaccess-docs.txt` в корне репозитория.
- **Ссылки**: все ссылки на репозиторий GitHub переведены с `max36895/universal_bot-ts` на `max36895/umbot` (README, SECURITY.md, CHANGELOG, `typedoc.json`, `package.json`, примеры, live-тест) в связи с переименованием репозитория; старые адреса продолжают открываться через штатный редирект GitHub.

### Безопасность

- **Алиса**: `session.skill_id` интерполировался в URL Yandex API без экранирования (`skills/${skillId}/images` у YandexImageRequest/YandexSoundRequest). Вебхук Алисы не подписывается, значит skill_id полностью контролируется отправителем — `../../other-skill/images?x=` уводил запрос с OAuth-токеном за пределы пути навыка. Значение экранируется через `encodeURIComponent` (зеркально фиксу userId у SmartApp в этом же релизе). Регресс-тесты в `tests/Request/YandexImageRequest.test.ts` и `tests/Request/YandexSoundRequest.test.ts`.
- **[Критично] CLI**: инъекция кода в генератор проектов. Последовательность `*/` в имени команды, шага, блока или в тексте ответа закрывала JSDoc-комментарий в `src/index.ts`, и всё, что шло следом, попадало в сгенерированный проект как исполняемый код. Так как `flow.json` приходит из визуального редактора и может быть получен извне, это давало выполнение произвольного кода в проекте пользователя. Добавлено экранирование (`escapeComment`) во всех местах, где текст из flow попадает в комментарий, и регресс-тест в `tests/cli/flowGenerator.test.ts`.
- **[Критично] Логи**: конвейер `maskSecrets` не покрывал реальные форматы секретов. Токен Telegram в «голом» виде (`<bot_id>:<35 символов>`, как он лежит в `.env`) не маскировался — прежний шаблон требовал литеральный префикс `bot` и срабатывал только внутри URL API; шаблон VK (`vk1a<79 символов>`) не совпадал ни с одним реальным токеном (настоящий формат — `vk1.a.<payload>` с точками). Добавлены шаблоны для голого Telegram-токена (включая хвосты короче 35 символов), реального формата VK, JWT (Сбер SmartApp, OAuth), IAM-токена Яндекса (`t1.<seg>.<seg>` — точки разбивали строку на сегменты короче порога), токена Viber (~46 hex, ниже порога), MAX-токена (UUID с дефисами), Яндекс OAuth (`y0_…`/`y1_A…`), Api-Key SpeechKit, пароля БД по ключам `pass`/`password` и `access_token` в JSON-строках. Плюс контейнерное маскирование: объекты и массивы под ключами `token`/`secret`/`password`/`api_key`/`authorization` (и т.п.) маскируются целиком на любой глубине вложенности — раньше `tokens: {telegram: ...}` попадал в лог как есть. Все прежние покрытия сохранены. Регресс-тесты в `tests/Core/maskSecrets.test.ts`.
- **[Критично] SmartApp**: `userId` подставлялся в URL внешнего хранилища без экранирования. У SmartApp нет проверки подписи вебхука, поэтому `uuid.userId` полностью подконтролен отправителю: значение вида `../../admin?x=` выводило запрос за пределы пути хранилища и позволяло подменить query-параметры. Значение экранируется через `encodeURIComponent`.
- **ReDoS**: небезопасное регулярное выражение вне `strictMode` регистрировалось с обычным предупреждением. Без пакета `re2` (он опциональный) движок Node уязвим к катастрофическому бэктрекингу — на выражении вроде `^(a+)+$` одно сообщение из 41 символа занимает поток на минуты, и бот перестаёт отвечать всем пользователям. Теперь такая связка логируется как ошибка с объяснением последствий и тремя вариантами решения (`npm i re2`, `strictMode`, переписать выражение). ⚠️ Сама возможность зарегистрировать опасное выражение вне `strictMode` сохранена ради обратной совместимости — это решение остаётся за разработчиком приложения.
- **CLI**: инъекция в генерируемые конфигурационные файлы. Ключи и значения токенов из `flow.json` записывались в `.env` и `serverless.yml` без санитизации: перевод строки в ключе или значении позволял дописать произвольные переменные окружения или YAML-поля (файл приходит из визуального редактора и может быть получен извне). Имена переменных приводятся к безопасному виду `A-Z0-9_`, значения очищаются от переводов строк, а при перезаписи существующего `.env` выводится явное предупреждение. Регресс-тесты в `tests/cli/flowGenerator.test.ts`.
- **[Критично] CLI/Cloud**: command injection в генерируемом `scripts/deploy.js` на Windows. Скрипт вызывал `spawnSync('yc', args, { shell: true })`, при котором Node склеивает команду и аргументы в одну строку без экранирования: спецсимволы (`&`, `|`, `"`, `^`) в значении токена из `.env` исполнялись командной оболочкой, а значения приходят из недоверенного `flow.json`. Аргументы теперь оборачиваются в двойные кавычки (для cmd внутри кавычек метасимволы литеральны), значения `.env` чистятся от переводов строк, управляющих символов и кавычек. Регресс-тест в `tests/cli/flowGenerator.test.ts`.
- **Логирование**: `logMetric` передавал имя метрики и label в кастомный логгер без маскирования. `Request` кладёт в label полный URL запроса, а для Telegram он содержит токен бота (`https://api.telegram.org/bot<ТОКЕН>/...`) — токен утекал в системы наблюдаемости. Label метрик проходит тот же конвейер маскирования, что и logError/logWarn. Регресс-тест в `tests/Core/maskSecrets.test.ts`.
- **CLI**: `create` с параметрами записывал токены/пароли в `.env` без вырезания переводов строк — значение вида `x\nMALICIOUS_VAR=1` дописывало произвольные переменные окружения. Значения санитизируются так же, как в генераторе из `flow.json`. Регресс-тест в `tests/cli/consoleController.test.ts`.
- **FileAdapter**: выборка по первичному ключу `__proto__`/`constructor`/`prototype` возвращала унаследованные свойства `Object.prototype` как «найденную запись» (пользователь с таким platform-id ошибочно считался существующим, а его данные потом молча не сохранялись). Чтение защищено тем же guard'ом, что запись и удаление. Регресс-тесты в `tests/DbModel/fileAdapterSelect.test.ts`.
- **ReDoS**: `isRegexLikelySafe` получила эвристику «лестницы квантифицированных атомов» — раньше паттерны вида `.*.*.*!`, `x+x+y`, `\w*\w*\w*!` (последовательные неограниченные квантификаторы по пересекающимся классам символов, без групп) проходили проверку во всех режимах, включая `strict_prod`, и давали до ~75 секунд блокировки event loop на одном сообщении из 1024 символов (замер Node 24). Повторный red-team-аудит вскрыл обходы первого варианта эвристики, финальная версия закрывает и их: **тела групп анализируются рекурсивно** с «склейкой» хвоста цепочки изнутри группы с внешними атомами — без этого `(?:a*a*)c` проходил проверку и блокировал поток на ~2.7 минуты на 7000 символах; ловятся **backreference-лестницы** `(a+)(?:\1)+x`, `([a-z]+)[^]*\1z` (backref после неограниченного квантификатора) и `[^]` как взрывное тело группы. Ложные срабатывания первого варианта сняты: цепочки 3+ без хвоста (`.*.*.*`, `\w+\w+\w+`, `[0-9]+[0-9]+[0-9]+`) опасны только с последующим якорем конца строки `$`/`\Z`/`\z` (замер: без якоря V8 завершает early-exit за 0 мс, с якорем — таймаут), поэтому легитимные паттерны, ломавшиеся в strict_prod, снова проходят. Непересекающиеся классы (`[a-z]+[0-9]+x`) и пары без хвоста (`.*.*`) проходят как раньше. Ограниченные интервалы внутри квантифицированной группы (`(\d{1,3}\.){3}\d{1,3}` — IP-адрес, `(a{1,3}){3}`) проходят: ревью релиза показало, что раньше `isExplosiveGroupBody` браковал любой `{`/`.` в теле группы, и легитимный IP-паттерн молча отключался в strict_prod. Опасность теперь определяется по неограниченности внутреннего квантификатора (`+`, `*`, `{n,}` опасны; `{n}`/`{n,m}` — нет), а «интервал под неограниченным внешним квантификатором» (`(?:a{2,3})+`, `(a+){10,100}`, `(a{1,}){3}`) по-прежнему блокируется.
- **ReDoS**: `Bot.#runApp` обрезает `controller.userCommand` до 7000 символов (максимум легитимного сообщения среди платформ, лимит Viber) до NLU и матчинга команд. Платформенные лимиты Алисы (1024) фреймворком не проверялись: напрямую сконфигурированный вебхук без подписи мог протолкнуть в регулярки строку до 2 МБ. `originalUserCommand` не трогается — бизнес-логика сохраняет полный текст.
- **Исходящие запросы**: `Request` выставляет `redirect: 'manual'` — клиенты фреймворка ходят на фиксированные endpoint'ы API платформ с секретами в заголовках (`Authorization` у MAX, `X-Viber-Auth-Token`, OAuth у Яндекса) и в URL (токен Telegram в path). Автоследование редиректу при компрометации DNS/CDN унесло бы секреты на сторонний хост; теперь 3xx — ошибка запроса.
- **CLI**: `npx umbot create config.json` мигрирует токены из вложенного формата `config.tokens.{platform}.token` (задокументирован в README) в `.env` при `isEnv: true`, а при `isEnv: false`/отсутствии флага — вычищает их из генерируемого `src/config/*Config.ts` с предупреждением в консоль (раньше без флага токены сериализовались литералом в коммит-файл, не покрытый `.gitignore` сгенерированного проекта). Имя env-переменной из ключа платформы санитизируется до `A-Z0-9_`: ключ с переводом строки не может дописать произвольную переменную в `.env`.
- **CLI**: `create from-flow` больше не перезаписывает существующий `.env` — пользовательские значения (вписанные вручную реальные токены) сохраняются, дописываются только недостающие переменные. Раньше повторная генерация с `--force` молча затирала реальные токены значениями из flow.json.
- **CLI**: `sanitizeEnvValue` в генерируемом `scripts/deploy.js` экранирует `%` удвоением (`%%` раскрывается cmd.exe в литеральный `%`), а не вырезает: cmd раскрывает `%VAR%` даже внутри двойных кавычек, и значение из недоверенного flow.json могло подставить содержимое переменных окружения машины пользователя в аргументы деплоя — при этом легитимные секреты с процентом больше не искажаются.
- **Mongo**: `MongoAdapter` разделяет операторы на разрешённые в условиях выборки и опасные. Документированный API операторов сравнения (`$gt`, `$gte`, `$lt`, `$lte`, `$ne`, `$in`, `$nin`, `$or`, `$and`, `$elemMatch` и др. — JSDoc `IQueryData`, dbAdapter.md) продолжает работать; отклоняются до драйвера исполняющие на сервере БД операторы (`$where`, `$function`, `$accumulator`, `$expr`) и любые незнакомые `$`-операторы, ключи прототипа (включая dotted-пути вида `'a.__proto__.x'`, `'user.$ne'`) и вложенность глубже 10 уровней. В данных записи (update/insert) `$`-ключи запрещены полностью: фреймворк сам оборачивает данные в `$set`, а Mongo не хранит `$`-поля. Стандартный поток и раньше не пропускал операторы (объекты размываются `escapeString`), но кастомные модели и пользовательские `where` могли пронести их в `findOne`/`updateOne` напрямую.
- **ipFilter**: при запросе без IP клиента (fail-open by design для тестов/консоли) пишет предупреждение — однократно на инстанс middleware.
- **Bot**: предупреждения при `bot.start()` о небезопасной конфигурации (не блокируя запуск): режим `dev` (проверка ReDoS отключена), платформы с неподтверждённым вебхуком (секрет задан, но проверка подписи не включена — по точной логике каждого адаптера через новый опциональный метод `IPlatformAdapter.isSignatureCheckEnabled()`; определение платформы учитывает и VK, у которого подпись приходит в теле, без `signatureName`), прослушивание всех интерфейсов (`0.0.0.0`/`::`). Новый контракт `signatureName`/`isSignatureCheckEnabled` в `IPlatformAdapter` — backwards-compatible расширение.
- **Документация**: JSDoc `AppContext.log()` явно предупреждает, что аргументы НЕ маскируются (в отличие от `logError`/`logWarn`/`logMetric`) — метод предназначен для операционных сообщений, секреты передавать нельзя.
- **Бенчмарки**: `benchmark/request.js` — убраны захардкоженные живые токены Telegram/VK и ID чатов; теперь читаются из переменных окружения. Токены из git-истории считаются скомпрометированными — выведите их через @BotFather / настройки приложения VK.
- **ReDoS / re2**: при установленном `re2` RegExp-объекты из слотов команд выполнялись штатным движком Node в обход него — через `getRegExpOrSelf`, быстрый путь одиночного RegExp-слота и прямое использование слотов при числе команд меньше 500. Теперь такие слоты один раз пересобираются через `re2` (с кэшем); если `re2` не поддерживает синтаксис выражения (lookbehind, обратные ссылки), используется исходный объект.
- **ReDoS**: убраны квадратичные регулярки, работающие с входными данными: снятие тегов `/<[^>]*>/` (текст Telegram при обрезке HTML, текст Маруси из TTS, речь для SpeechKit) заменено линейной функцией `stripTags` (экспортируется из `umbot/utils`), проверка SSML в SmartApp, поиск номера элемента в `Navigation.selectedElement` и поиск ссылок `Nlu.getLink` переписаны без откатов. Поведение прежнее.

### Исправлено

- **Медиа без БД (все платформы)**: загруженный токен картинки/звука возвращался только при успешной записи в БД (`model.save()`), а без DB-адаптера запись всегда неуспешна — картинки и звуки из локальных файлов не доходили до пользователя на VK, MAX, Алисе, Марусе (у Telegram терялся кэш), хотя загрузка выполнялась. Кэширование вынесено в `cacheMediaToken`: токен возвращается всегда, неудачная запись в БД — только предупреждение.
- **Алиса/Маруся (кнопки)**: запрос `ButtonPressed` по протоколу не содержит `command`/`original_utterance` — только `payload` и `nlu.tokens`. Для объектного payload команда бралась из пустого `command`, и нажатие кнопки уходило в fallback. Теперь команда берётся из `payload.command`/`payload.action` (как `addAction` на Telegram/VK/MAX), иначе — из текста кнопки. Строковый payload кнопки (протокол требует JSON-объект) оборачивается в `{command: '...'}`; у Маруси из кнопки убрано недокументированное поле `hide`.
- **Алиса/Маруся (health-check)**: ответ на `ping` не содержал обязательного `end_session` (Алиса) и `session` (Маруся) — невалидный по протоколу ответ на периодическую проверку, из-за которой Яндекс отключает «неотвечающие» навыки.
- **Маруся (карточки)**: формат приведён к протоколу скиллов (сверено по архивной копии документации: VK удалил её после закрытия пользовательских скиллов 20.12.2024): `BigImage {type, image_id}`, `ItemsList {type, items: [{image_id}]}`, `image_id` — integer. Раньше отправлялась схема Алисы (заголовки, описания, кнопки, header/footer) и несуществующий у Маруси тип `ImageGallery` (теперь галерея уходит как ItemsList). Поля `title`/`description`/`button` в `IMarusiaImage` помечены `@deprecated`, `image_id` расширен до `number | string`.
- **Маруся**: `response.text` по протоколу не может быть пустым (в отличие от Алисы) — при пустом тексте он заполняется из `tts` без звуковой разметки и знаков ударения.
- **SmartApp (кнопки)**: `server_action` кнопки отправлялся гибридом `{action_id, payload}`, а в запросе SERVER_ACTION читался `parameters` — payload терялся, `userCommand` оставался пустым, нажатие уходило в fallback. Теперь используется документированная форма `{action_id, parameters}` (устаревшая `{type, payload}` читается для совместимости), имя действия — собственный `action_id` либо `command`/`action`/`value` из параметров.
- **SmartApp**: `payload.items` — обязательное поле ANSWER_TO_USER — отсутствовало в ответе без текста и карточек; успешное сохранение во внешнее хранилище (ответ 200 с пустым телом) логировалось как ошибка на каждом запросе.
- **Голос в чатах (Telegram/VK/MAX)**: вопреки документации («при заданном `speech_kit_token` tts озвучивается») синтез запускался только при непустом `controller.sound.sounds` — одного `tts` было недостаточно. Теперь достаточно `tts` + `speech_kit_token`; из текста для синтеза вычищается разметка голосовых платформ (`#game_win#`, `<speaker>`, паузы), которую SpeechKit иначе зачитывал вслух.
- **Telegram (TTS)**: синтезированный OGG/Opus отправлялся через `sendAudio`, который по Bot API принимает только MP3/M4A. Добавлен `TelegramRequest.sendVoice()`, TTS уходит голосовым сообщением.
- **MAX (`bot_started`)**: событие нажатия «Начать» помечалось `skipAutoReply` — приветствие не уходило никогда, даже из `bot.addEvent('start')`. Теперь на `bot_started` отвечает обычный конвейер (welcome / обработчик события), deep-link `payload` события попадает в `controller.payload`.
- **Алиса/Маруся**: предупреждение о нескольких хранилищах state выводилось на каждый запрос, хотя платформа присылает их одновременно штатно — теперь один раз на адаптер.
- **Ядро**: при `isLocalStorage: true` на платформах без локального хранилища (Telegram, VK, MAX, Viber) `userData` не читалась из подключённой БД, но писалась в неё — `oldIntentName` терялся между запросами, и все сценарии `addStep` уходили в fallback. Flow-редактор выставляет `isLocalStorage: true` по умолчанию, поэтому ломались сгенерированные чат-боты. Теперь, когда localStorage платформы недоступен, источник `userData` — БД, а без DB-адаптера — сессия в памяти процесса (см. «Добавлено»). Регресс-тест в `tests/Bot/regressions.test.ts`.
- **Ядро**: async-шаг (`addStep`), вернувший `false`, считался выполненным — конвейер команд не продолжался, пользователь получал пустой ответ. Теперь `Promise<false>` обрабатывается как синхронный `false` (тип `IStepParam.cb` расширен: `void | false | Promise<void | false>`).
- **Ядро**: синхронное исключение из `action()` и из `customCommandResolver` пробрасывалось до webhook-обработчика и давало HTTP 500 (Telegram повторяет апдейт, VK отключает сервер), тогда как async-ошибки тех же колбэков гасились. Теперь ошибка логируется: `action()` → текст «Не удалось выполнить команду», резолвер → команда считается не найденной (intent → fallback).
- **SpeechKit (TTS в чатах)**: запрос не соответствовал контракту Yandex Cloud и отклонялся всегда — тело уходило JSON (API принимает только `application/x-www-form-urlencoded`), авторизация — схемой `OAuth` (SpeechKit понимает только `Api-Key` и `Bearer`). Тело теперь сериализуется формой; токен `t1.…` уходит как `Bearer` (IAM), остальные — как `Api-Key`; явно заданная схема передаётся как есть. У API Диалогов (загрузка медиа Алисы) схема `OAuth` сохранена.
- **VK**: фото загружались в multipart-поле `file`, а сервер загрузки фото принимает только `photo` — картинки карточек/карусели и `controller.api.sendPhoto` из локальных файлов не отправлялись. `VkRequest.upload()` получил необязательный параметр `fieldName` (`'file'` по умолчанию, обратная совместимость).
- **VK**: `docs.save` возвращает обёртку `{type, doc | audio_message}`, а идентификаторы читались с верхнего уровня — в attachment уходило `docundefined_undefined` (голосовые/TTS и `controller.api.sendDocument`). Добавлен разбор вложенного объекта (`VkSound.getVkDocAttachment`).
- **MAX**: ответ сервера загрузки для `audio`/`video` — `retval` (не JSON), а `MaxRequest.upload()` разбирал его как JSON и падал — аудио и TTS не отправлялись. Ответ читается текстом, токен берётся из `token`/`photos.<id>.token` или с шага `POST /uploads`. При отсутствии токена `upload()` возвращает `null`: раньше одноразовый upload-URL кэшировался как токен картинки/звука и уходил битым `payload.url`.
- **MAX**: link-кнопка с payload получала недокументированное поле `payload` (оно есть только у callback); `controller.api.answerCallback()` в личном диалоге не передавал ключ диалога — очередь «2 callback-ответа/сек» не включалась.
- **VK/MAX (TTS)**: токен каждой озвучки кэшировался в `SoundTokens` по уникальному пути временного файла — таблица (и кэш FileAdapter в памяти) росла на каждый TTS-ответ без повторного использования. TTS загружается без кэширования.
- **Viber**: приветствие на `conversation_started` отправлялось REST `send_message`, который Viber отклоняет для неподписанного пользователя — welcome никогда не доходил. Теперь оно возвращается JSON-телом webhook-ответа (единственный документированный канал; новый `ViberRequest.buildWelcomeMessage()`). Карточки и звуки в приветствие не попадают (warn).
- **Telegram**: inline-ответ брал только `text` — при заполненном лишь `tts` уходил пустой результат (как в обычном ответе, используется `getChatText`).
- **Card**: `clear()` не сбрасывал кнопки карточки — они протекали в следующий запрос переиспользуемого контроллера (`BotTest`, `simulate`).
- **VK**: элемент карусели без кнопки в не-gallery ветке выбрасывался молча — добавлено предупреждение, как в gallery-ветке.
- **VK**: карусель с разным числом кнопок у элементов VK отклонял целиком (документация требует одинаковую структуру всех элементов). Число кнопок выравнивается до минимального среди элементов, с предупреждением.
- **VK**: сообщение с каруселью без текста VK отклоняет (`message` обязателен). Если текста ответа нет, в `message` подставляется заголовок карточки (`card.title`), затем заголовок первого элемента; если нет и их — предупреждение.
- **Маруся**: картинки загружались в multipart-поле `file`; сервер загрузки (та же инфраструктура, что у фото VK, ответ `{server, photo, hash}`) принимает поле `photo`.
- **MAX**: поля, специфичные для типа кнопки, уходили в кнопку любого типа. По схеме официального SDK `@maxhub/max-bot-api`: `quick` — только у `request_geo_location`, `contact_id` — только у `open_app`. `intent` в текущей документации и SDK отсутствует (наследие TamTam) и передаётся только `callback`-кнопке.
- **Preload**: для Алисы загрузка и удаление ресурсов не работали вовсе — API ресурсов Алисы адресуется по навыку, а skill_id брался из запроса, которого при предзагрузке нет. Добавлена опция `alisaSkillId` в `IOptions` (для `loadImages`/`loadSounds`, а также новым необязательным третьим аргументом `removeImages`/`removeSounds`); без неё Алиса пропускается с предупреждением.
- **Preload**: `removeImages()`/`removeSounds()` никогда не удаляли — поиск шёл через `where()` (не заполняет модель) по несуществующему полю `type`, а одна модель делилась между параллельными промисами. Теперь `whereOne({path, platform})` и своя модель на операцию.
- **CLI (create)**: без `isEnv` плоские `params.*_token` и `config.db.pass` сериализовались plaintext в коммит-файлы `Params.ts`/`Config.ts` (а `config.tokens` при этом вычищались). Теперь они удаляются с предупреждением (фреймворк 3.x плоские `*_token` не читает; `DB_PASSWORD` из окружения приоритетнее `db.pass`).
- **CLI (from-flow)**: сгенерированный `tsconfig.json` не задавал `types: ["node"]` — с TypeScript 6.0 (`types` по умолчанию пуст) проект с `http_request` не собирался (`TS2339: 'unref' does not exist on type 'number'`). Хелпер тайпчека в тестах больше не подставляет `types` сам и проверяет ровно пользовательский tsconfig.
- **Ядро**: устранено нарушение инварианта «core не зависит от plugins» — `Bot.run()` подключал фасад `controller.api` ленивым `require('../plugins/platforms/Base/apiFacade')` (хак в обход архитектуры), а сам диспетчер держал жёсткий `switch` по `appType` с зашитыми ветками встроенных платформ. Для любой кастомной платформы `controller.api` навсегда оставался `null` — фасад было невозможно ни получить, ни переопределить. Фасад теперь выбирается адаптером через новый метод `createApi(controller)` контракта `IPlatformAdapter` (см. «Добавлено»); из ядра удалены require-хак, кэш фабрики и знание о конкретных платформах. Поведение встроенных платформ не изменилось; для кастомных `null` сменяется на рабочий фасад, если адаптер переопределил `createApi`. Регресс-тесты в `tests/Platforms/apiFacade.test.ts` (в т.ч. полный цикл `BotTest.simulate` с кастомной платформой).
- **Bot**: warn при старте в режиме `dev` заявлял «проверка регулярных выражений на ReDoS отключена» — на деле проверка в dev выполняется всегда: опасные выражения логируются, но не отклоняются (отклонение — только `strict_prod`). Текст приведён к реальному поведению.
- **Viber**: payload-кнопки не нормализовались — Viber оставался единственной кнопочной платформой без `normalizeActionPayload` (Telegram/VK/MAX её применяют): кнопка с объектным payload `{"command":"buy"}` приходит текстовым сообщением с JSON-строкой, `userCommand` оставался сырой строкой, и `addAction`/`addCommand` не срабатывали, хотя на остальных платформах работали. Payload нормализуется в имя действия (`'buy'` или `'{"command":"buy"}'` → `'buy'`); поведение обычного текста не изменилось (lower + trim, как раньше). Регресс-тесты в `tests/Platforms/Viber/adapter.test.ts`.
- **CLI (create)**: повторная генерация с `--force` перезаписывала существующий `.env` значениями-черновиками из JSON-конфига — реальные токены, вписанные пользователем вручную, молча терялись (from-flow ведёт себя иначе: append-only). Create-ветка приведена к тому же контракту: существующий `.env` не перезаписывается, дописываются только отсутствующие переменные, при дописывании — предупреждение. Регресс-тест в `tests/cli/consoleController.test.ts`.
- **CLI (from-flow)**: санитизация Mongo-кредов (`DB_USER`/`DB_PASSWORD`) вырезает кавычки и управляющие символы из значения, попадающего в `.env`, — пароль `p"ss` превращался в `pss`, и подключение падало с «Authentication failed» без видимой причины (в `.env` лежал «правильный на вид» пароль). Искажение значения теперь сопровождается явным предупреждением с именем переменной и подсказкой. Регресс-тест в `tests/cli/flowGenerator.test.ts`.
- **CLI (from-flow)**: генератор писал `.env` (токены, `DB_USER`/`DB_PASSWORD`), но генерируемый `bot.setAppConfig` не содержал `env: './.env'` — ядро в тихом режиме читало только `process.env`, файл игнорировал, и записанные генератором токены/креды не действовали при обычном `npm start` (несимметрично с create-веткой). Генерат получает `env: './.env'`, когда генератор реально пишет `.env` (токены или Mongo-креды). Регресс-тест в `tests/cli/flowGenerator.test.ts`.
- **CLI (deploy.js)**: значения `.env` с запятой разрезали `--environment` на две битые переменные (yc CLI делит пары по запятым) — такая переменная теперь пропускается с явным warn вместо молчаливой порчи секрета.
- **CLI (create)**: объект вместо строки в `config.tokens.{platform}.token` записывался в `.env` как `[object Object]` — теперь переменная пропускается с предупреждением.
- **ReDoS**: `isRegexLikelySafe` — закрыто ложное срабатывание на ограниченные интервалы: `(\d{1,3}\.){3}\d{1,3}` (IP-адрес) и `(a{1,3}){3}` проходят; `(a+){10,100}`, `(a{1,}){3}`, `(a*){2}` и «интервал под неограниченным квантификатором» (`(?:a{2,3})+`) блокируются. Регресс-тесты в `tests/Core/regexSafety.test.ts`.
- **Env**: смена `appConfig.env` с одного файла на другой не перечитывала файл — кэш первого пути удерживался («победа первого значения»). Кэш теперь привязан к пути: смена пути перечитывает файл. Регресс-тест в `tests/Bot/env-process.test.ts`.
- **Telegram**: `cb.from?.id as number` / `iq.from?.id as number` / `cb.message?.message_id as number` — при отсутствии полей в `userId`/`messageId` молча попадал `undefined`; теперь честный `?? null` без кастов.
- **Платформы (стабильность)**: исключения слоя БД/API из асинхронных `cardProcessing` (Алиса, Маруся, VK) и `soundProcessing` (Telegram) роняли весь ответ в 500 — серия 5xx у VK отключает вебхук, Telegram реплеит апдейт. Теперь ответ деградирует без карточки/звука, ошибка логируется (по образцу одиночной ветки Telegram Card).
- **VK**: gallery-элемент карусели без валидной кнопки уходил в template без `action`/`buttons` (не-gallery ветка такие элементы отбрасывает) — VK отклонял всю карусель. Элемент пропускается с warn, симметрично не-gallery ветке.
- **Маруся**: `session.user_id` опционален в протоколе — `undefined + ''` давал литеральную строку `"undefined"`, попадавшую в ключ БД и в `session.user_id` ответа. Теперь `?? ''`. Регресс-тесты в `tests/Platforms/Marusia/adapter.test.ts`.
- **Viber**: заголовок и описание карточки rich_media склеивались в одну строку — между ними добавлен `<br>` (как в примере IViberCard). Платформенная опция `Text: ''` больше не может обойти фильтр пустой подписи кнопки.
- **Telegram**: кнопки теперь подставляют `style` из `options.style` (`TG_STYLE_PRIMARY`/`TG_STYLE_SUCCESS`/`TG_STYLE_DANGER` — ровно значения Bot API 9.4+ `primary`/`success`/`danger`; неизвестный стиль пропускается с warn, чтобы Telegram не отклонил всё сообщение) — константы были экспортированы, но поле не проставлялось (фича существовала только в типах).
- **Кнопки (все платформы)**: молчаливое усечение списка кнопок сверх лимита (`getCorrectButtons`) теперь сопровождается warn — раньше «лишние» кнопки исчезали без объяснения; warn о невалидной кнопке в карточках Алисы/Маруси/Viber больше не теряется (appContext передаётся в обработку кнопок карточек).
- **VK (типы)**: `IVkRequestObject.peer_id` стал опциональным — поле реально присутствует только в `message_event`; обязательность ломала типизированный разбор `message_new` (в нём peer_id лежит в `object.message.peer_id`).
- **[Критично] Viber**: `buttonProcessing` обращался к `button.options.request_contact`/`request_location` без защиты — Viber был единственным адаптером без `?.`/`?? {}` (Telegram/VK/Max защищены), и кнопка, собранная вручную вне компонента `Buttons` (JS-потребители, миграции), роняла весь `getContent()` с TypeError → webhook отвечал 5xx → Viber после серии 5xx отключал вебхук. Доступ — через `?.`, платформенные опции — через `?? {}`. Регресс-тесты в `tests/Platforms/Viber/buttonOptions.test.ts`.
- **[Критично] FileAdapter**: потеря последних записей при graceful shutdown — `close()` форсил запись только при живых debounce-таймерах: если таймер уже отработал, а асинхронная запись (tmp+rename) была в полёте, `process.exit()` через 500 мс дренажа убивал процесс до её завершения. Все записи регистрируются как in-flight промисы (`#inFlightSaves`), `close()`/`destroy()` дожидаются их и сохраняют данные таблиц независимо от состояния таймеров; повторный `close()` (после сброса кэша) не перезаписывает файл пустым объектом. Плюс: `destroy()` собирает таблицы из обоих хранилищ кэша (раньше — только из `databaseInfo` адаптера). Регресс-тесты в `tests/DbModel/fileAdapterShutdown.test.ts`.
- **Безопасность (ReDoS)**: байпас эвристики `isRegexLikelySafe` — квантифицированный backreference вне тела группы (`(\w+)z\1{2,}`, `([a-z]+)x\1+`) не ловился ни одной из шести проверок: квадратичный бэктрекинг на 7000-символьном входе блокировал event loop на секунды при advisory-режиме фильтра (strictMode=false по умолчанию). Квантифицированный backreference теперь признаётся опасным независимо от соседних квантификаторов; неквантифицированный (`(\w+)-\1`) и с фиксированным интервалом (`\1{2}`) по-прежнему легитимны. Регресс-тесты в `tests/Core/regexSafety.test.ts`.
- **Viber**: `ViberRequest` — единственный API-клиент без явного таймаута — работал с дефолтом `Request` (2000 мс): при латентности chatapi.viber.com выше 2 с `send_message`/`rich_media` молча обрывались по `AbortSignal.timeout`, тогда как соседние адаптеры (VK/Telegram/MAX — 5500 мс) продолжали работать. Установлен `maxTimeQuery = 5500`. Регресс-тест в `tests/Request/ViberRequest.test.ts`.
- **Request**: сброс `send()` выполнял контракт переиспользования инстанса частично — `header`, `isConvertJson`, `isBinaryResponse` оставались от прошлого вызова: заголовок одного API (например, `Authorization` MAX) мог уйти в запрос другого, а бинарный режим SpeechKit — в JSON-запрос. Все встроенные клиенты (VK/MAX/Viber/Yandex) выставляют их перед каждым `send()` (проверено), поэтому сброс безопасен и закрывает контракт; `maxTimeQuery` намеренно не сбрасывается — это настройка клиента. Плюс: исключение fetch нормализуется (`e instanceof Error ? e : String(e)`) — строковые rejection'и больше не маскируются под Error с `undefined`-message в логах. Регресс-тесты в `tests/Request/sendReset.test.ts`.
- **FileAdapter**: детекция многопроцессного доступа — два воркера (PM2 cluster) на одной директории `json/` молча теряли записи друг друга (mtime-кэш не видит чужих изменений, полная перезапись стирает их). При первом чтении таблицы файл с mtime новее запуска адаптера (за вычетом 5-секундного grace-порога на гранулярность ФС и собственную запись при инициализации) даёт предупреждение о переходе на MongoAdapter. Регресс-тесты в `tests/DbModel/fileAdapterShutdown.test.ts`.
- **Navigation**: `selectedElement` трактовал первую цифру в любом месте текста как выбор элемента — «закажи 2 литра» на странице выбора возвращало 2-й элемент вместо поиска по схожести. Число выбирает элемент только как последнее слово текста («2», «выбери 1», «вариант 3») — контракт прежних сценариев сохранён. Регресс-тест в `tests/Navigation/navigation.test.ts`.
- **Card**: `clear()` сбрасывал только изображения — при переиспользовании контроллера (`BotTest`, `_setBotController`) заголовок, описание и `template` протекали из прошлого запроса в новый ответ. Сбрасываются `title`, `desc`, `template`. Заодно warn про обход адаптеров через `template` выводится один раз на инстанс Card (депонирован) — стабильное использование больше не замусоривает логи. Регресс-тесты в `tests/Card/card.test.ts`.
- **Viber**: у карточки rich_media с несколькими кнопками молча использовалась первая — ячейка вмещает одну кнопку, но отбрасывание остальных не сопровождалось предупреждением, и разработчик не понимал, почему «половина кнопок пропала». Добавлен warn с количеством. Регресс-тест в `tests/Platforms/Viber/card.test.ts`.
- **MongoAdapter**: ошибки не-Error типа в кастомных `_query`-колбэках логировались как `[object Object]` (двойной каст `err as string, err as Error`) — реальный текст ошибки не читался без раскопок meta. Нормализация: `instanceof Error ? message : String(err)`. Регресс-тесты в `tests/DbModel/mongoSanitize.test.ts`.
- **CLI (from-flow)**: user/pass MongoDB из flow.json молча терялись — генератор передавал в `MongoAdapter` только host/database, а креды в `.env` не мигрировали (несимметрично с create-веткой, где сценарий решён): подключение падало без объяснения. Креды переносятся в `.env` (`DB_USER`/`DB_PASSWORD`) с вычисткой CRLF/кавычек; запись токенов больше не затирает их (дополнение вместо перезаписи). Регресс-тесты в `tests/cli/flowGenerator.test.ts`.
- **CLI**: JSON-конфиг без поля `name` падал внутри CreateController с невнятным `Cannot read properties of undefined` — теперь понятная ошибка «отсутствует поле name» рядом с парсингом (как в flowGenerator для flow.json).
- **CLI (тесты)**: правила санитизации `.env` для `deploy.js` (`sanitizeEnvValue`/`quoteArg` против shell-инъекций cmd.exe: CRLF, управляющие символы, кавычки, `%`→`%%`) зафиксированы тестами: контракт-зеркало правил + дословная проверка цепочки в генерате (раньше логика квотинга не покрывалась тестами на враждебные значения). Новый сьют `tests/cli/deploySanitize.test.ts` использует собственную директорию вывода — общая с flowGenerator.test.ts директория ломала параллельный прогон (rmSync в beforeEach удалял файлы соседнего воркера).
- **Бенчмарки (стенды)**: два исправления честности compare-стендов. (1) Метки «лучший/паритет» (`markRow`) помечали победой любого единственного лидера без проверки величины отрыва — теперь победа требует отрыва, пробивающего оба порога стенда (≥5% и ≥0.3 мкс); логика вынесена в тестируемый модуль `benchmark/comparison/markRow.js` с юнит-тестами. (2) Конкурентам передавались строковые слоты как есть, хотя их `hears(строка)`/`hear(строка)` матчит только точное совпадение — обёртка `asRegExp` (экранирование в unanchored RegExp) применяется во всех пяти стендах, чтобы «частичное совпадение» измерялось одинаково у всех участников. Подробности — в BENCHMARKS.md.
- **Env**: переменная окружения `SMARTAPP_TOKEN` объявлена в `IEnvConfig` и генерируется CLI в `.env`, но `AppContext` её не читал — значение молча отбрасывалось и не попадало в `tokens.smart_app.token`. Теперь переменная подхватывается из `.env`-файла и `process.env` по общим правилам (тихий подхват не перезаписывает явно заданные токены). Регресс-тесты в `tests/Bot/env-process.test.ts` и `tests/Bot/bot.test.ts` (env init).
- **VK**: опечатка в публичном типе — поля голосовых сообщений назывались `waleform` вместо `waveform` (`IVkAudioMessageInfo`, `IVkDocSave`), так что типизированный код пользователей не компилировался с реальным ответом `docs.save`. Поля переименованы в `waveform` (см. «Миграция с 3.0.x»).
- **VK**: поле `message` в `IVkRequestObject` было обязательным, хотя события без текста (в частности, `message_event` — callback-кнопка) реально приходят без него: типизированный разбор `requestObject` на callback-запросах не компилировался. Поле стало опциональным (`message?: IVkMessage`; см. «Миграция с 3.0.x»).
- **Viber**: координаты `location.lat`/`location.lon` в публичном типе `IViberParams` были объявлены строками, хотя Viber передаёт их числами (см. `LocationMessage` официального SDK viber-bot-node) — типизированная отправка геолокации не компилировалась. Тип исправлен на `number` (см. «Миграция с 3.0.x»).
- **[Критично] NLU**: `Nlu.getLink()` обрезал каждую ссылку на первой точке — в `LINK_REGEX` точка была исключена из тела совпадения, поэтому `https://example.com/page.html` превращался в `https://example`. Шаблон исправлен: точки внутри URL сохраняются, конечная пунктуация по-прежнему отбрасывается; ReDoS-безопасность проверена. Регресс-тесты в `tests/Nlu/nlu.test.ts`.
- **[Критично] Env**: `setPlatformParams` всегда перезаписывал токены из окружения (`overwrite=true`), в том числе в тихом режиме без `appConfig.env` — токен, явно переданный в конструкторе адаптера, затирался пустым значением из `process.env`. Теперь тихий подхват не трогает явно заданные токены; явный `env` сохраняет прежнее поведение с перезаписью. Там же: кэш прочитанного `.env` не сбрасывался, из-за чего явно переданный путь к env-файлу игнорировался после тихого подхвата. Регресс-тесты в `tests/Bot/env-process.test.ts`.
- **VK**: `message_event` без `payload` возвращал `false`, и VK Callback API отключал сервер после серии 5xx. Событие подтверждается статусом 200 через `skipAutoReply`. Регресс-тест в `tests/Platforms/VK/adapter.test.ts`.
- **Telegram**: fallback на `sendPhoto` для одиночного изображения после фильтрации отправлял поле `photo` с префиксом `attach://` — префикс валиден только в FormData `sendMediaGroup`, и изображение не отправлялось. Префикс снимается. Регресс-тест в `tests/Platforms/Telegram/card.test.ts`.
- **Telegram**: при `buttons.remove()` с пустым текстом клавиатура фактически не снималась (Telegram требует текст в сообщении с `remove_keyboard`), а в лог уходило только общее предупреждение о пустом ответе. Предупреждение теперь явно указывает, что для снятия клавиатуры ответ должен содержать текст. Регресс-тест в `tests/Platforms/Telegram/adapter.test.ts`.
- **Viber**: в Rich Media карточке без кнопок текст `title`/`description` терялся — элемент отправлялся без поля `Text`. Текст заполняется соответствующими шрифтами; при отсутствии заголовка и описания пустой `Text` не добавляется. Регресс-тесты в `tests/Platforms/Viber/card.test.ts`.
- **Viber**: событие `unsubscribed` пыталось отправить автоответ пользователю, который уже отписался. Событие подтверждается через `skipAutoReply` без отправки сообщения. Регресс-тест в `tests/Platforms/serviceEvents.test.ts`.
- **SmartApp**: определение SSML давало ложное срабатывание на тексте вида «x < y» — ответ помечался `application/ssml`, и парсер Сбера ломался на обычном тексте. Шаблон распознаёт только реальные теги. Регресс-тест в `tests/Platforms/SmartApp/adapter.test.ts`.
- **Маруся**: `resizeMarusiaTts` обрезал TTS-текст по символам без учёта тегов `<speaker>`/`sil` — при лимите тег разрывался посередине и разметка ломалась. Обрезка выполняется по видимому тексту (лимит 1024 символа), теги не учитываются в лимите и сохраняются целиком — по аналогии с Алисой. Регресс-тесты в `tests/Platforms/Marusia/adapter.test.ts`.
- **Вебхук**: тело, которое разбиралось как валидный JSON, но не являлось объектом (строка, число, массив), обрабатывалось как обычный запрос и давало неинформативную ошибку — теперь возвращается `400` с сообщением «Тело запроса не является JSON-объектом». Невалидный JSON в `webhookEvent` возвращает `400` вместо `422` — статусы унифицированы под политики повторов платформ. Регресс-тесты в `tests/Bot/bot.test.ts`.
- **CLI**: условие «содержит» в flow генерировало выражение `x.includes(y)` для переменных произвольного типа — сгенерированный проект не компилировался, если переменная была числом. Выражение оборачивается в `String(x).includes(String(y))`. Регресс-тест в `tests/cli/flowGenerator.test.ts`.
- **CLI**: Step-блок с `saveAs: 'original'` сохранял `ctrl.userCommand`, который все адаптеры платформ приводят к нижнему регистру для матчинга слотов — «оригинальный» ввод искажался («Иван» → «иван») во всех ботах со Step-блоками, созданных `create from-flow`. Теперь генерируется `ctrl.originalUserCommand ?? ctrl.userCommand ?? ''` — тот же контракт, что у встроенных форм (`addForm`). Режим `saveAs: 'lowercase'` не изменился. Регресс-тест в `tests/cli/flowGenerator.test.ts`.
- **CLI**: в шаблоне quiz-проекта (`quizParams.js`) не были заданы интенты `welcome`/`help` — реплики «начать» и «помощь» попадали в fallback. Интенты с типовыми слотами добавлены в шаблон. Регресс-тест в `tests/cli/createController.test.ts`.
- **[Критично] Telegram/движок**: повторная отправка файлов по `file_id` больше не трактует токен как локальный путь; `customCommandResolver` вызывает `action()` и при результате `null` продолжает цепочку intent/fallback.
- **[Критично] CLI/Cloud**: исправлен порядок аргументов `addCommand(..., cb, isPattern)` в проектах из flow; cloud-деплой собирает отдельный архив без `.env`/исходников и передаёт переменные через поддерживаемый `yc --environment`.
- **Платформы**: усилена валидация VK-кнопок и вложений, TTS Алисы обрезается без учёта служебных тегов, Viber Rich Media требует API 7, а неподдерживаемые звуковые маркеры SmartApp удаляются из TTS.
- **CLI**: flow валидируется до генерации, зарезервированные ключи прототипа отклоняются, Mongo-проекты получают зависимость `mongodb`; Docker и GitHub Actions используют выбранный порт и Node.js 20, установка работает без lock-файла.
- **Надёжность**: загрузкам Telegram/Yandex назначены ограниченные увеличенные таймауты; `AppContext.close()` дожидается логов, циклические данные дают rejected promise, отсутствующие числовые поля Mongo не превращаются в `NaN`.
- **[Критично] Логирование**: при сбое записи файла лога `logError` входил в бесконечный цикл самовоспроизводящихся ошибок: каждая неудачная запись порождала новую запись об ошибке, которая снова уходила на запись, а текст разрастался лавинообразно (в демо — 61 МБ вывода за 8 секунд при одной исходной ошибке). Путь обработки сбоя вынесен из конвейера логирования: после 3 неудачных попыток подряд запись файлов приостанавливается на 60 секунд (записи за время паузы отбрасываются), в stderr уходит одно сообщение, после паузы запись возобновляется автоматически. Поведение `saveFileData` не изменилось — ошибки записи данных по-прежнему логируются.
- **Request**: при нечитаемом или несуществующем attach-файле всё равно выполнялся `fetch(url, undefined)` — паразитный GET-запрос к API платформы без тела и метода, затиравший локальную причину отказа (например, при передаче `file_id` в `MaxRequest.upload`). Запрос не отправляется, ошибка attach возвращается вызывающему коду. Регресс-тесты в `tests/api/request/attach.test.ts`.
- **Middleware**: rateLimiter допускал до ~2× лимита платформы за окно: задачи из очереди исполнялись с безусловным инкрементом счётчика, и параллельно «свежие» запросы проходили проверку `count < limit`. Пачка теперь уважает счётчик. Регресс-тест в `tests/Middleware/rateLimiter.test.ts`.
- **Middleware**: вытеснение записи rateLimiter при переполнении карты состояния отклоняло промисы очереди, но работающий `processQueue` продолжал исполнять их задачи; `destroyRateLimiter` — то же для уже взятой в работу пачки. Записи помечаются `dead` и больше не обслуживаются.
- **Middleware**: переполнение очереди rateLimiter теперь выбрасывает экспортируемый класс `RateLimitQueueOverflowError` и выставляет `ctx.platformOptions.rateLimitOverflow = true` — раньше исключение перехватывалось ядром как «middleware прервал обработку», и перегрузку было невозможно отличить от ошибки в бизнес-логике.
- **Middleware**: `ipFilter` поддерживает IPv6-адреса и CIDR (включая `::`-сжатие, IPv4-хвост и `::ffff:`-mapped в hex-форме). Раньше настоящий IPv6 не сравнивался ни с одним правилом: в whitelist-режиме все IPv6-клиенты блокировались (fail-closed), в blacklist-режиме — никогда не блокировались (fail-open). Правила по-прежнему сравниваются по версии адреса; для фильтрации IPv6 добавляйте IPv6-правила. Регресс-тесты в `tests/Middleware/middleware.test.ts`.
- **Env**: значения `.env` с `#` без пробела перед ним больше не обрезаются: `DB_PASSWORD=pass#word` — это значение, а не комментарий (конвенция dotenv). Инлайн-комментарий по-прежнему отсекается, когда `#` начинается с пробела. Регресс-тесты в `tests/Utils/envConfig.test.ts`.
- **NLU**: `isSayTrue`/`isSayFalse` не распознавали ответы с пунктуацией — «Да!», «Нет, спасибо» не считались подтверждением/отрицанием и ломали сценарии `isIntentConfirm`/`isIntentReject`. Границы слова задаются lookaround'ами: пунктуация после ключевого слова допускается. Регресс-тесты в `tests/Text/text.test.ts`.
- **Telegram**: url-кнопка отбрасывалась, если у кнопки задан payload больше 64 байт — лимит `callback_data` проверялся до ветки url, хотя url-кнопки не используют `callback_data`. Проверка применяется только к callback-кнопкам. Регресс-тесты в `tests/Platforms/Telegram/button.test.ts`.
- **VK**: `hash` у vkpay-кнопки отправлялся на верхнем уровне объекта кнопки, а по документации VK API он находится внутри `action`; при отсутствии hash в payload в клавиатуру уходило `hash: null`, и VK отклонял всё сообщение ошибкой 100. Регресс-тесты в `tests/Platforms/VK/button.test.ts`.
- **VK/MAX**: загрузка файлов на сервера платформ обрывалась стандартным таймаутом 5.5 с — на медленном восходящем канале карточки и звуки молча терялись. Для upload-операций таймаут поднят до 30 с (как у Telegram).
- **Платформы**: malformed-запросы без обязательных полей не вызывают TypeError и 500: `initUserCommand` защищает отсутствие `original_utterance` (Алиса/Маруся), Telegram-адаптер — отсутствие `chat` в апдейте (такие апдейты пропускаются с `skipAutoReply`). Доступ к `button.options` в адаптерах VK, MAX и Viber защищён опциональной цепочкой на случай кнопок, собранных вне компонента `Buttons`.
- **Наблюдаемость**: молчаливое усечение карточек теперь логируется — медиа-группа Telegram (>10 изображений, элементы без токена/пути) и карусель VK (более 10 элементов, сбой загрузки image_id).
- **Команды**: при превышении лимита активных групп строковый паттерн группы перекомпилировался в RegExp на каждом запросе. Компиляция кэшируется в `getGroupRegExpCompiled` (WeakMap по объекту группы с проверкой актуальности паттерна).
- **RegExp**: эвристика `isRegexLikelySafe` отбраковывала безопасные паттерны — любую квантифицированную группу `(abc)+` и любое `.*`. Проверка заменена точечным анализом квантифицированных групп: опасны группы с квантификатором, альтернативой или `.` внутри (`(a+)+`, `(a|aa)+`, `(?:\w+\.)+`); в `strictMode` больше не отключаются валидные команды. Регресс-тесты в `tests/Core/regexSafety.test.ts`.
- **CLI**: `npx umbot create 2025bot` генерировал несобираемый TypeScript (`import { 2025botController }`) — имя, начинающееся с цифры, получает префикс `_`. Регресс-тест в `tests/cli/createController.test.ts`.
- **requestId**: удалено write-only скрытое поле `appContext.__lastRequestId` — оно никем не читалось, а JSDoc обещала несуществующую интеграцию с логгером. Единственный контракт middleware — `platformOptions.requestId`.
- **Типы**: `getErrorMsg` и лог-хелперы API-клиентов принимают `Error | string` — `Request.send()` фактически возвращает объект Error в `err` (интерфейс `IRequestSend.err` уже это допускал).
- **CLI**: `fetchWithTimeout` в сгенерированных проектах использует `.unref()` у таймера прерывания; в `serverless.yml` добавлен комментарий, что деплой выполняется через `scripts/deploy.js`, который этот файл не читает.
- **Telegram**: в групповых чатах `userId` теперь берётся из `from.id` (человек), а не из `chat.id` (группа). Раньше один и тот же пользователь получал в группе две разные записи в БД: сообщения сохранялись под ID группы, а нажатия inline-кнопок (`callback_query`) — под ID человека. Ответ по-прежнему доставляется в исходный чат (он сохраняется в `requestData.chatId`). `edited_message` обрабатывается как обычное сообщение с тем же источником `userId`; NLU-данные отправителя (`thisUser`) берутся из `from`, а не из чата. На личных чатах поведение не меняется (`from.id == chat.id`).
- **[Критично] Чат-платформы**: если бизнес-логика заполнила только `ctx.tts` (типично для кода, писавшегося под голосовую платформу), Telegram, VK, Viber и MAX не отправляли **вообще ничего** — пользователь видел молчание. Теперь при пустом `text` в чат уходит `tts` с вырезанной звуковой разметкой (`<speaker>`, `sil`, `#pause_<[мс]>#`).
- **VK / Viber**: кнопка с пустой подписью отправлялась платформе (`label: ""` у ВК, `Text: ""` у Viber) — ВК отклоняет такую кнопку вместе со всей клавиатурой. Теперь она пропускается с предупреждением, как это уже делали Telegram, MAX и Алиса.
- **Telegram**: пустой список кнопок больше не приводит к отправке `remove_keyboard` — снять клавиатуру можно только явным `buttons.remove()`. Отфильтрованные невалидные кнопки клавиатуру не трогают.
- **[Критично] Платформы**: служебные события больше не приводят к ответу `500`. Раньше `setQueryData()` возвращал `false` и `run()` бросал исключение на событии `webhook` у Viber (из-за чего вебхук вообще не удавалось зарегистрировать), на событиях группы VK (`message_reply`, `group_join`, `message_allow` и др. — VK Callback API отключал сервер после серии неудач) и на апдейтах Telegram вроде `my_chat_member` (Telegram бесконечно повторял апдейт и блокировал очередь). Теперь такие события помечаются `skipAutoReply` и подтверждаются статусом 200. Добавлен `tests/Platforms/serviceEvents.test.ts`.
- **[Критично] Команды**: команда, зарегистрированная с глобальным регулярным выражением (`addCommand('x', [/привет/g], cb, true)`), срабатывала через раз — `test()` продвигал `lastIndex` у кэшированного между запросами объекта. Флаги `g`/`y` снимаются при компиляции (`getRegExp`, `getRegExpOrSelf`), а `Text` дополнительно сбрасывает `lastIndex` перед проверкой.
- **[Критично] Текст**: `Text.resize()` разрывал суррогатные пары — обрезка по границе эмодзи оставляла одинокий суррогат, невалидный в UTF-8, и платформы отбивали такое сообщение. Обрезка выполняется по границе символа.
- **[Критично] HTTP**: кастомный заголовок (`Authorization` у MAX, `X-Viber-Auth-Token` у Viber) полностью затирал `Content-Type: application/json`, и JSON-тело уходило без Content-Type. Заголовки теперь объединяются; для `FormData` Content-Type по-прежнему выставляет сам `fetch`.
- **[Критично] SmartApp**: ответ помечался `pronounceTextType: 'application/ssml'` даже для обычного текста (ядро копирует `text` в `tts` для голосовых платформ) — любой `&`, `<` или `>` в пользовательских данных ломал SSML-парсер Сбера. Тип разметки определяется по фактическому наличию тегов. Там же: `intent` больше не уходит как `null`, а запрос без `payload`/`uuid` отклоняется понятной ошибкой вместо `TypeError`.
- **Алиса/Маруся**: если для элемента карточки была задана картинка, но получить `image_id` не удалось, элемент по-прежнему показывается (карточка с одним текстом — рабочий сценарий), но в лог пишется предупреждение с путём до картинки — раньше она пропадала молча.
- **[Критично] MAX**: при пустой клавиатуре в сообщение добавлялось вложение `inline_keyboard` с `buttons: []`, которое API отклоняет; кнопка без текста уходила как `{"type":"message","text":null}`. Оба случая отсекаются.
- **VK**: `messagesSend` при одновременно заданных `keyboard` и `template` отправлял в теле запроса строку `template=undefined` (VK отвечает ошибкой 100) — поле теперь удаляется. `httpBuildQuery` дополнительно пропускает `undefined`/`null`, чтобы такие значения не сериализовались как строки.
- **Telegram**: `TelegramRequest.initToken()` не влиял на адрес запроса — URL всегда собирался из `appConfig`, что ломало мульти-ботовые сценарии. Теперь приоритет у явно заданного токена.
- **Telegram**: при одновременно заданных inline- и обычных кнопках последние молча пропадали (платформа принимает только один тип клавиатуры в сообщении). Добавлено предупреждение в лог с объяснением, что делать.
- **[Критично] BotTest**: `clearStoreData()` сбрасывал `skipAutoReply`, поэтому со второго хода консольного тестирования адаптеры уходили в реальные API платформ. Флаг переустанавливается на каждой итерации.
- **BotController**: `clearStoreData()` теперь очищает `platformOptions` и кэш разобранных сущностей `Nlu`. Раньше технические данные запроса (`requestData`, `sendInInit`, `session`, `error`) и сущности NLU протекали в следующий запрос — например, VK-подтверждение возвращалось в ответ на любое сообщение.
- **Ядро**: `#getPlatformContent` не дожидался ответа адаптера перед валидацией, из-за чего проверка на `null` применялась к промису и никогда не срабатывала для асинхронных адаптеров (все, кроме SmartApp), а локальное хранилище записывалось параллельно формированию ответа.
- **Ядро**: `setLocalStorage()` больше не вызывается с пустым состоянием — у SmartApp это уходило вторым лишним HTTP-запросом (причём методом GET) на каждый ответ.
- **Ядро**: `bot.close()` дожидается закрытия HTTP-сервера, как и обещано в документации метода.
- **HTTP**: в текст ошибки попадает тело ответа платформы (`description` у Telegram, `error_msg` у VK) — раньше в логах оставался только код статуса. `Request.send()` также сбрасывает `get` и `customRequest`, чтобы параметры одного вызова не утекали в следующий у переиспользуемого инстанса.
- **VK/MAX/Telegram**: приведены к общему виду поля контроллера — `originalUserCommand` заполняется для callback-кнопок VK и для сообщений Telegram без текста, `username` в NLU у MAX приходит как `null`, а не `undefined`.
- **[Критично] Ядро**: `Bot` читал сохранённые данные пользователя по `userToken`, а записывал по `userId` — у авторизованных пользователей (например, Алиса с `isAuthUser`) `userData` не находилась между запросами и фактически не персистилась. Чтение унифицировано на `userId` (ключ записи). Добавлен регресс-тест в `tests/Bot/bot.test.ts`.
- **[Критично] VK**: `users.get` всегда возвращает массив, но адаптер читал `first_name`/`last_name` как у объекта — имя пользователя VK в `ctx.nlu.getUserName()` всегда было `null`. `VkRequest.usersGet` теперь типизирован как `IVkUsersGet[] | null` (по аналогии с `photosSaveMessagesPhoto`), адаптер берёт первый элемент массива; числовой ID отправляется документированным параметром `user_ids` (ранее — недокументированный `user_id`). Добавлены регресс-тесты в `tests/Platforms/VK/adapter.test.ts` и `tests/Request/VkRequest.test.ts`.
- **[Критично] Маруся**: `MarusiaRequest` наследовал токен VK от родителя и при отсутствии токена Маруси отправлял его в запросы к API Маруси (утечка секрета на чужой endpoint + нерабочие загрузки). Теперь при отсутствии `tokens.marusia.token` токен сбрасывается в `null`. Добавлен регресс-тест в `tests/Request/MarusiaRequest.test.ts`.
- **Документация**: полный копирайт/SEO/ИИ-аудит публикуемой документации (гайды, README, JSDoc) с применением правок:
    - Терминология унифицирована: «Сбер Салют»/«Салют»/«Сбер Salute» → «Сбер SmartApp» (README, GUIDE, deployment, JSDoc `index.ts`/`plugins.ts`/`Preload.ts`/адаптеров); «хендлер» → «обработчик» (гайды + JSDoc ядра, включая текст предупреждения `bot.addEvent`); «MAX (ВК)»/«MAX (мессенджер VK)» → «MAX».
    - Таблицы `supportedEvents` в api-reference.md и platform-integration.md приведены к коду: Маруся поддерживает `auth` (событие привязки аккаунта, симметрично Алисе) — раньше в доках был только `message`.
    - H1 гайдов переформулированы под поисковую выдачу: getting-started (96 симв. → «Быстрый старт: первый навык или чат-бот на umbot»), «API Reference» → «Справочник API umbot», «Часто задаваемые вопросы» → «FAQ по фреймворку umbot» (+ вводный абзац для meta description), «JSON-формат конфигурации umbot» → «Формат flow.json визуального редактора umbot».
    - Числовые расхождения сведены к одному канону: таймауты в README теперь описаны через реальные пороги фреймворка (2000/2900 мс, ориентир ~3 с) вместо невыводимой «более 2.5 секунд»; «первый ответ > 1 сек / 3 сек» в чеклистах заменён на измеренные «200–1000 мс на файл»; `Preload.ts` — «2–3 секунды» → «~3 секунды» с порогами. Из README убрано непроверяемое «80%+ покрытие тестами».
    - Пример `photosSaveMessagesPhoto` в JSDoc `VkRequest` исправлен на массив (`photo[0].owner_id`/`photo[0].id`) — сигнатура возвращает `IVkPhotosSave[]`; в `IVkApi` задокументирована семантика конверта `{response: ...}`.
    - Все 16 относительных `.md`-ссылок между гайдами переписаны в абсолютные URL сайта (инвариант пайплайна: относительные ссылки битые после публикации).
    - JSDoc-заголовки namespace-страниц (`export * as X` в `plugins/index.ts` + `SoundConstants`): 26 страниц API-справочника получили реальные первые предложения вместо шаблонного «Документация по X в фреймворке umbot» (meta description сайта); первые строки `voicePlatforms`/`botPlatforms` разведены (устранён тройной дубль description).
    - Мелкие правки: «долгой ответ» → «долгий» (GUIDE), «У Алисы/Марусии» → «Маруси», «и тд» → «и т.д.» и пропущенные запятые (build.ts), H1 GUIDE без бэктиков, TG_STYLE-опции кнопок Telegram задокументированы в GUIDE (Bot API 9.4+, CHANGELOG-фича ранее не попадала в гайды).
- **Документация**: `action()` вызывается фреймворком синхронно и не ожидается — примеры с `async action()` в JSDoc `BotController`, `UsersData` и `GUIDE.md` переписаны (асинхронная логика показана через `addCommand`/fire-and-forget), в JSDoc `action()` добавлено явное предупреждение.
- **Документация**: скрипт проверки и актуализации ссылок (`scripts/fix-doc.js`, `npm run doc:fix`) помечал корректные markdown-ссылки с якорем (вида `./GUIDE.md#раздел`) как битые — фрагмент `#...` не отсекался перед проверкой существования файла. Дополнительно якорь больше не теряется при переписывании ссылки на URL. Добавлены регресс-тесты в `tests/scripts/fix-doc.test.ts`.
- **[Критично] Ядро**: устранена циклическая зависимость `core ↔ controller`, из-за которой при стандартном импорте `from 'umbot'` константы `FALLBACK_COMMAND`, `WELCOME_INTENT_NAME`, `HELP_INTENT_NAME` захватывались как `undefined`. В результате fallback-команда `'*'`, `welcome_text` и `help_text` молча не работали. Константы вынесены в листовой модуль `core/constants.ts` без импортов, который всегда полностью инициализирован независимо от порядка загрузки. Добавлен регресс-тест `tests/Bot/fallbackConstants.test.ts`.
- **Middleware**: `requestId()` теперь корректно возвращает промис цепочки (`await next()`) — ранее middleware обрывал ожидание, из-за чего запрос мог обрабатываться параллельно с ответом, а ошибки в последующих middleware приводили к unhandled promise rejection.
- **Middleware**: при блокировке запроса middleware (`authGuard`, `maintenance`, `ipFilter`) ответ формируется через адаптер платформы из `ctx.text` — пользователь получает `deniedText` валидным ответом платформы вместо служебной строки.
- **Middleware**: `ipFilter` получает реальный IP клиента из сокета HTTP-запроса (`platformOptions.clientIp`, заполняется в `webhookHandle`) — ранее IP искался в теле запроса платформы и фильтрация не работала.
- **Env**: `ALISA_TOKEN` и `VK_SECRET_KEY` теперь реально читаются из окружения в `AppContext` (ранее были только объявлены в `IEnvConfig`).
- **CLI**: токен Алисы (`alisa_token`) больше не попадает в сгенерированный `src/config/{{name}}Params.ts` — удаляется вместе с остальными токенами перед сериализацией.
- **CLI**: `validate` проверяет целостность рёбер `edges` (существование `from`/`to`, допустимый `type`), по которым реально ходит генератор; циклы в графе допустимы.
- **CLI**: при генерации с `--prod` теперь создаётся `.dockerignore` (исключает `.env`, `node_modules`, логи из образа), а `Dockerfile` использует `npm ci` при наличии lock-файла — версии зависимостей совпадают между build и runtime стадиями.
- **CLI**: сгенерированный `deploy.yml` больше не передаёт секреты флагами `-e` (они видны в `ps` и истории docker) — используется `--env-file`; имя образа и контейнера подставляется из имени проекта; добавлены `npm ci`, шаг с тестами и `--restart unless-stopped`.
- **CLI**: генерация `.env` больше не пишет строки вида `TELEGRAM_TOKEN=undefined` — в файл попадают только переменные, для которых реально передано значение.
- **CLI**: `flowGenerator` для `database.type === 'mongo'` учитывает `config.host` из flow.json (ранее всегда подставлялся `localhost`); `bot.start()` берёт опциональные `hostname`/`port` из документа вместо захардкоженных `localhost:3000`.
- **CLI**: в шаблоне `package.json` команда `deploy` использует `pm2 startOrRestart` вместо `pm2 restart` — корректно работает и при первом запуске процесса.
- **CLI**: из шаблона `indexDevMin` убран неиспользуемый импорт `Bot`.
- **[Критично] CLI**: сгенерированный обработчик Yandex Cloud Function вызывал `bot.run()` напрямую, минуя проверку подлинности вебхука (`isCorrectQuery`) — деплой Telegram/VK с секретом вебхука принимал поддельные запросы на публичном эндпоинте. Обработчик переведён на `bot.webhookEvent()`, который передаёт заголовки запроса и отклоняет неверную подпись со статусом 401. Добавлены регресс-тесты в `tests/Bot/bot.test.ts` и `tests/cli/flowGenerator.test.ts`.
- **CLI**: `collectVarNames` теперь собирает имена переменных из `saveResponseTo` (результат HTTP-запроса) — ранее при использовании такой переменной в выражении `set_variable` генератор выпускал строковый литерал вместо `Number(ctrl.userData.<var>)`. Добавлен регресс-тест.
- **CLI**: `validate` теперь отклоняет циклы, состоящие только из исполняемых блоков (action/condition/response): они генерируются как рекурсивные вызовы и при выполнении переполняют стек. Циклы, проходящие через command/step, по-прежнему допустимы (навигация идёт через `thisIntentName`). Добавлены регресс-тесты.
- **Utils**: fallback для длинных строк в `similarText` возвращал 100% схожести при равных длинах и совпадении первых 50 символов, игнорируя полностью отличающиеся хвосты. Префиксная проверка заменена на сравнение нескольких окон, распределённых по всей длине строки. Добавлены регресс-тесты.
- **Middleware**: `destroyRateLimiter()` очищал только последний созданный инстанс `rateLimiter()` (модульные ссылки перезаписывались каждым новым вызовом) — интервалы и карты состояний более ранних инстансов утекали. Теперь все инстансы регистрируются в реестре, и destroy освобождает ресурсы каждого. Добавлен регресс-тест.
- **Документация**: `next-release.md` переименован в `migration-2x-to-3x.md` (содержимое — гайд миграции 2.x→3.0, старое название вводило в заблуждение); ссылки в README/FAQ/CHANGELOG обновлены.
- **Документация**: актуализированы `api-reference.md` и `GUIDE.md` — добавлены `addForm`/`removeForm`, `BotTest.simulate()`, исправлен пример кастомной `Model` (публичные абстрактные методы, допустимые типы правил), возвращаемые типы `Preload.loadImages/loadSounds`, версия фреймворка.
- **BotController**: асинхронная ошибка в callback шага теперь тоже выставляет безопасный fallback-текст, если обработчик не успел задать свой.
- **AppContext**: маскировка метаданных в `logError`/`logWarn` защищена try/catch — кидающий геттер в `meta` больше не ломает логирование и обработку запроса.
- **RateLimiter**: LRU-вытеснение при переполнении пропускает записи, которые прямо сейчас обрабатывают очередь, — больше не создаются две параллельные очереди на пользователя.
- **[Критично] Производительность/Логирование**: из асинхронной `saveData` убрана валидация через `JSON.parse`. Она никогда не блокировала запись (в отличие от `saveDataSync`), но для строк лога в режиме дозаписи (начинаются с `[timestamp]`) парс гарантированно падал и через `errorLogger` запускал бесконечный цикл самовоспроизводящихся ошибок с ростом `error.log` каждые ~200 мс. Для `saveFileData` тот же парс выполнял лишнюю полную десериализацию всей таблицы при каждом сохранении `FileAdapter`.
- **Производительность/Viber**: `ViberRequest.call()` сериализует тело запроса один раз и переиспользует строку как тело (`postInString`) — ранее `JSON.stringify` выполнялся дважды (для проверки размера 30 КБ и внутри `Request._getOptions`).
- **Производительность/Telegram**: `getContent` больше не инстанцирует `Card` (и вложенный `Buttons`) на каждый запрос — добавлена проверка `isCardInit()`, как в остальных адаптерах.
- **Viber**: добавлена обработка всех типов событий webhook: `subscribed`, `unsubscribed`, `delivered`, `seen`, `failed` — ранее они вызывали ошибку, так как `setQueryData` возвращал `false`. Теперь `subscribed`/`unsubscribed` логируются, а `delivered`/`seen`/`failed` корректно подтверждаются без ошибок.
- **Viber**: исправлена детекция платформы — `isPlatformOnQuery` больше не требует `message_token`, так как события `conversation_started`, `subscribed`, `unsubscribed` его не содержат.
- **Viber**: для не-текстовых сообщений (picture, video, file, location, contact, sticker) теперь сохраняется тип сообщения (`viberMessageType`) и дополнительные данные (`media`, `location`, `contact`, `sticker_id`) в `controller.payload`.
- **Viber**: `getQueryExample` теперь включает поле `timestamp`, чтобы пример проходил `isPlatformOnQuery`.
- **Viber**: `IViberContent.message_token` и `sender` сделаны опциональными в интерфейсе — для событий без этих полей.
- **Viber**: Rich Media карточки без кнопок получают нейтральный `ActionType: none` и больше не отправляют придуманный фреймворком текст от имени пользователя.
- **Viber**: исправлено обращение к `query.user.name` вместо `query.sender.name` в событии `conversation_started`.
- **Max**: базовый URL API обновлён с `platform-api.max.ru` на `platform-api2.max.ru` в соответствии с миграцией платформы (июнь 2026).
- **Max**: в `IMaxButton.type` добавлен тип `clipboard` — кнопка, копирующая текст из `payload` в буфер обмена пользователя.
- **Max**: исправлено значение `update_type` с `message_editing` на `message_edited` для соответствия документации платформы.
- **Max**: добавлена опциональная проверка подписи webhook через заголовок `X-Max-Bot-Api-Secret`. Включается передачей `secret` в `additionalPlatformOptions` конструктора адаптера.
- **Max**: при `skipAutoReply=true` больше не выполняются лишние HTTP-вызовы загрузки изображений и аудио (buttonProcessing, cardProcessing, soundProcessing).
- **Max**: добавлена поддержка групповых чатов и каналов — при наличии `chat_id` в webhook ответ отправляется в чат, а не в личный диалог.
- **AppContext**: кастомный `logger.error`/`logger.warn` теперь маскирует секреты в сообщении и вложенных `meta` по умолчанию; `maskSecrets: false` по-прежнему оставляет явный opt-out для отладки.
- **BotController**: синхронные исключения в callback шага теперь логируются и переводят диалог в безопасный ответ вместо падения обработки.
- **MongoAdapter**: при переподключении закрывается предыдущий `mongoClient`, а не новый клиент перед `connect()`.
- **Telegram**: multi-image карточки ограничены лимитом `sendMediaGroup` в 10 элементов; одиночное изображение после фильтрации отправляется через `sendPhoto` без повторного вызова `sendMediaGroup`.
- **CLI**: generated HTTP-запросы теперь используют `fetchWithTimeout`, корректно обрабатывают `catch (e: unknown)` и безопасно сериализуют JSON body с переменными `{{var}}`.
- **CLI**: `serverless.yml` больше не получает реальные значения токенов из `flow.json`, а использует ссылки на переменные окружения.
- **CLI**: `create from-flow` больше не перезаписывает непустую директорию без `--force`.
- **CLI Docker**: builder-stage устанавливает devDependencies, поэтому TypeScript-компилятор доступен при `npm run build`.
- **Bot**: исправлена потенциальная потеря данных: `#saveUserData` теперь обёрнут в `try/finally`, гарантируя сохранение даже при исключениях в бизнес-логике.
- **Bot**: ошибка подключения к БД теперь корректно возвращает `undefined` вместо нерабочего экземпляра адаптера.
- **VK**: исправлена обработка `message_event`: адаптер теперь использует `messages.sendMessageEventAnswer` с обязательными `event_id`, `user_id` и `peer_id`, а также валидирует размер `event_data`.
- **VK**: преобразователь кнопок больше не мутирует исходные универсальные кнопки и не переносит в VK посторонние поля из `options`.
- **VK**: payload кнопки проверяется после сериализации; значение больше 255 символов не обрезается до повреждённого JSON — кнопка пропускается с предупреждением.
- **VK**: исправлен поиск hash для кнопок `vkpay` при передаче строкового payload.
- **Telegram**: добавлена проверка длины `callback_data` (лимит 64 байта). При превышении кнопка пропускается с предупреждением, а идентификатор разработчика не изменяется.
- **Telegram**: исправлена обработка multi-image: при фильтрации и остатке менее 2 элементов изображение корректно отправляется через `sendPhoto`.
- **Viber**: исправлен лимит `Columns` в карточках с 7 на 6 (в соответствии с Viber Rich Media API).
- **Viber**: `ActionBody` в кнопках теперь использует `button.payload` (если он есть), а не всегда `button.title`.
- **Viber**: текст ограничивается документированными 7000 символами, запросы больше 30 КБ не отправляются, а обязательные поля текстового сообщения нельзя случайно перезаписать через `params`.
- **Viber**: универсальные опции кнопок фильтруются и сопоставляются с нативными действиями; обязательные поля webhook/Rich Media защищены от перезаписи, а видимое имя sender больше не придумывается фреймворком.
- **Viber**: событие `conversation_started` теперь корректно обрабатывается (ранее блокировалось условием `if (query.message)`).
- **Маруся**: добавлена строгая валидация размера payload (лимит 4096 байт) и state (лимит 3584 байта).
- **AppContext**: `JSON.stringify` в методах `logError`/`logWarn` обёрнут в `try/catch` для защиты от циклических ссылок в метаданных.
- **AppContext**: `setAppConfig` теперь выполняет глубокое слияние (deep merge) для поля `db`.
- **AppContext**: `setPlatformParams` больше не мутирует исходные интенты пользователя.
- **Max Card / Telegram Card**: добавлена проверка границ (bounds) для `images[0]` при `showOne=true`.
- **Max Sound**: исправлена потеря данных: при отсутствии `speech_kit_token` теперь возвращаются уже обработанные звуки вместо `null`.
- **SmartApp Card**: `buttonProcessing` теперь возвращает `null` вместо `[]`, если `isCard=true`, но валидной кнопки не найдено.
- **CLI**: исправлен краш при выполнении `add docker` / `add deploy` (проблема инициализации `#name` вне `createProject()`).
- **CLI**: исправлена генерация невалидного кода при пустых условиях в шаблонах.
- **CLI**: `tsconfig.json` в flow-генераторе обновлён с устаревшего `moduleResolution: 'node'` на `Node16`.
- **Utils**: `httpBuildQuery` теперь использует `encodeURIComponent` вместо `encodeURI` для корректного кодирования спецсимволов (`&`, `=`, `#`) в значениях параметров.
- **Utils**: `fwriteSync`/`fwrite` теперь гарантированно удаляют временные файлы при ошибке переименования.
- **Utils**: удаление временных файлов в модулях VK Sound и Telegram Sound обёрнуто в `try/catch`.
- **EnvConfig**: добавлена корректная обработка inline-комментариев (`#`) в `.env` файлах.
- **RateLimiter**: `destroyRateLimiter` теперь очищает очередь (drain) перед полным удалением.
- **UsersData**: исправлено несоответствие ключей между `RULES` и `ATTRS_LABEL` (`platformName` заменён на `platform`).
- **CLI**: `.gitignore` в шаблоне проекта всегда оказывался пустым, из-за чего `.env` с токенами коммитился в Git. Теперь читается из правильного пути `cli/template/.gitignore`.
- **HTTP**: `Request` по умолчанию не имел таймаута — вешал бота при зависании API платформы. Установлен безопасный default `2000 мс` (переопределяется через свойство `request.maxTimeQuery`).
- **CLI**: при формате `tokens` в `flow.json` в стиле `{telegram: {token: "..."}}` (как в README) процесс генерации падал с `TypeError`. Поддержаны оба формата — плоский и вложенный.
- **CLI**: текстовые шаблоны вида `{{user.name}}` не разворачивались (regex `\w+` не захватывал точку). Теперь поддержаны.
- **CLI**: `execFileSync(prettier)` без таймаута мог навсегда зависнуть при сломанном prettier. Добавлен `timeout: 30_000`.
- **CLI**: `CreateController.init()` ломался на Windows при путях с обратным слешом. Поддержаны оба разделителя.
- **Navigation**: `selectedElement('12')` возвращал первый элемент вместо двенадцатого (`+data[0][0]` вместо `+data[0]`).
- **Telegram**: webhook-проверка подписи использовала HMAC от тела, но Telegram шлёт `x-telegram-bot-api-secret-token` как plain string. Все webhook-запросы отклонялись. Теперь — прямое `timingSafeEqual` с опциональным `appConfig.tokens.telegram.webhookSecret`.
- **Telegram**: исправлен тип `photo` в `ITelegramResultContent` — заменён с одиночного `ITelegramPhoto` на массив `ITelegramPhoto[]` в соответствии с Telegram Bot API (`sendPhoto` возвращает `PhotoSize[]`). `file_id` теперь извлекается из последнего элемента массива (самое большое разрешение).
- **Telegram**: `ITelegramResult.result` сделан опциональным (`result?: ITelegramResultContent | null`) — при `ok: false` результат отсутствует.
- **Telegram**: добавлена обработка `edited_message` и `edited_channel_post` в `setQueryData()` (ранее определялись в `isPlatformOnQuery`, но не обрабатывались).
- **Telegram**: для reply-клавиатуры добавлен `resize_keyboard: true` для автоматического подгона размера под экран.
- **Telegram**: типизированы поля `edited_message` и `edited_channel_post` в `ITelegramContent` — заменены с `Record<string, unknown>` на `ITelegramMessage`.
- **Telegram**: в `ITelegramKeyboard` добавлены поля `resize_keyboard`, `one_time_keyboard`, `input_field_placeholder`.
- **Telegram**: добавлены юнит-тесты для `TelegramAdapter`, `Button.ts`, `Card.ts` и `Sound.ts`.
- **Telegram**: callback подтверждается отдельно от сообщения в чат: `controller.text` больше не дублируется как toast. Явный `platformOptions.callbackNotificationText` и прямой `answerCallbackQuery.text` ограничиваются 200 символами.
- **Viber**: одиночная карточка (одно изображение) молча не отправлялась — `cardProcessing` возвращал объект, а не массив.
- **Marusia**: `marusiaCreateAudio` отправлял `audio_meta` как `[object Object]` вместо JSON-строки — загрузка звуков не работала.
- **MongoDB**: повторный вызов `connect()` переиспользовал "закрытый" `MongoClient`, вызывая ошибку `MongoClient is closed`. Каждая попытка теперь создаёт новый клиент.
- **MongoDB**: при `update` поля со значением `undefined` сохранялись в BSON и затирали существующие поля. Теперь фильтруются перед `$set`.
- **Models**: `ImageTokens.selectOne()` и `SoundTokens.selectOne()` искали по первичному ключу (который `null` на новой модели) вместо задокументированного паттерна поиска по `path`. Кэш токенов платформ не работал.
- **CommandReg**: `removeCommand` удалял команду до обновления regexp-группы, из-за чего команда оставалась внутри группы и создавалась "пустая" запись с ключом `undefined`.
- **Bot**: `gracefulShutdown` падал при ошибке `close()`, не вызывая `process.exit(0)`. Добавлен `try/catch`.
- **Bot**: при невалидном webhook токене в лог-файл улетал весь `IncomingMessage` (IP, headers, sockets). Теперь только metadata.
- **AppContext**: `#errWarnLog` при непрерывном потоке ошибок рос без ограничений. Добавлен cap в 1000 записей с немедленным flush.
- **AppContext**: `setAppConfig({db: ...})` затирал вложенный `db.options`. Теперь выполняется глубокое слияние.
- **RateLimiter**: при переполнении `stateMap` новые пользователи проходили без ограничений (fail-open). Заменено на LRU eviction.
- **Utils**: `fwriteSync`/`fwrite` при параллельной записи одного файла конфликтовали на одном `.tmp` имени. Добавлен уникальный суффикс `{pid}.{timestamp}.{rand}.tmp`.
- **Utils**: `similarText` при длинных строках (>2000 суммарно) блокировал event loop из-за O(n·m). Добавлен быстрый fallback по префиксу/длине.
- **MAX**: лимит текста был 4096, но API отклоняет >4000. Исправлено.
- **VK**: `random_id` выходил за диапазон int32 (`Date.now()*1000`). Ограничен до `2^31-1`.
- **VK**: при отправке по `screen_name` в тело запроса уходил `peer_id=undefined`. Поле удаляется вместо присваивания.
- **VK**: `signatureName = 'x-vk-signature'` ломал webhook при заданном token — у VK другая модель безопасности (через `secret_key` в теле запроса), HTTP-подписи нет. Теперь `signatureName = undefined`, чтобы базовая HMAC-проверка не блокировала webhook.
- **VK**: добавлена опциональная проверка `secret` из тела запроса VK Callback API. Если задан `vk_secret_key` в конструкторе адаптера или переменная `VK_SECRET_KEY` в `.env`, адаптер сверяет поле `secret` каждого запроса с сохранённым значением. Не влияет на `confirmation` событие.
- **VK**: `getContent()` для `message_event` теперь при ошибке бизнес-логики отправляет `sendMessageEvent` с `show_snackbar` и текстом ошибки, а не пустое подтверждение.
- **Viber**: действие `message` без `sender` больше не приводит к `TypeError: Cannot read properties of undefined` — адаптер корректно отклоняет malformed-запрос через `platformOptions.error`.
- **AppContext**: маскировка метаданных логгера больше не помечает повторяющиеся (не циклические) объекты как `[Circular]` — маркер срабатывает только при реальной циклической ссылке.
- **AppContext**: overflow-flush `#errWarnLog` сбрасывает только переполненную очередь (`errors` или `warnings`), не затирая вторую и не прерывая debounce.
- **Платформы**: устранены несовместимости с актуальными API: MAX использует raw `X-Max-Bot-Api-Secret`, `user_id`/`chat_id` в query, двухшаговую загрузку файлов, матрицу inline-клавиатуры и `answers` для callback; VK отвечает в `peer_id` беседы; Viber не отправляет пустые сообщения на служебные события; Алиса ограничивает state 1 КБ; Telegram не включает HTML-разметку без явной настройки и отвечает на inline-запросы через `answerInlineQuery`.
- **Платформы**: Telegram направляет ответ на callback-кнопку в исходный чат, VK сериализует кнопки карусели плоским массивом, SmartApp использует массив `actions` и документированный формат `server_action`, а Viber не вызывает `send_message` с пустым текстом перед Rich Media.
- **Платформы**: устранены дополнительные несовместимости форматов: MAX соблюдает лимит 2 сообщения и 2 callback-ответа/с на диалог, ограничивает клавиатуру 30 рядами, отправляет изображения отдельными attachments и принимает служебные webhook-события; Viber передаёт обязательный sender и реальные метаданные файлов; SmartApp формирует актуальный `left_right_cell_view`; Алиса ограничивает `ImageGallery` десятью элементами и `BigImage.description` 1024 символами.
- **Платформы**: адаптеры больше не подставляют пользовательские реплики «Ответ готов», «Ответ бота», «Смотрите на экране», «Выберите действие», «Изображение» и «Открыть». Пустой ответ Алисы/Маруси сохраняется без подстановки и исключения с диагностическим предупреждением; Telegram/MAX не отправляют недопустимое пустое сообщение.
- **Алиса**: `start_account_linking` перенесён из недопустимого верхнего уровня в `response.directives`; ответ авторизации теперь содержит обычный `response` согласно протоколу.
- **MAX**: пустой callback корректно подтверждается через `POST /answers` без придуманного сообщения; webhook URL с нестандартным портом отклоняется до HTTP-запроса.
- **Card**: `addImage()` и `addOneImage()` больше не подставляют пробел в отсутствующие `title`/`description`; используются честные пустые строки.
- **Кнопки платформ**: циклические и иные несериализуемые payload больше не обрушивают обработку запроса; невалидная кнопка пропускается без подмены данных.
- **CLI**: генератор больше не вставляет произвольные значения flow в TypeScript: для `set_variable` доступна ограниченная арифметика, HTTP-методы и границы случайного числа валидируются, а пустой успешный HTTP-ответ корректно сохраняется как `null`.
- **CLI**: `hostname` и `port` из пользовательской конфигурации безопасно сериализуются перед вставкой в шаблон; CLI на JavaScript теперь проходит ESLint.
- **CLI**: зависимости генерируемых проектов зафиксированы, а `package-lock.json` больше не исключается из Git.
- **CLI**: версия `umbot` в flow-генераторе синхронизирована с production-шаблоном `3.1.0`.
- **CLI**: flow-генератор теперь нормализует имена блоков в безопасные TypeScript-идентификаторы, сохраняет HTTP-метод у запросов без body и корректно обрабатывает проверки `isSay*` по текущему вводу.
- **CLI**: Cloud Functions-проект больше не запускает локальный HTTP-сервер, использует compiled entrypoint `dist/index.handler` и актуальный runtime Node.js 22.
- **CLI**: шаблоны Docker и GitHub Actions больше не требуют отсутствующий `package-lock.json`; опасные перезаписи `.env`, Dockerfile и workflow требуют явный `--force`.
- **CLI**: `npx umbot create` больше не показывает предупреждение «не удалось отформатировать код» каждому пользователю: prettier не входит в зависимости публикуемого пакета, поэтому раньше вызов всегда завершался ошибкой. Теперь форматтер ищется как модуль (доступен в репозитории фреймворка или если prettier установлен в проекте пользователя), при отсутствии молча пропускается, а запуск идёт через `node` — заодно это чинит Windows, где Node ≥ 20.12 запрещает запуск `.cmd` без shell.
- **CLI**: `create from-flow` принимает flow.json в любом месте аргументов — флаги `--output`/`--usecloud` можно ставить как до, так и после пути к файлу (раньше путь обязан был идти сразу после `from-flow`).
- **CommandReg**: регистрация более 300 pattern-команд теперь поддерживает произвольные имена команд, включая дефис.
- **VK**: токен подтверждения вебхука (событие `confirmation`) читался только из опции конструктора `vk_confirmation_token` — настройка через `.env`/`process.env` (`VK_CONFIRMATION_TOKEN` → `tokens.vk.confirmation_token`) не работала, что особенно заметно при `fullPlatforms`, где опции адаптеру не передать. Добавлен запасной вариант из конфигурации. Регресс-тесты в `tests/Platforms/VK/adapter.test.ts`.
- **CLI**: сгенерированный `DefaultController` имел пустой `action()` — бот отвечал пустым сообщением на любую реплику. Шаблон приветствует на `welcome`, показывает справку на `help` и повторяет команду пользователя в остальных случаях; `defaultParams.js` заполняет `welcome_text`/`help_text` и явно задаёт интенты welcome/help.
- **CLI**: в шаблонах quiz-проекта кейс `welcome` не задавал текст — Алиса получала невалидный пустой ответ на первой реплике. Приветствие добавлено в `QuizController` и `quizParams.js`.
- **Dockerfile**: builder-стадия корневого Dockerfile выполняла `npm ci --only=production`, из-за чего `tsc` отсутствовал (TypeScript — опциональная peer-зависимость) и `npm run build` падал. Builder ставит полный набор зависимостей (`npm ci`), runtime-стадия — только production (`npm ci --omit=dev`).
- **Examples**: общий конфиг `examples/config/skillDefaultParam.ts` заменял встроенные интенты массивом без `help` — команда «помощь» попадала в fallback во всех примерах, где контроллер обрабатывал help-кейс. Welcome/help-интенты добавлены в массив.
- **Бенчмарки (um.js)**: исправлена методология и вердикты `benchmark/um.js` (npm run comparison): «Warm» не измеряется первым проходом после `forceGC()` (полный GC сбивает inline-кэши V8 — замер получался полухолодным и завышал отставание до 2×); прогоны clean/umbot чередуются (lockstep), память разделена на retain-утечки и транзиентный мусор (в вердикте участвует только retain). Вердикт переписан: время — главный показатель, память в пределах порога шума (±1 КБ) не влияет. Результаты до исправления методологии завышали отставание umbot в 2–5 раз. Подробности — в BENCHMARKS.md.
- **Бенчмарки (регресс, исправлен)**: при подключении честной проверки доступной памяти (запись выше про `bench`/`stress`) она попала в per-request hot path `stress-test.js` — `getAvailableMemoryMB()` вызывалась на каждый запрос (~170 мкс, RPS падал с десятков тысяч до ~7k). Полная проверка выполняется один раз до цикла, внутри итераций — только дешёвый порог `heapUsed > 3700`. Уровень восстановлен (~55k RPS последовательного режима).
- **Документация**: `src/docs/BENCHMARKS.md` — добавлена секция comparison с актуальными числами (итог −59% времени по сумме сценариев; umbot быстрее «чистой» реализации на 20–98% во всех сценариях с реальным числом команд), описание честной методологии замера и инструкция `UM_COMPARISON_ROUTER`. Секция «Результаты на реальном сервере» (VDS) не менялась — это отдельное окружение.
- **CommandReg**: команда, у которой все слоты — RegExp, а `isPattern` не задан, склеивалась в одно выражение (и в группу регулярок) с флагами `ium` вместо флагов самих слотов: `[/^да$/, /^нет$/]` начинал совпадать с «ну\nда». Теперь склейка идёт с общими флагами слотов, группа хранит свои флаги (`IGroupData.flags`) и принимает только команды с такими же; слоты с разными флагами проверяются по отдельности. Явный `isPattern: true` склеивает с `ium`, как и раньше.
- **FileAdapter**: `close()`/`destroy()` начинали финальную запись таблицы, не дождавшись уже выполняемой debounce-записи — при обратном порядке завершения старый снимок перезаписывал итоговый. Финальная запись стартует после завершения предыдущей.
- **Telegram, VK, Viber**: служебные апдейты без ответа (`my_chat_member`, `poll_answer`, `message_reply`, `delivered`, `seen` и т.п., а также некорректные события) подтверждаются сразу, до бизнес-логики. Раньше `skipAutoReply` глушил только отправку ответа, а middleware и fallback-команда отрабатывали на каждый такой апдейт — `delivered`/`seen`/`message_reply` приходят на каждое исходящее сообщение бота.
- **Telegram**: `sendPoll` сортирует `correct_option_ids` и убирает дубли — Bot API требует монотонно возрастающий список индексов.
- **Тесты**: `tests/Bot/regressions.test.ts` импортировал `src/preload` вместо `src/Preload` — на регистрозависимой ФС (Linux CI) набор не запускался.

### Документация и внутренние улучшения (Chore)

- **Готовность к TypeScript 7**: тесты, проверяющие компилируемость артефактов (`tests/examples/compile.test.ts`, `tests/cli/createController.test.ts`, `tests/cli/flowGenerator.test.ts`), переведены с программного API компилятора (`require('typescript')` — `createProgram`/`getPreEmitDiagnostics`) на вызов CLI `tsc` через новый хелпер `tests/helpers/typecheck.ts` (временный `extends`-конфиг с подменой `umbot` → `dist/`, без `baseUrl`). TS 7 — нативный компилятор без JS API, но CLI работает одинаково на всех мажорных версиях; тесты больше не зависят от способа публикации компилятора. `examples/tsconfig.json` и `examples/umbot-demo/tsconfig.json`: `moduleResolution` `node` (node10, удалён в TS 7) → `Node16`, `module: CommonJS` → `Node16` (проверено сборкой). Дополнительно тест конфигурации примеров теперь фиксирует отсутствие deprecated-опций (`node10`/`baseUrl`/`module: CommonJS`).
- **CLI**: генерируемые проекты поднимаются на актуальный тулчейн — TypeScript `5.9.3` → `6.0.3`, `@types/node` `20.19.43` → `24.13.4` (шаблон `package.json.text` и `flowGenerator.js`); Docker-образ `node:20-alpine` → `node:24-alpine` (Node 20 — EOL с апреля 2026), Node в `deploy.yml` `20` → `24`, `engines.node` `>=20.19.0` → `>=24.0.0` (соответствует Active LTS Krypton). Сгенерированные проекты проверяются типечеком через CLI tsc на новых версиях — все варианты шаблонов (`umbot create` и flow-генератор) собираются.

- **Стиль кода**: единообразие по всему `src/` и в тестах — 342 однострочных JSDoc-комментария (`/** текст */`) развёрнуты в многострочные блоки, 50 однострочных веток (`if (x) stmt;`, включая `while` и `else if`) обёрнуты в фигурные скобки. Правило закреплено в ESLint: `curly: ["error", "all"]` — однострочные ветки больше не пройдут проверку. Изменения чисто косметические: поведение и публичный API не затронуты, все 1489 тестов зелёные.

- **Скиллы**: все 11 скиллов `.agents/skills/umbot-*` прошли аудит «симулированной задачей с ловушками» (субагенты выполняли воркфлоу скилла в read-only режиме) и актуализированы по его итогам. Ключевое: `umbot-platform-add` приведён к событийному слою 3.1.0 (`supportedEvents`, `controller.eventType`, `getQueryExample`, `static isVoice()` для регистрации в `botPlatforms`/`voicePlatforms`, API-фасад, env-связка токенов, обязательные тесты eventType/serviceEvents; «на 4xx/5xx» → «на 5xx»); `umbot-release-prepare` — формат тега зафиксирован по имени релизной ветки (`v-X.Y.Z`), полный список хардкодов версии (включая пины в `cli/flowGenerator.js` и `cli/template/package.json.text`), smoke-тест CLI перенесён на after-publish (E404 до публикации), состав пакета по `files` (dist+cli), порядок «Миграция» для breaking-изменений, compare-ссылка и дата секции в CHANGELOG-аудите; `umbot-write-tests`/`umbot-fix-bug` — битые референсы исправлены (`getAlisaQuery`→`getContent` как в `tests/Bot/bot.test.ts`, `TEST_DATA_DIR`→реальный образец `tests/DbModel/fileAdapterSelect.test.ts`), импорты в сниппетах приведены к конвенции репо (`../../src`, не self-reference `'umbot'`), `res.response?.text`, `bot.close()` в afterEach, добавлен паттерн «перехват лога» для репортов «в логах ошибка»; `umbot-code-review` — ссылка «AGENTS.md 10.2» заменена на раздел 10 п.2, отражены eslint-overrides для `tests/`/`cli/` (правила промисов там отключены), правило HTTP 200 распространено на весь путь запроса (не только адаптеры), «await без try/catch» как отдельный случай; `umbot-core-engineer` — конвенция breaking-изменений CHANGELOG («Миграция с X.Y.z» + `[Breaking]`), новое архитектурное правило про лимит/вытеснение накапливаемого состояния (DoS через attacker-controlled `userId` на неподписанных платформах), стоп-условия переписаны на реальные механизмы; `umbot-add-middleware` — `deniedText` у `ipFilter`, stub логгера в тест-каркасе, шаг дедупликации middleware; `umbot-platform-code-update` — полный список API-классов (`YandexRequest`, `constants.ts`, `interfaces/`), правило «не версионировано в коде» вместо выдумывания версий, VK-механики (`user_ids`, кэш `getVkUserInfo`, `vk_load_user_info`, passthrough параметров, наследование `MarusiaRequest`), исправлена грамматика ключевого раздела; `umbot-platform-code-audit` и матрица AGENTS.md (раздел 9) — webhook-подпись MAX исправлена на `x-max-bot-api-secret` (Authorization — auth исходящих запросов, не подпись); `umbot-doc-audit` — снято противоречие «минимум 2 раунда», правило «репорты — гипотезы».

- **Безопасность (дока)**: проверка подписи вебхука повышена с необязательных сносок в платформенных разделах до обязательного шага настройки production. Новый раздел «Проверка подписи вебхука (обязательно для production)» в `configuration.md` — таблица «платформа → что задать», генерация секрета, полный пример для Telegram (setWebhook + webhookSecret), предупреждение о поведении без секрета (адаптер принимает любой запрос от имени любого пользователя) и ограничения голосовых платформ без подписи (Алиса/Маруся/SmartApp: `userId` — не аутентифицированная идентичность). Production-чеклисты в `getting-started.md` и `GUIDE.md` дополнены пунктом про секреты вебхука (сверка по предупреждению в логе при старте); в `getting-started.md` добавлен блок «Проверка подписи вебхука — обязательный шаг» с примером конфигурации. В `platform-integration.md` шаги настройки Telegram/VK/MAX переписаны: явная генерация секрета, включение «Секретного ключа» у VK, конструктор `new MaxAdapter(token, { secret })`, и для каждой платформы описано, какие запросы проходят без секрета. `.env`-примеры (`configuration.md`, `GUIDE.md`) дополнены `TELEGRAM_WEBHOOK_SECRET`/`MAX_WEBHOOK_SECRET` с пометкой, что фреймворк читает их только вручную через `process.env`.
- **Контракт API-фасада в одном месте**: канонические `IControllerApi` и новый `TApiMethod` живут в `src/controller/BotController.ts`; `TApiFacade`/`TApiMethod` из `umbot/plugins` стали алиасами этих типов (раньше контракт дублировался в `apiFacade.ts` и рассинхронизировался руками). Публичные имена и места экспорта не изменились — обратная совместимость сохранена.
- Исправлен JSDoc-заголовок `IMaxAudio` (копипаст от `IMaxCard`); `answerCallback` фасада документирует, что `showAlert` поддерживает только Telegram (VK/MAX игнорируют).
- Исправлены некорректные форматы дат в истории изменений (например, `2025-30-11` → `2025-11-30`).
- Внутренний тип `TPatternRegExp` заменён на `(string | RegExp)[]` в `api-reference.md`.
- Относительные импорты `../../src` заменены на алиас `umbot` в `next-release.md`.
- Исправлены неточности в JSDoc: описание `@param controller` (Alisa), неверное название платформы в Marusia Card, описание конструктора "ВКонтакте" → "Max" в MaxRequest, добавлены пропущенные описания `@param`.
- Удалено избыточное описание "кнопки для кнопок" в Alisa/Card.ts.
- Исправлено форматирование кода (4 блока) в `adapter/platformAdapter.md` и обновлена таблица производительности в `FAQ.md`.
- **CLI**: описание статистики `umbot stats --log <path>` больше не упоминает RPS — отчёт выводит число строк/ошибок/предупреждений, топ команд и p50/p95/p99 latency.
- **CI**: `deploy.yml` репозитория запускает контейнер через `--env-file` и `--restart unless-stopped` вместо передачи токенов флагами `-e` (видны в `ps` и истории docker).
- Добавлен корневой `.dockerignore`: `.env`, логи, тесты, примеры, scratch-файлы и каталоги IDE исключены из образа.
- Из devDependencies удалён deprecated-заглушка `@types/mongodb`; для тестов Mongo-адаптера добавлен реальный `mongodb` (для пользователей он остаётся опциональной peer-зависимостью).
- `.gitignore` закрывает каталог `.zcode` и рабочие scratch-файлы (`pr.md`, `pr2.md`, `project-all-in-one.txt`, `scr.js`, `pulldoc.sh`).
- Устранена циклическая ссылка в `middleware.md` (заменена на ссылку на `GUIDE.md`), скорректирована иерархия заголовков в `FAQ.md`.
- Тест `MarusiaRequest` обновлён для соответствия новой версии VK API, добавлены тесты на критический функционал.
- Перефразирована META-фраза в `GUIDE.md` (убраны прямые упоминания LLM), сокращена секция плагинов в `FAQ.md` с добавлением абсолютной ссылки на документацию.
- В `platform-integration.md` добавлен отсутствовавший раздел «Создание бота для Viber» (регистрация в Viber Admin Panel, токен, sender, регистрация вебхука) и ссылки на порталы разработчика: консоль Маруси, портал Сбера (SmartApp), dev.vk.com (VK).
- Обновлены `examples/README.md` и README отдельных примеров: актуальные команды запуска через общий пакет примеров.
- В `getting-started.md` добавлен раздел «Локальная отладка с реальными платформами (туннель)» — как пробросить локальный вебхук в интернет через ngrok/cloudflared. Исправлен snippet `await bot.test()` (в CommonJS-проектах top-level await не работает), уточнены пункты про `welcome_text` (бот отвечает placeholder-текстом по умолчанию, а не молчит) и FileAdapter (держит таблицу в памяти, но пишет в JSON-файл).
- В `GUIDE.md` в быстрый старт добавлен пропущенный `npm run build` перед `npm run start` (без него запуск падал: `start` выполняет собранный код из `dist/`). Аналогичный snippet в `FAQ.md` уже был корректен.
- Исправлены неточности `cli/README.md`: `--prod` не создаёт `.env` (только Dockerfile, `.dockerignore` и `deploy.yml`), дефолтный hostname — `0.0.0.0`, а не `localhost`, ссылка на визуальный редактор ведёт на flow.maxim-m.ru, в структуре from-flow-проекта указан `.gitignore`. Пустая служебная папка `cli/my-bot` удалена из публикуемого пакета.
- Аудит документации с позиции нового пользователя: в `GUIDE.md` исправлены некомпилируемые примеры (в том числе `YandexImageRequest` — порядок аргументов конструктора, `setOAuth` без дублирующего префикса, поля ответа `checkOutPlace`), таблица подписей вебхуков (у Маруси и SmartApp подписи нет), лимиты (state 1 КБ, кнопки, пороги таймаутов 2000/2900 мс), описание `setAppMode` (маскировка секретов включена всегда и не зависит от режима; prod-режим предупреждает, но не блокирует), возвращаемые типы `Preload`, эффекты TTS (`S_EFFECT_*` подставляются только для Алисы, сырой SSML проходит в Марусе и SmartApp). Актуализированы `configuration.md` (приоритет токенов, тихий подхват из `process.env`), `deployment.md` (serverless-обработчик через `webhookEvent`), `testing.md` (`simulate()` без реальных API-вызовов), `platform-integration.md` (секреты вебхуков Telegram/MAX, порог таймаута), `FAQ.md`, `json-format.md` (генерируемый код условий — `if`, а не `switch`; импорт `rand`; наборы платформ `fullPlatforms`/`voicePlatforms`/`botPlatforms`), `migration-2x-to-3x.md`, `middleware.md`, `performance-and-guarantees.md`.
- JSDoc: порядок диспетчера запросов (step → command → intent → fallback) перенесён с приватного метода на публичный `BotController.run()` с примером; в `Bot.run()` добавлено описание параметра `clientIp`; в `isCorrectQuery` базового адаптера исправлено описание проверки подписи (VK — сравнение `secret` из тела запроса, MAX — заголовок `x-max-bot-api-secret`, без подписи — только Алиса, Маруся и SmartApp).
- `examples/`: пути к хранилищу в общих конфигах указывали внутрь `dist/` и терялись при пересборке — теперь это `examples/data/` (создаётся автоматически, в `.gitignore`); `skills/json/UsersData.json` наполнен реальной структурой `FileAdapter` (ключ — `userId`, `meta`/`data` — JSON-строки) с описанием в README; в пример `standard` добавлен обработчик `save`, демонстрирующий сохранение `userData` через `FileAdapter`. Все примеры проверены запуском: консольный режим `BotTest` и HTTP-сервер в `game`.
- Повторный аудит документации (второй проход): в `api-reference.md` исправлены комментарий `maskSecrets` (маскировка включена всегда, а не только в `strict_prod`), ложное утверждение об индексах `UsersData` (фреймворк их не создаёт), пример с переприсваиванием `userData` (заменено на `Object.assign`) и добавлено предупреждение, что `Preload` молча не работает без токенов. В `http-client.md` примеры приведены к компилируемому виду (сужение типа `input`) и перевод на `simulate()` вместо блокирующего `bot.test()`. В `getting-started.md` устаревший `YANDEX_TOKEN` заменён на `ALISA_TOKEN` и уточнён пункт чеклиста про `intents` (переданный массив заменяет встроенные `welcome`/`help`). В `adapter/platformAdapter.md` убрано противоречие про автоподключение `rateLimiter`, уточнено, что `_timeLimitLog` нужно вызывать в своём `getContent`, и исправлены strict-касты в примере `setQueryData`. В `adapter/readme.md` и `adapter/dbAdapter.md` примеры плагинов и DB-адаптера приведены к реальному контракту (`getData`, абстрактные методы `_select/_insert/_update/_remove/isConnected`). В `BENCHMARKS.md` и `performance-and-guarantees.md` актуализированы режимы `bench`/`stress:lite` и убрано невоспроизводимое число «4500 запросов». В `cli/README.md` уточнены `--minimal` (конфиги генерируются) и флаг `--force` у `add env`; в help-выводе CLI `docker` исправлен с `docker-compose.yml` на `Dockerfile и .dockerignore`. Пример `game` переведён на `BotTest`, чтобы закомментированный консольный блок заводился без правок.
- Аудит с позиции конечного пользователя (третий проход): в `GUIDE.md` исправлены сигнатура `card.addButton()` (принимает объект `TButton`, а не два аргумента), `null` в payload `addLink` (заменён на `''` — `null` не проходит strict-типизацию), пример inline-режима Telegram (невалидный `telegramApi.call('answerInlineQuery', ...)` заменён на существующий `telegramApi.answerInlineQuery()` с примечанием, что `call()` принимает только метод и `chat_id`), описание payload Telegram (фреймворк сам парсит JSON-строку в объект), примеры i18n (сужение типа вместо небезопасного каста; регистрация через `createPlugin` вместо перезаписи `ctx.t`), поведение маскировки секретов (работает во всех режимах, а не только в prod) и поведение state Алисы при превышении 1 КБ (поле не отправляется, данные не очищаются). В список методов `Bot`, не возвращающих `this`, добавлен `webhookEvent`.
- `testing.md`: во все примеры с `BotTest` добавлен `bot.use(fullPlatforms)` — без зарегистрированной платформы `test()`/`run()` бросают ошибку, и примеры из документации падали при копировании.
- `FAQ.md`: рецепт одновременной работы с БД и локальным хранилищем сделан null-безопасным — `Object.assign(cBot.state, ...)` бросал `TypeError` на чат-платформах (Telegram, VK, Viber, Max), где `state` равен `null`; заменено на `cBot.state = { ...cBot.state, ... }` с пояснением.
- `api-reference.md`: добавлены отсутствовавшие публичные API — метод `Bot.webhookEvent()`, четвёртый параметр `clientIp` у `Bot.run()`, свойство `BotController.appType`, методы `setAppContext`/`clearStoreData`/`isButtonsInit`/`isCardInit`/`isSoundInit`/`isNluInit`, `Buttons.remove()`/`isRemove` и полный раздел компонента `Nlu` (getFio/getGeo/getDateTime/getNumber/isIntentConfirm и статические getLink/getPhone/getEMail).
- `configuration.md`: уточнено поведение `strict_prod` — опасные RegExp не бросают исключение и не отменяют регистрацию команды, а отключаются (исключаются из слотов) с ошибкой в логах; в `dev`/`prod` используются как есть.
- `middleware.md`: rateLimiter использует фиксированное 1-секундное окно (счётчик сбрасывается каждую секунду), а не скользящее.
- `cli/README.md`: задокументировано, что CLI заменяет все не-буквенно-цифровые символы имени проекта на `_` (`my-bot` → `my_bot`), поэтому рекомендация kebab-case заменена на snake_case.
- JSDoc `IAppContext` (intents): описание ReDoS-проверки по режимам приведено к коду — в `prod` опасные выражения не фильтруются (фильтрация только в `strict_prod`).
- **Полный аудит документации (MD + JSDoc)**: исправлены все расхождения «дока ↔ код», найденные сверкой каждого утверждения с исходниками. Ключевое:
    - JSDoc: `events.ts` — событие `'message'` вызывается для всех текстовых запросов на любой платформе (без фильтрации по `supportedEvents`; фильтр — только предупреждение при регистрации); `Bot.setAppMode('dev')` — ReDoS-проверка не «отключена», а логируется без отклонения (отклонение — только `strict_prod`); `tryParse` возвращает `null` при null-входе; `IViberApi.min_api_version` — дефолт 7, а не 1; поле `mss` помечено устаревшим (в API Viber — `mcc`, TODO на переименование); `YandexRequest.call()` при ошибке API возвращает объект с `error`, а не null; `MaxRequest.messagesSend` — задокументирован 4-й параметр `recipientType`; примеры `IVkUploadServer`/`IVkPhotosSave`/`IVkDocSave` переписаны плоско (не компилировались); `isNluInit()` — `setThisUser` не создаёт объект Nlu; `SmartAppAdapter.getLocalStorage` возвращает данные либо `{}`; `IVkButton._group` и `IViberContent.user` описаны по фактической реализации. Массово добавлены недостающие `@returns`/`@example` у публичных методов (db-адаптеры, платформенные адаптеры, Bot/AppContext/BotTest), исправлены опечатки и битые `{@link}`/`@see`, устранены обе жалобы typedoc (`TEventType` в `pUtils`, `@internal` у `CommandReg`).
    - MD: `middleware.md` — порядок «глобальные → платформенные» переписан честно (код после `next()` глобальных middleware выполняется после платформенной цепочки, в обратном порядке) и `limit: 0` тоже отключает rateLimiter; `api-reference.md` — Viber `can()` возвращает `false` (не `✓`), `run()` — все параметры с дефолтами, `selectedElement` ищет только на текущей странице, `removeImages/removeSounds` реализованы только для Алисы/Маруси, добавлено предупреждение о молчаливой незарегистрации команды с пустыми слотами; `json-format.md` — «генерируемый код» http-паттерна приведён к реальному генератору (текст + JSON.parse с fallback, headers), `isEmpty` генерирует `!a`, суффиксы коллизий имён; `GUIDE.md` — Express-примеры без `express.json()` (конфликтует с `webhookHandle`), `isEnd` поддерживают только голосовые платформы, Viber `tts` при пустом `text` уходит как текст, `numberPage` требует цифру, реальное сообщение TS-ошибки, согласованы пороги FileAdapter (250/270 МБ) и таймауты (~3 c), лимит кнопок MAX (30 в реализации против 7×30 платформы); `platform-integration.md`/`platformAdapter.md` — примеры кастомного адаптера дополнены импортами (`BotController`, `Text`), `chatId`/`eventId` хранятся в `platformOptions.requestData`, `console.error` заменён на `logError`; `README.md` — MAX включён в перечень чат-платформ.
    - Examples: «живая сессия» `umbot-demo` — цена второго заказа исправлена (180 ₽, каждый 5-й бесплатный), в таблицу env добавлен `SMARTAPP_TOKEN`; лишняя кавычка в заголовке карточки `addCommand`; в `skillAuthParam`/`skillGameParam` добавлены TODO о недостижимых welcome/help-кейсах (массив intents заменяет встроенные).

## [3.0.14] - 2026-06-28

### Исправлено

- **Payload кнопок (Telegram, VK)**: payload нажатой кнопки приходил в обработчик сырой строкой JSON (`'{"command":"buy"}'`). Добавлена утилита `tryParse` (доступна в `pUtils`): строка, начинающаяся с `{`/`[`, автоматически разбирается в объект, всё остальное возвращается как есть. `controller.payload` у Telegram (`callback_query.data`) и VK (`message.payload`, `message_event.payload`) теперь содержит объект — без ручного `JSON.parse` в прикладном коде.
- **Базовые команды**: `welcome`/`help`, явно зарегистрированные через `addCommand`, не вызывались — интент всегда подставлял текст по умолчанию (`welcome_text`/`help_text`). Теперь при наличии зарегистрированной команды выполняется её обработчик.
- **Telegram**: кнопка с `payload` и без `url` уходила пользователю как reply-кнопка без действия. Теперь она отправляется inline-кнопкой с `callback_data`; за reply-кнопками закреплены опции `request_contact`/`request_location`.
- **Документация**: добавлена подробная инструкция по написанию приложений (`GUIDE.md`); ссылки на опубликованную документацию приведены к актуальному формату путей (для корректировки добавлена утилита `scripts/fix-doc.js`).

## [3.0.13] - 2026-06-20

### Добавлено

- Экспорт констант `EMPTY_QUERY_ERROR` и `EMPTY_CONTEXT_ERROR` из `umbot/plugins`. Тексты ошибок пустого запроса/контекста объявлены в `BasePlatform` ещё в 3.0.12, но не были доступны потребителям для типизации и обработки.

## [3.0.12] - 2026-06-12

### Изменено

- **Middleware**: цепочка выполняется до вызова бизнес-логики и загрузки данных пользователя из БД. Остановка в middleware больше не приводит к отправке ответа платформе — обработка запроса прерывается сразу (раньше контроллер при этом выполнялся, и пользователю уходил ответ).
- **Производительность**: оптимизация внутренней логики фреймворка. Поиск активного шага выполняется прямым обращением по ключу вместо перебора всех шагов (с fallback-поиском по интентам запроса); утилита `keysCount` (подсчёт ключей без аллокации массива `Object.keys`) — в горячих местах (`Bot`, `FileAdapter`, `VkRequest`, `MaxRequest`); шаблоны маскирования секретов вынесены в константу уровня модуля; для FormData убрана лишняя сериализация тела запроса.
- **Регулярные выражения**: задержка отложенной компиляции групп уменьшена со 100 мс до 35 мс. Внутренние таймеры (debounce регулярок, ленивое сохранение `FileAdapter`, задержки между запросами к Yandex API) помечены `.unref()` — не задерживают завершение процесса.
- **Алиса**: пустой `request.nlu` не инициализирует NLU; `_initTTS` может работать синхронно (тип `void | Promise<void>`) — лишний промис не создаётся.
- **Типы**: `payload` в интерфейсах Алисы и Маруси уточнён с `object` до `Record<string, unknown> | string`; `bot.getBotController()` помечен `@private` — метод внутренний, в прикладном коде использовать нельзя.
- **Бенчмарки**: новый тест сравнения производительности с «лёгкой реализацией» без универсального каркаса (`npm run cleancomp`).
- **CI**: `release-package.yml` переведён на Node 24 и `setup-node@v6`.

### Исправлено

- **Маруся**: `session.user_id` может прийти числом — идентификатор пользователя теперь приводится к строке, иначе ключи записей БД рассинхронизировались между запросами.

## [3.0.11] - 2026-05-30

Техническая публикация через несколько минут после 3.0.10, без изменений в коде фреймворка (файлы `dist` идентичны 3.0.10). Единственное изменение: в cli-шаблоне `package.json` генерируемых проектов в `keywords` добавлялось имя проекта (`{{name}} project`; в 3.0.12 от этой правки отказались).

## [3.0.10] - 2026-05-30

### Добавлено

- **Health-check**: `GET /health` у вебхук-сервера отвечает `{"status":"ok"}` — для балансировщиков и оркестраторов.
- **CLI**: шаблон `package.json` генерируемых проектов получил команду `update` (`npm update umbot`); Node.js из коробки повышен до 24-й (`@types/node`, `deploy.yml`).

### Изменено

- **Асинхронность адаптеров**: `destroy()` плагинов и БД-адаптеров, `soundProcessing` платформ поддерживают `Promise` — асинхронные операции больше не порождают плавающих промисов, ошибки логируются. `Model.destroy()` возвращает промис закрытия адаптера. Временные TTS-файлы удаляются с `await` (Telegram/Max/VK).
- **rateLimiter**: ошибки обработки очереди перехватываются и логируются; микрооптимизации горячего пути.
- **ESLint**: добавлены новые правила (код фреймворка им соответствует).

### Исправлено

- **Модели картинок/звуков**: `TABLE_NAME` у `ImageTokens`/`SoundTokens` был полем экземпляра, а не статическим полем класса — данные токенов могли не сохраняться.
- **Вебхук-подпись**: `isCorrectQuery` получает исходное тело запроса, а не объект после `JSON.parse` — HMAC-подпись считается по байтам, как их прислала платформа (повторная сериализация объекта меняла формат и «ломала» подпись).
- **VK**: `userCommand` из payload callback-кнопки (`message_event`) приводится к нижнему регистру, как и для текстовых сообщений — команды теперь находятся и по нажатию кнопки.

## [3.0.9] - 2026-05-20

### Исправлено

- Возвращён фикс стабильности из 3.0.6, потерянный при выпуске 3.0.7: доступ к токенам платформы через опциональную цепочку (`BasePlatform.isCorrectQuery`, запросы изображений/звуков Алисы), `init()` гарантирует существование объекта токенов платформы — мультиплатформенный бот без токена одной из платформ больше не падает при инициализации и проверке подписи.

## [3.0.8] - 2026-05-20

### Исправлено

- **Хранилище/шаги диалога (#76)**: сброс активного шага не пробрасывался в состояние сессии — при завершении диалога `oldIntentName` удалялся (`delete`) из `userData`/`state`, но у голосовых платформ отсутствие поля означает «оставить как было», поэтому записанный шаг восстанавливался при повторном входе в навык. Теперь вместо удаления явно записывается `null`.

## [3.0.7] - 2026-05-20

### Добавлено

- **Шаги (#73)**: обработчик шага может вернуть `false` — шаг считается не подлежащим обработке, и запрос идёт дальше по обычной цепочке команд. Актуально, когда пользователь вернулся в диалог, а активный шаг уже неактуален (например, начатая игра давно завершена).

### Изменено

- **CLI**: шаблон `package.json` генерируемых проектов дополнен командами `start:pm2` и `deploy` (pm2).

### Исправлено

- **CLI**: генерируемые приложения по умолчанию слушали только `localhost` — на сервере и в контейнере бот был недоступен с других интерфейсов. Значение по умолчанию изменено на `0.0.0.0`.
- Опечатки в JSDoc и шаблонах.

## [3.0.6] - 2026-05-16

### Исправлено

- **Стабильность (#75)**: обращение к токенам платформы без проверки (`appConfig.tokens[platform].token`) роняло работу адаптера, когда токен одной из платформ не задан — падали и `isCorrectQuery`, и загрузка изображений/звуков Алисы. `BasePlatform.init()` теперь гарантирует объект токенов платформы, доступы переведены на опциональную цепочку.

## [3.0.5] - 2026-05-16

### Исправлено

- **`bot.start()` (#74)**: повторный вызов `start()` выполнял полный `close()` — вычищал плагины и закрывал `AppContext` (включая подключения к БД). Теперь при повторном запуске закрывается только предыдущий HTTP-сервер; плагины и контекст приложения сохраняются.

## [3.0.4] - 2026-04-10

### Исправлено

- **`AppContext` из npm**: в опубликованных типах пакета вместо декларации класса оставались только экспортируемые константы — импорт типов `AppContext` из `umbot` ломал компиляцию TypeScript-проектов потребителей. Декларация класса восстановлена.
- **Request**: объекты в дополнительных полях FormData-запросов сериализуются в JSON вместо `"[object Object]"`.

### Изменено

- **README**: добавлен раздел про единый API с учётом специфики платформ (голосовые — полный функционал, чаты — необходимый набор); рекомендации по `re2` и выбору БД переехали из README в документацию.
- Доработана автодокументация: JSDoc `AppContext`, обзор работы `BotController.run()` и другие описания.

## [3.0.3] - 2026-04-07

### Добавлено

- **Логгер**: опция `maskSecrets: false` в `Bot.setLogger()` — отключение автоматической маскировки секретов в логах. Предназначена только для безопасных окружений (локальная отладка): в production маскировку отключать нельзя.

### Исправлено

- Опечатки и неточности в cli-инструменте и шаблонах генерируемых проектов.

## [3.0.2] - 2026-04-06

### Изменено

- Чтение `.env`-файлов переведено на штатную утилиту работы с файлами (`freadSync`) вместо прямого `fs.readFileSync` — единый формат ошибок чтения.

## [3.0.1] - 2026-04-06

### Изменено

- **CLI / TypeScript 6**: шаблон `tsconfig.json` генерируемых проектов переведён на `module: Node16` и `moduleResolution: Node16` (`rootDir: ./src`) — проекты корректно собираются под TypeScript 6.0. Шаблонный `package.json` хранится как `package.json.text`, чтобы директория шаблонов не считалась вложенным npm-пакетом.
- **Автодокументация**: примеры в JSDoc массово приведены к безопасным URL (`example.com` → `http://localhost`).
- Мелкая оптимизация кода.

## [3.0.0] - 2026-04-05

### Добавлено

- Возможность отправить ответ пользователю программно (`bot.send(userId, controllerOrText, platform)`) — бот инициирует
  диалог сам. Актуально для рассылок. Важно учитывать, что не все платформы поддерживают данный функционал: на
  неподдерживающих вернётся `false`.
- Добавлена возможность добавить шаги (`bot.addStep(name, slots, cb)`). Шаги — механика, основанная на
  `controller.oldIntentName`: если значение соответствует зарегистрированному шагу, выполнится его обработчик.
- Добавлена возможность переопределить ответ сервера. Для этого в 3 аргумент `bot.start()` можно передать свой обработчик
- Добавлена возможность указать свою реализацию для обработки регулярного выражения (плагин `plugins.regExp`).
  Функционал реализуется за счет использования плагинов.
- Добавлена возможность указать кастомный NLU-провайдер (плагин `plugins.nlu`). Функционал реализуется за счет
  использования плагинов.
- Добавлена возможность указать i18n (плагин `plugins.i18n`). Функционал реализуется за счет использования плагинов.
- Добавлена возможность повлиять на обработку регулярных выражений (`commandReg.setCommandGroupMode('auto' | 'group' | 'no-group')`).
  По умолчанию регулярные выражения собираются в группу при количестве команд более 300. Сейчас появилась возможность
  отключить группировку, либо включить группировку принудительно.
- В состав библиотеки добавлена middleware `rateLimiter()`, которая ограничивает количество запросов пользователя к
  платформе. Подключается через `bot.use(rateLimiter())`.
- Добавлена возможность указать кастомный обработчик для определения типа платформы (`bot.setPlatformResolver(fn)`)
- Добавлена механика проверки корректности запроса от платформы (`isCorrectQuery`), а именно проверяется токен
- Добавлена поддержка typescript 6

### Обновлено

- Вся работа с платформами настраивается через адаптеры. Подробнее о переводе на новое
  API смотрите [тут](/src/docs/migration-2x-to-3x.md)
- Работа с базами данных настраивается через адаптеры. Подробнее о переводе на новое API
  смотрите [тут](/src/docs/migration-2x-to-3x.md)
- Поменялось API для основных классов. Подробнее о переводе на новое API смотрите [тут](/src/docs/migration-2x-to-3x.md)
- Для обработчиков команд, добавлена возможность асинхронного выполнения. Критично для сценариев, когда в самой команде
  необходимо выполнить асинхронную операцию.
- Произошел переход на минимальную версию node.js 20
- В Preload добавлена возможность загрузки ресурсов для Telegram. Для этого необходимо указать пользователя, которому
  будут отправлены данные.
- Cli инструменты, согласно новому API
- Доработана логика работы файловой БД, а именно появились предупреждения при достижении определенного размера базы, а
  также принудительное сохранение данных
- Доработана логика работы state и userData. Ранее сохранялось только 1 из значений. Сейчас есть возможность сохранять
  состояние приложения в 2 состояния одновременно.
- В `BotController` удален `store`, так как его логика работы аналогична `state`
- Доработана логика поиска нужной команды. До 3.0, команды обходились последовательно, начиная с 3.0.0 логика поиска
  была оптимизирована. С начала происходит поиск по полному сходству запроса пользователя, с зарегистрированной
  командой, и в случае если команда не была найдена, происходит последовательный обход.
- При добавлении команд с одинаковыми активационными фразами, будет выведена ошибка в лог-файл.
- Удалена зависимость от `shx`, вместо библиотеки используется свое решение по очистке директории.
- Вся работа со звуками, и различными эффектами перенесена в SoundConstants. Ранее, для добавления различные эффектов,
  требовалось обращаться к нужной константе из платформы.
- Оптимизирована логика обработки tts. Сейчас дополнительная обработка tts происходит тогда, когда это действительно
  необходимо.
- В `Buttons` удалены поля `btns` и `links`, вместо них стоит использовать `addBtn` и `addLink`.
- Произведена внутренняя оптимизация логики фреймворка, благодаря чему удалось получить увеличение производительности
  при поиске команд в худшем случае в среднем на 10%. Также улучшена общая производительность обработки команд в среднем
  на 15-22% для текстовых команд и на 10-18% для регулярных выражений. Потребление памяти уменьшилось на 20-25%.
- Поправлена логика работы моделей. Ранее для доступа к состоянию использовался proxy, сейчас данный функционал должен
  реализовывать сам разработчик модели, добавляя set/get, записывая все данные в state.
- Для mongoDB количество одновременных подключений по умолчанию изменено с 1 до 50.
- Удален класс `Button`, вместо него работа происходит с методами `getButton` и `getLinkButton` для получения информации
  о кнопке, и `IButtonType` для типизации. Константы `B_LINK` и `B_BTNS` перенесены в `Buttons`. Также удален класс
  `Image`, вместо него необходимо использовать `getImage` для получения данных для изображения, и `IImageType`, для
  типизации. Подобное решение было принято в целях оптимизации.

### Исправлено

- Ошибки и неточности в документации
- Места с избыточной логикой, за счет чего удалось добиться небольшого повышения производительности
- Ошибка по которой данные загруженные из env могли не применяться.
- Ошибка, когда обработка схожести текста могла отработать некорректно.
- Ошибка, когда мог отправиться некорректный запрос в платформу
- Ошибки в генерации шаблонов через cli
- Доработан вывод для бенчмарка с командами
- Исправлены неточности стресс теста
- Ошибка, когда предыдущий интент(oldIntentName) не сбрасывался при последующих запросах

## [2.2.x] - 2025-11-30

### Добавлено

- Возможность в logger указать метрику (`setLogger({ metric: (name, value, labels) => {...} })`).
- Возможность указать кастомный обработчик команд (`bot.setCustomCommandResolver((userCommand, commands) => {...})`)
- Автоопределение типа приложения на основе запроса
- Метод для задания режима работы приложения `bot.setAppMode('dev' | 'prod' | 'strict_prod')`
- stress test для проверки фреймворка под нагрузкой
- Добавлена поддержка re2 для обработки регулярных выражений (`npm install re2` — фреймворк подхватывает пакет сам).
  Благодаря этому потребление памяти может сократиться, а также время обработки регулярных выражений ускорится
  примерно в 2-6 раз
- Добавлено предупреждение при добавлении большого количества команд

### Обновлено

- Ошибки во время работы приложения записываются как ошибки, а не как обычные логи
- Оптимизирована логика поиска нужного текста
- Поиск опасных регулярных выражений(ReDos) в интентах
- Сохранение логов стало асинхронной операцией
- Произведена оптимизация работы
- Поправлены шаблоны навыков в cli
- Удалены устаревшие методы
- Метод bot.initBotController принимает класс на BotController. Поддержка передачи инстанса осталась, но будет удалена в
  следующих обновлениях
- Удалена возможность указать тип приложения через get параметры.
- Более детальные логи при получении ошибки во время обращения к платформе
- Оптимизирована работа с регулярными выражениями
- Оптимизирована работа с файловой базой данных. Запись происходит асинхронно, и не так часто как ранее. Раньше запись
  происходила на каждое сохранение, сейчас данные из базы хранятся в памяти, и запись происходит через 500мс после
  бездействия.
- Доработан механизм поиска значений в файловой бд, теперь если идет поиск по ключу и данного ключа нет, поиск
  отрабатывает за O(1), а не за O(n), также если поиск идет только по ключу, то поиск также будет составлять O(1)
- Для удобства, константа FALLBACK_COMMAND стала иметь значение "\*", данный подход позволяет просто указать
  `bot.addCommand("\*",[], () => {...})`, чтобы указать команду для действия, когда нужная команда не была найдена
- Повышена производительность
- В `botController`, `isSend` переименован в `skipAutoReply`

### Исправлено

- Архитектурная проблема, из-за которой приложение могло работать некорректно под нагрузкой
- Ошибки с некорректной отправкой запроса к платформе
- Ошибка когда benchmark мог упасть, также доработан вывод результата
- Ошибка когда логи могли не сохраняться
- Ошибка с некорректной записью и чтением результатов из файловой бд
- При завершении работы приложения, сбрасываются все команды и происходит отключение от бд
- Ошибка в benchmark из-за чего он показывал результат лучше, чем есть на самом деле. Особенно на регулярных выражениях
- Ошибка с некорректным сбросом подключения к бд
- Проблема, когда при относительно большой файловой бд(более 10000 записей), время обработки могло сильно просесть.
- Исправлена ошибка, когда запросы не отправлялись в некоторые платформы

## [2.1.0] - 2025-10-19

### Добавлено

- Добавлена поддержка MAX (адаптер `new MaxAdapter(token)`)
- Метод `Text.textReplace(key, value, text)` для корректной замены текста по ключу (значение может быть массивом —
  подставится случайный элемент)
- Добавлено ограничение для кэширования регулярных выражений
- Добавлена возможность задать свою функцию для логирования (`Bot.setLogger(...)`)
- Добавлен класс `Preload`, который позволяет загрузить необходимые ресурсы до запуска навыка
  (`new Preload().loadImages(...)` / `loadSounds(...)`).
- Добавлена возможность указать свой httpClient (`bot.getAppContext().httpClient = ...`).
- Добавлена возможность в slots добавить честное регулярное выражение (`addCommand('cmd', [/привет/i], cb)`)
- Добавлен benchmark для проверки производительности (`npm run bench`)
- В platformParams добавлено поле `empty_text` — этот текст выводится пользователю, если нужная команда не была
  найдена.
- Если в addCommand передать название команды как FALLBACK_COMMAND (`'*'`), то команда будет выполнена в случае, если не
  получится найти команду для обработки
- Добавлена поддержка middleware (`bot.use(...)`)

### Обновлено

- Механика настройки приложения. Происходит отказ от использования глобального `mmApp` в пользу готовых механизмов в
  `Bot.setLogger`
- Методы initConfig и initParams помечены как deprecated. Вместо них стоит использовать setAppConfig и
  setPlatformParams.
- Небольшая оптимизация производительности
- Написано чуть больше тестов, за счет чего поднялся coverage
- Обновился eslint до актуальной версии
- BotController не обязательно задавать, если все можно сделать за счет `bot.addCommand`
- При записи логов в файл, все секреты маскируются

### Исправлено

- Ошибка при указании в tts нескольких звуков
- Ошибки некорректной работы некоторых приложений
- Ошибки в cli
- Исправлена ошибка, когда поиск по регулярному выражению мог возвращать некорректный результат
- Ошибки с некорректным отображением документации

## [2.0.0] - 2025-05-08

### Добавлено

- prettier для стилизации
- Добавлен eslint
- В cli добавлена возможность сгенерировать .env файл
- Добавлено форматирование сгенерированного cli проекта через prettier

### Обновлено

- Удалена поддержка старого подключения через micro
- Работа некоторых компонентов
- Поменялась логика работы модели
- Поменялась utm метка в ссылках по умолчанию. Раньше было Yandex_Alisa стало umBot
- Произведена оптимизация работы некоторых утилит
- Поменялось API для методов работы с файлами
- Добавлена новая логика для регистрации обрабатываем команд. Сейчас помимо добавления команды в mmApp.setParam, можно
  передать ее в mmApp.addCommand(commandName: string, slots: string, cb: (userCommand: string, botController?:
  BotController) => {})
- Для удаления ранее добавленной команды можно вызвать mmApp.removeCommand(commandName: string)

### Исправлено

- Документация некоторых компонентов
- Опечатки в документации
- Ошибки в cli

## [1.1.8] - 2022-10-06

### Добавлено

- Unit тесты на jest
- В консольный скрипт добавлена возможность указывать режим работы приложения.
- Добавлена возможность указывать режим работы для приложения. Режим разработки отличается от релизного тем, что в нем
  выводятся все сообщения об ошибках в консоль.

### Обновлено

- Структура проекта. Некоторые классы и компоненты переехали в другие каталоги
  ** `Text` из components переехал в `utils`
  ** Компоненты отвечающие за обработку и возврат ответа для конкретной платформы приложения переехали в `platforms`
- Найденные в ходе перемещения ошибки
- Метод `nav` в `components:Navigation `переименован в `getPageElements`
- Описание некоторых опций

### Исправлено

- Циклические зависимость
- Документация некоторых компонентов
- При установке через npm некорректно работала подсветка и подсказки
- Удалено все лишнее из `dist`
- Большие методы разбиты на более мелкие
- Поправлена автодокументация

## [1.1.6] - 2022-08-14

### Добавлено

- Проект добавлен в `npm`

### Обновлено

- Немного изменено имя

## [1.1.5] - 2022-08-14

### Исправлено

- Произведен мини рефакторинг проекта
- Недочеты в консольном скрипте

### Добавлено

- Возможность оценить приложение
- Новое состояние на приложении, которое позволяет понять какое действие было выполнено пользователем BotController:
  userEvents
- В BotController удалено состояние isAuthSuccess, место него стоит использовать userEvents.auth.status
- Добавлена проверка на установку логики приложения

### Обновлено

- Название некоторых методов, которые в название содержали нижнее подчеркивание
- Описание некоторых классов и их свойств

## [1.1.4] - 2022-05-01

### Исправлено

- Произведен рефакторинг проекта, лишнее удалено
- Ошибки с типизацией
- Ошибки с производительностью
- Ошибки стабильности

### Добавлено

- Новые интерфейсы
- Новые правила сборки проекта, из-за чего возросла надежность кода
- Новый класс для тестирования приложения. Основной класс запуска приложения, не запускает тестирование.

### Обновлено

- Логика поиска почты. Стало работать быстрее
- Описание некоторых классов и их свойств

## [1.1.3] - 2021-07-17

### Исправлено

- Незначительные ошибки и опечатки
- Произведена оптимизация кода

### Добавлено

- В Text добавлен новый метод isUrl
- Метод run в случае ошибки кидает исключение
- В Request добавлен метод для загрузки файлов на сервер
- run в случае ошибки кидает исключение
- Поддержка локального хранилища в Марусе
- Загрузка изображений и звуков в Марусе

## [1.1.2] - 2021-04-01

### Исправлено

- Ошибка с сохранением данных в SmartApp

## [1.1.1] - 2021-03-30

### Добавлено

- Отслеживание изменений(Changelog)
- Новые Unit тесты
- Консольный скрипт для автоматического создания проекта
- Для карточек добавлена возможность задавать свой шаблон
- Демка, демонстрирующая возможность указания своего обработчика для подключения к бд
- Для Сбер SmartApp была добавлена возможность локального хранения данных(данные сохраняются в бд Сбера)

### Исправлено

- Места, влияющие на производительность
- Интерфейс IModelRes перенесен в интерфейсы
- Поправлена ошибка при работе SmartApp
- Поправлена ошибка с сохранением данных в файл

### Обновлено

- Поменялось логика отображения карточки для SmartApp
- Изменилось имя для проекта для smartApp было smart-app стало smart_app

## [1.1] - 2021-02-23

Более стабильная версия, с исправлением большей части ошибок

### Добавлено

- Возможность задания своего контроллера для подключения в бд.
- Вспомогательный класс, хранящий данные для запросов к базе данных. Который также способен перевести строку в
  корректный для запроса формат

### Обновлено

- Изменена логика работы с базой данных
- При работе с текстом, а именно с поиском вхождения, поменялась логика поиска. Раньше поиск осуществлялся через
  регулярное выражение, в текущей версии через indexOf

### Исправлено

- Все найденные ошибки и замечания

## [1.0] - 2020-12-25

Появление стабильной версии, которую можно использовать в проекте

### Добавлено

- Поддержка Маруси и Сбер SmartApp
- Документация
- Unit тесты

### Исправлено

- Найденные ошибки

## [0.9-beta] - 2020-11-04

Создание бета-версии инструмента

[3.1.4]: https://github.com/max36895/umbot/compare/v-3.1.0...v-3.1.4
[3.1.0]: https://github.com/max36895/umbot/compare/a89231e...v-3.1.0
[3.0.14]: https://github.com/max36895/umbot/compare/83d4f92...a89231e
[3.0.13]: https://github.com/max36895/umbot/compare/3.0.12...83d4f92
[3.0.12]: https://github.com/max36895/umbot/compare/3.0.11...3.0.12
[3.0.11]: https://github.com/max36895/umbot/compare/150fd6a...3.0.11
[3.0.10]: https://github.com/max36895/umbot/compare/7ac593d...150fd6a
[3.0.9]: https://github.com/max36895/umbot/compare/bebbb6f...7ac593d
[3.0.8]: https://github.com/max36895/umbot/compare/b3f2728...bebbb6f
[3.0.7]: https://github.com/max36895/umbot/compare/c82a196...b3f2728
[3.0.6]: https://github.com/max36895/umbot/compare/c180b80...c82a196
[3.0.5]: https://github.com/max36895/umbot/compare/7f54c02...c180b80
[3.0.4]: https://github.com/max36895/umbot/compare/67ad827...7f54c02
[3.0.3]: https://github.com/max36895/umbot/compare/75d2f08...67ad827
[3.0.2]: https://github.com/max36895/umbot/compare/bd8b55b...75d2f08
[3.0.1]: https://github.com/max36895/umbot/compare/369776c...bd8b55b
[3.0.0]: https://github.com/max36895/umbot/compare/v2.2.x...369776c
[2.2.x]: https://github.com/max36895/umbot/compare/v2.1.0...v2.2.x
[2.1.0]: https://github.com/max36895/umbot/compare/2.0.0...v2.1.0
[2.0.0]: https://github.com/max36895/umbot/compare/1.1.8...2.0.0
[1.1.8]: https://github.com/max36895/umbot/compare/656ce54...1.1.8
[1.1.6]: https://github.com/max36895/umbot/compare/1.1.5...656ce54
[1.1.5]: https://github.com/max36895/umbot/compare/1036441...1.1.5
[1.1.4]: https://github.com/max36895/umbot/compare/8ca7440...1036441
[1.1.3]: https://github.com/max36895/umbot/compare/1.1.2...8ca7440
[1.1.2]: https://github.com/max36895/umbot/compare/fcb3e32...1.1.2
[1.1.1]: https://github.com/max36895/umbot/compare/1.1...fcb3e32
[1.1]: https://github.com/max36895/umbot/compare/1.0...1.1
[1.0]: https://github.com/max36895/umbot/compare/0.9...1.0
[0.9-beta]: https://github.com/max36895/umbot/releases/tag/0.9
