import { z } from "zod"; import type { AnalysisError } from "../analysisErrorBase.js"; import { analysisViewInputError } from "./analysisViewErrors.js"; import { uniqueSorted } from "../canonicalOrdering.js"; import { canBorrowParsedJsonSnapshot } from "../immutableJson.js"; import { interfaceBuilderAnalysisSchema, interfaceBuilderDocumentSchema, } from "../apple/interfaceBuilderGraph.js"; import { nativeInvestigationCoverageSchema, nativeInvestigationEdgeSchema, nativeInvestigationNodeSchema, nativeInvestigationProviderSchema, } from "../native/nativeInvestigationGraph.js"; import { err, ok, type Result } from "../result.js"; import { parseAnalysisViewResult } from "./analysisViewParent.js"; import { selectViewRows } from "./selectViewRows.js"; import type { AnalysisViewParent, AnalysisViewRequest, UnsignedAnalysisView, } from "./analysisView.js"; import { completeWithinViewCoverage, pageViewCoverage, } from "./analysisViewCoverage.js"; const page = { kind: z.literal("interface-builder"), offset: z.number().int().nonnegative().default(0), limit: z.number().int().positive().default(64), } as const; /** Original decoded rows; graph rows can be narrowed to one decoded document. */ export const interfaceBuilderViewRequestSchema = z.discriminatedUnion( "collection", [ z.strictObject({ ...page, collection: z.enum(["nodes", "edges"]), document: z .string() .min(1) .optional() .describe( "Exact documents[].relative_path; selects rows whose evidence names that archive.", ), }), z.strictObject({ ...page, collection: z.enum(["documents", "coverage"]), }), ], ); export const interfaceBuilderViewItemsSchema = z.union([ z.array(interfaceBuilderDocumentSchema), z.array(nativeInvestigationNodeSchema), z.array(nativeInvestigationEdgeSchema), z.array(nativeInvestigationCoverageSchema), ]); /** The decoder's own truncation, independent of page exhaustion. */ export const interfaceBuilderViewContextSchema = z.strictObject({ source_truncated: z.boolean(), }); const countsSchema = z.record(z.string(), z.number().int().positive()); /** Every decoded document with graph counts; rows and evidence stay retained. */ export const interfaceBuilderSummarySchema = z.strictObject({ target_sha256: z .string() .regex(/^[a-f0-9]{64}$/u) .nullable(), provider: nativeInvestigationProviderSchema, documents: interfaceBuilderDocumentSchema.array().max(64), available_counts: z.strictObject({ nodes: z.number().int().nonnegative(), edges: z.number().int().nonnegative(), coverage: z.number().int().nonnegative(), }), node_kinds: countsSchema, edge_relations: countsSchema, coverage_statuses: countsSchema, truncated: z.boolean(), limitation_count: z.number().int().nonnegative(), }); const tally = (values: readonly string[]): Record => { const counts = new Map(); for (const value of values) counts.set(value, (counts.get(value) ?? 0) + 1); return Object.fromEntries( [...counts].sort(([left], [right]) => (left < right ? -1 : 1)), ); }; type InterfaceBuilderReport = z.output; type InterfaceBuilderPage = z.output; const pageRows = (rows: readonly Row[], view: InterfaceBuilderPage) => ({ rows: rows.slice(view.offset, view.offset + view.limit), total: rows.length, }); const selectDocumentRows = < Row extends { readonly evidence: readonly { artifact_path: unknown }[] }, >( rows: readonly Row[], view: InterfaceBuilderPage, document: string | undefined, ) => document === undefined ? pageRows(rows, view) : selectViewRows( rows, view, (row) => row.evidence.some((evidence) => evidence.artifact_path === document), (row) => row, ); // Exact owned snapshot identity, with no second retained graph or live-file cache. // Weak membership ends with the Evidence lifetime; failed/mutable imports are not reused. const checkedReports = new WeakSet(); /** Admit a well-formed report bound to its recorded target, if one exists. */ const parseInterfaceBuilderReport = ( parent: AnalysisViewParent, ): Result => { const source = parent.normalizedResult; const reusable = parent.sealedResult === true && typeof source === "object" && source !== null; const known = reusable && checkedReports.has(source); const parsed = known ? ok(source as InterfaceBuilderReport) : parseAnalysisViewResult(interfaceBuilderAnalysisSchema, parent); if (!parsed.ok) return parsed; const report = parsed.value; if ( report.graph.target_sha256 !== report.target_sha256 || (parent.subject != null && parent.subject.digest.sha256 !== report.target_sha256) ) return err( analysisViewInputError([ { path: ["source", "subject"], reason: "invalid_value", message: "Interface Builder parent Evidence subject and graph must identify the report's target SHA-256.", }, ]), ); if (!known && reusable && canBorrowParsedJsonSnapshot(source, report)) { checkedReports.add(source); return ok(source); } return ok(report); }; /** Every decoded document with graph tallies; rows stay in the retained parent. */ const summarizeInterfaceBuilder = ( report: InterfaceBuilderReport, limitationCount: number, ) => { const { graph } = report; const total = report.documents.length + graph.nodes.length + graph.edges.length + graph.coverage.length; return { summary: interfaceBuilderSummarySchema.parse({ target_sha256: report.target_sha256, provider: graph.provider, documents: report.documents, available_counts: { nodes: graph.nodes.length, edges: graph.edges.length, coverage: graph.coverage.length, }, node_kinds: tally(graph.nodes.map((node) => node.kind)), edge_relations: tally(graph.edges.map((edge) => edge.relation)), coverage_statuses: tally(graph.coverage.map((row) => row.status)), truncated: graph.truncated, limitation_count: limitationCount, }), coverage: completeWithinViewCoverage(total, total), }; }; /** Project retained decode_interface_builder Evidence without reopening archives. */ export const projectInterfaceBuilderView = ( parent: AnalysisViewParent, view: AnalysisViewRequest, ): Result => { if (view.kind !== "summary" && view.kind !== "interface-builder") return err( analysisViewInputError([ { path: ["view"], reason: "invalid_value", message: "Interface Builder decoding supports summary and interface-builder views.", }, ]), ); const parsed = parseInterfaceBuilderReport(parent); if (!parsed.ok) return parsed; const report = parsed.value; const { graph } = report; const limitations = uniqueSorted([ ...parent.limitations, ...report.limitations, ]); const shared = { parent_evidence_id: parent.evidenceId, parent_operation: "decode_interface_builder" as const, parent_digest: parent.evidenceId.slice(3), artifact: { path: parent.subject?.local_path ?? report.resource_root?.canonical_path ?? "", sha256: report.target_sha256, }, ...(report.resource_root === undefined ? {} : { resource_root: report.resource_root }), limitations, unknowns: [ "Rows describe recognized archive objects and connections only; unrecognized classes, fields and runtime-created UI remain unknown.", ...(graph.truncated ? [ "The decoded graph is incomplete (see coverage and limitations for the cause); page exhaustion does not establish absence of undecoded objects or connections.", ] : []), ], }; if (view.kind === "summary") return ok({ kind: "summary", view, ...shared, ...summarizeInterfaceBuilder(report, limitations.length), }); const document = view.collection === "nodes" || view.collection === "edges" ? view.document : undefined; if ( document !== undefined && !report.documents.some((row) => row.relative_path === document) ) return err( analysisViewInputError([ { path: ["view", "document"], reason: "invalid_value", message: "Select an exact documents[].relative_path from this decoded report.", }, ]), ); const selected = view.collection === "documents" ? pageRows(report.documents, view) : view.collection === "coverage" ? pageRows(graph.coverage, view) : view.collection === "nodes" ? selectDocumentRows(graph.nodes, view, document) : selectDocumentRows(graph.edges, view, document); return ok({ kind: "interface-builder", view, items: selected.rows, ...shared, source_truncated: graph.truncated, coverage: pageViewCoverage( view.offset, selected.rows.length, selected.total, ), }); };