From 3334ee957be593e3cd94fa3635ed21432548053e Mon Sep 17 00:00:00 2001 From: PJ Date: Fri, 5 Jun 2026 22:41:54 +0530 Subject: [PATCH] feat(replay-ui): render witness evidence in the violations panel --- replay-ui/src/panels/ViolationsPanel.css | 51 +++++++++++++++++ replay-ui/src/panels/ViolationsPanel.tsx | 70 +++++++++++++++++++++++- replay-ui/src/types.ts | 11 ++++ 3 files changed, 131 insertions(+), 1 deletion(-) diff --git a/replay-ui/src/panels/ViolationsPanel.css b/replay-ui/src/panels/ViolationsPanel.css index 0b3c731..3ab7935 100644 --- a/replay-ui/src/panels/ViolationsPanel.css +++ b/replay-ui/src/panels/ViolationsPanel.css @@ -97,6 +97,57 @@ font-size: 13px; } +.violations-panel-witness { + margin-top: 6px; + display: flex; + flex-direction: column; + gap: 4px; + font-size: 12px; +} + +.violations-panel-witness-line { + display: flex; + align-items: baseline; + gap: 8px; + margin: 0; +} + +.violations-panel-witness-key { + flex-shrink: 0; + min-width: 96px; + color: var(--text-muted); + font-size: 11px; + text-transform: lowercase; +} + +.violations-panel-witness-value { + word-break: break-all; + white-space: pre-wrap; + margin: 0; +} + +.violations-panel-witness-step { + font-family: var(--font-mono); + font-size: 11px; + padding: 1px 6px; + background: var(--surface); + color: var(--text-primary); + border: 1px solid var(--border); + border-radius: 3px; + cursor: pointer; +} + +.violations-panel-witness-step:hover { + border-color: var(--border-strong); +} + +.violations-panel-witness-evidence { + display: flex; + flex-direction: column; + gap: 3px; + margin: 0; +} + .violations-panel-residual { margin-top: 6px; font-size: 12px; diff --git a/replay-ui/src/panels/ViolationsPanel.tsx b/replay-ui/src/panels/ViolationsPanel.tsx index b2d457c..8266ffc 100644 --- a/replay-ui/src/panels/ViolationsPanel.tsx +++ b/replay-ui/src/panels/ViolationsPanel.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import type { ResidualNode } from "../types"; +import type { ResidualNode, Witness } from "../types"; import ResidualNodeView from "../components/ResidualNode"; import "./ViolationsPanel.css"; @@ -7,8 +7,10 @@ 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; } @@ -36,12 +38,74 @@ function statusFor( return "pending"; } +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} + {evidence.length > 0 ? ( +
+ witness +
+ {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]); @@ -84,6 +148,7 @@ export default function ViolationsPanel({
    {rows.map(({ name, status }) => { const residual = residuals?.[name]; + const witness = status === "violated" ? witnesses?.[name] : undefined; return (
  • @@ -96,6 +161,9 @@ export default function ViolationsPanel({ {name}
    + {witness ? ( + + ) : null} {residual ? (
    residual diff --git a/replay-ui/src/types.ts b/replay-ui/src/types.ts index 773d18b..dfa8708 100644 --- a/replay-ui/src/types.ts +++ b/replay-ui/src/types.ts @@ -121,6 +121,16 @@ export interface ExtractorChange { curr: unknown; } +export interface Witness { + reason?: string; + is_error?: boolean; + // step is the step the failed obligation originated at: the causing step, + // which for deferred obligations (next, eventually) is earlier than the + // step whose record carries the witness. + step?: number; + extractors?: Record; +} + export interface Step { step: number; timestamp: string; @@ -134,4 +144,5 @@ export interface Step { residuals?: Record; metrics?: Metrics; extractor_changes?: Record; + witnesses?: Record; }