feat(verifier): name extractors for diff surfacing

bindExtract accepts an optional name argument; falls back to
extractor_N when omitted. The name is stored on extractorState
alongside prev/curr value caches that the next change will use to
emit per-step diffs.
This commit is contained in:
pj committed 2026-05-30 21:54:30 +05:30
1 parent cbef329cdd
commit 1b35e5a6a6
2 files changed
+39 -3

No files matched your search

+19 -3
View File
@@ -10,6 +10,12 @@ import (
type extractorState struct {
getter goja.Callable
handle *goja.Object
name string
// prev/curr cache the JSON-encoded extractor values from the prior and
// current PushSnapshot, used by ChangedExtractors to surface per-step
// diffs in the trace.
prev []byte
curr []byte
}
type formulaState struct {
@@ -139,19 +145,29 @@ func (v *Verifier) installRuntimeBindings() error {
}
func (v *Verifier) bindExtract(call goja.FunctionCall) goja.Value {
if len(call.Arguments) != 1 {
panic(v.runtime.NewTypeError("extract requires exactly one argument"))
if len(call.Arguments) < 1 || len(call.Arguments) > 2 {
panic(v.runtime.NewTypeError("extract requires (getter) or (getter, name)"))
}
getter, ok := goja.AssertFunction(call.Arguments[0])
if !ok {
panic(v.runtime.NewTypeError("extract argument must be a function"))
}
name := ""
if len(call.Arguments) == 2 {
arg := call.Arguments[1]
if !goja.IsUndefined(arg) && !goja.IsNull(arg) {
name = arg.String()
}
}
if name == "" {
name = fmt.Sprintf("extractor_%d", len(v.extractors))
}
handle := v.runtime.NewObject()
_ = handle.Set("current", goja.Undefined())
_ = handle.Set("previous", goja.Undefined())
v.extractors = append(v.extractors, &extractorState{getter: getter, handle: handle})
v.extractors = append(v.extractors, &extractorState{getter: getter, handle: handle, name: name})
return handle
}
+20
View File
@@ -894,6 +894,26 @@ func TestSelectorStringFromJS_CanonicalGrammar(t *testing.T) {
}
}
// TestExtract_DefaultsAndNamedNames verifies bindExtract assigns a fallback
// `extractor_N` name when no name is supplied and respects an explicit one.
func TestExtract_DefaultsAndNamedNames(t *testing.T) {
verifier := newVerifier(t)
mustLoad(t, verifier, `
__sanderling__.extract(state => 1);
__sanderling__.extract(state => 2, "ledgerRows");
__sanderling__.extract(state => 3);
`)
if len(verifier.extractors) != 3 {
t.Fatalf("extractors registered: got %d, want 3", len(verifier.extractors))
}
want := []string{"extractor_0", "ledgerRows", "extractor_2"}
for i, name := range want {
if got := verifier.extractors[i].name; got != name {
t.Errorf("extractor %d name: got %q, want %q", i, got, name)
}
}
}
// TestSelectorStringFromJS_NullEmpty verifies that nil/undefined args produce
// an empty string instead of "null"/"undefined" garbage.
func TestSelectorStringFromJS_NullEmpty(t *testing.T) {