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.
@@ -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)