Большинство плагин-систем начинаются одинаково: объявляется интерфейс IPlugin, все его реализуют, типовая система следит за контрактом. Чисто, привычно — и создаёт зависимость, которая будет преследовать вас ещё долго.
В KB Labs нет интерфейса плагина. Вместо этого — утиная типизация: если у пакета есть манифест с schema: 'kb.plugin/3', он плагин. Без базового класса. Без implements. Без вызова регистрации в рантайме.
Проблема с интерфейсами
Интерфейс IPlugin связывает каждый плагин с ядром на этапе компиляции. Это означает:
- Каждый плагин импортирует ядро (или общий пакет, реэкспортирующий интерфейс)
- Изменение интерфейса — это одновременно ломающее изменение для всех плагинов сразу
- Интерфейс должен быть объединением всех возможных возможностей плагина — команды, роуты, вебхуки, крон, Studio-страницы — что делает его либо огромным, либо бесполезным
- Циклические зависимости неизбежны, когда ядру нужно загружать плагины, зависящие от ядра
Мы через это прошли. Интерфейс вырос до 14 опциональных методов. Плагины реализовывали 2–3 из них, остальные — заглушки. Тип-система была довольна; дизайн — нет.
Что мы делаем вместо
Плагин — это любой npm-пакет, чей манифест объявляет schema: 'kb.plugin/3'. Манифест — простой объект, не экземпляр класса, не возврат фабричной функции.
// plugins/commit/entry/src/manifest.ts
export const manifest = {
schema: 'kb.plugin/3',
id: '@kb-labs/commit',
version: '1.0.0',
display: { name: 'Commit Generator', description: '...' },
permissions: combinePermissions()
.with(gitWorkflowPreset)
.with(kbPlatformPreset)
.build(),
cli: {
commands: [
{ name: 'commit', handler: './dist/commands/commit.js' }
]
}
};Возможности объявляются как секции. Плагин с CLI-командами имеет секцию cli. Плагин с REST-роутами — секцию rest. Плагин с воркфлоу-хендлерами — секцию workflows. Объявляешь только то, что используешь:
interface ManifestV3 {
schema: 'kb.plugin/3';
id: string;
version: string;
display?: DisplayMetadata;
permissions?: PermissionSpec;
cli?: { commands: CliCommandDecl[] };
rest?: RestConfig;
ws?: WebSocketConfig;
workflows?: { handlers: WorkflowHandlerDecl[] };
webhooks?: { handlers: WebhookHandlerDecl[] };
jobs?: JobsConfig;
cron?: { schedules: CronDecl[] };
studio?: StudioConfig;
lifecycle?: LifecycleHooks;
}Как работает дискавери без интерфейса
DiscoveryManager в core-discovery читает файл marketplace.lock — единственный источник истины об установленных плагинах. Для каждой записи манифест разрешается через три пути с фолбэком:
- Поле
kb.manifestвpackage.json - Файл
kb.plugin.jsonв корне пакета - Дефолтный экспорт
dist/index.js
Манифест валидируется через тайп-гард (isManifestV3), возможности извлекаются проверкой наличия секций. Плагин с секциями cli и workflows регистрируется одновременно как поставщик CLI-команд и воркфлоу-хендлеров — автоматически, без явного вызова регистрации.
Что получилось
- Нулевая связь с ядром. Единственная compile-time зависимость плагина —
@kb-labs/sdk(типы и хелперы). Ядро рантайма он никогда не импортирует. - Аддитивная эволюция. Добавить новую возможность (например,
cron) — значит добавить новую опциональную секцию в ManifestV3. Существующие плагины ничего не меняют. - Никаких церемоний регистрации. Установи пакет, добавь в lock-файл, перезапусти. Платформа найдёт сама.
- Тестирование в изоляции. Манифест — простой объект. Можно юнит-тестировать плагин без запуска всей платформы.
Утиная типизация — не универсальный паттерн. Но для плагин-систем, где ядро должно быть максимально стабильным, а расширения — максимально свободными, это именно тот паттерн, который позволяет обеим сторонам эволюционировать не ломая друг друга.