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 = ( 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; 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 = ( 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 => { 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 => { 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( (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), }); };