mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd234dedfa | ||
|
|
1f4edf466c | ||
|
|
ac30fbd658 | ||
|
|
5d21bddb18 | ||
|
|
95c8fc2aa5 | ||
|
|
2e2f981ccf | ||
|
|
f8bb792f15 | ||
|
|
367be83922 | ||
|
|
670d025e33 | ||
|
|
ef5a7e68b8 | ||
|
|
b7160bd7c6 | ||
|
|
8dd4cf4a9d | ||
|
|
30f5df2cd9 | ||
|
|
fd3b42b285 | ||
|
|
3310d82413 |
No files matched your search
@@ -310,8 +310,8 @@ entry it does not read. The module reads the central directory itself, as REA
|
||||
does, and shows `zipfile` only the records it lists, reading each entry in
|
||||
bounded chunks no further than a byte past its declared size. A JSON file
|
||||
holding a number JavaScript cannot read exactly, such as an integer above 2^53,
|
||||
or nested more than 500 levels deep, gets an `error` instead of a view, as in
|
||||
REA, while text that is not one JSON
|
||||
or whose retained value nests more than 500 levels deep, gets an `error` instead of a view, as in
|
||||
REA. Duplicate keys keep their last value before the depth check. Text that is not one JSON
|
||||
document, such as JSON Lines, stays text. Format detection follows REA's
|
||||
rules. Files larger than 64 MiB get an `error` instead of a view
|
||||
and are not read. XML is read with Python's `expat` twice: with namespace
|
||||
|
||||
+21
-6
@@ -129,7 +129,15 @@ side declares a required facet unavailable. An empty legacy facet without a
|
||||
coverage declaration is also unknown. A producer that actually observed an
|
||||
empty facet can declare it available so observed absence remains comparable.
|
||||
Call comparison additionally retains the Evidence relationship coverage
|
||||
checks. Ghidra and Hopper declare their observed dossier facets and complete
|
||||
checks. Its call status compares the provider-observed sets; discovery
|
||||
limitations stay on that dimension even when the observed sets match or differ.
|
||||
The calls dimension's `discovery_status` records whether discovery is complete,
|
||||
partial, or unknown across both sides and directions; older comparisons may omit
|
||||
it. Incomplete or unreported discovery also remains a residual unknown and an
|
||||
unresolved `find_changed_behavior` finding, including when observed sets match.
|
||||
A structural reconstruction claim about calls remains unknown unless both
|
||||
directions have established static direct-call discovery. An independent
|
||||
observed change in another dimension can still fail an overall claim. Ghidra and Hopper declare their observed dossier facets and complete
|
||||
provider-observed caller/callee sets, including empty sets. Missing decompiler
|
||||
output remains unavailable; complete call coverage does not claim every runtime
|
||||
target. When Ghidra's decompiler fails on one function, its dossier is still
|
||||
@@ -206,11 +214,18 @@ relationship coverage and read-specific `limitations`, alongside observed
|
||||
`calls`. When a provider does not classify its caveats, they remain on that
|
||||
node and coverage is `unknown`. An empty call list with partial or unknown
|
||||
coverage is not proof that the function has no calls.
|
||||
`trace_call_path` reports each limited expansion in `residual_unknowns` and
|
||||
sets `truncated: true`; any path already supported by observed edges remains
|
||||
visible. When a requested goal is not reached but an expansion is unresolved,
|
||||
`goal_status` is `unknown`; `not_reached` means traversal exhausted without
|
||||
unresolved relationship reads. Cancellation remains a tool error.
|
||||
`trace_call_path` reports limited relationship reads in `residual_unknowns` and
|
||||
sets `truncated: true`; any path supported by observed edges remains visible.
|
||||
`search_scope.observed_goal_status` describes the search of the provider-observed
|
||||
graph, while `search_scope.discovery` describes whether discovery of applicable
|
||||
direct relationships is established. A missing goal is `unknown` when either
|
||||
discovery or a relationship read is unestablished. A found path is shortest
|
||||
within the observed graph; undiscovered calls may produce a shorter path.
|
||||
`truncated` describes relationship reads, not unestablished discovery.
|
||||
`build_call_path` applies the same distinction to supplied function Evidence:
|
||||
its `search_scope.exhaustive` describes the supplied graph and
|
||||
`search_scope.observed_status` preserves that graph's result. Evidence without
|
||||
discovery metadata has unknown discovery. Cancellation remains a tool error.
|
||||
|
||||
`trace_feature` validates each reference's containing-procedure resolution using
|
||||
the same contract as navigation context. Explicit absence remains a successful
|
||||
|
||||
+5
-1
@@ -178,7 +178,11 @@ does not establish another instance.
|
||||
provider-observed direct relationship set. `complete` means that set was
|
||||
returned without a known read limit; it does not claim every possible runtime
|
||||
edge. Provider and execution caveats stay in `limitations` and do not imply
|
||||
incomplete relationship coverage. Missing coverage remains `unknown`.
|
||||
incomplete relationship coverage. Directional `discovery` separately describes
|
||||
whether applicable static relationships could have been missed by discovery;
|
||||
missing discovery is unknown. Preserve paths supported by observed edges,
|
||||
and qualify negative and shortest-path claims by discovery scope. Missing
|
||||
relationship coverage remains `unknown`.
|
||||
- Declare process, filesystem, network, UI, and mutation effects truthfully;
|
||||
enforce the requested target and lifecycle.
|
||||
- Add limits only for real format, protocol, authority, or measured resource
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CallRelationshipDirection } from "../contracts/enhancedInputs.js";
|
||||
import {
|
||||
CALL_RELATIONSHIP_TRAVERSALS,
|
||||
cleanupStoppedTraversal,
|
||||
readCallRelationships,
|
||||
type CallRelationshipObserver,
|
||||
@@ -18,7 +19,11 @@ import {
|
||||
type EnhancedResult,
|
||||
} from "./EnhancedToolTypes.js";
|
||||
import { resolveProcedureAddress } from "./ProcedureAddressResolution.js";
|
||||
import type { RelationshipCoverage } from "../domain/relationshipCoverage.js";
|
||||
import {
|
||||
combineRelationshipDiscovery,
|
||||
type RelationshipCoverage,
|
||||
type RelationshipDiscoveryStatus,
|
||||
} from "../domain/relationshipCoverage.js";
|
||||
|
||||
type AnalysisCall = (
|
||||
name: AnalysisOperation,
|
||||
@@ -57,6 +62,7 @@ interface TraceState {
|
||||
readonly residual: Set<string>;
|
||||
readonly limitations: Set<string>;
|
||||
readonly relationshipCoverage: Map<string, RelationshipCoverage>;
|
||||
discovery: RelationshipDiscoveryStatus;
|
||||
queueIndex: number;
|
||||
traversalPath: string[];
|
||||
}
|
||||
@@ -150,6 +156,7 @@ const createTraceState = (input: CallPathTraceInput): TraceState => ({
|
||||
residual: new Set(),
|
||||
limitations: new Set(),
|
||||
relationshipCoverage: new Map(),
|
||||
discovery: "complete",
|
||||
queueIndex: 0,
|
||||
traversalPath: input.goal === input.start ? [input.start] : [],
|
||||
});
|
||||
@@ -159,12 +166,20 @@ const expandAddress = async (
|
||||
expansion: AddressExpansion,
|
||||
): Promise<Result<boolean, AnalysisError>> => {
|
||||
const { input, state, address, current, signal } = expansion;
|
||||
const { relation } = CALL_RELATIONSHIP_TRAVERSALS[input.direction];
|
||||
const read = await readCallRelationships(
|
||||
call,
|
||||
input.direction,
|
||||
address,
|
||||
signal,
|
||||
);
|
||||
if (read.status !== "observed") {
|
||||
state.discovery = combineRelationshipDiscovery(
|
||||
state.discovery,
|
||||
null,
|
||||
relation,
|
||||
);
|
||||
}
|
||||
// A provider failure delivered with cancellation remains the selected outcome.
|
||||
if (
|
||||
read.status === "stopped" ||
|
||||
@@ -193,6 +208,11 @@ const expandAddress = async (
|
||||
reason: "Provider did not report relationship coverage.",
|
||||
};
|
||||
state.relationshipCoverage.set(address, coverage);
|
||||
state.discovery = combineRelationshipDiscovery(
|
||||
state.discovery,
|
||||
coverage,
|
||||
relation,
|
||||
);
|
||||
for (const diagnostic of "diagnostics" in coverage &&
|
||||
Array.isArray(coverage.diagnostics)
|
||||
? coverage.diagnostics
|
||||
@@ -255,47 +275,59 @@ const recordEdge = (
|
||||
});
|
||||
};
|
||||
|
||||
const projectTrace = (input: CallPathTraceInput, state: TraceState) => ({
|
||||
start: input.start,
|
||||
goal: input.goal ?? null,
|
||||
direction: input.direction,
|
||||
goal_status:
|
||||
const projectTrace = (input: CallPathTraceInput, state: TraceState) => {
|
||||
const observedGoalStatus =
|
||||
input.goal === undefined
|
||||
? "not_requested"
|
||||
: state.traversalPath.length > 0
|
||||
? "reached"
|
||||
: state.residual.size > 0
|
||||
? "unknown"
|
||||
: "not_reached",
|
||||
nodes: [...state.visited.entries()]
|
||||
.map(([address, { depth }]) => ({ address, depth }))
|
||||
.sort(
|
||||
: "not_reached";
|
||||
return {
|
||||
start: input.start,
|
||||
goal: input.goal ?? null,
|
||||
direction: input.direction,
|
||||
goal_status:
|
||||
observedGoalStatus === "not_reached" && state.discovery !== "complete"
|
||||
? "unknown"
|
||||
: observedGoalStatus,
|
||||
search_scope: {
|
||||
graph: "provider_observed_graph",
|
||||
discovery: state.discovery,
|
||||
observed_goal_status: observedGoalStatus,
|
||||
},
|
||||
nodes: [...state.visited.entries()]
|
||||
.map(([address, { depth }]) => ({ address, depth }))
|
||||
.sort(
|
||||
(left, right) =>
|
||||
left.depth - right.depth ||
|
||||
compareAddress(left.address, right.address),
|
||||
),
|
||||
edges: [...state.edges.values()].sort(
|
||||
(left, right) =>
|
||||
left.depth - right.depth || compareAddress(left.address, right.address),
|
||||
left.discovery_depth - right.discovery_depth ||
|
||||
compareAddress(left.source_address, right.source_address) ||
|
||||
compareAddress(left.target_address, right.target_address),
|
||||
),
|
||||
edges: [...state.edges.values()].sort(
|
||||
(left, right) =>
|
||||
left.discovery_depth - right.discovery_depth ||
|
||||
compareAddress(left.source_address, right.source_address) ||
|
||||
compareAddress(left.target_address, right.target_address),
|
||||
),
|
||||
traversal_path: state.traversalPath,
|
||||
failures: state.failures,
|
||||
traversal: {
|
||||
nodes_visited: state.visited.size,
|
||||
},
|
||||
relationship_coverage: Object.fromEntries(
|
||||
[...state.relationshipCoverage.entries()].sort(([left], [right]) =>
|
||||
compareAddress(left, right),
|
||||
traversal_path: state.traversalPath,
|
||||
failures: state.failures,
|
||||
traversal: {
|
||||
nodes_visited: state.visited.size,
|
||||
},
|
||||
relationship_coverage: Object.fromEntries(
|
||||
[...state.relationshipCoverage.entries()].sort(([left], [right]) =>
|
||||
compareAddress(left, right),
|
||||
),
|
||||
),
|
||||
),
|
||||
truncated: state.residual.size > 0,
|
||||
residual_unknowns: [...state.residual],
|
||||
limitations: [
|
||||
"Direct provider call relationships may omit unresolved indirect calls.",
|
||||
...state.limitations,
|
||||
],
|
||||
});
|
||||
truncated: state.residual.size > 0,
|
||||
residual_unknowns: [...state.residual],
|
||||
limitations: [
|
||||
"Direct provider call relationships may omit unresolved indirect calls.",
|
||||
...state.limitations,
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const compareAddress = (left: string, right: string): number =>
|
||||
left < right ? -1 : left > right ? 1 : 0;
|
||||
|
||||
@@ -24,7 +24,8 @@ export const workflowAnalysisProfile = (
|
||||
? { workflow_revision: 4 }
|
||||
: operation === "get_call_graph"
|
||||
? { workflow_revision: 3 }
|
||||
: operation === "trace_native_values"
|
||||
: operation === "trace_call_path" ||
|
||||
operation === "trace_native_values"
|
||||
? { workflow_revision: 2 }
|
||||
: { workflow_revision: 1 }),
|
||||
});
|
||||
@@ -1490,7 +1490,7 @@ describe("executions that report truncated references", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("revises the trace_feature workflow profile so earlier results are not replayed", () => {
|
||||
it("revises trace workflow profiles so earlier results are not replayed", () => {
|
||||
const profile = ANALYSIS_SNAPSHOT_PROFILE;
|
||||
|
||||
expect(
|
||||
@@ -1502,9 +1502,12 @@ it("revises the trace_feature workflow profile so earlier results are not replay
|
||||
workflowAnalysisProfile(profile),
|
||||
);
|
||||
expect(
|
||||
workflowAnalysisProfile(profile, "trace_call_path"),
|
||||
"other workflows keep their profile",
|
||||
).toEqual(workflowAnalysisProfile(profile));
|
||||
workflowAnalysisProfile(profile, "trace_call_path").parameters
|
||||
.workflow_revision,
|
||||
).toBe(2);
|
||||
expect(workflowAnalysisProfile(profile, "trace_call_path")).not.toEqual(
|
||||
workflowAnalysisProfile(profile),
|
||||
);
|
||||
});
|
||||
|
||||
it("binds native value-trace absence semantics without changing other workflows", () => {
|
||||
@@ -1514,7 +1517,7 @@ it("binds native value-trace absence semantics without changing other workflows"
|
||||
"recorded-versus-unavailable-v1",
|
||||
);
|
||||
expect(valueTrace.digest).not.toBe(workflowAnalysisProfile(profile).digest);
|
||||
expect(workflowAnalysisProfile(profile, "trace_call_path")).toEqual(
|
||||
expect(workflowAnalysisProfile(profile, "xrefs")).toEqual(
|
||||
workflowAnalysisProfile(profile),
|
||||
);
|
||||
});
|
||||
@@ -1,5 +1,8 @@
|
||||
import { functionComparisonInputSchema } from "../../domain/functionComparisonSchemas.js";
|
||||
import { FUNCTION_COMPARISON_PROVIDER } from "../../domain/comparisonEvidenceIdentity.js";
|
||||
import {
|
||||
FUNCTION_COMPARISON_PROFILE,
|
||||
FUNCTION_COMPARISON_PROVIDER,
|
||||
} from "../../domain/comparisonEvidenceIdentity.js";
|
||||
import { compareFunctions } from "../../domain/functionComparison.js";
|
||||
import { parseFunctionEvidence } from "../../domain/functionDossierEvidence.js";
|
||||
import { createEvidence } from "../../domain/evidence.js";
|
||||
@@ -50,6 +53,7 @@ export const compareFunctionEvidence = (
|
||||
const evidence = createEvidence(undefined, FUNCTION_COMPARISON_PROVIDER, {
|
||||
predicateType: "rea.function-comparison",
|
||||
operation: "compare_functions",
|
||||
analysisProfile: FUNCTION_COMPARISON_PROFILE,
|
||||
parameters: {
|
||||
left_evidence_id: leftEvidence.evidence_id,
|
||||
right_evidence_id: rightEvidence.evidence_id,
|
||||
@@ -81,29 +85,44 @@ const comparisonUnknownInput = (
|
||||
leftIds: readonly string[],
|
||||
rightIds: readonly string[],
|
||||
comparisonEvidenceId: string,
|
||||
): RecordUnknownInput | undefined =>
|
||||
functionUnknownInput({
|
||||
status: comparison.status,
|
||||
dimensions: comparison.dimensions
|
||||
.filter(({ status }) =>
|
||||
comparison.status === "unknown"
|
||||
? status === "unknown"
|
||||
: status === "changed",
|
||||
)
|
||||
.map(({ dimension }) => dimension),
|
||||
): RecordUnknownInput | undefined => {
|
||||
const discoveryUnresolved = comparison.dimensions.some(
|
||||
({ dimension, discovery_status }) =>
|
||||
dimension === "calls" && discovery_status !== "complete",
|
||||
);
|
||||
const status =
|
||||
comparison.status === "unchanged" && discoveryUnresolved
|
||||
? "unknown"
|
||||
: comparison.status;
|
||||
const dimensions = comparison.dimensions
|
||||
.filter(
|
||||
({ dimension, status: dimensionStatus }) =>
|
||||
(status === "unknown"
|
||||
? dimensionStatus === "unknown"
|
||||
: dimensionStatus === "changed") ||
|
||||
(dimension === "calls" && discoveryUnresolved),
|
||||
)
|
||||
.map(({ dimension }) => dimension);
|
||||
return functionUnknownInput({
|
||||
status,
|
||||
dimensions,
|
||||
discoveryUnresolved,
|
||||
leftIds,
|
||||
rightIds,
|
||||
comparisonEvidenceId,
|
||||
});
|
||||
};
|
||||
|
||||
const functionUnknownInput = ({
|
||||
status,
|
||||
dimensions,
|
||||
discoveryUnresolved,
|
||||
leftIds,
|
||||
rightIds,
|
||||
comparisonEvidenceId,
|
||||
}: {
|
||||
status: ReturnType<typeof compareFunctions>["status"];
|
||||
discoveryUnresolved: boolean;
|
||||
dimensions: readonly ReturnType<
|
||||
typeof compareFunctions
|
||||
>["dimensions"][number]["dimension"][];
|
||||
@@ -118,7 +137,7 @@ const functionUnknownInput = ({
|
||||
const contradictingIds = rightIds.filter((id) => !leftIds.includes(id));
|
||||
const contradictory = status === "changed";
|
||||
return {
|
||||
question: `Function comparison is ${status} (comparison ${comparisonEvidenceId})`,
|
||||
question: `Function comparison is ${status}${discoveryUnresolved ? "; call discovery is not fully established" : ""} (comparison ${comparisonEvidenceId})`,
|
||||
severity: status === "changed" ? "medium" : "high",
|
||||
domain: "function-comparison",
|
||||
comparison_conclusion_scope: {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { buildCallPath } from "../../domain/callPath.js";
|
||||
import { correlateStaticAndRuntime } from "../../domain/staticRuntimeCorrelation.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import { createAnalysisProfile } from "../../domain/analysisProfile.js";
|
||||
import { readImmutableEvidence } from "../../domain/evidenceValidation.js";
|
||||
import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import {
|
||||
@@ -92,6 +93,9 @@ export const buildCallPathEvidence = (
|
||||
const evidence = createEvidence(undefined, CALL_PATH_PROVIDER, {
|
||||
predicateType: "rea.call-path",
|
||||
operation: "build_call_path",
|
||||
analysisProfile: createAnalysisProfile(CALL_PATH_PROVIDER, {
|
||||
workflow_revision: 2,
|
||||
}),
|
||||
parameters: {
|
||||
start: selected.value.input.start.address,
|
||||
goal: selected.value.input.goal.address,
|
||||
@@ -112,7 +116,7 @@ export const buildCallPathEvidence = (
|
||||
{
|
||||
operation: "analyze_function",
|
||||
rationale:
|
||||
"Collect complete callee dossiers for the reported frontier addresses.",
|
||||
"Inspect reachable code and compare independent provider observations where call discovery is uncertain.",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -266,12 +266,10 @@ export const registerBinaryDiagnosticsCommands = (
|
||||
// "0x10" or "1e2".
|
||||
blockSize: cliDecimalField(
|
||||
inspectByteEntropyInputSchema.shape.block_size,
|
||||
).describe(
|
||||
"Power-of-two block size from 256 to 65536 bytes (default 4096)",
|
||||
),
|
||||
).describe("Power-of-two block size from 256 to 65536 bytes"),
|
||||
offset: cliDecimalField(
|
||||
inspectByteEntropyInputSchema.shape.offset,
|
||||
).describe("Window start as a decimal file byte offset (default 0)"),
|
||||
).describe("Window start as a decimal file byte offset"),
|
||||
length: cliDecimalField(
|
||||
inspectByteEntropyInputSchema.shape.length,
|
||||
).describe("Window length in bytes; omit to extend through EOF"),
|
||||
@@ -429,16 +427,16 @@ export const registerBinaryDiagnosticsCommands = (
|
||||
options: z.object({
|
||||
maxFileBytes: cliDecimalField(
|
||||
inspectDevicetreeInputSchema.shape.max_file_bytes,
|
||||
).describe("Maximum FDT file size in bytes (default 4194304)"),
|
||||
).describe("Maximum FDT file size in bytes"),
|
||||
maxNodes: cliDecimalField(
|
||||
inspectDevicetreeInputSchema.shape.max_nodes,
|
||||
).describe("Maximum number of device-tree nodes (default 8192)"),
|
||||
).describe("Maximum number of device-tree nodes"),
|
||||
maxProperties: cliDecimalField(
|
||||
inspectDevicetreeInputSchema.shape.max_properties,
|
||||
).describe("Maximum number of device-tree properties (default 32768)"),
|
||||
).describe("Maximum number of device-tree properties"),
|
||||
maxDepth: cliDecimalField(
|
||||
inspectDevicetreeInputSchema.shape.max_depth,
|
||||
).describe("Maximum device-tree nesting depth (default 64)"),
|
||||
).describe("Maximum device-tree nesting depth"),
|
||||
...analysisEvidenceDeliveryOptions,
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
|
||||
@@ -123,7 +123,7 @@ describe("CLI JSON path normalization", () => {
|
||||
expect.objectContaining({
|
||||
path: ["executable_path"],
|
||||
code: "invalid_format",
|
||||
message: expect.stringContaining("absolute local filesystem path"),
|
||||
format: "regex",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -87,7 +87,7 @@ export const ENHANCED_TOOL_CONTRACTS = [
|
||||
),
|
||||
enhanced(
|
||||
"trace_call_path",
|
||||
"Resolve start and optional goal symbols or addresses to canonical procedure entries, then trace direct callers or callees until the graph is exhausted or the goal is reached. Results use canonical addresses; Evidence preserves supplied selectors. Returns visited nodes, direct-call edges, a shortest traversal path, provider failures, and residual unknowns. A requested goal is unknown when unresolved expansions prevent proving it absent; unresolved indirect calls remain unknown.",
|
||||
"Resolve start and optional goal symbols or addresses, then trace direct callers or callees. Returns observed edges and a shortest path within the provider-observed graph. search_scope distinguishes the observed-graph result from whether discovery is established; a missing goal is unknown when relationship reads or discovery are incomplete. Unresolved indirect calls remain unknown.",
|
||||
enhancedInputSchemas.trace_call_path,
|
||||
),
|
||||
enhanced(
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { createEvidence } from "../domain/evidence.js";
|
||||
import { FUNCTION_COMPARISON_PROVIDER } from "../domain/comparisonEvidenceIdentity.js";
|
||||
import {
|
||||
FUNCTION_COMPARISON_PROFILE,
|
||||
FUNCTION_COMPARISON_PROVIDER,
|
||||
} from "../domain/comparisonEvidenceIdentity.js";
|
||||
import { compareProcessCaptureEvidence } from "../domain/process/processComparisonEvidence.js";
|
||||
import { PROCESS_PROVIDER } from "../domain/process/processEvidenceProvider.js";
|
||||
import { parseProcessCapture } from "../domain/process/processCaptureParsing.js";
|
||||
@@ -26,6 +29,7 @@ export const FUNCTION_COMPARISON_EVIDENCE = createEvidence(
|
||||
{
|
||||
predicateType: "rea.function-comparison",
|
||||
operation: "compare_functions",
|
||||
analysisProfile: FUNCTION_COMPARISON_PROFILE,
|
||||
parameters: {
|
||||
left_evidence_id: left.value.evidence.evidence_id,
|
||||
right_evidence_id: right.value.evidence.evidence_id,
|
||||
|
||||
@@ -279,6 +279,16 @@ const callPathTraceOutput = evidenceResultOf(
|
||||
goal: z.string().nullable(),
|
||||
direction: z.enum(["forward", "backward"]),
|
||||
goal_status: z.enum(["not_requested", "reached", "not_reached", "unknown"]),
|
||||
search_scope: z.object({
|
||||
graph: z.literal("provider_observed_graph"),
|
||||
discovery: z.enum(["complete", "partial", "unknown"]),
|
||||
observed_goal_status: z.enum([
|
||||
"not_requested",
|
||||
"reached",
|
||||
"not_reached",
|
||||
"unknown",
|
||||
]),
|
||||
}),
|
||||
nodes: z.array(
|
||||
z.object({
|
||||
address: z.string(),
|
||||
|
||||
@@ -18,8 +18,10 @@ const referenceCoverage = (
|
||||
source.diagnostics === undefined
|
||||
? {}
|
||||
: { diagnostic_ids: source.diagnostics.map(intern) };
|
||||
const discovery =
|
||||
source.discovery === undefined ? {} : { discovery: source.discovery };
|
||||
if (source.status === "complete")
|
||||
return { status: "complete", ...diagnostic };
|
||||
return { status: "complete", ...diagnostic, ...discovery };
|
||||
const directions =
|
||||
source.limited_directions === undefined
|
||||
? {}
|
||||
@@ -30,12 +32,14 @@ const referenceCoverage = (
|
||||
limitation_ids: source.limitations.map(intern),
|
||||
...directions,
|
||||
...diagnostic,
|
||||
...discovery,
|
||||
}
|
||||
: {
|
||||
status: "unknown",
|
||||
reason_id: intern(source.reason),
|
||||
...directions,
|
||||
...diagnostic,
|
||||
...discovery,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+44
-13
@@ -196,7 +196,15 @@ describe("call path reconstruction", () => {
|
||||
});
|
||||
|
||||
it("only reports not_found for exhaustive closure", () => {
|
||||
const notFound = run([observe("0x1000", []), observe("0x4000", [])]);
|
||||
const notFound = run([
|
||||
observe("0x1000", [], "rea-workflow", {
|
||||
relationshipCoverage: {
|
||||
status: "complete",
|
||||
discovery: { callees: { status: "complete" } },
|
||||
},
|
||||
}),
|
||||
observe("0x4000", []),
|
||||
]);
|
||||
expect(notFound.status).toBe("not_found");
|
||||
expect(
|
||||
callPathResultSchema.safeParse({
|
||||
@@ -227,12 +235,6 @@ describe("call path reconstruction", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("searches the complete supplied graph", () => {
|
||||
expect(
|
||||
run([observe("0x1000", ["0x2000"]), observe("0x2000", [])]).status,
|
||||
).toBe("not_found");
|
||||
});
|
||||
|
||||
it("returns all shortest paths inline without paging", () => {
|
||||
const result = run([
|
||||
observe("0x1000", ["0x2000", "0x3000"]),
|
||||
@@ -246,6 +248,19 @@ describe("call path reconstruction", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps an exhaustive supplied graph distinct from unknown discovery", () => {
|
||||
expect(
|
||||
run([observe("0x1000", ["0x2000"]), observe("0x2000", [])]),
|
||||
).toMatchObject({
|
||||
status: "unknown",
|
||||
search_scope: {
|
||||
exhaustive: true,
|
||||
discovery: "unknown",
|
||||
observed_status: "not_found",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe("call path topology and capacity", () => {
|
||||
it("retains every distinct generic limitation beyond the push argument limit", () => {
|
||||
const limitations = Array.from(
|
||||
@@ -430,6 +445,7 @@ describe("directional call relationship coverage", () => {
|
||||
status: "partial",
|
||||
limitations: [limitation],
|
||||
limited_directions: directions,
|
||||
discovery: { callees: { status: "complete" } },
|
||||
};
|
||||
const result = run([
|
||||
observe("0x1000", ["0x2000"], "rea-workflow", { relationshipCoverage }),
|
||||
@@ -514,14 +530,14 @@ describe("call path search extent", () => {
|
||||
label: "expanded cycle without the goal",
|
||||
graph: { "0x1000": ["0x2000"], "0x2000": ["0x1000", "0x2000"] },
|
||||
goal: "0x4000",
|
||||
status: "not_found",
|
||||
status: "unknown",
|
||||
explored: { nodes: 2, edges: 3, depth_reached: 1 },
|
||||
},
|
||||
{
|
||||
label: "unreachable goal with its own dossier",
|
||||
graph: { "0x1000": ["0x2000"], "0x2000": [], "0x4000": ["0x5000"] },
|
||||
goal: "0x4000",
|
||||
status: "not_found",
|
||||
status: "unknown",
|
||||
explored: { nodes: 2, edges: 1, depth_reached: 1 },
|
||||
},
|
||||
{
|
||||
@@ -630,10 +646,18 @@ describe("explicit call relationship coverage", () => {
|
||||
"Callers: Upstream xrefs do not establish direct caller relationships.";
|
||||
const result = run([
|
||||
observe("0x1000", ["0x2000"], "rea-workflow", {
|
||||
relationshipCoverage: { status: "unknown", reason },
|
||||
relationshipCoverage: {
|
||||
status: "unknown",
|
||||
reason,
|
||||
discovery: { callees: { status: "complete" } },
|
||||
},
|
||||
}),
|
||||
observe("0x2000", [], "rea-workflow", {
|
||||
relationshipCoverage: { status: "unknown", reason },
|
||||
relationshipCoverage: {
|
||||
status: "unknown",
|
||||
reason,
|
||||
discovery: { callees: { status: "complete" } },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(result).toMatchObject({
|
||||
@@ -683,7 +707,10 @@ describe("explicit call relationship coverage", () => {
|
||||
const reason = "An unrelated function's callees were not observed.";
|
||||
const result = run([
|
||||
observe("0x1000", [], "rea-workflow", {
|
||||
relationshipCoverage: { status: "complete" },
|
||||
relationshipCoverage: {
|
||||
status: "complete",
|
||||
discovery: { callees: { status: "complete" } },
|
||||
},
|
||||
}),
|
||||
observe("0x2000", ["0x5000"], "rea-workflow", {
|
||||
relationshipCoverage: { status: "unknown", reason },
|
||||
@@ -732,7 +759,11 @@ describe("call path evidence admission", () => {
|
||||
run([observe("0x001000", [])], {
|
||||
start: { address: "0X00001000" },
|
||||
}),
|
||||
).toMatchObject({ status: "not_found", start: "0x1000" });
|
||||
).toMatchObject({
|
||||
status: "unknown",
|
||||
start: "0x1000",
|
||||
search_scope: { observed_status: "not_found" },
|
||||
});
|
||||
expect(() =>
|
||||
run([observe("0x1000", [])], { start: { address: "1000" } }),
|
||||
).toThrow();
|
||||
|
||||
+31
-3
@@ -1,5 +1,9 @@
|
||||
import { z } from "zod";
|
||||
import { relationshipCoverageLimitations } from "./relationshipCoverage.js";
|
||||
import {
|
||||
combineRelationshipDiscovery,
|
||||
relationshipCoverageLimitations,
|
||||
type RelationshipDiscoveryStatus,
|
||||
} from "./relationshipCoverage.js";
|
||||
import {
|
||||
callPathInputSchema,
|
||||
callPathResultSchema,
|
||||
@@ -79,6 +83,7 @@ interface SearchState {
|
||||
*/
|
||||
readonly explored: CallPathResult["explored"];
|
||||
readonly exhaustive: boolean;
|
||||
readonly discovery: RelationshipDiscoveryStatus;
|
||||
readonly limitations: readonly string[];
|
||||
}
|
||||
|
||||
@@ -126,7 +131,16 @@ export const buildCallPath = (input: unknown): CallPathResult => {
|
||||
evidence_links: uniqueEvidence(snapshots.values()),
|
||||
limitations: [...new Set(limitations)].sort(compareUnicodeCodePoints),
|
||||
};
|
||||
const searchScope = { exhaustive };
|
||||
const searchScope = {
|
||||
exhaustive,
|
||||
graph: "supplied_evidence" as const,
|
||||
discovery: search.discovery,
|
||||
observed_status: found
|
||||
? ("found" as const)
|
||||
: exhaustive
|
||||
? ("not_found" as const)
|
||||
: ("unknown" as const),
|
||||
};
|
||||
if (found)
|
||||
return callPathResultSchema.parse({
|
||||
...resultContext,
|
||||
@@ -137,7 +151,8 @@ export const buildCallPath = (input: unknown): CallPathResult => {
|
||||
});
|
||||
return callPathResultSchema.parse({
|
||||
...resultContext,
|
||||
status: exhaustive ? "not_found" : "unknown",
|
||||
status:
|
||||
exhaustive && search.discovery === "complete" ? "not_found" : "unknown",
|
||||
shortest_hops: null,
|
||||
search_scope: searchScope,
|
||||
paths: [],
|
||||
@@ -259,6 +274,7 @@ const inspectSearch = ({
|
||||
const queue = [start];
|
||||
const limitations: string[] = [];
|
||||
let incompleteExpandedCoverage = false;
|
||||
let discovery: RelationshipDiscoveryStatus = "complete";
|
||||
let expandedEdges = 0;
|
||||
let depthReached = 0;
|
||||
for (let index = 0; index < queue.length; index += 1) {
|
||||
@@ -282,6 +298,16 @@ const inspectSearch = ({
|
||||
// cannot add a path to the goal, so its dossier limits are contextual
|
||||
// but do not make the searched start-to-goal closure incomplete.
|
||||
if (node === goal) continue;
|
||||
const discoveryFact = coverage?.discovery?.callees;
|
||||
if (
|
||||
discoveryFact !== undefined &&
|
||||
discoveryFact.status !== "complete" &&
|
||||
discoveryFact.reason !== undefined
|
||||
)
|
||||
limitations.push(
|
||||
`Call discovery for ${node} is ${discoveryFact.status}: ${discoveryFact.reason}`,
|
||||
);
|
||||
discovery = combineRelationshipDiscovery(discovery, coverage, "callees");
|
||||
if (snapshot.unresolvedCallCaveats.length > 0)
|
||||
incompleteExpandedCoverage = true;
|
||||
const callees = snapshot.dossier.facet_availability?.callees;
|
||||
@@ -306,6 +332,7 @@ const inspectSearch = ({
|
||||
continue;
|
||||
}
|
||||
if (snapshot === undefined) {
|
||||
discovery = combineRelationshipDiscovery(discovery, null, "callees");
|
||||
limitations.push(
|
||||
`No analyze_function Evidence covers reachable function ${node}`,
|
||||
);
|
||||
@@ -331,6 +358,7 @@ const inspectSearch = ({
|
||||
depth_reached: depthReached,
|
||||
},
|
||||
exhaustive: !incompleteExpandedCoverage,
|
||||
discovery,
|
||||
limitations,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -60,6 +60,11 @@ const resultContextShape = {
|
||||
evidence_links: z.array(evidenceIdSchema).min(1),
|
||||
limitations: z.array(z.string()),
|
||||
};
|
||||
const searchScope = {
|
||||
graph: z.literal("supplied_evidence"),
|
||||
discovery: z.enum(["complete", "partial", "unknown"]),
|
||||
observed_status: z.enum(["found", "not_found", "unknown"]),
|
||||
};
|
||||
|
||||
/** Evidence-cited directed call-path result containing every shortest path. */
|
||||
export const callPathResultSchema = z.union([
|
||||
@@ -67,21 +72,42 @@ export const callPathResultSchema = z.union([
|
||||
...resultContextShape,
|
||||
status: z.literal("found"),
|
||||
shortest_hops: z.int().min(0),
|
||||
search_scope: z.object({ exhaustive: z.boolean() }),
|
||||
search_scope: z.object({
|
||||
...searchScope,
|
||||
exhaustive: z.boolean(),
|
||||
observed_status: z.literal("found"),
|
||||
}),
|
||||
paths: z.array(pathSchema).min(1),
|
||||
}),
|
||||
z.object({
|
||||
...resultContextShape,
|
||||
status: z.literal("not_found"),
|
||||
shortest_hops: z.null(),
|
||||
search_scope: z.object({ exhaustive: z.literal(true) }),
|
||||
search_scope: z.object({
|
||||
...searchScope,
|
||||
exhaustive: z.literal(true),
|
||||
discovery: z.literal("complete"),
|
||||
observed_status: z.literal("not_found"),
|
||||
}),
|
||||
paths: emptyArraySchema,
|
||||
}),
|
||||
z.object({
|
||||
...resultContextShape,
|
||||
status: z.literal("unknown"),
|
||||
shortest_hops: z.null(),
|
||||
search_scope: z.object({ exhaustive: z.literal(false) }),
|
||||
search_scope: z.union([
|
||||
z.object({
|
||||
...searchScope,
|
||||
exhaustive: z.literal(false),
|
||||
observed_status: z.literal("unknown"),
|
||||
}),
|
||||
z.object({
|
||||
...searchScope,
|
||||
exhaustive: z.literal(true),
|
||||
discovery: z.enum(["partial", "unknown"]),
|
||||
observed_status: z.literal("not_found"),
|
||||
}),
|
||||
]),
|
||||
paths: emptyArraySchema,
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -51,7 +51,7 @@ const captureInputShape = z.strictObject({
|
||||
.int()
|
||||
.nonnegative()
|
||||
.default(0)
|
||||
.describe("Skip this many matching packets after filtering; default 0"),
|
||||
.describe("Skip this many matching packets after filtering"),
|
||||
match_limit: z
|
||||
.number()
|
||||
.int()
|
||||
|
||||
@@ -5,9 +5,7 @@ export const inspectPacketFieldsInputSchema = z.strictObject({
|
||||
kind: z
|
||||
.enum(["protocols", "fields"])
|
||||
.default("protocols")
|
||||
.describe(
|
||||
"Return protocol registrations or field definitions; defaults to protocols",
|
||||
),
|
||||
.describe("Return protocol registrations or field definitions"),
|
||||
protocol: z
|
||||
.string()
|
||||
.min(1)
|
||||
@@ -30,7 +28,7 @@ export const inspectPacketFieldsInputSchema = z.strictObject({
|
||||
.nonnegative()
|
||||
.max(Number.MAX_SAFE_INTEGER)
|
||||
.default(0)
|
||||
.describe("Number of matching records to skip; defaults to 0"),
|
||||
.describe("Number of matching records to skip"),
|
||||
limit: z
|
||||
.number()
|
||||
.int()
|
||||
|
||||
@@ -297,24 +297,38 @@ const functionFindings = (
|
||||
>,
|
||||
): Finding[] => {
|
||||
const { evidence, result } = comparison;
|
||||
return result.dimensions.flatMap((dimension) =>
|
||||
dimension.status === "unchanged"
|
||||
? []
|
||||
: [
|
||||
makeFinding({
|
||||
evidence,
|
||||
dimension: dimension.dimension,
|
||||
scope: "static_candidate",
|
||||
status: dimension.status,
|
||||
classification:
|
||||
dimension.status === "unknown"
|
||||
? "unresolved_branch"
|
||||
: dimension.conclusion_kind,
|
||||
limitations: dimension.limitations,
|
||||
links: dimension.evidence_links,
|
||||
}),
|
||||
],
|
||||
);
|
||||
return result.dimensions.flatMap((dimension) => {
|
||||
const finding = (status: "changed" | "unknown") =>
|
||||
makeFinding({
|
||||
evidence,
|
||||
dimension: dimension.dimension,
|
||||
scope: "static_candidate",
|
||||
status,
|
||||
classification:
|
||||
status === "unknown"
|
||||
? "unresolved_branch"
|
||||
: dimension.conclusion_kind,
|
||||
limitations:
|
||||
status === "unknown" &&
|
||||
dimension.dimension === "calls" &&
|
||||
dimension.discovery_status === undefined
|
||||
? [
|
||||
...dimension.limitations,
|
||||
"Static call discovery coverage was not recorded in the comparison.",
|
||||
]
|
||||
: dimension.limitations,
|
||||
links: dimension.evidence_links,
|
||||
});
|
||||
const findings =
|
||||
dimension.status === "unchanged" ? [] : [finding(dimension.status)];
|
||||
if (
|
||||
dimension.dimension === "calls" &&
|
||||
dimension.status !== "unknown" &&
|
||||
dimension.discovery_status !== "complete"
|
||||
)
|
||||
findings.push(finding("unknown"));
|
||||
return findings;
|
||||
});
|
||||
};
|
||||
|
||||
const artifactFindings = (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Evidence } from "./evidence.js";
|
||||
import { createAnalysisProfile } from "./analysisProfile.js";
|
||||
import {
|
||||
PROCESS_PROVIDER,
|
||||
isProcessEvidenceProvider,
|
||||
@@ -18,6 +19,11 @@ export const FUNCTION_COMPARISON_PROVIDER = {
|
||||
version: "1",
|
||||
} as const;
|
||||
|
||||
export const FUNCTION_COMPARISON_PROFILE = createAnalysisProfile(
|
||||
FUNCTION_COMPARISON_PROVIDER,
|
||||
{ workflow_revision: 2 },
|
||||
);
|
||||
|
||||
const COMPARISON_EVIDENCE_IDENTITIES = {
|
||||
compare_artifacts: {
|
||||
predicate: "rea.artifact-comparison",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PYTHON_JSON_READER } from "./appExportPythonJson.js";
|
||||
import { PYTHON_XML_READER } from "./appExportPythonXml.js";
|
||||
import { PYTHON_ZIP_HELPERS } from "./appExportPythonZip.js";
|
||||
|
||||
@@ -12,6 +13,7 @@ export const PYTHON_READY_CHECK = `
|
||||
|
||||
export const PYTHON_IMPORTS = `from __future__ import annotations
|
||||
|
||||
from array import array
|
||||
import contextlib
|
||||
import copy
|
||||
import hashlib
|
||||
@@ -57,11 +59,6 @@ MAX_PARSED_BYTES = 4 * 1024 * 1024
|
||||
PARSED_TOO_LARGE = "larger than the 4 MiB limit for JSON and key file views"
|
||||
MAX_JSON_VIEW_DEPTH = 500
|
||||
JSON_TOO_DEEP = f"nested deeper than the {MAX_JSON_VIEW_DEPTH} levels a JSON view keeps"
|
||||
#: A JSON token after any whitespace, by group: an opening, a closing, a comma, a colon, a string's quote, or a number
|
||||
#: or literal. Strings are read by json's scanner, since a pattern repeating per escape holds memory for each.
|
||||
JSON_TOKEN = re.compile(r'[ \t\n\r]*(?:([\[{])|([\]}])|(,)|(:)|(")|(-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?'
|
||||
r"|true|false|null))")
|
||||
JSON_SPACE = re.compile(r"[ \t\n\r]*")
|
||||
|
||||
|
||||
def _respelled(text: str, home: Path) -> str:
|
||||
@@ -254,143 +251,7 @@ def _keyfile(text: str) -> dict:
|
||||
|
||||
${PYTHON_XML_READER}
|
||||
|
||||
def _not_json(constant: str):
|
||||
raise ValueError(f"{constant} is not JSON")
|
||||
|
||||
|
||||
class _Inexact(Exception):
|
||||
"""A JSON number that REA's state view refuses, since reading it would change it."""
|
||||
|
||||
|
||||
def _digits(text: str) -> str:
|
||||
return re.split(r"[eE]", text.lstrip("-"))[0].replace(".", "").strip("0") or "0"
|
||||
|
||||
|
||||
def _js_string(value: float) -> str:
|
||||
"""How JavaScript's String() writes a finite double: an integer below 1e21 in full, any other in shortest form."""
|
||||
return str(int(value)) if value.is_integer() and abs(value) < 1e21 else repr(value)
|
||||
|
||||
|
||||
def _safe_number(text: str) -> bool:
|
||||
"""lossless-json's isSafeNumber: an integer spelling within 2^53, or a float spelling that keeps its digits."""
|
||||
if re.fullmatch(r"-?[0-9]+", text):
|
||||
return len(text.lstrip("-")) <= 16 and abs(int(text)) <= MAX_SAFE_INTEGER
|
||||
value = float(text)
|
||||
if not math.isfinite(value):
|
||||
return False
|
||||
written = _js_string(value)
|
||||
return written == text or _digits(written) == _digits(text)
|
||||
|
||||
|
||||
def _exact_number(text: str):
|
||||
"""A JSON number read only when reading it changes nothing, as REA's state view rules: its spelling and its
|
||||
written-back form both safe, and not -0."""
|
||||
value = float(text)
|
||||
if (not _safe_number(text) or not math.isfinite(value) or not _safe_number(_js_string(value))
|
||||
or (value == 0 and math.copysign(1, value) < 0)):
|
||||
raise _Inexact(text)
|
||||
return int(text) if re.fullmatch(r"-?[0-9]+", text) else value
|
||||
|
||||
|
||||
_INEXACT = object()
|
||||
_LEVEL_END = object()
|
||||
|
||||
|
||||
def _number_or_mark(spelling: str):
|
||||
"""A JSON number read exactly, or a mark for one REA's state view refuses."""
|
||||
try:
|
||||
return _exact_number(spelling)
|
||||
except _Inexact:
|
||||
return _INEXACT
|
||||
|
||||
|
||||
def _holds_inexact(value) -> bool:
|
||||
"""Whether a parsed JSON value still holds a marked number once duplicate keys kept their last value; one
|
||||
iterator per nesting level, so no child list is copied."""
|
||||
levels = [iter((value,))]
|
||||
while levels:
|
||||
item = next(levels[-1], _LEVEL_END)
|
||||
if item is _LEVEL_END:
|
||||
levels.pop()
|
||||
elif item is _INEXACT:
|
||||
return True
|
||||
elif isinstance(item, dict):
|
||||
levels.append(iter(item.values()))
|
||||
elif isinstance(item, list):
|
||||
levels.append(iter(item))
|
||||
return False
|
||||
|
||||
|
||||
def _deeper_than(root, limit: int) -> bool:
|
||||
"""Whether a parsed JSON value nests deeper than a limit, as REA checks it: a value's depth is the number of
|
||||
iterators open above it, one per nesting level, so no child list is copied."""
|
||||
levels = [iter((root,))]
|
||||
while levels:
|
||||
item = next(levels[-1], _LEVEL_END)
|
||||
if item is _LEVEL_END:
|
||||
levels.pop()
|
||||
elif len(levels) - 1 > limit:
|
||||
return True
|
||||
elif isinstance(item, dict):
|
||||
levels.append(iter(item.values()))
|
||||
elif isinstance(item, list):
|
||||
levels.append(iter(item))
|
||||
return False
|
||||
|
||||
|
||||
def _is_json(text: str) -> bool:
|
||||
"""Whether text is one JSON value as REA's state view reads it, checked a token at a time without building it or
|
||||
recursing: it holds a byte per open container and one string at a time, so text too deep for Python's parser or
|
||||
too large to parse into a view is still told apart from other text."""
|
||||
containers, expect, empty, at = bytearray(), "value", False, 0
|
||||
while True:
|
||||
token = JSON_TOKEN.match(text, at)
|
||||
if token is None:
|
||||
break
|
||||
kind, at = token.lastindex, token.end()
|
||||
if kind == 5:
|
||||
try:
|
||||
at = json.decoder.scanstring(text, at)[1]
|
||||
except ValueError:
|
||||
return False
|
||||
if kind == 1 and expect == "value":
|
||||
containers.append(ord(text[at - 1]))
|
||||
expect, empty = ("value" if text[at - 1] == "[" else "key"), True
|
||||
# A closing bracket's code is its opening's plus two.
|
||||
elif kind == 2 and (expect == "after" or empty) and containers and containers[-1] == ord(text[at - 1]) - 2:
|
||||
containers.pop()
|
||||
expect, empty = "after", False
|
||||
elif kind == 3 and expect == "after" and containers:
|
||||
expect = "value" if containers[-1] == ord("[") else "key"
|
||||
elif kind == 4 and expect == "colon":
|
||||
expect = "value"
|
||||
elif kind >= 5 and (expect == "value" or (kind == 5 and expect == "key")):
|
||||
expect, empty = ("colon" if expect == "key" else "after"), False
|
||||
else:
|
||||
return False
|
||||
return expect == "after" and not containers and JSON_SPACE.fullmatch(text, at) is not None
|
||||
|
||||
|
||||
def _json_view(text: str, size: int):
|
||||
"""A JSON text's format and view, or why it has none, as REA reads it; None when it is not JSON. Text over the
|
||||
parsed-view limit, or nested past Python's recursion limit, is told apart without building its value."""
|
||||
if size > MAX_PARSED_BYTES:
|
||||
return {"format": "json", "view": None, "error": PARSED_TOO_LARGE} if _is_json(text) else None
|
||||
# The whole text must parse, and duplicate keys settle, before an inexact number makes it JSON with an error.
|
||||
try:
|
||||
view = json.loads(text, parse_constant=_not_json, parse_float=_number_or_mark, parse_int=_number_or_mark)
|
||||
except RecursionError:
|
||||
# Nesting too deep for Python to parse is far deeper than REA's view keeps, once the rest of it is JSON too.
|
||||
return {"format": "json", "view": None, "error": JSON_TOO_DEEP} if _is_json(text) else None
|
||||
except ValueError:
|
||||
return None
|
||||
if _holds_inexact(view):
|
||||
return {"format": "json", "view": None,
|
||||
"error": "a number in it cannot be read exactly, such as an integer above 2^53"}
|
||||
if _deeper_than(view, MAX_JSON_VIEW_DEPTH):
|
||||
return {"format": "json", "view": None, "error": JSON_TOO_DEEP}
|
||||
return {"format": "json", "view": view, "truncated": False}
|
||||
|
||||
${PYTHON_JSON_READER}
|
||||
|
||||
def _text_view(name: str, text: str, size: int) -> dict:
|
||||
"""The format REA's state view detects for decoded text of size bytes, and its view; a JSON or key file over the
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
/** JSON parsing and bounded retained-view checks for exported desktop environments. */
|
||||
export const PYTHON_JSON_READER = String.raw`
|
||||
#: A JSON token after any whitespace, by group: an opening, a closing, a comma, a colon, a string's quote, or a number
|
||||
#: or literal. Strings are read by json's scanner, since a pattern repeating per escape holds memory for each.
|
||||
JSON_TOKEN = re.compile(r'[ \t\n\r]*(?:([\[{])|([\]}])|(,)|(:)|(")|(-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?'
|
||||
r"|true|false|null))")
|
||||
JSON_SPACE = re.compile(r"[ \t\n\r]*")
|
||||
|
||||
|
||||
class _Inexact(Exception):
|
||||
"""A JSON number that REA's state view refuses, since reading it would change it."""
|
||||
|
||||
|
||||
def _digits(text: str) -> str:
|
||||
return re.split(r"[eE]", text.lstrip("-"))[0].replace(".", "").strip("0") or "0"
|
||||
|
||||
|
||||
def _js_string(value: float) -> str:
|
||||
"""How JavaScript's String() writes a finite double: an integer below 1e21 in full, any other in shortest form."""
|
||||
return str(int(value)) if value.is_integer() and abs(value) < 1e21 else repr(value)
|
||||
|
||||
|
||||
def _safe_number(text: str) -> bool:
|
||||
"""lossless-json's isSafeNumber: an integer spelling within 2^53, or a float spelling that keeps its digits."""
|
||||
if re.fullmatch(r"-?[0-9]+", text):
|
||||
return len(text.lstrip("-")) <= 16 and abs(int(text)) <= MAX_SAFE_INTEGER
|
||||
value = float(text)
|
||||
if not math.isfinite(value):
|
||||
return False
|
||||
written = _js_string(value)
|
||||
return written == text or _digits(written) == _digits(text)
|
||||
|
||||
|
||||
def _exact_number(text: str):
|
||||
"""A JSON number read only when reading it changes nothing, as REA's state view rules: its spelling and its
|
||||
written-back form both safe, and not -0."""
|
||||
value = float(text)
|
||||
if (not _safe_number(text) or not math.isfinite(value) or not _safe_number(_js_string(value))
|
||||
or (value == 0 and math.copysign(1, value) < 0)):
|
||||
raise _Inexact(text)
|
||||
return int(text) if re.fullmatch(r"-?[0-9]+", text) else value
|
||||
|
||||
|
||||
_INEXACT = object()
|
||||
_DEEP = object()
|
||||
_DEEP_INEXACT = object()
|
||||
_LEVEL_END = object()
|
||||
_INVALID_JSON = object()
|
||||
JSON_ATOMS = {"true": True, "false": False, "null": None}
|
||||
|
||||
|
||||
def _number_or_mark(spelling: str):
|
||||
"""A JSON number read exactly, or a mark for one REA's state view refuses."""
|
||||
try:
|
||||
return _exact_number(spelling)
|
||||
except _Inexact:
|
||||
return _INEXACT
|
||||
|
||||
|
||||
def _holds_inexact(value) -> bool:
|
||||
"""Whether a parsed JSON value still holds a marked number once duplicate keys kept their last value; one
|
||||
iterator per nesting level, so no child list is copied."""
|
||||
levels = [iter((value,))]
|
||||
while levels:
|
||||
item = next(levels[-1], _LEVEL_END)
|
||||
if item is _LEVEL_END:
|
||||
levels.pop()
|
||||
elif item is _INEXACT or item is _DEEP_INEXACT:
|
||||
return True
|
||||
elif isinstance(item, dict):
|
||||
levels.append(iter(item.values()))
|
||||
elif isinstance(item, list):
|
||||
levels.append(iter(item))
|
||||
return False
|
||||
|
||||
|
||||
def _deeper_than(root, limit: int) -> bool:
|
||||
"""Whether a parsed JSON value nests deeper than a limit, as REA checks it: a value's depth is the number of
|
||||
iterators open above it, one per nesting level, so no child list is copied."""
|
||||
levels = [iter((root,))]
|
||||
while levels:
|
||||
item = next(levels[-1], _LEVEL_END)
|
||||
if item is _LEVEL_END:
|
||||
levels.pop()
|
||||
elif item is _DEEP or item is _DEEP_INEXACT:
|
||||
return True
|
||||
elif len(levels) - 1 > limit:
|
||||
return True
|
||||
elif isinstance(item, dict):
|
||||
levels.append(iter(item.values()))
|
||||
elif isinstance(item, list):
|
||||
levels.append(iter(item))
|
||||
return False
|
||||
|
||||
|
||||
def _read_json(text: str, build: bool = True):
|
||||
"""One JSON value, or _INVALID_JSON, read without recursion so duplicate keys settle before depth is checked.
|
||||
Deep containers are summarized rather than built; exact-number marks still follow last-key-wins semantics.
|
||||
With build=False, only syntax is checked: one byte per open container and one decoded string at a time."""
|
||||
containers, expect, empty, at = bytearray(), "value", False, 0
|
||||
values, keys, root = [], [], _INVALID_JSON
|
||||
deep_bad, deep_key_at, bad_keys = bytearray(), array("I"), {}
|
||||
|
||||
def keep(value) -> None:
|
||||
nonlocal root
|
||||
if not values:
|
||||
root = value
|
||||
elif containers[-1] == ord("["):
|
||||
values[-1].append(value)
|
||||
else:
|
||||
values[-1][keys[-1]] = value
|
||||
|
||||
def record_deep(bad: bool) -> None:
|
||||
"""Summarize a finished child; keep only object keys with live inexact values."""
|
||||
depth = len(deep_bad) - 1
|
||||
if containers[-1] == ord("["):
|
||||
deep_bad[-1] |= 2 | bad
|
||||
return
|
||||
marked = bad_keys.get(depth)
|
||||
if bad or marked is not None:
|
||||
key, _ = json.decoder.scanstring(text, deep_key_at[-1])
|
||||
if bad:
|
||||
if marked is None:
|
||||
marked = key
|
||||
elif isinstance(marked, str):
|
||||
if marked != key:
|
||||
marked = {marked, key}
|
||||
else:
|
||||
marked.add(key)
|
||||
else:
|
||||
if isinstance(marked, str):
|
||||
if marked == key:
|
||||
marked = None
|
||||
else:
|
||||
marked.discard(key)
|
||||
if len(marked) == 1:
|
||||
marked = next(iter(marked))
|
||||
if marked is None:
|
||||
bad_keys.pop(depth, None)
|
||||
else:
|
||||
bad_keys[depth] = marked
|
||||
deep_bad[-1] = 2 | (marked is not None)
|
||||
else:
|
||||
deep_bad[-1] |= 2
|
||||
|
||||
while True:
|
||||
token = JSON_TOKEN.match(text, at)
|
||||
if token is None:
|
||||
break
|
||||
kind, at = token.lastindex, token.end()
|
||||
if kind == 5:
|
||||
string_at = at
|
||||
try:
|
||||
value, at = json.decoder.scanstring(text, at)
|
||||
except ValueError:
|
||||
return _INVALID_JSON
|
||||
if kind == 1 and expect == "value":
|
||||
if build:
|
||||
if deep_bad or len(containers) >= MAX_JSON_VIEW_DEPTH:
|
||||
deep_bad.append(0)
|
||||
deep_key_at.append(0)
|
||||
else:
|
||||
value = [] if text[at - 1] == "[" else {}
|
||||
keep(value)
|
||||
values.append(value)
|
||||
keys.append(None)
|
||||
containers.append(ord(text[at - 1]))
|
||||
expect, empty = ("value" if text[at - 1] == "[" else "key"), True
|
||||
# A closing bracket's code is its opening's plus two.
|
||||
elif kind == 2 and (expect == "after" or empty) and containers and containers[-1] == ord(text[at - 1]) - 2:
|
||||
opening = containers.pop()
|
||||
if build:
|
||||
if deep_bad:
|
||||
state = deep_bad.pop()
|
||||
bad = bool(state & 1)
|
||||
deep_key_at.pop()
|
||||
bad_keys.pop(len(deep_bad), None)
|
||||
if deep_bad:
|
||||
record_deep(bad)
|
||||
else:
|
||||
keep((_DEEP_INEXACT if bad else _DEEP) if state & 2 else ([] if opening == ord("[") else {}))
|
||||
else:
|
||||
values.pop()
|
||||
keys.pop()
|
||||
expect, empty = "after", False
|
||||
elif kind == 3 and expect == "after" and containers:
|
||||
expect = "value" if containers[-1] == ord("[") else "key"
|
||||
elif kind == 4 and expect == "colon":
|
||||
expect = "value"
|
||||
elif kind >= 5 and (expect == "value" or (kind == 5 and expect == "key")):
|
||||
if build:
|
||||
if expect == "key":
|
||||
if deep_bad:
|
||||
deep_key_at[-1] = string_at
|
||||
else:
|
||||
keys[-1] = value
|
||||
else:
|
||||
if kind == 6:
|
||||
value = JSON_ATOMS.get(token[6], _INVALID_JSON)
|
||||
if value is _INVALID_JSON:
|
||||
value = _number_or_mark(token[6])
|
||||
if deep_bad:
|
||||
record_deep(value is _INEXACT)
|
||||
else:
|
||||
keep(value)
|
||||
expect, empty = ("colon" if expect == "key" else "after"), False
|
||||
else:
|
||||
return _INVALID_JSON
|
||||
if expect != "after" or containers or JSON_SPACE.fullmatch(text, at) is None:
|
||||
return _INVALID_JSON
|
||||
return root if build else True
|
||||
|
||||
|
||||
def _json_view(text: str, size: int):
|
||||
"""A JSON text's format and view, or why it has none, as REA reads it; None when it is not JSON. Text over the
|
||||
parsed-view limit is told apart without building its value."""
|
||||
if size > MAX_PARSED_BYTES:
|
||||
valid = _read_json(text, build=False) is not _INVALID_JSON
|
||||
return {"format": "json", "view": None, "error": PARSED_TOO_LARGE} if valid else None
|
||||
# The whole text must parse, and duplicate keys settle, before an inexact number makes it JSON with an error.
|
||||
view = _read_json(text)
|
||||
if view is _INVALID_JSON:
|
||||
return None
|
||||
if _holds_inexact(view):
|
||||
return {"format": "json", "view": None,
|
||||
"error": "a number in it cannot be read exactly, such as an integer above 2^53"}
|
||||
if _deeper_than(view, MAX_JSON_VIEW_DEPTH):
|
||||
return {"format": "json", "view": None, "error": JSON_TOO_DEEP}
|
||||
return {"format": "json", "view": view, "truncated": False}
|
||||
|
||||
`;
|
||||
@@ -110,6 +110,10 @@ describe("assessFunctionCallCoverage", () => {
|
||||
],
|
||||
unresolvedCallCaveats: ["Unresolved call at 0x1004: Computed target."],
|
||||
diagnostics: ["Indirect calls are unresolved."],
|
||||
discoveryStatus: "unknown",
|
||||
discoveryLimitations: [
|
||||
"Static direct-call discovery is not established for callers and callees.",
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
relationshipCoverageLimitations,
|
||||
type RelationshipCoverage,
|
||||
type RelationshipDiscoveryStatus,
|
||||
} from "./relationshipCoverage.js";
|
||||
import type { FunctionDossier } from "./native/functionDossier.js";
|
||||
import { uniqueSorted } from "./canonicalOrdering.js";
|
||||
@@ -20,7 +21,18 @@ export const combineFunctionRelationshipCoverage = (
|
||||
const diagnostics = uniqueSorted(
|
||||
reads.flatMap(({ coverage }) => coverage?.diagnostics ?? []),
|
||||
);
|
||||
const details = diagnostics.length > 0 ? { diagnostics } : {};
|
||||
const discovery = {
|
||||
...(callers?.discovery?.callers === undefined
|
||||
? {}
|
||||
: { callers: callers.discovery.callers }),
|
||||
...(callees?.discovery?.callees === undefined
|
||||
? {}
|
||||
: { callees: callees.discovery.callees }),
|
||||
};
|
||||
const details = {
|
||||
...(diagnostics.length > 0 ? { diagnostics } : {}),
|
||||
...(Object.keys(discovery).length === 0 ? {} : { discovery }),
|
||||
};
|
||||
const callersLimited = callers?.status !== "complete";
|
||||
const calleesLimited = callees?.status !== "complete";
|
||||
const limited_directions: Extract<
|
||||
@@ -85,6 +97,10 @@ export interface FunctionCallCoverageAssessment {
|
||||
readonly unresolvedCallCaveats: readonly string[];
|
||||
/** Provider caveats that remain relevant even with complete coverage. */
|
||||
readonly diagnostics: readonly string[];
|
||||
/** Discovery limits do not invalidate comparisons of the observed sets. */
|
||||
readonly discoveryLimitations: readonly string[];
|
||||
/** Whether all requested call discovery directions are established. */
|
||||
readonly discoveryStatus: RelationshipDiscoveryStatus;
|
||||
}
|
||||
|
||||
/** Keep observed-set blockers separate from descriptive provider caveats. */
|
||||
@@ -97,6 +113,25 @@ export const assessFunctionCallCoverage = (
|
||||
({ address, reason }) => `Unresolved call at ${address}: ${reason}`,
|
||||
),
|
||||
);
|
||||
const missingDiscovery: string[] = [];
|
||||
const discoveryLimitations: string[] = [];
|
||||
let discoveryStatus: RelationshipDiscoveryStatus = "complete";
|
||||
for (const direction of ["callers", "callees"] as const) {
|
||||
const discovery = coverage?.discovery?.[direction];
|
||||
if (discovery?.status === "partial") discoveryStatus = "partial";
|
||||
else if (discovery?.status !== "complete" && discoveryStatus !== "partial")
|
||||
discoveryStatus = "unknown";
|
||||
if (discovery?.status === "complete") continue;
|
||||
if (discovery?.reason === undefined) missingDiscovery.push(direction);
|
||||
else
|
||||
discoveryLimitations.push(
|
||||
`Static direct-call discovery for ${direction} is ${discovery.status}: ${discovery.reason}`,
|
||||
);
|
||||
}
|
||||
if (missingDiscovery.length > 0)
|
||||
discoveryLimitations.push(
|
||||
`Static direct-call discovery is not established for ${missingDiscovery.join(" and ")}.`,
|
||||
);
|
||||
return {
|
||||
blockers: uniqueSorted([
|
||||
...relationshipCoverageLimitations(coverage),
|
||||
@@ -104,5 +139,7 @@ export const assessFunctionCallCoverage = (
|
||||
]),
|
||||
unresolvedCallCaveats,
|
||||
diagnostics: uniqueSorted(coverage?.diagnostics ?? []),
|
||||
discoveryLimitations,
|
||||
discoveryStatus,
|
||||
};
|
||||
};
|
||||
@@ -117,7 +117,13 @@ const observe = (
|
||||
digit: string,
|
||||
value: ReturnType<typeof dossier>,
|
||||
providerId = "rea-workflow",
|
||||
relationshipCoverage: RelationshipCoverage | null = { status: "complete" },
|
||||
relationshipCoverage: RelationshipCoverage | null = {
|
||||
status: "complete",
|
||||
discovery: {
|
||||
callers: { status: "complete" },
|
||||
callees: { status: "complete" },
|
||||
},
|
||||
},
|
||||
): Evidence =>
|
||||
createEvidence(
|
||||
{
|
||||
@@ -499,7 +505,7 @@ describe("function call relationship coverage", () => {
|
||||
conclusion_kind: "unresolved_branch",
|
||||
left_count: 0,
|
||||
right_count: 0,
|
||||
limitations: [`Right: ${limitation}`],
|
||||
limitations: expect.arrayContaining([`Right: ${limitation}`]),
|
||||
});
|
||||
expect(result.status).toBe("unknown");
|
||||
},
|
||||
@@ -921,10 +927,18 @@ describe("function comparison", () => {
|
||||
const left = observe("a", dossier("return 0;", "0x1000"), "rea-workflow", {
|
||||
status: "complete",
|
||||
diagnostics: [diagnostic],
|
||||
discovery: {
|
||||
callers: { status: "complete" },
|
||||
callees: { status: "complete" },
|
||||
},
|
||||
});
|
||||
const right = observe("b", dossier("return 0;", "0x2000"), "rea-workflow", {
|
||||
status: "complete",
|
||||
diagnostics: [diagnostic],
|
||||
discovery: {
|
||||
callers: { status: "complete" },
|
||||
callees: { status: "complete" },
|
||||
},
|
||||
});
|
||||
const comparison = compareFunctions(
|
||||
functionSnapshotFixture(left),
|
||||
|
||||
@@ -78,13 +78,18 @@ export const compareDimensions = (
|
||||
];
|
||||
return limitations.length === 0
|
||||
? result
|
||||
: unresolvedDimension({
|
||||
dimension: result.dimension,
|
||||
links: context.links,
|
||||
leftCount: null,
|
||||
rightCount: null,
|
||||
limitations: [...result.limitations, ...limitations],
|
||||
});
|
||||
: {
|
||||
...unresolvedDimension({
|
||||
dimension: result.dimension,
|
||||
links: context.links,
|
||||
leftCount: null,
|
||||
rightCount: null,
|
||||
limitations: [...result.limitations, ...limitations],
|
||||
}),
|
||||
...(result.discovery_status === undefined
|
||||
? {}
|
||||
: { discovery_status: result.discovery_status }),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -594,30 +599,49 @@ const compareCalls = (
|
||||
const coverageDiagnostics = [
|
||||
...left.callCoverageDiagnostics.map((reason) => `Left: ${reason}`),
|
||||
...right.callCoverageDiagnostics.map((reason) => `Right: ${reason}`),
|
||||
...left.callDiscoveryLimitations.map((reason) => `Left: ${reason}`),
|
||||
...right.callDiscoveryLimitations.map((reason) => `Right: ${reason}`),
|
||||
];
|
||||
const discovery_status =
|
||||
left.callDiscoveryStatus === "partial" ||
|
||||
right.callDiscoveryStatus === "partial"
|
||||
? "partial"
|
||||
: left.callDiscoveryStatus === "complete" &&
|
||||
right.callDiscoveryStatus === "complete"
|
||||
? "complete"
|
||||
: "unknown";
|
||||
if (coverageLimitations.length > 0)
|
||||
return unresolvedDimension({
|
||||
dimension: "calls",
|
||||
links: context.links,
|
||||
leftCount: comparison.leftCount,
|
||||
rightCount: comparison.rightCount,
|
||||
limitations: [...coverageLimitations, ...coverageDiagnostics],
|
||||
});
|
||||
return {
|
||||
...unresolvedDimension({
|
||||
dimension: "calls",
|
||||
links: context.links,
|
||||
leftCount: comparison.leftCount,
|
||||
rightCount: comparison.rightCount,
|
||||
limitations: [...coverageLimitations, ...coverageDiagnostics],
|
||||
}),
|
||||
discovery_status,
|
||||
};
|
||||
if (comparison.status === "unknown")
|
||||
return unresolvedDimension({
|
||||
return {
|
||||
...unresolvedDimension({
|
||||
dimension: "calls",
|
||||
links: context.links,
|
||||
leftCount: comparison.leftCount,
|
||||
rightCount: comparison.rightCount,
|
||||
limitations: [...comparison.limitations, ...coverageDiagnostics],
|
||||
}),
|
||||
discovery_status,
|
||||
};
|
||||
return {
|
||||
...dimensionResult({
|
||||
dimension: "calls",
|
||||
links: context.links,
|
||||
leftCount: comparison.leftCount,
|
||||
rightCount: comparison.rightCount,
|
||||
...comparison,
|
||||
...context,
|
||||
textDelta: null,
|
||||
limitations: [...comparison.limitations, ...coverageDiagnostics],
|
||||
});
|
||||
return dimensionResult({
|
||||
dimension: "calls",
|
||||
...comparison,
|
||||
...context,
|
||||
textDelta: null,
|
||||
limitations: [...comparison.limitations, ...coverageDiagnostics],
|
||||
});
|
||||
}),
|
||||
discovery_status,
|
||||
};
|
||||
};
|
||||
|
||||
const compareCfg = (
|
||||
|
||||
@@ -36,6 +36,12 @@ const functionDimensionContextShape = {
|
||||
right_count: z.int().min(0).nullable(),
|
||||
evidence_links: z.array(evidenceIdSchema).min(2),
|
||||
limitations: z.array(z.string()),
|
||||
discovery_status: z
|
||||
.enum(["complete", "partial", "unknown"])
|
||||
.describe(
|
||||
"Static call discovery coverage for the compared observations; omission means unreported.",
|
||||
)
|
||||
.exactOptional(),
|
||||
};
|
||||
|
||||
const functionDimensionSchema = z.union([
|
||||
|
||||
@@ -21,6 +21,8 @@ export interface FunctionSnapshot {
|
||||
readonly callCoverageBlockers: readonly string[];
|
||||
readonly unresolvedCallCaveats: readonly string[];
|
||||
readonly callCoverageDiagnostics: readonly string[];
|
||||
readonly callDiscoveryLimitations: readonly string[];
|
||||
readonly callDiscoveryStatus: "complete" | "partial" | "unknown";
|
||||
}
|
||||
|
||||
/** Artifact identity and provider-owned mapping jointly establish coordinate applicability. */
|
||||
@@ -110,5 +112,7 @@ export const parseFunctionEvidence = (
|
||||
callCoverageBlockers: callCoverage.blockers,
|
||||
unresolvedCallCaveats: callCoverage.unresolvedCallCaveats,
|
||||
callCoverageDiagnostics: callCoverage.diagnostics,
|
||||
callDiscoveryLimitations: callCoverage.discoveryLimitations,
|
||||
callDiscoveryStatus: callCoverage.discoveryStatus,
|
||||
});
|
||||
};
|
||||
@@ -21,11 +21,20 @@ const PATHS = [
|
||||
];
|
||||
|
||||
it("matches the host path module for absolute local paths", () => {
|
||||
const checked: string[] = [];
|
||||
for (const path of PATHS) {
|
||||
checked.push(path);
|
||||
expect(absoluteLocalPathStringSchema.safeParse(path).success).toBe(
|
||||
isAbsolute(path),
|
||||
);
|
||||
}
|
||||
expect(checked).toEqual(
|
||||
expect.arrayContaining([
|
||||
"/absolute/path",
|
||||
"relative/path",
|
||||
"file:///absolute/path",
|
||||
]),
|
||||
);
|
||||
expect(absoluteLocalPathStringSchema.safeParse("").success).toBe(false);
|
||||
expect(
|
||||
absoluteLocalPathStringSchema.safeParse("/absolute/with\0nul").success,
|
||||
@@ -40,8 +49,18 @@ it("keeps the Windows absolute-path pattern valid in every client mode", () => {
|
||||
() => new RegExp(windowsAbsolutePathPattern.source, flags),
|
||||
flags,
|
||||
).not.toThrow();
|
||||
for (const path of PATHS)
|
||||
const checked: string[] = [];
|
||||
for (const path of PATHS) {
|
||||
checked.push(path);
|
||||
expect(windowsAbsolutePathPattern.test(path), path).toBe(
|
||||
win32.isAbsolute(path),
|
||||
);
|
||||
}
|
||||
expect(checked).toEqual(
|
||||
expect.arrayContaining([
|
||||
"C:\\absolute\\path",
|
||||
"C:relative/path",
|
||||
"/absolute/path",
|
||||
]),
|
||||
);
|
||||
});
|
||||
@@ -91,7 +91,15 @@ const source = (
|
||||
confidence: "derived",
|
||||
authority: "shipped-artifact",
|
||||
...(hasCoverage
|
||||
? { relationshipCoverage: { status: "complete" as const } }
|
||||
? {
|
||||
relationshipCoverage: {
|
||||
status: "complete" as const,
|
||||
discovery: {
|
||||
callers: { status: "complete" as const },
|
||||
callees: { status: "complete" as const },
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...observation,
|
||||
},
|
||||
@@ -354,10 +362,24 @@ describe("function reconstruction call coverage", () => {
|
||||
it("preserves complete call diagnostics while passing equal call claims", () => {
|
||||
const diagnostic = "Indirect calls remain unresolved.";
|
||||
const leftWithDiagnostic = source("a", {
|
||||
relationshipCoverage: { status: "complete", diagnostics: [diagnostic] },
|
||||
relationshipCoverage: {
|
||||
status: "complete",
|
||||
diagnostics: [diagnostic],
|
||||
discovery: {
|
||||
callers: { status: "complete" },
|
||||
callees: { status: "complete" },
|
||||
},
|
||||
},
|
||||
});
|
||||
const rightWithDiagnostic = source("b", {
|
||||
relationshipCoverage: { status: "complete", diagnostics: [diagnostic] },
|
||||
relationshipCoverage: {
|
||||
status: "complete",
|
||||
diagnostics: [diagnostic],
|
||||
discovery: {
|
||||
callers: { status: "complete" },
|
||||
callees: { status: "complete" },
|
||||
},
|
||||
},
|
||||
});
|
||||
const observed = compareFunctions(
|
||||
functionSnapshotFixture(leftWithDiagnostic),
|
||||
@@ -454,7 +476,9 @@ describe("function reconstruction call coverage", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("function reconstruction with independent changes", () => {
|
||||
it("keeps a confirmed historical overall change as a failure despite unknown calls", () => {
|
||||
const changedDossier = jsonValueSchema.parse({
|
||||
...functionDossierSchema.parse(dossier),
|
||||
|
||||
@@ -236,7 +236,10 @@ const readValidatedSourceFacts = (
|
||||
);
|
||||
functionSources.push({
|
||||
evidence_id: source.evidence_id,
|
||||
callCoverageBlockers: callCoverage.blockers,
|
||||
callCoverageBlockers: [
|
||||
...callCoverage.blockers,
|
||||
...callCoverage.discoveryLimitations,
|
||||
],
|
||||
callCoverageDiagnostics: callCoverage.diagnostics,
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -3,9 +3,20 @@ import { z } from "zod";
|
||||
import { fixedTupleSchema } from "./fixedTupleSchema.js";
|
||||
|
||||
const diagnostics = z.array(z.string().min(1)).min(1).exactOptional();
|
||||
/** Discovery describes possible static calls beyond the returned provider graph. */
|
||||
export const relationshipDiscoveryDirectionSchema = z.strictObject({
|
||||
status: z.enum(["complete", "partial", "unknown"]),
|
||||
reason: z.string().min(1).exactOptional(),
|
||||
});
|
||||
export const relationshipDiscoverySchema = z.strictObject({
|
||||
callers: relationshipDiscoveryDirectionSchema.exactOptional(),
|
||||
callees: relationshipDiscoveryDirectionSchema.exactOptional(),
|
||||
});
|
||||
const discovery = relationshipDiscoverySchema.exactOptional();
|
||||
export const completeRelationshipCoverageSchema = z.strictObject({
|
||||
status: z.literal("complete"),
|
||||
diagnostics,
|
||||
discovery,
|
||||
});
|
||||
const limitedDirections = z.union([
|
||||
fixedTupleSchema([z.literal("callers")]),
|
||||
@@ -17,12 +28,14 @@ export const partialRelationshipCoverageSchema = z.strictObject({
|
||||
limitations: z.array(z.string().min(1)).min(1),
|
||||
limited_directions: limitedDirections.exactOptional(),
|
||||
diagnostics,
|
||||
discovery,
|
||||
});
|
||||
export const unknownRelationshipCoverageSchema = z.strictObject({
|
||||
status: z.literal("unknown"),
|
||||
reason: z.string().min(1),
|
||||
limited_directions: limitedDirections.exactOptional(),
|
||||
diagnostics,
|
||||
discovery,
|
||||
});
|
||||
|
||||
/** Explicit coverage state for a provider's observed caller/callee set. */
|
||||
@@ -34,6 +47,22 @@ export const relationshipCoverageSchema = z.discriminatedUnion("status", [
|
||||
|
||||
export type RelationshipCoverage = z.infer<typeof relationshipCoverageSchema>;
|
||||
|
||||
export type RelationshipDiscoveryStatus = z.infer<
|
||||
typeof relationshipDiscoveryDirectionSchema
|
||||
>["status"];
|
||||
|
||||
/** Combine discovery facts without treating missing historical metadata as proof. */
|
||||
export const combineRelationshipDiscovery = (
|
||||
current: RelationshipDiscoveryStatus,
|
||||
coverage: RelationshipCoverage | null | undefined,
|
||||
direction: "callers" | "callees",
|
||||
): RelationshipDiscoveryStatus => {
|
||||
const next = coverage?.discovery?.[direction]?.status ?? "unknown";
|
||||
if (current === "partial" || next === "partial") return "partial";
|
||||
if (current === "unknown" || next === "unknown") return "unknown";
|
||||
return "complete";
|
||||
};
|
||||
|
||||
/** Reasons the observed relationship inventory cannot establish completeness. */
|
||||
export const relationshipCoverageLimitations = (
|
||||
coverage: RelationshipCoverage | null | undefined,
|
||||
|
||||
@@ -24,9 +24,7 @@ export const inspectSignalDecodersInputSchema = z.strictObject({
|
||||
.nonnegative()
|
||||
.max(Number.MAX_SAFE_INTEGER)
|
||||
.default(0)
|
||||
.describe(
|
||||
"Number of matching decoders or help lines to skip; defaults to 0",
|
||||
),
|
||||
.describe("Number of matching decoders or help lines to skip"),
|
||||
limit: z
|
||||
.number()
|
||||
.int()
|
||||
|
||||
@@ -31,7 +31,7 @@ const signalInputShape = z.strictObject({
|
||||
.int()
|
||||
.nonnegative()
|
||||
.default(0)
|
||||
.describe("Skip this many literal LF-separated output lines; default 0"),
|
||||
.describe("Skip this many literal LF-separated output lines"),
|
||||
line_limit: z
|
||||
.number()
|
||||
.int()
|
||||
|
||||
@@ -272,6 +272,7 @@ export class KunaProvider implements AnalysisProviderCandidate {
|
||||
schema_version: 4,
|
||||
bodies: false,
|
||||
edge_kind: "call",
|
||||
discovery: "body-classification-v1",
|
||||
},
|
||||
xrefs: { command: "xrefs", direction: "to", exact_address: true },
|
||||
strings: {
|
||||
|
||||
@@ -172,6 +172,18 @@ export class KunaReferenceQueries {
|
||||
},
|
||||
};
|
||||
});
|
||||
const calleeDiscovery =
|
||||
node.kind === "data"
|
||||
? {
|
||||
discovery: {
|
||||
callees: {
|
||||
status: "partial" as const,
|
||||
reason:
|
||||
"Kuna classifies this graph node as data and does not analyze it as a function body.",
|
||||
},
|
||||
},
|
||||
}
|
||||
: {};
|
||||
return ok({
|
||||
callers: identities(graph.callers.get(address) ?? []),
|
||||
callees: identities(graph.callees.get(address) ?? []),
|
||||
@@ -191,8 +203,9 @@ export class KunaReferenceQueries {
|
||||
limitations: [
|
||||
"Kuna reports indirect calls in this function without every target; additional callees remain unknown.",
|
||||
],
|
||||
...calleeDiscovery,
|
||||
}
|
||||
: { status: "complete" },
|
||||
: { status: "complete", ...calleeDiscovery },
|
||||
source: this.#source(graph, node),
|
||||
});
|
||||
}
|
||||
@@ -208,6 +221,7 @@ export class KunaReferenceQueries {
|
||||
has_indirect_calls: graph.hasIndirectCalls,
|
||||
function: {
|
||||
address: node.address,
|
||||
kind: node.kind,
|
||||
has_indirect_calls: node.hasIndirectCalls,
|
||||
forwards_to: node.forwardsTo,
|
||||
forwarding_target_in_graph:
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import type { Result } from "../domain/result.js";
|
||||
import { toolContract } from "../contracts/toolContracts.js";
|
||||
import { createEvidence } from "../domain/evidence.js";
|
||||
import { createAnalysisProfile } from "../domain/analysisProfile.js";
|
||||
import { recordWorkflowEvidence } from "./registerInvestigationTools/helpers.js";
|
||||
import { recordDerivedEvidence } from "./recordDerivedEvidence.js";
|
||||
import { runDerivedOperation } from "./runDerivedOperation.js";
|
||||
@@ -159,6 +160,9 @@ const registerReconstruction = (
|
||||
const evidence = createEvidence(undefined, RECONSTRUCTION_PROVIDER, {
|
||||
predicateType: "rea.reconstruction-verification",
|
||||
operation: contract.name,
|
||||
analysisProfile: createAnalysisProfile(RECONSTRUCTION_PROVIDER, {
|
||||
workflow_revision: 2,
|
||||
}),
|
||||
parameters: {
|
||||
specification_sha256: result.specification_sha256,
|
||||
claim_ids: input.specification.claims.map(
|
||||
|
||||
@@ -255,11 +255,11 @@ cliTest(
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
issues: [
|
||||
{
|
||||
expect.objectContaining({
|
||||
path: ["executable_path"],
|
||||
reason: "invalid_format",
|
||||
message: expect.stringContaining("absolute local filesystem path"),
|
||||
},
|
||||
expected: "regex",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
||||
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
|
||||
@@ -329,21 +330,84 @@ const pathRequests: readonly [string, Record<string, unknown>][] = [
|
||||
["start_gdb_session", { inferior_tty: nul }],
|
||||
];
|
||||
|
||||
const nulIssuePaths = (
|
||||
value: unknown,
|
||||
path: readonly (string | number)[] = [],
|
||||
): (string | number)[][] => {
|
||||
if (typeof value === "string") return value.includes("\0") ? [[...path]] : [];
|
||||
if (Array.isArray(value))
|
||||
return value.flatMap((item, index) =>
|
||||
nulIssuePaths(item, [...path, index]),
|
||||
);
|
||||
if (typeof value === "object" && value !== null)
|
||||
return Object.entries(value).flatMap(([key, item]) =>
|
||||
nulIssuePaths(item, [...path, key]),
|
||||
);
|
||||
return [];
|
||||
};
|
||||
|
||||
it("rejects OS-invalid local inputs through the real advertised MCP contracts", async () => {
|
||||
const { call } = await connectLocalToolsMcp();
|
||||
const checked: string[] = [];
|
||||
for (const [name, arguments_] of pathRequests) {
|
||||
const response = await call(name, arguments_);
|
||||
expect(response.isError, name).toBe(true);
|
||||
expect(JSON.stringify(response.content), name).toContain("NUL");
|
||||
const paths = nulIssuePaths(arguments_);
|
||||
expect(paths.length, name).toBeGreaterThan(0);
|
||||
const error = parseMcpToolError(await call(name, arguments_)).error;
|
||||
expect(error, name).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: { operation: name },
|
||||
});
|
||||
const issues = z
|
||||
.object({
|
||||
issues: z.array(
|
||||
z.object({
|
||||
path: z.array(z.union([z.string(), z.number()])),
|
||||
reason: z.string(),
|
||||
message: z.string(),
|
||||
expected: z.unknown().optional(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
.parse(error.details).issues;
|
||||
expect(issues, name).toEqual(
|
||||
expect.arrayContaining(
|
||||
paths.map((path) =>
|
||||
expect.objectContaining({
|
||||
path,
|
||||
reason: "invalid_format",
|
||||
expected: "regex",
|
||||
message: expect.stringMatching(/\bNUL\b|null byte|zero byte/i),
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
checked.push(name);
|
||||
}
|
||||
expect(checked).toEqual(
|
||||
expect.arrayContaining([
|
||||
"extract_firmware",
|
||||
"inspect_wasm_artifact",
|
||||
"demangle_swift",
|
||||
"capture_electron_scenario",
|
||||
]),
|
||||
);
|
||||
const relative = await call("extract_firmware", {
|
||||
path: "/tmp/firmware.bin",
|
||||
output_directory: "relative-output",
|
||||
});
|
||||
expect(relative.isError).toBe(true);
|
||||
expect(JSON.stringify(relative.content)).toContain(
|
||||
"absolute local filesystem path",
|
||||
);
|
||||
expect(parseMcpToolError(relative).error).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
operation: "extract_firmware",
|
||||
issues: [
|
||||
expect.objectContaining({
|
||||
path: ["output_directory"],
|
||||
reason: "invalid_format",
|
||||
expected: "regex",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
}, 60_000);
|
||||
|
||||
const ignoredOptions = [
|
||||
|
||||
@@ -7,8 +7,35 @@ import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { reconstructionVerificationResultSchema } from "../../../src/domain/reconstructionVerificationSchemas.js";
|
||||
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
||||
|
||||
const unchangedRight = () => {
|
||||
const left = FUNCTION_COMPARISON_EXAMPLE.left;
|
||||
const completeDiscovery = {
|
||||
callers: { status: "complete" },
|
||||
callees: { status: "complete" },
|
||||
} as const;
|
||||
|
||||
const unchangedLeft = () => {
|
||||
const fixture = FUNCTION_COMPARISON_EXAMPLE.left;
|
||||
return createEvidence(
|
||||
{
|
||||
path: fixture.subject!.local_path,
|
||||
sha256: fixture.subject!.digest.sha256,
|
||||
format: fixture.subject!.format,
|
||||
},
|
||||
fixture.provider,
|
||||
{
|
||||
operation: fixture.operation,
|
||||
parameters: fixture.parameters,
|
||||
result: fixture.normalized_result,
|
||||
confidence: fixture.confidence,
|
||||
authority: fixture.authority,
|
||||
relationshipCoverage: {
|
||||
status: "complete",
|
||||
discovery: completeDiscovery,
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const unchangedRight = (left: ReturnType<typeof unchangedLeft>) => {
|
||||
return createEvidence(
|
||||
{ path: "/fixture/rebuilt", sha256: "2".repeat(64), format: "mach-o" },
|
||||
left.provider,
|
||||
@@ -18,7 +45,10 @@ const unchangedRight = () => {
|
||||
result: left.normalized_result,
|
||||
confidence: left.confidence,
|
||||
authority: left.authority,
|
||||
relationshipCoverage: { status: "complete" },
|
||||
relationshipCoverage: {
|
||||
status: "complete",
|
||||
discovery: completeDiscovery,
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -50,8 +80,8 @@ it.each(["pseudocode", "overall"] as const)(
|
||||
"keeps an overall claim unknown for an active %s question through stdio MCP",
|
||||
async (dimension) => {
|
||||
const { call } = await connectLocalToolsMcp();
|
||||
const left = FUNCTION_COMPARISON_EXAMPLE.left;
|
||||
const right = unchangedRight();
|
||||
const left = unchangedLeft();
|
||||
const right = unchangedRight(left);
|
||||
const compared = await call("compare_functions", { left, right });
|
||||
expect(compared.isError, JSON.stringify(compared)).not.toBe(true);
|
||||
const comparison = parseEvidence(compared.structuredContent);
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createCli } from "../../../src/cli.js";
|
||||
|
||||
type CliInstance = ReturnType<typeof createCli>;
|
||||
|
||||
const serve = async (
|
||||
cli: CliInstance,
|
||||
argv: readonly string[],
|
||||
): Promise<{ readonly stdout: string; readonly exitCode: number }> => {
|
||||
let stdout = "";
|
||||
let exitCode = 0;
|
||||
await cli.serve([...argv], {
|
||||
env: {},
|
||||
exit: (code) => {
|
||||
exitCode = code;
|
||||
},
|
||||
stdout: (text) => {
|
||||
stdout += text;
|
||||
},
|
||||
});
|
||||
return { stdout, exitCode };
|
||||
};
|
||||
|
||||
/**
|
||||
* Options whose owning field already declares a schema default. The help
|
||||
* renderer appends that default, so the description must not restate it.
|
||||
*/
|
||||
const singleDefaultOptions: readonly {
|
||||
readonly command: string;
|
||||
readonly options: readonly string[];
|
||||
}[] = [
|
||||
{ command: "inspect-byte-entropy", options: ["--block-size", "--offset"] },
|
||||
{
|
||||
command: "inspect-devicetree",
|
||||
options: [
|
||||
"--max-file-bytes",
|
||||
"--max-nodes",
|
||||
"--max-properties",
|
||||
"--max-depth",
|
||||
],
|
||||
},
|
||||
{
|
||||
command: "decode-signal-recording",
|
||||
options: ["--line_offset", "--line_limit"],
|
||||
},
|
||||
{
|
||||
command: "inspect-packet-capture",
|
||||
options: ["--match_offset", "--match_limit"],
|
||||
},
|
||||
{
|
||||
command: "inspect-packet-fields",
|
||||
options: ["--kind", "--offset"],
|
||||
},
|
||||
{ command: "inspect-signal-decoders", options: ["--offset"] },
|
||||
];
|
||||
|
||||
const optionLine = (help: string, option: string): string | undefined => {
|
||||
const line = help
|
||||
.split(/\r?\n/u)
|
||||
.find((candidate) => candidate.trimStart().startsWith(`${option} `));
|
||||
return line?.trim();
|
||||
};
|
||||
|
||||
describe("CLI help default annotations", () => {
|
||||
it("states each schema-owned option default exactly once", async () => {
|
||||
const cli = createCli({});
|
||||
const repeated: string[] = [];
|
||||
for (const { command, options } of singleDefaultOptions) {
|
||||
const { stdout, exitCode } = await serve(cli, [command, "--help"]);
|
||||
expect(exitCode, command).toBe(0);
|
||||
for (const option of options) {
|
||||
const line = optionLine(stdout, option);
|
||||
if (line === undefined) {
|
||||
repeated.push(`${command} ${option}: option line missing`);
|
||||
continue;
|
||||
}
|
||||
const statements = line.match(/default/giu)?.length ?? 0;
|
||||
if (statements !== 1)
|
||||
repeated.push(
|
||||
`${command} ${option}: ${statements} default statements`,
|
||||
);
|
||||
}
|
||||
}
|
||||
expect(repeated).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,7 @@ interface PathInputCase {
|
||||
readonly field: string;
|
||||
readonly valid: Readonly<Record<string, unknown>>;
|
||||
readonly invalid: Readonly<Record<string, unknown>>;
|
||||
readonly issuePath?: readonly string[];
|
||||
readonly issuePath: readonly string[];
|
||||
}
|
||||
|
||||
const createMcp = async (
|
||||
@@ -104,7 +104,13 @@ const inputCases = (
|
||||
field: string,
|
||||
valid: Readonly<Record<string, unknown>>,
|
||||
invalid: Readonly<Record<string, unknown>>,
|
||||
): PathInputCase => ({ name, field, valid, invalid });
|
||||
): PathInputCase => ({
|
||||
name,
|
||||
field,
|
||||
valid,
|
||||
invalid,
|
||||
issuePath: field.split("."),
|
||||
});
|
||||
|
||||
return [
|
||||
pathCase("open_binary", "path", { path: absolute }, { path: relative }),
|
||||
@@ -246,8 +252,7 @@ it("advertises full host-absolute path constraints after SDK schema conversion",
|
||||
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||
const absolute = resolve("rea-absolute-input");
|
||||
const relative = "relative-input";
|
||||
|
||||
for (const { name, field, valid, invalid } of [
|
||||
const schemaCases = [
|
||||
...inputCases(absolute, relative),
|
||||
{
|
||||
name: "inspect_go_binary",
|
||||
@@ -276,7 +281,9 @@ it("advertises full host-absolute path constraints after SDK schema conversion",
|
||||
right_bundle_path: absolute,
|
||||
},
|
||||
},
|
||||
]) {
|
||||
];
|
||||
const checked: string[] = [];
|
||||
for (const { name, field, valid, invalid } of schemaCases) {
|
||||
const schema = listed.tools.find(
|
||||
(tool) => tool.name === name,
|
||||
)?.inputSchema;
|
||||
@@ -288,7 +295,16 @@ it("advertises full host-absolute path constraints after SDK schema conversion",
|
||||
validate(valid),
|
||||
`${name}.${field}: ${JSON.stringify(validate.errors)}`,
|
||||
).toBe(true);
|
||||
checked.push(`${name}.${field}`);
|
||||
}
|
||||
expect(checked).toEqual(
|
||||
expect.arrayContaining([
|
||||
"open_binary.path",
|
||||
"extract_firmware.output_directory",
|
||||
"inspect_go_binary.path NUL",
|
||||
"compare_bundles.left_bundle_path empty",
|
||||
]),
|
||||
);
|
||||
expect(firmwareCalls).toEqual([]);
|
||||
} finally {
|
||||
await close();
|
||||
@@ -299,19 +315,39 @@ it("rejects relative paths through in-memory MCP handlers", async () => {
|
||||
const firmwareCalls: string[] = [];
|
||||
const { client, close } = await createMcp(firmwareCalls);
|
||||
try {
|
||||
for (const { name, field, invalid, issuePath } of inputCases(
|
||||
resolve("rea-absolute-input"),
|
||||
"relative-input",
|
||||
)) {
|
||||
const cases = inputCases(resolve("rea-absolute-input"), "relative-input");
|
||||
const checked: string[] = [];
|
||||
for (const { name, field, invalid, issuePath } of cases) {
|
||||
const result = await client.callTool({ name, arguments: invalid });
|
||||
expect(result.isError, `${name}.${field}`).toBe(true);
|
||||
expect(JSON.stringify(result), `${name}.${field}`).toContain("absolute");
|
||||
if (issuePath !== undefined)
|
||||
expect(parseMcpToolError(result).error.details).toMatchObject({
|
||||
operation: name,
|
||||
issues: [{ path: issuePath, reason: "invalid_format" }],
|
||||
});
|
||||
expect(parseMcpToolError(result).error, `${name}.${field}`).toMatchObject(
|
||||
{
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
operation: name,
|
||||
issues: [
|
||||
expect.objectContaining({
|
||||
path: issuePath,
|
||||
reason: "invalid_format",
|
||||
expected: "regex",
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
checked.push(`${name}.${field}`);
|
||||
}
|
||||
expect(checked).toEqual(
|
||||
expect.arrayContaining([
|
||||
"open_binary.path",
|
||||
"open_binary.snapshot_path",
|
||||
"extract_firmware.path",
|
||||
"extract_firmware.output_directory",
|
||||
"inspect_analysis_view.source.path",
|
||||
"capture_browser_scenario.browser.executable_path",
|
||||
"capture_electron_scenario.application_root",
|
||||
]),
|
||||
);
|
||||
expect(firmwareCalls).toEqual([]);
|
||||
} finally {
|
||||
await close();
|
||||
|
||||
@@ -64,6 +64,7 @@ const childFailure = (operation: string, cleanup: boolean) =>
|
||||
const relationshipPort = (
|
||||
relationships: Relationships,
|
||||
reads: string[],
|
||||
completeDiscovery = false,
|
||||
): AnalysisOperationPort => ({
|
||||
execute: async (operation, parameters) => {
|
||||
const procedure = String(parameters.procedure);
|
||||
@@ -77,7 +78,17 @@ const relationshipPort = (
|
||||
? err(answer)
|
||||
: ok(
|
||||
createAnalysisExecution(answer, provider, {
|
||||
relationshipCoverage: { status: "complete" },
|
||||
relationshipCoverage: {
|
||||
status: "complete",
|
||||
...(completeDiscovery
|
||||
? {
|
||||
discovery: {
|
||||
callers: { status: "complete" },
|
||||
callees: { status: "complete" },
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
@@ -96,6 +107,27 @@ const call = (
|
||||
});
|
||||
|
||||
describe.each(directions)("%s relationship reads", (direction, operation) => {
|
||||
it("keeps discovery unknown when cleanup stops an otherwise complete trace", async () => {
|
||||
const reads: string[] = [];
|
||||
const client = await connect(
|
||||
relationshipPort(
|
||||
{ "0x1": ["0x2"], "0x2": childFailure(operation, true) },
|
||||
reads,
|
||||
true,
|
||||
),
|
||||
);
|
||||
const response = await client.callTool(call("trace_call_path", direction));
|
||||
const { error } = parseMcpToolError(response);
|
||||
expect(error.code).toBe("cleanup_incomplete");
|
||||
expect(reads).toEqual([`${operation}:0x1`, `${operation}:0x2`]);
|
||||
expect(
|
||||
parseEvidence(error.details?.partial_observation).normalized_result,
|
||||
).toMatchObject({
|
||||
goal_status: "unknown",
|
||||
search_scope: { discovery: "unknown", observed_goal_status: "unknown" },
|
||||
});
|
||||
});
|
||||
|
||||
it.each(tools)(
|
||||
"%s stops at unconfirmed cleanup and reports it with the admitted result",
|
||||
async (tool) => {
|
||||
|
||||
@@ -329,7 +329,7 @@ describe("trace_call_path MCP outcome semantics", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("reports an unreachable goal as absent after complete traversal", async () => {
|
||||
it("keeps a complete observed traversal distinct from unknown discovery", async () => {
|
||||
const client = await connect({
|
||||
execute: async (name, arguments_) => {
|
||||
if (name === "procedure_address")
|
||||
@@ -347,7 +347,11 @@ describe("trace_call_path MCP outcome semantics", () => {
|
||||
expect(trace.isError).not.toBe(true);
|
||||
expect(trace.structuredContent).toMatchObject({
|
||||
normalized_result: {
|
||||
goal_status: "not_reached",
|
||||
goal_status: "unknown",
|
||||
search_scope: {
|
||||
discovery: "unknown",
|
||||
observed_goal_status: "not_reached",
|
||||
},
|
||||
truncated: false,
|
||||
residual_unknowns: [],
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@ it.each(profiles)("preserves directional gaps (%s)", async (profile) => {
|
||||
status: "partial",
|
||||
limitations: [limitation],
|
||||
limited_directions: directions,
|
||||
discovery: { callees: { status: "complete" } },
|
||||
};
|
||||
const observation = observe(coverage);
|
||||
expect(session.recordEvidence(observation).ok).toBe(true);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js";
|
||||
@@ -6,14 +7,14 @@ import { functionDossierSchema } from "../../../src/domain/native/functionDossie
|
||||
import { jsonValueSchema } from "../../../src/domain/jsonValue.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
|
||||
import type { RelationshipCoverage } from "../../../src/domain/relationshipCoverage.js";
|
||||
import { functionComparisonResultSchema } from "../../../src/domain/functionComparisonSchemas.js";
|
||||
|
||||
const sourceWithCoverage = (
|
||||
template: typeof FUNCTION_COMPARISON_EXAMPLE.left,
|
||||
observation: string,
|
||||
result: typeof template.normalized_result,
|
||||
relationshipCoverage:
|
||||
| { readonly status: "complete" }
|
||||
| { readonly status: "unknown"; readonly reason: string },
|
||||
relationshipCoverage: RelationshipCoverage,
|
||||
) => {
|
||||
if (template.subject === null)
|
||||
throw new Error("Function comparison fixtures must be artifact-bound");
|
||||
@@ -35,6 +36,316 @@ const sourceWithCoverage = (
|
||||
);
|
||||
};
|
||||
|
||||
const expectAdvertisedComparisonSchema = async (
|
||||
client: Awaited<ReturnType<typeof createApplicationMcpHarness>>["client"],
|
||||
payload: unknown,
|
||||
) => {
|
||||
const advertised = (await client.listTools()).tools.find(
|
||||
({ name }) => name === "compare_functions",
|
||||
);
|
||||
if (advertised?.outputSchema === undefined)
|
||||
throw new Error("compare_functions did not advertise its output schema");
|
||||
const outputSchema: Record<string, unknown> = advertised.outputSchema;
|
||||
const validator = new Ajv2020({ strict: false, validateFormats: false });
|
||||
expect(validator.validateSchema(outputSchema)).toBe(true);
|
||||
const validate = validator.compile(outputSchema);
|
||||
expect(validate(payload), JSON.stringify(validate.errors)).toBe(true);
|
||||
};
|
||||
|
||||
const expectDiscoveryConsumers = async (
|
||||
client: Awaited<ReturnType<typeof createApplicationMcpHarness>>["client"],
|
||||
comparison: ReturnType<typeof parseEvidence>,
|
||||
label: "partial" | "unreported" | "complete",
|
||||
) => {
|
||||
const unknowns = await client.callTool({
|
||||
name: "list_unknowns",
|
||||
arguments: { domain: "function-comparison" },
|
||||
});
|
||||
expect(unknowns.isError).not.toBe(true);
|
||||
const unknownItems = (
|
||||
unknowns.structuredContent as { result: { items: unknown[] } }
|
||||
).result.items;
|
||||
if (label === "complete") expect(unknownItems).toHaveLength(0);
|
||||
else
|
||||
expect(unknownItems).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
comparison_conclusion_scope: expect.objectContaining({
|
||||
comparison_evidence_id: comparison.evidence_id,
|
||||
dimensions: expect.arrayContaining(["calls"]),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
const changed = await client.callTool({
|
||||
name: "find_changed_behavior",
|
||||
arguments: { comparisons: [comparison] },
|
||||
});
|
||||
expect(changed.isError, JSON.stringify(changed.structuredContent)).not.toBe(
|
||||
true,
|
||||
);
|
||||
const changedEvidence = parseEvidence(changed.structuredContent);
|
||||
const findings = (
|
||||
changedEvidence.normalized_result as { findings: { items: unknown[] } }
|
||||
).findings.items;
|
||||
if (label === "complete") expect(findings).toHaveLength(0);
|
||||
else
|
||||
expect(findings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
dimension: "calls",
|
||||
classification: "unresolved_branch",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
};
|
||||
|
||||
const expectHistoricalDiscovery = async (
|
||||
session: ReturnType<typeof createTestBinarySession>,
|
||||
client: Awaited<ReturnType<typeof createApplicationMcpHarness>>["client"],
|
||||
comparison: ReturnType<typeof parseEvidence>,
|
||||
) => {
|
||||
const current = functionComparisonResultSchema.parse(
|
||||
comparison.normalized_result,
|
||||
);
|
||||
const historical = createEvidence(undefined, comparison.provider, {
|
||||
predicateType: comparison.predicate_type,
|
||||
operation: comparison.operation,
|
||||
parameters: comparison.parameters,
|
||||
result: {
|
||||
...current,
|
||||
dimensions: current.dimensions.map((dimension) => {
|
||||
if (dimension.dimension !== "calls") return dimension;
|
||||
const { discovery_status: _discovery, ...historicalDimension } =
|
||||
dimension;
|
||||
return { ...historicalDimension, limitations: [] };
|
||||
}),
|
||||
},
|
||||
confidence: comparison.confidence,
|
||||
authority: comparison.authority,
|
||||
limitations: comparison.limitations,
|
||||
evidenceLinks: comparison.evidence_links,
|
||||
});
|
||||
expect(session.recordEvidence(historical).ok).toBe(true);
|
||||
const prior = await verify(
|
||||
(name, arguments_) => client.callTool({ name, arguments: arguments_ }),
|
||||
historical,
|
||||
["calls"],
|
||||
);
|
||||
expect(prior.normalized_result).toMatchObject({
|
||||
claims: {
|
||||
items: [{ status: "unknown", observed_status: "unchanged" }],
|
||||
},
|
||||
});
|
||||
const historicalChanged = await client.callTool({
|
||||
name: "find_changed_behavior",
|
||||
arguments: { comparisons: [historical] },
|
||||
});
|
||||
expect(historicalChanged.isError).not.toBe(true);
|
||||
expect(
|
||||
parseEvidence(historicalChanged.structuredContent).normalized_result,
|
||||
).toMatchObject({
|
||||
findings: {
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
dimension: "calls",
|
||||
classification: "unresolved_branch",
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
expect(session.evidenceById(historical.evidence_id)).toEqual(historical);
|
||||
};
|
||||
|
||||
it.each(["complete", "partial"] as const)(
|
||||
"retains %s discovery when a call facet is unavailable",
|
||||
async (discoveryStatus) => {
|
||||
const session = createTestBinarySession(() => {
|
||||
throw new Error("offline comparison must not launch a provider");
|
||||
});
|
||||
const { client } = await createApplicationMcpHarness({ session });
|
||||
const original = functionDossierSchema.parse(
|
||||
FUNCTION_COMPARISON_EXAMPLE.left.normalized_result,
|
||||
);
|
||||
const unavailable = {
|
||||
...original,
|
||||
facet_availability: {
|
||||
...original.facet_availability,
|
||||
callers: { available: false as const, reason: "Read unavailable." },
|
||||
},
|
||||
};
|
||||
const coverage: RelationshipCoverage = {
|
||||
status: "complete",
|
||||
discovery: {
|
||||
callers: { status: "complete" },
|
||||
callees:
|
||||
discoveryStatus === "complete"
|
||||
? { status: "complete" }
|
||||
: { status: "partial", reason: "Some bodies unclassified." },
|
||||
},
|
||||
};
|
||||
const left = sourceWithCoverage(
|
||||
FUNCTION_COMPARISON_EXAMPLE.left,
|
||||
"facet-left",
|
||||
unavailable,
|
||||
coverage,
|
||||
);
|
||||
const right = sourceWithCoverage(
|
||||
FUNCTION_COMPARISON_EXAMPLE.right,
|
||||
"facet-right",
|
||||
original,
|
||||
coverage,
|
||||
);
|
||||
const compared = await client.callTool({
|
||||
name: "compare_functions",
|
||||
arguments: { left, right },
|
||||
});
|
||||
expect(compared.isError).not.toBe(true);
|
||||
const comparison = parseEvidence(compared.structuredContent);
|
||||
expect(comparison.normalized_result).toMatchObject({
|
||||
dimensions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
dimension: "calls",
|
||||
status: "unknown",
|
||||
discovery_status: discoveryStatus,
|
||||
limitations: expect.arrayContaining([
|
||||
"Left: callers: Read unavailable.",
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
const changed = await client.callTool({
|
||||
name: "find_changed_behavior",
|
||||
arguments: { comparisons: [comparison] },
|
||||
});
|
||||
expect(changed.isError).not.toBe(true);
|
||||
const findings = (
|
||||
parseEvidence(changed.structuredContent).normalized_result as {
|
||||
findings: {
|
||||
items: Array<{ dimension: string; limitations: string[] }>;
|
||||
};
|
||||
}
|
||||
).findings.items;
|
||||
const calls = findings.find((finding) => finding.dimension === "calls");
|
||||
expect(calls).toBeDefined();
|
||||
expect(calls?.limitations).not.toContain(
|
||||
"Static call discovery coverage was not recorded in the comparison.",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["full", "compact"] as const)(
|
||||
"qualifies observed calls and verifies discovery independently (%s)",
|
||||
async (profile) => {
|
||||
for (const [label, coverage, status] of [
|
||||
[
|
||||
"partial",
|
||||
{
|
||||
status: "complete",
|
||||
discovery: {
|
||||
callers: { status: "complete" },
|
||||
callees: {
|
||||
status: "partial",
|
||||
reason: "The fixture body is classified as data.",
|
||||
},
|
||||
},
|
||||
},
|
||||
"unknown",
|
||||
],
|
||||
["unreported", { status: "complete" }, "unknown"],
|
||||
[
|
||||
"complete",
|
||||
{
|
||||
status: "complete",
|
||||
discovery: {
|
||||
callers: { status: "complete" },
|
||||
callees: { status: "complete" },
|
||||
},
|
||||
},
|
||||
"pass",
|
||||
],
|
||||
] as const) {
|
||||
const session = createTestBinarySession(() => {
|
||||
throw new Error("offline comparison must not launch a provider");
|
||||
});
|
||||
const { client } = await createApplicationMcpHarness({
|
||||
session,
|
||||
serverOptions: {
|
||||
environment: { REA_MCP_OUTPUT_SCHEMA_PROFILE: profile },
|
||||
},
|
||||
});
|
||||
const left = sourceWithCoverage(
|
||||
FUNCTION_COMPARISON_EXAMPLE.left,
|
||||
`${label}-left`,
|
||||
FUNCTION_COMPARISON_EXAMPLE.left.normalized_result,
|
||||
coverage,
|
||||
);
|
||||
const right = sourceWithCoverage(
|
||||
FUNCTION_COMPARISON_EXAMPLE.right,
|
||||
`${label}-right`,
|
||||
FUNCTION_COMPARISON_EXAMPLE.left.normalized_result,
|
||||
coverage,
|
||||
);
|
||||
const compared = await client.callTool({
|
||||
name: "compare_functions",
|
||||
arguments: { left, right },
|
||||
});
|
||||
if (compared.isError) throw new Error(JSON.stringify(compared));
|
||||
const comparison = parseEvidence(compared.structuredContent);
|
||||
if (label === "partial")
|
||||
await expectAdvertisedComparisonSchema(
|
||||
client,
|
||||
compared.structuredContent,
|
||||
);
|
||||
expect(comparison.normalized_result).toMatchObject({
|
||||
dimensions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
dimension: "calls",
|
||||
status: "unchanged",
|
||||
...(label === "partial"
|
||||
? {
|
||||
limitations: expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
"The fixture body is classified as data.",
|
||||
),
|
||||
]),
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
await expectDiscoveryConsumers(client, comparison, label);
|
||||
const result = await verify(
|
||||
(name, arguments_) => client.callTool({ name, arguments: arguments_ }),
|
||||
comparison,
|
||||
["calls"],
|
||||
);
|
||||
expect(result.normalized_result).toMatchObject({
|
||||
claims: {
|
||||
items: [
|
||||
{
|
||||
claim_id: "calls",
|
||||
status,
|
||||
observed_status: "unchanged",
|
||||
...(label === "partial"
|
||||
? {
|
||||
limitations: expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
"The fixture body is classified as data.",
|
||||
),
|
||||
]),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
if (label === "unreported")
|
||||
await expectHistoricalDiscovery(session, client, comparison);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const verify = async (
|
||||
call: (
|
||||
name: string,
|
||||
@@ -64,6 +375,89 @@ const verify = async (
|
||||
return parseEvidence(response.structuredContent);
|
||||
};
|
||||
|
||||
it("keeps an observed call-set difference without claiming a verified difference", async () => {
|
||||
const session = createTestBinarySession(() => {
|
||||
throw new Error("offline comparison must not launch a provider");
|
||||
});
|
||||
const { client } = await createApplicationMcpHarness({ session });
|
||||
const original = functionDossierSchema.parse(
|
||||
FUNCTION_COMPARISON_EXAMPLE.left.normalized_result,
|
||||
);
|
||||
const coverage: RelationshipCoverage = {
|
||||
status: "complete",
|
||||
discovery: {
|
||||
callers: { status: "complete" },
|
||||
callees: { status: "partial", reason: "Not all bodies were classified." },
|
||||
},
|
||||
};
|
||||
const left = sourceWithCoverage(
|
||||
FUNCTION_COMPARISON_EXAMPLE.left,
|
||||
"difference-left",
|
||||
jsonValueSchema.parse({ ...original, callees: [] }),
|
||||
coverage,
|
||||
);
|
||||
const {
|
||||
signature: _signature,
|
||||
locals: _locals,
|
||||
...helper
|
||||
} = original.procedure;
|
||||
const right = sourceWithCoverage(
|
||||
FUNCTION_COMPARISON_EXAMPLE.right,
|
||||
"difference-right",
|
||||
jsonValueSchema.parse({
|
||||
...original,
|
||||
callees: [{ ...helper, address: "0x2000", name: "helper" }],
|
||||
}),
|
||||
coverage,
|
||||
);
|
||||
const response = await client.callTool({
|
||||
name: "compare_functions",
|
||||
arguments: { left, right },
|
||||
});
|
||||
if (response.isError) throw new Error(JSON.stringify(response));
|
||||
const comparison = parseEvidence(response.structuredContent);
|
||||
expect(comparison.normalized_result).toMatchObject({
|
||||
dimensions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
dimension: "calls",
|
||||
status: "changed",
|
||||
limitations: expect.arrayContaining([
|
||||
expect.stringContaining("Not all bodies were classified."),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
const changed = await client.callTool({
|
||||
name: "find_changed_behavior",
|
||||
arguments: { comparisons: [comparison] },
|
||||
});
|
||||
expect(changed.isError).not.toBe(true);
|
||||
expect(
|
||||
parseEvidence(changed.structuredContent).normalized_result,
|
||||
).toMatchObject({
|
||||
findings: {
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
dimension: "calls",
|
||||
classification: "derived_relationship",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
dimension: "calls",
|
||||
classification: "unresolved_branch",
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
const result = await verify(
|
||||
(name, arguments_) => client.callTool({ name, arguments: arguments_ }),
|
||||
comparison,
|
||||
["calls"],
|
||||
);
|
||||
expect(result.normalized_result).toMatchObject({
|
||||
claims: { items: [{ status: "unknown", observed_status: "changed" }] },
|
||||
});
|
||||
});
|
||||
|
||||
const assertChangedDespiteUnknownCalls = async (
|
||||
call: Parameters<typeof verify>[0],
|
||||
left: ReturnType<typeof parseEvidence>,
|
||||
@@ -168,7 +562,11 @@ it("keeps incomplete calls unknown through public comparison and verification wh
|
||||
conclusion_kind: "unresolved_branch",
|
||||
left_count: 0,
|
||||
right_count: 0,
|
||||
limitations: ["Right: Call graph unavailable."],
|
||||
limitations: expect.arrayContaining([
|
||||
"Right: Call graph unavailable.",
|
||||
"Left: Static direct-call discovery is not established for callers and callees.",
|
||||
"Right: Static direct-call discovery is not established for callers and callees.",
|
||||
]),
|
||||
evidence_links: [left.evidence_id, right.evidence_id],
|
||||
}),
|
||||
]),
|
||||
|
||||
@@ -106,7 +106,7 @@ it.each([
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ comparisonStatus: "unchanged", verificationStatus: "pass" },
|
||||
{ comparisonStatus: "unchanged", verificationStatus: "unknown" },
|
||||
{ comparisonStatus: "changed", verificationStatus: "fail" },
|
||||
] as const)(
|
||||
"verifies public $comparisonStatus function comparison as $verificationStatus",
|
||||
@@ -202,15 +202,15 @@ it.each([
|
||||
status: verificationStatus,
|
||||
summary: {
|
||||
total: 4,
|
||||
passed: verificationStatus === "pass" ? 4 : 2,
|
||||
passed: comparisonStatus === "unchanged" ? 3 : 2,
|
||||
failed: verificationStatus === "fail" ? 2 : 0,
|
||||
unknown: 0,
|
||||
unknown: comparisonStatus === "unchanged" ? 1 : 0,
|
||||
},
|
||||
claims: {
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
dimension: "pseudocode",
|
||||
status: verificationStatus,
|
||||
status: comparisonStatus === "unchanged" ? "pass" : "fail",
|
||||
observed_status: comparisonStatus,
|
||||
comparison_evidence_id: comparison.evidence_id,
|
||||
left_evidence_ids: [left.evidence_id],
|
||||
@@ -447,7 +447,7 @@ describe("function comparison input-kind diagnostics", () => {
|
||||
it.each([
|
||||
{ status: "changed", expected: "fail" },
|
||||
{ status: "unknown", expected: "unknown" },
|
||||
{ status: "unchanged", expected: "pass" },
|
||||
{ status: "unchanged", expected: "unknown" },
|
||||
] as const)(
|
||||
"checks the aggregate status of imported $status function comparisons",
|
||||
async ({ status, expected }) => {
|
||||
|
||||
@@ -170,7 +170,13 @@ const evidence = (
|
||||
},
|
||||
confidence: "derived",
|
||||
authority: "shipped-artifact",
|
||||
relationshipCoverage: { status: "complete" },
|
||||
relationshipCoverage: {
|
||||
status: "complete",
|
||||
discovery: {
|
||||
callers: { status: "complete" },
|
||||
callees: { status: "complete" },
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -273,9 +273,36 @@ it.each([
|
||||
coverage: { status: "complete" } as RelationshipCoverage,
|
||||
providerLimitations: ["The provider version identity is unavailable."],
|
||||
dossierLimitations: ["The dossier has no native value-flow observation."],
|
||||
status: "unknown",
|
||||
exhaustive: true,
|
||||
},
|
||||
{
|
||||
label: "complete discovery",
|
||||
coverage: {
|
||||
status: "complete",
|
||||
discovery: { callees: { status: "complete" } },
|
||||
} as RelationshipCoverage,
|
||||
providerLimitations: [],
|
||||
dossierLimitations: [],
|
||||
status: "not_found",
|
||||
exhaustive: true,
|
||||
},
|
||||
{
|
||||
label: "partially discovered code",
|
||||
coverage: {
|
||||
status: "complete",
|
||||
discovery: {
|
||||
callees: {
|
||||
status: "partial",
|
||||
reason: "This node was classified as data.",
|
||||
},
|
||||
},
|
||||
} as RelationshipCoverage,
|
||||
providerLimitations: [],
|
||||
dossierLimitations: [],
|
||||
status: "unknown",
|
||||
exhaustive: true,
|
||||
},
|
||||
{
|
||||
label: "partial",
|
||||
coverage: {
|
||||
@@ -313,7 +340,7 @@ it.each([
|
||||
coverage: undefined,
|
||||
providerLimitations: [],
|
||||
dossierLimitations: [],
|
||||
status: "not_found",
|
||||
status: "unknown",
|
||||
exhaustive: true,
|
||||
},
|
||||
])(
|
||||
@@ -408,6 +435,11 @@ it.each([
|
||||
...scenario.providerLimitations,
|
||||
...scenario.dossierLimitations,
|
||||
...reasons,
|
||||
...(coverage?.discovery?.callees?.reason === undefined
|
||||
? []
|
||||
: [
|
||||
`Call discovery for 0x1000 is ${coverage.discovery.callees.status}: ${coverage.discovery.callees.reason}`,
|
||||
]),
|
||||
]),
|
||||
});
|
||||
expect(evidence.evidence_links).toEqual([dossier.evidence_id]);
|
||||
@@ -428,7 +460,7 @@ it.each([
|
||||
});
|
||||
expect(rejected.isError, JSON.stringify(rejected)).toBe(true);
|
||||
}
|
||||
if (scenario.exhaustive)
|
||||
if (scenario.status !== "unknown")
|
||||
expect(session.listUnknowns({ domain: "call-path" })).toEqual([]);
|
||||
else
|
||||
expect(session.listUnknowns({ domain: "call-path" })).toMatchObject([
|
||||
@@ -624,7 +656,27 @@ const close = async (
|
||||
it("keeps an aggregate function reconstruction unknown while a constituent dimension is unresolved", async () => {
|
||||
const { session, server, client } = await connected();
|
||||
try {
|
||||
const left = FUNCTION_COMPARISON_EXAMPLE.left;
|
||||
const fixture = FUNCTION_COMPARISON_EXAMPLE.left;
|
||||
const discovery = {
|
||||
callers: { status: "complete" },
|
||||
callees: { status: "complete" },
|
||||
} as const;
|
||||
const left = createEvidence(
|
||||
{
|
||||
path: fixture.subject!.local_path,
|
||||
sha256: fixture.subject!.digest.sha256,
|
||||
format: fixture.subject!.format,
|
||||
},
|
||||
fixture.provider,
|
||||
{
|
||||
operation: fixture.operation,
|
||||
parameters: fixture.parameters,
|
||||
result: fixture.normalized_result,
|
||||
confidence: fixture.confidence,
|
||||
authority: fixture.authority,
|
||||
relationshipCoverage: { status: "complete", discovery },
|
||||
},
|
||||
);
|
||||
const right = createEvidence(
|
||||
{
|
||||
path: "/tmp/rebuilt-function",
|
||||
@@ -638,7 +690,7 @@ it("keeps an aggregate function reconstruction unknown while a constituent dimen
|
||||
result: left.normalized_result,
|
||||
confidence: left.confidence,
|
||||
authority: left.authority,
|
||||
relationshipCoverage: { status: "complete" },
|
||||
relationshipCoverage: { status: "complete", discovery },
|
||||
},
|
||||
);
|
||||
const compared = await client.callTool({
|
||||
|
||||
@@ -175,6 +175,12 @@ it("follows native overflow continuations through MCP without repeating provider
|
||||
relationshipCoverage: {
|
||||
status: "partial",
|
||||
limitations: ["Indirect edges unknown"],
|
||||
discovery: {
|
||||
callees: {
|
||||
status: "partial",
|
||||
reason: "Body was classified as data.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
@@ -234,6 +240,24 @@ it("follows native overflow continuations through MCP without repeating provider
|
||||
kind: "native-observation",
|
||||
parent_operation: name,
|
||||
});
|
||||
if (name === "get_call_graph")
|
||||
expect(evidence.normalized_result).toMatchObject({
|
||||
items: {
|
||||
type: "graph-nodes",
|
||||
rows: [
|
||||
{
|
||||
relationship_coverage: {
|
||||
discovery: {
|
||||
callees: {
|
||||
status: "partial",
|
||||
reason: "Body was classified as data.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(reads).toBe(before);
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -709,8 +709,19 @@ describe("session filesystem path boundaries over MCP", () => {
|
||||
name: "export_evidence_bundle",
|
||||
arguments: { path: "relative-bundle.json" },
|
||||
});
|
||||
expect(exported.isError, JSON.stringify(exported.content)).toBe(true);
|
||||
expect(JSON.stringify(exported.content)).toContain("absolute");
|
||||
expect(parseMcpToolError(exported).error).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
operation: "export_evidence_bundle",
|
||||
issues: [
|
||||
expect.objectContaining({
|
||||
path: ["path"],
|
||||
reason: "invalid_format",
|
||||
expected: "regex",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const opened = await mcp.callTool({
|
||||
name: "open_binary",
|
||||
@@ -721,8 +732,19 @@ describe("session filesystem path boundaries over MCP", () => {
|
||||
name: "close_binary",
|
||||
arguments: { snapshot_path: "relative-analysis.json" },
|
||||
});
|
||||
expect(closed.isError, JSON.stringify(closed.content)).toBe(true);
|
||||
expect(JSON.stringify(closed.content)).toContain("absolute");
|
||||
expect(parseMcpToolError(closed).error).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
operation: "close_binary",
|
||||
issues: [
|
||||
expect.objectContaining({
|
||||
path: ["snapshot_path"],
|
||||
reason: "invalid_format",
|
||||
expected: "regex",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
await mcp.callTool({ name: "close_binary", arguments: {} });
|
||||
}, 10_000);
|
||||
});
|
||||
|
||||
@@ -383,7 +383,7 @@ it("attributes rejected WASM paths to their selected fields before provider work
|
||||
},
|
||||
});
|
||||
onTestFinished(harness.close);
|
||||
for (const [input, paths] of [
|
||||
const relativeCases = [
|
||||
[{ path: "relative.wasm" }, [["path"]]],
|
||||
[
|
||||
{ path: value.artifact.path, glue_paths: ["relative.js"] },
|
||||
@@ -412,7 +412,9 @@ it("attributes rejected WASM paths to their selected fields before provider work
|
||||
},
|
||||
[["path"], ["glue_paths", 0], ["candidate_paths", 0]],
|
||||
],
|
||||
] as const) {
|
||||
] as const;
|
||||
const checked: string[] = [];
|
||||
for (const [input, paths] of relativeCases) {
|
||||
const response = await harness.client.callTool({
|
||||
name: "inspect_wasm_artifact",
|
||||
arguments: input,
|
||||
@@ -423,14 +425,19 @@ it("attributes rejected WASM paths to their selected fields before provider work
|
||||
.object({ issues: z.array(z.unknown()) })
|
||||
.parse(failure.details);
|
||||
expect(issues).toEqual(
|
||||
paths.map((path) => ({
|
||||
path,
|
||||
reason: "invalid_format",
|
||||
expected: "regex",
|
||||
message: "must be an absolute local filesystem path",
|
||||
})),
|
||||
paths.map((path) =>
|
||||
expect.objectContaining({
|
||||
path,
|
||||
reason: "invalid_format",
|
||||
expected: "regex",
|
||||
}),
|
||||
),
|
||||
);
|
||||
checked.push(paths.map((path) => path.join(".")).join("+"));
|
||||
}
|
||||
expect(checked).toEqual(
|
||||
expect.arrayContaining(["path", "glue_paths.0", "candidate_paths.0"]),
|
||||
);
|
||||
expect(inspect).not.toHaveBeenCalled();
|
||||
const valid = await harness.client.callTool({
|
||||
name: "inspect_wasm_artifact",
|
||||
@@ -760,12 +767,13 @@ it("rejects malformed paths, refuses WASM summary delivery before provider work
|
||||
expect(
|
||||
z.object({ issues: z.array(z.unknown()) }).parse(rejected.details).issues,
|
||||
).toEqual(
|
||||
[["path"], ["glue_paths", 0]].map((path) => ({
|
||||
path,
|
||||
reason: "invalid_format",
|
||||
expected: "regex",
|
||||
message: "must be an absolute local filesystem path",
|
||||
})),
|
||||
[["path"], ["glue_paths", 0]].map((path) =>
|
||||
expect.objectContaining({
|
||||
path,
|
||||
reason: "invalid_format",
|
||||
expected: "regex",
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(inspect).not.toHaveBeenCalled();
|
||||
for (const input of [
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
PYTHON_HELPERS,
|
||||
PYTHON_IMPORTS,
|
||||
} from "../../../src/domain/desktop/appExportPython.js";
|
||||
import {
|
||||
detectStateFormat,
|
||||
textualView,
|
||||
} from "../../../src/domain/desktop/stateFormats.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const arrays = "[".repeat(1500) + "1" + "]".repeat(1500);
|
||||
const objects = '{"a":'.repeat(1500) + "1" + "}".repeat(1500);
|
||||
const cases = [
|
||||
["discarded arrays", `{"value":${arrays},"value":"done"}`, true],
|
||||
["discarded objects", `{"value":${objects},"value":"done"}`, true],
|
||||
["escaped duplicate key", `{"val\\u0075e":${arrays},"value":"done"}`, true],
|
||||
["retained deep value", `{"value":${arrays}}`, false],
|
||||
[
|
||||
"retained deep inexact number",
|
||||
`{"value":${"[".repeat(600)}9007199254740993${"]".repeat(600)}}`,
|
||||
false,
|
||||
],
|
||||
[
|
||||
"deep overwritten inexact number",
|
||||
`{"value":${"[".repeat(600)}{"x":9007199254740993,"x":1}${"]".repeat(600)}}`,
|
||||
false,
|
||||
],
|
||||
[
|
||||
"deep empty-key overwrite",
|
||||
`{"value":${"[".repeat(600)}{"":9007199254740993,"":1}${"]".repeat(600)}}`,
|
||||
false,
|
||||
],
|
||||
[
|
||||
"deep multiple inexact keys with one overwritten",
|
||||
`{"value":${"[".repeat(600)}{"a":9007199254740993,"b":9007199254740993,"a":1}${"]".repeat(600)}}`,
|
||||
false,
|
||||
],
|
||||
[
|
||||
"deep multiple inexact keys all overwritten",
|
||||
`{"value":${"[".repeat(600)}{"a":9007199254740993,"b":9007199254740993,"a":1,"b":2}${"]".repeat(600)}}`,
|
||||
false,
|
||||
],
|
||||
[
|
||||
"deep inexact key replaced by inexact",
|
||||
`{"value":${"[".repeat(600)}{"a":9007199254740993,"a":9007199254740995}${"]".repeat(600)}}`,
|
||||
false,
|
||||
],
|
||||
[
|
||||
"deep escaped duplicate overwritten",
|
||||
`{"value":${"[".repeat(600)}{"\\u0061":9007199254740993,"a":1}${"]".repeat(600)}}`,
|
||||
false,
|
||||
],
|
||||
[
|
||||
"discarded deep inexact subtree",
|
||||
`{"value":${"[".repeat(600)}9007199254740993${"]".repeat(600)},"value":"done"}`,
|
||||
true,
|
||||
],
|
||||
["last admitted depth", "[".repeat(500) + "1" + "]".repeat(500), true],
|
||||
["empty container at limit", "[".repeat(501) + "]".repeat(501), true],
|
||||
["empty container over limit", "[".repeat(502) + "]".repeat(502), false],
|
||||
["trailing comma", `{"value":${arrays},"value":"done",}`, false],
|
||||
["missing value", `{"value":${arrays},"value":}`, false],
|
||||
["wrong closing bracket", `{"value":${arrays},"value":"done"]`, false],
|
||||
] as const;
|
||||
|
||||
it.each(cases)("reads JSON with %s", async (_name, text, readable) => {
|
||||
const root = await createTestTempDirectory("desktop-json-depth-");
|
||||
const script = join(root, "read.py");
|
||||
const imports = PYTHON_IMPORTS.split("\n")
|
||||
.filter((line) => !line.includes("agentenv_protocol"))
|
||||
.join("\n");
|
||||
await writeFile(
|
||||
script,
|
||||
`${imports}${PYTHON_HELPERS}
|
||||
text = json.load(sys.stdin)
|
||||
print(json.dumps(_text_view("state.json", text, len(text.encode("utf-8")))))
|
||||
`,
|
||||
);
|
||||
const output = JSON.parse(
|
||||
execFileSync(
|
||||
process.platform === "win32" ? "python" : "python3",
|
||||
["-I", "-B", "-X", "utf8", script],
|
||||
{ input: JSON.stringify(text), encoding: "utf8" },
|
||||
),
|
||||
) as { format: string; view: unknown; error?: string };
|
||||
const bytes = Buffer.from(text);
|
||||
const format = detectStateFormat("state.json", bytes);
|
||||
if (format !== "json" && format !== "text")
|
||||
throw new Error(`Unexpected native format ${format}`);
|
||||
const native = textualView(format, bytes);
|
||||
expect(format === "json" && native.ok).toBe(readable);
|
||||
expect(output.format).toBe(format);
|
||||
if (native.ok) {
|
||||
expect(output.error).toBeUndefined();
|
||||
// Compare JSON values on their wire form, independent of object prototypes.
|
||||
expect(JSON.stringify(output.view)).toBe(JSON.stringify(native.value));
|
||||
if (
|
||||
readable &&
|
||||
(_name.startsWith("discarded") || _name === "escaped duplicate key")
|
||||
)
|
||||
expect(output.view).toEqual({ value: "done" });
|
||||
} else {
|
||||
expect(output.view).toBeNull();
|
||||
expect(output.error).toBe(native.error);
|
||||
}
|
||||
});
|
||||
|
||||
it("bounds construction before refusing retained excessive JSON depth", async () => {
|
||||
const root = await createTestTempDirectory("desktop-json-construction-");
|
||||
const script = join(root, "bounded.py");
|
||||
const imports = PYTHON_IMPORTS.split("\n")
|
||||
.filter((line) => !line.includes("agentenv_protocol"))
|
||||
.join("\n");
|
||||
await writeFile(
|
||||
script,
|
||||
`${imports}${PYTHON_HELPERS}
|
||||
import tracemalloc
|
||||
text = "[" * 20_000 + "0" + "]" * 20_000
|
||||
recursion_limit = sys.getrecursionlimit()
|
||||
tracemalloc.start()
|
||||
entry = _json_view(text, len(text))
|
||||
peak = tracemalloc.get_traced_memory()[1]
|
||||
tracemalloc.stop()
|
||||
print(json.dumps({"entry": entry, "peak": peak, "recursion_unchanged": sys.getrecursionlimit() == recursion_limit}))
|
||||
`,
|
||||
);
|
||||
const output = JSON.parse(
|
||||
execFileSync(
|
||||
process.platform === "win32" ? "python" : "python3",
|
||||
["-I", "-B", "-X", "utf8", script],
|
||||
{ encoding: "utf8" },
|
||||
),
|
||||
) as { entry: unknown; peak: number; recursion_unchanged: boolean };
|
||||
expect(output.entry).toEqual({
|
||||
format: "json",
|
||||
view: null,
|
||||
error: "nested deeper than the 500 levels a JSON view keeps",
|
||||
});
|
||||
expect(output.recursion_unchanged).toBe(true);
|
||||
// Python heap only, excluding source construction and interpreter/OS memory.
|
||||
// A value retained only to refuse it must not allocate every input container.
|
||||
expect(output.peak).toBeLessThan(1024 * 1024);
|
||||
});
|
||||
|
||||
it("bounds allocations when a very deep value is overwritten", async () => {
|
||||
const root = await createTestTempDirectory("desktop-json-memory-");
|
||||
const script = join(root, "read.py");
|
||||
const imports = PYTHON_IMPORTS.split("\n")
|
||||
.filter((line) => !line.includes("agentenv_protocol"))
|
||||
.join("\n");
|
||||
await writeFile(
|
||||
script,
|
||||
`${imports}\nimport tracemalloc\n${PYTHON_HELPERS}
|
||||
text = '{"value":' + '[' * 200000 + '1' + ']' * 200000 + ',"value":"done"}'
|
||||
tracemalloc.start()
|
||||
result = _json_view(text, len(text))
|
||||
_, peak = tracemalloc.get_traced_memory()
|
||||
print(json.dumps({"view": result["view"], "peak": peak}))
|
||||
`,
|
||||
);
|
||||
const measured = JSON.parse(
|
||||
execFileSync(
|
||||
process.platform === "win32" ? "python" : "python3",
|
||||
["-I", "-B", script],
|
||||
{ encoding: "utf8", timeout: 30000 },
|
||||
),
|
||||
) as { view: unknown; peak: number };
|
||||
expect(measured.view).toEqual({ value: "done" });
|
||||
expect(measured.peak).toBeLessThan(8 * 1024 * 1024);
|
||||
}, 40000);
|
||||
|
||||
it("bounds a wide container already beyond the retained view depth", async () => {
|
||||
const root = await createTestTempDirectory("desktop-json-wide-depth-");
|
||||
const script = join(root, "read.py");
|
||||
const imports = PYTHON_IMPORTS.split("\n")
|
||||
.filter((line) => !line.includes("agentenv_protocol"))
|
||||
.join("\n");
|
||||
await writeFile(
|
||||
script,
|
||||
`${imports}\nimport tracemalloc\n${PYTHON_HELPERS}
|
||||
text = '[' * 501 + ','.join(['"abc"'] * 600000) + ']' * 501
|
||||
tracemalloc.start()
|
||||
result = _json_view(text, len(text))
|
||||
_, peak = tracemalloc.get_traced_memory()
|
||||
print(json.dumps({"error": result.get("error"), "peak": peak}))
|
||||
`,
|
||||
);
|
||||
const measured = JSON.parse(
|
||||
execFileSync(
|
||||
process.platform === "win32" ? "python" : "python3",
|
||||
["-I", "-B", script],
|
||||
{ encoding: "utf8", timeout: 30000 },
|
||||
),
|
||||
) as { error: string; peak: number };
|
||||
expect(measured.error).toBe(
|
||||
"nested deeper than the 500 levels a JSON view keeps",
|
||||
);
|
||||
expect(measured.peak).toBeLessThan(8 * 1024 * 1024);
|
||||
}, 40000);
|
||||
@@ -34,7 +34,7 @@ const execPython = (
|
||||
try {
|
||||
writeFileSync(script, args[codeIndex + 1]!, "utf8");
|
||||
return execFileSync(
|
||||
"python3",
|
||||
process.platform === "win32" ? "python" : "python3",
|
||||
[
|
||||
"-X",
|
||||
"utf8",
|
||||
@@ -375,15 +375,16 @@ it("checks text for JSON the way REA does, token by token, without parsing it",
|
||||
"-I",
|
||||
"-B",
|
||||
"-c",
|
||||
`${imports}${PYTHON_HELPERS}\nprint(json.dumps([_is_json(text) for text in json.load(sys.stdin)]))`,
|
||||
`${imports}${PYTHON_HELPERS}\ntexts = json.load(sys.stdin)\nprint(json.dumps({"syntax": [_read_json(text, build=False) is not _INVALID_JSON for text in texts], "built": [_read_json(text) is not _INVALID_JSON for text in texts]}))`,
|
||||
],
|
||||
{ input: JSON.stringify(texts), encoding: "utf8" },
|
||||
),
|
||||
) as boolean[];
|
||||
) as { syntax: boolean[]; built: boolean[] };
|
||||
|
||||
const parsed = texts.map((text) => safeParseJson(text).ok);
|
||||
expect(texts.map((text) => isJsonText(text))).toEqual(parsed);
|
||||
expect(python).toEqual(parsed);
|
||||
expect(python.syntax).toEqual(parsed);
|
||||
expect(python.built).toEqual(parsed);
|
||||
expect(parsed.filter(Boolean)).toHaveLength(10);
|
||||
});
|
||||
|
||||
@@ -445,9 +446,13 @@ print(json.dumps([_text_view("settings.ini", text, len(text.encode()))["view"]["
|
||||
const root = await createTestTempDirectory("rea-keyfile-python-");
|
||||
const script = join(root, "fixture.py");
|
||||
await writeFile(script, program, "utf8");
|
||||
const output = execFileSync("python3", ["-I", "-B", "-X", "utf8", script], {
|
||||
input: JSON.stringify(inputs),
|
||||
encoding: "utf8",
|
||||
});
|
||||
const output = execFileSync(
|
||||
process.platform === "win32" ? "python" : "python3",
|
||||
["-I", "-B", "-X", "utf8", script],
|
||||
{
|
||||
input: JSON.stringify(inputs),
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
expect(JSON.parse(output)).toEqual(inputs.map(keyfileView));
|
||||
});
|
||||
@@ -71,6 +71,7 @@ export interface FakeKunaBehavior {
|
||||
| "with-bodies"
|
||||
| "noisy";
|
||||
readonly graphIndirectCalls?: boolean;
|
||||
readonly graphDataFunction?: string;
|
||||
/** Bytes of an extra root member in the graph JSON. */
|
||||
readonly graphPadding?: number;
|
||||
readonly xrefs?: "ok" | "hang" | "not-json";
|
||||
@@ -145,11 +146,11 @@ if (args[0] === "--version") {
|
||||
else {
|
||||
if (behavior.graph === "noisy") process.stderr.write(Buffer.alloc(9 * 1024 * 1024, 0x78));
|
||||
if (args[args.indexOf("--functions") + 1] !== "") throw new Error("graph must request no bodies");
|
||||
const nodes = [...functions, { name: "puts", addr: "0x2000" }].map((fn) => ({ address: parseInt(fn.addr, 16), name: fn.name, kind: "normal", hasIndirectCalls: behavior.graphIndirectCalls !== false && fn === functions[0], forwardsTo: null, codeC: behavior.graph === "with-bodies" ? "unrequested code" : null, assembly: null, error: null }));
|
||||
const nodes = [...functions, { name: "puts", addr: "0x2000" }].map((fn) => ({ address: parseInt(fn.addr, 16), name: fn.name, kind: fn.name === behavior.graphDataFunction ? "data" : "normal", hasIndirectCalls: behavior.graphIndirectCalls !== false && fn === functions[0], forwardsTo: null, codeC: behavior.graph === "with-bodies" ? "unrequested code" : null, assembly: null, error: null }));
|
||||
const edges = [];
|
||||
if (functions[1]) edges.push({ callerAddress: nodes[0].address, calleeAddress: nodes[1].address, kind: "call", calleeOrder: 0 });
|
||||
if (functions[2]) edges.push({ callerAddress: nodes[0].address, calleeAddress: nodes[2].address, kind: functions[2].error ? "jump" : "call", calleeOrder: 1 });
|
||||
if (functions[1]?.name === "parse") edges.push({ callerAddress: nodes[1].address, calleeAddress: 8192, kind: "call", calleeOrder: 0 });
|
||||
if (functions[1]?.name === "parse" && nodes[1].kind !== "data") edges.push({ callerAddress: nodes[1].address, calleeAddress: 8192, kind: "call", calleeOrder: 0 });
|
||||
setTimeout(() => process.stdout.write(JSON.stringify({ schemaVersion: 4, binary: { sourcePath: args[1], functionCount: nodes.length + (behavior.graph === "miscounted" ? 1 : 0), edgeCount: edges.length }, functions: nodes, edges, padding: "x".repeat(behavior.graphPadding ?? 0) })), behavior.referenceDelayMs ?? 0);
|
||||
}
|
||||
} else if (args[0] === "xrefs") {
|
||||
|
||||
@@ -7,6 +7,63 @@ import { FUNCTIONS, run, session } from "./kunaSessionFixture.js";
|
||||
|
||||
const it = test.skipIf(process.platform === "win32");
|
||||
|
||||
it("preserves observed edges while qualifying undiscovered calls in both directions", async () => {
|
||||
const { open } = await session("rea-kuna-data-call-path-", {
|
||||
graphIndirectCalls: false,
|
||||
graphDataFunction: "parse",
|
||||
});
|
||||
const client = open();
|
||||
try {
|
||||
const tools = new EnhancedTools(client);
|
||||
const forward = await tools.execute("trace_call_path", {
|
||||
start: "main",
|
||||
goal: "0x2000",
|
||||
direction: "forward",
|
||||
});
|
||||
if (!forward.ok) throw forward.error;
|
||||
expect(forward.value).toMatchObject({
|
||||
goal_status: "unknown",
|
||||
search_scope: {
|
||||
discovery: "partial",
|
||||
observed_goal_status: "not_reached",
|
||||
},
|
||||
relationship_coverage: {
|
||||
"0x1040": {
|
||||
status: "complete",
|
||||
discovery: { callees: { status: "partial" } },
|
||||
},
|
||||
},
|
||||
truncated: false,
|
||||
});
|
||||
const backward = await tools.execute("trace_call_path", {
|
||||
start: "0x2000",
|
||||
goal: "main",
|
||||
direction: "backward",
|
||||
});
|
||||
if (!backward.ok) throw backward.error;
|
||||
expect(backward.value).toMatchObject({
|
||||
goal_status: "unknown",
|
||||
search_scope: {
|
||||
discovery: "unknown",
|
||||
observed_goal_status: "not_reached",
|
||||
},
|
||||
});
|
||||
const observed = await tools.execute("trace_call_path", {
|
||||
start: "main",
|
||||
goal: "parse",
|
||||
direction: "forward",
|
||||
});
|
||||
if (!observed.ok) throw observed.error;
|
||||
expect(observed.value).toMatchObject({
|
||||
goal_status: "reached",
|
||||
traversal_path: ["0x1000", "0x1040"],
|
||||
search_scope: { discovery: "unknown", observed_goal_status: "reached" },
|
||||
});
|
||||
} finally {
|
||||
expect(await client.close()).toEqual({ ok: true, value: null });
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves graph-only imports as graph roots and path endpoints without exporting C", async () => {
|
||||
const { open, kuna } = await session("rea-kuna-graph-only-import-", {
|
||||
inventoryImports: false,
|
||||
|
||||
@@ -66,7 +66,15 @@ it("matches a simple-path oracle for every three-node directed graph in both dir
|
||||
createAnalysisExecution(
|
||||
[...related, ...related],
|
||||
{ id: "oracle", name: "Oracle", version: null },
|
||||
{ relationshipCoverage: { status: "complete" } },
|
||||
{
|
||||
relationshipCoverage: {
|
||||
status: "complete",
|
||||
discovery: {
|
||||
callers: { status: "complete" },
|
||||
callees: { status: "complete" },
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user