diff --git a/src/application/firmware/FirmwareAnalysisService.ts b/src/application/firmware/FirmwareAnalysisService.ts index e8281b0f6b..1d0e25a606 100644 --- a/src/application/firmware/FirmwareAnalysisService.ts +++ b/src/application/firmware/FirmwareAnalysisService.ts @@ -9,12 +9,11 @@ import { } from "../../domain/firmware/firmwareAnalysis.js"; import { AnalysisCancelledError, - AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import type { Evidence } from "../../domain/evidence.js"; -import { projectInputIssues } from "../../domain/inputIssueProjection.js"; +import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { err, type Result } from "../../domain/result.js"; import { completeProviderEvidence } from "../ProviderOperationExecution.js"; @@ -39,10 +38,11 @@ export class FirmwareAnalysisService { const parsed = firmwareRequestSchema.safeParse({ operation, input }); if (!parsed.success) return err( - new AnalysisInputError( + analysisInputErrorFromIssues( operation, + parsed.error.issues, + { operation, input }, { cause: parsed.error }, - projectInputIssues(parsed.error.issues, { operation, input }), ), ); const executed = await this.provider.execute(parsed.data, options); diff --git a/src/application/process/ProcessCli.ts b/src/application/process/ProcessCli.ts index 3a8f64d1ab..76d75a73d4 100644 --- a/src/application/process/ProcessCli.ts +++ b/src/application/process/ProcessCli.ts @@ -149,10 +149,11 @@ const parseCaptureEvidence = (input: unknown, side: "left" | "right") => { const failure = parsed.error; switch (failure.kind) { case "invalid-evidence": - throw new AnalysisInputError( + throw analysisInputErrorFromIssues( "compare_process_captures", + failure.cause.issues, + input, { cause: failure.cause }, - projectInputIssues(failure.cause.issues, input), ); case "wrong-workflow": throw processInputFailure( diff --git a/src/artifacts/apple/DylibResolutionReader.ts b/src/artifacts/apple/DylibResolutionReader.ts index 30e1205275..dc73bbbd43 100644 --- a/src/artifacts/apple/DylibResolutionReader.ts +++ b/src/artifacts/apple/DylibResolutionReader.ts @@ -13,7 +13,7 @@ import { type DylibTreeView, type MachoImageFacts, } from "../../domain/apple/dylibResolution.js"; -import { projectInputIssues } from "../../domain/inputIssueProjection.js"; +import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { ArtifactReaderFailure } from "../ArtifactReader.js"; import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js"; import { consumeWithOrderedReads } from "../OrderedReads.js"; @@ -109,10 +109,11 @@ export const traceDylibResolution = async (options: { }): Promise => { const parsed = dylibResolutionInputSchema.safeParse(options.parameters); if (!parsed.success) - throw new AnalysisInputError( + throw analysisInputErrorFromIssues( OPERATION, + parsed.error.issues, + options.parameters, { cause: parsed.error }, - projectInputIssues(parsed.error.issues, options.parameters), ); const resources = options.resourceScope ?? new ArtifactResourceScope(); return await resources.run(async () => { diff --git a/src/cli/browserAdvancedCommands.ts b/src/cli/browserAdvancedCommands.ts index 4eb08e01fe..d1b2c61a33 100644 --- a/src/cli/browserAdvancedCommands.ts +++ b/src/cli/browserAdvancedCommands.ts @@ -20,7 +20,10 @@ import { import { compareBrowserScenariosInputSchema } from "../domain/browserScenarioDiffValues.js"; import { browserScenarioCaptureSchema } from "../domain/browserScenarioCapture.js"; import { compareWebCapturesInputSchema } from "../domain/webCaptureDiffSchemas.js"; -import { projectInputIssues } from "../domain/inputIssueProjection.js"; +import { + analysisInputErrorFromIssues, + projectInputIssues, +} from "../domain/inputIssueProjection.js"; import { discoverWebMcpToolsInputSchema } from "../domain/webMcpDiscovery.js"; import { captureWebScreenshotInputSchema, @@ -251,10 +254,11 @@ const registerScreenshotDiff = ( const parsed = compareWebScreenshotsInputSchema.safeParse(input); if (!parsed.success) return browserCliError( - new AnalysisInputError( + analysisInputErrorFromIssues( operation, + parsed.error.issues, + input, { cause: parsed.error }, - projectInputIssues(parsed.error.issues, input), ), ); const result = await compareWebScreenshotEvidence( diff --git a/src/domain/changedBehavior.ts b/src/domain/changedBehavior.ts index 57aae499c6..49c5afe833 100644 --- a/src/domain/changedBehavior.ts +++ b/src/domain/changedBehavior.ts @@ -11,7 +11,7 @@ import { comparisonSourceEvidenceIds, } from "./comparisonSourceEvidence.js"; import { evidenceSchema, type Evidence } from "./evidence.js"; -import { AnalysisInputError } from "./analysisErrorCore.js"; +import type { AnalysisInputError } from "./analysisErrorCore.js"; import { EvidenceIntegrityError } from "./evidenceErrors.js"; import { analysisInputErrorFromIssues } from "./inputIssueProjection.js"; import { readEvidence, readEvidencePayload } from "./evidenceValidation.js"; @@ -128,10 +128,14 @@ export const findChangedBehavior = (input: unknown): FoundChangedBehavior => { .strictObject({ comparisons: z.array(z.unknown()).min(1) }) .safeParse(input); if (!envelope.success) - throw inputSchemaError( + throw analysisInputErrorFromIssues( + OPERATION, + envelope.error.issues, input, - envelope.error, - "Provide at least one comparison Evidence item.", + { + cause: envelope.error, + userMessage: "Provide at least one comparison Evidence item.", + }, ); const { comparisons } = envelope.data; const evidence = comparisons.map((item, index) => @@ -428,38 +432,15 @@ const firstDuplicateIndex = (values: readonly string[]): number | undefined => { const OPERATION = "find_changed_behavior"; -const inputSchemaError = ( - input: unknown, - cause: z.ZodError, - userMessage: string, -): AnalysisInputError => { - const projected = analysisInputErrorFromIssues( - OPERATION, - cause.issues, - input, - { cause }, - ); - return new AnalysisInputError( - OPERATION, - { cause, userMessage }, - projected.issues, - ); -}; - const callerInputError = ( input: unknown, path: readonly (string | number)[], message: string, -): AnalysisInputError => { - const projected = analysisInputErrorFromIssues(OPERATION, [], input, { +): AnalysisInputError => + analysisInputErrorFromIssues(OPERATION, [], input, { + userMessage: message, additionalIssues: [{ path, reason: "invalid_value", message }], }); - return new AnalysisInputError( - OPERATION, - { userMessage: message }, - projected.issues, - ); -}; const evidenceIntegrityError = (message: string): EvidenceIntegrityError => new EvidenceIntegrityError(message, { userMessage: message }); diff --git a/src/domain/inputIssueProjection.test.ts b/src/domain/inputIssueProjection.test.ts index 2543d97b48..8dfe406d5e 100644 --- a/src/domain/inputIssueProjection.test.ts +++ b/src/domain/inputIssueProjection.test.ts @@ -20,6 +20,7 @@ describe("input issue projection", () => { input, { cause, + userMessage: "Supply an integer count.", additionalIssues: [ { path: ["count"], reason: "out_of_range", minimum: 0 }, ], @@ -27,6 +28,7 @@ describe("input issue projection", () => { ); expect(error.cause).toBe(cause); + expect(error.userMessage).toBe("Supply an integer count."); expect(error.issues).toEqual([ { path: ["count"], reason: "invalid_type", expected: "number" }, { path: ["count"], reason: "out_of_range", minimum: 0 }, diff --git a/src/domain/inputIssueProjection.ts b/src/domain/inputIssueProjection.ts index 1d5db83b65..c0392848f3 100644 --- a/src/domain/inputIssueProjection.ts +++ b/src/domain/inputIssueProjection.ts @@ -11,12 +11,18 @@ export const analysisInputErrorFromIssues = ( input: unknown, options: { readonly cause?: unknown; + readonly userMessage?: string; readonly additionalIssues?: readonly AnalysisInputIssue[]; } = {}, ): AnalysisInputError => new AnalysisInputError( operation, - options.cause === undefined ? undefined : { cause: options.cause }, + { + ...(options.cause === undefined ? {} : { cause: options.cause }), + ...(options.userMessage === undefined + ? {} + : { userMessage: options.userMessage }), + }, [...projectInputIssues(issues, input), ...(options.additionalIssues ?? [])], ); diff --git a/src/domain/reconstructionVerification.ts b/src/domain/reconstructionVerification.ts index 135dc6512f..11f3ef7eb8 100644 --- a/src/domain/reconstructionVerification.ts +++ b/src/domain/reconstructionVerification.ts @@ -14,7 +14,7 @@ import { hasComparisonEvidenceIdentity } from "./comparisonEvidenceIdentity.js"; import { readEvidenceBundle } from "./evidenceBundle.js"; import { AnalysisInputError } from "./analysisErrorCore.js"; import { EvidenceIntegrityError } from "./evidenceErrors.js"; -import { projectInputIssues } from "./inputIssueProjection.js"; +import { analysisInputErrorFromIssues } from "./inputIssueProjection.js"; import { readEvidencePayload } from "./evidenceValidation.js"; import { functionComparisonResultSchema } from "./functionComparisonSchemas.js"; import { functionDossierSchema } from "./hopperValues.js"; @@ -58,10 +58,11 @@ export const verifyReconstruction = ( const parsedSpecification = reconstructionSpecificationSchema.safeParse(specificationInput); if (!parsedSpecification.success) - throw new AnalysisInputError( + throw analysisInputErrorFromIssues( "verify_reconstruction", + parsedSpecification.error.issues, + specificationInput, { cause: parsedSpecification.error }, - projectInputIssues(parsedSpecification.error.issues, specificationInput), ); const specification = parsedSpecification.data; const bundleResult = readEvidenceBundle(bundleInput); diff --git a/src/domain/staticRuntimeCorrelation.ts b/src/domain/staticRuntimeCorrelation.ts index 8eed65a485..9e0d83cc03 100644 --- a/src/domain/staticRuntimeCorrelation.ts +++ b/src/domain/staticRuntimeCorrelation.ts @@ -2,7 +2,7 @@ import { digestCanonicalValue } from "./canonicalDigest.js"; import { z } from "zod"; import { artifactComparisonResultSchema } from "./artifactComparison.js"; -import { AnalysisInputError } from "./analysisErrorCore.js"; +import type { AnalysisInputError } from "./analysisErrorCore.js"; import { EvidenceIntegrityError } from "./evidenceErrors.js"; import { analysisInputErrorFromIssues } from "./inputIssueProjection.js"; import { readEvidence, readEvidencePayload } from "./evidenceValidation.js"; @@ -169,11 +169,11 @@ export const correlateStaticAndRuntime = ( ): StaticRuntimeCorrelationResult => { const parsed = staticRuntimeCorrelationEnvelopeSchema.safeParse(input); if (!parsed.success) - throw inputSchemaError( - input, - parsed.error, - "Provide valid static and runtime comparison Evidence and mappings.", - ); + throw analysisInputErrorFromIssues(OPERATION, parsed.error.issues, input, { + cause: parsed.error, + userMessage: + "Provide valid static and runtime comparison Evidence and mappings.", + }); const { data } = parsed; const staticEvidence = data.static_comparisons.map((item, index) => parseStatic(item, input, index), @@ -513,38 +513,15 @@ const firstDuplicateIndex = (values: readonly string[]): number | undefined => { const OPERATION = "correlate_static_and_runtime"; -const inputSchemaError = ( - input: unknown, - cause: z.ZodError, - userMessage: string, -): AnalysisInputError => { - const projected = analysisInputErrorFromIssues( - OPERATION, - cause.issues, - input, - { cause }, - ); - return new AnalysisInputError( - OPERATION, - { cause, userMessage }, - projected.issues, - ); -}; - const callerInputError = ( input: unknown, path: readonly (string | number)[], message: string, -): AnalysisInputError => { - const projected = analysisInputErrorFromIssues(OPERATION, [], input, { +): AnalysisInputError => + analysisInputErrorFromIssues(OPERATION, [], input, { + userMessage: message, additionalIssues: [{ path, reason: "invalid_value", message }], }); - return new AnalysisInputError( - OPERATION, - { userMessage: message }, - projected.issues, - ); -}; const evidenceIntegrityError = (message: string): EvidenceIntegrityError => new EvidenceIntegrityError(message, { userMessage: message });