fix(native): require canonical dispatch inventory rows

Remove the undocumented name alias from list_names fallback and validate the canonical value rows with the existing strict policy. Keep record defaults in the public input contract and require explicit decoder budgets. Preserve provenance, partial coverage, source coordinates and typed failures in active application and MCP tests.
This commit is contained in:
morluto committed 2026-10-24 09:28:17 +08:00
1 parent 96ad94f69e
commit 06822aed63
7 files changed
+53 -46

No files matched your search

@@ -200,7 +200,7 @@ const testAnalysis = (
});
case "list_names":
return execution(operation, [
{ address: "0x1000", name: "-[BuildViewController buildTapped:]" },
{ address: "0x1000", value: "-[BuildViewController buildTapped:]" },
]);
case "procedure_callees":
return execution(
@@ -535,10 +535,6 @@ describe("native dispatch inventory", () => {
it.each([
{ label: "plain value", entry: { address: "0x1000", value: methodName } },
{ label: "value with symbol facts", entry: methodInventoryEntry },
{
label: "legacy name with symbol facts",
entry: { address: "0x1000", name: methodName, symbol: methodSymbol },
},
])(
"binds $label inventory to the target without overstating dispatch coverage",
async ({ entry }) => {
@@ -583,6 +579,10 @@ describe("native dispatch inventory", () => {
);
it.each([
{
label: "legacy name with symbol facts",
entry: { address: "0x1000", name: methodName, symbol: methodSymbol },
},
{
label: "malformed symbol flag",
entry: {
@@ -836,11 +836,11 @@ describe("native UI trace boundaries", () => {
it("matches handlers after the dispatch inventory's former default limit", async () => {
const names = Array.from({ length: 5_000 }, (_, index) => ({
address: `0x${(index + 1).toString(16)}`,
name: `unrelated_symbol_${index}`,
value: `unrelated_symbol_${index}`,
}));
names.push({
address: "0x2000",
name: "-[BuildViewController buildTapped:]",
value: "-[BuildViewController buildTapped:]",
});
const result = await new EnhancedTools(
testAnalysis({ list_names: () => execution("list_names", names) }),
@@ -1,5 +1,4 @@
import { z } from "zod";
import { addressedValueSchema } from "../../domain/native/nativeInventory.js";
import { nativeInventoryRowsSchemas } from "../../domain/native/nativeInventory.js";
import {
nativeDispatchMetadataResultSchema,
inspectNativeDispatchMetadata,
@@ -10,6 +9,10 @@ import { err, ok } from "../../domain/result.js";
import type { AnalysisOperationPort } from "../AnalysisProvider.js";
import type { EnhancedResult } from "../EnhancedToolTypes.js";
const dispatchNameInventorySchema = nativeInventoryRowsSchemas.names.element
.strict()
.array();
/** Source operations for this workflow, shared with capability discovery. */
export const NATIVE_DISPATCH_METADATA_SOURCES = {
binary: "inspect_native_dispatch_metadata",
@@ -64,17 +67,7 @@ export const inspectNativeDispatch = async (
signal === undefined ? {} : { signal },
);
if (!execution.ok) return err(execution.error);
const names = z
.array(
z.union([
addressedValueSchema.strict(),
addressedValueSchema
.omit({ value: true })
.extend({ name: z.string() })
.strict(),
]),
)
.safeParse(execution.value.result);
const names = dispatchNameInventorySchema.safeParse(execution.value.result);
if (!names.success)
return err(
new AnalysisOutputError(
@@ -85,7 +78,7 @@ export const inspectNativeDispatch = async (
const result = inspectNativeDispatchMetadata(
names.data.map((entry) => ({
address: entry.address,
name: "value" in entry ? entry.value : entry.name,
name: entry.value,
})),
maxRecords,
);
+22 -12
View File
@@ -20,9 +20,10 @@ describe("symbol-only Objective-C implementation identity", () => {
])(
"preserves $source without inventing a numeric method identity",
({ source, canonical, numeric }) => {
const result = inspectNativeDispatchMetadata([
{ address: source, name: "-[StoreController buildTapped:]" },
]);
const result = inspectNativeDispatchMetadata(
[{ address: source, name: "-[StoreController buildTapped:]" }],
1,
);
expect(result.objc_classes[0]?.methods[0]?.address).toBe(numeric);
expect(result.objc_dispatch_implementations[0]).toMatchObject({
implementation_address: canonical,
@@ -34,10 +35,13 @@ describe("symbol-only Objective-C implementation identity", () => {
it("retains unparseable symbol coordinates as reported evidence, without claiming a location", () => {
const source = "invalid::0x2000";
const result = inspectNativeDispatchMetadata([
{ address: source, name: "-[StoreController buildTapped:]" },
{ address: source, name: "$s5Store7launchyyF" },
]);
const result = inspectNativeDispatchMetadata(
[
{ address: source, name: "-[StoreController buildTapped:]" },
{ address: source, name: "$s5Store7launchyyF" },
],
2,
);
expect(result.objc_dispatch_implementations[0]).toMatchObject({
implementation_address: null,
location: { address: null },
@@ -57,6 +61,7 @@ describe("symbol-only Objective-C implementation identity", () => {
address,
name: "-[StoreController buildTapped:]",
})),
ordered.length,
);
expect(result.objc_dispatch_implementations).toHaveLength(1);
expect(result.objc_dispatch_implementations[0]).toMatchObject({
@@ -87,6 +92,7 @@ describe("symbol-only Objective-C implementation identity", () => {
address,
name: "-[StoreController buildTapped:]",
})),
2,
);
expect(result.objc_dispatch_implementations).toHaveLength(2);
expect(result.objc_classes[0]?.methods).toEqual([
@@ -176,11 +182,14 @@ describe("ObjC/Swift metadata", () => {
});
it("retains unlocated Swift and Objective-C symbols with explicit unknown addresses and source evidence", () => {
const result = inspectNativeDispatchMetadata([
{ name: "$s4Test7MyClassC", address: "" },
{ name: "-[StoreController buildTapped:]", address: "" },
{ name: "$s5Store7launchyyF", address: "ram:0x3000" },
]);
const result = inspectNativeDispatchMetadata(
[
{ name: "$s4Test7MyClassC", address: "" },
{ name: "-[StoreController buildTapped:]", address: "" },
{ name: "$s5Store7launchyyF", address: "ram:0x3000" },
],
3,
);
expect(result.swift_symbols).toMatchObject([
{
mangled_name: "$s4Test7MyClassC",
@@ -226,6 +235,7 @@ it("derives classes only from complete class symbol leaves", () => {
"_OBJC_METACLASS_$_Local",
"OwnController::viewDidLoad",
].map((name, index) => ({ address: `0x${String(index + 1)}`, name })),
7,
);
expect(result.objc_classes.map(({ name }) => name)).toEqual([
"Local",
+1 -1
View File
@@ -474,7 +474,7 @@ export const nativeDispatchMetadataResultSchema = z.strictObject({
/** Decode only facts visible in a provider's existing name inventory. */
export const inspectNativeDispatchMetadata = (
names: readonly { readonly address: string; readonly name: string }[],
maxRecords = DEFAULT_DISPATCH_METADATA_RECORDS,
maxRecords: number,
): ObjcSwiftMetadata => {
const symbolEvidence = (
entry: (typeof names)[number],
@@ -35,14 +35,17 @@ const identity = {
name: "Fixture analysis provider",
version: "1",
};
const dispatchMetadata = inspectNativeDispatchMetadata([
{
address: "0x1000",
name: "-[BuildViewController buildTapped:]",
},
]);
const dispatchMetadata = inspectNativeDispatchMetadata(
[
{
address: "0x1000",
name: "-[BuildViewController buildTapped:]",
},
],
1,
);
const symbolInventory = [
{ address: "0x1000", name: "-[BuildViewController buildTapped:]" },
{ address: "0x1000", value: "-[BuildViewController buildTapped:]" },
];
type Operation = "inspect_native_dispatch_metadata" | "list_names";
@@ -64,11 +64,12 @@ const controllerName = "BuildViewController";
const selector = "buildTapped:";
const handlerAddress = "0x1000";
const calleeAddress = "0x2000";
const dispatchMetadata = inspectNativeDispatchMetadata([
{ address: handlerAddress, name: `-[${controllerName} ${selector}]` },
]);
const dispatchMetadata = inspectNativeDispatchMetadata(
[{ address: handlerAddress, name: `-[${controllerName} ${selector}]` }],
1,
);
const symbolInventory = [
{ address: handlerAddress, name: `-[${controllerName} ${selector}]` },
{ address: handlerAddress, value: `-[${controllerName} ${selector}]` },
];
const interfaceBuilderFor = (targetSha256: string) =>
buildInterfaceBuilderAnalysis({
@@ -137,7 +137,7 @@ function publicTrace(plan: Plan, maxNodes = 12, maxEdges = 500) {
target_sha256: sha256,
provider: identity,
analysis_profile_digest: null,
result: inspectNativeDispatchMetadata([]),
result: inspectNativeDispatchMetadata([], 1),
},
identity,
{ subject },