Compare commits

...
Author SHA1 Message Date
morluto bd234dedfa Merge commit 'ac30fbd' into HEAD 2026-11-07 23:15:35 +08:00
morluto 1f4edf466c fix: preserve call discovery across facet limits 2026-11-07 23:15:08 +08:00
morluto ac30fbd658 style(test): format CLI help default annotations (#9280) 2026-11-07 23:02:44 +08:00
Nianze Wuandmorluto 5d21bddb18 fix(desktop): read JSON with deeply nested overwritten values (#9277)
* fix(desktop): read JSON with deeply nested overwritten values

* fix(desktop): bound exported JSON construction by view depth

* fix(desktop): preserve deep JSON numeric facts within bounded memory

---------

Co-authored-by: morluto <[email protected]>
2026-11-07 22:57:15 +08:00
morluto 95c8fc2aa5 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-07 22:53:59 +08:00
morlutoandCursor Agent 2e2f981ccf test: assert absolute-path facts without frozen sentences (#9231)
* test: assert absolute-path facts without frozen sentences

Sibling path tests now require the field path, invalid_format, and regex
expectation, and they record that each negative case ran. NUL rejection
follows the argument that contains the byte instead of searching for the
word NUL.

* test: retain actionable NUL path diagnostics

---------

Co-authored-by: Cursor Agent <[email protected]>
2026-11-07 22:46:17 +08:00
Heo Hao f8bb792f15 fix(cli): stop restating schema defaults in help descriptions (#9278)
The help renderer appends "(default: X)" whenever the option field declares a
schema default, so descriptions that also spell the default out print it twice:

  --block-size <value>  Power-of-two block size from 256 to 65536 bytes
                        (default 4096) (default: 4096)

Remove the hand-written default from the eleven options that own a schema
default across inspect-byte-entropy, inspect-devicetree,
inspect-packet-capture, inspect-packet-fields, inspect-signal-decoders and
decode-signal-recording, matching the rule in cliDecimalField's contract that
the domain field owns bounds, defaults and help.

Options without a schema default keep their prose: --match_limit, --line_limit,
--limit and --packet_limit are nullable or optional, so the renderer emits no
annotation and the description is the only place the effective value appears.

Add a boundary test asserting each such option states its default exactly once,
so a restated default cannot come back unnoticed.
2026-11-07 22:38:03 +08:00
Cursor Agent 367be83922 merge(main): revise call-path profiles without collapsing value traces
trace_call_path and trace_native_values both use workflow revision 2.
Value-trace absence stays specific to trace_native_values, and an
unrevised operation still matches the default profile.
2026-11-07 11:53:30 +00:00
morluto 670d025e33 merge: integrate current main into call discovery scope 2026-11-04 08:31:28 +08:00
morluto ef5a7e68b8 merge: integrate current main into call discovery scope 2026-11-02 07:47:23 +08:00
morluto b7160bd7c6 test: declare discovery in reconstruction controls 2026-11-01 14:43:11 +08:00
morluto 8dd4cf4a9d fix(evidence): retain discovery limits in call verification 2026-11-01 14:29:34 +08:00
morluto 30f5df2cd9 fix: preserve call discovery reasons in path evidence 2026-11-01 14:07:07 +08:00
morluto fd3b42b285 fix: preserve compact call coverage schema guidance 2026-11-01 14:03:20 +08:00
morluto 3310d82413 fix!: qualify call paths by provider discovery 2026-11-01 13:53:43 +08:00
58 changed files with 1882 additions and 372 deletions

No files matched your search

+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
+65 -33
View File
@@ -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;
+2 -1
View File
@@ -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.",
},
],
});
+6 -8
View File
@@ -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 }) =>
+1 -1
View File
@@ -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",
}),
]);
});
+1 -1
View File
@@ -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(
+5 -1
View File
@@ -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,
+10
View File
@@ -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(),
+5 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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,
};
};
+29 -3
View File
@@ -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,
}),
]);
+1 -1
View File
@@ -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()
+2 -4
View File
@@ -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()
+32 -18
View File
@@ -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 = (
+6
View File
@@ -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",
+3 -142
View File
@@ -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
+231
View File
@@ -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}
`;
+4
View File
@@ -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.",
],
});
});
});
+38 -1
View File
@@ -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,
};
};
+16 -2
View File
@@ -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),
+50 -26
View File
@@ -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 = (
+6
View File
@@ -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([
+4
View File
@@ -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,
});
};
+20 -1
View File
@@ -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),
+4 -1
View File
@@ -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 {
+29
View File
@@ -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,
+1 -3
View File
@@ -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()
+1 -1
View File
@@ -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()
+1
View File
@@ -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: {
+15 -1
View File
@@ -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:
+4
View File
@@ -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 {
+26 -4
View File
@@ -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);
});
+22 -14
View File
@@ -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));
});
+3 -2
View File
@@ -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" },
},
},
},
),
);
},