diff --git a/src/application/DirectAnalysis.ts b/src/application/DirectAnalysis.ts index 839a758f3..c4d130f63 100644 --- a/src/application/DirectAnalysis.ts +++ b/src/application/DirectAnalysis.ts @@ -560,13 +560,8 @@ const analysisProfileForRoute = ( const providerIdentityForRoute = ( route: SessionProviderRoute, operation: string, -): ProviderIdentity => { - const selected = - route.capabilities?.get(operation)?.provider ?? route.identity; - return route.profile?.provider.id === selected.id - ? route.profile.provider - : selected; -}; +): ProviderIdentity => + route.capabilities?.get(operation)?.provider ?? route.identity; const analysisProfileForEvidence = ( session: BinarySession, diff --git a/src/application/binary/AnalysisProviderRegistry.ts b/src/application/binary/AnalysisProviderRegistry.ts index 43e6bbb9c..5b7e78813 100644 --- a/src/application/binary/AnalysisProviderRegistry.ts +++ b/src/application/binary/AnalysisProviderRegistry.ts @@ -50,6 +50,8 @@ export interface AnalysisProviderBinding { readonly identity: ProviderIdentity & { readonly version: string }; readonly selectionSource: "request" | "environment" | "auto-single-candidate"; readonly profile: AnalysisProfileCommitment; + /** The provider's capabilities, attributed to its resolved identity. */ + readonly capabilities: readonly CapabilityDescriptor[]; } /** Complete result used for both a bound and intentionally unbound target. */ @@ -361,18 +363,25 @@ const selectedResult = ( }, statuses: readonly AnalysisProviderCandidateStatus[], ): AnalysisProviderSelection => { + const identity = evaluated.profile.provider; + const capabilities = evaluated.status.capabilities.map((capability) => ({ + ...capability, + provider: structuredClone(identity), + })); const selectedStatus = { ...evaluated.status, - provider: structuredClone(evaluated.profile.provider), + provider: structuredClone(identity), selected: true, + capabilities, } satisfies AnalysisProviderCandidateStatus; return { requestedProviderId, binding: { provider: evaluated.candidate, - identity: structuredClone(evaluated.profile.provider), + identity: structuredClone(identity), selectionSource: source, profile: structuredClone(evaluated.profile), + capabilities, }, candidates: replaceCandidate(statuses, selectedStatus), }; diff --git a/src/application/binary/BinarySession.ts b/src/application/binary/BinarySession.ts index d5841aa93..da8d8e679 100644 --- a/src/application/binary/BinarySession.ts +++ b/src/application/binary/BinarySession.ts @@ -142,14 +142,7 @@ export class BinarySession } } } - const profile = this.#active?.profile; - return structuredClone( - profile !== null && - profile !== undefined && - profile.provider.id === selected.id - ? profile.provider - : selected, - ); + return structuredClone(selected); } /** External live state cannot be replayed by a direct or composed CLI workflow. */ diff --git a/src/application/binary/SessionProviderRouter.ts b/src/application/binary/SessionProviderRouter.ts index dac5c183e..cf5653537 100644 --- a/src/application/binary/SessionProviderRouter.ts +++ b/src/application/binary/SessionProviderRouter.ts @@ -125,7 +125,11 @@ const selectableRoute = ( ? undefined : new CompositeProvider(providers); const identity = provider?.identity() ?? emptyProviderIdentity(); - const capabilities = capabilityMap(provider?.capabilities() ?? []); + // The binding attributes deep-provider capabilities to the resolved version. + const capabilities = capabilityMap([ + ...auxiliaryProviders.flatMap((auxiliary) => auxiliary.capabilities()), + ...(binding?.capabilities ?? []), + ]); return { identity, capabilities, diff --git a/tests/composition/analysis-sessions/providerBinding.test.ts b/tests/composition/analysis-sessions/providerBinding.test.ts index b6d726fdb..9ed21abfa 100644 --- a/tests/composition/analysis-sessions/providerBinding.test.ts +++ b/tests/composition/analysis-sessions/providerBinding.test.ts @@ -191,6 +191,38 @@ describe("target-bound provider routing", () => { }); }); +describe("selected provider capability provenance", () => { + it("labels selected-provider capabilities with the resolved identity", async () => { + const target = await databaseTarget(); + const alpha = deepProvider("alpha"); + const session = selectableSession([alpha.provider], auxiliaryProvider()); + + expect((await session.open(target, { providerId: "alpha" })).ok).toBe(true); + const resolved = { id: "alpha", name: "alpha provider", version: "1" }; + const status = bindingStatus(session); + expect(status).toMatchObject({ + analysis_provider_binding: { provider: resolved }, + analysis_provider_candidates: [ + { + provider: resolved, + selected: true, + capabilities: [{ provider: resolved }], + }, + ], + }); + expect( + z + .object({ capabilities: z.array(z.object({ provider: z.unknown() })) }) + .parse(status) + .capabilities.map(({ provider }) => provider), + ).toEqual([ + { id: "alpha", name: "alpha provider", version: "1" }, + { id: "auxiliary", name: "auxiliary provider", version: "1" }, + ]); + await session.close(); + }); +}); + interface ClientState { readonly calls: string[]; closed: boolean;