From f7250fe35f20108cbb1b278422d4f35e44f35d17 Mon Sep 17 00:00:00 2001 From: Kaoru0822 Date: Thu, 15 Oct 2026 07:37:20 -0700 Subject: [PATCH] fix(kuna): say why Kuna cannot answer instead of a generic message (#4113) Kuna capability gaps such as "no code at this address" or "no function node at this address" were built without a user message, so MCP and the CLI showed only "This analysis is unavailable for the current target. Choose another analysis or target." and kept the reason in details. One Kuna helper now presents the reason as the message for every such gap, and the unsupported-operation reason names call relationships and xrefs, which Kuna now answers. Refs #1957 Co-authored-by: Claude Opus 5.5 --- src/kuna/KunaProjectReader.ts | 5 +-- src/kuna/KunaProvider.ts | 23 ++--------- src/kuna/KunaReferenceQueries.ts | 5 +-- src/kuna/KunaSessionClient.ts | 21 ++-------- src/kuna/KunaUnavailable.ts | 18 +++++++++ .../kuna/kunaCommandFailures.test.ts | 38 +++++++++++++++++++ 6 files changed, 68 insertions(+), 42 deletions(-) create mode 100644 src/kuna/KunaUnavailable.ts diff --git a/src/kuna/KunaProjectReader.ts b/src/kuna/KunaProjectReader.ts index d4608b11f2..6b534ef6e0 100644 --- a/src/kuna/KunaProjectReader.ts +++ b/src/kuna/KunaProjectReader.ts @@ -4,7 +4,6 @@ import { join } from "node:path"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; import { - AnalysisCapabilityUnavailableError, AnalysisCancelledError, AnalysisResourceConstraintError, } from "../domain/analysisErrorCore.js"; @@ -19,6 +18,7 @@ import { } from "./KunaProjectFiles.js"; import { KUNA_PROVIDER_ID } from "./KunaRelease.js"; import { KUNA_FUNCTION_TEXT_BYTES } from "./KunaLimits.js"; +import { kunaUnavailable } from "./KunaUnavailable.js"; /** The exact producing range for one facet, including a failed C block. */ export interface KunaProjectFileSpan { @@ -134,8 +134,7 @@ export class KunaProjectReader { const span = index.value.code.get(address); if (span === undefined) return err( - new AnalysisCapabilityUnavailableError( - KUNA_PROVIDER_ID, + kunaUnavailable( operation, `Kuna's export lists no code at ${address}; import slots and data entries have no listing.`, ), diff --git a/src/kuna/KunaProvider.ts b/src/kuna/KunaProvider.ts index ac5c0bc5f8..c2a103431a 100644 --- a/src/kuna/KunaProvider.ts +++ b/src/kuna/KunaProvider.ts @@ -8,10 +8,7 @@ import type { ProviderAvailability, ProviderTargetSupport, } from "../application/AnalysisProvider.js"; -import { - AnalysisCancelledError, - AnalysisCapabilityUnavailableError, -} from "../domain/analysisErrorCore.js"; +import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; import { createAnalysisProfile, type AnalysisProfileCommitment, @@ -33,6 +30,7 @@ import { import { KUNA_COMMAND_REMEDIATION, KUNA_PROVIDER_ID } from "./KunaRelease.js"; import { KunaRunSlots } from "./KunaRunSlots.js"; import { KunaSessionClient } from "./KunaSessionClient.js"; +import { kunaUnavailable } from "./KunaUnavailable.js"; /** Architectures REA verified Kuna on: x86-64 and AArch64 ELF and Mach-O, plus ARM Thumb. */ const VERIFIED_ARCHITECTURES: ReadonlySet = new Set([ @@ -169,14 +167,7 @@ export class KunaProvider implements AnalysisProviderCandidate { ...(options?.signal === undefined ? {} : { signal: options.signal }), }); if (!installation.ok) - return err( - new AnalysisCapabilityUnavailableError( - KUNA_PROVIDER_ID, - "open_binary", - installation.error.reason, - { userMessage: installation.error.reason }, - ), - ); + return err(kunaUnavailable("open_binary", installation.error.reason)); const { value } = installation; return ok({ profile: createAnalysisProfile( @@ -218,13 +209,7 @@ export class KunaProvider implements AnalysisProviderCandidate { if (!configuration.ok) return { execute: async (operation) => - err( - new AnalysisCapabilityUnavailableError( - KUNA_PROVIDER_ID, - operation, - configuration.error.reason, - ), - ), + err(kunaUnavailable(operation, configuration.error.reason)), close: async () => ok(null), }; return new KunaSessionClient(target, profile, { diff --git a/src/kuna/KunaReferenceQueries.ts b/src/kuna/KunaReferenceQueries.ts index ffc8b45b4d..44256f705e 100644 --- a/src/kuna/KunaReferenceQueries.ts +++ b/src/kuna/KunaReferenceQueries.ts @@ -1,7 +1,6 @@ import type { AnalysisError } from "../domain/analysisErrorBase.js"; import { AnalysisCancelledError, - AnalysisCapabilityUnavailableError, AnalysisResourceConstraintError, } from "../domain/analysisErrorCore.js"; import type { JsonValue } from "../domain/jsonValue.js"; @@ -21,6 +20,7 @@ import { } from "./KunaReferenceFiles.js"; import { KUNA_PROVIDER_ID } from "./KunaRelease.js"; import type { KunaPendingQuery, KunaQueryWork } from "./KunaQueryWork.js"; +import { kunaUnavailable } from "./KunaUnavailable.js"; /** Charge raw JSON bytes conservatively; retained projections are smaller. */ const XREF_CACHE_BYTES = 16 * 1024 * 1024; @@ -158,8 +158,7 @@ export class KunaReferenceQueries { const node = graph.nodes.get(address); if (node === undefined) return err( - new AnalysisCapabilityUnavailableError( - KUNA_PROVIDER_ID, + kunaUnavailable( operation, `Kuna's graph has no function node at ${address}; its relationships are unknown.`, ), diff --git a/src/kuna/KunaSessionClient.ts b/src/kuna/KunaSessionClient.ts index 07254050a2..e8a44a7627 100644 --- a/src/kuna/KunaSessionClient.ts +++ b/src/kuna/KunaSessionClient.ts @@ -69,6 +69,7 @@ import { KunaProjectReader, type KunaProjectFileSpan, } from "./KunaProjectReader.js"; +import { kunaUnavailable } from "./KunaUnavailable.js"; /** How often a request waiting on the project export reports that it still waits. */ const WAIT_PROGRESS_INTERVAL_MS = 10_000; @@ -278,13 +279,7 @@ export class KunaSessionClient implements AnalysisClient { options: ExecutionOptions = {}, ): Promise> { if (this.#closing) - return err( - new AnalysisCapabilityUnavailableError( - KUNA_PROVIDER_ID, - operation, - "This Kuna session is closed.", - ), - ); + return err(kunaUnavailable(operation, "This Kuna session is closed.")); return this.#own( this.#execute(operation, parameters, { ...options, @@ -312,8 +307,7 @@ export class KunaSessionClient implements AnalysisClient { return err(new AnalysisCancelledError(operation)); if (operation !== "health" && !isKunaOperation(operation)) return err( - new AnalysisCapabilityUnavailableError( - KUNA_PROVIDER_ID, + kunaUnavailable( operation, "Kuna answers procedure and string queries, pseudocode, assembly and references; this operation needs another provider.", ), @@ -1229,14 +1223,7 @@ export class KunaSessionClient implements AnalysisClient { this.#installation ??= inspectKunaInstallation(this.#environment).then( (inspected): Result => { if (!inspected.ok) - return err( - new AnalysisCapabilityUnavailableError( - KUNA_PROVIDER_ID, - "health", - inspected.error.reason, - { userMessage: inspected.error.reason }, - ), - ); + return err(kunaUnavailable("health", inspected.error.reason)); const committed = this.#profile?.parameters["kuna_sha256"]; if (committed !== undefined && committed !== inspected.value.sha256) return err( diff --git a/src/kuna/KunaUnavailable.ts b/src/kuna/KunaUnavailable.ts new file mode 100644 index 0000000000..514c7c486c --- /dev/null +++ b/src/kuna/KunaUnavailable.ts @@ -0,0 +1,18 @@ +import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js"; +import type { AnalysisErrorOptions } from "../domain/analysisErrorBase.js"; +import { KUNA_PROVIDER_ID } from "./KunaRelease.js"; + +/** + * A Kuna capability gap whose reason is what the caller needs to read. Without + * a user message the projection shows only "choose another analysis or + * target", which hides facts such as "no code at this address". + */ +export const kunaUnavailable = ( + operation: string, + reason: string, + options: AnalysisErrorOptions = {}, +): AnalysisCapabilityUnavailableError => + new AnalysisCapabilityUnavailableError(KUNA_PROVIDER_ID, operation, reason, { + ...options, + userMessage: reason, + }); diff --git a/tests/boundary/providers/kuna/kunaCommandFailures.test.ts b/tests/boundary/providers/kuna/kunaCommandFailures.test.ts index 30aec2b7a1..426f18eb8a 100644 --- a/tests/boundary/providers/kuna/kunaCommandFailures.test.ts +++ b/tests/boundary/providers/kuna/kunaCommandFailures.test.ts @@ -80,3 +80,41 @@ it("reports a graph beyond REA's buffer once per session instead of rebuilding i expect(await client.close()).toEqual({ ok: true, value: null }); } }); + +it("says why Kuna cannot answer instead of a generic unavailable message", async () => { + const { open } = await session("rea-kuna-unavailable-reasons-"); + const client = open(); + try { + const cases = [ + { + operation: "procedure_assembly", + parameters: { procedure: "0x2000" }, + message: + "Kuna's export lists no code at 0x2000; import slots and data entries have no listing.", + }, + { + operation: "procedure_callers", + parameters: { procedure: "0x1004" }, + message: + "Kuna's graph has no function node at 0x1004; its relationships are unknown.", + }, + { + operation: "list_segments", + parameters: {}, + message: + "Kuna answers procedure and string queries, pseudocode, assembly and references; this operation needs another provider.", + }, + ] as const; + for (const { operation, parameters, message } of cases) { + const result = await client.execute(operation, parameters); + if (result.ok) throw new Error(`${operation} unexpectedly succeeded`); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "capability_unavailable", + message, + retryable: false, + }); + } + } finally { + expect(await client.close()).toEqual({ ok: true, value: null }); + } +});