From af8c4efb4a415e4bdeae0151a8d29cd71af61916 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Mon, 13 Jul 2026 11:51:37 +0000 Subject: [PATCH] fix(cli): render actionable analysis errors --- src/application/DirectAnalysis.ts | 19 +++++++++++-------- src/cli.ts | 6 +++++- src/cliEvidenceCommands.ts | 28 +++++++++++++--------------- src/main.ts | 5 +++-- 4 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/application/DirectAnalysis.ts b/src/application/DirectAnalysis.ts index 141f71686..6c92b39dd 100644 --- a/src/application/DirectAnalysis.ts +++ b/src/application/DirectAnalysis.ts @@ -6,6 +6,7 @@ import { silentLogger, type Logger } from "../logger.js"; import { createEvidence } from "../domain/evidence.js"; import type { NativeToolName } from "../contracts/nativeToolContracts.js"; import type { ArtifactToolName } from "../contracts/artifactToolContracts.js"; +import { projectAnalysisError, type AnalysisError } from "../domain/errors.js"; const WORKFLOW_PROVIDER = { id: "rea-workflow", @@ -47,8 +48,7 @@ export const runSessionStatus = async ( logger: Logger = silentLogger, ): Promise => { const config = parseConfig(process.env); - if (!config.ok) - return { error: config.error._tag, message: config.error.message }; + if (!config.ok) return cliError(config.error); const session = createBinarySession(config.value, logger); try { return session.status(); @@ -64,13 +64,11 @@ const runAnalysis = async ( logger: Logger, ): Promise => { const config = parseConfig(process.env); - if (!config.ok) - return { error: config.error._tag, message: config.error.message }; + if (!config.ok) return cliError(config.error); const session = createBinarySession(config.value, logger); try { const opened = await session.open(path); - if (!opened.ok) - return { error: opened.error._tag, message: opened.error.message }; + if (!opened.ok) return cliError(opened.error); if ( tool === "binary_overview" || tool === "analyze_function" || @@ -91,7 +89,7 @@ const runAnalysis = async ( limitations: ["Derived by an REA composed workflow."], }, ) - : { error: result.error._tag, message: result.error.message }; + : cliError(result.error); } const result = await session.execute(tool, arguments_); return result.ok @@ -107,8 +105,13 @@ const runAnalysis = async ( locations: result.value.locations, }, ) - : { error: result.error._tag, message: result.error.message }; + : cliError(result.error); } finally { await session.close(); } }; + +const cliError = (error: AnalysisError): JsonValue => ({ + error: "Analysis failed", + ...projectAnalysisError(error), +}); diff --git a/src/cli.ts b/src/cli.ts index 2b027e9aa..8e3378190 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -18,6 +18,7 @@ import { parseConfig } from "./config.js"; import { importReferenceSource } from "./application/ReferenceSourceImport.js"; import { registerEvidenceCommands } from "./cliEvidenceCommands.js"; import { registerProcessCommands } from "./cliProcessCommands.js"; +import { projectAnalysisError } from "./domain/errors.js"; /** * Build the one-shot Incur CLI without starting Hopper at import time. @@ -374,7 +375,10 @@ const registerReferenceSourceCommand = ( logCliCommand(logger, "import-reference-source", async () => { const config = parseConfig(process.env); if (!config.ok) - return { error: config.error._tag, message: config.error.message }; + return { + error: "Import failed", + ...projectAnalysisError(config.error), + }; const imported = await importReferenceSource({ root: args.root, caller: "rea-cli", diff --git a/src/cliEvidenceCommands.ts b/src/cliEvidenceCommands.ts index c602d4309..59a5cf2e1 100644 --- a/src/cliEvidenceCommands.ts +++ b/src/cliEvidenceCommands.ts @@ -8,6 +8,8 @@ import { import { parseConfig } from "./config.js"; import { logCliCommand } from "./cliLogging.js"; import type { Logger } from "./logger.js"; +import type { JsonValue } from "./domain/jsonValue.js"; +import { projectAnalysisError, type AnalysisError } from "./domain/errors.js"; /** Register filesystem-gated Evidence v2 commands. */ export const registerEvidenceCommands = ( @@ -22,15 +24,12 @@ export const registerEvidenceCommands = ( run: ({ args }) => logCliCommand(logger, "evidence-import", async () => { const config = parseConfig(process.env); - if (!config.ok) - return { error: config.error._tag, message: config.error.message }; + if (!config.ok) return cliError(config.error); const imported = await importEvidenceBundleCommand( args.path, config.value.evidenceFilePolicy, ); - return imported.ok - ? imported.value - : { error: imported.error._tag, message: imported.error.message }; + return imported.ok ? imported.value : cliError(imported.error); }), }); cli.command("evidence-export", { @@ -45,17 +44,14 @@ export const registerEvidenceCommands = ( run: ({ args, options }) => logCliCommand(logger, "evidence-export", async () => { const config = parseConfig(process.env); - if (!config.ok) - return { error: config.error._tag, message: config.error.message }; + if (!config.ok) return cliError(config.error); const exported = await exportEvidenceBundleCommand( args.source, args.output, options.overwrite, config.value.evidenceFilePolicy, ); - return exported.ok - ? exported.value - : { error: exported.error._tag, message: exported.error.message }; + return exported.ok ? exported.value : cliError(exported.error); }), }); cli.command("compare", { @@ -72,8 +68,7 @@ export const registerEvidenceCommands = ( run: ({ args, options }) => logCliCommand(logger, "compare", async () => { const config = parseConfig(process.env); - if (!config.ok) - return { error: config.error._tag, message: config.error.message }; + if (!config.ok) return cliError(config.error); const compared = await compareEvidenceBundlesCommand({ leftPath: args.left, rightPath: args.right, @@ -81,9 +76,12 @@ export const registerEvidenceCommands = ( limit: options.limit, policy: config.value.evidenceFilePolicy, }); - return compared.ok - ? compared.value - : { error: compared.error._tag, message: compared.error.message }; + return compared.ok ? compared.value : cliError(compared.error); }), }); }; + +const cliError = (error: AnalysisError): JsonValue => ({ + error: "Analysis failed", + ...projectAnalysisError(error), +}); diff --git a/src/main.ts b/src/main.ts index 4956551cb..9f1d00d83 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,6 +12,7 @@ import { parseConfig } from "./config.js"; import { createBinarySession } from "./application/runtime.js"; import { createServer } from "./server/createServer.js"; import { createLogger } from "./logger.js"; +import { projectAnalysisError } from "./domain/errors.js"; /** * Start the long-lived MCP adapter and install idempotent shutdown handlers. @@ -21,7 +22,7 @@ import { createLogger } from "./logger.js"; export const run = async (): Promise => { const config = parseConfig(process.env); if (!config.ok) { - process.stderr.write(`${config.error._tag}: ${config.error.message}\n`); + process.stderr.write(`${projectAnalysisError(config.error).message}\n`); return 1; } const logger = createLogger("mcp", config.value.logLevel); @@ -38,7 +39,7 @@ export const run = async (): Promise => { { errorTag: opened.error._tag }, "Initial target failed to open", ); - process.stderr.write(`${opened.error._tag}: ${opened.error.message}\n`); + process.stderr.write(`${projectAnalysisError(opened.error).message}\n`); return 1; } }