mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
037709a52c
compareJavaScriptApplicationVersions built the change graph with createJavaScriptApplicationGraph, which validates it and re-derives every node, observation, edge and graph ID, then parsed the result with a schema that embeds the full graph schema, repeating all of that work. Seal the change graph with the owned immutable factory and accept that exact sealed graph in the result parse, as owned analysis results already do. Items, coverage and the public result schema are unchanged; output is byte-identical.
255 lines
8.4 KiB
TypeScript
255 lines
8.4 KiB
TypeScript
import { z } from "zod";
|
|
|
|
import { emptyArraySchema } from "../emptyArraySchema.js";
|
|
import { evidenceSchema } from "../evidence.js";
|
|
import {
|
|
isValidatedImmutableJavaScriptApplicationGraph,
|
|
javascriptApplicationGraphSchema,
|
|
type JavaScriptApplicationGraph,
|
|
} 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);
|
|
|
|
/** Check paired version identities and per-side native identity uniqueness. */
|
|
const validateApplicationVersionIdentities = (
|
|
input: {
|
|
readonly left: { readonly evidence_id: string };
|
|
readonly right: { readonly evidence_id: string };
|
|
readonly left_native_observations: readonly {
|
|
readonly evidence_id: string;
|
|
}[];
|
|
readonly right_native_observations: readonly {
|
|
readonly evidence_id: string;
|
|
}[];
|
|
},
|
|
context: z.RefinementCtx,
|
|
): void => {
|
|
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 evidenceIdentitySchema = z.object({ evidence_id: evidenceIdSchema });
|
|
|
|
/** Validate paired version identities without capturing whole analysis graphs. */
|
|
export const applicationVersionIdentitiesSchema = z
|
|
.object({
|
|
left: evidenceIdentitySchema,
|
|
right: evidenceIdentitySchema,
|
|
left_native_observations: z.array(evidenceIdentitySchema),
|
|
right_native_observations: z.array(evidenceIdentitySchema),
|
|
})
|
|
.superRefine(validateApplicationVersionIdentities);
|
|
|
|
/** 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(validateApplicationVersionIdentities);
|
|
|
|
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",
|
|
});
|
|
});
|
|
|
|
/** Reuse the exact owned change graph while validating the rest of the result. */
|
|
export const ownedApplicationVersionComparisonResultSchema =
|
|
applicationVersionComparisonResultSchema.safeExtend({
|
|
graph: z.custom<JavaScriptApplicationGraph>(
|
|
isValidatedImmutableJavaScriptApplicationGraph,
|
|
),
|
|
});
|
|
|
|
export type ApplicationVersionComparisonItem = z.infer<
|
|
typeof comparisonItemSchema
|
|
>;
|
|
export type ApplicationVersionComparisonResult = z.infer<
|
|
typeof applicationVersionComparisonResultSchema
|
|
>;
|