MiniWeb-demo / src /miniwebgen /sampler.py
sparklabuofu's picture
Update the demo
22de727 verified
Raw History Blame Contribute Delete
19 kB
"""Stage 2 β€” parameter sampler. Programmatically picks (site, target element, argument,
start state) tuples; diversity is enforced HERE, not by the suggester.
* train sites only, and only sites whose macro_locations entry lists the macro and whose
site map has a usable (probed) control of the macro's kind;
* stratified round-robin across those sites (seeded order), per-site cap, least-used
control first within a site, options drawn without replacement;
* dedup key (site, target element, argument) β€” element = the control's backend identity
"METHOD path param", stable across re-crawls; keys kept in earlier runs are excluded;
* start state: `fresh` (the control's page), `preset` (another control on the same listing
already set via the start URL) or `mid_chain` (a preceding macro on the same page is
executed first and its actions become the history). The target check then also
requires that earlier state to survive (`keep`);
* every tuple is dry-run with privileged calls in a throw-away session: the check must
FAIL on the start state (not vacuous) and PASS after applying the target (solvable).
"""
from __future__ import annotations
import hashlib
import json
import random
import re
from urllib.parse import urlencode
from miniwebgen import browser as B
from miniwebgen import checks, config, kinds, sitemap
PHRASINGS = ("imperative", "goal", "conversational")
def element_key(ctrl):
"""The control's backend identity (stable across re-crawls), as its kind defines it."""
return kinds.of(ctrl).element_key(ctrl)
def dedup_key(site, ctrl, option):
return [site, element_key(ctrl), kinds.of(ctrl).dedup_value(option)]
def _options(ctrl, macro=None):
return kinds.of(ctrl).arguments_for(ctrl, macro) if macro else kinds.of(ctrl).arguments(ctrl)
def _ctrl_view(ctrl):
return kinds.of(ctrl).view(ctrl)
def candidate_sites(macro, train, sitemaps):
from miniweb.annotation.macro_locations import MACRO_LOCATIONS
out, dropped = {}, {}
# a macro no train site lists (its demos are all on held-out sites) may use any train site
# whose site map has a control serving it
unlisted = not any(macro in (MACRO_LOCATIONS.get(s) or {}) for s in train)
for site in train:
if not unlisted and macro not in (MACRO_LOCATIONS.get(site) or {}):
continue
ctrls = [c for c in sitemap.usable_for(sitemaps.get(site), macro) if kinds.of(c).has_arguments(c)]
if ctrls:
out[site] = ctrls
else:
dropped[site] = "no usable control in site map" if sitemaps.get(site) else "no site map"
return out, dropped
def _companions(site_ctrls, ctrl):
"""Other usable controls on the same page that hit the same endpoint (for preset / mid-chain)."""
out = []
if not kinds.of(ctrl).chainable:
return out
for c in site_ctrls:
if c is ctrl or c["page"] != ctrl["page"] or not c.get("usable") or not kinds.of(c).chainable:
continue
if c["signature"]["path"] != ctrl["signature"]["path"] or c["signature"].get("param") == ctrl["signature"].get("param"):
continue
if _options(c):
out.append(c)
return out
def propose(macro, n, *, train, sitemaps, seed=0, per_site_cap=2, mid_chain_share=0.3, preset_share=0.15,
exclude_keys=(), prior_site_counts=None, fill=False):
"""Up to `n` unvalidated tuples (the sampler over-draws; the dry run filters).
`prior_site_counts` {site: n} = trajectories already kept for this macro by earlier runs;
they count against the per-site cap. Start kinds follow quotas: every prefix of the list
holds ~mid_chain_share mid-chain and ~preset_share preset tuples (where the page allows)."""
rng = random.Random(f"{seed}:{macro}")
cands, dropped = candidate_sites(macro, train, sitemaps)
sites = sorted(cands)
rng.shuffle(sites)
used_keys = {tuple(k) for k in exclude_keys}
per_site, ctrl_use, out = dict(prior_site_counts or {}), {}, []
kinds = {"mid_chain": 0, "preset": 0}
all_ctrls = {s: [c for c in sitemaps[s]["controls"] if c.get("usable")] for s in sites}
progress, cap = True, per_site_cap
while len(out) < n and (progress or (fill and cap == per_site_cap)):
if not progress: # fill: the even cap left the macro short; sites with more take it
cap = 10 ** 6
progress = False
for site in sites:
if len(out) >= n:
break
if per_site.get(site, 0) >= cap:
continue
ctrls = sorted(cands[site], key=lambda c: (ctrl_use.get(c["control_id"], 0), rng.random()))
pick = None
for ctrl in ctrls:
opts = [o for o in _options(ctrl, macro) if tuple(dedup_key(site, ctrl, o)) not in used_keys]
if opts:
pick = (ctrl, rng.choice(opts))
break
if not pick:
continue
ctrl, opt = pick
used_keys.add(tuple(dedup_key(site, ctrl, opt)))
per_site[site] = per_site.get(site, 0) + 1
ctrl_use[ctrl["control_id"]] = ctrl_use.get(ctrl["control_id"], 0) + 1
i = len(out) + 1
want = ("mid_chain" if kinds["mid_chain"] < mid_chain_share * i else
"preset" if kinds["preset"] < preset_share * i else "fresh")
t = _make(macro, site, ctrl, opt, all_ctrls[site], rng, want)
if t["start"]["kind"] != "mid_chain" and want == "mid_chain" and kinds["preset"] < preset_share * i:
t = _make(macro, site, ctrl, opt, all_ctrls[site], rng, "preset")
kinds[t["start"]["kind"]] = kinds.get(t["start"]["kind"], 0) + 1
out.append(t)
progress = True
return out, dropped
_LO = re.compile(r"\b(min|minimum|from|low|lower|start)|\bat least\b")
_HI = re.compile(r"\b(max|maximum|high|upper|end|until)|\b(to|at most|up to)\b")
def _bound(ctrl):
"""'lo' / 'hi' for one end of a numeric range pair (Min Price / Max Price, price_from / price_to), else None."""
s = re.sub(r"[_\-\[\]]", " ", f"{ctrl.get('name') or ''} {ctrl.get('label') or ''}").lower()
lo, hi = bool(_LO.search(s)), bool(_HI.search(s))
return "lo" if lo and not hi else "hi" if hi and not lo else None
def _consistent(ctrl, opt, other, o2):
"""A preset / prefix on the other end of the same range must leave it non-empty: min < max. 2026-09-29: a
preset Min Price above the task's Max Price gave an empty listing and a self-contradicting task."""
a, b = _bound(ctrl), _bound(other)
if not a or not b or a == b:
return True
try:
v, w = float(opt["value"]), float(o2["value"])
except (TypeError, ValueError, KeyError):
return True
return v < w if a == "lo" else w < v
def _make(macro, site, ctrl, opt, site_ctrls, rng, want="fresh"):
start = {"kind": "fresh", "url": ctrl["page"], "keep": {}}
comps = [c for c in _companions(site_ctrls, ctrl) if any(_consistent(ctrl, opt, c, o) for o in _options(c))]
if comps and want == "mid_chain":
other = rng.choice(comps)
o2 = rng.choice([o for o in _options(other) if _consistent(ctrl, opt, other, o)])
start = {"kind": "mid_chain", "url": ctrl["page"], "keep": {other["signature"]["param"]: o2["value"]},
"prefix": {"macro": kinds.macro_of(other), "control": _ctrl_view(other),
"option": o2}}
elif comps and want == "preset" and ctrl["page"].rstrip("/") == ctrl["signature"]["path"].rstrip("/"):
other = rng.choice([c for c in comps if c["signature"]["method"] == "GET"] or comps)
o2 = rng.choice([o for o in _options(other) if _consistent(ctrl, opt, other, o)])
if other["signature"]["method"] == "GET":
q = {other["signature"]["param"]: o2["value"]}
start = {"kind": "preset", "url": ctrl["page"] + "?" + urlencode(q), "keep": q,
"preset": {"control": _ctrl_view(other), "option": o2}}
key = dedup_key(site, ctrl, opt)
pid = "p-" + hashlib.sha1(json.dumps([macro, key, start["kind"], start.get("keep")]).encode()).hexdigest()[:10]
return {"param_id": pid, "macro": macro, "site": site, "control": _ctrl_view(ctrl), "option": opt,
**({"op": opt["op"]} if opt.get("op") else {}),
"start": start, "phrasing": rng.choice(PHRASINGS), "dedup_key": key,
"check": kinds.of(ctrl).check(ctrl, opt, keep=start["keep"])}
# ── feasibility dry run (privileged) ──────────────────────────────────────────
def _pin_status(chk, entries):
"""Record the status class the successful request had (see checks._status_class_ok)."""
if chk.get("kind") != "request":
return
ok = [e for e in entries if checks._accepted(e) and (e.get("method") or "GET").upper() == chk["method"]
and checks._path(e.get("path")) == checks._path(chk["path"])
and checks._carries(e, {**chk.get("params", {}), **chk.get("keep", {})})]
if ok and str(ok[-1].get("status", ""))[:1] == "3":
chk["status"] = "3xx"
def _weak_check(t, before, after=(), changes=None):
"""A check that can pass without the task being done (the flash judge caught these, 2026-09-28):
None when the check is sound, else the reason to drop the task."""
chk = t["check"]
if chk.get("kind") != "request":
return None
want = {**chk.get("params", {}), **chk.get("keep", {})}
# a rejected login / registration re-renders the form (200) and changes nothing; other POSTs (tools,
# joins, shares, uploads) legitimately answer 200 without a data change
if chk["method"] == "POST" and not chk.get("response") and \
(t.get("macro") in ("sign_by_text", "authenticate_by_form", "create_by_form", "toggle_relationship")
or re.search(r"/(login|signin|sign-in|register|signup|sign-up)\b", chk["path"])):
hits = [e for e in after if checks._accepted(e) and (e.get("method") or "").upper() == "POST"
and checks._path(e.get("path")) == checks._path(chk["path"]) and checks._carries(e, want)]
redirected = any(str(e.get("status", ""))[:1] == "3" for e in hits)
if hits and not redirected and changes is not None and not changes:
return ("weak check: the submission shows no success (no redirect, no data change): a rejected "
"form re-renders the same way (invalid login, username taken)")
if chk["method"] == "GET" and not want and "/api/" in chk["path"]:
return "weak check: a parameterless background API request (polling happens without the action)"
if chk["method"] == "GET" and not want and any(checks._path(e.get("path")) == checks._path(chk["path"]) for e in before):
return "weak check: the start state already makes this request"
from miniwebgen import formvalues as FV
typed = {FV.field_key(f) for f in (t["control"].get("fields") or [])
if (f.get("type") or "text") in ("text", "textarea", "search", "email", "")}
texts = [v for k, v in ((t.get("option") or {}).get("values") or {}).items()
if k in typed and isinstance(v, str) and len(v.strip()) > 12 and " " in v.strip()]
carried = json.dumps(want, ensure_ascii=False)
missing = [v for v in texts if v.strip() not in carried]
if missing:
return f"weak check: typed text never reaches the checked request ({missing[0][:40]!r})"
return None
def _oracle_once(t, base, b):
"""One privileged run of an oracle step in a fresh session: its evidence
{entry, requests, changes, clipboard, downloads}."""
ctx = B.new_context(b)
page = ctx.new_page()
downloads = []
page.on("download", lambda dl: downloads.append({"filename": dl.suggested_filename, "url": dl.url}))
try:
kinds.of(t["control"]).setup(ctx, base, t["control"], t["option"])
B.goto_start(page, base + t["start"]["url"])
before = B.session_log(ctx, base)
where = kinds.of(t["control"]).changes_site(t)
ch0 = B.session_changes(ctx, base, where) or []
ctrl = t["control"] | {"form": {"submit": t["control"].get("apply_button")}} if t["control"]["kind"] != "form" else dict(t["control"])
e, _k, _m, _new = sitemap.apply_privileged(page, ctrl, t["option"], base, ctx, len(before))
after = B.session_log(ctx, base)
ch1 = B.session_changes(ctx, base, where) or []
return {"entry": e, "requests": after[len(before):], "changes": [c for c in ch1 if c not in ch0],
"clipboard": B.clipboard_writes(ctx, base), "downloads": downloads}
finally:
ctx.close()
def _stabilize(chk, changes2):
"""Keep only the expected fields a second, independent run reproduces (drops per-request
values such as confirmation codes). None if an expected change did not recur at all."""
out = []
for want in chk["expect"]:
same = [c for c in changes2 if c["collection"] == want["collection"] and c["op"] == want["op"]
and (want["item_id"] is None or str(c["item_id"]) == str(want["item_id"]))]
if not same:
return None
best = max(same, key=lambda c: sum(checks._essential(c).get(k) == v for k, v in want["fields"].items()))
ess = checks._essential(best)
out.append(dict(want, fields={k: v for k, v in want["fields"].items() if ess.get(k) == v}))
return dict(chk, expect=out)
def dry_run_oracle(t, base, b):
"""(feasible, detail) for kinds whose check is recorded: run the step twice (fresh sessions),
record what it changed (else the request it sent), keep what both runs agree on."""
kind = kinds.of(t["control"])
ev1 = _oracle_once(t, base, b)
req1, ch1 = ev1["requests"], ev1["changes"]
chk = kind.observed_check(ev1, t["site"], t["control"])
if chk is None:
return False, "the step changed nothing observable (no data change, request, clipboard or download)"
if chk["kind"] != "state":
t["check"] = chk
ok, detail = checks.evaluate(chk, req1, ch1, ev1["clipboard"])
return (True, detail) if ok else (False, "the recorded check does not pass its own run: " + detail)
ch2 = _oracle_once(t, base, b)["changes"]
stable = _stabilize(chk, ch2)
if stable is None:
return False, "the step's data change did not recur in a second run (unstable)"
t["check"] = stable
ok1, detail = checks.evaluate(stable, req1, ch1)
ok2, detail2 = checks.evaluate(stable, [], ch2)
if ok1 and ok2:
return True, detail
return False, "the recorded check does not pass its own runs: " + (detail if not ok1 else detail2)
def dry_run(t, base, b):
"""(feasible, detail). Privileged select_option/clicks in a throw-away session."""
try:
own = kinds.of(t["control"]).dry_run(t, base, b)
except Exception as exc:
return False, f"dry-run error {type(exc).__name__}: {str(exc)[:160]}"
if own is not None:
return own
if getattr(kinds.of(t["control"]), "oracle", False):
try:
return dry_run_oracle(t, base, b)
except Exception as exc:
return False, f"dry-run error {type(exc).__name__}: {str(exc)[:160]}"
ctx = B.new_context(b)
page = ctx.new_page()
try:
kinds.of(t["control"]).setup(ctx, base, t["control"], t["option"])
B.goto_start(page, base + t["start"]["url"])
if t["start"]["kind"] == "mid_chain":
pre = t["start"]["prefix"]
n0 = len(B.session_log(ctx, base))
e, _k, _m, _new = sitemap.apply_privileged(page, pre["control"] | {"form": {"submit": pre["control"].get("apply_button")}},
pre["option"], base, ctx, n0)
if not e:
return False, "prefix step produced no request"
before = B.session_log(ctx, base)
ok0, _ = checks.evaluate(t["check"], before)
if ok0:
return False, "vacuous: the start state already satisfies the check"
ctrl = t["control"] | {"form": {"submit": t["control"].get("apply_button")}}
if ctrl["kind"] in ("link", "chips"):
ctrl["key"] = ctrl["signature"]["param"]
e, _k, _m, _new = sitemap.apply_privileged(page, ctrl, t["option"], base, ctx, len(before))
after = B.session_log(ctx, base)
ok1, detail = checks.evaluate(t["check"], after)
if not ok1:
return False, "not solvable by applying the control: " + detail
weak = _weak_check(t, before, after, B.session_changes(ctx, base))
if weak:
return False, weak
_pin_status(t["check"], after)
return True, detail
except Exception as exc:
return False, f"dry-run error {type(exc).__name__}: {str(exc)[:160]}"
finally:
ctx.close()
def sample(macro, n, *, train, sitemaps, bases, workers=4, seed=0, overdraw=3, log=print, **kw):
"""(feasible[:n], infeasible, dropped sites, spare feasible) β€” fewer than n if candidates run out."""
import queue
import threading
proposed, dropped = propose(macro, n * overdraw, train=train, sitemaps=sitemaps, seed=seed, **kw)
# keep the stratified order: validate in order, stop once n are feasible
results = [None] * len(proposed)
todo = queue.Queue()
for i, t in enumerate(proposed):
todo.put(i)
lock = threading.Lock()
state = {"ok": 0}
def worker(wi):
with B.browser() as b:
while True:
with lock:
if state["ok"] >= n:
return
try:
i = todo.get_nowait()
except queue.Empty:
return
ok, detail = dry_run(proposed[i], bases[wi % len(bases)], b)
results[i] = (ok, detail)
with lock:
state["ok"] += ok
threads = [threading.Thread(target=worker, args=(w,), daemon=True) for w in range(max(1, workers))]
for th in threads:
th.start()
for th in threads:
th.join()
feasible, infeasible = [], []
for t, r in zip(proposed, results):
if r is None:
continue
t["feasibility"] = r[1]
(feasible if r[0] else infeasible).append(t)
# workers validate in parallel, so a few extra feasible tuples may exist: keep the first n
# in stratified order, the rest are returned as `spare` (feasible, unused)
feasible, spare = feasible[:n], feasible[n:]
log(f"sample {macro}: validated {len(feasible) + len(spare) + len(infeasible)}, feasible "
f"{len(feasible) + len(spare)} (using {len(feasible)}), infeasible {len(infeasible)}, "
f"sites dropped {len(dropped)}")
return feasible, infeasible, dropped, spare