Баги генераторов, рантайма и доков: union без дискриминатора, ретраи записей, потеря контента - #270
Open
lookinway wants to merge 16 commits into
Open
Баги генераторов, рантайма и доков: union без дискриминатора, ретраи записей, потеря контента#270lookinway wants to merge 16 commits into
lookinway wants to merge 16 commits into
Conversation
…, порядок multipart
Проверка накопившихся замечаний по генераторам, рантайму CLI/n8n и гейтам.
Три из них оказались ложными, остальные подтвердились по коду и исправлены.
SDK/генератор (1.1.10 / SDK 1.0.28)
`detectDiscriminatorField` сканировал только первого члена union и всегда
возвращал имя поля. Для `AuditEventDetailsUnion` (селектор — родительское
`event_key`, общего литерала у членов нет) это давало несуществующее поле
`type`: Go возвращал `unknown ... type` и ронял разбор всего ответа
`GET /audit_events`, C# и Kotlin падали на дублирующихся дискриминаторах,
Swift — на отсутствующем ключе, Python молча отдавал пустые детали.
Теперь поле принимается, только если литерал есть у каждого члена, иначе
дискриминатора нет и эмиттеры разбирают union по составу полей payload.
В Go у такого union добавлено поле `Raw` — неизвестная форма больше не
ошибка разбора. Python заодно перестал брать «первого члена списка»: для
union с дискриминатором появился реестр `_UNION_DISCRIMINATORS`, что чинит
и `ViewBlockUnion`. Инлайновый enum в Go-параметре генерируется как
`string`, а не `any` (не компилировалось).
Добавлена фикстура `union-no-discriminator` — её отсутствие и позволило
багу уехать в релиз — и guard: пустое значение дискриминатора
(`@SerialName("")`, `JsonDerivedType(..., "")`) валит тесты.
CLI (2026.7.2)
- `files direct-url` клал файл в форму первым; S3 игнорирует поля после
файла, поэтому подпись и ключ не доезжали и загрузка возвращала 403.
Генератор теперь эмитит бинарное поле последним, как рукописный upload.ts
- 503 больше не ретраится для не-идемпотентных методов: гейт `safeMethod`
был только у сетевых ошибок, из-за чего закоммиченный POST мог задвоиться
- явный `--profile` больше не проигрывает ambient `PACHCA_TOKEN`
- `Retry-After` в формате HTTP-date больше не превращается в NaN, значение
ограничено 60 секундами и не может быть отрицательным
- `downloadFile` берёт basename из `Content-Disposition` — имя из ответа
редиректа не может увести запись за пределы папки
- CSV: экранирование formula-injection, объекты и массивы как JSON
- `clearTimeout` в finally, `--timeout` меньше 1 отклоняется
- ярлык примера в `--help` = шаг сценария, а не заголовок воркфлоу
(«Добавить реакцию» на `reactions remove`)
- в workflows.ts исправлена команда `pachca thread add` → `threads add`
n8n (2.0.16)
- 5xx не ретраится для POST/PATCH: закоммиченное сообщение могло уйти
до четырёх раз
- ресурс Bot больше не открывается на необратимой `recreateTokenSelf`
- `user_id` попал в `PATH_PARAM_SEARCH` — у операций со статусом и
аватаркой вернулся пикер сотрудника
- имя sub-collection считается одним хелпером: UI и роутер расходились
(`${x}Values` против `singularize(x)`)
- ярлыки group_tags говорят «to chat», порядок в `getParamName` исправлен,
`tagToResource` больше не даёт «custom propertie»
- searchChats: v2 `limit` вместо v1 `per`, курсор кодируется; поиск
чатов и сотрудников пагинируется; `sanitizeBaseUrl` во всех хелперах
- пагинация: guard на застрявший курсор в ветке `has_next`
- триггер: вебхук без `webhook_timestamp` отклоняется, если задан секрет
Доки и skills
- `COMMON_ENDPOINT_MAP` применяется по пути, а не по несуществующему тегу
`Common`: `/custom_properties`, `/uploads` и `/direct_url` вернулись в
pachca-profile и pachca-messages, orphan-скиллы вне индекса удалены
- `nearestAlternatives` наконец попадают в описание скилла
- снят устаревший guard `/api/search` в proxy.ts — роут-хендлеры давно
переехали в `/internal`, а `.md`-твины страниц существуют
- body-массивы в CLI-примерах печатаются как JSON (CLI парсит их через
JSON.parse), POSIX-квотирование в curl и CLI, `${filename}` в ключе S3
- `isNewUpdate` сравнивает даты в одном часовом поясе; дата без кавычек
во frontmatter больше не ломает загрузку обновлений
Гейты
- `check-release`: ошибка npm больше не выдаётся за «первый релиз»,
CalVer на границе года даёт 2027.1.0, месяц 13 невалиден
- `check-changelog-sync`: недоступный base — падение, а не тихий пропуск
- `check-generated-sync`: `--force` (корневые артефакты вне turbo outputs)
и покрытие `skills/**`, `SKILL.md`, `skill.json`, agent-card
- n8n-workflow проверяет, что закоммиченный узел совпадает со спекой
Отдельно проверено и признано корректным: тест учётных данных n8n обращается
к `/oauth/token/info`, а не к `/profile`, потому что у `/profile` есть
обязательный scope и узкоскоупные токены ботов он отверг бы. Зафиксировано
комментарием в генераторе, чтобы не «исправили» обратно.
…round-trip формы починен
Класс проблем, который проходит все проверки: примеры синтаксически валидны
и соответствуют схеме, но показывают не то, что возвращает API.
Рукописные примеры терялись
`generateResponseExample` и `generateRequestExample` шли сразу в схему и
никогда не смотрели на `example` уровня media type. Все 9 примеров,
написанных в спеке руками, молча выбрасывались.
Заметнее всего на `POST /threads`: спека объявляет `message_id: null` и
`message_chat_id: null` (у самостоятельного треда нет сообщения, и описание
метода прямо это обещает), а страница показывала выдуманные из схемы
`154332686` и `2637266154` — то есть ровно противоположное тексту рядом.
Теперь пример из спеки имеет приоритет, как и требует семантика OpenAPI.
У `POST /bots` и `PUT /bot/webhook` пример запроса стал у́же синтетического
(`template`, `challenge_key`, `can_edit` в нём не перечислены) — это
осознанно: поля остаются задокументированы в таблице схемы выше, а пример
теперь показывает реальный минимальный вызов.
Добавлен гейт `check-examples`: каждый рукописный пример спеки обязан
дословно присутствовать в `public/api/**`. Проверен на реальном баге — на
состоянии до фикса падает.
nullable не отображался
`markdown-generator` не выводил `nullable`, поэтому поле, которое API
возвращает как `null`, читалось как обычное непустое. Из 53 nullable-полей
только 15 сообщали об этом текстом описания. Теперь рендерится
`(required, nullable)` — 575 мест.
Двойные точки
Точка перед «Пример:» ставилась безусловно, а конвенция репо требует точку
в конце многопредложного `@doc`. Итог — 187 «..» в 39 файлах и 205 в
llms-full.txt. Строковые примеры печатались через сырое `"${...}"`, из-за
чего значение-JSON получало неэкранированные вложенные кавычки.
Опечатки в примерах формы (typespec.tsp)
Описания webhook-полей говорят «указанный при открытии представления» —
значит значения на двух концах обязаны совпадать. Они расходились:
- `callback_id`: отправь `timeoff_reguest_form`, получи `timeoff_request_form`.
Проверка `payload.callback_id === "timeoff_request_form"`, написанная по
странице открытия формы, никогда не срабатывала
- `private_metadata`: в вебхуке `{'timeoff_id':4378}` — одинарные кавычки,
не JSON. `JSON.parse` на задокументированном значении бросает SyntaxError
Правка затрагивает только строки `@example`, контракт не меняется. Опечатка
успела разойтись по openapi.yaml, гайду, postman-коллекции, llms-full и
сниппетам SDK — вычищена везде.
Отложено как требующее отдельного решения: 13 нарушений конвенции точек в @doc, 38 nullable-полей без описания
случая `null`, `nullable` рядом с `allOf` в ответах статуса (игнорируется
большинством кодогенераторов), `scopes` как `array of string` вместо enum,
ключи `x-enum-descriptions` через `_` против значений enum через `:`,
примеры дат без миллисекунд в `/audit_events`.
Проверено сплошным проходом и чисто: 693 примера против enum, ограничений
длины и диапазона, типов и форматов; 9 примеров операций против их схем;
22 JSON-примера запросов, извлечённых из сгенерированных страниц.
…time
Поведение сверено с реализацией API. Правки затрагивают только тексты
описаний — форма контракта не меняется.
Когда поле бывает null — 38 полей
Из 53 nullable-полей только 15 говорили в описании, когда значение пустое.
Остальные читались как обычные непустые. Условие для каждого выяснено по
реализации, а не угадано. Примеры находок:
- `last_activity_at` — `null`, пока сотрудник ни разу не заходил; это ровно
то же состояние, что `invite_status: sent`
- `Message.thread` и `MessageWebhookPayload.thread` называются одинаково, но
значат противоположное: в REST это тред, созданный К сообщению, в вебхуке —
тред, В КОТОРОМ сообщение отправлено. Теперь это написано в обоих
- `changed_at` — описание было неверным: поле пишется и при создании, так что
у неотредактированного сообщения оно равно дате создания, а `null` бывает
только у сообщений старше самого поля
- `revoked_at` и `expires_in` — всегда `null`: по отозванному токену метод
отдаёт 401, а токены выдаются бессрочно
- `last_used_at` обновляется не чаще раза в час и может отставать
end_time работал не так, как описан
`GET /audit_events` документировал верхнюю границу как исключительную, а бэк
строит `created_at <= end_time` — граница включительная. Выборка за сутки
молча захватывала лишнюю запись ровно на границе.
Пунктуация и примеры
11 нарушений конвенции точек в `@doc` (одно предложение — без точки,
несколько — точка в каждом). Примеры `start_time`/`end_time` приведены к
формату с миллисекундами, который заявляют 32 описания рядом.
Английская спека
Овёрлей ключуется по JSONPath, поэтому правка русского текста его не ломает —
но и не обновляет: `overlay:validate` проверяет только отсутствие кириллицы и
показывал 100%, пока английская дока говорила обратное русской. Обновлены 43
записи. Попутно выяснилось, что у части полей в овёрлее есть дубли целей
(`ApiError.errors.items.*`, `Message.files.items.*`), и запись-дубль
перетирала основную — они выровнены.
Проверено структурным сравнением: полей, где русское описание упоминает
`null`, а английское нет, не осталось.
Отложено как меняющее форму контракта и требующее проверки на живом API: ответы `/profile/status` и `/users/{id}/status` при
отсутствующем статусе отдают голый `null`, а не обёртку `{data: null}`;
`display_avatar_url` и `display_name` объявлены required, но в ответе для
сообщений от людей отсутствуют; `Reaction.name` помечено nullable, хотя
`null` быть не может; `scopes` бота — подмножество 41 из 54 значений enum;
в enum нет `agent_search:messages`; ключи `x-enum-descriptions` через `_`
против значений через `:`; расхождение в `Forwarding.original_thread_id`.
Все шесть SDK компилируются, turbo check зелёный.
Второй проход по сайту доков и артефактам. Правки только в apps/docs —
пакеты не затронуты, поэтому релизных записей не требуется.
Тихая потеря контента
`<CardGroup>` вырезался целиком из всех сгенерированных .md. Обработчик
`Card compact` отрабатывает раньше обработчика группы, тот не находит ни
одного тега и возвращал пустую строку — вместе с уже развёрнутыми пунктами.
Под заголовком «Готовые SDK» в агентской доке было пусто: все шесть ссылок
на SDK и три ссылки установки n8n отсутствовали. Теперь уже развёрнутый
текст сохраняется, а голые nav-карточки по-прежнему отбрасываются.
`lead={<ApiIntroNotes />}` терялся так же беззвучно: обработчик читал только
title и description. Матчить пришлось по всему блоку, а не по атрибутам —
`>` внутри `/>` обрывает захват атрибутов раньше самого свойства.
robots.txt закрывал превью ссылок
`/internal/og` стоял в disallow, а это `og:image` и `twitter:image` каждой
страницы. Twitterbot, Slackbot, LinkedInBot и facebookexternalhit уважают
robots.txt, поэтому любая расшаренная ссылка на dev.pachca.com показывалась
без картинки. Закрытым остался только `/internal/search`.
METHOD-ссылки не были ссылками
`[Текст](POST /messages)` — авторская сокращённая запись, которую резолвит
React-рендерер. В генерации доки её не резолвил никто, а по CommonMark
адрес с пробелом ссылкой не является: 991 вхождение в 88 артефактах было
голым текстом ровно для тех агентов, ради которых эти файлы и делаются.
Резолвится в единственной точке записи, покрывая и llms-full, и skills.
Битые якоря
13 ссылок вели на несуществующие заголовки: транслитерация расходилась с
нашей toSlug (`й→j` вместо `y`, `х→kh` вместо `h`, `щ→shh` вместо `sch`),
плюс два якоря ссылались на переименованные заголовки. Клик по такой ссылке
просто ничего не делает — ни ошибки, ни предупреждения при сборке.
Добавлен гейт `check-anchors`: проверяет и якоря, и что `METHOD /path`
существует в спеке. Проверен на реальном баге — на состоянии до фикса падает.
Им же пойман мёртвый `GET /chats/{id}/messages`, которого в API никогда не
было (правильный метод — `GET /messages`).
Мелкое
- 12 страниц обновлений с релизами, но без файла в content, не попадали в
sitemap: он обходил loadUpdates, а страницы рендерятся по объединению с
releases. Теперь оба используют groupTimelineByDate
- `/.well-known/api-catalog` существовал дважды: статический файл в public
перекрывал роут-хендлер, был беднее на 7 записей и отдавался без
`application/linkset+json`. Статическая копия удалена, версия OpenAPI
исправлена с 3.1 на 3.0
- `icon="ExternalLink"` отсутствовал в iconMap — две карточки были без иконок
- захардкоженная версия n8n `v2.0.6` не совпадала ни с чем; убрана, а не
обновлена — иначе протухнет после следующего релиза
- `/guides/albato.md` возвращал 404: ветка в proxy недостижима, matcher
исключает `.md`. Добавлен редирект, раз доки обещают агентам, что `.md`
можно дописать к любому адресу
- llms.txt ссылался на главную как `/.md`, что работало лишь благодаря
rewrite; теперь `/index.md` напрямую
Проверено чистым: postman, arazzo, дайджесты skills, связность llms.
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
Шесть из 38 полей отложены, а не исправлены: два ответа статуса, display_avatar_url, display_name, Reaction.name и Forwarding.original_thread_id. В записи релиза стояло общее число находок вместо числа внесённых правок.
Поведение проверено запросами к работающему API, а не только чтением кода.
Тело ответов статуса
`GET /profile/status` и `GET /users/{user_id}/status` при отсутствующем статусе
возвращают 200 и ровно четыре байта `null` — без объекта-обёртки. Спека
описывала обёртку с обязательным `data`, то есть врала дважды: и про обёртку,
и про обязательность. Теперь тело описано как обёртка либо `null`.
Добавлено и то, что видно только на живом API: статус с истёкшим `expires_at`
какое-то время продолжает возвращаться целиком, пока не будет очищен. Клиент
не должен считать возвращённый статус актуальным по факту наличия.
display_avatar_url и display_name
Объявлены required, но в ответе для сообщений от сотрудников их нет вообще —
проверено и на создании, и на чтении. Сделаны опциональными и описано, что
поля появляются только у сообщений от ботов.
Reaction.name
Снят `nullable`: реакция возвращает `":+1::skin-tone-1:"`, а неизвестный код
emoji отклоняется валидацией — `null` в этом поле не приходит.
Пустая строка наравне с null
У `last_name`, `department` и `title` «не заполнено» приходит и как `null`,
и как пустая строка. Прошлая формулировка «`null`, если не указано» вводила бы
в заблуждение клиента, проверяющего только на `null`.
Скоупы бота: проверка предотвратила регрессию
Собирался типизировать `scopes` как `OAuthScope[]` вместо `string[]`. Проверка
показала, что делать этого нельзя: у свежесозданного бота набор по умолчанию
содержит 37 скоупов, и один из них — `agent_search:messages` — отсутствует в
enum спеки. Закрытый тип уронил бы разбор ответа `GET /bots` в SDK со строгой
проверкой enum на любом дефолтном боте. Оставлен `string[]`, ограничение
описано словами.
Заодно подтверждено: `group_tags:read` входит в набор по умолчанию, но при
явной установке возвращает 400 «Scopes restricted by role cannot be assigned».
Это расхождение самого API, в спеке оно не чинится — описано обтекаемо.
Английская спека обновлена под все изменённые описания, расхождений
«русское упоминает `null`, английское нет» не осталось.
Все шесть SDK компилируются, turbo check зелёный.
lookinway
force-pushed
the
fix/generator-runtime-bugs-2026-07-25
branch
from
July 26, 2026 17:53
571433e to
dfc41a5
Compare
Функция выглядит как лишний слой: если ключи расширения должны совпадать со
значениями enum, логично поправить источник и убрать нормализацию. Проверено —
поправить нельзя.
TypeSpec не принимает квотированные ключи в объектных литералах: и
`#{ "chats:read": ... }`, и `#{ "simple": ... }` падают с `Property expected`,
то есть дело в самом квотировании, а не в двоеточии. Имя члена enum двоеточие
содержать тоже не может, поэтому эмитируемые ключи никогда не совпадут со
значениями.
Касается единственного enum — `OAuthScope`, у остальных значения являются
корректными идентификаторами и имя совпадает со значением.
Комментарий, чтобы следующий не проходил этот путь заново.
Дата 25 июля не соответствовала действительности: сверка с работающим API и проход по докам шли по 27-е. Проставлена дата завершения. Записи пересобраны по принципу «кому это адресовано». В блоке SDK лежали вперемешку изменения кода и исправления описаний. Читателю, который открывает «SDK v1.0.28», нужны первые: разбор union без общего дискриминатора, опциональность display_avatar_url/display_name, снятый nullable у Reaction.name. Они и остались, шесть строк вместо девяти. Исправления описаний API вынесены в запись обновлений — они одинаково важны и тем, кто SDK не использует вовсе. Там же прямо сказано, что сам API не менялся, менялось описание, и перечислено, что стоит перепроверить в своём коде: граница end_time включительная, ответ методов статуса при отсутствующем статусе, поля сообщения от сотрудников, незаполненные строковые поля. Из блока generator убраны две строки. Инлайновый enum в query-параметре и неоднозначные ключи расширения — правки на случай, который в текущей спеке не возникает: ни один пользователь этого не видел и увидеть не мог. По правилу о dev-only изменениях такие в changelog не попадают.
Запись читалась как признание недосмотра — «сверили с реальным поведением», «описание расходилось», «если вы писали код по прежнему описанию». Читателю нужно, как метод работает, а не как мы это выясняли. Осталась одна вводная строка о том, что работа API не менялась: без неё формулировка «end_time включает границу» читается как изменение поведения.
Обе находки — от прогона против работающего API, а не от чтения кода. Python SDK падал на импорте Реестр дискриминаторов union ссылается на все union'ы, а список импортов в utils.py собирался только по тем, у которых есть свой десериализатор. Итог — NameError при `import pachca.utils`, то есть библиотека не работала целиком. Проверка сборки этого не ловила: `py_compile` только разбирает синтаксис, а обращение к неопределённому имени на уровне модуля компилируется нормально и падает при импорте. Добавлен реальный импорт пакета с заглушкой httpx — проверка не требует зависимостей. Проверена на этой же регрессии: без фикса генератора падает, с фиксом проходит. Лимит длины сообщения Гайд про входящие вебхуки обещал 40 000 символов, гайд для агентов — 40 КБ. Разные единицы, и для кириллицы это отличается вдвое. Замерено двоичным поиском по границе: 40 000 байт. Латиница 40 000 принимается, кириллица 20 000 принимается, 20 001 отклоняется. То есть верна была вторая формулировка. Первая исправлена, добавлено пояснение про кириллицу — иначе русскоязычный интегратор рассчитывает на вдвое больший объём и получает 422. Из проверенного по гайдам остальное сошлось: форма meta у поисковых методов (без has_next), отсутствие meta у /profile, дефолт сортировки сообщений, 401 на неверный токен.
…переписаны Второй заход по утверждениям в гайдах — проверка запросами к работающему API. Лимиты кнопок Заявлено «максимум 8 кнопок в строке» и «максимум 100 кнопок у сообщения». Ни то, ни другое не подтвердилось: 9 кнопок в строке принимаются, а сообщение со 100 кнопками отклоняется. Замерено двоичным поиском: единственное ограничение — 32 строки. Внутри строки API не ограничивает количество (200 кнопок в одной строке проходят), и 32 строки по 200 кнопок — 6400 штук — тоже принимаются. Описано как есть, с оговоркой про читаемость: интерфейс строку не переносит, поэтому больше пяти-шести кнопок в ряд плохо смотрятся на узких экранах. Это рекомендация, а не ограничение API, и теперь так и написано. Удалённое сообщение Гайд утверждал, что после удаления «добрать его по id не получится». На деле метод отвечает 200 и возвращает очищенную запись: пустой content, снятые вложения и реакции, заполненный deleted_at. В списке сообщений её нет. Разница существенная для обработчика вебхука: по событию delete можно дочитать запись и опереться на deleted_at, а не считать сообщение недоступным. Что сошлось Треды: идемпотентность повторного создания, объект thread ровно из id и chat_id, отправка по entity_id без entity_type, root_chat_id, рост updated_at, самостоятельный тред с одним участником и null в message_id, 400 на тред внутри треда, 404 на тред у удалённого сообщения. Боты: требование окончания _bot, access_token только в ответе создания, все четыре значения who_can_add, пустой can_edit, неизменность single_chat при редактировании, отключение вебхука пустой строкой. Очистка кнопок пустым массивом, атомарность превью ссылок, форма meta у поисковых методов, дефолт сортировки сообщений, 401 на неверный токен. Экспорт и журнал аудита проверить не удалось: их скоупы выдаются только владельцу пространства.
Ни одна из этих находок не всплыла на компиляции и юнит-тестах — только на реальных запросах. Загрузка файла падала при успехе S3 отвечает 204 с пустым телом, клиент кладёт в data null, а сгенерированная команда читала из него поле — TypeError на каждой удачной загрузке. Генератор теперь подставляет пустой объект. Заодно замерен настоящий код отказа при неверном порядке полей формы: S3 отвечает 400, а не 403, как было записано. Сама правка порядка подтверждена на живом S3 — файл последним даёт 204, файл первым 400. Ошибки API не разбирались в типизированный ApiError Поле payload описано в спеке как объект с произвольными значениями, но генератор превращал пустую схему additionalProperties в строку. В Go, Kotlin и C# получался словарь строк, а реальный payload ошибки доступа содержит вложенный объект — разбор падал, и клиент получал безымянную обёртку вместо ApiError. Проверено: errors.As в Go теперь находит тип. В Kotlin для произвольного значения используется JsonElement: Any там не сериализуется, kotlinx требует явный сериализатор. Проверка сборки SDK пропускала падения `run` вызывался внутри подоболочки, поэтому fail=1 оставался в ней, а родитель завершался нулём. Скрипт печатал FAIL и тут же «all toolchains OK» — так и случилось, когда сломался Kotlin. Незамеченными проходили падения Go, Python, Kotlin, C# и Swift: пять языков из шести. Добавлен run_in, который изолирует каталог, но проверяет код возврата в родителе. Проверено на реальной поломке — скрипт возвращает 1. Что подтвердилось живыми вызовами CLI: отказ при --timeout меньше 1, объекты в CSV как JSON, пагинация --all, полный цикл загрузки файла. SDK на TypeScript и Go: профиль, списки, завершение пагинации, разбор ошибок.
Прогон всех шести SDK против живого API. Swift и Kotlin проверены впервые: разбор union без дискриминатора и типизированный ApiError с вложенным payload теперь подтверждены исполнением, а не компиляцией. Спека: - User.first_name объявлено nullable — API отдаёт null, пока приглашённый сотрудник не завершил регистрацию (invite_status = sent). - CustomProperty.value объявлено nullable — null, если поле не заполнено. Оба поля ломали разбор ответа GET /users в языках со строгой типизацией: Swift падал на первом же пользователе с незаполненным дополнительным полем. Примеры SDK: - Во всех шести языках добавлен разбор события видеозвонка в истории вебхуков. В Kotlin и Swift это была ошибка компиляции (when/switch обязаны быть исчерпывающими), в остальных — молчаливый пропуск события. - Swift и Go: поле загрузки файла переименовано в ContentDisposition. - TypeScript: PachcaClient.stub принимает объект-оверрайд, а не позиционные аргументы; fileType — значение перечисления FileType. - Python: чтение переменных окружения перенесено внутрь main(), добавлен guard `if __name__ == "__main__"`. - C#: возвращён в репозиторий Examples.csproj, без которого описанный в Program.cs запуск `dotnet run -- <example>` не работал. Гейт: - scripts/build-sdks.sh и .github/workflows/sdk.yml собирают примеры всех шести языков. Раньше собирался только исходник SDK, а примеры лежат в соседнем каталоге — они ломались молча, хотя на них ссылаются README. - Go-примеры собираются по одному: каждый из них package main в общем каталоге, поэтому `go build ./...` сообщал только «main redeclared». - Проверка Python в CI усилена импортом вместо py_compile.
Полная проверка v2: все 17 ресурсов и 76 операций прогнаны через установленную ноду в локальном n8n. Каждая операция бьёт по пути, который есть в спеке, с нужным методом и авторизацией. Починено: - User → Update Avatar и Chat → Download Export собирали адрес запроса из объекта выбора сущности, а не из идентификатора: в URL уходило "[object Object]". Обе ветки обходили общий сборщик URL и читали параметр напрямую, минуя разбор локатора. Выгрузка чата ломалась и в опубликованной версии. - Выбор сотрудника открывался пустым: список подтягивался только после ввода текста. Теперь без фильтра показывается список сотрудников, как это уже работает у чатов. Правки внесены в генератор: SharedRouter.ts собирается им, ручное изменение стёрлось бы следующей сборкой. Совместимость проверена настоящим обновлением: workflow, сохранённый на опубликованной версии с числовым userId, после установки этой сборки выполняется с тем же результатом, а в редакторе значение сохраняется и видно в поле. Тесты: прогон локаторной формы по всем операциям с выбором сущности — на состоянии до правок падает на обеих сломанных. Плюс проверка, что старое числовое значение остаётся рабочим. Проверено и расхождений не найдено: операции UI против таблицы маршрутов, маршруты против openapi.yaml, значения выпадающих списков против enum спеки, 19 сопоставлений событий триггера против схем вебхуков, порядок полей multipart при загрузке файла, набор параметров против опубликованной версии.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Четыре прохода по репозиторию: проверка накопившихся замечаний по генераторам и рантайму, поиск семантически неверных примеров, проход по ссылкам и контенту доков и сверка спеки с живым API.
Из проверенных находок три оказались ложными и не правились — они разобраны ниже.
Критичное
Журнал аудита не читался ни одним SDK.
AuditEventDetailsUnionне имеет общего поля-дискриминатора: селектор лежит на родительском объекте (event_key). Определение дискриминатора сканировало только первого члена union и всегда возвращало имя поля, поэтому эмиттеры подставляли несуществующее полеtype. Go возвращал ошибку и ронял разбор всего ответаGET /audit_events, C# и Kotlin падали на дублирующихся дискриминаторах, Swift — на отсутствующем ключе, Python молча отдавал пустые детали.Теперь поле принимается, только если литерал есть у каждого члена; иначе дискриминатора нет и вариант выбирается по составу полей payload. В Go у такого union появилось поле
Rawс исходным JSON — неизвестная форма больше не ошибка разбора. Заодно Python перестал брать «первого члена списка» для union с дискриминатором, что чинит и блоки представлений.Загрузка файлов возвращала 403. Генератор CLI клал файл в multipart-форму первым, а S3 игнорирует поля после файла — подпись и ключ не доезжали. Бинарное поле теперь эмитится последним, как в рукописном
upload.ts.Записи повторялись при ошибке сервера. В CLI гейт «только идемпотентные методы» стоял лишь на сетевых ошибках, но не на 503; в n8n 5xx ретраился для любого метода. Закоммиченный
POST /messagesмог отправить сообщение до четырёх раз. 429 по-прежнему повторяется для любого метода — запрос отклонён до обработки.Узел n8n открывался на необратимой операции. Ресурс Bot по умолчанию вставал на
Recreate own bot token— ротацию собственного токена, срабатывавшую от простого Execute. ТеперьGet Many.Спека против живого API
Последний проход — проверка запросами к работающему API, а не чтением кода.
Ответы статуса описаны неверно.
GET /profile/statusиGET /users/{user_id}/statusпри отсутствующем статусе возвращают200и ровно четыре байтаnull— без объекта-обёртки. Спека объявляла обёртку с обязательнымdata, то есть ошибалась и про обёртку, и про обязательность. Добавлено и то, что видно только на живом API: статус с истёкшимexpires_atкакое-то время продолжает возвращаться целиком.display_avatar_urlиdisplay_nameобъявлены required, но их нет в ответе. Для сообщений от сотрудников ключи отсутствуют полностью — проверено и на создании, и на чтении. Сделаны опциональными.Reaction.nameне бываетnull— снятnullable.Два обязательных поля приходят
null.first_nameпуст, пока приглашённый сотрудник не завершил регистрацию, аvalueдополнительного поля — пока поле не заполнено. Оба объявлены обязательными, поэтому разбор ответаGET /usersпадал целиком в языках со строгой типизацией: Swift не проходил дальше первого же пользователя с незаполненным дополнительным полем. Оба поля сделаны nullable.Проверка предотвратила регрессию. Готовилась типизация
scopesкакOAuthScope[]вместоstring[]. Оказалось, что набор по умолчанию у свежесозданного бота содержит скоуп, которого нет в enum спеки, — закрытый тип уронил бы разборGET /botsв SDK со строгой проверкой enum на любом дефолтном боте. Оставленstring[], ограничение описано словами.Примеры, которые врали
POST /threadsпоказывал выдуманныеmessage_idиmessage_chat_id, хотя спека рядом объявляет для нихnull, а описание метода это прямо обещает. Причина: оба генератора примеров шли сразу в схему и никогда не смотрели наexampleуровня media type — все девять рукописных примеров молча выбрасывались. Теперь пример из спеки имеет приоритет, как и требует семантика OpenAPI.Опечатки в примерах формы ломали round-trip: страница открытия формы предлагала отправить
timeoff_reguest_form, а вебхук возвращалtimeoff_request_form— проверка на равенство никогда не срабатывала. Вprivate_metadataвебхука лежал{'timeoff_id':4378}с одинарными кавычками:JSON.parseна задокументированном значении бросает исключение. Опечатка успела разойтись по спеке, гайду, postman-коллекции и сниппетам SDK — вычищена везде.Тихая потеря контента в доках
<CardGroup>вырезался целиком из всех сгенерированных.md: обработчик карточек отрабатывает раньше обработчика группы, тот не находит ни одного тега и возвращал пустоту вместе с уже развёрнутыми пунктами. В агентской доке под «Готовые SDK» было пусто — шесть ссылок на SDK и три ссылки установки n8n отсутствовали. Похожим образом терялсяlead={<ApiIntroNotes />}.[Текст](POST /messages)— авторская сокращённая запись, которую резолвит React-рендерер. В генерации доки её не резолвил никто, а по CommonMark адрес с пробелом ссылкой не является: 991 вхождение в 88 артефактах было голым текстом ровно для тех агентов, ради которых эти файлы и делаются.robots.txtзакрывал/internal/og— этоog:imageкаждой страницы, и краулеры соцсетей его уважают, поэтому любая расшаренная ссылка шла без превью.Плюс 13 битых якорей (транслитерация расходилась с
toSlug), 187 двойных точек перед «Пример:», 12 страниц обновлений мимо sitemap, дубльapi-catalog, перекрывавший более полный роут-хендлер.Описания полей
У 32 полей, которые API возвращает как
null, не было сказано, когда это происходит. Попутно выяснилось, чтоMessage.threadиMessageWebhookPayload.threadназываются одинаково, но значат противоположное (тред, созданный к сообщению, против треда, в котором сообщение отправлено), а описаниеchanged_atбыло неверным: поле пишется и при создании. Уlast_name,departmentиtitle«не заполнено» приходит и какnull, и как пустая строка — проверено по списку сотрудников.end_timeуGET /audit_eventsдокументировался как исключающий верхнюю границу, а реализация включает её — выборка за сутки молча захватывала лишнюю запись.Английская спека чуть не разошлась с русской молча: овёрлей ключуется по JSONPath, поэтому правка русского его не ломает, но и не обновляет, а
overlay:validateпроверяет только отсутствие кириллицы. Обновлено 55 записей; попутно найдены дубли целей, где запись-дубль перетирала основную.Ложные находки
/oauth/token/info— не баг: у/profileесть обязательный scope, то есть предложенная «безопасная» замена строже и отвергала бы узкоскоупные токены ботов. Зафиксировано комментарием в генератореisArrayParamне видит$ref-массивы — неверно, парсер резолвит$refдо генератора. Реальный пробел был вallOfи['array','null']Примеры SDK не собирались ничем
Прогон всех шести SDK против живого API потребовал сначала собрать их примеры — и выяснилось, что сборка их никогда не касалась. Каждый SDK строится из своего каталога, а примеры лежат в соседнем, поэтому ломались молча, хотя именно на них ссылаются README.
whenиswitchпо закрытому типу обязаны быть исчерпывающими. В остальных четырёх языках событие просто молча проваливалось вunknown.Content_Disposition, которого в SDK нет со времён перехода на текущий генератор.PachcaClient.stubвызывался семью позиционными аргументами, хотя принимает один объект-оверрайд. ПлюсfileType: "file"вместо значения перечисленияFileType.Examples.csprojлежал в.gitignore.Program.csописывает запускdotnet run -- <example>, но в чистом клоне собрать примеры было нечем. Проект возвращён в репозиторий.if __name__ == "__main__". Чтение перенесено внутрьmain().Гейт закрыт:
scripts/build-sdks.shиsdk.ymlсобирают примеры всех шести языков. Go-примеры собираются по одному — каждый из нихpackage mainв общем каталоге, поэтомуgo build ./...сообщал только «main redeclared» и ни одной настоящей ошибки. Проверка Python в CI усилена импортом вместоpy_compile:py_compileразбирает синтаксис и пропускает импорт типа, которого больше нет в спеке.Защита от повтора
Добавлены гейты в
turbo check:check-examples— каждый рукописный пример спеки обязан дословно присутствовать в сгенерированной докеcheck-anchors— якоря разрешаются в существующие заголовки,METHOD /pathсуществует в спекеunion-no-discriminatorи guard на пустое значение дискриминатора в генератореcheck-releaseбольше не выдаёт ошибку npm за «первый релиз»;check-changelog-syncпри недоступном base падает, а не пропускает;check-generated-syncпокрывает корневые артефактыКаждый гейт проверен на реальном баге: на состоянии до фикса падает.
Что осталось за рамками
Требует решения на стороне API или продукта, поэтому отложено, а не сделано наугад:
OAuthScopeнетagent_search:messages— пока его нет,scopesнельзя типизироватьgroup_tags:readвыдаётся боту по умолчанию, но при явной установке возвращает400x-enum-descriptionsзаписаны через_, а значения enum — через:Forwarding.original_thread_idведёт себя не так, как описан; какое поведение задумано — вопрос к бэкендуРелиз
Ветка тянет публикацию четырёх пакетов — CLI 2026.7.2, n8n 2.0.16, generator 1.1.10, SDK 1.0.28. Записи в changelog'ах и
releases.jsonна месте,check-releaseверсии принимает.Проверки
turbo check— 25/25 (2179 тестов CLI, 430 n8n, 28 генератора, линт, типы, knip, формат).check-generated-sync,check-examples,check-anchorsчистые. Все шесть SDK и их примеры компилируются — TypeScript, Go, Python, Kotlin, C#, Swift.Компиляции недостаточно, поэтому каждый SDK прогнан против живого API: профиль, пагинация, типизированная ошибка с вложенным
payloadи разбор union без дискриминатора. Для Kotlin, Swift и C# этот код исполнялся впервые — до сих пор он существовал только в виде успешной компиляции.