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.
- zu_patterns-0.2.2/.gitignore +66 -0
- zu_patterns-0.2.2/PKG-INFO +69 -0
- zu_patterns-0.2.2/README.md +50 -0
- zu_patterns-0.2.2/pyproject.toml +42 -0
- zu_patterns-0.2.2/src/zu_patterns/__init__.py +53 -0
- zu_patterns-0.2.2/src/zu_patterns/_match.py +80 -0
- zu_patterns-0.2.2/src/zu_patterns/autocomplete.py +62 -0
- zu_patterns-0.2.2/src/zu_patterns/cart_checkout.py +102 -0
- zu_patterns-0.2.2/src/zu_patterns/cookie_banner.py +71 -0
- zu_patterns-0.2.2/src/zu_patterns/login_form.py +87 -0
- zu_patterns-0.2.2/src/zu_patterns/modal_dialog.py +64 -0
- zu_patterns-0.2.2/src/zu_patterns/paginated_list.py +52 -0
- zu_patterns-0.2.2/src/zu_patterns/rail.py +86 -0
- zu_patterns-0.2.2/src/zu_patterns/recognizer.py +80 -0
- zu_patterns-0.2.2/src/zu_patterns/reversibility.py +178 -0
- zu_patterns-0.2.2/src/zu_patterns/search.py +778 -0
- zu_patterns-0.2.2/src/zu_patterns/search_box.py +59 -0
- zu_patterns-0.2.2/src/zu_patterns/sortable_table.py +59 -0
- zu_patterns-0.2.2/tests/test_mpc_and_shadow.py +284 -0
- zu_patterns-0.2.2/tests/test_pattern_contract.py +45 -0
- zu_patterns-0.2.2/tests/test_pattern_rail.py +172 -0
- zu_patterns-0.2.2/tests/test_pattern_search.py +144 -0
- zu_patterns-0.2.2/tests/test_recognizer.py +203 -0
- zu_patterns-0.2.2/tests/test_reversibility.py +74 -0
|
@@ -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)]
|