From cebd46d15b954e14a3d495897affbe8b61e8c3d0 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:21:02 +0800 Subject: [PATCH] fix(native): validate matching observation context at selection --- docs/native-function-matching.md | 5 ++ .../NativeFunctionMatchingService.test.ts | 54 ++++++++++++++++++- .../native/NativeMatchingSources.ts | 27 +++++++++- src/domain/native/functionInventory.ts | 32 +---------- 4 files changed, 84 insertions(+), 34 deletions(-) diff --git a/docs/native-function-matching.md b/docs/native-function-matching.md index c44f043e7d..6021177acf 100644 --- a/docs/native-function-matching.md +++ b/docs/native-function-matching.md @@ -25,6 +25,11 @@ existing automatic-name exclusion rule applies and each function retains that limitation. Exact names establish name correspondence, not unchanged code or original-source identity. +Authentic Evidence selected for the wrong side, role, target, provider, profile +or document returns an input error naming the selected field or observation +index. Re-select compatible observations; authentication or contradictory producer +facts retain their separate integrity failures. + For unmatched candidates, optionally supply `left_observations` and `right_observations`. `procedure_info` or existing `analyze_function` Evidence gives exact inclusive body ranges; `read_bytes` supplies memory bytes. Observations diff --git a/src/application/native/NativeFunctionMatchingService.test.ts b/src/application/native/NativeFunctionMatchingService.test.ts index 0de9978d11..ce836a41d4 100644 --- a/src/application/native/NativeFunctionMatchingService.test.ts +++ b/src/application/native/NativeFunctionMatchingService.test.ts @@ -669,7 +669,8 @@ it("assigns source authentication, role selection, context, and lookup failures right: left, left_observations: nativeMatchingDetails("2", "0x10", "c3"), }, - tag: "EvidenceIntegrityError", + tag: "AnalysisInputError", + path: ["left_observations", 0], }, { input: { @@ -975,3 +976,54 @@ it("retains zero-byte reads with unavailable addresses when no request coordinat emptyRead.evidence_id, ); }); + +it.each(["left_observations", "right_observations"] as const)( + "reports authentic mismatched provider/profile/document selections at %s without reauthentication advice", + async (field) => { + const inventory = nativeMatchingInventory("1", [ + { address: "0x10", value: "main" }, + ]); + const detail = nativeMatchingDetails("1", "0x10", "c3")[0]!; + const variants = [ + createEvidence( + nativeMatchingTarget("1"), + { ...nativeMatchingProvider, version: "other" }, + { + operation: detail.operation, + parameters: detail.parameters, + result: detail.normalized_result, + }, + ), + createEvidence(nativeMatchingTarget("1"), nativeMatchingProvider, { + operation: detail.operation, + parameters: detail.parameters, + result: detail.normalized_result, + analysisProfile: createAnalysisProfile(nativeMatchingProvider, { + loader: "different", + }), + }), + createEvidence(nativeMatchingTarget("1"), nativeMatchingProvider, { + operation: detail.operation, + parameters: { ...detail.parameters, document: "another-document" }, + result: detail.normalized_result, + }), + ]; + for (const observation of variants) { + const result = await matchNativeFunctions({ + left: inventory, + right: inventory, + [field]: [detail, observation], + }); + expect(result).toMatchObject({ + ok: false, + error: { _tag: "AnalysisInputError", issues: [{ path: [field, 1] }] }, + }); + } + const restored = await compare({ + left: inventory, + right: inventory, + [field]: [detail], + }); + expect(restored.report.summary.matched).toBe(1); + }, +); diff --git a/src/application/native/NativeMatchingSources.ts b/src/application/native/NativeMatchingSources.ts index 2a3dd61bb9..edb89e524b 100644 --- a/src/application/native/NativeMatchingSources.ts +++ b/src/application/native/NativeMatchingSources.ts @@ -1,4 +1,5 @@ import type { MatchNativeFunctionsInput } from "../../contracts/native/functionMatchingContracts.js"; +import { digestCanonicalValue } from "../../domain/canonicalDigest.js"; import { AnalysisInputError } from "../../domain/analysisErrorCore.js"; import type { Evidence } from "../../domain/evidence.js"; import { readEvidence } from "../../domain/evidenceValidation.js"; @@ -56,9 +57,10 @@ export const resolveNativeMatchingSources = async ( const evidence = await resolve(raw); if ( evidence.predicate_type !== "rea.analysis" || - !operations.includes(evidence.operation) + !operations.includes(evidence.operation) || + evidence.subject === null ) { - const message = `Expected rea.analysis ${operations.join(" or ")} Evidence, received ${evidence.predicate_type} ${evidence.operation}`; + const message = `Expected artifact-bound rea.analysis ${operations.join(" or ")} Evidence, received ${evidence.predicate_type} ${evidence.operation}`; throw new AnalysisInputError( admission.operation, { userMessage: message }, @@ -72,6 +74,7 @@ export const resolveNativeMatchingSources = async ( const observations = async ( inputs: readonly unknown[], field: "left_observations" | "right_observations", + inventory: Evidence, ): Promise => { const result: Evidence[] = []; const seen = new Set(); @@ -81,6 +84,24 @@ export const resolveNativeMatchingSources = async ( [field, index], OBSERVATION_OPERATIONS, ); + if ( + digestCanonicalValue(record.subject) !== + digestCanonicalValue(inventory.subject) || + digestCanonicalValue(record.provider) !== + digestCanonicalValue(inventory.provider) || + digestCanonicalValue(record.analysis_profile) !== + digestCanonicalValue(inventory.analysis_profile) || + (record.parameters.document ?? null) !== + (inventory.parameters.document ?? null) + ) { + const message = + "Select an observation from the inventory's exact target, provider, analysis profile and document context."; + throw new AnalysisInputError( + admission.operation, + { userMessage: message }, + [{ path: [field, index], reason: "invalid_value", message }], + ); + } if (!seen.has(record.evidence_id)) { seen.add(record.evidence_id); result.push(record); @@ -91,10 +112,12 @@ export const resolveNativeMatchingSources = async ( const leftObservations = await observations( input.left_observations, "left_observations", + left, ); const rightObservations = await observations( input.right_observations, "right_observations", + right, ); const directLinks = [...sources.keys()].sort(); for (const source of sources.values()) diff --git a/src/domain/native/functionInventory.ts b/src/domain/native/functionInventory.ts index 3aa40b6a68..0f95c2aeaa 100644 --- a/src/domain/native/functionInventory.ts +++ b/src/domain/native/functionInventory.ts @@ -84,23 +84,6 @@ interface Facts { readonly bytes: FunctionByteCoverage; } -/** Supplementary observations must belong to this exact provider/target/profile/document. */ -const requireContext = (inventory: Evidence, observation: Evidence): void => { - if ( - observation.predicate_type !== "rea.analysis" || - digestCanonicalValue(observation.subject) !== - digestCanonicalValue(inventory.subject) || - digestCanonicalValue(observation.provider) !== - digestCanonicalValue(inventory.provider) || - digestCanonicalValue(observation.analysis_profile) !== - digestCanonicalValue(inventory.analysis_profile) || - (observation.parameters.document ?? null) !== - (inventory.parameters.document ?? null) - ) - throw new EvidenceIntegrityError( - "Function observation does not belong to its selected inventory context", - ); -}; const addBody = (observation: Evidence, details: Facts["details"]): void => { const dossier = observation.operation === "analyze_function" @@ -290,20 +273,12 @@ const functionFact = async ( }; }; -/** Consume normalized producer facts once; never infer body extents from neighbor entries. */ +/** Consume application-admitted normalized facts; never infer body extents from neighbor entries. */ export const readFunctionInventory = async ( inventory: Evidence, observations: readonly Evidence[], checkpoint: NativeMatchingCheckpoint, ): Promise => { - if ( - inventory.operation !== "list_procedures" || - inventory.predicate_type !== "rea.analysis" || - inventory.subject === null - ) - throw new EvidenceIntegrityError( - "Expected artifact-bound list_procedures Evidence", - ); const rows = sourcePayload(matchingRowsSchema, inventory.normalized_result); const facts: Facts = { details: new Map(), @@ -312,7 +287,6 @@ export const readFunctionInventory = async ( }; for (const observation of observations) { await checkpoint(); - requireContext(inventory, observation); switch (observation.operation) { case "list_names": await addNames(observation, facts.symbols, checkpoint); @@ -324,10 +298,6 @@ export const readFunctionInventory = async ( case "analyze_function": addBody(observation, facts.details); break; - default: - throw new EvidenceIntegrityError( - "Function matching observations must be list_names, procedure_info, read_bytes or analyze_function Evidence", - ); } } const functions: MatchingFunction[] = [];