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
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)
- Read
survey/daemon/survey_agent_graph.py, survey/snapshot.py, survey/accessibility.py, survey/reliability/dlq.py, survey/reliability/retry_policy.py.
- Write
survey/reliability/verifier.py mit Per-Type-Funktionen + dispatch.
- Write
tests/test_verifier.py (testen mit FakePage aus existing fixtures).
- Edit
survey/daemon/survey_agent_graph.py: Knoten + Edges + State.
- Edit
survey/daemon/answer_engine.py (oder wo answer-Node läuft): snapshot vor Klick speichern in State.
- Verify mit
pytest survey-cli/tests/test_verifier.py survey-cli/tests/test_survey_agent_graph.py -v.
- Ruff + line-length check (max 100).
- PR mit Body, der Latency-Messung enthält.
Related
SR-167 — Phase 1: Post-Action-Verifier-Node im LangGraph
Problem
survey-cli/survey/daemon/survey_agent_graph.pyhat heute diese Kanten:Zwischen
answerundsubmitgibt es keinen Verify-Knoten. Wenn:onInput-Handler-Sanitization überschrieben wurde,→ 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_answerPosition: zwischen
answerundsubmit.B) Neuer Modul
survey/reliability/verifier.pyC) Routing-Funktion
D) State-Erweiterung
Acceptance Criteria
survey/reliability/verifier.pymitverify_answer_applied()für alle 10 Question-Types (6 SR-150 + 4 base: radio, checkbox, input, slider).verify_answer+ die conditional edges + dendlq-Endknoten.SurveyStateenthältsnapshot_before,last_verification,verify_attempts.reliability/dlq.py::enqueue_failure()mit voller Evidence.tests/test_verifier.pymit ≥ 20 Fällen (jeder Question-Type × ok/fail).aria-checkedändert sich nicht) → State landet aufdlq.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.Out of Scope (folgt in Phase 2)
Implementierungs-Reihenfolge (für den Agent, der das übernimmt)
survey/daemon/survey_agent_graph.py,survey/snapshot.py,survey/accessibility.py,survey/reliability/dlq.py,survey/reliability/retry_policy.py.survey/reliability/verifier.pymit Per-Type-Funktionen + dispatch.tests/test_verifier.py(testen mitFakePageaus existing fixtures).survey/daemon/survey_agent_graph.py: Knoten + Edges + State.survey/daemon/answer_engine.py(oder woanswer-Node läuft): snapshot vor Klick speichern in State.pytest survey-cli/tests/test_verifier.py survey-cli/tests/test_survey_agent_graph.py -v.Related