fix(contracts)!: require observed provider output metadata

Reject omitted segment bounds, procedure classification and body facts, unresolved-call observations, dossier facets, and Ghidra parameter flow metadata. Providers emit explicit nulls for unsupported facets. Verify those unknowns in the real Hopper workflow.

BREAKING CHANGE: Provider analysis outputs must explicitly supply metadata previously repaired with defaults; unsupported nullable facets must be reported as null.
This commit is contained in:
morluto committed 2026-10-10 03:52:13 +08:00
1 parent e3bbb9ac45
commit a4ba8aecd5
29 files changed
+465 -112

No files matched your search

+18
View File
@@ -58,6 +58,12 @@ async function verifyNavigation(
procedure: address,
});
const info = await call("procedure_info", { procedure: address });
assert.equal(info.classification, null);
assert.equal(info.body.available, false);
assert.match(
info.body.reason,
/public Python API does not expose complete function body ranges/u,
);
assert.ok(info.locals.length > 0, "fixture omitted real stack locals");
for (const local of info.locals) {
assert.equal(typeof local.name, "string");
@@ -68,6 +74,18 @@ async function verifyNavigation(
}
const dossier = await call("analyze_function", { procedure: address });
assert.deepEqual(dossier.procedure.locals, info.locals);
assert.equal(dossier.procedure.classification, null);
assert.equal(dossier.procedure.body.available, false);
assert.match(
dossier.procedure.body.reason,
/public Python API does not expose complete function body ranges/u,
);
assert.equal(dossier.native_api, null);
assert.equal(dossier.native_value_flow, null);
assert.ok(
Array.isArray(dossier.limitations) && dossier.limitations.length > 0,
"function dossier omitted Hopper's unsupported-facet limitations",
);
const coordinates = instructions.instructions.map(
(line) => /^0x[0-9a-f]+/u.exec(line)?.[0],
);
+25 -3
View File
@@ -11,15 +11,28 @@ export const requireTruthfulMemoryRegions = (segments) => {
...(Array.isArray(segment.sections) ? segment.sections : []),
]);
for (const region of regions) {
if (
typeof region?.name !== "string" ||
region.name.length === 0 ||
typeof region.start !== "string" ||
typeof region.end !== "string"
) {
throw new Error(
"list_segments omitted observed region identity or bounds",
);
}
for (const permission of ["readable", "writable", "executable"]) {
if (![true, false, null].includes(region?.[permission])) {
throw new Error(`list_segments omitted tri-state ${permission}`);
if (region[permission] !== null) {
throw new Error(
`list_segments invented or omitted unknown ${permission}`,
);
}
}
if (
region.provenance !== "hopper-public-python-api" ||
region.permissions?.available !== false ||
typeof region.permissions.reason !== "string"
typeof region.permissions.reason !== "string" ||
region.permissions.reason.length === 0
) {
throw new Error("list_segments omitted permission provenance");
}
@@ -141,6 +154,15 @@ export async function verifyRealHopperFixture({
"procedure_references",
);
assert.deepEqual(entryDossier.unresolved_calls, observed.unresolved_calls);
assert.equal(
observed.reference_kinds_available,
false,
"Hopper's partial CallReference observations do not expose detailed reference kinds",
);
assert.ok(
Array.isArray(observed.unresolved_calls),
"procedure_references omitted its observed unresolved CallReference results",
);
for (const call of calls) {
assert.equal(call.call.provider_type, 2);
assert.equal(
+6
View File
@@ -62,6 +62,10 @@ export function functionDossier(name) {
thunk_target: null,
provenance: "synthetic-provider",
},
body: {
available: false,
reason: "Synthetic provider does not supply function body ranges.",
},
signature: null,
locals: [],
},
@@ -75,6 +79,8 @@ export function functionDossier(name) {
referenced_strings: [],
referenced_names: [],
basic_blocks: [],
native_api: null,
native_value_flow: null,
limitations: [],
};
}