mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 11:13:21 +00:00
fix(decompile): keep each successful batch item's own limitations
A successful batch_decompile item carried no item-level provenance: only the batch Evidence aggregated limitations, so when a provider such as Kuna answered repeated selectors from different sources, the caveat did not say which occurrence it applied to. Successful items now keep their own observation's limitations (exactOptional, so retained records still parse), and the retained items view projects them. The batch unavailable item also dropped the provider's typed pseudocodeAbsence reason; it now carries pseudocode_absence. Fixes #8350 Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
1 parent
6cfc2fa62d
commit
9d2a57fd8d
8 files changed
+113
-19
No files matched your search
@@ -247,8 +247,10 @@ observation's own `limitations`, so a reason the provider reported for that
|
||||
entry, such as a Kuna export failure or a missing body, stays with the item.
|
||||
Null pseudocode is `capability_unavailable`; empty or non-text pseudocode is
|
||||
`unreadable_output`. Provider failures keep their typed error. Successful items
|
||||
do not repeat limitations; the batch Evidence aggregates every observation's
|
||||
limitations by provider and operation.
|
||||
also keep their own observation's limitations, so repeated selectors that the
|
||||
provider answered differently stay distinguishable, and an unavailable item
|
||||
keeps the provider's `pseudocode_absence` reason. The batch Evidence still
|
||||
aggregates every observation's limitations by provider and operation.
|
||||
Resolved entries also appear in the composed Evidence locations. New batch
|
||||
Evidence commits the revised workflow profile; retained historical Evidence
|
||||
remains valid. This tool
|
||||
|
||||
@@ -866,8 +866,9 @@ and provider identities:
|
||||
|
||||
Page `coverage` counts the selection only: exhausting it establishes nothing
|
||||
about functions outside the batch. Evidence `limitations` aggregate every item's
|
||||
provider observations and are not attributed to one successful item; unavailable and error
|
||||
items keep their own. A page over the response budget offers a smaller selection
|
||||
provider observations; every item, successful or not, also keeps its own
|
||||
observation's limitations, and an unavailable item keeps the provider's
|
||||
`pseudocode_absence` reason when one was reported. A page over the response budget offers a smaller selection
|
||||
from the same `source`: fewer rows, or one body's `pseudocode` pages when a single
|
||||
row does not fit. An inline source offers none, because the record cannot be
|
||||
repeated in that reply; resend it with a smaller selection.
|
||||
|
||||
@@ -506,21 +506,32 @@ export class EnhancedTools {
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const { result: pseudocode } = observed.value;
|
||||
const { result: pseudocode, pseudocodeAbsence } = observed.value;
|
||||
// Each item keeps its own observation's caveats, so repeated selectors
|
||||
// that the provider answered differently stay distinguishable; the batch
|
||||
// Evidence still aggregates them.
|
||||
const limitations = [...new Set(observed.value.limitations)];
|
||||
const itemLimitations = limitations.length === 0 ? {} : { limitations };
|
||||
if (typeof pseudocode === "string" && pseudocode.length > 0) {
|
||||
items.push({ address, procedure, status: "ok", pseudocode });
|
||||
items.push({
|
||||
address,
|
||||
procedure,
|
||||
status: "ok",
|
||||
pseudocode,
|
||||
...itemLimitations,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
// Only this observation can explain its missing text, so its caveats
|
||||
// stay with the item; the batch Evidence still aggregates them.
|
||||
const limitations = [...new Set(observed.value.limitations)];
|
||||
if (pseudocode === null) {
|
||||
items.push({
|
||||
address,
|
||||
procedure,
|
||||
status: "unavailable",
|
||||
reason: "no_pseudocode",
|
||||
...(limitations.length === 0 ? {} : { limitations }),
|
||||
...(pseudocodeAbsence === undefined
|
||||
? {}
|
||||
: { pseudocode_absence: pseudocodeAbsence }),
|
||||
...itemLimitations,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -529,7 +540,7 @@ export class EnhancedTools {
|
||||
procedure,
|
||||
status: "error",
|
||||
error: projectAnalysisError(missingPseudocodeError(pseudocode)),
|
||||
...(limitations.length === 0 ? {} : { limitations }),
|
||||
...itemLimitations,
|
||||
});
|
||||
}
|
||||
return ok(batchDecompileResult(items));
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type CommittedProviderIdentity,
|
||||
} from "../analysisProfile.js";
|
||||
import { digestSchema } from "../digests.js";
|
||||
import { pseudocodeAbsenceSchema } from "../native/pseudocodeAbsence.js";
|
||||
import { providerSchema } from "../evidence.js";
|
||||
import {
|
||||
batchDecompileResultSchema,
|
||||
@@ -114,11 +115,13 @@ const batchDecompileRowSchema = z.discriminatedUnion("status", [
|
||||
status: z.literal("ok"),
|
||||
pseudocode_code_units: z.int().positive(),
|
||||
pseudocode: z.string().min(1).exactOptional(),
|
||||
limitations: batchItemLimitationsSchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
...itemIdentity,
|
||||
status: z.literal("unavailable"),
|
||||
reason: z.literal("no_pseudocode"),
|
||||
pseudocode_absence: pseudocodeAbsenceSchema.exactOptional(),
|
||||
limitations: batchItemLimitationsSchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
@@ -253,7 +256,7 @@ const batchUnknowns = (items: readonly BatchDecompileItem[]): string[] => {
|
||||
: [
|
||||
`${String(unknownIdentity)} items have unknown procedure identity; their address is the caller's selector, not an observed entry.`,
|
||||
]),
|
||||
"Evidence limitations aggregate every item's provider observations and are not attributed to one successful item. A batch covers only its selected procedures, and exhausting a page establishes nothing outside it.",
|
||||
"Evidence limitations aggregate every item's provider observations; each item row also carries its own observation's limitations. A batch covers only its selected procedures, and exhausting a page establishes nothing outside it.",
|
||||
];
|
||||
};
|
||||
|
||||
@@ -276,12 +279,18 @@ const itemRow = (
|
||||
status: "ok",
|
||||
pseudocode_code_units: item.pseudocode.length,
|
||||
...(includePseudocode ? { pseudocode: item.pseudocode } : {}),
|
||||
...(item.limitations === undefined
|
||||
? {}
|
||||
: { limitations: item.limitations }),
|
||||
};
|
||||
if (item.status === "unavailable")
|
||||
return {
|
||||
...identity,
|
||||
status: "unavailable",
|
||||
reason: item.reason,
|
||||
...(item.pseudocode_absence === undefined
|
||||
? {}
|
||||
: { pseudocode_absence: item.pseudocode_absence }),
|
||||
...(item.limitations === undefined
|
||||
? {}
|
||||
: { limitations: item.limitations }),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { analysisErrorProjectionSchema } from "../../contracts/errorSchemas.js";
|
||||
import { pseudocodeAbsenceSchema } from "./pseudocodeAbsence.js";
|
||||
|
||||
/** Identity facts attached to one requested batch selector. */
|
||||
export const batchProcedureIdentitySchema = z.discriminatedUnion("status", [
|
||||
@@ -27,7 +28,7 @@ export const batchItemLimitationsSchema = z
|
||||
.array(z.string())
|
||||
.exactOptional()
|
||||
.describe(
|
||||
"Caveats of this item's own provider observation when it supplied no pseudocode, including any reason the provider reported.",
|
||||
"Caveats of this item's own provider observation, including any reason the provider reported when it supplied no pseudocode.",
|
||||
);
|
||||
|
||||
/** One ordered batch outcome; array position keeps duplicate selectors distinct. */
|
||||
@@ -37,12 +38,16 @@ export const batchDecompileItemSchema = z.discriminatedUnion("status", [
|
||||
procedure: batchProcedureIdentitySchema,
|
||||
status: z.literal("ok"),
|
||||
pseudocode: z.string().min(1),
|
||||
limitations: batchItemLimitationsSchema,
|
||||
}),
|
||||
z.object({
|
||||
address: batchSelectorSchema,
|
||||
procedure: batchProcedureIdentitySchema,
|
||||
status: z.literal("unavailable"),
|
||||
reason: z.literal("no_pseudocode"),
|
||||
pseudocode_absence: pseudocodeAbsenceSchema
|
||||
.exactOptional()
|
||||
.describe("The provider's reported reason for null pseudocode."),
|
||||
limitations: batchItemLimitationsSchema,
|
||||
}),
|
||||
z.object({
|
||||
|
||||
@@ -678,7 +678,13 @@ describe("enhanced MCP batch answers without pseudocode", () => {
|
||||
const evidence = contract.outputSchema.parse(result.structuredContent);
|
||||
expect(evidence.normalized_result).toMatchObject({
|
||||
items: [
|
||||
{ address: "0x1", status: "ok", pseudocode: answers["0x1"]?.result },
|
||||
// Successful text keeps its own observation's caveats (#8350).
|
||||
{
|
||||
address: "0x1",
|
||||
status: "ok",
|
||||
pseudocode: answers["0x1"]?.result,
|
||||
limitations: answers["0x1"]?.limitations,
|
||||
},
|
||||
{
|
||||
address: "0x2",
|
||||
status: "unavailable",
|
||||
@@ -725,9 +731,9 @@ describe("enhanced MCP batch answers without pseudocode", () => {
|
||||
const { items } = batchDecompileResultSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
// Successful text, a caveat-free null and a typed provider failure carry
|
||||
// no item caveats; the batch Evidence aggregates every observation's.
|
||||
for (const index of [0, 2, 5])
|
||||
// A caveat-free null and a typed provider failure carry no item caveats;
|
||||
// the batch Evidence aggregates every observation's.
|
||||
for (const index of [2, 5])
|
||||
expect(items[index]).not.toHaveProperty("limitations");
|
||||
expect(evidence.limitations).toEqual(
|
||||
expect.arrayContaining([
|
||||
|
||||
@@ -118,7 +118,22 @@ describe("Ghidra MCP capability routing", () => {
|
||||
batch.structuredContent,
|
||||
).normalized_result,
|
||||
);
|
||||
expect(result.items[0]).not.toHaveProperty("limitations");
|
||||
// The successful item keeps its own observation's caveats (#8350).
|
||||
const answered = parseEvidence(
|
||||
(
|
||||
await harness.mcp.callTool({
|
||||
name: "procedure_pseudo_code",
|
||||
arguments: { procedure: "0x401000" },
|
||||
})
|
||||
).structuredContent,
|
||||
);
|
||||
const answeredLimitations = [...new Set(answered.limitations)];
|
||||
if (answeredLimitations.length === 0)
|
||||
expect(result.items[0]).not.toHaveProperty("limitations");
|
||||
else
|
||||
expect(result.items[0]).toMatchObject({
|
||||
limitations: answeredLimitations,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
items: [
|
||||
expect.objectContaining({ address: "0x401000", status: "ok" }),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AnalysisProviderRegistry } from "../../../../src/application/binary/AnalysisProviderRegistry.js";
|
||||
import { composeBinarySession } from "../../../../src/application/binary/BinarySessionComposition.js";
|
||||
@@ -38,6 +39,40 @@ const getEvidenceBundle = SESSION_TOOL_CONTRACTS.find(
|
||||
> => contract.name === "get_evidence_bundle",
|
||||
);
|
||||
|
||||
/** Read the retained batch's items view and check the per-item provider facts. */
|
||||
const expectRetainedItems = async (
|
||||
call: (
|
||||
name: string,
|
||||
arguments_: Record<string, unknown>,
|
||||
) => Promise<{ structuredContent?: unknown }>,
|
||||
evidenceId: string,
|
||||
broken: { readonly limitations: readonly string[] },
|
||||
puts: { readonly limitations: readonly string[] },
|
||||
) => {
|
||||
const items = z
|
||||
.looseObject({ items: z.array(z.looseObject({ address: z.string() })) })
|
||||
.parse(
|
||||
parseEvidence(
|
||||
(
|
||||
await call("inspect_analysis_view", {
|
||||
source: { kind: "retained-evidence", evidence_id: evidenceId },
|
||||
view: { kind: "batch-decompile", collection: "items" },
|
||||
})
|
||||
).structuredContent,
|
||||
).normalized_result,
|
||||
).items;
|
||||
const row = (address: string) =>
|
||||
items.find((item) => item.address === address);
|
||||
expect(row("broken")).toMatchObject({
|
||||
pseudocode_absence: { kind: "provider-failure" },
|
||||
limitations: broken.limitations,
|
||||
});
|
||||
expect(row("puts")).toMatchObject({
|
||||
pseudocode_absence: { kind: "not-in-export" },
|
||||
limitations: puts.limitations,
|
||||
});
|
||||
};
|
||||
|
||||
test("keeps each Kuna no-pseudocode observation's limitations on its batch item", async ({
|
||||
mcp,
|
||||
}) => {
|
||||
@@ -75,6 +110,7 @@ test("keeps each Kuna no-pseudocode observation's limitations on its batch item"
|
||||
parseEvidence(
|
||||
(await call("procedure_pseudo_code", { procedure })).structuredContent,
|
||||
);
|
||||
const mainDirect = await direct("main");
|
||||
const broken = await direct("broken");
|
||||
const importSlot = await direct("puts");
|
||||
expect(broken.normalized_result).toBeNull();
|
||||
@@ -136,7 +172,11 @@ test("keeps each Kuna no-pseudocode observation's limitations on its batch item"
|
||||
expect(result).toMatchObject({ total: 4, succeeded: 1, failed: 3 });
|
||||
const [main, failed, imported, unknown] = result.items;
|
||||
expect(main).toMatchObject({ address: "main", status: "ok" });
|
||||
expect(main).not.toHaveProperty("limitations");
|
||||
// A successful item keeps its own observation's caveats too, so a
|
||||
// repeated selector answered differently stays distinguishable.
|
||||
if (mainDirect.limitations.length === 0)
|
||||
expect(main).not.toHaveProperty("limitations");
|
||||
else expect(main).toMatchObject({ limitations: mainDirect.limitations });
|
||||
const noPseudocode = {
|
||||
status: "unavailable",
|
||||
reason: "no_pseudocode",
|
||||
@@ -147,12 +187,14 @@ test("keeps each Kuna no-pseudocode observation's limitations on its batch item"
|
||||
address: "broken",
|
||||
procedure: { status: "resolved", address: "0x1080", name: "broken" },
|
||||
...noPseudocode,
|
||||
pseudocode_absence: { kind: "provider-failure" },
|
||||
limitations: broken.limitations,
|
||||
});
|
||||
expect(imported).toMatchObject({
|
||||
address: "puts",
|
||||
procedure: { status: "resolved", address: "0x2000" },
|
||||
...noPseudocode,
|
||||
pseudocode_absence: { kind: "not-in-export" },
|
||||
limitations: importSlot.limitations,
|
||||
});
|
||||
expect(imported).not.toEqual(
|
||||
@@ -174,6 +216,9 @@ test("keeps each Kuna no-pseudocode observation's limitations on its batch item"
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
// The retained items view projects the same per-item facts.
|
||||
await expectRetainedItems(call, batch.evidence_id, broken, importSlot);
|
||||
|
||||
// A batch-only bundle keeps the association without another provider read.
|
||||
const bundle = evidenceBundleSchema.parse(
|
||||
getEvidenceBundle.outputSchema.parse(
|
||||
|
||||
Reference in new issue
Block a user