Files
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

737 lines
32 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# bench_env task code spec
> This is the **hard spec** for all Python code under `bench_env/task/` — naming, forbidden patterns, error handling, file responsibilities, docstrings. Every new or modified task must comply; PR reviews follow this doc.
>
> Authoring workflow: [`TASK_AUTHORING_GUIDE.md`](TASK_AUTHORING_GUIDE.md); testing: [`TASK_TESTING_GUIDE.md`](TASK_TESTING_GUIDE.md).
---
## 1. File responsibilities
Per-suite file responsibilities:
| File | Responsibility | Must NOT contain |
|---|---|---|
| `tasks.py` / `defs/<Name>.py` | Task class definitions | Data-access helpers, generic utilities, App-private constants, custom base classes |
| `app.py` | App accessor (subclass of `BaseApp`): data methods / answer methods / `check_*` methods / `prepare_state_with_*` helpers / App-private samplers / `expected_changes` constants | Task-specific decision logic, business rules, runtime-environment operations |
| `__init__.py` | Module marker | Any real code |
### 1.1 Task files (`tasks.py` / `defs/*.py`)
**Allowed**:
- Task class definitions (inheriting from `BaseTask` / `CriteriaTask` / `AnswerTask`)
- Importing and composing `expected_changes` constants from app.py
- import statements
- Task Index comments (only in `tasks.py`)
**Forbidden**:
- Custom base classes, mixins, abstract intermediaries (all tasks must inherit a standard base class directly)
- Module-level helper functions (`_chk()` / `_build_xxx()`) → belong in app.py or utils.py
- App-private data constants (sampling pools, route tables, value-mapping constants) → belong in app.py
- Inter-task inheritance (task A must not inherit from task B then tweak a single check; it looks like dedup but actually couples them)
- Private compute/aggregation methods on the task class (`_rain_days_next_week()` etc.) → generalize into App methods
- Sampler function definitions → `sample_*` belongs on the App class
**Authoring principles**:
1. **Each task is self-contained** — does not inherit from another task, does not depend on file-level shared helpers
2. **Inherit only from the standard base classes**
3. **Declarative first** — if you can express it via `answer = ".path"` / `criteria = {...}`, don't write a method
4. **Task class only composes; it doesn't compute** — data traversal, aggregation, and computation move to App methods
### 1.2 App class (`app.py`)
**Encapsulate**:
| Yes | No |
|---|---|
| Data-access complexity (multi-step lookup, fuzzy matching, field compatibility) | Task-specific conditional branching |
| Data traversal, aggregation (even if only one task uses it today) | Embedded UI-layer formatting |
| init vs current diffs (generic comparison) | Hard-coded business rules (`"转账" in name`) |
| Schema-coupled setup (`prepare_state_with_*`) | Direct manipulation of `env.get_state()` / `env.set_state()` |
| App-private samplers (`@staticmethod sample_*`) | UI layout logic |
**Decision criteria**:
1. About where data lives, how to fetch it, what fields exist → App
2. About what to do with data after fetching → Task
3. Hard-codes a specific business rule → Task
4. Used by only one task? **Still check whether it's data-access complexity** — if so, App; if not, Task
For the detailed three-tier breakdown (data / answer / check methods) see [`TASK_AUTHORING_GUIDE.md`](TASK_AUTHORING_GUIDE.md) §2.
### 1.3 `task/utils.py`
Cross-suite pure functions: text processing, time utilities, data parsing, check combinators (`check_alternatives`).
`check_alternatives(*check_arrays)` is candidate-wise OR: same-index checks across arrays form one candidate, the first all-pass candidate is returned, and if none pass the first candidate is returned for diagnostics. Arrays must be non-empty and the same length.
**Forbidden**:
- Defining shared utilities locally in a task file
- Inlining generic parsing logic in app.py
---
## 2. Naming conventions
### 2.1 App class names
Correspond to `manifest.id`, PascalCase, **without** an `App` suffix:
| `manifest.id` | Correct | Wrong |
|---|---|---|
| `wechat` | `Wechat` | `WechatApp` |
| `bilibili` | `Bilibili` | `BilibiliApp` |
| `railway12306` | `Railway12306` | `RailwayApp` |
### 2.2 Task class names
Must **accurately reflect the task goal**:
```python
# ❌ Name doesn't match behavior
class BalanceThresholdCheck(AnswerTask): # Actually just reads balance; no threshold
class ClearHistory(BaseTask): # Actually sets map orientation
# ✅
class CheckBalance(AnswerTask):
class SetMapOrientation(BaseTask):
```
### 2.3 `objective` must match content
```python
# ❌ objective is wrong
class OpenChatWithContact(CriteriaTask):
objective = "query" # Actually a navigation operation
# ✅
class OpenChatWithContact(CriteriaTask):
objective = "operate"
```
### 2.4 Objective and phrasing alignment
| Objective | Correct phrasing | Wrong pattern |
|---|---|---|
| `operate` | "Set XX to YY", "Open XX" | "Search XX, view YY" (viewing without output) |
| `query` | "View/check XX and tell me", "How much is XX?" | "Set XX" (state-changing instruction) |
| `hybrid` | "Find XX for me, tell me how many" | Same operate/query wrong patterns |
### 2.5 `expected_changes` constants
Named `<APP_NAME>_<ACTION>_CHANGES`, defined in the corresponding `app.py`:
```python
# wechat/app.py
WECHAT_SEND_CHANGES = ["wechat.chats"]
WECHAT_MOMENT_CHANGES = ["wechat.moments"]
```
---
## 3. Template authoring
### 3.1 Express intent, not steps
```python
# ❌ Step description
templates = ["搜索地点'{place}',查看从当前位置到该地点的驾车路线"]
# ✅ Intent
templates = ["帮我在地图上找到从当前位置到'{place}'的驾车路线"]
```
### 3.2 Don't make the Agent "view" without output
If the instruction contains "view"/"check":
- Make it an `AnswerTask` and let the Agent reply → "View XX and tell me"
- Or rephrase to drop the "view" → "Open the XX page for me"
### 3.3 Templates must not leak answers
`AnswerTask` `templates` **must not contain placeholders for the answer** (e.g., `{phone}` / `{income}` / `{balance}`). The answer should be derived from App state via `get_answer()` or declarative `answer`. Declarative `answer` reads `input.apps_init` by default; use an explicit `get_answer()` with `input.apps` for final-state answers after an operation.
```python
# ❌ Template leaks the answer
templates = ["找到好友'{name}',并记录其电话号码 {phone}"]
# ✅ Drop {phone}, use a declarative answer
templates = ["在支付宝里找到好友'{name}',告诉我他的电话号码"]
answer = ".contacts[name={name}].phone"
```
### 3.4 `{param}` position inside templates
When a bool parameter combined with `values` dict is rendered, the full sentence must read naturally:
```python
# ✅ Renders as: "在 Spotify 中关闭'向他人展示我的收听活动'"
templates = ["在 Spotify 中{share_activity}'向他人展示我的收听活动'"]
parameters = {"share_activity": {"type": "bool",
"values": {"开启": True, "关闭": False}}}
# ❌ Renders as: "在隐私设置中关闭 XXX 开启并确认状态更新"
templates = ["在隐私设置中关闭 XXX {share_off} 并确认状态更新"]
```
### 3.5 L3 / L4 tasks should provide multiple template variants
To prevent the Agent from overfitting specific wording. **Critical constraint**: every variant must require the same answer content (same number of slots, same fields, same information).
```python
class FilterHeadphones(CriteriaTask):
templates = [
"帮我找最便宜的全新 Sony 耳机,只看日本发货且包邮的,告诉我有几款",
"我想要一副 Sony 的新耳机,从日本发货、不要运费的那种,有多少选择?",
]
```
---
## 4. Error handling: task-design error vs. agent execution failure
**One of the most important rules.**
| Situation | Correct | Wrong |
|---|---|---|
| Environment data missing (contact doesn't exist, DB empty, fixture missing) | App method `raise ValueError(...)` or task `raise RuntimeError(...)` | `return False` ❌ |
| App accessor data missing (field not found, no query result) | App method `raise ValueError(...)` | Return `""` / `None` ❌ |
| Agent failed to act (wrong route, wrong value) | `passed=False` | `raise RuntimeError(...)` ❌ |
```python
# ✅ App accessor raises when data is missing
class Map(BaseApp):
def place_address(self, name):
place = self._find_place(name)
if not place:
raise ValueError(f"Place {name!r} not found in state")
return place["address"]
# ❌ Silent empty return; task ends up with _require_runtime_answer() boilerplate
class Map(BaseApp):
def place_address(self, name):
place = self._find_place(name)
return place.get("address", "") if place else ""
```
**Why this matters**:
- `raise RuntimeError` → there's a bug in the task or environment; needs fixing
- `passed=False` → the Agent didn't succeed; a normal evaluation outcome
- Conflating the two attributes a sampler bug to the Agent silently
---
## 5. No defensive coding
**All code** under `bench_env/task/` must not defensively guard. Missing data, missing keys, type errors — they should surface as exceptions.
| ❌ Forbidden | ✅ Correct |
|---|---|
| `latest_order or {}` + `.get()` | Direct key access `latest_order["field"]` |
| `.get("key", "")` for a required field | `["key"]` |
| `(passenger or {}).get("name", "") or "Unknown"` | Declarative `answer = ".passengers[isDefault=True].name"` |
| `if x is not None` guard | Use `x` directly |
| `try / except` with a fallback return | Call directly and let the exception propagate |
| `input.apps.get("xxx", {})` | `input.apps["xxx"]` (framework guarantees a dict) |
| `(input.apps_init or {}).get("xxx")` | `input.apps_init["xxx"]` |
| `input.os or {}` | `input.os` |
**Legitimate handling of Agent failures**: use explicit ternaries in check dicts, not `(x or {}).get()`:
```python
# ✅ Agent may not have created the order; order being None is legitimate
return [{
"field": "newPendingOrder.trainNo",
"expected": target_train["trainNo"],
"actual": order["trainNo"] if order else None,
"passed": order is not None and order["trainNo"] == target_train["trainNo"],
}]
# ❌ or {} masks None
"actual": (order or {}).get("trainNo")
```
**No fallback returns in `get_answer()`**:
```python
# ❌
def get_answer(self, input):
temp = w.weather_now(self.p.city).get("temp")
return temp if temp is not None else "无法判断"
# ✅ Missing data should be raised by the App accessor
def get_answer(self, input):
return Weather(input.apps["weather"]).current_temp(self.p.city)
```
---
## 6. Forbidden time APIs
The following APIs **must not** be used under `bench_env/task/` to obtain or derive "current time":
| ❌ Forbidden | ✅ Replacement |
|---|---|
| `datetime.date.today()` | `sim_today(os_state)` |
| `datetime.datetime.now()` | `sim_datetime(os_state)` |
| `time.time()` | `now_ms(os_state)` |
| `datetime.datetime.fromtimestamp(time.time())` | `sim_datetime(os_state)` |
**No fallback to local time** — if `os_state` lacks `time`, `raise ValueError`; don't silently degrade.
**Exception**: converting an already-stored timestamp (e.g., `transferRecords[].timestamp`) with `datetime.datetime.fromtimestamp(ts)` is fine — this isn't about local time vs. simulated time, just formatting an existing absolute value.
---
## 7. CriteriaTask rules
### 7.1 `criteria` must be a class variable
```python
# ❌
class EnableDarkMode(CriteriaTask):
@property
def criteria(self):
return {"settings.general.darkMode": True}
# ✅
class EnableDarkMode(CriteriaTask):
criteria = {"settings.general.darkMode": True}
```
### 7.2 Parameterize with `"{param}"` template
```python
# ❌
@property
def criteria(self):
return {"searchForm.from": self.p.station}
# ✅
criteria = {"searchForm.from": "{station}"}
```
**The `"{param}"` template preserves the original Python type** for all parameter types — when the entire value is a pure single-parameter reference (e.g., `"{flag}"`, `"{count}"`), the framework returns `self.params[key]` directly, so `bool`/`int`/`float` are not converted to strings by `str.format()`. Mixed templates (e.g., `"prefix-{key}"`) still go through `str.format()`.
### 7.3 Value mapping uses `values` dict; no `_XXX_MAP`
```python
# ❌
_SIZE_MAP = {"最小": 0, "较小": 1, ...}
@property
def criteria(self):
return {"settings.fontSizeLevel": _SIZE_MAP[self.p.size_label]}
# ✅
parameters = {"font_size": {"type": "enum",
"values": {"最小": 0, "较小": 1, "标准": 2}}}
criteria = {"settings.fontSizeLevel": "{font_size}"}
```
### 7.4 Parameter semantics must match the store
```python
# ❌ Reversed semantics, forces a `not`
parameters = {"share_off": {"type": "bool", "default": True}}
@property
def criteria(self):
return {"settings.shareActivity": not self.p.share_off}
# ✅ Parameter value maps directly to store value
parameters = {"share_activity": {"type": "bool",
"values": {"开启": True, "关闭": False}}}
criteria = {"settings.shareActivity": "{share_activity}"}
```
---
## 8. `check_goals` return format
### 8.1 Each check must include `passed`
The framework raises `ValueError` for missing `passed` in `is_successful()` / `evaluate()`.
| Field | Type | Required | Description |
|---|---|---|---|
| `field` | `str` | yes | Check item name |
| `expected` | `Any` | yes | Expected value (must be readable) |
| `actual` | `Any` | yes | Actual value (must be readable) |
| `passed` | `bool` | **yes** | Pass / fail |
### 8.2 `expected` / `actual` must be diagnostic
These two values appear directly in logs and are the only clue when a task fails. **Do not write `expected=True, actual=None`**:
```python
# ❌ No diagnostic value
{"expected": True, "actual": order, ...}
# Log shows: expected=True, actual=None → can't tell what was expected or what the Agent did
# ✅ Human-readable summary
{"expected": "上海→南京 2026-03-21 G7002 二等 ×1 (赵宇轩)",
"actual": "未创建新订单", ...}
```
Full `check_goals` authoring rules: [`TASK_AUTHORING_GUIDE.md`](TASK_AUTHORING_GUIDE.md) §4.7.
---
## 9. Direct `JudgeInput` access
`JudgeInput.apps` / `apps_init` / `os` are guaranteed dicts by the framework — **index them directly**:
```python
# ✅
rail = Railway12306(
input.apps["railway12306"],
init=input.apps_init["railway12306"],
)
# ❌
app = Wechat(
input.apps.get("wechat", {}),
init=(input.apps_init or {}).get("wechat"),
)
```
A missing key means a config bug — let `KeyError` surface; don't mask it with `.get()` or `or {}`.
### 9.1 Declarative `answer` resolves against initial state
**All declarative `AnswerTask.answer` forms (path / tuple / dict / callable) read from `input.apps_init`** — the state captured at task setup. This matches pure-query semantics: the ground truth was frozen the moment the task started; whatever the Agent does shouldn't change "what the right answer was."
| Form | Reads from |
|---|---|
| `answer = ".x.y"` / `"app:.x.y"` | `input.apps_init` |
| `answer = (".x", fn)` | `input.apps_init` (then `fn`) |
| `answer = {"slot": ".x", ...}` | `input.apps_init` (per slot) |
| `answer = staticmethod(lambda t, apps_init: ...)` | full `input.apps_init` dict — index by app name (cross-app supported) |
| `def get_answer(self, input): ...` (override) | **you choose** — read `apps_init` for queries, `apps` for post-action |
**Migration note:** prior to this change, declarative `answer` resolved against `input.apps`. If you wrote an `AnswerTask` whose answer is supposed to reflect state *after* the Agent acts (hybrid query-after-action), switch from the declarative shorthand to an explicit `get_answer()` that reads `input.apps`.
---
## 10. Data source policy
Priority for fetching App data: **`getState()` runtime state > app offline data files > hard-coded constants**.
**Before defining a constant, check whether the data is already in `getState()`**. If it is (e.g., `recentPlays` / `likedSongs` / `contacts`), **never duplicate it as a Python module-level constant** — the copy will drift from the source.
Parameter sampling priority: **`source` > `sampler` > hard-coded `enum`**. Use `enum` only when the value domain is unrelated to environment data (e.g., a fixed enum like "last half year / last month").
If a parameter has only one meaningful value, use `default` only. Do not add a fake `source` / `sampler` just to look generalized. Numeric parameters intended to vary must declare `min`/`max`, `values`, `source`, or `sampler`.
---
## 11. Placeholder task marker
Tasks where the App functionality is incomplete must be marked with `note`:
```python
class CheckSesameCredit(AnswerTask):
note = "App page not yet implemented; needs sesame-credit page"
```
**Don't** hard-code return values in `get_answer()` to fake content (`return "59"`). Pure query answers should derive from `input.apps_init` / `input.os_init`; post-action answers should derive from `input.apps` / `input.os`.
---
## 12. Task docstring convention
A Task class docstring captures **design decisions that the code can't self-document**.
### 12.1 Write these
**1. Verdict: what counts as done, and why it can be judged correctly**
- **What is checked** — the success state in semantic terms ("WeChat new message contains the hotter city's name and its temperature")
- **Why it can be judged** (only when non-obvious) — the design point that disambiguates:
- Phrasing disambiguation: "first" means the first item in the search result list
- Parameter design: `{city}` is shared between weather and map, so consistency is intrinsic
- Sampling constraint: sampled to guarantee two books have different ratings; comparison is unambiguous
- Template anchor: the template fixes the note title, giving the judge a stable anchor
- Branch design: state which branch maps to which expected content
**2. Data injection — only when the task needs preset env state**
- "Alipay balance is randomly placed at 80%120% of ticket price to cover both branches"
- "Target video must be un-liked; otherwise the operation has no effect"
- "Route must have ≥2 high-speed trains, otherwise 'earliest' has no meaning"
### 12.2 Don't write these
- **The implementation path of the judgment** — `check_searched()` / `redbook.first_search_note(keyword).title` etc. belong with the code itself
- **Restating the template** — anything that can be read from the template
- **Generic design intent** — what's already inferable from class name and template
### 12.3 Examples
```python
# Simple task — name + template already self-explanatory
class WeatherSummaryToWechat(BaseTask):
"""Verdict: WeChat new message contains {city}'s current weather and temperature."""
templates = ["查一下{city}现在天气怎么样,发给微信好友{contact}"]
# Disambiguation design
class WeatherFilterNonRainyDays(BaseTask):
"""The template fixes the note title '适合出行的日子' as a stable anchor for the judge.
Verdict: note title matches; body contains every non-rainy date."""
templates = ["查{city}未来五天天气,把不下雨的日期记在笔记里,标题写'适合出行的日子'"]
# Needs data injection
class RailwayPriceVsBalance(AnswerTask):
"""Verdict: the agent's 'enough'/'not enough' answer agrees with the actual comparison.
Injection: random Alipay balance in 1001000 to cover both branches."""
templates = ["查{date}{from}{to}最便宜的高铁票多少钱,再看看支付宝余额够不够买"]
```
### 12.4 Rules
1. **Document only what the code can't self-document** — if the name + template + parameters explain it, don't add words
2. **State what and why, not how** — describe the success condition and the reason it's judgeable; don't restate `check_goals`
3. **Keep it short** — 15 lines. If the logic needs paragraphs of explanation, the task itself probably needs simplification
4. **Update the docstring when you change the task** — a stale docstring is worse than none
---
## 13. Common pitfalls
Every entry below comes from a real past bug.
### 13.1 Pipeline awareness: state may already be enriched
App state goes through `defaults.json → data/index.ts (enrichment) → store → bench_env state`. Many apps enrich records in `data/index.ts` or store actions (e.g., Alipay's `enrichTransferRecord` fills in `category` / `kind` / `displayTitle`), so those fields **already exist** in the state the judge sees.
**Typical mistake**: writing 80 lines of if-else regex in `app.py` to infer `category`, when `category` is already a field on the record.
**Rule**: before writing judge logic, check whether the target field is already present in `data/index.ts` or `state.ts`. **Read it directly; don't re-derive it in Python**.
### 13.2 `get_answer()` return type must match `match_value` and Agent phrasing habits
The Agent is a VLM — it reads the screen and replies in its own words. `match_value` has different semantics per type. If the two don't line up, judging is wrong.
| Pitfall | Wrong | Correct |
|---|---|---|
| Trailing zeros | `f"{total:.2f}"``"278.20"` | `f"{round(total, 2):g}"``"278.2"` |
| Month format | `"2026-01"` | `f"{year}年{month}月"` |
| Returns str instead of number | `return str(count)` | `return count` (int) |
| Time/duration via `match_value` | `build_answer_checks({"历时": "0小时59分"}, answer)` | Use `match_duration` / `match_time` in `check_goals` |
**Principle**: think from the Agent's perspective — how would it phrase the answer after seeing the screen? The return type of `get_answer()` must let `match_value` match reasonable variants.
### 13.3 CriteriaTask must ensure initial state ≠ target state
If `criteria`'s target value happens to equal the initial value in `defaults.json`, the task passes without the Agent doing anything.
| Scenario | Needs `_invert_criteria` |
|---|---|
| Target varies (toggle / enum parameter) | **Yes** |
| Target fixed, but equals initial value | **Yes** |
| Target fixed, initial value is already the opposite | No |
### 13.4 `expected_changes` must cover all side effects
Undeclared side effects cause `clean=False` warnings.
| Operation | Easily-missed fields |
|---|---|
| Viewing messages | `conversations.lastReadAt` |
| Transfer / payment | `transferDraft` / `transferReceipt` / `lastPaymentHint` |
| Search | `searchHistory` / `billSearchHistory` |
| Favorite / like | `favoriteIds` / `likedIds` |
**Rule**: run the task once in the UI, diff before vs. after, and add every changed path.
### 13.5 `_prepare()` must not hard-code data replacements
`_prepare()` exists to configure initial state when `defaults.json` defaults are insufficient. **It is not** a place to hard-code data to "control difficulty".
```python
# ❌ Defaults are fine; still injects a hard-coded copy
async def _prepare(self, env):
await env.set_state({"apps": {"spotify": {"likedSongs": [
{"id": "song_001", "artist": "周杰伦", ...},
...
]}}})
# ✅ Use defaults.json; if defaults are wrong, fix defaults.json
```
**Rule**:
1. Prefer the data already in `defaults.json`
2. If defaults don't fit, **stop and raise the issue** — change `defaults.json`, don't silently overlay hard-coded data in `_prepare()`
3. When injection truly is needed, push the schema-coupled construction down to a `prepare_state_with_*` helper in the corresponding `app.py`
### 13.6 Route criteria must account for query params
Routes often contain query params (e.g., `/chat?id=conv_p_10&type=person`); a hard-coded `criteria` of `"/chat"` fails.
**Rule**:
- If the task goal is not "navigate to a specific page", **do not put `route` in `criteria`** — the state change itself is enough
- If you do need a route check, confirm the actual route format and use an appropriate matcher
---
## 14. Final checklist
Use this checklist when adding or modifying a task. PR reviews compare against it.
### Design & base class
- [ ] **Merge vs split**: for multi-variant tasks, are the parameters orthogonal and under the same interaction? If params are coupled or interactions differ, split them.
- [ ] **Base class**: using the most suitable standard class? `CriteriaTask` / `AnswerTask` before `BaseTask`.
- [ ] **Declarative first**: anything expressible via `answer = ".path"` / `criteria = {"key": "value"}` is declared, not coded.
- [ ] **Class name**: accurately reflects the goal.
- [ ] **`objective` correct**: matches the actual behavior.
### Metadata
- [ ] **Four axes + capabilities**: `scope` / `objective` / `composition` / `difficulty` / `capabilities` all set.
- [ ] **Difficulty calibrated**: matches the Golden Steps range.
- [ ] **`max_steps` valid if set**: omit it for the difficulty default, or set exactly one of `15`, `30`, `45`, `60`.
- [ ] **`capabilities` only core**: 14 entries; nav and other prerequisite steps not labeled.
### Template
- [ ] **Intent, not steps**.
- [ ] **No "view" without output**: only `query` tasks use "view"; `operate` uses "do this for me".
- [ ] **No answer leak**: `AnswerTask` templates don't carry answer params (`{phone}` / `{income}`).
- [ ] **`{param}` positioned**: rendered sentence reads naturally for bool with `values`.
- [ ] **L3/L4 has variants**: 2+ templates with identical expected answer content.
### CriteriaTask
- [ ] **`criteria` is a class variable** (not `@property`).
- [ ] **Parameterize with `"{param}"`**.
- [ ] **Value mapping via `values` dict** (no `_XXX_MAP`).
- [ ] **Array lookup uses `[field={param}]`** (no hand-written `check_goals`).
- [ ] **Missing target checks use `None` deliberately**: `[field={param}] = None` means the resolved path is absent or `None`.
- [ ] **Parameter semantics match store** (no `not` to flip).
- [ ] **Initial state ≠ target**: toggle/enum tasks use `_post_sample` + `_invert_criteria`?
- [ ] **Custom `check_goals()` preserves criteria** via `super().check_goals(input)`, or the task uses `BaseTask`.
- [ ] **Array/filter paths don't rely on `_invert_criteria`**; write custom `_post_sample()` when the path contains `[`.
### AnswerTask
- [ ] **Prefer `answer` class var**: path / dict-of-paths / callable / literal.
- [ ] **`get_answer()` only when necessary**.
- [ ] **`get_answer()` returns ground truth, not judgment logic**.
- [ ] **Ties / synonyms use `re.Pattern`** (not a hard-coded string).
- [ ] **Answer-computation pushed into App answer methods** (one-line `get_answer()`).
- [ ] **Boolean queries**: detect negation before affirmation inside `check_goals`.
- [ ] **Date matching uses `date_match_labels(date, input.os)`** (pass `os_state` for relative dates).
- [ ] **Time/duration uses `match_time` / `match_duration`** (not `match_value` substring).
- [ ] **Don't validate non-answer content via `input.answer`** (messages/operations should check state).
### check_goals
- [ ] **Every check has `passed`**.
- [ ] **expected / actual are readable** (no `expected=True, actual=None`).
- [ ] **Only check Agent behavior** (no env preconditions or out-of-control conditions).
- [ ] **One semantic goal = one check** (don't split into field-level checks).
- [ ] **High-frequency patterns use App `check_*`** (task-specific logic stays inline).
- [ ] **Cover the template's implicit constraints** ("send to Moments" implies no image attachments).
- [ ] **`operate` tasks only check final state** (no mid-process checks; navigation tasks are an exception).
- [ ] **True dependencies fill placeholders in subsequent checks** (return list keeps a stable length).
### Judging reliability
- [ ] **Soundness**: wrong paths can't pass; no broad-keyword or weak-trace fields letting the Agent through.
- [ ] **Completeness**: reasonable rewordings or alternative completion paths can't fail; no binding to original title / full text / fixed phrasing.
- [ ] **Checkpoint reliability**: if checking an intermediate state, it must be both necessary for the task and stably observable.
### Error handling
- [ ] **App accessor raises `ValueError`** on missing data (no `""` / `None`).
- [ ] **Task has no data-validation boilerplate** (rely on App accessor errors).
- [ ] **Env-data issue → `raise`; Agent failure → `passed=False`**.
### Defensive coding
- [ ] **No `or {}`, no `.get("key", "")`, no `"无法判断"` fallback**.
- [ ] **Index `input.apps["xxx"]` directly** (no `.get()` / `or {}`).
- [ ] **No fallback returns in `get_answer()`**.
### Parameters
- [ ] **Prefer `source` over hard-coded `enum`**.
- [ ] **Upgrade to `sampler` for filtering / dedup**.
- [ ] **`source` path actually exists in defaults/runtime state**.
- [ ] **Single-value params use `default` only** (no fake `source` / `sampler`).
- [ ] **Numeric params that should vary declare a real domain** (`min`/`max`, `values`, `source`, or `sampler`).
- [ ] **Param values match the store**.
- [ ] **No module-level constants duplicating data already in `getState()`**.
### `_prepare` necessity
- [ ] **Prefer `defaults.json` defaults**.
- [ ] **If defaults are wrong, change `defaults.json`** — don't hard-code a replacement in `_prepare()`.
- [ ] **Necessary injections push down to App `prepare_state_with_*` helpers**.
### expected_changes
- [ ] **Path format correct** (single-app uses relative path; multi-app uses `appName.path`).
- [ ] **`[field=value]` filters target stable fields**; exact match wins, substring fallback is only for legacy convenience.
- [ ] **Covers every side effect** (verify by running the UI and diffing).
- [ ] **Constants defined in the corresponding `app.py`** (named `<APP>_<ACTION>_CHANGES`).
### Time API
- [ ] **Use `sim_today` / `sim_datetime`** (no `time.time()` / `date.today()` / `datetime.now()`).
### File responsibilities
- [ ] **tasks.py / defs/<Name>.py is clean**: no custom base classes, module-level helpers, App constants, or private compute methods.
- [ ] **New helpers live in `app.py`**: data methods / answer methods / `check_*` / `prepare_state_with_*` in their proper tier.
- [ ] **Cross-suite utilities live in `utils.py`**.
### Docstring
- [ ] **Task class has a docstring**.
- [ ] **States "what counts as done" and (when non-obvious) "why it's judgeable" + "what to inject"**.
- [ ] **Doesn't restate the template or describe `how`**.
### Tests
- [ ] **Each task has positive and negative cases** (see [`TASK_TESTING_GUIDE.md`](TASK_TESTING_GUIDE.md)).
- [ ] **AnswerTask positive `answer` is natural language** (not the raw ground truth).
- [ ] **Completeness check passes** (CI won't miss a new task).
### Grounded mode (only if `answer_fields` is declared)
- [ ] **Right field type**: prefer `choice` over `text`; pure numbers use `number`.
- [ ] **Right matcher**: `"time"` for time, `"date"` for date, `"duration"` for duration; otherwise auto by type.
- [ ] **Override `get_expected_response()` when `get_answer()` returns `re.Pattern`** — grounded mode needs exact values.
- [ ] **Override `get_expected_response()` when `get_answer()` returns a dict but `answer_fields` count doesn't match**.
- [ ] **repeatable field**: `get_expected_response()` returns `[[v1, v2, ...]]` (outer list = 1 entry, inner = full list).
- [ ] **hint cross-validates with check**: hint sample value must match both the task's natural expectation and what the check actually accepts.
- [ ] **Custom `check_goals` compatible with both modes**: in grounded mode, `input.answer` is the `", "`-joined AnswerSheet values.
- [ ] **Multi-field same-type false-positive risk**: such tasks shouldn't write custom `check_goals`; let the framework go via Path A.
See [`GROUNDED_MODE.md`](GROUNDED_MODE.md).
### Code-vs-docs
- [ ] **APIs referenced in examples / docs must exist** — every App method, constant, parameter referenced in an example must be findable in the matching `app.py`.
---
## Appendix: why custom base classes are banned
Historically, several suites (railway12306, bilibili, etc.) used custom base classes like `_RailwayBaseTask` / `_RailwayMixin` to deduplicate. They created three problems:
1. **Coupling propagated implicitly through the base** — changing one task could break others
2. **Tasks were no longer self-contained** — reading a single task required understanding the base
3. **Inconsistent abstraction levels** — some tasks went through the base, others didn't, drifting the convention
**Hard rule**: every task inherits one of `BaseTask` / `CriteriaTask` / `AnswerTask` directly. To reduce duplication, push shared logic down into App `check_*` methods; don't introduce intermediate base classes.