---
phase: 01-measurement
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
  - scripts/check-regression.sh
  - .github/workflows/benchmark.yml
  - .github/workflows/update-baseline.yml
autonomous: true
requirements:
  - MEAS-04

must_haves:
  truths:
    - "scripts/check-regression.sh 存在，可本地运行，读取 estimates.json 并与 baseline.json 对比"
    - "退化超过 5% 时脚本 exit 1 并输出 baseline 值、current 值、退化百分比（格式由 D-10 指定）"
    - "benchmark.yml 在 Run parser benchmarks 之后有 Check regression 步骤，调用 scripts/check-regression.sh"
    - "update-baseline.yml 存在，手动触发（workflow_dispatch），提交新 baseline.json 时 commit message 含 [skip ci]"
    - "baseline.json 的 key 格式为 parser_group/{bench_name}（不含 _rps 后缀变体）"
  artifacts:
    - path: "scripts/check-regression.sh"
      provides: "CI 回归门禁脚本，可本地复现"
      contains:
        - "THRESHOLD=5"
        - "mean.point_estimate"
        - "exit $FAILED"
        - "FAIL:"
    - path: ".github/workflows/benchmark.yml"
      provides: "修改后的 CI workflow，包含门禁 step"
      contains:
        - "Check regression"
        - "scripts/check-regression.sh"
    - path: ".github/workflows/update-baseline.yml"
      provides: "手动触发的 baseline 重标定 workflow"
      contains:
        - "workflow_dispatch"
        - "[skip ci]"
        - "baseline.json"
  key_links:
    - from: "benchmark.yml（Check regression step）"
      to: "scripts/check-regression.sh"
      via: "bash 调用"
    - from: "scripts/check-regression.sh"
      to: "target/criterion/{group}/{bench}/new/estimates.json"
      via: "jq -r '.mean.point_estimate'"
    - from: "scripts/check-regression.sh"
      to: "benchmarks/baseline.json"
      via: "jq -r 'keys[]' 遍历"
---

<objective>
创建 CI 回归门禁基础设施：编写 scripts/check-regression.sh 脚本，修改 benchmark.yml 添加门禁 step，新建 update-baseline.yml 手动重标定 workflow。

Purpose: 自动检测 benchmark 吞吐量退化，超过 5% 时 CI 失败并报告具体数字，确保后续优化不会意外降低性能。
Output: scripts/check-regression.sh（新建），.github/workflows/benchmark.yml（修改），.github/workflows/update-baseline.yml（新建）。
</objective>

<execution_context>
@/Users/guang/.claude/get-shit-done/workflows/execute-plan.md
@/Users/guang/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-measurement/01-CONTEXT.md

<interfaces>
<!-- 现有文件接口，执行者直接使用 -->

From .github/workflows/benchmark.yml（现有 step 结构）:
```yaml
# 现有 step 顺序：
# 1. Checkout repository
# 2. Install Rust
# 3. Cache cargo registry
# 4. Install cargo-criterion
# 5. Run parser benchmarks   ← 在此之后添加 Check regression step
# 6. Upload benchmark results
# 7. Comment PR with benchmark results
```

From benchmarks/baseline.json（当前格式）:
```json
{
    "parser_group/parse_sqllog_file_5mb": 674425.4692488897
}
```
<!-- 新增变体后 update-baseline.yml 将生成含更多 key 的版本 -->
<!-- key 格式：{group}/{bench_name}，_rps 变体不纳入门禁 -->

From target/criterion 目录结构（已验证）:
```
target/criterion/parser_group/parse_sqllog_file_5mb/new/estimates.json
```
<!-- baseline.json key 解析方式：split('/') 得到 [group, bench_name] -->
<!-- estimates.json 访问路径：target/criterion/{group}/{bench_name}/new/estimates.json -->

estimates.json 结构（已验证）:
```json
{
  "mean": {
    "point_estimate": 536284.86,
    ...
  }
}
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="false">
  <name>Task 1: 创建 CI 回归门禁脚本</name>
  <files>scripts/check-regression.sh</files>
  <read_first>
    - benchmarks/baseline.json（确认 key 格式为 "parser_group/parse_sqllog_file_5mb"）
    - .github/workflows/benchmark.yml（了解 CI 环境，确认 ubuntu-latest 有 jq）
  </read_first>
  <action>
创建 scripts/ 目录（如不存在），创建 scripts/check-regression.sh 文件，内容如下：

```bash
#!/usr/bin/env bash
# scripts/check-regression.sh
# CI 回归门禁脚本：对比 criterion estimates.json 与 baseline.json
# 用法：bash scripts/check-regression.sh
# 退化超过阈值时 exit 1，并输出具体数字（D-10）
set -euo pipefail

BASELINE="benchmarks/baseline.json"
THRESHOLD=5  # 退化百分比阈值（D-11：5%）
FAILED=0

if [[ ! -f "$BASELINE" ]]; then
    echo "ERROR: baseline file not found: $BASELINE"
    exit 1
