# 插件开发指南

ABI 插件在共享生命周期 API 背后暴露生物学分析类型。

## 推荐的声明式接口

将 `abi-plugin.yaml` 放在插件模块旁边，并继承
`DeclarativeABIPlugin`。基类会从 manifest 读取插件身份、工具注册表和
标准表路径，因此这些信息只需声明一次：

```python
from abi.plugin import DeclarativeABIPlugin


class MyPlugin(DeclarativeABIPlugin):
    def load_config(self, config_path=None, **kwargs): ...
    def build_plan(self, config, *, check_files=True): ...
    def parse_outputs(self, tool_id, output_dir, sample_id): ...
    def write_report(self, plan, result_dir): ...
```

如果 monorepo 将声明文件与 Python 模块分开存放，只需设置一个类属性，
例如 `plugin_root = Path("plugins/my_analysis")`。

基类会在导入时校验 manifest 及其声明的所有路径。插件发现还会强制
entry-point 名称、manifest 中的 `plugin_id` 与 `entry_point` 一致。运行时
注册表、工具合约和环境校验保持不变；发布前运行
`abi contract-lint --strict`。

## 底层 Python 接口

实现 `abi.interfaces.ABIPlugin` 协议：

- `plugin_id`
- `display_name`
- `description`
- `report_title`
- `load_config()`
- `build_plan()`
- `registry()`
- `table_schemas()`
- `parse_outputs()`
- `write_report()`

通过以下方式注册插件：

```toml
[project.entry-points."abi.plugins"]
my_analysis = "my_package.plugins:MyPlugin"
```

entry-point 的键必须与 `abi-plugin.yaml` 中的 `plugin_id` 完全一致。

## 插件目录

推荐布局：

```text
plugins/my_analysis/
  abi-plugin.yaml
  config_default.yaml
  sample_sheet_template.tsv
  tool_registry.yaml
  standard_tables.yaml
  limitations.yaml      ← 强制且非空
  tool_contracts/
    tool_a.yaml
  skills/               ← 随包捆绑的 SKILL.md 文件
    tool_a/SKILL.md
  _engine/             ← 可选：复杂引擎代码（参见 metagenomic_plasmid）
```

每个插件必须提供非空的 `limitations.yaml`；否则 contract lint 以
`missing_limitations`、`invalid_limitations` 或 `empty_limitations` 失败。
生成的报告始终包含 limitations 章节（列表为空时使用明确的兜底文本）。

对于具有大量内部逻辑的复杂插件，使用带有私有 `_engine/` 子目录的自包含包。参见 `plugins/metagenomic_plasmid/` 获取规范示例。

## 技能与 Agent 集成

每个工具应在 `skills/<tool_name>/SKILL.md` 下有一个 `SKILL.md` 文件。
技能捆绑在包内 `src/abi/skills/` 中，并通过以下方式安装到 Claude Code：

```bash
abi install-skills      # → ~/.claude/skills/abi/
```

要添加新技能，在 `src/abi/skills/<tool_name>/SKILL.md` 下创建目录和 SKILL.md 文件。`abi_agent/SKILL.md` 技能教会 Claude Code 如何使用 `abi` CLI 本身；其他技能记录各个生物信息学工具。

工具 Skill 解释科学目的、参数、输入、输出和失败模式，但不会新增 MCP 函数，也不能绕过 ABI 生命周期。

## 让插件可被 Agent 调用

Agent 传输层暴露共享生命周期工具。新插件通过 entry-point 被发现后，Agent 使用 `analysis_type` 参数调用它；不要创建独立的 `abi_my_plugin_plan` 或 `abi_my_plugin_run` 工具。

### 提供可发现的生物学身份

manifest 身份是未经专门训练的 Agent 通过 `abi_list_types` 和 `abi_export_agent_context` 看到的信息：

```yaml
abi_version: "0.1"
plugin_id: my_analysis
display_name: My Biological Analysis
description: "分析 <输入> 并生成 <主要生物学结果>。"
report_title: My Biological Analysis Report
entry_point: my_package.plugin:MyPlugin
```

`description` 用于插件选择，应说明生物学目标、输入类型和主要结果，避免只写实现细节或宣传性内容。

