Metadata-Version: 2.4
Name: ascend-forge
Version: 0.1.0
Summary: NPU operator build & test framework
Author-email: xiaoleixin <xiaoleixin1@huawei.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://gitee.com/xiaoleixin/bisect
Project-URL: Repository, https://gitee.com/xiaoleixin/bisect
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Build Tools
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Dynamic: license-file

# forge

NPU 算子编译测试框架。通过 JSON 配置文件驱动，统一管理不同代码仓的算子编译和测试流程，支持精度测试、性能测试和 git bisect 问题定位。

新增算子仓只需添加一个 JSON 配置文件，无需修改任何代码。

**零第三方运行依赖**，仅使用 Python 标准库。

## 安装

### 系统依赖

- Python >= 3.9
- Git（bisect 功能需要）

### Python 依赖

运行时无需任何第三方库，仅使用 Python 标准库（argparse、json、csv、subprocess 等）。

开发依赖：[pytest](https://docs.pytest.org/) >= 7.0（仅运行测试需要）

### 安装步骤

**方式一：venv 虚拟环境**

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
```

**方式二：conda 环境**

```bash
# 在已有 conda 环境中安装
conda activate your_env
pip install -e ".[dev]"

# 或者新建专用环境
conda create -n forge python=3.9 -y
conda activate forge
pip install -e ".[dev]"
```

**仅安装（不含 pytest）：**

```bash
pip install -e .
```

## 架构

```
用户 CLI 命令
    │
    ▼
cli.py ─── 解析参数，分派命令（argparse）
    │
    ▼
config.py ─── 加载 JSON 配置，校验字段，替换占位符
    │
    ├──▶ task.py ─── 通用任务执行引擎（TaskRunner 基类）
    │        ├─ 单步/多步执行、skip 检查、capture 变量传递
    │        ├─ 日志记录、超时控制、ignore_failure
    │        └─ 子类通过 _post_step/_make_result 扩展
    │
    ├──▶ builder.py ─── 编译任务（BuildRunner → TaskRunner）
    │        ├─ _post_step: 产物检查（存在性、非空、时效性）
    │        └─ 支持 checkout 到指定 commit 编译（自动 stash/恢复）
    │
    ├──▶ tester.py ─── 测试任务（TestRunner → TaskRunner）
    │        ├─ _post_step: 结果提取（stdout 正则 / CSV 文件）
    │        └─ source_env 环境注入、repeat 重复聚合
    │
    ├──▶ result.py ─── 结果持久化（~/.forge/results/）与对比
    │
    └──▶ bisector.py ─── 二分查找问题 commit
             └─ 循环：checkout → build → test → 判断 good/bad
```

### 项目结构

```
forge/
├── forge/
│   ├── cli.py          # argparse CLI 入口
│   ├── config.py       # 配置加载、校验、占位符替换
│   ├── executor.py     # 通用子进程执行器（超时、输出捕获、实时流式输出）
│   ├── task.py         # 通用任务执行引擎（TaskRunner 基类 + 共享工具函数）
│   ├── builder.py      # 编译任务（BuildRunner 子类 + git checkout/restore）
│   ├── tester.py       # 测试任务（TestRunner 子类 + 结果提取 + repeat 聚合）
│   ├── result.py       # 结果持久化与对比
│   ├── bisector.py     # Git bisect 编排
│   ├── log.py          # 日志系统（文件持久化 + 屏幕输出）
│   └── formatter.py    # ASCII 表格格式化（bisect 结果输出）
├── configs/            # 算子仓配置文件（JSON）
├── tests/              # 测试用例（213 个，含端到端测试）
└── pyproject.toml
```

### 核心模块职责

| 模块 | 职责 |
|------|------|
| `config.py` | 加载 JSON、校验必填字段、`{placeholder}` 替换、路径解析 |
| `executor.py` | subprocess 封装：超时控制、环境变量注入、输出捕获、实时流式输出 |
| `task.py` | 通用任务执行引擎（`TaskRunner` 基类），封装单步/多步执行、skip、capture、日志等公共流程 |
| `builder.py` | `BuildRunner` 子类：产物检查（`_post_step`）+ git checkout/restore |
| `tester.py` | `TestRunner` 子类：结果提取（`_post_step`）+ source_env + repeat 聚合 |
| `result.py` | 测试结果 JSON 持久化到 `~/.forge/results/`，支持按 commit 加载和结果对比 |
| `bisector.py` | 在 good/bad commit 之间二分，协调 build → test → 判断，支持 skip 和分支恢复 |
| `log.py` | 日志文件持久化到 `~/.forge/logs/`，控制屏幕输出级别 |

## 配置文件

每个算子仓对应一个 JSON 文件，放在 `configs/` 目录下（或通过 `--config-dir` / `OPTOOL_CONFIG_DIR` 指定其他目录）。

### 完整示例

```json
{
  "name": "my_repo",
  "description": "Ascend 910 算子库",
  "repo_path": "/home/user/repos/my_repo",

  "env": {
    "http_proxy": "http://proxy.example.com:8080"
  },

  "build": {
    "command": "bash build.sh",
    "args": ["--target=ascend910", "--build-type=Release"],
    "working_dir": ".",
    "env": {
      "ASCEND_HOME": "/usr/local/Ascend"
    },
    "timeout": 600,
    "success_pattern": "Build succeeded",
    "artifacts": ["output/libkernel.so", "output/op_*.bin"]
  },

  "tests": {
    "accuracy": {
      "command": "python run_accuracy.py",
      "args": ["--op", "{op_name}"],
      "working_dir": "tests/",
      "timeout": 300,
      "success_pattern": "PASS",
      "result": {
        "type": "stdout",
        "pattern": "accuracy: ([0-9.]+)"
      }
    },
    "performance": {
      "command": "bash run_perf.sh",
      "args": ["--op", "{op_name}"],
      "working_dir": "tests/",
      "timeout": 0,
      "result": {
        "type": "file",
        "path": "output/perf_result.csv",
        "format": "csv",
        "value_column": 3,
        "op_column": 0,
        "header": true,
        "delimiter": ","
      }
    }
  },

  "bisect": {
    "build_before_test": true,
    "regression": 15.0,
    "threshold": null
  }
}
```

### 字段说明

| 字段 | 必填 | 说明 |
|------|------|------|
| `name` | 是 | 仓库唯一标识 |
| `description` | 否 | 描述信息 |
| `repo_path` | 是 | 仓库路径（绝对路径，或相对于配置文件所在目录） |
| `vars` | 否 | 自定义变量字典，可在所有占位符中使用。CLI `--arg` 可覆盖 |
| `env` | 否 | 全局环境变量字典，所有任务继承。task/step 级别的 `env` 可覆盖 |
| `test_path` | 否 | 测试工程路径，test 中 `working_dir` 相对于此路径。绝对路径或相对于 repo_path，默认等于 repo_path |
| `source_env` | 否 | 测试前 source 的环境变量脚本（绝对路径，支持 `{placeholder}`）。优先级：`tests.<type>` > `tests` > 顶层 |
| `build.command` | 是 | 编译命令 |
| `build.args` | 否 | 命令参数列表 |
| `build.working_dir` | 否 | 工作目录，相对于 repo_path，默认 `"."` |
| `build.env` | 否 | 额外环境变量 |
| `build.timeout` | 否 | 超时秒数，默认 600。设为 `0` 或 `null` 不限时 |
| `build.success_pattern` | 否 | 正则匹配 stdout 判断成功，支持字符串或数组（AND）。不配置则仅依赖退出码 |
| `build.artifacts` | 否 | 编译产物路径列表，支持 glob 通配符（如 `output/op_*.bin`）。检查文件存在、非空、且修改时间在本次编译之后 |
| `tests.<type>.command` | 是 | 测试命令 |
| `tests.<type>.source_env` | 否 | 该测试类型专用的 source_env，覆盖上层配置 |
| `tests.source_env` | 否 | 所有测试类型共用的 source_env |
| `tests.<type>.result.type` | 否 | `"stdout"` 或 `"file"` |
| `tests.<type>.result.pattern` | stdout 时必填 | 正则表达式，第一个捕获组为数值 |
| `tests.<type>.result.path` | file 时必填 | CSV 文件路径，相对于 working_dir |
| `tests.<type>.result.value_column` | file 时必填 | 数值所在列（从 0 开始） |
| `tests.<type>.result.op_column` | 否 | 算子名所在列 |
| `tests.<type>.result.header` | 否 | 是否有表头行，默认 true |
| `tests.<type>.result.delimiter` | 否 | CSV 分隔符，默认 `","` |
| `tests.<type>.result.aggregate` | 否 | 多行聚合方式：`avg`、`last`、`min`、`max`，不配置则取最后一行 |
| `tests.<type>.result.op_filter` | 否 | CSV 算子名匹配模板，支持 `{placeholder}` 和通配符。不配置则直接用 `--op` 值匹配 |
| `tests.<type>.repeat` | 否 | 性能测试重复次数。CLI `--repeat` 优先。精度测试忽略此项 |
| `tests.<type>.repeat_agg` | 否 | 重复测试聚合方式：`avg`（默认）或 `max`。CLI `--repeat-agg` 优先 |
| `bisect.build_before_test` | 否 | bisect 每步是否先编译，默认 true |
| `bisect.regression` | 否 | 性能退化阈值百分比，默认 10。CLI `--regression` 优先 |
| `bisect.threshold` | 否 | 绝对性能阈值，不配置则使用百分比模式。CLI `--threshold` 优先 |

### 全局环境变量（env）

顶层 `env` 字段定义全局环境变量，所有任务自动继承：

```json
{
  "env": {
    "http_proxy": "http://proxy.example.com:8080",
    "https_proxy": "http://proxy.example.com:8080"
  },
  "tasks": {
    "build": {"command": "bash build.sh"},
    "deploy": {
      "command": "bash deploy.sh",
      "env": {"DEBUG": "1"}
    }
  }
}
```

**优先级（高到低）：** step `env` > task `env` > 顶层 `env` > `source_env`

### 环境变量注入（source_env）

安装算子后通常需要 source 一个脚本设置环境变量，供测试使用。可在顶层或 `tests.<type>` 下配置，test 级别优先：

```json
{
  "name": "my_repo",
  "repo_path": "/path/to/repo",
  "tests": {
    "accuracy": {
      "source_env": "{install_path}/{commit}/set_env.bash",
      "steps": [...]
    }
  }
}
```

**流程：** 执行测试前，框架先 `source <script>`，捕获新增/修改的环境变量，注入到测试命令的环境中。

- **必须是绝对路径**（替换占位符后），相对路径会报错
- 支持 `{placeholder}` 替换（如 `{install_path}`、`{commit}`）
- `tests.<type>.source_env` 优先于顶层 `source_env`
- 环境变量对所有测试步骤生效
- 步骤级别的 `env` 配置优先于 `source_env`

### 占位符

配置中的 `command`、`args`、`result.path`、`artifacts` 支持占位符，运行时自动替换。

**内置占位符：**

| 占位符 | 来源 | 说明 |
|--------|------|------|
| `{repo_path}` | 配置文件 | 仓库绝对路径 |
| `{test_path}` | 配置文件 | 测试工程路径（未配置则等于 repo_path） |
| `{commit}` | `--commit` 或自动检测 | 当前 git commit hash（未指定 `--commit` 时自动获取 HEAD） |
| `{timestamp}` | 自动生成 | 当前时间戳，格式 `20260327_103000`，用于区分同名文件 |

**自定义占位符：**

通过配置文件 `vars` 字段预定义常用变量，避免每次 CLI 传入：

```json
{
  "name": "my_repo",
  "repo_path": "/path/to/repo",
  "vars": {
    "install_path": "/usr/local/Ascend",
    "target": "ascend910"
  },
  "build": {
    "command": "bash build.sh",
    "args": ["--target={target}"]
  }
}
```

也可通过 `--arg KEY=VALUE` 在 CLI 传入或覆盖配置中的变量：

```bash
# 传入自定义变量
forge build my_repo --arg op_name=matmul --arg target=ascend910

# 快捷方式
# --op  → --arg op_name=VALUE
# --cs  → --arg case=VALUE
# --ip  → --arg install_path=VALUE
forge build my_repo --op matmul
forge test my_repo -t acc --cs case1
forge build my_repo --ip /usr/local/Ascend

# CLI 覆盖配置中的 vars（target 从 ascend910 变为 ascend310）
forge build my_repo --arg target=ascend310
```

**优先级（高到低）：** CLI `--arg` > `capture`（运行时捕获） > 配置 `vars` > 内置变量

**变量预检查：** 执行前自动扫描配置中所有占位符，缺失则报错。通过检查后打印变量表：

```
Variables:
  op_name = matmul
  commit = abc1234567890
```

配置文件中使用：
```json
"build": {
  "command": "bash build.sh",
  "args": ["--op", "{op_name}", "--target", "{target}"]
}
```

`build`、`test`、`bisect` 三个命令均支持 `--arg` 和 `--op`。若配置中使用了占位符但未通过 CLI 传入，会报错提示。

### 多步骤编译流程

编译可以由多个步骤组成（如编译 → 安装），使用 `steps` 列表：

```json
"build": {
  "steps": [
    {
      "name": "compile",
      "command": "bash build.sh",
      "args": ["--ops={op_name}"],
      "working_dir": ".",
      "timeout": 600,
      "success_pattern": "Build succeeded",
      "artifacts": ["output/libkernel.so"]
    },
    {
      "name": "install",
      "command": "bash install.sh",
      "working_dir": ".",
      "timeout": 120,
      "success_pattern": "Install succeeded"
    }
  ]
}
```

**规则：**
- 步骤按顺序执行，任一步骤失败则整体失败，后续步骤不执行
- 每个步骤可独立设置 `command`、`args`、`working_dir`、`timeout`、`success_pattern`、`artifacts`、`capture`
- `skip_if_exists`：指定产物路径（字符串或数组），支持 glob 和 `{placeholder}`，所有文件存在且非空则跳过
- `skip_if_artifacts_exist: true`：复用该步骤的 `artifacts` 列表作为跳过检查条件
- `artifacts` 检查在对应步骤执行后进行
- **向后兼容**：不使用 `steps` 时，原有单 `command` 写法不受影响

### 步骤跳过示例

```json
{
  "name": "compile",
  "command": "bash build.sh",
  "artifacts": ["output/libkernel.so", "output/op_*.bin"],
  "skip_if_artifacts_exist": true
}
```

等价于：

```json
{
  "name": "compile",
  "command": "bash build.sh",
  "skip_if_exists": ["output/libkernel.so", "output/op_*.bin"]
}
```

产物已存在且非空时输出：`Skipped (artifacts exist)`

### 步骤间变量传递（capture）

步骤可以从 stdout 中提取值，存为变量供后续步骤使用：

```json
"steps": [
  {
    "name": "get version",
    "command": "cat version.txt",
    "capture": {
      "version": "v([0-9.]+)"
    }
  },
  {
    "name": "install",
    "command": "bash install.sh",
    "args": ["--version", "{version}", "--path", "{install_path}"]
  }
]
```

**规则：**
- `capture` 是一个 dict，key 为变量名，value 为正则表达式（第一个捕获组）
- 从步骤的 stdout 中匹配，成功后注入到 variables，后续步骤可用 `{变量名}` 引用
- 匹配失败会打印 warning，不中断执行
- 变量预检查时会识别 `capture` 定义的变量，不会误报缺失

输出示例：
```
  Step 1/2: get version
    Captured: version = 2.5.1
  Step 2/2: install
    Command: bash install.sh --version 2.5.1 --path /usr/local
```

### 成功判断逻辑

1. 检查进程退出码，非 0 即失败
2. 若配置了 `success_pattern`，退出码为 0 时额外匹配 stdout，不匹配也判定失败
   - 字符串：单个正则匹配
   - 数组：所有正则都需匹配（AND 逻辑），如 `["Build succeeded", "Install succeeded"]`
3. 若配置了 `artifacts`，检查每个产物文件存在、非空、且修改时间在本次编译之后
4. 以上均为可选，不配置则仅依赖退出码

### 结果提取

精度测试和性能测试统一使用 `result` 配置块，支持两种方式：

**从标准输出提取：**
```json
{
  "result": {
    "type": "stdout",
    "pattern": "accuracy: ([0-9.]+)"
  }
}
```

**从 CSV 文件提取：**
```json
{
  "result": {
    "type": "file",
    "path": "output/result.csv",
    "format": "csv",
    "value_column": 3,
    "op_column": 0,
    "header": true,
    "delimiter": ",",
    "aggregate": "avg"
  }
}
```

**聚合方式（`aggregate` 字段）：** 同一算子有多行数据时的聚合策略：

| 值 | 说明 |
|----|------|
| `avg` | 平均值 |
| `last` | 最后一行 |
| `min` | 最小值 |
| `max` | 最大值 |
| 不配置 | 取最后一行（向后兼容） |

**算子名通配符匹配：** `--op` 支持 `fnmatch` 通配符（`*`、`?`、`[seq]`）：

```bash
# 匹配所有 matmul 变体
forge test my_repo --type performance --op "matmul*"

# 匹配 add_v1, add_v2（不匹配 add_v10）
forge test my_repo --type performance --op "add_v?"
```

### 多步骤测试流程

测试可以由多个步骤组成，按顺序执行。使用 `steps` 列表替代单个 `command`：

```json
"tests": {
  "accuracy": {
    "steps": [
      {
        "name": "generate input",
        "command": "python gen_input.py",
        "args": ["--op", "{op_name}"],
        "working_dir": "tests/"
      },
      {
        "name": "run operator",
        "command": "bash run_op.sh",
        "args": ["{op_name}"],
        "working_dir": "."
      },
      {
        "name": "compare result",
        "command": "python compare.py",
        "args": ["--op", "{op_name}"],
        "working_dir": "tests/",
        "success_pattern": "PASS",
        "result": {
          "type": "stdout",
          "pattern": "accuracy: ([0-9.]+)"
        }
      }
    ],
    "timeout": 300
  }
}
```

**规则：**
- 步骤按顺序执行，任一步骤失败则整体失败，后续步骤不执行
- 每个步骤可独立设置 `command`、`args`、`working_dir`、`success_pattern`、`timeout`、`capture`
- `skip_if_exists`：指定产物路径（字符串或数组），支持 glob 和 `{placeholder}`，所有文件存在且非空则跳过
- `skip_if_artifacts_exist: true`：复用该步骤的 `artifacts` 列表作为跳过检查条件
- `result` 配置放在需要提取结果的步骤上（通常是最后一步）
- `timeout` 可在顶层设（所有步骤共享），也可每步单独设
- **向后兼容**：不使用 `steps` 时，原有单 `command` 写法不受影响

## CLI 使用

全局选项必须放在子命令前面：

```bash
forge [--config-dir PATH] [-v/--verbose] <command> [options]
```

### 短选项速查

| 短选项 | 长选项 | 适用命令 |
|--------|--------|----------|
| `-v` | `--verbose` | 全局 |
| `-t` | `--type` | test, run, bisect |
| `-c` | `--commit` | build, test, run |
| `-s` | `--step` | build, run |
| `-b` | `--baseline` | test, run |
| `-g` | `--good` | bisect |
| `-b` | `--bad` | bisect |

`--type` 支持缩写：`acc` = `accuracy`，`perf` = `performance`。

### Daemon 和 Task 子命令

| 命令 | 说明 |
|------|------|
| `forge daemon start [--foreground]` | 启动 daemon（后台运行） |
| `forge daemon stop` | 停止 daemon |
| `forge daemon status` | 查看 daemon 状态和任务统计 |
| `forge daemon install` | 安装 systemd 服务（需 sudo） |
| `forge daemon uninstall` | 卸载 systemd 服务（需 sudo） |
| `forge task list [--all\|--running\|--completed]` | 列出任务 |
| `forge task show <id> [--tail N] [--follow]` | 查看任务输出 |
| `forge task cancel <id>` | 取消任务 |
| `forge task resume <id>` | 恢复被中断的任务 |

### 查看配置

```bash
# 列出所有已配置的算子仓
forge list

# 查看某个仓库的完整配置
forge show my_repo

# 校验配置文件是否合法
forge validate my_repo

# 使用自定义配置目录
forge --config-dir /path/to/configs list
```

### 编译

```bash
# 编译当前 HEAD
forge build my_repo

# 编译指定 commit（自动 stash 未提交更改，编译后恢复）
forge build my_repo --commit abc123

# 编译指定算子
forge build my_repo --op matmul

# 传入多个自定义参数
forge build my_repo --arg op_name=matmul --arg target=ascend910

# 只执行指定步骤（多步骤配置时）
forge build my_repo --step install     # 只安装，跳过编译
forge build my_repo --step compile     # 只编译，跳过安装
```

### 测试

```bash
# 精度测试
forge test my_repo --type accuracy

# 性能测试
forge test my_repo --type performance

# 测试特定算子
forge test my_repo --type accuracy --op matmul

# 在指定 commit 上测试（自动编译）
forge test my_repo --type performance --commit abc123

# 与指定 commit 的历史结果对比
forge test my_repo --type performance --baseline abc123

# 与上次保存的结果对比
forge test my_repo --type performance --compare-last

# 性能测试跑 3 次取平均值
forge test my_repo -t perf --op matmul --repeat 3

# 性能测试跑 3 次取最大值
forge test my_repo -t perf --op matmul --repeat 3 --repeat-agg max
```

### 编译+测试（run）

一个命令完成 build → test，build 失败则不执行 test。

```bash
# 编译后跑精度测试
forge run my_repo --type accuracy --op matmul

# 编译后跑性能测试，并与上次结果对比
forge run my_repo --type performance --op matmul --compare-last

# 在指定 commit 上编译+测试
forge run my_repo --type accuracy --commit abc123

# 只执行指定编译步骤后测试
forge run my_repo --type accuracy --step install
```

### 自定义任务

配置文件中可通过 `tasks` 定义任意名称的任务：

```json
{
  "tasks": {
    "deploy": {
      "command": "bash deploy.sh",
      "artifacts": ["lib/libkernel.so"]
    },
    "lint": {
      "steps": [
        {"name": "check", "command": "flake8 .", "success_pattern": "0 errors"},
        {"name": "format", "command": "black --check ."}
      ]
    }
  }
}
```

```bash
# 运行自定义任务
forge task my_repo deploy
forge task my_repo lint

# 支持所有通用选项
forge task my_repo deploy --arg key=val --op matmul
```

自定义任务支持所有步骤特性：`command`、`args`、`working_dir`、`timeout`、`env`、`success_pattern`、`artifacts`、`result`、`capture`、`skip_if_exists`、`steps`。

**向后兼容：** 旧的 `build` 和 `tests` 配置自动映射为任务（`build` → `build`，`tests.accuracy` → `test_accuracy`）。`forge build`、`forge test`、`forge run`、`forge bisect` 命令不受影响。

### Workflow（任务编排）

通过 `workflows` 定义多个任务的串行执行序列：

```json
{
  "tasks": {
    "build": {"command": "bash build.sh"},
    "deploy": {"command": "bash deploy.sh"},
    "verify": {"command": "bash verify.sh"}
  },
  "workflows": {
    "full_deploy": ["build", "deploy", "verify"],
    "ci": ["build", "test_accuracy", "test_performance"]
  }
}
```

```bash
# 运行 workflow
forge workflow my_repo full_deploy
forge workflow my_repo ci --arg op_name=matmul
```

按顺序执行，任一任务失败则停止。

### Bisect 问题定位

在 good 和 bad 两个 commit 之间自动二分查找引入问题的 commit。

```bash
# 精度 bisect：找到精度测试开始失败的 commit
forge bisect my_repo --good abc123 --bad def456 --type accuracy

# 性能 bisect：找到性能退化的 commit
forge bisect my_repo --good abc123 --bad def456 --type performance --op matmul

# 自定义退化阈值（默认 10%）
forge bisect my_repo --good abc123 --bad def456 --type performance --regression 20

# 使用绝对阈值（耗时超过 15 即判定为 bad）
forge bisect my_repo --good abc123 --bad def456 --type performance --threshold 15
```

**Bisect 流程：**

1. 获取 good..bad 之间的 commit 列表
2. 性能测试时，先在 good commit 上运行测试获取基线值
3. 二分查找：checkout → build → test → 判断 good/bad
4. 精度 bisect：退出码 + success_pattern 判断
5. 性能 bisect：与基线对比，超过退化阈值判定为 bad
6. 编译失败或测试超时的 commit 自动跳过
7. 跳过超过总步数 50% 时中止
8. 完成后自动恢复原始分支

### 环境变量

| 变量 | 说明 |
|------|------|
| `OPTOOL_CONFIG_DIR` | 配置文件目录路径，等价于 `--config-dir` |

## 后台任务（Daemon 模式）

Daemon 是一个常驻的后台进程，管理和执行提交的后台任务，支持任务持久化和自动恢复。

### 启动和停止

```bash
# 启动 daemon（后台运行，自动 fork）
forge daemon start

# 前台运行（调试用）
forge daemon start --foreground

# 停止 daemon
forge daemon stop

# 查看状态
forge daemon status
```

### 提交后台任务

任意命令加上 `--daemon` 即可后台运行：

```bash
# 后台编译
forge build my_repo --daemon

# 后台测试
forge test my_repo --type accuracy --daemon

# 后台 bisect
forge bisect my_repo --good abc --bad def --type accuracy --daemon
```

提交后立即返回 task_id：`Task submitted: task_abc123 (running in background)`

### 管理任务

```bash
# 查看运行中的任务（默认）
forge task list

# 查看所有任务
forge task list --all

# 查看已完成的任务
forge task list --completed

# 取消任务
forge task cancel task_abc123

# 恢复被中断的任务
forge task resume task_abc123
```

### 查看任务输出

```bash
# 查看累积输出（默认最近 100 行）
forge task show task_abc123

# 指定行数
forge task show task_abc123 --tail 50

# 实时跟踪（Ctrl+C 停止）
forge task show task_abc123 --follow
```

输出文件保存在 `~/.forge/logs/task_{task_id}.log`，任务结束后保留。

### 自动恢复

Daemon 重启后自动恢复被中断的任务（最多重试 3 次）。

### systemd 服务

```bash
# 安装为系统服务（需 root）
sudo forge daemon install

# 卸载服务
sudo forge daemon uninstall
```

安装后 daemon 随系统自动启动。

## 结果存储

测试结果自动保存到 `~/.forge/results/<repo_name>/`，JSON 格式：

```json
{
  "repo": "my_repo",
  "commit": "abc1234",
  "timestamp": "2026-03-23T10:00:00",
  "type": "performance",
  "results": {
    "matmul": 12.3,
    "add": 0.8
  }
}
```

可通过 `--compare-last` 或 `--baseline` 与历史结果对比。

## 日志系统

每次编译和测试的完整输出自动保存到日志文件，方便问题定位。

### 日志文件位置

```
~/.forge/logs/<repo_name>/
├── build_2026-03-24T10-30-00.log
├── test_accuracy_2026-03-24T10-35-00.log
└── test_performance_2026-03-24T10-40-00.log
```

### 日志内容

每个日志文件包含：
- 执行时间、完整命令、工作目录
- 退出码、是否超时、是否成功
- 编译/测试脚本的 **完整 stdout 和 stderr**

### 屏幕输出

```bash
# 默认模式：打印执行的命令和工作目录，失败时打印 stderr
forge build my_repo

# verbose 模式：实时逐行打印脚本的 stdout/stderr
forge --verbose build my_repo
forge --verbose test my_repo --type accuracy
```

### 排查编译/测试失败

1. 加 `--verbose` 查看实时输出：`forge --verbose build my_repo`
2. 查看日志文件：`cat ~/.forge/logs/my_repo/build_*.log`
3. 检查日志中的 stderr 和退出码
4. 对比手动执行时的环境变量（配置文件中 `build.env` 是否缺失关键变量）

## 扩展

- **新增算子仓**：在 `configs/` 下添加一个 JSON 文件即可，无需改代码
- **新增测试类型**：在配置文件的 `tests` 下新增 key（如 `stress`），框架通用处理
- **新增结果文件格式**：在 `tester.py` 的 `extract_result_from_file` 中扩展解析器
- **新增任务类型**：继承 `TaskRunner`，覆盖 `_post_step()` 和 `_make_result()` 即可。执行引擎（单步/多步、skip、capture、日志等）自动继承

## 开发

```bash
# 运行测试
source .venv/bin/activate
python -m pytest tests/ -v
```
