Files
sanderling/cmd/internal-tools/oracle-reduction/load.go
T
pj a45ba76d8e feat(oracle-reduction): replay stored traces under four reduced oracles
re-evaluates each trace offline under the full engine, a crash-only detector, a single-state check and a single-step property triple, and reports what each refutes: the oracles vary while the traces stay fixed, which separates a defect an oracle cannot express from one an explorer never reached. a disagreement with the verdicts a run recorded exits nonzero rather than being counted as a finding.
2026-08-16 17:45:41 +05:30

224 lines
6.3 KiB
Go

package main
import (
"bufio"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"github.com/priyanshujain/sanderling/internal/trace"
"github.com/priyanshujain/sanderling/internal/verifier"
)
const maxStepBytes = 64 * 1024 * 1024
// loadedRun is one run directory: its meta, the steps in file order, and the
// synthetic end-of-run record the runner writes when Finalize convicted a
// liveness obligation.
type loadedRun struct {
Directory string
Meta trace.Meta
Steps []trace.Step
Finalize *trace.Step
}
// loadRun reads a run directory and refuses anything E3 cannot replay. A step
// written before the format change carries no element depths, so its hierarchy
// decodes with a nil root and every selector resolves to nothing: the refusal
// has to name the version rather than let the replay report an empty screen.
func loadRun(directory string) (loadedRun, error) {
metaBody, err := os.ReadFile(filepath.Join(directory, "meta.json"))
if err != nil {
return loadedRun{}, fmt.Errorf("read meta: %w", err)
}
var meta trace.Meta
if err := json.Unmarshal(metaBody, &meta); err != nil {
return loadedRun{}, fmt.Errorf("decode meta: %w", err)
}
file, err := os.Open(filepath.Join(directory, "trace.jsonl"))
if err != nil {
return loadedRun{}, fmt.Errorf("open trace: %w", err)
}
defer file.Close()
run := loadedRun{Directory: directory, Meta: meta}
scanner := bufio.NewScanner(file)
scanner.Buffer(make([]byte, 0, 1024*1024), maxStepBytes)
line := 0
for scanner.Scan() {
line++
if len(scanner.Bytes()) == 0 {
continue
}
var step trace.Step
if err := json.Unmarshal(scanner.Bytes(), &step); err != nil {
return loadedRun{}, fmt.Errorf(
"decode step on line %d: %w",
line,
err,
)
}
if step.TraceVersion != trace.TraceVersion {
return loadedRun{}, fmt.Errorf(
"step %d is trace_version %d, and E3 replays version %d only: "+
"an older step stores no element depths, so its hierarchy decodes "+
"with a nil root and every selector resolves to nothing on replay",
step.Index, step.TraceVersion, trace.TraceVersion)
}
run.Steps = append(run.Steps, step)
}
if err := scanner.Err(); err != nil {
return loadedRun{}, fmt.Errorf("read trace: %w", err)
}
if len(run.Steps) == 0 {
return loadedRun{}, fmt.Errorf("trace has no steps")
}
if last := run.Steps[len(run.Steps)-1]; len(run.Steps) > 1 &&
last.Hierarchy == nil &&
len(last.Violations) > 0 {
run.Finalize = &last
run.Steps = run.Steps[:len(run.Steps)-1]
}
if meta.Seed == 0 {
return loadedRun{}, fmt.Errorf(
"meta records no seed, so the run's bundle cannot be reproduced",
)
}
return run, nil
}
// finalizeIndex is the step index the runner gives its end-of-run record: one
// past the last step it wrote.
func (r loadedRun) finalizeIndex() int {
return r.Steps[len(r.Steps)-1].Index + 1
}
// extractorFold reconstructs every extractor's value at each step from the
// recorded per-step diffs. A run's trace stores what changed, so the value an
// extractor held at a step is the last change at or before it; an extractor
// that never changed held JSON null throughout, which is what an unwritten
// diff means.
func extractorFold(
steps []trace.Step,
names []string,
) ([]map[int]json.RawMessage, error) {
index := make(map[string]int, len(names))
for position, name := range names {
index[name] = position
}
current := make(map[int]json.RawMessage, len(names))
for position := range names {
current[position] = json.RawMessage("null")
}
folded := make([]map[int]json.RawMessage, len(steps))
for step := range steps {
for name, change := range steps[step].ExtractorChanges {
position, ok := index[name]
if !ok {
return nil, fmt.Errorf(
"step %d records extractor %q, which the spec does not register; "+
"the trace and the spec are not the same bundle",
steps[step].Index,
name,
)
}
current[position] = change.Curr
}
snapshot := make(map[int]json.RawMessage, len(current))
for position, value := range current {
snapshot[position] = value
}
folded[step] = snapshot
}
return folded, nil
}
// lastActionFor rebuilds the action the runner had applied before the next
// step observed. An action the runner chose but never dispatched left
// state.lastAction null, and the recorded skip reason is what says so.
func lastActionFor(step trace.Step) *verifier.Action {
if step.NextAction == nil || step.ActionSkipped != "" {
return nil
}
recorded := *step.NextAction
action := verifier.Action{
Kind: verifier.ActionKind(recorded.Kind),
On: recorded.Selector,
Text: recorded.Text,
X: recorded.X,
Y: recorded.Y,
FromX: recorded.FromX,
FromY: recorded.FromY,
ToX: recorded.ToX,
ToY: recorded.ToY,
Key: recorded.Key,
DurationMillis: recorded.DurationMillis,
}
return &action
}
func traceLogs(entries []trace.LogEntry) []verifier.LogEntry {
if len(entries) == 0 {
return nil
}
logs := make([]verifier.LogEntry, 0, len(entries))
for _, entry := range entries {
logs = append(logs, verifier.LogEntry{
UnixMillis: entry.UnixMillis,
Level: entry.Level,
Tag: entry.Tag,
Message: entry.Message,
})
}
return logs
}
func traceExceptions(entries []trace.Exception) []verifier.Exception {
if len(entries) == 0 {
return nil
}
exceptions := make([]verifier.Exception, 0, len(entries))
for _, entry := range entries {
exceptions = append(exceptions, verifier.Exception{
Class: entry.Class,
Message: entry.Message,
StackTrace: entry.StackTrace,
UnixMillis: entry.UnixMillis,
})
}
return exceptions
}
// discoverRuns finds every run directory at or below root, a run directory
// being one holding both meta.json and trace.jsonl.
func discoverRuns(root string) ([]string, error) {
var directories []string
err := filepath.Walk(
root,
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
return nil
}
if _, statErr := os.Stat(filepath.Join(path, "trace.jsonl")); statErr != nil {
return nil
}
if _, statErr := os.Stat(filepath.Join(path, "meta.json")); statErr != nil {
return nil
}
directories = append(directories, path)
return nil
},
)
if err != nil {
return nil, err
}
sort.Strings(directories)
return directories, nil
}