entry-point 键、manifest `plugin_id` 和插件类身份必须一致。提供 `config_default.yaml` 和 `sample_sheet_template.tsv`，让用户与 Agent 从明确 Schema 开始，而不是编造元数据。

### 让工作流可以被查询

`abi_query` 读取声明式 DAG 和工具注册表。使用稳定的节点与工具 ID，声明平台，并为每个节点提供有意义的输入、输出、依赖关系和说明。

Agent 应能在不读取源码的情况下回答：

```bash
abi query --type my_analysis --what stages
abi query --type my_analysis --what tools
abi query --type my_analysis --what platforms
abi query --type my_analysis --step qc_tool --what inputs
abi query --type my_analysis --step qc_tool --what outputs
```

资源属于特定节点时，应明确声明关联关系，使步骤级资源查询和预检诊断能够解释缺少的内容。

### 保持规划安全且确定

- `load_config()` 可以读取配置，但不得安装资源或运行工具。
- `build_plan()` 必须根据显式输入生成确定的步骤和路径。
- `plan` 和 `dry_run` 可以写入审查产物，但不得执行外部分析工具。
- 真实工具只能在共享确认门之后通过 `run` 执行。
- `report` 必须读取已有发布结果，不能静默重新运行工作流。

Agent 权限 profile 由 ABI 核心层管理。插件不得在规划、解析、报告或诊断中创建隐藏执行路径。

### 发布 Agent 能解释的结果

插件的 `table_schemas()` 定义 `abi_export_agent_context` 返回的 `standard_tables`。表名、列、单位、标识符和缺失值约定必须稳定。

解析器应把工具输出标准化到这些表格。报告和图形应读取已发布表格，而不是重新解析原始日志或私有中间文件。

不适合标准表格的稳定最终产物通过 `published_outputs(plan)` 发布。工作流包含多个输出分支时，发布方法、溯源、局限性和带分支限定的 manifest。

可恢复失败应抛出 ABI 错误类型或提供结构化诊断。Agent 使用 `error_code` 和 `diagnostic_hints`；原始 traceback 不是操作契约。

### 检查 Agent 上下文

安装插件后，检查其机器可读契约：

```bash
abi list-types --output-json
abi export-agent-context --type my_analysis
abi doctor-agent --type my_analysis
abi export-tools --type my_analysis --format openai
```

确认插件说明足以支持选择，标准表格列表完整，且执行工具只有在明确请求时才被导出。

不要为插件手工编辑 OpenAI、Anthropic、Gemini 或 MCP Schema。ABI 从单一事实来源生成生命周期描述符，并在导出时注入插件范围。

### 添加端到端 Agent 契约测试

除插件单元测试外，还应通过 `ABIAgentInterface` 覆盖发现、上下文导出、规划、dry-run、执行确认和结果验证。

```python
import json

from abi.agent import ABIAgentInterface


def call(tool, arguments):
    return json.loads(ABIAgentInterface().dispatch(tool, arguments))


def test_agent_can_discover_and_plan_my_plugin(tmp_path, plugin_fixture):
    listed = call("abi_list_types", {})
    plugin_ids = {
        row["analysis_type"]
        for row in listed["result"]["analysis_types"]
    }
    assert "my_analysis" in plugin_ids

    context = call(
        "abi_export_agent_context",
        {"analysis_type": "my_analysis"},
    )
    assert context["status"] == "success"
    assert context["result"]["standard_tables"]
    assert "abi_run" in context["result"]["unsafe_tools"]

    common = {
        "analysis_type": "my_analysis",
        "config_path": str(plugin_fixture.config),
        "sample_sheet": str(plugin_fixture.sample_sheet),
    }
    plan = call("abi_plan", {**common, "outdir": str(tmp_path / "plan")})
    assert plan["status"] == "success"

    dry = call("abi_dry_run", {**common, "outdir": str(tmp_path / "dry")})
    assert dry["status"] == "success"

    run = call("abi_run", {**common, "confirm_execution": False})
    assert run["status"] == "confirmation_required"
```

在 `golden_traces/<plugin_id>.jsonl` 添加已知正确的生命周期，并在 `tests/integration/test_golden_traces.py` 回放。Trace 应包含错误恢复和需要确认的 run 尝试。

