From 39c7f7e3592a09b7dc2e854a77c97ffd85011d58 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:42:18 +0800 Subject: [PATCH] fix(evidence): retain malformed function observations --- .../FunctionAnalysisEvidence.test.ts | 46 ++++++++++++++++++- src/application/FunctionAnalysisEvidence.ts | 24 +++++++--- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/application/FunctionAnalysisEvidence.test.ts b/src/application/FunctionAnalysisEvidence.test.ts index 9d8cc3bbaf..9a8cee9ab4 100644 --- a/src/application/FunctionAnalysisEvidence.test.ts +++ b/src/application/FunctionAnalysisEvidence.test.ts @@ -7,6 +7,7 @@ import { createAnalysisProfile } from "../domain/analysisProfile.js"; import { ghidraFunctionDossier } from "../domain/ghidraValues.fixture.js"; import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; import { err, ok } from "../domain/result.js"; +import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; const provider = { id: "dossier-fixture", @@ -157,6 +158,49 @@ it.each(["provider-failure", "invalid-dossier"] as const)( _tag: "AnalysisOutputError", operation: "analyze_function", }); - expect(result.error.partialObservation).toBeUndefined(); + if (kind === "provider-failure") + expect(result.error.partialObservation).toBeUndefined(); + else { + const retained = parseEvidence(result.error.partialObservation); + expect(retained.normalized_result).toEqual({ procedure: "incomplete" }); + expect(retained.subject?.local_path).toBe(target.path); + } }, ); + +it("retains malformed dossier facts and source links through public error projection", async () => { + const source = createEvidence(undefined, provider, { + operation: "health", + parameters: {}, + result: "retained source", + }); + const malformed = { procedure: "incomplete", partial: ["caller fact"] }; + const result = await executeFunctionAnalysisEvidence( + { + execute: async () => + ok( + createAnalysisExecution(malformed, provider, { + subject: null, + rawResult: { text: "original provider output" }, + limitations: ["Incomplete provider response"], + locations: [{ kind: "address", address: "0x401000" }], + sourceEvidence: [source], + }), + ), + }, + parameters, + target, + ); + if (result.ok) throw new Error("Expected invalid dossier failure"); + expect(result.sourceEvidence).toEqual([source]); + const projected = projectAnalysisError(result.error); + const retained = parseEvidence(projected.details?.partial_observation); + expect(retained.normalized_result).toEqual(malformed); + expect(retained.raw_result).toEqual({ text: "original provider output" }); + expect(retained.subject).toBeNull(); + expect(retained.evidence_links).toEqual([source.evidence_id]); + expect(retained.locations).toEqual([ + { kind: "address", address: "0x401000" }, + ]); + expect(retained.limitations).toContain("Incomplete provider response"); +}); diff --git a/src/application/FunctionAnalysisEvidence.ts b/src/application/FunctionAnalysisEvidence.ts index 835f81ed80..2b0d97ea1b 100644 --- a/src/application/FunctionAnalysisEvidence.ts +++ b/src/application/FunctionAnalysisEvidence.ts @@ -49,9 +49,25 @@ export const executeFunctionAnalysisEvidence = async ( ); const sources = sourceLedger.recordsForAnalysis(); if (!recorded.ok) return withEvidenceSources(recorded, sources); + const observation = { + ...execution.value, + sourceEvidence: sources, + ...(execution.value.subject === undefined && target !== undefined + ? { subject: target } + : {}), + }; const dossier = parseFunctionDossier(execution.value.result); - if (!dossier.ok) return withEvidenceSources(dossier, sources); - const observation = execution.value; + if (!dossier.ok) { + dossier.error.retainPartialObservation( + createAnalysisExecutionEvidence( + "analyze_function", + arguments_, + observation, + "observed", + ), + ); + return withEvidenceSources(dossier, sources); + } return withEvidenceSources( completeProviderEvidence( createAnalysisExecutionEvidence( @@ -59,11 +75,7 @@ export const executeFunctionAnalysisEvidence = async ( arguments_, { ...observation, - sourceEvidence: sources, result: dossier.value, - ...(observation.subject === undefined && target !== undefined - ? { subject: target } - : {}), }, "observed", ),