«У вас розетка неправильная.» — «Нет, это у вас вилка неправильная.»
Этот документ описывает, как именно связка @tv/extension-sdk + tv-api устроена так, чтобы при любой проблеме у стороннего разработчика модуля можно было однозначно показать где именно проходит граница ответственности — без споров про вилку и розетку.
Документ предназначен для:
- Внутренней команды — как защититься от претензий вида «у вас платформа сломалась».
- Сторонних разработчиков модулей — как понять, что именно платформа гарантирует и где начинается их зона ответственности.
TL;DR — Принцип семи замков
Между разработчиком модуля и платформой стоит семь явных контрактов. Каждый из них имеет однозначно проверяемый артефакт (схема, снимок, версия) и однозначное место, где его нарушение ловится. Если что-то не работает — это всегда видно, нарушение какого именно контракта:
| # | Контракт | Артефакт | Где ловится нарушение | Чья ответственность |
|---|---|---|---|---|
| 1 | Манифест модуля | module-manifest.json + Zod-схема | tv-sdk validate в CI разработчика | Разработчик (если CI не запускал) |
| 2 | Уровни стабильности (@stable / @experimental / @internal) | JSDoc-тэги + STABILITY.md | Документировано, semver-связан | Разработчик (если использовал @experimental без подписки на риск) |
| 3 | Каталог разрешений | permissions.snapshot.json (в SDK) | tv-sdk snapshot-permissions + CI | Платформа гарантирует фиксацию |
| 4 | OpenAPI-снимок REST API | openapi.snapshot.json (в SDK) | compareApiSpec() в CI | Платформа гарантирует совместимость |
| 5 | Каталог событий и пакетов | tv-events.snapshot.json (в SDK) | tv-sdk check-events | Платформа гарантирует версионность |
| 6 | Liveness (биение) | @tv/extension-sdk/heartbeat | Building OS показывает статус модуля | Разработчик (нет биения → STOPPED) |
| 7 | Изоляция контекста через PlatformContext | createMockPlatformContext() | Юнит-тесты модуля | Разработчик (если тестов нет) |
Каждый контракт — это тот самый штамп с подписью, на который можно сослаться в споре. Если разработчик нарушил пункт 1 — спор закончен в его CI ещё до того, как код доехал до платформы.
Контракт 1: Манифест модуля
Каждый модуль обязан положить в корень module-manifest.json, который:
- Проходит Zod-валидацию из
@tv/extension-sdk/manifest. - Соответствует JSON-схеме
manifest.schema.json, версионированной по semver. - Проходит проверку CLI:
npx @tv/extension-sdk validate module-manifest.json.
$ tv-sdk validate module-manifest.json
✓ Manifest is valid (version 1.0.0)
$ echo $?
0$ tv-sdk validate broken-manifest.json
✗ permissions[0].subject: must be one of [building.spaces, building.elements, ...]
$ echo $?
1Три места, где манифест проверяется:
- CI разработчика —
tv-sdk validateв его собственном пайплайне. - Реестр модулей при публикации —
tv-module-catalogотвергает невалидный манифест. - Building OS при загрузке федерации — Module Federation shell делает финальную проверку.
Аргумент против претензии «ваша платформа не загрузила мой модуль»:
Какая ошибка валидации манифеста была в вашем CI? Если CI прошёл — приложите вывод
tv-sdk validate. Если не запускали — это нарушение пункта 1 контракта.
Контракт 2: Уровни стабильности
STABILITY.md явно разделяет API SDK на три уровня:
| Уровень | Обещание | Как помечается |
|---|---|---|
@stable | Никаких ломающих изменений в пределах мажорной версии. Удаление — только через 6 месяцев деприкейта. | JSDoc-тэг на barrel-файле или на конкретном экспорте |
@experimental | Может измениться или исчезнуть в любом релизе. CHANGELOG фиксирует изменения, заранее не предупреждаем. | JSDoc-тэг |
@internal | Не экспортируется. Может быть удалено молча. | Подчёркивание + отсутствие в dist/index.d.ts |
Всё, что явно не помечено как @experimental или @internal, считается @stable по умолчанию.
Аргумент против претензии «вы поломали мой модуль»:
Вы использовали
@stable-символ или@experimental? Если@stable— мы выпустили мажорную версию, посмотрите CHANGELOG и миграционный гайд. Если@experimental— вы подписались на риск явным образом, см. пункт «Уровни стабильности» в README.
Контракт 3: Каталог разрешений
В SDK поставляется артефакт permissions.snapshot.json — заморожённый, версионированный список всех разрешений платформы.
- Генерируется CLI:
tv-sdk snapshot-permissions. - Источник истины —
core/tv-api/src/permissions/permission-registry.service.ts. - CI на стороне tv-platform запрещает любые расхождения между источником и снимком.
- Разработчик модуля читает разрешения только через
import { ... } from '@tv/extension-sdk/permissions'.
Что это даёт:
- Разрешения не могут измениться молча. Платформа не может «случайно переименовать» субъект разрешения — это будет сразу же видно в diff
permissions.snapshot.json. - Разработчик может офлайн проверить корректность
permissions[]в манифесте против каталога. Не нужен живой tv-api.
Аргумент против претензии «вы у меня забрали права»:
Когда вы публиковали модуль, какая версия
permissions.snapshot.jsonбыла у вас в lock-файле? Зафиксируйте — все разрешения, использованные на момент сборки, мы обязаны поддерживать до конца окна деприкейта (6 месяцев для@stable).
Контракт 4: OpenAPI-снимок REST API
В SDK поставляется openapi.snapshot.json — полная OpenAPI 3 спецификация tv-api на момент релиза SDK.
- Контракт-чек в CI tv-api (
compareApiSpec) ловит любое ломающее изменение: удаление эндпоинта, изменение типа поля, добавление обязательного параметра. - При обнаружении ломающего изменения PR в tv-api не пройдёт без явной регенерации снимка и обновления CHANGELOG.
- Добавление новых эндпоинтов классифицируется как additive — модули, написанные против старого снимка, продолжают работать.
Что это даёт:
- Контракт API защищён в обе стороны: платформа не может удалить или поломать существующий REST-эндпоинт случайно.
- Разработчик может посмотреть точную форму запроса/ответа офлайн, не запуская tv-api.
Аргумент против претензии «ваш API вернул не то, что описано в документации»:
Документация на момент сборки вашего модуля — это
openapi.snapshot.jsonиз той версии SDK, которая у вас в lock-файле. Откройте, найдите эндпоинт, сравните с фактическим ответом. Если форма ответа отличается — это наш баг (откроем тикет, исправим, возможна компенсация по SLA). Если совпадает — это вопрос к интеграции на вашей стороне.
Контракт 5: Каталог событий и пакетов
Каждый модуль декларирует в манифесте:
"events": {
"publishes": [{ "name": "cafm.work-order.created", "version": "1.0.0" }],
"subscribes": [{ "name": "building.alarm.triggered", "version": "1.0.0" }]
}В SDK поставляется tv-events.snapshot.json — каталог событий с версионированными схемами payload'ов. CLI tv-sdk check-events проверяет:
- Каждое событие, на которое модуль подписывается, существует в каталоге.
- Каждое событие, которое модуль публикует, либо существует, либо это новое модульное событие — тогда оно должно идти с собственной schema-декларацией.
- Версии совместимы.
Аргумент против претензии «моё событие не дошло»:
Покажите вывод
tv-sdk check-eventsдля вашей версии SDK. Если он зелёный, а событие не пришло — это инцидент на нашей стороне, заведите тикет. Если красный — нужно поправить версию или schema, см. наш гайд по событийным контрактам.
Контракт 6: Heartbeat — кто умер, тот и виноват
@tv/extension-sdk/heartbeat поставляет startHeartbeat() — однострочный декоратор:
TvHeartbeatModule.register({ slug: 'cafm' })Бэкенд модуля раз в ~30 секунд (с джиттером ±20%) пингует Platform Registry: «я жив». Building OS красит модуль в боковой панели:
- Зелёный — heartbeat пришёл за последние 60 секунд.
- Жёлтый — позже 60 секунд, но раньше 5 минут.
- Красный — heartbeat не приходил больше 5 минут (модуль скорее всего стоит).
Что это даёт:
- Когда модуль не работает, индикатор показывает тот самый модуль, а не «всё сломалось».
- Пользователь и оператор не приходят к нам с жалобой «у вас платформа упала», когда упал один модуль из двадцати.
Аргумент против претензии «у вас платформа лежит»:
Какие именно модули красные в Building OS? Если красные все — тогда да, инцидент на платформе. Если красный конкретный модуль — это его heartbeat не идёт, источник проблемы — в нём.
Контракт 7: PlatformContext + mock
Модуль работает только через PlatformContext, который ему передаёт платформа:
import { usePlatformContext, useBuilding } from '@tv/extension-sdk/react';
export function WorkOrderList() {
const { api } = usePlatformContext();
const building = useBuilding();
return useQuery({
queryKey: ['work-orders', building.id],
queryFn: () => api.get(`/api/v1/buildings/${building.id}/work-orders`),
});
}api — это уже аутентифицированный, scoped по тенанту HTTP-клиент. Модуль не работает с токенами, не строит URL'ы, не знает про Keycloak.
Для тестов поставляется createMockPlatformContext():
const ctx = createMockPlatformContext({
user: { ...defaultUser, roles: ['manager'] },
});
render(<PlatformProvider value={ctx}><WorkOrderList /></PlatformProvider>);Что это даёт:
- Разработчик не может «случайно» сходить мимо контракта (например, дёрнуть tv-api напрямую с собственным токеном).
- В тестах разработчик использует точно тот же объект контекста, что и в продакшене, только заполненный фейковыми данными.
Аргумент против претензии «у меня в проде не работает, а на тестах работает»:
Использовали ли вы
createMockPlatformContext()или собрали свойPlatformContextруками? Если последнее — расхождение с продом ожидаемо. Если первое — пришлите воспроизведение, разберёмся, инцидент или баг SDK.
Версионирование как мета-контракт
SDK следует строгому semver начиная с 1.0:
- Мажорная версия — ломающие изменения
@stableповерхности. - Минорная — аддитивные расширения.
- Патч — фиксы без изменения публичного API.
Манифест модуля декларирует совместимые версии:
"minCoreVersion": ">=2.0.0",
"sdkVersion": "^1.4.0"Платформа отвергает запуск модуля, чей sdkVersion выходит за пределы поддерживаемого ядром диапазона. Несовместимость — это hard error, а не загадочное падение в рантайме.
Соответственно при разборе любого инцидента первый вопрос — на какой версии SDK собран модуль — отвечается за секунду.
Что мы НЕ обещаем (и почему это тоже контракт)
Открытость про границы важна не меньше, чем сами гарантии:
- Производительность модуля — не наша зона. SDK даёт инструменты (heartbeat, telemetry), но если модуль делает N+1 запросов — это его авторам.
- Совместимость двух модулей друг с другом — только если они оба декларируют это через
manifest.dependencies. Иначе модули не знают о существовании друг друга. - Работа на устаревших версиях SDK —
@stableгарантия действует 6 месяцев после деприкейта; после этого срока возможны фиксы только за отдельную плату. - UI-консистентность — модуль волен рисовать что угодно. Мы предоставляем
@tv/design-tokensи@tv/ui— рекомендуем использовать, но не enforced.
Эти границы прописаны явно, чтобы в спорной ситуации не возникало вопроса «а кто должен был это сделать».
Что делать, если что-то всё-таки не работает
Чек-лист для разработчика:
- Какая версия SDK?
cat node_modules/@tv/extension-sdk/package.json | jq .version - Валиден ли манифест?
npx tv-sdk validate module-manifest.json - Все ли разрешения и события из каталога?
tv-sdk check-events - Использовал ли
@experimental-символ? Поищите@experimentalв JSDoc используемых API. - Идёт ли heartbeat? Зайдите в Building OS, посмотрите цвет своего модуля в боковой панели.
- Используете ли
createMockPlatformContext()в тестах?
Если на все ответ «да, всё чисто» — это наш инцидент: заведите тикет с приложением вывода чек-листа. Мы признаём и фиксим.
Если хотя бы одно «нет» — стоит сначала пройти этот пункт. В большинстве случаев проблема уйдёт на этом шаге.
Резюме
Семь контрактов — это не бюрократия, это способ закончить спор за минуту, а не за неделю. У каждого контракта есть:
- Артефакт (JSON-файл, JSDoc-тэг, CLI-команда).
- Место, где его нарушение ловится автоматически.
- Сторона, которая отвечает за его соблюдение.
Когда разработчик приходит с проблемой, разговор идёт не «у вас вилка / у вас розетка», а:
«Покажите вывод пункта 1. Спасибо. Покажите пункт 3. Так, здесь у вас расхождение со снимком — обновитесь, и должно поехать.»
Конец спора. Время от заявки до ответа — минуты.
Версия документа
Версия 1.0, синхронизирована с @tv/extension-sdk@1.0.0. Изменения отслеживаются в CHANGELOG.md. Источник истины по уровням стабильности — STABILITY.md. Источник истины по версионированию SDK — ADR-016.