在插件测试套件中定义 `plugin_fixture`，让它生成有效的最小配置和样本表。Fixture 应使用小型合成数据，并且不依赖特定机器的资源路径。

### Agent 可调用插件检查清单

- 插件出现在 `abi list-types --output-json` 中。
- 上下文导出包含正确的标准表格、产物、权限和错误码。
- DAG 阶段、工具、平台、输入、输出和资源都可以查询。
- 默认配置和样本表模板没有含义模糊的字段。
- plan、check 和 dry-run 不执行外部工具即可完成。
- 未授权时 `abi_run` 返回 `confirmation_required`。
- 真实结果通过 `abi_validate_result`，报告使用标准表格。
- 模型描述符和 MCP profile 不需要插件特有传输代码。
- Golden trace 和回归测试覆盖完整 Agent 生命周期。

## 工具合约

合约是权威的、机器可读的工具声明：

- `tool_id`
- `category`
- `execution.executable`
- `execution.command_template`
- 声明的输入/输出模板字段
- 标准化的标准表名称

`tool_registry.yaml` 是精简的运行策略索引。每个合约必须有且仅有一个
注册表条目，但条目只需要工具 ID 和运行策略：

```yaml
tools:
  - id: fastp
    required: true
    default_enabled: true
    skill_path: skills/fastp/SKILL.md
```

不要在其中重复 `name`、`category`、`executable` 或 `command_template`。
旧注册表仍可包含这些字段，但如果重复声明冲突，ABI 会直接拒绝，
而不是静默选择其中一份。

环境名称不存储在单独的合约或注册表中。它们集中在 `environments.yaml` 中的
`tool_assignments:` 下（每个插件一个映射），由 `ToolRegistry` 在运行时注入正确的
`env_name`。参见：`environments.yaml`、`scripts/emit_env_yamls.py`。

在插件测试中使用 `assert_plugin_contract(plugin)`。

## 步骤输出合约

复杂插件可以在执行计划中嵌入逐步骤合约。对于 DAG 驱动的 metagenomic plasmid 插件，`pipeline_dag.yaml` 中的每个节点声明其 `outputs` 和可选的 `assertions`；规划器将这些字段复制到 `PlanStep.params["_contract"]` 中以供运行时执行。

契约覆盖率是强制的：每个调用外部工具的节点必须为输出声明 `contract` 检查；无产出的节点（纯聚合或标记步骤）必须显式设置 `contract: {exempt: true, reason: ...}`。该门槛由 `abi contract-lint`（`missing_output_contract`、`exempt_missing_reason`、`exempt_mixed_with_checks`）和插件校验强制执行；`scripts/audit_contract_coverage.py` 可输出各插件的覆盖率报告。

支持的输出检查包括：

- `min_size`：文件或目录的最小字节大小，如 `"1KB"`。
- `extensions`：允许的文件后缀，如 `[.fastq, .fastq.gz]`。
- `contains`：输出目录中必须包含的文件。
- `min_files`：目录下最少的常规文件数，适用于生成的索引。
- `min_contigs`：最少的 FASTA contig 数量。
- `required_keys`：JSON 输出必需的顶层键。
- `schema`：带有简单类型/范围约束的点分 JSON 字段。

检查项必须嵌套在输出的 `contract` 键下；将 `min_size` 等检查与 `type`
并列是无效声明，运行时不会执行：

```yaml
outputs:
  clean_read1:
    type: file
    format: fastq.gz
    path: "{outdir}/{sample_id}.clean.fastq.gz"
    contract:
      min_size: "1KB"
      extensions: [".fastq.gz"]
```

断言在输出验证之后根据 `output_files`、`output_json` 和 `return_code` 进行评估。示例：

```yaml
assertions:
  - "output_json.summary.after_filtering.total_reads > 0"
  - "output_files.clean_read1 exists"
```

当声明创建自身输出目录的工具的输出时，继续使用 `output_dir`。通用执行器有意仅创建父目录，因为某些工具在 `output_dir` 于执行前已存在时会失败。

如果规划器输出抽象输出路径而工具写入固定名称，请确保合约的 `format` 和文件名约定是明确的。执行器在检查合约之前按 `output_dir`、format、样本 ID 和 R1/R2 双端提示解析实际文件。

