Spaces:
Running
Running
File size: 19,025 Bytes
22de727 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 | """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
|