fix(runner): record a witness's detection step in the trace

Claude-Session: https://claude.ai/code/session_01Fj4wJUikdABuMQEETwW55J
This commit is contained in:
pj committed 2026-08-12 16:47:25 +05:30
1 parent 16134f2584
commit eb492e53de
3 files changed
+21 -7

No files matched your search

+9 -2
View File
@@ -1006,7 +1006,9 @@ func violationRecords(properties []string, witnesses map[string]trace.Witness, d
// collectWitnesses gathers the violation witness for each newly-violated
// property, logs its cause, and returns them keyed by property name for the
// trace. Properties without a captured witness are skipped.
// trace. Properties without a captured witness are skipped. stepIndex is the
// trace line the witness lands on, and stands in as the detection step for a
// verifier that observed no labeled step (a run-end finalize).
func collectWitnesses(verifierInstance *verifier.Verifier, properties []string, logger *slog.Logger, stepIndex int) map[string]trace.Witness {
if len(properties) == 0 {
return nil
@@ -1017,13 +1019,18 @@ func collectWitnesses(verifierInstance *verifier.Verifier, properties []string,
if witness == nil {
continue
}
detectedStep := witness.DetectedStep
if detectedStep == 0 {
detectedStep = stepIndex
}
logger.Warn("property violated",
"step", witness.Step, "detected_step", stepIndex,
"step", witness.Step, "detected_step", detectedStep,
"property", name, "reason", witness.Reason, "error", witness.IsError)
witnesses[name] = trace.Witness{
Reason: witness.Reason,
IsError: witness.IsError,
Step: witness.Step,
DetectedStep: detectedStep,
Extractors: witness.Extractors,
}
}
+5
View File
@@ -363,6 +363,11 @@ globalThis.actions = actions(() => []);
if witness.Step != 2 {
t.Errorf("witness step: got %d, want 2 (causing step)", witness.Step)
}
// The two indices the witness spans are recorded separately: the
// extractor snapshot it carries is step 3's state, not step 2's.
if witness.DetectedStep != 3 {
t.Errorf("witness detected step: got %d, want 3", witness.DetectedStep)
}
}
if err := scanner.Err(); err != nil {
t.Fatalf("scan trace: %v", err)
+7 -5
View File
@@ -40,16 +40,18 @@ type Step struct {
Witnesses map[string]Witness `json:"witnesses,omitempty"`
}
// Witness is the trace-side record of a property violation: why it fired and a
// snapshot of every extractor's value at the violating step.
// Witness is the trace-side record of a property violation: why it fired, the
// two steps a deferred obligation spans, and the extractor values behind it.
type Witness struct {
Reason string `json:"reason,omitempty"`
IsError bool `json:"is_error,omitempty"`
// Step is the step the failed obligation originated at: the step that
// caused the violation. For a deferred obligation (a next, an eventually)
// this is earlier than the step whose record carries the witness, which is
// where the failure was detected.
// armed it. For a deferred obligation (a next, an eventually) this is
// earlier than the step at which the failure was detected.
Step int `json:"step,omitempty"`
// DetectedStep is the observation whose evaluation produced the violation.
// Extractors is that step's state, not Step's.
DetectedStep int `json:"detected_step,omitempty"`
Extractors map[string]json.RawMessage `json:"extractors,omitempty"`
}