## 标准表

解析器必须仅写入插件声明的表。空表仍应以稳定的表头存在，以便 Agent 在不解析原始工具输出的情况下检查结果。

## 发布输出

插件可以实现 `published_outputs(plan)`，将插件特有的最终产物加入传输无关的
`RuntimeResult.outputs` 映射。只应返回稳定且已经存在的路径，并使用不会与 ABI
公共结果键冲突的标签。该钩子适合暴露带版本的产物清单等最终结果；中间文件仍
通过执行计划发现，不应逐个发布。

如果一个预设会生成多个最终报告，应发布带分支限定的标签；只有在恰好存在一套完整
报告时才添加通用别名。例如，EasyMetagenome 单分支运行发布 `report_manifest`，组合
预设则发布 `taxonomy_report_manifest` 和 `functional_report_manifest`。带版本的清单
应标明工作流，并关联其汇总的标准表和报告。

## 共享基础设施

插件应从公开 SDK 导入：

| 模块 | 用途 |
| --- | --- |
| `abi.schemas` | `SampleInput`、`SampleContext`、`PlanStep`、`ExecutionPlan` |
| `abi.tools` | `ToolRegistry`、`ToolSkill`、`GenericCommandSkill`、`RunResult` |
| `abi.provenance` | `RunLogger`、`PipelineProgressRecorder`、TSV 写入器 |
| `abi.errors` | `ABIError`、`ConfigError`、`SampleSheetError`、`ToolError` |
| `abi.diagnostics` | `DiagnosticHint`、`classify_exception`、`ERROR_CODES` |
| `abi.json_utils` | `load_json_file`、`load_json_payload` 及其 `ABIJSONError` |
| `abi.interfaces` | `ABIPlugin`、`ABIDryRunPlugin`、`ABIInitializablePlugin`、`ABIPublishedOutputsPlugin` 协议 |
| `abi.plugin` | `DeclarativeABIPlugin` — 由 manifest 提供身份、注册表和标准表 schema |
| `abi._shared` | `_read_tsv`、`_display_command`、`_plan_dict`、`_common_overrides` |
| `abi.dag_planner` | `UniversalDAG`、`build_plan_from_dag`、`PathTemplateContext` — DAG 驱动的 `build_plan()`（2026-06-18 新增） |
| `abi.tsv_mapping` | `TSVMapper`、`generate_rows` — 声明式 TSV 列映射（2026-06-18 新增） |
| `abi.sciplot` | `FigureSpec`、`render_figure`、`validate_spec`、`lint_figure` — 论文级科研图形编译器。15 种图表类型（含 PCoA、火山图、堆叠柱状图、系统发育热图），plotnine+seaborn 后端。（v1.4.0，2026-06-20 新增） |
| `abi.contracts` | `WorkflowSpec`、`WorkflowStepSpec`、`load_workflow_spec`、`run_contract_lint` — L1/L2/L3 工作流声明与验证 |
| `abi.report` | `write_plugin_report`、`render_figures_via_sciplot` — 报告生成与图表渲染 |

## DAG 驱动的计划构建

与其手写遍历样本、构造 `PlanStep` 对象的 `build_plan()`（约 200 行样板代码），插件必须在 `pipeline_dag.yaml` 文件中声明工作流，并使用通用 DAG 规划器：

```python
# 在插件的 build_plan() 中：
def build_plan(self, config, *, check_files=True):
    context = self.build_sample_context(config, check_files=check_files)
    from abi.dag_planner import build_plan_from_dag
    return build_plan_from_dag(
        self.root / "pipeline_dag.yaml", config, context
    )
```

### `pipeline_dag.yaml` 结构