fi

# 遍历 baseline.json 中所有 benchmark key（格式：{group}/{bench_name}）
while IFS= read -r bench_key; do
    baseline_ns=$(jq -r --arg k "$bench_key" '.[$k]' "$BASELINE")

    # 解析 key 为路径：parser_group/parse_sqllog_file_5mb
    # → target/criterion/parser_group/parse_sqllog_file_5mb/new/estimates.json
    estimates_file="target/criterion/${bench_key}/new/estimates.json"

    if [[ ! -f "$estimates_file" ]]; then
        echo "WARNING: estimates file not found: $estimates_file (skipping)"
        continue
    fi

    current_ns=$(jq -r '.mean.point_estimate' "$estimates_file")

    # 计算退化百分比：(current - baseline) / baseline * 100
    regression=$(awk "BEGIN { printf \"%.1f\", ($current_ns - $baseline_ns) / $baseline_ns * 100 }")

    # 超过阈值则报告失败（D-10：输出格式）
    if awk "BEGIN { exit ($regression > $THRESHOLD) ? 0 : 1 }"; then
        echo "FAIL: $bench_key"
        echo "  baseline: $(printf '%.0f' $baseline_ns) ns"
        echo "  current:  $(printf '%.0f' $current_ns) ns"
        echo "  regression: ${regression}% (threshold: ${THRESHOLD}%)"
        FAILED=1
    else
        echo "PASS: $bench_key (${regression}%)"
    fi
done < <(jq -r 'keys[]' "$BASELINE")

if [[ $FAILED -eq 0 ]]; then
    echo "All benchmarks within threshold."
fi

exit $FAILED
```

创建文件后执行 `chmod +x scripts/check-regression.sh`。

注意：
- THRESHOLD=5 对应 D-11（5% 阈值）
- 失败输出格式严格按照 D-10：FAIL 行 + baseline 行 + current 行 + regression 行
- `_rps` 后缀变体不在 baseline.json 中，无需门禁（D-09 仅对比 mean.point_estimate）
- jq 在 ubuntu-latest 内置，无需安装
  </action>
  <verify>
    <automated>bash -n scripts/check-regression.sh && echo "syntax OK"</automated>
  </verify>
  <acceptance_criteria>
    - 文件 scripts/check-regression.sh 存在
    - 文件有可执行权限（chmod +x）
    - `bash -n scripts/check-regression.sh` 语法检查通过
    - 文件包含 `THRESHOLD=5`
    - 文件包含 `mean.point_estimate`
    - 文件包含 `FAIL: $bench_key`（失败输出格式）
    - 文件包含 `exit $FAILED`
    - 文件包含 `[skip ci]` 不需要（此文件只是脚本，[skip ci] 在 update-baseline.yml 中）
    - 失败输出包含 `baseline:` 和 `current:` 和 `regression:` 三行（对应 D-10）
  </acceptance_criteria>
  <done>scripts/check-regression.sh 已创建，语法通过，包含 5% 阈值和 D-10 规定的失败输出格式</done>
</task>

<task type="auto" tdd="false">
  <name>Task 2: 修改 benchmark.yml 添加门禁 step，创建 update-baseline.yml</name>
  <files>.github/workflows/benchmark.yml, .github/workflows/update-baseline.yml</files>
  <read_first>
    - .github/workflows/benchmark.yml（读取现有 step 结构，在 Run parser benchmarks 之后插入）
    - scripts/check-regression.sh（确认 Task 1 已完成，脚本路径正确）
  </read_first>
  <action>
**修改 .github/workflows/benchmark.yml：**

在 `Run parser benchmarks` step（`run: cargo bench --bench parser_benchmark`）之后，`Upload benchmark results` step 之前，插入以下 step：

```yaml
      - name: Check regression vs baseline
        run: bash scripts/check-regression.sh
```

不修改文件中的任何其他内容。

---

**新建 .github/workflows/update-baseline.yml（对应 D-07）：**

内容如下：

```yaml
name: Update Benchmark Baseline

on:
  workflow_dispatch:

env:
  CARGO_TERM_COLOR: always
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
  update-baseline:
    name: Update Baseline
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          toolchain: stable

      - name: Cache cargo registry
        uses: actions/cache@v5
        with:
          path: |
            ~/.cargo/bin/
            ~/.cargo/registry/index/
            ~/.cargo/registry/cache/
            ~/.cargo/git/db/
            target/
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-cargo-

      - name: Run benchmarks
        run: cargo bench --bench parser_benchmark

      - name: Update baseline.json from estimates
        run: |
          python3 - <<'PYEOF'
          import json, glob, os

          baselines = {}
          # 从所有 estimates.json 提取 mean.point_estimate
          # 路径格式：target/criterion/{group}/{bench}/new/estimates.json
          for path in glob.glob('target/criterion/**/**/new/estimates.json', recursive=True):
              parts = path.replace('\\', '/').split('/')
              # parts: ['target', 'criterion', '{group}', '{bench}', 'new', 'estimates.json']
              if len(parts) < 6:
                  continue
              group = parts[-4]
              bench = parts[-3]
              # 跳过 _rps 后缀变体（不纳入门禁基线，D-09）
              if bench.endswith('_rps'):
                  continue
              key = f'{group}/{bench}'
              with open(path) as f:
                  data = json.load(f)
              baselines[key] = data['mean']['point_estimate']

          with open('benchmarks/baseline.json', 'w') as f:
              json.dump(baselines, f, indent=4, sort_keys=True)

          print('Updated baseline.json:')
          for k, v in sorted(baselines.items()):
              print(f'  {k}: {v:.0f} ns')
          PYEOF

      - name: Commit updated baseline
        run: |
          git config user.name 'github-actions[bot]'
          git config user.email 'github-actions[bot]@users.noreply.github.com'
          git add benchmarks/baseline.json
          git diff --staged --quiet || git commit -m "chore(bench): update CI baseline [skip ci]"
          git push
```

注意：
- D-06：`on: workflow_dispatch` 手动触发
- D-07：独立文件 update-baseline.yml，不与 benchmark.yml 合并
- D-05：在 ubuntu-latest 运行，标定 CI 环境 baseline
- Pitfall 5：commit message 含 `[skip ci]` 避免触发 benchmark.yml
- `_rps` 变体跳过（不纳入 baseline.json，见 RESEARCH.md Code Examples 说明）
  </action>
  <verify>
    <automated>grep -n "Check regression" .github/workflows/benchmark.yml && grep -n "workflow_dispatch" .github/workflows/update-baseline.yml && echo "both files updated"</automated>
  </verify>
  <acceptance_criteria>
    - .github/workflows/benchmark.yml 包含 `Check regression vs baseline` step
    - benchmark.yml 中 Check regression step 位于 `Run parser benchmarks` step 之后（grep 行号验证：Check regression 行号 > Run parser benchmarks 行号）
    - benchmark.yml 中 Check regression step 调用 `bash scripts/check-regression.sh`
    - .github/workflows/update-baseline.yml 文件存在
    - update-baseline.yml 包含 `workflow_dispatch`
    - update-baseline.yml 包含 `[skip ci]`（在 commit message 中）
    - update-baseline.yml 包含 `benchmarks/baseline.json`（作为写入目标）
    - update-baseline.yml 包含 `endswith('_rps')`（跳过 rps 变体）
    - `python3 -c "import json; json.load(open('benchmarks/baseline.json'))"` 通过（baseline.json 格式有效）
  </acceptance_criteria>
  <done>benchmark.yml 已添加门禁 step，update-baseline.yml 已创建，两个文件均通过验证条件</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| CI runner → shell 脚本 | 脚本读取本地文件，无外部输入 |
| update-baseline.yml → git push | 使用 GITHUB_TOKEN，权限限于仓库写入 |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-01-02 | Tampering | update-baseline.yml git push | accept | 仅 workflow_dispatch 手动触发，非自动；commit message 含 [skip ci] 避免循环触发；仓库无分支保护时 GITHUB_TOKEN 已足够（A2 假设） |
| T-01-03 | Denial of Service | check-regression.sh 无限循环 | accept | 脚本使用 `while read` + jq 遍历固定 key 集合，输入完全由本地文件控制，无外部源 |
</threat_model>

<verification>
```bash
# 验证门禁脚本语法
bash -n scripts/check-regression.sh && echo "syntax OK"

# 验证 benchmark.yml 包含门禁 step
grep -n "Check regression" .github/workflows/benchmark.yml

# 验证 step 顺序（门禁在 bench 之后）
grep -n "Run parser benchmarks\|Check regression" .github/workflows/benchmark.yml

# 验证 update-baseline.yml 存在且含关键字段
grep -n "workflow_dispatch" .github/workflows/update-baseline.yml
grep -n "skip ci" .github/workflows/update-baseline.yml
grep -n "_rps" .github/workflows/update-baseline.yml

# 验证 baseline.json 格式有效
python3 -c "import json; d=json.load(open('benchmarks/baseline.json')); print('keys:', list(d.keys()))"
```
</verification>

<success_criteria>
- scripts/check-regression.sh 存在，语法通过，THRESHOLD=5，输出格式含 baseline/current/regression 三行（D-10/D-11）
- .github/workflows/benchmark.yml 在 Run parser benchmarks 之后有 Check regression step 调用 scripts/check-regression.sh
- .github/workflows/update-baseline.yml 存在，workflow_dispatch 触发，commit message 含 [skip ci]，跳过 _rps 变体
- benchmarks/baseline.json 是有效 JSON，key 格式为 parser_group/{bench_name}
</success_criteria>

<output>
完成后创建 `.planning/phases/01-measurement/01-02-SUMMARY.md`
</output>
