perf(managed): add retained member summary and pages

inspect_managed_members always returned the assembly-wide member_refs, so
one selected or unmatched type produced megabytes, and an unselected
ordinary assembly exceeded the MCP response budget. Add opt-in
detail: "summary" that retains the complete Evidence and returns identity,
selection with unmatched names, coverage, user-string coverage, limitations
and per-collection counts. Page original rows through inspect_analysis_view
with kind managed-members; each page names its collection so no row schema
is chosen by union order. Complete delivery stays the default.

Refs #3010

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822 2026-10-14 00:40:46 +00:00
parent 30128a03cf
commit 54e89bd5c8
11 changed files with 610 additions and 42 deletions

View File

@ -169,6 +169,14 @@ repeat parsing. Returned-row exhaustion
does not prove complete metadata or absence under partial/unavailable coverage;
preserve build-local tokens and their artifact/MVID scope.
`inspect_managed_members` always returns the assembly-wide `member_refs`, even
for one selected type. For questions about a selected type, select
`detail: "summary"` with `type_names`; it returns the selection (with unmatched
names), coverage and per-collection counts. Page rows with
`view: {kind: "managed-members", collection: "call_edges", offset: 0, limit: 64}`
over `types`, `exported_types`, `fields`, `methods`, `member_refs`, `call_edges`,
`field_accesses` or `user_strings`.
### .NET NativeAOT
NativeAOT binaries contain native machine code, so ordinary CIL decompilers

View File

@ -81,6 +81,40 @@ including the existing `inspect-analysis-view` CLI. Session record lifetime and
export/import semantics remain unchanged. A server without Evidence retention
rejects selected summary before analysis; complete delivery remains available.
### Member summary and retained member rows
Complete `inspect_managed_members` delivery always includes the assembly-wide
`member_refs` and `exported_types`, which `type_names` does not narrow, so even
one selected or unmatched type can return megabytes. Select
`{"path":"/absolute/path/Assembly.dll","type_names":["Namespace.Type"],"detail":"summary"}`
to keep the complete source in the session and receive identity, metadata
status, the selection with its unmatched names, coverage/issues, the separate
user-string coverage, limitations and per-collection counts. Then page original
rows with view kind `managed-members`:
```json
{
"source": {
"kind": "retained-evidence",
"evidence_id": "<parent_evidence_id>"
},
"view": {
"kind": "managed-members",
"collection": "call_edges",
"offset": 0,
"limit": 64
}
}
```
Collections are `types`, `exported_types`, `fields`, `methods`, `member_refs`,
`call_edges`, `field_accesses` and `user_strings` (the literals). Each page names
its collection, retains original rows, order, tokens, signatures and body facts,
and carries the source selection and coverage; `user_strings` pages carry the
literal pool's own coverage. Older Evidence that never examined exported types
or user strings reports a null count and refuses those pages rather than
returning an empty one.
## Shipped scope
The canonical parser admits PE/CLI bytes and their metadata/CIL without loading

File diff suppressed because one or more lines are too long

View File

