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

+60
View File
@@ -138,6 +138,66 @@ func TestCacheStep_LazyDecodeReturnsFullStep(t *testing.T) {
}
}
func TestCacheOpen_ViolationMarkerMovesToCausingStep(t *testing.T) {
// The violation is detected at step 3 but its witness attributes it to
// step 2 (the step that spawned the next obligation). The action-list
// marker belongs on step 2; the full step 3 payload keeps the violation
// record itself.
root := t.TempDir()
startedAt := time.Now().UTC()
steps := []trace.Step{
{Index: 1, Timestamp: startedAt},
{Index: 2, Timestamp: startedAt.Add(time.Second)},
{
Index: 3,
Timestamp: startedAt.Add(2 * time.Second),
Violations: []string{"prop1"},
Witnesses: map[string]trace.Witness{"prop1": {Reason: "predicate false", Step: 2}},
},
}
writeRun(t, root, "r1", trace.Meta{StartedAt: startedAt, EndedAt: timePointer(startedAt.Add(3 * time.Second))}, steps)
cache := NewCache(root)
run, err := cache.Open("r1")
if err != nil {
t.Fatalf("Open: %v", err)
}
if !run.Steps[1].HasViolations {
t.Error("step 2 (causing step) should carry the violation marker")
}
if run.Steps[2].HasViolations {
t.Error("step 3 (detection step) should not carry the marker")
}
full, err := cache.Step(run, 3)
if err != nil {
t.Fatalf("Step(3): %v", err)
}
if len(full.Violations) != 1 || full.Violations[0] != "prop1" {
t.Errorf("step 3 payload violations = %v, want [prop1]", full.Violations)
}
}
func TestCacheOpen_ViolationWithoutWitnessStepKeepsDetectionStep(t *testing.T) {
// Traces written before witnesses carried a step field fall back to
// marking the detection step, the old behavior.
root := t.TempDir()
startedAt := time.Now().UTC()
steps := []trace.Step{
{Index: 1, Timestamp: startedAt},
{Index: 2, Timestamp: startedAt.Add(time.Second), Violations: []string{"prop1"}},
}
writeRun(t, root, "r1", trace.Meta{StartedAt: startedAt, EndedAt: timePointer(startedAt.Add(2 * time.Second))}, steps)
cache := NewCache(root)
run, err := cache.Open("r1")
if err != nil {
t.Fatalf("Open: %v", err)
}
if !run.Steps[1].HasViolations {
t.Error("step 2 should keep the marker when the witness has no step")
}
}
func TestDecodeStepSummary_ActionLabelPerKind(t *testing.T) {
cases := []struct {
line string