/** * capability-contract.ts — ONE composed view of every provider route and * generation backend videoclaw knows about. * * There are four separate id spaces here and they stay separate on purpose: * * - **core routes** (`PROVIDER_ROUTE_IDS`) — the live `vclaw video` execution * routes, with their capability envelope and their prerequisites. * - **cinema routes** (`CINEMA_PROVIDER_ROUTES`) — the durable Cinema queue's * provider-neutral routes. Deliberately NOT merged with the core ids: every * Cinema route declares `fallbackPolicy: 'forbidden'` and its own transport, * and ADR 0001 (no silent fallback) depends on a route never being confusable * with a materially different one. * - **audio backends** — the music/TTS/SFX registries. * - **finish backends** — the upscale/finishing pass. * * The manifest is a READ-ONLY projection: it composes what those registries * already declare and never re-derives or re-guesses a value. It is pure — no * env reads, no filesystem, no clock — so callers get the same manifest every * time and tests can compare consumers against it. * * `capability-contract.test.ts` is the drift gate: it asserts the key sets match * `PROVIDER_ROUTE_IDS`, that the specialist lane subsets in `batch-queue.ts` and * `execution-runtime.ts` equal the routes declaring those lanes, that the * provider-status report / creator-ui capability surface / MCP tool description * agree with the manifest, and that the fenced tables in `docs/CAPABILITIES.md` * and `docs/PROVIDER_PLATFORM.md` list every id. */ import { MUSIC_BACKENDS, SFX_BACKENDS, TTS_BACKENDS, backendRequiredEnvVars, } from './audio-platform/registry.js'; import type { AudioBackendDescriptor, AudioBackendKind } from './audio-platform/types.js'; import { CINEMA_PROVIDER_ROUTES, CINEMA_PROVIDER_ROUTE_REGISTRY, type CinemaProviderRouteId, } from './cinema-provider-projection.js'; import { FINISH_BACKEND_IDS, type FinishBackend } from './finish.js'; import { DEFAULT_PROVIDER_REGISTRY } from './provider-platform/registry.js'; import { ROUTE_CAPABILITIES, type RouteCapabilities } from './provider-platform/route-capabilities.js'; import { ROUTE_PREREQUISITES, type RoutePrerequisites } from './provider-platform/route-prerequisites.js'; import { PROVIDER_ROUTE_IDS, type ProviderRouteId } from './provider-platform/types.js'; import type { CinemaProviderAccountClass, CinemaProviderMediaKind } from './cinema-provider-types.js'; /** One live `vclaw video` execution route: what it is, can do, and needs. */ export interface CapabilityManifestCoreRoute { id: ProviderRouteId; displayName: string; provider: string; /** `direct` (first-party API) or `useapi` (aggregator). */ path: string; summary: string; capabilities: RouteCapabilities; prerequisites: RoutePrerequisites; } /** One durable-Cinema-queue route. Its id space never merges with the core one. */ export interface CapabilityManifestCinemaRoute { id: CinemaProviderRouteId; transportId: string; accountClass: CinemaProviderAccountClass; mediaKinds: CinemaProviderMediaKind[]; fallbackPolicy: 'forbidden'; description: string; } /** One music / TTS / SFX backend, with its env prerequisites already expanded. */ export interface CapabilityManifestAudioBackend { id: string; kind: AudioBackendKind; displayName: string; summary: string; /** Declared `requiredEnv` plus the pool vars the `requires*` gates stand for. */ requiredEnvVars: string[]; } /** The composed contract. Bump `schemaVersion` on any shape change. */ export interface CapabilityManifest { schemaVersion: 1; coreRoutes: CapabilityManifestCoreRoute[]; cinemaRoutes: CapabilityManifestCinemaRoute[]; audioBackends: CapabilityManifestAudioBackend[]; finishBackends: FinishBackend[]; /** Every id in the manifest, flattened — the allowlist for drift scans. */ allKnownIds: string[]; } function audioBackend(backend: AudioBackendDescriptor): CapabilityManifestAudioBackend { return { id: backend.id, kind: backend.kind, displayName: backend.displayName, summary: backend.summary, requiredEnvVars: backendRequiredEnvVars(backend), }; } /** * Compose the capability manifest from the registries that already declare each * fact. Pure and deterministic: same output every call, no env or I/O. * * Core routes are emitted in `DEFAULT_PROVIDER_REGISTRY` order (the order * `vclaw video providers` prints); cinema routes, audio backends and finish * backends keep their own registries' declared order. */ export function buildCapabilityManifest(): CapabilityManifest { const coreRoutes: CapabilityManifestCoreRoute[] = DEFAULT_PROVIDER_REGISTRY.map((route) => ({ id: route.id, displayName: route.displayName, provider: route.provider, path: route.path, summary: route.summary, capabilities: ROUTE_CAPABILITIES[route.id], prerequisites: ROUTE_PREREQUISITES[route.id], })); const cinemaRoutes: CapabilityManifestCinemaRoute[] = CINEMA_PROVIDER_ROUTES.map((routeId) => { const route = CINEMA_PROVIDER_ROUTE_REGISTRY[routeId]; return { id: route.routeId, transportId: route.transportId, accountClass: route.accountClass, mediaKinds: route.mediaKinds, fallbackPolicy: route.fallbackPolicy, description: route.description, }; }); const audioBackends: CapabilityManifestAudioBackend[] = [ ...MUSIC_BACKENDS.map(audioBackend), ...TTS_BACKENDS.map(audioBackend), ...SFX_BACKENDS.map(audioBackend), ]; const finishBackends: FinishBackend[] = [...FINISH_BACKEND_IDS]; return { schemaVersion: 1, coreRoutes, cinemaRoutes, audioBackends, finishBackends, allKnownIds: [ ...PROVIDER_ROUTE_IDS, ...cinemaRoutes.map((route) => route.id), ...audioBackends.map((backend) => backend.id), ...finishBackends, ], }; }