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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""The recognizer pass — a pure, $0 classification over a core ``SurfaceView``.
|
|
2
|
+
|
|
3
|
+
This is the move-ordering prior of the §5 stack: given the affordances at one
|
|
4
|
+
step, ask every registered pattern whether it recognizes the situation, sort the
|
|
5
|
+
hits by confidence, and surface the best (above a threshold) plus the full
|
|
6
|
+
candidate list for audit/move-ordering. It is deterministic — no model, no I/O —
|
|
7
|
+
so a low-confidence step yields NO hint and the policy + safe search take over.
|
|
8
|
+
|
|
9
|
+
The recognizer NEVER chooses the task action: it ENUMERATES/CLASSIFIES (archetype
|
|
10
|
+
+ confidence + a PROPOSED script); disposing/deciding stays with the policy and
|
|
11
|
+
the guided search. ``record_recognition`` builds the ``data.pattern.recognized``
|
|
12
|
+
payload a harness shim can put on the log — "what the agent perceived/inferred",
|
|
13
|
+
never an instruction it obeyed.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Sequence
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
|
|
21
|
+
from zu_core import events as ev
|
|
22
|
+
from zu_core.ports import Pattern, RecognitionResult
|
|
23
|
+
from zu_core.surface import SurfaceView
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class Recognition:
|
|
28
|
+
"""The result of the recognizer pass.
|
|
29
|
+
|
|
30
|
+
``result`` is the best hit at or above ``min_confidence`` (``None`` ⇒
|
|
31
|
+
low-confidence fall-through: NO hint). ``candidates`` is every pattern that
|
|
32
|
+
fired, confidence-sorted, for audit and move-ordering in the planner.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
result: RecognitionResult | None
|
|
36
|
+
candidates: tuple[RecognitionResult, ...]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def recognize(
|
|
40
|
+
surface: SurfaceView,
|
|
41
|
+
patterns: Sequence[Pattern],
|
|
42
|
+
*,
|
|
43
|
+
min_confidence: float = 0.6,
|
|
44
|
+
) -> Recognition:
|
|
45
|
+
"""Run every pattern's ``recognize`` over ``surface`` and pick the best hit.
|
|
46
|
+
|
|
47
|
+
Pure and deterministic: the patterns are structural matchers, the sort is a
|
|
48
|
+
stable confidence sort, and the threshold gate (``min_confidence``) decides
|
|
49
|
+
confident-path vs fall-through. A blind surface is recognizable too (a
|
|
50
|
+
pattern may still match its visible affordances), but the blind signal rides
|
|
51
|
+
on the surface for the policy to weigh.
|
|
52
|
+
"""
|
|
53
|
+
hits: list[RecognitionResult] = []
|
|
54
|
+
for p in patterns:
|
|
55
|
+
r = p.recognize(surface)
|
|
56
|
+
if r is not None:
|
|
57
|
+
hits.append(r)
|
|
58
|
+
hits.sort(key=lambda r: r.confidence, reverse=True)
|
|
59
|
+
best = hits[0] if hits and hits[0].confidence >= min_confidence else None
|
|
60
|
+
return Recognition(result=best, candidates=tuple(hits))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def record_recognition(result: RecognitionResult, *, blind: bool = False) -> dict:
|
|
64
|
+
"""The ``data.pattern.recognized`` payload for a confident recognition.
|
|
65
|
+
|
|
66
|
+
A harness shim emits ``zu_core.events.PATTERN_RECOGNIZED`` with this payload,
|
|
67
|
+
parented to the turn — the auditable record of what the agent inferred. A
|
|
68
|
+
low-confidence (``None``) recognition emits NOTHING: no hint masquerading as
|
|
69
|
+
ground truth (the rail is what verifies a prior; see ZU-RAIL-9)."""
|
|
70
|
+
return {
|
|
71
|
+
"archetype": result.archetype,
|
|
72
|
+
"confidence": result.confidence,
|
|
73
|
+
"matched_handles": list(result.matched_handles),
|
|
74
|
+
"blind": blind,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# The event type the harness shim stamps for ``record_recognition`` — re-exported
|
|
79
|
+
# so a consumer does not have to reach into zu_core.events directly.
|
|
80
|
+
PATTERN_RECOGNIZED = ev.PATTERN_RECOGNIZED
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""The reversible-vs-committing action classifier — principled & generic.
|
|
2
|
+
|
|
3
|
+
It marks the boundary the guided search must not cross during LIVE exploration
|
|
4
|
+
(and the rail's commit boundary): a REVERSIBLE action is read-only/idempotent
|
|
5
|
+
(safe to explore), a COMMITTING action is side-effecting/irreversible (the
|
|
6
|
+
boundary). The discipline is **default-to-committing** — the safe rail behaviour
|
|
7
|
+
— whenever the signals are inconclusive. There is NO site-specific magic keyword
|
|
8
|
+
blocklist; the signals are principled and generic:
|
|
9
|
+
|
|
10
|
+
1. EXPLICIT rail annotation (``ctx.annotations["consequence"]``, ZU-RAIL-4 — a
|
|
11
|
+
content-free consequence class) — authoritative when present.
|
|
12
|
+
2. HTTP method / idempotency when OBSERVABLE — RFC 7231 safe/idempotent
|
|
13
|
+
semantics (GET/HEAD/OPTIONS ⇒ reversible; POST/PUT/PATCH/DELETE ⇒ committing),
|
|
14
|
+
not site words.
|
|
15
|
+
3. Affordance SEMANTICS from role/op — generic interaction verbs (``fill``,
|
|
16
|
+
``read``, ``open`` ⇒ reversible-leaning; ``submit``, ``confirm``, ``pay``,
|
|
17
|
+
``delete`` ⇒ committing-leaning), not site words.
|
|
18
|
+
4. A small EXTENSIBLE prior set a pattern/plugin contributes (additive,
|
|
19
|
+
community-extensible, never hardcoded into core).
|
|
20
|
+
5. DEFAULT: uncertain ⇒ COMMITTING.
|
|
21
|
+
|
|
22
|
+
Pure, deterministic, hand-testable. It never decides the task action; it only
|
|
23
|
+
classifies an action's consequence class for the planner and the rail.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from collections.abc import Callable, Sequence
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from enum import Enum
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Commitment(str, Enum):
|
|
34
|
+
REVERSIBLE = "reversible" # read-only / idempotent: safe to explore live
|
|
35
|
+
COMMITTING = "committing" # side-effecting / irreversible: the boundary
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class Signal:
|
|
40
|
+
"""An extensible piece of evidence toward a verdict. ``weight`` is positive
|
|
41
|
+
toward COMMITTING, negative toward REVERSIBLE."""
|
|
42
|
+
|
|
43
|
+
name: str
|
|
44
|
+
weight: float
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class ActionPrior:
|
|
49
|
+
"""A community-extensible prior a pattern/plugin contributes: when ``matcher``
|
|
50
|
+
holds for an action's signals, contribute ``commitment`` with ``weight``. The
|
|
51
|
+
classifier sums priors; this is how a checkout pattern declares its submit
|
|
52
|
+
step COMMITTING without a hardcoded core constant."""
|
|
53
|
+
|
|
54
|
+
name: str
|
|
55
|
+
matcher: Callable[[dict], bool]
|
|
56
|
+
commitment: Commitment
|
|
57
|
+
weight: float = 1.0
|
|
58
|
+
|
|
59
|
+
def signal(self, facts: dict) -> Signal | None:
|
|
60
|
+
if not self.matcher(facts):
|
|
61
|
+
return None
|
|
62
|
+
w = self.weight if self.commitment is Commitment.COMMITTING else -self.weight
|
|
63
|
+
return Signal(name=self.name, weight=w)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# RFC 7231 §4.2: safe methods never have observable side effects.
|
|
67
|
+
_SAFE_METHODS = frozenset({"GET", "HEAD", "OPTIONS", "TRACE"})
|
|
68
|
+
# Methods that, by HTTP semantics, may create/modify/remove server state.
|
|
69
|
+
_WRITE_METHODS = frozenset({"POST", "PUT", "PATCH", "DELETE"})
|
|
70
|
+
|
|
71
|
+
# Generic interaction verbs (NOT site words): what the OP semantically does.
|
|
72
|
+
_REVERSIBLE_OPS = frozenset({"fill", "read", "open", "reduce", "select", "expand", "focus"})
|
|
73
|
+
_COMMITTING_OPS = frozenset(
|
|
74
|
+
{"submit", "confirm", "purchase", "pay", "checkout", "delete", "place_order"}
|
|
75
|
+
)
|
|
76
|
+
# Roles whose interaction is, by accessibility semantics, read-only/navigational.
|
|
77
|
+
_REVERSIBLE_ROLES = frozenset(
|
|
78
|
+
{"textbox", "searchbox", "combobox", "checkbox", "radio", "switch", "tab", "option", "link"}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _op_signal(op: str | None) -> Signal | None:
|
|
83
|
+
if not op:
|
|
84
|
+
return None
|
|
85
|
+
o = op.strip().lower()
|
|
86
|
+
if o in _COMMITTING_OPS:
|
|
87
|
+
return Signal(name=f"op:{o}", weight=1.0)
|
|
88
|
+
if o in _REVERSIBLE_OPS:
|
|
89
|
+
return Signal(name=f"op:{o}", weight=-0.5)
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _role_signal(role: str | None) -> Signal | None:
|
|
94
|
+
if not role:
|
|
95
|
+
return None
|
|
96
|
+
r = role.strip().lower()
|
|
97
|
+
if r in _REVERSIBLE_ROLES:
|
|
98
|
+
return Signal(name=f"role:{r}", weight=-0.5)
|
|
99
|
+
# A plain ``button`` is ambiguous (it might submit a form) — no signal, so it
|
|
100
|
+
# falls to the default-committing floor unless another signal resolves it.
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _http_signal(http_method: str | None, idempotent: bool | None) -> Signal | None:
|
|
105
|
+
if http_method:
|
|
106
|
+
m = http_method.strip().upper()
|
|
107
|
+
if m in _SAFE_METHODS:
|
|
108
|
+
return Signal(name=f"http:{m}", weight=-1.0)
|
|
109
|
+
if m in _WRITE_METHODS:
|
|
110
|
+
# An explicitly idempotent write (PUT/DELETE with an idempotency key)
|
|
111
|
+
# is still a state change — committing — but a caller may down-weight.
|
|
112
|
+
return Signal(name=f"http:{m}", weight=1.0)
|
|
113
|
+
if idempotent is True:
|
|
114
|
+
return Signal(name="idempotent", weight=-0.5)
|
|
115
|
+
if idempotent is False:
|
|
116
|
+
return Signal(name="non_idempotent", weight=1.0)
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# A safe-default empty prior set; a consumer passes its own (or a pattern's).
|
|
121
|
+
DEFAULT_PRIORS: tuple[ActionPrior, ...] = ()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def classify_action(
|
|
125
|
+
*,
|
|
126
|
+
http_method: str | None = None,
|
|
127
|
+
role: str | None = None,
|
|
128
|
+
op: str | None = None,
|
|
129
|
+
idempotent: bool | None = None,
|
|
130
|
+
annotations: dict | None = None,
|
|
131
|
+
priors: Sequence[ActionPrior] = DEFAULT_PRIORS,
|
|
132
|
+
) -> Commitment:
|
|
133
|
+
"""Classify one action as REVERSIBLE or COMMITTING (pure, deterministic).
|
|
134
|
+
|
|
135
|
+
Priority: an explicit rail ``annotations["consequence"]`` is authoritative
|
|
136
|
+
when present (ZU-RAIL-4); otherwise sum the HTTP/op/role/prior signals. A net
|
|
137
|
+
negative sum is REVERSIBLE; zero or positive — including the no-signal case —
|
|
138
|
+
is COMMITTING (default-to-safe). The signed sum, not any single keyword,
|
|
139
|
+
decides, so the result is robust to a single weak hint.
|
|
140
|
+
"""
|
|
141
|
+
# 1) authoritative rail annotation: a content-free consequence class.
|
|
142
|
+
if annotations:
|
|
143
|
+
consequence = annotations.get("consequence")
|
|
144
|
+
if isinstance(consequence, str):
|
|
145
|
+
c = consequence.strip().lower()
|
|
146
|
+
if c in {"read", "readonly", "read_only", "reversible", "none", "safe"}:
|
|
147
|
+
return Commitment.REVERSIBLE
|
|
148
|
+
if c in {"write", "commit", "committing", "irreversible", "payment", "purchase"}:
|
|
149
|
+
return Commitment.COMMITTING
|
|
150
|
+
|
|
151
|
+
signals: list[Signal] = []
|
|
152
|
+
facts = {"http_method": http_method, "role": role, "op": op, "idempotent": idempotent}
|
|
153
|
+
for maybe in (
|
|
154
|
+
_http_signal(http_method, idempotent),
|
|
155
|
+
_op_signal(op),
|
|
156
|
+
_role_signal(role),
|
|
157
|
+
):
|
|
158
|
+
if maybe is not None:
|
|
159
|
+
signals.append(maybe)
|
|
160
|
+
# 4) extensible priors (a pattern's contributed evidence).
|
|
161
|
+
for prior in priors:
|
|
162
|
+
s = prior.signal(facts)
|
|
163
|
+
if s is not None:
|
|
164
|
+
signals.append(s)
|
|
165
|
+
|
|
166
|
+
score = sum(s.weight for s in signals)
|
|
167
|
+
# 5) DEFAULT-TO-COMMITTING: only a net-reversible balance of evidence flips it.
|
|
168
|
+
return Commitment.REVERSIBLE if score < 0 else Commitment.COMMITTING
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True)
|
|
172
|
+
class ClassifiedAction:
|
|
173
|
+
"""A concrete action with its consequence class — the value the planner reads
|
|
174
|
+
to know which FSM edges are safe to cross live."""
|
|
175
|
+
|
|
176
|
+
label: str
|
|
177
|
+
commitment: Commitment
|
|
178
|
+
signals: tuple[Signal, ...] = field(default_factory=tuple)
|