| name | e2e-diagnostician |
| description | Diagnose a Baoyu Translate e2e-runner artifact set and act as the gate that decides whether downstream quality + reporter agents run. Reads tests/e2e-out/<ts-sha>/ runner products (captured.md / status.md / console.jsonl / failure.md?), produces a diagnosis.md, and — when the gate fails — additionally writes an e2e-report.md in acceptance form so the upstream caller can stop the pipeline. Use when the user asks to "diagnose e2e", "run e2e gate", "为什么 e2e 没过", or after a /e2e-runner run is complete. |
e2e-diagnostician
Slice 6 of PRD #26. 读 e2e-runner 的全部产物,跑诊断判定清单,产出
diagnosis.md(永远写),并在 gate 不通过时额外写 e2e-report.md
(验收形态,含问题清单 + 影响点 + 修复建议)——让上游调用方知道后续
e2e-quality / e2e-reporter 不执行。
不复现用户路径(那是 runner 的工作),不评翻译质量(那是 quality
的工作),不写最终 e2e-report.md 的成功版(reporter 的工作)。
输入契约
tests/e2e-out/<iso8601-timestamp>_<git-sha>/
├── article-zh-CN-quick/
│ ├── captured.md # 注入译文集合
│ ├── status.md # url/mode/status/段落数/批次/duration
│ ├── console.jsonl # 过滤 [baoyu] 后的全量日志
│ └── failure.md? # 失败时存在
├── article-zh-CN-normal/ (同上)
├── article-zh-CN-refined/ (同上)
└── e2e-report.md # runner 写的跑通/跑挂概览占位
- 必出三模式子目录 + 每模式至少
captured.md/status.md/console.jsonl。
- 缺失任意必出文件 → 整体判未通过,归类为"Incomplete runner artifacts"
(前置条件失败,不属于判定清单 C1-C4 任一项)。
failure.md / e2e-report.md 条件性出现。
- Reference 段落数固定从
tests/reference/article-zh-CN-<mode>/translation.md
读 —— runner 不复制 reference,不重新计数。
判定清单与 runner 产物 schema 的详细定义见 .claude/skills/e2e-runner/SKILL.md。
修改产物 schema 需同步两处(本 skill 读取端 + runner 写入端)。
输出契约
tests/e2e-out/<ts-sha>/
├── diagnosis.md # 永远写:全量判定证据 + 修复建议
└── e2e-report.md # 仅未通过时写:验收形态,问题清单 + 影响点
diagnosis.md 永远写。包含:per-criterion pass/fail + 证据、
per-mode 状态表、最终 verdict(PASS / FAIL)、fix 建议。
e2e-report.md 仅 gate 未通过时写。与 diagnosis.md 内容相同
但 framing 改成"验收形态":第一段直接说"未通过 + 简述根因",
把 issue list 提到头部便于人扫读;不复述判定证据(那部分只在
diagnosis.md)。成功时不写,留给 e2e-reporter 在 e2e-quality 之后
落最终版。
Gate 行为
Gate 是布尔决策,粒度 = 一次 e2e(三个模式聚合):
| 决策 | 写 diagnosis.md | 写 e2e-report.md | 后续 quality / reporter |
|---|
| PASS (无 C1-C4 触发) | ✓ | ✗ | 调用方决定是否继续 |
| FAIL (任一 C1-C4 触发) | ✓ | ✓ | 不执行(被 gate 截断) |
PRD #26 明确:"诊断未通过 → 后续 quality + reporter 不执行"。
e2e-diagnostician 不调用 quality/reporter,只写产物;上游 orchestrator
根据 diagnosis.md 顶端 verdict 决定是否继续调用它们。
verdict 行固定写 <ts-sha> verdict: <PASS|FAIL>(整行可 grep 判定结果)。
诊断判定清单
按以下顺序检查,任一未通过 → 整体 FAIL,记录对应 finding 到
diagnosis.md issue list(每条 finding 含:问题、影响点、修复建议)。
C0 — 前置:runner 产物完整性
- 期望:三模式子目录均存在,每模式至少
captured.md/status.md/console.jsonl
三个文件(非空)。
- 失败 → 标
Incomplete runner artifacts,提示重跑 e2e-runner。不算
判定 C1-C4 失败,直接判 FAIL。
C1 — 无 failure.md
- 期望:三模式子目录下均无
failure.md。
- 失败 → 该模式判 fail,整体 FAIL。Finding 含
failure.md 里的
error/stack/last_log_* 字段。
C2 — 单批 duration 异常
- 读
status.md 的 duration_ms / batch_count:
batch_count == 0 或 duration_ms 缺失 → 跳过本判定(归 C0 或 C1)。
- 计算
avg_batch_ms = duration_ms / batch_count。
- 阈值:单批平均耗时 > 60s(issue #32 example;
status.md 只暴露
duration_ms / batch_count,无 per-batch 计时,所以公式取平均
avg_batch_ms = duration_ms / batch_count,超 60000 即视为异常,
记 finding "Single batch duration > 60s")。
- 影响:单批 60s 意味着要么 API 慢、要么该批段落超长、要么协议层卡住。
修复建议:看
console.jsonl 找 phase:'translate', event:'batch' 的
data.batchStartTs 间隔;若 API 慢则检查 ai-provider-test.txt 与
chrome.storage.local 端 baseURL;若超长则看 data.batchSize。
- 注:此处阈值与
.claude/skills/baoyu-translate/.../refined-workflow.md
无关,是 e2e 流水线级的硬性 SLA,锁在诊断里便于回归。
C3 — console error 事件
- 读
console.jsonl,逐行 JSON.parse,找:
phase == 'task' && event == 'error'
phase == 'ai' && event == 'error'
- 期望:三模式 console.jsonl 中均无以上事件。
- 失败 → 该 finding 列出命中行(
ts + data 摘要,不含 apiKey —— 已被
shared/logging.ts::redactPayload 替换为 [REDACTED])。
- 注:
phase == 'task' && event == 'cancel' 不算 error(
对应用户点取消的合法路径)。
C4 — captured.md 段落数 ±30%
- 读
captured.md 的 paragraph_count: 行(或对文件数空行 split 计数
—— runner 已写好 paragraph_count: 行,直接读)。
- 读
tests/reference/article-zh-CN-<mode>/translation.md 的段落数
(空行 split,与 captured.md 计数方式一致)。
- 阈值:
captured / reference ∈ [0.7, 1.3]。超出 → 标 "Paragraph count
drift ±30%"。
- 边界:若 reference = 0(e.g. reference 文件被误删),跳过本判定并标
"Reference missing" finding,不算 FAIL(这是 C0-C4 唯一不阻断
PASS 的情形),但要写进
diagnosis.md 便于维护者补 reference。
- 影响:段落数偏离意味着 extractor 漏抽或合并段落、injector 重复注入。
修复建议:用
tests/e2e-out/<ts-sha>/<mode>/captured.md 的人工 grep
对照 tests/reference/article-zh-CN-<mode>/translation.md 找差异模式;
视情况回溯 src/content/extractor.ts(见 ADR-0001)。
diagnosis.md shape
# e2e diagnosis — <ts-sha>
verdict: <PASS|FAIL> # 调用方 grep 这一行决定后续
generated_at: <ISO timestamp>
## Per-mode summary
| mode | status | paragraphs (captured / reference) | duration_ms | batches | failure.md | console.error |
|-------------|-----------|-----------------------------------|-------------|---------|------------|---------------|
| quick | complete | 73 / 75 (97%) | 4200 | 1 | absent | 0 |
| normal | complete | 78 / 75 (104%) | 12300 | 2 | absent | 0 |
| refined | FAILED | 12 / 84 (14%) | 6200 | 1 | present | 1 |
## Criteria results
- [x] C0 runner artifacts complete — 3 modes, all files present
- [x] C1 no failure.md — 3 modes, none present
- [x] C2 single batch ≤ 60s — quick avg=4200ms OK; normal avg=6150ms OK; refined avg=6200ms OK
- [ ] C3 no console error events — refined: 1 hit, see findings
- [ ] C4 paragraph count within ±30% — refined captured=12 ref=84 → 14% drift → FAIL
## Findings (FAIL only; ordered by impact)
### F1 — refined mode failure.md present (C1)
- **problem**: `tests/e2e-out/<ts-sha>/article-zh-CN-refined/failure.md` exists.
- error: "Translation cancelled by user via popup" ← 由 failure.md 读出
- last_log_phase: "translate"
- last_log_event: "cancel"
- **impact**: refined 模式未产出完整译文 → quality / reporter 缺一条输入;
反映到端到端是用户能复现的"点 refined 翻译后没反应"。
- **fix suggestion**:
1. 看 console.jsonl 找 `phase:'translate', event:'cancel'` 的 ts 与
`data.reason`;若 `reason == 'user'`,此为用户行为,非缺陷,改
e2e-runner 跑挂模拟。
2. 若非用户取消,grep 全部 pipeline phase(`extract`/`analyze`/
`translate`/`review`/`polish`/`inject`)的 `event:'error'`
找根因(看 SW 端 `src/background/ai-adapter.ts` / `pipeline.ts`
的对应阶段)。
### F2 — console error event in refined (C3)
- **problem**: `tests/e2e-out/<ts-sha>/article-zh-CN-refined/console.jsonl`
含一条 `phase:'task', event:'error'`:
`{"phase":"task","event":"error","ts":1717329014900,"data":{"message":"translate batch failed"}}`。
- `apiKey` 字段不在该 payload(已脱敏)。
- **impact**: translate 阶段在 refined 跑批时失败一次,和 F1 的 cancel 事件
时间相邻(`task.error` ts=1717329014900 vs `task.cancel` ts=1717329015000),
极可能是 F1 的直接上游;用户表现为"点 refined 后译文没出来"。
- **fix suggestion**: 先解决 F1 的 cancel 根因(F1 fix-suggestion #2)后,
该 task.error 通常会一并消失;若仍在,grep 同 console.jsonl 里
`phase:'ai', event:'error'` / `phase:'ai', event:'retry'` 看 provider 端。
### F3 — paragraph count drift (C4)
- **problem**: refined 模式 captured 段落数 = 12,reference = 84,比值 14%
(阈值 70%–130%)。
- **impact**: extractor 漏抽 86% 段落,或 injector 只注入了首批。极可能
伴随 `failure.md` (本例就是)。
- **fix suggestion**: 先解决 F1 后重跑;若 F1 解决后 F3 仍在,看
`console.jsonl` 里 `phase:'extract', event:'done'` 的
`data.paragraphCount` —— 若该值 = 12,问题在 extractor;若 = 84,问题在
injector / 状态恢复。
## Next step
- If verdict = PASS: caller decides to invoke /e2e-quality → /e2e-reporter.
- If verdict = FAIL: STOP. Forward `e2e-report.md` to maintainer.
diagnosis.md 的 issue list (Findings) 只在 FAIL 时写;PASS 时
只写 verdict + per-criteria + per-mode summary + "Next step: invoke
e2e-quality"。
e2e-report.md shape (FAIL only)
# e2e report — <ts-sha>
verdict: <PASS|FAIL> # = diagnosis.md 同 verdict
status: FAILED # 显式一行,人扫读
generated_at: <ISO timestamp>
## TL;DR
<一段话概括:哪几个模式挂了、第一条 finding 是什么、影响是什么>
## Issue list (acceptance form)
| # | mode | criterion | problem | impact |
|---|------|-----------|---------|--------|
| 1 | refined | C1 | failure.md present | 用户能复现的"refined 翻译无反应" |
| 2 | refined | C3 | console task.error (1 hit) | translate 批次失败一次,直接上游是 F1 |
| 3 | refined | C4 | 12/84 段落 (14%) | 86% 段落未注入 |
| ... |
## Per-mode snapshot
<一行/mode,同 diagnosis.md 的 per-mode summary 表>
## Refer to diagnosis.md for full evidence
判定证据(per-criteria pass/fail + 修复建议)完整版在同目录
`diagnosis.md`;本文件是验收形态,只挑结论。
## Next step
**STOP.** 不调用 /e2e-quality / /e2e-reporter。
Maintainer: 修完代码后重跑 `/e2e-runner` + `/e2e-diagnostician`。
e2e-report.md 是给"人"看的(cat / PR 评论 / CI artifact 摘要),
diagnosis.md 是给"调用方 + 修复者"看的(grep verdict 决策 + 看 finding
排查)。两者 verdict 必须一致;两份都写同样的 <ts-sha> verdict: ... 行。
跨契约
ADR 契约(修改需同步两处):
[baoyu] 前缀由 ADR-0002 写入端定义,本 skill 读取端通过
console.jsonl 间接消费(runner 已过滤)。修改前缀需同步 ADR-0002。
- gate 行为由 ADR-0003 §Consequences 锁定。修改 gate 阈值或决策
需同步 ADR-0003。
代码库契约(复用既有实现,不重新声明):
- LogPhase / LogEvent 枚举复用
src/shared/logging.ts(C3 判定
phase == 'task' 与 phase == 'ai' 来自该处)。
- TranslationMode 枚举复用
src/shared/types.ts,与 runner 共享
三模式子目录命名。
- reference 路径固定
tests/reference/article-zh-CN-<mode>/translation.md,
与 ADR-0003 §4.2 锁定。
产物契约(与 runner 严格同步):
tests/e2e-out/<ts-sha>/<mode>/{captured.md,status.md,console.jsonl,failure.md?}
的 schema 与字段名(paragraph_count/duration_ms/batch_count/
status/final_a11y_text/error/stack/last_log_phase 等)由
.claude/skills/e2e-runner/SKILL.md 锁定;本 skill 读取端按该 schema
解析,任一字段缺失回退到 C0 失败(不强行猜)。
手工验证范式
PR / 调试时可手工跑(无需真实 e2e-runner),构造 runner 产物最小集:
TS_SHA="2026-06-02T10-30-00_abcdef1"
RUN="tests/e2e-out/${TS_SHA}"
mkdir -p "$RUN/article-zh-CN-quick" "$RUN/article-zh-CN-normal" "$RUN/article-zh-CN-refined"
场景 A:全 success,期望 PASS
for mode in quick normal refined; do
cat > "$RUN/article-zh-CN-$mode/captured.md" <<EOF
# article-zh-CN-$mode
paragraph_count: 75
captured_at: 2026-06-02T10:30:42Z
<译文段落 1>
<译文段落 2>
EOF
cat > "$RUN/article-zh-CN-$mode/status.md" <<EOF
# article-zh-CN-$mode
url: https://takes.jamesomalley.co.uk/p/this-might-be-oversharing
mode: $mode
status: complete
paragraph_count: 75
batch_count: 1
duration_ms: 4200
final_a11y_text: complete
EOF
echo '{"phase":"task","event":"complete","ts":1717329030000,"data":{"durationMs":4200,"paragraphCount":75}}' \
> "$RUN/article-zh-CN-$mode/console.jsonl"
done
期望:触发本 skill,diagnosis.md 写 + verdict=PASS,e2e-report.md 不写。
调用方可继续 /e2e-quality → /e2e-reporter。
场景 B:某模式 failure.md 存在 + 段落漂移,期望 FAIL
for mode in quick normal; do cp -r "$RUN/article-zh-CN-quick" "$RUN/article-zh-CN-$mode"; done
cat > "$RUN/article-zh-CN-refined/failure.md" <<'EOF'
error: Translation cancelled by user via popup
stack:
final_a11y_text: cancelled
console_jsonl_lines: 8
last_log_phase: translate
last_log_event: cancel
last_log_ts: 1717329015000
EOF
cat > "$RUN/article-zh-CN-refined/status.md" <<EOF
# article-zh-CN-refined
url: https://takes.jamesomalley.co.uk/p/this-might-be-oversharing
mode: refined
status: cancelled
paragraph_count: 12
batch_count: 1
duration_ms: 6200
final_a11y_text: cancelled
EOF
sed -i 's/paragraph_count: 75/paragraph_count: 12/' "$RUN/article-zh-CN-refined/captured.md"
echo '{"phase":"task","event":"error","ts":1717329014900,"data":{"message":"translate batch failed"}}' \
>> "$RUN/article-zh-CN-refined/console.jsonl"
期望:触发本 skill,diagnosis.md 写 + verdict=FAIL,e2e-report.md 也写。
Issue list 含 F1 (failure.md / C1) + F2 (console task.error / C3) +
F3 (段落漂移 / C4)。
不调用 /e2e-quality / /e2e-reporter。
不在本 skill 范围
- 跑 runner:
/e2e-runner 的工作。本 skill 假定产物已存在。
- 质量对比:
captured.md vs reference 的 jaccard / 2-gram / Claude
judge 评分由 /e2e-quality 负责。本 skill 只看结构层信号
(段落数 / 失败标志 / error 事件 / 耗时),不评译文本身。
- 成功 case 的最终 e2e-report.md:本 skill 不写;成功时留给
/e2e-reporter 在 /e2e-quality 之后覆盖。
- CI artifact retention:与 runner 一致,本地产物由
.gitignore
屏蔽(Slice 5 落地),CI retention 由 CI 系统决定。
- threshold 调优:C2/C4 阈值由 issue #32 / ADR-0003 锁定,本 skill
不暴露配置项;调整需改 ADR 并开新 issue。
用法
触发本 skill 后:
- 定位 run 目录:
- 若用户给了
<ts-sha>,用 tests/e2e-out/<ts-sha>/。
- 若没给,
ls -1 tests/e2e-out/ | tail -1 取最新一个时间戳目录。
- C0 完整性检查:三模式子目录 + 必出文件。失败直接判 FAIL 写产物。
- C1-C4 逐条判定:每条收集证据(
failure.md 内容 / status.md
数值 / console.jsonl 命中行 / captured.md 段落数)。
- 写
diagnosis.md:per-mode 表 + criteria 结果 + findings (FAIL 时)
- 若 FAIL,写
e2e-report.md:TL;DR + issue list 表 + next step
"STOP"。
- 回报用户:
- PASS:"Diagnosis PASS for
<ts-sha>. diagnosis.md 写了;e2e-report.md
未写(留给 e2e-reporter)。下一步:调 /e2e-quality。"
- FAIL:"Diagnosis FAIL for
<ts-sha>. diagnosis.md + e2e-report.md
都写了 — 验收形态在 e2e-report.md。后续 quality / reporter 不执行,
修代码后重跑 runner + diagnostician。"
故障排查
| 现象 | 根因 | 修复 |
|---|
| verdict=PASS 但 runner 明显挂了 | C1-C4 漏判;检查 console.jsonl 是不是没被过滤 [baoyu] | 看 raw console 是否含 phase:'task', event:'error' 但 console.jsonl 缺它 → runner 没过滤干净 |
| C4 reference 段落数算错 | reference 文件被手工改过,或读取方式不一致 | awk 'NF>0' tests/reference/article-zh-CN-quick/translation.md | wc -l 对照 captured.md 的 paragraph_count: |
| verdict=FAIL 但实际三个模式都过 | C0 误判(产物缺文件)或 C1 误判(failure.md 不该有) | 看 diagnosis.md 的 criteria 段,看是哪一条先红 |
e2e-report.md 在 PASS 时被生成 | 漏检 verdict 后再写 e2e-report.md 的逻辑 | 写产物前 if-分支:verdict == FAIL 才写 |
console.jsonl 是 null / 不可 parse | runner 写盘时没序列化第二参数 | 重跑 runner;该 bug 不在本 skill 修 |
参考
- PRD #26:父 PRD(Slice 6 / 第 6 片)。
- ADR-0002:日志 schema +
[baoyu] 前缀契约(C3 判定用到 phase 枚举)。
- ADR-0003:e2e 流水线 + gate 行为 + 产物契约。
.claude/skills/e2e-runner/SKILL.md:输入端产物 schema 来源(Slice 5)。
src/shared/logging.ts:LogPhase / LogEvent / LogPayload 定义。
src/shared/types.ts:TranslationMode 枚举。
tests/reference/article-zh-CN-<mode>/translation.md:C4 ground truth。