From 1635eadfcc49bbb80178838ab74667c2981de383 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:04:39 +0800 Subject: [PATCH] refactor(errors): centralize input failure construction Carry caller messages through the existing schema issue owner instead of constructing and discarding an intermediate input error. Remove forwarding schema helpers and migrate equivalent constructors across investigation, firmware, dylib, process, and screenshot workflows while preserving diagnostic paths and causes. --- .../firmware/FirmwareAnalysisService.ts | 8 ++-- src/application/process/ProcessCli.ts | 5 ++- src/artifacts/apple/DylibResolutionReader.ts | 7 ++-- src/cli/browserAdvancedCommands.ts | 10 +++-- src/domain/changedBehavior.ts | 41 +++++-------------- src/domain/inputIssueProjection.test.ts | 2 + src/domain/inputIssueProjection.ts | 8 +++- src/domain/reconstructionVerification.ts | 7 ++-- src/domain/staticRuntimeCorrelation.ts | 41 ++++--------------- 9 files changed, 51 insertions(+), 78 deletions(-) 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 });