Утиная типизация для плагинов: почему мы убили интерфейс IPlugin

← All posts

Большинство плагин-систем начинаются одинаково: объявляется интерфейс IPlugin, все его реализуют, типовая система следит за контрактом. Чисто, привычно — и создаёт зависимость, которая будет преследовать вас ещё долго.

В KB Labs нет интерфейса плагина. Вместо этого — утиная типизация: если у пакета есть манифест с schema: 'kb.plugin/3', он плагин. Без базового класса. Без implements. Без вызова регистрации в рантайме.

Проблема с интерфейсами

Интерфейс IPlugin связывает каждый плагин с ядром на этапе компиляции. Это означает:

Мы через это прошли. Интерфейс вырос до 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 — единственный источник истины об установленных плагинах. Для каждой записи манифест разрешается через три пути с фолбэком:

  1. Поле kb.manifest в package.json
  2. Файл kb.plugin.json в корне пакета
  3. Дефолтный экспорт dist/index.js

Манифест валидируется через тайп-гард (isManifestV3), возможности извлекаются проверкой наличия секций. Плагин с секциями cli и workflows регистрируется одновременно как поставщик CLI-команд и воркфлоу-хендлеров — автоматически, без явного вызова регистрации.

Что получилось

Утиная типизация — не универсальный паттерн. Но для плагин-систем, где ядро должно быть максимально стабильным, а расширения — максимально свободными, это именно тот паттерн, который позволяет обеим сторонам эволюционировать не ломая друг друга.