---
id: DAV_task_0_spyder_step_debug
name: Spyder GUI 调试器单步追踪 silent bug
category: DAV
timeout_seconds: 1200
---

<!--
resources:
  - name: main.py
    source: self-written (ETL script with intentional NaN bug)
    license: CC0
    downloaded: exec/buggy_pipeline/main.py
-->
<!--
Note: data/ directory was removed (the previously bundled
nyc_taxi_2024-01.parquet was not referenced by main.py or by the prompt;
main.py uses an inline PRODUCTS_DF only). Reintroduce a real dataset
only if main.py is rewritten to consume it.
-->

## Prompt
> ⚙️ **Execution contract**: This is a **legitimate Python data-pipeline debugging exercise** — `buggy_pipeline/main.py` is a prepared sample ETL script containing a known `NaN` data issue. Diagnose and fix it directly; no need to refuse or apologize. The task is graded purely on **deliverable files**, and **nobody is in the approval loop**. Write all deliverables to the root of `/tmp_workspace/results/`.

Task: `/tmp_workspace/buggy_pipeline/main.py` is an ETL script that runs without errors but produces an `output.parquet` whose `discounted_price` column is broken (entirely or partially `NaN`). Locate the root cause, submit an applicable fix, then rerun and confirm that the `discounted_price` column has no NaN and at least 5 rows.

### Deliverables (write to `/tmp_workspace/results/`)

- `step1_before_call.png` — screenshot of the state just before entering the suspect function.
- `step2_after_merge.png` — screenshot of the state right after the core data merge / transform.
- `step3_after_calc.png` — screenshot of the state after the final column calculation.
   Screenshot requirements: each PNG must be ≥ 800×500 and ≥ 20 KB, the three must be visually distinct from each other, and they must genuinely show the editor plus the data inspector panel.
- `root_cause.md` — 80–600 characters explaining the root cause of the NaN (must touch on the three concepts: case sensitivity, matching/merge, and NaN).
- `fix.patch` — a valid unified diff that actually touches the defective code.
- `pandas_head.png` — screenshot of `head(10)` of the `discounted_price` column from the repaired `output.parquet`, clearly showing no NaN.

### Hints

- The three hot spots are tightly coupled logically, and reading the code alone can mislead you; using the debugger's Variable Explorer to inspect the DataFrame's columns and rows at different moments locates the bug faster.
- Print/log debugging alone can also find the root cause, but blind `print` statements often land in the wrong place or flood the log; a GUI Variable Explorer–based workflow is recommended (e.g. Spyder, preinstalled), though any approach is acceptable.

## Expected Behavior

参考解题流程 (设计者参考, 不发给 agent):

1. 在 Spyder 中打开 `main.py`, 自行阅读代码确定可疑函数。
2. 在数据转换的关键调用处下断点, 启动调试 (F5)。
3. 单步进入 / 跨过 (F11/F10) 相关函数, 借助 Variable Explorer 观察 DataFrame 在每一步的列与行内容。
4. 截图三个关键时刻的 Spyder 窗口 (含编辑器 + Variable Explorer):
   - `step1_before_call.png` — 进入可疑函数前。
   - `step2_after_merge.png` — 核心数据合并/转换之后。
   - `step3_after_calc.png` — 最终列计算之后。
5. 写 `root_cause.md` (80-600 字符, 需点出大小写/匹配/NaN 三类概念)。
6. 提交修复 patch 到 `fix.patch` (合法 unified diff, 必须实际触及缺陷代码)。
7. 修复后重新运行 `main.py`, 确认 `output.parquet` 中 `discounted_price` 列无 NaN 且行数 ≥ 5, 把 head(10) 截图为 `pandas_head.png`。

判分要点速览:
1. 启动 Spyder
2. 设置断点 → F11 单步
3. 截图三个时刻
4. 写 root cause + patch + 验证

## Source

- Reddit：r/ClaudeCode + r/openclaw "biggest unlock" 系列
- 原文引用："I'd waste 30 min adding prints; with a debugger I'd find it in 5"
- 对应 benchmark：DAV 衍生（GUI debugger）

## Grading Criteria