```yaml
pipeline_id: my_analysis
platforms: [illumina]

# 类别 → 子目录映射
category_dirs:
  qc: 01_qc
  alignment: 02_alignment

nodes:
  qc_fastp:
    tool_id: fastp
    category: qc
    scope: per_sample        # per_sample（默认）或 cross_sample
    depends_on: []
    inputs:
      read1: {type: file, source: sample_sheet}
      read2: {type: file, source: sample_sheet}
    outputs:
      clean_read1:
        type: file
        path: "{outdir}/{category_dir}/{sample_id}/{sample_id}_R1.clean.fastq.gz"
      output_dir:
        type: directory
        path: "{outdir}/{category_dir}/{sample_id}"

  aggregation_step:
    tool_id: my_aggregator
    scope: cross_sample      # 运行一次，收集所有 per-sample 输出
    depends_on: [qc_fastp]
    inputs:
      per_sample_data: {aggregate: per_sample_outputs}
```

### 声明式 TSV 解析

对于输出简单 TSV/JSON/日志的工具，在 `parsers.yaml` 中声明列映射，代替手写 Python 解析器函数。支持三种源类型：

| 源类型 | 用途 | 示例工具 |
|---|---|---|
| `tsv_mapping` | CSV/TSV 列重映射 | AMRFinderPlus、featureCounts |
| `json_mapping` | 嵌套 JSON 展平 | fastp（summary before/after 块） |
| `key_value_log` | 分隔符日志解析 | STAR（Log.final.out 管道分隔） |

`tsv_mapping` 示例：

```yaml
parsers:
  my_tool:
    source:
      type: tsv_mapping
      pattern: "*.tsv"
      delimiter: "\t"
    target_table: my_standard_table
    columns:
      gene_name: {sources: [Gene, gene_name], default: ""}
      coverage:  {sources: [Coverage, cov_pct], default: "0"}
    constants:
      tool: my_tool
```

在 `parse_outputs()` 中接入：

```python
def parse_outputs(self, tool_id, output_dir, sample_id):
    if self._tsv_mapper.has_parser(tool_id):
        rows = self._tsv_mapper.parse(tool_id, output_dir, sample_id=sample_id)
        if rows:
            return {self._tsv_mapper.get_target_table(tool_id): rows}
    # 复杂解析器保留为 Python
    ...
```

## 测试插件

每个插件必须包含测试。新插件的最小测试套件覆盖三个方面：合约合规、注册表加载和计划生成。

### 最小测试文件 (`tests/test_my_plugin.py`)

```python
import pytest
from abi.testing import assert_plugin_contract
from your_package.plugin import MyPlugin  # 你的已安装插件入口点


def test_plugin_contract():
    """插件满足 ABIPlugin 协议。"""
    plugin = MyPlugin()
    assert_plugin_contract(plugin)


def test_registry_loads():
    """工具注册表 YAML 解析无错误。"""
    plugin = MyPlugin()
    registry = plugin.registry()
    tools = registry.list_tools()
    assert len(tools) > 0
    # 验证预期工具已注册
    tool_ids = [t["id"] for t in tools]
    assert "fastp" in tool_ids


def test_build_plan(mock_sample_context, tmp_path):
    """build_plan() 对默认配置返回有效的 ExecutionPlan。"""
    plugin = MyPlugin()
    config = plugin.load_config()
    config["outdir"] = str(tmp_path)
    plan = plugin.build_plan(config)
    assert len(plan.steps) > 0
    # QC 步骤始终最先
    assert plan.steps[0].step_id.startswith("qc_")


def test_parse_outputs_handles_missing_files(tmp_path):
    """解析器对缺失文件返回空结果（而非错误）。"""
    plugin = MyPlugin()
    result = plugin.parse_outputs("fastp", tmp_path, "S1")
    assert isinstance(result, dict)


@pytest.mark.smoke
@pytest.mark.requires_tools
def test_real_execution_smoke(tmp_path):
    """使用合成数据执行完整管线。"""
    # 生成最小测试数据，运行真实工具，验证输出……
    pass
```

### 插件测试可用的 Fixtures

`tests/conftest.py` 中的所有 fixtures 无需导入即可使用：

| Fixture | 类型 | 用途 |
|---------|------|------|
| `mock_sample` | `ABISample` | 带有 illumina 平台的单样本输入 |
| `mock_sample_context` | `ABISampleContext` | 包含两个分组的单样本上下文 |
| `mock_contract_dict` | `dict` | 用于脚手架的最小有效工具合约 |
| `tmp_project` | `Path` | 包含 results/logs/provenance/tables/ 的临时目录 |

### 基准测试

