import type { AnalysisProviderSelector } from "../../contracts/providerSelection.js"; import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js"; import type { BinaryTarget } from "../../domain/binaryTargetTypes.js"; import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js"; import { ProviderSelectionError } from "../../domain/providerSelectionError.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { err, ok, type Result } from "../../domain/result.js"; import type { AnalysisClient, AnalysisClientContext, AnalysisProvider, CapabilityDescriptor, ProviderIdentity, } from "../AnalysisProvider.js"; import { AnalysisProviderRegistry, type AnalysisProviderBinding, type AnalysisProviderCandidateStatus, type AnalysisProviderSelection, } from "./AnalysisProviderRegistry.js"; import { CompositeProvider } from "./CompositeProvider.js"; /** Immutable operation routes and binding metadata for one target transition. */ export interface SessionProviderRoute { readonly identity: ProviderIdentity; readonly capabilities: ReadonlyMap; readonly profile: AnalysisProfileCommitment | null; readonly binding: AnalysisProviderBinding | null; readonly selection: AnalysisProviderSelection | undefined; createClient( target: BinaryTarget, context: AnalysisClientContext, ): AnalysisClient; } /** Resolve one registry-selected deep provider and disjoint auxiliary families. */ export class SessionProviderRouter { private constructor( private readonly registry: AnalysisProviderRegistry, private readonly auxiliaryProviders: readonly AnalysisProvider[], private readonly route: SessionProviderRoute, ) {} /** Create selection-aware routing while keeping auxiliary families disjoint. */ static selectable( registry: AnalysisProviderRegistry, auxiliaryProviders: readonly AnalysisProvider[], ): SessionProviderRouter { const identities = [ ...registry.identities(), ...auxiliaryProviders.map((provider) => provider.identity()), ]; assertUniqueProviderIds(identities); const auxiliaryOperations = assertDisjoint(auxiliaryProviders); for (const operation of registry.declaredOperations()) if (auxiliaryOperations.has(operation)) throw new TypeError( `Deep and auxiliary providers both declare operation ${operation}`, ); const initialRoute = selectableRoute(auxiliaryProviders, undefined); return new SessionProviderRouter( registry, [...auxiliaryProviders], initialRoute, ); } /** Target-free routes and discovery metadata. */ initialRoute(): SessionProviderRoute { return this.route; } /** Resolve target routes before any provider client is created. */ async resolve( target: BinaryTarget, providerId?: AnalysisProviderSelector, signal?: AbortSignal, ): Promise> { const resolutionOptions = signal === undefined ? undefined : { signal }; const selected = await this.registry.select( target, providerId, resolutionOptions, ); return selected.ok ? ok(selectableRoute(this.auxiliaryProviders, selected.value)) : selected; } /** Candidate status authoritative for the active or target-free route. */ candidateStatuses( route: SessionProviderRoute, ): readonly AnalysisProviderCandidateStatus[] { return route.selection?.candidates ?? this.registry.candidates(); } /** Produce exact candidate rejections for an intentionally unbound route. */ unboundOperationError( operation: string, route: SessionProviderRoute, ): ProviderSelectionError | undefined { if ( route.binding !== null || route.selection === undefined || !this.registry.declaredOperations().includes(operation) ) return undefined; return this.registry.unboundOperationError(operation, route.selection); } } const selectableRoute = ( auxiliaryProviders: readonly AnalysisProvider[], selection: AnalysisProviderSelection | undefined, ): SessionProviderRoute => { const binding = selection?.binding ?? null; const providers = [ ...auxiliaryProviders, ...(binding === null ? [] : [binding.provider]), ]; const provider = binding === null && providers.length === 1 ? providers[0] : providers.length === 0 ? undefined : new CompositeProvider(providers); const identity = provider?.identity() ?? emptyProviderIdentity(); // The binding attributes deep-provider capabilities to the resolved version. const capabilities = capabilityMap([ ...auxiliaryProviders.flatMap((auxiliary) => auxiliary.capabilities()), ...(binding?.capabilities ?? []), ]); return { identity, capabilities, profile: binding?.profile ?? null, binding, selection, createClient: (target, context) => provider?.createClient(target, binding?.profile, context) ?? emptyClient(identity), }; }; const emptyClient = (identity: ProviderIdentity): AnalysisClient => ({ execute: (operation) => operation === "health" ? Promise.resolve( ok({ result: null, rawResult: null, provider: identity, limitations: [], locations: [], subject: null, }), ) : Promise.resolve( err( new AnalysisCapabilityUnavailableError( identity.id, operation, "operation is not declared by this provider set", ), ), ), close: () => Promise.resolve(ok(null)), }); const emptyProviderIdentity = (): ProviderIdentity => ({ id: "composite:none", name: "REA composite analysis provider", version: null, }); const capabilityMap = ( capabilities: readonly CapabilityDescriptor[], ): ReadonlyMap => new Map(capabilities.map((descriptor) => [descriptor.operation, descriptor])); const assertDisjoint = ( providers: readonly AnalysisProvider[], ): ReadonlySet => { const routes = new Set(); for (const provider of providers) for (const { operation } of provider.capabilities()) { if (routes.has(operation)) throw new TypeError( `Multiple auxiliary providers declare operation ${operation}`, ); routes.add(operation); } return routes; }; const assertUniqueProviderIds = ( identities: readonly ProviderIdentity[], ): void => { const ids = new Set(); for (const identity of identities) { if (ids.has(identity.id)) throw new TypeError(`Duplicate configured provider ID: ${identity.id}`); ids.add(identity.id); } };