@ -21,13 +21,13 @@ export const managedTargetInputSchema = z.strictObject({
.optional(),
});
/** Delivery selection belongs only to artifact inspection, not member/native queries. */
/** Delivery selection belongs to artifact and member inspection, not native queries. */
export const managedArtifactInputSchema = managedTargetInputSchema.extend({
detail: analysisDetailSchema.unwrap().optional(),
});
/** Member inspection may select declaring types before members are decoded. */
export const managedMembersInputSchema = managedTargetInputSchema.extend({
export const managedMembersInputSchema = managedArtifactInputSchema.extend({
type_names: z
.array(
z
@ -87,7 +87,7 @@ export const MANAGED_TOOL_CONTRACTS = [
name: "inspect_managed_members",
...toolContractMetadata("inspect_managed_members"),
description:
"Inspect PE/CLI metadata members, signatures, raw CIL hashes, decoded-instruction-tuple hashes, separately reported exception regions, call edges, field-access anchors, and the #US string literals that ldstr loads, without loading or executing target code. Select type_names to decode only the members of those exact declaring types and the literals their bodies load, which keeps large assemblies such as a base class library within the representation budget. Metadata tokens are reported as build-local coordinates bound to the artifact SHA-256 and MVID; the tuple hash does not resolve them or fully commit control flow.",
'Inspect PE/CLI metadata members, signatures, raw CIL hashes, decoded-instruction-tuple hashes, separately reported exception regions, call edges, field-access anchors, and the #US string literals that ldstr loads, without loading or executing target code. Select type_names to decode only the members of those exact declaring types and the literals their bodies load, which keeps large assemblies such as a base class library within the representation budget. Metadata tokens are reported as build-local coordinates bound to the artifact SHA-256 and MVID; the tuple hash does not resolve them or fully commit control flow. Omitted or complete detail returns every collection, including the assembly-wide member_refs and exported_types, which type_names does not narrow. Select detail: "summary" for identity, metadata status, selection with unmatched names, coverage/issues, limitations and per-collection counts; it retains complete Evidence and returns a linked inspect_analysis_view summary. Page original rows with view kind managed-members, normalized_result.parent_evidence_id and collection types/exported_types/fields/methods/member_refs/call_edges/field_accesses/user_strings.',
kind: "managed-provider",
inputSchema: managedMembersInputSchema,
outputSchema: memberOutputSchema,
@ -103,6 +103,14 @@ export const MANAGED_TOOL_CONTRACTS = [
type_names: ["System.Threading.Monitor"],
},
},
{
title: "Summarize selected managed types and retain their members",
input: {
path: "/tmp/mscorlib.dll",
type_names: ["System.Threading.Monitor"],
detail: "summary",
},
},
],
},
{

View File

@ -4,6 +4,7 @@ import {
artifactInventorySummaryViewResultSchema,
machoSummaryViewResultSchema,
managedArtifactSummaryViewResultSchema,
managedMemberSummaryViewResultSchema,
} from "../domain/analysisView/analysisView.js";
import { evidenceBundleSummarySchema } from "../domain/evidenceRecordSummary.js";
import { BINARY_ARCHITECTURES } from "../domain/binaryTargetTypes.js";
@ -418,7 +419,12 @@ export const managedOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
managedArtifactSummaryViewResultSchema,
]),
),
inspect_managed_members: evidenceResultOf(managedMemberInspectionSchema),
inspect_managed_members: evidenceResultOf(
z.union([
managedMemberInspectionSchema,
managedMemberSummaryViewResultSchema,
]),
),
inspect_managed_native_boundaries: evidenceResultOf(
managedNativeBoundaryInspectionSchema,
),

View File

@ -49,6 +49,13 @@ import {
managedSummarySchema,
managedViewContextSchema,
} from "./managedArtifactView.js";
import {
projectManagedMemberView,
managedMembersViewRequestSchema,
managedMembersViewPageSchema,
managedMemberSummarySchema,
managedMembersViewContextSchema,
} from "./managedMemberView.js";
import {
projectWasmArtifactView,
wasmViewRequestSchema,
@ -208,6 +215,7 @@ export const analysisViewRequestSchema = z.discriminatedUnion("kind", [
machoViewRequestSchema,
evmViewRequestSchema,
managedViewRequestSchema,
managedMembersViewRequestSchema,
androidViewRequestSchema,
apiSurfaceViewSchema,
captureViewRequestSchema,
@ -328,6 +336,7 @@ const viewResultBase = {
"inspect_macho",
"inspect_evm_interface",
"inspect_managed_artifact",
"inspect_managed_members",
"inspect_web_network_capture",
"inspect_android_package",
"inspect_pcap_capture",
@ -396,6 +405,13 @@ export const managedArtifactSummaryViewResultSchema = z.strictObject({
summary: managedSummarySchema,
});
/** Exact member selection, coverage and counts for the selected delivery. */
export const managedMemberSummaryViewResultSchema = z.strictObject({
...summaryViewResultBase,
parent_operation: z.literal("inspect_managed_members"),
summary: managedMemberSummarySchema,
});
/** Exact historical capture summary, with no body or member-inventory payload. */
export const webNetworkCaptureSummaryViewResultSchema = z.strictObject({
...summaryViewResultBase,
@ -446,6 +462,7 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
machoSummarySchema,
evmSummarySchema,
managedSummarySchema,
managedMemberSummarySchema,
captureSummarySchema,
androidPackageSummarySchema,
pcapSummarySchema,
@ -529,6 +546,14 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
...managedViewContextSchema.shape,
parent_operation: z.literal("inspect_managed_artifact"),
}),
z.strictObject({
kind: z.literal("managed-members"),
view: managedMembersViewRequestSchema,
page: managedMembersViewPageSchema,
...viewResultBase,
...managedMembersViewContextSchema.shape,
parent_operation: z.literal("inspect_managed_members"),
}),
z.strictObject({
kind: z.literal("android"),
view: androidViewRequestSchema,
@ -622,6 +647,10 @@ export type UnsignedAnalysisView =
Extract<AnalysisViewResult, { readonly kind: "managed" }>,
"view_digest"
>
| Omit<
Extract<AnalysisViewResult, { readonly kind: "managed-members" }>,
"view_digest"
>
| Omit<
Extract<AnalysisViewResult, { readonly kind: "android" }>,
"view_digest"
@ -674,7 +703,7 @@ const unsupportedParent = (
new AnalysisUnsupportedTargetError(
INSPECT_ANALYSIS_VIEW_OPERATION,
parentArtifactPath(parent),
`Parent Evidence operation ${JSON.stringify(parent.operation)} does not support analysis views. Use completed binary layout, JavaScript application, native function, artifact inventory/inspection, WASM, Mach-O, EVM, managed artifact, Android package, Dart AOT, or web network capture Evidence.`,
`Parent Evidence operation ${JSON.stringify(parent.operation)} does not support analysis views. Use completed binary layout, JavaScript application, native function, artifact inventory/inspection, WASM, Mach-O, EVM, managed artifact or member, Android package, Dart AOT, or web network capture Evidence.`,
);
const parentArtifactPath = (parent: AnalysisViewParent): string => {
@ -704,6 +733,7 @@ const retainedViewProjectors = new Map<string, typeof projectWasmArtifactView>([
["inspect_macho", projectMachoView],
["inspect_evm_interface", projectEvmInterfaceView],
["inspect_managed_artifact", projectManagedArtifactView],
["inspect_managed_members", projectManagedMemberView],
["inspect_android_package", projectAndroidPackageView],
["inspect_dart_aot", projectDartAotView],
["inspect_artifact", projectArtifactInventoryView],

View File

@ -51,6 +51,24 @@ export const managedViewContextSchema = z.strictObject({
source_coverage: managedArtifactInspectionSchema.shape.coverage,
});
/** Token scope and identity limits shared by every managed metadata view. */
export const managedViewIdentityUnknowns = (
parent: AnalysisViewParent,
mvid: string | null,
): string[] => [
"Metadata tokens are build-local coordinates bound to the recorded artifact SHA-256 and module MVID; this view does not load or execute the assembly.",
...(parent.subject == null
? [
"The parent has no recorded subject; its normalized artifact identity cannot be cross-checked against a subject digest.",
]
: []),
...(mvid === null
? [
"The module MVID is unknown; tokens cannot establish a cross-observation module match.",
]
: []),
];
/** Project authenticated metadata without reading a path or invoking a provider. */
export const projectManagedArtifactView = (
parent: AnalysisViewParent,
@ -96,17 +114,7 @@ export const projectManagedArtifactView = (
limitations: uniqueSorted([...parent.limitations, ...report.limitations]),
unknowns: [
"Collection counts and pages cover producer-returned rows; metadata completeness remains the original metadata status and coverage, not an absence claim about unreturned rows.",
"Metadata tokens are build-local coordinates bound to the recorded artifact SHA-256 and module MVID; this view does not load or execute the assembly.",
...(parent.subject == null
? [
"The parent has no recorded subject; its normalized artifact identity cannot be cross-checked against a subject digest.",
]
: []),
...(identity_scope.requires_mvid === null
? [
"The module MVID is unknown; tokens cannot establish a cross-observation module match.",
]
: []),
...managedViewIdentityUnknowns(parent, identity_scope.requires_mvid),
],
};
if (view.kind === "summary") {

View File

@ -0,0 +1,270 @@
import { z } from "zod";
import type { AnalysisError } from "../analysisErrorBase.js";
import { uniqueSorted } from "../canonicalOrdering.js";
import { analysisInputErrorFromIssues } from "../inputIssueProjection.js";
import {
managedMemberInspectionSchema,
type ManagedMemberInspection,
} from "../managed/managedArtifact.js";
import { err, ok, type Result } from "../result.js";
import {
analysisViewInputError,
type AnalysisViewParent,
type AnalysisViewRequest,
type UnsignedAnalysisView,
} from "./analysisView.js";
import {
completeWithinViewCoverage,
pageViewCoverage,
} from "./analysisViewCoverage.js";
import { managedViewIdentityUnknowns } from "./managedArtifactView.js";
const member = managedMemberInspectionSchema.shape;
/** Original member rows; offsets count entries in the returned parent collection. */
export const managedMembersViewRequestSchema = z.strictObject({
kind: z.literal("managed-members"),
collection: z.enum([
"types",
"exported_types",
"fields",
"methods",
"member_refs",
"call_edges",
"field_accesses",
"user_strings",
]),
offset: z.number().int().nonnegative().default(0),
limit: z.number().int().positive().default(64),
});
const rowsOf = <C extends string, S extends z.ZodArray>(
collection: C,
rows: S,
) => z.strictObject({ collection: z.literal(collection), rows });
/**
* Rows of one collection, keyed by that collection: several row schemas accept
* each other's rows after stripping keys, so no union may pick one by order.
*/
export const managedMembersViewPageSchema = z.discriminatedUnion("collection", [
rowsOf("types", member.types),
rowsOf("exported_types", member.exported_types.unwrap().unwrap()),
rowsOf("fields", member.fields),
rowsOf("methods", member.methods),
rowsOf("member_refs", member.member_refs),
rowsOf("call_edges", member.call_edges),
rowsOf("field_accesses", member.field_accesses),
rowsOf("user_strings", member.user_strings.unwrap().unwrap().shape.literals),
]);
type ManagedMembersViewPage = z.infer<typeof managedMembersViewPageSchema>;
const nullableCount = z.number().int().nonnegative().nullable();
/** Keep identity, selection and coverage; member collections remain retained. */
export const managedMemberSummarySchema = z.strictObject({
artifact: member.artifact,
module: member.module,
metadata: member.metadata,
identity_scope: member.identity_scope,
selection: member.selection.unwrap(),
coverage: member.coverage,
user_strings: z
.strictObject({
state: member.user_strings.unwrap().unwrap().shape.state,
issues: member.user_strings.unwrap().unwrap().shape.issues,
})
.nullable(),
limitations: member.limitations,
reported_counts: z.strictObject({
types: z.number().int().nonnegative(),
exported_types: nullableCount,
fields: z.number().int().nonnegative(),
methods: z.number().int().nonnegative(),
member_refs: z.number().int().nonnegative(),
call_edges: z.number().int().nonnegative(),
field_accesses: z.number().int().nonnegative(),
user_strings: nullableCount,
}),
});
export const managedMembersViewContextSchema = z.strictObject({
identity_scope: member.identity_scope,
selection: member.selection.unwrap(),
source_metadata_status: member.metadata.shape.status,
source_coverage: member.coverage,
});
type ManagedMembersViewRequest = z.infer<
typeof managedMembersViewRequestSchema
>;
type MemberCollection = ManagedMembersViewRequest["collection"];
const slice = <C extends MemberCollection, T>(
collection: C,
rows: readonly T[] | null | undefined,
{ offset, limit }: { readonly offset: number; readonly limit: number },
) =>
rows == null
? null
: {
page: { collection, rows: rows.slice(offset, offset + limit) },
total: rows.length,
};
/** One page of a collection, or null when the parent never examined it. */
const collectionPage = (
report: ManagedMemberInspection,
view: ManagedMembersViewRequest,
): { readonly page: ManagedMembersViewPage; readonly total: number } | null => {
const { collection } = view;
switch (collection) {
case "types":
return slice(collection, report.types, view);
case "exported_types":
return slice(collection, report.exported_types, view);
case "fields":
return slice(collection, report.fields, view);
case "methods":
return slice(collection, report.methods, view);
case "member_refs":
return slice(collection, report.member_refs, view);
case "call_edges":
return slice(collection, report.call_edges, view);
case "field_accesses":
return slice(collection, report.field_accesses, view);
case "user_strings":
return slice(collection, report.user_strings?.literals, view);
}
};
/** Everything but the member collections, which are replaced by their counts. */
const summarizeMembers = (
report: ManagedMemberInspection,
): z.infer<typeof managedMemberSummarySchema> => {
const {
types,
exported_types,
fields,
methods,
member_refs,
call_edges,
field_accesses,
user_strings,
...identity
} = report;
return {
...identity,
user_strings:
user_strings === null
? null
: { state: user_strings.state, issues: user_strings.issues },
reported_counts: {
types: types.length,
exported_types: exported_types?.length ?? null,
fields: fields.length,
methods: methods.length,
member_refs: member_refs.length,
call_edges: call_edges.length,
field_accesses: field_accesses.length,
user_strings: user_strings?.literals.length ?? null,
},
};
};
/** Project authenticated member Evidence without reading a path or invoking a provider. */
export const projectManagedMemberView = (
parent: AnalysisViewParent,
view: AnalysisViewRequest,
): Result<UnsignedAnalysisView, AnalysisError> => {
if (view.kind !== "summary" && view.kind !== "managed-members")
return err(
analysisViewInputError([
{
path: ["view"],
reason: "invalid_value",
message:
"Managed member inspection supports summary and managed-members views.",
},
]),
);
const parsed = managedMemberInspectionSchema.safeParse(
parent.normalizedResult,
);
if (!parsed.success)
return err(
analysisInputErrorFromIssues(
"inspect_analysis_view",
parsed.error.issues.map((issue) => ({
...issue,
path: ["source", "normalized_result", ...issue.path],
})),
{ source: { normalized_result: parent.normalizedResult } },
{ cause: parsed.error },
),
);
const report = parsed.data;
const shared = {
parent_evidence_id: parent.evidenceId,
parent_operation: "inspect_managed_members" as const,
parent_digest: parent.evidenceId.slice(3),
artifact: { path: report.artifact.path, sha256: report.artifact.sha256 },
limitations: uniqueSorted([...parent.limitations, ...report.limitations]),
unknowns: [
"Collection counts and pages cover producer-returned rows; member completeness remains the original coverage, not an absence claim about unreturned rows.",
...(report.selection === null
? []
: [
"Exported types and member references cover the whole assembly; the other collections cover only the selected declaring types.",
]),
...managedViewIdentityUnknowns(
parent,
report.identity_scope.requires_mvid,
),
],
};
if (view.kind === "summary") {
const summary = summarizeMembers(report);
const total = Object.values(summary.reported_counts).reduce<number>(
(sum, count) => sum + (count ?? 0),
0,
);
return ok({
kind: "summary",
view,
...shared,
summary,
coverage: completeWithinViewCoverage(total, total),
});
}
const selected = collectionPage(report, view);
if (selected === null)
return err(
analysisViewInputError([
{
path: ["view", "collection"],
reason: "invalid_value",
message: `The parent Evidence did not examine ${view.collection}; repeat inspect_managed_members with the current runtime to record it.`,
},
]),
);
const { page, total } = selected;
return ok({
kind: "managed-members",
view,
page,
...shared,
identity_scope: report.identity_scope,
selection: report.selection,
source_metadata_status: report.metadata.status,
source_coverage:
view.collection === "user_strings" && report.user_strings !== null
? {
state: report.user_strings.state,
issues: report.user_strings.issues,
}
: report.coverage,
coverage: pageViewCoverage(view.offset, page.rows.length, total),
});
};

View File

@ -16,6 +16,15 @@ import { registerEvidenceTools } from "./registerEvidenceTools.js";
import { runManagedProviderExecution } from "../composition/directAnalysis.js";
import { isManagedToolName } from "../contracts/managed/managedToolContracts.js";
/** Operations whose caller may select a retained summary instead of complete delivery. */
const summarizesRetainedEvidence = (
operation: string,
parameters: Readonly<Record<string, unknown>>,
): boolean =>
(operation === "inspect_managed_artifact" ||
operation === "inspect_managed_members") &&
parameters.detail === "summary";
/** Register execution-free managed PE/CLI inspection. */
export const registerManagedTools = (
server: EvidenceMcpServer,
@ -34,11 +43,8 @@ export const registerManagedTools = (
execute: async (operation, parameters, executionOptions) => {
if (!isManagedToolName(operation))
return analysis.execute(operation, parameters, executionOptions);
const { detail: _detail, ...artifactParameters } = parameters;
if (
operation === "inspect_managed_artifact" &&
parameters.detail === "summary"
) {
const { detail: _detail, ...providerParameters } = parameters;
if (summarizesRetainedEvidence(operation, parameters)) {
const ready = requireAnalysisViewRetention(operation, {
recordEvidence: options.recordEvidence,
evidenceById: options.evidenceById,
@ -69,9 +75,7 @@ export const registerManagedTools = (
path,
operation,
executionOptions?.signal,
operation === "inspect_managed_artifact"
? artifactParameters
: parameters,
providerParameters,
);
if (execution.ok && typeof requestedPath === "string")
selectedManagedPath = requestedPath;
@ -82,8 +86,7 @@ export const registerManagedTools = (
...options,
analysisForAdmission,
projectEvidence: (operation, parameters, evidence) =>
operation === "inspect_managed_artifact" &&
parameters.detail === "summary"
summarizesRetainedEvidence(operation, parameters)
? summarizeRetainedAnalysis(evidence, {
recordEvidence: options.recordEvidence,
evidenceById: options.evidenceById,

View File

@ -49,16 +49,13 @@ it("returns a first-call overview and pages the unchanged source after its file
const ajv = new Ajv2020({ strict: false, validateFormats: false });
expect(ajv.validateSchema(definition.inputSchema)).toBe(true);
expect(ajv.validateSchema(outputSchema)).toBe(true);
for (const name of [
"inspect_managed_members",
"inspect_managed_native_boundaries",
])
expect(
ajv.validate(catalog.find((x) => x.name === name)!.inputSchema, {
path,
detail: "summary",
}),
).toBe(false);
expect(
ajv.validate(
catalog.find((x) => x.name === "inspect_managed_native_boundaries")!
.inputSchema,
{ path, detail: "summary" },
),
).toBe(false);
const reply = await client.callTool({
name: "inspect_managed_artifact",
arguments: { path, detail: "summary" },
@ -239,11 +236,15 @@ it("refuses selected delivery before file access when the server cannot retain E
const [a, b] = InMemoryTransport.createLinkedPair();
await server.connect(b);
await client.connect(a);
const rejected = await client.callTool({
name: "inspect_managed_artifact",
arguments: { path: join(directory, "absent.dll"), detail: "summary" },
});
expect(parseMcpToolError(rejected).error.code).toBe("capability_unavailable");
for (const name of ["inspect_managed_artifact", "inspect_managed_members"]) {
const rejected = await client.callTool({
name,
arguments: { path: join(directory, "absent.dll"), detail: "summary" },
});
expect(parseMcpToolError(rejected).error.code).toBe(
"capability_unavailable",
);
}
const complete = await client.callTool({
name: "inspect_managed_artifact",
arguments: { path },

View File

@ -0,0 +1,200 @@
import { writeFile, unlink } from "node:fs/promises";
import { join } from "node:path";
import { Ajv2020 } from "ajv/dist/2020.js";
import { expect, it, onTestFinished } from "vitest";
import { composeBinarySession } from "../../../src/application/binary/BinarySessionComposition.js";
import { AnalysisProviderRegistry } from "../../../src/application/binary/AnalysisProviderRegistry.js";
import { inspectAnalysisView } from "../../../src/application/analysisView/AnalysisViewService.js";
import { ManagedStaticProvider } from "../../../src/dotnet/ManagedStaticProvider.js";
import { buildManagedPeFixture } from "../../../src/dotnet/ManagedPe.fixture.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import {
analysisViewResultSchema,
managedMemberSummaryViewResultSchema,
} from "../../../src/domain/analysisView/analysisView.js";
import { managedMemberInspectionSchema } from "../../../src/domain/managed/managedArtifact.js";
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const COLLECTIONS = [
"types",
"exported_types",
"fields",
"methods",
"member_refs",
"call_edges",
"field_accesses",
"user_strings",
] as const;
const createHarness = async () => {
const session = composeBinarySession(new AnalysisProviderRegistry([]), [
new ManagedStaticProvider(),
]);
const harness = await createApplicationMcpHarness({ session });
onTestFinished(harness.close);
return harness;
};
it("summarizes selected members and pages every original collection after the file disappears", async () => {
const directory = await createTestTempDirectory("rea-member-summary-");
const path = join(directory, "fixture.dll");
await writeFile(path, buildManagedPeFixture({ methodRowCount: 2 }));
const { client, session } = await createHarness();
const definition = (await client.listTools()).tools.find(
(x) => x.name === "inspect_managed_members",
);
if (definition?.outputSchema === undefined)
throw new Error("Managed member schemas are absent");
const outputSchema: Record<string, unknown> = definition.outputSchema;
const ajv = new Ajv2020({ strict: false, validateFormats: false });
expect(ajv.validateSchema(definition.inputSchema)).toBe(true);
expect(ajv.validateSchema(outputSchema)).toBe(true);
const type_names = ["Fixture.Program", "Missing.Type"];
expect(
ajv.validate(definition.inputSchema, {
path,
type_names,
detail: "summary",
}),
).toBe(true);
const reply = await client.callTool({
name: "inspect_managed_members",
arguments: { path, type_names, detail: "summary" },
});
expect(reply.isError).not.toBe(true);
expect(
ajv.validate(outputSchema, reply.structuredContent),
JSON.stringify(ajv.errors),
).toBe(true);
const receipt = parseEvidence(reply.structuredContent);
const view = managedMemberSummaryViewResultSchema.parse(
receipt.normalized_result,
);
const parent = session.evidenceById(view.parent_evidence_id);
if (parent === undefined)
throw new Error("Complete member source was not retained");
expect(parent.operation).toBe("inspect_managed_members");
expect(receipt.evidence_links).toEqual([parent.evidence_id]);
const report = managedMemberInspectionSchema.parse(parent.normalized_result);
expect(view.summary.selection).toEqual({
type_names,
unmatched_type_names: ["Missing.Type"],
});
expect(view.summary.coverage).toEqual(report.coverage);
expect(view.summary.identity_scope).toEqual(report.identity_scope);
expect(view.summary).not.toHaveProperty("methods");
expect(report.methods.length).toBeGreaterThan(0);
expect(report.member_refs.length).toBeGreaterThan(0);
for (const detail of [undefined, "complete"]) {
const complete = parseEvidence(
(
await client.callTool({
name: "inspect_managed_members",
arguments: { path, type_names, ...(detail ? { detail } : {}) },
})
).structuredContent,
);
expect(complete.operation).toBe("inspect_managed_members");
expect(complete.normalized_result).toEqual(parent.normalized_result);
}
const original = JSON.stringify(parent);
await unlink(path);
for (const collection of COLLECTIONS) {
const rows =
collection === "user_strings"
? report.user_strings?.literals
: report[collection];
if (rows == null) throw new Error(`${collection} was not examined`);
expect(view.summary.reported_counts[collection]).toBe(rows.length);
const paged: unknown[] = [];
for (let offset: number | null = 0; offset !== null;) {
const result = await client.callTool({
name: "inspect_analysis_view",
arguments: {
source: {
kind: "retained-evidence",
evidence_id: parent.evidence_id,
},
view: { kind: "managed-members", collection, offset, limit: 1 },
},
});
expect(result.isError).not.toBe(true);
const page = analysisViewResultSchema.parse(
parseEvidence(result.structuredContent).normalized_result,
);
if (page.kind !== "managed-members")
throw new Error("Unexpected page kind");
expect(page.page.collection).toBe(collection);
expect(page.selection).toEqual(report.selection);
expect(page.coverage.total).toBe(rows.length);
paged.push(...page.page.rows);
offset = page.coverage.next_offset;
}
// Method rows carry body facts that a looser row schema would strip.
expect(paged).toEqual(rows);
}
expect(JSON.stringify(session.evidenceById(parent.evidence_id))).toBe(
original,
);
const portable = inspectAnalysisView({
source: { kind: "inline", evidence: parent },
view: { kind: "summary" },
});
if (!portable.ok) throw portable.error;
expect(portable.value.normalized_result).toEqual(view);
}, 30000);
it("keeps collections that historical member Evidence never examined unknown", async () => {
const directory = await createTestTempDirectory("rea-member-historical-");
const path = join(directory, "fixture.dll");
await writeFile(path, buildManagedPeFixture());
const { client } = await createHarness();
const parent = parseEvidence(
(
await client.callTool({
name: "inspect_managed_members",
arguments: { path },
})
).structuredContent,
);
const {
exported_types: _exported,
user_strings: _strings,
selection: _selection,
...recorded
} = managedMemberInspectionSchema.parse(parent.normalized_result);
const historical = createEvidence(undefined, parent.provider, {
operation: parent.operation,
parameters: parent.parameters,
result: recorded,
limitations: parent.limitations,
authority: parent.authority,
});
const summary = inspectAnalysisView({
source: { kind: "inline", evidence: historical },
view: { kind: "summary" },
});
if (!summary.ok) throw summary.error;
const view = managedMemberSummaryViewResultSchema.parse(
summary.value.normalized_result,
);
expect(view.summary.reported_counts.exported_types).toBeNull();
expect(view.summary.reported_counts.user_strings).toBeNull();
expect(view.summary.user_strings).toBeNull();
expect(view.summary.selection).toBeNull();
expect(view.unknowns.join(" ")).toContain("no recorded subject");
for (const collection of ["exported_types", "user_strings"] as const)
expect(
inspectAnalysisView({
source: { kind: "inline", evidence: historical },
view: { kind: "managed-members", collection, offset: 0, limit: 1 },
}),
).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } });
expect(
inspectAnalysisView({
source: { kind: "inline", evidence: historical },
view: { kind: "managed", collection: "attributes", offset: 0, limit: 1 },
}),
).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } });
}, 30000);