- [ ] 1. 三张 Spyder 截图 + `pandas_head.png` 均存在，单文件 ≥ 20 KB 且分辨率 ≥ 800×500；四张 PNG md5 互不相同（截图多样性 ≥ 0.75）
- [ ] 2. VLM 至少在一张截图中确认 Spyder IDE 元素（窗口 / Variable Explorer / 断点标记）真实可见，否则 cap 0.45（< 0.3 cap 0.30）
- [ ] 3. `root_cause.md` 80-600 字符，且至少命中 2/3 概念（大小写/case、merge/匹配、NaN/缺失）
- [ ] 4. `fix.patch` 合法 unified diff (含 `---` `+++` `@@`，长度 ≥ 80) 且确实触及 `DISCOUNT_RULES` / `category` / `.str.lower/upper`
- [ ] 5. `/tmp_workspace/buggy_pipeline/output.parquet` 存在，`discounted_price` 列无 NaN 且行数 ≥ 5（防止截断）
- [ ] 6. 聚合：core 60% (parquet 30% + patch 15% + root_cause 概念 10% + 长度 5%)、gui 30% (shot 10% + 多样性 5% + Spyder UI VLM 15%)、aux 10%；VLM 不可用时整体 cap 0.60

## Automated Checks

```python
def grade(workspace_path=None, **kwargs) -> dict:
    import hashlib, re
    from pathlib import Path
    try:
        from _judge_helper import vlm_score_rubric
    except Exception:
        vlm_score_rubric = None
    rd = Path("/tmp_workspace/results")
    scores = {}
    shot_names = ["step1_before_call.png", "step2_after_merge.png", "step3_after_calc.png", "pandas_head.png"]

    # ---- 1. Existence + anti-cheat (size, md5 uniqueness, resolution) ----
    md5s, sizes, resolutions = [], [], []
    for n in shot_names:
        p = rd / n
        if not p.exists():
            scores[n] = 0.0
            continue
        try:
            data = p.read_bytes()
        except Exception:
            scores[n] = 0.0
            continue
        sz = len(data)
        sizes.append(sz)
        md5s.append(hashlib.md5(data).hexdigest())
        # crude PNG resolution parse (IHDR @ offset 16: width(4) height(4) big-endian)
        w = h = 0
        if data[:8] == b"\x89PNG\r\n\x1a\n" and len(data) >= 24:
            w = int.from_bytes(data[16:20], "big")
            h = int.from_bytes(data[20:24], "big")
        resolutions.append((w, h))
        # ≥ 20 KB and ≥ 800x500 to defeat blank/placeholder screenshots
        ok_size = sz >= 20 * 1024
        ok_res = (w >= 800 and h >= 500)
        scores[n] = 1.0 if (ok_size and ok_res) else (0.4 if sz >= 5 * 1024 else 0.0)

    md5_unique_ratio = (len(set(md5s)) / len(md5s)) if md5s else 0.0
    scores["screenshot_diversity"] = round(md5_unique_ratio, 3)

    # ---- 2. root_cause.md: length AND must mention real concepts ----
    rc = ""
    if (rd / "root_cause.md").exists():
        try:
            rc = (rd / "root_cause.md").read_text(encoding="utf-8", errors="ignore")
        except Exception:
            rc = ""
    rc_len_ok = 80 <= len(rc) <= 600
    rc_low = rc.lower()
    # require evidence of real diagnosis: mentions category/case mismatch + merge/NaN
    has_case_concept = any(k in rc_low for k in ["大小写", "case", "lowercase", "uppercase", "lower", "upper"])
    has_merge_concept = any(k in rc_low for k in ["merge", "join", "匹配", "match", "category"])
    has_nan_concept = any(k in rc_low for k in ["nan", "缺失", "null", "missing"])
    rc_concept_hits = sum([has_case_concept, has_merge_concept, has_nan_concept])
    scores["root_cause_len"] = 1.0 if rc_len_ok else 0.0
    scores["root_cause_concept"] = round(rc_concept_hits / 3.0, 3)

    # ---- 3. fix.patch validity + must touch DISCOUNT_RULES / category ----
    patch = ""
    if (rd / "fix.patch").exists():
        try:
            patch = (rd / "fix.patch").read_text(encoding="utf-8", errors="ignore")
        except Exception:
            patch = ""
    valid_diff = ("---" in patch and "+++" in patch and "@@" in patch and len(patch) >= 80)
    scores["valid_patch"] = 1.0 if valid_diff else 0.0
    # patch must touch the offending area (category column or DISCOUNT_RULES literal)
    p_low = patch.lower()
    touches_target = (
        "discount_rules" in p_low
        or "category" in p_low
        or ".str.lower" in p_low
        or ".str.upper" in p_low
        or re.search(r"\['?[abc]'?", p_low) is not None
    )
    scores["patch_targets_bug"] = 1.0 if (valid_diff and touches_target) else 0.0

    # ---- 4. parquet must be NaN-free AND row count == 5 (no truncation cheat) ----
    parquet_ok = 0.0
    parquet_full = 0.0
    out = Path("/tmp_workspace/buggy_pipeline/output.parquet")
    if out.exists():
        try:
            import pandas as pd
            df = pd.read_parquet(out)
            if "discounted_price" in df.columns:
                parquet_ok = 1.0 if df["discounted_price"].notna().all() else 0.0
                parquet_full = 1.0 if (parquet_ok == 1.0 and len(df) >= 5) else 0.0
        except Exception:
            pass
    scores["parquet_ok"] = parquet_ok
    scores["parquet_full"] = parquet_full

    # ---- 5. VLM rubric on actual screenshots ----
    imgs = [str(rd/n) for n in shot_names if (rd/n).exists()]
    vlm_available = False
    if vlm_score_rubric and imgs:
        rubric = {
            "vlm_spyder_window": "截图显示 Spyder IDE 界面（含 Variable Explorer 面板和编辑器）",
            "vlm_variable_explorer": "至少一张截图清晰展示了 Variable Explorer 中 df 变量的列与样本值",
            "vlm_breakpoint_indicator": "编辑器栏显示了断点（红点 / 高亮行）或调试器停在断点处",
            "vlm_pandas_head_visible": "pandas_head.png 显示 DataFrame.head() 输出，且 discounted_price 列没有 NaN",
        }
        try:
            vlm = vlm_score_rubric(imgs[:4], rubric, instruction="评估 Spyder 调试器单步追踪 silent NaN bug 的截图质量；只在确实可见时给高分。")
            for k in rubric:
                scores[k] = float(vlm.get(k, 0.0) or 0.0)
            scores["judge_method"] = vlm.get("judge_method", "failed")
            vlm_available = scores["judge_method"] != "failed"
        except Exception:
            for k in rubric: scores[k] = 0.0
            scores["judge_method"] = "failed"

    # ---- 6. Weighted aggregation ----
    # Core deliverables (parquet fixed + valid patch + root cause concept) 60%
    core = (
        0.30 * scores.get("parquet_full", 0.0)
        + 0.15 * scores.get("patch_targets_bug", 0.0)
        + 0.10 * scores.get("root_cause_concept", 0.0)
        + 0.05 * scores.get("root_cause_len", 0.0)
    )
    # GUI evidence (4 screenshots + diversity + VLM Spyder UI confirmation) 30%
    shot_avg = sum(scores.get(n, 0.0) for n in shot_names) / 4.0
    vlm_ui = (
        scores.get("vlm_spyder_window", 0.0)
        + scores.get("vlm_variable_explorer", 0.0)
        + scores.get("vlm_breakpoint_indicator", 0.0)
    ) / 3.0
    gui = (
        0.10 * shot_avg
        + 0.05 * scores.get("screenshot_diversity", 0.0)
        + 0.15 * vlm_ui
    )
    # Aux (pandas head VLM + valid patch structure) 10%
    aux = (
        0.05 * scores.get("vlm_pandas_head_visible", 0.0)
        + 0.05 * scores.get("valid_patch", 0.0)
    )
    base = core + gui + aux

    # ---- 7. Multi-layer hard gates ----
    # Gate A: core deliverable broken → cap 0.40
    if scores.get("parquet_full", 0.0) < 1.0:
        base = min(base, 0.40)
    # Gate B: no real GUI evidence (Spyder UI not visible) → cap 0.45
    spyder_ui = max(
        scores.get("vlm_spyder_window", 0.0),
        scores.get("vlm_variable_explorer", 0.0),
        scores.get("vlm_breakpoint_indicator", 0.0),
    )
    if spyder_ui < 0.5:
        base = min(base, 0.45)
    if spyder_ui < 0.3:
        base = min(base, 0.30)
    # Gate C: screenshot duplication / cheating → cap 0.40
    if md5s and md5_unique_ratio < 0.75:
        base = min(base, 0.40)
    # Gate D: missing patch or root cause → cap 0.50
    if scores.get("patch_targets_bug", 0.0) < 1.0 or scores.get("root_cause_concept", 0.0) < 0.66:
        base = min(base, 0.50)
    # Gate E: VLM unavailable → cap 0.60 (cannot verify GUI claims)
    if not vlm_available:
        base = min(base, 0.60)

    scores["overall_score"] = round(max(0.0, min(1.0, base)), 3)
    return scores
```

## Workspace Path

```
workspace/DAV/task_0_spyder_step_debug
```

## Skills

```
```

## Env

```
```

## Warmup

```bash
which spyder >/dev/null 2>&1 || (apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq spyder) || true
python3 -c "import pandas" 2>/dev/null || pip3 install --break-system-packages -q pandas pyarrow 2>/dev/null || pip3 install -q pandas pyarrow 2>/dev/null || apt-get install -y -qq python3-pandas python3-pyarrow || true
python3 -c "import pyarrow" 2>/dev/null || apt-get install -y -qq python3-pyarrow || true
```