mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(native): validate matching observation context at selection
This commit is contained in:
1 parent
5e8f8b3163
commit
cebd46d15b
4 files changed
+84
-34
No files matched your search
@@ -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
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
@@ -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<Evidence[]> => {
|
||||
const result: Evidence[] = [];
|
||||
const seen = new Set<string>();
|
||||
@@ -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())
|
||||
|
||||
@@ -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<MatchingFunction[]> => {
|
||||
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[] = [];
|
||||
|
||||
Reference in new issue
Block a user