Skip to content

SR-167: Post-Action Verifier Node in LangGraph (Reliability Phase 1) #167

@Delqhi

Description

@Delqhi

SR-167 — Phase 1: Post-Action-Verifier-Node im LangGraph

Reliability-Push Phase 1 of 5 — see meta-tracker for full picture.
Owner-Note (CEO): Höchster ROI. Schließt das Loch, durch das Conjoint heute morgen entkommen ist (selected_card = None wäre live in Produktion gelandet, weil keiner zwischen answer und submit schaut, ob der Klick tatsächlich gegriffen hat).

Problem

survey-cli/survey/daemon/survey_agent_graph.py hat heute diese Kanten:

START → navigate → parse → check_status → (captcha | answer | complete | error)
        answer → submit → END

Zwischen answer und submit gibt es keinen Verify-Knoten. Wenn:

  • der Klick auf eine andere Stelle gerutscht ist (React-Re-Render-Race),
  • das Radio-Button-Toggle stillschweigend gefailed ist (z. B. weil das Form per JS validiert und disabled),
  • der Input-Text durch eine onInput-Handler-Sanitization überschrieben wurde,
  • ein iFrame-Survey eine Overlay-Modal-Maske geblockt hat,

→ wird die Antwort als "erfolgreich" verbucht und committed. Keiner merkt's, außer dem Survey-Anbieter, der uns dann bant.

Lösung

A) Neuer LangGraph-Knoten verify_answer

Position: zwischen answer und submit.

# survey/daemon/survey_agent_graph.py (sketch)
graph.add_node("verify_answer", verify_answer_node)

graph.add_edge("answer", "verify_answer")
graph.add_conditional_edges(
    "verify_answer",
    route_after_verify,
    {
        "ok":       "submit",
        "retry":    "answer",        # weight-bounded loop, max 3
        "give_up":  "dlq",
    },
)

B) Neuer Modul survey/reliability/verifier.py

@dataclass
class VerificationResult:
    ok: bool
    reason: str
    evidence: dict           # before/after DOM hashes, AX-tree diff
    confidence: float        # 0.0–1.0

def verify_answer_applied(
    page: Page,
    question: Question,
    answer: Answer,
    snapshot_before: SnapshotV2,
    snapshot_after: SnapshotV2,
) -> VerificationResult:
    """
    Per-question-type checks:
    - radio:        aria-checked='true' on exactly one option == answer.choice_id
    - checkbox:     aria-checked set matches answer.choices
    - text/input:   target element .value == answer.text
    - slider:       aria-valuenow within ±1 of answer.value
    - rating:       aria-checked or .selected on the rated star
    - drag-drop:    DOM order of [data-rank] matches answer.ranking
    - conjoint:     answer.selected_card is not None AND that card has [data-selected]
    - hotspot:      DOM has a marker with answer.coordinates ± tolerance
    - max-diff:     two selections present matching answer.best, answer.worst
    - video/audio:  ad container has data-viewed='true' or similar
    """

C) Routing-Funktion

def route_after_verify(state: SurveyState) -> str:
    v = state["last_verification"]
    if v.ok:
        return "ok"
    if state["verify_attempts"] >= 3:
        return "give_up"
    # classify like retry_policy: only transient failures retry
    if v.reason in {"dom_unstable", "selector_drift", "timing_race"}:
        return "retry"
    return "give_up"

D) State-Erweiterung

class SurveyState(TypedDict):
    ...
    snapshot_before: Optional[SnapshotV2]      # taken in answer node
    last_verification: Optional[VerificationResult]
    verify_attempts: int                        # incremented on retry

Acceptance Criteria

  • survey/reliability/verifier.py mit verify_answer_applied() für alle 10 Question-Types (6 SR-150 + 4 base: radio, checkbox, input, slider).
  • LangGraph hat den Knoten verify_answer + die conditional edges + den dlq-Endknoten.
  • SurveyState enthält snapshot_before, last_verification, verify_attempts.
  • Bei 3 fehlgeschlagenen Verifizierungen → reliability/dlq.py::enqueue_failure() mit voller Evidence.
  • Unit-Tests tests/test_verifier.py mit ≥ 20 Fällen (jeder Question-Type × ok/fail).
  • Integration-Test: simulierter „stiller Klick-Fail" (Element existiert, aria-checked ändert sich nicht) → State landet auf dlq.
  • test_conjoint_happy_path (heute xfail in SR-150) wird durch den Verifier gefangen statt durchgelassen — d. h. der Test wird umgewidmet zu „verifier flags missing selected_card" und auf passing gesetzt.
  • Latency-Budget: Verifier ≤ 200 ms p95 pro Frage. Messen, in Observability loggen.

Out of Scope (folgt in Phase 2)

  • Visual-Pixel-Diff (kommt in SR-168).
  • Selector-Stability-Wait (kommt in SR-169).

Implementierungs-Reihenfolge (für den Agent, der das übernimmt)

  1. Read survey/daemon/survey_agent_graph.py, survey/snapshot.py, survey/accessibility.py, survey/reliability/dlq.py, survey/reliability/retry_policy.py.
  2. Write survey/reliability/verifier.py mit Per-Type-Funktionen + dispatch.
  3. Write tests/test_verifier.py (testen mit FakePage aus existing fixtures).
  4. Edit survey/daemon/survey_agent_graph.py: Knoten + Edges + State.
  5. Edit survey/daemon/answer_engine.py (oder wo answer-Node läuft): snapshot vor Klick speichern in State.
  6. Verify mit pytest survey-cli/tests/test_verifier.py survey-cli/tests/test_survey_agent_graph.py -v.
  7. Ruff + line-length check (max 100).
  8. PR mit Body, der Latency-Messung enthält.

Related

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions