zu-patterns 0.2.2__tar.gz

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,66 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+
9
+ # uv / venv
10
+ .venv/
11
+ uv.lock.bak
12
+
13
+ # Test / type caches
14
+ .pytest_cache/
15
+ .mypy_cache/
16
+ .ruff_cache/
17
+ .coverage
18
+ htmlcov/
19
+
20
+ # Zu runtime artifacts
21
+ *.db
22
+ zu.db
23
+ zu.yaml.local
24
+ zu_review.jsonl
25
+ *.review.jsonl
26
+ # Per-agent cost telemetry ledger — machine-local run history, not source.
27
+ cost.jsonl
28
+ # A recorded replay path is learned per-run and machine-local — regenerated on
29
+ # every successful run, not source. The agent ships; its track does not.
30
+ track.json
31
+ # …except the flagship example ships its track on purpose, as a demo of the
32
+ # record/replay convergence (committed; re-runs show as ordinary modifications).
33
+ !examples/agents/vet-appointment/track.json
34
+
35
+ # Editor / OS
36
+ .idea/
37
+ .vscode/
38
+ .DS_Store
39
+
40
+ # Claude Code local session state
41
+ .claude/
42
+
43
+ # Secrets
44
+ .env
45
+ .env.*
46
+ !.env.example
47
+
48
+ # Microsoft Office temp/lock files
49
+ ~$*
50
+
51
+ # Internal design / strategy docs — kept local, never in the public repo
52
+ *.docx
53
+ *.pdf
54
+ # BUILD.md is the internal build-sequence / deferred-gaps ledger — kept local.
55
+ # (ARCHITECTURE.md is public: an onboarding agent needs the structural map.)
56
+ docs/BUILD.md
57
+
58
+ # Local secret — API key for live validation, never commit
59
+ zu_demo_key.md
60
+ *_key.md
61
+
62
+ # Local PyPI publish token — never commit
63
+ /pypi
64
+
65
+ # Local Discord credentials (bot token / app secrets) — never commit
66
+ /discord
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: zu-patterns
3
+ Version: 0.2.2
4
+ Summary: Zu pattern library: the policy-prior / move-ordering layer over the Action Surface (§5)
5
+ Project-URL: Homepage, https://github.com/k3-mt/zu
6
+ Project-URL: Repository, https://github.com/k3-mt/zu
7
+ License-Expression: Apache-2.0
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
15
+ Classifier: Typing :: Typed
16
+ Requires-Python: >=3.11
17
+ Requires-Dist: zu-core==0.2.9
18
+ Description-Content-Type: text/markdown
19
+
20
+ # zu-patterns — the policy-prior / move-ordering layer (§5)
21
+
22
+ A UI is a state space; the **Action Surface** is the move generator (affordances
23
+ = legal moves). This package is the **policy prior + guided search** layer over
24
+ that surface — the *AlphaZero* shape, not Deep Blue. It does **not** brute-force a
25
+ live space (visiting a node might charge a card). It proposes the promising
26
+ interaction *without* exploring, and the rail (§1) verifies the prediction.
27
+
28
+ ## The new port — `Pattern` (group `zu.patterns`)
29
+
30
+ The `Pattern` Protocol lives in **zu-core** (`zu_core.ports.Pattern`), like the
31
+ other ports. A pattern is **read-only**: it `recognize`s a situation over a core
32
+ `SurfaceView` (`zu_core.surface`) and emits `success_invariants` /
33
+ `failure_invariants` (declarative `zu_core.invariants.Invariant`s the rail
34
+ verifies). It **never** calls a tool and **never** decides the task action — that
35
+ is the policy's job. A recognized pattern is a **prior to be confirmed by
36
+ observation, never ground truth** (ZU-RAIL-9): its success criteria compile (via
37
+ `compile_spec`) to Monitors, and a behaviour mismatch fires a detector.
38
+
39
+ The boundary that makes this clean: `recognize` takes the **core** `SurfaceView`,
40
+ never zu-tools' `Surface`. zu-tools projects its `Surface` onto `SurfaceView`
41
+ through a one-way adapter (`zu_tools.surface_adapter.to_surface_view`), so
42
+ zu-patterns depends only on zu-core.
43
+
44
+ ## The pieces
45
+
46
+ - `recognizer.py` — a pure pass over a `SurfaceView`: classify → archetype +
47
+ confidence. Low confidence ⇒ **no hint** (fall through to the model).
48
+ - `reversibility.py` — a principled, default-to-committing classifier of an
49
+ action as **reversible** (read-only/idempotent, safe to explore live) vs
50
+ **committing** (side-effecting — the live-search/rail commit boundary). No
51
+ site-specific keyword blocklist: HTTP-method/idempotency, affordance semantics,
52
+ an extensible prior set, default-to-committing on uncertainty.
53
+ - `rail.py` — the success/failure → `Invariant` helpers shared by the patterns.
54
+ - `search.py` — an offline best-first planner **over the Phase-1
55
+ `zu_core.reachability.Fsm`** with the recognizer as the move-ordering prior,
56
+ plus an event-log → `Fsm` transition-model builder. Pure, offline, $0. The
57
+ live guided-MPC loop and the Shadow-sourced transition model are **deferred
58
+ seams** (stubbed/documented).
59
+
60
+ ## The 8 starter archetypes
61
+
62
+ `cookie_banner`, `login_form`, `search_box`, `modal_dialog`, `paginated_list`,
63
+ `sortable_table`, `autocomplete`, `cart_checkout` — the last is the canonical
64
+ **irreversible-boundary** pattern (its place-order/pay step is classified
65
+ COMMITTING; the script stops before it).
66
+
67
+ All recognition is **deterministic** structural matching over roles/labels/states
68
+ (no model), so every pattern is tested at $0 with hand-built `SurfaceView`s. A
69
+ small-model recognizer is a future plugin behind the same Protocol.
@@ -0,0 +1,50 @@
1
+ # zu-patterns — the policy-prior / move-ordering layer (§5)
2
+
3
+ A UI is a state space; the **Action Surface** is the move generator (affordances
4
+ = legal moves). This package is the **policy prior + guided search** layer over
5
+ that surface — the *AlphaZero* shape, not Deep Blue. It does **not** brute-force a
6
+ live space (visiting a node might charge a card). It proposes the promising
7
+ interaction *without* exploring, and the rail (§1) verifies the prediction.
8
+
9
+ ## The new port — `Pattern` (group `zu.patterns`)
10
+
11
+ The `Pattern` Protocol lives in **zu-core** (`zu_core.ports.Pattern`), like the
12
+ other ports. A pattern is **read-only**: it `recognize`s a situation over a core
13
+ `SurfaceView` (`zu_core.surface`) and emits `success_invariants` /
14
+ `failure_invariants` (declarative `zu_core.invariants.Invariant`s the rail
15
+ verifies). It **never** calls a tool and **never** decides the task action — that
16
+ is the policy's job. A recognized pattern is a **prior to be confirmed by
17
+ observation, never ground truth** (ZU-RAIL-9): its success criteria compile (via
18
+ `compile_spec`) to Monitors, and a behaviour mismatch fires a detector.
19
+
20
+ The boundary that makes this clean: `recognize` takes the **core** `SurfaceView`,
21
+ never zu-tools' `Surface`. zu-tools projects its `Surface` onto `SurfaceView`
22
+ through a one-way adapter (`zu_tools.surface_adapter.to_surface_view`), so
23
+ zu-patterns depends only on zu-core.
24
+
25
+ ## The pieces
26
+
27
+ - `recognizer.py` — a pure pass over a `SurfaceView`: classify → archetype +
28
+ confidence. Low confidence ⇒ **no hint** (fall through to the model).
29
+ - `reversibility.py` — a principled, default-to-committing classifier of an
30
+ action as **reversible** (read-only/idempotent, safe to explore live) vs
31
+ **committing** (side-effecting — the live-search/rail commit boundary). No
32
+ site-specific keyword blocklist: HTTP-method/idempotency, affordance semantics,
33
+ an extensible prior set, default-to-committing on uncertainty.
34
+ - `rail.py` — the success/failure → `Invariant` helpers shared by the patterns.
35
+ - `search.py` — an offline best-first planner **over the Phase-1
36
+ `zu_core.reachability.Fsm`** with the recognizer as the move-ordering prior,
37
+ plus an event-log → `Fsm` transition-model builder. Pure, offline, $0. The
38
+ live guided-MPC loop and the Shadow-sourced transition model are **deferred
39
+ seams** (stubbed/documented).
40
+
41
+ ## The 8 starter archetypes
42
+
43
+ `cookie_banner`, `login_form`, `search_box`, `modal_dialog`, `paginated_list`,
44
+ `sortable_table`, `autocomplete`, `cart_checkout` — the last is the canonical
45
+ **irreversible-boundary** pattern (its place-order/pay step is classified
46
+ COMMITTING; the script stops before it).
47
+
48
+ All recognition is **deterministic** structural matching over roles/labels/states
49
+ (no model), so every pattern is tested at $0 with hand-built `SurfaceView`s. A
50
+ small-model recognizer is a future plugin behind the same Protocol.
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "zu-patterns"
3
+ version = "0.2.2"
4
+ description = "Zu pattern library: the policy-prior / move-ordering layer over the Action Surface (§5)"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "Apache-2.0"
8
+ classifiers = [
9
+ "Development Status :: 4 - Beta",
10
+ "Intended Audience :: Developers",
11
+ "License :: OSI Approved :: Apache Software License",
12
+ "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3.11",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
16
+ "Typing :: Typed",
17
+ ]
18
+ # The boundary holds: zu-patterns speaks ONLY the core SurfaceView — it depends
19
+ # on zu-core and NOT on zu-tools. A pattern recognizes over the core type; the
20
+ # zu-tools Surface → SurfaceView projection lives in zu-tools, one-way.
21
+ dependencies = ["zu-core==0.2.9"]
22
+
23
+ [project.entry-points."zu.patterns"] # <- how a pattern is registered (§5)
24
+ cookie_banner = "zu_patterns.cookie_banner:CookieBanner"
25
+ login_form = "zu_patterns.login_form:LoginForm"
26
+ search_box = "zu_patterns.search_box:SearchBox"
27
+ modal_dialog = "zu_patterns.modal_dialog:ModalDialog"
28
+ paginated_list = "zu_patterns.paginated_list:PaginatedList"
29
+ sortable_table = "zu_patterns.sortable_table:SortableTable"
30
+ autocomplete = "zu_patterns.autocomplete:Autocomplete"
31
+ cart_checkout = "zu_patterns.cart_checkout:CartCheckout"
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/k3-mt/zu"
35
+ Repository = "https://github.com/k3-mt/zu"
36
+
37
+ [build-system]
38
+ requires = ["hatchling"]
39
+ build-backend = "hatchling.build"
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["src/zu_patterns"]
@@ -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
+ ]
@@ -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)]