From e44ad56749121a14f139dad46ae000bf1518bb58 Mon Sep 17 00:00:00 2001 From: PJ Date: Sat, 30 May 2026 21:58:08 +0530 Subject: [PATCH] feat(trace): emit extractor_changes per step Add ExtractorChanges to trace.Step and a runner helper that converts the verifier's diff map into the trace shape. The inspect UI keys its violation breadcrumbs off this field. --- internal/runner/runner.go | 31 +++++++++++++++++++++++-------- internal/trace/writer.go | 30 ++++++++++++++++++++---------- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/internal/runner/runner.go b/internal/runner/runner.go index 7799676..725ca25 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -187,14 +187,15 @@ func Run(ctx context.Context, options Options) (Summary, error) { } step := trace.Step{ - Index: stepIndex, - Timestamp: stepStart, - Screen: screen, - Action: traceAction, - Violations: violations, - Hierarchy: tree, - Residuals: residuals, - Metrics: metrics, + Index: stepIndex, + Timestamp: stepStart, + Screen: screen, + Action: traceAction, + Violations: violations, + Hierarchy: tree, + Residuals: residuals, + Metrics: metrics, + ExtractorChanges: encodeExtractorChanges(options.Verifier.ChangedExtractors()), } if err := options.TraceWriter.WriteStep(step); err != nil { return summary, fmt.Errorf("step %d trace: %w", stepIndex, err) @@ -554,6 +555,20 @@ func captureScreenshot(ctx context.Context, options Options, logger *slog.Logger } } +func encodeExtractorChanges(changes map[string]verifier.ExtractorChange) map[string]trace.ExtractorChange { + if len(changes) == 0 { + return nil + } + out := make(map[string]trace.ExtractorChange, len(changes)) + for name, change := range changes { + out[name] = trace.ExtractorChange{ + Prev: json.RawMessage(change.Prev), + Curr: json.RawMessage(change.Curr), + } + } + return out +} + func encodeResiduals(residuals map[string]ltl.Formula) (map[string]json.RawMessage, error) { if len(residuals) == 0 { return nil, nil diff --git a/internal/trace/writer.go b/internal/trace/writer.go index ea1f14c..c73d081 100644 --- a/internal/trace/writer.go +++ b/internal/trace/writer.go @@ -13,16 +13,26 @@ import ( ) type Step struct { - Index int `json:"step"` - Timestamp time.Time `json:"timestamp"` - Screen string `json:"screen,omitempty"` - Snapshots map[string]json.RawMessage `json:"snapshots,omitempty"` - Action *Action `json:"action,omitempty"` - Exceptions []Exception `json:"exceptions,omitempty"` - Violations []string `json:"violations,omitempty"` - Hierarchy *hierarchy.Tree `json:"hierarchy,omitempty"` - Residuals map[string]json.RawMessage `json:"residuals,omitempty"` - Metrics *Metrics `json:"metrics,omitempty"` + Index int `json:"step"` + Timestamp time.Time `json:"timestamp"` + Screen string `json:"screen,omitempty"` + Snapshots map[string]json.RawMessage `json:"snapshots,omitempty"` + Action *Action `json:"action,omitempty"` + Exceptions []Exception `json:"exceptions,omitempty"` + Violations []string `json:"violations,omitempty"` + Hierarchy *hierarchy.Tree `json:"hierarchy,omitempty"` + Residuals map[string]json.RawMessage `json:"residuals,omitempty"` + Metrics *Metrics `json:"metrics,omitempty"` + ExtractorChanges map[string]ExtractorChange `json:"extractor_changes,omitempty"` +} + +// ExtractorChange records the prev/curr JSON values of an extractor whose +// observation differed between two consecutive steps. Surfaced under +// violation rows in the inspect UI as a "what changed at this step" +// breadcrumb. +type ExtractorChange struct { + Prev json.RawMessage `json:"prev"` + Curr json.RawMessage `json:"curr"` } type Metrics struct {