refactor(spec): one candidate producer over one target-eligibility rule

Both hosts routed verbs themselves and both policies enumerated their own
actions, and all four drifted. Web sent `swipes` to scrollable containers only,
so swipe-to-dismiss on a list row was reachable on native and unreachable on
web; the model policy folded gestures its own way and could not reach what the
seeded picker drew.

A host now reports facts about every element and never decides which verb may
act on it: targets.ts acceptsTarget owns that for both. pick.ts builtinCandidates
is the single enumeration, and the model policy reads it through
__sanderlingEnumerateBuiltin__ instead of reimplementing it in Go.

Gesture verbs change with it: scrolls stay vertical over scrollable containers,
swipes go free-form in all four directions from any element with real bounds.

Claude-Session: https://claude.ai/code/session_01Fj4wJUikdABuMQEETwW55J
This commit is contained in:
pj committed 2026-08-12 16:48:27 +05:30
1 parent ef04a6de9d
commit b5f64bf665
23 files changed
+1494 -594

No files matched your search

+11 -8
View File
@@ -48,22 +48,25 @@ func TestCrossRuntimeParity(t *testing.T) {
}
// installStubHost replaces the verifier's hierarchy-backed __sanderlingHost__
// with one returning a FIXED candidate list for every verb, keeping the seed the
// verifier was constructed with. It must run BEFORE Load, because the bundled
// goja runtime entry captures the host when the spec evaluates.
// with one returning a FIXED target list, keeping the seed the verifier was
// constructed with. Every fact is set so the shared eligibility rule admits all
// three targets for every verb, leaving the draw order as the only variable. It
// must run BEFORE Load, because the bundled goja runtime entry captures the host
// when the spec evaluates.
func installStubHost(t *testing.T, verifier *Verifier) {
t.Helper()
const stub = `
const candidates = [
{ x: 50, y: 60, selector: "id:alpha", width: 100, height: 40 },
{ x: 150, y: 160, selector: "id:beta", width: 120, height: 48 },
{ x: 250, y: 260, selector: "id:gamma", width: 80, height: 32 },
const facts = { clickable: true, enabled: true, editable: true, scrollable: true };
const targets = [
{ x: 50, y: 60, selector: "id:alpha", width: 100, height: 40, ...facts },
{ x: 150, y: 160, selector: "id:beta", width: 120, height: 48, ...facts },
{ x: 250, y: 260, selector: "id:gamma", width: 80, height: 32, ...facts },
];
const seedHi = globalThis.__sanderlingHost__.seedHi;
const seedLo = globalThis.__sanderlingHost__.seedLo;
globalThis.__sanderlingHost__ = {
platform: () => "android",
queryCandidates: () => candidates,
queryTargets: () => targets,
reportUnsupported: () => {},
seedHi,
seedLo,