mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd4130d654 | ||
|
|
0a9da652a8 | ||
|
|
c5f935031b |
No files matched your search
@@ -218,6 +218,23 @@ Malformed provider replies return `unreadable_output` for
|
||||
`resolve_containing_procedure` before successful-result projection;
|
||||
cancellation and incomplete cleanup remain errors.
|
||||
|
||||
A large trace can be read on demand. `detail: "summary"` retains the complete
|
||||
trace in the session and returns its `query`, `truncated`, match counts by type,
|
||||
reference counts by owner outcome (`found`, `not-found`, `unavailable`) and the
|
||||
residual-unknown count. `inspect_analysis_view` with the returned
|
||||
`parent_evidence_id` and `{"kind":"native-trace","collection":...}` pages
|
||||
`references`, `matches` or `residual_unknowns` in retained order. Each row keeps
|
||||
its `source_index`, and reference rows also carry their derived `owner`.
|
||||
References filter by exact `where.target_addresses` (no address normalization)
|
||||
and `where.owners`; matches filter by `where.types`. Omitted filters select all;
|
||||
empty arrays select none. Page coverage counts the retained provider-reported
|
||||
rows only: a missing row never establishes an absent reference, and `truncated`
|
||||
stays visible on every page. Residual unknowns are paged, not repeated in each
|
||||
view, and their session Unknowns are recorded exactly as for complete
|
||||
delivery. An oversized complete trace offers a ready-to-call first `references`
|
||||
page. The CLI keeps complete JSON; save it and use `inspect-analysis-view` with
|
||||
an `evidence-file` source.
|
||||
|
||||
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
|
||||
|
||||
@@ -80,8 +80,10 @@ 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. Select detail summary for match and reference counts by owner outcome; native-trace pages of references, matches and residual unknowns retain complete source access.",
|
||||
enhancedInputSchemas.trace_feature.extend({
|
||||
detail: analysisDetailSchema,
|
||||
}),
|
||||
),
|
||||
enhanced(
|
||||
"trace_call_path",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
nativeAddressListSchema,
|
||||
nativeObservationSourceSchemas,
|
||||
traceContainingProcedureSchema,
|
||||
} from "../domain/native/nativeObservation.js";
|
||||
import { overviewInventoryCoverageSchema } from "../domain/native/inventoryCoverage.js";
|
||||
import {
|
||||
@@ -16,8 +15,9 @@ import {
|
||||
machoSummaryViewResultSchema,
|
||||
managedArtifactSummaryViewResultSchema,
|
||||
managedMemberSummaryViewResultSchema,
|
||||
nativeTraceSummaryViewResultSchema,
|
||||
} from "../domain/analysisView/analysisView.js";
|
||||
import { literalFeatureMatchSchema } from "../domain/native/literalFeatureMatch.js";
|
||||
import { literalFeatureTraceResultSchema } 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";
|
||||
@@ -252,21 +252,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(
|
||||
literalFeatureTraceResultSchema,
|
||||
nativeTraceSummaryViewResultSchema,
|
||||
);
|
||||
|
||||
const callPathTraceOutput = evidenceResultOf(
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { projectJavaScriptRoutesView } from "./javascriptRoutesView.js";
|
||||
import { projectWebBundleView } from "./webBundleView.js";
|
||||
import { projectSourceToBundleView } from "./sourceToBundleView.js";
|
||||
import { projectNativeTraceView } from "./nativeTraceView.js";
|
||||
import { projectGpuCodeObjectsView } from "./gpuCodeObjectsView.js";
|
||||
import { projectModelArtifactView } from "./modelArtifactView.js";
|
||||
import { projectCommandObservationView } from "./commandObservationView.js";
|
||||
@@ -147,6 +148,7 @@ import {
|
||||
nativeFunctionSummaryViewResultSchema,
|
||||
sourceToBundleSummaryViewResultSchema,
|
||||
sourceToBundlePageViewResultSchema,
|
||||
nativeTraceSummaryViewResultSchema,
|
||||
analysisViewResultSchema,
|
||||
type AnalysisViewResult,
|
||||
type UnsignedAnalysisView,
|
||||
@@ -211,6 +213,7 @@ export {
|
||||
nativeFunctionSummaryViewResultSchema,
|
||||
sourceToBundleSummaryViewResultSchema,
|
||||
sourceToBundlePageViewResultSchema,
|
||||
nativeTraceSummaryViewResultSchema,
|
||||
analysisViewResultSchema,
|
||||
type AnalysisViewResult,
|
||||
type UnsignedAnalysisView,
|
||||
@@ -327,6 +330,7 @@ const retainedViewProjectors = new Map<string, RetainedViewProjector>([
|
||||
retain("inspect_gpu_code_objects", projectGpuCodeObjectsView),
|
||||
retain("match_native_functions", projectNativeFunctionMatchingView),
|
||||
retain("compare_source_to_bundle", projectSourceToBundleView),
|
||||
retain("trace_feature", projectNativeTraceView),
|
||||
retain("inspect_artifact", projectArtifactInventoryView),
|
||||
retain("inventory_artifact", projectArtifactInventoryView),
|
||||
retain("analyze_web_bundle", projectWebBundleView),
|
||||
@@ -426,6 +430,7 @@ export const projectAnalysisView = (
|
||||
parent.operation === "match_native_functions"
|
||||
? NATIVE_FUNCTION_MATCHING_PREDICATE
|
||||
: parent.operation === "analyze_function" ||
|
||||
parent.operation === "trace_feature" ||
|
||||
isNativeObservationOperation(parent.operation) ||
|
||||
isNativeInventoryOperation(parent.operation)
|
||||
? ANALYSIS_EVIDENCE_PREDICATE
|
||||
|
||||
@@ -10,6 +10,7 @@ import { nativeExportsViewRequestSchema } from "./nativeExportsView.js";
|
||||
import { javaScriptRoutesViewRequestSchema } from "./javascriptRoutesView.js";
|
||||
import { webBundleViewRequestSchema } from "./webBundleView.js";
|
||||
import { sourceToBundleViewRequestSchema } from "./sourceToBundleView.js";
|
||||
import { nativeTraceViewRequestSchema } from "./nativeTraceView.js";
|
||||
import { gpuCodeObjectsViewRequestSchema } from "./gpuCodeObjectsView.js";
|
||||
import { modelArtifactViewRequestSchema } from "../model/modelArtifactQuery.js";
|
||||
import { rustBinaryViewRequestSchema } from "../rust/rustBinaryQuery.js";
|
||||
@@ -66,6 +67,7 @@ export const analysisViewRequestSchema = z.discriminatedUnion("kind", [
|
||||
webBundleViewRequestSchema,
|
||||
nativeFunctionMatchingViewRequestSchema,
|
||||
sourceToBundleViewRequestSchema,
|
||||
nativeTraceViewRequestSchema,
|
||||
summaryViewSchema,
|
||||
goBinaryViewRequestSchema,
|
||||
rustBinaryViewRequestSchema,
|
||||
|
||||
@@ -63,6 +63,11 @@ import {
|
||||
sourceToBundleViewItemsSchema,
|
||||
sourceToBundleViewContextSchema,
|
||||
} from "./sourceToBundleView.js";
|
||||
import {
|
||||
nativeTraceSummarySchema,
|
||||
nativeTraceViewCollections,
|
||||
nativeTraceViewContextSchema,
|
||||
} from "./nativeTraceView.js";
|
||||
import {
|
||||
gpuCodeObjectsViewRequestSchema,
|
||||
gpuCodeObjectsSummarySchema,
|
||||
@@ -946,6 +951,42 @@ export const sourceToBundlePageViewResultSchema = z.strictObject({
|
||||
items: sourceToBundleViewItemsSchema,
|
||||
});
|
||||
|
||||
/** Compact retained native feature-trace summary; pages carry the same trace identity. */
|
||||
export const nativeTraceSummaryViewResultSchema = z.strictObject({
|
||||
...summaryViewResultBase,
|
||||
...nativeViewSourceContext,
|
||||
artifact: artifactSchema.nullable(),
|
||||
parent_operation: z.literal("trace_feature"),
|
||||
summary: nativeTraceSummarySchema,
|
||||
});
|
||||
const nativeTracePage = <
|
||||
Collection extends keyof typeof nativeTraceViewCollections,
|
||||
>(
|
||||
collection: Collection,
|
||||
) =>
|
||||
z.strictObject({
|
||||
...viewResultBase,
|
||||
...nativeViewSourceContext,
|
||||
...nativeTraceViewContextSchema.shape,
|
||||
kind: z.literal("native-trace"),
|
||||
artifact: artifactSchema.nullable(),
|
||||
parent_operation: z.literal("trace_feature"),
|
||||
view: nativeTraceViewCollections[collection].request,
|
||||
items: nativeTraceViewCollections[collection].rows,
|
||||
});
|
||||
// As with Mach-O pages, the kind tag keeps dispatch while each collection is
|
||||
// advertised with exactly its own rows.
|
||||
const nativeTraceViewResultSchema = z
|
||||
.object({ kind: z.literal("native-trace") })
|
||||
.passthrough()
|
||||
.pipe(
|
||||
z.union([
|
||||
nativeTracePage("references"),
|
||||
nativeTracePage("matches"),
|
||||
nativeTracePage("residual_unknowns"),
|
||||
]),
|
||||
);
|
||||
|
||||
/** Bind the existing nested facet selection to its producer-owned rows. */
|
||||
const machoPage = <Facet extends keyof typeof machoViewRows>(facet: Facet) =>
|
||||
z.strictObject({
|
||||
@@ -984,6 +1025,7 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
|
||||
javaScriptRoutesViewResultSchema,
|
||||
webBundlePageViewResultSchema,
|
||||
sourceToBundlePageViewResultSchema,
|
||||
nativeTraceViewResultSchema,
|
||||
z.strictObject({
|
||||
...viewResultBase,
|
||||
...nativeFunctionMatchingContextSchema.shape,
|
||||
@@ -1073,6 +1115,7 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
|
||||
nativeFunctionMatchingSummaryViewResultSchema,
|
||||
nativeFunctionSummaryViewResultSchema,
|
||||
sourceToBundleSummaryViewResultSchema,
|
||||
nativeTraceSummaryViewResultSchema,
|
||||
webBundleSummaryViewResultSchema,
|
||||
]),
|
||||
z.strictObject({
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
import { z } from "zod";
|
||||
import { jsonValueSchema } from "../jsonValue.js";
|
||||
import { literalFeatureMatchSchema } from "../native/literalFeatureMatch.js";
|
||||
import {
|
||||
literalFeatureReferenceSchema,
|
||||
literalFeatureTraceResultSchema,
|
||||
literalReferenceOwner,
|
||||
} from "../native/literalFeatureTrace.js";
|
||||
import { err, ok } from "../result.js";
|
||||
import {
|
||||
boundedViewMessages,
|
||||
completeWithinViewCoverage,
|
||||
pageViewCoverage,
|
||||
} from "./analysisViewCoverage.js";
|
||||
import { analysisViewInputError } from "./analysisViewErrors.js";
|
||||
import {
|
||||
createAnalysisViewResultReader,
|
||||
type AnalysisViewParent,
|
||||
} from "./analysisViewParent.js";
|
||||
import {
|
||||
ownedView,
|
||||
type AdmittedView,
|
||||
type SummaryAnalysisView,
|
||||
} from "./analysisViewShape.js";
|
||||
import { selectViewRows } from "./selectViewRows.js";
|
||||
|
||||
const OPERATION = "trace_feature" as const;
|
||||
|
||||
/** Every view states that retained rows are not complete discovery. */
|
||||
export const NATIVE_TRACE_COVERAGE_UNKNOWN =
|
||||
"Counts and pages describe the provider-reported matches and references retained by this trace, not complete discovery. A missing row does not establish an absent reference.";
|
||||
/** Every view of a capped trace carries the provider's reference cap. */
|
||||
export const NATIVE_TRACE_CAP_UNKNOWN =
|
||||
"truncated is true: the provider reported a reference cap, so references beyond it were not observed.";
|
||||
const ownerSchema = z.enum(["found", "not-found", "unavailable"]);
|
||||
const page = {
|
||||
kind: z.literal("native-trace"),
|
||||
offset: z.int().nonnegative().default(0),
|
||||
limit: z.int().positive().default(64),
|
||||
};
|
||||
|
||||
const referencesRequestSchema = z.strictObject({
|
||||
...page,
|
||||
collection: z.literal("references"),
|
||||
where: z
|
||||
.strictObject({
|
||||
target_addresses: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
"Exact match addresses as recorded; no address normalization. Omitted selects all; [] selects none.",
|
||||
),
|
||||
owners: z
|
||||
.array(ownerSchema)
|
||||
.optional()
|
||||
.describe(
|
||||
"Containing-procedure outcome: found, not-found (outside segments or no procedure) or unavailable (the provider could not resolve ownership).",
|
||||
),
|
||||
})
|
||||
.default({}),
|
||||
});
|
||||
const matchesRequestSchema = z.strictObject({
|
||||
...page,
|
||||
collection: z.literal("matches"),
|
||||
where: z
|
||||
.strictObject({
|
||||
types: z.array(z.enum(["string", "procedure"])).optional(),
|
||||
})
|
||||
.default({}),
|
||||
});
|
||||
const unknownsRequestSchema = z.strictObject({
|
||||
...page,
|
||||
collection: z.literal("residual_unknowns"),
|
||||
});
|
||||
|
||||
/** Page one retained collection of a completed literal feature trace. */
|
||||
export const nativeTraceViewRequestSchema = z.discriminatedUnion("collection", [
|
||||
referencesRequestSchema,
|
||||
matchesRequestSchema,
|
||||
unknownsRequestSchema,
|
||||
]);
|
||||
type NativeTraceViewRequest = z.output<typeof nativeTraceViewRequestSchema>;
|
||||
|
||||
const sourceIndex = z.int().nonnegative();
|
||||
/**
|
||||
* Each collection's request bound to its own rows, so a reply cannot pair one
|
||||
* collection with another's rows. source_index addresses the complete retained
|
||||
* array, so duplicates stay distinct.
|
||||
*/
|
||||
export const nativeTraceViewCollections = {
|
||||
references: {
|
||||
request: referencesRequestSchema,
|
||||
rows: z.array(
|
||||
z.strictObject({
|
||||
source_index: sourceIndex,
|
||||
owner: ownerSchema,
|
||||
reference: literalFeatureReferenceSchema,
|
||||
}),
|
||||
),
|
||||
},
|
||||
matches: {
|
||||
request: matchesRequestSchema,
|
||||
rows: z.array(
|
||||
z.strictObject({
|
||||
source_index: sourceIndex,
|
||||
match: literalFeatureMatchSchema,
|
||||
}),
|
||||
),
|
||||
},
|
||||
residual_unknowns: {
|
||||
request: unknownsRequestSchema,
|
||||
rows: z.array(
|
||||
z.strictObject({ source_index: sourceIndex, unknown: z.string() }),
|
||||
),
|
||||
},
|
||||
} as const;
|
||||
|
||||
const counts = z.int().nonnegative();
|
||||
/** Trace identity, provider outcome and counts repeated with every page. */
|
||||
export const nativeTraceViewContextSchema = z.strictObject({
|
||||
query: z.string(),
|
||||
search_mode: z.literal("literal"),
|
||||
truncated: z
|
||||
.boolean()
|
||||
.describe("The provider reported a reference cap for at least one match."),
|
||||
});
|
||||
export const nativeTraceSummarySchema = nativeTraceViewContextSchema.extend({
|
||||
matches: z.strictObject({
|
||||
total: counts,
|
||||
string: counts,
|
||||
procedure: counts,
|
||||
}),
|
||||
references: z.strictObject({
|
||||
total: counts,
|
||||
found: counts,
|
||||
"not-found": counts,
|
||||
unavailable: counts,
|
||||
}),
|
||||
residual_unknowns: counts,
|
||||
});
|
||||
|
||||
const readTrace = createAnalysisViewResultReader(
|
||||
literalFeatureTraceResultSchema,
|
||||
);
|
||||
const selected = <Value>(values: readonly Value[] | undefined) =>
|
||||
values === undefined ? undefined : new Set(values);
|
||||
|
||||
/** Select retained trace rows without another provider read or reinterpretation. */
|
||||
export const projectNativeTraceView = (
|
||||
parent: AnalysisViewParent,
|
||||
admitted: AdmittedView,
|
||||
) => {
|
||||
const view = ownedView<SummaryAnalysisView | NativeTraceViewRequest>(
|
||||
admitted,
|
||||
"summary",
|
||||
"native-trace",
|
||||
);
|
||||
if (view === undefined)
|
||||
return err(
|
||||
analysisViewInputError([
|
||||
{
|
||||
path: ["view"],
|
||||
reason: "invalid_value",
|
||||
message:
|
||||
"Native feature traces support summary and native-trace pages of references, matches or residual_unknowns.",
|
||||
},
|
||||
]),
|
||||
);
|
||||
const parsed = readTrace(parent);
|
||||
if (!parsed.ok) return parsed;
|
||||
const trace = parsed.value;
|
||||
const shared = {
|
||||
parent_evidence_id: parent.evidenceId,
|
||||
parent_operation: OPERATION,
|
||||
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",
|
||||
),
|
||||
// Residual unknowns can number one per reference; page them instead of
|
||||
// repeating every text in each view.
|
||||
unknowns: [
|
||||
NATIVE_TRACE_COVERAGE_UNKNOWN,
|
||||
...(trace.truncated ? [NATIVE_TRACE_CAP_UNKNOWN] : []),
|
||||
...(trace.residual_unknowns.length > 0
|
||||
? [
|
||||
`${String(trace.residual_unknowns.length)} residual unknowns remain in the parent trace; page them with collection residual_unknowns.`,
|
||||
]
|
||||
: []),
|
||||
],
|
||||
};
|
||||
const context = {
|
||||
query: trace.query,
|
||||
search_mode: trace.search_mode,
|
||||
truncated: trace.truncated,
|
||||
};
|
||||
if (view.kind === "summary") {
|
||||
const owners = { found: 0, "not-found": 0, unavailable: 0 };
|
||||
for (const reference of trace.references)
|
||||
owners[literalReferenceOwner(reference)] += 1;
|
||||
const strings = trace.matches.filter(({ type }) => type === "string");
|
||||
return ok({
|
||||
...shared,
|
||||
kind: "summary" as const,
|
||||
view,
|
||||
summary: {
|
||||
...context,
|
||||
matches: {
|
||||
total: trace.matches.length,
|
||||
string: strings.length,
|
||||
procedure: trace.matches.length - strings.length,
|
||||
},
|
||||
references: { total: trace.references.length, ...owners },
|
||||
residual_unknowns: trace.residual_unknowns.length,
|
||||
},
|
||||
coverage: completeWithinViewCoverage(1, 1),
|
||||
});
|
||||
}
|
||||
const pageOf = <View extends NativeTraceViewRequest, Row>(
|
||||
selection: View,
|
||||
rows: { readonly rows: Row[]; readonly total: number },
|
||||
) => ({
|
||||
...shared,
|
||||
...context,
|
||||
kind: "native-trace" as const,
|
||||
view: selection,
|
||||
items: rows.rows,
|
||||
coverage: pageViewCoverage(selection.offset, rows.rows.length, rows.total),
|
||||
});
|
||||
if (view.collection === "residual_unknowns")
|
||||
return ok(
|
||||
pageOf(
|
||||
view,
|
||||
selectViewRows(
|
||||
trace.residual_unknowns,
|
||||
view,
|
||||
() => true,
|
||||
(unknown, source_index) => ({ source_index, unknown }),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (view.collection === "matches") {
|
||||
const types = selected(view.where.types);
|
||||
return ok(
|
||||
pageOf(
|
||||
view,
|
||||
selectViewRows(
|
||||
trace.matches,
|
||||
view,
|
||||
(match) => types === undefined || types.has(match.type),
|
||||
(match, source_index) => ({ source_index, match }),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
const targets = selected(view.where.target_addresses);
|
||||
const owners = selected(view.where.owners);
|
||||
return ok(
|
||||
pageOf(
|
||||
view,
|
||||
selectViewRows(
|
||||
trace.references,
|
||||
view,
|
||||
(reference) =>
|
||||
(targets === undefined || targets.has(reference.target_address)) &&
|
||||
(owners === undefined ||
|
||||
owners.has(literalReferenceOwner(reference))),
|
||||
(reference, source_index) => ({
|
||||
source_index,
|
||||
owner: literalReferenceOwner(reference),
|
||||
reference,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { z } from "zod";
|
||||
import { literalFeatureMatchSchema } from "./literalFeatureMatch.js";
|
||||
import { traceContainingProcedureSchema } from "./nativeObservation.js";
|
||||
|
||||
/** One provider-reported reference to a literal match and its owner outcome. */
|
||||
export const literalFeatureReferenceSchema = z.object({
|
||||
target_address: z.string(),
|
||||
source_address: z.string(),
|
||||
containing_procedure: traceContainingProcedureSchema,
|
||||
});
|
||||
export type LiteralFeatureReference = z.infer<
|
||||
typeof literalFeatureReferenceSchema
|
||||
>;
|
||||
|
||||
/** Complete trace_feature result, owned once for delivery and retained views. */
|
||||
export const literalFeatureTraceResultSchema = 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 LiteralFeatureTraceResult = z.infer<
|
||||
typeof literalFeatureTraceResultSchema
|
||||
>;
|
||||
|
||||
/** Owner outcome of one reference, without reinterpreting its provider facts. */
|
||||
export const literalReferenceOwner = (
|
||||
reference: LiteralFeatureReference,
|
||||
): "found" | "not-found" | "unavailable" => {
|
||||
const owner = reference.containing_procedure;
|
||||
if ("status" in owner) return "unavailable";
|
||||
return owner.found ? "found" : "not-found";
|
||||
};
|
||||
@@ -204,10 +204,11 @@ export const registerEnhancedTools = (
|
||||
server.registerTool(
|
||||
traceFeature.name,
|
||||
toolRegistrationOptions(traceFeature),
|
||||
(input, context) =>
|
||||
({ detail, ...input }, context) =>
|
||||
executeEnhancedTool(registration, traceFeature, {
|
||||
validatedCall: { name: "trace_feature", input },
|
||||
context,
|
||||
detail,
|
||||
}),
|
||||
);
|
||||
server.registerTool(
|
||||
|
||||
@@ -16,18 +16,25 @@ export const createToolResultDelivery = (
|
||||
retainedEvidenceContinuation,
|
||||
);
|
||||
|
||||
/** Offer a named, schema-validated source page only for retained native analysis. */
|
||||
/** Offer a named, schema-validated first page for retained analysis whose owner supports paging. */
|
||||
const retainedEvidenceContinuation = (
|
||||
evidence: Evidence,
|
||||
): Readonly<Record<string, JsonValue>> => {
|
||||
const view =
|
||||
evidence.operation === "analyze_function"
|
||||
? { kind: "native", facet: "pseudocode", offset: 0 }
|
||||
: isNativeInventoryOperation(evidence.operation)
|
||||
? { kind: "native-inventory", offset: 0, limit: 64 }
|
||||
: isNativeObservationOperation(evidence.operation)
|
||||
? { kind: "native-observation", offset: 0, limit: 64 }
|
||||
: undefined;
|
||||
: evidence.operation === "trace_feature"
|
||||
? {
|
||||
kind: "native-trace",
|
||||
collection: "references",
|
||||
offset: 0,
|
||||
limit: 64,
|
||||
}
|
||||
: isNativeInventoryOperation(evidence.operation)
|
||||
? { kind: "native-inventory", offset: 0, limit: 64 }
|
||||
: isNativeObservationOperation(evidence.operation)
|
||||
? { kind: "native-observation", offset: 0, limit: 64 }
|
||||
: undefined;
|
||||
if (view === undefined) return {};
|
||||
const input = inspectAnalysisViewInputSchema.safeParse({
|
||||
source: { kind: "retained-evidence", evidence_id: evidence.evidence_id },
|
||||
|
||||
@@ -1421,3 +1421,85 @@ cliTest.for(
|
||||
expect(proof.listeners).toBeGreaterThan(0);
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"pages a saved native feature trace through the shared view owner",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-native-trace-cli-");
|
||||
const unavailable = {
|
||||
query_address: "0x21",
|
||||
status: "unavailable" as const,
|
||||
provider_id: "ghidra",
|
||||
reason: "not supported",
|
||||
};
|
||||
const parent = createEvidence(
|
||||
{ path: "/fixtures/native.exe", format: "pe", sha256: "a".repeat(64) },
|
||||
{ id: "ghidra", name: "Ghidra", version: "12.1.4" },
|
||||
{
|
||||
operation: "trace_feature",
|
||||
parameters: { query: "license", case_sensitive: false },
|
||||
result: {
|
||||
query: "license",
|
||||
search_mode: "literal",
|
||||
matches: [{ type: "string", address: "0x10", value: "license" }],
|
||||
references: [
|
||||
{
|
||||
target_address: "0x10",
|
||||
source_address: "0x20",
|
||||
containing_procedure: {
|
||||
query_address: "0x20",
|
||||
found: false,
|
||||
procedure: null,
|
||||
reason: "not_in_procedure",
|
||||
},
|
||||
},
|
||||
{
|
||||
target_address: "0x10",
|
||||
source_address: "0x21",
|
||||
containing_procedure: unavailable,
|
||||
},
|
||||
],
|
||||
truncated: false,
|
||||
residual_unknowns: ["Which procedure contains 0x21?"],
|
||||
},
|
||||
},
|
||||
);
|
||||
const path = join(root, "trace.json");
|
||||
await writeFile(path, JSON.stringify(parent));
|
||||
const inspect = async (view: Record<string, unknown>) => {
|
||||
const response = await cli.run({
|
||||
arguments: [
|
||||
"inspect-analysis-view",
|
||||
JSON.stringify({ source: { kind: "evidence-file", path }, view }),
|
||||
"--json",
|
||||
],
|
||||
});
|
||||
expect(response.exitCode, response.stderr).toBe(0);
|
||||
return parseEvidence(response.json).normalized_result;
|
||||
};
|
||||
expect(await inspect({ kind: "summary" })).toMatchObject({
|
||||
parent_evidence_id: parent.evidence_id,
|
||||
parent_operation: "trace_feature",
|
||||
summary: {
|
||||
references: { total: 2, "not-found": 1, unavailable: 1, found: 0 },
|
||||
residual_unknowns: 1,
|
||||
},
|
||||
});
|
||||
expect(
|
||||
await inspect({
|
||||
kind: "native-trace",
|
||||
collection: "references",
|
||||
where: { owners: ["unavailable"] },
|
||||
}),
|
||||
).toMatchObject({
|
||||
items: [
|
||||
{
|
||||
source_index: 1,
|
||||
owner: "unavailable",
|
||||
reference: { containing_procedure: unavailable },
|
||||
},
|
||||
],
|
||||
coverage: { total: 1, exhausted: true },
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,476 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { afterAll, expect, it, onTestFinished } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
|
||||
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
||||
import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { jsonObjectSchema } from "../../../src/domain/jsonValue.js";
|
||||
import {
|
||||
literalFeatureTraceResultSchema,
|
||||
literalReferenceOwner,
|
||||
type LiteralFeatureTraceResult,
|
||||
} from "../../../src/domain/native/literalFeatureTrace.js";
|
||||
import { err, ok } from "../../../src/domain/result.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { createToolResultDelivery } from "../../../src/server/toolResult.js";
|
||||
import {
|
||||
NATIVE_TRACE_CAP_UNKNOWN,
|
||||
NATIVE_TRACE_COVERAGE_UNKNOWN,
|
||||
} from "../../../src/domain/analysisView/nativeTraceView.js";
|
||||
import { ghidraFunctionIdentity } from "../../../src/domain/ghidraValues.fixture.js";
|
||||
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const schemaValidation = createMcpSchemaValidation();
|
||||
afterAll(() => schemaValidation.clear());
|
||||
|
||||
const PROVIDER = { id: "fixture", name: "Fixture", version: "1" } as const;
|
||||
const STRINGS = ["0x10", "0x11", "0x12"];
|
||||
/** Each match is referenced from several sources with mixed owner outcomes. */
|
||||
const SOURCES: Readonly<Record<string, readonly string[]>> = {
|
||||
"0x10": ["0x20", "0x21", "0x22"],
|
||||
"0x11": ["0x23"],
|
||||
"0x12": ["0x24", "0x25"],
|
||||
"0x30": ["0x26"],
|
||||
};
|
||||
const UNAVAILABLE = new Set(["0x21", "0x24"]);
|
||||
const NOT_IN_PROCEDURE = new Set(["0x22"]);
|
||||
|
||||
const connect = async (
|
||||
options: {
|
||||
/** The provider reports a reference cap for the first string match. */
|
||||
readonly capped?: boolean;
|
||||
readonly profile?: "full" | "compact";
|
||||
readonly budget?: number;
|
||||
} = {},
|
||||
) => {
|
||||
const session = createTestBinarySession(() => ({
|
||||
execute: async (operation, input) => {
|
||||
const address = String(input.address);
|
||||
switch (operation) {
|
||||
case "search_strings":
|
||||
return ok(
|
||||
createAnalysisExecution(
|
||||
STRINGS.map((value) => ({ address: value, value: "license" })),
|
||||
PROVIDER,
|
||||
),
|
||||
);
|
||||
case "search_procedures":
|
||||
return ok(
|
||||
createAnalysisExecution(
|
||||
[{ address: "0x30", value: "check_license" }],
|
||||
PROVIDER,
|
||||
),
|
||||
);
|
||||
case "xrefs":
|
||||
return ok(
|
||||
createAnalysisExecution([...(SOURCES[address] ?? [])], PROVIDER, {
|
||||
...(options.capped === true && address === "0x10"
|
||||
? { referencesTruncated: true }
|
||||
: {}),
|
||||
}),
|
||||
);
|
||||
case "resolve_containing_procedure":
|
||||
if (UNAVAILABLE.has(address))
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"fixture",
|
||||
operation,
|
||||
"not supported",
|
||||
),
|
||||
);
|
||||
return ok(
|
||||
createAnalysisExecution(
|
||||
NOT_IN_PROCEDURE.has(address)
|
||||
? {
|
||||
query_address: address,
|
||||
found: false,
|
||||
procedure: null,
|
||||
reason: "not_in_procedure",
|
||||
}
|
||||
: {
|
||||
query_address: address,
|
||||
found: true,
|
||||
procedure: ghidraFunctionIdentity(),
|
||||
},
|
||||
PROVIDER,
|
||||
),
|
||||
);
|
||||
default:
|
||||
return ok(createAnalysisExecution(null, PROVIDER));
|
||||
}
|
||||
},
|
||||
close: async () => ok(null),
|
||||
}));
|
||||
const { client } = await createApplicationMcpHarness({
|
||||
session,
|
||||
clientIdentity: { name: "native-trace-view", version: "1" },
|
||||
schemaValidator: schemaValidation,
|
||||
serverOptions: {
|
||||
environment: {
|
||||
...process.env,
|
||||
REA_MCP_OUTPUT_SCHEMA_PROFILE: options.profile ?? "full",
|
||||
},
|
||||
...(options.budget === undefined
|
||||
? {}
|
||||
: { delivery: createToolResultDelivery(options.budget) }),
|
||||
},
|
||||
});
|
||||
const directory = await createTestTempDirectory("rea-native-trace-view-");
|
||||
const target = join(directory, "fixture.com");
|
||||
await writeFile(target, Buffer.from([0xb8, 0x00, 0x4c, 0xcd, 0x21]));
|
||||
const opened = await client.callTool({
|
||||
name: "open_binary",
|
||||
arguments: { path: target, format: "dos-com" },
|
||||
});
|
||||
expect(opened.isError, JSON.stringify(opened)).not.toBe(true);
|
||||
const reply = (name: string, args: Record<string, unknown>) =>
|
||||
client.callTool({ name, arguments: args });
|
||||
const call = async (name: string, args: Record<string, unknown>) => {
|
||||
const result = await reply(name, args);
|
||||
expect(result.isError, JSON.stringify(result)).not.toBe(true);
|
||||
return { result, evidence: parseEvidence(result.structuredContent) };
|
||||
};
|
||||
return { client, session, reply, call };
|
||||
};
|
||||
|
||||
const traced = (value: unknown): LiteralFeatureTraceResult =>
|
||||
literalFeatureTraceResultSchema.parse(value);
|
||||
|
||||
it.each(["full", "compact"] as const)(
|
||||
"summarizes and pages a retained native feature trace (%s)",
|
||||
async (profile) => {
|
||||
const { client, session, call, reply } = await connect({ profile });
|
||||
// Summary first: the summary call alone retains the complete trace.
|
||||
const summary = await call("trace_feature", {
|
||||
query: "license",
|
||||
detail: "summary",
|
||||
});
|
||||
const parentId = z
|
||||
.object({ parent_evidence_id: z.string() })
|
||||
.parse(summary.evidence.normalized_result).parent_evidence_id;
|
||||
const parent = parseEvidence(session.evidenceById(parentId));
|
||||
expect(parent.operation).toBe("trace_feature");
|
||||
const full = traced(parent.normalized_result);
|
||||
const owners = (owner: string) =>
|
||||
full.references.filter(
|
||||
(reference) => literalReferenceOwner(reference) === owner,
|
||||
);
|
||||
expect(owners("unavailable")).toHaveLength(UNAVAILABLE.size);
|
||||
expect(full.residual_unknowns.length).toBeGreaterThan(0);
|
||||
expect(summary.evidence.normalized_result).toMatchObject({
|
||||
kind: "summary",
|
||||
parent_operation: "trace_feature",
|
||||
source_provider: parent.provider,
|
||||
source_analysis_profile_digest: parent.analysis_profile?.digest ?? null,
|
||||
summary: {
|
||||
query: "license",
|
||||
truncated: false,
|
||||
matches: { total: 4, string: 3, procedure: 1 },
|
||||
references: {
|
||||
total: full.references.length,
|
||||
found: owners("found").length,
|
||||
"not-found": owners("not-found").length,
|
||||
unavailable: owners("unavailable").length,
|
||||
},
|
||||
residual_unknowns: full.residual_unknowns.length,
|
||||
},
|
||||
});
|
||||
// Summary delivery still records one residual Unknown per unresolved owner.
|
||||
const unknowns = await reply("list_unknowns", {});
|
||||
expect(
|
||||
z
|
||||
.object({ result: z.object({ total: z.int() }) })
|
||||
.parse(unknowns.structuredContent).result.total,
|
||||
).toBe(full.residual_unknowns.length);
|
||||
// Unknown texts are paged rather than repeated in every view.
|
||||
const unknownTexts = z
|
||||
.object({ unknowns: z.array(z.string()) })
|
||||
.parse(summary.evidence.normalized_result).unknowns;
|
||||
for (const text of full.residual_unknowns)
|
||||
expect(unknownTexts).not.toContain(text);
|
||||
|
||||
const { tools } = await client.listTools();
|
||||
const tool = tools.find(({ name }) => name === "trace_feature")!;
|
||||
const ajv = schemaValidation.assertions;
|
||||
expect(ajv.validateSchema(tool.inputSchema)).toBe(true);
|
||||
const outputSchema: Record<string, unknown> = tool.outputSchema!;
|
||||
expect(ajv.validate(outputSchema, summary.result.structuredContent)).toBe(
|
||||
true,
|
||||
);
|
||||
const complete = await call("trace_feature", { query: "license" });
|
||||
expect(complete.evidence.evidence_id).toBe(parentId);
|
||||
expect(ajv.validate(outputSchema, complete.result.structuredContent)).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
await assertRetainedTracePages({ call, reply }, parentId, full);
|
||||
},
|
||||
);
|
||||
|
||||
/** Page the retained trace through inspect_analysis_view without another provider read. */
|
||||
const assertRetainedTracePages = async (
|
||||
{ call, reply }: Pick<Awaited<ReturnType<typeof connect>>, "call" | "reply">,
|
||||
parentId: string,
|
||||
full: LiteralFeatureTraceResult,
|
||||
) => {
|
||||
const source = { kind: "retained-evidence", evidence_id: parentId };
|
||||
const view = async (selection: Record<string, unknown>) => {
|
||||
const page = analysisViewResultSchema.parse(
|
||||
(await call("inspect_analysis_view", { source, view: selection }))
|
||||
.evidence.normalized_result,
|
||||
);
|
||||
if (page.kind !== "native-trace") throw new Error("Expected trace page");
|
||||
return page;
|
||||
};
|
||||
const collect = async (selection: Record<string, unknown>) => {
|
||||
const rows: Record<string, unknown>[] = [];
|
||||
let offset: number | null = 0;
|
||||
while (offset !== null) {
|
||||
const page = await view({
|
||||
kind: "native-trace",
|
||||
...selection,
|
||||
offset,
|
||||
limit: 2,
|
||||
});
|
||||
expect(page).toMatchObject({ query: "license", truncated: false });
|
||||
rows.push(...(page.items as Record<string, unknown>[]));
|
||||
offset = page.coverage.next_offset;
|
||||
}
|
||||
return rows;
|
||||
};
|
||||
// Pages reproduce every retained row with its source index and derived owner.
|
||||
expect(await collect({ collection: "references" })).toEqual(
|
||||
full.references.map((reference, source_index) => ({
|
||||
source_index,
|
||||
owner: literalReferenceOwner(reference),
|
||||
reference,
|
||||
})),
|
||||
);
|
||||
expect(await collect({ collection: "matches" })).toEqual(
|
||||
full.matches.map((match, source_index) => ({ source_index, match })),
|
||||
);
|
||||
expect(await collect({ collection: "residual_unknowns" })).toEqual(
|
||||
full.residual_unknowns.map((unknown, source_index) => ({
|
||||
source_index,
|
||||
unknown,
|
||||
})),
|
||||
);
|
||||
expect(
|
||||
await collect({
|
||||
collection: "references",
|
||||
where: { owners: ["unavailable"] },
|
||||
}),
|
||||
).toEqual(
|
||||
full.references.flatMap((reference, source_index) =>
|
||||
literalReferenceOwner(reference) === "unavailable"
|
||||
? [{ source_index, owner: "unavailable", reference }]
|
||||
: [],
|
||||
),
|
||||
);
|
||||
expect(
|
||||
(
|
||||
await collect({
|
||||
collection: "references",
|
||||
where: { target_addresses: ["0x10"] },
|
||||
})
|
||||
).map(({ source_index }) => source_index),
|
||||
).toEqual(
|
||||
full.references.flatMap(({ target_address }, index) =>
|
||||
target_address === "0x10" ? [index] : [],
|
||||
),
|
||||
);
|
||||
expect(
|
||||
await collect({ collection: "matches", where: { types: ["procedure"] } }),
|
||||
).toEqual(
|
||||
full.matches.flatMap((match, source_index) =>
|
||||
match.type === "procedure" ? [{ source_index, match }] : [],
|
||||
),
|
||||
);
|
||||
for (const selection of [
|
||||
{ collection: "references", where: { owners: [] } },
|
||||
{ collection: "references", where: { target_addresses: ["0X10"] } },
|
||||
{ collection: "matches", where: { types: [] } },
|
||||
{ collection: "references", offset: full.references.length },
|
||||
])
|
||||
expect(await view({ kind: "native-trace", ...selection })).toMatchObject({
|
||||
items: [],
|
||||
coverage: { status: "empty", exhausted: true },
|
||||
});
|
||||
for (const selection of [
|
||||
{ kind: "native-observation", offset: 0, limit: 1 },
|
||||
{
|
||||
kind: "native-trace",
|
||||
collection: "references",
|
||||
where: { owners: ["missing"] },
|
||||
},
|
||||
{ kind: "native-trace", collection: "callers" },
|
||||
])
|
||||
expect(
|
||||
parseMcpToolError(
|
||||
await reply("inspect_analysis_view", { source, view: selection }),
|
||||
),
|
||||
).toMatchObject({ error: { code: "invalid_request" } });
|
||||
};
|
||||
|
||||
it("recovers an oversized complete trace through a retained references page", async () => {
|
||||
const budget = 8 * 1024;
|
||||
const { session, call, reply } = await connect({ budget });
|
||||
const failed = parseMcpToolError(
|
||||
await reply("trace_feature", { query: "license" }),
|
||||
).error;
|
||||
expect(failed.code).toBe("resource_constraint");
|
||||
const limits = z
|
||||
.object({ details: z.object({ reported_limits: jsonObjectSchema }) })
|
||||
.parse(failed).details.reported_limits;
|
||||
const reference = z
|
||||
.object({ kind: z.literal("retained-evidence"), evidence_id: z.string() })
|
||||
.parse(limits.evidence_reference);
|
||||
const full = traced(
|
||||
parseEvidence(session.evidenceById(reference.evidence_id))
|
||||
.normalized_result,
|
||||
);
|
||||
const continuation = z
|
||||
.object({
|
||||
name: z.literal("inspect_analysis_view"),
|
||||
arguments: toolContract("inspect_analysis_view").inputSchema,
|
||||
})
|
||||
.parse(limits.continuation);
|
||||
expect(continuation.arguments).toEqual({
|
||||
source: reference,
|
||||
view: {
|
||||
kind: "native-trace",
|
||||
collection: "references",
|
||||
where: {},
|
||||
offset: 0,
|
||||
limit: 64,
|
||||
},
|
||||
});
|
||||
// The default page may still be too large for this deliberately tiny budget;
|
||||
// a smaller page of the same retained collection fits.
|
||||
const page = await call(continuation.name, {
|
||||
...continuation.arguments,
|
||||
view: { ...continuation.arguments.view, limit: 1 },
|
||||
});
|
||||
expect(page.evidence.normalized_result).toMatchObject({
|
||||
kind: "native-trace",
|
||||
items: [{ source_index: 0, reference: full.references[0] }],
|
||||
coverage: { total: full.references.length, next_offset: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses summary delivery before analysis when the server retains no Evidence", async () => {
|
||||
const calls: string[] = [];
|
||||
const server = createServer({
|
||||
kind: "fixed",
|
||||
analysis: {
|
||||
execute: async (operation) => {
|
||||
calls.push(operation);
|
||||
return ok(createAnalysisExecution([], PROVIDER));
|
||||
},
|
||||
},
|
||||
});
|
||||
const client = new Client({ name: "native-trace-fixed", version: "1" });
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
onTestFinished(async () => {
|
||||
await client.close();
|
||||
await server.close();
|
||||
});
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
expect(
|
||||
parseMcpToolError(
|
||||
await client.callTool({
|
||||
name: "trace_feature",
|
||||
arguments: { query: "license", detail: "summary" },
|
||||
}),
|
||||
),
|
||||
).toMatchObject({ error: { code: "capability_unavailable" } });
|
||||
expect(calls).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps a provider reference cap on every view of the trace", async () => {
|
||||
const { client, call } = await connect({ capped: true });
|
||||
const summary = await call("trace_feature", {
|
||||
query: "license",
|
||||
detail: "summary",
|
||||
});
|
||||
const parentId = z
|
||||
.object({ parent_evidence_id: z.string() })
|
||||
.parse(summary.evidence.normalized_result).parent_evidence_id;
|
||||
const source = { kind: "retained-evidence", evidence_id: parentId };
|
||||
const views = [
|
||||
summary.evidence.normalized_result,
|
||||
...(await Promise.all(
|
||||
[
|
||||
{ kind: "summary" },
|
||||
{ kind: "native-trace", collection: "references", limit: 1 },
|
||||
{
|
||||
kind: "native-trace",
|
||||
collection: "references",
|
||||
where: { owners: ["unavailable"] },
|
||||
},
|
||||
{ kind: "native-trace", collection: "matches", offset: 99 },
|
||||
{ kind: "native-trace", collection: "residual_unknowns" },
|
||||
].map(
|
||||
async (view) =>
|
||||
(await call("inspect_analysis_view", { source, view })).evidence
|
||||
.normalized_result,
|
||||
),
|
||||
)),
|
||||
];
|
||||
for (const view of views) {
|
||||
const parsed = z
|
||||
.object({
|
||||
unknowns: z.array(z.string()),
|
||||
truncated: z.boolean().optional(),
|
||||
summary: z.object({ truncated: z.boolean() }).optional(),
|
||||
})
|
||||
.parse(view);
|
||||
expect(parsed.truncated ?? parsed.summary?.truncated).toBe(true);
|
||||
expect(parsed.unknowns).toEqual(
|
||||
expect.arrayContaining([
|
||||
NATIVE_TRACE_COVERAGE_UNKNOWN,
|
||||
NATIVE_TRACE_CAP_UNKNOWN,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
// The advertised page contract binds each collection to its own rows.
|
||||
const page = await call("inspect_analysis_view", {
|
||||
source,
|
||||
view: { kind: "native-trace", collection: "references", limit: 1 },
|
||||
});
|
||||
const matches = await call("inspect_analysis_view", {
|
||||
source,
|
||||
view: { kind: "native-trace", collection: "matches", limit: 1 },
|
||||
});
|
||||
const mixed = {
|
||||
...jsonObjectSchema.parse(page.evidence.normalized_result),
|
||||
items: (matches.evidence.normalized_result as { items: unknown }).items,
|
||||
};
|
||||
expect(analysisViewResultSchema.safeParse(mixed).success).toBe(false);
|
||||
const inspectTool = (await client.listTools()).tools.find(
|
||||
({ name }) => name === "inspect_analysis_view",
|
||||
)!;
|
||||
const advertised: Record<string, unknown> = inspectTool.outputSchema!;
|
||||
const ajv = schemaValidation.assertions;
|
||||
expect(ajv.validate(advertised, page.result.structuredContent)).toBe(true);
|
||||
expect(
|
||||
ajv.validate(advertised, {
|
||||
...jsonObjectSchema.parse(page.result.structuredContent),
|
||||
normalized_result: mixed,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
analysisViewResultSchema.safeParse(page.evidence.normalized_result).success,
|
||||
).toBe(true);
|
||||
});
|
||||
Reference in new issue
Block a user