Attribute violations to the causing step and render witness evidence (#59)

* feat(ltl): attribute violations to the obligation origin step

* feat(verifier): label evaluator observations with the runner step index

* feat(trace): carry the causing step in violation witnesses and summary

* feat(replay): move the violation marker to the causing step

* feat(replay-ui): render witness evidence in the violations panel

* feat(replay-ui): wire witnesses and step jump into violation panels

* fix(ltl): treat next obligations as vacuous at run end

* fix(runner): give the finalize trace record its own step index
This commit is contained in:
pj authored and GitHub committed 2026-06-05 23:46:36 +05:30
1 parent 19a470121f
commit 9958b0ddc8
15 files changed
+719 -85

No files matched your search

+69 -1
View File
@@ -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<string, ResidualNode>;
witnesses?: Record<string, Witness>;
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 (
<div className="violations-panel-witness">
{witness.reason ? (
<div className="violations-panel-witness-line">
<span className="violations-panel-witness-key">
{witness.is_error ? "error" : "reason"}
</span>
<span className="violations-panel-witness-value">{witness.reason}</span>
</div>
) : null}
{witness.step ? (
<div className="violations-panel-witness-line">
<span className="violations-panel-witness-key">caused at</span>
{onJumpToStep ? (
<button
type="button"
className="violations-panel-witness-step"
onClick={() => onJumpToStep(witness.step as number)}
>
step {witness.step}
</button>
) : (
<span className="violations-panel-witness-value">step {witness.step}</span>
)}
</div>
) : null}
{evidence.length > 0 ? (
<details className="violations-panel-residual" open={open}>
<summary>witness</summary>
<dl className="violations-panel-witness-evidence">
{evidence.map(([name, value]) => (
<div key={name} className="violations-panel-witness-line">
<dt className="violations-panel-witness-key">{name}</dt>
<dd className="violations-panel-witness-value">{formatValue(value)}</dd>
</div>
))}
</dl>
</details>
) : null}
</div>
);
}
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({
<ul className="violations-panel-list">
{rows.map(({ name, status }) => {
const residual = residuals?.[name];
const witness = status === "violated" ? witnesses?.[name] : undefined;
return (
<li key={name} className="violations-panel-row" data-status={status}>
<div className="violations-panel-row-head">
@@ -96,6 +161,9 @@ export default function ViolationsPanel({
</span>
<span className="violations-panel-name">{name}</span>
</div>
{witness ? (
<WitnessView witness={witness} onJumpToStep={onJumpToStep} open={violationsOnly} />
) : null}
{residual ? (
<details className="violations-panel-residual">
<summary>residual</summary>