Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions packages/flow/openadapt_flow/risk.py
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,41 @@ def opener_explanation(confirm_id: str) -> str:
return f"opens a confirmation dialog; the save is the next click ({confirm_id})"


_OPENER_EXPLANATION_RE = re.compile(
r"^opens a confirmation dialog; the save is the next click "
r"\(([A-Za-z0-9][A-Za-z0-9_.-]{0,63})\)$"
)


def is_sealed_confirmation_opener(step: Step) -> bool:
"""A confirmation-dialog opener the compiler sealed with dialog evidence.

Policy re-derives risk from labels, so a write-shaped opener label such as
"Create New Patient" would otherwise become irreversible again and demand
a system-of-record effect on a click that saves nothing. Both halves of the
seal are required: the explanation that names the confirm step, and a
postcondition that the dialog opened (``text_present`` or
``no_duplicate_candidate``). Without the postcondition the run would treat
a missing dialog as a successful save.
"""

if step.action not in _POINTER_ACTIONS:
return False
if step.effects or step.api_binding is not None:
return False
if _OPENER_EXPLANATION_RE.fullmatch(step.risk_explanation or "") is None:
return False
return any(
(
p.kind is PostconditionKind.TEXT_PRESENT
and isinstance(p.text, str)
and bool(p.text.strip())
)
or p.kind is PostconditionKind.NO_DUPLICATE_CANDIDATE
for p in step.expect
)


def compiled_confirmation_pairs(
steps: Iterable[Step],
) -> tuple[tuple[str, str], ...]:
Expand Down Expand Up @@ -842,6 +877,12 @@ def infer_step_risk(
(step.risk_explanation or LOCAL_EDIT_PREFIX)[:512],
(action_evidence, "pattern:local_edit"),
)
if write_keyword is not None and is_sealed_confirmation_opener(step):
return RiskInference(
"reversible",
(step.risk_explanation or "")[:512],
(action_evidence, "pattern:confirmation_opener"),
)
if write_keyword is not None and step.action in {
ActionKind.CLICK,
ActionKind.DOUBLE_CLICK,
Expand Down
64 changes: 62 additions & 2 deletions packages/flow/tests/test_confirmation_opener.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,21 @@

import pytest

from openadapt_flow.ir import ActionKind, Anchor, Step, StructuralLocator
from openadapt_flow.risk import confirmation_opener_step_ids, infer_step_risk
from openadapt_flow.ir import (
ActionKind,
Anchor,
Postcondition,
PostconditionKind,
Step,
StructuralLocator,
)
from openadapt_flow.policy import effective_step_risk, step_tags
from openadapt_flow.risk import (
confirmation_opener_step_ids,
infer_step_risk,
is_sealed_confirmation_opener,
opener_explanation,
)


def _click(step_id: str, label: str, selector: str = "button") -> Step:
Expand Down Expand Up @@ -71,3 +84,50 @@ def test_an_opener_with_its_own_effect_or_binding_stays_the_save():
def test_two_confirms_in_a_row_keep_the_first():
steps = [_click("a", "Confirm"), _click("b", "Confirm")]
assert confirmation_opener_step_ids(steps) == ()


def _seal_opener(opener: Step, confirm_id: str, dialog_text: str) -> Step:
opener.risk = "reversible"
opener.risk_explanation = opener_explanation(confirm_id)
opener.risk_review_required = False
opener.expect = [
Postcondition(kind=PostconditionKind.TEXT_PRESENT, text=dialog_text)
]
return opener


def test_sealed_opener_stays_reversible_under_policy_rederive():
"""Policy re-runs classify_step_risk; sealed opener evidence must win."""

opener = _seal_opener(
_click("step_039", "Create New Patient", "#create"),
"step_040",
"Confirm Create New Patient",
)
assert is_sealed_confirmation_opener(opener)
assert infer_step_risk(opener).risk == "reversible"
assert effective_step_risk(opener) == "reversible"
assert "write" not in step_tags(opener)


def test_explanation_alone_is_not_a_seal():
"""Without a dialog postcondition, write-shaped labels stay irreversible."""

opener = _click("step_039", "Create New Patient", "#create")
opener.risk = "reversible"
opener.risk_explanation = opener_explanation("step_040")
assert not is_sealed_confirmation_opener(opener)
assert infer_step_risk(opener).risk == "irreversible"
assert effective_step_risk(opener) == "irreversible"
assert "write" in step_tags(opener)


def test_sealed_opener_with_own_effect_stays_a_write():
opener = _seal_opener(
_click("a", "Create New Patient"),
"b",
"Confirm",
)
opener.effects = [object()] # type: ignore[list-item]
assert not is_sealed_confirmation_opener(opener)
assert infer_step_risk(opener).risk == "irreversible"
Loading