rea/src/domain/analysisView/managedMemberView.ts

271 lines
8.7 KiB
TypeScript

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