zu-patterns 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zu_patterns/__init__.py +53 -0
- zu_patterns/_match.py +80 -0
- zu_patterns/autocomplete.py +62 -0
- zu_patterns/cart_checkout.py +102 -0
- zu_patterns/cookie_banner.py +71 -0
- zu_patterns/login_form.py +87 -0
- zu_patterns/modal_dialog.py +64 -0
- zu_patterns/paginated_list.py +52 -0
- zu_patterns/rail.py +86 -0
- zu_patterns/recognizer.py +80 -0
- zu_patterns/reversibility.py +178 -0
- zu_patterns/search.py +778 -0
- zu_patterns/search_box.py +59 -0
- zu_patterns/sortable_table.py +59 -0
- zu_patterns-0.2.2.dist-info/METADATA +69 -0
- zu_patterns-0.2.2.dist-info/RECORD +18 -0
- zu_patterns-0.2.2.dist-info/WHEEL +4 -0
- zu_patterns-0.2.2.dist-info/entry_points.txt +9 -0
zu_patterns/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""zu-patterns — the policy-prior / move-ordering layer over the Action Surface.
|
|
2
|
+
|
|
3
|
+
The ``Pattern`` port itself lives in zu-core (``zu_core.ports.Pattern``); this
|
|
4
|
+
package ships the built-in patterns, the recognizer pass, the reversible-vs-
|
|
5
|
+
committing classifier, and the offline guided search over the Phase-1 FSM.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .recognizer import Recognition, recognize
|
|
11
|
+
from .reversibility import (
|
|
12
|
+
DEFAULT_PRIORS,
|
|
13
|
+
ActionPrior,
|
|
14
|
+
Commitment,
|
|
15
|
+
Signal,
|
|
16
|
+
classify_action,
|
|
17
|
+
)
|
|
18
|
+
from .search import (
|
|
19
|
+
Candidate,
|
|
20
|
+
MpcDecision,
|
|
21
|
+
MpcOutcome,
|
|
22
|
+
Plan,
|
|
23
|
+
PlanStep,
|
|
24
|
+
fsm_from_events,
|
|
25
|
+
fsm_from_shadow,
|
|
26
|
+
fsm_from_shadow_events,
|
|
27
|
+
live_mpc_step,
|
|
28
|
+
merge_transition_models,
|
|
29
|
+
mpc_run,
|
|
30
|
+
plan,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"Recognition",
|
|
35
|
+
"recognize",
|
|
36
|
+
"Commitment",
|
|
37
|
+
"Signal",
|
|
38
|
+
"ActionPrior",
|
|
39
|
+
"DEFAULT_PRIORS",
|
|
40
|
+
"classify_action",
|
|
41
|
+
"Plan",
|
|
42
|
+
"PlanStep",
|
|
43
|
+
"fsm_from_events",
|
|
44
|
+
"plan",
|
|
45
|
+
"live_mpc_step",
|
|
46
|
+
"mpc_run",
|
|
47
|
+
"Candidate",
|
|
48
|
+
"MpcDecision",
|
|
49
|
+
"MpcOutcome",
|
|
50
|
+
"fsm_from_shadow",
|
|
51
|
+
"fsm_from_shadow_events",
|
|
52
|
+
"merge_transition_models",
|
|
53
|
+
]
|
zu_patterns/_match.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Shared, purely-structural matching helpers for the built-in patterns.
|
|
2
|
+
|
|
3
|
+
These are deterministic predicates over a core ``SurfaceView``'s affordances —
|
|
4
|
+
roles, normalized labels, states. No model, no site constants: a pattern derives
|
|
5
|
+
its ``label_hint`` from the surface it matched, never from a hardcoded magic
|
|
6
|
+
string. The token lists below (``user``/``password``/``search``/``accept`` …) are
|
|
7
|
+
GENERIC, language-of-the-archetype vocabulary — the same way an accessibility
|
|
8
|
+
checker knows ``button`` means "actionable" — not site-specific keys.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Iterable
|
|
14
|
+
|
|
15
|
+
from zu_core.surface import SurfaceAffordance, SurfaceView
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def norm(s: str) -> str:
|
|
19
|
+
"""Lowercase, collapse whitespace — the canonical form for label matching."""
|
|
20
|
+
return " ".join(s.lower().split())
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def label_has(aff: SurfaceAffordance, tokens: Iterable[str]) -> bool:
|
|
24
|
+
"""True iff the affordance's normalized label contains any token."""
|
|
25
|
+
lbl = norm(aff.label)
|
|
26
|
+
return any(t in lbl for t in tokens)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def has_state(aff: SurfaceAffordance, *states: str) -> bool:
|
|
30
|
+
sset = {norm(s) for s in aff.states}
|
|
31
|
+
return any(norm(s) in sset for s in states)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def of_role(surface: SurfaceView, *roles: str) -> list[SurfaceAffordance]:
|
|
35
|
+
rset = {r.lower() for r in roles}
|
|
36
|
+
return [a for a in surface.affordances if a.role.lower() in rset]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def first(
|
|
40
|
+
surface: SurfaceView,
|
|
41
|
+
*,
|
|
42
|
+
roles: Iterable[str] = (),
|
|
43
|
+
tokens: Iterable[str] = (),
|
|
44
|
+
states: Iterable[str] = (),
|
|
45
|
+
) -> SurfaceAffordance | None:
|
|
46
|
+
"""The first affordance matching ALL of the supplied predicates (any omitted
|
|
47
|
+
predicate is satisfied vacuously)."""
|
|
48
|
+
rset = {r.lower() for r in roles}
|
|
49
|
+
tlist = list(tokens)
|
|
50
|
+
slist = list(states)
|
|
51
|
+
for a in surface.affordances:
|
|
52
|
+
if rset and a.role.lower() not in rset:
|
|
53
|
+
continue
|
|
54
|
+
if tlist and not label_has(a, tlist):
|
|
55
|
+
continue
|
|
56
|
+
if slist and not has_state(a, *slist):
|
|
57
|
+
continue
|
|
58
|
+
return a
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def context_has(surface: SurfaceView, tokens: Iterable[str]) -> bool:
|
|
63
|
+
blob = norm(" ".join(surface.context))
|
|
64
|
+
return any(t in blob for t in tokens)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Generic archetype vocabularies (NOT site constants).
|
|
68
|
+
USER_TOKENS = ("user", "email", "e-mail", "login", "username", "account name")
|
|
69
|
+
PASSWORD_TOKENS = ("password", "passcode", "pass word")
|
|
70
|
+
SUBMIT_TOKENS = ("sign in", "log in", "login", "submit", "continue", "next", "go")
|
|
71
|
+
SEARCH_TOKENS = ("search", "find", "query", "look up")
|
|
72
|
+
ACCEPT_TOKENS = ("accept", "agree", "allow", "got it", "ok", "i accept", "consent")
|
|
73
|
+
REJECT_TOKENS = ("reject", "decline", "deny", "refuse", "no thanks")
|
|
74
|
+
CLOSE_TOKENS = ("close", "dismiss", "×", "x")
|
|
75
|
+
CONFIRM_TOKENS = ("confirm", "yes", "ok", "proceed", "continue")
|
|
76
|
+
NEXT_TOKENS = ("next", "next page", ">", "more", "load more")
|
|
77
|
+
PREV_TOKENS = ("prev", "previous", "back", "<")
|
|
78
|
+
CART_TOKENS = ("add to cart", "add to bag", "add to basket")
|
|
79
|
+
CHECKOUT_TOKENS = ("checkout", "check out")
|
|
80
|
+
PLACE_ORDER_TOKENS = ("place order", "buy now", "pay", "complete purchase", "confirm order")
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""autocomplete — a textbox/combobox (expanded / controls a listbox of options).
|
|
2
|
+
|
|
3
|
+
Script: fill partial, pick option. Typing + picking is REVERSIBLE (it fills a
|
|
4
|
+
field). Success: the option fills the field.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from zu_core.invariants import Invariant
|
|
10
|
+
from zu_core.ports import PatternStep, RecognitionResult
|
|
11
|
+
from zu_core.surface import SurfaceView
|
|
12
|
+
|
|
13
|
+
from . import _match as m
|
|
14
|
+
from .rail import surface_shows
|
|
15
|
+
|
|
16
|
+
_EXPAND_STATES = ("expanded", "haspopup", "autocomplete")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Autocomplete:
|
|
20
|
+
name = "autocomplete"
|
|
21
|
+
archetype = "autocomplete"
|
|
22
|
+
|
|
23
|
+
def recognize(self, surface: SurfaceView) -> RecognitionResult | None:
|
|
24
|
+
box = next(
|
|
25
|
+
(
|
|
26
|
+
a
|
|
27
|
+
for a in surface.affordances
|
|
28
|
+
if a.role.lower() in {"combobox", "textbox", "searchbox"}
|
|
29
|
+
and m.has_state(a, *_EXPAND_STATES)
|
|
30
|
+
),
|
|
31
|
+
None,
|
|
32
|
+
)
|
|
33
|
+
if box is None:
|
|
34
|
+
return None
|
|
35
|
+
options = m.of_role(surface, "option")
|
|
36
|
+
# An expanded combobox with options present is the strong case.
|
|
37
|
+
confidence = 0.85 if options else 0.62
|
|
38
|
+
script = [PatternStep(op="fill", role=box.role, label_hint=m.norm(box.label), note="type")]
|
|
39
|
+
handles = [box.handle]
|
|
40
|
+
if options:
|
|
41
|
+
opt = options[0]
|
|
42
|
+
script.append(
|
|
43
|
+
PatternStep(op="select", role="option", label_hint=m.norm(opt.label), note="pick")
|
|
44
|
+
)
|
|
45
|
+
handles.append(opt.handle)
|
|
46
|
+
return RecognitionResult(
|
|
47
|
+
archetype=self.archetype,
|
|
48
|
+
confidence=confidence,
|
|
49
|
+
matched_handles=tuple(handles),
|
|
50
|
+
script=tuple(script),
|
|
51
|
+
detail="autocomplete",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def success_invariants(self, result: RecognitionResult) -> list[Invariant]:
|
|
55
|
+
handle = result.matched_handles[0] if result.matched_handles else None
|
|
56
|
+
# Done = the field EVENTUALLY present (filled) by the deadline.
|
|
57
|
+
return [surface_shows(self.archetype, "option_filled", handle=handle, liveness=True)]
|
|
58
|
+
|
|
59
|
+
def failure_invariants(self, result: RecognitionResult) -> list[Invariant]:
|
|
60
|
+
# Failure CONTEXT = a "no results" empty-state appears. Safety shape:
|
|
61
|
+
# THROUGHOUT NOT contains(no results) — fires the instant it lands.
|
|
62
|
+
return [surface_shows(self.archetype, "no_options", label="no results", negate=True)]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""cart_checkout — the canonical IRREVERSIBLE-BOUNDARY pattern.
|
|
2
|
+
|
|
3
|
+
Recognizes a button cluster {add to cart, checkout, place order, pay} alongside
|
|
4
|
+
line-item context. The proposed script STOPS BEFORE the committing step: it adds
|
|
5
|
+
to cart / proceeds to checkout, but the place-order/pay step is classified
|
|
6
|
+
COMMITTING — the live-search and rail commit boundary. This is the discipline
|
|
7
|
+
made into a pattern: search may explore up to the boundary, never auto-cross it.
|
|
8
|
+
Success: an order-confirmation surface. The place-order step never auto-executes.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from zu_core.invariants import Invariant
|
|
14
|
+
from zu_core.ports import PatternStep, RecognitionResult
|
|
15
|
+
from zu_core.surface import SurfaceView
|
|
16
|
+
|
|
17
|
+
from . import _match as m
|
|
18
|
+
from .rail import surface_shows
|
|
19
|
+
from .reversibility import ActionPrior, Commitment
|
|
20
|
+
|
|
21
|
+
_LINE_ITEM_CONTEXT = ("cart", "basket", "bag", "subtotal", "order summary", "line item", "qty")
|
|
22
|
+
_CONFIRM_CONTEXT = ("order confirmed", "thank you for your order", "order number", "confirmation")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CartCheckout:
|
|
26
|
+
name = "cart_checkout"
|
|
27
|
+
archetype = "cart_checkout"
|
|
28
|
+
|
|
29
|
+
def recognize(self, surface: SurfaceView) -> RecognitionResult | None:
|
|
30
|
+
add = m.first(surface, roles=("button",), tokens=m.CART_TOKENS)
|
|
31
|
+
checkout = m.first(surface, roles=("button", "link"), tokens=m.CHECKOUT_TOKENS)
|
|
32
|
+
place = m.first(surface, roles=("button",), tokens=m.PLACE_ORDER_TOKENS)
|
|
33
|
+
if add is None and checkout is None and place is None:
|
|
34
|
+
return None
|
|
35
|
+
line_ctx = m.context_has(surface, _LINE_ITEM_CONTEXT)
|
|
36
|
+
# Confidence: a cart/checkout button WITH line-item context is strong.
|
|
37
|
+
present = [x for x in (add, checkout, place) if x is not None]
|
|
38
|
+
confidence = 0.85 if (line_ctx and present) else 0.6
|
|
39
|
+
# The proposed script advances toward — but STOPS BEFORE — the committing
|
|
40
|
+
# place-order/pay step. We propose the safe step (add / go to checkout) and
|
|
41
|
+
# mark the committing step with an ``expect`` (a boundary marker), never a
|
|
42
|
+
# ``submit`` the search would auto-cross.
|
|
43
|
+
script: list[PatternStep] = []
|
|
44
|
+
handles: list[str] = []
|
|
45
|
+
safe = add or checkout
|
|
46
|
+
if safe is not None:
|
|
47
|
+
script.append(
|
|
48
|
+
PatternStep(op="click", role=safe.role, label_hint=m.norm(safe.label), note="proceed")
|
|
49
|
+
)
|
|
50
|
+
handles.append(safe.handle)
|
|
51
|
+
if place is not None:
|
|
52
|
+
# The boundary: marked, not proposed for execution.
|
|
53
|
+
script.append(
|
|
54
|
+
PatternStep(
|
|
55
|
+
op="expect",
|
|
56
|
+
role="button",
|
|
57
|
+
label_hint=m.norm(place.label),
|
|
58
|
+
note="COMMIT BOUNDARY: place-order/pay is committing — do not auto-cross",
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
handles.append(place.handle)
|
|
62
|
+
return RecognitionResult(
|
|
63
|
+
archetype=self.archetype,
|
|
64
|
+
confidence=confidence,
|
|
65
|
+
matched_handles=tuple(handles),
|
|
66
|
+
script=tuple(script),
|
|
67
|
+
detail="cart/checkout (irreversible boundary)",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def success_invariants(self, result: RecognitionResult) -> list[Invariant]:
|
|
71
|
+
# Done = an order-confirmation surface EVENTUALLY appears (by the deadline).
|
|
72
|
+
# A committed-but-never-confirmed run violates this liveness at the deadline.
|
|
73
|
+
return [
|
|
74
|
+
surface_shows(self.archetype, "order_confirmed", label="order confirmed", liveness=True)
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
def failure_invariants(self, result: RecognitionResult) -> list[Invariant]:
|
|
78
|
+
# Failure CONTEXT = a payment/checkout error appears. Safety shape:
|
|
79
|
+
# THROUGHOUT NOT contains(error) — fires the instant the error lands.
|
|
80
|
+
return [surface_shows(self.archetype, "checkout_error", label="error", negate=True)]
|
|
81
|
+
|
|
82
|
+
# The reversibility prior this pattern CONTRIBUTES: its place-order/pay step is
|
|
83
|
+
# COMMITTING. A planner/classifier passes this into ``classify_action`` so the
|
|
84
|
+
# boundary is declared by the pattern, not hardcoded into the core classifier.
|
|
85
|
+
@staticmethod
|
|
86
|
+
def commit_prior() -> ActionPrior:
|
|
87
|
+
def _is_place_order(facts: dict) -> bool:
|
|
88
|
+
note = str(facts.get("note", "")).lower()
|
|
89
|
+
op = str(facts.get("op", "")).lower()
|
|
90
|
+
label = str(facts.get("label_hint", "")).lower()
|
|
91
|
+
return (
|
|
92
|
+
op in {"place_order", "pay", "purchase", "checkout"}
|
|
93
|
+
or "commit boundary" in note
|
|
94
|
+
or any(tok in label for tok in m.PLACE_ORDER_TOKENS)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return ActionPrior(
|
|
98
|
+
name="cart_checkout.place_order",
|
|
99
|
+
matcher=_is_place_order,
|
|
100
|
+
commitment=Commitment.COMMITTING,
|
|
101
|
+
weight=2.0,
|
|
102
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""cookie_banner — a consent/cookie banner with an accept/reject button cluster.
|
|
2
|
+
|
|
3
|
+
Recognized when the surface's context mentions cookies/consent (or an
|
|
4
|
+
alert/region carries it) and the affordances are dominated by accept/agree/reject
|
|
5
|
+
buttons with no other task affordances. Dismissing is idempotent ⇒ REVERSIBLE.
|
|
6
|
+
Success: the accept button is GONE from the next surface (a negated
|
|
7
|
+
SURFACE_CONTAINS). Failure: the banner persists.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from zu_core.invariants import Invariant
|
|
13
|
+
from zu_core.ports import PatternStep, RecognitionResult
|
|
14
|
+
from zu_core.surface import SurfaceView
|
|
15
|
+
|
|
16
|
+
from . import _match as m
|
|
17
|
+
from .rail import surface_shows
|
|
18
|
+
|
|
19
|
+
_CONSENT_CONTEXT = ("cookie", "consent", "gdpr", "privacy", "tracking")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CookieBanner:
|
|
23
|
+
name = "cookie_banner"
|
|
24
|
+
archetype = "cookie_banner"
|
|
25
|
+
|
|
26
|
+
def recognize(self, surface: SurfaceView) -> RecognitionResult | None:
|
|
27
|
+
accept = m.first(surface, roles=("button",), tokens=m.ACCEPT_TOKENS)
|
|
28
|
+
if accept is None:
|
|
29
|
+
return None
|
|
30
|
+
buttons = m.of_role(surface, "button")
|
|
31
|
+
consent_ctx = m.context_has(surface, _CONSENT_CONTEXT)
|
|
32
|
+
consent_label = any(m.label_has(b, _CONSENT_CONTEXT) for b in buttons)
|
|
33
|
+
if not (consent_ctx or consent_label):
|
|
34
|
+
return None
|
|
35
|
+
# Confidence: a small banner (few affordances) dominated by accept/reject
|
|
36
|
+
# is a strong match; a page with many other affordances is weaker.
|
|
37
|
+
non_consent = [
|
|
38
|
+
a
|
|
39
|
+
for a in surface.affordances
|
|
40
|
+
if a.handle != accept.handle
|
|
41
|
+
and not m.label_has(a, m.ACCEPT_TOKENS + m.REJECT_TOKENS + m.CLOSE_TOKENS)
|
|
42
|
+
]
|
|
43
|
+
confidence = 0.9 if len(non_consent) <= 1 else 0.65
|
|
44
|
+
if consent_ctx and consent_label:
|
|
45
|
+
confidence = min(1.0, confidence + 0.05)
|
|
46
|
+
return RecognitionResult(
|
|
47
|
+
archetype=self.archetype,
|
|
48
|
+
confidence=confidence,
|
|
49
|
+
matched_handles=(accept.handle,),
|
|
50
|
+
script=(
|
|
51
|
+
PatternStep(
|
|
52
|
+
op="click", role="button", label_hint=m.norm(accept.label), note="accept"
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
detail="consent banner",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def success_invariants(self, result: RecognitionResult) -> list[Invariant]:
|
|
59
|
+
handle = result.matched_handles[0] if result.matched_handles else None
|
|
60
|
+
# Done = the accept button is EVENTUALLY gone (a liveness-of-ABSENCE check:
|
|
61
|
+
# the banner is present pre-dismiss, so it must not fire until the deadline;
|
|
62
|
+
# if the button never goes away by the deadline, that liveness VIOLATES —
|
|
63
|
+
# which IS the "banner persists" failure, captured without a redundant
|
|
64
|
+
# positive must-contain-THROUGHOUT invariant).
|
|
65
|
+
return [surface_shows(self.archetype, "dismissed", handle=handle, negate=True, liveness=True)]
|
|
66
|
+
|
|
67
|
+
def failure_invariants(self, result: RecognitionResult) -> list[Invariant]:
|
|
68
|
+
# Failure CONTEXT = a consent-wall error appears. Safety shape: THROUGHOUT
|
|
69
|
+
# NOT contains(error) — fires the instant it lands. (The "banner persists"
|
|
70
|
+
# mode is the success liveness deadline-violation, not duplicated here.)
|
|
71
|
+
return [surface_shows(self.archetype, "consent_error", label="error", negate=True)]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""login_form — a username/email textbox + a password textbox + a submit button.
|
|
2
|
+
|
|
3
|
+
Script (a PROPOSAL, never auto-run): fill user, fill password, click submit.
|
|
4
|
+
Submit is COMMITTING (a form POST). Success: a post-submit surface shows an
|
|
5
|
+
account/logout/profile affordance. Failure: an error alert/status appears.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from zu_core import events as ev
|
|
11
|
+
from zu_core.invariants import Invariant
|
|
12
|
+
from zu_core.ports import PatternStep, RecognitionResult
|
|
13
|
+
from zu_core.surface import SurfaceView
|
|
14
|
+
|
|
15
|
+
from . import _match as m
|
|
16
|
+
from .rail import surface_shows
|
|
17
|
+
|
|
18
|
+
_ACCOUNT_TOKENS = ("logout", "log out", "sign out", "account", "profile", "my account")
|
|
19
|
+
_ERROR_TOKENS = ("invalid", "incorrect", "error", "wrong password", "failed", "try again")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LoginForm:
|
|
23
|
+
name = "login_form"
|
|
24
|
+
archetype = "login_form"
|
|
25
|
+
|
|
26
|
+
def recognize(self, surface: SurfaceView) -> RecognitionResult | None:
|
|
27
|
+
user = m.first(surface, roles=("textbox", "searchbox"), tokens=m.USER_TOKENS)
|
|
28
|
+
# A password field is a textbox carrying a 'password' state, or labelled so.
|
|
29
|
+
pw = next(
|
|
30
|
+
(
|
|
31
|
+
a
|
|
32
|
+
for a in m.of_role(surface, "textbox")
|
|
33
|
+
if m.has_state(a, "password") or m.label_has(a, m.PASSWORD_TOKENS)
|
|
34
|
+
),
|
|
35
|
+
None,
|
|
36
|
+
)
|
|
37
|
+
submit = m.first(surface, roles=("button",), tokens=m.SUBMIT_TOKENS)
|
|
38
|
+
if user is None or pw is None:
|
|
39
|
+
return None
|
|
40
|
+
# Confidence rises with a submit button present and a password state.
|
|
41
|
+
confidence = 0.7
|
|
42
|
+
if submit is not None:
|
|
43
|
+
confidence += 0.15
|
|
44
|
+
if m.has_state(pw, "password"):
|
|
45
|
+
confidence += 0.1
|
|
46
|
+
confidence = min(1.0, confidence)
|
|
47
|
+
handles = tuple(h for h in (user.handle, pw.handle, submit.handle if submit else None) if h)
|
|
48
|
+
script = [
|
|
49
|
+
PatternStep(op="fill", role="textbox", label_hint=m.norm(user.label), note="username"),
|
|
50
|
+
PatternStep(op="fill", role="textbox", label_hint=m.norm(pw.label), note="password"),
|
|
51
|
+
]
|
|
52
|
+
if submit is not None:
|
|
53
|
+
script.append(
|
|
54
|
+
PatternStep(
|
|
55
|
+
op="submit", role="button", label_hint=m.norm(submit.label), note="commit"
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
return RecognitionResult(
|
|
59
|
+
archetype=self.archetype,
|
|
60
|
+
confidence=confidence,
|
|
61
|
+
matched_handles=handles,
|
|
62
|
+
script=tuple(script),
|
|
63
|
+
detail="login form",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def success_invariants(self, result: RecognitionResult) -> list[Invariant]:
|
|
67
|
+
# Done = a post-submit surface EVENTUALLY shows an account/logout
|
|
68
|
+
# affordance (a liveness-by-deadline postcondition: absent until the submit
|
|
69
|
+
# completes, so it must NOT fire on the pre-submit login surface).
|
|
70
|
+
return [
|
|
71
|
+
surface_shows(self.archetype, "reached_account", label=tok, liveness=True)
|
|
72
|
+
for tok in ("Logout", "Sign out", "Account")
|
|
73
|
+
][:1]
|
|
74
|
+
|
|
75
|
+
def failure_invariants(self, result: RecognitionResult) -> list[Invariant]:
|
|
76
|
+
# Failure CONTEXT = an error alert/status appeared. Correct safety shape:
|
|
77
|
+
# "THROUGHOUT: NOT contains(error)" — the Monitor fires the instant an
|
|
78
|
+
# error context lands; the pre-interaction surface (no error) satisfies it.
|
|
79
|
+
return [
|
|
80
|
+
surface_shows(
|
|
81
|
+
self.archetype,
|
|
82
|
+
"error_alert",
|
|
83
|
+
label="error",
|
|
84
|
+
event_type=ev.SURFACE_CAPTURED,
|
|
85
|
+
negate=True,
|
|
86
|
+
)
|
|
87
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""modal_dialog — an alertdialog/dialog trapping focus with a close/confirm.
|
|
2
|
+
|
|
3
|
+
Reversible when the proposed step is a close/dismiss; COMMITTING when it is a
|
|
4
|
+
confirm/proceed (a confirm may commit an action). Success: the dialog is gone.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from zu_core.invariants import Invariant
|
|
10
|
+
from zu_core.ports import PatternStep, RecognitionResult
|
|
11
|
+
from zu_core.surface import SurfaceView
|
|
12
|
+
|
|
13
|
+
from . import _match as m
|
|
14
|
+
from .rail import surface_shows
|
|
15
|
+
|
|
16
|
+
_DIALOG_CONTEXT = ("dialog", "modal", "are you sure", "please confirm")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ModalDialog:
|
|
20
|
+
name = "modal_dialog"
|
|
21
|
+
archetype = "modal_dialog"
|
|
22
|
+
|
|
23
|
+
def recognize(self, surface: SurfaceView) -> RecognitionResult | None:
|
|
24
|
+
# A dialog surfaces as a close button and/or a confirm button, with
|
|
25
|
+
# dialog-ish context. (Cookie banners are their own pattern and rank
|
|
26
|
+
# higher via consent vocabulary; this is the generic modal.)
|
|
27
|
+
close = m.first(surface, roles=("button",), tokens=m.CLOSE_TOKENS)
|
|
28
|
+
confirm = m.first(surface, roles=("button",), tokens=m.CONFIRM_TOKENS)
|
|
29
|
+
if close is None and confirm is None:
|
|
30
|
+
return None
|
|
31
|
+
dialogish = m.context_has(surface, _DIALOG_CONTEXT)
|
|
32
|
+
# Avoid stealing the cookie-banner case: if it reads as consent, defer.
|
|
33
|
+
if m.context_has(surface, ("cookie", "consent")):
|
|
34
|
+
return None
|
|
35
|
+
if not dialogish and close is None:
|
|
36
|
+
return None
|
|
37
|
+
chosen = close or confirm
|
|
38
|
+
assert chosen is not None
|
|
39
|
+
# Prefer the reversible close step in the proposed script.
|
|
40
|
+
op = "click" if chosen is close else "confirm"
|
|
41
|
+
confidence = 0.8 if dialogish else 0.6
|
|
42
|
+
return RecognitionResult(
|
|
43
|
+
archetype=self.archetype,
|
|
44
|
+
confidence=confidence,
|
|
45
|
+
matched_handles=(chosen.handle,),
|
|
46
|
+
script=(
|
|
47
|
+
PatternStep(op=op, role="button", label_hint=m.norm(chosen.label), note="dismiss"),
|
|
48
|
+
),
|
|
49
|
+
detail="modal dialog",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def success_invariants(self, result: RecognitionResult) -> list[Invariant]:
|
|
53
|
+
handle = result.matched_handles[0] if result.matched_handles else None
|
|
54
|
+
# Done = the dialog is EVENTUALLY gone (liveness-of-ABSENCE: the dialog is
|
|
55
|
+
# present pre-dismiss, so it must not fire until the deadline; a dialog that
|
|
56
|
+
# never closes by the deadline VIOLATES this liveness — the "persists"
|
|
57
|
+
# failure, captured without a redundant positive must-contain-THROUGHOUT).
|
|
58
|
+
return [surface_shows(self.archetype, "dismissed", handle=handle, negate=True, liveness=True)]
|
|
59
|
+
|
|
60
|
+
def failure_invariants(self, result: RecognitionResult) -> list[Invariant]:
|
|
61
|
+
# Failure CONTEXT = an error appears inside/after the dialog. Safety shape:
|
|
62
|
+
# THROUGHOUT NOT contains(error) — fires the instant it lands. (The
|
|
63
|
+
# "persists" mode is the success liveness deadline-violation, not duplicated.)
|
|
64
|
+
return [surface_shows(self.archetype, "dialog_error", label="error", negate=True)]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""paginated_list — a list/listbox plus a next/prev/page-N link cluster.
|
|
2
|
+
|
|
3
|
+
Script: click next. Navigation ⇒ REVERSIBLE. Success: the list refreshes / the
|
|
4
|
+
page context advances.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from zu_core.invariants import Invariant
|
|
10
|
+
from zu_core.ports import PatternStep, RecognitionResult
|
|
11
|
+
from zu_core.surface import SurfaceView
|
|
12
|
+
|
|
13
|
+
from . import _match as m
|
|
14
|
+
from .rail import surface_shows
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PaginatedList:
|
|
18
|
+
name = "paginated_list"
|
|
19
|
+
archetype = "paginated_list"
|
|
20
|
+
|
|
21
|
+
def recognize(self, surface: SurfaceView) -> RecognitionResult | None:
|
|
22
|
+
has_list = bool(m.of_role(surface, "list", "listbox", "table", "grid"))
|
|
23
|
+
# A 'next' affordance is a link or a button labelled next/more.
|
|
24
|
+
nxt = m.first(surface, roles=("link", "button"), tokens=m.NEXT_TOKENS)
|
|
25
|
+
prev = m.first(surface, roles=("link", "button"), tokens=m.PREV_TOKENS)
|
|
26
|
+
if nxt is None and prev is None:
|
|
27
|
+
return None
|
|
28
|
+
# Pagination is strongest with a list AND a next control.
|
|
29
|
+
confidence = 0.8 if (has_list and nxt is not None) else 0.6
|
|
30
|
+
target = nxt or prev
|
|
31
|
+
assert target is not None
|
|
32
|
+
return RecognitionResult(
|
|
33
|
+
archetype=self.archetype,
|
|
34
|
+
confidence=confidence,
|
|
35
|
+
matched_handles=(target.handle,),
|
|
36
|
+
script=(
|
|
37
|
+
PatternStep(
|
|
38
|
+
op="click", role=target.role, label_hint=m.norm(target.label), note="paginate"
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
detail="paginated list",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def success_invariants(self, result: RecognitionResult) -> list[Invariant]:
|
|
45
|
+
# Done = a fresh list surface EVENTUALLY appears (a results/list affordance
|
|
46
|
+
# present by the deadline). Liveness, not THROUGHOUT.
|
|
47
|
+
return [surface_shows(self.archetype, "page_advanced", label="results", liveness=True)]
|
|
48
|
+
|
|
49
|
+
def failure_invariants(self, result: RecognitionResult) -> list[Invariant]:
|
|
50
|
+
# Failure CONTEXT = an error/empty-state banner appears (navigation broke).
|
|
51
|
+
# Safety shape: THROUGHOUT NOT contains(error) — fires when it lands.
|
|
52
|
+
return [surface_shows(self.archetype, "page_error", label="error", negate=True)]
|
zu_patterns/rail.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Pattern → rail helpers — success/failure criteria as declarable Invariants.
|
|
2
|
+
|
|
3
|
+
A pattern's predicted "done" state and its known failure modes are expressed as
|
|
4
|
+
``zu_core.invariants.Invariant``s, which compile (via ``compile_spec``) to
|
|
5
|
+
Monitors the loop's ZU-RAIL-5 checkpoint runs. This is FULL reuse of the §1
|
|
6
|
+
machinery — no new monitor type. A breach yields ``MonitorVerdict(VIOLATION)`` →
|
|
7
|
+
the existing escalation path, which is the ZU-RAIL-9 guarantee: a recognized
|
|
8
|
+
pattern's prediction is VERIFIED, never trusted as ground truth.
|
|
9
|
+
|
|
10
|
+
The Monitor names are namespaced ``pattern.<archetype>.<criterion>`` so audits
|
|
11
|
+
read cleanly.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from zu_core import events as ev
|
|
17
|
+
from zu_core.invariants import Invariant, InvariantKind, Predicate, PredicateKind
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _name(archetype: str, criterion: str) -> str:
|
|
21
|
+
return f"pattern.{archetype}.{criterion}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def surface_shows(
|
|
25
|
+
archetype: str,
|
|
26
|
+
criterion: str,
|
|
27
|
+
*,
|
|
28
|
+
label: str | None = None,
|
|
29
|
+
handle: str | None = None,
|
|
30
|
+
recognized_archetype: str | None = None,
|
|
31
|
+
event_type: str = ev.SURFACE_CAPTURED,
|
|
32
|
+
negate: bool = False,
|
|
33
|
+
liveness: bool = False,
|
|
34
|
+
deadline: str | None = None,
|
|
35
|
+
) -> Invariant:
|
|
36
|
+
"""An Invariant over a surface event — the seam a Pattern's success/failure
|
|
37
|
+
criterion compiles to (ZU-RAIL-9).
|
|
38
|
+
|
|
39
|
+
Exactly one of ``label`` / ``handle`` / ``recognized_archetype`` is the token
|
|
40
|
+
SURFACE_CONTAINS folds the event log for. ``negate=True`` asserts ABSENCE (the
|
|
41
|
+
natural shape for "the banner is gone").
|
|
42
|
+
|
|
43
|
+
Two semantics, chosen by ``liveness``:
|
|
44
|
+
|
|
45
|
+
* ``liveness=True`` — a SUCCESS / POSTCONDITION criterion: an
|
|
46
|
+
``EVENTUALLY``-by-deadline property. The predicted post-state is, by
|
|
47
|
+
definition, ABSENT until the interaction completes, so it must NOT be a
|
|
48
|
+
violation that early/pre-interaction surfaces lack it. The Monitor stays
|
|
49
|
+
inert until the post-state appears (then satisfied forever) OR the
|
|
50
|
+
``deadline`` event arrives without it (then, and only then, VIOLATION).
|
|
51
|
+
``deadline`` is the deadline event TYPE; ``None`` ⇒ any terminal event
|
|
52
|
+
(``TASK_TERMINAL``/``TASK_COMPLETED``) marking the interaction/run complete.
|
|
53
|
+
For a non-negated success token we also require the token to ACTUALLY appear
|
|
54
|
+
(``require_present``) so "no surface ever showed it" correctly violates at
|
|
55
|
+
the deadline rather than passing vacuously.
|
|
56
|
+
|
|
57
|
+
* ``liveness=False`` (default) — a SAFETY criterion: ``THROUGHOUT``. The
|
|
58
|
+
correct shape for a FAILURE CONTEXT is ``negate=True`` ("throughout: NOT
|
|
59
|
+
contains(error-context)") so the Monitor fires the instant the failure
|
|
60
|
+
context appears, and the pre-interaction state (where the context is absent)
|
|
61
|
+
satisfies it. Do NOT model a failure as a positive must-contain-THROUGHOUT —
|
|
62
|
+
that wrongly fires on every normal surface lacking the token.
|
|
63
|
+
"""
|
|
64
|
+
params: dict = {"event_type": event_type, "negate": negate}
|
|
65
|
+
if recognized_archetype is not None:
|
|
66
|
+
params["archetype"] = recognized_archetype
|
|
67
|
+
elif handle is not None:
|
|
68
|
+
params["handle"] = handle
|
|
69
|
+
elif label is not None:
|
|
70
|
+
params["label"] = label
|
|
71
|
+
if liveness:
|
|
72
|
+
# A non-negated liveness token must genuinely appear by the deadline;
|
|
73
|
+
# a negated one (a state that must become ABSENT) needs no evidence floor.
|
|
74
|
+
if not negate:
|
|
75
|
+
params["require_present"] = True
|
|
76
|
+
return Invariant(
|
|
77
|
+
name=_name(archetype, criterion),
|
|
78
|
+
kind=InvariantKind.EVENTUALLY,
|
|
79
|
+
predicate=Predicate(kind=PredicateKind.SURFACE_CONTAINS, params=params),
|
|
80
|
+
applies_to=deadline,
|
|
81
|
+
)
|
|
82
|
+
return Invariant(
|
|
83
|
+
name=_name(archetype, criterion),
|
|
84
|
+
kind=InvariantKind.THROUGHOUT,
|
|
85
|
+
predicate=Predicate(kind=PredicateKind.SURFACE_CONTAINS, params=params),
|
|
86
|
+
)
|