Compare commits

...
25 changed files with 930 additions and 67 deletions

No files matched your search

+3
View File
@@ -612,6 +612,9 @@ Saved snapshots include only cache entries whose subject identifies the selected
target. Unbound observations and observations of child artifacts remain available
in the open session and its Evidence bundle; they are excluded from the target's
saved replay entries without changing their subjects.
The snapshot also retains available Evidence linked by selected records, including
transitive sources with an unbound or different subject. Those sources are not
replay entries for the selected target. Unrelated records remain excluded.
Snapshots retain eligible target-scoped question histories in full, including
mutation Evidence recorded while another target was active. Observations and
+12
View File
@@ -218,6 +218,12 @@ Malformed provider replies return `unreadable_output` for
`resolve_containing_procedure` before successful-result projection;
cancellation and incomplete cleanup remain errors.
For a procedure match, `procedure_references` with `direction: "incoming"` can
provide the selected provider's reference kinds when supported. Its scope covers
the procedure body, so compare target addresses when selecting references to the
exact entry. It is a separate observation and does not establish runtime use or
complete reference discovery.
REA prepares complete MCP results within the pinned stdio client's 10 MiB
receive-buffer budget, including both text and structured representations and
room for the JSON-RPC envelope. Tool errors omit `structuredContent` from the
@@ -377,6 +383,12 @@ is null. `total` counts all matching heads; each page preserves complete records
in stable unknown ID order. Filters apply before paging. Pages reflect current
session state, so keep the unknown ledger unchanged while traversing it.
`list_unknowns` only lists registered heads. Import restores the Unknown records
present in a bundle; it does not turn residual question text embedded in Evidence
into registry entries. An empty registry therefore does not establish that
retained Evidence has no open questions. Inspect the relevant Evidence or its
analysis view for its residual questions.
Automatically retained workflow questions include their immutable result
Evidence ID. Distinct results retain independent questions and source links;
repeating the same result does not create another unknown. Workflow Evidence
+14
View File
@@ -300,6 +300,20 @@ relationships but reports their completeness as unknown. In particular, a resolv
tail call observed in one direction does not prove that the reverse inventory is
complete. An empty Hopper inventory cannot establish that no such call exists.
No whole-binary reverse scan is performed.
For large native traces, `trace_feature` accepts `detail: "summary"`. It retains
the complete trace and returns counts and its parent Evidence ID. Use
`inspect_analysis_view` with that ID and `view: {kind: "native-feature-trace",
collection: "references", offset: 0, limit: 64}` to page the original rows;
`matches` and `unknowns` are separate collections. Matches accept an exact
`address`; references accept exact `source_address` and `target_address`
filters. Row indices refer to the retained collection, and page coverage does not establish provider discovery
completeness. `list_unknowns` pages the independently managed question heads.
In the one-shot CLI, `rea trace ... --detail summary --evidence_output PATH`
stores the trace and its linked source Evidence for later
`inspect-analysis-view` calls. This bundle does not include the CLI session's
Unknown registry; its original question texts remain available in the trace's
`unknowns` pages.
Regex searches use ECMAScript Unicode syntax in a cancellable worker with a
matching deadline and a worker old-generation heap. Deadline or cancellation
stops matching while leaving the Hopper API available. The deadline is
+40
View File
@@ -114,6 +114,46 @@ export const verifyKunaReferences = async ({
);
}
assert.equal(trace.value.truncated, false);
const summary = await call("trace_feature", {
query: name,
detail: "summary",
});
assert.equal(
summary.value.parent_evidence_id,
trace.structured.evidence_id,
);
assert.equal(
summary.value.summary.references,
trace.value.references.length,
);
assert.equal(
summary.value.summary.unknowns,
trace.value.residual_unknowns.length,
);
for (const [collection, values] of [
["matches", trace.value.matches],
["references", trace.value.references],
["unknowns", trace.value.residual_unknowns],
]) {
const view = await call("inspect_analysis_view", {
source: {
kind: "retained-evidence",
evidence_id: trace.structured.evidence_id,
},
view: {
kind: "native-feature-trace",
collection,
offset: 0,
limit: 1,
},
});
assert.equal(view.value.coverage.total, values.length);
assert.deepEqual(
view.value.items.rows.map(({ value }) => value),
values.slice(0, 1),
);
assert.equal(view.value.items.collection, collection);
}
}
composed = {
graph_ms: graph.ms,
+6 -11
View File
@@ -18,7 +18,10 @@ import { silentLogger } from "../logger.js";
import type { Logger } from "pino";
import { withEvidenceCallerInputPath } from "../domain/evidence.js";
import { createImmutableEvidenceBundle } from "../domain/evidenceBundle.js";
import { retainAnalysisResultSources } from "./investigation/recordEvidenceSources.js";
import {
retainAnalysisResultSources,
type AnalysisResultWithSources,
} from "./investigation/recordEvidenceSources.js";
import {
createInvestigationEvidenceClosureChecker,
resolveInvestigationEvidenceClosure,
@@ -145,11 +148,7 @@ export const runDirectAnalysis = async (
readonly espFlash?: EspFlashSelection;
readonly includeEvidenceSources?: boolean;
} = {},
): Promise<
Result<Evidence, JsonValue> & {
readonly sourceEvidence?: readonly Evidence[];
}
> =>
): Promise<AnalysisResultWithSources<Evidence, JsonValue>> =>
withProcessCancellation(options.signal, (signal) =>
runAnalysis(dependencies, path, tool, arguments_, {
logger: options.logger ?? silentLogger,
@@ -384,11 +383,7 @@ const runAnalysis = async (
readonly entryPoint?: string;
readonly espFlash?: EspFlashSelection;
},
): Promise<
Result<Evidence, JsonValue> & {
readonly sourceEvidence?: readonly Evidence[];
}
> => {
): Promise<AnalysisResultWithSources<Evidence, JsonValue>> => {
const { logger, signal, snapshotPath } = options;
const evidenceParameters = options.evidenceParameters ?? arguments_;
let session: BinarySession;
+2 -6
View File
@@ -3,7 +3,7 @@ import type { z } from "zod";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { JsonValue } from "../domain/jsonValue.js";
import type { TraceContainingProcedure } from "../domain/native/nativeObservation.js";
import type { LiteralFeatureReference } from "../domain/native/literalFeatureTrace.js";
import { err, type Result } from "../domain/result.js";
import {
enhancedInputSchemas,
@@ -27,11 +27,7 @@ export const failedWithPartialResult = (
error: AnalysisError,
partialResult: JsonValue,
): PartialEnhancedFailure => ({ ok: false, error, partialResult });
export type TraceReference = {
readonly target_address: string;
readonly source_address: string;
readonly containing_procedure: TraceContainingProcedure;
};
export type TraceReference = LiteralFeatureReference;
/** One enhanced call whose input was parsed at its owning adapter boundary. */
export type ValidatedEnhancedCall = {
@@ -5,7 +5,10 @@ import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
/** Source facts remain available even when a completed observation is cancelled. */
export type AnalysisResultWithSources<Value> = Result<Value, AnalysisError> & {
export type AnalysisResultWithSources<Value, Failure = AnalysisError> = Result<
Value,
Failure
> & {
readonly sourceEvidence?: readonly Evidence[];
};
+2 -3
View File
@@ -32,6 +32,7 @@ import {
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import type { ZodSafeParseResult } from "zod";
import { bindCliEvidenceCallerPath } from "./cliEvidenceCallerPath.js";
import type { AnalysisResultWithSources } from "../application/investigation/recordEvidenceSources.js";
export const analysisEvidenceDeliveryOptions = {
detail: z
@@ -203,9 +204,7 @@ const withUndeliveredSourceClosure = (
/** Publish already finalized CLI analysis without opening a second provider/session. */
export const publishCliAnalysisEvidence = async (
result: Result<Evidence, JsonValue> & {
readonly sourceEvidence?: readonly Evidence[];
},
result: AnalysisResultWithSources<Evidence, JsonValue>,
input: CliEvidenceDelivery,
): Promise<JsonValue> => {
input = {
+32 -15
View File
@@ -357,24 +357,41 @@ const registerTraceCommand = (
.default(false)
.describe("Match the query with exact letter case"),
snapshot: snapshotSelectionOption,
...analysisEvidenceDeliveryOptions,
...nativeOpenOptionFields,
}),
run: ({ args, options }) =>
logCliCommand(logger, "trace", () =>
runDirectAnalysis(
args.path,
"trace_feature",
{
query: resolveCliAnalysisSelector(
args.query,
options.query,
"query",
"trace_feature",
),
case_sensitive: options.caseSensitive,
},
directAnalysisOptionsFromCli(logger, options),
).then(projectCliAnalysisResult),
withCommandCancellation((signal) =>
logCliCommand(logger, "trace", async () => {
const delivery = {
providerId: "auto",
operation: "trace_feature",
signal,
detail: options.detail,
evidenceOutput: options.evidence_output,
};
const inputError = cliEvidenceDeliveryInputError(delivery);
if (inputError !== undefined) return projectAnalysisError(inputError);
const complete = await runDirectAnalysis(
args.path,
"trace_feature",
{
query: resolveCliAnalysisSelector(
args.query,
options.query,
"query",
"trace_feature",
),
case_sensitive: options.caseSensitive,
},
{
...directAnalysisOptionsFromCli(logger, options),
signal,
includeEvidenceSources: options.evidence_output !== undefined,
},
);
return publishCliAnalysisEvidence(complete, delivery);
}),
),
});
};
+1 -1
View File
@@ -17,7 +17,7 @@ export const ANALYSIS_VIEW_TOOL_CONTRACTS = [
...toolContractMetadata("inspect_analysis_view"),
kind: "application",
description:
'Use javascript-routes collections to query route patterns, source-linked occurrences, source status, unknowns or summary counts from retained JavaScript application or web-bundle Evidence. Use web-bundle collections endpoints or inferences to page endpoint observations and inferences from retained analyze_web_bundle Evidence; summary returns target URL, origin, and counts. Inspect selected views of supported completed Evidence without repeating analysis. Choose a view compatible with the parent operation described below. Command summary views preserve ordered GDB records or opaque objdump stdout/stderr once, source parameters, completion facts and parent identity. They do not establish current target state. Historical capture summaries return identity and counts without bodies or archive inventories. Capture views page retained records: view {kind: "capture", detail: "preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields and binary descriptors, complete returns exact records with all sidecars. Offsets follow the selected parent record order, preserving original producer ordinals. Paging never reopens the source or reruns the decoder. Native export tables use kind native-exports, collection exports/issues, exact names or original indices, selected fields and matching-row pagination; parent Evidence plus source_index identifies each row. Source coverage stays separate from page coverage. BEAM views use kind beam, collections atoms/exports/imports/lambdas/chunks, exact names/indices/fields and matching-row pages; import modules intersects function names. source_index identifies original parent-and-collection rows, preserving every arity and raw atom index. beam.counts preserves null missing tables versus decoded empty tables; runtime absence remains unknown. PYC views use kind python-pyc, collection code_objects, names/indices/fields and paging; source_index scopes every same-name alternative to the parent, nested_code keeps code-constant references, and pyc.coverage preserves source coverage. Go metadata uses kind go, collection dependencies/settings, exact paths/keys or indices, fields and pagination. Row index refers to the original collection; empty selections return no rows, and dependency coverage remains unknown. Rust metadata uses kind rust, collection packages/edges, exact names/versions or original indices, selected fields, edge from_indices/to_indices and matching-row pagination. Embedded-record coverage is separate from unknown runtime dependencies. For inspect_pcap_capture parents, kind pcap pages endpoints, dns-queries, tls-sni, protocols, http-uris or unattributed-addresses. Select an exact query for dns-addresses, or a tshark/capinfos stream for UTF-16 diagnostics text. Page totals count retained facts; source coverage remains explicit. Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams, loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except traces, select trace_index; stream-specific facets also require exact stream_id and direction. Facts retain all-recorded-events-before-selection scope, and interval overlap or endpoint loss declarations do not prove retransmission or network loss. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. The environment-manifest view projects the same HAR requests on the primary origin (most JSON-like records) into a replayable tool surface: view {kind: "environment-manifest", template_rules: ["after-uid-literal", "opaque-token", "sibling-variance"], include_query_parameter_names: true}. Each tool has a deterministic name, a path template and parameters from the four format rules plus the selected opt-in rules, observed query parameter names, a value-free body shape for POST/PUT/PATCH, status and content-type sets and record ordinals; records are pointers into the parent, excluded groups carry a reason and three fixed unknowns are always stated. Templates, parameters and names are inferences; the view copies no header, cookie or body values and refuses parents inspected with sensitive values. A capture_browser_scenario parent also supports summary and browser-scenario steps/events/transactions/text pages, preserving step_indices, event_kinds, transaction_ids and selected artifacts. A capture_browser_scenario parent that selected response bodies or header values projects the same way, one record per network transaction in request order; it relies on capture-time redaction of declared secrets instead of that refusal, and a transaction whose URL was redacted is left unrepresented. The environment-state view extracts, from the same parents, the initial state the recorded reads observed: view {kind: "environment-state", template_rules: [...]} with the rules of the manifest it accompanies. It reads response bodies recorded before the fLine truncated
'Use javascript-routes collections to query route patterns, source-linked occurrences, source status, unknowns or summary counts from retained JavaScript application or web-bundle Evidence. Use web-bundle collections endpoints or inferences to page endpoint observations and inferences from retained analyze_web_bundle Evidence; summary returns target URL, origin, and counts. For retained trace_feature Evidence, kind native-feature-trace pages matches, references or unknowns; matches filter exact address, references filter exact source_address and target_address, source_index identifies each original row, and list_unknowns pages durable question heads. Inspect selected views of supported completed Evidence without repeating analysis. Choose a view compatible with the parent operation described below. Command summary views preserve ordered GDB records or opaque objdump stdout/stderr once, source parameters, completion facts and parent identity. They do not establish current target state. Historical capture summaries return identity and counts without bodies or archive inventories. Capture views page retained records: view {kind: "capture", detail: "preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields and binary descriptors, complete returns exact records with all sidecars. Offsets follow the selected parent record order, preserving original producer ordinals. Paging never reopens the source or reruns the decoder. Native export tables use kind native-exports, collection exports/issues, exact names or original indices, selected fields and matching-row pagination; parent Evidence plus source_index identifies each row. Source coverage stays separate from page coverage. BEAM views use kind beam, collections atoms/exports/imports/lambdas/chunks, exact names/indices/fields and matching-row pages; import modules intersects function names. source_index identifies original parent-and-collection rows, preserving every arity and raw atom index. beam.counts preserves null missing tables versus decoded empty tables; runtime absence remains unknown. PYC views use kind python-pyc, collection code_objects, names/indices/fields and paging; source_index scopes every same-name alternative to the parent, nested_code keeps code-constant references, and pyc.coverage preserves source coverage. Go metadata uses kind go, collection dependencies/settings, exact paths/keys or indices, fields and pagination. Row index refers to the original collection; empty selections return no rows, and dependency coverage remains unknown. Rust metadata uses kind rust, collection packages/edges, exact names/versions or original indices, selected fields, edge from_indices/to_indices and matching-row pagination. Embedded-record coverage is separate from unknown runtime dependencies. For inspect_pcap_capture parents, kind pcap pages endpoints, dns-queries, tls-sni, protocols, http-uris or unattributed-addresses. Select an exact query for dns-addresses, or a tshark/capinfos stream for UTF-16 diagnostics text. Page totals count retained facts; source coverage remains explicit. Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams, loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except traces, select trace_index; stream-specific facets also require exact stream_id and direction. Facts retain all-recorded-events-before-selection scope, and interval overlap or endpoint loss declarations do not prove retransmission or network loss. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. The environment-manifest view projects the same HAR requests on the primary origin (most JSON-like records) into a replayable tool surface: view {kind: "environment-manifest", template_rules: ["after-uid-literal", "opaque-token", "sibling-variance"], include_query_parameter_names: true}. Each tool has a deterministic name, a path template and parameters from the four format rules plus the selected opt-in rules, observed query parameter names, a value-free body shape for POST/PUT/PATCH, status and content-type sets and record ordinals; records are pointers into the parent, excluded groups carry a reason and three fixed unknowns are always stated. Templates, parameters and names are inferences; the view copies no header, cookie or body values and refuses parents inspected with sensitive values. A capture_browser_scenario parent also supports summary and browser-scenario steps/events/transactions/text pages, preserving step_indices, event_kinds, transaction_ids and selected artifacts. A capture_browser_scenario parent that selected response bodies or header values projects the same way, one record per network transaction in request order; it relies on capture-time redaction of declared secrets instead of that refusal, and a transaction whose URL was redactLine truncated
inputSchema: inspectAnalysisViewInputSchema,
outputSchema: evidenceResultOf(analysisViewResultSchema),
examples: [
+2 -2
View File
@@ -82,8 +82,8 @@ export const ENHANCED_TOOL_CONTRACTS = [
...FUNCTION_WORKFLOW_TOOL_CONTRACTS,
enhanced(
"trace_feature",
"Trace the complete literal query, preserving spaces, through every matching string and procedure and their xrefs. Preserve an explicit unavailable owner for a non-retryable containment capability refusal. Returns observations and residual unknowns without inferring reference kinds; truncated only reports an observed reference cap.",
enhancedInputSchemas.trace_feature,
"Trace the complete literal query, preserving spaces, through every matching string and procedure and their xrefs. Preserve an explicit unavailable owner for a non-retryable containment capability refusal. Returns observations and residual unknowns without inferring reference kinds; truncated only reports an observed reference cap. For a procedure match, use procedure_references with direction incoming for provider-reported kinds when supported; it covers the procedure body, so compare target addresses when selecting exact-entry references. Select detail summary to retain complete Evidence and page matches, references and unknowns with inspect_analysis_view kind native-feature-trace.",
enhancedInputSchemas.trace_feature.extend({ detail: analysisDetailSchema }),
),
enhanced(
"trace_call_path",
+2 -2
View File
@@ -98,7 +98,7 @@ export const SESSION_TOOL_CONTRACTS = [
),
session(
"import_evidence_bundle",
"Read the JSON bundle at the supplied local path, validate every Evidence ID and canonical manifest, then atomically merge it. Imported content is data only and is never executed.",
"Read the JSON bundle at the supplied local path, validate every Evidence ID and canonical manifest, then atomically merge it. Imported content is data only and is never executed. Import restores registry entries present in the bundle; questions embedded in Evidence do not automatically become Unknown registry entries.",
importEvidenceBundleInputSchema,
),
session(
@@ -153,7 +153,7 @@ export const SESSION_TOOL_CONTRACTS = [
),
session(
"list_unknowns",
"List current residual-unknown heads in deterministic ID order, with optional exact status, severity, and domain filters. Results are complete inline by default; supply offset and limit to page the filtered order. total is the matching-head count and next_offset is null when the page exhausts the result. This is read-only; unresolved, contradicted, and non-truth dispositions remain distinct.",
"List current residual-unknown heads in deterministic ID order, with optional exact status, severity, and domain filters. Results are complete inline by default; supply offset and limit to page the filtered order. total is the matching-head count and next_offset is null when the page exhausts the result. This is read-only; unresolved, contradicted, and non-truth dispositions remain distinct. An empty registry does not mean retained Evidence has no residual questions; inspect the relevant Evidence or its analysis view.",
listUnknownsInputSchema,
),
session(
+5 -17
View File
@@ -1,7 +1,6 @@
import {
nativeAddressListSchema,
nativeObservationSourceSchemas,
traceContainingProcedureSchema,
} from "../domain/native/nativeObservation.js";
import { overviewInventoryCoverageSchema } from "../domain/native/inventoryCoverage.js";
import {
@@ -10,6 +9,7 @@ import {
nativeFunctionMatchingSummaryViewResultSchema,
nativeFunctionSummaryViewResultSchema,
nativeInventorySummaryViewResultSchema,
nativeFeatureTraceSummaryViewResultSchema,
batchDecompileSummaryViewResultSchema,
nativeLoadImageSummaryViewResultSchema,
artifactInventorySummaryViewResultSchema,
@@ -18,7 +18,7 @@ import {
managedArtifactSummaryViewResultSchema,
managedMemberSummaryViewResultSchema,
} from "../domain/analysisView/analysisView.js";
import { literalFeatureMatchSchema } from "../domain/native/literalFeatureMatch.js";
import { literalFeatureTraceSchema } from "../domain/native/literalFeatureTrace.js";
import { byteEntropySchema } from "../domain/native/byteEntropy.js";
import { nativeFunctionMatchingSchema } from "../domain/native/functionMatching.js";
import { nativeByteReadSchema } from "../domain/native/nativeByteRead.js";
@@ -263,21 +263,9 @@ export const officialOutputSchemas = {
xrefs: evidenceResultOf(nativeAddressListSchema),
} as const satisfies Readonly<Record<string, z.ZodObject>>;
const literalTraceOutput = evidenceResultOf(
z.object({
query: z.string(),
search_mode: z.literal("literal"),
matches: z.array(literalFeatureMatchSchema),
references: z.array(
z.object({
target_address: z.string(),
source_address: z.string(),
containing_procedure: traceContainingProcedureSchema,
}),
),
truncated: z.boolean(),
residual_unknowns: z.array(z.string()),
}),
const literalTraceOutput = analysisDetailOutputSchemaOf(
literalFeatureTraceSchema,
nativeFeatureTraceSummaryViewResultSchema,
);
const callPathTraceOutput = evidenceResultOf(
+56 -1
View File
@@ -462,7 +462,7 @@ describe("analysis snapshot canonical ordering and scope", () => {
foreign,
]),
}),
).toThrow(/another target/u);
).toThrow(/unrelated to the selected target/u);
});
});
@@ -511,6 +511,61 @@ it("does not select arbitrary provenance when multiple authenticated records mat
).toBeUndefined();
});
it("retains linked foreign Evidence without treating it as selected-target replay", () => {
const { current, snapshot } = snapshotWithHistoricalEvidence();
const foreignTarget = { ...ANALYSIS_SNAPSHOT_TARGET, sha256: "b".repeat(64) };
const foreign = createEvidence(foreignTarget, ANALYSIS_SNAPSHOT_PROVIDER, {
operation: current.operation,
parameters: current.parameters,
result: current.normalized_result,
analysisProfile: ANALYSIS_SNAPSHOT_PROFILE,
});
const linked = createEvidence(
ANALYSIS_SNAPSHOT_TARGET,
ANALYSIS_SNAPSHOT_PROVIDER,
{
operation: current.operation,
parameters: current.parameters,
result: current.normalized_result,
analysisProfile: ANALYSIS_SNAPSHOT_PROFILE,
evidenceLinks: [foreign.evidence_id],
},
);
const bundle = createEvidenceBundle([
...snapshot.evidence_bundle.records.filter(
(record) => record.evidence_id !== current.evidence_id,
),
linked,
foreign,
]);
const admitted = parseAnalysisSnapshot({
...snapshot,
evidence_bundle: bundle,
});
expect(
snapshotEvidenceForQuery(admitted, {
target: ANALYSIS_SNAPSHOT_TARGET,
bindingProfile: ANALYSIS_SNAPSHOT_PROFILE,
operation: linked.operation,
parameters: linked.parameters,
provider: ANALYSIS_SNAPSHOT_PROVIDER,
evidenceProfile: ANALYSIS_SNAPSHOT_PROFILE,
}),
).toEqual(linked);
expect(snapshotMatchesTarget(admitted.target, foreignTarget)).toBe(false);
const unrelated = createEvidence(foreignTarget, ANALYSIS_SNAPSHOT_PROVIDER, {
operation: "unrelated",
parameters: {},
result: "foreign",
});
expect(() =>
parseAnalysisSnapshot({
...snapshot,
evidence_bundle: createEvidenceBundle([...bundle.records, unrelated]),
}),
).toThrow("unrelated to the selected target and its linked sources");
});
describe("analysis snapshot Evidence binding", () => {
it("finds only Evidence committed to the exact binding and profile", () => {
const { current, snapshot } = snapshotWithHistoricalEvidence();
+1 -1
View File
@@ -738,7 +738,7 @@ const restoreSnapshot = (
scopedEvidence.unknowns.length !== bundle.unknowns.length
)
throw new TypeError(
"Analysis snapshot evidence contains records for another target",
"Analysis snapshot evidence contains records unrelated to the selected target and its linked sources",
);
if (!hasCanonicalQueryOrder(providerRows.map(({ entry }) => entry)))
throw new TypeError("Analysis snapshot entries are not canonical");
+5
View File
@@ -18,6 +18,7 @@ import { projectJavaScriptRoutesView } from "./javascriptRoutesView.js";
import { projectWebBundleView } from "./webBundleView.js";
import { projectSourceToBundleView } from "./sourceToBundleView.js";
import { projectFeatureTraceView } from "./featureTraceView.js";
import { projectNativeFeatureTraceView } from "./nativeFeatureTraceView.js";
import { projectBatchDecompileView } from "./batchDecompileView.js";
import { projectGpuCodeObjectsView } from "./gpuCodeObjectsView.js";
import { projectModelArtifactView } from "./modelArtifactView.js";
@@ -144,6 +145,7 @@ import {
modelArtifactPageViewResultSchema,
il2cppMetadataSummaryViewResultSchema,
nativeInventorySummaryViewResultSchema,
nativeFeatureTraceSummaryViewResultSchema,
nativeLoadImageSummaryViewResultSchema,
nativeFunctionMatchingSummaryViewResultSchema,
nativeFunctionSummaryViewResultSchema,
@@ -165,6 +167,7 @@ export {
};
export {
nativeObservationSummaryViewResultSchemas,
nativeFeatureTraceSummaryViewResultSchema,
byteEntropySummaryViewResultSchema,
nativeExportsSummaryViewResultSchema,
devicetreeSummaryViewResultSchema,
@@ -300,6 +303,7 @@ const retain = (
): readonly [string, RetainedViewProjector] => [operation, project];
const retainedViewProjectors = new Map<string, RetainedViewProjector>([
retain("trace_feature", projectNativeFeatureTraceView),
...NATIVE_OBSERVATION_OPERATIONS.map((operation) =>
retain(operation, projectNativeObservationView),
),
@@ -436,6 +440,7 @@ export const projectAnalysisView = (
parent.operation === "match_native_functions"
? NATIVE_FUNCTION_MATCHING_PREDICATE
: parent.operation === "analyze_function" ||
parent.operation === "trace_feature" ||
parent.operation === "batch_decompile" ||
isNativeObservationOperation(parent.operation) ||
isNativeInventoryOperation(parent.operation)
@@ -11,6 +11,7 @@ import { javaScriptRoutesViewRequestSchema } from "./javascriptRoutesView.js";
import { webBundleViewRequestSchema } from "./webBundleView.js";
import { sourceToBundleViewRequestSchema } from "./sourceToBundleView.js";
import { featureTraceViewRequestSchema } from "./featureTraceView.js";
import { nativeFeatureTraceViewRequestSchema } from "./nativeFeatureTraceView.js";
import { batchDecompileViewRequestSchema } from "./batchDecompileView.js";
import { gpuCodeObjectsViewRequestSchema } from "./gpuCodeObjectsView.js";
import { modelArtifactViewRequestSchema } from "../model/modelArtifactQuery.js";
@@ -69,6 +70,7 @@ export const analysisViewRequestSchema = z.discriminatedUnion("kind", [
nativeFunctionMatchingViewRequestSchema,
sourceToBundleViewRequestSchema,
featureTraceViewRequestSchema,
nativeFeatureTraceViewRequestSchema,
batchDecompileViewRequestSchema,
summaryViewSchema,
goBinaryViewRequestSchema,
@@ -68,6 +68,11 @@ import {
featureTraceViewCollections,
featureTraceViewContextSchema,
} from "./featureTraceView.js";
import {
nativeFeatureTraceSummarySchema,
nativeFeatureTraceViewItems,
nativeFeatureTraceRequests,
} from "./nativeFeatureTraceView.js";
import {
batchDecompileSummarySchema,
batchDecompileViewCollections,
@@ -833,6 +838,40 @@ export const nativeInventorySummaryViewResultSchema = z.strictObject({
summary: nativeInventorySummarySchema,
});
export const nativeFeatureTraceSummaryViewResultSchema = z.strictObject({
...summaryViewResultBase,
...nativeViewSourceContext,
artifact: artifactSchema.nullable(),
parent_operation: z.literal("trace_feature"),
summary: nativeFeatureTraceSummarySchema,
});
const nativeFeatureTracePage = <
Collection extends keyof typeof nativeFeatureTraceViewItems,
>(
collection: Collection,
) =>
z.strictObject({
...viewResultBase,
...nativeViewSourceContext,
artifact: artifactSchema.nullable(),
kind: z.literal("native-feature-trace"),
parent_operation: z.literal("trace_feature"),
view: nativeFeatureTraceRequests[collection],
summary: nativeFeatureTraceSummarySchema,
items: nativeFeatureTraceViewItems[collection],
});
const nativeFeatureTracePageViewResultSchema = z
.object({ kind: z.literal("native-feature-trace") })
.passthrough()
.pipe(
z.union([
nativeFeatureTracePage("matches"),
nativeFeatureTracePage("references"),
nativeFeatureTracePage("unknowns"),
]),
);
/** Compact provider load observations retain the independent verification state. */
export const nativeLoadImageSummaryViewResultSchema = z.strictObject({
...summaryViewResultBase,
@@ -1153,6 +1192,7 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
modelArtifactSummaryViewResultSchema,
gpuCodeObjectsSummaryViewResultSchema,
nativeInventorySummaryViewResultSchema,
nativeFeatureTraceSummaryViewResultSchema,
nativeLoadImageSummaryViewResultSchema,
nativeFunctionMatchingSummaryViewResultSchema,
nativeFunctionSummaryViewResultSchema,
@@ -1188,6 +1228,7 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
view: nativeInventoryViewRequestSchema,
rows: z.array(nativeInventoryViewRowSchema),
}),
nativeFeatureTracePageViewResultSchema,
z.strictObject({
...viewResultBase,
kind: z.literal("native-load-image"),
@@ -0,0 +1,224 @@
import { z } from "zod";
import { literalFeatureMatchSchema } from "../native/literalFeatureMatch.js";
import {
literalFeatureReferenceSchema,
literalFeatureTraceSchema,
} from "../native/literalFeatureTrace.js";
import { jsonValueSchema } from "../jsonValue.js";
import { err, ok } from "../result.js";
import { analysisViewInputError } from "./analysisViewErrors.js";
import { selectViewRows } from "./selectViewRows.js";
import {
createAnalysisViewResultReader,
type AnalysisViewParent,
} from "./analysisViewParent.js";
import {
assertOwnedView,
type AdmittedView,
type SummaryAnalysisView,
} from "./analysisViewShape.js";
import {
boundedViewMessages,
completeWithinViewCoverage,
pageViewCoverage,
} from "./analysisViewCoverage.js";
const page = {
kind: z.literal("native-feature-trace"),
offset: z.int().nonnegative().default(0),
limit: z.int().positive().default(64),
};
export const nativeFeatureTraceRequests = {
matches: z.strictObject({
...page,
collection: z.literal("matches"),
address: z
.string()
.min(1)
.optional()
.describe("Select rows with this exact retained address."),
}),
references: z.strictObject({
...page,
collection: z.literal("references"),
source_address: z
.string()
.min(1)
.optional()
.describe("Select this exact retained source address."),
target_address: z
.string()
.min(1)
.optional()
.describe("Select this exact retained target address."),
}),
unknowns: z.strictObject({ ...page, collection: z.literal("unknowns") }),
} as const;
export const nativeFeatureTraceViewRequestSchema = z.discriminatedUnion(
"collection",
[
nativeFeatureTraceRequests.matches,
nativeFeatureTraceRequests.references,
nativeFeatureTraceRequests.unknowns,
],
);
const row = <T extends z.ZodType>(value: T) =>
z.strictObject({ source_index: z.int().nonnegative(), value });
export const nativeFeatureTraceViewItems = {
matches: z.strictObject({
collection: z.literal("matches"),
rows: z.array(row(literalFeatureMatchSchema)),
}),
references: z.strictObject({
collection: z.literal("references"),
rows: z.array(row(literalFeatureReferenceSchema)),
}),
unknowns: z.strictObject({
collection: z.literal("unknowns"),
rows: z.array(row(z.string())),
}),
} as const;
export const nativeFeatureTraceSummarySchema = z.strictObject({
query: z.string(),
search_mode: z.literal("literal"),
matches: z.int().nonnegative(),
references: z.int().nonnegative(),
unknowns: z.int().nonnegative(),
truncated: z.boolean(),
});
const readTrace = createAnalysisViewResultReader(literalFeatureTraceSchema);
/** Page retained trace rows without collapsing independently resolvable unknowns. */
export const projectNativeFeatureTraceView = (
parent: AnalysisViewParent,
view: AdmittedView,
) => {
if (view.kind !== "summary" && view.kind !== "native-feature-trace")
return err(
analysisViewInputError([
{
path: ["view"],
reason: "invalid_value",
message:
"Native feature traces support summary and native-feature-trace views.",
},
]),
);
assertOwnedView<
SummaryAnalysisView | z.output<typeof nativeFeatureTraceViewRequestSchema>
>(view, "summary", "native-feature-trace");
const parsed = readTrace(parent);
if (!parsed.ok) return parsed;
const trace = parsed.value;
const shared = {
parent_evidence_id: parent.evidenceId,
parent_operation: "trace_feature" as const,
parent_digest: parent.evidenceId.slice(3),
artifact: parent.artifact ?? null,
source_provider: parent.provider ?? null,
source_analysis_profile_digest: parent.analysisProfile?.digest ?? null,
source_parameters: jsonValueSchema.parse(parent.parameters ?? null),
limitations: boundedViewMessages(
parent.limitations,
parent.evidenceId,
"limitations",
),
unknowns: [
"Page coverage describes retained trace rows, not completeness of provider discovery. See the retained unknowns and source Evidence for limitations.",
],
summary: {
query: trace.query,
search_mode: trace.search_mode,
matches: trace.matches.length,
references: trace.references.length,
unknowns: trace.residual_unknowns.length,
truncated: trace.truncated,
},
};
if (view.kind === "summary")
return ok({
...shared,
kind: "summary" as const,
view,
coverage: completeWithinViewCoverage(
trace.matches.length +
trace.references.length +
trace.residual_unknowns.length,
trace.matches.length +
trace.references.length +
trace.residual_unknowns.length,
),
});
const select = <Value>(
values: readonly Value[],
matches?: (value: Value) => boolean,
) => {
if (matches === undefined) {
const rows = values
.slice(view.offset, view.offset + view.limit)
.map((value, index) => ({ source_index: view.offset + index, value }));
return {
rows,
coverage: pageViewCoverage(view.offset, rows.length, values.length),
};
}
const { rows, total } = selectViewRows(
values,
view,
matches,
(value, source_index) => ({ source_index, value }),
);
return {
rows,
coverage: pageViewCoverage(view.offset, rows.length, total),
};
};
switch (view.collection) {
case "matches": {
const { rows, coverage } = select(
trace.matches,
view.address === undefined
? undefined
: (row) => row.address === view.address,
);
return ok({
...shared,
kind: "native-feature-trace" as const,
view: { ...view, collection: "matches" as const },
items: { collection: "matches" as const, rows },
coverage,
});
}
case "references": {
const { rows, coverage } = select(
trace.references,
view.source_address === undefined && view.target_address === undefined
? undefined
: (row) =>
(view.source_address === undefined ||
row.source_address === view.source_address) &&
(view.target_address === undefined ||
row.target_address === view.target_address),
);
return ok({
...shared,
kind: "native-feature-trace" as const,
view: { ...view, collection: "references" as const },
items: { collection: "references" as const, rows },
coverage,
});
}
case "unknowns": {
const { rows, coverage } = select(trace.residual_unknowns);
return ok({
...shared,
kind: "native-feature-trace" as const,
view: { ...view, collection: "unknowns" as const },
items: { collection: "unknowns" as const, rows },
coverage,
});
}
}
};
+33
View File
@@ -127,6 +127,39 @@ const makeUnknown = (
};
};
it("retains only linked cross-target source closure in a target snapshot", () => {
const leaf = createEvidence(undefined, provider, {
operation: "source_leaf",
parameters: {},
result: "leaf",
});
const otherTarget = { ...ANALYSIS_SNAPSHOT_TARGET, sha256: foreignDigest };
const source = createEvidence(otherTarget, provider, {
operation: "source",
parameters: {},
result: "source",
evidenceLinks: [leaf.evidence_id],
});
const trace = createEvidence(ANALYSIS_SNAPSHOT_TARGET, provider, {
operation: "trace",
parameters: {},
result: "trace",
evidenceLinks: [source.evidence_id, "ev_" + "c".repeat(64)],
});
const unrelated = createEvidence(otherTarget, provider, {
operation: "unrelated",
parameters: {},
result: "unrelated",
});
const selected = evidenceBundleForTarget(
createEvidenceBundle([trace, source, leaf, unrelated]),
targetDigest,
);
expect(selected).toEqual(createEvidenceBundle([trace, source, leaf]));
expect(evidenceBundleForTarget(selected, targetDigest)).toEqual(selected);
expect(parseEvidenceBundle(selected)).toEqual(selected);
});
describe("evidenceBundleForTarget", () => {
it("retains mutation evidence from other targets for complete revision histories", () => {
const first = makeUnknown(
+24 -6
View File
@@ -157,7 +157,7 @@ export const createEvidenceBundle = (
};
};
/** Retain target records and eligible complete unknown histories with their mutation evidence. */
/** Retain target records, their linked sources, and eligible complete unknown histories. */
export const evidenceBundleForTarget = (
bundle: EvidenceBundle,
sha256: string,
@@ -218,12 +218,30 @@ export const evidenceBundleForTarget = (
const mutationIds = new Set(
unknowns.flatMap(({ mutation_evidence_ids: ids }) => ids),
);
const selectedIds = new Set(
bundle.records
.filter(
(record) =>
record.subject?.digest.sha256 === sha256 ||
mutationIds.has(record.evidence_id),
)
.map((record) => record.evidence_id),
);
// Source Evidence may have no subject or describe a different artifact.
// Follow only explicit links from selected records, retaining missing links
// as missing rather than inventing or importing unrelated observations.
const pendingEvidence = [...selectedIds];
for (let index = 0; index < pendingEvidence.length; index += 1) {
const record = recordsById.get(pendingEvidence[index] ?? "");
for (const id of record?.evidence_links ?? []) {
if (!selectedIds.has(id) && recordsById.has(id)) {
selectedIds.add(id);
pendingEvidence.push(id);
}
}
}
return createEvidenceBundle(
bundle.records.filter(
(record) =>
record.subject?.digest.sha256 === sha256 ||
mutationIds.has(record.evidence_id),
),
bundle.records.filter((record) => selectedIds.has(record.evidence_id)),
unknowns,
);
};
+24
View File
@@ -0,0 +1,24 @@
import { z } from "zod";
import { literalFeatureMatchSchema } from "./literalFeatureMatch.js";
import { traceContainingProcedureSchema } from "./nativeObservation.js";
/** One observed reference with an independently resolved or unavailable owner. */
export const literalFeatureReferenceSchema = z.object({
target_address: z.string(),
source_address: z.string(),
containing_procedure: traceContainingProcedureSchema,
});
/** Complete result of a native literal feature trace. */
export const literalFeatureTraceSchema = z.object({
query: z.string(),
search_mode: z.literal("literal"),
matches: z.array(literalFeatureMatchSchema),
references: z.array(literalFeatureReferenceSchema),
truncated: z.boolean(),
residual_unknowns: z.array(z.string()),
});
export type LiteralFeatureReference = z.infer<
typeof literalFeatureReferenceSchema
>;
+2 -1
View File
@@ -207,8 +207,9 @@ export const registerEnhancedTools = (
server.registerTool(
traceFeature.name,
toolRegistrationOptions(traceFeature),
(input, context) =>
({ detail, ...input }, context) =>
executeEnhancedTool(registration, traceFeature, {
detail,
validatedCall: { name: "trace_feature", input },
context,
}),
@@ -0,0 +1,226 @@
import { readFile, stat, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Cli } from "incur";
import pino from "pino";
import { expect, it } from "vitest";
import {
createAnalysisExecution,
type AnalysisProvider,
} from "../../../src/application/AnalysisProvider.js";
import { runDirectAnalysis } from "../../../src/application/DirectAnalysis.js";
import type { DirectAnalysisDependencies } from "../../../src/application/DirectAnalysisDependencies.js";
import { registerCoreBinaryCommands } from "../../../src/cli/coreBinaryCommands.js";
import { parseConfig } from "../../../src/config/parseConfig.js";
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { parseEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
import { ok } from "../../../src/domain/result.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const identity = { id: "fixture", name: "Fixture", version: "1" } as const;
const effects = {
mutatesArtifact: false,
launchesProcess: false,
mayShowUi: false,
mayAccessNetwork: false,
mayWriteFilesystem: false,
changesPermissions: false,
requiresRoot: false,
};
const verifyReplay = async (
cli: ReturnType<typeof Cli.create>,
path: string,
snapshot: string,
root: string,
originalBundle: ReturnType<typeof parseEvidenceBundle>,
traceId: string | undefined,
) => {
const output = join(root, "replay-evidence.json");
let stdout = "";
await cli.serve(
[
"trace",
path,
"goal",
"--provider",
"fixture",
"--snapshot",
snapshot,
"--detail",
"summary",
"--evidence_output",
output,
"--json",
],
{
env: {},
stdout: (text) => {
stdout += text;
},
exit: () => undefined,
},
);
const response = JSON.parse(stdout);
expect(response, JSON.stringify(response)).toHaveProperty("summary");
const bundle = parseEvidenceBundle(
JSON.parse(await readFile(output, "utf8")),
);
expect(response.summary.normalized_result.parent_evidence_id).toBe(traceId);
expect(bundle.records.map((record) => record.evidence_id).sort()).toEqual(
originalBundle.records.map((record) => record.evidence_id).sort(),
);
};
it.each([
{ detail: "summary", missing: false },
{ detail: "complete", missing: false },
{ detail: "summary", missing: true },
{ detail: "summary", missing: false, replay: true },
])(
"publishes $detail trace only when its source closure is available (missing=$missing, replay=$replay)",
async ({ detail, missing, replay }) => {
const root = await createTestTempDirectory("rea-native-trace-cli-source-");
const path = join(root, "fixture.hop");
const output = join(root, "evidence.json");
const snapshot = join(root, "snapshot.json");
let allowProvider = true;
await writeFile(path, "synthetic analysis database");
const history = createEvidence(undefined, identity, {
operation: "health",
parameters: { facet: "historical-project-analysis-reports" },
result: { origin: "caller-project" },
confidence: "observed",
...(missing ? { evidenceLinks: ["ev_" + "a".repeat(64)] } : {}),
});
const provider: AnalysisProvider = {
identity: () => identity,
capabilities: () =>
(
[
"search_strings",
"search_procedures",
"xrefs",
"resolve_containing_procedure",
] as const
).map((operation) => ({
provider: identity,
operation,
available: true,
reason: null,
cachePolicy: replay ? "snapshot" : "live",
limitations: [],
effects,
})),
resolveAnalysisProfile: async () =>
ok({ profile: createAnalysisProfile(identity, { fixture: true }) }),
createClient: () => {
if (!allowProvider)
throw new Error("Snapshot replay reopened the provider");
return {
execute: async (operation) => {
const value =
operation === "search_procedures"
? [{ address: "0x1000", value: "goal" }]
: operation === "xrefs"
? ["0x1010"]
: operation === "resolve_containing_procedure"
? {
query_address: "0x1010",
found: false,
procedure: null,
reason: "not_in_procedure",
}
: operation === "search_strings"
? []
: null;
return ok(
createAnalysisExecution(value, identity, {
sourceEvidence: [history],
}),
);
},
close: async () => ok(null),
};
},
};
const createSession = () => createTestBinarySession(provider);
const dependencies: DirectAnalysisDependencies = {
readConfiguration: () => parseConfig({}),
createBinarySession: createSession,
createNativeDispatchBinarySession: createSession,
createArtifactBinarySession: createSession,
createNativeBinarySession: createSession,
createManagedBinarySession: createSession,
};
const cli = Cli.create("rea-native-trace-cli-source", { sync: false });
registerCoreBinaryCommands(
cli,
pino({ enabled: false }),
(target, tool, args, options) =>
runDirectAnalysis(dependencies, target, tool, args, options),
{},
);
let stdout = "";
await cli.serve(
[
"trace",
path,
"goal",
"--provider",
"fixture",
...(replay ? ["--snapshot", snapshot] : []),
"--detail",
detail,
"--evidence_output",
output,
"--json",
],
{
env: {},
stdout: (text) => {
stdout += text;
},
exit: () => undefined,
},
);
const response = JSON.parse(stdout);
if (missing) {
expect(response).toMatchObject({
code: "evidence_integrity_mismatch",
details: { reason: "missing", evidence_id: "ev_" + "a".repeat(64) },
});
await expect(stat(output)).rejects.toMatchObject({ code: "ENOENT" });
} else {
const bundle = parseEvidenceBundle(
JSON.parse(await readFile(output, "utf8")),
);
const trace = bundle.records.find(
(record) => record.operation === "trace_feature",
);
expect(
detail === "summary"
? response.summary.normalized_result.parent_evidence_id
: response.evidence_id,
).toBe(trace?.evidence_id);
expect(trace?.evidence_links).toContain(history.evidence_id);
expect(
bundle.records.some(
(record) => record.evidence_id === history.evidence_id,
),
).toBe(true);
if (replay) {
allowProvider = false;
await verifyReplay(
cli,
path,
snapshot,
root,
bundle,
trace?.evidence_id,
);
}
}
},
);
@@ -0,0 +1,167 @@
import { join } from "node:path";
import { expect, it } from "vitest";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js";
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const traceFixture = () => ({
query: "goal",
search_mode: "literal" as const,
matches: [{ type: "procedure" as const, address: "0x1000", value: "goal" }],
references: ["0x1010", "0x1020", "0x1010"].map((source_address) => ({
target_address: "0x1000",
source_address,
containing_procedure: {
query_address: source_address,
status: "unavailable" as const,
provider_id: "kuna",
reason: "unsupported",
},
})),
truncated: true,
residual_unknowns: ["0x1010 owner unknown", "0x1020 owner unknown"],
});
it.each(["full", "compact"] as const)(
"pages retained native trace rows and preserves source coverage across %s offline handoff",
async (profile) => {
const result = traceFixture();
const parent = createEvidence(
undefined,
{ id: "rea-workflow", name: "REA workflow", version: "1" },
{
operation: "trace_feature",
parameters: { query: "goal", case_sensitive: false },
result,
confidence: "derived",
limitations: ["fixture provider coverage"],
},
);
const setup = async () => {
const session = createTestBinarySession(() => {
throw new Error("View must not open a provider");
});
const harness = await createApplicationMcpHarness({
session,
clientIdentity: { name: "native-feature-view", version: "1" },
serverOptions: {
environment: {
...process.env,
REA_MCP_INPUT_SCHEMA_PROFILE: profile,
REA_MCP_OUTPUT_SCHEMA_PROFILE: profile,
},
},
});
return { ...harness, session };
};
const first = await setup();
const call = async (
client: typeof first.client,
name: string,
args: Record<string, unknown>,
) => {
const reply = await client.callTool({ name, arguments: args });
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
return reply.structuredContent;
};
const source = {
kind: "retained-evidence",
evidence_id: parent.evidence_id,
};
const view = async (
client: typeof first.client,
collection: "matches" | "references" | "unknowns",
offset: number,
limit: number,
) => {
const response = parseEvidence(
await call(client, "inspect_analysis_view", {
source,
view: { kind: "native-feature-trace", collection, offset, limit },
}),
);
return analysisViewResultSchema.parse(response.normalized_result);
};
const root = await createTestTempDirectory(
"rea-native-feature-trace-view-",
);
const path = join(root, "evidence.json");
try {
expect(first.session.recordEvidence(parent).ok).toBe(true);
const summary = analysisViewResultSchema.parse(
parseEvidence(
await call(first.client, "inspect_analysis_view", {
source,
view: { kind: "summary" },
}),
).normalized_result,
);
expect(summary).toMatchObject({
parent_evidence_id: parent.evidence_id,
summary: { matches: 1, references: 3, unknowns: 2, truncated: true },
});
const firstPage = await view(first.client, "references", 0, 2);
const lastPage = await view(first.client, "references", 2, 2);
expect(
analysisViewResultSchema.safeParse({
...firstPage,
items: { collection: "unknowns", rows: [] },
}).success,
).toBe(false);
expect(firstPage).toMatchObject({
coverage: { total: 3, next_offset: 2, exhausted: false },
items: {
rows: [
{ source_index: 0, value: result.references[0] },
{ source_index: 1, value: result.references[1] },
],
},
});
expect(lastPage).toMatchObject({
coverage: { total: 3, next_offset: null, exhausted: true },
items: { rows: [{ source_index: 2, value: result.references[2] }] },
});
const filtered = analysisViewResultSchema.parse(
parseEvidence(
await call(first.client, "inspect_analysis_view", {
source,
view: {
kind: "native-feature-trace",
collection: "references",
source_address: "0x1010",
offset: 1,
limit: 1,
},
}),
).normalized_result,
);
expect(filtered).toMatchObject({
coverage: { total: 2, next_offset: null, exhausted: true },
items: { rows: [{ source_index: 2, value: result.references[2] }] },
});
expect(await view(first.client, "unknowns", 1, 5)).toMatchObject({
items: {
rows: [{ source_index: 1, value: result.residual_unknowns[1] }],
},
});
expect(await view(first.client, "matches", 9, 1)).toMatchObject({
coverage: { status: "empty", total: 1, next_offset: null },
});
await call(first.client, "export_evidence_bundle", { path });
} finally {
await first.close();
}
const second = await setup();
try {
await call(second.client, "import_evidence_bundle", { path });
expect(await view(second.client, "references", 2, 1)).toMatchObject({
parent_evidence_id: parent.evidence_id,
items: { rows: [{ source_index: 2, value: result.references[2] }] },
});
} finally {
await second.close();
}
},
);