Files
sanderling/pkg/spec/test/parity-harness.ts
T
pj b5f64bf665 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
2026-08-12 16:48:27 +05:30

57 lines
2.6 KiB
TypeScript

// Shared cross-runtime parity scenario: the FIXED seed, FIXED target list,
// and FIXED action root that both the node test (parity.test.ts) and the goja
// test (internal/verifier/parity_test.go) drive. Each side runs the SAME
// pick.ts over the SAME Pcg and asserts the SAME committed golden
// (fixtures/parity-golden.json); matching one golden on both sides proves the
// two engines agree without either invoking the other.
//
// The target ORDER and the per-tick PCG draw order are the parity contract.
// The weighted root mixes a tap branch (1 candidate draw) with a typing branch
// (1 candidate draw + 1 corpus draw), so a tick exercises weighted selection, a
// builtin, and the input corpus together; reordering candidates or adding or
// dropping a draw on either side shifts the stream and fails the golden.
import { Pcg } from "../src/pcg.ts";
import { nextAction } from "../src/pick.ts";
import { serializeAction, type SerializedAction } from "../src/runtime-entry.ts";
import { taps, typing, weighted } from "../src/actions.ts";
import type { GeneratorNode, Host, TargetElement } from "../src/action-tree.ts";
export const PARITY_SEED_HI = 0x9e3779b97f4a7c15n;
export const PARITY_STEPS = 20;
// Every fact is set so the shared eligibility rule admits all three targets for
// every verb, leaving the draw order as the only variable.
const EVERY_FACT = { clickable: true, enabled: true, editable: true, scrollable: true };
export const PARITY_TARGETS: TargetElement[] = [
{ x: 50, y: 60, selector: "id:alpha", width: 100, height: 40, ...EVERY_FACT },
{ x: 150, y: 160, selector: "id:beta", width: 120, height: 48, ...EVERY_FACT },
{ x: 250, y: 260, selector: "id:gamma", width: 80, height: 32, ...EVERY_FACT },
];
// A 3:1 weighted split over taps and typing. The stub host offers the same
// target list to every verb so the only variables are the draw order and the JS
// engine's number/bigint behavior.
export const PARITY_ROOT: GeneratorNode = weighted([3, taps], [1, typing]);
const HOST: Host = {
platform: () => "android",
queryTargets: () => PARITY_TARGETS,
reportUnsupported: () => {},
seedHi: () => PARITY_SEED_HI,
seedLo: () => 0n,
};
// runParity emits the parity scenario's action stream from a fresh Pcg. It
// drives pick.ts directly (not installRuntime, whose globals are locked once)
// so the caller can run it repeatedly to assert determinism.
export function runParity(): (SerializedAction | null)[] {
const rng = new Pcg(PARITY_SEED_HI, 0n);
const stream: (SerializedAction | null)[] = [];
for (let i = 0; i < PARITY_STEPS; i++) {
stream.push(serializeAction(nextAction(PARITY_ROOT, rng, HOST)));
}
return stream;
}