对于数值级验证，使用 `run_benchmark()`：

```python
from abi.testing.benchmark import run_benchmark

@pytest.mark.smoke
@pytest.mark.requires_tools
def test_my_plugin_benchmark(tmp_path):
    result = run_benchmark(
        plugin_id="my_analysis",
        dataset_path=Path("data/benchmarks/my_analysis"),
        outdir=tmp_path / "results",
    )
    assert result.total > 0
    assert result.passed >= result.total * 0.7  # 开发阶段阈值
```

参见 `docs/zh/testing.md` 获取完整测试指南。

## 资源管理

ABI 为生物信息学数据库提供资源发现和自动安装系统。插件作者声明资源需求；ABI 负责检查、下载和安装后钩子。

### 声明资源

资源在 `plugins/<name>/abi-plugin.yaml` 中声明：

```yaml
resources:
  my_database:
    name: "My Database"
    description: "MyTool 的参考数据库"
    url: "https://example.com/my_database.tar.gz"
    size_gb: 2.5
    required_by: [my_tool]
    install_post: "makeblastdb -in {resource_dir}/sequences.fasta -dbtype nucl"
    env_name: my_env
```

### ResourceSpec 字段

| 字段 | 类型 | 描述 |
|------|------|------|
| `name` | `str` | 人类可读的名称 |
| `description` | `str` | 资源提供的内容 |
| `url` | `str` | 下载 URL（支持 http/https/S3） |
| `size_gb` | `float` | 大约下载大小 |
| `required_by` | `list[str]` | 依赖此资源的工具 ID |
| `install_post` | `str` 或 `None` | 下载后运行的 shell 命令（如 `makeblastdb`） |
| `env_name` | `str` | 提供安装后工具的 conda 环境 |

### CLI 命令

```bash
# 检查哪些资源可用/缺失
abi check-resources --type my_analysis

# 下载并安装缺失的资源（需要确认）
abi setup-resources --type my_analysis --confirm
```

### 环境解析

工具 → 环境分配在 `environments.yaml` 中，而非在单个工具合约中。注册工具时，将其环境分配添加到 `environments.yaml`：

```yaml
tool_assignments:
  my_analysis:
    my_tool: my_env
```

`ToolRegistry` 在运行时注入正确的 `env_name`。运行 `scripts/emit_env_yamls.py` 重新生成每个环境的 `envs/*.yml` 文件。

## 断言表达式参考

步骤合约支持以简单表达式语言编写的断言。断言在工具执行后根据解析后的输出进行评估。

### 变量

| 变量 | 类型 | 示例 |
|------|------|------|
| `output_json.<key>` | Any | `output_json.summary.after_filtering.total_reads` |
| `output_files.<name>` | Path | `output_files.clean_read1` |
| `output_dir` | Path | `output_dir` |
| `return_code` | int | `return_code` |

### 运算符

| 运算符 | 示例 | 含义 |
|--------|------|------|
| `>` | `output_json.total > 0` | 大于 |
| `>=` | `output_json.qual >= 30` | 大于等于 |
| `<` | `output_json.errors < 10` | 小于 |
| `<=` | `return_code <= 0` | 小于等于 |
| `==` | `output_json.status == "complete"` | 等于 |
| `!=` | `return_code != 1` | 不等于 |
| `exists` | `output_files.clean_read1 exists` | 文件/目录存在 |
| `contains` | `output_json.log contains "done"` | 字符串包含 |

### 在 pipeline_dag.yaml 中编写断言

```yaml
nodes:
  qc_fastp:
    tool_id: fastp
    # ...
    assertions:
      - "output_json.summary.after_filtering.total_reads > 0"
      - "output_json.summary.after_filtering.q30_rate >= 0.8"
      - "output_files.clean_read1 exists"
      - "output_files.clean_read2 exists"
      - "return_code == 0"
```

### 断言评估

断言在输出验证之后进行评估。如果任何断言失败，步骤将被标记为失败，并抛出包含失败断言详情的 `ContractViolationError`。所有断言必须通过步骤才能成功。

## 执行安全

插件应使 `plan` 和 `dry_run` 对 Agent 安全。真实的外部工具执行只能在显式确认后通过 `run` 进行。
