Skip to content

«У вас розетка неправильная.» — «Нет, это у вас вилка неправильная.»

Этот документ описывает, как именно связка @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Платформа гарантирует фиксацию
4OpenAPI-снимок REST APIopenapi.snapshot.json (в SDK)compareApiSpec() в CIПлатформа гарантирует совместимость
5Каталог событий и пакетовtv-events.snapshot.json (в SDK)tv-sdk check-eventsПлатформа гарантирует версионность
6Liveness (биение)@tv/extension-sdk/heartbeatBuilding OS показывает статус модуляРазработчик (нет биения → STOPPED)
7Изоляция контекста через PlatformContextcreateMockPlatformContext()Юнит-тесты модуляРазработчик (если тестов нет)

Каждый контракт — это тот самый штамп с подписью, на который можно сослаться в споре. Если разработчик нарушил пункт 1 — спор закончен в его CI ещё до того, как код доехал до платформы.


Контракт 1: Манифест модуля

Каждый модуль обязан положить в корень module-manifest.json, который:

  1. Проходит Zod-валидацию из @tv/extension-sdk/manifest.
  2. Соответствует JSON-схеме manifest.schema.json, версионированной по semver.
  3. Проходит проверку CLI: npx @tv/extension-sdk validate module-manifest.json.
bash
$ tv-sdk validate module-manifest.json
 Manifest is valid (version 1.0.0)
$ echo $?
0
bash
$ 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: Каталог событий и пакетов

Каждый модуль декларирует в манифесте:

json
"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 проверяет:

  1. Каждое событие, на которое модуль подписывается, существует в каталоге.
  2. Каждое событие, которое модуль публикует, либо существует, либо это новое модульное событие — тогда оно должно идти с собственной schema-декларацией.
  3. Версии совместимы.

Аргумент против претензии «моё событие не дошло»:

Покажите вывод tv-sdk check-events для вашей версии SDK. Если он зелёный, а событие не пришло — это инцидент на нашей стороне, заведите тикет. Если красный — нужно поправить версию или schema, см. наш гайд по событийным контрактам.


Контракт 6: Heartbeat — кто умер, тот и виноват

@tv/extension-sdk/heartbeat поставляет startHeartbeat() — однострочный декоратор:

ts
TvHeartbeatModule.register({ slug: 'cafm' })

Бэкенд модуля раз в ~30 секунд (с джиттером ±20%) пингует Platform Registry: «я жив». Building OS красит модуль в боковой панели:

  • Зелёный — heartbeat пришёл за последние 60 секунд.
  • Жёлтый — позже 60 секунд, но раньше 5 минут.
  • Красный — heartbeat не приходил больше 5 минут (модуль скорее всего стоит).

Что это даёт:

  • Когда модуль не работает, индикатор показывает тот самый модуль, а не «всё сломалось».
  • Пользователь и оператор не приходят к нам с жалобой «у вас платформа упала», когда упал один модуль из двадцати.

Аргумент против претензии «у вас платформа лежит»:

Какие именно модули красные в Building OS? Если красные все — тогда да, инцидент на платформе. Если красный конкретный модуль — это его heartbeat не идёт, источник проблемы — в нём.


Контракт 7: PlatformContext + mock

Модуль работает только через PlatformContext, который ему передаёт платформа:

tsx
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():

ts
const ctx = createMockPlatformContext({
  user: { ...defaultUser, roles: ['manager'] },
});
render(<PlatformProvider value={ctx}><WorkOrderList /></PlatformProvider>);

Что это даёт:

  • Разработчик не может «случайно» сходить мимо контракта (например, дёрнуть tv-api напрямую с собственным токеном).
  • В тестах разработчик использует точно тот же объект контекста, что и в продакшене, только заполненный фейковыми данными.

Аргумент против претензии «у меня в проде не работает, а на тестах работает»:

Использовали ли вы createMockPlatformContext() или собрали свой PlatformContext руками? Если последнее — расхождение с продом ожидаемо. Если первое — пришлите воспроизведение, разберёмся, инцидент или баг SDK.


Версионирование как мета-контракт

SDK следует строгому semver начиная с 1.0:

  • Мажорная версия — ломающие изменения @stable поверхности.
  • Минорная — аддитивные расширения.
  • Патч — фиксы без изменения публичного API.

Манифест модуля декларирует совместимые версии:

json
"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.

Эти границы прописаны явно, чтобы в спорной ситуации не возникало вопроса «а кто должен был это сделать».


Что делать, если что-то всё-таки не работает

Чек-лист для разработчика:

  1. Какая версия SDK? cat node_modules/@tv/extension-sdk/package.json | jq .version
  2. Валиден ли манифест? npx tv-sdk validate module-manifest.json
  3. Все ли разрешения и события из каталога? tv-sdk check-events
  4. Использовал ли @experimental-символ? Поищите @experimental в JSDoc используемых API.
  5. Идёт ли heartbeat? Зайдите в Building OS, посмотрите цвет своего модуля в боковой панели.
  6. Используете ли createMockPlatformContext() в тестах?

Если на все ответ «да, всё чисто» — это наш инцидент: заведите тикет с приложением вывода чек-листа. Мы признаём и фиксим.

Если хотя бы одно «нет» — стоит сначала пройти этот пункт. В большинстве случаев проблема уйдёт на этом шаге.


Резюме

Семь контрактов — это не бюрократия, это способ закончить спор за минуту, а не за неделю. У каждого контракта есть:

  • Артефакт (JSON-файл, JSDoc-тэг, CLI-команда).
  • Место, где его нарушение ловится автоматически.
  • Сторона, которая отвечает за его соблюдение.

Когда разработчик приходит с проблемой, разговор идёт не «у вас вилка / у вас розетка», а:

«Покажите вывод пункта 1. Спасибо. Покажите пункт 3. Так, здесь у вас расхождение со снимком — обновитесь, и должно поехать.»

Конец спора. Время от заявки до ответа — минуты.


Версия документа

Версия 1.0, синхронизирована с @tv/extension-sdk@1.0.0. Изменения отслеживаются в CHANGELOG.md. Источник истины по уровням стабильности — STABILITY.md. Источник истины по версионированию SDK — ADR-016.

Built on the Tango Vision platform.