import { z } from "zod"; import { emptyArraySchema } from "../emptyArraySchema.js"; import { evidenceSchema } from "../evidence.js"; import { javascriptApplicationGraphSchema } from "./javascriptApplicationGraph.js"; import { JAVASCRIPT_APPLICATION_NODE_KINDS } from "./javascriptApplicationGraphSchemas.js"; import { prefixedDigestSchema } from "../digests.js"; const evidenceIdSchema = prefixedDigestSchema("ev"); const nodeIdSchema = prefixedDigestSchema("jag_node"); const textSchema = z.string().min(1); /** Two authenticated application versions and their native observations. */ export const compareApplicationVersionsInputSchema = z .strictObject({ left: evidenceSchema, right: evidenceSchema, left_native_observations: z.array(evidenceSchema).default([]), right_native_observations: z.array(evidenceSchema).default([]), }) .superRefine((input, context) => { if (input.left.evidence_id === input.right.evidence_id) context.addIssue({ code: "custom", path: ["right"], message: "Application version Evidence must be distinct", }); for (const side of [ "left_native_observations", "right_native_observations", ] as const) { const ids = input[side].map(({ evidence_id: id }) => id); if (new Set(ids).size !== ids.length) context.addIssue({ code: "custom", path: [side], message: "Native observations must be unique on each side", }); } }); const candidateNodesSchema = z.tuple([nodeIdSchema]).rest(nodeIdSchema); const exactMatchSchema = z.strictObject({ status: z.literal("matched"), basis: z.enum([ "exact-node-identity", "exact-content-digest", "exact-module-source-digest", ]), confidence: z.literal("exact"), candidate_left_node_ids: emptyArraySchema, candidate_right_node_ids: emptyArraySchema, }); const sourceMapMatchSchema = z.strictObject({ status: z.literal("matched"), basis: z.literal("source-map-identity"), confidence: z.literal("high"), candidate_left_node_ids: emptyArraySchema, candidate_right_node_ids: emptyArraySchema, }); const inferredMatchSchema = z.strictObject({ status: z.literal("matched"), basis: z.enum(["structural-fingerprint", "semantic-key"]), confidence: z.literal("medium"), candidate_left_node_ids: emptyArraySchema, candidate_right_node_ids: emptyArraySchema, }); const matchedItemMatchSchema = z.union([ exactMatchSchema, sourceMapMatchSchema, inferredMatchSchema, ]); const unmatchedItemMatchSchema = z.strictObject({ status: z.literal("unmatched"), basis: z.literal("none"), confidence: z.literal("unknown"), candidate_left_node_ids: emptyArraySchema, candidate_right_node_ids: emptyArraySchema, }); const leftAmbiguousMatchSchema = z.strictObject({ status: z.literal("ambiguous"), basis: z.literal("none"), confidence: z.literal("unknown"), candidate_left_node_ids: emptyArraySchema, candidate_right_node_ids: candidateNodesSchema, }); const rightAmbiguousMatchSchema = z.strictObject({ status: z.literal("ambiguous"), basis: z.literal("none"), confidence: z.literal("unknown"), candidate_left_node_ids: candidateNodesSchema, candidate_right_node_ids: emptyArraySchema, }); const comparisonItemContextShape = { item_id: prefixedDigestSchema("javc_item"), node_kind: z.enum(JAVASCRIPT_APPLICATION_NODE_KINDS), dimensions: z.array( z.enum([ "content", "location", "properties", "relationships", "availability", "coverage", ]), ), evidence_links: z.array(evidenceIdSchema).min(2), limitations: z.array(textSchema), }; const comparisonItemSchema = z.union([ z.strictObject({ ...comparisonItemContextShape, status: z.enum(["unchanged", "changed", "unknown"]), left_node_id: nodeIdSchema, right_node_id: nodeIdSchema, match: matchedItemMatchSchema, }), z.strictObject({ ...comparisonItemContextShape, status: z.enum(["removed", "unknown"]), left_node_id: nodeIdSchema, right_node_id: z.null(), match: unmatchedItemMatchSchema, }), z.strictObject({ ...comparisonItemContextShape, status: z.enum(["added", "unknown"]), left_node_id: z.null(), right_node_id: nodeIdSchema, match: unmatchedItemMatchSchema, }), z.strictObject({ ...comparisonItemContextShape, status: z.literal("unknown"), left_node_id: nodeIdSchema, right_node_id: z.null(), match: leftAmbiguousMatchSchema, }), z.strictObject({ ...comparisonItemContextShape, status: z.literal("unknown"), left_node_id: z.null(), right_node_id: nodeIdSchema, match: rightAmbiguousMatchSchema, }), ]); const comparisonCoverageSchema = z.strictObject({ left_graph_status: z.enum(["complete", "partial", "unknown", "unavailable"]), right_graph_status: z.enum(["complete", "partial", "unknown", "unavailable"]), left_graph_omitted_count: z.number().int().min(0).nullable(), right_graph_omitted_count: z.number().int().min(0).nullable(), status: z.enum(["complete-within-inputs", "partial", "truncated"]), }); /** Tiered module/entity matching plus its complete cross-version change graph. */ export const applicationVersionComparisonResultSchema = z .strictObject({ comparison_id: prefixedDigestSchema("javc"), left: z.strictObject({ evidence_id: evidenceIdSchema, graph_id: prefixedDigestSchema("jag"), root_artifact_sha256: z.string().regex(/^[a-f0-9]{64}$/u), }), right: z.strictObject({ evidence_id: evidenceIdSchema, graph_id: prefixedDigestSchema("jag"), root_artifact_sha256: z.string().regex(/^[a-f0-9]{64}$/u), }), summary: z.strictObject({ unchanged: z.number().int().min(0), added: z.number().int().min(0), removed: z.number().int().min(0), changed: z.number().int().min(0), unknown: z.number().int().min(0), }), matching: z.strictObject({ exact_node_identity: z.number().int().min(0), exact_content_digest: z.number().int().min(0), exact_module_source_digest: z.number().int().min(0), source_map_identity: z.number().int().min(0), structural_fingerprint: z.number().int().min(0), semantic_key: z.number().int().min(0), ambiguous: z.number().int().min(0), unmatched: z.number().int().min(0), }), items: z.array(comparisonItemSchema), graph: javascriptApplicationGraphSchema, coverage: comparisonCoverageSchema, evidence_links: z.array(evidenceIdSchema).min(2), limitations: z.array(textSchema), }) .superRefine((result, context) => { const sourceGraphsComplete = result.coverage.left_graph_status === "complete" && result.coverage.right_graph_status === "complete"; if ( (result.coverage.status === "complete-within-inputs") !== sourceGraphsComplete ) context.addIssue({ code: "custom", path: ["coverage", "status"], message: "Comparison coverage must match source graph completeness", }); }); export type ApplicationVersionComparisonItem = z.infer< typeof comparisonItemSchema >; export type ApplicationVersionComparisonResult = z.infer< typeof applicationVersionComparisonResultSchema >;