import { useMemo } from "react"; import type { ResidualNode, Witness } from "../types"; import { STATUS_ORDER, statusFor } from "../lib/property-status"; import ResidualNodeView from "../components/ResidualNode"; import "./ViolationsPanel.css"; export interface ViolationsPanelProps { propertyNames: string[]; violations: string[]; residuals?: Record; witnesses?: Record; onJumpToFirstViolation: () => void; hasFirstViolation: boolean; onJumpToStep?: (step: number) => void; /** When true, only render violated rows and hide the header button row. */ violationsOnly?: boolean; } function formatValue(value: unknown): string { const encoded = JSON.stringify(value); return encoded === undefined ? String(value) : encoded; } function WitnessView({ witness, onJumpToStep, open, }: { witness: Witness; onJumpToStep?: (step: number) => void; open: boolean; }) { const evidence = Object.entries(witness.extractors ?? {}) .filter(([, value]) => value !== null && value !== undefined) .sort(([a], [b]) => a.localeCompare(b)); return (
{witness.reason ? (
{witness.is_error ? "error" : "reason"} {witness.reason}
) : null} {witness.step ? (
caused at {onJumpToStep ? ( ) : ( step {witness.step} )}
) : null} {witness.detected_step && witness.detected_step !== witness.step ? (
detected at {onJumpToStep ? ( ) : ( step {witness.detected_step} )}
) : null} {evidence.length > 0 ? (
witness {witness.detected_step ? ` (state at step ${witness.detected_step})` : ""}
{evidence.map(([name, value]) => (
{name}
{formatValue(value)}
))}
) : null}
); } export default function ViolationsPanel({ propertyNames, violations, residuals, witnesses, onJumpToFirstViolation, hasFirstViolation, onJumpToStep, violationsOnly = false, }: ViolationsPanelProps) { const violationSet = useMemo(() => new Set(violations), [violations]); const rows = useMemo(() => { const sorted = [...propertyNames].sort((a, b) => a.localeCompare(b)); const all = sorted.map((name) => ({ name, status: statusFor(name, violationSet, residuals), })); const filtered = violationsOnly ? all.filter((row) => row.status === "violated") : all; return filtered.sort((a, b) => { const groupDelta = STATUS_ORDER[a.status] - STATUS_ORDER[b.status]; if (groupDelta !== 0) { return groupDelta; } return a.name.localeCompare(b.name); }); }, [propertyNames, violationSet, residuals, violationsOnly]); if (violationsOnly && rows.length === 0) { return
no violations
; } return (
row.status === "violated").length} > {violationsOnly ? null : (

properties

)}
    {rows.map(({ name, status }) => { const residual = residuals?.[name]; const witness = status === "violated" ? witnesses?.[name] : undefined; return (
  • {status} {name}
    {witness ? ( ) : null} {residual ? (
    residual
    ) : null}
  • ); })}
); }